From 6304cbb71e51d94e934d3ee57525d75930f37ab7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Apr 2022 16:30:03 +0200 Subject: [PATCH 0001/3516] Bumped version to 2022.5.0b0 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ffe5825bf89..941caad17ae 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 4b23f5f7988..2a512907a39 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.5.0.dev0 +version = 2022.5.0b0 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From f963270a800d110f5ebd26fdf144e0fec57594ad Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 27 Apr 2022 11:24:26 -0400 Subject: [PATCH 0002/3516] Improve ZHA startup performance (#70111) * Remove semaphores and background mains init * additional logging * correct cache usage and update tests --- .../components/zha/core/channels/__init__.py | 14 +------- .../components/zha/core/channels/base.py | 27 +++++++++++++--- .../components/zha/core/channels/general.py | 4 ++- .../zha/core/channels/homeautomation.py | 2 +- homeassistant/components/zha/core/device.py | 18 +++++++++++ homeassistant/components/zha/core/gateway.py | 32 ++++++++----------- homeassistant/components/zha/light.py | 2 +- tests/components/zha/conftest.py | 1 + tests/components/zha/test_device.py | 6 ++-- tests/components/zha/test_fan.py | 15 +++++++-- 10 files changed, 76 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 2011f92a63b..00409794473 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from collections.abc import Coroutine from typing import TYPE_CHECKING, Any, TypeVar import zigpy.endpoint @@ -50,7 +49,6 @@ class Channels: self._pools: list[ChannelPool] = [] self._power_config: base.ZigbeeChannel | None = None self._identify: base.ZigbeeChannel | None = None - self._semaphore = asyncio.Semaphore(3) self._unique_id = str(zha_device.ieee) self._zdo_channel = base.ZDOChannel(zha_device.device.endpoints[0], zha_device) self._zha_device = zha_device @@ -82,11 +80,6 @@ class Channels: if self._identify is None: self._identify = channel - @property - def semaphore(self) -> asyncio.Semaphore: - """Return semaphore for concurrent tasks.""" - return self._semaphore - @property def zdo_channel(self) -> base.ZDOChannel: """Return ZDO channel.""" @@ -336,13 +329,8 @@ class ChannelPool: async def _execute_channel_tasks(self, func_name: str, *args: Any) -> None: """Add a throttled channel task and swallow exceptions.""" - - async def _throttle(coro: Coroutine[Any, Any, None]) -> None: - async with self._channels.semaphore: - return await coro - channels = [*self.claimed_channels.values(), *self.client_channels.values()] - tasks = [_throttle(getattr(ch, func_name)(*args)) for ch in channels] + tasks = [getattr(ch, func_name)(*args) for ch in channels] results = await asyncio.gather(*tasks, return_exceptions=True) for channel, outcome in zip(channels, results): if isinstance(outcome, Exception): diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index b10209f45f4..7beefe2f0d0 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -310,11 +310,14 @@ class ZigbeeChannel(LogMixin): """Set cluster binding and attribute reporting.""" if not self._ch_pool.skip_configuration: if self.BIND: + self.debug("Performing cluster binding") await self.bind() if self.cluster.is_server: + self.debug("Configuring cluster attribute reporting") await self.configure_reporting() ch_specific_cfg = getattr(self, "async_configure_channel_specific", None) if ch_specific_cfg: + self.debug("Performing channel specific configuration") await ch_specific_cfg() self.debug("finished channel configuration") else: @@ -325,6 +328,7 @@ class ZigbeeChannel(LogMixin): async def async_initialize(self, from_cache: bool) -> None: """Initialize channel.""" if not from_cache and self._ch_pool.skip_configuration: + self.debug("Skipping channel initialization") self._status = ChannelStatus.INITIALIZED return @@ -334,12 +338,23 @@ class ZigbeeChannel(LogMixin): uncached.extend([cfg["attr"] for cfg in self.REPORT_CONFIG]) if cached: - await self._get_attributes(True, cached, from_cache=True) + self.debug("initializing cached channel attributes: %s", cached) + await self._get_attributes( + True, cached, from_cache=True, only_cache=from_cache + ) if uncached: - await self._get_attributes(True, uncached, from_cache=from_cache) + self.debug( + "initializing uncached channel attributes: %s - from cache[%s]", + uncached, + from_cache, + ) + await self._get_attributes( + True, uncached, from_cache=from_cache, only_cache=from_cache + ) ch_specific_init = getattr(self, "async_initialize_channel_specific", None) if ch_specific_init: + self.debug("Performing channel specific initialization: %s", uncached) await ch_specific_init(from_cache=from_cache) self.debug("finished channel initialization") @@ -407,7 +422,7 @@ class ZigbeeChannel(LogMixin): self._cluster, [attribute], allow_cache=from_cache, - only_cache=from_cache and not self._ch_pool.is_mains_powered, + only_cache=from_cache, manufacturer=manufacturer, ) return result.get(attribute) @@ -417,6 +432,7 @@ class ZigbeeChannel(LogMixin): raise_exceptions: bool, attributes: list[int | str], from_cache: bool = True, + only_cache: bool = True, ) -> dict[int | str, Any]: """Get the values for a list of attributes.""" manufacturer = None @@ -428,17 +444,18 @@ class ZigbeeChannel(LogMixin): result = {} while chunk: try: + self.debug("Reading attributes in chunks: %s", chunk) read, _ = await self.cluster.read_attributes( attributes, allow_cache=from_cache, - only_cache=from_cache and not self._ch_pool.is_mains_powered, + only_cache=only_cache, manufacturer=manufacturer, ) result.update(read) except (asyncio.TimeoutError, zigpy.exceptions.ZigbeeException) as ex: self.debug( "failed to get attributes '%s' on '%s' cluster: %s", - attributes, + chunk, self.cluster.ep_attribute, str(ex), ) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index f528057c313..81152bb8869 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -463,7 +463,9 @@ class PowerConfigurationChannel(ZigbeeChannel): "battery_size", "battery_quantity", ] - return self.get_attributes(attributes, from_cache=from_cache) + return self.get_attributes( + attributes, from_cache=from_cache, only_cache=from_cache + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerProfile.cluster_id) diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index 02fa835dc20..e1019ed31bf 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -97,7 +97,7 @@ class ElectricalMeasurementChannel(ZigbeeChannel): for a in self.REPORT_CONFIG if a["attr"] not in self.cluster.unsupported_attributes ] - result = await self.get_attributes(attrs, from_cache=False) + result = await self.get_attributes(attrs, from_cache=False, only_cache=False) if result: for attr, value in result.items(): self.async_send_signal( diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 41d90b48869..854d80ffb78 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -351,11 +351,15 @@ class ZHADevice(LogMixin): if self.is_coordinator: return if self.last_seen is None: + self.debug("last_seen is None, marking the device unavailable") self.update_available(False) return difference = time.time() - self.last_seen if difference < self.consider_unavailable_time: + self.debug( + "Device seen - marking the device available and resetting counter" + ) self.update_available(True) self._checkins_missed_count = 0 return @@ -365,6 +369,10 @@ class ZHADevice(LogMixin): or self.manufacturer == "LUMI" or not self._channels.pools ): + self.debug( + "last_seen is %s seconds ago and ping attempts have been exhausted, marking the device unavailable", + difference, + ) self.update_available(False) return @@ -386,13 +394,23 @@ class ZHADevice(LogMixin): def update_available(self, available: bool) -> None: """Update device availability and signal entities.""" + self.debug( + "Update device availability - device available: %s - new availability: %s - changed: %s", + self.available, + available, + self.available ^ available, + ) availability_changed = self.available ^ available self.available = available if availability_changed and available: # reinit channels then signal entities + self.debug( + "Device availability changed and device became available, reinitializing channels" + ) self.hass.async_create_task(self._async_became_available()) return if availability_changed and not available: + self.debug("Device availability changed and device became unavailable") self._channels.zha_send_event( { "device_event_type": "device_offline", diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 64f7b24ff99..1280f3defe3 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -239,29 +239,25 @@ class ZHAGateway: async def async_initialize_devices_and_entities(self) -> None: """Initialize devices and load entities.""" - semaphore = asyncio.Semaphore(2) - async def _throttle(zha_device: ZHADevice, cached: bool) -> None: - async with semaphore: - await zha_device.async_initialize(from_cache=cached) - - _LOGGER.debug("Loading battery powered devices") + _LOGGER.warning("Loading all devices") await asyncio.gather( - *( - _throttle(dev, cached=True) - for dev in self.devices.values() - if not dev.is_mains_powered - ) + *(dev.async_initialize(from_cache=True) for dev in self.devices.values()) ) - _LOGGER.debug("Loading mains powered devices") - await asyncio.gather( - *( - _throttle(dev, cached=False) - for dev in self.devices.values() - if dev.is_mains_powered + async def fetch_updated_state() -> None: + """Fetch updated state for mains powered devices.""" + _LOGGER.warning("Fetching current state for mains powered devices") + await asyncio.gather( + *( + dev.async_initialize(from_cache=False) + for dev in self.devices.values() + if dev.is_mains_powered + ) ) - ) + + # background the fetching of state for mains powered devices + asyncio.create_task(fetch_updated_state()) def device_joined(self, device: zigpy.device.Device) -> None: """Handle device joined. diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index b6d344a57e7..ef7205feb87 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -488,7 +488,7 @@ class Light(BaseLight, ZhaEntity): ] results = await self._color_channel.get_attributes( - attributes, from_cache=False + attributes, from_cache=False, only_cache=False ) if (color_mode := results.get("color_mode")) is not None: diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index cb562cd5eaa..482a11b95de 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -177,6 +177,7 @@ def zha_device_joined(hass, setup_zha): """Return a newly joined ZHA device.""" async def _zha_device(zigpy_dev): + zigpy_dev.last_seen = time.time() await setup_zha() zha_gateway = common.get_zha_gateway(hass) await zha_gateway.async_device_initialized(zigpy_dev) diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index 76877e71ffc..0f2caceada5 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -106,7 +106,7 @@ def _send_time_changed(hass, seconds): @patch( "homeassistant.components.zha.core.channels.general.BasicChannel.async_initialize", - new=mock.MagicMock(), + new=mock.AsyncMock(), ) async def test_check_available_success( hass, device_with_basic_channel, zha_device_restored @@ -160,7 +160,7 @@ async def test_check_available_success( @patch( "homeassistant.components.zha.core.channels.general.BasicChannel.async_initialize", - new=mock.MagicMock(), + new=mock.AsyncMock(), ) async def test_check_available_unsuccessful( hass, device_with_basic_channel, zha_device_restored @@ -203,7 +203,7 @@ async def test_check_available_unsuccessful( @patch( "homeassistant.components.zha.core.channels.general.BasicChannel.async_initialize", - new=mock.MagicMock(), + new=mock.AsyncMock(), ) async def test_check_available_no_basic_channel( hass, device_without_basic_channel, zha_device_restored, caplog diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index e367857b260..19f5f39a22f 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -471,7 +471,10 @@ async def test_fan_update_entity( assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 0 assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 3 - assert cluster.read_attributes.await_count == 2 + if zha_device_joined_restored.name == "zha_device_joined": + assert cluster.read_attributes.await_count == 2 + else: + assert cluster.read_attributes.await_count == 4 await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() @@ -480,7 +483,10 @@ async def test_fan_update_entity( "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True ) assert hass.states.get(entity_id).state == STATE_OFF - assert cluster.read_attributes.await_count == 3 + if zha_device_joined_restored.name == "zha_device_joined": + assert cluster.read_attributes.await_count == 3 + else: + assert cluster.read_attributes.await_count == 5 cluster.PLUGGED_ATTR_READS = {"fan_mode": 1} await hass.services.async_call( @@ -490,4 +496,7 @@ async def test_fan_update_entity( assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 33 assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 3 - assert cluster.read_attributes.await_count == 4 + if zha_device_joined_restored.name == "zha_device_joined": + assert cluster.read_attributes.await_count == 4 + else: + assert cluster.read_attributes.await_count == 6 From d00d82389dcb9de919dfaba829e2d1c157b0e3f2 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 27 Apr 2022 23:08:18 +0100 Subject: [PATCH 0003/3516] Remove invalid unique id from generic camera (#70568) Co-authored-by: J. Nick Koston --- homeassistant/components/generic/__init__.py | 22 +++++++++++- homeassistant/components/generic/camera.py | 2 +- .../components/generic/config_flow.py | 2 -- tests/components/generic/test_config_flow.py | 34 +++++++++++++++++++ 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/generic/__init__.py b/homeassistant/components/generic/__init__.py index f243f1639b3..cb669d8b906 100644 --- a/homeassistant/components/generic/__init__.py +++ b/homeassistant/components/generic/__init__.py @@ -1,8 +1,12 @@ """The generic component.""" +from __future__ import annotations + +from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er DOMAIN = "generic" PLATFORMS = [Platform.CAMERA] @@ -13,9 +17,25 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> Non await hass.config_entries.async_reload(entry.entry_id) +async def _async_migrate_unique_ids(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Migrate entities to the new unique id.""" + + @callback + def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, Any] | None: + if entity_entry.unique_id == entry.entry_id: + # Already correct, nothing to do + return None + # There is only one entity, and its unique id + # should always be the same as the config entry entry_id + return {"new_unique_id": entry.entry_id} + + await er.async_migrate_entries(hass, entry.entry_id, _async_migrator) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up generic IP camera from a config entry.""" + await _async_migrate_unique_ids(hass, entry) hass.config_entries.async_setup_platforms(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index e83cb0df0aa..d20032e2607 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -109,7 +109,7 @@ async def async_setup_entry( """Set up a generic IP Camera.""" async_add_entities( - [GenericCamera(hass, entry.options, entry.unique_id, entry.title)] + [GenericCamera(hass, entry.options, entry.entry_id, entry.title)] ) diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 3d15069bca8..4298b473681 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -274,7 +274,6 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): # is always jpeg user_input[CONF_CONTENT_TYPE] = "image/jpeg" - await self.async_set_unique_id(self.flow_id) return self.async_create_entry( title=name, data={}, options=user_input ) @@ -302,7 +301,6 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): import_config[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False still_format = import_config.get(CONF_CONTENT_TYPE, "image/jpeg") import_config[CONF_CONTENT_TYPE] = still_format - await self.async_set_unique_id(self.flow_id) return self.async_create_entry(title=name, data={}, options=import_config) diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 3caa1aa7adf..f5411ed3ea0 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -28,6 +28,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, HTTP_BASIC_AUTHENTICATION, ) +from homeassistant.helpers import entity_registry from tests.common import MockConfigEntry @@ -577,3 +578,36 @@ async def test_reload_on_title_change(hass) -> None: await hass.async_block_till_done() assert hass.states.get("camera.my_title").attributes["friendly_name"] == "New Title" + + +async def test_migrate_existing_ids(hass) -> None: + """Test that existing ids are migrated for issue #70568.""" + + registry = entity_registry.async_get(hass) + + test_data = TESTDATA_OPTIONS.copy() + test_data[CONF_CONTENT_TYPE] = "image/png" + old_unique_id = "54321" + entity_id = "camera.sample_camera" + + mock_entry = MockConfigEntry( + domain=DOMAIN, unique_id=old_unique_id, options=test_data, title="My Title" + ) + new_unique_id = mock_entry.entry_id + mock_entry.add_to_hass(hass) + + entity_entry = registry.async_get_or_create( + "camera", + DOMAIN, + old_unique_id, + suggested_object_id="sample camera", + config_entry=mock_entry, + ) + assert entity_entry.entity_id == entity_id + assert entity_entry.unique_id == old_unique_id + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + entity_entry = registry.async_get(entity_id) + assert entity_entry.unique_id == new_unique_id From 627bd0d58a9bca4db5286359e449a5d4ab4a3460 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 27 Apr 2022 17:05:00 +0200 Subject: [PATCH 0004/3516] Handle removed entites in collection.sync_entity_lifecycle (#70759) * Handle removed entites in collection.sync_entity_lifecycle * Add comment --- homeassistant/helpers/collection.py | 15 ++- homeassistant/helpers/entity.py | 18 ++- homeassistant/helpers/entity_platform.py | 2 +- tests/helpers/test_collection.py | 142 ++++++++++++++++++++++- 4 files changed, 168 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 9017c60c23f..7a73f90539c 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -334,7 +334,13 @@ def sync_entity_lifecycle( ent_reg = entity_registry.async_get(hass) async def _add_entity(change_set: CollectionChangeSet) -> Entity: + def entity_removed() -> None: + """Remove entity from entities if it's removed or not added.""" + if change_set.item_id in entities: + entities.pop(change_set.item_id) + entities[change_set.item_id] = create_entity(change_set.item) + entities[change_set.item_id].async_on_remove(entity_removed) return entities[change_set.item_id] async def _remove_entity(change_set: CollectionChangeSet) -> None: @@ -343,11 +349,16 @@ def sync_entity_lifecycle( ) if ent_to_remove is not None: ent_reg.async_remove(ent_to_remove) - else: + elif change_set.item_id in entities: await entities[change_set.item_id].async_remove(force_remove=True) - entities.pop(change_set.item_id) + # Unconditionally pop the entity from the entity list to avoid racing against + # the entity registry event handled by Entity._async_registry_updated + if change_set.item_id in entities: + entities.pop(change_set.item_id) async def _update_entity(change_set: CollectionChangeSet) -> None: + if change_set.item_id not in entities: + return await entities[change_set.item_id].async_update_config(change_set.item) # type: ignore[attr-defined] _func_map: dict[ diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index f94b4257d30..791a80f7731 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -759,7 +759,7 @@ class Entity(ABC): @callback def async_on_remove(self, func: CALLBACK_TYPE) -> None: - """Add a function to call when entity removed.""" + """Add a function to call when entity is removed or not added.""" if self._on_remove is None: self._on_remove = [] self._on_remove.append(func) @@ -788,13 +788,23 @@ class Entity(ABC): self.parallel_updates = parallel_updates self._platform_state = EntityPlatformState.ADDED + def _call_on_remove_callbacks(self) -> None: + """Call callbacks registered by async_on_remove.""" + if self._on_remove is None: + return + while self._on_remove: + self._on_remove.pop()() + @callback def add_to_platform_abort(self) -> None: """Abort adding an entity to a platform.""" + + self._platform_state = EntityPlatformState.NOT_ADDED + self._call_on_remove_callbacks() + self.hass = None # type: ignore[assignment] self.platform = None self.parallel_updates = None - self._platform_state = EntityPlatformState.NOT_ADDED async def add_to_platform_finish(self) -> None: """Finish adding an entity to a platform.""" @@ -819,9 +829,7 @@ class Entity(ABC): self._platform_state = EntityPlatformState.REMOVED - if self._on_remove is not None: - while self._on_remove: - self._on_remove.pop()() + self._call_on_remove_callbacks() await self.async_internal_will_remove_from_hass() await self.async_will_remove_from_hass() diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 65cfe706f14..6972dbf7c16 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -611,7 +611,7 @@ class EntityPlatform: self.hass.states.async_reserve(entity.entity_id) def remove_entity_cb() -> None: - """Remove entity from entities list.""" + """Remove entity from entities dict.""" self.entities.pop(entity_id) entity.async_on_remove(remove_entity_cb) diff --git a/tests/helpers/test_collection.py b/tests/helpers/test_collection.py index 11ab0f46ce4..cd4bbba1a3e 100644 --- a/tests/helpers/test_collection.py +++ b/tests/helpers/test_collection.py @@ -4,7 +4,13 @@ import logging import pytest import voluptuous as vol -from homeassistant.helpers import collection, entity, entity_component, storage +from homeassistant.helpers import ( + collection, + entity, + entity_component, + entity_registry as er, + storage, +) from tests.common import flush_store @@ -261,6 +267,140 @@ async def test_attach_entity_component_collection(hass): assert hass.states.get("test.mock_1") is None +async def test_entity_component_collection_abort(hass): + """Test aborted entity adding is handled.""" + ent_comp = entity_component.EntityComponent(_LOGGER, "test", hass) + coll = collection.ObservableCollection(_LOGGER) + + async_update_config_calls = [] + async_remove_calls = [] + + class MockMockEntity(MockEntity): + """Track calls to async_update_config and async_remove.""" + + async def async_update_config(self, config): + nonlocal async_update_config_calls + async_update_config_calls.append(None) + await super().async_update_config() + + async def async_remove(self, *, force_remove: bool = False): + nonlocal async_remove_calls + async_remove_calls.append(None) + await super().async_remove() + + collection.sync_entity_lifecycle( + hass, "test", "test", ent_comp, coll, MockMockEntity + ) + entity_registry = er.async_get(hass) + entity_registry.async_get_or_create( + "test", + "test", + "mock_id", + suggested_object_id="mock_1", + disabled_by=er.RegistryEntryDisabler.INTEGRATION, + ) + + await coll.notify_changes( + [ + collection.CollectionChangeSet( + collection.CHANGE_ADDED, + "mock_id", + {"id": "mock_id", "state": "initial", "name": "Mock 1"}, + ) + ], + ) + + assert hass.states.get("test.mock_1") is None + + await coll.notify_changes( + [ + collection.CollectionChangeSet( + collection.CHANGE_UPDATED, + "mock_id", + {"id": "mock_id", "state": "second", "name": "Mock 1 updated"}, + ) + ], + ) + + assert hass.states.get("test.mock_1") is None + assert len(async_update_config_calls) == 0 + + await coll.notify_changes( + [collection.CollectionChangeSet(collection.CHANGE_REMOVED, "mock_id", None)], + ) + + assert hass.states.get("test.mock_1") is None + assert len(async_remove_calls) == 0 + + +async def test_entity_component_collection_entity_removed(hass): + """Test entity removal is handled.""" + ent_comp = entity_component.EntityComponent(_LOGGER, "test", hass) + coll = collection.ObservableCollection(_LOGGER) + + async_update_config_calls = [] + async_remove_calls = [] + + class MockMockEntity(MockEntity): + """Track calls to async_update_config and async_remove.""" + + async def async_update_config(self, config): + nonlocal async_update_config_calls + async_update_config_calls.append(None) + await super().async_update_config() + + async def async_remove(self, *, force_remove: bool = False): + nonlocal async_remove_calls + async_remove_calls.append(None) + await super().async_remove() + + collection.sync_entity_lifecycle( + hass, "test", "test", ent_comp, coll, MockMockEntity + ) + entity_registry = er.async_get(hass) + entity_registry.async_get_or_create( + "test", "test", "mock_id", suggested_object_id="mock_1" + ) + + await coll.notify_changes( + [ + collection.CollectionChangeSet( + collection.CHANGE_ADDED, + "mock_id", + {"id": "mock_id", "state": "initial", "name": "Mock 1"}, + ) + ], + ) + + assert hass.states.get("test.mock_1").name == "Mock 1" + assert hass.states.get("test.mock_1").state == "initial" + + entity_registry.async_remove("test.mock_1") + await hass.async_block_till_done() + assert hass.states.get("test.mock_1") is None + assert len(async_remove_calls) == 1 + + await coll.notify_changes( + [ + collection.CollectionChangeSet( + collection.CHANGE_UPDATED, + "mock_id", + {"id": "mock_id", "state": "second", "name": "Mock 1 updated"}, + ) + ], + ) + + assert hass.states.get("test.mock_1") is None + assert len(async_update_config_calls) == 0 + + await coll.notify_changes( + [collection.CollectionChangeSet(collection.CHANGE_REMOVED, "mock_id", None)], + ) + + assert hass.states.get("test.mock_1") is None + assert len(async_remove_calls) == 1 + + async def test_storage_collection_websocket(hass, hass_ws_client): """Test exposing a storage collection via websockets.""" store = storage.Store(hass, 1, "test-data") From 5a618e7f586149cfdc698adf078e002e1c9fe7e2 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Wed, 27 Apr 2022 19:05:42 +0200 Subject: [PATCH 0005/3516] Bump hatasmota to 0.4.1 (#70799) Co-authored-by: Erik Montnemery --- .../components/tasmota/manifest.json | 2 +- homeassistant/components/tasmota/sensor.py | 23 ++++++++++++++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_sensor.py | 4 ++-- 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 2f3a1b66fea..6a743683d94 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.4.0"], + "requirements": ["hatasmota==0.4.1"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/homeassistant/components/tasmota/sensor.py b/homeassistant/components/tasmota/sensor.py index feb15cd5639..34526904f17 100644 --- a/homeassistant/components/tasmota/sensor.py +++ b/homeassistant/components/tasmota/sensor.py @@ -54,11 +54,24 @@ ICON = "icon" # A Tasmota sensor type may be mapped to either a device class or an icon, not both SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { + hc.SENSOR_ACTIVE_ENERGYEXPORT: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ACTIVE_ENERGYIMPORT: { + DEVICE_CLASS: SensorDeviceClass.ENERGY, + STATE_CLASS: SensorStateClass.TOTAL, + }, + hc.SENSOR_ACTIVE_POWERUSAGE: { + DEVICE_CLASS: SensorDeviceClass.POWER, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, hc.SENSOR_AMBIENT: { DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE, STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_APPARENT_POWERUSAGE: { + ICON: "mdi:flash", STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_BATTERY: { @@ -80,6 +93,11 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { ICON: "mdi:alpha-a-circle-outline", STATE_CLASS: SensorStateClass.MEASUREMENT, }, + hc.SENSOR_CURRENTNEUTRAL: { + ICON: "mdi:alpha-a-circle-outline", + DEVICE_CLASS: SensorDeviceClass.CURRENT, + STATE_CLASS: SensorStateClass.MEASUREMENT, + }, hc.SENSOR_DEWPOINT: { DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, ICON: "mdi:weather-rainy", @@ -141,7 +159,10 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_PROXIMITY: {ICON: "mdi:ruler"}, + hc.SENSOR_REACTIVE_ENERGYEXPORT: {STATE_CLASS: SensorStateClass.TOTAL}, + hc.SENSOR_REACTIVE_ENERGYIMPORT: {STATE_CLASS: SensorStateClass.TOTAL}, hc.SENSOR_REACTIVE_POWERUSAGE: { + ICON: "mdi:flash", STATE_CLASS: SensorStateClass.MEASUREMENT, }, hc.SENSOR_STATUS_LAST_RESTART_TIME: {DEVICE_CLASS: SensorDeviceClass.TIMESTAMP}, @@ -162,7 +183,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = { hc.SENSOR_TODAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY}, hc.SENSOR_TOTAL: { DEVICE_CLASS: SensorDeviceClass.ENERGY, - STATE_CLASS: SensorStateClass.TOTAL_INCREASING, + STATE_CLASS: SensorStateClass.TOTAL, }, hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"}, hc.SENSOR_TVOC: {ICON: "mdi:air-filter"}, diff --git a/requirements_all.txt b/requirements_all.txt index daa9ae78610..ca5a28b5cb2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -792,7 +792,7 @@ hass-nabucasa==0.54.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.4.0 +hatasmota==0.4.1 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9975c08b5df..326eb00e596 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -562,7 +562,7 @@ hangups==0.4.18 hass-nabucasa==0.54.0 # homeassistant.components.tasmota -hatasmota==0.4.0 +hatasmota==0.4.1 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 8e810c82a43..b27a9b8ca49 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -284,7 +284,7 @@ async def test_indexed_sensor_state_via_mqtt2(hass, mqtt_mock, setup_tasmota): state = hass.states.get("sensor.tasmota_energy_total") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) - assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL_INCREASING + assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() @@ -333,7 +333,7 @@ async def test_indexed_sensor_state_via_mqtt3(hass, mqtt_mock, setup_tasmota): state = hass.states.get("sensor.tasmota_energy_total_1") assert state.state == "unavailable" assert not state.attributes.get(ATTR_ASSUMED_STATE) - assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL_INCREASING + assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") await hass.async_block_till_done() From 4f9ff2a252fa2d0aa4bcafa4352702d70684c9e2 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 27 Apr 2022 13:55:31 -0400 Subject: [PATCH 0006/3516] Bump ZHA dependencies (#70900) * Bump ZHA libs * bump Zigpy --- homeassistant/components/zha/manifest.json | 6 +++--- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 3704e9715fb..8befa039382 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,9 +7,9 @@ "bellows==0.29.0", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.72", - "zigpy-deconz==0.14.0", - "zigpy==0.44.2", + "zha-quirks==0.0.73", + "zigpy-deconz==0.16.0", + "zigpy==0.45.1", "zigpy-xbee==0.14.0", "zigpy-zigate==0.7.4", "zigpy-znp==0.7.0" diff --git a/requirements_all.txt b/requirements_all.txt index ca5a28b5cb2..c76ee52cff7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2480,7 +2480,7 @@ zengge==0.2 zeroconf==0.38.4 # homeassistant.components.zha -zha-quirks==0.0.72 +zha-quirks==0.0.73 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2489,7 +2489,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.14.0 +zigpy-deconz==0.16.0 # homeassistant.components.zha zigpy-xbee==0.14.0 @@ -2501,7 +2501,7 @@ zigpy-zigate==0.7.4 zigpy-znp==0.7.0 # homeassistant.components.zha -zigpy==0.44.2 +zigpy==0.45.1 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 326eb00e596..bc1eb01a3d2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1620,10 +1620,10 @@ youless-api==0.16 zeroconf==0.38.4 # homeassistant.components.zha -zha-quirks==0.0.72 +zha-quirks==0.0.73 # homeassistant.components.zha -zigpy-deconz==0.14.0 +zigpy-deconz==0.16.0 # homeassistant.components.zha zigpy-xbee==0.14.0 @@ -1635,7 +1635,7 @@ zigpy-zigate==0.7.4 zigpy-znp==0.7.0 # homeassistant.components.zha -zigpy==0.44.2 +zigpy==0.45.1 # homeassistant.components.zwave_js zwave-js-server-python==0.36.0 From a5ac263276a604bf73699ec0d6f4e0a9b58ae9d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Apr 2022 05:54:47 -1000 Subject: [PATCH 0007/3516] Add additional OUI for tplink light devices (#70922) --- homeassistant/components/tplink/manifest.json | 4 ++++ homeassistant/generated/dhcp.py | 1 + 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 383031d3417..9a419302a18 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -74,6 +74,10 @@ "hostname": "k[lp]*", "macaddress": "1027F5*" }, + { + "hostname": "k[lp]*", + "macaddress": "B0A7B9*" + }, { "hostname": "k[lp]*", "macaddress": "403F8C*" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 3ae5b0a1376..d0ffb9b097a 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -137,6 +137,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'tplink', 'hostname': 'k[lp]*', 'macaddress': '60A4B7*'}, {'domain': 'tplink', 'hostname': 'k[lp]*', 'macaddress': '005F67*'}, {'domain': 'tplink', 'hostname': 'k[lp]*', 'macaddress': '1027F5*'}, + {'domain': 'tplink', 'hostname': 'k[lp]*', 'macaddress': 'B0A7B9*'}, {'domain': 'tplink', 'hostname': 'k[lp]*', 'macaddress': '403F8C*'}, {'domain': 'tplink', 'hostname': 'k[lp]*', 'macaddress': 'C0C9E3*'}, {'domain': 'tplink', 'hostname': 'k[lp]*', 'macaddress': '909A4A*'}, From 32d7f04a6553ef034c0375527e4dfc7e8f24762a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Apr 2022 18:18:32 -1000 Subject: [PATCH 0008/3516] Add discovery support for polisy to isy994 (#70940) --- .../components/isy994/config_flow.py | 5 +- homeassistant/components/isy994/manifest.json | 3 +- homeassistant/generated/dhcp.py | 1 + tests/components/isy994/test_config_flow.py | 55 +++++++++++++++++++ 4 files changed, 62 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 866ec800402..dea4bce4eeb 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -203,7 +203,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> data_entry_flow.FlowResult: """Handle a discovered isy994 via dhcp.""" friendly_name = discovery_info.hostname - url = f"http://{discovery_info.ip}" + if friendly_name.startswith("polisy"): + url = f"http://{discovery_info.ip}:8080" + else: + url = f"http://{discovery_info.ip}" mac = discovery_info.macaddress isy_mac = ( f"{mac[0:2]}:{mac[2:4]}:{mac[4:6]}:{mac[6:8]}:{mac[8:10]}:{mac[10:12]}" diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index d92226a4277..d131a150fb3 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -13,7 +13,8 @@ ], "dhcp": [ { "registered_devices": true }, - { "hostname": "isy*", "macaddress": "0021B9*" } + { "hostname": "isy*", "macaddress": "0021B9*" }, + { "hostname": "polisy*", "macaddress": "000DB9*" } ], "iot_class": "local_push", "loggers": ["pyisy"] diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index d0ffb9b097a..4ebe47ded17 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -54,6 +54,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'intellifire', 'hostname': 'zentrios-*'}, {'domain': 'isy994', 'registered_devices': True}, {'domain': 'isy994', 'hostname': 'isy*', 'macaddress': '0021B9*'}, + {'domain': 'isy994', 'hostname': 'polisy*', 'macaddress': '000DB9*'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '48A2E6*'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': 'B82CA0*'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '00D02D*'}, diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index b16f5c0070d..60e9fd964b6 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -44,6 +44,12 @@ MOCK_USER_INPUT = { CONF_PASSWORD: MOCK_PASSWORD, CONF_TLS_VER: MOCK_TLS_VERSION, } +MOCK_POLISY_USER_INPUT = { + CONF_HOST: f"http://{MOCK_HOSTNAME}:8080", + CONF_USERNAME: MOCK_USERNAME, + CONF_PASSWORD: MOCK_PASSWORD, + CONF_TLS_VER: MOCK_TLS_VERSION, +} MOCK_IMPORT_WITH_SSL = { CONF_HOST: f"https://{MOCK_HOSTNAME}", CONF_USERNAME: MOCK_USERNAME, @@ -69,6 +75,7 @@ MOCK_IMPORT_FULL_CONFIG = { MOCK_DEVICE_NAME = "Name of the device" MOCK_UUID = "ce:fb:72:31:b7:b9" MOCK_MAC = "cefb7231b7b9" +MOCK_POLISY_MAC = "000db9123456" MOCK_CONFIG_RESPONSE = """ @@ -95,6 +102,14 @@ PATCH_ASYNC_SETUP = f"{INTEGRATION}.async_setup" PATCH_ASYNC_SETUP_ENTRY = f"{INTEGRATION}.async_setup_entry" +def _get_schema_default(schema, key_name): + """Iterate schema to find a key.""" + for schema_key in schema: + if schema_key == key_name: + return schema_key.default() + raise KeyError(f"{key_name} not found in schema") + + async def test_form(hass: HomeAssistant): """Test we get the form.""" @@ -544,6 +559,46 @@ async def test_form_dhcp(hass: HomeAssistant): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_dhcp_with_polisy(hass: HomeAssistant): + """Test we can setup from dhcp with polisy.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.2.3.4", + hostname="polisy", + macaddress=MOCK_POLISY_MAC, + ), + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + assert ( + _get_schema_default(result["data_schema"].schema, CONF_HOST) + == "http://1.2.3.4:8080" + ) + + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( + PATCH_ASYNC_SETUP, return_value=True + ) as mock_setup, patch( + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_POLISY_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" + assert result2["result"].unique_id == MOCK_UUID + assert result2["data"] == MOCK_POLISY_USER_INPUT + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_form_dhcp_existing_entry(hass: HomeAssistant): """Test we update the ip of an existing entry from dhcp.""" From f3a18bc8f2af640d8c865113afed0e26a5768c53 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Apr 2022 18:19:36 -1000 Subject: [PATCH 0009/3516] Adjust get_latest_short_term_statistics query to be postgresql compatible (#70953) --- homeassistant/components/recorder/statistics.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 0056a81fb60..1c993b32bb6 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -1136,16 +1136,20 @@ def get_latest_short_term_statistics( ] most_recent_statistic_row = ( session.query( - StatisticsShortTerm.id, - func.max(StatisticsShortTerm.start), + StatisticsShortTerm.metadata_id, + func.max(StatisticsShortTerm.start).label("start_max"), ) + .filter(StatisticsShortTerm.metadata_id.in_(metadata_ids)) .group_by(StatisticsShortTerm.metadata_id) - .having(StatisticsShortTerm.metadata_id.in_(metadata_ids)) ).subquery() stats = execute( session.query(*QUERY_STATISTICS_SHORT_TERM).join( most_recent_statistic_row, - StatisticsShortTerm.id == most_recent_statistic_row.c.id, + ( + StatisticsShortTerm.metadata_id # pylint: disable=comparison-with-callable + == most_recent_statistic_row.c.metadata_id + ) + & (StatisticsShortTerm.start == most_recent_statistic_row.c.start_max), ) ) if not stats: From e41490f8bab0578c033b2f53fa2e5d833663b412 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 27 Apr 2022 20:52:32 -0400 Subject: [PATCH 0010/3516] Fix flaky ZHA tests (#70956) --- tests/components/zha/test_gateway.py | 11 ++++++----- tests/components/zha/test_select.py | 6 +++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index a263a6d7ed3..67ce6481542 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,5 +1,6 @@ """Test ZHA Gateway.""" import asyncio +import math import time from unittest.mock import patch @@ -207,14 +208,14 @@ async def test_updating_device_store(hass, zigpy_dev_basic, zha_dev_basic): assert zha_dev_basic.last_seen is not None entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) - assert entry.last_seen == zha_dev_basic.last_seen + assert math.isclose(entry.last_seen, zha_dev_basic.last_seen, rel_tol=1e-06) assert zha_dev_basic.last_seen is not None last_seen = zha_dev_basic.last_seen # test that we can't set None as last seen any more zha_dev_basic.async_update_last_seen(None) - assert last_seen == zha_dev_basic.last_seen + assert math.isclose(last_seen, zha_dev_basic.last_seen, rel_tol=1e-06) # test that we won't put None in storage zigpy_dev_basic.last_seen = None @@ -222,18 +223,18 @@ async def test_updating_device_store(hass, zigpy_dev_basic, zha_dev_basic): await zha_gateway.async_update_device_storage() await hass.async_block_till_done() entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) - assert entry.last_seen == last_seen + assert math.isclose(entry.last_seen, last_seen, rel_tol=1e-06) # test that we can still set a good last_seen last_seen = time.time() zha_dev_basic.async_update_last_seen(last_seen) - assert last_seen == zha_dev_basic.last_seen + assert math.isclose(last_seen, zha_dev_basic.last_seen, rel_tol=1e-06) # test that we still put good values in storage await zha_gateway.async_update_device_storage() await hass.async_block_till_done() entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) - assert entry.last_seen == last_seen + assert math.isclose(entry.last_seen, last_seen, rel_tol=1e-06) async def test_cleaning_up_storage(hass, zigpy_dev_basic, zha_dev_basic, hass_storage): diff --git a/tests/components/zha/test_select.py b/tests/components/zha/test_select.py index a761b8ea36b..452939420bf 100644 --- a/tests/components/zha/test_select.py +++ b/tests/components/zha/test_select.py @@ -193,7 +193,11 @@ async def test_on_off_select(hass, light, zha_device_joined_restored): state = hass.states.get(entity_id) assert state - assert state.state == STATE_UNKNOWN + if zha_device_joined_restored.name == "zha_device_joined": + assert state.state == general.OnOff.StartUpOnOff.On.name + else: + assert state.state == STATE_UNKNOWN + assert state.attributes["options"] == ["Off", "On", "Toggle", "PreviousValue"] entity_entry = entity_registry.async_get(entity_id) From 6a131ca7deef880f6adb73b8972a8a36506edda4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Apr 2022 18:18:59 -1000 Subject: [PATCH 0011/3516] Add dhcp hostname of older ZJ series Magic Home bulbs to discovery (#70958) --- homeassistant/components/flux_led/manifest.json | 3 +++ homeassistant/generated/dhcp.py | 1 + 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 1dac7b81d89..e28b55869c7 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -38,6 +38,9 @@ "macaddress": "8CCE4E*", "hostname": "lwip*" }, + { + "hostname": "hf-lpb100-zj*" + }, { "hostname": "zengge_[0-9a-f][0-9a-f]_*" }, diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 4ebe47ded17..015d70e2939 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -36,6 +36,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'flux_led', 'hostname': '[ba][lk]*', 'macaddress': 'B4E842*'}, {'domain': 'flux_led', 'hostname': '[hba][flk]*', 'macaddress': 'F0FE6B*'}, {'domain': 'flux_led', 'hostname': 'lwip*', 'macaddress': '8CCE4E*'}, + {'domain': 'flux_led', 'hostname': 'hf-lpb100-zj*'}, {'domain': 'flux_led', 'hostname': 'zengge_[0-9a-f][0-9a-f]_*'}, {'domain': 'flux_led', 'hostname': 'sta*', 'macaddress': 'C82E47*'}, {'domain': 'fronius', 'macaddress': '0003AC*'}, From 8a33eb4418931f89b9411f1f9ec4293d97e2791c Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 27 Apr 2022 21:17:25 -0700 Subject: [PATCH 0012/3516] Bump gcal_sync 0.6.3 to fix calendar path encoding bug (#70959) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 8af1fa00437..0e9a2fe7ddb 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["auth"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", - "requirements": ["gcal-sync==0.6.2", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==0.6.3", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index c76ee52cff7..df74fa12364 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -689,7 +689,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==0.6.2 +gcal-sync==0.6.3 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc1eb01a3d2..ec628688ad0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -486,7 +486,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==0.6.2 +gcal-sync==0.6.3 # homeassistant.components.usgs_earthquakes_feed geojson_client==0.6 From a1f47b203636f824d3cb0c2d3216fdb6fde203b2 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 27 Apr 2022 21:16:50 -0700 Subject: [PATCH 0013/3516] Set nest climate min/max temp range (#70960) --- homeassistant/components/nest/climate_sdm.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 26fb88482ee..89048b9f624 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -71,6 +71,8 @@ FAN_MODE_MAP = { FAN_INV_MODE_MAP = {v: k for k, v in FAN_MODE_MAP.items()} MAX_FAN_DURATION = 43200 # 15 hours is the max in the SDM API +MIN_TEMP = 10 +MAX_TEMP = 32 async def async_setup_sdm_entry( @@ -94,6 +96,9 @@ async def async_setup_sdm_entry( class ThermostatEntity(ClimateEntity): """A nest thermostat climate entity.""" + _attr_min_temp = MIN_TEMP + _attr_max_temp = MAX_TEMP + def __init__(self, device: Device) -> None: """Initialize ThermostatEntity.""" self._device = device From dd739e95d00e1e57fad3028628147415d9edb011 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Apr 2022 21:59:41 -0700 Subject: [PATCH 0014/3516] Bumped version to 2022.5.0b1 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 941caad17ae..17b38e7a566 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 2a512907a39..4775b35a195 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.5.0b0 +version = 2022.5.0b1 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 6d917973668e7a9c4164dc85b88129c2b2d4886e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Apr 2022 17:34:40 +0200 Subject: [PATCH 0015/3516] Manually update translations for 2022.5 (#71005) --- .../accuweather/translations/ca.json | 3 + .../accuweather/translations/de.json | 3 + .../accuweather/translations/el.json | 3 + .../accuweather/translations/en.json | 3 + .../accuweather/translations/et.json | 3 + .../accuweather/translations/fr.json | 5 +- .../accuweather/translations/hu.json | 5 +- .../accuweather/translations/id.json | 3 + .../accuweather/translations/it.json | 3 + .../accuweather/translations/ja.json | 3 + .../accuweather/translations/nl.json | 3 + .../accuweather/translations/no.json | 3 + .../accuweather/translations/pl.json | 3 + .../accuweather/translations/pt-BR.json | 3 + .../accuweather/translations/ru.json | 3 + .../accuweather/translations/tr.json | 3 + .../accuweather/translations/zh-Hant.json | 3 + .../components/adax/translations/hu.json | 4 +- .../components/adax/translations/nl.json | 2 +- .../components/aemet/translations/ca.json | 2 +- .../components/aemet/translations/de.json | 2 +- .../components/aemet/translations/en.json | 2 +- .../components/aemet/translations/et.json | 2 +- .../components/aemet/translations/fr.json | 2 +- .../components/aemet/translations/hu.json | 2 +- .../components/aemet/translations/id.json | 2 +- .../components/aemet/translations/it.json | 2 +- .../components/aemet/translations/nl.json | 2 +- .../components/aemet/translations/no.json | 2 +- .../components/aemet/translations/pl.json | 2 +- .../components/aemet/translations/pt-BR.json | 2 +- .../components/aemet/translations/ru.json | 2 +- .../components/aemet/translations/tr.json | 2 +- .../aemet/translations/zh-Hant.json | 2 +- .../components/agent_dvr/translations/hu.json | 2 +- .../components/airly/translations/ca.json | 2 +- .../components/airly/translations/de.json | 2 +- .../components/airly/translations/en.json | 2 +- .../components/airly/translations/et.json | 2 +- .../components/airly/translations/fr.json | 2 +- .../components/airly/translations/hu.json | 4 +- .../components/airly/translations/id.json | 2 +- .../components/airly/translations/it.json | 2 +- .../components/airly/translations/nl.json | 2 +- .../components/airly/translations/no.json | 2 +- .../components/airly/translations/pl.json | 2 +- .../components/airly/translations/pt-BR.json | 2 +- .../components/airly/translations/ru.json | 2 +- .../components/airly/translations/tr.json | 2 +- .../airly/translations/zh-Hant.json | 2 +- .../components/airnow/translations/ca.json | 2 +- .../components/airnow/translations/de.json | 2 +- .../components/airnow/translations/en.json | 2 +- .../components/airnow/translations/et.json | 2 +- .../components/airnow/translations/fr.json | 2 +- .../components/airnow/translations/hu.json | 2 +- .../components/airnow/translations/id.json | 2 +- .../components/airnow/translations/it.json | 2 +- .../components/airnow/translations/nl.json | 2 +- .../components/airnow/translations/no.json | 2 +- .../components/airnow/translations/pl.json | 2 +- .../components/airnow/translations/pt-BR.json | 2 +- .../components/airnow/translations/ru.json | 2 +- .../components/airnow/translations/tr.json | 2 +- .../airnow/translations/zh-Hant.json | 2 +- .../airvisual/translations/sensor.fr.json | 2 +- .../components/airzone/translations/ca.json | 3 +- .../components/airzone/translations/cs.json | 18 ++ .../components/airzone/translations/de.json | 3 +- .../components/airzone/translations/el.json | 3 +- .../components/airzone/translations/et.json | 3 +- .../components/airzone/translations/fr.json | 3 +- .../components/airzone/translations/hu.json | 3 +- .../components/airzone/translations/id.json | 3 +- .../components/airzone/translations/it.json | 3 +- .../components/airzone/translations/ja.json | 3 +- .../components/airzone/translations/nl.json | 3 +- .../components/airzone/translations/no.json | 3 +- .../components/airzone/translations/pl.json | 3 +- .../airzone/translations/pt-BR.json | 3 +- .../components/airzone/translations/ru.json | 3 +- .../components/airzone/translations/tr.json | 20 ++ .../airzone/translations/zh-Hant.json | 3 +- .../components/almond/translations/hu.json | 4 +- .../components/almond/translations/it.json | 2 +- .../components/ambee/translations/hu.json | 2 +- .../ambiclimate/translations/hu.json | 4 +- .../ambiclimate/translations/it.json | 4 +- .../ambient_station/translations/hu.json | 2 +- .../components/androidtv/translations/hu.json | 10 +- .../components/apple_tv/translations/bg.json | 1 + .../components/apple_tv/translations/ca.json | 1 + .../components/apple_tv/translations/de.json | 1 + .../components/apple_tv/translations/el.json | 1 + .../components/apple_tv/translations/en.json | 6 +- .../components/apple_tv/translations/et.json | 1 + .../components/apple_tv/translations/fr.json | 1 + .../components/apple_tv/translations/he.json | 1 + .../components/apple_tv/translations/hu.json | 13 +- .../components/apple_tv/translations/id.json | 1 + .../components/apple_tv/translations/it.json | 1 + .../components/apple_tv/translations/ja.json | 1 + .../components/apple_tv/translations/nl.json | 1 + .../components/apple_tv/translations/no.json | 1 + .../components/apple_tv/translations/pl.json | 1 + .../apple_tv/translations/pt-BR.json | 1 + .../components/apple_tv/translations/ru.json | 1 + .../components/apple_tv/translations/tr.json | 1 + .../apple_tv/translations/zh-Hant.json | 1 + .../components/arcam_fmj/translations/hu.json | 2 +- .../components/arcam_fmj/translations/it.json | 4 +- .../components/asuswrt/translations/ca.json | 2 +- .../components/asuswrt/translations/de.json | 2 +- .../components/asuswrt/translations/et.json | 2 +- .../components/asuswrt/translations/fr.json | 2 +- .../components/asuswrt/translations/he.json | 2 +- .../components/asuswrt/translations/hu.json | 4 +- .../components/asuswrt/translations/id.json | 2 +- .../components/asuswrt/translations/it.json | 4 +- .../components/asuswrt/translations/nl.json | 2 +- .../components/asuswrt/translations/no.json | 2 +- .../components/asuswrt/translations/pl.json | 2 +- .../asuswrt/translations/pt-BR.json | 2 +- .../components/asuswrt/translations/ru.json | 2 +- .../components/asuswrt/translations/tr.json | 2 +- .../asuswrt/translations/zh-Hant.json | 2 +- .../components/aurora/translations/hu.json | 4 +- .../components/auth/translations/fr.json | 4 +- .../components/axis/translations/hu.json | 2 +- .../azure_devops/translations/it.json | 4 +- .../azure_event_hub/translations/de.json | 2 +- .../binary_sensor/translations/cs.json | 4 + .../binary_sensor/translations/he.json | 8 +- .../binary_sensor/translations/pt-BR.json | 2 +- .../binary_sensor/translations/zh-Hant.json | 2 +- .../components/blebox/translations/it.json | 2 +- .../components/braviatv/translations/ca.json | 2 +- .../components/braviatv/translations/de.json | 2 +- .../components/braviatv/translations/en.json | 2 +- .../components/braviatv/translations/et.json | 2 +- .../components/braviatv/translations/fr.json | 2 +- .../components/braviatv/translations/hu.json | 2 +- .../components/braviatv/translations/id.json | 2 +- .../components/braviatv/translations/it.json | 2 +- .../components/braviatv/translations/nl.json | 2 +- .../components/braviatv/translations/no.json | 2 +- .../components/braviatv/translations/pl.json | 2 +- .../braviatv/translations/pt-BR.json | 2 +- .../components/braviatv/translations/ru.json | 2 +- .../components/braviatv/translations/tr.json | 2 +- .../braviatv/translations/zh-Hant.json | 2 +- .../components/broadlink/translations/hu.json | 4 +- .../components/brother/translations/ca.json | 5 +- .../components/brother/translations/da.json | 3 +- .../components/brother/translations/de.json | 5 +- .../components/brother/translations/el.json | 3 +- .../components/brother/translations/en.json | 5 +- .../brother/translations/es-419.json | 3 +- .../components/brother/translations/es.json | 3 +- .../components/brother/translations/et.json | 5 +- .../components/brother/translations/fr.json | 5 +- .../components/brother/translations/hu.json | 5 +- .../components/brother/translations/id.json | 5 +- .../components/brother/translations/it.json | 5 +- .../components/brother/translations/ja.json | 3 +- .../components/brother/translations/ko.json | 3 +- .../components/brother/translations/lb.json | 3 +- .../components/brother/translations/nl.json | 5 +- .../components/brother/translations/no.json | 5 +- .../components/brother/translations/pl.json | 5 +- .../brother/translations/pt-BR.json | 5 +- .../components/brother/translations/ru.json | 5 +- .../components/brother/translations/sl.json | 3 +- .../components/brother/translations/sv.json | 3 +- .../components/brother/translations/tr.json | 5 +- .../components/brother/translations/uk.json | 3 +- .../brother/translations/zh-Hans.json | 3 - .../brother/translations/zh-Hant.json | 3 +- .../components/bsblan/translations/hu.json | 2 +- .../components/cast/translations/cs.json | 32 +++ .../components/climacell/translations/ca.json | 2 +- .../components/climacell/translations/de.json | 2 +- .../components/climacell/translations/en.json | 23 +- .../components/climacell/translations/fr.json | 4 +- .../components/climacell/translations/hu.json | 6 +- .../components/climacell/translations/pl.json | 2 +- .../climacell/translations/sensor.fr.json | 2 +- .../climacell/translations/sensor.hu.json | 2 +- .../components/climate/translations/et.json | 4 +- .../cloudflare/translations/he.json | 3 +- .../components/coinbase/translations/ca.json | 3 +- .../components/coinbase/translations/de.json | 3 +- .../components/coinbase/translations/el.json | 3 +- .../components/coinbase/translations/en.json | 6 +- .../components/coinbase/translations/et.json | 3 +- .../components/coinbase/translations/fr.json | 3 +- .../components/coinbase/translations/hu.json | 3 +- .../components/coinbase/translations/id.json | 3 +- .../components/coinbase/translations/it.json | 3 +- .../components/coinbase/translations/ja.json | 3 +- .../components/coinbase/translations/nl.json | 3 +- .../components/coinbase/translations/no.json | 3 +- .../components/coinbase/translations/pl.json | 3 +- .../coinbase/translations/pt-BR.json | 3 +- .../components/coinbase/translations/ru.json | 3 +- .../components/coinbase/translations/tr.json | 3 +- .../coinbase/translations/zh-Hant.json | 3 +- .../coronavirus/translations/hu.json | 2 +- .../components/cover/translations/he.json | 4 +- .../components/cpuspeed/translations/bg.json | 1 - .../components/cpuspeed/translations/ca.json | 1 - .../components/cpuspeed/translations/de.json | 1 - .../components/cpuspeed/translations/el.json | 1 - .../components/cpuspeed/translations/en.json | 1 - .../components/cpuspeed/translations/es.json | 1 - .../components/cpuspeed/translations/et.json | 1 - .../components/cpuspeed/translations/fr.json | 1 - .../components/cpuspeed/translations/he.json | 7 +- .../components/cpuspeed/translations/hu.json | 1 - .../components/cpuspeed/translations/id.json | 1 - .../components/cpuspeed/translations/it.json | 1 - .../components/cpuspeed/translations/ja.json | 1 - .../components/cpuspeed/translations/nl.json | 1 - .../components/cpuspeed/translations/no.json | 1 - .../components/cpuspeed/translations/pl.json | 1 - .../cpuspeed/translations/pt-BR.json | 1 - .../components/cpuspeed/translations/ru.json | 1 - .../components/cpuspeed/translations/tr.json | 1 - .../cpuspeed/translations/zh-Hans.json | 1 - .../cpuspeed/translations/zh-Hant.json | 1 - .../crownstone/translations/ca.json | 21 -- .../crownstone/translations/cs.json | 10 - .../crownstone/translations/de.json | 21 -- .../crownstone/translations/el.json | 21 -- .../crownstone/translations/en.json | 21 -- .../crownstone/translations/es-419.json | 3 - .../crownstone/translations/es.json | 21 -- .../crownstone/translations/et.json | 21 -- .../crownstone/translations/fr.json | 21 -- .../crownstone/translations/he.json | 10 - .../crownstone/translations/hu.json | 21 -- .../crownstone/translations/id.json | 21 -- .../crownstone/translations/it.json | 21 -- .../crownstone/translations/ja.json | 21 -- .../crownstone/translations/ko.json | 16 -- .../crownstone/translations/nl.json | 21 -- .../crownstone/translations/no.json | 21 -- .../crownstone/translations/pl.json | 21 -- .../crownstone/translations/pt-BR.json | 21 -- .../crownstone/translations/ru.json | 21 -- .../crownstone/translations/tr.json | 21 -- .../crownstone/translations/zh-Hant.json | 21 -- .../components/deconz/translations/he.json | 2 +- .../components/deconz/translations/hu.json | 2 +- .../components/deluge/translations/bg.json | 21 ++ .../components/deluge/translations/ca.json | 23 ++ .../components/deluge/translations/cs.json | 21 ++ .../components/deluge/translations/de.json | 23 ++ .../components/deluge/translations/el.json | 23 ++ .../components/deluge/translations/en.json | 26 +-- .../components/deluge/translations/et.json | 23 ++ .../components/deluge/translations/fr.json | 23 ++ .../components/deluge/translations/he.json | 21 ++ .../components/deluge/translations/hu.json | 23 ++ .../components/deluge/translations/id.json | 23 ++ .../components/deluge/translations/it.json | 23 ++ .../components/deluge/translations/ja.json | 23 ++ .../components/deluge/translations/nl.json | 23 ++ .../components/deluge/translations/no.json | 23 ++ .../components/deluge/translations/pl.json | 23 ++ .../components/deluge/translations/pt-BR.json | 23 ++ .../components/deluge/translations/ru.json | 23 ++ .../components/deluge/translations/tr.json | 23 ++ .../deluge/translations/zh-Hant.json | 23 ++ .../components/demo/translations/it.json | 4 +- .../components/denonavr/translations/bg.json | 3 + .../components/denonavr/translations/ca.json | 3 + .../components/denonavr/translations/de.json | 3 + .../components/denonavr/translations/el.json | 3 + .../components/denonavr/translations/en.json | 3 + .../components/denonavr/translations/et.json | 3 + .../components/denonavr/translations/fr.json | 3 + .../components/denonavr/translations/hu.json | 7 +- .../components/denonavr/translations/id.json | 3 + .../components/denonavr/translations/it.json | 3 + .../components/denonavr/translations/ja.json | 3 + .../components/denonavr/translations/nl.json | 3 + .../components/denonavr/translations/no.json | 3 + .../components/denonavr/translations/pl.json | 3 + .../denonavr/translations/pt-BR.json | 3 + .../components/denonavr/translations/ru.json | 3 + .../components/denonavr/translations/tr.json | 3 + .../denonavr/translations/zh-Hant.json | 3 + .../derivative/translations/bg.json | 25 +++ .../derivative/translations/ca.json | 59 ++++++ .../derivative/translations/cs.json | 11 + .../derivative/translations/de.json | 59 ++++++ .../derivative/translations/el.json | 59 ++++++ .../derivative/translations/en.json | 16 ++ .../derivative/translations/et.json | 59 ++++++ .../derivative/translations/fr.json | 59 ++++++ .../derivative/translations/he.json | 11 + .../derivative/translations/hu.json | 59 ++++++ .../derivative/translations/id.json | 59 ++++++ .../derivative/translations/it.json | 59 ++++++ .../derivative/translations/ja.json | 59 ++++++ .../derivative/translations/nl.json | 59 ++++++ .../derivative/translations/no.json | 59 ++++++ .../derivative/translations/pl.json | 59 ++++++ .../derivative/translations/pt-BR.json | 59 ++++++ .../derivative/translations/ru.json | 53 +++++ .../derivative/translations/sv.json | 12 ++ .../derivative/translations/tr.json | 59 ++++++ .../derivative/translations/zh-Hans.json | 59 ++++++ .../derivative/translations/zh-Hant.json | 59 ++++++ .../components/directv/translations/it.json | 4 +- .../components/discord/translations/bg.json | 13 ++ .../components/discord/translations/ca.json | 27 +++ .../components/discord/translations/de.json | 27 +++ .../components/discord/translations/el.json | 27 +++ .../components/discord/translations/en.json | 4 +- .../components/discord/translations/et.json | 27 +++ .../components/discord/translations/fr.json | 27 +++ .../components/discord/translations/he.json | 25 +++ .../components/discord/translations/hu.json | 27 +++ .../components/discord/translations/id.json | 27 +++ .../components/discord/translations/it.json | 27 +++ .../components/discord/translations/ja.json | 27 +++ .../components/discord/translations/nl.json | 27 +++ .../components/discord/translations/no.json | 27 +++ .../components/discord/translations/pl.json | 27 +++ .../discord/translations/pt-BR.json | 27 +++ .../components/discord/translations/ru.json | 27 +++ .../components/discord/translations/tr.json | 27 +++ .../discord/translations/zh-Hant.json | 27 +++ .../components/dlna_dmr/translations/hu.json | 4 +- .../components/dlna_dms/translations/cs.json | 20 ++ .../components/dlna_dms/translations/hu.json | 2 +- .../components/doorbird/translations/ca.json | 3 + .../components/doorbird/translations/de.json | 3 + .../components/doorbird/translations/el.json | 3 + .../components/doorbird/translations/en.json | 3 + .../components/doorbird/translations/et.json | 3 + .../components/doorbird/translations/fr.json | 3 + .../components/doorbird/translations/hu.json | 3 + .../components/doorbird/translations/id.json | 3 + .../components/doorbird/translations/it.json | 3 + .../components/doorbird/translations/ja.json | 3 + .../components/doorbird/translations/nl.json | 3 + .../components/doorbird/translations/no.json | 3 + .../components/doorbird/translations/pl.json | 3 + .../doorbird/translations/pt-BR.json | 3 + .../components/doorbird/translations/ru.json | 3 + .../components/doorbird/translations/tr.json | 3 + .../doorbird/translations/zh-Hant.json | 3 + .../components/dsmr/translations/it.json | 8 +- .../components/dunehd/translations/ca.json | 2 +- .../components/dunehd/translations/de.json | 2 +- .../components/dunehd/translations/en.json | 2 +- .../components/dunehd/translations/et.json | 2 +- .../components/dunehd/translations/fr.json | 2 +- .../components/dunehd/translations/hu.json | 2 +- .../components/dunehd/translations/id.json | 2 +- .../components/dunehd/translations/it.json | 2 +- .../components/dunehd/translations/nl.json | 2 +- .../components/dunehd/translations/no.json | 2 +- .../components/dunehd/translations/pl.json | 2 +- .../components/dunehd/translations/pt-BR.json | 2 +- .../components/dunehd/translations/ru.json | 2 +- .../components/dunehd/translations/tr.json | 2 +- .../dunehd/translations/zh-Hant.json | 2 +- .../components/ecobee/translations/hu.json | 2 +- .../components/elkm1/translations/bg.json | 4 +- .../components/elkm1/translations/ca.json | 12 +- .../components/elkm1/translations/cs.json | 7 - .../components/elkm1/translations/de.json | 12 +- .../components/elkm1/translations/el.json | 12 +- .../components/elkm1/translations/es-419.json | 8 - .../components/elkm1/translations/es.json | 8 +- .../components/elkm1/translations/et.json | 12 +- .../components/elkm1/translations/fr.json | 12 +- .../components/elkm1/translations/he.json | 9 +- .../components/elkm1/translations/hu.json | 14 +- .../components/elkm1/translations/id.json | 12 +- .../components/elkm1/translations/it.json | 12 +- .../components/elkm1/translations/ja.json | 12 +- .../components/elkm1/translations/ko.json | 8 - .../components/elkm1/translations/lb.json | 8 - .../components/elkm1/translations/nl.json | 12 +- .../components/elkm1/translations/no.json | 12 +- .../components/elkm1/translations/pl.json | 12 +- .../components/elkm1/translations/pt-BR.json | 12 +- .../components/elkm1/translations/pt.json | 9 - .../components/elkm1/translations/ru.json | 12 +- .../components/elkm1/translations/sl.json | 8 - .../components/elkm1/translations/sv.json | 7 - .../components/elkm1/translations/tr.json | 12 +- .../components/elkm1/translations/uk.json | 8 - .../elkm1/translations/zh-Hant.json | 12 +- .../emulated_roku/translations/hu.json | 2 +- .../enphase_envoy/translations/en.json | 3 +- .../environment_canada/translations/de.json | 2 +- .../components/epson/translations/hu.json | 2 +- .../components/esphome/translations/cs.json | 2 +- .../components/esphome/translations/hu.json | 2 +- .../evil_genius_labs/translations/cs.json | 1 + .../components/ezviz/translations/it.json | 2 +- .../components/fan/translations/hu.json | 1 + .../components/fibaro/translations/bg.json | 21 ++ .../components/fibaro/translations/ca.json | 22 ++ .../components/fibaro/translations/de.json | 22 ++ .../components/fibaro/translations/el.json | 22 ++ .../components/fibaro/translations/en.json | 2 +- .../components/fibaro/translations/et.json | 22 ++ .../components/fibaro/translations/fr.json | 22 ++ .../components/fibaro/translations/he.json | 20 ++ .../components/fibaro/translations/hu.json | 22 ++ .../components/fibaro/translations/id.json | 22 ++ .../components/fibaro/translations/it.json | 22 ++ .../components/fibaro/translations/ja.json | 22 ++ .../components/fibaro/translations/nl.json | 22 ++ .../components/fibaro/translations/no.json | 22 ++ .../components/fibaro/translations/pl.json | 22 ++ .../components/fibaro/translations/pt-BR.json | 22 ++ .../components/fibaro/translations/ru.json | 22 ++ .../components/fibaro/translations/tr.json | 22 ++ .../fibaro/translations/zh-Hant.json | 22 ++ .../components/filesize/translations/bg.json | 19 ++ .../components/filesize/translations/ca.json | 19 ++ .../components/filesize/translations/de.json | 19 ++ .../components/filesize/translations/el.json | 19 ++ .../components/filesize/translations/en.json | 28 +-- .../components/filesize/translations/et.json | 19 ++ .../components/filesize/translations/fr.json | 19 ++ .../components/filesize/translations/he.json | 7 + .../components/filesize/translations/hu.json | 19 ++ .../components/filesize/translations/id.json | 19 ++ .../components/filesize/translations/it.json | 19 ++ .../components/filesize/translations/ja.json | 19 ++ .../components/filesize/translations/nl.json | 19 ++ .../components/filesize/translations/no.json | 19 ++ .../components/filesize/translations/pl.json | 19 ++ .../filesize/translations/pt-BR.json | 19 ++ .../components/filesize/translations/ru.json | 19 ++ .../components/filesize/translations/tr.json | 19 ++ .../filesize/translations/zh-Hant.json | 19 ++ .../components/fivem/translations/hu.json | 2 +- .../components/flux_led/translations/hu.json | 2 +- .../forecast_solar/translations/ca.json | 1 + .../forecast_solar/translations/de.json | 1 + .../forecast_solar/translations/el.json | 1 + .../forecast_solar/translations/en.json | 2 +- .../forecast_solar/translations/et.json | 1 + .../forecast_solar/translations/fr.json | 1 + .../forecast_solar/translations/hu.json | 3 +- .../forecast_solar/translations/id.json | 1 + .../forecast_solar/translations/it.json | 1 + .../forecast_solar/translations/ja.json | 1 + .../forecast_solar/translations/nl.json | 1 + .../forecast_solar/translations/no.json | 1 + .../forecast_solar/translations/pl.json | 1 + .../forecast_solar/translations/pt-BR.json | 1 + .../forecast_solar/translations/ru.json | 1 + .../forecast_solar/translations/tr.json | 1 + .../forecast_solar/translations/zh-Hant.json | 1 + .../components/foscam/translations/ca.json | 3 +- .../components/foscam/translations/cs.json | 3 +- .../components/foscam/translations/de.json | 3 +- .../components/foscam/translations/el.json | 3 +- .../components/foscam/translations/en.json | 3 +- .../foscam/translations/es-419.json | 3 +- .../components/foscam/translations/es.json | 3 +- .../components/foscam/translations/et.json | 3 +- .../components/foscam/translations/fr.json | 3 +- .../components/foscam/translations/hu.json | 3 +- .../components/foscam/translations/id.json | 3 +- .../components/foscam/translations/it.json | 3 +- .../components/foscam/translations/ja.json | 3 +- .../components/foscam/translations/ko.json | 3 +- .../components/foscam/translations/lb.json | 3 +- .../components/foscam/translations/nl.json | 3 +- .../components/foscam/translations/no.json | 3 +- .../components/foscam/translations/pl.json | 3 +- .../components/foscam/translations/pt-BR.json | 3 +- .../components/foscam/translations/ru.json | 3 +- .../components/foscam/translations/tr.json | 3 +- .../foscam/translations/zh-Hant.json | 3 +- .../components/freebox/translations/hu.json | 2 +- .../components/freebox/translations/it.json | 2 +- .../components/fritz/translations/ca.json | 1 + .../components/fritz/translations/de.json | 1 + .../components/fritz/translations/el.json | 1 + .../components/fritz/translations/en.json | 11 + .../components/fritz/translations/et.json | 1 + .../components/fritz/translations/fr.json | 1 + .../components/fritz/translations/hu.json | 1 + .../components/fritz/translations/id.json | 1 + .../components/fritz/translations/it.json | 1 + .../components/fritz/translations/ja.json | 1 + .../components/fritz/translations/nl.json | 1 + .../components/fritz/translations/no.json | 1 + .../components/fritz/translations/pl.json | 1 + .../components/fritz/translations/pt-BR.json | 1 + .../components/fritz/translations/ru.json | 1 + .../components/fritz/translations/tr.json | 1 + .../fritz/translations/zh-Hant.json | 1 + .../components/fritzbox/translations/ca.json | 1 + .../components/fritzbox/translations/de.json | 1 + .../components/fritzbox/translations/el.json | 1 + .../components/fritzbox/translations/et.json | 1 + .../components/fritzbox/translations/fr.json | 1 + .../components/fritzbox/translations/hu.json | 3 +- .../components/fritzbox/translations/id.json | 1 + .../components/fritzbox/translations/it.json | 1 + .../components/fritzbox/translations/ja.json | 1 + .../components/fritzbox/translations/nl.json | 1 + .../components/fritzbox/translations/no.json | 1 + .../components/fritzbox/translations/pl.json | 1 + .../fritzbox/translations/pt-BR.json | 1 + .../components/fritzbox/translations/ru.json | 1 + .../components/fritzbox/translations/tr.json | 1 + .../fritzbox/translations/zh-Hant.json | 1 + .../components/generic/translations/bg.json | 53 +++++ .../components/generic/translations/ca.json | 88 ++++++++ .../components/generic/translations/cs.json | 33 +++ .../components/generic/translations/de.json | 88 ++++++++ .../components/generic/translations/el.json | 88 ++++++++ .../components/generic/translations/en.json | 2 + .../components/generic/translations/et.json | 88 ++++++++ .../components/generic/translations/fr.json | 88 ++++++++ .../components/generic/translations/he.json | 88 ++++++++ .../components/generic/translations/hu.json | 88 ++++++++ .../components/generic/translations/id.json | 88 ++++++++ .../components/generic/translations/it.json | 88 ++++++++ .../components/generic/translations/ja.json | 88 ++++++++ .../components/generic/translations/nl.json | 88 ++++++++ .../components/generic/translations/no.json | 88 ++++++++ .../components/generic/translations/pl.json | 88 ++++++++ .../generic/translations/pt-BR.json | 88 ++++++++ .../components/generic/translations/ru.json | 88 ++++++++ .../components/generic/translations/tr.json | 88 ++++++++ .../generic/translations/zh-Hant.json | 88 ++++++++ .../components/gios/translations/hu.json | 2 +- .../components/glances/translations/hu.json | 2 +- .../components/goalzero/translations/ca.json | 2 +- .../components/goalzero/translations/de.json | 2 +- .../components/goalzero/translations/en.json | 2 +- .../components/goalzero/translations/et.json | 2 +- .../components/goalzero/translations/fr.json | 2 +- .../components/goalzero/translations/hu.json | 4 +- .../components/goalzero/translations/id.json | 2 +- .../components/goalzero/translations/it.json | 2 +- .../components/goalzero/translations/nl.json | 2 +- .../components/goalzero/translations/no.json | 2 +- .../components/goalzero/translations/pl.json | 2 +- .../goalzero/translations/pt-BR.json | 2 +- .../components/goalzero/translations/ru.json | 2 +- .../components/goalzero/translations/tr.json | 2 +- .../goalzero/translations/zh-Hant.json | 2 +- .../components/goodwe/translations/hu.json | 2 +- .../components/google/translations/bg.json | 20 ++ .../components/google/translations/ca.json | 31 +++ .../components/google/translations/cs.json | 23 ++ .../components/google/translations/de.json | 31 +++ .../components/google/translations/el.json | 31 +++ .../components/google/translations/es.json | 12 ++ .../components/google/translations/et.json | 31 +++ .../components/google/translations/fr.json | 31 +++ .../components/google/translations/he.json | 27 +++ .../components/google/translations/hu.json | 31 +++ .../components/google/translations/id.json | 31 +++ .../components/google/translations/it.json | 31 +++ .../components/google/translations/ja.json | 31 +++ .../components/google/translations/nl.json | 31 +++ .../components/google/translations/no.json | 31 +++ .../components/google/translations/pl.json | 31 +++ .../components/google/translations/pt-BR.json | 31 +++ .../components/google/translations/ru.json | 31 +++ .../components/google/translations/tr.json | 31 +++ .../google/translations/zh-Hant.json | 31 +++ .../google_travel_time/translations/hu.json | 2 +- .../components/group/translations/bg.json | 109 +++++++++- .../components/group/translations/ca.json | 105 +++++++++- .../components/group/translations/cs.json | 196 ++++++++++++++++++ .../components/group/translations/de.json | 105 +++++++++- .../components/group/translations/el.json | 95 ++++++++- .../components/group/translations/en.json | 77 ++++++- .../components/group/translations/et.json | 95 ++++++++- .../components/group/translations/fr.json | 105 +++++++++- .../components/group/translations/he.json | 93 ++++++++- .../components/group/translations/hu.json | 115 ++++++++-- .../components/group/translations/id.json | 105 +++++++++- .../components/group/translations/it.json | 105 +++++++++- .../components/group/translations/ja.json | 93 ++++++++- .../components/group/translations/nl.json | 105 +++++++++- .../components/group/translations/no.json | 105 +++++++++- .../components/group/translations/pl.json | 107 +++++++++- .../components/group/translations/pt-BR.json | 105 +++++++++- .../components/group/translations/ru.json | 105 +++++++++- .../components/group/translations/tr.json | 158 ++++++++++++-- .../group/translations/zh-Hant.json | 93 ++++++++- .../growatt_server/translations/hu.json | 2 +- .../components/guardian/translations/hu.json | 2 +- .../components/hangouts/translations/ca.json | 2 +- .../components/hangouts/translations/de.json | 2 +- .../components/hangouts/translations/et.json | 2 +- .../components/hangouts/translations/fr.json | 6 +- .../components/hangouts/translations/he.json | 2 +- .../components/hangouts/translations/hu.json | 2 +- .../components/hangouts/translations/id.json | 2 +- .../components/hangouts/translations/it.json | 2 +- .../components/hangouts/translations/nl.json | 2 +- .../components/hangouts/translations/no.json | 2 +- .../components/hangouts/translations/pl.json | 2 +- .../hangouts/translations/pt-BR.json | 2 +- .../components/hangouts/translations/ru.json | 2 +- .../components/hangouts/translations/tr.json | 2 +- .../hangouts/translations/zh-Hant.json | 2 +- .../components/hassio/translations/ja.json | 2 +- .../home_connect/translations/hu.json | 4 +- .../home_connect/translations/it.json | 2 +- .../home_plus_control/translations/hu.json | 6 +- .../home_plus_control/translations/it.json | 2 +- .../components/homekit/translations/bg.json | 6 - .../components/homekit/translations/ca.json | 9 - .../components/homekit/translations/cs.json | 9 - .../components/homekit/translations/de.json | 9 - .../components/homekit/translations/el.json | 9 - .../components/homekit/translations/en.json | 9 - .../homekit/translations/es-419.json | 3 - .../components/homekit/translations/es.json | 9 - .../components/homekit/translations/et.json | 9 - .../components/homekit/translations/fr.json | 9 - .../components/homekit/translations/he.json | 9 +- .../components/homekit/translations/hu.json | 11 +- .../components/homekit/translations/id.json | 9 - .../components/homekit/translations/it.json | 9 - .../components/homekit/translations/ja.json | 9 - .../components/homekit/translations/ka.json | 8 - .../components/homekit/translations/ko.json | 9 - .../components/homekit/translations/lb.json | 9 - .../components/homekit/translations/nl.json | 9 - .../components/homekit/translations/no.json | 9 - .../components/homekit/translations/pl.json | 9 - .../homekit/translations/pt-BR.json | 11 +- .../components/homekit/translations/pt.json | 8 - .../components/homekit/translations/ru.json | 9 - .../components/homekit/translations/sl.json | 10 - .../components/homekit/translations/sv.json | 3 - .../components/homekit/translations/tr.json | 9 - .../components/homekit/translations/uk.json | 9 - .../homekit/translations/zh-Hans.json | 9 - .../homekit/translations/zh-Hant.json | 9 - .../homekit_controller/translations/hu.json | 2 +- .../homematicip_cloud/translations/fr.json | 2 +- .../homematicip_cloud/translations/hu.json | 2 +- .../homematicip_cloud/translations/it.json | 2 +- .../components/honeywell/translations/ca.json | 14 +- .../components/honeywell/translations/de.json | 14 +- .../components/honeywell/translations/el.json | 14 +- .../components/honeywell/translations/en.json | 6 +- .../components/honeywell/translations/es.json | 3 +- .../components/honeywell/translations/et.json | 14 +- .../components/honeywell/translations/fr.json | 14 +- .../components/honeywell/translations/hu.json | 14 +- .../components/honeywell/translations/id.json | 14 +- .../components/honeywell/translations/it.json | 14 +- .../components/honeywell/translations/ja.json | 14 +- .../components/honeywell/translations/nl.json | 14 +- .../components/honeywell/translations/no.json | 14 +- .../components/honeywell/translations/pl.json | 14 +- .../honeywell/translations/pt-BR.json | 14 +- .../components/honeywell/translations/ru.json | 14 +- .../components/honeywell/translations/tr.json | 14 +- .../honeywell/translations/zh-Hant.json | 14 +- .../huawei_lte/translations/hu.json | 2 +- .../components/hue/translations/ca.json | 1 + .../components/hue/translations/de.json | 1 + .../components/hue/translations/el.json | 1 + .../components/hue/translations/en.json | 1 + .../components/hue/translations/et.json | 1 + .../components/hue/translations/fr.json | 1 + .../components/hue/translations/hu.json | 3 +- .../components/hue/translations/id.json | 1 + .../components/hue/translations/it.json | 1 + .../components/hue/translations/nl.json | 1 + .../components/hue/translations/no.json | 1 + .../components/hue/translations/pl.json | 1 + .../components/hue/translations/pt-BR.json | 1 + .../components/hue/translations/ru.json | 1 + .../components/hue/translations/tr.json | 1 + .../components/hue/translations/zh-Hant.json | 1 + .../humidifier/translations/hu.json | 1 + .../components/hyperion/translations/hu.json | 4 +- .../components/ifttt/translations/ja.json | 2 +- .../components/insteon/translations/ca.json | 5 + .../components/insteon/translations/de.json | 5 + .../components/insteon/translations/el.json | 5 + .../components/insteon/translations/en.json | 1 + .../components/insteon/translations/et.json | 5 + .../components/insteon/translations/fr.json | 5 + .../components/insteon/translations/he.json | 1 + .../components/insteon/translations/hu.json | 5 + .../components/insteon/translations/id.json | 5 + .../components/insteon/translations/it.json | 7 +- .../components/insteon/translations/ja.json | 6 +- .../components/insteon/translations/nl.json | 5 + .../components/insteon/translations/no.json | 5 + .../components/insteon/translations/pl.json | 5 + .../insteon/translations/pt-BR.json | 5 + .../components/insteon/translations/ru.json | 5 + .../components/insteon/translations/tr.json | 5 + .../insteon/translations/zh-Hant.json | 5 + .../translations/bg.json} | 2 +- .../integration/translations/ca.json | 42 ++++ .../integration/translations/cs.json | 41 ++++ .../integration/translations/de.json | 42 ++++ .../integration/translations/el.json | 42 ++++ .../integration/translations/en.json | 6 + .../integration/translations/et.json | 42 ++++ .../integration/translations/fr.json | 42 ++++ .../integration/translations/he.json | 40 ++++ .../integration/translations/hu.json | 42 ++++ .../integration/translations/id.json | 42 ++++ .../integration/translations/it.json | 42 ++++ .../integration/translations/ja.json | 42 ++++ .../integration/translations/nl.json | 42 ++++ .../integration/translations/no.json | 42 ++++ .../integration/translations/pl.json | 42 ++++ .../integration/translations/pt-BR.json | 42 ++++ .../integration/translations/ru.json | 42 ++++ .../integration/translations/tr.json | 42 ++++ .../integration/translations/zh-Hans.json | 42 ++++ .../integration/translations/zh-Hant.json | 42 ++++ .../intellifire/translations/bg.json | 15 ++ .../intellifire/translations/ca.json | 29 ++- .../intellifire/translations/cs.json | 11 + .../intellifire/translations/de.json | 29 ++- .../intellifire/translations/el.json | 29 ++- .../intellifire/translations/en.json | 16 +- .../intellifire/translations/et.json | 29 ++- .../intellifire/translations/fr.json | 29 ++- .../intellifire/translations/he.json | 19 +- .../intellifire/translations/hu.json | 29 ++- .../intellifire/translations/id.json | 29 ++- .../intellifire/translations/it.json | 29 ++- .../intellifire/translations/ja.json | 29 ++- .../intellifire/translations/nl.json | 29 ++- .../intellifire/translations/no.json | 29 ++- .../intellifire/translations/pl.json | 29 ++- .../intellifire/translations/pt-BR.json | 29 ++- .../intellifire/translations/ru.json | 29 ++- .../intellifire/translations/tr.json | 29 ++- .../intellifire/translations/zh-Hant.json | 29 ++- .../components/iotawatt/translations/hu.json | 2 +- .../components/iotawatt/translations/id.json | 2 +- .../components/ipma/translations/hu.json | 2 +- .../components/ipp/translations/it.json | 2 +- .../components/ipp/translations/pl.json | 2 +- .../components/iss/translations/hu.json | 2 +- .../components/iss/translations/pt-BR.json | 4 +- .../kaleidescape/translations/cs.json | 13 ++ .../kaleidescape/translations/hu.json | 2 +- .../components/knx/translations/bg.json | 18 +- .../components/knx/translations/ca.json | 78 ++++++- .../components/knx/translations/de.json | 82 ++++++-- .../components/knx/translations/el.json | 60 +++++- .../components/knx/translations/en.json | 6 +- .../components/knx/translations/et.json | 78 ++++++- .../components/knx/translations/fr.json | 78 ++++++- .../components/knx/translations/hu.json | 78 ++++++- .../components/knx/translations/id.json | 78 ++++++- .../components/knx/translations/it.json | 78 ++++++- .../components/knx/translations/ja.json | 58 +++++- .../components/knx/translations/nl.json | 78 ++++++- .../components/knx/translations/no.json | 78 ++++++- .../components/knx/translations/pl.json | 78 ++++++- .../components/knx/translations/pt-BR.json | 78 ++++++- .../components/knx/translations/ru.json | 78 ++++++- .../components/knx/translations/tr.json | 78 ++++++- .../components/knx/translations/zh-Hant.json | 78 ++++++- .../components/konnected/translations/fr.json | 2 +- .../components/konnected/translations/hu.json | 8 +- .../components/konnected/translations/it.json | 6 +- .../konnected/translations/pt-BR.json | 2 +- .../kostal_plenticore/translations/ca.json | 3 +- .../kostal_plenticore/translations/de.json | 3 +- .../kostal_plenticore/translations/el.json | 3 +- .../kostal_plenticore/translations/en.json | 3 +- .../kostal_plenticore/translations/es.json | 3 +- .../kostal_plenticore/translations/et.json | 3 +- .../kostal_plenticore/translations/fr.json | 3 +- .../kostal_plenticore/translations/hu.json | 3 +- .../kostal_plenticore/translations/id.json | 3 +- .../kostal_plenticore/translations/it.json | 3 +- .../kostal_plenticore/translations/ja.json | 3 +- .../kostal_plenticore/translations/nl.json | 3 +- .../kostal_plenticore/translations/no.json | 3 +- .../kostal_plenticore/translations/pl.json | 3 +- .../kostal_plenticore/translations/pt-BR.json | 3 +- .../kostal_plenticore/translations/ru.json | 3 +- .../kostal_plenticore/translations/tr.json | 3 +- .../translations/zh-Hant.json | 3 +- .../components/kraken/translations/it.json | 8 +- .../components/light/translations/hu.json | 1 + .../logi_circle/translations/hu.json | 4 +- .../logi_circle/translations/it.json | 2 +- .../components/lookin/translations/hu.json | 4 +- .../components/luftdaten/translations/hu.json | 2 +- .../luftdaten/translations/pt-BR.json | 2 +- .../lutron_caseta/translations/pl.json | 2 +- .../components/lyric/translations/hu.json | 2 +- .../components/lyric/translations/it.json | 2 +- .../components/mailgun/translations/ja.json | 2 +- .../components/mazda/translations/ca.json | 6 +- .../components/mazda/translations/de.json | 6 +- .../components/mazda/translations/el.json | 6 +- .../components/mazda/translations/en.json | 6 +- .../components/mazda/translations/es.json | 6 +- .../components/mazda/translations/et.json | 6 +- .../components/mazda/translations/fr.json | 6 +- .../components/mazda/translations/hu.json | 6 +- .../components/mazda/translations/id.json | 6 +- .../components/mazda/translations/it.json | 6 +- .../components/mazda/translations/ja.json | 6 +- .../components/mazda/translations/ko.json | 6 +- .../components/mazda/translations/nl.json | 6 +- .../components/mazda/translations/no.json | 6 +- .../components/mazda/translations/pl.json | 6 +- .../components/mazda/translations/pt-BR.json | 6 +- .../components/mazda/translations/ru.json | 6 +- .../components/mazda/translations/tr.json | 6 +- .../mazda/translations/zh-Hant.json | 6 +- .../components/meater/translations/bg.json | 17 ++ .../components/meater/translations/ca.json | 18 ++ .../components/meater/translations/cs.json | 16 ++ .../components/meater/translations/de.json | 18 ++ .../components/meater/translations/el.json | 18 ++ .../components/meater/translations/et.json | 18 ++ .../components/meater/translations/fr.json | 18 ++ .../components/meater/translations/he.json | 16 ++ .../components/meater/translations/hu.json | 18 ++ .../components/meater/translations/id.json | 18 ++ .../components/meater/translations/it.json | 18 ++ .../components/meater/translations/ja.json | 18 ++ .../components/meater/translations/nl.json | 18 ++ .../components/meater/translations/no.json | 18 ++ .../components/meater/translations/pl.json | 18 ++ .../components/meater/translations/pt-BR.json | 18 ++ .../components/meater/translations/ru.json | 18 ++ .../components/meater/translations/tr.json | 18 ++ .../meater/translations/zh-Hant.json | 18 ++ .../media_player/translations/de.json | 3 + .../media_player/translations/el.json | 3 + .../media_player/translations/et.json | 3 + .../media_player/translations/fr.json | 3 + .../media_player/translations/hu.json | 4 + .../media_player/translations/id.json | 3 + .../media_player/translations/it.json | 3 + .../media_player/translations/nl.json | 3 + .../media_player/translations/no.json | 3 + .../media_player/translations/pl.json | 3 + .../media_player/translations/pt-BR.json | 3 + .../media_player/translations/zh-Hant.json | 3 + .../components/met/translations/hu.json | 2 +- .../met_eireann/translations/hu.json | 2 +- .../meteo_france/translations/bg.json | 6 +- .../meteo_france/translations/ca.json | 8 +- .../meteo_france/translations/cs.json | 6 +- .../meteo_france/translations/da.json | 3 +- .../meteo_france/translations/de.json | 6 +- .../meteo_france/translations/el.json | 6 +- .../meteo_france/translations/en.json | 6 +- .../meteo_france/translations/es-419.json | 3 +- .../meteo_france/translations/es.json | 6 +- .../meteo_france/translations/et.json | 6 +- .../meteo_france/translations/fr.json | 6 +- .../meteo_france/translations/hu.json | 6 +- .../meteo_france/translations/id.json | 6 +- .../meteo_france/translations/it.json | 6 +- .../meteo_france/translations/ja.json | 6 +- .../meteo_france/translations/ko.json | 6 +- .../meteo_france/translations/lb.json | 6 +- .../meteo_france/translations/nl.json | 6 +- .../meteo_france/translations/no.json | 6 +- .../meteo_france/translations/pl.json | 6 +- .../meteo_france/translations/pt-BR.json | 6 +- .../meteo_france/translations/pt.json | 3 +- .../meteo_france/translations/ru.json | 6 +- .../meteo_france/translations/sl.json | 3 +- .../meteo_france/translations/sv.json | 3 +- .../meteo_france/translations/tr.json | 6 +- .../meteo_france/translations/uk.json | 6 +- .../meteo_france/translations/zh-Hant.json | 6 +- .../meteoclimatic/translations/bg.json | 5 - .../meteoclimatic/translations/ca.json | 5 +- .../meteoclimatic/translations/de.json | 5 +- .../meteoclimatic/translations/el.json | 5 +- .../meteoclimatic/translations/en.json | 5 +- .../meteoclimatic/translations/es.json | 4 +- .../meteoclimatic/translations/et.json | 5 +- .../meteoclimatic/translations/fr.json | 5 +- .../meteoclimatic/translations/gl.json | 9 - .../meteoclimatic/translations/hu.json | 5 +- .../meteoclimatic/translations/id.json | 5 +- .../meteoclimatic/translations/it.json | 5 +- .../meteoclimatic/translations/ja.json | 5 +- .../meteoclimatic/translations/nl.json | 5 +- .../meteoclimatic/translations/no.json | 5 +- .../meteoclimatic/translations/pl.json | 5 +- .../meteoclimatic/translations/pt-BR.json | 5 +- .../meteoclimatic/translations/pt.json | 9 - .../meteoclimatic/translations/ru.json | 5 +- .../meteoclimatic/translations/tr.json | 5 +- .../meteoclimatic/translations/zh-Hant.json | 5 +- .../components/mikrotik/translations/hu.json | 2 +- .../sk.json => min_max/translations/bg.json} | 2 +- .../components/min_max/translations/ca.json | 42 ++++ .../components/min_max/translations/cs.json | 34 +++ .../components/min_max/translations/de.json | 42 ++++ .../components/min_max/translations/el.json | 42 ++++ .../components/min_max/translations/en.json | 8 + .../components/min_max/translations/et.json | 42 ++++ .../components/min_max/translations/fr.json | 42 ++++ .../components/min_max/translations/he.json | 37 ++++ .../components/min_max/translations/hu.json | 42 ++++ .../components/min_max/translations/id.json | 42 ++++ .../components/min_max/translations/it.json | 42 ++++ .../components/min_max/translations/ja.json | 42 ++++ .../components/min_max/translations/nl.json | 42 ++++ .../components/min_max/translations/no.json | 42 ++++ .../components/min_max/translations/pl.json | 42 ++++ .../min_max/translations/pt-BR.json | 42 ++++ .../components/min_max/translations/ru.json | 42 ++++ .../components/min_max/translations/sv.json | 11 + .../components/min_max/translations/tr.json | 42 ++++ .../min_max/translations/zh-Hans.json | 42 ++++ .../min_max/translations/zh-Hant.json | 42 ++++ .../minecraft_server/translations/hu.json | 2 +- .../minecraft_server/translations/pl.json | 2 +- .../components/mjpeg/translations/fr.json | 2 + .../components/mjpeg/translations/hu.json | 4 +- .../modem_callerid/translations/hu.json | 4 +- .../modern_forms/translations/et.json | 2 +- .../components/moon/translations/cs.json | 13 ++ .../moon/translations/sensor.fi.json | 6 +- .../motion_blinds/translations/ca.json | 4 +- .../motion_blinds/translations/de.json | 4 +- .../motion_blinds/translations/en.json | 6 +- .../motion_blinds/translations/et.json | 4 +- .../motion_blinds/translations/fr.json | 4 +- .../motion_blinds/translations/he.json | 2 +- .../motion_blinds/translations/hu.json | 12 +- .../motion_blinds/translations/id.json | 4 +- .../motion_blinds/translations/it.json | 4 +- .../motion_blinds/translations/nl.json | 4 +- .../motion_blinds/translations/no.json | 4 +- .../motion_blinds/translations/pl.json | 4 +- .../motion_blinds/translations/pt-BR.json | 4 +- .../motion_blinds/translations/ru.json | 4 +- .../motion_blinds/translations/tr.json | 4 +- .../motion_blinds/translations/zh-Hant.json | 4 +- .../components/nam/translations/hu.json | 2 +- .../components/nanoleaf/translations/fr.json | 8 + .../components/nanoleaf/translations/hu.json | 2 +- .../components/neato/translations/hu.json | 4 +- .../components/neato/translations/it.json | 2 +- .../components/nest/translations/hu.json | 8 +- .../components/nest/translations/it.json | 2 +- .../components/nest/translations/pl.json | 2 +- .../components/netatmo/translations/hu.json | 4 +- .../components/netatmo/translations/it.json | 2 +- .../netatmo/translations/pt-BR.json | 2 +- .../nfandroidtv/translations/ca.json | 2 +- .../nfandroidtv/translations/de.json | 2 +- .../nfandroidtv/translations/en.json | 2 +- .../nfandroidtv/translations/et.json | 2 +- .../nfandroidtv/translations/fr.json | 2 +- .../nfandroidtv/translations/he.json | 2 +- .../nfandroidtv/translations/hu.json | 4 +- .../nfandroidtv/translations/id.json | 2 +- .../nfandroidtv/translations/it.json | 2 +- .../nfandroidtv/translations/nl.json | 2 +- .../nfandroidtv/translations/no.json | 2 +- .../nfandroidtv/translations/pl.json | 2 +- .../nfandroidtv/translations/pt-BR.json | 2 +- .../nfandroidtv/translations/ru.json | 2 +- .../nfandroidtv/translations/tr.json | 2 +- .../nfandroidtv/translations/zh-Hant.json | 2 +- .../components/nina/translations/zh-Hant.json | 18 +- .../nmap_tracker/translations/en.json | 3 +- .../nmap_tracker/translations/et.json | 4 +- .../nmap_tracker/translations/hu.json | 4 +- .../nmap_tracker/translations/no.json | 4 +- .../nmap_tracker/translations/pt-BR.json | 12 +- .../nmap_tracker/translations/tr.json | 4 +- .../components/nzbget/translations/hu.json | 2 +- .../ondilo_ico/translations/hu.json | 2 +- .../ondilo_ico/translations/it.json | 2 +- .../components/onewire/translations/es.json | 5 + .../components/onewire/translations/fr.json | 3 +- .../components/onewire/translations/nl.json | 4 + .../components/onvif/translations/hu.json | 8 +- .../open_meteo/translations/ca.json | 2 +- .../opentherm_gw/translations/hu.json | 2 +- .../openweathermap/translations/ca.json | 4 +- .../openweathermap/translations/de.json | 4 +- .../openweathermap/translations/en.json | 4 +- .../openweathermap/translations/et.json | 4 +- .../openweathermap/translations/fr.json | 4 +- .../openweathermap/translations/he.json | 2 +- .../openweathermap/translations/hu.json | 4 +- .../openweathermap/translations/id.json | 4 +- .../openweathermap/translations/it.json | 4 +- .../openweathermap/translations/nl.json | 4 +- .../openweathermap/translations/no.json | 4 +- .../openweathermap/translations/pl.json | 4 +- .../openweathermap/translations/pt-BR.json | 4 +- .../openweathermap/translations/ru.json | 2 +- .../openweathermap/translations/tr.json | 4 +- .../openweathermap/translations/zh-Hant.json | 4 +- .../components/overkiz/translations/ca.json | 1 + .../components/overkiz/translations/de.json | 1 + .../components/overkiz/translations/el.json | 1 + .../components/overkiz/translations/et.json | 1 + .../components/overkiz/translations/fr.json | 1 + .../components/overkiz/translations/hu.json | 1 + .../components/overkiz/translations/id.json | 1 + .../components/overkiz/translations/it.json | 1 + .../components/overkiz/translations/ja.json | 1 + .../components/overkiz/translations/nl.json | 1 + .../components/overkiz/translations/no.json | 1 + .../components/overkiz/translations/pl.json | 1 + .../overkiz/translations/pt-BR.json | 1 + .../components/overkiz/translations/ru.json | 1 + .../components/overkiz/translations/tr.json | 1 + .../overkiz/translations/zh-Hant.json | 1 + .../components/owntracks/translations/cs.json | 1 + .../p1_monitor/translations/hu.json | 2 +- .../panasonic_viera/translations/hu.json | 2 +- .../components/peco/translations/bg.json | 7 + .../components/peco/translations/ca.json | 16 ++ .../components/peco/translations/cs.json | 7 + .../components/peco/translations/de.json | 16 ++ .../components/peco/translations/el.json | 16 ++ .../components/peco/translations/en.json | 4 +- .../components/peco/translations/et.json | 16 ++ .../components/peco/translations/fr.json | 16 ++ .../components/peco/translations/he.json | 7 + .../components/peco/translations/hu.json | 16 ++ .../components/peco/translations/id.json | 16 ++ .../components/peco/translations/it.json | 16 ++ .../components/peco/translations/ja.json | 16 ++ .../components/peco/translations/nl.json | 16 ++ .../components/peco/translations/no.json | 16 ++ .../components/peco/translations/pl.json | 16 ++ .../components/peco/translations/pt-BR.json | 16 ++ .../components/peco/translations/ru.json | 16 ++ .../components/peco/translations/tr.json | 16 ++ .../components/peco/translations/zh-Hant.json | 16 ++ .../components/pi_hole/translations/hu.json | 2 +- .../components/plaato/translations/cs.json | 1 + .../plant/translations/zh-Hant.json | 2 +- .../components/plex/translations/hu.json | 2 +- .../components/plugwise/translations/ca.json | 1 + .../components/plugwise/translations/de.json | 1 + .../components/plugwise/translations/el.json | 1 + .../components/plugwise/translations/en.json | 1 + .../components/plugwise/translations/et.json | 1 + .../components/plugwise/translations/fr.json | 1 + .../components/plugwise/translations/hu.json | 1 + .../components/plugwise/translations/id.json | 1 + .../components/plugwise/translations/it.json | 1 + .../components/plugwise/translations/ja.json | 1 + .../components/plugwise/translations/nl.json | 1 + .../components/plugwise/translations/no.json | 1 + .../components/plugwise/translations/pl.json | 1 + .../plugwise/translations/pt-BR.json | 1 + .../components/plugwise/translations/ru.json | 1 + .../components/plugwise/translations/tr.json | 1 + .../plugwise/translations/zh-Hant.json | 1 + .../components/point/translations/hu.json | 6 +- .../components/point/translations/it.json | 2 +- .../components/powerwall/translations/de.json | 2 +- .../components/powerwall/translations/es.json | 3 + .../components/ps4/translations/ca.json | 6 + .../components/ps4/translations/de.json | 6 + .../components/ps4/translations/el.json | 6 + .../components/ps4/translations/en.json | 6 + .../components/ps4/translations/et.json | 6 + .../components/ps4/translations/fr.json | 6 + .../components/ps4/translations/hu.json | 16 +- .../components/ps4/translations/id.json | 6 + .../components/ps4/translations/it.json | 6 + .../components/ps4/translations/ja.json | 6 + .../components/ps4/translations/nl.json | 6 + .../components/ps4/translations/no.json | 6 + .../components/ps4/translations/pl.json | 6 + .../components/ps4/translations/pt-BR.json | 6 + .../components/ps4/translations/ru.json | 6 + .../components/ps4/translations/tr.json | 6 + .../components/ps4/translations/zh-Hant.json | 6 + .../pure_energie/translations/cs.json | 19 ++ .../pure_energie/translations/fr.json | 4 + .../pvpc_hourly_pricing/translations/ja.json | 4 +- .../components/qnap_qsw/translations/de.json | 21 ++ .../components/qnap_qsw/translations/el.json | 21 ++ .../components/qnap_qsw/translations/en.json | 4 +- .../components/qnap_qsw/translations/et.json | 21 ++ .../components/qnap_qsw/translations/fr.json | 21 ++ .../components/qnap_qsw/translations/hu.json | 21 ++ .../components/qnap_qsw/translations/id.json | 21 ++ .../components/qnap_qsw/translations/it.json | 21 ++ .../components/qnap_qsw/translations/nl.json | 21 ++ .../components/qnap_qsw/translations/no.json | 21 ++ .../components/qnap_qsw/translations/pl.json | 21 ++ .../qnap_qsw/translations/pt-BR.json | 21 ++ .../components/qnap_qsw/translations/ru.json | 21 ++ .../qnap_qsw/translations/zh-Hant.json | 21 ++ .../radio_browser/translations/cs.json | 7 + .../radio_browser/translations/fr.json | 5 + .../radio_browser/translations/he.json | 5 + .../components/remote/translations/hu.json | 1 + .../components/roku/translations/hu.json | 2 +- .../components/roku/translations/it.json | 8 +- .../components/roomba/translations/pl.json | 4 +- .../components/roon/translations/ca.json | 7 + .../components/roon/translations/de.json | 7 + .../components/roon/translations/el.json | 7 + .../components/roon/translations/en.json | 7 + .../components/roon/translations/et.json | 7 + .../components/roon/translations/fr.json | 7 + .../components/roon/translations/he.json | 6 + .../components/roon/translations/hu.json | 13 +- .../components/roon/translations/id.json | 7 + .../components/roon/translations/it.json | 11 +- .../components/roon/translations/ja.json | 7 + .../components/roon/translations/nl.json | 7 + .../components/roon/translations/no.json | 7 + .../components/roon/translations/pl.json | 7 + .../components/roon/translations/pt-BR.json | 7 + .../components/roon/translations/ru.json | 7 + .../components/roon/translations/tr.json | 7 + .../components/roon/translations/zh-Hant.json | 7 + .../rtsp_to_webrtc/translations/hu.json | 3 +- .../components/sabnzbd/translations/de.json | 18 ++ .../components/sabnzbd/translations/el.json | 18 ++ .../components/sabnzbd/translations/en.json | 8 +- .../components/sabnzbd/translations/et.json | 18 ++ .../components/sabnzbd/translations/fr.json | 18 ++ .../components/sabnzbd/translations/hu.json | 18 ++ .../components/sabnzbd/translations/id.json | 18 ++ .../components/sabnzbd/translations/it.json | 18 ++ .../components/sabnzbd/translations/nl.json | 18 ++ .../components/sabnzbd/translations/no.json | 18 ++ .../components/sabnzbd/translations/pl.json | 18 ++ .../sabnzbd/translations/pt-BR.json | 18 ++ .../sabnzbd/translations/zh-Hant.json | 18 ++ .../components/samsungtv/translations/bg.json | 6 + .../components/samsungtv/translations/ca.json | 14 +- .../components/samsungtv/translations/cs.json | 9 + .../components/samsungtv/translations/de.json | 14 +- .../components/samsungtv/translations/el.json | 14 +- .../components/samsungtv/translations/en.json | 4 +- .../components/samsungtv/translations/et.json | 14 +- .../components/samsungtv/translations/fr.json | 16 +- .../components/samsungtv/translations/hu.json | 18 +- .../components/samsungtv/translations/id.json | 14 +- .../components/samsungtv/translations/it.json | 14 +- .../components/samsungtv/translations/ja.json | 12 +- .../components/samsungtv/translations/nl.json | 14 +- .../components/samsungtv/translations/no.json | 14 +- .../components/samsungtv/translations/pl.json | 14 +- .../samsungtv/translations/pt-BR.json | 14 +- .../components/samsungtv/translations/ru.json | 14 +- .../components/samsungtv/translations/tr.json | 14 +- .../samsungtv/translations/zh-Hant.json | 14 +- .../screenlogic/translations/fr.json | 2 +- .../components/season/translations/cs.json | 14 ++ .../components/sense/translations/cs.json | 9 +- .../components/sense/translations/fr.json | 4 +- .../components/sensibo/translations/hu.json | 2 +- .../components/sensor/translations/pl.json | 4 +- .../components/sentry/translations/ca.json | 2 +- .../components/sentry/translations/de.json | 2 +- .../components/sentry/translations/en.json | 2 +- .../components/sentry/translations/et.json | 2 +- .../components/sentry/translations/fr.json | 2 +- .../components/sentry/translations/he.json | 2 +- .../components/sentry/translations/hu.json | 2 +- .../components/sentry/translations/id.json | 2 +- .../components/sentry/translations/it.json | 2 +- .../components/sentry/translations/nl.json | 2 +- .../components/sentry/translations/no.json | 2 +- .../components/sentry/translations/pl.json | 2 +- .../components/sentry/translations/pt-BR.json | 2 +- .../components/sentry/translations/ru.json | 2 +- .../components/sentry/translations/tr.json | 2 +- .../sentry/translations/zh-Hant.json | 2 +- .../components/senz/translations/bg.json | 7 + .../components/senz/translations/ca.json | 20 ++ .../components/senz/translations/de.json | 20 ++ .../components/senz/translations/el.json | 20 ++ .../components/senz/translations/et.json | 20 ++ .../components/senz/translations/fr.json | 20 ++ .../components/senz/translations/he.json | 20 ++ .../components/senz/translations/hu.json | 20 ++ .../components/senz/translations/id.json | 20 ++ .../components/senz/translations/it.json | 20 ++ .../components/senz/translations/ja.json | 20 ++ .../components/senz/translations/nl.json | 20 ++ .../components/senz/translations/no.json | 20 ++ .../components/senz/translations/pl.json | 20 ++ .../components/senz/translations/pt-BR.json | 20 ++ .../components/senz/translations/ru.json | 20 ++ .../components/senz/translations/tr.json | 20 ++ .../components/senz/translations/zh-Hant.json | 20 ++ .../simplisafe/translations/bg.json | 5 - .../simplisafe/translations/ca.json | 8 - .../simplisafe/translations/cs.json | 1 - .../simplisafe/translations/de.json | 21 +- .../simplisafe/translations/el.json | 15 +- .../simplisafe/translations/en.json | 13 +- .../simplisafe/translations/es.json | 8 - .../simplisafe/translations/et.json | 21 +- .../simplisafe/translations/fr.json | 21 +- .../simplisafe/translations/hu.json | 21 +- .../simplisafe/translations/id.json | 21 +- .../simplisafe/translations/it.json | 21 +- .../simplisafe/translations/ja.json | 8 - .../simplisafe/translations/ko.json | 1 - .../simplisafe/translations/lb.json | 1 - .../simplisafe/translations/nl.json | 21 +- .../simplisafe/translations/no.json | 21 +- .../simplisafe/translations/pl.json | 21 +- .../simplisafe/translations/pt-BR.json | 21 +- .../simplisafe/translations/ru.json | 8 - .../simplisafe/translations/sl.json | 7 - .../simplisafe/translations/tr.json | 8 - .../simplisafe/translations/uk.json | 1 - .../simplisafe/translations/zh-Hant.json | 21 +- .../components/sleepiq/translations/fr.json | 1 + .../components/slimproto/translations/de.json | 7 + .../components/slimproto/translations/el.json | 7 + .../components/slimproto/translations/en.json | 14 +- .../components/slimproto/translations/et.json | 7 + .../components/slimproto/translations/fr.json | 7 + .../components/slimproto/translations/hu.json | 13 ++ .../components/slimproto/translations/id.json | 7 + .../components/slimproto/translations/it.json | 13 ++ .../components/slimproto/translations/nl.json | 7 + .../components/slimproto/translations/no.json | 7 + .../components/slimproto/translations/pl.json | 7 + .../slimproto/translations/pt-BR.json | 7 + .../slimproto/translations/zh-Hant.json | 7 + .../components/smappee/translations/hu.json | 4 +- .../components/smappee/translations/it.json | 2 +- .../smartthings/translations/it.json | 4 +- .../components/smhi/translations/hu.json | 5 +- .../components/solax/translations/es.json | 7 + .../components/soma/translations/it.json | 2 +- .../components/somfy/translations/hu.json | 4 +- .../components/somfy/translations/it.json | 2 +- .../components/sonarr/translations/cs.json | 1 + .../components/sonarr/translations/hu.json | 2 +- .../components/spotify/translations/hu.json | 4 +- .../components/spotify/translations/it.json | 2 +- .../components/sql/translations/ca.json | 55 +++++ .../components/sql/translations/de.json | 55 +++++ .../components/sql/translations/el.json | 55 +++++ .../components/sql/translations/en.json | 4 +- .../components/sql/translations/et.json | 55 +++++ .../components/sql/translations/fr.json | 55 +++++ .../components/sql/translations/hu.json | 55 +++++ .../components/sql/translations/id.json | 55 +++++ .../components/sql/translations/it.json | 55 +++++ .../components/sql/translations/nl.json | 53 +++++ .../components/sql/translations/no.json | 55 +++++ .../components/sql/translations/pl.json | 55 +++++ .../components/sql/translations/pt-BR.json | 55 +++++ .../components/sql/translations/ru.json | 55 +++++ .../components/sql/translations/tr.json | 55 +++++ .../components/sql/translations/zh-Hant.json | 55 +++++ .../srp_energy/translations/ca.json | 3 +- .../srp_energy/translations/cs.json | 3 +- .../srp_energy/translations/de.json | 3 +- .../srp_energy/translations/el.json | 3 +- .../srp_energy/translations/en.json | 3 +- .../srp_energy/translations/es.json | 3 +- .../srp_energy/translations/et.json | 3 +- .../srp_energy/translations/fr.json | 3 +- .../srp_energy/translations/hu.json | 3 +- .../srp_energy/translations/id.json | 3 +- .../srp_energy/translations/it.json | 3 +- .../srp_energy/translations/ja.json | 3 +- .../srp_energy/translations/ka.json | 3 +- .../srp_energy/translations/ko.json | 3 +- .../srp_energy/translations/nl.json | 3 +- .../srp_energy/translations/no.json | 3 +- .../srp_energy/translations/pl.json | 3 +- .../srp_energy/translations/pt-BR.json | 3 +- .../srp_energy/translations/ru.json | 3 +- .../srp_energy/translations/tr.json | 3 +- .../srp_energy/translations/uk.json | 3 +- .../srp_energy/translations/zh-Hans.json | 3 +- .../srp_energy/translations/zh-Hant.json | 3 +- .../steam_online/translations/de.json | 36 ++++ .../steam_online/translations/el.json | 36 ++++ .../steam_online/translations/en.json | 18 +- .../steam_online/translations/et.json | 36 ++++ .../steam_online/translations/fr.json | 36 ++++ .../steam_online/translations/hu.json | 36 ++++ .../steam_online/translations/id.json | 36 ++++ .../steam_online/translations/it.json | 36 ++++ .../steam_online/translations/nl.json | 36 ++++ .../steam_online/translations/no.json | 36 ++++ .../steam_online/translations/pl.json | 36 ++++ .../steam_online/translations/pt-BR.json | 36 ++++ .../steam_online/translations/ru.json | 25 +++ .../steam_online/translations/zh-Hant.json | 36 ++++ .../components/steamist/translations/hu.json | 2 +- .../components/subaru/translations/ca.json | 19 +- .../components/subaru/translations/de.json | 19 +- .../components/subaru/translations/el.json | 19 +- .../components/subaru/translations/en.json | 19 +- .../components/subaru/translations/et.json | 19 +- .../components/subaru/translations/fr.json | 19 +- .../components/subaru/translations/hu.json | 21 +- .../components/subaru/translations/id.json | 19 +- .../components/subaru/translations/it.json | 19 +- .../components/subaru/translations/ja.json | 19 +- .../components/subaru/translations/nl.json | 19 +- .../components/subaru/translations/no.json | 19 +- .../components/subaru/translations/pl.json | 19 +- .../components/subaru/translations/pt-BR.json | 19 +- .../components/subaru/translations/ru.json | 19 +- .../components/subaru/translations/tr.json | 19 +- .../subaru/translations/zh-Hant.json | 19 +- .../components/sun/translations/bg.json | 5 + .../components/sun/translations/ca.json | 10 + .../components/sun/translations/cs.json | 10 + .../components/sun/translations/de.json | 10 + .../components/sun/translations/el.json | 10 + .../components/sun/translations/en.json | 10 + .../components/sun/translations/et.json | 10 + .../components/sun/translations/fr.json | 10 + .../components/sun/translations/he.json | 10 + .../components/sun/translations/hu.json | 10 + .../components/sun/translations/id.json | 10 + .../components/sun/translations/it.json | 10 + .../components/sun/translations/ja.json | 10 + .../components/sun/translations/nl.json | 10 + .../components/sun/translations/no.json | 10 + .../components/sun/translations/pl.json | 10 + .../components/sun/translations/pt-BR.json | 10 + .../components/sun/translations/ru.json | 10 + .../components/sun/translations/tr.json | 10 + .../components/sun/translations/zh-Hant.json | 10 + .../components/switch/translations/cs.json | 10 + .../components/switch/translations/el.json | 4 +- .../components/switch/translations/hu.json | 1 + .../components/switch/translations/pt-BR.json | 4 +- .../switch_as_x/translations/bg.json | 11 + .../switch_as_x/translations/ca.json | 16 +- .../switch_as_x/translations/cs.json | 18 ++ .../switch_as_x/translations/de.json | 16 +- .../switch_as_x/translations/el.json | 10 +- .../switch_as_x/translations/en.json | 8 +- .../switch_as_x/translations/et.json | 16 +- .../switch_as_x/translations/fr.json | 16 +- .../switch_as_x/translations/he.json | 4 +- .../switch_as_x/translations/hu.json | 16 +- .../switch_as_x/translations/id.json | 16 +- .../switch_as_x/translations/it.json | 16 +- .../switch_as_x/translations/ja.json | 10 +- .../switch_as_x/translations/nl.json | 16 +- .../switch_as_x/translations/no.json | 16 +- .../switch_as_x/translations/pl.json | 16 +- .../switch_as_x/translations/pt-BR.json | 16 +- .../switch_as_x/translations/pt.json | 2 +- .../switch_as_x/translations/ru.json | 14 +- .../switch_as_x/translations/sv.json | 8 +- .../switch_as_x/translations/tr.json | 20 ++ .../switch_as_x/translations/zh-Hans.json | 20 ++ .../switch_as_x/translations/zh-Hant.json | 16 +- .../components/switchbot/translations/fr.json | 2 +- .../components/switchbot/translations/hu.json | 2 +- .../components/switchbot/translations/it.json | 4 +- .../components/syncthing/translations/ca.json | 3 +- .../components/syncthing/translations/de.json | 3 +- .../components/syncthing/translations/el.json | 3 +- .../components/syncthing/translations/en.json | 3 +- .../components/syncthing/translations/es.json | 3 +- .../components/syncthing/translations/et.json | 3 +- .../components/syncthing/translations/fr.json | 3 +- .../components/syncthing/translations/he.json | 3 +- .../components/syncthing/translations/hu.json | 3 +- .../components/syncthing/translations/id.json | 3 +- .../components/syncthing/translations/it.json | 3 +- .../components/syncthing/translations/ja.json | 3 +- .../components/syncthing/translations/nl.json | 3 +- .../components/syncthing/translations/no.json | 3 +- .../components/syncthing/translations/pl.json | 3 +- .../syncthing/translations/pt-BR.json | 3 +- .../components/syncthing/translations/ru.json | 3 +- .../components/syncthing/translations/sv.json | 3 +- .../components/syncthing/translations/tr.json | 3 +- .../syncthing/translations/zh-Hans.json | 3 +- .../syncthing/translations/zh-Hant.json | 3 +- .../components/syncthru/translations/hu.json | 4 +- .../system_bridge/translations/ca.json | 3 +- .../system_bridge/translations/de.json | 3 +- .../system_bridge/translations/el.json | 3 +- .../system_bridge/translations/en.json | 3 +- .../system_bridge/translations/es.json | 3 +- .../system_bridge/translations/et.json | 3 +- .../system_bridge/translations/fr.json | 3 +- .../system_bridge/translations/he.json | 3 +- .../system_bridge/translations/hu.json | 3 +- .../system_bridge/translations/id.json | 3 +- .../system_bridge/translations/it.json | 3 +- .../system_bridge/translations/ja.json | 3 +- .../system_bridge/translations/nl.json | 3 +- .../system_bridge/translations/no.json | 3 +- .../system_bridge/translations/pl.json | 3 +- .../system_bridge/translations/pt-BR.json | 3 +- .../system_bridge/translations/ru.json | 3 +- .../system_bridge/translations/tr.json | 3 +- .../system_bridge/translations/zh-Hant.json | 3 +- .../system_health/translations/pt-BR.json | 2 +- .../components/tado/translations/ca.json | 4 +- .../components/tado/translations/de.json | 4 +- .../components/tado/translations/en.json | 4 +- .../components/tado/translations/et.json | 4 +- .../components/tado/translations/fr.json | 4 +- .../components/tado/translations/hu.json | 4 +- .../components/tado/translations/id.json | 4 +- .../components/tado/translations/it.json | 4 +- .../components/tado/translations/nl.json | 4 +- .../components/tado/translations/no.json | 4 +- .../components/tado/translations/pl.json | 4 +- .../components/tado/translations/pt-BR.json | 4 +- .../components/tado/translations/ru.json | 4 +- .../components/tado/translations/tr.json | 4 +- .../components/tado/translations/zh-Hant.json | 4 +- .../components/tailscale/translations/ca.json | 2 +- .../components/tailscale/translations/de.json | 2 +- .../components/tailscale/translations/et.json | 2 +- .../components/tailscale/translations/fr.json | 2 +- .../components/tailscale/translations/hu.json | 2 +- .../components/tailscale/translations/id.json | 2 +- .../components/tailscale/translations/it.json | 2 +- .../components/tailscale/translations/nl.json | 2 +- .../components/tailscale/translations/no.json | 2 +- .../components/tailscale/translations/pl.json | 2 +- .../tailscale/translations/pt-BR.json | 2 +- .../components/tailscale/translations/ru.json | 2 +- .../components/tailscale/translations/tr.json | 2 +- .../tailscale/translations/zh-Hant.json | 2 +- .../tankerkoenig/translations/bg.json | 29 +++ .../tankerkoenig/translations/ca.json | 41 ++++ .../tankerkoenig/translations/de.json | 41 ++++ .../tankerkoenig/translations/el.json | 41 ++++ .../tankerkoenig/translations/et.json | 41 ++++ .../tankerkoenig/translations/fr.json | 41 ++++ .../tankerkoenig/translations/he.json | 18 ++ .../tankerkoenig/translations/hu.json | 41 ++++ .../tankerkoenig/translations/id.json | 41 ++++ .../tankerkoenig/translations/it.json | 41 ++++ .../tankerkoenig/translations/ja.json | 41 ++++ .../tankerkoenig/translations/nl.json | 41 ++++ .../tankerkoenig/translations/no.json | 41 ++++ .../tankerkoenig/translations/pl.json | 41 ++++ .../tankerkoenig/translations/pt-BR.json | 41 ++++ .../tankerkoenig/translations/ru.json | 41 ++++ .../tankerkoenig/translations/tr.json | 41 ++++ .../tankerkoenig/translations/zh-Hant.json | 41 ++++ .../components/tautulli/translations/de.json | 30 +++ .../components/tautulli/translations/el.json | 30 +++ .../components/tautulli/translations/en.json | 55 +++-- .../components/tautulli/translations/et.json | 30 +++ .../components/tautulli/translations/fr.json | 30 +++ .../components/tautulli/translations/hu.json | 30 +++ .../components/tautulli/translations/id.json | 30 +++ .../components/tautulli/translations/it.json | 30 +++ .../components/tautulli/translations/nl.json | 28 +++ .../components/tautulli/translations/no.json | 30 +++ .../components/tautulli/translations/pl.json | 30 +++ .../tautulli/translations/pt-BR.json | 30 +++ .../components/tautulli/translations/ru.json | 30 +++ .../tautulli/translations/zh-Hant.json | 30 +++ .../tellduslive/translations/hu.json | 2 +- .../tesla_wall_connector/translations/ca.json | 10 - .../tesla_wall_connector/translations/de.json | 10 - .../tesla_wall_connector/translations/el.json | 10 - .../tesla_wall_connector/translations/en.json | 10 - .../tesla_wall_connector/translations/es.json | 10 - .../tesla_wall_connector/translations/et.json | 10 - .../tesla_wall_connector/translations/fr.json | 10 - .../tesla_wall_connector/translations/hu.json | 10 - .../tesla_wall_connector/translations/id.json | 10 - .../tesla_wall_connector/translations/it.json | 10 - .../tesla_wall_connector/translations/ja.json | 10 - .../tesla_wall_connector/translations/nl.json | 10 - .../tesla_wall_connector/translations/no.json | 10 - .../tesla_wall_connector/translations/pl.json | 10 - .../translations/pt-BR.json | 10 - .../tesla_wall_connector/translations/ru.json | 10 - .../tesla_wall_connector/translations/tr.json | 10 - .../translations/zh-Hans.json | 10 - .../translations/zh-Hant.json | 10 - .../components/threshold/translations/bg.json | 22 ++ .../components/threshold/translations/ca.json | 40 ++++ .../components/threshold/translations/de.json | 40 ++++ .../components/threshold/translations/el.json | 40 ++++ .../components/threshold/translations/en.json | 2 + .../components/threshold/translations/et.json | 40 ++++ .../components/threshold/translations/fr.json | 40 ++++ .../components/threshold/translations/hu.json | 40 ++++ .../components/threshold/translations/id.json | 40 ++++ .../components/threshold/translations/it.json | 40 ++++ .../components/threshold/translations/ja.json | 40 ++++ .../components/threshold/translations/nl.json | 40 ++++ .../components/threshold/translations/no.json | 40 ++++ .../components/threshold/translations/pl.json | 40 ++++ .../threshold/translations/pt-BR.json | 40 ++++ .../components/threshold/translations/ru.json | 40 ++++ .../components/threshold/translations/tr.json | 40 ++++ .../threshold/translations/zh-Hans.json | 40 ++++ .../threshold/translations/zh-Hant.json | 40 ++++ .../components/tod/translations/bg.json | 11 + .../components/tod/translations/ca.json | 31 +++ .../components/tod/translations/de.json | 31 +++ .../components/tod/translations/el.json | 31 +++ .../components/tod/translations/en.json | 7 +- .../components/tod/translations/et.json | 31 +++ .../components/tod/translations/fr.json | 31 +++ .../components/tod/translations/he.json | 30 +++ .../components/tod/translations/hu.json | 31 +++ .../components/tod/translations/id.json | 31 +++ .../components/tod/translations/it.json | 31 +++ .../components/tod/translations/ja.json | 31 +++ .../components/tod/translations/nl.json | 31 +++ .../components/tod/translations/no.json | 31 +++ .../components/tod/translations/pl.json | 31 +++ .../components/tod/translations/pt-BR.json | 31 +++ .../components/tod/translations/ru.json | 31 +++ .../components/tod/translations/tr.json | 31 +++ .../components/tod/translations/zh-Hans.json | 31 +++ .../components/tod/translations/zh-Hant.json | 31 +++ .../tomorrowio/translations/bg.json | 20 ++ .../tomorrowio/translations/ca.json | 33 +++ .../tomorrowio/translations/cs.json | 11 + .../tomorrowio/translations/de.json | 33 +++ .../tomorrowio/translations/el.json | 33 +++ .../tomorrowio/translations/en.json | 1 + .../tomorrowio/translations/et.json | 33 +++ .../tomorrowio/translations/fr.json | 33 +++ .../tomorrowio/translations/he.json | 20 ++ .../tomorrowio/translations/hu.json | 33 +++ .../tomorrowio/translations/id.json | 33 +++ .../tomorrowio/translations/it.json | 33 +++ .../tomorrowio/translations/ja.json | 33 +++ .../tomorrowio/translations/nl.json | 33 +++ .../tomorrowio/translations/no.json | 33 +++ .../tomorrowio/translations/pl.json | 33 +++ .../tomorrowio/translations/pt-BR.json | 33 +++ .../tomorrowio/translations/ru.json | 33 +++ .../tomorrowio/translations/sensor.bg.json | 8 + .../tomorrowio/translations/sensor.ca.json | 27 +++ .../tomorrowio/translations/sensor.de.json | 27 +++ .../tomorrowio/translations/sensor.el.json | 27 +++ .../tomorrowio/translations/sensor.et.json | 27 +++ .../tomorrowio/translations/sensor.fr.json | 27 +++ .../tomorrowio/translations/sensor.hu.json | 27 +++ .../tomorrowio/translations/sensor.id.json | 27 +++ .../tomorrowio/translations/sensor.it.json | 27 +++ .../tomorrowio/translations/sensor.ja.json | 27 +++ .../tomorrowio/translations/sensor.nl.json | 27 +++ .../tomorrowio/translations/sensor.no.json | 27 +++ .../tomorrowio/translations/sensor.pl.json | 27 +++ .../tomorrowio/translations/sensor.pt-BR.json | 27 +++ .../tomorrowio/translations/sensor.ru.json | 27 +++ .../tomorrowio/translations/sensor.tr.json | 27 +++ .../translations/sensor.zh-Hant.json | 27 +++ .../tomorrowio/translations/tr.json | 33 +++ .../tomorrowio/translations/zh-Hant.json | 33 +++ .../components/toon/translations/hu.json | 2 +- .../components/toon/translations/it.json | 2 +- .../components/tplink/translations/it.json | 2 +- .../components/tradfri/translations/hu.json | 2 +- .../trafikverket_ferry/translations/de.json | 30 +++ .../trafikverket_ferry/translations/el.json | 30 +++ .../trafikverket_ferry/translations/et.json | 30 +++ .../trafikverket_ferry/translations/fr.json | 30 +++ .../trafikverket_ferry/translations/hu.json | 30 +++ .../trafikverket_ferry/translations/id.json | 30 +++ .../trafikverket_ferry/translations/it.json | 30 +++ .../trafikverket_ferry/translations/nl.json | 30 +++ .../trafikverket_ferry/translations/no.json | 30 +++ .../trafikverket_ferry/translations/pl.json | 30 +++ .../translations/pt-BR.json | 30 +++ .../trafikverket_ferry/translations/ru.json | 30 +++ .../translations/zh-Hant.json | 30 +++ .../trafikverket_train/translations/bg.json | 24 +++ .../trafikverket_train/translations/ca.json | 32 +++ .../trafikverket_train/translations/de.json | 32 +++ .../trafikverket_train/translations/el.json | 32 +++ .../trafikverket_train/translations/en.json | 22 +- .../trafikverket_train/translations/et.json | 32 +++ .../trafikverket_train/translations/fr.json | 32 +++ .../trafikverket_train/translations/he.json | 24 +++ .../trafikverket_train/translations/hu.json | 32 +++ .../trafikverket_train/translations/id.json | 32 +++ .../trafikverket_train/translations/it.json | 32 +++ .../trafikverket_train/translations/ja.json | 32 +++ .../trafikverket_train/translations/nl.json | 32 +++ .../trafikverket_train/translations/no.json | 32 +++ .../trafikverket_train/translations/pl.json | 32 +++ .../translations/pt-BR.json | 32 +++ .../trafikverket_train/translations/ru.json | 32 +++ .../trafikverket_train/translations/tr.json | 32 +++ .../translations/zh-Hant.json | 32 +++ .../transmission/translations/hu.json | 2 +- .../tuya/translations/select.he.json | 2 +- .../tuya/translations/select.pt-BR.json | 12 +- .../twentemilieu/translations/bg.json | 3 +- .../twentemilieu/translations/ca.json | 3 +- .../twentemilieu/translations/cs.json | 3 +- .../twentemilieu/translations/da.json | 3 +- .../twentemilieu/translations/de.json | 3 +- .../twentemilieu/translations/el.json | 3 +- .../twentemilieu/translations/en.json | 3 +- .../twentemilieu/translations/es-419.json | 3 +- .../twentemilieu/translations/es.json | 3 +- .../twentemilieu/translations/et.json | 3 +- .../twentemilieu/translations/fr.json | 3 +- .../twentemilieu/translations/hu.json | 3 +- .../twentemilieu/translations/id.json | 3 +- .../twentemilieu/translations/it.json | 3 +- .../twentemilieu/translations/ja.json | 3 +- .../twentemilieu/translations/ko.json | 3 +- .../twentemilieu/translations/lb.json | 3 +- .../twentemilieu/translations/nl.json | 3 +- .../twentemilieu/translations/nn.json | 9 - .../twentemilieu/translations/no.json | 3 +- .../twentemilieu/translations/pl.json | 3 +- .../twentemilieu/translations/pt-BR.json | 3 +- .../twentemilieu/translations/ru.json | 3 +- .../twentemilieu/translations/sl.json | 3 +- .../twentemilieu/translations/sv.json | 3 +- .../twentemilieu/translations/tr.json | 3 +- .../twentemilieu/translations/uk.json | 3 +- .../twentemilieu/translations/zh-Hant.json | 3 +- .../components/twilio/translations/cs.json | 1 + .../components/twilio/translations/id.json | 2 +- .../components/twilio/translations/ja.json | 2 +- .../components/twinkly/translations/ca.json | 4 +- .../components/twinkly/translations/cs.json | 4 +- .../components/twinkly/translations/de.json | 4 +- .../components/twinkly/translations/el.json | 4 +- .../components/twinkly/translations/en.json | 4 +- .../components/twinkly/translations/es.json | 4 +- .../components/twinkly/translations/et.json | 4 +- .../components/twinkly/translations/fr.json | 4 +- .../components/twinkly/translations/hu.json | 6 +- .../components/twinkly/translations/id.json | 4 +- .../components/twinkly/translations/it.json | 4 +- .../components/twinkly/translations/ja.json | 4 +- .../components/twinkly/translations/ka.json | 4 +- .../components/twinkly/translations/ko.json | 4 +- .../components/twinkly/translations/nl.json | 4 +- .../components/twinkly/translations/no.json | 4 +- .../components/twinkly/translations/pl.json | 4 +- .../twinkly/translations/pt-BR.json | 4 +- .../components/twinkly/translations/ru.json | 4 +- .../components/twinkly/translations/tr.json | 4 +- .../components/twinkly/translations/uk.json | 4 +- .../twinkly/translations/zh-Hant.json | 4 +- .../components/unifi/translations/hu.json | 4 +- .../components/unifi/translations/it.json | 4 +- .../components/update/translations/ca.json | 7 + .../components/update/translations/cs.json | 9 + .../components/update/translations/de.json | 7 + .../components/update/translations/el.json | 7 + .../components/update/translations/es.json | 3 + .../components/update/translations/et.json | 7 + .../components/update/translations/fr.json | 7 + .../components/update/translations/he.json | 10 + .../components/update/translations/hu.json | 7 + .../components/update/translations/id.json | 7 + .../components/update/translations/it.json | 7 + .../components/update/translations/ja.json | 7 + .../components/update/translations/nl.json | 7 + .../components/update/translations/no.json | 7 + .../components/update/translations/pl.json | 7 + .../components/update/translations/pt-BR.json | 7 + .../components/update/translations/ru.json | 7 + .../components/update/translations/tr.json | 7 + .../update/translations/zh-Hans.json | 9 + .../update/translations/zh-Hant.json | 7 + .../components/upnp/translations/it.json | 8 +- .../components/uptime/translations/bg.json | 12 ++ .../components/uptime/translations/cs.json | 12 ++ .../components/uptime/translations/id.json | 13 ++ .../components/uptime/translations/it.json | 3 + .../components/uptime/translations/no.json | 13 ++ .../components/uptime/translations/pl.json | 13 ++ .../components/uptime/translations/pt-BR.json | 2 +- .../components/uptime/translations/tr.json | 13 ++ .../uptimerobot/translations/ca.json | 5 +- .../uptimerobot/translations/de.json | 5 +- .../uptimerobot/translations/el.json | 1 + .../uptimerobot/translations/et.json | 5 +- .../uptimerobot/translations/fr.json | 5 +- .../uptimerobot/translations/hu.json | 5 +- .../uptimerobot/translations/id.json | 5 +- .../uptimerobot/translations/it.json | 5 +- .../uptimerobot/translations/ja.json | 1 + .../uptimerobot/translations/nl.json | 5 +- .../uptimerobot/translations/no.json | 5 +- .../uptimerobot/translations/pl.json | 5 +- .../uptimerobot/translations/pt-BR.json | 5 +- .../uptimerobot/translations/ru.json | 5 +- .../uptimerobot/translations/tr.json | 5 +- .../uptimerobot/translations/zh-Hant.json | 5 +- .../utility_meter/translations/bg.json | 11 + .../utility_meter/translations/ca.json | 35 ++++ .../utility_meter/translations/cs.json | 35 ++++ .../utility_meter/translations/de.json | 35 ++++ .../utility_meter/translations/el.json | 35 ++++ .../utility_meter/translations/et.json | 35 ++++ .../utility_meter/translations/fr.json | 35 ++++ .../utility_meter/translations/hu.json | 35 ++++ .../utility_meter/translations/id.json | 35 ++++ .../utility_meter/translations/it.json | 35 ++++ .../utility_meter/translations/ja.json | 35 ++++ .../utility_meter/translations/nl.json | 35 ++++ .../utility_meter/translations/no.json | 35 ++++ .../utility_meter/translations/pl.json | 35 ++++ .../utility_meter/translations/pt-BR.json | 35 ++++ .../utility_meter/translations/ru.json | 35 ++++ .../utility_meter/translations/tr.json | 35 ++++ .../utility_meter/translations/zh-Hans.json | 35 ++++ .../utility_meter/translations/zh-Hant.json | 35 ++++ .../components/vallox/translations/bg.json | 6 +- .../components/vallox/translations/ca.json | 7 +- .../components/vallox/translations/cs.json | 7 - .../components/vallox/translations/de.json | 7 +- .../components/vallox/translations/el.json | 7 +- .../components/vallox/translations/en.json | 7 +- .../components/vallox/translations/es.json | 7 +- .../components/vallox/translations/et.json | 7 +- .../components/vallox/translations/fr.json | 7 +- .../components/vallox/translations/he.json | 3 +- .../components/vallox/translations/hu.json | 7 +- .../components/vallox/translations/id.json | 7 +- .../components/vallox/translations/it.json | 7 +- .../components/vallox/translations/ja.json | 7 +- .../components/vallox/translations/lv.json | 9 - .../components/vallox/translations/nl.json | 7 +- .../components/vallox/translations/no.json | 7 +- .../components/vallox/translations/pl.json | 7 +- .../components/vallox/translations/pt-BR.json | 7 +- .../components/vallox/translations/ru.json | 7 +- .../components/vallox/translations/tr.json | 7 +- .../vallox/translations/zh-Hant.json | 7 +- .../components/venstar/translations/pl.json | 2 +- .../components/vera/translations/ca.json | 5 +- .../components/vera/translations/cs.json | 4 +- .../components/vera/translations/de.json | 5 +- .../components/vera/translations/el.json | 5 +- .../components/vera/translations/en.json | 5 +- .../components/vera/translations/es-419.json | 4 +- .../components/vera/translations/es.json | 4 +- .../components/vera/translations/et.json | 5 +- .../components/vera/translations/fr.json | 5 +- .../components/vera/translations/hu.json | 5 +- .../components/vera/translations/id.json | 5 +- .../components/vera/translations/it.json | 5 +- .../components/vera/translations/ja.json | 5 +- .../components/vera/translations/ko.json | 4 +- .../components/vera/translations/lb.json | 4 +- .../components/vera/translations/nl.json | 5 +- .../components/vera/translations/no.json | 5 +- .../components/vera/translations/pl.json | 5 +- .../components/vera/translations/pt-BR.json | 5 +- .../components/vera/translations/ru.json | 5 +- .../components/vera/translations/sl.json | 4 +- .../components/vera/translations/tr.json | 5 +- .../components/vera/translations/uk.json | 4 +- .../components/vera/translations/zh-Hant.json | 5 +- .../components/verisure/translations/hu.json | 4 +- .../components/version/translations/he.json | 11 + .../components/vicare/translations/fr.json | 2 +- .../components/vicare/translations/hu.json | 2 +- .../components/vizio/translations/hu.json | 2 +- .../vlc_telnet/translations/hu.json | 2 +- .../components/vulcan/translations/bg.json | 30 +++ .../components/vulcan/translations/ca.json | 69 ++++++ .../components/vulcan/translations/de.json | 69 ++++++ .../components/vulcan/translations/el.json | 69 ++++++ .../components/vulcan/translations/en.json | 130 ++++++------ .../components/vulcan/translations/et.json | 69 ++++++ .../components/vulcan/translations/fr.json | 69 ++++++ .../components/vulcan/translations/hu.json | 69 ++++++ .../components/vulcan/translations/id.json | 69 ++++++ .../components/vulcan/translations/it.json | 69 ++++++ .../components/vulcan/translations/ja.json | 69 ++++++ .../components/vulcan/translations/nl.json | 69 ++++++ .../components/vulcan/translations/no.json | 69 ++++++ .../components/vulcan/translations/pl.json | 69 ++++++ .../components/vulcan/translations/pt-BR.json | 69 ++++++ .../components/vulcan/translations/ru.json | 69 ++++++ .../components/vulcan/translations/tr.json | 69 ++++++ .../vulcan/translations/zh-Hant.json | 69 ++++++ .../components/wallbox/translations/bg.json | 3 +- .../components/wallbox/translations/ca.json | 3 +- .../components/wallbox/translations/de.json | 3 +- .../components/wallbox/translations/el.json | 3 +- .../components/wallbox/translations/en.json | 3 +- .../components/wallbox/translations/es.json | 3 +- .../components/wallbox/translations/et.json | 3 +- .../components/wallbox/translations/fr.json | 3 +- .../components/wallbox/translations/he.json | 3 +- .../components/wallbox/translations/hu.json | 3 +- .../components/wallbox/translations/id.json | 3 +- .../components/wallbox/translations/it.json | 3 +- .../components/wallbox/translations/ja.json | 3 +- .../components/wallbox/translations/nl.json | 3 +- .../components/wallbox/translations/no.json | 3 +- .../components/wallbox/translations/pl.json | 3 +- .../wallbox/translations/pt-BR.json | 3 +- .../components/wallbox/translations/ru.json | 3 +- .../components/wallbox/translations/tr.json | 3 +- .../wallbox/translations/zh-Hant.json | 3 +- .../waze_travel_time/translations/hu.json | 2 +- .../components/webostv/translations/hu.json | 47 +++++ .../components/wilight/translations/ca.json | 2 +- .../components/wilight/translations/de.json | 2 +- .../components/wilight/translations/en.json | 2 +- .../components/wilight/translations/et.json | 2 +- .../components/wilight/translations/fr.json | 2 +- .../components/wilight/translations/hu.json | 2 +- .../components/wilight/translations/id.json | 2 +- .../components/wilight/translations/it.json | 2 +- .../components/wilight/translations/nl.json | 2 +- .../components/wilight/translations/no.json | 2 +- .../components/wilight/translations/pl.json | 2 +- .../wilight/translations/pt-BR.json | 2 +- .../components/wilight/translations/ru.json | 2 +- .../components/wilight/translations/tr.json | 2 +- .../wilight/translations/zh-Hant.json | 2 +- .../components/withings/translations/hu.json | 4 +- .../components/withings/translations/it.json | 2 +- .../components/wiz/translations/hu.json | 2 +- .../wolflink/translations/sensor.hu.json | 2 +- .../components/xbox/translations/hu.json | 2 +- .../components/xbox/translations/it.json | 2 +- .../xiaomi_aqara/translations/ca.json | 6 +- .../xiaomi_aqara/translations/de.json | 6 +- .../xiaomi_aqara/translations/en.json | 6 +- .../xiaomi_aqara/translations/et.json | 6 +- .../xiaomi_aqara/translations/fr.json | 6 +- .../xiaomi_aqara/translations/he.json | 4 +- .../xiaomi_aqara/translations/hu.json | 8 +- .../xiaomi_aqara/translations/id.json | 6 +- .../xiaomi_aqara/translations/it.json | 6 +- .../xiaomi_aqara/translations/nl.json | 6 +- .../xiaomi_aqara/translations/no.json | 6 +- .../xiaomi_aqara/translations/pl.json | 6 +- .../xiaomi_aqara/translations/pt-BR.json | 8 +- .../xiaomi_aqara/translations/ru.json | 6 +- .../xiaomi_aqara/translations/tr.json | 6 +- .../xiaomi_aqara/translations/zh-Hant.json | 6 +- .../xiaomi_miio/translations/he.json | 2 +- .../xiaomi_miio/translations/hu.json | 2 +- .../xiaomi_miio/translations/pt-BR.json | 2 +- .../yale_smart_alarm/translations/hu.json | 4 +- .../components/yeelight/translations/cs.json | 2 +- .../components/yeelight/translations/fr.json | 2 +- .../components/youless/translations/hu.json | 2 +- .../components/zha/translations/cs.json | 2 +- .../components/zwave_js/translations/bg.json | 3 + .../components/zwave_js/translations/ca.json | 4 + .../components/zwave_js/translations/de.json | 4 + .../components/zwave_js/translations/el.json | 4 + .../components/zwave_js/translations/en.json | 5 +- .../components/zwave_js/translations/et.json | 4 + .../components/zwave_js/translations/fr.json | 4 + .../components/zwave_js/translations/hu.json | 6 +- .../components/zwave_js/translations/id.json | 6 +- .../components/zwave_js/translations/it.json | 4 + .../components/zwave_js/translations/ja.json | 4 + .../components/zwave_js/translations/nl.json | 4 + .../components/zwave_js/translations/no.json | 4 + .../components/zwave_js/translations/pl.json | 4 + .../zwave_js/translations/pt-BR.json | 4 + .../components/zwave_js/translations/ru.json | 4 + .../components/zwave_js/translations/tr.json | 4 + .../zwave_js/translations/zh-Hant.json | 4 + .../components/zwave_me/translations/ca.json | 4 +- .../components/zwave_me/translations/de.json | 6 +- .../components/zwave_me/translations/en.json | 4 +- .../components/zwave_me/translations/et.json | 4 +- .../components/zwave_me/translations/fr.json | 4 +- .../components/zwave_me/translations/he.json | 2 +- .../components/zwave_me/translations/hu.json | 4 +- .../components/zwave_me/translations/id.json | 4 +- .../components/zwave_me/translations/it.json | 4 +- .../components/zwave_me/translations/nl.json | 4 +- .../components/zwave_me/translations/no.json | 4 +- .../components/zwave_me/translations/pl.json | 4 +- .../zwave_me/translations/pt-BR.json | 4 +- .../components/zwave_me/translations/ru.json | 4 +- .../components/zwave_me/translations/tr.json | 4 +- .../zwave_me/translations/zh-Hant.json | 4 +- 1917 files changed, 23024 insertions(+), 3546 deletions(-) create mode 100644 homeassistant/components/airzone/translations/cs.json create mode 100644 homeassistant/components/airzone/translations/tr.json delete mode 100644 homeassistant/components/crownstone/translations/ko.json create mode 100644 homeassistant/components/deluge/translations/bg.json create mode 100644 homeassistant/components/deluge/translations/ca.json create mode 100644 homeassistant/components/deluge/translations/cs.json create mode 100644 homeassistant/components/deluge/translations/de.json create mode 100644 homeassistant/components/deluge/translations/el.json create mode 100644 homeassistant/components/deluge/translations/et.json create mode 100644 homeassistant/components/deluge/translations/fr.json create mode 100644 homeassistant/components/deluge/translations/he.json create mode 100644 homeassistant/components/deluge/translations/hu.json create mode 100644 homeassistant/components/deluge/translations/id.json create mode 100644 homeassistant/components/deluge/translations/it.json create mode 100644 homeassistant/components/deluge/translations/ja.json create mode 100644 homeassistant/components/deluge/translations/nl.json create mode 100644 homeassistant/components/deluge/translations/no.json create mode 100644 homeassistant/components/deluge/translations/pl.json create mode 100644 homeassistant/components/deluge/translations/pt-BR.json create mode 100644 homeassistant/components/deluge/translations/ru.json create mode 100644 homeassistant/components/deluge/translations/tr.json create mode 100644 homeassistant/components/deluge/translations/zh-Hant.json create mode 100644 homeassistant/components/derivative/translations/bg.json create mode 100644 homeassistant/components/derivative/translations/ca.json create mode 100644 homeassistant/components/derivative/translations/cs.json create mode 100644 homeassistant/components/derivative/translations/de.json create mode 100644 homeassistant/components/derivative/translations/el.json create mode 100644 homeassistant/components/derivative/translations/et.json create mode 100644 homeassistant/components/derivative/translations/fr.json create mode 100644 homeassistant/components/derivative/translations/he.json create mode 100644 homeassistant/components/derivative/translations/hu.json create mode 100644 homeassistant/components/derivative/translations/id.json create mode 100644 homeassistant/components/derivative/translations/it.json create mode 100644 homeassistant/components/derivative/translations/ja.json create mode 100644 homeassistant/components/derivative/translations/nl.json create mode 100644 homeassistant/components/derivative/translations/no.json create mode 100644 homeassistant/components/derivative/translations/pl.json create mode 100644 homeassistant/components/derivative/translations/pt-BR.json create mode 100644 homeassistant/components/derivative/translations/ru.json create mode 100644 homeassistant/components/derivative/translations/sv.json create mode 100644 homeassistant/components/derivative/translations/tr.json create mode 100644 homeassistant/components/derivative/translations/zh-Hans.json create mode 100644 homeassistant/components/derivative/translations/zh-Hant.json create mode 100644 homeassistant/components/discord/translations/bg.json create mode 100644 homeassistant/components/discord/translations/ca.json create mode 100644 homeassistant/components/discord/translations/de.json create mode 100644 homeassistant/components/discord/translations/el.json create mode 100644 homeassistant/components/discord/translations/et.json create mode 100644 homeassistant/components/discord/translations/fr.json create mode 100644 homeassistant/components/discord/translations/he.json create mode 100644 homeassistant/components/discord/translations/hu.json create mode 100644 homeassistant/components/discord/translations/id.json create mode 100644 homeassistant/components/discord/translations/it.json create mode 100644 homeassistant/components/discord/translations/ja.json create mode 100644 homeassistant/components/discord/translations/nl.json create mode 100644 homeassistant/components/discord/translations/no.json create mode 100644 homeassistant/components/discord/translations/pl.json create mode 100644 homeassistant/components/discord/translations/pt-BR.json create mode 100644 homeassistant/components/discord/translations/ru.json create mode 100644 homeassistant/components/discord/translations/tr.json create mode 100644 homeassistant/components/discord/translations/zh-Hant.json create mode 100644 homeassistant/components/dlna_dms/translations/cs.json create mode 100644 homeassistant/components/fibaro/translations/bg.json create mode 100644 homeassistant/components/fibaro/translations/ca.json create mode 100644 homeassistant/components/fibaro/translations/de.json create mode 100644 homeassistant/components/fibaro/translations/el.json create mode 100644 homeassistant/components/fibaro/translations/et.json create mode 100644 homeassistant/components/fibaro/translations/fr.json create mode 100644 homeassistant/components/fibaro/translations/he.json create mode 100644 homeassistant/components/fibaro/translations/hu.json create mode 100644 homeassistant/components/fibaro/translations/id.json create mode 100644 homeassistant/components/fibaro/translations/it.json create mode 100644 homeassistant/components/fibaro/translations/ja.json create mode 100644 homeassistant/components/fibaro/translations/nl.json create mode 100644 homeassistant/components/fibaro/translations/no.json create mode 100644 homeassistant/components/fibaro/translations/pl.json create mode 100644 homeassistant/components/fibaro/translations/pt-BR.json create mode 100644 homeassistant/components/fibaro/translations/ru.json create mode 100644 homeassistant/components/fibaro/translations/tr.json create mode 100644 homeassistant/components/fibaro/translations/zh-Hant.json create mode 100644 homeassistant/components/filesize/translations/bg.json create mode 100644 homeassistant/components/filesize/translations/ca.json create mode 100644 homeassistant/components/filesize/translations/de.json create mode 100644 homeassistant/components/filesize/translations/el.json create mode 100644 homeassistant/components/filesize/translations/et.json create mode 100644 homeassistant/components/filesize/translations/fr.json create mode 100644 homeassistant/components/filesize/translations/he.json create mode 100644 homeassistant/components/filesize/translations/hu.json create mode 100644 homeassistant/components/filesize/translations/id.json create mode 100644 homeassistant/components/filesize/translations/it.json create mode 100644 homeassistant/components/filesize/translations/ja.json create mode 100644 homeassistant/components/filesize/translations/nl.json create mode 100644 homeassistant/components/filesize/translations/no.json create mode 100644 homeassistant/components/filesize/translations/pl.json create mode 100644 homeassistant/components/filesize/translations/pt-BR.json create mode 100644 homeassistant/components/filesize/translations/ru.json create mode 100644 homeassistant/components/filesize/translations/tr.json create mode 100644 homeassistant/components/filesize/translations/zh-Hant.json create mode 100644 homeassistant/components/generic/translations/bg.json create mode 100644 homeassistant/components/generic/translations/ca.json create mode 100644 homeassistant/components/generic/translations/cs.json create mode 100644 homeassistant/components/generic/translations/de.json create mode 100644 homeassistant/components/generic/translations/el.json create mode 100644 homeassistant/components/generic/translations/et.json create mode 100644 homeassistant/components/generic/translations/fr.json create mode 100644 homeassistant/components/generic/translations/he.json create mode 100644 homeassistant/components/generic/translations/hu.json create mode 100644 homeassistant/components/generic/translations/id.json create mode 100644 homeassistant/components/generic/translations/it.json create mode 100644 homeassistant/components/generic/translations/ja.json create mode 100644 homeassistant/components/generic/translations/nl.json create mode 100644 homeassistant/components/generic/translations/no.json create mode 100644 homeassistant/components/generic/translations/pl.json create mode 100644 homeassistant/components/generic/translations/pt-BR.json create mode 100644 homeassistant/components/generic/translations/ru.json create mode 100644 homeassistant/components/generic/translations/tr.json create mode 100644 homeassistant/components/generic/translations/zh-Hant.json create mode 100644 homeassistant/components/google/translations/bg.json create mode 100644 homeassistant/components/google/translations/ca.json create mode 100644 homeassistant/components/google/translations/cs.json create mode 100644 homeassistant/components/google/translations/de.json create mode 100644 homeassistant/components/google/translations/el.json create mode 100644 homeassistant/components/google/translations/es.json create mode 100644 homeassistant/components/google/translations/et.json create mode 100644 homeassistant/components/google/translations/fr.json create mode 100644 homeassistant/components/google/translations/he.json create mode 100644 homeassistant/components/google/translations/hu.json create mode 100644 homeassistant/components/google/translations/id.json create mode 100644 homeassistant/components/google/translations/it.json create mode 100644 homeassistant/components/google/translations/ja.json create mode 100644 homeassistant/components/google/translations/nl.json create mode 100644 homeassistant/components/google/translations/no.json create mode 100644 homeassistant/components/google/translations/pl.json create mode 100644 homeassistant/components/google/translations/pt-BR.json create mode 100644 homeassistant/components/google/translations/ru.json create mode 100644 homeassistant/components/google/translations/tr.json create mode 100644 homeassistant/components/google/translations/zh-Hant.json rename homeassistant/components/{vallox/translations/is.json => integration/translations/bg.json} (73%) create mode 100644 homeassistant/components/integration/translations/ca.json create mode 100644 homeassistant/components/integration/translations/cs.json create mode 100644 homeassistant/components/integration/translations/de.json create mode 100644 homeassistant/components/integration/translations/el.json create mode 100644 homeassistant/components/integration/translations/et.json create mode 100644 homeassistant/components/integration/translations/fr.json create mode 100644 homeassistant/components/integration/translations/he.json create mode 100644 homeassistant/components/integration/translations/hu.json create mode 100644 homeassistant/components/integration/translations/id.json create mode 100644 homeassistant/components/integration/translations/it.json create mode 100644 homeassistant/components/integration/translations/ja.json create mode 100644 homeassistant/components/integration/translations/nl.json create mode 100644 homeassistant/components/integration/translations/no.json create mode 100644 homeassistant/components/integration/translations/pl.json create mode 100644 homeassistant/components/integration/translations/pt-BR.json create mode 100644 homeassistant/components/integration/translations/ru.json create mode 100644 homeassistant/components/integration/translations/tr.json create mode 100644 homeassistant/components/integration/translations/zh-Hans.json create mode 100644 homeassistant/components/integration/translations/zh-Hant.json create mode 100644 homeassistant/components/kaleidescape/translations/cs.json create mode 100644 homeassistant/components/meater/translations/bg.json create mode 100644 homeassistant/components/meater/translations/ca.json create mode 100644 homeassistant/components/meater/translations/cs.json create mode 100644 homeassistant/components/meater/translations/de.json create mode 100644 homeassistant/components/meater/translations/el.json create mode 100644 homeassistant/components/meater/translations/et.json create mode 100644 homeassistant/components/meater/translations/fr.json create mode 100644 homeassistant/components/meater/translations/he.json create mode 100644 homeassistant/components/meater/translations/hu.json create mode 100644 homeassistant/components/meater/translations/id.json create mode 100644 homeassistant/components/meater/translations/it.json create mode 100644 homeassistant/components/meater/translations/ja.json create mode 100644 homeassistant/components/meater/translations/nl.json create mode 100644 homeassistant/components/meater/translations/no.json create mode 100644 homeassistant/components/meater/translations/pl.json create mode 100644 homeassistant/components/meater/translations/pt-BR.json create mode 100644 homeassistant/components/meater/translations/ru.json create mode 100644 homeassistant/components/meater/translations/tr.json create mode 100644 homeassistant/components/meater/translations/zh-Hant.json delete mode 100644 homeassistant/components/meteoclimatic/translations/gl.json delete mode 100644 homeassistant/components/meteoclimatic/translations/pt.json rename homeassistant/components/{vallox/translations/sk.json => min_max/translations/bg.json} (73%) create mode 100644 homeassistant/components/min_max/translations/ca.json create mode 100644 homeassistant/components/min_max/translations/cs.json create mode 100644 homeassistant/components/min_max/translations/de.json create mode 100644 homeassistant/components/min_max/translations/el.json create mode 100644 homeassistant/components/min_max/translations/et.json create mode 100644 homeassistant/components/min_max/translations/fr.json create mode 100644 homeassistant/components/min_max/translations/he.json create mode 100644 homeassistant/components/min_max/translations/hu.json create mode 100644 homeassistant/components/min_max/translations/id.json create mode 100644 homeassistant/components/min_max/translations/it.json create mode 100644 homeassistant/components/min_max/translations/ja.json create mode 100644 homeassistant/components/min_max/translations/nl.json create mode 100644 homeassistant/components/min_max/translations/no.json create mode 100644 homeassistant/components/min_max/translations/pl.json create mode 100644 homeassistant/components/min_max/translations/pt-BR.json create mode 100644 homeassistant/components/min_max/translations/ru.json create mode 100644 homeassistant/components/min_max/translations/sv.json create mode 100644 homeassistant/components/min_max/translations/tr.json create mode 100644 homeassistant/components/min_max/translations/zh-Hans.json create mode 100644 homeassistant/components/min_max/translations/zh-Hant.json create mode 100644 homeassistant/components/moon/translations/cs.json create mode 100644 homeassistant/components/peco/translations/bg.json create mode 100644 homeassistant/components/peco/translations/ca.json create mode 100644 homeassistant/components/peco/translations/cs.json create mode 100644 homeassistant/components/peco/translations/de.json create mode 100644 homeassistant/components/peco/translations/el.json create mode 100644 homeassistant/components/peco/translations/et.json create mode 100644 homeassistant/components/peco/translations/fr.json create mode 100644 homeassistant/components/peco/translations/he.json create mode 100644 homeassistant/components/peco/translations/hu.json create mode 100644 homeassistant/components/peco/translations/id.json create mode 100644 homeassistant/components/peco/translations/it.json create mode 100644 homeassistant/components/peco/translations/ja.json create mode 100644 homeassistant/components/peco/translations/nl.json create mode 100644 homeassistant/components/peco/translations/no.json create mode 100644 homeassistant/components/peco/translations/pl.json create mode 100644 homeassistant/components/peco/translations/pt-BR.json create mode 100644 homeassistant/components/peco/translations/ru.json create mode 100644 homeassistant/components/peco/translations/tr.json create mode 100644 homeassistant/components/peco/translations/zh-Hant.json create mode 100644 homeassistant/components/pure_energie/translations/cs.json create mode 100644 homeassistant/components/qnap_qsw/translations/de.json create mode 100644 homeassistant/components/qnap_qsw/translations/el.json create mode 100644 homeassistant/components/qnap_qsw/translations/et.json create mode 100644 homeassistant/components/qnap_qsw/translations/fr.json create mode 100644 homeassistant/components/qnap_qsw/translations/hu.json create mode 100644 homeassistant/components/qnap_qsw/translations/id.json create mode 100644 homeassistant/components/qnap_qsw/translations/it.json create mode 100644 homeassistant/components/qnap_qsw/translations/nl.json create mode 100644 homeassistant/components/qnap_qsw/translations/no.json create mode 100644 homeassistant/components/qnap_qsw/translations/pl.json create mode 100644 homeassistant/components/qnap_qsw/translations/pt-BR.json create mode 100644 homeassistant/components/qnap_qsw/translations/ru.json create mode 100644 homeassistant/components/qnap_qsw/translations/zh-Hant.json create mode 100644 homeassistant/components/radio_browser/translations/cs.json create mode 100644 homeassistant/components/sabnzbd/translations/de.json create mode 100644 homeassistant/components/sabnzbd/translations/el.json create mode 100644 homeassistant/components/sabnzbd/translations/et.json create mode 100644 homeassistant/components/sabnzbd/translations/fr.json create mode 100644 homeassistant/components/sabnzbd/translations/hu.json create mode 100644 homeassistant/components/sabnzbd/translations/id.json create mode 100644 homeassistant/components/sabnzbd/translations/it.json create mode 100644 homeassistant/components/sabnzbd/translations/nl.json create mode 100644 homeassistant/components/sabnzbd/translations/no.json create mode 100644 homeassistant/components/sabnzbd/translations/pl.json create mode 100644 homeassistant/components/sabnzbd/translations/pt-BR.json create mode 100644 homeassistant/components/sabnzbd/translations/zh-Hant.json create mode 100644 homeassistant/components/season/translations/cs.json create mode 100644 homeassistant/components/senz/translations/bg.json create mode 100644 homeassistant/components/senz/translations/ca.json create mode 100644 homeassistant/components/senz/translations/de.json create mode 100644 homeassistant/components/senz/translations/el.json create mode 100644 homeassistant/components/senz/translations/et.json create mode 100644 homeassistant/components/senz/translations/fr.json create mode 100644 homeassistant/components/senz/translations/he.json create mode 100644 homeassistant/components/senz/translations/hu.json create mode 100644 homeassistant/components/senz/translations/id.json create mode 100644 homeassistant/components/senz/translations/it.json create mode 100644 homeassistant/components/senz/translations/ja.json create mode 100644 homeassistant/components/senz/translations/nl.json create mode 100644 homeassistant/components/senz/translations/no.json create mode 100644 homeassistant/components/senz/translations/pl.json create mode 100644 homeassistant/components/senz/translations/pt-BR.json create mode 100644 homeassistant/components/senz/translations/ru.json create mode 100644 homeassistant/components/senz/translations/tr.json create mode 100644 homeassistant/components/senz/translations/zh-Hant.json create mode 100644 homeassistant/components/slimproto/translations/de.json create mode 100644 homeassistant/components/slimproto/translations/el.json create mode 100644 homeassistant/components/slimproto/translations/et.json create mode 100644 homeassistant/components/slimproto/translations/fr.json create mode 100644 homeassistant/components/slimproto/translations/hu.json create mode 100644 homeassistant/components/slimproto/translations/id.json create mode 100644 homeassistant/components/slimproto/translations/it.json create mode 100644 homeassistant/components/slimproto/translations/nl.json create mode 100644 homeassistant/components/slimproto/translations/no.json create mode 100644 homeassistant/components/slimproto/translations/pl.json create mode 100644 homeassistant/components/slimproto/translations/pt-BR.json create mode 100644 homeassistant/components/slimproto/translations/zh-Hant.json create mode 100644 homeassistant/components/solax/translations/es.json create mode 100644 homeassistant/components/sql/translations/ca.json create mode 100644 homeassistant/components/sql/translations/de.json create mode 100644 homeassistant/components/sql/translations/el.json create mode 100644 homeassistant/components/sql/translations/et.json create mode 100644 homeassistant/components/sql/translations/fr.json create mode 100644 homeassistant/components/sql/translations/hu.json create mode 100644 homeassistant/components/sql/translations/id.json create mode 100644 homeassistant/components/sql/translations/it.json create mode 100644 homeassistant/components/sql/translations/nl.json create mode 100644 homeassistant/components/sql/translations/no.json create mode 100644 homeassistant/components/sql/translations/pl.json create mode 100644 homeassistant/components/sql/translations/pt-BR.json create mode 100644 homeassistant/components/sql/translations/ru.json create mode 100644 homeassistant/components/sql/translations/tr.json create mode 100644 homeassistant/components/sql/translations/zh-Hant.json create mode 100644 homeassistant/components/steam_online/translations/de.json create mode 100644 homeassistant/components/steam_online/translations/el.json create mode 100644 homeassistant/components/steam_online/translations/et.json create mode 100644 homeassistant/components/steam_online/translations/fr.json create mode 100644 homeassistant/components/steam_online/translations/hu.json create mode 100644 homeassistant/components/steam_online/translations/id.json create mode 100644 homeassistant/components/steam_online/translations/it.json create mode 100644 homeassistant/components/steam_online/translations/nl.json create mode 100644 homeassistant/components/steam_online/translations/no.json create mode 100644 homeassistant/components/steam_online/translations/pl.json create mode 100644 homeassistant/components/steam_online/translations/pt-BR.json create mode 100644 homeassistant/components/steam_online/translations/ru.json create mode 100644 homeassistant/components/steam_online/translations/zh-Hant.json create mode 100644 homeassistant/components/switch_as_x/translations/bg.json create mode 100644 homeassistant/components/switch_as_x/translations/cs.json create mode 100644 homeassistant/components/switch_as_x/translations/tr.json create mode 100644 homeassistant/components/switch_as_x/translations/zh-Hans.json create mode 100644 homeassistant/components/tankerkoenig/translations/bg.json create mode 100644 homeassistant/components/tankerkoenig/translations/ca.json create mode 100644 homeassistant/components/tankerkoenig/translations/de.json create mode 100644 homeassistant/components/tankerkoenig/translations/el.json create mode 100644 homeassistant/components/tankerkoenig/translations/et.json create mode 100644 homeassistant/components/tankerkoenig/translations/fr.json create mode 100644 homeassistant/components/tankerkoenig/translations/he.json create mode 100644 homeassistant/components/tankerkoenig/translations/hu.json create mode 100644 homeassistant/components/tankerkoenig/translations/id.json create mode 100644 homeassistant/components/tankerkoenig/translations/it.json create mode 100644 homeassistant/components/tankerkoenig/translations/ja.json create mode 100644 homeassistant/components/tankerkoenig/translations/nl.json create mode 100644 homeassistant/components/tankerkoenig/translations/no.json create mode 100644 homeassistant/components/tankerkoenig/translations/pl.json create mode 100644 homeassistant/components/tankerkoenig/translations/pt-BR.json create mode 100644 homeassistant/components/tankerkoenig/translations/ru.json create mode 100644 homeassistant/components/tankerkoenig/translations/tr.json create mode 100644 homeassistant/components/tankerkoenig/translations/zh-Hant.json create mode 100644 homeassistant/components/tautulli/translations/de.json create mode 100644 homeassistant/components/tautulli/translations/el.json create mode 100644 homeassistant/components/tautulli/translations/et.json create mode 100644 homeassistant/components/tautulli/translations/fr.json create mode 100644 homeassistant/components/tautulli/translations/hu.json create mode 100644 homeassistant/components/tautulli/translations/id.json create mode 100644 homeassistant/components/tautulli/translations/it.json create mode 100644 homeassistant/components/tautulli/translations/nl.json create mode 100644 homeassistant/components/tautulli/translations/no.json create mode 100644 homeassistant/components/tautulli/translations/pl.json create mode 100644 homeassistant/components/tautulli/translations/pt-BR.json create mode 100644 homeassistant/components/tautulli/translations/ru.json create mode 100644 homeassistant/components/tautulli/translations/zh-Hant.json create mode 100644 homeassistant/components/threshold/translations/bg.json create mode 100644 homeassistant/components/threshold/translations/ca.json create mode 100644 homeassistant/components/threshold/translations/de.json create mode 100644 homeassistant/components/threshold/translations/el.json create mode 100644 homeassistant/components/threshold/translations/et.json create mode 100644 homeassistant/components/threshold/translations/fr.json create mode 100644 homeassistant/components/threshold/translations/hu.json create mode 100644 homeassistant/components/threshold/translations/id.json create mode 100644 homeassistant/components/threshold/translations/it.json create mode 100644 homeassistant/components/threshold/translations/ja.json create mode 100644 homeassistant/components/threshold/translations/nl.json create mode 100644 homeassistant/components/threshold/translations/no.json create mode 100644 homeassistant/components/threshold/translations/pl.json create mode 100644 homeassistant/components/threshold/translations/pt-BR.json create mode 100644 homeassistant/components/threshold/translations/ru.json create mode 100644 homeassistant/components/threshold/translations/tr.json create mode 100644 homeassistant/components/threshold/translations/zh-Hans.json create mode 100644 homeassistant/components/threshold/translations/zh-Hant.json create mode 100644 homeassistant/components/tod/translations/bg.json create mode 100644 homeassistant/components/tod/translations/ca.json create mode 100644 homeassistant/components/tod/translations/de.json create mode 100644 homeassistant/components/tod/translations/el.json create mode 100644 homeassistant/components/tod/translations/et.json create mode 100644 homeassistant/components/tod/translations/fr.json create mode 100644 homeassistant/components/tod/translations/he.json create mode 100644 homeassistant/components/tod/translations/hu.json create mode 100644 homeassistant/components/tod/translations/id.json create mode 100644 homeassistant/components/tod/translations/it.json create mode 100644 homeassistant/components/tod/translations/ja.json create mode 100644 homeassistant/components/tod/translations/nl.json create mode 100644 homeassistant/components/tod/translations/no.json create mode 100644 homeassistant/components/tod/translations/pl.json create mode 100644 homeassistant/components/tod/translations/pt-BR.json create mode 100644 homeassistant/components/tod/translations/ru.json create mode 100644 homeassistant/components/tod/translations/tr.json create mode 100644 homeassistant/components/tod/translations/zh-Hans.json create mode 100644 homeassistant/components/tod/translations/zh-Hant.json create mode 100644 homeassistant/components/tomorrowio/translations/bg.json create mode 100644 homeassistant/components/tomorrowio/translations/ca.json create mode 100644 homeassistant/components/tomorrowio/translations/cs.json create mode 100644 homeassistant/components/tomorrowio/translations/de.json create mode 100644 homeassistant/components/tomorrowio/translations/el.json create mode 100644 homeassistant/components/tomorrowio/translations/et.json create mode 100644 homeassistant/components/tomorrowio/translations/fr.json create mode 100644 homeassistant/components/tomorrowio/translations/he.json create mode 100644 homeassistant/components/tomorrowio/translations/hu.json create mode 100644 homeassistant/components/tomorrowio/translations/id.json create mode 100644 homeassistant/components/tomorrowio/translations/it.json create mode 100644 homeassistant/components/tomorrowio/translations/ja.json create mode 100644 homeassistant/components/tomorrowio/translations/nl.json create mode 100644 homeassistant/components/tomorrowio/translations/no.json create mode 100644 homeassistant/components/tomorrowio/translations/pl.json create mode 100644 homeassistant/components/tomorrowio/translations/pt-BR.json create mode 100644 homeassistant/components/tomorrowio/translations/ru.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.bg.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.ca.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.de.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.el.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.et.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.fr.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.hu.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.id.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.it.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.ja.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.nl.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.no.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.pl.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.pt-BR.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.ru.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.tr.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.zh-Hant.json create mode 100644 homeassistant/components/tomorrowio/translations/tr.json create mode 100644 homeassistant/components/tomorrowio/translations/zh-Hant.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/de.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/el.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/et.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/fr.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/hu.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/id.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/it.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/nl.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/no.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/pl.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/pt-BR.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/ru.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/zh-Hant.json create mode 100644 homeassistant/components/trafikverket_train/translations/bg.json create mode 100644 homeassistant/components/trafikverket_train/translations/ca.json create mode 100644 homeassistant/components/trafikverket_train/translations/de.json create mode 100644 homeassistant/components/trafikverket_train/translations/el.json create mode 100644 homeassistant/components/trafikverket_train/translations/et.json create mode 100644 homeassistant/components/trafikverket_train/translations/fr.json create mode 100644 homeassistant/components/trafikverket_train/translations/he.json create mode 100644 homeassistant/components/trafikverket_train/translations/hu.json create mode 100644 homeassistant/components/trafikverket_train/translations/id.json create mode 100644 homeassistant/components/trafikverket_train/translations/it.json create mode 100644 homeassistant/components/trafikverket_train/translations/ja.json create mode 100644 homeassistant/components/trafikverket_train/translations/nl.json create mode 100644 homeassistant/components/trafikverket_train/translations/no.json create mode 100644 homeassistant/components/trafikverket_train/translations/pl.json create mode 100644 homeassistant/components/trafikverket_train/translations/pt-BR.json create mode 100644 homeassistant/components/trafikverket_train/translations/ru.json create mode 100644 homeassistant/components/trafikverket_train/translations/tr.json create mode 100644 homeassistant/components/trafikverket_train/translations/zh-Hant.json delete mode 100644 homeassistant/components/twentemilieu/translations/nn.json create mode 100644 homeassistant/components/update/translations/cs.json create mode 100644 homeassistant/components/update/translations/es.json create mode 100644 homeassistant/components/update/translations/he.json create mode 100644 homeassistant/components/update/translations/zh-Hans.json create mode 100644 homeassistant/components/uptime/translations/bg.json create mode 100644 homeassistant/components/uptime/translations/cs.json create mode 100644 homeassistant/components/uptime/translations/id.json create mode 100644 homeassistant/components/uptime/translations/no.json create mode 100644 homeassistant/components/uptime/translations/pl.json create mode 100644 homeassistant/components/uptime/translations/tr.json create mode 100644 homeassistant/components/utility_meter/translations/bg.json create mode 100644 homeassistant/components/utility_meter/translations/ca.json create mode 100644 homeassistant/components/utility_meter/translations/cs.json create mode 100644 homeassistant/components/utility_meter/translations/de.json create mode 100644 homeassistant/components/utility_meter/translations/el.json create mode 100644 homeassistant/components/utility_meter/translations/et.json create mode 100644 homeassistant/components/utility_meter/translations/fr.json create mode 100644 homeassistant/components/utility_meter/translations/hu.json create mode 100644 homeassistant/components/utility_meter/translations/id.json create mode 100644 homeassistant/components/utility_meter/translations/it.json create mode 100644 homeassistant/components/utility_meter/translations/ja.json create mode 100644 homeassistant/components/utility_meter/translations/nl.json create mode 100644 homeassistant/components/utility_meter/translations/no.json create mode 100644 homeassistant/components/utility_meter/translations/pl.json create mode 100644 homeassistant/components/utility_meter/translations/pt-BR.json create mode 100644 homeassistant/components/utility_meter/translations/ru.json create mode 100644 homeassistant/components/utility_meter/translations/tr.json create mode 100644 homeassistant/components/utility_meter/translations/zh-Hans.json create mode 100644 homeassistant/components/utility_meter/translations/zh-Hant.json delete mode 100644 homeassistant/components/vallox/translations/lv.json create mode 100644 homeassistant/components/vulcan/translations/bg.json create mode 100644 homeassistant/components/vulcan/translations/ca.json create mode 100644 homeassistant/components/vulcan/translations/de.json create mode 100644 homeassistant/components/vulcan/translations/el.json create mode 100644 homeassistant/components/vulcan/translations/et.json create mode 100644 homeassistant/components/vulcan/translations/fr.json create mode 100644 homeassistant/components/vulcan/translations/hu.json create mode 100644 homeassistant/components/vulcan/translations/id.json create mode 100644 homeassistant/components/vulcan/translations/it.json create mode 100644 homeassistant/components/vulcan/translations/ja.json create mode 100644 homeassistant/components/vulcan/translations/nl.json create mode 100644 homeassistant/components/vulcan/translations/no.json create mode 100644 homeassistant/components/vulcan/translations/pl.json create mode 100644 homeassistant/components/vulcan/translations/pt-BR.json create mode 100644 homeassistant/components/vulcan/translations/ru.json create mode 100644 homeassistant/components/vulcan/translations/tr.json create mode 100644 homeassistant/components/vulcan/translations/zh-Hant.json create mode 100644 homeassistant/components/webostv/translations/hu.json diff --git a/homeassistant/components/accuweather/translations/ca.json b/homeassistant/components/accuweather/translations/ca.json index 8178a5caef0..feda485d8fd 100644 --- a/homeassistant/components/accuweather/translations/ca.json +++ b/homeassistant/components/accuweather/translations/ca.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, + "create_entry": { + "default": "Alguns sensors no estan activats de manera predeterminada. Els pots activar des del registre d'entitats, despr\u00e9s de la configuraci\u00f3 de la integraci\u00f3.\nLa previsi\u00f3 meteorol\u00f2gica no est\u00e0 activada de manera predeterminada. Pots activar-la a les opcions de la integraci\u00f3." + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_api_key": "Clau API inv\u00e0lida", diff --git a/homeassistant/components/accuweather/translations/de.json b/homeassistant/components/accuweather/translations/de.json index 17eb0ee31fc..ac0c430de04 100644 --- a/homeassistant/components/accuweather/translations/de.json +++ b/homeassistant/components/accuweather/translations/de.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, + "create_entry": { + "default": "Einige Sensoren sind standardm\u00e4\u00dfig nicht aktiviert. Du kannst sie nach der Integrationskonfiguration in der Entit\u00e4tsregistrierung aktivieren.\nDie Wettervorhersage ist nicht standardm\u00e4\u00dfig aktiviert. Du kannst sie in den Integrationsoptionen aktivieren." + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", diff --git a/homeassistant/components/accuweather/translations/el.json b/homeassistant/components/accuweather/translations/el.json index 4f7a23e1d6f..43a65158455 100644 --- a/homeassistant/components/accuweather/translations/el.json +++ b/homeassistant/components/accuweather/translations/el.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." }, + "create_entry": { + "default": "\u039f\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03b9 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03b9 \u03b1\u03c0\u03cc \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03bf \u03bc\u03b7\u03c4\u03c1\u03ce\u03bf \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03bc\u03b5\u03c4\u03ac \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2.\n \u0397 \u03c0\u03c1\u03cc\u03b3\u03bd\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b1\u03c0\u03cc \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2." + }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API", diff --git a/homeassistant/components/accuweather/translations/en.json b/homeassistant/components/accuweather/translations/en.json index 8f2261b93c7..a984080fd86 100644 --- a/homeassistant/components/accuweather/translations/en.json +++ b/homeassistant/components/accuweather/translations/en.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Already configured. Only a single configuration possible." }, + "create_entry": { + "default": "Some sensors are not enabled by default. You can enable them in the entity registry after the integration configuration.\nWeather forecast is not enabled by default. You can enable it in the integration options." + }, "error": { "cannot_connect": "Failed to connect", "invalid_api_key": "Invalid API key", diff --git a/homeassistant/components/accuweather/translations/et.json b/homeassistant/components/accuweather/translations/et.json index 6e2dc1ffd96..429e2dec61e 100644 --- a/homeassistant/components/accuweather/translations/et.json +++ b/homeassistant/components/accuweather/translations/et.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Sidumine juba tehtud. V\u00f5imalik on ainult 1 sidumine." }, + "create_entry": { + "default": "M\u00f5ned andurid ei ole vaikimisi lubatud. Saad neid lubada \u00fcksuse registris p\u00e4rast sidumise seadistamist.\nIlmaprognoos ei ole vaikimisi lubatud. Saad selle lubada sidumise valikutes." + }, "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_api_key": "API v\u00f5ti on vale", diff --git a/homeassistant/components/accuweather/translations/fr.json b/homeassistant/components/accuweather/translations/fr.json index 1de45f8da22..cf407ef3962 100644 --- a/homeassistant/components/accuweather/translations/fr.json +++ b/homeassistant/components/accuweather/translations/fr.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, + "create_entry": { + "default": "Certains capteurs ne sont pas activ\u00e9s par d\u00e9faut. Vous pouvez les activer dans le registre des entit\u00e9s une fois la configuration de l'int\u00e9gration termin\u00e9e.\nLes pr\u00e9visions m\u00e9t\u00e9orologiques ne sont pas activ\u00e9es par d\u00e9faut. Vous pouvez les activer dans les options de l'int\u00e9gration." + }, "error": { "cannot_connect": "\u00c9chec de connexion", "invalid_api_key": "Cl\u00e9 d'API non valide", @@ -16,7 +19,7 @@ "longitude": "Longitude", "name": "Nom" }, - "description": "Si vous avez besoin d'aide pour la configuration, consultez le site suivant : https://www.home-assistant.io/integrations/accuweather/\n\nCertains capteurs ne sont pas activ\u00e9s par d\u00e9faut. Vous pouvez les activer dans le registre des entit\u00e9s apr\u00e8s la configuration de l'int\u00e9gration.\nLes pr\u00e9visions m\u00e9t\u00e9orologiques ne sont pas activ\u00e9es par d\u00e9faut. Vous pouvez l'activer dans les options d'int\u00e9gration.", + "description": "Si vous avez besoin d'aide pour la configuration, consultez\u00a0: https://www.home-assistant.io/integrations/accuweather/\n\nCertains capteurs ne sont pas activ\u00e9s par d\u00e9faut. Vous pouvez les activer dans le registre des entit\u00e9s une fois la configuration de l'int\u00e9gration termin\u00e9e.\nLes pr\u00e9visions m\u00e9t\u00e9orologiques ne sont pas activ\u00e9es par d\u00e9faut. Vous pouvez les activer dans les options de l'int\u00e9gration.", "title": "AccuWeather" } } diff --git a/homeassistant/components/accuweather/translations/hu.json b/homeassistant/components/accuweather/translations/hu.json index 8b0409d1f22..1b30dd09daf 100644 --- a/homeassistant/components/accuweather/translations/hu.json +++ b/homeassistant/components/accuweather/translations/hu.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, + "create_entry": { + "default": "Egyes \u00e9rz\u00e9kel\u0151k alap\u00e9rtelmez\u00e9s szerint nincsenek enged\u00e9lyezve. Az integr\u00e1ci\u00f3s konfigur\u00e1ci\u00f3 ut\u00e1n enged\u00e9lyezheti \u0151ket az entit\u00e1s rendszerle\u00edr\u00f3 adatb\u00e1zis\u00e1ban.\nAz id\u0151j\u00e1r\u00e1s-el\u0151rejelz\u00e9s alap\u00e9rtelmez\u00e9s szerint nincs enged\u00e9lyezve. Ezt az integr\u00e1ci\u00f3s be\u00e1ll\u00edt\u00e1sokban enged\u00e9lyezheti." + }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", @@ -14,7 +17,7 @@ "api_key": "API kulcs", "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" }, "description": "Ha seg\u00edts\u00e9gre van sz\u00fcks\u00e9ge a konfigur\u00e1l\u00e1shoz, n\u00e9zze meg itt: https://www.home-assistant.io/integrations/accuweather/ \n\nEgyes \u00e9rz\u00e9kel\u0151k alap\u00e9rtelmez\u00e9s szerint nincsenek enged\u00e9lyezve. Az integr\u00e1ci\u00f3s konfigur\u00e1ci\u00f3 ut\u00e1n enged\u00e9lyezheti \u0151ket az entit\u00e1s-nyilv\u00e1ntart\u00e1sban.\nAz id\u0151j\u00e1r\u00e1s-el\u0151rejelz\u00e9s alap\u00e9rtelmez\u00e9s szerint nincs enged\u00e9lyezve. Ezt az integr\u00e1ci\u00f3s be\u00e1ll\u00edt\u00e1sokban enged\u00e9lyezheti.", "title": "AccuWeather" diff --git a/homeassistant/components/accuweather/translations/id.json b/homeassistant/components/accuweather/translations/id.json index 970b3a026b7..7bf9f27e8b2 100644 --- a/homeassistant/components/accuweather/translations/id.json +++ b/homeassistant/components/accuweather/translations/id.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, + "create_entry": { + "default": "Beberapa sensor tidak diaktifkan secara default. Anda dapat mengaktifkannya di registri entitas setelah konfigurasi integrasi.\nPrakiraan cuaca tidak diaktifkan secara default. Anda dapat mengaktifkannya di opsi integrasi." + }, "error": { "cannot_connect": "Gagal terhubung", "invalid_api_key": "Kunci API tidak valid", diff --git a/homeassistant/components/accuweather/translations/it.json b/homeassistant/components/accuweather/translations/it.json index 8a1f9b96463..9daaf378fb4 100644 --- a/homeassistant/components/accuweather/translations/it.json +++ b/homeassistant/components/accuweather/translations/it.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, + "create_entry": { + "default": "Alcuni sensori non sono abilitati per impostazione predefinita. Puoi abilitarli nel registro delle entit\u00e0 dopo la configurazione dell'integrazione.\nLe previsioni del tempo non sono abilitate per impostazione predefinita. Puoi abilitarlo nelle opzioni di integrazione." + }, "error": { "cannot_connect": "Impossibile connettersi", "invalid_api_key": "Chiave API non valida", diff --git a/homeassistant/components/accuweather/translations/ja.json b/homeassistant/components/accuweather/translations/ja.json index 10fe398ba04..d05e75e74b5 100644 --- a/homeassistant/components/accuweather/translations/ja.json +++ b/homeassistant/components/accuweather/translations/ja.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, + "create_entry": { + "default": "\u4e00\u90e8\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u69cb\u6210\u5f8c\u3001\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30ec\u30b8\u30b9\u30c8\u30ea\u3067\u305d\u308c\u3089\u3092\u6709\u52b9\u306b\u3067\u304d\u307e\u3059\u3002\n\u5929\u6c17\u4e88\u5831\u306f\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u6709\u52b9\u306b\u3067\u304d\u307e\u3059\u3002" + }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", diff --git a/homeassistant/components/accuweather/translations/nl.json b/homeassistant/components/accuweather/translations/nl.json index f04d93b5921..2a6dd1a812b 100644 --- a/homeassistant/components/accuweather/translations/nl.json +++ b/homeassistant/components/accuweather/translations/nl.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, + "create_entry": { + "default": "Sommige sensoren zijn standaard niet ingeschakeld. U kunt ze inschakelen in het entiteitenregister na de integratieconfiguratie.\nWeersvoorspelling is niet standaard ingeschakeld. U kunt deze inschakelen in de integratieopties." + }, "error": { "cannot_connect": "Kan geen verbinding maken", "invalid_api_key": "API-sleutel", diff --git a/homeassistant/components/accuweather/translations/no.json b/homeassistant/components/accuweather/translations/no.json index be87b1ab244..af689b2fd1c 100644 --- a/homeassistant/components/accuweather/translations/no.json +++ b/homeassistant/components/accuweather/translations/no.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, + "create_entry": { + "default": "Noen sensorer er ikke aktivert som standard. Du kan aktivere dem i enhetsregisteret etter integrasjonskonfigurasjonen.\n V\u00e6rmelding er ikke aktivert som standard. Du kan aktivere det i integreringsalternativene." + }, "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_api_key": "Ugyldig API-n\u00f8kkel", diff --git a/homeassistant/components/accuweather/translations/pl.json b/homeassistant/components/accuweather/translations/pl.json index 2794bc8b7b6..a8ef8a3bfa0 100644 --- a/homeassistant/components/accuweather/translations/pl.json +++ b/homeassistant/components/accuweather/translations/pl.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, + "create_entry": { + "default": "Niekt\u00f3re sensory nie s\u0105 domy\u015blnie w\u0142\u0105czone. Mo\u017cesz je w\u0142\u0105czy\u0107 w rejestrze encji po skonfigurowaniu integracji.\nPrognoza pogody nie jest domy\u015blnie w\u0142\u0105czona. Mo\u017cesz to w\u0142\u0105czy\u0107 w opcjach integracji." + }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_api_key": "Nieprawid\u0142owy klucz API", diff --git a/homeassistant/components/accuweather/translations/pt-BR.json b/homeassistant/components/accuweather/translations/pt-BR.json index 469dd81ff91..4bb1bbbc97e 100644 --- a/homeassistant/components/accuweather/translations/pt-BR.json +++ b/homeassistant/components/accuweather/translations/pt-BR.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, + "create_entry": { + "default": "Alguns sensores n\u00e3o s\u00e3o ativados por padr\u00e3o. Voc\u00ea pode habilit\u00e1-los no registro da entidade ap\u00f3s a configura\u00e7\u00e3o da integra\u00e7\u00e3o.\n A previs\u00e3o do tempo n\u00e3o est\u00e1 habilitada por padr\u00e3o. Voc\u00ea pode habilit\u00e1-lo nas op\u00e7\u00f5es de integra\u00e7\u00e3o." + }, "error": { "cannot_connect": "Falha ao conectar", "invalid_api_key": "Chave de API inv\u00e1lida", diff --git a/homeassistant/components/accuweather/translations/ru.json b/homeassistant/components/accuweather/translations/ru.json index 7bc767b1baf..dfd24b6f147 100644 --- a/homeassistant/components/accuweather/translations/ru.json +++ b/homeassistant/components/accuweather/translations/ru.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, + "create_entry": { + "default": "\u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u044b \u0441\u043a\u0440\u044b\u0442\u044b \u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u043d\u0443\u0436\u043d\u044b\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 \u0432 \u0440\u0435\u0435\u0441\u0442\u0440\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0438 \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438." + }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", diff --git a/homeassistant/components/accuweather/translations/tr.json b/homeassistant/components/accuweather/translations/tr.json index 7b0fa476458..b5efeb7080e 100644 --- a/homeassistant/components/accuweather/translations/tr.json +++ b/homeassistant/components/accuweather/translations/tr.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, + "create_entry": { + "default": "Baz\u0131 sens\u00f6rler varsay\u0131lan olarak etkin de\u011fildir. Bunlar\u0131, entegrasyon yap\u0131land\u0131rmas\u0131ndan sonra varl\u0131k kay\u0131t defterinde etkinle\u015ftirebilirsiniz.\n Hava tahmini varsay\u0131lan olarak etkin de\u011fildir. Entegrasyon se\u00e7eneklerinde etkinle\u015ftirebilirsiniz." + }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", diff --git a/homeassistant/components/accuweather/translations/zh-Hant.json b/homeassistant/components/accuweather/translations/zh-Hant.json index fbf72991e92..6d9e7e1c36f 100644 --- a/homeassistant/components/accuweather/translations/zh-Hant.json +++ b/homeassistant/components/accuweather/translations/zh-Hant.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, + "create_entry": { + "default": "\u90e8\u5206\u611f\u6e2c\u5668\u9810\u8a2d\u70ba\u4e0d\u555f\u7528\u72c0\u614b\u3002\u53ef\u4ee5\u7a0d\u5f8c\u65bc\u6574\u5408\u8a2d\u5b9a\u9801\u9762\u4e2d\u7684\u5be6\u9ad4\u8a3b\u518a\u8868\u9032\u884c\u555f\u7528\u3002\n\u5929\u6c23\u9810\u5831\u9810\u8a2d\u70ba\u4e0d\u555f\u7528\u3001\u53ef\u4ee5\u65bc\u6574\u5408\u9078\u9805\u4e2d\u9032\u884c\u555f\u7528\u3002" + }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_api_key": "API \u91d1\u9470\u7121\u6548", diff --git a/homeassistant/components/adax/translations/hu.json b/homeassistant/components/adax/translations/hu.json index 9f37837420f..a2a6dcdacaf 100644 --- a/homeassistant/components/adax/translations/hu.json +++ b/homeassistant/components/adax/translations/hu.json @@ -20,9 +20,9 @@ "local": { "data": { "wifi_pswd": "WiFi jelsz\u00f3", - "wifi_ssid": "WiFi ssid" + "wifi_ssid": "Wi-Fi SSID" }, - "description": "\u00c1ll\u00edtsa vissza a f\u0171t\u0151berendez\u00e9st a + \u00e9s az OK gomb nyomvatart\u00e1s\u00e1val, m\u00edg a kijelz\u0151n a \"Reset\" (Vissza\u00e1ll\u00edt\u00e1s) felirat nem jelenik meg. Ezut\u00e1n nyomja meg \u00e9s tartsa lenyomva a f\u0171t\u0151berendez\u00e9s OK gombj\u00e1t, am\u00edg a k\u00e9k led villogni nem kezd, majd nyomja meg a K\u00fcld\u00e9s gombot. A f\u0171t\u0151berendez\u00e9s konfigur\u00e1l\u00e1sa n\u00e9h\u00e1ny percet vehet ig\u00e9nybe." + "description": "\u00c1ll\u00edtsa vissza a f\u0171t\u0151berendez\u00e9st a + \u00e9s az OK gomb nyomvatart\u00e1s\u00e1val, m\u00edg a kijelz\u0151n a \"Reset\" (Vissza\u00e1ll\u00edt\u00e1s) felirat nem jelenik meg. Ezut\u00e1n nyomja meg \u00e9s tartsa lenyomva a f\u0171t\u0151berendez\u00e9s OK gombj\u00e1t, am\u00edg a k\u00e9k led villogni nem kezd, l\u00e9pjen tov\u00e1bb. A f\u0171t\u0151berendez\u00e9s konfigur\u00e1l\u00e1sa n\u00e9h\u00e1ny percet vehet ig\u00e9nybe." }, "user": { "data": { diff --git a/homeassistant/components/adax/translations/nl.json b/homeassistant/components/adax/translations/nl.json index 026c3c06dae..ced3b8bdab0 100644 --- a/homeassistant/components/adax/translations/nl.json +++ b/homeassistant/components/adax/translations/nl.json @@ -31,7 +31,7 @@ "host": "Host", "password": "Wachtwoord" }, - "description": "Selecteer verbindingstype. Lokaal vereist verwarming met bluetooth" + "description": "Selecteer verbindingstype. Lokaal vereist verwarming met Bluetooth." } } } diff --git a/homeassistant/components/aemet/translations/ca.json b/homeassistant/components/aemet/translations/ca.json index 75784ddfc87..7dab53fc049 100644 --- a/homeassistant/components/aemet/translations/ca.json +++ b/homeassistant/components/aemet/translations/ca.json @@ -14,7 +14,7 @@ "longitude": "Longitud", "name": "Nom de la integraci\u00f3" }, - "description": "Configura la integraci\u00f3 d'AEMET OpenData. Per generar la clau API, v\u00e9s a https://opendata.aemet.es/centrodedescargas/altaUsuario", + "description": "Per generar la clau API, v\u00e9s a https://opendata.aemet.es/centrodedescargas/altaUsuario", "title": "AEMET OpenData" } } diff --git a/homeassistant/components/aemet/translations/de.json b/homeassistant/components/aemet/translations/de.json index 0704e7d71ba..25f374f2e43 100644 --- a/homeassistant/components/aemet/translations/de.json +++ b/homeassistant/components/aemet/translations/de.json @@ -14,7 +14,7 @@ "longitude": "L\u00e4ngengrad", "name": "Name der Integration" }, - "description": "Richte die AEMET OpenData Integration ein. Um den API-Schl\u00fcssel zu generieren, besuche https://opendata.aemet.es/centrodedescargas/altaUsuario", + "description": "Um den API-Schl\u00fcssel zu generieren, besuche https://opendata.aemet.es/centrodedescargas/altaUsuario", "title": "AEMET OpenData" } } diff --git a/homeassistant/components/aemet/translations/en.json b/homeassistant/components/aemet/translations/en.json index 3888ccdafc0..7c10d1dc676 100644 --- a/homeassistant/components/aemet/translations/en.json +++ b/homeassistant/components/aemet/translations/en.json @@ -14,7 +14,7 @@ "longitude": "Longitude", "name": "Name of the integration" }, - "description": "Set up AEMET OpenData integration. To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario", + "description": "To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario", "title": "AEMET OpenData" } } diff --git a/homeassistant/components/aemet/translations/et.json b/homeassistant/components/aemet/translations/et.json index 0d542fcc744..04292b3d912 100644 --- a/homeassistant/components/aemet/translations/et.json +++ b/homeassistant/components/aemet/translations/et.json @@ -14,7 +14,7 @@ "longitude": "Pikkuskraad", "name": "Sidumise nimi" }, - "description": "Seadista AEMET OpenData sidumine. API v\u00f5tme loomiseks mine aadressile https://opendata.aemet.es/centrodedescargas/altaUsuario", + "description": "API-v\u00f5tme loomiseks mine aadressile https://opendata.aemet.es/centrodedescargas/altaUsuario", "title": "AEMET OpenData" } } diff --git a/homeassistant/components/aemet/translations/fr.json b/homeassistant/components/aemet/translations/fr.json index 0d3d0e77a5e..bddfd2507bf 100644 --- a/homeassistant/components/aemet/translations/fr.json +++ b/homeassistant/components/aemet/translations/fr.json @@ -14,7 +14,7 @@ "longitude": "Longitude", "name": "Nom de l'int\u00e9gration" }, - "description": "Configurez l'int\u00e9gration AEMET OpenData. Pour g\u00e9n\u00e9rer la cl\u00e9 API, acc\u00e9dez \u00e0 https://opendata.aemet.es/centrodedescargas/altaUsuario", + "description": "Pour g\u00e9n\u00e9rer une cl\u00e9 d'API, rendez-vous sur https://opendata.aemet.es/centrodedescargas/altaUsuario", "title": "AEMET OpenData" } } diff --git a/homeassistant/components/aemet/translations/hu.json b/homeassistant/components/aemet/translations/hu.json index 31a7654efd9..2f2984b1c90 100644 --- a/homeassistant/components/aemet/translations/hu.json +++ b/homeassistant/components/aemet/translations/hu.json @@ -14,7 +14,7 @@ "longitude": "Hossz\u00fas\u00e1g", "name": "Az integr\u00e1ci\u00f3 neve" }, - "description": "\u00c1ll\u00edtsa be az AEMET OpenData integr\u00e1ci\u00f3t. Az API-kulcs el\u0151\u00e1ll\u00edt\u00e1s\u00e1hoz keresse fel a https://opendata.aemet.es/centrodedescargas/altaUsuario webhelyet.", + "description": "Az API-kulcs l\u00e9trehoz\u00e1s\u00e1hoz keresse fel a https://opendata.aemet.es/centrodedescargas/altaUsuario webhelyet", "title": "AEMET OpenData" } } diff --git a/homeassistant/components/aemet/translations/id.json b/homeassistant/components/aemet/translations/id.json index e3a602a9a7c..62239172aae 100644 --- a/homeassistant/components/aemet/translations/id.json +++ b/homeassistant/components/aemet/translations/id.json @@ -14,7 +14,7 @@ "longitude": "Bujur", "name": "Nama integrasi" }, - "description": "Siapkan integrasi AEMET OpenData. Untuk menghasilkan kunci API, buka https://opendata.aemet.es/centrodedescargas/altaUsuario", + "description": "Untuk membuat kunci API, buka https://opendata.aemet.es/centrodedescargas/altaUsuario", "title": "AEMET OpenData" } } diff --git a/homeassistant/components/aemet/translations/it.json b/homeassistant/components/aemet/translations/it.json index a55e003ca4e..4b3f6f2251b 100644 --- a/homeassistant/components/aemet/translations/it.json +++ b/homeassistant/components/aemet/translations/it.json @@ -14,7 +14,7 @@ "longitude": "Logitudine", "name": "Nome dell'integrazione" }, - "description": "Imposta l'integrazione di AEMET OpenData. Per generare la chiave API, vai su https://opendata.aemet.es/centrodedescargas/altaUsuario", + "description": "Per generare la chiave API, vai su https://opendata.aemet.es/centrodescargas/altaUsuario", "title": "AEMET OpenData" } } diff --git a/homeassistant/components/aemet/translations/nl.json b/homeassistant/components/aemet/translations/nl.json index 40fab5d9e0f..abf590e5a36 100644 --- a/homeassistant/components/aemet/translations/nl.json +++ b/homeassistant/components/aemet/translations/nl.json @@ -14,7 +14,7 @@ "longitude": "Lengtegraad", "name": "Naam van de integratie" }, - "description": "Stel AEMET OpenData-integratie in. Ga naar https://opendata.aemet.es/centrodedescargas/altaUsuario om een API-sleutel te genereren", + "description": "Om een API sleutel te genereren ga naar https://opendata.aemet.es/centrodedescargas/altaUsuario", "title": "AEMET OpenData" } } diff --git a/homeassistant/components/aemet/translations/no.json b/homeassistant/components/aemet/translations/no.json index fe36ff835ee..a6076844122 100644 --- a/homeassistant/components/aemet/translations/no.json +++ b/homeassistant/components/aemet/translations/no.json @@ -14,7 +14,7 @@ "longitude": "Lengdegrad", "name": "Navnet p\u00e5 integrasjonen" }, - "description": "Sett opp AEMET OpenData-integrasjon. For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://opendata.aemet.es/centrodedescargas/altaUsuario", + "description": "For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://opendata.aemet.es/centrodedescargas/altaUsuario", "title": "AEMET OpenData" } } diff --git a/homeassistant/components/aemet/translations/pl.json b/homeassistant/components/aemet/translations/pl.json index 8531ca47fd6..f1021585140 100644 --- a/homeassistant/components/aemet/translations/pl.json +++ b/homeassistant/components/aemet/translations/pl.json @@ -14,7 +14,7 @@ "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "name": "Nazwa integracji" }, - "description": "Skonfiguruj integracj\u0119 AEMET OpenData. Aby wygenerowa\u0107 klucz API, przejd\u017a do https://opendata.aemet.es/centrodedescargas/altaUsuario", + "description": "Aby wygenerowa\u0107 klucz API, przejd\u017a do https://opendata.aemet.es/centrodedescargas/altaUsuario", "title": "AEMET OpenData" } } diff --git a/homeassistant/components/aemet/translations/pt-BR.json b/homeassistant/components/aemet/translations/pt-BR.json index 6a0c8800b02..7c5972dccd8 100644 --- a/homeassistant/components/aemet/translations/pt-BR.json +++ b/homeassistant/components/aemet/translations/pt-BR.json @@ -14,7 +14,7 @@ "longitude": "Longitude", "name": "Nome da integra\u00e7\u00e3o" }, - "description": "Configure a integra\u00e7\u00e3o AEMET OpenData. Para gerar a chave API acesse https://opendata.aemet.es/centrodedescargas/altaUsuario", + "description": "Para gerar a chave API acesse https://opendata.aemet.es/centrodedescargas/altaUsuario", "title": "AEMET OpenData" } } diff --git a/homeassistant/components/aemet/translations/ru.json b/homeassistant/components/aemet/translations/ru.json index f9278af712b..9d8496cbb2d 100644 --- a/homeassistant/components/aemet/translations/ru.json +++ b/homeassistant/components/aemet/translations/ru.json @@ -14,7 +14,7 @@ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 AEMET OpenData. \u0427\u0442\u043e\u0431\u044b \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 https://opendata.aemet.es/centrodedescargas/altaUsuario.", + "description": "\u0427\u0442\u043e\u0431\u044b \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://opendata.aemet.es/centrodedescargas/altaUsuario.", "title": "AEMET OpenData" } } diff --git a/homeassistant/components/aemet/translations/tr.json b/homeassistant/components/aemet/translations/tr.json index 7cb0048b0e0..e1e3cae01ff 100644 --- a/homeassistant/components/aemet/translations/tr.json +++ b/homeassistant/components/aemet/translations/tr.json @@ -14,7 +14,7 @@ "longitude": "Boylam", "name": "Entegrasyonun ad\u0131" }, - "description": "AEMET OpenData entegrasyonunu ayarlay\u0131n. API anahtar\u0131 olu\u015fturmak i\u00e7in https://opendata.aemet.es/centrodedescargas/altaUsuario adresine gidin.", + "description": "API anahtar\u0131 olu\u015fturmak i\u00e7in https://opendata.aemet.es/centrodedescargas/altaUsuario adresine gidin.", "title": "AEMET OpenData" } } diff --git a/homeassistant/components/aemet/translations/zh-Hant.json b/homeassistant/components/aemet/translations/zh-Hant.json index e064a6c0192..4d77ca17505 100644 --- a/homeassistant/components/aemet/translations/zh-Hant.json +++ b/homeassistant/components/aemet/translations/zh-Hant.json @@ -14,7 +14,7 @@ "longitude": "\u7d93\u5ea6", "name": "\u6574\u5408\u540d\u7a31" }, - "description": "\u6b32\u8a2d\u5b9a AEMET OpenData \u6574\u5408\u3002\u8acb\u81f3 https://opendata.aemet.es/centrodedescargas/altaUsuario \u7522\u751f API \u91d1\u9470", + "description": "\u8acb\u81f3 https://opendata.aemet.es/centrodedescargas/altaUsuario \u4ee5\u7522\u751f API \u91d1\u9470", "title": "AEMET OpenData" } } diff --git a/homeassistant/components/agent_dvr/translations/hu.json b/homeassistant/components/agent_dvr/translations/hu.json index 83751d72eaf..fdddd0ba92e 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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { diff --git a/homeassistant/components/airly/translations/ca.json b/homeassistant/components/airly/translations/ca.json index 0d5177df33e..ac956185c14 100644 --- a/homeassistant/components/airly/translations/ca.json +++ b/homeassistant/components/airly/translations/ca.json @@ -15,7 +15,7 @@ "longitude": "Longitud", "name": "Nom" }, - "description": "Configura la integraci\u00f3 de qualitat de l'aire Airly. Per generar la clau API, v\u00e9s a https://developer.airly.eu/register", + "description": "Per generar la clau API, v\u00e9s a https://developer.airly.eu/register", "title": "Airly" } } diff --git a/homeassistant/components/airly/translations/de.json b/homeassistant/components/airly/translations/de.json index b13798c0ae3..216a8c56c6d 100644 --- a/homeassistant/components/airly/translations/de.json +++ b/homeassistant/components/airly/translations/de.json @@ -15,7 +15,7 @@ "longitude": "L\u00e4ngengrad", "name": "Name" }, - "description": "Einrichtung der Airly-Luftqualit\u00e4t Integration. Um einen API-Schl\u00fcssel zu generieren, registriere dich auf https://developer.airly.eu/register", + "description": "Um einen API-Schl\u00fcssel zu generieren, besuche https://developer.airly.eu/register", "title": "Airly" } } diff --git a/homeassistant/components/airly/translations/en.json b/homeassistant/components/airly/translations/en.json index 0a5426c87d8..249905eff2a 100644 --- a/homeassistant/components/airly/translations/en.json +++ b/homeassistant/components/airly/translations/en.json @@ -15,7 +15,7 @@ "longitude": "Longitude", "name": "Name" }, - "description": "Set up Airly air quality integration. To generate API key go to https://developer.airly.eu/register", + "description": "To generate API key go to https://developer.airly.eu/register", "title": "Airly" } } diff --git a/homeassistant/components/airly/translations/et.json b/homeassistant/components/airly/translations/et.json index 6730e131ac2..6a55262ac6c 100644 --- a/homeassistant/components/airly/translations/et.json +++ b/homeassistant/components/airly/translations/et.json @@ -15,7 +15,7 @@ "longitude": "Pikkuskraad", "name": "Nimi" }, - "description": "Seadista Airly \u00f5hukvaliteedi andmete sidumine. API v\u00f5tme loomiseks mine aadressile https://developer.airly.eu/register", + "description": "API-v\u00f5tme loomiseks mine aadressile https://developer.airly.eu/register", "title": "" } } diff --git a/homeassistant/components/airly/translations/fr.json b/homeassistant/components/airly/translations/fr.json index 85d1a3b478e..17a7248f7ad 100644 --- a/homeassistant/components/airly/translations/fr.json +++ b/homeassistant/components/airly/translations/fr.json @@ -15,7 +15,7 @@ "longitude": "Longitude", "name": "Nom" }, - "description": "Configurez l'int\u00e9gration de la qualit\u00e9 de l'air Airly. Pour g\u00e9n\u00e9rer une cl\u00e9 API, rendez-vous sur https://developer.airly.eu/register.", + "description": "Pour g\u00e9n\u00e9rer une cl\u00e9 d'API, rendez-vous sur https://developer.airly.eu/register", "title": "Airly" } } diff --git a/homeassistant/components/airly/translations/hu.json b/homeassistant/components/airly/translations/hu.json index f730edde85f..04d667f4079 100644 --- a/homeassistant/components/airly/translations/hu.json +++ b/homeassistant/components/airly/translations/hu.json @@ -13,9 +13,9 @@ "api_key": "API kulcs", "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" }, - "description": "Az Airly leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Api-kulcs l\u00e9trehoz\u00e1s\u00e1hoz nyissa meg a k\u00f6vetkez\u0151 weboldalt: https://developer.airly.eu/register", + "description": "Az API-kulcs l\u00e9trehoz\u00e1s\u00e1hoz keresse fel a https://developer.airly.eu/register webhelyet", "title": "Airly" } } diff --git a/homeassistant/components/airly/translations/id.json b/homeassistant/components/airly/translations/id.json index 57b4c0d95f9..bc97db4135a 100644 --- a/homeassistant/components/airly/translations/id.json +++ b/homeassistant/components/airly/translations/id.json @@ -15,7 +15,7 @@ "longitude": "Bujur", "name": "Nama" }, - "description": "Siapkan integrasi kualitas udara Airly. Untuk membuat kunci API, buka https://developer.airly.eu/register", + "description": "Untuk membuat kunci API, buka https://developer.airly.eu/register", "title": "Airly" } } diff --git a/homeassistant/components/airly/translations/it.json b/homeassistant/components/airly/translations/it.json index 170455ffb15..b96d5c8a74c 100644 --- a/homeassistant/components/airly/translations/it.json +++ b/homeassistant/components/airly/translations/it.json @@ -15,7 +15,7 @@ "longitude": "Logitudine", "name": "Nome" }, - "description": "Configurazione dell'integrazione della qualit\u00e0 dell'aria Airly. Per generare la chiave API vai su https://developer.airly.eu/register", + "description": "Per generare la chiave API, vai su https://developer.airly.eu/register", "title": "Airly" } } diff --git a/homeassistant/components/airly/translations/nl.json b/homeassistant/components/airly/translations/nl.json index 14cbaf1711e..70fbca2074f 100644 --- a/homeassistant/components/airly/translations/nl.json +++ b/homeassistant/components/airly/translations/nl.json @@ -15,7 +15,7 @@ "longitude": "Lengtegraad", "name": "Naam" }, - "description": "Airly-integratie van luchtkwaliteit instellen. Ga naar https://developer.airly.eu/register om de API-sleutel te genereren", + "description": "Om een API sleutel te genereren ga naar https://developer.airly.eu/register", "title": "Airly" } } diff --git a/homeassistant/components/airly/translations/no.json b/homeassistant/components/airly/translations/no.json index 4c81422d93c..015e95af8f2 100644 --- a/homeassistant/components/airly/translations/no.json +++ b/homeassistant/components/airly/translations/no.json @@ -15,7 +15,7 @@ "longitude": "Lengdegrad", "name": "Navn" }, - "description": "Sett opp Airly luftkvalitet integrasjon. For \u00e5 opprette API-n\u00f8kkel, g\u00e5 til [https://developer.airly.eu/register](https://developer.airly.eu/register)", + "description": "For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://developer.airly.eu/register", "title": "" } } diff --git a/homeassistant/components/airly/translations/pl.json b/homeassistant/components/airly/translations/pl.json index f205a569474..dd44f2bf635 100644 --- a/homeassistant/components/airly/translations/pl.json +++ b/homeassistant/components/airly/translations/pl.json @@ -15,7 +15,7 @@ "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "name": "Nazwa" }, - "description": "Konfiguracja integracji Airly. By wygenerowa\u0107 klucz API, przejd\u017a na stron\u0119 https://developer.airly.eu/register", + "description": "By wygenerowa\u0107 klucz API, przejd\u017a na stron\u0119 https://developer.airly.eu/register", "title": "Airly" } } diff --git a/homeassistant/components/airly/translations/pt-BR.json b/homeassistant/components/airly/translations/pt-BR.json index e1f2fe097a6..0e9913b559c 100644 --- a/homeassistant/components/airly/translations/pt-BR.json +++ b/homeassistant/components/airly/translations/pt-BR.json @@ -15,7 +15,7 @@ "longitude": "Longitude", "name": "Nome" }, - "description": "Configure a integra\u00e7\u00e3o da qualidade do ar airly. Para gerar a chave de API v\u00e1 para https://developer.airly.eu/register", + "description": "Para gerar a chave de API, acesse https://developer.airly.eu/register", "title": "Airly" } } diff --git a/homeassistant/components/airly/translations/ru.json b/homeassistant/components/airly/translations/ru.json index 41ca90a8c02..80c6a98813c 100644 --- a/homeassistant/components/airly/translations/ru.json +++ b/homeassistant/components/airly/translations/ru.json @@ -15,7 +15,7 @@ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043f\u043e \u0430\u043d\u0430\u043b\u0438\u0437\u0443 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 Airly. \u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://developer.airly.eu/register.", + "description": "\u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://developer.airly.eu/register.", "title": "Airly" } } diff --git a/homeassistant/components/airly/translations/tr.json b/homeassistant/components/airly/translations/tr.json index fcae9294da2..7f5643be2d0 100644 --- a/homeassistant/components/airly/translations/tr.json +++ b/homeassistant/components/airly/translations/tr.json @@ -15,7 +15,7 @@ "longitude": "Boylam", "name": "Ad" }, - "description": "Airly hava kalitesi entegrasyonunu ayarlay\u0131n. API anahtar\u0131 olu\u015fturmak i\u00e7in https://developer.airly.eu/register adresine gidin.", + "description": "API anahtar\u0131 olu\u015fturmak i\u00e7in https://developer.airly.eu/register adresine gidin.", "title": "Airly" } } diff --git a/homeassistant/components/airly/translations/zh-Hant.json b/homeassistant/components/airly/translations/zh-Hant.json index e289bc7cd50..65f0bf8cde5 100644 --- a/homeassistant/components/airly/translations/zh-Hant.json +++ b/homeassistant/components/airly/translations/zh-Hant.json @@ -15,7 +15,7 @@ "longitude": "\u7d93\u5ea6", "name": "\u540d\u7a31" }, - "description": "\u6b32\u8a2d\u5b9a Airly \u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u8acb\u81f3 https://developer.airly.eu/register \u7522\u751f API \u91d1\u9470", + "description": "\u8acb\u81f3 https://developer.airly.eu/register \u4ee5\u7522\u751f API \u91d1\u9470", "title": "Airly" } } diff --git a/homeassistant/components/airnow/translations/ca.json b/homeassistant/components/airnow/translations/ca.json index 2db3cfad563..9a2b0aa9afc 100644 --- a/homeassistant/components/airnow/translations/ca.json +++ b/homeassistant/components/airnow/translations/ca.json @@ -17,7 +17,7 @@ "longitude": "Longitud", "radius": "Radi de l'estaci\u00f3 (milles; opcional)" }, - "description": "Configura la integraci\u00f3 de qualitat d'aire AirNow. Per generar la clau API, v\u00e9s a https://docs.airnowapi.org/account/request/", + "description": "Per generar la clau API, v\u00e9s a https://docs.airnowapi.org/account/request/", "title": "AirNow" } } diff --git a/homeassistant/components/airnow/translations/de.json b/homeassistant/components/airnow/translations/de.json index adf9ddf85a3..3c207a39cce 100644 --- a/homeassistant/components/airnow/translations/de.json +++ b/homeassistant/components/airnow/translations/de.json @@ -17,7 +17,7 @@ "longitude": "L\u00e4ngengrad", "radius": "Stationsradius (Meilen; optional)" }, - "description": "Richte die AirNow-Luftqualit\u00e4tsintegration ein. Um den API-Schl\u00fcssel zu generieren, besuche https://docs.airnowapi.org/account/request/.", + "description": "Um den API-Schl\u00fcssel zu generieren, besuche https://docs.airnowapi.org/account/request/", "title": "AirNow" } } diff --git a/homeassistant/components/airnow/translations/en.json b/homeassistant/components/airnow/translations/en.json index 371bb270ac1..cb3081284b4 100644 --- a/homeassistant/components/airnow/translations/en.json +++ b/homeassistant/components/airnow/translations/en.json @@ -17,7 +17,7 @@ "longitude": "Longitude", "radius": "Station Radius (miles; optional)" }, - "description": "Set up AirNow air quality integration. To generate API key go to https://docs.airnowapi.org/account/request/", + "description": "To generate API key go to https://docs.airnowapi.org/account/request/", "title": "AirNow" } } diff --git a/homeassistant/components/airnow/translations/et.json b/homeassistant/components/airnow/translations/et.json index 52b2bb618e0..3bdf8661d36 100644 --- a/homeassistant/components/airnow/translations/et.json +++ b/homeassistant/components/airnow/translations/et.json @@ -17,7 +17,7 @@ "longitude": "Pikkuskraad", "radius": "Jaama raadius (miilid; valikuline)" }, - "description": "Seadista AirNow \u00f5hukvaliteedi sidumine. API-v\u00f5tme loomiseks mine aadressile https://docs.airnowapi.org/account/request/", + "description": "API-v\u00f5tme loomiseks mine aadressile https://docs.airnowapi.org/account/request/", "title": "" } } diff --git a/homeassistant/components/airnow/translations/fr.json b/homeassistant/components/airnow/translations/fr.json index 1cfe1652771..2da7427e1be 100644 --- a/homeassistant/components/airnow/translations/fr.json +++ b/homeassistant/components/airnow/translations/fr.json @@ -17,7 +17,7 @@ "longitude": "Longitude", "radius": "Rayon d'action de la station (en miles, facultatif)" }, - "description": "Configurez l'int\u00e9gration de la qualit\u00e9 de l'air AirNow. Pour g\u00e9n\u00e9rer la cl\u00e9 API, acc\u00e9dez \u00e0 https://docs.airnowapi.org/account/request/", + "description": "Pour g\u00e9n\u00e9rer une cl\u00e9 d'API, rendez-vous sur https://docs.airnowapi.org/account/request/", "title": "AirNow" } } diff --git a/homeassistant/components/airnow/translations/hu.json b/homeassistant/components/airnow/translations/hu.json index 3f1bef471ee..52bf8cd63a0 100644 --- a/homeassistant/components/airnow/translations/hu.json +++ b/homeassistant/components/airnow/translations/hu.json @@ -17,7 +17,7 @@ "longitude": "Hossz\u00fas\u00e1g", "radius": "\u00c1llom\u00e1s sugara (m\u00e9rf\u00f6ld; opcion\u00e1lis)" }, - "description": "\u00c1ll\u00edtsa be az AirNow leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3t. Az API-kulcs el\u0151\u00e1ll\u00edt\u00e1s\u00e1hoz keresse fel a https://docs.airnowapi.org/account/request/ oldalt.", + "description": "Az API-kulcs l\u00e9trehoz\u00e1s\u00e1hoz keresse fel a https://docs.airnowapi.org/account/request/ webhelyet", "title": "AirNow" } } diff --git a/homeassistant/components/airnow/translations/id.json b/homeassistant/components/airnow/translations/id.json index 66fdff72fae..4d4ac987320 100644 --- a/homeassistant/components/airnow/translations/id.json +++ b/homeassistant/components/airnow/translations/id.json @@ -17,7 +17,7 @@ "longitude": "Bujur", "radius": "Radius Stasiun (mil; opsional)" }, - "description": "Siapkan integrasi kualitas udara AirNow. Untuk membuat kunci API buka https://docs.airnowapi.org/account/request/", + "description": "Untuk membuat kunci API buka https://docs.airnowapi.org/account/request/", "title": "AirNow" } } diff --git a/homeassistant/components/airnow/translations/it.json b/homeassistant/components/airnow/translations/it.json index 9dda15dfbd2..77715910c75 100644 --- a/homeassistant/components/airnow/translations/it.json +++ b/homeassistant/components/airnow/translations/it.json @@ -17,7 +17,7 @@ "longitude": "Logitudine", "radius": "Raggio stazione (miglia; opzionale)" }, - "description": "Configura l'integrazione per la qualit\u00e0 dell'aria AirNow. Per generare la chiave API, vai su https://docs.airnowapi.org/account/request/", + "description": "Per generare la chiave API vai su https://docs.airnowapi.org/account/request/", "title": "AirNow" } } diff --git a/homeassistant/components/airnow/translations/nl.json b/homeassistant/components/airnow/translations/nl.json index 090a5363823..5e5e33bf93d 100644 --- a/homeassistant/components/airnow/translations/nl.json +++ b/homeassistant/components/airnow/translations/nl.json @@ -17,7 +17,7 @@ "longitude": "Lengtegraad", "radius": "Stationsradius (mijl; optioneel)" }, - "description": "AirNow luchtkwaliteit integratie opzetten. Om een API sleutel te genereren ga naar https://docs.airnowapi.org/account/request/", + "description": "Om een API sleutel te genereren ga naar https://docs.airnowapi.org/account/request/", "title": "AirNow" } } diff --git a/homeassistant/components/airnow/translations/no.json b/homeassistant/components/airnow/translations/no.json index 19fa7e12207..b56b7096e2b 100644 --- a/homeassistant/components/airnow/translations/no.json +++ b/homeassistant/components/airnow/translations/no.json @@ -17,7 +17,7 @@ "longitude": "Lengdegrad", "radius": "Stasjonsradius (miles; valgfritt)" }, - "description": "Konfigurer integrering av luftkvalitet i AirNow. For \u00e5 generere en API-n\u00f8kkel, g\u00e5r du til https://docs.airnowapi.org/account/request/", + "description": "For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://docs.airnowapi.org/account/request/", "title": "" } } diff --git a/homeassistant/components/airnow/translations/pl.json b/homeassistant/components/airnow/translations/pl.json index fe4310607b9..f29a48dc47b 100644 --- a/homeassistant/components/airnow/translations/pl.json +++ b/homeassistant/components/airnow/translations/pl.json @@ -17,7 +17,7 @@ "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "radius": "Promie\u0144 od stacji (w milach; opcjonalnie)" }, - "description": "Konfiguracja integracji jako\u015bci powietrza AirNow. Aby wygenerowa\u0107 klucz API, przejd\u017a do https://docs.airnowapi.org/account/request/", + "description": "Aby wygenerowa\u0107 klucz API, przejd\u017a do https://docs.airnowapi.org/account/request/", "title": "AirNow" } } diff --git a/homeassistant/components/airnow/translations/pt-BR.json b/homeassistant/components/airnow/translations/pt-BR.json index fa24093b419..c1bda0098cd 100644 --- a/homeassistant/components/airnow/translations/pt-BR.json +++ b/homeassistant/components/airnow/translations/pt-BR.json @@ -17,7 +17,7 @@ "longitude": "Longitude", "radius": "Raio da Esta\u00e7\u00e3o (milhas; opcional)" }, - "description": "Configure a integra\u00e7\u00e3o da qualidade do ar AirNow. Para gerar a chave de API, acesse https://docs.airnowapi.org/account/request/", + "description": "Para gerar a chave de API, acesse https://docs.airnowapi.org/account/request/", "title": "AirNow" } } diff --git a/homeassistant/components/airnow/translations/ru.json b/homeassistant/components/airnow/translations/ru.json index 9667accb7c4..c5a9137f4c7 100644 --- a/homeassistant/components/airnow/translations/ru.json +++ b/homeassistant/components/airnow/translations/ru.json @@ -17,7 +17,7 @@ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "radius": "\u0420\u0430\u0434\u0438\u0443\u0441 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 (\u0432 \u043c\u0438\u043b\u044f\u0445; \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" }, - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043f\u043e \u0430\u043d\u0430\u043b\u0438\u0437\u0443 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 AirNow. \u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://docs.airnowapi.org/account/request/.", + "description": "\u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://docs.airnowapi.org/account/request/.", "title": "AirNow" } } diff --git a/homeassistant/components/airnow/translations/tr.json b/homeassistant/components/airnow/translations/tr.json index 590332b496c..1f80d7805da 100644 --- a/homeassistant/components/airnow/translations/tr.json +++ b/homeassistant/components/airnow/translations/tr.json @@ -17,7 +17,7 @@ "longitude": "Boylam", "radius": "\u0130stasyon Yar\u0131\u00e7ap\u0131 (mil; iste\u011fe ba\u011fl\u0131)" }, - "description": "AirNow hava kalitesi entegrasyonunu ayarlay\u0131n. API anahtar\u0131 olu\u015fturmak i\u00e7in https://docs.airnowapi.org/account/request/ adresine gidin.", + "description": "API anahtar\u0131 olu\u015fturmak i\u00e7in https://docs.airnowapi.org/account/request/ adresine gidin.", "title": "AirNow" } } diff --git a/homeassistant/components/airnow/translations/zh-Hant.json b/homeassistant/components/airnow/translations/zh-Hant.json index 08d7aab5878..bb18a5d8975 100644 --- a/homeassistant/components/airnow/translations/zh-Hant.json +++ b/homeassistant/components/airnow/translations/zh-Hant.json @@ -17,7 +17,7 @@ "longitude": "\u7d93\u5ea6", "radius": "\u89c0\u6e2c\u7ad9\u534a\u5f91\uff08\u82f1\u91cc\uff1b\u9078\u9805\uff09" }, - "description": "\u6b32\u8a2d\u5b9a AirNow \u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u8acb\u81f3 https://docs.airnowapi.org/account/request/ \u7522\u751f API \u91d1\u9470", + "description": "\u8acb\u81f3 https://docs.airnowapi.org/account/request/ \u4ee5\u7522\u751f API \u91d1\u9470", "title": "AirNow" } } diff --git a/homeassistant/components/airvisual/translations/sensor.fr.json b/homeassistant/components/airvisual/translations/sensor.fr.json index 47feff6bd79..2f9aacc775b 100644 --- a/homeassistant/components/airvisual/translations/sensor.fr.json +++ b/homeassistant/components/airvisual/translations/sensor.fr.json @@ -10,7 +10,7 @@ }, "airvisual__pollutant_level": { "good": "Bon", - "hazardous": "Hasardeux", + "hazardous": "Dangereux", "moderate": "Mod\u00e9r\u00e9", "unhealthy": "Malsain", "unhealthy_sensitive": "Malsain pour les groupes sensibles", diff --git a/homeassistant/components/airzone/translations/ca.json b/homeassistant/components/airzone/translations/ca.json index bbff47a9904..111a53fcf3a 100644 --- a/homeassistant/components/airzone/translations/ca.json +++ b/homeassistant/components/airzone/translations/ca.json @@ -4,7 +4,8 @@ "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { - "cannot_connect": "Ha fallat la connexi\u00f3" + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_system_id": "ID de sistema Airzone inv\u00e0lid" }, "step": { "user": { diff --git a/homeassistant/components/airzone/translations/cs.json b/homeassistant/components/airzone/translations/cs.json new file mode 100644 index 00000000000..aad89c1cbe3 --- /dev/null +++ b/homeassistant/components/airzone/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/de.json b/homeassistant/components/airzone/translations/de.json index 7b3f5030f06..2edb50330f4 100644 --- a/homeassistant/components/airzone/translations/de.json +++ b/homeassistant/components/airzone/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_system_id": "Ung\u00fcltige Airzone-System-ID" }, "step": { "user": { diff --git a/homeassistant/components/airzone/translations/el.json b/homeassistant/components/airzone/translations/el.json index 7b04fe27743..13da6efe27d 100644 --- a/homeassistant/components/airzone/translations/el.json +++ b/homeassistant/components/airzone/translations/el.json @@ -4,7 +4,8 @@ "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" }, "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_system_id": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 Airzone" }, "step": { "user": { diff --git a/homeassistant/components/airzone/translations/et.json b/homeassistant/components/airzone/translations/et.json index 307aa0de0a5..dff9d1173f6 100644 --- a/homeassistant/components/airzone/translations/et.json +++ b/homeassistant/components/airzone/translations/et.json @@ -4,7 +4,8 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud" }, "error": { - "cannot_connect": "\u00dchendamine nurjus" + "cannot_connect": "\u00dchendamine nurjus", + "invalid_system_id": "Airzone'i s\u00fcsteemi ID on vigane" }, "step": { "user": { diff --git a/homeassistant/components/airzone/translations/fr.json b/homeassistant/components/airzone/translations/fr.json index 1fdf1e4397b..40d22ad8bd9 100644 --- a/homeassistant/components/airzone/translations/fr.json +++ b/homeassistant/components/airzone/translations/fr.json @@ -4,7 +4,8 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "cannot_connect": "\u00c9chec de connexion" + "cannot_connect": "\u00c9chec de connexion", + "invalid_system_id": "ID syst\u00e8me Airzone non valide" }, "step": { "user": { diff --git a/homeassistant/components/airzone/translations/hu.json b/homeassistant/components/airzone/translations/hu.json index 88e7449a1c4..9a835f88dfc 100644 --- a/homeassistant/components/airzone/translations/hu.json +++ b/homeassistant/components/airzone/translations/hu.json @@ -4,7 +4,8 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_system_id": "\u00c9rv\u00e9nytelen Airzone rendszerazonos\u00edt\u00f3" }, "step": { "user": { diff --git a/homeassistant/components/airzone/translations/id.json b/homeassistant/components/airzone/translations/id.json index c37058bac34..ec37639811b 100644 --- a/homeassistant/components/airzone/translations/id.json +++ b/homeassistant/components/airzone/translations/id.json @@ -4,7 +4,8 @@ "already_configured": "Perangkat sudah dikonfigurasi" }, "error": { - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "invalid_system_id": "ID Sistem Airzone Tidak Valid" }, "step": { "user": { diff --git a/homeassistant/components/airzone/translations/it.json b/homeassistant/components/airzone/translations/it.json index db377b36fcc..32f8c67b545 100644 --- a/homeassistant/components/airzone/translations/it.json +++ b/homeassistant/components/airzone/translations/it.json @@ -4,7 +4,8 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { - "cannot_connect": "Impossibile connettersi" + "cannot_connect": "Impossibile connettersi", + "invalid_system_id": "ID sistema Airzone non valido" }, "step": { "user": { diff --git a/homeassistant/components/airzone/translations/ja.json b/homeassistant/components/airzone/translations/ja.json index 71b8bf1c908..28ebf809dbf 100644 --- a/homeassistant/components/airzone/translations/ja.json +++ b/homeassistant/components/airzone/translations/ja.json @@ -4,7 +4,8 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_system_id": "Airzone\u30b7\u30b9\u30c6\u30e0ID\u304c\u7121\u52b9\u3067\u3059" }, "step": { "user": { diff --git a/homeassistant/components/airzone/translations/nl.json b/homeassistant/components/airzone/translations/nl.json index 4611c23eb1e..0e84f756de1 100644 --- a/homeassistant/components/airzone/translations/nl.json +++ b/homeassistant/components/airzone/translations/nl.json @@ -4,7 +4,8 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kon niet verbinden" + "cannot_connect": "Kon niet verbinden", + "invalid_system_id": "Ongeldige Airzone systeem ID" }, "step": { "user": { diff --git a/homeassistant/components/airzone/translations/no.json b/homeassistant/components/airzone/translations/no.json index f078016b761..6eeaee3a53a 100644 --- a/homeassistant/components/airzone/translations/no.json +++ b/homeassistant/components/airzone/translations/no.json @@ -4,7 +4,8 @@ "already_configured": "Enheten er allerede konfigurert" }, "error": { - "cannot_connect": "Tilkobling mislyktes" + "cannot_connect": "Tilkobling mislyktes", + "invalid_system_id": "Ugyldig Airzone System ID" }, "step": { "user": { diff --git a/homeassistant/components/airzone/translations/pl.json b/homeassistant/components/airzone/translations/pl.json index 38dc359b248..e389618ff80 100644 --- a/homeassistant/components/airzone/translations/pl.json +++ b/homeassistant/components/airzone/translations/pl.json @@ -4,7 +4,8 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_system_id": "Nieprawid\u0142owy identyfikator systemu Airzone" }, "step": { "user": { diff --git a/homeassistant/components/airzone/translations/pt-BR.json b/homeassistant/components/airzone/translations/pt-BR.json index 1a8df1fef99..c2668c937b4 100644 --- a/homeassistant/components/airzone/translations/pt-BR.json +++ b/homeassistant/components/airzone/translations/pt-BR.json @@ -4,7 +4,8 @@ "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falha ao conectar" + "cannot_connect": "Falha ao conectar", + "invalid_system_id": "ID do sistema Airzone inv\u00e1lido" }, "step": { "user": { diff --git a/homeassistant/components/airzone/translations/ru.json b/homeassistant/components/airzone/translations/ru.json index 6032b0bdf00..d480866b262 100644 --- a/homeassistant/components/airzone/translations/ru.json +++ b/homeassistant/components/airzone/translations/ru.json @@ -4,7 +4,8 @@ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_system_id": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0441\u0438\u0441\u0442\u0435\u043c\u044b Airzone." }, "step": { "user": { diff --git a/homeassistant/components/airzone/translations/tr.json b/homeassistant/components/airzone/translations/tr.json new file mode 100644 index 00000000000..c911478ec32 --- /dev/null +++ b/homeassistant/components/airzone/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_system_id": "Ge\u00e7ersiz Airzone Sistem Kimli\u011fi" + }, + "step": { + "user": { + "data": { + "host": "Sunucu", + "port": "Port" + }, + "description": "Airzone entegrasyonunu ayarlay\u0131n." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/zh-Hant.json b/homeassistant/components/airzone/translations/zh-Hant.json index 01e2db9730b..42166fe39ec 100644 --- a/homeassistant/components/airzone/translations/zh-Hant.json +++ b/homeassistant/components/airzone/translations/zh-Hant.json @@ -4,7 +4,8 @@ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557" + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_system_id": "\u7121\u6548\u7684 Airzone \u7cfb\u7d71 ID" }, "step": { "user": { diff --git a/homeassistant/components/almond/translations/hu.json b/homeassistant/components/almond/translations/hu.json index b0a3a1b2461..b424675faaa 100644 --- a/homeassistant/components/almond/translations/hu.json +++ b/homeassistant/components/almond/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "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.", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "step": { @@ -12,7 +12,7 @@ "title": "Almond - Home Assistant b\u0151v\u00edtm\u00e9nnyel" }, "pick_implementation": { - "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" } } } diff --git a/homeassistant/components/almond/translations/it.json b/homeassistant/components/almond/translations/it.json index 58eadad0d80..1d028e73111 100644 --- a/homeassistant/components/almond/translations/it.json +++ b/homeassistant/components/almond/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "cannot_connect": "Impossibile connettersi", - "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, diff --git a/homeassistant/components/ambee/translations/hu.json b/homeassistant/components/ambee/translations/hu.json index e4cef44c5ba..80b14ac7470 100644 --- a/homeassistant/components/ambee/translations/hu.json +++ b/homeassistant/components/ambee/translations/hu.json @@ -19,7 +19,7 @@ "api_key": "API kulcs", "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" }, "description": "Integr\u00e1lja \u00f6ssze Ambeet Home Assistanttal." } diff --git a/homeassistant/components/ambiclimate/translations/hu.json b/homeassistant/components/ambiclimate/translations/hu.json index 1e67873f1aa..3de93421f42 100644 --- a/homeassistant/components/ambiclimate/translations/hu.json +++ b/homeassistant/components/ambiclimate/translations/hu.json @@ -9,12 +9,12 @@ "default": "Sikeres hiteles\u00edt\u00e9s" }, "error": { - "follow_link": "K\u00e9rem, k\u00f6vesse a hivatkoz\u00e1st \u00e9s hiteles\u00edtse mag\u00e1t miel\u0151tt megnyomn\u00e1 a K\u00fcld\u00e9s gombot", + "follow_link": "K\u00e9rem, k\u00f6vesse a hivatkoz\u00e1st \u00e9s hiteles\u00edtse mag\u00e1t miel\u0151tt folytatn\u00e1", "no_token": "Ambiclimate-al nem siker\u00fclt a hiteles\u00edt\u00e9s" }, "step": { "auth": { - "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})", + "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 **Mehet** 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/ambiclimate/translations/it.json b/homeassistant/components/ambiclimate/translations/it.json index ccf0836ee1d..d48f6748bbe 100644 --- a/homeassistant/components/ambiclimate/translations/it.json +++ b/homeassistant/components/ambiclimate/translations/it.json @@ -3,13 +3,13 @@ "abort": { "access_token": "Errore sconosciuto durante la generazione di un token di accesso.", "already_configured": "L'account \u00e8 gi\u00e0 configurato", - "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione." + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione." }, "create_entry": { "default": "Autenticazione riuscita" }, "error": { - "follow_link": "Si prega di seguire il link e di autenticarsi prima di premere Invia", + "follow_link": "Segui il collegamento e autenticati prima di premere Invia", "no_token": "Non autenticato con Ambiclimate" }, "step": { diff --git a/homeassistant/components/ambient_station/translations/hu.json b/homeassistant/components/ambient_station/translations/hu.json index 6974bda2c20..bc2ff29b7ee 100644 --- a/homeassistant/components/ambient_station/translations/hu.json +++ b/homeassistant/components/ambient_station/translations/hu.json @@ -5,7 +5,7 @@ }, "error": { "invalid_key": "\u00c9rv\u00e9nytelen API kulcs", - "no_devices": "Nincs a fi\u00f3kodban tal\u00e1lhat\u00f3 eszk\u00f6z" + "no_devices": "Nem tal\u00e1lhat\u00f3 a fi\u00f3kj\u00e1ban eszk\u00f6z" }, "step": { "user": { diff --git a/homeassistant/components/androidtv/translations/hu.json b/homeassistant/components/androidtv/translations/hu.json index c8d8c0fd97b..3b75153a351 100644 --- a/homeassistant/components/androidtv/translations/hu.json +++ b/homeassistant/components/androidtv/translations/hu.json @@ -43,12 +43,12 @@ "init": { "data": { "apps": "Alkalmaz\u00e1slista konfigur\u00e1l\u00e1sa", - "exclude_unnamed_apps": "Ismeretlen nev\u0171 alkalmaz\u00e1s kiz\u00e1r\u00e1sa", - "get_sources": "A fut\u00f3 alkalmaz\u00e1sok forr\u00e1slist\u00e1jak\u00e9nt val\u00f3 megjelen\u00edt\u00e9se", - "screencap": "A k\u00e9perny\u0151n megjelen\u0151 k\u00e9p legyen-e az albumbor\u00edt\u00f3", + "exclude_unnamed_apps": "Az ismeretlen nev\u0171 alkalmaz\u00e1sok kiz\u00e1r\u00e1sa a forr\u00e1slist\u00e1b\u00f3l", + "get_sources": "A fut\u00f3 alkalmaz\u00e1sok megjelen\u00edt\u00e9se a bemeneti forr\u00e1sok list\u00e1j\u00e1ban", + "screencap": "Haszn\u00e1ljon k\u00e9perny\u0151felv\u00e9telt az albumbor\u00edt\u00f3khoz", "state_detection_rules": "\u00c1llapotfelismer\u00e9si szab\u00e1lyok konfigur\u00e1l\u00e1sa", - "turn_off_command": "ADB shell parancs az alap\u00e9rtelmezett kikapcsol\u00e1si (turn_off) parancs fel\u00fcl\u00edr\u00e1s\u00e1ra", - "turn_on_command": "ADB shell parancs az alap\u00e9rtelmezett bekapcsol\u00e1si (turn_on) parancs fel\u00fcl\u00edr\u00e1s\u00e1ra" + "turn_off_command": "ADB shell kikapcsol\u00e1si parancs (alap\u00e9rtelmez\u00e9s szerint hagyja \u00fcresen)", + "turn_on_command": "ADB shell bekapcsol\u00e1si parancs (alap\u00e9rtelmez\u00e9s szerint hagyja \u00fcresen)" }, "title": "Android TV be\u00e1ll\u00edt\u00e1sok" }, diff --git a/homeassistant/components/apple_tv/translations/bg.json b/homeassistant/components/apple_tv/translations/bg.json index 4629a79d152..b1923cab650 100644 --- a/homeassistant/components/apple_tv/translations/bg.json +++ b/homeassistant/components/apple_tv/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", "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", + "ipv6_not_supported": "IPv6 \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430.", "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", "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" diff --git a/homeassistant/components/apple_tv/translations/ca.json b/homeassistant/components/apple_tv/translations/ca.json index 88c49059067..c672733193a 100644 --- a/homeassistant/components/apple_tv/translations/ca.json +++ b/homeassistant/components/apple_tv/translations/ca.json @@ -9,6 +9,7 @@ "device_not_found": "No s'ha trobat el dispositiu durant el descobriment, prova de tornar-lo a afegir.", "inconsistent_device": "Els protocols esperats no s'han trobat durant el descobriment. Normalment aix\u00f2 indica un problema amb el DNS multicast (Zeroconf). Prova d'afegir el dispositiu de nou.", "invalid_config": "La configuraci\u00f3 d'aquest dispositiu no est\u00e0 completa. Intenta'l tornar a afegir.", + "ipv6_not_supported": "IPv6 no est\u00e0 suportat.", "no_devices_found": "No s'han trobat dispositius a la xarxa", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "setup_failed": "No s'ha pogut configurar el dispositiu.", diff --git a/homeassistant/components/apple_tv/translations/de.json b/homeassistant/components/apple_tv/translations/de.json index ca6f69f5442..48149ce8394 100644 --- a/homeassistant/components/apple_tv/translations/de.json +++ b/homeassistant/components/apple_tv/translations/de.json @@ -9,6 +9,7 @@ "device_not_found": "Das Ger\u00e4t wurde bei der Erkennung nicht gefunden. Bitte versuche es erneut hinzuzuf\u00fcgen.", "inconsistent_device": "Die erwarteten Protokolle wurden bei der Erkennung nicht gefunden. Dies deutet normalerweise auf ein Problem mit Multicast-DNS (Zeroconf) hin. Bitte versuche das Ger\u00e4t erneut hinzuzuf\u00fcgen.", "invalid_config": "Die Konfiguration f\u00fcr dieses Ger\u00e4t ist unvollst\u00e4ndig. Bitte versuche, es erneut hinzuzuf\u00fcgen.", + "ipv6_not_supported": "IPv6 wird nicht unterst\u00fctzt.", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "setup_failed": "Fehler beim Einrichten des Ger\u00e4ts.", diff --git a/homeassistant/components/apple_tv/translations/el.json b/homeassistant/components/apple_tv/translations/el.json index 11a61899b84..cbe36bf56fb 100644 --- a/homeassistant/components/apple_tv/translations/el.json +++ b/homeassistant/components/apple_tv/translations/el.json @@ -9,6 +9,7 @@ "device_not_found": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", "inconsistent_device": "\u03a4\u03b1 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b1 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03b1 \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7. \u0391\u03c5\u03c4\u03cc \u03c3\u03c5\u03bd\u03ae\u03b8\u03c9\u03c2 \u03c5\u03c0\u03bf\u03b4\u03b7\u03bb\u03ce\u03bd\u03b5\u03b9 \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1 \u03bc\u03b5 \u03c4\u03bf multicast DNS (Zeroconf). \u03a0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae.", "invalid_config": "\u0397 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bb\u03bb\u03b9\u03c0\u03ae\u03c2. \u03a0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", + "ipv6_not_supported": "\u03a4\u03bf IPv6 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9.", "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", "setup_failed": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2.", diff --git a/homeassistant/components/apple_tv/translations/en.json b/homeassistant/components/apple_tv/translations/en.json index f455d590d79..792eaf32c29 100644 --- a/homeassistant/components/apple_tv/translations/en.json +++ b/homeassistant/components/apple_tv/translations/en.json @@ -2,11 +2,13 @@ "config": { "abort": { "already_configured": "Device is already configured", + "already_configured_device": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "backoff": "Device does not accept pairing requests at this time (you might have entered an invalid PIN code too many times), try again later.", "device_did_not_pair": "No attempt to finish pairing process was made from the device.", "device_not_found": "Device was not found during discovery, please try adding it again.", "inconsistent_device": "Expected protocols were not found during discovery. This normally indicates a problem with multicast DNS (Zeroconf). Please try adding the device again.", + "invalid_config": "The configuration for this device is incomplete. Please try adding it again.", "ipv6_not_supported": "IPv6 is not supported.", "no_devices_found": "No devices found on the network", "reauth_successful": "Re-authentication was successful", @@ -17,6 +19,7 @@ "already_configured": "Device is already configured", "invalid_auth": "Invalid authentication", "no_devices_found": "No devices found on the network", + "no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.", "unknown": "Unexpected error" }, "flow_title": "{name} ({type})", @@ -70,5 +73,6 @@ "description": "Configure general device settings" } } - } + }, + "title": "Apple TV" } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/et.json b/homeassistant/components/apple_tv/translations/et.json index fa660662d8b..8180b95fe12 100644 --- a/homeassistant/components/apple_tv/translations/et.json +++ b/homeassistant/components/apple_tv/translations/et.json @@ -9,6 +9,7 @@ "device_not_found": "Seadet avastamise ajal ei leitud, proovi seda uuesti lisada.", "inconsistent_device": "Eeldatavaid protokolle avastamise ajal ei leitud. See n\u00e4itab tavaliselt probleemi multcast DNS-iga (Zeroconf). Proovi seade uuesti lisada.", "invalid_config": "Selle seadme s\u00e4tted on puudulikud. Proovi see uuesti lisada.", + "ipv6_not_supported": "IPv6 ei ole toetatud.", "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet", "reauth_successful": "Taastuvastamine \u00f5nnestus", "setup_failed": "Seadme h\u00e4\u00e4lestamine nurjus.", diff --git a/homeassistant/components/apple_tv/translations/fr.json b/homeassistant/components/apple_tv/translations/fr.json index 510d22c79a5..6d7deea6e5a 100644 --- a/homeassistant/components/apple_tv/translations/fr.json +++ b/homeassistant/components/apple_tv/translations/fr.json @@ -9,6 +9,7 @@ "device_not_found": "L'appareil n'a pas \u00e9t\u00e9 trouv\u00e9 lors de la d\u00e9couverte, veuillez r\u00e9essayer de l'ajouter.", "inconsistent_device": "Les protocoles attendus n'ont pas \u00e9t\u00e9 trouv\u00e9s lors de la d\u00e9couverte. Cela indique normalement un probl\u00e8me avec le DNS multicast (Zeroconf). Veuillez r\u00e9essayer d'ajouter l'appareil.", "invalid_config": "La configuration de cet appareil est incompl\u00e8te. Veuillez r\u00e9essayer de l'ajouter.", + "ipv6_not_supported": "IPv6 n'est pas pris en charge.", "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", "setup_failed": "\u00c9chec de la configuration de l'appareil.", diff --git a/homeassistant/components/apple_tv/translations/he.json b/homeassistant/components/apple_tv/translations/he.json index 03e7dc5d4fe..61ca863bd66 100644 --- a/homeassistant/components/apple_tv/translations/he.json +++ b/homeassistant/components/apple_tv/translations/he.json @@ -4,6 +4,7 @@ "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_configured_device": "\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", + "ipv6_not_supported": "IPv6 \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da.", "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" diff --git a/homeassistant/components/apple_tv/translations/hu.json b/homeassistant/components/apple_tv/translations/hu.json index f76063c5eeb..73a30fbdd9a 100644 --- a/homeassistant/components/apple_tv/translations/hu.json +++ b/homeassistant/components/apple_tv/translations/hu.json @@ -3,12 +3,13 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "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.", "device_not_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a felder\u00edt\u00e9s sor\u00e1n, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni.", "inconsistent_device": "Az elv\u00e1rt protokollok nem tal\u00e1lhat\u00f3k a felder\u00edt\u00e9s sor\u00e1n. Ez \u00e1ltal\u00e1ban a multicast DNS (Zeroconf) probl\u00e9m\u00e1j\u00e1t jelzi. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni az eszk\u00f6zt.", "invalid_config": "Az eszk\u00f6z konfigur\u00e1l\u00e1sa nem teljes. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni.", + "ipv6_not_supported": "Az IPv6 nem t\u00e1mogatott.", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "setup_failed": "Az eszk\u00f6z be\u00e1ll\u00edt\u00e1sa sikertelen.", @@ -21,14 +22,14 @@ "no_usable_service": "Tal\u00e1ltunk egy eszk\u00f6zt, de nem tudtuk azonos\u00edtani, hogyan lehetne kapcsolatot l\u00e9tes\u00edteni vele. Ha tov\u00e1bbra is ezt az \u00fczenetet l\u00e1tja, pr\u00f3b\u00e1lja meg megadni az IP-c\u00edm\u00e9t, vagy ind\u00edtsa \u00fajra az Apple TV-t.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, - "flow_title": "{name}", + "flow_title": "{name} ({type})", "step": { "confirm": { - "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!", + "description": "\u00d6n a `{name}`, `{type}` t\u00edpus\u00fa eszk\u00f6zt k\u00e9sz\u00fcl hozz\u00e1adni a Home Assistanthoz.\n\n**A folyamat befejez\u00e9s\u00e9hez t\u00f6bb PIN k\u00f3dot is meg kell adnia.**\n\nK\u00e9rj\u00fck, vegye figyelembe, hogy ezzel az integr\u00e1ci\u00f3val *nem* tudja kikapcsolni az Apple TV k\u00e9sz\u00fcl\u00e9k\u00e9t. Csak a Home Assistant m\u00e9dialej\u00e1tsz\u00f3ja fog kikapcsolni!", "title": "Apple TV sikeresen hozz\u00e1adva" }, "pair_no_pin": { - "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.", + "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a {protocol} szolg\u00e1ltat\u00e1shoz. A folytat\u00e1shoz k\u00e9rj\u00fck, \u00edrja be k\u00e9sz\u00fcl\u00e9ken a PIN k\u00f3dot: {pin}.", "title": "P\u00e1ros\u00edt\u00e1s" }, "pair_with_pin": { @@ -47,7 +48,7 @@ "title": "A p\u00e1ros\u00edt\u00e1s nem lehets\u00e9ges" }, "reconfigure": { - "description": "Ez az Apple TV csatlakoz\u00e1si neh\u00e9zs\u00e9gekkel k\u00fczd, ez\u00e9rt \u00fajra kell konfigur\u00e1lni.", + "description": "Konfigur\u00e1lja \u00fajra ezt az eszk\u00f6zt a m\u0171k\u00f6d\u0151k\u00e9pess\u00e9g vissza\u00e1ll\u00edt\u00e1s\u00e1hoz.", "title": "Eszk\u00f6z \u00fajrakonfigur\u00e1l\u00e1sa" }, "service_problem": { @@ -58,7 +59,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\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}", + "description": "Kezdje a hozz\u00e1adni k\u00edv\u00e1nt Apple TV eszk\u00f6znev\u00e9nek (pl. Konyha vagy H\u00e1l\u00f3szoba) vagy IP-c\u00edm\u00e9nek megad\u00e1s\u00e1val. \n\n Ha nem l\u00e1tja az eszk\u00f6zt, vagy b\u00e1rmilyen probl\u00e9m\u00e1t tapasztal, pr\u00f3b\u00e1lja meg megadni az eszk\u00f6z IP-c\u00edm\u00e9t.", "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 8a978eca737..7120d0671b8 100644 --- a/homeassistant/components/apple_tv/translations/id.json +++ b/homeassistant/components/apple_tv/translations/id.json @@ -9,6 +9,7 @@ "device_not_found": "Perangkat tidak ditemukan selama penemuan, coba tambahkan lagi.", "inconsistent_device": "Protokol yang diharapkan tidak ditemukan selama penemuan. Ini biasanya terjadi karena masalah dengan DNS multicast (Zeroconf). Coba tambahkan perangkat lagi.", "invalid_config": "Konfigurasi untuk perangkat ini tidak lengkap. Coba tambahkan lagi.", + "ipv6_not_supported": "IPv6 tidak didukung.", "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", "reauth_successful": "Autentikasi ulang berhasil", "setup_failed": "Gagal menyiapkan perangkat.", diff --git a/homeassistant/components/apple_tv/translations/it.json b/homeassistant/components/apple_tv/translations/it.json index 47bd8612265..b1e3a06440f 100644 --- a/homeassistant/components/apple_tv/translations/it.json +++ b/homeassistant/components/apple_tv/translations/it.json @@ -9,6 +9,7 @@ "device_not_found": "Il dispositivo non \u00e8 stato trovato durante il rilevamento, prova ad aggiungerlo di nuovo.", "inconsistent_device": "I protocolli previsti non sono stati trovati durante il rilevamento. Questo normalmente indica un problema con DNS multicast (Zeroconf). Prova ad aggiungere di nuovo il dispositivo.", "invalid_config": "La configurazione per questo dispositivo \u00e8 incompleta. Prova ad aggiungerlo di nuovo.", + "ipv6_not_supported": "IPv6 non \u00e8 supportato.", "no_devices_found": "Nessun dispositivo trovato sulla rete", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "setup_failed": "Impossibile configurare il dispositivo.", diff --git a/homeassistant/components/apple_tv/translations/ja.json b/homeassistant/components/apple_tv/translations/ja.json index c70dda18d01..9984006365e 100644 --- a/homeassistant/components/apple_tv/translations/ja.json +++ b/homeassistant/components/apple_tv/translations/ja.json @@ -9,6 +9,7 @@ "device_not_found": "\u691c\u51fa\u4e2d\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u3082\u3046\u4e00\u5ea6\u8ffd\u52a0\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", "inconsistent_device": "\u691c\u51fa\u4e2d\u306b\u671f\u5f85\u3057\u305f\u30d7\u30ed\u30c8\u30b3\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u3053\u308c\u306f\u901a\u5e38\u3001\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8DNS(Zeroconf)\u306b\u554f\u984c\u304c\u3042\u308b\u3053\u3068\u3092\u793a\u3057\u3066\u3044\u307e\u3059\u3002\u30c7\u30d0\u30a4\u30b9\u3092\u3082\u3046\u4e00\u5ea6\u8ffd\u52a0\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", "invalid_config": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a\u306f\u4e0d\u5b8c\u5168\u3067\u3059\u3002\u3082\u3046\u4e00\u5ea6\u8ffd\u52a0\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", + "ipv6_not_supported": "IPv6\u306b\u306f\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093\u3002", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "setup_failed": "\u30c7\u30d0\u30a4\u30b9\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", diff --git a/homeassistant/components/apple_tv/translations/nl.json b/homeassistant/components/apple_tv/translations/nl.json index 7fdc20c7291..8aaf120403c 100644 --- a/homeassistant/components/apple_tv/translations/nl.json +++ b/homeassistant/components/apple_tv/translations/nl.json @@ -9,6 +9,7 @@ "device_not_found": "Apparaat werd niet gevonden tijdens het zoeken, probeer het opnieuw toe te voegen.", "inconsistent_device": "De verwachte protocollen zijn niet gevonden tijdens het zoeken. Dit wijst gewoonlijk op een probleem met multicast DNS (Zeroconf). Probeer het apparaat opnieuw toe te voegen.", "invalid_config": "De configuratie voor dit apparaat is onvolledig. Probeer het opnieuw toe te voegen.", + "ipv6_not_supported": "IPv6 wordt niet ondersteund.", "no_devices_found": "Geen apparaten gevonden op het netwerk", "reauth_successful": "Herauthenticatie was succesvol", "setup_failed": "Kan het apparaat niet instellen.", diff --git a/homeassistant/components/apple_tv/translations/no.json b/homeassistant/components/apple_tv/translations/no.json index b24c48a396e..e364633597e 100644 --- a/homeassistant/components/apple_tv/translations/no.json +++ b/homeassistant/components/apple_tv/translations/no.json @@ -9,6 +9,7 @@ "device_not_found": "Enheten ble ikke funnet under oppdagelsen. Pr\u00f8v \u00e5 legge den til p\u00e5 nytt.", "inconsistent_device": "Forventede protokoller ble ikke funnet under oppdagelsen. Dette indikerer vanligvis et problem med multicast DNS (Zeroconf). Pr\u00f8v \u00e5 legge til enheten p\u00e5 nytt.", "invalid_config": "Konfigurasjonen for denne enheten er ufullstendig. Pr\u00f8v \u00e5 legge den til p\u00e5 nytt.", + "ipv6_not_supported": "IPv6 st\u00f8ttes ikke.", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "setup_failed": "Kunne ikke konfigurere enheten.", diff --git a/homeassistant/components/apple_tv/translations/pl.json b/homeassistant/components/apple_tv/translations/pl.json index 1ba9b46dc3b..303de09c1dd 100644 --- a/homeassistant/components/apple_tv/translations/pl.json +++ b/homeassistant/components/apple_tv/translations/pl.json @@ -9,6 +9,7 @@ "device_not_found": "Urz\u0105dzenie nie zosta\u0142o znalezione podczas wykrywania, spr\u00f3buj doda\u0107 je ponownie.", "inconsistent_device": "Oczekiwane protoko\u0142y nie zosta\u0142y znalezione podczas wykrywania. Zwykle wskazuje to na problem z multicastem DNS (Zeroconf). Spr\u00f3buj ponownie doda\u0107 urz\u0105dzenie.", "invalid_config": "Konfiguracja tego urz\u0105dzenia jest niekompletna. Spr\u00f3buj doda\u0107 go ponownie.", + "ipv6_not_supported": "IPv6 nie jest obs\u0142ugiwany.", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", "setup_failed": "Nie uda\u0142o si\u0119 skonfigurowa\u0107 urz\u0105dzenia.", diff --git a/homeassistant/components/apple_tv/translations/pt-BR.json b/homeassistant/components/apple_tv/translations/pt-BR.json index 79fee5f02ec..539335c17d3 100644 --- a/homeassistant/components/apple_tv/translations/pt-BR.json +++ b/homeassistant/components/apple_tv/translations/pt-BR.json @@ -9,6 +9,7 @@ "device_not_found": "O dispositivo n\u00e3o foi encontrado durante a descoberta. Tente adicion\u00e1-lo novamente.", "inconsistent_device": "Os protocolos esperados n\u00e3o foram encontrados durante a descoberta. Isso normalmente indica um problema com o DNS multicast (Zeroconf). Tente adicionar o dispositivo novamente.", "invalid_config": "A configura\u00e7\u00e3o deste dispositivo est\u00e1 incompleta. Tente adicion\u00e1-lo novamente.", + "ipv6_not_supported": "IPv6 n\u00e3o \u00e9 suportado.", "no_devices_found": "Nenhum dispositivo encontrado na rede", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", "setup_failed": "Falha ao configurar o dispositivo.", diff --git a/homeassistant/components/apple_tv/translations/ru.json b/homeassistant/components/apple_tv/translations/ru.json index 4863a541603..88516d45af3 100644 --- a/homeassistant/components/apple_tv/translations/ru.json +++ b/homeassistant/components/apple_tv/translations/ru.json @@ -9,6 +9,7 @@ "device_not_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0433\u043e \u0435\u0449\u0451 \u0440\u0430\u0437.", "inconsistent_device": "\u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0435 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u044b. \u041e\u0431\u044b\u0447\u043d\u043e \u044d\u0442\u043e \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u043d\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443 \u0441 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u044b\u043c DNS (Zeroconf). \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0435\u0449\u0451 \u0440\u0430\u0437.", "invalid_config": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0433\u043e \u0435\u0449\u0451 \u0440\u0430\u0437.", + "ipv6_not_supported": "IPv6 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", "setup_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e.", diff --git a/homeassistant/components/apple_tv/translations/tr.json b/homeassistant/components/apple_tv/translations/tr.json index 4b9c2a4ca07..cee9fcce81e 100644 --- a/homeassistant/components/apple_tv/translations/tr.json +++ b/homeassistant/components/apple_tv/translations/tr.json @@ -9,6 +9,7 @@ "device_not_found": "Cihaz ke\u015fif s\u0131ras\u0131nda bulunamad\u0131, l\u00fctfen tekrar eklemeyi deneyin.", "inconsistent_device": "Ke\u015fif s\u0131ras\u0131nda beklenen protokoller bulunamad\u0131. Bu normalde \u00e7ok noktaya yay\u0131n DNS (Zeroconf) ile ilgili bir sorunu g\u00f6sterir. L\u00fctfen cihaz\u0131 tekrar eklemeyi deneyin.", "invalid_config": "Bu ayg\u0131t\u0131n yap\u0131land\u0131rmas\u0131 tamamlanmad\u0131. L\u00fctfen tekrar eklemeyi deneyin.", + "ipv6_not_supported": "IPv6 desteklenmiyor.", "no_devices_found": "A\u011fda cihaz bulunamad\u0131", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", "setup_failed": "Cihaz kurulumu ba\u015far\u0131s\u0131z.", diff --git a/homeassistant/components/apple_tv/translations/zh-Hant.json b/homeassistant/components/apple_tv/translations/zh-Hant.json index 41732c0813e..b4e5108d474 100644 --- a/homeassistant/components/apple_tv/translations/zh-Hant.json +++ b/homeassistant/components/apple_tv/translations/zh-Hant.json @@ -9,6 +9,7 @@ "device_not_found": "\u641c\u5c0b\u4e0d\u5230\u88dd\u7f6e\u3001\u8acb\u8a66\u8457\u518d\u65b0\u589e\u4e00\u6b21\u3002", "inconsistent_device": "\u641c\u5c0b\u4e0d\u5230\u9810\u671f\u7684\u901a\u8a0a\u5354\u5b9a\u3002\u901a\u5e38\u539f\u56e0\u70ba Multicast DNS (Zeroconf) \u554f\u984c\u3001\u8acb\u8a66\u8457\u518d\u65b0\u589e\u4e00\u6b21\u3002", "invalid_config": "\u6b64\u88dd\u7f6e\u8a2d\u5b9a\u4e0d\u5b8c\u6574\uff0c\u8acb\u7a0d\u5019\u518d\u8a66\u4e00\u6b21\u3002", + "ipv6_not_supported": "\u4e0d\u652f\u63f4 IPv6\u3002", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", "setup_failed": "\u88dd\u7f6e\u8a2d\u5b9a\u5931\u6557\u3002", diff --git a/homeassistant/components/arcam_fmj/translations/hu.json b/homeassistant/components/arcam_fmj/translations/hu.json index 964ebe2a33d..897b462eb48 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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "error": { diff --git a/homeassistant/components/arcam_fmj/translations/it.json b/homeassistant/components/arcam_fmj/translations/it.json index 2b99566888b..637bfa6533d 100644 --- a/homeassistant/components/arcam_fmj/translations/it.json +++ b/homeassistant/components/arcam_fmj/translations/it.json @@ -6,8 +6,8 @@ "cannot_connect": "Impossibile connettersi" }, "error": { - "one": "Pi\u00f9", - "other": "Altri" + "one": "Vuoto", + "other": "Vuoti" }, "flow_title": "{host}", "step": { diff --git a/homeassistant/components/asuswrt/translations/ca.json b/homeassistant/components/asuswrt/translations/ca.json index d034aefb407..9149cd1ae7a 100644 --- a/homeassistant/components/asuswrt/translations/ca.json +++ b/homeassistant/components/asuswrt/translations/ca.json @@ -18,7 +18,7 @@ "mode": "Mode", "name": "Nom", "password": "Contrasenya", - "port": "Port", + "port": "Port (deixa-ho buit pel predeterminat del protocol)", "protocol": "Protocol de comunicacions a utilitzar", "ssh_key": "Ruta al fitxer de claus SSH (en lloc de la contrasenya)", "username": "Nom d'usuari" diff --git a/homeassistant/components/asuswrt/translations/de.json b/homeassistant/components/asuswrt/translations/de.json index f5e41e09ac2..af4beb984a4 100644 --- a/homeassistant/components/asuswrt/translations/de.json +++ b/homeassistant/components/asuswrt/translations/de.json @@ -18,7 +18,7 @@ "mode": "Modus", "name": "Name", "password": "Passwort", - "port": "Port", + "port": "Port (leer lassen f\u00fcr Protokollstandard)", "protocol": "Zu verwendendes Kommunikationsprotokoll", "ssh_key": "Pfad zu deiner SSH-Schl\u00fcsseldatei (anstelle des Passworts)", "username": "Benutzername" diff --git a/homeassistant/components/asuswrt/translations/et.json b/homeassistant/components/asuswrt/translations/et.json index 61e8b1a8d4f..7b7a0869061 100644 --- a/homeassistant/components/asuswrt/translations/et.json +++ b/homeassistant/components/asuswrt/translations/et.json @@ -18,7 +18,7 @@ "mode": "Re\u017eiim", "name": "Nimi", "password": "Salas\u00f5na", - "port": "Port", + "port": "Port (vaikepordi kasutamiseks j\u00e4ta t\u00fchjaks)", "protocol": "Kasutatav sideprotokoll", "ssh_key": "Rada SSH v\u00f5tmefailini (parooli asemel)", "username": "Kasutajanimi" diff --git a/homeassistant/components/asuswrt/translations/fr.json b/homeassistant/components/asuswrt/translations/fr.json index 0d53f3f24cf..5c8882d5813 100644 --- a/homeassistant/components/asuswrt/translations/fr.json +++ b/homeassistant/components/asuswrt/translations/fr.json @@ -18,7 +18,7 @@ "mode": "Mode", "name": "Nom", "password": "Mot de passe", - "port": "Port", + "port": "Port (laisser vide pour utiliser la valeur par d\u00e9faut du protocole)", "protocol": "Protocole de communication \u00e0 utiliser", "ssh_key": "Chemin d'acc\u00e8s \u00e0 votre fichier de cl\u00e9s SSH (au lieu du mot de passe)", "username": "Nom d'utilisateur" diff --git a/homeassistant/components/asuswrt/translations/he.json b/homeassistant/components/asuswrt/translations/he.json index 2d2cebaa7e3..867cc6e4f5c 100644 --- a/homeassistant/components/asuswrt/translations/he.json +++ b/homeassistant/components/asuswrt/translations/he.json @@ -18,7 +18,7 @@ "mode": "\u05de\u05e6\u05d1", "name": "\u05e9\u05dd", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "port": "\u05e4\u05ea\u05d7\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4 (\u05e8\u05d9\u05e7 \u05e2\u05d1\u05d5\u05e8 \u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc)", "ssh_key": "\u05e0\u05ea\u05d9\u05d1 \u05dc\u05e7\u05d5\u05d1\u05e5 \u05d4\u05de\u05e4\u05ea\u05d7 \u05e9\u05dc SSH (\u05d1\u05de\u05e7\u05d5\u05dd \u05dc\u05e1\u05d9\u05e1\u05de\u05d4)", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } diff --git a/homeassistant/components/asuswrt/translations/hu.json b/homeassistant/components/asuswrt/translations/hu.json index ff64372f1b0..d8133061380 100644 --- a/homeassistant/components/asuswrt/translations/hu.json +++ b/homeassistant/components/asuswrt/translations/hu.json @@ -16,9 +16,9 @@ "data": { "host": "C\u00edm", "mode": "M\u00f3d", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3", - "port": "Port", + "port": "Port (hagyja \u00fcresen az alap\u00e9rtelmezetthez)", "protocol": "Haszn\u00e1lhat\u00f3 kommunik\u00e1ci\u00f3s protokoll", "ssh_key": "Az SSH kulcsf\u00e1jl el\u00e9r\u00e9si \u00fatja (jelsz\u00f3 helyett)", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" diff --git a/homeassistant/components/asuswrt/translations/id.json b/homeassistant/components/asuswrt/translations/id.json index aa4eebd1f86..83ade7b6462 100644 --- a/homeassistant/components/asuswrt/translations/id.json +++ b/homeassistant/components/asuswrt/translations/id.json @@ -18,7 +18,7 @@ "mode": "Mode", "name": "Nama", "password": "Kata Sandi", - "port": "Port", + "port": "Port (Biarkan kosong untuk nilai default)", "protocol": "Protokol komunikasi yang akan digunakan", "ssh_key": "Jalur ke file kunci SSH Anda (bukan kata sandi)", "username": "Nama Pengguna" diff --git a/homeassistant/components/asuswrt/translations/it.json b/homeassistant/components/asuswrt/translations/it.json index 824db386176..0cf06679d83 100644 --- a/homeassistant/components/asuswrt/translations/it.json +++ b/homeassistant/components/asuswrt/translations/it.json @@ -7,7 +7,7 @@ "cannot_connect": "Impossibile connettersi", "invalid_host": "Nome host o indirizzo IP non valido", "pwd_and_ssh": "Fornire solo la password o il file della chiave SSH", - "pwd_or_ssh": "Si prega di fornire la password o il file della chiave SSH", + "pwd_or_ssh": "Fornisci la password o il file della chiave SSH", "ssh_not_file": "File chiave SSH non trovato", "unknown": "Errore imprevisto" }, @@ -18,7 +18,7 @@ "mode": "Modalit\u00e0", "name": "Nome", "password": "Password", - "port": "Porta", + "port": "Porta (lascia vuoto per impostazione predefinita del protocollo)", "protocol": "Protocollo di comunicazione da utilizzare", "ssh_key": "Percorso del file della chiave SSH (invece della password)", "username": "Nome utente" diff --git a/homeassistant/components/asuswrt/translations/nl.json b/homeassistant/components/asuswrt/translations/nl.json index f6f347f771f..e6db5fae621 100644 --- a/homeassistant/components/asuswrt/translations/nl.json +++ b/homeassistant/components/asuswrt/translations/nl.json @@ -18,7 +18,7 @@ "mode": "Mode", "name": "Naam", "password": "Wachtwoord", - "port": "Poort", + "port": "Poort (leeg laten voor protocol standaard)", "protocol": "Te gebruiken communicatieprotocol", "ssh_key": "Pad naar uw SSH-sleutelbestand (in plaats van wachtwoord)", "username": "Gebruikersnaam" diff --git a/homeassistant/components/asuswrt/translations/no.json b/homeassistant/components/asuswrt/translations/no.json index c1e5fd5d99a..84ece53ed42 100644 --- a/homeassistant/components/asuswrt/translations/no.json +++ b/homeassistant/components/asuswrt/translations/no.json @@ -18,7 +18,7 @@ "mode": "Modus", "name": "Navn", "password": "Passord", - "port": "Port", + "port": "Port (la st\u00e5 tomt for protokollstandard)", "protocol": "Kommunikasjonsprotokoll som skal brukes", "ssh_key": "Bane til SSH-n\u00f8kkelfilen (i stedet for passord)", "username": "Brukernavn" diff --git a/homeassistant/components/asuswrt/translations/pl.json b/homeassistant/components/asuswrt/translations/pl.json index 9fd5d00b1c4..76d8f30d950 100644 --- a/homeassistant/components/asuswrt/translations/pl.json +++ b/homeassistant/components/asuswrt/translations/pl.json @@ -18,7 +18,7 @@ "mode": "Tryb", "name": "Nazwa", "password": "Has\u0142o", - "port": "Port", + "port": "Port (pozostaw puste dla domy\u015blnego protoko\u0142u)", "protocol": "Wybierz protok\u00f3\u0142 komunikacyjny", "ssh_key": "\u015acie\u017cka do pliku z kluczem SSH (zamiast has\u0142a)", "username": "Nazwa u\u017cytkownika" diff --git a/homeassistant/components/asuswrt/translations/pt-BR.json b/homeassistant/components/asuswrt/translations/pt-BR.json index 06982ade622..88999128895 100644 --- a/homeassistant/components/asuswrt/translations/pt-BR.json +++ b/homeassistant/components/asuswrt/translations/pt-BR.json @@ -18,7 +18,7 @@ "mode": "Modo", "name": "Nome", "password": "Senha", - "port": "Porta", + "port": "Porta (deixe em branco para o protocolo padr\u00e3o)", "protocol": "Protocolo de comunica\u00e7\u00e3o a ser usado", "ssh_key": "Caminho para seu arquivo de chave SSH (em vez de senha)", "username": "Usu\u00e1rio" diff --git a/homeassistant/components/asuswrt/translations/ru.json b/homeassistant/components/asuswrt/translations/ru.json index 35254821f23..8edc9786f5b 100644 --- a/homeassistant/components/asuswrt/translations/ru.json +++ b/homeassistant/components/asuswrt/translations/ru.json @@ -18,7 +18,7 @@ "mode": "\u0420\u0435\u0436\u0438\u043c", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "port": "\u041f\u043e\u0440\u0442", + "port": "\u041f\u043e\u0440\u0442 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e)", "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0441\u0432\u044f\u0437\u0438", "ssh_key": "\u041f\u0443\u0442\u044c \u043a \u0444\u0430\u0439\u043b\u0443 \u043a\u043b\u044e\u0447\u0435\u0439 SSH (\u0432\u043c\u0435\u0441\u0442\u043e \u043f\u0430\u0440\u043e\u043b\u044f)", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" diff --git a/homeassistant/components/asuswrt/translations/tr.json b/homeassistant/components/asuswrt/translations/tr.json index 9b7870a3fba..3225c78b40a 100644 --- a/homeassistant/components/asuswrt/translations/tr.json +++ b/homeassistant/components/asuswrt/translations/tr.json @@ -18,7 +18,7 @@ "mode": "Mod", "name": "Ad", "password": "Parola", - "port": "Port", + "port": "Port (protokol varsay\u0131lan\u0131 i\u00e7in bo\u015f b\u0131rak\u0131n)", "protocol": "Kullan\u0131lacak ileti\u015fim protokol\u00fc", "ssh_key": "SSH anahtar dosyan\u0131z\u0131n yolu (\u015fifre yerine)", "username": "Kullan\u0131c\u0131 Ad\u0131" diff --git a/homeassistant/components/asuswrt/translations/zh-Hant.json b/homeassistant/components/asuswrt/translations/zh-Hant.json index d0997e495c5..17c5cd698cd 100644 --- a/homeassistant/components/asuswrt/translations/zh-Hant.json +++ b/homeassistant/components/asuswrt/translations/zh-Hant.json @@ -18,7 +18,7 @@ "mode": "\u6a21\u5f0f", "name": "\u540d\u7a31", "password": "\u5bc6\u78bc", - "port": "\u901a\u8a0a\u57e0", + "port": "\u901a\u8a0a\u57e0 (\u4fdd\u6301\u7a7a\u767d\u4f7f\u7528\u9810\u8a2d\u5354\u5b9a)", "protocol": "\u4f7f\u7528\u901a\u8a0a\u606f\u5354\u5b9a", "ssh_key": "SSH \u91d1\u9470\u6a94\u6848\u8def\u5f91\uff08\u975e\u5bc6\u78bc\uff09", "username": "\u4f7f\u7528\u8005\u540d\u7a31" diff --git a/homeassistant/components/aurora/translations/hu.json b/homeassistant/components/aurora/translations/hu.json index 292ed552235..cbca495254d 100644 --- a/homeassistant/components/aurora/translations/hu.json +++ b/homeassistant/components/aurora/translations/hu.json @@ -8,7 +8,7 @@ "data": { "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" } } } @@ -17,7 +17,7 @@ "step": { "init": { "data": { - "threshold": "K\u00fcsz\u00f6b (%)" + "threshold": "K\u00fcsz\u00f6b\u00e9rt\u00e9k (%)" } } } diff --git a/homeassistant/components/auth/translations/fr.json b/homeassistant/components/auth/translations/fr.json index cf0a1888495..79b467d0255 100644 --- a/homeassistant/components/auth/translations/fr.json +++ b/homeassistant/components/auth/translations/fr.json @@ -5,7 +5,7 @@ "no_available_service": "Aucun service de notification disponible." }, "error": { - "invalid_code": "Code invalide. Veuillez essayer \u00e0 nouveau." + "invalid_code": "Code non valide, veuillez r\u00e9essayer." }, "step": { "init": { @@ -21,7 +21,7 @@ }, "totp": { "error": { - "invalid_code": "Code invalide. Veuillez essayez \u00e0 nouveau. Si cette erreur persiste, assurez-vous que l'horloge de votre syst\u00e8me Home Assistant est correcte." + "invalid_code": "Code non valide, veuillez r\u00e9essayer. Si cette erreur persiste, assurez-vous que l'heure de votre syst\u00e8me Home Assistant est correcte." }, "step": { "init": { diff --git a/homeassistant/components/axis/translations/hu.json b/homeassistant/components/axis/translations/hu.json index cb2f9a17c93..c9121ed2f54 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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, diff --git a/homeassistant/components/azure_devops/translations/it.json b/homeassistant/components/azure_devops/translations/it.json index 232264d1026..bd14e7926cd 100644 --- a/homeassistant/components/azure_devops/translations/it.json +++ b/homeassistant/components/azure_devops/translations/it.json @@ -21,10 +21,10 @@ "user": { "data": { "organization": "Organizzazione", - "personal_access_token": "Token di Accesso Personale (PAT)", + "personal_access_token": "Token di accesso personale (PAT)", "project": "Progetto" }, - "description": "Configura un'istanza di DevOps di Azure per accedere al progetto. Un Token di Accesso Personale (PAT) \u00e8 richiesto solo per un progetto privato.", + "description": "Configura un'istanza di DevOps di Azure per accedere al progetto. Un token di accesso personale (PAT) \u00e8 richiesto solo per un progetto privato.", "title": "Aggiungere un progetto Azure DevOps" } } diff --git a/homeassistant/components/azure_event_hub/translations/de.json b/homeassistant/components/azure_event_hub/translations/de.json index c1552371f76..b72ef47ab34 100644 --- a/homeassistant/components/azure_event_hub/translations/de.json +++ b/homeassistant/components/azure_event_hub/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Der Dienst ist bereits konfiguriert", "cannot_connect": "Die Verbindung mit den Anmeldeinformationen aus der configuration.yaml ist fehlgeschlagen, bitte entferne diese aus der yaml und verwende den Konfigurationsfluss.", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", - "unknown": "Die Verbindung mit den Anmeldeinformationen aus der configuration.yaml ist mit einem unbekannten Fehler fehlgeschlagen. Bitte entferne diese aus der yaml und verwende den Konfigurationsfluss." + "unknown": "Die Verbindung mit den Anmeldeinformationen aus der configuration.yaml ist mit einem unbekannten Fehler fehlgeschlagen. Bitte entferne diese aus der yaml und verwende den Konfigurationsfluss." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/binary_sensor/translations/cs.json b/homeassistant/components/binary_sensor/translations/cs.json index fb19cc00fda..ced02ffc326 100644 --- a/homeassistant/components/binary_sensor/translations/cs.json +++ b/homeassistant/components/binary_sensor/translations/cs.json @@ -112,6 +112,10 @@ "off": "Nenab\u00edj\u00ed se", "on": "Nab\u00edjen\u00ed" }, + "carbon_monoxide": { + "off": "\u017d\u00e1dn\u00fd plyn", + "on": "Zji\u0161t\u011bn plyn" + }, "cold": { "off": "Norm\u00e1ln\u00ed", "on": "Studen\u00e9" diff --git a/homeassistant/components/binary_sensor/translations/he.json b/homeassistant/components/binary_sensor/translations/he.json index 293a4f3f264..5f0e14ccac1 100644 --- a/homeassistant/components/binary_sensor/translations/he.json +++ b/homeassistant/components/binary_sensor/translations/he.json @@ -151,11 +151,11 @@ "on": "\u05de\u05d7\u05d5\u05d1\u05e8" }, "door": { - "off": "\u05e1\u05d2\u05d5\u05e8", + "off": "\u05e0\u05e1\u05d2\u05e8", "on": "\u05e4\u05ea\u05d5\u05d7" }, "garage_door": { - "off": "\u05e1\u05d2\u05d5\u05e8", + "off": "\u05e0\u05e1\u05d2\u05e8", "on": "\u05e4\u05ea\u05d5\u05d7" }, "gas": { @@ -191,7 +191,7 @@ "on": "\u05d6\u05d5\u05d4\u05d4" }, "opening": { - "off": "\u05e1\u05d2\u05d5\u05e8", + "off": "\u05e0\u05e1\u05d2\u05e8", "on": "\u05e4\u05ea\u05d5\u05d7" }, "plug": { @@ -231,7 +231,7 @@ "on": "\u05d6\u05d5\u05d4\u05d4" }, "window": { - "off": "\u05e1\u05d2\u05d5\u05e8", + "off": "\u05e0\u05e1\u05d2\u05e8", "on": "\u05e4\u05ea\u05d5\u05d7" } }, diff --git a/homeassistant/components/binary_sensor/translations/pt-BR.json b/homeassistant/components/binary_sensor/translations/pt-BR.json index d858dec6664..5b1ba2a1abf 100644 --- a/homeassistant/components/binary_sensor/translations/pt-BR.json +++ b/homeassistant/components/binary_sensor/translations/pt-BR.json @@ -179,7 +179,7 @@ "on": "Molhado" }, "motion": { - "off": "Sem movimento", + "off": "N\u00e3o detectado", "on": "Detectado" }, "moving": { diff --git a/homeassistant/components/binary_sensor/translations/zh-Hant.json b/homeassistant/components/binary_sensor/translations/zh-Hant.json index a0adaaab083..417ca652cb5 100644 --- a/homeassistant/components/binary_sensor/translations/zh-Hant.json +++ b/homeassistant/components/binary_sensor/translations/zh-Hant.json @@ -203,7 +203,7 @@ "on": "\u5728\u5bb6" }, "problem": { - "off": "\u78ba\u5b9a", + "off": "\u6b63\u5e38", "on": "\u7570\u5e38" }, "running": { diff --git a/homeassistant/components/blebox/translations/it.json b/homeassistant/components/blebox/translations/it.json index 6d377840e90..86025ffdeee 100644 --- a/homeassistant/components/blebox/translations/it.json +++ b/homeassistant/components/blebox/translations/it.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Impossibile connettersi", "unknown": "Errore imprevisto", - "unsupported_version": "Il dispositivo BleBox ha un firmware obsoleto. Si prega di aggiornarlo prima." + "unsupported_version": "Il dispositivo BleBox ha un firmware obsoleto. Aggiornalo prima." }, "flow_title": "{name} ({host})", "step": { diff --git a/homeassistant/components/braviatv/translations/ca.json b/homeassistant/components/braviatv/translations/ca.json index 94fe36dcddc..b8616f5e8ba 100644 --- a/homeassistant/components/braviatv/translations/ca.json +++ b/homeassistant/components/braviatv/translations/ca.json @@ -21,7 +21,7 @@ "data": { "host": "Amfitri\u00f3" }, - "description": "Configura la integraci\u00f3 de televisor Sony Bravia. Si tens problemes durant la configuraci\u00f3, v\u00e9s a: https://www.home-assistant.io/integrations/braviatv\n\nAssegura't que el televisor estigui engegat.", + "description": "Assegura't que el televisor est\u00e0 engegat abans de configurar-lo.", "title": "Televisor Sony Bravia" } } diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json index cca5c5aa47f..55ddbec464e 100644 --- a/homeassistant/components/braviatv/translations/de.json +++ b/homeassistant/components/braviatv/translations/de.json @@ -21,7 +21,7 @@ "data": { "host": "Host" }, - "description": "Richte die Sony Bravia TV-Integration ein. Wenn du Probleme mit der Konfiguration hast, gehe zu: https://www.home-assistant.io/integrations/braviatv \n\nStelle sicher, dass dein Fernseher eingeschaltet ist.", + "description": "Stelle sicher, dass dein Fernseher eingeschaltet ist, bevor du versuchst, ihn einzurichten.", "title": "Sony Bravia TV" } } diff --git a/homeassistant/components/braviatv/translations/en.json b/homeassistant/components/braviatv/translations/en.json index 05ced6af856..9cc2ac23d17 100644 --- a/homeassistant/components/braviatv/translations/en.json +++ b/homeassistant/components/braviatv/translations/en.json @@ -21,7 +21,7 @@ "data": { "host": "Host" }, - "description": "Set up Sony Bravia TV integration. If you have problems with configuration go to: https://www.home-assistant.io/integrations/braviatv \n\nEnsure that your TV is turned on.", + "description": "Ensure that your TV is turned on before trying to set it up.", "title": "Sony Bravia TV" } } diff --git a/homeassistant/components/braviatv/translations/et.json b/homeassistant/components/braviatv/translations/et.json index 6930186aeba..c63e3635185 100644 --- a/homeassistant/components/braviatv/translations/et.json +++ b/homeassistant/components/braviatv/translations/et.json @@ -21,7 +21,7 @@ "data": { "host": "" }, - "description": "Seadista Sony Bravia TV sidumine. Kuion probleeme seadetega mine: https://www.home-assistant.io/integrations/braviatv \n\nVeendu, et teler on sisse l\u00fclitatud.", + "description": "Enne teleri seadistamist veendu, et see oleks sisse l\u00fclitatud.", "title": "" } } diff --git a/homeassistant/components/braviatv/translations/fr.json b/homeassistant/components/braviatv/translations/fr.json index d609f1a2fa1..f85aea705fb 100644 --- a/homeassistant/components/braviatv/translations/fr.json +++ b/homeassistant/components/braviatv/translations/fr.json @@ -21,7 +21,7 @@ "data": { "host": "H\u00f4te" }, - "description": "Configurez l'int\u00e9gration du t\u00e9l\u00e9viseur Sony Bravia. Si vous rencontrez des probl\u00e8mes de configuration, rendez-vous sur: https://www.home-assistant.io/integrations/braviatv \n\n Assurez-vous que votre t\u00e9l\u00e9viseur est allum\u00e9.", + "description": "Assurez-vous que votre t\u00e9l\u00e9viseur est allum\u00e9 avant d'essayer de le configurer.", "title": "Sony Bravia TV" } } diff --git a/homeassistant/components/braviatv/translations/hu.json b/homeassistant/components/braviatv/translations/hu.json index 00e88955c81..554e74c52b1 100644 --- a/homeassistant/components/braviatv/translations/hu.json +++ b/homeassistant/components/braviatv/translations/hu.json @@ -21,7 +21,7 @@ "data": { "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.", + "description": "Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a TV k\u00e9sz\u00fcl\u00e9k be van kapcsolva a be\u00e1ll\u00edt\u00e1s el\u0151tt.", "title": "Sony Bravia TV" } } diff --git a/homeassistant/components/braviatv/translations/id.json b/homeassistant/components/braviatv/translations/id.json index def84dacdbb..cb7d5396f7a 100644 --- a/homeassistant/components/braviatv/translations/id.json +++ b/homeassistant/components/braviatv/translations/id.json @@ -21,7 +21,7 @@ "data": { "host": "Host" }, - "description": "Siapkan integrasi TV Sony Bravia. Jika Anda memiliki masalah dengan konfigurasi, buka: https://www.home-assistant.io/integrations/braviatv \n\nPastikan TV Anda dinyalakan.", + "description": "Pastikan TV Anda dinyalakan sebelum menyiapkan.", "title": "TV Sony Bravia" } } diff --git a/homeassistant/components/braviatv/translations/it.json b/homeassistant/components/braviatv/translations/it.json index bbd02157496..7ab149fa6c7 100644 --- a/homeassistant/components/braviatv/translations/it.json +++ b/homeassistant/components/braviatv/translations/it.json @@ -21,7 +21,7 @@ "data": { "host": "Host" }, - "description": "Configura l'integrazione TV di Sony Bravia. In caso di problemi con la configurazione visita: https://www.home-assistant.io/integrations/braviatv\n\nAssicurati che il televisore sia acceso.", + "description": "Assicurati che la tua TV sia accesa prima di provare a configurarla.", "title": "Sony Bravia TV" } } diff --git a/homeassistant/components/braviatv/translations/nl.json b/homeassistant/components/braviatv/translations/nl.json index 5354f5761ec..133c5277045 100644 --- a/homeassistant/components/braviatv/translations/nl.json +++ b/homeassistant/components/braviatv/translations/nl.json @@ -21,7 +21,7 @@ "data": { "host": "Host" }, - "description": "Stel Sony Bravia TV-integratie in. Als je problemen hebt met de configuratie ga dan naar: https://www.home-assistant.io/integrations/braviatv \n\nZorg ervoor dat uw tv is ingeschakeld.", + "description": "Zorg ervoor dat uw TV aan staat voordat u hem probeert in te stellen.", "title": "Sony Bravia TV" } } diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json index 0c960a850e2..6465db8d3c0 100644 --- a/homeassistant/components/braviatv/translations/no.json +++ b/homeassistant/components/braviatv/translations/no.json @@ -21,7 +21,7 @@ "data": { "host": "Vert" }, - "description": "Sett opp Sony Bravia TV-integrasjon. Hvis du har problemer med konfigurasjonen, g\u00e5 til: [https://www.home-assistant.io/integrations/braviatv](https://www.home-assistant.io/integrations/braviatv)\n\n Forsikre deg om at TV-en er sl\u00e5tt p\u00e5.", + "description": "S\u00f8rg for at TV-en er sl\u00e5tt p\u00e5 f\u00f8r du pr\u00f8ver \u00e5 sette den opp.", "title": "" } } diff --git a/homeassistant/components/braviatv/translations/pl.json b/homeassistant/components/braviatv/translations/pl.json index 1aa5d7cb58a..354962a62d9 100644 --- a/homeassistant/components/braviatv/translations/pl.json +++ b/homeassistant/components/braviatv/translations/pl.json @@ -21,7 +21,7 @@ "data": { "host": "Nazwa hosta lub adres IP" }, - "description": "Konfiguracja integracji telewizora Sony Bravia. Je\u015bli masz problemy z konfiguracj\u0105, przejd\u017a do strony: https://www.home-assistant.io/integrations/braviatv\n\nUpewnij si\u0119, \u017ce telewizor jest w\u0142\u0105czony.", + "description": "Upewnij si\u0119, \u017ce telewizor jest w\u0142\u0105czony, zanim spr\u00f3bujesz go skonfigurowa\u0107.", "title": "Sony Bravia TV" } } diff --git a/homeassistant/components/braviatv/translations/pt-BR.json b/homeassistant/components/braviatv/translations/pt-BR.json index bd6d47af018..a784e30d84c 100644 --- a/homeassistant/components/braviatv/translations/pt-BR.json +++ b/homeassistant/components/braviatv/translations/pt-BR.json @@ -21,7 +21,7 @@ "data": { "host": "Nome do host" }, - "description": "Configure a integra\u00e7\u00e3o do Sony Bravia TV. Se voc\u00ea tiver problemas com a configura\u00e7\u00e3o, acesse: https://www.home-assistant.io/integrations/braviatv \n\n Verifique se a sua TV est\u00e1 ligada.", + "description": "Certifique-se de que sua TV esteja ligada antes de tentar configur\u00e1-la.", "title": "Sony Bravia TV" } } diff --git a/homeassistant/components/braviatv/translations/ru.json b/homeassistant/components/braviatv/translations/ru.json index add32dd0d79..3aeb83f7686 100644 --- a/homeassistant/components/braviatv/translations/ru.json +++ b/homeassistant/components/braviatv/translations/ru.json @@ -21,7 +21,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0435\u0439 \u043f\u043e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438:\nhttps://www.home-assistant.io/integrations/braviatv", + "description": "\u041f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d.", "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Sony Bravia" } } diff --git a/homeassistant/components/braviatv/translations/tr.json b/homeassistant/components/braviatv/translations/tr.json index 6d0f82e29a4..b6df4a7999a 100644 --- a/homeassistant/components/braviatv/translations/tr.json +++ b/homeassistant/components/braviatv/translations/tr.json @@ -21,7 +21,7 @@ "data": { "host": "Ana Bilgisayar" }, - "description": "Sony Bravia TV entegrasyonunu ayarlay\u0131n. Yap\u0131land\u0131rmayla ilgili sorunlar\u0131n\u0131z varsa \u015fu adrese gidin: https://www.home-assistant.io/integrations/braviatv \n\n TV'nizin a\u00e7\u0131k oldu\u011fundan emin olun.", + "description": "Kurmaya \u00e7al\u0131\u015fmadan \u00f6nce TV'nizin a\u00e7\u0131k oldu\u011fundan emin olun.", "title": "Sony Bravia TV" } } diff --git a/homeassistant/components/braviatv/translations/zh-Hant.json b/homeassistant/components/braviatv/translations/zh-Hant.json index f736b601a74..35ef6ef2e4f 100644 --- a/homeassistant/components/braviatv/translations/zh-Hant.json +++ b/homeassistant/components/braviatv/translations/zh-Hant.json @@ -21,7 +21,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u8a2d\u5b9a Sony Bravia \u96fb\u8996\u6574\u5408\u3002\u5047\u5982\u65bc\u8a2d\u5b9a\u904e\u7a0b\u4e2d\u906d\u9047\u56f0\u7136\uff0c\u8acb\u53c3\u95b1\uff1ahttps://www.home-assistant.io/integrations/braviatv \n\n\u78ba\u5b9a\u96fb\u8996\u5df2\u7d93\u958b\u555f\u3002", + "description": "\u65bc\u8a2d\u5b9a\u524d\u78ba\u5b9a\u96fb\u8996\u5df2\u7d93\u958b\u555f\u3002", "title": "Sony Bravia \u96fb\u8996" } } diff --git a/homeassistant/components/broadlink/translations/hu.json b/homeassistant/components/broadlink/translations/hu.json index 0bab0c1752f..70fd71b5897 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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", "not_supported": "Az eszk\u00f6z nem t\u00e1mogatott", @@ -20,7 +20,7 @@ }, "finish": { "data": { - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" }, "title": "V\u00e1lasszonegy nevet az eszk\u00f6znek" }, diff --git a/homeassistant/components/brother/translations/ca.json b/homeassistant/components/brother/translations/ca.json index 689495478bb..d9b664dfe82 100644 --- a/homeassistant/components/brother/translations/ca.json +++ b/homeassistant/components/brother/translations/ca.json @@ -15,14 +15,13 @@ "data": { "host": "Amfitri\u00f3", "type": "Tipus d'impressora" - }, - "description": "Configura la integraci\u00f3 d'impressora Brother. Si tens problemes amb la configuraci\u00f3, visita: https://www.home-assistant.io/integrations/brother" + } }, "zeroconf_confirm": { "data": { "type": "Tipus d'impressora" }, - "description": "Vols afegir la impressora Brother {model} amb n\u00famero de s\u00e8rie `{serial_number}` a Home Assistant?", + "description": "Vols afegir la impressora {model} amb n\u00famero de s\u00e8rie `{serial_number}` a Home Assistant?", "title": "Impressora Brother descoberta" } } diff --git a/homeassistant/components/brother/translations/da.json b/homeassistant/components/brother/translations/da.json index fc9cd0079d8..7dcd8188085 100644 --- a/homeassistant/components/brother/translations/da.json +++ b/homeassistant/components/brother/translations/da.json @@ -14,8 +14,7 @@ "data": { "host": "Printerens v\u00e6rtsnavn eller IP-adresse", "type": "Type af printer" - }, - "description": "Konfigurer Brother-printerintegration. Hvis du har problemer med konfiguration, kan du g\u00e5 til: https://www.home-assistant.io/integrations/brother" + } }, "zeroconf_confirm": { "data": { diff --git a/homeassistant/components/brother/translations/de.json b/homeassistant/components/brother/translations/de.json index 8126a04f21d..e8056b0505a 100644 --- a/homeassistant/components/brother/translations/de.json +++ b/homeassistant/components/brother/translations/de.json @@ -15,14 +15,13 @@ "data": { "host": "Host", "type": "Typ des Druckers" - }, - "description": "Einrichten der Brother-Drucker-Integration. Wenn Du Probleme mit der Konfiguration hast, gehe zu: https://www.home-assistant.io/integrations/brother" + } }, "zeroconf_confirm": { "data": { "type": "Typ des Druckers" }, - "description": "M\u00f6chtest du den Brother Drucker {model} mit der Seriennummer `{serial_number}` zum Home Assistant hinzuf\u00fcgen?", + "description": "M\u00f6chtest du den Drucker {model} mit der Seriennummer ` {serial_number} ` zu Home Assistant hinzuf\u00fcgen?", "title": "Brother-Drucker entdeckt" } } diff --git a/homeassistant/components/brother/translations/el.json b/homeassistant/components/brother/translations/el.json index f8a27353a6a..e8c9e4ea8e7 100644 --- a/homeassistant/components/brother/translations/el.json +++ b/homeassistant/components/brother/translations/el.json @@ -15,8 +15,7 @@ "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", "type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c4\u03bf\u03c5 \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae" - }, - "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae Brother. \u0395\u03ac\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: https://www.home-assistant.io/integrations/brother" + } }, "zeroconf_confirm": { "data": { diff --git a/homeassistant/components/brother/translations/en.json b/homeassistant/components/brother/translations/en.json index 8ec9d84c7fb..997cdb1ea83 100644 --- a/homeassistant/components/brother/translations/en.json +++ b/homeassistant/components/brother/translations/en.json @@ -15,14 +15,13 @@ "data": { "host": "Host", "type": "Type of the printer" - }, - "description": "Set up Brother printer integration. If you have problems with configuration go to: https://www.home-assistant.io/integrations/brother" + } }, "zeroconf_confirm": { "data": { "type": "Type of the printer" }, - "description": "Do you want to add the Brother Printer {model} with serial number `{serial_number}` to Home Assistant?", + "description": "Do you want to add the printer {model} with serial number `{serial_number}` to Home Assistant?", "title": "Discovered Brother Printer" } } diff --git a/homeassistant/components/brother/translations/es-419.json b/homeassistant/components/brother/translations/es-419.json index 33d06705017..21c57cab021 100644 --- a/homeassistant/components/brother/translations/es-419.json +++ b/homeassistant/components/brother/translations/es-419.json @@ -14,8 +14,7 @@ "data": { "host": "Nombre de host de la impresora o direcci\u00f3n IP", "type": "Tipo de impresora" - }, - "description": "Configure la integraci\u00f3n de la impresora Brother. Si tiene problemas con la configuraci\u00f3n, vaya a: https://www.home-assistant.io/integrations/brother" + } }, "zeroconf_confirm": { "data": { diff --git a/homeassistant/components/brother/translations/es.json b/homeassistant/components/brother/translations/es.json index 43b3cefdded..bc6aa749445 100644 --- a/homeassistant/components/brother/translations/es.json +++ b/homeassistant/components/brother/translations/es.json @@ -15,8 +15,7 @@ "data": { "host": "Host", "type": "Tipo de impresora" - }, - "description": "Configura la integraci\u00f3n de impresoras Brother. Si tienes problemas con la configuraci\u00f3n, ve a: https://www.home-assistant.io/integrations/brother" + } }, "zeroconf_confirm": { "data": { diff --git a/homeassistant/components/brother/translations/et.json b/homeassistant/components/brother/translations/et.json index 1115e5cb3ca..99928a5d1ab 100644 --- a/homeassistant/components/brother/translations/et.json +++ b/homeassistant/components/brother/translations/et.json @@ -15,14 +15,13 @@ "data": { "host": "Host", "type": "Printeri t\u00fc\u00fcp" - }, - "description": "Seadista Brotheri printeri sidumine. Kui seadistamisega on probleeme mine aadressile https://www.home-assistant.io/integrations/brother" + } }, "zeroconf_confirm": { "data": { "type": "Printeri t\u00fc\u00fcp" }, - "description": "Kas soovite lisada Home Assistanti Brotheri printeri {model} seerianumbriga \" {serial_number} \"?", + "description": "Kas lisada Home Assistantile printer {model} seerianumbriga ` {serial_number} `?", "title": "Avastatud Brotheri printer" } } diff --git a/homeassistant/components/brother/translations/fr.json b/homeassistant/components/brother/translations/fr.json index 851d46f1772..4d248d05102 100644 --- a/homeassistant/components/brother/translations/fr.json +++ b/homeassistant/components/brother/translations/fr.json @@ -15,14 +15,13 @@ "data": { "host": "H\u00f4te", "type": "Type d'imprimante" - }, - "description": "Configurez l'int\u00e9gration de l'imprimante Brother. Si vous avez des probl\u00e8mes avec la configuration, allez \u00e0 : https://www.home-assistant.io/integrations/brother" + } }, "zeroconf_confirm": { "data": { "type": "Type d'imprimante" }, - "description": "Voulez-vous ajouter l'imprimante Brother {model} avec le num\u00e9ro de s\u00e9rie `{serial_number}` \u00e0 Home Assistant ?", + "description": "Voulez-vous ajouter l'imprimante {model} portant le num\u00e9ro de s\u00e9rie `{serial_number}` \u00e0 Home Assistant\u00a0?", "title": "Imprimante Brother d\u00e9couverte" } } diff --git a/homeassistant/components/brother/translations/hu.json b/homeassistant/components/brother/translations/hu.json index f0218dc2647..93c368cf74a 100644 --- a/homeassistant/components/brother/translations/hu.json +++ b/homeassistant/components/brother/translations/hu.json @@ -15,14 +15,13 @@ "data": { "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" + } }, "zeroconf_confirm": { "data": { "type": "A nyomtat\u00f3 t\u00edpusa" }, - "description": "Hozz\u00e1 szeretn\u00e9 adni a {model} Brother nyomtat\u00f3t, amelynek sorsz\u00e1ma: `{serial_number}`, Home Assistanthoz?", + "description": "Hozz\u00e1 szeretn\u00e9 adni a {model} nyomtat\u00f3t, amelynek sorsz\u00e1ma: `{serial_number}`, Home Assistanthoz?", "title": "Felfedezett Brother nyomtat\u00f3" } } diff --git a/homeassistant/components/brother/translations/id.json b/homeassistant/components/brother/translations/id.json index ed02999710e..135274450d3 100644 --- a/homeassistant/components/brother/translations/id.json +++ b/homeassistant/components/brother/translations/id.json @@ -15,14 +15,13 @@ "data": { "host": "Host", "type": "Jenis printer" - }, - "description": "Siapkan integrasi printer Brother. Jika Anda memiliki masalah dengan konfigurasi, buka: https://www.home-assistant.io/integrations/brother" + } }, "zeroconf_confirm": { "data": { "type": "Jenis printer" }, - "description": "Ingin menambahkan Printer Brother {model} dengan nomor seri `{serial_number}` ke Home Assistant?", + "description": "Ingin menambahkan printer {model} dengan nomor seri `{serial_number}` ke Home Assistant?", "title": "Perangkat Printer Brother yang Ditemukan" } } diff --git a/homeassistant/components/brother/translations/it.json b/homeassistant/components/brother/translations/it.json index 29059723159..21fd1bf15ab 100644 --- a/homeassistant/components/brother/translations/it.json +++ b/homeassistant/components/brother/translations/it.json @@ -15,14 +15,13 @@ "data": { "host": "Host", "type": "Tipo di stampante" - }, - "description": "Configura l'integrazione della stampante Brother. In caso di problemi con la configurazione, visita: https://www.home-assistant.io/integrations/brother" + } }, "zeroconf_confirm": { "data": { "type": "Tipo di stampante" }, - "description": "Vuoi aggiungere la stampante Brother {model} con il numero seriale `{serial_number}` a Home Assistant?", + "description": "Vuoi aggiungere la stampante {model} con il numero seriale `{serial_number}` a Home Assistant?", "title": "Trovata stampante Brother" } } diff --git a/homeassistant/components/brother/translations/ja.json b/homeassistant/components/brother/translations/ja.json index 6c0e1e57767..ce3341ef676 100644 --- a/homeassistant/components/brother/translations/ja.json +++ b/homeassistant/components/brother/translations/ja.json @@ -15,8 +15,7 @@ "data": { "host": "\u30db\u30b9\u30c8", "type": "\u30d7\u30ea\u30f3\u30bf\u30fc\u306e\u7a2e\u985e" - }, - "description": "\u30d6\u30e9\u30b6\u30fc\u793e\u88fd\u30d7\u30ea\u30f3\u30bf\u30fc\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/brother \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + } }, "zeroconf_confirm": { "data": { diff --git a/homeassistant/components/brother/translations/ko.json b/homeassistant/components/brother/translations/ko.json index 2d3b587e475..a69c415f061 100644 --- a/homeassistant/components/brother/translations/ko.json +++ b/homeassistant/components/brother/translations/ko.json @@ -15,8 +15,7 @@ "data": { "host": "\ud638\uc2a4\ud2b8", "type": "\ud504\ub9b0\ud130\uc758 \uc885\ub958" - }, - "description": "\ube0c\ub77c\ub354 \ud504\ub9b0\ud130 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. \uad6c\uc131\uc5d0 \ubb38\uc81c\uac00\uc788\ub294 \uacbd\uc6b0 https://www.home-assistant.io/integrations/brother \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694" + } }, "zeroconf_confirm": { "data": { diff --git a/homeassistant/components/brother/translations/lb.json b/homeassistant/components/brother/translations/lb.json index 91964ec90ca..0a0d3785274 100644 --- a/homeassistant/components/brother/translations/lb.json +++ b/homeassistant/components/brother/translations/lb.json @@ -15,8 +15,7 @@ "data": { "host": "Host", "type": "Typ vum Printer" - }, - "description": "Brother Printer Integratioun ariichten. Am Fall vun Problemer kuckt op: https://www.home-assistant.io/integrations/brother" + } }, "zeroconf_confirm": { "data": { diff --git a/homeassistant/components/brother/translations/nl.json b/homeassistant/components/brother/translations/nl.json index 6440be77e75..97c506299a7 100644 --- a/homeassistant/components/brother/translations/nl.json +++ b/homeassistant/components/brother/translations/nl.json @@ -15,14 +15,13 @@ "data": { "host": "Host", "type": "Type printer" - }, - "description": "Zet Brother printerintegratie op. Als u problemen heeft met de configuratie ga dan naar: https://www.home-assistant.io/integrations/brother" + } }, "zeroconf_confirm": { "data": { "type": "Type printer" }, - "description": "Wilt u het Brother Printer {model} met serienummer {serial_number}' toevoegen aan Home Assistant?", + "description": "Wilt u de printer {model} met serienummer ` {serial_number} ` toevoegen aan Home Assistant?", "title": "Ontdekte Brother Printer" } } diff --git a/homeassistant/components/brother/translations/no.json b/homeassistant/components/brother/translations/no.json index 9d3618cad62..2f383701288 100644 --- a/homeassistant/components/brother/translations/no.json +++ b/homeassistant/components/brother/translations/no.json @@ -15,14 +15,13 @@ "data": { "host": "Vert", "type": "Skriver type" - }, - "description": "Sett opp Brother skriver integrasjonen. Hvis du har problemer med konfigurasjonen, bes\u00f8k dokumentasjonen her: [https://www.home-assistant.io/integrations/brother](https://www.home-assistant.io/integrations/brother)" + } }, "zeroconf_confirm": { "data": { "type": "Type skriver" }, - "description": "Vil du legge til Brother-skriveren {model} med serienummeret `{serial_number}` til Home Assistant?", + "description": "Vil du legge til skriveren {model} med serienummeret ` {serial_number} ` til Home Assistant?", "title": "Oppdaget Brother Skriver" } } diff --git a/homeassistant/components/brother/translations/pl.json b/homeassistant/components/brother/translations/pl.json index 20031bf94ad..159b0308ab7 100644 --- a/homeassistant/components/brother/translations/pl.json +++ b/homeassistant/components/brother/translations/pl.json @@ -15,14 +15,13 @@ "data": { "host": "Nazwa hosta lub adres IP", "type": "Typ drukarki" - }, - "description": "Konfiguracja integracji drukarek Brother. Je\u015bli masz problemy z konfiguracj\u0105, przejd\u017a na stron\u0119: https://www.home-assistant.io/integrations/brother" + } }, "zeroconf_confirm": { "data": { "type": "Typ drukarki" }, - "description": "Czy chcesz doda\u0107 drukark\u0119 Brother {model} o numerze seryjnym `{serial_number}` do Home Assistanta?", + "description": "Czy chcesz doda\u0107 drukark\u0119 {model} o numerze seryjnym `{serial_number}` do Home Assistanta?", "title": "Wykryto drukark\u0119 Brother" } } diff --git a/homeassistant/components/brother/translations/pt-BR.json b/homeassistant/components/brother/translations/pt-BR.json index 33113d12881..5e938d90edd 100644 --- a/homeassistant/components/brother/translations/pt-BR.json +++ b/homeassistant/components/brother/translations/pt-BR.json @@ -15,14 +15,13 @@ "data": { "host": "Nome do host", "type": "Tipo de impressora" - }, - "description": "Configure a integra\u00e7\u00e3o da impressora Brother. Se voc\u00ea tiver problemas com a configura\u00e7\u00e3o, acesse: https://www.home-assistant.io/integrations/brother" + } }, "zeroconf_confirm": { "data": { "type": "Tipo de impressora" }, - "description": "Deseja adicionar a impressora Brother {model} com n\u00famero de s\u00e9rie ` {serial_number} ` ao Home Assistant?", + "description": "Deseja adicionar a impressora {model} com n\u00famero de s\u00e9rie ` {serial_number} ` ao Home Assistant?", "title": "Impressora Brother descoberta" } } diff --git a/homeassistant/components/brother/translations/ru.json b/homeassistant/components/brother/translations/ru.json index 6fd15e30ac3..a9f6158ccf8 100644 --- a/homeassistant/components/brother/translations/ru.json +++ b/homeassistant/components/brother/translations/ru.json @@ -15,14 +15,13 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "type": "\u0422\u0438\u043f \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430" - }, - "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0435\u0439 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438: https://www.home-assistant.io/integrations/brother." + } }, "zeroconf_confirm": { "data": { "type": "\u0422\u0438\u043f \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430" }, - "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 \u043f\u0440\u0438\u043d\u0442\u0435\u0440 Brother {model} \u0441 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c `{serial_number}`?", + "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 \u043f\u0440\u0438\u043d\u0442\u0435\u0440 {model} \u0441 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c `{serial_number}`?", "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u043f\u0440\u0438\u043d\u0442\u0435\u0440 Brother" } } diff --git a/homeassistant/components/brother/translations/sl.json b/homeassistant/components/brother/translations/sl.json index 53ce1423819..fcd53a65413 100644 --- a/homeassistant/components/brother/translations/sl.json +++ b/homeassistant/components/brother/translations/sl.json @@ -14,8 +14,7 @@ "data": { "host": "Gostiteljsko ime tiskalnika ali naslov IP", "type": "Vrsta tiskalnika" - }, - "description": "Nastavite integracijo tiskalnika Brother. \u010ce imate te\u017eave s konfiguracijo, pojdite na: https://www.home-assistant.io/integrations/brother" + } }, "zeroconf_confirm": { "data": { diff --git a/homeassistant/components/brother/translations/sv.json b/homeassistant/components/brother/translations/sv.json index 3a049bb92c8..00d29aa3a0a 100644 --- a/homeassistant/components/brother/translations/sv.json +++ b/homeassistant/components/brother/translations/sv.json @@ -14,8 +14,7 @@ "data": { "host": "Skrivarens v\u00e4rdnamn eller IP-adress", "type": "Typ av skrivare" - }, - "description": "St\u00e4ll in Brother-skrivarintegration. Om du har problem med konfigurationen g\u00e5r du till: https://www.home-assistant.io/integrations/brother" + } }, "zeroconf_confirm": { "data": { diff --git a/homeassistant/components/brother/translations/tr.json b/homeassistant/components/brother/translations/tr.json index c989844c6f7..291be1c4138 100644 --- a/homeassistant/components/brother/translations/tr.json +++ b/homeassistant/components/brother/translations/tr.json @@ -15,14 +15,13 @@ "data": { "host": "Sunucu", "type": "Yaz\u0131c\u0131n\u0131n t\u00fcr\u00fc" - }, - "description": "Brother yaz\u0131c\u0131 entegrasyonunu ayarlay\u0131n. Yap\u0131land\u0131rmayla ilgili sorunlar\u0131n\u0131z varsa \u015fu adrese gidin: https://www.home-assistant.io/integrations/brother" + } }, "zeroconf_confirm": { "data": { "type": "Yaz\u0131c\u0131n\u0131n t\u00fcr\u00fc" }, - "description": "Seri numaras\u0131 ` {serial_number} ` olan Brother Yaz\u0131c\u0131 {model} }'i Home Assistant'a eklemek ister misiniz?", + "description": "Seri numaras\u0131 ` {serial_number} ` olan {model} yaz\u0131c\u0131y\u0131 Home Assistant'a eklemek istiyor musunuz?", "title": "Ke\u015ffedilen Brother Yaz\u0131c\u0131" } } diff --git a/homeassistant/components/brother/translations/uk.json b/homeassistant/components/brother/translations/uk.json index ac5943aa85c..89ce35d988e 100644 --- a/homeassistant/components/brother/translations/uk.json +++ b/homeassistant/components/brother/translations/uk.json @@ -15,8 +15,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "type": "\u0422\u0438\u043f \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430" - }, - "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0454\u044e \u043f\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044e \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457: https://www.home-assistant.io/integrations/brother." + } }, "zeroconf_confirm": { "data": { diff --git a/homeassistant/components/brother/translations/zh-Hans.json b/homeassistant/components/brother/translations/zh-Hans.json index 91e0c310dd1..5a0e6f80393 100644 --- a/homeassistant/components/brother/translations/zh-Hans.json +++ b/homeassistant/components/brother/translations/zh-Hans.json @@ -8,9 +8,6 @@ "snmp_error": "SNMP\u670d\u52a1\u5668\u5df2\u5173\u95ed\u6216\u4e0d\u652f\u6301\u6253\u5370\u3002" }, "step": { - "user": { - "description": "\u8bbe\u7f6e Brother \u6253\u5370\u673a\u96c6\u6210\u3002\u5982\u679c\u60a8\u9047\u5230\u95ee\u9898\uff0c\u8bf7\u8bbf\u95ee\uff1ahttps://www.home-assistant.io/integrations/brother" - }, "zeroconf_confirm": { "data": { "type": "\u6253\u5370\u673a\u7c7b\u578b" diff --git a/homeassistant/components/brother/translations/zh-Hant.json b/homeassistant/components/brother/translations/zh-Hant.json index 016d83309f2..1fdeedbb556 100644 --- a/homeassistant/components/brother/translations/zh-Hant.json +++ b/homeassistant/components/brother/translations/zh-Hant.json @@ -15,8 +15,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef", "type": "\u5370\u8868\u6a5f\u985e\u5225" - }, - "description": "\u8a2d\u5b9a Brother \u5370\u8868\u6a5f\u6574\u5408\u3002\u5047\u5982\u9700\u8981\u5354\u52a9\uff0c\u8acb\u53c3\u8003\uff1ahttps://www.home-assistant.io/integrations/brother" + } }, "zeroconf_confirm": { "data": { diff --git a/homeassistant/components/bsblan/translations/hu.json b/homeassistant/components/bsblan/translations/hu.json index f04a0a3af0e..0c02cfb733d 100644 --- a/homeassistant/components/bsblan/translations/hu.json +++ b/homeassistant/components/bsblan/translations/hu.json @@ -17,7 +17,7 @@ "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "\u00c1ll\u00edtsa be a BSB-Lan eszk\u00f6zt az HomeAssistantba val\u00f3 integr\u00e1ci\u00f3hoz.", + "description": "\u00c1ll\u00edtsa be a BSB-Lan eszk\u00f6zt az Home Assistantba val\u00f3 integr\u00e1ci\u00f3hoz.", "title": "Csatlakoz\u00e1s a BSB-Lan eszk\u00f6zh\u00f6z" } } diff --git a/homeassistant/components/cast/translations/cs.json b/homeassistant/components/cast/translations/cs.json index f04465341e3..cfcc3594575 100644 --- a/homeassistant/components/cast/translations/cs.json +++ b/homeassistant/components/cast/translations/cs.json @@ -3,10 +3,42 @@ "abort": { "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." }, + "error": { + "invalid_known_hosts": "Zn\u00e1m\u00ed hostitel\u00e9 mus\u00ed b\u00fdt seznam hostitel\u016f odd\u011blen\u00fd \u010d\u00e1rkou." + }, "step": { + "config": { + "data": { + "known_hosts": "Zn\u00e1m\u00ed hostitel\u00e9" + }, + "description": "Zn\u00e1m\u00ed hostitel\u00e9 - seznam n\u00e1zv\u016f hostitel\u016f nebo IP adres za\u0159\u00edzen\u00ed odd\u011blen\u00fd \u010d\u00e1rkou, kter\u00fd se pou\u017eije, pokud nefunguje zji\u0161\u0165ov\u00e1n\u00ed pomoc\u00ed mDNS.", + "title": "Konfigurace Google Cast" + }, "confirm": { "description": "Chcete za\u010d\u00edt nastavovat?" } } + }, + "options": { + "error": { + "invalid_known_hosts": "Zn\u00e1m\u00ed hostitel\u00e9 mus\u00ed b\u00fdt seznam hostitel\u016f odd\u011blen\u00fd \u010d\u00e1rkou." + }, + "step": { + "advanced_options": { + "data": { + "ignore_cec": "Ignorovat CEC", + "uuid": "Povolen\u00e9 UUID" + }, + "description": "Allowed UUIDs - \u010d\u00e1rkou odd\u011blen\u00fd seznam UUID za\u0159\u00edzen\u00ed Cast, kter\u00e9 chcete p\u0159idat do aplikace Home Assistant. Pou\u017eijte pouze v p\u0159\u00edpad\u011b, \u017ee nechcete p\u0159idat v\u0161echna dostupn\u00e1 za\u0159\u00edzen\u00ed Cast.\nIgnorovat CEC - \u010c\u00e1rkou odd\u011blen\u00fd seznam za\u0159\u00edzen\u00ed Chromecast, kter\u00e1 maj\u00ed p\u0159i ur\u010dov\u00e1n\u00ed aktivn\u00edho vstupu ignorovat data CEC. Tento \u00fadaj bude p\u0159ed\u00e1n do pychromecast.IGNORE_CEC.", + "title": "Pokro\u010dil\u00e1 konfigurace Google Cast" + }, + "basic_options": { + "data": { + "known_hosts": "Zn\u00e1m\u00ed hostitel\u00e9" + }, + "description": "Zn\u00e1m\u00ed hostitel\u00e9 - seznam n\u00e1zv\u016f hostitel\u016f nebo IP adres za\u0159\u00edzen\u00ed odd\u011blen\u00fd \u010d\u00e1rkou, kter\u00fd se pou\u017eije, pokud nefunguje zji\u0161\u0165ov\u00e1n\u00ed pomoc\u00ed mDNS.", + "title": "Konfigurace Google Cast" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ca.json b/homeassistant/components/climacell/translations/ca.json index fa924ae8272..78d43991e90 100644 --- a/homeassistant/components/climacell/translations/ca.json +++ b/homeassistant/components/climacell/translations/ca.json @@ -25,7 +25,7 @@ "data": { "timestep": "Minuts entre previsions NowCast" }, - "description": "Si decideixes activar l'entitat de predicci\u00f3 \"nowcast\", podr\u00e0s configurar l'interval en minuts entre cada previsi\u00f3. El nombre de previsions proporcionades dep\u00e8n d'aquest interval de minuts.", + "description": "Si decideixes activar l'entitat de previsi\u00f3 `nowcast`, podr\u00e0s configurar l'interval en minuts entre cada previsi\u00f3. El nombre de previsions proporcionades dep\u00e8n d'aquest interval de minuts.", "title": "Actualitzaci\u00f3 d'opcions de ClimaCell" } } diff --git a/homeassistant/components/climacell/translations/de.json b/homeassistant/components/climacell/translations/de.json index eb5f3f73faf..01e9a81647e 100644 --- a/homeassistant/components/climacell/translations/de.json +++ b/homeassistant/components/climacell/translations/de.json @@ -23,7 +23,7 @@ "step": { "init": { "data": { - "timestep": "Minuten zwischen den Kurzvorhersagen" + "timestep": "Minuten zwischen den NowCast Kurzvorhersagen" }, "description": "Wenn du die Vorhersage-Entitit\u00e4t \"Kurzvorhersage\" aktivierst, kannst du die Anzahl der Minuten zwischen den einzelnen Vorhersagen konfigurieren. Die Anzahl der bereitgestellten Vorhersagen h\u00e4ngt von der Anzahl der zwischen den Vorhersagen gew\u00e4hlten Minuten ab.", "title": "ClimaCell-Optionen aktualisieren" diff --git a/homeassistant/components/climacell/translations/en.json b/homeassistant/components/climacell/translations/en.json index a35be85d5b2..3e5cd436ba8 100644 --- a/homeassistant/components/climacell/translations/en.json +++ b/homeassistant/components/climacell/translations/en.json @@ -1,4 +1,24 @@ { + "config": { + "error": { + "cannot_connect": "Failed to connect", + "invalid_api_key": "Invalid API key", + "rate_limited": "Currently rate limited, please try again later.", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "api_version": "API Version", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name" + }, + "description": "If Latitude and Longitude are not provided, the default values in the Home Assistant configuration will be used. An entity will be created for each forecast type but only the ones you select will be enabled by default." + } + } + }, "options": { "step": { "init": { @@ -9,5 +29,6 @@ "title": "Update ClimaCell Options" } } - } + }, + "title": "ClimaCell" } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/fr.json b/homeassistant/components/climacell/translations/fr.json index 19b986a3db7..9194073bb5a 100644 --- a/homeassistant/components/climacell/translations/fr.json +++ b/homeassistant/components/climacell/translations/fr.json @@ -23,9 +23,9 @@ "step": { "init": { "data": { - "timestep": "Min. Entre les pr\u00e9visions NowCast" + "timestep": "Min. entre les pr\u00e9visions NowCast" }, - "description": "Si vous choisissez d'activer l'entit\u00e9 de pr\u00e9vision \u00abnowcast\u00bb, vous pouvez configurer le nombre de minutes entre chaque pr\u00e9vision. Le nombre de pr\u00e9visions fournies d\u00e9pend du nombre de minutes choisies entre les pr\u00e9visions.", + "description": "Si vous choisissez d'activer l'entit\u00e9 de pr\u00e9vision \u00ab\u00a0nowcast\u00a0\u00bb, vous pouvez configurer le nombre de minutes entre chaque pr\u00e9vision. Le nombre de pr\u00e9visions fournies d\u00e9pend du nombre de minutes choisies entre les pr\u00e9visions.", "title": "Mettre \u00e0 jour les options ClimaCell" } } diff --git a/homeassistant/components/climacell/translations/hu.json b/homeassistant/components/climacell/translations/hu.json index 3454a489455..bdeb913275c 100644 --- a/homeassistant/components/climacell/translations/hu.json +++ b/homeassistant/components/climacell/translations/hu.json @@ -3,7 +3,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", - "rate_limited": "Jelenleg korl\u00e1tozott sebess\u00e9g\u0171, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", + "rate_limited": "Jelenleg korl\u00e1tozott a hozz\u00e1f\u00e9r\u00e9s, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { @@ -13,7 +13,7 @@ "api_version": "API Verzi\u00f3", "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" }, "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." } @@ -26,7 +26,7 @@ "timestep": "Min. A NowCast el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt" }, "description": "Ha a `nowcast` el\u0151rejelz\u00e9si entit\u00e1s enged\u00e9lyez\u00e9s\u00e9t v\u00e1lasztja, be\u00e1ll\u00edthatja az egyes el\u0151rejelz\u00e9sek k\u00f6z\u00f6tti percek sz\u00e1m\u00e1t. A megadott el\u0151rejelz\u00e9sek sz\u00e1ma az el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt kiv\u00e1lasztott percek sz\u00e1m\u00e1t\u00f3l f\u00fcgg.", - "title": "Friss\u00edtse a ClimaCell be\u00e1ll\u00edt\u00e1sokat" + "title": "ClimaCell be\u00e1ll\u00edt\u00e1sok friss\u00edt\u00e9se" } } }, diff --git a/homeassistant/components/climacell/translations/pl.json b/homeassistant/components/climacell/translations/pl.json index dc08cb9b1bf..e107be1e001 100644 --- a/homeassistant/components/climacell/translations/pl.json +++ b/homeassistant/components/climacell/translations/pl.json @@ -23,7 +23,7 @@ "step": { "init": { "data": { - "timestep": "Minuty pomi\u0119dzy prognozami" + "timestep": "Czas (min) mi\u0119dzy prognozami NowCast" }, "description": "Je\u015bli zdecydujesz si\u0119 w\u0142\u0105czy\u0107 encj\u0119 prognozy \u201enowcast\u201d, mo\u017cesz skonfigurowa\u0107 liczb\u0119 minut mi\u0119dzy ka\u017cd\u0105 prognoz\u0105. Liczba dostarczonych prognoz zale\u017cy od liczby minut wybranych mi\u0119dzy prognozami.", "title": "Opcje aktualizacji ClimaCell" diff --git a/homeassistant/components/climacell/translations/sensor.fr.json b/homeassistant/components/climacell/translations/sensor.fr.json index 95625c2b8da..acff91fc570 100644 --- a/homeassistant/components/climacell/translations/sensor.fr.json +++ b/homeassistant/components/climacell/translations/sensor.fr.json @@ -2,7 +2,7 @@ "state": { "climacell__health_concern": { "good": "Bon", - "hazardous": "Hasardeux", + "hazardous": "Dangereux", "moderate": "Mod\u00e9r\u00e9", "unhealthy": "Mauvais pour la sant\u00e9", "unhealthy_for_sensitive_groups": "Mauvaise qualit\u00e9 pour les groupes sensibles", diff --git a/homeassistant/components/climacell/translations/sensor.hu.json b/homeassistant/components/climacell/translations/sensor.hu.json index d864d505143..656a460f429 100644 --- a/homeassistant/components/climacell/translations/sensor.hu.json +++ b/homeassistant/components/climacell/translations/sensor.hu.json @@ -17,7 +17,7 @@ "very_low": "Nagyon alacsony" }, "climacell__precipitation_type": { - "freezing_rain": "Fagyos es\u0151", + "freezing_rain": "Havas es\u0151", "ice_pellets": "\u00d3nos es\u0151", "none": "Nincs", "rain": "Es\u0151", diff --git a/homeassistant/components/climate/translations/et.json b/homeassistant/components/climate/translations/et.json index 7be57f4cdaa..c57a4d72991 100644 --- a/homeassistant/components/climate/translations/et.json +++ b/homeassistant/components/climate/translations/et.json @@ -18,8 +18,8 @@ "_": { "auto": "Automaatne", "cool": "Jahuta", - "dry": "Kuiv", - "fan_only": "Ainult ventilaator", + "dry": "Kuivata", + "fan_only": "Ventileeri", "heat": "Soojenda", "heat_cool": "K\u00fcta/jahuta", "off": "V\u00e4ljas" diff --git a/homeassistant/components/cloudflare/translations/he.json b/homeassistant/components/cloudflare/translations/he.json index 1f53e94240c..9303ea9143c 100644 --- a/homeassistant/components/cloudflare/translations/he.json +++ b/homeassistant/components/cloudflare/translations/he.json @@ -25,7 +25,8 @@ "user": { "data": { "api_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" - } + }, + "description": "\u05e9\u05d9\u05dc\u05d5\u05d1 \u05d6\u05d4 \u05d3\u05d5\u05e8\u05e9 \u05d0\u05e1\u05d9\u05de\u05d5\u05df API \u05e9\u05e0\u05d5\u05e6\u05e8 \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea Zone:Zone:Read and Zone:DNS:\u05e2\u05e8\u05d9\u05db\u05ea \u05d4\u05e8\u05e9\u05d0\u05d5\u05ea \u05e2\u05d1\u05d5\u05e8 \u05db\u05dc \u05d4\u05d0\u05d6\u05d5\u05e8\u05d9\u05dd \u05d1\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da." }, "zone": { "data": { diff --git a/homeassistant/components/coinbase/translations/ca.json b/homeassistant/components/coinbase/translations/ca.json index 0772a04aa15..0543f97003a 100644 --- a/homeassistant/components/coinbase/translations/ca.json +++ b/homeassistant/components/coinbase/translations/ca.json @@ -36,7 +36,8 @@ "data": { "account_balance_currencies": "Saldos de cartera a informar.", "exchange_base": "Moneda base per als sensors de canvi de tipus.", - "exchange_rate_currencies": "Tipus de canvi a informar." + "exchange_rate_currencies": "Tipus de canvi a informar.", + "exchnage_rate_precision": "Nombre de posicions decimals per als tipus de canvi." }, "description": "Ajusta les opcions de Coinbase" } diff --git a/homeassistant/components/coinbase/translations/de.json b/homeassistant/components/coinbase/translations/de.json index 4c1d2edc5ca..d4b58ae42bd 100644 --- a/homeassistant/components/coinbase/translations/de.json +++ b/homeassistant/components/coinbase/translations/de.json @@ -36,7 +36,8 @@ "data": { "account_balance_currencies": "Zu meldende Wallet-Guthaben.", "exchange_base": "Basisw\u00e4hrung f\u00fcr Wechselkurssensoren.", - "exchange_rate_currencies": "Zu meldende Wechselkurse." + "exchange_rate_currencies": "Zu meldende Wechselkurse.", + "exchnage_rate_precision": "Anzahl der Dezimalstellen f\u00fcr Wechselkurse." }, "description": "Coinbase-Optionen anpassen" } diff --git a/homeassistant/components/coinbase/translations/el.json b/homeassistant/components/coinbase/translations/el.json index 196aaad8643..05d6a1f7415 100644 --- a/homeassistant/components/coinbase/translations/el.json +++ b/homeassistant/components/coinbase/translations/el.json @@ -36,7 +36,8 @@ "data": { "account_balance_currencies": "\u03a5\u03c0\u03cc\u03bb\u03bf\u03b9\u03c0\u03b1 \u03c0\u03bf\u03c1\u03c4\u03bf\u03c6\u03bf\u03bb\u03b9\u03bf\u03cd \u03c0\u03c1\u03bf\u03c2 \u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac.", "exchange_base": "\u0392\u03b1\u03c3\u03b9\u03ba\u03cc \u03bd\u03cc\u03bc\u03b9\u03c3\u03bc\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03c5\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03c3\u03c5\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03ce\u03bd \u03b9\u03c3\u03bf\u03c4\u03b9\u03bc\u03b9\u03ce\u03bd.", - "exchange_rate_currencies": "\u03a3\u03c5\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b9\u03c3\u03bf\u03c4\u03b9\u03bc\u03af\u03b5\u03c2 \u03c0\u03c1\u03bf\u03c2 \u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac." + "exchange_rate_currencies": "\u03a3\u03c5\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b9\u03c3\u03bf\u03c4\u03b9\u03bc\u03af\u03b5\u03c2 \u03c0\u03c1\u03bf\u03c2 \u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac.", + "exchnage_rate_precision": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03b3\u03b9\u03b1 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b9\u03c3\u03bf\u03c4\u03b9\u03bc\u03af\u03b5\u03c2." }, "description": "\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd Coinbase" } diff --git a/homeassistant/components/coinbase/translations/en.json b/homeassistant/components/coinbase/translations/en.json index 019159c8057..d5d7483e260 100644 --- a/homeassistant/components/coinbase/translations/en.json +++ b/homeassistant/components/coinbase/translations/en.json @@ -14,7 +14,9 @@ "user": { "data": { "api_key": "API Key", - "api_token": "API Secret" + "api_token": "API Secret", + "currencies": "Account Balance Currencies", + "exchange_rates": "Exchange Rates" }, "description": "Please enter the details of your API key as provided by Coinbase.", "title": "Coinbase API Key Details" @@ -24,7 +26,9 @@ "options": { "error": { "currency_unavailable": "One or more of the requested currency balances is not provided by your Coinbase API.", + "currency_unavaliable": "One or more of the requested currency balances is not provided by your Coinbase API.", "exchange_rate_unavailable": "One or more of the requested exchange rates is not provided by Coinbase.", + "exchange_rate_unavaliable": "One or more of the requested exchange rates is not provided by Coinbase.", "unknown": "Unexpected error" }, "step": { diff --git a/homeassistant/components/coinbase/translations/et.json b/homeassistant/components/coinbase/translations/et.json index 0c30ec9ff34..821d17656d2 100644 --- a/homeassistant/components/coinbase/translations/et.json +++ b/homeassistant/components/coinbase/translations/et.json @@ -36,7 +36,8 @@ "data": { "account_balance_currencies": "Rahakoti saldod teavitamine.", "exchange_base": "Vahetuskursiandurite baasvaluuta.", - "exchange_rate_currencies": "Vahetuskursside aruanne." + "exchange_rate_currencies": "Vahetuskursside aruanne.", + "exchnage_rate_precision": "Vahetuskursside k\u00fcmnendkohtade arv." }, "description": "Kohanda Coinbase'i valikuid" } diff --git a/homeassistant/components/coinbase/translations/fr.json b/homeassistant/components/coinbase/translations/fr.json index 01cd7ec6dbf..77664cca73d 100644 --- a/homeassistant/components/coinbase/translations/fr.json +++ b/homeassistant/components/coinbase/translations/fr.json @@ -36,7 +36,8 @@ "data": { "account_balance_currencies": "Soldes du portefeuille \u00e0 d\u00e9clarer.", "exchange_base": "Devise de base pour les capteurs de taux de change.", - "exchange_rate_currencies": "Taux de change \u00e0 d\u00e9clarer." + "exchange_rate_currencies": "Taux de change \u00e0 d\u00e9clarer.", + "exchnage_rate_precision": "Nombre de d\u00e9cimales pour les taux de change." }, "description": "Ajuster les options de Coinbase" } diff --git a/homeassistant/components/coinbase/translations/hu.json b/homeassistant/components/coinbase/translations/hu.json index 3db29058e68..44287da0ee6 100644 --- a/homeassistant/components/coinbase/translations/hu.json +++ b/homeassistant/components/coinbase/translations/hu.json @@ -36,7 +36,8 @@ "data": { "account_balance_currencies": "Jelentend\u0151 p\u00e9nzt\u00e1rca egyenlegek.", "exchange_base": "Az \u00e1rfolyam-\u00e9rz\u00e9kel\u0151k alapvalut\u00e1ja.", - "exchange_rate_currencies": "Jelentend\u0151 \u00e1rfolyamok." + "exchange_rate_currencies": "Jelentend\u0151 \u00e1rfolyamok.", + "exchnage_rate_precision": "A tizedesjegyek sz\u00e1ma az \u00e1tv\u00e1lt\u00e1si \u00e1rfolyamokn\u00e1l." }, "description": "\u00c1ll\u00edtsa be a Coinbase opci\u00f3kat" } diff --git a/homeassistant/components/coinbase/translations/id.json b/homeassistant/components/coinbase/translations/id.json index 1d431ffd2fd..477aceafa45 100644 --- a/homeassistant/components/coinbase/translations/id.json +++ b/homeassistant/components/coinbase/translations/id.json @@ -36,7 +36,8 @@ "data": { "account_balance_currencies": "Saldo dompet untuk dilaporkan.", "exchange_base": "Mata uang dasar untuk sensor nilai tukar.", - "exchange_rate_currencies": "Nilai tukar untuk dilaporkan." + "exchange_rate_currencies": "Nilai tukar untuk dilaporkan.", + "exchnage_rate_precision": "Jumlah digit desimal untuk nilai tukar." }, "description": "Sesuaikan Opsi Coinbase" } diff --git a/homeassistant/components/coinbase/translations/it.json b/homeassistant/components/coinbase/translations/it.json index ecc0d93e747..64b5b0cdca7 100644 --- a/homeassistant/components/coinbase/translations/it.json +++ b/homeassistant/components/coinbase/translations/it.json @@ -36,7 +36,8 @@ "data": { "account_balance_currencies": "Saldi del portafoglio da segnalare.", "exchange_base": "Valuta di base per i sensori di tasso di cambio.", - "exchange_rate_currencies": "Tassi di cambio da segnalare." + "exchange_rate_currencies": "Tassi di cambio da segnalare.", + "exchnage_rate_precision": "Numero di cifre decimali per i tassi di cambio." }, "description": "Regola le opzioni di Coinbase" } diff --git a/homeassistant/components/coinbase/translations/ja.json b/homeassistant/components/coinbase/translations/ja.json index 6b6791d40b2..321e0c05d9d 100644 --- a/homeassistant/components/coinbase/translations/ja.json +++ b/homeassistant/components/coinbase/translations/ja.json @@ -36,7 +36,8 @@ "data": { "account_balance_currencies": "\u30a6\u30a9\u30ec\u30c3\u30c8\u306e\u6b8b\u9ad8\u3092\u5831\u544a\u3059\u308b\u3002", "exchange_base": "\u70ba\u66ff\u30ec\u30fc\u30c8\u30bb\u30f3\u30b5\u30fc\u306e\u57fa\u6e96\u901a\u8ca8\u3002", - "exchange_rate_currencies": "\u30ec\u30dd\u30fc\u30c8\u3059\u3079\u304d\u70ba\u66ff\u30ec\u30fc\u30c8" + "exchange_rate_currencies": "\u30ec\u30dd\u30fc\u30c8\u3059\u3079\u304d\u70ba\u66ff\u30ec\u30fc\u30c8", + "exchnage_rate_precision": "\u70ba\u66ff\u30ec\u30fc\u30c8\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3002" }, "description": "Coinbase\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u8abf\u6574" } diff --git a/homeassistant/components/coinbase/translations/nl.json b/homeassistant/components/coinbase/translations/nl.json index fc2068cb01a..98763deb9a7 100644 --- a/homeassistant/components/coinbase/translations/nl.json +++ b/homeassistant/components/coinbase/translations/nl.json @@ -36,7 +36,8 @@ "data": { "account_balance_currencies": "Wallet-saldi om te rapporteren.", "exchange_base": "Basisvaluta voor wisselkoerssensoren.", - "exchange_rate_currencies": "Wisselkoersen om te rapporteren." + "exchange_rate_currencies": "Wisselkoersen om te rapporteren.", + "exchnage_rate_precision": "Aantal decimalen voor wisselkoersen." }, "description": "Coinbase-opties aanpassen" } diff --git a/homeassistant/components/coinbase/translations/no.json b/homeassistant/components/coinbase/translations/no.json index 9a24d984c32..5171814cf9d 100644 --- a/homeassistant/components/coinbase/translations/no.json +++ b/homeassistant/components/coinbase/translations/no.json @@ -36,7 +36,8 @@ "data": { "account_balance_currencies": "Lommeboksaldoer som skal rapporteres.", "exchange_base": "Standardvaluta for valutakurssensorer.", - "exchange_rate_currencies": "Valutakurser som skal rapporteres." + "exchange_rate_currencies": "Valutakurser som skal rapporteres.", + "exchnage_rate_precision": "Antall desimaler for valutakurser." }, "description": "Juster Coinbase-alternativer" } diff --git a/homeassistant/components/coinbase/translations/pl.json b/homeassistant/components/coinbase/translations/pl.json index 14432be5928..7465ae24486 100644 --- a/homeassistant/components/coinbase/translations/pl.json +++ b/homeassistant/components/coinbase/translations/pl.json @@ -36,7 +36,8 @@ "data": { "account_balance_currencies": "Salda portfela do zg\u0142oszenia.", "exchange_base": "Waluta bazowa dla sensor\u00f3w kurs\u00f3w walut.", - "exchange_rate_currencies": "Kursy walut do zg\u0142oszenia." + "exchange_rate_currencies": "Kursy walut do zg\u0142oszenia.", + "exchnage_rate_precision": "Liczba miejsc po przecinku dla kurs\u00f3w walut." }, "description": "Dostosuj opcje Coinbase" } diff --git a/homeassistant/components/coinbase/translations/pt-BR.json b/homeassistant/components/coinbase/translations/pt-BR.json index 6596135208b..5ed52fa7afc 100644 --- a/homeassistant/components/coinbase/translations/pt-BR.json +++ b/homeassistant/components/coinbase/translations/pt-BR.json @@ -36,7 +36,8 @@ "data": { "account_balance_currencies": "Saldos da carteira a relatar.", "exchange_base": "Moeda base para sensores de taxa de c\u00e2mbio.", - "exchange_rate_currencies": "Taxas de c\u00e2mbio a informar." + "exchange_rate_currencies": "Taxas de c\u00e2mbio a informar.", + "exchnage_rate_precision": "N\u00famero de casas decimais para taxas de c\u00e2mbio." }, "description": "Ajustar as op\u00e7\u00f5es da Coinbase" } diff --git a/homeassistant/components/coinbase/translations/ru.json b/homeassistant/components/coinbase/translations/ru.json index 41439aacea8..951c0182320 100644 --- a/homeassistant/components/coinbase/translations/ru.json +++ b/homeassistant/components/coinbase/translations/ru.json @@ -36,7 +36,8 @@ "data": { "account_balance_currencies": "\u0411\u0430\u043b\u0430\u043d\u0441\u044b \u043a\u043e\u0448\u0435\u043b\u044c\u043a\u0430 \u0434\u043b\u044f \u043e\u0442\u0447\u0435\u0442\u043d\u043e\u0441\u0442\u0438.", "exchange_base": "\u0411\u0430\u0437\u043e\u0432\u0430\u044f \u0432\u0430\u043b\u044e\u0442\u0430 \u0434\u043b\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432 \u043e\u0431\u043c\u0435\u043d\u043d\u043e\u0433\u043e \u043a\u0443\u0440\u0441\u0430.", - "exchange_rate_currencies": "\u041a\u0443\u0440\u0441\u044b \u0432\u0430\u043b\u044e\u0442 \u0434\u043b\u044f \u043e\u0442\u0447\u0435\u0442\u043d\u043e\u0441\u0442\u0438." + "exchange_rate_currencies": "\u041a\u0443\u0440\u0441\u044b \u0432\u0430\u043b\u044e\u0442 \u0434\u043b\u044f \u043e\u0442\u0447\u0435\u0442\u043d\u043e\u0441\u0442\u0438.", + "exchnage_rate_precision": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439 \u0434\u043b\u044f \u043e\u0431\u043c\u0435\u043d\u043d\u044b\u0445 \u043a\u0443\u0440\u0441\u043e\u0432." }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 Coinbase" } diff --git a/homeassistant/components/coinbase/translations/tr.json b/homeassistant/components/coinbase/translations/tr.json index e52b6a81e76..e21cab489e4 100644 --- a/homeassistant/components/coinbase/translations/tr.json +++ b/homeassistant/components/coinbase/translations/tr.json @@ -36,7 +36,8 @@ "data": { "account_balance_currencies": "Rapor edilecek c\u00fczdan bakiyeleri.", "exchange_base": "D\u00f6viz kuru sens\u00f6rleri i\u00e7in temel para birimi.", - "exchange_rate_currencies": "Raporlanacak d\u00f6viz kurlar\u0131." + "exchange_rate_currencies": "Raporlanacak d\u00f6viz kurlar\u0131.", + "exchnage_rate_precision": "D\u00f6viz kurlar\u0131 i\u00e7in ondal\u0131k basamak say\u0131s\u0131." }, "description": "Coinbase Se\u00e7eneklerini Ayarlay\u0131n" } diff --git a/homeassistant/components/coinbase/translations/zh-Hant.json b/homeassistant/components/coinbase/translations/zh-Hant.json index 4510515bff2..315fe90254f 100644 --- a/homeassistant/components/coinbase/translations/zh-Hant.json +++ b/homeassistant/components/coinbase/translations/zh-Hant.json @@ -36,7 +36,8 @@ "data": { "account_balance_currencies": "\u5e33\u6236\u9918\u984d\u56de\u5831\u503c\u3002", "exchange_base": "\u532f\u7387\u611f\u6e2c\u5668\u57fa\u6e96\u8ca8\u5e63\u3002", - "exchange_rate_currencies": "\u532f\u7387\u56de\u5831\u503c\u3002" + "exchange_rate_currencies": "\u532f\u7387\u56de\u5831\u503c\u3002", + "exchnage_rate_precision": "\u532f\u7387\u5c0f\u6578\u4f4d\u6578\u3002" }, "description": "\u8abf\u6574 Coinbase \u9078\u9805" } diff --git a/homeassistant/components/coronavirus/translations/hu.json b/homeassistant/components/coronavirus/translations/hu.json index 9b79c82a014..880990f35c0 100644 --- a/homeassistant/components/coronavirus/translations/hu.json +++ b/homeassistant/components/coronavirus/translations/hu.json @@ -9,7 +9,7 @@ "data": { "country": "Orsz\u00e1g" }, - "title": "V\u00e1lassz egy orsz\u00e1got a megfigyel\u00e9shez" + "title": "V\u00e1lasszon egy orsz\u00e1got a figyel\u00e9shez" } } } diff --git a/homeassistant/components/cover/translations/he.json b/homeassistant/components/cover/translations/he.json index 66f6b9f3bbe..9e6fa77594f 100644 --- a/homeassistant/components/cover/translations/he.json +++ b/homeassistant/components/cover/translations/he.json @@ -10,7 +10,7 @@ "stop": "\u05e2\u05e6\u05d5\u05e8 {entity_name}" }, "condition_type": { - "is_closed": "{entity_name} \u05e1\u05d2\u05d5\u05e8", + "is_closed": "{entity_name} \u05e0\u05e1\u05d2\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", @@ -28,7 +28,7 @@ }, "state": { "_": { - "closed": "\u05e1\u05d2\u05d5\u05e8", + "closed": "\u05e0\u05e1\u05d2\u05e8", "closing": "\u05e1\u05d5\u05d2\u05e8", "open": "\u05e4\u05ea\u05d5\u05d7", "opening": "\u05e4\u05d5\u05ea\u05d7", diff --git a/homeassistant/components/cpuspeed/translations/bg.json b/homeassistant/components/cpuspeed/translations/bg.json index df41c1d7b0a..22f23d4428d 100644 --- a/homeassistant/components/cpuspeed/translations/bg.json +++ b/homeassistant/components/cpuspeed/translations/bg.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_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.", "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." }, "step": { diff --git a/homeassistant/components/cpuspeed/translations/ca.json b/homeassistant/components/cpuspeed/translations/ca.json index 17e6295a5a9..79d3ccdf412 100644 --- a/homeassistant/components/cpuspeed/translations/ca.json +++ b/homeassistant/components/cpuspeed/translations/ca.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_configured": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "already_configured": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "not_compatible": "No es pot obtenir la informaci\u00f3 de CPU, aquesta integraci\u00f3 no \u00e9s compatible amb el teu sistema" }, diff --git a/homeassistant/components/cpuspeed/translations/de.json b/homeassistant/components/cpuspeed/translations/de.json index f32ef08adba..60f5123a61c 100644 --- a/homeassistant/components/cpuspeed/translations/de.json +++ b/homeassistant/components/cpuspeed/translations/de.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_configured": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "already_configured": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "not_compatible": "CPU-Informationen k\u00f6nnen nicht abgerufen werden, diese Integration ist nicht mit deinem System kompatibel" }, diff --git a/homeassistant/components/cpuspeed/translations/el.json b/homeassistant/components/cpuspeed/translations/el.json index 4c3541e2640..aac30ca2a82 100644 --- a/homeassistant/components/cpuspeed/translations/el.json +++ b/homeassistant/components/cpuspeed/translations/el.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_configured": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae.", "already_configured": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae.", "not_compatible": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03bb\u03ae\u03c8\u03b7 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03b9\u03ce\u03bd CPU, \u03b1\u03c5\u03c4\u03ae \u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bc\u03b2\u03b1\u03c4\u03ae \u03bc\u03b5 \u03c4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03ac \u03c3\u03b1\u03c2" }, diff --git a/homeassistant/components/cpuspeed/translations/en.json b/homeassistant/components/cpuspeed/translations/en.json index adcf047b382..d482e5d3d3d 100644 --- a/homeassistant/components/cpuspeed/translations/en.json +++ b/homeassistant/components/cpuspeed/translations/en.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_configured": "Already configured. Only a single configuration possible.", "already_configured": "Already configured. Only a single configuration possible.", "not_compatible": "Unable to get CPU information, this integration is not compatible with your system" }, diff --git a/homeassistant/components/cpuspeed/translations/es.json b/homeassistant/components/cpuspeed/translations/es.json index 03b2a17dba3..87ff7b0c783 100644 --- a/homeassistant/components/cpuspeed/translations/es.json +++ b/homeassistant/components/cpuspeed/translations/es.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_configured": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "already_configured": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "not_compatible": "No se puede obtener informaci\u00f3n de la CPU, esta integraci\u00f3n no es compatible con su sistema" }, diff --git a/homeassistant/components/cpuspeed/translations/et.json b/homeassistant/components/cpuspeed/translations/et.json index 037d1b73086..69805d26d58 100644 --- a/homeassistant/components/cpuspeed/translations/et.json +++ b/homeassistant/components/cpuspeed/translations/et.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_configured": "Juba seadistatud. Lubatud on ainult \u00fcks sidumine.", "already_configured": "Juba seadistatud. Lubatud on ainult \u00fcks seadistamine.", "not_compatible": "CPU teavet ei saa hankida, see sidumine ei \u00fchildu s\u00fcsteemiga" }, diff --git a/homeassistant/components/cpuspeed/translations/fr.json b/homeassistant/components/cpuspeed/translations/fr.json index 5de49927c49..26428d40ef2 100644 --- a/homeassistant/components/cpuspeed/translations/fr.json +++ b/homeassistant/components/cpuspeed/translations/fr.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_configured": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "already_configured": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", "not_compatible": "Impossible d'obtenir les informations CPU, cette int\u00e9gration n'est pas compatible avec votre syst\u00e8me" }, diff --git a/homeassistant/components/cpuspeed/translations/he.json b/homeassistant/components/cpuspeed/translations/he.json index e38d585f2a4..9c1bb588a8f 100644 --- a/homeassistant/components/cpuspeed/translations/he.json +++ b/homeassistant/components/cpuspeed/translations/he.json @@ -1,13 +1,14 @@ { "config": { "abort": { - "alread_configured": "\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.", "already_configured": "\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": { "user": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?", + "title": "\u05de\u05d4\u05d9\u05e8\u05d5\u05ea \u05de\u05e2\u05d1\u05d3" } } - } + }, + "title": "\u05de\u05d4\u05d9\u05e8\u05d5\u05ea \u05de\u05e2\u05d1\u05d3" } \ No newline at end of file diff --git a/homeassistant/components/cpuspeed/translations/hu.json b/homeassistant/components/cpuspeed/translations/hu.json index 9ab3aa355dd..400baa4e362 100644 --- a/homeassistant/components/cpuspeed/translations/hu.json +++ b/homeassistant/components/cpuspeed/translations/hu.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_configured": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", "already_configured": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", "not_compatible": "Nem lehet CPU-inform\u00e1ci\u00f3kat lek\u00e9rni, ez az integr\u00e1ci\u00f3 nem kompatibilis a rendszer\u00e9vel." }, diff --git a/homeassistant/components/cpuspeed/translations/id.json b/homeassistant/components/cpuspeed/translations/id.json index ab3b16da4e9..e8a87d3707d 100644 --- a/homeassistant/components/cpuspeed/translations/id.json +++ b/homeassistant/components/cpuspeed/translations/id.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_configured": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", "already_configured": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", "not_compatible": "Tidak dapat mendapatkan informasi CPU, integrasi ini tidak kompatibel dengan sistem Anda" }, diff --git a/homeassistant/components/cpuspeed/translations/it.json b/homeassistant/components/cpuspeed/translations/it.json index 803458e48d7..ff84b5bf5ad 100644 --- a/homeassistant/components/cpuspeed/translations/it.json +++ b/homeassistant/components/cpuspeed/translations/it.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_configured": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", "already_configured": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", "not_compatible": "Impossibile ottenere informazioni sulla CPU, questa integrazione non \u00e8 compatibile con il tuo sistema" }, diff --git a/homeassistant/components/cpuspeed/translations/ja.json b/homeassistant/components/cpuspeed/translations/ja.json index d8e2fa85fb7..e2264b57dcb 100644 --- a/homeassistant/components/cpuspeed/translations/ja.json +++ b/homeassistant/components/cpuspeed/translations/ja.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", "not_compatible": "CPU\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3002\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306f\u3001\u304a\u4f7f\u3044\u306e\u30b7\u30b9\u30c6\u30e0\u3068\u4e92\u63db\u6027\u304c\u3042\u308a\u307e\u305b\u3093\u3002" }, diff --git a/homeassistant/components/cpuspeed/translations/nl.json b/homeassistant/components/cpuspeed/translations/nl.json index b29d9c9ddf1..c3d09b0b8a6 100644 --- a/homeassistant/components/cpuspeed/translations/nl.json +++ b/homeassistant/components/cpuspeed/translations/nl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_configured": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", "already_configured": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", "not_compatible": "Kan geen CPU-informatie ophalen, deze integratie is niet compatibel met uw systeem" }, diff --git a/homeassistant/components/cpuspeed/translations/no.json b/homeassistant/components/cpuspeed/translations/no.json index 40d5ed1de01..9f3cb464424 100644 --- a/homeassistant/components/cpuspeed/translations/no.json +++ b/homeassistant/components/cpuspeed/translations/no.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_configured": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", "already_configured": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", "not_compatible": "Kan ikke hente CPU-informasjon, denne integrasjonen er ikke kompatibel med systemet ditt" }, diff --git a/homeassistant/components/cpuspeed/translations/pl.json b/homeassistant/components/cpuspeed/translations/pl.json index 81c37098c29..be477365437 100644 --- a/homeassistant/components/cpuspeed/translations/pl.json +++ b/homeassistant/components/cpuspeed/translations/pl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_configured": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "already_configured": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "not_compatible": "Nie mo\u017cna uzyska\u0107 informacji o procesorze, ta integracja nie jest zgodna z twoim systemem" }, diff --git a/homeassistant/components/cpuspeed/translations/pt-BR.json b/homeassistant/components/cpuspeed/translations/pt-BR.json index f39ce9b4c9a..b21fcb00def 100644 --- a/homeassistant/components/cpuspeed/translations/pt-BR.json +++ b/homeassistant/components/cpuspeed/translations/pt-BR.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_configured": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "already_configured": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "not_compatible": "N\u00e3o \u00e9 poss\u00edvel obter informa\u00e7\u00f5es da CPU, esta integra\u00e7\u00e3o n\u00e3o \u00e9 compat\u00edvel com seu sistema" }, diff --git a/homeassistant/components/cpuspeed/translations/ru.json b/homeassistant/components/cpuspeed/translations/ru.json index 9638a43f7f7..0a44b56086c 100644 --- a/homeassistant/components/cpuspeed/translations/ru.json +++ b/homeassistant/components/cpuspeed/translations/ru.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_configured": "\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.", "already_configured": "\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.", "not_compatible": "\u041d\u0435 \u0443\u0434\u0430\u0451\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u043e\u0440\u0435, \u044d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u0430 \u0441 \u0412\u0430\u0448\u0435\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u043e\u0439." }, diff --git a/homeassistant/components/cpuspeed/translations/tr.json b/homeassistant/components/cpuspeed/translations/tr.json index d4dd5a271e0..f5689f2f3b4 100644 --- a/homeassistant/components/cpuspeed/translations/tr.json +++ b/homeassistant/components/cpuspeed/translations/tr.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_configured": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "already_configured": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", "not_compatible": "CPU bilgisi al\u0131nam\u0131yor, bu entegrasyon sisteminizle uyumlu de\u011fil" }, diff --git a/homeassistant/components/cpuspeed/translations/zh-Hans.json b/homeassistant/components/cpuspeed/translations/zh-Hans.json index 41130cbdd39..95f5766edc9 100644 --- a/homeassistant/components/cpuspeed/translations/zh-Hans.json +++ b/homeassistant/components/cpuspeed/translations/zh-Hans.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_configured": "\u5f53\u524d\u96c6\u6210\u5df2\u88ab\u914d\u7f6e\uff0c\u4ec5\u80fd\u53ea\u6709\u4e00\u4e2a\u914d\u7f6e", "already_configured": "\u5f53\u524d\u96c6\u6210\u5df2\u88ab\u914d\u7f6e\uff0c\u4ec5\u80fd\u53ea\u6709\u4e00\u4e2a\u914d\u7f6e", "not_compatible": "\u65e0\u6cd5\u83b7\u53d6 CPU \u4fe1\u606f\uff0c\u8be5\u96c6\u6210\u4e0e\u60a8\u7684\u7cfb\u7edf\u4e0d\u517c\u5bb9" }, diff --git a/homeassistant/components/cpuspeed/translations/zh-Hant.json b/homeassistant/components/cpuspeed/translations/zh-Hant.json index 4885aead70a..b88f1d1b1d4 100644 --- a/homeassistant/components/cpuspeed/translations/zh-Hant.json +++ b/homeassistant/components/cpuspeed/translations/zh-Hant.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "alread_configured": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "already_configured": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "not_compatible": "\u7121\u6cd5\u53d6\u5f97 CPU \u8cc7\u8a0a\uff0c\u9019\u500b\u63d2\u4ef6\u8207\u4f60\u7684\u7cfb\u7d71\u4e0d\u76f8\u5bb9" }, diff --git a/homeassistant/components/crownstone/translations/ca.json b/homeassistant/components/crownstone/translations/ca.json index 9de845d87c6..deded7c6b71 100644 --- a/homeassistant/components/crownstone/translations/ca.json +++ b/homeassistant/components/crownstone/translations/ca.json @@ -56,13 +56,6 @@ "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" @@ -70,26 +63,12 @@ "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" } } } diff --git a/homeassistant/components/crownstone/translations/cs.json b/homeassistant/components/crownstone/translations/cs.json index 1f14cdd8eff..b6c0cae1be2 100644 --- a/homeassistant/components/crownstone/translations/cs.json +++ b/homeassistant/components/crownstone/translations/cs.json @@ -28,20 +28,10 @@ "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/de.json b/homeassistant/components/crownstone/translations/de.json index a969d9b2999..054d6987c29 100644 --- a/homeassistant/components/crownstone/translations/de.json +++ b/homeassistant/components/crownstone/translations/de.json @@ -56,13 +56,6 @@ "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" @@ -70,26 +63,12 @@ "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" } } } diff --git a/homeassistant/components/crownstone/translations/el.json b/homeassistant/components/crownstone/translations/el.json index 81cfbd9ad39..b682dedcb1b 100644 --- a/homeassistant/components/crownstone/translations/el.json +++ b/homeassistant/components/crownstone/translations/el.json @@ -56,13 +56,6 @@ "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1 \u03c4\u03bf\u03c5 Crownstone USB dongle.\n\n\u0391\u03bd\u03b1\u03b6\u03b7\u03c4\u03ae\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03b5 VID 10C4 \u03ba\u03b1\u03b9 PID EA60.", "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 Crownstone USB dongle" }, - "usb_config_option": { - "data": { - "usb_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1 \u03c4\u03bf\u03c5 Crownstone USB dongle.\n\n\u0391\u03bd\u03b1\u03b6\u03b7\u03c4\u03ae\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03b5 VID 10C4 \u03ba\u03b1\u03b9 PID EA60.", - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 Crownstone USB dongle" - }, "usb_manual_config": { "data": { "usb_manual_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" @@ -70,26 +63,12 @@ "description": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03b5\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae\u03c2 \u03b5\u03bd\u03cc\u03c2 dongle USB Crownstone.", "title": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03b5\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae\u03c2 USB dongle Crownstone" }, - "usb_manual_config_option": { - "data": { - "usb_manual_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" - }, - "description": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03b5\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae\u03c2 \u03b5\u03bd\u03cc\u03c2 dongle USB Crownstone.", - "title": "\u03a7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03b5\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae\u03c2 USB dongle Crownstone" - }, "usb_sphere_config": { "data": { "usb_sphere": "Crownstone Sphere" }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03af\u03b1 Crownstone Sphere \u03cc\u03c0\u03bf\u03c5 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c4\u03bf USB.", "title": "Crownstone USB Sphere" - }, - "usb_sphere_config_option": { - "data": { - "usb_sphere": "Crownstone Sphere" - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03af\u03b1 Crownstone Sphere \u03cc\u03c0\u03bf\u03c5 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c4\u03bf USB.", - "title": "Crownstone USB Sphere" } } } diff --git a/homeassistant/components/crownstone/translations/en.json b/homeassistant/components/crownstone/translations/en.json index d6070c90a0f..09a26b9739c 100644 --- a/homeassistant/components/crownstone/translations/en.json +++ b/homeassistant/components/crownstone/translations/en.json @@ -56,13 +56,6 @@ "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" @@ -70,26 +63,12 @@ "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-419.json b/homeassistant/components/crownstone/translations/es-419.json index 1c904302541..40b4cebe97b 100644 --- a/homeassistant/components/crownstone/translations/es-419.json +++ b/homeassistant/components/crownstone/translations/es-419.json @@ -39,9 +39,6 @@ "usb_config": { "description": "Seleccione el puerto serie del dongle USB Crownstone. \n\n Busque un dispositivo con VID 10C4 y PID EA60.", "title": "Configuraci\u00f3n del dongle USB Crownstone" - }, - "usb_config_option": { - "description": "Seleccione el puerto serie del dongle USB Crownstone. \n\n Busque un dispositivo con VID 10C4 y PID EA60." } } } diff --git a/homeassistant/components/crownstone/translations/es.json b/homeassistant/components/crownstone/translations/es.json index 9e4ac60832f..f52b0074322 100644 --- a/homeassistant/components/crownstone/translations/es.json +++ b/homeassistant/components/crownstone/translations/es.json @@ -56,13 +56,6 @@ "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" - }, - "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_manual_config": { "data": { "usb_manual_path": "Ruta del dispositivo USB" @@ -70,26 +63,12 @@ "description": "Introduzca manualmente la ruta de un dispositivo USB Crownstone.", "title": "Ruta manual del dispositivo USB Crownstone" }, - "usb_manual_config_option": { - "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_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" } } } diff --git a/homeassistant/components/crownstone/translations/et.json b/homeassistant/components/crownstone/translations/et.json index 3a651257e1a..da7831e8959 100644 --- a/homeassistant/components/crownstone/translations/et.json +++ b/homeassistant/components/crownstone/translations/et.json @@ -56,13 +56,6 @@ "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" @@ -70,26 +63,12 @@ "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" } } } diff --git a/homeassistant/components/crownstone/translations/fr.json b/homeassistant/components/crownstone/translations/fr.json index 61a2f0da436..59f9aa0f20b 100644 --- a/homeassistant/components/crownstone/translations/fr.json +++ b/homeassistant/components/crownstone/translations/fr.json @@ -56,13 +56,6 @@ "description": "S\u00e9lectionnez le port s\u00e9rie du dongle USB Crownstone. \n\n Recherchez un appareil avec VID 10C4 et PID EA60.", "title": "Configuration du dongle USB Crownstone" }, - "usb_config_option": { - "data": { - "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" - }, - "description": "S\u00e9lectionnez le port s\u00e9rie du dongle USB Crownstone. \n\n Recherchez un appareil avec VID 10C4 et PID EA60.", - "title": "Configuration du dongle USB Crownstone" - }, "usb_manual_config": { "data": { "usb_manual_path": "Chemin du p\u00e9riph\u00e9rique USB" @@ -70,26 +63,12 @@ "description": "Entrez manuellement le chemin d'un dongle USB Crownstone.", "title": "Chemin d'acc\u00e8s manuel du dongle USB Crownstone" }, - "usb_manual_config_option": { - "data": { - "usb_manual_path": "Chemin du p\u00e9riph\u00e9rique USB" - }, - "description": "Entrez manuellement le chemin d'un dongle USB Crownstone.", - "title": "Chemin d'acc\u00e8s manuel du dongle USB Crownstone" - }, "usb_sphere_config": { "data": { "usb_sphere": "Crownstone Sphere" }, "description": "S\u00e9lectionnez une sph\u00e8re Crownstone o\u00f9 se trouve l\u2019USB.", "title": "Sph\u00e8re USB Crownstone" - }, - "usb_sphere_config_option": { - "data": { - "usb_sphere": "Crownstone Sphere" - }, - "description": "S\u00e9lectionnez une sph\u00e8re Crownstone o\u00f9 se trouve l\u2019USB.", - "title": "Sph\u00e8re USB Crownstone" } } } diff --git a/homeassistant/components/crownstone/translations/he.json b/homeassistant/components/crownstone/translations/he.json index af11b65839b..db4289ffebc 100644 --- a/homeassistant/components/crownstone/translations/he.json +++ b/homeassistant/components/crownstone/translations/he.json @@ -33,20 +33,10 @@ "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" - } } } } diff --git a/homeassistant/components/crownstone/translations/hu.json b/homeassistant/components/crownstone/translations/hu.json index 2c2a2e34fe1..f2ffe6f2b07 100644 --- a/homeassistant/components/crownstone/translations/hu.json +++ b/homeassistant/components/crownstone/translations/hu.json @@ -56,13 +56,6 @@ "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" @@ -70,26 +63,12 @@ "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" } } } diff --git a/homeassistant/components/crownstone/translations/id.json b/homeassistant/components/crownstone/translations/id.json index aef98346fd2..381645f1453 100644 --- a/homeassistant/components/crownstone/translations/id.json +++ b/homeassistant/components/crownstone/translations/id.json @@ -56,13 +56,6 @@ "description": "Pilih port serial dongle USB Crownstone. \n\nCari perangkat dengan VID 10C4 dan PID EA60.", "title": "Konfigurasi dongle USB Crownstone" }, - "usb_config_option": { - "data": { - "usb_path": "Jalur Perangkat USB" - }, - "description": "Pilih port serial dongle USB Crownstone. \n\nCari perangkat dengan VID 10C4 dan PID EA60.", - "title": "Konfigurasi dongle USB Crownstone" - }, "usb_manual_config": { "data": { "usb_manual_path": "Jalur Perangkat USB" @@ -70,26 +63,12 @@ "description": "Masukkan jalur dongle USB Crownstone secara manual.", "title": "Jalur manual dongle USB Crownstone" }, - "usb_manual_config_option": { - "data": { - "usb_manual_path": "Jalur Perangkat USB" - }, - "description": "Masukkan jalur dongle USB Crownstone secara manual.", - "title": "Jalur manual dongle USB Crownstone" - }, "usb_sphere_config": { "data": { "usb_sphere": "Crownstone Sphere" }, "description": "Pilih Crownstone Sphere tempat USB berada.", "title": "Crownstone USB Sphere" - }, - "usb_sphere_config_option": { - "data": { - "usb_sphere": "Crownstone Sphere" - }, - "description": "Pilih Crownstone Sphere tempat USB berada.", - "title": "Crownstone USB Sphere" } } } diff --git a/homeassistant/components/crownstone/translations/it.json b/homeassistant/components/crownstone/translations/it.json index 062f77c9349..5600ff09de8 100644 --- a/homeassistant/components/crownstone/translations/it.json +++ b/homeassistant/components/crownstone/translations/it.json @@ -56,13 +56,6 @@ "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" @@ -70,26 +63,12 @@ "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": "Immetti 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" } } } diff --git a/homeassistant/components/crownstone/translations/ja.json b/homeassistant/components/crownstone/translations/ja.json index 6ab8f858af4..639922c4d94 100644 --- a/homeassistant/components/crownstone/translations/ja.json +++ b/homeassistant/components/crownstone/translations/ja.json @@ -56,13 +56,6 @@ "description": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u30b7\u30ea\u30a2\u30eb \u30dd\u30fc\u30c8\u3092\u9078\u629e\u3057\u307e\u3059\u3002\n\nVID 10C4 \u3067 PID EA60 \u306a\u5024\u306e\u30c7\u30d0\u30a4\u30b9\u3092\u63a2\u3057\u307e\u3059\u3002", "title": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u8a2d\u5b9a" }, - "usb_config_option": { - "data": { - "usb_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" - }, - "description": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u30b7\u30ea\u30a2\u30eb \u30dd\u30fc\u30c8\u3092\u9078\u629e\u3057\u307e\u3059\u3002\n\nVID 10C4 \u3067 PID EA60 \u306a\u5024\u306e\u30c7\u30d0\u30a4\u30b9\u3092\u63a2\u3057\u307e\u3059\u3002", - "title": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u8a2d\u5b9a" - }, "usb_manual_config": { "data": { "usb_manual_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" @@ -70,26 +63,12 @@ "description": "\u624b\u52d5\u3067\u3001Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u30d1\u30b9\u3092\u5165\u529b\u3057\u307e\u3059\u3002", "title": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u3078\u306e\u624b\u52d5\u30d1\u30b9" }, - "usb_manual_config_option": { - "data": { - "usb_manual_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" - }, - "description": "\u624b\u52d5\u3067\u3001Crownstone USB\u30c9\u30f3\u30b0\u30eb\u306e\u30d1\u30b9\u3092\u5165\u529b\u3057\u307e\u3059\u3002", - "title": "Crownstone USB\u30c9\u30f3\u30b0\u30eb\u3078\u306e\u624b\u52d5\u30d1\u30b9" - }, "usb_sphere_config": { "data": { "usb_sphere": "Crownstone Sphere" }, "description": "USB\u306b\u914d\u7f6e\u3055\u308c\u3066\u3044\u308b\u3001CrownstoneSphere\u3092\u9078\u629e\u3057\u307e\u3059\u3002", "title": "Crownstone USB Sphere" - }, - "usb_sphere_config_option": { - "data": { - "usb_sphere": "Crownstone Sphere" - }, - "description": "USB\u306b\u914d\u7f6e\u3055\u308c\u3066\u3044\u308b\u3001CrownstoneSphere\u3092\u9078\u629e\u3057\u307e\u3059\u3002", - "title": "Crownstone USB Sphere" } } } diff --git a/homeassistant/components/crownstone/translations/ko.json b/homeassistant/components/crownstone/translations/ko.json deleted file mode 100644 index aadd2d3da42..00000000000 --- a/homeassistant/components/crownstone/translations/ko.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "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 index 1da12c8f841..91dddf8619e 100644 --- a/homeassistant/components/crownstone/translations/nl.json +++ b/homeassistant/components/crownstone/translations/nl.json @@ -56,13 +56,6 @@ "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" @@ -70,26 +63,12 @@ "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" } } } diff --git a/homeassistant/components/crownstone/translations/no.json b/homeassistant/components/crownstone/translations/no.json index 88f3578a9a4..33e61211205 100644 --- a/homeassistant/components/crownstone/translations/no.json +++ b/homeassistant/components/crownstone/translations/no.json @@ -56,13 +56,6 @@ "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" @@ -70,26 +63,12 @@ "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" } } } diff --git a/homeassistant/components/crownstone/translations/pl.json b/homeassistant/components/crownstone/translations/pl.json index 12ac55d668c..49a1d888a94 100644 --- a/homeassistant/components/crownstone/translations/pl.json +++ b/homeassistant/components/crownstone/translations/pl.json @@ -56,13 +56,6 @@ "description": "Wybierz port szeregowy urz\u0105dzenia USB Crownstone. \n\nPoszukaj urz\u0105dzenia z VID 10C4 i PID EA60.", "title": "Konfiguracja urz\u0105dzenia USB Crownstone" }, - "usb_config_option": { - "data": { - "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" - }, - "description": "Wybierz port szeregowy urz\u0105dzenia USB Crownstone. \n\nPoszukaj urz\u0105dzenia z VID 10C4 i PID EA60.", - "title": "Konfiguracja urz\u0105dzenia USB Crownstone" - }, "usb_manual_config": { "data": { "usb_manual_path": "\u015acie\u017cka urz\u0105dzenia USB" @@ -70,26 +63,12 @@ "description": "Wprowad\u017a r\u0119cznie \u015bcie\u017ck\u0119 urz\u0105dzenia USB Crownstone.", "title": "Wpisz r\u0119cznie \u015bcie\u017ck\u0119 urz\u0105dzenia USB Crownstone" }, - "usb_manual_config_option": { - "data": { - "usb_manual_path": "\u015acie\u017cka urz\u0105dzenia USB" - }, - "description": "Wprowad\u017a r\u0119cznie \u015bcie\u017ck\u0119 urz\u0105dzenia USB Crownstone.", - "title": "Wpisz r\u0119cznie \u015bcie\u017ck\u0119 urz\u0105dzenia USB Crownstone" - }, "usb_sphere_config": { "data": { "usb_sphere": "Crownstone Sphere" }, "description": "Wybierz USB, w kt\u00f3rym znajduje si\u0119 Crownstone Sphere.", "title": "USB Crownstone Sphere" - }, - "usb_sphere_config_option": { - "data": { - "usb_sphere": "Crownstone Sphere" - }, - "description": "Wybierz USB w kt\u00f3rym znajduje si\u0119 Crownstone Sphere.", - "title": "USB Crownstone Sphere" } } } diff --git a/homeassistant/components/crownstone/translations/pt-BR.json b/homeassistant/components/crownstone/translations/pt-BR.json index df4e446837e..f68c7a7f483 100644 --- a/homeassistant/components/crownstone/translations/pt-BR.json +++ b/homeassistant/components/crownstone/translations/pt-BR.json @@ -56,13 +56,6 @@ "description": "Selecione a porta serial do dongle USB Crownstone. \n\n Procure um dispositivo com VID 10C4 e PID EA60.", "title": "Configura\u00e7\u00e3o do dongle USB Crownstone" }, - "usb_config_option": { - "data": { - "usb_path": "Caminho do Dispositivo USB" - }, - "description": "Selecione a porta serial do dongle USB Crownstone. \n\n Procure um dispositivo com VID 10C4 e PID EA60.", - "title": "Configura\u00e7\u00e3o do dongle USB Crownstone" - }, "usb_manual_config": { "data": { "usb_manual_path": "Caminho do Dispositivo USB" @@ -70,26 +63,12 @@ "description": "Insira manualmente o caminho de um dongle USB Crownstone.", "title": "Caminho manual do dongle USB Crownstone" }, - "usb_manual_config_option": { - "data": { - "usb_manual_path": "Caminho do Dispositivo USB" - }, - "description": "Insira manualmente o caminho de um dongle USB Crownstone.", - "title": "Caminho manual do dongle USB Crownstone" - }, "usb_sphere_config": { "data": { "usb_sphere": "Esfera da Pedra da Coroa" }, "description": "Selecione um Crownstone Sphere onde o USB est\u00e1 localizado.", "title": "Esfera USB Crownstone" - }, - "usb_sphere_config_option": { - "data": { - "usb_sphere": "Esfera de Crownstone" - }, - "description": "Selecione um Crownstone Sphere onde o USB est\u00e1 localizado.", - "title": "Esfera USB Crownstone" } } } diff --git a/homeassistant/components/crownstone/translations/ru.json b/homeassistant/components/crownstone/translations/ru.json index 7dfd88bd63e..d16b16f457e 100644 --- a/homeassistant/components/crownstone/translations/ru.json +++ b/homeassistant/components/crownstone/translations/ru.json @@ -56,13 +56,6 @@ "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" @@ -70,26 +63,12 @@ "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" } } } diff --git a/homeassistant/components/crownstone/translations/tr.json b/homeassistant/components/crownstone/translations/tr.json index 1c97cbdf170..d244ed239a3 100644 --- a/homeassistant/components/crownstone/translations/tr.json +++ b/homeassistant/components/crownstone/translations/tr.json @@ -56,13 +56,6 @@ "description": "Crownstone USB donan\u0131m kilidinin seri ba\u011flant\u0131 noktas\u0131n\u0131 se\u00e7in. \n\n VID 10C4 ve PID EA60'a sahip bir cihaz aray\u0131n.", "title": "Crownstone USB dongle yap\u0131land\u0131rmas\u0131" }, - "usb_config_option": { - "data": { - "usb_path": "USB Cihaz Yolu" - }, - "description": "Crownstone USB donan\u0131m kilidinin seri ba\u011flant\u0131 noktas\u0131n\u0131 se\u00e7in. \n\n VID 10C4 ve PID EA60'a sahip bir cihaz aray\u0131n.", - "title": "Crownstone USB dongle yap\u0131land\u0131rmas\u0131" - }, "usb_manual_config": { "data": { "usb_manual_path": "USB Cihaz Yolu" @@ -70,26 +63,12 @@ "description": "Crownstone USB dongle'\u0131n yolunu manuel olarak girin.", "title": "Crownstone USB dongle manuel yolu" }, - "usb_manual_config_option": { - "data": { - "usb_manual_path": "USB Cihaz Yolu" - }, - "description": "Crownstone USB dongle'\u0131n yolunu manuel olarak girin.", - "title": "Crownstone USB dongle manuel yolu" - }, "usb_sphere_config": { "data": { "usb_sphere": "Crownstone Stick" }, "description": "USB'nin bulundu\u011fu bir Crownstone Stick se\u00e7in.", "title": "Crownstone USB Stick" - }, - "usb_sphere_config_option": { - "data": { - "usb_sphere": "Crownstone Stick" - }, - "description": "USB'nin bulundu\u011fu bir Crownstone Stick se\u00e7in.", - "title": "Crownstone USB Stick" } } } diff --git a/homeassistant/components/crownstone/translations/zh-Hant.json b/homeassistant/components/crownstone/translations/zh-Hant.json index 2c362ba0bcb..32ef596aba3 100644 --- a/homeassistant/components/crownstone/translations/zh-Hant.json +++ b/homeassistant/components/crownstone/translations/zh-Hant.json @@ -56,13 +56,6 @@ "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" @@ -70,26 +63,12 @@ "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" } } } diff --git a/homeassistant/components/deconz/translations/he.json b/homeassistant/components/deconz/translations/he.json index 3e2b350a0d9..97274180aef 100644 --- a/homeassistant/components/deconz/translations/he.json +++ b/homeassistant/components/deconz/translations/he.json @@ -12,7 +12,7 @@ "flow_title": "{host}", "step": { "link": { - "description": "\u05d1\u05d8\u05dc \u05d0\u05ea \u05e0\u05e2\u05d9\u05dc\u05ea \u05d4\u05de\u05e9\u05e8 deCONZ \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05e2\u05dd Home Assistant.\n\n 1. \u05e2\u05d1\u05d5\u05e8 \u05d0\u05dc \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05de\u05e2\u05e8\u05db\u05ea deCONZ \n .2 \u05dc\u05d7\u05e5 \u05e2\u05dc \"Unlock Gateway\"", + "description": "\u05d1\u05d8\u05dc \u05d0\u05ea \u05e0\u05e2\u05d9\u05dc\u05ea \u05d4\u05de\u05e9\u05e8 deCONZ \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05e2\u05dd Home Assistant.\n\n 1. \u05d9\u05e9 \u05dc\u05e2\u05d1\u05d5\u05e8 \u05d0\u05dc \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea deCONZ > \u05e9\u05e2\u05e8 > \u05de\u05ea\u05e7\u05d3\u05dd\n 2. \u05dc\u05d7\u05d9\u05e6\u05d4 \u05e2\u05dc \u05db\u05e4\u05ea\u05d5\u05e8 \"\u05d0\u05d9\u05de\u05d5\u05ea \u05d9\u05d9\u05e9\u05d5\u05dd\"", "title": "\u05e7\u05e9\u05e8 \u05e2\u05dd deCONZ" }, "manual_input": { diff --git a/homeassistant/components/deconz/translations/hu.json b/homeassistant/components/deconz/translations/hu.json index 4ffc1662ecb..47bb343247c 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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "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", diff --git a/homeassistant/components/deluge/translations/bg.json b/homeassistant/components/deluge/translations/bg.json new file mode 100644 index 00000000000..2a0cd494422 --- /dev/null +++ b/homeassistant/components/deluge/translations/bg.json @@ -0,0 +1,21 @@ +{ + "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\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" + }, + "step": { + "user": { + "data": { + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/ca.json b/homeassistant/components/deluge/translations/ca.json new file mode 100644 index 00000000000..df0a2a31ee4 --- /dev/null +++ b/homeassistant/components/deluge/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari", + "web_port": "Port web (per al servei de visita)" + }, + "description": "Per poder utilitzar aquesta integraci\u00f3, has d'activar l'opci\u00f3 seg\u00fcent a la configuraci\u00f3 de Deluge: Daemon > Permet controls remots" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/cs.json b/homeassistant/components/deluge/translations/cs.json new file mode 100644 index 00000000000..2845835ef00 --- /dev/null +++ b/homeassistant/components/deluge/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/de.json b/homeassistant/components/deluge/translations/de.json new file mode 100644 index 00000000000..9e8d559a523 --- /dev/null +++ b/homeassistant/components/deluge/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort", + "port": "Port", + "username": "Benutzername", + "web_port": "Webport (f\u00fcr Besuchsdienste)" + }, + "description": "Um diese Integration nutzen zu k\u00f6nnen, musst du die folgende Option in den Deluge-Einstellungen aktivieren: Daemon > Fernsteuerungen zulassen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/el.json b/homeassistant/components/deluge/translations/el.json new file mode 100644 index 00000000000..48645a7378a --- /dev/null +++ b/homeassistant/components/deluge/translations/el.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", + "web_port": "\u0398\u03cd\u03c1\u03b1 \u0399\u03c3\u03c4\u03bf\u03cd (\u03b3\u03b9\u03b1 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03b5\u03c0\u03af\u03c3\u03ba\u03b5\u03c8\u03b7\u03c2)" + }, + "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03bc\u03c0\u03bf\u03c1\u03ad\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c3\u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03ba\u03b1\u03c4\u03b1\u03ba\u03bb\u03c5\u03c3\u03bc\u03bf\u03cd: Daemon > \u039d\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03bf \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/en.json b/homeassistant/components/deluge/translations/en.json index a3a2f539126..3f4e4b0b445 100644 --- a/homeassistant/components/deluge/translations/en.json +++ b/homeassistant/components/deluge/translations/en.json @@ -1,23 +1,23 @@ { "config": { - "step": { - "user": { - "description": "To be able to use this integration, you have to enable the following option in deluge settings: Daemon > Allow remote controls", - "data": { - "host": "Host", - "username": "Username", - "password": "Password", - "port": "Port", - "web_port": "Web port (for visiting service)" - } - } + "abort": { + "already_configured": "Service is already configured" }, "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication" }, - "abort": { - "already_configured": "Service is already configured" + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "username": "Username", + "web_port": "Web port (for visiting service)" + }, + "description": "To be able to use this integration, you have to enable the following option in deluge settings: Daemon > Allow remote controls" + } } } } \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/et.json b/homeassistant/components/deluge/translations/et.json new file mode 100644 index 00000000000..26d8d23438d --- /dev/null +++ b/homeassistant/components/deluge/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na", + "port": "Port", + "username": "Kasutajanimi", + "web_port": "Veebiport (teenuse k\u00fclastamiseks)" + }, + "description": "Selle sidumise kasutamiseks deluge s\u00e4tetes lubama j\u00e4rgmise suvandi: Daemon > Luba kaugjuhtimispuldid" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/fr.json b/homeassistant/components/deluge/translations/fr.json new file mode 100644 index 00000000000..4607d78f78c --- /dev/null +++ b/homeassistant/components/deluge/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur", + "web_port": "Port web (pour consulter le service)" + }, + "description": "Afin de pouvoir utiliser cette int\u00e9gration, vous devez activer l'option suivante dans les param\u00e8tres de Deluge\u00a0: D\u00e9mon > Autoriser les connexion \u00e0 distance" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/he.json b/homeassistant/components/deluge/translations/he.json new file mode 100644 index 00000000000..80971c19dfc --- /dev/null +++ b/homeassistant/components/deluge/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/hu.json b/homeassistant/components/deluge/translations/hu.json new file mode 100644 index 00000000000..6058a985c55 --- /dev/null +++ b/homeassistant/components/deluge/translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "web_port": "Web port (a szolg\u00e1ltat\u00e1s l\u00e1togat\u00e1s\u00e1hoz)" + }, + "description": "Ahhoz, hogy ezt az integr\u00e1ci\u00f3t haszn\u00e1lni tudja, enged\u00e9lyeznie kell a k\u00f6vetkez\u0151 opci\u00f3t a be\u00e1ll\u00edt\u00e1sokban: Daemon > Allow remote controls" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/id.json b/homeassistant/components/deluge/translations/id.json new file mode 100644 index 00000000000..8a2fce22fd5 --- /dev/null +++ b/homeassistant/components/deluge/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "port": "Port", + "username": "Nama Pengguna", + "web_port": "Port web (untuk mengunjungi layanan)" + }, + "description": "Untuk dapat menggunakan integrasi ini, Anda harus mengaktifkan opsi berikut dalam pengaturan deluge: Daemon > Izinkan kendali jarak jauh" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/it.json b/homeassistant/components/deluge/translations/it.json new file mode 100644 index 00000000000..d9407f0c29f --- /dev/null +++ b/homeassistant/components/deluge/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Porta", + "username": "Nome utente", + "web_port": "Porta web (per il servizio di visita)" + }, + "description": "Per poter utilizzare questa integrazione, devi abilitare la seguente opzione nelle impostazioni di diluvio: Demone > Consenti controlli " + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/ja.json b/homeassistant/components/deluge/translations/ja.json new file mode 100644 index 00000000000..ab796327717 --- /dev/null +++ b/homeassistant/components/deluge/translations/ja.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d", + "web_port": "Web\u30dd\u30fc\u30c8\uff08\u8a2a\u554f\u30b5\u30fc\u30d3\u30b9\u7528\uff09" + }, + "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u5229\u7528\u3059\u308b\u305f\u3081\u306b\u306f\u3001deluge\u306e\u8a2d\u5b9a\u3067\u4ee5\u4e0b\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u6709\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u30c7\u30fc\u30e2\u30f3 -> \u30ea\u30e2\u30fc\u30c8\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u306e\u8a31\u53ef" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/nl.json b/homeassistant/components/deluge/translations/nl.json new file mode 100644 index 00000000000..6c824130708 --- /dev/null +++ b/homeassistant/components/deluge/translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Service is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam", + "web_port": "Webpoort (voor bezoekdienst)" + }, + "description": "Om deze integratie te kunnen gebruiken, moet u de volgende optie inschakelen in de deluge instellingen: Daemon > Afstandsbediening toestaan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/no.json b/homeassistant/components/deluge/translations/no.json new file mode 100644 index 00000000000..02d22dfb1a5 --- /dev/null +++ b/homeassistant/components/deluge/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "port": "Port", + "username": "Brukernavn", + "web_port": "Webport (for bes\u00f8kstjeneste)" + }, + "description": "For \u00e5 kunne bruke denne integrasjonen, m\u00e5 du aktivere f\u00f8lgende alternativ i deluge-innstillingene: Daemon > Tillat fjernkontroller" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/pl.json b/homeassistant/components/deluge/translations/pl.json new file mode 100644 index 00000000000..64269b9ab36 --- /dev/null +++ b/homeassistant/components/deluge/translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika", + "web_port": "Port WWW (do odwiedzania us\u0142ugi)" + }, + "description": "Aby m\u00f3c korzysta\u0107 z tej integracji, musisz w\u0142\u0105czy\u0107 nast\u0119puj\u0105c\u0105 opcj\u0119 w ustawieniach Deluge: Daemon > Zezw\u00f3l na zdalne sterowanie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/pt-BR.json b/homeassistant/components/deluge/translations/pt-BR.json new file mode 100644 index 00000000000..ba45a44117b --- /dev/null +++ b/homeassistant/components/deluge/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "host": "Nome do host", + "password": "Senha", + "port": "Porta", + "username": "Usu\u00e1rio", + "web_port": "Porta Web (para servi\u00e7o de visita)" + }, + "description": "Para poder usar essa integra\u00e7\u00e3o, voc\u00ea deve habilitar a seguinte op\u00e7\u00e3o nas configura\u00e7\u00f5es de Deluge: Daemon > Permitir controles remotos" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/ru.json b/homeassistant/components/deluge/translations/ru.json new file mode 100644 index 00000000000..04ab9df50e5 --- /dev/null +++ b/homeassistant/components/deluge/translations/ru.json @@ -0,0 +1,23 @@ +{ + "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." + }, + "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." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", + "web_port": "\u0412\u0435\u0431-\u043f\u043e\u0440\u0442 (\u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0441\u043b\u0443\u0436\u0431\u044b)" + }, + "description": "\u0414\u043b\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439, \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0439\u0442\u0435 \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 Deluge \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0435 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435: Daemon > Allow remote controls." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/tr.json b/homeassistant/components/deluge/translations/tr.json new file mode 100644 index 00000000000..0cf6d9ffe35 --- /dev/null +++ b/homeassistant/components/deluge/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "host": "Sunucu", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131", + "web_port": "Web ba\u011flant\u0131 noktas\u0131 (ziyaret hizmeti i\u00e7in)" + }, + "description": "Bu entegrasyonu kullanabilmek i\u00e7in, deluge ayarlar\u0131nda a\u015fa\u011f\u0131daki se\u00e7ene\u011fi etkinle\u015ftirmeniz gerekir: Daemon > Uzaktan kontrollere izin ver" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/zh-Hant.json b/homeassistant/components/deluge/translations/zh-Hant.json new file mode 100644 index 00000000000..c9ad139b2dc --- /dev/null +++ b/homeassistant/components/deluge/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "web_port": "Web \u901a\u8a0a\u57e0\uff08\u8a2a\u554f\u670d\u52d9\uff09" + }, + "description": "\u6b32\u4f7f\u7528\u6b64\u6574\u5408\uff0c\u5fc5\u9808\u5148\u5728 deluge \u8a2d\u5b9a\u4e2d\u958b\u555f\u4ee5\u4e0b\u9078\u9805\uff1aDaemon > \u5141\u8a31\u9060\u7aef\u5b58\u53d6" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/it.json b/homeassistant/components/demo/translations/it.json index 0fb3f52b916..1c94dea053f 100644 --- a/homeassistant/components/demo/translations/it.json +++ b/homeassistant/components/demo/translations/it.json @@ -3,8 +3,8 @@ "step": { "init": { "data": { - "one": "Pi\u00f9", - "other": "Altri" + "one": "Vuoto", + "other": "Vuoti" } }, "options_1": { diff --git a/homeassistant/components/denonavr/translations/bg.json b/homeassistant/components/denonavr/translations/bg.json index 6ec6215c6e1..4b4384d0bc9 100644 --- a/homeassistant/components/denonavr/translations/bg.json +++ b/homeassistant/components/denonavr/translations/bg.json @@ -8,6 +8,9 @@ "user": { "data": { "host": "IP \u0430\u0434\u0440\u0435\u0441" + }, + "data_description": { + "host": "\u041e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, \u0437\u0430 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435" } } } diff --git a/homeassistant/components/denonavr/translations/ca.json b/homeassistant/components/denonavr/translations/ca.json index f499c6a0a17..11065514ce4 100644 --- a/homeassistant/components/denonavr/translations/ca.json +++ b/homeassistant/components/denonavr/translations/ca.json @@ -27,6 +27,9 @@ "data": { "host": "Adre\u00e7a IP" }, + "data_description": { + "host": "Deixeu-ho en blanc per utilitzar descobriment autom\u00e0tic" + }, "description": "Connecta el teu receptor, si no es configura l'adre\u00e7a IP, s'utilitza el descobriment autom\u00e0tic", "title": "Receptors de xarxa AVR de Denon" } diff --git a/homeassistant/components/denonavr/translations/de.json b/homeassistant/components/denonavr/translations/de.json index 414bd798f62..1c9a1a8ec95 100644 --- a/homeassistant/components/denonavr/translations/de.json +++ b/homeassistant/components/denonavr/translations/de.json @@ -27,6 +27,9 @@ "data": { "host": "IP-Adresse" }, + "data_description": { + "host": "Leer lassen, um automatische Erkennung zu verwenden" + }, "description": "Verbinde dich mit deinem Receiver, wenn die IP-Adresse nicht eingestellt ist, wird die automatische Erkennung verwendet", "title": "Denon AVR-Netzwerk-Receiver" } diff --git a/homeassistant/components/denonavr/translations/el.json b/homeassistant/components/denonavr/translations/el.json index 180cacc2459..d176fd944d6 100644 --- a/homeassistant/components/denonavr/translations/el.json +++ b/homeassistant/components/denonavr/translations/el.json @@ -27,6 +27,9 @@ "data": { "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" }, + "data_description": { + "host": "\u0391\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7" + }, "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b4\u03ad\u03ba\u03c4\u03b7 \u03c3\u03b1\u03c2, \u03b5\u03ac\u03bd \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7.", "title": "\u0394\u03ad\u03ba\u03c4\u03b5\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 Denon AVR" } diff --git a/homeassistant/components/denonavr/translations/en.json b/homeassistant/components/denonavr/translations/en.json index 383ae5a502c..2d937856b1c 100644 --- a/homeassistant/components/denonavr/translations/en.json +++ b/homeassistant/components/denonavr/translations/en.json @@ -27,6 +27,9 @@ "data": { "host": "IP Address" }, + "data_description": { + "host": "Leave blank to use auto-discovery" + }, "description": "Connect to your receiver, if the IP address is not set, auto-discovery is used", "title": "Denon AVR Network Receivers" } diff --git a/homeassistant/components/denonavr/translations/et.json b/homeassistant/components/denonavr/translations/et.json index 5dc9f3cc771..f133aaf9dd7 100644 --- a/homeassistant/components/denonavr/translations/et.json +++ b/homeassistant/components/denonavr/translations/et.json @@ -27,6 +27,9 @@ "data": { "host": "IP aadress" }, + "data_description": { + "host": "Automaatse avastamise kasutamiseks j\u00e4ta v\u00e4li t\u00fchjaks." + }, "description": "Kui IP-aadressi pole m\u00e4\u00e4ratud, kasutatakse automaatset avastamist", "title": "" } diff --git a/homeassistant/components/denonavr/translations/fr.json b/homeassistant/components/denonavr/translations/fr.json index 474f02f5e21..27a72477164 100644 --- a/homeassistant/components/denonavr/translations/fr.json +++ b/homeassistant/components/denonavr/translations/fr.json @@ -27,6 +27,9 @@ "data": { "host": "Adresse IP" }, + "data_description": { + "host": "Laissez le champ vide pour utiliser la d\u00e9couverte automatique" + }, "description": "Connectez-vous \u00e0 votre r\u00e9cepteur, si l'adresse IP n'est pas d\u00e9finie, la d\u00e9tection automatique est utilis\u00e9e", "title": "R\u00e9cepteurs r\u00e9seaux Denon AVR" } diff --git a/homeassistant/components/denonavr/translations/hu.json b/homeassistant/components/denonavr/translations/hu.json index 874f190ff01..6891d18a9c4 100644 --- a/homeassistant/components/denonavr/translations/hu.json +++ b/homeassistant/components/denonavr/translations/hu.json @@ -2,9 +2,9 @@ "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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "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_manufacturer": "Nem egy Denon AVR h\u00e1l\u00f3zati vev\u0151, a felfedezett gy\u00e1rt\u00f3n\u00e9v nem megfelel\u0151", "not_denonavr_missing": "Nem Denon AVR h\u00e1l\u00f3zati vev\u0151, a felfedez\u00e9si inform\u00e1ci\u00f3k nem teljesek" }, "error": { @@ -27,6 +27,9 @@ "data": { "host": "IP c\u00edm" }, + "data_description": { + "host": "Az automatikus felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz hagyja \u00fcresen" + }, "description": "Csatlakozzon a vev\u0151h\u00f6z, ha az IP-c\u00edm nincs be\u00e1ll\u00edtva, az automatikus felder\u00edt\u00e9st haszn\u00e1lja", "title": "Denon AVR h\u00e1l\u00f3zati vev\u0151k\u00e9sz\u00fcl\u00e9kek" } diff --git a/homeassistant/components/denonavr/translations/id.json b/homeassistant/components/denonavr/translations/id.json index b2543d1d877..50a28ef5447 100644 --- a/homeassistant/components/denonavr/translations/id.json +++ b/homeassistant/components/denonavr/translations/id.json @@ -27,6 +27,9 @@ "data": { "host": "Alamat IP" }, + "data_description": { + "host": "Kosongkan untuk menggunakan penemuan otomatis" + }, "description": "Hubungkan ke Receiver Anda. Jika alamat IP tidak ditentukan, penemuan otomatis akan digunakan", "title": "Network Receiver Denon AVR" } diff --git a/homeassistant/components/denonavr/translations/it.json b/homeassistant/components/denonavr/translations/it.json index 76d0d627ca3..23fffd3ab44 100644 --- a/homeassistant/components/denonavr/translations/it.json +++ b/homeassistant/components/denonavr/translations/it.json @@ -27,6 +27,9 @@ "data": { "host": "Indirizzo IP" }, + "data_description": { + "host": "Lascia vuoto per usare il rilevamento automatico" + }, "description": "Collega il ricevitore, se l'indirizzo IP non \u00e8 impostato, sar\u00e0 utilizzato il rilevamento automatico", "title": "Ricevitori di rete Denon AVR" } diff --git a/homeassistant/components/denonavr/translations/ja.json b/homeassistant/components/denonavr/translations/ja.json index 1a5b41a3368..cd3198b7e10 100644 --- a/homeassistant/components/denonavr/translations/ja.json +++ b/homeassistant/components/denonavr/translations/ja.json @@ -27,6 +27,9 @@ "data": { "host": "IP\u30a2\u30c9\u30ec\u30b9" }, + "data_description": { + "host": "\u81ea\u52d5\u691c\u51fa\u3092\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u307e\u3059" + }, "description": "\u53d7\u4fe1\u6a5f\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002IP\u30a2\u30c9\u30ec\u30b9\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u81ea\u52d5\u691c\u51fa\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059", "title": "\u30c7\u30ce\u30f3(Denon)AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc" } diff --git a/homeassistant/components/denonavr/translations/nl.json b/homeassistant/components/denonavr/translations/nl.json index 47da10106f3..f03895452df 100644 --- a/homeassistant/components/denonavr/translations/nl.json +++ b/homeassistant/components/denonavr/translations/nl.json @@ -27,6 +27,9 @@ "data": { "host": "IP-adres" }, + "data_description": { + "host": "Leeg laten om auto-discovery te gebruiken" + }, "description": "Maak verbinding met uw ontvanger. Als het IP-adres niet is ingesteld, wordt automatische detectie gebruikt", "title": "Denon AVR Netwerk Ontvangers" } diff --git a/homeassistant/components/denonavr/translations/no.json b/homeassistant/components/denonavr/translations/no.json index 892cf8d5767..333c55a44f7 100644 --- a/homeassistant/components/denonavr/translations/no.json +++ b/homeassistant/components/denonavr/translations/no.json @@ -27,6 +27,9 @@ "data": { "host": "IP adresse" }, + "data_description": { + "host": "La feltet st\u00e5 tomt hvis du vil bruke automatisk s\u00f8k" + }, "description": "Koble til mottakeren, hvis IP-adressen ikke er angitt, brukes automatisk oppdagelse", "title": "Denon AVR Network Receivers" } diff --git a/homeassistant/components/denonavr/translations/pl.json b/homeassistant/components/denonavr/translations/pl.json index 6b09baf7d4c..054371c6ba1 100644 --- a/homeassistant/components/denonavr/translations/pl.json +++ b/homeassistant/components/denonavr/translations/pl.json @@ -27,6 +27,9 @@ "data": { "host": "Adres IP" }, + "data_description": { + "host": "Pozostaw puste, aby u\u017cy\u0107 automatycznego wykrywania" + }, "description": "\u0141\u0105czenie z urz\u0105dzeniem, je\u015bli adres IP nie jest zdefiniowany, u\u017cywane jest automatyczne wykrywanie.", "title": "Denon AVR" } diff --git a/homeassistant/components/denonavr/translations/pt-BR.json b/homeassistant/components/denonavr/translations/pt-BR.json index 084c7dd3c18..2a716dacdca 100644 --- a/homeassistant/components/denonavr/translations/pt-BR.json +++ b/homeassistant/components/denonavr/translations/pt-BR.json @@ -27,6 +27,9 @@ "data": { "host": "Endere\u00e7o IP" }, + "data_description": { + "host": "Deixe em branco para usar a descoberta autom\u00e1tica" + }, "description": "Conecte-se ao seu receptor, se o endere\u00e7o IP n\u00e3o estiver definido, a descoberta autom\u00e1tica ser\u00e1 usada", "title": "Receptores de rede Denon AVR" } diff --git a/homeassistant/components/denonavr/translations/ru.json b/homeassistant/components/denonavr/translations/ru.json index c1fb25a9889..1db49decaad 100644 --- a/homeassistant/components/denonavr/translations/ru.json +++ b/homeassistant/components/denonavr/translations/ru.json @@ -27,6 +27,9 @@ "data": { "host": "IP-\u0430\u0434\u0440\u0435\u0441" }, + "data_description": { + "host": "\u041e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435" + }, "description": "\u0415\u0441\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d, \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435", "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon" } diff --git a/homeassistant/components/denonavr/translations/tr.json b/homeassistant/components/denonavr/translations/tr.json index 2c32de293b8..046e535c621 100644 --- a/homeassistant/components/denonavr/translations/tr.json +++ b/homeassistant/components/denonavr/translations/tr.json @@ -27,6 +27,9 @@ "data": { "host": "IP Adresi" }, + "data_description": { + "host": "Otomatik bulmay\u0131 kullanmak i\u00e7in bo\u015f b\u0131rak\u0131n" + }, "description": "Al\u0131c\u0131n\u0131za ba\u011flan\u0131n, IP adresi ayarlanmazsa otomatik bulma kullan\u0131l\u0131r", "title": "Denon AVR A\u011f Al\u0131c\u0131lar\u0131" } diff --git a/homeassistant/components/denonavr/translations/zh-Hant.json b/homeassistant/components/denonavr/translations/zh-Hant.json index 073de46866d..1217a6d7b87 100644 --- a/homeassistant/components/denonavr/translations/zh-Hant.json +++ b/homeassistant/components/denonavr/translations/zh-Hant.json @@ -27,6 +27,9 @@ "data": { "host": "IP \u4f4d\u5740" }, + "data_description": { + "host": "\u4fdd\u6301\u7a7a\u767d\u4ee5\u4f7f\u7528\u81ea\u52d5\u641c\u7d22" + }, "description": "\u9023\u7dda\u81f3\u63a5\u6536\u5668\u3002\u5047\u5982\u672a\u8a2d\u5b9a IP \u4f4d\u5740\uff0c\u5c07\u4f7f\u7528\u81ea\u52d5\u63a2\u7d22\u3002", "title": "Denon AVR \u7db2\u8def\u63a5\u6536\u5668" } diff --git a/homeassistant/components/derivative/translations/bg.json b/homeassistant/components/derivative/translations/bg.json new file mode 100644 index 00000000000..14946d95fe0 --- /dev/null +++ b/homeassistant/components/derivative/translations/bg.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u0418\u043c\u0435" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "\u0418\u043c\u0435" + } + }, + "options": { + "data": { + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/ca.json b/homeassistant/components/derivative/translations/ca.json new file mode 100644 index 00000000000..3003b9349fd --- /dev/null +++ b/homeassistant/components/derivative/translations/ca.json @@ -0,0 +1,59 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nom", + "round": "Precisi\u00f3", + "source": "Sensor d'entrada", + "time_window": "Finestra de temps", + "unit_prefix": "Prefix m\u00e8tric", + "unit_time": "Unitat de temps" + }, + "data_description": { + "round": "Controla el nombre de d\u00edgits decimals a la sortida.", + "time_window": "Si s'estableix, el valor del sensor \u00e9s una mitjana m\u00f2bil ponderada en el temps de les derivades dins d'aquesta finestra.", + "unit_prefix": "La sortida s'escalar\u00e0 segons el prefix m\u00e8tric i la unitat de temps de la derivada seleccionats." + }, + "description": "Crea un sensor que estima la derivada d'un altre sensor.", + "title": "Afegeix sensor derivatiu" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Nom", + "round": "Precisi\u00f3", + "source": "Sensor d'entrada", + "time_window": "Finestra de temps", + "unit_prefix": "Prefix m\u00e8tric", + "unit_time": "Unitat de temps" + }, + "data_description": { + "round": "Controla el nombre de d\u00edgits decimals a la sortida.", + "time_window": "Si s'estableix, el valor del sensor \u00e9s una mitjana m\u00f2bil ponderada en el temps de les derivades dins d'aquesta finestra.", + "unit_prefix": "La sortida s'escalar\u00e0 segons el prefix m\u00e8tric i la unitat de temps de la derivada seleccionats." + } + }, + "options": { + "data": { + "name": "Nom", + "round": "Precisi\u00f3", + "source": "Sensor d'entrada", + "time_window": "Finestra de temps", + "unit_prefix": "Prefix m\u00e8tric", + "unit_time": "Unitat de temps" + }, + "data_description": { + "round": "Controla el nombre de d\u00edgits decimals a la sortida.", + "time_window": "Si s'estableix, el valor del sensor \u00e9s una mitjana m\u00f2bil ponderada en el temps de les derivades dins d'aquesta finestra.", + "unit_prefix": "La sortida s'escalar\u00e0 segons el prefix m\u00e8tric i la unitat de temps de la derivada seleccionats." + }, + "description": "Crea un sensor que estima la derivada d'un altre sensor." + } + } + }, + "title": "Sensor derivatiu" +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/cs.json b/homeassistant/components/derivative/translations/cs.json new file mode 100644 index 00000000000..ee8ee0e6d86 --- /dev/null +++ b/homeassistant/components/derivative/translations/cs.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data_description": { + "unit_prefix": "." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/de.json b/homeassistant/components/derivative/translations/de.json new file mode 100644 index 00000000000..4e1dbc1929c --- /dev/null +++ b/homeassistant/components/derivative/translations/de.json @@ -0,0 +1,59 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Name", + "round": "Genauigkeit", + "source": "Eingangssensor", + "time_window": "Zeitfenster", + "unit_prefix": "Metrisches Pr\u00e4fix", + "unit_time": "Zeiteinheit" + }, + "data_description": { + "round": "Steuert die Anzahl der Dezimalstellen in der Ausgabe.", + "time_window": "Wenn gesetzt, ist der Sensorwert ein zeitgewichteter gleitender Durchschnitt von Ableitungen innerhalb dieses Fensters.", + "unit_prefix": "Die Ausgabe wird gem\u00e4\u00df dem ausgew\u00e4hlten metrischen Pr\u00e4fix und der Zeiteinheit der Ableitung skaliert." + }, + "description": "Erstelle einen Sensor, der die Ableitung eines Sensors sch\u00e4tzt.", + "title": "Ableitungssensor hinzuf\u00fcgen" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Name", + "round": "Genauigkeit", + "source": "Eingangssensor", + "time_window": "Zeitfenster", + "unit_prefix": "Metrisches Pr\u00e4fix", + "unit_time": "Zeiteinheit" + }, + "data_description": { + "round": "Steuert die Anzahl der Dezimalstellen in der Ausgabe.", + "time_window": "Wenn gesetzt, ist der Sensorwert ein zeitgewichteter gleitender Durchschnitt von Ableitungen innerhalb dieses Fensters.", + "unit_prefix": "Die Ausgabe wird gem\u00e4\u00df dem ausgew\u00e4hlten metrischen Pr\u00e4fix und der Zeiteinheit der Ableitung skaliert.." + } + }, + "options": { + "data": { + "name": "Name", + "round": "Genauigkeit", + "source": "Eingangssensor", + "time_window": "Zeitfenster", + "unit_prefix": "Metrisches Pr\u00e4fix", + "unit_time": "Zeiteinheit" + }, + "data_description": { + "round": "Steuert die Anzahl der Dezimalstellen in der Ausgabe.", + "time_window": "Wenn gesetzt, ist der Sensorwert ein zeitgewichteter gleitender Durchschnitt von Ableitungen innerhalb dieses Fensters.", + "unit_prefix": "Die Ausgabe wird gem\u00e4\u00df dem ausgew\u00e4hlten metrischen Pr\u00e4fix und der Zeiteinheit der Ableitung skaliert.." + }, + "description": "Erstelle einen Sensor, der die Ableitung eines Sensors sch\u00e4tzt." + } + } + }, + "title": "Ableitungssensor" +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/el.json b/homeassistant/components/derivative/translations/el.json new file mode 100644 index 00000000000..a5a19efc1e5 --- /dev/null +++ b/homeassistant/components/derivative/translations/el.json @@ -0,0 +1,59 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "round": "\u0391\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1", + "source": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5", + "time_window": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03c0\u03b1\u03c1\u03ac\u03b8\u03c5\u03c1\u03bf", + "unit_prefix": "\u039c\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1", + "unit_time": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b1 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5" + }, + "data_description": { + "round": "\u0395\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf.", + "time_window": "\u0395\u03ac\u03bd \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af, \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03ac \u03c3\u03c4\u03b1\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03b9\u03bd\u03b7\u03c4\u03cc\u03c2 \u03bc\u03ad\u03c3\u03bf\u03c2 \u03cc\u03c1\u03bf\u03c2 \u03c4\u03c9\u03bd \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03c9\u03bd \u03b5\u03bd\u03c4\u03cc\u03c2 \u03b1\u03c5\u03c4\u03bf\u03cd \u03c4\u03bf\u03c5 \u03c0\u03b1\u03c1\u03b1\u03b8\u03cd\u03c1\u03bf\u03c5.", + "unit_prefix": "\u0397 \u03c0\u03b1\u03c1\u03ac\u03b3\u03c9\u03b3\u03bf\u03c2 \u03b8\u03b1 \u03ba\u03bb\u03b9\u03bc\u03b1\u03ba\u03c9\u03b8\u03b5\u03af \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5 \u03c4\u03b7\u03c2 \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03bf\u03c5." + }, + "description": "\u0397 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf.\n\u0395\u03ac\u03bd \u03c4\u03bf \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03c0\u03b1\u03c1\u03ac\u03b8\u03c5\u03c1\u03bf \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 0, \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03ac \u03c3\u03c4\u03b1\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03b9\u03bd\u03b7\u03c4\u03cc\u03c2 \u03bc\u03ad\u03c3\u03bf\u03c2 \u03cc\u03c1\u03bf\u03c2 \u03c4\u03c9\u03bd \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03c9\u03bd \u03b5\u03bd\u03c4\u03cc\u03c2 \u03c4\u03bf\u03c5 \u03c0\u03b1\u03c1\u03b1\u03b8\u03cd\u03c1\u03bf\u03c5.\n\u0397 \u03c0\u03b1\u03c1\u03ac\u03b3\u03c9\u03b3\u03bf\u03c2 \u03b8\u03b1 \u03ba\u03bb\u03b9\u03bc\u03b1\u03ba\u03ce\u03bd\u03b5\u03c4\u03b1\u03b9 \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5 \u03c4\u03b7\u03c2 \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03bf\u03c5.", + "title": "\u039d\u03ad\u03bf\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 Derivative" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "round": "\u0391\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1", + "source": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5", + "time_window": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03c0\u03b1\u03c1\u03ac\u03b8\u03c5\u03c1\u03bf", + "unit_prefix": "\u039c\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1", + "unit_time": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b1 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5" + }, + "data_description": { + "round": "\u0395\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf.", + "time_window": "\u0395\u03ac\u03bd \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af, \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03ac \u03c3\u03c4\u03b1\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03b9\u03bd\u03b7\u03c4\u03cc\u03c2 \u03bc\u03ad\u03c3\u03bf\u03c2 \u03cc\u03c1\u03bf\u03c2 \u03c4\u03c9\u03bd \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03c9\u03bd \u03b5\u03bd\u03c4\u03cc\u03c2 \u03b1\u03c5\u03c4\u03bf\u03cd \u03c4\u03bf\u03c5 \u03c0\u03b1\u03c1\u03b1\u03b8\u03cd\u03c1\u03bf\u03c5.", + "unit_prefix": "\u0397 \u03c0\u03b1\u03c1\u03ac\u03b3\u03c9\u03b3\u03bf\u03c2 \u03b8\u03b1 \u03ba\u03bb\u03b9\u03bc\u03b1\u03ba\u03c9\u03b8\u03b5\u03af \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5 \u03c4\u03b7\u03c2 \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03bf\u03c5." + } + }, + "options": { + "data": { + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "round": "\u0391\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1", + "source": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5", + "time_window": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03c0\u03b1\u03c1\u03ac\u03b8\u03c5\u03c1\u03bf", + "unit_prefix": "\u039c\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1", + "unit_time": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b1 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5" + }, + "data_description": { + "round": "\u0395\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf.", + "time_window": "\u0395\u03ac\u03bd \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af, \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03ac \u03c3\u03c4\u03b1\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03b9\u03bd\u03b7\u03c4\u03cc\u03c2 \u03bc\u03ad\u03c3\u03bf\u03c2 \u03cc\u03c1\u03bf\u03c2 \u03c4\u03c9\u03bd \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03c9\u03bd \u03b5\u03bd\u03c4\u03cc\u03c2 \u03b1\u03c5\u03c4\u03bf\u03cd \u03c4\u03bf\u03c5 \u03c0\u03b1\u03c1\u03b1\u03b8\u03cd\u03c1\u03bf\u03c5.", + "unit_prefix": "\u0397 \u03c0\u03b1\u03c1\u03ac\u03b3\u03c9\u03b3\u03bf\u03c2 \u03b8\u03b1 \u03ba\u03bb\u03b9\u03bc\u03b1\u03ba\u03c9\u03b8\u03b5\u03af \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5 \u03c4\u03b7\u03c2 \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03bf\u03c5." + }, + "description": "\u0397 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf.\n\u0395\u03ac\u03bd \u03c4\u03bf \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03c0\u03b1\u03c1\u03ac\u03b8\u03c5\u03c1\u03bf \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 0, \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03ac \u03c3\u03c4\u03b1\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03b9\u03bd\u03b7\u03c4\u03cc\u03c2 \u03bc\u03ad\u03c3\u03bf\u03c2 \u03cc\u03c1\u03bf\u03c2 \u03c4\u03c9\u03bd \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03c9\u03bd \u03b5\u03bd\u03c4\u03cc\u03c2 \u03c4\u03bf\u03c5 \u03c0\u03b1\u03c1\u03b1\u03b8\u03cd\u03c1\u03bf\u03c5.\n\u0397 \u03c0\u03b1\u03c1\u03ac\u03b3\u03c9\u03b3\u03bf\u03c2 \u03b8\u03b1 \u03ba\u03bb\u03b9\u03bc\u03b1\u03ba\u03ce\u03bd\u03b5\u03c4\u03b1\u03b9 \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5 \u03c4\u03b7\u03c2 \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03bf\u03c5." + } + } + }, + "title": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03c9\u03bd" +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/en.json b/homeassistant/components/derivative/translations/en.json index b91318b5237..884f1fa5244 100644 --- a/homeassistant/components/derivative/translations/en.json +++ b/homeassistant/components/derivative/translations/en.json @@ -36,6 +36,22 @@ "time_window": "If set, the sensor's value is a time weighted moving average of derivatives within this window.", "unit_prefix": "The output will be scaled according to the selected metric prefix and time unit of the derivative.." } + }, + "options": { + "data": { + "name": "Name", + "round": "Precision", + "source": "Input sensor", + "time_window": "Time window", + "unit_prefix": "Metric prefix", + "unit_time": "Time unit" + }, + "data_description": { + "round": "Controls the number of decimal digits in the output.", + "time_window": "If set, the sensor's value is a time weighted moving average of derivatives within this window.", + "unit_prefix": "The output will be scaled according to the selected metric prefix and time unit of the derivative.." + }, + "description": "Create a sensor that estimates the derivative of a sensor." } } }, diff --git a/homeassistant/components/derivative/translations/et.json b/homeassistant/components/derivative/translations/et.json new file mode 100644 index 00000000000..45c566fac9b --- /dev/null +++ b/homeassistant/components/derivative/translations/et.json @@ -0,0 +1,59 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nimi", + "round": "T\u00e4psus", + "source": "Sisendandur", + "time_window": "Ajavahemik", + "unit_prefix": "M\u00f5\u00f5diku eesliide", + "unit_time": "Aja\u00fchik" + }, + "data_description": { + "round": "K\u00fcmnendkohtade arv v\u00e4ljundis.", + "time_window": "Kui see on m\u00e4\u00e4ratud on anduri v\u00e4\u00e4rtus selle akna tuletisinstrumentide ajaga kaalutud liikuv keskmine.", + "unit_prefix": "Tuletis skaleeritakse vastavalt valitud meetrilisele eesliitele ja tuletise aja\u00fchikule." + }, + "description": "Loo andur mis hindab anduri tuletist.", + "title": "Lisa uus tuletisandur" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Nimi", + "round": "T\u00e4psus", + "source": "Sisendandur", + "time_window": "Ajavahemik", + "unit_prefix": "M\u00f5\u00f5diku eesliide", + "unit_time": "Aja\u00fchik" + }, + "data_description": { + "round": "K\u00fcmnendkohtade arv v\u00e4ljundis.", + "time_window": "Kui see on m\u00e4\u00e4ratud on anduri v\u00e4\u00e4rtus selle akna tuletisinstrumentide ajaga kaalutud liikuv keskmine.", + "unit_prefix": "Tuletis skaleeritakse vastavalt valitud meetrilisele eesliitele ja tuletise aja\u00fchikule." + } + }, + "options": { + "data": { + "name": "Nimi", + "round": "T\u00e4psus", + "source": "Sisendandur", + "time_window": "Ajavahemik", + "unit_prefix": "M\u00f5\u00f5diku eesliide", + "unit_time": "Aja\u00fchik" + }, + "data_description": { + "round": "K\u00fcmnendkohtade arv v\u00e4ljundis.", + "time_window": "Kui see on m\u00e4\u00e4ratud, on anduri v\u00e4\u00e4rtus selle akna tuletisinstrumentide ajaga kaalutud liikuv keskmine.", + "unit_prefix": "Tuletis skaleeritakse vastavalt valitud meetrilisele eesliitele ja tuletise aja\u00fchikule." + }, + "description": "T\u00e4psus reguleerib k\u00fcmnendkohtade arvu v\u00e4ljundis.\n Kui ajaaken ei ole 0, on anduri v\u00e4\u00e4rtuseks aknas olevate tuletisinstrumentide ajaga kaalutud liikuv keskmine.\n Tuletist skaleeritakse vastavalt valitud meetrilise prefiksile ja tuletise aja\u00fchikule." + } + } + }, + "title": "Tuletisandur" +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/fr.json b/homeassistant/components/derivative/translations/fr.json new file mode 100644 index 00000000000..d967a3abc89 --- /dev/null +++ b/homeassistant/components/derivative/translations/fr.json @@ -0,0 +1,59 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nom", + "round": "Pr\u00e9cision", + "source": "Capteur d'entr\u00e9e", + "time_window": "P\u00e9riode", + "unit_prefix": "Pr\u00e9fixe m\u00e9trique", + "unit_time": "Unit\u00e9 de temps" + }, + "data_description": { + "round": "Contr\u00f4le le nombre de chiffres d\u00e9cimaux dans la sortie.", + "time_window": "Si d\u00e9finie, la valeur du capteur est une moyenne mobile pond\u00e9r\u00e9e dans le temps des d\u00e9riv\u00e9es dans cette p\u00e9riode.", + "unit_prefix": "La sortie sera mise \u00e0 l'\u00e9chelle en fonction du pr\u00e9fixe m\u00e9trique et de l'unit\u00e9 de temps de la d\u00e9riv\u00e9e s\u00e9lectionn\u00e9s." + }, + "description": "Cr\u00e9ez un capteur calculant la d\u00e9riv\u00e9e d'un autre capteur.", + "title": "Ajouter un capteur de d\u00e9riv\u00e9e" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Nom", + "round": "Pr\u00e9cision", + "source": "Capteur d'entr\u00e9e", + "time_window": "P\u00e9riode", + "unit_prefix": "Pr\u00e9fixe m\u00e9trique", + "unit_time": "Unit\u00e9 de temps" + }, + "data_description": { + "round": "Contr\u00f4le le nombre de chiffres d\u00e9cimaux dans la sortie.", + "time_window": "Si d\u00e9finie, la valeur du capteur est une moyenne mobile pond\u00e9r\u00e9e dans le temps des d\u00e9riv\u00e9es dans cette p\u00e9riode.", + "unit_prefix": "La sortie sera mise \u00e0 l'\u00e9chelle en fonction du pr\u00e9fixe m\u00e9trique et de l'unit\u00e9 de temps de la d\u00e9riv\u00e9e s\u00e9lectionn\u00e9s.." + } + }, + "options": { + "data": { + "name": "Nom", + "round": "Pr\u00e9cision", + "source": "Capteur d'entr\u00e9e", + "time_window": "P\u00e9riode", + "unit_prefix": "Pr\u00e9fixe m\u00e9trique", + "unit_time": "Unit\u00e9 de temps" + }, + "data_description": { + "round": "Contr\u00f4le le nombre de chiffres d\u00e9cimaux dans la sortie.", + "time_window": "Si d\u00e9finie, la valeur du capteur est une moyenne mobile pond\u00e9r\u00e9e dans le temps des d\u00e9riv\u00e9es dans cette p\u00e9riode.", + "unit_prefix": "La sortie sera mise \u00e0 l'\u00e9chelle en fonction du pr\u00e9fixe m\u00e9trique et de l'unit\u00e9 de temps de la d\u00e9riv\u00e9e s\u00e9lectionn\u00e9s." + }, + "description": "Cr\u00e9ez un capteur calculant la d\u00e9riv\u00e9e d'un autre capteur." + } + } + }, + "title": "Capteur de d\u00e9riv\u00e9e" +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/he.json b/homeassistant/components/derivative/translations/he.json new file mode 100644 index 00000000000..317b836da6d --- /dev/null +++ b/homeassistant/components/derivative/translations/he.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "options": { + "data_description": { + "unit_prefix": "." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/hu.json b/homeassistant/components/derivative/translations/hu.json new file mode 100644 index 00000000000..e71175d2a32 --- /dev/null +++ b/homeassistant/components/derivative/translations/hu.json @@ -0,0 +1,59 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Elnevez\u00e9s", + "round": "Pontoss\u00e1g", + "source": "Forr\u00e1s \u00e9rz\u00e9kel\u0151", + "time_window": "Id\u0151ablak", + "unit_prefix": "M\u00e9rt\u00e9kegys\u00e9g el\u0151tag", + "unit_time": "Id\u0151egys\u00e9g" + }, + "data_description": { + "round": "Az eredm\u00e9ny tizedesjegyeinek sz\u00e1ma.", + "time_window": "Ha be van \u00e1ll\u00edtva, az \u00e9rz\u00e9kel\u0151 \u00e9rt\u00e9ke az ablakon bel\u00fcli sz\u00e1rmaz\u00e9kok id\u0151vel s\u00falyozott mozg\u00f3\u00e1tlaga.", + "unit_prefix": "A sz\u00e1rmaz\u00e9kos \u00e9rt\u00e9k a sz\u00e1rmaztatott term\u00e9k kiv\u00e1lasztott m\u00e9rt\u00e9kegys\u00e9g el\u0151tagja \u00e9s id\u0151egys\u00e9ge szerint lesz sk\u00e1l\u00e1zva." + }, + "description": "Hozzon l\u00e9tre egy \u00e9rz\u00e9kel\u0151t, amely megbecs\u00fcli a forr\u00e1s \u00e9rz\u00e9kel\u0151 sz\u00e1rmaz\u00e9k\u00e1t.", + "title": "\u00daj sz\u00e1rmaz\u00e9kos \u00e9rz\u00e9kel\u0151" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Elnevez\u00e9s", + "round": "Pontoss\u00e1g", + "source": "Forr\u00e1s \u00e9rz\u00e9kel\u0151", + "time_window": "Id\u0151ablak", + "unit_prefix": "M\u00e9rt\u00e9kegys\u00e9g el\u0151tag", + "unit_time": "Id\u0151egys\u00e9g" + }, + "data_description": { + "round": "Az eredm\u00e9ny tizedesjegyeinek sz\u00e1ma.", + "time_window": "Ha be van \u00e1ll\u00edtva, az \u00e9rz\u00e9kel\u0151 \u00e9rt\u00e9ke az ablakon bel\u00fcli sz\u00e1rmaz\u00e9kok id\u0151vel s\u00falyozott mozg\u00f3\u00e1tlaga.", + "unit_prefix": "A sz\u00e1rmaz\u00e9kos \u00e9rt\u00e9k a sz\u00e1rmaztatott term\u00e9k kiv\u00e1lasztott m\u00e9rt\u00e9kegys\u00e9g el\u0151tagja \u00e9s id\u0151egys\u00e9ge szerint lesz sk\u00e1l\u00e1zva.." + } + }, + "options": { + "data": { + "name": "Elnevez\u00e9s", + "round": "Pontoss\u00e1g", + "source": "Forr\u00e1s \u00e9rz\u00e9kel\u0151", + "time_window": "Id\u0151ablak", + "unit_prefix": "M\u00e9rt\u00e9kegys\u00e9g el\u0151tag", + "unit_time": "Id\u0151egys\u00e9g" + }, + "data_description": { + "round": "Az eredm\u00e9ny tizedesjegyeinek sz\u00e1ma.", + "time_window": "Ha be van \u00e1ll\u00edtva, az \u00e9rz\u00e9kel\u0151 \u00e9rt\u00e9ke az ablakon bel\u00fcli sz\u00e1rmaz\u00e9kok id\u0151vel s\u00falyozott mozg\u00f3\u00e1tlaga.", + "unit_prefix": "A sz\u00e1rmaz\u00e9kos \u00e9rt\u00e9k a sz\u00e1rmaztatott term\u00e9k kiv\u00e1lasztott m\u00e9rt\u00e9kegys\u00e9g el\u0151tagja \u00e9s id\u0151egys\u00e9ge szerint lesz sk\u00e1l\u00e1zva.." + }, + "description": "Hozzon l\u00e9tre egy \u00e9rz\u00e9kel\u0151t, amely megbecs\u00fcli a forr\u00e1s \u00e9rz\u00e9kel\u0151 sz\u00e1rmaz\u00e9k\u00e1t." + } + } + }, + "title": "Sz\u00e1rmaz\u00e9kos \u00e9rz\u00e9kel\u0151" +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/id.json b/homeassistant/components/derivative/translations/id.json new file mode 100644 index 00000000000..67d6752a182 --- /dev/null +++ b/homeassistant/components/derivative/translations/id.json @@ -0,0 +1,59 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nama", + "round": "Presisi", + "source": "Sensor input", + "time_window": "Jangka waktu", + "unit_prefix": "Prefiks metrik", + "unit_time": "Unit waktu" + }, + "data_description": { + "round": "Mengontrol jumlah digit desimal dalam output.", + "time_window": "Jika disetel, nilai sensor adalah rata-rata bergerak berbobot waktu dari turunan dalam jangka ini.", + "unit_prefix": "Output akan diskalakan sesuai dengan prefiks metrik yang dipilih dan unit waktu turunan." + }, + "description": "Buat sensor yang memperkirakan nilai turunan dari sebuah sensor.", + "title": "Tambahkan sensor Turunan" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Nama", + "round": "Presisi", + "source": "Sensor input", + "time_window": "Jangka waktu", + "unit_prefix": "Prefiks metrik", + "unit_time": "Unit waktu" + }, + "data_description": { + "round": "Mengontrol jumlah digit desimal dalam output.", + "time_window": "Jika disetel, nilai sensor adalah rata-rata bergerak berbobot waktu dari turunan dalam jangka ini.", + "unit_prefix": "Output akan diskalakan sesuai dengan prefiks metrik yang dipilih dan unit waktu turunan.." + } + }, + "options": { + "data": { + "name": "Nama", + "round": "Presisi", + "source": "Sensor input", + "time_window": "Jangka waktu", + "unit_prefix": "Prefiks metrik", + "unit_time": "Unit waktu" + }, + "data_description": { + "round": "Mengontrol jumlah digit desimal dalam output.", + "time_window": "Jika disetel, nilai sensor adalah rata-rata bergerak berbobot waktu dari turunan dalam jangka ini.", + "unit_prefix": "Output akan diskalakan sesuai dengan prefiks metrik yang dipilih dan unit waktu turunan.." + }, + "description": "Buat sensor yang memperkirakan nilai turunan dari sebuah sensor." + } + } + }, + "title": "Sensor Turunan" +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/it.json b/homeassistant/components/derivative/translations/it.json new file mode 100644 index 00000000000..ad888e13745 --- /dev/null +++ b/homeassistant/components/derivative/translations/it.json @@ -0,0 +1,59 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome", + "round": "Precisione", + "source": "Sensore di ingresso", + "time_window": "Finestra temporale", + "unit_prefix": "Prefisso metrico", + "unit_time": "Unit\u00e0 di tempo" + }, + "data_description": { + "round": "Controlla il numero di cifre decimali nell'uscita.", + "time_window": "Se impostato, il valore del sensore \u00e8 una media mobile delle derivate ponderata nel tempo all'interno di questa finestra.", + "unit_prefix": "L'output sar\u00e0 ridimensionato in base al prefisso metrico selezionato e all'unit\u00e0 di tempo della derivata." + }, + "description": "Crea un sensore che stimi la derivata di un sensore.", + "title": "Aggiungi sensore derivata" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Nome", + "round": "Precisione", + "source": "Sensore di ingresso", + "time_window": "Finestra temporale", + "unit_prefix": "Prefisso metrico", + "unit_time": "Unit\u00e0 di tempo" + }, + "data_description": { + "round": "Controlla il numero di cifre decimali nell'uscita.", + "time_window": "Se impostato, il valore del sensore \u00e8 una media mobile delle derivate ponderata nel tempo all'interno di questa finestra.", + "unit_prefix": "L'output sar\u00e0 ridimensionato in base al prefisso metrico selezionato e all'unit\u00e0 di tempo della derivata.." + } + }, + "options": { + "data": { + "name": "Nome", + "round": "Precisione", + "source": "Sensore di ingresso", + "time_window": "Finestra temporale", + "unit_prefix": "Prefisso metrico", + "unit_time": "Unit\u00e0 di tempo" + }, + "data_description": { + "round": "Controlla il numero di cifre decimali nell'uscita.", + "time_window": "Se impostato, il valore del sensore \u00e8 una media mobile delle derivate ponderata nel tempo all'interno di questa finestra.", + "unit_prefix": "L'output sar\u00e0 ridimensionato in base al prefisso metrico selezionato e all'unit\u00e0 di tempo della derivata.." + }, + "description": "Crea un sensore che stimi la derivata di un sensore." + } + } + }, + "title": "Sensore derivata" +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/ja.json b/homeassistant/components/derivative/translations/ja.json new file mode 100644 index 00000000000..10232f996d6 --- /dev/null +++ b/homeassistant/components/derivative/translations/ja.json @@ -0,0 +1,59 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u540d\u524d", + "round": "\u7cbe\u5ea6", + "source": "\u5165\u529b\u30bb\u30f3\u30b5\u30fc", + "time_window": "\u6642\u9593\u8ef8", + "unit_prefix": "\u30e1\u30c8\u30ea\u30c3\u30af\u63a5\u982d\u8f9e", + "unit_time": "\u6642\u9593\u5358\u4f4d" + }, + "data_description": { + "round": "\u51fa\u529b\u5024\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3002", + "time_window": "\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u308b\u5834\u5408\u3001\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u306f\u3053\u306e\u30a6\u30a3\u30f3\u30c9\u30a6\u5185\u306e\u5fae\u5206\u306e\u6642\u9593\u52a0\u91cd\u79fb\u52d5\u5e73\u5747\u3068\u306a\u308a\u307e\u3059\u3002", + "unit_prefix": "\u5fae\u5206\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u5fae\u5206\u306e\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002" + }, + "description": "\u7cbe\u5ea6\u306f\u3001\u51fa\u529b\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u6642\u9593\u7a93\u304c0\u3067\u306a\u3044\u5834\u5408\u3001\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u306f\u7a93\u5185\u306e\u5fae\u5206\u306e\u6642\u9593\u52a0\u91cd\u79fb\u52d5\u5e73\u5747\u306b\u306a\u308a\u307e\u3059\u3002\n\u5fae\u5206\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ea\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u5fae\u5206\u306e\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002", + "title": "\u65b0\u3057\u3044\u6d3e\u751f(Derivative)\u30bb\u30f3\u30b5\u30fc" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "\u540d\u524d", + "round": "\u7cbe\u5ea6", + "source": "\u5165\u529b\u30bb\u30f3\u30b5\u30fc", + "time_window": "\u6642\u9593\u8ef8", + "unit_prefix": "\u30e1\u30c8\u30ea\u30c3\u30af\u63a5\u982d\u8f9e", + "unit_time": "\u6642\u9593\u5358\u4f4d" + }, + "data_description": { + "round": "\u51fa\u529b\u5024\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3002", + "time_window": "\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u308b\u5834\u5408\u3001\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u306f\u3053\u306e\u30a6\u30a3\u30f3\u30c9\u30a6\u5185\u306e\u5fae\u5206\u306e\u6642\u9593\u52a0\u91cd\u79fb\u52d5\u5e73\u5747\u3068\u306a\u308a\u307e\u3059\u3002", + "unit_prefix": "\u5fae\u5206\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u5fae\u5206\u306e\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002." + } + }, + "options": { + "data": { + "name": "\u540d\u524d", + "round": "\u7cbe\u5ea6", + "source": "\u5165\u529b\u30bb\u30f3\u30b5\u30fc", + "time_window": "\u6642\u9593\u8ef8", + "unit_prefix": "\u30e1\u30c8\u30ea\u30c3\u30af\u63a5\u982d\u8f9e", + "unit_time": "\u6642\u9593\u5358\u4f4d" + }, + "data_description": { + "round": "\u51fa\u529b\u5024\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3002", + "time_window": "\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u308b\u5834\u5408\u3001\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u306f\u3053\u306e\u30a6\u30a3\u30f3\u30c9\u30a6\u5185\u306e\u5fae\u5206\u306e\u6642\u9593\u52a0\u91cd\u79fb\u52d5\u5e73\u5747\u3068\u306a\u308a\u307e\u3059\u3002", + "unit_prefix": "\u5fae\u5206\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u5fae\u5206\u306e\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002." + }, + "description": "\u7cbe\u5ea6\u306f\u3001\u51fa\u529b\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u6642\u9593\u7a93\u304c0\u3067\u306a\u3044\u5834\u5408\u3001\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u306f\u7a93\u5185\u306e\u5fae\u5206\u306e\u6642\u9593\u52a0\u91cd\u79fb\u52d5\u5e73\u5747\u306b\u306a\u308a\u307e\u3059\u3002\n\u5fae\u5206\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ea\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u5fae\u5206\u306e\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002" + } + } + }, + "title": "\u6d3e\u751f(Derivative)\u30bb\u30f3\u30b5\u30fc" +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/nl.json b/homeassistant/components/derivative/translations/nl.json new file mode 100644 index 00000000000..8b7cf5a9402 --- /dev/null +++ b/homeassistant/components/derivative/translations/nl.json @@ -0,0 +1,59 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Naam", + "round": "Precisie", + "source": "Invoer sensor", + "time_window": "Tijdsvenster", + "unit_prefix": "Metrisch voorvoegsel", + "unit_time": "Tijdseenheid" + }, + "data_description": { + "round": "Regelt het aantal decimale cijfers in de uitvoer.", + "time_window": "Indien ingesteld, is de waarde van de sensor een tijdgewogen voortschrijdend gemiddelde van de afgeleiden binnen dit venster.", + "unit_prefix": "De uitvoer wordt geschaald volgens het geselecteerde metrische voorvoegsel en de tijdseenheid van de afgeleide" + }, + "description": "Maak een sensor die de afgeleide van een sensor schat.", + "title": "Voeg afgeleide sensor toe" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Naam", + "round": "Precisie", + "source": "Invoer sensor", + "time_window": "Tijdsvenster", + "unit_prefix": "Metrisch voorvoegsel", + "unit_time": "Tijdseenheid" + }, + "data_description": { + "round": "Regelt het aantal decimale cijfers in de uitvoer.", + "time_window": "Indien ingesteld, is de waarde van de sensor een tijdgewogen voortschrijdend gemiddelde van de afgeleiden binnen dit venster.", + "unit_prefix": "De uitvoer wordt geschaald volgens het geselecteerde metrische voorvoegsel en de tijdseenheid van de afgeleide." + } + }, + "options": { + "data": { + "name": "Naam", + "round": "Precisie", + "source": "Invoer sensor", + "time_window": "Tijdsvenster", + "unit_prefix": "Metrisch voorvoegsel", + "unit_time": "Tijdseenheid" + }, + "data_description": { + "round": "Regelt het aantal decimale cijfers in de uitvoer.", + "time_window": "Indien ingesteld, is de waarde van de sensor een tijdgewogen voortschrijdend gemiddelde van de afgeleiden binnen dit venster.", + "unit_prefix": "De uitvoer wordt geschaald volgens het geselecteerde metrische voorvoegsel en de tijdseenheid van de afgeleide." + }, + "description": "Maak een sensor die de afgeleide van een sensor schat." + } + } + }, + "title": "Derivatieve sensor" +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/no.json b/homeassistant/components/derivative/translations/no.json new file mode 100644 index 00000000000..735f1fae6ae --- /dev/null +++ b/homeassistant/components/derivative/translations/no.json @@ -0,0 +1,59 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Navn", + "round": "Presisjon", + "source": "Inngangssensor", + "time_window": "Tidsvindu", + "unit_prefix": "Metrisk prefiks", + "unit_time": "Tidsenhet" + }, + "data_description": { + "round": "Styrer antall desimaler i utdataene.", + "time_window": "Hvis den er angitt, er sensorens verdi et tidsvektet glidende gjennomsnitt av derivater i dette vinduet.", + "unit_prefix": "Utdataene skaleres i henhold til det valgte metriske prefikset og tidsenheten til den deriverte." + }, + "description": "Lag en sensor som estimerer den deriverte av en sensor.", + "title": "Legg til derivatsensor" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Navn", + "round": "Presisjon", + "source": "Inngangssensor", + "time_window": "Tidsvindu", + "unit_prefix": "Metrisk prefiks", + "unit_time": "Tidsenhet" + }, + "data_description": { + "round": "Styrer antall desimaler i utdataene.", + "time_window": "Hvis den er angitt, er sensorens verdi et tidsvektet glidende gjennomsnitt av derivater i dette vinduet.", + "unit_prefix": "Utdataene skaleres i henhold til det valgte metriske prefikset og tidsenheten til den deriverte.." + } + }, + "options": { + "data": { + "name": "Navn", + "round": "Presisjon", + "source": "Inngangssensor", + "time_window": "Tidsvindu", + "unit_prefix": "Metrisk prefiks", + "unit_time": "Tidsenhet" + }, + "data_description": { + "round": "Styrer antall desimaler i utdataene.", + "time_window": "Hvis den er angitt, er sensorens verdi et tidsvektet glidende gjennomsnitt av derivater i dette vinduet.", + "unit_prefix": "Utdataene skaleres i henhold til det valgte metriske prefikset og tidsenheten til den deriverte.." + }, + "description": "Lag en sensor som estimerer den deriverte av en sensor." + } + } + }, + "title": "Avledet sensor" +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/pl.json b/homeassistant/components/derivative/translations/pl.json new file mode 100644 index 00000000000..041d52ffeff --- /dev/null +++ b/homeassistant/components/derivative/translations/pl.json @@ -0,0 +1,59 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nazwa", + "round": "Precyzja", + "source": "Sensor wej\u015bciowy", + "time_window": "Okno czasowe", + "unit_prefix": "Prefiks metryczny", + "unit_time": "Jednostka czasu" + }, + "data_description": { + "round": "Kontroluje liczb\u0119 cyfr dziesi\u0119tnych w danych wyj\u015bciowych.", + "time_window": "Je\u015bli jest ustawiona, warto\u015b\u0107 sensora jest wa\u017con\u0105 w czasie \u015bredni\u0105 ruchom\u0105 pochodnych w tym oknie.", + "unit_prefix": "Wynik b\u0119dzie skalowany zgodnie z wybranym prefiksem metrycznym i jednostk\u0105 czasu pochodnej." + }, + "description": "Tworzy sensor, kt\u00f3ry szacuje pochodn\u0105 sensora.", + "title": "Dodaj sensor pochodnej" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Nazwa", + "round": "Precyzja", + "source": "Sensor wej\u015bciowy", + "time_window": "Okno czasowe", + "unit_prefix": "Prefiks metryczny", + "unit_time": "Jednostka czasu" + }, + "data_description": { + "round": "Kontroluje liczb\u0119 cyfr dziesi\u0119tnych w danych wyj\u015bciowych.", + "time_window": "Je\u015bli jest ustawiona, warto\u015b\u0107 sensora jest wa\u017con\u0105 w czasie \u015bredni\u0105 ruchom\u0105 pochodnych w tym oknie.", + "unit_prefix": "Wynik b\u0119dzie skalowany zgodnie z wybranym prefiksem metrycznym i jednostk\u0105 czasu pochodnej." + } + }, + "options": { + "data": { + "name": "Nazwa", + "round": "Precyzja", + "source": "Sensor wej\u015bciowy", + "time_window": "Okno czasowe", + "unit_prefix": "Prefiks metryczny", + "unit_time": "Jednostka czasu" + }, + "data_description": { + "round": "Kontroluje liczb\u0119 cyfr dziesi\u0119tnych w danych wyj\u015bciowych.", + "time_window": "Je\u015bli jest ustawiona, warto\u015b\u0107 sensora jest wa\u017con\u0105 w czasie \u015bredni\u0105 ruchom\u0105 pochodnych w tym oknie.", + "unit_prefix": "Wynik b\u0119dzie skalowany zgodnie z wybranym prefiksem metrycznym i jednostk\u0105 czasu pochodnej." + }, + "description": "Tworzy sensor, kt\u00f3ry szacuje pochodn\u0105 sensora." + } + } + }, + "title": "Sensor pochodnej" +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/pt-BR.json b/homeassistant/components/derivative/translations/pt-BR.json new file mode 100644 index 00000000000..4d29a3970ee --- /dev/null +++ b/homeassistant/components/derivative/translations/pt-BR.json @@ -0,0 +1,59 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome", + "round": "Precis\u00e3o", + "source": "Sensor de entrada", + "time_window": "Janela do tempo", + "unit_prefix": "Prefixo da m\u00e9trica", + "unit_time": "Unidade de tempo" + }, + "data_description": { + "round": "Controla o n\u00famero de d\u00edgitos decimais na sa\u00edda.", + "time_window": "Se definido, o valor do sensor \u00e9 uma m\u00e9dia m\u00f3vel ponderada no tempo das derivadas dentro desta janela.", + "unit_prefix": "A sa\u00edda ser\u00e1 dimensionada de acordo com o prefixo m\u00e9trico selecionado e a unidade de tempo da derivada." + }, + "description": "Crie um sensor que estime a derivada de um sensor.", + "title": "Adicionar sensor Derivative" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Nome", + "round": "Precis\u00e3o", + "source": "Sensor de entrada", + "time_window": "Janela do tempo", + "unit_prefix": "Prefixo da m\u00e9trica", + "unit_time": "Unidade de tempo" + }, + "data_description": { + "round": "Controla o n\u00famero de d\u00edgitos decimais na sa\u00edda.", + "time_window": "Se definido, o valor do sensor \u00e9 uma m\u00e9dia m\u00f3vel ponderada no tempo das derivadas dentro desta janela.", + "unit_prefix": "A sa\u00edda ser\u00e1 dimensionada de acordo com o prefixo m\u00e9trico selecionado e a unidade de tempo da derivada.." + } + }, + "options": { + "data": { + "name": "Nome", + "round": "Precis\u00e3o", + "source": "Sensor de entrada", + "time_window": "Janela do tempo", + "unit_prefix": "Prefixo da m\u00e9trica", + "unit_time": "Unidade de tempo" + }, + "data_description": { + "round": "Controla o n\u00famero de d\u00edgitos decimais na sa\u00edda.", + "time_window": "Se definido, o valor do sensor \u00e9 uma m\u00e9dia m\u00f3vel ponderada no tempo das derivadas dentro desta janela.", + "unit_prefix": "A sa\u00edda ser\u00e1 dimensionada de acordo com o prefixo m\u00e9trico selecionado e a unidade de tempo da derivada.." + }, + "description": "Crie um sensor que estime a derivada de um sensor." + } + } + }, + "title": "Sensor derivativo" +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/ru.json b/homeassistant/components/derivative/translations/ru.json new file mode 100644 index 00000000000..d7c62f070e1 --- /dev/null +++ b/homeassistant/components/derivative/translations/ru.json @@ -0,0 +1,53 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "round": "\u041e\u043a\u0440\u0443\u0433\u043b\u0435\u043d\u0438\u0435", + "source": "\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440", + "time_window": "\u0412\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0435 \u043e\u043a\u043d\u043e", + "unit_prefix": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441 \u043c\u0435\u0442\u0440\u0438\u043a\u0438", + "unit_time": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438" + }, + "data_description": { + "round": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439." + }, + "title": "\u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u0430\u044f" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "round": "\u041e\u043a\u0440\u0443\u0433\u043b\u0435\u043d\u0438\u0435", + "source": "\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440", + "time_window": "\u0412\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0435 \u043e\u043a\u043d\u043e", + "unit_prefix": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441 \u043c\u0435\u0442\u0440\u0438\u043a\u0438", + "unit_time": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438" + }, + "data_description": { + "round": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439.", + "unit_prefix": "." + } + }, + "options": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "round": "\u041e\u043a\u0440\u0443\u0433\u043b\u0435\u043d\u0438\u0435", + "source": "\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440", + "time_window": "\u0412\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0435 \u043e\u043a\u043d\u043e", + "unit_prefix": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441 \u043c\u0435\u0442\u0440\u0438\u043a\u0438", + "unit_time": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438" + }, + "data_description": { + "round": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439.", + "unit_prefix": "." + } + } + } + }, + "title": "\u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u0430\u044f" +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/sv.json b/homeassistant/components/derivative/translations/sv.json new file mode 100644 index 00000000000..66dcd34b1d7 --- /dev/null +++ b/homeassistant/components/derivative/translations/sv.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "init": { + "data": { + "name": "Namn", + "round": "Precision" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/tr.json b/homeassistant/components/derivative/translations/tr.json new file mode 100644 index 00000000000..68016c74372 --- /dev/null +++ b/homeassistant/components/derivative/translations/tr.json @@ -0,0 +1,59 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Ad", + "round": "Hassas", + "source": "Giri\u015f sens\u00f6r\u00fc", + "time_window": "Zaman penceresi", + "unit_prefix": "Metrik \u00f6neki", + "unit_time": "Zaman birimi" + }, + "data_description": { + "round": "\u00c7\u0131kt\u0131daki ondal\u0131k basamak say\u0131s\u0131n\u0131 kontrol eder.", + "time_window": "Ayarlan\u0131rsa, sens\u00f6r\u00fcn de\u011feri, bu penceredeki t\u00fcrevlerin zaman a\u011f\u0131rl\u0131kl\u0131 hareketli ortalamas\u0131d\u0131r.", + "unit_prefix": "\u00c7\u0131kt\u0131, t\u00fcrevin se\u00e7ilen metrik \u00f6nekine ve zaman birimine g\u00f6re \u00f6l\u00e7eklenecektir." + }, + "description": "Bir sens\u00f6r\u00fcn t\u00fcrevini tahmin eden bir sens\u00f6r olu\u015fturun.", + "title": "T\u00fcrev sens\u00f6r\u00fc ekle" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Ad", + "round": "Hassas", + "source": "Giri\u015f sens\u00f6r\u00fc", + "time_window": "Zaman penceresi", + "unit_prefix": "Metrik \u00f6neki", + "unit_time": "Zaman birimi" + }, + "data_description": { + "round": "\u00c7\u0131kt\u0131daki ondal\u0131k basamak say\u0131s\u0131n\u0131 kontrol eder.", + "time_window": "Ayarlan\u0131rsa, sens\u00f6r\u00fcn de\u011feri, bu penceredeki t\u00fcrevlerin zaman a\u011f\u0131rl\u0131kl\u0131 hareketli ortalamas\u0131d\u0131r.", + "unit_prefix": "\u00c7\u0131kt\u0131, t\u00fcrevin se\u00e7ilen metrik \u00f6nekine ve zaman birimine g\u00f6re \u00f6l\u00e7eklenecektir.." + } + }, + "options": { + "data": { + "name": "Ad", + "round": "Hassas", + "source": "Giri\u015f sens\u00f6r\u00fc", + "time_window": "Zaman penceresi", + "unit_prefix": "Metrik \u00f6neki", + "unit_time": "Zaman birimi" + }, + "data_description": { + "round": "\u00c7\u0131kt\u0131daki ondal\u0131k basamak say\u0131s\u0131n\u0131 kontrol eder.", + "time_window": "Ayarlan\u0131rsa, sens\u00f6r\u00fcn de\u011feri, bu penceredeki t\u00fcrevlerin zaman a\u011f\u0131rl\u0131kl\u0131 hareketli ortalamas\u0131d\u0131r.", + "unit_prefix": "\u00c7\u0131kt\u0131, t\u00fcrevin se\u00e7ilen metrik \u00f6nekine ve zaman birimine g\u00f6re \u00f6l\u00e7eklenecektir.." + }, + "description": "Bir sens\u00f6r\u00fcn t\u00fcrevini tahmin eden bir sens\u00f6r olu\u015fturun." + } + } + }, + "title": "T\u00fcrev sens\u00f6r\u00fc" +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/zh-Hans.json b/homeassistant/components/derivative/translations/zh-Hans.json new file mode 100644 index 00000000000..689f057dec0 --- /dev/null +++ b/homeassistant/components/derivative/translations/zh-Hans.json @@ -0,0 +1,59 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u540d\u79f0", + "round": "\u7cbe\u5ea6", + "source": "\u8f93\u5165\u4f20\u611f\u5668", + "time_window": "\u65f6\u95f4\u7a97\u53e3", + "unit_prefix": "\u5355\u4f4d\u524d\u7f00", + "unit_time": "\u65f6\u95f4\u5355\u4f4d" + }, + "data_description": { + "round": "\u63a7\u5236\u8f93\u51fa\u7684\u5c0f\u6570\u4f4d\u6570\u3002", + "time_window": "\u5982\u679c\u8bbe\u7f6e\uff0c\u4f20\u611f\u5668\u5c06\u8f93\u51fa\u6b64\u65f6\u95f4\u7a97\u53e3\u5185\u7684\u53d8\u5316\u7387\u6309\u7167\u201c\u65f6\u95f4\u52a0\u6743\u79fb\u52a8\u5e73\u5747\u6cd5\u201d\u5904\u7406\u540e\u7684\u503c\u3002", + "unit_prefix": "\u8f93\u51fa\u503c\u5c06\u6839\u636e\u6240\u9009\u7684\u5355\u4f4d\u524d\u7f00\u548c\u65f6\u95f4\u5355\u4f4d\u8fdb\u884c\u7f29\u653e\u3002" + }, + "description": "\u521b\u5efa\u4f20\u611f\u5668\u6765\u4f30\u7b97\u53e6\u4e00\u4e2a\u4f20\u611f\u5668\u7684\u53d8\u5316\u7387\u3002", + "title": "\u6dfb\u52a0\u53d8\u5316\u7387\uff08\u5bfc\u6570\uff09\u4f20\u611f\u5668" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "\u540d\u79f0", + "round": "\u7cbe\u5ea6", + "source": "\u8f93\u5165\u4f20\u611f\u5668", + "time_window": "\u65f6\u95f4\u7a97\u53e3", + "unit_prefix": "\u5355\u4f4d\u524d\u7f00", + "unit_time": "\u65f6\u95f4\u5355\u4f4d" + }, + "data_description": { + "round": "\u63a7\u5236\u8f93\u51fa\u7684\u5c0f\u6570\u4f4d\u6570\u3002", + "time_window": "\u5982\u679c\u8bbe\u7f6e\uff0c\u4f20\u611f\u5668\u5c06\u8f93\u51fa\u6b64\u65f6\u95f4\u7a97\u53e3\u5185\u7684\u53d8\u5316\u7387\u6309\u7167\u201c\u65f6\u95f4\u52a0\u6743\u79fb\u52a8\u5e73\u5747\u6cd5\u201d\u5904\u7406\u540e\u7684\u503c\u3002", + "unit_prefix": "\u8f93\u51fa\u503c\u5c06\u6839\u636e\u6240\u9009\u7684\u5355\u4f4d\u524d\u7f00\u548c\u65f6\u95f4\u5355\u4f4d\u8fdb\u884c\u7f29\u653e\u3002" + } + }, + "options": { + "data": { + "name": "\u540d\u79f0", + "round": "\u7cbe\u5ea6", + "source": "\u8f93\u5165\u4f20\u611f\u5668", + "time_window": "\u65f6\u95f4\u7a97\u53e3", + "unit_prefix": "\u5355\u4f4d\u524d\u7f00", + "unit_time": "\u65f6\u95f4\u5355\u4f4d" + }, + "data_description": { + "round": "\u63a7\u5236\u8f93\u51fa\u7684\u5c0f\u6570\u4f4d\u6570\u3002", + "time_window": "\u5982\u679c\u8bbe\u7f6e\uff0c\u4f20\u611f\u5668\u5c06\u8f93\u51fa\u6b64\u65f6\u95f4\u7a97\u53e3\u5185\u7684\u53d8\u5316\u7387\u6309\u7167\u201c\u65f6\u95f4\u52a0\u6743\u79fb\u52a8\u5e73\u5747\u6cd5\u201d\u5904\u7406\u540e\u7684\u503c\u3002", + "unit_prefix": "\u8f93\u51fa\u503c\u5c06\u6839\u636e\u6240\u9009\u7684\u5355\u4f4d\u524d\u7f00\u548c\u65f6\u95f4\u5355\u4f4d\u8fdb\u884c\u7f29\u653e\u3002" + }, + "description": "\u521b\u5efa\u4f20\u611f\u5668\u6765\u4f30\u7b97\u53e6\u4e00\u4e2a\u4f20\u611f\u5668\u7684\u53d8\u5316\u7387\u3002" + } + } + }, + "title": "\u53d8\u5316\u7387\u4f20\u611f\u5668" +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/zh-Hant.json b/homeassistant/components/derivative/translations/zh-Hant.json new file mode 100644 index 00000000000..3c100df8034 --- /dev/null +++ b/homeassistant/components/derivative/translations/zh-Hant.json @@ -0,0 +1,59 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u540d\u7a31", + "round": "\u6e96\u78ba\u5ea6", + "source": "\u8f38\u5165\u611f\u6e2c\u5668", + "time_window": "\u6642\u9593\u8996\u7a97", + "unit_prefix": "\u516c\u5236\u524d\u7db4", + "unit_time": "\u6642\u9593\u55ae\u4f4d" + }, + "data_description": { + "round": "\u63a7\u5236\u8f38\u51fa\u4e2d\u7684\u5c0f\u6578\u4f4d\u6578\u3002", + "time_window": "\u8a2d\u5b9a\u5f8c\u3001\u611f\u6e2c\u5668\u6578\u503c\u5c07\u70ba\u8996\u7a97\u5167\u5c0e\u6578\u7684\u6642\u9593\u52a0\u6b0a\u52a0\u6b0a\u79fb\u52d5\u5e73\u5747\u503c\u3002", + "unit_prefix": "\u8f38\u51fa\u5c07\u53d7\u6240\u9078\u64c7\u516c\u5236\u524d\u7db4\u53ca\u5c0e\u6578\u6642\u9593\u55ae\u4f4d\u800c\u8b8a\u5316\u3002" + }, + "description": "\u65b0\u589e\u9810\u4f30\u611f\u6e2c\u5668\u5c0e\u6578\u4e4b\u611f\u6e2c\u5668\u3002", + "title": "\u65b0\u589e\u5c0e\u6578\u611f\u6e2c\u5668" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "\u540d\u7a31", + "round": "\u6e96\u78ba\u5ea6", + "source": "\u8f38\u5165\u611f\u6e2c\u5668", + "time_window": "\u6642\u9593\u8996\u7a97", + "unit_prefix": "\u516c\u5236\u524d\u7db4", + "unit_time": "\u6642\u9593\u55ae\u4f4d" + }, + "data_description": { + "round": "\u63a7\u5236\u8f38\u51fa\u4e2d\u7684\u5c0f\u6578\u4f4d\u6578\u3002", + "time_window": "\u8a2d\u5b9a\u5f8c\u3001\u611f\u6e2c\u5668\u6578\u503c\u5c07\u70ba\u8996\u7a97\u5167\u5c0e\u6578\u7684\u6642\u9593\u52a0\u6b0a\u52a0\u6b0a\u79fb\u52d5\u5e73\u5747\u503c\u3002", + "unit_prefix": "\u8f38\u51fa\u5c07\u53d7\u6240\u9078\u64c7\u516c\u5236\u524d\u7db4\u53ca\u5c0e\u6578\u6642\u9593\u55ae\u4f4d\u800c\u8b8a\u5316\u3002." + } + }, + "options": { + "data": { + "name": "\u540d\u7a31", + "round": "\u6e96\u78ba\u5ea6", + "source": "\u8f38\u5165\u611f\u6e2c\u5668", + "time_window": "\u6642\u9593\u8996\u7a97", + "unit_prefix": "\u516c\u5236\u524d\u7db4", + "unit_time": "\u6642\u9593\u55ae\u4f4d" + }, + "data_description": { + "round": "\u63a7\u5236\u8f38\u51fa\u4e2d\u7684\u5c0f\u6578\u4f4d\u6578\u3002", + "time_window": "\u8a2d\u5b9a\u5f8c\u3001\u611f\u6e2c\u5668\u6578\u503c\u5c07\u70ba\u8996\u7a97\u5167\u5c0e\u6578\u7684\u6642\u9593\u52a0\u6b0a\u52a0\u6b0a\u79fb\u52d5\u5e73\u5747\u503c\u3002", + "unit_prefix": "\u8f38\u51fa\u5c07\u53d7\u6240\u9078\u64c7\u516c\u5236\u524d\u7db4\u53ca\u5c0e\u6578\u6642\u9593\u55ae\u4f4d\u800c\u8b8a\u5316\u3002" + }, + "description": "\u65b0\u589e\u9810\u4f30\u611f\u6e2c\u5668\u5c0e\u6578\u4e4b\u611f\u6e2c\u5668\u3002" + } + } + }, + "title": "\u5c0e\u6578\u611f\u6e2c\u5668" +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/it.json b/homeassistant/components/directv/translations/it.json index 9fb0932c342..3653e3876c2 100644 --- a/homeassistant/components/directv/translations/it.json +++ b/homeassistant/components/directv/translations/it.json @@ -11,8 +11,8 @@ "step": { "ssdp_confirm": { "data": { - "one": "Pi\u00f9", - "other": "Altri" + "one": "Vuoto", + "other": "Vuoti" }, "description": "Vuoi impostare {name} ?" }, diff --git a/homeassistant/components/discord/translations/bg.json b/homeassistant/components/discord/translations/bg.json new file mode 100644 index 00000000000..00faba1155f --- /dev/null +++ b/homeassistant/components/discord/translations/bg.json @@ -0,0 +1,13 @@ +{ + "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", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "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/discord/translations/ca.json b/homeassistant/components/discord/translations/ca.json new file mode 100644 index 00000000000..bd87ba1f8dc --- /dev/null +++ b/homeassistant/components/discord/translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "Token d'API" + }, + "description": "Consulta la documentaci\u00f3 per obtenir la teva clau de bot de Discord. \n\n{url}" + }, + "user": { + "data": { + "api_token": "Token d'API" + }, + "description": "Consulta la documentaci\u00f3 per obtenir la teva clau de bot de Discord. \n\n{url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/translations/de.json b/homeassistant/components/discord/translations/de.json new file mode 100644 index 00000000000..e4f745573c0 --- /dev/null +++ b/homeassistant/components/discord/translations/de.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "API-Token" + }, + "description": "Weitere Informationen zum Abrufen deines Discord-Bot-Schl\u00fcssels findest du in der Dokumentation. \n\n {url}" + }, + "user": { + "data": { + "api_token": "API-Token" + }, + "description": "Weitere Informationen zum Abrufen deines Discord-Bot-Schl\u00fcssels findest du in der Dokumentation. \n\n {url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/translations/el.json b/homeassistant/components/discord/translations/el.json new file mode 100644 index 00000000000..a090d61d3c4 --- /dev/null +++ b/homeassistant/components/discord/translations/el.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API" + }, + "description": "\u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03bb\u03ae\u03c8\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd Discord bot. \n\n {url}" + }, + "user": { + "data": { + "api_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API" + }, + "description": "\u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03bb\u03ae\u03c8\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd Discord bot. \n\n {url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/translations/en.json b/homeassistant/components/discord/translations/en.json index 77e16e9312d..ee72905b4d3 100644 --- a/homeassistant/components/discord/translations/en.json +++ b/homeassistant/components/discord/translations/en.json @@ -10,13 +10,13 @@ "unknown": "Unexpected error" }, "step": { - "user": { + "reauth_confirm": { "data": { "api_token": "API Token" }, "description": "Refer to the documentation on getting your Discord bot key.\n\n{url}" }, - "reauth_confirm": { + "user": { "data": { "api_token": "API Token" }, diff --git a/homeassistant/components/discord/translations/et.json b/homeassistant/components/discord/translations/et.json new file mode 100644 index 00000000000..ba69a715841 --- /dev/null +++ b/homeassistant/components/discord/translations/et.json @@ -0,0 +1,27 @@ +{ + "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" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "API v\u00f5ti" + }, + "description": "Vaata oma Discordi roboti v\u00f5tme hankimise dokumentatsiooni. \n\n {url}" + }, + "user": { + "data": { + "api_token": "API v\u00f5ti" + }, + "description": "Vaata oma Discordi roboti v\u00f5tme hankimise dokumentatsiooni. \n\n {url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/translations/fr.json b/homeassistant/components/discord/translations/fr.json new file mode 100644 index 00000000000..3ffac86c505 --- /dev/null +++ b/homeassistant/components/discord/translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "Jeton d'API" + }, + "description": "Consultez la documentation pour obtenir votre cl\u00e9 de bot Discord.\n\n{url}" + }, + "user": { + "data": { + "api_token": "Jeton d'API" + }, + "description": "Consultez la documentation pour obtenir votre cl\u00e9 de bot Discord.\n\n{url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/translations/he.json b/homeassistant/components/discord/translations/he.json new file mode 100644 index 00000000000..d23c46a99bc --- /dev/null +++ b/homeassistant/components/discord/translations/he.json @@ -0,0 +1,25 @@ +{ + "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" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" + } + }, + "user": { + "data": { + "api_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/translations/hu.json b/homeassistant/components/discord/translations/hu.json new file mode 100644 index 00000000000..bcbd8748808 --- /dev/null +++ b/homeassistant/components/discord/translations/hu.json @@ -0,0 +1,27 @@ +{ + "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" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "API Token" + }, + "description": "Tekintse meg a Discord botkulcs beszerz\u00e9s\u00e9nek dokument\u00e1ci\u00f3j\u00e1t. \n\n {url}" + }, + "user": { + "data": { + "api_token": "API Token" + }, + "description": "Tekintse meg a Discord botkulcs beszerz\u00e9s\u00e9nek dokument\u00e1ci\u00f3j\u00e1t. \n\n {url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/translations/id.json b/homeassistant/components/discord/translations/id.json new file mode 100644 index 00000000000..2e5d7d41f23 --- /dev/null +++ b/homeassistant/components/discord/translations/id.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan 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": { + "api_token": "Token API" + }, + "description": "Lihat dokumentasi tentang mendapatkan kunci bot Discord Anda. \n\n {url}" + }, + "user": { + "data": { + "api_token": "Token API" + }, + "description": "Lihat dokumentasi tentang mendapatkan kunci bot Discord Anda. \n\n {url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/translations/it.json b/homeassistant/components/discord/translations/it.json new file mode 100644 index 00000000000..c8be2cb0ed1 --- /dev/null +++ b/homeassistant/components/discord/translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "Token API" + }, + "description": "Fai riferimento alla documentazione su come ottenere la chiave del bot Discord. \n\n {url}" + }, + "user": { + "data": { + "api_token": "Token API" + }, + "description": "Fai riferimento alla documentazione su come ottenere la chiave del bot Discord. \n\n {url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/translations/ja.json b/homeassistant/components/discord/translations/ja.json new file mode 100644 index 00000000000..3d96ea99890 --- /dev/null +++ b/homeassistant/components/discord/translations/ja.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "API\u30c8\u30fc\u30af\u30f3" + }, + "description": "Discord\u30dc\u30c3\u30c8\u30ad\u30fc\u306e\u53d6\u5f97\u306b\u3064\u3044\u3066\u306f\u3001\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n{url}" + }, + "user": { + "data": { + "api_token": "API\u30c8\u30fc\u30af\u30f3" + }, + "description": "Discord\u30dc\u30c3\u30c8\u30ad\u30fc\u306e\u53d6\u5f97\u306b\u3064\u3044\u3066\u306f\u3001\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n{url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/translations/nl.json b/homeassistant/components/discord/translations/nl.json new file mode 100644 index 00000000000..55fb894031d --- /dev/null +++ b/homeassistant/components/discord/translations/nl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Service is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "API-token" + }, + "description": "Raadpleeg de documentatie over het verkrijgen van uw Discord bot key.\n\n{url}" + }, + "user": { + "data": { + "api_token": "API-token" + }, + "description": "Raadpleeg de documentatie over het verkrijgen van uw Discord bot key.\n\n{url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/translations/no.json b/homeassistant/components/discord/translations/no.json new file mode 100644 index 00000000000..e8a36e5c794 --- /dev/null +++ b/homeassistant/components/discord/translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "API-token" + }, + "description": "Se dokumentasjonen for \u00e5 f\u00e5 din Discord-botn\u00f8kkel. \n\n {url}" + }, + "user": { + "data": { + "api_token": "API-token" + }, + "description": "Se dokumentasjonen for \u00e5 f\u00e5 din Discord-botn\u00f8kkel. \n\n {url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/translations/pl.json b/homeassistant/components/discord/translations/pl.json new file mode 100644 index 00000000000..96fb45d1bc5 --- /dev/null +++ b/homeassistant/components/discord/translations/pl.json @@ -0,0 +1,27 @@ +{ + "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" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "Token API" + }, + "description": "Zapoznaj si\u0119 z dokumentacj\u0105 dotycz\u0105c\u0105 uzyskiwania klucza bota Discord. \n\n{url}" + }, + "user": { + "data": { + "api_token": "Token API" + }, + "description": "Zapoznaj si\u0119 z dokumentacj\u0105 dotycz\u0105c\u0105 uzyskiwania klucza bota Discord. \n\n{url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/translations/pt-BR.json b/homeassistant/components/discord/translations/pt-BR.json new file mode 100644 index 00000000000..cedd60f6fb6 --- /dev/null +++ b/homeassistant/components/discord/translations/pt-BR.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "Token da API" + }, + "description": "Consulte a documenta\u00e7\u00e3o sobre como obter sua chave de bot do Discord. \n\n {url}" + }, + "user": { + "data": { + "api_token": "Token da API" + }, + "description": "Consulte a documenta\u00e7\u00e3o sobre como obter sua chave de bot do Discord. \n\n {url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/translations/ru.json b/homeassistant/components/discord/translations/ru.json new file mode 100644 index 00000000000..19be65ea874 --- /dev/null +++ b/homeassistant/components/discord/translations/ru.json @@ -0,0 +1,27 @@ +{ + "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." + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "\u0422\u043e\u043a\u0435\u043d API" + }, + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439 \u043f\u043e \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044e \u043a\u043b\u044e\u0447\u0430 \u0431\u043e\u0442\u0430 Discord. \n\n{url}" + }, + "user": { + "data": { + "api_token": "\u0422\u043e\u043a\u0435\u043d API" + }, + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439 \u043f\u043e \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044e \u043a\u043b\u044e\u0447\u0430 \u0431\u043e\u0442\u0430 Discord. \n\n{url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/translations/tr.json b/homeassistant/components/discord/translations/tr.json new file mode 100644 index 00000000000..2bce7e54907 --- /dev/null +++ b/homeassistant/components/discord/translations/tr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "API Anahtar\u0131" + }, + "description": "Discord bot anahtar\u0131n\u0131z\u0131 almayla ilgili belgelere bak\u0131n. \n\n {url}" + }, + "user": { + "data": { + "api_token": "API Anahtar\u0131" + }, + "description": "Discord bot anahtar\u0131n\u0131z\u0131 almayla ilgili belgelere bak\u0131n. \n\n {url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/translations/zh-Hant.json b/homeassistant/components/discord/translations/zh-Hant.json new file mode 100644 index 00000000000..5f0f4500903 --- /dev/null +++ b/homeassistant/components/discord/translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "API \u6b0a\u6756" + }, + "description": "\u8acb\u53c3\u8003\u6587\u4ef6\u4ee5\u53d6\u5f97 Discord \u6a5f\u5668\u4eba\u91d1\u9470\u3002\n\n{url}" + }, + "user": { + "data": { + "api_token": "API \u6b0a\u6756" + }, + "description": "\u8acb\u53c3\u8003\u6587\u4ef6\u4ee5\u53d6\u5f97 Discord \u6a5f\u5668\u4eba\u91d1\u9470\u3002\n\n{url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/hu.json b/homeassistant/components/dlna_dmr/translations/hu.json index 6596e71e05e..5e2ba148602 100644 --- a/homeassistant/components/dlna_dmr/translations/hu.json +++ b/homeassistant/components/dlna_dmr/translations/hu.json @@ -21,7 +21,7 @@ "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" + "description": "Kapcsolja be az eszk\u00f6zt, majd folytassa a migr\u00e1ci\u00f3t" }, "manual": { "data": { @@ -35,7 +35,7 @@ "host": "C\u00edm", "url": "URL" }, - "description": "V\u00e1lassz egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt vagy adj meg egy URL-t", + "description": "V\u00e1lassza ki a konfigur\u00e1lni k\u00edv\u00e1nt eszk\u00f6zt, vagy hagyja \u00fcresen az URL-c\u00edm k\u00e9zi megad\u00e1s\u00e1hoz", "title": "DLNA digit\u00e1lis m\u00e9dia renderel\u0151" } } diff --git a/homeassistant/components/dlna_dms/translations/cs.json b/homeassistant/components/dlna_dms/translations/cs.json new file mode 100644 index 00000000000..cfcad870880 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/cs.json @@ -0,0 +1,20 @@ +{ + "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" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Chcete za\u010d\u00edt nastavovat?" + }, + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/hu.json b/homeassistant/components/dlna_dms/translations/hu.json index 8c645d42aa8..74ba47053a4 100644 --- a/homeassistant/components/dlna_dms/translations/hu.json +++ b/homeassistant/components/dlna_dms/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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "bad_ssdp": "Az SSDP-adatok hi\u00e1nyosak", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "not_dms": "A m\u00e9diaszerver eszk\u00f6z nem t\u00e1mogatott" diff --git a/homeassistant/components/doorbird/translations/ca.json b/homeassistant/components/doorbird/translations/ca.json index 880360234b1..0049709caa3 100644 --- a/homeassistant/components/doorbird/translations/ca.json +++ b/homeassistant/components/doorbird/translations/ca.json @@ -29,6 +29,9 @@ "data": { "events": "Llista d'esdeveniments separats per comes." }, + "data_description": { + "events": "Afegeix el/s noms del/s esdeveniment/s que vulguis seguir separats per comes. Despr\u00e9s d'introduir-los, utilitza l'aplicaci\u00f3 de DoorBird per assignar-los a un esdeveniment espec\u00edfic.\n\nExemple: algu_ha_premut_el_boto, moviment" + }, "description": "Afegeix el/s noms del/s esdeveniment/s que vulguis seguir separats per comes. Despr\u00e9s d'introduir-los, utilitza l'aplicaci\u00f3 de DoorBird per assignar-los a un esdeveniment espec\u00edfic. Consulta la documentaci\u00f3 a https://www.home-assistant.io/integrations/doorbird/#events.\nExemple: algu_ha_premut_el_boto, moviment_detectat" } } diff --git a/homeassistant/components/doorbird/translations/de.json b/homeassistant/components/doorbird/translations/de.json index 3f025e67386..7a60a3bf4ae 100644 --- a/homeassistant/components/doorbird/translations/de.json +++ b/homeassistant/components/doorbird/translations/de.json @@ -29,6 +29,9 @@ "data": { "events": "Durch Kommas getrennte Liste von Ereignissen." }, + "data_description": { + "events": "F\u00fcge f\u00fcr jedes Ereignis, das du verfolgen m\u00f6chtest, einen durch Komma getrennten Ereignisnamen hinzu. Nachdem du sie hier eingegeben hast, verwende die DoorBird-App, um sie einem bestimmten Ereignis zuzuordnen.\n\nBeispiel: jemand_drueckte_den_Knopf, bewegung" + }, "description": "F\u00fcge f\u00fcr jedes Ereignis, das du verfolgen m\u00f6chtest, einen durch Kommas getrennten Ereignisnamen hinzu. Nachdem du sie hier eingegeben hast, verwende die DoorBird-App, um sie einem bestimmten Ereignis zuzuweisen. Weitere Informationen findest du in der Dokumentation unter https://www.home-assistant.io/integrations/doorbird/#events. Beispiel: jemand_hat_den_knopf_gedr\u00fcckt, bewegung" } } diff --git a/homeassistant/components/doorbird/translations/el.json b/homeassistant/components/doorbird/translations/el.json index ffbf523e7d8..da06dc3e0be 100644 --- a/homeassistant/components/doorbird/translations/el.json +++ b/homeassistant/components/doorbird/translations/el.json @@ -29,6 +29,9 @@ "data": { "events": "\u039b\u03af\u03c3\u03c4\u03b1 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03c9\u03bd \u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1." }, + "data_description": { + "events": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03bf\u03c2 \u03c0\u03bf\u03c5 \u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1 \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03b5\u03c4\u03b5. \u0391\u03c6\u03bf\u03cd \u03c4\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03b5\u03b4\u03ce, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae DoorBird \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c4\u03b1 \u03b1\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03b9\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03bf \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd.\n\n\u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: somebody_pressed_the_button, motion" + }, "description": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03bf\u03c2 \u03c0\u03bf\u03c5 \u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1 \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03b5\u03c4\u03b5. \u0391\u03c6\u03bf\u03cd \u03c4\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03b5\u03b4\u03ce, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae DoorBird \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c4\u03b1 \u03b1\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03b9\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03bf \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://www.home-assistant.io/integrations/doorbird/#events. \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: somebody_pressed_the_button, motion" } } diff --git a/homeassistant/components/doorbird/translations/en.json b/homeassistant/components/doorbird/translations/en.json index db1cea2d73f..ee005dfbfed 100644 --- a/homeassistant/components/doorbird/translations/en.json +++ b/homeassistant/components/doorbird/translations/en.json @@ -29,6 +29,9 @@ "data": { "events": "Comma separated list of events." }, + "data_description": { + "events": "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event.\n\nExample: somebody_pressed_the_button, motion" + }, "description": "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event. See the documentation at https://www.home-assistant.io/integrations/doorbird/#events. Example: somebody_pressed_the_button, motion" } } diff --git a/homeassistant/components/doorbird/translations/et.json b/homeassistant/components/doorbird/translations/et.json index 08b163c0ca2..f37d5d0244b 100644 --- a/homeassistant/components/doorbird/translations/et.json +++ b/homeassistant/components/doorbird/translations/et.json @@ -29,6 +29,9 @@ "data": { "events": "Komadega eraldatud s\u00fcndmuste loend." }, + "data_description": { + "events": "Lisa iga s\u00fcndmuse jaoks, mida soovid j\u00e4lgida, komaga eraldatud s\u00fcndmuse nimi. P\u00e4rast nende sisestamist siia kasuta DoorBirdi rakendust, et m\u00e4\u00e4rata need konkreetsele s\u00fcndmusele.\n\nN\u00e4ide: somebody_pressed_the_button, liikumine" + }, "description": "Lisa komaga eraldatud s\u00fcndmuse nimi igale j\u00e4lgitavale s\u00fcndmusele. P\u00e4rast nende sisestamist kasuta rakendust DoorBird, et siduda need konkreetse s\u00fcndmusega. Vaata dokumentatsiooni aadressil https://www.home-assistant.io/integrations/doorbird/#events. N\u00e4ide: somebody_pressed_the_button, motion" } } diff --git a/homeassistant/components/doorbird/translations/fr.json b/homeassistant/components/doorbird/translations/fr.json index 40569250e5f..b4819897366 100644 --- a/homeassistant/components/doorbird/translations/fr.json +++ b/homeassistant/components/doorbird/translations/fr.json @@ -29,6 +29,9 @@ "data": { "events": "Liste d'\u00e9v\u00e9nements s\u00e9par\u00e9s par des virgules." }, + "data_description": { + "events": "Ajoutez les noms des \u00e9v\u00e9nements que vous souhaitez suivre, s\u00e9par\u00e9s par des virgules. Apr\u00e8s les avoir saisis ici, utilisez l'application DoorBird pour les affecter \u00e0 un \u00e9v\u00e9nement sp\u00e9cifique.\n\nExemple\u00a0: somebody_pressed_the_button, motion" + }, "description": "Ajoutez un nom d'\u00e9v\u00e9nement s\u00e9par\u00e9 par des virgules pour chaque \u00e9v\u00e9nement que vous souhaitez suivre. Apr\u00e8s les avoir saisis ici, utilisez l'application DoorBird pour les affecter \u00e0 un \u00e9v\u00e9nement sp\u00e9cifique. Consultez la documentation sur https://www.home-assistant.io/integrations/doorbird/#events. Exemple: somebody_pressed_the_button, motion" } } diff --git a/homeassistant/components/doorbird/translations/hu.json b/homeassistant/components/doorbird/translations/hu.json index 48a124b4f17..51bbb0864dd 100644 --- a/homeassistant/components/doorbird/translations/hu.json +++ b/homeassistant/components/doorbird/translations/hu.json @@ -29,6 +29,9 @@ "data": { "events": "Vessz\u0151vel elv\u00e1lasztott esem\u00e9nyek list\u00e1ja." }, + "data_description": { + "events": "Adjon hozz\u00e1 egy vessz\u0151vel elv\u00e1lasztott esem\u00e9nynevet minden egyes esem\u00e9nyhez, amelyet nyomon k\u00edv\u00e1n k\u00f6vetni. Miut\u00e1n be\u00edrta \u0151ket ide, a DoorBird alkalmaz\u00e1ssal rendelje \u0151ket egy adott esem\u00e9nyhez.\n\nP\u00e9lda: valaki_megnyomta_a_gombot, motion" + }, "description": "Adjon hozz\u00e1 vessz\u0151vel elv\u00e1lasztott esem\u00e9nynevet minden k\u00f6vetni k\u00edv\u00e1nt esem\u00e9nyhez. Miut\u00e1n itt megadta \u0151ket, haszn\u00e1lja a DoorBird alkalmaz\u00e1st, hogy hozz\u00e1rendelje \u0151ket egy adott esem\u00e9nyhez. Tekintse meg a dokument\u00e1ci\u00f3t a https://www.home-assistant.io/integrations/doorbird/#events c\u00edmen. P\u00e9lda: valaki_pr\u00e9selt_gomb, mozg\u00e1s" } } diff --git a/homeassistant/components/doorbird/translations/id.json b/homeassistant/components/doorbird/translations/id.json index 60348ec26a1..c5d5733457d 100644 --- a/homeassistant/components/doorbird/translations/id.json +++ b/homeassistant/components/doorbird/translations/id.json @@ -29,6 +29,9 @@ "data": { "events": "Daftar event yang dipisahkan koma." }, + "data_description": { + "events": "Tambahkan nama event yang dipisahkan koma untuk setiap event yang ingin dilacak. Setelah memasukkannya di sini, gunakan aplikasi DoorBird untuk menetapkannya ke event tertentu.\n\nMisalnya: ada_yang_menekan_tombol, gerakan" + }, "description": "Tambahkan nama event yang dipisahkan koma untuk setiap event yang ingin dilacak. Setelah memasukkannya di sini, gunakan aplikasi DoorBird untuk menetapkannya ke event tertentu. Baca dokumentasi di https://www.home-assistant.io/integrations/doorbird/#events. Contoh: somebody_pressed_the_button, motion" } } diff --git a/homeassistant/components/doorbird/translations/it.json b/homeassistant/components/doorbird/translations/it.json index 83f1ab9c5eb..9864f100954 100644 --- a/homeassistant/components/doorbird/translations/it.json +++ b/homeassistant/components/doorbird/translations/it.json @@ -29,6 +29,9 @@ "data": { "events": "Elenco di eventi separati da virgole." }, + "data_description": { + "events": "Aggiungi un nome evento separato da virgole per ogni evento che desideri monitorare. Dopo averli inseriti qui, usa l'applicazione DoorBird per assegnarli a un evento specifico. \n\nEsempio: somebody_pressed_the_button, movimento" + }, "description": "Aggiungere un nome di evento separato da virgola per ogni evento che desideri monitorare. Dopo averli inseriti qui, usa l'applicazione DoorBird per assegnarli a un evento specifico. Consulta la documentazione su https://www.home-assistant.io/integrations/doorbird/#events. Esempio: qualcuno_premuto_il_pulsante, movimento" } } diff --git a/homeassistant/components/doorbird/translations/ja.json b/homeassistant/components/doorbird/translations/ja.json index 179edc8943c..55363310ec5 100644 --- a/homeassistant/components/doorbird/translations/ja.json +++ b/homeassistant/components/doorbird/translations/ja.json @@ -29,6 +29,9 @@ "data": { "events": "\u30a4\u30d9\u30f3\u30c8\u306e\u30b3\u30f3\u30de\u533a\u5207\u308a\u30ea\u30b9\u30c8\u3002" }, + "data_description": { + "events": "\u8ffd\u8de1\u3059\u308b\u30a4\u30d9\u30f3\u30c8\u3054\u3068\u306b\u30b3\u30f3\u30de\u533a\u5207\u308a\u306e\u30a4\u30d9\u30f3\u30c8\u540d\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002\u3053\u3053\u306b\u5165\u529b\u5f8c\u3001DoorBird\u30a2\u30d7\u30ea\u3092\u4f7f\u7528\u3057\u3066\u7279\u5b9a\u306e\u30a4\u30d9\u30f3\u30c8\u306b\u5272\u308a\u5f53\u3066\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002 \n\n\u4f8b: somebody_pressed_the_button, motion" + }, "description": "\u8ffd\u8de1\u3059\u308b\u30a4\u30d9\u30f3\u30c8\u3054\u3068\u306b\u3001\u30b3\u30f3\u30de\u533a\u5207\u308a\u3067\u30a4\u30d9\u30f3\u30c8\u540d\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002\u3053\u3053\u306b\u5165\u529b\u3057\u305f\u5f8c\u3001DoorBird\u30a2\u30d7\u30ea\u3092\u4f7f\u7528\u3057\u3066\u7279\u5b9a\u306e\u30a4\u30d9\u30f3\u30c8\u306b\u5272\u308a\u5f53\u3066\u307e\u3059\u3002https://www.home-assistant.io/integrations/doorbird/#events. \u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u4f8b: somebody_pressed_the_button, motion" } } diff --git a/homeassistant/components/doorbird/translations/nl.json b/homeassistant/components/doorbird/translations/nl.json index db32d96d831..83e47ca4c5e 100644 --- a/homeassistant/components/doorbird/translations/nl.json +++ b/homeassistant/components/doorbird/translations/nl.json @@ -29,6 +29,9 @@ "data": { "events": "Door komma's gescheiden lijst met gebeurtenissen." }, + "data_description": { + "events": "Voeg een door komma's gescheiden evenementnaam toe voor elk evenement dat u wilt volgen. Nadat u ze hier hebt ingevoerd, gebruikt u de DoorBird app om ze aan een specifieke gebeurtenis toe te wijzen.\n\nVoorbeeld: iemand_drukt_op_de_knop, beweging" + }, "description": "Voeg een door komma's gescheiden evenementnaam toe voor elk evenement dat u wilt volgen. Nadat je ze hier hebt ingevoerd, gebruik je de DoorBird-app om ze toe te wijzen aan een specifiek evenement. Zie de documentatie op https://www.home-assistant.io/integrations/doorbird/#events. Voorbeeld: iemand_drukte_knop, beweging" } } diff --git a/homeassistant/components/doorbird/translations/no.json b/homeassistant/components/doorbird/translations/no.json index 356c86a8b54..a30fc93013a 100644 --- a/homeassistant/components/doorbird/translations/no.json +++ b/homeassistant/components/doorbird/translations/no.json @@ -29,6 +29,9 @@ "data": { "events": "Kommaseparert liste over hendelser." }, + "data_description": { + "events": "Legg til et navn p\u00e5 en kommadelt hendelse for hver hendelse du vil spore. N\u00e5r du har skrevet dem inn her, bruker du DoorBird-appen til \u00e5 tilordne dem til en bestemt hendelse.\n\nEksempel: somebody_pressed_the_button, bevegelse" + }, "description": "Legg til et kommaseparert hendelsesnavn for hvert arrangement du \u00f8nsker \u00e5 spore. Etter \u00e5 ha skrevet dem inn her, bruker du DoorBird-appen til \u00e5 tilordne dem til en bestemt hendelse. Se dokumentasjonen p\u00e5 https://www.home-assistant.io/integrations/doorbird/#events. Eksempel: noen_trykket_knappen, bevegelse" } } diff --git a/homeassistant/components/doorbird/translations/pl.json b/homeassistant/components/doorbird/translations/pl.json index f55d2d406f2..85a7a8765a7 100644 --- a/homeassistant/components/doorbird/translations/pl.json +++ b/homeassistant/components/doorbird/translations/pl.json @@ -29,6 +29,9 @@ "data": { "events": "Lista wydarze\u0144 oddzielona przecinkami" }, + "data_description": { + "events": "Dodaj nazw\u0119 oddzielon\u0105 przecinkami dla ka\u017cdego wydarzenia, kt\u00f3re chcesz \u015bledzi\u0107. Po wprowadzeniu ich tutaj u\u017cyj aplikacji DoorBird, aby przypisa\u0107 je do konkretnego wydarzenia. \n\nPrzyk\u0142ad: kto\u015b_nacisn\u0105\u0142_przycisk, ruch" + }, "description": "Dodaj nazwy wydarze\u0144 oddzielonych przecinkami, kt\u00f3re chcesz \u015bledzi\u0107. Po wprowadzeniu ich tutaj u\u017cyj aplikacji DoorBird, aby przypisa\u0107 je do okre\u015blonych zdarze\u0144. Zapoznaj si\u0119 z dokumentacj\u0105 na stronie https://www.home-assistant.io/integrations/doorbird/#events.\nPrzyk\u0142ad: nacisniecie_przycisku, ruch" } } diff --git a/homeassistant/components/doorbird/translations/pt-BR.json b/homeassistant/components/doorbird/translations/pt-BR.json index 3f2c479df8f..07170c086b7 100644 --- a/homeassistant/components/doorbird/translations/pt-BR.json +++ b/homeassistant/components/doorbird/translations/pt-BR.json @@ -29,6 +29,9 @@ "data": { "events": "Lista de eventos separados por v\u00edrgulas." }, + "data_description": { + "events": "Adicione um nome de evento separado por v\u00edrgula para cada evento que voc\u00ea deseja rastrear. Depois de inseri-los aqui, use o aplicativo DoorBird para atribu\u00ed-los a um evento espec\u00edfico. \n\n Exemplo: someone_pressed_the_button, movimento" + }, "description": "Adicione um nome de evento separado por v\u00edrgula para cada evento que voc\u00ea deseja rastrear. Depois de inseri-los aqui, use o aplicativo DoorBird para atribu\u00ed-los a um evento espec\u00edfico. Consulte a documenta\u00e7\u00e3o em https://www.home-assistant.io/integrations/doorbird/#events. Exemplo: alguem_pressionou_o_botao movimento" } } diff --git a/homeassistant/components/doorbird/translations/ru.json b/homeassistant/components/doorbird/translations/ru.json index df156bb640a..045092a2b97 100644 --- a/homeassistant/components/doorbird/translations/ru.json +++ b/homeassistant/components/doorbird/translations/ru.json @@ -29,6 +29,9 @@ "data": { "events": "\u0421\u043f\u0438\u0441\u043e\u043a \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e." }, + "data_description": { + "events": "\u0414\u043e\u0431\u0430\u0432\u044c\u0442\u0435 \u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0441\u043e\u0431\u044b\u0442\u0438\u0439, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c. \u041f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 DoorBird, \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0437\u043d\u0430\u0447\u0438\u0442\u044c \u0438\u0445 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u043e\u043c\u0443 \u0441\u043e\u0431\u044b\u0442\u0438\u044e.\n\n\u041f\u0440\u0438\u043c\u0435\u0440: somebody_pressed_the_button, motion." + }, "description": "\u0414\u043e\u0431\u0430\u0432\u044c\u0442\u0435 \u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0441\u043e\u0431\u044b\u0442\u0438\u0439, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c. \u041f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 DoorBird, \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0437\u043d\u0430\u0447\u0438\u0442\u044c \u0438\u0445 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u043e\u043c\u0443 \u0441\u043e\u0431\u044b\u0442\u0438\u044e. \u041f\u0440\u0438\u043c\u0435\u0440: somebody_pressed_the_button, motion. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438: https://www.home-assistant.io/integrations/doorbird/#events." } } diff --git a/homeassistant/components/doorbird/translations/tr.json b/homeassistant/components/doorbird/translations/tr.json index 1bb08b6f81e..1e472a43535 100644 --- a/homeassistant/components/doorbird/translations/tr.json +++ b/homeassistant/components/doorbird/translations/tr.json @@ -29,6 +29,9 @@ "data": { "events": "Virg\u00fclle ayr\u0131lm\u0131\u015f olaylar\u0131n listesi." }, + "data_description": { + "events": "\u0130zlemek istedi\u011finiz her etkinlik i\u00e7in virg\u00fclle ayr\u0131lm\u0131\u015f bir etkinlik ad\u0131 ekleyin. Bunlar\u0131 buraya girdikten sonra, onlar\u0131 belirli bir etkinli\u011fe atamak i\u00e7in DoorBird uygulamas\u0131n\u0131 kullan\u0131n. \n\n \u00d6rnek: birisi_butona_basti, hareket" + }, "description": "\u0130zlemek istedi\u011finiz her etkinlik i\u00e7in virg\u00fclle ayr\u0131lm\u0131\u015f bir etkinlik ad\u0131 ekleyin. Bunlar\u0131 buraya girdikten sonra, onlar\u0131 belirli bir etkinli\u011fe atamak i\u00e7in DoorBird uygulamas\u0131n\u0131 kullan\u0131n. https://www.home-assistant.io/integrations/doorbird/#events adresindeki belgelere bak\u0131n. \u00d6rnek: birisi_pressed_the_button, hareket" } } diff --git a/homeassistant/components/doorbird/translations/zh-Hant.json b/homeassistant/components/doorbird/translations/zh-Hant.json index 020267b4921..616e346b0dc 100644 --- a/homeassistant/components/doorbird/translations/zh-Hant.json +++ b/homeassistant/components/doorbird/translations/zh-Hant.json @@ -29,6 +29,9 @@ "data": { "events": "\u4ee5\u9017\u865f\u5206\u5225\u4e8b\u4ef6\u5217\u8868\u3002" }, + "data_description": { + "events": "\u4ee5\u9017\u865f\u5206\u5225\u6240\u8981\u8ffd\u8e64\u7684\u4e8b\u4ef6\u540d\u7a31\u3002\u65bc\u6b64\u8f38\u5165\u5f8c\uff0c\u4f7f\u7528 DoorBird App \u6307\u5b9a\u81f3\u7279\u5b9a\u4e8b\u4ef6\u3002\n\n\u4f8b\u5982\uff1asomebody_pressed_the_button, motion" + }, "description": "\u4ee5\u9017\u865f\u5206\u5225\u6240\u8981\u8ffd\u8e64\u7684\u4e8b\u4ef6\u540d\u7a31\u3002\u65bc\u6b64\u8f38\u5165\u5f8c\uff0c\u4f7f\u7528 DoorBird App \u6307\u5b9a\u81f3\u7279\u5b9a\u4e8b\u4ef6\u3002\u8acb\u53c3\u95b1\u6587\u4ef6\uff1ahttps://www.home-assistant.io/integrations/doorbird/#events\u3002\u4f8b\u5982\uff1asomebody_pressed_the_button, motion" } } diff --git a/homeassistant/components/dsmr/translations/it.json b/homeassistant/components/dsmr/translations/it.json index 9b50979d016..67ca750dd02 100644 --- a/homeassistant/components/dsmr/translations/it.json +++ b/homeassistant/components/dsmr/translations/it.json @@ -9,12 +9,12 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "cannot_communicate": "Impossibile comunicare", "cannot_connect": "Impossibile connettersi", - "one": "Pi\u00f9", - "other": "Altri" + "one": "Vuoto", + "other": "Vuoti" }, "step": { - "one": "Pi\u00f9", - "other": "Altri", + "one": "Vuoto", + "other": "Vuoti", "setup_network": { "data": { "dsmr_version": "Seleziona la versione DSMR", diff --git a/homeassistant/components/dunehd/translations/ca.json b/homeassistant/components/dunehd/translations/ca.json index 12f139afe60..08dd841bf4f 100644 --- a/homeassistant/components/dunehd/translations/ca.json +++ b/homeassistant/components/dunehd/translations/ca.json @@ -13,7 +13,7 @@ "data": { "host": "Amfitri\u00f3" }, - "description": "Configura la integraci\u00f3 Dune HD. Si tens problemes durant la configuraci\u00f3, v\u00e9s a: https://www.home-assistant.io/integrations/dunehd\n\nAssegura't que el reproductor estigui engegat.", + "description": "Assegura't que el reproductor est\u00e0 engegat.", "title": "Dune HD" } } diff --git a/homeassistant/components/dunehd/translations/de.json b/homeassistant/components/dunehd/translations/de.json index f3d7ecd725a..bcab02b2a09 100644 --- a/homeassistant/components/dunehd/translations/de.json +++ b/homeassistant/components/dunehd/translations/de.json @@ -13,7 +13,7 @@ "data": { "host": "Host" }, - "description": "Richte die Dune HD-Integration ein. Wenn du Probleme mit der Konfiguration hast, gehe zu: https://www.home-assistant.io/integrations/dunehd \n\nStelle sicher, dass dein Player eingeschaltet ist.", + "description": "Stelle sicher, dass dein Player eingeschaltet ist.", "title": "Dune HD" } } diff --git a/homeassistant/components/dunehd/translations/en.json b/homeassistant/components/dunehd/translations/en.json index 41540c987be..d6392509fcf 100644 --- a/homeassistant/components/dunehd/translations/en.json +++ b/homeassistant/components/dunehd/translations/en.json @@ -13,7 +13,7 @@ "data": { "host": "Host" }, - "description": "Set up Dune HD integration. If you have problems with configuration go to: https://www.home-assistant.io/integrations/dunehd \n\nEnsure that your player is turned on.", + "description": "Ensure that your player is turned on.", "title": "Dune HD" } } diff --git a/homeassistant/components/dunehd/translations/et.json b/homeassistant/components/dunehd/translations/et.json index bb53d124200..3f4a9b4b085 100644 --- a/homeassistant/components/dunehd/translations/et.json +++ b/homeassistant/components/dunehd/translations/et.json @@ -13,7 +13,7 @@ "data": { "host": "" }, - "description": "Seadista Dune HD sidumine. Kui seadistamisega on probleeme, mine aadressile https://www.home-assistant.io/integrations/dunehd\n\n Veendu, et seade on sisse l\u00fclitatud.", + "description": "Veendu, et m\u00e4ngija on sisse l\u00fclitatud.", "title": "" } } diff --git a/homeassistant/components/dunehd/translations/fr.json b/homeassistant/components/dunehd/translations/fr.json index 0e8cb6d6ff8..8c3bc89a0e6 100644 --- a/homeassistant/components/dunehd/translations/fr.json +++ b/homeassistant/components/dunehd/translations/fr.json @@ -13,7 +13,7 @@ "data": { "host": "H\u00f4te" }, - "description": "Configurez l'int\u00e9gration Dune HD. Si vous rencontrez des probl\u00e8mes de configuration, acc\u00e9dez \u00e0: https://www.home-assistant.io/integrations/dunehd \n\n Assurez-vous que votre lecteur est allum\u00e9.", + "description": "Assurez-vous que votre lecteur est allum\u00e9.", "title": "Dune HD" } } diff --git a/homeassistant/components/dunehd/translations/hu.json b/homeassistant/components/dunehd/translations/hu.json index 15b2d297363..1dc7c8ec6cc 100644 --- a/homeassistant/components/dunehd/translations/hu.json +++ b/homeassistant/components/dunehd/translations/hu.json @@ -13,7 +13,7 @@ "data": { "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.", + "description": "Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a lej\u00e1tsz\u00f3 be van kapcsolva.", "title": "Dune HD" } } diff --git a/homeassistant/components/dunehd/translations/id.json b/homeassistant/components/dunehd/translations/id.json index 25cb96bedea..5a86947a07a 100644 --- a/homeassistant/components/dunehd/translations/id.json +++ b/homeassistant/components/dunehd/translations/id.json @@ -13,7 +13,7 @@ "data": { "host": "Host" }, - "description": "Siapkan integrasi Dune HD. Jika Anda memiliki masalah dengan konfigurasi, buka: https://www.home-assistant.io/integrations/dunehd \n\nPastikan pemutar Anda dinyalakan.", + "description": "Pastikan pemutar Anda dinyalakan.", "title": "Dune HD" } } diff --git a/homeassistant/components/dunehd/translations/it.json b/homeassistant/components/dunehd/translations/it.json index e296a59e474..0e33feb2640 100644 --- a/homeassistant/components/dunehd/translations/it.json +++ b/homeassistant/components/dunehd/translations/it.json @@ -13,7 +13,7 @@ "data": { "host": "Host" }, - "description": "Configura l'integrazione Dune HD. In caso di problemi con la configurazione, visita: https://www.home-assistant.io/integrations/dunehd \n\n Assicurati che il tuo lettore sia acceso.", + "description": "Assicurati che il tuo lettore sia acceso.", "title": "Dune HD" } } diff --git a/homeassistant/components/dunehd/translations/nl.json b/homeassistant/components/dunehd/translations/nl.json index bb3dd7def47..f4c54a5dba5 100644 --- a/homeassistant/components/dunehd/translations/nl.json +++ b/homeassistant/components/dunehd/translations/nl.json @@ -13,7 +13,7 @@ "data": { "host": "Host" }, - "description": "Stel Dune HD integratie in. Als u problemen heeft met de configuratie ga dan naar: https://www.home-assistant.io/integrations/dunehd \n\nZorg ervoor dat uw speler is ingeschakeld.", + "description": "Zorg ervoor dat uw speler is ingeschakeld.", "title": "Dune HD" } } diff --git a/homeassistant/components/dunehd/translations/no.json b/homeassistant/components/dunehd/translations/no.json index a887228c322..1b35f5581b1 100644 --- a/homeassistant/components/dunehd/translations/no.json +++ b/homeassistant/components/dunehd/translations/no.json @@ -13,7 +13,7 @@ "data": { "host": "Vert" }, - "description": "Konfigurer Dune HD-integrering. Hvis du har problemer med konfigurasjonen, kan du g\u00e5 til: https://www.home-assistant.io/integrations/dunehd \n\nKontroller at spilleren er sl\u00e5tt p\u00e5.", + "description": "S\u00f8rg for at spilleren er sl\u00e5tt p\u00e5.", "title": "" } } diff --git a/homeassistant/components/dunehd/translations/pl.json b/homeassistant/components/dunehd/translations/pl.json index 5def31d1c46..376b84e05d9 100644 --- a/homeassistant/components/dunehd/translations/pl.json +++ b/homeassistant/components/dunehd/translations/pl.json @@ -13,7 +13,7 @@ "data": { "host": "Nazwa hosta lub adres IP" }, - "description": "Konfiguracja integracji odtwarzacza Dune HD. Je\u015bli masz problemy z konfiguracj\u0105, przejd\u017a do strony: https://www.home-assistant.io/integrations/dunehd\n\nUpewnij si\u0119, \u017ce odtwarzacz jest w\u0142\u0105czony.", + "description": "Upewnij si\u0119, \u017ce odtwarzacz jest w\u0142\u0105czony.", "title": "Dune HD" } } diff --git a/homeassistant/components/dunehd/translations/pt-BR.json b/homeassistant/components/dunehd/translations/pt-BR.json index 072cf6011ea..4775f1af379 100644 --- a/homeassistant/components/dunehd/translations/pt-BR.json +++ b/homeassistant/components/dunehd/translations/pt-BR.json @@ -13,7 +13,7 @@ "data": { "host": "Nome do host" }, - "description": "Configure a integra\u00e7\u00e3o Dune HD. Se voc\u00ea tiver problemas com a configura\u00e7\u00e3o, acesse: https://www.home-assistant.io/integrations/dunehd \n\n Certifique-se de que seu player est\u00e1 ligado.", + "description": "Certifique-se de que seu player est\u00e1 ligado.", "title": "Dune HD" } } diff --git a/homeassistant/components/dunehd/translations/ru.json b/homeassistant/components/dunehd/translations/ru.json index c1537de579f..134511e66f2 100644 --- a/homeassistant/components/dunehd/translations/ru.json +++ b/homeassistant/components/dunehd/translations/ru.json @@ -13,7 +13,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Dune HD. \u0415\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0432\u043e\u0437\u043d\u0438\u043a\u043b\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443: https://www.home-assistant.io/integrations/dunehd\n\n\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 \u043f\u043b\u0435\u0435\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d.", + "description": "\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043f\u0440\u043e\u0438\u0433\u0440\u044b\u0432\u0430\u0442\u0435\u043b\u044c \u0432\u043a\u043b\u044e\u0447\u0435\u043d.", "title": "Dune HD" } } diff --git a/homeassistant/components/dunehd/translations/tr.json b/homeassistant/components/dunehd/translations/tr.json index 121267c34e3..79c2f033cfc 100644 --- a/homeassistant/components/dunehd/translations/tr.json +++ b/homeassistant/components/dunehd/translations/tr.json @@ -13,7 +13,7 @@ "data": { "host": "Sunucu" }, - "description": "Dune HD entegrasyonunu ayarlay\u0131n. Yap\u0131land\u0131rmayla ilgili sorunlar\u0131n\u0131z varsa \u015fu adrese gidin: https://www.home-assistant.io/integrations/dunehd \n\n Oynat\u0131c\u0131n\u0131z\u0131n a\u00e7\u0131k oldu\u011fundan emin olun.", + "description": "Oynat\u0131c\u0131n\u0131z\u0131n a\u00e7\u0131k oldu\u011fundan emin olun.", "title": "Dune HD" } } diff --git a/homeassistant/components/dunehd/translations/zh-Hant.json b/homeassistant/components/dunehd/translations/zh-Hant.json index a81055b9576..993a6d9a210 100644 --- a/homeassistant/components/dunehd/translations/zh-Hant.json +++ b/homeassistant/components/dunehd/translations/zh-Hant.json @@ -13,7 +13,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u8a2d\u5b9a Dune HD \u6574\u5408\u3002\u5047\u5982\u65bc\u8a2d\u5b9a\u904e\u7a0b\u4e2d\u906d\u9047\u56f0\u7136\uff0c\u8acb\u53c3\u95b1\uff1ahttps://www.home-assistant.io/integrations/dunehd \n\n\u78ba\u5b9a\u64ad\u653e\u68c4\u5df2\u7d93\u958b\u555f\u3002", + "description": "\u78ba\u5b9a\u64ad\u653e\u5668\u5df2\u7d93\u958b\u555f\u3002", "title": "Dune HD" } } diff --git a/homeassistant/components/ecobee/translations/hu.json b/homeassistant/components/ecobee/translations/hu.json index a91478ff038..a2fdd5553a4 100644 --- a/homeassistant/components/ecobee/translations/hu.json +++ b/homeassistant/components/ecobee/translations/hu.json @@ -9,7 +9,7 @@ }, "step": { "authorize": { - "description": "K\u00e9rj\u00fck, enged\u00e9lyezze ezt az alkalmaz\u00e1st a https://www.ecobee.com/consumerportal/index.html c\u00edmen a k\u00f6vetkez\u0151 PIN-k\u00f3ddal: \n\n {pin} \n \n Ezut\u00e1n nyomja meg a K\u00fcld\u00e9s gombot.", + "description": "K\u00e9rj\u00fck, enged\u00e9lyezze ezt az alkalmaz\u00e1st a https://www.ecobee.com/consumerportal/index.html c\u00edmen a k\u00f6vetkez\u0151 PIN-k\u00f3ddal: \n\n {pin} \n \nEzut\u00e1n nyomja meg a Mehet gombot.", "title": "Alkalmaz\u00e1s enged\u00e9lyez\u00e9se ecobee.com-on" }, "user": { diff --git a/homeassistant/components/elkm1/translations/bg.json b/homeassistant/components/elkm1/translations/bg.json index 8fd587a0640..5e83523d419 100644 --- a/homeassistant/components/elkm1/translations/bg.json +++ b/homeassistant/components/elkm1/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", + "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": "{mac_address} ({host})", "step": { diff --git a/homeassistant/components/elkm1/translations/ca.json b/homeassistant/components/elkm1/translations/ca.json index 2317e475a37..b0a979f98b5 100644 --- a/homeassistant/components/elkm1/translations/ca.json +++ b/homeassistant/components/elkm1/translations/ca.json @@ -4,7 +4,9 @@ "address_already_configured": "Ja hi ha un Elk-M1 configurat amb aquesta adre\u00e7a", "already_configured": "Ja hi ha un Elk-M1 configurat amb aquest prefix", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", - "cannot_connect": "Ha fallat la connexi\u00f3" + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -37,13 +39,7 @@ }, "user": { "data": { - "address": "Adre\u00e7a IP, domini o port s\u00e8rie (en cas d'una connexi\u00f3 s\u00e8rie).", - "device": "Dispositiu", - "password": "Contrasenya", - "prefix": "Prefix \u00fanic (deixa-ho en blanc si nom\u00e9s tens un \u00fanic controlador Elk-M1).", - "protocol": "Protocol", - "temperature_unit": "Unitats de temperatura que utilitza l'Elk-M1.", - "username": "Nom d'usuari" + "device": "Dispositiu" }, "description": "Selecciona un sistema descobert o 'entrada manual' si no s'han descobert dispositius.", "title": "Connexi\u00f3 amb el controlador Elk-M1" diff --git a/homeassistant/components/elkm1/translations/cs.json b/homeassistant/components/elkm1/translations/cs.json index f2f4c17af9c..9ff8da3067d 100644 --- a/homeassistant/components/elkm1/translations/cs.json +++ b/homeassistant/components/elkm1/translations/cs.json @@ -28,13 +28,6 @@ "title": "P\u0159ipojen\u00ed k ovlada\u010di Elk-M1" }, "user": { - "data": { - "password": "Heslo", - "prefix": "Jedine\u010dn\u00fd prefix (ponechte pr\u00e1zdn\u00e9, pokud m\u00e1te pouze jeden ElkM1).", - "protocol": "Protokol", - "temperature_unit": "Jednotka teploty pou\u017e\u00edvan\u00e1 ElkM1.", - "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - }, "title": "P\u0159ipojen\u00ed k ovlada\u010di Elk-M1" } } diff --git a/homeassistant/components/elkm1/translations/de.json b/homeassistant/components/elkm1/translations/de.json index cf318a626c9..7c08a9b254d 100644 --- a/homeassistant/components/elkm1/translations/de.json +++ b/homeassistant/components/elkm1/translations/de.json @@ -4,7 +4,9 @@ "address_already_configured": "Ein ElkM1 mit dieser Adresse ist bereits konfiguriert", "already_configured": "Ein ElkM1 mit diesem Pr\u00e4fix ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", - "cannot_connect": "Verbinden fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -37,13 +39,7 @@ }, "user": { "data": { - "address": "Die IP-Adresse, die Domain oder der serielle Port bei einer seriellen Verbindung.", - "device": "Ger\u00e4t", - "password": "Passwort", - "prefix": "Ein eindeutiges Pr\u00e4fix (leer lassen, wenn du nur einen ElkM1 hast).", - "protocol": "Protokoll", - "temperature_unit": "Die von ElkM1 verwendete Temperatureinheit.", - "username": "Benutzername" + "device": "Ger\u00e4t" }, "description": "W\u00e4hle ein erkanntes System oder \"Manuelle Eingabe\", wenn keine Ger\u00e4te erkannt wurden.", "title": "Stelle eine Verbindung zur Elk-M1-Steuerung her" diff --git a/homeassistant/components/elkm1/translations/el.json b/homeassistant/components/elkm1/translations/el.json index 9f600806d83..bc1d64649aa 100644 --- a/homeassistant/components/elkm1/translations/el.json +++ b/homeassistant/components/elkm1/translations/el.json @@ -4,7 +4,9 @@ "address_already_configured": "\u0388\u03bd\u03b1 ElkM1 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "already_configured": "\u0388\u03bd\u03b1 ElkM1 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", @@ -37,13 +39,7 @@ }, "user": { "data": { - "address": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ae \u03bf \u03c4\u03bf\u03bc\u03ad\u03b1\u03c2 \u03ae \u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1 \u03b5\u03ac\u03bd \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b3\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bc\u03ad\u03c3\u03c9 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2.", - "device": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "prefix": "\u0388\u03bd\u03b1 \u03bc\u03bf\u03bd\u03b1\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b1\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03bc\u03cc\u03bd\u03bf \u03ad\u03bd\u03b1 ElkM1).", - "protocol": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf", - "temperature_unit": "\u0397 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03bf ElkM1.", - "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + "device": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03c0\u03bf\u03c5 \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03ae \"\u039c\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b9\u03c3\u03b7\" \u03b5\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2.", "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf Elk-M1 Control" diff --git a/homeassistant/components/elkm1/translations/es-419.json b/homeassistant/components/elkm1/translations/es-419.json index 02271c4ea6c..09b5bc190f7 100644 --- a/homeassistant/components/elkm1/translations/es-419.json +++ b/homeassistant/components/elkm1/translations/es-419.json @@ -11,14 +11,6 @@ }, "step": { "user": { - "data": { - "address": "La direcci\u00f3n IP, dominio o puerto serie si se conecta via serial.", - "password": "Contrase\u00f1a (solo segura).", - "prefix": "Un prefijo \u00fanico (d\u00e9jelo en blanco si solo tiene un ElkM1).", - "protocol": "Protocolo", - "temperature_unit": "La unidad de temperatura que utiliza ElkM1.", - "username": "Nombre de usuario (solo seguro)." - }, "description": "La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n[:puerto]' para 'seguro' y 'no seguro'. Ejemplo: '192.168.1.1'. El puerto es opcional y el valor predeterminado es 2101 para 'no seguro' y 2601 para 'seguro'. Para el protocolo serie, la direcci\u00f3n debe estar en la forma 'tty[:baudios]'. Ejemplo: '/dev/ttyS1'. La velocidad en baudios es opcional y su valor predeterminado es 115200.", "title": "Con\u00e9ctese al control Elk-M1" } diff --git a/homeassistant/components/elkm1/translations/es.json b/homeassistant/components/elkm1/translations/es.json index 06c0d9e257e..46e25dd288f 100644 --- a/homeassistant/components/elkm1/translations/es.json +++ b/homeassistant/components/elkm1/translations/es.json @@ -37,13 +37,7 @@ }, "user": { "data": { - "address": "La direcci\u00f3n IP o dominio o puerto serie si se conecta a trav\u00e9s de serie.", - "device": "Dispositivo", - "password": "Contrase\u00f1a", - "prefix": "Un prefijo \u00fanico (d\u00e9jalo en blanco si s\u00f3lo tienes un Elk-M1).", - "protocol": "Protocolo", - "temperature_unit": "La temperatura que usa la unidad Elk-M1", - "username": "Usuario" + "device": "Dispositivo" }, "description": "La cadena de direcci\u00f3n debe estar en el formato 'direcci\u00f3n[:puerto]' para 'seguro' y 'no-seguro'. Ejemplo: '192.168.1.1'. El puerto es opcional y el valor predeterminado es 2101 para 'no-seguro' y 2601 para 'seguro'. Para el protocolo serie, la direcci\u00f3n debe tener la forma 'tty[:baudios]'. Ejemplo: '/dev/ttyS1'. Los baudios son opcionales y el valor predeterminado es 115200.", "title": "Conectar con Control Elk-M1" diff --git a/homeassistant/components/elkm1/translations/et.json b/homeassistant/components/elkm1/translations/et.json index d874763045f..fbefde0a118 100644 --- a/homeassistant/components/elkm1/translations/et.json +++ b/homeassistant/components/elkm1/translations/et.json @@ -4,7 +4,9 @@ "address_already_configured": "Selle aadressiga ElkM1 on juba seadistatud", "already_configured": "Selle eesliitega ElkM1 on juba seadistatud", "already_in_progress": "Seadistamine juba k\u00e4ib", - "cannot_connect": "\u00dchendumine nurjus" + "cannot_connect": "\u00dchendumine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" }, "error": { "cannot_connect": "\u00dchendamine nurjus", @@ -37,13 +39,7 @@ }, "user": { "data": { - "address": "IP-aadress v\u00f5i domeen v\u00f5i jadaport, kui \u00fchendatakse jadaliidese kaudu.", - "device": "Seade", - "password": "Salas\u00f5na", - "prefix": "Unikaalne eesliide (j\u00e4ta t\u00fchjaks kui on ainult \u00fcks ElkM1).", - "protocol": "Protokoll", - "temperature_unit": "ElkM1'i temperatuuri\u00fchik.", - "username": "Kasutajanimi" + "device": "Seade" }, "description": "Vali avastatud s\u00fcsteem v\u00f5i \"K\u00e4sitsi sisestamine\" kui \u00fchtegi seadet ei ole avastatud.", "title": "\u00dchendu Elk-M1 Control" diff --git a/homeassistant/components/elkm1/translations/fr.json b/homeassistant/components/elkm1/translations/fr.json index 07b5d132968..e7512a596d1 100644 --- a/homeassistant/components/elkm1/translations/fr.json +++ b/homeassistant/components/elkm1/translations/fr.json @@ -4,7 +4,9 @@ "address_already_configured": "Un ElkM1 avec cette adresse est d\u00e9j\u00e0 configur\u00e9", "already_configured": "Un ElkM1 avec ce pr\u00e9fixe est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", - "cannot_connect": "\u00c9chec de connexion" + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -37,13 +39,7 @@ }, "user": { "data": { - "address": "L'adresse IP ou le domaine ou le port s\u00e9rie si vous vous connectez via s\u00e9rie.", - "device": "Appareil", - "password": "Mot de passe", - "prefix": "Un pr\u00e9fixe unique (laissez vide si vous n'avez qu'un seul ElkM1).", - "protocol": "Protocole", - "temperature_unit": "L'unit\u00e9 de temp\u00e9rature utilis\u00e9e par ElkM1.", - "username": "Nom d'utilisateur" + "device": "Appareil" }, "description": "Choisissez un syst\u00e8me d\u00e9couvert ou 'Entr\u00e9e manuelle' si aucun appareil n'a \u00e9t\u00e9 d\u00e9couvert.", "title": "Se connecter a Elk-M1 Control" diff --git a/homeassistant/components/elkm1/translations/he.json b/homeassistant/components/elkm1/translations/he.json index 8d7b5c9b945..3dda78332f9 100644 --- a/homeassistant/components/elkm1/translations/he.json +++ b/homeassistant/components/elkm1/translations/he.json @@ -2,7 +2,9 @@ "config": { "abort": { "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + "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" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", @@ -31,10 +33,7 @@ }, "user": { "data": { - "device": "\u05d4\u05ea\u05e7\u05df", - "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "protocol": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + "device": "\u05d4\u05ea\u05e7\u05df" }, "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05d0\u05dc \u05d1\u05e7\u05e8\u05ea Elk-M1" } diff --git a/homeassistant/components/elkm1/translations/hu.json b/homeassistant/components/elkm1/translations/hu.json index d076ad684c2..d744ac578ab 100644 --- a/homeassistant/components/elkm1/translations/hu.json +++ b/homeassistant/components/elkm1/translations/hu.json @@ -3,8 +3,10 @@ "abort": { "address_already_configured": "Az ElkM1 ezzel a c\u00edmmel m\u00e1r konfigur\u00e1lva van", "already_configured": "Az ezzel az el\u0151taggal rendelkez\u0151 ElkM1 m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -37,13 +39,7 @@ }, "user": { "data": { - "address": "Az IP-c\u00edm vagy tartom\u00e1ny vagy soros port, ha soros kapcsolaton kereszt\u00fcl csatlakozik.", - "device": "Eszk\u00f6z", - "password": "Jelsz\u00f3", - "prefix": "Egyedi el\u0151tag (hagyja \u00fcresen, ha csak egy ElkM1 van).", - "protocol": "Protokoll", - "temperature_unit": "Az ElkM1 h\u0151m\u00e9rs\u00e9kleti egys\u00e9g haszn\u00e1lja.", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + "device": "Eszk\u00f6z" }, "description": "V\u00e1lasszon egy felfedezett rendszert vagy a \u201eK\u00e9zi bevitelt\u201d, ha nem \u00e9szlelt eszk\u00f6zt.", "title": "Csatlakoz\u00e1s az Elk-M1 vez\u00e9rl\u0151h\u00f6z" diff --git a/homeassistant/components/elkm1/translations/id.json b/homeassistant/components/elkm1/translations/id.json index 782906fac0a..4512ad040e1 100644 --- a/homeassistant/components/elkm1/translations/id.json +++ b/homeassistant/components/elkm1/translations/id.json @@ -4,7 +4,9 @@ "address_already_configured": "ElkM1 dengan alamat ini sudah dikonfigurasi", "already_configured": "ElkM1 dengan prefiks ini sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" }, "error": { "cannot_connect": "Gagal terhubung", @@ -37,13 +39,7 @@ }, "user": { "data": { - "address": "Alamat IP atau domain atau port serial jika terhubung melalui serial.", - "device": "Perangkat", - "password": "Kata Sandi", - "prefix": "Prefiks unik (kosongkan jika hanya ada satu ElkM1).", - "protocol": "Protokol", - "temperature_unit": "Unit suhu yang digunakan ElkM1.", - "username": "Nama Pengguna" + "device": "Perangkat" }, "description": "Pilih sistem yang ditemukan atau 'Entri Manual' jika tidak ada perangkat yang ditemukan.", "title": "Hubungkan ke Kontrol Elk-M1" diff --git a/homeassistant/components/elkm1/translations/it.json b/homeassistant/components/elkm1/translations/it.json index fa05ab5d436..9284d2a495c 100644 --- a/homeassistant/components/elkm1/translations/it.json +++ b/homeassistant/components/elkm1/translations/it.json @@ -4,7 +4,9 @@ "address_already_configured": "Un ElkM1 con questo indirizzo \u00e8 gi\u00e0 configurato", "already_configured": "Un ElkM1 con questo prefisso \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", - "cannot_connect": "Impossibile connettersi" + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" }, "error": { "cannot_connect": "Impossibile connettersi", @@ -37,13 +39,7 @@ }, "user": { "data": { - "address": "L'indirizzo IP o il dominio o la porta seriale se ci si connette tramite seriale.", - "device": "Dispositivo", - "password": "Password", - "prefix": "Un prefisso univoco (lascia vuoto se disponi di un solo ElkM1).", - "protocol": "Protocollo", - "temperature_unit": "L'unit\u00e0 di temperatura utilizzata da ElkM1.", - "username": "Nome utente" + "device": "Dispositivo" }, "description": "Scegli un sistema rilevato o \"Inserimento manuale\" se non sono stati rilevati dispositivi.", "title": "Connettiti al controllo Elk-M1" diff --git a/homeassistant/components/elkm1/translations/ja.json b/homeassistant/components/elkm1/translations/ja.json index f280d6d6276..3d86e5f673d 100644 --- a/homeassistant/components/elkm1/translations/ja.json +++ b/homeassistant/components/elkm1/translations/ja.json @@ -4,7 +4,9 @@ "address_already_configured": "\u3053\u306e\u30a2\u30c9\u30ec\u30b9\u306eElkM1\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_configured": "\u3053\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3092\u6301\u3064ElkM1\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", @@ -37,13 +39,7 @@ }, "user": { "data": { - "address": "IP\u30a2\u30c9\u30ec\u30b9\u307e\u305f\u306f\u30c9\u30e1\u30a4\u30f3\u3001\u30b7\u30ea\u30a2\u30eb\u3067\u63a5\u7d9a\u3059\u308b\u5834\u5408\u306f\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3002", - "device": "\u30c7\u30d0\u30a4\u30b9", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "prefix": "\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)\u306a\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9(ElkM1\u304c1\u3064\u3057\u304b\u306a\u3044\u5834\u5408\u306f\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u307e\u3059)", - "protocol": "\u30d7\u30ed\u30c8\u30b3\u30eb", - "temperature_unit": "ElkM1\u304c\u4f7f\u7528\u3059\u308b\u6e29\u5ea6\u5358\u4f4d\u3002", - "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + "device": "\u30c7\u30d0\u30a4\u30b9" }, "description": "\u30a2\u30c9\u30ec\u30b9\u6587\u5b57\u5217\u306f\u3001 '\u30bb\u30ad\u30e5\u30a2 '\u304a\u3088\u3073 '\u975e\u30bb\u30ad\u30e5\u30a2 '\u306e\u5834\u5408\u306f\u3001'address[:port]'\u306e\u5f62\u5f0f\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u4f8b: '192.168.1.1'\u3002\u30dd\u30fc\u30c8\u306f\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u306f'\u975e\u30bb\u30ad\u30e5\u30a2'\u306e\u5834\u5408\u306f\u30012101 \u3067'\u30bb\u30ad\u30e5\u30a2'\u306e\u5834\u5408\u306f\u30012601 \u3067\u3059\u3002\u30b7\u30ea\u30a2\u30eb \u30d7\u30ed\u30c8\u30b3\u30eb\u306e\u5834\u5408\u3001\u30a2\u30c9\u30ec\u30b9\u306f\u3001'tty[:baud]' \u306e\u5f62\u5f0f\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002\u4f8b: '/dev/ttyS1'\u3002\u30dc\u30fc\u306f\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u306f115200\u3067\u3059\u3002", "title": "Elk-M1 Control\u306b\u63a5\u7d9a" diff --git a/homeassistant/components/elkm1/translations/ko.json b/homeassistant/components/elkm1/translations/ko.json index 507f741676a..6ffa3679946 100644 --- a/homeassistant/components/elkm1/translations/ko.json +++ b/homeassistant/components/elkm1/translations/ko.json @@ -11,14 +11,6 @@ }, "step": { "user": { - "data": { - "address": "\uc2dc\ub9ac\uc5bc\uc744 \ud1b5\ud574 \uc5f0\uacb0\ud558\ub294 \uacbd\uc6b0\uc758 IP \uc8fc\uc18c\ub098 \ub3c4\uba54\uc778 \ub610\ub294 \uc2dc\ub9ac\uc5bc \ud3ec\ud2b8.", - "password": "\ube44\ubc00\ubc88\ud638", - "prefix": "\uace0\uc720\ud55c \uc811\ub450\uc0ac (ElkM1 \uc774 \ud558\ub098\ub9cc \uc788\uc73c\uba74 \ube44\uc6cc\ub450\uc138\uc694).", - "protocol": "\ud504\ub85c\ud1a0\ucf5c", - "temperature_unit": "ElkM1 \uc774 \uc0ac\uc6a9\ud558\ub294 \uc628\ub3c4 \ub2e8\uc704.", - "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - }, "description": "\uc8fc\uc18c \ubb38\uc790\uc5f4\uc740 '\ubcf4\uc548' \ubc0f '\ube44\ubcf4\uc548'\uc5d0 \ub300\ud574 'address[:port]' \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608: '192.168.1.1'. \ud3ec\ud2b8\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 '\ube44\ubcf4\uc548' \uc758 \uacbd\uc6b0 2101 \uc774\uace0 '\ubcf4\uc548' \uc758 \uacbd\uc6b0 2601 \uc785\ub2c8\ub2e4. \uc2dc\ub9ac\uc5bc \ud504\ub85c\ud1a0\ucf5c\uc758 \uacbd\uc6b0 \uc8fc\uc18c\ub294 'tty[:baud]' \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608: '/dev/ttyS1'. \uc804\uc1a1 \uc18d\ub3c4\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 115200 \uc785\ub2c8\ub2e4.", "title": "Elk-M1 \uc81c\uc5b4\uc5d0 \uc5f0\uacb0\ud558\uae30" } diff --git a/homeassistant/components/elkm1/translations/lb.json b/homeassistant/components/elkm1/translations/lb.json index 084b4a1347f..14d2a82b97e 100644 --- a/homeassistant/components/elkm1/translations/lb.json +++ b/homeassistant/components/elkm1/translations/lb.json @@ -11,14 +11,6 @@ }, "step": { "user": { - "data": { - "address": "IP Adress oder Domain oder Serielle Port falls d'Verbindung seriell ass.", - "password": "Passwuert", - "prefix": "Een eenzegaartege Pr\u00e4fix (eidel lossen wann et n\u00ebmmen 1 ElkM1 g\u00ebtt)", - "protocol": "Protokoll", - "temperature_unit": "Temperatur Eenheet d\u00e9i den ElkM1 benotzt.", - "username": "Benotzernumm" - }, "description": "D'Adress muss an der Form 'adress[:port]' fir 'ges\u00e9chert' an 'onges\u00e9chert' sinn. Beispill: '192.168.1.1'. De Port os optionell an ass standardm\u00e9isseg op 2101 fir 'onges\u00e9chert' an op 2601 fir 'ges\u00e9chert' d\u00e9fin\u00e9iert. Fir de serielle Protokoll, muss d'Adress an der Form 'tty[:baud]' sinn. Beispill: '/dev/ttyS1'. Baud Rate ass optionell an ass standardm\u00e9isseg op 115200 d\u00e9fin\u00e9iert.", "title": "Mat Elk-M1 Control verbannen" } diff --git a/homeassistant/components/elkm1/translations/nl.json b/homeassistant/components/elkm1/translations/nl.json index c3efd45dbc0..c27293ce050 100644 --- a/homeassistant/components/elkm1/translations/nl.json +++ b/homeassistant/components/elkm1/translations/nl.json @@ -4,7 +4,9 @@ "address_already_configured": "Een ElkM1 met dit adres is al geconfigureerd", "already_configured": "Een ElkM1 met dit voorvoegsel is al geconfigureerd", "already_in_progress": "De configuratiestroom is al aan de gang", - "cannot_connect": "Kan geen verbinding maken" + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -37,13 +39,7 @@ }, "user": { "data": { - "address": "Het IP-adres of domein of seri\u00eble poort bij verbinding via serieel.", - "device": "Apparaat", - "password": "Wachtwoord", - "prefix": "Een uniek voorvoegsel (laat dit leeg als u maar \u00e9\u00e9n ElkM1 heeft).", - "protocol": "Protocol", - "temperature_unit": "De temperatuureenheid die ElkM1 gebruikt.", - "username": "Gebruikersnaam" + "device": "Apparaat" }, "description": "Kies een ontdekt systeem of 'Handmatige invoer' als er geen apparaten zijn ontdekt.", "title": "Maak verbinding met Elk-M1 Control" diff --git a/homeassistant/components/elkm1/translations/no.json b/homeassistant/components/elkm1/translations/no.json index ced77ff0d04..221b9206c81 100644 --- a/homeassistant/components/elkm1/translations/no.json +++ b/homeassistant/components/elkm1/translations/no.json @@ -4,7 +4,9 @@ "address_already_configured": "En ElkM1 med denne adressen er allerede konfigurert", "already_configured": "En ElkM1 med dette prefikset er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", - "cannot_connect": "Tilkobling mislyktes" + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -37,13 +39,7 @@ }, "user": { "data": { - "address": "IP-adressen eller domenet eller seriell port hvis du kobler til via seriell.", - "device": "Enhet", - "password": "Passord", - "prefix": "Et unikt prefiks (la v\u00e6re tomt hvis du bare har en ElkM1).", - "protocol": "Protokoll", - "temperature_unit": "Temperaturenheten ElkM1 bruker.", - "username": "Brukernavn" + "device": "Enhet" }, "description": "Velg et oppdaget system eller \"Manuell oppf\u00f8ring\" hvis ingen enheter har blitt oppdaget.", "title": "Koble til Elk-M1-kontroll" diff --git a/homeassistant/components/elkm1/translations/pl.json b/homeassistant/components/elkm1/translations/pl.json index 62900716f62..3b27226438f 100644 --- a/homeassistant/components/elkm1/translations/pl.json +++ b/homeassistant/components/elkm1/translations/pl.json @@ -4,7 +4,9 @@ "address_already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane z tym adresem", "already_configured": "ElkM1 z tym prefiksem jest ju\u017c skonfigurowany", "already_in_progress": "Konfiguracja jest ju\u017c w toku", - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -37,13 +39,7 @@ }, "user": { "data": { - "address": "Adres IP, domena lub port szeregowy w przypadku po\u0142\u0105czenia szeregowego.", - "device": "Urz\u0105dzenie", - "password": "Has\u0142o", - "prefix": "Unikatowy prefiks (pozostaw pusty, je\u015bli masz tylko jeden ElkM1).", - "protocol": "Protok\u00f3\u0142", - "temperature_unit": "Jednostka temperatury u\u017cywanej przez ElkM1.", - "username": "Nazwa u\u017cytkownika" + "device": "Urz\u0105dzenie" }, "description": "Wybierz wykryty system lub \u201eWpis r\u0119czny\u201d, je\u015bli nie wykryto \u017cadnych urz\u0105dze\u0144.", "title": "Pod\u0142\u0105czenie do sterownika Elk-M1" diff --git a/homeassistant/components/elkm1/translations/pt-BR.json b/homeassistant/components/elkm1/translations/pt-BR.json index 7cb9a5f101a..6fb03ab66e8 100644 --- a/homeassistant/components/elkm1/translations/pt-BR.json +++ b/homeassistant/components/elkm1/translations/pt-BR.json @@ -4,7 +4,9 @@ "address_already_configured": "Um ElkM1 com este endere\u00e7o j\u00e1 est\u00e1 configurado", "already_configured": "A conta j\u00e1 foi configurada", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", - "cannot_connect": "Falha ao conectar" + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" }, "error": { "cannot_connect": "Falha ao conectar", @@ -37,13 +39,7 @@ }, "user": { "data": { - "address": "O endere\u00e7o IP ou dom\u00ednio ou porta serial se estiver conectando via serial.", - "device": "Dispositivo", - "password": "Senha", - "prefix": "Um prefixo exclusivo (deixe em branco se voc\u00ea tiver apenas um ElkM1).", - "protocol": "Protocolo", - "temperature_unit": "A unidade de temperatura que ElkM1 usa.", - "username": "Usu\u00e1rio" + "device": "Dispositivo" }, "description": "Escolha um sistema descoberto ou 'Entrada Manual' se nenhum dispositivo foi descoberto.", "title": "Conecte ao controle Elk-M1" diff --git a/homeassistant/components/elkm1/translations/pt.json b/homeassistant/components/elkm1/translations/pt.json index 48d278ac354..2e669c21f1e 100644 --- a/homeassistant/components/elkm1/translations/pt.json +++ b/homeassistant/components/elkm1/translations/pt.json @@ -4,15 +4,6 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" - }, - "step": { - "user": { - "data": { - "password": "Palavra-passe (segura apenas)", - "protocol": "Protocolo", - "username": "Nome de utilizador (apenas seguro)." - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/ru.json b/homeassistant/components/elkm1/translations/ru.json index 1b4bf7250d7..624166fea48 100644 --- a/homeassistant/components/elkm1/translations/ru.json +++ b/homeassistant/components/elkm1/translations/ru.json @@ -4,7 +4,9 @@ "address_already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u044d\u0442\u0438\u043c \u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u044d\u0442\u0438\u043c \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "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." + "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." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -37,13 +39,7 @@ }, "user": { "data": { - "address": "IP-\u0430\u0434\u0440\u0435\u0441, \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442", - "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "prefix": "\u0423\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u0440\u0435\u0444\u0438\u043a\u0441 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0435\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d ElkM1)", - "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", - "temperature_unit": "\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", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u0443\u044e \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0438\u043b\u0438 'Manual Entry', \u0435\u0441\u043b\u0438 \u043d\u0438\u043a\u0430\u043a\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0431\u044b\u043b\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b.", "title": "Elk-M1 Control" diff --git a/homeassistant/components/elkm1/translations/sl.json b/homeassistant/components/elkm1/translations/sl.json index a815011988e..50988abb1d3 100644 --- a/homeassistant/components/elkm1/translations/sl.json +++ b/homeassistant/components/elkm1/translations/sl.json @@ -11,14 +11,6 @@ }, "step": { "user": { - "data": { - "address": "IP naslov, domena ali serijska vrata, \u010de se povezujete prek serijske povezave.", - "password": "Geslo (samo varno).", - "prefix": "Edinstvena predpona (pustite prazno, \u010de imate samo en ElkM1).", - "protocol": "Protokol", - "temperature_unit": "Temperaturna enota, ki jo uporablja ElkM1.", - "username": "Uporabni\u0161ko ime (samo varno)." - }, "description": "Naslov mora biti v obliki \"naslov[:port]\" za \"varno\" in \"ne-varno'. Primer: '192.168.1.1'. Vrata so neobvezna in so privzeto nastavljena na 2101 za \"non-secure\" in 2601 za 'varno'. Za serijski protokol, mora biti naslov v obliki \" tty[:baud]'. Primer: '/dev/ttyS1'. Baud je neobvezen in privzeto nastavljen na 115200.", "title": "Pove\u017eite se z Elk-M1 Control" } diff --git a/homeassistant/components/elkm1/translations/sv.json b/homeassistant/components/elkm1/translations/sv.json index 23a7d475a6f..19d9bb17e4b 100644 --- a/homeassistant/components/elkm1/translations/sv.json +++ b/homeassistant/components/elkm1/translations/sv.json @@ -4,13 +4,6 @@ "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" - }, - "step": { - "user": { - "data": { - "protocol": "Protokoll" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/tr.json b/homeassistant/components/elkm1/translations/tr.json index e8f147c8d57..d1c865bffd5 100644 --- a/homeassistant/components/elkm1/translations/tr.json +++ b/homeassistant/components/elkm1/translations/tr.json @@ -4,7 +4,9 @@ "address_already_configured": "Bu adrese sahip bir ElkM1 zaten yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r", "already_configured": "Bu \u00f6nek ile bir ElkM1 zaten yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", @@ -37,13 +39,7 @@ }, "user": { "data": { - "address": "Seri yoluyla ba\u011flan\u0131l\u0131yorsa IP adresi veya etki alan\u0131 veya seri ba\u011flant\u0131 noktas\u0131.", - "device": "Cihaz", - "password": "Parola", - "prefix": "Benzersiz bir \u00f6nek (yaln\u0131zca bir ElkM1'iniz varsa bo\u015f b\u0131rak\u0131n).", - "protocol": "Protokol", - "temperature_unit": "ElkM1'in kulland\u0131\u011f\u0131 s\u0131cakl\u0131k birimi.", - "username": "Kullan\u0131c\u0131 Ad\u0131" + "device": "Cihaz" }, "description": "Ke\u015ffedilen bir sistem veya ke\u015ffedilmemi\u015fse 'Manuel Giri\u015f' se\u00e7in.", "title": "Elk-M1 Kontrol\u00fcne Ba\u011flan\u0131n" diff --git a/homeassistant/components/elkm1/translations/uk.json b/homeassistant/components/elkm1/translations/uk.json index a8e711a4590..d794c6ecf69 100644 --- a/homeassistant/components/elkm1/translations/uk.json +++ b/homeassistant/components/elkm1/translations/uk.json @@ -11,14 +11,6 @@ }, "step": { "user": { - "data": { - "address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430, \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e \u043f\u043e\u0441\u043b\u0456\u0434\u043e\u0432\u043d\u0438\u0439 \u043f\u043e\u0440\u0442", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "prefix": "\u0423\u043d\u0456\u043a\u0430\u043b\u044c\u043d\u0438\u0439 \u043f\u0440\u0435\u0444\u0456\u043a\u0441 (\u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c, \u044f\u043a\u0449\u043e \u0443 \u0412\u0430\u0441 \u0442\u0456\u043b\u044c\u043a\u0438 \u043e\u0434\u0438\u043d ElkM1)", - "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", - "temperature_unit": "\u041e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u0443 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438", - "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" - }, "description": "\u0420\u044f\u0434\u043e\u043a \u0430\u0434\u0440\u0435\u0441\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0430 \u0431\u0443\u0442\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'addres[:port]' \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0456\u0432 'secure' \u0456 'non-secure' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: '192.168.1.1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'port' \u0432\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u043d \u0434\u043e\u0440\u0456\u0432\u043d\u044e\u0454 2101 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'non-secure' \u0456 2601 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'secure'. \u0414\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'serial' \u0430\u0434\u0440\u0435\u0441\u0430 \u043f\u043e\u0432\u0438\u043d\u043d\u0430 \u0431\u0443\u0442\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'tty[:baud]' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: '/dev/ttyS1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'baud' \u0432\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u043d \u0434\u043e\u0440\u0456\u0432\u043d\u044e\u0454 115200.", "title": "Elk-M1 Control" } diff --git a/homeassistant/components/elkm1/translations/zh-Hant.json b/homeassistant/components/elkm1/translations/zh-Hant.json index 543f93626ce..9405180fe0d 100644 --- a/homeassistant/components/elkm1/translations/zh-Hant.json +++ b/homeassistant/components/elkm1/translations/zh-Hant.json @@ -4,7 +4,9 @@ "address_already_configured": "\u4f7f\u7528\u6b64\u4f4d\u5740\u7684\u4e00\u7d44 ElkM1 \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_configured": "\u4f7f\u7528\u6b64 Prefix \u7684\u4e00\u7d44 ElkM1 \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "cannot_connect": "\u9023\u7dda\u5931\u6557" + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -37,13 +39,7 @@ }, "user": { "data": { - "address": "IP \u6216\u7db2\u57df\u540d\u7a31\u3001\u5e8f\u5217\u57e0\uff08\u5047\u5982\u900f\u904e\u5e8f\u5217\u9023\u7dda\uff09\u3002", - "device": "\u88dd\u7f6e", - "password": "\u5bc6\u78bc", - "prefix": "\u7368\u4e00\u7684 Prefix\uff08\u5047\u5982\u50c5\u6709\u4e00\u7d44 ElkM1 \u5247\u4fdd\u7559\u7a7a\u767d\uff09\u3002", - "protocol": "\u901a\u8a0a\u5354\u5b9a", - "temperature_unit": "ElkM1 \u4f7f\u7528\u6eab\u5ea6\u55ae\u4f4d\u3002", - "username": "\u4f7f\u7528\u8005\u540d\u7a31" + "device": "\u88dd\u7f6e" }, "description": "\u9078\u64c7\u6240\u767c\u73fe\u5230\u7684\u7cfb\u7d71\uff0c\u6216\u5047\u5982\u6c92\u627e\u5230\u7684\u8a71\u9032\u884c\u624b\u52d5\u8f38\u5165\u3002", "title": "\u9023\u7dda\u81f3 Elk-M1 Control" diff --git a/homeassistant/components/emulated_roku/translations/hu.json b/homeassistant/components/emulated_roku/translations/hu.json index 53b66f6db19..74cbdbcec79 100644 --- a/homeassistant/components/emulated_roku/translations/hu.json +++ b/homeassistant/components/emulated_roku/translations/hu.json @@ -10,7 +10,7 @@ "advertise_port": "Port k\u00f6zl\u00e9se", "host_ip": "IP c\u00edm", "listen_port": "Port", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "upnp_bind_multicast": "K\u00f6t\u00f6tt multicast (igaz/hamis)" }, "title": "A kiszolg\u00e1l\u00f3 szerver konfigur\u00e1l\u00e1sa" diff --git a/homeassistant/components/enphase_envoy/translations/en.json b/homeassistant/components/enphase_envoy/translations/en.json index ff600fea454..5d4617ed9fa 100644 --- a/homeassistant/components/enphase_envoy/translations/en.json +++ b/homeassistant/components/enphase_envoy/translations/en.json @@ -2,8 +2,7 @@ "config": { "abort": { "already_configured": "Device is already configured", - "reauth_successful": "Re-authentication was successful", - "not_ipv4_address": "Only IPv4 addresess are supported" + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/environment_canada/translations/de.json b/homeassistant/components/environment_canada/translations/de.json index c351683007f..6c9be14be27 100644 --- a/homeassistant/components/environment_canada/translations/de.json +++ b/homeassistant/components/environment_canada/translations/de.json @@ -15,7 +15,7 @@ "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.", + "description": "Es muss entweder eine Stations-ID oder der Breitengrad/L\u00e4ngengrad angegeben werden. Als Standardwerte f\u00fcr Breitengrad/L\u00e4ngengrad werden die in deiner 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": "Standort Kanada: Wetterstandort und Sprache" } } diff --git a/homeassistant/components/epson/translations/hu.json b/homeassistant/components/epson/translations/hu.json index e3aa507b7c1..ff56db57da9 100644 --- a/homeassistant/components/epson/translations/hu.json +++ b/homeassistant/components/epson/translations/hu.json @@ -8,7 +8,7 @@ "user": { "data": { "host": "C\u00edm", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" } } } diff --git a/homeassistant/components/esphome/translations/cs.json b/homeassistant/components/esphome/translations/cs.json index c6885e06851..cdd86bd2577 100644 --- a/homeassistant/components/esphome/translations/cs.json +++ b/homeassistant/components/esphome/translations/cs.json @@ -11,7 +11,7 @@ "invalid_psk": "Transportn\u00ed \u0161ifrovac\u00ed kl\u00ed\u010d je neplatn\u00fd. Ujist\u011bte se, \u017ee odpov\u00edd\u00e1 tomu, co m\u00e1te ve sv\u00e9 konfiguraci", "resolve_error": "Nelze naj\u00edt IP adresu uzlu ESP. Pokud tato chyba p\u0159etrv\u00e1v\u00e1, nastavte statickou adresu IP: 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/hu.json b/homeassistant/components/esphome/translations/hu.json index 17af0e57d26..e8e6c9b2dc2 100644 --- a/homeassistant/components/esphome/translations/hu.json +++ b/homeassistant/components/esphome/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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { diff --git a/homeassistant/components/evil_genius_labs/translations/cs.json b/homeassistant/components/evil_genius_labs/translations/cs.json index e1bf8e7f45f..7a929c1286f 100644 --- a/homeassistant/components/evil_genius_labs/translations/cs.json +++ b/homeassistant/components/evil_genius_labs/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "error": { + "timeout": "Vypr\u0161el \u010dasov\u00fd limit pro nav\u00e1z\u00e1n\u00ed spojen\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" } } diff --git a/homeassistant/components/ezviz/translations/it.json b/homeassistant/components/ezviz/translations/it.json index 0c6ce669ba3..febba0cad51 100644 --- a/homeassistant/components/ezviz/translations/it.json +++ b/homeassistant/components/ezviz/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "L'account \u00e8 gi\u00e0 configurato", - "ezviz_cloud_account_missing": "Ezviz cloud account mancante. Si prega di riconfigurare l'account Ezviz cloud", + "ezviz_cloud_account_missing": "Ezviz cloud account mancante. Riconfigura l'account Ezviz cloud", "unknown": "Errore imprevisto" }, "error": { diff --git a/homeassistant/components/fan/translations/hu.json b/homeassistant/components/fan/translations/hu.json index ec3175b8290..50e659b001a 100644 --- a/homeassistant/components/fan/translations/hu.json +++ b/homeassistant/components/fan/translations/hu.json @@ -9,6 +9,7 @@ "is_on": "{entity_name} be van kapcsolva" }, "trigger_type": { + "changed_states": "{entity_name} be- vagy kikapcsolt", "toggled": "{entity_name} \u00e1tkapcsolt", "turned_off": "{entity_name} ki lett kapcsolva", "turned_on": "{entity_name} be lett kapcsolva" diff --git a/homeassistant/components/fibaro/translations/bg.json b/homeassistant/components/fibaro/translations/bg.json new file mode 100644 index 00000000000..9f39b8c9185 --- /dev/null +++ b/homeassistant/components/fibaro/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\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", + "url": "URL \u0432\u044a\u0432 \u0444\u043e\u0440\u043c\u0430\u0442 URL", + "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/fibaro/translations/ca.json b/homeassistant/components/fibaro/translations/ca.json new file mode 100644 index 00000000000..8e04b3567f8 --- /dev/null +++ b/homeassistant/components/fibaro/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": { + "import_plugins": "Vols importar les entitats dels complements Fibaro?", + "password": "Contrasenya", + "url": "URL en el format http://HOST/api/", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/de.json b/homeassistant/components/fibaro/translations/de.json new file mode 100644 index 00000000000..831abc85929 --- /dev/null +++ b/homeassistant/components/fibaro/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": { + "import_plugins": "Entit\u00e4ten aus Fibaro-Plugins importieren?", + "password": "Passwort", + "url": "URL im Format http://HOST/api/", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/el.json b/homeassistant/components/fibaro/translations/el.json new file mode 100644 index 00000000000..d2a2659646f --- /dev/null +++ b/homeassistant/components/fibaro/translations/el.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "import_plugins": "\u0395\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03b1\u03c0\u03cc \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03b1 fibaro;", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03bc\u03b5 \u03c4\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae http://HOST/api/", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/en.json b/homeassistant/components/fibaro/translations/en.json index 2baeb3a7213..6bcff530798 100644 --- a/homeassistant/components/fibaro/translations/en.json +++ b/homeassistant/components/fibaro/translations/en.json @@ -11,9 +11,9 @@ "step": { "user": { "data": { - "url": "URL in the format http://HOST/api/", "import_plugins": "Import entities from fibaro plugins?", "password": "Password", + "url": "URL in the format http://HOST/api/", "username": "Username" } } diff --git a/homeassistant/components/fibaro/translations/et.json b/homeassistant/components/fibaro/translations/et.json new file mode 100644 index 00000000000..d9f140f8380 --- /dev/null +++ b/homeassistant/components/fibaro/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "import_plugins": "Kas importida olemid fibaro pistikprogrammidest?", + "password": "Salas\u00f5na", + "url": "URL vormingus http://HOST/api/", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/fr.json b/homeassistant/components/fibaro/translations/fr.json new file mode 100644 index 00000000000..8dee1529959 --- /dev/null +++ b/homeassistant/components/fibaro/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "import_plugins": "Importer les entit\u00e9s \u00e0 partir des plugins fibaro\u00a0?", + "password": "Mot de passe", + "url": "URL au format http://HOST/api/", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/he.json b/homeassistant/components/fibaro/translations/he.json new file mode 100644 index 00000000000..c479d8488f2 --- /dev/null +++ b/homeassistant/components/fibaro/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" + }, + "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/fibaro/translations/hu.json b/homeassistant/components/fibaro/translations/hu.json new file mode 100644 index 00000000000..d976d4b1a96 --- /dev/null +++ b/homeassistant/components/fibaro/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": { + "import_plugins": "Import\u00e1ln\u00e1 az entit\u00e1sokat a fibaro be\u00e9p\u00fcl\u0151 modulokb\u00f3l?", + "password": "Jelsz\u00f3", + "url": "URL a k\u00f6vetkez\u0151 form\u00e1tumban: http://C\u00cdM/api/", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/id.json b/homeassistant/components/fibaro/translations/id.json new file mode 100644 index 00000000000..715ad91c275 --- /dev/null +++ b/homeassistant/components/fibaro/translations/id.json @@ -0,0 +1,22 @@ +{ + "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": { + "import_plugins": "Impor entitas dari plugin fibaro?", + "password": "Kata Sandi", + "url": "URL dalam format http://HOST/api/", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/it.json b/homeassistant/components/fibaro/translations/it.json new file mode 100644 index 00000000000..641ed94e49f --- /dev/null +++ b/homeassistant/components/fibaro/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": { + "import_plugins": "Vuoi importare le entit\u00e0 dai plugin fibaro?", + "password": "Password", + "url": "URL nel formato http://HOST/api/", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/ja.json b/homeassistant/components/fibaro/translations/ja.json new file mode 100644 index 00000000000..8fc6562ff3b --- /dev/null +++ b/homeassistant/components/fibaro/translations/ja.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "import_plugins": "fibaro\u30d7\u30e9\u30b0\u30a4\u30f3\u304b\u3089\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u30a4\u30f3\u30dd\u30fc\u30c8\u3057\u307e\u3059\u304b\uff1f", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "url": "URL\u306e\u5f62\u5f0f\u306f\u3001http://HOST/api/ \u3067\u3059", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/nl.json b/homeassistant/components/fibaro/translations/nl.json new file mode 100644 index 00000000000..72b8588d1e4 --- /dev/null +++ b/homeassistant/components/fibaro/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kon niet verbinden", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "import_plugins": "Entiteiten importeren uit fibaro-plug-ins?", + "password": "Wachtwoord", + "url": "URL in het formaat http://HOST/api/", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/no.json b/homeassistant/components/fibaro/translations/no.json new file mode 100644 index 00000000000..8c868bb1ad8 --- /dev/null +++ b/homeassistant/components/fibaro/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": { + "import_plugins": "Importere enheter fra fibaro plugins?", + "password": "Passord", + "url": "URL i formatet http://HOST/api/", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/pl.json b/homeassistant/components/fibaro/translations/pl.json new file mode 100644 index 00000000000..ce4b2652d80 --- /dev/null +++ b/homeassistant/components/fibaro/translations/pl.json @@ -0,0 +1,22 @@ +{ + "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": { + "import_plugins": "Zaimportowa\u0107 encje z wtyczek fibaro?", + "password": "Has\u0142o", + "url": "Adres URL w formacie http://HOST/api/", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/pt-BR.json b/homeassistant/components/fibaro/translations/pt-BR.json new file mode 100644 index 00000000000..3e0f3139fa7 --- /dev/null +++ b/homeassistant/components/fibaro/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "import_plugins": "Importar entidades de plugins fibaro?", + "password": "Senha", + "url": "URL no formato http://HOST/api/", + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/ru.json b/homeassistant/components/fibaro/translations/ru.json new file mode 100644 index 00000000000..56e75b5fa34 --- /dev/null +++ b/homeassistant/components/fibaro/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": { + "import_plugins": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u0437 \u043f\u043b\u0430\u0433\u0438\u043d\u043e\u0432 fibaro", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "url": "URL-\u0430\u0434\u0440\u0435\u0441 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 http://HOST/api/", + "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/fibaro/translations/tr.json b/homeassistant/components/fibaro/translations/tr.json new file mode 100644 index 00000000000..c873e0dafd3 --- /dev/null +++ b/homeassistant/components/fibaro/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "import_plugins": "Varl\u0131klar\u0131 fibaro eklentilerinden i\u00e7e aktar\u0131ls\u0131n m\u0131?", + "password": "Parola", + "url": "http://HOST/api/ bi\u00e7imindeki URL", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/zh-Hant.json b/homeassistant/components/fibaro/translations/zh-Hant.json new file mode 100644 index 00000000000..e494fb1012f --- /dev/null +++ b/homeassistant/components/fibaro/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": { + "import_plugins": "\u5f9e fibaro \u5916\u639b\u532f\u5165\u5be6\u9ad4\uff1f", + "password": "\u5bc6\u78bc", + "url": "URL \u683c\u5f0f\u70ba http://HOST/api/", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/bg.json b/homeassistant/components/filesize/translations/bg.json new file mode 100644 index 00000000000..cfee062d421 --- /dev/null +++ b/homeassistant/components/filesize/translations/bg.json @@ -0,0 +1,19 @@ +{ + "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": { + "not_allowed": "\u041f\u044a\u0442\u044f\u0442 \u043d\u0435 \u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d", + "not_valid": "\u041f\u044a\u0442\u044f\u0442 \u043d\u0435 \u0435 \u0432\u0430\u043b\u0438\u0434\u0435\u043d" + }, + "step": { + "user": { + "data": { + "file_path": "\u041f\u044a\u0442 \u043a\u044a\u043c \u0444\u0430\u0439\u043b\u0430" + } + } + } + }, + "title": "\u0420\u0430\u0437\u043c\u0435\u0440 \u043d\u0430 \u0444\u0430\u0439\u043b\u0430" +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/ca.json b/homeassistant/components/filesize/translations/ca.json new file mode 100644 index 00000000000..7bdcb4ae522 --- /dev/null +++ b/homeassistant/components/filesize/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, + "error": { + "not_allowed": "Ruta no permesa", + "not_valid": "Ruta inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "file_path": "Ruta al fitxer" + } + } + } + }, + "title": "Mida de fitxer" +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/de.json b/homeassistant/components/filesize/translations/de.json new file mode 100644 index 00000000000..ac101d35783 --- /dev/null +++ b/homeassistant/components/filesize/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, + "error": { + "not_allowed": "Pfad nicht erlaubt", + "not_valid": "Pfad ung\u00fcltig" + }, + "step": { + "user": { + "data": { + "file_path": "Pfad zur Datei" + } + } + } + }, + "title": "Dateigr\u00f6\u00dfe" +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/el.json b/homeassistant/components/filesize/translations/el.json new file mode 100644 index 00000000000..c1b8f4eab2c --- /dev/null +++ b/homeassistant/components/filesize/translations/el.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + }, + "error": { + "not_allowed": "\u0397 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03b4\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9", + "not_valid": "\u0397 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7" + }, + "step": { + "user": { + "data": { + "file_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf" + } + } + } + }, + "title": "Filesize" +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/en.json b/homeassistant/components/filesize/translations/en.json index cd8954e5a71..1bad0b9b081 100644 --- a/homeassistant/components/filesize/translations/en.json +++ b/homeassistant/components/filesize/translations/en.json @@ -1,19 +1,19 @@ { "config": { - "step": { - "user": { - "data": { - "file_path": "Path to file" - } + "abort": { + "already_configured": "Service is already configured" + }, + "error": { + "not_allowed": "Path is not allowed", + "not_valid": "Path is not valid" + }, + "step": { + "user": { + "data": { + "file_path": "Path to file" + } + } } - }, - "error": { - "not_valid": "Path is not valid", - "not_allowed": "Path is not allowed" - }, - "abort": { - "already_configured": "Filepath is already configured" - } }, "title": "Filesize" - } \ No newline at end of file +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/et.json b/homeassistant/components/filesize/translations/et.json new file mode 100644 index 00000000000..b27b482e4cd --- /dev/null +++ b/homeassistant/components/filesize/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud" + }, + "error": { + "not_allowed": "Kirje asukoht on keelatud", + "not_valid": "Kirjet ei leitud" + }, + "step": { + "user": { + "data": { + "file_path": "Kirje asukoht" + } + } + } + }, + "title": "Filesize" +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/fr.json b/homeassistant/components/filesize/translations/fr.json new file mode 100644 index 00000000000..8f2f8ffe1ce --- /dev/null +++ b/homeassistant/components/filesize/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "not_allowed": "Le chemin d'acc\u00e8s n'est pas autoris\u00e9", + "not_valid": "Le chemin d'acc\u00e8s n'est pas valide" + }, + "step": { + "user": { + "data": { + "file_path": "Chemin d'acc\u00e8s au fichier" + } + } + } + }, + "title": "Taille de fichier" +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/he.json b/homeassistant/components/filesize/translations/he.json new file mode 100644 index 00000000000..48a6eeeea33 --- /dev/null +++ b/homeassistant/components/filesize/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/hu.json b/homeassistant/components/filesize/translations/hu.json new file mode 100644 index 00000000000..74c9f598bb9 --- /dev/null +++ b/homeassistant/components/filesize/translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "not_allowed": "Az el\u00e9r\u00e9si \u00fat nem enged\u00e9lyezett", + "not_valid": "Az el\u00e9r\u00e9si \u00fat \u00e9rv\u00e9nytelen" + }, + "step": { + "user": { + "data": { + "file_path": "A f\u00e1jl el\u00e9r\u00e9si \u00fatja" + } + } + } + }, + "title": "F\u00e1jlm\u00e9ret" +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/id.json b/homeassistant/components/filesize/translations/id.json new file mode 100644 index 00000000000..cb4bfdebaa7 --- /dev/null +++ b/homeassistant/components/filesize/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "not_allowed": "Jalur tidak diperbolehkan", + "not_valid": "Jalur tidak valid" + }, + "step": { + "user": { + "data": { + "file_path": "Jalur ke file" + } + } + } + }, + "title": "Ukuran file" +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/it.json b/homeassistant/components/filesize/translations/it.json new file mode 100644 index 00000000000..4372477b24c --- /dev/null +++ b/homeassistant/components/filesize/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + }, + "error": { + "not_allowed": "Il percorso non \u00e8 consentito", + "not_valid": "Il percorso non \u00e8 valido" + }, + "step": { + "user": { + "data": { + "file_path": "Percorso del file" + } + } + } + }, + "title": "Dimensione file" +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/ja.json b/homeassistant/components/filesize/translations/ja.json new file mode 100644 index 00000000000..3d8ca72452b --- /dev/null +++ b/homeassistant/components/filesize/translations/ja.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "not_allowed": "\u30d1\u30b9\u304c\u8a31\u53ef\u3055\u308c\u3066\u3044\u307e\u305b\u3093", + "not_valid": "\u30d1\u30b9\u304c\u7121\u52b9\u3067\u3059" + }, + "step": { + "user": { + "data": { + "file_path": "\u30d5\u30a1\u30a4\u30eb\u3078\u306e\u30d1\u30b9" + } + } + } + }, + "title": "\u30d5\u30a1\u30a4\u30eb\u30b5\u30a4\u30ba" +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/nl.json b/homeassistant/components/filesize/translations/nl.json new file mode 100644 index 00000000000..d8a35446044 --- /dev/null +++ b/homeassistant/components/filesize/translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Service is al geconfigureerd" + }, + "error": { + "not_allowed": "Pad is niet toegestaan", + "not_valid": "Pad is niet geldig" + }, + "step": { + "user": { + "data": { + "file_path": "Pad naar bestand" + } + } + } + }, + "title": "Bestandsgrootte" +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/no.json b/homeassistant/components/filesize/translations/no.json new file mode 100644 index 00000000000..18bdf37d667 --- /dev/null +++ b/homeassistant/components/filesize/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, + "error": { + "not_allowed": "Stien er ikke tillatt", + "not_valid": "Banen er ikke gyldig" + }, + "step": { + "user": { + "data": { + "file_path": "Bane til fil" + } + } + } + }, + "title": "Filst\u00f8rrelse" +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/pl.json b/homeassistant/components/filesize/translations/pl.json new file mode 100644 index 00000000000..2b2cd21a79c --- /dev/null +++ b/homeassistant/components/filesize/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + }, + "error": { + "not_allowed": "Niedozwolona \u015bcie\u017cka", + "not_valid": "Nieprawid\u0142owa \u015bcie\u017cka" + }, + "step": { + "user": { + "data": { + "file_path": "\u015acie\u017cka do pliku" + } + } + } + }, + "title": "Rozmiar pliku" +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/pt-BR.json b/homeassistant/components/filesize/translations/pt-BR.json new file mode 100644 index 00000000000..dfcc9cc5348 --- /dev/null +++ b/homeassistant/components/filesize/translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "not_allowed": "O caminho n\u00e3o \u00e9 permitido", + "not_valid": "O caminho n\u00e3o \u00e9 v\u00e1lido" + }, + "step": { + "user": { + "data": { + "file_path": "Caminho para o arquivo" + } + } + } + }, + "title": "Tamanho do arquivo" +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/ru.json b/homeassistant/components/filesize/translations/ru.json new file mode 100644 index 00000000000..1071fd09ca5 --- /dev/null +++ b/homeassistant/components/filesize/translations/ru.json @@ -0,0 +1,19 @@ +{ + "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." + }, + "error": { + "not_allowed": "\u041f\u0443\u0442\u044c \u043a \u0444\u0430\u0439\u043b\u0443 \u043d\u0435 \u0438\u043c\u0435\u0435\u0442 \u043d\u0443\u0436\u043d\u044b\u0445 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u0439.", + "not_valid": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043f\u0443\u0442\u044c \u043a \u0444\u0430\u0439\u043b\u0443." + }, + "step": { + "user": { + "data": { + "file_path": "\u041f\u0443\u0442\u044c \u043a \u0444\u0430\u0439\u043b\u0443" + } + } + } + }, + "title": "\u0420\u0430\u0437\u043c\u0435\u0440 \u0444\u0430\u0439\u043b\u0430" +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/tr.json b/homeassistant/components/filesize/translations/tr.json new file mode 100644 index 00000000000..cf62d56284e --- /dev/null +++ b/homeassistant/components/filesize/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "not_allowed": "Yola izin verilmiyor", + "not_valid": "Yol ge\u00e7erli de\u011fil" + }, + "step": { + "user": { + "data": { + "file_path": "Dosya yolu" + } + } + } + }, + "title": "Dosya boyutu" +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/zh-Hant.json b/homeassistant/components/filesize/translations/zh-Hant.json new file mode 100644 index 00000000000..692d770d337 --- /dev/null +++ b/homeassistant/components/filesize/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "not_allowed": "\u8def\u5f91\u4e0d\u5141\u8a31", + "not_valid": "\u8def\u5f91\u7121\u6548" + }, + "step": { + "user": { + "data": { + "file_path": "\u6a94\u6848\u8def\u5f91" + } + } + } + }, + "title": "\u6a94\u6848\u5927\u5c0f" +} \ No newline at end of file diff --git a/homeassistant/components/fivem/translations/hu.json b/homeassistant/components/fivem/translations/hu.json index b307c6e79fc..4e56dc3c17f 100644 --- a/homeassistant/components/fivem/translations/hu.json +++ b/homeassistant/components/fivem/translations/hu.json @@ -13,7 +13,7 @@ "user": { "data": { "host": "C\u00edm", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "port": "Port" } } diff --git a/homeassistant/components/flux_led/translations/hu.json b/homeassistant/components/flux_led/translations/hu.json index 1208f87fe70..f2f5d9b8751 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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" }, "error": { diff --git a/homeassistant/components/forecast_solar/translations/ca.json b/homeassistant/components/forecast_solar/translations/ca.json index 7bd31828080..141ad11c165 100644 --- a/homeassistant/components/forecast_solar/translations/ca.json +++ b/homeassistant/components/forecast_solar/translations/ca.json @@ -22,6 +22,7 @@ "azimuth": "Azimut (360 graus, 0 = nord, 90 = est, 180 = sud, 270 = oest)", "damping": "Factor d'amortiment: ajusta els resultats al mat\u00ed i al vespre", "declination": "Inclinaci\u00f3 (0 = horitzontal, 90 = vertical)", + "inverter_size": "Pot\u00e8ncia de l'inversor (Watts)", "modules power": "Pot\u00e8ncia m\u00e0xima total dels panells solars" }, "description": "Aquests valors permeten ajustar els resultats de Solar.Forecast. Consulta la documentaci\u00f3 si tens dubtes sobre algun camp." diff --git a/homeassistant/components/forecast_solar/translations/de.json b/homeassistant/components/forecast_solar/translations/de.json index 43b60424cf1..06e51e04659 100644 --- a/homeassistant/components/forecast_solar/translations/de.json +++ b/homeassistant/components/forecast_solar/translations/de.json @@ -22,6 +22,7 @@ "azimuth": "Azimut (360 Grad, 0 = Norden, 90 = Osten, 180 = S\u00fcden, 270 = Westen)", "damping": "D\u00e4mpfungsfaktor: passt die Ergebnisse morgens und abends an", "declination": "Deklination (0 = Horizontal, 90 = Vertikal)", + "inverter_size": "Wechselrichtergr\u00f6\u00dfe (Watt)", "modules power": "Gesamt-Watt-Spitzenleistung deiner Solarmodule" }, "description": "Mit diesen Werten kann das Solar.Forecast-Ergebnis angepasst werden. Wenn ein Feld unklar ist, lies bitte in der Dokumentation nach." diff --git a/homeassistant/components/forecast_solar/translations/el.json b/homeassistant/components/forecast_solar/translations/el.json index 0f6603a622b..e2e75c05f65 100644 --- a/homeassistant/components/forecast_solar/translations/el.json +++ b/homeassistant/components/forecast_solar/translations/el.json @@ -22,6 +22,7 @@ "azimuth": "\u0391\u03b6\u03b9\u03bc\u03bf\u03cd\u03b8\u03b9\u03bf (360 \u03bc\u03bf\u03af\u03c1\u03b5\u03c2, 0 = \u0392\u03bf\u03c1\u03c1\u03ac\u03c2, 90 = \u0391\u03bd\u03b1\u03c4\u03bf\u03bb\u03ae, 180 = \u039d\u03cc\u03c4\u03bf\u03c2, 270 = \u0394\u03cd\u03c3\u03b7)", "damping": "\u03a3\u03c5\u03bd\u03c4\u03b5\u03bb\u03b5\u03c3\u03c4\u03ae\u03c2 \u03b1\u03c0\u03cc\u03c3\u03b2\u03b5\u03c3\u03b7\u03c2: \u03c1\u03c5\u03b8\u03bc\u03af\u03b6\u03b5\u03b9 \u03c4\u03b1 \u03b1\u03c0\u03bf\u03c4\u03b5\u03bb\u03ad\u03c3\u03bc\u03b1\u03c4\u03b1 \u03c0\u03c1\u03c9\u03af \u03ba\u03b1\u03b9 \u03b2\u03c1\u03ac\u03b4\u03c5", "declination": "\u0391\u03c0\u03cc\u03ba\u03bb\u03b9\u03c3\u03b7 (0 = \u03bf\u03c1\u03b9\u03b6\u03cc\u03bd\u03c4\u03b9\u03b1, 90 = \u03ba\u03b1\u03c4\u03b1\u03ba\u03cc\u03c1\u03c5\u03c6\u03b7)", + "inverter_size": "\u039c\u03ad\u03b3\u03b5\u03b8\u03bf\u03c2 \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ad\u03b1 (Watt)", "modules power": "\u03a3\u03c5\u03bd\u03bf\u03bb\u03b9\u03ba\u03ae \u03bc\u03ad\u03b3\u03b9\u03c3\u03c4\u03b7 \u03b9\u03c3\u03c7\u03cd\u03c2 Watt \u03c4\u03c9\u03bd \u03b7\u03bb\u03b9\u03b1\u03ba\u03ce\u03bd \u03c3\u03b1\u03c2 \u03bc\u03bf\u03bd\u03ac\u03b4\u03c9\u03bd" }, "description": "\u0391\u03c5\u03c4\u03ad\u03c2 \u03bf\u03b9 \u03c4\u03b9\u03bc\u03ad\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03bf\u03c5\u03bd \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03c0\u03bf\u03c4\u03b5\u03bb\u03ad\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2 Solar.Forecast. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b5\u03ac\u03bd \u03ba\u03ac\u03c0\u03bf\u03b9\u03bf \u03c0\u03b5\u03b4\u03af\u03bf \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03b1\u03c6\u03ad\u03c2." diff --git a/homeassistant/components/forecast_solar/translations/en.json b/homeassistant/components/forecast_solar/translations/en.json index db9bead2e8c..2aa5a37cd1c 100644 --- a/homeassistant/components/forecast_solar/translations/en.json +++ b/homeassistant/components/forecast_solar/translations/en.json @@ -21,8 +21,8 @@ "api_key": "Forecast.Solar API Key (optional)", "azimuth": "Azimuth (360 degrees, 0 = North, 90 = East, 180 = South, 270 = West)", "damping": "Damping factor: adjusts the results in the morning and evening", - "inverter_size": "Inverter size (Watt)", "declination": "Declination (0 = Horizontal, 90 = Vertical)", + "inverter_size": "Inverter size (Watt)", "modules power": "Total Watt peak power of your solar modules" }, "description": "These values allow tweaking the Solar.Forecast result. Please refer to the documentation if a field is unclear." diff --git a/homeassistant/components/forecast_solar/translations/et.json b/homeassistant/components/forecast_solar/translations/et.json index 7aa87f4cf58..d2b72b30708 100644 --- a/homeassistant/components/forecast_solar/translations/et.json +++ b/homeassistant/components/forecast_solar/translations/et.json @@ -22,6 +22,7 @@ "azimuth": "Asimuut (360 kraadi, 0 = p\u00f5hi, 90 = ida, 180 = l\u00f5una, 270 = l\u00e4\u00e4s)", "damping": "Summutustegur: reguleerib tulemusi hommikul ja \u00f5htul", "declination": "Deklinatsioon (0 = horisontaalne, 90 = vertikaalne)", + "inverter_size": "Inverteri v\u00f5imsus (vatti)", "modules power": "P\u00e4ikesemoodulite koguv\u00f5imsus vattides" }, "description": "Need v\u00e4\u00e4rtused v\u00f5imaldavad muuta Solar.Forecast tulemust. Vaata dokumentatsiooni kui asi on ebaselge." diff --git a/homeassistant/components/forecast_solar/translations/fr.json b/homeassistant/components/forecast_solar/translations/fr.json index efd9f7be3a6..78b4d3f8f88 100644 --- a/homeassistant/components/forecast_solar/translations/fr.json +++ b/homeassistant/components/forecast_solar/translations/fr.json @@ -22,6 +22,7 @@ "azimuth": "Azimut (360 degr\u00e9s, 0 = Nord, 90 = Est, 180 = Sud, 270 = Ouest)", "damping": "Facteur d'amortissement : ajuste les r\u00e9sultats matin et soir", "declination": "D\u00e9clinaison (0 = horizontale, 90 = verticale)", + "inverter_size": "Taille de l'onduleur (en watts)", "modules power": "Puissance de cr\u00eate totale en watts de vos modules solaires" }, "description": "Ces valeurs permettent de peaufiner le r\u00e9sultat Solar.Forecast. Veuillez vous r\u00e9f\u00e9rer \u00e0 la documentation si un champ n'est pas clair." diff --git a/homeassistant/components/forecast_solar/translations/hu.json b/homeassistant/components/forecast_solar/translations/hu.json index 0bd814f16be..33a69ad2fd7 100644 --- a/homeassistant/components/forecast_solar/translations/hu.json +++ b/homeassistant/components/forecast_solar/translations/hu.json @@ -8,7 +8,7 @@ "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g", "modules power": "A napelemmodulok teljes cs\u00facsteljes\u00edtm\u00e9nye (Watt)", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" }, "description": "T\u00f6ltse ki a napelemek adatait. K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, ha egy mez\u0151 nem egy\u00e9rtelm\u0171." } @@ -22,6 +22,7 @@ "azimuth": "Azimut (360 fok, 0 = \u00e9szak, 90 = keleti, 180 = d\u00e9li, 270 = nyugati)", "damping": "Csillap\u00edt\u00e1si t\u00e9nyez\u0151: be\u00e1ll\u00edtja az eredm\u00e9nyeket reggelre \u00e9s est\u00e9re", "declination": "Deklin\u00e1ci\u00f3 (0 = v\u00edzszintes, 90 = f\u00fcgg\u0151leges)", + "inverter_size": "Inverter m\u00e9rete (Watt)", "modules power": "A napelemmodulok teljes cs\u00facsteljes\u00edtm\u00e9nye (Watt)" }, "description": "Ezek az \u00e9rt\u00e9kek lehet\u0151v\u00e9 teszik a Solar.Forecast eredm\u00e9ny m\u00f3dos\u00edt\u00e1s\u00e1t. K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, ha egy mez\u0151 nem egy\u00e9rtelm\u0171." diff --git a/homeassistant/components/forecast_solar/translations/id.json b/homeassistant/components/forecast_solar/translations/id.json index 27ef16e0266..5bd1236d6a6 100644 --- a/homeassistant/components/forecast_solar/translations/id.json +++ b/homeassistant/components/forecast_solar/translations/id.json @@ -22,6 +22,7 @@ "azimuth": "Azimuth (360 derajat, 0 = Utara, 90 = Timur, 180 = Selatan, 270 = Barat)", "damping": "Faktor redaman: menyesuaikan hasil di pagi dan sore hari", "declination": "Deklinasi (0 = Horizontal, 90 = Vertikal)", + "inverter_size": "Ukuran inverter (Watt)", "modules power": "Total daya puncak modul surya Anda dalam Watt" }, "description": "Nilai-nilai ini memungkinkan penyesuaian hasil Solar.Forecast. Rujuk ke dokumentasi jika bidang isian tidak jelas." diff --git a/homeassistant/components/forecast_solar/translations/it.json b/homeassistant/components/forecast_solar/translations/it.json index 7920eee43eb..598c67695cc 100644 --- a/homeassistant/components/forecast_solar/translations/it.json +++ b/homeassistant/components/forecast_solar/translations/it.json @@ -22,6 +22,7 @@ "azimuth": "Azimut (360 gradi, 0 = Nord, 90 = Est, 180 = Sud, 270 = Ovest)", "damping": "Fattore di smorzamento: regola i risultati al mattino e alla sera", "declination": "Declinazione (0 = Orizzontale, 90 = Verticale)", + "inverter_size": "Dimensioni inverter (Watt)", "modules power": "Potenza di picco totale in Watt dei tuoi moduli solari" }, "description": "Questi valori consentono di modificare il risultato di Solar.Forecast. Fai riferimento alla documentazione se un campo non \u00e8 chiaro." diff --git a/homeassistant/components/forecast_solar/translations/ja.json b/homeassistant/components/forecast_solar/translations/ja.json index 62090376bed..d86dc08f2b7 100644 --- a/homeassistant/components/forecast_solar/translations/ja.json +++ b/homeassistant/components/forecast_solar/translations/ja.json @@ -22,6 +22,7 @@ "azimuth": "\u65b9\u4f4d\u89d2(360\u5ea6\u30010=\u5317\u300190=\u6771\u3001180=\u5357\u3001270=\u897f)", "damping": "\u6e1b\u8870\u4fc2\u6570(\u30c0\u30f3\u30d4\u30f3\u30b0\u30d5\u30a1\u30af\u30bf\u30fc): \u671d\u3068\u5915\u65b9\u306e\u7d50\u679c\u3092\u8abf\u6574\u3059\u308b", "declination": "\u504f\u89d2(0\uff1d\u6c34\u5e73\u300190\uff1d\u5782\u76f4)", + "inverter_size": "\u30a4\u30f3\u30d0\u30fc\u30bf\u30fc\u306e\u30b5\u30a4\u30ba\uff08\u30ef\u30c3\u30c8\uff09", "modules power": "\u30bd\u30fc\u30e9\u30fc\u30e2\u30b8\u30e5\u30fc\u30eb\u306e\u7dcf\u30ef\u30c3\u30c8\u30d4\u30fc\u30af\u96fb\u529b" }, "description": "\u3053\u308c\u3089\u306e\u5024\u306b\u3088\u308a\u3001Solar.Forecast\u306e\u7d50\u679c\u3092\u5fae\u8abf\u6574\u3067\u304d\u307e\u3059\u3002\u30d5\u30a3\u30fc\u30eb\u30c9\u304c\u4e0d\u660e\u306a\u5834\u5408\u306f\u3001\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" diff --git a/homeassistant/components/forecast_solar/translations/nl.json b/homeassistant/components/forecast_solar/translations/nl.json index c66d272782d..dbc966e59fc 100644 --- a/homeassistant/components/forecast_solar/translations/nl.json +++ b/homeassistant/components/forecast_solar/translations/nl.json @@ -22,6 +22,7 @@ "azimuth": "Azimut (360 graden, 0 = Noord, 90 = Oost, 180 = Zuid, 270 = West)", "damping": "Dempingsfactor: past de resultaten 's ochtends en 's avonds aan", "declination": "Declinatie (0 = Horizontaal, 90 = Verticaal)", + "inverter_size": "Omvormer grootte (Watt)", "modules power": "Totaal Watt piekvermogen van uw zonnepanelen" }, "description": "Met deze waarden kan het resultaat van Solar.Forecast worden aangepast. Raadpleeg de documentatie als een veld onduidelijk is." diff --git a/homeassistant/components/forecast_solar/translations/no.json b/homeassistant/components/forecast_solar/translations/no.json index 1504727c1ae..a9acbb86f00 100644 --- a/homeassistant/components/forecast_solar/translations/no.json +++ b/homeassistant/components/forecast_solar/translations/no.json @@ -22,6 +22,7 @@ "azimuth": "Azimut (360 grader, 0 = Nord, 90 = \u00d8st, 180 = S\u00f8r, 270 = Vest)", "damping": "Dempingsfaktor: justerer resultatene om morgenen og kvelden", "declination": "Deklinasjon (0 = horisontal, 90 = vertikal)", + "inverter_size": "Inverterst\u00f8rrelse (Watt)", "modules power": "Total Watt-toppeffekt i solcellemodulene dine" }, "description": "Disse verdiene tillater justering av Solar.Forecast -resultatet. Se dokumentasjonen hvis et felt er uklart." diff --git a/homeassistant/components/forecast_solar/translations/pl.json b/homeassistant/components/forecast_solar/translations/pl.json index 3fc782fe7c3..ad01ce4bb54 100644 --- a/homeassistant/components/forecast_solar/translations/pl.json +++ b/homeassistant/components/forecast_solar/translations/pl.json @@ -22,6 +22,7 @@ "azimuth": "Azymut (360 stopni, 0 = P\u00f3\u0142noc, 90 = Wsch\u00f3d, 180 = Po\u0142udnie, 270 = Zach\u00f3d)", "damping": "Wsp\u00f3\u0142czynnik t\u0142umienia: dostosowuje wyniki rano i wieczorem", "declination": "Deklinacja (0 = Poziomo, 90 = Pionowo)", + "inverter_size": "Rozmiar falownika (Wat)", "modules power": "Ca\u0142kowita moc szczytowa modu\u0142\u00f3w fotowoltaicznych w watach" }, "description": "Te warto\u015bci pozwalaj\u0105 dostosowa\u0107 wyniki dla Solar.Forecast. Prosz\u0119 zapozna\u0107 si\u0119 z dokumentacj\u0105, je\u015bli pole jest niejasne." diff --git a/homeassistant/components/forecast_solar/translations/pt-BR.json b/homeassistant/components/forecast_solar/translations/pt-BR.json index ad6cca066c4..6761e17e8bd 100644 --- a/homeassistant/components/forecast_solar/translations/pt-BR.json +++ b/homeassistant/components/forecast_solar/translations/pt-BR.json @@ -22,6 +22,7 @@ "azimuth": "Azimute (360\u00b0, 0\u00b0 = Norte, 90\u00b0 = Leste, 180\u00b0 = Sul, 270\u00b0 = Oeste)", "damping": "Fator de amortecimento: ajusta os resultados de manh\u00e3 e \u00e0 noite", "declination": "Declina\u00e7\u00e3o (0\u00b0 = Horizontal, 90\u00b0 = Vertical)", + "inverter_size": "Pot\u00eancia do inversor (Watt)", "modules power": "Pot\u00eancia de pico total em Watt de seus m\u00f3dulos solares" }, "description": "Preencha os dados de seus pain\u00e9is solares. Consulte a documenta\u00e7\u00e3o se um campo n\u00e3o estiver claro." diff --git a/homeassistant/components/forecast_solar/translations/ru.json b/homeassistant/components/forecast_solar/translations/ru.json index 9cf8e87a8e2..f7d4d502691 100644 --- a/homeassistant/components/forecast_solar/translations/ru.json +++ b/homeassistant/components/forecast_solar/translations/ru.json @@ -22,6 +22,7 @@ "azimuth": "\u0410\u0437\u0438\u043c\u0443\u0442 (360 \u0433\u0440\u0430\u0434\u0443\u0441\u043e\u0432, 0 = \u0441\u0435\u0432\u0435\u0440, 90 = \u0432\u043e\u0441\u0442\u043e\u043a, 180 = \u044e\u0433, 270 = \u0437\u0430\u043f\u0430\u0434)", "damping": "\u0424\u0430\u043a\u0442\u043e\u0440 \u0437\u0430\u0442\u0443\u0445\u0430\u043d\u0438\u044f: \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u0438\u0440\u0443\u0435\u0442 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u044b \u0443\u0442\u0440\u043e\u043c \u0438 \u0432\u0435\u0447\u0435\u0440\u043e\u043c", "declination": "\u0421\u043a\u043b\u043e\u043d\u0435\u043d\u0438\u0435 (0 = \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435, 90 = \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0435)", + "inverter_size": "\u041c\u043e\u0449\u043d\u043e\u0441\u0442\u044c \u0438\u043d\u0432\u0435\u0440\u0442\u043e\u0440\u0430 (\u0432 \u0412\u0430\u0442\u0442\u0430\u0445)", "modules power": "\u041e\u0431\u0449\u0430\u044f \u043f\u0438\u043a\u043e\u0432\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c \u0412\u0430\u0448\u0438\u0445 \u0441\u043e\u043b\u043d\u0435\u0447\u043d\u044b\u0445 \u043c\u043e\u0434\u0443\u043b\u0435\u0439 (\u0432 \u0412\u0430\u0442\u0442\u0430\u0445)" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Forecast.Solar." diff --git a/homeassistant/components/forecast_solar/translations/tr.json b/homeassistant/components/forecast_solar/translations/tr.json index fecd8d7889a..1fa6def5714 100644 --- a/homeassistant/components/forecast_solar/translations/tr.json +++ b/homeassistant/components/forecast_solar/translations/tr.json @@ -22,6 +22,7 @@ "azimuth": "Azimut (360 derece, 0 = Kuzey, 90 = Do\u011fu, 180 = G\u00fcney, 270 = Bat\u0131)", "damping": "S\u00f6n\u00fcmleme fakt\u00f6r\u00fc: sonu\u00e7lar\u0131 sabah ve ak\u015fam ayarlar", "declination": "Sapma (0 = Yatay, 90 = Dikey)", + "inverter_size": "\u0130nverter boyutu (Watt)", "modules power": "Solar mod\u00fcllerinizin toplam en y\u00fcksek Watt g\u00fcc\u00fc" }, "description": "Bu de\u011ferler Solar.Forecast sonucunun ayarlanmas\u0131na izin verir. Bir alan net de\u011filse l\u00fctfen belgelere bak\u0131n." diff --git a/homeassistant/components/forecast_solar/translations/zh-Hant.json b/homeassistant/components/forecast_solar/translations/zh-Hant.json index fca97b9da01..3870ca29846 100644 --- a/homeassistant/components/forecast_solar/translations/zh-Hant.json +++ b/homeassistant/components/forecast_solar/translations/zh-Hant.json @@ -22,6 +22,7 @@ "azimuth": "\u65b9\u4f4d\u89d2\uff08360 \u5ea6\u55ae\u4f4d\u30020 = \u5317\u300190 = \u6771\u3001180 = \u5357\u3001270 = \u897f\uff09", "damping": "\u963b\u5c3c\u56e0\u7d20\uff1a\u8abf\u6574\u6e05\u6668\u8207\u508d\u665a\u7d50\u679c", "declination": "\u504f\u89d2\uff080 = \u6c34\u5e73\u300190 = \u5782\u76f4\uff09", + "inverter_size": "\u8b8a\u6d41\u5668\u5c3a\u5bf8\uff08Watt\uff09", "modules power": "\u7e3d\u5cf0\u503c\u529f\u7387" }, "description": "\u6b64\u4e9b\u6578\u503c\u5141\u8a31\u5fae\u8abf Solar.Forecast \u7d50\u679c\u3002\u5982\u679c\u6709\u4e0d\u6e05\u695a\u7684\u5730\u65b9\u3001\u8acb\u53c3\u8003\u6587\u4ef6\u8aaa\u660e\u3002" diff --git a/homeassistant/components/foscam/translations/ca.json b/homeassistant/components/foscam/translations/ca.json index b7f71c8c922..2fbd19dc20c 100644 --- a/homeassistant/components/foscam/translations/ca.json +++ b/homeassistant/components/foscam/translations/ca.json @@ -21,6 +21,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/cs.json b/homeassistant/components/foscam/translations/cs.json index b6f3c40abf6..ae1fc69cc77 100644 --- a/homeassistant/components/foscam/translations/cs.json +++ b/homeassistant/components/foscam/translations/cs.json @@ -18,6 +18,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/de.json b/homeassistant/components/foscam/translations/de.json index bc1c12ea130..30d331848a4 100644 --- a/homeassistant/components/foscam/translations/de.json +++ b/homeassistant/components/foscam/translations/de.json @@ -21,6 +21,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/el.json b/homeassistant/components/foscam/translations/el.json index 0e6c9b7c65d..44aa096837c 100644 --- a/homeassistant/components/foscam/translations/el.json +++ b/homeassistant/components/foscam/translations/el.json @@ -21,6 +21,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/en.json b/homeassistant/components/foscam/translations/en.json index 16a7d0b7800..29fd01d030d 100644 --- a/homeassistant/components/foscam/translations/en.json +++ b/homeassistant/components/foscam/translations/en.json @@ -21,6 +21,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/es-419.json b/homeassistant/components/foscam/translations/es-419.json index 39027bdf914..720a14b8523 100644 --- a/homeassistant/components/foscam/translations/es-419.json +++ b/homeassistant/components/foscam/translations/es-419.json @@ -11,6 +11,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/es.json b/homeassistant/components/foscam/translations/es.json index 7e8b7c1427d..f80ef3335d1 100644 --- a/homeassistant/components/foscam/translations/es.json +++ b/homeassistant/components/foscam/translations/es.json @@ -21,6 +21,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/et.json b/homeassistant/components/foscam/translations/et.json index c21ffa0cdd1..9c2801c6135 100644 --- a/homeassistant/components/foscam/translations/et.json +++ b/homeassistant/components/foscam/translations/et.json @@ -21,6 +21,5 @@ } } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/fr.json b/homeassistant/components/foscam/translations/fr.json index f728c6d7414..dcc4b45cc83 100644 --- a/homeassistant/components/foscam/translations/fr.json +++ b/homeassistant/components/foscam/translations/fr.json @@ -21,6 +21,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/hu.json b/homeassistant/components/foscam/translations/hu.json index b303db792bb..575e2b34982 100644 --- a/homeassistant/components/foscam/translations/hu.json +++ b/homeassistant/components/foscam/translations/hu.json @@ -21,6 +21,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/id.json b/homeassistant/components/foscam/translations/id.json index 21a7682b92c..89d57eb8e4a 100644 --- a/homeassistant/components/foscam/translations/id.json +++ b/homeassistant/components/foscam/translations/id.json @@ -21,6 +21,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/it.json b/homeassistant/components/foscam/translations/it.json index 63868a0f07f..e38b7d3e8a2 100644 --- a/homeassistant/components/foscam/translations/it.json +++ b/homeassistant/components/foscam/translations/it.json @@ -21,6 +21,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/ja.json b/homeassistant/components/foscam/translations/ja.json index 5a02ae5f446..3089882c63e 100644 --- a/homeassistant/components/foscam/translations/ja.json +++ b/homeassistant/components/foscam/translations/ja.json @@ -21,6 +21,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/ko.json b/homeassistant/components/foscam/translations/ko.json index ba743f6b27a..762957e840e 100644 --- a/homeassistant/components/foscam/translations/ko.json +++ b/homeassistant/components/foscam/translations/ko.json @@ -21,6 +21,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/lb.json b/homeassistant/components/foscam/translations/lb.json index 123b3f4be76..11dd851f381 100644 --- a/homeassistant/components/foscam/translations/lb.json +++ b/homeassistant/components/foscam/translations/lb.json @@ -11,6 +11,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/nl.json b/homeassistant/components/foscam/translations/nl.json index 9bea23ad702..5a6bfcaa4b3 100644 --- a/homeassistant/components/foscam/translations/nl.json +++ b/homeassistant/components/foscam/translations/nl.json @@ -21,6 +21,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/no.json b/homeassistant/components/foscam/translations/no.json index 0184213de27..03b96cebe65 100644 --- a/homeassistant/components/foscam/translations/no.json +++ b/homeassistant/components/foscam/translations/no.json @@ -21,6 +21,5 @@ } } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/pl.json b/homeassistant/components/foscam/translations/pl.json index d7494e22063..03a6a47fb74 100644 --- a/homeassistant/components/foscam/translations/pl.json +++ b/homeassistant/components/foscam/translations/pl.json @@ -21,6 +21,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/pt-BR.json b/homeassistant/components/foscam/translations/pt-BR.json index b33dce1e6fe..18af16044e8 100644 --- a/homeassistant/components/foscam/translations/pt-BR.json +++ b/homeassistant/components/foscam/translations/pt-BR.json @@ -21,6 +21,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/ru.json b/homeassistant/components/foscam/translations/ru.json index 8e8404c501e..1089ef63d59 100644 --- a/homeassistant/components/foscam/translations/ru.json +++ b/homeassistant/components/foscam/translations/ru.json @@ -21,6 +21,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/tr.json b/homeassistant/components/foscam/translations/tr.json index e6e5adc434c..1f8aab543fb 100644 --- a/homeassistant/components/foscam/translations/tr.json +++ b/homeassistant/components/foscam/translations/tr.json @@ -21,6 +21,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/zh-Hant.json b/homeassistant/components/foscam/translations/zh-Hant.json index d10746842a8..007a378a36b 100644 --- a/homeassistant/components/foscam/translations/zh-Hant.json +++ b/homeassistant/components/foscam/translations/zh-Hant.json @@ -21,6 +21,5 @@ } } } - }, - "title": "Foscam" + } } \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/hu.json b/homeassistant/components/freebox/translations/hu.json index 873e1057c15..39cfe189449 100644 --- a/homeassistant/components/freebox/translations/hu.json +++ b/homeassistant/components/freebox/translations/hu.json @@ -10,7 +10,7 @@ }, "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 Home Assistant seg\u00edts\u00e9g\u00e9vel. \n\n![A gomb helye a routeren] (/static/images/config_freebox.png)", + "description": "A k\u00f6vetkez\u0151 l\u00e9p\u00e9sben \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": { diff --git a/homeassistant/components/freebox/translations/it.json b/homeassistant/components/freebox/translations/it.json index 4eb49d5fdfb..f1978217547 100644 --- a/homeassistant/components/freebox/translations/it.json +++ b/homeassistant/components/freebox/translations/it.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Impossibile connettersi", - "register_failed": "Errore in fase di registrazione, si prega di riprovare", + "register_failed": "Errore in fase di registrazione, riprova", "unknown": "Errore imprevisto" }, "step": { diff --git a/homeassistant/components/fritz/translations/ca.json b/homeassistant/components/fritz/translations/ca.json index d39805a6302..04d5b14cac3 100644 --- a/homeassistant/components/fritz/translations/ca.json +++ b/homeassistant/components/fritz/translations/ca.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "ignore_ip6_link_local": "L'enlla\u00e7 amb adreces IPv6 locals no est\u00e0 perm\u00e8s", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { diff --git a/homeassistant/components/fritz/translations/de.json b/homeassistant/components/fritz/translations/de.json index ae36b3450ca..d64845ba9b7 100644 --- a/homeassistant/components/fritz/translations/de.json +++ b/homeassistant/components/fritz/translations/de.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "ignore_ip6_link_local": "IPv6 link local address wird nicht unterst\u00fctzt.", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { diff --git a/homeassistant/components/fritz/translations/el.json b/homeassistant/components/fritz/translations/el.json index d514848040f..6dfc67e08d6 100644 --- a/homeassistant/components/fritz/translations/el.json +++ b/homeassistant/components/fritz/translations/el.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "ignore_ip6_link_local": "\u0397 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 IPv6 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9.", "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { diff --git a/homeassistant/components/fritz/translations/en.json b/homeassistant/components/fritz/translations/en.json index e7ee1568684..3ce31866c2f 100644 --- a/homeassistant/components/fritz/translations/en.json +++ b/homeassistant/components/fritz/translations/en.json @@ -10,6 +10,7 @@ "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "cannot_connect": "Failed to connect", + "connection_error": "Failed to connect", "invalid_auth": "Invalid authentication", "upnp_not_configured": "Missing UPnP settings on device." }, @@ -31,6 +32,16 @@ "description": "Update FRITZ!Box Tools credentials for: {host}.\n\nFRITZ!Box Tools is unable to log in to your FRITZ!Box.", "title": "Updating FRITZ!Box Tools - credentials" }, + "start_config": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "username": "Username" + }, + "description": "Setup FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.", + "title": "Setup FRITZ!Box Tools - mandatory" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/fritz/translations/et.json b/homeassistant/components/fritz/translations/et.json index 2051e9f4e63..89bf4a3b7c9 100644 --- a/homeassistant/components/fritz/translations/et.json +++ b/homeassistant/components/fritz/translations/et.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "H\u00e4\u00e4lestamine on k\u00e4imas", + "ignore_ip6_link_local": "IPv6 lingi kohalikku aadressi ei toetata.", "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { diff --git a/homeassistant/components/fritz/translations/fr.json b/homeassistant/components/fritz/translations/fr.json index 3219164d98b..cfcd942b530 100644 --- a/homeassistant/components/fritz/translations/fr.json +++ b/homeassistant/components/fritz/translations/fr.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "ignore_ip6_link_local": "Les adresses IPv6 de liaison locale ne sont pas prises en charge.", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { diff --git a/homeassistant/components/fritz/translations/hu.json b/homeassistant/components/fritz/translations/hu.json index 81d3bb19e27..d1ff2c0c6bf 100644 --- a/homeassistant/components/fritz/translations/hu.json +++ b/homeassistant/components/fritz/translations/hu.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van", + "ignore_ip6_link_local": "Az IPv6-kapcsolat helyi c\u00edme nem t\u00e1mogatott.", "reauth_successful": "Az \u00fajhiteles\u00edt\u00e9s sikeres volt" }, "error": { diff --git a/homeassistant/components/fritz/translations/id.json b/homeassistant/components/fritz/translations/id.json index 816885b6391..c31bdf8b77c 100644 --- a/homeassistant/components/fritz/translations/id.json +++ b/homeassistant/components/fritz/translations/id.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Perangkat sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", + "ignore_ip6_link_local": "Alamat lokal tautan IPv6 tidak didukung.", "reauth_successful": "Autentikasi ulang berhasil" }, "error": { diff --git a/homeassistant/components/fritz/translations/it.json b/homeassistant/components/fritz/translations/it.json index bf577223b6e..0516449f5db 100644 --- a/homeassistant/components/fritz/translations/it.json +++ b/homeassistant/components/fritz/translations/it.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "ignore_ip6_link_local": "L'indirizzo locale del collegamento IPv6 non \u00e8 supportato.", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { diff --git a/homeassistant/components/fritz/translations/ja.json b/homeassistant/components/fritz/translations/ja.json index afbda92d3fe..8bd9747c9bc 100644 --- a/homeassistant/components/fritz/translations/ja.json +++ b/homeassistant/components/fritz/translations/ja.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "ignore_ip6_link_local": "IPv6\u30ea\u30f3\u30af\u306e\u30ed\u30fc\u30ab\u30eb\u30a2\u30c9\u30ec\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { diff --git a/homeassistant/components/fritz/translations/nl.json b/homeassistant/components/fritz/translations/nl.json index b5577f11a0d..11fabceaf8d 100644 --- a/homeassistant/components/fritz/translations/nl.json +++ b/homeassistant/components/fritz/translations/nl.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratiestroom is al aan de gang", + "ignore_ip6_link_local": "Lokaal IPv6-linkadres wordt niet ondersteund.", "reauth_successful": "Herauthenticatie was succesvol" }, "error": { diff --git a/homeassistant/components/fritz/translations/no.json b/homeassistant/components/fritz/translations/no.json index 6d6a805bab8..a18df1a7b14 100644 --- a/homeassistant/components/fritz/translations/no.json +++ b/homeassistant/components/fritz/translations/no.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "ignore_ip6_link_local": "IPv6-lenkens lokale adresse st\u00f8ttes ikke.", "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { diff --git a/homeassistant/components/fritz/translations/pl.json b/homeassistant/components/fritz/translations/pl.json index 7ec1f539aab..fed010f1987 100644 --- a/homeassistant/components/fritz/translations/pl.json +++ b/homeassistant/components/fritz/translations/pl.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "ignore_ip6_link_local": "Adres lokalny \u0142\u0105cza IPv6 nie jest obs\u0142ugiwany.", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { diff --git a/homeassistant/components/fritz/translations/pt-BR.json b/homeassistant/components/fritz/translations/pt-BR.json index 2170b34b1ee..ceb295310c7 100644 --- a/homeassistant/components/fritz/translations/pt-BR.json +++ b/homeassistant/components/fritz/translations/pt-BR.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "ignore_ip6_link_local": "O endere\u00e7o local IPv6 n\u00e3o \u00e9 suportado.", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { diff --git a/homeassistant/components/fritz/translations/ru.json b/homeassistant/components/fritz/translations/ru.json index 82530cca298..e45c36fe736 100644 --- a/homeassistant/components/fritz/translations/ru.json +++ b/homeassistant/components/fritz/translations/ru.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "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.", + "ignore_ip6_link_local": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 IPv6 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\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": { diff --git a/homeassistant/components/fritz/translations/tr.json b/homeassistant/components/fritz/translations/tr.json index 9a9cd3da6bf..86cabbb782b 100644 --- a/homeassistant/components/fritz/translations/tr.json +++ b/homeassistant/components/fritz/translations/tr.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "ignore_ip6_link_local": "IPv6 ba\u011flant\u0131 yerel adresi desteklenmiyor.", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { diff --git a/homeassistant/components/fritz/translations/zh-Hant.json b/homeassistant/components/fritz/translations/zh-Hant.json index 7eeb3ae5e4b..778c38e36f7 100644 --- a/homeassistant/components/fritz/translations/zh-Hant.json +++ b/homeassistant/components/fritz/translations/zh-Hant.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "ignore_ip6_link_local": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef IPv6 \u4f4d\u5740", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { diff --git a/homeassistant/components/fritzbox/translations/ca.json b/homeassistant/components/fritzbox/translations/ca.json index efd81ddff84..c17b1fc87c9 100644 --- a/homeassistant/components/fritzbox/translations/ca.json +++ b/homeassistant/components/fritzbox/translations/ca.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "ignore_ip6_link_local": "L'enlla\u00e7 amb adreces IPv6 locals no est\u00e0 perm\u00e8s", "no_devices_found": "No s'han trobat dispositius a la xarxa", "not_supported": "Connectat a AVM FRITZ!Box per\u00f2 no es poden controlar dispositius Smart Home.", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" diff --git a/homeassistant/components/fritzbox/translations/de.json b/homeassistant/components/fritzbox/translations/de.json index 7da8e616cfc..ef2a6083608 100644 --- a/homeassistant/components/fritzbox/translations/de.json +++ b/homeassistant/components/fritzbox/translations/de.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "ignore_ip6_link_local": "IPv6 link local address wird nicht unterst\u00fctzt.", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "not_supported": "Verbunden mit AVM FRITZ!Box, kann jedoch keine Smart Home-Ger\u00e4te steuern.", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" diff --git a/homeassistant/components/fritzbox/translations/el.json b/homeassistant/components/fritzbox/translations/el.json index 975126772d5..9e1c95e41f1 100644 --- a/homeassistant/components/fritzbox/translations/el.json +++ b/homeassistant/components/fritzbox/translations/el.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "ignore_ip6_link_local": "\u0397 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 IPv6 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9.", "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", "not_supported": "\u03a3\u03c5\u03bd\u03b4\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c4\u03bf AVM FRITZ!Box \u03b1\u03bb\u03bb\u03ac \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b5\u03bb\u03ad\u03b3\u03be\u03b5\u03b9 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 Smart Home.", "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" diff --git a/homeassistant/components/fritzbox/translations/et.json b/homeassistant/components/fritzbox/translations/et.json index 849dc7fadee..3db11e7355c 100644 --- a/homeassistant/components/fritzbox/translations/et.json +++ b/homeassistant/components/fritzbox/translations/et.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Seadistamine on juba k\u00e4imas", + "ignore_ip6_link_local": "IPv6 lingi kohalikku aadressi ei toetata.", "no_devices_found": "V\u00f5rgust ei leitud seadmeid", "not_supported": "\u00dchendatud AVM FRITZ!Boxiga! kuid see ei saa juhtida Smart Home seadmeid.", "reauth_successful": "Taastuvastamine \u00f5nnestus" diff --git a/homeassistant/components/fritzbox/translations/fr.json b/homeassistant/components/fritzbox/translations/fr.json index 69f024e26d7..f9e3d354a6d 100644 --- a/homeassistant/components/fritzbox/translations/fr.json +++ b/homeassistant/components/fritzbox/translations/fr.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "ignore_ip6_link_local": "Les adresses IPv6 de liaison locale ne sont pas prises en charge.", "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", "not_supported": "Connect\u00e9 \u00e0 AVM FRITZ! Box mais impossible de contr\u00f4ler les appareils Smart Home.", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" diff --git a/homeassistant/components/fritzbox/translations/hu.json b/homeassistant/components/fritzbox/translations/hu.json index c1cf8154aea..f079bf8e1df 100644 --- a/homeassistant/components/fritzbox/translations/hu.json +++ b/homeassistant/components/fritzbox/translations/hu.json @@ -2,7 +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", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "ignore_ip6_link_local": "Az IPv6-kapcsolat helyi c\u00edme nem t\u00e1mogatott.", "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." diff --git a/homeassistant/components/fritzbox/translations/id.json b/homeassistant/components/fritzbox/translations/id.json index f9c4f09b4ae..63e0ebb4823 100644 --- a/homeassistant/components/fritzbox/translations/id.json +++ b/homeassistant/components/fritzbox/translations/id.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Perangkat sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", + "ignore_ip6_link_local": "Alamat lokal tautan IPv6 tidak didukung.", "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", "not_supported": "Tersambung ke AVM FRITZ!Box tetapi tidak dapat mengontrol perangkat Smart Home.", "reauth_successful": "Autentikasi ulang berhasil" diff --git a/homeassistant/components/fritzbox/translations/it.json b/homeassistant/components/fritzbox/translations/it.json index 68cbe08b1b9..65a819d8329 100644 --- a/homeassistant/components/fritzbox/translations/it.json +++ b/homeassistant/components/fritzbox/translations/it.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "ignore_ip6_link_local": "L'indirizzo locale del collegamento IPv6 non \u00e8 supportato.", "no_devices_found": "Nessun dispositivo trovato sulla rete", "not_supported": "Collegato a AVM FRITZ!Box, ma non \u00e8 in grado di controllare i dispositivi Smart Home.", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" diff --git a/homeassistant/components/fritzbox/translations/ja.json b/homeassistant/components/fritzbox/translations/ja.json index c246ea5fb0d..047442d5ee7 100644 --- a/homeassistant/components/fritzbox/translations/ja.json +++ b/homeassistant/components/fritzbox/translations/ja.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "ignore_ip6_link_local": "IPv6\u30ea\u30f3\u30af\u306e\u30ed\u30fc\u30ab\u30eb\u30a2\u30c9\u30ec\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "not_supported": "AVM FRITZ!Box\u306b\u63a5\u7d9a\u3057\u307e\u3057\u305f\u304c\u3001Smart Home devices\u3092\u5236\u5fa1\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" diff --git a/homeassistant/components/fritzbox/translations/nl.json b/homeassistant/components/fritzbox/translations/nl.json index b1be4c8214f..43d51df760d 100644 --- a/homeassistant/components/fritzbox/translations/nl.json +++ b/homeassistant/components/fritzbox/translations/nl.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratiestroom is al aan de gang", + "ignore_ip6_link_local": "IPv6 link lokaal adres wordt niet ondersteund.", "no_devices_found": "Geen apparaten gevonden op het netwerk", "not_supported": "Verbonden met AVM FRITZ! Box, maar het kan geen Smart Home-apparaten bedienen.", "reauth_successful": "Herauthenticatie was succesvol" diff --git a/homeassistant/components/fritzbox/translations/no.json b/homeassistant/components/fritzbox/translations/no.json index 5ec0cc1acdc..98174053a61 100644 --- a/homeassistant/components/fritzbox/translations/no.json +++ b/homeassistant/components/fritzbox/translations/no.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "ignore_ip6_link_local": "IPv6-lenkens lokale adresse st\u00f8ttes ikke.", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", "not_supported": "Tilkoblet AVM FRITZ! Box, men den klarer ikke \u00e5 kontrollere Smart Home-enheter.", "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" diff --git a/homeassistant/components/fritzbox/translations/pl.json b/homeassistant/components/fritzbox/translations/pl.json index d9832ee51a4..f1ff975af8d 100644 --- a/homeassistant/components/fritzbox/translations/pl.json +++ b/homeassistant/components/fritzbox/translations/pl.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "ignore_ip6_link_local": "Adres lokalny \u0142\u0105cza IPv6 nie jest obs\u0142ugiwany.", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", "not_supported": "Po\u0142\u0105czony z AVM FRITZ!Box, ale nie jest w stanie kontrolowa\u0107 urz\u0105dze\u0144 Smart Home", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" diff --git a/homeassistant/components/fritzbox/translations/pt-BR.json b/homeassistant/components/fritzbox/translations/pt-BR.json index 3e3884cbc41..1fbe79772db 100644 --- a/homeassistant/components/fritzbox/translations/pt-BR.json +++ b/homeassistant/components/fritzbox/translations/pt-BR.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "ignore_ip6_link_local": "O endere\u00e7o local IPv6 n\u00e3o \u00e9 suportado.", "no_devices_found": "Nenhum dispositivo encontrado na rede", "not_supported": "Conectado ao AVM FRITZ!Box, mas n\u00e3o consegue controlar os dispositivos Smart Home.", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" diff --git a/homeassistant/components/fritzbox/translations/ru.json b/homeassistant/components/fritzbox/translations/ru.json index 51e9aedc632..5f939462db2 100644 --- a/homeassistant/components/fritzbox/translations/ru.json +++ b/homeassistant/components/fritzbox/translations/ru.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "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.", + "ignore_ip6_link_local": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 IPv6 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", "not_supported": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AVM FRITZ! Box \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e, \u043d\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c\u0438 Smart Home \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e.", "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." diff --git a/homeassistant/components/fritzbox/translations/tr.json b/homeassistant/components/fritzbox/translations/tr.json index 300ca1a096a..a870cbcae11 100644 --- a/homeassistant/components/fritzbox/translations/tr.json +++ b/homeassistant/components/fritzbox/translations/tr.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "ignore_ip6_link_local": "IPv6 ba\u011flant\u0131 yerel adresi desteklenmiyor.", "no_devices_found": "A\u011fda cihaz bulunamad\u0131", "not_supported": "AVM FRITZ!Box'a ba\u011fl\u0131 ancak Ak\u0131ll\u0131 Ev cihazlar\u0131n\u0131 kontrol edemiyor.", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" diff --git a/homeassistant/components/fritzbox/translations/zh-Hant.json b/homeassistant/components/fritzbox/translations/zh-Hant.json index b90b87aaee7..904cd81f625 100644 --- a/homeassistant/components/fritzbox/translations/zh-Hant.json +++ b/homeassistant/components/fritzbox/translations/zh-Hant.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "ignore_ip6_link_local": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef IPv6 \u4f4d\u5740", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "not_supported": "\u5df2\u9023\u7dda\u81f3 AVM FRITZ!Box \u4f46\u7121\u6cd5\u63a7\u5236\u667a\u80fd\u5bb6\u5ead\u88dd\u7f6e\u3002", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" diff --git a/homeassistant/components/generic/translations/bg.json b/homeassistant/components/generic/translations/bg.json new file mode 100644 index 00000000000..dc9bd439f0b --- /dev/null +++ b/homeassistant/components/generic/translations/bg.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "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", + "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": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "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\u0432\u0430\u043d\u0435\u0442\u043e?" + }, + "content_type": { + "data": { + "content_type": "\u0422\u0438\u043f \u0441\u044a\u0434\u044a\u0440\u0436\u0430\u043d\u0438\u0435" + }, + "description": "\u041f\u043e\u0441\u043e\u0447\u0435\u0442\u0435 \u0442\u0438\u043f\u0430 \u0441\u044a\u0434\u044a\u0440\u0436\u0430\u043d\u0438\u0435 \u0437\u0430 \u043f\u043e\u0442\u043e\u043a\u0430." + }, + "user": { + "data": { + "authentication": "\u0423\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "content_type": "\u0422\u0438\u043f \u0441\u044a\u0434\u044a\u0440\u0436\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "rtsp_transport": "RTSP \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u0435\u043d \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + }, + "options": { + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "content_type": { + "data": { + "content_type": "\u0422\u0438\u043f \u0441\u044a\u0434\u044a\u0440\u0436\u0430\u043d\u0438\u0435" + }, + "description": "\u041f\u043e\u0441\u043e\u0447\u0435\u0442\u0435 \u0442\u0438\u043f\u0430 \u0441\u044a\u0434\u044a\u0440\u0436\u0430\u043d\u0438\u0435 \u0437\u0430 \u043f\u043e\u0442\u043e\u043a\u0430." + }, + "init": { + "data": { + "authentication": "\u0423\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "content_type": "\u0422\u0438\u043f \u0441\u044a\u0434\u044a\u0440\u0436\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "rtsp_transport": "RTSP \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u0435\u043d \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "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/generic/translations/ca.json b/homeassistant/components/generic/translations/ca.json new file mode 100644 index 00000000000..3c2f5055ba5 --- /dev/null +++ b/homeassistant/components/generic/translations/ca.json @@ -0,0 +1,88 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "error": { + "already_exists": "Ja hi ha una c\u00e0mera amb aquest URL de configuraci\u00f3.", + "invalid_still_image": "L'URL no ha retornat una imatge fixa v\u00e0lida", + "no_still_image_or_stream_url": "Has d'especificar almenys una imatge un URL de flux", + "stream_file_not_found": "Fitxer no trobat mentre s'intentava connectar al flux de dades (est\u00e0 instal\u00b7lat ffmpeg?)", + "stream_http_not_found": "HTTP 404 'Not found' a l'intentar connectar-se al flux de dades ('stream')", + "stream_io_error": "Error d'entrada/sortida mentre s'intentava connectar al flux de dades. Protocol de transport RTSP incorrecte?", + "stream_no_route_to_host": "No s'ha pogut trobar l'amfitri\u00f3 mentre intentava connectar al flux de dades", + "stream_no_video": "El flux no cont\u00e9 v\u00eddeo", + "stream_not_permitted": "Operaci\u00f3 no permesa mentre s'intentava connectar al flux de dades. Protocol de transport RTSP incorrecte?", + "stream_unauthorised": "L'autoritzaci\u00f3 ha fallat mentre s'intentava connectar amb el flux de dades", + "timeout": "El temps m\u00e0xim de c\u00e0rrega de l'URL ha expirat", + "unable_still_load": "No s'ha pogut carregar cap imatge v\u00e0lida des de l'URL d'imatge fixa (pot ser per un amfitri\u00f3 o URL inv\u00e0lid o un error d'autenticaci\u00f3). Revisa els registres per a m\u00e9s informaci\u00f3.", + "unknown": "Error inesperat" + }, + "step": { + "confirm": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + }, + "content_type": { + "data": { + "content_type": "Tipus de contingut" + }, + "description": "Especifica el tipus de contingut per al flux de dades (stream)." + }, + "user": { + "data": { + "authentication": "Autenticaci\u00f3", + "content_type": "Tipus de contingut", + "framerate": "Freq\u00fc\u00e8ncia de visualitzaci\u00f3 (Hz)", + "limit_refetch_to_url_change": "Limita la lectura al canvi d'URL", + "password": "Contrasenya", + "rtsp_transport": "Protocol de transport RTSP", + "still_image_url": "URL d'imatge fixa (p. ex. http://...)", + "stream_source": "URL origen del flux (p. ex. rtsp://...)", + "username": "Nom d'usuari", + "verify_ssl": "Verifica el certificat SSL" + }, + "description": "Introdueix la configuraci\u00f3 de connexi\u00f3 amb la c\u00e0mera." + } + } + }, + "options": { + "error": { + "already_exists": "Ja hi ha una c\u00e0mera amb aquest URL de configuraci\u00f3.", + "invalid_still_image": "L'URL no ha retornat una imatge fixa v\u00e0lida", + "no_still_image_or_stream_url": "Has d'especificar almenys una imatge un URL de flux", + "stream_file_not_found": "Fitxer no trobat mentre s'intentava connectar al flux de dades (est\u00e0 instal\u00b7lat ffmpeg?)", + "stream_http_not_found": "HTTP 404 'Not found' a l'intentar connectar-se al flux de dades ('stream')", + "stream_io_error": "Error d'entrada/sortida mentre s'intentava connectar al flux de dades. Protocol de transport RTSP incorrecte?", + "stream_no_route_to_host": "No s'ha pogut trobar l'amfitri\u00f3 mentre intentava connectar al flux de dades", + "stream_no_video": "El flux no cont\u00e9 v\u00eddeo", + "stream_not_permitted": "Operaci\u00f3 no permesa mentre s'intentava connectar al flux de dades. Protocol de transport RTSP incorrecte?", + "stream_unauthorised": "L'autoritzaci\u00f3 ha fallat mentre s'intentava connectar amb el flux de dades", + "timeout": "El temps m\u00e0xim de c\u00e0rrega de l'URL ha expirat", + "unable_still_load": "No s'ha pogut carregar cap imatge v\u00e0lida des de l'URL d'imatge fixa (pot ser per un amfitri\u00f3 o URL inv\u00e0lid o un error d'autenticaci\u00f3). Revisa els registres per a m\u00e9s informaci\u00f3.", + "unknown": "Error inesperat" + }, + "step": { + "content_type": { + "data": { + "content_type": "Tipus de contingut" + }, + "description": "Especifica el tipus de contingut per al flux de dades (stream)." + }, + "init": { + "data": { + "authentication": "Autenticaci\u00f3", + "content_type": "Tipus de contingut", + "framerate": "Freq\u00fc\u00e8ncia de visualitzaci\u00f3 (Hz)", + "limit_refetch_to_url_change": "Limita la lectura al canvi d'URL", + "password": "Contrasenya", + "rtsp_transport": "Protocol de transport RTSP", + "still_image_url": "URL d'imatge fixa (p. ex. http://...)", + "stream_source": "URL origen del flux (p. ex. rtsp://...)", + "username": "Nom d'usuari", + "verify_ssl": "Verifica el certificat SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/cs.json b/homeassistant/components/generic/translations/cs.json new file mode 100644 index 00000000000..520e0743edd --- /dev/null +++ b/homeassistant/components/generic/translations/cs.json @@ -0,0 +1,33 @@ +{ + "config": { + "step": { + "content_type": { + "data": { + "content_type": "Typ obsahu" + } + }, + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + }, + "options": { + "step": { + "content_type": { + "data": { + "content_type": "Typ obsahu" + } + }, + "init": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no", + "verify_ssl": "Ov\u011b\u0159it certifik\u00e1t SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/de.json b/homeassistant/components/generic/translations/de.json new file mode 100644 index 00000000000..849555dcc5e --- /dev/null +++ b/homeassistant/components/generic/translations/de.json @@ -0,0 +1,88 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "error": { + "already_exists": "Es existiert bereits eine Kamera mit diesen URL-Einstellungen.", + "invalid_still_image": "URL hat kein g\u00fcltiges Standbild zur\u00fcckgegeben", + "no_still_image_or_stream_url": "Du musst mindestens eine Standbild- oder Stream-URL angeben", + "stream_file_not_found": "Datei nicht gefunden beim Versuch, eine Verbindung zum Stream herzustellen (ist ffmpeg installiert?)", + "stream_http_not_found": "HTTP 404 Not found beim Versuch, eine Verbindung zum Stream herzustellen", + "stream_io_error": "Eingabe-/Ausgabefehler beim Versuch, eine Verbindung zum Stream herzustellen. Falsches RTSP-Transportprotokoll?", + "stream_no_route_to_host": "Beim Versuch, eine Verbindung zum Stream herzustellen, konnte der Host nicht gefunden werden", + "stream_no_video": "Stream enth\u00e4lt kein Video", + "stream_not_permitted": "Beim Versuch, eine Verbindung zum Stream herzustellen, ist ein Vorgang nicht zul\u00e4ssig. Falsches RTSP-Transportprotokoll?", + "stream_unauthorised": "Autorisierung beim Versuch, eine Verbindung zum Stream herzustellen, fehlgeschlagen", + "timeout": "Zeit\u00fcberschreitung beim Laden der URL", + "unable_still_load": "Es konnte kein g\u00fcltiges Bild von der Standbild-URL geladen werden (z. B. ung\u00fcltiger Host, URL oder Authentifizierungsfehler). \u00dcberpr\u00fcfe das Protokoll f\u00fcr weitere Informationen.", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "confirm": { + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" + }, + "content_type": { + "data": { + "content_type": "Inhaltstyp" + }, + "description": "Gib den Inhaltstyp des Streams an." + }, + "user": { + "data": { + "authentication": "Authentifizierung", + "content_type": "Inhaltstyp", + "framerate": "Bildfrequenz (Hz)", + "limit_refetch_to_url_change": "Neuabruf auf URL-\u00c4nderung beschr\u00e4nken", + "password": "Passwort", + "rtsp_transport": "RTSP-Transportprotokoll", + "still_image_url": "Standbild-URL (z.B. http://...)", + "stream_source": "Stream-Quell-URL (z.B. rtsp://...)", + "username": "Benutzername", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + }, + "description": "Gib die Einstellungen f\u00fcr die Verbindung mit der Kamera ein." + } + } + }, + "options": { + "error": { + "already_exists": "Es existiert bereits eine Kamera mit diesen URL-Einstellungen.", + "invalid_still_image": "URL hat kein g\u00fcltiges Standbild zur\u00fcckgegeben", + "no_still_image_or_stream_url": "Du musst mindestens eine Standbild- oder Stream-URL angeben", + "stream_file_not_found": "Datei nicht gefunden beim Versuch, eine Verbindung zum Stream herzustellen (ist ffmpeg installiert?)", + "stream_http_not_found": "HTTP 404 Not found beim Versuch, eine Verbindung zum Stream herzustellen", + "stream_io_error": "Eingabe-/Ausgabefehler beim Versuch, eine Verbindung zum Stream herzustellen. Falsches RTSP-Transportprotokoll?", + "stream_no_route_to_host": "Beim Versuch, eine Verbindung zum Stream herzustellen, konnte der Host nicht gefunden werden", + "stream_no_video": "Stream enth\u00e4lt kein Video", + "stream_not_permitted": "Beim Versuch, eine Verbindung zum Stream herzustellen, ist ein Vorgang nicht zul\u00e4ssig. Falsches RTSP-Transportprotokoll?", + "stream_unauthorised": "Autorisierung beim Versuch, eine Verbindung zum Stream herzustellen, fehlgeschlagen", + "timeout": "Zeit\u00fcberschreitung beim Laden der URL", + "unable_still_load": "Es konnte kein g\u00fcltiges Bild von der Standbild-URL geladen werden (z. B. ung\u00fcltiger Host, URL oder Authentifizierungsfehler). \u00dcberpr\u00fcfe das Protokoll f\u00fcr weitere Informationen.", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "content_type": { + "data": { + "content_type": "Inhaltstyp" + }, + "description": "Gib den Inhaltstyp des Streams an." + }, + "init": { + "data": { + "authentication": "Authentifizierung", + "content_type": "Inhaltstyp", + "framerate": "Bildfrequenz (Hz)", + "limit_refetch_to_url_change": "Neuabruf auf URL-\u00c4nderung beschr\u00e4nken", + "password": "Passwort", + "rtsp_transport": "RTSP-Transportprotokoll", + "still_image_url": "Standbild-URL (z.B. http://...)", + "stream_source": "Stream-Quell-URL (z.B. rtsp://...)", + "username": "Benutzername", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/el.json b/homeassistant/components/generic/translations/el.json new file mode 100644 index 00000000000..f3fae37f5ca --- /dev/null +++ b/homeassistant/components/generic/translations/el.json @@ -0,0 +1,88 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + }, + "error": { + "already_exists": "\u03a5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 URL.", + "invalid_still_image": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b4\u03b5\u03bd \u03b5\u03c0\u03ad\u03c3\u03c4\u03c1\u03b5\u03c8\u03b5 \u03bc\u03b9\u03b1 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1", + "no_still_image_or_stream_url": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03bc\u03b9\u03b1 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1 \u03ae \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c1\u03bf\u03ae\u03c2", + "stream_file_not_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03c1\u03bf\u03ae (\u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b5\u03c3\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf \u03c4\u03bf ffmpeg;)", + "stream_http_not_found": "HTTP 404 \u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", + "stream_io_error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5/\u03b5\u03be\u03cc\u03b4\u03bf\u03c5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae. \u039b\u03ac\u03b8\u03bf\u03c2 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP;", + "stream_no_route_to_host": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", + "stream_no_video": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b2\u03af\u03bd\u03c4\u03b5\u03bf", + "stream_not_permitted": "\u0394\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03c1\u03bf\u03ae. \u039b\u03ac\u03b8\u03bf\u03c2 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP;", + "stream_unauthorised": "\u0397 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", + "timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL", + "unable_still_load": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7\u03c2 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b1\u03ba\u03af\u03bd\u03b7\u03c4\u03b7\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 (\u03c0.\u03c7. \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2, \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03ae \u03b1\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2). \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7;" + }, + "content_type": { + "data": { + "content_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5" + }, + "description": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c1\u03bf\u03ae." + }, + "user": { + "data": { + "authentication": "\u0395\u03bb\u03ad\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "content_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5", + "framerate": "\u03a1\u03c5\u03b8\u03bc\u03cc\u03c2 \u03ba\u03b1\u03c1\u03ad (Hz)", + "limit_refetch_to_url_change": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 \u03c3\u03c4\u03b7\u03bd \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae url", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "rtsp_transport": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP", + "still_image_url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 (\u03c0.\u03c7. http://...)", + "stream_source": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c0\u03b7\u03b3\u03ae\u03c2 \u03c1\u03bf\u03ae\u03c2 (\u03c0.\u03c7. rtsp://...)", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", + "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1." + } + } + }, + "options": { + "error": { + "already_exists": "\u03a5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 URL.", + "invalid_still_image": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b4\u03b5\u03bd \u03b5\u03c0\u03ad\u03c3\u03c4\u03c1\u03b5\u03c8\u03b5 \u03bc\u03b9\u03b1 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1", + "no_still_image_or_stream_url": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03bc\u03b9\u03b1 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1 \u03ae \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c1\u03bf\u03ae\u03c2", + "stream_file_not_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03c1\u03bf\u03ae (\u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b5\u03c3\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf \u03c4\u03bf ffmpeg;)", + "stream_http_not_found": "HTTP 404 \u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", + "stream_io_error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5/\u03b5\u03be\u03cc\u03b4\u03bf\u03c5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae. \u039b\u03ac\u03b8\u03bf\u03c2 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP;", + "stream_no_route_to_host": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", + "stream_no_video": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b2\u03af\u03bd\u03c4\u03b5\u03bf", + "stream_not_permitted": "\u0394\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03c1\u03bf\u03ae. \u039b\u03ac\u03b8\u03bf\u03c2 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP;", + "stream_unauthorised": "\u0397 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", + "timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL", + "unable_still_load": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7\u03c2 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b1\u03ba\u03af\u03bd\u03b7\u03c4\u03b7\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 (\u03c0.\u03c7. \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2, \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03ae \u03b1\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2). \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "content_type": { + "data": { + "content_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5" + }, + "description": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c1\u03bf\u03ae." + }, + "init": { + "data": { + "authentication": "\u0395\u03bb\u03ad\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "content_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5", + "framerate": "\u03a1\u03c5\u03b8\u03bc\u03cc\u03c2 \u03ba\u03b1\u03c1\u03ad (Hz)", + "limit_refetch_to_url_change": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 \u03c3\u03c4\u03b7\u03bd \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae url", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "rtsp_transport": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP", + "still_image_url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 (\u03c0.\u03c7. http://...)", + "stream_source": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c0\u03b7\u03b3\u03ae\u03c2 \u03c1\u03bf\u03ae\u03c2 (\u03c0.\u03c7. rtsp://...)", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", + "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json index 478ea76b476..b158488f178 100644 --- a/homeassistant/components/generic/translations/en.json +++ b/homeassistant/components/generic/translations/en.json @@ -32,6 +32,7 @@ "user": { "data": { "authentication": "Authentication", + "content_type": "Content Type", "framerate": "Frame Rate (Hz)", "limit_refetch_to_url_change": "Limit refetch to url change", "password": "Password", @@ -71,6 +72,7 @@ "init": { "data": { "authentication": "Authentication", + "content_type": "Content Type", "framerate": "Frame Rate (Hz)", "limit_refetch_to_url_change": "Limit refetch to url change", "password": "Password", diff --git a/homeassistant/components/generic/translations/et.json b/homeassistant/components/generic/translations/et.json new file mode 100644 index 00000000000..ae6f5aa75f8 --- /dev/null +++ b/homeassistant/components/generic/translations/et.json @@ -0,0 +1,88 @@ +{ + "config": { + "abort": { + "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet", + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + }, + "error": { + "already_exists": "Nende URL-i seadetega kaamera on juba olemas.", + "invalid_still_image": "URL ei tagastanud kehtivat pilti", + "no_still_image_or_stream_url": "Pead m\u00e4\u00e4rama v\u00e4hemalt liikumatu pildi v\u00f5i voo URL-i", + "stream_file_not_found": "Vooga \u00fchenduse loomisel ei leitud faili (kas ffmpeg on installitud?)", + "stream_http_not_found": "HTTP 404 viga kui \u00fcritatakse vooga \u00fchendust luua", + "stream_io_error": "Sisend-/v\u00e4ljundviga vooga \u00fchenduse loomisel. Vale RTSP transpordiprotokoll?", + "stream_no_route_to_host": "Vooga \u00fchenduse loomisel ei leitud hosti", + "stream_no_video": "Voos pole videot", + "stream_not_permitted": "Vooga \u00fchenduse loomisel pole toiming lubatud. Vale RTSP transpordiprotokoll?", + "stream_unauthorised": "Autoriseerimine eba\u00f5nnestus vooga \u00fchendamise ajal", + "timeout": "URL-i laadimise ajal\u00f5pp", + "unable_still_load": "Pilti ei saa laadida URL-ist (nt kehtetu host, URL v\u00f5i autentimise t\u00f5rge). Lisateabe saamiseks vaata logi.", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "confirm": { + "description": "Kas alustada seadistamist?" + }, + "content_type": { + "data": { + "content_type": "Sisu t\u00fc\u00fcp" + }, + "description": "M\u00e4\u00e4ra voo sisut\u00fc\u00fcp." + }, + "user": { + "data": { + "authentication": "Autentimine", + "content_type": "Sisu t\u00fc\u00fcp", + "framerate": "Kaadrisagedus (Hz)", + "limit_refetch_to_url_change": "Piira laadimist URL-i muutmiseni", + "password": "Salas\u00f5na", + "rtsp_transport": "RTSP transpordiprotokoll", + "still_image_url": "Pildi URL (nt http://...)", + "stream_source": "Voo allikas URL (nt rtsp://...)", + "username": "Kasutajanimi", + "verify_ssl": "Kontrolli SSL sertifikaati" + }, + "description": "Sisesta s\u00e4tted kaameraga \u00fchenduse loomiseks." + } + } + }, + "options": { + "error": { + "already_exists": "Nende URL-i seadetega kaamera on juba olemas.", + "invalid_still_image": "URL ei tagastanud kehtivat pilti", + "no_still_image_or_stream_url": "Pead m\u00e4\u00e4rama v\u00e4hemalt liikumatu pildi v\u00f5i voo URL-i", + "stream_file_not_found": "Vooga \u00fchenduse loomisel ei leitud faili (kas ffmpeg on installitud?)", + "stream_http_not_found": "HTTP 404 viga kui \u00fcritatakse vooga \u00fchendust luua", + "stream_io_error": "Sisend-/v\u00e4ljundviga vooga \u00fchenduse loomisel. Vale RTSP transpordiprotokoll?", + "stream_no_route_to_host": "Vooga \u00fchenduse loomisel ei leitud hosti", + "stream_no_video": "Voos pole videot", + "stream_not_permitted": "Vooga \u00fchenduse loomisel pole toiming lubatud. Vale RTSP transpordiprotokoll?", + "stream_unauthorised": "Autoriseerimine eba\u00f5nnestus vooga \u00fchendamise ajal", + "timeout": "URL-i laadimise ajal\u00f5pp", + "unable_still_load": "Pilti ei saa laadida URL-ist (nt kehtetu host, URL v\u00f5i autentimise t\u00f5rge). Lisateabe saamiseks vaata logi.", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "content_type": { + "data": { + "content_type": "Sisu t\u00fc\u00fcp" + }, + "description": "M\u00e4\u00e4ra voo sisut\u00fc\u00fcp." + }, + "init": { + "data": { + "authentication": "Autentimine", + "content_type": "Sisu t\u00fc\u00fcp", + "framerate": "Kaadrisagedus (Hz)", + "limit_refetch_to_url_change": "Piira laadimist URL-i muutmiseni", + "password": "Salas\u00f5na", + "rtsp_transport": "RTSP transpordiprotokoll", + "still_image_url": "Pildi URL (nt http://...)", + "stream_source": "Voo allikas URL (nt rtsp://...)", + "username": "Kasutajanimi", + "verify_ssl": "Kontrolli SSL sertifikaati" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/fr.json b/homeassistant/components/generic/translations/fr.json new file mode 100644 index 00000000000..4905afb64e7 --- /dev/null +++ b/homeassistant/components/generic/translations/fr.json @@ -0,0 +1,88 @@ +{ + "config": { + "abort": { + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "error": { + "already_exists": "Une cam\u00e9ra avec ces param\u00e8tres d'URL existe d\u00e9j\u00e0.", + "invalid_still_image": "L'URL n'a pas renvoy\u00e9 d'image fixe valide", + "no_still_image_or_stream_url": "Vous devez au moins renseigner une URL d'image fixe ou de flux", + "stream_file_not_found": "Fichier non trouv\u00e9 lors de la tentative de connexion au flux (ffmpeg est-il install\u00e9\u00a0?)", + "stream_http_not_found": "Erreur\u00a0404 (introuvable) lors de la tentative de connexion au flux", + "stream_io_error": "Erreur d'entr\u00e9e/sortie lors de la tentative de connexion au flux. Mauvais protocole de transport RTSP\u00a0?", + "stream_no_route_to_host": "Impossible de trouver l'h\u00f4te lors de la tentative de connexion au flux", + "stream_no_video": "Le flux ne contient pas de vid\u00e9o", + "stream_not_permitted": "Op\u00e9ration non autoris\u00e9e lors de la tentative de connexion au flux. Mauvais protocole de transport RTSP\u00a0?", + "stream_unauthorised": "\u00c9chec de l'autorisation lors de la tentative de connexion au flux", + "timeout": "D\u00e9lai d'attente expir\u00e9 lors du chargement de l'URL", + "unable_still_load": "Impossible de charger une image valide depuis l'URL d'image fixe (cela pourrait \u00eatre d\u00fb \u00e0 un h\u00f4te ou \u00e0 une URL non valide, ou \u00e0 un \u00e9chec de l'authentification). Consultez le journal pour plus d'informations.", + "unknown": "Erreur inattendue" + }, + "step": { + "confirm": { + "description": "Voulez-vous commencer la configuration\u00a0?" + }, + "content_type": { + "data": { + "content_type": "Type de contenu" + }, + "description": "Sp\u00e9cifiez le type de contenu du flux." + }, + "user": { + "data": { + "authentication": "Authentification", + "content_type": "Type de contenu", + "framerate": "Fr\u00e9quence d'images (en hertz)", + "limit_refetch_to_url_change": "Limiter la r\u00e9cup\u00e9ration aux changements d'URL", + "password": "Mot de passe", + "rtsp_transport": "Protocole de transport RTSP", + "still_image_url": "URL d'image fixe (par exemple, http://\u2026)", + "stream_source": "URL de la source du flux (par exemple, rtsp://\u2026)", + "username": "Nom d'utilisateur", + "verify_ssl": "V\u00e9rifier le certificat SSL" + }, + "description": "Saisissez les param\u00e8tres de connexion \u00e0 la cam\u00e9ra." + } + } + }, + "options": { + "error": { + "already_exists": "Une cam\u00e9ra avec ces param\u00e8tres d'URL existe d\u00e9j\u00e0.", + "invalid_still_image": "L'URL n'a pas renvoy\u00e9 d'image fixe valide", + "no_still_image_or_stream_url": "Vous devez au moins renseigner une URL d'image fixe ou de flux", + "stream_file_not_found": "Fichier non trouv\u00e9 lors de la tentative de connexion au flux (ffmpeg est-il install\u00e9\u00a0?)", + "stream_http_not_found": "Erreur\u00a0404 (introuvable) lors de la tentative de connexion au flux", + "stream_io_error": "Erreur d'entr\u00e9e/sortie lors de la tentative de connexion au flux. Mauvais protocole de transport RTSP\u00a0?", + "stream_no_route_to_host": "Impossible de trouver l'h\u00f4te lors de la tentative de connexion au flux", + "stream_no_video": "Le flux ne contient pas de vid\u00e9o", + "stream_not_permitted": "Op\u00e9ration non autoris\u00e9e lors de la tentative de connexion au flux. Mauvais protocole de transport RTSP\u00a0?", + "stream_unauthorised": "\u00c9chec de l'autorisation lors de la tentative de connexion au flux", + "timeout": "D\u00e9lai d'attente expir\u00e9 lors du chargement de l'URL", + "unable_still_load": "Impossible de charger une image valide depuis l'URL d'image fixe (cela pourrait \u00eatre d\u00fb \u00e0 un h\u00f4te ou \u00e0 une URL non valide, ou \u00e0 un \u00e9chec de l'authentification). Consultez le journal pour plus d'informations.", + "unknown": "Erreur inattendue" + }, + "step": { + "content_type": { + "data": { + "content_type": "Type de contenu" + }, + "description": "Sp\u00e9cifiez le type de contenu du flux." + }, + "init": { + "data": { + "authentication": "Authentification", + "content_type": "Type de contenu", + "framerate": "Fr\u00e9quence d'images (en hertz)", + "limit_refetch_to_url_change": "Limiter la r\u00e9cup\u00e9ration aux changements d'URL", + "password": "Mot de passe", + "rtsp_transport": "Protocole de transport RTSP", + "still_image_url": "URL d'image fixe (par exemple, http://\u2026)", + "stream_source": "URL de la source du flux (par exemple, rtsp://\u2026)", + "username": "Nom d'utilisateur", + "verify_ssl": "V\u00e9rifier le certificat SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/he.json b/homeassistant/components/generic/translations/he.json new file mode 100644 index 00000000000..b20b0ea88a7 --- /dev/null +++ b/homeassistant/components/generic/translations/he.json @@ -0,0 +1,88 @@ +{ + "config": { + "abort": { + "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": { + "already_exists": "\u05de\u05e6\u05dc\u05de\u05d4 \u05e2\u05dd \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05d5 \u05db\u05d1\u05e8 \u05e7\u05d9\u05d9\u05de\u05ea.", + "invalid_still_image": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05dc\u05d0 \u05d4\u05d7\u05d6\u05d9\u05e8\u05d4 \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 \u05d7\u05d5\u05e7\u05d9\u05ea", + "no_still_image_or_stream_url": "\u05d9\u05e9 \u05dc\u05e6\u05d9\u05d9\u05df \u05dc\u05e4\u05d7\u05d5\u05ea \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05d4\u05d6\u05e8\u05de\u05d4", + "stream_file_not_found": "\u05d4\u05e7\u05d5\u05d1\u05e5 \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4 (\u05d4\u05d0\u05dd ffmpeg \u05de\u05d5\u05ea\u05e7\u05df?)", + "stream_http_not_found": "HTTP 404 \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", + "stream_io_error": "\u05e9\u05d2\u05d9\u05d0\u05ea \u05e7\u05dc\u05d8/\u05e4\u05dc\u05d8 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4. \u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 \u05e9\u05d2\u05d5\u05d9 \u05e9\u05dc RTSP?", + "stream_no_route_to_host": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05d4\u05d9\u05d4 \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05ea \u05d4\u05de\u05d0\u05e8\u05d7 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", + "stream_no_video": "\u05d0\u05d9\u05df \u05d5\u05d9\u05d3\u05d9\u05d0\u05d5 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", + "stream_not_permitted": "\u05d4\u05e4\u05e2\u05d5\u05dc\u05d4 \u05d0\u05d9\u05e0\u05d4 \u05de\u05d5\u05ea\u05e8\u05ea \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4. \u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 \u05e9\u05d2\u05d5\u05d9 \u05e9\u05dc RTSP?", + "stream_unauthorised": "\u05d4\u05d4\u05e8\u05e9\u05d0\u05d4 \u05e0\u05db\u05e9\u05dc\u05d4 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", + "timeout": "\u05d6\u05de\u05df \u05e7\u05e6\u05d5\u05d1 \u05d1\u05e2\u05ea \u05d8\u05e2\u05d9\u05e0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", + "unable_still_load": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d8\u05e2\u05d5\u05df \u05ea\u05de\u05d5\u05e0\u05d4 \u05d7\u05d5\u05e7\u05d9\u05ea \u05de\u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05e9\u05dc \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, \u05db\u05e9\u05dc \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9 \u05d1\u05de\u05d7\u05e9\u05d1 \u05de\u05d0\u05e8\u05d7, \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d0\u05d5 \u05d0\u05d9\u05de\u05d5\u05ea). \u05e0\u05d0 \u05dc\u05e2\u05d9\u05d9\u05df \u05d1\u05d9\u05d5\u05de\u05df \u05d4\u05e8\u05d9\u05e9\u05d5\u05dd \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + }, + "content_type": { + "data": { + "content_type": "\u05e1\u05d5\u05d2 \u05ea\u05d5\u05db\u05df" + }, + "description": "\u05e0\u05d0 \u05dc\u05e6\u05d9\u05d9\u05df \u05d0\u05ea \u05e1\u05d5\u05d2 \u05d4\u05ea\u05d5\u05db\u05df \u05e2\u05d1\u05d5\u05e8 \u05d4\u05d6\u05e8\u05dd." + }, + "user": { + "data": { + "authentication": "\u05d0\u05d9\u05de\u05d5\u05ea", + "content_type": "\u05e1\u05d5\u05d2 \u05ea\u05d5\u05db\u05df", + "framerate": "\u05e7\u05e6\u05d1 \u05e4\u05e8\u05d9\u05d9\u05de\u05d9\u05dd (\u05d4\u05e8\u05e5)", + "limit_refetch_to_url_change": "\u05d4\u05d2\u05d1\u05dc\u05d4 \u05e9\u05dc \u05d0\u05d7\u05e1\u05d5\u05df \u05d7\u05d5\u05d6\u05e8 \u05dc\u05e9\u05d9\u05e0\u05d5\u05d9 \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "rtsp_transport": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 RTSP", + "still_image_url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, http://...)", + "stream_source": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05de\u05e7\u05d5\u05e8 \u05d6\u05e8\u05dd (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, rtsp://...)", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + }, + "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05db\u05d3\u05d9 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05e6\u05dc\u05de\u05d4." + } + } + }, + "options": { + "error": { + "already_exists": "\u05de\u05e6\u05dc\u05de\u05d4 \u05e2\u05dd \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05d5 \u05db\u05d1\u05e8 \u05e7\u05d9\u05d9\u05de\u05ea.", + "invalid_still_image": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05dc\u05d0 \u05d4\u05d7\u05d6\u05d9\u05e8\u05d4 \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 \u05d7\u05d5\u05e7\u05d9\u05ea", + "no_still_image_or_stream_url": "\u05d9\u05e9 \u05dc\u05e6\u05d9\u05d9\u05df \u05dc\u05e4\u05d7\u05d5\u05ea \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05d4\u05d6\u05e8\u05de\u05d4", + "stream_file_not_found": "\u05d4\u05e7\u05d5\u05d1\u05e5 \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4 (\u05d4\u05d0\u05dd ffmpeg \u05de\u05d5\u05ea\u05e7\u05df?)", + "stream_http_not_found": "HTTP 404 \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", + "stream_io_error": "\u05e9\u05d2\u05d9\u05d0\u05ea \u05e7\u05dc\u05d8/\u05e4\u05dc\u05d8 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4. \u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 \u05e9\u05d2\u05d5\u05d9 \u05e9\u05dc RTSP?", + "stream_no_route_to_host": "\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05d4\u05d9\u05d4 \u05dc\u05de\u05e6\u05d5\u05d0 \u05d0\u05ea \u05d4\u05de\u05d0\u05e8\u05d7 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", + "stream_no_video": "\u05d0\u05d9\u05df \u05d5\u05d9\u05d3\u05d9\u05d0\u05d5 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", + "stream_not_permitted": "\u05d4\u05e4\u05e2\u05d5\u05dc\u05d4 \u05d0\u05d9\u05e0\u05d4 \u05de\u05d5\u05ea\u05e8\u05ea \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4. \u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 \u05e9\u05d2\u05d5\u05d9 \u05e9\u05dc RTSP?", + "stream_unauthorised": "\u05d4\u05d4\u05e8\u05e9\u05d0\u05d4 \u05e0\u05db\u05e9\u05dc\u05d4 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", + "timeout": "\u05d6\u05de\u05df \u05e7\u05e6\u05d5\u05d1 \u05d1\u05e2\u05ea \u05d8\u05e2\u05d9\u05e0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", + "unable_still_load": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d8\u05e2\u05d5\u05df \u05ea\u05de\u05d5\u05e0\u05d4 \u05d7\u05d5\u05e7\u05d9\u05ea \u05de\u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05e9\u05dc \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, \u05db\u05e9\u05dc \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9 \u05d1\u05de\u05d7\u05e9\u05d1 \u05de\u05d0\u05e8\u05d7, \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d0\u05d5 \u05d0\u05d9\u05de\u05d5\u05ea). \u05e0\u05d0 \u05dc\u05e2\u05d9\u05d9\u05df \u05d1\u05d9\u05d5\u05de\u05df \u05d4\u05e8\u05d9\u05e9\u05d5\u05dd \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "content_type": { + "data": { + "content_type": "\u05e1\u05d5\u05d2 \u05ea\u05d5\u05db\u05df" + }, + "description": "\u05e0\u05d0 \u05dc\u05e6\u05d9\u05d9\u05df \u05d0\u05ea \u05e1\u05d5\u05d2 \u05d4\u05ea\u05d5\u05db\u05df \u05e2\u05d1\u05d5\u05e8 \u05d4\u05d6\u05e8\u05dd." + }, + "init": { + "data": { + "authentication": "\u05d0\u05d9\u05de\u05d5\u05ea", + "content_type": "\u05e1\u05d5\u05d2 \u05ea\u05d5\u05db\u05df", + "framerate": "\u05e7\u05e6\u05d1 \u05e4\u05e8\u05d9\u05d9\u05de\u05d9\u05dd (\u05d4\u05e8\u05e5)", + "limit_refetch_to_url_change": "\u05d4\u05d2\u05d1\u05dc\u05d4 \u05e9\u05dc \u05d0\u05d7\u05e1\u05d5\u05df \u05d7\u05d5\u05d6\u05e8 \u05dc\u05e9\u05d9\u05e0\u05d5\u05d9 \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "rtsp_transport": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 RTSP", + "still_image_url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, http://...)", + "stream_source": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05de\u05e7\u05d5\u05e8 \u05d6\u05e8\u05dd (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, rtsp://...)", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/hu.json b/homeassistant/components/generic/translations/hu.json new file mode 100644 index 00000000000..4e97e594d1e --- /dev/null +++ b/homeassistant/components/generic/translations/hu.json @@ -0,0 +1,88 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "already_exists": "M\u00e1r l\u00e9tezik egy kamera ezekkel az URL-be\u00e1ll\u00edt\u00e1sokkal.", + "invalid_still_image": "Az URL nem adott vissza \u00e9rv\u00e9nyes \u00e1ll\u00f3k\u00e9pet", + "no_still_image_or_stream_url": "Legal\u00e1bb egy \u00e1ll\u00f3k\u00e9pet vagy stream URL-c\u00edmet kell megadnia.", + "stream_file_not_found": "F\u00e1jl nem tal\u00e1lhat\u00f3 a streamhez val\u00f3 csatlakoz\u00e1s sor\u00e1n (telep\u00edtve van az ffmpeg?)", + "stream_http_not_found": "HTTP 404 Not found - hiba az adatfolyamhoz val\u00f3 csatlakoz\u00e1s k\u00f6zben", + "stream_io_error": "Bemeneti/kimeneti hiba t\u00f6rt\u00e9nt az adatfolyamhoz val\u00f3 kapcsol\u00f3d\u00e1s k\u00f6zben. Rossz RTSP sz\u00e1ll\u00edt\u00e1si protokoll?", + "stream_no_route_to_host": "Nem tal\u00e1lhat\u00f3 a c\u00edm, mik\u00f6zben a rendszer az adatfolyamhoz pr\u00f3b\u00e1l csatlakozni", + "stream_no_video": "Az adatfolyamban nincs vide\u00f3", + "stream_not_permitted": "A m\u0171velet nem enged\u00e9lyezett, mik\u00f6zben megpr\u00f3b\u00e1l csatlakozni a folyamhoz. Rossz fajta RTSP protokoll?", + "stream_unauthorised": "A hiteles\u00edt\u00e9s meghi\u00fasult, mik\u00f6zben megpr\u00f3b\u00e1lt csatlakozni az adatfolyamhoz", + "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az URL bet\u00f6lt\u00e9se k\u00f6zben", + "unable_still_load": "Nem siker\u00fclt \u00e9rv\u00e9nyes k\u00e9pet bet\u00f6lteni az \u00e1ll\u00f3k\u00e9p URL-c\u00edm\u00e9r\u0151l (pl. \u00e9rv\u00e9nytelen host, URL vagy hiteles\u00edt\u00e9si hiba). Tov\u00e1bbi inform\u00e1ci\u00f3\u00e9rt tekintse \u00e1t a napl\u00f3t.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "confirm": { + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" + }, + "content_type": { + "data": { + "content_type": "Tartalom t\u00edpus" + }, + "description": "Az adatfolyam tartalomt\u00edpua (Content-Type)." + }, + "user": { + "data": { + "authentication": "Hiteles\u00edt\u00e9s", + "content_type": "Tartalom t\u00edpusa", + "framerate": "K\u00e9pkockasebess\u00e9g (Hz)", + "limit_refetch_to_url_change": "Korl\u00e1tozza a visszah\u00edv\u00e1st az url v\u00e1ltoz\u00e1sra", + "password": "Jelsz\u00f3", + "rtsp_transport": "RTSP protokoll", + "still_image_url": "\u00c1ll\u00f3k\u00e9p URL (pl. http://...)", + "stream_source": "Mozg\u00f3k\u00e9p adatfolyam URL (pl. rtsp://...)", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + }, + "description": "Adja meg a kamer\u00e1hoz val\u00f3 csatlakoz\u00e1s be\u00e1ll\u00edt\u00e1sait." + } + } + }, + "options": { + "error": { + "already_exists": "M\u00e1r l\u00e9tezik egy kamera ezekkel az URL-be\u00e1ll\u00edt\u00e1sokkal.", + "invalid_still_image": "Az URL nem adott vissza \u00e9rv\u00e9nyes \u00e1ll\u00f3k\u00e9pet", + "no_still_image_or_stream_url": "Legal\u00e1bb egy \u00e1ll\u00f3k\u00e9pet vagy stream URL-c\u00edmet kell megadnia.", + "stream_file_not_found": "F\u00e1jl nem tal\u00e1lhat\u00f3 a streamhez val\u00f3 csatlakoz\u00e1s sor\u00e1n (telep\u00edtve van az ffmpeg?)", + "stream_http_not_found": "HTTP 404 Not found - hiba az adatfolyamhoz val\u00f3 csatlakoz\u00e1s k\u00f6zben", + "stream_io_error": "Bemeneti/kimeneti hiba t\u00f6rt\u00e9nt az adatfolyamhoz val\u00f3 kapcsol\u00f3d\u00e1s k\u00f6zben. Rossz RTSP sz\u00e1ll\u00edt\u00e1si protokoll?", + "stream_no_route_to_host": "Nem tal\u00e1lhat\u00f3 a c\u00edm, mik\u00f6zben a rendszer az adatfolyamhoz pr\u00f3b\u00e1l csatlakozni", + "stream_no_video": "Az adatfolyamban nincs vide\u00f3", + "stream_not_permitted": "A m\u0171velet nem enged\u00e9lyezett, mik\u00f6zben megpr\u00f3b\u00e1l csatlakozni a folyamhoz. Rossz fajta RTSP protokoll?", + "stream_unauthorised": "A hiteles\u00edt\u00e9s meghi\u00fasult, mik\u00f6zben megpr\u00f3b\u00e1lt csatlakozni az adatfolyamhoz", + "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az URL bet\u00f6lt\u00e9se k\u00f6zben", + "unable_still_load": "Nem siker\u00fclt \u00e9rv\u00e9nyes k\u00e9pet bet\u00f6lteni az \u00e1ll\u00f3k\u00e9p URL-c\u00edm\u00e9r\u0151l (pl. \u00e9rv\u00e9nytelen host, URL vagy hiteles\u00edt\u00e9si hiba). Tov\u00e1bbi inform\u00e1ci\u00f3\u00e9rt tekintse \u00e1t a napl\u00f3t.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "content_type": { + "data": { + "content_type": "Tartalom t\u00edpus" + }, + "description": "Az adatfolyam tartalomt\u00edpua (Content-Type)." + }, + "init": { + "data": { + "authentication": "Hiteles\u00edt\u00e9s", + "content_type": "Tartalom t\u00edpusa", + "framerate": "K\u00e9pkockasebess\u00e9g (Hz)", + "limit_refetch_to_url_change": "Korl\u00e1tozza a visszah\u00edv\u00e1st az url v\u00e1ltoz\u00e1sra", + "password": "Jelsz\u00f3", + "rtsp_transport": "RTSP protokoll", + "still_image_url": "\u00c1ll\u00f3k\u00e9p URL (pl. http://...)", + "stream_source": "Mozg\u00f3k\u00e9p adatfolyam URL (pl. rtsp://...)", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/id.json b/homeassistant/components/generic/translations/id.json new file mode 100644 index 00000000000..4b3106ad1df --- /dev/null +++ b/homeassistant/components/generic/translations/id.json @@ -0,0 +1,88 @@ +{ + "config": { + "abort": { + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "already_exists": "Kamera dengan setelan URL ini sudah ada.", + "invalid_still_image": "URL tidak mengembalikan gambar diam yang valid", + "no_still_image_or_stream_url": "Anda harus menentukan setidaknya gambar diam atau URL streaming", + "stream_file_not_found": "File tidak ditemukan saat mencoba menyambung ke streaming (sudahkah ffmpeg diinstal?)", + "stream_http_not_found": "HTTP 404 Tidak ditemukan saat mencoba menyambung ke streaming", + "stream_io_error": "Kesalahan Input/Output saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", + "stream_no_route_to_host": "Tidak dapat menemukan host saat mencoba menyambung ke streaming", + "stream_no_video": "Streaming tidak memiliki video", + "stream_not_permitted": "Operasi tidak diizinkan saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", + "stream_unauthorised": "Otorisasi gagal saat mencoba menyambung ke streaming", + "timeout": "Tenggang waktu habis saat memuat URL", + "unable_still_load": "Tidak dapat memuat gambar yang valid dari URL gambar diam (mis. host yang tidak valid, URL, atau kegagalan autentikasi). Tinjau log untuk info lebih lanjut.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "confirm": { + "description": "Ingin memulai penyiapan?" + }, + "content_type": { + "data": { + "content_type": "Jenis Konten" + }, + "description": "Tentukan jenis konten untuk streaming." + }, + "user": { + "data": { + "authentication": "Autentikasi", + "content_type": "Jenis Konten", + "framerate": "Frame Rate (Hz)", + "limit_refetch_to_url_change": "Batasi pengambilan ulang untuk perubahan URL", + "password": "Kata Sandi", + "rtsp_transport": "Protokol transportasi RTSP", + "still_image_url": "URL Gambar Diam (mis. http://...)", + "stream_source": "URL Sumber Streaming (mis. rtsp://...)", + "username": "Nama Pengguna", + "verify_ssl": "Verifikasi sertifikat SSL" + }, + "description": "Masukkan pengaturan untuk terhubung ke kamera." + } + } + }, + "options": { + "error": { + "already_exists": "Kamera dengan setelan URL ini sudah ada.", + "invalid_still_image": "URL tidak mengembalikan gambar diam yang valid", + "no_still_image_or_stream_url": "Anda harus menentukan setidaknya gambar diam atau URL streaming", + "stream_file_not_found": "File tidak ditemukan saat mencoba menyambung ke streaming (sudahkah ffmpeg diinstal?)", + "stream_http_not_found": "HTTP 404 Tidak ditemukan saat mencoba menyambung ke streaming", + "stream_io_error": "Kesalahan Input/Output saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", + "stream_no_route_to_host": "Tidak dapat menemukan host saat mencoba menyambung ke streaming", + "stream_no_video": "Streaming tidak memiliki video", + "stream_not_permitted": "Operasi tidak diizinkan saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", + "stream_unauthorised": "Otorisasi gagal saat mencoba menyambung ke streaming", + "timeout": "Tenggang waktu habis saat memuat URL", + "unable_still_load": "Tidak dapat memuat gambar yang valid dari URL gambar diam (mis. host yang tidak valid, URL, atau kegagalan autentikasi). Tinjau log untuk info lebih lanjut.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "content_type": { + "data": { + "content_type": "Jenis Konten" + }, + "description": "Tentukan jenis konten untuk streaming." + }, + "init": { + "data": { + "authentication": "Autentikasi", + "content_type": "Jenis Konten", + "framerate": "Frame Rate (Hz)", + "limit_refetch_to_url_change": "Batasi pengambilan ulang untuk perubahan URL", + "password": "Kata Sandi", + "rtsp_transport": "Protokol transportasi RTSP", + "still_image_url": "URL Gambar Diam (mis. http://...)", + "stream_source": "URL Sumber Streaming (mis. rtsp://...)", + "username": "Nama Pengguna", + "verify_ssl": "Verifikasi sertifikat SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/it.json b/homeassistant/components/generic/translations/it.json new file mode 100644 index 00000000000..880fcff500b --- /dev/null +++ b/homeassistant/components/generic/translations/it.json @@ -0,0 +1,88 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "error": { + "already_exists": "Esiste gi\u00e0 una telecamera con queste impostazioni URL.", + "invalid_still_image": "L'URL non ha restituito un'immagine fissa valida", + "no_still_image_or_stream_url": "Devi specificare almeno un'immagine fissa o un URL di un flusso", + "stream_file_not_found": "File non trovato durante il tentativo di connessione al (\u00e8 installato ffmpeg?)", + "stream_http_not_found": "HTTP 404 Non trovato durante il tentativo di connessione al flusso", + "stream_io_error": "Errore di input/output durante il tentativo di connessione al flusso. Protocollo di trasporto RTSP errato?", + "stream_no_route_to_host": "Impossibile trovare l'host durante il tentativo di connessione al flusso", + "stream_no_video": "Il flusso non ha video", + "stream_not_permitted": "Operazione non consentita durante il tentativo di connessione al . Protocollo di trasporto RTSP errato?", + "stream_unauthorised": "Autorizzazione non riuscita durante il tentativo di connessione al flusso", + "timeout": "Timeout durante il caricamento dell'URL", + "unable_still_load": "Impossibile caricare un'immagine valida dall'URL dell'immagine fissa (ad es. host, URL non valido o errore di autenticazione). Esamina il registro per ulteriori informazioni.", + "unknown": "Errore imprevisto" + }, + "step": { + "confirm": { + "description": "Vuoi iniziare la configurazione?" + }, + "content_type": { + "data": { + "content_type": "Tipo di contenuto" + }, + "description": "Specificare il tipo di contenuto per il flusso." + }, + "user": { + "data": { + "authentication": "Autenticazione", + "content_type": "Tipo di contenuto", + "framerate": "Frequenza fotogrammi (Hz)", + "limit_refetch_to_url_change": "Limita il recupero alla modifica dell'URL", + "password": "Password", + "rtsp_transport": "Protocollo di trasporto RTSP", + "still_image_url": "URL immagine fissa (ad es. http://...)", + "stream_source": "URL sorgente del flusso (ad es. rtsp://...)", + "username": "Nome utente", + "verify_ssl": "Verifica il certificato SSL" + }, + "description": "Inserisci le impostazioni per connetterti alla fotocamera." + } + } + }, + "options": { + "error": { + "already_exists": "Esiste gi\u00e0 una telecamera con queste impostazioni URL.", + "invalid_still_image": "L'URL non ha restituito un'immagine fissa valida", + "no_still_image_or_stream_url": "Devi specificare almeno un'immagine fissa o un URL di un flusso", + "stream_file_not_found": "File non trovato durante il tentativo di connessione al (\u00e8 installato ffmpeg?)", + "stream_http_not_found": "HTTP 404 Non trovato durante il tentativo di connessione al flusso", + "stream_io_error": "Errore di input/output durante il tentativo di connessione al flusso. Protocollo di trasporto RTSP errato?", + "stream_no_route_to_host": "Impossibile trovare l'host durante il tentativo di connessione al flusso", + "stream_no_video": "Il flusso non ha video", + "stream_not_permitted": "Operazione non consentita durante il tentativo di connessione al . Protocollo di trasporto RTSP errato?", + "stream_unauthorised": "Autorizzazione non riuscita durante il tentativo di connessione al flusso", + "timeout": "Timeout durante il caricamento dell'URL", + "unable_still_load": "Impossibile caricare un'immagine valida dall'URL dell'immagine fissa (ad es. host, URL non valido o errore di autenticazione). Esamina il registro per ulteriori informazioni.", + "unknown": "Errore imprevisto" + }, + "step": { + "content_type": { + "data": { + "content_type": "Tipo di contenuto" + }, + "description": "Specificare il tipo di contenuto per il flusso." + }, + "init": { + "data": { + "authentication": "Autenticazione", + "content_type": "Tipo di contenuto", + "framerate": "Frequenza fotogrammi (Hz)", + "limit_refetch_to_url_change": "Limita il recupero alla modifica dell'URL", + "password": "Password", + "rtsp_transport": "Protocollo di trasporto RTSP", + "still_image_url": "URL immagine fissa (ad es. http://...)", + "stream_source": "URL sorgente del flusso (ad es. rtsp://...)", + "username": "Nome utente", + "verify_ssl": "Verifica il certificato SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/ja.json b/homeassistant/components/generic/translations/ja.json new file mode 100644 index 00000000000..916142125e6 --- /dev/null +++ b/homeassistant/components/generic/translations/ja.json @@ -0,0 +1,88 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "error": { + "already_exists": "\u3053\u306eURL\u8a2d\u5b9a\u306e\u30ab\u30e1\u30e9\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059\u3002", + "invalid_still_image": "URL\u304c\u6709\u52b9\u306a\u9759\u6b62\u753b\u50cf\u3092\u8fd4\u3057\u307e\u305b\u3093\u3067\u3057\u305f", + "no_still_image_or_stream_url": "\u9759\u6b62\u753b\u50cf\u3082\u3057\u304f\u306f\u3001\u30b9\u30c8\u30ea\u30fc\u30e0URL\u306e\u3069\u3061\u3089\u304b\u3092\u6307\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "stream_file_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u3068\u304d\u306b\u30d5\u30a1\u30a4\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093(ffmpeg\u304c\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u3066\u3044\u307e\u3059\u304b\uff1f)", + "stream_http_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u3078\u306e\u63a5\u7d9a\u6642\u306b\u3001HTTP 404\u3067\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "stream_io_error": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u305f\u3068\u304d\u306b\u5165\u51fa\u529b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002RTSP\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30d7\u30ed\u30c8\u30b3\u30eb\u3092\u9593\u9055\u3048\u305f\uff1f", + "stream_no_route_to_host": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u307e\u3057\u305f\u304c\u3001\u30db\u30b9\u30c8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f", + "stream_no_video": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u52d5\u753b\u304c\u3042\u308a\u307e\u305b\u3093", + "stream_not_permitted": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u9593\u3001\u64cd\u4f5c\u3067\u304d\u307e\u305b\u3093\u3002RTSP\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30d7\u30ed\u30c8\u30b3\u30eb\u3092\u9593\u9055\u3048\u305f\uff1f", + "stream_unauthorised": "\u30b9\u30c8\u30ea\u30fc\u30e0\u3078\u306e\u63a5\u7d9a\u6642\u306b\u3001\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "timeout": "URL\u306e\u8aad\u307f\u8fbc\u307f\u4e2d\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", + "unable_still_load": "\u9759\u6b62\u753b\u306eURL\u304b\u3089\u6709\u52b9\u306a\u753b\u50cf\u3092\u8aad\u307f\u8fbc\u3080\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\uff08\u4f8b: \u7121\u52b9\u306a\u30db\u30b9\u30c8\u3001URL\u3001\u307e\u305f\u306f\u8a8d\u8a3c\u5931\u6557)\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "confirm": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + }, + "content_type": { + "data": { + "content_type": "\u30b3\u30f3\u30c6\u30f3\u30c4\u306e\u7a2e\u985e" + }, + "description": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306e\u30b3\u30f3\u30c6\u30f3\u30c4\u30bf\u30a4\u30d7\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002" + }, + "user": { + "data": { + "authentication": "\u8a8d\u8a3c", + "content_type": "\u30b3\u30f3\u30c6\u30f3\u30c4\u306e\u7a2e\u985e", + "framerate": "\u30d5\u30ec\u30fc\u30e0\u30ec\u30fc\u30c8\uff08Hz\uff09", + "limit_refetch_to_url_change": "URL\u5909\u66f4\u6642\u306e\u518d\u53d6\u5f97\u3092\u5236\u9650\u3059\u308b", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "rtsp_transport": "RTSP\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30d7\u30ed\u30c8\u30b3\u30eb", + "still_image_url": "\u9759\u6b62\u753b\u50cf\u306eURL(\u4f8b: http://...)", + "stream_source": "\u30b9\u30c8\u30ea\u30fc\u30e0\u30bd\u30fc\u30b9\u306eURL(\u4f8b: rtsp://...)", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" + }, + "description": "\u30ab\u30e1\u30e9\u306b\u63a5\u7d9a\u3059\u308b\u305f\u3081\u306e\u8a2d\u5b9a\u3092\u5165\u529b\u3057\u307e\u3059\u3002" + } + } + }, + "options": { + "error": { + "already_exists": "\u3053\u306eURL\u8a2d\u5b9a\u306e\u30ab\u30e1\u30e9\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059\u3002", + "invalid_still_image": "URL\u304c\u6709\u52b9\u306a\u9759\u6b62\u753b\u50cf\u3092\u8fd4\u3057\u307e\u305b\u3093\u3067\u3057\u305f", + "no_still_image_or_stream_url": "\u9759\u6b62\u753b\u50cf\u3082\u3057\u304f\u306f\u3001\u30b9\u30c8\u30ea\u30fc\u30e0URL\u306e\u3069\u3061\u3089\u304b\u3092\u6307\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "stream_file_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u3068\u304d\u306b\u30d5\u30a1\u30a4\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093(ffmpeg\u304c\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u3066\u3044\u307e\u3059\u304b\uff1f)", + "stream_http_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u3078\u306e\u63a5\u7d9a\u6642\u306b\u3001HTTP 404\u3067\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "stream_io_error": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u305f\u3068\u304d\u306b\u5165\u51fa\u529b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002RTSP\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30d7\u30ed\u30c8\u30b3\u30eb\u3092\u9593\u9055\u3048\u305f\uff1f", + "stream_no_route_to_host": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u307e\u3057\u305f\u304c\u3001\u30db\u30b9\u30c8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f", + "stream_no_video": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u52d5\u753b\u304c\u3042\u308a\u307e\u305b\u3093", + "stream_not_permitted": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u9593\u3001\u64cd\u4f5c\u3067\u304d\u307e\u305b\u3093\u3002RTSP\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30d7\u30ed\u30c8\u30b3\u30eb\u3092\u9593\u9055\u3048\u305f\uff1f", + "stream_unauthorised": "\u30b9\u30c8\u30ea\u30fc\u30e0\u3078\u306e\u63a5\u7d9a\u6642\u306b\u3001\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "timeout": "URL\u306e\u8aad\u307f\u8fbc\u307f\u4e2d\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", + "unable_still_load": "\u9759\u6b62\u753b\u306eURL\u304b\u3089\u6709\u52b9\u306a\u753b\u50cf\u3092\u8aad\u307f\u8fbc\u3080\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\uff08\u4f8b: \u7121\u52b9\u306a\u30db\u30b9\u30c8\u3001URL\u3001\u307e\u305f\u306f\u8a8d\u8a3c\u5931\u6557)\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "content_type": { + "data": { + "content_type": "\u30b3\u30f3\u30c6\u30f3\u30c4\u306e\u7a2e\u985e" + }, + "description": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306e\u30b3\u30f3\u30c6\u30f3\u30c4\u30bf\u30a4\u30d7\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002" + }, + "init": { + "data": { + "authentication": "\u8a8d\u8a3c", + "content_type": "\u30b3\u30f3\u30c6\u30f3\u30c4\u306e\u7a2e\u985e", + "framerate": "\u30d5\u30ec\u30fc\u30e0\u30ec\u30fc\u30c8\uff08Hz\uff09", + "limit_refetch_to_url_change": "URL\u5909\u66f4\u6642\u306e\u518d\u53d6\u5f97\u3092\u5236\u9650\u3059\u308b", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "rtsp_transport": "RTSP\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30d7\u30ed\u30c8\u30b3\u30eb", + "still_image_url": "\u9759\u6b62\u753b\u50cf\u306eURL(\u4f8b: http://...)", + "stream_source": "\u30b9\u30c8\u30ea\u30fc\u30e0\u30bd\u30fc\u30b9\u306eURL(\u4f8b: rtsp://...)", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/nl.json b/homeassistant/components/generic/translations/nl.json new file mode 100644 index 00000000000..167374c7aeb --- /dev/null +++ b/homeassistant/components/generic/translations/nl.json @@ -0,0 +1,88 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "error": { + "already_exists": "Een camera met deze URL instellingen bestaat al.", + "invalid_still_image": "URL heeft geen geldig stilstaand beeld geretourneerd", + "no_still_image_or_stream_url": "U moet ten minste een stilstaand beeld of stream-URL specificeren", + "stream_file_not_found": "Bestand niet gevonden tijdens verbinding met stream (is ffmpeg ge\u00efnstalleerd?)", + "stream_http_not_found": "HTTP 404 Niet gevonden bij poging om verbinding te maken met stream", + "stream_io_error": "Input/Output fout bij het proberen te verbinden met stream. Verkeerde RTSP transport protocol?", + "stream_no_route_to_host": "Kan de host niet vinden terwijl u verbinding probeert te maken met de stream", + "stream_no_video": "Stream heeft geen video", + "stream_not_permitted": "Operatie niet toegestaan bij poging om verbinding te maken met stream. Verkeerd RTSP transport protocol?", + "stream_unauthorised": "Autorisatie mislukt bij poging om verbinding te maken met stream", + "timeout": "Time-out tijdens het laden van URL", + "unable_still_load": "Kan geen geldige afbeelding laden van stilstaande afbeelding URL (b.v. ongeldige host, URL of authenticatie fout). Bekijk het log voor meer informatie.", + "unknown": "Onverwachte fout" + }, + "step": { + "confirm": { + "description": "Wilt u beginnen met instellen?" + }, + "content_type": { + "data": { + "content_type": "Inhoudstype" + }, + "description": "Geef het inhoudstype voor de stream op." + }, + "user": { + "data": { + "authentication": "Authenticatie", + "content_type": "Inhoudstype", + "framerate": "Framesnelheid (Hz)", + "limit_refetch_to_url_change": "Beperk refetch tot url verandering", + "password": "Wachtwoord", + "rtsp_transport": "RTSP-transportprotocol", + "still_image_url": "URL van stilstaande afbeelding (bijv. http://...)", + "stream_source": "Url van streambron (bijv. rtsp://...)", + "username": "Gebruikersnaam", + "verify_ssl": "SSL-certificaat verifi\u00ebren" + }, + "description": "Voer de instellingen in om verbinding te maken met de camera." + } + } + }, + "options": { + "error": { + "already_exists": "Een camera met deze URL instellingen bestaat al.", + "invalid_still_image": "URL heeft geen geldig stilstaand beeld geretourneerd", + "no_still_image_or_stream_url": "U moet ten minste een stilstaand beeld of stream-URL specificeren", + "stream_file_not_found": "Bestand niet gevonden tijdens verbinding met stream (is ffmpeg ge\u00efnstalleerd?)", + "stream_http_not_found": "HTTP 404 Niet gevonden bij poging om verbinding te maken met stream", + "stream_io_error": "Input/Output fout bij het proberen te verbinden met stream. Verkeerde RTSP transport protocol?", + "stream_no_route_to_host": "Kan de host niet vinden terwijl u verbinding probeert te maken met de stream", + "stream_no_video": "Stream heeft geen video", + "stream_not_permitted": "Operatie niet toegestaan bij poging om verbinding te maken met stream. Verkeerd RTSP transport protocol?", + "stream_unauthorised": "Autorisatie mislukt bij poging om verbinding te maken met stream", + "timeout": "Time-out tijdens het laden van URL", + "unable_still_load": "Kan geen geldige afbeelding laden van stilstaande afbeelding URL (b.v. ongeldige host, URL of authenticatie fout). Bekijk het log voor meer informatie.", + "unknown": "Onverwachte fout" + }, + "step": { + "content_type": { + "data": { + "content_type": "Inhoudstype" + }, + "description": "Geef het inhoudstype voor de stream op." + }, + "init": { + "data": { + "authentication": "Authenticatie", + "content_type": "Inhoudstype", + "framerate": "Framesnelheid (Hz)", + "limit_refetch_to_url_change": "Beperk refetch tot url verandering", + "password": "Wachtwoord", + "rtsp_transport": "RTSP-transportprotocol", + "still_image_url": "URL van stilstaande afbeelding (bijv. http://...)", + "stream_source": "Url van streambron (bijv. rtsp://...)", + "username": "Gebruikersnaam", + "verify_ssl": "SSL-certificaat verifi\u00ebren" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/no.json b/homeassistant/components/generic/translations/no.json new file mode 100644 index 00000000000..1f9eedaced4 --- /dev/null +++ b/homeassistant/components/generic/translations/no.json @@ -0,0 +1,88 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "error": { + "already_exists": "Et kamera med disse URL-innstillingene finnes allerede.", + "invalid_still_image": "URL returnerte ikke et gyldig stillbilde", + "no_still_image_or_stream_url": "Du m\u00e5 angi minst en URL-adresse for stillbilde eller dataflyt", + "stream_file_not_found": "Filen ble ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m (er ffmpeg installert?)", + "stream_http_not_found": "HTTP 404 Ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m", + "stream_io_error": "Inn-/utdatafeil under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m. Feil RTSP-transportprotokoll?", + "stream_no_route_to_host": "Kunne ikke finne verten under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8mmen", + "stream_no_video": "Stream har ingen video", + "stream_not_permitted": "Operasjon er ikke tillatt mens du pr\u00f8ver \u00e5 koble til str\u00f8m. Feil RTSP-transportprotokoll?", + "stream_unauthorised": "Autorisasjonen mislyktes under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8mmen", + "timeout": "Tidsavbrudd under innlasting av URL", + "unable_still_load": "Kan ikke laste inn gyldig bilde fra URL-adresse for stillbilde (f.eks. ugyldig verts-, URL- eller godkjenningsfeil). Se gjennom loggen hvis du vil ha mer informasjon.", + "unknown": "Uventet feil" + }, + "step": { + "confirm": { + "description": "Vil du starte oppsettet?" + }, + "content_type": { + "data": { + "content_type": "Innholdstype" + }, + "description": "Angi innholdstypen for str\u00f8mmen." + }, + "user": { + "data": { + "authentication": "Godkjenning", + "content_type": "Innholdstype", + "framerate": "Bildefrekvens (Hz)", + "limit_refetch_to_url_change": "Begrens gjenhenting til endring av nettadresse", + "password": "Passord", + "rtsp_transport": "RTSP transportprotokoll", + "still_image_url": "Stillbilde-URL (f.eks. http://...)", + "stream_source": "Str\u00f8mkilde-URL (f.eks. rtsp://...)", + "username": "Brukernavn", + "verify_ssl": "Verifisere SSL-sertifikat" + }, + "description": "Angi innstillingene for \u00e5 koble til kameraet." + } + } + }, + "options": { + "error": { + "already_exists": "Et kamera med disse URL-innstillingene finnes allerede.", + "invalid_still_image": "URL returnerte ikke et gyldig stillbilde", + "no_still_image_or_stream_url": "Du m\u00e5 angi minst en URL-adresse for stillbilde eller dataflyt", + "stream_file_not_found": "Filen ble ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m (er ffmpeg installert?)", + "stream_http_not_found": "HTTP 404 Ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m", + "stream_io_error": "Inn-/utdatafeil under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m. Feil RTSP-transportprotokoll?", + "stream_no_route_to_host": "Kunne ikke finne verten under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8mmen", + "stream_no_video": "Stream har ingen video", + "stream_not_permitted": "Operasjon er ikke tillatt mens du pr\u00f8ver \u00e5 koble til str\u00f8m. Feil RTSP-transportprotokoll?", + "stream_unauthorised": "Autorisasjonen mislyktes under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8mmen", + "timeout": "Tidsavbrudd under innlasting av URL", + "unable_still_load": "Kan ikke laste inn gyldig bilde fra URL-adresse for stillbilde (f.eks. ugyldig verts-, URL- eller godkjenningsfeil). Se gjennom loggen hvis du vil ha mer informasjon.", + "unknown": "Uventet feil" + }, + "step": { + "content_type": { + "data": { + "content_type": "Innholdstype" + }, + "description": "Angi innholdstypen for str\u00f8mmen." + }, + "init": { + "data": { + "authentication": "Godkjenning", + "content_type": "Innholdstype", + "framerate": "Bildefrekvens (Hz)", + "limit_refetch_to_url_change": "Begrens gjenhenting til endring av nettadresse", + "password": "Passord", + "rtsp_transport": "RTSP transportprotokoll", + "still_image_url": "Stillbilde-URL (f.eks. http://...)", + "stream_source": "Str\u00f8mkilde-URL (f.eks. rtsp://...)", + "username": "Brukernavn", + "verify_ssl": "Verifisere SSL-sertifikat" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/pl.json b/homeassistant/components/generic/translations/pl.json new file mode 100644 index 00000000000..a791a56c065 --- /dev/null +++ b/homeassistant/components/generic/translations/pl.json @@ -0,0 +1,88 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "error": { + "already_exists": "Kamera z tymi ustawieniami adresu URL ju\u017c istnieje.", + "invalid_still_image": "Adres URL nie zwr\u00f3ci\u0142 prawid\u0142owego obrazu nieruchomego (still image)", + "no_still_image_or_stream_url": "Musisz poda\u0107 przynajmniej nieruchomy obraz (still image) lub adres URL strumienia", + "stream_file_not_found": "Nie znaleziono pliku podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem (czy ffmpeg jest zainstalowany?)", + "stream_http_not_found": "\"HTTP 404 Nie znaleziono\" podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", + "stream_io_error": "B\u0142\u0105d wej\u015bcia/wyj\u015bcia podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem. Z\u0142y protok\u00f3\u0142 transportowy RTSP?", + "stream_no_route_to_host": "Nie mo\u017cna znale\u017a\u0107 hosta podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", + "stream_no_video": "Strumie\u0144 nie zawiera wideo", + "stream_not_permitted": "Operacja nie jest dozwolona podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem. Z\u0142y protok\u00f3\u0142 transportowy RTSP?", + "stream_unauthorised": "Autoryzacja nie powiod\u0142a si\u0119 podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", + "timeout": "Przekroczono limit czasu podczas \u0142adowania adresu URL", + "unable_still_load": "Nie mo\u017cna za\u0142adowa\u0107 prawid\u0142owego obrazu z adresu URL nieruchomego obrazu (np. nieprawid\u0142owy host, adres URL lub b\u0142\u0105d uwierzytelniania). Przejrzyj logi, aby uzyska\u0107 wi\u0119cej informacji.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "confirm": { + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + }, + "content_type": { + "data": { + "content_type": "Typ zawarto\u015bci" + }, + "description": "Okre\u015bl typ zawarto\u015bci strumienia." + }, + "user": { + "data": { + "authentication": "Uwierzytelnianie", + "content_type": "Typ zawarto\u015bci", + "framerate": "Od\u015bwie\u017canie (Hz)", + "limit_refetch_to_url_change": "Ogranicz pobieranie do zmiany adresu URL", + "password": "Has\u0142o", + "rtsp_transport": "Protok\u00f3\u0142 transportowy RTSP", + "still_image_url": "Adres URL obrazu nieruchomego (np. http://...)", + "stream_source": "Adres URL strumienia (np. rtsp://...)", + "username": "Nazwa u\u017cytkownika", + "verify_ssl": "Weryfikacja certyfikatu SSL" + }, + "description": "Wprowad\u017a ustawienia, aby po\u0142\u0105czy\u0107 si\u0119 z kamer\u0105." + } + } + }, + "options": { + "error": { + "already_exists": "Kamera z tymi ustawieniami adresu URL ju\u017c istnieje.", + "invalid_still_image": "Adres URL nie zwr\u00f3ci\u0142 prawid\u0142owego obrazu nieruchomego (still image)", + "no_still_image_or_stream_url": "Musisz poda\u0107 przynajmniej nieruchomy obraz (still image) lub adres URL strumienia", + "stream_file_not_found": "Nie znaleziono pliku podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem (czy ffmpeg jest zainstalowany?)", + "stream_http_not_found": "\"HTTP 404 Nie znaleziono\" podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", + "stream_io_error": "B\u0142\u0105d wej\u015bcia/wyj\u015bcia podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem. Z\u0142y protok\u00f3\u0142 transportowy RTSP?", + "stream_no_route_to_host": "Nie mo\u017cna znale\u017a\u0107 hosta podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", + "stream_no_video": "Strumie\u0144 nie zawiera wideo", + "stream_not_permitted": "Operacja nie jest dozwolona podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem. Z\u0142y protok\u00f3\u0142 transportowy RTSP?", + "stream_unauthorised": "Autoryzacja nie powiod\u0142a si\u0119 podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", + "timeout": "Przekroczono limit czasu podczas \u0142adowania adresu URL", + "unable_still_load": "Nie mo\u017cna za\u0142adowa\u0107 prawid\u0142owego obrazu z adresu URL nieruchomego obrazu (np. nieprawid\u0142owy host, adres URL lub b\u0142\u0105d uwierzytelniania). Przejrzyj logi, aby uzyska\u0107 wi\u0119cej informacji.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "content_type": { + "data": { + "content_type": "Typ zawarto\u015bci" + }, + "description": "Okre\u015bl typ zawarto\u015bci strumienia." + }, + "init": { + "data": { + "authentication": "Uwierzytelnianie", + "content_type": "Typ zawarto\u015bci", + "framerate": "Od\u015bwie\u017canie (Hz)", + "limit_refetch_to_url_change": "Ogranicz pobieranie do zmiany adresu URL", + "password": "Has\u0142o", + "rtsp_transport": "Protok\u00f3\u0142 transportowy RTSP", + "still_image_url": "Adres URL obrazu nieruchomego (np. http://...)", + "stream_source": "Adres URL strumienia (np. rtsp://...)", + "username": "Nazwa u\u017cytkownika", + "verify_ssl": "Weryfikacja certyfikatu SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/pt-BR.json b/homeassistant/components/generic/translations/pt-BR.json new file mode 100644 index 00000000000..0e3ce2bd75d --- /dev/null +++ b/homeassistant/components/generic/translations/pt-BR.json @@ -0,0 +1,88 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "already_exists": "J\u00e1 existe uma c\u00e2mera com essas configura\u00e7\u00f5es de URL.", + "invalid_still_image": "A URL n\u00e3o retornou uma imagem est\u00e1tica v\u00e1lida", + "no_still_image_or_stream_url": "Voc\u00ea deve especificar pelo menos uma imagem est\u00e1tica ou uma URL de stream", + "stream_file_not_found": "Arquivo n\u00e3o encontrado ao tentar se conectar a stream (o ffmpeg est\u00e1 instalado?)", + "stream_http_not_found": "HTTP 404 n\u00e3o encontrado ao tentar se conectar a stream", + "stream_io_error": "Erro de entrada/sa\u00edda ao tentar se conectar a stream. Protocolo RTSP errado?", + "stream_no_route_to_host": "N\u00e3o foi poss\u00edvel encontrar o host ao tentar se conectar a stream", + "stream_no_video": "A stream n\u00e3o tem v\u00eddeo", + "stream_not_permitted": "Opera\u00e7\u00e3o n\u00e3o permitida ao tentar se conectar a stream. Protocolo RTSP errado?", + "stream_unauthorised": "Falha na autoriza\u00e7\u00e3o ao tentar se conectar a stream", + "timeout": "Tempo limite ao carregar a URL", + "unable_still_load": "N\u00e3o foi poss\u00edvel carregar uma imagem v\u00e1lida do URL da imagem est\u00e1tica (por exemplo, host inv\u00e1lido, URL ou falha de autentica\u00e7\u00e3o). Revise o log para obter mais informa\u00e7\u00f5es.", + "unknown": "Erro inesperado" + }, + "step": { + "confirm": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + }, + "content_type": { + "data": { + "content_type": "Tipo de conte\u00fado" + }, + "description": "Especifique o tipo de conte\u00fado para o stream." + }, + "user": { + "data": { + "authentication": "Autentica\u00e7\u00e3o", + "content_type": "Tipo de conte\u00fado", + "framerate": "Taxa de quadros (Hz)", + "limit_refetch_to_url_change": "Limitar a nova busca \u00e0 altera\u00e7\u00e3o de URL", + "password": "Senha", + "rtsp_transport": "Protocolo RTSP", + "still_image_url": "URL da imagem est\u00e1tica (por exemplo, http://...)", + "stream_source": "URL de origem da Stream (por exemplo, rtsp://...)", + "username": "Usu\u00e1rio", + "verify_ssl": "Verifique o certificado SSL" + }, + "description": "Insira as configura\u00e7\u00f5es para se conectar \u00e0 c\u00e2mera." + } + } + }, + "options": { + "error": { + "already_exists": "J\u00e1 existe uma c\u00e2mera com essas configura\u00e7\u00f5es de URL.", + "invalid_still_image": "A URL n\u00e3o retornou uma imagem est\u00e1tica v\u00e1lida", + "no_still_image_or_stream_url": "Voc\u00ea deve especificar pelo menos uma imagem est\u00e1tica ou uma URL de stream", + "stream_file_not_found": "Arquivo n\u00e3o encontrado ao tentar se conectar a stream (o ffmpeg est\u00e1 instalado?)", + "stream_http_not_found": "HTTP 404 n\u00e3o encontrado ao tentar se conectar a stream", + "stream_io_error": "Erro de entrada/sa\u00edda ao tentar se conectar a stream. Protocolo RTSP errado?", + "stream_no_route_to_host": "N\u00e3o foi poss\u00edvel encontrar o host ao tentar se conectar a stream", + "stream_no_video": "A stream n\u00e3o tem v\u00eddeo", + "stream_not_permitted": "Opera\u00e7\u00e3o n\u00e3o permitida ao tentar se conectar a stream. Protocolo RTSP errado?", + "stream_unauthorised": "Falha na autoriza\u00e7\u00e3o ao tentar se conectar a stream", + "timeout": "Tempo limite ao carregar a URL", + "unable_still_load": "N\u00e3o foi poss\u00edvel carregar uma imagem v\u00e1lida do URL da imagem est\u00e1tica (por exemplo, host inv\u00e1lido, URL ou falha de autentica\u00e7\u00e3o). Revise o log para obter mais informa\u00e7\u00f5es.", + "unknown": "Erro inesperado" + }, + "step": { + "content_type": { + "data": { + "content_type": "Tipo de conte\u00fado" + }, + "description": "Especifique o tipo de conte\u00fado para o stream." + }, + "init": { + "data": { + "authentication": "Autentica\u00e7\u00e3o", + "content_type": "Tipo de conte\u00fado", + "framerate": "Taxa de quadros (Hz)", + "limit_refetch_to_url_change": "Limitar a nova busca \u00e0 altera\u00e7\u00e3o de URL", + "password": "Senha", + "rtsp_transport": "Protocolo RTSP", + "still_image_url": "URL da imagem est\u00e1tica (por exemplo, http://...)", + "stream_source": "URL de origem da Stream (por exemplo, rtsp://...)", + "username": "Usu\u00e1rio", + "verify_ssl": "Verifique o certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/ru.json b/homeassistant/components/generic/translations/ru.json new file mode 100644 index 00000000000..06f41613a50 --- /dev/null +++ b/homeassistant/components/generic/translations/ru.json @@ -0,0 +1,88 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "error": { + "already_exists": "\u041a\u0430\u043c\u0435\u0440\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c URL-\u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.", + "invalid_still_image": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435.", + "no_still_image_or_stream_url": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u043f\u043e\u0442\u043e\u043a\u0430.", + "stream_file_not_found": "\u0424\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u043b\u0438 ffmpeg?", + "stream_http_not_found": "HTTP 404 \u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", + "stream_io_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0432\u043e\u0434\u0430/\u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", + "stream_no_route_to_host": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0445\u043e\u0441\u0442 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", + "stream_no_video": "\u0412 \u043f\u043e\u0442\u043e\u043a\u0435 \u043d\u0435\u0442 \u0432\u0438\u0434\u0435\u043e.", + "stream_not_permitted": "\u041e\u043f\u0435\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", + "stream_unauthorised": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", + "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 URL-\u0430\u0434\u0440\u0435\u0441\u0430.", + "unable_still_load": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0441 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f (\u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0445\u043e\u0441\u0442, URL-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438). \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\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": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + }, + "content_type": { + "data": { + "content_type": "\u0422\u0438\u043f \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0442\u0438\u043f \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e \u0434\u043b\u044f \u043f\u043e\u0442\u043e\u043a\u0430." + }, + "user": { + "data": { + "authentication": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", + "content_type": "\u0422\u0438\u043f \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e", + "framerate": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043a\u0430\u0434\u0440\u043e\u0432 (\u0413\u0446)", + "limit_refetch_to_url_change": "\u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0438\u0432\u0430\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0443\u044e \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0443 \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 URL-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "rtsp_transport": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP", + "still_image_url": "URL-\u0430\u0434\u0440\u0435\u0441 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, http://...)", + "stream_source": "URL-\u0430\u0434\u0440\u0435\u0441 \u043f\u043e\u0442\u043e\u043a\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, rtsp://...)", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043a\u0430\u043c\u0435\u0440\u0435." + } + } + }, + "options": { + "error": { + "already_exists": "\u041a\u0430\u043c\u0435\u0440\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c URL-\u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.", + "invalid_still_image": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435.", + "no_still_image_or_stream_url": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u043f\u043e\u0442\u043e\u043a\u0430.", + "stream_file_not_found": "\u0424\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u043b\u0438 ffmpeg?", + "stream_http_not_found": "HTTP 404 \u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", + "stream_io_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0432\u043e\u0434\u0430/\u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", + "stream_no_route_to_host": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0445\u043e\u0441\u0442 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", + "stream_no_video": "\u0412 \u043f\u043e\u0442\u043e\u043a\u0435 \u043d\u0435\u0442 \u0432\u0438\u0434\u0435\u043e.", + "stream_not_permitted": "\u041e\u043f\u0435\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", + "stream_unauthorised": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", + "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 URL-\u0430\u0434\u0440\u0435\u0441\u0430.", + "unable_still_load": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0441 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f (\u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0445\u043e\u0441\u0442, URL-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438). \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\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": { + "content_type": { + "data": { + "content_type": "\u0422\u0438\u043f \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0442\u0438\u043f \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e \u0434\u043b\u044f \u043f\u043e\u0442\u043e\u043a\u0430." + }, + "init": { + "data": { + "authentication": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", + "content_type": "\u0422\u0438\u043f \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e", + "framerate": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043a\u0430\u0434\u0440\u043e\u0432 (\u0413\u0446)", + "limit_refetch_to_url_change": "\u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0438\u0432\u0430\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0443\u044e \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0443 \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 URL-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "rtsp_transport": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP", + "still_image_url": "URL-\u0430\u0434\u0440\u0435\u0441 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, http://...)", + "stream_source": "URL-\u0430\u0434\u0440\u0435\u0441 \u043f\u043e\u0442\u043e\u043a\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, rtsp://...)", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", + "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/generic/translations/tr.json b/homeassistant/components/generic/translations/tr.json new file mode 100644 index 00000000000..c2fb343f227 --- /dev/null +++ b/homeassistant/components/generic/translations/tr.json @@ -0,0 +1,88 @@ +{ + "config": { + "abort": { + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "already_exists": "Bu URL ayarlar\u0131na sahip bir kamera zaten var.", + "invalid_still_image": "URL ge\u00e7erli bir hareketsiz resim d\u00f6nd\u00fcrmedi", + "no_still_image_or_stream_url": "En az\u0131ndan bir dura\u011fan resim veya ak\u0131\u015f URL'si belirtmelisiniz", + "stream_file_not_found": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken dosya bulunamad\u0131 (ffmpeg y\u00fckl\u00fc m\u00fc?)", + "stream_http_not_found": "HTTP 404 Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken bulunamad\u0131", + "stream_io_error": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken Giri\u015f/\u00c7\u0131k\u0131\u015f hatas\u0131. Yanl\u0131\u015f RTSP aktar\u0131m protokol\u00fc?", + "stream_no_route_to_host": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken ana bilgisayar bulunamad\u0131", + "stream_no_video": "Ak\u0131\u015fta video yok", + "stream_not_permitted": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken i\u015fleme izin verilmiyor. Yanl\u0131\u015f RTSP aktar\u0131m protokol\u00fc?", + "stream_unauthorised": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken yetkilendirme ba\u015far\u0131s\u0131z oldu", + "timeout": "URL y\u00fcklenirken zaman a\u015f\u0131m\u0131", + "unable_still_load": "Hareketsiz resim URL'sinden ge\u00e7erli resim y\u00fcklenemiyor (\u00f6r. ge\u00e7ersiz ana bilgisayar, URL veya kimlik do\u011frulama hatas\u0131). Daha fazla bilgi i\u00e7in g\u00fcnl\u00fc\u011f\u00fc inceleyin.", + "unknown": "Beklenmeyen hata" + }, + "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + }, + "content_type": { + "data": { + "content_type": "\u0130\u00e7erik T\u00fcr\u00fc" + }, + "description": "Ak\u0131\u015f i\u00e7in i\u00e7erik t\u00fcr\u00fcn\u00fc belirtin." + }, + "user": { + "data": { + "authentication": "Kimlik Do\u011frulama", + "content_type": "\u0130\u00e7erik T\u00fcr\u00fc", + "framerate": "Kare H\u0131z\u0131 (Hz)", + "limit_refetch_to_url_change": "Yeniden getirmeyi url de\u011fi\u015fikli\u011fiyle s\u0131n\u0131rla", + "password": "Parola", + "rtsp_transport": "RTSP aktar\u0131m protokol\u00fc", + "still_image_url": "Hareketsiz G\u00f6r\u00fcnt\u00fc URL'si (\u00f6rne\u011fin http://...)", + "stream_source": "Ak\u0131\u015f Kayna\u011f\u0131 URL'si (\u00f6r. rtsp://...)", + "username": "Kullan\u0131c\u0131 Ad\u0131", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" + }, + "description": "Kameraya ba\u011flanmak i\u00e7in ayarlar\u0131 girin." + } + } + }, + "options": { + "error": { + "already_exists": "Bu URL ayarlar\u0131na sahip bir kamera zaten var.", + "invalid_still_image": "URL ge\u00e7erli bir hareketsiz resim d\u00f6nd\u00fcrmedi", + "no_still_image_or_stream_url": "En az\u0131ndan bir dura\u011fan resim veya ak\u0131\u015f URL'si belirtmelisiniz", + "stream_file_not_found": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken dosya bulunamad\u0131 (ffmpeg y\u00fckl\u00fc m\u00fc?)", + "stream_http_not_found": "HTTP 404 Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken bulunamad\u0131", + "stream_io_error": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken Giri\u015f/\u00c7\u0131k\u0131\u015f hatas\u0131. Yanl\u0131\u015f RTSP aktar\u0131m protokol\u00fc?", + "stream_no_route_to_host": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken ana bilgisayar bulunamad\u0131", + "stream_no_video": "Ak\u0131\u015fta video yok", + "stream_not_permitted": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken i\u015fleme izin verilmiyor. Yanl\u0131\u015f RTSP aktar\u0131m protokol\u00fc?", + "stream_unauthorised": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken yetkilendirme ba\u015far\u0131s\u0131z oldu", + "timeout": "URL y\u00fcklenirken zaman a\u015f\u0131m\u0131", + "unable_still_load": "Hareketsiz resim URL'sinden ge\u00e7erli resim y\u00fcklenemiyor (\u00f6r. ge\u00e7ersiz ana bilgisayar, URL veya kimlik do\u011frulama hatas\u0131). Daha fazla bilgi i\u00e7in g\u00fcnl\u00fc\u011f\u00fc inceleyin.", + "unknown": "Beklenmeyen hata" + }, + "step": { + "content_type": { + "data": { + "content_type": "\u0130\u00e7erik T\u00fcr\u00fc" + }, + "description": "Ak\u0131\u015f i\u00e7in i\u00e7erik t\u00fcr\u00fcn\u00fc belirtin." + }, + "init": { + "data": { + "authentication": "Kimlik Do\u011frulama", + "content_type": "\u0130\u00e7erik T\u00fcr\u00fc", + "framerate": "Kare H\u0131z\u0131 (Hz)", + "limit_refetch_to_url_change": "Yeniden getirmeyi url de\u011fi\u015fikli\u011fiyle s\u0131n\u0131rla", + "password": "Parola", + "rtsp_transport": "RTSP aktar\u0131m protokol\u00fc", + "still_image_url": "Hareketsiz G\u00f6r\u00fcnt\u00fc URL'si (\u00f6rne\u011fin http://...)", + "stream_source": "Ak\u0131\u015f Kayna\u011f\u0131 URL'si (\u00f6r. rtsp://...)", + "username": "Kullan\u0131c\u0131 Ad\u0131", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/zh-Hant.json b/homeassistant/components/generic/translations/zh-Hant.json new file mode 100644 index 00000000000..1b72dcb0ce4 --- /dev/null +++ b/homeassistant/components/generic/translations/zh-Hant.json @@ -0,0 +1,88 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "error": { + "already_exists": "\u5df2\u7d93\u6709\u4f7f\u7528\u76f8\u540c URL \u7684\u651d\u5f71\u6a5f\u3002", + "invalid_still_image": "URL \u56de\u50b3\u4e26\u975e\u6709\u6548\u975c\u614b\u5f71\u50cf", + "no_still_image_or_stream_url": "\u5fc5\u9808\u81f3\u5c11\u6307\u5b9a\u975c\u614b\u5f71\u50cf\u6216\u4e32\u6d41 URL", + "stream_file_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u627e\u4e0d\u5230\u6a94\u6848\u932f\u8aa4\uff08\u662f\u5426\u5df2\u5b89\u88dd ffmpeg\uff1f\uff09", + "stream_http_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe HTTP 404 \u672a\u627e\u5230\u932f\u8aa4", + "stream_io_error": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u8f38\u5165/\u8f38\u51fa\u932f\u8aa4\u3002\u8f38\u5165\u932f\u8aa4\u7684 RTSP \u50b3\u8f38\u5354\u5b9a\uff1f", + "stream_no_route_to_host": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u627e\u4e0d\u5230\u4e3b\u6a5f", + "stream_no_video": "\u4e32\u6d41\u6c92\u6709\u5f71\u50cf", + "stream_not_permitted": "\u5617\u8a66\u4e32\u6d41\u9023\u7dda\u6642\u4e0d\u5141\u8a31\u64cd\u4f5c\u3002\u8f38\u5165\u932f\u8aa4\u7684 RTSP \u50b3\u8f38\u5354\u5b9a\uff1f", + "stream_unauthorised": "\u5617\u8a66\u4e32\u6d41\u9023\u7dda\u6642\u8a8d\u8b49\u5931\u6557", + "timeout": "\u8f09\u5165 URL \u903e\u6642\u6642\u9593", + "unable_still_load": "\u7121\u6cd5\u7531\u8a2d\u5b9a\u975c\u614b\u5f71\u50cf URL \u8f09\u5165\u6709\u6548\u5f71\u50cf\uff08\u4f8b\u5982\uff1a\u7121\u6548\u4e3b\u6a5f\u3001URL \u6216\u8a8d\u8b49\u5931\u6557\uff09\u3002\u8acb\u53c3\u95b1\u65e5\u8a8c\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8a0a\u606f\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + }, + "content_type": { + "data": { + "content_type": "\u5167\u5bb9\u985e\u578b" + }, + "description": "\u6307\u5b9a\u4e32\u6d41\u5167\u5bb9\u985e\u5225" + }, + "user": { + "data": { + "authentication": "\u9a57\u8b49", + "content_type": "\u5167\u5bb9\u985e\u578b", + "framerate": "\u5f71\u683c\u7387 (Hz)", + "limit_refetch_to_url_change": "\u9650\u5236 URL \u8b8a\u66f4\u66f4\u65b0", + "password": "\u5bc6\u78bc", + "rtsp_transport": "RTSP \u50b3\u8f38\u5354\u5b9a", + "still_image_url": "\u975c\u614b\u5f71\u50cf URL\uff08\u4f8b\u5982 http://...\uff09", + "stream_source": "\u4e32\u6d41\u4f86\u6e90 URL\uff08\u4f8b\u5982 rtsp://...\uff09", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + }, + "description": "\u8f38\u5165\u651d\u5f71\u6a5f\u9023\u7dda\u8a2d\u5b9a\u3002" + } + } + }, + "options": { + "error": { + "already_exists": "\u5df2\u7d93\u6709\u4f7f\u7528\u76f8\u540c URL \u7684\u651d\u5f71\u6a5f\u3002", + "invalid_still_image": "URL \u56de\u50b3\u4e26\u975e\u6709\u6548\u975c\u614b\u5f71\u50cf", + "no_still_image_or_stream_url": "\u5fc5\u9808\u81f3\u5c11\u6307\u5b9a\u975c\u614b\u5f71\u50cf\u6216\u4e32\u6d41 URL", + "stream_file_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u627e\u4e0d\u5230\u6a94\u6848\u932f\u8aa4\uff08\u662f\u5426\u5df2\u5b89\u88dd ffmpeg\uff1f\uff09", + "stream_http_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe HTTP 404 \u672a\u627e\u5230\u932f\u8aa4", + "stream_io_error": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u8f38\u5165/\u8f38\u51fa\u932f\u8aa4\u3002\u8f38\u5165\u932f\u8aa4\u7684 RTSP \u50b3\u8f38\u5354\u5b9a\uff1f", + "stream_no_route_to_host": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u627e\u4e0d\u5230\u4e3b\u6a5f", + "stream_no_video": "\u4e32\u6d41\u6c92\u6709\u5f71\u50cf", + "stream_not_permitted": "\u5617\u8a66\u4e32\u6d41\u9023\u7dda\u6642\u4e0d\u5141\u8a31\u64cd\u4f5c\u3002\u8f38\u5165\u932f\u8aa4\u7684 RTSP \u50b3\u8f38\u5354\u5b9a\uff1f", + "stream_unauthorised": "\u5617\u8a66\u4e32\u6d41\u9023\u7dda\u6642\u8a8d\u8b49\u5931\u6557", + "timeout": "\u8f09\u5165 URL \u903e\u6642\u6642\u9593", + "unable_still_load": "\u7121\u6cd5\u7531\u8a2d\u5b9a\u975c\u614b\u5f71\u50cf URL \u8f09\u5165\u6709\u6548\u5f71\u50cf\uff08\u4f8b\u5982\uff1a\u7121\u6548\u4e3b\u6a5f\u3001URL \u6216\u8a8d\u8b49\u5931\u6557\uff09\u3002\u8acb\u53c3\u95b1\u65e5\u8a8c\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8a0a\u606f\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "content_type": { + "data": { + "content_type": "\u5167\u5bb9\u985e\u578b" + }, + "description": "\u6307\u5b9a\u4e32\u6d41\u5167\u5bb9\u985e\u5225" + }, + "init": { + "data": { + "authentication": "\u9a57\u8b49", + "content_type": "\u5167\u5bb9\u985e\u578b", + "framerate": "\u5f71\u683c\u7387 (Hz)", + "limit_refetch_to_url_change": "\u9650\u5236 URL \u8b8a\u66f4\u66f4\u65b0", + "password": "\u5bc6\u78bc", + "rtsp_transport": "RTSP \u50b3\u8f38\u5354\u5b9a", + "still_image_url": "\u975c\u614b\u5f71\u50cf URL\uff08\u4f8b\u5982 http://...\uff09", + "stream_source": "\u4e32\u6d41\u4f86\u6e90 URL\uff08\u4f8b\u5982 rtsp://...\uff09", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/hu.json b/homeassistant/components/gios/translations/hu.json index 9454aceb13d..1de37cee96a 100644 --- a/homeassistant/components/gios/translations/hu.json +++ b/homeassistant/components/gios/translations/hu.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "station_id": "A m\u00e9r\u0151\u00e1llom\u00e1s azonos\u00edt\u00f3ja" }, "description": "A GIO\u015a (lengyel k\u00f6rnyezetv\u00e9delmi f\u0151fel\u00fcgyel\u0151) leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Ha seg\u00edts\u00e9gre van sz\u00fcks\u00e9ged a konfigur\u00e1ci\u00f3val kapcsolatban, l\u00e1togass ide: https://www.home-assistant.io/integrations/gios", diff --git a/homeassistant/components/glances/translations/hu.json b/homeassistant/components/glances/translations/hu.json index d93fa4bb66e..71649b51d34 100644 --- a/homeassistant/components/glances/translations/hu.json +++ b/homeassistant/components/glances/translations/hu.json @@ -11,7 +11,7 @@ "user": { "data": { "host": "C\u00edm", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3", "port": "Port", "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", diff --git a/homeassistant/components/goalzero/translations/ca.json b/homeassistant/components/goalzero/translations/ca.json index 0f9979eb378..3e7d6e714d3 100644 --- a/homeassistant/components/goalzero/translations/ca.json +++ b/homeassistant/components/goalzero/translations/ca.json @@ -20,7 +20,7 @@ "host": "Amfitri\u00f3", "name": "Nom" }, - "description": "En primer lloc, has de descarregar-te l'aplicaci\u00f3 Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\nSegueix les instruccions per connectar el Yeti al teu Wi-Fi. Es recomana que la reserva DHCP del router estigui configurada, si no ho est\u00e0, pot ser que el dispositiu no estigui disponible mentre Home Assistant no detecti la nova IP. Consulta el manual del router.", + "description": "Consulta la documentaci\u00f3 per assegurar-te que compleixes tots els requisits.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/de.json b/homeassistant/components/goalzero/translations/de.json index 5133488c247..a9f7e3fa887 100644 --- a/homeassistant/components/goalzero/translations/de.json +++ b/homeassistant/components/goalzero/translations/de.json @@ -20,7 +20,7 @@ "host": "Host", "name": "Name" }, - "description": "Zuerst musst du die Goal Zero App herunterladen: https://www.goalzero.com/product-features/yeti-app/ \n\nFolge den Anweisungen, um deinen Yeti mit deinem WLAN-Netzwerk zu verbinden. Eine DHCP-Reservierung auf deinem Router wird empfohlen. Wenn es nicht eingerichtet ist, ist das Ger\u00e4t m\u00f6glicherweise nicht verf\u00fcgbar, bis Home Assistant die neue IP-Adresse erkennt. Schlage dazu im Benutzerhandbuch deines Routers nach.", + "description": "Bitte lies die Dokumentation, um sicherzustellen, dass alle Anforderungen erf\u00fcllt sind.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/en.json b/homeassistant/components/goalzero/translations/en.json index 26a92757a4b..fd27892c794 100644 --- a/homeassistant/components/goalzero/translations/en.json +++ b/homeassistant/components/goalzero/translations/en.json @@ -20,7 +20,7 @@ "host": "Host", "name": "Name" }, - "description": "First, you need to download the Goal Zero app: https://www.goalzero.com/product-features/yeti-app/\n\nFollow the instructions to connect your Yeti to your Wi-fi network. DHCP reservation on your router is recommended. If not set up, the device may become unavailable until Home Assistant detects the new ip address. Refer to your router's user manual.", + "description": "Please refer to the documentation to make sure all requirements are met.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/et.json b/homeassistant/components/goalzero/translations/et.json index 5d0111aa946..e529f0a3a4e 100644 --- a/homeassistant/components/goalzero/translations/et.json +++ b/homeassistant/components/goalzero/translations/et.json @@ -20,7 +20,7 @@ "host": "", "name": "Nimi" }, - "description": "Alustuseks pead alla laadima rakenduse Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nYeti Wifi-v\u00f5rguga \u00fchendamiseks j\u00e4rgi juhiseid. DHCP peab olema ruuteri seadetes seadistatud nii, et hosti IP ei muutuks. Vaata ruuteri kasutusjuhendit.", + "description": "Vaata dokumentatsiooni, et veenduda, et k\u00f5ik n\u00f5uded on t\u00e4idetud.", "title": "" } } diff --git a/homeassistant/components/goalzero/translations/fr.json b/homeassistant/components/goalzero/translations/fr.json index b554ec1ea1d..430ad9927c4 100644 --- a/homeassistant/components/goalzero/translations/fr.json +++ b/homeassistant/components/goalzero/translations/fr.json @@ -20,7 +20,7 @@ "host": "H\u00f4te", "name": "Nom" }, - "description": "Vous devez tout d'abord t\u00e9l\u00e9charger l'application Goal Zero\u00a0: https://www.goalzero.com/product-features/yeti-app/\n\nSuivez les instructions pour connecter votre Yeti \u00e0 votre r\u00e9seau Wi-Fi. Il est recommand\u00e9 d'utiliser la r\u00e9servation DHCP sur votre routeur, sans quoi l'appareil risque de devenir indisponible le temps que Home Assistant d\u00e9tecte la nouvelle adresse IP. Reportez-vous au manuel d'utilisation de votre routeur.", + "description": "Veuillez consulter la documentation afin de vous assurer que toutes les conditions sont respect\u00e9es.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/hu.json b/homeassistant/components/goalzero/translations/hu.json index 62c0a1626f9..00196952e0c 100644 --- a/homeassistant/components/goalzero/translations/hu.json +++ b/homeassistant/components/goalzero/translations/hu.json @@ -18,9 +18,9 @@ "user": { "data": { "host": "C\u00edm", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" }, - "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.", + "description": "K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, hogy megbizonyosodjon arr\u00f3l, hogy minden k\u00f6vetelm\u00e9ny teljes\u00fcl.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/id.json b/homeassistant/components/goalzero/translations/id.json index d5897a2d944..d524b13bb0c 100644 --- a/homeassistant/components/goalzero/translations/id.json +++ b/homeassistant/components/goalzero/translations/id.json @@ -20,7 +20,7 @@ "host": "Host", "name": "Nama" }, - "description": "Pertama, Anda perlu mengunduh aplikasi Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nIkuti petunjuk untuk menghubungkan Yeti Anda ke jaringan Wi-Fi Anda. Kemudian dapatkan IP host dari router Anda. DHCP harus disetel di pengaturan router Anda untuk perangkat host agar IP host tidak berubah. Lihat manual pengguna router Anda.", + "description": "Rujuk ke dokumentasi untuk memastikan semua persyaratan terpenuhi.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/it.json b/homeassistant/components/goalzero/translations/it.json index ad5042cc602..dbd71c82f4a 100644 --- a/homeassistant/components/goalzero/translations/it.json +++ b/homeassistant/components/goalzero/translations/it.json @@ -20,7 +20,7 @@ "host": "Host", "name": "Nome" }, - "description": "Innanzitutto, devi scaricare l'app Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\nSegui le istruzioni per connettere il tuo Yeti alla tua rete Wi-Fi. Si consiglia la prenotazione DHCP sul router. Se non configurato, il dispositivo potrebbe non essere disponibile fino a quando Home Assistant non rileva il nuovo indirizzo IP. Fare riferimento al manuale utente del router.", + "description": "Fai riferimento alla documentazione per assicurarti che tutti i requisiti siano soddisfatti.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/nl.json b/homeassistant/components/goalzero/translations/nl.json index d73c4d648ed..680ff1a10cc 100644 --- a/homeassistant/components/goalzero/translations/nl.json +++ b/homeassistant/components/goalzero/translations/nl.json @@ -20,7 +20,7 @@ "host": "Host", "name": "Naam" }, - "description": "Eerst moet u de Goal Zero-app downloaden: https://www.goalzero.com/product-features/yeti-app/ \n\n Volg de instructies om Yeti te verbinden met uw wifi-netwerk. DHCP-reservering op uw router wordt aanbevolen. Als het niet is ingesteld, is het apparaat mogelijk niet meer beschikbaar totdat Home Assistant het nieuwe IP-adres detecteert. Raadpleeg de gebruikershandleiding van uw router.", + "description": "Raadpleeg de documentatie om er zeker van te zijn dat aan alle vereisten wordt voldaan.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/no.json b/homeassistant/components/goalzero/translations/no.json index 1bc2d6feae1..6bb00952fab 100644 --- a/homeassistant/components/goalzero/translations/no.json +++ b/homeassistant/components/goalzero/translations/no.json @@ -20,7 +20,7 @@ "host": "Vert", "name": "Navn" }, - "description": "F\u00f8rst m\u00e5 du laste ned Goal Zero-appen: https://www.goalzero.com/product-features/yeti-app/\n\nF\u00f8lg instruksjonene for \u00e5 koble Yeti til Wi-Fi-nettverket ditt. DHCP-reservasjon p\u00e5 ruteren anbefales. Hvis den ikke er konfigurert, kan enheten bli utilgjengelig til Home Assistant oppdager den nye IP-adressen. Se brukerh\u00e5ndboken for ruteren.", + "description": "Se dokumentasjonen for \u00e5 sikre at alle krav er oppfylt.", "title": "" } } diff --git a/homeassistant/components/goalzero/translations/pl.json b/homeassistant/components/goalzero/translations/pl.json index 3aba221bc4a..ee8f2022a0d 100644 --- a/homeassistant/components/goalzero/translations/pl.json +++ b/homeassistant/components/goalzero/translations/pl.json @@ -20,7 +20,7 @@ "host": "Nazwa hosta lub adres IP", "name": "Nazwa" }, - "description": "Najpierw musisz pobra\u0107 aplikacj\u0119 Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nPost\u0119puj zgodnie z instrukcjami, aby pod\u0142\u0105czy\u0107 Yeti do sieci Wi-Fi. Zaleca si\u0119 rezerwacj\u0119 DHCP w ustawieniach routera. Je\u015bli tego nie ustawisz, urz\u0105dzenie mo\u017ce sta\u0107 si\u0119 niedost\u0119pne, do czasu a\u017c Home Assistant wykryje nowy adres IP. Post\u0119puj wg instrukcji obs\u0142ugi routera.", + "description": "Zapoznaj si\u0119 z dokumentacj\u0105, aby upewni\u0107 si\u0119, \u017ce wszystkie wymagania s\u0105 spe\u0142nione.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/pt-BR.json b/homeassistant/components/goalzero/translations/pt-BR.json index 7ecea696702..fa67129b08a 100644 --- a/homeassistant/components/goalzero/translations/pt-BR.json +++ b/homeassistant/components/goalzero/translations/pt-BR.json @@ -20,7 +20,7 @@ "host": "Nome do host", "name": "Nome" }, - "description": "Primeiro, voc\u00ea precisa baixar o aplicativo Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\n Siga as instru\u00e7\u00f5es para conectar seu Yeti \u00e0 sua rede Wi-fi. A reserva de DHCP em seu roteador \u00e9 recomendada. Se n\u00e3o estiver configurado, o dispositivo pode ficar indispon\u00edvel at\u00e9 que o Home Assistant detecte o novo endere\u00e7o IP. Consulte o manual do usu\u00e1rio do seu roteador.", + "description": "Consulte a documenta\u00e7\u00e3o para garantir que todos os requisitos sejam atendidos.", "title": "Gol Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/ru.json b/homeassistant/components/goalzero/translations/ru.json index 52d8bbcbdc7..ca114f6d92e 100644 --- a/homeassistant/components/goalzero/translations/ru.json +++ b/homeassistant/components/goalzero/translations/ru.json @@ -20,7 +20,7 @@ "host": "\u0425\u043e\u0441\u0442", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0441\u043a\u0430\u0447\u0430\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Goal Zero: https://www.goalzero.com/product-features/yeti-app/.\n\n\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c \u043f\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044e Yeti \u043a \u0441\u0435\u0442\u0438 WiFi. \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0442\u0430\u043a\u0438\u043c\u0438, \u0447\u0442\u043e\u0431\u044b IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u044f\u043b\u0441\u044f \u0441\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0435\u043c. \u0412 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435, \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043c\u043e\u0436\u0435\u0442 \u0441\u0442\u0430\u0442\u044c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c \u0434\u043e \u0442\u0435\u0445 \u043f\u043e\u0440, \u043f\u043e\u043a\u0430 Home Assistant \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442 \u043d\u043e\u0432\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u044d\u0442\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0440\u0443\u043a\u043e\u0432\u043e\u0434\u0441\u0442\u0432\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430.", + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439, \u0447\u0442\u043e\u0431\u044b \u0443\u0431\u0435\u0434\u0438\u0442\u044c\u0441\u044f, \u0447\u0442\u043e \u0432\u0441\u0435 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u043e\u0431\u043b\u044e\u0434\u0435\u043d\u044b.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/tr.json b/homeassistant/components/goalzero/translations/tr.json index a2c5b4f8986..9be07def514 100644 --- a/homeassistant/components/goalzero/translations/tr.json +++ b/homeassistant/components/goalzero/translations/tr.json @@ -20,7 +20,7 @@ "host": "Sunucu", "name": "Ad" }, - "description": "\u00d6ncelikle Goal Zero uygulamas\u0131n\u0131 indirmeniz gerekiyor: https://www.goalzero.com/product-features/yeti-app/ \n\n Yeti'nizi Wi-fi a\u011f\u0131n\u0131za ba\u011flamak i\u00e7in talimatlar\u0131 izleyin. Y\u00f6nlendiricinizde DHCP rezervasyonu yap\u0131lmas\u0131 \u00f6nerilir. Kurulmazsa, Home Assistant yeni ip adresini alg\u0131layana kadar cihaz kullan\u0131lamayabilir. Y\u00f6nlendiricinizin kullan\u0131m k\u0131lavuzuna bak\u0131n.", + "description": "T\u00fcm gereksinimlerin kar\u015f\u0131land\u0131\u011f\u0131ndan emin olmak i\u00e7in l\u00fctfen belgelere bak\u0131n.", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goalzero/translations/zh-Hant.json b/homeassistant/components/goalzero/translations/zh-Hant.json index 13c49f8d2ac..4f5b01fb9d4 100644 --- a/homeassistant/components/goalzero/translations/zh-Hant.json +++ b/homeassistant/components/goalzero/translations/zh-Hant.json @@ -20,7 +20,7 @@ "host": "\u4e3b\u6a5f\u7aef", "name": "\u540d\u7a31" }, - "description": "\u9996\u5148\u5fc5\u9808\u5148\u4e0b\u8f09 Goal Zero app\uff1ahttps://www.goalzero.com/product-features/yeti-app/\n\n\u8ddf\u96a8\u6307\u793a\u5c07 Yeti \u9023\u7dda\u81f3\u7121\u7dda\u7db2\u8def\u3002\u5efa\u8b70\u65bc\u8def\u7531\u5668\u7684 DHCP \u8a2d\u5b9a\u4e2d\u4fdd\u7559\u56fa\u5b9a IP\uff0c\u5047\u5982\u672a\u8a2d\u5b9a\u3001\u88dd\u7f6e\u53ef\u80fd\u6703\u5728 Home Assistant \u5075\u6e2c\u5230\u65b0 IP \u4e4b\u524d\u8b8a\u6210\u7121\u6cd5\u4f7f\u7528\u3002\u8acb\u53c3\u95b1\u8def\u7531\u5668\u624b\u518a\u4e86\u89e3\u5982\u4f55\u8a2d\u5b9a\u3002", + "description": "\u8acb\u53c3\u8003\u76f8\u95dc\u6587\u4ef6\u4ee5\u4e86\u89e3\u6240\u6709\u5fc5\u8981\u9700\u6c42\u3002", "title": "Goal Zero Yeti" } } diff --git a/homeassistant/components/goodwe/translations/hu.json b/homeassistant/components/goodwe/translations/hu.json index 6f3fa5bbff3..e6086eab7aa 100644 --- a/homeassistant/components/goodwe/translations/hu.json +++ b/homeassistant/components/goodwe/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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve" }, "error": { "connection_error": "Sikertelen csatlakoz\u00e1s" diff --git a/homeassistant/components/google/translations/bg.json b/homeassistant/components/google/translations/bg.json new file mode 100644 index 00000000000..38b08fc3616 --- /dev/null +++ b/homeassistant/components/google/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", + "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": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d\u043e" + }, + "step": { + "auth": { + "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0430\u043a\u0430\u0443\u043d\u0442 \u0432 Google" + }, + "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/google/translations/ca.json b/homeassistant/components/google/translations/ca.json new file mode 100644 index 00000000000..829ce1413d5 --- /dev/null +++ b/homeassistant/components/google/translations/ca.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "code_expired": "El codi d'autenticaci\u00f3 ha caducat o la configuraci\u00f3 de credencials no \u00e9s v\u00e0lida. Torna-ho a provar.", + "invalid_access_token": "Token d'acc\u00e9s inv\u00e0lid", + "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", + "oauth_error": "S'han rebut dades token inv\u00e0lides.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa" + }, + "progress": { + "exchange": "Per enlla\u00e7ar un compte de Google, v\u00e9s a [{url}]({url}) i introdueix el codi: \n\n{user_code}" + }, + "step": { + "auth": { + "title": "Vinculaci\u00f3 amb compte de Google" + }, + "pick_implementation": { + "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + }, + "reauth_confirm": { + "description": "La integraci\u00f3 Google Calendar ha de tornar a autenticar-se amb el teu compte", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/cs.json b/homeassistant/components/google/translations/cs.json new file mode 100644 index 00000000000..0c11a65f69b --- /dev/null +++ b/homeassistant/components/google/translations/cs.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "invalid_access_token": "Neplatn\u00fd p\u0159\u00edstupov\u00fd token", + "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", + "oauth_error": "P\u0159ijata neplatn\u00e1 data tokenu.", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + }, + "create_entry": { + "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" + }, + "step": { + "pick_implementation": { + "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + }, + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/de.json b/homeassistant/components/google/translations/de.json new file mode 100644 index 00000000000..1b15c465497 --- /dev/null +++ b/homeassistant/components/google/translations/de.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "code_expired": "Der Authentifizierungscode ist abgelaufen oder die Anmeldedaten sind ung\u00fcltig, bitte versuche es erneut.", + "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "oauth_error": "Ung\u00fcltige Token-Daten empfangen.", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "create_entry": { + "default": "Erfolgreich authentifiziert" + }, + "progress": { + "exchange": "Um dein Google-Konto zu verkn\u00fcpfen, besuche [{url}]({url}) und gib folgenden Code ein:\n\n{user_code}" + }, + "step": { + "auth": { + "title": "Google-Konto verkn\u00fcpfen" + }, + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + }, + "reauth_confirm": { + "description": "Die Google Kalender-Integration muss dein Konto erneut authentifizieren", + "title": "Integration erneut authentifizieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/el.json b/homeassistant/components/google/translations/el.json new file mode 100644 index 00000000000..11c78f96a93 --- /dev/null +++ b/homeassistant/components/google/translations/el.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "code_expired": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ad\u03bb\u03b7\u03be\u03b5 \u03ae \u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03b7\u03c1\u03af\u03c9\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ac\u03ba\u03c5\u03c1\u03b7, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", + "invalid_access_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "missing_configuration": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", + "oauth_error": "\u039b\u03ae\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd.", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + }, + "create_entry": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "progress": { + "exchange": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Google, \u03b5\u03c0\u03b9\u03c3\u03ba\u03b5\u03c6\u03b8\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf [{url}]({url}) \u03ba\u03b1\u03b9 \u03c0\u03bb\u03b7\u03ba\u03c4\u03c1\u03bf\u03bb\u03bf\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc:\n\n{user_code}" + }, + "step": { + "auth": { + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Google" + }, + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "reauth_confirm": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Nest \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bb\u03ad\u03b3\u03be\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03b7\u03bd \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c4\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03c3\u03b1\u03c2", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/es.json b/homeassistant/components/google/translations/es.json new file mode 100644 index 00000000000..8072ac95d4b --- /dev/null +++ b/homeassistant/components/google/translations/es.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n." + }, + "step": { + "auth": { + "title": "Vincular cuenta de Google" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/et.json b/homeassistant/components/google/translations/et.json new file mode 100644 index 00000000000..27dfa3f5290 --- /dev/null +++ b/homeassistant/components/google/translations/et.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "code_expired": "Tuvastuskood on aegunud v\u00f5i mandaadi seadistus on vale, proovi uuesti.", + "invalid_access_token": "Vigane juurdep\u00e4\u00e4sut\u00f5end", + "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.", + "oauth_error": "Saadi sobimatud loaandmed.", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "create_entry": { + "default": "Tuvastamine \u00f5nnestus" + }, + "progress": { + "exchange": "Google'i konto linkimiseks k\u00fclasta aadressi [ {url} ]( {url} ) ja sisesta kood: \n\n {user_code}" + }, + "step": { + "auth": { + "title": "Google'i konto linkimine" + }, + "pick_implementation": { + "title": "Vali tuvastusmeetod" + }, + "reauth_confirm": { + "description": "Google'i kalendri sidumine peab konto uuesti tuvastama", + "title": "Taastuvasta sidumine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/fr.json b/homeassistant/components/google/translations/fr.json new file mode 100644 index 00000000000..1224ba76c9b --- /dev/null +++ b/homeassistant/components/google/translations/fr.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "code_expired": "Le code d'authentification a expir\u00e9 ou la configuration des informations d'identification n'est pas valide, veuillez r\u00e9essayer.", + "invalid_access_token": "Jeton d'acc\u00e8s non valide", + "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", + "oauth_error": "Des donn\u00e9es de jeton non valides ont \u00e9t\u00e9 re\u00e7ues.", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "create_entry": { + "default": "Authentification r\u00e9ussie" + }, + "progress": { + "exchange": "Afin d'associer votre compte Google, rendez-vous sur [{url}]({url}) et saisissez le code suivant\u00a0:\n\n{user_code}" + }, + "step": { + "auth": { + "title": "Associer un compte Google" + }, + "pick_implementation": { + "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + }, + "reauth_confirm": { + "description": "L'int\u00e9gration Google Agenda doit r\u00e9-authentifier votre compte", + "title": "R\u00e9-authentifier l'int\u00e9gration" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/he.json b/homeassistant/components/google/translations/he.json new file mode 100644 index 00000000000..df5ec28163e --- /dev/null +++ b/homeassistant/components/google/translations/he.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "invalid_access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", + "oauth_error": "\u05d4\u05ea\u05e7\u05d1\u05dc\u05d5 \u05e0\u05ea\u05d5\u05e0\u05d9 \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd.", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "create_entry": { + "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" + }, + "step": { + "auth": { + "title": "\u05e7\u05d9\u05e9\u05d5\u05e8 \u05d7\u05e9\u05d1\u05d5\u05df \u05d2\u05d5\u05d2\u05dc" + }, + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + }, + "reauth_confirm": { + "description": "\u05e9\u05d9\u05dc\u05d5\u05d1 Nest \u05e6\u05e8\u05d9\u05da \u05dc\u05d0\u05de\u05ea \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da", + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/hu.json b/homeassistant/components/google/translations/hu.json new file mode 100644 index 00000000000..2c552eca30e --- /dev/null +++ b/homeassistant/components/google/translations/hu.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "code_expired": "A hiteles\u00edt\u00e9si k\u00f3d lej\u00e1rt vagy a hiteles\u00edt\u0151 adatok be\u00e1ll\u00edt\u00e1sa \u00e9rv\u00e9nytelen, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra.", + "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", + "oauth_error": "\u00c9rv\u00e9nytelen token adatok \u00e9rkeztek.", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + }, + "create_entry": { + "default": "Sikeres hiteles\u00edt\u00e9s" + }, + "progress": { + "exchange": "Google-fi\u00f3kja \u00f6sszekapcsol\u00e1s\u00e1hoz keresse fel a [{url}]({url}) c\u00edmet, \u00e9s \u00edrja be a k\u00f3dot: \n\n{user_code}" + }, + "step": { + "auth": { + "title": "\u00d6sszekapcsol\u00e1s Google-al" + }, + "pick_implementation": { + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" + }, + "reauth_confirm": { + "description": "A Google Napt\u00e1r integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie fi\u00f3kj\u00e1t", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/id.json b/homeassistant/components/google/translations/id.json new file mode 100644 index 00000000000..371fa7551b5 --- /dev/null +++ b/homeassistant/components/google/translations/id.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "code_expired": "Kode autentikasi kedaluwarsa atau penyiapan kredensial tidak valid, silakan coba lagi.", + "invalid_access_token": "Token akses tidak valid", + "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", + "oauth_error": "Menerima respons token yang tidak valid.", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "create_entry": { + "default": "Berhasil diautentikasi" + }, + "progress": { + "exchange": "Untuk menautkan akun Google Anda, kunjungi [{url}]({url}) dan masukkan kode:\n\n{user_code}" + }, + "step": { + "auth": { + "title": "Tautkan Akun Google" + }, + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + }, + "reauth_confirm": { + "description": "Integrasi Google Kalender perlu mengautentikasi ulang akun Anda", + "title": "Autentikasi Ulang Integrasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/it.json b/homeassistant/components/google/translations/it.json new file mode 100644 index 00000000000..4530a690ed6 --- /dev/null +++ b/homeassistant/components/google/translations/it.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "code_expired": "Il codice di autenticazione \u00e8 scaduto o la configurazione delle credenziali non \u00e8 valida, riprova.", + "invalid_access_token": "Token di accesso non valido", + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", + "oauth_error": "Ricevuti dati token non validi.", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "create_entry": { + "default": "Autenticazione riuscita" + }, + "progress": { + "exchange": "Per collegare il tuo account Google, visita [{url}]({url}) e inserisci il codice: \n\n {user_code}" + }, + "step": { + "auth": { + "title": "Collega l'account Google" + }, + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + }, + "reauth_confirm": { + "description": "L'integrazione di Google Calendar deve riautenticare il tuo account", + "title": "Autentica nuovamente l'integrazione" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/ja.json b/homeassistant/components/google/translations/ja.json new file mode 100644 index 00000000000..7eab5abc6f6 --- /dev/null +++ b/homeassistant/components/google/translations/ja.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "code_expired": "\u8a8d\u8a3c\u30b3\u30fc\u30c9\u306e\u6709\u52b9\u671f\u9650\u304c\u5207\u308c\u3066\u3044\u308b\u304b\u3001\u8cc7\u683c\u60c5\u5831\u306e\u8a2d\u5b9a\u304c\u7121\u52b9\u3067\u3059\u3002\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", + "invalid_access_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "oauth_error": "\u7121\u52b9\u306a\u30c8\u30fc\u30af\u30f3\u30c7\u30fc\u30bf\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f\u3002", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "create_entry": { + "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" + }, + "progress": { + "exchange": "Google\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30ea\u30f3\u30af\u3059\u308b\u306b\u306f\u3001 [{url}]({url}) \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044: \n\n{user_code}" + }, + "step": { + "auth": { + "title": "Google\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30ea\u30f3\u30af\u3059\u308b" + }, + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + }, + "reauth_confirm": { + "description": "Nest\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/nl.json b/homeassistant/components/google/translations/nl.json new file mode 100644 index 00000000000..e732fd7cd19 --- /dev/null +++ b/homeassistant/components/google/translations/nl.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "already_in_progress": "Configuratie flow is al in bewerking", + "code_expired": "De authenticatiecode is verlopen of de instelling van de inloggegevens is ongeldig, probeer het opnieuw.", + "invalid_access_token": "Ongeldige toegang token", + "missing_configuration": "Het component is niet geconfigureerd. Volg a.u.b. de documentatie", + "oauth_error": "Ongeldige token data ontvangen", + "reauth_successful": "Herauthentiecatie was succesvol" + }, + "create_entry": { + "default": "Authenticatie succesvol" + }, + "progress": { + "exchange": "Om uw Google-account te koppelen, gaat u naar de [ {url} ]( {url} ) en voert u de code in: \n\n {user_code}" + }, + "step": { + "auth": { + "title": "Link Google Account" + }, + "pick_implementation": { + "title": "Kies een authenticatie methode" + }, + "reauth_confirm": { + "description": "De Google Agenda-integratie moet uw account opnieuw verifi\u00ebren", + "title": "Herauthentiseer integratie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/no.json b/homeassistant/components/google/translations/no.json new file mode 100644 index 00000000000..4065583192c --- /dev/null +++ b/homeassistant/components/google/translations/no.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "code_expired": "Autentiseringskoden er utl\u00f8pt eller p\u00e5loggingsoppsettet er ugyldig. Pr\u00f8v p\u00e5 nytt.", + "invalid_access_token": "Ugyldig tilgangstoken", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", + "oauth_error": "Mottatt ugyldige token data.", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "create_entry": { + "default": "Vellykket godkjenning" + }, + "progress": { + "exchange": "Hvis du vil knytte sammen Google-kontoen din, g\u00e5r du til [{url}]({url}) og skriver inn kode:\n\n{user_code}" + }, + "step": { + "auth": { + "title": "Koble til Google-kontoen" + }, + "pick_implementation": { + "title": "Velg godkjenningsmetode" + }, + "reauth_confirm": { + "description": "Google Kalender-integrasjonen m\u00e5 autentisere kontoen din p\u00e5 nytt", + "title": "Godkjenne integrering p\u00e5 nytt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/pl.json b/homeassistant/components/google/translations/pl.json new file mode 100644 index 00000000000..b4f45c0abe4 --- /dev/null +++ b/homeassistant/components/google/translations/pl.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "code_expired": "Kod uwierzytelniaj\u0105cy wygas\u0142 lub konfiguracja po\u015bwiadcze\u0144 jest nieprawid\u0142owa, spr\u00f3buj ponownie.", + "invalid_access_token": "Niepoprawny token dost\u0119pu", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "oauth_error": "Otrzymano nieprawid\u0142owe dane tokena.", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono" + }, + "progress": { + "exchange": "Aby po\u0142\u0105czy\u0107 swoje konto Google, odwied\u017a [{url}]({url}) i wpisz kod: \n\n{user_code}" + }, + "step": { + "auth": { + "title": "Po\u0142\u0105czenie z kontem Google" + }, + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + }, + "reauth_confirm": { + "description": "Integracja Kalendarz Google wymaga ponownego uwierzytelnienia Twojego konta", + "title": "Ponownie uwierzytelnij integracj\u0119" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/pt-BR.json b/homeassistant/components/google/translations/pt-BR.json new file mode 100644 index 00000000000..8ab124f1b5c --- /dev/null +++ b/homeassistant/components/google/translations/pt-BR.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "code_expired": "O c\u00f3digo de autentica\u00e7\u00e3o expirou ou a configura\u00e7\u00e3o da credencial \u00e9 inv\u00e1lida. Tente novamente.", + "invalid_access_token": "Token de acesso inv\u00e1lido", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "oauth_error": "Dados de token recebidos inv\u00e1lidos.", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "progress": { + "exchange": "Para vincular sua conta do Google, visite o [{url}]({url}) e insira o c\u00f3digo: \n\n {user_code}" + }, + "step": { + "auth": { + "title": "Vincular Conta do Google" + }, + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "description": "A integra\u00e7\u00e3o do Google Agenda precisa autenticar novamente sua conta", + "title": "Reautenticar Integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/ru.json b/homeassistant/components/google/translations/ru.json new file mode 100644 index 00000000000..a7db4b2f48b --- /dev/null +++ b/homeassistant/components/google/translations/ru.json @@ -0,0 +1,31 @@ +{ + "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_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.", + "code_expired": "\u0421\u0440\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043a\u043e\u0434\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0438\u0441\u0442\u0435\u043a \u0438\u043b\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u044b \u043d\u0435\u0432\u0435\u0440\u043d\u043e, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", + "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", + "oauth_error": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u044b \u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0442\u043e\u043a\u0435\u043d\u0430.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "create_entry": { + "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "progress": { + "exchange": "\u0427\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Google, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 [{url}]({url}) \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u044d\u0442\u043e\u0442 \u043a\u043e\u0434: \n\n{user_code}" + }, + "step": { + "auth": { + "title": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Google" + }, + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "reauth_confirm": { + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Google.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/tr.json b/homeassistant/components/google/translations/tr.json new file mode 100644 index 00000000000..9d5fc8d2416 --- /dev/null +++ b/homeassistant/components/google/translations/tr.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "code_expired": "Kimlik do\u011frulama kodunun s\u00fcresi doldu veya kimlik bilgisi kurulumu ge\u00e7ersiz, l\u00fctfen tekrar deneyin.", + "invalid_access_token": "Ge\u00e7ersiz eri\u015fim anahtar\u0131", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "oauth_error": "Ge\u00e7ersiz anahtar verileri al\u0131nd\u0131.", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, + "progress": { + "exchange": "Google hesab\u0131n\u0131z\u0131 ba\u011flamak i\u00e7in [ {url} ]( {url} ) adresini ziyaret edin ve kodu girin: \n\n {user_code}" + }, + "step": { + "auth": { + "title": "Google Hesab\u0131n\u0131 Ba\u011fla" + }, + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + }, + "reauth_confirm": { + "description": "Google Takvim entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor", + "title": "Entegrasyonu Yeniden Do\u011frula" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/zh-Hant.json b/homeassistant/components/google/translations/zh-Hant.json new file mode 100644 index 00000000000..70e7d81c01e --- /dev/null +++ b/homeassistant/components/google/translations/zh-Hant.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "code_expired": "\u8a8d\u8b49\u78bc\u5df2\u904e\u671f\u6216\u6191\u8b49\u8a2d\u5b9a\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", + "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548", + "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "oauth_error": "\u6536\u5230\u7121\u6548\u7684\u6b0a\u6756\u8cc7\u6599\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" + }, + "progress": { + "exchange": "\u6b32\u9023\u7d50 Google \u5e33\u865f\u3001\u8acb\u700f\u89bd [{url}]({url}) \u4e26\u8f38\u5165\u4ee3\u78bc\uff1a\n{user_code}" + }, + "step": { + "auth": { + "title": "\u9023\u7d50 Google \u5e33\u865f" + }, + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, + "reauth_confirm": { + "description": "Google Calendar \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/hu.json b/homeassistant/components/google_travel_time/translations/hu.json index 85a15a98e58..03cfdc18b59 100644 --- a/homeassistant/components/google_travel_time/translations/hu.json +++ b/homeassistant/components/google_travel_time/translations/hu.json @@ -11,7 +11,7 @@ "data": { "api_key": "Api kucs", "destination": "C\u00e9l", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "origin": "Eredet" }, "description": "Az eredet \u00e9s a c\u00e9l megad\u00e1sakor megadhat egy vagy t\u00f6bb helyet a pipa karakterrel elv\u00e1lasztva, c\u00edm, sz\u00e9less\u00e9gi / hossz\u00fas\u00e1gi koordin\u00e1t\u00e1k vagy Google helyazonos\u00edt\u00f3 form\u00e1j\u00e1ban. Amikor a helyet megadja egy Google helyazonos\u00edt\u00f3val, akkor az azonos\u00edt\u00f3t el\u0151taggal kell ell\u00e1tni a `hely_azonos\u00edt\u00f3:` sz\u00f3val." diff --git a/homeassistant/components/group/translations/bg.json b/homeassistant/components/group/translations/bg.json index d28a170bcd0..584d48e3f28 100644 --- a/homeassistant/components/group/translations/bg.json +++ b/homeassistant/components/group/translations/bg.json @@ -1,9 +1,116 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435", + "name": "\u0418\u043c\u0435" + }, + "title": "\u041d\u043e\u0432\u0430 \u0433\u0440\u0443\u043f\u0430" + }, + "cover": { + "data": { + "title": "\u041d\u043e\u0432\u0430 \u0433\u0440\u0443\u043f\u0430" + } + }, + "fan": { + "data": { + "title": "\u041d\u043e\u0432\u0430 \u0433\u0440\u0443\u043f\u0430" + } + }, + "light": { + "data": { + "all": "\u0412\u0441\u0438\u0447\u043a\u0438 \u043e\u0431\u0435\u043a\u0442\u0438", + "title": "\u041d\u043e\u0432\u0430 \u0433\u0440\u0443\u043f\u0430" + } + }, + "lock": { + "data": { + "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435", + "name": "\u0418\u043c\u0435" + } + }, "media_player": { "data": { - "name": "\u0418\u043c\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0430" + "name": "\u0418\u043c\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0430", + "title": "\u041d\u043e\u0432\u0430 \u0433\u0440\u0443\u043f\u0430" + } + }, + "switch": { + "data": { + "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435", + "name": "\u0418\u043c\u0435" + }, + "title": "\u041d\u043e\u0432\u0430 \u0433\u0440\u0443\u043f\u0430" + }, + "user": { + "title": "\u041d\u043e\u0432\u0430 \u0433\u0440\u0443\u043f\u0430" + } + } + }, + "options": { + "step": { + "binary_sensor": { + "data": { + "all": "\u0412\u0441\u0438\u0447\u043a\u0438 \u043e\u0431\u0435\u043a\u0442\u0438", + "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" + } + }, + "binary_sensor_options": { + "data": { + "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" + } + }, + "cover": { + "data": { + "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" + } + }, + "cover_options": { + "data": { + "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" + } + }, + "fan": { + "data": { + "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" + } + }, + "fan_options": { + "data": { + "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" + } + }, + "light": { + "data": { + "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" + } + }, + "light_options": { + "data": { + "all": "\u0412\u0441\u0438\u0447\u043a\u0438 \u043e\u0431\u0435\u043a\u0442\u0438", + "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" + } + }, + "lock": { + "data": { + "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" + } + }, + "media_player": { + "data": { + "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" + } + }, + "media_player_options": { + "data": { + "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" + } + }, + "switch": { + "data": { + "all": "\u0412\u0441\u0438\u0447\u043a\u0438 \u043e\u0431\u0435\u043a\u0442\u0438", + "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" } } } diff --git a/homeassistant/components/group/translations/ca.json b/homeassistant/components/group/translations/ca.json index b17d98b5084..bd824fc7428 100644 --- a/homeassistant/components/group/translations/ca.json +++ b/homeassistant/components/group/translations/ca.json @@ -5,18 +5,21 @@ "data": { "all": "Totes les entitats", "entities": "Membres", + "hide_members": "Amaga membres", "name": "Nom" }, "description": "Si \"totes les entitats\" est\u00e0 activat, l'estat del grup estar\u00e0 activat (ON) si tots els membres estan activats. Si \"totes les entitats\" est\u00e0 desactivat, l'estat del grup s'activar\u00e0 si hi ha activat qualsevol membre.", - "title": "Nou grup" + "title": "Afegeix grup" }, "cover": { "data": { "entities": "Membres", + "hide_members": "Amaga membres", "name": "Nom", - "title": "Nou grup" + "title": "Afegeix grup" }, - "description": "Selecciona les opcions del grup" + "description": "Selecciona les opcions del grup", + "title": "Afegeix grup" }, "cover_options": { "data": { @@ -27,10 +30,12 @@ "fan": { "data": { "entities": "Membres", + "hide_members": "Amaga membres", "name": "Nom", - "title": "Nou grup" + "title": "Afegeix grup" }, - "description": "Selecciona les opcions del grup" + "description": "Selecciona les opcions del grup", + "title": "Afegeix grup" }, "fan_options": { "data": { @@ -46,11 +51,14 @@ }, "light": { "data": { + "all": "Totes les entitats", "entities": "Membres", + "hide_members": "Amaga membres", "name": "Nom", - "title": "Nou grup" + "title": "Afegeix grup" }, - "description": "Selecciona les opcions del grup" + "description": "Si \"totes les entitats\" est\u00e0 activat, l'estat del grup estar\u00e0 activat (ON) si tots els membres estan activats. Si \"totes les entitats\" est\u00e0 desactivat, l'estat del grup s'activar\u00e0 si hi ha activat qualsevol membre.", + "title": "Afegeix grup" }, "light_options": { "data": { @@ -58,13 +66,23 @@ }, "description": "Selecciona les opcions del grup" }, + "lock": { + "data": { + "entities": "Membres", + "hide_members": "Amaga membres", + "name": "Nom" + }, + "title": "Afegeix grup" + }, "media_player": { "data": { "entities": "Membres", + "hide_members": "Amaga membres", "name": "Nom", - "title": "Nou grup" + "title": "Afegeix grup" }, - "description": "Selecciona les opcions del grup" + "description": "Selecciona les opcions del grup", + "title": "Afegeix grup" }, "media_player_options": { "data": { @@ -72,41 +90,108 @@ }, "description": "Selecciona les opcions del grup" }, + "switch": { + "data": { + "entities": "Membres", + "hide_members": "Amaga membres", + "name": "Nom" + }, + "title": "Afegeix grup" + }, "user": { "data": { "group_type": "Tipus de grup" }, - "title": "Nou grup" + "description": "Els grups et permeten crear una nova entitat que representa m\u00faltiples entitats del mateix tipus.", + "menu_options": { + "binary_sensor": "Grup de sensors binaris", + "cover": "Grup de cobertes", + "fan": "Grup de ventiladors", + "light": "Grup de llums", + "lock": "Grup de panys", + "media_player": "Grup de reproductors multim\u00e8dia", + "switch": "Grup de commutadors" + }, + "title": "Afegeix grup" } } }, "options": { "step": { + "binary_sensor": { + "data": { + "all": "Totes les entitats", + "entities": "Membres", + "hide_members": "Amaga membres" + }, + "description": "Si \"totes les entitats\" est\u00e0 activat, l'estat del grup estar\u00e0 activat (ON) si tots els membres estan activats. Si \"totes les entitats\" est\u00e0 desactivat, l'estat del grup s'activar\u00e0 si hi ha activat qualsevol membre." + }, "binary_sensor_options": { "data": { "all": "Totes les entitats", "entities": "Membres" } }, + "cover": { + "data": { + "entities": "Membres", + "hide_members": "Amaga membres" + } + }, "cover_options": { "data": { "entities": "Membres" } }, + "fan": { + "data": { + "entities": "Membres", + "hide_members": "Amaga membres" + } + }, "fan_options": { "data": { "entities": "Membres" } }, + "light": { + "data": { + "all": "Totes les entitats", + "entities": "Membres", + "hide_members": "Amaga membres" + }, + "description": "Si \"totes les entitats\" est\u00e0 activat, l'estat del grup estar\u00e0 activat (ON) si tots els membres estan activats. Si \"totes les entitats\" est\u00e0 desactivat, l'estat del grup s'activar\u00e0 si hi ha activat qualsevol membre." + }, "light_options": { "data": { + "all": "Totes les entitats", "entities": "Membres" } }, + "lock": { + "data": { + "entities": "Membres", + "hide_members": "Amaga membres" + } + }, + "media_player": { + "data": { + "entities": "Membres", + "hide_members": "Amaga membres" + } + }, "media_player_options": { "data": { "entities": "Membres" } + }, + "switch": { + "data": { + "all": "Totes les entitats", + "entities": "Membres", + "hide_members": "Amaga membres" + }, + "description": "Si \"totes les entitats\" est\u00e0 activat, l'estat del grup estar\u00e0 activat (ON) si tots els membres estan activats. Si \"totes les entitats\" est\u00e0 desactivat, l'estat del grup s'activar\u00e0 si hi ha activat qualsevol membre." } } }, diff --git a/homeassistant/components/group/translations/cs.json b/homeassistant/components/group/translations/cs.json index d35e48058e7..232dbe9e629 100644 --- a/homeassistant/components/group/translations/cs.json +++ b/homeassistant/components/group/translations/cs.json @@ -1,4 +1,200 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "all": "V\u0161echny entity", + "entities": "\u010clenov\u00e9", + "hide_members": "Skr\u00fdt \u010dleny", + "name": "Jm\u00e9no" + }, + "description": "Pokud je povoleno \"v\u0161echny entity\", je stav skupiny zapnut\u00fd pouze tehdy, pokud jsou zapnuti v\u0161ichni \u010dlenov\u00e9.\nPokud je mo\u017enost \"v\u0161echny entity\" zak\u00e1z\u00e1na, je stav skupiny zapnut\u00fd, pokud je zapnut\u00fd kter\u00fdkoli \u010dlen.", + "title": "Nov\u00e1 skupina" + }, + "cover": { + "data": { + "entities": "\u010clenov\u00e9", + "hide_members": "Skr\u00fdt \u010dleny", + "name": "Jm\u00e9no", + "title": "Nov\u00e1 skupina" + }, + "description": "Vyberte mo\u017enosti skupiny", + "title": "Nov\u00e1 skupina" + }, + "cover_options": { + "data": { + "entities": "\u010clenov\u00e9 skupiny" + }, + "description": "Vyberte mo\u017enosti skupiny" + }, + "fan": { + "data": { + "entities": "\u010clenov\u00e9", + "hide_members": "Skr\u00fdt \u010dleny", + "name": "Jm\u00e9no", + "title": "Nov\u00e1 skupina" + }, + "description": "Vyberte mo\u017enosti skupiny", + "title": "Nov\u00e1 skupina" + }, + "fan_options": { + "data": { + "entities": "\u010clenov\u00e9 skupiny" + }, + "description": "Vyberte mo\u017enosti skupiny" + }, + "init": { + "data": { + "group_type": "Typ skupiny" + }, + "description": "Vyberte typ skupiny" + }, + "light": { + "data": { + "all": "V\u0161echny entity", + "entities": "\u010clenov\u00e9", + "hide_members": "Skr\u00fdt \u010dleny", + "name": "Jm\u00e9no", + "title": "Nov\u00e1 skupina" + }, + "description": "Pokud je povoleno \"v\u0161echny entity\", je stav skupiny zapnut\u00fd pouze tehdy, pokud jsou zapnuti v\u0161ichni \u010dlenov\u00e9.\nPokud je mo\u017enost \"v\u0161echny entity\" zak\u00e1z\u00e1na, je stav skupiny zapnut\u00fd, pokud je zapnut\u00fd kter\u00fdkoli \u010dlen.", + "title": "Nov\u00e1 skupina" + }, + "light_options": { + "data": { + "entities": "\u010clenov\u00e9 skupiny" + }, + "description": "Vyberte mo\u017enosti skupiny" + }, + "lock": { + "data": { + "entities": "\u010clenov\u00e9", + "hide_members": "Skr\u00fdt \u010dleny", + "name": "Jm\u00e9no" + }, + "title": "Nov\u00e1 skupina" + }, + "media_player": { + "data": { + "entities": "\u010clenov\u00e9", + "hide_members": "Skr\u00fdt \u010dleny", + "name": "Jm\u00e9no", + "title": "Nov\u00e1 skupina" + }, + "description": "Vyberte mo\u017enosti skupiny", + "title": "Nov\u00e1 skupina" + }, + "media_player_options": { + "data": { + "entities": "\u010clenov\u00e9 skupiny" + }, + "description": "Vyberte mo\u017enosti skupiny" + }, + "switch": { + "data": { + "entities": "\u010clenov\u00e9", + "hide_members": "Skr\u00fdt \u010dleny", + "name": "Jm\u00e9no" + }, + "title": "Nov\u00e1 skupina" + }, + "user": { + "data": { + "group_type": "Typ skupiny" + }, + "description": "Vyberte typ skupiny", + "menu_options": { + "binary_sensor": "Skupina bin\u00e1rn\u00edch senzor\u016f", + "cover": "Skupina rolet", + "fan": "Skupina ventil\u00e1tor\u016f", + "light": "Skupina sv\u011btel", + "lock": "Skupina z\u00e1mk\u016f", + "media_player": "Skupina p\u0159ehr\u00e1va\u010d\u016f m\u00e9di\u00ed", + "switch": "Skupina vyp\u00edna\u010d\u016f" + }, + "title": "Nov\u00e1 skupina" + } + } + }, + "options": { + "step": { + "binary_sensor": { + "data": { + "all": "V\u0161echny entity", + "entities": "\u010clenov\u00e9", + "hide_members": "Skr\u00fdt \u010dleny" + }, + "description": "Pokud je povoleno \"v\u0161echny entity\", je stav skupiny zapnut\u00fd pouze tehdy, pokud jsou zapnuti v\u0161ichni \u010dlenov\u00e9.\nPokud je mo\u017enost \"v\u0161echny entity\" zak\u00e1z\u00e1na, je stav skupiny zapnut\u00fd, pokud je zapnut\u00fd kter\u00fdkoli \u010dlen." + }, + "binary_sensor_options": { + "data": { + "all": "V\u0161echny entity", + "entities": "\u010clenov\u00e9" + } + }, + "cover": { + "data": { + "entities": "\u010clenov\u00e9", + "hide_members": "Skr\u00fdt \u010dleny" + } + }, + "cover_options": { + "data": { + "entities": "\u010clenov\u00e9" + } + }, + "fan": { + "data": { + "entities": "\u010clenov\u00e9", + "hide_members": "Skr\u00fdt \u010dleny" + } + }, + "fan_options": { + "data": { + "entities": "\u010clenov\u00e9" + } + }, + "light": { + "data": { + "all": "V\u0161echny entity", + "entities": "\u010clenov\u00e9", + "hide_members": "Skr\u00fdt \u010dleny" + }, + "description": "Pokud je povoleno \"v\u0161echny entity\", je stav skupiny zapnut\u00fd pouze tehdy, pokud jsou zapnuti v\u0161ichni \u010dlenov\u00e9.\nPokud je mo\u017enost \"v\u0161echny entity\" zak\u00e1z\u00e1na, je stav skupiny zapnut\u00fd, pokud je zapnut\u00fd kter\u00fdkoli \u010dlen." + }, + "light_options": { + "data": { + "all": "V\u0161echny entity", + "entities": "\u010clenov\u00e9" + } + }, + "lock": { + "data": { + "entities": "\u010clenov\u00e9", + "hide_members": "Skr\u00fdt \u010dleny" + } + }, + "media_player": { + "data": { + "entities": "\u010clenov\u00e9", + "hide_members": "Skr\u00fdt \u010dleny" + } + }, + "media_player_options": { + "data": { + "entities": "\u010clenov\u00e9" + } + }, + "switch": { + "data": { + "all": "V\u0161echny entity", + "entities": "\u010clenov\u00e9", + "hide_members": "Skr\u00fdt \u010dleny" + }, + "description": "Pokud je povoleno \"v\u0161echny entity\", je stav skupiny zapnut\u00fd pouze tehdy, pokud jsou zapnuti v\u0161ichni \u010dlenov\u00e9.\nPokud je mo\u017enost \"v\u0161echny entity\" zak\u00e1z\u00e1na, je stav skupiny zapnut\u00fd, pokud je zapnut\u00fd kter\u00fdkoli \u010dlen." + } + } + }, "state": { "_": { "closed": "Zav\u0159eno", diff --git a/homeassistant/components/group/translations/de.json b/homeassistant/components/group/translations/de.json index c06dd2a4a8f..aeaedd3a7cc 100644 --- a/homeassistant/components/group/translations/de.json +++ b/homeassistant/components/group/translations/de.json @@ -5,18 +5,21 @@ "data": { "all": "Alle Entit\u00e4ten", "entities": "Mitglieder", + "hide_members": "Mitglieder ausblenden", "name": "Name" }, "description": "Wenn \"alle Entit\u00e4ten\" aktiviert ist, ist der Status der Gruppe nur dann eingeschaltet, wenn alle Mitglieder eingeschaltet sind. Wenn \"alle Entit\u00e4ten\" deaktiviert ist, ist der Status der Gruppe eingeschaltet, wenn irgendein Mitglied eingeschaltet ist.", - "title": "Neue Gruppe" + "title": "Gruppe hinzuf\u00fcgen" }, "cover": { "data": { "entities": "Mitglieder", + "hide_members": "Mitglieder ausblenden", "name": "Name", - "title": "Neue Gruppe" + "title": "Gruppe hinzuf\u00fcgen" }, - "description": "Gruppenoptionen ausw\u00e4hlen" + "description": "Gruppenoptionen ausw\u00e4hlen", + "title": "Gruppe hinzuf\u00fcgen" }, "cover_options": { "data": { @@ -27,10 +30,12 @@ "fan": { "data": { "entities": "Mitglieder", + "hide_members": "Mitglieder ausblenden", "name": "Name", - "title": "Neue Gruppe" + "title": "Gruppe hinzuf\u00fcgen" }, - "description": "Gruppenoptionen ausw\u00e4hlen" + "description": "Gruppenoptionen ausw\u00e4hlen", + "title": "Gruppe hinzuf\u00fcgen" }, "fan_options": { "data": { @@ -46,11 +51,14 @@ }, "light": { "data": { + "all": "Alle Entit\u00e4ten", "entities": "Mitglieder", + "hide_members": "Mitglieder ausblenden", "name": "Name", - "title": "Neue Gruppe" + "title": "Gruppe hinzuf\u00fcgen" }, - "description": "Gruppenoptionen ausw\u00e4hlen" + "description": "Wenn \"alle Entit\u00e4ten\" aktiviert ist, ist der Status der Gruppe nur dann eingeschaltet, wenn alle Mitglieder eingeschaltet sind. Wenn \"alle Entit\u00e4ten\" deaktiviert ist, ist der Status der Gruppe eingeschaltet, wenn irgendein Mitglied eingeschaltet ist.", + "title": "Gruppe hinzuf\u00fcgen" }, "light_options": { "data": { @@ -58,13 +66,23 @@ }, "description": "Gruppenoptionen ausw\u00e4hlen" }, + "lock": { + "data": { + "entities": "Mitglieder", + "hide_members": "Mitglieder ausblenden", + "name": "Name" + }, + "title": "Gruppe hinzuf\u00fcgen" + }, "media_player": { "data": { "entities": "Mitglieder", + "hide_members": "Mitglieder ausblenden", "name": "Name", - "title": "Neue Gruppe" + "title": "Gruppe hinzuf\u00fcgen" }, - "description": "Gruppenoptionen ausw\u00e4hlen" + "description": "Gruppenoptionen ausw\u00e4hlen", + "title": "Gruppe hinzuf\u00fcgen" }, "media_player_options": { "data": { @@ -72,41 +90,108 @@ }, "description": "Gruppenoptionen ausw\u00e4hlen" }, + "switch": { + "data": { + "entities": "Mitglieder", + "hide_members": "Mitglieder ausblenden", + "name": "Name" + }, + "title": "Gruppe hinzuf\u00fcgen" + }, "user": { "data": { "group_type": "Gruppentyp" }, - "title": "Neue Gruppe" + "description": "Mit Gruppen kannst du eine neue Entit\u00e4t erstellen, die mehrere Entit\u00e4ten desselben Typs darstellt.", + "menu_options": { + "binary_sensor": "Bin\u00e4rer Sensor-Gruppe", + "cover": "Abdeckung-Gruppe", + "fan": "L\u00fcfter-Gruppe", + "light": "Licht-Gruppe", + "lock": "Gruppe sperren", + "media_player": "Media-Player-Gruppe", + "switch": "Gruppe wechseln" + }, + "title": "Gruppe hinzuf\u00fcgen" } } }, "options": { "step": { + "binary_sensor": { + "data": { + "all": "Alle Entit\u00e4ten", + "entities": "Mitglieder", + "hide_members": "Mitglieder ausblenden" + }, + "description": "Wenn \"alle Entit\u00e4ten\" aktiviert ist, ist der Status der Gruppe nur dann eingeschaltet, wenn alle Mitglieder eingeschaltet sind. Wenn \"alle Entit\u00e4ten\" deaktiviert ist, ist der Status der Gruppe eingeschaltet, wenn irgendein Mitglied eingeschaltet ist." + }, "binary_sensor_options": { "data": { "all": "Alle Entit\u00e4ten", "entities": "Mitglieder" } }, + "cover": { + "data": { + "entities": "Mitglieder", + "hide_members": "Mitglieder ausblenden" + } + }, "cover_options": { "data": { "entities": "Mitglieder" } }, + "fan": { + "data": { + "entities": "Mitglieder", + "hide_members": "Mitglieder ausblenden" + } + }, "fan_options": { "data": { "entities": "Mitglieder" } }, + "light": { + "data": { + "all": "Alle Entit\u00e4ten", + "entities": "Mitglieder", + "hide_members": "Mitglieder ausblenden" + }, + "description": "Wenn \"alle Entit\u00e4ten\" aktiviert ist, ist der Status der Gruppe nur dann eingeschaltet, wenn alle Mitglieder eingeschaltet sind. Wenn \"alle Entit\u00e4ten\" deaktiviert ist, ist der Status der Gruppe eingeschaltet, wenn irgendein Mitglied eingeschaltet ist." + }, "light_options": { "data": { + "all": "Alle Entit\u00e4ten", "entities": "Mitglieder" } }, + "lock": { + "data": { + "entities": "Mitglieder", + "hide_members": "Mitglieder ausblenden" + } + }, + "media_player": { + "data": { + "entities": "Mitglieder", + "hide_members": "Mitglieder ausblenden" + } + }, "media_player_options": { "data": { "entities": "Mitglieder" } + }, + "switch": { + "data": { + "all": "Alle Entit\u00e4ten", + "entities": "Mitglieder", + "hide_members": "Mitglieder ausblenden" + }, + "description": "Wenn \"alle Entit\u00e4ten\" aktiviert ist, ist der Status der Gruppe nur dann eingeschaltet, wenn alle Mitglieder eingeschaltet sind. Wenn \"alle Entit\u00e4ten\" deaktiviert ist, ist der Status der Gruppe eingeschaltet, wenn irgendein Mitglied eingeschaltet ist." } } }, diff --git a/homeassistant/components/group/translations/el.json b/homeassistant/components/group/translations/el.json index bebfb8f1cb8..edd67034de8 100644 --- a/homeassistant/components/group/translations/el.json +++ b/homeassistant/components/group/translations/el.json @@ -5,6 +5,7 @@ "data": { "all": "\u038c\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2", "entities": "\u039c\u03ad\u03bb\u03b7", + "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd", "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, "description": "\u0395\u03ac\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \"\u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\", \u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03bc\u03cc\u03bd\u03bf \u03b5\u03ac\u03bd \u03cc\u03bb\u03b1 \u03c4\u03b1 \u03bc\u03ad\u03bb\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b1. \u0395\u03ac\u03bd \u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \"\u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\" \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7, \u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b5\u03ac\u03bd \u03bf\u03c0\u03bf\u03b9\u03bf\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5 \u03bc\u03ad\u03bb\u03bf\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf.", @@ -13,10 +14,12 @@ "cover": { "data": { "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd", "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, "cover_options": { "data": { @@ -27,10 +30,12 @@ "fan": { "data": { "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd", "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, "fan_options": { "data": { @@ -46,11 +51,14 @@ }, "light": { "data": { + "all": "\u038c\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2", "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd", "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, "light_options": { "data": { @@ -58,13 +66,23 @@ }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" }, + "lock": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7", + "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + }, + "title": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, "media_player": { "data": { "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd", "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, "media_player_options": { "data": { @@ -72,41 +90,108 @@ }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" }, + "switch": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7", + "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + }, + "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" + }, "user": { "data": { "group_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" }, - "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03cd\u03c0\u03bf \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "menu_options": { + "binary_sensor": "\u039f\u03bc\u03ac\u03b4\u03b1 \u03b4\u03c5\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03c9\u03bd", + "cover": "\u039f\u03bc\u03ac\u03b4\u03b1 \u03ba\u03b1\u03bb\u03c5\u03bc\u03bc\u03ac\u03c4\u03c9\u03bd", + "fan": "\u039f\u03bc\u03ac\u03b4\u03b1 \u03b1\u03bd\u03b5\u03bc\u03b9\u03c3\u03c4\u03ae\u03c1\u03c9\u03bd", + "light": "\u039f\u03bc\u03ac\u03b4\u03b1 \u03c6\u03ce\u03c4\u03c9\u03bd", + "lock": "\u039f\u03bc\u03ac\u03b4\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2", + "media_player": "\u039f\u03bc\u03ac\u03b4\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd", + "switch": "\u0395\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + }, + "title": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" } } }, "options": { "step": { + "binary_sensor": { + "data": { + "all": "\u038c\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2", + "entities": "\u039c\u03ad\u03bb\u03b7", + "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd" + }, + "description": "\u0395\u03ac\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \"\u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\", \u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03bc\u03cc\u03bd\u03bf \u03b5\u03ac\u03bd \u03cc\u03bb\u03b1 \u03c4\u03b1 \u03bc\u03ad\u03bb\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b1. \u0395\u03ac\u03bd \u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \"\u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\" \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7, \u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b5\u03ac\u03bd \u03bf\u03c0\u03bf\u03b9\u03bf\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5 \u03bc\u03ad\u03bb\u03bf\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf." + }, "binary_sensor_options": { "data": { "all": "\u038c\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2", "entities": "\u039c\u03ad\u03bb\u03b7" } }, + "cover": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7", + "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd" + } + }, "cover_options": { "data": { "entities": "\u039c\u03ad\u03bb\u03b7" } }, + "fan": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7", + "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd" + } + }, "fan_options": { "data": { "entities": "\u039c\u03ad\u03bb\u03b7" } }, + "light": { + "data": { + "all": "\u038c\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2", + "entities": "\u039c\u03ad\u03bb\u03b7", + "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd" + }, + "description": "\u0395\u03ac\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \"\u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\", \u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03bc\u03cc\u03bd\u03bf \u03b5\u03ac\u03bd \u03cc\u03bb\u03b1 \u03c4\u03b1 \u03bc\u03ad\u03bb\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b1. \u0395\u03ac\u03bd \u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \"\u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\" \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7, \u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b5\u03ac\u03bd \u03bf\u03c0\u03bf\u03b9\u03bf\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5 \u03bc\u03ad\u03bb\u03bf\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf." + }, "light_options": { "data": { + "all": "\u038c\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2", "entities": "\u039c\u03ad\u03bb\u03b7" } }, + "lock": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7", + "hide_members": "\u0397 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + } + }, + "media_player": { + "data": { + "entities": "\u039c\u03ad\u03bb\u03b7", + "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd" + } + }, "media_player_options": { "data": { "entities": "\u039c\u03ad\u03bb\u03b7" } + }, + "switch": { + "data": { + "all": "\u038c\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2", + "entities": "\u039c\u03ad\u03bb\u03b7", + "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd" + }, + "description": "\u0395\u03ac\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \"\u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\", \u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03bc\u03cc\u03bd\u03bf \u03b5\u03ac\u03bd \u03cc\u03bb\u03b1 \u03c4\u03b1 \u03bc\u03ad\u03bb\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b1. \u0395\u03ac\u03bd \u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \"\u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\" \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7, \u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b5\u03ac\u03bd \u03bf\u03c0\u03bf\u03b9\u03bf\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5 \u03bc\u03ad\u03bb\u03bf\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf." } } }, diff --git a/homeassistant/components/group/translations/en.json b/homeassistant/components/group/translations/en.json index a67d23b812d..f7b3942c696 100644 --- a/homeassistant/components/group/translations/en.json +++ b/homeassistant/components/group/translations/en.json @@ -15,26 +15,57 @@ "data": { "entities": "Members", "hide_members": "Hide members", - "name": "Name" + "name": "Name", + "title": "Add Group" }, + "description": "Select group options", "title": "Add Group" }, + "cover_options": { + "data": { + "entities": "Group members" + }, + "description": "Select group options" + }, "fan": { "data": { "entities": "Members", "hide_members": "Hide members", - "name": "Name" + "name": "Name", + "title": "Add Group" }, + "description": "Select group options", "title": "Add Group" }, + "fan_options": { + "data": { + "entities": "Group members" + }, + "description": "Select group options" + }, + "init": { + "data": { + "group_type": "Group type" + }, + "description": "Select group type" + }, "light": { "data": { + "all": "All entities", "entities": "Members", "hide_members": "Hide members", - "name": "Name" + "name": "Name", + "title": "Add Group" }, + "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on.", "title": "Add Group" }, + "light_options": { + "data": { + "entities": "Group members" + }, + "description": "Select group options" + }, "lock": { "data": { "entities": "Members", @@ -47,10 +78,18 @@ "data": { "entities": "Members", "hide_members": "Hide members", - "name": "Name" + "name": "Name", + "title": "Add Group" }, + "description": "Select group options", "title": "Add Group" }, + "media_player_options": { + "data": { + "entities": "Group members" + }, + "description": "Select group options" + }, "switch": { "data": { "entities": "Members", @@ -60,6 +99,9 @@ "title": "Add Group" }, "user": { + "data": { + "group_type": "Group type" + }, "description": "Groups allow you to create a new entity that represents multiple entities of the same type.", "menu_options": { "binary_sensor": "Binary sensor group", @@ -84,18 +126,34 @@ }, "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on." }, + "binary_sensor_options": { + "data": { + "all": "All entities", + "entities": "Members" + } + }, "cover": { "data": { "entities": "Members", "hide_members": "Hide members" } }, + "cover_options": { + "data": { + "entities": "Members" + } + }, "fan": { "data": { "entities": "Members", "hide_members": "Hide members" } }, + "fan_options": { + "data": { + "entities": "Members" + } + }, "light": { "data": { "all": "All entities", @@ -104,6 +162,12 @@ }, "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on." }, + "light_options": { + "data": { + "all": "All entities", + "entities": "Members" + } + }, "lock": { "data": { "entities": "Members", @@ -116,6 +180,11 @@ "hide_members": "Hide members" } }, + "media_player_options": { + "data": { + "entities": "Members" + } + }, "switch": { "data": { "all": "All entities", diff --git a/homeassistant/components/group/translations/et.json b/homeassistant/components/group/translations/et.json index c45094aeedd..d0d1466e26b 100644 --- a/homeassistant/components/group/translations/et.json +++ b/homeassistant/components/group/translations/et.json @@ -5,6 +5,7 @@ "data": { "all": "K\u00f5ik olemid", "entities": "Liikmed", + "hide_members": "Peida grupi liikmed", "name": "Nimi" }, "description": "Kui \"k\u00f5ik olemid\" on lubatud,on r\u00fchma olek sees ainult siis kui k\u00f5ik liikmed on sisse l\u00fclitatud. Kui \"k\u00f5ik olemid\" on keelatud, on r\u00fchma olek sees kui m\u00f5ni liige on sisse l\u00fclitatud.", @@ -13,10 +14,12 @@ "cover": { "data": { "entities": "Liikmed", + "hide_members": "Peida grupi liikmed", "name": "Nimi", "title": "Uus r\u00fchm" }, - "description": "R\u00fchmasuvandite valimine" + "description": "R\u00fchmasuvandite valimine", + "title": "Uus grupp" }, "cover_options": { "data": { @@ -27,10 +30,12 @@ "fan": { "data": { "entities": "Liikmed", + "hide_members": "Peida grupi liikmed", "name": "Nimi", "title": "Uus r\u00fchm" }, - "description": "R\u00fchmasuvandite valimine" + "description": "R\u00fchmasuvandite valimine", + "title": "Uus grupp" }, "fan_options": { "data": { @@ -46,11 +51,14 @@ }, "light": { "data": { + "all": "K\u00f5ik olemid", "entities": "Liikmed", + "hide_members": "Peida grupi liikmed", "name": "Nimi", "title": "Uus r\u00fchm" }, - "description": "R\u00fchmasuvandite valimine" + "description": "Kui on vlitud \"K\u00f5ik olemid\" siis on grupi olek Sees kui k\u00f5ik olemid on sees. Kui \"K\u00f5ik olemid\" on keelatud siis on grupi olek Sees kui m\u00f5ni olem on sisse l\u00fclitatud.", + "title": "Uus grupp" }, "light_options": { "data": { @@ -58,13 +66,23 @@ }, "description": "R\u00fchmasuvandite valimine" }, + "lock": { + "data": { + "entities": "Liikmed", + "hide_members": "Peida liikmed", + "name": "Nimi" + }, + "title": "Uus grupp" + }, "media_player": { "data": { "entities": "Liikmed", + "hide_members": "Peida grupi liikmed", "name": "Nimi", "title": "Uus r\u00fchm" }, - "description": "R\u00fchmasuvandite valimine" + "description": "R\u00fchmasuvandite valimine", + "title": "Uus grupp" }, "media_player_options": { "data": { @@ -72,41 +90,108 @@ }, "description": "R\u00fchmasuvandite valimine" }, + "switch": { + "data": { + "entities": "Liikmed", + "hide_members": "Peida grupi liikmed", + "name": "Nimi" + }, + "title": "Uus grupp" + }, "user": { "data": { "group_type": "R\u00fchma t\u00fc\u00fcp" }, - "title": "Uus r\u00fchm" + "description": "R\u00fchmad v\u00f5imaldavad luua uue olemi,mis esindab mitut sama t\u00fc\u00fcpi olemit.", + "menu_options": { + "binary_sensor": "Olekuandurite r\u00fchm", + "cover": "Aknakatete r\u00fchm", + "fan": "Ventilaatorite r\u00fchm", + "light": "Valgustite r\u00fchm", + "lock": "Lukusta grupp", + "media_player": "Meediumipleieri r\u00fchm", + "switch": "Grupi vahetamine" + }, + "title": "Lisa grupp" } } }, "options": { "step": { + "binary_sensor": { + "data": { + "all": "K\u00f5ik olemid", + "entities": "Grupi liikmed", + "hide_members": "Peida grupi liikmed" + }, + "description": "Kui \"k\u00f5ik olemid\" on lubatud,on r\u00fchma olek sees ainult siis kui k\u00f5ik liikmed on sisse l\u00fclitatud. Kui \"k\u00f5ik olemid\" on keelatud, on r\u00fchma olek sees kui m\u00f5ni liige on sisse l\u00fclitatud." + }, "binary_sensor_options": { "data": { "all": "K\u00f5ik olemid", "entities": "Liikmed" } }, + "cover": { + "data": { + "entities": "Grupi liikmed", + "hide_members": "Peida grupi liikmed" + } + }, "cover_options": { "data": { "entities": "Liikmed" } }, + "fan": { + "data": { + "entities": "Grupi liikmed", + "hide_members": "Peida grupi liikmed" + } + }, "fan_options": { "data": { "entities": "Liikmed" } }, + "light": { + "data": { + "all": "K\u00f5ik olemid", + "entities": "Grupi liikmed", + "hide_members": "Peida grupi liikmed" + }, + "description": "Kui \"k\u00f5ik olemid\" on lubatud,on r\u00fchma olek sees ainult siis kui k\u00f5ik liikmed on sisse l\u00fclitatud. Kui \"k\u00f5ik olemid\" on keelatud, on r\u00fchma olek sees kui m\u00f5ni liige on sisse l\u00fclitatud." + }, "light_options": { "data": { + "all": "K\u00f5ik olemid", "entities": "Liikmed" } }, + "lock": { + "data": { + "entities": "Liikmed", + "hide_members": "Peida liikmed" + } + }, + "media_player": { + "data": { + "entities": "Grupi liikmed", + "hide_members": "Peida grupi liikmed" + } + }, "media_player_options": { "data": { "entities": "Liikmed" } + }, + "switch": { + "data": { + "all": "K\u00f5ik olemid", + "entities": "Liikmed", + "hide_members": "Peida grupi liikmed" + }, + "description": "Kui \"k\u00f5ik olemid\" on lubatud,on r\u00fchma olek sees ainult siis kui k\u00f5ik liikmed on sisse l\u00fclitatud. Kui \"k\u00f5ik olemid\" on keelatud, on r\u00fchma olek sees kui m\u00f5ni liige on sisse l\u00fclitatud." } } }, diff --git a/homeassistant/components/group/translations/fr.json b/homeassistant/components/group/translations/fr.json index 8e8b5ee4233..e8b74e2f374 100644 --- a/homeassistant/components/group/translations/fr.json +++ b/homeassistant/components/group/translations/fr.json @@ -5,18 +5,21 @@ "data": { "all": "Toutes les entit\u00e9s", "entities": "Membres", + "hide_members": "Cacher les membres", "name": "Nom" }, "description": "Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est activ\u00e9, l'\u00e9tat du groupe n'est activ\u00e9 que si tous les membres sont activ\u00e9s. Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est d\u00e9sactiv\u00e9, l'\u00e9tat du groupe est activ\u00e9 si au moins un membre est activ\u00e9.", - "title": "Nouveau groupe" + "title": "Ajouter un groupe" }, "cover": { "data": { "entities": "Membres", + "hide_members": "Cacher les membres", "name": "Nom", - "title": "Nouveau groupe" + "title": "Ajouter un groupe" }, - "description": "S\u00e9lectionnez les options du groupe" + "description": "S\u00e9lectionnez les options du groupe", + "title": "Ajouter un groupe" }, "cover_options": { "data": { @@ -27,10 +30,12 @@ "fan": { "data": { "entities": "Membres", + "hide_members": "Cacher les membres", "name": "Nom", - "title": "Nouveau groupe" + "title": "Ajouter un groupe" }, - "description": "S\u00e9lectionnez les options du groupe" + "description": "S\u00e9lectionnez les options du groupe", + "title": "Ajouter un groupe" }, "fan_options": { "data": { @@ -46,11 +51,14 @@ }, "light": { "data": { + "all": "Toutes les entit\u00e9s", "entities": "Membres", + "hide_members": "Cacher les membres", "name": "Nom", - "title": "Nouveau groupe" + "title": "Ajouter un groupe" }, - "description": "S\u00e9lectionnez les options du groupe" + "description": "Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est activ\u00e9, l'\u00e9tat du groupe n'est activ\u00e9 que si tous les membres sont activ\u00e9s. Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est d\u00e9sactiv\u00e9, l'\u00e9tat du groupe est activ\u00e9 si au moins un membre est activ\u00e9.", + "title": "Ajouter un groupe" }, "light_options": { "data": { @@ -58,13 +66,23 @@ }, "description": "S\u00e9lectionnez les options du groupe" }, + "lock": { + "data": { + "entities": "Membres", + "hide_members": "Cacher les membres", + "name": "Nom" + }, + "title": "Ajouter un groupe" + }, "media_player": { "data": { "entities": "Membres", + "hide_members": "Cacher les membres", "name": "Nom", - "title": "Nouveau groupe" + "title": "Ajouter un groupe" }, - "description": "S\u00e9lectionnez les options du groupe" + "description": "S\u00e9lectionnez les options du groupe", + "title": "Ajouter un groupe" }, "media_player_options": { "data": { @@ -72,41 +90,108 @@ }, "description": "S\u00e9lectionnez les options du groupe" }, + "switch": { + "data": { + "entities": "Membres", + "hide_members": "Cacher les membres", + "name": "Nom" + }, + "title": "Ajouter un groupe" + }, "user": { "data": { "group_type": "Type de groupe" }, - "title": "Nouveau groupe" + "description": "Les groupes vous permettent de cr\u00e9er une nouvelle entit\u00e9 repr\u00e9sentant plusieurs entit\u00e9s d'un m\u00eame type.", + "menu_options": { + "binary_sensor": "Groupe de capteurs binaires", + "cover": "Groupe de fermetures", + "fan": "Groupe de ventilateurs", + "light": "Groupe de lumi\u00e8res", + "lock": "Groupe de verrous", + "media_player": "Groupe de lecteurs multim\u00e9dia", + "switch": "Groupe d'interrupteurs" + }, + "title": "Ajouter un groupe" } } }, "options": { "step": { + "binary_sensor": { + "data": { + "all": "Toutes les entit\u00e9s", + "entities": "Membres", + "hide_members": "Cacher les membres" + }, + "description": "Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est activ\u00e9, l'\u00e9tat du groupe n'est activ\u00e9 que si tous les membres sont activ\u00e9s. Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est d\u00e9sactiv\u00e9, l'\u00e9tat du groupe est activ\u00e9 si au moins un membre est activ\u00e9." + }, "binary_sensor_options": { "data": { "all": "Toutes les entit\u00e9s", "entities": "Membres" } }, + "cover": { + "data": { + "entities": "Membres", + "hide_members": "Cacher les membres" + } + }, "cover_options": { "data": { "entities": "Membres" } }, + "fan": { + "data": { + "entities": "Membres", + "hide_members": "Cacher les membres" + } + }, "fan_options": { "data": { "entities": "Membres" } }, + "light": { + "data": { + "all": "Toutes les entit\u00e9s", + "entities": "Membres", + "hide_members": "Cacher les membres" + }, + "description": "Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est activ\u00e9, l'\u00e9tat du groupe n'est activ\u00e9 que si tous les membres sont activ\u00e9s. Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est d\u00e9sactiv\u00e9, l'\u00e9tat du groupe est activ\u00e9 si au moins un membre est activ\u00e9." + }, "light_options": { "data": { + "all": "Toutes les entit\u00e9s", "entities": "Membres" } }, + "lock": { + "data": { + "entities": "Membres", + "hide_members": "Cacher les membres" + } + }, + "media_player": { + "data": { + "entities": "Membres", + "hide_members": "Cacher les membres" + } + }, "media_player_options": { "data": { "entities": "Membres" } + }, + "switch": { + "data": { + "all": "Toutes les entit\u00e9s", + "entities": "Membres", + "hide_members": "Cacher les membres" + }, + "description": "Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est activ\u00e9, l'\u00e9tat du groupe n'est activ\u00e9 que si tous les membres sont activ\u00e9s. Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est d\u00e9sactiv\u00e9, l'\u00e9tat du groupe est activ\u00e9 si au moins un membre est activ\u00e9." } } }, diff --git a/homeassistant/components/group/translations/he.json b/homeassistant/components/group/translations/he.json index 06c1a8e0fa8..ab7a90cb699 100644 --- a/homeassistant/components/group/translations/he.json +++ b/homeassistant/components/group/translations/he.json @@ -5,6 +5,7 @@ "data": { "all": "\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea", "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd", "name": "\u05e9\u05dd" }, "description": "\u05d0\u05dd \u05d4\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d6\u05de\u05d9\u05e0\u05d4, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05e8\u05e7 \u05d0\u05dd \u05db\u05dc \u05d4\u05d7\u05d1\u05e8\u05d9\u05dd \u05e4\u05d5\u05e2\u05dc\u05d9\u05dd. \u05d0\u05dd \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d0\u05d9\u05e0\u05d5 \u05d6\u05de\u05d9\u05df, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05d0\u05dd \u05d7\u05d1\u05e8 \u05db\u05dc\u05e9\u05d4\u05d5 \u05e4\u05d5\u05e2\u05dc.", @@ -13,10 +14,12 @@ "cover": { "data": { "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd", "name": "\u05e9\u05dd", "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" }, - "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4", + "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" }, "cover_options": { "data": { @@ -27,10 +30,12 @@ "fan": { "data": { "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd", "name": "\u05e9\u05dd", "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" }, - "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4", + "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" }, "fan_options": { "data": { @@ -46,11 +51,14 @@ }, "light": { "data": { + "all": "\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea", "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd", "name": "\u05e9\u05dd", "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" }, - "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + "description": "\u05d0\u05dd \u05d4\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d6\u05de\u05d9\u05e0\u05d4, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05e8\u05e7 \u05d0\u05dd \u05db\u05dc \u05d4\u05d7\u05d1\u05e8\u05d9\u05dd \u05e4\u05d5\u05e2\u05dc\u05d9\u05dd. \u05d0\u05dd \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d0\u05d9\u05e0\u05d5 \u05d6\u05de\u05d9\u05df, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05d0\u05dd \u05d7\u05d1\u05e8 \u05db\u05dc\u05e9\u05d4\u05d5 \u05e4\u05d5\u05e2\u05dc.", + "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" }, "light_options": { "data": { @@ -58,13 +66,23 @@ }, "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" }, + "lock": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd", + "name": "\u05e9\u05dd" + }, + "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + }, "media_player": { "data": { "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd", "name": "\u05e9\u05dd", "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" }, - "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4", + "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" }, "media_player_options": { "data": { @@ -72,47 +90,112 @@ }, "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" }, + "switch": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd", + "name": "\u05e9\u05dd" + }, + "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + }, "user": { "data": { "group_type": "\u05e1\u05d5\u05d2 \u05e7\u05d1\u05d5\u05e6\u05d4" }, + "description": "\u05e7\u05d1\u05d5\u05e6\u05d5\u05ea \u05de\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05da \u05dc\u05d9\u05e6\u05d5\u05e8 \u05d9\u05e9\u05d5\u05ea \u05d7\u05d3\u05e9\u05d4 \u05d4\u05de\u05d9\u05d9\u05e6\u05d2\u05ea \u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05de\u05e8\u05d5\u05d1\u05d5\u05ea \u05de\u05d0\u05d5\u05ea\u05d5 \u05e1\u05d5\u05d2.", + "menu_options": { + "binary_sensor": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05d1\u05d9\u05e0\u05d0\u05e8\u05d9\u05d9\u05dd", + "cover": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05d5\u05d9\u05dc\u05d5\u05e0\u05d5\u05ea", + "fan": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05d0\u05d9\u05d5\u05d5\u05e8\u05d5\u05e8", + "light": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05ea\u05d0\u05d5\u05e8\u05d4", + "media_player": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05e0\u05d2\u05e0\u05d9 \u05de\u05d3\u05d9\u05d4" + }, "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" } } }, "options": { "step": { + "binary_sensor": { + "data": { + "all": "\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea", + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd" + }, + "description": "\u05d0\u05dd \u05d4\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d6\u05de\u05d9\u05e0\u05d4, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05e8\u05e7 \u05d0\u05dd \u05db\u05dc \u05d4\u05d7\u05d1\u05e8\u05d9\u05dd \u05e4\u05d5\u05e2\u05dc\u05d9\u05dd. \u05d0\u05dd \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d0\u05d9\u05e0\u05d5 \u05d6\u05de\u05d9\u05df, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05d0\u05dd \u05d7\u05d1\u05e8 \u05db\u05dc\u05e9\u05d4\u05d5 \u05e4\u05d5\u05e2\u05dc." + }, "binary_sensor_options": { "data": { "all": "\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea", "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" } }, + "cover": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd" + } + }, "cover_options": { "data": { "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" } }, + "fan": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd" + } + }, "fan_options": { "data": { "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" } }, + "light": { + "data": { + "all": "\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea", + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd" + }, + "description": "\u05d0\u05dd \u05d4\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d6\u05de\u05d9\u05e0\u05d4, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05e8\u05e7 \u05d0\u05dd \u05db\u05dc \u05d4\u05d7\u05d1\u05e8\u05d9\u05dd \u05e4\u05d5\u05e2\u05dc\u05d9\u05dd. \u05d0\u05dd \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d0\u05d9\u05e0\u05d5 \u05d6\u05de\u05d9\u05df, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05d0\u05dd \u05d7\u05d1\u05e8 \u05db\u05dc\u05e9\u05d4\u05d5 \u05e4\u05d5\u05e2\u05dc." + }, "light_options": { "data": { + "all": "\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea", "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" } }, + "lock": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd" + } + }, + "media_player": { + "data": { + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd" + } + }, "media_player_options": { "data": { "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" } + }, + "switch": { + "data": { + "all": "\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea", + "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", + "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd" + }, + "description": "\u05d0\u05dd \u05d4\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d6\u05de\u05d9\u05e0\u05d4, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05e8\u05e7 \u05d0\u05dd \u05db\u05dc \u05d4\u05d7\u05d1\u05e8\u05d9\u05dd \u05e4\u05d5\u05e2\u05dc\u05d9\u05dd. \u05d0\u05dd \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d0\u05d9\u05e0\u05d5 \u05d6\u05de\u05d9\u05df, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05d0\u05dd \u05d7\u05d1\u05e8 \u05db\u05dc\u05e9\u05d4\u05d5 \u05e4\u05d5\u05e2\u05dc." } } }, "state": { "_": { - "closed": "\u05e1\u05d2\u05d5\u05e8", + "closed": "\u05e0\u05e1\u05d2\u05e8", "home": "\u05d1\u05d1\u05d9\u05ea", "locked": "\u05e0\u05e2\u05d5\u05dc", "not_home": "\u05d1\u05d7\u05d5\u05e5", diff --git a/homeassistant/components/group/translations/hu.json b/homeassistant/components/group/translations/hu.json index 08c7d5e5afb..ba368532cff 100644 --- a/homeassistant/components/group/translations/hu.json +++ b/homeassistant/components/group/translations/hu.json @@ -5,18 +5,21 @@ "data": { "all": "Minden entit\u00e1s", "entities": "A csoport tagjai", - "name": "N\u00e9v" + "hide_members": "Tagok elrejt\u00e9se", + "name": "Elnevez\u00e9s" }, "description": "Ha \u201eMinden entit\u00e1s\u201d enged\u00e9lyezve van, a csoport \u00e1llapota csak akkor van bekapcsolva, ha minden tag \u00e1llapota bekapcsolt. Ha \u201eMinden entit\u00e1s\u201d le van tiltva, a csoport \u00e1llapota akkor van bekapcsolva, ha b\u00e1rmelyik tag bekapcsolt \u00e1llapotban van.", - "title": "\u00daj csoport" + "title": "Csoport hozz\u00e1ad\u00e1sa" }, "cover": { "data": { "entities": "A csoport tagjai", - "name": "Csoport neve", - "title": "\u00daj csoport" + "hide_members": "Tagok elrejt\u00e9se", + "name": "Elnevez\u00e9s", + "title": "Csoport hozz\u00e1ad\u00e1sa" }, - "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + "description": "Csoport be\u00e1ll\u00edt\u00e1sai", + "title": "Csoport hozz\u00e1ad\u00e1sa" }, "cover_options": { "data": { @@ -27,10 +30,12 @@ "fan": { "data": { "entities": "A csoport tagjai", - "name": "A csoport elnevez\u00e9se", - "title": "\u00daj csoport" + "hide_members": "Tagok elrejt\u00e9se", + "name": "Elnevez\u00e9s", + "title": "Csoport hozz\u00e1ad\u00e1sa" }, - "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + "description": "Csoport be\u00e1ll\u00edt\u00e1sai", + "title": "Csoport hozz\u00e1ad\u00e1sa" }, "fan_options": { "data": { @@ -46,11 +51,14 @@ }, "light": { "data": { + "all": "Minden entit\u00e1s", "entities": "A csoport tagjai", - "name": "A csoport elnevez\u00e9se", - "title": "\u00daj csoport" + "hide_members": "Tagok elrejt\u00e9se", + "name": "Elnevez\u00e9s", + "title": "Csoport hozz\u00e1ad\u00e1sa" }, - "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + "description": "Ha \u201eMinden entit\u00e1s\u201d enged\u00e9lyezve van, a csoport \u00e1llapota csak akkor van bekapcsolva, ha minden tag \u00e1llapota bekapcsolt. Ha \u201eMinden entit\u00e1s\u201d le van tiltva, a csoport \u00e1llapota akkor van bekapcsolva, ha b\u00e1rmelyik tag bekapcsolt \u00e1llapotban van.", + "title": "Csoport hozz\u00e1ad\u00e1sa" }, "light_options": { "data": { @@ -58,13 +66,23 @@ }, "description": "Csoport be\u00e1ll\u00edt\u00e1sai" }, + "lock": { + "data": { + "entities": "A csoport tagjai", + "hide_members": "Tagok elrejt\u00e9se", + "name": "Elnevez\u00e9s" + }, + "title": "Csoport hozz\u00e1ad\u00e1sa" + }, "media_player": { "data": { "entities": "A csoport tagjai", - "name": "A csoport elnevez\u00e9se", - "title": "\u00daj csoport" + "hide_members": "Tagok elrejt\u00e9se", + "name": "Elnevez\u00e9s", + "title": "Csoport hozz\u00e1ad\u00e1sa" }, - "description": "Csoport be\u00e1ll\u00edt\u00e1sai" + "description": "Csoport be\u00e1ll\u00edt\u00e1sai", + "title": "Csoport hozz\u00e1ad\u00e1sa" }, "media_player_options": { "data": { @@ -72,41 +90,108 @@ }, "description": "Csoport be\u00e1ll\u00edt\u00e1sai" }, + "switch": { + "data": { + "entities": "A csoport tagjai", + "hide_members": "Tagok elrejt\u00e9se", + "name": "Elnevez\u00e9s" + }, + "title": "Csoport hozz\u00e1ad\u00e1sa" + }, "user": { "data": { "group_type": "Csoport t\u00edpusa" }, - "title": "\u00daj csoport" + "description": "A csoportok lehet\u0151v\u00e9 teszik egy \u00faj entit\u00e1s l\u00e9trehoz\u00e1s\u00e1t, amely t\u00f6bb azonos t\u00edpus\u00fa entit\u00e1st k\u00e9pvisel.", + "menu_options": { + "binary_sensor": "Bin\u00e1ris \u00e9rz\u00e9kel\u0151 csoport", + "cover": "Red\u0151ny csoport", + "fan": "Ventil\u00e1tor csoport", + "light": "L\u00e1mpa csoport", + "lock": "Csoport z\u00e1rol\u00e1sa", + "media_player": "M\u00e9dialej\u00e1tsz\u00f3 csoport", + "switch": "Kapcsol\u00f3csoport" + }, + "title": "Csoport hozz\u00e1ad\u00e1sa" } } }, "options": { "step": { + "binary_sensor": { + "data": { + "all": "Minden entit\u00e1s", + "entities": "A csoport tagjai", + "hide_members": "Tagok elrejt\u00e9se" + }, + "description": "Ha \u201eMinden entit\u00e1s\u201d enged\u00e9lyezve van, a csoport \u00e1llapota csak akkor van bekapcsolva, ha minden tag \u00e1llapota bekapcsolt. Ha \u201eMinden entit\u00e1s\u201d le van tiltva, a csoport \u00e1llapota akkor van bekapcsolva, ha b\u00e1rmelyik tag bekapcsolt \u00e1llapotban van." + }, "binary_sensor_options": { "data": { "all": "Minden entit\u00e1s", "entities": "A csoport tagjai" } }, + "cover": { + "data": { + "entities": "A csoport tagjai", + "hide_members": "Tagok elrejt\u00e9se" + } + }, "cover_options": { "data": { "entities": "A csoport tagjai" } }, + "fan": { + "data": { + "entities": "A csoport tagjai", + "hide_members": "Tagok elrejt\u00e9se" + } + }, "fan_options": { "data": { "entities": "A csoport tagjai" } }, + "light": { + "data": { + "all": "Minden entit\u00e1s", + "entities": "A csoport tagjai", + "hide_members": "Tagok elrejt\u00e9se" + }, + "description": "Ha \u201eMinden entit\u00e1s\u201d enged\u00e9lyezve van, a csoport \u00e1llapota csak akkor van bekapcsolva, ha minden tag \u00e1llapota bekapcsolt. Ha \u201eMinden entit\u00e1s\u201d le van tiltva, a csoport \u00e1llapota akkor van bekapcsolva, ha b\u00e1rmelyik tag bekapcsolt \u00e1llapotban van." + }, "light_options": { "data": { + "all": "Minden entit\u00e1s", "entities": "A csoport tagjai" } }, + "lock": { + "data": { + "entities": "A csoport tagjai", + "hide_members": "Tagok elrejt\u00e9se" + } + }, + "media_player": { + "data": { + "entities": "A csoport tagjai", + "hide_members": "Tagok elrejt\u00e9se" + } + }, "media_player_options": { "data": { "entities": "A csoport tagjai" } + }, + "switch": { + "data": { + "all": "Minden entit\u00e1s", + "entities": "A csoport tagjai", + "hide_members": "Tagok elrejt\u00e9se" + }, + "description": "Ha \u201eMinden entit\u00e1s\u201d enged\u00e9lyezve van, a csoport \u00e1llapota csak akkor van bekapcsolva, ha minden tag \u00e1llapota bekapcsolt. Ha \u201eMinden entit\u00e1s\u201d le van tiltva, a csoport \u00e1llapota akkor van bekapcsolva, ha b\u00e1rmelyik tag bekapcsolt \u00e1llapotban van." } } }, diff --git a/homeassistant/components/group/translations/id.json b/homeassistant/components/group/translations/id.json index 2a92526d644..c0425c5ede6 100644 --- a/homeassistant/components/group/translations/id.json +++ b/homeassistant/components/group/translations/id.json @@ -5,18 +5,21 @@ "data": { "all": "Semua entitas", "entities": "Anggota", + "hide_members": "Sembunyikan anggota", "name": "Nama" }, "description": "Jika \"semua entitas\" diaktifkan, status grup akan menyala jika semua anggota nyala. Jika \"semua entitas\" dinonaktifkan, status grup akan menyala jika ada salah satu atau lebih anggota yang menyala.", - "title": "Grup Baru" + "title": "Tambahkan Grup" }, "cover": { "data": { "entities": "Anggota", + "hide_members": "Sembunyikan anggota", "name": "Nama", - "title": "Grup Baru" + "title": "Tambahkan Grup" }, - "description": "Pilih opsi grup" + "description": "Pilih opsi grup", + "title": "Tambahkan Grup" }, "cover_options": { "data": { @@ -27,10 +30,12 @@ "fan": { "data": { "entities": "Anggota", + "hide_members": "Sembunyikan anggota", "name": "Nama", - "title": "Grup Baru" + "title": "Tambahkan Grup" }, - "description": "Pilih opsi grup" + "description": "Pilih opsi grup", + "title": "Tambahkan Grup" }, "fan_options": { "data": { @@ -46,11 +51,14 @@ }, "light": { "data": { + "all": "Semua entitas", "entities": "Anggota", + "hide_members": "Sembunyikan anggota", "name": "Nama", - "title": "Grup Baru" + "title": "Tambahkan Grup" }, - "description": "Pilih opsi grup" + "description": "Jika \"semua entitas\" diaktifkan, status grup akan menyala jika semua anggota nyala. Jika \"semua entitas\" dinonaktifkan, status grup akan menyala jika ada salah satu atau lebih anggota yang menyala.", + "title": "Tambahkan Grup" }, "light_options": { "data": { @@ -58,13 +66,23 @@ }, "description": "Pilih opsi grup" }, + "lock": { + "data": { + "entities": "Anggota", + "hide_members": "Sembunyikan anggota", + "name": "Nama" + }, + "title": "Tambahkan Grup" + }, "media_player": { "data": { "entities": "Anggota", + "hide_members": "Sembunyikan anggota", "name": "Nama", - "title": "Grup Baru" + "title": "Tambahkan Grup" }, - "description": "Pilih opsi grup" + "description": "Pilih opsi grup", + "title": "Tambahkan Grup" }, "media_player_options": { "data": { @@ -72,41 +90,108 @@ }, "description": "Pilih opsi grup" }, + "switch": { + "data": { + "entities": "Anggota", + "hide_members": "Sembunyikan anggota", + "name": "Nama" + }, + "title": "Tambahkan Grup" + }, "user": { "data": { "group_type": "Jenis grup" }, - "title": "Grup Baru" + "description": "Grup memungkinkan Anda membuat entitas baru yang mewakili beberapa entitas dari jenis yang sama.", + "menu_options": { + "binary_sensor": "Grup sensor biner", + "cover": "Grup penutup", + "fan": "Grup kipas", + "light": "Grup lampu", + "lock": "Grup kunci", + "media_player": "Grup pemutar media", + "switch": "Ganti sakelar" + }, + "title": "Tambahkan Grup" } } }, "options": { "step": { + "binary_sensor": { + "data": { + "all": "Semua entitas", + "entities": "Anggota", + "hide_members": "Sembunyikan anggota" + }, + "description": "Jika \"semua entitas\" diaktifkan, status grup akan menyala jika semua anggota nyala. Jika \"semua entitas\" dinonaktifkan, status grup akan menyala jika ada salah satu atau lebih anggota yang menyala." + }, "binary_sensor_options": { "data": { "all": "Semua entitas", "entities": "Anggota" } }, + "cover": { + "data": { + "entities": "Anggota", + "hide_members": "Sembunyikan anggota" + } + }, "cover_options": { "data": { "entities": "Anggota" } }, + "fan": { + "data": { + "entities": "Anggota", + "hide_members": "Sembunyikan anggota" + } + }, "fan_options": { "data": { "entities": "Anggota" } }, + "light": { + "data": { + "all": "Semua entitas", + "entities": "Anggota", + "hide_members": "Sembunyikan anggota" + }, + "description": "Jika \"semua entitas\" diaktifkan, status grup akan menyala jika semua anggota nyala. Jika \"semua entitas\" dinonaktifkan, status grup akan menyala jika ada salah satu atau lebih anggota yang menyala." + }, "light_options": { "data": { + "all": "Semua entitas", "entities": "Anggota" } }, + "lock": { + "data": { + "entities": "Anggota", + "hide_members": "Sembunyikan anggota" + } + }, + "media_player": { + "data": { + "entities": "Anggota", + "hide_members": "Sembunyikan anggota" + } + }, "media_player_options": { "data": { "entities": "Anggota" } + }, + "switch": { + "data": { + "all": "Semua entitas", + "entities": "Anggota", + "hide_members": "Sembunyikan anggota" + }, + "description": "Jika \"semua entitas\" diaktifkan, status grup akan menyala jika semua anggota nyala. Jika \"semua entitas\" dinonaktifkan, status grup akan menyala jika ada salah satu atau lebih anggota yang menyala." } } }, diff --git a/homeassistant/components/group/translations/it.json b/homeassistant/components/group/translations/it.json index 6dc9ff2cce8..4cbac9145d8 100644 --- a/homeassistant/components/group/translations/it.json +++ b/homeassistant/components/group/translations/it.json @@ -5,18 +5,21 @@ "data": { "all": "Tutte le entit\u00e0", "entities": "Membri", + "hide_members": "Nascondi membri", "name": "Nome" }, "description": "Se \"tutte le entit\u00e0\" \u00e8 abilitata, lo stato del gruppo \u00e8 attivo solo se tutti i membri sono attivi. Se \"tutte le entit\u00e0\" \u00e8 disabilitata, lo stato del gruppo \u00e8 attivo se un membro \u00e8 attivo.", - "title": "Nuovo gruppo" + "title": "Aggiungi gruppo" }, "cover": { "data": { "entities": "Membri", + "hide_members": "Nascondi membri", "name": "Nome", - "title": "Nuovo gruppo" + "title": "Aggiungi gruppo" }, - "description": "Seleziona le opzioni di gruppo" + "description": "Seleziona le opzioni di gruppo", + "title": "Aggiungi gruppo" }, "cover_options": { "data": { @@ -27,10 +30,12 @@ "fan": { "data": { "entities": "Membri", + "hide_members": "Nascondi membri", "name": "Nome", - "title": "Nuovo gruppo" + "title": "Aggiungi gruppo" }, - "description": "Seleziona le opzioni di gruppo" + "description": "Seleziona le opzioni di gruppo", + "title": "Aggiungi gruppo" }, "fan_options": { "data": { @@ -46,11 +51,14 @@ }, "light": { "data": { + "all": "Tutte le entit\u00e0", "entities": "Membri", + "hide_members": "Nascondi membri", "name": "Nome", - "title": "Nuovo gruppo" + "title": "Aggiungi gruppo" }, - "description": "Seleziona le opzioni di gruppo" + "description": "Se \"tutte le entit\u00e0\" \u00e8 abilitata, lo stato del gruppo \u00e8 attivo solo se tutti i membri sono attivi. Se \"tutte le entit\u00e0\" \u00e8 disabilitata, lo stato del gruppo \u00e8 attivo se un membro \u00e8 attivo.", + "title": "Aggiungi gruppo" }, "light_options": { "data": { @@ -58,13 +66,23 @@ }, "description": "Seleziona le opzioni di gruppo" }, + "lock": { + "data": { + "entities": "Membri", + "hide_members": "Nascondi membri", + "name": "Nome" + }, + "title": "Aggiungi gruppo" + }, "media_player": { "data": { "entities": "Membri", + "hide_members": "Nascondi membri", "name": "Nome", - "title": "Nuovo gruppo" + "title": "Aggiungi gruppo" }, - "description": "Seleziona le opzioni di gruppo" + "description": "Seleziona le opzioni di gruppo", + "title": "Aggiungi gruppo" }, "media_player_options": { "data": { @@ -72,41 +90,108 @@ }, "description": "Seleziona le opzioni di gruppo" }, + "switch": { + "data": { + "entities": "Membri", + "hide_members": "Nascondi membri", + "name": "Nome" + }, + "title": "Aggiungi gruppo" + }, "user": { "data": { "group_type": "Tipo di gruppo" }, - "title": "Nuovo gruppo" + "description": "I gruppi consentono di creare una nuova entit\u00e0 che rappresenta pi\u00f9 entit\u00e0 dello stesso tipo.", + "menu_options": { + "binary_sensor": "Gruppo di sensori binari", + "cover": "Gruppo di coperture", + "fan": "Gruppo di ventole", + "light": "Gruppo di luci", + "lock": "Blocca gruppo", + "media_player": "Gruppo di lettori multimediali", + "switch": "Gruppo di interruttori" + }, + "title": "Aggiungi gruppo" } } }, "options": { "step": { + "binary_sensor": { + "data": { + "all": "Tutte le entit\u00e0", + "entities": "Membri", + "hide_members": "Nascondi membri" + }, + "description": "Se \"tutte le entit\u00e0\" \u00e8 abilitata, lo stato del gruppo \u00e8 attivo solo se tutti i membri sono attivi. Se \"tutte le entit\u00e0\" \u00e8 disabilitata, lo stato del gruppo \u00e8 attivo se un membro \u00e8 attivo." + }, "binary_sensor_options": { "data": { "all": "Tutte le entit\u00e0", "entities": "Membri" } }, + "cover": { + "data": { + "entities": "Membri", + "hide_members": "Nascondi membri" + } + }, "cover_options": { "data": { "entities": "Membri" } }, + "fan": { + "data": { + "entities": "Membri", + "hide_members": "Nascondi membri" + } + }, "fan_options": { "data": { "entities": "Membri" } }, + "light": { + "data": { + "all": "Tutte le entit\u00e0", + "entities": "Membri", + "hide_members": "Nascondi membri" + }, + "description": "Se \"tutte le entit\u00e0\" \u00e8 abilitata, lo stato del gruppo \u00e8 attivo solo se tutti i membri sono attivi. Se \"tutte le entit\u00e0\" \u00e8 disabilitata, lo stato del gruppo \u00e8 attivo se un membro \u00e8 attivo." + }, "light_options": { "data": { + "all": "Tutte le entit\u00e0", "entities": "Membri" } }, + "lock": { + "data": { + "entities": "Membri", + "hide_members": "Nascondi membri" + } + }, + "media_player": { + "data": { + "entities": "Membri", + "hide_members": "Nascondi membri" + } + }, "media_player_options": { "data": { "entities": "Membri" } + }, + "switch": { + "data": { + "all": "Tutte le entit\u00e0", + "entities": "Membri", + "hide_members": "Nascondi membri" + }, + "description": "Se \"tutte le entit\u00e0\" \u00e8 abilitata, lo stato del gruppo \u00e8 attivo solo se tutti i membri sono attivi. Se \"tutte le entit\u00e0\" \u00e8 disabilitata, lo stato del gruppo \u00e8 attivo se un membro \u00e8 attivo." } } }, diff --git a/homeassistant/components/group/translations/ja.json b/homeassistant/components/group/translations/ja.json index dac7583169d..55e414927a4 100644 --- a/homeassistant/components/group/translations/ja.json +++ b/homeassistant/components/group/translations/ja.json @@ -5,6 +5,7 @@ "data": { "all": "\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", "entities": "\u30e1\u30f3\u30d0\u30fc", + "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b", "name": "\u540d\u524d" }, "description": "\"\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\" \u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408\u3001\u30b0\u30eb\u30fc\u30d7\u306e\u72b6\u614b\u306f\u3001\u3059\u3079\u3066\u306e\u30e1\u30f3\u30d0\u30fc\u304c\u30aa\u30f3\u306e\u5834\u5408\u306b\u306e\u307f\u30aa\u30f3\u306b\u306a\u308a\u307e\u3059\u3002 \"\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\" \u304c\u7121\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408\u3001\u3044\u305a\u308c\u304b\u306e\u30e1\u30f3\u30d0\u30fc\u304c\u30aa\u30f3\u3067\u3042\u308c\u3070\u3001\u30b0\u30eb\u30fc\u30d7\u306e\u72b6\u614b\u306f\u30aa\u30f3\u306b\u306a\u308a\u307e\u3059\u3002", @@ -13,10 +14,12 @@ "cover": { "data": { "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", + "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b", "name": "\u30b0\u30eb\u30fc\u30d7\u540d", "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, - "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e", + "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, "cover_options": { "data": { @@ -27,10 +30,12 @@ "fan": { "data": { "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", + "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b", "name": "\u30b0\u30eb\u30fc\u30d7\u540d", "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, - "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e", + "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, "fan_options": { "data": { @@ -46,11 +51,14 @@ }, "light": { "data": { + "all": "\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", + "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b", "name": "\u30b0\u30eb\u30fc\u30d7\u540d", "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, - "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e", + "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, "light_options": { "data": { @@ -58,13 +66,23 @@ }, "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" }, + "lock": { + "data": { + "entities": "\u30e1\u30f3\u30d0\u30fc", + "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b", + "name": "\u540d\u524d" + }, + "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" + }, "media_player": { "data": { "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", + "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b", "name": "\u30b0\u30eb\u30fc\u30d7\u540d", "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, - "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e", + "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, "media_player_options": { "data": { @@ -72,41 +90,108 @@ }, "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" }, + "switch": { + "data": { + "entities": "\u30e1\u30f3\u30d0\u30fc", + "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b", + "name": "\u540d\u524d" + }, + "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" + }, "user": { "data": { "group_type": "\u30b0\u30eb\u30fc\u30d7\u30bf\u30a4\u30d7" }, + "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u7a2e\u985e\u3092\u9078\u629e", + "menu_options": { + "binary_sensor": "\u30d0\u30a4\u30ca\u30ea\u30fc\u30bb\u30f3\u30b5\u30fc\u30b0\u30eb\u30fc\u30d7", + "cover": "\u30ab\u30d0\u30fc\u30b0\u30eb\u30fc\u30d7", + "fan": "\u30d5\u30a1\u30f3\u30b0\u30eb\u30fc\u30d7", + "light": "\u30e9\u30a4\u30c8(Light)\u30b0\u30eb\u30fc\u30d7", + "lock": "\u30ed\u30c3\u30af\u30b0\u30eb\u30fc\u30d7", + "media_player": "\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc\u30b0\u30eb\u30fc\u30d7", + "switch": "\u30b9\u30a4\u30c3\u30c1\u30b0\u30eb\u30fc\u30d7" + }, "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" } } }, "options": { "step": { + "binary_sensor": { + "data": { + "all": "\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", + "entities": "\u30e1\u30f3\u30d0\u30fc", + "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b" + }, + "description": "\"\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\" \u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408\u3001\u30b0\u30eb\u30fc\u30d7\u306e\u72b6\u614b\u306f\u3001\u3059\u3079\u3066\u306e\u30e1\u30f3\u30d0\u30fc\u304c\u30aa\u30f3\u306e\u5834\u5408\u306b\u306e\u307f\u30aa\u30f3\u306b\u306a\u308a\u307e\u3059\u3002 \"\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\" \u304c\u7121\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408\u3001\u3044\u305a\u308c\u304b\u306e\u30e1\u30f3\u30d0\u30fc\u304c\u30aa\u30f3\u3067\u3042\u308c\u3070\u3001\u30b0\u30eb\u30fc\u30d7\u306e\u72b6\u614b\u306f\u30aa\u30f3\u306b\u306a\u308a\u307e\u3059\u3002" + }, "binary_sensor_options": { "data": { "all": "\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", "entities": "\u30e1\u30f3\u30d0\u30fc" } }, + "cover": { + "data": { + "entities": "\u30e1\u30f3\u30d0\u30fc", + "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b" + } + }, "cover_options": { "data": { "entities": "\u30e1\u30f3\u30d0\u30fc" } }, + "fan": { + "data": { + "entities": "\u30e1\u30f3\u30d0\u30fc", + "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b" + } + }, "fan_options": { "data": { "entities": "\u30e1\u30f3\u30d0\u30fc" } }, + "light": { + "data": { + "all": "\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", + "entities": "\u30e1\u30f3\u30d0\u30fc", + "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b" + }, + "description": "\"\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\" \u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408\u3001\u30b0\u30eb\u30fc\u30d7\u306e\u72b6\u614b\u306f\u3001\u3059\u3079\u3066\u306e\u30e1\u30f3\u30d0\u30fc\u304c\u30aa\u30f3\u306e\u5834\u5408\u306b\u306e\u307f\u30aa\u30f3\u306b\u306a\u308a\u307e\u3059\u3002 \"\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\" \u304c\u7121\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408\u3001\u3044\u305a\u308c\u304b\u306e\u30e1\u30f3\u30d0\u30fc\u304c\u30aa\u30f3\u3067\u3042\u308c\u3070\u3001\u30b0\u30eb\u30fc\u30d7\u306e\u72b6\u614b\u306f\u30aa\u30f3\u306b\u306a\u308a\u307e\u3059\u3002" + }, "light_options": { "data": { + "all": "\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", "entities": "\u30e1\u30f3\u30d0\u30fc" } }, + "lock": { + "data": { + "entities": "\u30e1\u30f3\u30d0\u30fc", + "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b" + } + }, + "media_player": { + "data": { + "entities": "\u30e1\u30f3\u30d0\u30fc", + "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b" + } + }, "media_player_options": { "data": { "entities": "\u30e1\u30f3\u30d0\u30fc" } + }, + "switch": { + "data": { + "all": "\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", + "entities": "\u30e1\u30f3\u30d0\u30fc", + "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b" + }, + "description": "\"\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\" \u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408\u3001\u30b0\u30eb\u30fc\u30d7\u306e\u72b6\u614b\u306f\u3001\u3059\u3079\u3066\u306e\u30e1\u30f3\u30d0\u30fc\u304c\u30aa\u30f3\u306e\u5834\u5408\u306b\u306e\u307f\u30aa\u30f3\u306b\u306a\u308a\u307e\u3059\u3002 \"\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\" \u304c\u7121\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408\u3001\u3044\u305a\u308c\u304b\u306e\u30e1\u30f3\u30d0\u30fc\u304c\u30aa\u30f3\u3067\u3042\u308c\u3070\u3001\u30b0\u30eb\u30fc\u30d7\u306e\u72b6\u614b\u306f\u30aa\u30f3\u306b\u306a\u308a\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/group/translations/nl.json b/homeassistant/components/group/translations/nl.json index c636c56f744..e670e2e853a 100644 --- a/homeassistant/components/group/translations/nl.json +++ b/homeassistant/components/group/translations/nl.json @@ -5,18 +5,21 @@ "data": { "all": "Alle entiteiten", "entities": "Leden", + "hide_members": "Verberg leden", "name": "Naam" }, "description": "Als \"alle entiteiten\" is ingeschakeld, is de groep alleen ingeschakeld als alle leden zijn ingeschakeld. Als \"all entities\" is uitgeschakeld, is de groep ingeschakeld als een lid is ingeschakeld.", - "title": "Nieuwe groep" + "title": "Groep toevoegen" }, "cover": { "data": { "entities": "Leden", + "hide_members": "Verberg leden", "name": "Naam", - "title": "Nieuwe groep" + "title": "Groep toevoegen" }, - "description": "Selecteer groepsopties" + "description": "Selecteer groepsopties", + "title": "Groep toevoegen" }, "cover_options": { "data": { @@ -27,10 +30,12 @@ "fan": { "data": { "entities": "Leden", + "hide_members": "Verberg leden", "name": "Naam", - "title": "Nieuwe groep" + "title": "Groep toevoegen" }, - "description": "Selecteer groepsopties" + "description": "Selecteer groepsopties", + "title": "Groep toevoegen" }, "fan_options": { "data": { @@ -46,11 +51,14 @@ }, "light": { "data": { + "all": "Alle entiteiten", "entities": "Leden", + "hide_members": "Verberg leden", "name": "Naam", - "title": "Nieuwe groep" + "title": "Groep toevoegen" }, - "description": "Selecteer groepsopties" + "description": "Als \"alle entiteiten\" is ingeschakeld, is de groep alleen ingeschakeld als alle leden zijn ingeschakeld. Als \"all entities\" is uitgeschakeld, is de groep ingeschakeld als een lid is ingeschakeld.", + "title": "Groep toevoegen" }, "light_options": { "data": { @@ -58,13 +66,23 @@ }, "description": "Selecteer groepsopties" }, + "lock": { + "data": { + "entities": "Leden", + "hide_members": "Verberg leden", + "name": "Naam" + }, + "title": "Groep toevoegen" + }, "media_player": { "data": { "entities": "Leden", + "hide_members": "Verberg leden", "name": "Naam", - "title": "Nieuwe groep" + "title": "Groep toevoegen" }, - "description": "Selecteer groepsopties" + "description": "Selecteer groepsopties", + "title": "Groep toevoegen" }, "media_player_options": { "data": { @@ -72,41 +90,108 @@ }, "description": "Selecteer groepsopties" }, + "switch": { + "data": { + "entities": "Leden", + "hide_members": "Verberg leden", + "name": "Naam" + }, + "title": "Nieuwe groep" + }, "user": { "data": { "group_type": "Groepstype" }, - "title": "Nieuwe groep" + "description": "Met groepen kunt u een nieuwe entiteit cre\u00ebren die meerdere entiteiten van hetzelfde type vertegenwoordigt.", + "menu_options": { + "binary_sensor": "Binaire sensorgroep", + "cover": "Rolluikgroep", + "fan": "Ventilatorgroep", + "light": "Lichtgroep", + "lock": "Groep vergrendelen", + "media_player": "Mediaspelergroep", + "switch": "Groep wisselen" + }, + "title": "Groep toevoegen" } } }, "options": { "step": { + "binary_sensor": { + "data": { + "all": "Alle entiteiten", + "entities": "Leden", + "hide_members": "Verberg leden" + }, + "description": "Als \"alle entiteiten\" is ingeschakeld, is de groep alleen ingeschakeld als alle leden zijn ingeschakeld. Als \"all entities\" is uitgeschakeld, is de groep ingeschakeld als een lid is ingeschakeld." + }, "binary_sensor_options": { "data": { "all": "Alle entiteiten", "entities": "Leden" } }, + "cover": { + "data": { + "entities": "Leden", + "hide_members": "Verberg leden" + } + }, "cover_options": { "data": { "entities": "Leden" } }, + "fan": { + "data": { + "entities": "Leden", + "hide_members": "Verberg leden" + } + }, "fan_options": { "data": { "entities": "Leden" } }, + "light": { + "data": { + "all": "Alle entiteiten", + "entities": "Leden", + "hide_members": "Verberg leden" + }, + "description": "Als \"alle entiteiten\" is ingeschakeld, is de groep alleen ingeschakeld als alle leden zijn ingeschakeld. Als \"all entities\" is uitgeschakeld, is de groep ingeschakeld als een lid is ingeschakeld." + }, "light_options": { "data": { + "all": "Alle entiteiten", "entities": "Leden" } }, + "lock": { + "data": { + "entities": "Leden", + "hide_members": "Verberg leden" + } + }, + "media_player": { + "data": { + "entities": "Leden", + "hide_members": "Verberg leden" + } + }, "media_player_options": { "data": { "entities": "Leden" } + }, + "switch": { + "data": { + "all": "Alle entiteiten", + "entities": "Leden", + "hide_members": "Verberg leden" + }, + "description": "Als \"alle entitieiten\" is ingeschakeld, is de status van de groep alleen ingeschakeld als alle leden zijn ingeschakeld. Als \" alle entititeiten\" is uitgeschakeld, is de status van de groep ingeschakeld als een lid is ingeschakeld." } } }, diff --git a/homeassistant/components/group/translations/no.json b/homeassistant/components/group/translations/no.json index 0046479d686..016a9c8e934 100644 --- a/homeassistant/components/group/translations/no.json +++ b/homeassistant/components/group/translations/no.json @@ -5,18 +5,21 @@ "data": { "all": "Alle enheter", "entities": "Medlemmer", + "hide_members": "Skjul medlemmer", "name": "Navn" }, "description": "Hvis \"alle enheter\" er aktivert, er gruppens tilstand p\u00e5 bare hvis alle medlemmene er p\u00e5. Hvis \"alle enheter\" er deaktivert, er gruppens tilstand p\u00e5 hvis et medlem er p\u00e5.", - "title": "Ny gruppe" + "title": "Legg til gruppe" }, "cover": { "data": { "entities": "Medlemmer", + "hide_members": "Skjul medlemmer", "name": "Navn", - "title": "Ny gruppe" + "title": "Legg til gruppe" }, - "description": "Velg gruppealternativer" + "description": "Velg gruppealternativer", + "title": "Legg til gruppe" }, "cover_options": { "data": { @@ -27,10 +30,12 @@ "fan": { "data": { "entities": "Medlemmer", + "hide_members": "Skjul medlemmer", "name": "Navn", - "title": "Ny gruppe" + "title": "Legg til gruppe" }, - "description": "Velg gruppealternativer" + "description": "Velg gruppealternativer", + "title": "Legg til gruppe" }, "fan_options": { "data": { @@ -46,11 +51,14 @@ }, "light": { "data": { + "all": "Alle enheter", "entities": "Medlemmer", + "hide_members": "Skjul medlemmer", "name": "Navn", - "title": "Ny gruppe" + "title": "Legg til gruppe" }, - "description": "Velg gruppealternativer" + "description": "Hvis \"alle enheter\" er aktivert, er gruppens tilstand p\u00e5 bare hvis alle medlemmene er p\u00e5. Hvis \"alle enheter\" er deaktivert, er gruppens tilstand p\u00e5 hvis et medlem er p\u00e5.", + "title": "Legg til gruppe" }, "light_options": { "data": { @@ -58,13 +66,23 @@ }, "description": "Velg gruppealternativer" }, + "lock": { + "data": { + "entities": "Medlemmer", + "hide_members": "Skjul medlemmer", + "name": "Navn" + }, + "title": "Legg til gruppe" + }, "media_player": { "data": { "entities": "Medlemmer", + "hide_members": "Skjul medlemmer", "name": "Navn", - "title": "Ny gruppe" + "title": "Legg til gruppe" }, - "description": "Velg gruppealternativer" + "description": "Velg gruppealternativer", + "title": "Legg til gruppe" }, "media_player_options": { "data": { @@ -72,41 +90,108 @@ }, "description": "Velg gruppealternativer" }, + "switch": { + "data": { + "entities": "Medlemmer", + "hide_members": "Skjul medlemmer", + "name": "Navn" + }, + "title": "Legg til gruppe" + }, "user": { "data": { "group_type": "Gruppetype" }, - "title": "Ny gruppe" + "description": "Grupper lar deg opprette en ny enhet som representerer flere enheter av samme type.", + "menu_options": { + "binary_sensor": "Bin\u00e6r sensorgruppe", + "cover": "Dekkgruppe", + "fan": "Viftegruppe", + "light": "Lys-gruppen", + "lock": "L\u00e5s gruppe", + "media_player": "Mediespillergruppe", + "switch": "Bytt gruppe" + }, + "title": "Legg til gruppe" } } }, "options": { "step": { + "binary_sensor": { + "data": { + "all": "Alle enheter", + "entities": "Medlemmer", + "hide_members": "Skjul medlemmer" + }, + "description": "Hvis \"alle enheter\" er aktivert, er gruppens tilstand p\u00e5 bare hvis alle medlemmene er p\u00e5. Hvis \"alle enheter\" er deaktivert, er gruppens tilstand p\u00e5 hvis et medlem er p\u00e5." + }, "binary_sensor_options": { "data": { "all": "Alle enheter", "entities": "Medlemmer" } }, + "cover": { + "data": { + "entities": "Medlemmer", + "hide_members": "Skjul medlemmer" + } + }, "cover_options": { "data": { "entities": "Medlemmer" } }, + "fan": { + "data": { + "entities": "Medlemmer", + "hide_members": "Skjul medlemmer" + } + }, "fan_options": { "data": { "entities": "Medlemmer" } }, + "light": { + "data": { + "all": "Alle enheter", + "entities": "Medlemmer", + "hide_members": "Skjul medlemmer" + }, + "description": "Hvis \"alle enheter\" er aktivert, er gruppens tilstand p\u00e5 bare hvis alle medlemmene er p\u00e5. Hvis \"alle enheter\" er deaktivert, er gruppens tilstand p\u00e5 hvis et medlem er p\u00e5." + }, "light_options": { "data": { + "all": "Alle enheter", "entities": "Medlemmer" } }, + "lock": { + "data": { + "entities": "Medlemmer", + "hide_members": "Skjul medlemmer" + } + }, + "media_player": { + "data": { + "entities": "Medlemmer", + "hide_members": "Skjul medlemmer" + } + }, "media_player_options": { "data": { "entities": "Medlemmer" } + }, + "switch": { + "data": { + "all": "Alle enheter", + "entities": "Medlemmer", + "hide_members": "Skjul medlemmer" + }, + "description": "Hvis \"alle enheter\" er aktivert, er gruppens tilstand p\u00e5 bare hvis alle medlemmene er p\u00e5. Hvis \"alle enheter\" er deaktivert, er gruppens tilstand p\u00e5 hvis et medlem er p\u00e5." } } }, diff --git a/homeassistant/components/group/translations/pl.json b/homeassistant/components/group/translations/pl.json index 08595dd8a59..84cbd9f7b4d 100644 --- a/homeassistant/components/group/translations/pl.json +++ b/homeassistant/components/group/translations/pl.json @@ -5,18 +5,21 @@ "data": { "all": "Wszystkie encje", "entities": "Encje", + "hide_members": "Ukryj encje", "name": "Nazwa" }, - "description": "Je\u015bli \u201ewszystkie encje\u201d jest w\u0142\u0105czone, stan grupy jest w\u0142\u0105czony tylko wtedy, gdy wszystkie encje s\u0105 w\u0142\u0105czone. Je\u015bli \u201ewszystkie encje\u201d s\u0105 wy\u0142\u0105czone, stan grupy jest w\u0142\u0105czony, je\u015bli kt\u00f3rakolwiek encja jest w\u0142\u0105czona.", - "title": "Nowa grupa" + "description": "Je\u015bli \u201ewszystkie encje\u201d jest w\u0142\u0105czone, stan grupy jest w\u0142\u0105czony tylko wtedy, gdy wszystkie encje s\u0105 w\u0142\u0105czone. Je\u015bli \u201ewszystkie encje\u201d jest wy\u0142\u0105czone, stan grupy jest w\u0142\u0105czony, je\u015bli kt\u00f3rakolwiek encja jest w\u0142\u0105czona.", + "title": "Dodaj grup\u0119" }, "cover": { "data": { "entities": "Encje", + "hide_members": "Ukryj encje", "name": "Nazwa", - "title": "Nowa grupa" + "title": "Dodaj grup\u0119" }, - "description": "Wybierz opcje grupy" + "description": "Wybierz opcje grupy", + "title": "Dodaj grup\u0119" }, "cover_options": { "data": { @@ -27,10 +30,12 @@ "fan": { "data": { "entities": "Encje", + "hide_members": "Ukryj encje", "name": "Nazwa", - "title": "Nowa grupa" + "title": "Dodaj grup\u0119" }, - "description": "Wybierz opcje grupy" + "description": "Wybierz opcje grupy", + "title": "Dodaj grup\u0119" }, "fan_options": { "data": { @@ -46,11 +51,14 @@ }, "light": { "data": { + "all": "Wszystkie encje", "entities": "Encje", + "hide_members": "Ukryj encje", "name": "Nazwa", - "title": "Nowa grupa" + "title": "Dodaj grup\u0119" }, - "description": "Wybierz opcje grupy" + "description": "Je\u015bli \u201ewszystkie encje\u201d jest w\u0142\u0105czone, stan grupy jest w\u0142\u0105czony tylko wtedy, gdy wszystkie encje s\u0105 w\u0142\u0105czone. Je\u015bli \u201ewszystkie encje\u201d jest wy\u0142\u0105czone, stan grupy jest w\u0142\u0105czony, je\u015bli kt\u00f3rakolwiek encja jest w\u0142\u0105czona.", + "title": "Dodaj grup\u0119" }, "light_options": { "data": { @@ -58,13 +66,23 @@ }, "description": "Wybierz opcje grupy" }, + "lock": { + "data": { + "entities": "Encje", + "hide_members": "Ukryj encje", + "name": "Nazwa" + }, + "title": "Dodaj grup\u0119" + }, "media_player": { "data": { "entities": "Encje", + "hide_members": "Ukryj encje", "name": "Nazwa", - "title": "Nowa grupa" + "title": "Dodaj grup\u0119" }, - "description": "Wybierz opcje grupy" + "description": "Wybierz opcje grupy", + "title": "Dodaj grup\u0119" }, "media_player_options": { "data": { @@ -72,41 +90,108 @@ }, "description": "Wybierz opcje grupy" }, + "switch": { + "data": { + "entities": "Encje", + "hide_members": "Ukryj encje", + "name": "Nazwa" + }, + "title": "Dodaj grup\u0119" + }, "user": { "data": { "group_type": "Rodzaj grupy" }, - "title": "Nowa grupa" + "description": "Grupy umo\u017cliwiaj\u0105 tworzenie nowej encji, kt\u00f3ra reprezentuje wiele encji tego samego typu.", + "menu_options": { + "binary_sensor": "Grupa sensor\u00f3w binarnych", + "cover": "Grupa rolet", + "fan": "Grupa wentylator\u00f3w", + "light": "Grupa \u015bwiate\u0142", + "lock": "Grupa zamk\u00f3w", + "media_player": "Grupa odtwarzaczy multimedialnych", + "switch": "Grupa prze\u0142\u0105cznik\u00f3w" + }, + "title": "Dodaj grup\u0119" } } }, "options": { "step": { + "binary_sensor": { + "data": { + "all": "Wszystkie encje", + "entities": "Encje", + "hide_members": "Ukryj encje" + }, + "description": "Je\u015bli \u201ewszystkie encje\u201d jest w\u0142\u0105czone, stan grupy jest w\u0142\u0105czony tylko wtedy, gdy wszystkie encje s\u0105 w\u0142\u0105czone. Je\u015bli \u201ewszystkie encje\u201d jest wy\u0142\u0105czone, stan grupy jest w\u0142\u0105czony, je\u015bli kt\u00f3rakolwiek encja jest w\u0142\u0105czona." + }, "binary_sensor_options": { "data": { "all": "Wszystkie encje", "entities": "Encje" } }, + "cover": { + "data": { + "entities": "Encje", + "hide_members": "Ukryj encje" + } + }, "cover_options": { "data": { "entities": "Encje" } }, + "fan": { + "data": { + "entities": "Encje", + "hide_members": "Ukryj encje" + } + }, "fan_options": { "data": { "entities": "Encje" } }, + "light": { + "data": { + "all": "Wszystkie encje", + "entities": "Encje", + "hide_members": "Ukryj encje" + }, + "description": "Je\u015bli \u201ewszystkie encje\u201d jest w\u0142\u0105czone, stan grupy jest w\u0142\u0105czony tylko wtedy, gdy wszystkie encje s\u0105 w\u0142\u0105czone. Je\u015bli \u201ewszystkie encje\u201d jest wy\u0142\u0105czone, stan grupy jest w\u0142\u0105czony, je\u015bli kt\u00f3rakolwiek encja jest w\u0142\u0105czona." + }, "light_options": { "data": { + "all": "Wszystkie encje", "entities": "Encje" } }, + "lock": { + "data": { + "entities": "Encje", + "hide_members": "Ukryj encje" + } + }, + "media_player": { + "data": { + "entities": "Encje", + "hide_members": "Ukryj encje" + } + }, "media_player_options": { "data": { "entities": "Encje" } + }, + "switch": { + "data": { + "all": "Wszystkie encje", + "entities": "Encje", + "hide_members": "Ukryj encje" + }, + "description": "Je\u015bli \u201ewszystkie encje\u201d jest w\u0142\u0105czone, stan grupy jest w\u0142\u0105czony tylko wtedy, gdy wszystkie encje s\u0105 w\u0142\u0105czone. Je\u015bli \u201ewszystkie encje\u201d jest wy\u0142\u0105czone, stan grupy jest w\u0142\u0105czony, je\u015bli kt\u00f3rakolwiek encja jest w\u0142\u0105czona." } } }, diff --git a/homeassistant/components/group/translations/pt-BR.json b/homeassistant/components/group/translations/pt-BR.json index 5959ba66da7..0987b41f4ae 100644 --- a/homeassistant/components/group/translations/pt-BR.json +++ b/homeassistant/components/group/translations/pt-BR.json @@ -5,18 +5,21 @@ "data": { "all": "Todas as entidades", "entities": "Membros", + "hide_members": "Ocultar membros", "name": "Nome" }, "description": "Se \"todas as entidades\" estiver habilitada, o estado do grupo estar\u00e1 ativado somente se todos os membros estiverem ativados. Se \"todas as entidades\" estiver desabilitada, o estado do grupo estar\u00e1 ativado se algum membro estiver ativado.", - "title": "Novo grupo" + "title": "Adicionar grupo" }, "cover": { "data": { "entities": "Membros", + "hide_members": "Ocultar membros", "name": "Nome", - "title": "Novo grupo" + "title": "Adicionar grupo" }, - "description": "Selecione as op\u00e7\u00f5es do grupo" + "description": "Selecione as op\u00e7\u00f5es do grupo", + "title": "Adicionar grupo" }, "cover_options": { "data": { @@ -27,10 +30,12 @@ "fan": { "data": { "entities": "Membros", + "hide_members": "Ocultar membros", "name": "Nome", - "title": "Novo grupo" + "title": "Adicionar grupo" }, - "description": "Selecione as op\u00e7\u00f5es do grupo" + "description": "Selecione as op\u00e7\u00f5es do grupo", + "title": "Adicionar grupo" }, "fan_options": { "data": { @@ -46,11 +51,14 @@ }, "light": { "data": { + "all": "Todas as entidades", "entities": "Membros", + "hide_members": "Ocultar membros", "name": "Nome", - "title": "Novo grupo" + "title": "Adicionar grupo" }, - "description": "Selecione as op\u00e7\u00f5es do grupo" + "description": "Se \"todas as entidades\" estiver habilitada, o estado do grupo estar\u00e1 ativado somente se todos os membros estiverem ativados. Se \"todas as entidades\" estiver desabilitada, o estado do grupo estar\u00e1 ativado se algum membro estiver ativado.", + "title": "Adicionar grupo" }, "light_options": { "data": { @@ -58,13 +66,23 @@ }, "description": "Selecione as op\u00e7\u00f5es do grupo" }, + "lock": { + "data": { + "entities": "Membros", + "hide_members": "Ocultar membros", + "name": "Nome" + }, + "title": "Adicionar grupo" + }, "media_player": { "data": { "entities": "Membros", + "hide_members": "Ocultar membros", "name": "Nome", - "title": "Novo grupo" + "title": "Adicionar grupo" }, - "description": "Selecione as op\u00e7\u00f5es do grupo" + "description": "Selecione as op\u00e7\u00f5es do grupo", + "title": "Adicionar grupo" }, "media_player_options": { "data": { @@ -72,41 +90,108 @@ }, "description": "Selecione as op\u00e7\u00f5es do grupo" }, + "switch": { + "data": { + "entities": "Membros", + "hide_members": "Ocultar membros", + "name": "Nome" + }, + "title": "Adicionar grupo" + }, "user": { "data": { "group_type": "Tipo de grupo" }, - "title": "Novo grupo" + "description": "Os grupos permitem que voc\u00ea crie uma nova entidade que representa v\u00e1rias entidades do mesmo tipo.", + "menu_options": { + "binary_sensor": "Grupo de sensores bin\u00e1rios", + "cover": "Grupo de persianas", + "fan": "Grupo de ventiladores", + "light": "Grupo de l\u00e2mpadas", + "lock": "Grupo de fechaduras", + "media_player": "Grupo de reprodutores de m\u00eddia", + "switch": "Grupo de interruptores" + }, + "title": "Adicionar grupo" } } }, "options": { "step": { + "binary_sensor": { + "data": { + "all": "Todas as entidades", + "entities": "Membros", + "hide_members": "Ocultar membros" + }, + "description": "Se \"todas as entidades\" estiver habilitada, o estado do grupo estar\u00e1 ativado somente se todos os membros estiverem ativados. Se \"todas as entidades\" estiver desabilitada, o estado do grupo estar\u00e1 ativado se algum membro estiver ativado." + }, "binary_sensor_options": { "data": { "all": "Todas as entidades", "entities": "Membros" } }, + "cover": { + "data": { + "entities": "Membros", + "hide_members": "Ocultar membros" + } + }, "cover_options": { "data": { "entities": "Membros" } }, + "fan": { + "data": { + "entities": "Membros", + "hide_members": "Ocultar membros" + } + }, "fan_options": { "data": { "entities": "Membros" } }, + "light": { + "data": { + "all": "Todas as entidades", + "entities": "Membros", + "hide_members": "Ocultar membros" + }, + "description": "Se \"todas as entidades\" estiver habilitada, o estado do grupo estar\u00e1 ativado somente se todos os membros estiverem ativados. Se \"todas as entidades\" estiver desabilitada, o estado do grupo estar\u00e1 ativado se algum membro estiver ativado." + }, "light_options": { "data": { + "all": "Todas as entidades", "entities": "Membros" } }, + "lock": { + "data": { + "entities": "Membros", + "hide_members": "Ocultar membros" + } + }, + "media_player": { + "data": { + "entities": "Membros", + "hide_members": "Ocultar membros" + } + }, "media_player_options": { "data": { "entities": "Membros" } + }, + "switch": { + "data": { + "all": "Todas as entidades", + "entities": "Membros", + "hide_members": "Ocultar membros" + }, + "description": "Se \"todas as entidades\" estiver habilitada, o estado do grupo estar\u00e1 ativado somente se todos os membros estiverem ativados. Se \"todas as entidades\" estiver desabilitada, o estado do grupo estar\u00e1 ativado se algum membro estiver ativado." } } }, diff --git a/homeassistant/components/group/translations/ru.json b/homeassistant/components/group/translations/ru.json index baba258b654..0ba6d79900e 100644 --- a/homeassistant/components/group/translations/ru.json +++ b/homeassistant/components/group/translations/ru.json @@ -5,18 +5,21 @@ "data": { "all": "\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, "description": "\u0415\u0441\u043b\u0438 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \"\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b\", \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0443\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0432\u0441\u0435 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438. \u0412 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u043b\u044e\u0431\u043e\u0439 \u0438\u0437 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432.", - "title": "\u041d\u043e\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430" + "title": "\u0413\u0440\u0443\u043f\u043f\u0430" }, "cover": { "data": { "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "title": "\u041d\u043e\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430" + "title": "\u0413\u0440\u0443\u043f\u043f\u0430" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b.", + "title": "\u0413\u0440\u0443\u043f\u043f\u0430" }, "cover_options": { "data": { @@ -27,10 +30,12 @@ "fan": { "data": { "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "title": "\u041d\u043e\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430" + "title": "\u0413\u0440\u0443\u043f\u043f\u0430" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b.", + "title": "\u0413\u0440\u0443\u043f\u043f\u0430" }, "fan_options": { "data": { @@ -46,11 +51,14 @@ }, "light": { "data": { + "all": "\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "title": "\u041d\u043e\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430" + "title": "\u0413\u0440\u0443\u043f\u043f\u0430" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + "description": "\u0415\u0441\u043b\u0438 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \"\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b\", \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0443\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0432\u0441\u0435 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438. \u0412 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u043b\u044e\u0431\u043e\u0439 \u0438\u0437 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432.", + "title": "\u0413\u0440\u0443\u043f\u043f\u0430" }, "light_options": { "data": { @@ -58,13 +66,23 @@ }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." }, + "lock": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "title": "\u0413\u0440\u0443\u043f\u043f\u0430" + }, "media_player": { "data": { "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "title": "\u041d\u043e\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430" + "title": "\u0413\u0440\u0443\u043f\u043f\u0430" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b.", + "title": "\u0413\u0440\u0443\u043f\u043f\u0430" }, "media_player_options": { "data": { @@ -72,41 +90,108 @@ }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." }, + "switch": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "title": "\u0413\u0440\u0443\u043f\u043f\u0430" + }, "user": { "data": { "group_type": "\u0422\u0438\u043f \u0433\u0440\u0443\u043f\u043f\u044b" }, - "title": "\u041d\u043e\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430" + "description": "\u0413\u0440\u0443\u043f\u043f\u044b \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u044e\u0442 \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u044e\u0449\u0438\u0435 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u043e\u0434\u043d\u043e\u0433\u043e \u0442\u0438\u043f\u0430.", + "menu_options": { + "binary_sensor": "\u0413\u0440\u0443\u043f\u043f\u0430 \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432", + "cover": "\u0413\u0440\u0443\u043f\u043f\u0430 \u0448\u0442\u043e\u0440 \u0438\u043b\u0438 \u0436\u0430\u043b\u044e\u0437\u0438", + "fan": "\u0413\u0440\u0443\u043f\u043f\u0430 \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0442\u043e\u0440\u043e\u0432", + "light": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f", + "lock": "\u0413\u0440\u0443\u043f\u043f\u0430 \u0437\u0430\u043c\u043a\u043e\u0432", + "media_player": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440\u043e\u0432", + "switch": "\u0413\u0440\u0443\u043f\u043f\u0430 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u0435\u0439" + }, + "title": "\u0413\u0440\u0443\u043f\u043f\u0430" } } }, "options": { "step": { + "binary_sensor": { + "data": { + "all": "\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432" + }, + "description": "\u0415\u0441\u043b\u0438 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \"\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b\", \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0443\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0432\u0441\u0435 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438. \u0412 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u043b\u044e\u0431\u043e\u0439 \u0438\u0437 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432." + }, "binary_sensor_options": { "data": { "all": "\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" } }, + "cover": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432" + } + }, "cover_options": { "data": { "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" } }, + "fan": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432" + } + }, "fan_options": { "data": { "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" } }, + "light": { + "data": { + "all": "\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432" + }, + "description": "\u0415\u0441\u043b\u0438 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \"\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b\", \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0443\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0432\u0441\u0435 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438. \u0412 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u043b\u044e\u0431\u043e\u0439 \u0438\u0437 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432." + }, "light_options": { "data": { + "all": "\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" } }, + "lock": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432" + } + }, + "media_player": { + "data": { + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432" + } + }, "media_player_options": { "data": { "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" } + }, + "switch": { + "data": { + "all": "\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", + "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", + "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432" + }, + "description": "\u0415\u0441\u043b\u0438 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \"\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b\", \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0443\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0432\u0441\u0435 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438. \u0412 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u043b\u044e\u0431\u043e\u0439 \u0438\u0437 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432." } } }, diff --git a/homeassistant/components/group/translations/tr.json b/homeassistant/components/group/translations/tr.json index bbb4600a4ed..354ed29214c 100644 --- a/homeassistant/components/group/translations/tr.json +++ b/homeassistant/components/group/translations/tr.json @@ -1,12 +1,25 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "all": "T\u00fcm varl\u0131klar", + "entities": "\u00dcyeler", + "hide_members": "\u00dcyeleri gizle", + "name": "Ad" + }, + "description": "\"T\u00fcm varl\u0131klar\" etkinle\u015ftirilirse, grubun durumu yaln\u0131zca t\u00fcm \u00fcyeler a\u00e7\u0131ksa a\u00e7\u0131kt\u0131r. \"T\u00fcm varl\u0131klar\" devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131rsa, herhangi bir \u00fcye a\u00e7\u0131ksa grubun durumu a\u00e7\u0131kt\u0131r.", + "title": "Grup Ekle" + }, "cover": { "data": { - "entities": "Grup \u00fcyeleri", - "name": "Grup ismi" + "entities": "\u00dcyeler", + "hide_members": "\u00dcyeleri gizle", + "name": "Ad", + "title": "Grup Ekle" }, - "description": "Grup se\u00e7eneklerini se\u00e7in" + "description": "Grup se\u00e7eneklerini se\u00e7in", + "title": "Grup Ekle" }, "cover_options": { "data": { @@ -16,10 +29,13 @@ }, "fan": { "data": { - "entities": "Grup \u00fcyeleri", - "name": "Grup ismi" + "entities": "\u00dcyeler", + "hide_members": "\u00dcyeleri gizle", + "name": "Ad", + "title": "Grup Ekle" }, - "description": "Grup se\u00e7eneklerini se\u00e7in" + "description": "Grup se\u00e7eneklerini se\u00e7in", + "title": "Grup Ekle" }, "fan_options": { "data": { @@ -35,10 +51,14 @@ }, "light": { "data": { - "entities": "Grup \u00fcyeleri", - "name": "Grup ismi" + "all": "T\u00fcm varl\u0131klar", + "entities": "\u00dcyeler", + "hide_members": "\u00dcyeleri gizle", + "name": "Ad", + "title": "Grup Ekle" }, - "description": "Grup se\u00e7eneklerini se\u00e7in" + "description": "\"T\u00fcm varl\u0131klar\" etkinle\u015ftirilirse, grubun durumu yaln\u0131zca t\u00fcm \u00fcyeler a\u00e7\u0131ksa a\u00e7\u0131kt\u0131r. \"T\u00fcm varl\u0131klar\" devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131rsa, herhangi bir \u00fcye a\u00e7\u0131ksa grubun durumu a\u00e7\u0131kt\u0131r.", + "title": "Grup Ekle" }, "light_options": { "data": { @@ -46,18 +66,132 @@ }, "description": "Grup se\u00e7eneklerini se\u00e7in" }, + "lock": { + "data": { + "entities": "\u00dcyeler", + "hide_members": "\u00dcyeleri gizle", + "name": "Ad" + }, + "title": "Grup Ekle" + }, "media_player": { "data": { - "entities": "Grup \u00fcyeleri", - "name": "Grup ismi" + "entities": "\u00dcyeler", + "hide_members": "\u00dcyeleri gizle", + "name": "Ad", + "title": "Grup Ekle" }, - "description": "Grup se\u00e7eneklerini se\u00e7in" + "description": "Grup se\u00e7eneklerini se\u00e7in", + "title": "Grup Ekle" }, "media_player_options": { "data": { "entities": "Grup \u00fcyeleri" }, "description": "Grup se\u00e7eneklerini se\u00e7in" + }, + "switch": { + "data": { + "entities": "\u00dcyeler", + "hide_members": "\u00dcyeleri gizle", + "name": "Ad" + }, + "title": "Grup Ekle" + }, + "user": { + "data": { + "group_type": "Grup t\u00fcr\u00fc" + }, + "description": "Gruplar, ayn\u0131 t\u00fcrden birden \u00e7ok varl\u0131\u011f\u0131 temsil eden yeni bir varl\u0131k olu\u015fturman\u0131za olanak tan\u0131r.", + "menu_options": { + "binary_sensor": "\u0130kili sens\u00f6r grubu", + "cover": "Kepenk grubu", + "fan": "Fan grubu", + "light": "I\u015f\u0131k grubu", + "lock": "Grubu kilitle", + "media_player": "Medya oynat\u0131c\u0131 grubu", + "switch": "Grubu de\u011fi\u015ftir" + }, + "title": "Grup Ekle" + } + } + }, + "options": { + "step": { + "binary_sensor": { + "data": { + "all": "T\u00fcm varl\u0131klar", + "entities": "\u00dcyeler", + "hide_members": "\u00dcyeleri gizle" + }, + "description": "\"T\u00fcm varl\u0131klar\" etkinle\u015ftirilirse, grubun durumu yaln\u0131zca t\u00fcm \u00fcyeler a\u00e7\u0131ksa a\u00e7\u0131kt\u0131r. \"T\u00fcm varl\u0131klar\" devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131rsa, herhangi bir \u00fcye a\u00e7\u0131ksa grubun durumu a\u00e7\u0131kt\u0131r." + }, + "binary_sensor_options": { + "data": { + "all": "T\u00fcm varl\u0131klar", + "entities": "\u00dcyeler" + } + }, + "cover": { + "data": { + "entities": "\u00dcyeler", + "hide_members": "\u00dcyeleri gizle" + } + }, + "cover_options": { + "data": { + "entities": "\u00dcyeler" + } + }, + "fan": { + "data": { + "entities": "\u00dcyeler", + "hide_members": "\u00dcyeleri gizle" + } + }, + "fan_options": { + "data": { + "entities": "\u00dcyeler" + } + }, + "light": { + "data": { + "all": "T\u00fcm varl\u0131klar", + "entities": "\u00dcyeler", + "hide_members": "\u00dcyeleri gizle" + }, + "description": "\"T\u00fcm varl\u0131klar\" etkinle\u015ftirilirse, grubun durumu yaln\u0131zca t\u00fcm \u00fcyeler a\u00e7\u0131ksa a\u00e7\u0131kt\u0131r. \"T\u00fcm varl\u0131klar\" devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131rsa, herhangi bir \u00fcye a\u00e7\u0131ksa grubun durumu a\u00e7\u0131kt\u0131r." + }, + "light_options": { + "data": { + "all": "T\u00fcm varl\u0131klar", + "entities": "\u00dcyeler" + } + }, + "lock": { + "data": { + "entities": "\u00dcyeler", + "hide_members": "\u00dcyeleri gizle" + } + }, + "media_player": { + "data": { + "entities": "\u00dcyeler", + "hide_members": "\u00dcyeleri gizle" + } + }, + "media_player_options": { + "data": { + "entities": "\u00dcyeler" + } + }, + "switch": { + "data": { + "all": "T\u00fcm varl\u0131klar", + "entities": "\u00dcyeler", + "hide_members": "\u00dcyeleri gizle" + }, + "description": "\"T\u00fcm varl\u0131klar\" etkinle\u015ftirilirse, grubun durumu yaln\u0131zca t\u00fcm \u00fcyeler a\u00e7\u0131ksa a\u00e7\u0131kt\u0131r. \"T\u00fcm varl\u0131klar\" devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131rsa, herhangi bir \u00fcye a\u00e7\u0131ksa grubun durumu a\u00e7\u0131kt\u0131r." } } }, diff --git a/homeassistant/components/group/translations/zh-Hant.json b/homeassistant/components/group/translations/zh-Hant.json index 9187455ad17..380c2216976 100644 --- a/homeassistant/components/group/translations/zh-Hant.json +++ b/homeassistant/components/group/translations/zh-Hant.json @@ -5,6 +5,7 @@ "data": { "all": "\u6240\u6709\u5be6\u9ad4", "entities": "\u6210\u54e1", + "hide_members": "\u96b1\u85cf\u6210\u54e1", "name": "\u540d\u7a31" }, "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u88dd\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002", @@ -13,10 +14,12 @@ "cover": { "data": { "entities": "\u6210\u54e1", + "hide_members": "\u96b1\u85cf\u6210\u54e1", "name": "\u540d\u7a31", "title": "\u65b0\u589e\u7fa4\u7d44" }, - "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805", + "title": "\u65b0\u589e\u7fa4\u7d44" }, "cover_options": { "data": { @@ -27,10 +30,12 @@ "fan": { "data": { "entities": "\u6210\u54e1", + "hide_members": "\u96b1\u85cf\u6210\u54e1", "name": "\u540d\u7a31", "title": "\u65b0\u589e\u7fa4\u7d44" }, - "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805", + "title": "\u65b0\u589e\u7fa4\u7d44" }, "fan_options": { "data": { @@ -46,11 +51,14 @@ }, "light": { "data": { + "all": "\u6240\u6709\u5be6\u9ad4", "entities": "\u6210\u54e1", + "hide_members": "\u96b1\u85cf\u6210\u54e1", "name": "\u540d\u7a31", "title": "\u65b0\u589e\u7fa4\u7d44" }, - "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u88dd\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002", + "title": "\u65b0\u589e\u7fa4\u7d44" }, "light_options": { "data": { @@ -58,13 +66,23 @@ }, "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" }, + "lock": { + "data": { + "entities": "\u6210\u54e1", + "hide_members": "\u96b1\u85cf\u6210\u54e1", + "name": "\u540d\u7a31" + }, + "title": "\u65b0\u589e\u7fa4\u7d44" + }, "media_player": { "data": { "entities": "\u6210\u54e1", + "hide_members": "\u96b1\u85cf\u6210\u54e1", "name": "\u540d\u7a31", "title": "\u65b0\u589e\u7fa4\u7d44" }, - "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" + "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805", + "title": "\u65b0\u589e\u7fa4\u7d44" }, "media_player_options": { "data": { @@ -72,41 +90,108 @@ }, "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" }, + "switch": { + "data": { + "entities": "\u6210\u54e1", + "hide_members": "\u96b1\u85cf\u6210\u54e1", + "name": "\u540d\u7a31" + }, + "title": "\u65b0\u589e\u7fa4\u7d44" + }, "user": { "data": { "group_type": "\u7fa4\u7d44\u985e\u5225" }, + "description": "\u7fa4\u7d44\u5141\u8a31\u4f7f\u7528\u76f8\u540c\u985e\u5225\u7684\u591a\u500b\u5be6\u9ad4\u7d44\u6210\u65b0\u5be6\u9ad4\u3002", + "menu_options": { + "binary_sensor": "\u4e8c\u9032\u4f4d\u611f\u6e2c\u5668\u7fa4\u7d44", + "cover": "\u6372\u7c3e\u7fa4\u7d44", + "fan": "\u98a8\u6247\u7fa4\u7d44", + "light": "\u71c8\u5149\u7fa4\u7d44", + "lock": "\u9580\u9396\u7fa4\u7d44", + "media_player": "\u5a92\u9ad4\u64ad\u653e\u5668\u7fa4\u7d44", + "switch": "\u958b\u95dc\u7fa4\u7d44" + }, "title": "\u65b0\u589e\u7fa4\u7d44" } } }, "options": { "step": { + "binary_sensor": { + "data": { + "all": "\u6240\u6709\u5be6\u9ad4", + "entities": "\u6210\u54e1", + "hide_members": "\u96b1\u85cf\u6210\u54e1" + }, + "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u88dd\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002" + }, "binary_sensor_options": { "data": { "all": "\u6240\u6709\u5be6\u9ad4", "entities": "\u6210\u54e1" } }, + "cover": { + "data": { + "entities": "\u6210\u54e1", + "hide_members": "\u96b1\u85cf\u6210\u54e1" + } + }, "cover_options": { "data": { "entities": "\u6210\u54e1" } }, + "fan": { + "data": { + "entities": "\u6210\u54e1", + "hide_members": "\u96b1\u85cf\u6210\u54e1" + } + }, "fan_options": { "data": { "entities": "\u6210\u54e1" } }, + "light": { + "data": { + "all": "\u6240\u6709\u5be6\u9ad4", + "entities": "\u6210\u54e1", + "hide_members": "\u96b1\u85cf\u6210\u54e1" + }, + "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u88dd\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002" + }, "light_options": { "data": { + "all": "\u6240\u6709\u5be6\u9ad4", "entities": "\u6210\u54e1" } }, + "lock": { + "data": { + "entities": "\u6210\u54e1", + "hide_members": "\u96b1\u85cf\u6210\u54e1" + } + }, + "media_player": { + "data": { + "entities": "\u6210\u54e1", + "hide_members": "\u96b1\u85cf\u6210\u54e1" + } + }, "media_player_options": { "data": { "entities": "\u6210\u54e1" } + }, + "switch": { + "data": { + "all": "\u6240\u6709\u5be6\u9ad4", + "entities": "\u6210\u54e1", + "hide_members": "\u96b1\u85cf\u6210\u54e1" + }, + "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u88dd\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002" } } }, diff --git a/homeassistant/components/growatt_server/translations/hu.json b/homeassistant/components/growatt_server/translations/hu.json index 5b2efc737fe..44d87bf753e 100644 --- a/homeassistant/components/growatt_server/translations/hu.json +++ b/homeassistant/components/growatt_server/translations/hu.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3", "url": "URL", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json index 04b62bb660e..82fd422abf7 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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { diff --git a/homeassistant/components/hangouts/translations/ca.json b/homeassistant/components/hangouts/translations/ca.json index b4114312f3e..8b629201cc1 100644 --- a/homeassistant/components/hangouts/translations/ca.json +++ b/homeassistant/components/hangouts/translations/ca.json @@ -24,7 +24,7 @@ "password": "Contrasenya" }, "description": "Buit", - "title": "Inici de sessi\u00f3 de Google Hangouts" + "title": "Inici de sessi\u00f3 de Google Chat" } } } diff --git a/homeassistant/components/hangouts/translations/de.json b/homeassistant/components/hangouts/translations/de.json index 02481670a09..b26618940be 100644 --- a/homeassistant/components/hangouts/translations/de.json +++ b/homeassistant/components/hangouts/translations/de.json @@ -23,7 +23,7 @@ "password": "Passwort" }, "description": "Leer", - "title": "Google Hangouts Login" + "title": "Google Chat Anmeldung" } } } diff --git a/homeassistant/components/hangouts/translations/et.json b/homeassistant/components/hangouts/translations/et.json index 7d6deb2ef53..96ed8b998ec 100644 --- a/homeassistant/components/hangouts/translations/et.json +++ b/homeassistant/components/hangouts/translations/et.json @@ -24,7 +24,7 @@ "password": "Salas\u00f5na" }, "description": "", - "title": "Google Hangoutsi sisselogimine" + "title": "Google Chat'i sisselogimine" } } } diff --git a/homeassistant/components/hangouts/translations/fr.json b/homeassistant/components/hangouts/translations/fr.json index da674b1c775..78a2517d29a 100644 --- a/homeassistant/components/hangouts/translations/fr.json +++ b/homeassistant/components/hangouts/translations/fr.json @@ -5,9 +5,9 @@ "unknown": "Erreur inattendue" }, "error": { - "invalid_2fa": "Authentification \u00e0 2 facteurs invalide, veuillez r\u00e9essayer.", + "invalid_2fa": "Authentification \u00e0 deux facteurs non valide, veuillez r\u00e9essayer.", "invalid_2fa_method": "M\u00e9thode 2FA non valide (v\u00e9rifiez sur le t\u00e9l\u00e9phone).", - "invalid_login": "Login invalide, veuillez r\u00e9essayer." + "invalid_login": "Identifiant non valide, veuillez r\u00e9essayer." }, "step": { "2fa": { @@ -24,7 +24,7 @@ "password": "Mot de passe" }, "description": "Vide", - "title": "Connexion \u00e0 Google Hangouts" + "title": "Connexion \u00e0 Google Chat" } } } diff --git a/homeassistant/components/hangouts/translations/he.json b/homeassistant/components/hangouts/translations/he.json index 9f0e3b48a62..ad696cad365 100644 --- a/homeassistant/components/hangouts/translations/he.json +++ b/homeassistant/components/hangouts/translations/he.json @@ -21,7 +21,7 @@ "email": "\u05d3\u05d5\u05d0\"\u05dc", "password": "\u05e1\u05d9\u05e1\u05de\u05d4" }, - "title": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc- Google Hangouts" + "title": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05dc\u05e6'\u05d0\u05d8 \u05e9\u05dc \u05d2\u05d5\u05d2\u05dc" } } } diff --git a/homeassistant/components/hangouts/translations/hu.json b/homeassistant/components/hangouts/translations/hu.json index eda0144a818..1ea997aa098 100644 --- a/homeassistant/components/hangouts/translations/hu.json +++ b/homeassistant/components/hangouts/translations/hu.json @@ -24,7 +24,7 @@ "password": "Jelsz\u00f3" }, "description": "\u00dcres", - "title": "Google Hangouts Bejelentkez\u00e9s" + "title": "Google Chat bejelentkez\u00e9s" } } } diff --git a/homeassistant/components/hangouts/translations/id.json b/homeassistant/components/hangouts/translations/id.json index 39c68dda211..2336b211c9e 100644 --- a/homeassistant/components/hangouts/translations/id.json +++ b/homeassistant/components/hangouts/translations/id.json @@ -24,7 +24,7 @@ "password": "Kata Sandi" }, "description": "Kosong", - "title": "Info Masuk Google Hangouts" + "title": "Info Masuk Google Chat" } } } diff --git a/homeassistant/components/hangouts/translations/it.json b/homeassistant/components/hangouts/translations/it.json index 3d9322b1152..76d81a184d9 100644 --- a/homeassistant/components/hangouts/translations/it.json +++ b/homeassistant/components/hangouts/translations/it.json @@ -24,7 +24,7 @@ "password": "Password" }, "description": "Vuoto", - "title": "Accesso a Google Hangouts" + "title": "Accesso a Google Chat" } } } diff --git a/homeassistant/components/hangouts/translations/nl.json b/homeassistant/components/hangouts/translations/nl.json index 456d2193922..276c310da5a 100644 --- a/homeassistant/components/hangouts/translations/nl.json +++ b/homeassistant/components/hangouts/translations/nl.json @@ -24,7 +24,7 @@ "password": "Wachtwoord" }, "description": "Leeg", - "title": "Google Hangouts inlog" + "title": "Google Chat-login" } } } diff --git a/homeassistant/components/hangouts/translations/no.json b/homeassistant/components/hangouts/translations/no.json index d4fe4dbb5a6..751d54c852e 100644 --- a/homeassistant/components/hangouts/translations/no.json +++ b/homeassistant/components/hangouts/translations/no.json @@ -24,7 +24,7 @@ "password": "Passord" }, "description": "", - "title": "Google Hangouts p\u00e5logging" + "title": "Google Chat-p\u00e5logging" } } } diff --git a/homeassistant/components/hangouts/translations/pl.json b/homeassistant/components/hangouts/translations/pl.json index 8fb7e9e64d9..4dafdc5d996 100644 --- a/homeassistant/components/hangouts/translations/pl.json +++ b/homeassistant/components/hangouts/translations/pl.json @@ -24,7 +24,7 @@ "password": "Has\u0142o" }, "description": "Pusty", - "title": "Logowanie do Google Hangouts" + "title": "Logowanie do Google Chat" } } } diff --git a/homeassistant/components/hangouts/translations/pt-BR.json b/homeassistant/components/hangouts/translations/pt-BR.json index c60d9d8ec47..9e4d04cd989 100644 --- a/homeassistant/components/hangouts/translations/pt-BR.json +++ b/homeassistant/components/hangouts/translations/pt-BR.json @@ -24,7 +24,7 @@ "password": "Senha" }, "description": "Vazio", - "title": "Login do Hangouts do Google" + "title": "Login no Google Chat" } } } diff --git a/homeassistant/components/hangouts/translations/ru.json b/homeassistant/components/hangouts/translations/ru.json index d352258ba33..781e1e25eef 100644 --- a/homeassistant/components/hangouts/translations/ru.json +++ b/homeassistant/components/hangouts/translations/ru.json @@ -24,7 +24,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, "description": "\u043f\u0443\u0441\u0442\u043e", - "title": "Google Hangouts" + "title": "Google Chat" } } } diff --git a/homeassistant/components/hangouts/translations/tr.json b/homeassistant/components/hangouts/translations/tr.json index 84fb80abaf5..5ddf2a64cbb 100644 --- a/homeassistant/components/hangouts/translations/tr.json +++ b/homeassistant/components/hangouts/translations/tr.json @@ -24,7 +24,7 @@ "password": "Parola" }, "description": "Bo\u015f", - "title": "Google Hangouts Giri\u015fi" + "title": "Google Sohbet Giri\u015fi" } } } diff --git a/homeassistant/components/hangouts/translations/zh-Hant.json b/homeassistant/components/hangouts/translations/zh-Hant.json index 678aacc5b62..f9884d5e214 100644 --- a/homeassistant/components/hangouts/translations/zh-Hant.json +++ b/homeassistant/components/hangouts/translations/zh-Hant.json @@ -24,7 +24,7 @@ "password": "\u5bc6\u78bc" }, "description": "\u7a7a\u767d", - "title": "\u767b\u5165 Google Hangouts" + "title": "\u767b\u5165 Google Chat" } } } diff --git a/homeassistant/components/hassio/translations/ja.json b/homeassistant/components/hassio/translations/ja.json index da3409d91cd..b7489852bdb 100644 --- a/homeassistant/components/hassio/translations/ja.json +++ b/homeassistant/components/hassio/translations/ja.json @@ -10,7 +10,7 @@ "installed_addons": "\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u6e08\u307f\u306e\u30a2\u30c9\u30aa\u30f3", "supervisor_api": "Supervisor API", "supervisor_version": "Supervisor\u306e\u30d0\u30fc\u30b8\u30e7\u30f3", - "supported": "\u30b5\u30dd\u30fc\u30c8", + "supported": "\u30b5\u30dd\u30fc\u30c8\u306e\u6709\u7121", "update_channel": "\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u30c1\u30e3\u30f3\u30cd\u30eb", "version_api": "\u30d0\u30fc\u30b8\u30e7\u30f3API" } diff --git a/homeassistant/components/home_connect/translations/hu.json b/homeassistant/components/home_connect/translations/hu.json index ca5f3e1e9ae..77b76df14d7 100644 --- a/homeassistant/components/home_connect/translations/hu.json +++ b/homeassistant/components/home_connect/translations/hu.json @@ -2,14 +2,14 @@ "config": { "abort": { "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." + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3." }, "create_entry": { "default": "Sikeres hiteles\u00edt\u00e9s" }, "step": { "pick_implementation": { - "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" } } } diff --git a/homeassistant/components/home_connect/translations/it.json b/homeassistant/components/home_connect/translations/it.json index 3dc834bfd85..74a598757d8 100644 --- a/homeassistant/components/home_connect/translations/it.json +++ b/homeassistant/components/home_connect/translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})" }, "create_entry": { diff --git a/homeassistant/components/home_plus_control/translations/hu.json b/homeassistant/components/home_plus_control/translations/hu.json index 09625a222f2..3dc4b451332 100644 --- a/homeassistant/components/home_plus_control/translations/hu.json +++ b/homeassistant/components/home_plus_control/translations/hu.json @@ -2,10 +2,10 @@ "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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "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.", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "create_entry": { @@ -13,7 +13,7 @@ }, "step": { "pick_implementation": { - "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" } } }, diff --git a/homeassistant/components/home_plus_control/translations/it.json b/homeassistant/components/home_plus_control/translations/it.json index 789a7db85eb..e79d22275f7 100644 --- a/homeassistant/components/home_plus_control/translations/it.json +++ b/homeassistant/components/home_plus_control/translations/it.json @@ -4,7 +4,7 @@ "already_configured": "L'account \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", - "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, diff --git a/homeassistant/components/homekit/translations/bg.json b/homeassistant/components/homekit/translations/bg.json index a7aa5bb792b..1a0ba552ae7 100644 --- a/homeassistant/components/homekit/translations/bg.json +++ b/homeassistant/components/homekit/translations/bg.json @@ -17,12 +17,6 @@ "entities": "\u041e\u0431\u0435\u043a\u0442\u0438" } }, - "include_exclude": { - "data": { - "entities": "\u041e\u0431\u0435\u043a\u0442\u0438", - "mode": "\u0420\u0435\u0436\u0438\u043c" - } - }, "init": { "data": { "mode": "\u0420\u0435\u0436\u0438\u043c" diff --git a/homeassistant/components/homekit/translations/ca.json b/homeassistant/components/homekit/translations/ca.json index f347268d90e..f1036cceb25 100644 --- a/homeassistant/components/homekit/translations/ca.json +++ b/homeassistant/components/homekit/translations/ca.json @@ -55,18 +55,9 @@ "description": "S'inclouran totes les entitats de \u201c{domains}\u201d tret que se'n seleccionin d'espec\u00edfiques.", "title": "Selecciona les entitats a incloure" }, - "include_exclude": { - "data": { - "entities": "Entitats", - "mode": "Mode" - }, - "description": "Tria les entitats a incloure. En mode accessori, nom\u00e9s s'inclou una sola entitat. En mode enlla\u00e7 inclusiu, s'inclouran totes les entitats del domini tret de que se'n seleccionin algunes en concret. En mode enlla\u00e7 excusiu, s'inclouran totes les entitats del domini excepte les entitats excloses. Per obtenir el millor rendiment, es crea una inst\u00e0ncia HomeKit per a cada repoductor multim\u00e8dia/TV i c\u00e0mera.", - "title": "Selecciona les entitats a incloure" - }, "init": { "data": { "domains": "Dominis a incloure", - "include_domains": "Dominis a incloure", "include_exclude_mode": "Mode d'inclusi\u00f3", "mode": "Mode de HomeKit" }, diff --git a/homeassistant/components/homekit/translations/cs.json b/homeassistant/components/homekit/translations/cs.json index 765b3c3c1b4..7103e1e3caa 100644 --- a/homeassistant/components/homekit/translations/cs.json +++ b/homeassistant/components/homekit/translations/cs.json @@ -28,18 +28,9 @@ "description": "Zkontrolujte v\u0161echny kamery, kter\u00e9 podporuj\u00ed nativn\u00ed streamy H.264. Pokud kamera nepodporuje stream H.264, syst\u00e9m video p\u0159ek\u00f3duje pro HomeKit na H.264. P\u0159ek\u00f3dov\u00e1n\u00ed vy\u017eaduje v\u00fdkonn\u00fd procesor a je pravd\u011bpodobn\u00e9, \u017ee nebude fungovat na po\u010d\u00edta\u010d\u00edch s jednou z\u00e1kladn\u00ed deskou.", "title": "Vyberte videokodek kamery." }, - "include_exclude": { - "data": { - "entities": "Entity", - "mode": "Re\u017eim" - }, - "description": "Vyberte entity, kter\u00e9 maj\u00ed b\u00fdt vystaveny. V re\u017eimu P\u0159\u00edslu\u0161enstv\u00ed je vystavena pouze jedna entita. V re\u017eimu Zahrnut\u00ed p\u0159emost\u011bn\u00ed budou vystaveny v\u0161echny entity v dom\u00e9n\u011b, pokud nebudou vybr\u00e1ny konkr\u00e9tn\u00ed entity. V re\u017eimu Vylou\u010den\u00ed p\u0159emost\u011bn\u00ed budou vystaveny v\u0161echny entity v dom\u00e9n\u011b krom\u011b vylou\u010den\u00fdch entit.", - "title": "Vyberte entity, kter\u00e9 chcete vystavit" - }, "init": { "data": { "domains": "Dom\u00e9ny, kter\u00e9 maj\u00ed b\u00fdt zahrnuty", - "include_domains": "Dom\u00e9ny, kter\u00e9 maj\u00ed b\u00fdt zahrnuty", "mode": "Re\u017eim" }, "title": "Vyberte dom\u00e9ny, kter\u00e9 chcete vystavit." diff --git a/homeassistant/components/homekit/translations/de.json b/homeassistant/components/homekit/translations/de.json index 24f6fd84eb0..d9c07e4cf21 100644 --- a/homeassistant/components/homekit/translations/de.json +++ b/homeassistant/components/homekit/translations/de.json @@ -55,18 +55,9 @@ "description": "Alle \"{domains}\"-Entit\u00e4ten werden einbezogen, sofern nicht bestimmte Entit\u00e4ten ausgew\u00e4hlt werden.", "title": "W\u00e4hle die einzuschlie\u00dfenden Entit\u00e4ten aus" }, - "include_exclude": { - "data": { - "entities": "Entit\u00e4ten", - "mode": "Modus" - }, - "description": "W\u00e4hle die einzubeziehenden Entit\u00e4ten aus. Im Zubeh\u00f6rmodus wird nur eine einzelne Entit\u00e4t eingeschlossen. Im Bridge-Include-Modus werden alle Entit\u00e4ten in der Dom\u00e4ne eingeschlossen, sofern nicht bestimmte Entit\u00e4ten ausgew\u00e4hlt sind. Im Bridge-Exclude-Modus werden alle Entit\u00e4ten in der Dom\u00e4ne eingeschlossen, au\u00dfer den ausgeschlossenen Entit\u00e4ten. F\u00fcr eine optimale Leistung wird f\u00fcr jeden TV-Media-Player, jede aktivit\u00e4tsbasierte Fernbedienung, jedes Schloss und jede Kamera ein separates HomeKit-Zubeh\u00f6r erstellt.", - "title": "W\u00e4hle die Entit\u00e4ten aus, die aufgenommen werden sollen" - }, "init": { "data": { "domains": "Einzubeziehende Domains", - "include_domains": "Einzubeziehende Domains", "include_exclude_mode": "Inklusionsmodus", "mode": "HomeKit-Modus" }, diff --git a/homeassistant/components/homekit/translations/el.json b/homeassistant/components/homekit/translations/el.json index 632d9aecd76..370617bd5fd 100644 --- a/homeassistant/components/homekit/translations/el.json +++ b/homeassistant/components/homekit/translations/el.json @@ -55,18 +55,9 @@ "description": "\u038c\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \"{domains}\" \u03b8\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd, \u03b5\u03ba\u03c4\u03cc\u03c2 \u03b5\u03ac\u03bd \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bf\u03cd\u03bd \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2.", "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd" }, - "include_exclude": { - "data": { - "entities": "\u039f\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2", - "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1" - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd. \u03a3\u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b1\u03be\u03b5\u03c3\u03bf\u03c5\u03ac\u03c1, \u03c0\u03b5\u03c1\u03b9\u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bc\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1. \u03a3\u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 include bridge, \u03b8\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd \u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c4\u03bf\u03c5 \u03c4\u03bf\u03bc\u03ad\u03b1, \u03b5\u03ba\u03c4\u03cc\u03c2 \u03b5\u03ac\u03bd \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bf\u03cd\u03bd \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2. \u03a3\u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b1\u03c0\u03bf\u03ba\u03bb\u03b5\u03b9\u03c3\u03bc\u03bf\u03cd \u03b3\u03ad\u03c6\u03c5\u03c1\u03b1\u03c2, \u03b8\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd \u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c4\u03bf\u03c5 \u03c4\u03bf\u03bc\u03ad\u03b1 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03c0\u03bf\u03ba\u03bb\u03b5\u03b9\u03c3\u03c4\u03b5\u03af. \u0393\u03b9\u03b1 \u03ba\u03b1\u03bb\u03cd\u03c4\u03b5\u03c1\u03b7 \u03b1\u03c0\u03cc\u03b4\u03bf\u03c3\u03b7, \u03b8\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03ad\u03bd\u03b1 \u03be\u03b5\u03c7\u03c9\u03c1\u03b9\u03c3\u03c4\u03cc \u03b1\u03be\u03b5\u03c3\u03bf\u03c5\u03ac\u03c1 HomeKit \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd tv, \u03c4\u03b7\u03bb\u03b5\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae\u03c1\u03b9\u03bf \u03bc\u03b5 \u03b2\u03ac\u03c3\u03b7 \u03c4\u03b7 \u03b4\u03c1\u03b1\u03c3\u03c4\u03b7\u03c1\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1, \u03ba\u03bb\u03b5\u03b9\u03b4\u03b1\u03c1\u03b9\u03ac \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1.", - "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd" - }, "init": { "data": { "domains": "\u03a4\u03bf\u03bc\u03b5\u03af\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd", - "include_domains": "\u03a4\u03bf\u03bc\u03b5\u03af\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd", "include_exclude_mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03af\u03bb\u03b7\u03c8\u03b7\u03c2", "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 HomeKit" }, diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json index 72c6ba9df36..14585b60f1e 100644 --- a/homeassistant/components/homekit/translations/en.json +++ b/homeassistant/components/homekit/translations/en.json @@ -55,18 +55,9 @@ "description": "All \u201c{domains}\u201d entities will be included unless specific entities are selected.", "title": "Select the entities to be included" }, - "include_exclude": { - "data": { - "entities": "Entities", - "mode": "Mode" - }, - "description": "Choose the entities to be included. In accessory mode, only a single entity is included. In bridge include mode, all entities in the domain will be included unless specific entities are selected. In bridge exclude mode, all entities in the domain will be included except for the excluded entities. For best performance, a separate HomeKit accessory will be created for each tv media player, activity based remote, lock, and camera.", - "title": "Select entities to be included" - }, "init": { "data": { "domains": "Domains to include", - "include_domains": "Domains to include", "include_exclude_mode": "Inclusion Mode", "mode": "HomeKit Mode" }, diff --git a/homeassistant/components/homekit/translations/es-419.json b/homeassistant/components/homekit/translations/es-419.json index 2b670f13c7e..459832cac80 100644 --- a/homeassistant/components/homekit/translations/es-419.json +++ b/homeassistant/components/homekit/translations/es-419.json @@ -34,9 +34,6 @@ "title": "Seleccione el c\u00f3dec de video de la c\u00e1mara." }, "init": { - "data": { - "include_domains": "Dominios para incluir" - }, "description": "Las entidades en los \"Dominios para incluir\" se vincular\u00e1n a HomeKit. Podr\u00e1 seleccionar qu\u00e9 entidades excluir de esta lista en la siguiente pantalla.", "title": "Seleccione dominios para puentear." }, diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index cc1925b8095..ef60610a8a1 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -40,17 +40,8 @@ "entities": "Entidades" } }, - "include_exclude": { - "data": { - "entities": "Entidades", - "mode": "Modo" - }, - "description": "Selecciona las entidades que deseas exponer. En el modo de accesorio, solo se expone una \u00fanica entidad. En el modo de inclusi\u00f3n de puente, todas las entidades del dominio se expondr\u00e1n a menos que se seleccionen entidades espec\u00edficas. En el modo de exclusi\u00f3n de puentes, todas las entidades del dominio se expondr\u00e1n excepto las entidades excluidas.", - "title": "Seleccionar entidades para exponer" - }, "init": { "data": { - "include_domains": "Dominios para incluir", "mode": "Modo" }, "description": "Las entidades de los \"Dominios que se van a incluir\" se establecer\u00e1n en HomeKit. Podr\u00e1 seleccionar qu\u00e9 entidades excluir de esta lista en la siguiente pantalla.", diff --git a/homeassistant/components/homekit/translations/et.json b/homeassistant/components/homekit/translations/et.json index d0ef2e79f03..671dede46e0 100644 --- a/homeassistant/components/homekit/translations/et.json +++ b/homeassistant/components/homekit/translations/et.json @@ -55,18 +55,9 @@ "description": "Kaasatakse k\u00f5ik olemid {domains}, v\u00e4lja arvatud juhul, kui valitud on konkreetsed olemid.", "title": "Vali kaasatavad olemid" }, - "include_exclude": { - "data": { - "entities": "Olemid", - "mode": "Re\u017eiim" - }, - "description": "Vali kaasatavad olemid. Tarvikute re\u017eiimis on kaasatav ainult \u00fcks olem. Silla re\u017eiimis kaasatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud juhul, kui valitud on kindlad olemid. Silla v\u00e4listamisre\u017eiimis kaasatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud v\u00e4listatud olemid. Parima kasutuskogemuse jaoks on eraldi HomeKit seadmed iga TV meediumim\u00e4ngija, luku, juhtpuldi ja kaamera jaoks.", - "title": "Vali kaasatavd olemid" - }, "init": { "data": { "domains": "Kaasa domeenid", - "include_domains": "Kaasatud domeenid", "include_exclude_mode": "Kaasamise re\u017eiim", "mode": "HomeKiti re\u017eiim" }, diff --git a/homeassistant/components/homekit/translations/fr.json b/homeassistant/components/homekit/translations/fr.json index 1977f7e6c18..e4850893bd3 100644 --- a/homeassistant/components/homekit/translations/fr.json +++ b/homeassistant/components/homekit/translations/fr.json @@ -55,18 +55,9 @@ "description": "Toutes les entit\u00e9s \"{domaines}\" seront incluses, sauf si des entit\u00e9s sp\u00e9cifiques sont s\u00e9lectionn\u00e9es.", "title": "S\u00e9lectionnez les entit\u00e9s \u00e0 inclure" }, - "include_exclude": { - "data": { - "entities": "Entit\u00e9s", - "mode": "Mode" - }, - "description": "Choisissez les entit\u00e9s \u00e0 inclure. En mode accessoire, une seule entit\u00e9 est incluse. En mode d'inclusion de pont, toutes les entit\u00e9s du domaine seront incluses \u00e0 moins que des entit\u00e9s sp\u00e9cifiques ne soient s\u00e9lectionn\u00e9es. En mode d'exclusion de pont, toutes les entit\u00e9s du domaine seront incluses \u00e0 l'exception des entit\u00e9s exclues. Pour de meilleures performances, un accessoire HomeKit distinct sera cr\u00e9\u00e9 pour chaque lecteur multim\u00e9dia TV et cam\u00e9ra.", - "title": "S\u00e9lectionnez les entit\u00e9s \u00e0 exposer" - }, "init": { "data": { "domains": "Domaines \u00e0 inclure", - "include_domains": "Domaines \u00e0 inclure", "include_exclude_mode": "Mode d'inclusion", "mode": "Mode HomeKit" }, diff --git a/homeassistant/components/homekit/translations/he.json b/homeassistant/components/homekit/translations/he.json index 22f3515b497..defce6a05a2 100644 --- a/homeassistant/components/homekit/translations/he.json +++ b/homeassistant/components/homekit/translations/he.json @@ -28,17 +28,10 @@ "entities": "\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea" } }, - "include_exclude": { - "data": { - "mode": "\u05de\u05e6\u05d1" - }, - "title": "\u05d1\u05d7\u05e8 \u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05e9\u05d9\u05d9\u05db\u05dc\u05dc\u05d5" - }, "init": { "data": { "domains": "\u05ea\u05d7\u05d5\u05de\u05d9\u05dd \u05e9\u05d9\u05e9 \u05dc\u05db\u05dc\u05d5\u05dc", - "include_domains": "\u05ea\u05d7\u05d5\u05de\u05d9\u05dd \u05e9\u05d9\u05e9 \u05dc\u05db\u05dc\u05d5\u05dc", - "mode": "\u05de\u05e6\u05d1" + "mode": "\u05de\u05e6\u05d1 HomeKit" } }, "yaml": { diff --git a/homeassistant/components/homekit/translations/hu.json b/homeassistant/components/homekit/translations/hu.json index 7c3a28abfd4..a1c64f6fac3 100644 --- a/homeassistant/components/homekit/translations/hu.json +++ b/homeassistant/components/homekit/translations/hu.json @@ -55,23 +55,14 @@ "description": "Az \u00f6sszes \"{domains}\" entit\u00e1s beker\u00fcl, kiv\u00e9ve, ha konkr\u00e9t entit\u00e1sok vannak kijel\u00f6lve.", "title": "V\u00e1lassza ki a felvenni k\u00edv\u00e1nt entit\u00e1sokat" }, - "include_exclude": { - "data": { - "entities": "Entit\u00e1sok", - "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\u00e1lassza ki a felvenni k\u00edv\u00e1nt entit\u00e1sokat" - }, "init": { "data": { "domains": "Felvenni k\u00edv\u00e1nt domainek", - "include_domains": "Felvenni k\u00edv\u00e1nt domainek", "include_exclude_mode": "Felv\u00e9tel m\u00f3dja", "mode": "HomeKit 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\u00e1lassza ki a felvenni k\u00edv\u00e1nt domaineket." + "title": "V\u00e1lassza ki a m\u00f3dot \u00e9s a tartom\u00e1nyokat." }, "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 5faedff893b..8293ad9d72c 100644 --- a/homeassistant/components/homekit/translations/id.json +++ b/homeassistant/components/homekit/translations/id.json @@ -55,18 +55,9 @@ "description": "Semua entitas \"{domains}\" akan disertakan kecuali entitas tertentu dipilih.", "title": "Pilih entitas yang akan disertakan" }, - "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, remote berbasis aktivitas, kunci, dan kamera.", - "title": "Pilih entitas untuk disertakan" - }, "init": { "data": { "domains": "Domain yang disertakan", - "include_domains": "Domain yang disertakan", "include_exclude_mode": "Mode Penyertaan", "mode": "Mode HomeKit" }, diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index 186bf989a4e..7acec1af5c3 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -55,18 +55,9 @@ "description": "Tutte le entit\u00e0 \"{domini}\" saranno incluse a eccezione delle entit\u00e0 escluse e delle entit\u00e0 categorizzate.", "title": "Seleziona le entit\u00e0 da includere" }, - "include_exclude": { - "data": { - "entities": "Entit\u00e0", - "mode": "Modalit\u00e0" - }, - "description": "Scegli le entit\u00e0 da includere. In modalit\u00e0 accessorio, \u00e8 inclusa una sola entit\u00e0. In modalit\u00e0 di inclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, a meno che non siano selezionate entit\u00e0 specifiche. In modalit\u00e0 di esclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, a eccezione delle entit\u00e0 escluse. Per prestazioni ottimali, sar\u00e0 creata una HomeKit separata accessoria per ogni lettore multimediale TV, telecomando basato sulle attivit\u00e0, serratura e videocamera.", - "title": "Seleziona le entit\u00e0 da includere" - }, "init": { "data": { "domains": "Domini da includere", - "include_domains": "Domini da includere", "include_exclude_mode": "Modalit\u00e0 di inclusione", "mode": "Modalit\u00e0 HomeKit" }, diff --git a/homeassistant/components/homekit/translations/ja.json b/homeassistant/components/homekit/translations/ja.json index 6d6b441484f..6bcad37ad61 100644 --- a/homeassistant/components/homekit/translations/ja.json +++ b/homeassistant/components/homekit/translations/ja.json @@ -55,18 +55,9 @@ "description": "\u7279\u5b9a\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u9078\u629e\u3055\u308c\u3066\u3044\u306a\u3044\u9650\u308a\u3001\u3059\u3079\u3066\u306e \u201c{domains}\u201d \u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u542b\u307e\u308c\u307e\u3059\u3002", "title": "\u542b\u3081\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9078\u629e" }, - "include_exclude": { - "data": { - "entities": "\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", - "mode": "\u30e2\u30fc\u30c9" - }, - "description": "\u542b\u307e\u308c\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea \u30e2\u30fc\u30c9\u3067\u306f\u30011\u3064\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306e\u307f\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u30d6\u30ea\u30c3\u30b8\u30a4\u30f3\u30af\u30eb\u30fc\u30c9\u30e2\u30fc\u30c9\u3067\u306f\u3001\u7279\u5b9a\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u9078\u629e\u3055\u308c\u3066\u3044\u306a\u3044\u9650\u308a\u3001\u30c9\u30e1\u30a4\u30f3\u5185\u306e\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u30d6\u30ea\u30c3\u30b8\u9664\u5916\u30e2\u30fc\u30c9\u3067\u306f\u3001\u9664\u5916\u3055\u308c\u305f\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9664\u3044\u3066\u3001\u30c9\u30e1\u30a4\u30f3\u5185\u306e\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u542b\u307e\u308c\u307e\u3059\u3002\u6700\u9ad8\u306e\u30d1\u30d5\u30a9\u30fc\u30de\u30f3\u30b9\u3092\u5b9f\u73fe\u3059\u308b\u305f\u3081\u306b\u3001\u30c6\u30ec\u30d3\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc\u3001\u30a2\u30af\u30c6\u30a3\u30d3\u30c6\u30a3\u30d9\u30fc\u30b9\u306e\u30ea\u30e2\u30b3\u30f3(remote)\u3001\u30ed\u30c3\u30af\u3001\u30ab\u30e1\u30e9\u306b\u5bfe\u3057\u3066\u500b\u5225\u306b\u3001HomeKit\u30a2\u30af\u30bb\u30b5\u30ea\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002", - "title": "\u542b\u3081\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u9078\u629e" - }, "init": { "data": { "domains": "\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3", - "include_domains": "\u542b\u3081\u308b\u30c9\u30e1\u30a4\u30f3", "include_exclude_mode": "\u30a4\u30f3\u30af\u30eb\u30fc\u30b8\u30e7\u30f3\u30e2\u30fc\u30c9", "mode": "\u30e2\u30fc\u30c9" }, diff --git a/homeassistant/components/homekit/translations/ka.json b/homeassistant/components/homekit/translations/ka.json index 97787f722fc..63b99f73416 100644 --- a/homeassistant/components/homekit/translations/ka.json +++ b/homeassistant/components/homekit/translations/ka.json @@ -1,14 +1,6 @@ { "options": { "step": { - "include_exclude": { - "data": { - "entities": "\u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d4\u10d1\u10d8", - "mode": "\u10e0\u10d4\u10df\u10db\u10d8" - }, - "description": "\u10e8\u10d4\u10d0\u10e0\u10e9\u10d8\u10d4\u10d7 \u10d2\u10d0\u10db\u10dd\u10e1\u10d0\u10d5\u10da\u10d4\u10dc\u10d8 \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d4\u10d1\u10d8. \u10d0\u10e5\u10e1\u10d4\u10e1\u10e3\u10d0\u10e0\u10d4\u10d1\u10d8\u10e1 \u10e0\u10d4\u10df\u10d8\u10db\u10e8\u10d8 \u10d2\u10d0\u10db\u10dd\u10d5\u10da\u10d4\u10dc\u10d0\u10e1 \u10d4\u10e5\u10d5\u10d4\u10db\u10d3\u10d4\u10d1\u10d0\u10e0\u10d4\u10d1\u10d0 \u10db\u10ee\u10dd\u10da\u10dd\u10d3 \u10d4\u10e0\u10d7\u10d8 \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d8. \u10d1\u10e0\u10d8\u10ef\u10d8\u10e1 \u10db\u10dd\u10ea\u10e3\u10da\u10dd\u10d1\u10d8\u10e1 \u10e0\u10d4\u10df\u10d8\u10db\u10e8\u10d8 \u10d7\u10e3 \u10e1\u10de\u10d4\u10ea\u10d8\u10e4\u10d8\u10d9\u10e3\u10e0\u10d8 \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d4\u10d1\u10d8 \u10d0\u10e0 \u10d0\u10e0\u10d8\u10e1 \u10e8\u10d4\u10e0\u10e9\u10d4\u10e3\u10da\u10d8, \u10db\u10d8\u10e1\u10d0\u10ec\u10d5\u10d3\u10dd\u10db\u10d8 \u10d8\u10e5\u10dc\u10d4\u10d1\u10d0 \u10d3\u10dd\u10db\u10d4\u10dc\u10d8\u10e1 \u10e7\u10d5\u10d4\u10da\u10d0 \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d8. \u10ee\u10d8\u10d3\u10d8\u10e1 \u10d2\u10d0\u10db\u10dd\u10e0\u10d8\u10ea\u10ee\u10d5\u10d8\u10e1 \u10e0\u10d4\u10df\u10d8\u10db\u10e8\u10d8, \u10d3\u10dd\u10db\u10d4\u10dc\u10d8\u10e1 \u10e7\u10d5\u10d4\u10da\u10d0 \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d8 \u10d8\u10e5\u10dc\u10d4\u10d1\u10d0 \u10dc\u10d8\u10e1\u10d0\u10ec\u10d5\u10d3\u10dd\u10db\u10d8 \u10d2\u10d0\u10db\u10dd\u10e0\u10d8\u10ea\u10ee\u10e3\u10da \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d4\u10d1\u10d8\u10e1 \u10d2\u10d0\u10e0\u10d3\u10d0.", - "title": "\u10d0\u10d8\u10e0\u10e9\u10d8\u10d4\u10d7 \u10d2\u10d0\u10db\u10dd\u10e1\u10d0\u10d5\u10da\u10d4\u10dc\u10d8 \u10dd\u10d1\u10d8\u10d4\u10e5\u10e2\u10d4\u10d1\u10d8" - }, "init": { "data": { "mode": "\u10e0\u10d4\u10df\u10d8\u10db\u10d8" diff --git a/homeassistant/components/homekit/translations/ko.json b/homeassistant/components/homekit/translations/ko.json index e93a55db971..cd8cbb81943 100644 --- a/homeassistant/components/homekit/translations/ko.json +++ b/homeassistant/components/homekit/translations/ko.json @@ -33,17 +33,8 @@ "description": "\ub124\uc774\ud2f0\ube0c H.264 \uc2a4\ud2b8\ub9bc\uc744 \uc9c0\uc6d0\ud558\ub294 \ubaa8\ub4e0 \uce74\uba54\ub77c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694. \uce74\uba54\ub77c\uac00 H.264 \uc2a4\ud2b8\ub9bc\uc744 \ucd9c\ub825\ud558\uc9c0 \uc54a\uc73c\uba74 \uc2dc\uc2a4\ud15c\uc740 \ube44\ub514\uc624\ub97c HomeKit\uc6a9 H.264 \ud3ec\ub9f7\uc73c\ub85c \ubcc0\ud658\uc2dc\ud0b5\ub2c8\ub2e4. \ud2b8\ub79c\uc2a4\ucf54\ub529 \ubcc0\ud658\uc5d0\ub294 \ub192\uc740 CPU \uc131\ub2a5\uc774 \ud544\uc694\ud558\uba70 Raspberry Pi\uc640 \uac19\uc740 \ub2e8\uc77c \ubcf4\ub4dc \ucef4\ud4e8\ud130\uc5d0\uc11c\ub294 \uc791\ub3d9\ud558\uc9c0 \uc54a\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "title": "\uce74\uba54\ub77c \ube44\ub514\uc624 \ucf54\ub371 \uc120\ud0dd\ud558\uae30" }, - "include_exclude": { - "data": { - "entities": "\uad6c\uc131\uc694\uc18c", - "mode": "\ubaa8\ub4dc" - }, - "description": "\ud3ec\ud568\ud560 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uc5d0\uc11c\ub294 \ub2e8\uc77c \uad6c\uc131\uc694\uc18c\ub9cc \ud3ec\ud568\ub429\ub2c8\ub2e4. \ube0c\ub9ac\uc9c0 \ud3ec\ud568 \ubaa8\ub4dc\uc5d0\uc11c\ub294 \ud2b9\uc815 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud558\uc9c0 \uc54a\ub294 \ud55c \ub3c4\uba54\uc778\uc758 \ubaa8\ub4e0 \uad6c\uc131\uc694\uc18c\uac00 \ud3ec\ud568\ub429\ub2c8\ub2e4. \ube0c\ub9ac\uc9c0 \uc81c\uc678 \ubaa8\ub4dc\uc5d0\uc11c\ub294 \uc81c\uc678\ub41c \uad6c\uc131\uc694\uc18c\ub97c \uc81c\uc678\ud55c \ub3c4\uba54\uc778\uc758 \ub098\uba38\uc9c0 \uad6c\uc131\uc694\uc18c\uac00 \ud3ec\ud568\ub429\ub2c8\ub2e4. \ucd5c\uc0c1\uc758 \uc131\ub2a5\uc744 \uc704\ud574 \uac01 TV \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4\uc640 \ud65c\ub3d9 \uae30\ubc18 \ub9ac\ubaa8\ucf58, \uc7a0\uae08\uae30\uae30, \uce74\uba54\ub77c\ub294 \ubcc4\ub3c4\uc758 HomeKit \uc561\uc138\uc11c\ub9ac\ub85c \uc0dd\uc131\ub429\ub2c8\ub2e4.", - "title": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778 \uc120\ud0dd\ud558\uae30" - }, "init": { "data": { - "include_domains": "\ud3ec\ud568\ud560 \ub3c4\uba54\uc778", "mode": "\ubaa8\ub4dc" }, "description": "\ube0c\ub9ac\uc9c0 \ub610\ub294 \ub2e8\uc77c \uc561\uc138\uc11c\ub9ac\ub97c \ub178\ucd9c\ud558\uc5ec HomeKit\ub97c \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uc5d0\uc11c\ub294 \ub2e8\uc77c \uad6c\uc131\uc694\uc18c\ub9cc \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. TV \uae30\uae30 \ud074\ub798\uc2a4\uac00 \uc788\ub294 \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4\uac00 \uc81c\ub300\ub85c \uc791\ub3d9\ud558\ub824\uba74 \uc561\uc138\uc11c\ub9ac \ubaa8\ub4dc\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. \"\ud3ec\ud568\ud560 \ub3c4\uba54\uc778\"\uc758 \uad6c\uc131\uc694\uc18c\uac00 HomeKit\uc5d0 \ud3ec\ud568\ub418\uba70, \ub2e4\uc74c \ud654\uba74\uc5d0\uc11c \uc774 \ubaa9\ub85d\uc5d0 \ud3ec\ud568\ud558\uac70\ub098 \uc81c\uc678\ud560 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", diff --git a/homeassistant/components/homekit/translations/lb.json b/homeassistant/components/homekit/translations/lb.json index 6261d4b5ed4..409d860940d 100644 --- a/homeassistant/components/homekit/translations/lb.json +++ b/homeassistant/components/homekit/translations/lb.json @@ -33,17 +33,8 @@ "description": "Iwwerpr\u00e9if all Kamera op nativ H.264 \u00cbnnerst\u00ebtzung. Falls d'Kamera keng H.264 Stream Ausgab huet, transkod\u00e9iert de System de Video op H.264 fir Homekit. Transkod\u00e9iere ben\u00e9idegt eng performant CPU an w\u00e4ert net anst\u00e4nneg op Single Board Computer funktion\u00e9ieren.", "title": "Kamera Video Codec auswielen." }, - "include_exclude": { - "data": { - "entities": "Entit\u00e9iten", - "mode": "Modus" - }, - "description": "Entit\u00e9iten auswielen d\u00e9i expos\u00e9iert solle ginn. Am Acessoire Modus g\u00ebtt n\u00ebmmen eng eenzeg Entit\u00e9it expos\u00e9iert. Am Bridge include Modus, ginn all Entit\u00e9iten vum Domaine expos\u00e9iert ausser spezifesch ausgewielten Entit\u00e9iten. Am Bride Exclude Modus, ginn all Entit\u00e9ite vum Domaine expos\u00e9iert ausser d\u00e9i ausgeschlossen Entit\u00e9iten.", - "title": "Entit\u00e9iten auswielen d\u00e9i expos\u00e9iert solle ginn" - }, "init": { "data": { - "include_domains": "Domaine d\u00e9i solle ageschloss ginn", "mode": "Modus" }, "description": "HomeKit kann entweder als Bridge oder Single Accessoire konfigur\u00e9iert ginn. Am Acessoire Modus ka n\u00ebmmen eng eenzeg Entit\u00e9it benotzt ginn. D\u00ebse Modus ass n\u00e9ideg fir de korretke Betrib vu Medie Spiller vun der TV Klasse. Entit\u00e9ite an der \"Domaine fir z'expos\u00e9ieren\" ginn mat HomeKit expos\u00e9iert. Du kanns am n\u00e4chste Schr\u00ebtt auswielen w\u00e9ieng Entit\u00e9ite aus oder ageschloss solle ginn.", diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json index 2e16ffde8ef..7f8ab1e2ff8 100644 --- a/homeassistant/components/homekit/translations/nl.json +++ b/homeassistant/components/homekit/translations/nl.json @@ -55,18 +55,9 @@ "description": "Alle \" {domains} \"-entiteiten zullen worden opgenomen, tenzij specifieke entiteiten zijn geselecteerd.", "title": "Selecteer de entiteiten die moeten worden opgenomen" }, - "include_exclude": { - "data": { - "entities": "Entiteiten", - "mode": "Mode" - }, - "description": "Kies de entiteiten die u wilt opnemen. In de accessoiremodus is slechts een enkele entiteit inbegrepen. In de bridge-include-modus worden alle entiteiten in het domein opgenomen, tenzij specifieke entiteiten zijn geselecteerd. In de bridge-uitsluitingsmodus worden alle entiteiten in het domein opgenomen, behalve de uitgesloten entiteiten. Voor de beste prestaties wordt voor elke media speler, activiteit gebaseerde afstandsbediening, slot en camera een afzonderlijke Homekit-accessoire gemaakt.", - "title": "Selecteer de entiteiten die u wilt opnemen" - }, "init": { "data": { "domains": "Domeinen om op te nemen", - "include_domains": "Domeinen om op te nemen", "include_exclude_mode": "Inclusiemodus", "mode": "HomeKit-modus" }, diff --git a/homeassistant/components/homekit/translations/no.json b/homeassistant/components/homekit/translations/no.json index 88e39d0835e..29f775853fd 100644 --- a/homeassistant/components/homekit/translations/no.json +++ b/homeassistant/components/homekit/translations/no.json @@ -55,18 +55,9 @@ "description": "Alle \" {domains} \"-enheter vil bli inkludert med mindre spesifikke enheter er valgt.", "title": "Velg enhetene som skal inkluderes" }, - "include_exclude": { - "data": { - "entities": "Entiteter", - "mode": "Modus" - }, - "description": "Velg entitetene som skal inkluderes. I tilbeh\u00f8rsmodus er bare en enkelt entitet inkludert. I bridge-inkluderingsmodus vil alle entiteter i domenet bli inkludert, med mindre spesifikke entiteter er valgt. I bridge-ekskluderingsmodus vil alle entiteter i domenet bli inkludert, bortsett fra de ekskluderte entitetene. For best ytelse opprettes et eget HomeKit-tilbeh\u00f8r for hver tv-mediaspiller, aktivitetsbasert fjernkontroll, l\u00e5s og kamera.", - "title": "Velg entiteter som skal inkluderes" - }, "init": { "data": { "domains": "Domener \u00e5 inkludere", - "include_domains": "Domener \u00e5 inkludere", "include_exclude_mode": "Inkluderingsmodus", "mode": "HomeKit-modus" }, diff --git a/homeassistant/components/homekit/translations/pl.json b/homeassistant/components/homekit/translations/pl.json index 745f07bdadb..73ed271d636 100644 --- a/homeassistant/components/homekit/translations/pl.json +++ b/homeassistant/components/homekit/translations/pl.json @@ -55,18 +55,9 @@ "description": "Wszystkie encje \"{domains}\" zostan\u0105 uwzgl\u0119dnione, chyba \u017ce zostan\u0105 wybrane konkretne encje.", "title": "Wybierz encje, kt\u00f3re maj\u0105 zosta\u0107 uwzgl\u0119dnione" }, - "include_exclude": { - "data": { - "entities": "Encje", - "mode": "Tryb" - }, - "description": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 uwzgl\u0119dnione. W trybie \"Akcesorium\" tylko jedna encja jest uwzgl\u0119dniona. W trybie \"Uwzgl\u0119dnij mostek\", wszystkie encje w danej domenie b\u0119d\u0105 uwzgl\u0119dnione, chyba \u017ce wybrane s\u0105 tylko konkretne encje. W trybie \"Wyklucz mostek\", wszystkie encje b\u0119d\u0105 uwzgl\u0119dnione, z wyj\u0105tkiem tych wybranych. Dla najlepszej wydajno\u015bci, zostanie utworzone oddzielne akcesorium HomeKit dla ka\u017cdego tv media playera, aktywno\u015bci pilota, zamka oraz kamery.", - "title": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 uwzgl\u0119dnione" - }, "init": { "data": { "domains": "Domeny do uwzgl\u0119dnienia", - "include_domains": "Domeny do uwzgl\u0119dnienia", "include_exclude_mode": "Tryb dodawania", "mode": "Tryb HomeKit" }, diff --git a/homeassistant/components/homekit/translations/pt-BR.json b/homeassistant/components/homekit/translations/pt-BR.json index ad8aab120f4..5fcc2748d0c 100644 --- a/homeassistant/components/homekit/translations/pt-BR.json +++ b/homeassistant/components/homekit/translations/pt-BR.json @@ -55,18 +55,9 @@ "description": "Todas as entidades \"{domains}\" ser\u00e3o inclu\u00eddas, a menos que entidades espec\u00edficas sejam selecionadas.", "title": "Selecione as entidades a serem inclu\u00eddas" }, - "include_exclude": { - "data": { - "entities": "Entidades", - "mode": "Modo" - }, - "description": "Escolha as entidades a serem inclu\u00eddas. No modo acess\u00f3rio, apenas uma \u00fanica entidade est\u00e1 inclu\u00edda. No modo de incluir ponte, todas as entidades do dom\u00ednio ser\u00e3o inclu\u00eddas a menos que entidades espec\u00edficas sejam selecionadas. No modo de exclus\u00e3o da ponte, todas as entidades do dom\u00ednio ser\u00e3o inclu\u00eddas, exceto para as entidades exclu\u00eddas. Para melhor desempenho, um acess\u00f3rio HomeKit separado ser\u00e1 criado para cada leitor de m\u00eddia de TV, controle remoto, bloqueio e c\u00e2mera baseados em atividades.", - "title": "Selecione as entidades a serem inclu\u00eddas" - }, "init": { "data": { - "domains": "Dom\u00ednios a serem inclu\u00eddos", - "include_domains": "Dom\u00ednios para incluir", + "domains": "Dom\u00ednios para incluir", "include_exclude_mode": "Modo de inclus\u00e3o", "mode": "Modo HomeKit" }, diff --git a/homeassistant/components/homekit/translations/pt.json b/homeassistant/components/homekit/translations/pt.json index 920beaa98df..3df84b70866 100644 --- a/homeassistant/components/homekit/translations/pt.json +++ b/homeassistant/components/homekit/translations/pt.json @@ -23,16 +23,8 @@ "cameras": { "title": "Selecione o codec de v\u00eddeo da c\u00e2mera." }, - "include_exclude": { - "data": { - "entities": "Entidades", - "mode": "Modo" - }, - "title": "Selecione as entidades a serem expostas" - }, "init": { "data": { - "include_domains": "Dom\u00ednios a incluir", "mode": "Modo" }, "title": "Selecione os dom\u00ednios a serem expostos." diff --git a/homeassistant/components/homekit/translations/ru.json b/homeassistant/components/homekit/translations/ru.json index 6de8ed572c0..81dd9afa302 100644 --- a/homeassistant/components/homekit/translations/ru.json +++ b/homeassistant/components/homekit/translations/ru.json @@ -55,18 +55,9 @@ "description": "\u0411\u0443\u0434\u0443\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u201c{domains}\u201c, \u0435\u0441\u043b\u0438 \u043d\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u044b \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b.", "title": "\u041e\u0431\u044a\u0435\u043a\u0442\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0432 \u0441\u043f\u0438\u0441\u043e\u043a" }, - "include_exclude": { - "data": { - "entities": "\u041e\u0431\u044a\u0435\u043a\u0442\u044b", - "mode": "\u0420\u0435\u0436\u0438\u043c" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043e\u0431\u044a\u0435\u043a\u0442. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u043c\u043e\u0441\u0442\u0430 \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u0435\u0441\u043b\u0438 \u043d\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u044b \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u043a\u0440\u043e\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u0445. \u0414\u043b\u044f \u0443\u043b\u0443\u0447\u0448\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440\u044b, \u043f\u0443\u043b\u044c\u0442\u044b \u0414\u0423 \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439, \u0437\u0430\u043c\u043a\u0438 \u0438 \u043a\u0430\u043c\u0435\u0440\u044b \u0431\u0443\u0434\u0443\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b \u043a\u0430\u043a \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0430\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430.", - "title": "\u0412\u044b\u0431\u043e\u0440 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit" - }, "init": { "data": { "domains": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b", - "include_domains": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b", "include_exclude_mode": "\u0420\u0435\u0436\u0438\u043c \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", "mode": "\u0420\u0435\u0436\u0438\u043c HomeKit" }, diff --git a/homeassistant/components/homekit/translations/sl.json b/homeassistant/components/homekit/translations/sl.json index dcfc2496011..333ec699253 100644 --- a/homeassistant/components/homekit/translations/sl.json +++ b/homeassistant/components/homekit/translations/sl.json @@ -32,17 +32,7 @@ }, "title": "Izberite video kodek kamere." }, - "include_exclude": { - "data": { - "entities": "Entitete", - "mode": "Na\u010din" - }, - "title": "Izberite entitete, ki jih \u017eelite izpostaviti" - }, "init": { - "data": { - "include_domains": "Domene za vklju\u010ditev" - }, "description": "Subjekti v domenah, ki jih \u017eelite vklju\u010diti, bodo prevezani na HomeKit. Na naslednjem zaslonu boste lahko izbrali subjekte, ki jih \u017eelite izklju\u010diti s tega seznama.", "title": "Izberite domene za premostitev." }, diff --git a/homeassistant/components/homekit/translations/sv.json b/homeassistant/components/homekit/translations/sv.json index 0bc23c456ff..be1e79453e8 100644 --- a/homeassistant/components/homekit/translations/sv.json +++ b/homeassistant/components/homekit/translations/sv.json @@ -19,9 +19,6 @@ "title": "V\u00e4lj kamerans videoavkodare." }, "init": { - "data": { - "include_domains": "Dom\u00e4ner att inkludera" - }, "title": "V\u00e4lj dom\u00e4ner som ska inkluderas." } } diff --git a/homeassistant/components/homekit/translations/tr.json b/homeassistant/components/homekit/translations/tr.json index 49d4a2cf1fa..52683302a34 100644 --- a/homeassistant/components/homekit/translations/tr.json +++ b/homeassistant/components/homekit/translations/tr.json @@ -55,18 +55,9 @@ "description": "Belirli varl\u0131klar se\u00e7ilmedi\u011fi s\u00fcrece t\u00fcm \u201c {domains} \u201d varl\u0131klar\u0131 dahil edilecektir.", "title": "Dahil edilecek varl\u0131klar\u0131 se\u00e7in" }, - "include_exclude": { - "data": { - "entities": "Varl\u0131klar", - "mode": "Mod" - }, - "description": "Dahil edilecek varl\u0131klar\u0131 se\u00e7in. Aksesuar modunda yaln\u0131zca tek bir varl\u0131k dahil edilir. K\u00f6pr\u00fc dahil modunda, belirli varl\u0131klar se\u00e7ilmedi\u011fi s\u00fcrece etki alan\u0131ndaki t\u00fcm varl\u0131klar dahil edilecektir. K\u00f6pr\u00fc hari\u00e7 tutma modunda, hari\u00e7 tutulan varl\u0131klar d\u0131\u015f\u0131nda etki alan\u0131ndaki t\u00fcm varl\u0131klar dahil edilecektir. En iyi performans i\u00e7in, her TV medya oynat\u0131c\u0131, aktivite tabanl\u0131 uzaktan kumanda, kilit ve kamera i\u00e7in ayr\u0131 bir HomeKit aksesuar\u0131 olu\u015fturulacakt\u0131r.", - "title": "Dahil edilecek varl\u0131klar\u0131 se\u00e7in" - }, "init": { "data": { "domains": "\u0130\u00e7erecek etki alanlar\u0131", - "include_domains": "\u0130\u00e7erecek etki alanlar\u0131", "include_exclude_mode": "Ekleme Modu", "mode": "HomeKit Modu" }, diff --git a/homeassistant/components/homekit/translations/uk.json b/homeassistant/components/homekit/translations/uk.json index 0210380ad38..52da83cca73 100644 --- a/homeassistant/components/homekit/translations/uk.json +++ b/homeassistant/components/homekit/translations/uk.json @@ -33,17 +33,8 @@ "description": "\u042f\u043a\u0449\u043e \u043a\u0430\u043c\u0435\u0440\u0430 \u043d\u0435 \u0432\u0438\u0432\u043e\u0434\u0438\u0442\u044c \u043f\u043e\u0442\u0456\u043a H.264, \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u043f\u0435\u0440\u0435\u043a\u043e\u0434\u043e\u0432\u0443\u0454 \u0432\u0456\u0434\u0435\u043e \u0432 H.264 \u0434\u043b\u044f HomeKit. \u0422\u0440\u0430\u043d\u0441\u043a\u043e\u0434\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u043c\u0430\u0433\u0430\u0454 \u0432\u0438\u0441\u043e\u043a\u043e\u043f\u0440\u043e\u0434\u0443\u043a\u0442\u0438\u0432\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0446\u0435\u0441\u043e\u0440\u0430 \u0456 \u043d\u0430\u0432\u0440\u044f\u0434 \u0447\u0438 \u0431\u0443\u0434\u0435 \u043f\u0440\u0430\u0446\u044e\u0432\u0430\u0442\u0438 \u043d\u0430 \u043e\u0434\u043d\u043e\u043f\u043b\u0430\u0442\u043d\u0438\u0445 \u043a\u043e\u043c\u043f'\u044e\u0442\u0435\u0440\u0430\u0445.", "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0432\u0456\u0434\u0435\u043e\u043a\u043e\u0434\u0435\u043a \u043a\u0430\u043c\u0435\u0440\u0438." }, - "include_exclude": { - "data": { - "entities": "\u0421\u0443\u0442\u043d\u043e\u0441\u0442\u0456", - "mode": "\u0420\u0435\u0436\u0438\u043c" - }, - "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0456 \u0432 HomeKit. \u0423 \u0440\u0435\u0436\u0438\u043c\u0456 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u0430 \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c. \u0423 \u0440\u0435\u0436\u0438\u043c\u0456 \u043c\u043e\u0441\u0442\u0430 \u0431\u0443\u0434\u0443\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u0456 \u0432\u0441\u0456 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456, \u0449\u043e \u043d\u0430\u043b\u0435\u0436\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u0443, \u044f\u043a\u0449\u043e \u043d\u0435 \u0432\u0438\u0431\u0440\u0430\u043d\u0456 \u043f\u0435\u0432\u043d\u0456 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456. \u0423 \u0440\u0435\u0436\u0438\u043c\u0456 \u0432\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0431\u0443\u0434\u0443\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u0456 \u0432\u0441\u0456 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456, \u0449\u043e \u043d\u0430\u043b\u0435\u0436\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u0443, \u043a\u0440\u0456\u043c \u0432\u0438\u0431\u0440\u0430\u043d\u0438\u0445.", - "title": "\u0412\u0438\u0431\u0456\u0440 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0435\u0439 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0456 \u0432 HomeKit" - }, "init": { "data": { - "include_domains": "\u0412\u0438\u0431\u0440\u0430\u0442\u0438 \u0434\u043e\u043c\u0435\u043d\u0438", "mode": "\u0420\u0435\u0436\u0438\u043c" }, "description": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0437 HomeKit \u043c\u043e\u0436\u0435 \u0431\u0443\u0442\u0438 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0430 \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u043c\u043e\u0441\u0442\u0430 \u0430\u0431\u043e \u044f\u043a \u043e\u043a\u0440\u0435\u043c\u0438\u0439 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440. \u0423 \u0440\u0435\u0436\u0438\u043c\u0456 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0442\u0456\u043b\u044c\u043a\u0438 \u043e\u0434\u0438\u043d \u043e\u0431'\u0454\u043a\u0442. \u041c\u0435\u0434\u0456\u0430\u043f\u043b\u0435\u0454\u0440\u0438, \u044f\u043a\u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u044e\u0442\u044c\u0441\u044f \u0432 Home Assistant \u0437 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u043c 'device_class: tv', \u0434\u043b\u044f \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0457 \u0440\u043e\u0431\u043e\u0442\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u043e\u0432\u0430\u043d\u0456 \u0432 Homekit \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0430. \u041e\u0431'\u0454\u043a\u0442\u0438, \u0449\u043e \u043d\u0430\u043b\u0435\u0436\u0430\u0442\u044c \u043e\u0431\u0440\u0430\u043d\u0438\u043c \u0434\u043e\u043c\u0435\u043d\u0430\u043c, \u0431\u0443\u0434\u0443\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u0456 \u0432 HomeKit. \u041d\u0430 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u043e\u043c\u0443 \u0435\u0442\u0430\u043f\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0412\u0438 \u0437\u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u0438\u0431\u0440\u0430\u0442\u0438, \u044f\u043a\u0456 \u043e\u0431'\u0454\u043a\u0442\u0438 \u0432\u0438\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0437 \u0446\u0438\u0445 \u0434\u043e\u043c\u0435\u043d\u0456\u0432.", diff --git a/homeassistant/components/homekit/translations/zh-Hans.json b/homeassistant/components/homekit/translations/zh-Hans.json index 3f6d005f045..852979fb12a 100644 --- a/homeassistant/components/homekit/translations/zh-Hans.json +++ b/homeassistant/components/homekit/translations/zh-Hans.json @@ -55,18 +55,9 @@ "description": "\u9664\u975e\u5df2\u9009\u62e9\u4e86\u6307\u5b9a\u7684\u5b9e\u4f53\uff0c\u5426\u5219\u6240\u6709\u201c{domains}\u201d\u7c7b\u578b\u7684\u5b9e\u4f53\u90fd\u5c06\u88ab\u5305\u542b\u3002", "title": "\u9009\u62e9\u8981\u5305\u542b\u7684\u5b9e\u4f53" }, - "include_exclude": { - "data": { - "entities": "\u5b9e\u4f53", - "mode": "\u6a21\u5f0f" - }, - "description": "\u9009\u62e9\u8981\u5f00\u653e\u7684\u5b9e\u4f53\u3002\n\u5728\u914d\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\n\u4e3a\u83b7\u5f97\u6700\u4f73\u4f53\u9a8c\uff0c\u5c06\u4f1a\u4e3a\u6bcf\u4e2a\u7535\u89c6\u5a92\u4f53\u64ad\u653e\u5668\u3001\u57fa\u4e8e\u6d3b\u52a8\u7684\u9065\u63a7\u5668\u3001\u9501\u548c\u6444\u50cf\u5934\u521b\u5efa\u5355\u72ec\u7684 HomeKit \u914d\u4ef6\u3002", - "title": "\u9009\u62e9\u8981\u5305\u542b\u7684\u5b9e\u4f53" - }, "init": { "data": { "domains": "\u8981\u5305\u542b\u7684\u57df", - "include_domains": "\u8981\u5305\u542b\u7684\u57df", "include_exclude_mode": "\u5305\u542b\u6a21\u5f0f", "mode": "HomeKit \u6a21\u5f0f" }, diff --git a/homeassistant/components/homekit/translations/zh-Hant.json b/homeassistant/components/homekit/translations/zh-Hant.json index b5aa54c72c4..3ad28a22f45 100644 --- a/homeassistant/components/homekit/translations/zh-Hant.json +++ b/homeassistant/components/homekit/translations/zh-Hant.json @@ -55,18 +55,9 @@ "description": "\u9664\u975e\u9078\u64c7\u7279\u5b9a\u5be6\u9ad4\u5916\uff0c\u5c07\u6703\u5305\u542b\u6240\u6709 \u201c{domains}\u201d \u5167\u5be6\u9ad4\u3002", "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4" }, - "include_exclude": { - "data": { - "entities": "\u5be6\u9ad4", - "mode": "\u6a21\u5f0f" - }, - "description": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4\u3002\u65bc\u914d\u4ef6\u6a21\u5f0f\u4e0b\u3001\u50c5\u6709\u55ae\u4e00\u5be6\u9ad4\u5c07\u6703\u5305\u542b\u3002\u65bc\u6a4b\u63a5\u5305\u542b\u6a21\u5f0f\u4e0b\u3001\u6240\u6709\u7db2\u57df\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u975e\u9078\u64c7\u7279\u5b9a\u7684\u5be6\u9ad4\u3002\u65bc\u6a4b\u63a5\u6392\u9664\u6a21\u5f0f\u4e2d\u3001\u6240\u6709\u7db2\u57df\u4e2d\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u4e86\u6392\u9664\u7684\u5be6\u9ad4\u3002\u70ba\u53d6\u5f97\u6700\u4f73\u6548\u80fd\u3001\u6bcf\u4e00\u500b\u96fb\u8996\u5a92\u9ad4\u64ad\u653e\u5668\u3001\u9060\u7aef\u9059\u63a7\u5668\u3001\u9580\u9396\u8207\u651d\u5f71\u6a5f\uff0c\u5c07\u65bc Homekit \u914d\u4ef6\u6a21\u5f0f\u9032\u884c\u3002", - "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4" - }, "init": { "data": { "domains": "\u5305\u542b\u7db2\u57df", - "include_domains": "\u5305\u542b\u7db2\u57df", "include_exclude_mode": "\u5305\u542b\u6a21\u5f0f", "mode": "HomeKit \u6a21\u5f0f" }, diff --git a/homeassistant/components/homekit_controller/translations/hu.json b/homeassistant/components/homekit_controller/translations/hu.json index 6fad9050a20..607c46ede6d 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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "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/homematicip_cloud/translations/fr.json b/homeassistant/components/homematicip_cloud/translations/fr.json index 01e1d1ed8c2..dd85878991f 100644 --- a/homeassistant/components/homematicip_cloud/translations/fr.json +++ b/homeassistant/components/homematicip_cloud/translations/fr.json @@ -6,7 +6,7 @@ "unknown": "Erreur inattendue" }, "error": { - "invalid_sgtin_or_pin": "Code SGTIN ou PIN invalide, veuillez r\u00e9essayer.", + "invalid_sgtin_or_pin": "Code SGTIN ou PIN non valide, veuillez r\u00e9essayer.", "press_the_button": "Veuillez appuyer sur le bouton bleu.", "register_failed": "\u00c9chec d'enregistrement. Veuillez r\u00e9essayer.", "timeout_button": "D\u00e9lai d'attente expir\u00e9, veuillez r\u00e9\u00e9ssayer." diff --git a/homeassistant/components/homematicip_cloud/translations/hu.json b/homeassistant/components/homematicip_cloud/translations/hu.json index 2915d442a37..975daa36126 100644 --- a/homeassistant/components/homematicip_cloud/translations/hu.json +++ b/homeassistant/components/homematicip_cloud/translations/hu.json @@ -15,7 +15,7 @@ "init": { "data": { "hapid": "Hozz\u00e1f\u00e9r\u00e9si pont azonos\u00edt\u00f3ja (SGTIN)", - "name": "N\u00e9v (opcion\u00e1lis, minden eszk\u00f6z n\u00e9vel\u0151tagjak\u00e9nt haszn\u00e1latos)", + "name": "Elnevez\u00e9s (opcion\u00e1lis, minden eszk\u00f6z n\u00e9vel\u0151tagjak\u00e9nt haszn\u00e1latos)", "pin": "PIN-k\u00f3d" }, "title": "V\u00e1lasszon HomematicIP hozz\u00e1f\u00e9r\u00e9si pontot" diff --git a/homeassistant/components/homematicip_cloud/translations/it.json b/homeassistant/components/homematicip_cloud/translations/it.json index 55c26532fb5..88a2dcd5fc8 100644 --- a/homeassistant/components/homematicip_cloud/translations/it.json +++ b/homeassistant/components/homematicip_cloud/translations/it.json @@ -6,7 +6,7 @@ "unknown": "Errore imprevisto" }, "error": { - "invalid_sgtin_or_pin": "SGTIN o Codice PIN non valido, si prega di riprovare.", + "invalid_sgtin_or_pin": "SGTIN o Codice PIN non valido, riprova.", "press_the_button": "Premi il pulsante blu.", "register_failed": "Registrazione non riuscita, riprova.", "timeout_button": "Timeout della pressione del pulsante blu, riprovare." diff --git a/homeassistant/components/honeywell/translations/ca.json b/homeassistant/components/honeywell/translations/ca.json index 34da1b89f10..0830d657173 100644 --- a/homeassistant/components/honeywell/translations/ca.json +++ b/homeassistant/components/honeywell/translations/ca.json @@ -9,8 +9,18 @@ "password": "Contrasenya", "username": "Nom d'usuari" }, - "description": "Introdueix les credencials utilitzades per iniciar sessi\u00f3 a mytotalconnectcomfort.com.", - "title": "Honeywell Total Connect Comfort (EUA)" + "description": "Introdueix les credencials utilitzades per iniciar sessi\u00f3 a mytotalconnectcomfort.com." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "Temperatura freda, mode fora", + "away_heat_temperature": "Temperatura calenta, mode fora" + }, + "description": "Opcions addicionals de configuraci\u00f3 de Honeywell. Les temperatures es configuren en Fahrenheit." } } } diff --git a/homeassistant/components/honeywell/translations/de.json b/homeassistant/components/honeywell/translations/de.json index a146d442eef..6cbceffce51 100644 --- a/homeassistant/components/honeywell/translations/de.json +++ b/homeassistant/components/honeywell/translations/de.json @@ -9,8 +9,18 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "Bitte gib die Anmeldedaten ein, mit denen du dich bei mytotalconnectcomfort.com anmeldest.", - "title": "Honeywell Total Connect Comfort (US)" + "description": "Bitte gib die Anmeldedaten ein, mit denen du dich bei mytotalconnectcomfort.com anmeldest." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "Abwesend K\u00fchlungstemperatur", + "away_heat_temperature": "Abwesend Heiztemperatur" + }, + "description": "Zus\u00e4tzliche Honeywell-Konfigurationsoptionen. Die Temperaturen werden in Fahrenheit eingestellt." } } } diff --git a/homeassistant/components/honeywell/translations/el.json b/homeassistant/components/honeywell/translations/el.json index 7ad0c5b0181..b3fd654ded8 100644 --- a/homeassistant/components/honeywell/translations/el.json +++ b/homeassistant/components/honeywell/translations/el.json @@ -9,8 +9,18 @@ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, - "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b1\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf mytotalconnectcomfort.com.", - "title": "Honeywell Total Connect Comfort (\u0397\u03a0\u0391)" + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b1\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf mytotalconnectcomfort.com." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "\u0398\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1 \u03c8\u03cd\u03be\u03b7\u03c2 \u03bc\u03b1\u03ba\u03c1\u03b9\u03ac", + "away_heat_temperature": "\u0398\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1 \u03b8\u03b5\u03c1\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b1\u03ba\u03c1\u03b9\u03ac" + }, + "description": "\u03a0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03b5\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 Honeywell. \u039f\u03b9 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b5\u03c2 \u03bf\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03b5 Fahrenheit." } } } diff --git a/homeassistant/components/honeywell/translations/en.json b/homeassistant/components/honeywell/translations/en.json index 168d3a5b93d..bf47a15be55 100644 --- a/homeassistant/components/honeywell/translations/en.json +++ b/homeassistant/components/honeywell/translations/en.json @@ -9,8 +9,7 @@ "password": "Password", "username": "Username" }, - "description": "Please enter the credentials used to log into mytotalconnectcomfort.com.", - "title": "Honeywell Total Connect Comfort (US)" + "description": "Please enter the credentials used to log into mytotalconnectcomfort.com." } } }, @@ -21,8 +20,7 @@ "away_cool_temperature": "Away cool temperature", "away_heat_temperature": "Away heat temperature" }, - "description": "Additional Honeywell config options. Temperatures are set in Fahrenheit.", - "title": "Honeywell Options" + "description": "Additional Honeywell config options. Temperatures are set in Fahrenheit." } } } diff --git a/homeassistant/components/honeywell/translations/es.json b/homeassistant/components/honeywell/translations/es.json index 9f6c562e888..bdae26b8794 100644 --- a/homeassistant/components/honeywell/translations/es.json +++ b/homeassistant/components/honeywell/translations/es.json @@ -9,8 +9,7 @@ "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)" + "description": "Por favor, introduzca las credenciales utilizadas para iniciar sesi\u00f3n en mytotalconnectcomfort.com." } } } diff --git a/homeassistant/components/honeywell/translations/et.json b/homeassistant/components/honeywell/translations/et.json index 264a1efeca5..6673959ad31 100644 --- a/homeassistant/components/honeywell/translations/et.json +++ b/homeassistant/components/honeywell/translations/et.json @@ -9,8 +9,18 @@ "password": "Salas\u00f5na", "username": "Kasutajanimi" }, - "description": "Sisesta saidile mytotalconnectcomfort.com sisenemiseks kasutatav mandaat.", - "title": "Honeywell Total Connect Comfort (USA)" + "description": "Sisesta saidile mytotalconnectcomfort.com sisenemiseks kasutatav mandaat." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "Eemaloleku jahutuse temperatuur", + "away_heat_temperature": "Eemaloleku k\u00fctte temperatuur" + }, + "description": "T\u00e4iendavad Honeywelli seadistusv\u00f5imalused. Temperatuurid on Fahrenheiti kraadides." } } } diff --git a/homeassistant/components/honeywell/translations/fr.json b/homeassistant/components/honeywell/translations/fr.json index ac11cf20576..0043e8990f1 100644 --- a/homeassistant/components/honeywell/translations/fr.json +++ b/homeassistant/components/honeywell/translations/fr.json @@ -9,8 +9,18 @@ "password": "Mot de passe", "username": "Nom d'utilisateur" }, - "description": "Veuillez saisir les informations d'identification utilis\u00e9es pour vous connecter \u00e0 mytotalconnectcomfort.com.", - "title": "Honeywell Total Connect Comfort (\u00c9tats-Unis)" + "description": "Veuillez saisir les informations d'identification utilis\u00e9es pour vous connecter \u00e0 mytotalconnectcomfort.com." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "Temp\u00e9rature de refroidissement en absence", + "away_heat_temperature": "Temp\u00e9rature de chauffage en absence" + }, + "description": "Options de configuration Honeywell suppl\u00e9mentaires. Les temp\u00e9ratures sont exprim\u00e9es en degr\u00e9s Fahrenheit." } } } diff --git a/homeassistant/components/honeywell/translations/hu.json b/homeassistant/components/honeywell/translations/hu.json index 5583dc22f2e..14ba9167ea5 100644 --- a/homeassistant/components/honeywell/translations/hu.json +++ b/homeassistant/components/honeywell/translations/hu.json @@ -9,8 +9,18 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rj\u00fck, adja meg a mytotalconnectcomfort.com webhelyre val\u00f3 bejelentkez\u00e9shez haszn\u00e1lt hiteles\u00edt\u0151 adatokat.", - "title": "Honeywell Total Connect Comfort (USA)" + "description": "K\u00e9rj\u00fck, adja meg a mytotalconnectcomfort.com webhelyre val\u00f3 bejelentkez\u00e9shez haszn\u00e1lt hiteles\u00edt\u0151 adatokat." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "H\u0171t\u00e9si h\u0151m\u00e9r\u00e9s\u00e9klet t\u00e1voll\u00e9ti m\u00f3dban", + "away_heat_temperature": "F\u0171t\u00e9si h\u0151m\u00e9r\u00e9s\u00e9klet t\u00e1voll\u00e9ti m\u00f3dban" + }, + "description": "Tov\u00e1bbi Honeywell konfigur\u00e1ci\u00f3s lehet\u0151s\u00e9gek. A h\u0151m\u00e9rs\u00e9kletek Fahrenheitben vannak be\u00e1ll\u00edtva." } } } diff --git a/homeassistant/components/honeywell/translations/id.json b/homeassistant/components/honeywell/translations/id.json index 62151da1481..5ed95a27a76 100644 --- a/homeassistant/components/honeywell/translations/id.json +++ b/homeassistant/components/honeywell/translations/id.json @@ -9,8 +9,18 @@ "password": "Kata Sandi", "username": "Nama Pengguna" }, - "description": "Masukkan kredensial yang digunakan untuk masuk ke mytotalconnectcomfort.com.", - "title": "Honeywell Total Connect Comfort (AS)" + "description": "Masukkan kredensial yang digunakan untuk masuk ke mytotalconnectcomfort.com." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "Suhu dingin ketika jauh", + "away_heat_temperature": "Suhu panas ketika jauh" + }, + "description": "Opsi konfigurasi Honeywell tambahan. Suhu diatur dalam Fahrenheit." } } } diff --git a/homeassistant/components/honeywell/translations/it.json b/homeassistant/components/honeywell/translations/it.json index 52c828ddcde..87669762fa2 100644 --- a/homeassistant/components/honeywell/translations/it.json +++ b/homeassistant/components/honeywell/translations/it.json @@ -9,8 +9,18 @@ "password": "Password", "username": "Nome utente" }, - "description": "Inserisci le credenziali utilizzate per accedere a mytotalconnectcomfort.com.", - "title": "Honeywell Total Connect Comfort (USA)" + "description": "Inserisci le credenziali utilizzate per accedere a mytotalconnectcomfort.com." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "Fuori casa temperatura fresca", + "away_heat_temperature": "Fuori casa temperatura calda" + }, + "description": "Ulteriori opzioni di configurazione di Honeywell. Le temperature sono impostate in Fahrenheit." } } } diff --git a/homeassistant/components/honeywell/translations/ja.json b/homeassistant/components/honeywell/translations/ja.json index 1d3e19750c6..1af30341d33 100644 --- a/homeassistant/components/honeywell/translations/ja.json +++ b/homeassistant/components/honeywell/translations/ja.json @@ -9,8 +9,18 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "description": "mytotalconnectcomfort.com \u306b\u30ed\u30b0\u30a4\u30f3\u3059\u308b\u305f\u3081\u306b\u4f7f\u7528\u3059\u308b\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "Honeywell Total Connect Comfort (US)" + "description": "mytotalconnectcomfort.com \u306b\u30ed\u30b0\u30a4\u30f3\u3059\u308b\u305f\u3081\u306b\u4f7f\u7528\u3059\u308b\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "\u30a2\u30a6\u30a7\u30a4\u30af\u30fc\u30eb(Away cool)\u6e29\u5ea6", + "away_heat_temperature": "\u30a2\u30a6\u30a7\u30a4\u30d2\u30fc\u30c8(Away heat)\u6e29\u5ea6" + }, + "description": "Honeywell\u306e\u8ffd\u52a0\u8a2d\u5b9a\u30aa\u30d7\u30b7\u30e7\u30f3\u3002\u6e29\u5ea6\u306f\u83ef\u6c0f(\u00b0F)\u3067\u8a2d\u5b9a\u3055\u308c\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/honeywell/translations/nl.json b/homeassistant/components/honeywell/translations/nl.json index 0abd80fa088..9d82ef53b21 100644 --- a/homeassistant/components/honeywell/translations/nl.json +++ b/homeassistant/components/honeywell/translations/nl.json @@ -9,8 +9,18 @@ "password": "Wachtwoord", "username": "Gebruikersnaam" }, - "description": "Voer de inloggegevens in die zijn gebruikt om in te loggen op mytotalconnectcomfort.com.", - "title": "Honeywell Total Connect Comfort (US)" + "description": "Voer de inloggegevens in die zijn gebruikt om in te loggen op mytotalconnectcomfort.com." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "Afwezig koeltemperatuur", + "away_heat_temperature": "Afwezig warmte temperatuur" + }, + "description": "Extra Honeywell configuratie opties. Temperaturen zijn ingesteld in Fahrenheit." } } } diff --git a/homeassistant/components/honeywell/translations/no.json b/homeassistant/components/honeywell/translations/no.json index 97d31d34961..e35e8a2b278 100644 --- a/homeassistant/components/honeywell/translations/no.json +++ b/homeassistant/components/honeywell/translations/no.json @@ -9,8 +9,18 @@ "password": "Passord", "username": "Brukernavn" }, - "description": "Vennligst skriv inn legitimasjonen som brukes for \u00e5 logge deg p\u00e5 mytotalconnectcomfort.com.", - "title": "Honeywell Total Connect Comfort (USA)" + "description": "Vennligst skriv inn legitimasjonen som brukes for \u00e5 logge deg p\u00e5 mytotalconnectcomfort.com." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "Borte kj\u00f8lig temperatur", + "away_heat_temperature": "Borte varmetemperatur" + }, + "description": "Ytterligere Honeywell-konfigurasjonsalternativer. Temperaturene er satt i Fahrenheit." } } } diff --git a/homeassistant/components/honeywell/translations/pl.json b/homeassistant/components/honeywell/translations/pl.json index c109565e33a..e484f88cb49 100644 --- a/homeassistant/components/honeywell/translations/pl.json +++ b/homeassistant/components/honeywell/translations/pl.json @@ -9,8 +9,18 @@ "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" }, - "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce u\u017cywane na mytotalconnectcomfort.com", - "title": "Honeywell Total Connect Comfort (USA)" + "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce u\u017cywane na mytotalconnectcomfort.com" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "Temperatura ch\u0142odzenia w trybie \"poza domem\"", + "away_heat_temperature": "Temperatura grzania w trybie \"poza domem\"" + }, + "description": "Dodatkowe opcje konfiguracji Honeywell. Temperatury s\u0105 ustawiane w stopniach Fahrenheita." } } } diff --git a/homeassistant/components/honeywell/translations/pt-BR.json b/homeassistant/components/honeywell/translations/pt-BR.json index f16a6c71637..4cae96c5b1b 100644 --- a/homeassistant/components/honeywell/translations/pt-BR.json +++ b/homeassistant/components/honeywell/translations/pt-BR.json @@ -9,8 +9,18 @@ "password": "Senha", "username": "Usu\u00e1rio" }, - "description": "Insira as credenciais usadas para fazer login em mytotalconnectcomfort.com.", - "title": "Honeywell Total Connect Comfort (EUA)" + "description": "Insira as credenciais usadas para fazer login em mytotalconnectcomfort.com." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "Temperatura de frio ausente", + "away_heat_temperature": "Temperatura de calor ausente" + }, + "description": "Op\u00e7\u00f5es adicionais de configura\u00e7\u00e3o Honeywell. As temperaturas s\u00e3o definidas em Fahrenheit." } } } diff --git a/homeassistant/components/honeywell/translations/ru.json b/homeassistant/components/honeywell/translations/ru.json index 1d775e6c2c7..b370df892f6 100644 --- a/homeassistant/components/honeywell/translations/ru.json +++ b/homeassistant/components/honeywell/translations/ru.json @@ -9,8 +9,18 @@ "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 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u043d\u0430 mytotalconnectcomfort.com.", - "title": "Honeywell Total Connect Comfort (\u0421\u0428\u0410)" + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u043d\u0430 mytotalconnectcomfort.com." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \"\u041d\u0435 \u0434\u043e\u043c\u0430\"", + "away_heat_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043d\u0430\u0433\u0440\u0435\u0432\u0430 \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \"\u041d\u0435 \u0434\u043e\u043c\u0430\"" + }, + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Honeywell. \u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0432 \u0433\u0440\u0430\u0434\u0443\u0441\u0430\u0445 \u0424\u0430\u0440\u0435\u043d\u0433\u0435\u0439\u0442\u0430." } } } diff --git a/homeassistant/components/honeywell/translations/tr.json b/homeassistant/components/honeywell/translations/tr.json index e6eb57aca1f..d0c02e5029f 100644 --- a/homeassistant/components/honeywell/translations/tr.json +++ b/homeassistant/components/honeywell/translations/tr.json @@ -9,8 +9,18 @@ "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "L\u00fctfen mytotalconnectcomfort.com'da oturum a\u00e7mak i\u00e7in kullan\u0131lan kimlik bilgilerini girin.", - "title": "Honeywell Toplam Ba\u011flant\u0131 Konforu (ABD)" + "description": "L\u00fctfen mytotalconnectcomfort.com'da oturum a\u00e7mak i\u00e7in kullan\u0131lan kimlik bilgilerini girin." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "Uzak so\u011futma derecesi", + "away_heat_temperature": "Uzak \u0131s\u0131tma derecesi" + }, + "description": "Ek Honeywell yap\u0131land\u0131rma se\u00e7enekleri. S\u0131cakl\u0131klar Fahrenheit cinsinden ayarlan\u0131r." } } } diff --git a/homeassistant/components/honeywell/translations/zh-Hant.json b/homeassistant/components/honeywell/translations/zh-Hant.json index 906506d41a5..c6a657b13be 100644 --- a/homeassistant/components/honeywell/translations/zh-Hant.json +++ b/homeassistant/components/honeywell/translations/zh-Hant.json @@ -9,8 +9,18 @@ "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8acb\u8f38\u5165\u767b\u5165 mytotalconnectcomfort.com \u4e4b\u6191\u8b49\u3002", - "title": "Honeywell Total Connect Comfort\uff08\u7f8e\u570b\uff09" + "description": "\u8acb\u8f38\u5165\u767b\u5165 mytotalconnectcomfort.com \u4e4b\u6191\u8b49\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "\u96e2\u5bb6\u5236\u51b7\u6eab\u5ea6", + "away_heat_temperature": "\u96e2\u5bb6\u52a0\u71b1\u6eab\u5ea6" + }, + "description": "\u9644\u52a0 Honeywell \u8a2d\u5b9a\u9078\u9805\u3002\u6eab\u5ea6\u4ee5\u83ef\u6c0f\u9032\u884c\u8a2d\u5b9a\u3002" } } } diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json index 36d08438fca..b3cc71d5a9d 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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "not_huawei_lte": "Nem Huawei LTE eszk\u00f6z" }, "error": { diff --git a/homeassistant/components/hue/translations/ca.json b/homeassistant/components/hue/translations/ca.json index 92fb30b15d9..3b192e80b00 100644 --- a/homeassistant/components/hue/translations/ca.json +++ b/homeassistant/components/hue/translations/ca.json @@ -6,6 +6,7 @@ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "cannot_connect": "Ha fallat la connexi\u00f3", "discover_timeout": "No s'han pogut descobrir enlla\u00e7os Hue", + "invalid_host": "Amfitri\u00f3 inv\u00e0lid", "no_bridges": "No s'han trobat enlla\u00e7os Philips Hue", "not_hue_bridge": "No \u00e9s un enlla\u00e7 Hue", "unknown": "Error inesperat" diff --git a/homeassistant/components/hue/translations/de.json b/homeassistant/components/hue/translations/de.json index 2f8485aae0c..c28014031cc 100644 --- a/homeassistant/components/hue/translations/de.json +++ b/homeassistant/components/hue/translations/de.json @@ -6,6 +6,7 @@ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "cannot_connect": "Verbindung fehlgeschlagen", "discover_timeout": "Es k\u00f6nnen keine Hue Bridges erkannt werden", + "invalid_host": "Ung\u00fcltiger Host", "no_bridges": "Keine Philips Hue Bridges erkannt", "not_hue_bridge": "Keine Philips Hue Bridge entdeckt", "unknown": "Unerwarteter Fehler" diff --git a/homeassistant/components/hue/translations/el.json b/homeassistant/components/hue/translations/el.json index 99bdc934929..7d1c2cabd44 100644 --- a/homeassistant/components/hue/translations/el.json +++ b/homeassistant/components/hue/translations/el.json @@ -6,6 +6,7 @@ "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "discover_timeout": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03bf\u03cd \u03b3\u03b5\u03c6\u03c5\u03c1\u03ce\u03bd Hue", + "invalid_host": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", "no_bridges": "\u0394\u03b5\u03bd \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03b3\u03ad\u03c6\u03c5\u03c1\u03b5\u03c2 Philips Hue", "not_hue_bridge": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b3\u03ad\u03c6\u03c5\u03c1\u03b1 Hue", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" diff --git a/homeassistant/components/hue/translations/en.json b/homeassistant/components/hue/translations/en.json index 7757aca9373..f617be431b9 100644 --- a/homeassistant/components/hue/translations/en.json +++ b/homeassistant/components/hue/translations/en.json @@ -6,6 +6,7 @@ "already_in_progress": "Configuration flow is already in progress", "cannot_connect": "Failed to connect", "discover_timeout": "Unable to discover Hue bridges", + "invalid_host": "Invalid host", "no_bridges": "No Philips Hue bridges discovered", "not_hue_bridge": "Not a Hue bridge", "unknown": "Unexpected error" diff --git a/homeassistant/components/hue/translations/et.json b/homeassistant/components/hue/translations/et.json index 748c5726dfe..df1288d415d 100644 --- a/homeassistant/components/hue/translations/et.json +++ b/homeassistant/components/hue/translations/et.json @@ -6,6 +6,7 @@ "already_in_progress": "Seadistamine on juba k\u00e4imas", "cannot_connect": "\u00dchendus nurjus", "discover_timeout": "Ei leia Philips Hue sildu", + "invalid_host": "Kehtetu host", "no_bridges": "Philips Hue sildu ei avastatud", "not_hue_bridge": "See pole Hue sild", "unknown": "Ilmnes tundmatu viga" diff --git a/homeassistant/components/hue/translations/fr.json b/homeassistant/components/hue/translations/fr.json index ec9d104aa65..cd07cff9fea 100644 --- a/homeassistant/components/hue/translations/fr.json +++ b/homeassistant/components/hue/translations/fr.json @@ -6,6 +6,7 @@ "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "cannot_connect": "\u00c9chec de connexion", "discover_timeout": "D\u00e9tection de ponts Philips Hue impossible", + "invalid_host": "H\u00f4te non valide", "no_bridges": "Aucun pont Philips Hue n'a \u00e9t\u00e9 d\u00e9couvert", "not_hue_bridge": "Pas de pont Hue", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/hue/translations/hu.json b/homeassistant/components/hue/translations/hu.json index 02b8652b253..dd5c13c5f59 100644 --- a/homeassistant/components/hue/translations/hu.json +++ b/homeassistant/components/hue/translations/hu.json @@ -3,9 +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\u00e1l\u00e1s m\u00e1r folyamatban van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "discover_timeout": "Nem tal\u00e1lhat\u00f3 a Hue bridge", + "invalid_host": "\u00c9rv\u00e9nytelen c\u00edm", "no_bridges": "Nem tal\u00e1lhat\u00f3 Philips Hue bridget", "not_hue_bridge": "Nem egy Hue Bridge", "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt" diff --git a/homeassistant/components/hue/translations/id.json b/homeassistant/components/hue/translations/id.json index d7178f6d135..d450adfb3c4 100644 --- a/homeassistant/components/hue/translations/id.json +++ b/homeassistant/components/hue/translations/id.json @@ -6,6 +6,7 @@ "already_in_progress": "Alur konfigurasi sedang berlangsung", "cannot_connect": "Gagal terhubung", "discover_timeout": "Tidak dapat menemukan bridge Hue", + "invalid_host": "Host tidak valid", "no_bridges": "Bridge Philips Hue tidak ditemukan", "not_hue_bridge": "Bukan bridge Hue", "unknown": "Kesalahan yang tidak diharapkan" diff --git a/homeassistant/components/hue/translations/it.json b/homeassistant/components/hue/translations/it.json index fdafef01f5b..0c1dfce4c1a 100644 --- a/homeassistant/components/hue/translations/it.json +++ b/homeassistant/components/hue/translations/it.json @@ -6,6 +6,7 @@ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "cannot_connect": "Impossibile connettersi", "discover_timeout": "Impossibile trovare i bridge Hue", + "invalid_host": "Host non valido", "no_bridges": "Nessun bridge di Philips Hue trovato", "not_hue_bridge": "Non \u00e8 un bridge Hue", "unknown": "Errore imprevisto" diff --git a/homeassistant/components/hue/translations/nl.json b/homeassistant/components/hue/translations/nl.json index a7d29534218..56bac6b89d8 100644 --- a/homeassistant/components/hue/translations/nl.json +++ b/homeassistant/components/hue/translations/nl.json @@ -6,6 +6,7 @@ "already_in_progress": "De configuratiestroom is al aan de gang", "cannot_connect": "Kan geen verbinding maken", "discover_timeout": "Hue bridges kunnen niet worden gevonden", + "invalid_host": "Ongeldige host", "no_bridges": "Geen Philips Hue bridges ontdekt", "not_hue_bridge": "Dit is geen Hue bridge", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/hue/translations/no.json b/homeassistant/components/hue/translations/no.json index 08c19a9cc51..9c18003aca0 100644 --- a/homeassistant/components/hue/translations/no.json +++ b/homeassistant/components/hue/translations/no.json @@ -6,6 +6,7 @@ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "cannot_connect": "Tilkobling mislyktes", "discover_timeout": "Kunne ikke oppdage Hue Bridger", + "invalid_host": "Ugyldig vert", "no_bridges": "Ingen Philips Hue Bridger oppdaget", "not_hue_bridge": "Ikke en Hue bro", "unknown": "Uventet feil" diff --git a/homeassistant/components/hue/translations/pl.json b/homeassistant/components/hue/translations/pl.json index 2bdcbc7e053..abfb6fa2a05 100644 --- a/homeassistant/components/hue/translations/pl.json +++ b/homeassistant/components/hue/translations/pl.json @@ -6,6 +6,7 @@ "already_in_progress": "Konfiguracja jest ju\u017c w toku", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "discover_timeout": "Nie mo\u017cna wykry\u0107 \u017cadnych mostk\u00f3w Hue", + "invalid_host": "Nieprawid\u0142owy host", "no_bridges": "Nie wykryto mostk\u00f3w Hue", "not_hue_bridge": "To nie jest mostek Hue", "unknown": "Nieoczekiwany b\u0142\u0105d" diff --git a/homeassistant/components/hue/translations/pt-BR.json b/homeassistant/components/hue/translations/pt-BR.json index 05dec678318..cc97e5e055e 100644 --- a/homeassistant/components/hue/translations/pt-BR.json +++ b/homeassistant/components/hue/translations/pt-BR.json @@ -6,6 +6,7 @@ "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "cannot_connect": "Falha ao conectar", "discover_timeout": "Incapaz de descobrir pontes Hue", + "invalid_host": "Host inv\u00e1lido", "no_bridges": "N\u00e3o h\u00e1 pontes Philips Hue descobertas", "not_hue_bridge": "N\u00e3o \u00e9 uma ponte Hue", "unknown": "Erro inesperado" diff --git a/homeassistant/components/hue/translations/ru.json b/homeassistant/components/hue/translations/ru.json index 8bd5ac77d52..6e470b74f1f 100644 --- a/homeassistant/components/hue/translations/ru.json +++ b/homeassistant/components/hue/translations/ru.json @@ -6,6 +6,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.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0445\u043e\u0441\u0442.", "no_bridges": "\u0428\u043b\u044e\u0437\u044b Philips Hue \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", "not_hue_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c Hue.", "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/hue/translations/tr.json b/homeassistant/components/hue/translations/tr.json index 8d23af54c66..32ab9bb1887 100644 --- a/homeassistant/components/hue/translations/tr.json +++ b/homeassistant/components/hue/translations/tr.json @@ -6,6 +6,7 @@ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "cannot_connect": "Ba\u011flanma hatas\u0131", "discover_timeout": "Hue k\u00f6pr\u00fcleri bulunam\u0131yor", + "invalid_host": "Ge\u00e7ersiz sunucu", "no_bridges": "Philips Hue k\u00f6pr\u00fcs\u00fc bulunamad\u0131", "not_hue_bridge": "Hue k\u00f6pr\u00fcs\u00fc de\u011fil", "unknown": "Beklenmeyen hata" diff --git a/homeassistant/components/hue/translations/zh-Hant.json b/homeassistant/components/hue/translations/zh-Hant.json index 6c37c699340..1816b459a85 100644 --- a/homeassistant/components/hue/translations/zh-Hant.json +++ b/homeassistant/components/hue/translations/zh-Hant.json @@ -6,6 +6,7 @@ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557", "discover_timeout": "\u7121\u6cd5\u641c\u5c0b\u5230 Hue Bridge", + "invalid_host": "\u4e3b\u6a5f\u7aef\u7121\u6548", "no_bridges": "\u672a\u767c\u73fe\u5230 Philips Hue Bridge", "not_hue_bridge": "\u975e Hue Bridge \u88dd\u7f6e", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" diff --git a/homeassistant/components/humidifier/translations/hu.json b/homeassistant/components/humidifier/translations/hu.json index d8e2ce86a9a..7979334429d 100644 --- a/homeassistant/components/humidifier/translations/hu.json +++ b/homeassistant/components/humidifier/translations/hu.json @@ -13,6 +13,7 @@ "is_on": "{entity_name} be van kapcsolva" }, "trigger_type": { + "changed_states": "{entity_name} be- vagy kikapcsolt", "target_humidity_changed": "{name} k\u00edv\u00e1nt p\u00e1ratartalom megv\u00e1ltozott", "toggled": "{entity_name} \u00e1tkapcsolt", "turned_off": "{entity_name} ki lett kapcsolva", diff --git a/homeassistant/components/hyperion/translations/hu.json b/homeassistant/components/hyperion/translations/hu.json index 750f2380624..aaffa705279 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\u00e1l\u00e1s m\u00e1r folyamatban van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "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", @@ -27,7 +27,7 @@ "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 **Mehet** 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": { diff --git a/homeassistant/components/ifttt/translations/ja.json b/homeassistant/components/ifttt/translations/ja.json index a3a63f532cf..87844be51d4 100644 --- a/homeassistant/components/ifttt/translations/ja.json +++ b/homeassistant/components/ifttt/translations/ja.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { - "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[IFTTT Webhook applet]({applet_url})\u306e\"Make a web request\"\u30a2\u30af\u30b7\u30e7\u30f3\u3092\u4f7f\u7528\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u4ee5\u4e0b\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n- Content Type: application/json\n\n\u53d7\u4fe1\u30c7\u30fc\u30bf\u3092\u51e6\u7406\u3059\u308b\u305f\u3081\u306b\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[IFTTT Webhook applet]({applet_url})\u306e\"Make a web request\"\u30a2\u30af\u30b7\u30e7\u30f3\u3092\u4f7f\u7528\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u4ee5\u4e0b\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n- Content Type(\u30b3\u30f3\u30c6\u30f3\u30c4\u306e\u7a2e\u985e): application/json\n\n\u53d7\u4fe1\u30c7\u30fc\u30bf\u3092\u51e6\u7406\u3059\u308b\u305f\u3081\u306b\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { "user": { diff --git a/homeassistant/components/insteon/translations/ca.json b/homeassistant/components/insteon/translations/ca.json index 59c711c3dae..9805d03c685 100644 --- a/homeassistant/components/insteon/translations/ca.json +++ b/homeassistant/components/insteon/translations/ca.json @@ -2,13 +2,18 @@ "config": { "abort": { "cannot_connect": "Ha fallat la connexi\u00f3", + "not_insteon_device": "El dispositiu descobert no \u00e9s un dispositiu Insteon", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "select_single": "Selecciona una opci\u00f3." }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "Vols configurar {name}?" + }, "hubv1": { "data": { "host": "Adre\u00e7a IP", diff --git a/homeassistant/components/insteon/translations/de.json b/homeassistant/components/insteon/translations/de.json index 0866f53481f..164fbccceed 100644 --- a/homeassistant/components/insteon/translations/de.json +++ b/homeassistant/components/insteon/translations/de.json @@ -2,13 +2,18 @@ "config": { "abort": { "cannot_connect": "Verbindung fehlgeschlagen", + "not_insteon_device": "Erkanntes Ger\u00e4t ist kein Insteon-Ger\u00e4t", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "select_single": "W\u00e4hle eine Option aus." }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "M\u00f6chtest du {name} einrichten?" + }, "hubv1": { "data": { "host": "IP-Adresse", diff --git a/homeassistant/components/insteon/translations/el.json b/homeassistant/components/insteon/translations/el.json index f26c9cba54c..4a4c402ddc0 100644 --- a/homeassistant/components/insteon/translations/el.json +++ b/homeassistant/components/insteon/translations/el.json @@ -2,13 +2,18 @@ "config": { "abort": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "not_insteon_device": "\u0397 \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03c5\u03c6\u03b8\u03b5\u03af\u03c3\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Insteon", "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "select_single": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03af\u03b1 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae." }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, "hubv1": { "data": { "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", diff --git a/homeassistant/components/insteon/translations/en.json b/homeassistant/components/insteon/translations/en.json index 4c4a439b938..f9e64dcf3cf 100644 --- a/homeassistant/components/insteon/translations/en.json +++ b/homeassistant/components/insteon/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "cannot_connect": "Failed to connect", + "not_insteon_device": "Discovered device not an Insteon device", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { diff --git a/homeassistant/components/insteon/translations/et.json b/homeassistant/components/insteon/translations/et.json index 69368300c7e..eff2fd9b9b2 100644 --- a/homeassistant/components/insteon/translations/et.json +++ b/homeassistant/components/insteon/translations/et.json @@ -2,13 +2,18 @@ "config": { "abort": { "cannot_connect": "\u00dchendamine nurjus", + "not_insteon_device": "Avastatud seade ei ole Insteoni seade", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." }, "error": { "cannot_connect": "\u00dchendamine nurjus", "select_single": "Vali \u00fcks suvand." }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "Kas seadistada {name} ?" + }, "hubv1": { "data": { "host": "IP aagress", diff --git a/homeassistant/components/insteon/translations/fr.json b/homeassistant/components/insteon/translations/fr.json index 2feb3c5d9c9..2bb0e4d2e33 100644 --- a/homeassistant/components/insteon/translations/fr.json +++ b/homeassistant/components/insteon/translations/fr.json @@ -2,13 +2,18 @@ "config": { "abort": { "cannot_connect": "\u00c9chec de connexion", + "not_insteon_device": "L'appareil d\u00e9couvert n'est pas un appareil Insteon", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { "cannot_connect": "\u00c9chec de connexion", "select_single": "S\u00e9lectionnez une option." }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, "hubv1": { "data": { "host": "Adresse IP", diff --git a/homeassistant/components/insteon/translations/he.json b/homeassistant/components/insteon/translations/he.json index 220bbcbb632..68f13ca5a01 100644 --- a/homeassistant/components/insteon/translations/he.json +++ b/homeassistant/components/insteon/translations/he.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, + "flow_title": "{name}", "step": { "hubv1": { "data": { diff --git a/homeassistant/components/insteon/translations/hu.json b/homeassistant/components/insteon/translations/hu.json index f34307a67a4..560c1fc118e 100644 --- a/homeassistant/components/insteon/translations/hu.json +++ b/homeassistant/components/insteon/translations/hu.json @@ -2,13 +2,18 @@ "config": { "abort": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "not_insteon_device": "A felfedezett eszk\u00f6z nem egy Insteon", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "select_single": "V\u00e1lasszon egy lehet\u0151s\u00e9get" }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, "hubv1": { "data": { "host": "IP c\u00edm", diff --git a/homeassistant/components/insteon/translations/id.json b/homeassistant/components/insteon/translations/id.json index efae5284150..aae7e9f71ac 100644 --- a/homeassistant/components/insteon/translations/id.json +++ b/homeassistant/components/insteon/translations/id.json @@ -2,13 +2,18 @@ "config": { "abort": { "cannot_connect": "Gagal terhubung", + "not_insteon_device": "Perangkat yang ditemukan bukan perangkat Insteon", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, "error": { "cannot_connect": "Gagal terhubung", "select_single": "Pilih satu opsi." }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "Ingin menyiapkan {name}?" + }, "hubv1": { "data": { "host": "Alamat IP", diff --git a/homeassistant/components/insteon/translations/it.json b/homeassistant/components/insteon/translations/it.json index 83a4e87d6f6..b47a06c9d7a 100644 --- a/homeassistant/components/insteon/translations/it.json +++ b/homeassistant/components/insteon/translations/it.json @@ -2,13 +2,18 @@ "config": { "abort": { "cannot_connect": "Impossibile connettersi", + "not_insteon_device": "Dispositivo rilevato non \u00e8 un dispositivo Insteon", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "error": { "cannot_connect": "Impossibile connettersi", "select_single": "Seleziona un'opzione." }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "Vuoi configurare {name}?" + }, "hubv1": { "data": { "host": "Indirizzo IP", @@ -46,7 +51,7 @@ "options": { "error": { "cannot_connect": "Impossibile connettersi", - "input_error": "Voci non valide, si prega di controllare i valori.", + "input_error": "Voci non valide, controlla i valori.", "select_single": "Seleziona un'opzione." }, "step": { diff --git a/homeassistant/components/insteon/translations/ja.json b/homeassistant/components/insteon/translations/ja.json index d4ddf083f1b..0b8d93518bd 100644 --- a/homeassistant/components/insteon/translations/ja.json +++ b/homeassistant/components/insteon/translations/ja.json @@ -8,7 +8,11 @@ "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "select_single": "\u30aa\u30d7\u30b7\u30e7\u30f3\u30921\u3064\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "{name} \u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f" + }, "hubv1": { "data": { "host": "IP\u30a2\u30c9\u30ec\u30b9", @@ -61,7 +65,7 @@ }, "add_x10": { "data": { - "housecode": "\u30cf\u30a6\u30b9\u30b3\u30fc\u30c9(a\uff5ep)", + "housecode": "\u30cf\u30a6\u30b9\u30b3\u30fc\u30c9(a - p)", "platform": "\u30d7\u30e9\u30c3\u30c8\u30db\u30fc\u30e0", "steps": "\u8abf\u5149\u30b9\u30c6\u30c3\u30d7(\u30e9\u30a4\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u307f\u3001\u30c7\u30d5\u30a9\u30eb\u30c822)", "unitcode": "\u30e6\u30cb\u30c3\u30c8\u30b3\u30fc\u30c9(1\u301c16)" diff --git a/homeassistant/components/insteon/translations/nl.json b/homeassistant/components/insteon/translations/nl.json index 63a0bb059d5..207e370e245 100644 --- a/homeassistant/components/insteon/translations/nl.json +++ b/homeassistant/components/insteon/translations/nl.json @@ -2,13 +2,18 @@ "config": { "abort": { "cannot_connect": "Kon niet verbinden", + "not_insteon_device": "Ontdekt apparaat is geen Insteon apparaat", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { "cannot_connect": "Kon niet verbinden", "select_single": "Selecteer een optie." }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "Wilt u {name} instellen?" + }, "hubv1": { "data": { "host": "IP-adres", diff --git a/homeassistant/components/insteon/translations/no.json b/homeassistant/components/insteon/translations/no.json index b5d6a2c3105..3716312b00f 100644 --- a/homeassistant/components/insteon/translations/no.json +++ b/homeassistant/components/insteon/translations/no.json @@ -2,13 +2,18 @@ "config": { "abort": { "cannot_connect": "Tilkobling mislyktes", + "not_insteon_device": "Oppdaget enhet, ikke en Insteon-enhet", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { "cannot_connect": "Tilkobling mislyktes", "select_single": "Velg ett alternativ." }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "Vil du konfigurere {name}?" + }, "hubv1": { "data": { "host": "IP adresse", diff --git a/homeassistant/components/insteon/translations/pl.json b/homeassistant/components/insteon/translations/pl.json index c4a58e0e09a..6d3b6c4391d 100644 --- a/homeassistant/components/insteon/translations/pl.json +++ b/homeassistant/components/insteon/translations/pl.json @@ -2,13 +2,18 @@ "config": { "abort": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "not_insteon_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem Insteon", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "select_single": "Wybierz jedn\u0105 z opcji" }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, "hubv1": { "data": { "host": "Adres IP", diff --git a/homeassistant/components/insteon/translations/pt-BR.json b/homeassistant/components/insteon/translations/pt-BR.json index 409ac9d6283..b5487d9260a 100644 --- a/homeassistant/components/insteon/translations/pt-BR.json +++ b/homeassistant/components/insteon/translations/pt-BR.json @@ -2,13 +2,18 @@ "config": { "abort": { "cannot_connect": "Falha ao conectar", + "not_insteon_device": "O dispositivo descoberto n\u00e3o \u00e9 um dispositivo Insteon", "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { "cannot_connect": "Falha ao conectar", "select_single": "Selecione uma op\u00e7\u00e3o." }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "Deseja configurar {name}?" + }, "hubv1": { "data": { "host": "Endere\u00e7o IP", diff --git a/homeassistant/components/insteon/translations/ru.json b/homeassistant/components/insteon/translations/ru.json index 69b5354fe87..3151fa35252 100644 --- a/homeassistant/components/insteon/translations/ru.json +++ b/homeassistant/components/insteon/translations/ru.json @@ -2,13 +2,18 @@ "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.", + "not_insteon_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 Insteon.", "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.", "select_single": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u043f\u0446\u0438\u044e." }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, "hubv1": { "data": { "host": "IP-\u0430\u0434\u0440\u0435\u0441", diff --git a/homeassistant/components/insteon/translations/tr.json b/homeassistant/components/insteon/translations/tr.json index 3cb23de55f7..2f74d6a98ae 100644 --- a/homeassistant/components/insteon/translations/tr.json +++ b/homeassistant/components/insteon/translations/tr.json @@ -2,13 +2,18 @@ "config": { "abort": { "cannot_connect": "Ba\u011flanma hatas\u0131", + "not_insteon_device": "Ke\u015ffedilen cihaz bir Insteon cihaz\u0131 de\u011fil", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "select_single": "Bir se\u00e7enek belirleyin." }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, "hubv1": { "data": { "host": "IP Adresi", diff --git a/homeassistant/components/insteon/translations/zh-Hant.json b/homeassistant/components/insteon/translations/zh-Hant.json index 2176ea67b94..c55a1ea0a5c 100644 --- a/homeassistant/components/insteon/translations/zh-Hant.json +++ b/homeassistant/components/insteon/translations/zh-Hant.json @@ -2,13 +2,18 @@ "config": { "abort": { "cannot_connect": "\u9023\u7dda\u5931\u6557", + "not_insteon_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Insteon \u88dd\u7f6e", "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "select_single": "\u9078\u64c7\u9078\u9805\u3002" }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, "hubv1": { "data": { "host": "IP \u4f4d\u5740", diff --git a/homeassistant/components/vallox/translations/is.json b/homeassistant/components/integration/translations/bg.json similarity index 73% rename from homeassistant/components/vallox/translations/is.json rename to homeassistant/components/integration/translations/bg.json index 6878b2ecf11..35cfa0ad1d7 100644 --- a/homeassistant/components/vallox/translations/is.json +++ b/homeassistant/components/integration/translations/bg.json @@ -3,7 +3,7 @@ "step": { "user": { "data": { - "name": "Nafn" + "name": "\u0418\u043c\u0435" } } } diff --git a/homeassistant/components/integration/translations/ca.json b/homeassistant/components/integration/translations/ca.json new file mode 100644 index 00000000000..bbe5e6e31b4 --- /dev/null +++ b/homeassistant/components/integration/translations/ca.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "M\u00e8tode d'integraci\u00f3", + "name": "Nom", + "round": "Precisi\u00f3", + "source": "Sensor d'entrada", + "unit_prefix": "Prefix m\u00e8tric", + "unit_time": "Unitat de temps" + }, + "data_description": { + "round": "Controla el nombre de d\u00edgits decimals a la sortida.", + "unit_prefix": "La sortida s'escalar\u00e0 segons el prefix m\u00e8tric seleccionat.", + "unit_time": "La sortida s'escalar\u00e0 segons la unitat de temps seleccionada." + }, + "description": "Crea un sensor que calcula la suma de Riemann que estima la integral d'un sensor.", + "title": "Afegeix sensor d'integraci\u00f3 de suma Riemann" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "Precisi\u00f3" + }, + "data_description": { + "round": "Controla el nombre de d\u00edgits decimals a la sortida." + } + }, + "options": { + "data": { + "round": "Precisi\u00f3" + }, + "description": "La precisi\u00f3 controla el nombre de d\u00edgits decimals a la sortida." + } + } + }, + "title": "Integraci\u00f3 - Sensor integral de suma de Riemann" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/cs.json b/homeassistant/components/integration/translations/cs.json new file mode 100644 index 00000000000..3e7fdfca729 --- /dev/null +++ b/homeassistant/components/integration/translations/cs.json @@ -0,0 +1,41 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "Metoda integrace", + "name": "Jm\u00e9no", + "round": "P\u0159esnost", + "source": "Vstupn\u00ed senzor", + "unit_time": "\u010casov\u00e1 jednotka" + }, + "data_description": { + "round": "Ur\u010duje po\u010det desetinn\u00fdch m\u00edst ve v\u00fdstupu.", + "unit_prefix": "V\u00fdstup bude \u0161k\u00e1lov\u00e1n podle vybran\u00e9ho metrick\u00e9ho prefixu.", + "unit_time": "V\u00fdstup bude \u0161k\u00e1lov\u00e1n podle zvolen\u00e9 \u010dasov\u00e9 jednotky." + }, + "description": "Vytvo\u0159\u00ed senzor, kter\u00fd vypo\u010d\u00edt\u00e1 Riemann\u016fv sou\u010det pro odhad integr\u00e1lu senzoru.", + "title": "P\u0159idat Riemann\u016fv sou\u010dtov\u00fd integr\u00e1ln\u00ed senzor" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "P\u0159esnost" + }, + "data_description": { + "round": "Ur\u010duje po\u010det desetinn\u00fdch m\u00edst ve v\u00fdstupu." + } + }, + "options": { + "data": { + "round": "P\u0159esnost" + }, + "description": "P\u0159esnost ur\u010duje po\u010det desetinn\u00fdch m\u00edst ve v\u00fdstupu." + } + } + }, + "title": "Integrace - Riemann\u016fv sou\u010dtov\u00fd integr\u00e1ln\u00ed senzor" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/de.json b/homeassistant/components/integration/translations/de.json new file mode 100644 index 00000000000..3013fa9d039 --- /dev/null +++ b/homeassistant/components/integration/translations/de.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "Integrationsmethode", + "name": "Name", + "round": "Genauigkeit", + "source": "Eingangssensor", + "unit_prefix": "Metrisches Pr\u00e4fix", + "unit_time": "Zeiteinheit" + }, + "data_description": { + "round": "Steuert die Anzahl der Dezimalstellen in der Ausgabe.", + "unit_prefix": "Die Ausgabe wird entsprechend dem gew\u00e4hlten metrischen Pr\u00e4fix skaliert.", + "unit_time": "Die Ausgabe wird entsprechend der gew\u00e4hlten Zeiteinheit skaliert." + }, + "description": "Erstelle einen Sensor, der eine Riemann-Summe berechnet, um das Integral eines Sensors zu sch\u00e4tzen.", + "title": "Riemann-Summenintegralsensor hinzuf\u00fcgen" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "Genauigkeit" + }, + "data_description": { + "round": "Steuert die Anzahl der Dezimalstellen in der Ausgabe." + } + }, + "options": { + "data": { + "round": "Genauigkeit" + }, + "description": "Die Genauigkeit steuert die Anzahl der Dezimalstellen in der Ausgabe." + } + } + }, + "title": "Integration - Riemann-Summenintegralsensor" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/el.json b/homeassistant/components/integration/translations/el.json new file mode 100644 index 00000000000..e366137fce1 --- /dev/null +++ b/homeassistant/components/integration/translations/el.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "\u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "round": "\u0391\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1", + "source": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5", + "unit_prefix": "\u039c\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1", + "unit_time": "\u03a7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, + "data_description": { + "round": "\u0395\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf.", + "unit_prefix": "\u0397 \u03ad\u03be\u03bf\u03b4\u03bf\u03c2 \u03b8\u03b1 \u03ba\u03bb\u03b9\u03bc\u03b1\u03ba\u03ce\u03bd\u03b5\u03c4\u03b1\u03b9 \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1.", + "unit_time": "\u0397 \u03ad\u03be\u03bf\u03b4\u03bf\u03c2 \u03b8\u03b1 \u03ba\u03bb\u03b9\u03bc\u03b1\u03ba\u03c9\u03b8\u03b5\u03af \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5." + }, + "description": "\u0397 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf.\n\u03a4\u03bf \u03ac\u03b8\u03c1\u03bf\u03b9\u03c3\u03bc\u03b1 \u03b8\u03b1 \u03ba\u03bb\u03b9\u03bc\u03b1\u03ba\u03ce\u03bd\u03b5\u03c4\u03b1\u03b9 \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03bf \u03c7\u03c1\u03cc\u03bd\u03bf \u03bf\u03bb\u03bf\u03ba\u03bb\u03ae\u03c1\u03c9\u03c3\u03b7\u03c2.", + "title": "\u039d\u03ad\u03bf\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "\u0391\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1" + }, + "data_description": { + "round": "\u0395\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf." + } + }, + "options": { + "data": { + "round": "\u0391\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1" + }, + "description": "\u0397 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf." + } + } + }, + "title": "\u039f\u03bb\u03bf\u03ba\u03bb\u03ae\u03c1\u03c9\u03c3\u03b7 - A\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b1\u03b8\u03c1\u03bf\u03af\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2 Riemann" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/en.json b/homeassistant/components/integration/translations/en.json index 1ee047b447f..3174eab3f69 100644 --- a/homeassistant/components/integration/translations/en.json +++ b/homeassistant/components/integration/translations/en.json @@ -29,6 +29,12 @@ "data_description": { "round": "Controls the number of decimal digits in the output." } + }, + "options": { + "data": { + "round": "Precision" + }, + "description": "Precision controls the number of decimal digits in the output." } } }, diff --git a/homeassistant/components/integration/translations/et.json b/homeassistant/components/integration/translations/et.json new file mode 100644 index 00000000000..31901bbb6f6 --- /dev/null +++ b/homeassistant/components/integration/translations/et.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "Integreerimismeetod", + "name": "Nimi", + "round": "T\u00e4psus", + "source": "Sisendandur", + "unit_prefix": "M\u00f5\u00f5diku eesliide", + "unit_time": "Aja\u00fchik" + }, + "data_description": { + "round": "K\u00fcmnendkohtade arv v\u00e4ljundis.", + "unit_prefix": "V\u00e4ljund skaleeritakse vastavalt valitud m\u00f5\u00f5diku prefiksile.", + "unit_time": "V\u00e4ljund skaleeritakse vastavalt valitud aja\u00fchikule." + }, + "description": "Looge andur, mis arvutab anduri integraali hindamiseks Riemanni summa.", + "title": "Lisa Riemanni summa integraalandur" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "T\u00e4psus" + }, + "data_description": { + "round": "M\u00e4\u00e4rab k\u00fcmnendkohtade arvu v\u00e4ljundis." + } + }, + "options": { + "data": { + "round": "T\u00e4psus" + }, + "description": "T\u00e4psus reguleerib k\u00fcmnendkohtade arvu v\u00e4ljundis." + } + } + }, + "title": "Sidumine - Riemanni integraalsumma andur" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/fr.json b/homeassistant/components/integration/translations/fr.json new file mode 100644 index 00000000000..33b5ab86e7f --- /dev/null +++ b/homeassistant/components/integration/translations/fr.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "M\u00e9thode de calcul de l'int\u00e9grale", + "name": "Nom", + "round": "Pr\u00e9cision", + "source": "Capteur d'entr\u00e9e", + "unit_prefix": "Pr\u00e9fixe m\u00e9trique", + "unit_time": "Unit\u00e9 de temps" + }, + "data_description": { + "round": "Contr\u00f4le le nombre de chiffres d\u00e9cimaux dans la sortie.", + "unit_prefix": "La sortie sera mise \u00e0 l'\u00e9chelle en fonction du pr\u00e9fixe m\u00e9trique s\u00e9lectionn\u00e9.", + "unit_time": "La sortie sera mise \u00e0 l'\u00e9chelle en fonction de l'unit\u00e9 de temps s\u00e9lectionn\u00e9e." + }, + "description": "Cr\u00e9ez un capteur qui calcule une somme de Riemann afin d'estimer l'int\u00e9grale d'un autre capteur.", + "title": "Ajouter un capteur d'int\u00e9grale de Riemann" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "Pr\u00e9cision" + }, + "data_description": { + "round": "Contr\u00f4le le nombre de chiffres d\u00e9cimaux dans la sortie." + } + }, + "options": { + "data": { + "round": "Pr\u00e9cision" + }, + "description": "La pr\u00e9cision contr\u00f4le le nombre de chiffres d\u00e9cimaux dans la sortie." + } + } + }, + "title": "Int\u00e9grale \u2013\u00a0Capteur d'int\u00e9grale de Riemann" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/he.json b/homeassistant/components/integration/translations/he.json new file mode 100644 index 00000000000..4061da5f233 --- /dev/null +++ b/homeassistant/components/integration/translations/he.json @@ -0,0 +1,40 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "\u05e9\u05d9\u05d8\u05ea \u05e9\u05d9\u05dc\u05d5\u05d1", + "name": "\u05e9\u05dd", + "round": "\u05d3\u05d9\u05d5\u05e7", + "source": "\u05d7\u05d9\u05d9\u05e9\u05df \u05e7\u05dc\u05d8", + "unit_prefix": "\u05e7\u05d9\u05d3\u05d5\u05de\u05ea \u05de\u05d8\u05e8\u05d9\u05ea", + "unit_time": "\u05d6\u05de\u05df \u05e9\u05d9\u05dc\u05d5\u05d1" + }, + "data_description": { + "round": "\u05e9\u05dc\u05d9\u05d8\u05d4 \u05d1\u05de\u05e1\u05e4\u05e8 \u05d4\u05e1\u05e4\u05e8\u05d5\u05ea \u05d4\u05e2\u05e9\u05e8\u05d5\u05e0\u05d9\u05d5\u05ea \u05d1\u05e4\u05dc\u05d8." + }, + "description": "\u05d3\u05d9\u05d5\u05e7 \u05e9\u05d5\u05dc\u05d8 \u05d1\u05de\u05e1\u05e4\u05e8 \u05d4\u05e1\u05e4\u05e8\u05d5\u05ea \u05d4\u05e2\u05e9\u05e8\u05d5\u05e0\u05d9\u05d5\u05ea \u05d1\u05e4\u05dc\u05d8.\n\u05d4\u05e1\u05db\u05d5\u05dd \u05d9\u05e9\u05ea\u05e0\u05d4 \u05d1\u05d4\u05ea\u05d0\u05dd \u05dc\u05e7\u05d9\u05d3\u05d5\u05de\u05ea \u05d4\u05de\u05d8\u05e8\u05d9\u05ea \u05e9\u05e0\u05d1\u05d7\u05e8\u05d4 \u05d5\u05d6\u05de\u05df \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1.", + "title": "\u05d7\u05d9\u05d9\u05e9\u05df \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d7\u05d3\u05e9" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "\u05d3\u05d9\u05d5\u05e7" + }, + "data_description": { + "round": "\u05e9\u05dc\u05d9\u05d8\u05d4 \u05d1\u05de\u05e1\u05e4\u05e8 \u05d4\u05e1\u05e4\u05e8\u05d5\u05ea \u05d4\u05e2\u05e9\u05e8\u05d5\u05e0\u05d9\u05d5\u05ea \u05d1\u05e4\u05dc\u05d8." + } + }, + "options": { + "data": { + "round": "\u05d3\u05d9\u05d5\u05e7" + }, + "description": "\u05d3\u05d9\u05d5\u05e7 \u05e9\u05d5\u05dc\u05d8 \u05d1\u05de\u05e1\u05e4\u05e8 \u05d4\u05e1\u05e4\u05e8\u05d5\u05ea \u05d4\u05e2\u05e9\u05e8\u05d5\u05e0\u05d9\u05d5\u05ea \u05d1\u05e4\u05dc\u05d8." + } + } + }, + "title": "\u05e9\u05d9\u05dc\u05d5\u05d1 - \u05d7\u05d9\u05d9\u05e9\u05df \u05e1\u05db\u05d5\u05dd \u05d0\u05d9\u05e0\u05d8\u05d2\u05e8\u05dc\u05d9 \u05e9\u05dc \u05e8\u05d9\u05de\u05df" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/hu.json b/homeassistant/components/integration/translations/hu.json new file mode 100644 index 00000000000..c892e858b3f --- /dev/null +++ b/homeassistant/components/integration/translations/hu.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "Integr\u00e1ci\u00f3s m\u00f3dszer", + "name": "Elnevez\u00e9s", + "round": "Pontoss\u00e1g", + "source": "Forr\u00e1s \u00e9rz\u00e9kel\u0151", + "unit_prefix": "M\u00e9rt\u00e9kegys\u00e9g el\u0151tag", + "unit_time": "Id\u0151egys\u00e9g" + }, + "data_description": { + "round": "Az eredm\u00e9ny tizedesjegyeinek sz\u00e1ma.", + "unit_prefix": "A kimenet a kiv\u00e1lasztott m\u00e9rt\u00e9kegys\u00e9gnek megfelel\u0151en lesz sk\u00e1l\u00e1zva.", + "unit_time": "A kimenet a kiv\u00e1lasztott id\u0151egys\u00e9gnek megfelel\u0151en lesz sk\u00e1l\u00e1zva." + }, + "description": "Hozzon l\u00e9tre egy \u00faj \u00e9rz\u00e9kel\u0151t, amely Riemann-integr\u00e1lt sz\u00e1mol egy m\u00e1sik, megl\u00e9v\u0151 \u00e9rz\u00e9kel\u0151 alapj\u00e1n.", + "title": "Riemann-integr\u00e1l \u00e9rz\u00e9kel\u0151 l\u00e9trehoz\u00e1sa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "Pontoss\u00e1g" + }, + "data_description": { + "round": "Az eredm\u00e9ny tizedesjegyeinek sz\u00e1ma." + } + }, + "options": { + "data": { + "round": "Pontoss\u00e1g" + }, + "description": "A pontoss\u00e1g hat\u00e1rozza meg az eredm\u00e9ny tizedesjegyeinek sz\u00e1m\u00e1t." + } + } + }, + "title": "Integr\u00e1ci\u00f3 - Riemann-integr\u00e1l" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/id.json b/homeassistant/components/integration/translations/id.json new file mode 100644 index 00000000000..d585a4409e9 --- /dev/null +++ b/homeassistant/components/integration/translations/id.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "Metode integrasi", + "name": "Nama", + "round": "Presisi", + "source": "Sensor input", + "unit_prefix": "Prefiks metrik", + "unit_time": "Unit waktu" + }, + "data_description": { + "round": "Mengontrol jumlah digit desimal dalam output.", + "unit_prefix": "Output akan diskalakan sesuai dengan prefiks metrik yang dipilih.", + "unit_time": "Output akan diskalakan sesuai dengan unit waktu dipilih." + }, + "description": "Buat sensor yang menghitung jumlah Riemann untuk memperkirakan integral dari sebuah sensor.", + "title": "Tambahkan sensor integral penjumlahan Riemann" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "Presisi" + }, + "data_description": { + "round": "Mengontrol jumlah digit desimal dalam output." + } + }, + "options": { + "data": { + "round": "Presisi" + }, + "description": "Presisi mengontrol jumlah digit desimal pada output." + } + } + }, + "title": "Integrasi - Sensor jumlah integral Riemann" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/it.json b/homeassistant/components/integration/translations/it.json new file mode 100644 index 00000000000..a1904f45cd1 --- /dev/null +++ b/homeassistant/components/integration/translations/it.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "Metodo di integrazione", + "name": "Nome", + "round": "Precisione", + "source": "Sensore di ingresso", + "unit_prefix": "Prefisso metrico", + "unit_time": "Unit\u00e0 di tempo" + }, + "data_description": { + "round": "Controlla il numero di cifre decimali nell'output.", + "unit_prefix": "L'output sar\u00e0 ridimensionato in base al prefisso della metrica selezionato.", + "unit_time": "L'output verr\u00e0 ridimensionato in base all'unit\u00e0 di tempo selezionata." + }, + "description": "Crea un sensore che calcoli una somma di Riemann per stimare l'integrale di un sensore.", + "title": "Aggiungi il sensore integrale della somma di Riemann" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "Precisione" + }, + "data_description": { + "round": "Controlla il numero di cifre decimali nell'output." + } + }, + "options": { + "data": { + "round": "Precisione" + }, + "description": "Precisione controlla il numero di cifre decimali nell'uscita." + } + } + }, + "title": "Integrazione - Sensore integrale di somma Riemann" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/ja.json b/homeassistant/components/integration/translations/ja.json new file mode 100644 index 00000000000..5fac6ba692b --- /dev/null +++ b/homeassistant/components/integration/translations/ja.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "\u7a4d\u7b97\u65b9\u6cd5", + "name": "\u540d\u524d", + "round": "\u7cbe\u5ea6", + "source": "\u5165\u529b\u30bb\u30f3\u30b5\u30fc", + "unit_prefix": "\u30e1\u30c8\u30ea\u30c3\u30af\u63a5\u982d\u8f9e", + "unit_time": "\u7a4d\u7b97\u6642\u9593" + }, + "data_description": { + "round": "\u51fa\u529b\u5024\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3002", + "unit_prefix": "\u51fa\u529b\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002", + "unit_time": "\u51fa\u529b\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002" + }, + "description": "\u7cbe\u5ea6\u306f\u3001\u51fa\u529b\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u5408\u8a08\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ea\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u7a4d\u5206\u6642\u9593\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002", + "title": "\u65b0\u3057\u3044\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u30bb\u30f3\u30b5\u30fc" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "\u7cbe\u5ea6" + }, + "data_description": { + "round": "\u51fa\u529b\u5024\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3002" + } + }, + "options": { + "data": { + "round": "\u7cbe\u5ea6" + }, + "description": "\u7cbe\u5ea6\u306f\u3001\u51fa\u529b\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002" + } + } + }, + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3 - \u30ea\u30fc\u30de\u30f3\u548c\u7a4d\u5206\u30bb\u30f3\u30b5\u30fc" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/nl.json b/homeassistant/components/integration/translations/nl.json new file mode 100644 index 00000000000..8753ee30ea7 --- /dev/null +++ b/homeassistant/components/integration/translations/nl.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "Integratie methode", + "name": "Naam", + "round": "Precisie", + "source": "Invoer sensor", + "unit_prefix": "Metrisch voorvoegsel", + "unit_time": "Tijdseenheid" + }, + "data_description": { + "round": "Regelt het aantal decimale cijfers in de uitvoer.", + "unit_prefix": "De uitvoer wordt geschaald volgens het geselecteerde metrische voorvoegsel.", + "unit_time": "De output wordt geschaald volgens de geselecteerde tijdseenheid." + }, + "description": "Maak een sensor die een Riemann som berekent om de integraal van een sensor te schatten.", + "title": "Riemann som integrale sensor toevoegen" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "Precisie" + }, + "data_description": { + "round": "Regelt het aantal decimale cijfers in de uitvoer." + } + }, + "options": { + "data": { + "round": "Precisie" + }, + "description": "Precisie bepaalt het aantal decimale cijfers in de uitvoer." + } + } + }, + "title": "Integratie - Riemann som integrale sensor" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/no.json b/homeassistant/components/integration/translations/no.json new file mode 100644 index 00000000000..aafa60b5811 --- /dev/null +++ b/homeassistant/components/integration/translations/no.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "Integrasjonsmetode", + "name": "Navn", + "round": "Presisjon", + "source": "Inngangssensor", + "unit_prefix": "Metrisk prefiks", + "unit_time": "Tidsenhet" + }, + "data_description": { + "round": "Styrer antall desimaler i utdataene.", + "unit_prefix": "Utdataene skaleres i henhold til det valgte metriske prefikset.", + "unit_time": "Utgangen vil bli skalert i henhold til den valgte tidsenheten." + }, + "description": "Lag en sensor som beregner en Riemann-sum for \u00e5 estimere integralet til en sensor.", + "title": "Legg til Riemann sum integral sensor" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "Presisjon" + }, + "data_description": { + "round": "Styrer antall desimaler i utdataene." + } + }, + "options": { + "data": { + "round": "Presisjon" + }, + "description": "Presisjon styrer antall desimaler i utdataene." + } + } + }, + "title": "Integrasjon - Riemann sum integral sensor" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/pl.json b/homeassistant/components/integration/translations/pl.json new file mode 100644 index 00000000000..7971d97129c --- /dev/null +++ b/homeassistant/components/integration/translations/pl.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "Metoda integracji", + "name": "Nazwa", + "round": "Precyzja", + "source": "Sensor wej\u015bciowy", + "unit_prefix": "Prefiks metryczny", + "unit_time": "Jednostka czasu" + }, + "data_description": { + "round": "Kontroluje liczb\u0119 cyfr dziesi\u0119tnych w danych wyj\u015bciowych.", + "unit_prefix": "Dane wyj\u015bciowe b\u0119d\u0105 skalowane zgodnie z wybranym prefiksem metrycznym.", + "unit_time": "Dane wyj\u015bciowe b\u0119d\u0105 skalowane zgodnie z wybran\u0105 jednostk\u0105 czasu." + }, + "description": "Utw\u00f3rz sensor, kt\u00f3ry oblicza sum\u0119 Riemanna, aby oszacowa\u0107 ca\u0142k\u0119 sensora.", + "title": "Dodaj sensor ca\u0142kuj\u0105cy sum\u0119 Riemanna" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "Precyzja" + }, + "data_description": { + "round": "Kontroluje liczb\u0119 cyfr dziesi\u0119tnych w danych wyj\u015bciowych." + } + }, + "options": { + "data": { + "round": "Precyzja" + }, + "description": "Precyzja kontroluje liczb\u0119 cyfr dziesi\u0119tnych w danych wyj\u015bciowych." + } + } + }, + "title": "Integracja - czujnik ca\u0142kuj\u0105cy sum\u0119 Riemanna" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/pt-BR.json b/homeassistant/components/integration/translations/pt-BR.json new file mode 100644 index 00000000000..ae512b93fd4 --- /dev/null +++ b/homeassistant/components/integration/translations/pt-BR.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "M\u00e9todo de integra\u00e7\u00e3o", + "name": "Nome", + "round": "Precis\u00e3o", + "source": "Sensor fonte", + "unit_prefix": "Prefixo m\u00e9trico", + "unit_time": "Unidade de tempo" + }, + "data_description": { + "round": "Controla o n\u00famero de d\u00edgitos decimais na sa\u00edda.", + "unit_prefix": "A sa\u00edda ser\u00e1 dimensionada de acordo com o prefixo m\u00e9trico selecionado.", + "unit_time": "A sa\u00edda ser\u00e1 dimensionada de acordo com a unidade de tempo selecionada." + }, + "description": "Crie um sensor que calcule uma soma de Riemann para estimar a integral de um sensor.", + "title": "Adicionar sensor de soma de Riemann" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "Precis\u00e3o" + }, + "data_description": { + "round": "Controla o n\u00famero de d\u00edgitos decimais na sa\u00edda." + } + }, + "options": { + "data": { + "round": "Precis\u00e3o" + }, + "description": "A precis\u00e3o controla o n\u00famero de d\u00edgitos decimais na sa\u00edda." + } + } + }, + "title": "Integra\u00e7\u00e3o - Sensor de soma de Riemann" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/ru.json b/homeassistant/components/integration/translations/ru.json new file mode 100644 index 00000000000..67891293d7b --- /dev/null +++ b/homeassistant/components/integration/translations/ru.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "\u041c\u0435\u0442\u043e\u0434 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "round": "\u041e\u043a\u0440\u0443\u0433\u043b\u0435\u043d\u0438\u0435", + "source": "\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440", + "unit_prefix": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441 \u043c\u0435\u0442\u0440\u0438\u043a\u0438", + "unit_time": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438" + }, + "data_description": { + "round": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439.", + "unit_prefix": "\u0414\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0438 \u0441 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u043c \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \u043c\u0435\u0442\u0440\u0438\u043a\u0438.", + "unit_time": "\u0414\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0438 \u0441 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0439 \u0435\u0434\u0438\u043d\u0438\u0446\u0435\u0439 \u0432\u0440\u0435\u043c\u0435\u043d\u0438." + }, + "description": "\u0412\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442 \u0441\u0443\u043c\u043c\u0443 \u0420\u0438\u043c\u0430\u043d\u0430 \u0434\u043b\u044f \u043e\u0446\u0435\u043d\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u043b\u0430 \u0441\u0435\u043d\u0441\u043e\u0440\u0430.", + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u043b \u0420\u0438\u043c\u0430\u043d\u0430" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "\u041e\u043a\u0440\u0443\u0433\u043b\u0435\u043d\u0438\u0435" + }, + "data_description": { + "round": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439." + } + }, + "options": { + "data": { + "round": "\u041e\u043a\u0440\u0443\u0433\u043b\u0435\u043d\u0438\u0435" + }, + "description": "\u041e\u043a\u0440\u0443\u0433\u043b\u0435\u043d\u0438\u0435 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439." + } + } + }, + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u043b \u0420\u0438\u043c\u0430\u043d\u0430" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/tr.json b/homeassistant/components/integration/translations/tr.json new file mode 100644 index 00000000000..de99ebd3633 --- /dev/null +++ b/homeassistant/components/integration/translations/tr.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "Entegrasyon y\u00f6ntemi", + "name": "Ad", + "round": "Hassas", + "source": "Giri\u015f sens\u00f6r\u00fc", + "unit_prefix": "Metrik \u00f6neki", + "unit_time": "Zaman birimi" + }, + "data_description": { + "round": "\u00c7\u0131kt\u0131daki ondal\u0131k basamak say\u0131s\u0131n\u0131 kontrol eder.", + "unit_prefix": "\u00c7\u0131kt\u0131, se\u00e7ilen metrik \u00f6nekine g\u00f6re \u00f6l\u00e7eklenecektir.", + "unit_time": "\u00c7\u0131kt\u0131, se\u00e7ilen zaman birimine g\u00f6re \u00f6l\u00e7eklenecektir." + }, + "description": "Bir sens\u00f6r\u00fcn integralini tahmin etmek i\u00e7in bir Riemann toplam\u0131n\u0131 hesaplayan bir sens\u00f6r olu\u015fturun.", + "title": "Riemann toplam integral sens\u00f6r\u00fcn\u00fc ekleyin" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "Hassas" + }, + "data_description": { + "round": "\u00c7\u0131kt\u0131daki ondal\u0131k basamak say\u0131s\u0131n\u0131 kontrol eder." + } + }, + "options": { + "data": { + "round": "Hassas" + }, + "description": "Kesinlik, \u00e7\u0131kt\u0131daki ondal\u0131k basamak say\u0131s\u0131n\u0131 kontrol eder." + } + } + }, + "title": "Entegrasyon - Riemann toplam integral sens\u00f6r\u00fc" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/zh-Hans.json b/homeassistant/components/integration/translations/zh-Hans.json new file mode 100644 index 00000000000..f7ebea98f4a --- /dev/null +++ b/homeassistant/components/integration/translations/zh-Hans.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "\u79ef\u5206\u65b9\u6cd5", + "name": "\u540d\u79f0", + "round": "\u7cbe\u5ea6", + "source": "\u8f93\u5165\u4f20\u611f\u5668", + "unit_prefix": "\u5355\u4f4d\u524d\u7f00", + "unit_time": "\u65f6\u95f4\u5355\u4f4d" + }, + "data_description": { + "round": "\u63a7\u5236\u8f93\u51fa\u7684\u5c0f\u6570\u4f4d\u6570\u3002", + "unit_prefix": "\u8f93\u51fa\u503c\u5c06\u6839\u636e\u6240\u9009\u7684\u5355\u4f4d\u524d\u7f00\u8fdb\u884c\u7f29\u653e\u3002", + "unit_time": "\u8f93\u51fa\u503c\u5c06\u6839\u636e\u6240\u9009\u7684\u65f6\u95f4\u5355\u4f4d\u8fdb\u884c\u7f29\u653e\u3002" + }, + "description": "\u521b\u5efa\u4f20\u611f\u5668\u6765\u8ba1\u7b97\u53e6\u4e00\u4e2a\u4f20\u611f\u5668\u7684\u5b9a\u79ef\u5206\u3002", + "title": "\u6dfb\u52a0\u5b9a\u79ef\u5206\u4f20\u611f\u5668" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "\u7cbe\u5ea6" + }, + "data_description": { + "round": "\u63a7\u5236\u8f93\u51fa\u7684\u5c0f\u6570\u4f4d\u6570\u3002" + } + }, + "options": { + "data": { + "round": "\u7cbe\u5ea6" + }, + "description": "\u7cbe\u5ea6\u63a7\u5236\u8f93\u51fa\u7684\u5c0f\u6570\u4f4d\u6570\u3002" + } + } + }, + "title": "\u5b9a\u79ef\u5206\u4f20\u611f\u5668" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/zh-Hant.json b/homeassistant/components/integration/translations/zh-Hant.json new file mode 100644 index 00000000000..2adbb3edc28 --- /dev/null +++ b/homeassistant/components/integration/translations/zh-Hant.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "\u6574\u5408\u65b9\u5f0f", + "name": "\u540d\u7a31", + "round": "\u6e96\u78ba\u5ea6", + "source": "\u8f38\u5165\u611f\u6e2c\u5668", + "unit_prefix": "\u516c\u5236\u524d\u7db4", + "unit_time": "\u6642\u9593\u55ae\u4f4d" + }, + "data_description": { + "round": "\u63a7\u5236\u8f38\u51fa\u4e2d\u7684\u5c0f\u6578\u4f4d\u6578\u3002", + "unit_prefix": "\u8f38\u51fa\u5c07\u53d7\u6240\u9078\u64c7\u516c\u5236\u524d\u7db4\u800c\u8b8a\u5316\u3002", + "unit_time": "\u8f38\u51fa\u5c07\u53d7\u6240\u9078\u64c7\u6642\u9593\u55ae\u4f4d\u800c\u8b8a\u5316\u3002" + }, + "description": "\u65b0\u589e\u9810\u4f30\u8a08\u7b97\u9ece\u66fc\u548c\u7a4d\u5206\u611f\u6e2c\u5668\u6574\u5408\u4e4b\u611f\u6e2c\u5668\u3002", + "title": "\u65b0\u589e\u9ece\u66fc\u548c\u7a4d\u5206\u611f\u6e2c\u5668\u6574\u5408" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "\u6e96\u78ba\u5ea6" + }, + "data_description": { + "round": "\u63a7\u5236\u8f38\u51fa\u4e2d\u7684\u5c0f\u6578\u4f4d\u6578\u3002" + } + }, + "options": { + "data": { + "round": "\u6e96\u78ba\u5ea6" + }, + "description": "\u7cbe\u6e96\u5ea6\u63a7\u5236\u8f38\u51fa\u4e2d\u7684\u5c0f\u6578\u4f4d\u6578\u3002" + } + } + }, + "title": "\u6574\u5408 - \u9ece\u66fc\u548c\u7a4d\u5206\u611f\u6e2c\u5668" +} \ No newline at end of file diff --git a/homeassistant/components/intellifire/translations/bg.json b/homeassistant/components/intellifire/translations/bg.json index cbf1e2ae7c9..1a8dd460097 100644 --- a/homeassistant/components/intellifire/translations/bg.json +++ b/homeassistant/components/intellifire/translations/bg.json @@ -7,7 +7,22 @@ "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \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": "{serial} ({host})", "step": { + "dhcp_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {host}\n\u0421\u0435\u0440\u0438\u0435\u043d: {serial}?" + }, + "manual_device_entry": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u041b\u043e\u043a\u0430\u043b\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f" + }, + "pick_device": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/intellifire/translations/ca.json b/homeassistant/components/intellifire/translations/ca.json index 3673c1c60c6..9894ec4920a 100644 --- a/homeassistant/components/intellifire/translations/ca.json +++ b/homeassistant/components/intellifire/translations/ca.json @@ -1,13 +1,40 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "not_intellifire_device": "No \u00e9s un dispositiu IntelliFire.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { + "api_error": "Ha fallat l'inici de sessi\u00f3", "cannot_connect": "Ha fallat la connexi\u00f3", + "iftapi_connect": "S'ha produ\u00eft un error en connectar a iftapi.net", "unknown": "Error inesperat" }, + "flow_title": "{serial} ({host})", "step": { + "api_config": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + } + }, + "dhcp_confirm": { + "description": "Vols configurar {host}\nS\u00e8rie: {serial}?" + }, + "manual_device_entry": { + "data": { + "host": "Amfitri\u00f3 (adre\u00e7a IP)" + }, + "description": "Configuraci\u00f3 local" + }, + "pick_device": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "S'han descobert els dispositius IntelliFire seg\u00fcents. Selecciona el que vulguis configurar.", + "title": "Selecci\u00f3 de dispositiu" + }, "user": { "data": { "host": "Amfitri\u00f3" diff --git a/homeassistant/components/intellifire/translations/cs.json b/homeassistant/components/intellifire/translations/cs.json index 5eac883adf0..48dbc509ea8 100644 --- a/homeassistant/components/intellifire/translations/cs.json +++ b/homeassistant/components/intellifire/translations/cs.json @@ -7,7 +7,18 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, + "flow_title": "{serial} ({host})", "step": { + "manual_device_entry": { + "data": { + "host": "Hostitel" + } + }, + "pick_device": { + "data": { + "host": "Hostitel" + } + }, "user": { "data": { "host": "Hostitel" diff --git a/homeassistant/components/intellifire/translations/de.json b/homeassistant/components/intellifire/translations/de.json index 6abbe1b2b27..f1c37a8a475 100644 --- a/homeassistant/components/intellifire/translations/de.json +++ b/homeassistant/components/intellifire/translations/de.json @@ -1,13 +1,40 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "not_intellifire_device": "Kein IntelliFire-Ger\u00e4t.", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { + "api_error": "Login fehlgeschlagen", "cannot_connect": "Verbindung fehlgeschlagen", + "iftapi_connect": "Fehler beim Verbinden mit iftapi.net", "unknown": "Unerwarteter Fehler" }, + "flow_title": "{serial} ({host})", "step": { + "api_config": { + "data": { + "password": "Passwort", + "username": "E-Mail" + } + }, + "dhcp_confirm": { + "description": "M\u00f6chtest du {host} einrichten\nSeriennummer: {serial}?" + }, + "manual_device_entry": { + "data": { + "host": "Host (IP-Adresse)" + }, + "description": "Lokale Konfiguration" + }, + "pick_device": { + "data": { + "host": "Host" + }, + "description": "Die folgenden IntelliFire-Ger\u00e4te wurden gefunden. Bitte w\u00e4hle aus, welche du konfigurieren m\u00f6chtest.", + "title": "Ger\u00e4teauswahl" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/intellifire/translations/el.json b/homeassistant/components/intellifire/translations/el.json index c7b88a9b3d8..fa72581a4a6 100644 --- a/homeassistant/components/intellifire/translations/el.json +++ b/homeassistant/components/intellifire/translations/el.json @@ -1,13 +1,40 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "not_intellifire_device": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae IntelliFire.", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { + "api_error": "\u0397 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "iftapi_connect": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf iftapi.net", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, + "flow_title": "{serial} ({host})", "step": { + "api_config": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "Email" + } + }, + "dhcp_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {host}\nSerial: {serial}?" + }, + "manual_device_entry": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "description": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7" + }, + "pick_device": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "description": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03bf\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 IntelliFire. \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03bf\u03b9\u03b1 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5.", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" + }, "user": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" diff --git a/homeassistant/components/intellifire/translations/en.json b/homeassistant/components/intellifire/translations/en.json index d7c157d8917..f0c317efb93 100644 --- a/homeassistant/components/intellifire/translations/en.json +++ b/homeassistant/components/intellifire/translations/en.json @@ -7,18 +7,17 @@ }, "error": { "api_error": "Login failed", - "cannot_connect": "Could not connect to a fireplace endpoint at url: http://{host}/poll\nVerify IP address and try again", - "iftapi_connect": "Error conecting to iftapi.net" + "cannot_connect": "Failed to connect", + "iftapi_connect": "Error conecting to iftapi.net", + "unknown": "Unexpected error" }, "flow_title": "{serial} ({host})", "step": { "api_config": { "data": { "password": "Password", - "username": "Username (Email)" - }, - "description": "IntelliFire will need to reach out to [iftapi.net](https://iftapi.net/webaccess/login.html) in order to obtain an API key. Once it has obtained this API key, the rest of its interactions will occur completely within the local network. If the API key were to expire it would again need to reach out to https://iftapi.net/webaccess/login.html\n\nUsername and Password are the same information used in your IntelliFire Android/iOS application. ", - "title": "IntelliFire - API Configuration" + "username": "Email" + } }, "dhcp_confirm": { "description": "Do you want to setup {host}\nSerial: {serial}?" @@ -35,6 +34,11 @@ }, "description": "The following IntelliFire devices were discovered. Please select which you wish to configure.", "title": "Device Selection" + }, + "user": { + "data": { + "host": "Host" + } } } } diff --git a/homeassistant/components/intellifire/translations/et.json b/homeassistant/components/intellifire/translations/et.json index 939fa44224f..53c87f112a7 100644 --- a/homeassistant/components/intellifire/translations/et.json +++ b/homeassistant/components/intellifire/translations/et.json @@ -1,13 +1,40 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "not_intellifire_device": "See pole IntelliFire seade.", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { + "api_error": "Sisselogimine nurjus", "cannot_connect": "\u00dchendamine nurjus", + "iftapi_connect": "\u00dchendumine iftapi.net'iga nurjus", "unknown": "Ootamatu t\u00f5rge" }, + "flow_title": "{serial} ({host})", "step": { + "api_config": { + "data": { + "password": "Salas\u00f5na", + "username": "E-posti aadress" + } + }, + "dhcp_confirm": { + "description": "Kas h\u00e4\u00e4lestada {host}\nSeerianumber: {serial}?" + }, + "manual_device_entry": { + "data": { + "host": "Host (IP aadress)" + }, + "description": "Kohalik konfiguratsioon" + }, + "pick_device": { + "data": { + "host": "Host" + }, + "description": "Avastati j\u00e4rgmised IntelliFire seadmed. Palun vali millist soovid seadistada.", + "title": "Seadme valik" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/intellifire/translations/fr.json b/homeassistant/components/intellifire/translations/fr.json index 3b0762be825..5c478358345 100644 --- a/homeassistant/components/intellifire/translations/fr.json +++ b/homeassistant/components/intellifire/translations/fr.json @@ -1,13 +1,40 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "not_intellifire_device": "N'est pas un appareil IntelliFire.", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { + "api_error": "La connexion a \u00e9chou\u00e9", "cannot_connect": "\u00c9chec de connexion", + "iftapi_connect": "Erreur lors de la connexion \u00e0 iftapi.net", "unknown": "Erreur inattendue" }, + "flow_title": "{serial} ({host})", "step": { + "api_config": { + "data": { + "password": "Mot de passe", + "username": "Courriel" + } + }, + "dhcp_confirm": { + "description": "Voulez-vous configurer {host}\nNum\u00e9ro de s\u00e9rie\u00a0: {serial}\u00a0?" + }, + "manual_device_entry": { + "data": { + "host": "H\u00f4te (adresse IP)" + }, + "description": "Configuration locale" + }, + "pick_device": { + "data": { + "host": "H\u00f4te" + }, + "description": "Les appareils IntelliFire suivants ont \u00e9t\u00e9 d\u00e9couverts. Veuillez s\u00e9lectionner celui que vous souhaitez configurer.", + "title": "S\u00e9lection de l'appareil" + }, "user": { "data": { "host": "H\u00f4te" diff --git a/homeassistant/components/intellifire/translations/he.json b/homeassistant/components/intellifire/translations/he.json index 1699e0f8e19..e4b64392380 100644 --- a/homeassistant/components/intellifire/translations/he.json +++ b/homeassistant/components/intellifire/translations/he.json @@ -1,13 +1,30 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { + "api_config": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05d3\u05d5\u05d0\"\u05dc" + } + }, + "manual_device_entry": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + }, + "pick_device": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7" diff --git a/homeassistant/components/intellifire/translations/hu.json b/homeassistant/components/intellifire/translations/hu.json index c46c7b02f5a..f5d11abe4c0 100644 --- a/homeassistant/components/intellifire/translations/hu.json +++ b/homeassistant/components/intellifire/translations/hu.json @@ -1,13 +1,40 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "not_intellifire_device": "Nem egy IntelliFire eszk\u00f6z.", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { + "api_error": "A bejelentkez\u00e9s sikertelen", "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "iftapi_connect": "Hiba az iftapi.net-hez val\u00f3 csatlakoz\u00e1sban", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, + "flow_title": "{serial} ({host})", "step": { + "api_config": { + "data": { + "password": "Jelsz\u00f3", + "username": "E-mail" + } + }, + "dhcp_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {host}\nGy\u00e1ri sz\u00e1m: {serial}?" + }, + "manual_device_entry": { + "data": { + "host": "IP C\u00edm" + }, + "description": "Helyi konfigur\u00e1ci\u00f3" + }, + "pick_device": { + "data": { + "host": "C\u00edm" + }, + "description": "A k\u00f6vetkez\u0151 IntelliFire eszk\u00f6z\u00f6k \u00e9szlelve. K\u00e9rj\u00fck, v\u00e1lassza ki, melyiket szeretn\u00e9 konfigur\u00e1lni.", + "title": "Eszk\u00f6z v\u00e1laszt\u00e1sa" + }, "user": { "data": { "host": "C\u00edm" diff --git a/homeassistant/components/intellifire/translations/id.json b/homeassistant/components/intellifire/translations/id.json index c04f83e4b59..07ec946b6ba 100644 --- a/homeassistant/components/intellifire/translations/id.json +++ b/homeassistant/components/intellifire/translations/id.json @@ -1,13 +1,40 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "not_intellifire_device": "Bukan Perangkat IntelliFire.", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { + "api_error": "Gagal masuk", "cannot_connect": "Gagal terhubung", + "iftapi_connect": "Kesalahan saat menyambung ke iftapi.net", "unknown": "Kesalahan yang tidak diharapkan" }, + "flow_title": "{serial} ({host})", "step": { + "api_config": { + "data": { + "password": "Kata Sandi", + "username": "Email" + } + }, + "dhcp_confirm": { + "description": "Ingin menyiapkan {host}\nSerial: ({serial})?" + }, + "manual_device_entry": { + "data": { + "host": "Host (Alamat IP)" + }, + "description": "Konfigurasi Lokal" + }, + "pick_device": { + "data": { + "host": "Host" + }, + "description": "Perangkat IntelliFire berikut ditemukan. Pilih yang ingin dikonfigurasikan.", + "title": "Pemilihan Perangkat" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/intellifire/translations/it.json b/homeassistant/components/intellifire/translations/it.json index e8bfd780908..8689840c82c 100644 --- a/homeassistant/components/intellifire/translations/it.json +++ b/homeassistant/components/intellifire/translations/it.json @@ -1,13 +1,40 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "not_intellifire_device": "Non \u00e8 un dispositivo IntelliFire.", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { + "api_error": "Accesso non riuscito", "cannot_connect": "Impossibile connettersi", + "iftapi_connect": "Errore durante la connessione a iftapi.net", "unknown": "Errore imprevisto" }, + "flow_title": "{serial} ({host})", "step": { + "api_config": { + "data": { + "password": "Password", + "username": "Email" + } + }, + "dhcp_confirm": { + "description": "Vuoi configurare {host}\n Seriale: {serial}?" + }, + "manual_device_entry": { + "data": { + "host": "Host (indirizzo IP)" + }, + "description": "Configurazione locale" + }, + "pick_device": { + "data": { + "host": "Host" + }, + "description": "Sono stati rilevati i seguenti dispositivi IntelliFire. Seleziona quello che desideri configurare.", + "title": "Selezione del dispositivo" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/intellifire/translations/ja.json b/homeassistant/components/intellifire/translations/ja.json index 718b4432290..c623f0f59cf 100644 --- a/homeassistant/components/intellifire/translations/ja.json +++ b/homeassistant/components/intellifire/translations/ja.json @@ -1,13 +1,40 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "not_intellifire_device": "IntelliFire\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { + "api_error": "\u30ed\u30b0\u30a4\u30f3\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "iftapi_connect": "iftapi.net\u3078\u306e\u63a5\u7d9a\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, + "flow_title": "{serial} ({host})", "step": { + "api_config": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "E\u30e1\u30fc\u30eb" + } + }, + "dhcp_confirm": { + "description": "{host} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f\n\u30b7\u30ea\u30a2\u30eb: {serial}\uff1f" + }, + "manual_device_entry": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "description": "\u30ed\u30fc\u30ab\u30eb\u8a2d\u5b9a" + }, + "pick_device": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "description": "\u4ee5\u4e0b\u306eIntelliFire\u30c7\u30d0\u30a4\u30b9\u304c\u691c\u51fa\u3055\u308c\u307e\u3057\u305f\u3002\u8a2d\u5b9a\u3057\u305f\u3044\u3082\u306e\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u30c7\u30d0\u30a4\u30b9\u306e\u9078\u629e" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8" diff --git a/homeassistant/components/intellifire/translations/nl.json b/homeassistant/components/intellifire/translations/nl.json index 735a4df572f..a4f2b1c304a 100644 --- a/homeassistant/components/intellifire/translations/nl.json +++ b/homeassistant/components/intellifire/translations/nl.json @@ -1,13 +1,40 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "not_intellifire_device": "Niet een IntelliFire apparaat.", + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { + "api_error": "Inloggen mislukt", "cannot_connect": "Kan geen verbinding maken", + "iftapi_connect": "Fout bij het verbinden met iftapi.net", "unknown": "Onverwachte fout" }, + "flow_title": "{serial} ({host})", "step": { + "api_config": { + "data": { + "password": "Wachtwoord", + "username": "E-mail" + } + }, + "dhcp_confirm": { + "description": "Wilt u {host} instellen\n Serieel: {serial} ?" + }, + "manual_device_entry": { + "data": { + "host": "Host (IP-adres)" + }, + "description": "Lokale configuratie" + }, + "pick_device": { + "data": { + "host": "Host" + }, + "description": "De volgende IntelliFire-apparaten zijn ontdekt. Selecteer welke u wilt configureren.", + "title": "Apparaat selectie" + }, "user": { "data": { "host": "Host" diff --git a/homeassistant/components/intellifire/translations/no.json b/homeassistant/components/intellifire/translations/no.json index 12ee27af925..8175a085f30 100644 --- a/homeassistant/components/intellifire/translations/no.json +++ b/homeassistant/components/intellifire/translations/no.json @@ -1,13 +1,40 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "not_intellifire_device": "Ikke en IntelliFire-enhet.", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { + "api_error": "Innlogging feilet", "cannot_connect": "Tilkobling mislyktes", + "iftapi_connect": "Feil ved tilkobling til iftapi.net", "unknown": "Uventet feil" }, + "flow_title": "{serial} ( {host} )", "step": { + "api_config": { + "data": { + "password": "Passord", + "username": "E-post" + } + }, + "dhcp_confirm": { + "description": "Vil du konfigurere {host}\n Serienummer: {serial} ?" + }, + "manual_device_entry": { + "data": { + "host": "Vert (IP-adresse)" + }, + "description": "Lokal konfigurasjon" + }, + "pick_device": { + "data": { + "host": "Vert" + }, + "description": "F\u00f8lgende IntelliFire-enheter ble oppdaget. Velg hvilken du \u00f8nsker \u00e5 konfigurere.", + "title": "Enhetsvalg" + }, "user": { "data": { "host": "Vert" diff --git a/homeassistant/components/intellifire/translations/pl.json b/homeassistant/components/intellifire/translations/pl.json index d455990b1f0..f63be88814f 100644 --- a/homeassistant/components/intellifire/translations/pl.json +++ b/homeassistant/components/intellifire/translations/pl.json @@ -1,13 +1,40 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "not_intellifire_device": "To nie jest urz\u0105dzenie IntelliFire.", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { + "api_error": "Logowanie nie powiod\u0142o si\u0119", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "iftapi_connect": "B\u0142\u0105d po\u0142\u0105czenia z iftapi.net", "unknown": "Nieoczekiwany b\u0142\u0105d" }, + "flow_title": "{serial} ({host})", "step": { + "api_config": { + "data": { + "password": "Has\u0142o", + "username": "Adres e-mail" + } + }, + "dhcp_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {host}\nNumer seryjny: {serial}?" + }, + "manual_device_entry": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Konfiguracja lokalna" + }, + "pick_device": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Wykryto nast\u0119puj\u0105ce urz\u0105dzenia IntelliFire. Wybierz, kt\u00f3re chcesz skonfigurowa\u0107.", + "title": "Wyb\u00f3r urz\u0105dzenia" + }, "user": { "data": { "host": "Nazwa hosta lub adres IP" diff --git a/homeassistant/components/intellifire/translations/pt-BR.json b/homeassistant/components/intellifire/translations/pt-BR.json index ff6ede166a9..babcc22bd97 100644 --- a/homeassistant/components/intellifire/translations/pt-BR.json +++ b/homeassistant/components/intellifire/translations/pt-BR.json @@ -1,13 +1,40 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "not_intellifire_device": "N\u00e3o \u00e9 um dispositivo IntelliFire.", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { + "api_error": "Login falhou", "cannot_connect": "Falha ao conectar", + "iftapi_connect": "Erro ao conectar ao iftapi.net", "unknown": "Erro inesperado" }, + "flow_title": "{serial} ({host})", "step": { + "api_config": { + "data": { + "password": "Senha", + "username": "Email" + } + }, + "dhcp_confirm": { + "description": "Voc\u00ea deseja configurar {host}\nSerial: {serial}?" + }, + "manual_device_entry": { + "data": { + "host": "Host (endere\u00e7o IP)" + }, + "description": "Configura\u00e7\u00e3o local" + }, + "pick_device": { + "data": { + "host": "Nome do host" + }, + "description": "Os seguintes dispositivos IntelliFire foram descobertos. Por favor, selecione o que voc\u00ea deseja configura.", + "title": "Sele\u00e7\u00e3o de dispositivo" + }, "user": { "data": { "host": "Nome do host" diff --git a/homeassistant/components/intellifire/translations/ru.json b/homeassistant/components/intellifire/translations/ru.json index ffde0514cde..b48cd06b592 100644 --- a/homeassistant/components/intellifire/translations/ru.json +++ b/homeassistant/components/intellifire/translations/ru.json @@ -1,13 +1,40 @@ { "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.", + "not_intellifire_device": "\u041d\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e IntelliFire.", + "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": { + "api_error": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u043d\u0435 \u0443\u0434\u0430\u043b\u0430\u0441\u044c.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "iftapi_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a iftapi.net", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, + "flow_title": "{serial} ({host})", "step": { + "api_config": { + "data": { + "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" + } + }, + "dhcp_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {host}\n\u0421\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440: {serial}?" + }, + "manual_device_entry": { + "data": { + "host": "\u0425\u043e\u0441\u0442 (IP-\u0430\u0434\u0440\u0435\u0441)" + }, + "description": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + }, + "pick_device": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0411\u044b\u043b\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 IntelliFire. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435, \u043a\u0430\u043a\u043e\u0435 \u0438\u0437 \u043d\u0438\u0445 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c.", + "title": "\u0412\u044b\u0431\u043e\u0440 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/intellifire/translations/tr.json b/homeassistant/components/intellifire/translations/tr.json index 05846c9d760..3a22e7c3680 100644 --- a/homeassistant/components/intellifire/translations/tr.json +++ b/homeassistant/components/intellifire/translations/tr.json @@ -1,13 +1,40 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "not_intellifire_device": "IntelliFire Cihaz\u0131 de\u011fil.", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { + "api_error": "Oturum a\u00e7ma ba\u015far\u0131s\u0131z oldu", "cannot_connect": "Ba\u011flanma hatas\u0131", + "iftapi_connect": "iftapi.net'e ba\u011flan\u0131rken hata olu\u015ftu", "unknown": "Beklenmeyen hata" }, + "flow_title": "{serial} ({host})", "step": { + "api_config": { + "data": { + "password": "Parola", + "username": "E-posta" + } + }, + "dhcp_confirm": { + "description": "Kurulumu yapmak {host} ister misiniz?\nSeri No: {serial}?" + }, + "manual_device_entry": { + "data": { + "host": "Sunucu (IP Adresi)" + }, + "description": "Yerel Yap\u0131land\u0131rma" + }, + "pick_device": { + "data": { + "host": "Sunucu" + }, + "description": "A\u015fa\u011f\u0131daki IntelliFire cihazlar\u0131 ke\u015ffedildi. L\u00fctfen yap\u0131land\u0131rmak istedi\u011finizi se\u00e7in.", + "title": "Cihaz Se\u00e7imi" + }, "user": { "data": { "host": "Sunucu" diff --git a/homeassistant/components/intellifire/translations/zh-Hant.json b/homeassistant/components/intellifire/translations/zh-Hant.json index 9847ae248f7..107f9cca74c 100644 --- a/homeassistant/components/intellifire/translations/zh-Hant.json +++ b/homeassistant/components/intellifire/translations/zh-Hant.json @@ -1,13 +1,40 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "not_intellifire_device": "\u975e IntelliFire \u88dd\u7f6e\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { + "api_error": "\u767b\u5165\u5931\u6557", "cannot_connect": "\u9023\u7dda\u5931\u6557", + "iftapi_connect": "\u9023\u7dda\u81f3 iftapi.net \u932f\u8aa4", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, + "flow_title": "{serial} ({host})", "step": { + "api_config": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6" + } + }, + "dhcp_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {host}\n\u5e8f\u865f\uff1a{serial}\uff1f" + }, + "manual_device_entry": { + "data": { + "host": "\u4e3b\u6a5f\uff08IP \u4f4d\u5740\uff09" + }, + "description": "\u672c\u5730\u7aef\u8a2d\u5b9a" + }, + "pick_device": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u641c\u5c0b\u5230\u4ee5\u4e0b IntelliFire \u88dd\u7f6e\uff0c\u8acb\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e\u3002", + "title": "\u88dd\u7f6e\u9078\u64c7" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef" diff --git a/homeassistant/components/iotawatt/translations/hu.json b/homeassistant/components/iotawatt/translations/hu.json index 52d46f97a84..5f5d30136da 100644 --- a/homeassistant/components/iotawatt/translations/hu.json +++ b/homeassistant/components/iotawatt/translations/hu.json @@ -11,7 +11,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Az IoTawatt eszk\u00f6z hiteles\u00edt\u00e9st ig\u00e9nyel. K\u00e9rj\u00fck, adja meg felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t, majd kattintson a K\u00fcld\u00e9s gombra." + "description": "Az IoTawatt eszk\u00f6z hiteles\u00edt\u00e9st ig\u00e9nyel. K\u00e9rj\u00fck, adja meg felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t, majd folytassa." }, "user": { "data": { diff --git a/homeassistant/components/iotawatt/translations/id.json b/homeassistant/components/iotawatt/translations/id.json index ef50c938292..bddda73fa50 100644 --- a/homeassistant/components/iotawatt/translations/id.json +++ b/homeassistant/components/iotawatt/translations/id.json @@ -11,7 +11,7 @@ "password": "Kata Sandi", "username": "Nama Pengguna" }, - "description": "Perangkat IoTawatt memerlukan otentikasi. Masukkan nama pengguna dan kata sandi dan klik tombol Kirim." + "description": "Perangkat IoTawatt memerlukan autentikasi. Masukkan nama pengguna dan kata sandi dan klik tombol Kirim." }, "user": { "data": { diff --git a/homeassistant/components/ipma/translations/hu.json b/homeassistant/components/ipma/translations/hu.json index 00ba66d2dd7..16653645052 100644 --- a/homeassistant/components/ipma/translations/hu.json +++ b/homeassistant/components/ipma/translations/hu.json @@ -9,7 +9,7 @@ "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g", "mode": "M\u00f3d", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" }, "description": "Portug\u00e1l Atmoszf\u00e9ra Int\u00e9zet", "title": "Elhelyezked\u00e9s" diff --git a/homeassistant/components/ipp/translations/it.json b/homeassistant/components/ipp/translations/it.json index c5a2a613eeb..96319641c6b 100644 --- a/homeassistant/components/ipp/translations/it.json +++ b/homeassistant/components/ipp/translations/it.json @@ -6,7 +6,7 @@ "connection_upgrade": "Impossibile connettersi alla stampante a causa della necessit\u00e0 dell'aggiornamento della connessione.", "ipp_error": "Si \u00e8 verificato un errore IPP.", "ipp_version_error": "Versione IPP non supportata dalla stampante.", - "parse_error": "Impossibile analizza la risposta dalla stampante.", + "parse_error": "Impossibile analizzare la risposta dalla stampante.", "unique_id_required": "Identificazione univoca del dispositivo mancante necessaria per l'individuazione." }, "error": { diff --git a/homeassistant/components/ipp/translations/pl.json b/homeassistant/components/ipp/translations/pl.json index b44904095de..b2eb67a6d93 100644 --- a/homeassistant/components/ipp/translations/pl.json +++ b/homeassistant/components/ipp/translations/pl.json @@ -24,7 +24,7 @@ "verify_ssl": "Weryfikacja certyfikatu SSL" }, "description": "Skonfiguruj drukark\u0119 za pomoc\u0105 protoko\u0142u IPP (Internet Printing Protocol), aby zintegrowa\u0107 j\u0105 z Home Assistantem.", - "title": "Po\u0142\u0105cz swoj\u0105 drukark\u0119" + "title": "Po\u0142\u0105czenie z Twoj\u0105 drukark\u0105" }, "zeroconf_confirm": { "description": "Czy chcesz doda\u0107 drukark\u0119 o nazwie `{name}` do Home Assistanta?", diff --git a/homeassistant/components/iss/translations/hu.json b/homeassistant/components/iss/translations/hu.json index 1d75dd9ed3f..bfe593e9592 100644 --- a/homeassistant/components/iss/translations/hu.json +++ b/homeassistant/components/iss/translations/hu.json @@ -9,7 +9,7 @@ "data": { "show_on_map": "Megjelenjen a t\u00e9rk\u00e9pen?" }, - "description": "Szeretn\u00e9 konfigur\u00e1lni a Nemzetk\u00f6zi \u0170r\u00e1llom\u00e1st?" + "description": "Szeretn\u00e9 konfigur\u00e1lni a Nemzetk\u00f6zi \u0170r\u00e1llom\u00e1st (ISS)?" } } }, diff --git a/homeassistant/components/iss/translations/pt-BR.json b/homeassistant/components/iss/translations/pt-BR.json index 5e34b2eec1b..c69fefed0c5 100644 --- a/homeassistant/components/iss/translations/pt-BR.json +++ b/homeassistant/components/iss/translations/pt-BR.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "show_on_map": "Mostrar no mapa?" + "show_on_map": "Mostrar no mapa" }, "description": "Deseja configurar a Esta\u00e7\u00e3o Espacial Internacional (ISS)?" } @@ -17,7 +17,7 @@ "step": { "init": { "data": { - "show_on_map": "Mostrar no mapa?" + "show_on_map": "Mostrar no mapa" } } } diff --git a/homeassistant/components/kaleidescape/translations/cs.json b/homeassistant/components/kaleidescape/translations/cs.json new file mode 100644 index 00000000000..deb0693b00d --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "flow_title": "{model} ({name})" + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/hu.json b/homeassistant/components/kaleidescape/translations/hu.json index 953de8119ed..cd8b00e18a3 100644 --- a/homeassistant/components/kaleidescape/translations/hu.json +++ b/homeassistant/components/kaleidescape/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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", "unsupported": "Nem t\u00e1mogatott eszk\u00f6z" }, diff --git a/homeassistant/components/knx/translations/bg.json b/homeassistant/components/knx/translations/bg.json index 43f72f49867..3cbc8b57b0b 100644 --- a/homeassistant/components/knx/translations/bg.json +++ b/homeassistant/components/knx/translations/bg.json @@ -5,7 +5,8 @@ "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\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_ip_address": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d IPv4 \u0430\u0434\u0440\u0435\u0441." }, "step": { "manual_tunnel": { @@ -14,11 +15,23 @@ "local_ip": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, \u0430\u043a\u043e \u043d\u0435 \u0441\u0442\u0435 \u0441\u0438\u0433\u0443\u0440\u043d\u0438)", "port": "\u041f\u043e\u0440\u0442", "tunneling_type": "KNX \u0442\u0443\u043d\u0435\u043b\u0435\u043d \u0442\u0438\u043f" + }, + "data_description": { + "local_ip": "\u041e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, \u0437\u0430 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435." } }, "routing": { "data": { "local_ip": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441 (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, \u0430\u043a\u043e \u043d\u0435 \u0441\u0442\u0435 \u0441\u0438\u0433\u0443\u0440\u043d\u0438)" + }, + "data_description": { + "local_ip": "\u041e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, \u0437\u0430 \u0434\u0430 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435." + } + }, + "secure_manual": { + "data": { + "user_id": "\u0418\u0414 \u043d\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f", + "user_password": "\u041f\u0430\u0440\u043e\u043b\u0430 \u043d\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f" } } } @@ -28,6 +41,9 @@ "init": { "data": { "local_ip": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441 (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, \u0430\u043a\u043e \u043d\u0435 \u0441\u0442\u0435 \u0441\u0438\u0433\u0443\u0440\u043d\u0438)" + }, + "data_description": { + "local_ip": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0439\u0442\u0435 `0.0.0.0` \u0437\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435." } }, "tunnel": { diff --git a/homeassistant/components/knx/translations/ca.json b/homeassistant/components/knx/translations/ca.json index dd553f293be..b79887e1586 100644 --- a/homeassistant/components/knx/translations/ca.json +++ b/homeassistant/components/knx/translations/ca.json @@ -5,29 +5,73 @@ "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "error": { - "cannot_connect": "Ha fallat la connexi\u00f3" + "cannot_connect": "Ha fallat la connexi\u00f3", + "file_not_found": "No s'ha trobat el fitxer `.knxkeys` especificat a la ruta config/.storage/knx/", + "invalid_individual_address": "El valor no coincideix amb el patr\u00f3 d'adre\u00e7a KNX individual.\n'area.line.device'", + "invalid_ip_address": "Adre\u00e7a IPv4 inv\u00e0lida.", + "invalid_signature": "La contrasenya per desxifrar el fitxer `.knxkeys` \u00e9s incorrecta." }, "step": { "manual_tunnel": { "data": { "host": "Amfitri\u00f3", "individual_address": "Adre\u00e7a individual de la connexi\u00f3", - "local_ip": "IP local de Home Assistant (deixa-ho en blanc si no n'est\u00e0s segur/a)", + "local_ip": "IP local de Home Assistant", "port": "Port", "route_back": "Encaminament de retorn / Mode NAT", "tunneling_type": "Tipus de t\u00fanel KNX" }, + "data_description": { + "host": "Adre\u00e7a IP del dispositiu de tunelitzaci\u00f3 KNX/IP.", + "local_ip": "Deixa-ho en blanc per utilitzar el descobriment autom\u00e0tic.", + "port": "Port del dispositiu de tunelitzaci\u00f3 KNX/IP." + }, "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del dispositiu de t\u00fanel." }, "routing": { "data": { - "individual_address": "Adre\u00e7a individual de la connexi\u00f3 d'encaminament", - "local_ip": "IP local de Home Assistant (deixa-ho en blanc si no n'est\u00e0s segur/a)", - "multicast_group": "Grup de multidifusi\u00f3 utilitzat per a l'encaminament", - "multicast_port": "Port de multidifusi\u00f3 utilitzat per a l'encaminament" + "individual_address": "Adre\u00e7a individual", + "local_ip": "IP local de Home Assistant", + "multicast_group": "Grup multidifusi\u00f3", + "multicast_port": "Port multidifusi\u00f3" + }, + "data_description": { + "individual_address": "Adre\u00e7a KNX per utilitzar amb Home Assistant, p. ex. `0.0.4`", + "local_ip": "Deixa-ho en blanc per utilitzar el descobriment autom\u00e0tic." }, "description": "Configura les opcions d'encaminament." }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Nom del teu fitxer `.knxkeys` (inclosa l'extensi\u00f3)", + "knxkeys_password": "Contrasenya per desxifrar el fitxer `.knxkeys`." + }, + "data_description": { + "knxkeys_filename": "S'espera que el fitxer es trobi al teu directori de configuraci\u00f3 a `.storage/knx/`.\nA Home Assistant aix\u00f2 estaria a `/config/.storage/knx/`\nExemple: `el_meu_projecte.knxkeys`", + "knxkeys_password": "S'ha definit durant l'exportaci\u00f3 del fitxer des d'ETS." + }, + "description": "Introdueix la informaci\u00f3 del teu fitxer `.knxkeys`." + }, + "secure_manual": { + "data": { + "device_authentication": "Contrasenya d'autenticaci\u00f3 del dispositiu", + "user_id": "ID d'usuari", + "user_password": "Contrasenya d'usuari" + }, + "data_description": { + "device_authentication": "S'estableix al panell 'IP' de la interf\u00edcie d'ETS.", + "user_id": "Sovint \u00e9s el n\u00famero del t\u00fanel +1. Per tant, 'T\u00fanel 2' tindria l'ID d'usuari '3'.", + "user_password": "Contrasenya per a la connexi\u00f3 t\u00fanel espec\u00edfica configurada al panell 'Propietats' del t\u00fanel a ETS." + }, + "description": "Introdueix la informaci\u00f3 de seguretat IP (IP Secure)." + }, + "secure_tunneling": { + "description": "Selecciona com vols configurar KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Utilitza un fitxer `.knxkeys` que contingui les claus de seguretat IP (IP Secure)", + "secure_manual": "Configura manualment les claus de seguretat IP (IP Secure)" + } + }, "tunnel": { "data": { "gateway": "Connexi\u00f3 t\u00fanel KNX" @@ -48,11 +92,19 @@ "data": { "connection_type": "Tipus de connexi\u00f3 KNX", "individual_address": "Adre\u00e7a individual predeterminada", - "local_ip": "IP local de Home Assistant (utilitza 0.0.0.0 per a detecci\u00f3 autom\u00e0tica)", - "multicast_group": "Grup de multidifusi\u00f3 utilitzat per a encaminament i descobriment", - "multicast_port": "Port de multidifusi\u00f3 utilitzat per a encaminament i descobriment", - "rate_limit": "Telegrames de sortida m\u00e0xims per segon", - "state_updater": "Habilita la lectura global d'estats del bus KNX" + "local_ip": "IP local de Home Assistant", + "multicast_group": "Grup multidifusi\u00f3", + "multicast_port": "Port multidifusi\u00f3", + "rate_limit": "Freq\u00fc\u00e8ncia m\u00e0xima", + "state_updater": "Actualitzador d'estat" + }, + "data_description": { + "individual_address": "Adre\u00e7a KNX per utilitzar amb Home Assistant, p. ex. `0.0.4`", + "local_ip": "Utilitza `0.0.0.0` per al descobriment autom\u00e0tic.", + "multicast_group": "Utilitzada per a l'encaminament i el descobriment. Per defecte: `224.0.23.12`", + "multicast_port": "Utilitzat per a l'encaminament i el descobriment. Per defecte: `3671`", + "rate_limit": "Telegrames de sortida m\u00e0xims per segon.\nRecomanat: de 20 a 40", + "state_updater": "Activa o desactiva globalment la lectura d'estats del bus KNX. Si est\u00e0 desactivat, Home Assistant no obtindr\u00e0 activament els estats del bus KNX, les opcions d'entitat `sync_state` no tindran cap efecte." } }, "tunnel": { @@ -62,6 +114,10 @@ "port": "Port", "route_back": "Encaminament de retorn / Mode NAT", "tunneling_type": "Tipus de t\u00fanel KNX" + }, + "data_description": { + "host": "Adre\u00e7a IP del dispositiu de tunelitzaci\u00f3 KNX/IP.", + "port": "Port del dispositiu de tunelitzaci\u00f3 KNX/IP." } } } diff --git a/homeassistant/components/knx/translations/de.json b/homeassistant/components/knx/translations/de.json index 2f7795153dc..588a853a8b6 100644 --- a/homeassistant/components/knx/translations/de.json +++ b/homeassistant/components/knx/translations/de.json @@ -5,34 +5,78 @@ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "file_not_found": "Die angegebene `.knxkeys`-Datei wurde im Pfad config/.storage/knx/ nicht gefunden.", + "invalid_individual_address": "Wert ist keine g\u00fcltige physikalische Adresse. 'Bereich.Linie.Teilnehmer'", + "invalid_ip_address": "Ung\u00fcltige IPv4 Adresse.", + "invalid_signature": "Das Passwort zum Entschl\u00fcsseln der `.knxkeys`-Datei ist ung\u00fcltig." }, "step": { "manual_tunnel": { "data": { "host": "Host", "individual_address": "Physikalische Adresse f\u00fcr die Verbindung", - "local_ip": "Lokale IP von Home Assistant (f\u00fcr automatische Erkennung leer lassen)", + "local_ip": "Lokale IP von Home Assistant", "port": "Port", "route_back": "Route Back / NAT-Modus", "tunneling_type": "KNX Tunneling Typ" }, - "description": "Bitte gib die Verbindungsinformationen deines Tunnelger\u00e4ts ein." + "data_description": { + "host": "IP-Adresse der KNX/IP-Tunneling Schnittstelle.", + "local_ip": "Lasse das Feld leer, um die automatische Erkennung zu verwenden.", + "port": "Port der KNX/IP-Tunneling Schnittstelle." + }, + "description": "Bitte gib die Verbindungsinformationen deiner Tunnel-Schnittstelle ein." }, "routing": { "data": { - "individual_address": "Physikalische Adresse f\u00fcr die Routingverbindung", - "local_ip": "Lokale IP von Home Assistant (f\u00fcr automatische Erkennung leer lassen)", - "multicast_group": "Die f\u00fcr das Routing verwendete Multicast-Gruppe", - "multicast_port": "Der f\u00fcr das Routing verwendete Multicast-Port" + "individual_address": "Physikalische Adresse", + "local_ip": "Lokale IP von Home Assistant", + "multicast_group": "Multicast-Gruppe", + "multicast_port": "Multicast-Port" + }, + "data_description": { + "individual_address": "Physikalische Adresse, die von Home Assistant verwendet werden soll, z. B. \u201e0.0.4\u201c.", + "local_ip": "Lasse das Feld leer, um die automatische Erkennung zu verwenden." }, "description": "Bitte konfiguriere die Routing-Optionen." }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Der Dateiname deiner `.knxkeys`-Datei (einschlie\u00dflich Erweiterung)", + "knxkeys_password": "Das Passwort zum Entschl\u00fcsseln der `.knxkeys`-Datei" + }, + "data_description": { + "knxkeys_filename": "Die Datei wird in deinem Konfigurationsverzeichnis unter `.storage/knx/` erwartet.\nIm Home Assistant OS w\u00e4re dies `/config/.storage/knx/`\nBeispiel: `my_project.knxkeys`", + "knxkeys_password": "Dies wurde beim Exportieren der Datei aus ETS gesetzt." + }, + "description": "Bitte gib die Informationen f\u00fcr deine `.knxkeys`-Datei ein." + }, + "secure_manual": { + "data": { + "device_authentication": "Ger\u00e4te-Authentifizierungscode", + "user_id": "Benutzer-ID", + "user_password": "Benutzer-Passwort" + }, + "data_description": { + "device_authentication": "Dies wird im Feld \"IP\" der Schnittstelle in ETS eingestellt.", + "user_id": "Dies ist oft die Tunnelnummer +1. \u201eTunnel 2\u201c h\u00e4tte also die Benutzer-ID \u201e3\u201c.", + "user_password": "Passwort f\u00fcr die spezifische Tunnelverbindung, die im Bereich \u201eEigenschaften\u201c des Tunnels in ETS festgelegt wurde." + }, + "description": "Bitte gib deine IP-Secure Informationen ein." + }, + "secure_tunneling": { + "description": "W\u00e4hle aus, wie du KNX/IP-Secure konfigurieren m\u00f6chtest.", + "menu_options": { + "secure_knxkeys": "Verwende eine `.knxkeys`-Datei, die IP-Secure-Schl\u00fcssel enth\u00e4lt", + "secure_manual": "IP-Secure Schl\u00fcssel manuell konfigurieren" + } + }, "tunnel": { "data": { "gateway": "KNX Tunnel Verbindung" }, - "description": "Bitte w\u00e4hle ein Gateway aus der Liste aus." + "description": "Bitte w\u00e4hle eine Schnittstelle aus der Liste aus." }, "type": { "data": { @@ -48,11 +92,19 @@ "data": { "connection_type": "KNX-Verbindungstyp", "individual_address": "Standard physikalische Adresse", - "local_ip": "Lokale IP von Home Assistant (verwende 0.0.0.0 f\u00fcr automatische Erkennung)", - "multicast_group": "Multicast-Gruppe f\u00fcr Routing und Discovery", - "multicast_port": "Multicast-Port f\u00fcr Routing und Discovery", - "rate_limit": "Maximal ausgehende Telegramme pro Sekunde", - "state_updater": "Lesen von Zust\u00e4nden von dem KNX Bus global freigeben" + "local_ip": "Lokale IP von Home Assistant", + "multicast_group": "Multicast-Gruppe", + "multicast_port": "Multicast-Port", + "rate_limit": "Telegrammdrossel", + "state_updater": "Status-Updater" + }, + "data_description": { + "individual_address": "Physikalische Adresse, die von Home Assistant verwendet werden soll, z.\u00a0B. \u201e0.0.4\u201c.", + "local_ip": "Verwende \"0.0.0.0\" f\u00fcr die automatische Erkennung.", + "multicast_group": "Wird f\u00fcr Routing und Netzwerkerkennung verwendet. Standard: `224.0.23.12`", + "multicast_port": "Wird f\u00fcr Routing und Netzwerkerkennung verwendet. Standard: \u201e3671\u201c.", + "rate_limit": "Maximal gesendete Telegramme pro Sekunde.\nEmpfohlen: 20 bis 40", + "state_updater": "Aktiviere oder deaktiviere global das Lesen von Zust\u00e4nden vom KNX-Bus. Wenn deaktiviert, ruft Home Assistant nicht aktiv Zust\u00e4nde vom KNX-Bus ab, \u201esync_state\u201c-Entity-Optionen haben keine Auswirkung." } }, "tunnel": { @@ -62,6 +114,10 @@ "port": "Port", "route_back": "Route Back / NAT-Modus", "tunneling_type": "KNX Tunneling Typ" + }, + "data_description": { + "host": "IP-Adresse der KNX/IP-Tunneling Schnittstelle.", + "port": "Port der KNX/IP-Tunneling Schnittstelle." } } } diff --git a/homeassistant/components/knx/translations/el.json b/homeassistant/components/knx/translations/el.json index 75be33560ca..81dd03f73fa 100644 --- a/homeassistant/components/knx/translations/el.json +++ b/homeassistant/components/knx/translations/el.json @@ -5,7 +5,11 @@ "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." }, "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "file_not_found": "\u03a4\u03bf \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf knxkeys \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae config/.storage/knx/", + "invalid_individual_address": "\u0397 \u03c4\u03b9\u03bc\u03ae \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03b5\u03b9 \u03bc\u03b5 \u03c4\u03bf \u03bc\u03bf\u03c4\u03af\u03b2\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03bc\u03b5\u03bc\u03bf\u03bd\u03c9\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 KNX.\n \"area.line.device\"", + "invalid_ip_address": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IPv4.", + "invalid_signature": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 knxkeys \u03b5\u03af\u03bd\u03b1\u03b9 \u03bb\u03ac\u03b8\u03bf\u03c2." }, "step": { "manual_tunnel": { @@ -17,6 +21,11 @@ "route_back": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 Route Back / NAT", "tunneling_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX" }, + "data_description": { + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b9\u03bf\u03c7\u03ad\u03c4\u03b5\u03c5\u03c3\u03b7\u03c2 KNX/IP.", + "local_ip": "\u0391\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7.", + "port": "\u0398\u03cd\u03c1\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b9\u03bf\u03c7\u03ad\u03c4\u03b5\u03c5\u03c3\u03b7\u03c2 KNX/IP." + }, "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03ac\u03c2 \u03c3\u03b1\u03c2." }, "routing": { @@ -26,8 +35,43 @@ "multicast_group": "\u0397 \u03bf\u03bc\u03ac\u03b4\u03b1 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ae\u03c2 \u03b5\u03ba\u03c0\u03bf\u03bc\u03c0\u03ae\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7", "multicast_port": "\u0397 \u03b8\u03cd\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ae\u03c2 \u03b4\u03b9\u03b1\u03bd\u03bf\u03bc\u03ae\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7" }, + "data_description": { + "individual_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 KNX \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant, \u03c0.\u03c7. `0.0.4`.", + "local_ip": "\u0391\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7." + }, "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2." }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "\u03a4\u03bf \u03c0\u03bb\u03ae\u03c1\u03b5\u03c2 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 knxkeys \u03c3\u03b1\u03c2", + "knxkeys_password": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 knxkeys" + }, + "data_description": { + "knxkeys_filename": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03b1\u03bd\u03b1\u03bc\u03ad\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03bf\u03bd \u03ba\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd \u03c3\u03c4\u03bf `.storage/knx/`.\n \u03a3\u03c4\u03bf Home Assistant OS \u03b1\u03c5\u03c4\u03cc \u03b8\u03b1 \u03ae\u03c4\u03b1\u03bd `/config/.storage/knx/`\n \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: `my_project.knxkeys`", + "knxkeys_password": "\u0391\u03c5\u03c4\u03cc \u03bf\u03c1\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03be\u03b1\u03b3\u03c9\u03b3\u03ae \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 \u03b1\u03c0\u03cc \u03c4\u03bf ETS." + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf knxkeys \u03c3\u03b1\u03c2." + }, + "secure_manual": { + "data": { + "device_authentication": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", + "user_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", + "user_password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "data_description": { + "device_authentication": "\u0391\u03c5\u03c4\u03cc \u03bf\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf\u03bd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u00abIP\u00bb \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae\u03c2 \u03c3\u03c4\u03bf ETS.", + "user_id": "\u0391\u03c5\u03c4\u03cc \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c7\u03bd\u03ac \u03c4\u03bf \u03bd\u03bf\u03cd\u03bc\u03b5\u03c1\u03bf +1 \u03c4\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2. \u0388\u03c4\u03c3\u03b9, \u03b7 '\u03a3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1 2' \u03b8\u03b1 \u03ad\u03c7\u03b5\u03b9 User-ID '3'.", + "user_password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03bf\u03bd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \"\u0399\u03b4\u03b9\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\" \u03c4\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 \u03c3\u03c4\u03bf ETS." + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP secure." + }, + "secure_tunneling": { + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03ce\u03c2 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf IP Secure.", + "menu_options": { + "secure_knxkeys": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf knxkeys \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP secure", + "secure_manual": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 IP secure \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1" + } + }, "tunnel": { "data": { "gateway": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX" @@ -47,12 +91,20 @@ "init": { "data": { "connection_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 KNX", - "individual_address": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b1\u03c4\u03bf\u03bc\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7", + "individual_address": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03c6\u03c5\u03c3\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7", "local_ip": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae IP \u03c4\u03bf\u03c5 Home Assistant (\u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 0.0.0.0.0 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7)", "multicast_group": "\u039f\u03bc\u03ac\u03b4\u03b1 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ae\u03c2 \u03b4\u03b9\u03b1\u03bd\u03bf\u03bc\u03ae\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7", "multicast_port": "\u0398\u03cd\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ae\u03c2 \u03b4\u03b9\u03b1\u03bd\u03bf\u03bc\u03ae\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7", "rate_limit": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03b1 \u03b5\u03be\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03b1 \u03c4\u03b7\u03bb\u03b5\u03b3\u03c1\u03b1\u03c6\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b1\u03bd\u03ac \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03bf", "state_updater": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b1\u03b8\u03bf\u03bb\u03b9\u03ba\u03ac \u03c4\u03b9\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03bf KNX Bus" + }, + "data_description": { + "individual_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 KNX \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant, \u03c0.\u03c7. `0.0.4`.", + "local_ip": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 `0.0.0.0.0` \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7.", + "multicast_group": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7. \u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae: `224.0.23.12`", + "multicast_port": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7. \u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae: `3671`", + "rate_limit": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03b1 \u03b5\u03be\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03b1 \u03c4\u03b7\u03bb\u03b5\u03b3\u03c1\u03b1\u03c6\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b1\u03bd\u03ac \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03bf.\n \u03a0\u03c1\u03bf\u03c4\u03b5\u03af\u03bd\u03b5\u03c4\u03b1\u03b9: 20 \u03ad\u03c9\u03c2 40", + "state_updater": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03ae \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03c9\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX. \u038c\u03c4\u03b1\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf, \u03c4\u03bf Home Assistant \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03ac \u03b5\u03bd\u03b5\u03c1\u03b3\u03ac \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2 \u03b1\u03c0\u03cc \u03c4\u03bf KNX Bus, \u03bf\u03b9 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 `sync_state` \u03b4\u03b5\u03bd \u03b8\u03b1 \u03ad\u03c7\u03bf\u03c5\u03bd \u03ba\u03b1\u03bc\u03af\u03b1 \u03b5\u03c0\u03af\u03b4\u03c1\u03b1\u03c3\u03b7." } }, "tunnel": { @@ -62,6 +114,10 @@ "port": "\u0398\u03cd\u03c1\u03b1", "route_back": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 Route Back / NAT", "tunneling_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX" + }, + "data_description": { + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b9\u03bf\u03c7\u03ad\u03c4\u03b5\u03c5\u03c3\u03b7\u03c2 KNX/IP.", + "port": "\u0398\u03cd\u03c1\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b9\u03bf\u03c7\u03ad\u03c4\u03b5\u03c5\u03c3\u03b7\u03c2 KNX/IP." } } } diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index 538f6b6a5c6..ba073eba888 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Failed to connect", "file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/", - "invalid_individual_address": "Value does not match pattern for KNX individual address.\n'..'", + "invalid_individual_address": "Value does not match pattern for KNX individual address.\n'area.line.device'", "invalid_ip_address": "Invalid IPv4 address.", "invalid_signature": "The password to decrypt the `.knxkeys` file is wrong." }, @@ -15,8 +15,10 @@ "manual_tunnel": { "data": { "host": "Host", + "individual_address": "Individual address for the connection", "local_ip": "Local IP of Home Assistant", "port": "Port", + "route_back": "Route Back / NAT Mode", "tunneling_type": "KNX Tunneling Type" }, "data_description": { @@ -108,7 +110,9 @@ "tunnel": { "data": { "host": "Host", + "local_ip": "Local IP (leave empty if unsure)", "port": "Port", + "route_back": "Route Back / NAT Mode", "tunneling_type": "KNX Tunneling Type" }, "data_description": { diff --git a/homeassistant/components/knx/translations/et.json b/homeassistant/components/knx/translations/et.json index e9417cdac37..c4410d490e4 100644 --- a/homeassistant/components/knx/translations/et.json +++ b/homeassistant/components/knx/translations/et.json @@ -5,29 +5,73 @@ "single_instance_allowed": "Juba seadistatud. Lubatud on ainult \u00fcks sidumine." }, "error": { - "cannot_connect": "\u00dchendamine nurjus" + "cannot_connect": "\u00dchendamine nurjus", + "file_not_found": "M\u00e4\u00e4ratud faili \".knxkeys\" ei leitud asukohas config/.storage/knx/", + "invalid_individual_address": "V\u00e4\u00e4rtus ei \u00fchti KNX-i individuaalse aadressi mustriga.\n 'area.line.device'", + "invalid_ip_address": "Kehtetu IPv4 aadress.", + "invalid_signature": "Parool faili `.knxkeys` dekr\u00fcpteerimiseks on vale." }, "step": { "manual_tunnel": { "data": { "host": "Host", "individual_address": "\u00dchenduse individuaalne aadress", - "local_ip": "Home Assistanti kohalik IP (automaatseks tuvastuseks j\u00e4ta t\u00fchjaks)", + "local_ip": "Home Assistanti kohalik IP aadress", "port": "Port", "route_back": "Marsruudi tagasitee / NAT-re\u017eiim", "tunneling_type": "KNX tunneli t\u00fc\u00fcp" }, + "data_description": { + "host": "KNX/IP tunneldusseadme IP-aadress.", + "local_ip": "Automaatse avastamise kasutamiseks j\u00e4ta t\u00fchjaks.", + "port": "KNX/IP-tunneldusseadme port." + }, "description": "Sisesta tunneldamisseadme \u00fchenduse teave." }, "routing": { "data": { - "individual_address": "Marsruutimis\u00fchenduse individuaalne aadress", - "local_ip": "Home Assistanti kohalik IP (automaatseks tuvastuseks j\u00e4ta t\u00fchjaks)", - "multicast_group": "Marsruutimiseks kasutatav multisaater\u00fchm", - "multicast_port": "Marsruutimiseks kasutatav multisaateport" + "individual_address": "Individuaalne aadress", + "local_ip": "Home Assistanti kohalik IP aadress", + "multicast_group": "Multicast grupp", + "multicast_port": "Mulicasti port" + }, + "data_description": { + "individual_address": "Home Assistantis kasutatav KNX-aadress, nt \"0.0.4\".", + "local_ip": "Automaatse avastamise kasutamiseks j\u00e4ta t\u00fchjaks." }, "description": "Konfigureeri marsruutimissuvandid." }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "`.nxkeys` faili t\u00e4ielik nimi (koos laiendiga)", + "knxkeys_password": "Parool `.knxkeys` faili dekr\u00fcpteerimiseks" + }, + "data_description": { + "knxkeys_filename": "Eeldatakse, et fail asub konfiguratsioonikataloogis kaustas \".storage/knx/\".\nHome Assistant OS-is oleks see `/config/.storage/knx/`\n N\u00e4ide: \"minu_projekt.knxkeys\".", + "knxkeys_password": "See m\u00e4\u00e4rati faili eksportimisel ETSist." + }, + "description": "Sisesta oma `.knxkeys` faili teave." + }, + "secure_manual": { + "data": { + "device_authentication": "Seadme autentimise parool", + "user_id": "Kasutaja ID", + "user_password": "Kasutaja salas\u00f5na" + }, + "data_description": { + "device_authentication": "See m\u00e4\u00e4ratakse ETSi liidese IP-paneelil.", + "user_id": "See on sageli tunneli number +1. Nii et tunnel 2 oleks kasutaja ID-ga 3.", + "user_password": "Konkreetse tunneli\u00fchenduse parool, mis on m\u00e4\u00e4ratud ETS-i tunneli paneelil \u201eAtribuudid\u201d." + }, + "description": "Sisesta IP Secure teave." + }, + "secure_tunneling": { + "description": "Vali kuidas soovid KNX/IP Secure'i seadistada.", + "menu_options": { + "secure_knxkeys": "Kasuta knxkeys fail, mis sisaldab IP Secure teavet.", + "secure_manual": "IP Secure v\u00f5tmete k\u00e4sitsi seadistamine" + } + }, "tunnel": { "data": { "gateway": "KNX tunneli \u00fchendus" @@ -48,11 +92,19 @@ "data": { "connection_type": "KNX \u00fchenduse t\u00fc\u00fcp", "individual_address": "Vaikimisi individuaalne aadress", - "local_ip": "Home Assistanti kohalik IP (sisesta 0.0.0.0 automaatseks tuvastuseks)", - "multicast_group": "Marsruutimiseks ja avastamiseks kasutatav multisaategrupp", - "multicast_port": "Marsruutimiseks ja avastamiseks kasutatav multisaateport", - "rate_limit": "Maksimaalne v\u00e4ljaminevate teavituste arv sekundis", - "state_updater": "Luba globaalselt seisundi lugemine KNX-siinilt" + "local_ip": "Home Assistanti kohalik IP aadress", + "multicast_group": "Multicast grupp", + "multicast_port": "Mulicasti port", + "rate_limit": "Teavituste m\u00e4\u00e4r", + "state_updater": "Oleku uuendaja" + }, + "data_description": { + "individual_address": "Home Assistantis kasutatav KNX-aadress, nt \"0.0.4\".", + "local_ip": "Automaatse tuvastamise jaoks kasuta `0.0.0.0.0`.", + "multicast_group": "Kasutatakse marsruutimiseks ja avastamiseks. Vaikimisi: \"224.0.23.12\"", + "multicast_port": "Kasutatakse marsruutimiseks ja avastamiseks. Vaikev\u00e4\u00e4rtus: \"3671\"", + "rate_limit": "Maksimaalne v\u00e4ljaminevate telegrammide arv sekundis.\nSoovitatav: 20 kuni 40", + "state_updater": "KNX siini lugemisolekute globaalne lubamine v\u00f5i keelamine. Kui see on keelatud, ei too Home Assistant aktiivselt olekuid KNX siinilt, olemi s\u00fcnkroonimisoleku valikudi ei m\u00f5juta." } }, "tunnel": { @@ -62,6 +114,10 @@ "port": "Port", "route_back": "Marsruudi tagasitee / NAT-re\u017eiim", "tunneling_type": "KNX tunneli t\u00fc\u00fcp" + }, + "data_description": { + "host": "KNX/IP tunneldusseadme IP-aadress.", + "port": "KNX/IP-tunneldusseadme port." } } } diff --git a/homeassistant/components/knx/translations/fr.json b/homeassistant/components/knx/translations/fr.json index 31221e900cd..c75fd76debb 100644 --- a/homeassistant/components/knx/translations/fr.json +++ b/homeassistant/components/knx/translations/fr.json @@ -5,29 +5,73 @@ "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { - "cannot_connect": "\u00c9chec de connexion" + "cannot_connect": "\u00c9chec de connexion", + "file_not_found": "Le fichier `.knxkeys` sp\u00e9cifi\u00e9 n'a pas \u00e9t\u00e9 trouv\u00e9 dans config/.storage/knx/", + "invalid_individual_address": "La valeur de l'adresse individuelle KNX ne correspond pas au mod\u00e8le.\n'area.line.device'", + "invalid_ip_address": "Adresse IPv4 non valide.", + "invalid_signature": "Le mot de passe pour d\u00e9chiffrer le fichier `.knxkeys` est erron\u00e9." }, "step": { "manual_tunnel": { "data": { "host": "H\u00f4te", "individual_address": "Adresse individuelle pour la connexion", - "local_ip": "IP locale (laisser vide en cas de doute)", + "local_ip": "IP locale de Home Assistant", "port": "Port", "route_back": "Retour/Mode NAT", "tunneling_type": "Type de tunnel KNX" }, + "data_description": { + "host": "Adresse IP de l'appareil de tunnel KNX/IP.", + "local_ip": "Laissez le champ vide pour utiliser la d\u00e9couverte automatique.", + "port": "Port de l'appareil de tunnel KNX/IP." + }, "description": "Veuillez saisir les informations de connexion de votre appareil de cr\u00e9ation de tunnel." }, "routing": { "data": { - "individual_address": "Adresse individuelle pour la connexion de routage", - "local_ip": "IP locale (laisser vide en cas de doute)", - "multicast_group": "Le groupe multicast utilis\u00e9 pour le routage", - "multicast_port": "Le port multicast utilis\u00e9 pour le routage" + "individual_address": "Adresse individuelle", + "local_ip": "IP locale de Home Assistant", + "multicast_group": "Groupe multicast", + "multicast_port": "Port multicast" + }, + "data_description": { + "individual_address": "Adresse KNX que Home Assistant doit utiliser, par exemple `0.0.4`.", + "local_ip": "Laissez le champ vide pour utiliser la d\u00e9couverte automatique." }, "description": "Veuillez configurer les options de routage." }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Le nom de votre fichier `.knxkeys` (extension incluse)", + "knxkeys_password": "Le mot de passe pour d\u00e9chiffrer le fichier `.knxkeys`" + }, + "data_description": { + "knxkeys_filename": "Le fichier devrait se trouver dans votre r\u00e9pertoire de configuration dans `.storage/knx/`.\nSous Home Assistant OS, il s'agirait de `/config/.storage/knx/`\nPar exemple\u00a0: `my_project.knxkeys`", + "knxkeys_password": "D\u00e9fini lors de l'exportation du fichier depuis ETS." + }, + "description": "Veuillez saisir les informations relatives \u00e0 votre fichier `.knxkeys`." + }, + "secure_manual": { + "data": { + "device_authentication": "Mot de passe d'authentification de l'appareil", + "user_id": "ID de l'utilisateur", + "user_password": "Mot de passe de l'utilisateur" + }, + "data_description": { + "device_authentication": "D\u00e9fini dans le panneau \u00ab\u00a0IP\u00a0\u00bb de l'interface dans ETS.", + "user_id": "G\u00e9n\u00e9ralement le num\u00e9ro du tunnel +\u00a01. Par exemple, \u00ab\u00a0Tunnel 2\u00a0\u00bb aurait l'ID utilisateur \u00ab\u00a03\u00a0\u00bb.", + "user_password": "Mot de passe pour la connexion de tunnel sp\u00e9cifique, d\u00e9fini dans le panneau \u00ab\u00a0Propri\u00e9t\u00e9s\u00a0\u00bb du tunnel dans ETS." + }, + "description": "Veuillez saisir vos informations de s\u00e9curit\u00e9 IP." + }, + "secure_tunneling": { + "description": "S\u00e9lectionnez la mani\u00e8re dont vous souhaitez configurer la s\u00e9curit\u00e9 IP de KNX.", + "menu_options": { + "secure_knxkeys": "Utiliser un fichier `.knxkeys` contenant les cl\u00e9s de s\u00e9curit\u00e9 IP", + "secure_manual": "Configurer manuellement les cl\u00e9s de s\u00e9curit\u00e9 IP" + } + }, "tunnel": { "data": { "gateway": "Connexion tunnel KNX" @@ -48,11 +92,19 @@ "data": { "connection_type": "Type de connexion KNX", "individual_address": "Adresse individuelle par d\u00e9faut", - "local_ip": "IP locale de Home Assistant (utilisez 0.0.0.0 pour la d\u00e9tection automatique)", - "multicast_group": "Groupe de multidiffusion utilis\u00e9 pour le routage et la d\u00e9couverte", - "multicast_port": "Port de multidiffusion utilis\u00e9 pour le routage et la d\u00e9couverte", - "rate_limit": "Nombre maximal de t\u00e9l\u00e9grammes sortants par seconde", - "state_updater": "Activer globalement la lecture des \u00e9tats depuis le bus KNX" + "local_ip": "IP locale de Home Assistant", + "multicast_group": "Groupe multicast", + "multicast_port": "Port multicast", + "rate_limit": "Limite d'envoi", + "state_updater": "Mises \u00e0 jour d'\u00e9tat" + }, + "data_description": { + "individual_address": "Adresse KNX que Home Assistant doit utiliser, par exemple `0.0.4`.", + "local_ip": "Utilisez `0.0.0.0` pour la d\u00e9couverte automatique.", + "multicast_group": "Utilis\u00e9 pour le routage et la d\u00e9couverte. Valeur par d\u00e9faut\u00a0: `224.0.23.12`", + "multicast_port": "Utilis\u00e9 pour le routage et la d\u00e9couverte. Valeur par d\u00e9faut\u00a0: `3671`", + "rate_limit": "Nombre maximal de t\u00e9l\u00e9grammes sortants par seconde.\nValeur recommand\u00e9e\u00a0: entre 20 et 40", + "state_updater": "Active ou d\u00e9sactive globalement la lecture des \u00e9tats depuis le bus KNX. Lorsqu'elle est d\u00e9sactiv\u00e9e, Home Assistant ne r\u00e9cup\u00e8re pas activement les \u00e9tats depuis le bus KNX et les options d'entit\u00e9 `sync_state` n'ont aucun effet." } }, "tunnel": { @@ -62,6 +114,10 @@ "port": "Port", "route_back": "Retour/Mode NAT", "tunneling_type": "Type de tunnel KNX" + }, + "data_description": { + "host": "Adresse IP de l'appareil de tunnel KNX/IP.", + "port": "Port de l'appareil de tunnel KNX/IP." } } } diff --git a/homeassistant/components/knx/translations/hu.json b/homeassistant/components/knx/translations/hu.json index 13bd4650378..02b8b0a0465 100644 --- a/homeassistant/components/knx/translations/hu.json +++ b/homeassistant/components/knx/translations/hu.json @@ -5,29 +5,73 @@ "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "file_not_found": "A megadott '.knxkeys' f\u00e1jl nem tal\u00e1lhat\u00f3 a config/.storage/knx/ el\u00e9r\u00e9si \u00fatvonalon.", + "invalid_individual_address": "Az \u00e9rt\u00e9k nem felel meg a KNX egyedi c\u00edm mint\u00e1j\u00e1nak.\n'area.line.device'", + "invalid_ip_address": "\u00c9rv\u00e9nytelen IPv4-c\u00edm.", + "invalid_signature": "A '.knxkeys' f\u00e1jl visszafejt\u00e9s\u00e9hez haszn\u00e1lt jelsz\u00f3 helytelen." }, "step": { "manual_tunnel": { "data": { "host": "C\u00edm", "individual_address": "A kapcsolat egy\u00e9ni c\u00edme", - "local_ip": "Helyi IP c\u00edm (hagyja \u00fcresen, ha nem biztos benne)", + "local_ip": "Home Assistant lok\u00e1lis IP c\u00edme", "port": "Port", "route_back": "\u00datvonal (route) vissza / NAT m\u00f3d", "tunneling_type": "KNX alag\u00fat t\u00edpusa" }, + "data_description": { + "host": "A KNX/IP tunnel eszk\u00f6z IP-c\u00edme.", + "local_ip": "Az automatikus felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz hagyja \u00fcresen.", + "port": "A KNX/IP tunnel eszk\u00f6z portsz\u00e1ma." + }, "description": "Adja meg az alag\u00fatkezel\u0151 (tunneling) eszk\u00f6z csatlakoz\u00e1si adatait." }, "routing": { "data": { - "individual_address": "Az \u00fatv\u00e1laszt\u00e1si (routing) kapcsolat egy\u00e9ni c\u00edme", - "local_ip": "Helyi IP c\u00edm (hagyja \u00fcresen, ha nem biztos benne)", - "multicast_group": "Az \u00fatv\u00e1laszt\u00e1shoz haszn\u00e1lt multicast csoport", - "multicast_port": "Az \u00fatv\u00e1laszt\u00e1shoz haszn\u00e1lt multicast portsz\u00e1m" + "individual_address": "Egy\u00e9ni c\u00edm", + "local_ip": "Home Assistant lok\u00e1lis IP c\u00edme", + "multicast_group": "Multicast csoport", + "multicast_port": "Multicast portsz\u00e1m" + }, + "data_description": { + "individual_address": "A Home Assistant \u00e1ltal haszn\u00e1land\u00f3 KNX-c\u00edm, pl. \"0.0.4\".", + "local_ip": "Az automatikus felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz hagyja \u00fcresen." }, "description": "K\u00e9rem, konfigur\u00e1lja az \u00fatv\u00e1laszt\u00e1si (routing) be\u00e1ll\u00edt\u00e1sokat." }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "A '.knxkeys' f\u00e1jl teljes neve (kiterjeszt\u00e9ssel)", + "knxkeys_password": "A '.knxkeys' f\u00e1jl visszafejt\u00e9s\u00e9hez sz\u00fcks\u00e9ges jelsz\u00f3" + }, + "data_description": { + "knxkeys_filename": "A f\u00e1jl a `.storage/knx/` konfigur\u00e1ci\u00f3s k\u00f6nyvt\u00e1r\u00e1ban helyezend\u0151.\nHome Assistant oper\u00e1ci\u00f3s rendszer eset\u00e9n ez a k\u00f6vetkez\u0151 lenne: `/config/.storage/knx/`\nP\u00e9lda: \"my_project.knxkeys\".", + "knxkeys_password": "Ez a be\u00e1ll\u00edt\u00e1s a f\u00e1jl ETS-b\u0151l t\u00f6rt\u00e9n\u0151 export\u00e1l\u00e1sakor t\u00f6rt\u00e9nt." + }, + "description": "K\u00e9rj\u00fck, adja meg a '.knxkeys' f\u00e1jl adatait." + }, + "secure_manual": { + "data": { + "device_authentication": "Eszk\u00f6z hiteles\u00edt\u00e9si jelsz\u00f3", + "user_id": "Felhaszn\u00e1l\u00f3i azonos\u00edt\u00f3", + "user_password": "Felhaszn\u00e1l\u00f3i jelsz\u00f3" + }, + "data_description": { + "device_authentication": "Ezt az ETS-ben az interf\u00e9sz \"IP\" panelj\u00e9n kell be\u00e1ll\u00edtani.", + "user_id": "Ez gyakran a tunnel sz\u00e1ma +1. Teh\u00e1t a \"Tunnel 2\" felhaszn\u00e1l\u00f3i azonos\u00edt\u00f3ja \"3\".", + "user_password": "Jelsz\u00f3 az adott tunnelhez, amely a tunnel \u201eProperties\u201d panelj\u00e9n van be\u00e1ll\u00edtva az ETS-ben." + }, + "description": "K\u00e9rj\u00fck, adja meg az IP secure adatokat." + }, + "secure_tunneling": { + "description": "V\u00e1lassza ki, hogyan szeretn\u00e9 konfigur\u00e1lni az KNX/IP secure-t.", + "menu_options": { + "secure_knxkeys": "IP secure kulcsokat tartalmaz\u00f3 '.knxkeys' f\u00e1jl haszn\u00e1lata", + "secure_manual": "IP secure kulcsok manu\u00e1lis be\u00e1ll\u00edt\u00e1sa" + } + }, "tunnel": { "data": { "gateway": "KNX alag\u00fat (tunnel) kapcsolat" @@ -48,11 +92,19 @@ "data": { "connection_type": "KNX csatlakoz\u00e1s t\u00edpusa", "individual_address": "Alap\u00e9rtelmezett egy\u00e9ni c\u00edm", - "local_ip": "Helyi IP c\u00edm (hagyja \u00fcresen, ha nem biztos benne)", - "multicast_group": "\u00datv\u00e1laszt\u00e1shoz \u00e9s felder\u00edt\u00e9shez haszn\u00e1lt multicast csoport", - "multicast_port": "\u00datv\u00e1laszt\u00e1shoz \u00e9s felder\u00edt\u00e9shez haszn\u00e1lt multicast portsz\u00e1m\n", - "rate_limit": "Maxim\u00e1lis kimen\u0151 \u00fczenet darabsz\u00e1m m\u00e1sodpercenk\u00e9nt", - "state_updater": "Glob\u00e1lisan enged\u00e9lyezi az \u00e1llapotok olvas\u00e1s\u00e1t a KNX buszr\u00f3l." + "local_ip": "Home Assistant lok\u00e1lis IP c\u00edme", + "multicast_group": "Multicast csoport", + "multicast_port": "Multicast portsz\u00e1m", + "rate_limit": "Lek\u00e9r\u00e9si korl\u00e1toz\u00e1s", + "state_updater": "\u00c1llapot friss\u00edt\u0151" + }, + "data_description": { + "individual_address": "A Home Assistant \u00e1ltal haszn\u00e1land\u00f3 KNX-c\u00edm, pl. \"0.0.4\".", + "local_ip": "Haszn\u00e1lja a `0.0.0.0` c\u00edmet az automatikus felder\u00edt\u00e9shez.", + "multicast_group": "\u00datv\u00e1laszt\u00e1shoz \u00e9s felder\u00edt\u00e9shez haszn\u00e1latos. Alap\u00e9rtelmezett: `224.0.23.12`.", + "multicast_port": "\u00datv\u00e1laszt\u00e1shoz \u00e9s felder\u00edt\u00e9shez haszn\u00e1latos. Alap\u00e9rtelmezett: `3671`", + "rate_limit": "Maxim\u00e1lis kimen\u0151 \u00fczenet m\u00e1sodpercenk\u00e9nt.\nAj\u00e1nlott: 20 \u00e9s 40 k\u00f6z\u00f6tt", + "state_updater": "Glob\u00e1lisan enged\u00e9lyezze vagy tiltsa le az olvas\u00e1si \u00e1llapotokat a KNX-buszr\u00f3l. Ha le van tiltva, a Home Assistant nem fogja akt\u00edvan lek\u00e9rni az \u00e1llapotokat a KNX-buszr\u00f3l, a \"sync_state\" entit\u00e1sopci\u00f3knak nincs hat\u00e1sa." } }, "tunnel": { @@ -62,6 +114,10 @@ "port": "Port", "route_back": "\u00datvonal (route) vissza / NAT m\u00f3d", "tunneling_type": "KNX alag\u00fat t\u00edpusa" + }, + "data_description": { + "host": "A KNX/IP tunnel eszk\u00f6z IP-c\u00edme.", + "port": "A KNX/IP tunnel eszk\u00f6z portsz\u00e1ma." } } } diff --git a/homeassistant/components/knx/translations/id.json b/homeassistant/components/knx/translations/id.json index 1cdb614a3cc..92d812c1b1b 100644 --- a/homeassistant/components/knx/translations/id.json +++ b/homeassistant/components/knx/translations/id.json @@ -5,29 +5,73 @@ "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, "error": { - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "file_not_found": "File `.knxkeys` yang ditentukan tidak ditemukan di jalur config/.storage/knx/", + "invalid_individual_address": "Nilai tidak cocok dengan pola untuk alamat individual KNX.\n'area.line.device'", + "invalid_ip_address": "Alamat IPv4 tidak valid", + "invalid_signature": "Kata sandi untuk mendekripsi file `.knxkeys` salah." }, "step": { "manual_tunnel": { "data": { "host": "Host", "individual_address": "Alamat individu untuk koneksi", - "local_ip": "IP lokal Home Assistant (kosongkan jika tidak yakin)", + "local_ip": "IP lokal Home Assistant", "port": "Port", "route_back": "Dirutekan Kembali/Mode NAT", "tunneling_type": "Jenis Tunnel KNX" }, + "data_description": { + "host": "Alamat IP perangkat tunneling KNX/IP.", + "local_ip": "Kosongkan untuk menggunakan penemuan otomatis.", + "port": "Port perangkat tunneling KNX/IP." + }, "description": "Masukkan informasi koneksi untuk perangkat tunneling Anda." }, "routing": { "data": { - "individual_address": "Alamat individu untuk koneksi routing", - "local_ip": "IP lokal Home Assistant (kosongkan jika tidak yakin)", - "multicast_group": "Grup multicast yang digunakan untuk routing", - "multicast_port": "Port multicast yang digunakan untuk routing" + "individual_address": "Alamat individual", + "local_ip": "IP lokal Home Assistant", + "multicast_group": "Grup multicast", + "multicast_port": "Port multicast" + }, + "data_description": { + "individual_address": "Alamat KNX yang akan digunakan oleh Home Assistant, misalnya `0.0.4`", + "local_ip": "Kosongkan untuk menggunakan penemuan otomatis." }, "description": "Konfigurasikan opsi routing." }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Nama file '.knxkeys' Anda (termasuk ekstensi)", + "knxkeys_password": "Kata sandi untuk mendekripsi file `.knxkeys`" + }, + "data_description": { + "knxkeys_filename": "File diharapkan dapat ditemukan di direktori konfigurasi Anda di `.storage/knx/`.\nDi Home Assistant OS ini akan menjadi `/config/.storage/knx/`\nContoh: `proyek_saya.knxkeys`", + "knxkeys_password": "Ini disetel saat mengekspor file dari ETS." + }, + "description": "Masukkan informasi untuk file `.knxkeys` Anda." + }, + "secure_manual": { + "data": { + "device_authentication": "Kata sandi autentikasi perangkat", + "user_id": "ID pengguna", + "user_password": "Kata sandi pengguna" + }, + "data_description": { + "device_authentication": "Ini diatur dalam panel 'IP' dalam antarmuka di ETS.", + "user_id": "Ini sering kali merupakan tunnel nomor +1. Jadi 'Tunnel 2' akan memiliki User-ID '3'.", + "user_password": "Kata sandi untuk koneksi tunnel tertentu yang diatur di panel 'Properties' tunnel di ETS." + }, + "description": "Masukkan informasi IP aman Anda." + }, + "secure_tunneling": { + "description": "Pilih cara Anda ingin mengonfigurasi KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Gunakan file `.knxkeys` yang berisi kunci aman IP", + "secure_manual": "Konfigurasikan kunci aman IP secara manual" + } + }, "tunnel": { "data": { "gateway": "Koneksi Tunnel KNX" @@ -48,11 +92,19 @@ "data": { "connection_type": "Jenis Koneksi KNX", "individual_address": "Alamat individu default", - "local_ip": "IP lokal Home Assistant (gunakan 0.0.0.0 untuk deteksi otomatis)", - "multicast_group": "Grup multicast yang digunakan untuk routing dan penemuan", - "multicast_port": "Port multicast yang digunakan untuk routing dan penemuan", - "rate_limit": "Jumlah maksimal telegram keluar per detik", - "state_updater": "Aktifkan status membaca secara global dari KNX Bus" + "local_ip": "IP lokal Home Assistant", + "multicast_group": "Grup multicast", + "multicast_port": "Port multicast", + "rate_limit": "Batas data", + "state_updater": "Pembaruan status" + }, + "data_description": { + "individual_address": "Alamat KNX yang akan digunakan oleh Home Assistant, misalnya `0.0.4`", + "local_ip": "Gunakan `0.0.0.0` untuk penemuan otomatis.", + "multicast_group": "Digunakan untuk perutean dan penemuan. Bawaan: `224.0.23.12`", + "multicast_port": "Digunakan untuk perutean dan penemuan. Bawaan: `3671`", + "rate_limit": "Telegram keluar maksimum per detik.\nDirekomendasikan: 20 hingga 40", + "state_updater": "Secara global mengaktifkan atau menonaktifkan status pembacaan dari KNX Bus. Saat dinonaktifkan, Home Assistant tidak akan secara aktif mengambil status dari KNX Bus, opsi entitas 'sync_state' tidak akan berpengaruh." } }, "tunnel": { @@ -62,6 +114,10 @@ "port": "Port", "route_back": "Dirutekan Kembali/Mode NAT", "tunneling_type": "Jenis Tunnel KNX" + }, + "data_description": { + "host": "Alamat IP perangkat tunneling KNX/IP.", + "port": "Port perangkat tunneling KNX/IP." } } } diff --git a/homeassistant/components/knx/translations/it.json b/homeassistant/components/knx/translations/it.json index ad4e9f34610..c05bdba3944 100644 --- a/homeassistant/components/knx/translations/it.json +++ b/homeassistant/components/knx/translations/it.json @@ -5,29 +5,73 @@ "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "error": { - "cannot_connect": "Impossibile connettersi" + "cannot_connect": "Impossibile connettersi", + "file_not_found": "Il file `.knxkeys` specificato non \u00e8 stato trovato nel percorso config/.storage/knx/", + "invalid_individual_address": "Il valore non corrisponde al modello per l'indirizzo individuale KNX. 'area.line.device'", + "invalid_ip_address": "Indirizzo IPv4 non valido.", + "invalid_signature": "La password per decifrare il file `.knxkeys` \u00e8 errata." }, "step": { "manual_tunnel": { "data": { "host": "Host", "individual_address": "Indirizzo individuale per la connessione", - "local_ip": "IP locale di Home Assistant (lascia vuoto per il rilevamento automatico)", + "local_ip": "IP locale di Home Assistant", "port": "Porta", "route_back": "Torna indietro / Modalit\u00e0 NAT", "tunneling_type": "Tipo tunnel KNX" }, + "data_description": { + "host": "Indirizzo IP del dispositivo di tunneling KNX/IP.", + "local_ip": "Lascia vuoto per usare il rilevamento automatico.", + "port": "Porta del dispositivo di tunneling KNX/IP." + }, "description": "Inserisci le informazioni di connessione del tuo dispositivo di tunneling." }, "routing": { "data": { - "individual_address": "Indirizzo individuale per la connessione di routing", - "local_ip": "IP locale di Home Assistant (lascia vuoto per il rilevamento automatico)", - "multicast_group": "Il gruppo multicast utilizzato per il routing", - "multicast_port": "La porta multicast usata per il routing" + "individual_address": "Indirizzo individuale", + "local_ip": "IP locale di Home Assistant", + "multicast_group": "Gruppo multicast", + "multicast_port": "Porta multicast" + }, + "data_description": { + "individual_address": "Indirizzo KNX che deve essere utilizzato da Home Assistant, ad es. `0.0.4`", + "local_ip": "Lasciare vuoto per usare il rilevamento automatico." }, "description": "Configura le opzioni di instradamento." }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Il nome del file `.knxkeys` (inclusa l'estensione)", + "knxkeys_password": "La password per decifrare il file `.knxkeys`" + }, + "data_description": { + "knxkeys_filename": "Il file dovrebbe essere trovato nella tua cartella di configurazione in `.storage/knx/`.\n Nel sistema operativo Home Assistant questo sarebbe `/config/.storage/knx/`\n Esempio: `mio_progetto.knxkeys`", + "knxkeys_password": "Questo \u00e8 stato impostato durante l'esportazione del file da ETS." + }, + "description": "Inserisci le informazioni per il tuo file `.knxkeys`." + }, + "secure_manual": { + "data": { + "device_authentication": "Password di autenticazione del dispositivo", + "user_id": "ID utente", + "user_password": "Password utente" + }, + "data_description": { + "device_authentication": "Questo \u00e8 impostato nel pannello 'IP' dell'interfaccia in ETS.", + "user_id": "Questo \u00e8 spesso il tunnel numero +1. Quindi \"Tunnel 2\" avrebbe l'ID utente \"3\".", + "user_password": "Password per la connessione specifica del tunnel impostata nel pannello 'Propriet\u00e0' del tunnel in ETS." + }, + "description": "Inserisci le tue informazioni di sicurezza IP." + }, + "secure_tunneling": { + "description": "Seleziona come vuoi configurare KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Utilizza un file `.knxkeys` contenente chiavi di sicurezza IP", + "secure_manual": "Configura manualmente le chiavi di sicurezza IP" + } + }, "tunnel": { "data": { "gateway": "Connessione tunnel KNX" @@ -48,11 +92,19 @@ "data": { "connection_type": "Tipo di connessione KNX", "individual_address": "Indirizzo individuale predefinito", - "local_ip": "IP locale di Home Assistant (usare 0.0.0.0 per il rilevamento automatico)", - "multicast_group": "Gruppo multicast utilizzato per il routing e il rilevamento", - "multicast_port": "Porta multicast utilizzata per il routing e il rilevamento", - "rate_limit": "Numero massimo di telegrammi in uscita al secondo", - "state_updater": "Abilita globalmente la lettura degli stati dal bus KNX" + "local_ip": "IP locale di Home Assistant", + "multicast_group": "Gruppo multicast", + "multicast_port": "Porta multicast", + "rate_limit": "Limite di tariffa", + "state_updater": "Aggiornatore di stato" + }, + "data_description": { + "individual_address": "Indirizzo KNX che deve essere utilizzato da Home Assistant, ad es. `0.0.4`", + "local_ip": "Usa `0.0.0.0` per il rilevamento automatico.", + "multicast_group": "Utilizzato per l'instradamento e il rilevamento. Predefinito: `224.0.23.12`", + "multicast_port": "Utilizzato per l'instradamento e il rilevamento. Predefinito: `3671`", + "rate_limit": "Numero massimo di telegrammi in uscita al secondo.\n Consigliato: da 20 a 40", + "state_updater": "Abilita o disabilita globalmente gli stati di lettura dal bus KNX. Se disabilitato, Home Assistant non recuperer\u00e0 attivamente gli stati dal bus KNX, le opzioni dell'entit\u00e0 `sync_state` non avranno alcun effetto." } }, "tunnel": { @@ -62,6 +114,10 @@ "port": "Porta", "route_back": "Torna indietro / Modalit\u00e0 NAT", "tunneling_type": "Tipo tunnel KNX" + }, + "data_description": { + "host": "Indirizzo IP del dispositivo di tunneling KNX/IP.", + "port": "Porta del dispositivo di tunneling KNX/IP." } } } diff --git a/homeassistant/components/knx/translations/ja.json b/homeassistant/components/knx/translations/ja.json index a4744a41a2c..52eed8780b8 100644 --- a/homeassistant/components/knx/translations/ja.json +++ b/homeassistant/components/knx/translations/ja.json @@ -5,7 +5,11 @@ "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "file_not_found": "\u6307\u5b9a\u3055\u308c\u305f'.knxkeys'\u30d5\u30a1\u30a4\u30eb\u304c\u3001\u30d1\u30b9: config/.storage/knx/ \u306b\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f", + "invalid_individual_address": "\u5024\u304cKNX\u500b\u5225\u30a2\u30c9\u30ec\u30b9\u306e\u30d1\u30bf\u30fc\u30f3\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002\n'area.line.device'", + "invalid_ip_address": "IPv4\u30a2\u30c9\u30ec\u30b9\u304c\u7121\u52b9\u3067\u3059\u3002", + "invalid_signature": "'.knxkeys'\u30d5\u30a1\u30a4\u30eb\u3092\u5fa9\u53f7\u5316\u3059\u308b\u305f\u3081\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u9593\u9055\u3063\u3066\u3044\u307e\u3059\u3002" }, "step": { "manual_tunnel": { @@ -17,6 +21,11 @@ "route_back": "\u30eb\u30fc\u30c8\u30d0\u30c3\u30af / NAT\u30e2\u30fc\u30c9", "tunneling_type": "KNX\u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u30bf\u30a4\u30d7" }, + "data_description": { + "host": "KNX/IP\u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u30c7\u30d0\u30a4\u30b9\u306eIP\u30a2\u30c9\u30ec\u30b9\u3002", + "local_ip": "\u81ea\u52d5\u691c\u51fa\u3092\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u307e\u3059\u3002", + "port": "KNX/IP\u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u30c7\u30d0\u30a4\u30b9\u306e\u30dd\u30fc\u30c8\u3002" + }, "description": "\u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u30c7\u30d0\u30a4\u30b9\u306e\u63a5\u7d9a\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "routing": { @@ -26,8 +35,43 @@ "multicast_group": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u306b\u4f7f\u7528\u3059\u308b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8\u30b0\u30eb\u30fc\u30d7", "multicast_port": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u306b\u4f7f\u7528\u3059\u308b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8\u30dd\u30fc\u30c8" }, + "data_description": { + "individual_address": "Home Assistant\u304c\u4f7f\u7528\u3059\u308bKNX\u30a2\u30c9\u30ec\u30b9\u3001\u4f8b. `0.0.4`", + "local_ip": "\u81ea\u52d5\u691c\u51fa\u3092\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u307e\u3059\u3002" + }, "description": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "'.knxkeys'\u30d5\u30a1\u30a4\u30eb\u306e\u30d5\u30a1\u30a4\u30eb\u540d(\u62e1\u5f35\u5b50\u3092\u542b\u3080)", + "knxkeys_password": "'.knxkeys'\u30d5\u30a1\u30a4\u30eb\u3092\u5fa9\u53f7\u5316\u3059\u308b\u305f\u3081\u306e\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "data_description": { + "knxkeys_filename": "\u3053\u306e\u30d5\u30a1\u30a4\u30eb\u306f\u3001config\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u306e `.storage/knx/` \u306b\u3042\u308b\u306f\u305a\u3067\u3059\u3002\nHome Assistant OS\u306e\u5834\u5408\u3001\u3053\u308c\u306f\u3001`/config/.storage/knx/` \u306b\u306a\u308a\u307e\u3059\n\u4f8b: `my_project.knxkeys`", + "knxkeys_password": "\u3053\u308c\u306f\u3001ETS\u304b\u3089\u30d5\u30a1\u30a4\u30eb\u3092\u30a8\u30af\u30b9\u30dd\u30fc\u30c8\u3059\u308b\u3068\u304d\u306b\u8a2d\u5b9a\u3055\u308c\u307e\u3057\u305f\u3002" + }, + "description": "'.knxkeys'\u30d5\u30a1\u30a4\u30eb\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "secure_manual": { + "data": { + "device_authentication": "\u30c7\u30d0\u30a4\u30b9\u8a8d\u8a3c\u30d1\u30b9\u30ef\u30fc\u30c9", + "user_id": "\u30e6\u30fc\u30b6\u30fcID", + "user_password": "\u30e6\u30fc\u30b6\u30fc\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "data_description": { + "device_authentication": "\u3053\u308c\u306f\u3001ETS\u306e\u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30fc\u30b9\u306e 'IP' \u30d1\u30cd\u30eb\u3067\u8a2d\u5b9a\u3057\u307e\u3059\u3002", + "user_id": "\u591a\u304f\u306e\u5834\u5408\u3001\u3053\u308c\u306f\u30c8\u30f3\u30cd\u30eb\u756a\u53f7+1\u3067\u3059\u3002\u3057\u305f\u304c\u3063\u3066\u3001 '\u30c8\u30f3\u30cd\u30eb2' \u306e\u30e6\u30fc\u30b6\u30fcID\u306f\u3001'3 '\u306b\u306a\u308a\u307e\u3059\u3002", + "user_password": "ETS\u306e\u30c8\u30f3\u30cd\u30eb\u306e\u3001'\u30d7\u30ed\u30d1\u30c6\u30a3' \u30d1\u30cd\u30eb\u3067\u8a2d\u5b9a\u3055\u308c\u305f\u7279\u5b9a\u306e\u30c8\u30f3\u30cd\u30eb\u63a5\u7d9a\u7528\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3002" + }, + "description": "IP\u30bb\u30ad\u30e5\u30a2\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "secure_tunneling": { + "description": "IP\u30bb\u30ad\u30e5\u30a2\u306e\u8a2d\u5b9a\u65b9\u6cd5\u3092\u9078\u629e\u3057\u307e\u3059\u3002", + "menu_options": { + "secure_knxkeys": "IP\u30bb\u30ad\u30e5\u30a2\u60c5\u5831\u3092\u542b\u3080knxkeys\u30d5\u30a1\u30a4\u30eb\u3092\u8a2d\u5b9a\u3057\u307e\u3059", + "secure_manual": "IP\u30bb\u30ad\u30e5\u30a2\u3092\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b" + } + }, "tunnel": { "data": { "gateway": "KNX\u30c8\u30f3\u30cd\u30eb\u63a5\u7d9a" @@ -53,6 +97,14 @@ "multicast_port": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3068\u691c\u51fa(discovery)\u306b\u4f7f\u7528\u3055\u308c\u308b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8\u30dd\u30fc\u30c8", "rate_limit": "1 \u79d2\u3042\u305f\u308a\u306e\u6700\u5927\u9001\u4fe1\u96fb\u5831(telegrams )\u6570", "state_updater": "KNX\u30d0\u30b9\u304b\u3089\u306e\u8aad\u307f\u53d6\u308a\u72b6\u614b\u3092\u30b0\u30ed\u30fc\u30d0\u30eb\u306b\u6709\u52b9\u306b\u3059\u308b" + }, + "data_description": { + "individual_address": "Home Assistant\u304c\u4f7f\u7528\u3059\u308bKNX\u30a2\u30c9\u30ec\u30b9\u3001\u4f8b. `0.0.4`", + "local_ip": "\u81ea\u52d5\u691c\u51fa\u306b\u306f\u3001`0.0.0.0` \u3092\u4f7f\u7528\u3057\u307e\u3059\u3002", + "multicast_group": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3068\u691c\u51fa\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002\u30c7\u30d5\u30a9\u30eb\u30c8t: `224.0.23.12`", + "multicast_port": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3068\u691c\u51fa\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002\u30c7\u30d5\u30a9\u30eb\u30c8: `3671`", + "rate_limit": "1\u79d2\u3042\u305f\u308a\u306e\u6700\u5927\u9001\u4fe1\u30c6\u30ec\u30b0\u30e9\u30e0\u3002\n\u63a8\u5968: 20\uff5e40", + "state_updater": "KNX Bus\u304b\u3089\u306e\u72b6\u614b\u306e\u8aad\u307f\u53d6\u308a\u3092\u30b0\u30ed\u30fc\u30d0\u30eb\u306b\u6709\u52b9\u307e\u305f\u306f\u7121\u52b9\u306b\u3057\u307e\u3059\u3002\u7121\u52b9\u306b\u3059\u308b\u3068\u3001Home Assistant\u306f\u3001KNX Bus\u304b\u3089\u30a2\u30af\u30c6\u30a3\u30d6\u306b\u72b6\u614b\u3092\u53d6\u5f97\u3057\u306a\u304f\u306a\u308b\u306e\u3067\u3001`sync_state`\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30aa\u30d7\u30b7\u30e7\u30f3\u306f\u610f\u5473\u3092\u6301\u305f\u306a\u304f\u306a\u308a\u307e\u3059\u3002" } }, "tunnel": { @@ -62,6 +114,10 @@ "port": "\u30dd\u30fc\u30c8", "route_back": "\u30eb\u30fc\u30c8\u30d0\u30c3\u30af / NAT\u30e2\u30fc\u30c9", "tunneling_type": "KNX\u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u30bf\u30a4\u30d7" + }, + "data_description": { + "host": "KNX/IP\u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u30c7\u30d0\u30a4\u30b9\u306eIP\u30a2\u30c9\u30ec\u30b9\u3002", + "port": "KNX/IP\u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u30c7\u30d0\u30a4\u30b9\u306e\u30dd\u30fc\u30c8\u3002" } } } diff --git a/homeassistant/components/knx/translations/nl.json b/homeassistant/components/knx/translations/nl.json index 9b68bd02d2f..f40b9d7729f 100644 --- a/homeassistant/components/knx/translations/nl.json +++ b/homeassistant/components/knx/translations/nl.json @@ -5,29 +5,73 @@ "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "error": { - "cannot_connect": "Kan geen verbinding maken" + "cannot_connect": "Kan geen verbinding maken", + "file_not_found": "Het opgegeven `.knxkeys`-bestand is niet gevonden in het pad config/.storage/knx/", + "invalid_individual_address": "Waarde komt niet overeen met patroon voor KNX individueel adres.\n\"area.line.device", + "invalid_ip_address": "Ongeldig IPv4-adres.", + "invalid_signature": "Het wachtwoord om het `.knxkeys`-bestand te decoderen is verkeerd." }, "step": { "manual_tunnel": { "data": { "host": "Host", "individual_address": "Individueel adres voor de verbinding", - "local_ip": "Lokaal IP van Home Assistant (leeg laten voor automatische detectie)", + "local_ip": "Lokale IP van Home Assistant", "port": "Poort", "route_back": "Route Back / NAT Mode", "tunneling_type": "KNX Tunneling Type" }, + "data_description": { + "host": "IP adres van het KNX/IP tunneling apparaat.", + "local_ip": "Leeg laten om auto-discovery te gebruiken.", + "port": "Poort van het KNX/IP-tunnelapparaat." + }, "description": "Voer de verbindingsinformatie van uw tunneling-apparaat in." }, "routing": { "data": { - "individual_address": "Individueel adres voor de routing verbinding", - "local_ip": "Lokaal IP van Home Assistant (leeg laten voor automatische detectie)", - "multicast_group": "De multicast groep gebruikt voor de routing", - "multicast_port": "De multicast-poort gebruikt voor de routing" + "individual_address": "Individueel adres", + "local_ip": "Lokale IP van Home Assistant", + "multicast_group": "Multicast-groep", + "multicast_port": "Multicast-poort" + }, + "data_description": { + "individual_address": "KNX-adres te gebruiken door Home Assistant, bijv. `0.0.4`", + "local_ip": "Leeg laten om auto-discovery te gebruiken." }, "description": "Configureer de routing opties" }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "De bestandsnaam van uw `.knxkeys` bestand (inclusief extensie)", + "knxkeys_password": "Het wachtwoord om het bestand `.knxkeys` te ontcijferen" + }, + "data_description": { + "knxkeys_filename": "Het bestand zal naar verwachting worden gevonden in uw configuratiemap in '.storage/knx/'.\nIn Home Assistant OS zou dit '/config/.storage/knx/' zijn.\nVoorbeeld: 'my_project.knxkeys'", + "knxkeys_password": "Dit werd ingesteld bij het exporteren van het bestand van ETS." + }, + "description": "Voer de informatie voor uw `.knxkeys` bestand in." + }, + "secure_manual": { + "data": { + "device_authentication": "Wachtwoord voor apparaatverificatie", + "user_id": "User ID", + "user_password": "Gebruikerswachtwoord" + }, + "data_description": { + "device_authentication": "Dit wordt ingesteld in het \"IP\"-paneel van de interface in ETS.", + "user_id": "Dit is vaak tunnelnummer +1. Dus 'Tunnel 2' zou User-ID '3' hebben.", + "user_password": "Wachtwoord voor de specifieke tunnelverbinding, ingesteld in het paneel \"Eigenschappen\" van de tunnel in ETS." + }, + "description": "Voer uw beveiligde IP-gegevens in." + }, + "secure_tunneling": { + "description": "Kies hoe u KNX/IP Secure wilt configureren.", + "menu_options": { + "secure_knxkeys": "Gebruik een `.knxkeys` bestand met IP beveiligde sleutels", + "secure_manual": "IP-beveiligingssleutels handmatig configureren" + } + }, "tunnel": { "data": { "gateway": "KNX Tunnel Connection" @@ -48,11 +92,19 @@ "data": { "connection_type": "KNX-verbindingstype", "individual_address": "Standaard individueel adres", - "local_ip": "Lokaal IP van Home Assistant (gebruik 0.0.0.0 voor automatische detectie)", - "multicast_group": "Multicast groep gebruikt voor routing en ontdekking", - "multicast_port": "Multicast poort gebruikt voor routing en ontdekking", - "rate_limit": "Maximaal aantal uitgaande telegrammen per seconde", - "state_updater": "Globaal vrijgeven van het lezen van de KNX bus" + "local_ip": "Lokale IP van Home Assistant", + "multicast_group": "Multicast-groep", + "multicast_port": "Multicast-poort", + "rate_limit": "Rate limit", + "state_updater": "Statusupdater" + }, + "data_description": { + "individual_address": "KNX-adres dat door Home Assistant moet worden gebruikt, bijv. `0.0.4`", + "local_ip": "Gebruik `0.0.0.0` voor auto-discovery.", + "multicast_group": "Gebruikt voor routing en discovery. Standaard: `224.0.23.12`.", + "multicast_port": "Gebruikt voor routing en discovery. Standaard: `3671`", + "rate_limit": "Maximaal aantal uitgaande telegrammen per seconde.\nAanbevolen: 20 tot 40", + "state_updater": "Globaal in- of uitschakelen van het lezen van de status van de KNX bus. Indien uitgeschakeld, zal Home Assistant niet actief de status van de KNX Bus ophalen, `sync_state` entiteitsopties zullen geen effect hebben." } }, "tunnel": { @@ -62,6 +114,10 @@ "port": "Poort", "route_back": "Route Back / NAT Mode", "tunneling_type": "KNX Tunneling Type" + }, + "data_description": { + "host": "IP adres van het KNX/IP tunneling apparaat.", + "port": "Poort van het KNX/IP-tunnelapparaat." } } } diff --git a/homeassistant/components/knx/translations/no.json b/homeassistant/components/knx/translations/no.json index 231945d5233..f5d03e3160c 100644 --- a/homeassistant/components/knx/translations/no.json +++ b/homeassistant/components/knx/translations/no.json @@ -5,29 +5,73 @@ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { - "cannot_connect": "Tilkobling mislyktes" + "cannot_connect": "Tilkobling mislyktes", + "file_not_found": "Den angitte `.knxkeys`-filen ble ikke funnet i banen config/.storage/knx/", + "invalid_individual_address": "Verdien samsvarer ikke med m\u00f8nsteret for individuelle KNX-adresser.\n 'area.line.device'", + "invalid_ip_address": "Ugyldig IPv4-adresse.", + "invalid_signature": "Passordet for \u00e5 dekryptere `.knxkeys`-filen er feil." }, "step": { "manual_tunnel": { "data": { "host": "Vert", "individual_address": "Individuell adresse for tilkoblingen", - "local_ip": "Lokal IP for Home Assistant (la st\u00e5 tomt for automatisk gjenkjenning)", + "local_ip": "Lokal IP for hjemmeassistent", "port": "Port", "route_back": "Rute tilbake / NAT-modus", "tunneling_type": "KNX tunneltype" }, + "data_description": { + "host": "IP-adressen til KNX/IP-tunnelenheten.", + "local_ip": "La st\u00e5 tomt for \u00e5 bruke automatisk oppdagelse.", + "port": "Port p\u00e5 KNX/IP-tunnelenheten." + }, "description": "Vennligst skriv inn tilkoblingsinformasjonen til tunnelenheten din." }, "routing": { "data": { - "individual_address": "Individuell adresse for ruteforbindelsen", - "local_ip": "Lokal IP for Home Assistant (la st\u00e5 tomt for automatisk gjenkjenning)", - "multicast_group": "Multicast-gruppen som brukes til ruting", - "multicast_port": "Multicast-porten som brukes til ruting" + "individual_address": "Individuell adresse", + "local_ip": "Lokal IP for hjemmeassistent", + "multicast_group": "Multicast gruppe", + "multicast_port": "Multicast port" + }, + "data_description": { + "individual_address": "KNX-adresse som skal brukes av Home Assistant, f.eks. `0.0.4`", + "local_ip": "La st\u00e5 tomt for \u00e5 bruke automatisk oppdagelse." }, "description": "Vennligst konfigurer rutealternativene." }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Filnavnet til `.knxkeys`-filen (inkludert utvidelse)", + "knxkeys_password": "Passordet for \u00e5 dekryptere `.knxkeys`-filen" + }, + "data_description": { + "knxkeys_filename": "Filen forventes \u00e5 bli funnet i konfigurasjonskatalogen din i `.storage/knx/`.\n I Home Assistant OS vil dette v\u00e6re `/config/.storage/knx/`\n Eksempel: `mitt_prosjekt.knxkeys`", + "knxkeys_password": "Dette ble satt ved eksport av filen fra ETS." + }, + "description": "Vennligst skriv inn informasjonen for `.knxkeys`-filen." + }, + "secure_manual": { + "data": { + "device_authentication": "Passord for enhetsgodkjenning", + "user_id": "bruker-ID", + "user_password": "Brukerpassord" + }, + "data_description": { + "device_authentication": "Dette settes i 'IP'-panelet til grensesnittet i ETS.", + "user_id": "Dette er ofte tunnelnummer +1. S\u00e5 'Tunnel 2' ville ha bruker-ID '3'.", + "user_password": "Passord for den spesifikke tunnelforbindelsen satt i 'Egenskaper'-panelet i tunnelen i ETS." + }, + "description": "Vennligst skriv inn din sikre IP-informasjon." + }, + "secure_tunneling": { + "description": "Velg hvordan du vil konfigurere KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Bruk en `.knxkeys`-fil som inneholder IP-sikre n\u00f8kler", + "secure_manual": "Konfigurer IP-sikre n\u00f8kler manuelt" + } + }, "tunnel": { "data": { "gateway": "KNX Tunneltilkobling" @@ -48,11 +92,19 @@ "data": { "connection_type": "KNX tilkoblingstype", "individual_address": "Standard individuell adresse", - "local_ip": "Lokal IP for Home Assistant (bruk 0.0.0.0 for automatisk deteksjon)", - "multicast_group": "Multicast-gruppe brukt til ruting og oppdagelse", - "multicast_port": "Multicast-port som brukes til ruting og oppdagelse", - "rate_limit": "Maksimalt utg\u00e5ende telegrammer per sekund", - "state_updater": "Aktiver lesetilstander globalt fra KNX-bussen" + "local_ip": "Lokal IP for hjemmeassistent", + "multicast_group": "Multicast gruppe", + "multicast_port": "Multicast port", + "rate_limit": "Satsgrense", + "state_updater": "Statens oppdatering" + }, + "data_description": { + "individual_address": "KNX-adresse som skal brukes av Home Assistant, f.eks. `0.0.4`", + "local_ip": "Bruk `0.0.0.0` for automatisk oppdagelse.", + "multicast_group": "Brukes til ruting og oppdagelse. Standard: `224.0.23.12`", + "multicast_port": "Brukes til ruting og oppdagelse. Standard: `3671`", + "rate_limit": "Maksimalt utg\u00e5ende telegrammer per sekund.\n Anbefalt: 20 til 40", + "state_updater": "Globalt aktiver eller deaktiver lesetilstander fra KNX-bussen. N\u00e5r den er deaktivert, vil ikke Home Assistant aktivt hente statuser fra KNX-bussen, \"sync_state\"-enhetsalternativer vil ikke ha noen effekt." } }, "tunnel": { @@ -62,6 +114,10 @@ "port": "Port", "route_back": "Rute tilbake / NAT-modus", "tunneling_type": "KNX tunneltype" + }, + "data_description": { + "host": "IP-adressen til KNX/IP-tunnelenheten.", + "port": "Port p\u00e5 KNX/IP-tunnelenheten." } } } diff --git a/homeassistant/components/knx/translations/pl.json b/homeassistant/components/knx/translations/pl.json index 8a30af3fd59..e0821090f29 100644 --- a/homeassistant/components/knx/translations/pl.json +++ b/homeassistant/components/knx/translations/pl.json @@ -5,29 +5,73 @@ "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "file_not_found": "Podany plik '.knxkeys' nie zosta\u0142 znaleziony w \u015bcie\u017cce config/.storage/knx/", + "invalid_individual_address": "Warto\u015b\u0107 nie pasuje do wzorca dla indywidualnego adresu KNX.\n 'obszar.linia.urz\u0105dzenie'", + "invalid_ip_address": "Nieprawid\u0142owy adres IPv4.", + "invalid_signature": "Has\u0142o do odszyfrowania pliku '.knxkeys' jest nieprawid\u0142owe." }, "step": { "manual_tunnel": { "data": { "host": "Nazwa hosta lub adres IP", "individual_address": "Indywidualny adres dla po\u0142\u0105czenia", - "local_ip": "Lokalny adres IP Home Assistant (pozostaw puste w celu automatycznego wykrywania)", + "local_ip": "Lokalny adres IP Home Assistanta", "port": "Port", "route_back": "Tryb Route Back / NAT", "tunneling_type": "Typ tunelowania KNX" }, + "data_description": { + "host": "Adres IP urz\u0105dzenia tuneluj\u0105cego KNX/IP.", + "local_ip": "Pozostaw puste, aby u\u017cy\u0107 automatycznego wykrywania.", + "port": "Port urz\u0105dzenia tuneluj\u0105cego KNX/IP." + }, "description": "Prosz\u0119 wprowadzi\u0107 informacje o po\u0142\u0105czeniu urz\u0105dzenia tuneluj\u0105cego." }, "routing": { "data": { - "individual_address": "Indywidualny adres dla po\u0142\u0105czenia routingowego", - "local_ip": "Lokalny adres IP Home Assistant (pozostaw puste w celu automatycznego wykrywania)", - "multicast_group": "Grupa multicast u\u017cyta do routingu", - "multicast_port": "Port multicast u\u017cyty do routingu" + "individual_address": "Adres indywidualny", + "local_ip": "Lokalny adres IP Home Assistanta", + "multicast_group": "Grupa multicast", + "multicast_port": "Port multicast" + }, + "data_description": { + "individual_address": "Adres KNX u\u017cywany przez Home Assistanta, np. `0.0.4`", + "local_ip": "Pozostaw puste, aby u\u017cy\u0107 automatycznego wykrywania." }, "description": "Prosz\u0119 skonfigurowa\u0107 opcje routingu." }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Nazwa pliku `.knxkeys` (wraz z rozszerzeniem)", + "knxkeys_password": "Has\u0142o do odszyfrowania pliku `.knxkeys`" + }, + "data_description": { + "knxkeys_filename": "Plik powinien znajdowa\u0107 si\u0119 w katalogu konfiguracyjnym w `.storage/knx/`.\nW systemie Home Assistant OS b\u0119dzie to `/config/.storage/knx/`\nPrzyk\u0142ad: `m\u00f3j_projekt.knxkeys`", + "knxkeys_password": "Zosta\u0142o to ustawione podczas eksportowania pliku z ETS." + }, + "description": "Wprowad\u017a informacje dotycz\u0105ce pliku `.knxkeys`." + }, + "secure_manual": { + "data": { + "device_authentication": "Has\u0142o uwierzytelniania urz\u0105dzenia", + "user_id": "Identyfikator u\u017cytkownika", + "user_password": "Has\u0142o u\u017cytkownika" + }, + "data_description": { + "device_authentication": "Jest to ustawiane w panelu \u201eIP\u201d interfejsu w ETS.", + "user_id": "Cz\u0119sto jest to numer tunelu plus 1. Tak wi\u0119c \u201eTunnel 2\u201d mia\u0142by identyfikator u\u017cytkownika \u201e3\u201d.", + "user_password": "Has\u0142o dla konkretnego po\u0142\u0105czenia tunelowego ustawione w panelu \u201eW\u0142a\u015bciwo\u015bci\u201d tunelu w ETS." + }, + "description": "Wprowad\u017a informacje o IP secure." + }, + "secure_tunneling": { + "description": "Wybierz, jak chcesz skonfigurowa\u0107 KNX/IP secure.", + "menu_options": { + "secure_knxkeys": "U\u017cyj pliku `.knxkeys` zawieraj\u0105cego klucze IP secure", + "secure_manual": "R\u0119czna konfiguracja kluczy IP secure" + } + }, "tunnel": { "data": { "gateway": "Po\u0142\u0105czenie tunelowe KNX" @@ -48,11 +92,19 @@ "data": { "connection_type": "Typ po\u0142\u0105czenia KNX", "individual_address": "Domy\u015blny adres indywidualny", - "local_ip": "Lokalny adres IP Home Assistant (u\u017cyj 0.0.0.0 w celu automatycznego wykrywania)", - "multicast_group": "Grupa multicast u\u017cywana do routingu i wykrywania", - "multicast_port": "Port multicast u\u017cywany do routingu i wykrywania", - "rate_limit": "Maksymalna liczba wychodz\u0105cych wiadomo\u015bci na sekund\u0119", - "state_updater": "Zezw\u00f3l globalnie na odczyt stan\u00f3w z magistrali KNX" + "local_ip": "Lokalny adres IP Home Assistanta", + "multicast_group": "Grupa multicast", + "multicast_port": "Port multicast", + "rate_limit": "Limit", + "state_updater": "Aktualizator stanu" + }, + "data_description": { + "individual_address": "Adres KNX u\u017cywany przez Home Assistanta, np. `0.0.4`", + "local_ip": "U\u017cyj `0.0.0.0` do automatycznego wykrywania.", + "multicast_group": "U\u017cywany do routingu i wykrywania. Domy\u015blnie: `224.0.23.12`", + "multicast_port": "U\u017cywany do routingu i wykrywania. Domy\u015blnie: `3671`", + "rate_limit": "Maksymalna liczba wychodz\u0105cych wiadomo\u015bci na sekund\u0119.\nZalecane: od 20 do 40", + "state_updater": "Globalnie w\u0142\u0105czaj lub wy\u0142\u0105czaj odczytywanie stan\u00f3w z magistrali KNX. Po wy\u0142\u0105czeniu, Home Assistant nie b\u0119dzie aktywnie pobiera\u0107 stan\u00f3w z magistrali KNX, opcje encji `sync_state` nie b\u0119d\u0105 mia\u0142y \u017cadnego efektu." } }, "tunnel": { @@ -62,6 +114,10 @@ "port": "Port", "route_back": "Tryb Route Back / NAT", "tunneling_type": "Typ tunelowania KNX" + }, + "data_description": { + "host": "Adres IP urz\u0105dzenia tuneluj\u0105cego KNX/IP.", + "port": "Port urz\u0105dzenia tuneluj\u0105cego KNX/IP." } } } diff --git a/homeassistant/components/knx/translations/pt-BR.json b/homeassistant/components/knx/translations/pt-BR.json index 0e8e3402961..950cedc0721 100644 --- a/homeassistant/components/knx/translations/pt-BR.json +++ b/homeassistant/components/knx/translations/pt-BR.json @@ -5,29 +5,73 @@ "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { - "cannot_connect": "Falha ao conectar" + "cannot_connect": "Falha ao conectar", + "file_not_found": "O arquivo `.knxkeys` especificado n\u00e3o foi encontrado no caminho config/.storage/knx/", + "invalid_individual_address": "O valor n\u00e3o corresponde ao padr\u00e3o do endere\u00e7o individual KNX.\n '\u00e1rea.linha.dispositivo'", + "invalid_ip_address": "Endere\u00e7o IPv4 inv\u00e1lido.", + "invalid_signature": "A senha para descriptografar o arquivo `.knxkeys` est\u00e1 errada." }, "step": { "manual_tunnel": { "data": { "host": "Nome do host", "individual_address": "Endere\u00e7o individual para a conex\u00e3o", - "local_ip": "IP local do Home Assistant (deixe em branco para detec\u00e7\u00e3o autom\u00e1tica)", + "local_ip": "IP local do Home Assistant", "port": "Porta", "route_back": "Modo Rota de Retorno / NAT", "tunneling_type": "Tipo de t\u00fanel KNX" }, + "data_description": { + "host": "Endere\u00e7o IP do dispositivo de tunelamento KNX/IP.", + "local_ip": "Deixe em branco para usar a descoberta autom\u00e1tica.", + "port": "Porta do dispositivo de tunelamento KNX/IP." + }, "description": "Por favor, digite as informa\u00e7\u00f5es de conex\u00e3o do seu dispositivo de tunelamento." }, "routing": { "data": { - "individual_address": "Endere\u00e7o individual para a conex\u00e3o de roteamento", - "local_ip": "IP local do Home Assistant (deixe vazio para detec\u00e7\u00e3o autom\u00e1tica)", - "multicast_group": "O grupo multicast usado para roteamento", - "multicast_port": "A porta multicast usada para roteamento" + "individual_address": "Endere\u00e7o individual", + "local_ip": "IP local do Home Assistant", + "multicast_group": "Grupo multicast", + "multicast_port": "Porta multicast" + }, + "data_description": { + "individual_address": "Endere\u00e7o KNX a ser usado pelo Home Assistant, por exemplo, `0.0.4`", + "local_ip": "Deixe em branco para usar a descoberta autom\u00e1tica." }, "description": "Por favor, configure as op\u00e7\u00f5es de roteamento." }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "O nome do seu arquivo `.knxkeys` (incluindo extens\u00e3o)", + "knxkeys_password": "A senha para descriptografar o arquivo `.knxkeys`" + }, + "data_description": { + "knxkeys_filename": "Espera-se que o arquivo seja encontrado em seu diret\u00f3rio de configura\u00e7\u00e3o em `.storage/knx/`.\n No sistema operacional Home Assistant seria `/config/.storage/knx/`\n Exemplo: `my_project.knxkeys`", + "knxkeys_password": "Isso foi definido ao exportar o arquivo do ETS." + }, + "description": "Por favor, insira as informa\u00e7\u00f5es para o seu arquivo `.knxkeys`." + }, + "secure_manual": { + "data": { + "device_authentication": "Senha de autentica\u00e7\u00e3o do dispositivo", + "user_id": "ID do usu\u00e1rio", + "user_password": "Senha do usu\u00e1rio" + }, + "data_description": { + "device_authentication": "Isso \u00e9 definido no painel 'IP' da interface no ETS.", + "user_id": "Isso geralmente \u00e9 o n\u00famero do t\u00fanel +1. Portanto, 'T\u00fanel 2' teria o ID de usu\u00e1rio '3'.", + "user_password": "Senha para a conex\u00e3o de t\u00fanel espec\u00edfica definida no painel 'Propriedades' do t\u00fanel no ETS." + }, + "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP." + }, + "secure_tunneling": { + "description": "Selecione como deseja configurar o KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Use um arquivo `.knxkeys` contendo chaves seguras de IP", + "secure_manual": "Configurar manualmente as chaves de seguran\u00e7a IP" + } + }, "tunnel": { "data": { "gateway": "Conex\u00e3o do t\u00fanel KNX" @@ -48,11 +92,19 @@ "data": { "connection_type": "Tipo de conex\u00e3o KNX", "individual_address": "Endere\u00e7o individual padr\u00e3o", - "local_ip": "IP local do Home Assistant (use 0.0.0.0 para detec\u00e7\u00e3o autom\u00e1tica)", - "multicast_group": "Grupo multicast usado para roteamento e descoberta", - "multicast_port": "Porta multicast usada para roteamento e descoberta", - "rate_limit": "M\u00e1ximo de telegramas de sa\u00edda por segundo", - "state_updater": "Permitir globalmente estados de leitura a partir do KNX Bus" + "local_ip": "IP local do Home Assistant", + "multicast_group": "Grupo multicast", + "multicast_port": "Porta multicast", + "rate_limit": "Taxa limite", + "state_updater": "Atualizador de estado" + }, + "data_description": { + "individual_address": "Endere\u00e7o KNX a ser usado pelo Home Assistant, por exemplo, `0.0.4`", + "local_ip": "Use `0.0.0.0` para descoberta autom\u00e1tica.", + "multicast_group": "Usado para roteamento e descoberta. Padr\u00e3o: `224.0.23.12`", + "multicast_port": "Usado para roteamento e descoberta. Padr\u00e3o: `3671`", + "rate_limit": "M\u00e1ximo de telegramas de sa\u00edda por segundo.\n Recomendado: 20 a 40", + "state_updater": "Habilite ou desabilite globalmente os estados de leitura do barramento KNX. Quando desativado, o Home Assistant n\u00e3o recuperar\u00e1 ativamente os estados do barramento KNX, as op\u00e7\u00f5es de entidade `sync_state` n\u00e3o ter\u00e3o efeito." } }, "tunnel": { @@ -62,6 +114,10 @@ "port": "Porta", "route_back": "Modo Rota de Retorno / NAT", "tunneling_type": "Tipo de t\u00fanel KNX" + }, + "data_description": { + "host": "Endere\u00e7o IP do dispositivo de tunelamento KNX/IP.", + "port": "Porta do dispositivo de tunelamento KNX/IP." } } } diff --git a/homeassistant/components/knx/translations/ru.json b/homeassistant/components/knx/translations/ru.json index 6c1a41dac91..14b9f919aa2 100644 --- a/homeassistant/components/knx/translations/ru.json +++ b/homeassistant/components/knx/translations/ru.json @@ -5,29 +5,73 @@ "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." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "file_not_found": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b `.knxkeys` \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u0432 config/.storage/knx/", + "invalid_individual_address": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0448\u0430\u0431\u043b\u043e\u043d\u0443 \u0434\u043b\u044f \u0438\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0430\u0434\u0440\u0435\u0441\u0430 KNX 'area.line.device'.", + "invalid_ip_address": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 IPv4.", + "invalid_signature": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0438 \u0444\u0430\u0439\u043b\u0430 `.knxkeys`." }, "step": { "manual_tunnel": { "data": { "host": "\u0425\u043e\u0441\u0442", "individual_address": "\u0418\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", - "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 Home Assistant (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f)", + "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 Home Assistant", "port": "\u041f\u043e\u0440\u0442", "route_back": "\u041e\u0431\u0440\u0430\u0442\u043d\u044b\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 / \u0440\u0435\u0436\u0438\u043c NAT", "tunneling_type": "\u0422\u0438\u043f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX" }, + "data_description": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX/IP.", + "local_ip": "\u041e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435.", + "port": "\u041f\u043e\u0440\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX/IP." + }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438." }, "routing": { "data": { - "individual_address": "\u0418\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0438\u0440\u0443\u0435\u043c\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f", - "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 Home Assistant (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f)", - "multicast_group": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438", - "multicast_port": "\u041f\u043e\u0440\u0442 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438" + "individual_address": "\u0418\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441", + "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 Home Assistant", + "multicast_group": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438", + "multicast_port": "\u041f\u043e\u0440\u0442 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438" + }, + "data_description": { + "individual_address": "\u0410\u0434\u0440\u0435\u0441 KNX, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f Home Assistant, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, `0.0.4`", + "local_ip": "\u041e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435." }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438." }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "\u0418\u043c\u044f \u0444\u0430\u0439\u043b\u0430 `.knxkeys` (\u0432\u043a\u043b\u044e\u0447\u0430\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435)", + "knxkeys_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0438 \u0444\u0430\u0439\u043b\u0430 `.knxkeys`" + }, + "data_description": { + "knxkeys_filename": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f, \u0447\u0442\u043e \u0444\u0430\u0439\u043b \u0431\u0443\u0434\u0435\u0442 \u043d\u0430\u0439\u0434\u0435\u043d \u0432 \u043a\u0430\u0442\u0430\u043b\u043e\u0433\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 `.storage/knx/`.\n\u0415\u0441\u043b\u0438 \u0412\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 Home Assistant OS \u044d\u0442\u043e\u0442 \u043f\u0443\u0442\u044c \u0431\u0443\u0434\u0435\u0442 `/config/.storage/knx/`\n\u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: `my_project.knxkeys`", + "knxkeys_password": "\u042d\u0442\u043e\u0442 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u0431\u044b\u043b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u043f\u0440\u0438 \u044d\u043a\u0441\u043f\u043e\u0440\u0442\u0435 \u0444\u0430\u0439\u043b\u0430 \u0438\u0437 ETS." + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0444\u0430\u0439\u043b\u0435 `.knxkeys`." + }, + "secure_manual": { + "data": { + "device_authentication": "\u041f\u0430\u0440\u043e\u043b\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "user_id": "ID \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", + "user_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "data_description": { + "device_authentication": "\u042d\u0442\u043e\u0442 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u0443\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 'IP' \u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 ETS.", + "user_id": "\u0427\u0430\u0441\u0442\u043e \u043d\u043e\u043c\u0435\u0440 \u0442\u0443\u043d\u043d\u0435\u043b\u044f +1. \u0422\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c, 'Tunnel 2' \u0431\u0443\u0434\u0435\u0442 \u0438\u043c\u0435\u0442\u044c User-ID '3'.", + "user_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e \u0442\u0443\u043d\u043d\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f, \u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0439 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 'Properties' \u0442\u0443\u043d\u043d\u0435\u043b\u044f \u0432 ETS." + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043f\u043e IP Secure." + }, + "secure_tunneling": { + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0444\u0430\u0439\u043b `.knxkeys`, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0438\u0439 \u043a\u043b\u044e\u0447\u0438 IP secure", + "secure_manual": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043a\u043b\u044e\u0447\u0438 IP Secure \u0432\u0440\u0443\u0447\u043d\u0443\u044e" + } + }, "tunnel": { "data": { "gateway": "\u0422\u0443\u043d\u043d\u0435\u043b\u044c\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f KNX" @@ -48,11 +92,19 @@ "data": { "connection_type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f KNX", "individual_address": "\u0418\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e", - "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 Home Assistant (0.0.0.0 \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f)", - "multicast_group": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f", - "multicast_port": "\u041f\u043e\u0440\u0442 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f", - "rate_limit": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0442\u0435\u043b\u0435\u0433\u0440\u0430\u043c\u043c \u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0443", - "state_updater": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u043e \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0441\u0447\u0438\u0442\u044b\u0432\u0430\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 \u0441 \u0448\u0438\u043d\u044b KNX" + "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 Home Assistant", + "multicast_group": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438", + "multicast_port": "\u041f\u043e\u0440\u0442 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u043e\u0439 \u0440\u0430\u0441\u0441\u044b\u043b\u043a\u0438", + "rate_limit": "\u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435 \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u0438", + "state_updater": "\u0421\u0440\u0435\u0434\u0441\u0442\u0432\u043e \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f" + }, + "data_description": { + "individual_address": "\u0410\u0434\u0440\u0435\u0441 KNX, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f Home Assistant, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, `0.0.4`", + "local_ip": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 `0.0.0.0` \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f.", + "multicast_group": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f. \u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: `224.0.23.12`", + "multicast_port": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f. \u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: `3671`", + "rate_limit": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0442\u0435\u043b\u0435\u0433\u0440\u0430\u043c\u043c \u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0443.\n\u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f: \u043e\u0442 20 \u0434\u043e 40", + "state_updater": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0438\u043b\u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0447\u0442\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0438\u0437 \u0448\u0438\u043d\u044b KNX. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d, Home Assistant \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0430\u043a\u0442\u0438\u0432\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0438\u0437 \u0448\u0438\u043d\u044b KNX, \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043e\u0431\u044a\u0435\u043a\u0442\u0430 sync_state \u043d\u0435 \u0431\u0443\u0434\u0443\u0442 \u0438\u043c\u0435\u0442\u044c \u043d\u0438\u043a\u0430\u043a\u043e\u0433\u043e \u044d\u0444\u0444\u0435\u043a\u0442\u0430." } }, "tunnel": { @@ -62,6 +114,10 @@ "port": "\u041f\u043e\u0440\u0442", "route_back": "\u041e\u0431\u0440\u0430\u0442\u043d\u044b\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 / \u0440\u0435\u0436\u0438\u043c NAT", "tunneling_type": "\u0422\u0438\u043f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX" + }, + "data_description": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX/IP.", + "port": "\u041f\u043e\u0440\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX/IP." } } } diff --git a/homeassistant/components/knx/translations/tr.json b/homeassistant/components/knx/translations/tr.json index 6267038a6e0..c10f35ca48f 100644 --- a/homeassistant/components/knx/translations/tr.json +++ b/homeassistant/components/knx/translations/tr.json @@ -5,29 +5,73 @@ "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "file_not_found": "Belirtilen `.knxkeys` dosyas\u0131 config/.storage/knx/ yolunda bulunamad\u0131", + "invalid_individual_address": "De\u011fer, KNX bireysel adresi i\u00e7in modelle e\u015fle\u015fmiyor.\n 'alan.hat.cihaz'", + "invalid_ip_address": "Ge\u00e7ersiz IPv4 adresi.", + "invalid_signature": "`.knxkeys` dosyas\u0131n\u0131n \u015fifresini \u00e7\u00f6zmek i\u00e7in \u015fifre yanl\u0131\u015f." }, "step": { "manual_tunnel": { "data": { "host": "Sunucu", "individual_address": "Ba\u011flant\u0131 i\u00e7in bireysel adres", - "local_ip": "Home Assistant Yerel IP'si (otomatik alg\u0131lama i\u00e7in bo\u015f b\u0131rak\u0131n)", + "local_ip": "Home Asistan\u0131n\u0131n Yerel IP'si", "port": "Port", "route_back": "Geri Y\u00f6nlendirme / NAT Modu", "tunneling_type": "KNX T\u00fcnel Tipi" }, + "data_description": { + "host": "KNX/IP t\u00fcnelleme cihaz\u0131n\u0131n IP adresi.", + "local_ip": "Otomatik bulmay\u0131 kullanmak i\u00e7in bo\u015f b\u0131rak\u0131n.", + "port": "KNX/IP t\u00fcnelleme cihaz\u0131n\u0131n ba\u011flant\u0131 noktas\u0131." + }, "description": "L\u00fctfen t\u00fcnel cihaz\u0131n\u0131z\u0131n ba\u011flant\u0131 bilgilerini girin." }, "routing": { "data": { - "individual_address": "Y\u00f6nlendirme ba\u011flant\u0131s\u0131 i\u00e7in bireysel adres", - "local_ip": "Home Assistant Yerel IP'si (otomatik alg\u0131lama i\u00e7in bo\u015f b\u0131rak\u0131n)", - "multicast_group": "Y\u00f6nlendirme i\u00e7in kullan\u0131lan \u00e7ok noktaya yay\u0131n grubu", - "multicast_port": "Y\u00f6nlendirme i\u00e7in kullan\u0131lan \u00e7ok noktaya yay\u0131n ba\u011flant\u0131 noktas\u0131" + "individual_address": "Bireysel adres", + "local_ip": "Home Asistan\u0131n\u0131n Yerel IP'si", + "multicast_group": "\u00c7ok noktaya yay\u0131n grubu", + "multicast_port": "\u00c7ok noktaya yay\u0131n ba\u011flant\u0131 noktas\u0131" + }, + "data_description": { + "individual_address": "Home Assistant taraf\u0131ndan kullan\u0131lacak KNX adresi, \u00f6r. \"0.0.4\"", + "local_ip": "Otomatik bulmay\u0131 kullanmak i\u00e7in bo\u015f b\u0131rak\u0131n." }, "description": "L\u00fctfen y\u00f6nlendirme se\u00e7eneklerini yap\u0131land\u0131r\u0131n." }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "`.knxkeys` dosyan\u0131z\u0131n dosya ad\u0131 (uzant\u0131 dahil)", + "knxkeys_password": "`.knxkeys` dosyas\u0131n\u0131n \u015fifresini \u00e7\u00f6zmek i\u00e7in \u015fifre" + }, + "data_description": { + "knxkeys_filename": "Dosyan\u0131n yap\u0131land\u0131rma dizininizde `.storage/knx/` i\u00e7inde bulunmas\u0131 bekleniyor.\n Home Assistant OS'de bu, `/config/.storage/knx/` olacakt\u0131r.\n \u00d6rnek: \"my_project.knxkeys\"", + "knxkeys_password": "Bu, dosyay\u0131 ETS'den d\u0131\u015fa aktar\u0131rken ayarland\u0131." + }, + "description": "L\u00fctfen `.knxkeys` dosyan\u0131z i\u00e7in bilgileri girin." + }, + "secure_manual": { + "data": { + "device_authentication": "Cihaz do\u011frulama \u015fifresi", + "user_id": "Kullan\u0131c\u0131 Kimli\u011fi", + "user_password": "Kullan\u0131c\u0131 \u015fifresi" + }, + "data_description": { + "device_authentication": "Bu, ETS'deki aray\u00fcz\u00fcn 'IP' panelinde ayarlan\u0131r.", + "user_id": "Bu genellikle t\u00fcnel numaras\u0131 +1'dir. Yani 'T\u00fcnel 2' Kullan\u0131c\u0131 Kimli\u011fi '3' olacakt\u0131r.", + "user_password": "ETS'de t\u00fcnelin '\u00d6zellikler' panelinde ayarlanan belirli t\u00fcnel ba\u011flant\u0131s\u0131 i\u00e7in \u015fifre." + }, + "description": "L\u00fctfen IP g\u00fcvenli bilgilerinizi giriniz." + }, + "secure_tunneling": { + "description": "KNX/IP Secure'u nas\u0131l yap\u0131land\u0131rmak istedi\u011finizi se\u00e7in.", + "menu_options": { + "secure_knxkeys": "IP g\u00fcvenli anahtarlar\u0131 i\u00e7eren bir \".knxkeys\" dosyas\u0131 kullan\u0131n", + "secure_manual": "IP g\u00fcvenli anahtarlar\u0131n\u0131 manuel olarak yap\u0131land\u0131r\u0131n" + } + }, "tunnel": { "data": { "gateway": "KNX T\u00fcnel Ba\u011flant\u0131s\u0131" @@ -48,11 +92,19 @@ "data": { "connection_type": "KNX Ba\u011flant\u0131 T\u00fcr\u00fc", "individual_address": "Varsay\u0131lan bireysel adres", - "local_ip": "Home Assistant Yerel IP'si (otomatik alg\u0131lama i\u00e7in 0.0.0.0 kullan\u0131n)", - "multicast_group": "Y\u00f6nlendirme ve ke\u015fif i\u00e7in kullan\u0131lan \u00e7ok noktaya yay\u0131n grubu", - "multicast_port": "Y\u00f6nlendirme ve ke\u015fif i\u00e7in kullan\u0131lan \u00e7ok noktaya yay\u0131n ba\u011flant\u0131 noktas\u0131", - "rate_limit": "Saniyede maksimum giden telegram say\u0131s\u0131", - "state_updater": "KNX Veri Yolu'ndan okuma durumlar\u0131n\u0131 genel olarak etkinle\u015ftirin" + "local_ip": "Home Asistan\u0131n\u0131n Yerel IP'si", + "multicast_group": "\u00c7ok noktaya yay\u0131n grubu", + "multicast_port": "\u00c7ok noktaya yay\u0131n ba\u011flant\u0131 noktas\u0131", + "rate_limit": "H\u0131z s\u0131n\u0131r\u0131", + "state_updater": "Durum g\u00fcncelleyici" + }, + "data_description": { + "individual_address": "Home Assistant taraf\u0131ndan kullan\u0131lacak KNX adresi, \u00f6r. \"0.0.4\"", + "local_ip": "Otomatik ke\u015fif i\u00e7in \"0.0.0.0\"\u0131 kullan\u0131n.", + "multicast_group": "Y\u00f6nlendirme ve ke\u015fif i\u00e7in kullan\u0131l\u0131r. Varsay\u0131lan: \"224.0.23.12\"", + "multicast_port": "Y\u00f6nlendirme ve ke\u015fif i\u00e7in kullan\u0131l\u0131r. Varsay\u0131lan: \"3671\"", + "rate_limit": "Saniyede maksimum giden telegram say\u0131s\u0131.\n \u00d6nerilen: 20 ila 40", + "state_updater": "KNX Bus'tan okuma durumlar\u0131n\u0131 k\u00fcresel olarak etkinle\u015ftirin veya devre d\u0131\u015f\u0131 b\u0131rak\u0131n. Devre d\u0131\u015f\u0131 b\u0131rak\u0131ld\u0131\u011f\u0131nda, Home Assistant KNX Bus'tan durumlar\u0131 aktif olarak almaz, 'sync_state' varl\u0131k se\u00e7eneklerinin hi\u00e7bir etkisi olmaz." } }, "tunnel": { @@ -62,6 +114,10 @@ "port": "Port", "route_back": "Geri Y\u00f6nlendirme / NAT Modu", "tunneling_type": "KNX T\u00fcnel Tipi" + }, + "data_description": { + "host": "KNX/IP t\u00fcnelleme cihaz\u0131n\u0131n IP adresi.", + "port": "KNX/IP t\u00fcnelleme cihaz\u0131n\u0131n ba\u011flant\u0131 noktas\u0131." } } } diff --git a/homeassistant/components/knx/translations/zh-Hant.json b/homeassistant/components/knx/translations/zh-Hant.json index b6c09456fb3..8f740b85f6d 100644 --- a/homeassistant/components/knx/translations/zh-Hant.json +++ b/homeassistant/components/knx/translations/zh-Hant.json @@ -5,29 +5,73 @@ "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557" + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "file_not_found": "\u8def\u5f91 config/.storage/knx/ \u5167\u627e\u4e0d\u5230\u6307\u5b9a `.knxkeys` \u6a94\u6848", + "invalid_individual_address": "\u6578\u503c\u8207 KNX \u500b\u5225\u4f4d\u5740\u4e0d\u76f8\u7b26\u3002\n'area.line.device'", + "invalid_ip_address": "IPv4 \u4f4d\u5740\u7121\u6548\u3002", + "invalid_signature": "\u52a0\u5bc6 `.knxkeys` \u6a94\u6848\u5bc6\u78bc\u932f\u8aa4\u3002" }, "step": { "manual_tunnel": { "data": { "host": "\u4e3b\u6a5f\u7aef", "individual_address": "\u9023\u7dda\u500b\u5225\u4f4d\u5740", - "local_ip": "Home Assistant \u672c\u5730\u7aef IP\uff08\u4fdd\u7559\u7a7a\u767d\u4ee5\u81ea\u52d5\u5075\u6e2c\uff09", + "local_ip": "Home Assistant \u672c\u5730\u7aef IP", "port": "\u901a\u8a0a\u57e0", "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f", "tunneling_type": "KNX \u901a\u9053\u985e\u5225" }, + "data_description": { + "host": "KNX/IP \u901a\u9053\u88dd\u7f6e IP \u4f4d\u5740\u3002", + "local_ip": "\u4fdd\u6301\u7a7a\u767d\u4ee5\u4f7f\u7528\u81ea\u52d5\u641c\u7d22\u3002", + "port": "KNX/IP \u901a\u9053\u88dd\u7f6e\u901a\u8a0a\u57e0\u3002" + }, "description": "\u8acb\u8f38\u5165\u901a\u9053\u88dd\u7f6e\u7684\u9023\u7dda\u8cc7\u8a0a\u3002" }, "routing": { "data": { - "individual_address": "\u8def\u7531\u9023\u7dda\u500b\u5225\u4f4d\u5740", - "local_ip": "Home Assistant \u672c\u5730\u7aef IP\uff08\u4fdd\u7559\u7a7a\u767d\u4ee5\u81ea\u52d5\u5075\u6e2c\uff09", - "multicast_group": "\u4f7f\u7528\u65bc\u8def\u7531\u7684 Multicast \u7fa4\u7d44", - "multicast_port": "\u4f7f\u7528\u65bc\u8def\u7531\u7684 Multicast \u901a\u8a0a\u57e0" + "individual_address": "\u500b\u5225\u4f4d\u5740", + "local_ip": "Home Assistant \u672c\u5730\u7aef IP", + "multicast_group": "Multicast \u7fa4\u7d44", + "multicast_port": "Multicast \u901a\u8a0a\u57e0" + }, + "data_description": { + "individual_address": "Home Assistant \u6240\u4f7f\u7528\u4e4b KNX \u4f4d\u5740\u3002\u4f8b\u5982\uff1a`0.0.4`", + "local_ip": "\u4fdd\u6301\u7a7a\u767d\u4ee5\u4f7f\u7528\u81ea\u52d5\u641c\u7d22\u3002" }, "description": "\u8acb\u8a2d\u5b9a\u8def\u7531\u9078\u9805\u3002" }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "`.knxkeys` \u6a94\u6848\u5168\u540d\uff08\u5305\u542b\u526f\u6a94\u540d\uff09", + "knxkeys_password": "\u52a0\u5bc6 `.knxkeys` \u6a94\u6848\u5bc6\u78bc" + }, + "data_description": { + "knxkeys_filename": "\u6a94\u6848\u61c9\u8a72\u4f4d\u65bc\u8a2d\u5b9a\u8cc7\u6599\u593e `.storage/knx/` \u5167\u3002\n\u82e5\u70ba Home Assistant OS\u3001\u5247\u61c9\u8a72\u70ba `/config/.storage/knx/`\n\u4f8b\u5982\uff1a`my_project.knxkeys`", + "knxkeys_password": "\u81ea ETS \u532f\u51fa\u6a94\u6848\u4e2d\u9032\u884c\u8a2d\u5b9a\u3002" + }, + "description": "\u8acb\u8f38\u5165 `.knxkeys` \u6a94\u6848\u8cc7\u8a0a\u3002" + }, + "secure_manual": { + "data": { + "device_authentication": "\u88dd\u7f6e\u8a8d\u8b49\u5bc6\u78bc", + "user_id": "\u4f7f\u7528\u8005 ID", + "user_password": "\u4f7f\u7528\u8005\u5bc6\u78bc" + }, + "data_description": { + "device_authentication": "\u65bc EST \u4ecb\u9762\u4e2d 'IP' \u9762\u677f\u9032\u884c\u8a2d\u5b9a\u3002", + "user_id": "\u901a\u5e38\u70ba\u901a\u9053\u6578 +1\u3002\u56e0\u6b64 'Tunnel 2' \u5c07\u5177\u6709\u4f7f\u7528\u8005 ID '3'\u3002", + "user_password": "\u65bc ETS \u901a\u9053 'Properties' \u9762\u677f\u53ef\u8a2d\u5b9a\u6307\u5b9a\u901a\u9053\u9023\u7dda\u5bc6\u78bc\u3002" + }, + "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002" + }, + "secure_tunneling": { + "description": "\u9078\u64c7\u5982\u4f55\u8a2d\u5b9a KNX/IP \u52a0\u5bc6\u3002", + "menu_options": { + "secure_knxkeys": "\u4f7f\u7528\u5305\u542b IP \u52a0\u5bc6\u91d1\u8000\u7684 knxkeys \u6a94\u6848", + "secure_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6\u91d1\u8000" + } + }, "tunnel": { "data": { "gateway": "KNX \u901a\u9053\u9023\u7dda" @@ -48,11 +92,19 @@ "data": { "connection_type": "KNX \u9023\u7dda\u985e\u5225", "individual_address": "\u9810\u8a2d\u500b\u5225\u4f4d\u5740", - "local_ip": "Home Assistant \u672c\u5730\u7aef IP\uff08\u586b\u5165 0.0.0.0 \u555f\u7528\u81ea\u52d5\u5075\u6e2c\uff09", - "multicast_group": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u641c\u7d22\u7684 Multicast \u7fa4\u7d44", - "multicast_port": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u641c\u7d22\u7684 Multicast \u901a\u8a0a\u57e0", - "rate_limit": "\u6700\u5927\u6bcf\u79d2\u767c\u51fa Telegram", - "state_updater": "\u7531 KNX Bus \u8b80\u53d6\u72c0\u614b\u5168\u555f\u7528" + "local_ip": "Home Assistant \u672c\u5730\u7aef IP", + "multicast_group": "Multicast \u7fa4\u7d44", + "multicast_port": "Multicast \u901a\u8a0a\u57e0", + "rate_limit": "\u983b\u7387\u9650\u5236", + "state_updater": "\u88dd\u614b\u66f4\u65b0\u5668" + }, + "data_description": { + "individual_address": "Home Assistant \u6240\u4f7f\u7528\u4e4b KNX \u4f4d\u5740\u3002\u4f8b\u5982\uff1a`0.0.4`", + "local_ip": "\u4f7f\u7528 `0.0.0.0` \u9032\u884c\u81ea\u52d5\u641c\u7d22\u3002", + "multicast_group": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u81ea\u52d5\u641c\u7d22\u3002\u9810\u8a2d\u503c\uff1a`224.0.23.12`", + "multicast_port": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u81ea\u52d5\u641c\u7d22\u3002\u9810\u8a2d\u503c\uff1a`3671`", + "rate_limit": "\u6bcf\u79d2\u6700\u5927 Telegram \u767c\u9001\u91cf\u3002\u5efa\u8b70\uff1a20 - 40", + "state_updater": "\u5168\u5c40\u958b\u555f\u6216\u95dc\u9589\u81ea KNX Bus \u8b80\u53d6\u72c0\u614b\u3002\u7576\u95dc\u9589\u6642\u3001Home Assistant \u5c07\u4e0d\u6703\u4e3b\u52d5\u5f9e KNX Bus \u7372\u53d6\u72c0\u614b\uff0c`sync_state` \u5be6\u9ad4\u9078\u9805\u5c07\u4e0d\u5177\u6548\u679c\u3002" } }, "tunnel": { @@ -62,6 +114,10 @@ "port": "\u901a\u8a0a\u57e0", "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f", "tunneling_type": "KNX \u901a\u9053\u985e\u5225" + }, + "data_description": { + "host": "KNX/IP \u901a\u9053\u88dd\u7f6e IP \u4f4d\u5740\u3002", + "port": "KNX/IP \u901a\u9053\u88dd\u7f6e\u901a\u8a0a\u57e0\u3002" } } } diff --git a/homeassistant/components/konnected/translations/fr.json b/homeassistant/components/konnected/translations/fr.json index 2ddd335fa9e..4b84efa351d 100644 --- a/homeassistant/components/konnected/translations/fr.json +++ b/homeassistant/components/konnected/translations/fr.json @@ -97,7 +97,7 @@ "options_switch": { "data": { "activation": "Sortie lorsque activ\u00e9", - "momentary": "Dur\u00e9e de l'impulsion (ms) (facultatif)", + "momentary": "Dur\u00e9e de l'impulsion (en millisecondes, facultatif)", "more_states": "Configurer des \u00e9tats suppl\u00e9mentaires pour cette zone", "name": "Nom (facultatif)", "pause": "Pause entre les impulsions (ms) (facultatif)", diff --git a/homeassistant/components/konnected/translations/hu.json b/homeassistant/components/konnected/translations/hu.json index e634727593b..c40b1823424 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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "not_konn_panel": "Nem felismert Konnected.io eszk\u00f6z", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" @@ -41,7 +41,7 @@ "options_binary": { "data": { "inverse": "Invert\u00e1lja a nyitott/z\u00e1rt \u00e1llapotot", - "name": "N\u00e9v (nem k\u00f6telez\u0151)", + "name": "Elnevez\u00e9s (nem k\u00f6telez\u0151)", "type": "Bin\u00e1ris \u00e9rz\u00e9kel\u0151 t\u00edpusa" }, "description": "{zone} opci\u00f3k", @@ -49,7 +49,7 @@ }, "options_digital": { "data": { - "name": "N\u00e9v (nem k\u00f6telez\u0151)", + "name": "Elnevez\u00e9s (nem k\u00f6telez\u0151)", "poll_interval": "Lek\u00e9rdez\u00e9si id\u0151k\u00f6z (perc) (opcion\u00e1lis)", "type": "\u00c9rz\u00e9kel\u0151 t\u00edpusa" }, @@ -99,7 +99,7 @@ "activation": "Kimenet bekapcsolt \u00e1llapotban", "momentary": "Impulzus id\u0151tartama (ms) (opcion\u00e1lis)", "more_states": "Tov\u00e1bbi \u00e1llapotok konfigur\u00e1l\u00e1sa ehhez a z\u00f3n\u00e1hoz", - "name": "N\u00e9v (nem k\u00f6telez\u0151)", + "name": "Elnevez\u00e9s (nem k\u00f6telez\u0151)", "pause": "Sz\u00fcnet impulzusok k\u00f6z\u00f6tt (ms) (opcion\u00e1lis)", "repeat": "Ism\u00e9tl\u00e9si id\u0151k (-1 = v\u00e9gtelen) (opcion\u00e1lis)" }, diff --git a/homeassistant/components/konnected/translations/it.json b/homeassistant/components/konnected/translations/it.json index 81127f0dff2..682a27b50d0 100644 --- a/homeassistant/components/konnected/translations/it.json +++ b/homeassistant/components/konnected/translations/it.json @@ -34,8 +34,8 @@ }, "error": { "bad_host": "URL host API di sostituzione non valido", - "one": "Pi\u00f9", - "other": "Altri" + "one": "Vuoto", + "other": "Vuoti" }, "step": { "options_binary": { @@ -92,7 +92,7 @@ "override_api_host": "Sovrascrivi l'URL predefinito del pannello host API di Home Assistant" }, "description": "Seleziona il comportamento desiderato per il tuo pannello", - "title": "Configura Altro" + "title": "Configura altro" }, "options_switch": { "data": { diff --git a/homeassistant/components/konnected/translations/pt-BR.json b/homeassistant/components/konnected/translations/pt-BR.json index b49b487ab0d..e3079fc50f7 100644 --- a/homeassistant/components/konnected/translations/pt-BR.json +++ b/homeassistant/components/konnected/translations/pt-BR.json @@ -102,7 +102,7 @@ "repeat": "Intervalo para repetir (-1=infinito) (opcional)" }, "description": "Selecione as op\u00e7\u00f5es para o switch conectado a {zone}: estado {state}", - "title": "Configurar o switch de sa\u00edda" + "title": "Configure o interruptor de sa\u00edda" } } } diff --git a/homeassistant/components/kostal_plenticore/translations/ca.json b/homeassistant/components/kostal_plenticore/translations/ca.json index 2ce39d904a6..fe22d6e3acd 100644 --- a/homeassistant/components/kostal_plenticore/translations/ca.json +++ b/homeassistant/components/kostal_plenticore/translations/ca.json @@ -16,6 +16,5 @@ } } } - }, - "title": "Inversor solar Kostal Plenticore" + } } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/de.json b/homeassistant/components/kostal_plenticore/translations/de.json index dfd568f937c..095487fff3f 100644 --- a/homeassistant/components/kostal_plenticore/translations/de.json +++ b/homeassistant/components/kostal_plenticore/translations/de.json @@ -16,6 +16,5 @@ } } } - }, - "title": "Kostal Plenticore Solar-Wechselrichter" + } } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/el.json b/homeassistant/components/kostal_plenticore/translations/el.json index 518070a76ef..c29ca36535e 100644 --- a/homeassistant/components/kostal_plenticore/translations/el.json +++ b/homeassistant/components/kostal_plenticore/translations/el.json @@ -16,6 +16,5 @@ } } } - }, - "title": "\u0397\u03bb\u03b9\u03b1\u03ba\u03cc\u03c2 \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ad\u03b1\u03c2 Kostal Plenticore" + } } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/en.json b/homeassistant/components/kostal_plenticore/translations/en.json index a058336b077..f9334da7aad 100644 --- a/homeassistant/components/kostal_plenticore/translations/en.json +++ b/homeassistant/components/kostal_plenticore/translations/en.json @@ -16,6 +16,5 @@ } } } - }, - "title": "Kostal Plenticore Solar Inverter" + } } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/es.json b/homeassistant/components/kostal_plenticore/translations/es.json index e763acfbe4d..66eed9e0642 100644 --- a/homeassistant/components/kostal_plenticore/translations/es.json +++ b/homeassistant/components/kostal_plenticore/translations/es.json @@ -16,6 +16,5 @@ } } } - }, - "title": "Inversor solar Kostal Plenticore" + } } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/et.json b/homeassistant/components/kostal_plenticore/translations/et.json index c96935d5db8..6b090a0c959 100644 --- a/homeassistant/components/kostal_plenticore/translations/et.json +++ b/homeassistant/components/kostal_plenticore/translations/et.json @@ -16,6 +16,5 @@ } } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/fr.json b/homeassistant/components/kostal_plenticore/translations/fr.json index 66888e8879b..c0f59229275 100644 --- a/homeassistant/components/kostal_plenticore/translations/fr.json +++ b/homeassistant/components/kostal_plenticore/translations/fr.json @@ -16,6 +16,5 @@ } } } - }, - "title": "Onduleur solaire Kostal Plenticore" + } } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/hu.json b/homeassistant/components/kostal_plenticore/translations/hu.json index 3ffe413a82b..10e87e8deca 100644 --- a/homeassistant/components/kostal_plenticore/translations/hu.json +++ b/homeassistant/components/kostal_plenticore/translations/hu.json @@ -16,6 +16,5 @@ } } } - }, - "title": "Kostal Plenticore szol\u00e1r inverter" + } } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/id.json b/homeassistant/components/kostal_plenticore/translations/id.json index c249355f8ca..90c843f5d4d 100644 --- a/homeassistant/components/kostal_plenticore/translations/id.json +++ b/homeassistant/components/kostal_plenticore/translations/id.json @@ -16,6 +16,5 @@ } } } - }, - "title": "Solar Inverter Kostal Plenticore" + } } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/it.json b/homeassistant/components/kostal_plenticore/translations/it.json index 8e46b765fe0..b2b7b688bc4 100644 --- a/homeassistant/components/kostal_plenticore/translations/it.json +++ b/homeassistant/components/kostal_plenticore/translations/it.json @@ -16,6 +16,5 @@ } } } - }, - "title": "Inverter solare Kostal Plenticore" + } } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/ja.json b/homeassistant/components/kostal_plenticore/translations/ja.json index 8a16c8c918e..0526d7f7729 100644 --- a/homeassistant/components/kostal_plenticore/translations/ja.json +++ b/homeassistant/components/kostal_plenticore/translations/ja.json @@ -16,6 +16,5 @@ } } } - }, - "title": "Kostal Plenticore Solar Inverter" + } } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/nl.json b/homeassistant/components/kostal_plenticore/translations/nl.json index 83a77fb6e0d..05bdafd606b 100644 --- a/homeassistant/components/kostal_plenticore/translations/nl.json +++ b/homeassistant/components/kostal_plenticore/translations/nl.json @@ -16,6 +16,5 @@ } } } - }, - "title": "Kostal Plenticore omvormer voor zonne-energie" + } } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/no.json b/homeassistant/components/kostal_plenticore/translations/no.json index 0f0d77a83e6..b79a6b1795c 100644 --- a/homeassistant/components/kostal_plenticore/translations/no.json +++ b/homeassistant/components/kostal_plenticore/translations/no.json @@ -16,6 +16,5 @@ } } } - }, - "title": "Kostal Plenticore Solar Inverter" + } } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/pl.json b/homeassistant/components/kostal_plenticore/translations/pl.json index 781bddfc979..c9025af7205 100644 --- a/homeassistant/components/kostal_plenticore/translations/pl.json +++ b/homeassistant/components/kostal_plenticore/translations/pl.json @@ -16,6 +16,5 @@ } } } - }, - "title": "Inwerter solarny Kostal Plenticore" + } } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/pt-BR.json b/homeassistant/components/kostal_plenticore/translations/pt-BR.json index a670c5a41be..b829ba6e92b 100644 --- a/homeassistant/components/kostal_plenticore/translations/pt-BR.json +++ b/homeassistant/components/kostal_plenticore/translations/pt-BR.json @@ -16,6 +16,5 @@ } } } - }, - "title": "Inversor Solar Kostal Plenticore" + } } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/ru.json b/homeassistant/components/kostal_plenticore/translations/ru.json index d272fd0f304..02ecda0034c 100644 --- a/homeassistant/components/kostal_plenticore/translations/ru.json +++ b/homeassistant/components/kostal_plenticore/translations/ru.json @@ -16,6 +16,5 @@ } } } - }, - "title": "Kostal Plenticore Solar Inverter" + } } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/tr.json b/homeassistant/components/kostal_plenticore/translations/tr.json index f0742d20e78..78ade55a949 100644 --- a/homeassistant/components/kostal_plenticore/translations/tr.json +++ b/homeassistant/components/kostal_plenticore/translations/tr.json @@ -16,6 +16,5 @@ } } } - }, - "title": "Kostal Plenticore Solar \u0130nverter" + } } \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/translations/zh-Hant.json b/homeassistant/components/kostal_plenticore/translations/zh-Hant.json index f115cf74c89..fa1b3d064be 100644 --- a/homeassistant/components/kostal_plenticore/translations/zh-Hant.json +++ b/homeassistant/components/kostal_plenticore/translations/zh-Hant.json @@ -16,6 +16,5 @@ } } } - }, - "title": "Kostal Plenticore \u592a\u967d\u80fd\u63db\u6d41\u5668" + } } \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/it.json b/homeassistant/components/kraken/translations/it.json index 4b646be7a8d..2fabbc9ad5c 100644 --- a/homeassistant/components/kraken/translations/it.json +++ b/homeassistant/components/kraken/translations/it.json @@ -4,14 +4,14 @@ "already_configured": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "error": { - "one": "Pi\u00f9", - "other": "Altri" + "one": "Vuoto", + "other": "Vuoti" }, "step": { "user": { "data": { - "one": "Pi\u00f9", - "other": "Altri" + "one": "Vuoto", + "other": "Vuoti" }, "description": "Vuoi iniziare la configurazione?" } diff --git a/homeassistant/components/light/translations/hu.json b/homeassistant/components/light/translations/hu.json index 0be84e20763..986fd5d1787 100644 --- a/homeassistant/components/light/translations/hu.json +++ b/homeassistant/components/light/translations/hu.json @@ -13,6 +13,7 @@ "is_on": "{entity_name} fel van kapcsolva" }, "trigger_type": { + "changed_states": "{entity_name} be- vagy kikapcsolt", "toggled": "{entity_name} \u00e1tkapcsolt", "turned_off": "{entity_name} le lett kapcsolva", "turned_on": "{entity_name} fel lett kapcsolva" diff --git a/homeassistant/components/logi_circle/translations/hu.json b/homeassistant/components/logi_circle/translations/hu.json index f79ab3944dc..947f11e4907 100644 --- a/homeassistant/components/logi_circle/translations/hu.json +++ b/homeassistant/components/logi_circle/translations/hu.json @@ -8,12 +8,12 @@ }, "error": { "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", - "follow_link": "K\u00e9rem, k\u00f6vesse a hivatkoz\u00e1st \u00e9s hiteles\u00edtse mag\u00e1t miel\u0151tt megnyomn\u00e1 a K\u00fcld\u00e9s gombot", + "follow_link": "K\u00e9rem, k\u00f6vesse a hivatkoz\u00e1st \u00e9s hiteles\u00edtse mag\u00e1t miel\u0151tt folytatn\u00e1", "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 **Mehet** gombot. \n\n [Link]({authorization_url})", "title": "Hiteles\u00edt\u00e9s a LogiCircle seg\u00edts\u00e9g\u00e9vel" }, "user": { diff --git a/homeassistant/components/logi_circle/translations/it.json b/homeassistant/components/logi_circle/translations/it.json index 1299c4c53e4..fe17a15cae3 100644 --- a/homeassistant/components/logi_circle/translations/it.json +++ b/homeassistant/components/logi_circle/translations/it.json @@ -4,7 +4,7 @@ "already_configured": "L'account \u00e8 gi\u00e0 configurato", "external_error": "Si \u00e8 verificata un'eccezione da un altro flusso.", "external_setup": "Logi Circle configurato con successo da un altro flusso.", - "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione." + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione." }, "error": { "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", diff --git a/homeassistant/components/lookin/translations/hu.json b/homeassistant/components/lookin/translations/hu.json index ab18b579bd4..b6db30f8d99 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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" }, @@ -15,7 +15,7 @@ "step": { "device_name": { "data": { - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" } }, "discovery_confirm": { diff --git a/homeassistant/components/luftdaten/translations/hu.json b/homeassistant/components/luftdaten/translations/hu.json index f7a241533da..7439546f796 100644 --- a/homeassistant/components/luftdaten/translations/hu.json +++ b/homeassistant/components/luftdaten/translations/hu.json @@ -9,7 +9,7 @@ "user": { "data": { "show_on_map": "Megjelen\u00edt\u00e9s a t\u00e9rk\u00e9pen", - "station_id": "Luftdaten \u00e9rz\u00e9kel\u0151 ID" + "station_id": "\u00c9rz\u00e9kel\u0151 azonos\u00edt\u00f3" }, "title": "Luftdaten be\u00e1ll\u00edt\u00e1sa" } diff --git a/homeassistant/components/luftdaten/translations/pt-BR.json b/homeassistant/components/luftdaten/translations/pt-BR.json index b4cdaf000ab..82b1f09735b 100644 --- a/homeassistant/components/luftdaten/translations/pt-BR.json +++ b/homeassistant/components/luftdaten/translations/pt-BR.json @@ -8,7 +8,7 @@ "step": { "user": { "data": { - "show_on_map": "Mostrar no mapa?", + "show_on_map": "Mostrar no mapa", "station_id": "ID do Sensor Luftdaten" }, "title": "Definir Luftdaten" diff --git a/homeassistant/components/lutron_caseta/translations/pl.json b/homeassistant/components/lutron_caseta/translations/pl.json index bc4f9d61f39..47e6a07e146 100644 --- a/homeassistant/components/lutron_caseta/translations/pl.json +++ b/homeassistant/components/lutron_caseta/translations/pl.json @@ -23,7 +23,7 @@ "host": "Nazwa hosta lub adres IP" }, "description": "Wprowad\u017a adres IP urz\u0105dzenia", - "title": "Po\u0142\u0105cz si\u0119 automatycznie z mostkiem" + "title": "Automatyczne po\u0142\u0105czenie z mostkiem" } } }, diff --git a/homeassistant/components/lyric/translations/hu.json b/homeassistant/components/lyric/translations/hu.json index 7586310c8a7..b7eac97525c 100644 --- a/homeassistant/components/lyric/translations/hu.json +++ b/homeassistant/components/lyric/translations/hu.json @@ -10,7 +10,7 @@ }, "step": { "pick_implementation": { - "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" }, "reauth_confirm": { "description": "A Lyric integr\u00e1ci\u00f3nak \u00fajra hiteles\u00edtenie kell a fi\u00f3kj\u00e1t.", diff --git a/homeassistant/components/lyric/translations/it.json b/homeassistant/components/lyric/translations/it.json index c544598079e..6fb3fb44275 100644 --- a/homeassistant/components/lyric/translations/it.json +++ b/homeassistant/components/lyric/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", - "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "create_entry": { diff --git a/homeassistant/components/mailgun/translations/ja.json b/homeassistant/components/mailgun/translations/ja.json index 58818dd99de..34c6ced3f38 100644 --- a/homeassistant/components/mailgun/translations/ja.json +++ b/homeassistant/components/mailgun/translations/ja.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { - "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[Webhooks with Mailgun]({mailgun_url})\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u4ee5\u4e0b\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n- Content Type: application/x-www-fjsorm-urlencoded\n\n\u53d7\u4fe1\u30c7\u30fc\u30bf\u3092\u51e6\u7406\u3059\u308b\u305f\u3081\u306b\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[Webhooks with Mailgun]({mailgun_url})\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u4ee5\u4e0b\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n- Content Type(\u30b3\u30f3\u30c6\u30f3\u30c4\u306e\u7a2e\u985e): application/x-www-fjsorm-urlencoded\n\n\u53d7\u4fe1\u30c7\u30fc\u30bf\u3092\u51e6\u7406\u3059\u308b\u305f\u3081\u306b\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { "user": { diff --git a/homeassistant/components/mazda/translations/ca.json b/homeassistant/components/mazda/translations/ca.json index 17ef370b007..2289ba3986c 100644 --- a/homeassistant/components/mazda/translations/ca.json +++ b/homeassistant/components/mazda/translations/ca.json @@ -17,10 +17,8 @@ "password": "Contrasenya", "region": "Regi\u00f3" }, - "description": "Introdueix el correu electr\u00f2nic i la contrasenya que utilitzes per iniciar sessi\u00f3 a l'aplicaci\u00f3 de m\u00f2bil MyMazda.", - "title": "Serveis connectats de Mazda - Afegeix un compte" + "description": "Introdueix el correu electr\u00f2nic i la contrasenya que utilitzes per iniciar sessi\u00f3 a l'aplicaci\u00f3 de m\u00f2bil MyMazda." } } - }, - "title": "Serveis connectats de Mazda" + } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/de.json b/homeassistant/components/mazda/translations/de.json index 58409ec9b8a..ba116a6fe2c 100644 --- a/homeassistant/components/mazda/translations/de.json +++ b/homeassistant/components/mazda/translations/de.json @@ -17,10 +17,8 @@ "password": "Passwort", "region": "Region" }, - "description": "Bitte gib die E-Mail-Adresse und das Passwort ein, die du f\u00fcr die Anmeldung bei der MyMazda Mobile App verwendest.", - "title": "Mazda Connected Services - Konto hinzuf\u00fcgen" + "description": "Bitte gib die E-Mail-Adresse und das Passwort ein, die du f\u00fcr die Anmeldung bei der MyMazda Mobile App verwendest." } } - }, - "title": "Mazda Connected Services" + } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/el.json b/homeassistant/components/mazda/translations/el.json index c95e8ad4747..7fbf538de63 100644 --- a/homeassistant/components/mazda/translations/el.json +++ b/homeassistant/components/mazda/translations/el.json @@ -17,10 +17,8 @@ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "region": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae" }, - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf\u03c5 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae MyMazda \u03b3\u03b9\u03b1 \u03ba\u03b9\u03bd\u03b7\u03c4\u03ac.", - "title": "Mazda Connected Services - \u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd" + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b5\u03af\u03bf\u03c5 \u03ba\u03b1\u03b9 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae MyMazda \u03b3\u03b9\u03b1 \u03ba\u03b9\u03bd\u03b7\u03c4\u03ac." } } - }, - "title": "Mazda Connected Services" + } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/en.json b/homeassistant/components/mazda/translations/en.json index b483947aaa0..9130a1b6334 100644 --- a/homeassistant/components/mazda/translations/en.json +++ b/homeassistant/components/mazda/translations/en.json @@ -17,10 +17,8 @@ "password": "Password", "region": "Region" }, - "description": "Please enter the email address and password you use to log into the MyMazda mobile app.", - "title": "Mazda Connected Services - Add Account" + "description": "Please enter the email address and password you use to log into the MyMazda mobile app." } } - }, - "title": "Mazda Connected Services" + } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/es.json b/homeassistant/components/mazda/translations/es.json index a9ffa26787c..f0ba0f4da49 100644 --- a/homeassistant/components/mazda/translations/es.json +++ b/homeassistant/components/mazda/translations/es.json @@ -17,10 +17,8 @@ "password": "Contrase\u00f1a", "region": "Regi\u00f3n" }, - "description": "Introduce la direcci\u00f3n de correo electr\u00f3nico y la contrase\u00f1a que utilizas para iniciar sesi\u00f3n en la aplicaci\u00f3n m\u00f3vil MyMazda.", - "title": "Servicios Conectados de Mazda - A\u00f1adir cuenta" + "description": "Introduce la direcci\u00f3n de correo electr\u00f3nico y la contrase\u00f1a que utilizas para iniciar sesi\u00f3n en la aplicaci\u00f3n m\u00f3vil MyMazda." } } - }, - "title": "Servicios Conectados de Mazda" + } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/et.json b/homeassistant/components/mazda/translations/et.json index 39ead99765c..1b6cac11093 100644 --- a/homeassistant/components/mazda/translations/et.json +++ b/homeassistant/components/mazda/translations/et.json @@ -17,10 +17,8 @@ "password": "Salas\u00f5na", "region": "Piirkond" }, - "description": "Sisesta e-posti aadress ja salas\u00f5na mida kasutad MyMazda mobiilirakendusse sisselogimiseks.", - "title": "Mazda Connected Services - lisa konto" + "description": "Sisesta e-posti aadress ja salas\u00f5na mida kasutad MyMazda mobiilirakendusse sisselogimiseks." } } - }, - "title": "Mazda Connected Services" + } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/fr.json b/homeassistant/components/mazda/translations/fr.json index 8024852de46..d42ab5453c4 100644 --- a/homeassistant/components/mazda/translations/fr.json +++ b/homeassistant/components/mazda/translations/fr.json @@ -17,10 +17,8 @@ "password": "Mot de passe", "region": "R\u00e9gion" }, - "description": "Veuillez saisir l'adresse e-mail et le mot de passe que vous utilisez pour vous connecter \u00e0 l'application mobile MyMazda.", - "title": "Services connect\u00e9s Mazda - Ajouter un compte" + "description": "Veuillez saisir l'adresse e-mail et le mot de passe que vous utilisez pour vous connecter \u00e0 l'application mobile MyMazda." } } - }, - "title": "Services connect\u00e9s Mazda" + } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/hu.json b/homeassistant/components/mazda/translations/hu.json index ec62b456021..42afc763687 100644 --- a/homeassistant/components/mazda/translations/hu.json +++ b/homeassistant/components/mazda/translations/hu.json @@ -17,10 +17,8 @@ "password": "Jelsz\u00f3", "region": "R\u00e9gi\u00f3" }, - "description": "K\u00e9rj\u00fck, adja meg azt az e-mail c\u00edmet \u00e9s jelsz\u00f3t, amelyet a MyMazda mobilalkalmaz\u00e1sba val\u00f3 bejelentkez\u00e9shez haszn\u00e1lt.", - "title": "Mazda Connected Services - Fi\u00f3k hozz\u00e1ad\u00e1sa" + "description": "K\u00e9rj\u00fck, adja meg azt az e-mail c\u00edmet \u00e9s jelsz\u00f3t, amelyet a MyMazda mobilalkalmaz\u00e1sba val\u00f3 bejelentkez\u00e9shez haszn\u00e1lt." } } - }, - "title": "Mazda Connected Services" + } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/id.json b/homeassistant/components/mazda/translations/id.json index fdcc736162e..cd4f73f23f6 100644 --- a/homeassistant/components/mazda/translations/id.json +++ b/homeassistant/components/mazda/translations/id.json @@ -17,10 +17,8 @@ "password": "Kata Sandi", "region": "Wilayah" }, - "description": "Masukkan alamat email dan kata sandi yang digunakan untuk masuk ke aplikasi seluler MyMazda.", - "title": "Mazda Connected Services - Tambahkan Akun" + "description": "Masukkan alamat email dan kata sandi yang digunakan untuk masuk ke aplikasi seluler MyMazda." } } - }, - "title": "Mazda Connected Services" + } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/it.json b/homeassistant/components/mazda/translations/it.json index 21d003b030b..920e2b5ce3c 100644 --- a/homeassistant/components/mazda/translations/it.json +++ b/homeassistant/components/mazda/translations/it.json @@ -17,10 +17,8 @@ "password": "Password", "region": "Area geografica" }, - "description": "Inserisci l'indirizzo email e la password che utilizzi per accedere all'app mobile MyMazda.", - "title": "Mazda Connected Services - Aggiungi account" + "description": "Inserisci l'indirizzo email e la password che utilizzi per accedere all'app mobile MyMazda." } } - }, - "title": "Mazda Connected Services" + } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/ja.json b/homeassistant/components/mazda/translations/ja.json index 3bf5b7f88b3..690f5a097de 100644 --- a/homeassistant/components/mazda/translations/ja.json +++ b/homeassistant/components/mazda/translations/ja.json @@ -17,10 +17,8 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" }, - "description": "MyMazda\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u306b\u30ed\u30b0\u30a4\u30f3\u3059\u308b\u969b\u306b\u4f7f\u7528\u3059\u308b\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u30de\u30c4\u30c0 \u30b3\u30cd\u30af\u30c6\u30c3\u30c9\u30b5\u30fc\u30d3\u30b9 - \u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u8ffd\u52a0" + "description": "MyMazda\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u306b\u30ed\u30b0\u30a4\u30f3\u3059\u308b\u969b\u306b\u4f7f\u7528\u3059\u308b\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } - }, - "title": "\u30de\u30c4\u30c0 \u30b3\u30cd\u30af\u30c6\u30c3\u30c9\u30b5\u30fc\u30d3\u30b9" + } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/ko.json b/homeassistant/components/mazda/translations/ko.json index 6023a053a7e..b95170c6bbd 100644 --- a/homeassistant/components/mazda/translations/ko.json +++ b/homeassistant/components/mazda/translations/ko.json @@ -17,10 +17,8 @@ "password": "\ube44\ubc00\ubc88\ud638", "region": "\uc9c0\uc5ed" }, - "description": "MyMazda \ubaa8\ubc14\uc77c \uc571\uc5d0 \ub85c\uadf8\uc778\ud558\uae30 \uc704\ud574 \uc0ac\uc6a9\ud558\ub294 \uc774\uba54\uc77c \uc8fc\uc18c\uc640 \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", - "title": "Mazda Connected Services - \uacc4\uc815 \ucd94\uac00\ud558\uae30" + "description": "MyMazda \ubaa8\ubc14\uc77c \uc571\uc5d0 \ub85c\uadf8\uc778\ud558\uae30 \uc704\ud574 \uc0ac\uc6a9\ud558\ub294 \uc774\uba54\uc77c \uc8fc\uc18c\uc640 \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." } } - }, - "title": "Mazda Connected Services" + } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/nl.json b/homeassistant/components/mazda/translations/nl.json index 3370fe29bb8..f7532b9fc45 100644 --- a/homeassistant/components/mazda/translations/nl.json +++ b/homeassistant/components/mazda/translations/nl.json @@ -17,10 +17,8 @@ "password": "Wachtwoord", "region": "Regio" }, - "description": "Voer het e-mailadres en wachtwoord in dat u gebruikt om in te loggen op de MyMazda mobiele app.", - "title": "Mazda Connected Services - Account toevoegen" + "description": "Voer het e-mailadres en wachtwoord in dat u gebruikt om in te loggen op de MyMazda mobiele app." } } - }, - "title": "Mazda Connected Services" + } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/no.json b/homeassistant/components/mazda/translations/no.json index 3f4db47d2b0..875e21e8c04 100644 --- a/homeassistant/components/mazda/translations/no.json +++ b/homeassistant/components/mazda/translations/no.json @@ -17,10 +17,8 @@ "password": "Passord", "region": "Region" }, - "description": "Vennligst skriv inn e-postadressen og passordet du bruker for \u00e5 logge p\u00e5 MyMazda-mobilappen.", - "title": "Mazda Connected Services - Legg til konto" + "description": "Vennligst skriv inn e-postadressen og passordet du bruker for \u00e5 logge p\u00e5 MyMazda-mobilappen." } } - }, - "title": "Mazda Connected Services" + } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/pl.json b/homeassistant/components/mazda/translations/pl.json index fdd1a8c5ce9..d4aea82b6c1 100644 --- a/homeassistant/components/mazda/translations/pl.json +++ b/homeassistant/components/mazda/translations/pl.json @@ -17,10 +17,8 @@ "password": "Has\u0142o", "region": "Region" }, - "description": "Wprowad\u017a adres e-mail i has\u0142o, kt\u00f3rych u\u017cywasz do logowania si\u0119 do aplikacji mobilnej MyMazda.", - "title": "Mazda Connected Services - Dodawanie konta" + "description": "Wprowad\u017a adres e-mail i has\u0142o, kt\u00f3rych u\u017cywasz do logowania si\u0119 do aplikacji mobilnej MyMazda." } } - }, - "title": "Mazda Connected Services" + } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/pt-BR.json b/homeassistant/components/mazda/translations/pt-BR.json index 7b28450bfd0..5dcfd6d59ab 100644 --- a/homeassistant/components/mazda/translations/pt-BR.json +++ b/homeassistant/components/mazda/translations/pt-BR.json @@ -17,10 +17,8 @@ "password": "Senha", "region": "Regi\u00e3o" }, - "description": "Digite o endere\u00e7o de e-mail e senha que voc\u00ea usa para entrar no aplicativo MyMazda.", - "title": "Mazda Connected Services - Adicionar conta" + "description": "Digite o endere\u00e7o de e-mail e senha que voc\u00ea usa para entrar no aplicativo MyMazda." } } - }, - "title": "Servi\u00e7os conectados Mazda" + } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/ru.json b/homeassistant/components/mazda/translations/ru.json index cf949416274..4d5082bcc49 100644 --- a/homeassistant/components/mazda/translations/ru.json +++ b/homeassistant/components/mazda/translations/ru.json @@ -17,10 +17,8 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "region": "\u0420\u0435\u0433\u0438\u043e\u043d" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0412\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u0432 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 MyMazda.", - "title": "Mazda Connected Services" + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0412\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u0432 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 MyMazda." } } - }, - "title": "Mazda Connected Services" + } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/tr.json b/homeassistant/components/mazda/translations/tr.json index 6c574d1de0b..788cee66b00 100644 --- a/homeassistant/components/mazda/translations/tr.json +++ b/homeassistant/components/mazda/translations/tr.json @@ -17,10 +17,8 @@ "password": "Parola", "region": "B\u00f6lge" }, - "description": "L\u00fctfen MyMazda mobil uygulamas\u0131na giri\u015f yapmak i\u00e7in kulland\u0131\u011f\u0131n\u0131z e-posta adresini ve \u015fifreyi giriniz.", - "title": "Mazda Connected Services - Hesap Ekle" + "description": "L\u00fctfen MyMazda mobil uygulamas\u0131na giri\u015f yapmak i\u00e7in kulland\u0131\u011f\u0131n\u0131z e-posta adresini ve \u015fifreyi giriniz." } } - }, - "title": "Mazda Connected Services" + } } \ No newline at end of file diff --git a/homeassistant/components/mazda/translations/zh-Hant.json b/homeassistant/components/mazda/translations/zh-Hant.json index 0c0e3f0fd8c..bb52f321766 100644 --- a/homeassistant/components/mazda/translations/zh-Hant.json +++ b/homeassistant/components/mazda/translations/zh-Hant.json @@ -17,10 +17,8 @@ "password": "\u5bc6\u78bc", "region": "\u5340\u57df" }, - "description": "\u8acb\u8f38\u5165\u767b\u5165MyMazda \u884c\u52d5 App \u4e4b Email \u5730\u5740\u8207\u5bc6\u78bc\u3002", - "title": "Mazda Connected \u670d\u52d9 - \u65b0\u589e\u5e33\u865f" + "description": "\u8acb\u8f38\u5165\u767b\u5165MyMazda \u884c\u52d5 App \u4e4b Email \u5730\u5740\u8207\u5bc6\u78bc\u3002" } } - }, - "title": "Mazda Connected \u670d\u52d9" + } } \ No newline at end of file diff --git a/homeassistant/components/meater/translations/bg.json b/homeassistant/components/meater/translations/bg.json new file mode 100644 index 00000000000..e5396fbc999 --- /dev/null +++ b/homeassistant/components/meater/translations/bg.json @@ -0,0 +1,17 @@ +{ + "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_auth_error": "\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" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0430\u043a\u0430\u0443\u043d\u0442\u0430 \u0441\u0438 \u0432 Meater Cloud." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/ca.json b/homeassistant/components/meater/translations/ca.json new file mode 100644 index 00000000000..0174767bc52 --- /dev/null +++ b/homeassistant/components/meater/translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "service_unavailable_error": "L'API no est\u00e0 disponible actualment, torna-ho a provar m\u00e9s tard.", + "unknown_auth_error": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Configura el teu compte de Meater Cloud." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/cs.json b/homeassistant/components/meater/translations/cs.json new file mode 100644 index 00000000000..72c98504526 --- /dev/null +++ b/homeassistant/components/meater/translations/cs.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown_auth_error": "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/meater/translations/de.json b/homeassistant/components/meater/translations/de.json new file mode 100644 index 00000000000..ed143e26b4c --- /dev/null +++ b/homeassistant/components/meater/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "service_unavailable_error": "Die API ist derzeit nicht verf\u00fcgbar. Bitte versuche es sp\u00e4ter erneut.", + "unknown_auth_error": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Richte dein Meater Cloud-Konto ein." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/el.json b/homeassistant/components/meater/translations/el.json new file mode 100644 index 00000000000..2d02110dd49 --- /dev/null +++ b/homeassistant/components/meater/translations/el.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "service_unavailable_error": "\u03a4\u03bf API \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03c0\u03b1\u03c1\u03cc\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03bf, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1.", + "unknown_auth_error": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Meater Cloud." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/et.json b/homeassistant/components/meater/translations/et.json new file mode 100644 index 00000000000..55328467d3a --- /dev/null +++ b/homeassistant/components/meater/translations/et.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_auth": "Tuvastamine nurjus", + "service_unavailable_error": "API pole praegu saadaval, proovi hiljem uuesti.", + "unknown_auth_error": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Seadista Meater Cloudi konto." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/fr.json b/homeassistant/components/meater/translations/fr.json new file mode 100644 index 00000000000..9940cb6e24b --- /dev/null +++ b/homeassistant/components/meater/translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_auth": "Authentification non valide", + "service_unavailable_error": "L'API est actuellement indisponible, veuillez r\u00e9essayer ult\u00e9rieurement.", + "unknown_auth_error": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Configurez votre compte Meater Cloud." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/he.json b/homeassistant/components/meater/translations/he.json new file mode 100644 index 00000000000..f1376b2cf0d --- /dev/null +++ b/homeassistant/components/meater/translations/he.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown_auth_error": "\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/meater/translations/hu.json b/homeassistant/components/meater/translations/hu.json new file mode 100644 index 00000000000..fd55bae3bdd --- /dev/null +++ b/homeassistant/components/meater/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "service_unavailable_error": "Az API jelenleg nem el\u00e9rhet\u0151, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", + "unknown_auth_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "\u00c1ll\u00edtsa be a Meater Cloud fi\u00f3kj\u00e1t." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/id.json b/homeassistant/components/meater/translations/id.json new file mode 100644 index 00000000000..5d9e28c583c --- /dev/null +++ b/homeassistant/components/meater/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentikasi tidak valid", + "service_unavailable_error": "API saat ini tidak tersedia, harap coba lagi nanti.", + "unknown_auth_error": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Siapkan akun Meater Cloud Anda." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/it.json b/homeassistant/components/meater/translations/it.json new file mode 100644 index 00000000000..fe3bc189ef6 --- /dev/null +++ b/homeassistant/components/meater/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_auth": "Autenticazione non valida", + "service_unavailable_error": "L'API non \u00e8 attualmente disponibile, riprova pi\u00f9 tardi.", + "unknown_auth_error": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Configura il tuo account Meater Cloud." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/ja.json b/homeassistant/components/meater/translations/ja.json new file mode 100644 index 00000000000..db4dd2e9c6b --- /dev/null +++ b/homeassistant/components/meater/translations/ja.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "service_unavailable_error": "\u73fe\u5728API\u304c\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\u5f8c\u3067\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", + "unknown_auth_error": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + }, + "description": "Meater Cloud\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/nl.json b/homeassistant/components/meater/translations/nl.json new file mode 100644 index 00000000000..a87175c7574 --- /dev/null +++ b/homeassistant/components/meater/translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_auth": "Ongeldige authenticatie", + "service_unavailable_error": "De API is momenteel niet beschikbaar, probeer het later opnieuw.", + "unknown_auth_error": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Stel uw Meater Cloud-account in." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/no.json b/homeassistant/components/meater/translations/no.json new file mode 100644 index 00000000000..60bec4e6864 --- /dev/null +++ b/homeassistant/components/meater/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_auth": "Ugyldig godkjenning", + "service_unavailable_error": "API-en er for \u00f8yeblikket utilgjengelig, pr\u00f8v igjen senere.", + "unknown_auth_error": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Sett opp din Meater Cloud-konto." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/pl.json b/homeassistant/components/meater/translations/pl.json new file mode 100644 index 00000000000..1816069dd34 --- /dev/null +++ b/homeassistant/components/meater/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie", + "service_unavailable_error": "Interfejs API jest obecnie niedost\u0119pny, spr\u00f3buj ponownie p\u00f3\u017aniej.", + "unknown_auth_error": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Skonfiguruj swoje konto w Meater Cloud." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/pt-BR.json b/homeassistant/components/meater/translations/pt-BR.json new file mode 100644 index 00000000000..103f3f76986 --- /dev/null +++ b/homeassistant/components/meater/translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "service_unavailable_error": "A API est\u00e1 indispon\u00edvel no momento. Tente novamente mais tarde.", + "unknown_auth_error": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "Configure sua conta Meeater Cloud." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/ru.json b/homeassistant/components/meater/translations/ru.json new file mode 100644 index 00000000000..a56f2f8ebbe --- /dev/null +++ b/homeassistant/components/meater/translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "service_unavailable_error": "\u0412 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f API \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", + "unknown_auth_error": "\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" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Meater Cloud." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/tr.json b/homeassistant/components/meater/translations/tr.json new file mode 100644 index 00000000000..03f77d2ed51 --- /dev/null +++ b/homeassistant/components/meater/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "service_unavailable_error": "API \u015fu anda kullan\u0131lam\u0131yor, l\u00fctfen daha sonra tekrar deneyin.", + "unknown_auth_error": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Meater Cloud hesab\u0131n\u0131z\u0131 kurun." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/zh-Hant.json b/homeassistant/components/meater/translations/zh-Hant.json new file mode 100644 index 00000000000..b04f4a54076 --- /dev/null +++ b/homeassistant/components/meater/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "service_unavailable_error": "API \u76ee\u524d\u7121\u6cd5\u4f7f\u7528\uff0c\u8acb\u7a0d\u5019\u518d\u8a66\u3002", + "unknown_auth_error": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8a2d\u5b9a Meater Cloud \u5e33\u865f\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/translations/de.json b/homeassistant/components/media_player/translations/de.json index 653533ba03a..d6468920628 100644 --- a/homeassistant/components/media_player/translations/de.json +++ b/homeassistant/components/media_player/translations/de.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_buffering": "{entity_name} puffert", "is_idle": "{entity_name} ist unt\u00e4tig", "is_off": "{entity_name} ist ausgeschaltet", "is_on": "{entity_name} ist eingeschaltet", @@ -8,6 +9,7 @@ "is_playing": "{entity_name} spielt" }, "trigger_type": { + "buffering": "{entity_name} beginnt mit dem Puffern", "changed_states": "{entity_name} hat den Status ge\u00e4ndert", "idle": "{entity_name} wird inaktiv", "paused": "{entity_name} ist angehalten", @@ -18,6 +20,7 @@ }, "state": { "_": { + "buffering": "Puffern", "idle": "Unt\u00e4tig", "off": "Aus", "on": "An", diff --git a/homeassistant/components/media_player/translations/el.json b/homeassistant/components/media_player/translations/el.json index 242de3e829a..d7819069e26 100644 --- a/homeassistant/components/media_player/translations/el.json +++ b/homeassistant/components/media_player/translations/el.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_buffering": "{entity_name} \u03be\u03b5\u03ba\u03b9\u03bd\u03ac\u03b5\u03b9 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c9\u03c1\u03b9\u03bd\u03ae \u03b1\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7", "is_idle": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03b4\u03c1\u03b1\u03bd\u03ad\u03c2", "is_off": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf", "is_on": "{entity_name} \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf", @@ -8,6 +9,7 @@ "is_playing": "{entity_name} \u03c0\u03b1\u03af\u03b6\u03b5\u03b9" }, "trigger_type": { + "buffering": "{entity_name} \u03be\u03b5\u03ba\u03b9\u03bd\u03ac\u03b5\u03b9 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c9\u03c1\u03b9\u03bd\u03ae \u03b1\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7", "changed_states": "\u03a4\u03bf {entity_name} \u03ac\u03bb\u03bb\u03b1\u03be\u03b5 \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2", "idle": "{entity_name} \u03b3\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03b1\u03b4\u03c1\u03b1\u03bd\u03ad\u03c2", "paused": "{entity_name} \u03c4\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c3\u03b5 \u03c0\u03b1\u03cd\u03c3\u03b7", @@ -18,6 +20,7 @@ }, "state": { "_": { + "buffering": "\u03a0\u03c1\u03bf\u03c3\u03c9\u03c1\u03b9\u03bd\u03ae \u03b1\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7", "idle": "\u03a3\u03b5 \u03b1\u03b4\u03c1\u03ac\u03bd\u03b5\u03b9\u03b1", "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc", diff --git a/homeassistant/components/media_player/translations/et.json b/homeassistant/components/media_player/translations/et.json index 5461600c9d5..ae3293e951f 100644 --- a/homeassistant/components/media_player/translations/et.json +++ b/homeassistant/components/media_player/translations/et.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_buffering": "{entity_name} puhverdab", "is_idle": "{entity_name} on j\u00f5udeolekus", "is_off": "{entity_name} on v\u00e4lja l\u00fclitatud", "is_on": "{entity_name} on sisse l\u00fclitatud", @@ -8,6 +9,7 @@ "is_playing": "{entity_name} m\u00e4ngib" }, "trigger_type": { + "buffering": "{entity_name} alustab puhverdamist", "changed_states": "{entity_name} muutis olekut", "idle": "{entity_name} muutub j\u00f5udeolekusse", "paused": "{entity_name} on pausil", @@ -18,6 +20,7 @@ }, "state": { "_": { + "buffering": "Puhverdamine", "idle": "Ootel", "off": "V\u00e4ljas", "on": "Sees", diff --git a/homeassistant/components/media_player/translations/fr.json b/homeassistant/components/media_player/translations/fr.json index 17a6cbf92b3..d6c41bf5e09 100644 --- a/homeassistant/components/media_player/translations/fr.json +++ b/homeassistant/components/media_player/translations/fr.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_buffering": "{entity_name} est en train de mettre en m\u00e9moire tampon", "is_idle": "{entity_name} est inactif", "is_off": "{entity_name} est d\u00e9sactiv\u00e9", "is_on": "{entity_name} est activ\u00e9", @@ -8,6 +9,7 @@ "is_playing": "{entity_name} joue" }, "trigger_type": { + "buffering": "{entity_name} commence \u00e0 mettre en m\u00e9moire tampon", "changed_states": "{entity_name} a chang\u00e9 d'\u00e9tat", "idle": "{entity_name} devient inactif", "paused": "{entity_name} est mis en pause", @@ -18,6 +20,7 @@ }, "state": { "_": { + "buffering": "Mise en m\u00e9moire tampon", "idle": "Inactif", "off": "D\u00e9sactiv\u00e9", "on": "Activ\u00e9", diff --git a/homeassistant/components/media_player/translations/hu.json b/homeassistant/components/media_player/translations/hu.json index 83b5dc4e122..3a885c70b64 100644 --- a/homeassistant/components/media_player/translations/hu.json +++ b/homeassistant/components/media_player/translations/hu.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_buffering": "{entity_name} pufferel", "is_idle": "{entity_name} t\u00e9tlen", "is_off": "{entity_name} ki van kapcsolva", "is_on": "{entity_name} be van kapcsolva", @@ -8,6 +9,8 @@ "is_playing": "{entity_name} lej\u00e1tszik" }, "trigger_type": { + "buffering": "{entity_name} pufferelni kezd", + "changed_states": "{entity_name} \u00e1llapota megv\u00e1ltozott", "idle": "{entity_name} t\u00e9tlenn\u00e9 v\u00e1lik", "paused": "{entity_name} sz\u00fcneteltetve van", "playing": "{entity_name} megkezdi a lej\u00e1tsz\u00e1st", @@ -17,6 +20,7 @@ }, "state": { "_": { + "buffering": "Pufferel\u00e9s", "idle": "T\u00e9tlen", "off": "Ki", "on": "Be", diff --git a/homeassistant/components/media_player/translations/id.json b/homeassistant/components/media_player/translations/id.json index 9446d1e9e89..469687f9110 100644 --- a/homeassistant/components/media_player/translations/id.json +++ b/homeassistant/components/media_player/translations/id.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_buffering": "{entity_name} sedang buffering", "is_idle": "{entity_name} siaga", "is_off": "{entity_name} mati", "is_on": "{entity_name} nyala", @@ -8,6 +9,7 @@ "is_playing": "{entity_name} sedang memutar" }, "trigger_type": { + "buffering": "{entity_name} mulai buffering", "changed_states": "{entity_name} mengubah status", "idle": "{entity_name} menjadi siaga", "paused": "{entity_name} dijeda", @@ -18,6 +20,7 @@ }, "state": { "_": { + "buffering": "Buffering", "idle": "Siaga", "off": "Mati", "on": "Nyala", diff --git a/homeassistant/components/media_player/translations/it.json b/homeassistant/components/media_player/translations/it.json index 7c075420aba..5fe897a9a3f 100644 --- a/homeassistant/components/media_player/translations/it.json +++ b/homeassistant/components/media_player/translations/it.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_buffering": "{entity_name} \u00e8 in buffering", "is_idle": "{entity_name} \u00e8 inattivo", "is_off": "{entity_name} \u00e8 spento", "is_on": "{entity_name} \u00e8 acceso", @@ -8,6 +9,7 @@ "is_playing": "{entity_name} \u00e8 in esecuzione" }, "trigger_type": { + "buffering": "{entity_name} avvia il buffering", "changed_states": "{entity_name} ha cambiato stato", "idle": "{entity_name} diventa inattivo", "paused": "{entity_name} \u00e8 in pausa", @@ -18,6 +20,7 @@ }, "state": { "_": { + "buffering": "Riempimento buffer", "idle": "Inattivo", "off": "Spento", "on": "Acceso", diff --git a/homeassistant/components/media_player/translations/nl.json b/homeassistant/components/media_player/translations/nl.json index 5fae215f4f9..23fe64d452a 100644 --- a/homeassistant/components/media_player/translations/nl.json +++ b/homeassistant/components/media_player/translations/nl.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_buffering": "{entity_name} is aan het bufferen", "is_idle": "{entity_name} is niet actief", "is_off": "{entity_name} is uitgeschakeld", "is_on": "{entity_name} is ingeschakeld", @@ -8,6 +9,7 @@ "is_playing": "{entity_name} wordt afgespeeld" }, "trigger_type": { + "buffering": "{entity_name} start met bufferen", "changed_states": "{entity_name} veranderde van status", "idle": "{entity_name} wordt inactief", "paused": "{entity_name} is gepauzeerd", @@ -18,6 +20,7 @@ }, "state": { "_": { + "buffering": "Bufferen", "idle": "Inactief", "off": "Uit", "on": "Aan", diff --git a/homeassistant/components/media_player/translations/no.json b/homeassistant/components/media_player/translations/no.json index c51920a67bd..d7dd387b4e3 100644 --- a/homeassistant/components/media_player/translations/no.json +++ b/homeassistant/components/media_player/translations/no.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_buffering": "{entity_name} bufre", "is_idle": "{entity_name} er inaktiv", "is_off": "{entity_name} er sl\u00e5tt av", "is_on": "{entity_name} er sl\u00e5tt p\u00e5", @@ -8,6 +9,7 @@ "is_playing": "{entity_name} spiller n\u00e5" }, "trigger_type": { + "buffering": "{entity_name} starter bufring", "changed_states": "{entity_name} endret tilstander", "idle": "{entity_name} blir inaktiv", "paused": "{entity_name} er satt p\u00e5 pause", @@ -18,6 +20,7 @@ }, "state": { "_": { + "buffering": "Bufring", "idle": "Inaktiv", "off": "Av", "on": "P\u00e5", diff --git a/homeassistant/components/media_player/translations/pl.json b/homeassistant/components/media_player/translations/pl.json index 08c664a909d..886cb07f93f 100644 --- a/homeassistant/components/media_player/translations/pl.json +++ b/homeassistant/components/media_player/translations/pl.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_buffering": "{entity_name} buforuje", "is_idle": "odtwarzacz {entity_name} jest nieaktywny", "is_off": "odtwarzacz {entity_name} jest wy\u0142\u0105czony", "is_on": "odtwarzacz {entity_name} jest w\u0142\u0105czony", @@ -8,6 +9,7 @@ "is_playing": "{entity_name} odtwarza media" }, "trigger_type": { + "buffering": "{entity_name} rozpocznie buforowanie", "changed_states": "{entity_name} zmieni\u0142o stan", "idle": "odtwarzacz {entity_name} stanie si\u0119 bezczynny", "paused": "odtwarzacz {entity_name} zostanie wstrzymany", @@ -18,6 +20,7 @@ }, "state": { "_": { + "buffering": "Buforowanie", "idle": "nieaktywny", "off": "wy\u0142.", "on": "w\u0142.", diff --git a/homeassistant/components/media_player/translations/pt-BR.json b/homeassistant/components/media_player/translations/pt-BR.json index 147f66ec0e7..332d4d0bcb4 100644 --- a/homeassistant/components/media_player/translations/pt-BR.json +++ b/homeassistant/components/media_player/translations/pt-BR.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_buffering": "{entity_name} est\u00e1 em buffer", "is_idle": "{entity_name} est\u00e1 ocioso", "is_off": "{entity_name} est\u00e1 desligado", "is_on": "{entity_name} est\u00e1 ligado", @@ -8,6 +9,7 @@ "is_playing": "{entity_name} est\u00e1 reproduzindo" }, "trigger_type": { + "buffering": "{entity_name} inicia o armazenamento em buffer", "changed_states": "{entity_name} ligado ou desligado", "idle": "{entity_name} ficar ocioso", "paused": "{entity_name} for pausado", @@ -18,6 +20,7 @@ }, "state": { "_": { + "buffering": "Carregando", "idle": "Ocioso", "off": "Desligado", "on": "Ligado", diff --git a/homeassistant/components/media_player/translations/zh-Hant.json b/homeassistant/components/media_player/translations/zh-Hant.json index b4e8442ea8d..99e55cc328c 100644 --- a/homeassistant/components/media_player/translations/zh-Hant.json +++ b/homeassistant/components/media_player/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_buffering": "{entity_name} \u7de9\u885d\u4e2d", "is_idle": "{entity_name}\u9592\u7f6e", "is_off": "{entity_name}\u95dc\u9589", "is_on": "{entity_name}\u958b\u555f", @@ -8,6 +9,7 @@ "is_playing": "{entity_name}\u6b63\u5728\u64ad\u653e" }, "trigger_type": { + "buffering": "{entity_name} \u958b\u59cb\u7de9\u885d", "changed_states": "{entity_name}\u5df2\u8b8a\u66f4\u72c0\u614b", "idle": "{entity_name}\u8b8a\u6210\u9592\u7f6e", "paused": "{entity_name}\u5df2\u66ab\u505c", @@ -18,6 +20,7 @@ }, "state": { "_": { + "buffering": "\u7de9\u885d", "idle": "\u9592\u7f6e", "off": "\u95dc\u9589", "on": "\u958b\u555f", diff --git a/homeassistant/components/met/translations/hu.json b/homeassistant/components/met/translations/hu.json index 38c84a3f8dc..ffa19641cd1 100644 --- a/homeassistant/components/met/translations/hu.json +++ b/homeassistant/components/met/translations/hu.json @@ -12,7 +12,7 @@ "elevation": "Magass\u00e1g", "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" }, "description": "Meteorol\u00f3giai int\u00e9zet", "title": "Elhelyezked\u00e9s" diff --git a/homeassistant/components/met_eireann/translations/hu.json b/homeassistant/components/met_eireann/translations/hu.json index b70aa2dcf67..8ce232f26a9 100644 --- a/homeassistant/components/met_eireann/translations/hu.json +++ b/homeassistant/components/met_eireann/translations/hu.json @@ -9,7 +9,7 @@ "elevation": "Magass\u00e1g", "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" }, "description": "Adja meg tart\u00f3zkod\u00e1si hely\u00e9t a Met \u00c9ireann Public Weather Forecast API id\u0151j\u00e1r\u00e1si adatainak haszn\u00e1lat\u00e1hoz", "title": "Elhelyezked\u00e9s" diff --git a/homeassistant/components/meteo_france/translations/bg.json b/homeassistant/components/meteo_france/translations/bg.json index c426ae62387..d4a3ab09c9d 100644 --- a/homeassistant/components/meteo_france/translations/bg.json +++ b/homeassistant/components/meteo_france/translations/bg.json @@ -9,14 +9,12 @@ "data": { "city": "\u0413\u0440\u0430\u0434" }, - "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0432\u0430\u0448\u0438\u044f \u0433\u0440\u0430\u0434 \u043e\u0442 \u0441\u043f\u0438\u0441\u044a\u043a\u0430", - "title": "M\u00e9t\u00e9o-France" + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0432\u0430\u0448\u0438\u044f \u0433\u0440\u0430\u0434 \u043e\u0442 \u0441\u043f\u0438\u0441\u044a\u043a\u0430" }, "user": { "data": { "city": "\u0413\u0440\u0430\u0434" - }, - "title": "M\u00e9t\u00e9o-France" + } } } } diff --git a/homeassistant/components/meteo_france/translations/ca.json b/homeassistant/components/meteo_france/translations/ca.json index 3fb28025f99..af3ee56159c 100644 --- a/homeassistant/components/meteo_france/translations/ca.json +++ b/homeassistant/components/meteo_france/translations/ca.json @@ -12,15 +12,13 @@ "data": { "city": "Ciutat" }, - "description": "Tria una ciutat de la llista", - "title": "M\u00e9t\u00e9o-France" + "description": "Tria una ciutat de la llista" }, "user": { "data": { "city": "Ciutat" }, - "description": "Introdueix el codi postal (nom\u00e9s recomanat per Fran\u00e7a) o nom de la ciutat", - "title": "M\u00e9t\u00e9o-France" + "description": "Introdueix el codi postal (nom\u00e9s recomanat per Fran\u00e7a) o nom de la ciutat" } } }, @@ -28,7 +26,7 @@ "step": { "init": { "data": { - "mode": "Mode de predicci\u00f3" + "mode": "Mode previsi\u00f3" } } } diff --git a/homeassistant/components/meteo_france/translations/cs.json b/homeassistant/components/meteo_france/translations/cs.json index d1c8f632220..0f05ce14ffc 100644 --- a/homeassistant/components/meteo_france/translations/cs.json +++ b/homeassistant/components/meteo_france/translations/cs.json @@ -12,15 +12,13 @@ "data": { "city": "M\u011bsto" }, - "description": "Vyberte m\u011bsto ze seznamu", - "title": "M\u00e9t\u00e9o-France" + "description": "Vyberte m\u011bsto ze seznamu" }, "user": { "data": { "city": "M\u011bsto" }, - "description": "Zadejte PS\u010c (pouze pro Francii, doporu\u010deno) nebo jm\u00e9no m\u011bsta.", - "title": "M\u00e9t\u00e9o-France" + "description": "Zadejte PS\u010c (pouze pro Francii, doporu\u010deno) nebo jm\u00e9no m\u011bsta." } } }, diff --git a/homeassistant/components/meteo_france/translations/da.json b/homeassistant/components/meteo_france/translations/da.json index d0fe7cba892..35c26b38083 100644 --- a/homeassistant/components/meteo_france/translations/da.json +++ b/homeassistant/components/meteo_france/translations/da.json @@ -9,8 +9,7 @@ "data": { "city": "By" }, - "description": "Indtast postnummer (kun for Frankrig, anbefalet) eller bynavn", - "title": "M\u00e9t\u00e9o-France" + "description": "Indtast postnummer (kun for Frankrig, anbefalet) eller bynavn" } } } diff --git a/homeassistant/components/meteo_france/translations/de.json b/homeassistant/components/meteo_france/translations/de.json index 04d038cb65d..ae0d7ab2f14 100644 --- a/homeassistant/components/meteo_france/translations/de.json +++ b/homeassistant/components/meteo_france/translations/de.json @@ -12,15 +12,13 @@ "data": { "city": "Stadt" }, - "description": "W\u00e4hle deine Stadt aus der Liste", - "title": "M\u00e9t\u00e9o-France" + "description": "W\u00e4hle deine Stadt aus der Liste" }, "user": { "data": { "city": "Stadt" }, - "description": "Gib die Postleitzahl (nur f\u00fcr Frankreich empfohlen) oder den St\u00e4dtenamen ein", - "title": "M\u00e9t\u00e9o-France" + "description": "Gib die Postleitzahl (nur f\u00fcr Frankreich empfohlen) oder den St\u00e4dtenamen ein" } } }, diff --git a/homeassistant/components/meteo_france/translations/el.json b/homeassistant/components/meteo_france/translations/el.json index 3dfad5a493d..038279abece 100644 --- a/homeassistant/components/meteo_france/translations/el.json +++ b/homeassistant/components/meteo_france/translations/el.json @@ -12,15 +12,13 @@ "data": { "city": "\u03a0\u03cc\u03bb\u03b7" }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03cc\u03bb\u03b7 \u03c3\u03b1\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1", - "title": "M\u00e9t\u00e9o-France" + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03cc\u03bb\u03b7 \u03c3\u03b1\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1" }, "user": { "data": { "city": "\u03a0\u03cc\u03bb\u03b7" }, - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b9\u03ba\u03cc \u03ba\u03ce\u03b4\u03b9\u03ba\u03b1 (\u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b7 \u0393\u03b1\u03bb\u03bb\u03af\u03b1, \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9) \u03ae \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03c0\u03cc\u03bb\u03b7\u03c2.", - "title": "M\u00e9t\u00e9o-France" + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b9\u03ba\u03cc \u03ba\u03ce\u03b4\u03b9\u03ba\u03b1 (\u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b7 \u0393\u03b1\u03bb\u03bb\u03af\u03b1, \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9) \u03ae \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03c0\u03cc\u03bb\u03b7\u03c2." } } }, diff --git a/homeassistant/components/meteo_france/translations/en.json b/homeassistant/components/meteo_france/translations/en.json index b1c4a7c0216..96302d7105b 100644 --- a/homeassistant/components/meteo_france/translations/en.json +++ b/homeassistant/components/meteo_france/translations/en.json @@ -12,15 +12,13 @@ "data": { "city": "City" }, - "description": "Choose your city from the list", - "title": "M\u00e9t\u00e9o-France" + "description": "Choose your city from the list" }, "user": { "data": { "city": "City" }, - "description": "Enter the postal code (only for France, recommended) or city name", - "title": "M\u00e9t\u00e9o-France" + "description": "Enter the postal code (only for France, recommended) or city name" } } }, diff --git a/homeassistant/components/meteo_france/translations/es-419.json b/homeassistant/components/meteo_france/translations/es-419.json index 471b965a824..e2c2feb5ed0 100644 --- a/homeassistant/components/meteo_france/translations/es-419.json +++ b/homeassistant/components/meteo_france/translations/es-419.json @@ -9,8 +9,7 @@ "data": { "city": "Ciudad" }, - "description": "Ingrese el c\u00f3digo postal (solo para Francia, recomendado) o el nombre de la ciudad", - "title": "M\u00e9t\u00e9o-Francia" + "description": "Ingrese el c\u00f3digo postal (solo para Francia, recomendado) o el nombre de la ciudad" } } } diff --git a/homeassistant/components/meteo_france/translations/es.json b/homeassistant/components/meteo_france/translations/es.json index 31d221eba09..8843d301779 100644 --- a/homeassistant/components/meteo_france/translations/es.json +++ b/homeassistant/components/meteo_france/translations/es.json @@ -12,15 +12,13 @@ "data": { "city": "Ciudad" }, - "description": "Elige tu ciudad de la lista", - "title": "M\u00e9t\u00e9o-France" + "description": "Elige tu ciudad de la lista" }, "user": { "data": { "city": "Ciudad" }, - "description": "Introduzca el c\u00f3digo postal (solo para Francia, recomendado) o el nombre de la ciudad", - "title": "M\u00e9t\u00e9o-France" + "description": "Introduzca el c\u00f3digo postal (solo para Francia, recomendado) o el nombre de la ciudad" } } }, diff --git a/homeassistant/components/meteo_france/translations/et.json b/homeassistant/components/meteo_france/translations/et.json index c23d2107f54..98c739fbcff 100644 --- a/homeassistant/components/meteo_france/translations/et.json +++ b/homeassistant/components/meteo_france/translations/et.json @@ -12,15 +12,13 @@ "data": { "city": "Linn" }, - "description": "Vali loendist oma linn", - "title": "" + "description": "Vali loendist oma linn" }, "user": { "data": { "city": "Linn" }, - "description": "Sisesta sihtnumber (ainult Prantsusmaa, soovitatav) v\u00f5i linna nimi", - "title": "" + "description": "Sisesta sihtnumber (ainult Prantsusmaa, soovitatav) v\u00f5i linna nimi" } } }, diff --git a/homeassistant/components/meteo_france/translations/fr.json b/homeassistant/components/meteo_france/translations/fr.json index 1121dd02b17..a2c543ee1fa 100644 --- a/homeassistant/components/meteo_france/translations/fr.json +++ b/homeassistant/components/meteo_france/translations/fr.json @@ -12,15 +12,13 @@ "data": { "city": "Ville" }, - "description": "Choisissez votre ville dans la liste", - "title": "M\u00e9t\u00e9o-France" + "description": "Choisissez votre ville dans la liste" }, "user": { "data": { "city": "Ville" }, - "description": "Entrez le code postal (uniquement pour la France, recommand\u00e9) ou le nom de la ville", - "title": "M\u00e9t\u00e9o-France" + "description": "Entrez le code postal (uniquement pour la France, recommand\u00e9) ou le nom de la ville" } } }, diff --git a/homeassistant/components/meteo_france/translations/hu.json b/homeassistant/components/meteo_france/translations/hu.json index 8034f6d0586..b9c2485a30b 100644 --- a/homeassistant/components/meteo_france/translations/hu.json +++ b/homeassistant/components/meteo_france/translations/hu.json @@ -12,15 +12,13 @@ "data": { "city": "V\u00e1ros" }, - "description": "V\u00e1lassza ki a v\u00e1rost a list\u00e1b\u00f3l", - "title": "M\u00e9t\u00e9o-France" + "description": "V\u00e1lassza ki a v\u00e1rost a list\u00e1b\u00f3l" }, "user": { "data": { "city": "V\u00e1ros" }, - "description": "\u00cdrja be az ir\u00e1ny\u00edt\u00f3sz\u00e1mot (csak Franciaorsz\u00e1g eset\u00e9ben aj\u00e1nlott) vagy a v\u00e1ros nev\u00e9t", - "title": "M\u00e9t\u00e9o-France" + "description": "\u00cdrja be az ir\u00e1ny\u00edt\u00f3sz\u00e1mot (csak Franciaorsz\u00e1g eset\u00e9ben aj\u00e1nlott) vagy a v\u00e1ros nev\u00e9t" } } }, diff --git a/homeassistant/components/meteo_france/translations/id.json b/homeassistant/components/meteo_france/translations/id.json index 07d8450e873..5c7d90710b5 100644 --- a/homeassistant/components/meteo_france/translations/id.json +++ b/homeassistant/components/meteo_france/translations/id.json @@ -12,15 +12,13 @@ "data": { "city": "Kota" }, - "description": "Pilih kota Anda dari daftar", - "title": "M\u00e9t\u00e9o-France" + "description": "Pilih kota Anda dari daftar" }, "user": { "data": { "city": "Kota" }, - "description": "Masukkan kode pos (hanya untuk Prancis, disarankan) atau nama kota", - "title": "M\u00e9t\u00e9o-France" + "description": "Masukkan kode pos (hanya untuk Prancis, disarankan) atau nama kota" } } }, diff --git a/homeassistant/components/meteo_france/translations/it.json b/homeassistant/components/meteo_france/translations/it.json index 37588fb4a32..78bad9d7dfd 100644 --- a/homeassistant/components/meteo_france/translations/it.json +++ b/homeassistant/components/meteo_france/translations/it.json @@ -12,15 +12,13 @@ "data": { "city": "Citt\u00e0" }, - "description": "Scegli la tua citt\u00e0 dall'elenco", - "title": "M\u00e9t\u00e9o-France" + "description": "Scegli la tua citt\u00e0 dall'elenco" }, "user": { "data": { "city": "Citt\u00e0" }, - "description": "Inserisci il codice postale (solo per la Francia, consigliato) o il nome della citt\u00e0", - "title": "M\u00e9t\u00e9o-France" + "description": "Inserisci il codice postale (solo per la Francia, consigliato) o il nome della citt\u00e0" } } }, diff --git a/homeassistant/components/meteo_france/translations/ja.json b/homeassistant/components/meteo_france/translations/ja.json index 2fa1f60225e..8e6b37cd53d 100644 --- a/homeassistant/components/meteo_france/translations/ja.json +++ b/homeassistant/components/meteo_france/translations/ja.json @@ -12,15 +12,13 @@ "data": { "city": "\u90fd\u5e02" }, - "description": "\u30ea\u30b9\u30c8\u304b\u3089\u3042\u306a\u305f\u306e\u90fd\u5e02\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044", - "title": "M\u00e9t\u00e9o-France" + "description": "\u30ea\u30b9\u30c8\u304b\u3089\u3042\u306a\u305f\u306e\u90fd\u5e02\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" }, "user": { "data": { "city": "\u90fd\u5e02" }, - "description": "\u90f5\u4fbf\u756a\u53f7(\u30d5\u30e9\u30f3\u30b9\u306e\u307f\u3001\u63a8\u5968) \u307e\u305f\u306f\u3001\u5e02\u533a\u753a\u6751\u540d\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", - "title": "M\u00e9t\u00e9o-France" + "description": "\u90f5\u4fbf\u756a\u53f7(\u30d5\u30e9\u30f3\u30b9\u306e\u307f\u3001\u63a8\u5968) \u307e\u305f\u306f\u3001\u5e02\u533a\u753a\u6751\u540d\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" } } }, diff --git a/homeassistant/components/meteo_france/translations/ko.json b/homeassistant/components/meteo_france/translations/ko.json index 83cda0e4dcf..f977d7d6915 100644 --- a/homeassistant/components/meteo_france/translations/ko.json +++ b/homeassistant/components/meteo_france/translations/ko.json @@ -12,15 +12,13 @@ "data": { "city": "\ub3c4\uc2dc" }, - "description": "\ubaa9\ub85d\uc5d0\uc11c \ub3c4\uc2dc\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694", - "title": "\ud504\ub791\uc2a4 \uae30\uc0c1\uccad (M\u00e9t\u00e9o-France)" + "description": "\ubaa9\ub85d\uc5d0\uc11c \ub3c4\uc2dc\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694" }, "user": { "data": { "city": "\ub3c4\uc2dc" }, - "description": "\uc6b0\ud3b8\ubc88\ud638 (\ud504\ub791\uc2a4) \ub610\ub294 \ub3c4\uc2dc \uc774\ub984\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", - "title": "\ud504\ub791\uc2a4 \uae30\uc0c1\uccad (M\u00e9t\u00e9o-France)" + "description": "\uc6b0\ud3b8\ubc88\ud638 (\ud504\ub791\uc2a4) \ub610\ub294 \ub3c4\uc2dc \uc774\ub984\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694" } } }, diff --git a/homeassistant/components/meteo_france/translations/lb.json b/homeassistant/components/meteo_france/translations/lb.json index 310d53b1d8f..ea204d6092e 100644 --- a/homeassistant/components/meteo_france/translations/lb.json +++ b/homeassistant/components/meteo_france/translations/lb.json @@ -12,15 +12,13 @@ "data": { "city": "Stad" }, - "description": "Wiel deng Stad aus der L\u00ebscht aus", - "title": "M\u00e9t\u00e9o-France" + "description": "Wiel deng Stad aus der L\u00ebscht aus" }, "user": { "data": { "city": "Stad" }, - "description": "Gitt de Postcode an (n\u00ebmme fir Frankr\u00e4ich, recommand\u00e9iert) oder den Numm vun der Stad", - "title": "M\u00e9t\u00e9o-France" + "description": "Gitt de Postcode an (n\u00ebmme fir Frankr\u00e4ich, recommand\u00e9iert) oder den Numm vun der Stad" } } }, diff --git a/homeassistant/components/meteo_france/translations/nl.json b/homeassistant/components/meteo_france/translations/nl.json index 11b0f567776..5e36e0585c7 100644 --- a/homeassistant/components/meteo_france/translations/nl.json +++ b/homeassistant/components/meteo_france/translations/nl.json @@ -12,15 +12,13 @@ "data": { "city": "Stad" }, - "description": "Kies uw stad uit de lijst", - "title": "M\u00e9t\u00e9o-France" + "description": "Kies uw stad uit de lijst" }, "user": { "data": { "city": "Stad" }, - "description": "Vul de postcode (alleen voor Frankrijk, aanbevolen) of de plaatsnaam in", - "title": "M\u00e9t\u00e9o-France" + "description": "Vul de postcode (alleen voor Frankrijk, aanbevolen) of de plaatsnaam in" } } }, diff --git a/homeassistant/components/meteo_france/translations/no.json b/homeassistant/components/meteo_france/translations/no.json index e323d5426da..dea58523260 100644 --- a/homeassistant/components/meteo_france/translations/no.json +++ b/homeassistant/components/meteo_france/translations/no.json @@ -12,15 +12,13 @@ "data": { "city": "By" }, - "description": "Velg din by fra listen", - "title": "" + "description": "Velg din by fra listen" }, "user": { "data": { "city": "By" }, - "description": "Fyll inn postnummeret (bare for Frankrike, anbefalt) eller bynavn", - "title": "" + "description": "Fyll inn postnummeret (bare for Frankrike, anbefalt) eller bynavn" } } }, diff --git a/homeassistant/components/meteo_france/translations/pl.json b/homeassistant/components/meteo_france/translations/pl.json index 0030aa83865..c826a92ac41 100644 --- a/homeassistant/components/meteo_france/translations/pl.json +++ b/homeassistant/components/meteo_france/translations/pl.json @@ -12,15 +12,13 @@ "data": { "city": "Miasto" }, - "description": "Wybierz swoje miasto z listy", - "title": "M\u00e9t\u00e9o-France" + "description": "Wybierz swoje miasto z listy" }, "user": { "data": { "city": "Miasto" }, - "description": "Wprowad\u017a kod pocztowy (tylko dla Francji, zalecane) lub nazw\u0119 miasta", - "title": "M\u00e9t\u00e9o-France" + "description": "Wprowad\u017a kod pocztowy (tylko dla Francji, zalecane) lub nazw\u0119 miasta" } } }, diff --git a/homeassistant/components/meteo_france/translations/pt-BR.json b/homeassistant/components/meteo_france/translations/pt-BR.json index 456c6eef17c..9d5bb35f586 100644 --- a/homeassistant/components/meteo_france/translations/pt-BR.json +++ b/homeassistant/components/meteo_france/translations/pt-BR.json @@ -12,15 +12,13 @@ "data": { "city": "Cidade" }, - "description": "Escolha sua cidade na lista", - "title": "M\u00e9t\u00e9o-France" + "description": "Escolha sua cidade na lista" }, "user": { "data": { "city": "Cidade" }, - "description": "Insira o c\u00f3digo postal (somente para a Fran\u00e7a, recomendado) ou o nome da cidade", - "title": "M\u00e9t\u00e9o-France" + "description": "Insira o c\u00f3digo postal (somente para a Fran\u00e7a, recomendado) ou o nome da cidade" } } }, diff --git a/homeassistant/components/meteo_france/translations/pt.json b/homeassistant/components/meteo_france/translations/pt.json index f53975ecf00..d059033bdcb 100644 --- a/homeassistant/components/meteo_france/translations/pt.json +++ b/homeassistant/components/meteo_france/translations/pt.json @@ -8,8 +8,7 @@ "user": { "data": { "city": "Cidade" - }, - "title": "" + } } } } diff --git a/homeassistant/components/meteo_france/translations/ru.json b/homeassistant/components/meteo_france/translations/ru.json index 6f42dae3b49..6ef7a82849e 100644 --- a/homeassistant/components/meteo_france/translations/ru.json +++ b/homeassistant/components/meteo_france/translations/ru.json @@ -12,15 +12,13 @@ "data": { "city": "\u0413\u043e\u0440\u043e\u0434" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0433\u043e\u0440\u043e\u0434 \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430", - "title": "M\u00e9t\u00e9o-France" + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0433\u043e\u0440\u043e\u0434 \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430" }, "user": { "data": { "city": "\u0413\u043e\u0440\u043e\u0434" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0424\u0440\u0430\u043d\u0446\u0438\u0438) \u0438\u043b\u0438 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0433\u043e\u0440\u043e\u0434\u0430", - "title": "M\u00e9t\u00e9o-France" + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0424\u0440\u0430\u043d\u0446\u0438\u0438) \u0438\u043b\u0438 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0433\u043e\u0440\u043e\u0434\u0430" } } }, diff --git a/homeassistant/components/meteo_france/translations/sl.json b/homeassistant/components/meteo_france/translations/sl.json index 912748ada8f..6acfeb28e08 100644 --- a/homeassistant/components/meteo_france/translations/sl.json +++ b/homeassistant/components/meteo_france/translations/sl.json @@ -9,8 +9,7 @@ "data": { "city": "Mesto" }, - "description": "Vnesite po\u0161tno \u0161tevilko (samo za Francijo) ali ime mesta", - "title": "M\u00e9t\u00e9o-France" + "description": "Vnesite po\u0161tno \u0161tevilko (samo za Francijo) ali ime mesta" } } } diff --git a/homeassistant/components/meteo_france/translations/sv.json b/homeassistant/components/meteo_france/translations/sv.json index 70fb537fc82..f7f1a68478f 100644 --- a/homeassistant/components/meteo_france/translations/sv.json +++ b/homeassistant/components/meteo_france/translations/sv.json @@ -14,8 +14,7 @@ "data": { "city": "Stad" }, - "description": "Ange postnumret (endast f\u00f6r Frankrike, rekommenderat) eller ortsnamn", - "title": "M\u00e9t\u00e9o-France" + "description": "Ange postnumret (endast f\u00f6r Frankrike, rekommenderat) eller ortsnamn" } } } diff --git a/homeassistant/components/meteo_france/translations/tr.json b/homeassistant/components/meteo_france/translations/tr.json index 4793dbb4fe7..38e60b14b8e 100644 --- a/homeassistant/components/meteo_france/translations/tr.json +++ b/homeassistant/components/meteo_france/translations/tr.json @@ -12,15 +12,13 @@ "data": { "city": "\u015eehir" }, - "description": "Listeden \u015fehrinizi se\u00e7in", - "title": "M\u00e9t\u00e9o-Fransa" + "description": "Listeden \u015fehrinizi se\u00e7in" }, "user": { "data": { "city": "\u015eehir" }, - "description": "Posta kodunu (yaln\u0131zca Fransa i\u00e7in, \u00f6nerilir) veya \u015fehir ad\u0131n\u0131 girin", - "title": "M\u00e9t\u00e9o-Fransa" + "description": "Posta kodunu (yaln\u0131zca Fransa i\u00e7in, \u00f6nerilir) veya \u015fehir ad\u0131n\u0131 girin" } } }, diff --git a/homeassistant/components/meteo_france/translations/uk.json b/homeassistant/components/meteo_france/translations/uk.json index a84c230e218..124aacb9d67 100644 --- a/homeassistant/components/meteo_france/translations/uk.json +++ b/homeassistant/components/meteo_france/translations/uk.json @@ -12,15 +12,13 @@ "data": { "city": "\u041c\u0456\u0441\u0442\u043e" }, - "description": "\u041e\u0431\u0435\u0440\u0456\u0442\u044c \u043c\u0456\u0441\u0442\u043e \u0437\u0456 \u0441\u043f\u0438\u0441\u043a\u0443", - "title": "M\u00e9t\u00e9o-France" + "description": "\u041e\u0431\u0435\u0440\u0456\u0442\u044c \u043c\u0456\u0441\u0442\u043e \u0437\u0456 \u0441\u043f\u0438\u0441\u043a\u0443" }, "user": { "data": { "city": "\u041c\u0456\u0441\u0442\u043e" }, - "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441 (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0454\u0442\u044c\u0441\u044f \u0442\u0456\u043b\u044c\u043a\u0438 \u0434\u043b\u044f \u0424\u0440\u0430\u043d\u0446\u0456\u0457) \u0430\u0431\u043e \u043d\u0430\u0437\u0432\u0443 \u043c\u0456\u0441\u0442\u0430", - "title": "M\u00e9t\u00e9o-France" + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441 (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0454\u0442\u044c\u0441\u044f \u0442\u0456\u043b\u044c\u043a\u0438 \u0434\u043b\u044f \u0424\u0440\u0430\u043d\u0446\u0456\u0457) \u0430\u0431\u043e \u043d\u0430\u0437\u0432\u0443 \u043c\u0456\u0441\u0442\u0430" } } }, diff --git a/homeassistant/components/meteo_france/translations/zh-Hant.json b/homeassistant/components/meteo_france/translations/zh-Hant.json index 32d92956d73..2f1aa7fe89e 100644 --- a/homeassistant/components/meteo_france/translations/zh-Hant.json +++ b/homeassistant/components/meteo_france/translations/zh-Hant.json @@ -12,15 +12,13 @@ "data": { "city": "\u57ce\u5e02\u540d\u7a31" }, - "description": "\u7531\u5217\u8868\u4e2d\u9078\u64c7\u57ce\u5e02", - "title": "M\u00e9t\u00e9o-France" + "description": "\u7531\u5217\u8868\u4e2d\u9078\u64c7\u57ce\u5e02" }, "user": { "data": { "city": "\u57ce\u5e02\u540d\u7a31" }, - "description": "\u8f38\u5165\u90f5\u905e\u5340\u865f\uff08\u50c5\u652f\u63f4\u6cd5\u570b\uff09\u6216\u57ce\u5e02\u540d\u7a31", - "title": "M\u00e9t\u00e9o-France" + "description": "\u8f38\u5165\u90f5\u905e\u5340\u865f\uff08\u50c5\u652f\u63f4\u6cd5\u570b\uff09\u6216\u57ce\u5e02\u540d\u7a31" } } }, diff --git a/homeassistant/components/meteoclimatic/translations/bg.json b/homeassistant/components/meteoclimatic/translations/bg.json index 63b0e7be8b7..4bac01649f7 100644 --- a/homeassistant/components/meteoclimatic/translations/bg.json +++ b/homeassistant/components/meteoclimatic/translations/bg.json @@ -6,11 +6,6 @@ }, "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/meteoclimatic/translations/ca.json b/homeassistant/components/meteoclimatic/translations/ca.json index 5f672d87535..e7d760253e7 100644 --- a/homeassistant/components/meteoclimatic/translations/ca.json +++ b/homeassistant/components/meteoclimatic/translations/ca.json @@ -12,8 +12,9 @@ "data": { "code": "Codi d'estaci\u00f3" }, - "description": "Introdueix el codi d'estaci\u00f3 meteorol\u00f2gica (per exemple, ESCAT4300000043206B)", - "title": "Meteoclimatic" + "data_description": { + "code": "T\u00e9 el format com ESCAT4300000043206B" + } } } } diff --git a/homeassistant/components/meteoclimatic/translations/de.json b/homeassistant/components/meteoclimatic/translations/de.json index c9b9ea90e61..7365dba674d 100644 --- a/homeassistant/components/meteoclimatic/translations/de.json +++ b/homeassistant/components/meteoclimatic/translations/de.json @@ -12,8 +12,9 @@ "data": { "code": "Stationscode" }, - "description": "Gib den Code der Meteoclimatic-Station ein (z. B. ESCAT4300000043206B)", - "title": "Meteoclimatic" + "data_description": { + "code": "Sieht aus wie ESCAT4300000043206B" + } } } } diff --git a/homeassistant/components/meteoclimatic/translations/el.json b/homeassistant/components/meteoclimatic/translations/el.json index 085e6fe1643..01854a07830 100644 --- a/homeassistant/components/meteoclimatic/translations/el.json +++ b/homeassistant/components/meteoclimatic/translations/el.json @@ -12,8 +12,9 @@ "data": { "code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd" }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd Meteoclimatic (\u03c0.\u03c7. ESCAT4300000043206B)", - "title": "Meteoclimatic" + "data_description": { + "code": "\u039c\u03bf\u03b9\u03ac\u03b6\u03b5\u03b9 \u03bc\u03b5 ESCAT4300000043206B" + } } } } diff --git a/homeassistant/components/meteoclimatic/translations/en.json b/homeassistant/components/meteoclimatic/translations/en.json index a066971be25..50a4795410d 100644 --- a/homeassistant/components/meteoclimatic/translations/en.json +++ b/homeassistant/components/meteoclimatic/translations/en.json @@ -12,8 +12,9 @@ "data": { "code": "Station code" }, - "description": "Enter the Meteoclimatic station code (e.g., ESCAT4300000043206B)", - "title": "Meteoclimatic" + "data_description": { + "code": "Looks like ESCAT4300000043206B" + } } } } diff --git a/homeassistant/components/meteoclimatic/translations/es.json b/homeassistant/components/meteoclimatic/translations/es.json index ab84e6604e3..027f85c60df 100644 --- a/homeassistant/components/meteoclimatic/translations/es.json +++ b/homeassistant/components/meteoclimatic/translations/es.json @@ -11,9 +11,7 @@ "user": { "data": { "code": "C\u00f3digo de la estaci\u00f3n" - }, - "description": "Introduzca el c\u00f3digo de la estaci\u00f3n Meteoclimatic (por ejemplo, ESCAT4300000043206B)", - "title": "Meteoclimatic" + } } } } diff --git a/homeassistant/components/meteoclimatic/translations/et.json b/homeassistant/components/meteoclimatic/translations/et.json index e019a4a0e12..886de9279f9 100644 --- a/homeassistant/components/meteoclimatic/translations/et.json +++ b/homeassistant/components/meteoclimatic/translations/et.json @@ -12,8 +12,9 @@ "data": { "code": "Jaama kood" }, - "description": "Sisesta Meteoclimatic jaama kood (nt ESCAT4300000043206B)", - "title": "Meteoclimatic" + "data_description": { + "code": "N\u00e4eb v\u00e4lja nagu ESCAT4300000043206B" + } } } } diff --git a/homeassistant/components/meteoclimatic/translations/fr.json b/homeassistant/components/meteoclimatic/translations/fr.json index ae087db2e6e..95151d8f553 100644 --- a/homeassistant/components/meteoclimatic/translations/fr.json +++ b/homeassistant/components/meteoclimatic/translations/fr.json @@ -12,8 +12,9 @@ "data": { "code": "Code de la station" }, - "description": "Entrer le code de la station m\u00e9t\u00e9orologique (par exemple, ESCAT4300000043206)", - "title": "M\u00e9t\u00e9oclimatique" + "data_description": { + "code": "Ressemble \u00e0 ESCAT4300000043206B" + } } } } diff --git a/homeassistant/components/meteoclimatic/translations/gl.json b/homeassistant/components/meteoclimatic/translations/gl.json deleted file mode 100644 index be1629dd6e4..00000000000 --- a/homeassistant/components/meteoclimatic/translations/gl.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "config": { - "step": { - "user": { - "description": "Introduza o c\u00f3digo da estaci\u00f3n Meteoclimatic (por exemplo, ESCAT4300000043206B)" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/hu.json b/homeassistant/components/meteoclimatic/translations/hu.json index 582c78f263d..d9583656eb2 100644 --- a/homeassistant/components/meteoclimatic/translations/hu.json +++ b/homeassistant/components/meteoclimatic/translations/hu.json @@ -12,8 +12,9 @@ "data": { "code": "\u00c1llom\u00e1s k\u00f3dja" }, - "description": "Adja meg a meteorol\u00f3giai \u00e1llom\u00e1s k\u00f3dj\u00e1t (pl. ESCAT4300000043206B)", - "title": "Meteoklimatikus" + "data_description": { + "code": "\u00dagy n\u00e9z ki, mint ESCAT4300000043206B" + } } } } diff --git a/homeassistant/components/meteoclimatic/translations/id.json b/homeassistant/components/meteoclimatic/translations/id.json index 4e23dad8dc4..3f8221f059a 100644 --- a/homeassistant/components/meteoclimatic/translations/id.json +++ b/homeassistant/components/meteoclimatic/translations/id.json @@ -12,8 +12,9 @@ "data": { "code": "Kode stasiun" }, - "description": "Masukkan kode stasiun Meteoclimatic (misalnya, ESCAT4300000043206B)", - "title": "Meteoclimatic" + "data_description": { + "code": "Sepertinya ESCAT4300000043206B" + } } } } diff --git a/homeassistant/components/meteoclimatic/translations/it.json b/homeassistant/components/meteoclimatic/translations/it.json index fcdfa25c496..f069b393a1b 100644 --- a/homeassistant/components/meteoclimatic/translations/it.json +++ b/homeassistant/components/meteoclimatic/translations/it.json @@ -12,8 +12,9 @@ "data": { "code": "Codice della stazione" }, - "description": "Immettere il codice della stazione Meteoclimatic (ad esempio ESCAT4300000043206B)", - "title": "Meteoclimatic" + "data_description": { + "code": "Assomiglia a ECAT4300000043206B" + } } } } diff --git a/homeassistant/components/meteoclimatic/translations/ja.json b/homeassistant/components/meteoclimatic/translations/ja.json index 0274ff70e88..adae157aeee 100644 --- a/homeassistant/components/meteoclimatic/translations/ja.json +++ b/homeassistant/components/meteoclimatic/translations/ja.json @@ -12,8 +12,9 @@ "data": { "code": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u30b3\u30fc\u30c9" }, - "description": "Meteoclimatic\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u30b3\u30fc\u30c9\u3092\u5165\u529b(\u4f8b: ESCAT4300000043206B)", - "title": "Meteoclimatic" + "data_description": { + "code": "ESCAT4300000043206B\u306e\u3088\u3046\u306b\u898b\u3048\u307e\u3059" + } } } } diff --git a/homeassistant/components/meteoclimatic/translations/nl.json b/homeassistant/components/meteoclimatic/translations/nl.json index dd2a318ec37..c6f7151ade6 100644 --- a/homeassistant/components/meteoclimatic/translations/nl.json +++ b/homeassistant/components/meteoclimatic/translations/nl.json @@ -12,8 +12,9 @@ "data": { "code": "Station code" }, - "description": "Voer de code van het meteoklimatologische station in (bv. ESCAT4300000043206B)", - "title": "Meteoclimatic" + "data_description": { + "code": "Lijkt op ESCAT43000043206B" + } } } } diff --git a/homeassistant/components/meteoclimatic/translations/no.json b/homeassistant/components/meteoclimatic/translations/no.json index 0e6e080d146..1bcf4ae09fe 100644 --- a/homeassistant/components/meteoclimatic/translations/no.json +++ b/homeassistant/components/meteoclimatic/translations/no.json @@ -12,8 +12,9 @@ "data": { "code": "Stasjonskode" }, - "description": "Angi Meteoclimatic stasjonskode (f.eks. ESCAT4300000043206B)", - "title": "Meteoclimatic" + "data_description": { + "code": "Ser ut som ESCAT4300000043206B" + } } } } diff --git a/homeassistant/components/meteoclimatic/translations/pl.json b/homeassistant/components/meteoclimatic/translations/pl.json index c4539bd6c8b..aab41ba7d12 100644 --- a/homeassistant/components/meteoclimatic/translations/pl.json +++ b/homeassistant/components/meteoclimatic/translations/pl.json @@ -12,8 +12,9 @@ "data": { "code": "Kod stacji" }, - "description": "Wpisz kod stacji Meteoclimatic (np. ESCAT4300000043206B)", - "title": "Meteoclimatic" + "data_description": { + "code": "Wygl\u0105da jak ESCAT4300000043206B" + } } } } diff --git a/homeassistant/components/meteoclimatic/translations/pt-BR.json b/homeassistant/components/meteoclimatic/translations/pt-BR.json index c81109b8939..88223d4b177 100644 --- a/homeassistant/components/meteoclimatic/translations/pt-BR.json +++ b/homeassistant/components/meteoclimatic/translations/pt-BR.json @@ -12,8 +12,9 @@ "data": { "code": "C\u00f3digo da esta\u00e7\u00e3o" }, - "description": "Digite o c\u00f3digo da esta\u00e7\u00e3o Meteoclim\u00e1tica (por exemplo, ESCAT4300000043206B)", - "title": "Meteoclimatic" + "data_description": { + "code": "\u00c9 parecido com isso ESCAT4300000043206B" + } } } } diff --git a/homeassistant/components/meteoclimatic/translations/pt.json b/homeassistant/components/meteoclimatic/translations/pt.json deleted file mode 100644 index 71eded9f5de..00000000000 --- a/homeassistant/components/meteoclimatic/translations/pt.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "config": { - "step": { - "user": { - "description": "Introduza o c\u00f3digo da esta\u00e7\u00e3o Meteoclimatic (por exemplo, ESCAT4300000043206B)" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/ru.json b/homeassistant/components/meteoclimatic/translations/ru.json index 14df114c903..79421f0f325 100644 --- a/homeassistant/components/meteoclimatic/translations/ru.json +++ b/homeassistant/components/meteoclimatic/translations/ru.json @@ -12,8 +12,9 @@ "data": { "code": "\u041a\u043e\u0434 \u0441\u0442\u0430\u043d\u0446\u0438\u0438" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 Meteoclimatic (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, ESCAT4300000043206B)", - "title": "Meteoclimatic" + "data_description": { + "code": "\u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, ESCAT4300000043206B" + } } } } diff --git a/homeassistant/components/meteoclimatic/translations/tr.json b/homeassistant/components/meteoclimatic/translations/tr.json index a56fe984e2e..5221bc971c0 100644 --- a/homeassistant/components/meteoclimatic/translations/tr.json +++ b/homeassistant/components/meteoclimatic/translations/tr.json @@ -12,8 +12,9 @@ "data": { "code": "\u0130stasyon kodu" }, - "description": "Meteoclimatic istasyon kodunu girin (\u00f6rne\u011fin, ESCAT4300000043206B)", - "title": "Meteoclimatic" + "data_description": { + "code": "ESCAT4300000043206B'ye benziyor" + } } } } diff --git a/homeassistant/components/meteoclimatic/translations/zh-Hant.json b/homeassistant/components/meteoclimatic/translations/zh-Hant.json index d5c3793be7d..7aa165509f2 100644 --- a/homeassistant/components/meteoclimatic/translations/zh-Hant.json +++ b/homeassistant/components/meteoclimatic/translations/zh-Hant.json @@ -12,8 +12,9 @@ "data": { "code": "\u6c23\u8c61\u7ad9\u4ee3\u78bc" }, - "description": "\u8f38\u5165 Meteoclimatic \u6c23\u8c61\u7ad9\u4ee3\u78bc\uff08\u4f8b\u5982 ESCAT4300000043206B\uff09", - "title": "Meteoclimatic" + "data_description": { + "code": "\u770b\u8d77\u4f86\u985e\u4f3c ESCAT4300000043206B" + } } } } diff --git a/homeassistant/components/mikrotik/translations/hu.json b/homeassistant/components/mikrotik/translations/hu.json index 3e5281fc06a..c15ba2f07aa 100644 --- a/homeassistant/components/mikrotik/translations/hu.json +++ b/homeassistant/components/mikrotik/translations/hu.json @@ -12,7 +12,7 @@ "user": { "data": { "host": "C\u00edm", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v", diff --git a/homeassistant/components/vallox/translations/sk.json b/homeassistant/components/min_max/translations/bg.json similarity index 73% rename from homeassistant/components/vallox/translations/sk.json rename to homeassistant/components/min_max/translations/bg.json index af15f92c2f2..35cfa0ad1d7 100644 --- a/homeassistant/components/vallox/translations/sk.json +++ b/homeassistant/components/min_max/translations/bg.json @@ -3,7 +3,7 @@ "step": { "user": { "data": { - "name": "N\u00e1zov" + "name": "\u0418\u043c\u0435" } } } diff --git a/homeassistant/components/min_max/translations/ca.json b/homeassistant/components/min_max/translations/ca.json new file mode 100644 index 00000000000..b114e8ecf40 --- /dev/null +++ b/homeassistant/components/min_max/translations/ca.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "Entitats d'entrada", + "name": "Nom", + "round_digits": "Precisi\u00f3", + "type": "Caracter\u00edstica estad\u00edstica" + }, + "data_description": { + "round_digits": "Controla el nombre de d\u00edgits decimals quan la caracter\u00edstica estad\u00edstica \u00e9s la mitjana o la mediana." + }, + "description": "Crea un sensor que calcula el m\u00ednim, m\u00e0xim, la mitjana o la mediana d'una llista de sensors d'entrada.", + "title": "Afegeix sensor m\u00edn / m\u00e0x / mitjana / mediana" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "Entitats d'entrada", + "round_digits": "Precisi\u00f3", + "type": "Caracter\u00edstica estad\u00edstica" + }, + "data_description": { + "round_digits": "Controla el nombre de d\u00edgits decimals quan la caracter\u00edstica estad\u00edstica \u00e9s la mitjana o la mediana." + } + }, + "options": { + "data": { + "entity_ids": "Entitats d'entrada", + "round_digits": "Precisi\u00f3", + "type": "Caracter\u00edstica estad\u00edstica" + }, + "description": "Crea un sensor que calcula el m\u00ednim, m\u00e0xim, la mitjana o la mediana d'una llista de sensors d'entrada." + } + } + }, + "title": "Sensor m\u00edn / m\u00e0x / mitjana / mediana" +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/cs.json b/homeassistant/components/min_max/translations/cs.json new file mode 100644 index 00000000000..a969a39234a --- /dev/null +++ b/homeassistant/components/min_max/translations/cs.json @@ -0,0 +1,34 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "Vstupn\u00ed entity", + "name": "Jm\u00e9no", + "round_digits": "P\u0159esnost", + "type": "Statistika" + }, + "description": "P\u0159esnost ur\u010duje po\u010det desetinn\u00fdch m\u00edst, kdy\u017e je statistikou pr\u016fm\u011br nebo medi\u00e1n." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "Vstupn\u00ed entity", + "round_digits": "P\u0159esnost", + "type": "Statistika" + } + }, + "options": { + "data": { + "entity_ids": "Vstupn\u00ed entity", + "round_digits": "P\u0159esnost", + "type": "Statistika" + }, + "description": "P\u0159esnost ur\u010duje po\u010det desetinn\u00fdch m\u00edst, kdy\u017e je statistikou pr\u016fm\u011br nebo medi\u00e1n." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/de.json b/homeassistant/components/min_max/translations/de.json new file mode 100644 index 00000000000..c8b9d5ddf63 --- /dev/null +++ b/homeassistant/components/min_max/translations/de.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "Eingabe-Entit\u00e4ten", + "name": "Name", + "round_digits": "Genauigkeit", + "type": "Statistisches Merkmal" + }, + "data_description": { + "round_digits": "Steuert die Anzahl der Dezimalstellen in der Ausgabe, wenn das Statistikmerkmal Mittelwert oder Median ist." + }, + "description": "Erstelle einen Sensor, der einen Mindest-, H\u00f6chst-, Mittel- oder Medianwert aus einer Liste von Eingabesensoren berechnet.", + "title": "Min/Max/Mittelwert/Median-Sensor hinzuf\u00fcgen" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "Eingabe-Entit\u00e4ten", + "round_digits": "Genauigkeit", + "type": "Statistisches Merkmal" + }, + "data_description": { + "round_digits": "Steuert die Anzahl der Dezimalstellen in der Ausgabe, wenn das Statistikmerkmal Mittelwert oder Median ist." + } + }, + "options": { + "data": { + "entity_ids": "Eingabe-Entit\u00e4ten", + "round_digits": "Genauigkeit", + "type": "Statistisches Merkmal" + }, + "description": "Erstelle einen Sensor, der einen Mindest-, H\u00f6chst-, Mittel- oder Medianwert aus einer Liste von Eingabesensoren berechnet." + } + } + }, + "title": "Min / Max / Mittelwert / Mediansensor" +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/el.json b/homeassistant/components/min_max/translations/el.json new file mode 100644 index 00000000000..218c46fd683 --- /dev/null +++ b/homeassistant/components/min_max/translations/el.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "\u039f\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5", + "name": "\u039f\u03bd\u03bf\u03bc\u03b1", + "round_digits": "\u0391\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1", + "type": "\u03a3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc" + }, + "data_description": { + "round_digits": "\u0395\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf \u03cc\u03c4\u03b1\u03bd \u03c4\u03bf \u03c3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03ad\u03c3\u03bf\u03c2 \u03ae \u03b4\u03b9\u03ac\u03bc\u03b5\u03c3\u03bf\u03c2." + }, + "description": "\u0397 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03cc\u03c4\u03b1\u03bd \u03c4\u03bf \u03c3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03b5\u03af\u03bd\u03b1\u03b9 \u03bf \u03bc\u03ad\u03c3\u03bf\u03c2 \u03cc\u03c1\u03bf\u03c2 \u03ae \u03b7 \u03b4\u03b9\u03ac\u03bc\u03b5\u03c3\u03bf\u03c2.", + "title": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 min / max / mean / median" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "\u039f\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5", + "round_digits": "\u0391\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1", + "type": "\u03a3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc" + }, + "data_description": { + "round_digits": "\u0395\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf \u03cc\u03c4\u03b1\u03bd \u03c4\u03bf \u03c3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03ad\u03c3\u03bf\u03c2 \u03ae \u03b4\u03b9\u03ac\u03bc\u03b5\u03c3\u03bf\u03c2." + } + }, + "options": { + "data": { + "entity_ids": "\u039f\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5", + "round_digits": "\u0391\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1", + "type": "\u03a3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc" + }, + "description": "\u0397 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03cc\u03c4\u03b1\u03bd \u03c4\u03bf \u03c3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03b5\u03af\u03bd\u03b1\u03b9 \u03bf \u03bc\u03ad\u03c3\u03bf\u03c2 \u03cc\u03c1\u03bf\u03c2 \u03ae \u03b7 \u03b4\u03b9\u03ac\u03bc\u03b5\u03c3\u03bf\u03c2." + } + } + }, + "title": "\u0395\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03c2 / \u03bc\u03ad\u03b3\u03b9\u03c3\u03c4\u03bf\u03c2 / \u03bc\u03ad\u03c3\u03bf\u03c2 / \u03b4\u03b9\u03ac\u03bc\u03b5\u03c3\u03bf\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2" +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/en.json b/homeassistant/components/min_max/translations/en.json index 8cc0d41c419..b5b6eb5d731 100644 --- a/homeassistant/components/min_max/translations/en.json +++ b/homeassistant/components/min_max/translations/en.json @@ -27,6 +27,14 @@ "data_description": { "round_digits": "Controls the number of decimal digits in the output when the statistics characteristic is mean or median." } + }, + "options": { + "data": { + "entity_ids": "Input entities", + "round_digits": "Precision", + "type": "Statistic characteristic" + }, + "description": "Create a sensor that calculates a min, max, mean or median value from a list of input sensors." } } }, diff --git a/homeassistant/components/min_max/translations/et.json b/homeassistant/components/min_max/translations/et.json new file mode 100644 index 00000000000..2f1689e8577 --- /dev/null +++ b/homeassistant/components/min_max/translations/et.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "Sisendolemid", + "name": "Nimi", + "round_digits": "T\u00e4psus", + "type": "Statistiline tunnus" + }, + "data_description": { + "round_digits": "M\u00e4\u00e4rab k\u00fcmnendkohtade arvu v\u00e4ljundis kui statistika tunnus on keskmine v\u00f5i mediaan." + }, + "description": "Loo andur mis arvutab sisendandurite loendist minimaalse, maksimaalse, keskmise v\u00f5i mediaanv\u00e4\u00e4rtuse.", + "title": "Lisa min / max / keskmine / mediaanandur" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "Sisendolemid", + "round_digits": "T\u00e4psus", + "type": "Statistiline omadus" + }, + "data_description": { + "round_digits": "M\u00e4\u00e4rab k\u00fcmnendkohtade arvu v\u00e4ljundis kui statistika tunnus on keskmine v\u00f5i mediaan." + } + }, + "options": { + "data": { + "entity_ids": "Sisendolemid", + "round_digits": "T\u00e4psus", + "type": "Statistiline tunnus" + }, + "description": "T\u00e4psus kontrollib k\u00fcmnendnumbrite arvu kui statistika tunnus on keskmine v\u00f5i mediaan." + } + } + }, + "title": "Min / max / keskmine / mediaanandur" +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/fr.json b/homeassistant/components/min_max/translations/fr.json new file mode 100644 index 00000000000..b42ccf0bb2a --- /dev/null +++ b/homeassistant/components/min_max/translations/fr.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "Entit\u00e9s d'entr\u00e9e", + "name": "Nom", + "round_digits": "Pr\u00e9cision", + "type": "Indicateur statistique" + }, + "data_description": { + "round_digits": "Contr\u00f4le le nombre de chiffres d\u00e9cimaux de la sortie lorsque l'indicateur statistique est la moyenne ou la m\u00e9diane." + }, + "description": "Cr\u00e9ez un capteur qui calcule une valeur minimale, maximale, moyenne ou m\u00e9diane \u00e0 partir d'une liste de capteurs d'entr\u00e9e.", + "title": "Ajouter un capteur de valeur minimale, maximale, moyenne ou m\u00e9diane" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "Entit\u00e9s d'entr\u00e9e", + "round_digits": "Pr\u00e9cision", + "type": "Indicateur statistique" + }, + "data_description": { + "round_digits": "Contr\u00f4le le nombre de chiffres d\u00e9cimaux de la sortie lorsque l'indicateur statistique est la moyenne ou la m\u00e9diane." + } + }, + "options": { + "data": { + "entity_ids": "Entit\u00e9s d'entr\u00e9e", + "round_digits": "Pr\u00e9cision", + "type": "Indicateur statistique" + }, + "description": "Cr\u00e9ez un capteur qui calcule une valeur minimale, maximale, moyenne ou m\u00e9diane \u00e0 partir d'une liste de capteurs d'entr\u00e9e." + } + } + }, + "title": "Capteur de valeur minimale, maximale, moyenne ou m\u00e9diane" +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/he.json b/homeassistant/components/min_max/translations/he.json new file mode 100644 index 00000000000..267e38f0ce2 --- /dev/null +++ b/homeassistant/components/min_max/translations/he.json @@ -0,0 +1,37 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05e7\u05dc\u05d8", + "round_digits": "\u05d3\u05d9\u05d5\u05e7", + "type": "\u05de\u05d0\u05e4\u05d9\u05d9\u05df \u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4" + }, + "data_description": { + "round_digits": "\u05e9\u05d5\u05dc\u05d8 \u05d1\u05de\u05e1\u05e4\u05e8 \u05d4\u05e1\u05e4\u05e8\u05d5\u05ea \u05d4\u05e2\u05e9\u05e8\u05d5\u05e0\u05d9\u05d5\u05ea \u05d1\u05e4\u05dc\u05d8 \u05db\u05d0\u05e9\u05e8 \u05de\u05d0\u05e4\u05d9\u05d9\u05df \u05d4\u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4 \u05d4\u05d5\u05d0 \u05de\u05de\u05d5\u05e6\u05e2 \u05d0\u05d5 \u05d7\u05e6\u05d9\u05d5\u05e0\u05d9." + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05e7\u05dc\u05d8", + "round_digits": "\u05d3\u05d9\u05d5\u05e7", + "type": "\u05de\u05d0\u05e4\u05d9\u05d9\u05df \u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4" + }, + "data_description": { + "round_digits": "\u05e9\u05d5\u05dc\u05d8 \u05d1\u05de\u05e1\u05e4\u05e8 \u05d4\u05e1\u05e4\u05e8\u05d5\u05ea \u05d4\u05e2\u05e9\u05e8\u05d5\u05e0\u05d9\u05d5\u05ea \u05d1\u05e4\u05dc\u05d8 \u05db\u05d0\u05e9\u05e8 \u05de\u05d0\u05e4\u05d9\u05d9\u05df \u05d4\u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4 \u05d4\u05d5\u05d0 \u05de\u05de\u05d5\u05e6\u05e2 \u05d0\u05d5 \u05d7\u05e6\u05d9\u05d5\u05e0\u05d9." + } + }, + "options": { + "data": { + "entity_ids": "\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05e7\u05dc\u05d8", + "round_digits": "\u05d3\u05d9\u05d5\u05e7", + "type": "\u05de\u05d0\u05e4\u05d9\u05d9\u05df \u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/hu.json b/homeassistant/components/min_max/translations/hu.json new file mode 100644 index 00000000000..98d021dbb31 --- /dev/null +++ b/homeassistant/components/min_max/translations/hu.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "Forr\u00e1s entit\u00e1sok", + "name": "Elnevez\u00e9s", + "round_digits": "Pontoss\u00e1g", + "type": "Statisztikai jellemz\u0151" + }, + "data_description": { + "round_digits": "Szab\u00e1lyozza a tizedesjegyek sz\u00e1m\u00e1t, amikor a statisztikai jellemz\u0151 az \u00e1tlag vagy a medi\u00e1n." + }, + "description": "A pontoss\u00e1g a tizedesjegyek sz\u00e1m\u00e1t szab\u00e1lyozza, ha a statisztikai jellemz\u0151 az \u00e1tlag vagy a medi\u00e1n.", + "title": "Min / max / \u00e1tlag / medi\u00e1n \u00e9rz\u00e9kel\u0151 hozz\u00e1ad\u00e1sa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "Forr\u00e1s entit\u00e1sok", + "round_digits": "Pontoss\u00e1g", + "type": "Statisztikai jellemz\u0151" + }, + "data_description": { + "round_digits": "Szab\u00e1lyozza a tizedesjegyek sz\u00e1m\u00e1t, amikor a statisztikai jellemz\u0151 az \u00e1tlag vagy a medi\u00e1n." + } + }, + "options": { + "data": { + "entity_ids": "Forr\u00e1s entit\u00e1sok", + "round_digits": "Pontoss\u00e1g", + "type": "Statisztikai jellemz\u0151" + }, + "description": "A pontoss\u00e1g a tizedesjegyek sz\u00e1m\u00e1t szab\u00e1lyozza, ha a statisztikai jellemz\u0151 az \u00e1tlag vagy a medi\u00e1n." + } + } + }, + "title": "Min / max / \u00e1tlag / medi\u00e1n \u00e9rz\u00e9kel\u0151" +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/id.json b/homeassistant/components/min_max/translations/id.json new file mode 100644 index 00000000000..32ccfd2596d --- /dev/null +++ b/homeassistant/components/min_max/translations/id.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "Entitas input", + "name": "Nama", + "round_digits": "Presisi", + "type": "Karakteristik statistik" + }, + "data_description": { + "round_digits": "Mengontrol jumlah digit desimal dalam output ketika karakteristik statistik adalah rata-rata atau median." + }, + "description": "Buat sensor yang menghitung nilai min, maks, rata-rata, atau median dari sejumlah sensor input.", + "title": "Tambahkan sensor min/maks/rata-rata/median" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "Entitas input", + "round_digits": "Presisi", + "type": "Karakteristik statistik" + }, + "data_description": { + "round_digits": "Mengontrol jumlah digit desimal dalam output ketika karakteristik statistik adalah rata-rata atau median." + } + }, + "options": { + "data": { + "entity_ids": "Entitas input", + "round_digits": "Presisi", + "type": "Karakteristik statistik" + }, + "description": "Buat sensor yang menghitung nilai min, maks, rata-rata, atau median dari sejumlah sensor input." + } + } + }, + "title": "Sensor min/maks/rata-rata/median" +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/it.json b/homeassistant/components/min_max/translations/it.json new file mode 100644 index 00000000000..49c715a7507 --- /dev/null +++ b/homeassistant/components/min_max/translations/it.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "Entit\u00e0 di ingresso", + "name": "Nome", + "round_digits": "Precisione", + "type": "Caratteristica statistica" + }, + "data_description": { + "round_digits": "Controlla il numero di cifre decimali nell'output quando la caratteristica statistica \u00e8 media o mediana." + }, + "description": "Crea un sensore che calcoli un valore minimo, massimo, medio o mediano da un elenco di sensori di input.", + "title": "Aggiungi sensore min / max / media / mediana" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "Entit\u00e0 di ingresso", + "round_digits": "Precisione", + "type": "Caratteristica statistica" + }, + "data_description": { + "round_digits": "Controlla il numero di cifre decimali nell'output quando la caratteristica statistica \u00e8 media o mediana." + } + }, + "options": { + "data": { + "entity_ids": "Entit\u00e0 di ingresso", + "round_digits": "Precisione", + "type": "Caratteristica statistica" + }, + "description": "Crea un sensore che calcoli un valore minimo, massimo, medio o mediano da un elenco di sensori di input." + } + } + }, + "title": "Sensore min / max / media / mediana" +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/ja.json b/homeassistant/components/min_max/translations/ja.json new file mode 100644 index 00000000000..c853a7c389e --- /dev/null +++ b/homeassistant/components/min_max/translations/ja.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "\u5165\u529b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", + "name": "\u540d\u524d", + "round_digits": "\u7cbe\u5ea6", + "type": "\u7d71\u8a08\u7684\u7279\u6027" + }, + "data_description": { + "round_digits": "\u7d71\u8a08\u7684\u7279\u6027\u304c\u5e73\u5747\u5024\u307e\u305f\u306f\u4e2d\u592e\u5024\u306e\u5834\u5408\u306b\u3001\u51fa\u529b\u306b\u542b\u307e\u308c\u308b\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002" + }, + "description": "\u7d71\u8a08\u7684\u7279\u6027\u304c\u5e73\u5747\u307e\u305f\u306f\u4e2d\u592e\u5024\u306a\u5834\u5408\u306e\u7cbe\u5ea6\u3067\u3001\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002", + "title": "\u6700\u5c0f/\u6700\u5927/\u5e73\u5747/\u4e2d\u592e\u5024 \u30bb\u30f3\u30b5\u30fc\u3092\u8ffd\u52a0" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "\u5165\u529b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", + "round_digits": "\u7cbe\u5ea6", + "type": "\u7d71\u8a08\u7684\u7279\u6027" + }, + "data_description": { + "round_digits": "\u7d71\u8a08\u7684\u7279\u6027\u304c\u5e73\u5747\u5024\u307e\u305f\u306f\u4e2d\u592e\u5024\u306e\u5834\u5408\u306b\u3001\u51fa\u529b\u306b\u542b\u307e\u308c\u308b\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002" + } + }, + "options": { + "data": { + "entity_ids": "\u5165\u529b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", + "round_digits": "\u7cbe\u5ea6", + "type": "\u7d71\u8a08\u7684\u7279\u6027" + }, + "description": "\u7d71\u8a08\u7684\u7279\u6027\u304c\u5e73\u5747\u307e\u305f\u306f\u4e2d\u592e\u5024\u306a\u5834\u5408\u306e\u7cbe\u5ea6\u3067\u3001\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002" + } + } + }, + "title": "\u6700\u5c0f/\u6700\u5927/\u5e73\u5747/\u4e2d\u592e\u5024 \u30bb\u30f3\u30b5\u30fc" +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/nl.json b/homeassistant/components/min_max/translations/nl.json new file mode 100644 index 00000000000..36f09f97d54 --- /dev/null +++ b/homeassistant/components/min_max/translations/nl.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "Entiteiten invoeren", + "name": "Naam", + "round_digits": "Precisie", + "type": "statistisch kenmerk" + }, + "data_description": { + "round_digits": "Regelt het aantal decimale cijfers in de uitvoer wanneer de statistische eigenschap gemiddelde of mediaan is." + }, + "description": "Maak een sensor die een min, max, gemiddelde of mediaanwaarde berekent uit een lijst van invoersensoren.", + "title": "Voeg min / max / gemiddelde / mediaan sensor toe" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "Entiteiten invoeren", + "round_digits": "Precisie", + "type": "statistisch kenmerk" + }, + "data_description": { + "round_digits": "Regelt het aantal decimale cijfers in de uitvoer wanneer de statistische eigenschap gemiddelde of mediaan is." + } + }, + "options": { + "data": { + "entity_ids": "Invoer entiteiten", + "round_digits": "Precisie", + "type": "Statistische karakteristieken" + }, + "description": "Precisie bepaalt het aantal decimale cijfers wanneer het statistische kenmerk gemiddelde of mediaan is." + } + } + }, + "title": "Min / max / gemiddelde / mediaan sensor" +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/no.json b/homeassistant/components/min_max/translations/no.json new file mode 100644 index 00000000000..798430cec03 --- /dev/null +++ b/homeassistant/components/min_max/translations/no.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "Inndataenheter", + "name": "Navn", + "round_digits": "Presisjon", + "type": "Statistisk karakteristikk" + }, + "data_description": { + "round_digits": "Styrer antall desimaler i utdata n\u00e5r statistikkkarakteristikken er gjennomsnitt eller median." + }, + "description": "Lag en sensor som beregner en min, maks, middelverdi eller medianverdi fra en liste over inngangssensorer.", + "title": "Legg til min / maks / gjennomsnitt / median sensor" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "Inndataenheter", + "round_digits": "Presisjon", + "type": "Statistisk karakteristikk" + }, + "data_description": { + "round_digits": "Styrer antall desimaler i utdata n\u00e5r statistikkkarakteristikken er gjennomsnitt eller median." + } + }, + "options": { + "data": { + "entity_ids": "Inndataenheter", + "round_digits": "Presisjon", + "type": "Statistisk karakteristikk" + }, + "description": "Lag en sensor som beregner en min, maks, middelverdi eller medianverdi fra en liste over inngangssensorer." + } + } + }, + "title": "Min / maks / gjennomsnitt / median sensor" +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/pl.json b/homeassistant/components/min_max/translations/pl.json new file mode 100644 index 00000000000..0a29ad78339 --- /dev/null +++ b/homeassistant/components/min_max/translations/pl.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "Encje wej\u015bciowe", + "name": "Nazwa", + "round_digits": "Precyzja", + "type": "Charakterystyka statystyczna" + }, + "data_description": { + "round_digits": "Kontroluje liczb\u0119 cyfr dziesi\u0119tnych w danych wyj\u015bciowych, gdy charakterystyka statystyki jest \u015bredni\u0105 lub median\u0105." + }, + "description": "Utw\u00f3rz sensor, kt\u00f3ry oblicza warto\u015b\u0107 minimaln\u0105, maksymaln\u0105, \u015bredni\u0105 lub median\u0119 z listy sensor\u00f3w wej\u015bciowych.", + "title": "Dodawanie sensora min / maks / \u015brednia / mediana" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "Encje wej\u015bciowe", + "round_digits": "Precyzja", + "type": "Charakterystyka statystyczna" + }, + "data_description": { + "round_digits": "Kontroluje liczb\u0119 cyfr dziesi\u0119tnych w danych wyj\u015bciowych, gdy charakterystyka statystyki jest \u015bredni\u0105 lub median\u0105." + } + }, + "options": { + "data": { + "entity_ids": "Encje wej\u015bciowe", + "round_digits": "Precyzja", + "type": "Charakterystyka statystyczna" + }, + "description": "Utw\u00f3rz sensor, kt\u00f3ry oblicza warto\u015b\u0107 minimaln\u0105, maksymaln\u0105, \u015bredni\u0105 lub median\u0119 z listy sensor\u00f3w wej\u015bciowych." + } + } + }, + "title": "Sensor min./maks./\u015brednia/mediana" +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/pt-BR.json b/homeassistant/components/min_max/translations/pt-BR.json new file mode 100644 index 00000000000..9c1c77c304a --- /dev/null +++ b/homeassistant/components/min_max/translations/pt-BR.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "Entidades de entrada", + "name": "Nome", + "round_digits": "Precis\u00e3o", + "type": "Caracter\u00edstica estat\u00edstica" + }, + "data_description": { + "round_digits": "Controla o n\u00famero de d\u00edgitos decimais na sa\u00edda quando a caracter\u00edstica estat\u00edstica \u00e9 m\u00e9dia ou mediana." + }, + "description": "Crie um sensor que calcula um valor m\u00ednimo, m\u00e1ximo, m\u00e9dio ou mediano de uma lista de sensores de entrada.", + "title": "Adicionar sensor de m\u00ednima / m\u00e1xima / m\u00e9dia / mediana" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "Entidades de entrada", + "round_digits": "Precis\u00e3o", + "type": "Caracter\u00edstica estat\u00edstica" + }, + "data_description": { + "round_digits": "Controla o n\u00famero de d\u00edgitos decimais na sa\u00edda quando a caracter\u00edstica estat\u00edstica \u00e9 m\u00e9dia ou mediana." + } + }, + "options": { + "data": { + "entity_ids": "Entidades de entrada", + "round_digits": "Precis\u00e3o", + "type": "Caracter\u00edstica estat\u00edstica" + }, + "description": "Crie um sensor que calcula um valor m\u00ednimo, m\u00e1ximo, m\u00e9dio ou mediano de uma lista de sensores de entrada." + } + } + }, + "title": "Sensor m\u00edn./m\u00e1x./m\u00e9dio/mediano" +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/ru.json b/homeassistant/components/min_max/translations/ru.json new file mode 100644 index 00000000000..551660fcabb --- /dev/null +++ b/homeassistant/components/min_max/translations/ru.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "round_digits": "\u041e\u043a\u0440\u0443\u0433\u043b\u0435\u043d\u0438\u0435", + "type": "\u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0430" + }, + "data_description": { + "round_digits": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439, \u043a\u043e\u0433\u0434\u0430 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0430 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0441\u0440\u0435\u0434\u043d\u0435\u0439 \u0438\u043b\u0438 \u043c\u0435\u0434\u0438\u0430\u043d\u043d\u043e\u0439." + }, + "description": "\u0412\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435, \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435, \u0441\u0440\u0435\u0434\u043d\u0435\u0435 \u0438\u043b\u0438 \u043c\u0435\u0434\u0438\u0430\u043d\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430 \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432.", + "title": "\u041c\u0438\u043d\u0438\u043c\u0443\u043c / \u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c / \u0441\u0440\u0435\u0434\u043d\u0435\u0435 / \u043c\u0435\u0434\u0438\u0430\u043d\u0430" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", + "round_digits": "\u041e\u043a\u0440\u0443\u0433\u043b\u0435\u043d\u0438\u0435", + "type": "\u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0430" + }, + "data_description": { + "round_digits": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439, \u043a\u043e\u0433\u0434\u0430 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0430 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0441\u0440\u0435\u0434\u043d\u0435\u0439 \u0438\u043b\u0438 \u043c\u0435\u0434\u0438\u0430\u043d\u043d\u043e\u0439." + } + }, + "options": { + "data": { + "entity_ids": "\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", + "round_digits": "\u041e\u043a\u0440\u0443\u0433\u043b\u0435\u043d\u0438\u0435", + "type": "\u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0430" + }, + "description": "\u0412\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435, \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435, \u0441\u0440\u0435\u0434\u043d\u0435\u0435 \u0438\u043b\u0438 \u043c\u0435\u0434\u0438\u0430\u043d\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430 \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432." + } + } + }, + "title": "\u041d\u043e\u0432\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440" +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/sv.json b/homeassistant/components/min_max/translations/sv.json new file mode 100644 index 00000000000..d39c277daff --- /dev/null +++ b/homeassistant/components/min_max/translations/sv.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "round_digits": "Precision" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/tr.json b/homeassistant/components/min_max/translations/tr.json new file mode 100644 index 00000000000..7f192888615 --- /dev/null +++ b/homeassistant/components/min_max/translations/tr.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "Giri\u015f varl\u0131klar\u0131", + "name": "Ad", + "round_digits": "Hassas", + "type": "\u0130statistik \u00f6zelli\u011fi" + }, + "data_description": { + "round_digits": "\u0130statistik \u00f6zelli\u011fi ortalama veya medyan oldu\u011funda \u00e7\u0131kt\u0131daki ondal\u0131k basamak say\u0131s\u0131n\u0131 kontrol eder." + }, + "description": "Giri\u015f sens\u00f6rleri listesinden minimum, maksimum, ortalama veya medyan de\u011feri hesaplayan bir sens\u00f6r olu\u015fturun.", + "title": "Min / maks / ortalama / medyan sens\u00f6r\u00fc ekle" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "Giri\u015f varl\u0131klar\u0131", + "round_digits": "Hassas", + "type": "\u0130statistik \u00f6zelli\u011fi" + }, + "data_description": { + "round_digits": "\u0130statistik \u00f6zelli\u011fi ortalama veya medyan oldu\u011funda \u00e7\u0131kt\u0131daki ondal\u0131k basamak say\u0131s\u0131n\u0131 kontrol eder." + } + }, + "options": { + "data": { + "entity_ids": "Giri\u015f varl\u0131klar\u0131", + "round_digits": "Hassas", + "type": "\u0130statistik \u00f6zelli\u011fi" + }, + "description": "Giri\u015f sens\u00f6rleri listesinden minimum, maksimum, ortalama veya medyan de\u011feri hesaplayan bir sens\u00f6r olu\u015fturun." + } + } + }, + "title": "Min / maks / ortalama / medyan sens\u00f6r" +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/zh-Hans.json b/homeassistant/components/min_max/translations/zh-Hans.json new file mode 100644 index 00000000000..ca5555bbd0a --- /dev/null +++ b/homeassistant/components/min_max/translations/zh-Hans.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "\u8f93\u5165\u5b9e\u4f53", + "name": "\u540d\u79f0", + "round_digits": "\u7cbe\u5ea6", + "type": "\u7edf\u8ba1\u9879" + }, + "data_description": { + "round_digits": "\u5f53\u7edf\u8ba1\u9879\u4e3a\u5e73\u5747\u503c\u6216\u4e2d\u4f4d\u6570\u65f6\uff0c\u63a7\u5236\u8f93\u51fa\u7684\u5c0f\u6570\u4f4d\u6570\u3002" + }, + "description": "\u521b\u5efa\u4f20\u611f\u5668\u6765\u8ba1\u7b97\u591a\u4e2a\u8f93\u5165\u4f20\u611f\u5668\u503c\u7684\u6700\u5927\u503c\u3001\u6700\u5c0f\u503c\u3001\u5e73\u5747\u503c\u3001\u6216\u4e2d\u4f4d\u6570\u3002", + "title": "\u6dfb\u52a0\u6700\u5927\u503c/\u6700\u5c0f\u503c/\u5e73\u5747\u503c/\u4e2d\u4f4d\u6570\u4f20\u611f\u5668" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "\u8f93\u5165\u5b9e\u4f53", + "round_digits": "\u7cbe\u5ea6", + "type": "\u7edf\u8ba1\u9879" + }, + "data_description": { + "round_digits": "\u5f53\u7edf\u8ba1\u9879\u4e3a\u5e73\u5747\u503c\u6216\u4e2d\u4f4d\u6570\u65f6\uff0c\u63a7\u5236\u8f93\u51fa\u7684\u5c0f\u6570\u4f4d\u6570\u3002" + } + }, + "options": { + "data": { + "entity_ids": "\u8f93\u5165\u5b9e\u4f53", + "round_digits": "\u7cbe\u5ea6", + "type": "\u7edf\u8ba1\u9879" + }, + "description": "\u521b\u5efa\u4f20\u611f\u5668\u6765\u8ba1\u7b97\u591a\u4e2a\u8f93\u5165\u4f20\u611f\u5668\u503c\u7684\u6700\u5927\u503c\u3001\u6700\u5c0f\u503c\u3001\u5e73\u5747\u503c\u3001\u6216\u4e2d\u4f4d\u6570\u3002" + } + } + }, + "title": "\u6700\u5927\u503c/\u6700\u5c0f\u503c/\u5e73\u5747\u503c/\u4e2d\u4f4d\u6570\u4f20\u611f\u5668" +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/zh-Hant.json b/homeassistant/components/min_max/translations/zh-Hant.json new file mode 100644 index 00000000000..b0166e26cac --- /dev/null +++ b/homeassistant/components/min_max/translations/zh-Hant.json @@ -0,0 +1,42 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "\u8f38\u5165\u5be6\u9ad4", + "name": "\u540d\u7a31", + "round_digits": "\u6e96\u78ba\u5ea6", + "type": "\u7d71\u8a08\u7279\u5fb5" + }, + "data_description": { + "round_digits": "\u7576\u7d71\u8a08\u7279\u5fb5\u70ba\u5e73\u5747\u503c\u6216\u4e2d\u503c\u6642\u3001\u63a7\u5236\u8f38\u51fa\u5c0f\u6578\u4f4d\u6578\u3002" + }, + "description": "\u65b0\u589e\u81ea\u8f38\u5165\u611f\u6e2c\u5668\u4e86\u8868\u4e2d\uff0c\u8a08\u7b97\u6700\u4f4e\u3001\u6700\u9ad8\u3001\u5e73\u5747\u503c\u6216\u4e2d\u503c\u611f\u6e2c\u5668\u3002", + "title": "\u65b0\u589e\u6700\u5c0f\u503c / \u6700\u5927\u503c / \u5e73\u5747\u503c / \u4e2d\u503c\u611f\u6e2c\u5668" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "\u8f38\u5165\u5be6\u9ad4", + "round_digits": "\u6e96\u78ba\u5ea6", + "type": "\u7d71\u8a08\u7279\u5fb5" + }, + "data_description": { + "round_digits": "\u7576\u7d71\u8a08\u7279\u5fb5\u70ba\u5e73\u5747\u503c\u6216\u4e2d\u503c\u6642\u3001\u63a7\u5236\u8f38\u51fa\u5c0f\u6578\u4f4d\u6578\u3002" + } + }, + "options": { + "data": { + "entity_ids": "\u8f38\u5165\u5be6\u9ad4", + "round_digits": "\u6e96\u78ba\u5ea6", + "type": "\u7d71\u8a08\u7279\u5fb5" + }, + "description": "\u65b0\u589e\u81ea\u8f38\u5165\u611f\u6e2c\u5668\u4e86\u8868\u4e2d\uff0c\u8a08\u7b97\u6700\u4f4e\u3001\u6700\u9ad8\u3001\u5e73\u5747\u503c\u6216\u4e2d\u503c\u611f\u6e2c\u5668\u3002" + } + } + }, + "title": "\u6700\u5c0f\u503c / \u6700\u5927\u503c / \u5e73\u5747\u503c / \u4e2d\u503c\u611f\u6e2c\u5668" +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/translations/hu.json b/homeassistant/components/minecraft_server/translations/hu.json index 02c2a06d8ab..00fff153254 100644 --- a/homeassistant/components/minecraft_server/translations/hu.json +++ b/homeassistant/components/minecraft_server/translations/hu.json @@ -12,7 +12,7 @@ "user": { "data": { "host": "C\u00edm", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" }, "description": "\u00c1ll\u00edtsa be a Minecraft Server p\u00e9ld\u00e1nyt, hogy lehet\u0151v\u00e9 tegye a megfigyel\u00e9st.", "title": "Kapcsold \u00f6ssze a Minecraft szervered" diff --git a/homeassistant/components/minecraft_server/translations/pl.json b/homeassistant/components/minecraft_server/translations/pl.json index fed8d250a90..88b5dd869e4 100644 --- a/homeassistant/components/minecraft_server/translations/pl.json +++ b/homeassistant/components/minecraft_server/translations/pl.json @@ -15,7 +15,7 @@ "name": "Nazwa" }, "description": "Skonfiguruj instancj\u0119 serwera Minecraft, aby umo\u017cliwi\u0107 monitorowanie.", - "title": "Po\u0142\u0105cz sw\u00f3j serwer Minecraft" + "title": "Po\u0142\u0105czenie z Twoim serwerem Minecraft" } } } diff --git a/homeassistant/components/mjpeg/translations/fr.json b/homeassistant/components/mjpeg/translations/fr.json index f54c60631a6..405cbc8bf51 100644 --- a/homeassistant/components/mjpeg/translations/fr.json +++ b/homeassistant/components/mjpeg/translations/fr.json @@ -13,6 +13,7 @@ "mjpeg_url": "URL MJPEG", "name": "Nom", "password": "Mot de passe", + "still_image_url": "URL de l'image fixe", "username": "Nom d'utilisateur", "verify_ssl": "V\u00e9rifier le certificat SSL" } @@ -31,6 +32,7 @@ "mjpeg_url": "URL MJPEG", "name": "Nom", "password": "Mot de passe", + "still_image_url": "URL de l'image fixe", "username": "Nom d'utilisateur", "verify_ssl": "V\u00e9rifier le certificat SSL" } diff --git a/homeassistant/components/mjpeg/translations/hu.json b/homeassistant/components/mjpeg/translations/hu.json index 0a87f484887..f3029663306 100644 --- a/homeassistant/components/mjpeg/translations/hu.json +++ b/homeassistant/components/mjpeg/translations/hu.json @@ -11,7 +11,7 @@ "user": { "data": { "mjpeg_url": "MJPEG URL-c\u00edme", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3", "still_image_url": "\u00c1ll\u00f3k\u00e9p URL-c\u00edme", "username": "Felhaszn\u00e1l\u00f3n\u00e9v", @@ -30,7 +30,7 @@ "init": { "data": { "mjpeg_url": "MJPEG URL-c\u00edme", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3", "still_image_url": "\u00c1ll\u00f3k\u00e9p URL-c\u00edme", "username": "Felhaszn\u00e1l\u00f3n\u00e9v", diff --git a/homeassistant/components/modem_callerid/translations/hu.json b/homeassistant/components/modem_callerid/translations/hu.json index 3a75df30a7d..2a53c24d902 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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "no_devices_found": "Nem tal\u00e1lhat\u00f3 egy\u00e9b eszk\u00f6z" }, "error": { @@ -15,7 +15,7 @@ }, "user": { "data": { - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "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.", diff --git a/homeassistant/components/modern_forms/translations/et.json b/homeassistant/components/modern_forms/translations/et.json index ee325440c13..7140888db84 100644 --- a/homeassistant/components/modern_forms/translations/et.json +++ b/homeassistant/components/modern_forms/translations/et.json @@ -19,7 +19,7 @@ "description": "Seadista Modern Forms'i ventilaator sidumiseks Home Assistantiga." }, "zeroconf_confirm": { - "description": "Kas lisada Modern Forms'i ventilaator nimega `{nimi}` Home Assistanti?", + "description": "Kas lisada Modern Forms'i ventilaator nimega `{name}` Home Assistanti?", "title": "Leitud Modern Forms ventilaator" } } diff --git a/homeassistant/components/moon/translations/cs.json b/homeassistant/components/moon/translations/cs.json new file mode 100644 index 00000000000..f1c26a20f6e --- /dev/null +++ b/homeassistant/components/moon/translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "step": { + "user": { + "description": "Chcete za\u010d\u00edt nastavovat?" + } + } + }, + "title": "M\u011bs\u00edc" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/sensor.fi.json b/homeassistant/components/moon/translations/sensor.fi.json index f7788972044..a35afac10d6 100644 --- a/homeassistant/components/moon/translations/sensor.fi.json +++ b/homeassistant/components/moon/translations/sensor.fi.json @@ -4,7 +4,11 @@ "first_quarter": "Ensimm\u00e4inen nelj\u00e4nnes", "full_moon": "T\u00e4ysikuu", "last_quarter": "Viimeinen nelj\u00e4nnes", - "new_moon": "Uusikuu" + "new_moon": "Uusikuu", + "waning_crescent": "V\u00e4henev\u00e4 sirppi", + "waning_gibbous": "V\u00e4henev\u00e4 kuperakuu", + "waxing_crescent": "Kasvava sirppi", + "waxing_gibbous": "Kasvava kuperakuu" } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/ca.json b/homeassistant/components/motion_blinds/translations/ca.json index 1db1976cbd3..18cc8f892d6 100644 --- a/homeassistant/components/motion_blinds/translations/ca.json +++ b/homeassistant/components/motion_blinds/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_configured": "El dispositiu ja est\u00e0 configurat, la configuraci\u00f3 de connexi\u00f3 s'ha actualitzat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "connection_error": "Ha fallat la connexi\u00f3" }, @@ -9,7 +9,7 @@ "discovery_error": "No s'ha pogut descobrir cap Motion Gateway", "invalid_interface": "Interf\u00edcie de xarxa no v\u00e0lida" }, - "flow_title": "Motion Blinds", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/motion_blinds/translations/de.json b/homeassistant/components/motion_blinds/translations/de.json index a4758c85d4b..c5ced3ddd55 100644 --- a/homeassistant/components/motion_blinds/translations/de.json +++ b/homeassistant/components/motion_blinds/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert, Verbindungseinstellungen werden aktualisiert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "connection_error": "Verbindung fehlgeschlagen" }, @@ -9,7 +9,7 @@ "discovery_error": "Motion-Gateway konnte nicht gefunden werden", "invalid_interface": "Ung\u00fcltige Netzwerkschnittstelle" }, - "flow_title": "Jalousien", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/motion_blinds/translations/en.json b/homeassistant/components/motion_blinds/translations/en.json index 4033a8f2016..92931ee27ab 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": "{short_mac} ({ip_address})", "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" diff --git a/homeassistant/components/motion_blinds/translations/et.json b/homeassistant/components/motion_blinds/translations/et.json index 7091c402a2e..935e345ea3a 100644 --- a/homeassistant/components/motion_blinds/translations/et.json +++ b/homeassistant/components/motion_blinds/translations/et.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_configured": "Seade on juba h\u00e4\u00e4lestatud, \u00fchenduse s\u00e4tted on v\u00e4rskendatud", "already_in_progress": "Seadistamine on juba k\u00e4imas", "connection_error": "\u00dchendamine nurjus" }, @@ -9,7 +9,7 @@ "discovery_error": "Motion Gateway avastamine nurjus", "invalid_interface": "Sobimatu v\u00f5rguliides" }, - "flow_title": "", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/motion_blinds/translations/fr.json b/homeassistant/components/motion_blinds/translations/fr.json index 75fb1daa9be..09cf93fde60 100644 --- a/homeassistant/components/motion_blinds/translations/fr.json +++ b/homeassistant/components/motion_blinds/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9, les param\u00e8tres de connexion ont \u00e9t\u00e9 mis \u00e0 jour", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "connection_error": "\u00c9chec de connexion" }, @@ -9,7 +9,7 @@ "discovery_error": "Impossible de d\u00e9couvrir une Motion Gateway", "invalid_interface": "Interface r\u00e9seau non valide" }, - "flow_title": "Stores de mouvement", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/motion_blinds/translations/he.json b/homeassistant/components/motion_blinds/translations/he.json index 0876de6504a..ce6f3d2918e 100644 --- a/homeassistant/components/motion_blinds/translations/he.json +++ b/homeassistant/components/motion_blinds/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4 \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e2\u05d5\u05d3\u05db\u05e0\u05d5", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "connection_error": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, diff --git a/homeassistant/components/motion_blinds/translations/hu.json b/homeassistant/components/motion_blinds/translations/hu.json index 9d0f41db143..64334c54a28 100644 --- a/homeassistant/components/motion_blinds/translations/hu.json +++ b/homeassistant/components/motion_blinds/translations/hu.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A konfigur\u00e1l\u00e1s m\u00e1r folyamatban van", + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van, a csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sai friss\u00edtve vannak", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "connection_error": "Sikertelen csatlakoz\u00e1s" }, "error": { "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", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { @@ -17,7 +17,7 @@ "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" + "title": "Red\u0151ny/rol\u00f3" }, "select": { "data": { @@ -32,7 +32,7 @@ "host": "IP c\u00edm" }, "description": "Csatlakozzon a Motion Gateway-hez, ha az IP-c\u00edm nincs be\u00e1ll\u00edtva, akkor az automatikus felder\u00edt\u00e9st haszn\u00e1lja", - "title": "Mozg\u00f3 red\u0151ny" + "title": "Red\u0151ny/rol\u00f3" } } }, @@ -43,7 +43,7 @@ "wait_for_push": "Multicast adatokra v\u00e1rakoz\u00e1s friss\u00edt\u00e9skor" }, "description": "Opcion\u00e1lis be\u00e1ll\u00edt\u00e1sok megad\u00e1sa", - "title": "Motion Blinds" + "title": "Red\u0151ny/rol\u00f3" } } } diff --git a/homeassistant/components/motion_blinds/translations/id.json b/homeassistant/components/motion_blinds/translations/id.json index 269470110ec..d576ced8c0a 100644 --- a/homeassistant/components/motion_blinds/translations/id.json +++ b/homeassistant/components/motion_blinds/translations/id.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi", + "already_configured": "Perangkat sudah dikonfigurasi, pengaturan koneksi diperbarui", "already_in_progress": "Alur konfigurasi sedang berlangsung", "connection_error": "Gagal terhubung" }, @@ -9,7 +9,7 @@ "discovery_error": "Gagal menemukan Motion Gateway", "invalid_interface": "Antarmuka jaringan tidak valid" }, - "flow_title": "Motion Blinds", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/motion_blinds/translations/it.json b/homeassistant/components/motion_blinds/translations/it.json index 6a9cef8a0af..0871ce07bf8 100644 --- a/homeassistant/components/motion_blinds/translations/it.json +++ b/homeassistant/components/motion_blinds/translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato, le impostazioni di connessione sono aggiornate", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "connection_error": "Impossibile connettersi" }, @@ -9,7 +9,7 @@ "discovery_error": "Impossibile rilevare un Motion Gateway", "invalid_interface": "Interfaccia di rete non valida" }, - "flow_title": "Tende Motion", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/motion_blinds/translations/nl.json b/homeassistant/components/motion_blinds/translations/nl.json index 3d3d078b814..6cb69b06b87 100644 --- a/homeassistant/components/motion_blinds/translations/nl.json +++ b/homeassistant/components/motion_blinds/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd", + "already_configured": "Apparaat is al geconfigureerd, verbindingsinstellingen zijn bijgewerkt", "already_in_progress": "De configuratiestroom is al aan de gang", "connection_error": "Kan geen verbinding maken" }, @@ -9,7 +9,7 @@ "discovery_error": "Kan geen Motion Gateway vinden", "invalid_interface": "Ongeldige netwerkinterface" }, - "flow_title": "Motion Blinds", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/motion_blinds/translations/no.json b/homeassistant/components/motion_blinds/translations/no.json index 242a6647da9..d6c4708ea8c 100644 --- a/homeassistant/components/motion_blinds/translations/no.json +++ b/homeassistant/components/motion_blinds/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert", + "already_configured": "Enheten er allerede konfigurert , tilkoblingsinnstillingene er oppdatert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "connection_error": "Tilkobling mislyktes" }, @@ -9,7 +9,7 @@ "discovery_error": "Kunne ikke oppdage en Motion Gateway", "invalid_interface": "Ugyldig nettverksgrensesnitt" }, - "flow_title": "Motion Blinds", + "flow_title": "{short_mac} ( {ip_address} )", "step": { "connect": { "data": { diff --git a/homeassistant/components/motion_blinds/translations/pl.json b/homeassistant/components/motion_blinds/translations/pl.json index 6161b6aa3da..2a042859b88 100644 --- a/homeassistant/components/motion_blinds/translations/pl.json +++ b/homeassistant/components/motion_blinds/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane, ustawienia po\u0142\u0105czenia zosta\u0142y zaktualizowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, @@ -9,7 +9,7 @@ "discovery_error": "Nie uda\u0142o si\u0119 wykry\u0107 bramki ruchu", "invalid_interface": "Nieprawid\u0142owy interfejs sieciowy" }, - "flow_title": "Rolety Motion", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/motion_blinds/translations/pt-BR.json b/homeassistant/components/motion_blinds/translations/pt-BR.json index dcabdbd16e5..01658cea852 100644 --- a/homeassistant/components/motion_blinds/translations/pt-BR.json +++ b/homeassistant/components/motion_blinds/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado, as configura\u00e7\u00f5es de conex\u00e3o s\u00e3o atualizadas", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "connection_error": "Falha ao conectar" }, @@ -9,7 +9,7 @@ "discovery_error": "Falha ao descobrir um Motion Gateway", "invalid_interface": "Interface de rede inv\u00e1lida" }, - "flow_title": "Cortinas de movimento", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/motion_blinds/translations/ru.json b/homeassistant/components/motion_blinds/translations/ru.json index 83fe77ff9eb..e7a4492bd54 100644 --- a/homeassistant/components/motion_blinds/translations/ru.json +++ b/homeassistant/components/motion_blinds/translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_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. \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u044b.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, @@ -9,7 +9,7 @@ "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", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/motion_blinds/translations/tr.json b/homeassistant/components/motion_blinds/translations/tr.json index c9a0efe2538..824569ce173 100644 --- a/homeassistant/components/motion_blinds/translations/tr.json +++ b/homeassistant/components/motion_blinds/translations/tr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f , ba\u011flant\u0131 ayarlar\u0131 g\u00fcncellendi", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "connection_error": "Ba\u011flanma hatas\u0131" }, @@ -9,7 +9,7 @@ "discovery_error": "Motion Gateway bulunamad\u0131", "invalid_interface": "Ge\u00e7ersiz a\u011f aray\u00fcz\u00fc" }, - "flow_title": "Hareketli Panjurlar", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/motion_blinds/translations/zh-Hant.json b/homeassistant/components/motion_blinds/translations/zh-Hant.json index df300e0511f..e7fa565ba36 100644 --- a/homeassistant/components/motion_blinds/translations/zh-Hant.json +++ b/homeassistant/components/motion_blinds/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u9023\u7dda\u8a2d\u5b9a\u5df2\u66f4\u65b0", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "connection_error": "\u9023\u7dda\u5931\u6557" }, @@ -9,7 +9,7 @@ "discovery_error": "\u641c\u7d22 Motion \u9598\u9053\u5668\u5931\u6557", "invalid_interface": "\u7db2\u8def\u4ecb\u9762\u7121\u6548" }, - "flow_title": "Motion Blinds", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/nam/translations/hu.json b/homeassistant/components/nam/translations/hu.json index f0cf505163c..e0aa942afd8 100644 --- a/homeassistant/components/nam/translations/hu.json +++ b/homeassistant/components/nam/translations/hu.json @@ -11,7 +11,7 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba" }, - "flow_title": "{name}", + "flow_title": "{host}", "step": { "confirm_discovery": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Nettigo Air Monitor-ot a {host} c\u00edmen?" diff --git a/homeassistant/components/nanoleaf/translations/fr.json b/homeassistant/components/nanoleaf/translations/fr.json index 5893635580b..5a9a8022ecb 100644 --- a/homeassistant/components/nanoleaf/translations/fr.json +++ b/homeassistant/components/nanoleaf/translations/fr.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Balayer vers le bas", + "swipe_left": "Balayer vers la gauche", + "swipe_right": "Balayer vers la droite", + "swipe_up": "Balayer vers le haut" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/hu.json b/homeassistant/components/nanoleaf/translations/hu.json index c67c4f958de..03ed5c92828 100644 --- a/homeassistant/components/nanoleaf/translations/hu.json +++ b/homeassistant/components/nanoleaf/translations/hu.json @@ -15,7 +15,7 @@ "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 **K\u00fcld\u00e9s** 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 **Mehet** gombra 30 m\u00e1sodpercen bel\u00fcl.", "title": "Nanoleaf link" }, "user": { diff --git a/homeassistant/components/neato/translations/hu.json b/homeassistant/components/neato/translations/hu.json index 281c5cd61a9..255378eda35 100644 --- a/homeassistant/components/neato/translations/hu.json +++ b/homeassistant/components/neato/translations/hu.json @@ -4,7 +4,7 @@ "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\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.", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "create_entry": { @@ -12,7 +12,7 @@ }, "step": { "pick_implementation": { - "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" }, "reauth_confirm": { "title": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" diff --git a/homeassistant/components/neato/translations/it.json b/homeassistant/components/neato/translations/it.json index 51d9119eb2c..000625a0294 100644 --- a/homeassistant/components/neato/translations/it.json +++ b/homeassistant/components/neato/translations/it.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", - "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json index 2bb9d2dbaec..1f98e162a7b 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.", "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token", "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.", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", "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." @@ -33,7 +33,7 @@ "data": { "flow_impl": "Szolg\u00e1ltat\u00f3" }, - "description": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert", + "description": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert", "title": "Hiteles\u00edt\u00e9si Szolg\u00e1ltat\u00f3" }, "link": { @@ -44,7 +44,7 @@ "title": "Nest fi\u00f3k \u00f6sszekapcsol\u00e1sa" }, "pick_implementation": { - "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" }, "pubsub": { "data": { @@ -54,7 +54,7 @@ "title": "Google Cloud konfigur\u00e1l\u00e1sa" }, "reauth_confirm": { - "description": "A Nest integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie a fi\u00f3kodat", + "description": "A Nest integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie a fi\u00f3kj\u00e1t", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" } } diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 7c92631351e..5942414bcf3 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "invalid_access_token": "Token di accesso non valido", - "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json index c81e5f180b9..5e9b9895db4 100644 --- a/homeassistant/components/nest/translations/pl.json +++ b/homeassistant/components/nest/translations/pl.json @@ -27,7 +27,7 @@ "code": "Token dost\u0119pu" }, "description": "Aby po\u0142\u0105czy\u0107 swoje konto Google, [authorize your account]({url}). \n\nPo autoryzacji skopiuj i wklej podany poni\u017cej token uwierzytelniaj\u0105cy.", - "title": "Po\u0142\u0105cz z kontem Google" + "title": "Po\u0142\u0105czenie z kontem Google" }, "init": { "data": { diff --git a/homeassistant/components/netatmo/translations/hu.json b/homeassistant/components/netatmo/translations/hu.json index ae781d86ca8..51c46edbe86 100644 --- a/homeassistant/components/netatmo/translations/hu.json +++ b/homeassistant/components/netatmo/translations/hu.json @@ -3,7 +3,7 @@ "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\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.", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, @@ -12,7 +12,7 @@ }, "step": { "pick_implementation": { - "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" }, "reauth_confirm": { "description": "A Netatmo integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie a fi\u00f3kj\u00e1t.", diff --git a/homeassistant/components/netatmo/translations/it.json b/homeassistant/components/netatmo/translations/it.json index e26cd64705f..b2210a4375c 100644 --- a/homeassistant/components/netatmo/translations/it.json +++ b/homeassistant/components/netatmo/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", - "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." diff --git a/homeassistant/components/netatmo/translations/pt-BR.json b/homeassistant/components/netatmo/translations/pt-BR.json index 32cc610f596..b47c0ea3646 100644 --- a/homeassistant/components/netatmo/translations/pt-BR.json +++ b/homeassistant/components/netatmo/translations/pt-BR.json @@ -52,7 +52,7 @@ "lon_ne": "Longitude nordeste", "lon_sw": "Longitude sudoeste", "mode": "C\u00e1lculo", - "show_on_map": "Mostrar no mapa?" + "show_on_map": "Mostrar no mapa" }, "description": "Configure um sensor meteorol\u00f3gico p\u00fablico para uma \u00e1rea.", "title": "Sensor meteorol\u00f3gico p\u00fablico Netatmo" diff --git a/homeassistant/components/nfandroidtv/translations/ca.json b/homeassistant/components/nfandroidtv/translations/ca.json index 861ad41a39b..0eda4938d9d 100644 --- a/homeassistant/components/nfandroidtv/translations/ca.json +++ b/homeassistant/components/nfandroidtv/translations/ca.json @@ -13,7 +13,7 @@ "host": "Amfitri\u00f3", "name": "Nom" }, - "description": "Aquesta integraci\u00f3 necessita l'aplicaci\u00f3 Notificacions per a Android TV. \n\nPer Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nPer Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\nHauries de configurar o b\u00e9 una reserva DHCP al router (consulta el manual del teu rounter) o b\u00e9 adre\u00e7a IP est\u00e0tica al dispositiu. Si no o fas, el disositiu acabar\u00e0 deixant d'estar disponible.", + "description": "Consulta la documentaci\u00f3 per assegurar-te que compleixes tots els requisits.", "title": "Notificacions per a Android TV / Fire TV" } } diff --git a/homeassistant/components/nfandroidtv/translations/de.json b/homeassistant/components/nfandroidtv/translations/de.json index b3adce9ac07..d9df9058d13 100644 --- a/homeassistant/components/nfandroidtv/translations/de.json +++ b/homeassistant/components/nfandroidtv/translations/de.json @@ -13,7 +13,7 @@ "host": "Host", "name": "Name" }, - "description": "Diese Integration erfordert die App \"Benachrichtigungen f\u00fcr Android TV\".\n\nF\u00fcr Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nF\u00fcr Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nDu solltest entweder eine DHCP-Reservierung auf deinem Router (siehe Benutzerhandbuch deines Routers) oder eine statische IP-Adresse auf dem Ger\u00e4t einrichten. Andernfalls wird das Ger\u00e4t irgendwann nicht mehr verf\u00fcgbar sein.", + "description": "Bitte lies die Dokumentation, um sicherzustellen, dass alle Anforderungen erf\u00fcllt sind.", "title": "Benachrichtigungen f\u00fcr Android TV / Fire TV" } } diff --git a/homeassistant/components/nfandroidtv/translations/en.json b/homeassistant/components/nfandroidtv/translations/en.json index f117428df35..3c1383b91b8 100644 --- a/homeassistant/components/nfandroidtv/translations/en.json +++ b/homeassistant/components/nfandroidtv/translations/en.json @@ -13,7 +13,7 @@ "host": "Host", "name": "Name" }, - "description": "This integration requires the Notifications for Android TV app.\n\nFor Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFor Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nYou should set up either DHCP reservation on your router (refer to your router's user manual) or a static IP address on the device. If not, the device will eventually become unavailable.", + "description": "Please refer to the documentation to make sure all requirements are met.", "title": "Notifications for Android TV / Fire TV" } } diff --git a/homeassistant/components/nfandroidtv/translations/et.json b/homeassistant/components/nfandroidtv/translations/et.json index f2405ab1421..f567795b608 100644 --- a/homeassistant/components/nfandroidtv/translations/et.json +++ b/homeassistant/components/nfandroidtv/translations/et.json @@ -13,7 +13,7 @@ "host": "Host", "name": "Nimi" }, - "description": "See sidumine n\u00f5uab Android TV rakenduse Notifications for Android TV kasutamist.\n\nAndroid TV jaoks: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV jaoks: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nPead seadma ruuterile kas DHCP-reservatsiooni (vt ruuteri kasutusjuhendit) v\u00f5i seadme staatilise IP-aadressi. Vastasel juhul muutub seade l\u00f5puks k\u00e4ttesaamatuks.", + "description": "Vaata dokumentatsiooni, et veenduda, et k\u00f5ik n\u00f5uded on t\u00e4idetud.", "title": "Android TV / Fire TV teavitused" } } diff --git a/homeassistant/components/nfandroidtv/translations/fr.json b/homeassistant/components/nfandroidtv/translations/fr.json index 6d00852889b..7c69bbc6ac0 100644 --- a/homeassistant/components/nfandroidtv/translations/fr.json +++ b/homeassistant/components/nfandroidtv/translations/fr.json @@ -13,7 +13,7 @@ "host": "H\u00f4te", "name": "Nom" }, - "description": "Cette int\u00e9gration n\u00e9cessite l'application Notifications pour Android TV. \n\nPour Android TV\u00a0: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nPour Fire TV\u00a0: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\nVous devez configurer soit une r\u00e9servation DHCP sur votre routeur (reportez-vous au manuel d'utilisation de votre routeur) soit une adresse IP statique sur l'appareil. Sinon, l'appareil finira par devenir indisponible.", + "description": "Veuillez consulter la documentation afin de vous assurer que toutes les conditions sont respect\u00e9es.", "title": "Notifications pour Android TV / Fire TV" } } diff --git a/homeassistant/components/nfandroidtv/translations/he.json b/homeassistant/components/nfandroidtv/translations/he.json index dc4d71c70ca..65eefcd05b4 100644 --- a/homeassistant/components/nfandroidtv/translations/he.json +++ b/homeassistant/components/nfandroidtv/translations/he.json @@ -13,7 +13,7 @@ "host": "\u05de\u05d0\u05e8\u05d7", "name": "\u05e9\u05dd" }, - "description": "\u05e9\u05d9\u05dc\u05d5\u05d1 \u05d6\u05d4 \u05d3\u05d5\u05e8\u05e9 \u05d0\u05ea \u05d4\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d9\u05d9\u05e9\u05d5\u05dd \u05d4\u05ea\u05e8\u05d0\u05d5\u05ea \u05e2\u05d1\u05d5\u05e8 \u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05ea \u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3.\n\n\u05e2\u05d1\u05d5\u05e8 \u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d9\u05ea \u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\n\u05e2\u05d1\u05d5\u05d3 \u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05ea \u05d0\u05de\u05d6\u05d5\u05df: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\n\u05e2\u05dc\u05d9\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d4\u05d6\u05de\u05e0\u05ea DHCP \u05d1\u05e0\u05ea\u05d1 \u05e9\u05dc\u05da (\u05e2\u05d9\u05d9\u05df \u05d1\u05de\u05d3\u05e8\u05d9\u05da \u05dc\u05de\u05e9\u05ea\u05de\u05e9 \u05e9\u05dc \u05d4\u05e0\u05ea\u05d1) \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05e1\u05d8\u05d8\u05d9\u05ea \u05d1\u05de\u05db\u05e9\u05d9\u05e8. \u05d0\u05dd \u05dc\u05d0, \u05d4\u05d4\u05ea\u05e7\u05df \u05d9\u05d4\u05e4\u05d5\u05da \u05d1\u05e1\u05d5\u05e4\u05d5 \u05e9\u05dc \u05d3\u05d1\u05e8 \u05dc\u05dc\u05d0 \u05d6\u05de\u05d9\u05df.", + "description": "\u05e0\u05d0 \u05dc\u05e2\u05d9\u05d9\u05df \u05d1\u05ea\u05d9\u05e2\u05d5\u05d3 \u05db\u05d3\u05d9 \u05dc\u05d5\u05d5\u05d3\u05d0 \u05e9\u05db\u05dc \u05d4\u05d3\u05e8\u05d9\u05e9\u05d5\u05ea \u05de\u05ea\u05e7\u05d9\u05d9\u05de\u05d5\u05ea.", "title": "\u05d4\u05ea\u05e8\u05d0\u05d5\u05ea \u05e2\u05d1\u05d5\u05e8 \u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d9\u05ea \u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3 / \u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d9\u05ea \u05d0\u05de\u05d6\u05d5\u05df" } } diff --git a/homeassistant/components/nfandroidtv/translations/hu.json b/homeassistant/components/nfandroidtv/translations/hu.json index c0dc8d679d6..0c8d0567483 100644 --- a/homeassistant/components/nfandroidtv/translations/hu.json +++ b/homeassistant/components/nfandroidtv/translations/hu.json @@ -11,9 +11,9 @@ "user": { "data": { "host": "C\u00edm", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" }, - "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.", + "description": "K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, hogy megbizonyosodjon arr\u00f3l, hogy minden k\u00f6vetelm\u00e9ny teljes\u00fcl.", "title": "\u00c9rtes\u00edt\u00e9sek Android TV / Fire TV eset\u00e9n" } } diff --git a/homeassistant/components/nfandroidtv/translations/id.json b/homeassistant/components/nfandroidtv/translations/id.json index fd70679d5a4..de4ded1e98a 100644 --- a/homeassistant/components/nfandroidtv/translations/id.json +++ b/homeassistant/components/nfandroidtv/translations/id.json @@ -13,7 +13,7 @@ "host": "Host", "name": "Nama" }, - "description": "Integrasi ini memerlukan aplikasi Notifikasi untuk Android TV.\n\nUntuk Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nUntuk Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nAnda harus mengatur reservasi DHCP di router Anda (lihat manual pengguna router Anda) atau alamat IP statis pada perangkat. Jika tidak, perangkat akhirnya akan menjadi tidak tersedia.", + "description": "Rujuk ke dokumentasi untuk memastikan semua persyaratan terpenuhi.", "title": "Notifikasi untuk Android TV/Fire TV" } } diff --git a/homeassistant/components/nfandroidtv/translations/it.json b/homeassistant/components/nfandroidtv/translations/it.json index 6251a31e3bc..83eea49b419 100644 --- a/homeassistant/components/nfandroidtv/translations/it.json +++ b/homeassistant/components/nfandroidtv/translations/it.json @@ -13,7 +13,7 @@ "host": "Host", "name": "Nome" }, - "description": "Questa integrazione richiede l'app Notifiche per Android TV. \n\nPer Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nPer Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\nDovresti configurare la prenotazione DHCP sul router (fai riferimento al manuale utente del router) o un indirizzo IP statico sul dispositivo. In caso contrario, il dispositivo alla fine non sar\u00e0 pi\u00f9 disponibile.", + "description": "Fai riferimento alla documentazione per assicurarti che tutti i requisiti siano soddisfatti.", "title": "Notifiche per Android TV / Fire TV" } } diff --git a/homeassistant/components/nfandroidtv/translations/nl.json b/homeassistant/components/nfandroidtv/translations/nl.json index acd936abe70..10c9f44a94a 100644 --- a/homeassistant/components/nfandroidtv/translations/nl.json +++ b/homeassistant/components/nfandroidtv/translations/nl.json @@ -13,7 +13,7 @@ "host": "Host", "name": "Naam" }, - "description": "Voor deze integratie is de app Notifications for Android TV vereist.\n\nVoor Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nVoor Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nU moet een DHCP-reservering op uw router instellen (raadpleeg de gebruikershandleiding van uw router) of een statisch IP-adres op het apparaat instellen. Zo niet, dan zal het apparaat uiteindelijk onbeschikbaar worden.", + "description": "Raadpleeg de documentatie om er zeker van te zijn dat aan alle vereisten is voldaan.", "title": "Meldingen voor Android TV / Fire TV" } } diff --git a/homeassistant/components/nfandroidtv/translations/no.json b/homeassistant/components/nfandroidtv/translations/no.json index e8aea574c96..8d59ca40b0d 100644 --- a/homeassistant/components/nfandroidtv/translations/no.json +++ b/homeassistant/components/nfandroidtv/translations/no.json @@ -13,7 +13,7 @@ "host": "Vert", "name": "Navn" }, - "description": "Denne integrasjonen krever Notifications for Android TV -appen. \n\n For Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\n For Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\n Du b\u00f8r konfigurere enten DHCP -reservasjon p\u00e5 ruteren din (se brukerh\u00e5ndboken til ruteren din) eller en statisk IP -adresse p\u00e5 enheten. Hvis ikke, vil enheten til slutt bli utilgjengelig.", + "description": "Se dokumentasjonen for \u00e5 sikre at alle krav er oppfylt.", "title": "Varsler for Android TV / Fire TV" } } diff --git a/homeassistant/components/nfandroidtv/translations/pl.json b/homeassistant/components/nfandroidtv/translations/pl.json index 4dd742b7c1f..a42335ceb10 100644 --- a/homeassistant/components/nfandroidtv/translations/pl.json +++ b/homeassistant/components/nfandroidtv/translations/pl.json @@ -13,7 +13,7 @@ "host": "Nazwa hosta lub adres IP", "name": "Nazwa" }, - "description": "Ta integracja wymaga aplikacji Powiadomienia dla Androida TV. \n\nAndroid TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\nNale\u017cy skonfigurowa\u0107 rezerwacj\u0119 DHCP na routerze (patrz instrukcja obs\u0142ugi routera) lub ustawi\u0107 statyczny adres IP na urz\u0105dzeniu. Je\u015bli tego nie zrobisz, urz\u0105dzenie ostatecznie stanie si\u0119 niedost\u0119pne.", + "description": "Zapoznaj si\u0119 z dokumentacj\u0105, aby upewni\u0107 si\u0119, \u017ce wszystkie wymagania s\u0105 spe\u0142nione.", "title": "Powiadomienia dla Android TV / Fire TV" } } diff --git a/homeassistant/components/nfandroidtv/translations/pt-BR.json b/homeassistant/components/nfandroidtv/translations/pt-BR.json index 43b7a296c21..f4488408b42 100644 --- a/homeassistant/components/nfandroidtv/translations/pt-BR.json +++ b/homeassistant/components/nfandroidtv/translations/pt-BR.json @@ -13,7 +13,7 @@ "host": "Nome do host", "name": "Nome" }, - "description": "Essa integra\u00e7\u00e3o requer as Notifica\u00e7\u00f5es para o aplicativo 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\nVoc\u00ea deve configurar a reserva DHCP no roteador (consulte o manual do usu\u00e1rio do roteador) ou um endere\u00e7o IP est\u00e1tico no dispositivo. Se n\u00e3o, o dispositivo acabar\u00e1 por ficar indispon\u00edvel.", + "description": "Consulte a documenta\u00e7\u00e3o para garantir que todos os requisitos sejam atendidos.", "title": "Notifica\u00e7\u00f5es para Android TV / Fire TV" } } diff --git a/homeassistant/components/nfandroidtv/translations/ru.json b/homeassistant/components/nfandroidtv/translations/ru.json index ce0d4651dfc..12451d4735b 100644 --- a/homeassistant/components/nfandroidtv/translations/ru.json +++ b/homeassistant/components/nfandroidtv/translations/ru.json @@ -13,7 +13,7 @@ "host": "\u0425\u043e\u0441\u0442", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u0414\u043b\u044f \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \"Notifications for Android TV\". \n\n\u0414\u043b\u044f Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\n\u0414\u043b\u044f Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\n\u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0440\u0435\u0437\u0435\u0440\u0432\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 DHCP \u043d\u0430 \u0432\u0430\u0448\u0435\u043c \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0435 (\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0440\u0443\u043a\u043e\u0432\u043e\u0434\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0443) \u0438\u043b\u0438 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435. \u0412 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043c\u043e\u0436\u0435\u0442 \u0441\u0442\u0430\u0442\u044c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c.", + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439, \u0447\u0442\u043e\u0431\u044b \u0443\u0431\u0435\u0434\u0438\u0442\u044c\u0441\u044f, \u0447\u0442\u043e \u0432\u0441\u0435 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u043e\u0431\u043b\u044e\u0434\u0435\u043d\u044b.", "title": "Notifications for Android TV / Fire TV" } } diff --git a/homeassistant/components/nfandroidtv/translations/tr.json b/homeassistant/components/nfandroidtv/translations/tr.json index b894cf83687..835e61eea66 100644 --- a/homeassistant/components/nfandroidtv/translations/tr.json +++ b/homeassistant/components/nfandroidtv/translations/tr.json @@ -13,7 +13,7 @@ "host": "Sunucu", "name": "Ad" }, - "description": "Bu entegrasyon, Android TV i\u00e7in Bildirimler uygulamas\u0131n\u0131 gerektirir. \n\n Android TV i\u00e7in: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\n Fire TV i\u00e7in: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\n Y\u00f6nlendiricinizde DHCP rezervasyonu (y\u00f6nlendiricinizin kullan\u0131m k\u0131lavuzuna bak\u0131n) veya cihazda statik bir IP adresi ayarlamal\u0131s\u0131n\u0131z. Aksi takdirde, cihaz sonunda kullan\u0131lamaz hale gelecektir.", + "description": "T\u00fcm gereksinimlerin kar\u015f\u0131land\u0131\u011f\u0131ndan emin olmak i\u00e7in l\u00fctfen belgelere bak\u0131n.", "title": "Android TV / Fire TV i\u00e7in Bildirimler" } } diff --git a/homeassistant/components/nfandroidtv/translations/zh-Hant.json b/homeassistant/components/nfandroidtv/translations/zh-Hant.json index b16d55a44bd..755ccdfeec8 100644 --- a/homeassistant/components/nfandroidtv/translations/zh-Hant.json +++ b/homeassistant/components/nfandroidtv/translations/zh-Hant.json @@ -13,7 +13,7 @@ "host": "\u4e3b\u6a5f\u7aef", "name": "\u540d\u7a31" }, - "description": "\u6b64\u6574\u5408\u9700\u8981\u5b89\u88dd Notifications for Android TV App\u3002\n\nAndroid TV \u7248\u672c\uff1ahttps://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV \u7248\u672c\uff1ahttps://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\n\u8acb\u65bc\u8def\u7531\u5668\uff08\u8acb\u53c3\u8003\u8def\u7531\u5668\u624b\u518a\uff09\u4e2d\u8a2d\u5b9a\u4fdd\u7559\u88dd\u7f6e DHCP IP \u6216\u975c\u614b IP\u3002\u5047\u5982\u672a\u9032\u884c\u6b64\u8a2d\u5b9a\uff0c\u88dd\u7f6e\u53ef\u80fd\u6703\u8b8a\u6210\u4e0d\u53ef\u7528\u3002", + "description": "\u8acb\u53c3\u8003\u76f8\u95dc\u6587\u4ef6\u4ee5\u4e86\u89e3\u6240\u6709\u5fc5\u8981\u9700\u6c42\u3002", "title": "Android TV / Fire TV \u901a\u77e5" } } diff --git a/homeassistant/components/nina/translations/zh-Hant.json b/homeassistant/components/nina/translations/zh-Hant.json index 0ba4436722d..212c65070d8 100644 --- a/homeassistant/components/nina/translations/zh-Hant.json +++ b/homeassistant/components/nina/translations/zh-Hant.json @@ -5,22 +5,22 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "no_selection": "\u8acb\u81f3\u5c11\u9078\u64c7\u4e00\u500b\u57ce\u5e02/\u570b\u5bb6", + "no_selection": "\u8acb\u81f3\u5c11\u9078\u64c7\u4e00\u500b\u57ce\u5e02/\u7e23\u5e02", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { "user": { "data": { - "_a_to_d": "\u57ce\u5e02/\u570b\u5bb6\uff08A-D\uff09", - "_e_to_h": "\u57ce\u5e02/\u570b\u5bb6\uff08E-H\uff09", - "_i_to_l": "\u57ce\u5e02/\u570b\u5bb6\uff08I-L\uff09", - "_m_to_q": "\u57ce\u5e02/\u570b\u5bb6\uff08M-Q\uff09", - "_r_to_u": "\u57ce\u5e02/\u570b\u5bb6\uff08R-U\uff09", - "_v_to_z": "\u57ce\u5e02/\u570b\u5bb6\uff08V-Z\uff09", + "_a_to_d": "\u57ce\u5e02/\u7e23\u5e02\uff08A-D\uff09", + "_e_to_h": "\u57ce\u5e02/\u7e23\u5e02\uff08E-H\uff09", + "_i_to_l": "\u57ce\u5e02/\u7e23\u5e02\uff08I-L\uff09", + "_m_to_q": "\u57ce\u5e02/\u7e23\u5e02\uff08M-Q\uff09", + "_r_to_u": "\u57ce\u5e02/\u7e23\u5e02\uff08R-U\uff09", + "_v_to_z": "\u57ce\u5e02/\u7e23\u5e02\uff08V-Z\uff09", "corona_filter": "\u79fb\u9664 Corona \u8b66\u544a", - "slots": "\u6bcf\u500b\u57ce\u5e02/\u570b\u5bb6\u6700\u5927\u8b66\u544a\u503c" + "slots": "\u6bcf\u500b\u57ce\u5e02/\u7e23\u5e02\u6700\u5927\u8b66\u544a\u503c" }, - "title": "\u9078\u64c7\u57ce\u5e02/\u570b\u5bb6" + "title": "\u9078\u64c7\u57ce\u5e02/\u7e23\u5e02" } } } diff --git a/homeassistant/components/nmap_tracker/translations/en.json b/homeassistant/components/nmap_tracker/translations/en.json index ae4175e0f14..ee615b87e91 100644 --- a/homeassistant/components/nmap_tracker/translations/en.json +++ b/homeassistant/components/nmap_tracker/translations/en.json @@ -30,7 +30,8 @@ "home_interval": "Minimum number of minutes between scans of active devices (preserve battery)", "hosts": "Network addresses (comma separated) to scan", "interval_seconds": "Scan interval", - "scan_options": "Raw configurable scan options for Nmap" + "scan_options": "Raw configurable scan options for Nmap", + "track_new_devices": "Track new devices" }, "description": "Configure hosts to be scanned by Nmap. Network address and excludes can be IP Addresses (192.168.1.1), IP Networks (192.168.0.0/24) or IP Ranges (192.168.1.0-32)." } diff --git a/homeassistant/components/nmap_tracker/translations/et.json b/homeassistant/components/nmap_tracker/translations/et.json index 538f1127448..ac23cb7004f 100644 --- a/homeassistant/components/nmap_tracker/translations/et.json +++ b/homeassistant/components/nmap_tracker/translations/et.json @@ -9,9 +9,9 @@ "step": { "user": { "data": { - "exclude": "V\u00f5rguaadressid (komadega eraldatud), mis tuleb skaneerimisest v\u00e4lja j\u00e4tta", + "exclude": "V\u00f5rguaadressid (komadega eraldatud) mis tuleb skaneerimisest v\u00e4lja j\u00e4tta", "home_interval": "Minimaalne minutite arv aktiivsete seadmete skaneerimise vahel (aku s\u00e4ilitamine)", - "hosts": "Skaneeritavad v\u00f5rgu aadressid (komadega eraldatud)", + "hosts": "V\u00f5rgu aadressid (komadega eraldatud)", "scan_options": "Nmapi algseadistavad skaneerimisvalikud" }, "description": "Konfigureeri hostid, mida Nmap skannib. V\u00f5rguaadress ja v\u00e4ljaj\u00e4etud v\u00f5ivad olla IP-aadressid (192.168.1.1), IP-v\u00f5rgud (192.168.0.0/24) v\u00f5i IP-vahemikud (192.168.1.0-32)." diff --git a/homeassistant/components/nmap_tracker/translations/hu.json b/homeassistant/components/nmap_tracker/translations/hu.json index 7385f12b3df..f54cf208e92 100644 --- a/homeassistant/components/nmap_tracker/translations/hu.json +++ b/homeassistant/components/nmap_tracker/translations/hu.json @@ -9,9 +9,9 @@ "step": { "user": { "data": { - "exclude": "H\u00e1l\u00f3zati c\u00edmek (vessz\u0151vel elv\u00e1lasztva), amelyeket kiz\u00e1r\u00e1sra ker\u00fclnek a vizsg\u00e1latb\u00f3l", + "exclude": "H\u00e1l\u00f3zati c\u00edmek (vessz\u0151vel elv\u00e1lasztva), amelyeket kiz\u00e1r\u00e1sra ker\u00fclnek a keres\u00e9sb\u0151l", "home_interval": "Minim\u00e1lis percsz\u00e1m az akt\u00edv eszk\u00f6z\u00f6k vizsg\u00e1lata k\u00f6z\u00f6tt (akkumul\u00e1tor k\u00edm\u00e9l\u00e9se)", - "hosts": "H\u00e1l\u00f3zati c\u00edmek (vessz\u0151vel elv\u00e1lasztva) a beolvas\u00e1shoz", + "hosts": "H\u00e1l\u00f3zati c\u00edmek (vessz\u0151vel elv\u00e1lasztva) a keres\u00e9shez", "scan_options": "Nyersen konfigur\u00e1lhat\u00f3 szkennel\u00e9si lehet\u0151s\u00e9gek az Nmap sz\u00e1m\u00e1ra" }, "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/no.json b/homeassistant/components/nmap_tracker/translations/no.json index acd25607c4f..1de32bed121 100644 --- a/homeassistant/components/nmap_tracker/translations/no.json +++ b/homeassistant/components/nmap_tracker/translations/no.json @@ -11,7 +11,7 @@ "data": { "exclude": "Nettverksadresser (kommaseparert) for \u00e5 ekskludere fra skanning", "home_interval": "Minimum antall minutter mellom skanninger av aktive enheter (lagre batteri)", - "hosts": "Nettverksadresser (kommaseparert) for \u00e5 skanne", + "hosts": "Nettverksadresser (atskilt med komma) som skal skannes", "scan_options": "R\u00e5 konfigurerbare skannealternativer for Nmap" }, "description": "Konfigurer verter som skal skannes av Nmap. Nettverksadresse og ekskluderer kan v\u00e6re IP-adresser (192.168.1.1), IP-nettverk (192.168.0.0/24) eller IP-omr\u00e5der (192.168.1.0-32)." @@ -28,7 +28,7 @@ "consider_home": "Sekunder \u00e5 vente til du merker en enhetssporing som ikke hjemme etter at den ikke er blitt sett.", "exclude": "Nettverksadresser (kommaseparert) for \u00e5 ekskludere fra skanning", "home_interval": "Minimum antall minutter mellom skanninger av aktive enheter (lagre batteri)", - "hosts": "Nettverksadresser (kommaseparert) for \u00e5 skanne", + "hosts": "Nettverksadresser (atskilt med komma) som skal skannes", "interval_seconds": "Skanneintervall", "scan_options": "R\u00e5 konfigurerbare skannealternativer for Nmap", "track_new_devices": "Spor nye enheter" diff --git a/homeassistant/components/nmap_tracker/translations/pt-BR.json b/homeassistant/components/nmap_tracker/translations/pt-BR.json index bf058495cb9..a80213ce5d6 100644 --- a/homeassistant/components/nmap_tracker/translations/pt-BR.json +++ b/homeassistant/components/nmap_tracker/translations/pt-BR.json @@ -9,9 +9,9 @@ "step": { "user": { "data": { - "exclude": "Endere\u00e7os de rede (separados por v\u00edrgula) para excluir do escaneamento", + "exclude": "Endere\u00e7os de rede (separados por v\u00edrgula) para excluir da verifica\u00e7\u00e3o", "home_interval": "N\u00famero m\u00ednimo de minutos entre escaneamento de dispositivos ativos (preservar bateria)", - "hosts": "Endere\u00e7os de rede (separados por v\u00edrgula) para escanear", + "hosts": "Endere\u00e7os de rede (separados por v\u00edrgula) para digitalizar", "scan_options": "Op\u00e7\u00f5es de escaneamento bruto configur\u00e1veis para Nmap" }, "description": "Configure os hosts a serem verificados pelo Nmap. O endere\u00e7o de rede e as exclus\u00f5es podem ser endere\u00e7os IP (192.168.1.1), redes IP (192.168.0.0/24) ou intervalos de IP (192.168.1.0-32)." @@ -26,14 +26,14 @@ "init": { "data": { "consider_home": "Segundos para esperar at\u00e9 marcar um rastreador de dispositivo como fora de casa depois de n\u00e3o ser visto.", - "exclude": "Endere\u00e7os de rede (separados por v\u00edrgula) para excluir do escaneamento", + "exclude": "Endere\u00e7os de rede (separados por v\u00edrgula) para excluir da verifica\u00e7\u00e3o", "home_interval": "N\u00famero m\u00ednimo de minutos entre escaneamento de dispositivos ativos (preservar bateria)", - "hosts": "Endere\u00e7os de rede (separados por v\u00edrgula) para escanear", + "hosts": "Endere\u00e7os de rede (separados por v\u00edrgula) para digitalizar", "interval_seconds": "Intervalo de varredura", - "scan_options": "Op\u00e7\u00f5es de varredura configur\u00e1veis brutas para Nmap", + "scan_options": "Op\u00e7\u00f5es de escaneamento bruto configur\u00e1veis para Nmap", "track_new_devices": "Rastrear novos dispositivos" }, - "description": "Configure hosts a serem digitalizados pelo Nmap. O endere\u00e7o de rede e exclus\u00f5es podem ser Endere\u00e7os IP (192.168.1.1), Redes IP (192.168.0.0/24) ou Faixas IP (192.168.1.0-32)." + "description": "Configure os hosts a serem verificados pelo Nmap. O endere\u00e7o de rede e as exclus\u00f5es podem ser endere\u00e7os IP (192.168.1.1), redes IP (192.168.0.0/24) ou intervalos de IP (192.168.1.0-32)." } } }, diff --git a/homeassistant/components/nmap_tracker/translations/tr.json b/homeassistant/components/nmap_tracker/translations/tr.json index 2d277e979b3..8e5dadb8100 100644 --- a/homeassistant/components/nmap_tracker/translations/tr.json +++ b/homeassistant/components/nmap_tracker/translations/tr.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "exclude": "Taraman\u0131n d\u0131\u015f\u0131nda tutulacak a\u011f adresleri (virg\u00fcl ayr\u0131lm\u0131\u015f)", + "exclude": "Tarama d\u0131\u015f\u0131nda b\u0131rak\u0131lacak a\u011f adresleri (virg\u00fclle ayr\u0131lm\u0131\u015f)", "home_interval": "Aktif cihazlar\u0131n taranmas\u0131 aras\u0131ndaki minimum dakika say\u0131s\u0131 (pilden tasarruf edin)", "hosts": "Taranacak a\u011f adresleri (virg\u00fclle ayr\u0131lm\u0131\u015f)", "scan_options": "Nmap i\u00e7in ham yap\u0131land\u0131r\u0131labilir tarama se\u00e7enekleri" @@ -26,7 +26,7 @@ "init": { "data": { "consider_home": "Bir cihaz izleyicisi g\u00f6r\u00fclmedikten sonra evde de\u011fil olarak i\u015faretlenene kadar beklemek i\u00e7in saniyeler kald\u0131.", - "exclude": "Taraman\u0131n d\u0131\u015f\u0131nda tutulacak a\u011f adresleri (virg\u00fcl ayr\u0131lm\u0131\u015f)", + "exclude": "Tarama d\u0131\u015f\u0131nda b\u0131rak\u0131lacak a\u011f adresleri (virg\u00fclle ayr\u0131lm\u0131\u015f)", "home_interval": "Aktif cihazlar\u0131n taranmas\u0131 aras\u0131ndaki minimum dakika say\u0131s\u0131 (pilden tasarruf edin)", "hosts": "Taranacak a\u011f adresleri (virg\u00fclle ayr\u0131lm\u0131\u015f)", "interval_seconds": "Tarama aral\u0131\u011f\u0131", diff --git a/homeassistant/components/nzbget/translations/hu.json b/homeassistant/components/nzbget/translations/hu.json index 6db44f83c28..928d054707b 100644 --- a/homeassistant/components/nzbget/translations/hu.json +++ b/homeassistant/components/nzbget/translations/hu.json @@ -12,7 +12,7 @@ "user": { "data": { "host": "C\u00edm", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3", "port": "Port", "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", diff --git a/homeassistant/components/ondilo_ico/translations/hu.json b/homeassistant/components/ondilo_ico/translations/hu.json index a6979721779..91e05b319b6 100644 --- a/homeassistant/components/ondilo_ico/translations/hu.json +++ b/homeassistant/components/ondilo_ico/translations/hu.json @@ -9,7 +9,7 @@ }, "step": { "pick_implementation": { - "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" } } } diff --git a/homeassistant/components/ondilo_ico/translations/it.json b/homeassistant/components/ondilo_ico/translations/it.json index 42536508716..f1e5fe58fb1 100644 --- a/homeassistant/components/ondilo_ico/translations/it.json +++ b/homeassistant/components/ondilo_ico/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", - "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione." + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione." }, "create_entry": { "default": "Autenticazione riuscita" diff --git a/homeassistant/components/onewire/translations/es.json b/homeassistant/components/onewire/translations/es.json index a2d8da218ba..d789e46875f 100644 --- a/homeassistant/components/onewire/translations/es.json +++ b/homeassistant/components/onewire/translations/es.json @@ -22,5 +22,10 @@ "title": "Configurar 1 cable" } } + }, + "options": { + "error": { + "device_not_selected": "Seleccionar los dispositivos a configurar" + } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/fr.json b/homeassistant/components/onewire/translations/fr.json index 0c75fb23ad1..08e3d3cf18b 100644 --- a/homeassistant/components/onewire/translations/fr.json +++ b/homeassistant/components/onewire/translations/fr.json @@ -29,7 +29,8 @@ }, "step": { "ack_no_options": { - "description": "Il n'y a pas d'option pour l'impl\u00e9mentation de SysBus" + "description": "Il n'y a pas d'option pour l'impl\u00e9mentation de SysBus", + "title": "Options de bus syst\u00e8me OneWire" }, "configure_device": { "data": { diff --git a/homeassistant/components/onewire/translations/nl.json b/homeassistant/components/onewire/translations/nl.json index cc310900d92..3ea1fd8e13c 100644 --- a/homeassistant/components/onewire/translations/nl.json +++ b/homeassistant/components/onewire/translations/nl.json @@ -29,6 +29,10 @@ }, "step": { "ack_no_options": { + "data": { + "one": "Leeg", + "other": "Ander" + }, "description": "Er zijn geen opties voor de SysBus implementatie", "title": "OneWire SysBus opties" }, diff --git a/homeassistant/components/onvif/translations/hu.json b/homeassistant/components/onvif/translations/hu.json index 6b777b76ab0..3754243a740 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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "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." @@ -21,7 +21,7 @@ "configure": { "data": { "host": "C\u00edm", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" @@ -44,7 +44,7 @@ "manual_input": { "data": { "host": "C\u00edm", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "port": "Port" }, "title": "ONVIF eszk\u00f6z konfigur\u00e1l\u00e1sa" @@ -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\u00f6vetkez\u0151 l\u00e9p\u00e9sben 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/open_meteo/translations/ca.json b/homeassistant/components/open_meteo/translations/ca.json index 4fb1a502b6f..a401eeaa99e 100644 --- a/homeassistant/components/open_meteo/translations/ca.json +++ b/homeassistant/components/open_meteo/translations/ca.json @@ -5,7 +5,7 @@ "data": { "zone": "Zona" }, - "description": "Selecciona la ubicaci\u00f3 que s'utilitzar\u00e0 per a la predicci\u00f3 meteorol\u00f2gica" + "description": "Selecciona la ubicaci\u00f3 que s'utilitzar\u00e0 per a la previsi\u00f3 meteorol\u00f2gica" } } } diff --git a/homeassistant/components/opentherm_gw/translations/hu.json b/homeassistant/components/opentherm_gw/translations/hu.json index 3127dc523ce..e2a284d7dd1 100644 --- a/homeassistant/components/opentherm_gw/translations/hu.json +++ b/homeassistant/components/opentherm_gw/translations/hu.json @@ -10,7 +10,7 @@ "data": { "device": "El\u00e9r\u00e9si \u00fat vagy URL", "id": "ID", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" }, "title": "OpenTherm \u00e1tj\u00e1r\u00f3" } diff --git a/homeassistant/components/openweathermap/translations/ca.json b/homeassistant/components/openweathermap/translations/ca.json index b3b1ebbb85e..f304a8d4f9f 100644 --- a/homeassistant/components/openweathermap/translations/ca.json +++ b/homeassistant/components/openweathermap/translations/ca.json @@ -15,9 +15,9 @@ "latitude": "Latitud", "longitude": "Longitud", "mode": "Mode", - "name": "Nom de la integraci\u00f3" + "name": "Nom" }, - "description": "Configura la integraci\u00f3 OpenWeatherMap. Per generar la clau API, ves a https://openweathermap.org/appid", + "description": "Per generar la clau API, v\u00e9s a https://openweathermap.org/appid", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/openweathermap/translations/de.json b/homeassistant/components/openweathermap/translations/de.json index 615a642a859..f74065ca1a1 100644 --- a/homeassistant/components/openweathermap/translations/de.json +++ b/homeassistant/components/openweathermap/translations/de.json @@ -15,9 +15,9 @@ "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad", "mode": "Modus", - "name": "Name der Integration" + "name": "Name" }, - "description": "Richte die OpenWeatherMap-Integration ein. Zum Generieren des API-Schl\u00fcssels gehe auf https://openweathermap.org/appid", + "description": "Um den API-Schl\u00fcssel zu generieren, besuche https://openweathermap.org/appid", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/openweathermap/translations/en.json b/homeassistant/components/openweathermap/translations/en.json index 57feaccf6b4..55f8d0a2338 100644 --- a/homeassistant/components/openweathermap/translations/en.json +++ b/homeassistant/components/openweathermap/translations/en.json @@ -15,9 +15,9 @@ "latitude": "Latitude", "longitude": "Longitude", "mode": "Mode", - "name": "Name of the integration" + "name": "Name" }, - "description": "Set up OpenWeatherMap integration. To generate API key go to https://openweathermap.org/appid", + "description": "To generate API key go to https://openweathermap.org/appid", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/openweathermap/translations/et.json b/homeassistant/components/openweathermap/translations/et.json index e548c07236e..0d991c99e3f 100644 --- a/homeassistant/components/openweathermap/translations/et.json +++ b/homeassistant/components/openweathermap/translations/et.json @@ -15,9 +15,9 @@ "latitude": "Laiuskraad", "longitude": "Pikkuskraad", "mode": "Re\u017eiim", - "name": "Sidumise nimi" + "name": "Nimi" }, - "description": "Seadista OpenWeatherMapi sidumine. API-v\u00f5tme loomiseks mine aadressile https://openweathermap.org/appid", + "description": "API-v\u00f5tme loomiseks mine aadressile https://openweathermap.org/appid", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/openweathermap/translations/fr.json b/homeassistant/components/openweathermap/translations/fr.json index efa76d4d7e4..387c3eefb28 100644 --- a/homeassistant/components/openweathermap/translations/fr.json +++ b/homeassistant/components/openweathermap/translations/fr.json @@ -15,9 +15,9 @@ "latitude": "Latitude", "longitude": "Longitude", "mode": "Mode", - "name": "Nom de l'int\u00e9gration" + "name": "Nom" }, - "description": "Configurez l'int\u00e9gration OpenWeatherMap. Pour g\u00e9n\u00e9rer la cl\u00e9 API, acc\u00e9dez \u00e0 https://openweathermap.org/appid", + "description": "Pour g\u00e9n\u00e9rer une cl\u00e9 d'API, rendez-vous sur https://openweathermap.org/appid", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/openweathermap/translations/he.json b/homeassistant/components/openweathermap/translations/he.json index ca5e388ea98..cadadeb1865 100644 --- a/homeassistant/components/openweathermap/translations/he.json +++ b/homeassistant/components/openweathermap/translations/he.json @@ -15,7 +15,7 @@ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", "mode": "\u05de\u05e6\u05d1", - "name": "\u05e9\u05dd \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1" + "name": "\u05e9\u05dd" }, "title": "\u05de\u05e4\u05ea OpenWeather" } diff --git a/homeassistant/components/openweathermap/translations/hu.json b/homeassistant/components/openweathermap/translations/hu.json index 99932ff5c68..f5a7dade833 100644 --- a/homeassistant/components/openweathermap/translations/hu.json +++ b/homeassistant/components/openweathermap/translations/hu.json @@ -15,9 +15,9 @@ "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g", "mode": "M\u00f3d", - "name": "Az integr\u00e1ci\u00f3 neve" + "name": "Elnevez\u00e9s" }, - "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", + "description": "Az API-kulcs l\u00e9trehoz\u00e1s\u00e1hoz keresse fel a https://openweathermap.org/appid webhelyet", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/openweathermap/translations/id.json b/homeassistant/components/openweathermap/translations/id.json index 61d2713f42a..d8ea0344cea 100644 --- a/homeassistant/components/openweathermap/translations/id.json +++ b/homeassistant/components/openweathermap/translations/id.json @@ -15,9 +15,9 @@ "latitude": "Lintang", "longitude": "Bujur", "mode": "Mode", - "name": "Nama integrasi" + "name": "Nama" }, - "description": "Siapkan integrasi OpenWeatherMap. Untuk membuat kunci API, buka https://openweathermap.org/appid", + "description": "Untuk membuat kunci API, buka https://openweathermap.org/appid", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/openweathermap/translations/it.json b/homeassistant/components/openweathermap/translations/it.json index ffe49a466dc..133fa704109 100644 --- a/homeassistant/components/openweathermap/translations/it.json +++ b/homeassistant/components/openweathermap/translations/it.json @@ -15,9 +15,9 @@ "latitude": "Latitudine", "longitude": "Logitudine", "mode": "Modalit\u00e0", - "name": "Nome dell'integrazione" + "name": "Nome" }, - "description": "Configura l'integrazione di OpenWeatherMap. Per generare la chiave API, vai su https://openweathermap.org/appid", + "description": "Per generare la chiave API, vai su https://openweathermap.org/appid", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/openweathermap/translations/nl.json b/homeassistant/components/openweathermap/translations/nl.json index 04f3c503353..1c72f10c6d9 100644 --- a/homeassistant/components/openweathermap/translations/nl.json +++ b/homeassistant/components/openweathermap/translations/nl.json @@ -15,9 +15,9 @@ "latitude": "Breedtegraad", "longitude": "Lengtegraad", "mode": "Mode", - "name": "Naam van de integratie" + "name": "Naam" }, - "description": "Stel OpenWeatherMap-integratie in. Ga naar https://openweathermap.org/appid om een API-sleutel te genereren", + "description": "Om een API sleutel te genereren ga naar https://openweathermap.org/appid", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/openweathermap/translations/no.json b/homeassistant/components/openweathermap/translations/no.json index 7982628fe62..d751553465a 100644 --- a/homeassistant/components/openweathermap/translations/no.json +++ b/homeassistant/components/openweathermap/translations/no.json @@ -15,9 +15,9 @@ "latitude": "Breddegrad", "longitude": "Lengdegrad", "mode": "Modus", - "name": "Navn p\u00e5 integrasjon" + "name": "Navn" }, - "description": "Sett opp OpenWeatherMap-integrasjon. For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://openweathermap.org/appid", + "description": "For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://openweathermap.org/appid", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/openweathermap/translations/pl.json b/homeassistant/components/openweathermap/translations/pl.json index b3f31831f09..de8d857f168 100644 --- a/homeassistant/components/openweathermap/translations/pl.json +++ b/homeassistant/components/openweathermap/translations/pl.json @@ -15,9 +15,9 @@ "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "mode": "Tryb", - "name": "Nazwa integracji" + "name": "Nazwa" }, - "description": "Konfiguracja integracji OpenWeatherMap. Aby wygenerowa\u0107 klucz API, przejd\u017a do https://openweathermap.org/appid", + "description": "Aby wygenerowa\u0107 klucz API, przejd\u017a do https://openweathermap.org/appid", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/openweathermap/translations/pt-BR.json b/homeassistant/components/openweathermap/translations/pt-BR.json index dd88767bb61..795db96f36f 100644 --- a/homeassistant/components/openweathermap/translations/pt-BR.json +++ b/homeassistant/components/openweathermap/translations/pt-BR.json @@ -15,9 +15,9 @@ "latitude": "Latitude", "longitude": "Longitude", "mode": "Modo", - "name": "Nome da integra\u00e7\u00e3o" + "name": "Nome" }, - "description": "Configure a integra\u00e7\u00e3o do OpenWeatherMap. Para gerar a chave de API, acesse https://openweathermap.org/appid", + "description": "Para gerar a chave de API, acesse https://openweathermap.org/appid", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/openweathermap/translations/ru.json b/homeassistant/components/openweathermap/translations/ru.json index 1a22b38d546..a0724e90f01 100644 --- a/homeassistant/components/openweathermap/translations/ru.json +++ b/homeassistant/components/openweathermap/translations/ru.json @@ -17,7 +17,7 @@ "mode": "\u0420\u0435\u0436\u0438\u043c", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 OpenWeatherMap. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 https://openweathermap.org/appid.", + "description": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 https://openweathermap.org/appid.", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/openweathermap/translations/tr.json b/homeassistant/components/openweathermap/translations/tr.json index 83109869e39..6458852712e 100644 --- a/homeassistant/components/openweathermap/translations/tr.json +++ b/homeassistant/components/openweathermap/translations/tr.json @@ -15,9 +15,9 @@ "latitude": "Enlem", "longitude": "Boylam", "mode": "Mod", - "name": "Cihaz\u0131n ad\u0131" + "name": "Ad" }, - "description": "OpenWeatherMap entegrasyonunu ayarlay\u0131n. API anahtar\u0131 olu\u015fturmak i\u00e7in https://openweathermap.org/appid adresine gidin.", + "description": "API anahtar\u0131 olu\u015fturmak i\u00e7in https://openweathermap.org/appid adresine gidin.", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/openweathermap/translations/zh-Hant.json b/homeassistant/components/openweathermap/translations/zh-Hant.json index 653fb373af3..fcebc12fe31 100644 --- a/homeassistant/components/openweathermap/translations/zh-Hant.json +++ b/homeassistant/components/openweathermap/translations/zh-Hant.json @@ -15,9 +15,9 @@ "latitude": "\u7def\u5ea6", "longitude": "\u7d93\u5ea6", "mode": "\u6a21\u5f0f", - "name": "\u6574\u5408\u540d\u7a31" + "name": "\u540d\u7a31" }, - "description": "\u6b32\u8a2d\u5b9a OpenWeatherMap \u6574\u5408\u3002\u8acb\u81f3 https://openweathermap.org/appid \u7522\u751f API \u91d1\u9470", + "description": "\u8acb\u81f3 https://openweathermap.org/appid \u4ee5\u7522\u751f API \u91d1\u9470", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/overkiz/translations/ca.json b/homeassistant/components/overkiz/translations/ca.json index 1e1bccd1fb7..d3a5a38edc3 100644 --- a/homeassistant/components/overkiz/translations/ca.json +++ b/homeassistant/components/overkiz/translations/ca.json @@ -9,6 +9,7 @@ "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "server_in_maintenance": "El servidor est\u00e0 inoperatiu per manteniment", + "too_many_attempts": "Massa intents amb un 'token' inv\u00e0lid, bloquejat temporalment", "too_many_requests": "Massa sol\u00b7licituds, torna-ho a provar m\u00e9s tard", "unknown": "Error inesperat" }, diff --git a/homeassistant/components/overkiz/translations/de.json b/homeassistant/components/overkiz/translations/de.json index 84d09f9116b..09cce8ea63f 100644 --- a/homeassistant/components/overkiz/translations/de.json +++ b/homeassistant/components/overkiz/translations/de.json @@ -9,6 +9,7 @@ "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "server_in_maintenance": "Server ist wegen Wartungsarbeiten au\u00dfer Betrieb", + "too_many_attempts": "Zu viele Versuche mit einem ung\u00fcltigen Token, vor\u00fcbergehend gesperrt", "too_many_requests": "Zu viele Anfragen, versuche es sp\u00e4ter erneut.", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/overkiz/translations/el.json b/homeassistant/components/overkiz/translations/el.json index 9f7ad60cb09..924ecc3219d 100644 --- a/homeassistant/components/overkiz/translations/el.json +++ b/homeassistant/components/overkiz/translations/el.json @@ -9,6 +9,7 @@ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "server_in_maintenance": "\u039f \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c3\u03c5\u03bd\u03c4\u03ae\u03c1\u03b7\u03c3\u03b7", + "too_many_attempts": "\u03a0\u03ac\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03ad\u03c2 \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b5\u03c2 \u03bc\u03b5 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc, \u03c0\u03c1\u03bf\u03c3\u03c9\u03c1\u03b9\u03bd\u03ac \u03b1\u03c0\u03bf\u03ba\u03bb\u03b5\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2", "too_many_requests": "\u03a0\u03ac\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03ac \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1, \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, diff --git a/homeassistant/components/overkiz/translations/et.json b/homeassistant/components/overkiz/translations/et.json index c19d9c39ca9..3cbfcb6af80 100644 --- a/homeassistant/components/overkiz/translations/et.json +++ b/homeassistant/components/overkiz/translations/et.json @@ -9,6 +9,7 @@ "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus", "server_in_maintenance": "Server on hoolduse t\u00f5ttu maas", + "too_many_attempts": "Liiga palju katseid kehtetu v\u00f5tmega, ajutiselt keelatud", "too_many_requests": "Liiga palju p\u00e4ringuid, proovi hiljem uuesti", "unknown": "Ootamatu t\u00f5rge" }, diff --git a/homeassistant/components/overkiz/translations/fr.json b/homeassistant/components/overkiz/translations/fr.json index f6a56db80a5..c919301d541 100644 --- a/homeassistant/components/overkiz/translations/fr.json +++ b/homeassistant/components/overkiz/translations/fr.json @@ -9,6 +9,7 @@ "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification non valide", "server_in_maintenance": "Le serveur est ferm\u00e9 pour maintenance", + "too_many_attempts": "Trop de tentatives avec un jeton non valide\u00a0: banni temporairement", "too_many_requests": "Trop de demandes, r\u00e9essayez plus tard.", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/overkiz/translations/hu.json b/homeassistant/components/overkiz/translations/hu.json index ef7cacaaf96..b6810749bd3 100644 --- a/homeassistant/components/overkiz/translations/hu.json +++ b/homeassistant/components/overkiz/translations/hu.json @@ -9,6 +9,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "server_in_maintenance": "A szerver karbantart\u00e1s miatt nem el\u00e9rhet\u0151", + "too_many_attempts": "T\u00fal sok pr\u00f3b\u00e1lkoz\u00e1s \u00e9rv\u00e9nytelen tokennel, ideiglenesen kitiltva", "too_many_requests": "T\u00fal sok a k\u00e9r\u00e9s, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/overkiz/translations/id.json b/homeassistant/components/overkiz/translations/id.json index becbae65f9e..c58b66b10a7 100644 --- a/homeassistant/components/overkiz/translations/id.json +++ b/homeassistant/components/overkiz/translations/id.json @@ -9,6 +9,7 @@ "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", "server_in_maintenance": "Server sedang dalam masa pemeliharaan", + "too_many_attempts": "Terlalu banyak percobaan dengan token yang tidak valid, untuk sementara diblokir", "too_many_requests": "Terlalu banyak permintaan, coba lagi nanti.", "unknown": "Kesalahan yang tidak diharapkan" }, diff --git a/homeassistant/components/overkiz/translations/it.json b/homeassistant/components/overkiz/translations/it.json index 00898513f1f..3dcc4e94e10 100644 --- a/homeassistant/components/overkiz/translations/it.json +++ b/homeassistant/components/overkiz/translations/it.json @@ -9,6 +9,7 @@ "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", "server_in_maintenance": "Il server \u00e8 inattivo per manutenzione", + "too_many_attempts": "Troppi tentativi con un token non valido, temporaneamente bandito", "too_many_requests": "Troppe richieste, riprova pi\u00f9 tardi.", "unknown": "Errore imprevisto" }, diff --git a/homeassistant/components/overkiz/translations/ja.json b/homeassistant/components/overkiz/translations/ja.json index e978a8f36f2..357408847fd 100644 --- a/homeassistant/components/overkiz/translations/ja.json +++ b/homeassistant/components/overkiz/translations/ja.json @@ -9,6 +9,7 @@ "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "server_in_maintenance": "\u30e1\u30f3\u30c6\u30ca\u30f3\u30b9\u306e\u305f\u3081\u30b5\u30fc\u30d0\u30fc\u304c\u30c0\u30a6\u30f3\u3057\u3066\u3044\u307e\u3059", + "too_many_attempts": "\u7121\u52b9\u306a\u30c8\u30fc\u30af\u30f3\u306b\u3088\u308b\u8a66\u884c\u56de\u6570\u304c\u591a\u3059\u304e\u305f\u305f\u3081\u3001\u4e00\u6642\u7684\u306b\u7981\u6b62\u3055\u308c\u307e\u3057\u305f\u3002", "too_many_requests": "\u30ea\u30af\u30a8\u30b9\u30c8\u304c\u591a\u3059\u304e\u307e\u3059\u3002\u3057\u3070\u3089\u304f\u3057\u3066\u304b\u3089\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/overkiz/translations/nl.json b/homeassistant/components/overkiz/translations/nl.json index d33dd5bb44c..5057f12afa6 100644 --- a/homeassistant/components/overkiz/translations/nl.json +++ b/homeassistant/components/overkiz/translations/nl.json @@ -9,6 +9,7 @@ "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "server_in_maintenance": "Server is offline wegens onderhoud", + "too_many_attempts": "Te veel pogingen met een ongeldig token, tijdelijk verbannen", "too_many_requests": "Te veel verzoeken, probeer het later opnieuw.", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/overkiz/translations/no.json b/homeassistant/components/overkiz/translations/no.json index ed691aa388f..e0d52e0fc0e 100644 --- a/homeassistant/components/overkiz/translations/no.json +++ b/homeassistant/components/overkiz/translations/no.json @@ -9,6 +9,7 @@ "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", "server_in_maintenance": "serveren er nede for vedlikehold", + "too_many_attempts": "For mange fors\u00f8k med et ugyldig token, midlertidig utestengt", "too_many_requests": "For mange foresp\u00f8rsler. Pr\u00f8v igjen senere", "unknown": "Uventet feil" }, diff --git a/homeassistant/components/overkiz/translations/pl.json b/homeassistant/components/overkiz/translations/pl.json index 6881d7edb5b..7044dc717fd 100644 --- a/homeassistant/components/overkiz/translations/pl.json +++ b/homeassistant/components/overkiz/translations/pl.json @@ -9,6 +9,7 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", "server_in_maintenance": "Serwer wy\u0142\u0105czony z powodu przerwy technicznej", + "too_many_attempts": "Zbyt wiele pr\u00f3b z nieprawid\u0142owym tokenem, konto tymczasowo zablokowane", "too_many_requests": "Zbyt wiele \u017c\u0105da\u0144, spr\u00f3buj ponownie p\u00f3\u017aniej.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, diff --git a/homeassistant/components/overkiz/translations/pt-BR.json b/homeassistant/components/overkiz/translations/pt-BR.json index 2f2c335ebd5..123b2a83d42 100644 --- a/homeassistant/components/overkiz/translations/pt-BR.json +++ b/homeassistant/components/overkiz/translations/pt-BR.json @@ -9,6 +9,7 @@ "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "server_in_maintenance": "O servidor est\u00e1 fora de servi\u00e7o para manuten\u00e7\u00e3o", + "too_many_attempts": "Muitas tentativas com um token inv\u00e1lido, banido temporariamente", "too_many_requests": "Muitas solicita\u00e7\u00f5es, tente novamente mais tarde", "unknown": "Erro inesperado" }, diff --git a/homeassistant/components/overkiz/translations/ru.json b/homeassistant/components/overkiz/translations/ru.json index 611c4784902..53f08e1dbd6 100644 --- a/homeassistant/components/overkiz/translations/ru.json +++ b/homeassistant/components/overkiz/translations/ru.json @@ -9,6 +9,7 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "server_in_maintenance": "\u0421\u0435\u0440\u0432\u0435\u0440 \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u0432 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u043c \u043e\u0431\u0441\u043b\u0443\u0436\u0438\u0432\u0430\u043d\u0438\u0435\u043c.", + "too_many_attempts": "\u0421\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u043f\u043e\u043f\u044b\u0442\u043e\u043a \u0441 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0442\u043e\u043a\u0435\u043d\u043e\u043c, \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e.", "too_many_requests": "\u0421\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/overkiz/translations/tr.json b/homeassistant/components/overkiz/translations/tr.json index 8e6a232db7f..1d04fbbd3cc 100644 --- a/homeassistant/components/overkiz/translations/tr.json +++ b/homeassistant/components/overkiz/translations/tr.json @@ -9,6 +9,7 @@ "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "server_in_maintenance": "Sunucu bak\u0131m nedeniyle kapal\u0131", + "too_many_attempts": "Ge\u00e7ersiz anahtarla \u00e7ok fazla deneme, ge\u00e7ici olarak yasakland\u0131", "too_many_requests": "\u00c7ok fazla istek var, daha sonra tekrar deneyin", "unknown": "Beklenmeyen hata" }, diff --git a/homeassistant/components/overkiz/translations/zh-Hant.json b/homeassistant/components/overkiz/translations/zh-Hant.json index 04c2fc2b07a..ab265301761 100644 --- a/homeassistant/components/overkiz/translations/zh-Hant.json +++ b/homeassistant/components/overkiz/translations/zh-Hant.json @@ -9,6 +9,7 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "server_in_maintenance": "\u4f3a\u670d\u5668\u7dad\u8b77\u4e2d", + "too_many_attempts": "\u4f7f\u7528\u7121\u6548\u6b0a\u6756\u5617\u8a66\u6b21\u6578\u904e\u591a\uff0c\u66ab\u6642\u906d\u5230\u5c01\u9396", "too_many_requests": "\u8acb\u6c42\u6b21\u6578\u904e\u591a\uff0c\u8acb\u7a0d\u5f8c\u91cd\u8a66\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, diff --git a/homeassistant/components/owntracks/translations/cs.json b/homeassistant/components/owntracks/translations/cs.json index d242a1275f1..a564efb190f 100644 --- a/homeassistant/components/owntracks/translations/cs.json +++ b/homeassistant/components/owntracks/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nen\u00ed p\u0159ipojeno k Home Assistant Cloud.", "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." }, "create_entry": { diff --git a/homeassistant/components/p1_monitor/translations/hu.json b/homeassistant/components/p1_monitor/translations/hu.json index f9025022c6d..b0c30613234 100644 --- a/homeassistant/components/p1_monitor/translations/hu.json +++ b/homeassistant/components/p1_monitor/translations/hu.json @@ -8,7 +8,7 @@ "user": { "data": { "host": "C\u00edm", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" }, "description": "\u00c1ll\u00edtsa be a P1 monitort az Otthoni asszisztenssel val\u00f3 integr\u00e1ci\u00f3hoz." } diff --git a/homeassistant/components/panasonic_viera/translations/hu.json b/homeassistant/components/panasonic_viera/translations/hu.json index e373a352a45..7fd4d2524da 100644 --- a/homeassistant/components/panasonic_viera/translations/hu.json +++ b/homeassistant/components/panasonic_viera/translations/hu.json @@ -20,7 +20,7 @@ "user": { "data": { "host": "IP c\u00edm", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" }, "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/peco/translations/bg.json b/homeassistant/components/peco/translations/bg.json new file mode 100644 index 00000000000..80a7cc489a9 --- /dev/null +++ b/homeassistant/components/peco/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/peco/translations/ca.json b/homeassistant/components/peco/translations/ca.json new file mode 100644 index 00000000000..1cca8a08a7f --- /dev/null +++ b/homeassistant/components/peco/translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, + "step": { + "user": { + "data": { + "county": "Comtat" + }, + "description": "Trieu el teu comtat a continuaci\u00f3.", + "title": "Comptador d'interrupcions PECO" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/peco/translations/cs.json b/homeassistant/components/peco/translations/cs.json new file mode 100644 index 00000000000..8440070c91a --- /dev/null +++ b/homeassistant/components/peco/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/peco/translations/de.json b/homeassistant/components/peco/translations/de.json new file mode 100644 index 00000000000..4eb10b3e5e9 --- /dev/null +++ b/homeassistant/components/peco/translations/de.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "county": "Bundesland" + }, + "description": "Bitte w\u00e4hle unten dein Bundesland aus.", + "title": "PECO-St\u00f6rungsz\u00e4hler" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/peco/translations/el.json b/homeassistant/components/peco/translations/el.json new file mode 100644 index 00000000000..6b47cc3ccc1 --- /dev/null +++ b/homeassistant/components/peco/translations/el.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + }, + "step": { + "user": { + "data": { + "county": "\u039a\u03bf\u03bc\u03b7\u03c4\u03b5\u03af\u03b1" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03bf\u03bc\u03b7\u03c4\u03b5\u03af\u03b1 \u03c3\u03b1\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9.", + "title": "\u039c\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae\u03c2 \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03ce\u03bd PECO" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/peco/translations/en.json b/homeassistant/components/peco/translations/en.json index 6f7ff2b0b12..60483a1d65c 100644 --- a/homeassistant/components/peco/translations/en.json +++ b/homeassistant/components/peco/translations/en.json @@ -7,7 +7,9 @@ "user": { "data": { "county": "County" - } + }, + "description": "Please choose your county below.", + "title": "PECO Outage Counter" } } } diff --git a/homeassistant/components/peco/translations/et.json b/homeassistant/components/peco/translations/et.json new file mode 100644 index 00000000000..117d0502ca3 --- /dev/null +++ b/homeassistant/components/peco/translations/et.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud" + }, + "step": { + "user": { + "data": { + "county": "Maakond" + }, + "description": "Vali allpool oma maakond.", + "title": "PECO katkestuste loendur" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/peco/translations/fr.json b/homeassistant/components/peco/translations/fr.json new file mode 100644 index 00000000000..e78f828f1af --- /dev/null +++ b/homeassistant/components/peco/translations/fr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + }, + "step": { + "user": { + "data": { + "county": "Comt\u00e9" + }, + "description": "Veuillez choisir votre comt\u00e9 ci-dessous.", + "title": "Compteur de pannes PECO" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/peco/translations/he.json b/homeassistant/components/peco/translations/he.json new file mode 100644 index 00000000000..48a6eeeea33 --- /dev/null +++ b/homeassistant/components/peco/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/peco/translations/hu.json b/homeassistant/components/peco/translations/hu.json new file mode 100644 index 00000000000..2e1e3482eb5 --- /dev/null +++ b/homeassistant/components/peco/translations/hu.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "user": { + "data": { + "county": "Megye" + }, + "description": "K\u00e9rj\u00fck, v\u00e1lassza ki az al\u00e1bbiakban a megy\u00e9t.", + "title": "PECO kimarad\u00e1s sz\u00e1ml\u00e1l\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/peco/translations/id.json b/homeassistant/components/peco/translations/id.json new file mode 100644 index 00000000000..0879348f593 --- /dev/null +++ b/homeassistant/components/peco/translations/id.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "step": { + "user": { + "data": { + "county": "Kota/kabupaten" + }, + "description": "Pilih kota/kabupaten Anda di bawah ini.", + "title": "Penghitung Pemadaman PECO" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/peco/translations/it.json b/homeassistant/components/peco/translations/it.json new file mode 100644 index 00000000000..a7b76c27f43 --- /dev/null +++ b/homeassistant/components/peco/translations/it.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + }, + "step": { + "user": { + "data": { + "county": "Provincia" + }, + "description": "Scegli la tua provincia qui sotto.", + "title": "Contatore interruzioni PECO" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/peco/translations/ja.json b/homeassistant/components/peco/translations/ja.json new file mode 100644 index 00000000000..139e1f77bd9 --- /dev/null +++ b/homeassistant/components/peco/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "step": { + "user": { + "data": { + "county": "\u90e1" + }, + "description": "\u4ee5\u4e0b\u304b\u3089\u304a\u4f4f\u307e\u3044\u306e\u56fd\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "PECO Outage\u30ab\u30a6\u30f3\u30bf\u30fc" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/peco/translations/nl.json b/homeassistant/components/peco/translations/nl.json new file mode 100644 index 00000000000..8e98f5077e4 --- /dev/null +++ b/homeassistant/components/peco/translations/nl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Service is al geconfigureerd" + }, + "step": { + "user": { + "data": { + "county": "County" + }, + "description": "Kies hieronder uw county", + "title": "PECO Uitval Teller" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/peco/translations/no.json b/homeassistant/components/peco/translations/no.json new file mode 100644 index 00000000000..00f9c025114 --- /dev/null +++ b/homeassistant/components/peco/translations/no.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, + "step": { + "user": { + "data": { + "county": "fylke" + }, + "description": "Velg ditt fylke nedenfor.", + "title": "PECO avbruddsteller" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/peco/translations/pl.json b/homeassistant/components/peco/translations/pl.json new file mode 100644 index 00000000000..bdd8a7ffb7c --- /dev/null +++ b/homeassistant/components/peco/translations/pl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + }, + "step": { + "user": { + "data": { + "county": "Hrabstwo" + }, + "description": "Wybierz swoje hrabstwo poni\u017cej.", + "title": "Licznik awarii PECO" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/peco/translations/pt-BR.json b/homeassistant/components/peco/translations/pt-BR.json new file mode 100644 index 00000000000..845baf5b479 --- /dev/null +++ b/homeassistant/components/peco/translations/pt-BR.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "county": "Munic\u00edpio" + }, + "description": "Por favor, escolha seu munic\u00edpio abaixo.", + "title": "Contador de Interrup\u00e7\u00e3o PECO" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/peco/translations/ru.json b/homeassistant/components/peco/translations/ru.json new file mode 100644 index 00000000000..6c2c8d85cc6 --- /dev/null +++ b/homeassistant/components/peco/translations/ru.json @@ -0,0 +1,16 @@ +{ + "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": { + "county": "\u041e\u043a\u0440\u0443\u0433" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 \u043e\u043a\u0440\u0443\u0433.", + "title": "\u0421\u0447\u0435\u0442\u0447\u0438\u043a \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 PECO" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/peco/translations/tr.json b/homeassistant/components/peco/translations/tr.json new file mode 100644 index 00000000000..6a76e600789 --- /dev/null +++ b/homeassistant/components/peco/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "county": "\u0130l\u00e7e" + }, + "description": "L\u00fctfen a\u015fa\u011f\u0131dan il\u00e7enizi se\u00e7iniz.", + "title": "PECO Kesinti Sayac\u0131" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/peco/translations/zh-Hant.json b/homeassistant/components/peco/translations/zh-Hant.json new file mode 100644 index 00000000000..94318397f6f --- /dev/null +++ b/homeassistant/components/peco/translations/zh-Hant.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "county": "\u7e23\u5e02" + }, + "description": "\u8acb\u9078\u64c7\u7e23\u5e02\u3002", + "title": "PECO Outage \u8a08\u6578\u5668" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/hu.json b/homeassistant/components/pi_hole/translations/hu.json index 71321c4cf85..e55c7d543e3 100644 --- a/homeassistant/components/pi_hole/translations/hu.json +++ b/homeassistant/components/pi_hole/translations/hu.json @@ -17,7 +17,7 @@ "api_key": "API kulcs", "host": "C\u00edm", "location": "Elhelyezked\u00e9s", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "port": "Port", "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", "statistics_only": "Csak statisztik\u00e1k", diff --git a/homeassistant/components/plaato/translations/cs.json b/homeassistant/components/plaato/translations/cs.json index 4d736d1c695..5e3c9682226 100644 --- a/homeassistant/components/plaato/translations/cs.json +++ b/homeassistant/components/plaato/translations/cs.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u00da\u010det je ji\u017e nastaven", + "cloud_not_connected": "Nen\u00ed p\u0159ipojeno k Home Assistant Cloud.", "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace.", "webhook_not_internet_accessible": "V\u00e1\u0161 Home Assistant mus\u00ed b\u00fdt p\u0159\u00edstupn\u00fd z internetu, aby mohl p\u0159ij\u00edmat zpr\u00e1vy webhook." }, diff --git a/homeassistant/components/plant/translations/zh-Hant.json b/homeassistant/components/plant/translations/zh-Hant.json index af06d09eef6..052086d04eb 100644 --- a/homeassistant/components/plant/translations/zh-Hant.json +++ b/homeassistant/components/plant/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "state": { "_": { - "ok": "\u5065\u5eb7", + "ok": "\u6b63\u5e38", "problem": "\u7570\u5e38" } }, diff --git a/homeassistant/components/plex/translations/hu.json b/homeassistant/components/plex/translations/hu.json index d3aabedc58d..3be797cc04f 100644 --- a/homeassistant/components/plex/translations/hu.json +++ b/homeassistant/components/plex/translations/hu.json @@ -3,7 +3,7 @@ "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", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "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/plugwise/translations/ca.json b/homeassistant/components/plugwise/translations/ca.json index c29fa230519..85f7c357803 100644 --- a/homeassistant/components/plugwise/translations/ca.json +++ b/homeassistant/components/plugwise/translations/ca.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_setup": "Afegeix l'Adam en lloc de l'Anna; consulta la documentaci\u00f3 de la integraci\u00f3 Plugwise de Home Assistant per a m\u00e9s informaci\u00f3.", "unknown": "Error inesperat" }, "flow_title": "{name}", diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index a5c11645d6f..f6ce5fb1b2d 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_setup": "F\u00fcge deinen Adam anstelle deiner Anna hinzu. Weitere Informationen findest du in der Dokumentation zur Integration von Home Assistant Plugwise.", "unknown": "Unerwarteter Fehler" }, "flow_title": "{name}", diff --git a/homeassistant/components/plugwise/translations/el.json b/homeassistant/components/plugwise/translations/el.json index a891a74d3ad..8407cb38cb1 100644 --- a/homeassistant/components/plugwise/translations/el.json +++ b/homeassistant/components/plugwise/translations/el.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "invalid_setup": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd Adam \u03c3\u03b1\u03c2 \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd Anna \u03c3\u03b1\u03c2, \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Home Assistant Plugwise \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "flow_title": "{name}", diff --git a/homeassistant/components/plugwise/translations/en.json b/homeassistant/components/plugwise/translations/en.json index 3ee5551fd83..616e450cb11 100644 --- a/homeassistant/components/plugwise/translations/en.json +++ b/homeassistant/components/plugwise/translations/en.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", + "invalid_setup": "Add your Adam instead of your Anna, see the Home Assistant Plugwise integration documentation for more information", "unknown": "Unexpected error" }, "flow_title": "{name}", diff --git a/homeassistant/components/plugwise/translations/et.json b/homeassistant/components/plugwise/translations/et.json index 029b334f7c4..f863dd30b19 100644 --- a/homeassistant/components/plugwise/translations/et.json +++ b/homeassistant/components/plugwise/translations/et.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus", + "invalid_setup": "Lisa oma Anna asemel oma Adam, lisateabe saamiseks vaata Home Assistant Plugwise'i sidumise dokumentatsiooni", "unknown": "Tundmatu viga" }, "flow_title": "{name}", diff --git a/homeassistant/components/plugwise/translations/fr.json b/homeassistant/components/plugwise/translations/fr.json index c6098db43f0..baa6074e68a 100644 --- a/homeassistant/components/plugwise/translations/fr.json +++ b/homeassistant/components/plugwise/translations/fr.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification non valide", + "invalid_setup": "Ajoutez votre Adam au lieu de votre Anna\u00a0; consultez la documentation de l'int\u00e9gration Plugwise de Home Assistant pour plus d'informations", "unknown": "Erreur inattendue" }, "flow_title": "{name}", diff --git a/homeassistant/components/plugwise/translations/hu.json b/homeassistant/components/plugwise/translations/hu.json index 4d588c84277..265a99186ba 100644 --- a/homeassistant/components/plugwise/translations/hu.json +++ b/homeassistant/components/plugwise/translations/hu.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_setup": "Adja hozz\u00e1 Adamot Anna helyett. Tov\u00e1bbi inform\u00e1ci\u00f3\u00e9rt tekintse meg a Home Assistant Plugwise integr\u00e1ci\u00f3s dokument\u00e1ci\u00f3j\u00e1t", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "flow_title": "{name}", diff --git a/homeassistant/components/plugwise/translations/id.json b/homeassistant/components/plugwise/translations/id.json index f3eeb0926ba..8878a4e775d 100644 --- a/homeassistant/components/plugwise/translations/id.json +++ b/homeassistant/components/plugwise/translations/id.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", + "invalid_setup": "Tambahkan Adam Anda, alih-alih Anna. Baca dokumentasi integrasi Plugwise Home Assistant untuk informasi lebih lanjut", "unknown": "Kesalahan yang tidak diharapkan" }, "flow_title": "{name}", diff --git a/homeassistant/components/plugwise/translations/it.json b/homeassistant/components/plugwise/translations/it.json index e60b91a106b..9e051a29e13 100644 --- a/homeassistant/components/plugwise/translations/it.json +++ b/homeassistant/components/plugwise/translations/it.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", + "invalid_setup": "Aggiungi il tuo Adam invece di Anna, consulta la documentazione sull'integrazione di Home Assistant Plugwise per ulteriori informazioni", "unknown": "Errore imprevisto" }, "flow_title": "{name}", diff --git a/homeassistant/components/plugwise/translations/ja.json b/homeassistant/components/plugwise/translations/ja.json index cc3810edcd3..f15d62d38d1 100644 --- a/homeassistant/components/plugwise/translations/ja.json +++ b/homeassistant/components/plugwise/translations/ja.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_setup": "Anna\u306e\u4ee3\u308f\u308a\u306b\u3001Adam\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001Home Assistant Plugwise\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name}", diff --git a/homeassistant/components/plugwise/translations/nl.json b/homeassistant/components/plugwise/translations/nl.json index 160cf182d3d..d295695016f 100644 --- a/homeassistant/components/plugwise/translations/nl.json +++ b/homeassistant/components/plugwise/translations/nl.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", + "invalid_setup": "Voeg je Adam toe in plaats van je Anna, zie de Home Assistant Plugwise integratiedocumentatie voor meer informatie", "unknown": "Onverwachte fout" }, "flow_title": "{name}", diff --git a/homeassistant/components/plugwise/translations/no.json b/homeassistant/components/plugwise/translations/no.json index 58ee9d4aed8..d8ce2d8956a 100644 --- a/homeassistant/components/plugwise/translations/no.json +++ b/homeassistant/components/plugwise/translations/no.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", + "invalid_setup": "Legg til din Adam i stedet for din Anna, se integrasjonsdokumentasjonen for Home Assistant Plugwise for mer informasjon", "unknown": "Uventet feil" }, "flow_title": "{name}", diff --git a/homeassistant/components/plugwise/translations/pl.json b/homeassistant/components/plugwise/translations/pl.json index 5d0bdd0f4e6..c4a2efe95c3 100644 --- a/homeassistant/components/plugwise/translations/pl.json +++ b/homeassistant/components/plugwise/translations/pl.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_setup": "Dodaj urz\u0105dzenie Adam zamiast Anna. Zobacz dokumentacj\u0119 integracji Plugwise dla Home Assistant, aby uzyska\u0107 wi\u0119cej informacji.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "flow_title": "{name}", diff --git a/homeassistant/components/plugwise/translations/pt-BR.json b/homeassistant/components/plugwise/translations/pt-BR.json index f53f6c7c379..35a08d93114 100644 --- a/homeassistant/components/plugwise/translations/pt-BR.json +++ b/homeassistant/components/plugwise/translations/pt-BR.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_setup": "Adicione seu Adam em vez de sua Anna, consulte a documenta\u00e7\u00e3o de integra\u00e7\u00e3o do Home Assistant Plugwise para obter mais informa\u00e7\u00f5es", "unknown": "Erro inesperado" }, "flow_title": "{name}", diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json index c7b28d5b415..62b7c1e27d3 100644 --- a/homeassistant/components/plugwise/translations/ru.json +++ b/homeassistant/components/plugwise/translations/ru.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_setup": "\u0414\u043e\u0431\u0430\u0432\u044c\u0442\u0435 Adam \u0432\u043c\u0435\u0441\u0442\u043e Anna. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439 \u043f\u043e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Plugwise \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "flow_title": "{name}", diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json index 4c752efbd4a..ae756bf15d4 100644 --- a/homeassistant/components/plugwise/translations/tr.json +++ b/homeassistant/components/plugwise/translations/tr.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_setup": "Anna'n\u0131z yerine Adam'\u0131n\u0131z\u0131 ekleyin, daha fazla bilgi i\u00e7in Home Assistant Plugwise entegrasyon belgelerine bak\u0131n", "unknown": "Beklenmeyen hata" }, "flow_title": "{name}", diff --git a/homeassistant/components/plugwise/translations/zh-Hant.json b/homeassistant/components/plugwise/translations/zh-Hant.json index 0a1e21a12ed..57a7add22e0 100644 --- a/homeassistant/components/plugwise/translations/zh-Hant.json +++ b/homeassistant/components/plugwise/translations/zh-Hant.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_setup": "\u65b0\u589e Adam \u800c\u975e Anna\u3001\u8acb\u53c3\u95b1 Home Assistant Plugwise \u6574\u5408\u8aaa\u660e\u6587\u4ef6\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "flow_title": "{name}", diff --git a/homeassistant/components/point/translations/hu.json b/homeassistant/components/point/translations/hu.json index c582bbfc7cd..27c35a1966f 100644 --- a/homeassistant/components/point/translations/hu.json +++ b/homeassistant/components/point/translations/hu.json @@ -11,12 +11,12 @@ "default": "Sikeres hiteles\u00edt\u00e9s" }, "error": { - "follow_link": "K\u00e9rem, k\u00f6vesse a hivatkoz\u00e1st \u00e9s hiteles\u00edtse mag\u00e1t miel\u0151tt megnyomn\u00e1 a K\u00fcld\u00e9s gombot", + "follow_link": "K\u00e9rem, k\u00f6vesse a hivatkoz\u00e1st \u00e9s hiteles\u00edtse mag\u00e1t miel\u0151tt folytatn\u00e1", "no_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token" }, "step": { "auth": { - "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})", + "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 **Mehet** gombot. \n\n [Link]({authorization_url})", "title": "Point hiteles\u00edt\u00e9se" }, "user": { @@ -24,7 +24,7 @@ "flow_impl": "Szolg\u00e1ltat\u00f3" }, "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?", - "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" } } } diff --git a/homeassistant/components/point/translations/it.json b/homeassistant/components/point/translations/it.json index 8a35f7acdf3..c527666e132 100644 --- a/homeassistant/components/point/translations/it.json +++ b/homeassistant/components/point/translations/it.json @@ -4,7 +4,7 @@ "already_setup": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "external_setup": "Point configurato correttamente da un altro flusso.", - "no_flows": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "no_flows": "Il componente non \u00e8 configurato. Segui la documentazione.", "unknown_authorize_url_generation": "Errore sconosciuto durante la generazione di un URL di autorizzazione." }, "create_entry": { diff --git a/homeassistant/components/powerwall/translations/de.json b/homeassistant/components/powerwall/translations/de.json index 17f85254208..e68254918fb 100644 --- a/homeassistant/components/powerwall/translations/de.json +++ b/homeassistant/components/powerwall/translations/de.json @@ -15,7 +15,7 @@ "step": { "confirm_discovery": { "description": "M\u00f6chtest du {name} ({ip_address}) einrichten?", - "title": "Powerwall verbinden" + "title": "Stelle eine Verbindung zur Powerwall her" }, "reauth_confim": { "data": { diff --git a/homeassistant/components/powerwall/translations/es.json b/homeassistant/components/powerwall/translations/es.json index 767f77e58bd..5d82ecda8e5 100644 --- a/homeassistant/components/powerwall/translations/es.json +++ b/homeassistant/components/powerwall/translations/es.json @@ -13,6 +13,9 @@ "flow_title": "Powerwall de Tesla ({ip_address})", "step": { "reauth_confim": { + "data": { + "password": "Contrase\u00f1a" + }, "title": "Reautorizar la powerwall" }, "user": { diff --git a/homeassistant/components/ps4/translations/ca.json b/homeassistant/components/ps4/translations/ca.json index 53d45e5104d..6486c154360 100644 --- a/homeassistant/components/ps4/translations/ca.json +++ b/homeassistant/components/ps4/translations/ca.json @@ -25,6 +25,9 @@ "name": "Nom", "region": "Regi\u00f3" }, + "data_description": { + "code": "V\u00e9s a 'Configuraci\u00f3' de la teva consola PlayStation 4. A continuaci\u00f3, v\u00e9s a 'Configuraci\u00f3 de connexi\u00f3 de l'aplicaci\u00f3 m\u00f2bil' i selecciona 'Afegeix un dispositiu' per obtenir el PIN." + }, "description": "Introdueix la informaci\u00f3 de la teva PlayStation 4. Per al Codi PIN, ves a 'Configuraci\u00f3' a la consola de la PlayStation 4. Despr\u00e9s navega fins a 'Configuraci\u00f3 de la connexi\u00f3 de l'aplicaci\u00f3 m\u00f2bil' i selecciona 'Afegir dispositiu'. Introdueix el Codi PIN que es mostra. Consulta la [documentaci\u00f3](https://www.home-assistant.io/components/ps4/) per a m\u00e9s informaci\u00f3.", "title": "PlayStation 4" }, @@ -33,6 +36,9 @@ "ip_address": "Adre\u00e7a IP (deixa-ho en blanc si fas servir la detecci\u00f3 autom\u00e0tica).", "mode": "Mode de configuraci\u00f3" }, + "data_description": { + "ip_address": "Deixa-ho en blanc si selecciones el descobriment autom\u00e0tic." + }, "description": "Selecciona el mode de configuraci\u00f3. El camp de l'Adre\u00e7a IP es pot deixar en blanc si selecciones descobriment autom\u00e0tic (els dispositius es descobriran autom\u00e0ticament).", "title": "PlayStation 4" } diff --git a/homeassistant/components/ps4/translations/de.json b/homeassistant/components/ps4/translations/de.json index c177c5d81cc..22a50cdf3e3 100644 --- a/homeassistant/components/ps4/translations/de.json +++ b/homeassistant/components/ps4/translations/de.json @@ -25,6 +25,9 @@ "name": "Name", "region": "Region" }, + "data_description": { + "code": "Navigiere auf deiner PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\", um den Pin zu erhalten." + }, "description": "Gib deine PlayStation 4-Informationen ein. Navigiere f\u00fcr den PIN-Code auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN-Code ein. Weitere Informationen findest du in der [Dokumentation](https://www.home-assistant.io/components/ps4/).", "title": "PlayStation 4" }, @@ -33,6 +36,9 @@ "ip_address": "IP-Adresse (Leer lassen, wenn automatische Erkennung verwendet wird).", "mode": "Konfigurationsmodus" }, + "data_description": { + "ip_address": "Lasse das Feld leer, wenn du die automatische Erkennung ausw\u00e4hlst." + }, "description": "W\u00e4hle den Modus f\u00fcr die Konfiguration aus. Das Feld IP-Adresse kann leer bleiben, wenn die automatische Erkennung ausgew\u00e4hlt wird, da Ger\u00e4te automatisch erkannt werden.", "title": "PlayStation 4" } diff --git a/homeassistant/components/ps4/translations/el.json b/homeassistant/components/ps4/translations/el.json index 5bc6b4a7b0e..f3899b682f3 100644 --- a/homeassistant/components/ps4/translations/el.json +++ b/homeassistant/components/ps4/translations/el.json @@ -25,6 +25,9 @@ "name": "\u038c\u03bd\u03bf\u03bc\u03b1", "region": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae" }, + "data_description": { + "code": "\u03a0\u03bb\u03bf\u03b7\u03b3\u03b7\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b9\u03c2 \"\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2\" \u03c3\u03c4\u03b7\u03bd \u03ba\u03bf\u03bd\u03c3\u03cc\u03bb\u03b1 PlayStation 4. \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c0\u03bb\u03bf\u03b7\u03b3\u03b7\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf 'Mobile App Connection Settings' \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 'Add Device' \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03bf pin." + }, "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03bf\u03c5 PlayStation 4. \u0393\u03b9\u03b1 \u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b9\u03c2 \"\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2\" \u03c3\u03c4\u03b7\u03bd \u03ba\u03bf\u03bd\u03c3\u03cc\u03bb\u03b1 PlayStation 4. \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c0\u03bb\u03bf\u03b7\u03b3\u03b7\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf 'Mobile App Connection Settings' (\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ce\u03bd \u03b3\u03b9\u03b1 \u03ba\u03b9\u03bd\u03b7\u03c4\u03ac) \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 'Add Device' (\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2). \u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03c0\u03bf\u03c5 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7](https://www.home-assistant.io/components/ps4/) \u03b3\u03b9\u03b1 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2.", "title": "PlayStation 4" }, @@ -33,6 +36,9 @@ "ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP (\u0391\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03cc \u03b5\u03ac\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u0391\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7).", "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2" }, + "data_description": { + "ip_address": "\u0391\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b5\u03ac\u03bd \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7." + }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2. \u03a4\u03bf \u03c0\u03b5\u03b4\u03af\u03bf \u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03af\u03bd\u03b5\u03b9 \u03ba\u03b5\u03bd\u03cc \u03b5\u03ac\u03bd \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03b5\u03c4\u03b5 Auto Discovery, \u03ba\u03b1\u03b8\u03ce\u03c2 \u03bf\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03b8\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03c4\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1.", "title": "PlayStation 4" } diff --git a/homeassistant/components/ps4/translations/en.json b/homeassistant/components/ps4/translations/en.json index 12b154f48de..1c8640efe2e 100644 --- a/homeassistant/components/ps4/translations/en.json +++ b/homeassistant/components/ps4/translations/en.json @@ -25,6 +25,9 @@ "name": "Name", "region": "Region" }, + "data_description": { + "code": "Navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device' to get the pin." + }, "description": "Enter your PlayStation 4 information. For PIN Code, navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN Code that is displayed. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info.", "title": "PlayStation 4" }, @@ -33,6 +36,9 @@ "ip_address": "IP Address (Leave empty if using Auto Discovery).", "mode": "Config Mode" }, + "data_description": { + "ip_address": "Leave blank if selecting auto-discovery." + }, "description": "Select mode for configuration. The IP Address field can be left blank if selecting Auto Discovery, as devices will be automatically discovered.", "title": "PlayStation 4" } diff --git a/homeassistant/components/ps4/translations/et.json b/homeassistant/components/ps4/translations/et.json index ddc6f0b73a9..83ef7a6f14c 100644 --- a/homeassistant/components/ps4/translations/et.json +++ b/homeassistant/components/ps4/translations/et.json @@ -25,6 +25,9 @@ "name": "Nimi", "region": "Piirkond" }, + "data_description": { + "code": "Navigeeri oma PlayStation 4 konsoolis jaotisse \u201eSeaded\u201d. Seej\u00e4rel navigeeri jaotisse \"Mobiilirakenduse \u00fchenduse s\u00e4tted\" ja vali PIN-koodi saamiseks \"Lisa seade\"." + }, "description": "Sisesta oma PlayStation 4 teave. PIN koodi saamiseks mine PlayStation 4 konsooli \u201eSeaded\u201d. Seej\u00e4rel liigu jaotisse \u201eMobiilirakenduse \u00fchenduse seaded\u201d ja vali \u201eLisa seade\u201d. SisestaPIN kood . Lisateavet leiad [dokumentatsioonist] (https://www.home-assistant.io/components/ps4/).", "title": "" }, @@ -33,6 +36,9 @@ "ip_address": "IP-aadress (J\u00e4tke t\u00fchjaks, kui kasutate automaatset tuvastamist)", "mode": "Seadistuste re\u017eiim" }, + "data_description": { + "ip_address": "Automaatse avastamise valimiseks j\u00e4ta v\u00e4li t\u00fchjaks." + }, "description": "Valikonfigureerimise re\u017eiim. V\u00e4lja IP-aadress saab automaatse tuvastamise valimisel t\u00fchjaks j\u00e4tta, kuna seadmed avastatakse automaatselt.", "title": "" } diff --git a/homeassistant/components/ps4/translations/fr.json b/homeassistant/components/ps4/translations/fr.json index 8fc216fd20b..8bc2575d246 100644 --- a/homeassistant/components/ps4/translations/fr.json +++ b/homeassistant/components/ps4/translations/fr.json @@ -25,6 +25,9 @@ "name": "Nom", "region": "R\u00e9gion" }, + "data_description": { + "code": "Acc\u00e9dez aux \u00ab\u00a0Param\u00e8tres\u00a0\u00bb sur votre console PlayStation\u00a04, puis acc\u00e9dez \u00e0 \u00ab\u00a0Param\u00e8tres de connexion de l'application mobile\u00a0\u00bb et s\u00e9lectionnez \u00ab\u00a0Ajouter un appareil\u00a0\u00bb afin d'obtenir le code PIN." + }, "description": "Saisissez les informations de votre PlayStation\u00a04. Pour le Code PIN, acc\u00e9dez aux \u00ab\u00a0Param\u00e8tres\u00a0\u00bb sur votre console PlayStation\u00a04, puis acc\u00e9dez \u00e0 \u00ab\u00a0Param\u00e8tres de connexion de l'application mobile\u00a0\u00bb et s\u00e9lectionnez \u00ab\u00a0Ajouter un appareil\u00a0\u00bb. Entrez le Code PIN affich\u00e9. Consultez la [documentation](https://www.home-assistant.io/components/ps4/) pour plus d'informations.", "title": "PlayStation 4" }, @@ -33,6 +36,9 @@ "ip_address": "Adresse IP (laissez vide si vous utilisez la d\u00e9couverte automatique).", "mode": "Mode de configuration" }, + "data_description": { + "ip_address": "Laissez le champ vide si vous s\u00e9lectionnez la d\u00e9couverte automatique." + }, "description": "S\u00e9lectionnez le mode de configuration. Le champ Adresse IP peut \u00eatre laiss\u00e9 vide si vous s\u00e9lectionnez D\u00e9couverte automatique, car les appareils seront automatiquement d\u00e9couverts.", "title": "PlayStation 4" } diff --git a/homeassistant/components/ps4/translations/hu.json b/homeassistant/components/ps4/translations/hu.json index 753ea60b282..24da7fc15f5 100644 --- a/homeassistant/components/ps4/translations/hu.json +++ b/homeassistant/components/ps4/translations/hu.json @@ -4,27 +4,30 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "credential_error": "Hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u0151 adatok beolvas\u00e1sa sor\u00e1n.", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", - "port_987_bind_error": "Nem siker\u00fclt a 987. porthoz kapcsol\u00f3dni. Tov\u00e1bbi inform\u00e1ci\u00f3 a [dokument\u00e1ci\u00f3ban] tal\u00e1lhat\u00f3 (https://www.home-assistant.io/components/ps4/).", - "port_997_bind_error": "Nem siker\u00fclt a 997-es porthoz kapcsol\u00f3dni. Tov\u00e1bbi inform\u00e1ci\u00f3 a [dokument\u00e1ci\u00f3ban] tal\u00e1lhat\u00f3 (https://www.home-assistant.io/components/ps4/)." + "port_987_bind_error": "Nem siker\u00fclt a 987. portot lefoglalni. Tov\u00e1bbi inform\u00e1ci\u00f3 a [dokument\u00e1ci\u00f3ban] tal\u00e1lhat\u00f3 (https://www.home-assistant.io/components/ps4/).", + "port_997_bind_error": "Nem siker\u00fclt a 997. portot lefoglalni. Tov\u00e1bbi inform\u00e1ci\u00f3 a [dokument\u00e1ci\u00f3ban] tal\u00e1lhat\u00f3 (https://www.home-assistant.io/components/ps4/)." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "credential_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s. Az \u00fajraind\u00edt\u00e1shoz nyomja meg a K\u00fcld\u00e9s gombot.", + "credential_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s. Az \u00fajraind\u00edt\u00e1shoz nyomja meg a Mehet 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" }, "step": { "creds": { - "description": "Hiteles\u00edt\u0151 adatok sz\u00fcks\u00e9gesek. Nyomja meg a \u201eK\u00fcld\u00e9s\u201d gombot, majd a PS4 2. k\u00e9perny\u0151 alkalmaz\u00e1sban friss\u00edtse az eszk\u00f6z\u00f6ket, \u00e9s a folytat\u00e1shoz v\u00e1lassza a \u201eHome-Assistant\u201d eszk\u00f6zt.", + "description": "Hiteles\u00edt\u0151 adatok sz\u00fcks\u00e9gesek. Nyomja meg a \u201eMehet\u201d gombot, majd a PS4 2. k\u00e9perny\u0151 alkalmaz\u00e1sban friss\u00edtse az eszk\u00f6z\u00f6ket, \u00e9s a folytat\u00e1shoz v\u00e1lassza a \u201eHome-Assistant\u201d eszk\u00f6zt.", "title": "PlayStation 4" }, "link": { "data": { "code": "PIN-k\u00f3d", "ip_address": "IP c\u00edm", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "region": "R\u00e9gi\u00f3" }, + "data_description": { + "code": "Keresse meg a PlayStation 4 konzol \"Be\u00e1ll\u00edt\u00e1sok\" gombj\u00e1t. Ezut\u00e1n keresse meg a \"Mobilalkalmaz\u00e1s-kapcsolat be\u00e1ll\u00edt\u00e1sai\" lehet\u0151s\u00e9get, \u00e9s v\u00e1lassza az \"Eszk\u00f6z hozz\u00e1ad\u00e1sa\" lehet\u0151s\u00e9get a pin lek\u00e9r\u00e9s\u00e9hez." + }, "description": "Adja meg a PlayStation 4 adatait. A PIN-k\u00f3d eset\u00e9ben keresse meg a PlayStation 4 konzol \u201eBe\u00e1ll\u00edt\u00e1sok\u201d elem\u00e9t. Ezut\u00e1n keresse meg a \u201eMobilalkalmaz\u00e1s-kapcsolat be\u00e1ll\u00edt\u00e1sai\u201d elemet, \u00e9s v\u00e1lassza az \u201eEszk\u00f6z hozz\u00e1ad\u00e1sa\u201d lehet\u0151s\u00e9get. \u00cdrja be a megjelen\u0151 PIN-k\u00f3d . Tov\u00e1bbi inform\u00e1ci\u00f3k a [dokument\u00e1ci\u00f3ban] tal\u00e1lhat\u00f3k (https://www.home-assistant.io/components/ps4/).", "title": "PlayStation 4" }, @@ -33,6 +36,9 @@ "ip_address": "IP c\u00edm (Hagyja \u00fcresen az Automatikus Felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz).", "mode": "Konfigur\u00e1ci\u00f3s m\u00f3d" }, + "data_description": { + "ip_address": "Az automatikus felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz hagyja \u00fcresen" + }, "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.", "title": "PlayStation 4" } diff --git a/homeassistant/components/ps4/translations/id.json b/homeassistant/components/ps4/translations/id.json index aab31564e16..beb15fb5c4c 100644 --- a/homeassistant/components/ps4/translations/id.json +++ b/homeassistant/components/ps4/translations/id.json @@ -25,6 +25,9 @@ "name": "Nama", "region": "Wilayah" }, + "data_description": { + "code": "Navigasikan ke 'Pengaturan' di konsol PlayStation 4 Anda. Kemudian navigasikan ke 'Pengaturan Koneksi Aplikasi Seluler' dan pilih 'Tambahkan Perangkat' untuk mendapatkan PIN." + }, "description": "Masukkan informasi PlayStation 4 Anda. Untuk Kode PIN, buka 'Pengaturan' di konsol PlayStation 4 Anda. Kemudian buka 'Pengaturan Koneksi Aplikasi Seluler' dan pilih 'Tambah Perangkat'. Masukkan Kode PIN yang ditampilkan. Lihat [dokumentasi] (https://www.home-assistant.io/components/ps4/) untuk info lebih lanjut.", "title": "PlayStation 4" }, @@ -33,6 +36,9 @@ "ip_address": "Alamat IP (Kosongkan jika menggunakan Penemuan Otomatis).", "mode": "Mode Konfigurasi" }, + "data_description": { + "ip_address": "Kosongkan jika memilih penemuan otomatis." + }, "description": "Pilih mode untuk konfigurasi. Alamat IP dapat dikosongkan jika memilih Penemuan Otomatis karena perangkat akan ditemukan secara otomatis.", "title": "PlayStation 4" } diff --git a/homeassistant/components/ps4/translations/it.json b/homeassistant/components/ps4/translations/it.json index 8df24f24afb..3faca963317 100644 --- a/homeassistant/components/ps4/translations/it.json +++ b/homeassistant/components/ps4/translations/it.json @@ -25,6 +25,9 @@ "name": "Nome", "region": "Area geografica" }, + "data_description": { + "code": "Vai a 'Impostazioni' sulla tua console PlayStation 4. Quindi vai su 'Impostazioni connessione app mobili' e seleziona 'Aggiungi dispositivo' per ottenere il pin." + }, "description": "Inserisci le informazioni per la tua PlayStation 4. Per il codice PIN, vai a \"Impostazioni\" sulla PlayStation 4. Quindi vai a 'Impostazioni di connessione Mobile App' e seleziona 'Aggiungi dispositivo'. Immettere il codice PIN visualizzato. Fai riferimento alla [documentazione](https://www.home-assistant.io/components/ps4/) per ulteriori informazioni.", "title": "PlayStation 4" }, @@ -33,6 +36,9 @@ "ip_address": "Indirizzo IP (Lascia vuoto se stai usando il rilevamento automatico).", "mode": "Modalit\u00e0 di configurazione" }, + "data_description": { + "ip_address": "Lascia vuoto se selezioni il rilevamento automatico." + }, "description": "Seleziona la modalit\u00e0 per la configurazione. Il campo per l'indirizzo IP pu\u00f2 essere lasciato vuoto se si seleziona il rilevamento automatico, poich\u00e9 i dispositivi saranno rilevati automaticamente.", "title": "PlayStation 4" } diff --git a/homeassistant/components/ps4/translations/ja.json b/homeassistant/components/ps4/translations/ja.json index 6b1a162b387..92be62f7229 100644 --- a/homeassistant/components/ps4/translations/ja.json +++ b/homeassistant/components/ps4/translations/ja.json @@ -25,6 +25,9 @@ "name": "\u540d\u524d", "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" }, + "data_description": { + "code": "PlayStation 4\u672c\u4f53\u306e' \u8a2d\u5b9a' \u306b\u79fb\u52d5\u3057\u307e\u3059\u3002\u6b21\u306b\u3001' \u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u63a5\u7d9a\u8a2d\u5b9a' \u306b\u79fb\u52d5\u3057\u3001' \u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u52a0' \u3092\u9078\u629e\u3057\u3066\u3001\u30d4\u30f3\u3092\u53d6\u5f97\u3057\u307e\u3059\u3002" + }, "description": "PlayStation4\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002 PIN\u30b3\u30fc\u30c9(PIN CodePIN Code)\u306e\u5834\u5408\u306f\u3001PlayStation4\u672c\u4f53\u306e '\u8a2d\u5b9a' \u306b\u79fb\u52d5\u3057\u307e\u3059\u3002\u6b21\u306b\u3001'\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u63a5\u7d9a\u8a2d\u5b9a' \u306b\u79fb\u52d5\u3057\u3066\u3001'\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u52a0' \u3092\u9078\u629e\u3057\u307e\u3059\u3002\u8868\u793a\u3055\u308c\u305f PIN\u30b3\u30fc\u30c9(PIN CodePIN Code) \u3092\u5165\u529b\u3057\u307e\u3059\u3002\u8a73\u7d30\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/components/ps4/) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Play Station 4" }, @@ -33,6 +36,9 @@ "ip_address": "IP\u30a2\u30c9\u30ec\u30b9(\u81ea\u52d5\u691c\u51fa\u3092\u4f7f\u7528\u3059\u308b\u5834\u5408\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u307e\u3059)", "mode": "\u30b3\u30f3\u30d5\u30a3\u30b0\u30e2\u30fc\u30c9" }, + "data_description": { + "ip_address": "\u81ea\u52d5\u691c\u51fa\u3092\u9078\u629e\u3059\u308b\u5834\u5408\u306f\u3001\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u307e\u3059\u3002" + }, "description": "\u30b3\u30f3\u30d5\u30a3\u30ae\u30e5\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30e2\u30fc\u30c9\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u81ea\u52d5\u691c\u51fa\u3092\u9078\u629e\u3059\u308b\u3068\u3001IP\u30a2\u30c9\u30ec\u30b9\u306e\u6b04\u304c\u7a7a\u767d\u306e\u307e\u307e\u3067\u3082\u30c7\u30d0\u30a4\u30b9\u306f\u81ea\u52d5\u7684\u306b\u691c\u51fa\u3055\u308c\u307e\u3059\u3002", "title": "Play Station 4" } diff --git a/homeassistant/components/ps4/translations/nl.json b/homeassistant/components/ps4/translations/nl.json index db59c7ab30a..1a6ddd26f7f 100644 --- a/homeassistant/components/ps4/translations/nl.json +++ b/homeassistant/components/ps4/translations/nl.json @@ -25,6 +25,9 @@ "name": "Naam", "region": "Regio" }, + "data_description": { + "code": "Navigeer naar 'Instellingen' op je PlayStation 4-systeem. Navigeer vervolgens naar 'Instellingen mobiele app-verbinding' en selecteer 'Apparaat toevoegen' om de pin te krijgen." + }, "description": "Voer je PlayStation 4-informatie in. Voor PIN-code navigeer je naar 'Instellingen' op je PlayStation 4-console. Ga vervolgens naar 'Verbindingsinstellingen mobiele app' en selecteer 'Apparaat toevoegen'. Voer de PIN-code in die wordt weergegeven. Raadpleeg de [documentatie](https://www.home-assistant.io/components/ps4/) voor meer informatie.", "title": "PlayStation 4" }, @@ -33,6 +36,9 @@ "ip_address": "IP-adres (leeg laten indien Auto Discovery wordt gebruikt).", "mode": "Configuratiemodus" }, + "data_description": { + "ip_address": "Leeg laten indien u auto-discovery selecteert." + }, "description": "Selecteer modus voor configuratie. Het veld IP-adres kan leeg worden gelaten als Auto Discovery wordt geselecteerd, omdat apparaten dan automatisch worden ontdekt.", "title": "PlayStation 4" } diff --git a/homeassistant/components/ps4/translations/no.json b/homeassistant/components/ps4/translations/no.json index 185b0e031c5..69332862a33 100644 --- a/homeassistant/components/ps4/translations/no.json +++ b/homeassistant/components/ps4/translations/no.json @@ -25,6 +25,9 @@ "name": "Navn", "region": "" }, + "data_description": { + "code": "Naviger til \"Innstillinger\" p\u00e5 PlayStation 4-konsollen. Naviger deretter til \"Mobile App Connection Settings\" og velg \"Add Device\" for \u00e5 hente pinnen." + }, "description": "Skriv inn PlayStation 4-informasjonen din. For PIN kode , naviger til 'Innstillinger' p\u00e5 PlayStation 4-konsollen. Naviger deretter til 'Innstillinger for tilkobling av mobilapp' og velg 'Legg til enhet'. Skriv inn PIN kode som vises. Se [dokumentasjon] (https://www.home-assistant.io/components/ps4/) for mer informasjon.", "title": "" }, @@ -33,6 +36,9 @@ "ip_address": "IP adresse (La v\u00e6re tom hvis du bruker automatisk oppdagelse)", "mode": "Konfigureringsmodus" }, + "data_description": { + "ip_address": "La st\u00e5 tomt hvis du velger automatisk oppdagelse." + }, "description": "Velg modus for konfigurasjon. Feltet IP adresse kan st\u00e5 tomt hvis du velger automatisk oppdagelse, da enheter automatisk blir oppdaget.", "title": "" } diff --git a/homeassistant/components/ps4/translations/pl.json b/homeassistant/components/ps4/translations/pl.json index 9840363630d..af4d36b9bf2 100644 --- a/homeassistant/components/ps4/translations/pl.json +++ b/homeassistant/components/ps4/translations/pl.json @@ -25,6 +25,9 @@ "name": "Nazwa", "region": "Region" }, + "data_description": { + "code": "Przejd\u017a do \u201eUstawienia\u201d na konsoli PlayStation 4. Nast\u0119pnie przejd\u017a do \u201eUstawienia po\u0142\u0105czenia aplikacji mobilnej\u201d i wybierz \u201eDodaj urz\u0105dzenie\u201d, aby uzyska\u0107 kod PIN." + }, "description": "Wprowad\u017a informacje o PlayStation 4. Aby uzyska\u0107 'PIN', przejd\u017a do 'Ustawienia' na konsoli PlayStation 4. Nast\u0119pnie przejd\u017a do 'Ustawienia po\u0142\u0105czenia aplikacji mobilnej' i wybierz 'Dodaj urz\u0105dzenie'. Wprowad\u017a wy\u015bwietlony kod PIN. Zapoznaj si\u0119 z [dokumentacj\u0105](https://www.home-assistant.io/components/ps4/), by pozna\u0107 szczeg\u00f3\u0142y.", "title": "PlayStation 4" }, @@ -33,6 +36,9 @@ "ip_address": "Adres IP (pozostaw puste, je\u015bli u\u017cywasz wykrywania)", "mode": "Tryb konfiguracji" }, + "data_description": { + "ip_address": "Pozostaw puste, je\u015bli wybierasz automatyczne wykrywanie." + }, "description": "Wybierz tryb konfiguracji. Pole adresu IP mo\u017cna pozostawi\u0107 puste, je\u015bli wybierzesz opcj\u0119 Auto Discovery, poniewa\u017c urz\u0105dzenia zostan\u0105 automatycznie wykryte.", "title": "PlayStation 4" } diff --git a/homeassistant/components/ps4/translations/pt-BR.json b/homeassistant/components/ps4/translations/pt-BR.json index 7938cbb6241..12d54264f78 100644 --- a/homeassistant/components/ps4/translations/pt-BR.json +++ b/homeassistant/components/ps4/translations/pt-BR.json @@ -25,6 +25,9 @@ "name": "Nome", "region": "Regi\u00e3o" }, + "data_description": { + "code": "Navegue at\u00e9 'Configura\u00e7\u00f5es' em seu console PlayStation 4. Em seguida, navegue at\u00e9 'Configura\u00e7\u00f5es de conex\u00e3o do aplicativo m\u00f3vel' e selecione 'Adicionar dispositivo' para obter o PIN." + }, "description": "Digite suas informa\u00e7\u00f5es do PlayStation 4. Para C\u00f3digo PIN, navegue at\u00e9 'Configura\u00e7\u00f5es' no seu console PlayStation 4. Em seguida, navegue at\u00e9 \"Configura\u00e7\u00f5es de conex\u00e3o de aplicativos m\u00f3veis\" e selecione \"Adicionar dispositivo\". Digite o C\u00f3digo PIN exibido. Consulte a [documenta\u00e7\u00e3o] (https://www.home-assistant.io/components/ps4/) para informa\u00e7\u00f5es adicionais.", "title": "Playstation 4" }, @@ -33,6 +36,9 @@ "ip_address": "Endere\u00e7o IP (Deixe em branco se estiver usando a Detec\u00e7\u00e3o autom\u00e1tica).", "mode": "Modo de configura\u00e7\u00e3o" }, + "data_description": { + "ip_address": "Deixe em branco se selecionar a descoberta autom\u00e1tica." + }, "description": "Selecione o modo para configura\u00e7\u00e3o. O campo Endere\u00e7o IP pode ser deixado em branco se selecionar Detec\u00e7\u00e3o Autom\u00e1tica, pois os dispositivos ser\u00e3o descobertos automaticamente.", "title": "Playstation 4" } diff --git a/homeassistant/components/ps4/translations/ru.json b/homeassistant/components/ps4/translations/ru.json index ea42e260ec8..646346095b0 100644 --- a/homeassistant/components/ps4/translations/ru.json +++ b/homeassistant/components/ps4/translations/ru.json @@ -25,6 +25,9 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "region": "\u0420\u0435\u0433\u0438\u043e\u043d" }, + "data_description": { + "code": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f PIN-\u043a\u043e\u0434\u0430 \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043f\u0443\u043d\u043a\u0442\u0443 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438** \u043d\u0430 \u043a\u043e\u043d\u0441\u043e\u043b\u0438 PlayStation 4. \u0417\u0430\u0442\u0435\u043c \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f** \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e**." + }, "description": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f PIN-\u043a\u043e\u0434\u0430 \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043f\u0443\u043d\u043a\u0442\u0443 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438** \u043d\u0430 \u043a\u043e\u043d\u0441\u043e\u043b\u0438 PlayStation 4. \u0417\u0430\u0442\u0435\u043c \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f** \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e**. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/components/ps4/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "title": "PlayStation 4" }, @@ -33,6 +36,9 @@ "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 \u0440\u0435\u0436\u0438\u043c\u0430 \u0430\u0432\u0442\u043e\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f)", "mode": "\u0420\u0435\u0436\u0438\u043c" }, + "data_description": { + "ip_address": "\u041e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435" + }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u041f\u043e\u043b\u0435 'IP-\u0430\u0434\u0440\u0435\u0441' \u043c\u043e\u0436\u043d\u043e \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043f\u0443\u0441\u0442\u044b\u043c, \u0435\u0441\u043b\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043e 'Auto Discovery', \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0431\u0443\u0434\u0443\u0442 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u044b \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438.", "title": "PlayStation 4" } diff --git a/homeassistant/components/ps4/translations/tr.json b/homeassistant/components/ps4/translations/tr.json index 8243e179a98..a2e8149db7d 100644 --- a/homeassistant/components/ps4/translations/tr.json +++ b/homeassistant/components/ps4/translations/tr.json @@ -25,6 +25,9 @@ "name": "Ad", "region": "B\u00f6lge" }, + "data_description": { + "code": "PlayStation 4 konsolunuzda 'Ayarlar'a gidin. Ard\u0131ndan, \"Mobil Uygulama Ba\u011flant\u0131 Ayarlar\u0131\"na gidin ve PIN'i almak i\u00e7in \"Cihaz Ekle\"yi se\u00e7in." + }, "description": "PlayStation 4 bilgilerinizi girin. PIN Kodu i\u00e7in PlayStation 4 konsolunuzda 'Ayarlar'a gidin. Ard\u0131ndan \"Mobil Uygulama Ba\u011flant\u0131 Ayarlar\u0131\"na gidin ve \"Cihaz Ekle\"yi se\u00e7in. PIN Kodu kodunu girin. Ek bilgi i\u00e7in [belgelere](https://www.home-assistant.io/components/ps4/) bak\u0131n.", "title": "PlayStation 4" }, @@ -33,6 +36,9 @@ "ip_address": "IP Adresi (Otomatik Ke\u015fif kullan\u0131l\u0131yorsa bo\u015f b\u0131rak\u0131n).", "mode": "Yap\u0131land\u0131rma Modu" }, + "data_description": { + "ip_address": "Otomatik bulma se\u00e7iliyorsa bo\u015f b\u0131rak\u0131n." + }, "description": "Yap\u0131land\u0131rma i\u00e7in modu se\u00e7in. IP Adresi alan\u0131, Otomatik Ke\u015fif se\u00e7ildi\u011finde cihazlar otomatik olarak ke\u015ffedilece\u011finden bo\u015f b\u0131rak\u0131labilir.", "title": "PlayStation 4" } diff --git a/homeassistant/components/ps4/translations/zh-Hant.json b/homeassistant/components/ps4/translations/zh-Hant.json index e9c8dac2a26..ae7e86377d8 100644 --- a/homeassistant/components/ps4/translations/zh-Hant.json +++ b/homeassistant/components/ps4/translations/zh-Hant.json @@ -25,6 +25,9 @@ "name": "\u540d\u7a31", "region": "\u5340\u57df" }, + "data_description": { + "code": "\u5207\u63db\u81f3 PlayStation 4 \u4e3b\u6a5f\u7684\u300c\u8a2d\u5b9a\u300d\u5167\uff0c\u4e26\u65bc\u300c\u884c\u52d5\u7a0b\u5f0f\u9023\u7dda\u8a2d\u5b9a\uff08Mobile App Connection Settings\uff09\u300d\u4e2d\u9078\u64c7\u300c\u65b0\u589e\u88dd\u7f6e\u300d\u4ee5\u53d6\u5f97 PIN \u78bc\u3002" + }, "description": "\u8f38\u5165\u60a8\u7684 PlayStation 4 \u8cc7\u8a0a\uff0c\u300cPIN \u78bc\u300d\u65bc PlayStation 4 \u4e3b\u6a5f\u7684\u300c\u8a2d\u5b9a\u300d\u5167\uff0c\u4e26\u65bc\u300c\u884c\u52d5\u7a0b\u5f0f\u9023\u7dda\u8a2d\u5b9a\uff08Mobile App Connection Settings\uff09\u300d\u4e2d\u9078\u64c7\u300c\u65b0\u589e\u88dd\u7f6e\u300d\u3002\u8f38\u5165\u6240\u986f\u793a\u7684 PIN \u78bc\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", "title": "PlayStation 4" }, @@ -33,6 +36,9 @@ "ip_address": "IP \u4f4d\u5740\uff08\u5982\u679c\u4f7f\u7528\u81ea\u52d5\u641c\u7d22\u65b9\u5f0f\uff0c\u8acb\u4fdd\u7559\u7a7a\u767d\uff09\u3002", "mode": "\u8a2d\u5b9a\u6a21\u5f0f" }, + "data_description": { + "ip_address": "\u5047\u5982\u9078\u64c7\u4f7f\u7528\u81ea\u52d5\u641c\u7d22\u8acb\u4fdd\u6301\u7a7a\u767d" + }, "description": "\u9078\u64c7\u6a21\u5f0f\u4ee5\u9032\u884c\u8a2d\u5b9a\u3002\u5047\u5982\u9078\u64c7\u81ea\u52d5\u641c\u7d22\u6a21\u5f0f\u7684\u8a71\uff0c\u7531\u65bc\u6703\u81ea\u52d5\u9032\u884c\u88dd\u7f6e\u641c\u5c0b\uff0cIP \u4f4d\u5740\u53ef\u4fdd\u7559\u70ba\u7a7a\u767d\u3002", "title": "PlayStation 4" } diff --git a/homeassistant/components/pure_energie/translations/cs.json b/homeassistant/components/pure_energie/translations/cs.json new file mode 100644 index 00000000000..76dabdfb963 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "flow_title": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/fr.json b/homeassistant/components/pure_energie/translations/fr.json index 7ab74aae074..e121c7f655c 100644 --- a/homeassistant/components/pure_energie/translations/fr.json +++ b/homeassistant/components/pure_energie/translations/fr.json @@ -13,6 +13,10 @@ "data": { "host": "H\u00f4te" } + }, + "zeroconf_confirm": { + "description": "Voulez-vous ajouter Pure Energie Meter (`{model}`) \u00e0 Home Assistant\u00a0?", + "title": "D\u00e9couverte de l'appareil Pure Energie Meter" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ja.json b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json index 919186d0e62..d88ee379678 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ja.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json @@ -9,7 +9,7 @@ "name": "\u30bb\u30f3\u30b5\u30fc\u540d", "power": "\u5951\u7d04\u96fb\u529b (kW)", "power_p3": "\u8c37\u9593(valley period) P3 (kW)\u306e\u5951\u7d04\u96fb\u529b", - "tariff": "\u5730\u57df\u5225\u9069\u7528\u95a2\u7a0e" + "tariff": "\u5730\u57df\u5225\u9069\u7528\u95a2\u7a0e(Applicable tariff)" }, "description": "\u3053\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u516c\u5f0fAPI\u3092\u4f7f\u7528\u3057\u3066\u3001\u30b9\u30da\u30a4\u30f3\u3067\u306e[\u96fb\u6c17\u306e\u6642\u9593\u4fa1\u683c((hourly pricing of electricity)PVPC)](https://www.esios.ree.es/es/pvpc) \u3092\u53d6\u5f97\u3057\u307e\u3059\u3002\n\u3088\u308a\u6b63\u78ba\u306a\u8aac\u660e\u306b\u3064\u3044\u3066\u306f\u3001[\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3 \u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/) \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u30bb\u30f3\u30b5\u30fc\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" @@ -22,7 +22,7 @@ "data": { "power": "\u5951\u7d04\u96fb\u529b (kW)", "power_p3": "\u8c37\u9593(valley period) P3 (kW)\u306e\u5951\u7d04\u96fb\u529b", - "tariff": "\u5730\u57df\u5225\u9069\u7528\u95a2\u7a0e" + "tariff": "\u5730\u57df\u5225\u9069\u7528\u95a2\u7a0e(Applicable tariff)" }, "description": "\u3053\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u516c\u5f0fAPI\u3092\u4f7f\u7528\u3057\u3066\u3001\u30b9\u30da\u30a4\u30f3\u3067\u306e[\u96fb\u6c17\u306e\u6642\u9593\u4fa1\u683c(hourly pricing of electricity)PVPC)](https://www.esios.ree.es/es/pvpc) \u3092\u53d6\u5f97\u3057\u307e\u3059\u3002\n\u3088\u308a\u6b63\u78ba\u306a\u8aac\u660e\u306b\u3064\u3044\u3066\u306f\u3001[\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3 \u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/) \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u30bb\u30f3\u30b5\u30fc\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" diff --git a/homeassistant/components/qnap_qsw/translations/de.json b/homeassistant/components/qnap_qsw/translations/de.json new file mode 100644 index 00000000000..ad2599e24c4 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "invalid_id": "Ger\u00e4t hat eine ung\u00fcltige eindeutige ID zur\u00fcckgegeben" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "url": "URL", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/el.json b/homeassistant/components/qnap_qsw/translations/el.json new file mode 100644 index 00000000000..1bea5dcc9d7 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "invalid_id": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03c0\u03ad\u03c3\u03c4\u03c1\u03b5\u03c8\u03b5 \u03ad\u03bd\u03b1 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03bc\u03bf\u03bd\u03b1\u03b4\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "step": { + "user": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/en.json b/homeassistant/components/qnap_qsw/translations/en.json index 2d0180d1670..b6f68f2f062 100644 --- a/homeassistant/components/qnap_qsw/translations/en.json +++ b/homeassistant/components/qnap_qsw/translations/en.json @@ -11,9 +11,9 @@ "step": { "user": { "data": { + "password": "Password", "url": "URL", - "username": "Username", - "password": "Password" + "username": "Username" } } } diff --git a/homeassistant/components/qnap_qsw/translations/et.json b/homeassistant/components/qnap_qsw/translations/et.json new file mode 100644 index 00000000000..eb7ddb6dbe7 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "invalid_id": "Seade tagastas kehtetu kordumatu ID" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus" + }, + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "url": "URL", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/fr.json b/homeassistant/components/qnap_qsw/translations/fr.json new file mode 100644 index 00000000000..2631bcdfc87 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "invalid_id": "L'appareil a renvoy\u00e9 un ID unique non valide" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "url": "URL", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/hu.json b/homeassistant/components/qnap_qsw/translations/hu.json new file mode 100644 index 00000000000..c41ba084d95 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "invalid_id": "A k\u00e9sz\u00fcl\u00e9k \u00e9rv\u00e9nytelen egyedi azonos\u00edt\u00f3t k\u00fcld\u00f6tt vissza" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "url": "URL", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/id.json b/homeassistant/components/qnap_qsw/translations/id.json new file mode 100644 index 00000000000..b34bcd046d9 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "invalid_id": "Perangkat mengembalikan ID unik yang tidak valid" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "url": "URL", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/it.json b/homeassistant/components/qnap_qsw/translations/it.json new file mode 100644 index 00000000000..0759ef4ca8a --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "invalid_id": "Il dispositivo ha restituito un ID univoco non valido" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "user": { + "data": { + "password": "Password", + "url": "URL", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/nl.json b/homeassistant/components/qnap_qsw/translations/nl.json new file mode 100644 index 00000000000..10ec94f589d --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "invalid_id": "Het apparaat heeft een ongeldige unieke ID teruggestuurd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "url": "URL", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/no.json b/homeassistant/components/qnap_qsw/translations/no.json new file mode 100644 index 00000000000..9eff9c22080 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "invalid_id": "Enheten returnerte en ugyldig unik ID" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "url": "URL", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/pl.json b/homeassistant/components/qnap_qsw/translations/pl.json new file mode 100644 index 00000000000..e90d527a7a7 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "invalid_id": "Urz\u0105dzenie zwr\u00f3ci\u0142o nieprawid\u0142owy unikalny identyfikator" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "url": "URL", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/pt-BR.json b/homeassistant/components/qnap_qsw/translations/pt-BR.json new file mode 100644 index 00000000000..b9829d78944 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "invalid_id": "O dispositivo retornou um ID exclusivo inv\u00e1lido" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "url": "URL", + "username": "Usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/ru.json b/homeassistant/components/qnap_qsw/translations/ru.json new file mode 100644 index 00000000000..ae3869552d0 --- /dev/null +++ b/homeassistant/components/qnap_qsw/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.", + "invalid_id": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0435\u0440\u043d\u0443\u043b\u043e \u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 ID." + }, + "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." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "url": "URL-\u0430\u0434\u0440\u0435\u0441", + "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/qnap_qsw/translations/zh-Hant.json b/homeassistant/components/qnap_qsw/translations/zh-Hant.json new file mode 100644 index 00000000000..f29c42d6eb6 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "invalid_id": "\u88dd\u7f6e\u56de\u8986\u4e86\u7121\u6548\u7684\u552f\u4e00 ID" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "url": "\u7db2\u5740", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/cs.json b/homeassistant/components/radio_browser/translations/cs.json new file mode 100644 index 00000000000..19f5d1e1587 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/fr.json b/homeassistant/components/radio_browser/translations/fr.json index 807ba246694..de1e4f25261 100644 --- a/homeassistant/components/radio_browser/translations/fr.json +++ b/homeassistant/components/radio_browser/translations/fr.json @@ -2,6 +2,11 @@ "config": { "abort": { "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "step": { + "user": { + "description": "Voulez-vous ajouter le navigateur radio \u00e0 Home Assistant\u00a0?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/he.json b/homeassistant/components/radio_browser/translations/he.json index d0c3523da94..89c5e30a440 100644 --- a/homeassistant/components/radio_browser/translations/he.json +++ b/homeassistant/components/radio_browser/translations/he.json @@ -2,6 +2,11 @@ "config": { "abort": { "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05d3\u05e4\u05d3\u05e4\u05df \u05e8\u05d3\u05d9\u05d5 \u05d0\u05dc Home Assistant?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/remote/translations/hu.json b/homeassistant/components/remote/translations/hu.json index 2291d4f8b94..52b24f79f08 100644 --- a/homeassistant/components/remote/translations/hu.json +++ b/homeassistant/components/remote/translations/hu.json @@ -10,6 +10,7 @@ "is_on": "{entity_name} be van kapcsolva" }, "trigger_type": { + "changed_states": "{entity_name} be- vagy kikapcsolt", "toggled": "{entity_name} \u00e1tkapcsolt", "turned_off": "{entity_name} ki lett kapcsolva", "turned_on": "{entity_name} be lett kapcsolva" diff --git a/homeassistant/components/roku/translations/hu.json b/homeassistant/components/roku/translations/hu.json index 256a8d45997..c28b0a712ea 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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { diff --git a/homeassistant/components/roku/translations/it.json b/homeassistant/components/roku/translations/it.json index 8c2d9d4e84a..9f42c49c74f 100644 --- a/homeassistant/components/roku/translations/it.json +++ b/homeassistant/components/roku/translations/it.json @@ -12,16 +12,16 @@ "step": { "discovery_confirm": { "data": { - "one": "Pi\u00f9", - "other": "Altri" + "one": "Vuoto", + "other": "Vuoti" }, "description": "Vuoi configurare {name}?", "title": "Roku" }, "ssdp_confirm": { "data": { - "one": "Pi\u00f9", - "other": "Altri" + "one": "Vuoto", + "other": "Vuoti" }, "description": "Vuoi impostare {name}?", "title": "Roku" diff --git a/homeassistant/components/roomba/translations/pl.json b/homeassistant/components/roomba/translations/pl.json index c56c00a98b0..d2d76bee5ff 100644 --- a/homeassistant/components/roomba/translations/pl.json +++ b/homeassistant/components/roomba/translations/pl.json @@ -16,7 +16,7 @@ "host": "Nazwa hosta lub adres IP" }, "description": "Wybierz Roomb\u0119 lub Braava", - "title": "Po\u0142\u0105cz si\u0119 automatycznie z urz\u0105dzeniem" + "title": "Automatyczne po\u0142\u0105czenie z urz\u0105dzeniem" }, "link": { "description": "Naci\u015bnij i przytrzymaj przycisk Home na {name} a\u017c urz\u0105dzenie wygeneruje d\u017awi\u0119k (oko\u0142o dwie sekundy), a nast\u0119pnie zatwierd\u017a w ci\u0105gu 30 sekund.", @@ -46,7 +46,7 @@ "password": "Has\u0142o" }, "description": "Wybierz Roomb\u0119 lub Braava", - "title": "Po\u0142\u0105cz si\u0119 automatycznie z urz\u0105dzeniem" + "title": "Automatyczne po\u0142\u0105czenie z urz\u0105dzeniem" } } }, diff --git a/homeassistant/components/roon/translations/ca.json b/homeassistant/components/roon/translations/ca.json index f05ac1a4acb..3f601ded490 100644 --- a/homeassistant/components/roon/translations/ca.json +++ b/homeassistant/components/roon/translations/ca.json @@ -8,6 +8,13 @@ "unknown": "Error inesperat" }, "step": { + "fallback": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port" + }, + "description": "No s'ha pogut descobrir el servidor Roon, introdueix l'amfitri\u00f3 i el port." + }, "link": { "description": "Has d'autoritzar Home Assistant a Roon. Despr\u00e9s de fer clic a envia, ves a l'aplicaci\u00f3 Roon Core, obre la Configuraci\u00f3 i activa Home Assistant a la pestanya d'Extensions.", "title": "Autoritza Home Assistant a Roon" diff --git a/homeassistant/components/roon/translations/de.json b/homeassistant/components/roon/translations/de.json index 4eadf9c363a..e41fe77c3cd 100644 --- a/homeassistant/components/roon/translations/de.json +++ b/homeassistant/components/roon/translations/de.json @@ -8,6 +8,13 @@ "unknown": "Unerwarteter Fehler" }, "step": { + "fallback": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Der Roon-Server konnte nicht gefunden werden, bitte gib deinen Hostnamen und Port ein." + }, "link": { "description": "Du musst den Home Assistant in Roon autorisieren. Nachdem du auf \"Submit\" geklickt hast, gehe zur Roon Core-Anwendung, \u00f6ffne die Einstellungen und aktiviere HomeAssistant auf der Registerkarte \"Extensions\".", "title": "HomeAssistant in Roon autorisieren" diff --git a/homeassistant/components/roon/translations/el.json b/homeassistant/components/roon/translations/el.json index e88cbed685c..63cda0821b1 100644 --- a/homeassistant/components/roon/translations/el.json +++ b/homeassistant/components/roon/translations/el.json @@ -8,6 +8,13 @@ "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { + "fallback": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + }, + "description": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc\u03c2 \u03bf \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Roon, \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03c3\u03b1\u03c2." + }, "link": { "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03c3\u03c4\u03bf Roon. \u0391\u03c6\u03bf\u03cd \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Roon Core, \u03b1\u03bd\u03bf\u03af\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03ba\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf HomeAssistant \u03c3\u03c4\u03b7\u03bd \u03ba\u03b1\u03c1\u03c4\u03ad\u03bb\u03b1 \u0395\u03c0\u03b5\u03ba\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2.", "title": "\u0395\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf HomeAssistant \u03c3\u03c4\u03bf Roon" diff --git a/homeassistant/components/roon/translations/en.json b/homeassistant/components/roon/translations/en.json index d1f86fbce5f..b0affedc026 100644 --- a/homeassistant/components/roon/translations/en.json +++ b/homeassistant/components/roon/translations/en.json @@ -8,6 +8,13 @@ "unknown": "Unexpected error" }, "step": { + "fallback": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Could not discover Roon server, please enter your Hostname and Port." + }, "link": { "description": "You must authorize Home Assistant in Roon. After you click submit, go to the Roon Core application, open Settings and enable HomeAssistant on the Extensions tab.", "title": "Authorize HomeAssistant in Roon" diff --git a/homeassistant/components/roon/translations/et.json b/homeassistant/components/roon/translations/et.json index bbdf2b5edc5..d091f8e4064 100644 --- a/homeassistant/components/roon/translations/et.json +++ b/homeassistant/components/roon/translations/et.json @@ -8,6 +8,13 @@ "unknown": "Tundmatu viga" }, "step": { + "fallback": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Rooni serverit ei leitud, sisesta hostinimi ja port." + }, "link": { "description": "Pead Roonis koduabilise volitama. Kui oled kl\u00f5psanud nuppu Esita, mine rakendusse Roon Core, ava Seaded ja luba vahekaardil Laiendused Home Assistant.", "title": "Volita HomeAssistant Roonis" diff --git a/homeassistant/components/roon/translations/fr.json b/homeassistant/components/roon/translations/fr.json index b0fb3e6784b..7af98de188a 100644 --- a/homeassistant/components/roon/translations/fr.json +++ b/homeassistant/components/roon/translations/fr.json @@ -8,6 +8,13 @@ "unknown": "Erreur inattendue" }, "step": { + "fallback": { + "data": { + "host": "H\u00f4te", + "port": "Port" + }, + "description": "Impossible de trouver le serveur Roon, veuillez saisir le nom de l'h\u00f4te et le port." + }, "link": { "description": "Vous devez autoriser Home Assistant dans Roon. Apr\u00e8s avoir cliqu\u00e9 sur soumettre, acc\u00e9dez \u00e0 l'application Roon Core, ouvrez Param\u00e8tres et activez Home Assistant dans l'onglet Extensions.", "title": "Autoriser Home Assistant dans Roon" diff --git a/homeassistant/components/roon/translations/he.json b/homeassistant/components/roon/translations/he.json index 9d3230d257e..05781910772 100644 --- a/homeassistant/components/roon/translations/he.json +++ b/homeassistant/components/roon/translations/he.json @@ -8,6 +8,12 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { + "fallback": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + }, "link": { "description": "\u05e2\u05dc\u05d9\u05da \u05dc\u05d0\u05e9\u05e8 \u05dc-Home Assistant \u05d1-Roon. \u05dc\u05d0\u05d7\u05e8 \u05e9\u05ea\u05dc\u05d7\u05e5 \u05e2\u05dc \u05e9\u05dc\u05d7, \u05e2\u05d1\u05d5\u05e8 \u05dc\u05d9\u05d9\u05e9\u05d5\u05dd Roon Core, \u05e4\u05ea\u05d7 \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d5\u05d4\u05e4\u05e2\u05dc \u05d0\u05ea HomeAssistant \u05d1\u05db\u05e8\u05d8\u05d9\u05e1\u05d9\u05d9\u05d4 \u05d4\u05e8\u05d7\u05d1\u05d5\u05ea." }, diff --git a/homeassistant/components/roon/translations/hu.json b/homeassistant/components/roon/translations/hu.json index 7d2b63f0f4b..3a9bcf3f522 100644 --- a/homeassistant/components/roon/translations/hu.json +++ b/homeassistant/components/roon/translations/hu.json @@ -8,15 +8,24 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "fallback": { + "data": { + "host": "C\u00edm", + "port": "Port" + }, + "description": "Nem siker\u00fclt felfedezni a Roon-kiszolg\u00e1l\u00f3t. K\u00e9rj\u00fck, adja meg g\u00e9p c\u00edm\u00e9t \u00e9s portj\u00e1t." + }, "link": { - "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.", + "description": "Enged\u00e9lyeznie kell az Home Assistantot a Roonban. Miut\u00e1n r\u00e1kattintott a Mehet 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": "C\u00edm" }, - "description": "A Roon szerver nem tal\u00e1lhat\u00f3, adja meg a hosztnev\u00e9t vagy c\u00edm\u00e9t" + "description": "A Roon szerver nem tal\u00e1lhat\u00f3, adja meg a hosztnev\u00e9t vagy c\u00edm\u00e9t", + "one": "\u00dcres", + "other": "\u00dcres" } } } diff --git a/homeassistant/components/roon/translations/id.json b/homeassistant/components/roon/translations/id.json index 96b26640320..85c1519e7be 100644 --- a/homeassistant/components/roon/translations/id.json +++ b/homeassistant/components/roon/translations/id.json @@ -8,6 +8,13 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "fallback": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Tidak dapat menemukan server Roon, masukkan Nama Host dan Port Anda." + }, "link": { "description": "Anda harus mengotorisasi Home Assistant di Roon. Setelah Anda mengeklik kirim, buka aplikasi Roon Core, buka Pengaturan dan aktifkan HomeAssistant pada tab Ekstensi.", "title": "Otorisasi HomeAssistant di Roon" diff --git a/homeassistant/components/roon/translations/it.json b/homeassistant/components/roon/translations/it.json index ac5922e1e56..3236ebd0bb2 100644 --- a/homeassistant/components/roon/translations/it.json +++ b/homeassistant/components/roon/translations/it.json @@ -8,6 +8,13 @@ "unknown": "Errore imprevisto" }, "step": { + "fallback": { + "data": { + "host": "Host", + "port": "Porta" + }, + "description": "Impossibile scoprire il server Roon, inserisci il tuo nome host e la porta." + }, "link": { "description": "Devi autorizzare l'Assistente Home in Roon. Dopo aver fatto clic su Invia, passa all'applicazione Roon Core, apri Impostazioni e abilita HomeAssistant nella scheda Estensioni.", "title": "Autorizza HomeAssistant in Roon" @@ -16,7 +23,9 @@ "data": { "host": "Host" }, - "description": "Impossibile individuare il server Roon, inserire l'hostname o l'IP." + "description": "Impossibile individuare il server Roon, inserire l'hostname o l'IP.", + "one": "Vuoto", + "other": "Vuoti" } } } diff --git a/homeassistant/components/roon/translations/ja.json b/homeassistant/components/roon/translations/ja.json index dba8ccc9237..400d982d27c 100644 --- a/homeassistant/components/roon/translations/ja.json +++ b/homeassistant/components/roon/translations/ja.json @@ -8,6 +8,13 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { + "fallback": { + "data": { + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" + }, + "description": "Roon\u30b5\u30fc\u30d0\u30fc\u3092\u691c\u51fa\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30db\u30b9\u30c8\u540d\u3068\u30dd\u30fc\u30c8\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "link": { "description": "Roon\u3067Home Assistant\u3092\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u9001\u4fe1(submit) \u3092\u30af\u30ea\u30c3\u30af\u3057\u305f\u5f8c\u3001Roon Core\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3067\u3001\u8a2d\u5b9a(Settings )\u3092\u958b\u304d\u3001\u6a5f\u80fd\u62e1\u5f35\u30bf\u30d6(extensions tab)\u3067Home Assistant\u3092\u6709\u52b9(enable )\u306b\u3057\u307e\u3059\u3002", "title": "Roon\u3067HomeAssistant\u3092\u8a8d\u8a3c\u3059\u308b" diff --git a/homeassistant/components/roon/translations/nl.json b/homeassistant/components/roon/translations/nl.json index df8fa80b4dd..9aafadc918b 100644 --- a/homeassistant/components/roon/translations/nl.json +++ b/homeassistant/components/roon/translations/nl.json @@ -8,6 +8,13 @@ "unknown": "Onverwachte fout" }, "step": { + "fallback": { + "data": { + "host": "Host", + "port": "Poort" + }, + "description": "Kon de Roon server niet vinden, voer uw Hostnaam en Poort in." + }, "link": { "description": "U moet Home Assistant autoriseren in Roon. Nadat je op verzenden hebt geklikt, ga je naar de Roon Core-applicatie, open je Instellingen en schakel je Home Assistant in op het tabblad Extensies.", "title": "Autoriseer Home Assistant in Roon" diff --git a/homeassistant/components/roon/translations/no.json b/homeassistant/components/roon/translations/no.json index 2558c2c27c7..11eb60a04ce 100644 --- a/homeassistant/components/roon/translations/no.json +++ b/homeassistant/components/roon/translations/no.json @@ -8,6 +8,13 @@ "unknown": "Uventet feil" }, "step": { + "fallback": { + "data": { + "host": "Vert", + "port": "Port" + }, + "description": "Kunne ikke finne Roon-serveren, skriv inn vertsnavnet og porten." + }, "link": { "description": "Du m\u00e5 godkjenne Home Assistant i Roon. N\u00e5r du klikker send inn, g\u00e5r du til Roon Core-programmet, \u00e5pner innstillingene og aktiverer Home Assistant p\u00e5 utvidelser-fanen.", "title": "Autoriser Home Assistant i Roon" diff --git a/homeassistant/components/roon/translations/pl.json b/homeassistant/components/roon/translations/pl.json index fab5fb9657e..b45a7fcb562 100644 --- a/homeassistant/components/roon/translations/pl.json +++ b/homeassistant/components/roon/translations/pl.json @@ -8,6 +8,13 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "fallback": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + }, + "description": "Nie mo\u017cna wykry\u0107 serwera Roon, wprowad\u017a nazw\u0119 hosta i port." + }, "link": { "description": "Musisz autoryzowa\u0107 Home Assistant w Roon. Po klikni\u0119ciu przycisku \"Zatwierd\u017a\", przejd\u017a do aplikacji Roon Core, otw\u00f3rz \"Ustawienia\" i w\u0142\u0105cz Home Assistant w karcie \"Rozszerzenia\" (Extensions).", "title": "Autoryzuj Home Assistant w Roon" diff --git a/homeassistant/components/roon/translations/pt-BR.json b/homeassistant/components/roon/translations/pt-BR.json index a96222b15f3..85628df4a7c 100644 --- a/homeassistant/components/roon/translations/pt-BR.json +++ b/homeassistant/components/roon/translations/pt-BR.json @@ -8,6 +8,13 @@ "unknown": "Erro inesperado" }, "step": { + "fallback": { + "data": { + "host": "Nome do host", + "port": "Porta" + }, + "description": "N\u00e3o foi poss\u00edvel descobrir o servidor Roon, digite seu nome de host e porta." + }, "link": { "description": "Voc\u00ea deve autorizar o Home Assistant no Roon. Depois de clicar em enviar, v\u00e1 para o aplicativo Roon principal, abra Configura\u00e7\u00f5es e habilite o HomeAssistant na aba Extens\u00f5es.", "title": "Autorizar HomeAssistant no Roon" diff --git a/homeassistant/components/roon/translations/ru.json b/homeassistant/components/roon/translations/ru.json index 1bcb07c7695..cc89dc2d2dd 100644 --- a/homeassistant/components/roon/translations/ru.json +++ b/homeassistant/components/roon/translations/ru.json @@ -8,6 +8,13 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "fallback": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 Roon, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442." + }, "link": { "description": "\u041f\u043e\u0441\u043b\u0435 \u043d\u0430\u0436\u0430\u0442\u0438\u044f \u043a\u043d\u043e\u043f\u043a\u0438 \u00ab\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\u00bb \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Roon Core, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u00ab\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u00bb \u0438 \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435 HomeAssistant \u043d\u0430 \u0432\u043a\u043b\u0430\u0434\u043a\u0435 \u00ab\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f\u00bb.", "title": "Roon" diff --git a/homeassistant/components/roon/translations/tr.json b/homeassistant/components/roon/translations/tr.json index 75fedf6383e..c18df90b9b8 100644 --- a/homeassistant/components/roon/translations/tr.json +++ b/homeassistant/components/roon/translations/tr.json @@ -8,6 +8,13 @@ "unknown": "Beklenmeyen hata" }, "step": { + "fallback": { + "data": { + "host": "Sunucu", + "port": "Port" + }, + "description": "Roon sunucusu bulunamad\u0131, l\u00fctfen Ana Bilgisayar Ad\u0131n\u0131z\u0131 ve Ba\u011flant\u0131 Noktan\u0131z\u0131 girin." + }, "link": { "description": "Roon'da HomeAssistant\u0131 yetkilendirmelisiniz. G\u00f6nder'e t\u0131klad\u0131ktan sonra, Roon Core uygulamas\u0131na gidin, Ayarlar'\u0131 a\u00e7\u0131n ve Uzant\u0131lar sekmesinde HomeAssistant'\u0131 etkinle\u015ftirin.", "title": "Roon'da HomeAssistant'\u0131 Yetkilendirme" diff --git a/homeassistant/components/roon/translations/zh-Hant.json b/homeassistant/components/roon/translations/zh-Hant.json index f8f52d11b1c..f887b2d9847 100644 --- a/homeassistant/components/roon/translations/zh-Hant.json +++ b/homeassistant/components/roon/translations/zh-Hant.json @@ -8,6 +8,13 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "fallback": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u627e\u4e0d\u5230 Roon \u4f3a\u670d\u5668\uff0c\u8acb\u8f38\u5165\u4e3b\u6a5f\u540d\u7a31\u53ca\u901a\u8a0a\u57e0\u3002" + }, "link": { "description": "\u5fc5\u9808\u65bc Roon \u4e2d\u8a8d\u8b49 Home Assistant\u3002\u9ede\u9078\u50b3\u9001\u5f8c\u3001\u958b\u555f Roon Core \u61c9\u7528\u7a0b\u5f0f\u3001\u6253\u958b\u8a2d\u5b9a\u4e26\u65bc\u64f4\u5145\uff08Extensions\uff09\u4e2d\u555f\u7528 HomeAssistant\u3002", "title": "\u65bc Roon \u4e2d\u8a8d\u8b49 HomeAssistant" diff --git a/homeassistant/components/rtsp_to_webrtc/translations/hu.json b/homeassistant/components/rtsp_to_webrtc/translations/hu.json index 75c471a6710..5da10dd88e4 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/hu.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/hu.json @@ -2,7 +2,8 @@ "config": { "abort": { "server_failure": "Az RTSPtoWebRTC szerver hib\u00e1t jelzett vissza. Tov\u00e1bbi inform\u00e1ci\u00f3k\u00e9rt ellen\u0151rizze a napl\u00f3kat.", - "server_unreachable": "Nem lehet kommunik\u00e1lni az RTSPtoWebRTC szerverrel. Tov\u00e1bbi inform\u00e1ci\u00f3k\u00e9rt ellen\u0151rizze a napl\u00f3kat." + "server_unreachable": "Nem lehet kommunik\u00e1lni az RTSPtoWebRTC szerverrel. Tov\u00e1bbi inform\u00e1ci\u00f3k\u00e9rt ellen\u0151rizze a napl\u00f3kat.", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "error": { "invalid_url": "\u00c9rv\u00e9nyes RTSPtoWebRTC szerver URL-nek kell lennie, pl. https://example.com", diff --git a/homeassistant/components/sabnzbd/translations/de.json b/homeassistant/components/sabnzbd/translations/de.json new file mode 100644 index 00000000000..9f128f2878f --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "name": "Name", + "path": "Pfad", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/el.json b/homeassistant/components/sabnzbd/translations/el.json new file mode 100644 index 00000000000..f3d461e3dee --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/el.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API" + }, + "step": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/en.json b/homeassistant/components/sabnzbd/translations/en.json index 2336ba4e198..4e857c42b64 100644 --- a/homeassistant/components/sabnzbd/translations/en.json +++ b/homeassistant/components/sabnzbd/translations/en.json @@ -9,11 +9,9 @@ "data": { "api_key": "API Key", "name": "Name", - "url": "URL", - "path": "Path" - }, - "description": "If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/sabnzbd/", - "title": "Sabnzbd" + "path": "Path", + "url": "URL" + } } } } diff --git a/homeassistant/components/sabnzbd/translations/et.json b/homeassistant/components/sabnzbd/translations/et.json new file mode 100644 index 00000000000..b940ab99569 --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/et.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_api_key": "Kehtetu API v\u00f5ti" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "name": "Nimi", + "path": "Rada", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/fr.json b/homeassistant/components/sabnzbd/translations/fr.json new file mode 100644 index 00000000000..9809ccf8a0b --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_api_key": "Cl\u00e9 d'API non valide" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "name": "Nom", + "path": "Chemin d'acc\u00e8s", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/hu.json b/homeassistant/components/sabnzbd/translations/hu.json new file mode 100644 index 00000000000..0136c11fc88 --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "name": "Elnevez\u00e9s", + "path": "\u00datvonal", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/id.json b/homeassistant/components/sabnzbd/translations/id.json new file mode 100644 index 00000000000..caa7e73815e --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_api_key": "Kunci API tidak valid" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "name": "Nama", + "path": "Jalur", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/it.json b/homeassistant/components/sabnzbd/translations/it.json new file mode 100644 index 00000000000..48f2dea100c --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_api_key": "Chiave API non valida" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "name": "Nome", + "path": "Percorso", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/nl.json b/homeassistant/components/sabnzbd/translations/nl.json new file mode 100644 index 00000000000..c737cd0d07e --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_api_key": "Ongeldige API-sleutel" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "name": "Naam", + "path": "Pad", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/no.json b/homeassistant/components/sabnzbd/translations/no.json new file mode 100644 index 00000000000..4da8a925a29 --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_api_key": "Ugyldig API-n\u00f8kkel" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "name": "Navn", + "path": "Sti", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/pl.json b/homeassistant/components/sabnzbd/translations/pl.json new file mode 100644 index 00000000000..b083df2b8dc --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_api_key": "Nieprawid\u0142owy klucz API" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "name": "Nazwa", + "path": "\u015acie\u017cka", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/pt-BR.json b/homeassistant/components/sabnzbd/translations/pt-BR.json new file mode 100644 index 00000000000..b9015b40b14 --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_api_key": "Chave de API inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API", + "name": "Nome", + "path": "Caminho", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/zh-Hant.json b/homeassistant/components/sabnzbd/translations/zh-Hant.json new file mode 100644 index 00000000000..018952ba66c --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_api_key": "API \u91d1\u9470\u7121\u6548" + }, + "step": { + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "name": "\u540d\u7a31", + "path": "\u8def\u5f91", + "url": "\u7db2\u5740" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/bg.json b/homeassistant/components/samsungtv/translations/bg.json index 332b6b62236..2246b4ad954 100644 --- a/homeassistant/components/samsungtv/translations/bg.json +++ b/homeassistant/components/samsungtv/translations/bg.json @@ -6,11 +6,17 @@ "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": { + "invalid_pin": "\u041f\u0418\u041d \u043a\u043e\u0434\u044a\u0442 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d, \u043c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." + }, "flow_title": "{device}", "step": { "confirm": { "title": "Samsung TV" }, + "encrypted_pairing": { + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u041f\u0418\u041d \u043a\u043e\u0434\u0430, \u043f\u043e\u043a\u0430\u0437\u0430\u043d \u043d\u0430 {device}." + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/samsungtv/translations/ca.json b/homeassistant/components/samsungtv/translations/ca.json index e701bdb1d92..95738ae65d9 100644 --- a/homeassistant/components/samsungtv/translations/ca.json +++ b/homeassistant/components/samsungtv/translations/ca.json @@ -12,7 +12,8 @@ "unknown": "Error inesperat" }, "error": { - "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." + "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.", + "invalid_pin": "El PIN \u00e9s inv\u00e0lid; torna-ho a provar." }, "flow_title": "{device}", "step": { @@ -20,8 +21,17 @@ "description": "Vols configurar {device}? Si mai abans has connectat Home Assistant hauries de veure una finestra emergent a la pantalla del televisor demanant autenticaci\u00f3.", "title": "Televisor Samsung" }, + "encrypted_pairing": { + "description": "Introdueix el PIN que es mostra a {device}." + }, + "pairing": { + "description": "Vols configurar {device}? Si mai abans has connectat Home Assistant hauries de veure una finestra emergent a la pantalla del televisor demanant autenticaci\u00f3." + }, "reauth_confirm": { - "description": "Despr\u00e9s d'enviar, tens 30 segons per acceptar la finestra emergent de {device} que sol\u00b7licita autoritzaci\u00f3." + "description": "Despr\u00e9s d'enviar, tens 30 segons per acceptar o introduir el PIN a la finestra emergent de {device} que sol\u00b7licita autoritzaci\u00f3." + }, + "reauth_confirm_encrypted": { + "description": "Introdueix el PIN que es mostra a {device}." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/cs.json b/homeassistant/components/samsungtv/translations/cs.json index d45c9247b4e..4c2241d4caa 100644 --- a/homeassistant/components/samsungtv/translations/cs.json +++ b/homeassistant/components/samsungtv/translations/cs.json @@ -19,6 +19,15 @@ "description": "Chcete nastavit {device}? Pokud jste Home Assistant doposud nikdy nep\u0159ipojili, m\u011bla by se v\u00e1m na televizi zobrazit \u017e\u00e1dost o povolen\u00ed.", "title": "Samsung TV" }, + "encrypted_pairing": { + "description": "Zadejte pros\u00edm PIN zobrazen\u00fd na {device}." + }, + "pairing": { + "description": "Chcete nastavit {device}? Pokud jste Home Assistant doposud nikdy nep\u0159ipojili, m\u011bla by se v\u00e1m na televizi zobrazit \u017e\u00e1dost o povolen\u00ed." + }, + "reauth_confirm_encrypted": { + "description": "Zadejte pros\u00edm PIN zobrazen\u00fd na {device}." + }, "user": { "data": { "host": "Hostitel", diff --git a/homeassistant/components/samsungtv/translations/de.json b/homeassistant/components/samsungtv/translations/de.json index ec5b791626a..bfacbcd4b3c 100644 --- a/homeassistant/components/samsungtv/translations/de.json +++ b/homeassistant/components/samsungtv/translations/de.json @@ -12,7 +12,8 @@ "unknown": "Unerwarteter Fehler" }, "error": { - "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." + "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.", + "invalid_pin": "PIN ist ung\u00fcltig, bitte versuche es erneut." }, "flow_title": "{device}", "step": { @@ -20,8 +21,17 @@ "description": "M\u00f6chtest du Samsung TV {device} einrichten? Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du eine Meldung auf deinem Fernseher sehen, die nach einer Autorisierung fragt. Manuelle Konfigurationen f\u00fcr dieses Fernsehger\u00e4t werden \u00fcberschrieben.", "title": "Samsung TV" }, + "encrypted_pairing": { + "description": "Bitte gib die PIN ein, die auf {device} angezeigt wird." + }, + "pairing": { + "description": "M\u00f6chtest du Samsung TV {device} einrichten? Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du eine Meldung auf deinem Fernseher sehen, die nach einer Autorisierung fragt. Manuelle Konfigurationen f\u00fcr dieses Fernsehger\u00e4t werden \u00fcberschrieben." + }, "reauth_confirm": { - "description": "Akzeptiere nach dem Absenden die Meldung auf {device}, das eine Autorisierung innerhalb von 30 Sekunden anfordert." + "description": "Akzeptiere nach dem Absenden das Popup-Fenster auf {device}, das dich auffordert, sich innerhalb von 30 Sekunden zu autorisieren, oder gib die PIN ein." + }, + "reauth_confirm_encrypted": { + "description": "Bitte gib die PIN ein, die auf {device} angezeigt wird." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/el.json b/homeassistant/components/samsungtv/translations/el.json index aa2ec61978b..0e6c4df03bc 100644 --- a/homeassistant/components/samsungtv/translations/el.json +++ b/homeassistant/components/samsungtv/translations/el.json @@ -12,17 +12,27 @@ "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { - "auth_missing": "\u03a4\u03bf Home Assistant \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03c3\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 Samsung. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c4\u03b7\u03c2 \u0394\u03b9\u03b1\u03c7\u03b5\u03af\u03c1\u03b9\u03c3\u03b7\u03c2 \u03b5\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03ce\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd \u03c4\u03b7\u03c2 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Home Assistant." + "auth_missing": "\u03a4\u03bf Home Assistant \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03c3\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 Samsung. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c4\u03b7\u03c2 \u0394\u03b9\u03b1\u03c7\u03b5\u03af\u03c1\u03b9\u03c3\u03b7\u03c2 \u03b5\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03ce\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd \u03c4\u03b7\u03c2 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Home Assistant.", + "invalid_pin": "\u03a4\u03bf PIN \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac." }, "flow_title": "{device}", "step": { "confirm": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 {device}; \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03b9 \u03c0\u03bf\u03c4\u03ad \u03c4\u03bf Home Assistant \u03c0\u03c1\u03b9\u03bd, \u03b8\u03b1 \u03b4\u03b5\u03af\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03bd\u03b1\u03b4\u03c5\u03cc\u03bc\u03b5\u03bd\u03bf \u03c0\u03b1\u03c1\u03ac\u03b8\u03c5\u03c1\u03bf \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03b6\u03b7\u03c4\u03ac \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7.", + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {device}; \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03b9 \u03c0\u03bf\u03c4\u03ad \u03c4\u03bf Home Assistant \u03c0\u03c1\u03b9\u03bd, \u03b8\u03b1 \u03b4\u03b5\u03af\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03bd\u03b1\u03b4\u03c5\u03cc\u03bc\u03b5\u03bd\u03bf \u03c0\u03b1\u03c1\u03ac\u03b8\u03c5\u03c1\u03bf \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03b6\u03b7\u03c4\u03ac \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7.", "title": "Samsung TV" }, + "encrypted_pairing": { + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf PIN \u03c0\u03bf\u03c5 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7 {device}." + }, + "pairing": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {device}; \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03b9 \u03c0\u03bf\u03c4\u03ad \u03c4\u03bf Home Assistant \u03c0\u03c1\u03b9\u03bd, \u03b8\u03b1 \u03b4\u03b5\u03af\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03bd\u03b1\u03b4\u03c5\u03cc\u03bc\u03b5\u03bd\u03bf \u03c0\u03b1\u03c1\u03ac\u03b8\u03c5\u03c1\u03bf \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03b6\u03b7\u03c4\u03ac \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7." + }, "reauth_confirm": { "description": "\u039c\u03b5\u03c4\u03ac \u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae, \u03b1\u03c0\u03bf\u03b4\u03b5\u03c7\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b4\u03c5\u03cc\u03bc\u03b5\u03bd\u03bf \u03c0\u03b1\u03c1\u03ac\u03b8\u03c5\u03c1\u03bf \u03c3\u03c4\u03b7 {device} \u03c0\u03bf\u03c5 \u03b6\u03b7\u03c4\u03ac \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 \u03b5\u03bd\u03c4\u03cc\u03c2 30 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03bf\u03bb\u03ad\u03c0\u03c4\u03c9\u03bd." }, + "reauth_confirm_encrypted": { + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf PIN \u03c0\u03bf\u03c5 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7 {device}." + }, "user": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", diff --git a/homeassistant/components/samsungtv/translations/en.json b/homeassistant/components/samsungtv/translations/en.json index c4e0e181090..827f9992e46 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" @@ -17,7 +18,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" }, "encrypted_pairing": { "description": "Please enter the PIN displayed on {device}." diff --git a/homeassistant/components/samsungtv/translations/et.json b/homeassistant/components/samsungtv/translations/et.json index 47360f4ed06..94e9db78ec2 100644 --- a/homeassistant/components/samsungtv/translations/et.json +++ b/homeassistant/components/samsungtv/translations/et.json @@ -12,7 +12,8 @@ "unknown": "Tundmatu t\u00f5rge" }, "error": { - "auth_missing": "Tuvastamine nurjus" + "auth_missing": "Tuvastamine nurjus", + "invalid_pin": "PIN on kehtetu, proovi uuesti." }, "flow_title": "{devicel}", "step": { @@ -20,8 +21,17 @@ "description": "Kas soovid seadistada {devicel} ? Kui seda pole kunagi enne Home Assistantiga \u00fchendatud, n\u00e4ed oma teleris h\u00fcpikakent, mis k\u00fcsib tuvastamist.", "title": "" }, + "encrypted_pairing": { + "description": "Sisesta seadmes {device} kuvatav PIN-kood." + }, + "pairing": { + "description": "Kas h\u00e4\u00e4lestada seade {device}? Kui varem pole Home Assistantiga \u00fchendutud peaks teler kuvama h\u00fcpikakna tuvastusteabe sisestamiseks." + }, "reauth_confirm": { - "description": "P\u00e4rast esitamist n\u00f5ustu {device} h\u00fcpikaknaga, mis taotleb autoriseerimist 30 sekundi jooksul." + "description": "P\u00e4rast esitamist n\u00f5ustu {device} h\u00fcpikaknaga, mis taotleb autoriseerimist 30 sekundi jooksul v\u00f5i sisesta PIN." + }, + "reauth_confirm_encrypted": { + "description": "Sisesta seadmes {device} kuvatav PIN-kood." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/fr.json b/homeassistant/components/samsungtv/translations/fr.json index 1d9bebb5569..438336f2818 100644 --- a/homeassistant/components/samsungtv/translations/fr.json +++ b/homeassistant/components/samsungtv/translations/fr.json @@ -12,16 +12,26 @@ "unknown": "Erreur inattendue" }, "error": { - "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." + "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.", + "invalid_pin": "Le code PIN n'est pas valide, veuillez r\u00e9essayer." }, "flow_title": "{device}", "step": { "confirm": { - "description": "Voulez vous installer la TV {device} Samsung? Si vous n'avez jamais connect\u00e9 Home Assistant avant, vous devriez voir une fen\u00eatre contextuelle sur votre t\u00e9l\u00e9viseur demandant une authentification. Les configurations manuelles de ce t\u00e9l\u00e9viseur seront \u00e9cras\u00e9es.", + "description": "Voulez-vous configurer {device}\u00a0? Si vous n'avez jamais connect\u00e9 Home Assistant auparavant, une fen\u00eatre contextuelle d'autorisation devrait appara\u00eetre sur votre t\u00e9l\u00e9viseur.", "title": "TV Samsung" }, + "encrypted_pairing": { + "description": "Veuillez saisir le code PIN affich\u00e9 sur {device}." + }, + "pairing": { + "description": "Voulez-vous configurer {device}\u00a0? Si vous n'avez jamais connect\u00e9 Home Assistant auparavant, une fen\u00eatre contextuelle d'autorisation devrait appara\u00eetre sur votre t\u00e9l\u00e9viseur." + }, "reauth_confirm": { - "description": "Apr\u00e8s avoir soumis, acceptez la fen\u00eatre contextuelle sur {device} demandant l'autorisation dans les 30 secondes." + "description": "Une fois envoy\u00e9, acceptez la fen\u00eatre contextuelle de demande d'autorisation sur {device} dans les 30\u00a0secondes ou saisissez le code PIN." + }, + "reauth_confirm_encrypted": { + "description": "Veuillez saisir le code PIN affich\u00e9 sur {device}." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/hu.json b/homeassistant/components/samsungtv/translations/hu.json index a85f295317d..dc42596e290 100644 --- a/homeassistant/components/samsungtv/translations/hu.json +++ b/homeassistant/components/samsungtv/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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "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.", @@ -12,7 +12,8 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { - "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." + "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.", + "invalid_pin": "A PIN k\u00f3d \u00e9rv\u00e9nytelen, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra." }, "flow_title": "{device}", "step": { @@ -20,13 +21,22 @@ "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" }, + "encrypted_pairing": { + "description": "K\u00e9rj\u00fck, adja meg az eszk\u00f6z\u00f6n megjelen\u0151 PIN-k\u00f3dot: {device}" + }, + "pairing": { + "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." + }, "reauth_confirm": { - "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." + "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, vagy adja meg a PIN k\u00f3dot." + }, + "reauth_confirm_encrypted": { + "description": "K\u00e9rj\u00fck, adja meg az eszk\u00f6z\u00f6n megjelen\u0151 PIN-k\u00f3dot: {device}" }, "user": { "data": { "host": "C\u00edm", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" }, "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/samsungtv/translations/id.json b/homeassistant/components/samsungtv/translations/id.json index 0714af37146..394cfc41ae1 100644 --- a/homeassistant/components/samsungtv/translations/id.json +++ b/homeassistant/components/samsungtv/translations/id.json @@ -12,7 +12,8 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "error": { - "auth_missing": "Home Assistant tidak diizinkan untuk tersambung ke TV Samsung ini. Periksa Manajer Perangkat Eksternal 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.", + "invalid_pin": "PIN tidak valid, silakan coba lagi." }, "flow_title": "{device}", "step": { @@ -20,8 +21,17 @@ "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" }, + "encrypted_pairing": { + "description": "Masukkan PIN yang ditampilkan di {device} ." + }, + "pairing": { + "description": "Apakah Anda ingin menyiapkan {device}? Jika Anda belum pernah menyambungkan Home Assistant sebelumnya, Anda akan melihat dialog di TV yang meminta otorisasi." + }, "reauth_confirm": { - "description": "Setelah mengirimkan, setujui pada popup di {device} yang meminta otorisasi dalam waktu 30 detik." + "description": "Setelah mengirimkan, setujui pada popup di {device} yang meminta otorisasi dalam waktu 30 detik atau masukkan PIN." + }, + "reauth_confirm_encrypted": { + "description": "Masukkan PIN yang ditampilkan di {device} ." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/it.json b/homeassistant/components/samsungtv/translations/it.json index 51f9b4e2ef9..6bfc26fb445 100644 --- a/homeassistant/components/samsungtv/translations/it.json +++ b/homeassistant/components/samsungtv/translations/it.json @@ -12,7 +12,8 @@ "unknown": "Errore imprevisto" }, "error": { - "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." + "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.", + "invalid_pin": "Il PIN non \u00e8 valido, riprova." }, "flow_title": "{device}", "step": { @@ -20,8 +21,17 @@ "description": "Vuoi configurare {device}? Se non hai mai collegato Home Assistant, dovresti vedere un popup sulla tua TV che chiede l'autorizzazione.", "title": "Samsung TV" }, + "encrypted_pairing": { + "description": "Digita il PIN visualizzato su {device}." + }, + "pairing": { + "description": "Vuoi configurare {device}? Se non hai mai collegato Home Assistant, dovresti vedere un popup sulla tua TV che chiede l'autorizzazione." + }, "reauth_confirm": { - "description": "Dopo l'invio, accetta il popup su {device} richiedendo l'autorizzazione entro 30 secondi." + "description": "Dopo l'invio, accetta la notifica su {device} richiedendo l'autorizzazione entro 30 secondi o digita il PIN." + }, + "reauth_confirm_encrypted": { + "description": "Digita il PIN visualizzato su {device}." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/ja.json b/homeassistant/components/samsungtv/translations/ja.json index 0cfad60efcb..d1b5670e25c 100644 --- a/homeassistant/components/samsungtv/translations/ja.json +++ b/homeassistant/components/samsungtv/translations/ja.json @@ -12,7 +12,8 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { - "auth_missing": "Home Assistant\u306f\u3001\u3053\u306eSamsungTV\u3078\u306e\u63a5\u7d9a\u3092\u8a31\u53ef\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c6\u30ec\u30d3\u306e\u5916\u90e8\u30c7\u30d0\u30a4\u30b9\u30de\u30cd\u30fc\u30b8\u30e3\u30fc\u306e\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u3001Home Assistant\u3092\u8a8d\u8a3c\u3057\u307e\u3059\u3002" + "auth_missing": "Home Assistant\u306f\u3001\u3053\u306eSamsungTV\u3078\u306e\u63a5\u7d9a\u3092\u8a31\u53ef\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c6\u30ec\u30d3\u306e\u5916\u90e8\u30c7\u30d0\u30a4\u30b9\u30de\u30cd\u30fc\u30b8\u30e3\u30fc\u306e\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u3001Home Assistant\u3092\u8a8d\u8a3c\u3057\u307e\u3059\u3002", + "invalid_pin": "PIN\u304c\u7121\u52b9\u3067\u3059\u3002\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002" }, "flow_title": "{device}", "step": { @@ -20,9 +21,18 @@ "description": "{device} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f\u3053\u308c\u307e\u3067\u306bHome Assistant\u3092\u4e00\u5ea6\u3082\u63a5\u7d9a\u3057\u305f\u3053\u3068\u304c\u306a\u3044\u5834\u5408\u306f\u3001\u30c6\u30ec\u30d3\u306b\u8a8d\u8a3c\u3092\u6c42\u3081\u308b\u30dd\u30c3\u30d7\u30a2\u30c3\u30d7\u304c\u8868\u793a\u3055\u308c\u307e\u3059\u3002", "title": "Samsung TV" }, + "encrypted_pairing": { + "description": "{device} \u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "pairing": { + "description": "{device} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f\u3053\u308c\u307e\u3067\u306bHome Assistant\u3092\u4e00\u5ea6\u3082\u63a5\u7d9a\u3057\u305f\u3053\u3068\u304c\u306a\u3044\u5834\u5408\u306f\u3001\u30c6\u30ec\u30d3\u306b\u8a8d\u8a3c\u3092\u6c42\u3081\u308b\u30dd\u30c3\u30d7\u30a2\u30c3\u30d7\u304c\u8868\u793a\u3055\u308c\u307e\u3059\u3002" + }, "reauth_confirm": { "description": "\u9001\u4fe1(submit)\u3001\u8a8d\u8a3c\u3092\u8981\u6c42\u3059\u308b {device} \u306e\u30dd\u30c3\u30d7\u30a2\u30c3\u30d7\u3092\u300130\u79d2\u4ee5\u5185\u306b\u53d7\u3051\u5165\u308c\u307e\u3059\u3002" }, + "reauth_confirm_encrypted": { + "description": "{device} \u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8", diff --git a/homeassistant/components/samsungtv/translations/nl.json b/homeassistant/components/samsungtv/translations/nl.json index 692c70642d6..b75f9b5970e 100644 --- a/homeassistant/components/samsungtv/translations/nl.json +++ b/homeassistant/components/samsungtv/translations/nl.json @@ -12,7 +12,8 @@ "unknown": "Onverwachte fout" }, "error": { - "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." + "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.", + "invalid_pin": "Pincode is ongeldig, probeer het opnieuw." }, "flow_title": "{device}", "step": { @@ -20,8 +21,17 @@ "description": "Wilt u Samsung TV {device} instellen? Als u nooit eerder Home Assistant hebt verbonden dan zou u een popup op uw TV moeten zien waarin u om toestemming wordt vraagt. Handmatige configuraties voor deze TV worden overschreven", "title": "Samsung TV" }, + "encrypted_pairing": { + "description": "Voer de pincode in die wordt weergegeven op {device} ." + }, + "pairing": { + "description": "Wil je {device} instellen? Als je de Home Assistant nog nooit eerder hebt aangesloten, zou je een pop-up op je tv moeten zien die om autorisatie vraagt." + }, "reauth_confirm": { - "description": "Na het indienen, accepteer binnen 30 seconden de pop-up op {device} om autorisatie toe te staan." + "description": "Na het indienen, accepteer binnen 30 seconden de pop-up op {device} om autorisatie toe te staan of voer PIN in." + }, + "reauth_confirm_encrypted": { + "description": "Voer de pincode in die wordt weergegeven op {device} ." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/no.json b/homeassistant/components/samsungtv/translations/no.json index 7b7108cbf77..2125b407f29 100644 --- a/homeassistant/components/samsungtv/translations/no.json +++ b/homeassistant/components/samsungtv/translations/no.json @@ -12,7 +12,8 @@ "unknown": "Uventet feil" }, "error": { - "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." + "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.", + "invalid_pin": "PIN-koden er ugyldig, pr\u00f8v igjen." }, "flow_title": "{device}", "step": { @@ -20,8 +21,17 @@ "description": "Vil du konfigurere {device} ? Hvis du aldri har koblet til Home Assistant f\u00f8r, b\u00f8r du se en popup p\u00e5 TV-en din som ber om autorisasjon.", "title": "" }, + "encrypted_pairing": { + "description": "Vennligst skriv inn PIN-koden som vises p\u00e5 {device} ." + }, + "pairing": { + "description": "Vil du konfigurere {device} ? Hvis du aldri har koblet til Home Assistant f\u00f8r, b\u00f8r du se en popup p\u00e5 TV-en din som ber om autorisasjon." + }, "reauth_confirm": { - "description": "Etter innsending, godta popup-vinduet p\u00e5 {device} ber om autorisasjon innen 30 sekunder." + "description": "Etter innsending, godta popup-vinduet p\u00e5 {device} ber om autorisasjon innen 30 sekunder eller angi PIN-kode." + }, + "reauth_confirm_encrypted": { + "description": "Vennligst skriv inn PIN-koden som vises p\u00e5 {device} ." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/pl.json b/homeassistant/components/samsungtv/translations/pl.json index 1f810248f92..015f048dc60 100644 --- a/homeassistant/components/samsungtv/translations/pl.json +++ b/homeassistant/components/samsungtv/translations/pl.json @@ -12,7 +12,8 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { - "auth_missing": "Home Assistant nie ma uprawnie\u0144 do po\u0142\u0105czenia si\u0119 z tym telewizorem Samsung. Sprawd\u017a ustawienia \"Mened\u017cera urz\u0105dze\u0144 zewn\u0119trznych\", aby autoryzowa\u0107 Home Assistant." + "auth_missing": "Home Assistant nie ma uprawnie\u0144 do po\u0142\u0105czenia si\u0119 z tym telewizorem Samsung. Sprawd\u017a ustawienia \"Mened\u017cera urz\u0105dze\u0144 zewn\u0119trznych\", aby autoryzowa\u0107 Home Assistant.", + "invalid_pin": "Nieprawid\u0142owy kod PIN, spr\u00f3buj ponownie." }, "flow_title": "{device}", "step": { @@ -20,8 +21,17 @@ "description": "Czy chcesz skonfigurowa\u0107 {device}? Je\u015bli nigdy wcze\u015bniej nie \u0142\u0105czy\u0142e\u015b go z Home Assistantem, na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie.", "title": "Samsung TV" }, + "encrypted_pairing": { + "description": "Wprowad\u017a kod PIN wy\u015bwietlony na {device}." + }, + "pairing": { + "description": "Czy chcesz skonfigurowa\u0107 {device}? Je\u015bli nigdy wcze\u015bniej nie \u0142\u0105czy\u0142e\u015b go z Home Assistantem, na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie." + }, "reauth_confirm": { - "description": "Po wys\u0142aniu \u017c\u0105dania, zaakceptuj wyskakuj\u0105ce okienko na {device} z pro\u015bb\u0105 o autoryzacj\u0119 w ci\u0105gu 30 sekund." + "description": "Po wys\u0142aniu \u017c\u0105dania, zaakceptuj wyskakuj\u0105ce okienko na {device} z pro\u015bb\u0105 o autoryzacj\u0119 w ci\u0105gu 30 sekund lub wprowad\u017a PIN." + }, + "reauth_confirm_encrypted": { + "description": "Wprowad\u017a kod PIN wy\u015bwietlony na {device}." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/pt-BR.json b/homeassistant/components/samsungtv/translations/pt-BR.json index 407e9d94d0a..dbb4b50fe93 100644 --- a/homeassistant/components/samsungtv/translations/pt-BR.json +++ b/homeassistant/components/samsungtv/translations/pt-BR.json @@ -12,7 +12,8 @@ "unknown": "Erro inesperado" }, "error": { - "auth_missing": "O Home Assistant n\u00e3o est\u00e1 autorizado a se conectar a esta TV Samsung. Verifique as configura\u00e7\u00f5es do Gerenciador de dispositivos externos da sua TV para autorizar o Home Assistant." + "auth_missing": "O Home Assistant n\u00e3o est\u00e1 autorizado a se conectar a esta TV Samsung. Verifique as configura\u00e7\u00f5es do Gerenciador de dispositivos externos da sua TV para autorizar o Home Assistant.", + "invalid_pin": "O PIN \u00e9 inv\u00e1lido, tente novamente." }, "flow_title": "{device}", "step": { @@ -20,8 +21,17 @@ "description": "Deseja configurar {device}? Se voc\u00ea nunca conectou o Home Assistant antes, aparecer\u00e1 um pop-up na sua TV pedindo autoriza\u00e7\u00e3o.", "title": "TV Samsung" }, + "encrypted_pairing": { + "description": "Insira o PIN exibido em {device}." + }, + "pairing": { + "description": "Deseja configurar {device}? Se voc\u00ea nunca conectou o Home Assistant antes, aparecer\u00e1 um pop-up na sua TV pedindo autoriza\u00e7\u00e3o." + }, "reauth_confirm": { - "description": "Ap\u00f3s o envio, aceite o pop-up em {device} solicitando autoriza\u00e7\u00e3o em 30 segundos." + "description": "Ap\u00f3s o envio, aceite o pop-up em {device} solicitando autoriza\u00e7\u00e3o em 30 segundos ou insira o PIN." + }, + "reauth_confirm_encrypted": { + "description": "Insira o PIN exibido em {device}." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/ru.json b/homeassistant/components/samsungtv/translations/ru.json index 111b30c5488..ee73070cf0e 100644 --- a/homeassistant/components/samsungtv/translations/ru.json +++ b/homeassistant/components/samsungtv/translations/ru.json @@ -12,7 +12,8 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "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." + "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.", + "invalid_pin": "PIN-\u043a\u043e\u0434 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443." }, "flow_title": "{device}", "step": { @@ -20,8 +21,17 @@ "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {device}? \u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0438\u043a\u043e\u0433\u0434\u0430 \u0440\u0430\u043d\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u043b\u0438 \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung" }, + "encrypted_pairing": { + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u044b\u0439 \u043d\u0430 {device}." + }, + "pairing": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {device}? \u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0438\u043a\u043e\u0433\u0434\u0430 \u0440\u0430\u043d\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u043b\u0438 \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438." + }, "reauth_confirm": { - "description": "\u041f\u043e\u0441\u043b\u0435 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u044d\u0442\u043e\u0439 \u0444\u043e\u0440\u043c\u044b, \u043f\u0440\u0438\u043c\u0438\u0442\u0435 \u0437\u0430\u043f\u0440\u043e\u0441 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u043e \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u043c \u043e\u043a\u043d\u0435 \u043d\u0430 {device} \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 30 \u0441\u0435\u043a\u0443\u043d\u0434." + "description": "\u041f\u043e\u0441\u043b\u0435 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u044d\u0442\u043e\u0439 \u0444\u043e\u0440\u043c\u044b, \u043f\u0440\u0438\u043c\u0438\u0442\u0435 \u0437\u0430\u043f\u0440\u043e\u0441 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u043e \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u043c \u043e\u043a\u043d\u0435 \u043d\u0430 {device} \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 30 \u0441\u0435\u043a\u0443\u043d\u0434 \u0438\u043b\u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434." + }, + "reauth_confirm_encrypted": { + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u044b\u0439 \u043d\u0430 {device}." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/tr.json b/homeassistant/components/samsungtv/translations/tr.json index 399f135bbe8..668b321e26d 100644 --- a/homeassistant/components/samsungtv/translations/tr.json +++ b/homeassistant/components/samsungtv/translations/tr.json @@ -12,7 +12,8 @@ "unknown": "Beklenmeyen hata" }, "error": { - "auth_missing": "Home Assistant'\u0131n bu Samsung TV'ye ba\u011flanma izni yok. Home Assistant'\u0131 yetkilendirmek i\u00e7in l\u00fctfen TV'nin ayarlar\u0131n\u0131 kontrol et." + "auth_missing": "Home Assistant'\u0131n bu Samsung TV'ye ba\u011flanma izni yok. Home Assistant'\u0131 yetkilendirmek i\u00e7in l\u00fctfen TV'nin ayarlar\u0131n\u0131 kontrol et.", + "invalid_pin": "PIN ge\u00e7ersiz, l\u00fctfen tekrar deneyin." }, "flow_title": "{device}", "step": { @@ -20,8 +21,17 @@ "description": "{device} kurulumunu yapmak istiyor musunuz? Home Assistant'\u0131 daha \u00f6nce hi\u00e7 ba\u011flamad\u0131ysan\u0131z, TV'nizde yetki isteyen bir a\u00e7\u0131l\u0131r pencere g\u00f6rmelisiniz.", "title": "Samsung TV" }, + "encrypted_pairing": { + "description": "L\u00fctfen {device} \u00fczerinde g\u00f6r\u00fcnt\u00fclenen PIN'i girin." + }, + "pairing": { + "description": "{device} kurulumunu yapmak istiyor musunuz? Home Assistant'\u0131 daha \u00f6nce hi\u00e7 ba\u011flamad\u0131ysan\u0131z, TV'nizde yetki isteyen bir a\u00e7\u0131l\u0131r pencere g\u00f6rmelisiniz." + }, "reauth_confirm": { - "description": "G\u00f6nderdikten sonra, 30 saniye i\u00e7inde yetkilendirme isteyen {device} \u00fczerindeki a\u00e7\u0131l\u0131r pencereyi kabul edin." + "description": "G\u00f6nderdikten sonra, 30 saniye i\u00e7inde yetkilendirme isteyen {device} \u00fczerindeki a\u00e7\u0131l\u0131r pencereyi kabul edin veya PIN'i girin." + }, + "reauth_confirm_encrypted": { + "description": "L\u00fctfen {device} \u00fczerinde g\u00f6r\u00fcnt\u00fclenen PIN'i girin." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/zh-Hant.json b/homeassistant/components/samsungtv/translations/zh-Hant.json index ba828665cea..8f30090dd24 100644 --- a/homeassistant/components/samsungtv/translations/zh-Hant.json +++ b/homeassistant/components/samsungtv/translations/zh-Hant.json @@ -12,7 +12,8 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { - "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" + "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", + "invalid_pin": "PIN \u78bc\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002" }, "flow_title": "{device}", "step": { @@ -20,8 +21,17 @@ "description": "\u662f\u5426\u8981\u8a2d\u5b9a {device}\uff1f\u5047\u5982\u60a8\u4e4b\u524d\u672a\u66fe\u9023\u7dda\u81f3 Home Assistant\uff0c\u61c9\u8a72\u6703\u65bc\u96fb\u8996\u4e0a\u6536\u5230\u9a57\u8b49\u8a0a\u606f\u3002", "title": "\u4e09\u661f\u96fb\u8996" }, + "encrypted_pairing": { + "description": "\u8acb\u8f38\u5165\u986f\u793a\u65bc {device} \u4e0a\u7684 PIN \u78bc\u3002" + }, + "pairing": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {device}\uff1f\u5047\u5982\u60a8\u4e4b\u524d\u672a\u66fe\u9023\u7dda\u81f3 Home Assistant\uff0c\u61c9\u8a72\u6703\u65bc\u96fb\u8996\u4e0a\u6536\u5230\u9a57\u8b49\u8a0a\u606f\u3002" + }, "reauth_confirm": { - "description": "\u50b3\u9001\u5f8c\u3001\u8acb\u65bc 30 \u79d2\u5167\u540c\u610f {device} \u4e0a\u9396\u986f\u793a\u7684\u5f48\u51fa\u8996\u7a97\u6388\u6b0a\u8a31\u53ef\u3002" + "description": "\u50b3\u9001\u5f8c\u3001\u8acb\u65bc 30 \u79d2\u5167\u540c\u610f {device} \u4e0a\u9396\u986f\u793a\u7684\u5f48\u51fa\u8996\u7a97\u6388\u6b0a\u8a31\u53ef\u6216\u8f38\u5165 PIN\u3002" + }, + "reauth_confirm_encrypted": { + "description": "\u8acb\u8f38\u5165\u986f\u793a\u65bc {device} \u4e0a\u7684 PIN \u78bc\u3002" }, "user": { "data": { diff --git a/homeassistant/components/screenlogic/translations/fr.json b/homeassistant/components/screenlogic/translations/fr.json index 62f26aae2b6..458deec97de 100644 --- a/homeassistant/components/screenlogic/translations/fr.json +++ b/homeassistant/components/screenlogic/translations/fr.json @@ -20,7 +20,7 @@ "data": { "selected_gateway": "Passerelle" }, - "description": "Les passerelles ScreenLogic suivantes ont \u00e9t\u00e9 d\u00e9couvertes. S\u2019il vous pla\u00eet s\u00e9lectionner un \u00e0 configurer, ou choisissez de configurer manuellement une passerelle ScreenLogic.", + "description": "Les passerelles ScreenLogic suivantes ont \u00e9t\u00e9 d\u00e9couvertes. Veuillez s\u00e9lectionner celle \u00e0 configurer. Vous pouvez \u00e9galement choisir de configurer une passerelle ScreenLogic manuellement.", "title": "ScreenLogic" } } diff --git a/homeassistant/components/season/translations/cs.json b/homeassistant/components/season/translations/cs.json new file mode 100644 index 00000000000..17ff202bb38 --- /dev/null +++ b/homeassistant/components/season/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena" + }, + "step": { + "user": { + "data": { + "type": "Typ definice sez\u00f3ny" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/cs.json b/homeassistant/components/sense/translations/cs.json index 59984b18992..aa5613d3ab4 100644 --- a/homeassistant/components/sense/translations/cs.json +++ b/homeassistant/components/sense/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": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", @@ -9,6 +10,12 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_validate": { + "data": { + "password": "Heslo" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, "user": { "data": { "email": "E-mail", diff --git a/homeassistant/components/sense/translations/fr.json b/homeassistant/components/sense/translations/fr.json index 000240517b9..ece97b4df4f 100644 --- a/homeassistant/components/sense/translations/fr.json +++ b/homeassistant/components/sense/translations/fr.json @@ -14,6 +14,7 @@ "data": { "password": "Mot de passe" }, + "description": "L'int\u00e9gration Sense doit r\u00e9-authentifier votre compte {email}.", "title": "R\u00e9-authentifier l'int\u00e9gration" }, "user": { @@ -27,7 +28,8 @@ "validation": { "data": { "code": "Code de v\u00e9rification" - } + }, + "title": "Authentification multi-facteurs Sense" } } } diff --git a/homeassistant/components/sensibo/translations/hu.json b/homeassistant/components/sensibo/translations/hu.json index 802b718cc82..065a7b982ed 100644 --- a/homeassistant/components/sensibo/translations/hu.json +++ b/homeassistant/components/sensibo/translations/hu.json @@ -20,7 +20,7 @@ "user": { "data": { "api_key": "API kulcs", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" } } } diff --git a/homeassistant/components/sensor/translations/pl.json b/homeassistant/components/sensor/translations/pl.json index 05c0a69708c..b6e93865425 100644 --- a/homeassistant/components/sensor/translations/pl.json +++ b/homeassistant/components/sensor/translations/pl.json @@ -7,8 +7,8 @@ "is_carbon_monoxide": "obecny poziom st\u0119\u017cenia tlenku w\u0119gla w {entity_name}", "is_current": "obecne nat\u0119\u017cenie pr\u0105du {entity_name}", "is_energy": "obecna energia {entity_name}", - "is_frequency": "Obecna cz\u0119stotliwo\u015b\u0107 {entity_name}", - "is_gas": "Obecny poziom gazu {entity_name}", + "is_frequency": "obecna cz\u0119stotliwo\u015b\u0107 {entity_name}", + "is_gas": "obecny poziom gazu {entity_name}", "is_humidity": "obecna wilgotno\u015b\u0107 {entity_name}", "is_illuminance": "obecne nat\u0119\u017cenie o\u015bwietlenia {entity_name}", "is_nitrogen_dioxide": "obecny poziom st\u0119\u017cenia dwutlenku azotu {entity_name}", diff --git a/homeassistant/components/sentry/translations/ca.json b/homeassistant/components/sentry/translations/ca.json index 25d5a5bf37e..d83abb16f1e 100644 --- a/homeassistant/components/sentry/translations/ca.json +++ b/homeassistant/components/sentry/translations/ca.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "dsn": "DSN" + "dsn": "Sentry DSN" }, "description": "Introdueix el DSN de Sentry", "title": "Sentry" diff --git a/homeassistant/components/sentry/translations/de.json b/homeassistant/components/sentry/translations/de.json index 62aca6a65e9..d408af0d885 100644 --- a/homeassistant/components/sentry/translations/de.json +++ b/homeassistant/components/sentry/translations/de.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "dsn": "DSN" + "dsn": "Sentry-DSN" }, "description": "Gib deine Sentry-DSN ein", "title": "Sentry" diff --git a/homeassistant/components/sentry/translations/en.json b/homeassistant/components/sentry/translations/en.json index f00116c85d9..16bdac6b510 100644 --- a/homeassistant/components/sentry/translations/en.json +++ b/homeassistant/components/sentry/translations/en.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "dsn": "DSN" + "dsn": "Sentry DSN" }, "description": "Enter your Sentry DSN", "title": "Sentry" diff --git a/homeassistant/components/sentry/translations/et.json b/homeassistant/components/sentry/translations/et.json index 5c501b2b0aa..01386fa97e6 100644 --- a/homeassistant/components/sentry/translations/et.json +++ b/homeassistant/components/sentry/translations/et.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "dsn": "" + "dsn": "Sentry DSN" }, "description": "Sisesta oma Sentry DSN", "title": "" diff --git a/homeassistant/components/sentry/translations/fr.json b/homeassistant/components/sentry/translations/fr.json index acad5566ec3..6850bfe8a71 100644 --- a/homeassistant/components/sentry/translations/fr.json +++ b/homeassistant/components/sentry/translations/fr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "dsn": "DSN" + "dsn": "DSN Sentry" }, "description": "Entrez votre DSN Sentry", "title": "Sentry" diff --git a/homeassistant/components/sentry/translations/he.json b/homeassistant/components/sentry/translations/he.json index 3383895686e..2a89b9e5556 100644 --- a/homeassistant/components/sentry/translations/he.json +++ b/homeassistant/components/sentry/translations/he.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "dsn": "DSN" + "dsn": "Sentry DSN" } } } diff --git a/homeassistant/components/sentry/translations/hu.json b/homeassistant/components/sentry/translations/hu.json index 9c28d57eb5d..0535657c3d0 100644 --- a/homeassistant/components/sentry/translations/hu.json +++ b/homeassistant/components/sentry/translations/hu.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "dsn": "DSN" + "dsn": "Sentry DSN" }, "description": "Adja meg a Sentry DSN-t", "title": "Sentry" diff --git a/homeassistant/components/sentry/translations/id.json b/homeassistant/components/sentry/translations/id.json index b2004e8e3d5..5f81eb1a445 100644 --- a/homeassistant/components/sentry/translations/id.json +++ b/homeassistant/components/sentry/translations/id.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "dsn": "DSN" + "dsn": "Sentry DSN" }, "description": "Masukkan DSN Sentry Anda", "title": "Sentry" diff --git a/homeassistant/components/sentry/translations/it.json b/homeassistant/components/sentry/translations/it.json index 9e3e6974883..10a0d1bad2b 100644 --- a/homeassistant/components/sentry/translations/it.json +++ b/homeassistant/components/sentry/translations/it.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "dsn": "DSN" + "dsn": "DSN Sentry" }, "description": "Inserisci il tuo DSN Sentry", "title": "Sentry" diff --git a/homeassistant/components/sentry/translations/nl.json b/homeassistant/components/sentry/translations/nl.json index 50af985b47c..67be245ffde 100644 --- a/homeassistant/components/sentry/translations/nl.json +++ b/homeassistant/components/sentry/translations/nl.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "dsn": "DSN" + "dsn": "Sentry DSN" }, "description": "Voer uw Sentry DSN in", "title": "Sentry" diff --git a/homeassistant/components/sentry/translations/no.json b/homeassistant/components/sentry/translations/no.json index 31f4ef30d12..c3bb4acd26e 100644 --- a/homeassistant/components/sentry/translations/no.json +++ b/homeassistant/components/sentry/translations/no.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "dsn": "DSN" + "dsn": "Sentry DSN" }, "description": "Fyll inn din Sentry DNS", "title": "" diff --git a/homeassistant/components/sentry/translations/pl.json b/homeassistant/components/sentry/translations/pl.json index 8d1bc2092aa..be5d008dcff 100644 --- a/homeassistant/components/sentry/translations/pl.json +++ b/homeassistant/components/sentry/translations/pl.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "dsn": "DSN" + "dsn": "Sentry DSN" }, "description": "Wprowad\u017a DSN Sentry", "title": "Sentry" diff --git a/homeassistant/components/sentry/translations/pt-BR.json b/homeassistant/components/sentry/translations/pt-BR.json index 21f1e3ef91a..c489de8ee6d 100644 --- a/homeassistant/components/sentry/translations/pt-BR.json +++ b/homeassistant/components/sentry/translations/pt-BR.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "dsn": "DSN" + "dsn": "Sentinela DSN" }, "description": "Digite seu DSN Sentry", "title": "Sentry" diff --git a/homeassistant/components/sentry/translations/ru.json b/homeassistant/components/sentry/translations/ru.json index eb6cbd0310c..6a7192a5385 100644 --- a/homeassistant/components/sentry/translations/ru.json +++ b/homeassistant/components/sentry/translations/ru.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "dsn": "DSN" + "dsn": "Sentry DSN" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448 DSN Sentry", "title": "Sentry" diff --git a/homeassistant/components/sentry/translations/tr.json b/homeassistant/components/sentry/translations/tr.json index fa3f39e9cde..6369f727d17 100644 --- a/homeassistant/components/sentry/translations/tr.json +++ b/homeassistant/components/sentry/translations/tr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "dsn": "DSN" + "dsn": "Sentry DSN" }, "description": "Sentry DSN'nizi girin", "title": "Sentry" diff --git a/homeassistant/components/sentry/translations/zh-Hant.json b/homeassistant/components/sentry/translations/zh-Hant.json index 04fe4682a42..6d4615db892 100644 --- a/homeassistant/components/sentry/translations/zh-Hant.json +++ b/homeassistant/components/sentry/translations/zh-Hant.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "dsn": "DSN" + "dsn": "Sentry DSN" }, "description": "\u8f38\u5165 Sentry DSN", "title": "Sentry" diff --git a/homeassistant/components/senz/translations/bg.json b/homeassistant/components/senz/translations/bg.json new file mode 100644 index 00000000000..ddd6040e9cf --- /dev/null +++ b/homeassistant/components/senz/translations/bg.json @@ -0,0 +1,7 @@ +{ + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/ca.json b/homeassistant/components/senz/translations/ca.json new file mode 100644 index 00000000000..20b2ceceddd --- /dev/null +++ b/homeassistant/components/senz/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "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.", + "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", + "oauth_error": "S'han rebut dades token inv\u00e0lides." + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa" + }, + "step": { + "pick_implementation": { + "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/de.json b/homeassistant/components/senz/translations/de.json new file mode 100644 index 00000000000..16c6d8a883d --- /dev/null +++ b/homeassistant/components/senz/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Diese Komponente ist nicht konfiguriert. Bitte in der Anleitung nachlesen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", + "oauth_error": "Ung\u00fcltige Token-Daten empfangen." + }, + "create_entry": { + "default": "Anmeldung erfolgreich" + }, + "step": { + "pick_implementation": { + "title": "Art der Anmeldung ausw\u00e4hlen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/el.json b/homeassistant/components/senz/translations/el.json new file mode 100644 index 00000000000..cd34896da39 --- /dev/null +++ b/homeassistant/components/senz/translations/el.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2.", + "missing_configuration": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", + "no_url_available": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL. \u0393\u03b9\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1, [\u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b2\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1\u03c2] ( {docs_url} )", + "oauth_error": "\u039b\u03ae\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd." + }, + "create_entry": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "step": { + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/et.json b/homeassistant/components/senz/translations/et.json new file mode 100644 index 00000000000..1ea0537a1b7 --- /dev/null +++ b/homeassistant/components/senz/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp", + "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.", + "no_url_available": "URL pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [spikrijaotis]({docs_url})", + "oauth_error": "Saadi sobimatud loaandmed." + }, + "create_entry": { + "default": "Tuvastamine \u00f5nnestus" + }, + "step": { + "pick_implementation": { + "title": "Vali tuvastusmeetod" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/fr.json b/homeassistant/components/senz/translations/fr.json new file mode 100644 index 00000000000..3190eb10b6d --- /dev/null +++ b/homeassistant/components/senz/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", + "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", + "oauth_error": "Des donn\u00e9es de jeton non valides ont \u00e9t\u00e9 re\u00e7ues." + }, + "create_entry": { + "default": "Authentification r\u00e9ussie" + }, + "step": { + "pick_implementation": { + "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/he.json b/homeassistant/components/senz/translations/he.json new file mode 100644 index 00000000000..fcac267c910 --- /dev/null +++ b/homeassistant/components/senz/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", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\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.", + "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})", + "oauth_error": "\u05d4\u05ea\u05e7\u05d1\u05dc\u05d5 \u05e0\u05ea\u05d5\u05e0\u05d9 \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd." + }, + "create_entry": { + "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" + }, + "step": { + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/hu.json b/homeassistant/components/senz/translations/hu.json new file mode 100644 index 00000000000..a3b07f0d3ef --- /dev/null +++ b/homeassistant/components/senz/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "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\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", + "oauth_error": "\u00c9rv\u00e9nytelen token adatok \u00e9rkeztek." + }, + "create_entry": { + "default": "Sikeres hiteles\u00edt\u00e9s" + }, + "step": { + "pick_implementation": { + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/id.json b/homeassistant/components/senz/translations/id.json new file mode 100644 index 00000000000..a2fdf8837bd --- /dev/null +++ b/homeassistant/components/senz/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", + "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})", + "oauth_error": "Menerima respons token yang tidak valid." + }, + "create_entry": { + "default": "Berhasil diautentikasi" + }, + "step": { + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/it.json b/homeassistant/components/senz/translations/it.json new file mode 100644 index 00000000000..c92ebb2a57c --- /dev/null +++ b/homeassistant/components/senz/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", + "oauth_error": "Ricevuti dati token non validi." + }, + "create_entry": { + "default": "Autenticazione riuscita" + }, + "step": { + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/ja.json b/homeassistant/components/senz/translations/ja.json new file mode 100644 index 00000000000..b6aa94ef30c --- /dev/null +++ b/homeassistant/components/senz/translations/ja.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", + "oauth_error": "\u7121\u52b9\u306a\u30c8\u30fc\u30af\u30f3\u30c7\u30fc\u30bf\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f\u3002" + }, + "create_entry": { + "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" + }, + "step": { + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/nl.json b/homeassistant/components/senz/translations/nl.json new file mode 100644 index 00000000000..0f50cb0f918 --- /dev/null +++ b/homeassistant/components/senz/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", + "oauth_error": "Ongeldige token data ontvangen." + }, + "create_entry": { + "default": "Succesvol geauthenticeerd" + }, + "step": { + "pick_implementation": { + "title": "Kies een authenticatie methode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/no.json b/homeassistant/components/senz/translations/no.json new file mode 100644 index 00000000000..6c384bb2e15 --- /dev/null +++ b/homeassistant/components/senz/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert. Venligst f\u00f8lg dokumentasjonen.", + "no_url_available": "Ingen URL tilgengelig. For mer informasjon om denne feilen, [sjekk hjelp seksjonen]({docs_url})", + "oauth_error": "Mottatt ugyldige token data." + }, + "create_entry": { + "default": "Vellykket Autentisering" + }, + "step": { + "pick_implementation": { + "title": "Velg Autentiserings metode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/pl.json b/homeassistant/components/senz/translations/pl.json new file mode 100644 index 00000000000..d58148cb8fa --- /dev/null +++ b/homeassistant/components/senz/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", + "oauth_error": "Otrzymano nieprawid\u0142owe dane tokena." + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono" + }, + "step": { + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/pt-BR.json b/homeassistant/components/senz/translations/pt-BR.json new file mode 100644 index 00000000000..7e3ff2f64a9 --- /dev/null +++ b/homeassistant/components/senz/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_url_available": "N\u00e3o h\u00e1 URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})", + "oauth_error": "Dados de token recebidos inv\u00e1lidos." + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/ru.json b/homeassistant/components/senz/translations/ru.json new file mode 100644 index 00000000000..2f572831b5b --- /dev/null +++ b/homeassistant/components/senz/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.", + "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.", + "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", + "oauth_error": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u044b \u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0442\u043e\u043a\u0435\u043d\u0430." + }, + "create_entry": { + "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/tr.json b/homeassistant/components/senz/translations/tr.json new file mode 100644 index 00000000000..3f6fa6f27ba --- /dev/null +++ b/homeassistant/components/senz/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})", + "oauth_error": "Ge\u00e7ersiz anahtar verileri al\u0131nd\u0131." + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, + "step": { + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/zh-Hant.json b/homeassistant/components/senz/translations/zh-Hant.json new file mode 100644 index 00000000000..3bf08cf34c7 --- /dev/null +++ b/homeassistant/components/senz/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", + "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", + "oauth_error": "\u6536\u5230\u7121\u6548\u7684\u6b0a\u6756\u8cc7\u6599\u3002" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" + }, + "step": { + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/bg.json b/homeassistant/components/simplisafe/translations/bg.json index 21cdaabf9a2..9d32e18ae5c 100644 --- a/homeassistant/components/simplisafe/translations/bg.json +++ b/homeassistant/components/simplisafe/translations/bg.json @@ -9,11 +9,6 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { - "input_auth_code": { - "data": { - "auth_code": "\u041a\u043e\u0434 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f" - } - }, "reauth_confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json index 3ab643b534f..7c590a052b1 100644 --- a/homeassistant/components/simplisafe/translations/ca.json +++ b/homeassistant/components/simplisafe/translations/ca.json @@ -12,15 +12,7 @@ "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" }, "reauth_confirm": { diff --git a/homeassistant/components/simplisafe/translations/cs.json b/homeassistant/components/simplisafe/translations/cs.json index bbb47121fbc..152c0282216 100644 --- a/homeassistant/components/simplisafe/translations/cs.json +++ b/homeassistant/components/simplisafe/translations/cs.json @@ -11,7 +11,6 @@ }, "step": { "mfa": { - "description": "Zkontrolujte, zda v\u00e1m do e-mail p\u0159i\u0161el odkaz od SimpliSafe. Po ov\u011b\u0159en\u00ed odkazu se vra\u0165te sem a dokon\u010dete instalaci integrace.", "title": "V\u00edcefaktorov\u00e9 ov\u011b\u0159ov\u00e1n\u00ed SimpliSafe" }, "reauth_confirm": { diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index 1ee4802e77f..51083565770 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -6,38 +6,37 @@ "wrong_account": "Die angegebenen Benutzeranmeldeinformationen stimmen nicht mit diesem SimpliSafe-Konto \u00fcberein." }, "error": { + "2fa_timed_out": "Zeit\u00fcberschreitung beim Warten auf Zwei-Faktor-Authentifizierung", "identifier_exists": "Konto bereits registriert", "invalid_auth": "Ung\u00fcltige Authentifizierung", "still_awaiting_mfa": "Immernoch warten auf MFA-E-Mail-Klick", "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" }, "reauth_confirm": { "data": { "password": "Passwort" }, - "description": "Dein Zugriffstoken ist abgelaufen oder wurde widerrufen. Gib dein Passwort ein, um dein Konto erneut zu verkn\u00fcpfen.", + "description": "Bitte gib das Passwort f\u00fcr {username} erneut ein.", "title": "Integration erneut authentifizieren" }, + "sms_2fa": { + "data": { + "code": "Code" + }, + "description": "Gib den Code f\u00fcr die Zwei-Faktor-Authentifizierung ein, den du per SMS erhalten hast." + }, "user": { "data": { "auth_code": "Autorisierungscode", "code": "Code (wird in der Benutzeroberfl\u00e4che von Home Assistant verwendet)", "password": "Passwort", - "username": "E-Mail" + "username": "Benutzername" }, - "description": "SimpliSafe authentifiziert sich bei Home Assistant \u00fcber die SimpliSafe Web-App. Aufgrund technischer Beschr\u00e4nkungen gibt es am Ende dieses Prozesses einen manuellen Schritt; bitte stelle sicher, dass du die [Dokumentation]({docs_url}) liest, bevor du beginnst.\n\n1. Klicke [hier]({url}), um die SimpliSafe-Webanwendung zu \u00f6ffnen und deine Anmeldedaten einzugeben.\n\n2. Wenn der Anmeldevorgang abgeschlossen ist, kehre hierher zur\u00fcck und gib unten stehenden Autorisierungscode ein.", + "description": "Gib deinen Benutzernamen und Passwort ein.", "title": "Gib deine Informationen ein" } } diff --git a/homeassistant/components/simplisafe/translations/el.json b/homeassistant/components/simplisafe/translations/el.json index d35c59bcc40..880097c6cef 100644 --- a/homeassistant/components/simplisafe/translations/el.json +++ b/homeassistant/components/simplisafe/translations/el.json @@ -6,21 +6,14 @@ "wrong_account": "\u03a4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03bf\u03c5\u03bd \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc SimpliSafe." }, "error": { + "2fa_timed_out": "\u0388\u03bb\u03b7\u03be\u03b5 \u03c4\u03bf \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae \u03b3\u03b9\u03b1 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd", "identifier_exists": "\u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ae\u03b4\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "still_awaiting_mfa": "\u0391\u03bd\u03b1\u03bc\u03ad\u03bd\u03b5\u03c4\u03b1\u03b9 \u03b1\u03ba\u03cc\u03bc\u03b7 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf email \u03c4\u03bf\u03c5 \u03a5\u03c0\u03bf\u03c5\u03c1\u03b3\u03b5\u03af\u03bf\u03c5 \u039f\u03b9\u03ba\u03bf\u03bd\u03bf\u03bc\u03b9\u03ba\u03ce\u03bd", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { - "input_auth_code": { - "data": { - "auth_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2" - }, - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c4\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 SimpliSafe web:", - "title": "\u039f\u03bb\u03bf\u03ba\u03bb\u03ae\u03c1\u03c9\u03c3\u03b7 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2" - }, "mfa": { - "description": "\u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf email \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03ad\u03bd\u03b1\u03bd \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd SimpliSafe. \u0391\u03c6\u03bf\u03cd \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf, \u03b5\u03c0\u03b9\u03c3\u03c4\u03c1\u03ad\u03c8\u03c4\u03b5 \u03b5\u03b4\u03ce \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2.", "title": "\u03a0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd SimpliSafe" }, "reauth_confirm": { @@ -30,6 +23,12 @@ "description": "\u0397 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03bb\u03ae\u03be\u03b5\u03b9 \u03ae \u03b1\u03bd\u03b1\u03ba\u03bb\u03b7\u03b8\u03b5\u03af. \u0395\u03b9\u03c3\u03ac\u03b3\u03b5\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 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2.", "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" }, + "sms_2fa": { + "data": { + "code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd \u03c0\u03bf\u03c5 \u03c3\u03b1\u03c2 \u03b5\u03c3\u03c4\u03ac\u03bb\u03b7 \u03bc\u03ad\u03c3\u03c9 SMS." + }, "user": { "data": { "auth_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 5dd146d757b..5d8cc4b4e1c 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -2,14 +2,20 @@ "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": { "2fa_timed_out": "Timed out while waiting for two-factor authentication", + "identifier_exists": "Account already registered", "invalid_auth": "Invalid authentication", + "still_awaiting_mfa": "Still awaiting MFA email click", "unknown": "Unexpected error" }, "step": { + "mfa": { + "title": "SimpliSafe Multi-Factor Authentication" + }, "reauth_confirm": { "data": { "password": "Password" @@ -25,10 +31,13 @@ }, "user": { "data": { + "auth_code": "Authorization Code", + "code": "Code (used in Home Assistant UI)", "password": "Password", "username": "Username" }, - "description": "Input your username and password." + "description": "Input your username and password.", + "title": "Fill in your information." } } }, diff --git a/homeassistant/components/simplisafe/translations/es.json b/homeassistant/components/simplisafe/translations/es.json index 690e01206d3..d4b066890f1 100644 --- a/homeassistant/components/simplisafe/translations/es.json +++ b/homeassistant/components/simplisafe/translations/es.json @@ -12,15 +12,7 @@ "unknown": "Error inesperado" }, "step": { - "input_auth_code": { - "data": { - "auth_code": "C\u00f3digo de Autorizaci\u00f3n" - }, - "description": "Introduzca el c\u00f3digo de autorizaci\u00f3n desde la URL de la aplicaci\u00f3n web de SimpliSafe:", - "title": "Terminar Autorizaci\u00f3n" - }, "mfa": { - "description": "Comprueba tu correo electr\u00f3nico para obtener un enlace desde SimpliSafe. Despu\u00e9s de verificar el enlace, vulve aqu\u00ed para completar la instalaci\u00f3n de la integraci\u00f3n.", "title": "Autenticaci\u00f3n Multi-Factor SimpliSafe" }, "reauth_confirm": { diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json index 97e59415123..83c441de9f9 100644 --- a/homeassistant/components/simplisafe/translations/et.json +++ b/homeassistant/components/simplisafe/translations/et.json @@ -6,38 +6,37 @@ "wrong_account": "Esitatud kasutaja mandaadid ei \u00fchti selle SimpliSafe kontoga." }, "error": { + "2fa_timed_out": "Kahefaktoriline autentimine aegus", "identifier_exists": "Konto on juba registreeritud", "invalid_auth": "Tuvastamise viga", "still_awaiting_mfa": "Ootan endiselt MFA e-posti klikki", "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" }, "reauth_confirm": { "data": { "password": "Salas\u00f5na" }, - "description": "Juurdep\u00e4\u00e4suluba on aegunud v\u00f5i on see t\u00fchistatud. Konto taassidumiseks sisesta salas\u00f5na.", + "description": "Taassisesta salas\u00f5na kasutajanimele {username}.", "title": "Taastuvasta SimpliSafe'i konto" }, + "sms_2fa": { + "data": { + "code": "Kood" + }, + "description": "Sisesta SMS-iga saadetud kahefaktoriline autentimiskood." + }, "user": { "data": { "auth_code": "Tuvastuskood", "code": "Kood (kasutatakse Home Assistant'i kasutajaliideses)", "password": "Salas\u00f5na", - "username": "E-post" + "username": "Kasutajanimi" }, - "description": "SimpliSafe autendib Home Assistantiga SimpliSafe'i veebirakenduse kaudu. Tehniliste piirangute t\u00f5ttu on selle protsessi l\u00f5pus k\u00e4sitsi samm; enne alustamist loe kindlasti [dokumentatsioon]( {docs_url} \n\n 1. SimpliSafe'i veebirakenduse avamiseks ja oma mandaatide sisestamiseks kl\u00f5psa [siin]( {url} \n\n 2. Kui sisselogimisprotsess on l\u00f5ppenud, naase siia ja sisesta alltoodud tuvastuskood.", + "description": "Sisesta kasutajatunnus ja salas\u00f5na", "title": "Sisesta oma teave." } } diff --git a/homeassistant/components/simplisafe/translations/fr.json b/homeassistant/components/simplisafe/translations/fr.json index d50ac11f851..5ed837d405a 100644 --- a/homeassistant/components/simplisafe/translations/fr.json +++ b/homeassistant/components/simplisafe/translations/fr.json @@ -6,38 +6,37 @@ "wrong_account": "Les informations d'identification d'utilisateur fournies ne correspondent pas \u00e0 ce compte SimpliSafe." }, "error": { + "2fa_timed_out": "D\u00e9lai d'attente de l'authentification \u00e0 deux facteurs expir\u00e9", "identifier_exists": "Compte d\u00e9j\u00e0 enregistr\u00e9", "invalid_auth": "Authentification non valide", "still_awaiting_mfa": "En attente de clic sur le message \u00e9lectronique d'authentification multi facteur", "unknown": "Erreur inattendue" }, "step": { - "input_auth_code": { - "data": { - "auth_code": "Code d'autorisation" - }, - "description": "Saisissez le code d'autorisation \u00e0 partir de l'URL de l'application Web SimpliSafe\u00a0:", - "title": "Terminer l'autorisation" - }, "mfa": { - "description": "V\u00e9rifiez votre messagerie pour un lien de SimpliSafe. Apr\u00e8s avoir v\u00e9rifi\u00e9 le lien, revenez ici pour terminer l'installation de l'int\u00e9gration.", "title": "Authentification multi facteur SimpliSafe" }, "reauth_confirm": { "data": { "password": "Mot de passe" }, - "description": "Votre jeton d'acc\u00e8s a expir\u00e9 ou a \u00e9t\u00e9 r\u00e9voqu\u00e9. Entrez votre mot de passe pour r\u00e9 associer votre compte.", + "description": "Veuillez de nouveau saisir le mot de passe pour {username}.", "title": "R\u00e9-authentifier l'int\u00e9gration" }, + "sms_2fa": { + "data": { + "code": "Code" + }, + "description": "Saisissez le code d'authentification \u00e0 deux facteurs qui vous a \u00e9t\u00e9 envoy\u00e9 par SMS." + }, "user": { "data": { "auth_code": "Code d'autorisation", "code": "Code (utilis\u00e9 dans l'interface Home Assistant)", "password": "Mot de passe", - "username": "Courriel" + "username": "Nom d'utilisateur" }, - "description": "SimpliSafe s'authentifie avec Home Assistant via l'application Web SimpliSafe. En raison de limitations techniques, il y a une \u00e9tape manuelle \u00e0 la fin de ce processus ; veuillez vous assurer de lire la [documentation]( {docs_url} ) avant de commencer. \n\n 1. Cliquez sur [ici]( {url} ) pour ouvrir l'application Web SimpliSafe et saisissez vos informations d'identification. \n\n 2. Une fois le processus de connexion termin\u00e9, revenez ici et saisissez le code d'autorisation ci-dessous.", + "description": "Saisissez votre nom d'utilisateur et votre mot de passe.", "title": "Veuillez saisir vos informations" } } diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index 69881266c4f..e1b8769f4b6 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -6,38 +6,37 @@ "wrong_account": "A megadott felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok nem j\u00f3k ehhez a SimpliSafe fi\u00f3khoz." }, "error": { + "2fa_timed_out": "A k\u00e9tfaktoros hiteles\u00edt\u00e9sre val\u00f3 v\u00e1rakoz\u00e1s ideje lej\u00e1rt", "identifier_exists": "Fi\u00f3k m\u00e1r regisztr\u00e1lva van", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "still_awaiting_mfa": "M\u00e9g v\u00e1r az MFA e-mail kattint\u00e1sra", "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" }, "reauth_confirm": { "data": { "password": "Jelsz\u00f3" }, - "description": "Hozz\u00e1f\u00e9r\u00e9se lej\u00e1rt vagy visszavont\u00e1k. Adja meg jelszav\u00e1t a fi\u00f3k \u00fajb\u00f3li \u00f6sszekapcsol\u00e1s\u00e1hoz.", + "description": "K\u00e9rem, adja meg ism\u00e9t {username} jelszav\u00e1t.", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, + "sms_2fa": { + "data": { + "code": "K\u00f3d" + }, + "description": "\u00cdrja be a k\u00e9tfaktoros hiteles\u00edt\u00e9si k\u00f3dot, amelyet SMS-ben k\u00fcldtek \u00d6nnek." + }, "user": { "data": { "auth_code": "Enged\u00e9lyez\u00e9si k\u00f3d", "code": "K\u00f3d (a Home Assistant felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9n haszn\u00e1latos)", "password": "Jelsz\u00f3", - "username": "E-mail" + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "A SimpliSafe rendszere webalkalmaz\u00e1son kereszt\u00fcl hiteles\u00edt\u00e9si mag\u00e1t. 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]({docs_url}).\n\n1. Ha k\u00e9szen \u00e1ll, kattintson [ide]({url}) a SimpliSafe webalkalmaz\u00e1s megnyit\u00e1s\u00e1hoz \u00e9s a hiteles\u00edt\u0151 adatok bevitel\u00e9hez. \n\n2. Amikor a folyamat befejez\u0151d\u00f6tt, t\u00e9rjen vissza ide, \u00e9s \u00edrja be a k\u00f3dot.", + "description": "Adja meg felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t.", "title": "T\u00f6ltse ki az adatait" } } diff --git a/homeassistant/components/simplisafe/translations/id.json b/homeassistant/components/simplisafe/translations/id.json index 743af691714..ad0ad01be63 100644 --- a/homeassistant/components/simplisafe/translations/id.json +++ b/homeassistant/components/simplisafe/translations/id.json @@ -6,38 +6,37 @@ "wrong_account": "Kredensial pengguna yang diberikan tidak cocok dengan akun SimpliSafe ini." }, "error": { + "2fa_timed_out": "Tenggang waktu habis saat menunggu autentikasi dua faktor", "identifier_exists": "Akun sudah terdaftar", "invalid_auth": "Autentikasi tidak valid", "still_awaiting_mfa": "Masih menunggu pengeklikan dari email MFA", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { - "input_auth_code": { - "data": { - "auth_code": "Kode Otorisasi" - }, - "description": "Masukkan kode otorisasi dari URL aplikasi web SimpliSafe:", - "title": "Selesaikan Otorisasi" - }, "mfa": { - "description": "Periksa email Anda untuk mendapatkan tautan dari SimpliSafe. Setelah memverifikasi tautan, kembali ke sini untuk menyelesaikan instalasi integrasi.", "title": "Autentikasi Multi-Faktor SimpliSafe" }, "reauth_confirm": { "data": { "password": "Kata Sandi" }, - "description": "Akses Anda telah kedaluwarsa atau dicabut. Masukkan kata sandi Anda untuk menautkan kembali akun Anda.", + "description": "Masukkan kembali kata sandi untuk {username}.", "title": "Autentikasi Ulang Integrasi" }, + "sms_2fa": { + "data": { + "code": "Kode" + }, + "description": "Masukkan kode autentikasi dua faktor yang dikirimkan kepada Anda melalui SMS." + }, "user": { "data": { "auth_code": "Kode Otorisasi", "code": "Kode (digunakan di antarmuka Home Assistant)", "password": "Kata Sandi", - "username": "Email" + "username": "Nama Pengguna" }, - "description": "SimpliSafe mengautentikasi dengan Home Assistant melalui aplikasi web. Karena keterbatasan teknis, ada langkah manual di akhir proses ini; pastikan Anda membaca [dokumentasi] ({docs_url}) sebelum memulai.\n\n1. Klik [di sini]({url}) untuk membuka aplikasi web SimpliSafe dan masukkan kredensial Anda. \n\n2. Setelah proses masuk selesai, kembali ke sini dan klik masukkan kode otorisasi di bawah ini.", + "description": "Masukkan nama pengguna dan kata sandi Anda.", "title": "Isi informasi Anda." } } diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index 0221536698c..ad9e491ab9a 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -6,38 +6,37 @@ "wrong_account": "Le credenziali utente fornite non corrispondono a questo account SimpliSafe." }, "error": { + "2fa_timed_out": "Timeout durante l'attesa dell'autenticazione a due fattori", "identifier_exists": "Account gi\u00e0 registrato", "invalid_auth": "Autenticazione non valida", "still_awaiting_mfa": "Ancora in attesa del clic sull'email MFA", "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 email per trovare un collegamento da SimpliSafe. Dopo aver verificato il collegamento, torna qui per completare l'installazione dell'integrazione.", "title": "Autenticazione a pi\u00f9 fattori (MFA) SimpliSafe " }, "reauth_confirm": { "data": { "password": "Password" }, - "description": "Il tuo accesso \u00e8 scaduto o revocato. Inserisci la password per ricollegare il tuo account.", + "description": "Digita nuovamente la password per {username}.", "title": "Autentica nuovamente l'integrazione" }, + "sms_2fa": { + "data": { + "code": "Codice" + }, + "description": "Digita il codice di autenticazione a due fattori che ti \u00e8 stato inviato tramite SMS." + }, "user": { "data": { "auth_code": "Codice di autorizzazione", "code": "Codice (utilizzato nell'Interfaccia Utente di Home Assistant)", "password": "Password", - "username": "Email" + "username": "Nome utente" }, - "description": "SimpliSafe si autentica con Home Assistant tramite l'app Web SimpliSafe. A causa di limitazioni tecniche, alla fine di questo processo \u00e8 previsto un passaggio manuale; assicurati di leggere la [documentazione]({docs_url}) prima di iniziare. \n\n 1. Fai clic su [qui]({url}) per aprire l'app Web SimpliSafe e inserire le tue credenziali. \n\n 2. Quando il processo di accesso \u00e8 completo, torna qui e inserisci il codice di autorizzazione di seguito.", + "description": "Digita il tuo nome utente e password.", "title": "Inserisci le tue informazioni." } } diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index 925ae801a0d..74b0b20c65e 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -12,15 +12,7 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { - "input_auth_code": { - "data": { - "auth_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9" - }, - "description": "SimpliSafe Web\u30a2\u30d7\u30ea\u306eURL\u304b\u3089\u8a8d\u8a3c\u30b3\u30fc\u30c9\u3092\u5165\u529b:", - "title": "\u627f\u8a8d\u7d42\u4e86" - }, "mfa": { - "description": "SimpliSafe\u304b\u3089\u306e\u30ea\u30f3\u30af\u306b\u3064\u3044\u3066\u306f\u30e1\u30fc\u30eb\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u30ea\u30f3\u30af\u3092\u78ba\u8a8d\u3057\u305f\u3089\u3001\u3053\u3053\u306b\u623b\u3063\u3066\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u5b8c\u4e86\u3057\u307e\u3059\u3002", "title": "SimpliSafe\u591a\u8981\u7d20\u8a8d\u8a3c" }, "reauth_confirm": { diff --git a/homeassistant/components/simplisafe/translations/ko.json b/homeassistant/components/simplisafe/translations/ko.json index 194fa6cecf4..5c98e8abaa6 100644 --- a/homeassistant/components/simplisafe/translations/ko.json +++ b/homeassistant/components/simplisafe/translations/ko.json @@ -12,7 +12,6 @@ }, "step": { "mfa": { - "description": "\uc774\uba54\uc77c\uc5d0\uc11c SimpliSafe\uc758 \ub9c1\ud06c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694. \ub9c1\ud06c\ub97c \ud655\uc778\ud55c \ud6c4 \uc5ec\uae30\ub85c \ub3cc\uc544\uc640 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc758 \uc124\uce58\ub97c \uc644\ub8cc\ud574\uc8fc\uc138\uc694.", "title": "SimpliSafe \ub2e4\ub2e8\uacc4 \uc778\uc99d" }, "reauth_confirm": { diff --git a/homeassistant/components/simplisafe/translations/lb.json b/homeassistant/components/simplisafe/translations/lb.json index 225c4457a10..080d8afd394 100644 --- a/homeassistant/components/simplisafe/translations/lb.json +++ b/homeassistant/components/simplisafe/translations/lb.json @@ -12,7 +12,6 @@ }, "step": { "mfa": { - "description": "Kuck den E-Mailen fir ee Link vun SimpliSafe. Nodeem de Link opgeruff gouf, komm heihinner zer\u00e9ck fir d'Installatioun vun der Integratioun ofzeschl\u00e9issen.", "title": "SimpliSafe Multi-Faktor Authentifikatioun" }, "reauth_confirm": { diff --git a/homeassistant/components/simplisafe/translations/nl.json b/homeassistant/components/simplisafe/translations/nl.json index 3380746db25..33bc10ac85d 100644 --- a/homeassistant/components/simplisafe/translations/nl.json +++ b/homeassistant/components/simplisafe/translations/nl.json @@ -6,38 +6,37 @@ "wrong_account": "De opgegeven gebruikersgegevens komen niet overeen met deze SimpliSafe-account." }, "error": { + "2fa_timed_out": "Timed out tijdens het wachten op twee-factor authenticatie", "identifier_exists": "Account bestaat al", "invalid_auth": "Ongeldige authenticatie", "still_awaiting_mfa": "Wacht nog steeds op MFA-e-mailklik", "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" }, "reauth_confirm": { "data": { "password": "Wachtwoord" }, - "description": "Uw toegangstoken is verlopen of ingetrokken. Voer uw wachtwoord in om uw account opnieuw te koppelen.", + "description": "Voer het wachtwoord voor {username} opnieuw in.", "title": "Verifieer de integratie opnieuw" }, + "sms_2fa": { + "data": { + "code": "Code" + }, + "description": "Voer de twee-factor authenticatiecode in die u heeft ontvangen via SMS" + }, "user": { "data": { "auth_code": "Autorisatie Code", "code": "Code (gebruikt in Home Assistant)", "password": "Wachtwoord", - "username": "E-mail" + "username": "Gebruikersnaam" }, - "description": "SimpliSafe verifieert met Home Assistant via de SimpliSafe web app. Vanwege technische beperkingen is er een handmatige stap aan het einde van dit proces; zorg ervoor dat u de [documentatie]({docs_url}) leest voordat u begint.\n\n1. Klik op [hier]({url}) om de SimpliSafe web app te openen en voer uw referenties in.\n\n2. Wanneer het aanmeldingsproces is voltooid, gaat u hier terug en voert u de onderstaande autorisatiecode in.", + "description": "Voer uw gebruikersnaam en wachtwoord in.", "title": "Vul uw gegevens in" } } diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json index 2ac57586f0b..6527f2e4ae2 100644 --- a/homeassistant/components/simplisafe/translations/no.json +++ b/homeassistant/components/simplisafe/translations/no.json @@ -6,38 +6,37 @@ "wrong_account": "Brukerlegitimasjonen som er oppgitt, samsvarer ikke med denne SimpliSafe -kontoen." }, "error": { + "2fa_timed_out": "Tidsavbrutt mens du ventet p\u00e5 tofaktorautentisering", "identifier_exists": "Konto er allerede registrert", "invalid_auth": "Ugyldig godkjenning", "still_awaiting_mfa": "Forventer fortsatt MFA-e-postklikk", "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" }, "reauth_confirm": { "data": { "password": "Passord" }, - "description": "Tilgangen din har utl\u00f8pt eller blitt tilbakekalt. Skriv inn passordet ditt for \u00e5 koble kontoen din til p\u00e5 nytt.", + "description": "Vennligst skriv inn passordet for {username} p\u00e5 nytt.", "title": "Godkjenne integrering p\u00e5 nytt" }, + "sms_2fa": { + "data": { + "code": "Kode" + }, + "description": "Skriv inn tofaktorautentiseringskoden sendt til deg via SMS." + }, "user": { "data": { "auth_code": "Autorisasjonskode", "code": "Kode (brukt i Home Assistant brukergrensesnittet)", "password": "Passord", - "username": "E-post" + "username": "Brukernavn" }, - "description": "SimpliSafe autentiserer med Home Assistant via SimpliSafe-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]( {docs_url} ) f\u00f8r du starter. \n\n 1. Klikk [her]( {url} ) for \u00e5 \u00e5pne SimpliSafe-nettappen og angi legitimasjonen din. \n\n 2. N\u00e5r p\u00e5loggingsprosessen er fullf\u00f8rt, g\u00e5 tilbake hit og skriv inn autorisasjonskoden nedenfor.", + "description": "Skriv inn brukernavn og passord.", "title": "Fyll ut informasjonen din." } } diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json index e95f99c1d2b..21f38d5bff5 100644 --- a/homeassistant/components/simplisafe/translations/pl.json +++ b/homeassistant/components/simplisafe/translations/pl.json @@ -6,38 +6,37 @@ "wrong_account": "Podane dane uwierzytelniaj\u0105ce u\u017cytkownika nie pasuj\u0105 do tego konta SimpliSafe." }, "error": { + "2fa_timed_out": "Przekroczono limit czasu oczekiwania na uwierzytelnianie dwusk\u0142adnikowe", "identifier_exists": "Konto jest ju\u017c zarejestrowane", "invalid_auth": "Niepoprawne uwierzytelnienie", "still_awaiting_mfa": "Wci\u0105\u017c nie potwierdzono linka w e-mailu od SimpliSafe", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { - "input_auth_code": { - "data": { - "auth_code": "Kod autoryzacji" - }, - "description": "Wprowad\u017a kod autoryzacyjny z adresu URL aplikacji internetowej SimpliSafe:", - "title": "Zako\u0144cz autoryzacj\u0119" - }, "mfa": { - "description": "Sprawd\u017a e-mail od SimpliSafe. Po zweryfikowaniu linka, wr\u00f3\u0107 tutaj, aby doko\u0144czy\u0107 instalacj\u0119 integracji.", "title": "Uwierzytelnianie wielosk\u0142adnikowe SimpliSafe" }, "reauth_confirm": { "data": { "password": "Has\u0142o" }, - "description": "Tw\u00f3j dost\u0119p wygas\u0142 lub zosta\u0142 uniewa\u017cniony. Wprowad\u017a has\u0142o, aby ponownie po\u0142\u0105czy\u0107 swoje konto.", + "description": "Wprowad\u017a ponownie has\u0142o dla u\u017cytkownika {username}.", "title": "Ponownie uwierzytelnij integracj\u0119" }, + "sms_2fa": { + "data": { + "code": "Kod" + }, + "description": "Wprowad\u017a kod uwierzytelniania dwusk\u0142adnikowego, kt\u00f3ry zosta\u0142 wys\u0142any do Ciebie SMS-em." + }, "user": { "data": { "auth_code": "Kod autoryzacji", "code": "Kod (u\u017cywany w interfejsie Home Assistant)", "password": "Has\u0142o", - "username": "Adres e-mail" + "username": "Nazwa u\u017cytkownika" }, - "description": "SimpliSafe uwierzytelnia si\u0119 z Home Assistantem za po\u015brednictwem aplikacji internetowej SimpliSafe. Ze wzgl\u0119du na ograniczenia techniczne na ko\u0144cu tego procesu znajduje si\u0119 r\u0119czny krok; upewnij si\u0119, \u017ce przed rozpocz\u0119ciem przeczyta\u0142e\u015b [dokumentacj\u0119]( {docs_url} ).\n\n1. Kliknij [tutaj]( {url} ), aby otworzy\u0107 aplikacj\u0119 internetow\u0105 SimpliSafe i wprowadzi\u0107 swoje dane uwierzytelniaj\u0105ce. \n\n2. Po zako\u0144czeniu procesu logowania wr\u00f3\u0107 tutaj i wprowad\u017a poni\u017cszy kod autoryzacyjny.", + "description": "Wprowad\u017a swoj\u0105 nazw\u0119 u\u017cytkownika i has\u0142o.", "title": "Wprowad\u017a dane" } } diff --git a/homeassistant/components/simplisafe/translations/pt-BR.json b/homeassistant/components/simplisafe/translations/pt-BR.json index d1473074cca..1dec82ec74d 100644 --- a/homeassistant/components/simplisafe/translations/pt-BR.json +++ b/homeassistant/components/simplisafe/translations/pt-BR.json @@ -6,38 +6,37 @@ "wrong_account": "As credenciais de usu\u00e1rio fornecidas n\u00e3o correspondem a esta conta SimpliSafe." }, "error": { + "2fa_timed_out": "Expirou enquanto aguardava a autentica\u00e7\u00e3o de dois fatores", "identifier_exists": "Conta j\u00e1 cadastrada", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "still_awaiting_mfa": "Ainda aguardando clique no e-mail da MFA", "unknown": "Erro inesperado" }, "step": { - "input_auth_code": { - "data": { - "auth_code": "C\u00f3digo de Autoriza\u00e7\u00e3o" - }, - "description": "Insira o c\u00f3digo de autoriza\u00e7\u00e3o do URL do aplicativo Web SimpliSafe:", - "title": "Concluir autoriza\u00e7\u00e3o" - }, "mfa": { - "description": "Verifique seu e-mail para obter um link do SimpliSafe. Ap\u00f3s verificar o link, volte aqui para concluir a instala\u00e7\u00e3o da integra\u00e7\u00e3o.", "title": "Autentica\u00e7\u00e3o SimpliSafe multifator" }, "reauth_confirm": { "data": { "password": "Senha" }, - "description": "Seu acesso expirou ou foi revogado. Digite sua senha para vincular novamente sua conta.", + "description": "Por favor, digite novamente a senha para {username} .", "title": "Reautenticar Integra\u00e7\u00e3o" }, + "sms_2fa": { + "data": { + "code": "C\u00f3digo" + }, + "description": "Insira o c\u00f3digo de autentica\u00e7\u00e3o de dois fatores enviado a voc\u00ea via SMS." + }, "user": { "data": { "auth_code": "C\u00f3digo de autoriza\u00e7\u00e3o", "code": "C\u00f3digo (usado na IU do Home Assistant)", "password": "Senha", - "username": "Email" + "username": "Usu\u00e1rio" }, - "description": "O SimpliSafe autentica com o Home Assistant por meio do aplicativo da Web SimpliSafe. Por limita\u00e7\u00f5es t\u00e9cnicas, existe uma etapa manual ao final deste processo; certifique-se de ler a [documenta\u00e7\u00e3o]( {docs_url} ) antes de come\u00e7ar. \n\n 1. Clique [aqui]( {url} ) para abrir o aplicativo da web SimpliSafe e insira suas credenciais. \n\n 2. Quando o processo de login estiver conclu\u00eddo, retorne aqui e insira o c\u00f3digo de autoriza\u00e7\u00e3o abaixo.", + "description": "Insira seu nome de usu\u00e1rio e senha.", "title": "Preencha suas informa\u00e7\u00f5es." } } diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index 306a0180d88..f2907bd6c47 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -12,15 +12,7 @@ "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" }, "reauth_confirm": { diff --git a/homeassistant/components/simplisafe/translations/sl.json b/homeassistant/components/simplisafe/translations/sl.json index bbbe8034d06..6f326bc5b68 100644 --- a/homeassistant/components/simplisafe/translations/sl.json +++ b/homeassistant/components/simplisafe/translations/sl.json @@ -8,13 +8,6 @@ "identifier_exists": "Ra\u010dun je \u017ee registriran" }, "step": { - "input_auth_code": { - "data": { - "auth_code": "Avtorizacijska koda" - }, - "description": "Vnesite avtorizacijsko kodo iz URL-ja spletne aplikacije SimpliSafe:", - "title": "Dokon\u010daj avtorizacijo" - }, "user": { "data": { "code": "Koda (uporablja se v uporabni\u0161kem vmesniku Home Assistant)", diff --git a/homeassistant/components/simplisafe/translations/tr.json b/homeassistant/components/simplisafe/translations/tr.json index 721502ca0b1..255207833c7 100644 --- a/homeassistant/components/simplisafe/translations/tr.json +++ b/homeassistant/components/simplisafe/translations/tr.json @@ -12,15 +12,7 @@ "unknown": "Beklenmeyen hata" }, "step": { - "input_auth_code": { - "data": { - "auth_code": "Yetkilendirme Kodu" - }, - "description": "SimpliSafe web uygulamas\u0131 URL'sinden yetkilendirme kodunu girin:", - "title": "Yetkilendirmeyi Bitir" - }, "mfa": { - "description": "SimpliSafe'den bir ba\u011flant\u0131 i\u00e7in e-postan\u0131z\u0131 kontrol edin. Ba\u011flant\u0131y\u0131 do\u011frulad\u0131ktan sonra, entegrasyonun kurulumunu tamamlamak i\u00e7in buraya geri d\u00f6n\u00fcn.", "title": "SimpliSafe \u00c7ok Fakt\u00f6rl\u00fc Kimlik Do\u011frulama" }, "reauth_confirm": { diff --git a/homeassistant/components/simplisafe/translations/uk.json b/homeassistant/components/simplisafe/translations/uk.json index 0a51f129e5f..8ca29743c5b 100644 --- a/homeassistant/components/simplisafe/translations/uk.json +++ b/homeassistant/components/simplisafe/translations/uk.json @@ -12,7 +12,6 @@ }, "step": { "mfa": { - "description": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0441\u0432\u043e\u044e \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0443 \u043f\u043e\u0448\u0442\u0443 \u043d\u0430 \u043d\u0430\u044f\u0432\u043d\u0456\u0441\u0442\u044c \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0432\u0456\u0434 SimpliSafe. \u041f\u0456\u0441\u043b\u044f \u0442\u043e\u0433\u043e \u044f\u043a \u0432\u0456\u0434\u043a\u0440\u0438\u0454\u0442\u0435 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f, \u043f\u043e\u0432\u0435\u0440\u043d\u0456\u0442\u044c\u0441\u044f \u0441\u044e\u0434\u0438, \u0449\u043e\u0431 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457.", "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f SimpliSafe" }, "reauth_confirm": { diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json index ae22f8c8f0d..1b10153ab05 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/translations/zh-Hant.json @@ -6,38 +6,37 @@ "wrong_account": "\u6240\u4ee5\u63d0\u4f9b\u7684\u6191\u8b49\u8207 Simplisafe \u5e33\u865f\u4e0d\u7b26\u3002" }, "error": { + "2fa_timed_out": "\u7b49\u5f85\u5169\u6b65\u9a5f\u9a57\u8b49\u78bc\u903e\u6642", "identifier_exists": "\u5e33\u865f\u5df2\u8a3b\u518a", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "still_awaiting_mfa": "\u4ecd\u5728\u7b49\u5019\u9ede\u64ca\u591a\u6b65\u9a5f\u8a8d\u8b49\u90f5\u4ef6", "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" }, "reauth_confirm": { "data": { "password": "\u5bc6\u78bc" }, - "description": "\u5b58\u53d6\u6b0a\u9650\u5df2\u7d93\u904e\u671f\u6216\u53d6\u6d88\uff0c\u8acb\u8f38\u5165\u5bc6\u78bc\u4ee5\u91cd\u65b0\u9023\u7d50\u5e33\u865f\u3002", + "description": "\u8acb\u8f38\u5165\u5e33\u865f {username} \u5bc6\u78bc\u3002", "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" }, + "sms_2fa": { + "data": { + "code": "\u9a57\u8b49\u78bc" + }, + "description": "\u8f38\u5165\u7c21\u8a0a\u6240\u6536\u5230\u7684\u5169\u6b65\u9a5f\u9a57\u8b49\u78bc\u3002" + }, "user": { "data": { "auth_code": "\u8a8d\u8b49\u78bc", "code": "\u9a57\u8b49\u78bc\uff08\u4f7f\u7528\u65bc Home Assistant UI\uff09", "password": "\u5bc6\u78bc", - "username": "\u96fb\u5b50\u90f5\u4ef6" + "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "SimpliSafe \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]({docs_url})\u3002\n\n1. \u9ede\u9078 [\u6b64\u8655] ({url}) \u4ee5\u958b\u555f SimpliSafe Web App \u4e26\u8f38\u5165\u9a57\u8b49\u3002\n\n2. \u7576\u767b\u5165\u5b8c\u6210\u5f8c\u3001\u56de\u5230\u6b64\u8655\u65bc\u4e0b\u65b9\u8f38\u5165\u8a8d\u8b49\u78bc\u3002", + "description": "\u8f38\u5165\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002", "title": "\u586b\u5beb\u8cc7\u8a0a\u3002" } } diff --git a/homeassistant/components/sleepiq/translations/fr.json b/homeassistant/components/sleepiq/translations/fr.json index 9c43b00feba..f40576959bb 100644 --- a/homeassistant/components/sleepiq/translations/fr.json +++ b/homeassistant/components/sleepiq/translations/fr.json @@ -13,6 +13,7 @@ "data": { "password": "Mot de passe" }, + "description": "L'int\u00e9gration SleepIQ doit r\u00e9-authentifier votre compte {username}.", "title": "R\u00e9-authentifier l'int\u00e9gration" }, "user": { diff --git a/homeassistant/components/slimproto/translations/de.json b/homeassistant/components/slimproto/translations/de.json new file mode 100644 index 00000000000..69b89b76225 --- /dev/null +++ b/homeassistant/components/slimproto/translations/de.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/el.json b/homeassistant/components/slimproto/translations/el.json new file mode 100644 index 00000000000..df583a2f8f8 --- /dev/null +++ b/homeassistant/components/slimproto/translations/el.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/en.json b/homeassistant/components/slimproto/translations/en.json index fcefa6de190..2d579aab7c8 100644 --- a/homeassistant/components/slimproto/translations/en.json +++ b/homeassistant/components/slimproto/translations/en.json @@ -1,11 +1,7 @@ { - "config": { - "abort": { - "single_instance_allowed": "Already configured. Only a single configuration possible." - }, - "step": { - "user": { - } - } - } + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + } + } } \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/et.json b/homeassistant/components/slimproto/translations/et.json new file mode 100644 index 00000000000..e8b5eae4149 --- /dev/null +++ b/homeassistant/components/slimproto/translations/et.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/fr.json b/homeassistant/components/slimproto/translations/fr.json new file mode 100644 index 00000000000..807ba246694 --- /dev/null +++ b/homeassistant/components/slimproto/translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/hu.json b/homeassistant/components/slimproto/translations/hu.json new file mode 100644 index 00000000000..edd3c258b81 --- /dev/null +++ b/homeassistant/components/slimproto/translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "user": { + "one": "\u00dcres", + "other": "\u00dcres" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/id.json b/homeassistant/components/slimproto/translations/id.json new file mode 100644 index 00000000000..3a870e47986 --- /dev/null +++ b/homeassistant/components/slimproto/translations/id.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/it.json b/homeassistant/components/slimproto/translations/it.json new file mode 100644 index 00000000000..42bc1ef9f7d --- /dev/null +++ b/homeassistant/components/slimproto/translations/it.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "step": { + "user": { + "one": "Vuoto", + "other": "Vuoti" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/nl.json b/homeassistant/components/slimproto/translations/nl.json new file mode 100644 index 00000000000..79aaec23123 --- /dev/null +++ b/homeassistant/components/slimproto/translations/nl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/no.json b/homeassistant/components/slimproto/translations/no.json new file mode 100644 index 00000000000..aa380f0385a --- /dev/null +++ b/homeassistant/components/slimproto/translations/no.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/pl.json b/homeassistant/components/slimproto/translations/pl.json new file mode 100644 index 00000000000..96fba53c20f --- /dev/null +++ b/homeassistant/components/slimproto/translations/pl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/pt-BR.json b/homeassistant/components/slimproto/translations/pt-BR.json new file mode 100644 index 00000000000..9ab59f40649 --- /dev/null +++ b/homeassistant/components/slimproto/translations/pt-BR.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/zh-Hant.json b/homeassistant/components/slimproto/translations/zh-Hant.json new file mode 100644 index 00000000000..942e17282bb --- /dev/null +++ b/homeassistant/components/slimproto/translations/zh-Hant.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/hu.json b/homeassistant/components/smappee/translations/hu.json index 9c3d90ac43e..712cbd0951d 100644 --- a/homeassistant/components/smappee/translations/hu.json +++ b/homeassistant/components/smappee/translations/hu.json @@ -7,7 +7,7 @@ "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\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." + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3." }, "flow_title": "{name}", "step": { @@ -24,7 +24,7 @@ "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" + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" }, "zeroconf_confirm": { "description": "Hozz\u00e1 szeretn\u00e9 adni a \"{serialnumber} serialnumber}\" sorozatsz\u00e1m\u00fa Smappee -eszk\u00f6zt az HomeAssistanthoz?", diff --git a/homeassistant/components/smappee/translations/it.json b/homeassistant/components/smappee/translations/it.json index 3a493371b16..7c18d944ab7 100644 --- a/homeassistant/components/smappee/translations/it.json +++ b/homeassistant/components/smappee/translations/it.json @@ -6,7 +6,7 @@ "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "cannot_connect": "Impossibile connettersi", "invalid_mdns": "Dispositivo non supportato per l'integrazione Smappee.", - "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})" }, "flow_title": "{name}", diff --git a/homeassistant/components/smartthings/translations/it.json b/homeassistant/components/smartthings/translations/it.json index 4f2006e52c3..258abb0da8d 100644 --- a/homeassistant/components/smartthings/translations/it.json +++ b/homeassistant/components/smartthings/translations/it.json @@ -19,8 +19,8 @@ "data": { "access_token": "Token di accesso" }, - "description": "Inserisci un SmartThings [Personal Access Token]({token_url}) che \u00e8 stato creato secondo le [istruzioni]({component_url}). Questo sar\u00e0 utilizzato per creare l'integrazione Home Assistant all'interno del tuo account SmartThings.", - "title": "Inserisci il Token di Accesso Personale" + "description": "Inserisci un [token di accesso personale]({token_url}) che \u00e8 stato creato secondo le [istruzioni]({component_url}). Questo sar\u00e0 utilizzato per creare l'integrazione Home Assistant all'interno del tuo account SmartThings.", + "title": "Inserisci il token di accesso personale" }, "select_location": { "data": { diff --git a/homeassistant/components/smhi/translations/hu.json b/homeassistant/components/smhi/translations/hu.json index 425cf927631..e83ea03a193 100644 --- a/homeassistant/components/smhi/translations/hu.json +++ b/homeassistant/components/smhi/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, "error": { "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik", "wrong_location": "Csak sv\u00e9dorsz\u00e1gi helysz\u00edn megengedett" @@ -9,7 +12,7 @@ "data": { "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" }, "title": "Helysz\u00edn Sv\u00e9dorsz\u00e1gban" } diff --git a/homeassistant/components/solax/translations/es.json b/homeassistant/components/solax/translations/es.json new file mode 100644 index 00000000000..4728aed6395 --- /dev/null +++ b/homeassistant/components/solax/translations/es.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Error inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/translations/it.json b/homeassistant/components/soma/translations/it.json index c3cd51e6351..00d454e2c96 100644 --- a/homeassistant/components/soma/translations/it.json +++ b/homeassistant/components/soma/translations/it.json @@ -4,7 +4,7 @@ "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.", + "missing_configuration": "Il componente Soma non \u00e8 configurato. Segui la documentazione.", "result_error": "SOMA Connect ha risposto con stato di errore." }, "create_entry": { diff --git a/homeassistant/components/somfy/translations/hu.json b/homeassistant/components/somfy/translations/hu.json index 06b0894faf1..96b873b2c42 100644 --- a/homeassistant/components/somfy/translations/hu.json +++ b/homeassistant/components/somfy/translations/hu.json @@ -3,7 +3,7 @@ "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\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.", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "create_entry": { @@ -11,7 +11,7 @@ }, "step": { "pick_implementation": { - "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" } } } diff --git a/homeassistant/components/somfy/translations/it.json b/homeassistant/components/somfy/translations/it.json index 1c6650232dc..0201e1e2569 100644 --- a/homeassistant/components/somfy/translations/it.json +++ b/homeassistant/components/somfy/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", - "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, diff --git a/homeassistant/components/sonarr/translations/cs.json b/homeassistant/components/sonarr/translations/cs.json index 00ff1611319..9be2106b4e2 100644 --- a/homeassistant/components/sonarr/translations/cs.json +++ b/homeassistant/components/sonarr/translations/cs.json @@ -21,6 +21,7 @@ "host": "Hostitel", "port": "Port", "ssl": "Pou\u017e\u00edv\u00e1 SSL certifik\u00e1t", + "url": "URL", "verify_ssl": "Ov\u011b\u0159it certifik\u00e1t SSL" } } diff --git a/homeassistant/components/sonarr/translations/hu.json b/homeassistant/components/sonarr/translations/hu.json index c3cc2ac0f6c..5aabd38a974 100644 --- a/homeassistant/components/sonarr/translations/hu.json +++ b/homeassistant/components/sonarr/translations/hu.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "A Sonarr integr\u00e1ci\u00f3t manu\u00e1lisan kell hiteles\u00edteni: {host}", + "description": "A Sonarr-integr\u00e1ci\u00f3t manu\u00e1lisan \u00fajra kell hiteles\u00edteni a Sonarr API-n\u00e1l: {url}", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "user": { diff --git a/homeassistant/components/spotify/translations/hu.json b/homeassistant/components/spotify/translations/hu.json index 136e6185b46..f387ff6bb3a 100644 --- a/homeassistant/components/spotify/translations/hu.json +++ b/homeassistant/components/spotify/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "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.", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", "reauth_account_mismatch": "A Spotify-fi\u00f3kkal hiteles\u00edtett fi\u00f3k nem egyezik meg az \u00faj hiteles\u00edt\u00e9shez sz\u00fcks\u00e9ges fi\u00f3kkal." }, "create_entry": { @@ -11,7 +11,7 @@ }, "step": { "pick_implementation": { - "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" }, "reauth_confirm": { "description": "A Spotify integr\u00e1ci\u00f3nak \u00fajra hiteles\u00edtenie kell a Spotify fi\u00f3kot: {account}", diff --git a/homeassistant/components/spotify/translations/it.json b/homeassistant/components/spotify/translations/it.json index d40fa60b4d4..445b7a233e4 100644 --- a/homeassistant/components/spotify/translations/it.json +++ b/homeassistant/components/spotify/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Tempo scaduto nella generazione dell'URL di autorizzazione.", - "missing_configuration": "L'integrazione di Spotify non \u00e8 configurata. Si prega di seguire la documentazione.", + "missing_configuration": "L'integrazione di Spotify non \u00e8 configurata. Segui la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", "reauth_account_mismatch": "L'account Spotify con cui sei autenticato non corrisponde all'account necessario per la nuova autenticazione." }, diff --git a/homeassistant/components/sql/translations/ca.json b/homeassistant/components/sql/translations/ca.json new file mode 100644 index 00000000000..d69f22ca855 --- /dev/null +++ b/homeassistant/components/sql/translations/ca.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat" + }, + "error": { + "db_url_invalid": "URL de la base de dades inv\u00e0lid", + "query_invalid": "Consulta SQL inv\u00e0lida", + "value_template_invalid": "Plantilla de valor inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "column": "Columna", + "db_url": "URL de la base de dades", + "query": "Selecciona la consulta", + "unit_of_measurement": "Unitat de mesura", + "value_template": "Plantilla de valor" + }, + "data_description": { + "column": "Columna de resposta de la consulta per presentar com a estat", + "db_url": "URL de la base de dades, deixa-ho en blanc per utilitzar la predeterminada de HA", + "query": "Consulta a executar, ha de comen\u00e7ar per 'SELECT'", + "unit_of_measurement": "Unitat de mesura (opcional)", + "value_template": "Plantilla de valor (opcional)" + } + } + } + }, + "options": { + "error": { + "db_url_invalid": "URL de la base de dades inv\u00e0lid", + "query_invalid": "Consulta SQL inv\u00e0lida", + "value_template_invalid": "Plantilla de valor inv\u00e0lida" + }, + "step": { + "init": { + "data": { + "column": "Columna", + "db_url": "URL de la base de dades", + "query": "Selecciona la consulta", + "unit_of_measurement": "Unitat de mesura", + "value_template": "Plantilla de valor" + }, + "data_description": { + "column": "Columna de resposta de la consulta per presentar com a estat", + "db_url": "URL de la base de dades, deixa-ho en blanc per utilitzar la predeterminada de HA", + "query": "Consulta a executar, ha de comen\u00e7ar per 'SELECT'", + "unit_of_measurement": "Unitat de mesura (opcional)", + "value_template": "Plantilla de valor (opcional)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/de.json b/homeassistant/components/sql/translations/de.json new file mode 100644 index 00000000000..2207997133d --- /dev/null +++ b/homeassistant/components/sql/translations/de.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, + "error": { + "db_url_invalid": "Datenbank-URL ung\u00fcltig", + "query_invalid": "SQL-Abfrage ung\u00fcltig", + "value_template_invalid": "Wertvorlage ung\u00fcltig" + }, + "step": { + "user": { + "data": { + "column": "Spalte", + "db_url": "Datenbank-URL", + "query": "Abfrage ausw\u00e4hlen", + "unit_of_measurement": "Ma\u00dfeinheit", + "value_template": "Wertvorlage" + }, + "data_description": { + "column": "Spalte f\u00fcr die zur\u00fcckgegebene Abfrage, die als Status angezeigt werden soll", + "db_url": "Datenbank-URL, leer lassen, um die Standard-HA-Datenbank zu verwenden", + "query": "Auszuf\u00fchrende Abfrage, muss mit 'SELECT' beginnen", + "unit_of_measurement": "Ma\u00dfeinheit (optional)", + "value_template": "Wertvorlage (optional)" + } + } + } + }, + "options": { + "error": { + "db_url_invalid": "Datenbank-URL ung\u00fcltig", + "query_invalid": "SQL-Abfrage ung\u00fcltig", + "value_template_invalid": "Wertvorlage ung\u00fcltig" + }, + "step": { + "init": { + "data": { + "column": "Spalte", + "db_url": "Datenbank-URL", + "query": "Abfrage ausw\u00e4hlen", + "unit_of_measurement": "Ma\u00dfeinheit", + "value_template": "Wertvorlage" + }, + "data_description": { + "column": "Spalte f\u00fcr die zur\u00fcckgegebene Abfrage, die als Status angezeigt werden soll", + "db_url": "Datenbank-URL, leer lassen, um die Standard-HA-Datenbank zu verwenden", + "query": "Auszuf\u00fchrende Abfrage, muss mit 'SELECT' beginnen", + "unit_of_measurement": "Ma\u00dfeinheit (optional)", + "value_template": "Wertvorlage (optional)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/el.json b/homeassistant/components/sql/translations/el.json new file mode 100644 index 00000000000..4f19401a8f3 --- /dev/null +++ b/homeassistant/components/sql/translations/el.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "db_url_invalid": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b2\u03ac\u03c3\u03b7\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd", + "query_invalid": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b5\u03c1\u03ce\u03c4\u03b7\u03bc\u03b1 SQL", + "value_template_invalid": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03c0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf \u03c4\u03b9\u03bc\u03ae\u03c2" + }, + "step": { + "user": { + "data": { + "column": "\u03a3\u03c4\u03ae\u03bb\u03b7", + "db_url": "URL \u03b2\u03ac\u03c3\u03b7\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd", + "query": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b5\u03c1\u03c9\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2", + "unit_of_measurement": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2", + "value_template": "\u03a0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf \u03b1\u03be\u03af\u03b1\u03c2" + }, + "data_description": { + "column": "\u03a3\u03c4\u03ae\u03bb\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b5\u03c0\u03b9\u03c3\u03c4\u03c1\u03b5\u03c6\u03cc\u03bc\u03b5\u03bd\u03bf \u03b5\u03c1\u03ce\u03c4\u03b7\u03bc\u03b1 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \u03c9\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7", + "db_url": "URL \u03c4\u03b7\u03c2 \u03b2\u03ac\u03c3\u03b7\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd, \u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b2\u03ac\u03c3\u03b7 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd HA", + "query": "\u03a4\u03bf \u03b5\u03c1\u03ce\u03c4\u03b7\u03bc\u03b1 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03c3\u03c4\u03b5\u03af \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ac \u03bc\u03b5 'SELECT'", + "unit_of_measurement": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "value_template": "\u03a0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf \u03c4\u03b9\u03bc\u03ae\u03c2 (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)" + } + } + } + }, + "options": { + "error": { + "db_url_invalid": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b2\u03ac\u03c3\u03b7\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd", + "query_invalid": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b5\u03c1\u03ce\u03c4\u03b7\u03bc\u03b1 SQL", + "value_template_invalid": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03c0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf \u03c4\u03b9\u03bc\u03ae\u03c2" + }, + "step": { + "init": { + "data": { + "column": "\u03a3\u03c4\u03ae\u03bb\u03b7", + "db_url": "URL \u03b2\u03ac\u03c3\u03b7\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd", + "query": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b5\u03c1\u03c9\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2", + "unit_of_measurement": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2", + "value_template": "\u03a0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf \u03c4\u03b9\u03bc\u03ae\u03c2" + }, + "data_description": { + "column": "\u03a3\u03c4\u03ae\u03bb\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b5\u03c0\u03b9\u03c3\u03c4\u03c1\u03b5\u03c6\u03cc\u03bc\u03b5\u03bd\u03bf \u03b5\u03c1\u03ce\u03c4\u03b7\u03bc\u03b1 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \u03c9\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7", + "db_url": "URL \u03c4\u03b7\u03c2 \u03b2\u03ac\u03c3\u03b7\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd, \u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b2\u03ac\u03c3\u03b7 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd HA", + "query": "\u03a4\u03bf \u03b5\u03c1\u03ce\u03c4\u03b7\u03bc\u03b1 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03c3\u03c4\u03b5\u03af \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ac \u03bc\u03b5 'SELECT'", + "unit_of_measurement": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "value_template": "\u03a0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf \u03c4\u03b9\u03bc\u03ae\u03c2 (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/en.json b/homeassistant/components/sql/translations/en.json index 2d3bf3c1999..3b1fc223c00 100644 --- a/homeassistant/components/sql/translations/en.json +++ b/homeassistant/components/sql/translations/en.json @@ -18,9 +18,9 @@ "value_template": "Value Template" }, "data_description": { + "column": "Column for returned query to present as state", "db_url": "Database URL, leave empty to use default HA database", "query": "Query to run, needs to start with 'SELECT'", - "column": "Column for returned query to present as state", "unit_of_measurement": "Unit of Measure (optional)", "value_template": "Value Template (optional)" } @@ -43,9 +43,9 @@ "value_template": "Value Template" }, "data_description": { + "column": "Column for returned query to present as state", "db_url": "Database URL, leave empty to use default HA database", "query": "Query to run, needs to start with 'SELECT'", - "column": "Column for returned query to present as state", "unit_of_measurement": "Unit of Measure (optional)", "value_template": "Value Template (optional)" } diff --git a/homeassistant/components/sql/translations/et.json b/homeassistant/components/sql/translations/et.json new file mode 100644 index 00000000000..b1f7339aba2 --- /dev/null +++ b/homeassistant/components/sql/translations/et.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud" + }, + "error": { + "db_url_invalid": "Andmebaasi URL on vigane", + "query_invalid": "SQL-p\u00e4ring on kehtetu", + "value_template_invalid": "V\u00e4\u00e4rtuse mall on kehtetu" + }, + "step": { + "user": { + "data": { + "column": "Veerg", + "db_url": "Andmebaasi URL", + "query": "Vali p\u00e4ring", + "unit_of_measurement": "M\u00f5\u00f5t\u00fchik", + "value_template": "V\u00e4\u00e4rtuse mall" + }, + "data_description": { + "column": "Tagastatud p\u00e4ringu veerg olekuna esitamiseks", + "db_url": "Andmebaasi URL, j\u00e4ta t\u00fchjaks, et kasutada HA vaikeandmebaasi", + "query": "K\u00e4ivitatav p\u00e4ring peab algama 'SELECT'-iga.", + "unit_of_measurement": "M\u00f5\u00f5t\u00fchik (valikuline)", + "value_template": "V\u00e4\u00e4rtuse mall (valikuline)" + } + } + } + }, + "options": { + "error": { + "db_url_invalid": "Andmebaasi URL on vigane", + "query_invalid": "V\u00e4\u00e4rtuse mall on kehtetu", + "value_template_invalid": "V\u00e4\u00e4rtuse mall on kehtetu" + }, + "step": { + "init": { + "data": { + "column": "Veerg", + "db_url": "Andmebaasi URL", + "query": "Vali p\u00e4ring", + "unit_of_measurement": "M\u00f5\u00f5t\u00fchik", + "value_template": "Tagastatud p\u00e4ringu veerg olekuna esitamiseks" + }, + "data_description": { + "column": "Tagastatud p\u00e4ringu veerg olekuna esitamiseks", + "db_url": "Andmebaasi URL, j\u00e4ta t\u00fchjaks, et kasutada HA vaikeandmebaasi", + "query": "K\u00e4ivitatav p\u00e4ring peab algama 'SELECT'-iga.", + "unit_of_measurement": "M\u00f5\u00f5t\u00fchik (valikuline)", + "value_template": "V\u00e4\u00e4rtuse mall (valikuline)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/fr.json b/homeassistant/components/sql/translations/fr.json new file mode 100644 index 00000000000..078fdc44d24 --- /dev/null +++ b/homeassistant/components/sql/translations/fr.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "db_url_invalid": "URL de la base de donn\u00e9es non valide", + "query_invalid": "Requ\u00eate SQL non valide", + "value_template_invalid": "Mod\u00e8le de valeur non valide" + }, + "step": { + "user": { + "data": { + "column": "Colonne", + "db_url": "URL de la base de donn\u00e9es", + "query": "Requ\u00eate de s\u00e9lection", + "unit_of_measurement": "Unit\u00e9 de mesure", + "value_template": "Mod\u00e8le de valeur" + }, + "data_description": { + "column": "La colonne de la r\u00e9ponse \u00e0 la requ\u00eate \u00e0 pr\u00e9senter en tant qu'\u00e9tat", + "db_url": "L'URL de la base de donn\u00e9es, laisser vide pour utiliser la base de donn\u00e9es HA par d\u00e9faut", + "query": "La requ\u00eate \u00e0 ex\u00e9cuter, doit commencer par \u00ab\u00a0SELECT\u00a0\u00bb", + "unit_of_measurement": "Unit\u00e9 de mesure (facultatif)", + "value_template": "Mod\u00e8le de valeur (facultatif)" + } + } + } + }, + "options": { + "error": { + "db_url_invalid": "URL de la base de donn\u00e9es non valide", + "query_invalid": "Requ\u00eate SQL non valide", + "value_template_invalid": "Mod\u00e8le de valeur non valide" + }, + "step": { + "init": { + "data": { + "column": "Colonne", + "db_url": "URL de la base de donn\u00e9es", + "query": "Requ\u00eate de s\u00e9lection", + "unit_of_measurement": "Unit\u00e9 de mesure", + "value_template": "Mod\u00e8le de valeur" + }, + "data_description": { + "column": "La colonne de la r\u00e9ponse \u00e0 la requ\u00eate \u00e0 pr\u00e9senter en tant qu'\u00e9tat", + "db_url": "L'URL de la base de donn\u00e9es, laisser vide pour utiliser la base de donn\u00e9es HA par d\u00e9faut", + "query": "La requ\u00eate \u00e0 ex\u00e9cuter, doit commencer par \u00ab\u00a0SELECT\u00a0\u00bb", + "unit_of_measurement": "Unit\u00e9 de mesure (facultatif)", + "value_template": "Mod\u00e8le de valeur (facultatif)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/hu.json b/homeassistant/components/sql/translations/hu.json new file mode 100644 index 00000000000..cf125215ae3 --- /dev/null +++ b/homeassistant/components/sql/translations/hu.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "db_url_invalid": "\u00c9rv\u00e9nytelen adatb\u00e1zis URL-c\u00edm", + "query_invalid": "\u00c9rv\u00e9nytelen SQL-lek\u00e9rdez\u00e9s", + "value_template_invalid": "\u00c9rv\u00e9nytelen \u00e9rt\u00e9ksablon" + }, + "step": { + "user": { + "data": { + "column": "Oszlop", + "db_url": "Adatb\u00e1zis URL", + "query": "Lek\u00e9rdez\u00e9s kijel\u00f6l\u00e9se", + "unit_of_measurement": "M\u00e9rt\u00e9kegys\u00e9g", + "value_template": "\u00c9rt\u00e9ksablon" + }, + "data_description": { + "column": "\u00c1llapotk\u00e9nt megjelen\u00edtend\u0151, lek\u00e9rdezett oszlop", + "db_url": "Adatb\u00e1zis URL-c\u00edme, hagyja \u00fcresen az alap\u00e9rtelmezett HA-adatb\u00e1zis haszn\u00e1lat\u00e1hoz", + "query": "A futtatand\u00f3 lek\u00e9rdez\u00e9snek 'SELECT'-el kell kezd\u0151dnie.", + "unit_of_measurement": "M\u00e9rt\u00e9kegys\u00e9g (nem k\u00f6telez\u0151)", + "value_template": "\u00c9rt\u00e9ksablon (nem k\u00f6telez\u0151)" + } + } + } + }, + "options": { + "error": { + "db_url_invalid": "\u00c9rv\u00e9nytelen adatb\u00e1zis URL-c\u00edm", + "query_invalid": "\u00c9rv\u00e9nytelen SQL-lek\u00e9rdez\u00e9s", + "value_template_invalid": "\u00c9rv\u00e9nytelen \u00e9rt\u00e9ksablon" + }, + "step": { + "init": { + "data": { + "column": "Oszlop", + "db_url": "Adatb\u00e1zis URL", + "query": "Lek\u00e9rdez\u00e9s kijel\u00f6l\u00e9se", + "unit_of_measurement": "M\u00e9rt\u00e9kegys\u00e9g", + "value_template": "\u00c9rt\u00e9ksablon" + }, + "data_description": { + "column": "\u00c1llapotk\u00e9nt megjelen\u00edtend\u0151, lek\u00e9rdezett oszlop", + "db_url": "Adatb\u00e1zis URL-c\u00edme, hagyja \u00fcresen az alap\u00e9rtelmezett HA-adatb\u00e1zis haszn\u00e1lat\u00e1hoz", + "query": "A futtatand\u00f3 lek\u00e9rdez\u00e9snek 'SELECT'-el kell kezd\u0151dnie.", + "unit_of_measurement": "M\u00e9rt\u00e9kegys\u00e9g (nem k\u00f6telez\u0151)", + "value_template": "\u00c9rt\u00e9ksablon (nem k\u00f6telez\u0151)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/id.json b/homeassistant/components/sql/translations/id.json new file mode 100644 index 00000000000..9cd3574c6c9 --- /dev/null +++ b/homeassistant/components/sql/translations/id.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "db_url_invalid": "URL database tidak valid", + "query_invalid": "Kueri SQL tidak valid", + "value_template_invalid": "Templat Nilai tidak valid" + }, + "step": { + "user": { + "data": { + "column": "Kolom", + "db_url": "URL Database", + "query": "Kueri Select", + "unit_of_measurement": "Satuan Ukuran", + "value_template": "Templat Nilai" + }, + "data_description": { + "column": "Kolom pada kueri yang dikembalikan untuk ditampilkan sebagai status", + "db_url": "URL database, kosongkan untuk menggunakan database HA default", + "query": "Kueri untuk dijalankan, perlu dimulai dengan 'SELECT'", + "unit_of_measurement": "Satuan Ukuran (opsional)", + "value_template": "Template Nilai (opsional)" + } + } + } + }, + "options": { + "error": { + "db_url_invalid": "URL database tidak valid", + "query_invalid": "Kueri SQL tidak valid", + "value_template_invalid": "Templat Nilai tidak valid" + }, + "step": { + "init": { + "data": { + "column": "Kolom", + "db_url": "URL Database", + "query": "Kueri Select", + "unit_of_measurement": "Satuan Ukuran", + "value_template": "Templat Nilai" + }, + "data_description": { + "column": "Kolom pada kueri yang dikembalikan untuk ditampilkan sebagai status", + "db_url": "URL database, kosongkan untuk menggunakan database HA default", + "query": "Kueri untuk dijalankan, perlu dimulai dengan 'SELECT'", + "unit_of_measurement": "Satuan Ukuran (opsional)", + "value_template": "Template Nilai (opsional)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/it.json b/homeassistant/components/sql/translations/it.json new file mode 100644 index 00000000000..01672eb5a51 --- /dev/null +++ b/homeassistant/components/sql/translations/it.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato" + }, + "error": { + "db_url_invalid": "URL database non valido", + "query_invalid": "Query SQL non valida", + "value_template_invalid": "Modello di valore non valido" + }, + "step": { + "user": { + "data": { + "column": "Colonna", + "db_url": "URL del database", + "query": "Query Select", + "unit_of_measurement": "Unit\u00e0 di misura", + "value_template": "Modello di valore" + }, + "data_description": { + "column": "Colonna per la query restituita da presentare come stato", + "db_url": "URL del database, lascia vuoto per utilizzare il database predefinito di Home Assistant", + "query": "Query da eseguire, deve iniziare con 'SELECT'", + "unit_of_measurement": "Unit\u00e0 di misura (opzionale)", + "value_template": "Modello di valore (opzionale)" + } + } + } + }, + "options": { + "error": { + "db_url_invalid": "URL database non valido", + "query_invalid": "Query SQL non valida", + "value_template_invalid": "Modello di valore non valido" + }, + "step": { + "init": { + "data": { + "column": "Colonna", + "db_url": "URL del database", + "query": "Query Select", + "unit_of_measurement": "Unit\u00e0 di misura", + "value_template": "Modello di valore" + }, + "data_description": { + "column": "Colonna per la query restituita da presentare come stato", + "db_url": "URL del database, lascia vuoto per utilizzare il database predefinito di Home Assistant", + "query": "Query da eseguire, deve iniziare con 'SELECT'", + "unit_of_measurement": "Unit\u00e0 di misura (opzionale)", + "value_template": "Modello di valore (opzionale)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/nl.json b/homeassistant/components/sql/translations/nl.json new file mode 100644 index 00000000000..ee6ad3503bb --- /dev/null +++ b/homeassistant/components/sql/translations/nl.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd" + }, + "error": { + "db_url_invalid": "Database URL ongeldig", + "query_invalid": "SQL Query ongeldig", + "value_template_invalid": "Waarde Template ongeldig" + }, + "step": { + "user": { + "data": { + "column": "Kolom", + "db_url": "Database URL", + "query": "Selecteer Query", + "unit_of_measurement": "Meeteenheid", + "value_template": "Waarde Template" + }, + "data_description": { + "column": "Kolom voor teruggestuurde query om als status te presenteren", + "db_url": "Database URL, leeg laten om de standaard HA database te gebruiken", + "unit_of_measurement": "Meeteenheid (optioneel)", + "value_template": "Waarde Template (optioneel)" + } + } + } + }, + "options": { + "error": { + "db_url_invalid": "Database URL ongeldig", + "query_invalid": "SQL Query ongeldig", + "value_template_invalid": "Waarde Template ongeldig" + }, + "step": { + "init": { + "data": { + "column": "Kolom", + "db_url": "Database URL", + "query": "Selecteer Query", + "unit_of_measurement": "Meeteenheid", + "value_template": "Waarde Template" + }, + "data_description": { + "column": "Kolom voor teruggestuurde query om als status te presenteren", + "db_url": "Database URL, leeg laten om de standaard HA database te gebruiken", + "unit_of_measurement": "Meeteenheid (optioneel)", + "value_template": "Waarde Template (optioneel)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/no.json b/homeassistant/components/sql/translations/no.json new file mode 100644 index 00000000000..292486d3c0f --- /dev/null +++ b/homeassistant/components/sql/translations/no.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, + "error": { + "db_url_invalid": "Database-URL er ugyldig", + "query_invalid": "SQL-sp\u00f8rringen er ugyldig", + "value_template_invalid": "Verdimalen er ugyldig" + }, + "step": { + "user": { + "data": { + "column": "Kolonne", + "db_url": "Database URL", + "query": "Velg Sp\u00f8rring", + "unit_of_measurement": "M\u00e5leenhet", + "value_template": "Verdimal" + }, + "data_description": { + "column": "Kolonne for returnert foresp\u00f8rsel for \u00e5 presentere som tilstand", + "db_url": "Database-URL, la st\u00e5 tom for \u00e5 bruke standard HA-database", + "query": "Sp\u00f8rsm\u00e5let skal kj\u00f8res, m\u00e5 starte med \"SELECT\"", + "unit_of_measurement": "M\u00e5leenhet (valgfritt)", + "value_template": "Verdimal (valgfritt)" + } + } + } + }, + "options": { + "error": { + "db_url_invalid": "Database-URL er ugyldig", + "query_invalid": "SQL-sp\u00f8rringen er ugyldig", + "value_template_invalid": "Verdimalen er ugyldig" + }, + "step": { + "init": { + "data": { + "column": "Kolonne", + "db_url": "Database URL", + "query": "Velg Sp\u00f8rring", + "unit_of_measurement": "M\u00e5leenhet", + "value_template": "Verdimal" + }, + "data_description": { + "column": "Kolonne for returnert foresp\u00f8rsel for \u00e5 presentere som tilstand", + "db_url": "Database-URL, la st\u00e5 tom for \u00e5 bruke standard HA-database", + "query": "Sp\u00f8rsm\u00e5let skal kj\u00f8res, m\u00e5 starte med \"SELECT\"", + "unit_of_measurement": "M\u00e5leenhet (valgfritt)", + "value_template": "Verdimal (valgfritt)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/pl.json b/homeassistant/components/sql/translations/pl.json new file mode 100644 index 00000000000..0e8cb47b148 --- /dev/null +++ b/homeassistant/components/sql/translations/pl.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane" + }, + "error": { + "db_url_invalid": "Nieprawid\u0142owy adres URL bazy danych", + "query_invalid": "Nieprawid\u0142owe zapytanie SQL", + "value_template_invalid": "Nieprawid\u0142owy szablon warto\u015bci" + }, + "step": { + "user": { + "data": { + "column": "Kolumna", + "db_url": "Adres URL bazy danych", + "query": "Wybierz zapytanie", + "unit_of_measurement": "Jednostka miary", + "value_template": "Szablon warto\u015bci" + }, + "data_description": { + "column": "Kolumna dla zwr\u00f3conego zapytania do przedstawienia jako stan", + "db_url": "Adres URL bazy danych, pozostaw puste, aby u\u017cy\u0107 domy\u015blnej bazy danych HA", + "query": "Zapytanie do uruchomienia, musi zaczyna\u0107 si\u0119 od \u201eSELECT\u201d", + "unit_of_measurement": "Jednostka miary (opcjonalnie)", + "value_template": "Szablon warto\u015bci (opcjonalnie)" + } + } + } + }, + "options": { + "error": { + "db_url_invalid": "Nieprawid\u0142owy adres URL bazy danych", + "query_invalid": "Nieprawid\u0142owe zapytanie SQL", + "value_template_invalid": "Nieprawid\u0142owy szablon warto\u015bci" + }, + "step": { + "init": { + "data": { + "column": "Kolumna", + "db_url": "Adres URL bazy danych", + "query": "Wybierz zapytanie", + "unit_of_measurement": "Jednostka miary", + "value_template": "Szablon warto\u015bci" + }, + "data_description": { + "column": "Kolumna dla zwr\u00f3conego zapytania do przedstawienia jako stan", + "db_url": "Adres URL bazy danych, pozostaw puste, aby u\u017cy\u0107 domy\u015blnej bazy danych HA", + "query": "Zapytanie do uruchomienia, musi zaczyna\u0107 si\u0119 od \u201eSELECT\u201d", + "unit_of_measurement": "Jednostka miary (opcjonalnie)", + "value_template": "Szablon warto\u015bci (opcjonalnie)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/pt-BR.json b/homeassistant/components/sql/translations/pt-BR.json new file mode 100644 index 00000000000..10df43c7e1f --- /dev/null +++ b/homeassistant/components/sql/translations/pt-BR.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada" + }, + "error": { + "db_url_invalid": "URL do banco de dados inv\u00e1lida", + "query_invalid": "Consulta SQL inv\u00e1lida", + "value_template_invalid": "Modelo do valor inv\u00e1lido" + }, + "step": { + "user": { + "data": { + "column": "Coluna", + "db_url": "URL do banco de dados", + "query": "Selecionar consulta", + "unit_of_measurement": "Unidade de medida", + "value_template": "Modelo do valor" + }, + "data_description": { + "column": "Coluna para a consulta retornada para apresentar como estado", + "db_url": "URL do banco de dados, deixe em branco para usar o banco de dados padr\u00e3o", + "query": "Consulte para ser executada, precisa come\u00e7ar com 'SELECT'", + "unit_of_measurement": "Unidade de medida (opcional)", + "value_template": "Modelo do valor (opcional)" + } + } + } + }, + "options": { + "error": { + "db_url_invalid": "URL do banco de dados inv\u00e1lida", + "query_invalid": "Consulta SQL inv\u00e1lida", + "value_template_invalid": "Modelo do valor inv\u00e1lido" + }, + "step": { + "init": { + "data": { + "column": "Coluna", + "db_url": "URL do banco de dados", + "query": "Selecionar consulta", + "unit_of_measurement": "Unidade de medida", + "value_template": "Modelo do valor" + }, + "data_description": { + "column": "Coluna para a consulta retornada para apresentar como estado", + "db_url": "URL do banco de dados, deixe em branco para usar o banco de dados padr\u00e3o", + "query": "Consulte para ser executada, precisa come\u00e7ar com 'SELECT'", + "unit_of_measurement": "Unidade de medida (opcional)", + "value_template": "Modelo do valor (opcional)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/ru.json b/homeassistant/components/sql/translations/ru.json new file mode 100644 index 00000000000..8f8e0741583 --- /dev/null +++ b/homeassistant/components/sql/translations/ru.json @@ -0,0 +1,55 @@ +{ + "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": { + "db_url_invalid": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445.", + "query_invalid": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 SQL-\u0437\u0430\u043f\u0440\u043e\u0441.", + "value_template_invalid": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u0448\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f." + }, + "step": { + "user": { + "data": { + "column": "\u0421\u0442\u043e\u043b\u0431\u0435\u0446", + "db_url": "URL-\u0430\u0434\u0440\u0435\u0441 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445", + "query": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0437\u0430\u043f\u0440\u043e\u0441", + "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f", + "value_template": "\u0428\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f" + }, + "data_description": { + "column": "\u0421\u0442\u043e\u043b\u0431\u0435\u0446 \u0434\u043b\u044f \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0432 \u0432\u0438\u0434\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", + "db_url": "URL-\u0430\u0434\u0440\u0435\u0441 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445. \u041e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0431\u0430\u0437\u0443 \u0434\u0430\u043d\u043d\u044b\u0445 HA \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e", + "query": "\u0417\u0430\u043f\u0440\u043e\u0441 \u0434\u043b\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f, \u0434\u043e\u043b\u0436\u0435\u043d \u043d\u0430\u0447\u0438\u043d\u0430\u0442\u044c\u0441\u044f \u0441 'SELECT'", + "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "value_template": "\u0428\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" + } + } + } + }, + "options": { + "error": { + "db_url_invalid": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445.", + "query_invalid": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 SQL-\u0437\u0430\u043f\u0440\u043e\u0441.", + "value_template_invalid": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u0448\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f." + }, + "step": { + "init": { + "data": { + "column": "\u0421\u0442\u043e\u043b\u0431\u0435\u0446", + "db_url": "URL-\u0430\u0434\u0440\u0435\u0441 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445", + "query": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0437\u0430\u043f\u0440\u043e\u0441", + "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f", + "value_template": "\u0428\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f" + }, + "data_description": { + "column": "\u0421\u0442\u043e\u043b\u0431\u0435\u0446 \u0434\u043b\u044f \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0432 \u0432\u0438\u0434\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", + "db_url": "URL-\u0430\u0434\u0440\u0435\u0441 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445. \u041e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0431\u0430\u0437\u0443 \u0434\u0430\u043d\u043d\u044b\u0445 HA \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e", + "query": "\u0417\u0430\u043f\u0440\u043e\u0441 \u0434\u043b\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f, \u0434\u043e\u043b\u0436\u0435\u043d \u043d\u0430\u0447\u0438\u043d\u0430\u0442\u044c\u0441\u044f \u0441 'SELECT'", + "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "value_template": "\u0428\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/tr.json b/homeassistant/components/sql/translations/tr.json new file mode 100644 index 00000000000..edbce58d65a --- /dev/null +++ b/homeassistant/components/sql/translations/tr.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "db_url_invalid": "Veritaban\u0131 URL'si ge\u00e7ersiz", + "query_invalid": "SQL Sorgusu ge\u00e7ersiz", + "value_template_invalid": "De\u011fer \u015eablonu ge\u00e7ersiz" + }, + "step": { + "user": { + "data": { + "column": "S\u00fctun", + "db_url": "Veritaban\u0131 URL'si", + "query": "Sorgu Se\u00e7", + "unit_of_measurement": "\u00d6l\u00e7\u00fc Birimi", + "value_template": "De\u011fer \u015eablonu" + }, + "data_description": { + "column": "D\u00f6nd\u00fcr\u00fclen sorgunun durum olarak sunulmas\u0131 i\u00e7in s\u00fctun", + "db_url": "Veritaban\u0131 URL'si, varsay\u0131lan HA veritaban\u0131n\u0131 kullanmak i\u00e7in bo\u015f b\u0131rak\u0131n", + "query": "\u00c7al\u0131\u015ft\u0131r\u0131lacak sorgu, 'SE\u00c7' ile ba\u015flamal\u0131d\u0131r", + "unit_of_measurement": "\u00d6l\u00e7\u00fc Birimi (iste\u011fe ba\u011fl\u0131)", + "value_template": "De\u011fer \u015eablonu (iste\u011fe ba\u011fl\u0131)" + } + } + } + }, + "options": { + "error": { + "db_url_invalid": "Veritaban\u0131 URL'si ge\u00e7ersiz", + "query_invalid": "SQL Sorgusu ge\u00e7ersiz", + "value_template_invalid": "De\u011fer \u015eablonu ge\u00e7ersiz" + }, + "step": { + "init": { + "data": { + "column": "S\u00fctun", + "db_url": "Veritaban\u0131 URL'si", + "query": "Sorgu Se\u00e7", + "unit_of_measurement": "\u00d6l\u00e7\u00fc Birimi", + "value_template": "De\u011fer \u015eablonu" + }, + "data_description": { + "column": "D\u00f6nd\u00fcr\u00fclen sorgunun durum olarak sunulmas\u0131 i\u00e7in s\u00fctun", + "db_url": "Veritaban\u0131 URL'si, varsay\u0131lan HA veritaban\u0131n\u0131 kullanmak i\u00e7in bo\u015f b\u0131rak\u0131n", + "query": "\u00c7al\u0131\u015ft\u0131r\u0131lacak sorgu, 'SE\u00c7' ile ba\u015flamal\u0131d\u0131r", + "unit_of_measurement": "\u00d6l\u00e7\u00fc Birimi (iste\u011fe ba\u011fl\u0131)", + "value_template": "De\u011fer \u015eablonu (iste\u011fe ba\u011fl\u0131)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/zh-Hant.json b/homeassistant/components/sql/translations/zh-Hant.json new file mode 100644 index 00000000000..801d973049d --- /dev/null +++ b/homeassistant/components/sql/translations/zh-Hant.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "db_url_invalid": "\u8cc7\u6599\u5eab URL \u7121\u6548", + "query_invalid": "SQL \u67e5\u8a62\u7121\u6548", + "value_template_invalid": "\u6578\u503c\u6a21\u677f\u7121\u6548" + }, + "step": { + "user": { + "data": { + "column": "\u6b04\u4f4d", + "db_url": "\u8cc7\u6599\u5eab URL", + "query": "\u9078\u64c7\u67e5\u8a62", + "unit_of_measurement": "\u6e2c\u91cf\u55ae\u4f4d", + "value_template": "\u6578\u503c\u6a21\u677f" + }, + "data_description": { + "column": "\u67e5\u8a62\u56de\u8986\u6b04\u4f4d\u70ba\u72c0\u614b", + "db_url": "\u8cc7\u6599\u5eab URL\u3001\u4fdd\u7559\u7a7a\u767d\u4ee5\u4f7f\u7528\u9810\u8a2d HA \u8cc7\u6599\u5eab", + "query": "\u57f7\u884c\u7684\u67e5\u8a62\u3001\u9700\u8981\u4ee5 'SELECT' \u4f5c\u70ba\u958b\u982d", + "unit_of_measurement": "\u6e2c\u91cf\u55ae\u4f4d\uff08\u9078\u9805\uff09", + "value_template": "\u6578\u503c\u6a21\u677f\uff08\u9078\u9805\uff09" + } + } + } + }, + "options": { + "error": { + "db_url_invalid": "\u8cc7\u6599\u5eab URL \u7121\u6548", + "query_invalid": "SQL \u67e5\u8a62\u7121\u6548", + "value_template_invalid": "\u6578\u503c\u6a21\u677f\u7121\u6548" + }, + "step": { + "init": { + "data": { + "column": "\u6b04\u4f4d", + "db_url": "\u8cc7\u6599\u5eab URL", + "query": "\u9078\u64c7\u67e5\u8a62", + "unit_of_measurement": "\u6e2c\u91cf\u55ae\u4f4d", + "value_template": "\u6578\u503c\u6a21\u677f" + }, + "data_description": { + "column": "\u67e5\u8a62\u56de\u8986\u6b04\u4f4d\u70ba\u72c0\u614b", + "db_url": "\u8cc7\u6599\u5eab URL\u3001\u4fdd\u7559\u7a7a\u767d\u4ee5\u4f7f\u7528\u9810\u8a2d HA \u8cc7\u6599\u5eab", + "query": "\u57f7\u884c\u7684\u67e5\u8a62\u3001\u9700\u8981\u4ee5 'SELECT' \u4f5c\u70ba\u958b\u982d", + "unit_of_measurement": "\u6e2c\u91cf\u55ae\u4f4d\uff08\u9078\u9805\uff09", + "value_template": "\u6578\u503c\u6a21\u677f\uff08\u9078\u9805\uff09" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/ca.json b/homeassistant/components/srp_energy/translations/ca.json index c6617e617d4..45a2d2fa943 100644 --- a/homeassistant/components/srp_energy/translations/ca.json +++ b/homeassistant/components/srp_energy/translations/ca.json @@ -19,6 +19,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/cs.json b/homeassistant/components/srp_energy/translations/cs.json index 74b4bd53090..74045d75b2e 100644 --- a/homeassistant/components/srp_energy/translations/cs.json +++ b/homeassistant/components/srp_energy/translations/cs.json @@ -18,6 +18,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/de.json b/homeassistant/components/srp_energy/translations/de.json index a7992cac9b1..d1975b8f59d 100644 --- a/homeassistant/components/srp_energy/translations/de.json +++ b/homeassistant/components/srp_energy/translations/de.json @@ -19,6 +19,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/el.json b/homeassistant/components/srp_energy/translations/el.json index 4583eb47823..99a5f1ea850 100644 --- a/homeassistant/components/srp_energy/translations/el.json +++ b/homeassistant/components/srp_energy/translations/el.json @@ -19,6 +19,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/en.json b/homeassistant/components/srp_energy/translations/en.json index 99926b18b4f..a031e56022e 100644 --- a/homeassistant/components/srp_energy/translations/en.json +++ b/homeassistant/components/srp_energy/translations/en.json @@ -19,6 +19,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/es.json b/homeassistant/components/srp_energy/translations/es.json index 849c5019d3b..ebd4583fe43 100644 --- a/homeassistant/components/srp_energy/translations/es.json +++ b/homeassistant/components/srp_energy/translations/es.json @@ -19,6 +19,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/et.json b/homeassistant/components/srp_energy/translations/et.json index 558bb4a19ed..52f3de847fb 100644 --- a/homeassistant/components/srp_energy/translations/et.json +++ b/homeassistant/components/srp_energy/translations/et.json @@ -19,6 +19,5 @@ } } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/fr.json b/homeassistant/components/srp_energy/translations/fr.json index b8544d4d469..93c6ade07c4 100644 --- a/homeassistant/components/srp_energy/translations/fr.json +++ b/homeassistant/components/srp_energy/translations/fr.json @@ -19,6 +19,5 @@ } } } - }, - "title": "\u00c9nergie SRP" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/hu.json b/homeassistant/components/srp_energy/translations/hu.json index 4d617e09cfc..b032f24e5fc 100644 --- a/homeassistant/components/srp_energy/translations/hu.json +++ b/homeassistant/components/srp_energy/translations/hu.json @@ -19,6 +19,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/id.json b/homeassistant/components/srp_energy/translations/id.json index fefcbff2ecb..113e07b987f 100644 --- a/homeassistant/components/srp_energy/translations/id.json +++ b/homeassistant/components/srp_energy/translations/id.json @@ -19,6 +19,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/it.json b/homeassistant/components/srp_energy/translations/it.json index d5ccf02d74c..f81b5e2e1a4 100644 --- a/homeassistant/components/srp_energy/translations/it.json +++ b/homeassistant/components/srp_energy/translations/it.json @@ -19,6 +19,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/ja.json b/homeassistant/components/srp_energy/translations/ja.json index 432c553910b..805a500502b 100644 --- a/homeassistant/components/srp_energy/translations/ja.json +++ b/homeassistant/components/srp_energy/translations/ja.json @@ -19,6 +19,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/ka.json b/homeassistant/components/srp_energy/translations/ka.json index f4e15ad5d9e..3aec1ed2f43 100644 --- a/homeassistant/components/srp_energy/translations/ka.json +++ b/homeassistant/components/srp_energy/translations/ka.json @@ -19,6 +19,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/ko.json b/homeassistant/components/srp_energy/translations/ko.json index b329cf1f2b1..bb0d912111e 100644 --- a/homeassistant/components/srp_energy/translations/ko.json +++ b/homeassistant/components/srp_energy/translations/ko.json @@ -19,6 +19,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/nl.json b/homeassistant/components/srp_energy/translations/nl.json index 3cf2298b348..ce4ac90c223 100644 --- a/homeassistant/components/srp_energy/translations/nl.json +++ b/homeassistant/components/srp_energy/translations/nl.json @@ -19,6 +19,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/no.json b/homeassistant/components/srp_energy/translations/no.json index 5505e140cd3..51bb1c7fd18 100644 --- a/homeassistant/components/srp_energy/translations/no.json +++ b/homeassistant/components/srp_energy/translations/no.json @@ -19,6 +19,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/pl.json b/homeassistant/components/srp_energy/translations/pl.json index f89165a9065..eec873b6820 100644 --- a/homeassistant/components/srp_energy/translations/pl.json +++ b/homeassistant/components/srp_energy/translations/pl.json @@ -19,6 +19,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/pt-BR.json b/homeassistant/components/srp_energy/translations/pt-BR.json index b0dbfe9c6d0..e7613d468a3 100644 --- a/homeassistant/components/srp_energy/translations/pt-BR.json +++ b/homeassistant/components/srp_energy/translations/pt-BR.json @@ -19,6 +19,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/ru.json b/homeassistant/components/srp_energy/translations/ru.json index a492fa7dfb2..a94e18aa20a 100644 --- a/homeassistant/components/srp_energy/translations/ru.json +++ b/homeassistant/components/srp_energy/translations/ru.json @@ -19,6 +19,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/tr.json b/homeassistant/components/srp_energy/translations/tr.json index ead8238d82c..0c815f97186 100644 --- a/homeassistant/components/srp_energy/translations/tr.json +++ b/homeassistant/components/srp_energy/translations/tr.json @@ -19,6 +19,5 @@ } } } - }, - "title": "SRP Enerji" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/uk.json b/homeassistant/components/srp_energy/translations/uk.json index 144e40f15bb..2d4b399947f 100644 --- a/homeassistant/components/srp_energy/translations/uk.json +++ b/homeassistant/components/srp_energy/translations/uk.json @@ -19,6 +19,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/zh-Hans.json b/homeassistant/components/srp_energy/translations/zh-Hans.json index 36016f3e217..b0b26b02261 100644 --- a/homeassistant/components/srp_energy/translations/zh-Hans.json +++ b/homeassistant/components/srp_energy/translations/zh-Hans.json @@ -8,6 +8,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/zh-Hant.json b/homeassistant/components/srp_energy/translations/zh-Hant.json index adbf635100c..bed1cd5c3c1 100644 --- a/homeassistant/components/srp_energy/translations/zh-Hant.json +++ b/homeassistant/components/srp_energy/translations/zh-Hant.json @@ -19,6 +19,5 @@ } } } - }, - "title": "SRP Energy" + } } \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/de.json b/homeassistant/components/steam_online/translations/de.json new file mode 100644 index 00000000000..17950e14e24 --- /dev/null +++ b/homeassistant/components/steam_online/translations/de.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_account": "Ung\u00fcltige Konto-ID", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "reauth_confirm": { + "description": "Die Steam-Integration muss manuell erneut authentifiziert werden \n\nDeinen Schl\u00fcssel findest du hier: https://steamcommunity.com/dev/apikey", + "title": "Integration erneut authentifizieren" + }, + "user": { + "data": { + "account": "Steam-Konto-ID", + "api_key": "API-Schl\u00fcssel" + }, + "description": "Verwende https://steamid.io, um deine Steam-Konto-ID zu finden" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "accounts": "Namen der zu \u00fcberwachenden Konten" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/el.json b/homeassistant/components/steam_online/translations/el.json new file mode 100644 index 00000000000..0f598dbc395 --- /dev/null +++ b/homeassistant/components/steam_online/translations/el.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_account": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "reauth_confirm": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Steam \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03c5\u03c4\u03b5\u03af \u03be\u03b1\u03bd\u03ac \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf \n\n \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c3\u03b1\u03c2 \u03b5\u03b4\u03ce: https://steamcommunity.com/dev/apikey", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, + "user": { + "data": { + "account": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Steam", + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, + "description": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf https://steamid.io \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c4\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Steam" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "accounts": "\u039f\u03bd\u03cc\u03bc\u03b1\u03c4\u03b1 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03ce\u03bd \u03c0\u03c1\u03bf\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/en.json b/homeassistant/components/steam_online/translations/en.json index 69d80890d5a..8b34a85922d 100644 --- a/homeassistant/components/steam_online/translations/en.json +++ b/homeassistant/components/steam_online/translations/en.json @@ -6,22 +6,22 @@ }, "error": { "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", "invalid_account": "Invalid account ID", + "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, "step": { + "reauth_confirm": { + "description": "The Steam integration needs to be manually re-authenticated\n\nYou can find your key here: https://steamcommunity.com/dev/apikey", + "title": "Reauthenticate Integration" + }, "user": { "data": { - "api_key": "API Key", - "account": "Steam account ID" + "account": "Steam account ID", + "api_key": "API Key" }, - "description": "Documentation: https://www.home-assistant.io/integrations/steam_online\n\nUse https://steamid.io/ to find your Steam account ID" - }, - "reauth_confirm": { - "title": "Reauthenticate Integration", - "description": "The Steam integration needs to be manually re-authenticated\n\nYou can find your key here: https://steamcommunity.com/dev/apikey" - } + "description": "Use https://steamid.io to find your Steam account ID" + } } }, "options": { diff --git a/homeassistant/components/steam_online/translations/et.json b/homeassistant/components/steam_online/translations/et.json new file mode 100644 index 00000000000..8d501ccf50d --- /dev/null +++ b/homeassistant/components/steam_online/translations/et.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_account": "Kehtetu konto ID", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "reauth_confirm": { + "description": "Steami sidumine tuleb uuesti autentida\n\nV\u00f5tme leiad siit: https://steamcommunity.com/dev/apikey", + "title": "Taastuvasta sidumine" + }, + "user": { + "data": { + "account": "Steami konto ID", + "api_key": "API v\u00f5ti" + }, + "description": "Kasuta https://steamid.io, et leida oma Steam'i konto ID" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "accounts": "J\u00e4lgitavate kontode nimed" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/fr.json b/homeassistant/components/steam_online/translations/fr.json new file mode 100644 index 00000000000..5ee6892eef7 --- /dev/null +++ b/homeassistant/components/steam_online/translations/fr.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_account": "ID de compte non valide", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "reauth_confirm": { + "description": "L'int\u00e9gration Steam doit \u00eatre r\u00e9-authentifi\u00e9e manuellement\n\nVous pouvez trouver votre cl\u00e9 ici\u00a0: https://steamcommunity.com/dev/apikey", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, + "user": { + "data": { + "account": "ID de compte Steam", + "api_key": "Cl\u00e9 d'API" + }, + "description": "Utilisez https://steamid.io pour trouver l'ID de votre compte Steam" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "accounts": "Noms des comptes \u00e0 surveiller" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/hu.json b/homeassistant/components/steam_online/translations/hu.json new file mode 100644 index 00000000000..9bd0976345e --- /dev/null +++ b/homeassistant/components/steam_online/translations/hu.json @@ -0,0 +1,36 @@ +{ + "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_account": "\u00c9rv\u00e9nytelen fi\u00f3kazonos\u00edt\u00f3", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "reauth_confirm": { + "description": "A Steam integr\u00e1ci\u00f3t manu\u00e1lisan \u00fajra kell hiteles\u00edteni .\n\nA kulcs itt tal\u00e1lhat\u00f3: https://steamcommunity.com/dev/apikey", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, + "user": { + "data": { + "account": "Steam fi\u00f3k azonos\u00edt\u00f3ja", + "api_key": "API kulcs" + }, + "description": "A https://steamid.io haszn\u00e1lat\u00e1val kaphat\u00f3 meg a Steam fi\u00f3kazonos\u00edt\u00f3" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "accounts": "A nyomon k\u00f6vetend\u0151 fi\u00f3kok nevei" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/id.json b/homeassistant/components/steam_online/translations/id.json new file mode 100644 index 00000000000..07d708067fc --- /dev/null +++ b/homeassistant/components/steam_online/translations/id.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_account": "ID akun tidak valid", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "reauth_confirm": { + "description": "Integrasi Steam perlu diautentikasi ulang secara manual \n\n Anda dapat menemukan kunci Anda di sini: https://steamcommunity.com/dev/apikey", + "title": "Autentikasi Ulang Integrasi" + }, + "user": { + "data": { + "account": "ID akun Steam", + "api_key": "Kunci API" + }, + "description": "Gunakan https://steamid.io untuk menemukan ID akun Steam Anda" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "accounts": "Nama akun yang akan dipantau" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/it.json b/homeassistant/components/steam_online/translations/it.json new file mode 100644 index 00000000000..71036870961 --- /dev/null +++ b/homeassistant/components/steam_online/translations/it.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_account": "ID account non valido", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "reauth_confirm": { + "description": "L'integrazione di Steam richiede una nuova autenticazione manuale \n\nPuoi trovare la tua chiave qui: https://steamcommunity.com/dev/apikey", + "title": "Autentica nuovamente l'integrazione" + }, + "user": { + "data": { + "account": "ID dell'account Steam", + "api_key": "Chiave API" + }, + "description": "Usa https://steamid.io per trovare l'ID del tuo account Steam" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "accounts": "Nomi degli account da monitorare" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/nl.json b/homeassistant/components/steam_online/translations/nl.json new file mode 100644 index 00000000000..20600463f6a --- /dev/null +++ b/homeassistant/components/steam_online/translations/nl.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Service is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_account": "Ongeldige account ID", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "reauth_confirm": { + "description": "De Steam-integratie moet handmatig opnieuw worden geauthenticeerd.\n\nU vind uw API-sleutel hier: https://steamcommunity.com/dev/apikey", + "title": "Verifieer de integratie opnieuw" + }, + "user": { + "data": { + "account": "Steam Account-ID", + "api_key": "API-sleutel" + }, + "description": "Gebruik https://steamid.io om je Steam Account-ID te vinden." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "accounts": "Namen van de accounts die gemonitord moeten worden" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/no.json b/homeassistant/components/steam_online/translations/no.json new file mode 100644 index 00000000000..7e0122dfe18 --- /dev/null +++ b/homeassistant/components/steam_online/translations/no.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_account": "Ugyldig konto-ID", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "reauth_confirm": { + "description": "Steam-integrasjonen m\u00e5 re-autentiseres manuelt \n\n Du finner n\u00f8kkelen din her: https://steamcommunity.com/dev/apikey", + "title": "Godkjenne integrering p\u00e5 nytt" + }, + "user": { + "data": { + "account": "Steam-konto-ID", + "api_key": "API-n\u00f8kkel" + }, + "description": "Bruk https://steamid.io for \u00e5 finne din Steam-konto-ID" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "accounts": "Navn p\u00e5 kontoer som skal overv\u00e5kes" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/pl.json b/homeassistant/components/steam_online/translations/pl.json new file mode 100644 index 00000000000..09c95bca9c4 --- /dev/null +++ b/homeassistant/components/steam_online/translations/pl.json @@ -0,0 +1,36 @@ +{ + "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_account": "Niepoprawny identyfikator konta", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "reauth_confirm": { + "description": "Integracja Steam musi zosta\u0107 ponownie uwierzytelniona r\u0119cznie\n\nSw\u00f3j klucz znajdziesz tutaj: https://steamcommunity.com/dev/apikey", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, + "user": { + "data": { + "account": "Identyfikator konta Steam", + "api_key": "Klucz API" + }, + "description": "U\u017cyj https://steamid.io, aby znale\u017a\u0107 sw\u00f3j identyfikator konta Steam" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "accounts": "Nazwy kont, kt\u00f3re maj\u0105 by\u0107 monitorowane" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/pt-BR.json b/homeassistant/components/steam_online/translations/pt-BR.json new file mode 100644 index 00000000000..93ff3e00dc5 --- /dev/null +++ b/homeassistant/components/steam_online/translations/pt-BR.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_account": "ID de conta inv\u00e1lido", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "description": "A integra\u00e7\u00e3o da Steam precisa ser autenticada manualmente\n\nVoc\u00ea pode encontrar sua chave aqui: https://steamcommunity.com/dev/apikey", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "account": "ID da conta Steam", + "api_key": "Chave da API" + }, + "description": "Use https://steamid.io para encontrar o ID da sua conta Steam" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "accounts": "Nomes das contas a serem monitoradas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/ru.json b/homeassistant/components/steam_online/translations/ru.json new file mode 100644 index 00000000000..990226036d9 --- /dev/null +++ b/homeassistant/components/steam_online/translations/ru.json @@ -0,0 +1,25 @@ +{ + "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_account": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438.", + "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": { + "reauth_confirm": { + "description": "\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0439\u0442\u0438 \u0441\u0432\u043e\u0439 \u043a\u043b\u044e\u0447 \u0437\u0434\u0435\u0441\u044c: https://steamcommunity.com/dev/apikey", + "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": { + "account": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Steam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/zh-Hant.json b/homeassistant/components/steam_online/translations/zh-Hant.json new file mode 100644 index 00000000000..775c9710592 --- /dev/null +++ b/homeassistant/components/steam_online/translations/zh-Hant.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_account": "\u5e33\u865f ID \u7121\u6548", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "reauth_confirm": { + "description": "Steam \u6574\u5408\u9700\u8981\u624b\u52d5\u91cd\u65b0\u8a8d\u8b49\n\n\u53ef\u4ee5\u65bc\u5f8c\u65b9\u7db2\u5740\u627e\u5230\u91d1\u9470\uff1ahttps://steamcommunity.com/dev/apikey", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, + "user": { + "data": { + "account": "Steam \u5e33\u865f ID", + "api_key": "API \u91d1\u9470" + }, + "description": "\u4f7f\u7528 https://steamid.io \u4ee5\u78ba\u8a8d\u60a8\u7684 Steam \u5e33\u865f ID" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "accounts": "\u6240\u8981\u76e3\u63a7\u7684\u5e33\u865f\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steamist/translations/hu.json b/homeassistant/components/steamist/translations/hu.json index 50cf36394cc..66448b03ab2 100644 --- a/homeassistant/components/steamist/translations/hu.json +++ b/homeassistant/components/steamist/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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "not_steamist_device": "Nem egy seamist k\u00e9sz\u00fcl\u00e9k" diff --git a/homeassistant/components/subaru/translations/ca.json b/homeassistant/components/subaru/translations/ca.json index 6b83af06bb8..31ea78bcd46 100644 --- a/homeassistant/components/subaru/translations/ca.json +++ b/homeassistant/components/subaru/translations/ca.json @@ -6,9 +6,12 @@ }, "error": { "bad_pin_format": "El PIN ha de tenir 4 d\u00edgits", + "bad_validation_code_format": "El codi de validaci\u00f3 ha de tenir 6 d\u00edgits", "cannot_connect": "Ha fallat la connexi\u00f3", "incorrect_pin": "PIN incorrecte", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + "incorrect_validation_code": "Codi de validaci\u00f3 inv\u00e0lid", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "two_factor_request_failed": "La sol\u00b7licitud de codi 2FA ha fallat. Torna-ho a provar" }, "step": { "pin": { @@ -18,6 +21,20 @@ "description": "Introdueix el teu PIN de MySubaru\nNOTA: tots els vehicles associats a un compte han de tenir el mateix PIN", "title": "Configuraci\u00f3 de Subaru Starlink" }, + "two_factor": { + "data": { + "contact_method": "Selecciona un m\u00e8tode de contacte:" + }, + "description": "Autenticaci\u00f3 de dos factors requerida", + "title": "Configuraci\u00f3 de Subaru Starlink" + }, + "two_factor_validate": { + "data": { + "validation_code": "Codi de validaci\u00f3" + }, + "description": "Introdueix el codi de validaci\u00f3 rebut", + "title": "Configuraci\u00f3 de Subaru Starlink" + }, "user": { "data": { "country": "Selecciona un pa\u00eds", diff --git a/homeassistant/components/subaru/translations/de.json b/homeassistant/components/subaru/translations/de.json index 4f10a4266ae..2156df0d1a8 100644 --- a/homeassistant/components/subaru/translations/de.json +++ b/homeassistant/components/subaru/translations/de.json @@ -6,9 +6,12 @@ }, "error": { "bad_pin_format": "Die PIN sollte 4-stellig sein", + "bad_validation_code_format": "Der Validierungscode sollte 6-stellig sein", "cannot_connect": "Verbindung fehlgeschlagen", "incorrect_pin": "Falsche PIN", - "invalid_auth": "Ung\u00fcltige Authentifizierung" + "incorrect_validation_code": "Falscher Validierungscode", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "two_factor_request_failed": "Anfrage f\u00fcr 2FA-Code fehlgeschlagen, bitte versuche es erneut" }, "step": { "pin": { @@ -18,6 +21,20 @@ "description": "Bitte gib deinen MySubaru-PIN ein\nHINWEIS: Alle Fahrzeuge im Konto m\u00fcssen dieselbe PIN haben", "title": "Subaru Starlink Konfiguration" }, + "two_factor": { + "data": { + "contact_method": "Bitte w\u00e4hle eine Kontaktmethode:" + }, + "description": "Zwei-Faktor-Authentifizierung erforderlich", + "title": "Subaru Starlink Konfiguration" + }, + "two_factor_validate": { + "data": { + "validation_code": "Validierungscode" + }, + "description": "Bitte gib den erhaltenen Validierungscode ein", + "title": "Subaru Starlink Konfiguration" + }, "user": { "data": { "country": "Land ausw\u00e4hlen", diff --git a/homeassistant/components/subaru/translations/el.json b/homeassistant/components/subaru/translations/el.json index 30ff37ccc1e..0376e95225b 100644 --- a/homeassistant/components/subaru/translations/el.json +++ b/homeassistant/components/subaru/translations/el.json @@ -6,9 +6,12 @@ }, "error": { "bad_pin_format": "\u03a4\u03bf PIN \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 4 \u03c8\u03b7\u03c6\u03af\u03b1", + "bad_validation_code_format": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03c0\u03b9\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 6 \u03c8\u03b7\u03c6\u03af\u03c9\u03bd", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "incorrect_pin": "\u039b\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03bf PIN", - "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + "incorrect_validation_code": "\u0395\u03c3\u03c6\u03b1\u03bb\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03c0\u03b9\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "two_factor_request_failed": "\u03a4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03b3\u03b9\u03b1 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc 2FA \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac" }, "step": { "pin": { @@ -18,6 +21,20 @@ "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf PIN \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf MySubaru\n\u03a3\u0397\u039c\u0395\u0399\u03a9\u03a3\u0397: \u038c\u03bb\u03b1 \u03c4\u03b1 \u03bf\u03c7\u03ae\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03bf\u03c5\u03bd \u03c4\u03bf\u03bd \u03af\u03b4\u03b9\u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc PIN", "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Subaru Starlink" }, + "two_factor": { + "data": { + "contact_method": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b1\u03c2:" + }, + "description": "\u0391\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Subaru Starlink" + }, + "two_factor_validate": { + "data": { + "validation_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03c0\u03b9\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7\u03c2" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03c0\u03b9\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03b5\u03bb\u03ae\u03c6\u03b8\u03b7", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Subaru Starlink" + }, "user": { "data": { "country": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c7\u03ce\u03c1\u03b1\u03c2", diff --git a/homeassistant/components/subaru/translations/en.json b/homeassistant/components/subaru/translations/en.json index 722363d4d74..ca2ad3c2c92 100644 --- a/homeassistant/components/subaru/translations/en.json +++ b/homeassistant/components/subaru/translations/en.json @@ -6,9 +6,12 @@ }, "error": { "bad_pin_format": "PIN should be 4 digits", + "bad_validation_code_format": "Validation code should be 6 digits", "cannot_connect": "Failed to connect", "incorrect_pin": "Incorrect PIN", - "invalid_auth": "Invalid authentication" + "incorrect_validation_code": "Incorrect validation code", + "invalid_auth": "Invalid authentication", + "two_factor_request_failed": "Request for 2FA code failed, please try again" }, "step": { "pin": { @@ -18,6 +21,20 @@ "description": "Please enter your MySubaru PIN\nNOTE: All vehicles in account must have the same PIN", "title": "Subaru Starlink Configuration" }, + "two_factor": { + "data": { + "contact_method": "Please select a contact method:" + }, + "description": "Two factor authentication required", + "title": "Subaru Starlink Configuration" + }, + "two_factor_validate": { + "data": { + "validation_code": "Validation code" + }, + "description": "Please enter validation code received", + "title": "Subaru Starlink Configuration" + }, "user": { "data": { "country": "Select country", diff --git a/homeassistant/components/subaru/translations/et.json b/homeassistant/components/subaru/translations/et.json index e7390b3b77f..acfa5055402 100644 --- a/homeassistant/components/subaru/translations/et.json +++ b/homeassistant/components/subaru/translations/et.json @@ -6,9 +6,12 @@ }, "error": { "bad_pin_format": "PIN-kood peaks olema 4-kohaline", + "bad_validation_code_format": "Kinnituskood peaks olema 6 kohaline", "cannot_connect": "\u00dchendamine nurjus", "incorrect_pin": "Vale PIN-kood", - "invalid_auth": "Vigane autentimine" + "incorrect_validation_code": "Vale kinnituskood", + "invalid_auth": "Vigane autentimine", + "two_factor_request_failed": "2FA koodi taotlus nurjus, proovi uuesti" }, "step": { "pin": { @@ -18,6 +21,20 @@ "description": "Sisesta oma MySubaru PIN-kood\n M\u00c4RKUS. K\u00f5igil kontol olevatel s\u00f5idukitel peab olema sama PIN-kood", "title": "Subaru Starlinki konfiguratsioon" }, + "two_factor": { + "data": { + "contact_method": "Vali kontaktimeetod" + }, + "description": "N\u00f5utav on kaheastmeline autentimine", + "title": "Subaru Starlink s\u00e4tted" + }, + "two_factor_validate": { + "data": { + "validation_code": "Kinnituskood" + }, + "description": "Sisesta saadud kinnituskood", + "title": "Subaru Starlink s\u00e4tted" + }, "user": { "data": { "country": "Vali riik", diff --git a/homeassistant/components/subaru/translations/fr.json b/homeassistant/components/subaru/translations/fr.json index fafcf81157b..a2b7159a6be 100644 --- a/homeassistant/components/subaru/translations/fr.json +++ b/homeassistant/components/subaru/translations/fr.json @@ -6,9 +6,12 @@ }, "error": { "bad_pin_format": "Le code PIN doit \u00eatre compos\u00e9 de 4 chiffres", + "bad_validation_code_format": "Le code de validation doit \u00eatre compos\u00e9 de six chiffres", "cannot_connect": "\u00c9chec de connexion", "incorrect_pin": "PIN incorrect", - "invalid_auth": "Authentification non valide" + "incorrect_validation_code": "Code de validation incorrect", + "invalid_auth": "Authentification non valide", + "two_factor_request_failed": "La demande de code 2FA a \u00e9chou\u00e9, veuillez r\u00e9essayer" }, "step": { "pin": { @@ -18,6 +21,20 @@ "description": "Veuillez entrer votre NIP MySubaru\nREMARQUE : Tous les v\u00e9hicules en compte doivent avoir le m\u00eame NIP", "title": "Configuration de Subaru Starlink" }, + "two_factor": { + "data": { + "contact_method": "Veuillez s\u00e9lectionner une m\u00e9thode de contact\u00a0:" + }, + "description": "Authentification \u00e0 deux facteurs requise", + "title": "Configuration Subaru Starlink" + }, + "two_factor_validate": { + "data": { + "validation_code": "Code de validation" + }, + "description": "Veuillez saisir le code de validation re\u00e7u", + "title": "Configuration Subaru Starlink" + }, "user": { "data": { "country": "Choisissez le pays", diff --git a/homeassistant/components/subaru/translations/hu.json b/homeassistant/components/subaru/translations/hu.json index a54ddd57c39..42f9f6bb2c9 100644 --- a/homeassistant/components/subaru/translations/hu.json +++ b/homeassistant/components/subaru/translations/hu.json @@ -6,9 +6,12 @@ }, "error": { "bad_pin_format": "A PIN-nek 4 sz\u00e1mjegy\u0171nek kell lennie", + "bad_validation_code_format": "Az \u00e9rv\u00e9nyes\u00edt\u0151 k\u00f3dnak 6 sz\u00e1mjegyb\u0151l kell \u00e1llnia", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "incorrect_pin": "Helytelen PIN", - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + "incorrect_validation_code": "Helytelen \u00e9rv\u00e9nyes\u00edt\u00e9si k\u00f3d", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "two_factor_request_failed": "A k\u00e9tfaktoros hiteles\u00edt\u00e9si k\u00f3d lek\u00e9r\u00e9se sikertelen, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra" }, "step": { "pin": { @@ -18,9 +21,23 @@ "description": "K\u00e9rj\u00fck, adja meg MySubaru PIN-k\u00f3dj\u00e1t\n MEGJEGYZ\u00c9S: A sz\u00e1ml\u00e1n szerepl\u0151 \u00f6sszes j\u00e1rm\u0171nek azonos PIN-k\u00f3ddal kell rendelkeznie", "title": "Subaru Starlink konfigur\u00e1ci\u00f3" }, + "two_factor": { + "data": { + "contact_method": "V\u00e1lasszon kapcsolatfelv\u00e9teli m\u00f3dot:" + }, + "description": "K\u00e9tfaktoros hiteles\u00edt\u00e9s sz\u00fcks\u00e9ges", + "title": "Subaru Starlink konfigur\u00e1ci\u00f3" + }, + "two_factor_validate": { + "data": { + "validation_code": "\u00c9rv\u00e9nyes\u00edt\u00e9si k\u00f3d" + }, + "description": "K\u00e9rj\u00fck, adja meg az \u00e9rv\u00e9nyes\u00edt\u00e9si k\u00f3dot", + "title": "Subaru Starlink konfigur\u00e1ci\u00f3" + }, "user": { "data": { - "country": "V\u00e1lassz orsz\u00e1got", + "country": "V\u00e1lasszon orsz\u00e1got", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, diff --git a/homeassistant/components/subaru/translations/id.json b/homeassistant/components/subaru/translations/id.json index 2a44ec16b92..cf0d37a0c30 100644 --- a/homeassistant/components/subaru/translations/id.json +++ b/homeassistant/components/subaru/translations/id.json @@ -6,9 +6,12 @@ }, "error": { "bad_pin_format": "PIN harus terdiri dari 4 angka", + "bad_validation_code_format": "Kode validasi harus terdiri dari 6 angka", "cannot_connect": "Gagal terhubung", "incorrect_pin": "PIN salah", - "invalid_auth": "Autentikasi tidak valid" + "incorrect_validation_code": "Kode validasi salah", + "invalid_auth": "Autentikasi tidak valid", + "two_factor_request_failed": "Permintaan kode 2FA gagal, silakan coba lagi" }, "step": { "pin": { @@ -18,6 +21,20 @@ "description": "Masukkan PIN MySubaru Anda\nCATATAN: Semua kendaraan dalam akun harus memiliki PIN yang sama", "title": "Konfigurasi Subaru Starlink" }, + "two_factor": { + "data": { + "contact_method": "Pilih metode kontak:" + }, + "description": "Diperlukan autentikasi dua faktor", + "title": "Konfigurasi Subaru Starlink" + }, + "two_factor_validate": { + "data": { + "validation_code": "Kode validasi" + }, + "description": "Masukkan kode validasi yang diterima", + "title": "Konfigurasi Subaru Starlink" + }, "user": { "data": { "country": "Pilih negara", diff --git a/homeassistant/components/subaru/translations/it.json b/homeassistant/components/subaru/translations/it.json index a6752b810eb..ea8399dbdc5 100644 --- a/homeassistant/components/subaru/translations/it.json +++ b/homeassistant/components/subaru/translations/it.json @@ -6,9 +6,12 @@ }, "error": { "bad_pin_format": "Il PIN deve essere di 4 cifre", + "bad_validation_code_format": "Il codice di convalida deve essere di 6 cifre", "cannot_connect": "Impossibile connettersi", "incorrect_pin": "PIN errato", - "invalid_auth": "Autenticazione non valida" + "incorrect_validation_code": "Codice di convalida errato", + "invalid_auth": "Autenticazione non valida", + "two_factor_request_failed": "Richiesta di codice 2FA non riuscita, riprova" }, "step": { "pin": { @@ -18,6 +21,20 @@ "description": "Inserisci il tuo PIN MySubaru\nNOTA: tutti i veicoli nell'account devono avere lo stesso PIN", "title": "Configurazione Subaru Starlink" }, + "two_factor": { + "data": { + "contact_method": "Seleziona un metodo di contatto:" + }, + "description": "Autenticazione a due fattori richiesta", + "title": "Configurazione Subaru Starlink" + }, + "two_factor_validate": { + "data": { + "validation_code": "Codice di validazione" + }, + "description": "Inserisci il codice di convalida ricevuto", + "title": "Configurazione Subaru Starlink" + }, "user": { "data": { "country": "Seleziona il paese", diff --git a/homeassistant/components/subaru/translations/ja.json b/homeassistant/components/subaru/translations/ja.json index b8949709464..8dcba4b4b23 100644 --- a/homeassistant/components/subaru/translations/ja.json +++ b/homeassistant/components/subaru/translations/ja.json @@ -6,9 +6,12 @@ }, "error": { "bad_pin_format": "PIN\u306f4\u6841\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "bad_validation_code_format": "\u691c\u8a3c\u30b3\u30fc\u30c9\u306f6\u6841\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "incorrect_pin": "PIN\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093", - "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + "incorrect_validation_code": "\u691c\u8a3c\u30b3\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "two_factor_request_failed": "2FA\u30b3\u30fc\u30c9\u306e\u30ea\u30af\u30a8\u30b9\u30c8\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044" }, "step": { "pin": { @@ -18,6 +21,20 @@ "description": "MySubaru PIN\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\n\u6ce8: \u30a2\u30ab\u30a6\u30f3\u30c8\u5185\u306e\u3059\u3079\u3066\u306e\u8eca\u4e21\u306f\u3001\u540c\u3058PIN\u3092\u6301\u3063\u3066\u3044\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "title": "Subaru Starlink\u306e\u8a2d\u5b9a" }, + "two_factor": { + "data": { + "contact_method": "\u9023\u7d61\u65b9\u6cd5\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\uff1a" + }, + "description": "\u4e8c\u8981\u7d20\u8a8d\u8a3c\u304c\u5fc5\u8981", + "title": "Subaru Starlink\u306e\u8a2d\u5b9a" + }, + "two_factor_validate": { + "data": { + "validation_code": "\u691c\u8a3c\u30b3\u30fc\u30c9" + }, + "description": "\u53d7\u3051\u53d6\u3063\u305f\u691c\u8a3c\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "Subaru Starlink\u306e\u8a2d\u5b9a" + }, "user": { "data": { "country": "\u56fd\u3092\u9078\u629e", diff --git a/homeassistant/components/subaru/translations/nl.json b/homeassistant/components/subaru/translations/nl.json index 931ed6f967f..256180d444b 100644 --- a/homeassistant/components/subaru/translations/nl.json +++ b/homeassistant/components/subaru/translations/nl.json @@ -6,9 +6,12 @@ }, "error": { "bad_pin_format": "De pincode moet uit 4 cijfers bestaan", + "bad_validation_code_format": "De validatiecode moet uit 6 cijfers bestaan", "cannot_connect": "Kan geen verbinding maken", "incorrect_pin": "Onjuiste PIN", - "invalid_auth": "Ongeldige authenticatie" + "incorrect_validation_code": "Onjuiste validatiecode", + "invalid_auth": "Ongeldige authenticatie", + "two_factor_request_failed": "Verzoek om 2FA code mislukt, probeer opnieuw" }, "step": { "pin": { @@ -18,6 +21,20 @@ "description": "Voer uw MySubaru-pincode in\n OPMERKING: Alle voertuigen in een account moeten dezelfde pincode hebben", "title": "Subaru Starlink Configuratie" }, + "two_factor": { + "data": { + "contact_method": "Selecteer een contactmethode:" + }, + "description": "Tweefactorauthenticatie vereist", + "title": "Subaru Starlink-configuratie" + }, + "two_factor_validate": { + "data": { + "validation_code": "Validatiecode" + }, + "description": "Voer de ontvangen validatiecode in", + "title": "Subaru Starlink-configuratie" + }, "user": { "data": { "country": "Selecteer land", diff --git a/homeassistant/components/subaru/translations/no.json b/homeassistant/components/subaru/translations/no.json index 6ac200ec131..3e3a78949db 100644 --- a/homeassistant/components/subaru/translations/no.json +++ b/homeassistant/components/subaru/translations/no.json @@ -6,9 +6,12 @@ }, "error": { "bad_pin_format": "PIN-koden skal best\u00e5 av fire sifre", + "bad_validation_code_format": "Valideringskoden skal v\u00e6re p\u00e5 6 sifre", "cannot_connect": "Tilkobling mislyktes", "incorrect_pin": "Feil PIN", - "invalid_auth": "Ugyldig godkjenning" + "incorrect_validation_code": "Feil valideringskode", + "invalid_auth": "Ugyldig godkjenning", + "two_factor_request_failed": "Foresp\u00f8rsel om 2FA-kode mislyktes, pr\u00f8v igjen" }, "step": { "pin": { @@ -18,6 +21,20 @@ "description": "Vennligst skriv inn MySubaru PIN-koden\n MERKNAD: Alle kj\u00f8ret\u00f8yer som er kontoen m\u00e5 ha samme PIN-kode", "title": "Subaru Starlink-konfigurasjon" }, + "two_factor": { + "data": { + "contact_method": "Velg en kontaktmetode:" + }, + "description": "Tofaktorautentisering kreves", + "title": "Subaru Starlink-konfigurasjon" + }, + "two_factor_validate": { + "data": { + "validation_code": "Valideringskode" + }, + "description": "Vennligst skriv inn valideringskode mottatt", + "title": "Subaru Starlink-konfigurasjon" + }, "user": { "data": { "country": "Velg land", diff --git a/homeassistant/components/subaru/translations/pl.json b/homeassistant/components/subaru/translations/pl.json index b0d491d475e..679e5779507 100644 --- a/homeassistant/components/subaru/translations/pl.json +++ b/homeassistant/components/subaru/translations/pl.json @@ -6,9 +6,12 @@ }, "error": { "bad_pin_format": "PIN powinien sk\u0142ada\u0107 si\u0119 z 4 cyfr", + "bad_validation_code_format": "Kod weryfikacyjny powinien mie\u0107 6 cyfr", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "incorrect_pin": "Nieprawid\u0142owy PIN", - "invalid_auth": "Niepoprawne uwierzytelnienie" + "incorrect_validation_code": "Nieprawid\u0142owy kod weryfikacyjny", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "two_factor_request_failed": "\u017b\u0105danie kodu 2FA nie powiod\u0142o si\u0119, spr\u00f3buj ponownie" }, "step": { "pin": { @@ -18,6 +21,20 @@ "description": "Wprowad\u017a sw\u00f3j PIN dla MySubaru\nUWAGA: Wszystkie pojazdy na koncie musz\u0105 mie\u0107 ten sam kod PIN", "title": "Konfiguracja Subaru Starlink" }, + "two_factor": { + "data": { + "contact_method": "Prosz\u0119 wybra\u0107 spos\u00f3b kontaktu:" + }, + "description": "Wymagane uwierzytelnianie dwusk\u0142adnikowe", + "title": "Konfiguracja Subaru Starlink" + }, + "two_factor_validate": { + "data": { + "validation_code": "Kod weryfikacyjny" + }, + "description": "Wprowad\u017a otrzymany kod weryfikacyjny", + "title": "Konfiguracja Subaru Starlink" + }, "user": { "data": { "country": "Wybierz kraj", diff --git a/homeassistant/components/subaru/translations/pt-BR.json b/homeassistant/components/subaru/translations/pt-BR.json index e88a9e883c0..e53a16fa33f 100644 --- a/homeassistant/components/subaru/translations/pt-BR.json +++ b/homeassistant/components/subaru/translations/pt-BR.json @@ -6,9 +6,12 @@ }, "error": { "bad_pin_format": "O PIN deve ter 4 d\u00edgitos", + "bad_validation_code_format": "O c\u00f3digo de valida\u00e7\u00e3o deve ter 6 d\u00edgitos", "cannot_connect": "Falha ao conectar", "incorrect_pin": "PIN incorreto", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + "incorrect_validation_code": "C\u00f3digo de valida\u00e7\u00e3o incorreto", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "two_factor_request_failed": "Falha na solicita\u00e7\u00e3o do c\u00f3digo 2FA, por favor tente novamente" }, "step": { "pin": { @@ -18,6 +21,20 @@ "description": "Por favor, digite seu PIN MySubaru\n NOTA: Todos os ve\u00edculos em conta devem ter o mesmo PIN", "title": "Configura\u00e7\u00e3o do Subaru Starlink" }, + "two_factor": { + "data": { + "contact_method": "Selecione um m\u00e9todo de contato:" + }, + "description": "Autentica\u00e7\u00e3o de dois fatores necess\u00e1ria", + "title": "Configura\u00e7\u00e3o do Subaru Starlink" + }, + "two_factor_validate": { + "data": { + "validation_code": "C\u00f3digo de valida\u00e7\u00e3o" + }, + "description": "Insira o c\u00f3digo de valida\u00e7\u00e3o recebido", + "title": "Configura\u00e7\u00e3o do Subaru Starlink" + }, "user": { "data": { "country": "Selecione o pa\u00eds", diff --git a/homeassistant/components/subaru/translations/ru.json b/homeassistant/components/subaru/translations/ru.json index 87f56f8ac8a..45eddd02bc1 100644 --- a/homeassistant/components/subaru/translations/ru.json +++ b/homeassistant/components/subaru/translations/ru.json @@ -6,9 +6,12 @@ }, "error": { "bad_pin_format": "PIN-\u043a\u043e\u0434 \u0434\u043e\u043b\u0436\u0435\u043d \u0441\u043e\u0441\u0442\u043e\u044f\u0442\u044c \u0438\u0437 4 \u0446\u0438\u0444\u0440.", + "bad_validation_code_format": "\u041a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f \u0434\u043e\u043b\u0436\u0435\u043d \u0441\u043e\u0441\u0442\u043e\u044f\u0442\u044c \u0438\u0437 6 \u0446\u0438\u0444\u0440.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "incorrect_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434.", - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + "incorrect_validation_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "two_factor_request_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0440\u043e\u0441\u0438\u0442\u044c \u043a\u043e\u0434 2FA. \u041f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443." }, "step": { "pin": { @@ -18,6 +21,20 @@ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434 MySubaru.\n\u0412\u0441\u0435 \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0438\u043b\u0438 \u0432 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0438\u043c\u0435\u0442\u044c \u043e\u0434\u0438\u043d\u0430\u043a\u043e\u0432\u044b\u0439 PIN-\u043a\u043e\u0434.", "title": "Subaru Starlink" }, + "two_factor": { + "data": { + "contact_method": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0441\u0432\u044f\u0437\u0438:" + }, + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\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", + "title": "Subaru Starlink" + }, + "two_factor_validate": { + "data": { + "validation_code": "\u041a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f" + }, + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f.", + "title": "Subaru Starlink" + }, "user": { "data": { "country": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0442\u0440\u0430\u043d\u0443", diff --git a/homeassistant/components/subaru/translations/tr.json b/homeassistant/components/subaru/translations/tr.json index 5724bd4f2c3..3b9f1acc60e 100644 --- a/homeassistant/components/subaru/translations/tr.json +++ b/homeassistant/components/subaru/translations/tr.json @@ -6,9 +6,12 @@ }, "error": { "bad_pin_format": "PIN 4 haneli olmal\u0131d\u0131r", + "bad_validation_code_format": "Do\u011frulama kodu 6 basamakl\u0131 olmal\u0131d\u0131r", "cannot_connect": "Ba\u011flanma hatas\u0131", "incorrect_pin": "Yanl\u0131\u015f PIN", - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + "incorrect_validation_code": "Yanl\u0131\u015f do\u011frulama kodu", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "two_factor_request_failed": "2FA kodu iste\u011fi ba\u015far\u0131s\u0131z oldu, l\u00fctfen tekrar deneyin" }, "step": { "pin": { @@ -18,6 +21,20 @@ "description": "L\u00fctfen MySubaru PIN'inizi girin\n NOT: Hesaptaki t\u00fcm ara\u00e7lar ayn\u0131 PIN'e sahip olmal\u0131d\u0131r", "title": "Subaru Starlink Yap\u0131land\u0131rmas\u0131" }, + "two_factor": { + "data": { + "contact_method": "L\u00fctfen bir ileti\u015fim y\u00f6ntemi se\u00e7in:" + }, + "description": "\u0130ki fakt\u00f6rl\u00fc kimlik do\u011frulama gerekli", + "title": "Subaru Starlink Yap\u0131land\u0131rmas\u0131" + }, + "two_factor_validate": { + "data": { + "validation_code": "Do\u011frulama kodu" + }, + "description": "L\u00fctfen al\u0131nan do\u011frulama kodunu girin", + "title": "Subaru Starlink Yap\u0131land\u0131rmas\u0131" + }, "user": { "data": { "country": "\u00dclkeyi se\u00e7", diff --git a/homeassistant/components/subaru/translations/zh-Hant.json b/homeassistant/components/subaru/translations/zh-Hant.json index 6264f01791e..2e4a22423e0 100644 --- a/homeassistant/components/subaru/translations/zh-Hant.json +++ b/homeassistant/components/subaru/translations/zh-Hant.json @@ -6,9 +6,12 @@ }, "error": { "bad_pin_format": "PIN \u78bc\u61c9\u8a72\u70ba 4 \u4f4d\u6578\u5b57", + "bad_validation_code_format": "\u9a57\u8b49\u78bc\u5fc5\u9808\u5305\u542b 6 \u4f4d\u6578\u5b57", "cannot_connect": "\u9023\u7dda\u5931\u6557", "incorrect_pin": "PIN \u78bc\u932f\u8aa4", - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + "incorrect_validation_code": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "two_factor_request_failed": "\u6240\u9700\u96d9\u91cd\u8a8d\u8b49\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21" }, "step": { "pin": { @@ -18,6 +21,20 @@ "description": "\u8acb\u8f38\u5165 MySubaru PIN \u78bc\n\u6ce8\u610f\uff1a\u6240\u4ee5\u5e33\u865f\u5167\u8eca\u8f1b\u90fd\u5fc5\u9808\u4f7f\u7528\u76f8\u540c PIN \u78bc", "title": "Subaru Starlink \u8a2d\u5b9a" }, + "two_factor": { + "data": { + "contact_method": "\u8acb\u9078\u64c7\u806f\u7d61\u65b9\u5f0f\uff1a" + }, + "description": "\u9700\u8981\u96d9\u91cd\u8a8d\u8b49\u78bc", + "title": "Subaru Starlink \u8a2d\u5b9a" + }, + "two_factor_validate": { + "data": { + "validation_code": "\u9a57\u8b49\u78bc" + }, + "description": "\u8acb\u8f38\u5165\u6240\u6536\u5230\u7684\u9a57\u8b49\u78bc", + "title": "Subaru Starlink \u8a2d\u5b9a" + }, "user": { "data": { "country": "\u9078\u64c7\u570b\u5bb6", diff --git a/homeassistant/components/sun/translations/bg.json b/homeassistant/components/sun/translations/bg.json index 7b6c5241cd2..81ead95c95f 100644 --- a/homeassistant/components/sun/translations/bg.json +++ b/homeassistant/components/sun/translations/bg.json @@ -1,4 +1,9 @@ { + "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." + } + }, "state": { "_": { "above_horizon": "\u041d\u0430\u0434 \u0445\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430", diff --git a/homeassistant/components/sun/translations/ca.json b/homeassistant/components/sun/translations/ca.json index 5a49afefc5d..00a37d8b2b3 100644 --- a/homeassistant/components/sun/translations/ca.json +++ b/homeassistant/components/sun/translations/ca.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "user": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + } + } + }, "state": { "_": { "above_horizon": "Sobre l'horitz\u00f3", diff --git a/homeassistant/components/sun/translations/cs.json b/homeassistant/components/sun/translations/cs.json index fe13d3e4fb0..b4a01f4473e 100644 --- a/homeassistant/components/sun/translations/cs.json +++ b/homeassistant/components/sun/translations/cs.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "step": { + "user": { + "description": "Chcete za\u010d\u00edt nastavovat?" + } + } + }, "state": { "_": { "above_horizon": "Nad obzorem", diff --git a/homeassistant/components/sun/translations/de.json b/homeassistant/components/sun/translations/de.json index 6b81cf14f76..405c9dac6d4 100644 --- a/homeassistant/components/sun/translations/de.json +++ b/homeassistant/components/sun/translations/de.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "user": { + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" + } + } + }, "state": { "_": { "above_horizon": "\u00dcber dem Horizont", diff --git a/homeassistant/components/sun/translations/el.json b/homeassistant/components/sun/translations/el.json index 5079c2476aa..fa3d87d574b 100644 --- a/homeassistant/components/sun/translations/el.json +++ b/homeassistant/components/sun/translations/el.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + }, + "step": { + "user": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7;" + } + } + }, "state": { "_": { "above_horizon": "\u03a0\u03ac\u03bd\u03c9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03bf\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1", diff --git a/homeassistant/components/sun/translations/en.json b/homeassistant/components/sun/translations/en.json index 2278e262bb8..367fdab917c 100644 --- a/homeassistant/components/sun/translations/en.json +++ b/homeassistant/components/sun/translations/en.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "user": { + "description": "Do you want to start set up?" + } + } + }, "state": { "_": { "above_horizon": "Above horizon", diff --git a/homeassistant/components/sun/translations/et.json b/homeassistant/components/sun/translations/et.json index 1a4020215f0..d2fe374ddfc 100644 --- a/homeassistant/components/sun/translations/et.json +++ b/homeassistant/components/sun/translations/et.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + }, + "step": { + "user": { + "description": "Kas soovid alustada seadistamist?" + } + } + }, "state": { "_": { "above_horizon": "T\u00f5usnud", diff --git a/homeassistant/components/sun/translations/fr.json b/homeassistant/components/sun/translations/fr.json index a878c8022b1..b743a33499d 100644 --- a/homeassistant/components/sun/translations/fr.json +++ b/homeassistant/components/sun/translations/fr.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "step": { + "user": { + "description": "Voulez-vous commencer la configuration\u00a0?" + } + } + }, "state": { "_": { "above_horizon": "Au-dessus de l'horizon", diff --git a/homeassistant/components/sun/translations/he.json b/homeassistant/components/sun/translations/he.json index 26a2def7d00..2c851de857e 100644 --- a/homeassistant/components/sun/translations/he.json +++ b/homeassistant/components/sun/translations/he.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } + } + }, "state": { "_": { "above_horizon": "\u05de\u05e2\u05dc \u05d4\u05d0\u05d5\u05e4\u05e7", diff --git a/homeassistant/components/sun/translations/hu.json b/homeassistant/components/sun/translations/hu.json index 2275d61b35a..402320f02f9 100644 --- a/homeassistant/components/sun/translations/hu.json +++ b/homeassistant/components/sun/translations/hu.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "user": { + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" + } + } + }, "state": { "_": { "above_horizon": "L\u00e1t\u00f3hat\u00e1r felett", diff --git a/homeassistant/components/sun/translations/id.json b/homeassistant/components/sun/translations/id.json index df6c960e67d..789a42556e9 100644 --- a/homeassistant/components/sun/translations/id.json +++ b/homeassistant/components/sun/translations/id.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "user": { + "description": "Ingin memulai penyiapan?" + } + } + }, "state": { "_": { "above_horizon": "Terbit", diff --git a/homeassistant/components/sun/translations/it.json b/homeassistant/components/sun/translations/it.json index fe2c65461cd..48f0e4a8d90 100644 --- a/homeassistant/components/sun/translations/it.json +++ b/homeassistant/components/sun/translations/it.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "step": { + "user": { + "description": "Vuoi iniziare la configurazione?" + } + } + }, "state": { "_": { "above_horizon": "Sopra l'orizzonte", diff --git a/homeassistant/components/sun/translations/ja.json b/homeassistant/components/sun/translations/ja.json index 758beba9dbf..8188e950389 100644 --- a/homeassistant/components/sun/translations/ja.json +++ b/homeassistant/components/sun/translations/ja.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "step": { + "user": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + } + } + }, "state": { "_": { "above_horizon": "\u5730\u5e73\u7dda\u3088\u308a\u4e0a", diff --git a/homeassistant/components/sun/translations/nl.json b/homeassistant/components/sun/translations/nl.json index 6abe34481fa..8c284e4e43d 100644 --- a/homeassistant/components/sun/translations/nl.json +++ b/homeassistant/components/sun/translations/nl.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "step": { + "user": { + "description": "Wilt u beginnen met instellen?" + } + } + }, "state": { "_": { "above_horizon": "Boven de horizon", diff --git a/homeassistant/components/sun/translations/no.json b/homeassistant/components/sun/translations/no.json index 8597fea2ce5..18bb6e25545 100644 --- a/homeassistant/components/sun/translations/no.json +++ b/homeassistant/components/sun/translations/no.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "step": { + "user": { + "description": "Vil du starte oppsettet?" + } + } + }, "state": { "_": { "above_horizon": "Over horisonten", diff --git a/homeassistant/components/sun/translations/pl.json b/homeassistant/components/sun/translations/pl.json index 1f00babd1fd..7e95dd568c6 100644 --- a/homeassistant/components/sun/translations/pl.json +++ b/homeassistant/components/sun/translations/pl.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "user": { + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + } + } + }, "state": { "_": { "above_horizon": "nad horyzontem", diff --git a/homeassistant/components/sun/translations/pt-BR.json b/homeassistant/components/sun/translations/pt-BR.json index 2f060112a0c..fe965b08372 100644 --- a/homeassistant/components/sun/translations/pt-BR.json +++ b/homeassistant/components/sun/translations/pt-BR.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + } + } + }, "state": { "_": { "above_horizon": "Acima do horizonte", diff --git a/homeassistant/components/sun/translations/ru.json b/homeassistant/components/sun/translations/ru.json index 7ddf3165aa9..2eca13058de 100644 --- a/homeassistant/components/sun/translations/ru.json +++ b/homeassistant/components/sun/translations/ru.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + } + } + }, "state": { "_": { "above_horizon": "\u041d\u0430\u0434 \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u043e\u043c", diff --git a/homeassistant/components/sun/translations/tr.json b/homeassistant/components/sun/translations/tr.json index 50634454b98..cff510523fa 100644 --- a/homeassistant/components/sun/translations/tr.json +++ b/homeassistant/components/sun/translations/tr.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "user": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } + } + }, "state": { "_": { "above_horizon": "G\u00fcnd\u00fcz", diff --git a/homeassistant/components/sun/translations/zh-Hant.json b/homeassistant/components/sun/translations/zh-Hant.json index 4b8da898c70..e3b7cfb9c54 100644 --- a/homeassistant/components/sun/translations/zh-Hant.json +++ b/homeassistant/components/sun/translations/zh-Hant.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + } + } + }, "state": { "_": { "above_horizon": "\u65e5\u51fa\u6771\u6d77", diff --git a/homeassistant/components/switch/translations/cs.json b/homeassistant/components/switch/translations/cs.json index fe398d9b72e..a7b7f35033e 100644 --- a/homeassistant/components/switch/translations/cs.json +++ b/homeassistant/components/switch/translations/cs.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "init": { + "data": { + "entity_id": "Entita vyp\u00edna\u010de" + }, + "description": "Vyberte vyp\u00edna\u010d pro sv\u011btlo." + } + } + }, "device_automation": { "action_type": { "toggle": "P\u0159epnout {entity_name}", diff --git a/homeassistant/components/switch/translations/el.json b/homeassistant/components/switch/translations/el.json index 2067276af42..986ee7cff92 100644 --- a/homeassistant/components/switch/translations/el.json +++ b/homeassistant/components/switch/translations/el.json @@ -28,8 +28,8 @@ }, "state": { "_": { - "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc\u03c2", - "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc\u03c2" + "off": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", + "on": "\u0391\u03bd\u03bf\u03b9\u03c7\u03c4\u03cc" } }, "title": "\u0394\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7\u03c2" diff --git a/homeassistant/components/switch/translations/hu.json b/homeassistant/components/switch/translations/hu.json index 423a48f345d..2dff00040ca 100644 --- a/homeassistant/components/switch/translations/hu.json +++ b/homeassistant/components/switch/translations/hu.json @@ -20,6 +20,7 @@ "is_on": "{entity_name} be van kapcsolva" }, "trigger_type": { + "changed_states": "{entity_name} be- vagy kikapcsolt", "toggled": "{entity_name} \u00e1tkapcsolt", "turned_off": "{entity_name} ki lett kapcsolva", "turned_on": "{entity_name} be lett kapcsolva" diff --git a/homeassistant/components/switch/translations/pt-BR.json b/homeassistant/components/switch/translations/pt-BR.json index 6f7f076332a..0d24cd74bfe 100644 --- a/homeassistant/components/switch/translations/pt-BR.json +++ b/homeassistant/components/switch/translations/pt-BR.json @@ -3,9 +3,9 @@ "step": { "init": { "data": { - "entity_id": "Entidade de switch" + "entity_id": "Entidade de interruptor" }, - "description": "Selecione o switch para o interruptor de luz." + "description": "Selecione o interruptor para a l\u00e2mpada." } } }, diff --git a/homeassistant/components/switch_as_x/translations/bg.json b/homeassistant/components/switch_as_x/translations/bg.json new file mode 100644 index 00000000000..f5c2a6e0433 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "target_domain": "\u0422\u0438\u043f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/ca.json b/homeassistant/components/switch_as_x/translations/ca.json index 07d6288d33e..f48a744066c 100644 --- a/homeassistant/components/switch_as_x/translations/ca.json +++ b/homeassistant/components/switch_as_x/translations/ca.json @@ -1,14 +1,20 @@ { "config": { "step": { - "init": { + "config": { + "user": { + "entity_id": "Entitat d'interruptor" + } + }, + "user": { "data": { - "entity_id": "Entitat d'interruptor", - "target_domain": "Tipus" + "entity_id": "Commutador", + "target_domain": "Nou tipus" }, - "title": "Converteix un interruptor en \u2026" + "description": "Tria un commutador que vulguis que aparegui a Home Assistant com a llum, coberta o qualsevol altra cosa. El commutador original s'amagar\u00e0.", + "title": "Canvia el tipus de dispositiu commutador" } } }, - "title": "Interruptor com a X" + "title": "Canvia el tipus de dispositiu d'un commutador" } \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/cs.json b/homeassistant/components/switch_as_x/translations/cs.json new file mode 100644 index 00000000000..11f8471a62a --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "config": { + "user": { + "entity_id": "Entita vyp\u00edna\u010de" + } + }, + "user": { + "data": { + "target_domain": "Typ" + }, + "title": "Ud\u011blejte vyp\u00edna\u010dem ..." + } + } + }, + "title": "Zm\u011bna typy vyp\u00edna\u010de" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/de.json b/homeassistant/components/switch_as_x/translations/de.json index 63a99ad40e2..1125f4f5a8f 100644 --- a/homeassistant/components/switch_as_x/translations/de.json +++ b/homeassistant/components/switch_as_x/translations/de.json @@ -1,14 +1,20 @@ { "config": { "step": { - "init": { + "config": { + "user": { + "entity_id": "Switch-Entit\u00e4t" + } + }, + "user": { "data": { - "entity_id": "Switch-Entit\u00e4t", - "target_domain": "Typ" + "entity_id": "Schalter", + "target_domain": "Neuer Typ" }, - "title": "Mache einen Schalter zu..." + "description": "W\u00e4hle einen Schalter, der im Home Assistant als Licht, Abdeckung oder sonstiges angezeigt werden soll. Der urspr\u00fcngliche Schalter wird ausgeblendet.", + "title": "Switch-Ger\u00e4tetyp \u00e4ndern" } } }, - "title": "Schalter als X" + "title": "Ger\u00e4tetyp eines Schalters \u00e4ndern" } \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/el.json b/homeassistant/components/switch_as_x/translations/el.json index 1fb546c13b7..ed10f241927 100644 --- a/homeassistant/components/switch_as_x/translations/el.json +++ b/homeassistant/components/switch_as_x/translations/el.json @@ -1,11 +1,17 @@ { "config": { "step": { - "init": { + "config": { + "user": { + "entity_id": "\u039f\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7" + } + }, + "user": { "data": { - "entity_id": "\u039f\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7", + "entity_id": "\u0394\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7\u03c2", "target_domain": "\u03a4\u03cd\u03c0\u03bf\u03c2" }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant \u03c9\u03c2 \u03c6\u03c9\u03c2, \u03ba\u03ac\u03bb\u03c5\u03bc\u03bc\u03b1 \u03ae \u03bf\u03c4\u03b9\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5 \u03ac\u03bb\u03bb\u03bf. \u039f \u03b1\u03c1\u03c7\u03b9\u03ba\u03cc\u03c2 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7\u03c2 \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03c1\u03c5\u03bc\u03bc\u03ad\u03bd\u03bf\u03c2.", "title": "\u039a\u03ac\u03bd\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7 \u03ad\u03bd\u03b1 ..." } } diff --git a/homeassistant/components/switch_as_x/translations/en.json b/homeassistant/components/switch_as_x/translations/en.json index 7709a27cf35..4253f0506ef 100644 --- a/homeassistant/components/switch_as_x/translations/en.json +++ b/homeassistant/components/switch_as_x/translations/en.json @@ -1,12 +1,18 @@ { "config": { "step": { + "config": { + "user": { + "entity_id": "Switch entity" + } + }, "user": { "data": { "entity_id": "Switch", "target_domain": "New Type" }, - "description": "Pick a switch that you want to show up in Home Assistant as a light, cover or anything else. The original switch will be hidden." + "description": "Pick a switch that you want to show up in Home Assistant as a light, cover or anything else. The original switch will be hidden.", + "title": "Change switch device type" } } }, diff --git a/homeassistant/components/switch_as_x/translations/et.json b/homeassistant/components/switch_as_x/translations/et.json index 9e18ddced09..9d5a5839fbf 100644 --- a/homeassistant/components/switch_as_x/translations/et.json +++ b/homeassistant/components/switch_as_x/translations/et.json @@ -1,14 +1,20 @@ { "config": { "step": { - "init": { + "config": { + "user": { + "entity_id": "L\u00fcliti olem" + } + }, + "user": { "data": { - "entity_id": "L\u00fcliti olem", - "target_domain": "T\u00fc\u00fcp" + "entity_id": "L\u00fcliti", + "target_domain": "Uus t\u00fc\u00fcp" }, - "title": "Tee l\u00fcliti ..." + "description": "Vali l\u00fcliti mida soovid Home Assistantis valgustina, avakattena v\u00f5i millegi muuna kuvada. Algne l\u00fcliti peidetakse.", + "title": "Muuda l\u00fclitusseadme t\u00fc\u00fcpi" } } }, - "title": "L\u00fclita kui X" + "title": "Muuda l\u00fcliti t\u00fc\u00fcpi" } \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/fr.json b/homeassistant/components/switch_as_x/translations/fr.json index 4b5bd6beebe..fa14bd80885 100644 --- a/homeassistant/components/switch_as_x/translations/fr.json +++ b/homeassistant/components/switch_as_x/translations/fr.json @@ -1,14 +1,20 @@ { "config": { "step": { - "init": { + "config": { + "user": { + "entity_id": "Entit\u00e9 du commutateur" + } + }, + "user": { "data": { - "entity_id": "Entit\u00e9 du commutateur", - "target_domain": "Type" + "entity_id": "Interrupteur", + "target_domain": "Nouveau type" }, - "title": "Transformer un commutateur en \u2026" + "description": "Choisissez un interrupteur que vous voulez faire appara\u00eetre dans Home Assistant comme une lumi\u00e8re, une fermeture ou autre. L'interrupteur original sera cach\u00e9.", + "title": "Modifier le type d'appareil de l'interrupteur" } } }, - "title": "Commutateur en tant que X" + "title": "Modifier le type d'appareil d'un commutateur" } \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/he.json b/homeassistant/components/switch_as_x/translations/he.json index 8ca833876fe..39889dab709 100644 --- a/homeassistant/components/switch_as_x/translations/he.json +++ b/homeassistant/components/switch_as_x/translations/he.json @@ -1,8 +1,8 @@ { "config": { "step": { - "init": { - "data": { + "config": { + "user": { "entity_id": "\u05d9\u05e9\u05d5\u05ea \u05de\u05ea\u05d2" } } diff --git a/homeassistant/components/switch_as_x/translations/hu.json b/homeassistant/components/switch_as_x/translations/hu.json index b3ea0af39fd..0d919f5ecd3 100644 --- a/homeassistant/components/switch_as_x/translations/hu.json +++ b/homeassistant/components/switch_as_x/translations/hu.json @@ -1,14 +1,20 @@ { "config": { "step": { - "init": { + "config": { + "user": { + "entity_id": "Kapcsol\u00f3 entit\u00e1s" + } + }, + "user": { "data": { - "entity_id": "Kapcsol\u00f3 entit\u00e1s", - "target_domain": "T\u00edpus" + "entity_id": "Kapcsol\u00f3", + "target_domain": "\u00daj t\u00edpus" }, - "title": "Kapcsol\u00f3 mint..." + "description": "V\u00e1lassza ki azt a kapcsol\u00f3t, amelyet meg szeretne jelen\u00edteni a Home Assistantban l\u00e1mpak\u00e9nt, red\u0151nyk\u00e9nt vagy b\u00e1rmi m\u00e1sk\u00e9nt. Az eredeti kapcsol\u00f3 el lesz rejtve.", + "title": "Kapcsol\u00f3 eszk\u00f6zt\u00edpus m\u00f3dos\u00edt\u00e1sa" } } }, - "title": "Kapcsol\u00f3 mint X" + "title": "Kapcsol\u00f3 mint m\u00e1s eszk\u00f6z" } \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/id.json b/homeassistant/components/switch_as_x/translations/id.json index 1a5cce08f01..47d31e26c03 100644 --- a/homeassistant/components/switch_as_x/translations/id.json +++ b/homeassistant/components/switch_as_x/translations/id.json @@ -1,14 +1,20 @@ { "config": { "step": { - "init": { + "config": { + "user": { + "entity_id": "Entitas saklar" + } + }, + "user": { "data": { - "entity_id": "Entitas saklar", - "target_domain": "Jenis" + "entity_id": "Sakelar", + "target_domain": "Tipe Baru" }, - "title": "Jadikan saklar sebagai\u2026" + "description": "Pilih sakelar yang ingin Anda tampilkan di Home Assistant sebagai lampu, penutup, atau apa pun. Sakelar asli akan disembunyikan.", + "title": "Ubah jenis perangkat sakelar" } } }, - "title": "Saklar sebagai X" + "title": "Ubah jenis perangkat sakelar" } \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/it.json b/homeassistant/components/switch_as_x/translations/it.json index 1ef154b0578..4706b4d9ece 100644 --- a/homeassistant/components/switch_as_x/translations/it.json +++ b/homeassistant/components/switch_as_x/translations/it.json @@ -1,14 +1,20 @@ { "config": { "step": { - "init": { + "config": { + "user": { + "entity_id": "Cambia entit\u00e0" + } + }, + "user": { "data": { - "entity_id": "Cambia entit\u00e0", - "target_domain": "Tipo" + "entity_id": "Interruttore", + "target_domain": "Nuovo tipo" }, - "title": "Rendi un interruttore un..." + "description": "Scegli un interruttore che vuoi mostrare in Home Assistant come luce, copertura o qualsiasi altra cosa. L'interruttore originale sar\u00e0 nascosto.", + "title": "Cambia tipo di dispositivo dell'interruttore" } } }, - "title": "Interruttore come X" + "title": "Cambia il tipo di dispositivo di un interruttore" } \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/ja.json b/homeassistant/components/switch_as_x/translations/ja.json index 44ceaecdd75..434e1b588d5 100644 --- a/homeassistant/components/switch_as_x/translations/ja.json +++ b/homeassistant/components/switch_as_x/translations/ja.json @@ -1,11 +1,17 @@ { "config": { "step": { - "init": { + "config": { + "user": { + "entity_id": "\u30b9\u30a4\u30c3\u30c1\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3" + } + }, + "user": { "data": { - "entity_id": "\u30b9\u30a4\u30c3\u30c1\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", + "entity_id": "\u30b9\u30a4\u30c3\u30c1", "target_domain": "\u30bf\u30a4\u30d7" }, + "description": "Home Assistant\u306b\u3001\u30e9\u30a4\u30c8\u3084\u30ab\u30d0\u30fc\u306a\u3069\u306e\u8868\u793a\u3055\u305b\u305f\u3044\u30b9\u30a4\u30c3\u30c1\u3092\u9078\u3073\u307e\u3059\u3002\u5143\u306e\u30b9\u30a4\u30c3\u30c1\u306f\u975e\u8868\u793a\u306b\u306a\u308a\u307e\u3059\u3002", "title": "\u30b9\u30a4\u30c3\u30c1\u3092..." } } diff --git a/homeassistant/components/switch_as_x/translations/nl.json b/homeassistant/components/switch_as_x/translations/nl.json index b1712904a76..4fcea818b9a 100644 --- a/homeassistant/components/switch_as_x/translations/nl.json +++ b/homeassistant/components/switch_as_x/translations/nl.json @@ -1,14 +1,20 @@ { "config": { "step": { - "init": { + "config": { + "user": { + "entity_id": "Entiteit wijzigen" + } + }, + "user": { "data": { - "entity_id": "Entiteit wijzigen", - "target_domain": "Type" + "entity_id": "Schakelaar", + "target_domain": "Nieuw type" }, - "title": "Schakel een..." + "description": "Kies een schakelaar die u in Home Assistant wilt laten verschijnen als licht, klep of iets anders. De oorspronkelijke schakelaar wordt verborgen.", + "title": "Type schakelapparaat wijzigen" } } }, - "title": "Schakelen als X" + "title": "Apparaattype van een schakelaar wijzigen" } \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/no.json b/homeassistant/components/switch_as_x/translations/no.json index 09d8d4e813e..5958c87f201 100644 --- a/homeassistant/components/switch_as_x/translations/no.json +++ b/homeassistant/components/switch_as_x/translations/no.json @@ -1,14 +1,20 @@ { "config": { "step": { - "init": { + "config": { + "user": { + "entity_id": "Bytt enhet" + } + }, + "user": { "data": { - "entity_id": "Bytt enhet", - "target_domain": "Type" + "entity_id": "Bryter", + "target_domain": "Ny type" }, - "title": "Gj\u00f8r en bryter til en ..." + "description": "Velg en bryter du vil vise i Home Assistant som lys, deksel eller noe annet. Den opprinnelige bryteren vil v\u00e6re skjult.", + "title": "Endre bryterenhetstype" } } }, - "title": "Bryter som X" + "title": "Endre enhetstype for en bryter" } \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/pl.json b/homeassistant/components/switch_as_x/translations/pl.json index c3f9af2601d..7491ef0d7fc 100644 --- a/homeassistant/components/switch_as_x/translations/pl.json +++ b/homeassistant/components/switch_as_x/translations/pl.json @@ -1,14 +1,20 @@ { "config": { "step": { - "init": { + "config": { + "user": { + "entity_id": "Encja prze\u0142\u0105cznika" + } + }, + "user": { "data": { - "entity_id": "Encja prze\u0142\u0105cznika", - "target_domain": "Rodzaj" + "entity_id": "Prze\u0142\u0105cznik", + "target_domain": "Nowy rodzaj" }, - "title": "Zmie\u0144 prze\u0142\u0105cznik na ..." + "description": "Wybierz prze\u0142\u0105cznik, kt\u00f3ry chcesz pokaza\u0107 w Home Assistant jako \u015bwiat\u0142o, rolet\u0119 lub cokolwiek innego. Oryginalny prze\u0142\u0105cznik zostanie ukryty.", + "title": "Zmiana typu urz\u0105dzenia prze\u0142\u0105czaj\u0105cego" } } }, - "title": "Prze\u0142\u0105cznik jako \"X\"" + "title": "Zmiana typu urz\u0105dzenia w prze\u0142\u0105czniku" } \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/pt-BR.json b/homeassistant/components/switch_as_x/translations/pt-BR.json index e8ccabe7841..bf8bc276781 100644 --- a/homeassistant/components/switch_as_x/translations/pt-BR.json +++ b/homeassistant/components/switch_as_x/translations/pt-BR.json @@ -1,14 +1,20 @@ { "config": { "step": { - "init": { + "config": { + "user": { + "entity_id": "Entidade de interruptor" + } + }, + "user": { "data": { - "entity_id": "Entidade de interruptor", - "target_domain": "Tipo" + "entity_id": "Interruptor", + "target_domain": "Novo tipo" }, - "title": "Fa\u00e7a um interruptor um ..." + "description": "Escolha um interruptor que voc\u00ea deseja que apare\u00e7a no Home Assistant como luz, cortina ou qualquer outra coisa. A op\u00e7\u00e3o original ficar\u00e1 oculta.", + "title": "Alterar o tipo de dispositivo do interruptor" } } }, - "title": "Switch as X" + "title": "Alterar o tipo de dispositivo de um interruptor" } \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/pt.json b/homeassistant/components/switch_as_x/translations/pt.json index 558030dc42d..70f238e0a08 100644 --- a/homeassistant/components/switch_as_x/translations/pt.json +++ b/homeassistant/components/switch_as_x/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "step": { - "init": { + "user": { "data": { "target_domain": "Tipo" } diff --git a/homeassistant/components/switch_as_x/translations/ru.json b/homeassistant/components/switch_as_x/translations/ru.json index b4136768f03..3074365dd76 100644 --- a/homeassistant/components/switch_as_x/translations/ru.json +++ b/homeassistant/components/switch_as_x/translations/ru.json @@ -1,14 +1,20 @@ { "config": { "step": { - "init": { + "config": { + "user": { + "entity_id": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c" + } + }, + "user": { "data": { "entity_id": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c", - "target_domain": "\u0422\u0438\u043f" + "target_domain": "\u041d\u043e\u0432\u044b\u0439 \u0442\u0438\u043f" }, - "title": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u043a\u0430\u043a \u2026" + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u043d\u043e \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0432 Home Assistant \u043a\u0430\u043a \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u0435, \u0448\u0442\u043e\u0440\u044b \u0438\u043b\u0438 \u0447\u0442\u043e-\u0442\u043e \u0435\u0449\u0451. \u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u0431\u0443\u0434\u0435\u0442 \u0441\u043a\u0440\u044b\u0442.", + "title": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0442\u0438\u043f \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044f" } } }, - "title": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u043a\u0430\u043a \u2026" + "title": "\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u0442\u0438\u043f\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044f" } \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/sv.json b/homeassistant/components/switch_as_x/translations/sv.json index 835de2f5a7f..96419c39bf1 100644 --- a/homeassistant/components/switch_as_x/translations/sv.json +++ b/homeassistant/components/switch_as_x/translations/sv.json @@ -1,9 +1,13 @@ { "config": { "step": { - "init": { + "config": { + "user": { + "entity_id": "Kontakt-entitet" + } + }, + "user": { "data": { - "entity_id": "Kontakt-entitet", "target_domain": "Typ" }, "title": "G\u00f6r en kontakt till ..." diff --git a/homeassistant/components/switch_as_x/translations/tr.json b/homeassistant/components/switch_as_x/translations/tr.json new file mode 100644 index 00000000000..b793be6baf0 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "config": { + "user": { + "entity_id": "Varl\u0131\u011f\u0131 de\u011fi\u015ftir" + } + }, + "user": { + "data": { + "entity_id": "Anahtar", + "target_domain": "Yeni T\u00fcr" + }, + "description": "Home Assistant'ta \u0131\u015f\u0131k, \u00f6rt\u00fc veya ba\u015fka bir \u015fey olarak g\u00f6r\u00fcnmesini istedi\u011finiz bir anahtar se\u00e7in. Orijinal anahtar gizlenecektir.", + "title": "Anahtar cihaz t\u00fcr\u00fcn\u00fc de\u011fi\u015ftir" + } + } + }, + "title": "Bir anahtar\u0131n cihaz t\u00fcr\u00fcn\u00fc de\u011fi\u015ftirme" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/zh-Hans.json b/homeassistant/components/switch_as_x/translations/zh-Hans.json new file mode 100644 index 00000000000..e765a436849 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/zh-Hans.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "config": { + "user": { + "entity_id": "\u5f00\u5173\u5b9e\u4f53" + } + }, + "user": { + "data": { + "entity_id": "\u5f00\u5173", + "target_domain": "\u65b0\u7c7b\u578b" + }, + "description": "\u9009\u62e9\u4e00\u4e2a\u5f00\u5173\uff0c\u8ba9\u5b83\u5728 Home Assistant \u4e2d\u663e\u793a\u4e3a\u706f\u3001\u5377\u5e18\u7b49\u5404\u79cd\u7c7b\u578b\u3002\u539f\u6765\u7684\u5f00\u5173\u5c06\u88ab\u9690\u85cf\u3002", + "title": "\u66f4\u6539\u5f00\u5173\u7684\u8bbe\u5907\u7c7b\u578b" + } + } + }, + "title": "\u66f4\u6539\u5f00\u5173\u7684\u8bbe\u5907\u7c7b\u578b" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/zh-Hant.json b/homeassistant/components/switch_as_x/translations/zh-Hant.json index 231f5b58eff..bd6a1e15ba0 100644 --- a/homeassistant/components/switch_as_x/translations/zh-Hant.json +++ b/homeassistant/components/switch_as_x/translations/zh-Hant.json @@ -1,14 +1,20 @@ { "config": { "step": { - "init": { + "config": { + "user": { + "entity_id": "\u958b\u95dc\u5be6\u9ad4" + } + }, + "user": { "data": { - "entity_id": "\u958b\u95dc\u5be6\u9ad4", - "target_domain": "\u985e\u5225" + "entity_id": "\u958b\u95dc", + "target_domain": "\u65b0\u589e\u985e\u5225" }, - "title": "\u5c07\u958b\u95dc\u8a2d\u5b9a\u70ba ..." + "description": "\u9078\u64c7\u6240\u8981\u65bc Home Assistant \u4e2d\u986f\u793a\u70ba\u71c8\u5149\u7684\u958b\u95dc\u3001\u7a97\u7c3e\u6216\u5176\u4ed6\u5be6\u9ad4\u3002\u539f\u59cb\u958b\u95dc\u5c07\u6703\u9032\u884c\u96b1\u85cf\u3002", + "title": "\u8b8a\u66f4\u958b\u95dc\u985e\u5225" } } }, - "title": "\u958b\u95dc\u8a2d\u70ba X" + "title": "\u8b8a\u66f4\u958b\u95dc\u985e\u5225" } \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/fr.json b/homeassistant/components/switchbot/translations/fr.json index 41b52f253ac..aca51b4e4c4 100644 --- a/homeassistant/components/switchbot/translations/fr.json +++ b/homeassistant/components/switchbot/translations/fr.json @@ -29,7 +29,7 @@ "retry_count": "Nombre de nouvelles tentatives", "retry_timeout": "D\u00e9lai d'attente entre les tentatives", "scan_timeout": "Dur\u00e9e de la recherche de donn\u00e9es publicitaires", - "update_time": "Dur\u00e9e (en secondes) entre deux mises \u00e0 jour" + "update_time": "Intervalle de temps entre deux mises \u00e0 jour (en secondes)" } } } diff --git a/homeassistant/components/switchbot/translations/hu.json b/homeassistant/components/switchbot/translations/hu.json index 5af1acb5d35..2b80fbedbd8 100644 --- a/homeassistant/components/switchbot/translations/hu.json +++ b/homeassistant/components/switchbot/translations/hu.json @@ -17,7 +17,7 @@ "user": { "data": { "mac": "Eszk\u00f6z MAC-c\u00edme", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3" }, "title": "Switchbot eszk\u00f6z be\u00e1ll\u00edt\u00e1sa" diff --git a/homeassistant/components/switchbot/translations/it.json b/homeassistant/components/switchbot/translations/it.json index 68071ec94e5..f589046d4db 100644 --- a/homeassistant/components/switchbot/translations/it.json +++ b/homeassistant/components/switchbot/translations/it.json @@ -9,8 +9,8 @@ }, "error": { "cannot_connect": "Impossibile connettersi", - "one": "Pi\u00f9", - "other": "Altri" + "one": "Vuoto", + "other": "Vuoti" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/syncthing/translations/ca.json b/homeassistant/components/syncthing/translations/ca.json index a10b5d8c134..e6a1625159d 100644 --- a/homeassistant/components/syncthing/translations/ca.json +++ b/homeassistant/components/syncthing/translations/ca.json @@ -17,6 +17,5 @@ } } } - }, - "title": "Syncthing" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/de.json b/homeassistant/components/syncthing/translations/de.json index 06db3e89b97..a753ae08cd9 100644 --- a/homeassistant/components/syncthing/translations/de.json +++ b/homeassistant/components/syncthing/translations/de.json @@ -17,6 +17,5 @@ } } } - }, - "title": "Syncthing" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/el.json b/homeassistant/components/syncthing/translations/el.json index 7d8c1e635df..d31063f30d3 100644 --- a/homeassistant/components/syncthing/translations/el.json +++ b/homeassistant/components/syncthing/translations/el.json @@ -17,6 +17,5 @@ } } } - }, - "title": "Syncthing" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/en.json b/homeassistant/components/syncthing/translations/en.json index 68efde737f2..e25581817a9 100644 --- a/homeassistant/components/syncthing/translations/en.json +++ b/homeassistant/components/syncthing/translations/en.json @@ -17,6 +17,5 @@ } } } - }, - "title": "Syncthing" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/es.json b/homeassistant/components/syncthing/translations/es.json index 550c1874010..e39a1436c2a 100644 --- a/homeassistant/components/syncthing/translations/es.json +++ b/homeassistant/components/syncthing/translations/es.json @@ -17,6 +17,5 @@ } } } - }, - "title": "Syncthing" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/et.json b/homeassistant/components/syncthing/translations/et.json index 12922ad3f6d..06addc30e2f 100644 --- a/homeassistant/components/syncthing/translations/et.json +++ b/homeassistant/components/syncthing/translations/et.json @@ -17,6 +17,5 @@ } } } - }, - "title": "Syncthing" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/fr.json b/homeassistant/components/syncthing/translations/fr.json index f0d93986445..75589f10740 100644 --- a/homeassistant/components/syncthing/translations/fr.json +++ b/homeassistant/components/syncthing/translations/fr.json @@ -17,6 +17,5 @@ } } } - }, - "title": "Synchroniser" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/he.json b/homeassistant/components/syncthing/translations/he.json index 7e87dacd7e5..5df905492f0 100644 --- a/homeassistant/components/syncthing/translations/he.json +++ b/homeassistant/components/syncthing/translations/he.json @@ -16,6 +16,5 @@ } } } - }, - "title": "\u05e1\u05d9\u05e0\u05db\u05e8\u05d5\u05df" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/hu.json b/homeassistant/components/syncthing/translations/hu.json index 59ed4021b3f..90aca8becea 100644 --- a/homeassistant/components/syncthing/translations/hu.json +++ b/homeassistant/components/syncthing/translations/hu.json @@ -17,6 +17,5 @@ } } } - }, - "title": "Szinkroniz\u00e1l\u00e1s" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/id.json b/homeassistant/components/syncthing/translations/id.json index d8f2a76c41d..db83504f4af 100644 --- a/homeassistant/components/syncthing/translations/id.json +++ b/homeassistant/components/syncthing/translations/id.json @@ -17,6 +17,5 @@ } } } - }, - "title": "Syncthing" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/it.json b/homeassistant/components/syncthing/translations/it.json index 0973a3ccf92..061be57e295 100644 --- a/homeassistant/components/syncthing/translations/it.json +++ b/homeassistant/components/syncthing/translations/it.json @@ -17,6 +17,5 @@ } } } - }, - "title": "Syncthing" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/ja.json b/homeassistant/components/syncthing/translations/ja.json index 482087ed730..2a725cbf3cc 100644 --- a/homeassistant/components/syncthing/translations/ja.json +++ b/homeassistant/components/syncthing/translations/ja.json @@ -17,6 +17,5 @@ } } } - }, - "title": "Syncthing" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/nl.json b/homeassistant/components/syncthing/translations/nl.json index 358490c6c83..4f66222ada8 100644 --- a/homeassistant/components/syncthing/translations/nl.json +++ b/homeassistant/components/syncthing/translations/nl.json @@ -17,6 +17,5 @@ } } } - }, - "title": "Syncthing" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/no.json b/homeassistant/components/syncthing/translations/no.json index 9fdb9f5f305..00a4de27e7f 100644 --- a/homeassistant/components/syncthing/translations/no.json +++ b/homeassistant/components/syncthing/translations/no.json @@ -17,6 +17,5 @@ } } } - }, - "title": "Synkronisering" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/pl.json b/homeassistant/components/syncthing/translations/pl.json index e4e2619f1eb..367b1522930 100644 --- a/homeassistant/components/syncthing/translations/pl.json +++ b/homeassistant/components/syncthing/translations/pl.json @@ -17,6 +17,5 @@ } } } - }, - "title": "Syncthing" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/pt-BR.json b/homeassistant/components/syncthing/translations/pt-BR.json index 08f66569d93..d7f246e1aa6 100644 --- a/homeassistant/components/syncthing/translations/pt-BR.json +++ b/homeassistant/components/syncthing/translations/pt-BR.json @@ -17,6 +17,5 @@ } } } - }, - "title": "Sincroniza\u00e7\u00e3o" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/ru.json b/homeassistant/components/syncthing/translations/ru.json index 337b0214c6b..60897aade50 100644 --- a/homeassistant/components/syncthing/translations/ru.json +++ b/homeassistant/components/syncthing/translations/ru.json @@ -17,6 +17,5 @@ } } } - }, - "title": "Syncthing" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/sv.json b/homeassistant/components/syncthing/translations/sv.json index 7862919ad54..9be5ea00256 100644 --- a/homeassistant/components/syncthing/translations/sv.json +++ b/homeassistant/components/syncthing/translations/sv.json @@ -9,6 +9,5 @@ } } } - }, - "title": "Synkronisering" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/tr.json b/homeassistant/components/syncthing/translations/tr.json index eeeb7e02c3c..e490274eb25 100644 --- a/homeassistant/components/syncthing/translations/tr.json +++ b/homeassistant/components/syncthing/translations/tr.json @@ -17,6 +17,5 @@ } } } - }, - "title": "E\u015fitleme" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/zh-Hans.json b/homeassistant/components/syncthing/translations/zh-Hans.json index 87d3db5c83f..96af1e51799 100644 --- a/homeassistant/components/syncthing/translations/zh-Hans.json +++ b/homeassistant/components/syncthing/translations/zh-Hans.json @@ -17,6 +17,5 @@ } } } - }, - "title": "Syncthing" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/zh-Hant.json b/homeassistant/components/syncthing/translations/zh-Hant.json index f06c16ac157..6b6bc948c5f 100644 --- a/homeassistant/components/syncthing/translations/zh-Hant.json +++ b/homeassistant/components/syncthing/translations/zh-Hant.json @@ -17,6 +17,5 @@ } } } - }, - "title": "Syncthing" + } } \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/hu.json b/homeassistant/components/syncthru/translations/hu.json index a5b645200db..3de6022dcb6 100644 --- a/homeassistant/components/syncthru/translations/hu.json +++ b/homeassistant/components/syncthru/translations/hu.json @@ -12,13 +12,13 @@ "step": { "confirm": { "data": { - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "url": "Webes fel\u00fclet URL-je" } }, "user": { "data": { - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "url": "Webes fel\u00fclet URL-je" } } diff --git a/homeassistant/components/system_bridge/translations/ca.json b/homeassistant/components/system_bridge/translations/ca.json index aac2e139db0..b17295d59e0 100644 --- a/homeassistant/components/system_bridge/translations/ca.json +++ b/homeassistant/components/system_bridge/translations/ca.json @@ -27,6 +27,5 @@ "description": "Introdueix les dades de connexi\u00f3." } } - }, - "title": "Enlla\u00e7 de sistema" + } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/de.json b/homeassistant/components/system_bridge/translations/de.json index dc85589dd32..0ea5a0abd3a 100644 --- a/homeassistant/components/system_bridge/translations/de.json +++ b/homeassistant/components/system_bridge/translations/de.json @@ -27,6 +27,5 @@ "description": "Bitte gib Verbindungsdaten ein." } } - }, - "title": "System-Br\u00fccke" + } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/el.json b/homeassistant/components/system_bridge/translations/el.json index 5576af325de..cc15d636f72 100644 --- a/homeassistant/components/system_bridge/translations/el.json +++ b/homeassistant/components/system_bridge/translations/el.json @@ -27,6 +27,5 @@ "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2." } } - }, - "title": "\u0393\u03ad\u03c6\u03c5\u03c1\u03b1 \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2" + } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/en.json b/homeassistant/components/system_bridge/translations/en.json index 3ebcfc6959e..dc94dfe2ac6 100644 --- a/homeassistant/components/system_bridge/translations/en.json +++ b/homeassistant/components/system_bridge/translations/en.json @@ -27,6 +27,5 @@ "description": "Please enter your connection details." } } - }, - "title": "System Bridge" + } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/es.json b/homeassistant/components/system_bridge/translations/es.json index 2b7a859fcd1..d7fba5e7c32 100644 --- a/homeassistant/components/system_bridge/translations/es.json +++ b/homeassistant/components/system_bridge/translations/es.json @@ -27,6 +27,5 @@ "description": "Por favor, introduce tus datos de conexi\u00f3n." } } - }, - "title": "Pasarela del sistema" + } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/et.json b/homeassistant/components/system_bridge/translations/et.json index 3724895f147..69f32524c3b 100644 --- a/homeassistant/components/system_bridge/translations/et.json +++ b/homeassistant/components/system_bridge/translations/et.json @@ -27,6 +27,5 @@ "description": "Sisesta oma \u00fchenduse \u00fcksikasjad." } } - }, - "title": "S\u00fcsteemi sild" + } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/fr.json b/homeassistant/components/system_bridge/translations/fr.json index cbcad2d0330..e04dca1b751 100644 --- a/homeassistant/components/system_bridge/translations/fr.json +++ b/homeassistant/components/system_bridge/translations/fr.json @@ -27,6 +27,5 @@ "description": "Veuillez saisir vos informations de connexion." } } - }, - "title": "Pont syst\u00e8me" + } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/he.json b/homeassistant/components/system_bridge/translations/he.json index 5eb235ffb8f..ed758978398 100644 --- a/homeassistant/components/system_bridge/translations/he.json +++ b/homeassistant/components/system_bridge/translations/he.json @@ -27,6 +27,5 @@ "description": "\u05e0\u05d0 \u05d4\u05d6\u05df \u05d0\u05ea \u05e4\u05e8\u05d8\u05d9 \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e9\u05dc\u05da." } } - }, - "title": "\u05d2\u05e9\u05e8 \u05d4\u05de\u05e2\u05e8\u05db\u05ea" + } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/hu.json b/homeassistant/components/system_bridge/translations/hu.json index 8fdacd8718f..c570cd2e3c2 100644 --- a/homeassistant/components/system_bridge/translations/hu.json +++ b/homeassistant/components/system_bridge/translations/hu.json @@ -27,6 +27,5 @@ "description": "K\u00e9rj\u00fck, adja meg kapcsolati adatait." } } - }, - "title": "Rendszer h\u00edd" + } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/id.json b/homeassistant/components/system_bridge/translations/id.json index 9995253cbca..7cc07ebb2e1 100644 --- a/homeassistant/components/system_bridge/translations/id.json +++ b/homeassistant/components/system_bridge/translations/id.json @@ -27,6 +27,5 @@ "description": "Masukkan detail koneksi Anda." } } - }, - "title": "Jembatan Sistem" + } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/it.json b/homeassistant/components/system_bridge/translations/it.json index 2b6885edc44..7257d92ed7a 100644 --- a/homeassistant/components/system_bridge/translations/it.json +++ b/homeassistant/components/system_bridge/translations/it.json @@ -27,6 +27,5 @@ "description": "Inserisci i dettagli della tua connessione." } } - }, - "title": "Bridge di sistema" + } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/ja.json b/homeassistant/components/system_bridge/translations/ja.json index 48ee7fc2378..8358130359f 100644 --- a/homeassistant/components/system_bridge/translations/ja.json +++ b/homeassistant/components/system_bridge/translations/ja.json @@ -27,6 +27,5 @@ "description": "\u63a5\u7d9a\u306e\u8a73\u7d30\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } - }, - "title": "\u30b7\u30b9\u30c6\u30e0\u30d6\u30ea\u30c3\u30b8" + } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/nl.json b/homeassistant/components/system_bridge/translations/nl.json index eefadebda35..c419123e50d 100644 --- a/homeassistant/components/system_bridge/translations/nl.json +++ b/homeassistant/components/system_bridge/translations/nl.json @@ -27,6 +27,5 @@ "description": "Voer uw verbindingsgegevens in." } } - }, - "title": "System Bridge" + } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/no.json b/homeassistant/components/system_bridge/translations/no.json index 5651dd7cb67..22ab7f91a57 100644 --- a/homeassistant/components/system_bridge/translations/no.json +++ b/homeassistant/components/system_bridge/translations/no.json @@ -27,6 +27,5 @@ "description": "Vennligst skriv inn tilkoblingsdetaljene dine." } } - }, - "title": "System Bridge" + } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/pl.json b/homeassistant/components/system_bridge/translations/pl.json index 0ae579842d6..6c3a1561b5d 100644 --- a/homeassistant/components/system_bridge/translations/pl.json +++ b/homeassistant/components/system_bridge/translations/pl.json @@ -27,6 +27,5 @@ "description": "Wprowad\u017a dane po\u0142\u0105czenia." } } - }, - "title": "Mostek System" + } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/pt-BR.json b/homeassistant/components/system_bridge/translations/pt-BR.json index ed1bcd01ded..6b86e19ab35 100644 --- a/homeassistant/components/system_bridge/translations/pt-BR.json +++ b/homeassistant/components/system_bridge/translations/pt-BR.json @@ -27,6 +27,5 @@ "description": "Por favor, insira os detalhes da sua conex\u00e3o." } } - }, - "title": "Ponte do sistema" + } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/ru.json b/homeassistant/components/system_bridge/translations/ru.json index be2cddfdc69..c18bd977413 100644 --- a/homeassistant/components/system_bridge/translations/ru.json +++ b/homeassistant/components/system_bridge/translations/ru.json @@ -27,6 +27,5 @@ "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." } } - }, - "title": "System Bridge" + } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/tr.json b/homeassistant/components/system_bridge/translations/tr.json index 17e43680cca..15259c306a8 100644 --- a/homeassistant/components/system_bridge/translations/tr.json +++ b/homeassistant/components/system_bridge/translations/tr.json @@ -27,6 +27,5 @@ "description": "L\u00fctfen ba\u011flant\u0131 bilgilerinizi giriniz." } } - }, - "title": "System Bridge" + } } \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/zh-Hant.json b/homeassistant/components/system_bridge/translations/zh-Hant.json index 5f45cc5dfcd..cca10d161b1 100644 --- a/homeassistant/components/system_bridge/translations/zh-Hant.json +++ b/homeassistant/components/system_bridge/translations/zh-Hant.json @@ -27,6 +27,5 @@ "description": "\u8acb\u8f38\u5165\u9023\u7dda\u8a0a\u606f\u3002" } } - }, - "title": "System Bridge" + } } \ No newline at end of file diff --git a/homeassistant/components/system_health/translations/pt-BR.json b/homeassistant/components/system_health/translations/pt-BR.json index eb6f66e8784..15f9774cb16 100644 --- a/homeassistant/components/system_health/translations/pt-BR.json +++ b/homeassistant/components/system_health/translations/pt-BR.json @@ -1,3 +1,3 @@ { - "title": "Integridade Do Sistema" + "title": "Integridade do Sistema" } \ No newline at end of file diff --git a/homeassistant/components/tado/translations/ca.json b/homeassistant/components/tado/translations/ca.json index 935b58483d7..c34d937cf7f 100644 --- a/homeassistant/components/tado/translations/ca.json +++ b/homeassistant/components/tado/translations/ca.json @@ -23,9 +23,9 @@ "step": { "init": { "data": { - "fallback": "Activa el mode salvaguarda." + "fallback": "Tria el mode alternatiu." }, - "description": "El mode salvaguarda canviar\u00e0 a Planificaci\u00f3 Intel\u00b7ligent en el proper canvi de programaci\u00f3 o quan s'ajusti manualment una zona.", + "description": "El mode alternatiu et permet triar quan passar al mode alternatiu d'horari intel\u00b7ligent des de la teva zona manual. (NEXT_TIME_BLOCK:= Canvia al pr\u00f2xim canvi de l'horari intel\u00b7ligent; MANUAL:= No canvia fins que es cancel\u00b7la; TADO_DEFAULT:= Canvia en funci\u00f3 de la configuraci\u00f3 de l'app Tado).", "title": "Ajusta les opcions de Tado" } } diff --git a/homeassistant/components/tado/translations/de.json b/homeassistant/components/tado/translations/de.json index dec90a46aef..73f068949c3 100644 --- a/homeassistant/components/tado/translations/de.json +++ b/homeassistant/components/tado/translations/de.json @@ -23,9 +23,9 @@ "step": { "init": { "data": { - "fallback": "Aktiviert den Fallback-Modus." + "fallback": "Aktiviere den Fallback-Modus." }, - "description": "Der Fallback-Modus wechselt beim n\u00e4chsten Zeitplanwechsel nach dem manuellen Anpassen einer Zone zu Smart Schedule.", + "description": "Mit dem Fallback-Modus kannst du festlegen, wann von deinem manuellen Zonen-Overlay auf den intelligenten Zeitplan umgeschaltet werden soll. (NEXT_TIME_BLOCK:= Wechsel bei der n\u00e4chsten Smart Schedule-\u00c4nderung; MANUAL:= Kein Wechsel, bis du abbrichst; TADO_DEFAULT:= Wechsel basierend auf deiner Einstellung in der Tado-App).", "title": "Passe die Tado-Optionen an." } } diff --git a/homeassistant/components/tado/translations/en.json b/homeassistant/components/tado/translations/en.json index 4e0df459437..30d090c77a1 100644 --- a/homeassistant/components/tado/translations/en.json +++ b/homeassistant/components/tado/translations/en.json @@ -23,9 +23,9 @@ "step": { "init": { "data": { - "fallback": "Enable fallback mode." + "fallback": "Choose fallback mode." }, - "description": "Fallback mode will switch to Smart Schedule at next schedule switch after manually adjusting a zone.", + "description": "Fallback mode lets you choose when to fallback to Smart Schedule from your manual zone overlay. (NEXT_TIME_BLOCK:= Change at next Smart Schedule change; MANUAL:= Dont change until you cancel; TADO_DEFAULT:= Change based on your setting in Tado App).", "title": "Adjust Tado options." } } diff --git a/homeassistant/components/tado/translations/et.json b/homeassistant/components/tado/translations/et.json index 6a201b4fd2d..cb8cf14ad3b 100644 --- a/homeassistant/components/tado/translations/et.json +++ b/homeassistant/components/tado/translations/et.json @@ -23,9 +23,9 @@ "step": { "init": { "data": { - "fallback": "Luba varure\u017eiim." + "fallback": "Vali varure\u017eiim." }, - "description": "P\u00e4rast tsooni k\u00e4sitsi reguleerimist l\u00fclitub varure\u017eiim j\u00e4rgmisel ajakava l\u00fclitil nutikasse ajakavasse.", + "description": "Varure\u017eiim v\u00f5imaldab valida millal naasta nutikale ajakavale k\u00e4sitsi tsooni \u00fclekattega. (NEXT_TIME_BLOCK:= Muuda j\u00e4rgmisel nutika ajakava muudatusel; MANUAL:= \u00c4ra muuda enne kui oled t\u00fchistanud; TADO_DEFAULT:= Muuda, l\u00e4htudes Tado rakenduse seadistustest).", "title": "Kohanda Tado suvandeid." } } diff --git a/homeassistant/components/tado/translations/fr.json b/homeassistant/components/tado/translations/fr.json index 2e10dc38a2e..a3ff3a9fabf 100644 --- a/homeassistant/components/tado/translations/fr.json +++ b/homeassistant/components/tado/translations/fr.json @@ -23,9 +23,9 @@ "step": { "init": { "data": { - "fallback": "Activer le mode restreint." + "fallback": "Choisissez le mode de retour." }, - "description": "Le mode de repli passera au programme intelligent au prochain changement de programme apr\u00e8s avoir ajust\u00e9 manuellement une zone.", + "description": "Le mode de retour vous permet de choisir le moment du retour \u00e0 la programmation intelligente depuis votre zone de superposition manuelle. (NEXT_TIME_BLOCK\u00a0:= Modifier au prochain changement de programmation intelligente\u00a0; MANUAL\u00a0:= Ne pas modifier jusqu'\u00e0 annulation manuelle\u00a0; TADO_DEFAULT\u00a0:= Modifier en fonction de vos param\u00e8tres dans l'application Tado).", "title": "Ajustez les options de Tado." } } diff --git a/homeassistant/components/tado/translations/hu.json b/homeassistant/components/tado/translations/hu.json index dfde73ce428..8a96591e429 100644 --- a/homeassistant/components/tado/translations/hu.json +++ b/homeassistant/components/tado/translations/hu.json @@ -23,9 +23,9 @@ "step": { "init": { "data": { - "fallback": "A tartal\u00e9k m\u00f3d enged\u00e9lyez\u00e9se." + "fallback": "A tartal\u00e9k m\u00f3d be\u00e1ll\u00edt\u00e1sa." }, - "description": "A tartal\u00e9k m\u00f3d intelligens \u00fctemez\u00e9sre v\u00e1lt a k\u00f6vetkez\u0151 \u00fctemez\u00e9s kapcsol\u00f3n\u00e1l, miut\u00e1n manu\u00e1lisan be\u00e1ll\u00edtotta a z\u00f3n\u00e1t.", + "description": "A tartal\u00e9k m\u00f3dban kiv\u00e1laszthatja, hogy mikor t\u00e9rjen vissza Smart Schedule-ra a k\u00e9zi m\u00f3db\u00f3l. (NEXT_TIME_BLOCK:= V\u00e1ltoz\u00e1s a k\u00f6vetkez\u0151 intelligens \u00fctemez\u00e9s m\u00f3dos\u00edt\u00e1s\u00e1n\u00e1l; MANUAL:= Ne m\u00f3dos\u00edtsa, am\u00edg le nem mondja; TADO_DEFAULT:= V\u00e1ltoz\u00e1s a Tado App be\u00e1ll\u00edt\u00e1sai alapj\u00e1n).", "title": "\u00c1ll\u00edtsa be a Tado-t." } } diff --git a/homeassistant/components/tado/translations/id.json b/homeassistant/components/tado/translations/id.json index e6a00f9ee07..c3bd3d305e6 100644 --- a/homeassistant/components/tado/translations/id.json +++ b/homeassistant/components/tado/translations/id.json @@ -23,9 +23,9 @@ "step": { "init": { "data": { - "fallback": "Aktifkan mode alternatif." + "fallback": "Pilih mode alternatif" }, - "description": "Mode fallback akan beralih ke Smart Schedule pada pergantian jadwal berikutnya setelah menyesuaikan zona secara manual.", + "description": "Mode alternatif memungkinkan Anda memilih kapan harus berubah ke Smart Schedule dari overlay zona manual Anda. (NEXT_TIME_BLOCK:= Ubah pada perubahan Smart Schedule berikutnya; MANUAL:= Jangan ubah sampai Anda membatalkan; TADO_DEFAULT:= Ubah berdasarkan pengaturan Anda di Aplikasi Tado).", "title": "Sesuaikan opsi Tado." } } diff --git a/homeassistant/components/tado/translations/it.json b/homeassistant/components/tado/translations/it.json index 34c9dfe597d..6c42534b32a 100644 --- a/homeassistant/components/tado/translations/it.json +++ b/homeassistant/components/tado/translations/it.json @@ -23,9 +23,9 @@ "step": { "init": { "data": { - "fallback": "Abilita la modalit\u00e0 di ripiego." + "fallback": "Scegli la modalit\u00e0 di ripiego." }, - "description": "La modalit\u00e0 di ripiego passer\u00e0 a Smart Schedule al prossimo cambio di programma dopo aver regolato manualmente una zona.", + "description": "La modalit\u00e0 di ripiego ti consente di scegliere quando eseguire il ripiego alla pianificazione intelligente dalla sovrapposizione manuale delle zone. (NEXT_TIME_BLOCK:= Modifica alla successiva modifica della pianificazione intelligente; MANUAL:= Non modificare fino all'annullamento; TADO_DEFAULT:= Modifica in base alle tue impostazioni nell'applicazione Tado).", "title": "Regola le opzioni di Tado." } } diff --git a/homeassistant/components/tado/translations/nl.json b/homeassistant/components/tado/translations/nl.json index 3b6d914b71c..43b969b69eb 100644 --- a/homeassistant/components/tado/translations/nl.json +++ b/homeassistant/components/tado/translations/nl.json @@ -23,9 +23,9 @@ "step": { "init": { "data": { - "fallback": "Schakel de terugvalmodus in." + "fallback": "Kies de terugvalmodus." }, - "description": "De fallback-modus schakelt over naar Smart Schedule bij de volgende schemaschakeling na het handmatig aanpassen van een zone.", + "description": "Met de terugvalmodus kunt u kiezen wanneer u wilt terugvallen op Smart Schedule vanuit uw handmatige zoneoverlay. (NEXT_TIME_BLOCK:= Verander bij de volgende wijziging van Smart Schedule; MANUAL:= Verander niet tot je annuleert; TADO_DEFAULT:= Verander op basis van je instelling in Tado App).", "title": "Pas Tado-opties aan." } } diff --git a/homeassistant/components/tado/translations/no.json b/homeassistant/components/tado/translations/no.json index 4b6db19e0af..f2ef0b7de6e 100644 --- a/homeassistant/components/tado/translations/no.json +++ b/homeassistant/components/tado/translations/no.json @@ -23,9 +23,9 @@ "step": { "init": { "data": { - "fallback": "Aktiver tilbakefallsmodus." + "fallback": "Velg reservemodus." }, - "description": "Fallback-modus bytter til Smart Schedule ved neste planbryter etter manuell justering av en sone.", + "description": "Reservemodus lar deg velge n\u00e5r du vil falle tilbake til Smart Schedule fra det manuelle soneoverlegget ditt. (NEXT_TIME_BLOCK:= Endre ved neste Smart Schedule-endring; MANUAL:= Ikke endre f\u00f8r du avbryter; TADO_DEFAULT:= Endre basert p\u00e5 innstillingen din i Tado-appen).", "title": "Juster Tado-alternativene." } } diff --git a/homeassistant/components/tado/translations/pl.json b/homeassistant/components/tado/translations/pl.json index 4f109fc9e98..d88fb939530 100644 --- a/homeassistant/components/tado/translations/pl.json +++ b/homeassistant/components/tado/translations/pl.json @@ -23,9 +23,9 @@ "step": { "init": { "data": { - "fallback": "W\u0142\u0105cz tryb awaryjny." + "fallback": "Wybierz tryb awaryjny." }, - "description": "Tryb rezerwowy prze\u0142\u0105czy si\u0119 na inteligentny harmonogram przy nast\u0119pnym prze\u0142\u0105czeniu z harmonogramu po r\u0119cznym dostosowaniu strefy.", + "description": "Tryb awaryjny pozwala wybra\u0107, kiedy wr\u00f3ci\u0107 do inteligentnego harmonogramu z r\u0119cznej nak\u0142adki strefy. (NEXT_TIME_BLOCK:= Zmie\u0144 przy nast\u0119pnej zmianie inteligentnego harmonogramu; MANUAL:= Nie zmieniaj, dop\u00f3ki nie anulujesz; TADO_DEFAULT:= Zmie\u0144 na podstawie ustawie\u0144 w aplikacji Tado).", "title": "Dostosuj opcje Tado" } } diff --git a/homeassistant/components/tado/translations/pt-BR.json b/homeassistant/components/tado/translations/pt-BR.json index 68cf24fe5df..e0c11a94830 100644 --- a/homeassistant/components/tado/translations/pt-BR.json +++ b/homeassistant/components/tado/translations/pt-BR.json @@ -23,9 +23,9 @@ "step": { "init": { "data": { - "fallback": "Ative o modo de fallback." + "fallback": "Escolha o modo de fallback." }, - "description": "O modo Fallback mudar\u00e1 para Smart Schedule na pr\u00f3xima troca de agendamento ap\u00f3s ajustar manualmente uma zona.", + "description": "O modo fallback permite que voc\u00ea escolha quando fazer fallback para o Smart Schedule a partir de sua sobreposi\u00e7\u00e3o de zona manual. (NEXT_TIME_BLOCK:= Altere na pr\u00f3xima altera\u00e7\u00e3o do Smart Schedule; MANUAL:= N\u00e3o altere at\u00e9 cancelar; TADO_DEFAULT:= Altere com base na sua configura\u00e7\u00e3o no Tado App).", "title": "Ajuste as op\u00e7\u00f5es do Tado." } } diff --git a/homeassistant/components/tado/translations/ru.json b/homeassistant/components/tado/translations/ru.json index 18e9ddff67b..af2e96ae5ef 100644 --- a/homeassistant/components/tado/translations/ru.json +++ b/homeassistant/components/tado/translations/ru.json @@ -23,9 +23,9 @@ "step": { "init": { "data": { - "fallback": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c Fallback" + "fallback": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c." }, - "description": "\u0420\u0435\u0436\u0438\u043c Fallback \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043d\u0430 Smart Schedule \u043f\u0440\u0438 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u043c \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u0440\u0430\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u043f\u043e\u0441\u043b\u0435 \u0440\u0443\u0447\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0437\u043e\u043d\u044b.", + "description": "\u0420\u0435\u0437\u0435\u0440\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0432\u044b\u0431\u0440\u0430\u0442\u044c, \u043a\u043e\u0433\u0434\u0430 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0432\u0435\u0440\u043d\u0443\u0442\u044c\u0441\u044f \u043a \u0438\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u043c\u0443 \u0440\u0430\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u044e \u0438\u0437 \u0440\u0443\u0447\u043d\u043e\u0433\u043e \u043d\u0430\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0437\u043e\u043d\u044b. (NEXT_TIME_BLOCK:= \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u0440\u0438 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u043c \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 \u0441\u043c\u0430\u0440\u0442-\u0440\u0430\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u044f; MANUAL:= \u041d\u0435 \u0438\u0437\u043c\u0435\u043d\u044f\u0442\u044c, \u043f\u043e\u043a\u0430 \u0412\u044b \u043d\u0435 \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u0435; TADO_DEFAULT:= \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u0412\u0430\u0448\u0438\u0445 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 Tado).", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Tado" } } diff --git a/homeassistant/components/tado/translations/tr.json b/homeassistant/components/tado/translations/tr.json index 30fc86b8e6a..76e4c25eefb 100644 --- a/homeassistant/components/tado/translations/tr.json +++ b/homeassistant/components/tado/translations/tr.json @@ -23,9 +23,9 @@ "step": { "init": { "data": { - "fallback": "Geri d\u00f6n\u00fc\u015f modunu etkinle\u015ftirin." + "fallback": "Geri d\u00f6n\u00fc\u015f modunu se\u00e7in." }, - "description": "Geri d\u00f6n\u00fc\u015f modu, bir b\u00f6lgeyi manuel olarak ayarlad\u0131ktan sonra bir sonraki program anahtar\u0131nda Ak\u0131ll\u0131 Programa ge\u00e7ecektir.", + "description": "Geri d\u00f6n\u00fc\u015f modu, manuel b\u00f6lge yerle\u015fiminizden Ak\u0131ll\u0131 Programa ne zaman geri d\u00f6nece\u011finizi se\u00e7menizi sa\u011flar. (NEXT_TIME_BLOCK:= Sonraki Ak\u0131ll\u0131 Program de\u011fi\u015fikli\u011finde de\u011fi\u015ftirin; MANUAL:= \u0130ptal edene kadar de\u011fi\u015ftirmeyin; TADO_DEFAULT:= Tado Uygulamas\u0131ndaki ayar\u0131n\u0131za g\u00f6re de\u011fi\u015ftirin).", "title": "Tado se\u00e7eneklerini ayarlay\u0131n." } } diff --git a/homeassistant/components/tado/translations/zh-Hant.json b/homeassistant/components/tado/translations/zh-Hant.json index e7f1f41ce3b..794496d7994 100644 --- a/homeassistant/components/tado/translations/zh-Hant.json +++ b/homeassistant/components/tado/translations/zh-Hant.json @@ -23,9 +23,9 @@ "step": { "init": { "data": { - "fallback": "\u958b\u555f\u5f8c\u964d\u6a21\u5f0f" + "fallback": "\u9078\u64c7\u5f8c\u964d\u6a21\u5f0f\u3002" }, - "description": "\u5f8c\u964d\u6a21\u5f0f\u5c07\u6703\u65bc\u624b\u52d5\u8abf\u6574\u5340\u57df\u5f8c\uff0c\u4e0b\u4e00\u6b21\u898f\u5283\u5207\u63db\u6642\u3001\u5207\u63db\u5230\u667a\u80fd\u884c\u7a0b\u3002", + "description": "\u5f8c\u964d\u6a21\u5f0f\u8b93\u60a8\u9078\u64c7\u7576\u81ea\u624b\u52d5\u8abf\u6574\u5340\u57df\u5f8c\u3001\u5207\u63db\u5230\u667a\u80fd\u6a21\u5f0f\u3002\uff08NEXT_TIME_BLOCK:= \u65bc\u4e0b\u6b21\u667a\u80fd\u6a21\u5f0f\u8b8a\u66f4\u6642\u8b8a\u66f4; MANUAL:= \u76f4\u5230\u53d6\u6d88\u524d\u4e0d\u8981\u8b8a\u66f4; TADO_DEFAULT:= \u57fa\u65bc Tado App \u4e2d\u8a2d\u5b9a\u8b8a\u66f4)\u3002", "title": "\u8abf\u6574 Tado \u9078\u9805\u3002" } } diff --git a/homeassistant/components/tailscale/translations/ca.json b/homeassistant/components/tailscale/translations/ca.json index d111da767f2..a339ce6a22b 100644 --- a/homeassistant/components/tailscale/translations/ca.json +++ b/homeassistant/components/tailscale/translations/ca.json @@ -19,7 +19,7 @@ "api_key": "Clau API", "tailnet": "Tailnet" }, - "description": "Per autenticar-te amb Tailscale, has de crear una clau API a https://login.tailscale.com/admin/settings/authkeys. \n\nLa Tailnet \u00e9s el nom de la teva xarxa Tailscale. La pots trobar a l'extrem superior esquerre del tauler d'administraci\u00f3 de Tailscale (al costat del logotip de Tailscale)." + "description": "Aquesta integraci\u00f3 monitoritza la teva xarxa Tailscale, per\u00f2 NO fa que Home Assistant sigui accessible a trav\u00e9s de la VPN de Tailscale.\n\nPer autenticar-te amb Tailscale, has de crear una clau API a {authkeys_url}. \n\nTailnet \u00e9s el nom de la teva xarxa Tailscale. La pots trobar a l'extrem superior esquerre del tauler d'administraci\u00f3 de Tailscale (al costat del logotip de Tailscale)." } } } diff --git a/homeassistant/components/tailscale/translations/de.json b/homeassistant/components/tailscale/translations/de.json index 9fbcceaa674..fca05d46a6e 100644 --- a/homeassistant/components/tailscale/translations/de.json +++ b/homeassistant/components/tailscale/translations/de.json @@ -19,7 +19,7 @@ "api_key": "API-Schl\u00fcssel", "tailnet": "Tailnet" }, - "description": "Um sich bei Tailscale zu authentifizieren, musst du einen API-Schl\u00fcssel unter https://login.tailscale.com/admin/settings/authkeys erstellen.\n\nEin Tailnet ist der Name Ihres Tailscale-Netzwerks. Sie finden ihn in der linken oberen Ecke des Tailscale Admin Panels (neben dem Tailscale Logo)." + "description": "Diese Integration \u00fcberwacht dein Tailscale-Netzwerk, sie macht deinen Home Assistant **nicht** \u00fcber Tailscale VPN zug\u00e4nglich. \n\nUm sich bei Tailscale zu authentifizieren, musst du einen API-Schl\u00fcssel unter {authkeys_url} erstellen.\n\nEin Tailnet ist der Name deines Tailscale-Netzwerks. Du findest es oben links im Tailscale Admin Panel (neben dem Tailscale Logo)." } } } diff --git a/homeassistant/components/tailscale/translations/et.json b/homeassistant/components/tailscale/translations/et.json index d542c930ecd..1f8a677f2af 100644 --- a/homeassistant/components/tailscale/translations/et.json +++ b/homeassistant/components/tailscale/translations/et.json @@ -19,7 +19,7 @@ "api_key": "API v\u00f5ti", "tailnet": "Tailnet" }, - "description": "Tailscale'iga autentimiseks pead looma API v\u00f5tme aadressil https://login.tailscale.com/admin/settings/authkeys. \n\n Tailnet on Tailscale v\u00f5rgu nimi. Leiad selle Tailscale halduspaneeli vasakus \u00fclanurgas (Tailscale logo k\u00f5rval)." + "description": "See sidumine j\u00e4lgib Tailscale v\u00f5rku, see **EI TEE** seda Home Assistantile juurdep\u00e4\u00e4setavaks Tailscale VPN-i kaudu. \n\n Tailscale'iga autentimiseks pead looma API v\u00f5tme aadressil {authkeys_url} . \n\n Tailnet on Tailscale v\u00f5rgu nimi. Leiadselle Tailscale halduspaneeli vasakus \u00fclanurgas (Tailscale logo k\u00f5rval)." } } } diff --git a/homeassistant/components/tailscale/translations/fr.json b/homeassistant/components/tailscale/translations/fr.json index 1163ec2d151..c879c2a72e7 100644 --- a/homeassistant/components/tailscale/translations/fr.json +++ b/homeassistant/components/tailscale/translations/fr.json @@ -19,7 +19,7 @@ "api_key": "Cl\u00e9 d'API", "tailnet": "Tailnet" }, - "description": "Pour vous authentifier avec Tailscale, vous devrez cr\u00e9er une cl\u00e9 API sur https://login.tailscale.com/admin/settings/authkeys. \n\n Un Tailnet est le nom de votre r\u00e9seau Tailscale. Vous pouvez le trouver dans le coin sup\u00e9rieur gauche du panneau d'administration Tailscale (\u00e0 c\u00f4t\u00e9 du logo Tailscale)." + "description": "Cette int\u00e9gration permet de surveiller votre r\u00e9seau Tailscale\u00a0; elle **NE REND PAS** votre instance Home Assistant accessible via le VPN Tailscale.\n\nAfin de vous authentifier aupr\u00e8s de Tailscale, vous devez cr\u00e9er une cl\u00e9 d'API sur {authkeys_url}.\n\nUn Tailnet est le nom de votre r\u00e9seau Tailscale. Vous pouvez le trouver dans le coin sup\u00e9rieur gauche du panneau d'administration Tailscale (\u00e0 c\u00f4t\u00e9 du logo Tailscale)." } } } diff --git a/homeassistant/components/tailscale/translations/hu.json b/homeassistant/components/tailscale/translations/hu.json index ec727cbc00f..bbc41483704 100644 --- a/homeassistant/components/tailscale/translations/hu.json +++ b/homeassistant/components/tailscale/translations/hu.json @@ -19,7 +19,7 @@ "api_key": "API kulcs", "tailnet": "Tailnet" }, - "description": "A Tailscale-rel val\u00f3 hiteles\u00edt\u00e9shez l\u00e9tre kell hoznia egy API-kulcsot a https://login.tailscale.com/admin/settings/authkeys oldalon.\n\nTailnet az \u00f6n tailscale h\u00e1l\u00f3zat\u00e1nak neve. Megtal\u00e1lhat\u00f3 a bal fels\u0151 sarokban a Tailscale Admin panelen (a Tailscale log\u00f3 mellett)." + "description": "Ez az integr\u00e1ci\u00f3 figyeli a Tailscale h\u00e1l\u00f3zat\u00e1t, \u00e9s **NEM** teszi el\u00e9rhet\u0151v\u00e9 az otthoni asszisztenst a Tailscale VPN-en kereszt\u00fcl. \n\nA Tailscale-n\u00e1l t\u00f6rt\u00e9n\u0151 hiteles\u00edt\u00e9shez l\u00e9tre kell hoznia egy API-kulcsot a {authkeys_url} c\u00edmen.\n\nA Tailnet az \u00d6n Tailscale h\u00e1l\u00f3zat\u00e1nak neve. Ezt a Tailscale Admin Panel bal fels\u0151 sark\u00e1ban tal\u00e1lja (a Tailscale log\u00f3 mellett)." } } } diff --git a/homeassistant/components/tailscale/translations/id.json b/homeassistant/components/tailscale/translations/id.json index d88a47fa82e..53b82c440aa 100644 --- a/homeassistant/components/tailscale/translations/id.json +++ b/homeassistant/components/tailscale/translations/id.json @@ -19,7 +19,7 @@ "api_key": "Kunci API", "tailnet": "Tailnet" }, - "description": "Untuk mengautentikasi dengan Tailscale, Anda harus membuat kunci API di https://login.tailscale.com/admin/settings/authkeys. \n\nTailnet adalah nama jaringan Tailscale Anda. Anda dapat menemukannya di pojok kiri atas di Panel Admin Tailscale (di samping logo Tailscale)." + "description": "Integrasi ini memantau jaringan Tailscale Anda, integrasi ini **TIDAK** membuat Home Assistant dapat diakses melalui Tailscale VPN. \n\nUntuk mengautentikasi dengan Tailscale, Anda harus membuat kunci API di {authkeys_url} . \n\nTailnet adalah nama jaringan Tailscale Anda. Anda dapat menemukannya di sudut kiri atas di Panel Admin Tailscale (di samping logo Tailscale)." } } } diff --git a/homeassistant/components/tailscale/translations/it.json b/homeassistant/components/tailscale/translations/it.json index 0dfe90b8f2e..dba31833cf9 100644 --- a/homeassistant/components/tailscale/translations/it.json +++ b/homeassistant/components/tailscale/translations/it.json @@ -19,7 +19,7 @@ "api_key": "Chiave API", "tailnet": "Tailnet" }, - "description": "Per autenticarti con Tailscale dovrai creare una chiave API su https://login.tailscale.com/admin/settings/authkeys. \n\nUna Tailnet \u00e8 il nome della tua rete Tailscale. Puoi trovarlo nell'angolo in alto a sinistra nel pannello di amministrazione di Tailscale (accanto al logo Tailscale)." + "description": "Questa integrazione monitora la tua rete Tailscale, **NON** rende il tuo Home Assistant accessibile tramite Tailscale VPN. \n\nPer autenticarti con Tailscale dovrai creare una chiave API in {authkeys_url}. \n\nUn Tailnet \u00e8 il nome della tua rete Tailscale. Puoi trovarlo nell'angolo in alto a sinistra del pannello di amministrazione di Tailscale (accanto al logo di Tailscale)." } } } diff --git a/homeassistant/components/tailscale/translations/nl.json b/homeassistant/components/tailscale/translations/nl.json index 5e46f4f0511..1a59a3ca029 100644 --- a/homeassistant/components/tailscale/translations/nl.json +++ b/homeassistant/components/tailscale/translations/nl.json @@ -19,7 +19,7 @@ "api_key": "API-sleutel", "tailnet": "Tailnet" }, - "description": "Om te authenticeren met Tailscale moet je een API-sleutel maken op https://login.tailscale.com/admin/settings/authkeys. \n\n Een Tailnet is de naam van uw Tailscale-netwerk. Je vindt het in de linkerbovenhoek in het Tailscale Admin Panel (naast het Tailscale-logo)." + "description": "Deze integratie controleert uw Tailscale netwerk, het maakt uw Home Assistant **NIET** toegankelijk via Tailscale VPN. \n\nOm te authenticeren met Tailscale moet u een API sleutel aanmaken op {authkeys_url}.\n\nEen Tailnet is de naam van uw Tailscale netwerk. U kunt het vinden in de linkerbovenhoek in het Tailscale Admin Panel (naast het Tailscale logo)." } } } diff --git a/homeassistant/components/tailscale/translations/no.json b/homeassistant/components/tailscale/translations/no.json index 627facd8f66..5c1ae4c6bc0 100644 --- a/homeassistant/components/tailscale/translations/no.json +++ b/homeassistant/components/tailscale/translations/no.json @@ -19,7 +19,7 @@ "api_key": "API-n\u00f8kkel", "tailnet": "Tailnet" }, - "description": "For \u00e5 autentisere med Tailscale m\u00e5 du opprette en API-n\u00f8kkel p\u00e5 https://login.tailscale.com/admin/settings/authkeys. \n\n Et Tailnet er navnet p\u00e5 Tailscale-nettverket ditt. Du finner den i \u00f8verste venstre hj\u00f8rne i Tailscale Admin Panel (ved siden av Tailscale-logoen)." + "description": "Denne integrasjonen overv\u00e5ker Tailscale-nettverket ditt, det ** GJ\u00d8R IKKE ** gj\u00f8r hjemmeassistenten din tilgjengelig via Tailscale VPN. \n\nHvis du vil godkjenne med Tailscale, m\u00e5 du opprette en API-n\u00f8kkel p\u00e5 {authkeys_url}.\n\nEt Tailnet er navnet p\u00e5 Tailscale-nettverket. Du finner den \u00f8verst til venstre i Tailscale Admin Panel (ved siden av Tailscale-logoen)." } } } diff --git a/homeassistant/components/tailscale/translations/pl.json b/homeassistant/components/tailscale/translations/pl.json index 579d8de83e1..ef5f1a4dabf 100644 --- a/homeassistant/components/tailscale/translations/pl.json +++ b/homeassistant/components/tailscale/translations/pl.json @@ -19,7 +19,7 @@ "api_key": "Klucz API", "tailnet": "Tailnet" }, - "description": "Aby uwierzytelni\u0107 si\u0119 w Tailscale, musisz utworzy\u0107 klucz API na stronie https://login.tailscale.com/admin/settings/authkeys.\n\nTailnet to nazwa Twojej sieci Tailscale. Mo\u017cna j\u0105 znale\u017a\u0107 w lewym g\u00f3rnym rogu w panelu administracyjnym Tailscale (obok loga Tailscale)." + "description": "Ta integracja monitoruje Twoj\u0105 sie\u0107 Tailscale, a **NIE SPRAWIA**, \u017ce Tw\u00f3j Home Assistant jest dost\u0119pny przez Tailscale VPN.\n\nAby uwierzytelni\u0107 si\u0119 w Tailscale, musisz utworzy\u0107 klucz API na stronie {authkeys_url}.\n\nTailnet to nazwa Twojej sieci Tailscale. Mo\u017cna j\u0105 znale\u017a\u0107 w lewym g\u00f3rnym rogu w panelu administracyjnym Tailscale (obok loga Tailscale)." } } } diff --git a/homeassistant/components/tailscale/translations/pt-BR.json b/homeassistant/components/tailscale/translations/pt-BR.json index ddbdda9d5a6..edd55ea6e87 100644 --- a/homeassistant/components/tailscale/translations/pt-BR.json +++ b/homeassistant/components/tailscale/translations/pt-BR.json @@ -19,7 +19,7 @@ "api_key": "Chave da API", "tailnet": "Tailnet" }, - "description": "Para autenticar com o Tailscale, voc\u00ea precisar\u00e1 criar uma chave de API em https://login.tailscale.com/admin/settings/authkeys. \n\nTailnet \u00e9 o nome da sua rede Tailscale. Voc\u00ea pode encontr\u00e1-lo no canto superior esquerdo no painel de administra\u00e7\u00e3o do Tailscale (ao lado do logotipo do Tailscale)." + "description": "Esta integra\u00e7\u00e3o monitora sua rede Tailscale, **N\u00c3O** torna seu Home Assistant acess\u00edvel via Tailscale VPN. \n\n Para autenticar com o Tailscale, voc\u00ea precisar\u00e1 criar uma chave de API em {authkeys_url} . \n\n Um Tailnet \u00e9 o nome da sua rede Tailscale. Voc\u00ea pode encontr\u00e1-lo no canto superior esquerdo no painel de administra\u00e7\u00e3o do Tailscale (ao lado do logotipo do Tailscale)." } } } diff --git a/homeassistant/components/tailscale/translations/ru.json b/homeassistant/components/tailscale/translations/ru.json index 1b97b0998e7..c05e3a09eac 100644 --- a/homeassistant/components/tailscale/translations/ru.json +++ b/homeassistant/components/tailscale/translations/ru.json @@ -19,7 +19,7 @@ "api_key": "\u041a\u043b\u044e\u0447 API", "tailnet": "Tailnet" }, - "description": "\u0414\u043b\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 https://login.tailscale.com/admin/settings/authkeys. \n\nTailnet \u2014 \u044d\u0442\u043e \u0438\u043c\u044f \u0412\u0430\u0448\u0435\u0439 \u0441\u0435\u0442\u0438 Tailscale. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0439\u0442\u0438 \u0435\u0433\u043e \u0432 \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u043b\u0435\u0432\u043e\u043c \u0443\u0433\u043b\u0443 \u043f\u0430\u043d\u0435\u043b\u0438 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430 Tailscale (\u0440\u044f\u0434\u043e\u043c \u0441 \u043b\u043e\u0433\u043e\u0442\u0438\u043f\u043e\u043c Tailscale)." + "description": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u0442 \u0412\u0430\u0448\u0443 \u0441\u0435\u0442\u044c Tailscale, \u043e\u043d\u0430 **\u041d\u0415** \u0434\u0435\u043b\u0430\u0435\u0442 \u0412\u0430\u0448 Home Assistant \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c \u0447\u0435\u0440\u0435\u0437 Tailscale VPN. \n\n\u0414\u043b\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 {authkeys_url}. \n\nTailnet \u2014 \u044d\u0442\u043e \u0438\u043c\u044f \u0412\u0430\u0448\u0435\u0439 \u0441\u0435\u0442\u0438 Tailscale. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0439\u0442\u0438 \u0435\u0433\u043e \u0432 \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u043b\u0435\u0432\u043e\u043c \u0443\u0433\u043b\u0443 \u043f\u0430\u043d\u0435\u043b\u0438 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430 Tailscale (\u0440\u044f\u0434\u043e\u043c \u0441 \u043b\u043e\u0433\u043e\u0442\u0438\u043f\u043e\u043c Tailscale)." } } } diff --git a/homeassistant/components/tailscale/translations/tr.json b/homeassistant/components/tailscale/translations/tr.json index 680acd65d70..784bda86b7d 100644 --- a/homeassistant/components/tailscale/translations/tr.json +++ b/homeassistant/components/tailscale/translations/tr.json @@ -19,7 +19,7 @@ "api_key": "API Anahtar\u0131", "tailnet": "Tailnet" }, - "description": "Tailscale ile kimlik do\u011frulamas\u0131 yapmak i\u00e7in https://login.tailscale.com/admin/settings/authkeys adresinde bir API anahtar\u0131 olu\u015fturman\u0131z gerekir. \n\n Kuyruk a\u011f\u0131, Kuyruk \u00f6l\u00e7e\u011fi a\u011f\u0131n\u0131z\u0131n ad\u0131d\u0131r. Bunu, Tailscale Y\u00f6netici Panelinin sol \u00fcst k\u00f6\u015fesinde (Tailscale logosunun yan\u0131nda) bulabilirsiniz." + "description": "Bu entegrasyon, Tailscale a\u011f\u0131n\u0131z\u0131 izler, ancak Ev Asistan\u0131n\u0131z\u0131 Tailscale VPN arac\u0131l\u0131\u011f\u0131yla eri\u015filebilir k\u0131lmaz. \n\n Tailscale ile kimlik do\u011frulamas\u0131 yapmak i\u00e7in {authkeys_url} adresinde bir API anahtar\u0131 olu\u015fturman\u0131z gerekir. \n\n Kuyruk a\u011f\u0131, Kuyruk \u00f6l\u00e7e\u011fi a\u011f\u0131n\u0131z\u0131n ad\u0131d\u0131r. Bunu, Tailscale Y\u00f6netici Panelinin sol \u00fcst k\u00f6\u015fesinde (Tailscale logosunun yan\u0131nda) bulabilirsiniz." } } } diff --git a/homeassistant/components/tailscale/translations/zh-Hant.json b/homeassistant/components/tailscale/translations/zh-Hant.json index 5ed5f1deb8f..316168fa67a 100644 --- a/homeassistant/components/tailscale/translations/zh-Hant.json +++ b/homeassistant/components/tailscale/translations/zh-Hant.json @@ -19,7 +19,7 @@ "api_key": "API \u91d1\u9470", "tailnet": "Tailnet" }, - "description": "\u6b32\u4f7f\u7528 Tailscale \u8a8d\u8b49\u3001\u5c07\u9700\u8981\u65bc https://login.tailscale.com/admin/settings/authkeys \u65b0\u589e\u4e00\u7d44 API \u91d1\u9470 \n\nTailnet \u70ba Tailscale \u7db2\u8def\u7684\u540d\u7a31\uff0c\u53ef\u4ee5\u65bc Tailscale \u7ba1\u7406\u9762\u677f\uff08Tailscale Logo \u65c1\uff09\u7684\u5de6\u4e0a\u65b9\u627e\u5230\u6b64\u8cc7\u8a0a\u3002" + "description": "\u6574\u5408\u5c07\u76e3\u63a7 Tailscale \u7db2\u8def\uff0c**\u4e26\u975e** \u8b93\u60a8\u7684 Home Assistant \u900f\u904e Tailscale VPN \u5b58\u53d6\u3002 \n\n\u6b32\u4f7f\u7528 Tailscale \u8a8d\u8b49\u3001\u5c07\u9700\u8981\u65bc {authkeys_url} \u65b0\u589e\u4e00\u7d44 API \u91d1\u9470\u3002\n\nTailnet \u70ba Tailscale \u7db2\u8def\u7684\u540d\u7a31\uff0c\u53ef\u4ee5\u65bc Tailscale \u7ba1\u7406\u9762\u677f\uff08Tailscale Logo \u65c1\uff09\u7684\u5de6\u4e0a\u65b9\u627e\u5230\u6b64\u8cc7\u8a0a\u3002" } } } diff --git a/homeassistant/components/tankerkoenig/translations/bg.json b/homeassistant/components/tankerkoenig/translations/bg.json new file mode 100644 index 00000000000..700c4f3000b --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/bg.json @@ -0,0 +1,29 @@ +{ + "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": { + "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", + "fuel_types": "\u0412\u0438\u0434\u043e\u0432\u0435 \u0433\u043e\u0440\u0438\u0432\u043e", + "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441 \u043d\u0430 \u0442\u044a\u0440\u0441\u0435\u043d\u0435" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043d\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/ca.json b/homeassistant/components/tankerkoenig/translations/ca.json new file mode 100644 index 00000000000..676bb1ccb55 --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/ca.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "no_stations": "No s'ha pogut trobar cap estaci\u00f3 a l'abast." + }, + "step": { + "select_station": { + "data": { + "stations": "Estacions" + }, + "description": "S'han trobat {stations_count} estacions dins el radi", + "title": "Selecciona les estacions a afegir" + }, + "user": { + "data": { + "api_key": "Clau API", + "fuel_types": "Tipus de combustible", + "location": "Ubicaci\u00f3", + "name": "Nom de la regi\u00f3", + "radius": "Radi de cerca", + "stations": "Estacions de servei addicionals" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Interval d'actualitzaci\u00f3", + "show_on_map": "Mostra les estacions al mapa" + }, + "title": "Opcions de Tankerkoenig" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/de.json b/homeassistant/components/tankerkoenig/translations/de.json new file mode 100644 index 00000000000..3c2a5f1ec72 --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/de.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Standort ist bereits konfiguriert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "no_stations": "Konnte keine Station in Reichweite finden." + }, + "step": { + "select_station": { + "data": { + "stations": "Stationen" + }, + "description": "{stations_count} Stationen im Umkreis gefunden", + "title": "W\u00e4hle Stationen zum Hinzuf\u00fcgen aus" + }, + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "fuel_types": "Kraftstoffarten", + "location": "Standort", + "name": "Name der Region", + "radius": "Suchradius", + "stations": "Zus\u00e4tzliche Tankstellen" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update-Intervall", + "show_on_map": "Stationen auf der Karte anzeigen" + }, + "title": "Tankerkoenig Optionen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/el.json b/homeassistant/components/tankerkoenig/translations/el.json new file mode 100644 index 00000000000..7f814b9760c --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/el.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "no_stations": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd \u03b5\u03bd\u03c4\u03cc\u03c2 \u03b5\u03bc\u03b2\u03ad\u03bb\u03b5\u03b9\u03b1\u03c2." + }, + "step": { + "select_station": { + "data": { + "stations": "\u03a3\u03c4\u03b1\u03b8\u03bc\u03bf\u03af" + }, + "description": "\u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd {stations_count} \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03af \u03c3\u03b5 \u03b1\u03ba\u03c4\u03af\u03bd\u03b1", + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7" + }, + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "fuel_types": "\u03a4\u03cd\u03c0\u03bf\u03b9 \u03ba\u03b1\u03c5\u03c3\u03af\u03bc\u03c9\u03bd", + "location": "\u03a4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae\u03c2", + "radius": "\u0391\u03ba\u03c4\u03af\u03bd\u03b1 \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7\u03c2", + "stations": "\u03a0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03b1 \u03c0\u03c1\u03b1\u03c4\u03ae\u03c1\u03b9\u03b1 \u03ba\u03b1\u03c5\u03c3\u03af\u03bc\u03c9\u03bd" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2", + "show_on_map": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c3\u03c4\u03b1\u03b8\u03bc\u03ce\u03bd \u03c3\u03c4\u03bf \u03c7\u03ac\u03c1\u03c4\u03b7" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 Tankerkoenig" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/et.json b/homeassistant/components/tankerkoenig/translations/et.json new file mode 100644 index 00000000000..c15e4b78ddf --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/et.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Asukoht on juba m\u00e4\u00e4ratud" + }, + "error": { + "invalid_auth": "Tuvastamine nurjus", + "no_stations": "Piirkonnas ei leitud \u00fchtegi tanklat" + }, + "step": { + "select_station": { + "data": { + "stations": "Tanklad" + }, + "description": "piirkonnas on leitud {stations_count} tanklat", + "title": "Vali lisatavad tanklad" + }, + "user": { + "data": { + "api_key": "API v\u00f5ti", + "fuel_types": "K\u00fctuse liigid", + "location": "Asukoht", + "name": "Piirkonna nimi", + "radius": "Otsingu raadius", + "stations": "T\u00e4iendavad tanklad" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "V\u00e4rskendamise intervall", + "show_on_map": "N\u00e4ita jaamu kaardil" + }, + "title": "Tankerkoenig valikud" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/fr.json b/homeassistant/components/tankerkoenig/translations/fr.json new file mode 100644 index 00000000000..1a52eee5d39 --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/fr.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "invalid_auth": "Authentification non valide", + "no_stations": "Aucune station-service n'a \u00e9t\u00e9 trouv\u00e9e dans le rayon indiqu\u00e9." + }, + "step": { + "select_station": { + "data": { + "stations": "Stations-services" + }, + "description": "{stations_count}\u00a0stations-services trouv\u00e9es dans le rayon", + "title": "S\u00e9lectionnez les stations-services \u00e0 ajouter" + }, + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "fuel_types": "Types de carburant", + "location": "Emplacement", + "name": "Nom de la r\u00e9gion", + "radius": "Rayon de recherche", + "stations": "Stations-services suppl\u00e9mentaires" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalle de mise \u00e0 jour", + "show_on_map": "Afficher les stations-services sur la carte" + }, + "title": "Options Tankerkoenig" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/he.json b/homeassistant/components/tankerkoenig/translations/he.json new file mode 100644 index 00000000000..9ccb9aecfe6 --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "location": "\u05de\u05d9\u05e7\u05d5\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/hu.json b/homeassistant/components/tankerkoenig/translations/hu.json new file mode 100644 index 00000000000..e2c31e9e354 --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/hu.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "no_stations": "Nem tal\u00e1lhat\u00f3 \u00e1llom\u00e1s a hat\u00f3t\u00e1vols\u00e1gon bel\u00fcl." + }, + "step": { + "select_station": { + "data": { + "stations": "\u00c1llom\u00e1sok" + }, + "description": "{stations_count} \u00e1llom\u00e1s tal\u00e1lhat\u00f3 a hat\u00f3t\u00e1vols\u00e1gon bel\u00fcl", + "title": "Hozz\u00e1adand\u00f3 \u00e1llom\u00e1sok kiv\u00e1laszt\u00e1sa" + }, + "user": { + "data": { + "api_key": "API kulcs", + "fuel_types": "\u00dczemanyag t\u00edpusok", + "location": "Elhelyezked\u00e9s", + "name": "R\u00e9gi\u00f3 neve", + "radius": "Keres\u00e9s hat\u00f3t\u00e1vols\u00e1ga", + "stations": "Tov\u00e1bbi \u00fczemanyagt\u00f6lt\u0151 \u00e1llom\u00e1sok" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Friss\u00edt\u00e9si id\u0151k\u00f6z", + "show_on_map": "\u00c1llom\u00e1sok megjelen\u00edt\u00e9se a t\u00e9rk\u00e9pen" + }, + "title": "Tankerkoenig be\u00e1ll\u00edt\u00e1sok" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/id.json b/homeassistant/components/tankerkoenig/translations/id.json new file mode 100644 index 00000000000..cddeb02b17f --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/id.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid", + "no_stations": "Tidak dapat menemukan SPBU dalam jangkauan." + }, + "step": { + "select_station": { + "data": { + "stations": "SPBU" + }, + "description": "ditemukan {stations_count} SPBU dalam radius", + "title": "Pilih SPBU untuk ditambahkan" + }, + "user": { + "data": { + "api_key": "Kunci API", + "fuel_types": "Jenis bahan bakar", + "location": "Lokasi", + "name": "Nama wilayah", + "radius": "Radius pencarian", + "stations": "SPBU tambahan" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Interval pembaruan", + "show_on_map": "Tampilkan SPBU di peta" + }, + "title": "Opsi Tankerkoenig" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/it.json b/homeassistant/components/tankerkoenig/translations/it.json new file mode 100644 index 00000000000..e24c353d4f1 --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/it.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "La posizione \u00e8 gi\u00e0 configurata" + }, + "error": { + "invalid_auth": "Autenticazione non valida", + "no_stations": "Impossibile trovare nessuna stazione nel raggio d'azione." + }, + "step": { + "select_station": { + "data": { + "stations": "Stazioni" + }, + "description": "trovato {stations_count} stazioni nel raggio", + "title": "Seleziona le stazioni da aggiungere" + }, + "user": { + "data": { + "api_key": "Chiave API", + "fuel_types": "Tipi di carburante", + "location": "Posizione", + "name": "Nome della regione", + "radius": "Raggio di ricerca", + "stations": "Stazioni di servizio aggiuntive" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervallo di aggiornamento", + "show_on_map": "Mostra stazioni sulla mappa" + }, + "title": "Opzioni Tankerkoenig" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/ja.json b/homeassistant/components/tankerkoenig/translations/ja.json new file mode 100644 index 00000000000..687e05322d5 --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/ja.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "no_stations": "\u7bc4\u56f2\u5185\u306b\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002" + }, + "step": { + "select_station": { + "data": { + "stations": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3" + }, + "description": "\u534a\u5f84\u5185\u306b {stations_count} \u500b\u306e\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u304c\u898b\u3064\u304b\u308a\u307e\u3057\u305f", + "title": "\u8ffd\u52a0\u3059\u308b\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u3092\u9078\u629e\u3057\u307e\u3059" + }, + "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "fuel_types": "\u71c3\u6599\u306e\u7a2e\u985e", + "location": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3", + "name": "\u5730\u57df\u540d", + "radius": "\u691c\u7d22\u534a\u5f84", + "stations": "\u8ffd\u52a0\u306e\u71c3\u6599\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u9593\u9694", + "show_on_map": "\u5730\u56f3\u4e0a\u306b\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u3092\u8868\u793a\u3059\u308b" + }, + "title": "Tankerkoenig\u30aa\u30d7\u30b7\u30e7\u30f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/nl.json b/homeassistant/components/tankerkoenig/translations/nl.json new file mode 100644 index 00000000000..96de058ad74 --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/nl.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Locatie is al geconfigureerd." + }, + "error": { + "invalid_auth": "Ongeldige authenticatie", + "no_stations": "Kon geen station in bereik vinden." + }, + "step": { + "select_station": { + "data": { + "stations": "Stations" + }, + "description": "{stations_count} gevonden in radius", + "title": "Selecteer stations om toe te voegen" + }, + "user": { + "data": { + "api_key": "API-sleutel", + "fuel_types": "Brandstofsoorten", + "location": "Locatie", + "name": "Regionaam", + "radius": "Zoekradius", + "stations": "Extra tankstations" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update Interval", + "show_on_map": "Toon stations op kaart" + }, + "title": "Tankerkoenig opties" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/no.json b/homeassistant/components/tankerkoenig/translations/no.json new file mode 100644 index 00000000000..9d0b6ddab52 --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/no.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Plasseringen er allerede konfigurert" + }, + "error": { + "invalid_auth": "Ugyldig godkjenning", + "no_stations": "Kunne ikke finne noen stasjon innen rekkevidde." + }, + "step": { + "select_station": { + "data": { + "stations": "Stasjoner" + }, + "description": "fant {stations_count} stasjoner i radius", + "title": "Velg stasjoner du vil legge til" + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "fuel_types": "Drivstofftyper", + "location": "Plassering", + "name": "Navn p\u00e5 omr\u00e5de", + "radius": "Radius for s\u00f8k", + "stations": "Flere bensinstasjoner" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Oppdateringsintervall", + "show_on_map": "Vis stasjoner p\u00e5 kart" + }, + "title": "Tankerkoenig alternativer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/pl.json b/homeassistant/components/tankerkoenig/translations/pl.json new file mode 100644 index 00000000000..e13ae2c4783 --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/pl.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie", + "no_stations": "Nie mo\u017cna znale\u017a\u0107 \u017cadnej stacji w zasi\u0119gu." + }, + "step": { + "select_station": { + "data": { + "stations": "Stacje" + }, + "description": "liczba znalezionych stacji w promieniu: {stations_count}", + "title": "Wybierz stacje do dodania" + }, + "user": { + "data": { + "api_key": "Klucz API", + "fuel_types": "Rodzaje paliw", + "location": "Lokalizacja", + "name": "Nazwa regionu", + "radius": "Promie\u0144 wyszukiwania", + "stations": "Dodatkowe stacje paliw" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji", + "show_on_map": "Poka\u017c stacje na mapie" + }, + "title": "Opcje Tankerkoenig" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/pt-BR.json b/homeassistant/components/tankerkoenig/translations/pt-BR.json new file mode 100644 index 00000000000..b24e0b1dca8 --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/pt-BR.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_stations": "N\u00e3o foi poss\u00edvel encontrar nenhum posto ao alcance." + }, + "step": { + "select_station": { + "data": { + "stations": "Postos de combustiveis" + }, + "description": "encontrou {stations_count} postos no raio", + "title": "Selecione postos para adicionar" + }, + "user": { + "data": { + "api_key": "Chave da API", + "fuel_types": "Tipos de combust\u00edvel", + "location": "Localiza\u00e7\u00e3o", + "name": "Nome da regi\u00e3o", + "radius": "Raio de pesquisa", + "stations": "Postos de combust\u00edvel adicionais" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalo de atualiza\u00e7\u00e3o", + "show_on_map": "Mostrar postos no mapa" + }, + "title": "Op\u00e7\u00f5es de Tankerkoenig" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/ru.json b/homeassistant/components/tankerkoenig/translations/ru.json new file mode 100644 index 00000000000..bf61fddc0c5 --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/ru.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "no_stations": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u043d\u0438 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u0432 \u0440\u0430\u0434\u0438\u0443\u0441\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f." + }, + "step": { + "select_station": { + "data": { + "stations": "\u0421\u0442\u0430\u043d\u0446\u0438\u0438" + }, + "description": "\u041d\u0430\u0439\u0434\u0435\u043d\u043e {stations_count} \u0441\u0442\u0430\u043d\u0446\u0438\u0439 \u0432 \u0440\u0430\u0434\u0438\u0443\u0441\u0435.", + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0442\u0430\u043d\u0446\u0438\u0438" + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "fuel_types": "\u0412\u0438\u0434\u044b \u0442\u043e\u043f\u043b\u0438\u0432\u0430", + "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0440\u0435\u0433\u0438\u043e\u043d\u0430", + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441 \u043f\u043e\u0438\u0441\u043a\u0430", + "stations": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0437\u0430\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u044b\u0435 \u0441\u0442\u0430\u043d\u0446\u0438\u0438" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f", + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 \u043a\u0430\u0440\u0442\u0435" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Tankerkoenig" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/tr.json b/homeassistant/components/tankerkoenig/translations/tr.json new file mode 100644 index 00000000000..2d88d2fa670 --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/tr.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "no_stations": "Menzilde herhangi bir istasyon bulunamad\u0131." + }, + "step": { + "select_station": { + "data": { + "stations": "\u0130stasyonlar" + }, + "description": "yar\u0131\u00e7ap i\u00e7inde {stations_count} istasyon bulundu", + "title": "Eklenecek istasyonlar\u0131 se\u00e7in" + }, + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "fuel_types": "Yak\u0131t t\u00fcrleri", + "location": "Konum", + "name": "B\u00f6lge ad\u0131", + "radius": "Yar\u0131\u00e7ap\u0131 ara\u015ft\u0131r", + "stations": "Ek yak\u0131t istasyonlar\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "G\u00fcncelle\u015ftirme aral\u0131\u011f\u0131", + "show_on_map": "\u0130stasyonlar\u0131 haritada g\u00f6ster" + }, + "title": "Tankerkoenig se\u00e7enekleri" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/zh-Hant.json b/homeassistant/components/tankerkoenig/translations/zh-Hant.json new file mode 100644 index 00000000000..059d07ccdc6 --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/zh-Hant.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "no_stations": "\u7bc4\u570d\u5167\u627e\u4e0d\u5230\u4efb\u4f55\u52a0\u6cb9\u7ad9\u3002" + }, + "step": { + "select_station": { + "data": { + "stations": "\u52a0\u6cb9\u7ad9" + }, + "description": "\u65bc\u534a\u5f91\u5167\u627e\u5230 {stations_count} \u5ea7\u52a0\u6cb9\u7ad9", + "title": "\u9078\u64c7\u65b0\u589e\u52a0\u6cb9\u7ad9" + }, + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "fuel_types": "\u71c3\u6599\u985e\u5225", + "location": "\u5ea7\u6a19", + "name": "\u5340\u57df\u540d\u7a31", + "radius": "\u641c\u5c0b\u534a\u5f91", + "stations": "\u5176\u4ed6\u52a0\u6cb9\u7ad9" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u983b\u7387", + "show_on_map": "\u65bc\u5730\u5716\u986f\u793a\u52a0\u6cb9\u7ad9" + }, + "title": "Tankerkoenig \u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/de.json b/homeassistant/components/tautulli/translations/de.json new file mode 100644 index 00000000000..bb1e2bc8c95 --- /dev/null +++ b/homeassistant/components/tautulli/translations/de.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-Schl\u00fcssel" + }, + "description": "Um deinen API-Schl\u00fcssel zu finden, \u00f6ffne die Tautulli-Webseite und navigiere zu Einstellungen und dann zu Webinterface. Der API-Schl\u00fcssel befindet sich unten auf dieser Seite.", + "title": "Tautulli erneut authentifizieren" + }, + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "url": "URL", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + }, + "description": "Um deinen API-Schl\u00fcssel zu finden, \u00f6ffne die Tautulli-Webseite und navigiere zu Einstellungen und dann zu Webinterface. Der API-Schl\u00fcssel befindet sich unten auf dieser Seite.\n\nBeispiel f\u00fcr die URL: ```http://192.168.0.10:8181`` mit 8181 als Standard-Port." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/el.json b/homeassistant/components/tautulli/translations/el.json new file mode 100644 index 00000000000..a83662fb6e6 --- /dev/null +++ b/homeassistant/components/tautulli/translations/el.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, + "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API, \u03b1\u03bd\u03bf\u03af\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b9\u03c3\u03c4\u03bf\u03c3\u03b5\u03bb\u03af\u03b4\u03b1 Tautulli \u03ba\u03b1\u03b9 \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b9\u03c2 \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03ba\u03b1\u03b9 \u03bc\u03b5\u03c4\u03ac \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u0399\u03c3\u03c4\u03bf\u03cd. \u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b8\u03b1 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf \u03ba\u03ac\u03c4\u03c9 \u03bc\u03ad\u03c1\u03bf\u03c2 \u03b1\u03c5\u03c4\u03ae\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1\u03c2.", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 Tautulli" + }, + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", + "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" + }, + "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API, \u03b1\u03bd\u03bf\u03af\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b9\u03c3\u03c4\u03bf\u03c3\u03b5\u03bb\u03af\u03b4\u03b1 Tautulli \u03ba\u03b1\u03b9 \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b9\u03c2 \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03ba\u03b1\u03b9 \u03bc\u03b5\u03c4\u03ac \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u0399\u03c3\u03c4\u03bf\u03cd. \u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b8\u03b1 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf \u03ba\u03ac\u03c4\u03c9 \u03bc\u03ad\u03c1\u03bf\u03c2 \u03b1\u03c5\u03c4\u03ae\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1\u03c2. \n\n \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL: ```http://192.168.0.10:8181``` \u03bc\u03b5 \u03c4\u03bf 8181 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b7 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/en.json b/homeassistant/components/tautulli/translations/en.json index 90ce8c7bbfc..c24497cfbd9 100644 --- a/homeassistant/components/tautulli/translations/en.json +++ b/homeassistant/components/tautulli/translations/en.json @@ -1,31 +1,30 @@ { - "config": { - "step": { - "user": { - "description": "To find your API key, open the Tautulli webpage and navigate to Settings and then to Web interface. The API key will be at the bottom of that page.\n\nExample of the URL: ```http://192.168.0.10:8181``` with 8181 being the default port.", - "data": { - "api_key": "Api Key", - "url": "URL", - "verify_ssl": "Verify SSL certificate" + "config": { + "abort": { + "reauth_successful": "Re-authentication was successful", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API Key" + }, + "description": "To find your API key, open the Tautulli webpage and navigate to Settings and then to Web interface. The API key will be at the bottom of that page.", + "title": "Re-authenticate Tautulli" + }, + "user": { + "data": { + "api_key": "API Key", + "url": "URL", + "verify_ssl": "Verify SSL certificate" + }, + "description": "To find your API key, open the Tautulli webpage and navigate to Settings and then to Web interface. The API key will be at the bottom of that page.\n\nExample of the URL: ```http://192.168.0.10:8181``` with 8181 being the default port." + } } - }, - "reauth_confirm": { - "title": "Re-authenticate Tautulli", - "description": "To find your API key, open the Tautulli webpage and navigate to Settings and then to Web interface. The API key will be at the bottom of that page.", - "data": { - "api_key": "Api Key" - } - } - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, - "abort": { - "single_instance_allowed": "Already configured. Only a single configuration possible.", - "reauth_successful": "Re-authentication was successful" } - } -} - \ No newline at end of file +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/et.json b/homeassistant/components/tautulli/translations/et.json new file mode 100644 index 00000000000..bc690db7a28 --- /dev/null +++ b/homeassistant/components/tautulli/translations/et.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API v\u00f5ti" + }, + "description": "API-v\u00f5tme leidmiseks ava Tautulli veebileht ja navigeeri jaotisse Seaded ja seej\u00e4rel veebiliidesesse. API v\u00f5ti asub selle lehe allosas.", + "title": "Taastuvasta Tautulli" + }, + "user": { + "data": { + "api_key": "API v\u00f5ti", + "url": "URL", + "verify_ssl": "Kontrolli SSL sertifikaati" + }, + "description": "API-v\u00f5tme leidmiseks ava Tautulli veebileht ja navigeeri jaotisse Seaded ja seej\u00e4rel veebiliidesesse. API v\u00f5ti asub selle lehe allosas. \n\n URL-i n\u00e4ide: ```http://192.168.0.10:8181```, vaikeportiks on 8181." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/fr.json b/homeassistant/components/tautulli/translations/fr.json new file mode 100644 index 00000000000..05b66af0972 --- /dev/null +++ b/homeassistant/components/tautulli/translations/fr.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Cl\u00e9 d'API" + }, + "description": "Pour trouver votre cl\u00e9 d'API, ouvrez la page web de Tautulli et acc\u00e9dez aux param\u00e8tres puis \u00e0 l'interface web. La cl\u00e9 d'API se trouve au bas de cette page.", + "title": "R\u00e9-authentifier Tautulli" + }, + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "url": "URL", + "verify_ssl": "V\u00e9rifier le certificat SSL" + }, + "description": "Pour trouver votre cl\u00e9 d'API, ouvrez la page web de Tautulli et acc\u00e9dez aux param\u00e8tres puis \u00e0 l'interface web. La cl\u00e9 d'API se trouve au bas de cette page.\n\nExemple d'URL\u00a0: ```http://192.168.0.10:8181``` o\u00f9 8181 est le port par d\u00e9faut." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/hu.json b/homeassistant/components/tautulli/translations/hu.json new file mode 100644 index 00000000000..b654081ecd1 --- /dev/null +++ b/homeassistant/components/tautulli/translations/hu.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API kulcs" + }, + "description": "Az API-kulcs megkeres\u00e9s\u00e9hez nyissa meg a Tautulli weblapot, \u00e9s keresse meg a Be\u00e1ll\u00edt\u00e1sok, majd a webes fel\u00fcletet. Az API-kulcs az oldal alj\u00e1n lesz.", + "title": "Tautulli \u00fajrahiteles\u00edt\u00e9se" + }, + "user": { + "data": { + "api_key": "API kulcs", + "url": "URL", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + }, + "description": "Az API-kulcs megtal\u00e1l\u00e1s\u00e1hoz nyissa meg a Tautulli weboldalt, \u00e9s navig\u00e1ljon a Be\u00e1ll\u00edt\u00e1sok, majd a Webes fel\u00fcletre. Az API-kulcs az oldal alj\u00e1n tal\u00e1lhat\u00f3. \n\nP\u00e9lda az URL-re: ```http://192.168.0.10:8181```, ahol a 8181 az alap\u00e9rtelmezett port." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/id.json b/homeassistant/components/tautulli/translations/id.json new file mode 100644 index 00000000000..c2042dacffa --- /dev/null +++ b/homeassistant/components/tautulli/translations/id.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "reauth_successful": "Autentikasi ulang berhasil", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Kunci API" + }, + "description": "Untuk menemukan kunci API Anda, buka halaman web Tautulli dan navigasikan ke Pengaturan lalu ke antarmuka Web. Kunci API akan berada di bagian bawah halaman tersebut.", + "title": "Autentikasi Ulang Tautulli" + }, + "user": { + "data": { + "api_key": "Kunci API", + "url": "URL", + "verify_ssl": "Verifikasi sertifikat SSL" + }, + "description": "Untuk menemukan kunci API Anda, buka halaman web Tautulli dan navigasikan ke Pengaturan lalu ke antarmuka Web. Kunci API akan berada di bagian bawah halaman tersebut.\n\nContoh URL: ```http://192.168.0.10:8181``` dengan 8181 sebagai port default." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/it.json b/homeassistant/components/tautulli/translations/it.json new file mode 100644 index 00000000000..7dcfb1dd057 --- /dev/null +++ b/homeassistant/components/tautulli/translations/it.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chiave API" + }, + "description": "Per trovare la tua chiave API, apri la pagina web di Tautulli e vai su Impostazioni e poi su Interfaccia web. La chiave API sar\u00e0 in fondo a quella pagina.", + "title": "Autentica nuovamente Tautulli" + }, + "user": { + "data": { + "api_key": "Chiave API", + "url": "URL", + "verify_ssl": "Verifica il certificato SSL" + }, + "description": "Per trovare la tua chiave API, apri la pagina web di Tautulli e vai su Impostazioni e poi su Interfaccia web. La chiave API sar\u00e0 in fondo a quella pagina. \n\n Esempio dell'URL: ```http://192.168.0.10:8181``` con 8181 come porta predefinita." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/nl.json b/homeassistant/components/tautulli/translations/nl.json new file mode 100644 index 00000000000..eeecbd56477 --- /dev/null +++ b/homeassistant/components/tautulli/translations/nl.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "reauth_successful": "Herauthenticatie was succesvol", + "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-sleutel" + }, + "title": "Herauthenticeer Tautulli" + }, + "user": { + "data": { + "api_key": "API-sleutel", + "url": "URL", + "verify_ssl": "SSL-certificaat verifi\u00ebren" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/no.json b/homeassistant/components/tautulli/translations/no.json new file mode 100644 index 00000000000..cd6667b26fe --- /dev/null +++ b/homeassistant/components/tautulli/translations/no.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "description": "For \u00e5 finne API-n\u00f8kkelen din, \u00e5pne Tautulli-nettsiden og naviger til Innstillinger og deretter til nettgrensesnitt. API-n\u00f8kkelen vil v\u00e6re nederst p\u00e5 siden.", + "title": "Autentiser Tautulli p\u00e5 nytt" + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "url": "URL", + "verify_ssl": "Verifisere SSL-sertifikat" + }, + "description": "For \u00e5 finne API-n\u00f8kkelen din, \u00e5pne Tautulli-nettsiden og naviger til Innstillinger og deretter til nettgrensesnitt. API-n\u00f8kkelen vil v\u00e6re nederst p\u00e5 siden. \n\n Eksempel p\u00e5 URL: ```http://192.168.0.10:8181``` med 8181 som standardport." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/pl.json b/homeassistant/components/tautulli/translations/pl.json new file mode 100644 index 00000000000..25684ac6b3c --- /dev/null +++ b/homeassistant/components/tautulli/translations/pl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Klucz API" + }, + "description": "Aby znale\u017a\u0107 klucz API, otw\u00f3rz stron\u0119 Tautulli i przejd\u017a do \"Settings\", a nast\u0119pnie do \"Web interface\". Klucz API b\u0119dzie znajdowa\u0107 si\u0119 na dole tej strony.", + "title": "Ponowne uwierzytelnienie Tautulli" + }, + "user": { + "data": { + "api_key": "Klucz API", + "url": "URL", + "verify_ssl": "Weryfikacja certyfikatu SSL" + }, + "description": "Aby znale\u017a\u0107 klucz API, otw\u00f3rz stron\u0119 Tautulli i przejd\u017a do \"Settings\", a nast\u0119pnie do \"Web interface\". Klucz API b\u0119dzie znajdowa\u0107 si\u0119 na dole tej strony. \n\nPrzyk\u0142ad adresu URL: \"http://192.168.0.10:8181\" z portem domy\u015blnym 8181." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/pt-BR.json b/homeassistant/components/tautulli/translations/pt-BR.json new file mode 100644 index 00000000000..45c5b508f96 --- /dev/null +++ b/homeassistant/components/tautulli/translations/pt-BR.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + }, + "description": "Para encontrar sua chave de API, abra a p\u00e1gina da Web do Tautulli e navegue at\u00e9 Configura\u00e7\u00f5es e, em seguida, para a interface da Web. A chave da API estar\u00e1 na parte inferior dessa p\u00e1gina.", + "title": "Re-autenticar Tautulli" + }, + "user": { + "data": { + "api_key": "Chave da API", + "url": "URL", + "verify_ssl": "Verifique o certificado SSL" + }, + "description": "Para encontrar sua chave de API, abra a p\u00e1gina da Web do Tautulli e navegue at\u00e9 Configura\u00e7\u00f5es e, em seguida, para a interface da Web. A chave da API estar\u00e1 na parte inferior dessa p\u00e1gina. \n\n Exemplo da URL: ```http://192.168.0.10:8181``` com 8181 sendo a porta padr\u00e3o." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/ru.json b/homeassistant/components/tautulli/translations/ru.json new file mode 100644 index 00000000000..fc5e6584157 --- /dev/null +++ b/homeassistant/components/tautulli/translations/ru.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "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": { + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0427\u0442\u043e\u0431\u044b \u043d\u0430\u0439\u0442\u0438 \u043a\u043b\u044e\u0447 API, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u0432\u0435\u0431-\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 Tautulli \u0438 \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u00ab\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u00bb, \u0430 \u0437\u0430\u0442\u0435\u043c \u0432 \u00ab\u0412\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u00bb. \u041a\u043b\u044e\u0447 API \u0431\u0443\u0434\u0435\u0442 \u0432\u043d\u0438\u0437\u0443 \u044d\u0442\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f" + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "url": "URL-\u0430\u0434\u0440\u0435\u0441", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "description": "\u0427\u0442\u043e\u0431\u044b \u043d\u0430\u0439\u0442\u0438 \u0441\u0432\u043e\u0439 \u043a\u043b\u044e\u0447 API, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u0432\u0435\u0431-\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 Tautulli \u0438 \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u00ab\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u00bb, \u0430 \u0437\u0430\u0442\u0435\u043c \u0432 \u00ab\u0412\u0435\u0431-\u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u00bb. \u041a\u043b\u044e\u0447 API \u0431\u0443\u0434\u0435\u0442 \u0432\u043d\u0438\u0437\u0443 \u044d\u0442\u043e\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b. \n\n\u041f\u0440\u0438\u043c\u0435\u0440 URL-\u0430\u0434\u0440\u0435\u0441\u0430: ```http://192.168.0.10:8181```, \u0433\u0434\u0435 8181 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u0440\u0442\u043e\u043c \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/zh-Hant.json b/homeassistant/components/tautulli/translations/zh-Hant.json new file mode 100644 index 00000000000..aed9ce361ee --- /dev/null +++ b/homeassistant/components/tautulli/translations/zh-Hant.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u91d1\u9470" + }, + "description": "\u8981\u53d6\u5f97 API \u91d1\u9470\uff0c\u958b\u555f Tautulli \u7db2\u9801\u3001\u4e26\u700f\u89bd\u8a2d\u5b9a\u4e2d\u7684\u7db2\u7ad9\u4ecb\u9762\uff08Web interface\uff09\u3002\u53ef\u4ee5\u65bc\u9801\u9762\u4e0b\u65b9\u627e\u5230 API \u91d1\u9470\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49 Tautulli" + }, + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "url": "\u7db2\u5740", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + }, + "description": "\u8981\u53d6\u5f97 API \u91d1\u9470\uff0c\u958b\u555f Tautulli \u7db2\u9801\u3001\u4e26\u700f\u89bd\u8a2d\u5b9a\u4e2d\u7684\u7db2\u7ad9\u4ecb\u9762\uff08Web interface\uff09\u3002\u53ef\u4ee5\u65bc\u9801\u9762\u4e0b\u65b9\u627e\u5230 API \u91d1\u9470\u3002\n\nURL \u7bc4\u4f8b\uff1a```http://192.168.0.10:8181``` \u4e26\u4ee5 8181 \u70ba\u9810\u8a2d\u901a\u8a0a\u57e0\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/hu.json b/homeassistant/components/tellduslive/translations/hu.json index a07259b67f9..2844415f630 100644 --- a/homeassistant/components/tellduslive/translations/hu.json +++ b/homeassistant/components/tellduslive/translations/hu.json @@ -11,7 +11,7 @@ }, "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\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})", + "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 **Mehet** gombra. \n\n [Link TelldusLive-fi\u00f3k]({auth_url})", "title": "Hiteles\u00edtsen a TelldusLive-on" }, "user": { diff --git a/homeassistant/components/tesla_wall_connector/translations/ca.json b/homeassistant/components/tesla_wall_connector/translations/ca.json index 7f0056f3413..ff92037845f 100644 --- a/homeassistant/components/tesla_wall_connector/translations/ca.json +++ b/homeassistant/components/tesla_wall_connector/translations/ca.json @@ -16,15 +16,5 @@ "title": "Configuraci\u00f3 de Tesla Wall Connector" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Freq\u00fc\u00e8ncia d'actualitzaci\u00f3" - }, - "title": "Configuraci\u00f3 d'opcions de Tesla Wall Connector" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/de.json b/homeassistant/components/tesla_wall_connector/translations/de.json index 3500fc1aa7b..3fac77da9b1 100644 --- a/homeassistant/components/tesla_wall_connector/translations/de.json +++ b/homeassistant/components/tesla_wall_connector/translations/de.json @@ -16,15 +16,5 @@ "title": "Tesla Wall Connector konfigurieren" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Aktualisierungsfrequenz" - }, - "title": "Optionen f\u00fcr Tesla Wall Connector konfigurieren" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/el.json b/homeassistant/components/tesla_wall_connector/translations/el.json index 3a835ab5d57..c007cd83f2a 100644 --- a/homeassistant/components/tesla_wall_connector/translations/el.json +++ b/homeassistant/components/tesla_wall_connector/translations/el.json @@ -16,15 +16,5 @@ "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Tesla Wall Connector" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "\u03a3\u03c5\u03c7\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2" - }, - "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c4\u03bf Tesla Wall Connector" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/en.json b/homeassistant/components/tesla_wall_connector/translations/en.json index 79a3005299f..436cc06ffb4 100644 --- a/homeassistant/components/tesla_wall_connector/translations/en.json +++ b/homeassistant/components/tesla_wall_connector/translations/en.json @@ -16,15 +16,5 @@ "title": "Configure Tesla Wall Connector" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Update frequency" - }, - "title": "Configure options for Tesla Wall Connector" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/es.json b/homeassistant/components/tesla_wall_connector/translations/es.json index 0e531ec1c01..34cdf425528 100644 --- a/homeassistant/components/tesla_wall_connector/translations/es.json +++ b/homeassistant/components/tesla_wall_connector/translations/es.json @@ -16,15 +16,5 @@ "title": "Configurar el conector de pared Tesla" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Frecuencia de actualizaci\u00f3n" - }, - "title": "Configurar las opciones del conector de pared Tesla" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/et.json b/homeassistant/components/tesla_wall_connector/translations/et.json index a13b447d542..877e80759e6 100644 --- a/homeassistant/components/tesla_wall_connector/translations/et.json +++ b/homeassistant/components/tesla_wall_connector/translations/et.json @@ -16,15 +16,5 @@ "title": "Tesla Wall Connector'i seadistamine" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "V\u00e4rskendussagedus" - }, - "title": "Tesla Wall Connector'i seadistamise valikud" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/fr.json b/homeassistant/components/tesla_wall_connector/translations/fr.json index 6cf4aacf7d3..679b5f4b489 100644 --- a/homeassistant/components/tesla_wall_connector/translations/fr.json +++ b/homeassistant/components/tesla_wall_connector/translations/fr.json @@ -16,15 +16,5 @@ "title": "Configurer le connecteur mural Tesla" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" - }, - "title": "Configurer les options pour le connecteur mural Tesla" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/hu.json b/homeassistant/components/tesla_wall_connector/translations/hu.json index 951d4de5a86..ef490721c43 100644 --- a/homeassistant/components/tesla_wall_connector/translations/hu.json +++ b/homeassistant/components/tesla_wall_connector/translations/hu.json @@ -16,15 +16,5 @@ "title": "Tesla Wall Connector konfigur\u00e1l\u00e1sa" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Friss\u00edt\u00e9si gyakoris\u00e1g" - }, - "title": "Tesla Wall Connector konfigur\u00e1l\u00e1sa" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/id.json b/homeassistant/components/tesla_wall_connector/translations/id.json index 6214e3d153f..3ff19213a6c 100644 --- a/homeassistant/components/tesla_wall_connector/translations/id.json +++ b/homeassistant/components/tesla_wall_connector/translations/id.json @@ -16,15 +16,5 @@ "title": "Konfigurasikan Konektor Dinding Tesla" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Frekuensi pembaruan" - }, - "title": "Konfigurasikan opsi untuk Konektor Dinding Tesla" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/it.json b/homeassistant/components/tesla_wall_connector/translations/it.json index 5f79a0ee9e0..c67ff4c8cd0 100644 --- a/homeassistant/components/tesla_wall_connector/translations/it.json +++ b/homeassistant/components/tesla_wall_connector/translations/it.json @@ -16,15 +16,5 @@ "title": "Configura Tesla Wall Connector" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Frequenza di aggiornamento" - }, - "title": "Configura le opzioni per Tesla Wall Connector" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/ja.json b/homeassistant/components/tesla_wall_connector/translations/ja.json index 5370b0b161a..2eb8c3e695b 100644 --- a/homeassistant/components/tesla_wall_connector/translations/ja.json +++ b/homeassistant/components/tesla_wall_connector/translations/ja.json @@ -16,15 +16,5 @@ "title": "Tesla Wall Connector\u306e\u8a2d\u5b9a" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "\u66f4\u65b0\u983b\u5ea6" - }, - "title": "Tesla Wall Connector\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/nl.json b/homeassistant/components/tesla_wall_connector/translations/nl.json index b6182cdcf5a..f766433231b 100644 --- a/homeassistant/components/tesla_wall_connector/translations/nl.json +++ b/homeassistant/components/tesla_wall_connector/translations/nl.json @@ -16,15 +16,5 @@ "title": "Configureer Tesla Wall Connector" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Update frequentie" - }, - "title": "Configureer opties voor Tesla Wall Connector" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/no.json b/homeassistant/components/tesla_wall_connector/translations/no.json index ed6b4c30cfd..7c5c10de679 100644 --- a/homeassistant/components/tesla_wall_connector/translations/no.json +++ b/homeassistant/components/tesla_wall_connector/translations/no.json @@ -16,15 +16,5 @@ "title": "Konfigurer Tesla Wall Connector" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Oppdateringsfrekvens" - }, - "title": "Konfigurer alternativer for Tesla Wall Connector" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/pl.json b/homeassistant/components/tesla_wall_connector/translations/pl.json index 6ee749485ee..59ff982a024 100644 --- a/homeassistant/components/tesla_wall_connector/translations/pl.json +++ b/homeassistant/components/tesla_wall_connector/translations/pl.json @@ -16,15 +16,5 @@ "title": "Konfiguracja z\u0142\u0105cza \u015bciennego Tesla" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji" - }, - "title": "Konfiguracja opcji dla z\u0142\u0105cza \u015bciennego Tesla" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/pt-BR.json b/homeassistant/components/tesla_wall_connector/translations/pt-BR.json index da2c9d50f34..8b38c7654e3 100644 --- a/homeassistant/components/tesla_wall_connector/translations/pt-BR.json +++ b/homeassistant/components/tesla_wall_connector/translations/pt-BR.json @@ -16,15 +16,5 @@ "title": "Configurar o conector de parede Tesla" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" - }, - "title": "Configurar op\u00e7\u00f5es para o Tesla Wall Connector" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/ru.json b/homeassistant/components/tesla_wall_connector/translations/ru.json index 0fc1ef88790..7af1aa27a6b 100644 --- a/homeassistant/components/tesla_wall_connector/translations/ru.json +++ b/homeassistant/components/tesla_wall_connector/translations/ru.json @@ -16,15 +16,5 @@ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Tesla Wall Connector" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" - }, - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Tesla Wall Connector" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/tr.json b/homeassistant/components/tesla_wall_connector/translations/tr.json index b409c4ddc33..89d926e7eab 100644 --- a/homeassistant/components/tesla_wall_connector/translations/tr.json +++ b/homeassistant/components/tesla_wall_connector/translations/tr.json @@ -16,15 +16,5 @@ "title": "Tesla Duvar Ba\u011flant\u0131s\u0131n\u0131 Yap\u0131land\u0131r\u0131n" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "G\u00fcncelleme s\u0131kl\u0131\u011f\u0131" - }, - "title": "Tesla Duvar Konekt\u00f6r\u00fc i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/zh-Hans.json b/homeassistant/components/tesla_wall_connector/translations/zh-Hans.json index 21ee02edbf0..22533320802 100644 --- a/homeassistant/components/tesla_wall_connector/translations/zh-Hans.json +++ b/homeassistant/components/tesla_wall_connector/translations/zh-Hans.json @@ -16,15 +16,5 @@ "title": "\u914d\u7f6e Tesla \u58c1\u6302\u5f0f\u5145\u7535\u8fde\u63a5\u5668" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "\u66f4\u65b0\u9891\u7387" - }, - "title": "Tesla \u58c1\u6302\u5f0f\u5145\u7535\u8fde\u63a5\u5668\u914d\u7f6e\u9009\u9879" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/tesla_wall_connector/translations/zh-Hant.json b/homeassistant/components/tesla_wall_connector/translations/zh-Hant.json index 69de7dd2eb3..7e1a967689c 100644 --- a/homeassistant/components/tesla_wall_connector/translations/zh-Hant.json +++ b/homeassistant/components/tesla_wall_connector/translations/zh-Hant.json @@ -16,15 +16,5 @@ "title": "\u8a2d\u5b9a\u7279\u65af\u62c9\u58c1\u639b\u5f0f\u5145\u96fb\u5ea7" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "\u66f4\u65b0\u983b\u7387" - }, - "title": "\u7279\u65af\u62c9\u58c1\u639b\u5f0f\u5145\u96fb\u5ea7\u8a2d\u5b9a\u9078\u9805" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/bg.json b/homeassistant/components/threshold/translations/bg.json new file mode 100644 index 00000000000..05bbb89437a --- /dev/null +++ b/homeassistant/components/threshold/translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "data": { + "hysteresis": "\u0425\u0438\u0441\u0442\u0435\u0440\u0435\u0437\u0438\u0441", + "name": "\u0418\u043c\u0435" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "hysteresis": "\u0425\u0438\u0441\u0442\u0435\u0440\u0435\u0437\u0438\u0441", + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/ca.json b/homeassistant/components/threshold/translations/ca.json new file mode 100644 index 00000000000..b33bd8a04ed --- /dev/null +++ b/homeassistant/components/threshold/translations/ca.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "need_lower_upper": "Els l\u00edmits inferior i superior no poden estar tots dos buits" + }, + "step": { + "user": { + "data": { + "entity_id": "Sensor d'entrada", + "hysteresis": "Hist\u00e8resi", + "lower": "L\u00edmit inferior", + "mode": "Mode llindar", + "name": "Nom", + "upper": "L\u00edmit superior" + }, + "description": "Crea un sensor binari que s'activa o es desactiva en funci\u00f3 del valor d'un sensor\n\nNom\u00e9s amb l\u00edmit inferior configurat - S'activa quan el valor del sensor d'entrada est\u00e0 per sota del l\u00edmit.\nNom\u00e9s amb l\u00edmit superior configurat - S'activa quan el valor del sensor d'entrada est\u00e0 per sobre del l\u00edmit.\nAmbd\u00f3s l\u00edmits configurats - S'activa quan el valor del sensor d'entrada est\u00e0 dins l'interval entre els l\u00edmits [inferior .. superior].", + "title": "Afegeix sensor de llindar" + } + } + }, + "options": { + "error": { + "need_lower_upper": "Els l\u00edmits inferior i superior no poden estar tots dos buits" + }, + "step": { + "init": { + "data": { + "entity_id": "Sensor d'entrada", + "hysteresis": "Hist\u00e8resi", + "lower": "L\u00edmit inferior", + "mode": "Mode llindar", + "name": "Nom", + "upper": "L\u00edmit superior" + }, + "description": "Nom\u00e9s amb l\u00edmit inferior configurat - S'activa quan el valor del sensor d'entrada est\u00e0 per sota del l\u00edmit.\nNom\u00e9s amb l\u00edmit superior configurat - S'activa quan el valor del sensor d'entrada est\u00e0 per sobre del l\u00edmit.\nAmbd\u00f3s l\u00edmits configurats - S'activa quan el valor del sensor d'entrada est\u00e0 dins l'interval entre els l\u00edmits [inferior .. superior]." + } + } + }, + "title": "Sensor de llindar" +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/de.json b/homeassistant/components/threshold/translations/de.json new file mode 100644 index 00000000000..579dd4f49b0 --- /dev/null +++ b/homeassistant/components/threshold/translations/de.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "need_lower_upper": "Untere und obere Grenze k\u00f6nnen nicht beide leer sein" + }, + "step": { + "user": { + "data": { + "entity_id": "Eingangssensor", + "hysteresis": "Hysterese", + "lower": "Untere Grenze", + "mode": "Schwellenwertmodus", + "name": "Name", + "upper": "Obergrenze" + }, + "description": "Erstellen eines bin\u00e4ren Sensors, der sich je nach Wert eines Sensors ein- und ausschaltet\n\nNur unterer Grenzwert konfiguriert - Einschalten, wenn der Wert des Eingangssensors kleiner als der untere Grenzwert ist.\nNur oberer Grenzwert konfiguriert - Einschalten, wenn der Wert des Eingangssensors gr\u00f6\u00dfer als der obere Grenzwert ist.\nSowohl untere als auch obere Grenze konfiguriert - Einschalten, wenn der Wert des Eingangssensors im Bereich [untere Grenze ... obere Grenze] liegt.", + "title": "Schwellenwertsensor hinzuf\u00fcgen" + } + } + }, + "options": { + "error": { + "need_lower_upper": "Untere und obere Grenze k\u00f6nnen nicht beide leer sein" + }, + "step": { + "init": { + "data": { + "entity_id": "Eingangssensor", + "hysteresis": "Hysterese", + "lower": "Untere Grenze", + "mode": "Schwellenwertmodus", + "name": "Name", + "upper": "Obergrenze" + }, + "description": "Nur unterer Grenzwert konfiguriert - Einschalten, wenn der Wert des Eingangssensors kleiner als der untere Grenzwert ist.\nNur oberer Grenzwert konfiguriert - Einschalten, wenn der Wert des Eingangssensors gr\u00f6\u00dfer als der obere Grenzwert ist.\nSowohl unterer als auch oberer Grenzwert konfiguriert - Einschalten, wenn der Wert des Eingangssensors im Bereich [unterer Grenzwert ... oberer Grenzwert] liegt." + } + } + }, + "title": "Schwellenwertsensor" +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/el.json b/homeassistant/components/threshold/translations/el.json new file mode 100644 index 00000000000..2ad5f25e5f8 --- /dev/null +++ b/homeassistant/components/threshold/translations/el.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "need_lower_upper": "\u03a4\u03b1 \u03ba\u03ac\u03c4\u03c9 \u03ba\u03b1\u03b9 \u03ac\u03bd\u03c9 \u03cc\u03c1\u03b9\u03b1 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03bd \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03b1\u03b9 \u03c4\u03b1 \u03b4\u03cd\u03bf \u03ba\u03b5\u03bd\u03ac" + }, + "step": { + "user": { + "data": { + "entity_id": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5", + "hysteresis": "\u03a5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7", + "lower": "\u039a\u03ac\u03c4\u03c9 \u03cc\u03c1\u03b9\u03bf", + "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03bf\u03c1\u03af\u03bf\u03c5", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "upper": "\u0386\u03bd\u03c9 \u03cc\u03c1\u03b9\u03bf" + }, + "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03c0\u03cc\u03c4\u03b5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03bf \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2. \n\n \u039c\u03cc\u03bd\u03bf \u03c4\u03bf \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af - \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03ba\u03c1\u03cc\u03c4\u03b5\u03c1\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf.\n \u039c\u03cc\u03bd\u03bf \u03c4\u03bf \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af - \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b5\u03b3\u03b1\u03bb\u03cd\u03c4\u03b5\u03c1\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf.\n \u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf\u03c5 \u03ba\u03b1\u03b9 \u03ac\u03bd\u03c9 \u03bf\u03c1\u03af\u03bf\u03c5 - \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c4\u03bf \u03b5\u03cd\u03c1\u03bf\u03c2 [\u03ba\u03ac\u03c4\u03c9 \u03cc\u03c1\u03b9\u03bf .. \u03ac\u03bd\u03c9 \u03cc\u03c1\u03b9\u03bf].", + "title": "\u039d\u03ad\u03bf\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03bf\u03c1\u03af\u03bf\u03c5" + } + } + }, + "options": { + "error": { + "need_lower_upper": "\u03a4\u03b1 \u03ba\u03ac\u03c4\u03c9 \u03ba\u03b1\u03b9 \u03ac\u03bd\u03c9 \u03cc\u03c1\u03b9\u03b1 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03bd \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03b1\u03b9 \u03c4\u03b1 \u03b4\u03cd\u03bf \u03ba\u03b5\u03bd\u03ac" + }, + "step": { + "init": { + "data": { + "entity_id": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5", + "hysteresis": "\u03a5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7", + "lower": "\u039a\u03ac\u03c4\u03c9 \u03cc\u03c1\u03b9\u03bf", + "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03bf\u03c1\u03af\u03bf\u03c5", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "upper": "\u0386\u03bd\u03c9 \u03cc\u03c1\u03b9\u03bf" + }, + "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03c0\u03cc\u03c4\u03b5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03bf \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2. \n\n \u039c\u03cc\u03bd\u03bf \u03c4\u03bf \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af - \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03ba\u03c1\u03cc\u03c4\u03b5\u03c1\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf.\n \u039c\u03cc\u03bd\u03bf \u03c4\u03bf \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af - \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b5\u03b3\u03b1\u03bb\u03cd\u03c4\u03b5\u03c1\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf.\n \u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf\u03c5 \u03ba\u03b1\u03b9 \u03ac\u03bd\u03c9 \u03bf\u03c1\u03af\u03bf\u03c5 - \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c4\u03bf \u03b5\u03cd\u03c1\u03bf\u03c2 [\u03ba\u03ac\u03c4\u03c9 \u03cc\u03c1\u03b9\u03bf .. \u03ac\u03bd\u03c9 \u03cc\u03c1\u03b9\u03bf]." + } + } + }, + "title": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03bf\u03c1\u03af\u03bf\u03c5" +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/en.json b/homeassistant/components/threshold/translations/en.json index 461ca244353..66a2bb33ddb 100644 --- a/homeassistant/components/threshold/translations/en.json +++ b/homeassistant/components/threshold/translations/en.json @@ -9,6 +9,7 @@ "entity_id": "Input sensor", "hysteresis": "Hysteresis", "lower": "Lower limit", + "mode": "Threshold mode", "name": "Name", "upper": "Upper limit" }, @@ -27,6 +28,7 @@ "entity_id": "Input sensor", "hysteresis": "Hysteresis", "lower": "Lower limit", + "mode": "Threshold mode", "name": "Name", "upper": "Upper limit" }, diff --git a/homeassistant/components/threshold/translations/et.json b/homeassistant/components/threshold/translations/et.json new file mode 100644 index 00000000000..7a41c0bfc3f --- /dev/null +++ b/homeassistant/components/threshold/translations/et.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "need_lower_upper": "Alam- ja \u00fclempiir ei saa korraga puududa" + }, + "step": { + "user": { + "data": { + "entity_id": "Sisendandur", + "hysteresis": "H\u00fcsterees", + "lower": "Alampiir", + "mode": "L\u00e4vendi kasutamine", + "name": "Nimi", + "upper": "\u00dclempiir" + }, + "description": "Loo olekuandur mis l\u00fcltub sisse v\u00f5i v\u00e4lja vastavalt m\u00e4\u00e4ratud ajale.\n\nAinult alampiir - l\u00fclitu sisse kui anduri v\u00e4\u00e4rtus on alla almpiiri.\nAinult \u00fclempiir - l\u00fclitu sisse kui anduri v\u00e4\u00e4rtus on \u00fcle \u00fclempiiri.\nAlam- ja \u00fclempiir m\u00f5lemad m\u00e4\u00e4ratud - l\u00fclitu sisse kui anduri v\u00e4\u00e4rtus on vahemikus [alampiir .. \u00fclempiir]", + "title": "Lisa uus l\u00e4vendiandur" + } + } + }, + "options": { + "error": { + "need_lower_upper": "Alam- ja \u00fclempiir ei saa korraga puududa" + }, + "step": { + "init": { + "data": { + "entity_id": "Sisendandur", + "hysteresis": "H\u00fcsterees", + "lower": "Alampiir", + "mode": "L\u00e4vendi kasutamine", + "name": "Nimi", + "upper": "\u00dclempiir" + }, + "description": "Ainult alampiir - l\u00fclitu sisse kui anduri v\u00e4\u00e4rtus on alla almpiiri.\nAinult \u00fclempiir - l\u00fclitu sisse kui anduri v\u00e4\u00e4rtus on \u00fcle \u00fclempiiri.\nAlam- ja \u00fclempiir m\u00f5lemad m\u00e4\u00e4ratud - l\u00fclitu sisse kui anduri v\u00e4\u00e4rtus on vahemikus [alampiir .. \u00fclempiir]" + } + } + }, + "title": "L\u00e4vendiandur" +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/fr.json b/homeassistant/components/threshold/translations/fr.json new file mode 100644 index 00000000000..29bc21e9cd5 --- /dev/null +++ b/homeassistant/components/threshold/translations/fr.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "need_lower_upper": "Les limites inf\u00e9rieure et sup\u00e9rieure ne peuvent pas \u00eatre toutes les deux vides" + }, + "step": { + "user": { + "data": { + "entity_id": "Capteur d'entr\u00e9e", + "hysteresis": "Hyst\u00e9r\u00e9sis", + "lower": "Limite inf\u00e9rieure", + "mode": "Mode de seuil", + "name": "Nom", + "upper": "Limite sup\u00e9rieure" + }, + "description": "Cr\u00e9ez un capteur binaire qui s'active et se d\u00e9sactive en fonction de la valeur d'un autre capteur.\n\nSi seule la limite inf\u00e9rieure est configur\u00e9e, le capteur est activ\u00e9 lorsque la valeur du capteur d'entr\u00e9e est inf\u00e9rieure \u00e0 cette limite.\nSi seule la limite sup\u00e9rieure est configur\u00e9e, le capteur est activ\u00e9 lorsque la valeur du capteur d'entr\u00e9e est sup\u00e9rieure \u00e0 cette limite.\nSi les deux limites sont configur\u00e9es, le capteur est activ\u00e9 lorsque la valeur du capteur d'entr\u00e9e est comprise entre les limites inf\u00e9rieure et sup\u00e9rieure.", + "title": "Ajouter un capteur de seuil" + } + } + }, + "options": { + "error": { + "need_lower_upper": "Les limites inf\u00e9rieure et sup\u00e9rieure ne peuvent pas \u00eatre toutes les deux vides" + }, + "step": { + "init": { + "data": { + "entity_id": "Capteur d'entr\u00e9e", + "hysteresis": "Hyst\u00e9r\u00e9sis", + "lower": "Limite inf\u00e9rieure", + "mode": "Mode de seuil", + "name": "Nom", + "upper": "Limite sup\u00e9rieure" + }, + "description": "Si seule la limite inf\u00e9rieure est configur\u00e9e, le capteur est activ\u00e9 lorsque la valeur du capteur d'entr\u00e9e est inf\u00e9rieure \u00e0 cette limite.\nSi seule la limite sup\u00e9rieure est configur\u00e9e, le capteur est activ\u00e9 lorsque la valeur du capteur d'entr\u00e9e est sup\u00e9rieure \u00e0 cette limite.\nSi les deux limites sont configur\u00e9es, le capteur est activ\u00e9 lorsque la valeur du capteur d'entr\u00e9e est comprise entre les limites inf\u00e9rieure et sup\u00e9rieure." + } + } + }, + "title": "Capteur de seuil" +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/hu.json b/homeassistant/components/threshold/translations/hu.json new file mode 100644 index 00000000000..944a13680d3 --- /dev/null +++ b/homeassistant/components/threshold/translations/hu.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "need_lower_upper": "Az als\u00f3 \u00e9s a fels\u0151 hat\u00e1r\u00e9rt\u00e9k nem lehet \u00fcres" + }, + "step": { + "user": { + "data": { + "entity_id": "Forr\u00e1s \u00e9rz\u00e9kel\u0151", + "hysteresis": "Hiszter\u00e9zis", + "lower": "Als\u00f3 hat\u00e1r", + "mode": "K\u00fcsz\u00f6b\u00e9rt\u00e9k m\u00f3d", + "name": "Elnevez\u00e9s", + "upper": "Fels\u0151 hat\u00e1r" + }, + "description": "Itt \u00e1ll\u00edthatja be, hogy az \u00e9rz\u00e9kel\u0151 mikor kapcsoljon be \u00e9s mikor kapcsoljon ki.\n\nCsak als\u00f3 hat\u00e1r\u00e9rt\u00e9k konfigur\u00e1lva - Bekapcsol\u00e1s, amikor a forr\u00e1s \u00e9rz\u00e9kel\u0151 \u00e9rt\u00e9ke kisebb, mint az als\u00f3 hat\u00e1r\u00e9rt\u00e9k.\nCsak fels\u0151 hat\u00e1r\u00e9rt\u00e9k konfigur\u00e1lva - Bekapcsol, ha a forr\u00e1s \u00e9rz\u00e9kel\u0151 \u00e9rt\u00e9ke nagyobb, mint a fels\u0151 hat\u00e1r\u00e9rt\u00e9k.\nAls\u00f3 \u00e9s fels\u0151 hat\u00e1r\u00e9rt\u00e9k konfigur\u00e1lva - Bekapcsol\u00e1s, ha a forr\u00e1s \u00e9rz\u00e9kel\u0151 \u00e9rt\u00e9ke az als\u00f3 hat\u00e1r\u00e9rt\u00e9k \u00e9s fels\u0151 hat\u00e1r\u00e9rt\u00e9k k\u00f6z\u00f6tti tartom\u00e1nyban van.", + "title": "\u00daj k\u00fcsz\u00f6b\u00e9rt\u00e9k-\u00e9rz\u00e9kel\u0151" + } + } + }, + "options": { + "error": { + "need_lower_upper": "Az als\u00f3 \u00e9s a fels\u0151 hat\u00e1r\u00e9rt\u00e9k nem lehet \u00fcres" + }, + "step": { + "init": { + "data": { + "entity_id": "Forr\u00e1s \u00e9rz\u00e9kel\u0151", + "hysteresis": "Hiszter\u00e9zis", + "lower": "Als\u00f3 hat\u00e1r", + "mode": "K\u00fcsz\u00f6b\u00e9rt\u00e9k m\u00f3d", + "name": "Elnevez\u00e9s", + "upper": "Fels\u0151 hat\u00e1r" + }, + "description": "Itt \u00e1ll\u00edthatja be, hogy az \u00e9rz\u00e9kel\u0151 mikor kapcsoljon be \u00e9s mikor kapcsoljon ki.\n\nCsak als\u00f3 hat\u00e1r\u00e9rt\u00e9k konfigur\u00e1lva - Bekapcsol\u00e1s, amikor a forr\u00e1s \u00e9rz\u00e9kel\u0151 \u00e9rt\u00e9ke kisebb, mint az als\u00f3 hat\u00e1r\u00e9rt\u00e9k.\nCsak fels\u0151 hat\u00e1r\u00e9rt\u00e9k konfigur\u00e1lva - Bekapcsol, ha a forr\u00e1s \u00e9rz\u00e9kel\u0151 \u00e9rt\u00e9ke nagyobb, mint a fels\u0151 hat\u00e1r\u00e9rt\u00e9k.\nAls\u00f3 \u00e9s fels\u0151 hat\u00e1r\u00e9rt\u00e9k konfigur\u00e1lva - Bekapcsol\u00e1s, ha a forr\u00e1s \u00e9rz\u00e9kel\u0151 \u00e9rt\u00e9ke az als\u00f3 hat\u00e1r\u00e9rt\u00e9k \u00e9s fels\u0151 hat\u00e1r\u00e9rt\u00e9k k\u00f6z\u00f6tti tartom\u00e1nyban van." + } + } + }, + "title": "K\u00fcsz\u00f6b\u00e9rt\u00e9k \u00e9rz\u00e9kel\u0151" +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/id.json b/homeassistant/components/threshold/translations/id.json new file mode 100644 index 00000000000..7356dd772e1 --- /dev/null +++ b/homeassistant/components/threshold/translations/id.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "need_lower_upper": "Batas bawah dan batas atas tidak boleh kosong sekaligus" + }, + "step": { + "user": { + "data": { + "entity_id": "Sensor input", + "hysteresis": "Histeresis", + "lower": "Batas bawah", + "mode": "Mode ambang batas", + "name": "Nama", + "upper": "Batas atas" + }, + "description": "Buat sensor biner yang nyala dan mati tergantung pada nilai sensor \n\nHanya batas bawah yang dikonfigurasi: Nyalakan saat nilai sensor input kurang dari batas bawah.\nHanya batas atas yang dikonfigurasi: Nyalakan saat nilai sensor input lebih besar dari batas atas.\nKedua batas dikonfigurasi: Nyalakan ketika nilai sensor input berada dalam rentang [batas bawah ... batas atas].", + "title": "Tambahkan Sensor Ambang Batas" + } + } + }, + "options": { + "error": { + "need_lower_upper": "Batas bawah dan batas atas tidak boleh kosong sekaligus" + }, + "step": { + "init": { + "data": { + "entity_id": "Sensor input", + "hysteresis": "Histeresis", + "lower": "Batas bawah", + "mode": "Mode ambang batas", + "name": "Nama", + "upper": "Batas atas" + }, + "description": "Hanya batas bawah yang dikonfigurasi: Nyalakan saat nilai sensor input kurang dari batas bawah.\nHanya batas atas yang dikonfigurasi: Nyalakan saat nilai sensor input lebih besar dari batas atas.\nKedua batas dikonfigurasi: Nyalakan ketika nilai sensor input berada dalam rentang [batas bawah ... batas atas]." + } + } + }, + "title": "Sensor Ambang Batas" +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/it.json b/homeassistant/components/threshold/translations/it.json new file mode 100644 index 00000000000..6bc62dce765 --- /dev/null +++ b/homeassistant/components/threshold/translations/it.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "need_lower_upper": "I limiti inferiore e superiore non possono essere entrambi vuoti" + }, + "step": { + "user": { + "data": { + "entity_id": "Sensore di ingresso", + "hysteresis": "Isteresi", + "lower": "Limite inferiore", + "mode": "Modalit\u00e0 soglia", + "name": "Nome", + "upper": "Limite superiore" + }, + "description": "Crea un sensore binario che si accende e si spegne a seconda del valore di un sensore \n\nSolo limite inferiore configurato - si accende quando il valore del sensore di ingresso \u00e8 inferiore al limite inferiore.\nSolo limite superiore configurato - si accende quando il valore del sensore di ingresso \u00e8 maggiore del limite superiore.\nSia il limite inferiore che quello superiore configurati - si accende quando il valore del sensore di ingresso \u00e8 compreso nell'intervallo [limite inferiore .. limite superiore].", + "title": "Aggiungi sensore di soglia" + } + } + }, + "options": { + "error": { + "need_lower_upper": "I limiti inferiore e superiore non possono essere entrambi vuoti" + }, + "step": { + "init": { + "data": { + "entity_id": "Sensore di ingresso", + "hysteresis": "Isteresi", + "lower": "Limite inferiore", + "mode": "Modalit\u00e0 soglia", + "name": "Nome", + "upper": "Limite superiore" + }, + "description": "Solo limite inferiore configurato - si accende quando il valore del sensore di ingresso \u00e8 inferiore al limite inferiore.\nSolo limite superiore configurato - si accende quando il valore del sensore di ingresso \u00e8 maggiore del limite superiore.\nSia il limite inferiore che quello superiore configurati - si accende quando il valore del sensore di ingresso \u00e8 compreso nell'intervallo [limite inferiore .. limite superiore]." + } + } + }, + "title": "Sensore di soglia" +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/ja.json b/homeassistant/components/threshold/translations/ja.json new file mode 100644 index 00000000000..7d5a6cc2ce7 --- /dev/null +++ b/homeassistant/components/threshold/translations/ja.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "need_lower_upper": "\u4e0b\u9650\u3068\u4e0a\u9650\u306e\u4e21\u65b9\u3092\u7a7a\u306b\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093" + }, + "step": { + "user": { + "data": { + "entity_id": "\u5165\u529b\u30bb\u30f3\u30b5\u30fc", + "hysteresis": "\u30d2\u30b9\u30c6\u30ea\u30b7\u30b9", + "lower": "\u4e0b\u9650\u5024", + "mode": "\u3057\u304d\u3044\u5024\u30e2\u30fc\u30c9", + "name": "\u540d\u524d", + "upper": "\u4e0a\u9650\u5024" + }, + "description": "\u30bb\u30f3\u30b5\u30fc\u306e\u30aa\u30f3\u3068\u30aa\u30d5\u3092\u5207\u308a\u66ff\u3048\u308b\u30bf\u30a4\u30df\u30f3\u30b0\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\n\n\u4e0b\u9650\u306e\u307f\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c\u4e0b\u9650\u5024\u3088\u308a\u5c0f\u3055\u3044\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002\n\u4e0a\u9650\u306e\u307f\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c\u4e0a\u9650\u5024\u3088\u308a\u5927\u304d\u3044\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002\n\u4e0b\u9650\u3068\u4e0a\u9650\u306e\u4e21\u65b9\u3092\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c[\u4e0b\u9650..\u4e0a\u9650]\u306e\u7bc4\u56f2\u306b\u3042\u308b\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002", + "title": "\u65b0\u3057\u3044\u3057\u304d\u3044\u5024\u30bb\u30f3\u30b5\u30fc" + } + } + }, + "options": { + "error": { + "need_lower_upper": "\u4e0b\u9650\u3068\u4e0a\u9650\u306e\u4e21\u65b9\u3092\u7a7a\u306b\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093" + }, + "step": { + "init": { + "data": { + "entity_id": "\u5165\u529b\u30bb\u30f3\u30b5\u30fc", + "hysteresis": "\u30d2\u30b9\u30c6\u30ea\u30b7\u30b9", + "lower": "\u4e0b\u9650\u5024", + "mode": "\u3057\u304d\u3044\u5024\u30e2\u30fc\u30c9", + "name": "\u540d\u524d", + "upper": "\u4e0a\u9650\u5024" + }, + "description": "\u30bb\u30f3\u30b5\u30fc\u306e\u30aa\u30f3\u3068\u30aa\u30d5\u3092\u5207\u308a\u66ff\u3048\u308b\u30bf\u30a4\u30df\u30f3\u30b0\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\n\n\u4e0b\u9650\u306e\u307f\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c\u4e0b\u9650\u5024\u3088\u308a\u5c0f\u3055\u3044\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002\n\u4e0a\u9650\u306e\u307f\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c\u4e0a\u9650\u5024\u3088\u308a\u5927\u304d\u3044\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002\n\u4e0b\u9650\u3068\u4e0a\u9650\u306e\u4e21\u65b9\u3092\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c[\u4e0b\u9650..\u4e0a\u9650]\u306e\u7bc4\u56f2\u306b\u3042\u308b\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002" + } + } + }, + "title": "\u3057\u304d\u3044\u5024\u30bb\u30f3\u30b5\u30fc" +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/nl.json b/homeassistant/components/threshold/translations/nl.json new file mode 100644 index 00000000000..73af8fcc8b7 --- /dev/null +++ b/homeassistant/components/threshold/translations/nl.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "need_lower_upper": "Onder- en bovengrens kunnen niet beide leeg zijn" + }, + "step": { + "user": { + "data": { + "entity_id": "Invoer sensor", + "hysteresis": "Hyseterise", + "lower": "Ondergrens", + "mode": "Drempelmodus", + "name": "Naam", + "upper": "Bovengrens" + }, + "description": "Maak een binaire sensor die aan en uit gaat afhankelijk van de waarde van een sensor\n\nAlleen ondergrens geconfigureerd - Schakelt in wanneer de waarde van de ingangssensor kleiner is dan de ondergrens.\nAlleen bovengrens geconfigureerd - Schakelt in wanneer de waarde van de ingangssensor groter is dan de bovengrens.\nZowel ondergrens als bovengrens ingesteld - Schakelt in wanneer de waarde van de ingangssensor binnen het bereik [ondergrens ... bovengrens] ligt.", + "title": "Drempelsensor toevoegen" + } + } + }, + "options": { + "error": { + "need_lower_upper": "Onder- en bovengrens kunnen niet beide leeg zijn" + }, + "step": { + "init": { + "data": { + "entity_id": "Invoer sensor", + "hysteresis": "Hyseterise", + "lower": "Ondergrens", + "mode": "Drempelmodus", + "name": "Naam", + "upper": "Bovengrens" + }, + "description": "Alleen ondergrens geconfigureerd - Schakelt in wanneer de waarde van de ingangssensor lager is dan de ondergrens.\nAlleen bovengrens geconfigureerd - Schakelt in wanneer de waarde van de ingangssensor groter is dan de bovengrens.\nZowel ondergrens als bovengrens ingesteld - Schakelt in wanneer de waarde van de ingangssensor binnen het bereik [ondergrens ... bovengrens] ligt." + } + } + }, + "title": "Drempelsensor" +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/no.json b/homeassistant/components/threshold/translations/no.json new file mode 100644 index 00000000000..66c8461d8cc --- /dev/null +++ b/homeassistant/components/threshold/translations/no.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "need_lower_upper": "Nedre og \u00f8vre grense kan ikke begge v\u00e6re tomme" + }, + "step": { + "user": { + "data": { + "entity_id": "Inngangssensor", + "hysteresis": "Hysterese", + "lower": "Nedre grense", + "mode": "Terskelverdi-modus", + "name": "Navn", + "upper": "\u00d8vre grense" + }, + "description": "Lag en bin\u00e6r sensor som sl\u00e5s av og p\u00e5 avhengig av verdien til en sensor \n\n Kun nedre grense konfigurert - Sl\u00e5 p\u00e5 n\u00e5r inngangssensorens verdi er mindre enn den nedre grensen.\n Bare \u00f8vre grense konfigurert - Sl\u00e5 p\u00e5 n\u00e5r inngangssensorens verdi er st\u00f8rre enn den \u00f8vre grensen.\n B\u00e5de nedre og \u00f8vre grense konfigurert - Sl\u00e5 p\u00e5 n\u00e5r inngangssensorens verdi er innenfor omr\u00e5det [nedre grense .. \u00f8vre grense].", + "title": "Legg til terskelsensor" + } + } + }, + "options": { + "error": { + "need_lower_upper": "Nedre og \u00f8vre grense kan ikke begge v\u00e6re tomme" + }, + "step": { + "init": { + "data": { + "entity_id": "Inngangssensor", + "hysteresis": "Hysterese", + "lower": "Nedre grense", + "mode": "Terskelverdi-modus", + "name": "Navn", + "upper": "\u00d8vre grense" + }, + "description": "Kun nedre grense konfigurert - Sl\u00e5 p\u00e5 n\u00e5r inngangssensorens verdi er mindre enn den nedre grensen.\n Bare \u00f8vre grense konfigurert - Sl\u00e5 p\u00e5 n\u00e5r inngangssensorens verdi er st\u00f8rre enn den \u00f8vre grensen.\n B\u00e5de nedre og \u00f8vre grense konfigurert - Sl\u00e5 p\u00e5 n\u00e5r inngangssensorens verdi er innenfor omr\u00e5det [nedre grense .. \u00f8vre grense]." + } + } + }, + "title": "Terskelsensor" +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/pl.json b/homeassistant/components/threshold/translations/pl.json new file mode 100644 index 00000000000..5db231947c2 --- /dev/null +++ b/homeassistant/components/threshold/translations/pl.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "need_lower_upper": "Dolny i g\u00f3rny limit nie mog\u0105 by\u0107 jednocze\u015bnie puste" + }, + "step": { + "user": { + "data": { + "entity_id": "Sensor wej\u015bciowy", + "hysteresis": "Op\u00f3\u017anienie", + "lower": "Dolny limit", + "mode": "Tryb progowy", + "name": "Nazwa", + "upper": "G\u00f3rny limit" + }, + "description": "Utw\u00f3rz sensor binarny, kt\u00f3ry w\u0142\u0105cza si\u0119 lub wy\u0142\u0105cza w zale\u017cno\u015bci od warto\u015bci sensora.\n\nSkonfigurowano tylko dolny limit - W\u0142\u0105cza si\u0119, gdy warto\u015b\u0107 sensora wej\u015bciowego jest mniejsza ni\u017c dolny limit.\nSkonfigurowano tylko g\u00f3rny limit - W\u0142\u0105cza si\u0119, gdy warto\u015b\u0107 sensora wej\u015bciowego jest wi\u0119ksza ni\u017c g\u00f3rny limit.\nSkonfigurowano dolny i g\u00f3rny limit - W\u0142\u0105cza si\u0119, gdy warto\u015b\u0107 czujnika wej\u015bciowego znajduje si\u0119 w zakresie [dolny limit .. g\u00f3rny limit].", + "title": "Dodaj sensor progowy" + } + } + }, + "options": { + "error": { + "need_lower_upper": "Dolny i g\u00f3rny limit nie mog\u0105 by\u0107 jednocze\u015bnie puste" + }, + "step": { + "init": { + "data": { + "entity_id": "Sensor wej\u015bciowy", + "hysteresis": "Op\u00f3\u017anienie", + "lower": "Dolny limit", + "mode": "Tryb progowy", + "name": "Nazwa", + "upper": "G\u00f3rny limit" + }, + "description": "Skonfigurowano tylko dolny limit - W\u0142\u0105cza si\u0119, gdy warto\u015b\u0107 sensora wej\u015bciowego jest mniejsza ni\u017c dolny limit.\nSkonfigurowano tylko g\u00f3rny limit - W\u0142\u0105cza, gdy warto\u015b\u0107 sensora wej\u015bciowego jest wi\u0119ksza ni\u017c g\u00f3rny limit.\nSkonfigurowano zar\u00f3wno dolny, jak i g\u00f3rny limit - W\u0142\u0105cza, gdy warto\u015b\u0107 sensora wej\u015bciowego znajduje si\u0119 w zakresie [dolny limit .. g\u00f3rny limit]." + } + } + }, + "title": "Sensor progowy" +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/pt-BR.json b/homeassistant/components/threshold/translations/pt-BR.json new file mode 100644 index 00000000000..1aa7358086a --- /dev/null +++ b/homeassistant/components/threshold/translations/pt-BR.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "need_lower_upper": "Os limites inferior e superior n\u00e3o podem estar vazios" + }, + "step": { + "user": { + "data": { + "entity_id": "Sensor de entrada", + "hysteresis": "Histerese", + "lower": "Limite inferior", + "mode": "Modo Threshold", + "name": "Nome", + "upper": "Limite superior" + }, + "description": "Crie um sensor bin\u00e1rio que liga e desliga dependendo do valor de um sensor \n\n Somente limite inferior configurado - Liga quando o valor do sensor de entrada for menor que o limite inferior.\n Somente limite superior configurado - Liga quando o valor do sensor de entrada for maior que o limite superior.\n Ambos os limites inferior e superior configurados - Liga quando o valor do sensor de entrada est\u00e1 na faixa [limite inferior .. limite superior].", + "title": "Adicionar sensor Threshold" + } + } + }, + "options": { + "error": { + "need_lower_upper": "Os limites inferior e superior n\u00e3o podem estar vazios" + }, + "step": { + "init": { + "data": { + "entity_id": "Sensor de entrada", + "hysteresis": "Histerese", + "lower": "Limite inferior", + "mode": "Modo Threshold", + "name": "Nome", + "upper": "Limite superior" + }, + "description": "Somente o limite inferior \u00e9 configurado - Liga quando o valor do sensor de entrada for menor que o limite inferior.\nSomente o limite superior \u00e9 configurado - Liga quando o valor do sensor de entrada for maior que o limite superior.\nAmbos os limites inferior e superior s\u00e3o configurados - Liga quando o valor do sensor de entrada est\u00e1 na faixa [limite inferior .. limite superior]." + } + } + }, + "title": "Sensor Threshold" +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/ru.json b/homeassistant/components/threshold/translations/ru.json new file mode 100644 index 00000000000..5b8c4546823 --- /dev/null +++ b/homeassistant/components/threshold/translations/ru.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "need_lower_upper": "\u041d\u0438\u0436\u043d\u0438\u0439 \u0438 \u0432\u0435\u0440\u0445\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b\u044b \u043d\u0435 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u043f\u0443\u0441\u0442\u044b\u043c\u0438." + }, + "step": { + "user": { + "data": { + "entity_id": "\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440", + "hysteresis": "\u0413\u0438\u0441\u0442\u0435\u0440\u0435\u0437\u0438\u0441", + "lower": "\u041d\u0438\u0436\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b", + "mode": "\u041f\u043e\u0440\u043e\u0433\u043e\u0432\u044b\u0439 \u0440\u0435\u0436\u0438\u043c", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "upper": "\u0412\u0435\u0440\u0445\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b" + }, + "description": "\u0411\u0438\u043d\u0430\u0440\u043d\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430. \n\n\u041d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u0442\u043e\u043b\u044c\u043a\u043e \u043d\u0438\u0436\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b \u2014 \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f, \u043a\u043e\u0433\u0434\u0430 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u043c\u0435\u043d\u044c\u0448\u0435 \u043d\u0438\u0436\u043d\u0435\u0433\u043e \u043f\u0440\u0435\u0434\u0435\u043b\u0430.\n\u041d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u0435\u0440\u0445\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b \u2014 \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f, \u043a\u043e\u0433\u0434\u0430 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0431\u043e\u043b\u044c\u0448\u0435 \u0432\u0435\u0440\u0445\u043d\u0435\u0433\u043e \u043f\u0440\u0435\u0434\u0435\u043b\u0430.\n\u041d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b \u043a\u0430\u043a \u043d\u0438\u0436\u043d\u0438\u0439, \u0442\u0430\u043a \u0438 \u0432\u0435\u0440\u0445\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b \u2014 \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f, \u043a\u043e\u0433\u0434\u0430 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u0435 [\u043d\u0438\u0436\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b \u2026 \u0432\u0435\u0440\u0445\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b].", + "title": "\u041f\u043e\u0440\u043e\u0433\u043e\u0432\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435" + } + } + }, + "options": { + "error": { + "need_lower_upper": "\u041d\u0438\u0436\u043d\u0438\u0439 \u0438 \u0432\u0435\u0440\u0445\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b\u044b \u043d\u0435 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u043f\u0443\u0441\u0442\u044b\u043c\u0438." + }, + "step": { + "init": { + "data": { + "entity_id": "\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440", + "hysteresis": "\u0413\u0438\u0441\u0442\u0435\u0440\u0435\u0437\u0438\u0441", + "lower": "\u041d\u0438\u0436\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b", + "mode": "\u041f\u043e\u0440\u043e\u0433\u043e\u0432\u044b\u0439 \u0440\u0435\u0436\u0438\u043c", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "upper": "\u0412\u0435\u0440\u0445\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u0442\u043e\u043b\u044c\u043a\u043e \u043d\u0438\u0436\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b \u2014 \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f, \u043a\u043e\u0433\u0434\u0430 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u043c\u0435\u043d\u044c\u0448\u0435 \u043d\u0438\u0436\u043d\u0435\u0433\u043e \u043f\u0440\u0435\u0434\u0435\u043b\u0430.\n\u041d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u0435\u0440\u0445\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b \u2014 \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f, \u043a\u043e\u0433\u0434\u0430 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0431\u043e\u043b\u044c\u0448\u0435 \u0432\u0435\u0440\u0445\u043d\u0435\u0433\u043e \u043f\u0440\u0435\u0434\u0435\u043b\u0430.\n\u041d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b \u0438 \u043d\u0438\u0436\u043d\u0438\u0439 \u0438 \u0432\u0435\u0440\u0445\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b\u044b \u2014 \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f, \u043a\u043e\u0433\u0434\u0430 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u0435 [\u043d\u0438\u0436\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b \u2026 \u0432\u0435\u0440\u0445\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b]." + } + } + }, + "title": "\u041f\u043e\u0440\u043e\u0433\u043e\u0432\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435" +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/tr.json b/homeassistant/components/threshold/translations/tr.json new file mode 100644 index 00000000000..0cb338ef681 --- /dev/null +++ b/homeassistant/components/threshold/translations/tr.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "need_lower_upper": "Alt ve \u00fcst limitlerin ikisi de bo\u015f olamaz" + }, + "step": { + "user": { + "data": { + "entity_id": "Giri\u015f sens\u00f6r\u00fc", + "hysteresis": "Histeresis", + "lower": "Alt s\u0131n\u0131r", + "mode": "E\u015fik modu", + "name": "Ad", + "upper": "\u00dcst s\u0131n\u0131r" + }, + "description": "Bir sens\u00f6r\u00fcn de\u011ferine ba\u011fl\u0131 olarak a\u00e7\u0131l\u0131p kapanan bir ikili sens\u00f6r olu\u015fturun \n\n Yaln\u0131zca alt limit konfig\u00fcre edildi - Giri\u015f sens\u00f6r\u00fcn\u00fcn de\u011feri alt limitten d\u00fc\u015f\u00fck oldu\u011funda a\u00e7\u0131n.\n Yaln\u0131zca \u00fcst limit konfig\u00fcre edildi - Giri\u015f sens\u00f6r\u00fcn\u00fcn de\u011feri \u00fcst limitten b\u00fcy\u00fck oldu\u011funda a\u00e7\u0131n.\n Hem alt hem de \u00fcst limit konfig\u00fcre edildi - Giri\u015f sens\u00f6r\u00fcn\u00fcn de\u011feri [alt limit .. \u00fcst limit] aral\u0131\u011f\u0131nda oldu\u011funda a\u00e7\u0131n.", + "title": "E\u015fik Sens\u00f6r\u00fc Ekle" + } + } + }, + "options": { + "error": { + "need_lower_upper": "Alt ve \u00fcst limitlerin ikisi de bo\u015f olamaz" + }, + "step": { + "init": { + "data": { + "entity_id": "Giri\u015f sens\u00f6r\u00fc", + "hysteresis": "Histeresis", + "lower": "Alt s\u0131n\u0131r", + "mode": "E\u015fik modu", + "name": "Ad", + "upper": "\u00dcst s\u0131n\u0131r" + }, + "description": "Yaln\u0131zca alt limit konfig\u00fcre edildi - Giri\u015f sens\u00f6r\u00fcn\u00fcn de\u011feri alt limitten d\u00fc\u015f\u00fck oldu\u011funda a\u00e7\u0131n.\n Yaln\u0131zca \u00fcst limit konfig\u00fcre edildi - Giri\u015f sens\u00f6r\u00fcn\u00fcn de\u011feri \u00fcst limitten b\u00fcy\u00fck oldu\u011funda a\u00e7\u0131n.\n Hem alt hem de \u00fcst limit konfig\u00fcre edildi - Giri\u015f sens\u00f6r\u00fcn\u00fcn de\u011feri [alt limit .. \u00fcst limit] aral\u0131\u011f\u0131nda oldu\u011funda a\u00e7\u0131n." + } + } + }, + "title": "E\u015fik Sens\u00f6r\u00fc" +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/zh-Hans.json b/homeassistant/components/threshold/translations/zh-Hans.json new file mode 100644 index 00000000000..35bc919f0d1 --- /dev/null +++ b/homeassistant/components/threshold/translations/zh-Hans.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "need_lower_upper": "\u4e0b\u9650\u548c\u4e0a\u9650\u4e0d\u80fd\u540c\u65f6\u4e3a\u7a7a" + }, + "step": { + "user": { + "data": { + "entity_id": "\u8f93\u5165\u4f20\u611f\u5668", + "hysteresis": "\u9632\u6296\u8303\u56f4", + "lower": "\u4e0b\u9650", + "mode": "\u9608\u503c\u6a21\u5f0f", + "name": "\u540d\u79f0", + "upper": "\u4e0a\u9650" + }, + "description": "\u521b\u5efa\u6839\u636e\u5176\u4ed6\u4f20\u611f\u5668\u7684\u503c\u6539\u53d8\u5f00\u5173\u72b6\u6001\u7684\u4e8c\u5143\u4f20\u611f\u5668\u3002\n\n\u5982\u679c\u53ea\u914d\u7f6e\u4e0b\u9650\uff0c\u5219\u5f53\u8f93\u5165\u4f20\u611f\u5668\u7684\u503c\u4f4e\u4e8e\u4e0b\u9650\u65f6\u4e3a\u5f00\u3002\n\u5982\u679c\u53ea\u914d\u7f6e\u4e0a\u9650\uff0c\u5219\u5f53\u8f93\u5165\u4f20\u611f\u5668\u7684\u503c\u9ad8\u4e8e\u4e0a\u7ebf\u65f6\u4e3a\u5f00\u3002\n\u5982\u679c\u540c\u65f6\u914d\u7f6e\u4e0a\u9650\u548c\u4e0b\u9650\uff0c\u5219\u5f53\u8f93\u5165\u4f20\u611f\u5668\u7684\u503c\u4ecb\u4e8e\u4e0a\u9650\u548c\u4e0b\u9650\u4e4b\u95f4\uff08\u542b\u4e0a\u9650\u548c\u4e0b\u9650\uff09\u65f6\u4e3a\u5f00\u3002", + "title": "\u6dfb\u52a0\u9608\u503c\u4f20\u611f\u5668" + } + } + }, + "options": { + "error": { + "need_lower_upper": "\u4e0b\u9650\u548c\u4e0a\u9650\u4e0d\u80fd\u540c\u65f6\u4e3a\u7a7a" + }, + "step": { + "init": { + "data": { + "entity_id": "\u8f93\u5165\u4f20\u611f\u5668", + "hysteresis": "\u9632\u6296\u8303\u56f4", + "lower": "\u4e0b\u9650", + "mode": "\u9608\u503c\u6a21\u5f0f", + "name": "\u540d\u79f0", + "upper": "\u4e0a\u9650" + }, + "description": "\u5982\u679c\u53ea\u914d\u7f6e\u4e0b\u9650\uff0c\u5219\u5f53\u8f93\u5165\u4f20\u611f\u5668\u7684\u503c\u4f4e\u4e8e\u4e0b\u9650\u65f6\u4e3a\u5f00\u3002\n\u5982\u679c\u53ea\u914d\u7f6e\u4e0a\u9650\uff0c\u5219\u5f53\u8f93\u5165\u4f20\u611f\u5668\u7684\u503c\u9ad8\u4e8e\u4e0a\u9650\u65f6\u4e3a\u5f00\u3002\n\u5982\u679c\u540c\u65f6\u914d\u7f6e\u4e0a\u9650\u548c\u4e0b\u9650\uff0c\u5219\u5f53\u8f93\u5165\u4f20\u611f\u5668\u7684\u503c\u4ecb\u4e8e\u4e0a\u9650\u548c\u4e0b\u9650\u4e4b\u95f4\uff08\u542b\u4e0a\u9650\u548c\u4e0b\u9650\uff09\u65f6\u4e3a\u5f00\u3002" + } + } + }, + "title": "\u9608\u503c\u4f20\u611f\u5668" +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/zh-Hant.json b/homeassistant/components/threshold/translations/zh-Hant.json new file mode 100644 index 00000000000..66bdfe6915a --- /dev/null +++ b/homeassistant/components/threshold/translations/zh-Hant.json @@ -0,0 +1,40 @@ +{ + "config": { + "error": { + "need_lower_upper": "\u4e0a\u4e0b\u9650\u4e0d\u53ef\u540c\u6642\u70ba\u7a7a\u767d" + }, + "step": { + "user": { + "data": { + "entity_id": "\u8f38\u5165\u611f\u6e2c\u5668", + "hysteresis": "\u9072\u6eef", + "lower": "\u4e0b\u9650", + "mode": "\u81e8\u754c\u9ede\u5f0f", + "name": "\u540d\u7a31", + "upper": "\u4e0a\u9650" + }, + "description": "\u65b0\u589e\u6839\u64da\u6578\u503c\u6c7a\u5b9a\u958b\u95dc\u4e4b\u4e8c\u9032\u4f4d\u611f\u6e2c\u5668\u3002\n\n\u50c5\u8a2d\u5b9a\u4e0b\u9650 - \u7576\u8f38\u5165\u611f\u6e2c\u5668\u6578\u503c\u4f4e\u65bc\u4e0b\u9650\u6642\u3001\u958b\u555f\u3002\n\u50c5\u8a2d\u5b9a\u4e0a\u9650 - \u7576\u8f38\u5165\u611f\u6e2c\u5668\u6578\u503c\u9ad8\u65bc\u4e0a\u9650\u6642\u3001\u958b\u555f\u3002\n\u540c\u6642\u8a2d\u5b9a\u4e0a\u4e0b\u9650 - \u7576\u8f38\u5165\u611f\u6e2c\u5668\u6578\u503c\u65bc\u7bc4\u570d\u5167 [\u4e0b\u9650 .. \u4e0a\u9650] \u6642\u958b\u555f\u3002", + "title": "\u65b0\u589e\u81e8\u754c\u611f\u6e2c\u5668" + } + } + }, + "options": { + "error": { + "need_lower_upper": "\u4e0a\u4e0b\u9650\u4e0d\u53ef\u540c\u6642\u70ba\u7a7a\u767d" + }, + "step": { + "init": { + "data": { + "entity_id": "\u8f38\u5165\u611f\u6e2c\u5668", + "hysteresis": "\u9072\u6eef", + "lower": "\u4e0b\u9650", + "mode": "\u81e8\u754c\u9ede\u5f0f", + "name": "\u540d\u7a31", + "upper": "\u4e0a\u9650" + }, + "description": "\u50c5\u8a2d\u5b9a\u4e0b\u9650 - \u7576\u8f38\u5165\u611f\u6e2c\u5668\u6578\u503c\u4f4e\u65bc\u4e0b\u9650\u6642\u3001\u958b\u555f\u3002\n\u50c5\u8a2d\u5b9a\u4e0a\u9650 - \u7576\u8f38\u5165\u611f\u6e2c\u5668\u6578\u503c\u9ad8\u65bc\u4e0a\u9650\u6642\u3001\u958b\u555f\u3002\n\u540c\u6642\u8a2d\u5b9a\u4e0a\u4e0b\u9650 - \u7576\u8f38\u5165\u611f\u6e2c\u5668\u6578\u503c\u65bc\u7bc4\u570d\u5167 [\u4e0b\u9650 .. \u4e0a\u9650] \u6642\u958b\u555f\u3002" + } + } + }, + "title": "\u81e8\u754c\u611f\u6e2c\u5668" +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/bg.json b/homeassistant/components/tod/translations/bg.json new file mode 100644 index 00000000000..35cfa0ad1d7 --- /dev/null +++ b/homeassistant/components/tod/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/tod/translations/ca.json b/homeassistant/components/tod/translations/ca.json new file mode 100644 index 00000000000..3908cb7761f --- /dev/null +++ b/homeassistant/components/tod/translations/ca.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after": "Activa despr\u00e9s de", + "after_time": "Temps d'activaci\u00f3", + "before": "Desactiva despr\u00e9s de", + "before_time": "Temps de desactivaci\u00f3", + "name": "Nom" + }, + "description": "Crea un sensor binari que s'activa o es desactiva en funci\u00f3 de l'hora.", + "title": "Afegeix sensor temps del dia" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after": "Activa despr\u00e9s de", + "after_time": "Temps d'activaci\u00f3", + "before": "Desactiva despr\u00e9s de", + "before_time": "Temps de desactivaci\u00f3" + }, + "description": "Crea un sensor binari que s'activa o es desactiva en funci\u00f3 de l'hora." + } + } + }, + "title": "Sensor temps del dia" +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/de.json b/homeassistant/components/tod/translations/de.json new file mode 100644 index 00000000000..eeb3d4dd8f9 --- /dev/null +++ b/homeassistant/components/tod/translations/de.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after": "Angeschaltet nach", + "after_time": "Einschaltzeit", + "before": "Ausgeschaltet nach", + "before_time": "Ausschaltzeit", + "name": "Name" + }, + "description": "Erstelle einen bin\u00e4ren Sensor, der sich je nach Uhrzeit ein- oder ausschaltet.", + "title": "Tageszeitsensor hinzuf\u00fcgen" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after": "Angeschaltet nach", + "after_time": "Einschaltzeit", + "before": "Ausgeschaltet nach", + "before_time": "Ausschaltzeit" + }, + "description": "Erstelle einen bin\u00e4ren Sensor, der sich je nach Uhrzeit ein- oder ausschaltet." + } + } + }, + "title": "Tageszeitensensor" +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/el.json b/homeassistant/components/tod/translations/el.json new file mode 100644 index 00000000000..ffa426365a4 --- /dev/null +++ b/homeassistant/components/tod/translations/el.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03bc\u03b5\u03c4\u03ac", + "after_time": "\u0395\u03bd\u03c4\u03cc\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5", + "before": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03bc\u03b5\u03c4\u03ac", + "before_time": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + }, + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c0\u03cc\u03c4\u03b5 \u03bf \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9.", + "title": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 New Times of the Day" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03bc\u03b5\u03c4\u03ac", + "after_time": "\u0395\u03bd\u03c4\u03cc\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5", + "before": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03bc\u03b5\u03c4\u03ac", + "before_time": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5" + }, + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c0\u03cc\u03c4\u03b5 \u03bf \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9." + } + } + }, + "title": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 Times of the Day" +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/en.json b/homeassistant/components/tod/translations/en.json index 2ecb2c695c8..ced14151519 100644 --- a/homeassistant/components/tod/translations/en.json +++ b/homeassistant/components/tod/translations/en.json @@ -3,7 +3,9 @@ "step": { "user": { "data": { + "after": "On after", "after_time": "On time", + "before": "Off after", "before_time": "Off time", "name": "Name" }, @@ -16,9 +18,12 @@ "step": { "init": { "data": { + "after": "On after", "after_time": "On time", + "before": "Off after", "before_time": "Off time" - } + }, + "description": "Create a binary sensor that turns on or off depending on the time." } } }, diff --git a/homeassistant/components/tod/translations/et.json b/homeassistant/components/tod/translations/et.json new file mode 100644 index 00000000000..06b40e0ce72 --- /dev/null +++ b/homeassistant/components/tod/translations/et.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after": "Kestus sissel\u00fclitumisest", + "after_time": "Seesoleku aeg", + "before": "Kestus v\u00e4ljal\u00fclitumisest", + "before_time": "V\u00e4ljasoleku aeg", + "name": "Nimi" + }, + "description": "Loo olekuandur mis l\u00fcltub sisse v\u00f5i v\u00e4lja vastavalt m\u00e4\u00e4ratud ajale.", + "title": "Lisa \u00f6\u00f6p\u00e4eva andur" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after": "Kestus sissel\u00fclitumisest", + "after_time": "Seesoleku aeg", + "before": "Kestus v\u00e4ljal\u00fclitumisest", + "before_time": "V\u00e4ljasoleku aeg" + }, + "description": "Vali millal andur on sisse v\u00f5i v\u00e4lja l\u00fclitatud." + } + } + }, + "title": "\u00d6\u00f6p\u00e4eva andur" +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/fr.json b/homeassistant/components/tod/translations/fr.json new file mode 100644 index 00000000000..799e286343b --- /dev/null +++ b/homeassistant/components/tod/translations/fr.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after": "Activ\u00e9 apr\u00e8s", + "after_time": "Heure d'activation", + "before": "D\u00e9sactiv\u00e9 apr\u00e8s", + "before_time": "Heure de d\u00e9sactivation", + "name": "Nom" + }, + "description": "Cr\u00e9ez un capteur binaire qui s'active et se d\u00e9sactive en fonction de l'heure.", + "title": "Ajouter un capteur de moment de la journ\u00e9e" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after": "Activ\u00e9 apr\u00e8s", + "after_time": "Heure d'activation", + "before": "D\u00e9sactiv\u00e9 apr\u00e8s", + "before_time": "Heure de d\u00e9sactivation" + }, + "description": "Cr\u00e9ez un capteur binaire qui s'active et se d\u00e9sactive en fonction de l'heure." + } + } + }, + "title": "Capteur de moment de la journ\u00e9e" +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/he.json b/homeassistant/components/tod/translations/he.json new file mode 100644 index 00000000000..b10f9e2b1ca --- /dev/null +++ b/homeassistant/components/tod/translations/he.json @@ -0,0 +1,30 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after": "\u05d4\u05e4\u05e2\u05dc\u05d4 \u05dc\u05d0\u05d7\u05e8", + "after_time": "\u05d6\u05de\u05df \u05d4\u05e4\u05e2\u05dc\u05d4", + "before": "\u05db\u05d9\u05d1\u05d5\u05d9 \u05dc\u05d0\u05d7\u05e8", + "before_time": "\u05d6\u05de\u05df \u05db\u05d9\u05d1\u05d5\u05d9", + "name": "\u05e9\u05dd" + }, + "description": "\u05d9\u05e6\u05d9\u05e8\u05ea \u05d7\u05d9\u05d9\u05e9\u05df \u05d1\u05d9\u05e0\u05d0\u05e8\u05d9 \u05d4\u05de\u05d5\u05e4\u05e2\u05dc \u05d0\u05d5 \u05e0\u05db\u05d1\u05d4 \u05d1\u05d4\u05ea\u05d0\u05dd \u05dc\u05d6\u05de\u05df.", + "title": "\u05d4\u05d5\u05e1\u05e4\u05ea \u05d7\u05d9\u05d9\u05e9\u05df \u05e9\u05e2\u05d5\u05ea \u05d1\u05d9\u05d5\u05dd" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after": "\u05d4\u05e4\u05e2\u05dc\u05d4 \u05dc\u05d0\u05d7\u05e8", + "after_time": "\u05d6\u05de\u05df \u05d4\u05e4\u05e2\u05dc\u05d4", + "before": "\u05db\u05d9\u05d1\u05d5\u05d9 \u05dc\u05d0\u05d7\u05e8", + "before_time": "\u05d6\u05de\u05df \u05db\u05d9\u05d1\u05d5\u05d9" + }, + "description": "\u05d9\u05e6\u05d9\u05e8\u05ea \u05d7\u05d9\u05d9\u05e9\u05df \u05d1\u05d9\u05e0\u05d0\u05e8\u05d9 \u05d4\u05de\u05d5\u05e4\u05e2\u05dc \u05d0\u05d5 \u05e0\u05db\u05d1\u05d4 \u05d1\u05d4\u05ea\u05d0\u05dd \u05dc\u05d6\u05de\u05df." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/hu.json b/homeassistant/components/tod/translations/hu.json new file mode 100644 index 00000000000..28af029f230 --- /dev/null +++ b/homeassistant/components/tod/translations/hu.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after": "BE ut\u00e1n", + "after_time": "BE id\u0151pont", + "before": "KI ut\u00e1n", + "before_time": "KI id\u0151pont", + "name": "Elnevez\u00e9s" + }, + "description": "\u00c1ll\u00edtsa be, hogy az \u00e9rz\u00e9kel\u0151 mikor kapcsoljon be \u00e9s ki.", + "title": "I\u0151pontok \u00e9rz\u00e9kel\u0151 hozz\u00e1ad\u00e1sa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after": "BE ut\u00e1n", + "after_time": "BE id\u0151pont", + "before": "KI ut\u00e1n", + "before_time": "KI id\u0151pont" + }, + "description": "\u00c1ll\u00edtsa be, hogy az \u00e9rz\u00e9kel\u0151 mikor kapcsoljon be \u00e9s ki." + } + } + }, + "title": "I\u0151pontok \u00e9rz\u00e9kel\u0151" +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/id.json b/homeassistant/components/tod/translations/id.json new file mode 100644 index 00000000000..2ceef6c3a40 --- /dev/null +++ b/homeassistant/components/tod/translations/id.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after": "Nyala setelah", + "after_time": "Nyala pada", + "before": "Mati setelah", + "before_time": "Mati pada", + "name": "Nama" + }, + "description": "Buat sensor biner yang nyala atau mati tergantung waktu.", + "title": "Tambahkan Sensor Waktu Pada Hari Ini" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after": "Nyala setelah", + "after_time": "Nyala pada", + "before": "Mati setelah", + "before_time": "Mati pada" + }, + "description": "Buat sensor biner yang nyala atau mati tergantung waktu." + } + } + }, + "title": "Sensor Waktu Pada Hari Ini" +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/it.json b/homeassistant/components/tod/translations/it.json new file mode 100644 index 00000000000..072cc80f5ff --- /dev/null +++ b/homeassistant/components/tod/translations/it.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after": "Acceso dopo", + "after_time": "Ora di accensione", + "before": "Spento dopo", + "before_time": "Ora di spegnimento", + "name": "Nome" + }, + "description": "Crea un sensore binario che si accende o si spegne a seconda dell'ora.", + "title": "Aggiungi sensore delle ore del giorno" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after": "Acceso dopo", + "after_time": "Ora di accensione", + "before": "Spento dopo", + "before_time": "Ora di spegnimento" + }, + "description": "Crea un sensore binario che si accende o si spegne a seconda dell'ora." + } + } + }, + "title": "Sensore ore del giorno" +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/ja.json b/homeassistant/components/tod/translations/ja.json new file mode 100644 index 00000000000..a497f3e3c98 --- /dev/null +++ b/homeassistant/components/tod/translations/ja.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after": "\u5f8c\u306b\u30aa\u30f3(On after)", + "after_time": "\u30aa\u30f3\u30bf\u30a4\u30e0(On time)", + "before": "\u30aa\u30d5\u5f8c(Off after)", + "before_time": "\u30aa\u30d5\u30bf\u30a4\u30e0(Off time)", + "name": "\u540d\u524d" + }, + "description": "\u30bb\u30f3\u30b5\u30fc\u306e\u30aa\u30f3\u3068\u30aa\u30d5\u3092\u5207\u308a\u66ff\u3048\u308b\u30bf\u30a4\u30df\u30f3\u30b0\u3092\u69cb\u6210\u3057\u307e\u3059\u3002", + "title": "\u65b0\u3057\u3044\u6642\u523b\u30bb\u30f3\u30b5\u30fc" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after": "\u5f8c\u306b\u30aa\u30f3(On after)", + "after_time": "\u30aa\u30f3\u30bf\u30a4\u30e0(On time)", + "before": "\u30aa\u30d5\u5f8c(Off after)", + "before_time": "\u30aa\u30d5\u30bf\u30a4\u30e0(Off time)" + }, + "description": "\u30bb\u30f3\u30b5\u30fc\u306e\u30aa\u30f3\u3068\u30aa\u30d5\u3092\u5207\u308a\u66ff\u3048\u308b\u30bf\u30a4\u30df\u30f3\u30b0\u3092\u69cb\u6210\u3057\u307e\u3059\u3002" + } + } + }, + "title": "\u6642\u9593\u5e2f\u30bb\u30f3\u30b5\u30fc(Times of the Day Sensor)" +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/nl.json b/homeassistant/components/tod/translations/nl.json new file mode 100644 index 00000000000..82f06ea3f4f --- /dev/null +++ b/homeassistant/components/tod/translations/nl.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after": "Aan na", + "after_time": "Op tijd", + "before": "Uit na", + "before_time": "Uit tijd", + "name": "Naam\n" + }, + "description": "Maak een binaire sensor die afhankelijk van de tijd in- of uitgeschakeld wordt.", + "title": "Tijden van de dag sensor toevoegen" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after": "Aan na", + "after_time": "Op tijd", + "before": "Uit na", + "before_time": "Uit tijd" + }, + "description": "Maak een binaire sensor die afhankelijk van de tijd in- of uitgeschakeld wordt." + } + } + }, + "title": "Tijd van de dag Sensor" +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/no.json b/homeassistant/components/tod/translations/no.json new file mode 100644 index 00000000000..a2cbf7e6427 --- /dev/null +++ b/homeassistant/components/tod/translations/no.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after": "P\u00e5 etter", + "after_time": "P\u00e5 tide", + "before": "Av etter", + "before_time": "Utenfor arbeidstid", + "name": "Navn" + }, + "description": "Lag en bin\u00e6r sensor som sl\u00e5s av eller p\u00e5 avhengig av tiden.", + "title": "Legg til klokkeslettsensor" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after": "P\u00e5 etter", + "after_time": "P\u00e5 tide", + "before": "Av etter", + "before_time": "Utenfor arbeidstid" + }, + "description": "Lag en bin\u00e6r sensor som sl\u00e5s av eller p\u00e5 avhengig av tiden." + } + } + }, + "title": "Tidssensor for dagen" +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/pl.json b/homeassistant/components/tod/translations/pl.json new file mode 100644 index 00000000000..8b03a798f5c --- /dev/null +++ b/homeassistant/components/tod/translations/pl.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after": "W\u0142\u0105cz po", + "after_time": "Czas w\u0142\u0105czenia", + "before": "Wy\u0142\u0105cz po", + "before_time": "Czas wy\u0142\u0105czenia", + "name": "Nazwa" + }, + "description": "Utw\u00f3rz sensor binarny, kt\u00f3ry w\u0142\u0105cza si\u0119 lub wy\u0142\u0105cza w zale\u017cno\u015bci od czasu.", + "title": "Dodaj sensor p\u00f3r dnia" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after": "W\u0142\u0105cz po", + "after_time": "Czas w\u0142\u0105czenia", + "before": "Wy\u0142\u0105cz po", + "before_time": "Czas wy\u0142\u0105czenia" + }, + "description": "Utw\u00f3rz sensor binarny, kt\u00f3ry w\u0142\u0105cza si\u0119 lub wy\u0142\u0105cza w zale\u017cno\u015bci od czasu." + } + } + }, + "title": "Sensor p\u00f3r dnia" +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/pt-BR.json b/homeassistant/components/tod/translations/pt-BR.json new file mode 100644 index 00000000000..e9784076fd7 --- /dev/null +++ b/homeassistant/components/tod/translations/pt-BR.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after": "Ligado depois", + "after_time": "Ligado na hora", + "before": "Desligado depois", + "before_time": "Desligado na hora", + "name": "Nome" + }, + "description": "Crie um sensor bin\u00e1rio que liga ou desliga dependendo do tempo.", + "title": "Adicionar sensor de horas do dia" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after": "Ligado depois", + "after_time": "Ligado na hora", + "before": "Desligado depois", + "before_time": "Desligado na hora" + }, + "description": "Crie um sensor bin\u00e1rio que liga ou desliga dependendo do tempo." + } + } + }, + "title": "Sensor de horas do dia" +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/ru.json b/homeassistant/components/tod/translations/ru.json new file mode 100644 index 00000000000..eda3e4efd77 --- /dev/null +++ b/homeassistant/components/tod/translations/ru.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u043e\u0441\u043b\u0435", + "after_time": "\u0412\u0440\u0435\u043c\u044f \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", + "before": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u043e\u0441\u043b\u0435", + "before_time": "\u0412\u0440\u0435\u043c\u044f \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0411\u0438\u043d\u0430\u0440\u043d\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0438\u043b\u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0432\u0440\u0435\u043c\u0435\u043d\u0438.", + "title": "\u0412\u0440\u0435\u043c\u044f \u0441\u0443\u0442\u043e\u043a" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u043e\u0441\u043b\u0435", + "after_time": "\u0412\u0440\u0435\u043c\u044f \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", + "before": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u043e\u0441\u043b\u0435", + "before_time": "\u0412\u0440\u0435\u043c\u044f \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + }, + "description": "\u0411\u0438\u043d\u0430\u0440\u043d\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0438\u043b\u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0432\u0440\u0435\u043c\u0435\u043d\u0438." + } + } + }, + "title": "\u0412\u0440\u0435\u043c\u044f \u0441\u0443\u0442\u043e\u043a" +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/tr.json b/homeassistant/components/tod/translations/tr.json new file mode 100644 index 00000000000..e2f04757369 --- /dev/null +++ b/homeassistant/components/tod/translations/tr.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after": "Daha sonra", + "after_time": "Vaktinde", + "before": "Sonra kapat", + "before_time": "Kapatma zaman\u0131", + "name": "Ad" + }, + "description": "Zamana ba\u011fl\u0131 olarak a\u00e7\u0131l\u0131p kapanan bir ikili sens\u00f6r olu\u015fturun.", + "title": "G\u00fcn\u00fcn Saatleri Sens\u00f6r\u00fc Ekleyin" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after": "Daha sonra", + "after_time": "Vaktinde", + "before": "Sonra kapat", + "before_time": "Kapatma zaman\u0131" + }, + "description": "Zamana ba\u011fl\u0131 olarak a\u00e7\u0131l\u0131p kapanan bir ikili sens\u00f6r olu\u015fturun." + } + } + }, + "title": "G\u00fcn\u00fcn Saatleri Sens\u00f6r\u00fc" +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/zh-Hans.json b/homeassistant/components/tod/translations/zh-Hans.json new file mode 100644 index 00000000000..1c0b6ba1597 --- /dev/null +++ b/homeassistant/components/tod/translations/zh-Hans.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after": "\u5728\u6b64\u65f6\u95f4\u540e\u6253\u5f00", + "after_time": "\u6253\u5f00\u65f6\u95f4", + "before": "\u5728\u6b64\u65f6\u95f4\u540e\u5173\u95ed", + "before_time": "\u5173\u95ed\u65f6\u95f4", + "name": "\u540d\u79f0" + }, + "description": "\u521b\u5efa\u6839\u636e\u65f6\u95f4\u6539\u53d8\u5f00\u5173\u72b6\u6001\u7684\u4e8c\u5143\u4f20\u611f\u5668\u3002", + "title": "\u6dfb\u52a0\u65f6\u95f4\u8303\u56f4\u4f20\u611f\u5668" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after": "\u5728\u6b64\u65f6\u95f4\u540e\u6253\u5f00", + "after_time": "\u6253\u5f00\u65f6\u95f4", + "before": "\u5728\u6b64\u65f6\u95f4\u540e\u5173\u95ed", + "before_time": "\u5173\u95ed\u65f6\u95f4" + }, + "description": "\u521b\u5efa\u6839\u636e\u65f6\u95f4\u6539\u53d8\u5f00\u5173\u72b6\u6001\u7684\u4e8c\u5143\u4f20\u611f\u5668\u3002" + } + } + }, + "title": "\u65f6\u95f4\u8303\u56f4\u4f20\u611f\u5668" +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/zh-Hant.json b/homeassistant/components/tod/translations/zh-Hant.json new file mode 100644 index 00000000000..0a47c33a318 --- /dev/null +++ b/homeassistant/components/tod/translations/zh-Hant.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after": "\u958b\u555f\u6642\u9577\u5f8c", + "after_time": "\u958b\u555f\u6642\u9593", + "before": "\u95dc\u9589\u6642\u9577\u5f8c", + "before_time": "\u95dc\u9589\u6642\u9593", + "name": "\u540d\u7a31" + }, + "description": "\u65b0\u589e\u6839\u64da\u6642\u9593\u6c7a\u5b9a\u958b\u95dc\u4e4b\u6642\u9593\u611f\u6e2c\u5668\u3002", + "title": "\u65b0\u589e\u6bcf\u65e5\u5b9a\u6642\u611f\u6e2c\u5668" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after": "\u958b\u555f\u6642\u9577\u5f8c", + "after_time": "\u958b\u555f\u6642\u9593", + "before": "\u95dc\u9589\u6642\u9577\u5f8c", + "before_time": "\u95dc\u9589\u6642\u9593" + }, + "description": "\u65b0\u589e\u6839\u64da\u6642\u9593\u6c7a\u5b9a\u958b\u95dc\u4e4b\u6642\u9593\u611f\u6e2c\u5668\u3002" + } + } + }, + "title": "\u6bcf\u65e5\u5b9a\u6642\u611f\u6e2c\u5668" +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/bg.json b/homeassistant/components/tomorrowio/translations/bg.json new file mode 100644 index 00000000000..783408dab84 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \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": { + "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", + "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", + "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/tomorrowio/translations/ca.json b/homeassistant/components/tomorrowio/translations/ca.json new file mode 100644 index 00000000000..fc351430ffb --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/ca.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_api_key": "Clau API inv\u00e0lida", + "rate_limited": "Freq\u00fc\u00e8ncia limitada temporalment, torna-ho a provar m\u00e9s tard.", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "latitude": "Latitud", + "location": "Ubicaci\u00f3", + "longitude": "Longitud", + "name": "Nom" + }, + "description": "Per obtenir una clau API, registra't a [Tomorrow.io](https://app.tomorrow.io/signup)." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. entre previsions de NowCast" + }, + "description": "Si decideixes activar l'entitat de previsi\u00f3 `nowcast`, podr\u00e0s configurar l'interval en minuts entre cada previsi\u00f3. El nombre de previsions proporcionades dep\u00e8n d'aquest interval de minuts.", + "title": "Actualitza les opcions de Tomorrow.io" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/cs.json b/homeassistant/components/tomorrowio/translations/cs.json new file mode 100644 index 00000000000..6b50656cd00 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Um\u00edst\u011bn\u00ed" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/de.json b/homeassistant/components/tomorrowio/translations/de.json new file mode 100644 index 00000000000..03739ce5c2f --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/de.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "rate_limited": "Aktuelle Aktualisierungsrate gedrosselt, bitte versuche es sp\u00e4ter erneut.", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "latitude": "Breitengrad", + "location": "Standort", + "longitude": "L\u00e4ngengrad", + "name": "Name" + }, + "description": "Um einen API-Schl\u00fcssel zu erhalten, melde dich bei [Tomorrow.io] (https://app.tomorrow.io/signup) an." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "Minuten zwischen den NowCast Kurzvorhersagen" + }, + "description": "Wenn du die Vorhersage-Entitit\u00e4t \"Kurzvorhersage\" aktivierst, kannst du die Anzahl der Minuten zwischen den einzelnen Vorhersagen konfigurieren. Die Anzahl der bereitgestellten Vorhersagen h\u00e4ngt von der Anzahl der zwischen den Vorhersagen gew\u00e4hlten Minuten ab.", + "title": "Tomorrow.io Optionen aktualisieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/el.json b/homeassistant/components/tomorrowio/translations/el.json new file mode 100644 index 00000000000..28e3f56c379 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/el.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API", + "rate_limited": "\u0391\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c4\u03b9\u03b3\u03bc\u03ae \u03b7 \u03c4\u03b9\u03bc\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", + "location": "\u03a4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1", + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + }, + "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API, \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf [Tomorrow.io](https://app.tomorrow.io/signup)." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "\u0395\u03bb\u03ac\u03c7. \u039c\u03b5\u03c4\u03b1\u03be\u03cd NowCast Forecasts" + }, + "description": "\u0395\u03ac\u03bd \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c0\u03c1\u03cc\u03b2\u03bb\u03b5\u03c8\u03b7\u03c2 \"nowcast\", \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03ba\u03ac\u03b8\u03b5 \u03c0\u03c1\u03cc\u03b2\u03bb\u03b5\u03c8\u03b7\u03c2. \u039f \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c4\u03c9\u03bd \u03c0\u03b1\u03c1\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03c9\u03bd \u03c0\u03c1\u03bf\u03b2\u03bb\u03ad\u03c8\u03b5\u03c9\u03bd \u03b5\u03be\u03b1\u03c1\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03c0\u03bf\u03c5 \u03b5\u03c0\u03b9\u03bb\u03ad\u03b3\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03c0\u03c1\u03bf\u03b2\u03bb\u03ad\u03c8\u03b5\u03c9\u03bd.", + "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd Tomorrow.io" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/en.json b/homeassistant/components/tomorrowio/translations/en.json index 7c653b00574..103f1c81679 100644 --- a/homeassistant/components/tomorrowio/translations/en.json +++ b/homeassistant/components/tomorrowio/translations/en.json @@ -11,6 +11,7 @@ "data": { "api_key": "API Key", "latitude": "Latitude", + "location": "Location", "longitude": "Longitude", "name": "Name" }, diff --git a/homeassistant/components/tomorrowio/translations/et.json b/homeassistant/components/tomorrowio/translations/et.json new file mode 100644 index 00000000000..7ac38239349 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/et.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_api_key": "Vigane API v\u00f5ti", + "rate_limited": "Hetkel on m\u00e4\u00e4r piiratud, proovi hiljem uuesti.", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "latitude": "Laiuskraad", + "location": "Asukoht", + "longitude": "Pikkuskraad", + "name": "Nimi" + }, + "description": "API v\u00f5tme saamiseks registreeru aadressil [Tomorrow.io](https://app.tomorrow.io/signup)." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "Minuteid NowCasti prognooside vahel" + }, + "description": "Kui otsustad lubada olemi \"nowcast\", saad seadistada iga prognoosi vahelise minutite arvu. Esitatud prognooside arv s\u00f5ltub prognooside vahel valitud minutite arvust.", + "title": "V\u00e4rskenda Tomorrow.io valikuid" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/fr.json b/homeassistant/components/tomorrowio/translations/fr.json new file mode 100644 index 00000000000..bcb97eb3fc5 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/fr.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_api_key": "Cl\u00e9 d'API non valide", + "rate_limited": "Nombre maximal de tentatives de connexion d\u00e9pass\u00e9, veuillez r\u00e9essayer ult\u00e9rieurement.", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "latitude": "Latitude", + "location": "Emplacement", + "longitude": "Longitude", + "name": "Nom" + }, + "description": "Pour obtenir une cl\u00e9 d'API, inscrivez-vous sur [Tomorrow.io](https://app.tomorrow.io/signup)." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. entre les pr\u00e9visions NowCast" + }, + "description": "Si vous choisissez d'activer l'entit\u00e9 de pr\u00e9vision \u00ab\u00a0nowcast\u00a0\u00bb, vous pouvez configurer le nombre de minutes entre chaque pr\u00e9vision. Le nombre de pr\u00e9visions fournies d\u00e9pend du nombre de minutes choisies entre les pr\u00e9visions.", + "title": "Mettre \u00e0 jour les options de Tomorrow.io" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/he.json b/homeassistant/components/tomorrowio/translations/he.json new file mode 100644 index 00000000000..20b48520f18 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/he.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API", + "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", + "location": "\u05de\u05d9\u05e7\u05d5\u05dd", + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/hu.json b/homeassistant/components/tomorrowio/translations/hu.json new file mode 100644 index 00000000000..8f90392234e --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/hu.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", + "rate_limited": "Jelenleg korl\u00e1tozott a hozz\u00e1f\u00e9r\u00e9s, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "latitude": "Sz\u00e9less\u00e9g", + "location": "Elhelyezked\u00e9s", + "longitude": "Hossz\u00fas\u00e1g", + "name": "Elnevez\u00e9s" + }, + "description": "API-kulcs beszerz\u00e9s\u00e9hez regisztr\u00e1ljon a [Tomorrow.io] (https://app.tomorrow.io/signup) oldalon." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "Percek NowCast el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt" + }, + "description": "Ha enged\u00e9lyezi a 'nowcast' id\u0151j\u00e1r\u00e1st, be\u00e1ll\u00edthatja az egyes el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt eltelt percek sz\u00e1m\u00e1t. Az el\u0151rejelz\u00e9sek sz\u00e1ma az el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt v\u00e1lasztott percek sz\u00e1m\u00e1t\u00f3l f\u00fcgg.", + "title": "Tomorrow.io be\u00e1ll\u00edt\u00e1sok friss\u00edt\u00e9se" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/id.json b/homeassistant/components/tomorrowio/translations/id.json new file mode 100644 index 00000000000..b428648e799 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/id.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_api_key": "Kunci API tidak valid", + "rate_limited": "Saat ini tingkatnya dibatasi, coba lagi nanti.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "latitude": "Lintang", + "location": "Lokasi", + "longitude": "Bujur", + "name": "Nama" + }, + "description": "Untuk mendapatkan kunci API, daftar di [Tomorrow.io](https://app.tomorrow.io/signup)." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "Jarak Interval Prakiraan NowCast dalam Menit" + }, + "description": "Jika Anda memilih untuk mengaktifkan entitas prakiraan 'nowcast', Anda dapat mengonfigurasi jumlah menit antar-prakiraan. Jumlah prakiraan yang diberikan tergantung pada jumlah menit yang dipilih antar-prakiraan.", + "title": "Perbarui Opsi Tomorrow.io" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/it.json b/homeassistant/components/tomorrowio/translations/it.json new file mode 100644 index 00000000000..9a79896a3d8 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/it.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_api_key": "Chiave API non valida", + "rate_limited": "Attualmente la tariffa \u00e8 limitata, riprova pi\u00f9 tardi.", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "latitude": "Latitudine", + "location": "Posizione", + "longitude": "Logitudine", + "name": "Nome" + }, + "description": "Per ottenere una chiave API, registrati su [Tomorrow.io](https://app.tomorrow.io/signup)." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. tra le previsioni di NowCast" + }, + "description": "Se scegli di abilitare l'entit\u00e0 di previsione `nowcast`, puoi configurare il numero di minuti tra ciascuna previsione. Il numero di previsioni fornite dipende dal numero di minuti scelti tra le previsioni.", + "title": "Aggiorna le opzioni di Tomorrow.io" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/ja.json b/homeassistant/components/tomorrowio/translations/ja.json new file mode 100644 index 00000000000..17d31f74214 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/ja.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", + "rate_limited": "\u73fe\u5728\u30ec\u30fc\u30c8\u304c\u5236\u9650\u3055\u308c\u3066\u3044\u307e\u3059\u306e\u3067\u3001\u5f8c\u3067\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "latitude": "\u7def\u5ea6", + "location": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3", + "longitude": "\u7d4c\u5ea6", + "name": "\u540d\u524d" + }, + "description": "API\u30ad\u30fc\u3092\u53d6\u5f97\u3059\u308b\u306b\u306f\u3001 [Tomorrow.io](https://app.tomorrow.io/signup) \u306b\u30b5\u30a4\u30f3\u30a2\u30c3\u30d7\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "\u6700\u5c0f: Between NowCast Forecasts" + }, + "description": "`nowcast` forecast(\u4e88\u6e2c) \u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u6709\u52b9\u306b\u3059\u308b\u3053\u3068\u3092\u9078\u629e\u3057\u305f\u5834\u5408\u3001\u5404\u4e88\u6e2c\u9593\u306e\u5206\u6570\u3092\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u63d0\u4f9b\u3055\u308c\u308bforecast(\u4e88\u6e2c)\u306e\u6570\u306f\u3001forecast(\u4e88\u6e2c)\u306e\u9593\u306b\u9078\u629e\u3057\u305f\u5206\u6570\u306b\u4f9d\u5b58\u3057\u307e\u3059\u3002", + "title": "Tomorrow.io\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u66f4\u65b0" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/nl.json b/homeassistant/components/tomorrowio/translations/nl.json new file mode 100644 index 00000000000..d1efb7d75c3 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/nl.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_api_key": "Ongeldige API-sleutel", + "rate_limited": "Aantal aanvragen bereikt, probeer later opnieuw", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "latitude": "Breedtegraad", + "location": "Locatie", + "longitude": "Lengtegraad", + "name": "Naam" + }, + "description": "Om een API sleutel te krijgen, meld je aan bij [Tomorrow.io](https://app.tomorrow.io/signup)." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. Tussen NowCast Voorspellingen" + }, + "description": "Indien u ervoor kiest om de `nowcast` voorspellingsentiteit in te schakelen, kan u het aantal minuten tussen elke voorspelling configureren. Het aantal voorspellingen hangt af van het aantal gekozen minuten tussen de voorspellingen.", + "title": "Update Tomorrow.io Opties" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/no.json b/homeassistant/components/tomorrowio/translations/no.json new file mode 100644 index 00000000000..bf366895a70 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/no.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_api_key": "Ugyldig API-n\u00f8kkel", + "rate_limited": "For \u00f8yeblikket prisbegrenset, pr\u00f8v igjen senere.", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "latitude": "Breddegrad", + "location": "Plassering", + "longitude": "Lengdegrad", + "name": "Navn" + }, + "description": "For \u00e5 f\u00e5 en API-n\u00f8kkel, registrer deg p\u00e5 [Tomorrow.io](https://app.tomorrow.io/signup)." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. Mellom NowCast-prognoser" + }, + "description": "Hvis du velger \u00e5 aktivere prognoseenheten \"nowcast\", kan du konfigurere antall minutter mellom hver prognose. Antallet prognoser som gis avhenger av antall minutter valgt mellom prognosene.", + "title": "Oppdater Tomorrow.io-alternativer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/pl.json b/homeassistant/components/tomorrowio/translations/pl.json new file mode 100644 index 00000000000..4637a553aa4 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/pl.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_api_key": "Nieprawid\u0142owy klucz API", + "rate_limited": "Przekroczono limit, spr\u00f3buj ponownie p\u00f3\u017aniej.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "location": "Lokalizacja", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa" + }, + "description": "Aby uzyska\u0107 klucz API, zarejestruj si\u0119 na [Tomorrow.io](https://app.tomorrow.io/signup)." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "Czas (min) mi\u0119dzy prognozami NowCast" + }, + "description": "Je\u015bli zdecydujesz si\u0119 w\u0142\u0105czy\u0107 encj\u0119 prognozy \u201enowcast\u201d, mo\u017cesz skonfigurowa\u0107 liczb\u0119 minut mi\u0119dzy ka\u017cd\u0105 prognoz\u0105. Liczba dostarczonych prognoz zale\u017cy od liczby minut wybranych mi\u0119dzy prognozami.", + "title": "Zaktualizuj opcje Tomorrow.io" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/pt-BR.json b/homeassistant/components/tomorrowio/translations/pt-BR.json new file mode 100644 index 00000000000..83f78e31b8e --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/pt-BR.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_api_key": "Chave de API inv\u00e1lida", + "rate_limited": "Taxa atualmente limitada. Tente novamente mais tarde.", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API", + "latitude": "Latitude", + "location": "Localiza\u00e7\u00e3o", + "longitude": "Longitude", + "name": "Nome" + }, + "description": "Para obter uma chave de API, inscreva-se em [Tomorrow.io](https://app.tomorrow.io/signup)." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. Entre Previs\u00f5es NowCast" + }, + "description": "Se voc\u00ea optar por ativar a entidade de previs\u00e3o `nowcast`, poder\u00e1 configurar o n\u00famero de minutos entre cada previs\u00e3o. O n\u00famero de previs\u00f5es fornecidas depende do n\u00famero de minutos escolhidos entre as previs\u00f5es.", + "title": "Atualizar op\u00e7\u00f5es do Tomorrow.io" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/ru.json b/homeassistant/components/tomorrowio/translations/ru.json new file mode 100644 index 00000000000..08e24c5a47a --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/ru.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "rate_limited": "\u041f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u043f\u044b\u0442\u043e\u043a, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0427\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API, \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0439\u0442\u0435\u0441\u044c \u043d\u0430 [Tomorrow.io](https://app.tomorrow.io/signup)." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043c\u0435\u0436\u0434\u0443 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430\u043c\u0438 NowCast (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)" + }, + "description": "\u0415\u0441\u043b\u0438 \u0412\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0435\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 'nowcast', \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430. \u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u043e\u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u0437\u0430\u0434\u0430\u043d\u043d\u043e\u0433\u043e \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b\u0430 \u043c\u0435\u0436\u0434\u0443 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430\u043c\u0438.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Tomorrow.io" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.bg.json b/homeassistant/components/tomorrowio/translations/sensor.bg.json new file mode 100644 index 00000000000..1a5c2989702 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.bg.json @@ -0,0 +1,8 @@ +{ + "state": { + "tomorrowio__precipitation_type": { + "rain": "\u0414\u044a\u0436\u0434", + "snow": "\u0421\u043d\u044f\u0433" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.ca.json b/homeassistant/components/tomorrowio/translations/sensor.ca.json new file mode 100644 index 00000000000..161978d1919 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.ca.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "Bo", + "hazardous": "Perill\u00f3s", + "moderate": "Moderat", + "unhealthy": "Poc saludable", + "unhealthy_for_sensitive_groups": "No saludable per a grups sensibles", + "very_unhealthy": "Gens saludable" + }, + "tomorrowio__pollen_index": { + "high": "Alt", + "low": "Baix", + "medium": "Mitj\u00e0", + "none": "Cap", + "very_high": "Molt alt", + "very_low": "Molt baix" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "Pluja congelada", + "ice_pellets": "Gran\u00eds", + "none": "Cap", + "rain": "Pluja", + "snow": "Neu" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.de.json b/homeassistant/components/tomorrowio/translations/sensor.de.json new file mode 100644 index 00000000000..801833520f7 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.de.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "Gut", + "hazardous": "Gef\u00e4hrlich", + "moderate": "M\u00e4\u00dfig", + "unhealthy": "Ungesund", + "unhealthy_for_sensitive_groups": "Ungesund f\u00fcr sensible Gruppen", + "very_unhealthy": "Sehr ungesund" + }, + "tomorrowio__pollen_index": { + "high": "Hoch", + "low": "Niedrig", + "medium": "Mittel", + "none": "Keiner", + "very_high": "Sehr hoch", + "very_low": "Sehr niedrig" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "Gefrierender Regen", + "ice_pellets": "Graupel", + "none": "Keiner", + "rain": "Regen", + "snow": "Schnee" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.el.json b/homeassistant/components/tomorrowio/translations/sensor.el.json new file mode 100644 index 00000000000..fd4ca22f8d0 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.el.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "\u039a\u03b1\u03bb\u03cc", + "hazardous": "\u0395\u03c0\u03b9\u03ba\u03af\u03bd\u03b4\u03c5\u03bd\u03bf", + "moderate": "\u039c\u03ad\u03c4\u03c1\u03b9\u03bf", + "unhealthy": "\u0391\u03bd\u03b8\u03c5\u03b3\u03b9\u03b5\u03b9\u03bd\u03cc", + "unhealthy_for_sensitive_groups": "\u0391\u03bd\u03b8\u03c5\u03b3\u03b9\u03b5\u03b9\u03bd\u03cc \u03b3\u03b9\u03b1 \u03b5\u03c5\u03b1\u03af\u03c3\u03b8\u03b7\u03c4\u03b5\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b5\u03c2", + "very_unhealthy": "\u03a0\u03bf\u03bb\u03cd \u0391\u03bd\u03b8\u03c5\u03b3\u03b9\u03b5\u03b9\u03bd\u03cc" + }, + "tomorrowio__pollen_index": { + "high": "\u03a5\u03c8\u03b7\u03bb\u03cc", + "low": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03cc", + "medium": "\u039c\u03b5\u03c3\u03b1\u03af\u03bf", + "none": "\u03a4\u03af\u03c0\u03bf\u03c4\u03b1", + "very_high": "\u03a0\u03bf\u03bb\u03cd \u03c5\u03c8\u03b7\u03bb\u03cc", + "very_low": "\u03a0\u03bf\u03bb\u03cd \u03c7\u03b1\u03bc\u03b7\u03bb\u03cc" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "\u03a0\u03b1\u03b3\u03c9\u03bc\u03ad\u03bd\u03b7 \u03b2\u03c1\u03bf\u03c7\u03ae", + "ice_pellets": "\u03a7\u03b1\u03bb\u03ac\u03b6\u03b9", + "none": "\u03a4\u03af\u03c0\u03bf\u03c4\u03b1", + "rain": "\u0392\u03c1\u03bf\u03c7\u03ae", + "snow": "\u03a7\u03b9\u03cc\u03bd\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.et.json b/homeassistant/components/tomorrowio/translations/sensor.et.json new file mode 100644 index 00000000000..56482b21e12 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.et.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "Hea", + "hazardous": "Ohtlik", + "moderate": "M\u00f5\u00f5dukas", + "unhealthy": "Ebatervislik", + "unhealthy_for_sensitive_groups": "Ebatervislik riskir\u00fchmale", + "very_unhealthy": "V\u00e4ga ebatervislik" + }, + "tomorrowio__pollen_index": { + "high": "K\u00f5rge", + "low": "Madal", + "medium": "Keskmine", + "none": "Saastet pole", + "very_high": "V\u00e4ga k\u00f5rge", + "very_low": "V\u00e4ga madal" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "J\u00e4\u00e4vihm", + "ice_pellets": "J\u00e4\u00e4kruubid", + "none": "Sademeid pole", + "rain": "Vihm", + "snow": "Lumi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.fr.json b/homeassistant/components/tomorrowio/translations/sensor.fr.json new file mode 100644 index 00000000000..c0e2ee8747d --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.fr.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "Bon", + "hazardous": "Dangereux", + "moderate": "Mod\u00e9r\u00e9", + "unhealthy": "Malsain", + "unhealthy_for_sensitive_groups": "Malsain pour les groupes sensibles", + "very_unhealthy": "Tr\u00e8s malsain" + }, + "tomorrowio__pollen_index": { + "high": "\u00c9lev\u00e9", + "low": "Faible", + "medium": "Moyen", + "none": "Nul", + "very_high": "Tr\u00e8s \u00e9lev\u00e9", + "very_low": "Tr\u00e8s faible" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "Pluie vergla\u00e7ante", + "ice_pellets": "Gr\u00e9sil", + "none": "Aucune", + "rain": "Pluie", + "snow": "Neige" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.hu.json b/homeassistant/components/tomorrowio/translations/sensor.hu.json new file mode 100644 index 00000000000..3377ecb47dd --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.hu.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "J\u00f3", + "hazardous": "Vesz\u00e9lyes", + "moderate": "M\u00e9rs\u00e9kelt", + "unhealthy": "Eg\u00e9szs\u00e9gtelen", + "unhealthy_for_sensitive_groups": "Eg\u00e9szs\u00e9gtelen \u00e9rz\u00e9keny csoportok sz\u00e1m\u00e1ra", + "very_unhealthy": "Nagyon eg\u00e9szs\u00e9gtelen" + }, + "tomorrowio__pollen_index": { + "high": "Magas", + "low": "Alacsony", + "medium": "K\u00f6zepes", + "none": "Nincs", + "very_high": "Nagyon magas", + "very_low": "Nagyon alacsony" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "Havas es\u0151", + "ice_pellets": "\u00d3nos es\u0151", + "none": "Nincs", + "rain": "Es\u0151", + "snow": "Havaz\u00e1s" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.id.json b/homeassistant/components/tomorrowio/translations/sensor.id.json new file mode 100644 index 00000000000..6323cf6beef --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.id.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "Bagus", + "hazardous": "Berbahaya", + "moderate": "Sedang", + "unhealthy": "Tidak Sehat", + "unhealthy_for_sensitive_groups": "Tidak Sehat untuk Kelompok Sensitif", + "very_unhealthy": "Sangat Tidak Sehat" + }, + "tomorrowio__pollen_index": { + "high": "Tinggi", + "low": "Rendah", + "medium": "Sedang", + "none": "Tidak Ada", + "very_high": "Sangat Tinggi", + "very_low": "Sangat Rendah" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "Hujan Beku", + "ice_pellets": "Hujan Es", + "none": "Tidak Ada", + "rain": "Hujan", + "snow": "Salju" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.it.json b/homeassistant/components/tomorrowio/translations/sensor.it.json new file mode 100644 index 00000000000..b8f281a462c --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.it.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "Buono", + "hazardous": "Pericoloso", + "moderate": "Moderato", + "unhealthy": "Malsano", + "unhealthy_for_sensitive_groups": "Malsano per i gruppi sensibili", + "very_unhealthy": "Molto malsano" + }, + "tomorrowio__pollen_index": { + "high": "Alto", + "low": "Basso", + "medium": "Medio", + "none": "Nessuno", + "very_high": "Molto alto", + "very_low": "Molto basso" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "Pioggia gelata", + "ice_pellets": "Grandine", + "none": "Nessuna", + "rain": "Pioggia", + "snow": "Neve" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.ja.json b/homeassistant/components/tomorrowio/translations/sensor.ja.json new file mode 100644 index 00000000000..286be339435 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.ja.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "\u826f\u3044", + "hazardous": "\u5371\u967a", + "moderate": "\u9069\u5ea6", + "unhealthy": "\u4e0d\u5065\u5eb7", + "unhealthy_for_sensitive_groups": "\u654f\u611f\u306a\u30b0\u30eb\u30fc\u30d7\u306b\u3068\u3063\u3066\u4e0d\u5065\u5eb7", + "very_unhealthy": "\u3068\u3066\u3082\u4e0d\u5065\u5eb7" + }, + "tomorrowio__pollen_index": { + "high": "\u9ad8", + "low": "\u4f4e", + "medium": "\u4e2d", + "none": "\u306a\u3057", + "very_high": "\u975e\u5e38\u306b\u9ad8\u3044", + "very_low": "\u3068\u3066\u3082\u4f4e\u3044" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "\u51cd\u3066\u3064\u304f\u96e8", + "ice_pellets": "\u51cd\u96e8", + "none": "\u306a\u3057", + "rain": "\u96e8", + "snow": "\u96ea" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.nl.json b/homeassistant/components/tomorrowio/translations/sensor.nl.json new file mode 100644 index 00000000000..5ca31721f94 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.nl.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "Goed", + "hazardous": "Gevaarlijk", + "moderate": "Gematigd", + "unhealthy": "Ongezond", + "unhealthy_for_sensitive_groups": "Ongezond voor gevoelige groepen", + "very_unhealthy": "Zeer ongezond" + }, + "tomorrowio__pollen_index": { + "high": "Hoog", + "low": "Laag", + "medium": "Medium", + "none": "Geen", + "very_high": "Zeer Hoog", + "very_low": "Zeer Laag" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "IJzel", + "ice_pellets": "Hagel", + "none": "Geen", + "rain": "Regen", + "snow": "Sneeuw" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.no.json b/homeassistant/components/tomorrowio/translations/sensor.no.json new file mode 100644 index 00000000000..2eeabe5b569 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.no.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "Bra", + "hazardous": "Farlig", + "moderate": "Moderat", + "unhealthy": "Usunt", + "unhealthy_for_sensitive_groups": "Usunt for sensitive grupper", + "very_unhealthy": "Veldig usunt" + }, + "tomorrowio__pollen_index": { + "high": "H\u00f8y", + "low": "Lav", + "medium": "Medium", + "none": "Ingen", + "very_high": "Veldig h\u00f8y", + "very_low": "Veldig lav" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "Underkj\u00f8lt regn", + "ice_pellets": "Is tapper", + "none": "Ingen", + "rain": "Regn", + "snow": "Sn\u00f8" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.pl.json b/homeassistant/components/tomorrowio/translations/sensor.pl.json new file mode 100644 index 00000000000..7140524fc0b --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.pl.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "dobre", + "hazardous": "niebezpieczne", + "moderate": "umiarkowane", + "unhealthy": "niezdrowe", + "unhealthy_for_sensitive_groups": "niezdrowe dla wra\u017cliwych grup", + "very_unhealthy": "bardzo niezdrowe" + }, + "tomorrowio__pollen_index": { + "high": "wysoki", + "low": "niski", + "medium": "\u015bredni", + "none": "brak", + "very_high": "bardzo wysoki", + "very_low": "bardzo niski" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "marzn\u0105cy deszcz", + "ice_pellets": "granulki lodu", + "none": "brak", + "rain": "deszcz", + "snow": "\u015bnieg" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.pt-BR.json b/homeassistant/components/tomorrowio/translations/sensor.pt-BR.json new file mode 100644 index 00000000000..b63572d731d --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.pt-BR.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "Bom", + "hazardous": "Perigoso", + "moderate": "Moderado", + "unhealthy": "Insalubre", + "unhealthy_for_sensitive_groups": "Insalubre para grupos sens\u00edveis", + "very_unhealthy": "Muito Insalubre" + }, + "tomorrowio__pollen_index": { + "high": "Alto", + "low": "Baixo", + "medium": "M\u00e9dio", + "none": "Nenhum", + "very_high": "Muito alto", + "very_low": "Muito baixo" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "Chuva congelante", + "ice_pellets": "Pelotas de Gelo", + "none": "Nenhum", + "rain": "Chuva", + "snow": "Neve" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.ru.json b/homeassistant/components/tomorrowio/translations/sensor.ru.json new file mode 100644 index 00000000000..f9be80a4bad --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.ru.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "\u0425\u043e\u0440\u043e\u0448\u043e", + "hazardous": "\u041e\u043f\u0430\u0441\u043d\u043e", + "moderate": "\u0421\u0440\u0435\u0434\u043d\u0435", + "unhealthy": "\u0412\u0440\u0435\u0434\u043d\u043e", + "unhealthy_for_sensitive_groups": "\u0412\u0440\u0435\u0434\u043d\u043e \u0434\u043b\u044f \u0443\u044f\u0437\u0432\u0438\u043c\u044b\u0445 \u0433\u0440\u0443\u043f\u043f", + "very_unhealthy": "\u041e\u0447\u0435\u043d\u044c \u0432\u0440\u0435\u0434\u043d\u043e" + }, + "tomorrowio__pollen_index": { + "high": "\u0412\u044b\u0441\u043e\u043a\u0438\u0439", + "low": "\u041d\u0438\u0437\u043a\u0438\u0439", + "medium": "\u0421\u0440\u0435\u0434\u043d\u0438\u0439", + "none": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442", + "very_high": "\u041e\u0447\u0435\u043d\u044c \u0432\u044b\u0441\u043e\u043a\u0438\u0439", + "very_low": "\u041e\u0447\u0435\u043d\u044c \u043d\u0438\u0437\u043a\u0438\u0439" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "\u041b\u0435\u0434\u044f\u043d\u043e\u0439 \u0434\u043e\u0436\u0434\u044c", + "ice_pellets": "\u041b\u0435\u0434\u044f\u043d\u0430\u044f \u043a\u0440\u0443\u043f\u0430", + "none": "\u0411\u0435\u0437 \u043e\u0441\u0430\u0434\u043a\u043e\u0432", + "rain": "\u0414\u043e\u0436\u0434\u044c", + "snow": "\u0421\u043d\u0435\u0433" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.tr.json b/homeassistant/components/tomorrowio/translations/sensor.tr.json new file mode 100644 index 00000000000..553d72f6178 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.tr.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "\u0130yi", + "hazardous": "Tehlikeli", + "moderate": "Il\u0131ml\u0131", + "unhealthy": "Sa\u011fl\u0131ks\u0131z", + "unhealthy_for_sensitive_groups": "Hassas Gruplar \u0130\u00e7in Sa\u011fl\u0131ks\u0131z", + "very_unhealthy": "\u00c7ok Sa\u011fl\u0131ks\u0131z" + }, + "tomorrowio__pollen_index": { + "high": "Y\u00fcksek", + "low": "D\u00fc\u015f\u00fck", + "medium": "Orta", + "none": "Hi\u00e7biri", + "very_high": "\u00c7ok Y\u00fcksek", + "very_low": "\u00c7ok D\u00fc\u015f\u00fck" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "Dondurucu Ya\u011fmur", + "ice_pellets": "Buz Peletleri", + "none": "Hi\u00e7biri", + "rain": "Ya\u011fmur", + "snow": "Kar" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.zh-Hant.json b/homeassistant/components/tomorrowio/translations/sensor.zh-Hant.json new file mode 100644 index 00000000000..6cc5c1a42dd --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.zh-Hant.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "\u826f\u597d", + "hazardous": "\u5371\u96aa", + "moderate": "\u4e2d\u7b49", + "unhealthy": "\u4e0d\u5065\u5eb7", + "unhealthy_for_sensitive_groups": "\u5c0d\u654f\u611f\u65cf\u7fa4\u4e0d\u5065\u5eb7", + "very_unhealthy": "\u975e\u5e38\u4e0d\u5065\u5eb7" + }, + "tomorrowio__pollen_index": { + "high": "\u9ad8", + "low": "\u4f4e", + "medium": "\u4e2d", + "none": "\u7121", + "very_high": "\u6975\u9ad8", + "very_low": "\u6975\u4f4e" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "\u51cd\u96e8", + "ice_pellets": "\u51b0\u73e0", + "none": "\u7121", + "rain": "\u4e0b\u96e8", + "snow": "\u4e0b\u96ea" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/tr.json b/homeassistant/components/tomorrowio/translations/tr.json new file mode 100644 index 00000000000..4193428459c --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/tr.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "rate_limited": "\u015eu anda oran s\u0131n\u0131rl\u0131, l\u00fctfen daha sonra tekrar deneyin.", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "location": "Konum", + "longitude": "Boylam", + "name": "Ad" + }, + "description": "API anahtar\u0131 almak i\u00e7in [Tomorrow.io](https://app.tomorrow.io/signup) adresinden kaydolun." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "NowCast Tahminleri Aras\u0131nda Min." + }, + "description": "\"\u015eimdi yay\u0131n\" tahmin varl\u0131\u011f\u0131n\u0131 etkinle\u015ftirmeyi se\u00e7erseniz, her tahmin aras\u0131ndaki dakika say\u0131s\u0131n\u0131 yap\u0131land\u0131rabilirsiniz. Sa\u011flanan tahminlerin say\u0131s\u0131, tahminler aras\u0131nda se\u00e7ilen dakika say\u0131s\u0131na ba\u011fl\u0131d\u0131r.", + "title": "Tomorrow.io Se\u00e7eneklerini G\u00fcncelleyin" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/zh-Hant.json b/homeassistant/components/tomorrowio/translations/zh-Hant.json new file mode 100644 index 00000000000..bf7043b0225 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/zh-Hant.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_api_key": "API \u91d1\u9470\u7121\u6548", + "rate_limited": "\u9054\u5230\u9650\u5236\u983b\u7387\u3001\u8acb\u7a0d\u5019\u518d\u8a66\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "latitude": "\u7def\u5ea6", + "location": "\u5ea7\u6a19", + "longitude": "\u7d93\u5ea6", + "name": "\u540d\u7a31" + }, + "description": "\u8acb\u53c3\u95b1\u7db2\u5740\u4ee5\u4e86\u89e3\u5982\u4f55\u53d6\u5f97 API \u91d1\u9470\uff1a[Tomorrow.io](https://app.tomorrow.io/signup)\u3002" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "NowCast \u9810\u5831\u9593\u9694\u5206\u9418" + }, + "description": "\u5047\u5982\u9078\u64c7\u958b\u555f `nowcast` \u9810\u5831\u5be6\u9ad4\u3001\u5c07\u53ef\u4ee5\u8a2d\u5b9a\u9810\u5831\u983b\u7387\u9593\u9694\u5206\u9418\u6578\u3002\u6839\u64da\u6240\u8f38\u5165\u7684\u9593\u9694\u6642\u9593\u5c07\u6c7a\u5b9a\u9810\u5831\u7684\u6578\u76ee\u3002", + "title": "\u66f4\u65b0 Tomorrow.io \u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/hu.json b/homeassistant/components/toon/translations/hu.json index b1a69144dd5..a9192ed013a 100644 --- a/homeassistant/components/toon/translations/hu.json +++ b/homeassistant/components/toon/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_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.", + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", "unknown_authorize_url_generation": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n." }, "step": { diff --git a/homeassistant/components/toon/translations/it.json b/homeassistant/components/toon/translations/it.json index 724c61dba3c..9125bac09db 100644 --- a/homeassistant/components/toon/translations/it.json +++ b/homeassistant/components/toon/translations/it.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "L'accordo selezionato \u00e8 gi\u00e0 configurato.", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", - "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", "no_agreements": "Questo account non ha display Toon.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", "unknown_authorize_url_generation": "Errore sconosciuto durante la generazione di un URL di autorizzazione." diff --git a/homeassistant/components/tplink/translations/it.json b/homeassistant/components/tplink/translations/it.json index 3fd30d8d12c..6ab46615373 100644 --- a/homeassistant/components/tplink/translations/it.json +++ b/homeassistant/components/tplink/translations/it.json @@ -25,7 +25,7 @@ "data": { "host": "Host" }, - "description": "Se si lascia vuoto l'host, l'individuazione verr\u00e0 utilizzata per trovare i dispositivi." + "description": "Se si lascia vuoto l'host, l'individuazione sar\u00e0 utilizzata per trovare i dispositivi." } } } diff --git a/homeassistant/components/tradfri/translations/hu.json b/homeassistant/components/tradfri/translations/hu.json index 01bb51b0af3..03a8ddd8fa6 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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve" }, "error": { "cannot_authenticate": "Sikertelen hiteles\u00edt\u00e9s. A Gateway egy m\u00e1sik eszk\u00f6zzel van p\u00e1ros\u00edtva, mint p\u00e9ld\u00e1ul a Homekittel?", diff --git a/homeassistant/components/trafikverket_ferry/translations/de.json b/homeassistant/components/trafikverket_ferry/translations/de.json new file mode 100644 index 00000000000..298a0e1711c --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/de.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "incorrect_api_key": "Ung\u00fcltiger API-Schl\u00fcssel f\u00fcr ausgew\u00e4hltes Konto", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_route": "Konnte keine Route mit den angegebenen Informationen finden" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-Schl\u00fcssel" + } + }, + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "from": "Vom Hafen", + "time": "Zeit", + "to": "Zum Hafen", + "weekday": "Wochentage" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/el.json b/homeassistant/components/trafikverket_ferry/translations/el.json new file mode 100644 index 00000000000..552e8ad0160 --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/el.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "incorrect_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "invalid_route": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae\u03c2 \u03bc\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + } + }, + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "from": "\u0391\u03c0\u03cc \u03c4\u03bf \u039b\u03b9\u03bc\u03ac\u03bd\u03b9", + "time": "\u038f\u03c1\u03b1", + "to": "\u03a0\u03c1\u03bf\u03c2 \u039b\u03b9\u03bc\u03ac\u03bd\u03b9", + "weekday": "\u0395\u03c1\u03b3\u03ac\u03c3\u03b9\u03bc\u03b5\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/et.json b/homeassistant/components/trafikverket_ferry/translations/et.json new file mode 100644 index 00000000000..ec791013788 --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/et.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "incorrect_api_key": "Valitud konto API-v\u00f5ti on kehtetu", + "invalid_auth": "Tuvastamine nurjus", + "invalid_route": "Esitatud teabega marsruuti ei leitud" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API v\u00f5ti" + } + }, + "user": { + "data": { + "api_key": "API v\u00f5ti", + "from": "Sadamast", + "time": "Kellaaeg", + "to": "Sadamasse", + "weekday": "N\u00e4dalap\u00e4ev" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/fr.json b/homeassistant/components/trafikverket_ferry/translations/fr.json new file mode 100644 index 00000000000..4288f56908f --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/fr.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "incorrect_api_key": "Cl\u00e9 d'API non valide pour le compte s\u00e9lectionn\u00e9", + "invalid_auth": "Authentification non valide", + "invalid_route": "Impossible de trouver un itin\u00e9raire avec les informations fournies" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Cl\u00e9 d'API" + } + }, + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "from": "Port de d\u00e9part", + "time": "Heure", + "to": "Port d'arriv\u00e9e", + "weekday": "Jours de la semaine" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/hu.json b/homeassistant/components/trafikverket_ferry/translations/hu.json new file mode 100644 index 00000000000..88b2b2ebfa7 --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/hu.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "incorrect_api_key": "\u00c9rv\u00e9nytelen API-kulcs a megadott fi\u00f3khoz", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_route": "Nem tal\u00e1lhat\u00f3 \u00fatvonal a megadott inform\u00e1ci\u00f3k alapj\u00e1n" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API kulcs" + } + }, + "user": { + "data": { + "api_key": "API kulcs", + "from": "Kik\u00f6t\u0151b\u0151l", + "time": "Id\u0151pont", + "to": "Kik\u00f6t\u0151be", + "weekday": "H\u00e9tk\u00f6znap" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/id.json b/homeassistant/components/trafikverket_ferry/translations/id.json new file mode 100644 index 00000000000..1d476f2c0e1 --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/id.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "incorrect_api_key": "Kunci API tidak valid untuk akun yang dipilih", + "invalid_auth": "Autentikasi tidak valid", + "invalid_route": "Tidak dapat menemukan rute dengan informasi yang ditentukan" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Kunci API" + } + }, + "user": { + "data": { + "api_key": "Kunci API", + "from": "Dari Pelabuhan", + "time": "Waktu", + "to": "Ke Pelabuhan", + "weekday": "Hari kerja" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/it.json b/homeassistant/components/trafikverket_ferry/translations/it.json new file mode 100644 index 00000000000..160953d9f82 --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/it.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "incorrect_api_key": "Chiave API non valida per l'account selezionato", + "invalid_auth": "Autenticazione non valida", + "invalid_route": "Impossibile trovare il percorso con le informazioni fornite" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chiave API" + } + }, + "user": { + "data": { + "api_key": "Chiave API", + "from": "Dal porto", + "time": "Orario", + "to": "Al porto", + "weekday": "Giorni della settimana" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/nl.json b/homeassistant/components/trafikverket_ferry/translations/nl.json new file mode 100644 index 00000000000..f84232839c8 --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/nl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "incorrect_api_key": "Ongeldige API-sleutel voor geselecteerde account", + "invalid_auth": "Ongeldige authenticatie", + "invalid_route": "Kon geen route vinden met verstrekte informatie" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-sleutel" + } + }, + "user": { + "data": { + "api_key": "API-sleutel", + "from": "Van de haven", + "time": "Tijd", + "to": "Naar de haven", + "weekday": "Weekdagen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/no.json b/homeassistant/components/trafikverket_ferry/translations/no.json new file mode 100644 index 00000000000..4cc6286d78f --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/no.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "incorrect_api_key": "Ugyldig API-n\u00f8kkel for valgt konto", + "invalid_auth": "Ugyldig godkjenning", + "invalid_route": "Kunne ikke finne rute med oppgitt informasjon" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-n\u00f8kkel" + } + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "from": "Fra havnen", + "time": "Tid", + "to": "Til havn", + "weekday": "Hverdager" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/pl.json b/homeassistant/components/trafikverket_ferry/translations/pl.json new file mode 100644 index 00000000000..5e7d133b1e5 --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/pl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "incorrect_api_key": "Nieprawid\u0142owy klucz API dla wybranego konta", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_route": "Nie mo\u017cna znale\u017a\u0107 trasy dla podanych informacjami" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Klucz API" + } + }, + "user": { + "data": { + "api_key": "Klucz API", + "from": "Z portu", + "time": "Czas", + "to": "Do portu", + "weekday": "Dni powszednie" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/pt-BR.json b/homeassistant/components/trafikverket_ferry/translations/pt-BR.json new file mode 100644 index 00000000000..e2349c181c8 --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/pt-BR.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "incorrect_api_key": "Chave de API inv\u00e1lida para a conta selecionada", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_route": "N\u00e3o foi poss\u00edvel encontrar a rota com as informa\u00e7\u00f5es fornecidas" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + } + }, + "user": { + "data": { + "api_key": "Chave da API", + "from": "Do porto", + "time": "Tempo", + "to": "Ao porto", + "weekday": "Dias \u00fateis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/ru.json b/homeassistant/components/trafikverket_ferry/translations/ru.json new file mode 100644 index 00000000000..c87b6e941f3 --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/ru.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "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.", + "incorrect_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0434\u043b\u044f \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_route": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 \u0441 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0435\u0439." + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "from": "\u0418\u0437 \u0433\u0430\u0432\u0430\u043d\u0438", + "time": "\u0412\u0440\u0435\u043c\u044f", + "to": "\u0412 \u0433\u0430\u0432\u0430\u043d\u044c", + "weekday": "\u0411\u0443\u0434\u043d\u0438\u0435 \u0434\u043d\u0438" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/zh-Hant.json b/homeassistant/components/trafikverket_ferry/translations/zh-Hant.json new file mode 100644 index 00000000000..9acd15fdfb1 --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/zh-Hant.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "incorrect_api_key": "\u6240\u9078\u64c7\u5e33\u865f\u4e4b API \u91d1\u9470\u7121\u6548", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_route": "\u627e\u4e0d\u5230\u63d0\u4f9b\u8cc7\u8a0a\u4e4b\u8def\u7dda" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u91d1\u9470" + } + }, + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "from": "\u81ea\u6d77\u6e2f", + "time": "\u6642\u9593", + "to": "\u81f3\u6d77\u6e2f", + "weekday": "\u5e73\u65e5" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/bg.json b/homeassistant/components/trafikverket_train/translations/bg.json new file mode 100644 index 00000000000..91bb9c04e35 --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/bg.json @@ -0,0 +1,24 @@ +{ + "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\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" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "weekday": "\u0414\u043d\u0438" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/ca.json b/homeassistant/components/trafikverket_train/translations/ca.json new file mode 100644 index 00000000000..ed9a28841a2 --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/ca.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "incorrect_api_key": "Clau API inv\u00e0lida per al compte seleccionat", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_station": "No s'ha trobat cap estaci\u00f3 amb el nom especificat", + "invalid_time": "Hora proporcionada inv\u00e0lida", + "more_stations": "S'han trobat m\u00faltiples estacions amb el nom especificat" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Clau API" + } + }, + "user": { + "data": { + "api_key": "Clau API", + "from": "Des de l'estaci\u00f3", + "time": "Hora (opcional)", + "to": "A l'estaci\u00f3", + "weekday": "Dies" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/de.json b/homeassistant/components/trafikverket_train/translations/de.json new file mode 100644 index 00000000000..057a416d70c --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/de.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "incorrect_api_key": "Ung\u00fcltiger API-Schl\u00fcssel f\u00fcr ausgew\u00e4hltes Konto", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_station": "Es konnte kein Bahnhof mit dem angegebenen Namen gefunden werden", + "invalid_time": "Ung\u00fcltige Zeitangabe", + "more_stations": "Mehrere Bahnh\u00f6fe mit dem angegebenen Namen gefunden" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-Schl\u00fcssel" + } + }, + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "from": "Vom Bahnhof", + "time": "Zeit (optional)", + "to": "Zum Bahnhof", + "weekday": "Tage" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/el.json b/homeassistant/components/trafikverket_train/translations/el.json new file mode 100644 index 00000000000..1dea0a7a8da --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/el.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "incorrect_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "invalid_station": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd \u03bc\u03b5 \u03c4\u03bf \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1", + "invalid_time": "\u0394\u03cc\u03b8\u03b7\u03ba\u03b5 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03ce\u03c1\u03b1", + "more_stations": "\u0392\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03bf\u03af \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03af \u03bc\u03b5 \u03c4\u03bf \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + } + }, + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "from": "\u0391\u03c0\u03cc \u03c3\u03c4\u03b1\u03b8\u03bc\u03cc", + "time": "\u03a7\u03c1\u03cc\u03bd\u03bf\u03c2 (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "to": "\u03a0\u03c1\u03bf\u03c2 \u03c3\u03c4\u03b1\u03b8\u03bc\u03cc", + "weekday": "\u0397\u03bc\u03ad\u03c1\u03b5\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/en.json b/homeassistant/components/trafikverket_train/translations/en.json index dc91c2097f3..86969fc7d96 100644 --- a/homeassistant/components/trafikverket_train/translations/en.json +++ b/homeassistant/components/trafikverket_train/translations/en.json @@ -6,26 +6,26 @@ }, "error": { "cannot_connect": "Failed to connect", + "incorrect_api_key": "Invalid API key for selected account", "invalid_auth": "Invalid authentication", "invalid_station": "Could not find a station with the specified name", - "more_stations": "Found multiple stations with the specified name", "invalid_time": "Invalid time provided", - "incorrect_api_key": "Invalid API key for selected account" + "more_stations": "Found multiple stations with the specified name" }, "step": { - "user": { - "data": { - "api_key": "API Key", - "to": "To station", - "from": "From station", - "time": "Time (optional)", - "weekday": "Days" - } - }, "reauth_confirm": { "data": { "api_key": "API Key" } + }, + "user": { + "data": { + "api_key": "API Key", + "from": "From station", + "time": "Time (optional)", + "to": "To station", + "weekday": "Days" + } } } } diff --git a/homeassistant/components/trafikverket_train/translations/et.json b/homeassistant/components/trafikverket_train/translations/et.json new file mode 100644 index 00000000000..9712adacce7 --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/et.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "incorrect_api_key": "Valitud konto API-v\u00f5ti on kehtetu", + "invalid_auth": "Tuvastamine nurjus", + "invalid_station": "M\u00e4\u00e4ratud nimega jaama ei leitud.", + "invalid_time": "Esitatud on kehtetu aeg", + "more_stations": "Leiti mitu m\u00e4\u00e4ratud nimega jaama" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API v\u00f5ti" + } + }, + "user": { + "data": { + "api_key": "API v\u00f5ti", + "from": "L\u00e4htejaam", + "time": "Aeg (valikuline)", + "to": "Sihtjaam", + "weekday": "N\u00e4dalap\u00e4evad" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/fr.json b/homeassistant/components/trafikverket_train/translations/fr.json new file mode 100644 index 00000000000..357edca46fa --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/fr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "incorrect_api_key": "Cl\u00e9 d'API non valide pour le compte s\u00e9lectionn\u00e9", + "invalid_auth": "Authentification non valide", + "invalid_station": "Aucune gare portant le nom sp\u00e9cifi\u00e9 n'a \u00e9t\u00e9 trouv\u00e9e", + "invalid_time": "Heure fournie non valide", + "more_stations": "Plusieurs gares portant le nom sp\u00e9cifi\u00e9 ont \u00e9t\u00e9 trouv\u00e9es" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Cl\u00e9 d'API" + } + }, + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "from": "Gare de d\u00e9part", + "time": "Heure (facultatif)", + "to": "Gare d'arriv\u00e9e", + "weekday": "Jours" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/he.json b/homeassistant/components/trafikverket_train/translations/he.json new file mode 100644 index 00000000000..29f4fc720da --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/he.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + }, + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/hu.json b/homeassistant/components/trafikverket_train/translations/hu.json new file mode 100644 index 00000000000..ff4a4ee7142 --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/hu.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "incorrect_api_key": "\u00c9rv\u00e9nytelen API-kulcs a megadott fi\u00f3khoz", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_station": "Nem tal\u00e1lhat\u00f3 megadott nev\u0171 \u00e1llom\u00e1s", + "invalid_time": "\u00c9rv\u00e9nytelen megadott id\u0151", + "more_stations": "T\u00f6bb \u00e1llom\u00e1s tal\u00e1lhat\u00f3 a megadott n\u00e9vvel" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API kulcs" + } + }, + "user": { + "data": { + "api_key": "API kulcs", + "from": "\u00c1llom\u00e1sr\u00f3l", + "time": "Id\u0151 (opcion\u00e1lis)", + "to": "\u00c1llom\u00e1sig", + "weekday": "Napok" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/id.json b/homeassistant/components/trafikverket_train/translations/id.json new file mode 100644 index 00000000000..4723183257c --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/id.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "incorrect_api_key": "Kunci API tidak valid untuk akun yang dipilih", + "invalid_auth": "Autentikasi tidak valid", + "invalid_station": "Tidak dapat menemukan SPBU dengan nama yang ditentukan", + "invalid_time": "Waktu yang disediakan tidak valid", + "more_stations": "Ditemukan beberapa SPBU dengan nama yang ditentukan" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Kunci API" + } + }, + "user": { + "data": { + "api_key": "Kunci API", + "from": "Dari stasiun", + "time": "Waktu (opsional)", + "to": "Ke stasiun", + "weekday": "Hari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/it.json b/homeassistant/components/trafikverket_train/translations/it.json new file mode 100644 index 00000000000..fe9c37e8e17 --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/it.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "incorrect_api_key": "Chiave API non valida per l'account selezionato", + "invalid_auth": "Autenticazione non valida", + "invalid_station": "Impossibile trovare una stazione con il nome specificato", + "invalid_time": "Tempo fornito non valido", + "more_stations": "Trovate pi\u00f9 stazioni con il nome specificato" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chiave API" + } + }, + "user": { + "data": { + "api_key": "Chiave API", + "from": "Dalla stazione", + "time": "Tempo (facoltativo)", + "to": "Alla stazione", + "weekday": "Tariffe supportate" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/ja.json b/homeassistant/components/trafikverket_train/translations/ja.json new file mode 100644 index 00000000000..ec4b5a4015a --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/ja.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "incorrect_api_key": "\u9078\u629e\u3057\u305f\u30a2\u30ab\u30a6\u30f3\u30c8\u306eAPI\u30ad\u30fc\u304c\u7121\u52b9\u3067\u3059", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_station": "\u6307\u5b9a\u3055\u308c\u305f\u540d\u524d\u306e\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f", + "invalid_time": "\u7121\u52b9\u306a\u6642\u9593\u304c\u6307\u5b9a\u3055\u308c\u307e\u3057\u305f", + "more_stations": "\u6307\u5b9a\u3055\u308c\u305f\u540d\u524d\u306e\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u3092\u8907\u6570\u767a\u898b" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API\u30ad\u30fc" + } + }, + "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "from": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u304b\u3089", + "time": "\u6642\u9593\uff08\u30aa\u30d7\u30b7\u30e7\u30f3\uff09", + "to": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u3078", + "weekday": "\u65e5" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/nl.json b/homeassistant/components/trafikverket_train/translations/nl.json new file mode 100644 index 00000000000..d076f8b2961 --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/nl.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "incorrect_api_key": "Ongeldige API-sleutel voor geselecteerd account", + "invalid_auth": "Ongeldige authenticatie", + "invalid_station": "Kon geen station vinden met de opgegeven naam", + "invalid_time": "Ongeldige tijd opgegeven", + "more_stations": "Meerdere stations gevonden met de opgegeven naam" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-sleutel" + } + }, + "user": { + "data": { + "api_key": "API-sleutel", + "from": "Van station", + "time": "Tijd (optioneel)", + "to": "Naar station", + "weekday": "Dagen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/no.json b/homeassistant/components/trafikverket_train/translations/no.json new file mode 100644 index 00000000000..12feb2f6abf --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/no.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "incorrect_api_key": "Ugyldig API-n\u00f8kkel for valgt konto", + "invalid_auth": "Ugyldig godkjenning", + "invalid_station": "Kunne ikke finne en stasjon med det angitte navnet", + "invalid_time": "Ugyldig tid oppgitt", + "more_stations": "Fant flere stasjoner med det angitte navnet" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-n\u00f8kkel" + } + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "from": "Fra stasjon", + "time": "Tid (valgfritt)", + "to": "Til stasjon", + "weekday": "Dager" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/pl.json b/homeassistant/components/trafikverket_train/translations/pl.json new file mode 100644 index 00000000000..0ff0f49c701 --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/pl.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "incorrect_api_key": "Nieprawid\u0142owy klucz API dla wybranego konta", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_station": "Nie mo\u017cna znale\u017a\u0107 stacji o podanej nazwie", + "invalid_time": "Podano nieprawid\u0142owy czas", + "more_stations": "Znaleziono wiele stacji o podanej nazwie" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Klucz API" + } + }, + "user": { + "data": { + "api_key": "Klucz API", + "from": "Ze stacji", + "time": "Czas (opcjonalnie)", + "to": "Do stacji", + "weekday": "Dni" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/pt-BR.json b/homeassistant/components/trafikverket_train/translations/pt-BR.json new file mode 100644 index 00000000000..43dcf558a18 --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/pt-BR.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 foi configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "incorrect_api_key": "Chave de API inv\u00e1lida para a conta selecionada", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_station": "N\u00e3o foi poss\u00edvel encontrar uma esta\u00e7\u00e3o com o nome especificado", + "invalid_time": "Tempo inv\u00e1lido fornecido", + "more_stations": "Encontrado v\u00e1rias esta\u00e7\u00f5es com o nome especificado" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + } + }, + "user": { + "data": { + "api_key": "Chave da API", + "from": "Da esta\u00e7\u00e3o", + "time": "Tempo (opcional)", + "to": "Para esta\u00e7\u00e3o", + "weekday": "Dias" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/ru.json b/homeassistant/components/trafikverket_train/translations/ru.json new file mode 100644 index 00000000000..27a0b1d6393 --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/ru.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "incorrect_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0434\u043b\u044f \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_station": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u044e \u0441 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u043c \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c.", + "invalid_time": "\u0423\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f.", + "more_stations": "\u041d\u0430\u0439\u0434\u0435\u043d\u043e \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u0430\u043d\u0446\u0438\u0439 \u0441 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u043c \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c." + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "from": "\u041e\u0442 \u0441\u0442\u0430\u043d\u0446\u0438\u0438", + "time": "\u0412\u0440\u0435\u043c\u044f (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "to": "\u041d\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044e", + "weekday": "\u0414\u043d\u0438" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/tr.json b/homeassistant/components/trafikverket_train/translations/tr.json new file mode 100644 index 00000000000..9e7f21b5685 --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/tr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "incorrect_api_key": "Se\u00e7ilen hesap i\u00e7in ge\u00e7ersiz API anahtar\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_station": "Belirtilen ada sahip bir istasyon bulunamad\u0131", + "invalid_time": "Ge\u00e7ersiz s\u00fcre sa\u011fland\u0131", + "more_stations": "Belirtilen ada sahip birden fazla istasyon bulundu" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API Anahtar\u0131" + } + }, + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "from": "\u0130stasyondan", + "time": "Zaman (iste\u011fe ba\u011fl\u0131)", + "to": "\u0130stasyona", + "weekday": "G\u00fcn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/zh-Hant.json b/homeassistant/components/trafikverket_train/translations/zh-Hant.json new file mode 100644 index 00000000000..be3d51c19fd --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/zh-Hant.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "incorrect_api_key": "\u6240\u9078\u64c7\u5e33\u865f\u4e4b API \u91d1\u9470\u7121\u6548", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_station": "\u627e\u4e0d\u5230\u8a72\u6307\u5b9a\u540d\u7a31\u4e4b\u8eca\u7ad9", + "invalid_time": "\u63d0\u4f9b\u6642\u9593\u7121\u6548", + "more_stations": "\u6307\u5b9a\u540d\u7a31\u627e\u5230\u591a\u500b\u8eca\u7ad9" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u91d1\u9470" + } + }, + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "from": "\u51fa\u767c\u52a0\u6cb9\u7ad9", + "time": "\u6642\u9593\uff08\u9078\u9805\uff09", + "to": "\u62b5\u9054\u52a0\u6cb9\u7ad9", + "weekday": "\u5929" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/hu.json b/homeassistant/components/transmission/translations/hu.json index 5e3dcfd2b6c..79a60dc2b5b 100644 --- a/homeassistant/components/transmission/translations/hu.json +++ b/homeassistant/components/transmission/translations/hu.json @@ -12,7 +12,7 @@ "user": { "data": { "host": "C\u00edm", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3", "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" diff --git a/homeassistant/components/tuya/translations/select.he.json b/homeassistant/components/tuya/translations/select.he.json index f31e63515c8..5a29489bdcc 100644 --- a/homeassistant/components/tuya/translations/select.he.json +++ b/homeassistant/components/tuya/translations/select.he.json @@ -98,7 +98,7 @@ "power_on": "\u05de\u05d5\u05e4\u05e2\u05dc" }, "tuya__vacuum_cistern": { - "closed": "\u05e1\u05d2\u05d5\u05e8", + "closed": "\u05e0\u05e1\u05d2\u05e8", "high": "\u05d2\u05d1\u05d5\u05d4", "low": "\u05e0\u05de\u05d5\u05da", "middle": "\u05d0\u05de\u05e6\u05e2" diff --git a/homeassistant/components/tuya/translations/select.pt-BR.json b/homeassistant/components/tuya/translations/select.pt-BR.json index aed86dfe4ce..90ae768c418 100644 --- a/homeassistant/components/tuya/translations/select.pt-BR.json +++ b/homeassistant/components/tuya/translations/select.pt-BR.json @@ -109,19 +109,19 @@ "small": "Pequeno" }, "tuya__vacuum_mode": { - "bow": "", + "bow": "Bow", "chargego": "Retornar para Base", - "left_bow": "", - "left_spiral": "", + "left_bow": "Bow Left", + "left_spiral": "Spiral Left", "mop": "Esfregar (Mop)", "part": "Parcial", - "partial_bow": "", + "partial_bow": "Bow Partially", "pick_zone": "C\u00f4modos Selecionados", "point": "Ponto", "pose": "Ponto Definido", "random": "Aleat\u00f3rio", - "right_bow": "", - "right_spiral": "", + "right_bow": "Bow Right", + "right_spiral": "Spiral Right", "single": "Simples", "smart": "Autom\u00e1tica", "spiral": "Espiral", diff --git a/homeassistant/components/twentemilieu/translations/bg.json b/homeassistant/components/twentemilieu/translations/bg.json index 8844b6124e0..6ddc25a367e 100644 --- a/homeassistant/components/twentemilieu/translations/bg.json +++ b/homeassistant/components/twentemilieu/translations/bg.json @@ -10,8 +10,7 @@ "house_number": "\u041d\u043e\u043c\u0435\u0440 \u043d\u0430 \u043a\u044a\u0449\u0430", "post_code": "\u041f\u043e\u0449\u0435\u043d\u0441\u043a\u0438 \u043a\u043e\u0434" }, - "description": "\u0421\u044a\u0437\u0434\u0430\u0439\u0442\u0435 Twente Milieu, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u044f\u0449\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 \u0441\u044a\u0431\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043e\u0442\u043f\u0430\u0434\u044a\u0446\u0438 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u0430\u0434\u0440\u0435\u0441.", - "title": "Twente Milieu" + "description": "\u0421\u044a\u0437\u0434\u0430\u0439\u0442\u0435 Twente Milieu, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u044f\u0449\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0437\u0430 \u0441\u044a\u0431\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043e\u0442\u043f\u0430\u0434\u044a\u0446\u0438 \u043d\u0430 \u0432\u0430\u0448\u0438\u044f \u0430\u0434\u0440\u0435\u0441." } } } diff --git a/homeassistant/components/twentemilieu/translations/ca.json b/homeassistant/components/twentemilieu/translations/ca.json index 6c1469f16c1..c9a3b0bb6cb 100644 --- a/homeassistant/components/twentemilieu/translations/ca.json +++ b/homeassistant/components/twentemilieu/translations/ca.json @@ -14,8 +14,7 @@ "house_number": "N\u00famero de casa", "post_code": "Codi postal" }, - "description": "Configura Twente Milieu amb informaci\u00f3 de la recollida de residus a la teva adre\u00e7a.", - "title": "Twente Milieu" + "description": "Configura Twente Milieu amb informaci\u00f3 de la recollida de residus a la teva adre\u00e7a." } } } diff --git a/homeassistant/components/twentemilieu/translations/cs.json b/homeassistant/components/twentemilieu/translations/cs.json index 2eb6e267b2c..edcb6a22594 100644 --- a/homeassistant/components/twentemilieu/translations/cs.json +++ b/homeassistant/components/twentemilieu/translations/cs.json @@ -12,8 +12,7 @@ "house_letter": "Roz\u0161\u00ed\u0159en\u00ed ozna\u010den\u00ed \u010d\u00edsla domu", "house_number": "\u010c\u00edslo domu", "post_code": "PS\u010c" - }, - "title": "Twente Milieu" + } } } } diff --git a/homeassistant/components/twentemilieu/translations/da.json b/homeassistant/components/twentemilieu/translations/da.json index e1c9b6024ad..6930a37adb4 100644 --- a/homeassistant/components/twentemilieu/translations/da.json +++ b/homeassistant/components/twentemilieu/translations/da.json @@ -10,8 +10,7 @@ "house_number": "Husnummer", "post_code": "Postnummer" }, - "description": "Konfigurer Twente Milieu, der leverer oplysninger om indsamling af affald p\u00e5 din adresse.", - "title": "Twente Milieu" + "description": "Konfigurer Twente Milieu, der leverer oplysninger om indsamling af affald p\u00e5 din adresse." } } } diff --git a/homeassistant/components/twentemilieu/translations/de.json b/homeassistant/components/twentemilieu/translations/de.json index 4ce9ed23ea7..36ea2123bdb 100644 --- a/homeassistant/components/twentemilieu/translations/de.json +++ b/homeassistant/components/twentemilieu/translations/de.json @@ -14,8 +14,7 @@ "house_number": "Hausnummer", "post_code": "Postleitzahl" }, - "description": "Richte Twente Milieu mit Informationen zur Abfallsammlung unter deiner Adresse ein.", - "title": "Twente Milieu" + "description": "Richte Twente Milieu mit Informationen zur Abfallsammlung unter deiner Adresse ein." } } } diff --git a/homeassistant/components/twentemilieu/translations/el.json b/homeassistant/components/twentemilieu/translations/el.json index f4949d3832d..d8cdd9672bd 100644 --- a/homeassistant/components/twentemilieu/translations/el.json +++ b/homeassistant/components/twentemilieu/translations/el.json @@ -14,8 +14,7 @@ "house_number": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c3\u03c0\u03b9\u03c4\u03b9\u03bf\u03cd", "post_code": "\u03a4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b9\u03ba\u03cc\u03c2 \u03ba\u03ce\u03b4\u03b9\u03ba\u03b1\u03c2" }, - "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf Twente Milieu \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae \u03b1\u03c0\u03bf\u03b2\u03bb\u03ae\u03c4\u03c9\u03bd \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03ae \u03c3\u03b1\u03c2.", - "title": "Twente Milieu" + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf Twente Milieu \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03bb\u03bb\u03bf\u03b3\u03ae \u03b1\u03c0\u03bf\u03b2\u03bb\u03ae\u03c4\u03c9\u03bd \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03ae \u03c3\u03b1\u03c2." } } } diff --git a/homeassistant/components/twentemilieu/translations/en.json b/homeassistant/components/twentemilieu/translations/en.json index fdc4023f89b..ac5f9e54b2e 100644 --- a/homeassistant/components/twentemilieu/translations/en.json +++ b/homeassistant/components/twentemilieu/translations/en.json @@ -14,8 +14,7 @@ "house_number": "House number", "post_code": "Postal code" }, - "description": "Set up Twente Milieu providing waste collection information on your address.", - "title": "Twente Milieu" + "description": "Set up Twente Milieu providing waste collection information on your address." } } } diff --git a/homeassistant/components/twentemilieu/translations/es-419.json b/homeassistant/components/twentemilieu/translations/es-419.json index 8adddffd407..7eb541978d3 100644 --- a/homeassistant/components/twentemilieu/translations/es-419.json +++ b/homeassistant/components/twentemilieu/translations/es-419.json @@ -10,8 +10,7 @@ "house_number": "N\u00famero de casa", "post_code": "C\u00f3digo postal" }, - "description": "Configure Twente Milieu proporcionando informaci\u00f3n de recolecci\u00f3n de residuos en su direcci\u00f3n.", - "title": "Twente Milieu" + "description": "Configure Twente Milieu proporcionando informaci\u00f3n de recolecci\u00f3n de residuos en su direcci\u00f3n." } } } diff --git a/homeassistant/components/twentemilieu/translations/es.json b/homeassistant/components/twentemilieu/translations/es.json index cf8410c55ad..259d202a9c3 100644 --- a/homeassistant/components/twentemilieu/translations/es.json +++ b/homeassistant/components/twentemilieu/translations/es.json @@ -14,8 +14,7 @@ "house_number": "N\u00famero de casa", "post_code": "C\u00f3digo postal" }, - "description": "Configure Twente Milieu proporcionando informaci\u00f3n sobre la recolecci\u00f3n de residuos en su direcci\u00f3n.", - "title": "Twente Milieu" + "description": "Configure Twente Milieu proporcionando informaci\u00f3n sobre la recolecci\u00f3n de residuos en su direcci\u00f3n." } } } diff --git a/homeassistant/components/twentemilieu/translations/et.json b/homeassistant/components/twentemilieu/translations/et.json index a77657d19ff..31c8ea6ee95 100644 --- a/homeassistant/components/twentemilieu/translations/et.json +++ b/homeassistant/components/twentemilieu/translations/et.json @@ -14,8 +14,7 @@ "house_number": "Maja number", "post_code": "Sihtnumber" }, - "description": "Seadista Twente Milieu, mis pakub j\u00e4\u00e4tmekogumist teie aadressil.", - "title": "" + "description": "Seadista Twente Milieu, mis pakub j\u00e4\u00e4tmekogumist teie aadressil." } } } diff --git a/homeassistant/components/twentemilieu/translations/fr.json b/homeassistant/components/twentemilieu/translations/fr.json index 6530216a5a4..560fcb89c29 100644 --- a/homeassistant/components/twentemilieu/translations/fr.json +++ b/homeassistant/components/twentemilieu/translations/fr.json @@ -14,8 +14,7 @@ "house_number": "Num\u00e9ro de maison", "post_code": "Code postal" }, - "description": "Configurez Twente Milieu en fournissant des informations sur la collecte des d\u00e9chets sur votre adresse.", - "title": "Twente Milieu" + "description": "Configurez Twente Milieu en fournissant des informations sur la collecte des d\u00e9chets sur votre adresse." } } } diff --git a/homeassistant/components/twentemilieu/translations/hu.json b/homeassistant/components/twentemilieu/translations/hu.json index 637dadb5baf..97a0cc8f022 100644 --- a/homeassistant/components/twentemilieu/translations/hu.json +++ b/homeassistant/components/twentemilieu/translations/hu.json @@ -14,8 +14,7 @@ "house_number": "h\u00e1zsz\u00e1m", "post_code": "ir\u00e1ny\u00edt\u00f3sz\u00e1m" }, - "description": "\u00c1ll\u00edtsa be a Twente Milieu szolg\u00e1ltat\u00e1st, amely hullad\u00e9kgy\u0171jt\u00e9si inform\u00e1ci\u00f3kat biztos\u00edt a c\u00edm\u00e9re.", - "title": "Twente Milieu" + "description": "\u00c1ll\u00edtsa be a Twente Milieu szolg\u00e1ltat\u00e1st, amely hullad\u00e9kgy\u0171jt\u00e9si inform\u00e1ci\u00f3kat biztos\u00edt a c\u00edm\u00e9re." } } } diff --git a/homeassistant/components/twentemilieu/translations/id.json b/homeassistant/components/twentemilieu/translations/id.json index 38746dfd12f..60148dc396c 100644 --- a/homeassistant/components/twentemilieu/translations/id.json +++ b/homeassistant/components/twentemilieu/translations/id.json @@ -14,8 +14,7 @@ "house_number": "Nomor rumah", "post_code": "Kode pos" }, - "description": "Siapkan Twente Milieu untuk memberikan informasi pengumpulan sampah di alamat Anda.", - "title": "Twente Milieu" + "description": "Siapkan Twente Milieu untuk memberikan informasi pengumpulan sampah di alamat Anda." } } } diff --git a/homeassistant/components/twentemilieu/translations/it.json b/homeassistant/components/twentemilieu/translations/it.json index a374885e7aa..7648d24b5e6 100644 --- a/homeassistant/components/twentemilieu/translations/it.json +++ b/homeassistant/components/twentemilieu/translations/it.json @@ -14,8 +14,7 @@ "house_number": "Numero civico", "post_code": "CAP" }, - "description": "Imposta Twente Milieu fornendo le informazioni sulla raccolta dei rifiuti al tuo indirizzo.", - "title": "Twente Milieu" + "description": "Imposta Twente Milieu fornendo le informazioni sulla raccolta dei rifiuti al tuo indirizzo." } } } diff --git a/homeassistant/components/twentemilieu/translations/ja.json b/homeassistant/components/twentemilieu/translations/ja.json index 8ec65b24fb5..5cd78fbba3c 100644 --- a/homeassistant/components/twentemilieu/translations/ja.json +++ b/homeassistant/components/twentemilieu/translations/ja.json @@ -14,8 +14,7 @@ "house_number": "\u5bb6\u5c4b\u756a\u53f7", "post_code": "\u90f5\u4fbf\u756a\u53f7" }, - "description": "\u3042\u306a\u305f\u306e\u4f4f\u6240\u306eTwente Milieu providing waste collection(\u30b4\u30df\u53ce\u96c6\u60c5\u5831)\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7", - "title": "Twente Milieu" + "description": "\u3042\u306a\u305f\u306e\u4f4f\u6240\u306eTwente Milieu providing waste collection(\u30b4\u30df\u53ce\u96c6\u60c5\u5831)\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/twentemilieu/translations/ko.json b/homeassistant/components/twentemilieu/translations/ko.json index a27df565f1b..985650ff15b 100644 --- a/homeassistant/components/twentemilieu/translations/ko.json +++ b/homeassistant/components/twentemilieu/translations/ko.json @@ -14,8 +14,7 @@ "house_number": "\uc9d1 \ubc88\ud638", "post_code": "\uc6b0\ud3b8\ubc88\ud638" }, - "description": "\uc8fc\uc18c\uc5d0 \uc4f0\ub808\uae30 \uc218\uac70 \uc815\ubcf4\ub97c \ub123\uc5b4 Twente Milieu \ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694.", - "title": "Twente Milieu" + "description": "\uc8fc\uc18c\uc5d0 \uc4f0\ub808\uae30 \uc218\uac70 \uc815\ubcf4\ub97c \ub123\uc5b4 Twente Milieu \ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694." } } } diff --git a/homeassistant/components/twentemilieu/translations/lb.json b/homeassistant/components/twentemilieu/translations/lb.json index 98454f62dbb..ca1591417e7 100644 --- a/homeassistant/components/twentemilieu/translations/lb.json +++ b/homeassistant/components/twentemilieu/translations/lb.json @@ -14,8 +14,7 @@ "house_number": "Haus Nummer", "post_code": "Postleitzuel" }, - "description": "Offallsammlung Informatiounen vun Twente Milieu zu \u00e4erer Adresse ariichten.", - "title": "Twente Milieu" + "description": "Offallsammlung Informatiounen vun Twente Milieu zu \u00e4erer Adresse ariichten." } } } diff --git a/homeassistant/components/twentemilieu/translations/nl.json b/homeassistant/components/twentemilieu/translations/nl.json index 54611aa9ab8..575c642a777 100644 --- a/homeassistant/components/twentemilieu/translations/nl.json +++ b/homeassistant/components/twentemilieu/translations/nl.json @@ -14,8 +14,7 @@ "house_number": "Huisnummer", "post_code": "Postcode" }, - "description": "Stel Twente Milieu in voor het inzamelen van afval op uw adres.", - "title": "Twente Milieu" + "description": "Stel Twente Milieu in voor het inzamelen van afval op uw adres." } } } diff --git a/homeassistant/components/twentemilieu/translations/nn.json b/homeassistant/components/twentemilieu/translations/nn.json deleted file mode 100644 index ed333bb9b51..00000000000 --- a/homeassistant/components/twentemilieu/translations/nn.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "Twente Milieu" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/translations/no.json b/homeassistant/components/twentemilieu/translations/no.json index ea7df786247..76a023e44e3 100644 --- a/homeassistant/components/twentemilieu/translations/no.json +++ b/homeassistant/components/twentemilieu/translations/no.json @@ -14,8 +14,7 @@ "house_number": "Husnummer", "post_code": "Postnummer" }, - "description": "Sett opp Twente Milieu som gir informasjon om innsamling av avfall p\u00e5 adressen din.", - "title": "" + "description": "Sett opp Twente Milieu som gir informasjon om innsamling av avfall p\u00e5 adressen din." } } } diff --git a/homeassistant/components/twentemilieu/translations/pl.json b/homeassistant/components/twentemilieu/translations/pl.json index 67f2d2b362b..d7f53bb9bf1 100644 --- a/homeassistant/components/twentemilieu/translations/pl.json +++ b/homeassistant/components/twentemilieu/translations/pl.json @@ -14,8 +14,7 @@ "house_number": "Numer domu", "post_code": "Kod pocztowy" }, - "description": "Skonfiguruj Twente Milieu, dostarczaj\u0105c informacji o zbieraniu odpad\u00f3w pod swoim adresem.", - "title": "Twente Milieu" + "description": "Skonfiguruj Twente Milieu, dostarczaj\u0105c informacji o zbieraniu odpad\u00f3w pod swoim adresem." } } } diff --git a/homeassistant/components/twentemilieu/translations/pt-BR.json b/homeassistant/components/twentemilieu/translations/pt-BR.json index 943b46849b6..8a7fa74b554 100644 --- a/homeassistant/components/twentemilieu/translations/pt-BR.json +++ b/homeassistant/components/twentemilieu/translations/pt-BR.json @@ -14,8 +14,7 @@ "house_number": "N\u00famero da casa", "post_code": "C\u00f3digo postal" }, - "description": "Configure o Twente Milieu, fornecendo informa\u00e7\u00f5es de coleta de lixo em seu endere\u00e7o.", - "title": "Twente Milieu" + "description": "Configure o Twente Milieu, fornecendo informa\u00e7\u00f5es de coleta de lixo em seu endere\u00e7o." } } } diff --git a/homeassistant/components/twentemilieu/translations/ru.json b/homeassistant/components/twentemilieu/translations/ru.json index f7da9b628fd..33748e9d16b 100644 --- a/homeassistant/components/twentemilieu/translations/ru.json +++ b/homeassistant/components/twentemilieu/translations/ru.json @@ -14,8 +14,7 @@ "house_number": "\u041d\u043e\u043c\u0435\u0440 \u0434\u043e\u043c\u0430", "post_code": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Twente Milieu \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u0432\u044b\u0432\u043e\u0437\u0435 \u043c\u0443\u0441\u043e\u0440\u0430 \u043f\u043e \u0412\u0430\u0448\u0435\u043c\u0443 \u0430\u0434\u0440\u0435\u0441\u0443.", - "title": "Twente Milieu" + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Twente Milieu \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u0432\u044b\u0432\u043e\u0437\u0435 \u043c\u0443\u0441\u043e\u0440\u0430 \u043f\u043e \u0412\u0430\u0448\u0435\u043c\u0443 \u0430\u0434\u0440\u0435\u0441\u0443." } } } diff --git a/homeassistant/components/twentemilieu/translations/sl.json b/homeassistant/components/twentemilieu/translations/sl.json index affe8269613..313a5ae5a87 100644 --- a/homeassistant/components/twentemilieu/translations/sl.json +++ b/homeassistant/components/twentemilieu/translations/sl.json @@ -10,8 +10,7 @@ "house_number": "Hi\u0161na \u0161tevilka", "post_code": "Po\u0161tna \u0161tevilka" }, - "description": "Nastavite Twente milieu, ki zagotavlja informacije o zbiranju odpadkov na va\u0161em naslovu.", - "title": "Twente Milieu" + "description": "Nastavite Twente milieu, ki zagotavlja informacije o zbiranju odpadkov na va\u0161em naslovu." } } } diff --git a/homeassistant/components/twentemilieu/translations/sv.json b/homeassistant/components/twentemilieu/translations/sv.json index fa1481f7e10..4e8bb592d05 100644 --- a/homeassistant/components/twentemilieu/translations/sv.json +++ b/homeassistant/components/twentemilieu/translations/sv.json @@ -10,8 +10,7 @@ "house_number": "Husnummer", "post_code": "Postnummer" }, - "description": "St\u00e4ll in Twente Milieu som ger information om avfallshantering p\u00e5 din adress.", - "title": "Twente Milieu" + "description": "St\u00e4ll in Twente Milieu som ger information om avfallshantering p\u00e5 din adress." } } } diff --git a/homeassistant/components/twentemilieu/translations/tr.json b/homeassistant/components/twentemilieu/translations/tr.json index 363128d9c1e..9f03d2c9189 100644 --- a/homeassistant/components/twentemilieu/translations/tr.json +++ b/homeassistant/components/twentemilieu/translations/tr.json @@ -14,8 +14,7 @@ "house_number": "Ev numaras\u0131", "post_code": "Posta kodu" }, - "description": "Adresinizde at\u0131k toplama bilgileri sa\u011flayan Twente Milieu'yu kurun.", - "title": "Twente Milieu" + "description": "Adresinizde at\u0131k toplama bilgileri sa\u011flayan Twente Milieu'yu kurun." } } } diff --git a/homeassistant/components/twentemilieu/translations/uk.json b/homeassistant/components/twentemilieu/translations/uk.json index 435bd79fb85..5fa562a9afa 100644 --- a/homeassistant/components/twentemilieu/translations/uk.json +++ b/homeassistant/components/twentemilieu/translations/uk.json @@ -14,8 +14,7 @@ "house_number": "\u041d\u043e\u043c\u0435\u0440 \u0431\u0443\u0434\u0438\u043d\u043a\u0443", "post_code": "\u041f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441" }, - "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Twente Milieu \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0432\u0438\u0432\u0435\u0437\u0435\u043d\u043d\u044f \u0441\u043c\u0456\u0442\u0442\u044f \u0437\u0430 \u0412\u0430\u0448\u043e\u044e \u0430\u0434\u0440\u0435\u0441\u043e\u044e.", - "title": "Twente Milieu" + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Twente Milieu \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0432\u0438\u0432\u0435\u0437\u0435\u043d\u043d\u044f \u0441\u043c\u0456\u0442\u0442\u044f \u0437\u0430 \u0412\u0430\u0448\u043e\u044e \u0430\u0434\u0440\u0435\u0441\u043e\u044e." } } } diff --git a/homeassistant/components/twentemilieu/translations/zh-Hant.json b/homeassistant/components/twentemilieu/translations/zh-Hant.json index 11cf62d2d60..51373552418 100644 --- a/homeassistant/components/twentemilieu/translations/zh-Hant.json +++ b/homeassistant/components/twentemilieu/translations/zh-Hant.json @@ -14,8 +14,7 @@ "house_number": "\u9580\u724c\u865f\u78bc", "post_code": "\u90f5\u905e\u5340\u865f" }, - "description": "\u8a2d\u5b9a Twente Milieu \u4ee5\u53d6\u5f97\u8a72\u5730\u5740\u5ee2\u68c4\u7269\u56de\u6536\u8cc7\u8a0a\u3002", - "title": "Twente Milieu" + "description": "\u8a2d\u5b9a Twente Milieu \u4ee5\u53d6\u5f97\u8a72\u5730\u5740\u5ee2\u68c4\u7269\u56de\u6536\u8cc7\u8a0a\u3002" } } } diff --git a/homeassistant/components/twilio/translations/cs.json b/homeassistant/components/twilio/translations/cs.json index f45c4bf881e..8572109edcd 100644 --- a/homeassistant/components/twilio/translations/cs.json +++ b/homeassistant/components/twilio/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Nen\u00ed p\u0159ipojeno k Home Assistant Cloud.", "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace.", "webhook_not_internet_accessible": "V\u00e1\u0161 Home Assistant mus\u00ed b\u00fdt p\u0159\u00edstupn\u00fd z internetu, aby mohl p\u0159ij\u00edmat zpr\u00e1vy webhook." }, diff --git a/homeassistant/components/twilio/translations/id.json b/homeassistant/components/twilio/translations/id.json index 06a77bc974e..6c447e22f63 100644 --- a/homeassistant/components/twilio/translations/id.json +++ b/homeassistant/components/twilio/translations/id.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Instans Home Assistant Anda harus dapat diakses dari internet untuk menerima pesan webhook." }, "create_entry": { - "default": "Untuk mengirim event ke Home Assistant, Anda harus menyiapkan [Webhooks dengan Twilio]({twilio_url}).\n\nIsikan info berikut:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content-Type: application/x-www-form-urlencoded\n\nBaca [dokumentasi]({docs_url}) tentang cara mengonfigurasi otomasi untuk menangani data masuk." + "default": "Untuk mengirim event ke Home Assistant, Anda harus menyiapkan [Webhooks dengan Twilio]({twilio_url}).\n\nIsi info berikut:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content-Type: application/x-www-form-urlencoded\n\nBaca [dokumentasi]({docs_url}) tentang cara mengonfigurasi otomasi untuk menangani data masuk." }, "step": { "user": { diff --git a/homeassistant/components/twilio/translations/ja.json b/homeassistant/components/twilio/translations/ja.json index 84ea72878e7..521fee184f2 100644 --- a/homeassistant/components/twilio/translations/ja.json +++ b/homeassistant/components/twilio/translations/ja.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { - "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[Webhooks with Twilio]({twilio_url})\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u4ee5\u4e0b\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n- Content Type: application/x-www-form-urlencoded\n\n\u53d7\u4fe1\u30c7\u30fc\u30bf\u3092\u51e6\u7406\u3059\u308b\u305f\u3081\u306b\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[Webhooks with Twilio]({twilio_url})\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u4ee5\u4e0b\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n- Content Type(\u30b3\u30f3\u30c6\u30f3\u30c4\u306e\u7a2e\u985e): application/x-www-form-urlencoded\n\n\u53d7\u4fe1\u30c7\u30fc\u30bf\u3092\u51e6\u7406\u3059\u308b\u305f\u3081\u306b\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { "user": { diff --git a/homeassistant/components/twinkly/translations/ca.json b/homeassistant/components/twinkly/translations/ca.json index ecf2c4ed12c..4813f7736f9 100644 --- a/homeassistant/components/twinkly/translations/ca.json +++ b/homeassistant/components/twinkly/translations/ca.json @@ -13,9 +13,7 @@ "user": { "data": { "host": "Amfitri\u00f3" - }, - "description": "Configura la teva tira LED de Twinkly", - "title": "Twinkly" + } } } } diff --git a/homeassistant/components/twinkly/translations/cs.json b/homeassistant/components/twinkly/translations/cs.json index be5b291744b..26afb737dce 100644 --- a/homeassistant/components/twinkly/translations/cs.json +++ b/homeassistant/components/twinkly/translations/cs.json @@ -13,9 +13,7 @@ "user": { "data": { "host": "Hostitel" - }, - "description": "Nastavte sv\u016fj LED p\u00e1sek Twinkly", - "title": "Twinkly" + } } } } diff --git a/homeassistant/components/twinkly/translations/de.json b/homeassistant/components/twinkly/translations/de.json index d31b243f7b9..5d096c5b594 100644 --- a/homeassistant/components/twinkly/translations/de.json +++ b/homeassistant/components/twinkly/translations/de.json @@ -13,9 +13,7 @@ "user": { "data": { "host": "Host" - }, - "description": "Einrichten deiner Twinkly-Led-Kette", - "title": "Twinkly" + } } } } diff --git a/homeassistant/components/twinkly/translations/el.json b/homeassistant/components/twinkly/translations/el.json index 8e0294b87b5..1210c6dc76a 100644 --- a/homeassistant/components/twinkly/translations/el.json +++ b/homeassistant/components/twinkly/translations/el.json @@ -13,9 +13,7 @@ "user": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" - }, - "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf Twinkly led string \u03c3\u03b1\u03c2", - "title": "Twinkly" + } } } } diff --git a/homeassistant/components/twinkly/translations/en.json b/homeassistant/components/twinkly/translations/en.json index fbb63fbc3e8..720a258161f 100644 --- a/homeassistant/components/twinkly/translations/en.json +++ b/homeassistant/components/twinkly/translations/en.json @@ -13,9 +13,7 @@ "user": { "data": { "host": "Host" - }, - "description": "Set up your Twinkly led string", - "title": "Twinkly" + } } } } diff --git a/homeassistant/components/twinkly/translations/es.json b/homeassistant/components/twinkly/translations/es.json index 691c0afcda6..e18d54adb9e 100644 --- a/homeassistant/components/twinkly/translations/es.json +++ b/homeassistant/components/twinkly/translations/es.json @@ -13,9 +13,7 @@ "user": { "data": { "host": "Host (o direcci\u00f3n IP) de tu dispositivo Twinkly" - }, - "description": "Configura tu tira led Twinkly", - "title": "Twinkly" + } } } } diff --git a/homeassistant/components/twinkly/translations/et.json b/homeassistant/components/twinkly/translations/et.json index bdb4ef3772c..bf902984d26 100644 --- a/homeassistant/components/twinkly/translations/et.json +++ b/homeassistant/components/twinkly/translations/et.json @@ -13,9 +13,7 @@ "user": { "data": { "host": "Host" - }, - "description": "Seadista oma Twinkly LED riba", - "title": "" + } } } } diff --git a/homeassistant/components/twinkly/translations/fr.json b/homeassistant/components/twinkly/translations/fr.json index c5b01400457..cf6fe97ce88 100644 --- a/homeassistant/components/twinkly/translations/fr.json +++ b/homeassistant/components/twinkly/translations/fr.json @@ -13,9 +13,7 @@ "user": { "data": { "host": "H\u00f4te" - }, - "description": "Configurer votre Twinkly", - "title": "Twinkly" + } } } } diff --git a/homeassistant/components/twinkly/translations/hu.json b/homeassistant/components/twinkly/translations/hu.json index 94983ce4893..0ad6208ca16 100644 --- a/homeassistant/components/twinkly/translations/hu.json +++ b/homeassistant/components/twinkly/translations/hu.json @@ -12,10 +12,8 @@ }, "user": { "data": { - "host": "A Twinkly eszk\u00f6z c\u00edme" - }, - "description": "\u00c1ll\u00edtsa be a Twinkly led-karakterl\u00e1nc\u00e1t", - "title": "Twinkly" + "host": "C\u00edm" + } } } } diff --git a/homeassistant/components/twinkly/translations/id.json b/homeassistant/components/twinkly/translations/id.json index 330bcf6ce11..b37e20bf023 100644 --- a/homeassistant/components/twinkly/translations/id.json +++ b/homeassistant/components/twinkly/translations/id.json @@ -13,9 +13,7 @@ "user": { "data": { "host": "Host" - }, - "description": "Siapkan string led Twinkly Anda", - "title": "Twinkly" + } } } } diff --git a/homeassistant/components/twinkly/translations/it.json b/homeassistant/components/twinkly/translations/it.json index a64ecaeb425..fec569379d4 100644 --- a/homeassistant/components/twinkly/translations/it.json +++ b/homeassistant/components/twinkly/translations/it.json @@ -13,9 +13,7 @@ "user": { "data": { "host": "Host" - }, - "description": "Configura la tua stringa led Twinkly", - "title": "Twinkly" + } } } } diff --git a/homeassistant/components/twinkly/translations/ja.json b/homeassistant/components/twinkly/translations/ja.json index e3955e4286f..2ec0d28e0bf 100644 --- a/homeassistant/components/twinkly/translations/ja.json +++ b/homeassistant/components/twinkly/translations/ja.json @@ -13,9 +13,7 @@ "user": { "data": { "host": "Twinkly device\u306e\u30db\u30b9\u30c8(\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9)" - }, - "description": "Twinkly led string\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7", - "title": "Twinkly" + } } } } diff --git a/homeassistant/components/twinkly/translations/ka.json b/homeassistant/components/twinkly/translations/ka.json index d0d6b61f4cc..293552c37d4 100644 --- a/homeassistant/components/twinkly/translations/ka.json +++ b/homeassistant/components/twinkly/translations/ka.json @@ -10,9 +10,7 @@ "user": { "data": { "host": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 twinkly \u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d8\u10e1 \u10f0\u10dd\u10e1\u10e2\u10d8 (\u10d0\u10dc IP \u10db\u10d8\u10e1\u10d0\u10db\u10d0\u10e0\u10d7\u10d8)" - }, - "description": "\u10d7\u10e5\u10d5\u10d4\u10dc\u10d8 Twinkly \u10e8\u10e3\u10e5\u10d3\u10d8\u10dd\u10d3\u10d8\u10e1 \u10da\u10d4\u10dc\u10e2\u10d8\u10e1 \u10d3\u10d0\u10e7\u10d4\u10dc\u10d4\u10d1\u10d0", - "title": "Twinkly" + } } } } diff --git a/homeassistant/components/twinkly/translations/ko.json b/homeassistant/components/twinkly/translations/ko.json index b3b23991e3d..257be47f610 100644 --- a/homeassistant/components/twinkly/translations/ko.json +++ b/homeassistant/components/twinkly/translations/ko.json @@ -10,9 +10,7 @@ "user": { "data": { "host": "Twinkly \uae30\uae30\uc758 \ud638\uc2a4\ud2b8 (\ub610\ub294 IP \uc8fc\uc18c)" - }, - "description": "Twinkly LED \uc904 \uc870\uba85 \uc124\uc815\ud558\uae30", - "title": "Twinkly" + } } } } diff --git a/homeassistant/components/twinkly/translations/nl.json b/homeassistant/components/twinkly/translations/nl.json index 38331177895..b41baa0b403 100644 --- a/homeassistant/components/twinkly/translations/nl.json +++ b/homeassistant/components/twinkly/translations/nl.json @@ -13,9 +13,7 @@ "user": { "data": { "host": "Host" - }, - "description": "Uw Twinkly LED-string instellen", - "title": "Twinkly" + } } } } diff --git a/homeassistant/components/twinkly/translations/no.json b/homeassistant/components/twinkly/translations/no.json index af3b74a9686..72f88054aa7 100644 --- a/homeassistant/components/twinkly/translations/no.json +++ b/homeassistant/components/twinkly/translations/no.json @@ -13,9 +13,7 @@ "user": { "data": { "host": "Vert" - }, - "description": "Sett opp Twinkly-led-strengen", - "title": "Twinkly" + } } } } diff --git a/homeassistant/components/twinkly/translations/pl.json b/homeassistant/components/twinkly/translations/pl.json index 8036d6cc2f6..a9538177101 100644 --- a/homeassistant/components/twinkly/translations/pl.json +++ b/homeassistant/components/twinkly/translations/pl.json @@ -13,9 +13,7 @@ "user": { "data": { "host": "Nazwa hosta lub adres IP" - }, - "description": "Konfiguracja Twinkly", - "title": "Twinkly" + } } } } diff --git a/homeassistant/components/twinkly/translations/pt-BR.json b/homeassistant/components/twinkly/translations/pt-BR.json index 3aff4eb867d..789802f44e9 100644 --- a/homeassistant/components/twinkly/translations/pt-BR.json +++ b/homeassistant/components/twinkly/translations/pt-BR.json @@ -13,9 +13,7 @@ "user": { "data": { "host": "Nome do host" - }, - "description": "Configure sua fita de led Twinkly", - "title": "Twinkly" + } } } } diff --git a/homeassistant/components/twinkly/translations/ru.json b/homeassistant/components/twinkly/translations/ru.json index a7e0ff145cd..d1e06eee9cc 100644 --- a/homeassistant/components/twinkly/translations/ru.json +++ b/homeassistant/components/twinkly/translations/ru.json @@ -13,9 +13,7 @@ "user": { "data": { "host": "\u0425\u043e\u0441\u0442" - }, - "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/twinkly/translations/tr.json b/homeassistant/components/twinkly/translations/tr.json index 0e78648d811..39bf9b32e55 100644 --- a/homeassistant/components/twinkly/translations/tr.json +++ b/homeassistant/components/twinkly/translations/tr.json @@ -13,9 +13,7 @@ "user": { "data": { "host": "Sunucu" - }, - "description": "Twinkly led dizinizi ayarlay\u0131n", - "title": "Twinkly" + } } } } diff --git a/homeassistant/components/twinkly/translations/uk.json b/homeassistant/components/twinkly/translations/uk.json index bd256d31b03..8f3d6d0cbaa 100644 --- a/homeassistant/components/twinkly/translations/uk.json +++ b/homeassistant/components/twinkly/translations/uk.json @@ -10,9 +10,7 @@ "user": { "data": { "host": "\u0406\u043c'\u044f \u0445\u043e\u0441\u0442\u0430 (\u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430) \u0412\u0430\u0448\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Twinkly" - }, - "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u0432\u0456\u0442\u043b\u043e\u0434\u0456\u043e\u0434\u043d\u043e\u0457 \u0441\u0442\u0440\u0456\u0447\u043a\u0438 Twinkly", - "title": "Twinkly" + } } } } diff --git a/homeassistant/components/twinkly/translations/zh-Hant.json b/homeassistant/components/twinkly/translations/zh-Hant.json index 77e0a69cf65..d016c286034 100644 --- a/homeassistant/components/twinkly/translations/zh-Hant.json +++ b/homeassistant/components/twinkly/translations/zh-Hant.json @@ -13,9 +13,7 @@ "user": { "data": { "host": "\u4e3b\u6a5f\u7aef" - }, - "description": "\u8a2d\u5b9a Twinkly LED \u71c8\u4e32", - "title": "Twinkly" + } } } } diff --git a/homeassistant/components/unifi/translations/hu.json b/homeassistant/components/unifi/translations/hu.json index 7692342bf89..ea4c7484d44 100644 --- a/homeassistant/components/unifi/translations/hu.json +++ b/homeassistant/components/unifi/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "A vez\u00e9rl\u0151 webhelye m\u00e1r konfigur\u00e1lva van", + "already_configured": "Az UniFi Network webhely m\u00e1r konfigur\u00e1lva van", "configuration_updated": "A konfigur\u00e1ci\u00f3 friss\u00edtve.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, @@ -39,7 +39,7 @@ "device_tracker": { "data": { "detection_time": "Id\u0151 m\u00e1sodpercben az utols\u00f3 l\u00e1t\u00e1st\u00f3l a t\u00e1vol tart\u00e1sig", - "ignore_wired_bug": "Az UniFi vezet\u00e9kes hibalogika letilt\u00e1sa", + "ignore_wired_bug": "Az UniFi Network vezet\u00e9kes hibalogika letilt\u00e1sa", "ssid_filter": "V\u00e1lassza ki az SSID -ket a vezet\u00e9k n\u00e9lk\u00fcli \u00fcgyfelek nyomon k\u00f6vet\u00e9s\u00e9hez", "track_clients": "K\u00f6vesse nyomon a h\u00e1l\u00f3zati \u00fcgyfeleket", "track_devices": "H\u00e1l\u00f3zati eszk\u00f6z\u00f6k nyomon k\u00f6vet\u00e9se (Ubiquiti eszk\u00f6z\u00f6k)", diff --git a/homeassistant/components/unifi/translations/it.json b/homeassistant/components/unifi/translations/it.json index eeda8a8c9bb..7ddc0fde4fe 100644 --- a/homeassistant/components/unifi/translations/it.json +++ b/homeassistant/components/unifi/translations/it.json @@ -50,8 +50,8 @@ }, "init": { "data": { - "one": "Pi\u00f9", - "other": "Altri" + "one": "Vuoto", + "other": "Vuoti" } }, "simple_options": { diff --git a/homeassistant/components/update/translations/ca.json b/homeassistant/components/update/translations/ca.json index 396e79c14c0..b008c0693c0 100644 --- a/homeassistant/components/update/translations/ca.json +++ b/homeassistant/components/update/translations/ca.json @@ -1,3 +1,10 @@ { + "device_automation": { + "trigger_type": { + "changed_states": "La disponibilitat de l'actualitzaci\u00f3 de {entity_name} canvi\u00ef", + "turned_off": "{entity_name} s'actualitzi", + "turned_on": "{entity_name} tingui una actualitzaci\u00f3 disponible" + } + }, "title": "Actualitza" } \ No newline at end of file diff --git a/homeassistant/components/update/translations/cs.json b/homeassistant/components/update/translations/cs.json new file mode 100644 index 00000000000..39380573af6 --- /dev/null +++ b/homeassistant/components/update/translations/cs.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "trigger_type": { + "changed_states": "Dostupnost aktualizace {entity_name} zm\u011bn\u011bna", + "turned_off": "{entity_name} se stal aktu\u00e1ln\u00edm", + "turned_on": "{entity_name} m\u00e1 k dispozici aktualizaci" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/de.json b/homeassistant/components/update/translations/de.json index 18562d81eaf..bc3e9b19322 100644 --- a/homeassistant/components/update/translations/de.json +++ b/homeassistant/components/update/translations/de.json @@ -1,3 +1,10 @@ { + "device_automation": { + "trigger_type": { + "changed_states": "{entity_name} Update Verf\u00fcgbarkeit ge\u00e4ndert", + "turned_off": "{entity_name} wurde auf den neuesten Stand gebracht", + "turned_on": "F\u00fcr {entity_name} ist ein Update verf\u00fcgbar" + } + }, "title": "Aktualisieren" } \ No newline at end of file diff --git a/homeassistant/components/update/translations/el.json b/homeassistant/components/update/translations/el.json index d687d342ec3..0a4d6190150 100644 --- a/homeassistant/components/update/translations/el.json +++ b/homeassistant/components/update/translations/el.json @@ -1,3 +1,10 @@ { + "device_automation": { + "trigger_type": { + "changed_states": "\u0397 \u03b4\u03b9\u03b1\u03b8\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2 {entity_name} \u03ac\u03bb\u03bb\u03b1\u03be\u03b5", + "turned_off": "{entity_name} \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5", + "turned_on": "{entity_name} \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03bc\u03b9\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7" + } + }, "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7" } \ No newline at end of file diff --git a/homeassistant/components/update/translations/es.json b/homeassistant/components/update/translations/es.json new file mode 100644 index 00000000000..79993c92b20 --- /dev/null +++ b/homeassistant/components/update/translations/es.json @@ -0,0 +1,3 @@ +{ + "title": "Actualizar" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/et.json b/homeassistant/components/update/translations/et.json index 7db9a98a507..e89acfbe30f 100644 --- a/homeassistant/components/update/translations/et.json +++ b/homeassistant/components/update/translations/et.json @@ -1,3 +1,10 @@ { + "device_automation": { + "trigger_type": { + "changed_states": "Olemi {entity_name} v\u00e4rskenduse saadavus muutus", + "turned_off": "Olem {entity_name} muutus ajakohaseks", + "turned_on": "Olemile {entity_name} on saadaval v\u00e4rskendus" + } + }, "title": "Uuenda" } \ No newline at end of file diff --git a/homeassistant/components/update/translations/fr.json b/homeassistant/components/update/translations/fr.json index 1a49a0cab9f..94d23e72a7a 100644 --- a/homeassistant/components/update/translations/fr.json +++ b/homeassistant/components/update/translations/fr.json @@ -1,3 +1,10 @@ { + "device_automation": { + "trigger_type": { + "changed_states": "La disponibilit\u00e9 d'une mise \u00e0 jour pour {entity_name} a chang\u00e9", + "turned_off": "{entity_name} a \u00e9t\u00e9 mis \u00e0 jour", + "turned_on": "Une mise \u00e0 jour est disponible pour {entity_name}" + } + }, "title": "Mettre \u00e0 jour" } \ No newline at end of file diff --git a/homeassistant/components/update/translations/he.json b/homeassistant/components/update/translations/he.json new file mode 100644 index 00000000000..6c81fac9447 --- /dev/null +++ b/homeassistant/components/update/translations/he.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "trigger_type": { + "changed_states": "{entity_name} \u05d6\u05de\u05d9\u05e0\u05d5\u05ea \u05d4\u05e2\u05d3\u05db\u05d5\u05df \u05d4\u05e9\u05ea\u05e0\u05ea\u05d4", + "turned_off": "{entity_name} \u05d4\u05e4\u05da \u05dc\u05de\u05e2\u05d5\u05d3\u05db\u05df", + "turned_on": "{entity_name} \u05e7\u05d9\u05d1\u05dc \u05e2\u05d3\u05db\u05d5\u05df \u05d6\u05de\u05d9\u05df" + } + }, + "title": "\u05e2\u05d3\u05db\u05d5\u05df" +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/hu.json b/homeassistant/components/update/translations/hu.json index 1e2ec425a88..7732ee56d62 100644 --- a/homeassistant/components/update/translations/hu.json +++ b/homeassistant/components/update/translations/hu.json @@ -1,3 +1,10 @@ { + "device_automation": { + "trigger_type": { + "changed_states": "{entity_name} friss\u00edt\u00e9s el\u00e9rhet\u0151s\u00e9ge megv\u00e1ltozott", + "turned_off": "{entity_name} naprak\u00e9sz lett", + "turned_on": "{entity_name} kapott egy el\u00e9rhet\u0151 friss\u00edt\u00e9st" + } + }, "title": "Friss\u00edt\u00e9s" } \ No newline at end of file diff --git a/homeassistant/components/update/translations/id.json b/homeassistant/components/update/translations/id.json index 70f495575fa..c4e6c43ab5c 100644 --- a/homeassistant/components/update/translations/id.json +++ b/homeassistant/components/update/translations/id.json @@ -1,3 +1,10 @@ { + "device_automation": { + "trigger_type": { + "changed_states": "Ketersediaan pembaruan {entity_name} berubah", + "turned_off": "{entity_name} menjadi yang terbaru", + "turned_on": "{entity_name} mendapat pembaruan yang tersedia" + } + }, "title": "Versi Baru" } \ No newline at end of file diff --git a/homeassistant/components/update/translations/it.json b/homeassistant/components/update/translations/it.json index 539f0bb4294..97d2b4020df 100644 --- a/homeassistant/components/update/translations/it.json +++ b/homeassistant/components/update/translations/it.json @@ -1,3 +1,10 @@ { + "device_automation": { + "trigger_type": { + "changed_states": "La disponibilit\u00e0 dell'aggiornamento di {entity_name} \u00e8 cambiata", + "turned_off": "{entity_name} \u00e8 stato aggiornato", + "turned_on": "{entity_name} ha un aggiornamento disponibile" + } + }, "title": "Aggiornamento" } \ No newline at end of file diff --git a/homeassistant/components/update/translations/ja.json b/homeassistant/components/update/translations/ja.json index 4cb5e4959a1..63ac9a7ddf5 100644 --- a/homeassistant/components/update/translations/ja.json +++ b/homeassistant/components/update/translations/ja.json @@ -1,3 +1,10 @@ { + "device_automation": { + "trigger_type": { + "changed_states": "{entity_name} \u66f4\u65b0\u306e\u53ef\u7528\u6027(availability)\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f", + "turned_off": "{entity_name} \u304c\u6700\u65b0\u306b\u306a\u308a\u307e\u3057\u305f", + "turned_on": "{entity_name} \u306f\u3001\u5229\u7528\u53ef\u80fd\u306a\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u3092\u53d6\u5f97\u3057\u307e\u3057\u305f\u3002" + } + }, "title": "\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8" } \ No newline at end of file diff --git a/homeassistant/components/update/translations/nl.json b/homeassistant/components/update/translations/nl.json index 95b82de3b4d..840fa03da8a 100644 --- a/homeassistant/components/update/translations/nl.json +++ b/homeassistant/components/update/translations/nl.json @@ -1,3 +1,10 @@ { + "device_automation": { + "trigger_type": { + "changed_states": "{entity_name} update beschikbaarheid gewijzigd", + "turned_off": "{entity_name} werd up-to-date", + "turned_on": "{entity_name} heeft een update beschikbaar" + } + }, "title": "Update" } \ No newline at end of file diff --git a/homeassistant/components/update/translations/no.json b/homeassistant/components/update/translations/no.json index e98d60ab4fc..8f292b0b141 100644 --- a/homeassistant/components/update/translations/no.json +++ b/homeassistant/components/update/translations/no.json @@ -1,3 +1,10 @@ { + "device_automation": { + "trigger_type": { + "changed_states": "{entity_name} -oppdateringstilgjengeligheten er endret", + "turned_off": "{entity_name} ble oppdatert", + "turned_on": "{entity_name} har en oppdatering tilgjengelig" + } + }, "title": "Oppdater" } \ No newline at end of file diff --git a/homeassistant/components/update/translations/pl.json b/homeassistant/components/update/translations/pl.json index eff0431a518..ee29b58f8c3 100644 --- a/homeassistant/components/update/translations/pl.json +++ b/homeassistant/components/update/translations/pl.json @@ -1,3 +1,10 @@ { + "device_automation": { + "trigger_type": { + "changed_states": "zmieni si\u0119 dost\u0119pno\u015b\u0107 aktualizacji {entity_name}", + "turned_off": "wykonano aktualizacj\u0119 dla {entity_name}", + "turned_on": "{entity_name} ma dost\u0119pn\u0105 aktualizacj\u0119" + } + }, "title": "Aktualizacja" } \ No newline at end of file diff --git a/homeassistant/components/update/translations/pt-BR.json b/homeassistant/components/update/translations/pt-BR.json index 4003445e2c3..dbdbae4fa88 100644 --- a/homeassistant/components/update/translations/pt-BR.json +++ b/homeassistant/components/update/translations/pt-BR.json @@ -1,3 +1,10 @@ { + "device_automation": { + "trigger_type": { + "changed_states": "{entity_name} disponibilidade de atualiza\u00e7\u00e3o alterada", + "turned_off": "{entity_name} foi atualizado", + "turned_on": "{entity_name} tem uma atualiza\u00e7\u00e3o dispon\u00edvel" + } + }, "title": "Atualiza\u00e7\u00e3o" } \ No newline at end of file diff --git a/homeassistant/components/update/translations/ru.json b/homeassistant/components/update/translations/ru.json index a2ee79efd15..f78b838708e 100644 --- a/homeassistant/components/update/translations/ru.json +++ b/homeassistant/components/update/translations/ru.json @@ -1,3 +1,10 @@ { + "device_automation": { + "trigger_type": { + "changed_states": "\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0441\u0442\u044c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f {entity_name}", + "turned_off": "{entity_name} \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u0442\u0441\u044f", + "turned_on": "\u0421\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0441\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 {entity_name}" + } + }, "title": "\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435" } \ No newline at end of file diff --git a/homeassistant/components/update/translations/tr.json b/homeassistant/components/update/translations/tr.json index 30c3c90c437..11d0b3a2c92 100644 --- a/homeassistant/components/update/translations/tr.json +++ b/homeassistant/components/update/translations/tr.json @@ -1,3 +1,10 @@ { + "device_automation": { + "trigger_type": { + "changed_states": "{entity_name} g\u00fcncellemesinin kullan\u0131labilirli\u011fi de\u011fi\u015fti", + "turned_off": "{entity_name} g\u00fcncellendi", + "turned_on": "{entity_name} bir g\u00fcncelleme ald\u0131" + } + }, "title": "G\u00fcncelle" } \ No newline at end of file diff --git a/homeassistant/components/update/translations/zh-Hans.json b/homeassistant/components/update/translations/zh-Hans.json new file mode 100644 index 00000000000..8b7c885cccb --- /dev/null +++ b/homeassistant/components/update/translations/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "trigger_type": { + "changed_states": "{entity_name} \u7684\u66f4\u65b0\u53ef\u7528\u6027\u53d8\u5316", + "turned_off": "{entity_name} \u53d8\u4e3a\u6700\u65b0\u72b6\u6001", + "turned_on": "{entity_name} \u6536\u5230\u66f4\u65b0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/zh-Hant.json b/homeassistant/components/update/translations/zh-Hant.json index 46cdfb48f90..492af201404 100644 --- a/homeassistant/components/update/translations/zh-Hant.json +++ b/homeassistant/components/update/translations/zh-Hant.json @@ -1,3 +1,10 @@ { + "device_automation": { + "trigger_type": { + "changed_states": "{entity_name} \u53ef\u7528\u66f4\u65b0\u5df2\u8b8a\u66f4", + "turned_off": "{entity_name} \u5df2\u6700\u65b0", + "turned_on": "{entity_name} \u6709\u66f4\u65b0" + } + }, "title": "\u66f4\u65b0" } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/it.json b/homeassistant/components/upnp/translations/it.json index ce637bbad08..a57429ac78a 100644 --- a/homeassistant/components/upnp/translations/it.json +++ b/homeassistant/components/upnp/translations/it.json @@ -6,14 +6,14 @@ "no_devices_found": "Nessun dispositivo trovato sulla rete" }, "error": { - "one": "Pi\u00f9", - "other": "Altri" + "one": "Vuoto", + "other": "Vuoti" }, "flow_title": "{name}", "step": { "init": { - "one": "Pi\u00f9", - "other": "Altri" + "one": "Vuoto", + "other": "Vuoti" }, "ssdp_confirm": { "description": "Vuoi configurare questo dispositivo UPnP/IGD?" diff --git a/homeassistant/components/uptime/translations/bg.json b/homeassistant/components/uptime/translations/bg.json new file mode 100644 index 00000000000..1290144ec04 --- /dev/null +++ b/homeassistant/components/uptime/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": { + "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\u0432\u0430\u043d\u0435\u0442\u043e?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/cs.json b/homeassistant/components/uptime/translations/cs.json new file mode 100644 index 00000000000..ce19e127348 --- /dev/null +++ b/homeassistant/components/uptime/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "step": { + "user": { + "description": "Chcete za\u010d\u00edt nastavovat?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/id.json b/homeassistant/components/uptime/translations/id.json new file mode 100644 index 00000000000..bf6ea606f2b --- /dev/null +++ b/homeassistant/components/uptime/translations/id.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "user": { + "description": "Ingin memulai penyiapan?" + } + } + }, + "title": "Uptime" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/it.json b/homeassistant/components/uptime/translations/it.json index cba8d0a264f..9913180a309 100644 --- a/homeassistant/components/uptime/translations/it.json +++ b/homeassistant/components/uptime/translations/it.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, "step": { "user": { "description": "Vuoi iniziare la configurazione?" diff --git a/homeassistant/components/uptime/translations/no.json b/homeassistant/components/uptime/translations/no.json new file mode 100644 index 00000000000..9ac16f0a20c --- /dev/null +++ b/homeassistant/components/uptime/translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "step": { + "user": { + "description": "Vil du starte oppsettet?" + } + } + }, + "title": "Oppetid" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/pl.json b/homeassistant/components/uptime/translations/pl.json new file mode 100644 index 00000000000..bca14b2f14c --- /dev/null +++ b/homeassistant/components/uptime/translations/pl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "user": { + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + } + } + }, + "title": "Uptime" +} \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/pt-BR.json b/homeassistant/components/uptime/translations/pt-BR.json index fdec46961b5..d3dddae8233 100644 --- a/homeassistant/components/uptime/translations/pt-BR.json +++ b/homeassistant/components/uptime/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o poss\u00edvel." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "user": { diff --git a/homeassistant/components/uptime/translations/tr.json b/homeassistant/components/uptime/translations/tr.json new file mode 100644 index 00000000000..ed090a38398 --- /dev/null +++ b/homeassistant/components/uptime/translations/tr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "user": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } + } + }, + "title": "\u00c7al\u0131\u015fma S\u00fcresi" +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/ca.json b/homeassistant/components/uptimerobot/translations/ca.json index 9ae97109fc1..a6ed19c11f0 100644 --- a/homeassistant/components/uptimerobot/translations/ca.json +++ b/homeassistant/components/uptimerobot/translations/ca.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_api_key": "Clau API inv\u00e0lida", + "not_main_key": "S'ha detectat un tipus de clau API incorrecta, utilitza la clau API 'principal'", "reauth_failed_matching_account": "La clau API proporcionada no correspon amb l'identificador del compte de la configuraci\u00f3 actual.", "unknown": "Error inesperat" }, @@ -17,14 +18,14 @@ "data": { "api_key": "Clau API" }, - "description": "Has de proporcionar una nova clau API de nom\u00e9s lectura d'UptimeRobot", + "description": "Has de proporcionar una nova clau API 'principal' 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'UptimeRobot" + "description": "Has de proporcionar la clau API 'principal' d'UptimeRobot" } } } diff --git a/homeassistant/components/uptimerobot/translations/de.json b/homeassistant/components/uptimerobot/translations/de.json index 3ac63f84de2..ee2af73ea20 100644 --- a/homeassistant/components/uptimerobot/translations/de.json +++ b/homeassistant/components/uptimerobot/translations/de.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "not_main_key": "Falscher API-Schl\u00fcsseltyp erkannt, verwende den API-Hauptschl\u00fcssel", "reauth_failed_matching_account": "Der von dir angegebene API-Schl\u00fcssel stimmt nicht mit der Konto-ID f\u00fcr die vorhandene Konfiguration \u00fcberein.", "unknown": "Unerwarteter Fehler" }, @@ -17,14 +18,14 @@ "data": { "api_key": "API-Schl\u00fcssel" }, - "description": "Du musst einen neuen schreibgesch\u00fctzten API-Schl\u00fcssel von UptimeRobot bereitstellen", + "description": "Du musst einen neuen Haupt-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 UptimeRobot bereitstellen" + "description": "Du musst den Haupt-API-Schl\u00fcssel von UptimeRobot bereitstellen" } } } diff --git a/homeassistant/components/uptimerobot/translations/el.json b/homeassistant/components/uptimerobot/translations/el.json index 2f15a945ab0..98034c40eee 100644 --- a/homeassistant/components/uptimerobot/translations/el.json +++ b/homeassistant/components/uptimerobot/translations/el.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API", + "not_main_key": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bb\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c4\u03cd\u03c0\u03bf\u03c2 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd API, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 'main' API", "reauth_failed_matching_account": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03c0\u03bf\u03c5 \u03b4\u03ce\u03c3\u03b1\u03c4\u03b5 \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03b5\u03b9 \u03bc\u03b5 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7.", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, diff --git a/homeassistant/components/uptimerobot/translations/et.json b/homeassistant/components/uptimerobot/translations/et.json index 9b00c2a5b44..ed2e46b7cc2 100644 --- a/homeassistant/components/uptimerobot/translations/et.json +++ b/homeassistant/components/uptimerobot/translations/et.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_api_key": "Vigane API v\u00f5ti", + "not_main_key": "Tuvastati vale API-v\u00f5tme t\u00fc\u00fcp, kasuta API peamist v\u00f5tit", "reauth_failed_matching_account": "Sisestatud API v\u00f5ti ei vasta olemasoleva konto ID s\u00e4tetele.", "unknown": "Ootamatu t\u00f5rge" }, @@ -17,14 +18,14 @@ "data": { "api_key": "API v\u00f5ti" }, - "description": "Pead sisestama uue UptimeRoboti kirjutuskaitstud API-v\u00f5tme", + "description": "Pead sisestama uue UptimeRoboti peamise API-v\u00f5tme", "title": "Taastuvasta sidumine" }, "user": { "data": { "api_key": "API v\u00f5ti" }, - "description": "Pead sisestama UptimeRoboti kirjutuskaitstud API-v\u00f5tme" + "description": "Pead sisestama UptimeRoboti peamise API-v\u00f5tme" } } } diff --git a/homeassistant/components/uptimerobot/translations/fr.json b/homeassistant/components/uptimerobot/translations/fr.json index 674180a1d90..ebea3087fc7 100644 --- a/homeassistant/components/uptimerobot/translations/fr.json +++ b/homeassistant/components/uptimerobot/translations/fr.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "invalid_api_key": "Cl\u00e9 d'API non valide", + "not_main_key": "Mauvais type de cl\u00e9 d'API d\u00e9tect\u00e9, utilisez la cl\u00e9 d'API \u00ab\u00a0principale\u00a0\u00bb", "reauth_failed_matching_account": "La cl\u00e9 API que vous avez fournie ne correspond pas \u00e0 l\u2019ID de compte pour la configuration existante.", "unknown": "Erreur inattendue" }, @@ -17,14 +18,14 @@ "data": { "api_key": "Cl\u00e9 d'API" }, - "description": "Vous devez fournir une nouvelle cl\u00e9 API en lecture seule \u00e0 partir d'Uptime Robot", + "description": "Vous devez fournir une nouvelle cl\u00e9 d'API \u00ab\u00a0principale\u00a0\u00bb \u00e0 partir d'UptimeRobot", "title": "R\u00e9-authentifier l'int\u00e9gration" }, "user": { "data": { "api_key": "Cl\u00e9 d'API" }, - "description": "Vous devez fournir une cl\u00e9 API en lecture seule \u00e0 partir d'Uptime Robot" + "description": "Vous devez fournir la cl\u00e9 d'API \u00ab\u00a0principale\u00a0\u00bb \u00e0 partir d'UptimeRobot" } } } diff --git a/homeassistant/components/uptimerobot/translations/hu.json b/homeassistant/components/uptimerobot/translations/hu.json index 000851093a5..d17c9bf8ac1 100644 --- a/homeassistant/components/uptimerobot/translations/hu.json +++ b/homeassistant/components/uptimerobot/translations/hu.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "Nem siker\u00fclt csatlakozni", "invalid_api_key": "\u00c9rv\u00e9nytelen API-kulcs", + "not_main_key": "Nem megfelel\u0151 API-kulcst\u00edpus, haszn\u00e1lja a \"main\" API-kulcsot", "reauth_failed_matching_account": "A megadott API -kulcs nem egyezik a megl\u00e9v\u0151 konfigur\u00e1ci\u00f3 fi\u00f3kazonos\u00edt\u00f3j\u00e1val.", "unknown": "V\u00e1ratlan hiba" }, @@ -17,14 +18,14 @@ "data": { "api_key": "API kulcs" }, - "description": "Meg kell adnia egy \u00faj, csak olvashat\u00f3 API-kulcsot az Uptime Robot-t\u00f3l", + "description": "Meg kell adnia egy \u00faj \u201ef\u0151\u201d API-kulcsot az UptimeRobott\u00f3l", "title": "Integr\u00e1ci\u00f3 \u00fajb\u00f3li hiteles\u00edt\u00e9se" }, "user": { "data": { "api_key": "API kulcs" }, - "description": "Meg kell adnia egy csak olvashat\u00f3 API-kulcsot az Uptime Robot-t\u00f3l" + "description": "Meg kell adnia a \u201ef\u0151\u201d API-kulcsot az UptimeRobott\u00f3l" } } } diff --git a/homeassistant/components/uptimerobot/translations/id.json b/homeassistant/components/uptimerobot/translations/id.json index dac2e7e2814..b92f8f74027 100644 --- a/homeassistant/components/uptimerobot/translations/id.json +++ b/homeassistant/components/uptimerobot/translations/id.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "Gagal terhubung", "invalid_api_key": "Kunci API tidak valid", + "not_main_key": "Jenis kunci API yang salah terdeteksi, gunakan kunci API 'main'", "reauth_failed_matching_account": "Kunci API yang Anda berikan tidak cocok dengan ID akun untuk konfigurasi yang ada.", "unknown": "Kesalahan yang tidak diharapkan" }, @@ -17,14 +18,14 @@ "data": { "api_key": "Kunci API" }, - "description": "Anda perlu menyediakan kunci API hanya-baca yang baru dari UptimeRobot", + "description": "Anda perlu menyediakan kunci API 'main' yang baru dari UptimeRobot", "title": "Autentikasi Ulang Integrasi" }, "user": { "data": { "api_key": "Kunci API" }, - "description": "Anda perlu menyediakan kunci API hanya-baca dari UptimeRobot" + "description": "Anda perlu menyediakan kunci API 'main' dari UptimeRobot" } } } diff --git a/homeassistant/components/uptimerobot/translations/it.json b/homeassistant/components/uptimerobot/translations/it.json index 3a6c87c5ea9..4359ab682ee 100644 --- a/homeassistant/components/uptimerobot/translations/it.json +++ b/homeassistant/components/uptimerobot/translations/it.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "Impossibile connettersi", "invalid_api_key": "Chiave API non valida", + "not_main_key": "Rilevato tipo di chiave API errato, utilizza la chiave API 'principale'.", "reauth_failed_matching_account": "La chiave API che hai fornito non corrisponde all'ID account per la configurazione esistente.", "unknown": "Errore imprevisto" }, @@ -17,14 +18,14 @@ "data": { "api_key": "Chiave API" }, - "description": "Devi fornire una nuova chiave API di sola lettura da UptimeRobot", + "description": "Devi fornire una nuova chiave API 'principale' da UptimeRobot", "title": "Autentica nuovamente l'integrazione" }, "user": { "data": { "api_key": "Chiave API" }, - "description": "Devi fornire una chiave API di sola lettura da UptimeRobot" + "description": "Devi fornire la chiave API 'principale' da UptimeRobot" } } } diff --git a/homeassistant/components/uptimerobot/translations/ja.json b/homeassistant/components/uptimerobot/translations/ja.json index d890780eab9..e8c66b5088b 100644 --- a/homeassistant/components/uptimerobot/translations/ja.json +++ b/homeassistant/components/uptimerobot/translations/ja.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", + "not_main_key": "\u9593\u9055\u3063\u305fAPI\u30ad\u30fc\u30bf\u30a4\u30d7\u304c\u691c\u51fa\u3055\u308c\u307e\u3057\u305f\u3002 'main' API\u30ad\u30fc\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044", "reauth_failed_matching_account": "\u6307\u5b9a\u3055\u308c\u305fAPI\u30ad\u30fc\u304c\u3001\u3059\u3067\u306b\u3042\u308b\u8a2d\u5b9a\u306e\u30a2\u30ab\u30a6\u30f3\u30c8ID\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/uptimerobot/translations/nl.json b/homeassistant/components/uptimerobot/translations/nl.json index 3431a9cbfa5..d3ee1f7515a 100644 --- a/homeassistant/components/uptimerobot/translations/nl.json +++ b/homeassistant/components/uptimerobot/translations/nl.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "Kan geen verbinding maken", "invalid_api_key": "Ongeldige API-sleutel", + "not_main_key": "Verkeerde API sleutel gedetecteerd, gebruik de 'hoofd' API sleutel", "reauth_failed_matching_account": "De API sleutel die u heeft opgegeven komt niet overeen met de account ID voor de bestaande configuratie.", "unknown": "Onverwachte fout" }, @@ -17,14 +18,14 @@ "data": { "api_key": "API-sleutel" }, - "description": "U moet een nieuwe alleen-lezen API-sleutel van UptimeRobot opgeven", + "description": "U moet een nieuwe 'hoofd'-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 UptimeRobot opgeven" + "description": "U moet de 'hoofd' API-sleutel van UptimeRobot opgeven" } } } diff --git a/homeassistant/components/uptimerobot/translations/no.json b/homeassistant/components/uptimerobot/translations/no.json index cbb5066a747..df7a7f8045a 100644 --- a/homeassistant/components/uptimerobot/translations/no.json +++ b/homeassistant/components/uptimerobot/translations/no.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_api_key": "Ugyldig API-n\u00f8kkel", + "not_main_key": "Feil API-n\u00f8kkeltype oppdaget, bruk 'hoved' API-n\u00f8kkelen", "reauth_failed_matching_account": "API-n\u00f8kkelen du oppgav, samsvarer ikke med konto-IDen for eksisterende konfigurasjon.", "unknown": "Uventet feil" }, @@ -17,14 +18,14 @@ "data": { "api_key": "API-n\u00f8kkel" }, - "description": "Du m\u00e5 levere en ny skrivebeskyttet API-n\u00f8kkel fra UptimeRobot", + "description": "Du m\u00e5 oppgi en ny 'hoved' API-n\u00f8kkel fra UptimeRobot", "title": "Godkjenne integrering p\u00e5 nytt" }, "user": { "data": { "api_key": "API-n\u00f8kkel" }, - "description": "Du m\u00e5 levere en skrivebeskyttet API-n\u00f8kkel fra UptimeRobot" + "description": "Du m\u00e5 oppgi \"hoved\" API-n\u00f8kkelen fra UptimeRobot" } } } diff --git a/homeassistant/components/uptimerobot/translations/pl.json b/homeassistant/components/uptimerobot/translations/pl.json index 18c40afec1e..d5698192e6d 100644 --- a/homeassistant/components/uptimerobot/translations/pl.json +++ b/homeassistant/components/uptimerobot/translations/pl.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_api_key": "Nieprawid\u0142owy klucz API", + "not_main_key": "Wykryto nieprawid\u0142owy typ klucza API, u\u017cyj \"g\u0142\u00f3wnego\" klucza API", "reauth_failed_matching_account": "Podany klucz API nie jest zgodny z identyfikatorem konta istniej\u0105cej konfiguracji.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, @@ -17,14 +18,14 @@ "data": { "api_key": "Klucz API" }, - "description": "Musisz poda\u0107 nowy, tylko do odczytu, klucz API od Uptime Robot", + "description": "Musisz poda\u0107 nowy \"g\u0142\u00f3wny\" klucz API od Uptime Robot", "title": "Ponownie uwierzytelnij integracj\u0119" }, "user": { "data": { "api_key": "Klucz API" }, - "description": "Musisz poda\u0107 klucz API (tylko do odczytu) od Uptime Robot" + "description": "Musisz poda\u0107 \"g\u0142\u00f3wny\" klucz API od Uptime Robot" } } } diff --git a/homeassistant/components/uptimerobot/translations/pt-BR.json b/homeassistant/components/uptimerobot/translations/pt-BR.json index 0d9bea96b12..4e905f67b31 100644 --- a/homeassistant/components/uptimerobot/translations/pt-BR.json +++ b/homeassistant/components/uptimerobot/translations/pt-BR.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "Falha ao conectar", "invalid_api_key": "Chave de API inv\u00e1lida", + "not_main_key": "Tipo de chave de API incorreto detectado, use a chave de API 'principal'", "reauth_failed_matching_account": "A chave de API fornecida n\u00e3o corresponde ao ID da conta da configura\u00e7\u00e3o existente.", "unknown": "Erro inesperado" }, @@ -17,14 +18,14 @@ "data": { "api_key": "Chave da API" }, - "description": "Voc\u00ea precisa fornecer uma nova chave de API somente leitura do UptimeRobot", + "description": "Voc\u00ea precisa fornecer uma nova chave de API 'principal' do UptimeRobot", "title": "Reautenticar Integra\u00e7\u00e3o" }, "user": { "data": { "api_key": "Chave da API" }, - "description": "Voc\u00ea precisa fornecer uma chave de API somente leitura do UptimeRobot" + "description": "Voc\u00ea precisa fornecer a chave de API 'principal' do UptimeRobot" } } } diff --git a/homeassistant/components/uptimerobot/translations/ru.json b/homeassistant/components/uptimerobot/translations/ru.json index fd26dbf969a..4497dc5a362 100644 --- a/homeassistant/components/uptimerobot/translations/ru.json +++ b/homeassistant/components/uptimerobot/translations/ru.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "not_main_key": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u0442\u0438\u043f \u043a\u043b\u044e\u0447\u0430 API, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043a\u043b\u044e\u0447 API 'main'.", "reauth_failed_matching_account": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0443 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0434\u043b\u044f \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\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." }, @@ -17,14 +18,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 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", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043d\u043e\u0432\u044b\u0439 'main' \u043a\u043b\u044e\u0447 API UptimeRobot.", "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 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" + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 'main' \u043a\u043b\u044e\u0447 API UptimeRobot." } } } diff --git a/homeassistant/components/uptimerobot/translations/tr.json b/homeassistant/components/uptimerobot/translations/tr.json index c209afdfd8e..8f7291d57fd 100644 --- a/homeassistant/components/uptimerobot/translations/tr.json +++ b/homeassistant/components/uptimerobot/translations/tr.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "not_main_key": "Yanl\u0131\u015f API anahtar\u0131 t\u00fcr\u00fc alg\u0131land\u0131, 'ana' API anahtar\u0131n\u0131 kullan\u0131n", "reauth_failed_matching_account": "Sa\u011flad\u0131\u011f\u0131n\u0131z API anahtar\u0131, mevcut yap\u0131land\u0131rman\u0131n hesap kimli\u011fiyle e\u015fle\u015fmiyor.", "unknown": "Beklenmeyen hata" }, @@ -17,14 +18,14 @@ "data": { "api_key": "API Anahtar\u0131" }, - "description": "UptimeRobot'tan yeni bir salt okunur API anahtar\u0131 sa\u011flaman\u0131z gerekiyor", + "description": "UptimeRobot'tan yeni bir 'ana' API anahtar\u0131 sa\u011flaman\u0131z gerekiyor", "title": "Entegrasyonu Yeniden Do\u011frula" }, "user": { "data": { "api_key": "API Anahtar\u0131" }, - "description": "UptimeRobot'tan salt okunur bir API anahtar\u0131 sa\u011flaman\u0131z gerekiyor" + "description": "UptimeRobot'tan 'ana' API anahtar\u0131n\u0131 sa\u011flaman\u0131z gerekiyor" } } } diff --git a/homeassistant/components/uptimerobot/translations/zh-Hant.json b/homeassistant/components/uptimerobot/translations/zh-Hant.json index 8b01cab6d7c..e8edfbf1934 100644 --- a/homeassistant/components/uptimerobot/translations/zh-Hant.json +++ b/homeassistant/components/uptimerobot/translations/zh-Hant.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_api_key": "API \u91d1\u9470\u7121\u6548", + "not_main_key": "\u5075\u6e2c\u5230\u932f\u8aa4\u7684 API \u91d1\u9470\u985e\u578b\u3001\u4f7f\u7528 'main' API \u91d1\u9470", "reauth_failed_matching_account": "\u6240\u63d0\u4f9b\u7684\u91d1\u9470\u8207\u73fe\u6709\u8a2d\u5b9a\u5e33\u865f ID \u4e0d\u7b26\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, @@ -17,14 +18,14 @@ "data": { "api_key": "API \u91d1\u9470" }, - "description": "\u9700\u8981\u63d0\u4f9b\u7531 UptimeRobot \u53d6\u5f97\u4e00\u7d44\u65b0\u7684\u552f\u8b80 API \u91d1\u9470", + "description": "\u5fc5\u9808\u63d0\u4f9b\u7531 UptimeRobot \u53d6\u5f97\u4e00\u7d44\u65b0\u7684 'main' API \u91d1\u9470", "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" }, "user": { "data": { "api_key": "API \u91d1\u9470" }, - "description": "\u9700\u8981\u63d0\u4f9b\u7531 UptimeRobot \u53d6\u5f97\u552f\u8b80 API \u91d1\u9470" + "description": "\u5fc5\u9808\u63d0\u4f9b\u7531 UptimeRobot \u53d6\u5f97\u4e4b 'main' API \u91d1\u9470" } } } diff --git a/homeassistant/components/utility_meter/translations/bg.json b/homeassistant/components/utility_meter/translations/bg.json new file mode 100644 index 00000000000..35cfa0ad1d7 --- /dev/null +++ b/homeassistant/components/utility_meter/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/utility_meter/translations/ca.json b/homeassistant/components/utility_meter/translations/ca.json new file mode 100644 index 00000000000..6781fc9533c --- /dev/null +++ b/homeassistant/components/utility_meter/translations/ca.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "Cicle de reinici del comptador", + "delta_values": "Valors delta", + "name": "Nom", + "net_consumption": "Consum net", + "offset": "Despla\u00e7ament del reinici del comptador", + "source": "Sensor d'entrada", + "tariffs": "Tarifes suportades" + }, + "data_description": { + "delta_values": "Activa-ho si els les lectures s\u00f3n valors delta (actual - anterior) en lloc de valors absoluts.", + "net_consumption": "Activa-ho si \u00e9s un comptador net, \u00e9s a dir, pot augmentar i disminuir.", + "offset": "Despla\u00e7a el dia de restabliment mensual del comptador.", + "tariffs": "Llista de tarifes admeses, deixa-la en blanc si utilitzes una \u00fanica tarifa." + }, + "description": "El sensor de comptador permet fer un seguiment dels consums de diversos serveis (per exemple, energia, gas, aigua o calefacci\u00f3) durant un per\u00edode de temps establert, normalment mensual. El sensor comptador tamb\u00e9 permet dividir el consum per tarifes; en aquest cas, es crear\u00e0 un sensor per a cada tarifa i una entitat de selecci\u00f3 que tria la tarifa actual.", + "title": "Afegeix comptador" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "Sensor d'entrada" + } + } + } + }, + "title": "Comptador" +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/cs.json b/homeassistant/components/utility_meter/translations/cs.json new file mode 100644 index 00000000000..552ce790742 --- /dev/null +++ b/homeassistant/components/utility_meter/translations/cs.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "Cyklus vynulov\u00e1n\u00ed m\u011b\u0159i\u010de", + "delta_values": "Rozd\u00edlov\u00e9 hodnoty", + "name": "Jm\u00e9no", + "net_consumption": "\u010cist\u00e1 spot\u0159eba", + "offset": "Posun vynulov\u00e1n\u00ed m\u011b\u0159i\u010de", + "source": "Vstupn\u00ed senzor", + "tariffs": "Podporovan\u00e9 tarify" + }, + "data_description": { + "delta_values": "Povolte, pokud jsou zdrojov\u00e9 hodnoty rozd\u00edlov\u00e9 hodnoty od posledn\u00edho ode\u010dtu nam\u00edsto absolutn\u00edch hodnot.", + "net_consumption": "Povolte, pokud je zdroj \u010dist\u00fdm m\u011b\u0159i\u010dem, co\u017e znamen\u00e1, \u017ee se m\u016f\u017ee zvy\u0161ovat i sni\u017eovat.", + "offset": "Posunut\u00ed dne m\u011bs\u00ed\u010dn\u00edho vynulov\u00e1n\u00ed m\u011b\u0159i\u010de.", + "tariffs": "Seznam podporovan\u00fdch tarif\u016f, pokud je pot\u0159eba pouze jeden tarif, nechte pr\u00e1zdn\u00e9." + }, + "description": "Vytvo\u0159\u00ed senzor, kter\u00fd sleduje spot\u0159ebu r\u016fzn\u00fdch zdroj\u016f (nap\u0159. energie, plynu, vody, vyt\u00e1p\u011bn\u00ed) za nastaven\u00e9 \u010dasov\u00e9 obdob\u00ed, obvykle m\u011bs\u00ed\u010dn\u011b. Senzor m\u011b\u0159i\u010de m\u00e9di\u00ed voliteln\u011b podporuje rozd\u011blen\u00ed spot\u0159eby podle tarif\u016f, v takov\u00e9m p\u0159\u00edpad\u011b se vytvo\u0159\u00ed jeden senzor pro ka\u017ed\u00fd tarif a tak\u00e9 v\u00fdb\u011brov\u00e1 entita pro v\u00fdb\u011br aktu\u00e1ln\u00edho tarifu.", + "title": "P\u0159idat m\u011b\u0159i\u010d" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "Vstupn\u00ed senzor" + } + } + } + }, + "title": "M\u011b\u0159i\u010d" +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/de.json b/homeassistant/components/utility_meter/translations/de.json new file mode 100644 index 00000000000..870772e0dbe --- /dev/null +++ b/homeassistant/components/utility_meter/translations/de.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "Z\u00e4hler-Reset-Zyklus", + "delta_values": "Delta-Werte", + "name": "Name", + "net_consumption": "Netzverbrauch", + "offset": "Z\u00e4hler-Reset-Offset", + "source": "Eingangssensor", + "tariffs": "Unterst\u00fctzte Tarife" + }, + "data_description": { + "delta_values": "Aktiviere diese Option, wenn die Quellwerte Deltawerte seit dem letzten Lesen anstelle von absoluten Werten sind.", + "net_consumption": "Aktiviere diese Option, wenn die Quelle ein Nettoz\u00e4hler ist, was bedeutet, dass sie sowohl steigen als auch fallen kann.", + "offset": "Versetzen des Tages einer monatlichen Z\u00e4hlerr\u00fccksetzung.", + "tariffs": "Eine Liste der unterst\u00fctzten Tarife; leer lassen, wenn nur ein einziger Tarif ben\u00f6tigt wird." + }, + "description": "Erstelle einen Sensor, der den Verbrauch verschiedener Versorgungsleistungen (z. B. Energie, Gas, Wasser, Heizung) \u00fcber einen konfigurierten Zeitraum, in der Regel monatlich, erfasst. Der Sensor f\u00fcr den Verbrauchsz\u00e4hler unterst\u00fctzt optional die Aufteilung des Verbrauchs nach Tarifen. In diesem Fall wird ein Sensor f\u00fcr jeden Tarif sowie eine Auswahlm\u00f6glichkeit zur Auswahl des aktuellen Tarifs erstellt.", + "title": "Verbrauchsz\u00e4hler hinzuf\u00fcgen" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "Eingangssensor" + } + } + } + }, + "title": "Verbrauchsz\u00e4hler" +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/el.json b/homeassistant/components/utility_meter/translations/el.json new file mode 100644 index 00000000000..3264503bab9 --- /dev/null +++ b/homeassistant/components/utility_meter/translations/el.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "\u039a\u03cd\u03ba\u03bb\u03bf\u03c2 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae", + "delta_values": "\u03a4\u03b9\u03bc\u03ad\u03c2 \u0394\u03ad\u03bb\u03c4\u03b1", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "net_consumption": "\u039a\u03b1\u03b8\u03b1\u03c1\u03ae \u03ba\u03b1\u03c4\u03b1\u03bd\u03ac\u03bb\u03c9\u03c3\u03b7", + "offset": "\u039c\u03b5\u03c4\u03b1\u03c4\u03cc\u03c0\u03b9\u03c3\u03b7 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae", + "source": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5", + "tariffs": "\u03a5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b1 \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03b1" + }, + "data_description": { + "delta_values": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03ac\u03bd \u03bf\u03b9 \u03c4\u03b9\u03bc\u03ad\u03c2 \u03c0\u03b7\u03b3\u03ae\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03b9\u03bc\u03ad\u03c2 \u03b4\u03ad\u03bb\u03c4\u03b1 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03b1\u03c0\u03cc\u03bb\u03c5\u03c4\u03b5\u03c2 \u03c4\u03b9\u03bc\u03ad\u03c2.", + "net_consumption": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03ac\u03bd \u03b7 \u03c0\u03b7\u03b3\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03b1\u03b8\u03b1\u03c1\u03cc\u03c2 \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae\u03c2, \u03b4\u03b7\u03bb\u03b1\u03b4\u03ae \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03c4\u03cc\u03c3\u03bf \u03bd\u03b1 \u03b1\u03c5\u03be\u03ac\u03bd\u03b5\u03c4\u03b1\u03b9 \u03cc\u03c3\u03bf \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03bc\u03b5\u03b9\u03ce\u03bd\u03b5\u03c4\u03b1\u03b9.", + "offset": "\u0391\u03bd\u03c4\u03b9\u03c3\u03c4\u03ac\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b7\u03bc\u03ad\u03c1\u03b1\u03c2 \u03bc\u03b7\u03bd\u03b9\u03b1\u03af\u03b1\u03c2 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 \u03c4\u03bf\u03c5 \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae.", + "tariffs": "\u039c\u03b9\u03b1 \u03bb\u03af\u03c3\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03b1 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b1 \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03b1, \u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03ae \u03b5\u03ac\u03bd \u03c7\u03c1\u03b5\u03b9\u03ac\u03b6\u03b5\u03c4\u03b1\u03b9 \u03bc\u03cc\u03bd\u03bf \u03ad\u03bd\u03b1 \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03bf." + }, + "description": "\u039f \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae \u03ba\u03bf\u03b9\u03bd\u03ae\u03c2 \u03c9\u03c6\u03ad\u03bb\u03b5\u03b9\u03b1\u03c2 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03b9 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7\u03c2 \u03c4\u03c9\u03bd \u03ba\u03b1\u03c4\u03b1\u03bd\u03b1\u03bb\u03ce\u03c3\u03b5\u03c9\u03bd \u03b4\u03b9\u03b1\u03c6\u03cc\u03c1\u03c9\u03bd \u03b2\u03bf\u03b7\u03b8\u03b7\u03c4\u03b9\u03ba\u03ce\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd (\u03c0.\u03c7. \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1, \u03c6\u03c5\u03c3\u03b9\u03ba\u03cc \u03b1\u03ad\u03c1\u03b9\u03bf, \u03bd\u03b5\u03c1\u03cc, \u03b8\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7) \u03c3\u03b5 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03ae \u03c0\u03b5\u03c1\u03af\u03bf\u03b4\u03bf, \u03c3\u03c5\u03bd\u03ae\u03b8\u03c9\u03c2 \u03bc\u03b7\u03bd\u03b9\u03b1\u03af\u03b1. \u039f \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03c4\u03bf\u03c5 \u03b2\u03bf\u03b7\u03b8\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9 \u03b5\u03c0\u03af\u03c3\u03b7\u03c2 \u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03cc \u03c4\u03b7\u03c2 \u03ba\u03b1\u03c4\u03b1\u03bd\u03ac\u03bb\u03c9\u03c3\u03b7\u03c2 \u03b1\u03bd\u03ac \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03b1.\n \u0397 \u03bc\u03b5\u03c4\u03b1\u03c4\u03cc\u03c0\u03b9\u03c3\u03b7 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03b9 \u03c4\u03b7 \u03bc\u03b5\u03c4\u03b1\u03c4\u03cc\u03c0\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b7\u03bc\u03ad\u03c1\u03b1\u03c2 \u03c4\u03b7\u03c2 \u03bc\u03b7\u03bd\u03b9\u03b1\u03af\u03b1\u03c2 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 \u03c4\u03bf\u03c5 \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae.\n \u03a4\u03b1 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b1 \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03bb\u03af\u03c3\u03c4\u03b1 \u03bc\u03b5 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b1 \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b1 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1\u03c4\u03b1, \u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03ae \u03b5\u03ac\u03bd \u03c7\u03c1\u03b5\u03b9\u03ac\u03b6\u03b5\u03c4\u03b1\u03b9 \u03bc\u03cc\u03bd\u03bf \u03ad\u03bd\u03b1 \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03bf.", + "title": "\u039d\u03ad\u03bf\u03c2 \u03b2\u03bf\u03b7\u03b8\u03b7\u03c4\u03b9\u03ba\u03cc\u03c2 \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae\u03c2" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5" + } + } + } + }, + "title": "\u039c\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae\u03c2 \u03ba\u03bf\u03b9\u03bd\u03ae\u03c2 \u03c9\u03c6\u03ad\u03bb\u03b5\u03b9\u03b1\u03c2" +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/et.json b/homeassistant/components/utility_meter/translations/et.json new file mode 100644 index 00000000000..579933130fb --- /dev/null +++ b/homeassistant/components/utility_meter/translations/et.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "M\u00f5\u00f5turi l\u00e4htestamise ts\u00fckkel", + "delta_values": "Delta v\u00e4\u00e4rtused", + "name": "Nimi", + "net_consumption": "Netotarbimine", + "offset": "Arvesti l\u00e4htestamise nihe", + "source": "Sisendandur", + "tariffs": "Toetatud tariifid" + }, + "data_description": { + "delta_values": "Luba kui l\u00e4htev\u00e4\u00e4rtused on absoluutv\u00e4\u00e4rtuste asemel delta v\u00e4\u00e4rtused alates viimasest lugemisest.", + "net_consumption": "Luba, kui allikaks on netoarvesti, mis t\u00e4hendab, et see v\u00f5ib nii suureneda kui ka v\u00e4heneda.", + "offset": "Arvesti igakuise l\u00e4htestamise p\u00e4eva nihutamine.", + "tariffs": "Toetatud tariifide loend, j\u00e4ta t\u00fchjaks kui vajad ainult \u00fchte tariifi." + }, + "description": "Loo andur mis j\u00e4lgib erinevate kommunaalteenuste (nt energia, gaas, vesi, k\u00fcte) tarbimist seadistatud aja jooksul, tavaliselt kord kuus. Kommunaalarvesti andur toetab valikuliselt tarbimise jagamist tariifide kaupa, sellisel juhul luuakse iga tariifi kohta \u00fcks andur ning ka valitud olem kehtiva tariifi valimiseks.", + "title": "Lisa kommunaalm\u00f5\u00f5tja" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "Sisendandur" + } + } + } + }, + "title": "Kommunaalarvesti" +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/fr.json b/homeassistant/components/utility_meter/translations/fr.json new file mode 100644 index 00000000000..9659cf413bb --- /dev/null +++ b/homeassistant/components/utility_meter/translations/fr.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "Cycle de remise \u00e0 z\u00e9ro du compteur", + "delta_values": "Valeurs delta", + "name": "Nom", + "net_consumption": "Consommation nette", + "offset": "D\u00e9calage de la remise \u00e0 z\u00e9ro du compteur", + "source": "Capteur d'entr\u00e9e", + "tariffs": "Tarifs pris en charge" + }, + "data_description": { + "delta_values": "Activer si les valeurs de la source sont des valeurs delta depuis la derni\u00e8re lecture au lieu de valeurs absolues.", + "net_consumption": "Activer si la source est un compteur net, c'est-\u00e0-dire qu'il peut \u00e0 la fois augmenter et diminuer.", + "offset": "D\u00e9calage du jour de r\u00e9initialisation mensuelle du compteur.", + "tariffs": "Liste des tarifs pris en charge\u00a0; laisser vide si un seul tarif est n\u00e9cessaire." + }, + "description": "Cr\u00e9ez un capteur permettant de suivre les consommations de divers services publics (comme l'\u00e9nergie, le gaz, l'eau ou le chauffage) sur une p\u00e9riode configur\u00e9e, g\u00e9n\u00e9ralement mensuelle. Le capteur de compteur de services publics peut, si n\u00e9cessaire, r\u00e9partir la consommation par tarifs, auquel cas un capteur sera cr\u00e9\u00e9 pour chaque tarif ainsi qu'une entit\u00e9 de s\u00e9lection permettant de choisir le tarif actuel.", + "title": "Ajouter un compteur de services publics (eau, gaz, \u00e9lectricit\u00e9\u2026)" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "Capteur d'entr\u00e9e" + } + } + } + }, + "title": "Compteur de services publics (eau, gaz, \u00e9lectricit\u00e9\u2026)" +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/hu.json b/homeassistant/components/utility_meter/translations/hu.json new file mode 100644 index 00000000000..646517cfd24 --- /dev/null +++ b/homeassistant/components/utility_meter/translations/hu.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "M\u00e9r\u0151 null\u00e1z\u00e1si ciklusa", + "delta_values": "Delta \u00e9rt\u00e9kek", + "name": "Elnevez\u00e9s", + "net_consumption": "Nett\u00f3 fogyaszt\u00e1s", + "offset": "M\u00e9r\u0151 null\u00e1z\u00e1s\u00e1nak eltol\u00e1sa", + "source": "Forr\u00e1s \u00e9rz\u00e9kel\u0151", + "tariffs": "T\u00e1mogatott tarif\u00e1k" + }, + "data_description": { + "delta_values": "Akkor kapcsolja be, ha az \u00e9rt\u00e9kek az utols\u00f3 olvasat \u00f3ta k\u00fcl\u00f6nb\u00f6zeti \u00e9rt\u00e9kek, nem pedig abszol\u00fat \u00e9rt\u00e9kek.", + "net_consumption": "Enged\u00e9lyezze, ha a forr\u00e1s nett\u00f3 m\u00e9r\u0151, ami azt jelenti, hogy n\u00f6vekedhet \u00e9s cs\u00f6kkenhet is.", + "offset": "A havi m\u00e9r\u0151-vissza\u00e1ll\u00edt\u00e1s napj\u00e1nak eltol\u00e1sa.", + "tariffs": "A t\u00e1mogatott tarif\u00e1k list\u00e1ja, hagyja \u00fcresen, ha csak egyetlen tarif\u00e1ra van sz\u00fcks\u00e9g." + }, + "description": "A k\u00f6z\u00fczemi fogyaszt\u00e1sm\u00e9r\u0151 \u00e9rz\u00e9kel\u0151 lehet\u0151s\u00e9get ad a k\u00fcl\u00f6nf\u00e9le k\u00f6zm\u0171vek (pl. energia, g\u00e1z, v\u00edz, f\u0171t\u00e9s) fogyaszt\u00e1s\u00e1nak nyomon k\u00f6vet\u00e9s\u00e9re egy konfigur\u00e1lt id\u0151tartamon kereszt\u00fcl, jellemz\u0151en havonta. Az \u00e9rz\u00e9kel\u0151 a fogyaszt\u00e1s tarif\u00e1k szerinti megoszt\u00e1s\u00e1t is t\u00e1mogatja.", + "title": "\u00daj k\u00f6z\u00fczemi m\u00e9r\u0151" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "Forr\u00e1s \u00e9rz\u00e9kel\u0151" + } + } + } + }, + "title": "K\u00f6z\u00fczemi fogyaszt\u00e1sm\u00e9r\u0151" +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/id.json b/homeassistant/components/utility_meter/translations/id.json new file mode 100644 index 00000000000..d2f9bab72c5 --- /dev/null +++ b/homeassistant/components/utility_meter/translations/id.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "Siklus setel ulang meter", + "delta_values": "Nilai delta", + "name": "Nama", + "net_consumption": "Konsumsi netto", + "offset": "Offset setel ulang meter", + "source": "Sensor input", + "tariffs": "Tarif yang didukung" + }, + "data_description": { + "delta_values": "Aktifkan jika nilai sumber adalah nilai delta sejak pembacaan terakhir, bukan nilai absolut.", + "net_consumption": "Aktifkan jika sumbernya adalah ukuran netto, artinya bisa bertambah dan berkurang.", + "offset": "Ofset hari setelah penyetelan ulang bulanan", + "tariffs": "Daftar tarif yang didukung, kosongkan jika hanya diperlukan satu tarif." + }, + "description": "Buat sensor yang melacak konsumsi berbagai utilitas (misalnya, energi, gas, air, pemanas) selama periode waktu yang dikonfigurasi, biasanya bulanan. Sensor meter utilitas secara opsional mendukung pemisahan konsumsi berdasarkan tarif, dalam hal ini satu sensor untuk setiap tarif dibuat serta entitas terpilih untuk memilih tarif saat ini.", + "title": "Tambahkan Meter Utilitas" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "Sensor Input" + } + } + } + }, + "title": "Meter Utilitas" +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/it.json b/homeassistant/components/utility_meter/translations/it.json new file mode 100644 index 00000000000..03512cc0b7a --- /dev/null +++ b/homeassistant/components/utility_meter/translations/it.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "Ciclo di ripristino del contatore", + "delta_values": "Valori delta", + "name": "Nome", + "net_consumption": "Consumo netto", + "offset": "Offset azzeramento contatore", + "source": "Sensore di ingresso", + "tariffs": "Tariffe supportate" + }, + "data_description": { + "delta_values": "Abilita se i valori di origine sono valori delta dall'ultima lettura anzich\u00e9 valori assoluti.", + "net_consumption": "Abilita se la sorgente \u00e8 un contatore netto, il che significa che pu\u00f2 sia aumentare che diminuire.", + "offset": "Offset del giorno di un ripristino mensile del contatore.", + "tariffs": "Un elenco di tariffe supportate, lascia vuoto se \u00e8 necessaria una sola tariffa." + }, + "description": "Crea un sensore che tenga traccia del consumo di varie utenze (ad es. energia, gas, acqua, riscaldamento) in un periodo di tempo configurato, generalmente mensile. Il sensore del contatore di utenze supporta opzionalmente la suddivisione del consumo per tariffe, in tal caso viene creato un sensore per ciascuna tariffa e un'entit\u00e0 selezionata per scegliere la tariffa corrente.", + "title": "Aggiungi misuratore di utilit\u00e0" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "Sensore di ingresso" + } + } + } + }, + "title": "Misuratore di utilit\u00e0" +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/ja.json b/homeassistant/components/utility_meter/translations/ja.json new file mode 100644 index 00000000000..90c175f16ec --- /dev/null +++ b/homeassistant/components/utility_meter/translations/ja.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "\u30e1\u30fc\u30bf\u30fc\u30ea\u30bb\u30c3\u30c8\u306e\u30b5\u30a4\u30af\u30eb", + "delta_values": "\u30c7\u30eb\u30bf\u5024", + "name": "\u540d\u524d", + "net_consumption": "\u7d14\u6d88\u8cbb\u91cf", + "offset": "\u30e1\u30fc\u30bf\u30fc\u30ea\u30bb\u30c3\u30c8\u30aa\u30d5\u30bb\u30c3\u30c8", + "source": "\u5165\u529b\u30bb\u30f3\u30b5\u30fc", + "tariffs": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u95a2\u7a0e(Tariffs)" + }, + "data_description": { + "delta_values": "\u30bd\u30fc\u30b9\u5024\u304c\u7d76\u5bfe\u5024\u3067\u306f\u306a\u304f\u3001\u6700\u5f8c\u306e\u8aad\u307f\u53d6\u308a\u4ee5\u964d\u304c\u30c7\u30eb\u30bf\u5024\u3067\u3042\u308b\u5834\u5408\u306f\u3001\u6709\u52b9\u306b\u3057\u307e\u3059\u3002", + "net_consumption": "\u30bd\u30fc\u30b9\u304c\u30cd\u30c3\u30c8\u30e1\u30fc\u30bf(net meter)\u3001\u3064\u307e\u308a\u5897\u52a0\u3082\u3059\u308b\u3057\u6e1b\u5c11\u3082\u3059\u308b\u5834\u5408\u306f\u3001\u6709\u52b9\u306b\u3057\u307e\u3059\u3002", + "offset": "\u6bce\u6708\u306e\u30e1\u30fc\u30bf\u30fc\u30ea\u30bb\u30c3\u30c8\u306e\u65e5\u3092\u30aa\u30d5\u30bb\u30c3\u30c8\u3057\u307e\u3059\u3002", + "tariffs": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u6599\u91d1(Tariff)\u306e\u30ea\u30b9\u30c8\u3002\u5358\u4e00\u306e\u6599\u91d1\u8868\u306e\u307f\u304c\u5fc5\u8981\u306a\u5834\u5408\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u307e\u3059\u3002" + }, + "description": "\u8a2d\u5b9a\u3055\u308c\u305f\u671f\u9593\u3001\u901a\u5e38\u306f\u6bce\u6708\u3001\u69d8\u3005\u306a\u30e6\u30fc\u30c6\u30a3\u30ea\u30c6\u30a3(\u30a8\u30cd\u30eb\u30ae\u30fc\u3001\u30ac\u30b9\u3001\u6c34\u3001\u6696\u623f\u306a\u3069)\u306e\u6d88\u8cbb\u91cf\u3092\u8ffd\u8de1\u3059\u308b\u30bb\u30f3\u30b5\u30fc\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002\u30e6\u30fc\u30c6\u30a3\u30ea\u30c6\u30a3\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u30aa\u30d7\u30b7\u30e7\u30f3\u3068\u3057\u3066\u3001\u6599\u91d1\u8868\u306b\u3088\u308b\u6d88\u8cbb\u91cf\u306e\u5206\u5272\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u307e\u3059\u3002\u3053\u306e\u5834\u5408\u3001\u5404\u6599\u91d1\u8868\u306e\u305f\u3081\u306e1\u3064\u306e\u30bb\u30f3\u30b5\u30fc\u3068\u3001\u73fe\u5728\u306e\u6599\u91d1(Tariff)\u3092\u9078\u629e\u3059\u308b\u305f\u3081\u306e\u9078\u629e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u4f5c\u6210\u3055\u308c\u307e\u3059\u3002", + "title": "\u65b0\u3057\u3044\u30e6\u30fc\u30c6\u30a3\u30ea\u30c6\u30a3\u30e1\u30fc\u30bf\u30fc" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "\u5165\u529b\u30bb\u30f3\u30b5\u30fc" + } + } + } + }, + "title": "\u30e6\u30fc\u30c6\u30a3\u30ea\u30c6\u30a3\u30e1\u30fc\u30bf\u30fc" +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/nl.json b/homeassistant/components/utility_meter/translations/nl.json new file mode 100644 index 00000000000..8e1d986ac6b --- /dev/null +++ b/homeassistant/components/utility_meter/translations/nl.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "Meter reset cyclus", + "delta_values": "Deltawaarden", + "name": "Name", + "net_consumption": "Netto verbruik", + "offset": "Meter reset offset", + "source": "Invoer sensor", + "tariffs": "Ondersteunde tarieven" + }, + "data_description": { + "delta_values": "Schakel in als de bronwaarden deltawaarden zijn sinds de laatste meting in plaats van absolute waarden.", + "net_consumption": "Schakel in als de bron een nettometer is, wat betekent dat deze zowel kan toenemen als afnemen.", + "offset": "Compenseer de dag van een maandelijkse meterreset.", + "tariffs": "Een lijst met ondersteunde tarieven, laat leeg als er maar \u00e9\u00e9n tarief nodig is." + }, + "description": "Cre\u00eber een sensor die het verbruik van verschillende nutsvoorzieningen (bv. energie, gas, water, verwarming) over een geconfigureerde tijdsperiode bijhoudt, meestal maandelijks. De sensor voor de meter voor nutsvoorzieningen ondersteunt optioneel het opsplitsen van het verbruik in tarieven, in dat geval wordt een sensor voor elk tarief aangemaakt, evenals een selectie-entiteit om het huidige tarief te kiezen.", + "title": "Nutsmeter toevoegen" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "Invoer sensor" + } + } + } + }, + "title": "Nutsmeter" +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/no.json b/homeassistant/components/utility_meter/translations/no.json new file mode 100644 index 00000000000..05218c7010b --- /dev/null +++ b/homeassistant/components/utility_meter/translations/no.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "Syklus for tilbakestilling av m\u00e5ler", + "delta_values": "Delta-verdier", + "name": "Navn", + "net_consumption": "Netto forbruk", + "offset": "Forskyvning for tilbakestilling av meter", + "source": "Inngangssensor", + "tariffs": "St\u00f8ttede tariffer" + }, + "data_description": { + "delta_values": "Aktiver hvis kildeverdiene er deltaverdier siden siste lesing i stedet for absolutte verdier.", + "net_consumption": "Aktiver hvis kilden er en nettom\u00e5ler, noe som betyr at den b\u00e5de kan \u00f8ke og redusere.", + "offset": "Forskyv dagen for en m\u00e5nedlig tilbakestilling av m\u00e5leren.", + "tariffs": "En liste over st\u00f8ttede tariffer, la st\u00e5 tom hvis bare en enkelt tariff er n\u00f8dvendig." + }, + "description": "Lag en sensor som sporer forbruket til ulike verkt\u00f8y (f.eks. energi, gass, vann, oppvarming) over en konfigurert tidsperiode, vanligvis m\u00e5nedlig. M\u00e5lersensoren st\u00f8tter valgfritt \u00e5 dele forbruket etter tariffer, i s\u00e5 fall opprettes en sensor for hver tariff samt en valgt enhet for \u00e5 velge gjeldende tariff.", + "title": "Legg til verkt\u00f8ym\u00e5ler" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "Inngangssensor" + } + } + } + }, + "title": "Verkt\u00f8ym\u00e5ler" +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/pl.json b/homeassistant/components/utility_meter/translations/pl.json new file mode 100644 index 00000000000..9be6f68384b --- /dev/null +++ b/homeassistant/components/utility_meter/translations/pl.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "Cykl resetowania licznika", + "delta_values": "Warto\u015bci delta", + "name": "Nazwa", + "net_consumption": "Zu\u017cycie netto", + "offset": "Przesuni\u0119cie resetowania licznika", + "source": "Sensor wej\u015bciowy", + "tariffs": "Obs\u0142ugiwane taryfy" + }, + "data_description": { + "delta_values": "W\u0142\u0105cz, je\u015bli warto\u015bci \u017ar\u00f3d\u0142owe s\u0105 warto\u015bciami delta od ostatniego odczytu, a nie warto\u015bciami bezwzgl\u0119dnymi.", + "net_consumption": "W\u0142\u0105cz, je\u015bli \u017ar\u00f3d\u0142em jest licznik netto, co oznacza, \u017ce jego warto\u015bci mog\u0105 si\u0119 zar\u00f3wno zwi\u0119ksza\u0107 jak i zmniejsza\u0107.", + "offset": "Przesuni\u0119cie dnia miesi\u0119cznego zerowania licznika.", + "tariffs": "Lista obs\u0142ugiwanych taryf. Pozostaw puste, je\u015bli potrzebna jest tylko jedna taryfa." + }, + "description": "Utw\u00f3rz sensor, kt\u00f3ry \u015bledzi u\u017cycie r\u00f3\u017cnych medi\u00f3w (np. energii, gazu, wody, ogrzewania) przez skonfigurowany okres czasu, zwykle co miesi\u0105c. Sensor licznika medi\u00f3w opcjonalnie obs\u0142uguje podzia\u0142 u\u017cycia wed\u0142ug taryf, w takim przypadku tworzony jest jeden czujnik dla ka\u017cdej taryfy oraz encja do wyboru aktualnej taryfy.", + "title": "Dodaj licznik medi\u00f3w" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "Sensor wej\u015bciowy" + } + } + } + }, + "title": "Licznik medi\u00f3w" +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/pt-BR.json b/homeassistant/components/utility_meter/translations/pt-BR.json new file mode 100644 index 00000000000..856360b05b2 --- /dev/null +++ b/homeassistant/components/utility_meter/translations/pt-BR.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "Ciclo de reinicializa\u00e7\u00e3o do medidor", + "delta_values": "Valores delta", + "name": "Nome", + "net_consumption": "Consumo l\u00edquido", + "offset": "Compensa\u00e7\u00e3o do reajuste do medidor", + "source": "Sensor de entrada", + "tariffs": "Tarifas compat\u00edveis" + }, + "data_description": { + "delta_values": "Habilite se os valores de origem forem valores delta desde a \u00faltima leitura em vez de valores absolutos.", + "net_consumption": "Ative se a fonte for um medidor de rede, o que significa que pode aumentar e diminuir.", + "offset": "Desloque o dia de uma reinicializa\u00e7\u00e3o mensal do medidor.", + "tariffs": "Uma lista de tarifas suportadas, deixe em branco se apenas uma \u00fanica tarifa for necess\u00e1ria." + }, + "description": "Crie um sensor que rastreie o consumo de v\u00e1rias utilidades (por exemplo, energia, g\u00e1s, \u00e1gua, aquecimento) durante um per\u00edodo de tempo configurado, normalmente mensalmente. O sensor do contador da concession\u00e1ria suporta opcionalmente a divis\u00e3o do consumo por tarif\u00e1rio, neste caso \u00e9 criado um sensor para cada tarif\u00e1rio e uma entidade seleccionada para escolher o tarif\u00e1rio atual.", + "title": "Adicionar medidor de utilidades" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "Sensor de entrada" + } + } + } + }, + "title": "Medidor de utilidade" +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/ru.json b/homeassistant/components/utility_meter/translations/ru.json new file mode 100644 index 00000000000..3eda01a8116 --- /dev/null +++ b/homeassistant/components/utility_meter/translations/ru.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "\u0426\u0438\u043a\u043b \u0441\u0431\u0440\u043e\u0441\u0430 \u0441\u0447\u0435\u0442\u0447\u0438\u043a\u0430", + "delta_values": "\u0414\u0435\u043b\u044c\u0442\u0430-\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "net_consumption": "\u0427\u0438\u0441\u0442\u043e\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u043b\u0435\u043d\u0438\u0435", + "offset": "\u0421\u043c\u0435\u0449\u0435\u043d\u0438\u0435 \u0441\u0431\u0440\u043e\u0441\u0430 \u0441\u0447\u0435\u0442\u0447\u0438\u043a\u0430", + "source": "\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440", + "tariffs": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u0442\u0430\u0440\u0438\u0444\u044b" + }, + "data_description": { + "delta_values": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0435, \u0435\u0441\u043b\u0438 \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u044f\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0434\u0435\u043b\u044c\u0442\u0430-\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u0441 \u043c\u043e\u043c\u0435\u043d\u0442\u0430 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0433\u043e \u0447\u0442\u0435\u043d\u0438\u044f, \u0430 \u043d\u0435 \u0430\u0431\u0441\u043e\u043b\u044e\u0442\u043d\u044b\u043c\u0438 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043c\u0438.", + "net_consumption": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0435, \u0435\u0441\u043b\u0438 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u0435\u0442\u0442\u043e-\u0441\u0447\u0435\u0442\u0447\u0438\u043a\u043e\u043c, \u0442\u043e \u0435\u0441\u0442\u044c \u043e\u043d \u043c\u043e\u0436\u0435\u0442 \u043a\u0430\u043a \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0442\u044c\u0441\u044f, \u0442\u0430\u043a \u0438 \u0443\u043c\u0435\u043d\u044c\u0448\u0430\u0442\u044c\u0441\u044f.", + "offset": "\u0421\u043c\u0435\u0449\u0435\u043d\u0438\u0435 \u0434\u043d\u044f \u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e\u0433\u043e \u043e\u0431\u043d\u0443\u043b\u0435\u043d\u0438\u044f \u0441\u0447\u0435\u0442\u0447\u0438\u043a\u0430.", + "tariffs": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0445 \u0442\u0430\u0440\u0438\u0444\u043e\u0432, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0435\u0441\u043b\u0438 \u043d\u0443\u0436\u0435\u043d \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u0442\u0430\u0440\u0438\u0444." + }, + "description": "\u0421\u0435\u043d\u0441\u043e\u0440 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0435\u0442 \u0444\u0443\u043d\u043a\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u044b\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u0434\u043b\u044f \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u0442\u0440\u0435\u0431\u043b\u0435\u043d\u0438\u044f \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0445 \u043a\u043e\u043c\u043c\u0443\u043d\u0430\u043b\u044c\u043d\u044b\u0445 \u0443\u0441\u043b\u0443\u0433 (\u044d\u043d\u0435\u0440\u0433\u0438\u0438, \u0433\u0430\u0437\u0430, \u0432\u043e\u0434\u044b, \u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u044f) \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0438\u043e\u0434\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043e\u0431\u044b\u0447\u043d\u043e \u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e. \u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u043b\u0435\u043d\u0438\u044f \u043f\u043e \u0442\u0430\u0440\u0438\u0444\u0430\u043c, \u0432 \u044d\u0442\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0431\u0443\u0434\u0435\u0442 \u0441\u043e\u0437\u0434\u0430\u043d \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0442\u0430\u0440\u0438\u0444\u0430, \u0430 \u0442\u0430\u043a \u0436\u0435 \u043e\u0431\u044a\u0435\u043a\u0442 \u0434\u043b\u044f \u0432\u044b\u0431\u043e\u0440\u0430 \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u0442\u0430\u0440\u0438\u0444\u0430.", + "title": "\u0421\u0447\u0435\u0442\u0447\u0438\u043a \u043a\u043e\u043c\u043c\u0443\u043d\u0430\u043b\u044c\u043d\u044b\u0445 \u0443\u0441\u043b\u0443\u0433" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440" + } + } + } + }, + "title": "\u0421\u0447\u0435\u0442\u0447\u0438\u043a \u043a\u043e\u043c\u043c\u0443\u043d\u0430\u043b\u044c\u043d\u044b\u0445 \u0443\u0441\u043b\u0443\u0433" +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/tr.json b/homeassistant/components/utility_meter/translations/tr.json new file mode 100644 index 00000000000..0eedf15d93e --- /dev/null +++ b/homeassistant/components/utility_meter/translations/tr.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "Saya\u00e7 s\u0131f\u0131rlama d\u00f6ng\u00fcs\u00fc", + "delta_values": "Delta de\u011ferleri", + "name": "Ad", + "net_consumption": "Net t\u00fcketim", + "offset": "Saya\u00e7 s\u0131f\u0131rlama uzakl\u0131\u011f\u0131", + "source": "Giri\u015f sens\u00f6r\u00fc", + "tariffs": "Desteklenen tarifeler" + }, + "data_description": { + "delta_values": "Kaynak de\u011ferler, mutlak de\u011ferler yerine son okumadan itibaren delta de\u011ferler ise etkinle\u015ftirin.", + "net_consumption": "Kaynak bir net saya\u00e7 ise etkinle\u015ftirin, yani hem artabilir hem de azalabilir.", + "offset": "Ayl\u0131k saya\u00e7 s\u0131f\u0131rlama g\u00fcn\u00fcn\u00fc dengeleyin.", + "tariffs": "Desteklenen tarifelerin listesi, yaln\u0131zca tek bir tarife gerekiyorsa bo\u015f b\u0131rak\u0131n." + }, + "description": "\u00c7e\u015fitli hizmetlerin (\u00f6rn. enerji, gaz, su, \u0131s\u0131tma) t\u00fcketimini, yap\u0131land\u0131r\u0131lm\u0131\u015f bir s\u00fcre boyunca, genellikle ayl\u0131k olarak izleyen bir sens\u00f6r olu\u015fturun. \u015eebeke sayac\u0131 sens\u00f6r\u00fc iste\u011fe ba\u011fl\u0131 olarak t\u00fcketimin tarifelere g\u00f6re b\u00f6l\u00fcnmesini destekler, bu durumda her tarife i\u00e7in bir sens\u00f6r ve mevcut tarifeyi se\u00e7mek i\u00e7in se\u00e7ilen bir varl\u0131k olu\u015fturulur.", + "title": "Kullan\u0131m Sayac\u0131 Ekle" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "Giri\u015f sens\u00f6r\u00fc" + } + } + } + }, + "title": "Yard\u0131mc\u0131 Saya\u00e7" +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/zh-Hans.json b/homeassistant/components/utility_meter/translations/zh-Hans.json new file mode 100644 index 00000000000..a1db639adfb --- /dev/null +++ b/homeassistant/components/utility_meter/translations/zh-Hans.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "\u8ba1\u91cf\u590d\u4f4d\u5468\u671f", + "delta_values": "\u8f93\u5165\u4e3a\u5dee\u503c", + "name": "\u540d\u79f0", + "net_consumption": "\u6570\u503c\u53ef\u56de\u9000", + "offset": "\u8ba1\u91cf\u590d\u4f4d\u504f\u79fb", + "source": "\u8f93\u5165\u4f20\u611f\u5668", + "tariffs": "\u652f\u6301\u7684\u8d39\u7387" + }, + "data_description": { + "delta_values": "\u5982\u679c\u6e90\u6570\u636e\u662f\u81ea\u4e0a\u6b21\u8bfb\u6570\u4ee5\u6765\u7684\u5dee\u503c\uff0c\u8bf7\u542f\u7528\u6b64\u9009\u9879\u3002", + "net_consumption": "\u5982\u679c\u6e90\u6570\u636e\u4e0d\u662f\u6301\u7eed\u589e\u957f\uff0c\u800c\u662f\u53ef\u4ee5\u964d\u4f4e\uff0c\u8bf7\u542f\u7528\u6b64\u9009\u9879\u3002", + "offset": "\u6309\u6708\u8ba1\u91cf\u65f6\u590d\u4f4d\u63a8\u8fdf\u7684\u5929\u6570\u3002", + "tariffs": "\u4ee5\u5217\u8868\u7684\u5f62\u5f0f\u5217\u51fa\u6240\u6709\u53ef\u80fd\u7684\u8d39\u7387\u3002\u5982\u679c\u8d39\u7387\u552f\u4e00\uff0c\u8bf7\u7559\u7a7a\u3002" + }, + "description": "\u521b\u5efa\u4f20\u611f\u5668\u6765\u5468\u671f\u6027\u5730\u76d1\u6d4b\u516c\u5171\u80fd\u6e90\uff08\u4f8b\u5982\u6c34\u3001\u7535\u3001\u71c3\u6c14\uff09\u7684\u6d88\u8017\u60c5\u51b5\u3002\u6b64\u7c7b\u4f20\u611f\u5668\u8fd8\u652f\u6301\u591a\u8d39\u7387\u5206\u522b\u7edf\u8ba1\uff08\u4f8b\u5982\u9636\u68af\u6c34\u4ef7\u3001\u5cf0\u8c37\u7535\u4ef7\uff09\uff0c\u4e3a\u6bcf\u79cd\u8d39\u7387\u5206\u522b\u521b\u5efa\u4e00\u4e2a\u4f20\u611f\u5668\uff0c\u5e76\u63d0\u4f9b\u4e00\u4e2a\u9009\u62e9\u5668\u5b9e\u4f53\u6765\u9009\u62e9\u5f53\u524d\u6267\u884c\u7684\u8d39\u7387\u3002", + "title": "\u6dfb\u52a0\u4eea\u8868\u7edf\u8ba1" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "\u8f93\u5165\u4f20\u611f\u5668" + } + } + } + }, + "title": "\u4eea\u8868\u7edf\u8ba1" +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/zh-Hant.json b/homeassistant/components/utility_meter/translations/zh-Hant.json new file mode 100644 index 00000000000..dc23f73a89e --- /dev/null +++ b/homeassistant/components/utility_meter/translations/zh-Hant.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "\u529f\u8017\u8868\u91cd\u7f6e\u9031\u671f", + "delta_values": "\u589e\u91cf\u503c", + "name": "\u540d\u7a31", + "net_consumption": "\u6de8\u8017\u80fd", + "offset": "\u529f\u8017\u8868\u91cd\u7f6e\u504f\u79fb", + "source": "\u8f38\u5165\u611f\u6e2c\u5668", + "tariffs": "\u652f\u63f4\u5206\u5340\u9069\u7528\u8cbb\u7387" + }, + "data_description": { + "delta_values": "\u5047\u5982\u4f86\u6e90\u70ba\u4e0a\u6b21\u8b80\u53d6\u589e\u91cf\u503c\u3001\u800c\u975e\u7d55\u5c0d\u503c\u5247\u958b\u555f\u3002", + "net_consumption": "\u5047\u5982\u4f86\u6e90\u70ba\u6de8\u529f\u8017\u5247\u958b\u555f\u3001\u8868\u793a\u53ef\u540c\u6642\u589e\u52a0\u53ca\u6e1b\u5c11\u3002", + "offset": "\u6bcf\u6708\u529f\u8017\u91cd\u7f6e\u504f\u79fb\u5929\u6578\u3002", + "tariffs": "\u652f\u63f4\u5206\u5340\u9069\u7528\u8cbb\u7387\u5217\u8868\uff0c\u5047\u5982\u9700\u8981\u55ae\u4e00\u532f\u7387\u3001\u5247\u4fdd\u7559\u7a7a\u767d\u3002" + }, + "description": "\u65b0\u589e\u65bc\u8a2d\u5b9a\u9031\u671f\u6642\u9593\u5167\uff08\u901a\u5e38\u70ba\u6bcf\u6708\uff09\u8ddf\u8e2a\u5404\u7a2e\u516c\u7528\u4e8b\u696d\uff08\u4f8b\u5982\uff0c\u96fb\u529b\u3001\u5929\u7136\u6c23\u3001\u6c34\u3001\u4f9b\u6696\uff09\u8017\u80fd\u6578\u503c\u611f\u6e2c\u5668\u3002\u529f\u8017\u8868\u611f\u6e2c\u5668\u540c\u6642\u9078\u9805\u652f\u63f4\u5206\u5340\u9069\u7528\u8cbb\u7387\u8a08\u7b97\uff0c\u5728\u6b64\u60c5\u6cc1\u4e0b\u3001\u6bcf\u500b\u8cbb\u7387\u7686\u6703\u65b0\u589e\u611f\u6e2c\u5668\u3001\u53ca\u9078\u64c7\u5be6\u9ad4\u4ee5\u9078\u64c7\u76ee\u524d\u8cbb\u7387\u3002", + "title": "\u65b0\u589e\u529f\u8017\u8868" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "\u8f38\u5165\u611f\u6e2c\u5668" + } + } + } + }, + "title": "\u529f\u8017\u8868" +} \ No newline at end of file diff --git a/homeassistant/components/vallox/translations/bg.json b/homeassistant/components/vallox/translations/bg.json index 1c2e0aee24c..ec27ad32d0b 100644 --- a/homeassistant/components/vallox/translations/bg.json +++ b/homeassistant/components/vallox/translations/bg.json @@ -14,10 +14,8 @@ "step": { "user": { "data": { - "host": "\u0425\u043e\u0441\u0442", - "name": "\u0418\u043c\u0435" - }, - "title": "Vallox" + "host": "\u0425\u043e\u0441\u0442" + } } } } diff --git a/homeassistant/components/vallox/translations/ca.json b/homeassistant/components/vallox/translations/ca.json index 7a92c24a558..ba8d6287ed4 100644 --- a/homeassistant/components/vallox/translations/ca.json +++ b/homeassistant/components/vallox/translations/ca.json @@ -14,11 +14,8 @@ "step": { "user": { "data": { - "host": "Amfitri\u00f3", - "name": "Nom" - }, - "description": "Configura la integraci\u00f3 Vallox. Si tens problemes amb la configuraci\u00f3, v\u00e9s a {integration_docs_url}.", - "title": "Vallox" + "host": "Amfitri\u00f3" + } } } } diff --git a/homeassistant/components/vallox/translations/cs.json b/homeassistant/components/vallox/translations/cs.json index 2a364830dbf..ffef74e8f9c 100644 --- a/homeassistant/components/vallox/translations/cs.json +++ b/homeassistant/components/vallox/translations/cs.json @@ -8,13 +8,6 @@ }, "error": { "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - }, - "step": { - "user": { - "data": { - "name": "Jm\u00e9no" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/vallox/translations/de.json b/homeassistant/components/vallox/translations/de.json index 661f45bdfcf..711ef828ad3 100644 --- a/homeassistant/components/vallox/translations/de.json +++ b/homeassistant/components/vallox/translations/de.json @@ -14,11 +14,8 @@ "step": { "user": { "data": { - "host": "Host", - "name": "Name" - }, - "description": "Richte die Vallox-Integration ein. Wenn du Probleme mit der Konfiguration hast, gehe zu {integration_docs_url} .", - "title": "Vallox" + "host": "Host" + } } } } diff --git a/homeassistant/components/vallox/translations/el.json b/homeassistant/components/vallox/translations/el.json index 4b15c926247..cab5c683ae5 100644 --- a/homeassistant/components/vallox/translations/el.json +++ b/homeassistant/components/vallox/translations/el.json @@ -14,11 +14,8 @@ "step": { "user": { "data": { - "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1" - }, - "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Vallox. \u0395\u03ac\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {integration_docs_url}.", - "title": "Vallox" + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } } } } diff --git a/homeassistant/components/vallox/translations/en.json b/homeassistant/components/vallox/translations/en.json index efa32604646..3390fd34854 100644 --- a/homeassistant/components/vallox/translations/en.json +++ b/homeassistant/components/vallox/translations/en.json @@ -14,11 +14,8 @@ "step": { "user": { "data": { - "host": "Host", - "name": "Name" - }, - "description": "Set up the Vallox integration. If you have problems with configuration go to {integration_docs_url}.", - "title": "Vallox" + "host": "Host" + } } } } diff --git a/homeassistant/components/vallox/translations/es.json b/homeassistant/components/vallox/translations/es.json index 3373f38a725..373ae333ec3 100644 --- a/homeassistant/components/vallox/translations/es.json +++ b/homeassistant/components/vallox/translations/es.json @@ -14,11 +14,8 @@ "step": { "user": { "data": { - "host": "Host", - "name": "Nombre" - }, - "description": "Configure la integraci\u00f3n de Vallox. Si tiene problemas con la configuraci\u00f3n, vaya a {integration_docs_url} .", - "title": "Vallox" + "host": "Host" + } } } } diff --git a/homeassistant/components/vallox/translations/et.json b/homeassistant/components/vallox/translations/et.json index 8f3a1c7cb3e..14f1854aded 100644 --- a/homeassistant/components/vallox/translations/et.json +++ b/homeassistant/components/vallox/translations/et.json @@ -14,11 +14,8 @@ "step": { "user": { "data": { - "host": "Host", - "name": "Nimi" - }, - "description": "Seadista Valloxi sidumine. Kui seadistamisega on probleeme, mine aadressile {integration_docs_url}.", - "title": "Vallox" + "host": "Host" + } } } } diff --git a/homeassistant/components/vallox/translations/fr.json b/homeassistant/components/vallox/translations/fr.json index c9823756185..c5db6b81e9e 100644 --- a/homeassistant/components/vallox/translations/fr.json +++ b/homeassistant/components/vallox/translations/fr.json @@ -14,11 +14,8 @@ "step": { "user": { "data": { - "host": "H\u00f4te", - "name": "Nom" - }, - "description": "Mettre en place l'int\u00e9gration Vallox. Si vous rencontrez des probl\u00e8mes de configuration, acc\u00e9dez \u00e0 {integration_docs_url} .", - "title": "Vallox" + "host": "H\u00f4te" + } } } } diff --git a/homeassistant/components/vallox/translations/he.json b/homeassistant/components/vallox/translations/he.json index ad8f603cb5a..067445268a8 100644 --- a/homeassistant/components/vallox/translations/he.json +++ b/homeassistant/components/vallox/translations/he.json @@ -14,8 +14,7 @@ "step": { "user": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7", - "name": "\u05e9\u05dd" + "host": "\u05de\u05d0\u05e8\u05d7" } } } diff --git a/homeassistant/components/vallox/translations/hu.json b/homeassistant/components/vallox/translations/hu.json index 1b3366b5f0e..58404c9ad73 100644 --- a/homeassistant/components/vallox/translations/hu.json +++ b/homeassistant/components/vallox/translations/hu.json @@ -14,11 +14,8 @@ "step": { "user": { "data": { - "host": "C\u00edm", - "name": "N\u00e9v" - }, - "description": "A Vallox integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sa. Ha probl\u00e9m\u00e1i vannak a konfigur\u00e1ci\u00f3val, l\u00e1togasson el a https://www.home-assistant.io/integrations/vallox oldalra.", - "title": "Vallox" + "host": "C\u00edm" + } } } } diff --git a/homeassistant/components/vallox/translations/id.json b/homeassistant/components/vallox/translations/id.json index 4f8fbbff6e9..90e7c60b49f 100644 --- a/homeassistant/components/vallox/translations/id.json +++ b/homeassistant/components/vallox/translations/id.json @@ -14,11 +14,8 @@ "step": { "user": { "data": { - "host": "Host", - "name": "Nama" - }, - "description": "Siapkan integrasi Vallox Jika Anda memiliki masalah dengan konfigurasi, buka {integration_docs_url}.", - "title": "Vallox" + "host": "Host" + } } } } diff --git a/homeassistant/components/vallox/translations/it.json b/homeassistant/components/vallox/translations/it.json index 9aa0e929c50..c82991be7e7 100644 --- a/homeassistant/components/vallox/translations/it.json +++ b/homeassistant/components/vallox/translations/it.json @@ -14,11 +14,8 @@ "step": { "user": { "data": { - "host": "Host", - "name": "Nome" - }, - "description": "Configura l'integrazione Vallox. Se hai problemi con la configurazione vai su {integration_docs_url}.", - "title": "Vallox" + "host": "Host" + } } } } diff --git a/homeassistant/components/vallox/translations/ja.json b/homeassistant/components/vallox/translations/ja.json index bac8c6a7732..a5467460d22 100644 --- a/homeassistant/components/vallox/translations/ja.json +++ b/homeassistant/components/vallox/translations/ja.json @@ -14,11 +14,8 @@ "step": { "user": { "data": { - "host": "\u30db\u30b9\u30c8", - "name": "\u540d\u524d" - }, - "description": "Vallox\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/vallox \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "Vallox" + "host": "\u30db\u30b9\u30c8" + } } } } diff --git a/homeassistant/components/vallox/translations/lv.json b/homeassistant/components/vallox/translations/lv.json deleted file mode 100644 index cee9047f155..00000000000 --- a/homeassistant/components/vallox/translations/lv.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "Vallox" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/vallox/translations/nl.json b/homeassistant/components/vallox/translations/nl.json index 9cc6ea07a93..b8b5b6bdc57 100644 --- a/homeassistant/components/vallox/translations/nl.json +++ b/homeassistant/components/vallox/translations/nl.json @@ -14,11 +14,8 @@ "step": { "user": { "data": { - "host": "Host", - "name": "Naam" - }, - "description": "Stel de Vallox integratie in. Als u problemen heeft met de configuratie ga dan naar {integration_docs_url}", - "title": "Vallox" + "host": "Host" + } } } } diff --git a/homeassistant/components/vallox/translations/no.json b/homeassistant/components/vallox/translations/no.json index df8fd8e4535..4cc8f8321cb 100644 --- a/homeassistant/components/vallox/translations/no.json +++ b/homeassistant/components/vallox/translations/no.json @@ -14,11 +14,8 @@ "step": { "user": { "data": { - "host": "Vert", - "name": "Navn" - }, - "description": "Sett opp Vallox-integrasjonen. Hvis du har problemer med konfigurasjonen, g\u00e5 til {integration_docs_url} .", - "title": "Vallox" + "host": "Vert" + } } } } diff --git a/homeassistant/components/vallox/translations/pl.json b/homeassistant/components/vallox/translations/pl.json index f96481eba82..b608d4f8e0b 100644 --- a/homeassistant/components/vallox/translations/pl.json +++ b/homeassistant/components/vallox/translations/pl.json @@ -14,11 +14,8 @@ "step": { "user": { "data": { - "host": "Nazwa hosta lub adres IP", - "name": "Nazwa" - }, - "description": "Konfiguracja integracji Vallox. Je\u015bli masz problemy z konfiguracj\u0105 przejd\u017a do {integration_docs_url} .", - "title": "Vallox" + "host": "Nazwa hosta lub adres IP" + } } } } diff --git a/homeassistant/components/vallox/translations/pt-BR.json b/homeassistant/components/vallox/translations/pt-BR.json index 729e5257db8..cccafd932c8 100644 --- a/homeassistant/components/vallox/translations/pt-BR.json +++ b/homeassistant/components/vallox/translations/pt-BR.json @@ -14,11 +14,8 @@ "step": { "user": { "data": { - "host": "Nome do host", - "name": "Nome" - }, - "description": "Configure a integra\u00e7\u00e3o Vallox. Se voc\u00ea tiver problemas com a configura\u00e7\u00e3o, v\u00e1 para {integration_docs_url} .", - "title": "Vallox" + "host": "Nome do host" + } } } } diff --git a/homeassistant/components/vallox/translations/ru.json b/homeassistant/components/vallox/translations/ru.json index 70e9551ae11..a1165b287d1 100644 --- a/homeassistant/components/vallox/translations/ru.json +++ b/homeassistant/components/vallox/translations/ru.json @@ -14,11 +14,8 @@ "step": { "user": { "data": { - "host": "\u0425\u043e\u0441\u0442", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" - }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Vallox. \u0415\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0432\u043e\u0437\u043d\u0438\u043a\u043b\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439, \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: {integration_docs_url}.", - "title": "Vallox" + "host": "\u0425\u043e\u0441\u0442" + } } } } diff --git a/homeassistant/components/vallox/translations/tr.json b/homeassistant/components/vallox/translations/tr.json index 1958a0535b6..8d5780d2019 100644 --- a/homeassistant/components/vallox/translations/tr.json +++ b/homeassistant/components/vallox/translations/tr.json @@ -14,11 +14,8 @@ "step": { "user": { "data": { - "host": "Sunucu", - "name": "Ad" - }, - "description": "Vallox entegrasyonunu ayarlay\u0131n. Yap\u0131land\u0131rmayla ilgili sorunlar\u0131n\u0131z varsa {integration_docs_url} adresine gidin.", - "title": "Vallox" + "host": "Sunucu" + } } } } diff --git a/homeassistant/components/vallox/translations/zh-Hant.json b/homeassistant/components/vallox/translations/zh-Hant.json index c9f1d13fa08..d9e2150dce9 100644 --- a/homeassistant/components/vallox/translations/zh-Hant.json +++ b/homeassistant/components/vallox/translations/zh-Hant.json @@ -14,11 +14,8 @@ "step": { "user": { "data": { - "host": "\u4e3b\u6a5f\u7aef", - "name": "\u540d\u7a31" - }, - "description": "\u8a2d\u5b9a Vallox \u6574\u5408\u3002\u5047\u5982\u9700\u8981\u5354\u52a9\uff0c\u8acb\u53c3\u8003\uff1a{integration_docs_url}\u3002", - "title": "Vallox" + "host": "\u4e3b\u6a5f\u7aef" + } } } } diff --git a/homeassistant/components/venstar/translations/pl.json b/homeassistant/components/venstar/translations/pl.json index ad0e76435dc..fdaf30ead9f 100644 --- a/homeassistant/components/venstar/translations/pl.json +++ b/homeassistant/components/venstar/translations/pl.json @@ -16,7 +16,7 @@ "ssl": "Certyfikat SSL", "username": "Nazwa u\u017cytkownika" }, - "title": "Po\u0142\u0105cz z termostatem Venstar" + "title": "Po\u0142\u0105czenie z termostatem Venstar" } } } diff --git a/homeassistant/components/vera/translations/ca.json b/homeassistant/components/vera/translations/ca.json index 13a889cb7db..6aa7cd8292d 100644 --- a/homeassistant/components/vera/translations/ca.json +++ b/homeassistant/components/vera/translations/ca.json @@ -10,8 +10,9 @@ "lights": "Identificadors de dispositiu dels commutadors Vera a tractar com a llums a Home Assistant.", "vera_controller_url": "URL del controlador" }, - "description": "Proporciona un URL pel controlador Vera. Hauria de ser similar al seg\u00fcent: http://192.168.1.161:3480.", - "title": "Configuraci\u00f3 del controlador Vera" + "data_description": { + "vera_controller_url": "Hauria de tenir aquest aspecte: http://192.168.1.161:3480" + } } } }, diff --git a/homeassistant/components/vera/translations/cs.json b/homeassistant/components/vera/translations/cs.json index df93575a945..33b7cbf6625 100644 --- a/homeassistant/components/vera/translations/cs.json +++ b/homeassistant/components/vera/translations/cs.json @@ -9,9 +9,7 @@ "exclude": "ID za\u0159\u00edzen\u00ed Vera, kter\u00e1 chcete vylou\u010dit z Home Assistant.", "lights": "ID za\u0159\u00edzen\u00ed Vera, se kter\u00fdmi m\u00e1 Home Assistant zach\u00e1zet jako se sv\u011btly.", "vera_controller_url": "URL adresa ovlada\u010de" - }, - "description": "N\u00ed\u017ee zadejte adresu URL odvlada\u010de Vera. M\u011blo by to vypadat takto: http://192.168.1.161:3480.", - "title": "Nastaven\u00ed ovlada\u010de Vera" + } } } }, diff --git a/homeassistant/components/vera/translations/de.json b/homeassistant/components/vera/translations/de.json index 999dfc8213f..f13e347ffd9 100644 --- a/homeassistant/components/vera/translations/de.json +++ b/homeassistant/components/vera/translations/de.json @@ -10,8 +10,9 @@ "lights": "Vera Switch-Ger\u00e4te-IDs, die im Home Assistant als Lichter behandelt werden sollen.", "vera_controller_url": "Controller-URL" }, - "description": "Stelle unten eine Vera-Controller-URL zur Verf\u00fcgung. Sie sollte wie folgt aussehen: http://192.168.1.161:3480.", - "title": "Richte den Vera-Controller ein" + "data_description": { + "vera_controller_url": "Sie sollte wie folgt aussehen: http://192.168.1.161:3480" + } } } }, diff --git a/homeassistant/components/vera/translations/el.json b/homeassistant/components/vera/translations/el.json index 1039fb01ccd..bb667455132 100644 --- a/homeassistant/components/vera/translations/el.json +++ b/homeassistant/components/vera/translations/el.json @@ -10,8 +10,9 @@ "lights": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03ac \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03bc\u03b5\u03c4\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 Vera \u03b3\u03b9\u03b1 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c9\u03c2 \u03c6\u03ce\u03c4\u03b1 \u03c3\u03c4\u03bf Home Assistant.", "vera_controller_url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b5\u03bb\u03b5\u03b3\u03ba\u03c4\u03ae" }, - "description": "\u0394\u03ce\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c4\u03bf\u03c5 \u03b5\u03bb\u03b5\u03b3\u03ba\u03c4\u03ae Vera \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9. \u0398\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03bc\u03bf\u03b9\u03ac\u03b6\u03b5\u03b9 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc: http://192.168.1.161:3480.", - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b5\u03bb\u03b5\u03b3\u03ba\u03c4\u03ae Vera" + "data_description": { + "vera_controller_url": "\u0398\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03bc\u03bf\u03b9\u03ac\u03b6\u03b5\u03b9 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc: http://192.168.1.161:3480" + } } } }, diff --git a/homeassistant/components/vera/translations/en.json b/homeassistant/components/vera/translations/en.json index 94f490d71ee..53c60e39c01 100644 --- a/homeassistant/components/vera/translations/en.json +++ b/homeassistant/components/vera/translations/en.json @@ -10,8 +10,9 @@ "lights": "Vera switch device ids to treat as lights in Home Assistant.", "vera_controller_url": "Controller URL" }, - "description": "Provide a Vera controller URL below. It should look like this: http://192.168.1.161:3480.", - "title": "Setup Vera controller" + "data_description": { + "vera_controller_url": "It should look like this: http://192.168.1.161:3480" + } } } }, diff --git a/homeassistant/components/vera/translations/es-419.json b/homeassistant/components/vera/translations/es-419.json index 834c74b7ed3..40b78726144 100644 --- a/homeassistant/components/vera/translations/es-419.json +++ b/homeassistant/components/vera/translations/es-419.json @@ -9,9 +9,7 @@ "exclude": "Identificadores de dispositivo de Vera para excluir de Home Assistant.", "lights": "Vera cambia los identificadores del dispositivo para tratarlos como luces en Home Assistant.", "vera_controller_url": "URL del controlador" - }, - "description": "Proporcione una URL del controlador Vera a continuaci\u00f3n. Deber\u00eda verse as\u00ed: http://192.168.1.161:3480.", - "title": "Configurar el controlador Vera" + } } } }, diff --git a/homeassistant/components/vera/translations/es.json b/homeassistant/components/vera/translations/es.json index d34e106e9eb..0cccacaa88f 100644 --- a/homeassistant/components/vera/translations/es.json +++ b/homeassistant/components/vera/translations/es.json @@ -9,9 +9,7 @@ "exclude": "Identificadores de dispositivos Vera a excluir de Home Assistant", "lights": "Identificadores de interruptores Vera que deben ser tratados como luces en Home Assistant", "vera_controller_url": "URL del controlador" - }, - "description": "Introduce una URL para el controlador Vera a continuaci\u00f3n. Ser\u00eda algo como: http://192.168.1.161:3480.", - "title": "Configurar el controlador Vera" + } } } }, diff --git a/homeassistant/components/vera/translations/et.json b/homeassistant/components/vera/translations/et.json index 4afb098caa6..36d2cae71bf 100644 --- a/homeassistant/components/vera/translations/et.json +++ b/homeassistant/components/vera/translations/et.json @@ -10,8 +10,9 @@ "lights": "Vera l\u00fclitite ID'd mida neid k\u00e4sitleda Home Assistantis tuledena.", "vera_controller_url": "Kontrolleri URL" }, - "description": "Esita allpool Vera kontrolleri URL. See peaks v\u00e4lja n\u00e4gema selline: http://192.168.1.161:3480.", - "title": "Seadista Vera kontroller" + "data_description": { + "vera_controller_url": "See peaks v\u00e4lja n\u00e4gema selline: http://192.168.1.161:3480" + } } } }, diff --git a/homeassistant/components/vera/translations/fr.json b/homeassistant/components/vera/translations/fr.json index 133319c552d..f57145a9a24 100644 --- a/homeassistant/components/vera/translations/fr.json +++ b/homeassistant/components/vera/translations/fr.json @@ -10,8 +10,9 @@ "lights": "Identifiants des interrupteurs vera \u00e0 traiter comme des lumi\u00e8res dans Home Assistant", "vera_controller_url": "URL du contr\u00f4leur" }, - "description": "Fournissez une URL de contr\u00f4leur Vera ci-dessous. Cela devrait ressembler \u00e0 ceci : http://192.168.1.161:3480.", - "title": "Configurer le contr\u00f4leur Vera" + "data_description": { + "vera_controller_url": "Cela devrait ressembler \u00e0 ceci\u00a0: http://192.168.1.161:3480" + } } } }, diff --git a/homeassistant/components/vera/translations/hu.json b/homeassistant/components/vera/translations/hu.json index d1d4910c97a..30cf7e5e7d1 100644 --- a/homeassistant/components/vera/translations/hu.json +++ b/homeassistant/components/vera/translations/hu.json @@ -10,8 +10,9 @@ "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. Hasonl\u00f3k\u00e9ppen kell kin\u00e9znie: http://192.168.1.161:3480.", - "title": "Vera vez\u00e9rl\u0151 be\u00e1ll\u00edt\u00e1sa" + "data_description": { + "vera_controller_url": "\u00cdgy kell kin\u00e9znie: http://192.168.1.161:3480" + } } } }, diff --git a/homeassistant/components/vera/translations/id.json b/homeassistant/components/vera/translations/id.json index 435fc722dba..99422f10be6 100644 --- a/homeassistant/components/vera/translations/id.json +++ b/homeassistant/components/vera/translations/id.json @@ -10,8 +10,9 @@ "lights": "ID perangkat sakelar Vera yang diperlakukan sebagai lampu di Home Assistant", "vera_controller_url": "URL Pengontrol" }, - "description": "Tentukan URL pengontrol Vera di bawah. Ini akan terlihat seperti ini: http://192.168.1.161:3480.", - "title": "Siapkan pengontrol Vera" + "data_description": { + "vera_controller_url": "Seharusnya terlihat seperti ini: http://192.168.1.161:3480" + } } } }, diff --git a/homeassistant/components/vera/translations/it.json b/homeassistant/components/vera/translations/it.json index a3832914ac6..292e4048f0a 100644 --- a/homeassistant/components/vera/translations/it.json +++ b/homeassistant/components/vera/translations/it.json @@ -10,8 +10,9 @@ "lights": "Gli ID dei dispositivi switch Vera da trattare come luci in Home Assistant.", "vera_controller_url": "URL del controller" }, - "description": "Fornisci un URL controller Vera di seguito. Dovrebbe assomigliare a questo: http://192.168.1.161:3480.", - "title": "Configurazione controller Vera" + "data_description": { + "vera_controller_url": "Dovrebbe apparire cos\u00ec: http://192.168.1.161:3480" + } } } }, diff --git a/homeassistant/components/vera/translations/ja.json b/homeassistant/components/vera/translations/ja.json index 2f800ee7b83..83ca62f4abf 100644 --- a/homeassistant/components/vera/translations/ja.json +++ b/homeassistant/components/vera/translations/ja.json @@ -10,8 +10,9 @@ "lights": "Vera\u306f\u3001Home Assistant\u3067\u30e9\u30a4\u30c8\u3068\u3057\u3066\u6271\u3046\u30c7\u30d0\u30a4\u30b9ID\u3092\u5207\u308a\u66ff\u3048\u307e\u3059\u3002", "vera_controller_url": "\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306eURL" }, - "description": "Vera\u306e\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u306eURL\u3092\u4ee5\u4e0b\u306b\u793a\u3057\u307e\u3059: http://192.168.1.161:3480 \u306e\u3088\u3046\u306b\u306a\u3063\u3066\u3044\u308b\u306f\u305a\u3067\u3059\u3002", - "title": "Vera controller\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "data_description": { + "vera_controller_url": "\u6b21\u306e\u3088\u3046\u306b\u306a\u308a\u307e\u3059: http://192.168.1.161:3480" + } } } }, diff --git a/homeassistant/components/vera/translations/ko.json b/homeassistant/components/vera/translations/ko.json index 0990435cb4a..430af1be385 100644 --- a/homeassistant/components/vera/translations/ko.json +++ b/homeassistant/components/vera/translations/ko.json @@ -9,9 +9,7 @@ "exclude": "Home Assistant\uc5d0\uc11c \uc81c\uc678\ud560 Vera \uae30\uae30 ID.", "lights": "Vera \uc2a4\uc704\uce58 \uae30\uae30 ID \ub294 Home Assistant\uc5d0\uc11c \uc870\uba85\uc73c\ub85c \ucde8\uae09\ub429\ub2c8\ub2e4.", "vera_controller_url": "\ucee8\ud2b8\ub864\ub7ec URL" - }, - "description": "\uc544\ub798\uc5d0 Vera \ucee8\ud2b8\ub864\ub7ec URL \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. http://192.168.1.161:3480 \uacfc \uac19\uc740 \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4.", - "title": "Vera \ucee8\ud2b8\ub864\ub7ec \uc124\uc815\ud558\uae30" + } } } }, diff --git a/homeassistant/components/vera/translations/lb.json b/homeassistant/components/vera/translations/lb.json index 0f3c8acc028..307218b2b23 100644 --- a/homeassistant/components/vera/translations/lb.json +++ b/homeassistant/components/vera/translations/lb.json @@ -9,9 +9,7 @@ "exclude": "IDs vu Vera Apparater d\u00e9i vun Home Assistant ausgeschloss solle ginn.", "lights": "IDs vun Apparater vu Vera Schalter d\u00e9i als Luuchten am Home Assistant trait\u00e9iert ginn.", "vera_controller_url": "Kontroller URL" - }, - "description": "Vera Kontroller URL uginn: D\u00e9i sollt sou ausgesinn:\nhttp://192.168.1.161:3480.", - "title": "Vera Kontroller ariichten" + } } } }, diff --git a/homeassistant/components/vera/translations/nl.json b/homeassistant/components/vera/translations/nl.json index b4aa4a1d7a3..bc81fa3563e 100644 --- a/homeassistant/components/vera/translations/nl.json +++ b/homeassistant/components/vera/translations/nl.json @@ -10,8 +10,9 @@ "lights": "Vera-schakelapparaat id's behandelen als lichten in Home Assistant.", "vera_controller_url": "Controller-URL" }, - "description": "Geef hieronder een URL voor de Vera-controller op. Het zou er zo uit moeten zien: http://192.168.1.161:3480.", - "title": "Stel Vera controller in" + "data_description": { + "vera_controller_url": "Het zou er zo uit moeten zien: http://192.168.1.161:3480" + } } } }, diff --git a/homeassistant/components/vera/translations/no.json b/homeassistant/components/vera/translations/no.json index f1454be6799..807b44dc84e 100644 --- a/homeassistant/components/vera/translations/no.json +++ b/homeassistant/components/vera/translations/no.json @@ -10,8 +10,9 @@ "lights": "Vera bytter enhets ID-er for \u00e5 behandle som lys i Home Assistant", "vera_controller_url": "URL-adresse for kontroller" }, - "description": "Gi en Vera-kontroller-URL nedenfor. Det skal se slik ut: http://192.168.1.161:3480.", - "title": "Oppsett Vera-kontroller" + "data_description": { + "vera_controller_url": "Det skal se slik ut: http://192.168.1.161:3480" + } } } }, diff --git a/homeassistant/components/vera/translations/pl.json b/homeassistant/components/vera/translations/pl.json index 5b7b6886085..5be37d2a571 100644 --- a/homeassistant/components/vera/translations/pl.json +++ b/homeassistant/components/vera/translations/pl.json @@ -10,8 +10,9 @@ "lights": "Identyfikatory prze\u0142\u0105cznik\u00f3w Vera, kt\u00f3re maj\u0105 by\u0107 traktowane jako \u015bwiat\u0142a w Home Assistant.", "vera_controller_url": "Adres URL kontrolera" }, - "description": "Podaj adres URL kontrolera Vera. Powinien on wygl\u0105da\u0107 tak: http://192.168.1.161:3480.", - "title": "Konfiguracja kontrolera Vera" + "data_description": { + "vera_controller_url": "Powinno to wygl\u0105da\u0107 tak: http://192.168.1.161:3480" + } } } }, diff --git a/homeassistant/components/vera/translations/pt-BR.json b/homeassistant/components/vera/translations/pt-BR.json index 361b0487617..b3c39d4cbd7 100644 --- a/homeassistant/components/vera/translations/pt-BR.json +++ b/homeassistant/components/vera/translations/pt-BR.json @@ -10,8 +10,9 @@ "lights": "Vera - alternar os IDs do dispositivo para tratar como luzes no Home Assistant.", "vera_controller_url": "URL do controlador" }, - "description": "Forne\u00e7a um URL do controlador Vera abaixo. Deve ficar assim: http://192.168.1.161:3480.", - "title": "Configurar controlador Vera" + "data_description": { + "vera_controller_url": "Deve ficar assim: http://192.168.1.161:3480" + } } } }, diff --git a/homeassistant/components/vera/translations/ru.json b/homeassistant/components/vera/translations/ru.json index 857ffaa5bf5..43dc0cfd168 100644 --- a/homeassistant/components/vera/translations/ru.json +++ b/homeassistant/components/vera/translations/ru.json @@ -10,8 +10,9 @@ "lights": "ID \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u0435\u0439 Vera, \u0434\u043b\u044f \u0438\u043c\u043f\u043e\u0440\u0442\u0430 \u0432 \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u0435", "vera_controller_url": "URL-\u0430\u0434\u0440\u0435\u0441 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430" }, - "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 URL-\u0430\u0434\u0440\u0435\u0441 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 'http://192.168.1.161:3480').", - "title": "Vera" + "data_description": { + "vera_controller_url": "\u0414\u043e\u043b\u0436\u043d\u043e \u0432\u044b\u0433\u043b\u044f\u0434\u0435\u0442\u044c \u0442\u0430\u043a: http://192.168.1.161:3480" + } } } }, diff --git a/homeassistant/components/vera/translations/sl.json b/homeassistant/components/vera/translations/sl.json index 355c6353388..01b2f755ac5 100644 --- a/homeassistant/components/vera/translations/sl.json +++ b/homeassistant/components/vera/translations/sl.json @@ -9,9 +9,7 @@ "exclude": "ID-ji naprav Vera, ki jih \u017eelite izklju\u010diti iz programa Home Assistant.", "lights": "ID-ji stikal Vera, ki naj jih Home Assistant tretira kot lu\u010di.", "vera_controller_url": "URL krmilnika" - }, - "description": "Spodaj navedite URL krmilnika Vera. Izgledati bi moral takole: http://192.168.1.161:3480.", - "title": "Nastavite krmilnik Vera" + } } } }, diff --git a/homeassistant/components/vera/translations/tr.json b/homeassistant/components/vera/translations/tr.json index fa66c1c9806..465aed0cb90 100644 --- a/homeassistant/components/vera/translations/tr.json +++ b/homeassistant/components/vera/translations/tr.json @@ -10,8 +10,9 @@ "lights": "Vera, Home Assistant'ta \u0131\u015f\u0131k gibi davranmak i\u00e7in cihaz kimliklerini de\u011fi\u015ftirir.", "vera_controller_url": "Denetleyici URL'si" }, - "description": "A\u015fa\u011f\u0131da bir Vera denetleyici URL'si sa\u011flay\u0131n. \u015eu \u015fekilde g\u00f6r\u00fcnmelidir: http://192.168.1.161:3480.", - "title": "Vera denetleyicisini kurun" + "data_description": { + "vera_controller_url": "\u015eu \u015fekilde g\u00f6r\u00fcnmelidir: http://192.168.1.161:3480" + } } } }, diff --git a/homeassistant/components/vera/translations/uk.json b/homeassistant/components/vera/translations/uk.json index 8c591a1cc10..5042d04c26a 100644 --- a/homeassistant/components/vera/translations/uk.json +++ b/homeassistant/components/vera/translations/uk.json @@ -9,9 +9,7 @@ "exclude": "ID \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 Vera, \u0434\u043b\u044f \u0432\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0437 Home Assistant", "lights": "ID \u0432\u0438\u043c\u0438\u043a\u0430\u0447\u0456\u0432 Vera, \u0434\u043b\u044f \u0456\u043c\u043f\u043e\u0440\u0442\u0443 \u0432 \u043e\u0441\u0432\u0456\u0442\u043b\u0435\u043d\u043d\u044f", "vera_controller_url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430" - }, - "description": "\u0412\u043a\u0430\u0436\u0456\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441\u0443 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 'http://192.168.1.161:3480').", - "title": "Vera" + } } } }, diff --git a/homeassistant/components/vera/translations/zh-Hant.json b/homeassistant/components/vera/translations/zh-Hant.json index b8d7031ee12..802ce7c97f1 100644 --- a/homeassistant/components/vera/translations/zh-Hant.json +++ b/homeassistant/components/vera/translations/zh-Hant.json @@ -10,8 +10,9 @@ "lights": "\u65bc Home Assistant \u4e2d\u8996\u70ba\u71c8\u5149\u7684 Vera \u958b\u95dc\u88dd\u7f6e ID\u3002", "vera_controller_url": "\u63a7\u5236\u5668 URL" }, - "description": "\u65bc\u4e0b\u65b9\u63d0\u4f9b Vera \u63a7\u5236\u5668 URL\u3002\u683c\u5f0f\u61c9\u8a72\u70ba\uff1ahttp://192.168.1.161:3480\u3002", - "title": "\u8a2d\u5b9a Vera \u63a7\u5236\u5668" + "data_description": { + "vera_controller_url": "\u770b\u8d77\u4f86\u61c9\u8a72\u985e\u4f3c\uff1ahttp://192.168.1.161:3480" + } } } }, diff --git a/homeassistant/components/verisure/translations/hu.json b/homeassistant/components/verisure/translations/hu.json index 91c0292c3b8..324033b666a 100644 --- a/homeassistant/components/verisure/translations/hu.json +++ b/homeassistant/components/verisure/translations/hu.json @@ -17,14 +17,14 @@ }, "reauth_confirm": { "data": { - "description": "Hiteles\u00edts \u00fajra a Verisure My Pages fi\u00f3koddal.", + "description": "Hiteles\u00edtsen \u00fajra a Verisure My Pages fi\u00f3kj\u00e1val.", "email": "E-mail", "password": "Jelsz\u00f3" } }, "user": { "data": { - "description": "Jelentkezz be a Verisure My Pages fi\u00f3koddal.", + "description": "Jelentkezzen be a Verisure My Pages fi\u00f3kj\u00e1val.", "email": "E-mail", "password": "Jelsz\u00f3" } diff --git a/homeassistant/components/version/translations/he.json b/homeassistant/components/version/translations/he.json index cdb921611c4..c5508040834 100644 --- a/homeassistant/components/version/translations/he.json +++ b/homeassistant/components/version/translations/he.json @@ -2,6 +2,17 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "step": { + "user": { + "title": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05e1\u05d5\u05d2 \u05d4\u05ea\u05e7\u05e0\u05d4" + }, + "version_source": { + "data": { + "beta": "\u05dc\u05db\u05dc\u05d5\u05dc \u05d2\u05e8\u05e1\u05d0\u05d5\u05ea \u05d1\u05d8\u05d0" + }, + "title": "\u05d4\u05d2\u05d3\u05e8\u05d4" + } } } } \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/fr.json b/homeassistant/components/vicare/translations/fr.json index 260dbb25006..b3dce5ec826 100644 --- a/homeassistant/components/vicare/translations/fr.json +++ b/homeassistant/components/vicare/translations/fr.json @@ -18,7 +18,7 @@ "scan_interval": "Intervalle de balayage (secondes)", "username": "Courriel" }, - "description": "Configurer l'int\u00e9gration ViCare. Pour g\u00e9n\u00e9rer une cl\u00e9 API se rendre sur https://developer.viessmann.com", + "description": "Configurer l'int\u00e9gration ViCare. Pour g\u00e9n\u00e9rer une cl\u00e9 d'API, rendez-vous sur https://developer.viessmann.com", "title": "{name}" } } diff --git a/homeassistant/components/vicare/translations/hu.json b/homeassistant/components/vicare/translations/hu.json index 4dca080307d..0e69a395a90 100644 --- a/homeassistant/components/vicare/translations/hu.json +++ b/homeassistant/components/vicare/translations/hu.json @@ -13,7 +13,7 @@ "data": { "client_id": "API kulcs", "heating_type": "F\u0171t\u00e9s t\u00edpusa", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3", "scan_interval": "Beolvas\u00e1si id\u0151k\u00f6z (m\u00e1sodperc)", "username": "E-mail" diff --git a/homeassistant/components/vizio/translations/hu.json b/homeassistant/components/vizio/translations/hu.json index bb619e359c0..908dfad8f76 100644 --- a/homeassistant/components/vizio/translations/hu.json +++ b/homeassistant/components/vizio/translations/hu.json @@ -31,7 +31,7 @@ "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", "device_class": "Eszk\u00f6zt\u00edpus", "host": "C\u00edm", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" }, "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.", "title": "VIZIO SmartCast Eszk\u00f6z" diff --git a/homeassistant/components/vlc_telnet/translations/hu.json b/homeassistant/components/vlc_telnet/translations/hu.json index 0a76fc089ed..bac42b4d4c3 100644 --- a/homeassistant/components/vlc_telnet/translations/hu.json +++ b/homeassistant/components/vlc_telnet/translations/hu.json @@ -26,7 +26,7 @@ "user": { "data": { "host": "C\u00edm", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3", "port": "Port" } diff --git a/homeassistant/components/vulcan/translations/bg.json b/homeassistant/components/vulcan/translations/bg.json new file mode 100644 index 00000000000..27893871c14 --- /dev/null +++ b/homeassistant/components/vulcan/translations/bg.json @@ -0,0 +1,30 @@ +{ + "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 \u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435\u0442\u043e - \u043c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0432\u0430\u0448\u0430\u0442\u0430 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0432\u0440\u044a\u0437\u043a\u0430" + }, + "step": { + "select_saved_credentials": { + "data": { + "credentials": "\u0412\u0445\u043e\u0434" + } + } + } + }, + "options": { + "error": { + "error": "\u0412\u044a\u0437\u043d\u0438\u043a\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "init": { + "data": { + "message_notify": "\u041f\u043e\u043a\u0430\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u0437\u0432\u0435\u0441\u0442\u0438\u044f \u043f\u0440\u0438 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u043d\u043e\u0432\u043e \u0441\u044a\u043e\u0431\u0449\u0435\u043d\u0438\u0435", + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043d\u0430 \u043e\u0431\u043d\u043e\u0432\u044f\u0432\u0430\u043d\u0435 (\u0432 \u043c\u0438\u043d\u0443\u0442\u0438)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/ca.json b/homeassistant/components/vulcan/translations/ca.json new file mode 100644 index 00000000000..505bcb1d326 --- /dev/null +++ b/homeassistant/components/vulcan/translations/ca.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "all_student_already_configured": "Ja s'han afegit tots els alumnes.", + "already_configured": "Ja s'ha afegit aquest alumne.", + "reauth_successful": "Re-autenticaci\u00f3 exitosa" + }, + "error": { + "cannot_connect": "Error de connexi\u00f3, comprova la teva connexi\u00f3 a Internet", + "expired_credentials": "Credencials caducades, crea'n de noves a la p\u00e0gina de registre de l'aplicaci\u00f3 m\u00f2bil de Vulcan", + "expired_token": "Token caducat, genera un nou token", + "invalid_pin": "PIN inv\u00e0lid", + "invalid_symbol": "S\u00edmbol inv\u00e0lid", + "invalid_token": "Token inv\u00e0lid", + "unknown": "S'ha produ\u00eft un error desconegut" + }, + "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "Utilitza les credencials desades" + }, + "description": "Afegeix un altre alumne." + }, + "auth": { + "data": { + "pin": "PIN", + "region": "S\u00edmbol", + "token": "Token" + }, + "description": "Inicia sessi\u00f3 al teu compte de Vulcan mitjan\u00e7ant la p\u00e0gina de registre de l'aplicaci\u00f3 m\u00f2bil." + }, + "reauth": { + "data": { + "pin": "PIN", + "region": "S\u00edmbol", + "token": "Token" + }, + "description": "Inicia sessi\u00f3 al teu compte de Vulcan mitjan\u00e7ant la p\u00e0gina de registre de l'aplicaci\u00f3 m\u00f2bil." + }, + "select_saved_credentials": { + "data": { + "credentials": "Inici de sessi\u00f3" + }, + "description": "Selecciona credencials desades." + }, + "select_student": { + "data": { + "student_name": "Selecciona l'alumne" + }, + "description": "Selecciona l'alumne, pots afegir m\u00e9s alumnes tornant a afegir la integraci\u00f3 de nou." + } + } + }, + "options": { + "error": { + "error": "S'ha produ\u00eft un error" + }, + "step": { + "init": { + "data": { + "attendance_notify": "Mostra les notificacions sobre les darreres assist\u00e8ncies", + "grade_notify": "Mostra notificacions de les \u00faltimes qualificacions", + "message_notify": "Mostra notificacions quan es rebi un missatge nou", + "scan_interval": "Interval d'actualitzaci\u00f3 (minuts)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/de.json b/homeassistant/components/vulcan/translations/de.json new file mode 100644 index 00000000000..23d5032f5fa --- /dev/null +++ b/homeassistant/components/vulcan/translations/de.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "all_student_already_configured": "Alle Sch\u00fcler wurden bereits hinzugef\u00fcgt.", + "already_configured": "Dieser Sch\u00fcler wurde bereits hinzugef\u00fcgt.", + "reauth_successful": "Reauth erfolgreich" + }, + "error": { + "cannot_connect": "Verbindungsfehler - Bitte \u00fcberpr\u00fcfe deine Internetverbindung", + "expired_credentials": "Abgelaufene Anmeldedaten - bitte erstelle neue auf der Registrierungsseite der Vulcan Mobile App", + "expired_token": "Abgelaufener Token - bitte generiere einen neuen Token", + "invalid_pin": "Ung\u00fcltige PIN", + "invalid_symbol": "Ung\u00fcltiges Symbol", + "invalid_token": "Ung\u00fcltiges Token", + "unknown": "Unbekannter Fehler ist aufgetreten" + }, + "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "Gespeicherte Anmeldeinformationen verwenden" + }, + "description": "F\u00fcge einen weiteren Sch\u00fcler hinzu." + }, + "auth": { + "data": { + "pin": "PIN", + "region": "Symbol", + "token": "Token" + }, + "description": "Melde dich bei deinem Vulcan-Konto \u00fcber die Registrierungsseite der mobilen App an." + }, + "reauth": { + "data": { + "pin": "PIN", + "region": "Symbol", + "token": "Token" + }, + "description": "Melde dich bei deinem Vulcan-Konto \u00fcber die Registrierungsseite der mobilen App an." + }, + "select_saved_credentials": { + "data": { + "credentials": "Anmelden" + }, + "description": "W\u00e4hle gespeicherte Anmeldeinformationen aus." + }, + "select_student": { + "data": { + "student_name": "Sch\u00fcler ausw\u00e4hlen" + }, + "description": "W\u00e4hle Sch\u00fcler aus, Du kannst weitere Sch\u00fcler hinzuf\u00fcgen, indem du die Integration erneut hinzuf\u00fcgst." + } + } + }, + "options": { + "error": { + "error": "Ein Fehler ist aufgetreten" + }, + "step": { + "init": { + "data": { + "attendance_notify": "Benachrichtigungen \u00fcber die neuesten Anwesenheitseintr\u00e4ge anzeigen", + "grade_notify": "Benachrichtigungen \u00fcber die neuesten Noten anzeigen", + "message_notify": "Benachrichtigungen anzeigen, wenn eine neue Nachricht eingegangen ist", + "scan_interval": "Aktualisierungsintervall (in Minuten)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/el.json b/homeassistant/components/vulcan/translations/el.json new file mode 100644 index 00000000000..bb77c88f8e5 --- /dev/null +++ b/homeassistant/components/vulcan/translations/el.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "all_student_already_configured": "\u038c\u03bb\u03bf\u03b9 \u03bf\u03b9 \u03bc\u03b1\u03b8\u03b7\u03c4\u03ad\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03ae\u03b4\u03b7 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b5\u03b8\u03b5\u03af.", + "already_configured": "\u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03bc\u03b1\u03b8\u03b7\u03c4\u03ae\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b5\u03b8\u03b5\u03af.", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + }, + "error": { + "cannot_connect": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 - \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf \u0394\u03b9\u03b1\u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "expired_credentials": "\u039b\u03b7\u03b3\u03bc\u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 - \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03bd\u03ad\u03b1 \u03c3\u03c4\u03b7 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03c4\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03b9\u03bd\u03b7\u03c4\u03ac \u03c4\u03b7\u03c2 Vulcan", + "expired_token": "\u039b\u03b7\u03b3\u03bc\u03ad\u03bd\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc - \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03bd\u03ad\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc", + "invalid_pin": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf PIN", + "invalid_symbol": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03c3\u03cd\u03bc\u03b2\u03bf\u03bb\u03bf", + "invalid_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc", + "unknown": "\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ac\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b1\u03c0\u03bf\u03b8\u03b7\u03ba\u03b5\u03c5\u03bc\u03ad\u03bd\u03c9\u03bd \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03b7\u03c1\u03af\u03c9\u03bd" + }, + "description": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03ac\u03bb\u03bb\u03bf\u03c5 \u03bc\u03b1\u03b8\u03b7\u03c4\u03ae." + }, + "auth": { + "data": { + "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", + "region": "\u03a3\u03cd\u03bc\u03b2\u03bf\u03bb\u03bf", + "token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc" + }, + "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 Vulcan \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b7 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03b9\u03bd\u03b7\u03c4\u03ac." + }, + "reauth": { + "data": { + "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", + "region": "\u03a3\u03cd\u03bc\u03b2\u03bf\u03bb\u03bf", + "token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc" + }, + "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 Vulcan \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b7 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03b9\u03bd\u03b7\u03c4\u03ac." + }, + "select_saved_credentials": { + "data": { + "credentials": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b1\u03c0\u03bf\u03b8\u03b7\u03ba\u03b5\u03c5\u03bc\u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1." + }, + "select_student": { + "data": { + "student_name": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b1\u03b8\u03b7\u03c4\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b1\u03b8\u03b7\u03c4\u03ae, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03bf\u03c5\u03c2 \u03bc\u03b1\u03b8\u03b7\u03c4\u03ad\u03c2 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c4\u03bf\u03bd\u03c4\u03b1\u03c2 \u03be\u03b1\u03bd\u03ac \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7." + } + } + }, + "options": { + "error": { + "error": "\u03a0\u03c1\u03bf\u03ad\u03ba\u03c5\u03c8\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "init": { + "data": { + "attendance_notify": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c9\u03bd \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b9\u03bf \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03b5\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03ae\u03c3\u03b5\u03b9\u03c2 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ce\u03bd", + "grade_notify": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c9\u03bd \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03c0\u03b9\u03bf \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03bf\u03c5\u03c2 \u03b2\u03b1\u03b8\u03bc\u03bf\u03cd\u03c2", + "message_notify": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c9\u03bd \u03cc\u03c4\u03b1\u03bd \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03ad\u03bf \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1", + "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2 (\u03c3\u03b5 \u03bb\u03b5\u03c0\u03c4\u03ac)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/en.json b/homeassistant/components/vulcan/translations/en.json index abb3dce7c7f..48c054d2c15 100644 --- a/homeassistant/components/vulcan/translations/en.json +++ b/homeassistant/components/vulcan/translations/en.json @@ -1,69 +1,69 @@ { - "config": { - "abort": { - "already_configured": "That student has already been added.", - "all_student_already_configured": "All students have already been added.", - "reauth_successful": "Reauth successful" + "config": { + "abort": { + "all_student_already_configured": "All students have already been added.", + "already_configured": "That student has already been added.", + "reauth_successful": "Reauth successful" + }, + "error": { + "cannot_connect": "Connection error - please check your internet connection", + "expired_credentials": "Expired credentials - please create new on Vulcan mobile app registration page", + "expired_token": "Expired token - please generate a new token", + "invalid_pin": "Invalid pin", + "invalid_symbol": "Invalid symbol", + "invalid_token": "Invalid token", + "unknown": "Unknown error occurred" + }, + "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "Use saved credentials" + }, + "description": "Add another student." + }, + "auth": { + "data": { + "pin": "Pin", + "region": "Symbol", + "token": "Token" + }, + "description": "Login to your Vulcan Account using mobile app registration page." + }, + "reauth": { + "data": { + "pin": "Pin", + "region": "Symbol", + "token": "Token" + }, + "description": "Login to your Vulcan Account using mobile app registration page." + }, + "select_saved_credentials": { + "data": { + "credentials": "Login" + }, + "description": "Select saved credentials." + }, + "select_student": { + "data": { + "student_name": "Select student" + }, + "description": "Select student, you can add more students by adding integration again." + } + } }, - "error": { - "unknown": "Unknown error occurred", - "invalid_token": "Invalid token", - "expired_token": "Expired token - please generate a new token", - "invalid_pin": "Invalid pin", - "invalid_symbol": "Invalid symbol", - "expired_credentials": "Expired credentials - please create new on Vulcan mobile app registration page", - "cannot_connect": "Connection error - please check your internet connection" - }, - "step": { - "auth": { - "description": "Login to your Vulcan Account using mobile app registration page.", - "data": { - "token": "Token", - "region": "Symbol", - "pin": "Pin" + "options": { + "error": { + "error": "Error occurred" + }, + "step": { + "init": { + "data": { + "attendance_notify": "Show notifications about the latest attendance entries", + "grade_notify": "Show notifications about the latest grades", + "message_notify": "Show notifications when new message received", + "scan_interval": "Update interval (in minutes)" + } + } } - }, - "reauth": { - "description": "Login to your Vulcan Account using mobile app registration page.", - "data": { - "token": "Token", - "region": "Symbol", - "pin": "Pin" - } - }, - "select_student": { - "description": "Select student, you can add more students by adding integration again.", - "data": { - "student_name": "Select student" - } - }, - "select_saved_credentials": { - "description": "Select saved credentials.", - "data": { - "credentials": "Login" - } - }, - "add_next_config_entry": { - "description": "Add another student.", - "data": { - "use_saved_credentials": "Use saved credentials" - } - } } - }, - "options": { - "error": { - "error": "Error occurred" - }, - "step": { - "init": { - "data": { - "message_notify": "Show notifications when new message received", - "attendance_notify": "Show notifications about the latest attendance entries", - "grade_notify": "Show notifications about the latest grades", - "scan_interval": "Update interval (in minutes)" - } - } - } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/et.json b/homeassistant/components/vulcan/translations/et.json new file mode 100644 index 00000000000..ab3156cc676 --- /dev/null +++ b/homeassistant/components/vulcan/translations/et.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "all_student_already_configured": "K\u00f5ik \u00f5pilased on juba lisatud.", + "already_configured": "See \u00f5pilane on juba lisatud.", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendusviga - kontrolli oma interneti\u00fchendust", + "expired_credentials": "Aegunud mandaat \u2013 loo Vulcani mobiilirakenduse registreerimislehel uued", + "expired_token": "Token on aegunud - loo uus token.", + "invalid_pin": "Vigane PIN kood", + "invalid_symbol": "Vigane s\u00fcmbol", + "invalid_token": "Vigane token", + "unknown": "Ilmnes ootamatu t\u00f5rge" + }, + "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "Salvestatud identimisteabe kasutamine" + }, + "description": "Lisa veel \u00fcks \u00f5pilane." + }, + "auth": { + "data": { + "pin": "PIN kood", + "region": "S\u00fcmbol", + "token": "Token" + }, + "description": "Logi Vulcani kontole sisse mobiilirakenduse registreerimislehe kaudu." + }, + "reauth": { + "data": { + "pin": "PIN kood", + "region": "S\u00fcmbol", + "token": "Token" + }, + "description": "Logi Vulcani kontole sisse mobiilirakenduse registreerimislehe kaudu." + }, + "select_saved_credentials": { + "data": { + "credentials": "Sisselogimine" + }, + "description": "Vali salvestatud identimisteave." + }, + "select_student": { + "data": { + "student_name": "Vali \u00f5pilane" + }, + "description": "Vali \u00f5pilane, saad lisada rohkem \u00f5pilasi lisades sidumise veelkord." + } + } + }, + "options": { + "error": { + "error": "Ilmnes t\u00f5rge" + }, + "step": { + "init": { + "data": { + "attendance_notify": "Kuva m\u00e4rguanded viimaste osalemiskirjete kohta", + "grade_notify": "Kuva m\u00e4rguanded viimaste hinnete kohta", + "message_notify": "Kuva teated uue s\u00f5numi saabumisel", + "scan_interval": "V\u00e4rskendamise intervall (minutites)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/fr.json b/homeassistant/components/vulcan/translations/fr.json new file mode 100644 index 00000000000..812c069223e --- /dev/null +++ b/homeassistant/components/vulcan/translations/fr.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "all_student_already_configured": "Tous les \u00e9tudiants ont d\u00e9j\u00e0 \u00e9t\u00e9 ajout\u00e9s.", + "already_configured": "Cet \u00e9tudiant a d\u00e9j\u00e0 \u00e9t\u00e9 ajout\u00e9.", + "reauth_successful": "R\u00e9-authentification r\u00e9ussie" + }, + "error": { + "cannot_connect": "Erreur de connexion\u00a0; veuillez v\u00e9rifier votre connexion Internet", + "expired_credentials": "Identifiants expir\u00e9s\u00a0; veuillez en cr\u00e9er des nouveaux sur la page d'inscription de l'application mobile Vulcan", + "expired_token": "Jeton expir\u00e9\u00a0; veuillez g\u00e9n\u00e9rer un nouveau jeton", + "invalid_pin": "PIN non valide", + "invalid_symbol": "Symbole non valide", + "invalid_token": "Jeton non valide", + "unknown": "Une erreur inconnue est survenue" + }, + "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "Utiliser les informations d'identification enregistr\u00e9es" + }, + "description": "Ajoutez un autre \u00e9tudiant." + }, + "auth": { + "data": { + "pin": "PIN", + "region": "Symbole", + "token": "Jeton" + }, + "description": "Connectez-vous \u00e0 votre compte Vulcan en utilisant la page d'inscription de l'application mobile." + }, + "reauth": { + "data": { + "pin": "PIN", + "region": "Symbole", + "token": "Jeton" + }, + "description": "Connectez-vous \u00e0 votre compte Vulcan en utilisant la page d'inscription de l'application mobile." + }, + "select_saved_credentials": { + "data": { + "credentials": "Connexion" + }, + "description": "S\u00e9lectionnez les informations d'identification enregistr\u00e9es." + }, + "select_student": { + "data": { + "student_name": "S\u00e9lectionner un \u00e9tudiant" + }, + "description": "S\u00e9lectionnez l'\u00e9tudiant\u00a0; vous pouvez ajouter d'autres \u00e9tudiants en ajoutant \u00e0 nouveau l'int\u00e9gration." + } + } + }, + "options": { + "error": { + "error": "Une erreur est survenue" + }, + "step": { + "init": { + "data": { + "attendance_notify": "Afficher les notifications au sujet des derni\u00e8res entr\u00e9es de pr\u00e9sence", + "grade_notify": "Afficher les notifications au sujet des derni\u00e8res notes", + "message_notify": "Afficher les notifications lors de la r\u00e9ception d'un nouveau message", + "scan_interval": "Intervalle de mise \u00e0 jour (en minutes)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/hu.json b/homeassistant/components/vulcan/translations/hu.json new file mode 100644 index 00000000000..9e138254ebe --- /dev/null +++ b/homeassistant/components/vulcan/translations/hu.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "all_student_already_configured": "M\u00e1r minden tanul\u00f3 hozz\u00e1adva.", + "already_configured": "Ezt a tanul\u00f3t m\u00e1r hozz\u00e1adt\u00e1k.", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres" + }, + "error": { + "cannot_connect": "Csatlakoz\u00e1si hiba \u2013 ellen\u0151rizze az internetkapcsolatot", + "expired_credentials": "Lej\u00e1rt hiteles\u00edt\u0151 adatok - k\u00e9rj\u00fck, hozzon l\u00e9tre \u00fajakat a Vulcan mobilalkalmaz\u00e1s regisztr\u00e1ci\u00f3s oldal\u00e1n.", + "expired_token": "Lej\u00e1rt token \u2013 k\u00e9rj\u00fck, hozzon l\u00e9tre egy \u00fajat", + "invalid_pin": "\u00c9rv\u00e9nytelen PIN-k\u00f3d", + "invalid_symbol": "\u00c9rv\u00e9nytelen szimb\u00f3lum", + "invalid_token": "\u00c9rv\u00e9nytelen token", + "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt" + }, + "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "Mentett hiteles\u00edt\u0151 adatok haszn\u00e1lata" + }, + "description": "Adjon hozz\u00e1 egy m\u00e1sik tanul\u00f3t." + }, + "auth": { + "data": { + "pin": "PIN", + "region": "Szimb\u00f3lum", + "token": "Token" + }, + "description": "Jelentkezzen be Vulcan fi\u00f3kj\u00e1ba a mobilalkalmaz\u00e1s regisztr\u00e1ci\u00f3s oldal\u00e1n." + }, + "reauth": { + "data": { + "pin": "PIN", + "region": "Szimb\u00f3lum", + "token": "Token" + }, + "description": "Jelentkezzen be Vulcan fi\u00f3kj\u00e1ba a mobilalkalmaz\u00e1s regisztr\u00e1ci\u00f3s oldal\u00e1n." + }, + "select_saved_credentials": { + "data": { + "credentials": "Bejelentkez\u00e9s" + }, + "description": "V\u00e1lassza ki a mentett hiteles\u00edt\u0151 adatokat." + }, + "select_student": { + "data": { + "student_name": "Tanul\u00f3 kiv\u00e1laszt\u00e1sa" + }, + "description": "V\u00e1lassza ki a tanul\u00f3t, tov\u00e1bbi tanul\u00f3kat vehet fel az integr\u00e1ci\u00f3 \u00fajb\u00f3li hozz\u00e1ad\u00e1s\u00e1val." + } + } + }, + "options": { + "error": { + "error": "Hiba t\u00f6rt\u00e9nt" + }, + "step": { + "init": { + "data": { + "attendance_notify": "\u00c9rtes\u00edt\u00e9sek megjelen\u00edt\u00e9se a legut\u00f3bbi r\u00e9szv\u00e9teli bejegyz\u00e9sekr\u0151l", + "grade_notify": "\u00c9rtes\u00edt\u00e9sek megjelen\u00edt\u00e9se a leg\u00fajabb oszt\u00e1lyzatokr\u00f3l", + "message_notify": "\u00c9rtes\u00edt\u00e9sek megjelen\u00edt\u00e9se \u00faj \u00fczenet \u00e9rkez\u00e9sekor", + "scan_interval": "Friss\u00edt\u00e9si id\u0151k\u00f6z (percben)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/id.json b/homeassistant/components/vulcan/translations/id.json new file mode 100644 index 00000000000..a15ed1772d2 --- /dev/null +++ b/homeassistant/components/vulcan/translations/id.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "all_student_already_configured": "Semua siswa telah ditambahkan.", + "already_configured": "Siswa tersebut telah ditambahkan.", + "reauth_successful": "Otorisasi ulang berhasil" + }, + "error": { + "cannot_connect": "Kesalahan koneksi - periksa koneksi internet Anda", + "expired_credentials": "Kredensial kedaluwarsa - buat baru di halaman pendaftaran aplikasi seluler Vulcan", + "expired_token": "Token kedaluwarsa - buat token baru", + "invalid_pin": "PIN tidak valid", + "invalid_symbol": "Simbol tidak valid", + "invalid_token": "Token tidak valid", + "unknown": "Kesalahan tidak dikenal terjadi." + }, + "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "Menggunakan kredensial yang disimpan" + }, + "description": "Tambahkan siswa lain." + }, + "auth": { + "data": { + "pin": "PIN", + "region": "Simbol", + "token": "Token" + }, + "description": "Masuk ke Akun Vulcan Anda menggunakan halaman pendaftaran aplikasi seluler." + }, + "reauth": { + "data": { + "pin": "PIN", + "region": "Simbol", + "token": "Token" + }, + "description": "Masuk ke Akun Vulcan Anda menggunakan halaman pendaftaran aplikasi seluler." + }, + "select_saved_credentials": { + "data": { + "credentials": "Masuk" + }, + "description": "Pilih kredensial yang disimpan." + }, + "select_student": { + "data": { + "student_name": "Pilih siswa" + }, + "description": "Pilih siswa, Anda dapat menambahkan lebih banyak siswa dengan menambahkan integrasi lagi." + } + } + }, + "options": { + "error": { + "error": "Terjadi kesalahan" + }, + "step": { + "init": { + "data": { + "attendance_notify": "Tampilkan notifikasi tentang entri kehadiran terbaru", + "grade_notify": "Tampilkan notifikasi tentang nilai terbaru", + "message_notify": "Tampilkan saat pesan baru diterima", + "scan_interval": "Interval pembaruan (dalam menit)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/it.json b/homeassistant/components/vulcan/translations/it.json new file mode 100644 index 00000000000..72bb696a909 --- /dev/null +++ b/homeassistant/components/vulcan/translations/it.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "all_student_already_configured": "Tutti gli studenti sono gi\u00e0 stati aggiunti.", + "already_configured": "Quello studente \u00e8 gi\u00e0 stato aggiunto.", + "reauth_successful": "Nuova autenticazione avvenuta" + }, + "error": { + "cannot_connect": "Errore di connessione: controlla la tua connessione Internet", + "expired_credentials": "Credenziali scadute: creane una nuova nella pagina di registrazione dell'applicazione mobile Vulcan", + "expired_token": "Token scaduto: genera un nuovo token", + "invalid_pin": "Pin non valido", + "invalid_symbol": "Simbolo non valido", + "invalid_token": "Token non valido", + "unknown": "Si \u00e8 verificato un errore sconosciuto" + }, + "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "Usa le credenziali salvate" + }, + "description": "Aggiungi un altro studente." + }, + "auth": { + "data": { + "pin": "PIN", + "region": "Simbolo", + "token": "Token" + }, + "description": "Accedi al tuo account Vulcan utilizzando la pagina di registrazione dell'applicazione mobile." + }, + "reauth": { + "data": { + "pin": "PIN", + "region": "Simbolo", + "token": "Token" + }, + "description": "Accedi al tuo account Vulcan utilizzando la pagina di registrazione dell'applicazione mobile." + }, + "select_saved_credentials": { + "data": { + "credentials": "Accesso" + }, + "description": "Seleziona le credenziali salvate." + }, + "select_student": { + "data": { + "student_name": "Seleziona studente" + }, + "description": "Seleziona studente, puoi aggiungere pi\u00f9 studenti aggiungendo nuovamente l'integrazione." + } + } + }, + "options": { + "error": { + "error": "Si \u00e8 verificato un errore" + }, + "step": { + "init": { + "data": { + "attendance_notify": "Mostra le notifiche sulle ultime voci di partecipazione", + "grade_notify": "Mostra le notifiche sugli ultimi voti", + "message_notify": "Mostra le notifiche quando viene ricevuto un nuovo messaggio", + "scan_interval": "Intervallo di aggiornamento (in minuti)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/ja.json b/homeassistant/components/vulcan/translations/ja.json new file mode 100644 index 00000000000..98363f12f13 --- /dev/null +++ b/homeassistant/components/vulcan/translations/ja.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "all_student_already_configured": "\u3059\u3079\u3066\u306e\u751f\u5f92\u306f\u3059\u3067\u306b\u8ffd\u52a0\u3055\u308c\u3066\u3044\u307e\u3059\u3002", + "already_configured": "\u305d\u306e\u5b66\u751f\u306f\u3059\u3067\u306b\u8ffd\u52a0\u3055\u308c\u3066\u3044\u307e\u3059\u3002", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u30a8\u30e9\u30fc - \u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u63a5\u7d9a\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", + "expired_credentials": "\u6709\u52b9\u671f\u9650\u304c\u5207\u308c\u305f\u8a8d\u8a3c\u60c5\u5831 - Vulcan\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u306e\u767b\u9332\u30da\u30fc\u30b8\u3067\u65b0\u898f\u4f5c\u6210\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "expired_token": "\u6709\u52b9\u671f\u9650\u5207\u308c\u306e\u30c8\u30fc\u30af\u30f3 - \u65b0\u3057\u3044\u30c8\u30fc\u30af\u30f3\u3092\u751f\u6210\u3057\u3066\u304f\u3060\u3055\u3044", + "invalid_pin": "\u7121\u52b9\u306a\u30d4\u30f3", + "invalid_symbol": "\u7121\u52b9\u306a\u30b7\u30f3\u30dc\u30eb", + "invalid_token": "\u7121\u52b9\u306a\u30c8\u30fc\u30af\u30f3", + "unknown": "\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f" + }, + "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "\u4fdd\u5b58\u3055\u308c\u305f\u8cc7\u683c\u60c5\u5831\u3092\u4f7f\u7528\u3059\u308b" + }, + "description": "\u5225\u306e\u751f\u5f92\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002" + }, + "auth": { + "data": { + "pin": "\u30d4\u30f3", + "region": "\u30b7\u30f3\u30dc\u30eb", + "token": "\u30c8\u30fc\u30af\u30f3" + }, + "description": "\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u767b\u9332\u30da\u30fc\u30b8\u3092\u4f7f\u7528\u3057\u3066\u3001Vulcan\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3059\u3002" + }, + "reauth": { + "data": { + "pin": "\u30d4\u30f3", + "region": "\u30b7\u30f3\u30dc\u30eb", + "token": "\u30c8\u30fc\u30af\u30f3" + }, + "description": "\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u767b\u9332\u30da\u30fc\u30b8\u3092\u4f7f\u7528\u3057\u3066\u3001Vulcan\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3059\u3002" + }, + "select_saved_credentials": { + "data": { + "credentials": "\u30ed\u30b0\u30a4\u30f3" + }, + "description": "\u4fdd\u5b58\u3055\u308c\u305f\u8cc7\u683c\u60c5\u5831\u3092\u9078\u629e\u3057\u307e\u3059\u3002" + }, + "select_student": { + "data": { + "student_name": "\u751f\u5f92\u3092\u9078\u629e(Select student)" + }, + "description": "\u751f\u5f92\u3092\u9078\u629e\u3057\u307e\u3059(Select student)\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u518d\u5ea6\u8ffd\u52a0\u3059\u308b\u3053\u3068\u3067\u3001\u3088\u308a\u591a\u304f\u306e\u751f\u5f92\u3092\u8ffd\u52a0\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002" + } + } + }, + "options": { + "error": { + "error": "\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f" + }, + "step": { + "init": { + "data": { + "attendance_notify": "\u6700\u65b0\u306e\u51fa\u5e2d\u30a8\u30f3\u30c8\u30ea\u306b\u95a2\u3059\u308b\u901a\u77e5\u3092\u8868\u793a\u3059\u308b", + "grade_notify": "\u6700\u65b0\u306e\u6210\u7e3e\u306b\u95a2\u3059\u308b\u901a\u77e5\u3092\u8868\u793a\u3059\u308b", + "message_notify": "\u65b0\u3057\u3044\u30e1\u30c3\u30bb\u30fc\u30b8\u306e\u53d7\u4fe1\u6642\u306b\u901a\u77e5\u3092\u8868\u793a\u3059\u308b", + "scan_interval": "\u66f4\u65b0\u9593\u9694(\u5206\u5358\u4f4d)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/nl.json b/homeassistant/components/vulcan/translations/nl.json new file mode 100644 index 00000000000..b05b32937c0 --- /dev/null +++ b/homeassistant/components/vulcan/translations/nl.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "all_student_already_configured": "Alle studenten zijn al toegevoegd.", + "already_configured": "Die student is al toegevoegd.", + "reauth_successful": "Opnieuw verifi\u00ebren gelukt" + }, + "error": { + "cannot_connect": "Verbindingsfout - controleer uw internetverbinding", + "expired_credentials": "Verlopen inloggegevens - maak een nieuwe aan op de Vulcan mobiele app registratiepagina", + "expired_token": "Verlopen token - genereer een nieuw token", + "invalid_pin": "Ongeldige pin", + "invalid_symbol": "Ongeldig symbool", + "invalid_token": "Ongeldige Token", + "unknown": "Onbekende fout opgetreden" + }, + "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "Gebruik opgeslagen referenties" + }, + "description": "Voeg nog een student toe." + }, + "auth": { + "data": { + "pin": "Pin", + "region": "Symbool", + "token": "Token" + }, + "description": "Log in op uw Vulcan-account via de registratiepagina voor mobiele apps." + }, + "reauth": { + "data": { + "pin": "Pin", + "region": "Symbool", + "token": "Token" + }, + "description": "Log in op uw Vulcan-account via de registratiepagina voor mobiele apps." + }, + "select_saved_credentials": { + "data": { + "credentials": "Inloggen" + }, + "description": "Selecteer opgeslagen referenties." + }, + "select_student": { + "data": { + "student_name": "Selecteer student" + }, + "description": "Selecteer student, u kunt meer studenten toevoegen door integratie opnieuw toe te voegen." + } + } + }, + "options": { + "error": { + "error": "Er is een fout opgetreden." + }, + "step": { + "init": { + "data": { + "attendance_notify": "Toon meldingen over de laatste aanwezigheidsgegevens", + "grade_notify": "Toon meldingen over de laatste cijfers", + "message_notify": "Meldingen weergeven wanneer een nieuw bericht is ontvangen", + "scan_interval": "Update-interval (in minuten)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/no.json b/homeassistant/components/vulcan/translations/no.json new file mode 100644 index 00000000000..7cb8b68fa63 --- /dev/null +++ b/homeassistant/components/vulcan/translations/no.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "all_student_already_configured": "Alle elever er allerede lagt til.", + "already_configured": "Den studenten er allerede lagt til.", + "reauth_successful": "Reauth vellykket" + }, + "error": { + "cannot_connect": "Tilkoblingsfeil - sjekk internettforbindelsen din", + "expired_credentials": "Utl\u00f8pt p\u00e5loggingsinformasjon - vennligst opprett ny p\u00e5 registreringssiden for Vulcan-mobilappen", + "expired_token": "Utl\u00f8pt token - generer et nytt token", + "invalid_pin": "Ugyldig pinkode", + "invalid_symbol": "Ugyldig symbol", + "invalid_token": "Ugyldig token", + "unknown": "Ukjent feil oppstod" + }, + "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "Bruk lagret legitimasjon" + }, + "description": "Legg til en annen student." + }, + "auth": { + "data": { + "pin": "Pin", + "region": "Symbol", + "token": "Token" + }, + "description": "Logg p\u00e5 Vulcan-kontoen din ved \u00e5 bruke registreringssiden for mobilappen." + }, + "reauth": { + "data": { + "pin": "Pin", + "region": "Symbol", + "token": "Token" + }, + "description": "Logg p\u00e5 Vulcan-kontoen din ved \u00e5 bruke registreringssiden for mobilappen." + }, + "select_saved_credentials": { + "data": { + "credentials": "P\u00e5logging" + }, + "description": "Velg lagret legitimasjon." + }, + "select_student": { + "data": { + "student_name": "Velg elev" + }, + "description": "Velg student, du kan legge til flere studenter ved \u00e5 legge til integrering p\u00e5 nytt." + } + } + }, + "options": { + "error": { + "error": "Det oppstod en feil" + }, + "step": { + "init": { + "data": { + "attendance_notify": "Vis varsler om de siste fremm\u00f8teoppf\u00f8ringene", + "grade_notify": "Vis varsler om de nyeste vurderingene", + "message_notify": "Vis varsler n\u00e5r ny melding mottas", + "scan_interval": "Oppdateringsintervall (i minutter)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/pl.json b/homeassistant/components/vulcan/translations/pl.json new file mode 100644 index 00000000000..acbc51c6754 --- /dev/null +++ b/homeassistant/components/vulcan/translations/pl.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "all_student_already_configured": "Wszyscy uczniowie zostali ju\u017c dodani.", + "already_configured": "Ten ucze\u0144 zosta\u0142 ju\u017c dodany.", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "expired_credentials": "Dane uwierzytelniaj\u0105ce wygas\u0142y \u2014 utw\u00f3rz nowe na stronie rejestracji aplikacji mobilnej Vulcan", + "expired_token": "Token wygas\u0142, wygeneruj nowy", + "invalid_pin": "Nieprawid\u0142owy kod PIN", + "invalid_symbol": "Nieprawid\u0142owy symbol", + "invalid_token": "Nieprawid\u0142owy token", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "U\u017cyj zapisanych danych uwierzytelniaj\u0105cych" + }, + "description": "Dodaj kolejnego ucznia." + }, + "auth": { + "data": { + "pin": "Kod pin", + "region": "Symbol", + "token": "Token" + }, + "description": "Zaloguj si\u0119 do swojego konta Vulcan za pomoc\u0105 strony rejestracji aplikacji mobilnej." + }, + "reauth": { + "data": { + "pin": "Kod pin", + "region": "Symbol", + "token": "Token" + }, + "description": "Zaloguj si\u0119 do swojego konta Vulcan za pomoc\u0105 strony rejestracji aplikacji mobilnej." + }, + "select_saved_credentials": { + "data": { + "credentials": "Login" + }, + "description": "Wybierz zapisane dane uwierzytelniaj\u0105ce." + }, + "select_student": { + "data": { + "student_name": "Wybierz ucznia" + }, + "description": "Wybierz ucznia. Mo\u017cesz doda\u0107 wi\u0119cej uczni\u00f3w, ponownie dodaj\u0105c integracj\u0119." + } + } + }, + "options": { + "error": { + "error": "Wyst\u0105pi\u0142 b\u0142\u0105d" + }, + "step": { + "init": { + "data": { + "attendance_notify": "Poka\u017c powiadomienia o najnowszych wpisach o obecno\u015bci", + "grade_notify": "Poka\u017c powiadomienia o najnowszych ocenach", + "message_notify": "Pokazuj powiadomienia po otrzymaniu nowej wiadomo\u015bci", + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (w minutach)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/pt-BR.json b/homeassistant/components/vulcan/translations/pt-BR.json new file mode 100644 index 00000000000..ecef34ff96b --- /dev/null +++ b/homeassistant/components/vulcan/translations/pt-BR.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "all_student_already_configured": "Todos os alunos j\u00e1 foram adicionados.", + "already_configured": "Esse aluno j\u00e1 foi adicionado.", + "reauth_successful": "Autentica\u00e7\u00e3o bem-sucedida" + }, + "error": { + "cannot_connect": "Erro de conex\u00e3o - verifique sua conex\u00e3o com a Internet", + "expired_credentials": "Credenciais expiradas - crie uma nova na p\u00e1gina de registro do aplicativo m\u00f3vel Vulcan", + "expired_token": "Token expirado - gere um novo token", + "invalid_pin": "Pin inv\u00e1lido", + "invalid_symbol": "S\u00edmbolo inv\u00e1lido", + "invalid_token": "Token inv\u00e1lido", + "unknown": "Ocorreu um erro desconhecido" + }, + "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "Usar credenciais salvas" + }, + "description": "Adicione outro aluno." + }, + "auth": { + "data": { + "pin": "Pin", + "region": "S\u00edmbolo", + "token": "Token" + }, + "description": "Fa\u00e7a login na sua conta Vulcan usando a p\u00e1gina de registro do aplicativo m\u00f3vel." + }, + "reauth": { + "data": { + "pin": "Pin", + "region": "S\u00edmbolo", + "token": "Token" + }, + "description": "Fa\u00e7a login na sua conta Vulcan usando a p\u00e1gina de registro do aplicativo m\u00f3vel." + }, + "select_saved_credentials": { + "data": { + "credentials": "Login" + }, + "description": "Selecione as credenciais salvas." + }, + "select_student": { + "data": { + "student_name": "Selecionar aluno" + }, + "description": "Selecione aluno, voc\u00ea pode adicionar mais alunos adicionando integra\u00e7\u00e3o novamente." + } + } + }, + "options": { + "error": { + "error": "Ocorreu um erro" + }, + "step": { + "init": { + "data": { + "attendance_notify": "Mostrar notifica\u00e7\u00f5es sobre as entradas de participa\u00e7\u00e3o mais recentes", + "grade_notify": "Mostrar notifica\u00e7\u00f5es sobre as notas mais recentes", + "message_notify": "Mostrar notifica\u00e7\u00f5es quando uma nova mensagem for recebida", + "scan_interval": "Intervalo de atualiza\u00e7\u00e3o (em minutos)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/ru.json b/homeassistant/components/vulcan/translations/ru.json new file mode 100644 index 00000000000..88e64d9ca79 --- /dev/null +++ b/homeassistant/components/vulcan/translations/ru.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "all_student_already_configured": "\u0412\u0441\u0435 \u0441\u0442\u0443\u0434\u0435\u043d\u0442\u044b \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u044b.", + "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0442\u0443\u0434\u0435\u043d\u0442 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d.", + "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": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443.", + "expired_credentials": "\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0441 \u0438\u0441\u0442\u0435\u043a\u0448\u0438\u043c \u0441\u0440\u043e\u043a\u043e\u043c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f. \u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u043d\u043e\u0432\u044b\u0435 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f Vulcan.", + "expired_token": "\u0421\u0440\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430 \u0438\u0441\u0442\u0435\u043a. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0439\u0442\u0435 \u043d\u043e\u0432\u044b\u0439 \u0442\u043e\u043a\u0435\u043d.", + "invalid_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434.", + "invalid_symbol": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u0441\u0438\u043c\u0432\u043e\u043b.", + "invalid_token": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d.", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" + }, + "description": "\u0414\u043e\u0431\u0430\u0432\u044c\u0442\u0435 \u0435\u0449\u0435 \u043e\u0434\u043d\u043e\u0433\u043e \u0441\u0442\u0443\u0434\u0435\u043d\u0442\u0430." + }, + "auth": { + "data": { + "pin": "PIN-\u043a\u043e\u0434", + "region": "\u0421\u0438\u043c\u0432\u043e\u043b", + "token": "\u0422\u043e\u043a\u0435\u043d" + }, + "description": "\u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Vulcan, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f." + }, + "reauth": { + "data": { + "pin": "PIN-\u043a\u043e\u0434", + "region": "\u0421\u0438\u043c\u0432\u043e\u043b", + "token": "\u0422\u043e\u043a\u0435\u043d" + }, + "description": "\u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Vulcan, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f." + }, + "select_saved_credentials": { + "data": { + "credentials": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." + }, + "select_student": { + "data": { + "student_name": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0442\u0443\u0434\u0435\u043d\u0442\u0430" + }, + "description": "\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0431\u043e\u043b\u044c\u0448\u0435 \u0441\u0442\u0443\u0434\u0435\u043d\u0442\u043e\u0432, \u0441\u043d\u043e\u0432\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u0432 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e." + } + } + }, + "options": { + "error": { + "error": "\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "init": { + "data": { + "attendance_notify": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f \u043e \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0445 \u0437\u0430\u043f\u0438\u0441\u044f\u0445 \u043e \u043f\u043e\u0441\u0435\u0449\u0430\u0435\u043c\u043e\u0441\u0442\u0438", + "grade_notify": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f \u043e \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0445 \u043e\u0446\u0435\u043d\u043a\u0430\u0445", + "message_notify": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f \u043e \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u043d\u043e\u0432\u043e\u0433\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f", + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/tr.json b/homeassistant/components/vulcan/translations/tr.json new file mode 100644 index 00000000000..9f4324dfbcc --- /dev/null +++ b/homeassistant/components/vulcan/translations/tr.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "all_student_already_configured": "T\u00fcm \u00f6\u011frenciler zaten eklendi.", + "already_configured": "Bu \u00f6\u011frenci zaten eklendi.", + "reauth_successful": "Yeniden yetkilendirme ba\u015far\u0131l\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flant\u0131 hatas\u0131 - l\u00fctfen internet ba\u011flant\u0131n\u0131z\u0131 kontrol edin", + "expired_credentials": "S\u00fcresi dolmu\u015f kimlik bilgileri - l\u00fctfen Vulcan mobil uygulama kay\u0131t sayfas\u0131nda yeni olu\u015fturun", + "expired_token": "S\u00fcresi dolmu\u015f anahtar - l\u00fctfen yeni bir anahtar olu\u015fturun", + "invalid_pin": "Ge\u00e7ersiz PIN", + "invalid_symbol": "Ge\u00e7ersiz sembol", + "invalid_token": "Ge\u00e7ersiz anahtar", + "unknown": "Bilinmeyen hata olu\u015ftu" + }, + "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "Kaydedilmi\u015f kimlik bilgilerini kullan" + }, + "description": "Ba\u015fka bir \u00f6\u011frenci ekleyin." + }, + "auth": { + "data": { + "pin": "Pin", + "region": "Sembol", + "token": "Anahtar" + }, + "description": "Mobil uygulama kay\u0131t sayfas\u0131n\u0131 kullanarak Vulcan Hesab\u0131n\u0131za giri\u015f yap\u0131n." + }, + "reauth": { + "data": { + "pin": "Pin", + "region": "Sembol", + "token": "Anahtar" + }, + "description": "Mobil uygulama kay\u0131t sayfas\u0131n\u0131 kullanarak Vulcan Hesab\u0131n\u0131za giri\u015f yap\u0131n." + }, + "select_saved_credentials": { + "data": { + "credentials": "Oturum a\u00e7" + }, + "description": "Kaydedilmi\u015f kimlik bilgilerini se\u00e7in." + }, + "select_student": { + "data": { + "student_name": "\u00d6\u011frenci se\u00e7" + }, + "description": "\u00d6\u011frenciyi se\u00e7in, yeniden t\u00fcmle\u015ftirme ekleyerek daha fazla \u00f6\u011frenci ekleyebilirsiniz." + } + } + }, + "options": { + "error": { + "error": "Hata olu\u015ftu" + }, + "step": { + "init": { + "data": { + "attendance_notify": "En son kat\u0131l\u0131m giri\u015fleriyle ilgili bildirimleri g\u00f6ster", + "grade_notify": "En son notlarla ilgili bildirimleri g\u00f6ster", + "message_notify": "Yeni mesaj al\u0131nd\u0131\u011f\u0131nda bildirimleri g\u00f6ster", + "scan_interval": "G\u00fcncelleme aral\u0131\u011f\u0131 (dakika olarak)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/zh-Hant.json b/homeassistant/components/vulcan/translations/zh-Hant.json new file mode 100644 index 00000000000..3a1c06ce5ad --- /dev/null +++ b/homeassistant/components/vulcan/translations/zh-Hant.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "all_student_already_configured": "\u6240\u6709\u5b78\u751f\u90fd\u5df2\u7d93\u65b0\u589e\u3002", + "already_configured": "\u8a72\u5b78\u751f\u5df2\u7d93\u65b0\u589e\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u932f\u8aa4 - \u8acb\u6aa2\u5bdf\u7db2\u8def\u9023\u7dda", + "expired_credentials": "\u6191\u8b49\u904e\u671f - \u8acb\u900f\u904e Vulcan \u884c\u52d5 App \u8a3b\u518a\u9801\u9762\u91cd\u65b0\u65b0\u589e", + "expired_token": "\u6b0a\u6756\u5df2\u904e\u671f - \u8acb\u7522\u751f\u65b0\u6b0a\u6756", + "invalid_pin": "Pin \u7121\u6548", + "invalid_symbol": "\u7b26\u865f\u7121\u6548", + "invalid_token": "\u6b0a\u6756\u7121\u6548", + "unknown": "\u767c\u751f\u672a\u77e5\u932f\u8aa4" + }, + "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "\u4f7f\u7528\u5132\u5b58\u6191\u8b49" + }, + "description": "\u65b0\u589e\u5176\u4ed6\u5b78\u751f\u3002" + }, + "auth": { + "data": { + "pin": "Pin", + "region": "\u7b26\u865f", + "token": "\u6b0a\u6756" + }, + "description": "\u4f7f\u7528\u884c\u52d5 App \u8a3b\u518a\u9801\u9762\u767b\u5165 Vulcan \u5e33\u865f\u3002" + }, + "reauth": { + "data": { + "pin": "Pin", + "region": "\u7b26\u865f", + "token": "\u6b0a\u6756" + }, + "description": "\u4f7f\u7528\u884c\u52d5 App \u8a3b\u518a\u9801\u9762\u767b\u5165 Vulcan \u5e33\u865f\u3002" + }, + "select_saved_credentials": { + "data": { + "credentials": "\u767b\u5165" + }, + "description": "\u9078\u64c7\u5132\u5b58\u6191\u8b49\u3002" + }, + "select_student": { + "data": { + "student_name": "\u9078\u64c7\u5b78\u751f" + }, + "description": "\u9078\u64c7\u5b78\u751f\u3001\u53ef\u4ee5\u518d\u900f\u904e\u65b0\u589e\u6574\u5408\u65b0\u589e\u5176\u4ed6\u5b78\u751f\u3002" + } + } + }, + "options": { + "error": { + "error": "\u767c\u751f\u932f\u8aa4" + }, + "step": { + "init": { + "data": { + "attendance_notify": "\u95dc\u65bc\u6700\u65b0\u51fa\u52e4\u5be6\u9ad4\u986f\u793a\u901a\u77e5", + "grade_notify": "\u95dc\u65bc\u6700\u65b0\u6210\u7e3e\u986f\u793a\u901a\u77e5", + "message_notify": "\u7576\u6536\u5230\u65b0\u8a0a\u606f\u6642\u986f\u793a\u901a\u77e5", + "scan_interval": "\u66f4\u65b0\u983b\u7387\uff08\u5206\uff09" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/bg.json b/homeassistant/components/wallbox/translations/bg.json index 25f3bb50845..cc0ea3ece2d 100644 --- a/homeassistant/components/wallbox/translations/bg.json +++ b/homeassistant/components/wallbox/translations/bg.json @@ -24,6 +24,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/ca.json b/homeassistant/components/wallbox/translations/ca.json index b6a10e16e2a..6d76243a479 100644 --- a/homeassistant/components/wallbox/translations/ca.json +++ b/homeassistant/components/wallbox/translations/ca.json @@ -25,6 +25,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/de.json b/homeassistant/components/wallbox/translations/de.json index d415b4f9e7a..2aa206d1b7e 100644 --- a/homeassistant/components/wallbox/translations/de.json +++ b/homeassistant/components/wallbox/translations/de.json @@ -25,6 +25,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/el.json b/homeassistant/components/wallbox/translations/el.json index dd95266866f..48443b1b45f 100644 --- a/homeassistant/components/wallbox/translations/el.json +++ b/homeassistant/components/wallbox/translations/el.json @@ -25,6 +25,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/en.json b/homeassistant/components/wallbox/translations/en.json index 28ec5d08235..f32c7b7b481 100644 --- a/homeassistant/components/wallbox/translations/en.json +++ b/homeassistant/components/wallbox/translations/en.json @@ -25,6 +25,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/es.json b/homeassistant/components/wallbox/translations/es.json index 72c7b0587d6..1c5315d6745 100644 --- a/homeassistant/components/wallbox/translations/es.json +++ b/homeassistant/components/wallbox/translations/es.json @@ -25,6 +25,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/et.json b/homeassistant/components/wallbox/translations/et.json index f5d3b3aac73..cfe0720616a 100644 --- a/homeassistant/components/wallbox/translations/et.json +++ b/homeassistant/components/wallbox/translations/et.json @@ -25,6 +25,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/fr.json b/homeassistant/components/wallbox/translations/fr.json index b9499f72b15..e918573e13b 100644 --- a/homeassistant/components/wallbox/translations/fr.json +++ b/homeassistant/components/wallbox/translations/fr.json @@ -25,6 +25,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/he.json b/homeassistant/components/wallbox/translations/he.json index 6109bb22195..06a2f86efa9 100644 --- a/homeassistant/components/wallbox/translations/he.json +++ b/homeassistant/components/wallbox/translations/he.json @@ -23,6 +23,5 @@ } } } - }, - "title": "\u05ea\u05d9\u05d1\u05ea \u05e7\u05d9\u05e8" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/hu.json b/homeassistant/components/wallbox/translations/hu.json index 5579da0fe88..d6d0c4837d4 100644 --- a/homeassistant/components/wallbox/translations/hu.json +++ b/homeassistant/components/wallbox/translations/hu.json @@ -25,6 +25,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/id.json b/homeassistant/components/wallbox/translations/id.json index 08611ab3c2e..430a0eac114 100644 --- a/homeassistant/components/wallbox/translations/id.json +++ b/homeassistant/components/wallbox/translations/id.json @@ -25,6 +25,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/it.json b/homeassistant/components/wallbox/translations/it.json index ce4bdef9d95..726a77396e2 100644 --- a/homeassistant/components/wallbox/translations/it.json +++ b/homeassistant/components/wallbox/translations/it.json @@ -25,6 +25,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/ja.json b/homeassistant/components/wallbox/translations/ja.json index 8924bf891b9..4aa79afcb22 100644 --- a/homeassistant/components/wallbox/translations/ja.json +++ b/homeassistant/components/wallbox/translations/ja.json @@ -25,6 +25,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/nl.json b/homeassistant/components/wallbox/translations/nl.json index dbe8bd91f72..5cde7830b85 100644 --- a/homeassistant/components/wallbox/translations/nl.json +++ b/homeassistant/components/wallbox/translations/nl.json @@ -25,6 +25,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/no.json b/homeassistant/components/wallbox/translations/no.json index 74bbb1f39d5..498362fad1d 100644 --- a/homeassistant/components/wallbox/translations/no.json +++ b/homeassistant/components/wallbox/translations/no.json @@ -25,6 +25,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/pl.json b/homeassistant/components/wallbox/translations/pl.json index 51180d4c68e..369049c3812 100644 --- a/homeassistant/components/wallbox/translations/pl.json +++ b/homeassistant/components/wallbox/translations/pl.json @@ -25,6 +25,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/pt-BR.json b/homeassistant/components/wallbox/translations/pt-BR.json index 3fb6428603a..ea7b6c899ed 100644 --- a/homeassistant/components/wallbox/translations/pt-BR.json +++ b/homeassistant/components/wallbox/translations/pt-BR.json @@ -25,6 +25,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/ru.json b/homeassistant/components/wallbox/translations/ru.json index 426c07c9423..e35de176bf9 100644 --- a/homeassistant/components/wallbox/translations/ru.json +++ b/homeassistant/components/wallbox/translations/ru.json @@ -25,6 +25,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/tr.json b/homeassistant/components/wallbox/translations/tr.json index 1bee24a696d..a1c69aea28d 100644 --- a/homeassistant/components/wallbox/translations/tr.json +++ b/homeassistant/components/wallbox/translations/tr.json @@ -25,6 +25,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/zh-Hant.json b/homeassistant/components/wallbox/translations/zh-Hant.json index 3f282cda1f9..2688a050ce0 100644 --- a/homeassistant/components/wallbox/translations/zh-Hant.json +++ b/homeassistant/components/wallbox/translations/zh-Hant.json @@ -25,6 +25,5 @@ } } } - }, - "title": "Wallbox" + } } \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/hu.json b/homeassistant/components/waze_travel_time/translations/hu.json index 401eb3c814c..75bc311d814 100644 --- a/homeassistant/components/waze_travel_time/translations/hu.json +++ b/homeassistant/components/waze_travel_time/translations/hu.json @@ -10,7 +10,7 @@ "user": { "data": { "destination": "\u00c9rkez\u00e9s helye", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "origin": "Indul\u00e1s helye", "region": "R\u00e9gi\u00f3" }, diff --git a/homeassistant/components/webostv/translations/hu.json b/homeassistant/components/webostv/translations/hu.json new file mode 100644 index 00000000000..de3f59d8c90 --- /dev/null +++ b/homeassistant/components/webostv/translations/hu.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "error_pairing": "Csatlakozva az LG webOS TV-hez, de a p\u00e1ros\u00edt\u00e1s nem siker\u00fclt" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rj\u00fck, kapcsolja be a TV-t vagy ellen\u0151rizze az ip-c\u00edmet." + }, + "flow_title": "LG webOS Smart TV", + "step": { + "pairing": { + "description": "K\u00fcldje el a k\u00e9r\u00e9st, \u00e9s fogadja el a p\u00e1ros\u00edt\u00e1si k\u00e9relmet a t\u00e9v\u00e9n. \n\n ![Image](/static/images/config_webos.png)", + "title": "webOS TV p\u00e1ros\u00edt\u00e1s" + }, + "user": { + "data": { + "host": "C\u00edm", + "name": "Elnevez\u00e9s" + }, + "description": "Kapcsolja be a TV-t, t\u00f6ltse ki a k\u00f6vetkez\u0151 mez\u0151ket, ut\u00e1na folytassa", + "title": "Csatlakoz\u00e1s a webOS TV-hez" + } + } + }, + "device_automation": { + "trigger_type": { + "webostv.turn_on": "Az eszk\u00f6znek be kell lennie kapcsolva" + } + }, + "options": { + "error": { + "cannot_retrieve": "Nem siker\u00fclt lek\u00e9rni a forr\u00e1sok list\u00e1j\u00e1t. Gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy a k\u00e9sz\u00fcl\u00e9k be van kapcsolva", + "script_not_found": "A szkript nem tal\u00e1lhat\u00f3" + }, + "step": { + "init": { + "data": { + "sources": "Forr\u00e1sok list\u00e1ja" + }, + "description": "Enged\u00e9lyezett forr\u00e1sok kiv\u00e1laszt\u00e1sa", + "title": "A webOS Smart TV be\u00e1ll\u00edt\u00e1sai" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/ca.json b/homeassistant/components/wilight/translations/ca.json index 0ace653e050..22044549b0c 100644 --- a/homeassistant/components/wilight/translations/ca.json +++ b/homeassistant/components/wilight/translations/ca.json @@ -8,7 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Voleu configurar el WiLight {name}? \n\n Admet: {components}", + "description": "S'admeten els seg\u00fcents components: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/wilight/translations/de.json b/homeassistant/components/wilight/translations/de.json index 546f8cec7b5..27d48a90e58 100644 --- a/homeassistant/components/wilight/translations/de.json +++ b/homeassistant/components/wilight/translations/de.json @@ -8,7 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "M\u00f6chtest du WiLight {name} einrichten? \n\n Es unterst\u00fctzt: {components}", + "description": "Die folgenden Komponenten werden unterst\u00fctzt: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/wilight/translations/en.json b/homeassistant/components/wilight/translations/en.json index 3d3a83a0270..1f0733d28f3 100644 --- a/homeassistant/components/wilight/translations/en.json +++ b/homeassistant/components/wilight/translations/en.json @@ -8,7 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Do you want to set up WiLight {name}?\n\n It supports: {components}", + "description": "The following components are supported: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/wilight/translations/et.json b/homeassistant/components/wilight/translations/et.json index 1be23313837..dae085d1084 100644 --- a/homeassistant/components/wilight/translations/et.json +++ b/homeassistant/components/wilight/translations/et.json @@ -8,7 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Kas soovid seadistada WiLight'i {name} ?\n\n See toetab: {components}", + "description": "Toetatud on j\u00e4rgmised komponendid: {components}", "title": "" } } diff --git a/homeassistant/components/wilight/translations/fr.json b/homeassistant/components/wilight/translations/fr.json index 0a3851bb816..ddd85a5cc04 100644 --- a/homeassistant/components/wilight/translations/fr.json +++ b/homeassistant/components/wilight/translations/fr.json @@ -8,7 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Voulez-vous configurer la WiLight {name}\u00a0?\n\n Elle prend en charge\u00a0: {components}", + "description": "Les composants suivants sont pris en charge\u00a0: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/wilight/translations/hu.json b/homeassistant/components/wilight/translations/hu.json index 9ef669f1ed3..02db2f1b7df 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": "Szeretn\u00e9 be\u00e1ll\u00edtani a WiLight {name}-t ? \n\nT\u00e1mogatja: {components}", + "description": "A k\u00f6vetkez\u0151 komponensek t\u00e1mogatottak: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/wilight/translations/id.json b/homeassistant/components/wilight/translations/id.json index 06616b29e35..0489a0e96bb 100644 --- a/homeassistant/components/wilight/translations/id.json +++ b/homeassistant/components/wilight/translations/id.json @@ -8,7 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Apakah Anda ingin menyiapkan WiLight {name}?\n\nIni mendukung: {components}", + "description": "Komponen berikut didukung: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/wilight/translations/it.json b/homeassistant/components/wilight/translations/it.json index 84b323a0000..7b560b7bfdb 100644 --- a/homeassistant/components/wilight/translations/it.json +++ b/homeassistant/components/wilight/translations/it.json @@ -8,7 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Vuoi configurare WiLight {name}? \n\nSupporta: {components}", + "description": "Sono supportati i seguenti componenti: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/wilight/translations/nl.json b/homeassistant/components/wilight/translations/nl.json index c2820f0ed6e..51cefe8ce28 100644 --- a/homeassistant/components/wilight/translations/nl.json +++ b/homeassistant/components/wilight/translations/nl.json @@ -8,7 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Wil je WiLight {name} ? \n\n Het ondersteunt: {components}", + "description": "De volgende componenten worden ondersteund: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/wilight/translations/no.json b/homeassistant/components/wilight/translations/no.json index 170739145da..582b2289a32 100644 --- a/homeassistant/components/wilight/translations/no.json +++ b/homeassistant/components/wilight/translations/no.json @@ -8,7 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Vil du konfigurere WiLight {name} ? \n\n Den st\u00f8tter: {components}", + "description": "F\u00f8lgende komponenter st\u00f8ttes: {components}", "title": "" } } diff --git a/homeassistant/components/wilight/translations/pl.json b/homeassistant/components/wilight/translations/pl.json index 93957d016f5..c1f636b9d80 100644 --- a/homeassistant/components/wilight/translations/pl.json +++ b/homeassistant/components/wilight/translations/pl.json @@ -8,7 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Czy chcesz skonfigurowa\u0107 WiLight {name}?\n\nObs\u0142uguje: {components}", + "description": "Obs\u0142ugiwane s\u0105 nast\u0119puj\u0105ce komponenty: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/wilight/translations/pt-BR.json b/homeassistant/components/wilight/translations/pt-BR.json index 5da689b9b74..e70a286e812 100644 --- a/homeassistant/components/wilight/translations/pt-BR.json +++ b/homeassistant/components/wilight/translations/pt-BR.json @@ -8,7 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Voc\u00ea deseja configurar WiLight {name}?\n\nEle suporta: {components}", + "description": "Os seguintes componentes s\u00e3o compat\u00edveis: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/wilight/translations/ru.json b/homeassistant/components/wilight/translations/ru.json index 7d04a13518d..7873b8ca326 100644 --- a/homeassistant/components/wilight/translations/ru.json +++ b/homeassistant/components/wilight/translations/ru.json @@ -8,7 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "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 WiLight {name}? \n\n\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442: {components}", + "description": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b: {components}.", "title": "WiLight" } } diff --git a/homeassistant/components/wilight/translations/tr.json b/homeassistant/components/wilight/translations/tr.json index 446504f49d7..4762a678254 100644 --- a/homeassistant/components/wilight/translations/tr.json +++ b/homeassistant/components/wilight/translations/tr.json @@ -8,7 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "{name} kurmak istiyor musunuz? \n\n \u015eunlar\u0131 destekler: {components}", + "description": "A\u015fa\u011f\u0131daki bile\u015fenler desteklenir: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/wilight/translations/zh-Hant.json b/homeassistant/components/wilight/translations/zh-Hant.json index fe6c36f21d2..70a9bff0550 100644 --- a/homeassistant/components/wilight/translations/zh-Hant.json +++ b/homeassistant/components/wilight/translations/zh-Hant.json @@ -8,7 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a WiLight {name}\uff1f\n\n\u652f\u63f4\uff1a{components}", + "description": "\u652f\u63f4\u4ee5\u4e0b\u5143\u4ef6\uff1a{components}", "title": "WiLight" } } diff --git a/homeassistant/components/withings/translations/hu.json b/homeassistant/components/withings/translations/hu.json index 1a64a95de5e..7504c36c58e 100644 --- a/homeassistant/components/withings/translations/hu.json +++ b/homeassistant/components/withings/translations/hu.json @@ -4,7 +4,7 @@ "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\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." + "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3." }, "create_entry": { "default": "A Withings sikeresen hiteles\u00edtett." @@ -15,7 +15,7 @@ "flow_title": "{profile}", "step": { "pick_implementation": { - "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" }, "profile": { "data": { diff --git a/homeassistant/components/withings/translations/it.json b/homeassistant/components/withings/translations/it.json index f97933593a0..079acb0b503 100644 --- a/homeassistant/components/withings/translations/it.json +++ b/homeassistant/components/withings/translations/it.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Configurazione aggiornata per il profilo.", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", - "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})" }, "create_entry": { diff --git a/homeassistant/components/wiz/translations/hu.json b/homeassistant/components/wiz/translations/hu.json index 7c99571a9ae..63ae8479bb9 100644 --- a/homeassistant/components/wiz/translations/hu.json +++ b/homeassistant/components/wiz/translations/hu.json @@ -28,7 +28,7 @@ "user": { "data": { "host": "IP c\u00edm", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" }, "description": "Ha az IP-c\u00edmet \u00fcresen hagyja, akkor az eszk\u00f6z\u00f6k keres\u00e9se a felder\u00edt\u00e9ssel t\u00f6rt\u00e9nik." } diff --git a/homeassistant/components/wolflink/translations/sensor.hu.json b/homeassistant/components/wolflink/translations/sensor.hu.json index 0a257e570cf..71e8f1e42ca 100644 --- a/homeassistant/components/wolflink/translations/sensor.hu.json +++ b/homeassistant/components/wolflink/translations/sensor.hu.json @@ -65,7 +65,7 @@ "sparen": "Gazdas\u00e1gos", "spreizung_hoch": "dT t\u00fal sz\u00e9les", "spreizung_kf": "Spread KF", - "stabilisierung": "Stabiliz\u00e1ci\u00f3", + "stabilisierung": "Stabiliz\u00e1l\u00e1s", "standby": "K\u00e9szenl\u00e9t", "start": "Indul\u00e1s", "storung": "Hiba", diff --git a/homeassistant/components/xbox/translations/hu.json b/homeassistant/components/xbox/translations/hu.json index 24c46bb8ab0..fc8428b3101 100644 --- a/homeassistant/components/xbox/translations/hu.json +++ b/homeassistant/components/xbox/translations/hu.json @@ -10,7 +10,7 @@ }, "step": { "pick_implementation": { - "title": "V\u00e1lassz hiteles\u00edt\u00e9si m\u00f3dszert" + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" } } } diff --git a/homeassistant/components/xbox/translations/it.json b/homeassistant/components/xbox/translations/it.json index 010baad571e..e60c37c9e5f 100644 --- a/homeassistant/components/xbox/translations/it.json +++ b/homeassistant/components/xbox/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", - "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "create_entry": { diff --git a/homeassistant/components/xiaomi_aqara/translations/ca.json b/homeassistant/components/xiaomi_aqara/translations/ca.json index 46037fa5eea..78f5affd556 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ca.json +++ b/homeassistant/components/xiaomi_aqara/translations/ca.json @@ -18,7 +18,7 @@ "data": { "select_ip": "Adre\u00e7a IP" }, - "description": "Torna a executar la configuraci\u00f3 si vols connectar passarel\u00b7les addicionals", + "description": "Selecciona la passarel\u00b7la Xiaomi Aqara a la qual connectar-te", "title": "Selecciona la passarel\u00b7la Xiaomi Aqara a la qual connectar-te" }, "settings": { @@ -27,7 +27,7 @@ "name": "Nom de la passarel\u00b7la" }, "description": "La clau (contrasenya) es pot obtenir mitjan\u00e7ant aquest tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Si no es proporciona la clau, nom\u00e9s seran accessibles els sensors", - "title": "Passarel\u00b7la Xiaomi Aqara, configuraci\u00f3 opcional" + "title": "Configuracions opcionals" }, "user": { "data": { @@ -35,7 +35,7 @@ "interface": "Interf\u00edcie de xarxa a utilitzar", "mac": "Adre\u00e7a MAC (opcional)" }, - "description": "Connecta't a la teva passarel\u00b7la Xiaomi Aqara, si les adreces IP i MAC es deixen buides s'utilitzar\u00e0 els descobriment autom\u00e0tic", + "description": "Si les adreces IP i MAC es deixen buides s'utilitzar\u00e0 el descobriment autom\u00e0tic", "title": "Passarel\u00b7la Xiaomi Aqara" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/de.json b/homeassistant/components/xiaomi_aqara/translations/de.json index 469fa14bcc1..170442fc856 100644 --- a/homeassistant/components/xiaomi_aqara/translations/de.json +++ b/homeassistant/components/xiaomi_aqara/translations/de.json @@ -18,7 +18,7 @@ "data": { "select_ip": "IP-Adresse" }, - "description": "F\u00fchre das Setup erneut aus, wenn du zus\u00e4tzliche Gateways verbinden m\u00f6chtest", + "description": "W\u00e4hle das Xiaomi Aqara Gateway, das du verbinden m\u00f6chtest", "title": "W\u00e4hle das Xiaomi Aqara Gateway, das du verbinden m\u00f6chtest" }, "settings": { @@ -27,7 +27,7 @@ "name": "Name des Gateways" }, "description": "Der Schl\u00fcssel (das Passwort) kann mithilfe dieser Anleitung abgerufen werden: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Wenn der Schl\u00fcssel nicht angegeben wird, sind nur die Sensoren zug\u00e4nglich", - "title": "Xiaomi Aqara Gateway, optionale Einstellungen" + "title": "Optionale Einstellungen" }, "user": { "data": { @@ -35,7 +35,7 @@ "interface": "Die zu verwendende Netzwerkschnittstelle", "mac": "MAC-Adresse (optional)" }, - "description": "Stelle eine Verbindung zu deinem Xiaomi Aqara Gateway her. Wenn die IP- und MAC-Adressen leer bleiben, wird die automatische Erkennung verwendet", + "description": "Wenn die IP- und MAC-Adressen leer gelassen werden, wird die automatische Erkennung verwendet", "title": "Xiaomi Aqara Gateway" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/en.json b/homeassistant/components/xiaomi_aqara/translations/en.json index 53776111d37..72949820bc0 100644 --- a/homeassistant/components/xiaomi_aqara/translations/en.json +++ b/homeassistant/components/xiaomi_aqara/translations/en.json @@ -18,7 +18,7 @@ "data": { "select_ip": "IP Address" }, - "description": "Run the setup again if you want to connect additional gateways", + "description": "Select the Xiaomi Aqara Gateway that you wish to connect", "title": "Select the Xiaomi Aqara Gateway that you wish to connect" }, "settings": { @@ -27,7 +27,7 @@ "name": "Name of the Gateway" }, "description": "The key (password) can be retrieved using this tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. If the key is not provided only sensors will be accessible", - "title": "Xiaomi Aqara Gateway, optional settings" + "title": "Optional settings" }, "user": { "data": { @@ -35,7 +35,7 @@ "interface": "The network interface to use", "mac": "Mac Address (optional)" }, - "description": "Connect to your Xiaomi Aqara Gateway, if the IP and MAC addresses are left empty, auto-discovery is used", + "description": "If the IP and MAC addresses are left empty, auto-discovery is used", "title": "Xiaomi Aqara Gateway" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/et.json b/homeassistant/components/xiaomi_aqara/translations/et.json index cc94b0e9b95..087c59dd2e2 100644 --- a/homeassistant/components/xiaomi_aqara/translations/et.json +++ b/homeassistant/components/xiaomi_aqara/translations/et.json @@ -18,7 +18,7 @@ "data": { "select_ip": "L\u00fc\u00fcsi IP aadress" }, - "description": "K\u00e4ivita seadistamine uuesti kui soovid \u00fchendada t\u00e4iendavaid l\u00fc\u00fcse", + "description": "Vali Xiaomi Aqara Gateway mida soovid \u00fchendada.", "title": "Vali Xiaomi Aqara l\u00fc\u00fcs mida soovid \u00fchendada" }, "settings": { @@ -27,7 +27,7 @@ "name": "L\u00fc\u00fcsi nimi" }, "description": "V\u00f5tme (parooli) saab hankida selle \u00f5petuse abil: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Kui v\u00f5ti puudub on ligip\u00e4\u00e4s ainult anduritele", - "title": "Xiaomi Aqara Gateway, valikulised s\u00e4tted" + "title": "Valikulised s\u00e4tted" }, "user": { "data": { @@ -35,7 +35,7 @@ "interface": "Kasutatav v\u00f5rguliides", "mac": "MAC aadress (valikuline)" }, - "description": "\u00dchendu oma Xiaomi Aqara Gatewayga. Kui IP- ja MAC-aadressid j\u00e4etakse t\u00fchjaks, kasutatakse automaatset avastamist", + "description": "Kui IP- ja MAC-aadressid j\u00e4etakse t\u00fchjaks, kasutatakse automaatset avastamist", "title": "" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/fr.json b/homeassistant/components/xiaomi_aqara/translations/fr.json index 04be4e35e2f..ab042cb5f0e 100644 --- a/homeassistant/components/xiaomi_aqara/translations/fr.json +++ b/homeassistant/components/xiaomi_aqara/translations/fr.json @@ -18,7 +18,7 @@ "data": { "select_ip": "Adresse IP" }, - "description": "Ex\u00e9cutez \u00e0 nouveau la configuration si vous souhaitez connecter des passerelles suppl\u00e9mentaires", + "description": "S\u00e9lectionnez la passerelle Xiaomi Aqara que vous souhaitez connecter", "title": "S\u00e9lectionnez la passerelle Xiaomi Aqara que vous souhaitez connecter" }, "settings": { @@ -27,7 +27,7 @@ "name": "Nom de la passerelle" }, "description": "La cl\u00e9 (mot de passe) peut \u00eatre r\u00e9cup\u00e9r\u00e9e \u00e0 l'aide de ce tutoriel: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Si la cl\u00e9 n'est pas fournie, seuls les capteurs seront accessibles", - "title": "Passerelle Xiaomi Aqara, param\u00e8tres optionnels" + "title": "Param\u00e8tres optionnels" }, "user": { "data": { @@ -35,7 +35,7 @@ "interface": "Interface r\u00e9seau \u00e0 utiliser", "mac": "Adresse MAC (facultatif)" }, - "description": "Connectez-vous \u00e0 votre passerelle Xiaomi Aqara, si les adresses IP et mac sont laiss\u00e9es vides, la d\u00e9tection automatique est utilis\u00e9e", + "description": "Si les adresses IP et MAC sont laiss\u00e9es vides, la d\u00e9couverte automatique sera utilis\u00e9e", "title": "Passerelle Xiaomi Aqara" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/he.json b/homeassistant/components/xiaomi_aqara/translations/he.json index 7450bbd463c..ea6cd8e278b 100644 --- a/homeassistant/components/xiaomi_aqara/translations/he.json +++ b/homeassistant/components/xiaomi_aqara/translations/he.json @@ -27,7 +27,7 @@ "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" + "title": "\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9\u05d5\u05ea" }, "user": { "data": { @@ -35,7 +35,7 @@ "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", + "description": "\u05d0\u05dd \u05db\u05ea\u05d5\u05d1\u05d5\u05ea \u05d4-IP \u05d5\u05d4-MAC \u05e0\u05d5\u05ea\u05e8\u05d5\u05ea \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/xiaomi_aqara/translations/hu.json b/homeassistant/components/xiaomi_aqara/translations/hu.json index e38bebefe19..f0a1b076750 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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "not_xiaomi_aqara": "Nem egy Xiaomi Aqara Gateway, a felfedezett eszk\u00f6z nem egyezett az ismert \u00e1tj\u00e1r\u00f3kkal" }, "error": { @@ -18,7 +18,7 @@ "data": { "select_ip": "IP c\u00edm" }, - "description": "Futtassa \u00fajra a be\u00e1ll\u00edt\u00e1st, ha egy m\u00e1sik k\u00f6zponti egys\u00e9get szeretne csatlakoztatni", + "description": "V\u00e1lassza ki a csatlakoztatni k\u00edv\u00e1nt Xiaomi Aqara K\u00f6zponti egys\u00e9get", "title": "V\u00e1lassza ki a csatlakoztatni k\u00edv\u00e1nt Xiaomi Aqara K\u00f6zponti egys\u00e9get" }, "settings": { @@ -27,7 +27,7 @@ "name": "K\u00f6zponti egys\u00e9g neve" }, "description": "A kulcs (jelsz\u00f3) az al\u00e1bbi oktat\u00f3anyag seg\u00edts\u00e9g\u00e9vel t\u00f6lthet\u0151 le: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Ha a kulcs nincs megadva, csak az \u00e9rz\u00e9kel\u0151k lesznek hozz\u00e1f\u00e9rhet\u0151k", - "title": "Xiaomi Aqara k\u00f6zponti egys\u00e9g, opcion\u00e1lis be\u00e1ll\u00edt\u00e1sok" + "title": "Opcion\u00e1lis be\u00e1ll\u00edt\u00e1sok" }, "user": { "data": { @@ -35,7 +35,7 @@ "interface": "A haszn\u00e1lni k\u00edv\u00e1nt h\u00e1l\u00f3zati interf\u00e9sz", "mac": "Mac-c\u00edm (opcion\u00e1lis)" }, - "description": "Csatlakozzon a Xiaomi Aqara k\u00f6zponti egys\u00e9ghez, ha az IP- \u00e9s a MAC-c\u00edm \u00fcresen marad, akkor az automatikus felder\u00edt\u00e9st haszn\u00e1lja", + "description": "Ha az IP- \u00e9s MAC-c\u00edm mez\u0151ket \u00fcresen hagyja, akkor az automatikus felder\u00edt\u00e9s ker\u00fcl alkalmaz\u00e1sra.", "title": "Xiaomi Aqara k\u00f6zponti egys\u00e9g" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/id.json b/homeassistant/components/xiaomi_aqara/translations/id.json index eeab548f681..2b33af8237f 100644 --- a/homeassistant/components/xiaomi_aqara/translations/id.json +++ b/homeassistant/components/xiaomi_aqara/translations/id.json @@ -18,7 +18,7 @@ "data": { "select_ip": "Alamat IP" }, - "description": "Jalankan penyiapan lagi jika Anda ingin menghubungkan gateway lainnya", + "description": "Pilih Gateway Xiaomi Aqara yang ingin disambungkan", "title": "Pilih Gateway Xiaomi Aqara yang ingin dihubungkan" }, "settings": { @@ -27,7 +27,7 @@ "name": "Nama Gateway" }, "description": "Kunci (kata sandi) dapat diambil menggunakan tutorial ini: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Jika kunci tidak disediakan, hanya sensor yang akan dapat diakses", - "title": "Xiaomi Aqara Gateway, pengaturan opsional" + "title": "Pengaturan opsional" }, "user": { "data": { @@ -35,7 +35,7 @@ "interface": "Antarmuka jaringan yang akan digunakan", "mac": "Alamat MAC (opsional)" }, - "description": "Hubungkan ke Xiaomi Aqara Gateway Anda, jika alamat IP dan MAC dibiarkan kosong, penemuan otomatis digunakan", + "description": "Jika alamat IP dan MAC dikosongkan, penemuan otomatis digunakan", "title": "Xiaomi Aqara Gateway" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/it.json b/homeassistant/components/xiaomi_aqara/translations/it.json index 319a33f3964..ac5eff78190 100644 --- a/homeassistant/components/xiaomi_aqara/translations/it.json +++ b/homeassistant/components/xiaomi_aqara/translations/it.json @@ -18,7 +18,7 @@ "data": { "select_ip": "Indirizzo IP" }, - "description": "Esegui di nuovo la configurazione se desideri connettere gateway aggiuntivi", + "description": "Seleziona lo Xiaomi Aqara Gateway che desideri connettere", "title": "Seleziona il Gateway Xiaomi Aqara che si desidera collegare" }, "settings": { @@ -27,7 +27,7 @@ "name": "Nome del Gateway" }, "description": "La chiave (password) pu\u00f2 essere recuperata utilizzando questo tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Se la chiave non viene fornita, saranno accessibili solo i sensori", - "title": "Xiaomi Aqara Gateway, impostazioni opzionali" + "title": "Impostazioni facoltative" }, "user": { "data": { @@ -35,7 +35,7 @@ "interface": "L'interfaccia di rete da utilizzare", "mac": "Indirizzo Mac (opzionale)" }, - "description": "Connettiti al tuo Xiaomi Aqara Gateway, se gli indirizzi IP e MAC sono lasciati vuoti, sar\u00e0 utilizzato il rilevamento automatico", + "description": "Se gli indirizzi IP e MAC vengono lasciati vuoti, viene utilizzato il rilevamento automatico", "title": "Xiaomi Aqara Gateway" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/nl.json b/homeassistant/components/xiaomi_aqara/translations/nl.json index 9ef660a8e7f..c2042850fdb 100644 --- a/homeassistant/components/xiaomi_aqara/translations/nl.json +++ b/homeassistant/components/xiaomi_aqara/translations/nl.json @@ -18,7 +18,7 @@ "data": { "select_ip": "IP-adres" }, - "description": "Voer de installatie opnieuw uit als u extra gateways wilt aansluiten", + "description": "Selecteer de Xiaomi Aqara Gateway die u wilt verbinden", "title": "Selecteer de Xiaomi Aqara Gateway waarmee u verbinding wilt maken" }, "settings": { @@ -27,7 +27,7 @@ "name": "Naam van de Gateway" }, "description": "De sleutel (wachtwoord) kan worden opgehaald met behulp van deze tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Als de sleutel niet wordt meegeleverd, zijn alleen sensoren toegankelijk", - "title": "Xiaomi Aqara Gateway, optionele instellingen" + "title": "Optionele instellingen" }, "user": { "data": { @@ -35,7 +35,7 @@ "interface": "De netwerkinterface die moet worden gebruikt", "mac": "MAC-adres (optioneel)" }, - "description": "Maak verbinding met uw Xiaomi Aqara Gateway, als de IP- en mac-adressen leeg worden gelaten, wordt automatische detectie gebruikt", + "description": "Als de IP- en MAC-adressen leeg worden gelaten, wordt auto-discovery gebruikt", "title": "Xiaomi Aqara Gateway" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/no.json b/homeassistant/components/xiaomi_aqara/translations/no.json index 081b0e5e990..c325886dd22 100644 --- a/homeassistant/components/xiaomi_aqara/translations/no.json +++ b/homeassistant/components/xiaomi_aqara/translations/no.json @@ -18,7 +18,7 @@ "data": { "select_ip": "IP adresse" }, - "description": "Kj\u00f8r oppsettet p\u00e5 nytt hvis du vil koble til flere gatewayer", + "description": "Velg Xiaomi Aqara Gateway som du \u00f8nsker \u00e5 koble til", "title": "Velg Xiaomi Aqara Gateway som du \u00f8nsker \u00e5 koble til" }, "settings": { @@ -27,7 +27,7 @@ "name": "Navnet p\u00e5 gatewayen" }, "description": "N\u00f8kkelen (passordet) kan hentes ved hjelp av denne veiviseren: [https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz](https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz). Hvis n\u00f8kkelen ikke oppgis, vil bare sensorer bli tilgjengelige", - "title": "Xiaomi Aqara Gateway, valgfrie innstillinger" + "title": "Valgfrie innstillinger" }, "user": { "data": { @@ -35,7 +35,7 @@ "interface": "Nettverksgrensesnittet som skal brukes", "mac": "MAC-adresse (valgfritt)" }, - "description": "Koble til Xiaomi Aqara Gateway, hvis IP- og MAC-adressene blir tomme, brukes automatisk oppdagelse", + "description": "Hvis IP- og MAC-adressene er tomme, brukes automatisk oppdagelse", "title": "" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/pl.json b/homeassistant/components/xiaomi_aqara/translations/pl.json index da8a803d81f..e680415320c 100644 --- a/homeassistant/components/xiaomi_aqara/translations/pl.json +++ b/homeassistant/components/xiaomi_aqara/translations/pl.json @@ -18,7 +18,7 @@ "data": { "select_ip": "Adres IP" }, - "description": "Uruchom konfiguracj\u0119 ponownie, je\u015bli chcesz pod\u0142\u0105czy\u0107 dodatkowe bramki", + "description": "Wybierz bramk\u0119 Xiaomi Aqara, z kt\u00f3r\u0105 chcesz si\u0119 po\u0142\u0105czy\u0107", "title": "Wybierz bramk\u0119 Xiaomi Aqara, z kt\u00f3r\u0105 chcesz si\u0119 po\u0142\u0105czy\u0107" }, "settings": { @@ -27,7 +27,7 @@ "name": "Nazwa bramki" }, "description": "Klucz (has\u0142o) mo\u017cna uzyska\u0107 za pomoc\u0105 tej instrukcji: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Je\u015bli klucz nie zostanie podany, dost\u0119pne b\u0119d\u0105 tylko sensory.", - "title": "Brama Xiaomi Aqara, ustawienia opcjonalne" + "title": "Ustawienia opcjonalne" }, "user": { "data": { @@ -35,7 +35,7 @@ "interface": "Interfejs sieciowy", "mac": "Adres MAC (opcjonalnie)" }, - "description": "Po\u0142\u0105cz si\u0119 z bramk\u0105 Xiaomi Aqara, je\u015bli zostawisz Adres IP oraz MAC puste to bramka zostanie automatycznie wykryta.", + "description": "Je\u015bli zostawisz Adres IP oraz MAC puste to bramka zostanie automatycznie wykryta.", "title": "Bramka Xiaomi Aqara" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/pt-BR.json b/homeassistant/components/xiaomi_aqara/translations/pt-BR.json index 5e188e54565..62deb3ba97c 100644 --- a/homeassistant/components/xiaomi_aqara/translations/pt-BR.json +++ b/homeassistant/components/xiaomi_aqara/translations/pt-BR.json @@ -18,7 +18,7 @@ "data": { "select_ip": "Endere\u00e7o IP" }, - "description": "Execute a configura\u00e7\u00e3o novamente se quiser conectar gateways adicionais", + "description": "Selecione o Xiaomi Aqara Gateway que voc\u00ea deseja conectar", "title": "Selecione o Xiaomi Aqara Gateway que voc\u00ea deseja conectar" }, "settings": { @@ -27,15 +27,15 @@ "name": "Nome do Gateway" }, "description": "A chave (senha) pode ser recuperada usando este tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Se a chave n\u00e3o for fornecida, apenas os sensores estar\u00e3o acess\u00edveis", - "title": "Xiaomi Aqara Gateway, configura\u00e7\u00f5es opcionais" + "title": "Configura\u00e7\u00f5es opcionais" }, "user": { "data": { - "host": "Endere\u00e7o IP", + "host": "Endere\u00e7o IP (opcional)", "interface": "A interface de rede a ser usada", "mac": "Endere\u00e7o Mac (opcional)" }, - "description": "Conecte-se ao seu Xiaomi Aqara Gateway, se os endere\u00e7os IP e MAC ficarem vazios, a descoberta autom\u00e1tica \u00e9 usada", + "description": "Se os endere\u00e7os IP e MAC forem deixados vazios, a descoberta autom\u00e1tica \u00e9 usada", "title": "Gateway Xiaomi Aqara" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/ru.json b/homeassistant/components/xiaomi_aqara/translations/ru.json index aa18808d222..499837889df 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ru.json +++ b/homeassistant/components/xiaomi_aqara/translations/ru.json @@ -18,7 +18,7 @@ "data": { "select_ip": "IP-\u0430\u0434\u0440\u0435\u0441" }, - "description": "\u0417\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0435\u0449\u0451 \u0440\u0430\u0437, \u0435\u0441\u043b\u0438 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0449\u0451 \u043e\u0434\u0438\u043d \u0448\u043b\u044e\u0437.", + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 Xiaomi Aqara.", "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 Xiaomi Aqara" }, "settings": { @@ -27,7 +27,7 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, "description": "\u041a\u043b\u044e\u0447 (\u043f\u0430\u0440\u043e\u043b\u044c) \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u044d\u0442\u043e\u0433\u043e \u0440\u0443\u043a\u043e\u0432\u043e\u0434\u0441\u0442\u0432\u0430: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. \u0415\u0441\u043b\u0438 \u043a\u043b\u044e\u0447 \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d, \u0431\u0443\u0434\u0443\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u0430\u0442\u0447\u0438\u043a\u0438.", - "title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara" + "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" }, "user": { "data": { @@ -35,7 +35,7 @@ "interface": "\u0421\u0435\u0442\u0435\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441", "mac": "MAC-\u0430\u0434\u0440\u0435\u0441 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441\u043e \u0448\u043b\u044e\u0437\u043e\u043c Xiaomi Aqara. \u0414\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0448\u043b\u044e\u0437\u0430, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u044f IP \u0438 MAC \u0430\u0434\u0440\u0435\u0441\u043e\u0432 \u043f\u0443\u0441\u0442\u044b\u043c\u0438.", + "description": "\u0414\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0448\u043b\u044e\u0437\u0430, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u044f IP \u0438 MAC \u0430\u0434\u0440\u0435\u0441\u043e\u0432 \u043f\u0443\u0441\u0442\u044b\u043c\u0438.", "title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/tr.json b/homeassistant/components/xiaomi_aqara/translations/tr.json index 6281b7a397e..0c961a34a3c 100644 --- a/homeassistant/components/xiaomi_aqara/translations/tr.json +++ b/homeassistant/components/xiaomi_aqara/translations/tr.json @@ -18,7 +18,7 @@ "data": { "select_ip": "IP Adresi" }, - "description": "Ek a\u011f ge\u00e7itlerini ba\u011flamak istiyorsan\u0131z kurulumu tekrar \u00e7al\u0131\u015ft\u0131r\u0131n.", + "description": "Ba\u011flamak istedi\u011finiz Xiaomi Aqara A\u011f Ge\u00e7idini se\u00e7in", "title": "Ba\u011flamak istedi\u011finiz Xiaomi Aqara A\u011f Ge\u00e7idini se\u00e7in" }, "settings": { @@ -27,7 +27,7 @@ "name": "A\u011f Ge\u00e7idinin Ad\u0131" }, "description": "Anahtar (parola) bu \u00f6\u011fretici kullan\u0131larak al\u0131nabilir: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Anahtar sa\u011flanmazsa, yaln\u0131zca sens\u00f6rlere eri\u015filebilir", - "title": "Xiaomi Aqara A\u011f Ge\u00e7idi, iste\u011fe ba\u011fl\u0131 ayarlar" + "title": "\u0130ste\u011fe ba\u011fl\u0131 ayarlar" }, "user": { "data": { @@ -35,7 +35,7 @@ "interface": "Kullan\u0131lacak a\u011f aray\u00fcz\u00fc", "mac": "Mac Adresi (iste\u011fe ba\u011fl\u0131)" }, - "description": "Xiaomi Aqara Gateway'inize ba\u011flan\u0131n, IP ve MAC adresleri bo\u015f b\u0131rak\u0131l\u0131rsa otomatik ke\u015fif kullan\u0131l\u0131r", + "description": "IP ve MAC adresleri bo\u015f b\u0131rak\u0131l\u0131rsa otomatik ke\u015fif kullan\u0131l\u0131r", "title": "Xiaomi Aqara A\u011f Ge\u00e7idi" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json index 058efd39631..5ffe34d5d39 100644 --- a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json @@ -18,7 +18,7 @@ "data": { "select_ip": "IP \u4f4d\u5740" }, - "description": "\u5982\u679c\u9084\u9700\u8981\u9023\u7dda\u81f3\u5176\u4ed6\u7db2\u95dc\uff0c\u8acb\u518d\u57f7\u884c\u4e00\u6b21\u8a2d\u5b9a", + "description": "\u5982\u679c\u6240\u8981\u9023\u7dda\u7684\u5c0f\u7c73 Aqara \u7db2\u95dc", "title": "\u9078\u64c7\u6240\u8981\u9023\u7dda\u7684\u5c0f\u7c73 Aqara \u7db2\u95dc" }, "settings": { @@ -27,7 +27,7 @@ "name": "\u7db2\u95dc\u540d\u7a31" }, "description": "\u91d1\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\u91d1\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" + "title": "\u9078\u9805\u8a2d\u5b9a" }, "user": { "data": { @@ -35,7 +35,7 @@ "interface": "\u4f7f\u7528\u7684\u7db2\u8def\u4ecb\u9762", "mac": "Mac \u4f4d\u5740\uff08\u9078\u9805\uff09" }, - "description": "\u9023\u7dda\u81f3\u5c0f\u7c73 Aqara \u7db2\u95dc\uff0c\u5047\u5982 IP \u6216 Mac \u4f4d\u5740\u70ba\u7a7a\u767d\u3001\u5c07\u9032\u884c\u81ea\u52d5\u641c\u7d22", + "description": "\u5047\u5982 IP \u6216 Mac \u4f4d\u5740\u70ba\u7a7a\u767d\u3001\u5c07\u9032\u884c\u81ea\u52d5\u641c\u7d22", "title": "\u5c0f\u7c73 Aqara \u7db2\u95dc" } } diff --git a/homeassistant/components/xiaomi_miio/translations/he.json b/homeassistant/components/xiaomi_miio/translations/he.json index 4bb0251d6cb..db0cee16884 100644 --- a/homeassistant/components/xiaomi_miio/translations/he.json +++ b/homeassistant/components/xiaomi_miio/translations/he.json @@ -51,7 +51,7 @@ "name": "\u05e9\u05dd \u05d4\u05e9\u05e2\u05e8", "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" }, - "description": "\u05d0\u05ea\u05d4 \u05d6\u05e7\u05d5\u05e7 \u05dc-32 \u05ea\u05d5\u05d5\u05d9 \u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df API , \u05e8\u05d0\u05d4 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u05dc\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea. \u05e9\u05d9\u05dd \u05dc\u05d1, \u05db\u05d9 \u05d0\u05e1\u05d9\u05de\u05d5\u05df API \u05e9\u05d5\u05e0\u05d4 \u05de\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05de\u05e9\u05de\u05e9 \u05d0\u05ea \u05e9\u05d9\u05dc\u05d5\u05d1 Xiaomi Aqara.", + "description": "\u05e0\u05d3\u05e8\u05e9\u05d9\u05dd 32 \u05ea\u05d5\u05d5\u05d9 \u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df API, \u05e8\u05d0\u05d4 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u05dc\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea. \u05e9\u05d9\u05dd \u05dc\u05d1, \u05db\u05d9 \u05d0\u05e1\u05d9\u05de\u05d5\u05df API \u05e9\u05d5\u05e0\u05d4 \u05de\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05de\u05e9\u05de\u05e9 \u05d0\u05ea \u05e9\u05d9\u05dc\u05d5\u05d1 Xiaomi Aqara.", "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9" }, "manual": { diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json index 8d2340988dd..189e9906e24 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\u00e1l\u00e1s m\u00e1r folyamatban van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "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." diff --git a/homeassistant/components/xiaomi_miio/translations/pt-BR.json b/homeassistant/components/xiaomi_miio/translations/pt-BR.json index 9e966a541d5..1116de258fa 100644 --- a/homeassistant/components/xiaomi_miio/translations/pt-BR.json +++ b/homeassistant/components/xiaomi_miio/translations/pt-BR.json @@ -25,7 +25,7 @@ "cloud_username": "Usu\u00e1rio da Cloud", "manual": "Configurar manualmente (n\u00e3o recomendado)" }, - "description": "Fa\u00e7a login na Cloud Xiaomi Miio, consulte https://www.openhab.org/addons/bindings/miio/#country-servers para o servidor em cloud usar.", + "description": "Coloque o login da Cloud Xiaomi Miio e consulte https://www.openhab.org/addons/bindings/miio/#country-servers para saber qual servidor cloud usar.", "title": "Conecte-se a um dispositivo Xiaomi Miio ou Xiaomi Gateway" }, "connect": { diff --git a/homeassistant/components/yale_smart_alarm/translations/hu.json b/homeassistant/components/yale_smart_alarm/translations/hu.json index 028f2cd6f0f..a18480fcac0 100644 --- a/homeassistant/components/yale_smart_alarm/translations/hu.json +++ b/homeassistant/components/yale_smart_alarm/translations/hu.json @@ -12,7 +12,7 @@ "reauth_confirm": { "data": { "area_id": "Ter\u00fclet ID", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } @@ -20,7 +20,7 @@ "user": { "data": { "area_id": "Ter\u00fclet ID", - "name": "N\u00e9v", + "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" } diff --git a/homeassistant/components/yeelight/translations/cs.json b/homeassistant/components/yeelight/translations/cs.json index adc42efddb7..2a3084fd3eb 100644 --- a/homeassistant/components/yeelight/translations/cs.json +++ b/homeassistant/components/yeelight/translations/cs.json @@ -27,7 +27,7 @@ "init": { "data": { "model": "Model (voliteln\u00fd)", - "nightlight_switch": "Pou\u017e\u00edt p\u0159ep\u00edna\u010d no\u010dn\u00edho osv\u011btlen\u00ed", + "nightlight_switch": "Pou\u017e\u00edt vyp\u00edna\u010d no\u010dn\u00edho osv\u011btlen\u00ed", "save_on_change": "Ulo\u017eit stav p\u0159i zm\u011bn\u011b", "transition": "\u010cas p\u0159echodu (v ms)", "use_music_mode": "Povolit hudebn\u00ed re\u017eim" diff --git a/homeassistant/components/yeelight/translations/fr.json b/homeassistant/components/yeelight/translations/fr.json index b2153e6aec0..a319f15e36a 100644 --- a/homeassistant/components/yeelight/translations/fr.json +++ b/homeassistant/components/yeelight/translations/fr.json @@ -32,7 +32,7 @@ "model": "Mod\u00e8le", "nightlight_switch": "Utiliser le commutateur de veilleuse", "save_on_change": "Enregistrer l'\u00e9tat lors d'un changement", - "transition": "Dur\u00e9e de transition (en millisecondes)", + "transition": "Dur\u00e9e de la transition (en millisecondes)", "use_music_mode": "Activer le mode musique" }, "description": "Si vous ne pr\u00e9cisez pas le mod\u00e8le, il sera automatiquement d\u00e9tect\u00e9." diff --git a/homeassistant/components/youless/translations/hu.json b/homeassistant/components/youless/translations/hu.json index 31913b7fa6f..415753be254 100644 --- a/homeassistant/components/youless/translations/hu.json +++ b/homeassistant/components/youless/translations/hu.json @@ -7,7 +7,7 @@ "user": { "data": { "host": "C\u00edm", - "name": "N\u00e9v" + "name": "Elnevez\u00e9s" } } } diff --git a/homeassistant/components/zha/translations/cs.json b/homeassistant/components/zha/translations/cs.json index 6a7d4aa17ea..de16e3bd387 100644 --- a/homeassistant/components/zha/translations/cs.json +++ b/homeassistant/components/zha/translations/cs.json @@ -35,7 +35,7 @@ "button_6": "\u0160est\u00e9 tla\u010d\u00edtko", "close": "Zav\u0159\u00edt", "dim_down": "ztmavit", - "dim_up": "ro\u017ehnout", + "dim_up": "rozjasnit", "face_1": "aktivov\u00e1no tv\u00e1\u0159\u00ed 1", "face_2": "aktivov\u00e1no tv\u00e1\u0159\u00ed 2", "face_3": "aktivov\u00e1no tv\u00e1\u0159\u00ed 3", diff --git a/homeassistant/components/zwave_js/translations/bg.json b/homeassistant/components/zwave_js/translations/bg.json index bdae86569ac..59fcf968740 100644 --- a/homeassistant/components/zwave_js/translations/bg.json +++ b/homeassistant/components/zwave_js/translations/bg.json @@ -17,6 +17,9 @@ "data": { "url": "URL" } + }, + "zeroconf_confirm": { + "title": "\u041e\u0442\u043a\u0440\u0438\u0442 Z-Wave JS \u0441\u044a\u0440\u0432\u044a\u0440" } } }, diff --git a/homeassistant/components/zwave_js/translations/ca.json b/homeassistant/components/zwave_js/translations/ca.json index 5e0f834ef19..6691894b79d 100644 --- a/homeassistant/components/zwave_js/translations/ca.json +++ b/homeassistant/components/zwave_js/translations/ca.json @@ -59,6 +59,10 @@ }, "usb_confirm": { "description": "Vols configurar {name} amb el complement Z-Wave JS?" + }, + "zeroconf_confirm": { + "description": "Vols afegir el servidor Z-Wave JS amb ID {home_id} que es troba a {url} a Home Assistant?", + "title": "Servidor Z-Wave JS descobert" } } }, diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json index 4900c9055d9..02418f44306 100644 --- a/homeassistant/components/zwave_js/translations/de.json +++ b/homeassistant/components/zwave_js/translations/de.json @@ -59,6 +59,10 @@ }, "usb_confirm": { "description": "M\u00f6chtest du {name} mit dem Z-Wave JS Add-on einrichten?" + }, + "zeroconf_confirm": { + "description": "M\u00f6chtest du den Z-Wave JS-Server mit der Home-ID {home_id} , gefunden unter {url} , zu Home Assistant hinzuf\u00fcgen?", + "title": "Z-Wave JS-Server entdeckt" } } }, diff --git a/homeassistant/components/zwave_js/translations/el.json b/homeassistant/components/zwave_js/translations/el.json index 2a4a519369a..66330206b60 100644 --- a/homeassistant/components/zwave_js/translations/el.json +++ b/homeassistant/components/zwave_js/translations/el.json @@ -59,6 +59,10 @@ }, "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;" + }, + "zeroconf_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Z-Wave JS Server \u03bc\u03b5 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c3\u03c0\u03b9\u03c4\u03b9\u03bf\u03cd {home_id} \u03c0\u03bf\u03c5 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf {url} \u03c3\u03c4\u03bf Home Assistant;", + "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 Z-Wave JS" } } }, diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index 843f2aaf284..fa9794ed847 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", @@ -116,6 +117,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", @@ -144,5 +146,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/et.json b/homeassistant/components/zwave_js/translations/et.json index c6c5db1ebd7..29f50ccb290 100644 --- a/homeassistant/components/zwave_js/translations/et.json +++ b/homeassistant/components/zwave_js/translations/et.json @@ -59,6 +59,10 @@ }, "usb_confirm": { "description": "Kas seadistada Z-Wave JS lisandmoodul {name}?" + }, + "zeroconf_confirm": { + "description": "Kas lisada Z-Wave JS-server, mille ID {home_id} on leitud aadressil {url}, Home Assistant'ile?", + "title": "Avastatud Z-Wave JS Server" } } }, diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index 2645e573b6d..22e8ed19e32 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -59,6 +59,10 @@ }, "usb_confirm": { "description": "Voulez-vous configurer {name} avec le plugin Z-Wave JS ?" + }, + "zeroconf_confirm": { + "description": "Voulez-vous ajouter le serveur Z-Wave\u00a0JS portant l'ID {home_id} et trouv\u00e9 sur {url} \u00e0 Home Assistant\u00a0?", + "title": "Serveur Z-Wave\u00a0JS d\u00e9couvert" } } }, diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json index 1803cfa4a26..07dbb93b703 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 be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "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." @@ -59,6 +59,10 @@ }, "usb_confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani {name} alkalmaz\u00e1st a Z-Wave JS b\u0151v\u00edtm\u00e9nnyel?" + }, + "zeroconf_confirm": { + "description": "Szeretn\u00e9 hozz\u00e1adni a {url} c\u00edmen tal\u00e1lhat\u00f3 {home_id} azonos\u00edt\u00f3val rendelkez\u0151 Z-Wave JS szervert a Home Assistanthoz?", + "title": "Felfedezett Z-Wave JS szerver" } } }, diff --git a/homeassistant/components/zwave_js/translations/id.json b/homeassistant/components/zwave_js/translations/id.json index 7ffb9d7b713..ef14a550210 100644 --- a/homeassistant/components/zwave_js/translations/id.json +++ b/homeassistant/components/zwave_js/translations/id.json @@ -10,7 +10,7 @@ "already_in_progress": "Alur konfigurasi sedang berlangsung", "cannot_connect": "Gagal terhubung", "discovery_requires_supervisor": "Fitur penemuan membutuhkan supervisor.", - "not_zwave_device": "Perangkat yang ditemukan bukanperangkat Z-Wave." + "not_zwave_device": "Perangkat yang ditemukan bukan perangkat Z-Wave." }, "error": { "addon_start_failed": "Gagal memulai add-on Z-Wave JS. Periksa konfigurasi.", @@ -59,6 +59,10 @@ }, "usb_confirm": { "description": "Ingin menyiapkan {name} dengan add-on Z-Wave JS?" + }, + "zeroconf_confirm": { + "description": "Apakah Anda ingin menambahkan Server Z-Wave JS dengan ID rumah {home_id} di {url} ke Home Assistant?", + "title": "Server Z-Wave JS yang Ditemukan" } } }, diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json index d455b7befe6..5922601921a 100644 --- a/homeassistant/components/zwave_js/translations/it.json +++ b/homeassistant/components/zwave_js/translations/it.json @@ -59,6 +59,10 @@ }, "usb_confirm": { "description": "Vuoi configurare {name} con il componente aggiuntivo JS Z-Wave?" + }, + "zeroconf_confirm": { + "description": "Vuoi aggiungere il server Z-Wave JS con l'ID casa {home_id} trovato in {url} a Home Assistant?", + "title": "Server JS Z-Wave rilevato" } } }, diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index c85025c1394..6df591dd0ab 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -59,6 +59,10 @@ }, "usb_confirm": { "description": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u3067 {name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "zeroconf_confirm": { + "description": "{url} \u306b\u3042\u308b\u30db\u30fc\u30e0ID {home_id} \u306eZ-Wave JS\u30b5\u30fc\u30d0\u30fc\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u307e\u3059\u304b\uff1f", + "title": "Z-Wave JS\u30b5\u30fc\u30d0\u30fc\u3092\u767a\u898b" } } }, diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json index 1f99373c18f..2d3ce5421ed 100644 --- a/homeassistant/components/zwave_js/translations/nl.json +++ b/homeassistant/components/zwave_js/translations/nl.json @@ -59,6 +59,10 @@ }, "usb_confirm": { "description": "Wilt u {name} instellen met de Z-Wave JS add-on?" + }, + "zeroconf_confirm": { + "description": "Wilt u de Z-Wave JS Server met home ID {home_id} gevonden op {url} toevoegen aan Home Assistant?", + "title": "Ontdekt Z-Wave JS Server" } } }, diff --git a/homeassistant/components/zwave_js/translations/no.json b/homeassistant/components/zwave_js/translations/no.json index 24efdf1c573..854bd307abc 100644 --- a/homeassistant/components/zwave_js/translations/no.json +++ b/homeassistant/components/zwave_js/translations/no.json @@ -59,6 +59,10 @@ }, "usb_confirm": { "description": "Vil du konfigurere {name} med Z-Wave JS-tillegget?" + }, + "zeroconf_confirm": { + "description": "Vil du legge til Z-Wave JS Server med hjemme-ID {home_id} funnet p\u00e5 {url} til Home Assistant?", + "title": "Oppdaget Z-Wave JS Server" } } }, diff --git a/homeassistant/components/zwave_js/translations/pl.json b/homeassistant/components/zwave_js/translations/pl.json index 68dd6555552..2edb9fd8c1f 100644 --- a/homeassistant/components/zwave_js/translations/pl.json +++ b/homeassistant/components/zwave_js/translations/pl.json @@ -59,6 +59,10 @@ }, "usb_confirm": { "description": "Czy chcesz skonfigurowa\u0107 {name} z dodatkiem Z-Wave JS?" + }, + "zeroconf_confirm": { + "description": "Czy chcesz doda\u0107 do Home Assistanta serwer Z-Wave JS z identyfikatorem {home_id} znalezionym pod {url}?", + "title": "Wykryty serwer Z-Wave JS" } } }, diff --git a/homeassistant/components/zwave_js/translations/pt-BR.json b/homeassistant/components/zwave_js/translations/pt-BR.json index b8fadd65089..5e1e8610fc7 100644 --- a/homeassistant/components/zwave_js/translations/pt-BR.json +++ b/homeassistant/components/zwave_js/translations/pt-BR.json @@ -59,6 +59,10 @@ }, "usb_confirm": { "description": "Deseja configurar o {name} com o add-on Z-Wave JS?" + }, + "zeroconf_confirm": { + "description": "Deseja adicionar o servidor Z-Wave JS com o ID inicial {home_id} encontrado em {url} ao Home Assistant?", + "title": "Servidor Z-Wave JS descoberto" } } }, diff --git a/homeassistant/components/zwave_js/translations/ru.json b/homeassistant/components/zwave_js/translations/ru.json index bc1d3e2cbd4..14011c78517 100644 --- a/homeassistant/components/zwave_js/translations/ru.json +++ b/homeassistant/components/zwave_js/translations/ru.json @@ -59,6 +59,10 @@ }, "usb_confirm": { "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS?" + }, + "zeroconf_confirm": { + "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 \u0441\u0435\u0440\u0432\u0435\u0440 Z-Wave JS \u0441 home ID {home_id}, \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 {url}?", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0441\u0435\u0440\u0432\u0435\u0440 Z-Wave JS" } } }, diff --git a/homeassistant/components/zwave_js/translations/tr.json b/homeassistant/components/zwave_js/translations/tr.json index efaa87cc48d..a3b5f6fb9f4 100644 --- a/homeassistant/components/zwave_js/translations/tr.json +++ b/homeassistant/components/zwave_js/translations/tr.json @@ -59,6 +59,10 @@ }, "usb_confirm": { "description": "{name} Z-Wave JS eklentisiyle kurmak istiyor musunuz?" + }, + "zeroconf_confirm": { + "description": "{url} adresinde bulunan {home_id} ev kimli\u011fine sahip Z-Wave JS Sunucusunu Home Assistant'a eklemek istiyor musunuz?", + "title": "Ke\u015ffedilen Z-Wave JS Sunucusu" } } }, diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json index 664cd6b48d9..2ea728a591e 100644 --- a/homeassistant/components/zwave_js/translations/zh-Hant.json +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -59,6 +59,10 @@ }, "usb_confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u540d\u70ba {name} \u7684 Z-Wave JS \u9644\u52a0\u5143\u4ef6\uff1f" + }, + "zeroconf_confirm": { + "description": "\u662f\u5426\u8981\u65b0\u589e\u65bc {url} \u6240\u627e\u5230\u4e4b ID \u70ba {home_id} \u4e4b Z-Wave JS \u4f3a\u670d\u5668\u81f3 Home Assistant\uff1f", + "title": "\u6240\u767c\u73fe\u7684 Z-Wave JS \u4f3a\u670d\u5668" } } }, diff --git a/homeassistant/components/zwave_me/translations/ca.json b/homeassistant/components/zwave_me/translations/ca.json index a382cef5494..abd616b1ed5 100644 --- a/homeassistant/components/zwave_me/translations/ca.json +++ b/homeassistant/components/zwave_me/translations/ca.json @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "token": "Token", + "token": "Token d'API", "url": "URL" }, - "description": "Introdueix l'adre\u00e7a IP del servidor Z-Way i el 'token' d'acc\u00e9s Z-Way. Si s'utilitza HTTPS en lloc d'HTTP, l'adre\u00e7a IP es pot prefixar amb wss://. Per obtenir el 'token', v\u00e9s a la interf\u00edcie d'usuari de Z-Way > Men\u00fa > Configuraci\u00f3 > Usuari > Token API. Es recomana crear un nou usuari de Home Assistant i concedir-li acc\u00e9s als dispositius que necessitis controlar des de Home Assistant. Tamb\u00e9 \u00e9s possible utilitzar l'acc\u00e9s remot a trav\u00e9s de find.z-wave.me per connectar amb un Z-Way remot. Introdueix wss://find.z-wave.me al camp d'IP i copia el 'token' d'\u00e0mbit global (inicia sessi\u00f3 a Z-Way mitjan\u00e7ant find.z-wave.me)." + "description": "Introdueix l'adre\u00e7a IP del servidor Z-Way i el 'token' d'acc\u00e9s Z-Way. Per obtenir el 'token', v\u00e9s a la interf\u00edcie d'usuari de Z-Way Smart Home UI > Men\u00fa > Configuraci\u00f3 > Usuaris > Administrador > Token API.\n\nExemple de connexi\u00f3 a Z-Way en una xarxa local:\nURL: {local_url}\nToken: {local_token}\n\nExemple de connexi\u00f3 a Z-Way a trav\u00e9s de l'acc\u00e9s remot find.z-wave.me:\nURL: {find_url}\nToken: {find_token}\n\nExemple de connexi\u00f3 a Z-Way amb una adre\u00e7a IP p\u00fablica:\nURL: {remote_url}\nToken: {local_token}\n\nSi et connectes a trav\u00e9s de find.z-wave.me, has d'utilitzar un 'token' d'abast global (inicia sessi\u00f3 a Z-Way via find.z-wave.me per fer-ho)." } } } diff --git a/homeassistant/components/zwave_me/translations/de.json b/homeassistant/components/zwave_me/translations/de.json index 5afd788a3fb..747ccf0c9c8 100644 --- a/homeassistant/components/zwave_me/translations/de.json +++ b/homeassistant/components/zwave_me/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits eingerichtet", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "no_valid_uuid_set": "Keine g\u00fcltige UUID gesetzt" }, "error": { @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "token": "Token", + "token": "API-Token", "url": "URL" }, - "description": "Gib die IP-Adresse des Z-Way-Servers und das Z-Way-Zugangs-Token ein. Der IP-Adresse kann wss:// vorangestellt werden, wenn HTTPS anstelle von HTTP verwendet werden soll. Um das Token zu erhalten, gehe auf Z-Way-Benutzeroberfl\u00e4che > Men\u00fc > Einstellungen > Benutzer > API-Token. Es wird empfohlen, einen neuen Benutzer f\u00fcr Home Assistant zu erstellen und den Ger\u00e4ten, die du \u00fcber den Home Assistant steuern m\u00f6chtest, Zugriff zu gew\u00e4hren. Es ist auch m\u00f6glich, den Fernzugriff \u00fcber find.z-wave.me zu nutzen, um ein entferntes Z-Way zu verbinden. Gib wss://find.z-wave.me in das IP-Feld ein und kopiere das Token mit globalem Geltungsbereich (logge dich dazu \u00fcber find.z-wave.me bei Z-Way ein)." + "description": "Gib die IP-Adresse mit Port und Zugangs-Token des Z-Way-Servers ein. Um das Token zu erhalten, gehe zur Z-Way-Benutzeroberfl\u00e4che Smart Home UI > Men\u00fc > Einstellungen > Benutzer > Administrator > API-Token.\n\nBeispiel f\u00fcr die Verbindung zu Z-Way im lokalen Netzwerk:\nURL: {local_url}\nToken: {local_token}\n\nBeispiel f\u00fcr die Verbindung zu Z-Way \u00fcber den Fernzugriff find.z-wave.me:\nURL: {find_url}\nToken: {find_token}\n\nBeispiel f\u00fcr eine Verbindung zu Z-Way mit einer statischen \u00f6ffentlichen IP-Adresse:\nURL: {remote_url}\nToken: {local_token}\n\nWenn du dich \u00fcber find.z-wave.me verbindest, musst du ein Token mit globalem Geltungsbereich verwenden (logge dich dazu \u00fcber find.z-wave.me bei Z-Way ein)." } } } diff --git a/homeassistant/components/zwave_me/translations/en.json b/homeassistant/components/zwave_me/translations/en.json index 81d09d5c350..0ad88764806 100644 --- a/homeassistant/components/zwave_me/translations/en.json +++ b/homeassistant/components/zwave_me/translations/en.json @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "token": "Token", + "token": "API Token", "url": "URL" }, - "description": "Input IP address of Z-Way server and Z-Way access token. IP address can be prefixed with wss:// if HTTPS should be used instead of HTTP. To get the token go to the Z-Way user interface > Menu > Settings > User > API token. It is suggested to create a new user for Home Assistant and grant access to devices you need to control from Home Assistant. It is also possible to use remote access via find.z-wave.me to connect a remote Z-Way. Input wss://find.z-wave.me in IP field and copy the token with Global scope (log-in to Z-Way via find.z-wave.me for this)." + "description": "Input IP address with port and access token of Z-Way server. To get the token go to the Z-Way user interface Smart Home UI > Menu > Settings > Users > Administrator > API token.\n\nExample of connecting to Z-Way in the local network:\nURL: {local_url}\nToken: {local_token}\n\nExample of connecting to Z-Way via remote access find.z-wave.me:\nURL: {find_url}\nToken: {find_token}\n\nExample of connecting to Z-Way with a static public IP address:\nURL: {remote_url}\nToken: {local_token}\n\nWhen connecting via find.z-wave.me you need to use a token with a global scope (log-in to Z-Way via find.z-wave.me for this)." } } } diff --git a/homeassistant/components/zwave_me/translations/et.json b/homeassistant/components/zwave_me/translations/et.json index c07b4b2b5f8..1969e86fc4a 100644 --- a/homeassistant/components/zwave_me/translations/et.json +++ b/homeassistant/components/zwave_me/translations/et.json @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "token": "Token", + "token": "API v\u00f5ti", "url": "URL" }, - "description": "Sisesta Z-Way serveri IP-aadress ja Z-Way juurdep\u00e4\u00e4suluba. Kui HTTP asemel tuleks kasutada HTTPS-i, v\u00f5ib IP-aadressile lisada eesliite wss://. Tokeni hankimiseks mine Z-Way kasutajaliidesesse > Men\u00fc\u00fc > Seaded > Kasutaja > API tunnus. Soovitatav on luua Home Assistantile uus kasutaja ja anda juurdep\u00e4\u00e4s seadmetele mida pead Home Assistandi abil juhtima. Kaug-Z-Way \u00fchendamiseks on v\u00f5imalik kasutada ka kaugjuurdep\u00e4\u00e4su l\u00e4bi find.z-wave.me. Sisesta IP v\u00e4ljale wss://find.z-wave.me ja kopeeri globaalse ulatusega luba (selleks logi Z-Waysse sisse saidi find.z-wave.me kaudu)." + "description": "Sisesta IP-aadress koos pordi ja Z-Way serveri juurdep\u00e4\u00e4suv\u00f5tmega. Tokeni hankimiseks ava Z-Way kasutajaliides Smart Home UI > Men\u00fc\u00fc > Seaded > Kasutajad > Administraator > API luba. \n\n N\u00e4ide kohalikus v\u00f5rgus Z-Wayga \u00fchenduse loomisest:\n URL: {local_url}\n Token: {local_token} \n\n N\u00e4ide Z-Wayga \u00fchenduse loomisest kaugjuurdep\u00e4\u00e4su kaudu find.z-wave.me:\n URL: {find_url}\n Token: {find_token} \n\n N\u00e4ide Z-Wayga \u00fchenduse loomisest staatilise avaliku IP-aadressiga:\n URL: {remote_url}\n Token: {local_token} \n\n Kui \u00fchendud saidi find.z-wave.me kaudu, pead kasutama globaalse ulatusega luba (selleks logi Z-Waysse sisse saidi find.z-wave.me kaudu)." } } } diff --git a/homeassistant/components/zwave_me/translations/fr.json b/homeassistant/components/zwave_me/translations/fr.json index 1705796cf77..b53964d96ff 100644 --- a/homeassistant/components/zwave_me/translations/fr.json +++ b/homeassistant/components/zwave_me/translations/fr.json @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "token": "Jeton", + "token": "Jeton d'API", "url": "URL" }, - "description": "Entrez l'adresse IP du serveur Z-Way et le jeton d'acc\u00e8s Z-Way. L'adresse IP peut \u00eatre pr\u00e9c\u00e9d\u00e9e de wss:// si HTTPS doit \u00eatre utilis\u00e9 \u00e0 la place de HTTP. Pour obtenir le jeton, acc\u00e9dez \u00e0 l'interface utilisateur Z-Way > Menu > Param\u00e8tres > Utilisateur > Jeton API. Il est sugg\u00e9r\u00e9 de cr\u00e9er un nouvel utilisateur pour Home Assistant et d'accorder l'acc\u00e8s aux appareils que vous devez contr\u00f4ler \u00e0 partir de Home Assistant. Il est \u00e9galement possible d'utiliser l'acc\u00e8s \u00e0 distance via find.z-wave.me pour connecter un Z-Way distant. Entrez wss://find.z-wave.me dans le champ IP et copiez le jeton avec la port\u00e9e globale (connectez-vous \u00e0 Z-Way via find.z-wave.me pour cela)." + "description": "Saisissez l'adresse IP avec le port ainsi que le jeton d'acc\u00e8s au serveur Z-Way. Pour obtenir le jeton, acc\u00e9dez \u00e0 l'interface utilisateur Z-Way Smart Home UI > Menu > Param\u00e8tres > Utilisateurs > Administrateur > jeton d'API.\n\nExemple de connexion \u00e0 Z-Way sur le r\u00e9seau local\u00a0:\nURL\u00a0: {local_url}\nJeton\u00a0: {local_token}\n\nExemple de connexion \u00e0 Z-Way via l'acc\u00e8s \u00e0 distance find.z-wave.me\u00a0:\nURL\u00a0: {find_url}\nJeton\u00a0: {find_token}\n\nExemple de connexion \u00e0 Z-Way avec une adresse IP publique statique\u00a0:\nURL\u00a0: {remote_url}\nJeton\u00a0: {local_token}\n\nLorsque vous vous connectez via find.z-wave.me, vous devez utiliser un jeton avec une port\u00e9e globale (pour cela, connectez-vous \u00e0 Z-Way via find.z-wave.me)." } } } diff --git a/homeassistant/components/zwave_me/translations/he.json b/homeassistant/components/zwave_me/translations/he.json index 5689db627e2..ad8b69eaee6 100644 --- a/homeassistant/components/zwave_me/translations/he.json +++ b/homeassistant/components/zwave_me/translations/he.json @@ -6,7 +6,7 @@ "step": { "user": { "data": { - "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df", + "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API", "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8" } } diff --git a/homeassistant/components/zwave_me/translations/hu.json b/homeassistant/components/zwave_me/translations/hu.json index 679604d6cfa..21ed9026feb 100644 --- a/homeassistant/components/zwave_me/translations/hu.json +++ b/homeassistant/components/zwave_me/translations/hu.json @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "token": "Token", + "token": "API Token", "url": "URL" }, - "description": "Adja meg a Z-Way szerver IP-c\u00edm\u00e9t \u00e9s a Z-Way hozz\u00e1f\u00e9r\u00e9si tokent. Az IP-c\u00edm el\u00e9 wss:// el\u0151tagot lehet tenni, ha HTTP helyett HTTPS-t kell haszn\u00e1lni. A token megszerz\u00e9s\u00e9hez l\u00e9pjen a Z-Way felhaszn\u00e1l\u00f3i fel\u00fcletre > Men\u00fc > Be\u00e1ll\u00edt\u00e1sok > Felhaszn\u00e1l\u00f3 > API token men\u00fcpontba. Javasoljuk, hogy hozzon l\u00e9tre egy \u00faj felhaszn\u00e1l\u00f3t a Home Assistanthoz, \u00e9s adjon hozz\u00e1f\u00e9r\u00e9st azoknak az eszk\u00f6z\u00f6knek, amelyeket a Home Assistantb\u00f3l kell vez\u00e9relnie. A t\u00e1voli Z-Way csatlakoztat\u00e1s\u00e1hoz a find.z-wave.me oldalon kereszt\u00fcl t\u00e1voli hozz\u00e1f\u00e9r\u00e9st is haszn\u00e1lhat. \u00cdrja be az IP mez\u0151be a wss://find.z-wave.me c\u00edmet, \u00e9s m\u00e1solja be a tokent glob\u00e1lis hat\u00f3k\u00f6rrel (ehhez jelentkezzen be a Z-Way-be a find.z-wave.me oldalon kereszt\u00fcl)." + "description": "Adja meg a Z-Way szerver IP-c\u00edm\u00e9t, portj\u00e1t \u00e9s hozz\u00e1f\u00e9r\u00e9si jel\u00e9t. A token megszerz\u00e9s\u00e9hez n\u00e9zze meg a Z-Way felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9n a Smart Home UI > Menu > Settings > Users > Administrator > API token men\u00fcpontot.\n\nP\u00e9lda a Z-Wayhez val\u00f3 csatlakoz\u00e1sra a helyi h\u00e1l\u00f3zaton:\nURL: {local_url}\nToken: {local_token}\n\nP\u00e9lda a Z-Wayhez t\u00e1voli el\u00e9r\u00e9ssel t\u00f6rt\u00e9n\u0151 csatlakoz\u00e1sra find.z-wave.me:\nURL: {find_url}\nToken: {find_token}\n\nP\u00e9lda a Z-Wayhez val\u00f3 csatlakoz\u00e1sra statikus nyilv\u00e1nos IP-c\u00edmmel:\nURL: {remote_url}\nToken: {local_token}\n\nA find.z-wave.me oldalon kereszt\u00fcl t\u00f6rt\u00e9n\u0151 csatlakoz\u00e1skor glob\u00e1lis hat\u00f3k\u00f6r\u0171 tokent kell haszn\u00e1lnia (ehhez a find.z-wave.me oldalon kereszt\u00fcl kell bejelentkeznie a Z-Way-be)." } } } diff --git a/homeassistant/components/zwave_me/translations/id.json b/homeassistant/components/zwave_me/translations/id.json index da9ddeb6d67..7e934a6c258 100644 --- a/homeassistant/components/zwave_me/translations/id.json +++ b/homeassistant/components/zwave_me/translations/id.json @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "token": "Token", + "token": "Token API", "url": "URL" }, - "description": "Masukkan alamat IP server Z-Way dan token akses Z-Way. Alamat IP dapat diawali dengan wss:// jika HTTPS harus digunakan sebagai pengganti HTTP. Untuk mendapatkan token, buka antarmuka pengguna Z-Way > Menu > Pengaturan > Token API > Pengguna. Disarankan untuk membuat pengguna baru untuk Home Assistant dan memberikan akses ke perangkat yang perlu Anda kontrol dari Home Assistant. Dimungkinkan juga untuk menggunakan akses jarak jauh melalui find.z-wave.me untuk menghubungkan Z-Way jarak jauh. Masukkan wss://find.z-wave.me di bidang IP dan salin token dengan cakupan Global (masuk ke Z-Way melalui find.z-wave.me untuk ini)." + "description": "Masukkan alamat IP dengan port dan token akses server Z-Way. Untuk mendapatkan token, buka antarmuka pengguna Z-Way Smart Home UI > Menu > Settings > Users > Administrator > API token.\n\nContoh menghubungkan ke Z-Way di jaringan lokal:\nURL: {local_url}\nToken: {local_token}\n\nContoh menghubungkan ke Z-Way melalui akses jarak jauh find.z-wave.me:\nURL: {find_url}\nToken: {find_token}\n\nContoh menghubungkan ke Z-Way dengan alamat IP publik statis:\nURL: {remote_url}\nToken: {local_token}\n\nSaat terhubung melalui find.z-wave.me Anda perlu menggunakan token dengan cakupan global (masuk ke Z-Way melalui find.z-wave.me untuk hal ini)." } } } diff --git a/homeassistant/components/zwave_me/translations/it.json b/homeassistant/components/zwave_me/translations/it.json index d60ac60d899..048312af73e 100644 --- a/homeassistant/components/zwave_me/translations/it.json +++ b/homeassistant/components/zwave_me/translations/it.json @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "token": "Token", + "token": "Token API", "url": "URL" }, - "description": "Digita l'indirizzo IP del server Z-Way e il token di accesso Z-Way. L'indirizzo IP pu\u00f2 essere preceduto da wss:// se si deve utilizzare HTTPS invece di HTTP. Per ottenere il token, vai all'interfaccia utente di Z-Way > Menu > Impostazioni > Utente > Token API. Si consiglia di creare un nuovo utente per Home Assistant e concedere l'accesso ai dispositivi che devi controllare da Home Assistant. \u00c8 anche possibile utilizzare l'accesso remoto tramite find.z-wave.me per connettere uno Z-Way remoto. Inserisci wss://find.z-wave.me nel campo IP e copia il token con ambito globale (accedi a Z-Way tramite find.z-wave.me per questo)." + "description": "Digita l'indirizzo IP con la porta e il token di accesso del server Z-Way. Per ottenere il token, vai all'interfaccia utente di Z-Way Smart Home UI > Menu > Impostazioni > Utenti > Amministratore > API token. \n\nEsempio di connessione a Z-Way nella rete locale:\nURL: {local_url}\nToken: {local_token} \n\nEsempio di connessione a Z-Way tramite accesso remoto find.z-wave.me:\nURL: {find_url}\nToken: {find_token} \n\nEsempio di connessione a Z-Way con un indirizzo IP pubblico statico:\nURL: {remote_url}\nToken: {local_token} \n\nQuando ti connetti tramite find.z-wave.me devi usare un token con un ambito globale (accedi a Z-Way tramite find.z-wave.me per questo)." } } } diff --git a/homeassistant/components/zwave_me/translations/nl.json b/homeassistant/components/zwave_me/translations/nl.json index a2356ed1035..04692559e81 100644 --- a/homeassistant/components/zwave_me/translations/nl.json +++ b/homeassistant/components/zwave_me/translations/nl.json @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "token": "Token", + "token": "API-token", "url": "URL" }, - "description": "Voer het IP-adres van de Z-Way server en het Z-Way toegangstoken in. Het IP adres kan voorafgegaan worden door wss:// indien HTTPS gebruikt moet worden in plaats van HTTP. Om het token te verkrijgen gaat u naar de Z-Way gebruikersinterface > Menu > Instellingen > Gebruiker > API token. Het is aanbevolen om een nieuwe gebruiker voor Home Assistant aan te maken en toegang te verlenen aan apparaten die u wilt bedienen vanuit Home Assistant. Het is ook mogelijk om toegang op afstand te gebruiken via find.z-wave.me om een Z-Way op afstand te verbinden. Voer wss://find.z-wave.me in het IP veld in en kopieer het token met Global scope (log hiervoor in op Z-Way via find.z-wave.me)." + "description": "Voer IP adres met poort en toegangstoken van Z-Way server in. Om het token te verkrijgen gaat u naar de Z-Way gebruikersinterface Smart Home UI > Menu > Instellingen > Gebruikers > Beheerder > API token.\n\nVoorbeeld van verbinding met Z-Way in het lokale netwerk:\nURL: {local_url}\nToken: {local_token}\n\nVoorbeeld van verbinding maken met Z-Way via remote access find.z-wave.me:\nURL: {find_url}\nToken: {find_token}\n\nVoorbeeld van een verbinding met Z-Way met een statisch publiek IP-adres:\nURL: {remote_url}\nToken: {local_token}\n\nWanneer je verbinding maakt via find.z-wave.me moet je een token gebruiken met een globale scope (log hiervoor in op Z-Way via find.z-wave.me)." } } } diff --git a/homeassistant/components/zwave_me/translations/no.json b/homeassistant/components/zwave_me/translations/no.json index 8a9a6928e93..266f74fdb61 100644 --- a/homeassistant/components/zwave_me/translations/no.json +++ b/homeassistant/components/zwave_me/translations/no.json @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "token": "Token", + "token": "API-token", "url": "URL" }, - "description": "Skriv inn IP-adressen til Z-Way-serveren og Z-Way-tilgangstoken. IP-adressen kan settes foran med wss:// hvis HTTPS skal brukes i stedet for HTTP. For \u00e5 f\u00e5 tokenet, g\u00e5 til Z-Way-brukergrensesnittet > Meny > Innstillinger > Bruker > API-token. Det foresl\u00e5s \u00e5 opprette en ny bruker for Home Assistant og gi tilgang til enheter du m\u00e5 kontrollere fra Home Assistant. Det er ogs\u00e5 mulig \u00e5 bruke ekstern tilgang via find.z-wave.me for \u00e5 koble til en ekstern Z-Way. Skriv inn wss://find.z-wave.me i IP-feltet og kopier token med Global scope (logg inn p\u00e5 Z-Way via find.z-wave.me for dette)." + "description": "Skriv inn IP-adresse med port og tilgangstoken til Z-Way-serveren. For \u00e5 f\u00e5 tokenet, g\u00e5 til Z-Way-brukergrensesnittet Smart Home UI > Meny > Innstillinger > Brukere > Administrator > API-token. \n\n Eksempel p\u00e5 tilkobling til Z-Way i det lokale nettverket:\n URL: {local_url}\n Token: {local_token} \n\n Eksempel p\u00e5 tilkobling til Z-Way via fjerntilgang find.z-wave.me:\n URL: {find_url}\n Token: {find_token} \n\n Eksempel p\u00e5 tilkobling til Z-Way med en statisk offentlig IP-adresse:\n URL: {remote_url}\n Token: {local_token} \n\n N\u00e5r du kobler til via find.z-wave.me m\u00e5 du bruke en token med et globalt omfang (logg inn p\u00e5 Z-Way via find.z-wave.me for dette)." } } } diff --git a/homeassistant/components/zwave_me/translations/pl.json b/homeassistant/components/zwave_me/translations/pl.json index d75251367cd..bc9d43bed0d 100644 --- a/homeassistant/components/zwave_me/translations/pl.json +++ b/homeassistant/components/zwave_me/translations/pl.json @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "token": "Token", + "token": "Token API", "url": "URL" }, - "description": "Wprowad\u017a adres IP serwera Z-Way i token dost\u0119pu Z-Way. Adres IP mo\u017ce by\u0107 poprzedzony wss://, je\u015bli zamiast HTTP powinien by\u0107 u\u017cywany HTTPS. Aby uzyska\u0107 token, przejd\u017a do interfejsu u\u017cytkownika Z-Way > Menu > Ustawienia > U\u017cytkownik > Token API. Sugeruje si\u0119 utworzenie nowego u\u017cytkownika Home Assistant i przyznanie dost\u0119pu do urz\u0105dze\u0144, kt\u00f3rymi chcesz sterowa\u0107 z Home Assistant. Mo\u017cliwe jest r\u00f3wnie\u017c u\u017cycie zdalnego dost\u0119pu za po\u015brednictwem find.z-wave.me, aby po\u0142\u0105czy\u0107 si\u0119 ze zdalnym Z-Way. Wpisz wss://find.z-wave.me w polu IP i skopiuj token z zakresem globalnym (w tym celu zaloguj si\u0119 do Z-Way przez find.z-wave.me)." + "description": "Wprowad\u017a adres IP z portem i tokenem dost\u0119pu serwera Z-Way. Aby uzyska\u0107 token, przejd\u017a do interfejsu u\u017cytkownika Z-Way Smart Home UI > Menu > Ustawienia > U\u017cytkownicy > Administrator > Token API. \n\nPrzyk\u0142ad po\u0142\u0105czenia z Z-Way w sieci lokalnej:\nURL: {local_url}\nToken: {local_token} \n\nPrzyk\u0142ad po\u0142\u0105czenia z Z-Way przez zdalny dost\u0119p find.z-wave.me:\nURL: {find_url}\nToken: {find_token} \n\nPrzyk\u0142ad po\u0142\u0105czenia z Z-Way za pomoc\u0105 statycznego publicznego adresu IP:\nURL: {remote_url}\nToken: {local_token} \n\n\u0141\u0105cz\u0105c si\u0119 przez find.z-wave.me, musisz u\u017cy\u0107 tokena o zasi\u0119gu globalnym (w tym celu zaloguj si\u0119 do Z-Way przez find.z-wave.me)." } } } diff --git a/homeassistant/components/zwave_me/translations/pt-BR.json b/homeassistant/components/zwave_me/translations/pt-BR.json index 4a7be43ddb7..23bdcd4ef9a 100644 --- a/homeassistant/components/zwave_me/translations/pt-BR.json +++ b/homeassistant/components/zwave_me/translations/pt-BR.json @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "token": "Token", + "token": "Token da API", "url": "URL" }, - "description": "Insira o endere\u00e7o IP do servidor Z-Way e o token de acesso Z-Way. O endere\u00e7o IP pode ser prefixado com wss:// e o HTTPS deve ser usado em vez de HTTP. Para obter o token, v\u00e1 para a interface do usu\u00e1rio Z-Way > Menu > Configura\u00e7\u00f5es > Usu\u00e1rio > Token de API. Sugere-se criar um novo usu\u00e1rio para o Home Assistant e conceder acesso aos dispositivos que voc\u00ea quer controlar no Home Assistant. Tamb\u00e9m \u00e9 poss\u00edvel usar o acesso remoto via find.z-wave.me para conectar um Z-Way remoto. Insira wss://find.z-wave.me no campo IP e copie o token com escopo Global (fa\u00e7a login no Z-Way via find.z-wave.me para isso)." + "description": "Insira o endere\u00e7o IP com porta e token de acesso do servidor Z-Way. Para obter o token, v\u00e1 para a interface de usu\u00e1rio Z-Way Smart Home UI > Menu > Settings > Users > Administrator > API token. \n\n Exemplo de conex\u00e3o ao Z-Way na rede local:\n URL: {local_url}\n Token: {local_token} \n\n Exemplo de conex\u00e3o ao Z-Way via acesso remoto find.z-wave.me:\n URL: {find_url}\n Token: {find_token} \n\n Exemplo de conex\u00e3o ao Z-Way com um endere\u00e7o IP p\u00fablico est\u00e1tico:\n URL: {remote_url}\n Token: {local_token} \n\n Ao conectar via find.z-wave.me, voc\u00ea precisa usar um token com escopo global (fa\u00e7a login no Z-Way via find.z-wave.me para isso)." } } } diff --git a/homeassistant/components/zwave_me/translations/ru.json b/homeassistant/components/zwave_me/translations/ru.json index d24b2f7327a..f42a7fd20ca 100644 --- a/homeassistant/components/zwave_me/translations/ru.json +++ b/homeassistant/components/zwave_me/translations/ru.json @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "token": "\u0422\u043e\u043a\u0435\u043d", + "token": "\u0422\u043e\u043a\u0435\u043d API", "url": "URL-\u0430\u0434\u0440\u0435\u0441" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Z-Way \u0438 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 Z-Way. \u0415\u0441\u043b\u0438 \u0432\u043c\u0435\u0441\u0442\u043e HTTP \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c HTTPS, IP-\u0430\u0434\u0440\u0435\u0441 \u043d\u0443\u0436\u043d\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0441 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c 'wss://'. \u0427\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 Z-Way > \u041c\u0435\u043d\u044e > \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 > \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c > \u0422\u043e\u043a\u0435\u043d API. \u0417\u0430\u0442\u0435\u043c \u043d\u0443\u0436\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0434\u043b\u044f Home Assistant \u0438 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u043c\u0438 \u0412\u044b \u0445\u043e\u0442\u0435\u043b\u0438 \u0431\u044b \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0438\u0437 Home Assistant. \u0422\u0430\u043a\u0436\u0435 \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u0434\u043e\u0441\u0442\u0443\u043f \u0447\u0435\u0440\u0435\u0437 find.z-wave.me \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e Z-Way. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 wss://find.z-wave.me \u0432 \u043f\u043e\u043b\u0435 IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0441 \u0433\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u044c\u044e \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f (\u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0432\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 Z-Way \u0447\u0435\u0440\u0435\u0437 find.z-wave.me)." + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441, \u043f\u043e\u0440\u0442 \u0438 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Z-Way. \u0427\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 Z-Way Smart Home UI > \u041c\u0435\u043d\u044e > \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 > \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 > \u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440 > \u0422\u043e\u043a\u0435\u043d API. \n\n\u041f\u0440\u0438\u043c\u0435\u0440 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a Z-Way \u0432 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438:\nURL-\u0430\u0434\u0440\u0435\u0441: {local_url}\n\u0422\u043e\u043a\u0435\u043d: {local_token} \n\n\u041f\u0440\u0438\u043c\u0435\u0440 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a Z-Way \u0447\u0435\u0440\u0435\u0437 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0439 \u0434\u043e\u0441\u0442\u0443\u043f find.z-wave.me:\nURL-\u0430\u0434\u0440\u0435\u0441: {find_url}\n\u0422\u043e\u043a\u0435\u043d: {find_token} \n\n\u041f\u0440\u0438\u043c\u0435\u0440 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a Z-Way \u0441\u043e \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u043c \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u043c IP-\u0430\u0434\u0440\u0435\u0441\u043e\u043c:\nURL-\u0430\u0434\u0440\u0435\u0441: {remote_url}\n\u0422\u043e\u043a\u0435\u043d: {local_token} \n\n\u041f\u0440\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u0447\u0435\u0440\u0435\u0437 find.z-wave.me \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0442\u043e\u043a\u0435\u043d \u0441 \u0433\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u044c\u044e \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f (\u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c \u0432 Z-Way \u0447\u0435\u0440\u0435\u0437 find.z-wave.me)." } } } diff --git a/homeassistant/components/zwave_me/translations/tr.json b/homeassistant/components/zwave_me/translations/tr.json index 39d25423fab..90edc6bf4ff 100644 --- a/homeassistant/components/zwave_me/translations/tr.json +++ b/homeassistant/components/zwave_me/translations/tr.json @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "token": "Anahtar", + "token": "API Anahtar\u0131", "url": "URL" }, - "description": "Z-Way sunucusunun ve Z-Way eri\u015fim belirtecinin IP adresini girin. HTTP yerine HTTPS kullan\u0131lmas\u0131 gerekiyorsa, IP adresinin \u00f6n\u00fcne wss:// eklenebilir. Belirteci almak i\u00e7in Z-Way kullan\u0131c\u0131 aray\u00fcz\u00fc > Men\u00fc > Ayarlar > Kullan\u0131c\u0131 > API belirtecine gidin. Home Assistant i\u00e7in yeni bir kullan\u0131c\u0131 olu\u015fturman\u0131z ve Home Assistant'tan kontrol etmeniz gereken cihazlara eri\u015fim izni vermeniz \u00f6nerilir. Uzak bir Z-Way'i ba\u011flamak i\u00e7in find.z-wave.me arac\u0131l\u0131\u011f\u0131yla uzaktan eri\u015fimi kullanmak da m\u00fcmk\u00fcnd\u00fcr. IP alan\u0131na wss://find.z-wave.me yaz\u0131n ve belirteci Global kapsamla kopyalay\u0131n (bunun i\u00e7in find.z-wave.me arac\u0131l\u0131\u011f\u0131yla Z-Way'de oturum a\u00e7\u0131n)." + "description": "Z-Way sunucusunun ba\u011flant\u0131 noktas\u0131 ve eri\u015fim anahtar\u0131 ile IP adresini girin. Simgeyi almak i\u00e7in Z-Way kullan\u0131c\u0131 aray\u00fcz\u00fc Smart Home UI > Men\u00fc > Ayarlar > Kullan\u0131c\u0131lar > Y\u00f6netici > API anahtar\u0131na gidin. \n\n Yerel a\u011fda Z-Way'e ba\u011flanma \u00f6rne\u011fi:\n URL: {local_url}\n Belirte\u00e7: {local_token} \n\n Z-Way'e uzaktan eri\u015fim find.z-wave.me arac\u0131l\u0131\u011f\u0131yla ba\u011flanma \u00f6rne\u011fi:\n URL: {find_url}\n Belirte\u00e7: {find_token} \n\n Statik bir genel IP adresiyle Z-Way'e ba\u011flanma \u00f6rne\u011fi:\n URL: {remote_url}\n Belirte\u00e7: {local_token} \n\n find.z-wave.me arac\u0131l\u0131\u011f\u0131yla ba\u011flan\u0131rken, k\u00fcresel kapsaml\u0131 bir anahtar kullanman\u0131z gerekir (bunun i\u00e7in Z-Way'de find.z-wave.me arac\u0131l\u0131\u011f\u0131yla oturum a\u00e7\u0131n)." } } } diff --git a/homeassistant/components/zwave_me/translations/zh-Hant.json b/homeassistant/components/zwave_me/translations/zh-Hant.json index ee6eb1e9ae7..9c1178b4063 100644 --- a/homeassistant/components/zwave_me/translations/zh-Hant.json +++ b/homeassistant/components/zwave_me/translations/zh-Hant.json @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "token": "\u6b0a\u6756", + "token": "API \u6b0a\u6756", "url": "\u7db2\u5740" }, - "description": "\u8f38\u5165 Z-Way \u4f3a\u670d\u5668 IP \u4f4d\u5740\u8207 Z-Way \u5b58\u53d6\u6b0a\u6756\u3002\u5047\u5982\u4f7f\u7528 HTTPS \u800c\u975e HTTP\u3001IP \u4f4d\u5740\u524d\u7db4\u53ef\u80fd\u70ba wss://\u3002\u6b32\u53d6\u5f97\u6b0a\u6756\u3001\u8acb\u81f3 Z-Way \u4f7f\u7528\u8005\u4ecb\u9762 > \u9078\u55ae > \u8a2d\u5b9a > \u4f7f\u7528\u8005 > API \u6b0a\u6756\u7372\u5f97\u3002\u5efa\u8b70\u91dd\u5c0d Home Assistant \u65b0\u5275\u4f7f\u7528\u8005\u4e26\u7531 Home Assistant \u63a7\u5236\u53ef\u4ee5\u5b58\u53d6\u7684\u88dd\u7f6e\u3002\u53e6\u5916\u4e5f\u53ef\u4ee5\u900f\u904e find.z-wave.me \u9023\u7dda\u81f3\u9060\u7aef Z-Way \u4e26\u7372\u5f97\u9060\u7aef\u5b58\u53d6\u529f\u80fd\u3002\u65bc IP \u6b04\u4f4d\u8f38\u5165 wss://find.z-wave.me \u4e26\u7531 Global scope\uff08\u900f\u904e find.z-wave.me \u767b\u5165 Z-Way\uff09\u8907\u88fd\u6b0a\u6756\u3002" + "description": "\u8f38\u5165 Z-Way \u4f3a\u670d\u5668 IP \u4f4d\u5740\u901a\u4fe1\u57e0\u8207\u5b58\u53d6\u6b0a\u6756\u3002\u6b32\u53d6\u5f97\u6b0a\u6756\u3001\u8acb\u81f3 Z-Way \u4f7f\u7528\u8005 Smart Home \u4ecb\u9762 > \u9078\u55ae > \u8a2d\u5b9a > \u4f7f\u7528\u8005 > \u7ba1\u7406\u8005 > API \u6b0a\u6756\u7372\u5f97\u3002\n\n\u4ee5\u672c\u5730\u7aef\u9023\u7dda\u81f3 Z-Way \u7bc4\u4f8b\uff1a\nURL\uff1a{local_url}\n\u6b0a\u6756\uff1a{local_token}\n\n\u900f\u904e\u9060\u7aef\u5b58\u53d6 find.z-wave.me \u9023\u7dda\u81f3 Z-Way \u7bc4\u4f8b\uff1a\nURL\uff1a{find_url}\n\u6b0a\u6756\uff1a{find_token}\n\n\u4ee5\u975c\u614b\u516c\u773e IP \u4f4d\u5740\u9023\u7dda\u81f3 Z-Way \u7bc4\u4f8b\uff1a\nURL\uff1a{remote_url}\n\u6b0a\u6756\uff1a{local_token}\n\n\u7576\u900f\u904e find.z-wave.me \u9023\u7dda\u6642\u3001\u5c07\u9700\u8981\u4f7f\u7528\u4e00\u7d44\u5168\u7bc4\u570d\u91d1\u9470\uff08\u900f\u904e find.z-wave.me \u767b\u5165\u81f3 Z-Way \u4ee5\u53d6\u5f97\uff09\u3002" } } } From af0d61fb8dc254911a3d2929cdc9f0f95f10a9ee Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Thu, 28 Apr 2022 15:35:43 -0400 Subject: [PATCH 0016/3516] Insteon Device Control Panel (#70834) Co-authored-by: Paulus Schoutsen --- homeassistant/components/insteon/__init__.py | 37 +- .../components/insteon/api/__init__.py | 45 +- .../components/insteon/api/device.py | 66 ++- .../components/insteon/api/properties.py | 340 ++++--------- homeassistant/components/insteon/climate.py | 2 +- homeassistant/components/insteon/const.py | 2 + .../components/insteon/insteon_entity.py | 2 +- homeassistant/components/insteon/light.py | 2 +- .../components/insteon/manifest.json | 8 +- homeassistant/components/insteon/schemas.py | 2 + homeassistant/components/insteon/utils.py | 7 +- requirements_all.txt | 5 +- requirements_test_all.txt | 5 +- .../insteon/fixtures/iolinc_properties.json | 18 + tests/components/insteon/mock_devices.py | 38 +- tests/components/insteon/test_api_device.py | 51 +- .../components/insteon/test_api_properties.py | 467 ++++++++++-------- tests/components/insteon/test_config_flow.py | 50 +- tests/components/insteon/test_init.py | 22 + 19 files changed, 650 insertions(+), 519 deletions(-) create mode 100644 tests/components/insteon/fixtures/iolinc_properties.json diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 0f17e1231e4..15181cb827c 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -9,11 +9,13 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PLATFORM, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import ConfigType from . import api from .const import ( CONF_CAT, + CONF_DEV_PATH, CONF_DIM_STEPS, CONF_HOUSECODE, CONF_OVERRIDE, @@ -74,13 +76,19 @@ async def close_insteon_connection(*args): async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Insteon platform.""" + hass.data[DOMAIN] = {} if DOMAIN not in config: return True - conf = config[DOMAIN] + conf = dict(config[DOMAIN]) + hass.data[DOMAIN][CONF_DEV_PATH] = conf.pop(CONF_DEV_PATH, None) + + if not conf: + return True + data, options = convert_yaml_to_config_flow(conf) + if options: - hass.data[DOMAIN] = {} hass.data[DOMAIN][OPTIONS] = options # Create a config entry with the connection data hass.async_create_task( @@ -154,23 +162,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: platforms = get_device_platforms(device) if ON_OFF_EVENTS in platforms: add_on_off_event_device(hass, device) + create_insteon_device(hass, device, entry.entry_id) _LOGGER.debug("Insteon device count: %s", len(devices)) register_new_device_callback(hass) async_register_services(hass) - device_registry = await hass.helpers.device_registry.async_get_registry() - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - identifiers={(DOMAIN, str(devices.modem.address))}, - manufacturer="Smart Home", - name=f"{devices.modem.description} {devices.modem.address}", - model=f"{devices.modem.model} ({devices.modem.cat!r}, 0x{devices.modem.subcat:02x})", - sw_version=f"{devices.modem.firmware:02x} Engine Version: {devices.modem.engine_version}", - ) + create_insteon_device(hass, devices.modem, entry.entry_id) api.async_load_api(hass) + await api.async_register_insteon_frontend(hass) asyncio.create_task(async_get_device_config(hass, entry)) return True + + +def create_insteon_device(hass, device, config_entry_id): + """Create an Insteon device.""" + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=config_entry_id, # entry.entry_id, + identifiers={(DOMAIN, str(device.address))}, + manufacturer="SmartLabs, Inc", + name=f"{device.description} {device.address}", + model=f"{device.model} ({device.cat!r}, 0x{device.subcat:02x})", + sw_version=f"{device.firmware:02x} Engine Version: {device.engine_version}", + ) diff --git a/homeassistant/components/insteon/api/__init__.py b/homeassistant/components/insteon/api/__init__.py index 3b786a38343..71dd1a0463e 100644 --- a/homeassistant/components/insteon/api/__init__.py +++ b/homeassistant/components/insteon/api/__init__.py @@ -1,7 +1,10 @@ """Insteon API interface for the frontend.""" -from homeassistant.components import websocket_api -from homeassistant.core import callback +from insteon_frontend import get_build_id, locate_dir + +from homeassistant.components import panel_custom, websocket_api +from homeassistant.components.insteon.const import CONF_DEV_PATH, DOMAIN +from homeassistant.core import HomeAssistant, callback from .aldb import ( websocket_add_default_links, @@ -13,7 +16,11 @@ from .aldb import ( websocket_reset_aldb, websocket_write_aldb, ) -from .device import websocket_get_device +from .device import ( + websocket_add_device, + websocket_cancel_add_device, + websocket_get_device, +) from .properties import ( websocket_change_properties_record, websocket_get_properties, @@ -22,11 +29,15 @@ from .properties import ( websocket_write_properties, ) +URL_BASE = "/insteon_static" + @callback def async_load_api(hass): """Set up the web socket API.""" websocket_api.async_register_command(hass, websocket_get_device) + websocket_api.async_register_command(hass, websocket_add_device) + websocket_api.async_register_command(hass, websocket_cancel_add_device) websocket_api.async_register_command(hass, websocket_get_aldb) websocket_api.async_register_command(hass, websocket_change_aldb_record) @@ -42,3 +53,31 @@ def async_load_api(hass): websocket_api.async_register_command(hass, websocket_write_properties) websocket_api.async_register_command(hass, websocket_load_properties) websocket_api.async_register_command(hass, websocket_reset_properties) + + +def get_entrypoint(is_dev): + """Get the entry point for the frontend.""" + if is_dev: + return "entrypoint.js" + + +async def async_register_insteon_frontend(hass: HomeAssistant): + """Register the Insteon frontend configuration panel.""" + # Add to sidepanel if needed + if DOMAIN not in hass.data.get("frontend_panels", {}): + dev_path = hass.data.get(DOMAIN, {}).get(CONF_DEV_PATH) + is_dev = dev_path is not None + path = dev_path if dev_path else locate_dir() + build_id = get_build_id(is_dev) + hass.http.register_static_path(URL_BASE, path, cache_headers=not is_dev) + + await panel_custom.async_register_panel( + hass=hass, + frontend_url_path=DOMAIN, + webcomponent_name="insteon-frontend", + sidebar_title=DOMAIN.capitalize(), + sidebar_icon="mdi:power", + module_url=f"{URL_BASE}/entrypoint-{build_id}.js", + embed_iframe=True, + require_admin=True, + ) diff --git a/homeassistant/components/insteon/api/device.py b/homeassistant/components/insteon/api/device.py index 1071451876b..beef78394fa 100644 --- a/homeassistant/components/insteon/api/device.py +++ b/homeassistant/components/insteon/api/device.py @@ -1,17 +1,20 @@ """API interface to get an Insteon device.""" from pyinsteon import devices +from pyinsteon.constants import DeviceAction import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from ..const import ( + DEVICE_ADDRESS, DEVICE_ID, DOMAIN, HA_DEVICE_NOT_FOUND, ID, INSTEON_DEVICE_NOT_FOUND, + MULTIPLE, TYPE, ) @@ -21,6 +24,12 @@ def compute_device_name(ha_device): return ha_device.name_by_user if ha_device.name_by_user else ha_device.name +async def async_add_devices(address, multiple): + """Add one or more Insteon devices.""" + async for _ in devices.async_add_device(address=address, multiple=multiple): + pass + + def get_insteon_device_from_ha_device(ha_device): """Return the Insteon device from an HA device.""" for identifier in ha_device.identifiers: @@ -74,3 +83,58 @@ async def websocket_get_device( "aldb_status": str(device.aldb.status), } connection.send_result(msg[ID], device_info) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "insteon/device/add", + vol.Required(MULTIPLE): bool, + vol.Optional(DEVICE_ADDRESS): str, + } +) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_add_device( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Add one or more Insteon devices.""" + + @callback + def linking_complete(address: str, action: DeviceAction): + """Forward device events to websocket.""" + if action == DeviceAction.COMPLETED: + forward_data = {"type": "linking_stopped", "address": ""} + else: + return + connection.send_message(websocket_api.event_message(msg["id"], forward_data)) + + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + devices.unsubscribe(linking_complete) + + connection.subscriptions[msg["id"]] = async_cleanup + devices.subscribe(linking_complete) + + async for address in devices.async_add_device( + address=msg.get(DEVICE_ADDRESS), multiple=msg[MULTIPLE] + ): + forward_data = {"type": "device_added", "address": str(address)} + connection.send_message(websocket_api.event_message(msg["id"], forward_data)) + + connection.send_result(msg[ID]) + + +@websocket_api.websocket_command({vol.Required(TYPE): "insteon/device/add/cancel"}) +@websocket_api.require_admin +@websocket_api.async_response +async def websocket_cancel_add_device( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: + """Cancel the Insteon all-linking process.""" + await devices.async_cancel_all_linking() + connection.send_result(msg[ID]) diff --git a/homeassistant/components/insteon/api/properties.py b/homeassistant/components/insteon/api/properties.py index 6ec12a5fd89..47def71c1ab 100644 --- a/homeassistant/components/insteon/api/properties.py +++ b/homeassistant/components/insteon/api/properties.py @@ -1,17 +1,15 @@ """Property update methods and schemas.""" -from itertools import chain from pyinsteon import devices -from pyinsteon.constants import RAMP_RATES, ResponseStatus -from pyinsteon.device_types.device_base import Device -from pyinsteon.extended_property import ( - NON_TOGGLE_MASK, - NON_TOGGLE_ON_OFF_MASK, - OFF_MASK, - ON_MASK, - RAMP_RATE, +from pyinsteon.config import RADIO_BUTTON_GROUPS, RAMP_RATE_IN_SEC, get_usable_value +from pyinsteon.constants import ( + RAMP_RATES_SEC, + PropertyType, + RelayMode, + ResponseStatus, + ToggleMode, ) -from pyinsteon.utils import ramp_rate_to_seconds, seconds_to_ramp_rate +from pyinsteon.device_types.device_base import Device import voluptuous as vol import voluptuous_serialize @@ -29,19 +27,12 @@ from ..const import ( ) from .device import notify_device_not_found -TOGGLE_ON_OFF_MODE = "toggle_on_off_mode" -NON_TOGGLE_ON_MODE = "non_toggle_on_mode" -NON_TOGGLE_OFF_MODE = "non_toggle_off_mode" -RADIO_BUTTON_GROUP_PROP = "radio_button_group_" -TOGGLE_PROP = "toggle_" -RAMP_RATE_SECONDS = list(dict.fromkeys(RAMP_RATES.values())) +SHOW_ADVANCED = "show_advanced" +RAMP_RATE_SECONDS = list(dict.fromkeys(RAMP_RATES_SEC)) RAMP_RATE_SECONDS.sort() -TOGGLE_MODES = {TOGGLE_ON_OFF_MODE: 0, NON_TOGGLE_ON_MODE: 1, NON_TOGGLE_OFF_MODE: 2} -TOGGLE_MODES_SCHEMA = { - 0: TOGGLE_ON_OFF_MODE, - 1: NON_TOGGLE_ON_MODE, - 2: NON_TOGGLE_OFF_MODE, -} +RAMP_RATE_LIST = [str(seconds) for seconds in RAMP_RATE_SECONDS] +TOGGLE_MODES = [str(ToggleMode(v)).lower() for v in list(ToggleMode)] +RELAY_MODES = [str(RelayMode(v)).lower() for v in list(RelayMode)] def _bool_schema(name): @@ -52,239 +43,116 @@ def _byte_schema(name): return voluptuous_serialize.convert(vol.Schema({vol.Required(name): cv.byte}))[0] -def _ramp_rate_schema(name): +def _float_schema(name): + return voluptuous_serialize.convert(vol.Schema({vol.Required(name): float}))[0] + + +def _list_schema(name, values): return voluptuous_serialize.convert( - vol.Schema({vol.Required(name): vol.In(RAMP_RATE_SECONDS)}), + vol.Schema({vol.Required(name): vol.In(values)}), custom_serializer=cv.custom_serializer, )[0] -def get_properties(device: Device): +def _multi_select_schema(name, values): + return voluptuous_serialize.convert( + vol.Schema({vol.Optional(name): cv.multi_select(values)}), + custom_serializer=cv.custom_serializer, + )[0] + + +def _read_only_schema(name, value): + """Return a constant value schema.""" + return voluptuous_serialize.convert(vol.Schema({vol.Required(name): value}))[0] + + +def get_schema(prop, name, groups): + """Return the correct shema type.""" + if prop.is_read_only: + return _read_only_schema(name, prop.value) + if name == RAMP_RATE_IN_SEC: + return _list_schema(name, RAMP_RATE_LIST) + if name == RADIO_BUTTON_GROUPS: + button_list = {str(group): groups[group].name for group in groups if group != 1} + return _multi_select_schema(name, button_list) + if prop.value_type == bool: + return _bool_schema(name) + if prop.value_type == int: + return _byte_schema(name) + if prop.value_type == float: + return _float_schema(name) + if prop.value_type == ToggleMode: + return _list_schema(name, TOGGLE_MODES) + if prop.value_type == RelayMode: + return _list_schema(name, RELAY_MODES) + return None + + +def get_properties(device: Device, show_advanced=False): """Get the properties of an Insteon device and return the records and schema.""" properties = [] schema = {} - # Limit the properties we manage at this time. - for prop_name in device.operating_flags: - if not device.operating_flags[prop_name].is_read_only: - prop_dict, schema_dict = _get_property(device.operating_flags[prop_name]) - properties.append(prop_dict) - schema[prop_name] = schema_dict - - mask_found = False - for prop_name in device.properties: - if device.properties[prop_name].is_read_only: + for name, prop in device.configuration.items(): + if prop.is_read_only and not show_advanced: continue - if prop_name == RAMP_RATE: - rr_prop, rr_schema = _get_ramp_rate_property(device.properties[prop_name]) - properties.append(rr_prop) - schema[RAMP_RATE] = rr_schema + prop_schema = get_schema(prop, name, device.groups) + if name == "momentary_delay": + print(prop_schema) + if prop_schema is None: + continue + schema[name] = prop_schema + properties.append(property_to_dict(prop)) - elif not mask_found and "mask" in prop_name: - mask_found = True - toggle_props, toggle_schema = _get_toggle_properties(device) - properties.extend(toggle_props) - schema.update(toggle_schema) - - rb_props, rb_schema = _get_radio_button_properties(device) - properties.extend(rb_props) - schema.update(rb_schema) - else: - prop_dict, schema_dict = _get_property(device.properties[prop_name]) - properties.append(prop_dict) - schema[prop_name] = schema_dict + if show_advanced: + for name, prop in device.operating_flags.items(): + if prop.property_type != PropertyType.ADVANCED: + continue + prop_schema = get_schema(prop, name, device.groups) + if prop_schema is not None: + schema[name] = prop_schema + properties.append(property_to_dict(prop)) + for name, prop in device.properties.items(): + if prop.property_type != PropertyType.ADVANCED: + continue + prop_schema = get_schema(prop, name, device.groups) + if prop_schema is not None: + schema[name] = prop_schema + properties.append(property_to_dict(prop)) return properties, schema -def set_property(device, prop_name: str, value): - """Update a property value.""" - if isinstance(value, bool) and prop_name in device.operating_flags: - device.operating_flags[prop_name].new_value = value - - elif prop_name == RAMP_RATE: - device.properties[prop_name].new_value = seconds_to_ramp_rate(value) - - elif prop_name.startswith(RADIO_BUTTON_GROUP_PROP): - buttons = [int(button) for button in value] - rb_groups = _calc_radio_button_groups(device) - curr_group = int(prop_name[len(RADIO_BUTTON_GROUP_PROP) :]) - if len(rb_groups) > curr_group: - removed = [btn for btn in rb_groups[curr_group] if btn not in buttons] - if removed: - device.clear_radio_buttons(removed) - if buttons: - device.set_radio_buttons(buttons) - - elif prop_name.startswith(TOGGLE_PROP): - button_name = prop_name[len(TOGGLE_PROP) :] - for button in device.groups: - if device.groups[button].name == button_name: - device.set_toggle_mode(button, int(value)) - - else: - device.properties[prop_name].new_value = value - - -def _get_property(prop): +def property_to_dict(prop): """Return a property data row.""" - value, modified = _get_usable_value(prop) + value = get_usable_value(prop) + modified = value == prop.new_value + if prop.value_type in [ToggleMode, RelayMode] or prop.name == RAMP_RATE_IN_SEC: + value = str(value).lower() prop_dict = {"name": prop.name, "value": value, "modified": modified} - if isinstance(prop.value, bool): - schema = _bool_schema(prop.name) + return prop_dict + + +def update_property(device, prop_name, value): + """Update the value of a device property.""" + prop = device.configuration[prop_name] + if prop.value_type == ToggleMode: + toggle_mode = getattr(ToggleMode, value.upper()) + prop.new_value = toggle_mode + elif prop.value_type == RelayMode: + relay_mode = getattr(RelayMode, value.upper()) + prop.new_value = relay_mode else: - schema = _byte_schema(prop.name) - return prop_dict, {"name": prop.name, **schema} - - -def _get_toggle_properties(device): - """Generate the mask properties for a KPL device.""" - props = [] - schema = {} - toggle_prop = device.properties[NON_TOGGLE_MASK] - toggle_on_prop = device.properties[NON_TOGGLE_ON_OFF_MASK] - for button in device.groups: - name = f"{TOGGLE_PROP}{device.groups[button].name}" - value, modified = _toggle_button_value(toggle_prop, toggle_on_prop, button) - props.append({"name": name, "value": value, "modified": modified}) - toggle_schema = vol.Schema({vol.Required(name): vol.In(TOGGLE_MODES_SCHEMA)}) - toggle_schema_dict = voluptuous_serialize.convert( - toggle_schema, custom_serializer=cv.custom_serializer - ) - schema[name] = toggle_schema_dict[0] - return props, schema - - -def _toggle_button_value(non_toggle_prop, toggle_on_prop, button): - """Determine the toggle value of a button.""" - toggle_mask, toggle_modified = _get_usable_value(non_toggle_prop) - toggle_on_mask, toggle_on_modified = _get_usable_value(toggle_on_prop) - - bit = button - 1 - if not toggle_mask & 1 << bit: - value = 0 - else: - if toggle_on_mask & 1 << bit: - value = 1 - else: - value = 2 - - modified = False - if toggle_modified: - curr_bit = non_toggle_prop.value & 1 << bit - new_bit = non_toggle_prop.new_value & 1 << bit - modified = not curr_bit == new_bit - - if not modified and value != 0 and toggle_on_modified: - curr_bit = toggle_on_prop.value & 1 << bit - new_bit = toggle_on_prop.new_value & 1 << bit - modified = not curr_bit == new_bit - - return value, modified - - -def _get_radio_button_properties(device): - """Return the values and schema to set KPL buttons as radio buttons.""" - rb_groups = _calc_radio_button_groups(device) - props = [] - schema = {} - index = 0 - remaining_buttons = [] - - buttons_in_groups = list(chain.from_iterable(rb_groups)) - - # Identify buttons not belonging to any group - for button in device.groups: - if button not in buttons_in_groups: - remaining_buttons.append(button) - - for rb_group in rb_groups: - name = f"{RADIO_BUTTON_GROUP_PROP}{index}" - button_1 = rb_group[0] - button_str = f"_{button_1}" if button_1 != 1 else "" - on_mask = device.properties[f"{ON_MASK}{button_str}"] - off_mask = device.properties[f"{OFF_MASK}{button_str}"] - modified = on_mask.is_dirty or off_mask.is_dirty - - props.append( - { - "name": name, - "modified": modified, - "value": rb_group, - } - ) - - options = { - button: device.groups[button].name - for button in chain.from_iterable([rb_group, remaining_buttons]) - } - rb_schema = vol.Schema({vol.Optional(name): cv.multi_select(options)}) - - rb_schema_dict = voluptuous_serialize.convert( - rb_schema, custom_serializer=cv.custom_serializer - ) - schema[name] = rb_schema_dict[0] - - index += 1 - - if len(remaining_buttons) > 1: - name = f"{RADIO_BUTTON_GROUP_PROP}{index}" - - props.append( - { - "name": name, - "modified": False, - "value": [], - } - ) - - options = {button: device.groups[button].name for button in remaining_buttons} - rb_schema = vol.Schema({vol.Optional(name): cv.multi_select(options)}) - - rb_schema_dict = voluptuous_serialize.convert( - rb_schema, custom_serializer=cv.custom_serializer - ) - schema[name] = rb_schema_dict[0] - - return props, schema - - -def _calc_radio_button_groups(device): - """Return existing radio button groups.""" - rb_groups = [] - for button in device.groups: - if button not in list(chain.from_iterable(rb_groups)): - button_str = "" if button == 1 else f"_{button}" - on_mask, _ = _get_usable_value(device.properties[f"{ON_MASK}{button_str}"]) - if on_mask != 0: - rb_group = [button] - for bit in list(range(0, button - 1)) + list(range(button, 8)): - if on_mask & 1 << bit: - rb_group.append(bit + 1) - if len(rb_group) > 1: - rb_groups.append(rb_group) - return rb_groups - - -def _get_ramp_rate_property(prop): - """Return the value and schema of a ramp rate property.""" - rr_prop, _ = _get_property(prop) - rr_prop["value"] = ramp_rate_to_seconds(rr_prop["value"]) - return rr_prop, _ramp_rate_schema(prop.name) - - -def _get_usable_value(prop): - """Return the current or the modified value of a property.""" - value = prop.value if prop.new_value is None else prop.new_value - return value, prop.is_dirty + prop.new_value = value @websocket_api.websocket_command( { vol.Required(TYPE): "insteon/properties/get", vol.Required(DEVICE_ADDRESS): str, + vol.Required(SHOW_ADVANCED): bool, } ) @websocket_api.require_admin @@ -299,7 +167,7 @@ async def websocket_get_properties( notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return - properties, schema = get_properties(device) + properties, schema = get_properties(device, msg[SHOW_ADVANCED]) connection.send_result(msg[ID], {"properties": properties, "schema": schema}) @@ -324,7 +192,7 @@ async def websocket_change_properties_record( notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return - set_property(device, msg[PROPERTY_NAME], msg[PROPERTY_VALUE]) + update_property(device, msg[PROPERTY_NAME], msg[PROPERTY_VALUE]) connection.send_result(msg[ID]) @@ -346,10 +214,9 @@ async def websocket_write_properties( notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return - result1 = await device.async_write_op_flags() - result2 = await device.async_write_ext_properties() + result = await device.async_write_config() await devices.async_save(workdir=hass.config.config_dir) - if result1 != ResponseStatus.SUCCESS or result2 != ResponseStatus.SUCCESS: + if result != ResponseStatus.SUCCESS: connection.send_message( websocket_api.error_message( msg[ID], "write_failed", "properties not written to device" @@ -377,10 +244,9 @@ async def websocket_load_properties( notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return - result1 = await device.async_read_op_flags() - result2 = await device.async_read_ext_properties() + result, _ = await device.async_read_config(read_aldb=False) await devices.async_save(workdir=hass.config.config_dir) - if result1 != ResponseStatus.SUCCESS or result2 != ResponseStatus.SUCCESS: + if result != ResponseStatus.SUCCESS: connection.send_message( websocket_api.error_message( msg[ID], "load_failed", "properties not loaded from device" diff --git a/homeassistant/components/insteon/climate.py b/homeassistant/components/insteon/climate.py index e393c6eea0a..833180583e2 100644 --- a/homeassistant/components/insteon/climate.py +++ b/homeassistant/components/insteon/climate.py @@ -1,8 +1,8 @@ """Support for Insteon thermostat.""" from __future__ import annotations +from pyinsteon.config import CELSIUS from pyinsteon.constants import ThermostatMode -from pyinsteon.operating_flag import CELSIUS from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py index bc3eaf6234b..fb7b2387d73 100644 --- a/homeassistant/components/insteon/const.py +++ b/homeassistant/components/insteon/const.py @@ -70,6 +70,7 @@ CONF_DIM_STEPS = "dim_steps" CONF_X10_ALL_UNITS_OFF = "x10_all_units_off" CONF_X10_ALL_LIGHTS_ON = "x10_all_lights_on" CONF_X10_ALL_LIGHTS_OFF = "x10_all_lights_off" +CONF_DEV_PATH = "dev_path" PORT_HUB_V1 = 9761 PORT_HUB_V2 = 25105 @@ -172,5 +173,6 @@ PROPERTY_NAME = "name" PROPERTY_VALUE = "value" HA_DEVICE_NOT_FOUND = "ha_device_not_found" INSTEON_DEVICE_NOT_FOUND = "insteon_device_not_found" +MULTIPLE = "multiple" INSTEON_ADDR_REGEX = re.compile(r"([A-Fa-f0-9]{2}\.?[A-Fa-f0-9]{2}\.?[A-Fa-f0-9]{2})$") diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index d8fd9b2cbc9..60935f3f951 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -85,7 +85,7 @@ class InsteonEntity(Entity): """Return device information.""" return DeviceInfo( identifiers={(DOMAIN, str(self._insteon_device.address))}, - manufacturer="Smart Home", + manufacturer="SmartLabs, Inc", 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}", diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py index 0b1fd2270e8..05ad9794042 100644 --- a/homeassistant/components/insteon/light.py +++ b/homeassistant/components/insteon/light.py @@ -1,5 +1,5 @@ """Support for Insteon lights via PowerLinc Modem.""" -from pyinsteon.extended_property import ON_LEVEL +from pyinsteon.config import ON_LEVEL from homeassistant.components.light import ( ATTR_BRIGHTNESS, diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index d9e1f1bfb18..ad5736f4d53 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -2,13 +2,17 @@ "domain": "insteon", "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", - "requirements": ["pyinsteon==1.0.13"], + "dependencies": ["http", "websocket_api"], + "requirements": [ + "pyinsteon==1.1.0b1", + "insteon-frontend-home-assistant==0.1.0" + ], "codeowners": ["@teharris1"], "dhcp": [{ "macaddress": "000EF3*" }, { "registered_devices": true }], "config_flow": true, "iot_class": "local_push", "loggers": ["pyinsteon", "pypubsub"], - "after_dependencies": ["usb"], + "after_dependencies": ["panel_custom", "usb"], "usb": [ { "vid": "10BF" diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py index 09315919052..6bcde545e34 100644 --- a/homeassistant/components/insteon/schemas.py +++ b/homeassistant/components/insteon/schemas.py @@ -22,6 +22,7 @@ import homeassistant.helpers.config_validation as cv from .const import ( CONF_CAT, + CONF_DEV_PATH, CONF_DIM_STEPS, CONF_FIRMWARE, CONF_HOUSECODE, @@ -121,6 +122,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_X10): vol.All( cv.ensure_list_csv, [CONF_X10_SCHEMA] ), + vol.Optional(CONF_DEV_PATH): cv.string, }, extra=vol.ALLOW_EXTRA, required=True, diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py index 1599975f462..03647559345 100644 --- a/homeassistant/components/insteon/utils.py +++ b/homeassistant/components/insteon/utils.py @@ -4,7 +4,7 @@ import logging from pyinsteon import devices from pyinsteon.address import Address -from pyinsteon.constants import ALDBStatus +from pyinsteon.constants import ALDBStatus, DeviceAction from pyinsteon.events import OFF_EVENT, OFF_FAST_EVENT, ON_EVENT, ON_FAST_EVENT from pyinsteon.managers.link_manager import ( async_enter_linking_mode, @@ -137,9 +137,10 @@ def register_new_device_callback(hass): """Register callback for new Insteon device.""" @callback - def async_new_insteon_device(address=None): + def async_new_insteon_device(address, action: DeviceAction): """Detect device from transport to be delegated to platform.""" - hass.async_create_task(async_create_new_entities(address)) + if action == DeviceAction.ADDED: + hass.async_create_task(async_create_new_entities(address)) async def async_create_new_entities(address): _LOGGER.debug( diff --git a/requirements_all.txt b/requirements_all.txt index df74fa12364..e950616865e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -881,6 +881,9 @@ influxdb-client==1.24.0 # homeassistant.components.influxdb influxdb==5.3.1 +# homeassistant.components.insteon +insteon-frontend-home-assistant==0.1.0 + # homeassistant.components.intellifire intellifire4py==1.0.2 @@ -1544,7 +1547,7 @@ pyialarm==1.9.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.0.13 +pyinsteon==1.1.0b1 # homeassistant.components.intesishome pyintesishome==1.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec628688ad0..ccc958e2543 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -618,6 +618,9 @@ influxdb-client==1.24.0 # homeassistant.components.influxdb influxdb==5.3.1 +# homeassistant.components.insteon +insteon-frontend-home-assistant==0.1.0 + # homeassistant.components.intellifire intellifire4py==1.0.2 @@ -1026,7 +1029,7 @@ pyialarm==1.9.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.0.13 +pyinsteon==1.1.0b1 # homeassistant.components.ipma pyipma==2.0.5 diff --git a/tests/components/insteon/fixtures/iolinc_properties.json b/tests/components/insteon/fixtures/iolinc_properties.json new file mode 100644 index 00000000000..904ba054b7a --- /dev/null +++ b/tests/components/insteon/fixtures/iolinc_properties.json @@ -0,0 +1,18 @@ +{ + "operating_flags": { + "program_lock_on": false, + "blink_on_tx_on": true, + "relay_on_sense_on": false, + "momentary_on": true, + "momentary_on_off_trigger": false, + "x10_off": true, + "sense_sends_off": true, + "momentary_follow_sense": false + }, + "properties": { + "prescaler": 1, + "delay": 50, + "x10_house": 32, + "x10_unit": 0 + } +} diff --git a/tests/components/insteon/mock_devices.py b/tests/components/insteon/mock_devices.py index e28e25bf41b..6e6a8eccfcc 100644 --- a/tests/components/insteon/mock_devices.py +++ b/tests/components/insteon/mock_devices.py @@ -1,15 +1,20 @@ """Mock devices object to test Insteon.""" + +import asyncio from unittest.mock import AsyncMock, MagicMock from pyinsteon.address import Address from pyinsteon.constants import ALDBStatus, ResponseStatus from pyinsteon.device_types import ( DimmableLightingControl_KeypadLinc_8, - GeneralController, + GeneralController_RemoteLinc, Hub, + SensorsActuators_IOLink, SwitchedLightingControl_SwitchLinc, ) from pyinsteon.managers.saved_devices_manager import dict_to_aldb_record +from pyinsteon.topics import DEVICE_LIST_CHANGED +from pyinsteon.utils import subscribe_topic class MockSwitchLinc(SwitchedLightingControl_SwitchLinc): @@ -31,7 +36,10 @@ class MockDevices: self._connected = connected self.async_save = AsyncMock() self.add_x10_device = MagicMock() + self.async_read_config = AsyncMock() self.set_id = MagicMock() + self.async_add_device_called_with = {} + self.async_cancel_all_linking = AsyncMock() def __getitem__(self, address): """Return a a device from the device address.""" @@ -56,18 +64,24 @@ class MockDevices: addr1 = Address("11.11.11") addr2 = Address("22.22.22") addr3 = Address("33.33.33") + addr4 = Address("44.44.44") self._devices[addr0] = Hub(addr0, 0x03, 0x00, 0x00, "Hub AA.AA.AA", "0") self._devices[addr1] = MockSwitchLinc( addr1, 0x02, 0x00, 0x00, "Device 11.11.11", "1" ) - self._devices[addr2] = GeneralController( + self._devices[addr2] = GeneralController_RemoteLinc( addr2, 0x00, 0x00, 0x00, "Device 22.22.22", "2" ) self._devices[addr3] = DimmableLightingControl_KeypadLinc_8( addr3, 0x02, 0x00, 0x00, "Device 33.33.33", "3" ) + self._devices[addr4] = SensorsActuators_IOLink( + addr4, 0x07, 0x00, 0x00, "Device 44.44.44", "4" + ) - for device in [self._devices[addr] for addr in [addr1, addr2, addr3]]: + for device in [ + self._devices[addr] for addr in [addr1, addr2, addr3, addr4] + ]: device.async_read_config = AsyncMock() device.aldb.async_write = AsyncMock() device.aldb.async_load = AsyncMock() @@ -85,7 +99,7 @@ class MockDevices: return_value=ResponseStatus.SUCCESS ) - for device in [self._devices[addr] for addr in [addr2, addr3]]: + for device in [self._devices[addr] for addr in [addr2, addr3, addr4]]: device.async_status = AsyncMock() self._devices[addr1].async_status = AsyncMock(side_effect=AttributeError) self._devices[addr0].aldb.async_load = AsyncMock() @@ -104,6 +118,7 @@ class MockDevices: ) self.modem = self._devices[addr0] + self.modem.async_read_config = AsyncMock() def fill_aldb(self, address, records): """Fill the All-Link Database for a device.""" @@ -126,3 +141,18 @@ class MockDevices: value = properties[flag] if device.properties.get(flag): device.properties[flag].load(value) + + async def async_add_device(self, address=None, multiple=False): + """Mock the async_add_device method.""" + self.async_add_device_called_with = {"address": address, "multiple": multiple} + if multiple: + yield "aa.bb.cc" + await asyncio.sleep(0.01) + yield "bb.cc.dd" + if address: + yield address + await asyncio.sleep(0.01) + + def subscribe(self, listener): + """Mock the subscribe function.""" + subscribe_topic(listener, DEVICE_LIST_CHANGED) diff --git a/tests/components/insteon/test_api_device.py b/tests/components/insteon/test_api_device.py index 528d44cc691..49588c6ea8f 100644 --- a/tests/components/insteon/test_api_device.py +++ b/tests/components/insteon/test_api_device.py @@ -1,6 +1,11 @@ """Test the device level APIs.""" +import asyncio from unittest.mock import patch +from pyinsteon.constants import DeviceAction +from pyinsteon.topics import DEVICE_LIST_CHANGED +from pyinsteon.utils import publish_topic + from homeassistant.components import insteon from homeassistant.components.insteon.api import async_load_api from homeassistant.components.insteon.api.device import ( @@ -11,7 +16,7 @@ from homeassistant.components.insteon.api.device import ( TYPE, async_device_name, ) -from homeassistant.components.insteon.const import DOMAIN +from homeassistant.components.insteon.const import DOMAIN, MULTIPLE from homeassistant.helpers.device_registry import async_get_registry from .const import MOCK_USER_INPUT_PLM @@ -137,3 +142,47 @@ async def test_get_ha_device_name(hass, hass_ws_client): # Test no HA or Insteon device name = await async_device_name(device_reg, "BB.BB.BB") assert name == "" + + +async def test_add_device_api(hass, hass_ws_client): + """Test adding an Insteon device.""" + + ws_client, devices, _, _ = await _async_setup(hass, hass_ws_client) + with patch.object(insteon.api.device, "devices", devices): + await ws_client.send_json({ID: 2, TYPE: "insteon/device/add", MULTIPLE: True}) + + await asyncio.sleep(0.01) + assert devices.async_add_device_called_with.get("address") is None + assert devices.async_add_device_called_with["multiple"] is True + + msg = await ws_client.receive_json() + assert msg["event"]["type"] == "device_added" + assert msg["event"]["address"] == "aa.bb.cc" + + msg = await ws_client.receive_json() + assert msg["event"]["type"] == "device_added" + assert msg["event"]["address"] == "bb.cc.dd" + + publish_topic( + DEVICE_LIST_CHANGED, + address=None, + action=DeviceAction.COMPLETED, + ) + msg = await ws_client.receive_json() + assert msg["event"]["type"] == "linking_stopped" + + +async def test_cancel_add_device(hass, hass_ws_client): + """Test cancelling adding of a new device.""" + + ws_client, devices, _, _ = await _async_setup(hass, hass_ws_client) + + with patch.object(insteon.api.aldb, "devices", devices): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/device/add/cancel", + } + ) + msg = await ws_client.receive_json() + assert msg["success"] diff --git a/tests/components/insteon/test_api_properties.py b/tests/components/insteon/test_api_properties.py index 683e687ec85..7211402e343 100644 --- a/tests/components/insteon/test_api_properties.py +++ b/tests/components/insteon/test_api_properties.py @@ -1,8 +1,11 @@ """Test the Insteon properties APIs.""" import json -from unittest.mock import patch +from unittest.mock import AsyncMock, patch +from pyinsteon.config import MOMENTARY_DELAY, RELAY_MODE, TOGGLE_BUTTON +from pyinsteon.config.extended_property import ExtendedProperty +from pyinsteon.constants import RelayMode, ToggleMode import pytest from homeassistant.components import insteon @@ -11,19 +14,12 @@ from homeassistant.components.insteon.api.device import INSTEON_DEVICE_NOT_FOUND from homeassistant.components.insteon.api.properties import ( DEVICE_ADDRESS, ID, - NON_TOGGLE_MASK, - NON_TOGGLE_OFF_MODE, - NON_TOGGLE_ON_MODE, - NON_TOGGLE_ON_OFF_MASK, PROPERTY_NAME, PROPERTY_VALUE, - RADIO_BUTTON_GROUP_PROP, - TOGGLE_MODES, - TOGGLE_ON_OFF_MODE, - TOGGLE_PROP, + RADIO_BUTTON_GROUPS, + RAMP_RATE_IN_SEC, + SHOW_ADVANCED, TYPE, - _get_radio_button_properties, - _get_toggle_properties, ) from .mock_devices import MockDevices @@ -31,43 +27,172 @@ from .mock_devices import MockDevices from tests.common import load_fixture -@pytest.fixture(name="properties_data", scope="session") -def aldb_data_fixture(): +@pytest.fixture(name="kpl_properties_data", scope="session") +def kpl_properties_data_fixture(): """Load the controller state fixture data.""" return json.loads(load_fixture("insteon/kpl_properties.json")) -async def _setup(hass, hass_ws_client, properties_data): +@pytest.fixture(name="iolinc_properties_data", scope="session") +def iolinc_properties_data_fixture(): + """Load the controller state fixture data.""" + return json.loads(load_fixture("insteon/iolinc_properties.json")) + + +async def _setup(hass, hass_ws_client, address, properties_data): """Set up tests.""" ws_client = await hass_ws_client(hass) devices = MockDevices() await devices.async_load() - devices.fill_properties("33.33.33", properties_data) + devices.fill_properties(address, properties_data) async_load_api(hass) return ws_client, devices -async def test_get_properties(hass, hass_ws_client, properties_data): +async def test_get_properties( + hass, hass_ws_client, kpl_properties_data, iolinc_properties_data +): """Test getting an Insteon device's properties.""" - ws_client, devices = await _setup(hass, hass_ws_client, properties_data) - - with patch.object(insteon.api.properties, "devices", devices): - await ws_client.send_json( - {ID: 2, TYPE: "insteon/properties/get", DEVICE_ADDRESS: "33.33.33"} - ) - msg = await ws_client.receive_json() - assert msg["success"] - assert len(msg["result"]["properties"]) == 54 - - -async def test_change_operating_flag(hass, hass_ws_client, properties_data): - """Test changing an Insteon device's properties.""" - ws_client, devices = await _setup(hass, hass_ws_client, properties_data) + ws_client, devices = await _setup( + hass, hass_ws_client, "33.33.33", kpl_properties_data + ) + devices.fill_properties("44.44.44", iolinc_properties_data) with patch.object(insteon.api.properties, "devices", devices): await ws_client.send_json( { ID: 2, + TYPE: "insteon/properties/get", + DEVICE_ADDRESS: "33.33.33", + SHOW_ADVANCED: False, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["properties"]) == 18 + + await ws_client.send_json( + { + ID: 3, + TYPE: "insteon/properties/get", + DEVICE_ADDRESS: "44.44.44", + SHOW_ADVANCED: False, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["properties"]) == 6 + + await ws_client.send_json( + { + ID: 4, + TYPE: "insteon/properties/get", + DEVICE_ADDRESS: "33.33.33", + SHOW_ADVANCED: True, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["properties"]) == 69 + + await ws_client.send_json( + { + ID: 5, + TYPE: "insteon/properties/get", + DEVICE_ADDRESS: "44.44.44", + SHOW_ADVANCED: True, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["properties"]) == 14 + + +async def test_get_read_only_properties(hass, hass_ws_client, iolinc_properties_data): + """Test getting an Insteon device's properties.""" + mock_read_only = ExtendedProperty( + "44.44.44", "mock_read_only", bool, is_read_only=True + ) + mock_read_only.load(False) + + ws_client, devices = await _setup( + hass, hass_ws_client, "44.44.44", iolinc_properties_data + ) + device = devices["44.44.44"] + device.configuration["mock_read_only"] = mock_read_only + with patch.object(insteon.api.properties, "devices", devices): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/properties/get", + DEVICE_ADDRESS: "44.44.44", + SHOW_ADVANCED: False, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["properties"]) == 6 + await ws_client.send_json( + { + ID: 3, + TYPE: "insteon/properties/get", + DEVICE_ADDRESS: "44.44.44", + SHOW_ADVANCED: True, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["properties"]) == 15 + + +async def test_get_unknown_properties(hass, hass_ws_client, iolinc_properties_data): + """Test getting an Insteon device's properties.""" + + class UnknownType: + """Mock unknown data type.""" + + mock_unknown = ExtendedProperty("44.44.44", "mock_unknown", UnknownType) + + ws_client, devices = await _setup( + hass, hass_ws_client, "44.44.44", iolinc_properties_data + ) + device = devices["44.44.44"] + device.configuration["mock_unknown"] = mock_unknown + with patch.object(insteon.api.properties, "devices", devices): + await ws_client.send_json( + { + ID: 2, + TYPE: "insteon/properties/get", + DEVICE_ADDRESS: "44.44.44", + SHOW_ADVANCED: False, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["properties"]) == 6 + await ws_client.send_json( + { + ID: 3, + TYPE: "insteon/properties/get", + DEVICE_ADDRESS: "44.44.44", + SHOW_ADVANCED: True, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert len(msg["result"]["properties"]) == 14 + + +async def test_change_bool_property(hass, hass_ws_client, kpl_properties_data): + """Test changing a bool type properties.""" + ws_client, devices = await _setup( + hass, hass_ws_client, "33.33.33", kpl_properties_data + ) + + with patch.object(insteon.api.properties, "devices", devices): + await ws_client.send_json( + { + ID: 3, TYPE: "insteon/properties/change", DEVICE_ADDRESS: "33.33.33", PROPERTY_NAME: "led_off", @@ -79,29 +204,33 @@ async def test_change_operating_flag(hass, hass_ws_client, properties_data): assert devices["33.33.33"].operating_flags["led_off"].is_dirty -async def test_change_property(hass, hass_ws_client, properties_data): - """Test changing an Insteon device's properties.""" - ws_client, devices = await _setup(hass, hass_ws_client, properties_data) +async def test_change_int_property(hass, hass_ws_client, kpl_properties_data): + """Test changing a int type properties.""" + ws_client, devices = await _setup( + hass, hass_ws_client, "33.33.33", kpl_properties_data + ) with patch.object(insteon.api.properties, "devices", devices): await ws_client.send_json( { - ID: 2, + ID: 4, TYPE: "insteon/properties/change", DEVICE_ADDRESS: "33.33.33", - PROPERTY_NAME: "on_mask", + PROPERTY_NAME: "led_dimming", PROPERTY_VALUE: 100, } ) msg = await ws_client.receive_json() assert msg["success"] - assert devices["33.33.33"].properties["on_mask"].new_value == 100 - assert devices["33.33.33"].properties["on_mask"].is_dirty + assert devices["33.33.33"].properties["led_dimming"].new_value == 100 + assert devices["33.33.33"].properties["led_dimming"].is_dirty -async def test_change_ramp_rate_property(hass, hass_ws_client, properties_data): - """Test changing an Insteon device's properties.""" - ws_client, devices = await _setup(hass, hass_ws_client, properties_data) +async def test_change_ramp_rate_property(hass, hass_ws_client, kpl_properties_data): + """Test changing an Insteon device's ramp rate properties.""" + ws_client, devices = await _setup( + hass, hass_ws_client, "33.33.33", kpl_properties_data + ) with patch.object(insteon.api.properties, "devices", devices): await ws_client.send_json( @@ -109,7 +238,7 @@ async def test_change_ramp_rate_property(hass, hass_ws_client, properties_data): ID: 2, TYPE: "insteon/properties/change", DEVICE_ADDRESS: "33.33.33", - PROPERTY_NAME: "ramp_rate", + PROPERTY_NAME: RAMP_RATE_IN_SEC, PROPERTY_VALUE: 4.5, } ) @@ -119,208 +248,126 @@ async def test_change_ramp_rate_property(hass, hass_ws_client, properties_data): assert devices["33.33.33"].properties["ramp_rate"].is_dirty -async def test_change_radio_button_group(hass, hass_ws_client, properties_data): +async def test_change_radio_button_group(hass, hass_ws_client, kpl_properties_data): """Test changing an Insteon device's properties.""" - ws_client, devices = await _setup(hass, hass_ws_client, properties_data) - rb_props, schema = _get_radio_button_properties(devices["33.33.33"]) + ws_client, devices = await _setup( + hass, hass_ws_client, "33.33.33", kpl_properties_data + ) + rb_groups = devices["33.33.33"].configuration[RADIO_BUTTON_GROUPS] # Make sure the baseline is correct - assert rb_props[0]["name"] == f"{RADIO_BUTTON_GROUP_PROP}0" - assert rb_props[0]["value"] == [4, 5] - assert rb_props[1]["value"] == [7, 8] - assert rb_props[2]["value"] == [] - assert schema[f"{RADIO_BUTTON_GROUP_PROP}0"]["options"].get(1) - assert schema[f"{RADIO_BUTTON_GROUP_PROP}1"]["options"].get(1) - assert devices["33.33.33"].properties["on_mask"].value == 0 - assert devices["33.33.33"].properties["off_mask"].value == 0 - assert not devices["33.33.33"].properties["on_mask"].is_dirty - assert not devices["33.33.33"].properties["off_mask"].is_dirty + assert rb_groups.value[0] == [4, 5] + assert rb_groups.value[1] == [7, 8] # Add button 1 to the group - rb_props[0]["value"].append(1) + new_groups_1 = [[1, 4, 5], [7, 8]] with patch.object(insteon.api.properties, "devices", devices): await ws_client.send_json( { ID: 2, TYPE: "insteon/properties/change", DEVICE_ADDRESS: "33.33.33", - PROPERTY_NAME: f"{RADIO_BUTTON_GROUP_PROP}0", - PROPERTY_VALUE: rb_props[0]["value"], + PROPERTY_NAME: RADIO_BUTTON_GROUPS, + PROPERTY_VALUE: new_groups_1, } ) msg = await ws_client.receive_json() assert msg["success"] + assert rb_groups.new_value[0] == [1, 4, 5] + assert rb_groups.new_value[1] == [7, 8] - new_rb_props, _ = _get_radio_button_properties(devices["33.33.33"]) - assert 1 in new_rb_props[0]["value"] - assert 4 in new_rb_props[0]["value"] - assert 5 in new_rb_props[0]["value"] - assert schema[f"{RADIO_BUTTON_GROUP_PROP}0"]["options"].get(1) - assert schema[f"{RADIO_BUTTON_GROUP_PROP}1"]["options"].get(1) - - assert devices["33.33.33"].properties["on_mask"].new_value == 0x18 - assert devices["33.33.33"].properties["off_mask"].new_value == 0x18 - assert devices["33.33.33"].properties["on_mask"].is_dirty - assert devices["33.33.33"].properties["off_mask"].is_dirty - - # Remove button 5 - rb_props[0]["value"].remove(5) + new_groups_2 = [[1, 4], [7, 8]] await ws_client.send_json( { ID: 3, TYPE: "insteon/properties/change", DEVICE_ADDRESS: "33.33.33", - PROPERTY_NAME: f"{RADIO_BUTTON_GROUP_PROP}0", - PROPERTY_VALUE: rb_props[0]["value"], + PROPERTY_NAME: RADIO_BUTTON_GROUPS, + PROPERTY_VALUE: new_groups_2, } ) msg = await ws_client.receive_json() assert msg["success"] - - new_rb_props, _ = _get_radio_button_properties(devices["33.33.33"]) - assert 1 in new_rb_props[0]["value"] - assert 4 in new_rb_props[0]["value"] - assert 5 not in new_rb_props[0]["value"] - assert schema[f"{RADIO_BUTTON_GROUP_PROP}0"]["options"].get(1) - assert schema[f"{RADIO_BUTTON_GROUP_PROP}1"]["options"].get(1) - - assert devices["33.33.33"].properties["on_mask"].new_value == 0x08 - assert devices["33.33.33"].properties["off_mask"].new_value == 0x08 - assert devices["33.33.33"].properties["on_mask"].is_dirty - assert devices["33.33.33"].properties["off_mask"].is_dirty - - # Remove button group 1 - rb_props[1]["value"] = [] - await ws_client.send_json( - { - ID: 5, - TYPE: "insteon/properties/change", - DEVICE_ADDRESS: "33.33.33", - PROPERTY_NAME: f"{RADIO_BUTTON_GROUP_PROP}1", - PROPERTY_VALUE: rb_props[1]["value"], - } - ) - msg = await ws_client.receive_json() - assert msg["success"] - - new_rb_props, _ = _get_radio_button_properties(devices["33.33.33"]) - assert len(new_rb_props) == 2 - assert new_rb_props[0]["value"] == [1, 4] - assert new_rb_props[1]["value"] == [] + assert rb_groups.new_value[0] == [1, 4] + assert rb_groups.new_value[1] == [7, 8] -async def test_create_radio_button_group(hass, hass_ws_client, properties_data): - """Test changing an Insteon device's properties.""" - ws_client, devices = await _setup(hass, hass_ws_client, properties_data) - rb_props, _ = _get_radio_button_properties(devices["33.33.33"]) - - # Make sure the baseline is correct - assert len(rb_props) == 3 - - rb_props[0]["value"].append("1") - - with patch.object(insteon.api.properties, "devices", devices): - await ws_client.send_json( - { - ID: 2, - TYPE: "insteon/properties/change", - DEVICE_ADDRESS: "33.33.33", - PROPERTY_NAME: f"{RADIO_BUTTON_GROUP_PROP}2", - PROPERTY_VALUE: ["1", "3"], - } - ) - msg = await ws_client.receive_json() - assert msg["success"] - - new_rb_props, new_schema = _get_radio_button_properties(devices["33.33.33"]) - assert len(new_rb_props) == 4 - assert 1 in new_rb_props[0]["value"] - assert new_schema[f"{RADIO_BUTTON_GROUP_PROP}0"]["options"].get(1) - assert not new_schema[f"{RADIO_BUTTON_GROUP_PROP}1"]["options"].get(1) - - assert devices["33.33.33"].properties["on_mask"].new_value == 4 - assert devices["33.33.33"].properties["off_mask"].new_value == 4 - assert devices["33.33.33"].properties["on_mask"].is_dirty - assert devices["33.33.33"].properties["off_mask"].is_dirty - - -async def test_change_toggle_property(hass, hass_ws_client, properties_data): +async def test_change_toggle_property(hass, hass_ws_client, kpl_properties_data): """Update a button's toggle mode.""" - ws_client, devices = await _setup(hass, hass_ws_client, properties_data) + ws_client, devices = await _setup( + hass, hass_ws_client, "33.33.33", kpl_properties_data + ) device = devices["33.33.33"] - toggle_props, _ = _get_toggle_properties(devices["33.33.33"]) - - # Make sure the baseline is correct - assert toggle_props[0]["name"] == f"{TOGGLE_PROP}{device.groups[1].name}" - assert toggle_props[0]["value"] == TOGGLE_MODES[TOGGLE_ON_OFF_MODE] - assert toggle_props[1]["value"] == TOGGLE_MODES[NON_TOGGLE_ON_MODE] - assert device.properties[NON_TOGGLE_MASK].value == 2 - assert device.properties[NON_TOGGLE_ON_OFF_MASK].value == 2 - assert not device.properties[NON_TOGGLE_MASK].is_dirty - assert not device.properties[NON_TOGGLE_ON_OFF_MASK].is_dirty - + prop_name = f"{TOGGLE_BUTTON}_c" + toggle_prop = device.configuration[prop_name] + assert toggle_prop.value == ToggleMode.TOGGLE with patch.object(insteon.api.properties, "devices", devices): await ws_client.send_json( { ID: 2, TYPE: "insteon/properties/change", DEVICE_ADDRESS: "33.33.33", - PROPERTY_NAME: toggle_props[0]["name"], - PROPERTY_VALUE: 1, + PROPERTY_NAME: prop_name, + PROPERTY_VALUE: str(ToggleMode.ON_ONLY).lower(), } ) msg = await ws_client.receive_json() assert msg["success"] + assert toggle_prop.new_value == ToggleMode.ON_ONLY - new_toggle_props, _ = _get_toggle_properties(devices["33.33.33"]) - assert new_toggle_props[0]["value"] == TOGGLE_MODES[NON_TOGGLE_ON_MODE] - assert device.properties[NON_TOGGLE_MASK].new_value == 3 - assert device.properties[NON_TOGGLE_ON_OFF_MASK].new_value == 3 - assert device.properties[NON_TOGGLE_MASK].is_dirty - assert device.properties[NON_TOGGLE_ON_OFF_MASK].is_dirty +async def test_change_relay_mode(hass, hass_ws_client, iolinc_properties_data): + """Update a device's relay mode.""" + ws_client, devices = await _setup( + hass, hass_ws_client, "44.44.44", iolinc_properties_data + ) + device = devices["44.44.44"] + relay_prop = device.configuration[RELAY_MODE] + assert relay_prop.value == RelayMode.MOMENTARY_A + with patch.object(insteon.api.properties, "devices", devices): await ws_client.send_json( { - ID: 3, + ID: 2, TYPE: "insteon/properties/change", - DEVICE_ADDRESS: "33.33.33", - PROPERTY_NAME: toggle_props[0]["name"], - PROPERTY_VALUE: 2, + DEVICE_ADDRESS: "44.44.44", + PROPERTY_NAME: RELAY_MODE, + PROPERTY_VALUE: str(RelayMode.LATCHING).lower(), } ) msg = await ws_client.receive_json() assert msg["success"] + assert relay_prop.new_value == RelayMode.LATCHING - new_toggle_props, _ = _get_toggle_properties(devices["33.33.33"]) - assert new_toggle_props[0]["value"] == TOGGLE_MODES[NON_TOGGLE_OFF_MODE] - assert device.properties[NON_TOGGLE_MASK].new_value == 3 - assert device.properties[NON_TOGGLE_ON_OFF_MASK].new_value is None - assert device.properties[NON_TOGGLE_MASK].is_dirty - assert not device.properties[NON_TOGGLE_ON_OFF_MASK].is_dirty +async def test_change_float_property(hass, hass_ws_client, iolinc_properties_data): + """Update a float type property.""" + ws_client, devices = await _setup( + hass, hass_ws_client, "44.44.44", iolinc_properties_data + ) + device = devices["44.44.44"] + delay_prop = device.configuration[MOMENTARY_DELAY] + delay_prop.load(0) + with patch.object(insteon.api.properties, "devices", devices): await ws_client.send_json( { - ID: 4, + ID: 2, TYPE: "insteon/properties/change", - DEVICE_ADDRESS: "33.33.33", - PROPERTY_NAME: toggle_props[1]["name"], - PROPERTY_VALUE: 0, + DEVICE_ADDRESS: "44.44.44", + PROPERTY_NAME: MOMENTARY_DELAY, + PROPERTY_VALUE: 1.8, } ) msg = await ws_client.receive_json() assert msg["success"] - new_toggle_props, _ = _get_toggle_properties(devices["33.33.33"]) - assert new_toggle_props[1]["value"] == TOGGLE_MODES[TOGGLE_ON_OFF_MODE] - assert device.properties[NON_TOGGLE_MASK].new_value == 1 - assert device.properties[NON_TOGGLE_ON_OFF_MASK].new_value == 0 - assert device.properties[NON_TOGGLE_MASK].is_dirty - assert device.properties[NON_TOGGLE_ON_OFF_MASK].is_dirty + assert delay_prop.new_value == 1.8 -async def test_write_properties(hass, hass_ws_client, properties_data): +async def test_write_properties(hass, hass_ws_client, kpl_properties_data): """Test getting an Insteon device's properties.""" - ws_client, devices = await _setup(hass, hass_ws_client, properties_data) + ws_client, devices = await _setup( + hass, hass_ws_client, "33.33.33", kpl_properties_data + ) with patch.object(insteon.api.properties, "devices", devices): await ws_client.send_json( @@ -332,9 +379,11 @@ async def test_write_properties(hass, hass_ws_client, properties_data): assert devices["33.33.33"].async_write_ext_properties.call_count == 1 -async def test_write_properties_failure(hass, hass_ws_client, properties_data): +async def test_write_properties_failure(hass, hass_ws_client, kpl_properties_data): """Test getting an Insteon device's properties.""" - ws_client, devices = await _setup(hass, hass_ws_client, properties_data) + ws_client, devices = await _setup( + hass, hass_ws_client, "33.33.33", kpl_properties_data + ) with patch.object(insteon.api.properties, "devices", devices): await ws_client.send_json( @@ -345,39 +394,48 @@ async def test_write_properties_failure(hass, hass_ws_client, properties_data): assert msg["error"]["code"] == "write_failed" -async def test_load_properties(hass, hass_ws_client, properties_data): +async def test_load_properties(hass, hass_ws_client, kpl_properties_data): """Test getting an Insteon device's properties.""" - ws_client, devices = await _setup(hass, hass_ws_client, properties_data) + ws_client, devices = await _setup( + hass, hass_ws_client, "33.33.33", kpl_properties_data + ) + device = devices["33.33.33"] + device.async_read_config = AsyncMock(return_value=(1, 1)) with patch.object(insteon.api.properties, "devices", devices): await ws_client.send_json( {ID: 2, TYPE: "insteon/properties/load", DEVICE_ADDRESS: "33.33.33"} ) msg = await ws_client.receive_json() assert msg["success"] - assert devices["33.33.33"].async_read_op_flags.call_count == 1 - assert devices["33.33.33"].async_read_ext_properties.call_count == 1 + assert devices["33.33.33"].async_read_config.call_count == 1 -async def test_load_properties_failure(hass, hass_ws_client, properties_data): +async def test_load_properties_failure(hass, hass_ws_client, kpl_properties_data): """Test getting an Insteon device's properties.""" - ws_client, devices = await _setup(hass, hass_ws_client, properties_data) + ws_client, devices = await _setup( + hass, hass_ws_client, "33.33.33", kpl_properties_data + ) + device = devices["33.33.33"] + device.async_read_config = AsyncMock(return_value=(0, 0)) with patch.object(insteon.api.properties, "devices", devices): await ws_client.send_json( - {ID: 2, TYPE: "insteon/properties/load", DEVICE_ADDRESS: "22.22.22"} + {ID: 2, TYPE: "insteon/properties/load", DEVICE_ADDRESS: "33.33.33"} ) msg = await ws_client.receive_json() assert not msg["success"] assert msg["error"]["code"] == "load_failed" -async def test_reset_properties(hass, hass_ws_client, properties_data): +async def test_reset_properties(hass, hass_ws_client, kpl_properties_data): """Test getting an Insteon device's properties.""" - ws_client, devices = await _setup(hass, hass_ws_client, properties_data) + ws_client, devices = await _setup( + hass, hass_ws_client, "33.33.33", kpl_properties_data + ) device = devices["33.33.33"] - device.operating_flags["led_off"].new_value = True + device.configuration["led_off"].new_value = True device.properties["on_mask"].new_value = 100 assert device.operating_flags["led_off"].is_dirty assert device.properties["on_mask"].is_dirty @@ -391,20 +449,23 @@ async def test_reset_properties(hass, hass_ws_client, properties_data): assert not device.properties["on_mask"].is_dirty -async def test_bad_address(hass, hass_ws_client, properties_data): +async def test_bad_address(hass, hass_ws_client, kpl_properties_data): """Test for a bad Insteon address.""" - ws_client, _ = await _setup(hass, hass_ws_client, properties_data) + ws_client, devices = await _setup( + hass, hass_ws_client, "33.33.33", kpl_properties_data + ) ws_id = 0 for call in ["get", "write", "load", "reset"]: ws_id += 1 - await ws_client.send_json( - { - ID: ws_id, - TYPE: f"insteon/properties/{call}", - DEVICE_ADDRESS: "99.99.99", - } - ) + params = { + ID: ws_id, + TYPE: f"insteon/properties/{call}", + DEVICE_ADDRESS: "99.99.99", + } + if call == "get": + params[SHOW_ADVANCED] = False + await ws_client.send_json(params) msg = await ws_client.receive_json() assert not msg["success"] assert msg["error"]["message"] == INSTEON_DEVICE_NOT_FOUND diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index ce49f9df816..878b540b721 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -2,10 +2,8 @@ from unittest.mock import patch -import voluptuous_serialize - from homeassistant import config_entries, data_entry_flow -from homeassistant.components import dhcp, usb +from homeassistant.components import usb from homeassistant.components.insteon.config_flow import ( HUB1, HUB2, @@ -39,7 +37,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from .const import ( MOCK_HOSTNAME, @@ -651,48 +648,3 @@ async def test_discovery_via_usb_already_setup(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "single_instance_allowed" - - -async def test_discovery_via_dhcp_hubv1(hass): - """Test usb flow.""" - await _test_dhcp(hass, HUB1) - - -async def test_discovery_via_dhcp_hubv2(hass): - """Test usb flow.""" - await _test_dhcp(hass, HUB2) - - -async def _test_dhcp(hass, modem_type): - """Test the dhcp discovery for a moddem type.""" - discovery_info = dhcp.DhcpServiceInfo( - ip="11.22.33.44", hostname="", macaddress="00:0e:f3:aa:bb:cc" - ) - result = await hass.config_entries.flow.async_init( - "insteon", - context={"source": config_entries.SOURCE_DHCP}, - data=discovery_info, - ) - await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - with patch("homeassistant.components.insteon.config_flow.async_connect"), patch( - "homeassistant.components.insteon.async_setup_entry", return_value=True - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"modem_type": modem_type} - ) - await hass.async_block_till_done() - - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - schema = voluptuous_serialize.convert( - result2["data_schema"], - custom_serializer=cv.custom_serializer, - ) - for field in schema: - if field["name"] == "host": - assert field.get("default") == "11.22.33.44" - break diff --git a/tests/components/insteon/test_init.py b/tests/components/insteon/test_init.py index ecd3dfc5620..eb821f15cb5 100644 --- a/tests/components/insteon/test_init.py +++ b/tests/components/insteon/test_init.py @@ -7,6 +7,7 @@ from pyinsteon.address import Address from homeassistant.components import insteon from homeassistant.components.insteon.const import ( CONF_CAT, + CONF_DEV_PATH, CONF_OVERRIDE, CONF_SUBCAT, CONF_X10, @@ -222,3 +223,24 @@ async def test_setup_entry_failed_connection(hass: HomeAssistant, caplog): {}, ) assert "Could not connect to Insteon modem" in caplog.text + + +async def test_import_frontend_dev_url(hass: HomeAssistant): + """Test importing a dev_url config entry.""" + config = {} + config[DOMAIN] = {CONF_DEV_PATH: "/some/path"} + + with patch.object( + insteon, "async_connect", new=mock_successful_connection + ), patch.object(insteon, "close_insteon_connection"), patch.object( + insteon, "devices", new=MockDevices() + ), patch( + PATCH_CONNECTION, new=mock_successful_connection + ): + assert await async_setup_component( + hass, + insteon.DOMAIN, + config, + ) + await hass.async_block_till_done() + await asyncio.sleep(0.01) From 250a2aa260cf840f8cfbc45fbdf0607066aa3947 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 27 Apr 2022 11:59:05 -0500 Subject: [PATCH 0017/3516] Use standard attribute for Sonos group members (#70924) --- homeassistant/components/sonos/media_player.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 4b4dc99ad71..30fdb28b02f 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -76,8 +76,6 @@ REPEAT_TO_SONOS = { SONOS_TO_REPEAT = {meaning: mode for mode, meaning in REPEAT_TO_SONOS.items()} -ATTR_SONOS_GROUP = "sonos_group" - UPNP_ERRORS_TO_IGNORE = ["701", "711", "712"] SERVICE_JOIN = "join" @@ -265,6 +263,11 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Return the current coordinator SonosSpeaker.""" return self.speaker.coordinator or self.speaker + @property + def group_members(self) -> list[str] | None: + """List of entity_ids which are currently grouped together.""" + return self.speaker.sonos_group_entities + def __hash__(self) -> int: """Return a hash of self.""" return hash(self.unique_id) @@ -654,9 +657,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return entity specific state attributes.""" - attributes: dict[str, Any] = { - ATTR_SONOS_GROUP: self.speaker.sonos_group_entities - } + attributes: dict[str, Any] = {} if self.media.queue_position is not None: attributes[ATTR_QUEUE_POSITION] = self.media.queue_position From 7b69e20db7025c08401049db463b1aca2f749b5a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Apr 2022 22:32:13 -0700 Subject: [PATCH 0018/3516] Sync area changes to google (#70936) Co-authored-by: Martin Hjelmare --- .../components/cloud/google_config.py | 42 +++++++++++-- tests/components/cloud/test_google_config.py | 62 ++++++++++++++++++- 2 files changed, 98 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index e2b21ffc56d..8f190103e87 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -9,8 +9,8 @@ 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 -from homeassistant.core import CoreState, split_entity_id -from homeassistant.helpers import entity_registry as er, start +from homeassistant.core import CoreState, Event, callback, split_entity_id +from homeassistant.helpers import device_registry as dr, entity_registry as er, start from homeassistant.setup import async_setup_component from .const import ( @@ -103,6 +103,10 @@ class CloudGoogleConfig(AbstractConfig): er.EVENT_ENTITY_REGISTRY_UPDATED, self._handle_entity_registry_updated, ) + self.hass.bus.async_listen( + dr.EVENT_DEVICE_REGISTRY_UPDATED, + self._handle_device_registry_updated, + ) def should_expose(self, state): """If a state object should be exposed.""" @@ -217,9 +221,14 @@ class CloudGoogleConfig(AbstractConfig): self._cur_entity_prefs = prefs.google_entity_configs self._cur_default_expose = prefs.google_default_expose - async def _handle_entity_registry_updated(self, event): + @callback + def _handle_entity_registry_updated(self, event: Event) -> None: """Handle when entity registry updated.""" - if not self.enabled or not self._cloud.is_logged_in: + if ( + not self.enabled + or not self._cloud.is_logged_in + or self.hass.state != CoreState.running + ): return # Only consider entity registry updates if info relevant for Google has changed @@ -233,7 +242,30 @@ class CloudGoogleConfig(AbstractConfig): if not self._should_expose_entity_id(entity_id): return - if self.hass.state != CoreState.running: + self.async_schedule_google_sync_all() + + @callback + def _handle_device_registry_updated(self, event: Event) -> None: + """Handle when device registry updated.""" + if ( + not self.enabled + or not self._cloud.is_logged_in + or self.hass.state != CoreState.running + ): + return + + # Device registry is only used for area changes. All other changes are ignored. + if event.data["action"] != "update" or "area_id" not in event.data["changes"]: + return + + # Check if any exposed entity uses the device area + if not any( + entity_entry.area_id is None + and self._should_expose_entity_id(entity_entry.entity_id) + for entity_entry in er.async_entries_for_device( + er.async_get(self.hass), event.data["device_id"] + ) + ): return self.async_schedule_google_sync_all() diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 98674ff6c86..95746eb67ae 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -10,7 +10,7 @@ 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 from homeassistant.core import CoreState, State -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import EntityCategory from homeassistant.util.dt import utcnow @@ -191,6 +191,66 @@ async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): assert len(mock_sync.mock_calls) == 3 +async def test_google_device_registry_sync(hass, mock_cloud_login, cloud_prefs): + """Test Google config responds to device registry.""" + config = CloudGoogleConfig( + hass, GACTIONS_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + ) + ent_reg = er.async_get(hass) + entity_entry = ent_reg.async_get_or_create( + "light", "hue", "1234", device_id="1234", area_id="ABCD" + ) + + with patch.object(config, "async_sync_entities_all"): + await config.async_initialize() + await hass.async_block_till_done() + await config.async_connect_agent_user("mock-user-id") + + with patch.object(config, "async_schedule_google_sync_all") as mock_sync: + # Device registry updated with non-relevant changes + hass.bus.async_fire( + dr.EVENT_DEVICE_REGISTRY_UPDATED, + { + "action": "update", + "device_id": "1234", + "changes": ["manufacturer"], + }, + ) + await hass.async_block_till_done() + + assert len(mock_sync.mock_calls) == 0 + + # Device registry updated with relevant changes + # but entity has area ID so not impacted + hass.bus.async_fire( + dr.EVENT_DEVICE_REGISTRY_UPDATED, + { + "action": "update", + "device_id": "1234", + "changes": ["area_id"], + }, + ) + await hass.async_block_till_done() + + assert len(mock_sync.mock_calls) == 0 + + ent_reg.async_update_entity(entity_entry.entity_id, area_id=None) + + # Device registry updated with relevant changes + # but entity has area ID so not impacted + hass.bus.async_fire( + dr.EVENT_DEVICE_REGISTRY_UPDATED, + { + "action": "update", + "device_id": "1234", + "changes": ["area_id"], + }, + ) + await hass.async_block_till_done() + + assert len(mock_sync.mock_calls) == 1 + + async def test_sync_google_when_started(hass, mock_cloud_login, cloud_prefs): """Test Google config syncs on init.""" config = CloudGoogleConfig( From 8f1df6b9173d93974dacea6330ea53b72fb63345 Mon Sep 17 00:00:00 2001 From: j-a-n Date: Thu, 28 Apr 2022 10:37:23 +0200 Subject: [PATCH 0019/3516] Add unique_id attribute to Alpha2Climate entity (#70964) Co-authored-by: Franck Nijhof --- homeassistant/components/moehlenhoff_alpha2/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/moehlenhoff_alpha2/climate.py b/homeassistant/components/moehlenhoff_alpha2/climate.py index 71be199b622..225735293ca 100644 --- a/homeassistant/components/moehlenhoff_alpha2/climate.py +++ b/homeassistant/components/moehlenhoff_alpha2/climate.py @@ -50,6 +50,7 @@ class Alpha2Climate(CoordinatorEntity[Alpha2BaseCoordinator], ClimateEntity): """Initialize Alpha2 ClimateEntity.""" super().__init__(coordinator) self.heat_area_id = heat_area_id + self._attr_unique_id = heat_area_id @property def name(self) -> str: From 43b27de011fff84da80d0272ba5e5faf49ec5c6a Mon Sep 17 00:00:00 2001 From: Shai Ungar Date: Thu, 28 Apr 2022 23:25:17 +0300 Subject: [PATCH 0020/3516] Sabnzbd config flow improvments (#70981) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> Co-authored-by: Martin Hjelmare --- homeassistant/components/sabnzbd/__init__.py | 158 ++++++++++++++---- .../components/sabnzbd/config_flow.py | 10 +- homeassistant/components/sabnzbd/const.py | 5 +- homeassistant/components/sabnzbd/errors.py | 10 -- homeassistant/components/sabnzbd/sensor.py | 7 +- .../components/sabnzbd/services.yaml | 20 +++ tests/components/sabnzbd/test_config_flow.py | 59 +++---- 7 files changed, 181 insertions(+), 88 deletions(-) delete mode 100644 homeassistant/components/sabnzbd/errors.py diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index bbbfbe18bc1..aca50e404a2 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -1,24 +1,39 @@ """Support for monitoring an SABnzbd NZB client.""" +from collections.abc import Callable import logging from pysabnzbd import SabnzbdApiException import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_PATH, CONF_URL -from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryState +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_NAME, + CONF_PATH, + CONF_PORT, + CONF_SENSORS, + CONF_SSL, + CONF_URL, +) +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from .const import ( + ATTR_API_KEY, ATTR_SPEED, + DEFAULT_HOST, DEFAULT_NAME, + DEFAULT_PORT, DEFAULT_SPEED_LIMIT, + DEFAULT_SSL, DOMAIN, KEY_API, + KEY_API_DATA, KEY_NAME, SERVICE_PAUSE, SERVICE_RESUME, @@ -27,23 +42,50 @@ from .const import ( UPDATE_INTERVAL, ) from .sab import get_client +from .sensor import SENSOR_KEYS PLATFORMS = ["sensor"] _LOGGER = logging.getLogger(__name__) -SPEED_LIMIT_SCHEMA = vol.Schema( - {vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.string} +SERVICES = ( + SERVICE_PAUSE, + SERVICE_RESUME, + SERVICE_SET_SPEED, +) + +SERVICE_BASE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_API_KEY): cv.string, + } +) + +SERVICE_SPEED_SCHEMA = SERVICE_BASE_SCHEMA.extend( + { + vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.string, + } ) CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( - { - vol.Required(CONF_API_KEY): str, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, - vol.Required(CONF_URL): str, - vol.Optional(CONF_PATH): str, - } + vol.All( + cv.deprecated(CONF_HOST), + cv.deprecated(CONF_PORT), + cv.deprecated(CONF_SENSORS), + cv.deprecated(CONF_SSL), + { + vol.Required(CONF_API_KEY): str, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, + vol.Required(CONF_URL): str, + vol.Optional(CONF_PATH): str, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SENSORS): vol.All( + cv.ensure_list, [vol.In(SENSOR_KEYS)] + ), + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + }, + ) ) }, extra=vol.ALLOW_EXTRA, @@ -69,42 +111,73 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True +@callback +def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) -> str: + """Get the entry ID related to a service call (by device ID).""" + call_data_api_key = call.data[ATTR_API_KEY] + + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.data[ATTR_API_KEY] == call_data_api_key: + return entry.entry_id + + raise ValueError(f"No api for API key: {call_data_api_key}") + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the SabNzbd Component.""" sab_api = await get_client(hass, entry.data) if not sab_api: raise ConfigEntryNotReady + sab_api_data = SabnzbdApiData(sab_api) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { KEY_API: sab_api, + KEY_API_DATA: sab_api_data, KEY_NAME: entry.data[CONF_NAME], } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + @callback + def extract_api(func: Callable) -> Callable: + """Define a decorator to get the correct api for a service call.""" - sab_api_data = SabnzbdApiData(sab_api) + async def wrapper(call: ServiceCall) -> None: + """Wrap the service function.""" + entry_id = async_get_entry_id_for_service_call(hass, call) + api_data = hass.data[DOMAIN][entry_id][KEY_API_DATA] - async def async_service_handler(service: ServiceCall) -> None: - """Handle service calls.""" - if service.service == SERVICE_PAUSE: - await sab_api_data.async_pause_queue() - elif service.service == SERVICE_RESUME: - await sab_api_data.async_resume_queue() - elif service.service == SERVICE_SET_SPEED: - speed = service.data.get(ATTR_SPEED) - await sab_api_data.async_set_queue_speed(speed) + try: + await func(call, api_data) + except Exception as err: + raise HomeAssistantError( + f"Error while executing {func.__name__}: {err}" + ) from err - hass.services.async_register( - DOMAIN, SERVICE_PAUSE, async_service_handler, schema=vol.Schema({}) - ) + return wrapper - hass.services.async_register( - DOMAIN, SERVICE_RESUME, async_service_handler, schema=vol.Schema({}) - ) + @extract_api + async def async_pause_queue(call: ServiceCall, api: SabnzbdApiData) -> None: + await api.async_pause_queue() - hass.services.async_register( - DOMAIN, SERVICE_SET_SPEED, async_service_handler, schema=SPEED_LIMIT_SCHEMA - ) + @extract_api + async def async_resume_queue(call: ServiceCall, api: SabnzbdApiData) -> None: + await api.async_resume_queue() + + @extract_api + async def async_set_queue_speed(call: ServiceCall, api: SabnzbdApiData) -> None: + speed = call.data.get(ATTR_SPEED) + await api.async_set_queue_speed(speed) + + for service, method, schema in ( + (SERVICE_PAUSE, async_pause_queue, SERVICE_BASE_SCHEMA), + (SERVICE_RESUME, async_resume_queue, SERVICE_BASE_SCHEMA), + (SERVICE_SET_SPEED, async_set_queue_speed, SERVICE_SPEED_SCHEMA), + ): + + if hass.services.has_service(DOMAIN, service): + continue + + hass.services.async_register(DOMAIN, service, method, schema=schema) async def async_update_sabnzbd(now): """Refresh SABnzbd queue data.""" @@ -115,10 +188,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error(err) async_track_time_interval(hass, async_update_sabnzbd, UPDATE_INTERVAL) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a Sabnzbd config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + loaded_entries = [ + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + if len(loaded_entries) == 1: + # If this is the last loaded instance of Sabnzbd, deregister any services + # defined during integration setup: + for service_name in SERVICES: + hass.services.async_remove(DOMAIN, service_name) + + return unload_ok + + class SabnzbdApiData: """Class for storing/refreshing sabnzbd api queue data.""" diff --git a/homeassistant/components/sabnzbd/config_flow.py b/homeassistant/components/sabnzbd/config_flow.py index 914b1febefc..7930363b2ac 100644 --- a/homeassistant/components/sabnzbd/config_flow.py +++ b/homeassistant/components/sabnzbd/config_flow.py @@ -70,10 +70,8 @@ class SABnzbdConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_data): """Import sabnzbd config from configuration.yaml.""" - import_data[CONF_URL] = ( - ("https://" if import_data[CONF_SSL] else "http://") - + import_data[CONF_HOST] - + ":" - + str(import_data[CONF_PORT]) - ) + protocol = "https://" if import_data[CONF_SSL] else "http://" + import_data[ + CONF_URL + ] = f"{protocol}{import_data[CONF_HOST]}:{import_data[CONF_PORT]}" return await self.async_step_user(import_data) diff --git a/homeassistant/components/sabnzbd/const.py b/homeassistant/components/sabnzbd/const.py index 9092b877b1b..8add1f61493 100644 --- a/homeassistant/components/sabnzbd/const.py +++ b/homeassistant/components/sabnzbd/const.py @@ -5,8 +5,8 @@ DOMAIN = "sabnzbd" DATA_SABNZBD = "sabnzbd" ATTR_SPEED = "speed" -BASE_URL_FORMAT = "{}://{}:{}/" -CONFIG_FILE = "sabnzbd.conf" +ATTR_API_KEY = "api_key" + DEFAULT_HOST = "localhost" DEFAULT_NAME = "SABnzbd" DEFAULT_PORT = 8080 @@ -22,4 +22,5 @@ SERVICE_SET_SPEED = "set_speed" SIGNAL_SABNZBD_UPDATED = "sabnzbd_updated" KEY_API = "api" +KEY_API_DATA = "api_data" KEY_NAME = "name" diff --git a/homeassistant/components/sabnzbd/errors.py b/homeassistant/components/sabnzbd/errors.py deleted file mode 100644 index a14a0af4775..00000000000 --- a/homeassistant/components/sabnzbd/errors.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Errors for the Sabnzbd component.""" -from homeassistant.exceptions import HomeAssistantError - - -class AuthenticationError(HomeAssistantError): - """Wrong Username or Password.""" - - -class UnknownError(HomeAssistantError): - """Unknown Error.""" diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index 293b14a604b..1d661d90848 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -10,12 +10,12 @@ from homeassistant.components.sensor import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DOMAIN, SIGNAL_SABNZBD_UPDATED, SabnzbdApiData +from . import DOMAIN, SIGNAL_SABNZBD_UPDATED from ...config_entries import ConfigEntry from ...const import DATA_GIGABYTES, DATA_MEGABYTES, DATA_RATE_MEGABYTES_PER_SECOND from ...core import HomeAssistant from ...helpers.entity_platform import AddEntitiesCallback -from .const import KEY_API, KEY_NAME +from .const import KEY_API_DATA, KEY_NAME @dataclass @@ -109,9 +109,8 @@ async def async_setup_entry( ) -> None: """Set up a Sabnzbd sensor entry.""" - sab_api = hass.data[DOMAIN][config_entry.entry_id][KEY_API] + sab_api_data = hass.data[DOMAIN][config_entry.entry_id][KEY_API_DATA] client_name = hass.data[DOMAIN][config_entry.entry_id][KEY_NAME] - sab_api_data = SabnzbdApiData(sab_api) async_add_entities( [SabnzbdSensor(sab_api_data, client_name, sensor) for sensor in SENSOR_TYPES] diff --git a/homeassistant/components/sabnzbd/services.yaml b/homeassistant/components/sabnzbd/services.yaml index 38f68bfe5dd..2221eed169f 100644 --- a/homeassistant/components/sabnzbd/services.yaml +++ b/homeassistant/components/sabnzbd/services.yaml @@ -1,13 +1,33 @@ pause: name: Pause description: Pauses downloads. + fields: + api_key: + name: Sabnzbd API key + description: The Sabnzbd API key to pause downloads + required: true + selector: + text: resume: name: Resume description: Resumes downloads. + fields: + api_key: + name: Sabnzbd API key + description: The Sabnzbd API key to resume downloads + required: true + selector: + text: set_speed: name: Set speed description: Sets the download speed limit. fields: + api_key: + name: Sabnzbd API key + description: The Sabnzbd API key to set speed limit + required: true + selector: + text: speed: name: Speed description: Speed limit. If specified as a number with no units, will be interpreted as a percent. If units are provided (e.g., 500K) will be interpreted absolutely. diff --git a/tests/components/sabnzbd/test_config_flow.py b/tests/components/sabnzbd/test_config_flow.py index 381928457d2..d04c5b18ab1 100644 --- a/tests/components/sabnzbd/test_config_flow.py +++ b/tests/components/sabnzbd/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from pysabnzbd import SabnzbdApiException -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.sabnzbd import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import ( @@ -15,8 +15,7 @@ from homeassistant.const import ( CONF_SSL, CONF_URL, ) - -from tests.common import MockConfigEntry +from homeassistant.data_entry_flow import RESULT_TYPE_FORM VALID_CONFIG = { CONF_NAME: "Sabnzbd", @@ -37,21 +36,34 @@ VALID_CONFIG_OLD = { async def test_create_entry(hass): """Test that the user step works.""" + 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.sabnzbd.sab.SabnzbdApi.check_available", return_value=True, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=VALID_CONFIG, + ), patch( + "homeassistant.components.sabnzbd.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, ) + await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "edc3eee7330e" - assert result["data"][CONF_NAME] == "Sabnzbd" - assert result["data"][CONF_API_KEY] == "edc3eee7330e4fdda04489e3fbc283d0" - assert result["data"][CONF_PATH] == "" + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "edc3eee7330e" + assert result2["data"] == { + CONF_API_KEY: "edc3eee7330e4fdda04489e3fbc283d0", + CONF_NAME: "Sabnzbd", + CONF_PATH: "", + CONF_URL: "http://localhost:8080", + } + assert len(mock_setup_entry.mock_calls) == 1 async def test_auth_error(hass): @@ -69,27 +81,6 @@ async def test_auth_error(hass): assert result["errors"] == {"base": "cannot_connect"} -async def test_integration_already_exists(hass): - """Test we only allow a single config flow.""" - with patch( - "homeassistant.components.sabnzbd.sab.SabnzbdApi.check_available", - return_value=True, - ): - MockConfigEntry( - domain=DOMAIN, - unique_id="123456", - data=VALID_CONFIG, - ).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=VALID_CONFIG, - ) - - assert result["type"] == "create_entry" - - async def test_import_flow(hass) -> None: """Test the import configuration flow.""" with patch( From dea9ff4f18786dd8334c2f5fc6b6b08316dec6a8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 28 Apr 2022 12:45:37 -0600 Subject: [PATCH 0021/3516] Ensure SimpliSafe re-auth only looks at SimpliSafe config entries (#71009) * Ensure SimpliSafe re-auth only looks at SimpliSafe config entries * Add a test * Trigger Build * Linting * Comment * Simplify test --- .../components/simplisafe/config_flow.py | 3 ++- .../components/simplisafe/test_config_flow.py | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 93a2e152ad8..ad9614c0546 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -126,7 +126,8 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if existing_entries := [ entry for entry in self.hass.config_entries.async_entries() - if entry.unique_id in (self._username, user_id) + if entry.domain == DOMAIN + and entry.unique_id in (self._username, user_id) ]: existing_entry = existing_entries[0] self.hass.config_entries.async_update_entry( diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 91274f8f0c5..a4ef98e0e09 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -12,6 +12,8 @@ from homeassistant.const import CONF_CODE, CONF_TOKEN, CONF_USERNAME from .common import REFRESH_TOKEN, USER_ID, USERNAME +from tests.common import MockConfigEntry + CONF_USER_ID = "user_id" @@ -57,9 +59,15 @@ async def test_options_flow(hass, config_entry): @pytest.mark.parametrize("unique_id", [USERNAME, USER_ID]) async def test_step_reauth( - hass, config, config_entry, reauth_config, setup_simplisafe, sms_config + hass, config, config_entry, reauth_config, setup_simplisafe, sms_config, unique_id ): """Test the re-auth step (testing both username and user ID as unique ID).""" + # Add a second config entry (tied to a random domain, but with the same unique ID + # that could exist in a SimpliSafe entry) to ensure that this reauth process only + # touches the SimpliSafe entry: + entry = MockConfigEntry(domain="random", unique_id=USERNAME, data={"some": "data"}) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data=config ) @@ -78,11 +86,17 @@ async def test_step_reauth( assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "reauth_successful" - assert len(hass.config_entries.async_entries()) == 1 + assert len(hass.config_entries.async_entries()) == 2 + + # Test that the SimpliSafe config flow is updated: [config_entry] = hass.config_entries.async_entries(DOMAIN) assert config_entry.unique_id == USER_ID assert config_entry.data == config + # Test that the non-SimpliSafe config flow remains the same: + [config_entry] = hass.config_entries.async_entries("random") + assert config_entry == entry + @pytest.mark.parametrize( "exc,error_string", From 53181b409fc79ab2ca4ba83611b65b4b7f9fabe2 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 28 Apr 2022 14:57:26 -0400 Subject: [PATCH 0022/3516] Remove unnecessary update_before_add from ZHA (#71010) * Additional streamlining for ZHA entity init * fix tests --- homeassistant/components/zha/button.py | 1 - .../components/zha/core/discovery.py | 3 +- homeassistant/components/zha/entity.py | 1 + homeassistant/components/zha/fan.py | 1 - homeassistant/components/zha/number.py | 1 - homeassistant/components/zha/select.py | 13 +++- homeassistant/components/zha/sensor.py | 1 - homeassistant/components/zha/siren.py | 1 - tests/components/zha/test_cover.py | 8 +- tests/components/zha/test_device_tracker.py | 2 +- tests/components/zha/test_select.py | 76 +++++++++++++++++-- 11 files changed, 86 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py index abfa94f5906..f130936df02 100644 --- a/homeassistant/components/zha/button.py +++ b/homeassistant/components/zha/button.py @@ -41,7 +41,6 @@ async def async_setup_entry( discovery.async_add_entities, async_add_entities, entities_to_create, - update_before_add=False, ), ) config_entry.async_on_unload(unsub) diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 8d7d53468e2..cdad57834eb 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -54,14 +54,13 @@ async def async_add_entities( tuple[str, ZHADevice, list[base.ZigbeeChannel]], ] ], - update_before_add: bool = True, ) -> None: """Add entities helper.""" if not entities: return to_add = [ent_cls.create_entity(*args) for ent_cls, args in entities] entities_to_add = [entity for entity in to_add if entity is not None] - _async_add_entities(entities_to_add, update_before_add=update_before_add) + _async_add_entities(entities_to_add, update_before_add=False) entities.clear() diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 13e43aa9ff0..50ffb8f4fcd 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -291,6 +291,7 @@ class ZhaGroupEntity(BaseZhaEntity): async def async_added_to_hass(self) -> None: """Register callbacks.""" await super().async_added_to_hass() + await self.async_update() self.async_accept_signal( None, diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index bad1d8e94f1..1c2f52c6038 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -67,7 +67,6 @@ async def async_setup_entry( discovery.async_add_entities, async_add_entities, entities_to_create, - update_before_add=False, ), ) config_entry.async_on_unload(unsub) diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 3c5a374e588..e1191b4ece4 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -250,7 +250,6 @@ async def async_setup_entry( discovery.async_add_entities, async_add_entities, entities_to_create, - update_before_add=False, ), ) config_entry.async_on_unload(unsub) diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 0a67f1eac5f..b9237e24eef 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -3,6 +3,7 @@ from __future__ import annotations from enum import Enum import functools +import logging from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.clusters.security import IasWd @@ -30,6 +31,7 @@ from .entity import ZhaEntity CONFIG_DIAGNOSTIC_MATCH = functools.partial( ZHA_ENTITIES.config_diagnostic_match, Platform.SELECT ) +_LOGGER = logging.getLogger(__name__) async def async_setup_entry( @@ -47,7 +49,6 @@ async def async_setup_entry( discovery.async_add_entities, async_add_entities, entities_to_create, - update_before_add=False, ), ) config_entry.async_on_unload(unsub) @@ -163,7 +164,15 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity): Return entity if it is a supported configuration, otherwise return None """ channel = channels[0] - if cls._select_attr in channel.cluster.unsupported_attributes: + if ( + cls._select_attr in channel.cluster.unsupported_attributes + or channel.cluster.get(cls._select_attr) is None + ): + _LOGGER.debug( + "%s is not supported - skipping %s entity creation", + cls._select_attr, + cls.__name__, + ) return None return cls(unique_id, zha_device, channels, **kwargs) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index f150304c33d..0a5fe204648 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -103,7 +103,6 @@ async def async_setup_entry( discovery.async_add_entities, async_add_entities, entities_to_create, - update_before_add=False, ), ) config_entry.async_on_unload(unsub) diff --git a/homeassistant/components/zha/siren.py b/homeassistant/components/zha/siren.py index 587efc49c9c..38b58b8dc54 100644 --- a/homeassistant/components/zha/siren.py +++ b/homeassistant/components/zha/siren.py @@ -60,7 +60,6 @@ async def async_setup_entry( discovery.async_add_entities, async_add_entities, entities_to_create, - update_before_add=False, ), ) config_entry.async_on_unload(unsub) diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 60a4fab25be..3c00b5d3109 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -104,17 +104,14 @@ def zigpy_keen_vent(zigpy_device_mock): ) -@patch( - "homeassistant.components.zha.core.channels.closures.WindowCovering.async_initialize" -) -async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): +async def test_cover(hass, zha_device_joined_restored, zigpy_cover_device): """Test zha cover platform.""" # load up cover domain cluster = zigpy_cover_device.endpoints.get(1).window_covering cluster.PLUGGED_ATTR_READS = {"current_position_lift_percentage": 100} zha_device = await zha_device_joined_restored(zigpy_cover_device) - assert cluster.read_attributes.call_count == 2 + assert cluster.read_attributes.call_count == 1 assert "current_position_lift_percentage" in cluster.read_attributes.call_args[0][0] entity_id = await find_entity_id(Platform.COVER, zha_device, hass) @@ -193,6 +190,7 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): assert cluster.request.call_args[1]["expect_reply"] is True # test rejoin + cluster.PLUGGED_ATTR_READS = {"current_position_lift_percentage": 0} await async_test_rejoin(hass, zigpy_cover_device, [cluster], (1,)) assert hass.states.get(entity_id).state == STATE_OPEN diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index e345918179e..06caac91cb2 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -52,7 +52,7 @@ async def test_device_tracker(hass, zha_device_joined_restored, zigpy_device_dt) entity_id = await find_entity_id(Platform.DEVICE_TRACKER, zha_device, hass) assert entity_id is not None - assert hass.states.get(entity_id).state == STATE_HOME + assert hass.states.get(entity_id).state == STATE_NOT_HOME await async_enable_traffic(hass, [zha_device], enabled=False) # test that the device tracker was created and that it is unavailable assert hass.states.get(entity_id).state == STATE_UNAVAILABLE diff --git a/tests/components/zha/test_select.py b/tests/components/zha/test_select.py index 452939420bf..70b943d5ea2 100644 --- a/tests/components/zha/test_select.py +++ b/tests/components/zha/test_select.py @@ -1,5 +1,7 @@ """Test ZHA select entities.""" +from unittest.mock import call + import pytest from zigpy.const import SIG_EP_PROFILE import zigpy.profiles.zha as zha @@ -50,6 +52,7 @@ async def light(hass, zigpy_device_mock): SIG_EP_OUTPUT: [general.Ota.cluster_id], } }, + node_descriptor=b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", ) return zigpy_device @@ -173,15 +176,15 @@ async def test_select_restore_state( assert state.state == security.IasWd.Warning.WarningMode.Burglar.name -async def test_on_off_select(hass, light, zha_device_joined_restored): - """Test zha on off select.""" +async def test_on_off_select_new_join(hass, light, zha_device_joined): + """Test zha on off select - new join.""" entity_registry = er.async_get(hass) on_off_cluster = light.endpoints[1].on_off on_off_cluster.PLUGGED_ATTR_READS = { "start_up_on_off": general.OnOff.StartUpOnOff.On } - zha_device = await zha_device_joined_restored(light) + zha_device = await zha_device_joined(light) select_name = general.OnOff.StartUpOnOff.__name__ entity_id = await find_entity_id( Platform.SELECT, @@ -191,12 +194,19 @@ async def test_on_off_select(hass, light, zha_device_joined_restored): ) assert entity_id is not None + assert on_off_cluster.read_attributes.call_count == 2 + assert ( + call(["start_up_on_off"], allow_cache=True, only_cache=False, manufacturer=None) + in on_off_cluster.read_attributes.call_args_list + ) + assert ( + call(["on_off"], allow_cache=False, only_cache=False, manufacturer=None) + in on_off_cluster.read_attributes.call_args_list + ) + state = hass.states.get(entity_id) assert state - if zha_device_joined_restored.name == "zha_device_joined": - assert state.state == general.OnOff.StartUpOnOff.On.name - else: - assert state.state == STATE_UNKNOWN + assert state.state == general.OnOff.StartUpOnOff.On.name assert state.attributes["options"] == ["Off", "On", "Toggle", "PreviousValue"] @@ -225,6 +235,58 @@ async def test_on_off_select(hass, light, zha_device_joined_restored): assert state.state == general.OnOff.StartUpOnOff.Off.name +async def test_on_off_select_restored(hass, light, zha_device_restored): + """Test zha on off select - restored.""" + + entity_registry = er.async_get(hass) + on_off_cluster = light.endpoints[1].on_off + on_off_cluster.PLUGGED_ATTR_READS = { + "start_up_on_off": general.OnOff.StartUpOnOff.On + } + zha_device = await zha_device_restored(light) + + assert zha_device.is_mains_powered + + assert on_off_cluster.read_attributes.call_count == 4 + # first 2 calls hit cache only + assert ( + call(["start_up_on_off"], allow_cache=True, only_cache=True, manufacturer=None) + in on_off_cluster.read_attributes.call_args_list + ) + assert ( + call(["on_off"], allow_cache=True, only_cache=True, manufacturer=None) + in on_off_cluster.read_attributes.call_args_list + ) + + # 2nd set of calls can actually read from the device + assert ( + call(["start_up_on_off"], allow_cache=True, only_cache=False, manufacturer=None) + in on_off_cluster.read_attributes.call_args_list + ) + assert ( + call(["on_off"], allow_cache=False, only_cache=False, manufacturer=None) + in on_off_cluster.read_attributes.call_args_list + ) + + select_name = general.OnOff.StartUpOnOff.__name__ + entity_id = await find_entity_id( + Platform.SELECT, + zha_device, + hass, + qualifier=select_name.lower(), + ) + assert entity_id is not None + + state = hass.states.get(entity_id) + assert state + assert state.state == general.OnOff.StartUpOnOff.On.name + assert state.attributes["options"] == ["Off", "On", "Toggle", "PreviousValue"] + + entity_entry = entity_registry.async_get(entity_id) + assert entity_entry + assert entity_entry.entity_category == ENTITY_CATEGORY_CONFIG + + async def test_on_off_select_unsupported(hass, light, zha_device_joined_restored): """Test zha on off select unsupported.""" From e5797dff71924145882f1752f093d78fa0c8f787 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 28 Apr 2022 15:05:55 -0600 Subject: [PATCH 0023/3516] Ensure that email-based 2FA in SimpliSafe shows the progress UI (#71021) --- .../components/simplisafe/config_flow.py | 92 ++++++++++++------- .../components/simplisafe/strings.json | 5 +- .../simplisafe/translations/en.json | 4 + tests/components/simplisafe/conftest.py | 2 + .../components/simplisafe/test_config_flow.py | 46 +++++----- 5 files changed, 95 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index ad9614c0546..926f904a912 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -49,13 +49,14 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" + self._email_2fa_task: asyncio.Task | None = None self._password: str | None = None self._reauth: bool = False self._simplisafe: API | None = None self._username: str | None = None async def _async_authenticate( - self, error_step_id: str, error_schema: vol.Schema + self, originating_step_id: str, originating_step_schema: vol.Schema ) -> FlowResult: """Attempt to authenticate to the SimpliSafe API.""" assert self._password @@ -76,8 +77,8 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if errors: return self.async_show_form( - step_id=error_step_id, - data_schema=error_schema, + step_id=originating_step_id, + data_schema=originating_step_schema, errors=errors, description_placeholders={CONF_USERNAME: self._username}, ) @@ -86,6 +87,31 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if self._simplisafe.auth_state == AuthStates.PENDING_2FA_SMS: return await self.async_step_sms_2fa() + return await self.async_step_email_2fa() + + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> SimpliSafeOptionsFlowHandler: + """Define the config flow to handle options.""" + return SimpliSafeOptionsFlowHandler(config_entry) + + async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: + """Handle configuration by re-auth.""" + self._reauth = True + + if CONF_USERNAME not in config: + # Old versions of the config flow may not have the username by this point; + # in that case, we reauth them by making them go through the user flow: + return await self.async_step_user() + + self._username = config[CONF_USERNAME] + return await self.async_step_reauth_confirm() + + async def _async_get_email_2fa(self) -> None: + """Define a task to wait for email-based 2FA.""" + assert self._simplisafe try: async with async_timeout.timeout(DEFAULT_EMAIL_2FA_TIMEOUT): @@ -97,17 +123,39 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await asyncio.sleep(DEFAULT_EMAIL_2FA_SLEEP) else: break - except asyncio.TimeoutError: - return self.async_show_form( - step_id="user", - data_schema=STEP_USER_SCHEMA, - errors={"base": "2fa_timed_out"}, + finally: + self.hass.async_create_task( + self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) ) - return await self._async_finish_setup() + async def async_step_email_2fa( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle email-based two-factor authentication.""" + if not self._email_2fa_task: + self._email_2fa_task = self.hass.async_create_task( + self._async_get_email_2fa() + ) + return self.async_show_progress( + step_id="email_2fa", progress_action="email_2fa" + ) - async def _async_finish_setup(self) -> FlowResult: - """Complete setup with an authenticated API object.""" + try: + await self._email_2fa_task + except asyncio.TimeoutError: + return self.async_show_progress_done(next_step_id="email_2fa_error") + return self.async_show_progress_done(next_step_id="finish") + + async def async_step_email_2fa_error( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle an error during email-based two-factor authentication.""" + return self.async_abort(reason="email_2fa_timed_out") + + async def async_step_finish( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the final step.""" assert self._simplisafe assert self._username @@ -142,26 +190,6 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return self.async_create_entry(title=self._username, data=data) - @staticmethod - @callback - def async_get_options_flow( - config_entry: ConfigEntry, - ) -> SimpliSafeOptionsFlowHandler: - """Define the config flow to handle options.""" - return SimpliSafeOptionsFlowHandler(config_entry) - - async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: - """Handle configuration by re-auth.""" - self._reauth = True - - if CONF_USERNAME not in config: - # Old versions of the config flow may not have the username by this point; - # in that case, we reauth them by making them go through the user flow: - return await self.async_step_user() - - 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: @@ -197,7 +225,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors={CONF_CODE: "invalid_auth"}, ) - return await self._async_finish_setup() + return await self.async_step_finish() async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index a5962a89abc..45e0ef84f5a 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -23,13 +23,16 @@ } }, "error": { - "2fa_timed_out": "Timed out while waiting for two-factor authentication", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { "already_configured": "This SimpliSafe account is already in use.", + "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + }, + "progress": { + "email_2fa": "Input the two-factor authentication code\nsent to you via email." } }, "options": { diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 5d8cc4b4e1c..a3bf0049681 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "This SimpliSafe account is already in use.", + "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", "reauth_successful": "Re-authentication was successful", "wrong_account": "The user credentials provided do not match this SimpliSafe account." }, @@ -12,6 +13,9 @@ "still_awaiting_mfa": "Still awaiting MFA email click", "unknown": "Unexpected error" }, + "progress": { + "email_2fa": "Input the two-factor authentication code\nsent to you via email." + }, "step": { "mfa": { "title": "SimpliSafe Multi-Factor Authentication" diff --git a/tests/components/simplisafe/conftest.py b/tests/components/simplisafe/conftest.py index d3a07bd8fc8..56967ac24c5 100644 --- a/tests/components/simplisafe/conftest.py +++ b/tests/components/simplisafe/conftest.py @@ -102,6 +102,8 @@ def reauth_config_fixture(): async def setup_simplisafe_fixture(hass, api, config): """Define a fixture to set up SimpliSafe.""" with patch( + "homeassistant.components.simplisafe.config_flow.DEFAULT_EMAIL_2FA_SLEEP", 0 + ), patch( "homeassistant.components.simplisafe.config_flow.API.async_from_credentials", return_value=api, ), patch( diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index a4ef98e0e09..2e0b85bc6c6 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -118,6 +118,7 @@ async def test_step_reauth_errors(hass, config, error_string, exc, reauth_config result["flow_id"], user_input=reauth_config ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": error_string} @@ -191,12 +192,13 @@ async def test_step_user_errors(hass, credentials_config, error_string, exc): result["flow_id"], user_input=credentials_config ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" assert result["errors"] == {"base": error_string} @pytest.mark.parametrize("api_auth_state", [AuthStates.PENDING_2FA_EMAIL]) async def test_step_user_email_2fa( - api, hass, config, credentials_config, setup_simplisafe + api, api_auth_state, hass, config, credentials_config, setup_simplisafe ): """Test the user step with email-based 2FA.""" result = await hass.config_entries.flow.async_init( @@ -208,14 +210,15 @@ async def test_step_user_email_2fa( # Patch API.async_verify_2fa_email to first return pending, then return all done: api.async_verify_2fa_email.side_effect = [Verify2FAPending, None] - # Patch the amount of time slept between calls so to not slow down this test: - with patch( - "homeassistant.components.simplisafe.config_flow.DEFAULT_EMAIL_2FA_SLEEP", 0 - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=credentials_config - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=credentials_config + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS_DONE + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert len(hass.config_entries.async_entries()) == 1 [config_entry] = hass.config_entries.async_entries(DOMAIN) @@ -223,6 +226,7 @@ async def test_step_user_email_2fa( assert config_entry.data == config +@patch("homeassistant.components.simplisafe.config_flow.DEFAULT_EMAIL_2FA_TIMEOUT", 0) @pytest.mark.parametrize("api_auth_state", [AuthStates.PENDING_2FA_EMAIL]) async def test_step_user_email_2fa_timeout( api, hass, config, credentials_config, setup_simplisafe @@ -237,18 +241,18 @@ async def test_step_user_email_2fa_timeout( # Patch API.async_verify_2fa_email to return pending: api.async_verify_2fa_email.side_effect = Verify2FAPending - # Patch the amount of time slept between calls and the timeout duration so to not - # slow down this test: - with patch( - "homeassistant.components.simplisafe.config_flow.DEFAULT_EMAIL_2FA_SLEEP", 0 - ), patch( - "homeassistant.components.simplisafe.config_flow.DEFAULT_EMAIL_2FA_TIMEOUT", 0 - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=credentials_config - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "2fa_timed_out"} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=credentials_config + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS_DONE + assert result["step_id"] == "email_2fa_error" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "email_2fa_timed_out" async def test_step_user_sms_2fa( From e2b28082a3d414d07d1c0490a4c03bdff891393f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 28 Apr 2022 16:26:29 -0500 Subject: [PATCH 0024/3516] Fix Sonos races related to grouping and startup (#71026) --- .../components/sonos/media_player.py | 9 ++++ homeassistant/components/sonos/speaker.py | 41 ++++++++++++++++--- tests/components/sonos/conftest.py | 1 + 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 30fdb28b02f..f7f5e2722ff 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -258,6 +258,15 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if self.coordinator.uid == uid: self.async_write_ha_state() + @property + def available(self) -> bool: + """Return if the media_player is available.""" + return ( + self.speaker.available + and self.speaker.sonos_group_entities + and self.media.playback_status + ) + @property def coordinator(self) -> SonosSpeaker: """Return the current coordinator SonosSpeaker.""" diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 4435a65db3d..4e4661b389b 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -189,7 +189,12 @@ class SonosSpeaker: def setup(self, entry: ConfigEntry) -> None: """Run initial setup of the speaker.""" - self.set_basic_info() + self.media.play_mode = self.soco.play_mode + self.update_volume() + self.update_groups() + if self.is_coordinator: + self.media.poll_media() + future = asyncio.run_coroutine_threadsafe( self.async_setup_dispatchers(entry), self.hass.loop ) @@ -247,11 +252,6 @@ class SonosSpeaker: """Write states for associated SonosEntity instances.""" async_dispatcher_send(self.hass, f"{SONOS_STATE_UPDATED}-{self.soco.uid}") - def set_basic_info(self) -> None: - """Set basic information when speaker is reconnected.""" - self.media.play_mode = self.soco.play_mode - self.update_volume() - # # Properties # @@ -456,6 +456,34 @@ class SonosSpeaker: @callback def async_dispatch_media_update(self, event: SonosEvent) -> None: """Update information about currently playing media from an event.""" + # The new coordinator can be provided in a media update event but + # before the ZoneGroupState updates. If this happens the playback + # state will be incorrect and should be ignored. Switching to the + # new coordinator will use its media. The regrouping process will + # be completed during the next ZoneGroupState update. + av_transport_uri = event.variables.get("av_transport_uri", "") + current_track_uri = event.variables.get("current_track_uri", "") + if av_transport_uri == current_track_uri and av_transport_uri.startswith( + "x-rincon:" + ): + new_coordinator_uid = av_transport_uri.split(":")[-1] + if new_coordinator_speaker := self.hass.data[DATA_SONOS].discovered.get( + new_coordinator_uid + ): + _LOGGER.debug( + "Media update coordinator (%s) received for %s", + new_coordinator_speaker.zone_name, + self.zone_name, + ) + self.coordinator = new_coordinator_speaker + else: + _LOGGER.debug( + "Media update coordinator (%s) for %s not yet available", + new_coordinator_uid, + self.zone_name, + ) + return + if crossfade := event.variables.get("current_crossfade_mode"): self.cross_fade = bool(int(crossfade)) @@ -774,6 +802,7 @@ class SonosSpeaker: self.zone_name, uid, ) + return if self.sonos_group_entities == sonos_group_entities: # Useful in polling mode for speakers with stereo pairs or surrounds diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index d18a5eecc8a..8c804f466d4 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -120,6 +120,7 @@ def soco_fixture( mock_soco.get_battery_info.return_value = battery_info mock_soco.all_zones = {mock_soco} mock_soco.visible_zones = {mock_soco} + mock_soco.group.coordinator = mock_soco yield mock_soco From 16ce74348c9bf983bac27163f38499092e3b272e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Apr 2022 14:09:54 -0700 Subject: [PATCH 0025/3516] Add redirect for server controls (#71027) Co-authored-by: Zack Barett --- homeassistant/components/frontend/__init__.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 558b24f3286..55d745eb309 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -364,15 +364,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async_register_built_in_panel(hass, "profile") - # To smooth transition to new urls, add redirects to new urls of dev tools - # Added June 27, 2019. Can be removed in 2021. - for panel in ("event", "service", "state", "template"): - hass.http.register_redirect(f"/dev-{panel}", f"/developer-tools/{panel}") - for panel in ("logs", "info", "mqtt"): - # Can be removed in 2021. - hass.http.register_redirect(f"/dev-{panel}", f"/config/{panel}") - # Added June 20 2020. Can be removed in 2022. - hass.http.register_redirect(f"/developer-tools/{panel}", f"/config/{panel}") + # Can be removed in 2023 + hass.http.register_redirect("/config/server_control", "/developer-tools/yaml") async_register_built_in_panel( hass, From 0db553d91cfa02abc5339714f45c30f928275f04 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 28 Apr 2022 16:24:04 -0500 Subject: [PATCH 0026/3516] Frontend Bump to 20220428.0 (#71029) --- 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 c622d930ab1..c806b21e723 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220427.0"], + "requirements": ["home-assistant-frontend==20220428.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f58eb91b087..9d258ddbd4a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220427.0 +home-assistant-frontend==20220428.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.1 diff --git a/requirements_all.txt b/requirements_all.txt index e950616865e..bde13718ac9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220427.0 +home-assistant-frontend==20220428.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ccc958e2543..7c4f147df7a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -580,7 +580,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220427.0 +home-assistant-frontend==20220428.0 # homeassistant.components.home_connect homeconnect==0.7.0 From 0fd4778462a6e08b545f2b248c77e48f86bf3cfc Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 28 Apr 2022 23:09:35 +0200 Subject: [PATCH 0027/3516] Bump pydeconz to v91 (#71030) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index e26c659ff73..dee41435779 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==90"], + "requirements": ["pydeconz==91"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" diff --git a/requirements_all.txt b/requirements_all.txt index bde13718ac9..2e107625169 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1432,7 +1432,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==90 +pydeconz==91 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c4f147df7a..ad3cec937d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -953,7 +953,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==90 +pydeconz==91 # homeassistant.components.dexcom pydexcom==0.2.3 From c52ad6e63c7a4b191262d2dbfda72ccb27ba809b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Apr 2022 15:14:47 -0700 Subject: [PATCH 0028/3516] Bumped version to 2022.5.0b2 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 17b38e7a566..35d82f64f93 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 4775b35a195..2d916bfbeda 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.5.0b1 +version = 2022.5.0b2 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 0b144be61ce7b13ad10cc486b68d49c85f4c1801 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Apr 2022 01:48:28 -0500 Subject: [PATCH 0029/3516] Prevent sqlalchemy Transparent SQL Compilation Caching from filling up during purge (#71015) --- homeassistant/components/recorder/purge.py | 261 +++++++++++++++++++-- 1 file changed, 246 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index e6f30b1c62d..d4061a69bab 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -3,12 +3,15 @@ from __future__ import annotations from collections.abc import Callable, Iterable from datetime import datetime +from itertools import zip_longest import logging from typing import TYPE_CHECKING -from sqlalchemy import column, func, select, union +from sqlalchemy import func, lambda_stmt, select, union_all from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import distinct +from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Select from homeassistant.const import EVENT_STATE_CHANGED @@ -112,6 +115,223 @@ def _select_event_state_and_attributes_ids_to_purge( return event_ids, state_ids, attributes_ids +def _state_attrs_exist(attr: int | None) -> Select: + """Check if a state attributes id exists in the states table.""" + return select(func.min(States.attributes_id)).where(States.attributes_id == attr) + + +def _generate_find_attr_lambda( + attr1: int, + attr2: int | None, + attr3: int | None, + attr4: int | None, + attr5: int | None, + attr6: int | None, + attr7: int | None, + attr8: int | None, + attr9: int | None, + attr10: int | None, + attr11: int | None, + attr12: int | None, + attr13: int | None, + attr14: int | None, + attr15: int | None, + attr16: int | None, + attr17: int | None, + attr18: int | None, + attr19: int | None, + attr20: int | None, + attr21: int | None, + attr22: int | None, + attr23: int | None, + attr24: int | None, + attr25: int | None, + attr26: int | None, + attr27: int | None, + attr28: int | None, + attr29: int | None, + attr30: int | None, + attr31: int | None, + attr32: int | None, + attr33: int | None, + attr34: int | None, + attr35: int | None, + attr36: int | None, + attr37: int | None, + attr38: int | None, + attr39: int | None, + attr40: int | None, + attr41: int | None, + attr42: int | None, + attr43: int | None, + attr44: int | None, + attr45: int | None, + attr46: int | None, + attr47: int | None, + attr48: int | None, + attr49: int | None, + attr50: int | None, + attr51: int | None, + attr52: int | None, + attr53: int | None, + attr54: int | None, + attr55: int | None, + attr56: int | None, + attr57: int | None, + attr58: int | None, + attr59: int | None, + attr60: int | None, + attr61: int | None, + attr62: int | None, + attr63: int | None, + attr64: int | None, + attr65: int | None, + attr66: int | None, + attr67: int | None, + attr68: int | None, + attr69: int | None, + attr70: int | None, + attr71: int | None, + attr72: int | None, + attr73: int | None, + attr74: int | None, + attr75: int | None, + attr76: int | None, + attr77: int | None, + attr78: int | None, + attr79: int | None, + attr80: int | None, + attr81: int | None, + attr82: int | None, + attr83: int | None, + attr84: int | None, + attr85: int | None, + attr86: int | None, + attr87: int | None, + attr88: int | None, + attr89: int | None, + attr90: int | None, + attr91: int | None, + attr92: int | None, + attr93: int | None, + attr94: int | None, + attr95: int | None, + attr96: int | None, + attr97: int | None, + attr98: int | None, + attr99: int | None, + attr100: int | None, +) -> StatementLambdaElement: + """Generate the find attributes select only once. + + https://docs.sqlalchemy.org/en/14/core/connections.html#quick-guidelines-for-lambdas + """ + return lambda_stmt( + lambda: union_all( + _state_attrs_exist(attr1), + _state_attrs_exist(attr2), + _state_attrs_exist(attr3), + _state_attrs_exist(attr4), + _state_attrs_exist(attr5), + _state_attrs_exist(attr6), + _state_attrs_exist(attr7), + _state_attrs_exist(attr8), + _state_attrs_exist(attr9), + _state_attrs_exist(attr10), + _state_attrs_exist(attr11), + _state_attrs_exist(attr12), + _state_attrs_exist(attr13), + _state_attrs_exist(attr14), + _state_attrs_exist(attr15), + _state_attrs_exist(attr16), + _state_attrs_exist(attr17), + _state_attrs_exist(attr18), + _state_attrs_exist(attr19), + _state_attrs_exist(attr20), + _state_attrs_exist(attr21), + _state_attrs_exist(attr22), + _state_attrs_exist(attr23), + _state_attrs_exist(attr24), + _state_attrs_exist(attr25), + _state_attrs_exist(attr26), + _state_attrs_exist(attr27), + _state_attrs_exist(attr28), + _state_attrs_exist(attr29), + _state_attrs_exist(attr30), + _state_attrs_exist(attr31), + _state_attrs_exist(attr32), + _state_attrs_exist(attr33), + _state_attrs_exist(attr34), + _state_attrs_exist(attr35), + _state_attrs_exist(attr36), + _state_attrs_exist(attr37), + _state_attrs_exist(attr38), + _state_attrs_exist(attr39), + _state_attrs_exist(attr40), + _state_attrs_exist(attr41), + _state_attrs_exist(attr42), + _state_attrs_exist(attr43), + _state_attrs_exist(attr44), + _state_attrs_exist(attr45), + _state_attrs_exist(attr46), + _state_attrs_exist(attr47), + _state_attrs_exist(attr48), + _state_attrs_exist(attr49), + _state_attrs_exist(attr50), + _state_attrs_exist(attr51), + _state_attrs_exist(attr52), + _state_attrs_exist(attr53), + _state_attrs_exist(attr54), + _state_attrs_exist(attr55), + _state_attrs_exist(attr56), + _state_attrs_exist(attr57), + _state_attrs_exist(attr58), + _state_attrs_exist(attr59), + _state_attrs_exist(attr60), + _state_attrs_exist(attr61), + _state_attrs_exist(attr62), + _state_attrs_exist(attr63), + _state_attrs_exist(attr64), + _state_attrs_exist(attr65), + _state_attrs_exist(attr66), + _state_attrs_exist(attr67), + _state_attrs_exist(attr68), + _state_attrs_exist(attr69), + _state_attrs_exist(attr70), + _state_attrs_exist(attr71), + _state_attrs_exist(attr72), + _state_attrs_exist(attr73), + _state_attrs_exist(attr74), + _state_attrs_exist(attr75), + _state_attrs_exist(attr76), + _state_attrs_exist(attr77), + _state_attrs_exist(attr78), + _state_attrs_exist(attr79), + _state_attrs_exist(attr80), + _state_attrs_exist(attr81), + _state_attrs_exist(attr82), + _state_attrs_exist(attr83), + _state_attrs_exist(attr84), + _state_attrs_exist(attr85), + _state_attrs_exist(attr86), + _state_attrs_exist(attr87), + _state_attrs_exist(attr88), + _state_attrs_exist(attr89), + _state_attrs_exist(attr90), + _state_attrs_exist(attr91), + _state_attrs_exist(attr92), + _state_attrs_exist(attr93), + _state_attrs_exist(attr94), + _state_attrs_exist(attr95), + _state_attrs_exist(attr96), + _state_attrs_exist(attr97), + _state_attrs_exist(attr98), + _state_attrs_exist(attr99), + _state_attrs_exist(attr100), + ) + ) + + def _select_unused_attributes_ids( session: Session, attributes_ids: set[int], using_sqlite: bool ) -> set[int]: @@ -132,9 +352,12 @@ def _select_unused_attributes_ids( # > explain select distinct attributes_id from states where attributes_id in (136723); # ...Using index # - id_query = session.query(distinct(States.attributes_id)).filter( - States.attributes_id.in_(attributes_ids) - ) + seen_ids = { + state[0] + for state in session.query(distinct(States.attributes_id)) + .filter(States.attributes_id.in_(attributes_ids)) + .all() + } else: # # This branch is for DBMS that cannot optimize the distinct query well and has to examine @@ -151,17 +374,25 @@ def _select_unused_attributes_ids( # > explain select min(attributes_id) from states where attributes_id = 136723; # ...Select tables optimized away # - id_query = session.query(column("id")).from_statement( - union( - *[ - select(func.min(States.attributes_id).label("id")).where( - States.attributes_id == attributes_id - ) - for attributes_id in attributes_ids - ] - ) - ) - to_remove = attributes_ids - {state[0] for state in id_query.all()} + # We used to generate a query based on how many attribute_ids to find but + # that meant sqlalchemy Transparent SQL Compilation Caching was working against + # us by cached up to MAX_ROWS_TO_PURGE different statements which could be + # up to 500MB for large database due to the complexity of the ORM objects. + # + # We now break the query into groups of 100 and use a lambda_stmt to ensure + # that the query is only cached once. + # + seen_ids = set() + groups = [iter(attributes_ids)] * 100 + for attr_ids in zip_longest(*groups, fillvalue=None): + seen_ids |= { + state[0] + for state in session.execute( + _generate_find_attr_lambda(*attr_ids) + ).all() + if state[0] is not None + } + to_remove = attributes_ids - seen_ids _LOGGER.debug( "Selected %s shared attributes to remove", len(to_remove), From 3ffdbc454e26c611af395e971a8d6ca0eb948f2e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Apr 2022 18:06:21 +0200 Subject: [PATCH 0030/3516] Support shorthand logical operators in script sequences (#71022) --- homeassistant/helpers/config_validation.py | 2 +- tests/helpers/test_script.py | 117 +++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index bbf8475c539..f459d96040b 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1599,7 +1599,7 @@ def determine_script_action(action: dict[str, Any]) -> str: if CONF_WAIT_TEMPLATE in action: return SCRIPT_ACTION_WAIT_TEMPLATE - if CONF_CONDITION in action: + if any(key in action for key in (CONF_CONDITION, "and", "or", "not")): return SCRIPT_ACTION_CHECK_CONDITION if CONF_EVENT in action: diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 7f1b35e4e4a..fb3b021daec 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -4744,3 +4744,120 @@ async def test_disabled_actions( "3": [{"result": {"event": "test_event", "event_data": {}}}], }, ) + + +async def test_condition_and_shorthand(hass, caplog): + """Test if we can use the shorthand and conditions in a script.""" + events = async_capture_events(hass, "test_event") + sequence = cv.SCRIPT_SCHEMA( + [ + {"event": "test_event"}, + { + "alias": "shorthand and condition", + "and": [ + { + "condition": "template", + "value_template": "{{ states('test.entity') == 'hello' }}", + } + ], + }, + {"event": "test_event"}, + ] + ) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + hass.states.async_set("test.entity", "hello") + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert "Test condition shorthand and condition: True" in caplog.text + assert len(events) == 2 + + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{"result": {"result": True}}], + "1/conditions/0": [ + {"result": {"entities": ["test.entity"], "result": True}} + ], + "2": [{"result": {"event": "test_event", "event_data": {}}}], + } + ) + + +async def test_condition_or_shorthand(hass, caplog): + """Test if we can use the shorthand or conditions in a script.""" + events = async_capture_events(hass, "test_event") + sequence = cv.SCRIPT_SCHEMA( + [ + {"event": "test_event"}, + { + "alias": "shorthand or condition", + "or": [ + { + "condition": "template", + "value_template": "{{ states('test.entity') == 'hello' }}", + } + ], + }, + {"event": "test_event"}, + ] + ) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + hass.states.async_set("test.entity", "hello") + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert "Test condition shorthand or condition: True" in caplog.text + assert len(events) == 2 + + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{"result": {"result": True}}], + "1/conditions/0": [ + {"result": {"entities": ["test.entity"], "result": True}} + ], + "2": [{"result": {"event": "test_event", "event_data": {}}}], + } + ) + + +async def test_condition_not_shorthand(hass, caplog): + """Test if we can use the shorthand not conditions in a script.""" + events = async_capture_events(hass, "test_event") + sequence = cv.SCRIPT_SCHEMA( + [ + {"event": "test_event"}, + { + "alias": "shorthand not condition", + "not": [ + { + "condition": "template", + "value_template": "{{ states('test.entity') == 'hello' }}", + } + ], + }, + {"event": "test_event"}, + ] + ) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + hass.states.async_set("test.entity", "not hello") + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert "Test condition shorthand not condition: True" in caplog.text + assert len(events) == 2 + + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{"result": {"result": True}}], + "1/conditions/0": [ + {"result": {"entities": ["test.entity"], "result": False}} + ], + "2": [{"result": {"event": "test_event", "event_data": {}}}], + } + ) From bff4c5f9d2692383938c3adc3e3079f476deabe9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Apr 2022 01:48:58 -0500 Subject: [PATCH 0031/3516] Fix history_stats for timezones with a positive offset from UTC (#71038) --- .../components/history_stats/data.py | 4 +- tests/components/history_stats/test_sensor.py | 81 +++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index c5d0ee0fb15..3f22f4cc32b 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -11,6 +11,8 @@ import homeassistant.util.dt as dt_util from .helpers import async_calculate_period, floored_timestamp +MIN_TIME_UTC = datetime.datetime.min.replace(tzinfo=dt_util.UTC) + @dataclass class HistoryStatsState: @@ -36,7 +38,7 @@ class HistoryStats: """Init the history stats manager.""" self.hass = hass self.entity_id = entity_id - self._period = (datetime.datetime.min, datetime.datetime.min) + self._period = (MIN_TIME_UTC, MIN_TIME_UTC) self._state: HistoryStatsState = HistoryStatsState(None, None, self._period) self._history_current_period: list[State] = [] self._previous_run_before_start = False diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 40a13116026..bfa0c8f415e 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -1306,3 +1306,84 @@ async def test_measure_from_end_going_backwards(hass, recorder_mock): assert hass.states.get("sensor.sensor2").state == "0.83" assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor4").state == "83.3" + + +async def test_measure_cet(hass, recorder_mock): + """Test the history statistics sensor measure with a non-UTC timezone.""" + hass.config.set_time_zone("Europe/Berlin") + start_time = dt_util.utcnow() - timedelta(minutes=60) + t0 = start_time + timedelta(minutes=20) + t1 = t0 + timedelta(minutes=10) + t2 = t1 + timedelta(minutes=10) + + # Start t0 t1 t2 End + # |--20min--|--20min--|--10min--|--10min--| + # |---off---|---on----|---off---|---on----| + + def _fake_states(*args, **kwargs): + return { + "binary_sensor.test_id": [ + ha.State("binary_sensor.test_id", "on", last_changed=t0), + ha.State("binary_sensor.test_id", "off", last_changed=t1), + ha.State("binary_sensor.test_id", "on", last_changed=t2), + ] + } + + await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor1", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor2", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor3", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "count", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor4", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "ratio", + }, + ] + }, + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.recorder.history.state_changes_during_period", + _fake_states, + ): + for i in range(1, 5): + await async_update_entity(hass, f"sensor.sensor{i}") + await hass.async_block_till_done() + + assert hass.states.get("sensor.sensor1").state == "0.83" + assert hass.states.get("sensor.sensor2").state == "0.83" + assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor4").state == "83.3" From 7072509bde74d37613b2ad71e6073160541bdca4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Apr 2022 23:32:46 -0500 Subject: [PATCH 0032/3516] Bump sqlalchemy to 1.4.36 (#71039) --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index e0e4862c18c..0fb44f99ae2 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.35", "fnvhash==0.1.0", "lru-dict==1.1.7"], + "requirements": ["sqlalchemy==1.4.36", "fnvhash==0.1.0", "lru-dict==1.1.7"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 272a5d1f685..c779e4567cd 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,7 +2,7 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.4.35"], + "requirements": ["sqlalchemy==1.4.36"], "codeowners": ["@dgomes", "@gjohansson-ST"], "config_flow": true, "iot_class": "local_polling" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9d258ddbd4a..bf6ad12b287 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ pyudev==0.22.0 pyyaml==6.0 requests==2.27.1 scapy==2.4.5 -sqlalchemy==1.4.35 +sqlalchemy==1.4.36 typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index 2e107625169..da39ee6ce91 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2208,7 +2208,7 @@ spotipy==2.19.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.35 +sqlalchemy==1.4.36 # homeassistant.components.srp_energy srpenergy==1.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ad3cec937d1..bb5f6f43234 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1444,7 +1444,7 @@ spotipy==2.19.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.35 +sqlalchemy==1.4.36 # homeassistant.components.srp_energy srpenergy==1.3.6 From 08def3c9339d4495c1b8b6198cf13b3ab0f67f81 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Apr 2022 23:45:57 -0700 Subject: [PATCH 0033/3516] Fix race causing google config pre-init access (#71042) --- homeassistant/components/cloud/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index ad34186b7df..c47544f9d99 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -118,14 +118,15 @@ class CloudClient(Interface): cloud_user = await self._prefs.get_cloud_user() - self._google_config = google_config.CloudGoogleConfig( + google_conf = google_config.CloudGoogleConfig( self._hass, self.google_user_config, cloud_user, self._prefs, self.cloud, ) - await self._google_config.async_initialize() + await google_conf.async_initialize() + self._google_config = google_conf return self._google_config From 28d864ea8d9b8a06ae30a8b0b2f4065fb92b3317 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 29 Apr 2022 12:44:59 -0400 Subject: [PATCH 0034/3516] Update ZHA switch entities to leverage Zigpy cache appropriately (#71062) --- .../components/zha/core/channels/general.py | 21 ++-- homeassistant/components/zha/switch.py | 118 +++++++++--------- 2 files changed, 67 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 81152bb8869..e6524c9aad1 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -6,6 +6,7 @@ from collections.abc import Coroutine from typing import Any import zigpy.exceptions +import zigpy.types as t from zigpy.zcl.clusters import general from zigpy.zcl.foundation import Status @@ -300,7 +301,6 @@ class OnOffChannel(ZigbeeChannel): ) -> None: """Initialize OnOffChannel.""" super().__init__(cluster, ch_pool) - self._state = None self._off_listener = None @property @@ -314,9 +314,9 @@ class OnOffChannel(ZigbeeChannel): cmd = parse_and_log_command(self, tsn, command_id, args) if cmd in ("off", "off_with_effect"): - self.attribute_updated(self.ON_OFF, False) + self.cluster.update_attribute(self.ON_OFF, t.Bool.false) elif cmd in ("on", "on_with_recall_global_scene"): - self.attribute_updated(self.ON_OFF, True) + self.cluster.update_attribute(self.ON_OFF, t.Bool.true) elif cmd == "on_with_timed_off": should_accept = args[0] on_time = args[1] @@ -325,7 +325,7 @@ class OnOffChannel(ZigbeeChannel): if self._off_listener is not None: self._off_listener() self._off_listener = None - self.attribute_updated(self.ON_OFF, True) + self.cluster.update_attribute(self.ON_OFF, t.Bool.true) if on_time > 0: self._off_listener = async_call_later( self._ch_pool.hass, @@ -333,13 +333,13 @@ class OnOffChannel(ZigbeeChannel): self.set_to_off, ) elif cmd == "toggle": - self.attribute_updated(self.ON_OFF, not bool(self._state)) + self.cluster.update_attribute(self.ON_OFF, not bool(self.on_off)) @callback def set_to_off(self, *_): """Set the state to off.""" self._off_listener = None - self.attribute_updated(self.ON_OFF, False) + self.cluster.update_attribute(self.ON_OFF, t.Bool.false) @callback def attribute_updated(self, attrid, value): @@ -348,11 +348,6 @@ class OnOffChannel(ZigbeeChannel): self.async_send_signal( f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", attrid, "on_off", value ) - self._state = bool(value) - - async def async_initialize_channel_specific(self, from_cache: bool) -> None: - """Initialize channel.""" - self._state = self.on_off async def async_update(self): """Initialize channel.""" @@ -360,9 +355,7 @@ class OnOffChannel(ZigbeeChannel): return from_cache = not self._ch_pool.is_mains_powered self.debug("attempting to update onoff state - from cache: %s", from_cache) - state = await self.get_attribute_value(self.ON_OFF, from_cache=from_cache) - if state is not None: - self._state = bool(state) + await self.get_attribute_value(self.ON_OFF, from_cache=from_cache) await super().async_update() diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 87d2407c2dc..254e3691da1 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -46,21 +46,73 @@ async def async_setup_entry( config_entry.async_on_unload(unsub) -class BaseSwitch(SwitchEntity): - """Common base class for zha switches.""" +@STRICT_MATCH(channel_names=CHANNEL_ON_OFF) +class Switch(ZhaEntity, SwitchEntity): + """ZHA switch.""" - def __init__(self, *args, **kwargs): + def __init__(self, unique_id, zha_device, channels, **kwargs): """Initialize the ZHA switch.""" - self._on_off_channel = None - self._state = None - super().__init__(*args, **kwargs) + super().__init__(unique_id, zha_device, channels, **kwargs) + self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) @property def is_on(self) -> bool: """Return if the switch is on based on the statemachine.""" - if self._state is None: + if self._on_off_channel.on_off is None: return False - return self._state + return self._on_off_channel.on_off + + async def async_turn_on(self, **kwargs) -> None: + """Turn the entity on.""" + result = await self._on_off_channel.on() + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + return + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs) -> None: + """Turn the entity off.""" + result = await self._on_off_channel.off() + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + return + self.async_write_ha_state() + + @callback + def async_set_state(self, attr_id: int, attr_name: str, value: Any): + """Handle state update from channel.""" + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Run when about to be added to hass.""" + await super().async_added_to_hass() + self.async_accept_signal( + self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_state + ) + + async def async_update(self) -> None: + """Attempt to retrieve on off state from the switch.""" + await super().async_update() + if self._on_off_channel: + await self._on_off_channel.get_attribute_value("on_off", from_cache=False) + + +@GROUP_MATCH() +class SwitchGroup(ZhaGroupEntity, SwitchEntity): + """Representation of a switch group.""" + + def __init__( + self, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs + ) -> None: + """Initialize a switch group.""" + super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) + self._available: bool + self._state: bool + group = self.zha_device.gateway.get_group(self._group_id) + self._on_off_channel = group.endpoint[OnOff.cluster_id] + + @property + def is_on(self) -> bool: + """Return if the switch is on based on the statemachine.""" + return bool(self._state) async def async_turn_on(self, **kwargs) -> None: """Turn the entity on.""" @@ -78,56 +130,6 @@ class BaseSwitch(SwitchEntity): self._state = False self.async_write_ha_state() - -@STRICT_MATCH(channel_names=CHANNEL_ON_OFF) -class Switch(BaseSwitch, ZhaEntity): - """ZHA switch.""" - - def __init__(self, unique_id, zha_device, channels, **kwargs): - """Initialize the ZHA switch.""" - super().__init__(unique_id, zha_device, channels, **kwargs) - self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) - - @callback - def async_set_state(self, attr_id: int, attr_name: str, value: Any): - """Handle state update from channel.""" - self._state = bool(value) - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - await super().async_added_to_hass() - self.async_accept_signal( - self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_state - ) - - @callback - def async_restore_last_state(self, last_state) -> None: - """Restore previous state.""" - self._state = last_state.state == STATE_ON - - async def async_update(self) -> None: - """Attempt to retrieve on off state from the switch.""" - await super().async_update() - if self._on_off_channel: - state = await self._on_off_channel.get_attribute_value("on_off") - if state is not None: - self._state = state - - -@GROUP_MATCH() -class SwitchGroup(BaseSwitch, ZhaGroupEntity): - """Representation of a switch group.""" - - def __init__( - self, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs - ) -> None: - """Initialize a switch group.""" - super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) - self._available: bool = False - group = self.zha_device.gateway.get_group(self._group_id) - self._on_off_channel = group.endpoint[OnOff.cluster_id] - async def async_update(self) -> None: """Query all members and determine the light group state.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] From 65c3ffd522345069e7a358641a3556e10be2f6e4 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 29 Apr 2022 19:00:44 +0200 Subject: [PATCH 0035/3516] Fix sql integration issues 5.0 beta (#71063) Co-authored-by: Paulus Schoutsen --- homeassistant/components/sql/config_flow.py | 30 ++++++++----------- homeassistant/components/sql/strings.json | 4 +++ .../components/sql/translations/en.json | 4 +++ tests/components/sql/__init__.py | 12 +++++++- tests/components/sql/test_config_flow.py | 27 ++++++++++------- tests/components/sql/test_sensor.py | 2 +- 6 files changed, 48 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/sql/config_flow.py b/homeassistant/components/sql/config_flow.py index 4bb9f6c724c..9150cb8f63d 100644 --- a/homeassistant/components/sql/config_flow.py +++ b/homeassistant/components/sql/config_flow.py @@ -23,17 +23,12 @@ _LOGGER = logging.getLogger(__name__) DATA_SCHEMA = vol.Schema( { - vol.Optional(CONF_DB_URL): selector.TextSelector(selector.TextSelectorConfig()), - vol.Required(CONF_COLUMN_NAME): selector.TextSelector( - selector.TextSelectorConfig() - ), - vol.Required(CONF_QUERY): selector.TextSelector(selector.TextSelectorConfig()), - vol.Optional(CONF_UNIT_OF_MEASUREMENT): selector.TextSelector( - selector.TextSelectorConfig() - ), - vol.Optional(CONF_VALUE_TEMPLATE): selector.TemplateSelector( - selector.TemplateSelectorConfig() - ), + vol.Required(CONF_NAME, default="Select SQL Query"): selector.TextSelector(), + vol.Optional(CONF_DB_URL): selector.TextSelector(), + vol.Required(CONF_COLUMN_NAME): selector.TextSelector(), + vol.Required(CONF_QUERY): selector.TextSelector(), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): selector.TextSelector(), + vol.Optional(CONF_VALUE_TEMPLATE): selector.TemplateSelector(), } ) @@ -109,8 +104,7 @@ class SQLConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): column = user_input[CONF_COLUMN_NAME] uom = user_input.get(CONF_UNIT_OF_MEASUREMENT) value_template = user_input.get(CONF_VALUE_TEMPLATE) - - name = f"Select {column} SQL query" + name = user_input[CONF_NAME] try: validate_sql_select(query) @@ -182,17 +176,17 @@ class SQLOptionsFlowHandler(config_entries.OptionsFlow): description={ "suggested_value": self.entry.options[CONF_DB_URL] }, - ): selector.selector({"text": {}}), + ): selector.TextSelector(), vol.Required( CONF_QUERY, description={"suggested_value": self.entry.options[CONF_QUERY]}, - ): selector.selector({"text": {}}), + ): selector.TextSelector(), vol.Required( CONF_COLUMN_NAME, description={ "suggested_value": self.entry.options[CONF_COLUMN_NAME] }, - ): selector.selector({"text": {}}), + ): selector.TextSelector(), vol.Optional( CONF_UNIT_OF_MEASUREMENT, description={ @@ -200,7 +194,7 @@ class SQLOptionsFlowHandler(config_entries.OptionsFlow): CONF_UNIT_OF_MEASUREMENT ) }, - ): selector.selector({"text": {}}), + ): selector.TextSelector(), vol.Optional( CONF_VALUE_TEMPLATE, description={ @@ -208,7 +202,7 @@ class SQLOptionsFlowHandler(config_entries.OptionsFlow): CONF_VALUE_TEMPLATE ) }, - ): selector.selector({"text": {}}), + ): selector.TemplateSelector(), } ), errors=errors, diff --git a/homeassistant/components/sql/strings.json b/homeassistant/components/sql/strings.json index 0d174060d1b..8d3a194ac3e 100644 --- a/homeassistant/components/sql/strings.json +++ b/homeassistant/components/sql/strings.json @@ -12,6 +12,7 @@ "user": { "data": { "db_url": "Database URL", + "name": "[%key:common::config_flow::data::name%]", "query": "Select Query", "column": "Column", "unit_of_measurement": "Unit of Measure", @@ -19,6 +20,7 @@ }, "data_description": { "db_url": "Database URL, leave empty to use default HA database", + "name": "Name that will be used for Config Entry and also the Sensor", "query": "Query to run, needs to start with 'SELECT'", "column": "Column for returned query to present as state", "unit_of_measurement": "Unit of Measure (optional)", @@ -32,6 +34,7 @@ "init": { "data": { "db_url": "[%key:component::sql::config::step::user::data::db_url%]", + "name": "[%key:component::sql::config::step::user::data::name%]", "query": "[%key:component::sql::config::step::user::data::query%]", "column": "[%key:component::sql::config::step::user::data::column%]", "unit_of_measurement": "[%key:component::sql::config::step::user::data::unit_of_measurement%]", @@ -39,6 +42,7 @@ }, "data_description": { "db_url": "[%key:component::sql::config::step::user::data_description::db_url%]", + "name": "[%key:component::sql::config::step::user::data_description::name%]", "query": "[%key:component::sql::config::step::user::data_description::query%]", "column": "[%key:component::sql::config::step::user::data_description::column%]", "unit_of_measurement": "[%key:component::sql::config::step::user::data_description::unit_of_measurement%]", diff --git a/homeassistant/components/sql/translations/en.json b/homeassistant/components/sql/translations/en.json index 3b1fc223c00..4b72d024df4 100644 --- a/homeassistant/components/sql/translations/en.json +++ b/homeassistant/components/sql/translations/en.json @@ -13,6 +13,7 @@ "data": { "column": "Column", "db_url": "Database URL", + "name": "Name", "query": "Select Query", "unit_of_measurement": "Unit of Measure", "value_template": "Value Template" @@ -20,6 +21,7 @@ "data_description": { "column": "Column for returned query to present as state", "db_url": "Database URL, leave empty to use default HA database", + "name": "Name that will be used for Config Entry and also the Sensor", "query": "Query to run, needs to start with 'SELECT'", "unit_of_measurement": "Unit of Measure (optional)", "value_template": "Value Template (optional)" @@ -38,6 +40,7 @@ "data": { "column": "Column", "db_url": "Database URL", + "name": "Name", "query": "Select Query", "unit_of_measurement": "Unit of Measure", "value_template": "Value Template" @@ -45,6 +48,7 @@ "data_description": { "column": "Column for returned query to present as state", "db_url": "Database URL, leave empty to use default HA database", + "name": "Name that will be used for Config Entry and also the Sensor", "query": "Query to run, needs to start with 'SELECT'", "unit_of_measurement": "Unit of Measure (optional)", "value_template": "Value Template (optional)" diff --git a/tests/components/sql/__init__.py b/tests/components/sql/__init__.py index 7138a86a5d6..db65034bd11 100644 --- a/tests/components/sql/__init__.py +++ b/tests/components/sql/__init__.py @@ -6,19 +6,28 @@ from typing import Any from homeassistant.components.recorder import CONF_DB_URL from homeassistant.components.sql.const import CONF_COLUMN_NAME, CONF_QUERY, DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_UNIT_OF_MEASUREMENT +from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry ENTRY_CONFIG = { CONF_DB_URL: "sqlite://", + CONF_NAME: "Get Value", CONF_QUERY: "SELECT 5 as value", CONF_COLUMN_NAME: "value", CONF_UNIT_OF_MEASUREMENT: "MiB", } ENTRY_CONFIG_INVALID_QUERY = { + CONF_DB_URL: "sqlite://", + CONF_NAME: "Get Value", + CONF_QUERY: "UPDATE 5 as value", + CONF_COLUMN_NAME: "size", + CONF_UNIT_OF_MEASUREMENT: "MiB", +} + +ENTRY_CONFIG_INVALID_QUERY_OPT = { CONF_DB_URL: "sqlite://", CONF_QUERY: "UPDATE 5 as value", CONF_COLUMN_NAME: "size", @@ -27,6 +36,7 @@ ENTRY_CONFIG_INVALID_QUERY = { ENTRY_CONFIG_NO_RESULTS = { CONF_DB_URL: "sqlite://", + CONF_NAME: "Get Value", CONF_QUERY: "SELECT kalle as value from no_table;", CONF_COLUMN_NAME: "value", CONF_UNIT_OF_MEASUREMENT: "MiB", diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index d38abce5da7..ec851b491b7 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -14,7 +14,12 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from . import ENTRY_CONFIG, ENTRY_CONFIG_INVALID_QUERY, ENTRY_CONFIG_NO_RESULTS +from . import ( + ENTRY_CONFIG, + ENTRY_CONFIG_INVALID_QUERY, + ENTRY_CONFIG_INVALID_QUERY_OPT, + ENTRY_CONFIG_NO_RESULTS, +) from tests.common import MockConfigEntry @@ -40,14 +45,14 @@ async def test_form(hass: HomeAssistant) -> None: print(ENTRY_CONFIG) assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "Select value SQL query" + assert result2["title"] == "Get Value" assert result2["options"] == { "db_url": "sqlite://", + "name": "Get Value", "query": "SELECT 5 as value", "column": "value", "unit_of_measurement": "MiB", "value_template": None, - "name": "Select value SQL query", } assert len(mock_setup_entry.mock_calls) == 1 @@ -67,14 +72,14 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "Select value SQL query" + assert result2["title"] == "Get Value" assert result2["options"] == { "db_url": "sqlite://", + "name": "Get Value", "query": "SELECT 5 as value", "column": "value", "unit_of_measurement": "MiB", "value_template": None, - "name": "Select value SQL query", } assert len(mock_setup_entry.mock_calls) == 1 @@ -158,14 +163,14 @@ async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None: ) assert result5["type"] == RESULT_TYPE_CREATE_ENTRY - assert result5["title"] == "Select value SQL query" + assert result5["title"] == "Get Value" assert result5["options"] == { "db_url": "sqlite://", + "name": "Get Value", "query": "SELECT 5 as value", "column": "value", "unit_of_measurement": "MiB", "value_template": None, - "name": "Select value SQL query", } @@ -176,11 +181,11 @@ async def test_options_flow(hass: HomeAssistant) -> None: data={}, options={ "db_url": "sqlite://", + "name": "Get Value", "query": "SELECT 5 as value", "column": "value", "unit_of_measurement": "MiB", "value_template": None, - "name": "Select value SQL query", }, ) entry.add_to_hass(hass) @@ -223,11 +228,11 @@ async def test_options_flow_fails_db_url(hass: HomeAssistant) -> None: data={}, options={ "db_url": "sqlite://", + "name": "Get Value", "query": "SELECT 5 as value", "column": "value", "unit_of_measurement": "MiB", "value_template": None, - "name": "Select value SQL query", }, ) entry.add_to_hass(hass) @@ -267,11 +272,11 @@ async def test_options_flow_fails_invalid_query( data={}, options={ "db_url": "sqlite://", + "name": "Get Value", "query": "SELECT 5 as value", "column": "value", "unit_of_measurement": "MiB", "value_template": None, - "name": "Select size SQL query", }, ) entry.add_to_hass(hass) @@ -287,7 +292,7 @@ async def test_options_flow_fails_invalid_query( result2 = await hass.config_entries.options.async_configure( result["flow_id"], - user_input=ENTRY_CONFIG_INVALID_QUERY, + user_input=ENTRY_CONFIG_INVALID_QUERY_OPT, ) assert result2["type"] == RESULT_TYPE_FORM diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index 77717ec400c..0eb3bf70683 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -52,7 +52,7 @@ async def test_import_query(hass: HomeAssistant) -> None: assert hass.config_entries.async_entries(DOMAIN) options = hass.config_entries.async_entries(DOMAIN)[0].options - assert options[CONF_NAME] == "Select value SQL query" + assert options[CONF_NAME] == "count_tables" async def test_query_value_template(hass: HomeAssistant) -> None: From 5a777966e7f528a193733acfe96f42b1c138fd0b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Apr 2022 10:02:18 -0700 Subject: [PATCH 0036/3516] Bumped version to 2022.5.0b3 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 35d82f64f93..c22f8ddc4c0 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 2d916bfbeda..3156fca93ec 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.5.0b2 +version = 2022.5.0b3 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From b657a7d68b76909da336a4cfba1a0d5f6cfab0d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Apr 2022 13:18:06 -0500 Subject: [PATCH 0037/3516] Fix unsafe websocket stop call in isy994 (#71071) Fixes ``` 2022-04-29 12:49:10 ERROR (MainThread) [homeassistant.config_entries] Error unloading entry Alexander (192.168.209.83) for isy994 Traceback (most recent call last): File "/Users/bdraco/home-assistant/homeassistant/config_entries.py", line 474, in async_unload result = await component.async_unload_entry(hass, self) File "/Users/bdraco/home-assistant/homeassistant/components/isy994/__init__.py", line 294, in async_unload_entry await hass.async_add_executor_job(_stop_auto_update) File "/opt/homebrew/Cellar/python@3.9/3.9.12/Frameworks/Python.framework/Versions/3.9/lib/python3.9/concurrent/futures/thread.py", line 58, in run result = self.fn(*self.args, **self.kwargs) File "/Users/bdraco/home-assistant/homeassistant/components/isy994/__init__.py", line 292, in _stop_auto_update isy.websocket.stop() File "/Users/bdraco/home-assistant/venv/lib/python3.9/site-packages/pyisy/events/websocket.py", line 110, in stop self.websocket_task.cancel() File "/opt/homebrew/Cellar/python@3.9/3.9.12/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/base_events.py", line 753, in call_soon self._check_thread() File "/opt/homebrew/Cellar/python@3.9/3.9.12/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/base_events.py", line 790, in _check_thread raise RuntimeError( RuntimeError: Non-thread-safe operation invoked on an event loop other than the current one ``` --- homeassistant/components/isy994/__init__.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index ba152c5d840..ae48fc18b5b 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -288,14 +288,10 @@ async def async_unload_entry( hass_isy_data = hass.data[DOMAIN][entry.entry_id] - isy = hass_isy_data[ISY994_ISY] + isy: ISY = hass_isy_data[ISY994_ISY] - def _stop_auto_update() -> None: - """Stop the isy auto update.""" - _LOGGER.debug("ISY Stopping Event Stream and automatic updates") - isy.websocket.stop() - - await hass.async_add_executor_job(_stop_auto_update) + _LOGGER.debug("ISY Stopping Event Stream and automatic updates") + isy.websocket.stop() if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) From cc174022e4403a68f930d861c51996581ee25637 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 29 Apr 2022 22:55:55 +0200 Subject: [PATCH 0038/3516] Pydeconz raise ResponseError when deCONZ Rest API Plugin is not yet ready (#71078) --- homeassistant/components/deconz/gateway.py | 2 +- tests/components/deconz/test_gateway.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index ef07a97ad8f..f54cd4076d3 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -289,6 +289,6 @@ async def get_deconz_session( LOGGER.warning("Invalid key for deCONZ at %s", config[CONF_HOST]) raise AuthenticationRequired from err - except (asyncio.TimeoutError, errors.RequestError) as err: + except (asyncio.TimeoutError, errors.RequestError, errors.ResponseError) as err: LOGGER.error("Error connecting to deCONZ gateway at %s", config[CONF_HOST]) raise CannotConnect from err diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 3a4f6b907af..e6ded3981c4 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -286,6 +286,7 @@ async def test_get_deconz_session(hass): [ (asyncio.TimeoutError, CannotConnect), (pydeconz.RequestError, CannotConnect), + (pydeconz.ResponseError, CannotConnect), (pydeconz.Unauthorized, AuthenticationRequired), ], ) From b5808a7b4abaa6f82193beb85921fe9bdca803aa Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 29 Apr 2022 22:57:38 +0200 Subject: [PATCH 0039/3516] Don't rely on deCONZ gateway object in config options flow (#71079) --- .../components/deconz/config_flow.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index fcfea60533f..806ac72c973 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -31,12 +31,15 @@ from .const import ( CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, CONF_ALLOW_NEW_DEVICES, + DEFAULT_ALLOW_CLIP_SENSOR, + DEFAULT_ALLOW_DECONZ_GROUPS, + DEFAULT_ALLOW_NEW_DEVICES, DEFAULT_PORT, DOMAIN, HASSIO_CONFIGURATION_URL, LOGGER, ) -from .gateway import DeconzGateway, get_gateway_from_config_entry +from .gateway import DeconzGateway DECONZ_MANUFACTURERURL = "http://www.dresden-elektronik.de" CONF_SERIAL = "serial" @@ -298,7 +301,6 @@ class DeconzOptionsFlowHandler(OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the deCONZ options.""" - self.gateway = get_gateway_from_config_entry(self.hass, self.config_entry) return await self.async_step_deconz_devices() async def async_step_deconz_devices( @@ -309,22 +311,20 @@ class DeconzOptionsFlowHandler(OptionsFlow): self.options.update(user_input) return self.async_create_entry(title="", data=self.options) + schema_options = {} + for option, default in ( + (CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR), + (CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS), + (CONF_ALLOW_NEW_DEVICES, DEFAULT_ALLOW_NEW_DEVICES), + ): + schema_options[ + vol.Optional( + option, + default=self.config_entry.options.get(option, default), + ) + ] = bool + return self.async_show_form( step_id="deconz_devices", - data_schema=vol.Schema( - { - vol.Optional( - CONF_ALLOW_CLIP_SENSOR, - default=self.gateway.option_allow_clip_sensor, - ): bool, - vol.Optional( - CONF_ALLOW_DECONZ_GROUPS, - default=self.gateway.option_allow_deconz_groups, - ): bool, - vol.Optional( - CONF_ALLOW_NEW_DEVICES, - default=self.gateway.option_allow_new_devices, - ): bool, - } - ), + data_schema=vol.Schema(schema_options), ) From b3e97577fd5634a9e871f491be9f0a7aa32cc5a9 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Fri, 29 Apr 2022 19:09:21 -0400 Subject: [PATCH 0040/3516] Patch Insteon Hub connectivity issues (#71081) --- homeassistant/components/insteon/manifest.json | 11 +++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index ad5736f4d53..fc8ade1ba4d 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -4,11 +4,18 @@ "documentation": "https://www.home-assistant.io/integrations/insteon", "dependencies": ["http", "websocket_api"], "requirements": [ - "pyinsteon==1.1.0b1", + "pyinsteon==1.1.0b3", "insteon-frontend-home-assistant==0.1.0" ], "codeowners": ["@teharris1"], - "dhcp": [{ "macaddress": "000EF3*" }, { "registered_devices": true }], + "dhcp": [ + { + "macaddress": "000EF3*" + }, + { + "registered_devices": true + } + ], "config_flow": true, "iot_class": "local_push", "loggers": ["pyinsteon", "pypubsub"], diff --git a/requirements_all.txt b/requirements_all.txt index da39ee6ce91..bccbb32b0fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1547,7 +1547,7 @@ pyialarm==1.9.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.0b1 +pyinsteon==1.1.0b3 # homeassistant.components.intesishome pyintesishome==1.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bb5f6f43234..3d7f9acc0a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1029,7 +1029,7 @@ pyialarm==1.9.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.0b1 +pyinsteon==1.1.0b3 # homeassistant.components.ipma pyipma==2.0.5 From 05d3ca7e02b950ddfb6d3f3cb0f75fcf475fded5 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 30 Apr 2022 00:34:33 +0200 Subject: [PATCH 0041/3516] Fix linking issue when deCONZ gateway is not unlocked (#71082) --- homeassistant/components/deconz/config_flow.py | 5 ++++- homeassistant/components/deconz/strings.json | 1 + .../components/deconz/translations/en.json | 1 + tests/components/deconz/test_config_flow.py | 16 +++++++++++++--- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 806ac72c973..af37ee96878 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -8,7 +8,7 @@ from typing import Any, cast from urllib.parse import urlparse import async_timeout -from pydeconz.errors import RequestError, ResponseError +from pydeconz.errors import LinkButtonNotPressed, RequestError, ResponseError from pydeconz.gateway import DeconzSession from pydeconz.utils import ( DiscoveredBridge, @@ -160,6 +160,9 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): async with async_timeout.timeout(10): api_key = await deconz_session.get_api_key() + except LinkButtonNotPressed: + errors["base"] = "linking_not_possible" + except (ResponseError, RequestError, asyncio.TimeoutError): errors["base"] = "no_key" diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 0fa929f9e63..4098951d714 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -23,6 +23,7 @@ } }, "error": { + "linking_not_possible": "Couldn't link with the gateway", "no_key": "Couldn't get an API key" }, "abort": { diff --git a/homeassistant/components/deconz/translations/en.json b/homeassistant/components/deconz/translations/en.json index a8e09fd20d7..16034e12414 100644 --- a/homeassistant/components/deconz/translations/en.json +++ b/homeassistant/components/deconz/translations/en.json @@ -9,6 +9,7 @@ "updated_instance": "Updated deCONZ instance with new host address" }, "error": { + "linking_not_possible": "Couldn't link with the gateway", "no_key": "Couldn't get an API key" }, "flow_title": "{host}", diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 8ed1e348fee..97b2f7ee164 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -4,6 +4,7 @@ import asyncio from unittest.mock import patch import pydeconz +import pytest from homeassistant.components import ssdp from homeassistant.components.deconz.config_flow import ( @@ -341,7 +342,16 @@ async def test_manual_configuration_timeout_get_bridge(hass, aioclient_mock): assert result["reason"] == "no_bridges" -async def test_link_get_api_key_ResponseError(hass, aioclient_mock): +@pytest.mark.parametrize( + "raised_error, error_string", + [ + (pydeconz.errors.LinkButtonNotPressed, "linking_not_possible"), + (asyncio.TimeoutError, "no_key"), + (pydeconz.errors.ResponseError, "no_key"), + (pydeconz.errors.RequestError, "no_key"), + ], +) +async def test_link_step_fails(hass, aioclient_mock, raised_error, error_string): """Test config flow should abort if no API key was possible to retrieve.""" aioclient_mock.get( pydeconz.utils.URL_DISCOVER, @@ -360,7 +370,7 @@ async def test_link_get_api_key_ResponseError(hass, aioclient_mock): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "link" - aioclient_mock.post("http://1.2.3.4:80/api", exc=pydeconz.errors.ResponseError) + aioclient_mock.post("http://1.2.3.4:80/api", exc=raised_error) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -368,7 +378,7 @@ async def test_link_get_api_key_ResponseError(hass, aioclient_mock): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "link" - assert result["errors"] == {"base": "no_key"} + assert result["errors"] == {"base": error_string} async def test_reauth_flow_update_configuration(hass, aioclient_mock): From 4b860452565012cb4d7dc5903537d1b472d813a4 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 29 Apr 2022 18:35:53 -0400 Subject: [PATCH 0042/3516] Fix ZHA cover initial state (#71083) --- .../components/zha/core/channels/security.py | 12 ++++++------ homeassistant/components/zha/core/gateway.py | 4 ++-- homeassistant/components/zha/entity.py | 6 ++---- homeassistant/components/zha/select.py | 6 ------ 4 files changed, 10 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 19be861178f..0e1e0f8e8a3 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -84,7 +84,7 @@ class IasAce(ZigbeeChannel): @callback def cluster_command(self, tsn, command_id, args) -> None: """Handle commands received to this cluster.""" - self.warning( + self.debug( "received command %s", self._cluster.server_commands[command_id].name ) self.command_map[command_id](*args) @@ -120,7 +120,7 @@ class IasAce(ZigbeeChannel): code != self.panel_code and self.armed_state != AceCluster.PanelStatus.Panel_Disarmed ): - self.warning("Invalid code supplied to IAS ACE") + self.debug("Invalid code supplied to IAS ACE") self.invalid_tries += 1 zigbee_reply = self.arm_response( AceCluster.ArmNotification.Invalid_Arm_Disarm_Code @@ -131,12 +131,12 @@ class IasAce(ZigbeeChannel): self.armed_state == AceCluster.PanelStatus.Panel_Disarmed and self.alarm_status == AceCluster.AlarmStatus.No_Alarm ): - self.warning("IAS ACE already disarmed") + self.debug("IAS ACE already disarmed") zigbee_reply = self.arm_response( AceCluster.ArmNotification.Already_Disarmed ) else: - self.warning("Disarming all IAS ACE zones") + self.debug("Disarming all IAS ACE zones") zigbee_reply = self.arm_response( AceCluster.ArmNotification.All_Zones_Disarmed ) @@ -177,12 +177,12 @@ class IasAce(ZigbeeChannel): ) -> None: """Arm the panel with the specified statuses.""" if self.code_required_arm_actions and code != self.panel_code: - self.warning("Invalid code supplied to IAS ACE") + self.debug("Invalid code supplied to IAS ACE") zigbee_reply = self.arm_response( AceCluster.ArmNotification.Invalid_Arm_Disarm_Code ) else: - self.warning("Arming all IAS ACE zones") + self.debug("Arming all IAS ACE zones") self.armed_state = panel_status zigbee_reply = self.arm_response(armed_type) return zigbee_reply diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 1280f3defe3..0ba375362f7 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -240,14 +240,14 @@ class ZHAGateway: async def async_initialize_devices_and_entities(self) -> None: """Initialize devices and load entities.""" - _LOGGER.warning("Loading all devices") + _LOGGER.debug("Loading all devices") await asyncio.gather( *(dev.async_initialize(from_cache=True) for dev in self.devices.values()) ) async def fetch_updated_state() -> None: """Fetch updated state for mains powered devices.""" - _LOGGER.warning("Fetching current state for mains powered devices") + _LOGGER.debug("Fetching current state for mains powered devices") await asyncio.gather( *( dev.async_initialize(from_cache=False) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 50ffb8f4fcd..e9ea9ee871a 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -206,10 +206,8 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): signal_override=True, ) - if not self.zha_device.is_mains_powered: - # mains powered devices will get real time state - if last_state := await self.async_get_last_state(): - self.async_restore_last_state(last_state) + if last_state := await self.async_get_last_state(): + self.async_restore_last_state(last_state) self.async_accept_signal( None, diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index b9237e24eef..afa2ee18da9 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -86,12 +86,6 @@ class ZHAEnumSelectEntity(ZhaEntity, SelectEntity): self._channel.data_cache[self._attr_name] = self._enum[option.replace(" ", "_")] self.async_write_ha_state() - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - await super().async_added_to_hass() - if last_state := await self.async_get_last_state(): - self.async_restore_last_state(last_state) - @callback def async_restore_last_state(self, last_state) -> None: """Restore previous state.""" From 2ab19464e54af012772689379558d33dde0ef9aa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Apr 2022 16:09:38 -0700 Subject: [PATCH 0043/3516] Fix /config/server_control redirect (#71084) --- homeassistant/components/frontend/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 55d745eb309..0c540a477ef 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -360,13 +360,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if os.path.isdir(local): hass.http.register_static_path("/local", local, not is_dev) + # Can be removed in 2023 + hass.http.register_redirect("/config/server_control", "/developer-tools/yaml") + hass.http.app.router.register_resource(IndexView(repo_path, hass)) async_register_built_in_panel(hass, "profile") - # Can be removed in 2023 - hass.http.register_redirect("/config/server_control", "/developer-tools/yaml") - async_register_built_in_panel( hass, "developer-tools", From e72b53371cfb923cd3b9eb719245d62741dc9d8f Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Fri, 29 Apr 2022 18:09:08 -0500 Subject: [PATCH 0044/3516] Frontend bump 20220429.0 (#71085) --- 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 c806b21e723..e57f06827a5 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220428.0"], + "requirements": ["home-assistant-frontend==20220429.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bf6ad12b287..0cb893be99d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220428.0 +home-assistant-frontend==20220429.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.1 diff --git a/requirements_all.txt b/requirements_all.txt index bccbb32b0fb..d2de6e8e19b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220428.0 +home-assistant-frontend==20220429.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d7f9acc0a4..e9592a41271 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -580,7 +580,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220428.0 +home-assistant-frontend==20220429.0 # homeassistant.components.home_connect homeconnect==0.7.0 From 1c6e5ea844cb8cf8349b64ffe0198360b1da96ce Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Apr 2022 16:10:40 -0700 Subject: [PATCH 0045/3516] Bumped version to 2022.5.0b4 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c22f8ddc4c0..a5db224ee60 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 3156fca93ec..03b13f004eb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.5.0b3 +version = 2022.5.0b4 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 285fdeb581c485047dd6beb9e9acd6b8798601b9 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 30 Apr 2022 17:21:30 -0700 Subject: [PATCH 0046/3516] Add calendar trigger offsets (#70963) * Add support for calendar trigger offsets * Add offset end test * Update homeassistant/components/calendar/trigger.py Co-authored-by: Martin Hjelmare * Always include offset in trigger data Co-authored-by: Martin Hjelmare --- homeassistant/components/calendar/trigger.py | 37 +++++++--- tests/components/calendar/test_trigger.py | 78 +++++++++++++++++++- 2 files changed, 101 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/calendar/trigger.py b/homeassistant/components/calendar/trigger.py index 3540a9f5148..bb6e874b47f 100644 --- a/homeassistant/components/calendar/trigger.py +++ b/homeassistant/components/calendar/trigger.py @@ -11,7 +11,7 @@ from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, ) -from homeassistant.const import CONF_ENTITY_ID, CONF_EVENT, CONF_PLATFORM +from homeassistant.const import CONF_ENTITY_ID, CONF_EVENT, CONF_OFFSET, CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv @@ -36,6 +36,7 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( vol.Required(CONF_PLATFORM): DOMAIN, vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Optional(CONF_EVENT, default=EVENT_START): vol.In({EVENT_START, EVENT_END}), + vol.Optional(CONF_OFFSET, default=datetime.timedelta(0)): cv.time_period, } ) @@ -50,12 +51,14 @@ class CalendarEventListener: trigger_data: dict[str, Any], entity: CalendarEntity, event_type: str, + offset: datetime.timedelta, ) -> None: """Initialize CalendarEventListener.""" self._hass = hass self._job = job self._trigger_data = trigger_data self._entity = entity + self._offset = offset self._unsub_event: CALLBACK_TYPE | None = None self._unsub_refresh: CALLBACK_TYPE | None = None # Upcoming set of events with their trigger time @@ -81,10 +84,18 @@ class CalendarEventListener: async def _fetch_events(self, last_endtime: datetime.datetime) -> None: """Update the set of eligible events.""" + # Use a sliding window for selecting in scope events in the next interval. The event + # search range is offset, then the fire time of the returned events are offset again below. # Event time ranges are exclusive so the end time is expanded by 1sec - end_time = last_endtime + UPDATE_INTERVAL + datetime.timedelta(seconds=1) - _LOGGER.debug("Fetching events between %s, %s", last_endtime, end_time) - events = await self._entity.async_get_events(self._hass, last_endtime, end_time) + start_time = last_endtime - self._offset + end_time = start_time + UPDATE_INTERVAL + datetime.timedelta(seconds=1) + _LOGGER.debug( + "Fetching events between %s, %s (offset=%s)", + start_time, + end_time, + self._offset, + ) + events = await self._entity.async_get_events(self._hass, start_time, end_time) # Build list of events and the appropriate time to trigger an alarm. The # returned events may have already started but matched the start/end time @@ -92,13 +103,14 @@ class CalendarEventListener: # trigger time. event_list = [] for event in events: - event_time = ( + event_fire_time = ( event.start_datetime_local if self._event_type == EVENT_START else event.end_datetime_local ) - if event_time > last_endtime: - event_list.append((event_time, event)) + event_fire_time += self._offset + if event_fire_time > last_endtime: + event_list.append((event_fire_time, event)) event_list.sort(key=lambda x: x[0]) self._events = event_list _LOGGER.debug("Populated event list %s", self._events) @@ -109,12 +121,12 @@ class CalendarEventListener: if not self._events: return - (event_datetime, _event) = self._events[0] - _LOGGER.debug("Scheduling next event trigger @ %s", event_datetime) + (event_fire_time, _event) = self._events[0] + _LOGGER.debug("Scheduled alarm for %s", event_fire_time) self._unsub_event = async_track_point_in_utc_time( self._hass, self._handle_calendar_event, - event_datetime, + event_fire_time, ) def _clear_event_listener(self) -> None: @@ -160,6 +172,7 @@ async def async_attach_trigger( """Attach trigger for the specified calendar.""" entity_id = config[CONF_ENTITY_ID] event_type = config[CONF_EVENT] + offset = config[CONF_OFFSET] component: EntityComponent = hass.data[DOMAIN] if not (entity := component.get_entity(entity_id)) or not isinstance( @@ -173,10 +186,10 @@ async def async_attach_trigger( **automation_info["trigger_data"], "platform": DOMAIN, "event": event_type, + "offset": offset, } - listener = CalendarEventListener( - hass, HassJob(action), trigger_data, entity, event_type + hass, HassJob(action), trigger_data, entity, event_type, offset ) await listener.async_attach() return listener.async_detach diff --git a/tests/components/calendar/test_trigger.py b/tests/components/calendar/test_trigger.py index 65655bc283c..a3b5bacca59 100644 --- a/tests/components/calendar/test_trigger.py +++ b/tests/components/calendar/test_trigger.py @@ -126,13 +126,15 @@ async def setup_calendar(hass: HomeAssistant, fake_schedule: FakeSchedule) -> No await hass.async_block_till_done() -async def create_automation(hass: HomeAssistant, event_type: str) -> None: +async def create_automation(hass: HomeAssistant, event_type: str, offset=None) -> None: """Register an automation.""" trigger_data = { "platform": calendar.DOMAIN, "entity_id": CALENDAR_ENTITY_ID, "event": event_type, } + if offset: + trigger_data["offset"] = offset assert await async_setup_component( hass, automation.DOMAIN, @@ -178,7 +180,43 @@ async def test_event_start_trigger(hass, calls, fake_schedule): assert len(calls()) == 0 await fake_schedule.fire_until( - datetime.datetime.fromisoformat("2022-04-19 11:15:00+00:00") + datetime.datetime.fromisoformat("2022-04-19 11:15:00+00:00"), + ) + assert calls() == [ + { + "platform": "calendar", + "event": EVENT_START, + "calendar_event": event_data, + } + ] + + +@pytest.mark.parametrize( + "offset_str, offset_delta", + [ + ("-01:00", datetime.timedelta(hours=-1)), + ("+01:00", datetime.timedelta(hours=1)), + ], +) +async def test_event_start_trigger_with_offset( + hass, calls, fake_schedule, offset_str, offset_delta +): + """Test the a calendar trigger based on start time with an offset.""" + event_data = fake_schedule.create_event( + start=datetime.datetime.fromisoformat("2022-04-19 12:00:00+00:00"), + end=datetime.datetime.fromisoformat("2022-04-19 12:30:00+00:00"), + ) + await create_automation(hass, EVENT_START, offset=offset_str) + + # No calls yet + await fake_schedule.fire_until( + datetime.datetime.fromisoformat("2022-04-19 11:55:00+00:00") + offset_delta, + ) + assert len(calls()) == 0 + + # Event has started w/ offset + await fake_schedule.fire_until( + datetime.datetime.fromisoformat("2022-04-19 12:05:00+00:00") + offset_delta, ) assert calls() == [ { @@ -216,6 +254,42 @@ async def test_event_end_trigger(hass, calls, fake_schedule): ] +@pytest.mark.parametrize( + "offset_str, offset_delta", + [ + ("-01:00", datetime.timedelta(hours=-1)), + ("+01:00", datetime.timedelta(hours=1)), + ], +) +async def test_event_end_trigger_with_offset( + hass, calls, fake_schedule, offset_str, offset_delta +): + """Test the a calendar trigger based on end time with an offset.""" + event_data = fake_schedule.create_event( + start=datetime.datetime.fromisoformat("2022-04-19 12:00:00+00:00"), + end=datetime.datetime.fromisoformat("2022-04-19 12:30:00+00:00"), + ) + await create_automation(hass, EVENT_END, offset=offset_str) + + # No calls yet + await fake_schedule.fire_until( + datetime.datetime.fromisoformat("2022-04-19 12:05:00+00:00") + offset_delta, + ) + assert len(calls()) == 0 + + # Event has started w/ offset + await fake_schedule.fire_until( + datetime.datetime.fromisoformat("2022-04-19 12:35:00+00:00") + offset_delta, + ) + assert calls() == [ + { + "platform": "calendar", + "event": EVENT_END, + "calendar_event": event_data, + } + ] + + async def test_calendar_trigger_with_no_events(hass, calls, fake_schedule): """Test a calendar trigger setup with no events.""" From ba386b58413a8f69d205d63976f4f7862e39485a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 1 May 2022 00:28:25 +0000 Subject: [PATCH 0047/3516] [ci skip] Translation update --- .../translations/ca.json | 3 +++ .../translations/de.json | 3 +++ .../translations/el.json | 3 +++ .../translations/en.json | 3 +++ .../translations/fr.json | 3 +++ .../translations/hu.json | 3 +++ .../translations/pt-BR.json | 3 +++ .../translations/ru.json | 3 +++ .../translations/sv.json | 3 +++ .../components/deconz/translations/bg.json | 1 + .../components/deconz/translations/ca.json | 1 + .../components/deconz/translations/de.json | 1 + .../components/deconz/translations/el.json | 1 + .../components/deconz/translations/hu.json | 1 + .../components/deconz/translations/nl.json | 1 + .../components/deconz/translations/pl.json | 1 + .../deconz/translations/zh-Hant.json | 1 + .../components/hue/translations/sv.json | 1 + .../components/isy994/translations/bg.json | 7 +++++++ .../components/isy994/translations/ca.json | 13 ++++++++++-- .../components/isy994/translations/de.json | 13 ++++++++++-- .../components/isy994/translations/el.json | 9 +++++++++ .../components/isy994/translations/fr.json | 13 ++++++++++-- .../components/isy994/translations/hu.json | 13 ++++++++++-- .../components/isy994/translations/nl.json | 13 ++++++++++-- .../components/isy994/translations/pl.json | 13 ++++++++++-- .../components/isy994/translations/pt-BR.json | 13 ++++++++++-- .../components/isy994/translations/ru.json | 9 +++++++++ .../isy994/translations/zh-Hant.json | 13 ++++++++++-- .../components/knx/translations/sv.json | 8 ++++++++ .../media_player/translations/bg.json | 1 + .../media_player/translations/sv.json | 5 +++++ .../rainmachine/translations/bg.json | 2 +- .../components/recorder/translations/ca.json | 8 ++++++++ .../components/recorder/translations/de.json | 8 ++++++++ .../components/recorder/translations/el.json | 8 ++++++++ .../components/recorder/translations/fr.json | 8 ++++++++ .../components/recorder/translations/hu.json | 8 ++++++++ .../recorder/translations/pt-BR.json | 8 ++++++++ .../components/recorder/translations/ru.json | 7 +++++++ .../components/sabnzbd/translations/bg.json | 6 ++++++ .../simplisafe/translations/bg.json | 5 +++++ .../simplisafe/translations/ca.json | 6 ++++-- .../simplisafe/translations/nl.json | 4 ++++ .../simplisafe/translations/pt-BR.json | 2 +- .../components/slimproto/translations/bg.json | 7 +++++++ .../components/sql/translations/bg.json | 20 +++++++++++++++++++ .../components/sql/translations/hu.json | 4 ++++ .../components/sql/translations/nl.json | 2 ++ .../components/sql/translations/pl.json | 4 ++++ .../components/sql/translations/pt-BR.json | 4 ++-- .../components/sql/translations/zh-Hant.json | 4 ++++ .../steam_online/translations/ca.json | 4 +++- .../steam_online/translations/de.json | 4 ++-- .../steam_online/translations/fr.json | 4 ++-- .../steam_online/translations/hu.json | 4 ++-- .../steam_online/translations/pt-BR.json | 4 ++-- .../components/tautulli/translations/ca.json | 7 +++++-- .../components/tautulli/translations/nl.json | 4 +++- .../trafikverket_ferry/translations/ca.json | 5 ++++- .../components/update/translations/sv.json | 9 +++++++++ 61 files changed, 314 insertions(+), 35 deletions(-) create mode 100644 homeassistant/components/application_credentials/translations/ca.json create mode 100644 homeassistant/components/application_credentials/translations/de.json create mode 100644 homeassistant/components/application_credentials/translations/el.json create mode 100644 homeassistant/components/application_credentials/translations/en.json create mode 100644 homeassistant/components/application_credentials/translations/fr.json create mode 100644 homeassistant/components/application_credentials/translations/hu.json create mode 100644 homeassistant/components/application_credentials/translations/pt-BR.json create mode 100644 homeassistant/components/application_credentials/translations/ru.json create mode 100644 homeassistant/components/application_credentials/translations/sv.json create mode 100644 homeassistant/components/recorder/translations/ca.json create mode 100644 homeassistant/components/recorder/translations/de.json create mode 100644 homeassistant/components/recorder/translations/el.json create mode 100644 homeassistant/components/recorder/translations/fr.json create mode 100644 homeassistant/components/recorder/translations/hu.json create mode 100644 homeassistant/components/recorder/translations/pt-BR.json create mode 100644 homeassistant/components/recorder/translations/ru.json create mode 100644 homeassistant/components/slimproto/translations/bg.json create mode 100644 homeassistant/components/sql/translations/bg.json create mode 100644 homeassistant/components/update/translations/sv.json diff --git a/homeassistant/components/application_credentials/translations/ca.json b/homeassistant/components/application_credentials/translations/ca.json new file mode 100644 index 00000000000..073fb242e72 --- /dev/null +++ b/homeassistant/components/application_credentials/translations/ca.json @@ -0,0 +1,3 @@ +{ + "title": "Credencials de l'aplicaci\u00f3" +} \ No newline at end of file diff --git a/homeassistant/components/application_credentials/translations/de.json b/homeassistant/components/application_credentials/translations/de.json new file mode 100644 index 00000000000..95198ef66cc --- /dev/null +++ b/homeassistant/components/application_credentials/translations/de.json @@ -0,0 +1,3 @@ +{ + "title": "Anmeldeinformationen" +} \ No newline at end of file diff --git a/homeassistant/components/application_credentials/translations/el.json b/homeassistant/components/application_credentials/translations/el.json new file mode 100644 index 00000000000..79c30a8b969 --- /dev/null +++ b/homeassistant/components/application_credentials/translations/el.json @@ -0,0 +1,3 @@ +{ + "title": "\u0394\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2" +} \ No newline at end of file diff --git a/homeassistant/components/application_credentials/translations/en.json b/homeassistant/components/application_credentials/translations/en.json new file mode 100644 index 00000000000..2a8755ad9d8 --- /dev/null +++ b/homeassistant/components/application_credentials/translations/en.json @@ -0,0 +1,3 @@ +{ + "title": "Application Credentials" +} \ No newline at end of file diff --git a/homeassistant/components/application_credentials/translations/fr.json b/homeassistant/components/application_credentials/translations/fr.json new file mode 100644 index 00000000000..859fa965023 --- /dev/null +++ b/homeassistant/components/application_credentials/translations/fr.json @@ -0,0 +1,3 @@ +{ + "title": "Informations d'identification de l'application" +} \ No newline at end of file diff --git a/homeassistant/components/application_credentials/translations/hu.json b/homeassistant/components/application_credentials/translations/hu.json new file mode 100644 index 00000000000..d24ea488f08 --- /dev/null +++ b/homeassistant/components/application_credentials/translations/hu.json @@ -0,0 +1,3 @@ +{ + "title": "Alkalmaz\u00e1s hiteles\u00edt\u0151 adatai" +} \ No newline at end of file diff --git a/homeassistant/components/application_credentials/translations/pt-BR.json b/homeassistant/components/application_credentials/translations/pt-BR.json new file mode 100644 index 00000000000..cc21106d6c5 --- /dev/null +++ b/homeassistant/components/application_credentials/translations/pt-BR.json @@ -0,0 +1,3 @@ +{ + "title": "Credenciais do aplicativo" +} \ No newline at end of file diff --git a/homeassistant/components/application_credentials/translations/ru.json b/homeassistant/components/application_credentials/translations/ru.json new file mode 100644 index 00000000000..e0edbab4c74 --- /dev/null +++ b/homeassistant/components/application_credentials/translations/ru.json @@ -0,0 +1,3 @@ +{ + "title": "\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f" +} \ No newline at end of file diff --git a/homeassistant/components/application_credentials/translations/sv.json b/homeassistant/components/application_credentials/translations/sv.json new file mode 100644 index 00000000000..1127f807c9f --- /dev/null +++ b/homeassistant/components/application_credentials/translations/sv.json @@ -0,0 +1,3 @@ +{ + "title": "Autentiseringsuppgifter f\u00f6r applikationer" +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/bg.json b/homeassistant/components/deconz/translations/bg.json index 3fe700efd3e..efba936a8d0 100644 --- a/homeassistant/components/deconz/translations/bg.json +++ b/homeassistant/components/deconz/translations/bg.json @@ -8,6 +8,7 @@ "updated_instance": "\u041e\u0431\u043d\u043e\u0432\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 deCONZ \u0441 \u043d\u043e\u0432 \u0430\u0434\u0440\u0435\u0441" }, "error": { + "linking_not_possible": "\u041d\u0435 \u043c\u043e\u0436\u0430 \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435 \u0441 \u0448\u043b\u044e\u0437\u0430", "no_key": "\u041d\u0435 \u043c\u043e\u0436\u0430 \u0434\u0430 \u0441\u0435 \u043f\u043e\u043b\u0443\u0447\u0438 API \u043a\u043b\u044e\u0447" }, "flow_title": "deCONZ Zigbee \u0448\u043b\u044e\u0437 ({host})", diff --git a/homeassistant/components/deconz/translations/ca.json b/homeassistant/components/deconz/translations/ca.json index 2f839b209eb..2d15801b590 100644 --- a/homeassistant/components/deconz/translations/ca.json +++ b/homeassistant/components/deconz/translations/ca.json @@ -9,6 +9,7 @@ "updated_instance": "S'ha actualitzat la inst\u00e0ncia de deCONZ amb una nova adre\u00e7a" }, "error": { + "linking_not_possible": "No s'ha pogut enlla\u00e7ar amb la passarel\u00b7la", "no_key": "No s'ha pogut obtenir una clau API" }, "flow_title": "{host}", diff --git a/homeassistant/components/deconz/translations/de.json b/homeassistant/components/deconz/translations/de.json index a24dbb44ad4..05fa6ce8b3f 100644 --- a/homeassistant/components/deconz/translations/de.json +++ b/homeassistant/components/deconz/translations/de.json @@ -9,6 +9,7 @@ "updated_instance": "deCONZ-Instanz mit neuer Host-Adresse aktualisiert" }, "error": { + "linking_not_possible": "Es konnte keine Verbindung mit dem Gateway hergestellt werden", "no_key": "Es konnte kein API-Schl\u00fcssel abgerufen werden" }, "flow_title": "{host}", diff --git a/homeassistant/components/deconz/translations/el.json b/homeassistant/components/deconz/translations/el.json index 273d939cc34..18eeaf700b6 100644 --- a/homeassistant/components/deconz/translations/el.json +++ b/homeassistant/components/deconz/translations/el.json @@ -9,6 +9,7 @@ "updated_instance": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03bc\u03ad\u03bd\u03b7 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 deCONZ \u03bc\u03b5 \u03bd\u03ad\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae" }, "error": { + "linking_not_possible": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03b7\u03bd \u03c0\u03cd\u03bb\u03b7", "no_key": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03bb\u03ae\u03c8\u03b7 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd API" }, "flow_title": "{host}", diff --git a/homeassistant/components/deconz/translations/hu.json b/homeassistant/components/deconz/translations/hu.json index 47bb343247c..c92a5dd36cd 100644 --- a/homeassistant/components/deconz/translations/hu.json +++ b/homeassistant/components/deconz/translations/hu.json @@ -9,6 +9,7 @@ "updated_instance": "A deCONZ-p\u00e9ld\u00e1ny \u00faj \u00e1llom\u00e1sc\u00edmmel friss\u00edtve" }, "error": { + "linking_not_possible": "Nem tudott kapcsol\u00f3dni az \u00e1tj\u00e1r\u00f3hoz", "no_key": "API kulcs lek\u00e9r\u00e9se nem siker\u00fclt" }, "flow_title": "{host}", diff --git a/homeassistant/components/deconz/translations/nl.json b/homeassistant/components/deconz/translations/nl.json index 9c83fb806e8..6339ad0fd10 100644 --- a/homeassistant/components/deconz/translations/nl.json +++ b/homeassistant/components/deconz/translations/nl.json @@ -9,6 +9,7 @@ "updated_instance": "DeCONZ-instantie bijgewerkt met nieuw host-adres" }, "error": { + "linking_not_possible": "Kan geen koppeling maken met de gateway", "no_key": "Kon geen API-sleutel ophalen" }, "flow_title": "{host}", diff --git a/homeassistant/components/deconz/translations/pl.json b/homeassistant/components/deconz/translations/pl.json index 945830a5734..7f49a259304 100644 --- a/homeassistant/components/deconz/translations/pl.json +++ b/homeassistant/components/deconz/translations/pl.json @@ -9,6 +9,7 @@ "updated_instance": "Zaktualizowano instancj\u0119 deCONZ o nowy adres hosta" }, "error": { + "linking_not_possible": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z bramk\u0105", "no_key": "Nie mo\u017cna uzyska\u0107 klucza API" }, "flow_title": "{host}", diff --git a/homeassistant/components/deconz/translations/zh-Hant.json b/homeassistant/components/deconz/translations/zh-Hant.json index d6d4dfeba45..d2e37ffa710 100644 --- a/homeassistant/components/deconz/translations/zh-Hant.json +++ b/homeassistant/components/deconz/translations/zh-Hant.json @@ -9,6 +9,7 @@ "updated_instance": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0 deCONZ \u88dd\u7f6e" }, "error": { + "linking_not_possible": "\u7121\u6cd5\u8207\u8def\u7531\u5668\u9023\u7dda", "no_key": "\u7121\u6cd5\u53d6\u5f97 API key" }, "flow_title": "{host}", diff --git a/homeassistant/components/hue/translations/sv.json b/homeassistant/components/hue/translations/sv.json index 80f7b179692..c4687726357 100644 --- a/homeassistant/components/hue/translations/sv.json +++ b/homeassistant/components/hue/translations/sv.json @@ -6,6 +6,7 @@ "already_in_progress": "Konfigurations fl\u00f6det f\u00f6r bryggan p\u00e5g\u00e5r redan.", "cannot_connect": "Det gick inte att ansluta till bryggan", "discover_timeout": "Det gick inte att uppt\u00e4cka n\u00e5gra Hue-bryggor", + "invalid_host": "Ogiltig v\u00e4rd", "no_bridges": "Inga Philips Hue-bryggor uppt\u00e4cktes", "not_hue_bridge": "Inte en Hue-brygga", "unknown": "Ett ok\u00e4nt fel intr\u00e4ffade" diff --git a/homeassistant/components/isy994/translations/bg.json b/homeassistant/components/isy994/translations/bg.json index 04a015b0d61..bba26a6cf89 100644 --- a/homeassistant/components/isy994/translations/bg.json +++ b/homeassistant/components/isy994/translations/bg.json @@ -6,10 +6,17 @@ "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", + "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": "{name} ({host})", "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": { "host": "URL", diff --git a/homeassistant/components/isy994/translations/ca.json b/homeassistant/components/isy994/translations/ca.json index cd0a2d2a1de..69c9d02b1bb 100644 --- a/homeassistant/components/isy994/translations/ca.json +++ b/homeassistant/components/isy994/translations/ca.json @@ -7,10 +7,19 @@ "cannot_connect": "[%key::common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key::common::config_flow::error::invalid_auth%]", "invalid_host": "L'entrada de l'amfitri\u00f3 no t\u00e9 el fromat d'URL complet, ex: http://192.168.10.100:80", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "unknown": "Error inesperat" }, "flow_title": "{name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Les credencials de {host} ja no s\u00f3n v\u00e0lides.", + "title": "Re-autenticaci\u00f3 d'ISY" + }, "user": { "data": { "host": "URL", @@ -19,7 +28,7 @@ "username": "[%key::common::config_flow::data::username%]" }, "description": "L'entrada de l'amfitri\u00f3 ha de tenir el format d'URL complet, ex: http://192.168.10.100:80", - "title": "Connexi\u00f3 amb ISY994" + "title": "Connexi\u00f3 amb ISY" } } }, @@ -33,7 +42,7 @@ "variable_sensor_string": "String de Sensor Variable" }, "description": "Configuraci\u00f3 de les opcions per a la integraci\u00f3 ISY: \n \u2022 String de Sensor Node: qualsevol dispositiu o carpeta que contingui 'String de Sensor Node' dins el nom ser\u00e0 tractat com a un sensor o sensor binari. \n \u2022 String Ignora: qualsevol dispositiu amb 'String Ignora' dins el nom ser\u00e0 ignorat. \n \u2022 String de Sensor Variable: qualsevol variable que contingui 'String de Sensor Variable' s'afegir\u00e0 com a un sensor. \n \u2022 Restaura la brillantor de la llum: si est\u00e0 activat, en encendre un llum es restablir\u00e0 la brillantor anterior en lloc del valor integrat dins el dispositiu.", - "title": "Opcions d'ISY994" + "title": "Opcions d'ISY" } } }, diff --git a/homeassistant/components/isy994/translations/de.json b/homeassistant/components/isy994/translations/de.json index a6e9e0a1498..a183e6f7853 100644 --- a/homeassistant/components/isy994/translations/de.json +++ b/homeassistant/components/isy994/translations/de.json @@ -7,10 +7,19 @@ "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_host": "Der Hosteintrag hatte nicht das vollst\u00e4ndige URL-Format, z. B. http://192.168.10.100:80", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "unknown": "Unerwarteter Fehler" }, "flow_title": "{name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Die Anmeldedaten f\u00fcr {host} sind nicht mehr g\u00fcltig.", + "title": "Reauthentifizierung deines ISY" + }, "user": { "data": { "host": "URL", @@ -19,7 +28,7 @@ "username": "Benutzername" }, "description": "Der Hosteintrag muss im vollst\u00e4ndigen URL-Format vorliegen, z. B. http://192.168.10.100:80", - "title": "Stelle eine Verbindung zu deinem ISY994 her" + "title": "Verbindung zu deinem ISY" } } }, @@ -33,7 +42,7 @@ "variable_sensor_string": "Variabler Sensor String" }, "description": "Stelle die Optionen f\u00fcr die ISY-Integration ein: \n - Node Sensor String: Jedes Ger\u00e4t oder jeder Ordner, der 'Node Sensor String' im Namen enth\u00e4lt, wird als Sensor oder bin\u00e4rer Sensor behandelt. \n - String ignorieren: Jedes Ger\u00e4t mit 'Ignore String' im Namen wird ignoriert. \n - Variable Sensor Zeichenfolge: Jede Variable, die 'Variable Sensor String' im Namen enth\u00e4lt, wird als Sensor hinzugef\u00fcgt. \n - Lichthelligkeit wiederherstellen: Wenn diese Option aktiviert ist, wird beim Einschalten eines Lichts die vorherige Helligkeit wiederhergestellt und nicht der integrierte Ein-Pegel des Ger\u00e4ts.", - "title": "ISY994 Optionen" + "title": "ISY-Optionen" } } }, diff --git a/homeassistant/components/isy994/translations/el.json b/homeassistant/components/isy994/translations/el.json index 470975e2117..ec32ea756bc 100644 --- a/homeassistant/components/isy994/translations/el.json +++ b/homeassistant/components/isy994/translations/el.json @@ -7,10 +7,19 @@ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_auth": "v", "invalid_host": "\u0397 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03b4\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03c3\u03b5 \u03c0\u03bb\u03ae\u03c1\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae URL, \u03c0.\u03c7. http://192.168.10.100:80", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "flow_title": "{name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "description": "\u03a4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03bf {host} \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03ad\u03b3\u03ba\u03c5\u03c1\u03b1.", + "title": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf ISY \u03c3\u03b1\u03c2" + }, "user": { "data": { "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", diff --git a/homeassistant/components/isy994/translations/fr.json b/homeassistant/components/isy994/translations/fr.json index c9e53845dfb..29a3c6e54fe 100644 --- a/homeassistant/components/isy994/translations/fr.json +++ b/homeassistant/components/isy994/translations/fr.json @@ -7,10 +7,19 @@ "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification non valide", "invalid_host": "L'entr\u00e9e d'h\u00f4te n'\u00e9tait pas au format URL complet, par exemple http://192.168.10.100:80", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", "unknown": "Erreur inattendue" }, "flow_title": "{name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "description": "Les informations d'identification pour {host} ne sont plus valides.", + "title": "R\u00e9authentifiez votre ISY" + }, "user": { "data": { "host": "URL", @@ -19,7 +28,7 @@ "username": "Nom d'utilisateur" }, "description": "L'entr\u00e9e d'h\u00f4te doit \u00eatre au format URL complet, par exemple, http://192.168.10.100:80", - "title": "Connect\u00e9 \u00e0 votre ISY994" + "title": "Connexion \u00e0 votre ISY" } } }, @@ -33,7 +42,7 @@ "variable_sensor_string": "Cha\u00eene de capteur variable" }, "description": "D\u00e9finir les options pour l'int\u00e9gration ISY: \n \u2022 Node Sensor String: tout p\u00e9riph\u00e9rique ou dossier contenant \u00abNode Sensor String\u00bb dans le nom sera trait\u00e9 comme un capteur ou un capteur binaire. \n \u2022 Ignore String : tout p\u00e9riph\u00e9rique avec \u00abIgnore String\u00bb dans le nom sera ignor\u00e9. \n \u2022 Variable Sensor String : toute variable contenant \u00abVariable Sensor String\u00bb sera ajout\u00e9e en tant que capteur. \n \u2022 Restaurer la luminosit\u00e9 : si cette option est activ\u00e9e, la luminosit\u00e9 pr\u00e9c\u00e9dente sera restaur\u00e9e lors de l'allumage d'une lumi\u00e8re au lieu de la fonction int\u00e9gr\u00e9e de l'appareil.", - "title": "Options ISY994" + "title": "Options ISY" } } }, diff --git a/homeassistant/components/isy994/translations/hu.json b/homeassistant/components/isy994/translations/hu.json index d9cce2fefcb..e5a5c4d6f98 100644 --- a/homeassistant/components/isy994/translations/hu.json +++ b/homeassistant/components/isy994/translations/hu.json @@ -7,10 +7,19 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_host": "A c\u00edm bejegyz\u00e9se nem volt teljes URL-form\u00e1tumban, p\u00e9ld\u00e1ul: http://192.168.10.100:80", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "flow_title": "{name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "{host} hiteles\u00edt\u0151 adatai m\u00e1r nem \u00e9rv\u00e9nyesek.", + "title": "ISY \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "host": "URL", @@ -19,7 +28,7 @@ "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, "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" + "title": "Csatlakozzon az ISY-hez" } } }, @@ -33,7 +42,7 @@ "variable_sensor_string": "V\u00e1ltoz\u00f3 \u00e9rz\u00e9kel\u0151 karakterl\u00e1nc" }, "description": "\u00c1ll\u00edtsa be az ISY integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sait:\n \u2022 Csom\u00f3pont -\u00e9rz\u00e9kel\u0151 karakterl\u00e1nc: B\u00e1rmely eszk\u00f6z vagy mappa, amelynek nev\u00e9ben \u201eNode Sensor String\u201d szerepel, \u00e9rz\u00e9kel\u0151k\u00e9nt vagy bin\u00e1ris \u00e9rz\u00e9kel\u0151k\u00e9nt fog kezelni.\n \u2022 Karakterl\u00e1nc figyelmen k\u00edv\u00fcl hagy\u00e1sa: Minden olyan eszk\u00f6z, amelynek a neve \u201eIgnore String\u201d, figyelmen k\u00edv\u00fcl marad.\n \u2022 V\u00e1ltoz\u00f3 \u00e9rz\u00e9kel\u0151 karakterl\u00e1nc: B\u00e1rmely v\u00e1ltoz\u00f3, amely tartalmazza a \u201eV\u00e1ltoz\u00f3 \u00e9rz\u00e9kel\u0151 karakterl\u00e1ncot\u201d, hozz\u00e1ad\u00f3dik \u00e9rz\u00e9kel\u0151k\u00e9nt.\n \u2022 F\u00e9nyer\u0151ss\u00e9g vissza\u00e1ll\u00edt\u00e1sa: Ha enged\u00e9lyezve van, akkor az el\u0151z\u0151 f\u00e9nyer\u0151 vissza\u00e1ll, amikor a k\u00e9sz\u00fcl\u00e9ket be\u00e9p\u00edtett On-Level helyett bekapcsolja.", - "title": "ISY994 Be\u00e1ll\u00edt\u00e1sok" + "title": "ISY be\u00e1ll\u00edt\u00e1sok" } } }, diff --git a/homeassistant/components/isy994/translations/nl.json b/homeassistant/components/isy994/translations/nl.json index c75cdf86473..580bd307a5c 100644 --- a/homeassistant/components/isy994/translations/nl.json +++ b/homeassistant/components/isy994/translations/nl.json @@ -7,10 +7,19 @@ "cannot_connect": "Kon niet verbinden", "invalid_auth": "Ongeldige authenticatie", "invalid_host": "De hostvermelding had de niet volledig URL-indeling, bijvoorbeeld http://192.168.10.100:80", + "reauth_successful": "Herauthenticatie was succesvol", "unknown": "Onverwachte fout" }, "flow_title": "{name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "De inloggegevens voor {host} zijn niet langer geldig.", + "title": "Herautoriseer uw ISY" + }, "user": { "data": { "host": "URL", @@ -19,7 +28,7 @@ "username": "Gebruikersnaam" }, "description": "Het hostvermelding moet de volledige URL-indeling hebben, bijvoorbeeld http://192.168.10.100:80", - "title": "Maak verbinding met uw ISY994" + "title": "Maak verbinding met uw ISY" } } }, @@ -33,7 +42,7 @@ "variable_sensor_string": "Variabele sensor string" }, "description": "Stel de opties in voor de ISY-integratie:\n \u2022 Node Sensor String: elk apparaat of elke map die 'Node Sensor String' in de naam bevat, wordt behandeld als een sensor of binaire sensor.\n \u2022 Ignore String: elk apparaat met 'Ignore String' in de naam wordt genegeerd.\n \u2022 Variabele sensorreeks: elke variabele die 'Variabele sensorreeks' bevat, wordt als sensor toegevoegd.\n \u2022 Lichthelderheid herstellen: indien ingeschakeld, wordt de vorige helderheid hersteld wanneer u een lamp inschakelt in plaats van het ingebouwde Aan-niveau van het apparaat.", - "title": "ISY994-opties" + "title": "ISY-opties" } } }, diff --git a/homeassistant/components/isy994/translations/pl.json b/homeassistant/components/isy994/translations/pl.json index 4deeefe391d..a1ca157bd14 100644 --- a/homeassistant/components/isy994/translations/pl.json +++ b/homeassistant/components/isy994/translations/pl.json @@ -7,10 +7,19 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", "invalid_host": "Wpis hosta nie by\u0142 w pe\u0142nym formacie URL, np. http://192.168.10.100:80", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "flow_title": "{name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Dane uwierzytelniaj\u0105ce dla {host} nie s\u0105 ju\u017c wa\u017cne.", + "title": "Ponownie uwierzytelnij ISY" + }, "user": { "data": { "host": "URL", @@ -19,7 +28,7 @@ "username": "Nazwa u\u017cytkownika" }, "description": "Wpis hosta musi by\u0107 w pe\u0142nym formacie URL, np. http://192.168.10.100:80.", - "title": "Po\u0142\u0105czenie z ISY994" + "title": "Po\u0142\u0105czenie z ISY" } } }, @@ -33,7 +42,7 @@ "variable_sensor_string": "Ci\u0105g zmiennej sensora" }, "description": "Ustaw opcje dla integracji ISY: \n \u2022 Ci\u0105g sensora w\u0119z\u0142a: ka\u017cde urz\u0105dzenie lub folder, kt\u00f3ry zawiera w nazwie ci\u0105g sensora w\u0119z\u0142a, b\u0119dzie traktowane jako sensor lub sensor binarny. \n \u2022 Ci\u0105g ignorowania: ka\u017cde urz\u0105dzenie z 'ci\u0105giem ignorowania' w nazwie zostanie zignorowane. \n \u2022 Ci\u0105g sensora zmiennej: ka\u017cda zmienna zawieraj\u0105ca 'ci\u0105g sensora zmiennej' zostanie dodana jako sensor. \n \u2022 Przywr\u00f3\u0107 jasno\u015b\u0107 \u015bwiat\u0142a: je\u015bli ta opcja jest w\u0142\u0105czona, poprzednia warto\u015b\u0107 jasno\u015bci zostanie przywr\u00f3cona po w\u0142\u0105czeniu \u015bwiat\u0142a zamiast domy\u015blnej warto\u015bci jasno\u015bci dla urz\u0105dzenia.", - "title": "Opcje ISY994" + "title": "Opcje ISY" } } }, diff --git a/homeassistant/components/isy994/translations/pt-BR.json b/homeassistant/components/isy994/translations/pt-BR.json index b644b6c1bfc..7b1d8e796ba 100644 --- a/homeassistant/components/isy994/translations/pt-BR.json +++ b/homeassistant/components/isy994/translations/pt-BR.json @@ -7,10 +7,19 @@ "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_host": "A entrada do host n\u00e3o est\u00e1 no formato de URL completo, por exemplo, http://192.168.10.100:80", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", "unknown": "Erro inesperado" }, "flow_title": "{name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "As credenciais para {host} n\u00e3o s\u00e3o mais v\u00e1lidas.", + "title": "Reautentique seu ISY" + }, "user": { "data": { "host": "URL", @@ -19,7 +28,7 @@ "username": "Usu\u00e1rio" }, "description": "A entrada do endere\u00e7o deve estar no formato de URL completo, por exemplo, http://192.168.10.100:80", - "title": "Conecte-se ao seu ISY994" + "title": "Conecte-se seu ISY" } } }, @@ -33,7 +42,7 @@ "variable_sensor_string": "Texto da vari\u00e1vel do sensor" }, "description": "Defina as op\u00e7\u00f5es para a Integra\u00e7\u00e3o ISY:\n \u2022 Cadeia de Sensores de N\u00f3: Qualquer dispositivo ou pasta que contenha 'Cadeia de Sensores de N\u00f3' no nome ser\u00e1 tratado como um sensor ou sensor bin\u00e1rio.\n \u2022 Ignore String: Qualquer dispositivo com 'Ignore String' no nome ser\u00e1 ignorado.\n \u2022 Variable Sensor String: Qualquer vari\u00e1vel que contenha 'Variable Sensor String' ser\u00e1 adicionada como sensor.\n \u2022 Restaurar o brilho da luz: Se ativado, o brilho anterior ser\u00e1 restaurado ao acender uma luz em vez do n\u00edvel integrado do dispositivo.", - "title": "ISY994 Op\u00e7\u00f5es" + "title": "Op\u00e7\u00f5es ISY" } } }, diff --git a/homeassistant/components/isy994/translations/ru.json b/homeassistant/components/isy994/translations/ru.json index a5dccc79ddd..61e2c79831a 100644 --- a/homeassistant/components/isy994/translations/ru.json +++ b/homeassistant/components/isy994/translations/ru.json @@ -7,10 +7,19 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "invalid_host": "URL-\u0430\u0434\u0440\u0435\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 'http://192.168.10.100:80').", + "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." }, "flow_title": "{name} ({host})", "step": { + "reauth_confirm": { + "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": "\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f {host} \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0412\u0430\u0448\u0435\u0433\u043e ISY" + }, "user": { "data": { "host": "URL-\u0430\u0434\u0440\u0435\u0441", diff --git a/homeassistant/components/isy994/translations/zh-Hant.json b/homeassistant/components/isy994/translations/zh-Hant.json index 6370d7bbbf8..c22bbd1b58d 100644 --- a/homeassistant/components/isy994/translations/zh-Hant.json +++ b/homeassistant/components/isy994/translations/zh-Hant.json @@ -7,10 +7,19 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "invalid_host": "\u4e3b\u6a5f\u7aef\u4e26\u672a\u4ee5\u5b8c\u6574\u7db2\u5740\u683c\u5f0f\u8f38\u5165\uff0c\u4f8b\u5982\uff1ahttp://192.168.10.100:80", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "flow_title": "{name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "{username} \u6191\u8b49\u4e0d\u518d\u6709\u6548\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49 ISY \u5e33\u865f" + }, "user": { "data": { "host": "\u7db2\u5740", @@ -19,7 +28,7 @@ "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "description": "\u4e3b\u6a5f\u7aef\u5fc5\u9808\u4ee5\u5b8c\u6574\u7db2\u5740\u683c\u5f0f\u8f38\u5165\uff0c\u4f8b\u5982\uff1ahttp://192.168.10.100:80", - "title": "\u9023\u7dda\u81f3 ISY994" + "title": "\u9023\u7dda\u81f3 ISY" } } }, @@ -33,7 +42,7 @@ "variable_sensor_string": "\u53ef\u8b8a\u611f\u6e2c\u5668\u5b57\u4e32" }, "description": "ISY \u6574\u5408\u8a2d\u5b9a\u9078\u9805\uff1a \n \u2022 \u7bc0\u9ede\u611f\u6e2c\u5668\u5b57\u4e32\uff08Node Sensor String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u6216\u8cc7\u6599\u593e\u5305\u542b\u300cNode Sensor String\u300d\u7684\u88dd\u7f6e\u90fd\u6703\u88ab\u8996\u70ba\u611f\u6e2c\u5668\u6216\u4e8c\u9032\u4f4d\u611f\u6e2c\u5668\u3002\n \u2022 \u5ffd\u7565\u5b57\u4e32\uff08Ignore String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u5305\u542b\u300cIgnore String\u300d\u7684\u88dd\u7f6e\u90fd\u6703\u88ab\u5ffd\u7565\u3002\n \u2022 \u53ef\u8b8a\u611f\u6e2c\u5668\u5b57\u4e32\uff08Variable Sensor String\uff09\uff1a\u4efb\u4f55\u5305\u542b\u300cVariable Sensor String\u300d\u7684\u8b8a\u6578\u90fd\u5c07\u65b0\u589e\u70ba\u611f\u6e2c\u5668\u3002 \n \u2022 \u56de\u5fa9\u4eae\u5ea6\uff08Restore Light Brightness\uff09\uff1a\u958b\u5553\u5f8c\u3001\u7576\u71c8\u5149\u958b\u555f\u6642\u6703\u56de\u5fa9\u5148\u524d\u7684\u4eae\u5ea6\uff0c\u800c\u4e0d\u662f\u4f7f\u7528\u88dd\u7f6e\u9810\u8a2d\u4eae\u5ea6\u3002", - "title": "ISY994 \u9078\u9805" + "title": "ISY \u9078\u9805" } } }, diff --git a/homeassistant/components/knx/translations/sv.json b/homeassistant/components/knx/translations/sv.json index b1be9557565..de5def120c9 100644 --- a/homeassistant/components/knx/translations/sv.json +++ b/homeassistant/components/knx/translations/sv.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "invalid_individual_address": "V\u00e4rdet matchar inte m\u00f6nstret f\u00f6r en individuell adress i KNX.\n'area.line.device'", + "invalid_ip_address": "Ogiltig IPv4-adress." + }, "step": { "manual_tunnel": { "data": { @@ -13,6 +17,10 @@ "tunnel": { "data": { "tunneling_type": "KNX tunneltyp" + }, + "data_description": { + "host": "IP adress till KNX/IP tunnelingsenhet.", + "port": "Port p\u00e5 KNX/IP tunnelingsenhet." } } } diff --git a/homeassistant/components/media_player/translations/bg.json b/homeassistant/components/media_player/translations/bg.json index 408f3c1e682..ddc7ab9ee6b 100644 --- a/homeassistant/components/media_player/translations/bg.json +++ b/homeassistant/components/media_player/translations/bg.json @@ -10,6 +10,7 @@ }, "state": { "_": { + "buffering": "\u0411\u0443\u0444\u0435\u0440\u0438\u0440\u0430\u043d\u0435", "idle": "\u041d\u0435\u0440\u0430\u0431\u043e\u0442\u0435\u0449", "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d", "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d", diff --git a/homeassistant/components/media_player/translations/sv.json b/homeassistant/components/media_player/translations/sv.json index 95f098f2f3e..6aae6d6b208 100644 --- a/homeassistant/components/media_player/translations/sv.json +++ b/homeassistant/components/media_player/translations/sv.json @@ -1,15 +1,20 @@ { "device_automation": { "condition_type": { + "is_buffering": "{entity_name} buffrar", "is_idle": "{entity_name} \u00e4r inaktiv", "is_off": "{entity_name} \u00e4r avst\u00e4ngd", "is_on": "{entity_name} \u00e4r p\u00e5", "is_paused": "{entity_name} \u00e4r pausad", "is_playing": "{entity_name} spelar" + }, + "trigger_type": { + "buffering": "{entity_name} b\u00f6rjar buffra" } }, "state": { "_": { + "buffering": "Buffrar", "idle": "Inaktiv", "off": "Av", "on": "P\u00e5", diff --git a/homeassistant/components/rainmachine/translations/bg.json b/homeassistant/components/rainmachine/translations/bg.json index b54660f8e9f..1239915231b 100644 --- a/homeassistant/components/rainmachine/translations/bg.json +++ b/homeassistant/components/rainmachine/translations/bg.json @@ -4,7 +4,7 @@ "step": { "user": { "data": { - "ip_address": "\u0410\u0434\u0440\u0435\u0441", + "ip_address": "\u0418\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "port": "\u041f\u043e\u0440\u0442" }, diff --git a/homeassistant/components/recorder/translations/ca.json b/homeassistant/components/recorder/translations/ca.json new file mode 100644 index 00000000000..b6e5a549ba7 --- /dev/null +++ b/homeassistant/components/recorder/translations/ca.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "current_recorder_run": "Hora d'inici de l'execuci\u00f3 actual", + "oldest_recorder_run": "Hora d'inici de l'execuci\u00f3 m\u00e9s antiga" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/de.json b/homeassistant/components/recorder/translations/de.json new file mode 100644 index 00000000000..823b4ae62bf --- /dev/null +++ b/homeassistant/components/recorder/translations/de.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "current_recorder_run": "Aktuelle Startzeit der Ausf\u00fchrung", + "oldest_recorder_run": "\u00c4lteste Startzeit der Ausf\u00fchrung" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/el.json b/homeassistant/components/recorder/translations/el.json new file mode 100644 index 00000000000..0ede908d151 --- /dev/null +++ b/homeassistant/components/recorder/translations/el.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "current_recorder_run": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03ce\u03c1\u03b1 \u03ad\u03bd\u03b1\u03c1\u03be\u03b7\u03c2 \u03b5\u03ba\u03c4\u03ad\u03bb\u03b5\u03c3\u03b7\u03c2", + "oldest_recorder_run": "\u03a0\u03b1\u03bb\u03b1\u03b9\u03cc\u03c4\u03b5\u03c1\u03b7 \u03ce\u03c1\u03b1 \u03ad\u03bd\u03b1\u03c1\u03be\u03b7\u03c2 \u03b5\u03ba\u03c4\u03ad\u03bb\u03b5\u03c3\u03b7\u03c2" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/fr.json b/homeassistant/components/recorder/translations/fr.json new file mode 100644 index 00000000000..31673d970f3 --- /dev/null +++ b/homeassistant/components/recorder/translations/fr.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "current_recorder_run": "Heure de d\u00e9marrage de l'ex\u00e9cution actuelle", + "oldest_recorder_run": "Heure de d\u00e9marrage de l'ex\u00e9cution la plus ancienne" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/hu.json b/homeassistant/components/recorder/translations/hu.json new file mode 100644 index 00000000000..4b0cd6067ad --- /dev/null +++ b/homeassistant/components/recorder/translations/hu.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "current_recorder_run": "Aktu\u00e1lis futtat\u00e1s kezd\u00e9si id\u0151pontja", + "oldest_recorder_run": "Legr\u00e9gebbi futtat\u00e1s kezd\u00e9si id\u0151pontja" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/pt-BR.json b/homeassistant/components/recorder/translations/pt-BR.json new file mode 100644 index 00000000000..78d43d14e52 --- /dev/null +++ b/homeassistant/components/recorder/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "current_recorder_run": "Hora atual de in\u00edcio de execu\u00e7\u00e3o", + "oldest_recorder_run": "Hora antiga de in\u00edcio de execu\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/ru.json b/homeassistant/components/recorder/translations/ru.json new file mode 100644 index 00000000000..6e5c928d77b --- /dev/null +++ b/homeassistant/components/recorder/translations/ru.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "oldest_recorder_run": "\u0412\u0440\u0435\u043c\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u0441\u0430\u043c\u043e\u0433\u043e \u0441\u0442\u0430\u0440\u043e\u0433\u043e \u0446\u0438\u043a\u043b\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/bg.json b/homeassistant/components/sabnzbd/translations/bg.json index 02c83a6e916..d3fcfb0789d 100644 --- a/homeassistant/components/sabnzbd/translations/bg.json +++ b/homeassistant/components/sabnzbd/translations/bg.json @@ -1,8 +1,14 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \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": { "user": { "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "name": "\u0418\u043c\u0435", "url": "URL" } } diff --git a/homeassistant/components/simplisafe/translations/bg.json b/homeassistant/components/simplisafe/translations/bg.json index 9d32e18ae5c..56cf0e346f1 100644 --- a/homeassistant/components/simplisafe/translations/bg.json +++ b/homeassistant/components/simplisafe/translations/bg.json @@ -15,6 +15,11 @@ }, "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" }, + "sms_2fa": { + "data": { + "code": "\u041a\u043e\u0434" + } + }, "user": { "data": { "auth_code": "\u041a\u043e\u0434 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f", diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json index a89b06bfcfc..827bd6dceec 100644 --- a/homeassistant/components/simplisafe/translations/ca.json +++ b/homeassistant/components/simplisafe/translations/ca.json @@ -7,6 +7,7 @@ "wrong_account": "Les credencials d'usuari proporcionades no coincideixen amb les d'aquest compte SimpliSafe." }, "error": { + "2fa_timed_out": "S'ha esgotat el temps d'espera m\u00e0xim durant l'autenticaci\u00f3 de dos factors", "identifier_exists": "Aquest compte ja est\u00e0 registrat", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "still_awaiting_mfa": "Esperant clic de l'enlla\u00e7 del correu MFA", @@ -29,7 +30,8 @@ "sms_2fa": { "data": { "code": "Codi" - } + }, + "description": "Introdueix el codi d'autenticaci\u00f3 de dos factors que s'ha enviat per SMS." }, "user": { "data": { @@ -38,7 +40,7 @@ "password": "Contrasenya", "username": "Nom d'usuari" }, - "description": "SimpliSafe s'autentica amb Home Assistant 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]({docs_url}) abans de comen\u00e7ar.\n\n1. Fes clic [aqu\u00ed]({url}) per obrir l'aplicaci\u00f3 web de SimpliSafe i introdueix les teves credencials.\n\n2. Quan el proc\u00e9s d'inici de sessi\u00f3 s'hagi completat, torna aqu\u00ed i introdueix a sota el codi d'autoritzaci\u00f3.", + "description": "Introdueix el teu nom d'usuari i contrasenya.", "title": "Introdueix la teva informaci\u00f3" } } diff --git a/homeassistant/components/simplisafe/translations/nl.json b/homeassistant/components/simplisafe/translations/nl.json index 33bc10ac85d..5ed0292b2d0 100644 --- a/homeassistant/components/simplisafe/translations/nl.json +++ b/homeassistant/components/simplisafe/translations/nl.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Dit SimpliSafe-account is al in gebruik.", + "email_2fa_timed_out": "Timed out tijdens het wachten op email-gebaseerde twee-factor authenticatie.", "reauth_successful": "Herauthenticatie was succesvol", "wrong_account": "De opgegeven gebruikersgegevens komen niet overeen met deze SimpliSafe-account." }, @@ -12,6 +13,9 @@ "still_awaiting_mfa": "Wacht nog steeds op MFA-e-mailklik", "unknown": "Onverwachte fout" }, + "progress": { + "email_2fa": "Voer de twee-factor authenticatie code in\ndie u per e-mail is toegezonden." + }, "step": { "mfa": { "title": "SimpliSafe Multi-Factor Authenticatie" diff --git a/homeassistant/components/simplisafe/translations/pt-BR.json b/homeassistant/components/simplisafe/translations/pt-BR.json index 596f1f441e6..5257950027b 100644 --- a/homeassistant/components/simplisafe/translations/pt-BR.json +++ b/homeassistant/components/simplisafe/translations/pt-BR.json @@ -24,7 +24,7 @@ "data": { "password": "Senha" }, - "description": "Por favor, digite novamente a senha para {username} .", + "description": "Por favor, digite novamente a senha para {username}.", "title": "Reautenticar Integra\u00e7\u00e3o" }, "sms_2fa": { diff --git a/homeassistant/components/slimproto/translations/bg.json b/homeassistant/components/slimproto/translations/bg.json new file mode 100644 index 00000000000..f49a3911eab --- /dev/null +++ b/homeassistant/components/slimproto/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\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/sql/translations/bg.json b/homeassistant/components/sql/translations/bg.json new file mode 100644 index 00000000000..b6c3577d1d0 --- /dev/null +++ b/homeassistant/components/sql/translations/bg.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u0418\u043c\u0435" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/hu.json b/homeassistant/components/sql/translations/hu.json index cf125215ae3..c7f8b02a2b6 100644 --- a/homeassistant/components/sql/translations/hu.json +++ b/homeassistant/components/sql/translations/hu.json @@ -13,6 +13,7 @@ "data": { "column": "Oszlop", "db_url": "Adatb\u00e1zis URL", + "name": "Elnevez\u00e9s", "query": "Lek\u00e9rdez\u00e9s kijel\u00f6l\u00e9se", "unit_of_measurement": "M\u00e9rt\u00e9kegys\u00e9g", "value_template": "\u00c9rt\u00e9ksablon" @@ -20,6 +21,7 @@ "data_description": { "column": "\u00c1llapotk\u00e9nt megjelen\u00edtend\u0151, lek\u00e9rdezett oszlop", "db_url": "Adatb\u00e1zis URL-c\u00edme, hagyja \u00fcresen az alap\u00e9rtelmezett HA-adatb\u00e1zis haszn\u00e1lat\u00e1hoz", + "name": "A konfigur\u00e1ci\u00f3s bejegyz\u00e9shez \u00e9s az \u00e9rz\u00e9kel\u0151h\u00f6z haszn\u00e1lt n\u00e9v", "query": "A futtatand\u00f3 lek\u00e9rdez\u00e9snek 'SELECT'-el kell kezd\u0151dnie.", "unit_of_measurement": "M\u00e9rt\u00e9kegys\u00e9g (nem k\u00f6telez\u0151)", "value_template": "\u00c9rt\u00e9ksablon (nem k\u00f6telez\u0151)" @@ -38,6 +40,7 @@ "data": { "column": "Oszlop", "db_url": "Adatb\u00e1zis URL", + "name": "Elnevez\u00e9s", "query": "Lek\u00e9rdez\u00e9s kijel\u00f6l\u00e9se", "unit_of_measurement": "M\u00e9rt\u00e9kegys\u00e9g", "value_template": "\u00c9rt\u00e9ksablon" @@ -45,6 +48,7 @@ "data_description": { "column": "\u00c1llapotk\u00e9nt megjelen\u00edtend\u0151, lek\u00e9rdezett oszlop", "db_url": "Adatb\u00e1zis URL-c\u00edme, hagyja \u00fcresen az alap\u00e9rtelmezett HA-adatb\u00e1zis haszn\u00e1lat\u00e1hoz", + "name": "A konfigur\u00e1ci\u00f3s bejegyz\u00e9shez \u00e9s az \u00e9rz\u00e9kel\u0151h\u00f6z haszn\u00e1lt n\u00e9v", "query": "A futtatand\u00f3 lek\u00e9rdez\u00e9snek 'SELECT'-el kell kezd\u0151dnie.", "unit_of_measurement": "M\u00e9rt\u00e9kegys\u00e9g (nem k\u00f6telez\u0151)", "value_template": "\u00c9rt\u00e9ksablon (nem k\u00f6telez\u0151)" diff --git a/homeassistant/components/sql/translations/nl.json b/homeassistant/components/sql/translations/nl.json index c95d4b67701..68aa1230432 100644 --- a/homeassistant/components/sql/translations/nl.json +++ b/homeassistant/components/sql/translations/nl.json @@ -21,6 +21,7 @@ "data_description": { "column": "Kolom voor teruggestuurde query om als status te presenteren", "db_url": "Database URL, leeg laten om de standaard HA database te gebruiken", + "name": "Naam die zal worden gebruikt voor Config Entry en ook de Sensor", "query": "Query die moet worden uitgevoerd, moet beginnen met 'SELECT'", "unit_of_measurement": "Meeteenheid (optioneel)", "value_template": "Waarde Template (optioneel)" @@ -47,6 +48,7 @@ "data_description": { "column": "Kolom voor teruggestuurde query om als status te presenteren", "db_url": "Database URL, leeg laten om de standaard HA database te gebruiken", + "name": "Naam die zal worden gebruikt voor Config Entry en ook de Sensor", "query": "Query die moet worden uitgevoerd, moet beginnen met 'SELECT'", "unit_of_measurement": "Meeteenheid (optioneel)", "value_template": "Waarde Template (optioneel)" diff --git a/homeassistant/components/sql/translations/pl.json b/homeassistant/components/sql/translations/pl.json index 0e8cb47b148..2527a8239b3 100644 --- a/homeassistant/components/sql/translations/pl.json +++ b/homeassistant/components/sql/translations/pl.json @@ -13,6 +13,7 @@ "data": { "column": "Kolumna", "db_url": "Adres URL bazy danych", + "name": "Nazwa", "query": "Wybierz zapytanie", "unit_of_measurement": "Jednostka miary", "value_template": "Szablon warto\u015bci" @@ -20,6 +21,7 @@ "data_description": { "column": "Kolumna dla zwr\u00f3conego zapytania do przedstawienia jako stan", "db_url": "Adres URL bazy danych, pozostaw puste, aby u\u017cy\u0107 domy\u015blnej bazy danych HA", + "name": "Nazwa, kt\u00f3ra b\u0119dzie u\u017cywana do wprowadzania konfiguracji, a tak\u017ce do sensora", "query": "Zapytanie do uruchomienia, musi zaczyna\u0107 si\u0119 od \u201eSELECT\u201d", "unit_of_measurement": "Jednostka miary (opcjonalnie)", "value_template": "Szablon warto\u015bci (opcjonalnie)" @@ -38,6 +40,7 @@ "data": { "column": "Kolumna", "db_url": "Adres URL bazy danych", + "name": "Nazwa", "query": "Wybierz zapytanie", "unit_of_measurement": "Jednostka miary", "value_template": "Szablon warto\u015bci" @@ -45,6 +48,7 @@ "data_description": { "column": "Kolumna dla zwr\u00f3conego zapytania do przedstawienia jako stan", "db_url": "Adres URL bazy danych, pozostaw puste, aby u\u017cy\u0107 domy\u015blnej bazy danych HA", + "name": "Nazwa, kt\u00f3ra b\u0119dzie u\u017cywana do wprowadzania konfiguracji, a tak\u017ce do sensora", "query": "Zapytanie do uruchomienia, musi zaczyna\u0107 si\u0119 od \u201eSELECT\u201d", "unit_of_measurement": "Jednostka miary (opcjonalnie)", "value_template": "Szablon warto\u015bci (opcjonalnie)" diff --git a/homeassistant/components/sql/translations/pt-BR.json b/homeassistant/components/sql/translations/pt-BR.json index 21a930488a6..1846b8d5488 100644 --- a/homeassistant/components/sql/translations/pt-BR.json +++ b/homeassistant/components/sql/translations/pt-BR.json @@ -21,7 +21,7 @@ "data_description": { "column": "Coluna para a consulta retornada para apresentar como estado", "db_url": "URL do banco de dados, deixe em branco para usar o banco de dados padr\u00e3o", - "name": "Nome que ser\u00e1 usado para Config Entry e tamb\u00e9m para o Sensor", + "name": "Nome que ser\u00e1 usado para entrada de configura\u00e7\u00e3o e tamb\u00e9m para o Sensor", "query": "Consulte para ser executada, precisa come\u00e7ar com 'SELECT'", "unit_of_measurement": "Unidade de medida (opcional)", "value_template": "Modelo do valor (opcional)" @@ -48,7 +48,7 @@ "data_description": { "column": "Coluna para a consulta retornada para apresentar como estado", "db_url": "URL do banco de dados, deixe em branco para usar o banco de dados padr\u00e3o", - "name": "Nome que ser\u00e1 usado para Config Entry e tamb\u00e9m para o Sensor", + "name": "Nome que ser\u00e1 usado para entrada de configura\u00e7\u00e3o e tamb\u00e9m para o Sensor", "query": "Consulte para ser executada, precisa come\u00e7ar com 'SELECT'", "unit_of_measurement": "Unidade de medida (opcional)", "value_template": "Modelo do valor (opcional)" diff --git a/homeassistant/components/sql/translations/zh-Hant.json b/homeassistant/components/sql/translations/zh-Hant.json index 801d973049d..43349c39295 100644 --- a/homeassistant/components/sql/translations/zh-Hant.json +++ b/homeassistant/components/sql/translations/zh-Hant.json @@ -13,6 +13,7 @@ "data": { "column": "\u6b04\u4f4d", "db_url": "\u8cc7\u6599\u5eab URL", + "name": "\u540d\u7a31", "query": "\u9078\u64c7\u67e5\u8a62", "unit_of_measurement": "\u6e2c\u91cf\u55ae\u4f4d", "value_template": "\u6578\u503c\u6a21\u677f" @@ -20,6 +21,7 @@ "data_description": { "column": "\u67e5\u8a62\u56de\u8986\u6b04\u4f4d\u70ba\u72c0\u614b", "db_url": "\u8cc7\u6599\u5eab URL\u3001\u4fdd\u7559\u7a7a\u767d\u4ee5\u4f7f\u7528\u9810\u8a2d HA \u8cc7\u6599\u5eab", + "name": "\u540d\u7a31\u5c07\u6703\u7528\u65bc\u8a2d\u5b9a\u5be6\u9ad4\u8207\u611f\u6e2c\u5668", "query": "\u57f7\u884c\u7684\u67e5\u8a62\u3001\u9700\u8981\u4ee5 'SELECT' \u4f5c\u70ba\u958b\u982d", "unit_of_measurement": "\u6e2c\u91cf\u55ae\u4f4d\uff08\u9078\u9805\uff09", "value_template": "\u6578\u503c\u6a21\u677f\uff08\u9078\u9805\uff09" @@ -38,6 +40,7 @@ "data": { "column": "\u6b04\u4f4d", "db_url": "\u8cc7\u6599\u5eab URL", + "name": "\u540d\u7a31", "query": "\u9078\u64c7\u67e5\u8a62", "unit_of_measurement": "\u6e2c\u91cf\u55ae\u4f4d", "value_template": "\u6578\u503c\u6a21\u677f" @@ -45,6 +48,7 @@ "data_description": { "column": "\u67e5\u8a62\u56de\u8986\u6b04\u4f4d\u70ba\u72c0\u614b", "db_url": "\u8cc7\u6599\u5eab URL\u3001\u4fdd\u7559\u7a7a\u767d\u4ee5\u4f7f\u7528\u9810\u8a2d HA \u8cc7\u6599\u5eab", + "name": "\u540d\u7a31\u5c07\u6703\u7528\u65bc\u8a2d\u5b9a\u5be6\u9ad4\u8207\u611f\u6e2c\u5668", "query": "\u57f7\u884c\u7684\u67e5\u8a62\u3001\u9700\u8981\u4ee5 'SELECT' \u4f5c\u70ba\u958b\u982d", "unit_of_measurement": "\u6e2c\u91cf\u55ae\u4f4d\uff08\u9078\u9805\uff09", "value_template": "\u6578\u503c\u6a21\u677f\uff08\u9078\u9805\uff09" diff --git a/homeassistant/components/steam_online/translations/ca.json b/homeassistant/components/steam_online/translations/ca.json index 4535fe6d31c..699630c3732 100644 --- a/homeassistant/components/steam_online/translations/ca.json +++ b/homeassistant/components/steam_online/translations/ca.json @@ -12,13 +12,15 @@ }, "step": { "reauth_confirm": { + "description": "La integraci\u00f3 Steam ha de tornar a autenticar-se manualment\n\nPots trobar la teva clau a: {api_key_url}", "title": "Reautenticaci\u00f3 de la integraci\u00f3" }, "user": { "data": { "account": "ID del compte de Steam", "api_key": "Clau API" - } + }, + "description": "Utilitza {account_id_url} per trobar l'ID del teu compte de Steam" } } }, diff --git a/homeassistant/components/steam_online/translations/de.json b/homeassistant/components/steam_online/translations/de.json index 17950e14e24..fe73cf71448 100644 --- a/homeassistant/components/steam_online/translations/de.json +++ b/homeassistant/components/steam_online/translations/de.json @@ -12,7 +12,7 @@ }, "step": { "reauth_confirm": { - "description": "Die Steam-Integration muss manuell erneut authentifiziert werden \n\nDeinen Schl\u00fcssel findest du hier: https://steamcommunity.com/dev/apikey", + "description": "Die Steam-Integration muss manuell erneut authentifiziert werden \nHier findest du deinen Schl\u00fcssel: {api_key_url}", "title": "Integration erneut authentifizieren" }, "user": { @@ -20,7 +20,7 @@ "account": "Steam-Konto-ID", "api_key": "API-Schl\u00fcssel" }, - "description": "Verwende https://steamid.io, um deine Steam-Konto-ID zu finden" + "description": "Verwende {account_id_url}, um deine Steam-Konto-ID zu finden" } } }, diff --git a/homeassistant/components/steam_online/translations/fr.json b/homeassistant/components/steam_online/translations/fr.json index 5ee6892eef7..ae8055638f7 100644 --- a/homeassistant/components/steam_online/translations/fr.json +++ b/homeassistant/components/steam_online/translations/fr.json @@ -12,7 +12,7 @@ }, "step": { "reauth_confirm": { - "description": "L'int\u00e9gration Steam doit \u00eatre r\u00e9-authentifi\u00e9e manuellement\n\nVous pouvez trouver votre cl\u00e9 ici\u00a0: https://steamcommunity.com/dev/apikey", + "description": "L'int\u00e9gration Steam doit \u00eatre r\u00e9-authentifi\u00e9e manuellement\n\nVous pouvez trouver votre cl\u00e9 ici\u00a0: {api_key_url}", "title": "R\u00e9-authentifier l'int\u00e9gration" }, "user": { @@ -20,7 +20,7 @@ "account": "ID de compte Steam", "api_key": "Cl\u00e9 d'API" }, - "description": "Utilisez https://steamid.io pour trouver l'ID de votre compte Steam" + "description": "Utilisez {account_id_url} pour trouver l'ID de votre compte Steam" } } }, diff --git a/homeassistant/components/steam_online/translations/hu.json b/homeassistant/components/steam_online/translations/hu.json index 9bd0976345e..edff854ab52 100644 --- a/homeassistant/components/steam_online/translations/hu.json +++ b/homeassistant/components/steam_online/translations/hu.json @@ -12,7 +12,7 @@ }, "step": { "reauth_confirm": { - "description": "A Steam integr\u00e1ci\u00f3t manu\u00e1lisan \u00fajra kell hiteles\u00edteni .\n\nA kulcs itt tal\u00e1lhat\u00f3: https://steamcommunity.com/dev/apikey", + "description": "A Steam integr\u00e1ci\u00f3t manu\u00e1lisan \u00fajra kell hiteles\u00edteni .\n\nA kulcs itt tal\u00e1lhat\u00f3: {api_key_url}", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "user": { @@ -20,7 +20,7 @@ "account": "Steam fi\u00f3k azonos\u00edt\u00f3ja", "api_key": "API kulcs" }, - "description": "A https://steamid.io haszn\u00e1lat\u00e1val kaphat\u00f3 meg a Steam fi\u00f3kazonos\u00edt\u00f3" + "description": "A {account_id_url} haszn\u00e1lat\u00e1val kaphat\u00f3 meg a Steam fi\u00f3kazonos\u00edt\u00f3" } } }, diff --git a/homeassistant/components/steam_online/translations/pt-BR.json b/homeassistant/components/steam_online/translations/pt-BR.json index 93ff3e00dc5..cca660e891e 100644 --- a/homeassistant/components/steam_online/translations/pt-BR.json +++ b/homeassistant/components/steam_online/translations/pt-BR.json @@ -12,7 +12,7 @@ }, "step": { "reauth_confirm": { - "description": "A integra\u00e7\u00e3o da Steam precisa ser autenticada manualmente\n\nVoc\u00ea pode encontrar sua chave aqui: https://steamcommunity.com/dev/apikey", + "description": "A integra\u00e7\u00e3o da Steam precisa ser autenticada manualmente \n\n Voc\u00ea pode encontrar sua chave aqui: {api_key_url}", "title": "Reautenticar Integra\u00e7\u00e3o" }, "user": { @@ -20,7 +20,7 @@ "account": "ID da conta Steam", "api_key": "Chave da API" }, - "description": "Use https://steamid.io para encontrar o ID da sua conta Steam" + "description": "Use {account_id_url} para encontrar o ID da sua conta Steam" } } }, diff --git a/homeassistant/components/tautulli/translations/ca.json b/homeassistant/components/tautulli/translations/ca.json index 15895636b8e..6e53dd83be5 100644 --- a/homeassistant/components/tautulli/translations/ca.json +++ b/homeassistant/components/tautulli/translations/ca.json @@ -13,14 +13,17 @@ "reauth_confirm": { "data": { "api_key": "Clau API" - } + }, + "description": "Per trobar la teva clau API, obre la web de Tautulli i v\u00e9s a Configuraci\u00f3 i despr\u00e9s a Interf\u00edcie web. La clau API estar\u00e0 a la part inferior d'aquesta p\u00e0gina.", + "title": "Re-autenticaci\u00f3 de Tautulli" }, "user": { "data": { "api_key": "Clau API", "url": "URL", "verify_ssl": "Verifica el certificat SSL" - } + }, + "description": "Per trobar la teva clau API, obre la web de Tautulli i v\u00e9s a Configuraci\u00f3 i despr\u00e9s a Interf\u00edcie web. La clau API estar\u00e0 a la part inferior d'aquesta p\u00e0gina.\n\nExemple de l'URL: ```http://192.168.0.10:8181``` on 8181 \u00e9s el port predeterminat." } } } diff --git a/homeassistant/components/tautulli/translations/nl.json b/homeassistant/components/tautulli/translations/nl.json index eeecbd56477..9f10f666030 100644 --- a/homeassistant/components/tautulli/translations/nl.json +++ b/homeassistant/components/tautulli/translations/nl.json @@ -14,6 +14,7 @@ "data": { "api_key": "API-sleutel" }, + "description": "Om uw API sleutel te vinden, opent u de Tautulli webpagina en navigeert u naar Instellingen en vervolgens naar Webinterface. De API sleutel vindt u onderaan de pagina.", "title": "Herauthenticeer Tautulli" }, "user": { @@ -21,7 +22,8 @@ "api_key": "API-sleutel", "url": "URL", "verify_ssl": "SSL-certificaat verifi\u00ebren" - } + }, + "description": "Om uw API sleutel te vinden, opent u de Tautulli webpagina en navigeert u naar Instellingen en vervolgens naar Webinterface. De API sleutel vindt u onderaan de pagina.\n\nVoorbeeld van de URL: ```http://192.168.0.10:8181`` met 8181 als standaard poort." } } } diff --git a/homeassistant/components/trafikverket_ferry/translations/ca.json b/homeassistant/components/trafikverket_ferry/translations/ca.json index 8780b02ee37..0af4ba34155 100644 --- a/homeassistant/components/trafikverket_ferry/translations/ca.json +++ b/homeassistant/components/trafikverket_ferry/translations/ca.json @@ -6,7 +6,9 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + "incorrect_api_key": "Clau API inv\u00e0lida per al compte seleccionat", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_route": "No s'ha pogut trobar la ruta amb la informaci\u00f3 proporcionada" }, "step": { "reauth_confirm": { @@ -18,6 +20,7 @@ "data": { "api_key": "Clau API", "from": "Des del port", + "time": "Hora", "to": "Al port", "weekday": "Laborables" } diff --git a/homeassistant/components/update/translations/sv.json b/homeassistant/components/update/translations/sv.json new file mode 100644 index 00000000000..53ecdb21377 --- /dev/null +++ b/homeassistant/components/update/translations/sv.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "trigger_type": { + "changed_states": "{entity_name} uppdateringstillg\u00e4nglighet \u00e4ndrad", + "turned_off": "{entity_name} blev uppdaterad", + "turned_on": "{entity_name} har en uppdatering tillg\u00e4nglig" + } + } +} \ No newline at end of file From ef09e8900671c729aacfea37c9274d4fc11bc18b Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sun, 1 May 2022 11:00:37 +0200 Subject: [PATCH 0048/3516] Update xknx to 0.21.1 (#71144) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index f5b1bdc6206..a000261ec3a 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.21.0"], + "requirements": ["xknx==0.21.1"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 6d2ef35b3eb..40800a0ff4a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2445,7 +2445,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.21.0 +xknx==0.21.1 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7fc6f066d0c..0283849e92e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1597,7 +1597,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.21.0 +xknx==0.21.1 # homeassistant.components.bluesound # homeassistant.components.fritz From 6e1b787ba6a7690340e2b1652a9831a8a9a570e9 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sun, 1 May 2022 13:21:27 +0200 Subject: [PATCH 0049/3516] Add missing type information for panel_custom (#71122) --- .../components/panel_custom/__init__.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/panel_custom/__init__.py b/homeassistant/components/panel_custom/__init__.py index d0ff0ad44a0..493a738c1ea 100644 --- a/homeassistant/components/panel_custom/__init__.py +++ b/homeassistant/components/panel_custom/__init__.py @@ -1,4 +1,6 @@ """Register a custom front end panel.""" +from __future__ import annotations + import logging import voluptuous as vol @@ -70,27 +72,27 @@ CONFIG_SCHEMA = vol.Schema( @bind_hass async def async_register_panel( - hass, + hass: HomeAssistant, # The url to serve the panel - frontend_url_path, + frontend_url_path: str, # The webcomponent name that loads your panel - webcomponent_name, + webcomponent_name: str, # Title/icon for sidebar - sidebar_title=None, - sidebar_icon=None, + sidebar_title: str | None = None, + sidebar_icon: str | None = None, # JS source of your panel - js_url=None, + js_url: str | None = None, # JS module of your panel - module_url=None, + module_url: str | None = None, # If your panel should be run inside an iframe - embed_iframe=DEFAULT_EMBED_IFRAME, + embed_iframe: bool = DEFAULT_EMBED_IFRAME, # Should user be asked for confirmation when loading external source - trust_external=DEFAULT_TRUST_EXTERNAL, + trust_external: bool = DEFAULT_TRUST_EXTERNAL, # Configuration to be passed to the panel - config=None, + config: ConfigType | None = None, # If your panel should only be shown to admin users - require_admin=False, -): + require_admin: bool = False, +) -> None: """Register a new custom panel.""" if js_url is None and module_url is None: raise ValueError("Either js_url, module_url or html_url is required.") From 4628b151ffa49387e2ff8e7b5dafb6e7cf7d85f5 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 1 May 2022 15:39:33 +0200 Subject: [PATCH 0050/3516] Bump pysensibo 1.0.14 (#71150) --- homeassistant/components/sensibo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 535824aaa19..308d991e675 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,7 +2,7 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": ["pysensibo==1.0.13"], + "requirements": ["pysensibo==1.0.14"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 40800a0ff4a..e0228c16571 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1783,7 +1783,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.13 +pysensibo==1.0.14 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0283849e92e..f9e059b2dad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1196,7 +1196,7 @@ pyruckus==0.12 pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.13 +pysensibo==1.0.14 # homeassistant.components.serial # homeassistant.components.zha From 2d720973ee3f9a8a9bf777540b9a01fc6ff475fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 May 2022 11:53:47 -0500 Subject: [PATCH 0051/3516] Fix incomplete recorder typing (#71158) --- .strict-typing | 2 ++ .../components/recorder/system_health.py | 4 +++- mypy.ini | 22 +++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/.strict-typing b/.strict-typing index ea4124abde5..bf53662ee67 100644 --- a/.strict-typing +++ b/.strict-typing @@ -185,7 +185,9 @@ homeassistant.components.recorder.models homeassistant.components.recorder.pool homeassistant.components.recorder.purge homeassistant.components.recorder.repack +homeassistant.components.recorder.run_history homeassistant.components.recorder.statistics +homeassistant.components.recorder.system_health homeassistant.components.recorder.util homeassistant.components.recorder.websocket_api homeassistant.components.remote.* diff --git a/homeassistant/components/recorder/system_health.py b/homeassistant/components/recorder/system_health.py index 23fb760e898..2a9c536a2d6 100644 --- a/homeassistant/components/recorder/system_health.py +++ b/homeassistant/components/recorder/system_health.py @@ -1,5 +1,7 @@ """Provide info to system health.""" +from typing import Any + from homeassistant.components import system_health from homeassistant.core import HomeAssistant, callback @@ -14,7 +16,7 @@ def async_register( register.async_register_info(system_health_info) -async def system_health_info(hass: HomeAssistant): +async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: """Get info for the info page.""" instance = get_instance(hass) run_history = instance.run_history diff --git a/mypy.ini b/mypy.ini index 74ed61e5c57..c35e4d471c8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1798,6 +1798,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.recorder.run_history] +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.recorder.statistics] check_untyped_defs = true disallow_incomplete_defs = true @@ -1809,6 +1820,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.recorder.system_health] +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.recorder.util] check_untyped_defs = true disallow_incomplete_defs = true From c842672ed1c55d030533beb1592ac947bc57bc32 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 May 2022 12:00:15 -0500 Subject: [PATCH 0052/3516] Fix missing device info in lutron_caseta (#71156) - There was a missing return due to a bad merge conflict resolution - Fixes #71154 --- .../components/lutron_caseta/__init__.py | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index d0561bc47ab..bb8f94f3abe 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -294,6 +294,8 @@ async def async_unload_entry( class LutronCasetaDevice(Entity): """Common base class for all Lutron Caseta devices.""" + _attr_should_poll = False + def __init__(self, device, bridge, bridge_device): """Set up the base class. @@ -304,6 +306,18 @@ class LutronCasetaDevice(Entity): self._device = device self._smartbridge = bridge self._bridge_device = bridge_device + info = DeviceInfo( + identifiers={(DOMAIN, self.serial)}, + manufacturer=MANUFACTURER, + model=f"{device['model']} ({device['type']})", + name=self.name, + via_device=(DOMAIN, self._bridge_device["serial"]), + configuration_url="https://device-login.lutron.com", + ) + area, _ = _area_and_name_from_name(device["name"]) + if area != UNASSIGNED_AREA: + info[ATTR_SUGGESTED_AREA] = area + self._attr_device_info = info async def async_added_to_hass(self): """Register callbacks.""" @@ -329,28 +343,7 @@ class LutronCasetaDevice(Entity): """Return the unique ID of the device (serial).""" return str(self.serial) - @property - def device_info(self) -> DeviceInfo: - """Return the device info.""" - device = self._device - info = DeviceInfo( - identifiers={(DOMAIN, self.serial)}, - manufacturer=MANUFACTURER, - model=f"{device['model']} ({device['type']})", - name=self.name, - via_device=(DOMAIN, self._bridge_device["serial"]), - configuration_url="https://device-login.lutron.com", - ) - area, _ = _area_and_name_from_name(device["name"]) - if area != UNASSIGNED_AREA: - info[ATTR_SUGGESTED_AREA] = area - @property def extra_state_attributes(self): """Return the state attributes.""" return {"device_id": self.device_id, "zone_id": self._device["zone"]} - - @property - def should_poll(self): - """No polling needed.""" - return False From 2cb9783cf5591851b146b8d378ad8fe1463fe9bb Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 1 May 2022 19:00:58 +0200 Subject: [PATCH 0053/3516] Small cleanup Sensibo (#71149) * Cleanup * fix temp * Modify set_temp * Apply suggestions from code review Co-authored-by: Shay Levy --- homeassistant/components/sensibo/__init__.py | 5 ++--- homeassistant/components/sensibo/climate.py | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sensibo/__init__.py b/homeassistant/components/sensibo/__init__.py index ab8e4e85d39..dc02e9ee686 100644 --- a/homeassistant/components/sensibo/__init__.py +++ b/homeassistant/components/sensibo/__init__.py @@ -26,12 +26,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Sensibo config entry.""" - if await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): del hass.data[DOMAIN][entry.entry_id] if not hass.data[DOMAIN]: del hass.data[DOMAIN] - return True - return False + return unload_ok async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 907105b5384..c1e690cd28a 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -178,7 +178,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): ) if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: - return + raise ValueError("No target temperature provided") if temperature == self.target_temperature: return @@ -192,7 +192,9 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): temperature = self.device_data.temp_list[0] else: - return + raise ValueError( + f"Target temperature has to be one off {str(self.device_data.temp_list)}" + ) await self._async_set_ac_state_property("targetTemperature", int(temperature)) From 2e20ec21c49d090987dc885e0097d83fee5a4e6e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 May 2022 12:49:17 -0500 Subject: [PATCH 0054/3516] Bump zeroconf to 0.38.5 (#71160) --- 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 dc4c7c001ae..e1ea2c82b1f 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.38.4"], + "requirements": ["zeroconf==0.38.5"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0cb893be99d..6dd904c858f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 yarl==1.7.2 -zeroconf==0.38.4 +zeroconf==0.38.5 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index e0228c16571..5fe40d3c7b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2480,7 +2480,7 @@ youtube_dl==2021.12.17 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.38.4 +zeroconf==0.38.5 # homeassistant.components.zha zha-quirks==0.0.73 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f9e059b2dad..d24235fb1ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1620,7 +1620,7 @@ yeelight==0.7.10 youless-api==0.16 # homeassistant.components.zeroconf -zeroconf==0.38.4 +zeroconf==0.38.5 # homeassistant.components.zha zha-quirks==0.0.73 From 63e309506228c4cbefc0c053a3d069a504ada8ab Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 1 May 2022 21:00:38 +0200 Subject: [PATCH 0055/3516] Abort UniFi Network options flow if integration is not setup (#71128) --- homeassistant/components/unifi/config_flow.py | 2 ++ homeassistant/components/unifi/strings.json | 5 ++++- homeassistant/components/unifi/translations/en.json | 3 +++ tests/components/unifi/test_config_flow.py | 11 +++++++++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 6e40798fb7a..d02f3f49a5e 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -256,6 +256,8 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Manage the UniFi Network options.""" + if self.config_entry.entry_id not in self.hass.data[UNIFI_DOMAIN]: + return self.async_abort(reason="integration_not_setup") self.controller = self.hass.data[UNIFI_DOMAIN][self.config_entry.entry_id] self.options[CONF_BLOCK_CLIENT] = self.controller.option_block_clients diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index 476a0bfdd61..8d6df90b704 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -21,11 +21,14 @@ }, "abort": { "already_configured": "UniFi Network site is already configured", - "configuration_updated": "Configuration updated.", + "configuration_updated": "Configuration updated", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "options": { + "abort": { + "integration_not_setup": "UniFi integration is not setup" + }, "step": { "device_tracker": { "data": { diff --git a/homeassistant/components/unifi/translations/en.json b/homeassistant/components/unifi/translations/en.json index 5d08c8b816d..2a1c17cb6e3 100644 --- a/homeassistant/components/unifi/translations/en.json +++ b/homeassistant/components/unifi/translations/en.json @@ -26,6 +26,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "UniFi integration is not setup" + }, "step": { "client_control": { "data": { diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 4c4ff7006fd..321cbdfd9e8 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -542,6 +542,17 @@ async def test_simple_option_flow(hass, aioclient_mock): } +async def test_option_flow_integration_not_setup(hass, aioclient_mock): + """Test advanced config flow options.""" + config_entry = await setup_unifi_integration(hass, aioclient_mock) + + hass.data[UNIFI_DOMAIN].pop(config_entry.entry_id) + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == "abort" + assert result["reason"] == "integration_not_setup" + + async def test_form_ssdp(hass): """Test we get the form with ssdp source.""" From 5b25b94a22f84143a6055d782fd2c6ac6bd8d238 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 1 May 2022 22:04:03 +0200 Subject: [PATCH 0056/3516] Fix template error in sql (#71169) --- homeassistant/components/sql/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 2ae626c67fa..5e748bed55e 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -42,7 +42,7 @@ _QUERY_SCHEME = vol.Schema( vol.Required(CONF_NAME): cv.string, vol.Required(CONF_QUERY): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_VALUE_TEMPLATE): cv.string, } ) From 40280cbd43ec3a5fa95465289fe86ba6ea57545f Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 1 May 2022 13:36:13 -0700 Subject: [PATCH 0057/3516] update python-smarttub to 0.0.32 (#71164) --- homeassistant/components/smarttub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 94e8185bb7d..1d7500b9185 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smarttub", "dependencies": [], "codeowners": ["@mdz"], - "requirements": ["python-smarttub==0.0.31"], + "requirements": ["python-smarttub==0.0.32"], "quality_scale": "platinum", "iot_class": "cloud_polling", "loggers": ["smarttub"] diff --git a/requirements_all.txt b/requirements_all.txt index 5fe40d3c7b5..ae3de70522a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1934,7 +1934,7 @@ python-qbittorrent==0.4.2 python-ripple-api==0.0.3 # homeassistant.components.smarttub -python-smarttub==0.0.31 +python-smarttub==0.0.32 # homeassistant.components.songpal python-songpal==0.14.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d24235fb1ff..8ec85ba314b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1269,7 +1269,7 @@ python-nest==4.2.0 python-picnic-api==1.1.0 # homeassistant.components.smarttub -python-smarttub==0.0.31 +python-smarttub==0.0.32 # homeassistant.components.songpal python-songpal==0.14.1 From b97ca8e26009baba53a038dba4559eadad5ddf7a Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 1 May 2022 14:20:44 -0700 Subject: [PATCH 0058/3516] Bump gcal_sync to 0.7.1 to fix calendar API timezone handling (#71173) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 46c5844819a..2cf852fc6af 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["auth"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", - "requirements": ["gcal-sync==0.7.0", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==0.7.1", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index ae3de70522a..a331efde6c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -689,7 +689,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==0.7.0 +gcal-sync==0.7.1 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ec85ba314b..67bba2141d5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -486,7 +486,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==0.7.0 +gcal-sync==0.7.1 # homeassistant.components.usgs_earthquakes_feed geojson_client==0.6 From ff48720c6ad7314a52c76ebaba8d0ca1b581db3a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 May 2022 18:11:11 -0500 Subject: [PATCH 0059/3516] Remove db schema v23 stats migration tests (#71137) --- tests/components/recorder/test_statistics.py | 612 +------------------ 1 file changed, 2 insertions(+), 610 deletions(-) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 8a73716e482..58f848edc5a 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -1,18 +1,13 @@ """The tests for sensor recorder platform.""" # pylint: disable=protected-access,invalid-name from datetime import timedelta -import importlib -import json -import sys from unittest.mock import patch, sentinel import pytest from pytest import approx -from sqlalchemy import create_engine -from sqlalchemy.orm import Session from homeassistant.components import recorder -from homeassistant.components.recorder import SQLITE_URL_PREFIX, history, statistics +from homeassistant.components.recorder import history, statistics from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import ( StatisticsShortTerm, @@ -37,7 +32,7 @@ import homeassistant.util.dt as dt_util from .common import async_wait_recording_done -from tests.common import get_test_home_assistant, mock_registry +from tests.common import mock_registry from tests.components.recorder.common import wait_recording_done ORIG_TZ = dt_util.DEFAULT_TIME_ZONE @@ -740,609 +735,6 @@ def test_monthly_statistics(hass_recorder, caplog, timezone): dt_util.set_default_time_zone(dt_util.get_time_zone("UTC")) -def _create_engine_test(*args, **kwargs): - """Test version of create_engine that initializes with old schema. - - This simulates an existing db with the old schema. - """ - module = "tests.components.recorder.models_schema_23" - importlib.import_module(module) - old_models = sys.modules[module] - engine = create_engine(*args, **kwargs) - old_models.Base.metadata.create_all(engine) - with Session(engine) as session: - session.add(recorder.models.StatisticsRuns(start=statistics.get_start_time())) - session.add( - recorder.models.SchemaChanges(schema_version=old_models.SCHEMA_VERSION) - ) - session.commit() - return engine - - -def test_delete_duplicates(caplog, tmpdir): - """Test removal of duplicated statistics.""" - test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") - dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" - - module = "tests.components.recorder.models_schema_23" - importlib.import_module(module) - old_models = sys.modules[module] - - period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) - period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) - period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) - period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) - - external_energy_statistics_1 = ( - { - "start": period1, - "last_reset": None, - "state": 0, - "sum": 2, - }, - { - "start": period2, - "last_reset": None, - "state": 1, - "sum": 3, - }, - { - "start": period3, - "last_reset": None, - "state": 2, - "sum": 4, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 5, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 5, - }, - ) - external_energy_metadata_1 = { - "has_mean": False, - "has_sum": True, - "name": "Total imported energy", - "source": "test", - "statistic_id": "test:total_energy_import_tariff_1", - "unit_of_measurement": "kWh", - } - external_energy_statistics_2 = ( - { - "start": period1, - "last_reset": None, - "state": 0, - "sum": 20, - }, - { - "start": period2, - "last_reset": None, - "state": 1, - "sum": 30, - }, - { - "start": period3, - "last_reset": None, - "state": 2, - "sum": 40, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 50, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 50, - }, - ) - external_energy_metadata_2 = { - "has_mean": False, - "has_sum": True, - "name": "Total imported energy", - "source": "test", - "statistic_id": "test:total_energy_import_tariff_2", - "unit_of_measurement": "kWh", - } - external_co2_statistics = ( - { - "start": period1, - "last_reset": None, - "mean": 10, - }, - { - "start": period2, - "last_reset": None, - "mean": 30, - }, - { - "start": period3, - "last_reset": None, - "mean": 60, - }, - { - "start": period4, - "last_reset": None, - "mean": 90, - }, - ) - external_co2_metadata = { - "has_mean": True, - "has_sum": False, - "name": "Fossil percentage", - "source": "test", - "statistic_id": "test:fossil_percentage", - "unit_of_measurement": "%", - } - - # Create some duplicated statistics with schema version 23 - with patch.object(recorder, "models", old_models), patch.object( - recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION - ), patch( - "homeassistant.components.recorder.create_engine", new=_create_engine_test - ): - hass = get_test_home_assistant() - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) - wait_recording_done(hass) - wait_recording_done(hass) - - with session_scope(hass=hass) as session: - session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) - ) - session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) - ) - session.add(recorder.models.StatisticsMeta.from_meta(external_co2_metadata)) - with session_scope(hass=hass) as session: - for stat in external_energy_statistics_1: - session.add(recorder.models.Statistics.from_stats(1, stat)) - for stat in external_energy_statistics_2: - session.add(recorder.models.Statistics.from_stats(2, stat)) - for stat in external_co2_statistics: - session.add(recorder.models.Statistics.from_stats(3, stat)) - - hass.stop() - dt_util.DEFAULT_TIME_ZONE = ORIG_TZ - - # Test that the duplicates are removed during migration from schema 23 - hass = get_test_home_assistant() - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) - hass.start() - wait_recording_done(hass) - wait_recording_done(hass) - hass.stop() - dt_util.DEFAULT_TIME_ZONE = ORIG_TZ - - assert "Deleted 2 duplicated statistics rows" in caplog.text - assert "Found non identical" not in caplog.text - assert "Found duplicated" not in caplog.text - - -def test_delete_duplicates_many(caplog, tmpdir): - """Test removal of duplicated statistics.""" - test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") - dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" - - module = "tests.components.recorder.models_schema_23" - importlib.import_module(module) - old_models = sys.modules[module] - - period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) - period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) - period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) - period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) - - external_energy_statistics_1 = ( - { - "start": period1, - "last_reset": None, - "state": 0, - "sum": 2, - }, - { - "start": period2, - "last_reset": None, - "state": 1, - "sum": 3, - }, - { - "start": period3, - "last_reset": None, - "state": 2, - "sum": 4, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 5, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 5, - }, - ) - external_energy_metadata_1 = { - "has_mean": False, - "has_sum": True, - "name": "Total imported energy", - "source": "test", - "statistic_id": "test:total_energy_import_tariff_1", - "unit_of_measurement": "kWh", - } - external_energy_statistics_2 = ( - { - "start": period1, - "last_reset": None, - "state": 0, - "sum": 20, - }, - { - "start": period2, - "last_reset": None, - "state": 1, - "sum": 30, - }, - { - "start": period3, - "last_reset": None, - "state": 2, - "sum": 40, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 50, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 50, - }, - ) - external_energy_metadata_2 = { - "has_mean": False, - "has_sum": True, - "name": "Total imported energy", - "source": "test", - "statistic_id": "test:total_energy_import_tariff_2", - "unit_of_measurement": "kWh", - } - external_co2_statistics = ( - { - "start": period1, - "last_reset": None, - "mean": 10, - }, - { - "start": period2, - "last_reset": None, - "mean": 30, - }, - { - "start": period3, - "last_reset": None, - "mean": 60, - }, - { - "start": period4, - "last_reset": None, - "mean": 90, - }, - ) - external_co2_metadata = { - "has_mean": True, - "has_sum": False, - "name": "Fossil percentage", - "source": "test", - "statistic_id": "test:fossil_percentage", - "unit_of_measurement": "%", - } - - # Create some duplicated statistics with schema version 23 - with patch.object(recorder, "models", old_models), patch.object( - recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION - ), patch( - "homeassistant.components.recorder.create_engine", new=_create_engine_test - ): - hass = get_test_home_assistant() - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) - wait_recording_done(hass) - wait_recording_done(hass) - - with session_scope(hass=hass) as session: - session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) - ) - session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) - ) - session.add(recorder.models.StatisticsMeta.from_meta(external_co2_metadata)) - with session_scope(hass=hass) as session: - for stat in external_energy_statistics_1: - session.add(recorder.models.Statistics.from_stats(1, stat)) - for _ in range(3000): - session.add( - recorder.models.Statistics.from_stats( - 1, external_energy_statistics_1[-1] - ) - ) - for stat in external_energy_statistics_2: - session.add(recorder.models.Statistics.from_stats(2, stat)) - for stat in external_co2_statistics: - session.add(recorder.models.Statistics.from_stats(3, stat)) - - hass.stop() - dt_util.DEFAULT_TIME_ZONE = ORIG_TZ - - # Test that the duplicates are removed during migration from schema 23 - hass = get_test_home_assistant() - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) - hass.start() - wait_recording_done(hass) - wait_recording_done(hass) - hass.stop() - dt_util.DEFAULT_TIME_ZONE = ORIG_TZ - - assert "Deleted 3002 duplicated statistics rows" in caplog.text - assert "Found non identical" not in caplog.text - assert "Found duplicated" not in caplog.text - - -@pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") -def test_delete_duplicates_non_identical(caplog, tmpdir): - """Test removal of duplicated statistics.""" - test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") - dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" - - module = "tests.components.recorder.models_schema_23" - importlib.import_module(module) - old_models = sys.modules[module] - - period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) - period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) - period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) - period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) - - external_energy_statistics_1 = ( - { - "start": period1, - "last_reset": None, - "state": 0, - "sum": 2, - }, - { - "start": period2, - "last_reset": None, - "state": 1, - "sum": 3, - }, - { - "start": period3, - "last_reset": None, - "state": 2, - "sum": 4, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 5, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 6, - }, - ) - external_energy_metadata_1 = { - "has_mean": False, - "has_sum": True, - "name": "Total imported energy", - "source": "test", - "statistic_id": "test:total_energy_import_tariff_1", - "unit_of_measurement": "kWh", - } - external_energy_statistics_2 = ( - { - "start": period1, - "last_reset": None, - "state": 0, - "sum": 20, - }, - { - "start": period2, - "last_reset": None, - "state": 1, - "sum": 30, - }, - { - "start": period3, - "last_reset": None, - "state": 2, - "sum": 40, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 50, - }, - { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 50, - }, - ) - external_energy_metadata_2 = { - "has_mean": False, - "has_sum": True, - "name": "Total imported energy", - "source": "test", - "statistic_id": "test:total_energy_import_tariff_2", - "unit_of_measurement": "kWh", - } - - # Create some duplicated statistics with schema version 23 - with patch.object(recorder, "models", old_models), patch.object( - recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION - ), patch( - "homeassistant.components.recorder.create_engine", new=_create_engine_test - ): - hass = get_test_home_assistant() - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) - wait_recording_done(hass) - wait_recording_done(hass) - - with session_scope(hass=hass) as session: - session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) - ) - session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) - ) - with session_scope(hass=hass) as session: - for stat in external_energy_statistics_1: - session.add(recorder.models.Statistics.from_stats(1, stat)) - for stat in external_energy_statistics_2: - session.add(recorder.models.Statistics.from_stats(2, stat)) - - hass.stop() - dt_util.DEFAULT_TIME_ZONE = ORIG_TZ - - # Test that the duplicates are removed during migration from schema 23 - hass = get_test_home_assistant() - hass.config.config_dir = tmpdir - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) - hass.start() - wait_recording_done(hass) - wait_recording_done(hass) - hass.stop() - dt_util.DEFAULT_TIME_ZONE = ORIG_TZ - - assert "Deleted 2 duplicated statistics rows" in caplog.text - assert "Deleted 1 non identical" in caplog.text - assert "Found duplicated" not in caplog.text - - isotime = dt_util.utcnow().isoformat() - backup_file_name = f".storage/deleted_statistics.{isotime}.json" - - with open(hass.config.path(backup_file_name)) as backup_file: - backup = json.load(backup_file) - - assert backup == [ - { - "duplicate": { - "created": "2021-08-01T00:00:00", - "id": 4, - "last_reset": None, - "max": None, - "mean": None, - "metadata_id": 1, - "min": None, - "start": "2021-10-31T23:00:00", - "state": 3.0, - "sum": 5.0, - }, - "original": { - "created": "2021-08-01T00:00:00", - "id": 5, - "last_reset": None, - "max": None, - "mean": None, - "metadata_id": 1, - "min": None, - "start": "2021-10-31T23:00:00", - "state": 3.0, - "sum": 6.0, - }, - } - ] - - -def test_delete_duplicates_short_term(caplog, tmpdir): - """Test removal of duplicated statistics.""" - test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") - dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" - - module = "tests.components.recorder.models_schema_23" - importlib.import_module(module) - old_models = sys.modules[module] - - period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) - - external_energy_metadata_1 = { - "has_mean": False, - "has_sum": True, - "name": "Total imported energy", - "source": "test", - "statistic_id": "test:total_energy_import_tariff_1", - "unit_of_measurement": "kWh", - } - statistic_row = { - "start": period4, - "last_reset": None, - "state": 3, - "sum": 5, - } - - # Create some duplicated statistics with schema version 23 - with patch.object(recorder, "models", old_models), patch.object( - recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION - ), patch( - "homeassistant.components.recorder.create_engine", new=_create_engine_test - ): - hass = get_test_home_assistant() - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) - wait_recording_done(hass) - wait_recording_done(hass) - - with session_scope(hass=hass) as session: - session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) - ) - with session_scope(hass=hass) as session: - session.add( - recorder.models.StatisticsShortTerm.from_stats(1, statistic_row) - ) - session.add( - recorder.models.StatisticsShortTerm.from_stats(1, statistic_row) - ) - - hass.stop() - dt_util.DEFAULT_TIME_ZONE = ORIG_TZ - - # Test that the duplicates are removed during migration from schema 23 - hass = get_test_home_assistant() - hass.config.config_dir = tmpdir - setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) - hass.start() - wait_recording_done(hass) - wait_recording_done(hass) - hass.stop() - dt_util.DEFAULT_TIME_ZONE = ORIG_TZ - - assert "duplicated statistics rows" not in caplog.text - assert "Found non identical" not in caplog.text - assert "Deleted duplicated short term statistic" in caplog.text - - def test_delete_duplicates_no_duplicates(hass_recorder, caplog): """Test removal of duplicated statistics.""" hass = hass_recorder() From 26c6328b1ffde1071718b5442ebaffc7fddc73d4 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Mon, 2 May 2022 00:13:21 +0100 Subject: [PATCH 0060/3516] Generic camera handle template adjacent to portnumber (#71031) --- .../components/generic/config_flow.py | 30 ++++++++----- tests/components/generic/test_config_flow.py | 42 +++++++++++++++++++ 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 4298b473681..086262aa0a1 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -130,10 +130,9 @@ async def async_test_still(hass, info) -> tuple[dict[str, str], str | None]: fmt = None if not (url := info.get(CONF_STILL_IMAGE_URL)): return {}, info.get(CONF_CONTENT_TYPE, "image/jpeg") - if not isinstance(url, template_helper.Template) and url: - url = cv.template(url) - url.hass = hass try: + if not isinstance(url, template_helper.Template): + url = template_helper.Template(url, hass) url = url.async_render(parse_result=False) except TemplateError as err: _LOGGER.warning("Problem rendering template %s: %s", url, err) @@ -168,11 +167,20 @@ async def async_test_still(hass, info) -> tuple[dict[str, str], str | None]: return {}, f"image/{fmt}" -def slug_url(url) -> str | None: +def slug(hass, template) -> str | None: """Convert a camera url into a string suitable for a camera name.""" - if not url: + if not template: return None - return slugify(yarl.URL(url).host) + if not isinstance(template, template_helper.Template): + template = template_helper.Template(template, hass) + try: + url = template.async_render(parse_result=False) + return slugify(yarl.URL(url).host) + except TemplateError as err: + _LOGGER.error("Syntax error in '%s': %s", template.template, err) + except (ValueError, TypeError) as err: + _LOGGER.error("Syntax error in '%s': %s", url, err) + return None async def async_test_stream(hass, info) -> dict[str, str]: @@ -252,6 +260,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle the start of the config flow.""" errors = {} + hass = self.hass if user_input: # Secondary validation because serialised vol can't seem to handle this complexity: if not user_input.get(CONF_STILL_IMAGE_URL) and not user_input.get( @@ -263,8 +272,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): errors = errors | await async_test_stream(self.hass, user_input) still_url = user_input.get(CONF_STILL_IMAGE_URL) stream_url = user_input.get(CONF_STREAM_SOURCE) - name = slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME - + name = slug(hass, still_url) or slug(hass, stream_url) or DEFAULT_NAME if not errors: user_input[CONF_CONTENT_TYPE] = still_format user_input[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False @@ -295,7 +303,8 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): still_url = import_config.get(CONF_STILL_IMAGE_URL) stream_url = import_config.get(CONF_STREAM_SOURCE) name = import_config.get( - CONF_NAME, slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME + CONF_NAME, + slug(self.hass, still_url) or slug(self.hass, stream_url) or DEFAULT_NAME, ) if CONF_LIMIT_REFETCH_TO_URL_CHANGE not in import_config: import_config[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False @@ -318,6 +327,7 @@ class GenericOptionsFlowHandler(OptionsFlow): ) -> FlowResult: """Manage Generic IP Camera options.""" errors: dict[str, str] = {} + hass = self.hass if user_input is not None: errors, still_format = await async_test_still( @@ -327,7 +337,7 @@ class GenericOptionsFlowHandler(OptionsFlow): still_url = user_input.get(CONF_STILL_IMAGE_URL) stream_url = user_input.get(CONF_STREAM_SOURCE) if not errors: - title = slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME + title = slug(hass, still_url) or slug(hass, stream_url) or DEFAULT_NAME if still_url is None: # If user didn't specify a still image URL, # The automatically generated still image that stream generates diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index f5411ed3ea0..457cac26aa5 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -165,6 +165,48 @@ async def test_form_only_still_sample(hass, user_flow, image_file): assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY +@respx.mock +@pytest.mark.parametrize( + ("template", "url", "expected_result"), + [ + # Test we can handle templates in strange parts of the url, #70961. + ( + "http://localhost:812{{3}}/static/icons/favicon-apple-180x180.png", + "http://localhost:8123/static/icons/favicon-apple-180x180.png", + data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + ), + ( + "{% if 1 %}https://bla{% else %}https://yo{% endif %}", + "https://bla/", + data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + ), + ( + "http://{{example.org", + "http://example.org", + data_entry_flow.RESULT_TYPE_FORM, + ), + ( + "invalid1://invalid:4\\1", + "invalid1://invalid:4%5c1", + data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + ), + ], +) +async def test_still_template( + hass, user_flow, fakeimgbytes_png, template, url, expected_result +) -> None: + """Test we can handle various templates.""" + respx.get(url).respond(stream=fakeimgbytes_png) + data = TESTDATA.copy() + data.pop(CONF_STREAM_SOURCE) + data[CONF_STILL_IMAGE_URL] = template + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + data, + ) + assert result2["type"] == expected_result + + @respx.mock async def test_form_rtsp_mode(hass, fakeimg_png, mock_av_open, user_flow): """Test we complete ok if the user enters a stream url.""" From d8ee9c1922b41188e04e2f33828363458c1a580c Mon Sep 17 00:00:00 2001 From: Kuba Wolanin Date: Mon, 2 May 2022 01:14:30 +0200 Subject: [PATCH 0061/3516] Add Show logs URL to integration errors notification (#71142) --- homeassistant/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index d5f9eb91b62..5c870918231 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -951,8 +951,9 @@ def async_notify_setup_error( message = "The following integrations and platforms could not be set up:\n\n" for name, link in errors.items(): + show_logs = f"[Show logs](/config/logs?filter={name})" part = f"[{name}]({link})" if link else name - message += f" - {part}\n" + message += f" - {part} ({show_logs})\n" message += "\nPlease check your config and [logs](/config/logs)." From ae01ec02e28d4b83ef64636e36de2baf59c19874 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 1 May 2022 19:26:22 -0400 Subject: [PATCH 0062/3516] Allow custom integrations to support application_credentials platform (#71129) --- .../application_credentials/__init__.py | 15 +++++--- homeassistant/loader.py | 15 ++++++++ script/hassfest/application_credentials.py | 2 +- script/hassfest/model.py | 4 +- .../application_credentials/test_init.py | 3 +- tests/test_loader.py | 37 +++++++++++++++++++ 6 files changed, 66 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/application_credentials/__init__.py b/homeassistant/components/application_credentials/__init__.py index cc5ed5e44bb..b5c828762c1 100644 --- a/homeassistant/components/application_credentials/__init__.py +++ b/homeassistant/components/application_credentials/__init__.py @@ -17,12 +17,15 @@ from homeassistant.components import websocket_api from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DOMAIN, CONF_ID from homeassistant.core import HomeAssistant, callback -from homeassistant.generated.application_credentials import APPLICATION_CREDENTIALS from homeassistant.helpers import collection, config_entry_oauth2_flow import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType -from homeassistant.loader import IntegrationNotFound, async_get_integration +from homeassistant.loader import ( + IntegrationNotFound, + async_get_application_credentials, + async_get_integration, +) from homeassistant.util import slugify __all__ = ["ClientCredential", "AuthorizationServer", "async_import_client_credential"] @@ -234,9 +237,11 @@ async def _get_platform( @websocket_api.websocket_command( {vol.Required("type"): "application_credentials/config"} ) -@callback -def handle_integration_list( +@websocket_api.async_response +async def handle_integration_list( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Handle integrations command.""" - connection.send_result(msg["id"], {"domains": APPLICATION_CREDENTIALS}) + connection.send_result( + msg["id"], {"domains": await async_get_application_credentials(hass)} + ) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 364f212a1be..589f316532b 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -24,6 +24,7 @@ from awesomeversion import ( AwesomeVersionStrategy, ) +from .generated.application_credentials import APPLICATION_CREDENTIALS from .generated.dhcp import DHCP from .generated.mqtt import MQTT from .generated.ssdp import SSDP @@ -210,6 +211,20 @@ async def async_get_config_flows( return flows +async def async_get_application_credentials(hass: HomeAssistant) -> list[str]: + """Return cached list of application credentials.""" + integrations = await async_get_custom_components(hass) + + return [ + *APPLICATION_CREDENTIALS, + *[ + integration.domain + for integration in integrations.values() + if "application_credentials" in integration.dependencies + ], + ] + + def async_process_zeroconf_match_dict(entry: dict[str, Any]) -> dict[str, Any]: """Handle backwards compat with zeroconf matchers.""" entry_without_type: dict[str, Any] = entry.copy() diff --git a/script/hassfest/application_credentials.py b/script/hassfest/application_credentials.py index 87a277bb2b8..48d812dba02 100644 --- a/script/hassfest/application_credentials.py +++ b/script/hassfest/application_credentials.py @@ -18,7 +18,7 @@ APPLICATION_CREDENTIALS = {} def generate_and_validate(integrations: dict[str, Integration], config: Config) -> str: - """Validate and generate config flow data.""" + """Validate and generate application_credentials data.""" match_list = [] diff --git a/script/hassfest/model.py b/script/hassfest/model.py index 2a6ea9ca85f..fc38e1db592 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -98,9 +98,9 @@ class Integration: return self.manifest.get("quality_scale") @property - def config_flow(self) -> str: + def config_flow(self) -> bool: """Return if the integration has a config flow.""" - return self.manifest.get("config_flow") + return self.manifest.get("config_flow", False) @property def requirements(self) -> list[str]: diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py index 31cf45f2b54..8929d8f9c54 100644 --- a/tests/components/application_credentials/test_init.py +++ b/tests/components/application_credentials/test_init.py @@ -615,8 +615,7 @@ async def test_websocket_integration_list(ws_client: ClientFixture): """Test websocket integration list command.""" client = await ws_client() with patch( - "homeassistant.components.application_credentials.APPLICATION_CREDENTIALS", - ["example1", "example2"], + "homeassistant.loader.APPLICATION_CREDENTIALS", ["example1", "example2"] ): assert await client.cmd_result("config") == { "domains": ["example1", "example2"] diff --git a/tests/test_loader.py b/tests/test_loader.py index 9f2aaff58b7..96694c43c7f 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -327,6 +327,26 @@ def _get_test_integration(hass, name, config_flow): ) +def _get_test_integration_with_application_credentials(hass, name): + """Return a generated test integration with application_credentials support.""" + return loader.Integration( + hass, + f"homeassistant.components.{name}", + None, + { + "name": name, + "domain": name, + "config_flow": True, + "dependencies": ["application_credentials"], + "requirements": [], + "zeroconf": [f"_{name}._tcp.local."], + "homekit": {"models": [name]}, + "ssdp": [{"manufacturer": name, "modelName": name}], + "mqtt": [f"{name}/discovery"], + }, + ) + + def _get_test_integration_with_zeroconf_matcher(hass, name, config_flow): """Return a generated test integration with a zeroconf matcher.""" return loader.Integration( @@ -479,6 +499,23 @@ async def test_get_zeroconf(hass): ] +async def test_get_application_credentials(hass): + """Verify that custom components with application_credentials are found.""" + test_1_integration = _get_test_integration(hass, "test_1", True) + test_2_integration = _get_test_integration_with_application_credentials( + hass, "test_2" + ) + + with patch("homeassistant.loader.async_get_custom_components") as mock_get: + mock_get.return_value = { + "test_1": test_1_integration, + "test_2": test_2_integration, + } + application_credentials = await loader.async_get_application_credentials(hass) + assert "test_2" in application_credentials + assert "test_1" not in application_credentials + + async def test_get_zeroconf_back_compat(hass): """Verify that custom components with zeroconf are found and legacy matchers are converted.""" test_1_integration = _get_test_integration(hass, "test_1", True) From 9b10658d0120a2d897a7a63c0cd01cf5e275b1e4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 2 May 2022 00:22:04 +0000 Subject: [PATCH 0063/3516] [ci skip] Translation update --- .../components/apple_tv/translations/it.json | 2 +- .../application_credentials/translations/et.json | 3 +++ .../application_credentials/translations/it.json | 3 +++ .../application_credentials/translations/pl.json | 3 +++ .../translations/zh-Hant.json | 3 +++ .../components/august/translations/it.json | 2 +- .../components/auth/translations/it.json | 2 +- .../components/deconz/translations/et.json | 1 + .../components/deconz/translations/it.json | 1 + .../components/deconz/translations/ru.json | 1 + .../components/elmax/translations/it.json | 2 +- homeassistant/components/fan/translations/it.json | 2 +- .../components/integration/translations/it.json | 2 +- .../components/isy994/translations/et.json | 13 +++++++++++-- .../components/isy994/translations/it.json | 15 ++++++++++++--- homeassistant/components/ps4/translations/it.json | 2 +- .../components/recorder/translations/et.json | 8 ++++++++ .../components/recorder/translations/it.json | 8 ++++++++ .../components/recorder/translations/pl.json | 8 ++++++++ .../components/recorder/translations/zh-Hant.json | 8 ++++++++ .../components/simplisafe/translations/it.json | 4 ++++ .../components/simplisafe/translations/ru.json | 4 ++++ homeassistant/components/sql/translations/it.json | 4 ++++ homeassistant/components/sql/translations/ru.json | 4 ++++ .../components/steam_online/translations/et.json | 4 ++-- .../components/steam_online/translations/it.json | 4 ++-- .../components/steam_online/translations/pl.json | 4 ++-- .../steam_online/translations/zh-Hant.json | 4 ++-- .../components/unifi/translations/de.json | 5 ++++- .../components/unifi/translations/en.json | 2 +- .../components/unifi/translations/fr.json | 5 ++++- .../components/unifi/translations/it.json | 5 ++++- .../components/unifi/translations/pt-BR.json | 5 ++++- .../components/vulcan/translations/it.json | 2 +- homeassistant/components/zha/translations/it.json | 2 +- 35 files changed, 120 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/application_credentials/translations/et.json create mode 100644 homeassistant/components/application_credentials/translations/it.json create mode 100644 homeassistant/components/application_credentials/translations/pl.json create mode 100644 homeassistant/components/application_credentials/translations/zh-Hant.json create mode 100644 homeassistant/components/recorder/translations/et.json create mode 100644 homeassistant/components/recorder/translations/it.json create mode 100644 homeassistant/components/recorder/translations/pl.json create mode 100644 homeassistant/components/recorder/translations/zh-Hant.json diff --git a/homeassistant/components/apple_tv/translations/it.json b/homeassistant/components/apple_tv/translations/it.json index b1e3a06440f..8b6e744ce53 100644 --- a/homeassistant/components/apple_tv/translations/it.json +++ b/homeassistant/components/apple_tv/translations/it.json @@ -19,7 +19,7 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "invalid_auth": "Autenticazione non valida", "no_devices_found": "Nessun dispositivo trovato sulla rete", - "no_usable_service": "\u00c8 stato trovato un dispositivo ma non \u00e8 stato possibile identificare alcun modo per stabilire una connessione ad esso. Se continui a vedere questo messaggio, prova a specificarne l'indirizzo IP o a riavviare l'Apple TV.", + "no_usable_service": "\u00c8 stato trovato un dispositivo, ma non \u00e8 stato possibile identificare alcun modo per stabilire una connessione con esso. Se continui a vedere questo messaggio, prova a specificarne l'indirizzo IP o a riavviare l'Apple TV.", "unknown": "Errore imprevisto" }, "flow_title": "{name} ({type})", diff --git a/homeassistant/components/application_credentials/translations/et.json b/homeassistant/components/application_credentials/translations/et.json new file mode 100644 index 00000000000..af619b0fb8d --- /dev/null +++ b/homeassistant/components/application_credentials/translations/et.json @@ -0,0 +1,3 @@ +{ + "title": "Rakenduse mandaadid" +} \ No newline at end of file diff --git a/homeassistant/components/application_credentials/translations/it.json b/homeassistant/components/application_credentials/translations/it.json new file mode 100644 index 00000000000..144f96c6342 --- /dev/null +++ b/homeassistant/components/application_credentials/translations/it.json @@ -0,0 +1,3 @@ +{ + "title": "Credenziali dell'applicazione" +} \ No newline at end of file diff --git a/homeassistant/components/application_credentials/translations/pl.json b/homeassistant/components/application_credentials/translations/pl.json new file mode 100644 index 00000000000..93c2ddb7534 --- /dev/null +++ b/homeassistant/components/application_credentials/translations/pl.json @@ -0,0 +1,3 @@ +{ + "title": "Po\u015bwiadczenia aplikacji" +} \ No newline at end of file diff --git a/homeassistant/components/application_credentials/translations/zh-Hant.json b/homeassistant/components/application_credentials/translations/zh-Hant.json new file mode 100644 index 00000000000..88ad70a7d10 --- /dev/null +++ b/homeassistant/components/application_credentials/translations/zh-Hant.json @@ -0,0 +1,3 @@ +{ + "title": "\u61c9\u7528\u6191\u8b49" +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/it.json b/homeassistant/components/august/translations/it.json index aebfc1b14cd..7a121915768 100644 --- a/homeassistant/components/august/translations/it.json +++ b/homeassistant/components/august/translations/it.json @@ -24,7 +24,7 @@ "username": "Nome utente" }, "description": "Se il metodo di accesso \u00e8 'email', il nome utente \u00e8 l'indirizzo email. Se il metodo di accesso \u00e8 'phone', il nome utente \u00e8 il numero di telefono nel formato '+NNNNNNNNNN'.", - "title": "Configura un account di August" + "title": "Configura un account August" }, "validation": { "data": { diff --git a/homeassistant/components/auth/translations/it.json b/homeassistant/components/auth/translations/it.json index e8091faf757..82d69b007ff 100644 --- a/homeassistant/components/auth/translations/it.json +++ b/homeassistant/components/auth/translations/it.json @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "Per attivare l'autenticazione a due fattori utilizzando le password monouso basate sul tempo, esegui la scansione del codice QR con l'app di autenticazione. Se non ne hai uno, ti consigliamo [Google Authenticator](https://support.google.com/accounts/answer/1066447) o [Authy](https://authy.com/). \n\n {qr_code} \n \nDopo la scansione, inserisci il codice a sei cifre dalla tua app per verificare la configurazione. Se riscontri problemi con la scansione del codice QR, esegui una configurazione manuale con il codice **`{code}`**.", + "description": "Per attivare l'autenticazione a due fattori utilizzando le password monouso basate sul tempo, esegui la scansione del codice QR con l'applicazione di autenticazione. Se non ne hai uno, ti consigliamo [Google Authenticator](https://support.google.com/accounts/answer/1066447) o [Authy](https://authy.com/). \n\n {qr_code} \n \nDopo la scansione, inserisci il codice a sei cifre dalla tua app per verificare la configurazione. Se riscontri problemi con la scansione del codice QR, esegui una configurazione manuale con il codice **`{code}`**.", "title": "Imposta l'autenticazione a due fattori usando TOTP" } }, diff --git a/homeassistant/components/deconz/translations/et.json b/homeassistant/components/deconz/translations/et.json index 6be6a7fbdc6..8519182878c 100644 --- a/homeassistant/components/deconz/translations/et.json +++ b/homeassistant/components/deconz/translations/et.json @@ -9,6 +9,7 @@ "updated_instance": "DeCONZ-i eksemplarile omistati uus hostiaadress" }, "error": { + "linking_not_possible": "\u00dchendus l\u00fc\u00fcsiga nurjus", "no_key": "API v\u00f5tit ei leitud" }, "flow_title": "{host}", diff --git a/homeassistant/components/deconz/translations/it.json b/homeassistant/components/deconz/translations/it.json index 2c3e42adcc8..44c204d7ed9 100644 --- a/homeassistant/components/deconz/translations/it.json +++ b/homeassistant/components/deconz/translations/it.json @@ -9,6 +9,7 @@ "updated_instance": "Istanza deCONZ aggiornata con nuovo indirizzo host" }, "error": { + "linking_not_possible": "Impossibile collegarsi al gateway", "no_key": "Impossibile ottenere una chiave API" }, "flow_title": "{host}", diff --git a/homeassistant/components/deconz/translations/ru.json b/homeassistant/components/deconz/translations/ru.json index 412c12198f3..f92ebd0e1c7 100644 --- a/homeassistant/components/deconz/translations/ru.json +++ b/homeassistant/components/deconz/translations/ru.json @@ -9,6 +9,7 @@ "updated_instance": "\u0410\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d." }, "error": { + "linking_not_possible": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443.", "no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API." }, "flow_title": "{host}", diff --git a/homeassistant/components/elmax/translations/it.json b/homeassistant/components/elmax/translations/it.json index 30098163498..07539c739bd 100644 --- a/homeassistant/components/elmax/translations/it.json +++ b/homeassistant/components/elmax/translations/it.json @@ -6,7 +6,7 @@ "error": { "bad_auth": "Autenticazione non valida", "invalid_auth": "Autenticazione non valida", - "invalid_pin": "Il pin fornito non \u00e8 valido", + "invalid_pin": "Il PIN fornito non \u00e8 valido", "network_error": "Si \u00e8 verificato un errore di rete", "no_panel_online": "Non \u00e8 stato trovato alcun pannello di controllo Elmax in linea.", "unknown": "Errore imprevisto", diff --git a/homeassistant/components/fan/translations/it.json b/homeassistant/components/fan/translations/it.json index ccde6f0a3a5..563b24b7c44 100644 --- a/homeassistant/components/fan/translations/it.json +++ b/homeassistant/components/fan/translations/it.json @@ -9,7 +9,7 @@ "is_on": "{entity_name} \u00e8 acceso" }, "trigger_type": { - "changed_states": "{entity_name} attivata o disattivat", + "changed_states": "{entity_name} attivata o disattivata", "toggled": "{entity_name} attiva o disattiva", "turned_off": "{entity_name} disattivato", "turned_on": "{entity_name} attivato" diff --git a/homeassistant/components/integration/translations/it.json b/homeassistant/components/integration/translations/it.json index a1904f45cd1..d07b702962c 100644 --- a/homeassistant/components/integration/translations/it.json +++ b/homeassistant/components/integration/translations/it.json @@ -13,7 +13,7 @@ "data_description": { "round": "Controlla il numero di cifre decimali nell'output.", "unit_prefix": "L'output sar\u00e0 ridimensionato in base al prefisso della metrica selezionato.", - "unit_time": "L'output verr\u00e0 ridimensionato in base all'unit\u00e0 di tempo selezionata." + "unit_time": "L'output sar\u00e0 ridimensionato in base all'unit\u00e0 di tempo selezionata." }, "description": "Crea un sensore che calcoli una somma di Riemann per stimare l'integrale di un sensore.", "title": "Aggiungi il sensore integrale della somma di Riemann" diff --git a/homeassistant/components/isy994/translations/et.json b/homeassistant/components/isy994/translations/et.json index 233b3a2bfe8..0f7e0e45503 100644 --- a/homeassistant/components/isy994/translations/et.json +++ b/homeassistant/components/isy994/translations/et.json @@ -7,10 +7,19 @@ "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus", "invalid_host": "Hostikirje ei olnud sobivas URL-vormingus, nt http://192.168.10.100:80", + "reauth_successful": "Taastuvastamine \u00f5nnestus", "unknown": "Tundmatu viga" }, "flow_title": "{name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Kasutaja {host} mandaat ei kehti enam.", + "title": "Taastuvasta ISY" + }, "user": { "data": { "host": "", @@ -19,7 +28,7 @@ "username": "Kasutajanimi" }, "description": "Hostikirje peab olema t\u00e4ielikus URL-vormingus, nt http://192.168.10.100:80", - "title": "\u00dchendu oma ISY994-ga" + "title": "\u00dchendu oma ISY-ga" } } }, @@ -33,7 +42,7 @@ "variable_sensor_string": "Muutuja sensori string" }, "description": "Isy isidumisei suvandite m\u00e4\u00e4ramine: \n \u2022 S\u00f5lme sensor string: iga seade v\u00f5i kaust, mis sisaldab nimes \"Node Sensor String\" k\u00e4sitletakse sensorina v\u00f5i binaarandurina. \n \u2022 Ignoreeri stringi: iga seadet, mille nimes on \"Ignore String\", ignoreeritakse. \n \u2022 Muutuv sensorstring: andurina lisatakse k\u00f5ik muutujad, mis sisaldavad \"Variable Sensor String\". \n \u2022 Valguse heleduse taastamine: kui see on lubatud, taastatakse eelmine heledus, kui l\u00fclitate sisse seadme sisseehitatud taseme asemel valguse.", - "title": "ISY994 valikud" + "title": "ISY valikud" } } }, diff --git a/homeassistant/components/isy994/translations/it.json b/homeassistant/components/isy994/translations/it.json index b59e312717a..158d9f1d400 100644 --- a/homeassistant/components/isy994/translations/it.json +++ b/homeassistant/components/isy994/translations/it.json @@ -7,10 +7,19 @@ "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", "invalid_host": "La voce host non era nel formato URL completo, ad esempio http://192.168.10.100:80", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "unknown": "Errore imprevisto" }, "flow_title": "{name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Le credenziali per {host} non sono pi\u00f9 valide.", + "title": "Autentica nuovamente il tuo ISY" + }, "user": { "data": { "host": "URL", @@ -19,7 +28,7 @@ "username": "Nome utente" }, "description": "La voce host deve essere nel formato URL completo, ad esempio, http://192.168.10.100:80", - "title": "Connettersi a ISY994" + "title": "Connettiti al tuo ISY" } } }, @@ -32,8 +41,8 @@ "sensor_string": "Stringa Nodo Sensore", "variable_sensor_string": "Stringa Variabile Sensore" }, - "description": "Imposta le opzioni per l'integrazione ISY: \n \u2022 Stringa Nodo Sensore: qualsiasi dispositivo o cartella che contiene \"Stringa Nodo Sensore\" nel nome verr\u00e0 trattato come un sensore o un sensore binario. \n \u2022 Ignora Stringa: qualsiasi dispositivo con \"Ignora Stringa\" nel nome verr\u00e0 ignorato. \n \u2022 Stringa Variabile Sensore: qualsiasi variabile che contiene \"Stringa Variabile Sensore\" verr\u00e0 aggiunta come sensore. \n \u2022 Ripristina Luminosit\u00e0 Luce: se abilitato, verr\u00e0 ripristinata la luminosit\u00e0 precedente quando si accende una luce al posto del livello incorporato nel dispositivo.", - "title": "Opzioni ISY994" + "description": "Imposta le opzioni per l'integrazione ISY: \n \u2022 Stringa nodo sensore: qualsiasi dispositivo o cartella che contiene \"Stringa nodo sensore\" nel nome verr\u00e0 trattato come un sensore o un sensore binario. \n \u2022 Ignora stringa: qualsiasi dispositivo con \"Ignora stringa\" nel nome verr\u00e0 ignorato. \n \u2022 Stringa variabile sensore: qualsiasi variabile che contiene \"Stringa variabile sensore\" verr\u00e0 aggiunta come sensore. \n \u2022 Ripristina luminosit\u00e0 luce: se abilitato, verr\u00e0 ripristinata la luminosit\u00e0 precedente quando si accende una luce al posto del livello incorporato nel dispositivo.", + "title": "Opzioni ISY" } } }, diff --git a/homeassistant/components/ps4/translations/it.json b/homeassistant/components/ps4/translations/it.json index 3faca963317..a8a97ad1144 100644 --- a/homeassistant/components/ps4/translations/it.json +++ b/homeassistant/components/ps4/translations/it.json @@ -15,7 +15,7 @@ }, "step": { "creds": { - "description": "Credenziali necessarie. Premi 'Invia' e poi, nella seconda schermata della App PS4, aggiorna i dispositivi e seleziona il dispositivo 'Home-Assistant' per continuare.", + "description": "Credenziali necessarie. Premi 'Invia' e poi, nell'applicazione PS4 Second Screen, aggiorna i dispositivi e seleziona il dispositivo 'Home-Assistant' per continuare.", "title": "PlayStation 4" }, "link": { diff --git a/homeassistant/components/recorder/translations/et.json b/homeassistant/components/recorder/translations/et.json new file mode 100644 index 00000000000..8455fb5ce7d --- /dev/null +++ b/homeassistant/components/recorder/translations/et.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "current_recorder_run": "Praegune k\u00e4ivitamise algusaeg", + "oldest_recorder_run": "Vanim k\u00e4ivitamise algusaeg" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/it.json b/homeassistant/components/recorder/translations/it.json new file mode 100644 index 00000000000..82f456981a8 --- /dev/null +++ b/homeassistant/components/recorder/translations/it.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "current_recorder_run": "Ora di inizio esecuzione corrente", + "oldest_recorder_run": "Ora di inizio esecuzione meno recente" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/pl.json b/homeassistant/components/recorder/translations/pl.json new file mode 100644 index 00000000000..1a31b3f7ec5 --- /dev/null +++ b/homeassistant/components/recorder/translations/pl.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "current_recorder_run": "Pocz\u0105tek aktualnej sesji rejestratora", + "oldest_recorder_run": "Pocz\u0105tek najstarszej sesji rejestratora" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/zh-Hant.json b/homeassistant/components/recorder/translations/zh-Hant.json new file mode 100644 index 00000000000..648bade5447 --- /dev/null +++ b/homeassistant/components/recorder/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "current_recorder_run": "\u76ee\u524d\u57f7\u884c\u958b\u59cb\u6642\u9593", + "oldest_recorder_run": "\u6700\u65e9\u57f7\u884c\u958b\u59cb\u6642\u9593" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index ad9e491ab9a..fb92029795d 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Questo account SimpliSafe \u00e8 gi\u00e0 in uso.", + "email_2fa_timed_out": "Timeout durante l'attesa dell'autenticazione a due fattori basata su email.", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "wrong_account": "Le credenziali utente fornite non corrispondono a questo account SimpliSafe." }, @@ -12,6 +13,9 @@ "still_awaiting_mfa": "Ancora in attesa del clic sull'email MFA", "unknown": "Errore imprevisto" }, + "progress": { + "email_2fa": "Digita il codice di autenticazione a due fattori\n inviato tramite email." + }, "step": { "mfa": { "title": "Autenticazione a pi\u00f9 fattori (MFA) SimpliSafe " diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index 66bd439a0c1..1788a9ca206 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -2,6 +2,7 @@ "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.", + "email_2fa_timed_out": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "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." }, @@ -12,6 +13,9 @@ "still_awaiting_mfa": "\u041e\u0436\u0438\u0434\u0430\u043d\u0438\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u043f\u043e \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u0435.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, + "progress": { + "email_2fa": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u0412\u0430\u043c \u043f\u043e \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u0435." + }, "step": { "mfa": { "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" diff --git a/homeassistant/components/sql/translations/it.json b/homeassistant/components/sql/translations/it.json index 01672eb5a51..3d7bee5454e 100644 --- a/homeassistant/components/sql/translations/it.json +++ b/homeassistant/components/sql/translations/it.json @@ -13,6 +13,7 @@ "data": { "column": "Colonna", "db_url": "URL del database", + "name": "Nome", "query": "Query Select", "unit_of_measurement": "Unit\u00e0 di misura", "value_template": "Modello di valore" @@ -20,6 +21,7 @@ "data_description": { "column": "Colonna per la query restituita da presentare come stato", "db_url": "URL del database, lascia vuoto per utilizzare il database predefinito di Home Assistant", + "name": "Nome che sar\u00e0 utilizzato per la voce di configurazione e anche per il sensore", "query": "Query da eseguire, deve iniziare con 'SELECT'", "unit_of_measurement": "Unit\u00e0 di misura (opzionale)", "value_template": "Modello di valore (opzionale)" @@ -38,6 +40,7 @@ "data": { "column": "Colonna", "db_url": "URL del database", + "name": "Nome", "query": "Query Select", "unit_of_measurement": "Unit\u00e0 di misura", "value_template": "Modello di valore" @@ -45,6 +48,7 @@ "data_description": { "column": "Colonna per la query restituita da presentare come stato", "db_url": "URL del database, lascia vuoto per utilizzare il database predefinito di Home Assistant", + "name": "Nome che sar\u00e0 utilizzato per la voce di configurazione e anche per il sensore", "query": "Query da eseguire, deve iniziare con 'SELECT'", "unit_of_measurement": "Unit\u00e0 di misura (opzionale)", "value_template": "Modello di valore (opzionale)" diff --git a/homeassistant/components/sql/translations/ru.json b/homeassistant/components/sql/translations/ru.json index 8f8e0741583..2d993776fa6 100644 --- a/homeassistant/components/sql/translations/ru.json +++ b/homeassistant/components/sql/translations/ru.json @@ -13,6 +13,7 @@ "data": { "column": "\u0421\u0442\u043e\u043b\u0431\u0435\u0446", "db_url": "URL-\u0430\u0434\u0440\u0435\u0441 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "query": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0437\u0430\u043f\u0440\u043e\u0441", "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f", "value_template": "\u0428\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f" @@ -20,6 +21,7 @@ "data_description": { "column": "\u0421\u0442\u043e\u043b\u0431\u0435\u0446 \u0434\u043b\u044f \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0432 \u0432\u0438\u0434\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", "db_url": "URL-\u0430\u0434\u0440\u0435\u0441 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445. \u041e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0431\u0430\u0437\u0443 \u0434\u0430\u043d\u043d\u044b\u0445 HA \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0437\u0430\u043f\u0438\u0441\u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430.", "query": "\u0417\u0430\u043f\u0440\u043e\u0441 \u0434\u043b\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f, \u0434\u043e\u043b\u0436\u0435\u043d \u043d\u0430\u0447\u0438\u043d\u0430\u0442\u044c\u0441\u044f \u0441 'SELECT'", "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", "value_template": "\u0428\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" @@ -38,6 +40,7 @@ "data": { "column": "\u0421\u0442\u043e\u043b\u0431\u0435\u0446", "db_url": "URL-\u0430\u0434\u0440\u0435\u0441 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "query": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0437\u0430\u043f\u0440\u043e\u0441", "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f", "value_template": "\u0428\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f" @@ -45,6 +48,7 @@ "data_description": { "column": "\u0421\u0442\u043e\u043b\u0431\u0435\u0446 \u0434\u043b\u044f \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0432 \u0432\u0438\u0434\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", "db_url": "URL-\u0430\u0434\u0440\u0435\u0441 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445. \u041e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0431\u0430\u0437\u0443 \u0434\u0430\u043d\u043d\u044b\u0445 HA \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0437\u0430\u043f\u0438\u0441\u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430.", "query": "\u0417\u0430\u043f\u0440\u043e\u0441 \u0434\u043b\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f, \u0434\u043e\u043b\u0436\u0435\u043d \u043d\u0430\u0447\u0438\u043d\u0430\u0442\u044c\u0441\u044f \u0441 'SELECT'", "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", "value_template": "\u0428\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" diff --git a/homeassistant/components/steam_online/translations/et.json b/homeassistant/components/steam_online/translations/et.json index 8d501ccf50d..7c09fa1de91 100644 --- a/homeassistant/components/steam_online/translations/et.json +++ b/homeassistant/components/steam_online/translations/et.json @@ -12,7 +12,7 @@ }, "step": { "reauth_confirm": { - "description": "Steami sidumine tuleb uuesti autentida\n\nV\u00f5tme leiad siit: https://steamcommunity.com/dev/apikey", + "description": "Steami sidumine tuleb k\u00e4sitsi uuesti autentida \n\n Oma v\u00f5tme leiad siit: {api_key_url}", "title": "Taastuvasta sidumine" }, "user": { @@ -20,7 +20,7 @@ "account": "Steami konto ID", "api_key": "API v\u00f5ti" }, - "description": "Kasuta https://steamid.io, et leida oma Steam'i konto ID" + "description": "Steami konto ID leidmiseks kasuta {account_id_url}" } } }, diff --git a/homeassistant/components/steam_online/translations/it.json b/homeassistant/components/steam_online/translations/it.json index 71036870961..edb7878234d 100644 --- a/homeassistant/components/steam_online/translations/it.json +++ b/homeassistant/components/steam_online/translations/it.json @@ -12,7 +12,7 @@ }, "step": { "reauth_confirm": { - "description": "L'integrazione di Steam richiede una nuova autenticazione manuale \n\nPuoi trovare la tua chiave qui: https://steamcommunity.com/dev/apikey", + "description": "L'integrazione Steam richiede una nuova autenticazione manuale \n\nPuoi trovare la tua chiave qui: {api_key_url}", "title": "Autentica nuovamente l'integrazione" }, "user": { @@ -20,7 +20,7 @@ "account": "ID dell'account Steam", "api_key": "Chiave API" }, - "description": "Usa https://steamid.io per trovare l'ID del tuo account Steam" + "description": "Usa {account_id_url} per trovare l'ID del tuo account Steam" } } }, diff --git a/homeassistant/components/steam_online/translations/pl.json b/homeassistant/components/steam_online/translations/pl.json index 09c95bca9c4..370b997b3fc 100644 --- a/homeassistant/components/steam_online/translations/pl.json +++ b/homeassistant/components/steam_online/translations/pl.json @@ -12,7 +12,7 @@ }, "step": { "reauth_confirm": { - "description": "Integracja Steam musi zosta\u0107 ponownie uwierzytelniona r\u0119cznie\n\nSw\u00f3j klucz znajdziesz tutaj: https://steamcommunity.com/dev/apikey", + "description": "Integracja Steam musi zosta\u0107 ponownie uwierzytelniona r\u0119cznie\n\nSw\u00f3j klucz znajdziesz tutaj: {api_key_url}", "title": "Ponownie uwierzytelnij integracj\u0119" }, "user": { @@ -20,7 +20,7 @@ "account": "Identyfikator konta Steam", "api_key": "Klucz API" }, - "description": "U\u017cyj https://steamid.io, aby znale\u017a\u0107 sw\u00f3j identyfikator konta Steam" + "description": "U\u017cyj {account_id_url}, aby znale\u017a\u0107 sw\u00f3j identyfikator konta Steam" } } }, diff --git a/homeassistant/components/steam_online/translations/zh-Hant.json b/homeassistant/components/steam_online/translations/zh-Hant.json index 775c9710592..17cff11185e 100644 --- a/homeassistant/components/steam_online/translations/zh-Hant.json +++ b/homeassistant/components/steam_online/translations/zh-Hant.json @@ -12,7 +12,7 @@ }, "step": { "reauth_confirm": { - "description": "Steam \u6574\u5408\u9700\u8981\u624b\u52d5\u91cd\u65b0\u8a8d\u8b49\n\n\u53ef\u4ee5\u65bc\u5f8c\u65b9\u7db2\u5740\u627e\u5230\u91d1\u9470\uff1ahttps://steamcommunity.com/dev/apikey", + "description": "Steam \u6574\u5408\u9700\u8981\u624b\u52d5\u91cd\u65b0\u8a8d\u8b49\n\n\u53ef\u4ee5\u65bc\u5f8c\u65b9\u7db2\u5740\u627e\u5230\u91d1\u9470\uff1a{api_key_url}", "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" }, "user": { @@ -20,7 +20,7 @@ "account": "Steam \u5e33\u865f ID", "api_key": "API \u91d1\u9470" }, - "description": "\u4f7f\u7528 https://steamid.io \u4ee5\u78ba\u8a8d\u60a8\u7684 Steam \u5e33\u865f ID" + "description": "\u4f7f\u7528 {account_id_url} \u4ee5\u78ba\u8a8d\u60a8\u7684 Steam \u5e33\u865f ID" } } }, diff --git a/homeassistant/components/unifi/translations/de.json b/homeassistant/components/unifi/translations/de.json index ce4047ced42..419bb3e8eba 100644 --- a/homeassistant/components/unifi/translations/de.json +++ b/homeassistant/components/unifi/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "UniFi-Netzwerkstandort ist bereits konfiguriert", - "configuration_updated": "Konfiguration aktualisiert.", + "configuration_updated": "Konfiguration aktualisiert", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { @@ -26,6 +26,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "UniFi-Integration ist nicht eingerichtet" + }, "step": { "client_control": { "data": { diff --git a/homeassistant/components/unifi/translations/en.json b/homeassistant/components/unifi/translations/en.json index 2a1c17cb6e3..6e36e517e16 100644 --- a/homeassistant/components/unifi/translations/en.json +++ b/homeassistant/components/unifi/translations/en.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "UniFi Network site is already configured", - "configuration_updated": "Configuration updated.", + "configuration_updated": "Configuration updated", "reauth_successful": "Re-authentication was successful" }, "error": { diff --git a/homeassistant/components/unifi/translations/fr.json b/homeassistant/components/unifi/translations/fr.json index d8f1bc0a84c..3da202d1cec 100644 --- a/homeassistant/components/unifi/translations/fr.json +++ b/homeassistant/components/unifi/translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Le contr\u00f4leur est d\u00e9j\u00e0 configur\u00e9", - "configuration_updated": "Configuration mise \u00e0 jour.", + "configuration_updated": "Configuration mise \u00e0 jour", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { @@ -26,6 +26,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "L'int\u00e9gration UniFi n'est pas configur\u00e9e" + }, "step": { "client_control": { "data": { diff --git a/homeassistant/components/unifi/translations/it.json b/homeassistant/components/unifi/translations/it.json index 7ddc0fde4fe..1ac0ff05798 100644 --- a/homeassistant/components/unifi/translations/it.json +++ b/homeassistant/components/unifi/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Il sito della rete UniFi \u00e8 gi\u00e0 configurato", - "configuration_updated": "Configurazione aggiornata.", + "configuration_updated": "Configurazione aggiornata", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { @@ -26,6 +26,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "L'integrazione UniFi non \u00e8 configurata" + }, "step": { "client_control": { "data": { diff --git a/homeassistant/components/unifi/translations/pt-BR.json b/homeassistant/components/unifi/translations/pt-BR.json index 0e5ba9af217..00dd0b08165 100644 --- a/homeassistant/components/unifi/translations/pt-BR.json +++ b/homeassistant/components/unifi/translations/pt-BR.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A conta j\u00e1 foi configurada", - "configuration_updated": "Configura\u00e7\u00e3o atualizada.", + "configuration_updated": "Configura\u00e7\u00e3o atualizada", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { @@ -26,6 +26,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "A integra\u00e7\u00e3o UniFi n\u00e3o est\u00e1 configurada" + }, "step": { "client_control": { "data": { diff --git a/homeassistant/components/vulcan/translations/it.json b/homeassistant/components/vulcan/translations/it.json index 72bb696a909..895f4469eb9 100644 --- a/homeassistant/components/vulcan/translations/it.json +++ b/homeassistant/components/vulcan/translations/it.json @@ -9,7 +9,7 @@ "cannot_connect": "Errore di connessione: controlla la tua connessione Internet", "expired_credentials": "Credenziali scadute: creane una nuova nella pagina di registrazione dell'applicazione mobile Vulcan", "expired_token": "Token scaduto: genera un nuovo token", - "invalid_pin": "Pin non valido", + "invalid_pin": "PIN non valido", "invalid_symbol": "Simbolo non valido", "invalid_token": "Token non valido", "unknown": "Si \u00e8 verificato un errore sconosciuto" diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 504ae7eadba..57be4c7acb6 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -86,7 +86,7 @@ "device_dropped": "Dispositivo caduto", "device_flipped": "Dispositivo capovolto \"{subtype}\"", "device_knocked": "Dispositivo bussato \"{subtype}\"", - "device_offline": "Dispositivo offline", + "device_offline": "Dispositivo non in linea", "device_rotated": "Dispositivo ruotato \" {subtype} \"", "device_shaken": "Dispositivo in vibrazione", "device_slid": "Dispositivo scivolato \"{subtype}\"", From b8442d9340b569d10e0593bb0576bdcdb9ea55e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 May 2022 19:33:31 -0500 Subject: [PATCH 0064/3516] Add json decode caching to logbook (#71080) --- .strict-typing | 1 + homeassistant/components/logbook/__init__.py | 261 ++++++++++++------- homeassistant/scripts/benchmark/__init__.py | 2 +- mypy.ini | 11 + tests/components/logbook/test_init.py | 87 ++++++- 5 files changed, 261 insertions(+), 101 deletions(-) diff --git a/.strict-typing b/.strict-typing index bf53662ee67..df54a082119 100644 --- a/.strict-typing +++ b/.strict-typing @@ -139,6 +139,7 @@ homeassistant.components.lcn.* homeassistant.components.light.* homeassistant.components.local_ip.* homeassistant.components.lock.* +homeassistant.components.logbook.* homeassistant.components.lookin.* homeassistant.components.luftdaten.* homeassistant.components.mailbox.* diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 8b00311a9e7..b136c7330ac 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -1,16 +1,18 @@ """Event parser and human readable log generator.""" from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Callable, Generator, Iterable from contextlib import suppress from datetime import datetime as dt, timedelta from http import HTTPStatus from itertools import groupby import json import re -from typing import Any +from typing import Any, cast +from aiohttp import web import sqlalchemy +from sqlalchemy.engine.row import Row from sqlalchemy.orm import aliased from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session @@ -19,7 +21,10 @@ import voluptuous as vol from homeassistant.components import frontend from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED -from homeassistant.components.history import sqlalchemy_filter_from_include_exclude_conf +from homeassistant.components.history import ( + Filters, + sqlalchemy_filter_from_include_exclude_conf, +) from homeassistant.components.http import HomeAssistantView from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.models import ( @@ -34,7 +39,6 @@ from homeassistant.const import ( ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, - ATTR_ICON, ATTR_NAME, ATTR_SERVICE, EVENT_CALL_SERVICE, @@ -45,6 +49,8 @@ from homeassistant.const import ( ) from homeassistant.core import ( DOMAIN as HA_DOMAIN, + Context, + Event, HomeAssistant, ServiceCall, callback, @@ -54,6 +60,7 @@ from homeassistant.exceptions import InvalidEntityFormatError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, + EntityFilter, convert_include_exclude_filter, generate_filter, ) @@ -65,6 +72,7 @@ from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util ENTITY_ID_JSON_TEMPLATE = '%"entity_id":"{}"%' +FRIENDLY_NAME_JSON_EXTRACT = re.compile('"friendly_name": ?"([^"]+)"') ENTITY_ID_JSON_EXTRACT = re.compile('"entity_id": ?"([^"]+)"') DOMAIN_JSON_EXTRACT = re.compile('"domain": ?"([^"]+)"') ICON_JSON_EXTRACT = re.compile('"icon": ?"([^"]+)"') @@ -111,7 +119,7 @@ EVENT_COLUMNS = [ Events.context_parent_id, ] -SCRIPT_AUTOMATION_EVENTS = [EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED] +SCRIPT_AUTOMATION_EVENTS = {EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED} LOG_MESSAGE_SCHEMA = vol.Schema( { @@ -124,14 +132,28 @@ LOG_MESSAGE_SCHEMA = vol.Schema( @bind_hass -def log_entry(hass, name, message, domain=None, entity_id=None, context=None): +def log_entry( + hass: HomeAssistant, + name: str, + message: str, + domain: str | None = None, + entity_id: str | None = None, + context: Context | None = None, +) -> None: """Add an entry to the logbook.""" hass.add_job(async_log_entry, hass, name, message, domain, entity_id, context) @callback @bind_hass -def async_log_entry(hass, name, message, domain=None, entity_id=None, context=None): +def async_log_entry( + hass: HomeAssistant, + name: str, + message: str, + domain: str | None = None, + entity_id: str | None = None, + context: Context | None = None, +) -> None: """Add an entry to the logbook.""" data = {ATTR_NAME: name, ATTR_MESSAGE: message} @@ -184,11 +206,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def _process_logbook_platform(hass, domain, platform): +async def _process_logbook_platform( + hass: HomeAssistant, domain: str, platform: Any +) -> None: """Process a logbook platform.""" @callback - def _async_describe_event(domain, event_name, describe_callback): + def _async_describe_event( + domain: str, + event_name: str, + describe_callback: Callable[[Event], dict[str, Any]], + ) -> None: """Teach logbook how to describe a new event.""" hass.data[DOMAIN][event_name] = (domain, describe_callback) @@ -202,41 +230,51 @@ class LogbookView(HomeAssistantView): name = "api:logbook" extra_urls = ["/api/logbook/{datetime}"] - def __init__(self, config, filters, entities_filter): + def __init__( + self, + config: dict[str, Any], + filters: Filters | None, + entities_filter: EntityFilter | None, + ) -> None: """Initialize the logbook view.""" self.config = config self.filters = filters self.entities_filter = entities_filter - async def get(self, request, datetime=None): + async def get( + self, request: web.Request, datetime: str | None = None + ) -> web.Response: """Retrieve logbook entries.""" if datetime: - if (datetime := dt_util.parse_datetime(datetime)) is None: + if (datetime_dt := dt_util.parse_datetime(datetime)) is None: return self.json_message("Invalid datetime", HTTPStatus.BAD_REQUEST) else: - datetime = dt_util.start_of_local_day() + datetime_dt = dt_util.start_of_local_day() - if (period := request.query.get("period")) is None: - period = 1 + if (period_str := request.query.get("period")) is None: + period: int = 1 else: - period = int(period) + period = int(period_str) - if entity_ids := request.query.get("entity"): + if entity_ids_str := request.query.get("entity"): try: - entity_ids = cv.entity_ids(entity_ids) + entity_ids = cv.entity_ids(entity_ids_str) except vol.Invalid: raise InvalidEntityFormatError( - f"Invalid entity id(s) encountered: {entity_ids}. " + f"Invalid entity id(s) encountered: {entity_ids_str}. " "Format should be ." ) from vol.Invalid + else: + entity_ids = None - if (end_time := request.query.get("end_time")) is None: - start_day = dt_util.as_utc(datetime) - timedelta(days=period - 1) + if (end_time_str := request.query.get("end_time")) is None: + start_day = dt_util.as_utc(datetime_dt) - timedelta(days=period - 1) end_day = start_day + timedelta(days=period) else: - start_day = datetime - if (end_day := dt_util.parse_datetime(end_time)) is None: + start_day = datetime_dt + if (end_day_dt := dt_util.parse_datetime(end_time_str)) is None: return self.json_message("Invalid end_time", HTTPStatus.BAD_REQUEST) + end_day = end_day_dt hass = request.app["hass"] @@ -248,7 +286,7 @@ class LogbookView(HomeAssistantView): "Can't combine entity with context_id", HTTPStatus.BAD_REQUEST ) - def json_events(): + def json_events() -> web.Response: """Fetch events and generate JSON.""" return self.json( _get_events( @@ -263,10 +301,17 @@ class LogbookView(HomeAssistantView): ) ) - return await get_instance(hass).async_add_executor_job(json_events) + return cast( + web.Response, await get_instance(hass).async_add_executor_job(json_events) + ) -def humanify(hass, events, entity_attr_cache, context_lookup): +def humanify( + hass: HomeAssistant, + events: Generator[LazyEventPartialState, None, None], + entity_attr_cache: EntityAttributeCache, + context_lookup: dict[str | None, LazyEventPartialState | None], +) -> Generator[dict[str, Any], None, None]: """Generate a converted list of events into Entry objects. Will try to group events if possible: @@ -320,6 +365,7 @@ def humanify(hass, events, entity_attr_cache, context_lookup): # Skip all but the last sensor state continue + assert entity_id is not None data = { "when": event.time_fired_isoformat, "name": _entity_name_from_event( @@ -420,27 +466,28 @@ def humanify(hass, events, entity_attr_cache, context_lookup): def _get_events( - hass, - start_day, - end_day, - entity_ids=None, - filters=None, - entities_filter=None, - entity_matches_only=False, - context_id=None, -): + hass: HomeAssistant, + start_day: dt, + end_day: dt, + entity_ids: list[str] | None = None, + filters: Filters | None = None, + entities_filter: EntityFilter | Callable[[str], bool] | None = None, + entity_matches_only: bool = False, + context_id: str | None = None, +) -> list[dict[str, Any]]: """Get events for a period of time.""" assert not ( entity_ids and context_id ), "can't pass in both entity_ids and context_id" entity_attr_cache = EntityAttributeCache(hass) - context_lookup = {None: None} + event_data_cache: dict[str, dict[str, Any]] = {} + context_lookup: dict[str | None, LazyEventPartialState | None] = {None: None} - def yield_events(query): + def yield_events(query: Query) -> Generator[LazyEventPartialState, None, None]: """Yield Events that are not filtered away.""" for row in query.yield_per(1000): - event = LazyEventPartialState(row) + event = LazyEventPartialState(row, event_data_cache) context_lookup.setdefault(event.context_id, event) if event.event_type == EVENT_CALL_SERVICE: continue @@ -482,7 +529,7 @@ def _get_events( ) if filters: query = query.filter( - filters.entity_filter() | (Events.event_type != EVENT_STATE_CHANGED) + filters.entity_filter() | (Events.event_type != EVENT_STATE_CHANGED) # type: ignore[no-untyped-call] ) if context_id is not None: @@ -634,7 +681,11 @@ def _apply_event_entity_id_matchers( ) -def _keep_event(hass, event, entities_filter): +def _keep_event( + hass: HomeAssistant, + event: LazyEventPartialState, + entities_filter: EntityFilter | Callable[[str], bool] | None = None, +) -> bool: if event.event_type in HOMEASSISTANT_EVENTS: return entities_filter is None or entities_filter(HA_DOMAIN_ENTITY_ID) @@ -648,15 +699,21 @@ def _keep_event(hass, event, entities_filter): else: domain = event.data_domain - if domain is None: - return False - - return entities_filter is None or entities_filter(f"{domain}._") + return domain is not None and ( + entities_filter is None or entities_filter(f"{domain}._") + ) def _augment_data_with_context( - data, entity_id, event, context_lookup, entity_attr_cache, external_events -): + data: dict[str, Any], + entity_id: str | None, + event: LazyEventPartialState, + context_lookup: dict[str | None, LazyEventPartialState | None], + entity_attr_cache: EntityAttributeCache, + external_events: dict[ + str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] + ], +) -> None: if not (context_event := context_lookup.get(event.context_id)): return @@ -691,18 +748,14 @@ def _augment_data_with_context( data["context_event_type"] = event_type return - if not entity_id: + if not entity_id or context_event == event: return - attr_entity_id = event_data.get(ATTR_ENTITY_ID) - if not isinstance(attr_entity_id, str) or ( + if (attr_entity_id := context_event.data_entity_id) is None or ( event_type in SCRIPT_AUTOMATION_EVENTS and attr_entity_id == entity_id ): return - if context_event == event: - return - data["context_entity_id"] = attr_entity_id data["context_entity_id_name"] = _entity_name_from_event( attr_entity_id, context_event, entity_attr_cache @@ -716,7 +769,11 @@ def _augment_data_with_context( data["context_name"] = name -def _entity_name_from_event(entity_id, event, entity_attr_cache): +def _entity_name_from_event( + entity_id: str, + event: LazyEventPartialState, + entity_attr_cache: EntityAttributeCache, +) -> str: """Extract the entity name from the event using the cache if possible.""" return entity_attr_cache.get( entity_id, ATTR_FRIENDLY_NAME, event @@ -739,85 +796,87 @@ class LazyEventPartialState: "context_user_id", "context_parent_id", "time_fired_minute", + "_event_data_cache", ] - def __init__(self, row): + def __init__( + self, + row: Row, + event_data_cache: dict[str, dict[str, Any]], + ) -> None: """Init the lazy event.""" self._row = row - self._event_data = None - self._time_fired_isoformat = None - self._attributes = None - self._domain = None - self.event_type = self._row.event_type - self.entity_id = self._row.entity_id + self._event_data: dict[str, Any] | None = None + self._time_fired_isoformat: dt | None = None + self._domain: str | None = None + self.event_type: str = self._row.event_type + self.entity_id: str | None = self._row.entity_id self.state = self._row.state - self.context_id = self._row.context_id - self.context_user_id = self._row.context_user_id - self.context_parent_id = self._row.context_parent_id - self.time_fired_minute = self._row.time_fired.minute + self.context_id: str | None = self._row.context_id + self.context_user_id: str | None = self._row.context_user_id + self.context_parent_id: str | None = self._row.context_parent_id + self.time_fired_minute: int = self._row.time_fired.minute + self._event_data_cache = event_data_cache @property - def domain(self): + def domain(self) -> str | None: """Return the domain for the state.""" if self._domain is None: + assert self.entity_id is not None self._domain = split_entity_id(self.entity_id)[0] return self._domain @property - def attributes_icon(self): + def attributes_icon(self) -> str | None: """Extract the icon from the decoded attributes or json.""" - if self._attributes: - return self._attributes.get(ATTR_ICON) result = ICON_JSON_EXTRACT.search( - self._row.shared_attrs or self._row.attributes + self._row.shared_attrs or self._row.attributes or "" ) - return result and result.group(1) + return result.group(1) if result else None @property - def data_entity_id(self): + def data_entity_id(self) -> str | None: """Extract the entity id from the decoded data or json.""" if self._event_data: return self._event_data.get(ATTR_ENTITY_ID) result = ENTITY_ID_JSON_EXTRACT.search(self._row.event_data) - return result and result.group(1) + return result.group(1) if result else None @property - def data_domain(self): + def attributes_friendly_name(self) -> str | None: + """Extract the friendly name from the decoded attributes or json.""" + result = FRIENDLY_NAME_JSON_EXTRACT.search( + self._row.shared_attrs or self._row.attributes or "" + ) + return result.group(1) if result else None + + @property + def data_domain(self) -> str | None: """Extract the domain from the decoded data or json.""" - if self._event_data: - return self._event_data.get(ATTR_DOMAIN) - result = DOMAIN_JSON_EXTRACT.search(self._row.event_data) - return result and result.group(1) + return result.group(1) if result else None @property - def attributes(self): - """State attributes.""" - if self._attributes is None: - source = self._row.shared_attrs or self._row.attributes - if source == EMPTY_JSON_OBJECT or source is None: - self._attributes = {} - else: - self._attributes = json.loads(source) - return self._attributes - - @property - def data(self): + def data(self) -> dict[str, Any]: """Event data.""" if not self._event_data: - if self._row.event_data == EMPTY_JSON_OBJECT: - self._event_data = {} + source: str = self._row.event_data + if event_data := self._event_data_cache.get(source): + self._event_data = event_data else: - self._event_data = json.loads(self._row.event_data) + self._event_data = self._event_data_cache[source] = cast( + dict[str, Any], json.loads(source) + ) return self._event_data @property - def time_fired_isoformat(self): + def time_fired_isoformat(self) -> dt | None: """Time event was fired in utc isoformat.""" if not self._time_fired_isoformat: - self._time_fired_isoformat = process_timestamp_to_utc_isoformat( - self._row.time_fired or dt_util.utcnow() + self._time_fired_isoformat = ( + process_timestamp_to_utc_isoformat(self._row.time_fired) + or dt_util.utcnow() ) return self._time_fired_isoformat @@ -841,15 +900,19 @@ class EntityAttributeCache: if attribute in self._cache[entity_id]: return self._cache[entity_id][attribute] else: - self._cache[entity_id] = {} + cache = self._cache[entity_id] = {} 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) + cache[attribute] = current_state.attributes.get(attribute) else: # If the entity has been removed, decode the attributes # instead - self._cache[entity_id][attribute] = event.attributes.get(attribute) + if attribute != ATTR_FRIENDLY_NAME: + raise ValueError( + f"{attribute} is not supported by {self.__class__.__name__}" + ) + cache[attribute] = event.attributes_friendly_name - return self._cache[entity_id][attribute] + return cache[attribute] diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index 35df75d5a1c..2062ef231b9 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -404,4 +404,4 @@ def _create_state_changed_event_from_old_new( # pylint: disable=import-outside-toplevel from homeassistant.components import logbook - return logbook.LazyEventPartialState(row) + return logbook.LazyEventPartialState(row, {}) diff --git a/mypy.ini b/mypy.ini index c35e4d471c8..a4f7ef80d30 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1292,6 +1292,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.logbook.*] +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.lookin.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index e1975b52e3c..68b9169224e 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -113,6 +113,35 @@ async def test_service_call_create_logbook_entry(hass_): assert last_call.data.get(logbook.ATTR_DOMAIN) == "logbook" +async def test_service_call_create_logbook_entry_invalid_entity_id(hass, recorder_mock): + """Test if service call create log book entry with an invalid entity id.""" + await async_setup_component(hass, "logbook", {}) + await hass.async_block_till_done() + hass.bus.async_fire( + logbook.EVENT_LOGBOOK_ENTRY, + { + logbook.ATTR_NAME: "Alarm", + logbook.ATTR_MESSAGE: "is triggered", + logbook.ATTR_DOMAIN: "switch", + logbook.ATTR_ENTITY_ID: 1234, + }, + ) + await async_wait_recording_done(hass) + + events = list( + logbook._get_events( + hass, + dt_util.utcnow() - timedelta(hours=1), + dt_util.utcnow() + timedelta(hours=1), + ) + ) + assert len(events) == 1 + assert events[0][logbook.ATTR_DOMAIN] == "switch" + assert events[0][logbook.ATTR_NAME] == "Alarm" + assert events[0][logbook.ATTR_ENTITY_ID] == 1234 + assert events[0][logbook.ATTR_MESSAGE] == "is triggered" + + async def test_service_call_create_log_book_entry_no_message(hass_): """Test if service call create log book entry without message.""" calls = async_capture_events(hass_, logbook.EVENT_LOGBOOK_ENTRY) @@ -175,6 +204,14 @@ def test_home_assistant_start_stop_grouped(hass_): ) +def test_unsupported_attributes_in_cache_throws(hass): + """Test unsupported attributes in cache.""" + entity_attr_cache = logbook.EntityAttributeCache(hass) + event = MockLazyEventPartialState(EVENT_STATE_CHANGED) + with pytest.raises(ValueError): + entity_attr_cache.get("sensor.xyz", "not_supported", event) + + def test_home_assistant_start(hass_): """Test if HA start is not filtered or converted into a restart.""" entity_id = "switch.bla" @@ -295,7 +332,7 @@ def create_state_changed_event_from_old_new( row.context_parent_id = None row.old_state_id = old_state and 1 row.state_id = new_state and 1 - return logbook.LazyEventPartialState(row) + return logbook.LazyEventPartialState(row, {}) async def test_logbook_view(hass, hass_client, recorder_mock): @@ -307,6 +344,26 @@ async def test_logbook_view(hass, hass_client, recorder_mock): assert response.status == HTTPStatus.OK +async def test_logbook_view_invalid_start_date_time(hass, hass_client, recorder_mock): + """Test the logbook view with an invalid date time.""" + await async_setup_component(hass, "logbook", {}) + await async_recorder_block_till_done(hass) + client = await hass_client() + response = await client.get("/api/logbook/INVALID") + assert response.status == HTTPStatus.BAD_REQUEST + + +async def test_logbook_view_invalid_end_date_time(hass, hass_client, recorder_mock): + """Test the logbook view.""" + await async_setup_component(hass, "logbook", {}) + await async_recorder_block_till_done(hass) + client = await hass_client() + response = await client.get( + f"/api/logbook/{dt_util.utcnow().isoformat()}?end_time=INVALID" + ) + assert response.status == HTTPStatus.BAD_REQUEST + + async def test_logbook_view_period_entity(hass, hass_client, recorder_mock, set_utc): """Test the logbook view with period and entity.""" await async_setup_component(hass, "logbook", {}) @@ -1437,6 +1494,34 @@ async def test_icon_and_state(hass, hass_client, recorder_mock): assert response_json[2]["state"] == STATE_OFF +async def test_fire_logbook_entries(hass, hass_client, recorder_mock): + """Test many logbook entry calls.""" + await async_setup_component(hass, "logbook", {}) + await async_recorder_block_till_done(hass) + + for _ in range(10): + hass.bus.async_fire( + logbook.EVENT_LOGBOOK_ENTRY, + { + logbook.ATTR_NAME: "Alarm", + logbook.ATTR_MESSAGE: "is triggered", + logbook.ATTR_DOMAIN: "switch", + logbook.ATTR_ENTITY_ID: "sensor.xyz", + }, + ) + hass.bus.async_fire( + logbook.EVENT_LOGBOOK_ENTRY, + {}, + ) + await async_wait_recording_done(hass) + + client = await hass_client() + response_json = await _async_fetch_logbook(client) + + # The empty events should be skipped + assert len(response_json) == 10 + + async def test_exclude_events_domain(hass, hass_client, recorder_mock): """Test if events are filtered if domain is excluded in config.""" entity_id = "switch.bla" From c23866e5e59cfec3085faa37a0cc5a56135a7064 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 May 2022 21:01:17 -0500 Subject: [PATCH 0065/3516] De-duplicate event data into a new event_data table (#71135) --- homeassistant/components/logbook/__init__.py | 59 ++-- homeassistant/components/recorder/__init__.py | 68 +++- .../components/recorder/migration.py | 6 +- homeassistant/components/recorder/models.py | 68 +++- homeassistant/components/recorder/purge.py | 322 +++++++++++++++++- tests/components/recorder/test_init.py | 53 ++- tests/components/recorder/test_models.py | 11 +- 7 files changed, 526 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index b136c7330ac..96e6b975e32 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -28,6 +28,7 @@ from homeassistant.components.history import ( from homeassistant.components.http import HomeAssistantView from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.models import ( + EventData, Events, StateAttributes, States, @@ -512,6 +513,7 @@ def _get_events( # When entity_matches_only is provided, contexts and events that do not # contain the entity_ids are not included in the logbook response. query = _apply_event_entity_id_matchers(query, entity_ids) + query = query.outerjoin(EventData, (Events.data_id == EventData.data_id)) query = query.union_all( _generate_states_query( @@ -535,6 +537,8 @@ def _get_events( if context_id is not None: query = query.filter(Events.context_id == context_id) + query = query.outerjoin(EventData, (Events.data_id == EventData.data_id)) + query = query.order_by(Events.time_fired) return list( @@ -545,6 +549,18 @@ def _get_events( def _generate_events_query(session: Session) -> Query: return session.query( *EVENT_COLUMNS, + EventData.shared_data, + States.state, + States.entity_id, + States.attributes, + StateAttributes.shared_attrs, + ) + + +def _generate_events_query_without_data(session: Session) -> Query: + return session.query( + *EVENT_COLUMNS, + literal(value=None, type_=sqlalchemy.Text).label("shared_data"), States.state, States.entity_id, States.attributes, @@ -555,6 +571,7 @@ def _generate_events_query(session: Session) -> Query: def _generate_events_query_without_states(session: Session) -> Query: return session.query( *EVENT_COLUMNS, + EventData.shared_data, literal(value=None, type_=sqlalchemy.String).label("state"), literal(value=None, type_=sqlalchemy.String).label("entity_id"), literal(value=None, type_=sqlalchemy.Text).label("attributes"), @@ -570,7 +587,7 @@ def _generate_states_query( entity_ids: Iterable[str], ) -> Query: return ( - _generate_events_query(session) + _generate_events_query_without_data(session) .outerjoin(Events, (States.event_id == Events.event_id)) .outerjoin(old_state, (States.old_state_id == old_state.state_id)) .filter(_missing_state_matcher(old_state)) @@ -671,14 +688,12 @@ def _apply_event_types_filter( def _apply_event_entity_id_matchers( events_query: Query, entity_ids: Iterable[str] ) -> Query: - return events_query.filter( - sqlalchemy.or_( - *( - Events.event_data.like(ENTITY_ID_JSON_TEMPLATE.format(entity_id)) - for entity_id in entity_ids - ) - ) - ) + ors = [] + for entity_id in entity_ids: + like = ENTITY_ID_JSON_TEMPLATE.format(entity_id) + ors.append(Events.event_data.like(like)) + ors.append(EventData.shared_data.like(like)) + return events_query.filter(sqlalchemy.or_(*ors)) def _keep_event( @@ -840,7 +855,17 @@ class LazyEventPartialState: if self._event_data: return self._event_data.get(ATTR_ENTITY_ID) - result = ENTITY_ID_JSON_EXTRACT.search(self._row.event_data) + result = ENTITY_ID_JSON_EXTRACT.search( + self._row.shared_data or self._row.event_data or "" + ) + return result.group(1) if result else None + + @property + def data_domain(self) -> str | None: + """Extract the domain from the decoded data or json.""" + result = DOMAIN_JSON_EXTRACT.search( + self._row.shared_data or self._row.event_data or "" + ) return result.group(1) if result else None @property @@ -851,18 +876,14 @@ class LazyEventPartialState: ) return result.group(1) if result else None - @property - def data_domain(self) -> str | None: - """Extract the domain from the decoded data or json.""" - result = DOMAIN_JSON_EXTRACT.search(self._row.event_data) - return result.group(1) if result else None - @property def data(self) -> dict[str, Any]: """Event data.""" - if not self._event_data: - source: str = self._row.event_data - if event_data := self._event_data_cache.get(source): + if self._event_data is None: + source: str = self._row.shared_data or self._row.event_data + if not source: + self._event_data = {} + elif event_data := self._event_data_cache.get(source): self._event_data = event_data else: self._event_data = self._event_data_cache[source] = cast( diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index ef53721efd2..d190ebd0a99 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -80,6 +80,7 @@ from .const import ( from .executor import DBInterruptibleThreadPoolExecutor from .models import ( Base, + EventData, Events, StateAttributes, States, @@ -158,6 +159,7 @@ EXPIRE_AFTER_COMMITS = 120 # - How frequently states with overlapping attributes will change # - How much memory our low end hardware has STATE_ATTRIBUTES_ID_CACHE_SIZE = 2048 +EVENT_DATA_ID_CACHE_SIZE = 2048 SHUTDOWN_TASK = object() @@ -639,10 +641,13 @@ class Recorder(threading.Thread): self._commits_without_expire = 0 self._old_states: dict[str, States] = {} self._state_attributes_ids: LRU = LRU(STATE_ATTRIBUTES_ID_CACHE_SIZE) + self._event_data_ids: LRU = LRU(EVENT_DATA_ID_CACHE_SIZE) self._pending_state_attributes: dict[str, StateAttributes] = {} + self._pending_event_data: dict[str, EventData] = {} self._pending_expunge: list[States] = [] self._bakery = bakery self._find_shared_attr_query: Query | None = None + self._find_shared_data_query: Query | None = None self.event_session: Session | None = None self.get_session: Callable[[], Session] | None = None self._completed_first_database_setup: bool | None = None @@ -1162,17 +1167,61 @@ class Recorder(threading.Thread): return cast(int, attributes[0]) return None + def _find_shared_data_in_db(self, data_hash: int, shared_data: str) -> int | None: + """Find shared event data in the db from the hash and shared_attrs.""" + # + # Avoid the event session being flushed since it will + # commit all the pending events and states to the database. + # + # The lookup has already have checked to see if the data is cached + # or going to be written in the next commit so there is no + # need to flush before checking the database. + # + assert self.event_session is not None + if self._find_shared_data_query is None: + self._find_shared_data_query = self._bakery( + lambda session: session.query(EventData.data_id) + .filter(EventData.hash == bindparam("data_hash")) + .filter(EventData.shared_data == bindparam("shared_data")) + ) + with self.event_session.no_autoflush: + if ( + data_id := self._find_shared_data_query(self.event_session) + .params(data_hash=data_hash, shared_data=shared_data) + .first() + ): + return cast(int, data_id[0]) + return None + def _process_event_into_session(self, event: Event) -> None: assert self.event_session is not None + dbevent = Events.from_event(event) - try: - if event.event_type == EVENT_STATE_CHANGED: - dbevent = Events.from_event(event, event_data=None) + if event.event_type != EVENT_STATE_CHANGED and event.data: + try: + shared_data = EventData.shared_data_from_event(event) + except (TypeError, ValueError): + _LOGGER.warning("Event is not JSON serializable: %s", event) + return + + # Matching attributes found in the pending commit + if pending_event_data := self._pending_event_data.get(shared_data): + dbevent.event_data_rel = pending_event_data + # Matching attributes id found in the cache + elif data_id := self._event_data_ids.get(shared_data): + dbevent.data_id = data_id else: - dbevent = Events.from_event(event) - except (TypeError, ValueError): - _LOGGER.warning("Event is not JSON serializable: %s", event) - return + data_hash = EventData.hash_shared_data(shared_data) + # Matching attributes found in the database + if data_id := self._find_shared_data_in_db(data_hash, shared_data): + self._event_data_ids[shared_data] = dbevent.data_id = data_id + # No matching attributes found, save them in the DB + else: + dbevent_data = EventData(shared_data=shared_data, hash=data_hash) + dbevent.event_data_rel = self._pending_event_data[ + shared_data + ] = dbevent_data + self.event_session.add(dbevent_data) self.event_session.add(dbevent) if event.event_type != EVENT_STATE_CHANGED: @@ -1286,6 +1335,9 @@ class Recorder(threading.Thread): state_attr.shared_attrs ] = state_attr.attributes_id self._pending_state_attributes = {} + for event_data in self._pending_event_data.values(): + self._event_data_ids[event_data.shared_data] = event_data.data_id + self._pending_event_data = {} # Expire is an expensive operation (frequently more expensive # than the flush and commit itself) so we only @@ -1307,7 +1359,9 @@ class Recorder(threading.Thread): """Close the event session.""" self._old_states = {} self._state_attributes_ids = {} + self._event_data_ids = {} self._pending_state_attributes = {} + self._pending_event_data = {} if not self.event_session: return diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 94614fe8cff..2ef51824737 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -380,6 +380,8 @@ def _apply_update(instance, new_version, old_version): # noqa: C901 """Perform operations to bring schema up to date.""" engine = instance.engine dialect = engine.dialect.name + big_int = "INTEGER(20)" if dialect == "mysql" else "INTEGER" + if new_version == 1: _create_index(instance, "events", "ix_events_time_fired") elif new_version == 2: @@ -643,11 +645,13 @@ def _apply_update(instance, new_version, old_version): # noqa: C901 "ix_statistics_short_term_statistic_id_start", ) elif new_version == 25: - big_int = "INTEGER(20)" if dialect == "mysql" else "INTEGER" _add_columns(instance, "states", [f"attributes_id {big_int}"]) _create_index(instance, "states", "ix_states_attributes_id") elif new_version == 26: _create_index(instance, "statistics_runs", "ix_statistics_runs_start") + elif new_version == 27: + _add_columns(instance, "events", [f"data_id {big_int}"]) + _create_index(instance, "events", "ix_events_data_id") 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 5b402dec7a3..212c0c7e7d4 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -35,7 +35,6 @@ from homeassistant.const import ( MAX_LENGTH_STATE_STATE, ) from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id -from homeassistant.helpers.typing import UNDEFINED, UndefinedType import homeassistant.util.dt as dt_util from .const import ALL_DOMAIN_EXCLUDE_ATTRS, JSON_DUMP @@ -44,13 +43,14 @@ from .const import ALL_DOMAIN_EXCLUDE_ATTRS, JSON_DUMP # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 26 +SCHEMA_VERSION = 27 _LOGGER = logging.getLogger(__name__) DB_TIMEZONE = "+00:00" TABLE_EVENTS = "events" +TABLE_EVENT_DATA = "event_data" TABLE_STATES = "states" TABLE_STATE_ATTRIBUTES = "state_attributes" TABLE_RECORDER_RUNS = "recorder_runs" @@ -60,6 +60,9 @@ TABLE_STATISTICS_META = "statistics_meta" TABLE_STATISTICS_RUNS = "statistics_runs" TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" +# Only add TABLE_STATE_ATTRIBUTES and TABLE_EVENT_DATA +# to the below list once we want to check for their +# instance in the sanity check. ALL_TABLES = [ TABLE_STATES, TABLE_EVENTS, @@ -103,24 +106,24 @@ class Events(Base): # type: ignore[misc,valid-type] context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + data_id = Column(Integer, ForeignKey("event_data.data_id"), index=True) + event_data_rel = relationship("EventData") def __repr__(self) -> str: """Return string representation of instance for debugging.""" return ( f"" + f", data_id={self.data_id})>" ) @staticmethod - def from_event( - event: Event, event_data: UndefinedType | None = UNDEFINED - ) -> Events: + def from_event(event: Event) -> Events: """Create an event database object from a native event.""" return Events( event_type=event.event_type, - event_data=JSON_DUMP(event.data) if event_data is UNDEFINED else event_data, + event_data=None, origin=str(event.origin.value), time_fired=event.time_fired, context_id=event.context.id, @@ -138,7 +141,7 @@ class Events(Base): # type: ignore[misc,valid-type] try: return Event( self.event_type, - json.loads(self.event_data), + json.loads(self.event_data) if self.event_data else {}, EventOrigin(self.origin), process_timestamp(self.time_fired), context=context, @@ -149,6 +152,53 @@ class Events(Base): # type: ignore[misc,valid-type] return None +class EventData(Base): # type: ignore[misc,valid-type] + """Event data history.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_EVENT_DATA + data_id = Column(Integer, Identity(), primary_key=True) + hash = Column(BigInteger, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> EventData: + """Create object from an event.""" + shared_data = JSON_DUMP(event.data) + return EventData( + shared_data=shared_data, hash=EventData.hash_shared_data(shared_data) + ) + + @staticmethod + def shared_data_from_event(event: Event) -> str: + """Create shared_attrs from an event.""" + return JSON_DUMP(event.data) + + @staticmethod + def hash_shared_data(shared_data: str) -> int: + """Return the hash of json encoded shared data.""" + return cast(int, fnv1a_32(shared_data.encode("utf-8"))) + + def to_native(self) -> dict[str, Any]: + """Convert to an HA state object.""" + try: + return cast(dict[str, Any], json.loads(self.shared_data)) + except ValueError: + _LOGGER.exception("Error converting row to event data: %s", self) + return {} + + class States(Base): # type: ignore[misc,valid-type] """State change history.""" diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index d4061a69bab..4aad3a28a88 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -17,6 +17,7 @@ from homeassistant.const import EVENT_STATE_CHANGED from .const import MAX_ROWS_TO_PURGE from .models import ( + EventData, Events, RecorderRuns, StateAttributes, @@ -53,7 +54,8 @@ def purge_old_data( event_ids, state_ids, attributes_ids, - ) = _select_event_state_and_attributes_ids_to_purge(session, purge_before) + data_ids, + ) = _select_event_state_attributes_ids_data_ids_to_purge(session, purge_before) statistics_runs = _select_statistics_runs_to_purge(session, purge_before) short_term_statistics = _select_short_term_statistics_to_purge( session, purge_before @@ -70,6 +72,11 @@ def purge_old_data( if event_ids: _purge_event_ids(session, event_ids) + if unused_data_ids_set := _select_unused_event_data_ids( + session, data_ids, using_sqlite + ): + _purge_event_data_ids(instance, session, unused_data_ids_set) + if statistics_runs: _purge_statistics_runs(session, statistics_runs) @@ -91,12 +98,14 @@ def purge_old_data( return True -def _select_event_state_and_attributes_ids_to_purge( +def _select_event_state_attributes_ids_data_ids_to_purge( session: Session, purge_before: datetime -) -> tuple[set[int], set[int], set[int]]: +) -> tuple[set[int], set[int], set[int], set[int]]: """Return a list of event, state, and attribute ids to purge.""" events = ( - session.query(Events.event_id, States.state_id, States.attributes_id) + session.query( + Events.event_id, Events.data_id, States.state_id, States.attributes_id + ) .outerjoin(States, Events.event_id == States.event_id) .filter(Events.time_fired < purge_before) .limit(MAX_ROWS_TO_PURGE) @@ -106,13 +115,16 @@ def _select_event_state_and_attributes_ids_to_purge( event_ids = set() state_ids = set() attributes_ids = set() + data_ids = set() for event in events: event_ids.add(event.event_id) if event.state_id: state_ids.add(event.state_id) if event.attributes_id: attributes_ids.add(event.attributes_id) - return event_ids, state_ids, attributes_ids + if event.data_id: + data_ids.add(event.data_id) + return event_ids, state_ids, attributes_ids, data_ids def _state_attrs_exist(attr: int | None) -> Select: @@ -400,6 +412,255 @@ def _select_unused_attributes_ids( return to_remove +def _event_data_id_exist(data_id: int | None) -> Select: + """Check if a event data id exists in the events table.""" + return select(func.min(Events.data_id)).where(Events.data_id == data_id) + + +def _generate_find_data_id_lambda( + id1: int, + id2: int | None, + id3: int | None, + id4: int | None, + id5: int | None, + id6: int | None, + id7: int | None, + id8: int | None, + id9: int | None, + id10: int | None, + id11: int | None, + id12: int | None, + id13: int | None, + id14: int | None, + id15: int | None, + id16: int | None, + id17: int | None, + id18: int | None, + id19: int | None, + id20: int | None, + id21: int | None, + id22: int | None, + id23: int | None, + id24: int | None, + id25: int | None, + id26: int | None, + id27: int | None, + id28: int | None, + id29: int | None, + id30: int | None, + id31: int | None, + id32: int | None, + id33: int | None, + id34: int | None, + id35: int | None, + id36: int | None, + id37: int | None, + id38: int | None, + id39: int | None, + id40: int | None, + id41: int | None, + id42: int | None, + id43: int | None, + id44: int | None, + id45: int | None, + id46: int | None, + id47: int | None, + id48: int | None, + id49: int | None, + id50: int | None, + id51: int | None, + id52: int | None, + id53: int | None, + id54: int | None, + id55: int | None, + id56: int | None, + id57: int | None, + id58: int | None, + id59: int | None, + id60: int | None, + id61: int | None, + id62: int | None, + id63: int | None, + id64: int | None, + id65: int | None, + id66: int | None, + id67: int | None, + id68: int | None, + id69: int | None, + id70: int | None, + id71: int | None, + id72: int | None, + id73: int | None, + id74: int | None, + id75: int | None, + id76: int | None, + id77: int | None, + id78: int | None, + id79: int | None, + id80: int | None, + id81: int | None, + id82: int | None, + id83: int | None, + id84: int | None, + id85: int | None, + id86: int | None, + id87: int | None, + id88: int | None, + id89: int | None, + id90: int | None, + id91: int | None, + id92: int | None, + id93: int | None, + id94: int | None, + id95: int | None, + id96: int | None, + id97: int | None, + id98: int | None, + id99: int | None, + id100: int | None, +) -> StatementLambdaElement: + """Generate the find event data select only once. + + https://docs.sqlalchemy.org/en/14/core/connections.html#quick-guidelines-for-lambdas + """ + return lambda_stmt( + lambda: union_all( + _event_data_id_exist(id1), + _event_data_id_exist(id2), + _event_data_id_exist(id3), + _event_data_id_exist(id4), + _event_data_id_exist(id5), + _event_data_id_exist(id6), + _event_data_id_exist(id7), + _event_data_id_exist(id8), + _event_data_id_exist(id9), + _event_data_id_exist(id10), + _event_data_id_exist(id11), + _event_data_id_exist(id12), + _event_data_id_exist(id13), + _event_data_id_exist(id14), + _event_data_id_exist(id15), + _event_data_id_exist(id16), + _event_data_id_exist(id17), + _event_data_id_exist(id18), + _event_data_id_exist(id19), + _event_data_id_exist(id20), + _event_data_id_exist(id21), + _event_data_id_exist(id22), + _event_data_id_exist(id23), + _event_data_id_exist(id24), + _event_data_id_exist(id25), + _event_data_id_exist(id26), + _event_data_id_exist(id27), + _event_data_id_exist(id28), + _event_data_id_exist(id29), + _event_data_id_exist(id30), + _event_data_id_exist(id31), + _event_data_id_exist(id32), + _event_data_id_exist(id33), + _event_data_id_exist(id34), + _event_data_id_exist(id35), + _event_data_id_exist(id36), + _event_data_id_exist(id37), + _event_data_id_exist(id38), + _event_data_id_exist(id39), + _event_data_id_exist(id40), + _event_data_id_exist(id41), + _event_data_id_exist(id42), + _event_data_id_exist(id43), + _event_data_id_exist(id44), + _event_data_id_exist(id45), + _event_data_id_exist(id46), + _event_data_id_exist(id47), + _event_data_id_exist(id48), + _event_data_id_exist(id49), + _event_data_id_exist(id50), + _event_data_id_exist(id51), + _event_data_id_exist(id52), + _event_data_id_exist(id53), + _event_data_id_exist(id54), + _event_data_id_exist(id55), + _event_data_id_exist(id56), + _event_data_id_exist(id57), + _event_data_id_exist(id58), + _event_data_id_exist(id59), + _event_data_id_exist(id60), + _event_data_id_exist(id61), + _event_data_id_exist(id62), + _event_data_id_exist(id63), + _event_data_id_exist(id64), + _event_data_id_exist(id65), + _event_data_id_exist(id66), + _event_data_id_exist(id67), + _event_data_id_exist(id68), + _event_data_id_exist(id69), + _event_data_id_exist(id70), + _event_data_id_exist(id71), + _event_data_id_exist(id72), + _event_data_id_exist(id73), + _event_data_id_exist(id74), + _event_data_id_exist(id75), + _event_data_id_exist(id76), + _event_data_id_exist(id77), + _event_data_id_exist(id78), + _event_data_id_exist(id79), + _event_data_id_exist(id80), + _event_data_id_exist(id81), + _event_data_id_exist(id82), + _event_data_id_exist(id83), + _event_data_id_exist(id84), + _event_data_id_exist(id85), + _event_data_id_exist(id86), + _event_data_id_exist(id87), + _event_data_id_exist(id88), + _event_data_id_exist(id89), + _event_data_id_exist(id90), + _event_data_id_exist(id91), + _event_data_id_exist(id92), + _event_data_id_exist(id93), + _event_data_id_exist(id94), + _event_data_id_exist(id95), + _event_data_id_exist(id96), + _event_data_id_exist(id97), + _event_data_id_exist(id98), + _event_data_id_exist(id99), + _event_data_id_exist(id100), + ) + ) + + +def _select_unused_event_data_ids( + session: Session, data_ids: set[int], using_sqlite: bool +) -> set[int]: + """Return a set of event data ids that are not used by any events in the database.""" + if not data_ids: + return set() + + # See _select_unused_attributes_ids for why this function + # branches for non-sqlite databases. + if using_sqlite: + seen_ids = { + state[0] + for state in session.query(distinct(Events.data_id)) + .filter(Events.data_id.in_(data_ids)) + .all() + } + else: + seen_ids = set() + groups = [iter(data_ids)] * 100 + for data_ids_group in zip_longest(*groups, fillvalue=None): + seen_ids |= { + state[0] + for state in session.execute( + _generate_find_data_id_lambda(*data_ids_group) + ).all() + if state[0] is not None + } + to_remove = data_ids - seen_ids + _LOGGER.debug("Selected %s shared event data to remove", len(to_remove)) + return to_remove + + def _select_statistics_runs_to_purge( session: Session, purge_before: datetime ) -> list[int]: @@ -477,6 +738,21 @@ def _evict_purged_states_from_old_states_cache( old_states.pop(old_state_reversed[purged_state_id], None) +def _evict_purged_data_from_data_cache( + instance: Recorder, purged_data_ids: set[int] +) -> None: + """Evict purged data ids from the data ids cache.""" + # Make a map from data_id to the data json + event_data_ids = instance._event_data_ids # pylint: disable=protected-access + event_data_ids_reversed = { + data_id: data for data, data_id in event_data_ids.items() + } + + # Evict any purged data from the event_data_ids cache + for purged_attribute_id in purged_data_ids.intersection(event_data_ids_reversed): + event_data_ids.pop(event_data_ids_reversed[purged_attribute_id], None) + + def _evict_purged_attributes_from_attributes_cache( instance: Recorder, purged_attributes_ids: set[int] ) -> None: @@ -515,6 +791,22 @@ def _purge_attributes_ids( _evict_purged_attributes_from_attributes_cache(instance, attributes_ids) +def _purge_event_data_ids( + instance: Recorder, session: Session, data_ids: set[int] +) -> None: + """Delete old event data ids.""" + + deleted_rows = ( + session.query(EventData) + .filter(EventData.data_id.in_(data_ids)) + .delete(synchronize_session=False) + ) + _LOGGER.debug("Deleted %s data events", deleted_rows) + + # Evict any entries in the event_data_ids cache referring to a purged state + _evict_purged_data_from_data_cache(instance, data_ids) + + def _purge_statistics_runs(session: Session, statistics_runs: list[int]) -> None: """Delete by run_id.""" deleted_rows = ( @@ -623,15 +915,15 @@ 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) - .filter(Events.event_type.in_(excluded_event_types)) - .limit(MAX_ROWS_TO_PURGE) - .all() + using_sqlite = instance.using_sqlite() + event_ids, data_ids = zip( + *( + session.query(Events.event_id, Events.data_id) + .filter(Events.event_type.in_(excluded_event_types)) + .limit(MAX_ROWS_TO_PURGE) + .all() + ) ) - event_ids: list[int] = [ - event.event_id for event in events if event.event_id is not None - ] _LOGGER.debug( "Selected %s event_ids to remove that should be filtered", len(event_ids) ) @@ -641,6 +933,10 @@ def _purge_filtered_events( state_ids: set[int] = {state.state_id for state in states} _purge_state_ids(instance, session, state_ids) _purge_event_ids(session, event_ids) + if unused_data_ids_set := _select_unused_event_data_ids( + session, set(data_ids), using_sqlite + ): + _purge_event_data_ids(instance, session, unused_data_ids_set) if EVENT_STATE_CHANGED in excluded_event_types: session.query(StateAttributes).delete(synchronize_session=False) instance._state_attributes_ids = {} # pylint: disable=protected-access diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 77bd3992e72..c3d22255ccc 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -6,6 +6,7 @@ import asyncio from datetime import datetime, timedelta import sqlite3 import threading +from typing import cast from unittest.mock import Mock, patch import pytest @@ -31,6 +32,7 @@ from homeassistant.components.recorder import ( ) from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import ( + EventData, Events, RecorderRuns, StateAttributes, @@ -47,7 +49,7 @@ from homeassistant.const import ( STATE_LOCKED, STATE_UNLOCKED, ) -from homeassistant.core import Context, CoreState, HomeAssistant, callback +from homeassistant.core import Context, CoreState, Event, HomeAssistant, callback from homeassistant.setup import async_setup_component, setup_component from homeassistant.util import dt as dt_util @@ -363,14 +365,25 @@ def test_saving_event(hass, hass_recorder): wait_recording_done(hass) assert len(events) == 1 - event = events[0] + event: Event = events[0] hass.data[DATA_INSTANCE].block_till_done() + events: list[Event] = [] with session_scope(hass=hass) as session: - db_events = list(session.query(Events).filter_by(event_type=event_type)) - assert len(db_events) == 1 - db_event = db_events[0].to_native() + for select_event, event_data in ( + session.query(Events, EventData) + .filter_by(event_type=event_type) + .outerjoin(EventData, Events.data_id == EventData.data_id) + ): + select_event = cast(Events, select_event) + event_data = cast(EventData, event_data) + + native_event = select_event.to_native() + native_event.data = event_data.to_native() + events.append(native_event) + + db_event = events[0] assert event.event_type == db_event.event_type assert event.data == db_event.data @@ -427,7 +440,18 @@ def _add_events(hass, events): wait_recording_done(hass) with session_scope(hass=hass) as session: - return [ev.to_native() for ev in session.query(Events)] + events = [] + for event, event_data in session.query(Events, EventData).outerjoin( + EventData, Events.data_id == EventData.data_id + ): + event = cast(Events, event) + event_data = cast(EventData, event_data) + + native_event = event.to_native() + if event_data: + native_event.data = event_data.to_native() + events.append(native_event) + return events def _state_empty_context(hass, entity_id): @@ -1073,11 +1097,22 @@ def test_service_disable_events_not_recording(hass, hass_recorder): assert events[0] != events[1] assert events[0].data != events[1].data + db_events = [] with session_scope(hass=hass) as session: - db_events = list(session.query(Events).filter_by(event_type=event_type)) - assert len(db_events) == 1 - db_event = db_events[0].to_native() + for select_event, event_data in ( + session.query(Events, EventData) + .filter_by(event_type=event_type) + .outerjoin(EventData, Events.data_id == EventData.data_id) + ): + select_event = cast(Events, select_event) + event_data = cast(EventData, event_data) + native_event = select_event.to_native() + native_event.data = event_data.to_native() + db_events.append(native_event) + + assert len(db_events) == 1 + db_event = db_events[0] event = events[1] assert event.event_type == db_event.event_type diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index 8382afe4353..3bdb7992c7c 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -8,6 +8,7 @@ from sqlalchemy.orm import scoped_session, sessionmaker from homeassistant.components.recorder.models import ( Base, + EventData, Events, LazyState, RecorderRuns, @@ -25,7 +26,9 @@ from homeassistant.util import dt, dt as dt_util def test_from_event_to_db_event(): """Test converting event to db event.""" event = ha.Event("test_event", {"some_data": 15}) - assert event == Events.from_event(event).to_native() + db_event = Events.from_event(event) + db_event.event_data = EventData.from_event(event).shared_data + assert event == db_event.to_native() def test_from_event_to_db_state(): @@ -231,10 +234,12 @@ async def test_event_to_db_model(): event = ha.Event( "state_changed", {"some": "attr"}, ha.EventOrigin.local, dt_util.utcnow() ) - native = Events.from_event(event).to_native() + db_event = Events.from_event(event) + db_event.event_data = EventData.from_event(event).shared_data + native = db_event.to_native() assert native == event - native = Events.from_event(event, event_data="{}").to_native() + native = Events.from_event(event).to_native() event.data = {} assert native == event From b770ca319e640e2af24221dfe96d9af530c85083 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 May 2022 21:04:05 -0500 Subject: [PATCH 0066/3516] Improve scrape performance by using lxml parser (#71087) * Improve scape performance by using lxml parser * load it * tweak * tweak * ensure libxml2 is installed in dev container --- Dockerfile.dev | 1 + homeassistant/components/scrape/manifest.json | 2 +- homeassistant/components/scrape/sensor.py | 2 +- requirements_all.txt | 3 +++ requirements_test_all.txt | 3 +++ 5 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Dockerfile.dev b/Dockerfile.dev index dc04efe56fb..322c63f53dd 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -18,6 +18,7 @@ RUN \ libavfilter-dev \ libpcap-dev \ libturbojpeg0 \ + libxml2 \ git \ cmake \ && apt-get clean \ diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index bf5865206e4..b1ccbb354a9 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -2,7 +2,7 @@ "domain": "scrape", "name": "Scrape", "documentation": "https://www.home-assistant.io/integrations/scrape", - "requirements": ["beautifulsoup4==4.11.1"], + "requirements": ["beautifulsoup4==4.11.1", "lxml==4.8.0"], "after_dependencies": ["rest"], "codeowners": ["@fabaff"], "iot_class": "cloud_polling" diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 8f2a672ef06..e15f7c5ba97 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -154,7 +154,7 @@ class ScrapeSensor(SensorEntity): def _extract_value(self) -> Any: """Parse the html extraction in the executor.""" - raw_data = BeautifulSoup(self.rest.data, "html.parser") + raw_data = BeautifulSoup(self.rest.data, "lxml") _LOGGER.debug(raw_data) try: diff --git a/requirements_all.txt b/requirements_all.txt index a331efde6c2..dd808efa0bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -974,6 +974,9 @@ lupupy==0.0.24 # homeassistant.components.lw12wifi lw12==0.9.2 +# homeassistant.components.scrape +lxml==4.8.0 + # homeassistant.components.nmap_tracker mac-vendor-lookup==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67bba2141d5..26f1c86d1af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -663,6 +663,9 @@ lru-dict==1.1.7 # homeassistant.components.luftdaten luftdaten==0.7.2 +# homeassistant.components.scrape +lxml==4.8.0 + # homeassistant.components.nmap_tracker mac-vendor-lookup==0.1.11 From 2a9f043039dc60fc25bc14bab724419bedf746bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 May 2022 23:44:54 -0500 Subject: [PATCH 0067/3516] Use ULID short format for context ids (#71119) --- homeassistant/core.py | 2 +- homeassistant/util/ulid.py | 63 +++++++++++++++++++++++++++++++++----- tests/util/test_ulid.py | 7 ++++- 3 files changed, 63 insertions(+), 9 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 7245dad7864..d6b1ac85d8b 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -695,7 +695,7 @@ class Context: user_id: str | None = attr.ib(default=None) parent_id: str | None = attr.ib(default=None) - id: str = attr.ib(factory=ulid_util.ulid_hex) + id: str = attr.ib(factory=ulid_util.ulid) def as_dict(self) -> dict[str, str | None]: """Return a dictionary representation of the context.""" diff --git a/homeassistant/util/ulid.py b/homeassistant/util/ulid.py index 888b5b2074b..c38d95bd169 100644 --- a/homeassistant/util/ulid.py +++ b/homeassistant/util/ulid.py @@ -4,11 +4,21 @@ from random import getrandbits import time -# In the future once we require python 3.10 and above, we can -# create a new function that uses base64.b32encodehex to shorten -# these to 26 characters. def ulid_hex() -> str: - """Generate a ULID in hex that will work for a UUID. + """Generate a ULID in lowercase hex that will work for a UUID. + + This ulid should not be used for cryptographically secure + operations. + + This string can be converted with https://github.com/ahawker/ulid + + ulid.from_uuid(uuid.UUID(ulid_hex)) + """ + return f"{int(time.time()*1000):012x}{getrandbits(80):020x}" + + +def ulid() -> str: + """Generate a ULID. This ulid should not be used for cryptographically secure operations. @@ -18,8 +28,47 @@ def ulid_hex() -> str: Timestamp Randomness 48bits 80bits - This string can be converted with https://github.com/ahawker/ulid + This string can be loaded directly with https://github.com/ahawker/ulid - ulid.from_uuid(uuid.UUID(value)) + import homeassistant.util.ulid as ulid_util + import ulid + ulid.parse(ulid_util.ulid()) """ - return f"{int(time.time()*1000):012x}{getrandbits(80):020x}" + ulid_bytes = int(time.time() * 1000).to_bytes(6, byteorder="big") + int( + getrandbits(80) + ).to_bytes(10, byteorder="big") + + # This is base32 crockford encoding with the loop unrolled for performance + # + # This code is adapted from: + # https://github.com/ahawker/ulid/blob/06289583e9de4286b4d80b4ad000d137816502ca/ulid/base32.py#L102 + # + enc = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" + return ( + enc[(ulid_bytes[0] & 224) >> 5] + + enc[ulid_bytes[0] & 31] + + enc[(ulid_bytes[1] & 248) >> 3] + + enc[((ulid_bytes[1] & 7) << 2) | ((ulid_bytes[2] & 192) >> 6)] + + enc[((ulid_bytes[2] & 62) >> 1)] + + enc[((ulid_bytes[2] & 1) << 4) | ((ulid_bytes[3] & 240) >> 4)] + + enc[((ulid_bytes[3] & 15) << 1) | ((ulid_bytes[4] & 128) >> 7)] + + enc[(ulid_bytes[4] & 124) >> 2] + + enc[((ulid_bytes[4] & 3) << 3) | ((ulid_bytes[5] & 224) >> 5)] + + enc[ulid_bytes[5] & 31] + + enc[(ulid_bytes[6] & 248) >> 3] + + enc[((ulid_bytes[6] & 7) << 2) | ((ulid_bytes[7] & 192) >> 6)] + + enc[(ulid_bytes[7] & 62) >> 1] + + enc[((ulid_bytes[7] & 1) << 4) | ((ulid_bytes[8] & 240) >> 4)] + + enc[((ulid_bytes[8] & 15) << 1) | ((ulid_bytes[9] & 128) >> 7)] + + enc[(ulid_bytes[9] & 124) >> 2] + + enc[((ulid_bytes[9] & 3) << 3) | ((ulid_bytes[10] & 224) >> 5)] + + enc[ulid_bytes[10] & 31] + + enc[(ulid_bytes[11] & 248) >> 3] + + enc[((ulid_bytes[11] & 7) << 2) | ((ulid_bytes[12] & 192) >> 6)] + + enc[(ulid_bytes[12] & 62) >> 1] + + enc[((ulid_bytes[12] & 1) << 4) | ((ulid_bytes[13] & 240) >> 4)] + + enc[((ulid_bytes[13] & 15) << 1) | ((ulid_bytes[14] & 128) >> 7)] + + enc[(ulid_bytes[14] & 124) >> 2] + + enc[((ulid_bytes[14] & 3) << 3) | ((ulid_bytes[15] & 224) >> 5)] + + enc[ulid_bytes[15] & 31] + ) diff --git a/tests/util/test_ulid.py b/tests/util/test_ulid.py index fc5d2af8c87..9f297bf373e 100644 --- a/tests/util/test_ulid.py +++ b/tests/util/test_ulid.py @@ -6,6 +6,11 @@ import homeassistant.util.ulid as ulid_util async def test_ulid_util_uuid_hex(): - """Verify we can generate a ulid.""" + """Verify we can generate a ulid in hex.""" assert len(ulid_util.ulid_hex()) == 32 assert uuid.UUID(ulid_util.ulid_hex()) + + +async def test_ulid_util_uuid(): + """Verify we can generate a ulid.""" + assert len(ulid_util.ulid()) == 26 From 63679d3d2927f9e6b1029bca994a3fe138480faa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 May 2022 06:49:50 +0200 Subject: [PATCH 0068/3516] Fix missing device & entity references in automations (#71103) --- .../components/automation/__init__.py | 33 +++++++--- tests/components/automation/test_init.py | 60 ++++++++++++++++--- 2 files changed, 77 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 857d2d4b49f..c743e1f83fd 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_CONDITION, CONF_DEVICE_ID, CONF_ENTITY_ID, + CONF_EVENT_DATA, CONF_ID, CONF_MODE, CONF_PLATFORM, @@ -360,9 +361,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): referenced |= condition.async_extract_devices(conf) for conf in self._trigger_config: - device = _trigger_extract_device(conf) - if device is not None: - referenced.add(device) + referenced |= set(_trigger_extract_device(conf)) self._referenced_devices = referenced return referenced @@ -764,12 +763,22 @@ async def _async_process_if(hass, name, config, p_config): @callback -def _trigger_extract_device(trigger_conf: dict) -> str | None: +def _trigger_extract_device(trigger_conf: dict) -> list[str]: """Extract devices from a trigger config.""" - if trigger_conf[CONF_PLATFORM] != "device": - return None + if trigger_conf[CONF_PLATFORM] == "device": + return [trigger_conf[CONF_DEVICE_ID]] - return trigger_conf[CONF_DEVICE_ID] + if ( + trigger_conf[CONF_PLATFORM] == "event" + and CONF_EVENT_DATA in trigger_conf + and CONF_DEVICE_ID in trigger_conf[CONF_EVENT_DATA] + ): + return [trigger_conf[CONF_EVENT_DATA][CONF_DEVICE_ID]] + + if trigger_conf[CONF_PLATFORM] == "tag" and CONF_DEVICE_ID in trigger_conf: + return trigger_conf[CONF_DEVICE_ID] + + return [] @callback @@ -778,6 +787,9 @@ def _trigger_extract_entities(trigger_conf: dict) -> list[str]: if trigger_conf[CONF_PLATFORM] in ("state", "numeric_state"): return trigger_conf[CONF_ENTITY_ID] + if trigger_conf[CONF_PLATFORM] == "calendar": + return [trigger_conf[CONF_ENTITY_ID]] + if trigger_conf[CONF_PLATFORM] == "zone": return trigger_conf[CONF_ENTITY_ID] + [trigger_conf[CONF_ZONE]] @@ -787,4 +799,11 @@ def _trigger_extract_entities(trigger_conf: dict) -> list[str]: if trigger_conf[CONF_PLATFORM] == "sun": return ["sun.sun"] + if ( + trigger_conf[CONF_PLATFORM] == "event" + and CONF_EVENT_DATA in trigger_conf + and CONF_ENTITY_ID in trigger_conf[CONF_EVENT_DATA] + ): + return [trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID]] + return [] diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 1c90abe72ca..ccb508c6acc 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1079,6 +1079,7 @@ async def test_automation_restore_last_triggered_with_initial_state(hass): async def test_extraction_functions(hass): """Test extraction functions.""" + await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) assert await async_setup_component( hass, DOMAIN, @@ -1086,7 +1087,24 @@ async def test_extraction_functions(hass): DOMAIN: [ { "alias": "test1", - "trigger": {"platform": "state", "entity_id": "sensor.trigger_1"}, + "trigger": [ + {"platform": "state", "entity_id": "sensor.trigger_state"}, + { + "platform": "numeric_state", + "entity_id": "sensor.trigger_numeric_state", + "above": 10, + }, + { + "platform": "calendar", + "entity_id": "calendar.trigger_calendar", + "event": "start", + }, + { + "platform": "event", + "event_type": "state_changed", + "event_data": {"entity_id": "sensor.trigger_event"}, + }, + ], "condition": { "condition": "state", "entity_id": "light.condition_state", @@ -1111,13 +1129,30 @@ async def test_extraction_functions(hass): }, { "alias": "test2", - "trigger": { - "platform": "device", - "domain": "light", - "type": "turned_on", - "entity_id": "light.trigger_2", - "device_id": "trigger-device-2", - }, + "trigger": [ + { + "platform": "device", + "domain": "light", + "type": "turned_on", + "entity_id": "light.trigger_2", + "device_id": "trigger-device-2", + }, + { + "platform": "tag", + "tag_id": "1234", + "device_id": "device-trigger-tag1", + }, + { + "platform": "tag", + "tag_id": "1234", + "device_id": ["device-trigger-tag2", "device-trigger-tag3"], + }, + { + "platform": "event", + "event_type": "esphome.button_pressed", + "event_data": {"device_id": "device-trigger-event"}, + }, + ], "condition": { "condition": "device", "device_id": "condition-device", @@ -1159,7 +1194,10 @@ async def test_extraction_functions(hass): "automation.test2", } assert set(automation.entities_in_automation(hass, "automation.test1")) == { - "sensor.trigger_1", + "calendar.trigger_calendar", + "sensor.trigger_state", + "sensor.trigger_numeric_state", + "sensor.trigger_event", "light.condition_state", "light.in_both", "light.in_first", @@ -1173,6 +1211,10 @@ async def test_extraction_functions(hass): "condition-device", "device-in-both", "device-in-last", + "device-trigger-event", + "device-trigger-tag1", + "device-trigger-tag2", + "device-trigger-tag3", } From 802adaf43c0d9ed344363b9b6bb6ee2e6234f7b4 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 1 May 2022 22:50:39 -0600 Subject: [PATCH 0069/3516] Fix issues with SimpliSafe email-based 2FA (#71180) * FIx issues with email-based SimpliSafe 2FA * Bump --- .../components/simplisafe/manifest.json | 2 +- homeassistant/components/simplisafe/strings.json | 2 +- .../components/simplisafe/translations/en.json | 16 +++------------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 8e4c1f5d82b..804523b3390 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==2022.04.1"], + "requirements": ["simplisafe-python==2022.05.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 45e0ef84f5a..85e579fd455 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -32,7 +32,7 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "progress": { - "email_2fa": "Input the two-factor authentication code\nsent to you via email." + "email_2fa": "Check your email for a verification link from Simplisafe." } }, "options": { diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index a3bf0049681..0da6f6442e4 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -3,23 +3,16 @@ "abort": { "already_configured": "This SimpliSafe account is already in use.", "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", - "reauth_successful": "Re-authentication was successful", - "wrong_account": "The user credentials provided do not match this SimpliSafe account." + "reauth_successful": "Re-authentication was successful" }, "error": { - "2fa_timed_out": "Timed out while waiting for two-factor authentication", - "identifier_exists": "Account already registered", "invalid_auth": "Invalid authentication", - "still_awaiting_mfa": "Still awaiting MFA email click", "unknown": "Unexpected error" }, "progress": { - "email_2fa": "Input the two-factor authentication code\nsent to you via email." + "email_2fa": "Check your email for a verification link from Simplisafe." }, "step": { - "mfa": { - "title": "SimpliSafe Multi-Factor Authentication" - }, "reauth_confirm": { "data": { "password": "Password" @@ -35,13 +28,10 @@ }, "user": { "data": { - "auth_code": "Authorization Code", - "code": "Code (used in Home Assistant UI)", "password": "Password", "username": "Username" }, - "description": "Input your username and password.", - "title": "Fill in your information." + "description": "Input your username and password." } } }, diff --git a/requirements_all.txt b/requirements_all.txt index dd808efa0bd..6b89774f4e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2156,7 +2156,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.04.1 +simplisafe-python==2022.05.0 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 26f1c86d1af..fed48ff4175 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1407,7 +1407,7 @@ sharkiq==0.0.1 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2022.04.1 +simplisafe-python==2022.05.0 # homeassistant.components.slack slackclient==2.5.0 From 205a8fc752f6ae6fd34543372ddfde4cfeaf57d2 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sat, 30 Apr 2022 00:16:05 +0100 Subject: [PATCH 0070/3516] update unit_of_measurement even if unit_of_measurement is known (#69699) --- homeassistant/components/integration/sensor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 40229404288..8cf5da5bc7c 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -196,10 +196,11 @@ class IntegrationSensor(RestoreEntity, SensorEntity): # or device_class. update_state = False - if self._unit_of_measurement is None: - unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if unit is not None: - self._unit_of_measurement = self._unit_template.format(unit) + unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + if unit is not None: + new_unit_of_measurement = self._unit_template.format(unit) + if self._unit_of_measurement != new_unit_of_measurement: + self._unit_of_measurement = new_unit_of_measurement update_state = True if ( From 51aa070e19408e59b3da795c29b546bbe5d0e721 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 29 Apr 2022 21:29:06 +0200 Subject: [PATCH 0071/3516] Fix "station is open" binary sensor in Tankerkoenig (#70928) --- homeassistant/components/tankerkoenig/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tankerkoenig/binary_sensor.py b/homeassistant/components/tankerkoenig/binary_sensor.py index 4b58ea26703..9a2b048e0b8 100644 --- a/homeassistant/components/tankerkoenig/binary_sensor.py +++ b/homeassistant/components/tankerkoenig/binary_sensor.py @@ -74,5 +74,5 @@ class StationOpenBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): @property def is_on(self) -> bool | None: """Return true if the station is open.""" - data = self.coordinator.data[self._station_id] - return data is not None and "status" in data + data: dict = self.coordinator.data[self._station_id] + return data is not None and data.get("status") == "open" From 5d37cfc61e684cbd7908db8247bbedbc9a9c2849 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Mon, 2 May 2022 00:13:21 +0100 Subject: [PATCH 0072/3516] Generic camera handle template adjacent to portnumber (#71031) --- .../components/generic/config_flow.py | 30 ++++++++----- tests/components/generic/test_config_flow.py | 42 +++++++++++++++++++ 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 4298b473681..086262aa0a1 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -130,10 +130,9 @@ async def async_test_still(hass, info) -> tuple[dict[str, str], str | None]: fmt = None if not (url := info.get(CONF_STILL_IMAGE_URL)): return {}, info.get(CONF_CONTENT_TYPE, "image/jpeg") - if not isinstance(url, template_helper.Template) and url: - url = cv.template(url) - url.hass = hass try: + if not isinstance(url, template_helper.Template): + url = template_helper.Template(url, hass) url = url.async_render(parse_result=False) except TemplateError as err: _LOGGER.warning("Problem rendering template %s: %s", url, err) @@ -168,11 +167,20 @@ async def async_test_still(hass, info) -> tuple[dict[str, str], str | None]: return {}, f"image/{fmt}" -def slug_url(url) -> str | None: +def slug(hass, template) -> str | None: """Convert a camera url into a string suitable for a camera name.""" - if not url: + if not template: return None - return slugify(yarl.URL(url).host) + if not isinstance(template, template_helper.Template): + template = template_helper.Template(template, hass) + try: + url = template.async_render(parse_result=False) + return slugify(yarl.URL(url).host) + except TemplateError as err: + _LOGGER.error("Syntax error in '%s': %s", template.template, err) + except (ValueError, TypeError) as err: + _LOGGER.error("Syntax error in '%s': %s", url, err) + return None async def async_test_stream(hass, info) -> dict[str, str]: @@ -252,6 +260,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle the start of the config flow.""" errors = {} + hass = self.hass if user_input: # Secondary validation because serialised vol can't seem to handle this complexity: if not user_input.get(CONF_STILL_IMAGE_URL) and not user_input.get( @@ -263,8 +272,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): errors = errors | await async_test_stream(self.hass, user_input) still_url = user_input.get(CONF_STILL_IMAGE_URL) stream_url = user_input.get(CONF_STREAM_SOURCE) - name = slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME - + name = slug(hass, still_url) or slug(hass, stream_url) or DEFAULT_NAME if not errors: user_input[CONF_CONTENT_TYPE] = still_format user_input[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False @@ -295,7 +303,8 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): still_url = import_config.get(CONF_STILL_IMAGE_URL) stream_url = import_config.get(CONF_STREAM_SOURCE) name = import_config.get( - CONF_NAME, slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME + CONF_NAME, + slug(self.hass, still_url) or slug(self.hass, stream_url) or DEFAULT_NAME, ) if CONF_LIMIT_REFETCH_TO_URL_CHANGE not in import_config: import_config[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False @@ -318,6 +327,7 @@ class GenericOptionsFlowHandler(OptionsFlow): ) -> FlowResult: """Manage Generic IP Camera options.""" errors: dict[str, str] = {} + hass = self.hass if user_input is not None: errors, still_format = await async_test_still( @@ -327,7 +337,7 @@ class GenericOptionsFlowHandler(OptionsFlow): still_url = user_input.get(CONF_STILL_IMAGE_URL) stream_url = user_input.get(CONF_STREAM_SOURCE) if not errors: - title = slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME + title = slug(hass, still_url) or slug(hass, stream_url) or DEFAULT_NAME if still_url is None: # If user didn't specify a still image URL, # The automatically generated still image that stream generates diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index f5411ed3ea0..457cac26aa5 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -165,6 +165,48 @@ async def test_form_only_still_sample(hass, user_flow, image_file): assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY +@respx.mock +@pytest.mark.parametrize( + ("template", "url", "expected_result"), + [ + # Test we can handle templates in strange parts of the url, #70961. + ( + "http://localhost:812{{3}}/static/icons/favicon-apple-180x180.png", + "http://localhost:8123/static/icons/favicon-apple-180x180.png", + data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + ), + ( + "{% if 1 %}https://bla{% else %}https://yo{% endif %}", + "https://bla/", + data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + ), + ( + "http://{{example.org", + "http://example.org", + data_entry_flow.RESULT_TYPE_FORM, + ), + ( + "invalid1://invalid:4\\1", + "invalid1://invalid:4%5c1", + data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + ), + ], +) +async def test_still_template( + hass, user_flow, fakeimgbytes_png, template, url, expected_result +) -> None: + """Test we can handle various templates.""" + respx.get(url).respond(stream=fakeimgbytes_png) + data = TESTDATA.copy() + data.pop(CONF_STREAM_SOURCE) + data[CONF_STILL_IMAGE_URL] = template + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + data, + ) + assert result2["type"] == expected_result + + @respx.mock async def test_form_rtsp_mode(hass, fakeimg_png, mock_av_open, user_flow): """Test we complete ok if the user enters a stream url.""" From 26b6952c06fe6fb4c6b436d83aa0da52d1467cdb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Apr 2022 23:24:26 -0500 Subject: [PATCH 0073/3516] Reduce calls to asyncio.iscoroutine (#71090) --- homeassistant/core.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 69e776a1dcd..7245dad7864 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -192,18 +192,15 @@ class HassJob(Generic[_R_co]): def __init__(self, target: Callable[..., _R_co]) -> None: """Create a job object.""" - if asyncio.iscoroutine(target): - raise ValueError("Coroutine not allowed to be passed to HassJob") - self.target = target - self.job_type = _get_callable_job_type(target) + self.job_type = _get_hassjob_callable_job_type(target) def __repr__(self) -> str: """Return the job.""" return f"" -def _get_callable_job_type(target: Callable[..., Any]) -> HassJobType: +def _get_hassjob_callable_job_type(target: Callable[..., Any]) -> HassJobType: """Determine the job type from the callable.""" # Check for partials to properly determine if coroutine function check_target = target @@ -214,6 +211,8 @@ def _get_callable_job_type(target: Callable[..., Any]) -> HassJobType: return HassJobType.Coroutinefunction if is_callback(check_target): return HassJobType.Callback + if asyncio.iscoroutine(check_target): + raise ValueError("Coroutine not allowed to be passed to HassJob") return HassJobType.Executor From 4c7c7b72b7bfe69db102f8c8c8b5721f4aa99078 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 30 Apr 2022 11:40:57 -0400 Subject: [PATCH 0074/3516] Clean up Steam integration (#71091) * Clean up Steam * uno mas * uno mas * uno mas --- .../components/steam_online/config_flow.py | 79 ++++++++----------- .../components/steam_online/const.py | 9 ++- .../components/steam_online/coordinator.py | 9 +-- .../components/steam_online/sensor.py | 6 +- .../components/steam_online/strings.json | 4 +- .../steam_online/translations/en.json | 4 +- tests/components/steam_online/__init__.py | 31 ++------ .../steam_online/test_config_flow.py | 7 +- 8 files changed, 58 insertions(+), 91 deletions(-) diff --git a/homeassistant/components/steam_online/config_flow.py b/homeassistant/components/steam_online/config_flow.py index cfac20da11b..246d54c0bff 100644 --- a/homeassistant/components/steam_online/config_flow.py +++ b/homeassistant/components/steam_online/config_flow.py @@ -13,7 +13,14 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.typing import ConfigType -from .const import CONF_ACCOUNT, CONF_ACCOUNTS, DEFAULT_NAME, DOMAIN, LOGGER +from .const import ( + CONF_ACCOUNT, + CONF_ACCOUNTS, + DEFAULT_NAME, + DOMAIN, + LOGGER, + PLACEHOLDERS, +) def validate_input(user_input: dict[str, str | int]) -> list[dict[str, str | int]]: @@ -52,14 +59,14 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if res[0] is not None: name = str(res[0]["personaname"]) else: - errors = {"base": "invalid_account"} + errors["base"] = "invalid_account" except (steam.api.HTTPError, steam.api.HTTPTimeoutError) as ex: - errors = {"base": "cannot_connect"} + errors["base"] = "cannot_connect" if "403" in str(ex): - errors = {"base": "invalid_auth"} + errors["base"] = "invalid_auth" except Exception as ex: # pylint:disable=broad-except LOGGER.exception("Unknown exception: %s", ex) - errors = {"base": "unknown"} + errors["base"] = "unknown" if not errors: entry = await self.async_set_unique_id(user_input[CONF_ACCOUNT]) if entry and self.source == config_entries.SOURCE_REAUTH: @@ -70,20 +77,12 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if self.source == config_entries.SOURCE_IMPORT: accounts_data = { CONF_ACCOUNTS: { - acc["steamid"]: { - "name": acc["personaname"], - "enabled": True, - } - for acc in res + acc["steamid"]: acc["personaname"] for acc in res } } user_input.pop(CONF_ACCOUNTS) else: - accounts_data = { - CONF_ACCOUNTS: { - user_input[CONF_ACCOUNT]: {"name": name, "enabled": True} - } - } + accounts_data = {CONF_ACCOUNTS: {user_input[CONF_ACCOUNT]: name}} return self.async_create_entry( title=name or DEFAULT_NAME, data=user_input, @@ -103,6 +102,7 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } ), errors=errors, + description_placeholders=PLACEHOLDERS, ) async def async_step_import(self, import_config: ConfigType) -> FlowResult: @@ -111,7 +111,7 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if entry.data[CONF_API_KEY] == import_config[CONF_API_KEY]: return self.async_abort(reason="already_configured") LOGGER.warning( - "Steam yaml config in now deprecated and has been imported. " + "Steam yaml config is now deprecated and has been imported. " "Please remove it from your config" ) import_config[CONF_ACCOUNT] = import_config[CONF_ACCOUNTS][0] @@ -131,7 +131,9 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() self._set_confirm_only() - return self.async_show_form(step_id="reauth_confirm") + return self.async_show_form( + step_id="reauth_confirm", description_placeholders=PLACEHOLDERS + ) class SteamOptionsFlowHandler(config_entries.OptionsFlow): @@ -148,56 +150,38 @@ class SteamOptionsFlowHandler(config_entries.OptionsFlow): """Manage Steam options.""" if user_input is not None: await self.hass.config_entries.async_unload(self.entry.entry_id) - for k in self.options[CONF_ACCOUNTS]: - if ( - self.options[CONF_ACCOUNTS][k]["enabled"] - and k not in user_input[CONF_ACCOUNTS] - and ( - entity_id := er.async_get(self.hass).async_get_entity_id( - Platform.SENSOR, DOMAIN, f"sensor.steam_{k}" - ) + for _id in self.options[CONF_ACCOUNTS]: + if _id not in user_input[CONF_ACCOUNTS] and ( + entity_id := er.async_get(self.hass).async_get_entity_id( + Platform.SENSOR, DOMAIN, f"sensor.steam_{_id}" ) ): er.async_get(self.hass).async_remove(entity_id) channel_data = { CONF_ACCOUNTS: { - k: { - "name": v["name"], - "enabled": k in user_input[CONF_ACCOUNTS], - } - for k, v in self.options[CONF_ACCOUNTS].items() - if k in user_input[CONF_ACCOUNTS] + _id: name + for _id, name in self.options[CONF_ACCOUNTS].items() + if _id in user_input[CONF_ACCOUNTS] } } await self.hass.config_entries.async_reload(self.entry.entry_id) return self.async_create_entry(title="", data=channel_data) try: users = { - name["steamid"]: {"name": name["personaname"], "enabled": False} + name["steamid"]: name["personaname"] for name in await self.hass.async_add_executor_job(self.get_accounts) } except steam.api.HTTPTimeoutError: users = self.options[CONF_ACCOUNTS] - _users = users | self.options[CONF_ACCOUNTS] - self.options[CONF_ACCOUNTS] = { - k: v - for k, v in _users.items() - if k in users or self.options[CONF_ACCOUNTS][k]["enabled"] - } options = { vol.Required( CONF_ACCOUNTS, - default={ - k - for k in self.options[CONF_ACCOUNTS] - if self.options[CONF_ACCOUNTS][k]["enabled"] - }, - ): cv.multi_select( - {k: v["name"] for k, v in self.options[CONF_ACCOUNTS].items()} - ), + default=set(self.options[CONF_ACCOUNTS]), + ): cv.multi_select(users | self.options[CONF_ACCOUNTS]), } + self.options[CONF_ACCOUNTS] = users | self.options[CONF_ACCOUNTS] return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) @@ -205,7 +189,6 @@ class SteamOptionsFlowHandler(config_entries.OptionsFlow): """Get accounts.""" interface = steam.api.interface("ISteamUser") friends = interface.GetFriendList(steamid=self.entry.data[CONF_ACCOUNT]) - friends = friends["friendslist"]["friends"] - _users_str = [user["steamid"] for user in friends] + _users_str = [user["steamid"] for user in friends["friendslist"]["friends"]] names = interface.GetPlayerSummaries(steamids=_users_str) return names["response"]["players"]["player"] diff --git a/homeassistant/components/steam_online/const.py b/homeassistant/components/steam_online/const.py index 63206230073..01f4410a7c9 100644 --- a/homeassistant/components/steam_online/const.py +++ b/homeassistant/components/steam_online/const.py @@ -11,6 +11,11 @@ DOMAIN: Final = "steam_online" LOGGER = logging.getLogger(__package__) +PLACEHOLDERS = { + "api_key_url": "https://steamcommunity.com/dev/apikey", + "account_id_url": "https://steamid.io", +} + STATE_OFFLINE = "offline" STATE_ONLINE = "online" STATE_BUSY = "busy" @@ -30,6 +35,4 @@ STEAM_STATUSES = { STEAM_API_URL = "https://steamcdn-a.akamaihd.net/steam/apps/" STEAM_HEADER_IMAGE_FILE = "header.jpg" STEAM_MAIN_IMAGE_FILE = "capsule_616x353.jpg" -STEAM_ICON_URL = ( - "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/%d/%s.jpg" -) +STEAM_ICON_URL = "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/" diff --git a/homeassistant/components/steam_online/coordinator.py b/homeassistant/components/steam_online/coordinator.py index 8f535999247..78c850b0ac9 100644 --- a/homeassistant/components/steam_online/coordinator.py +++ b/homeassistant/components/steam_online/coordinator.py @@ -28,7 +28,7 @@ class SteamDataUpdateCoordinator(DataUpdateCoordinator): name=DOMAIN, update_interval=timedelta(seconds=30), ) - self.game_icons: dict = {} + self.game_icons: dict[int, str] = {} self.player_interface: INTMethod = None self.user_interface: INTMethod = None steam.api.key.set(self.config_entry.data[CONF_API_KEY]) @@ -36,7 +36,7 @@ class SteamDataUpdateCoordinator(DataUpdateCoordinator): def _update(self) -> dict[str, dict[str, str | int]]: """Fetch data from API endpoint.""" accounts = self.config_entry.options[CONF_ACCOUNTS] - _ids = [k for k in accounts if accounts[k]["enabled"]] + _ids = list(accounts) if not self.user_interface or not self.player_interface: self.user_interface = steam.api.interface("ISteamUser") self.player_interface = steam.api.interface("IPlayerService") @@ -46,7 +46,7 @@ class SteamDataUpdateCoordinator(DataUpdateCoordinator): steamid=_id, include_appinfo=1 )["response"] self.game_icons = self.game_icons | { - game["appid"]: game["img_icon_url"] for game in res.get("games", {}) + game["appid"]: game["img_icon_url"] for game in res.get("games", []) } response = self.user_interface.GetPlayerSummaries(steamids=_ids) players = { @@ -56,8 +56,7 @@ class SteamDataUpdateCoordinator(DataUpdateCoordinator): } for k in players: data = self.player_interface.GetSteamLevel(steamid=players[k]["steamid"]) - data = data["response"] - players[k]["level"] = data["player_level"] + players[k]["level"] = data["response"]["player_level"] return players async def _async_update_data(self) -> dict[str, dict[str, str | int]]: diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py index 466bd46f38f..be175b41b66 100644 --- a/homeassistant/components/steam_online/sensor.py +++ b/homeassistant/components/steam_online/sensor.py @@ -65,7 +65,6 @@ async def async_setup_entry( async_add_entities( SteamSensor(hass.data[DOMAIN][entry.entry_id], account) for account in entry.options[CONF_ACCOUNTS] - if entry.options[CONF_ACCOUNTS][account]["enabled"] ) @@ -106,10 +105,7 @@ class SteamSensor(SteamEntity, SensorEntity): attrs["game_image_header"] = f"{game_url}{STEAM_HEADER_IMAGE_FILE}" attrs["game_image_main"] = f"{game_url}{STEAM_MAIN_IMAGE_FILE}" if info := self._get_game_icon(player): - attrs["game_icon"] = STEAM_ICON_URL % ( - game_id, - info, - ) + attrs["game_icon"] = f"{STEAM_ICON_URL}{game_id}/{info}.jpg" self._attr_name = player["personaname"] self._attr_entity_picture = player["avatarmedium"] if last_online := player.get("lastlogoff"): diff --git a/homeassistant/components/steam_online/strings.json b/homeassistant/components/steam_online/strings.json index 67484f0276b..6d80bb77f1b 100644 --- a/homeassistant/components/steam_online/strings.json +++ b/homeassistant/components/steam_online/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "Use https://steamid.io to find your Steam account ID", + "description": "Use {account_id_url} to find your Steam account ID", "data": { "api_key": "[%key:common::config_flow::data::api_key%]", "account": "Steam account ID" @@ -10,7 +10,7 @@ }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "The Steam integration needs to be manually re-authenticated\n\nYou can find your key here: https://steamcommunity.com/dev/apikey" + "description": "The Steam integration needs to be manually re-authenticated\n\nYou can find your key here: {api_key_url}" } }, "error": { diff --git a/homeassistant/components/steam_online/translations/en.json b/homeassistant/components/steam_online/translations/en.json index 8b34a85922d..990a33dbeff 100644 --- a/homeassistant/components/steam_online/translations/en.json +++ b/homeassistant/components/steam_online/translations/en.json @@ -12,7 +12,7 @@ }, "step": { "reauth_confirm": { - "description": "The Steam integration needs to be manually re-authenticated\n\nYou can find your key here: https://steamcommunity.com/dev/apikey", + "description": "The Steam integration needs to be manually re-authenticated\n\nYou can find your key here: {api_key_url}", "title": "Reauthenticate Integration" }, "user": { @@ -20,7 +20,7 @@ "account": "Steam account ID", "api_key": "API Key" }, - "description": "Use https://steamid.io to find your Steam account ID" + "description": "Use {account_id_url} to find your Steam account ID" } } }, diff --git a/tests/components/steam_online/__init__.py b/tests/components/steam_online/__init__.py index 729877e58bc..27958b76576 100644 --- a/tests/components/steam_online/__init__.py +++ b/tests/components/steam_online/__init__.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant.components.steam_online import DOMAIN from homeassistant.components.steam_online.const import CONF_ACCOUNT, CONF_ACCOUNTS -from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -19,38 +19,19 @@ CONF_DATA = { CONF_ACCOUNT: ACCOUNT_1, } -CONF_OPTIONS = { - CONF_ACCOUNTS: { - ACCOUNT_1: { - CONF_NAME: ACCOUNT_NAME_1, - "enabled": True, - } - } -} +CONF_OPTIONS = {CONF_ACCOUNTS: {ACCOUNT_1: ACCOUNT_NAME_1}} CONF_OPTIONS_2 = { CONF_ACCOUNTS: { - ACCOUNT_1: { - CONF_NAME: ACCOUNT_NAME_1, - "enabled": True, - }, - ACCOUNT_2: { - CONF_NAME: ACCOUNT_NAME_2, - "enabled": True, - }, + ACCOUNT_1: ACCOUNT_NAME_1, + ACCOUNT_2: ACCOUNT_NAME_2, } } CONF_IMPORT_OPTIONS = { CONF_ACCOUNTS: { - ACCOUNT_1: { - CONF_NAME: ACCOUNT_NAME_1, - "enabled": True, - }, - ACCOUNT_2: { - CONF_NAME: ACCOUNT_NAME_2, - "enabled": True, - }, + ACCOUNT_1: ACCOUNT_NAME_1, + ACCOUNT_2: ACCOUNT_NAME_2, } } diff --git a/tests/components/steam_online/test_config_flow.py b/tests/components/steam_online/test_config_flow.py index 68b7612f049..6d8a16f35f7 100644 --- a/tests/components/steam_online/test_config_flow.py +++ b/tests/components/steam_online/test_config_flow.py @@ -1,4 +1,6 @@ """Test Steam config flow.""" +from unittest.mock import patch + import steam from homeassistant import data_entry_flow @@ -25,7 +27,10 @@ from . import ( async def test_flow_user(hass: HomeAssistant) -> None: """Test user initialized flow.""" - with patch_interface(): + with patch_interface(), patch( + "homeassistant.components.steam_online.async_setup_entry", + return_value=True, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, From 4346d8cc2f2ac71fd9900cafb7bbad4310a01a44 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Sat, 30 Apr 2022 00:26:27 -0400 Subject: [PATCH 0075/3516] Fix Insteon tests (#71092) --- .../insteon/fixtures/kpl_properties.json | 2 +- tests/components/insteon/mock_devices.py | 20 ++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/components/insteon/fixtures/kpl_properties.json b/tests/components/insteon/fixtures/kpl_properties.json index 725432dbedc..db694e4a7eb 100644 --- a/tests/components/insteon/fixtures/kpl_properties.json +++ b/tests/components/insteon/fixtures/kpl_properties.json @@ -3,7 +3,7 @@ "program_lock_on": false, "blink_on_tx_on": false, "resume_dim_on": false, - "led_on": false, + "led_off": false, "key_beep_on": false, "rf_disable_on": false, "powerline_disable_on": false, diff --git a/tests/components/insteon/mock_devices.py b/tests/components/insteon/mock_devices.py index 6e6a8eccfcc..ef64b1e0969 100644 --- a/tests/components/insteon/mock_devices.py +++ b/tests/components/insteon/mock_devices.py @@ -1,7 +1,7 @@ """Mock devices object to test Insteon.""" import asyncio -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch from pyinsteon.address import Address from pyinsteon.constants import ALDBStatus, ResponseStatus @@ -129,18 +129,20 @@ class MockDevices: def fill_properties(self, address, props_dict): """Fill the operating flags and extended properties of a device.""" + device = self._devices[Address(address)] operating_flags = props_dict.get("operating_flags", {}) properties = props_dict.get("properties", {}) - for flag in operating_flags: - value = operating_flags[flag] - if device.operating_flags.get(flag): - device.operating_flags[flag].load(value) - for flag in properties: - value = properties[flag] - if device.properties.get(flag): - device.properties[flag].load(value) + with patch("pyinsteon.subscriber_base.publish_topic", MagicMock()): + for flag in operating_flags: + value = operating_flags[flag] + if device.operating_flags.get(flag): + device.operating_flags[flag].load(value) + for flag in properties: + value = properties[flag] + if device.properties.get(flag): + device.properties[flag].load(value) async def async_add_device(self, address=None, multiple=False): """Mock the async_add_device method.""" From c0b6d6a44e0093d147f5791092bfaf83bc45337b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 30 Apr 2022 02:59:39 -0400 Subject: [PATCH 0076/3516] Bump zwave-js-server-python to 0.36.1 (#71096) --- 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 c9edf28bd93..934c3f9a3f5 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.36.0"], + "requirements": ["zwave-js-server-python==0.36.1"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index d2de6e8e19b..f1f8b5425ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2510,7 +2510,7 @@ zigpy==0.45.1 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.36.0 +zwave-js-server-python==0.36.1 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e9592a41271..09a496e774b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1641,7 +1641,7 @@ zigpy-znp==0.7.0 zigpy==0.45.1 # homeassistant.components.zwave_js -zwave-js-server-python==0.36.0 +zwave-js-server-python==0.36.1 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 From 4f784c42ab9babef0df9269791873a78c2c2c73c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 May 2022 06:49:50 +0200 Subject: [PATCH 0077/3516] Fix missing device & entity references in automations (#71103) --- .../components/automation/__init__.py | 33 +++++++--- tests/components/automation/test_init.py | 60 ++++++++++++++++--- 2 files changed, 77 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 857d2d4b49f..c743e1f83fd 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_CONDITION, CONF_DEVICE_ID, CONF_ENTITY_ID, + CONF_EVENT_DATA, CONF_ID, CONF_MODE, CONF_PLATFORM, @@ -360,9 +361,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): referenced |= condition.async_extract_devices(conf) for conf in self._trigger_config: - device = _trigger_extract_device(conf) - if device is not None: - referenced.add(device) + referenced |= set(_trigger_extract_device(conf)) self._referenced_devices = referenced return referenced @@ -764,12 +763,22 @@ async def _async_process_if(hass, name, config, p_config): @callback -def _trigger_extract_device(trigger_conf: dict) -> str | None: +def _trigger_extract_device(trigger_conf: dict) -> list[str]: """Extract devices from a trigger config.""" - if trigger_conf[CONF_PLATFORM] != "device": - return None + if trigger_conf[CONF_PLATFORM] == "device": + return [trigger_conf[CONF_DEVICE_ID]] - return trigger_conf[CONF_DEVICE_ID] + if ( + trigger_conf[CONF_PLATFORM] == "event" + and CONF_EVENT_DATA in trigger_conf + and CONF_DEVICE_ID in trigger_conf[CONF_EVENT_DATA] + ): + return [trigger_conf[CONF_EVENT_DATA][CONF_DEVICE_ID]] + + if trigger_conf[CONF_PLATFORM] == "tag" and CONF_DEVICE_ID in trigger_conf: + return trigger_conf[CONF_DEVICE_ID] + + return [] @callback @@ -778,6 +787,9 @@ def _trigger_extract_entities(trigger_conf: dict) -> list[str]: if trigger_conf[CONF_PLATFORM] in ("state", "numeric_state"): return trigger_conf[CONF_ENTITY_ID] + if trigger_conf[CONF_PLATFORM] == "calendar": + return [trigger_conf[CONF_ENTITY_ID]] + if trigger_conf[CONF_PLATFORM] == "zone": return trigger_conf[CONF_ENTITY_ID] + [trigger_conf[CONF_ZONE]] @@ -787,4 +799,11 @@ def _trigger_extract_entities(trigger_conf: dict) -> list[str]: if trigger_conf[CONF_PLATFORM] == "sun": return ["sun.sun"] + if ( + trigger_conf[CONF_PLATFORM] == "event" + and CONF_EVENT_DATA in trigger_conf + and CONF_ENTITY_ID in trigger_conf[CONF_EVENT_DATA] + ): + return [trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID]] + return [] diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 1c90abe72ca..ccb508c6acc 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1079,6 +1079,7 @@ async def test_automation_restore_last_triggered_with_initial_state(hass): async def test_extraction_functions(hass): """Test extraction functions.""" + await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) assert await async_setup_component( hass, DOMAIN, @@ -1086,7 +1087,24 @@ async def test_extraction_functions(hass): DOMAIN: [ { "alias": "test1", - "trigger": {"platform": "state", "entity_id": "sensor.trigger_1"}, + "trigger": [ + {"platform": "state", "entity_id": "sensor.trigger_state"}, + { + "platform": "numeric_state", + "entity_id": "sensor.trigger_numeric_state", + "above": 10, + }, + { + "platform": "calendar", + "entity_id": "calendar.trigger_calendar", + "event": "start", + }, + { + "platform": "event", + "event_type": "state_changed", + "event_data": {"entity_id": "sensor.trigger_event"}, + }, + ], "condition": { "condition": "state", "entity_id": "light.condition_state", @@ -1111,13 +1129,30 @@ async def test_extraction_functions(hass): }, { "alias": "test2", - "trigger": { - "platform": "device", - "domain": "light", - "type": "turned_on", - "entity_id": "light.trigger_2", - "device_id": "trigger-device-2", - }, + "trigger": [ + { + "platform": "device", + "domain": "light", + "type": "turned_on", + "entity_id": "light.trigger_2", + "device_id": "trigger-device-2", + }, + { + "platform": "tag", + "tag_id": "1234", + "device_id": "device-trigger-tag1", + }, + { + "platform": "tag", + "tag_id": "1234", + "device_id": ["device-trigger-tag2", "device-trigger-tag3"], + }, + { + "platform": "event", + "event_type": "esphome.button_pressed", + "event_data": {"device_id": "device-trigger-event"}, + }, + ], "condition": { "condition": "device", "device_id": "condition-device", @@ -1159,7 +1194,10 @@ async def test_extraction_functions(hass): "automation.test2", } assert set(automation.entities_in_automation(hass, "automation.test1")) == { - "sensor.trigger_1", + "calendar.trigger_calendar", + "sensor.trigger_state", + "sensor.trigger_numeric_state", + "sensor.trigger_event", "light.condition_state", "light.in_both", "light.in_first", @@ -1173,6 +1211,10 @@ async def test_extraction_functions(hass): "condition-device", "device-in-both", "device-in-last", + "device-trigger-event", + "device-trigger-tag1", + "device-trigger-tag2", + "device-trigger-tag3", } From ce73b517b8185ec26101d06cd0bce9e1a6403ce2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 30 Apr 2022 13:59:59 +0200 Subject: [PATCH 0078/3516] Bump pysensibo to 1.0.13 (#71105) --- homeassistant/components/sensibo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 122fea51d6a..535824aaa19 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,7 +2,7 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": ["pysensibo==1.0.12"], + "requirements": ["pysensibo==1.0.13"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index f1f8b5425ed..95c1140ffcd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1783,7 +1783,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.12 +pysensibo==1.0.13 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 09a496e774b..9838a151c0f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1196,7 +1196,7 @@ pyruckus==0.12 pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.12 +pysensibo==1.0.13 # homeassistant.components.serial # homeassistant.components.zha From 3ce531e2f18ac775366f9b81ff45e79b10956c50 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 30 Apr 2022 18:28:47 +0200 Subject: [PATCH 0079/3516] Sensibo bugfix device on (#71106) Co-authored-by: J. Nick Koston --- homeassistant/components/sensibo/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 3ab6f06c4fb..907105b5384 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -45,7 +45,7 @@ HA_TO_SENSIBO = {value: key for key, value in SENSIBO_TO_HA.items()} AC_STATE_TO_DATA = { "targetTemperature": "target_temp", "fanLevel": "fan_mode", - "on": "on", + "on": "device_on", "mode": "hvac_mode", "swing": "swing_mode", } From 6a110e5a7760fc02b0c01db3d34eb44b72ac5053 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 30 Apr 2022 19:24:24 +0300 Subject: [PATCH 0080/3516] Add entity id to template error logging (#71107) * Add entity id to template error logging * Increase coverage --- .../template/alarm_control_panel.py | 3 +- homeassistant/components/template/cover.py | 3 +- homeassistant/components/template/fan.py | 30 ++++++++++++++----- homeassistant/components/template/light.py | 30 +++++++++++++------ homeassistant/components/template/vacuum.py | 13 +++++--- tests/components/template/test_light.py | 1 + 6 files changed, 57 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 07610a59457..9d81dce28fe 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -207,8 +207,9 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): return _LOGGER.error( - "Received invalid alarm panel state: %s. Expected: %s", + "Received invalid alarm panel state: %s for entity %s. Expected: %s", result, + self.entity_id, ", ".join(_VALID_STATES), ) self._state = None diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 7a05f9445f9..e1df61bf4a2 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -222,8 +222,9 @@ class CoverTemplate(TemplateEntity, CoverEntity): self._is_closing = state == STATE_CLOSING else: _LOGGER.error( - "Received invalid cover is_on state: %s. Expected: %s", + "Received invalid cover is_on state: %s for entity %s. Expected: %s", state, + self.entity_id, ", ".join(_VALID_STATES), ) if not self._position_template: diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 2ee63e7e318..6b0fdefc2f9 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -285,8 +285,9 @@ class TemplateFan(TemplateEntity, FanEntity): """Set the preset_mode of the fan.""" if self.preset_modes and preset_mode not in self.preset_modes: _LOGGER.error( - "Received invalid preset_mode: %s. Expected: %s", + "Received invalid preset_mode: %s for entity %s. Expected: %s", preset_mode, + self.entity_id, self.preset_modes, ) return @@ -322,8 +323,9 @@ class TemplateFan(TemplateEntity, FanEntity): ) else: _LOGGER.error( - "Received invalid direction: %s. Expected: %s", + "Received invalid direction: %s for entity %s. Expected: %s", direction, + self.entity_id, ", ".join(_VALID_DIRECTIONS), ) @@ -341,8 +343,9 @@ class TemplateFan(TemplateEntity, FanEntity): self._state = None else: _LOGGER.error( - "Received invalid fan is_on state: %s. Expected: %s", + "Received invalid fan is_on state: %s for entity %s. Expected: %s", result, + self.entity_id, ", ".join(_VALID_STATES), ) self._state = None @@ -390,7 +393,11 @@ class TemplateFan(TemplateEntity, FanEntity): try: percentage = int(float(percentage)) except ValueError: - _LOGGER.error("Received invalid percentage: %s", percentage) + _LOGGER.error( + "Received invalid percentage: %s for entity %s", + percentage, + self.entity_id, + ) self._percentage = 0 self._preset_mode = None return @@ -399,7 +406,11 @@ class TemplateFan(TemplateEntity, FanEntity): self._percentage = percentage self._preset_mode = None else: - _LOGGER.error("Received invalid percentage: %s", percentage) + _LOGGER.error( + "Received invalid percentage: %s for entity %s", + percentage, + self.entity_id, + ) self._percentage = 0 self._preset_mode = None @@ -416,8 +427,9 @@ class TemplateFan(TemplateEntity, FanEntity): self._preset_mode = None else: _LOGGER.error( - "Received invalid preset_mode: %s. Expected: %s", + "Received invalid preset_mode: %s for entity %s. Expected: %s", preset_mode, + self.entity_id, self.preset_mode, ) self._percentage = None @@ -434,8 +446,9 @@ class TemplateFan(TemplateEntity, FanEntity): self._oscillating = None else: _LOGGER.error( - "Received invalid oscillating: %s. Expected: True/False", + "Received invalid oscillating: %s for entity %s. Expected: True/False", oscillating, + self.entity_id, ) self._oscillating = None @@ -448,8 +461,9 @@ class TemplateFan(TemplateEntity, FanEntity): self._direction = None else: _LOGGER.error( - "Received invalid direction: %s. Expected: %s", + "Received invalid direction: %s for entity %s. Expected: %s", direction, + self.entity_id, ", ".join(_VALID_DIRECTIONS), ) self._direction = None diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index d2260aa10dc..5a79b3db8fc 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -393,8 +393,9 @@ class LightTemplate(TemplateEntity, LightEntity): effect = kwargs[ATTR_EFFECT] if effect not in self._effect_list: _LOGGER.error( - "Received invalid effect: %s. Expected one of: %s", + "Received invalid effect: %s for entity %s. Expected one of: %s", effect, + self.entity_id, self._effect_list, exc_info=True, ) @@ -443,7 +444,9 @@ class LightTemplate(TemplateEntity, LightEntity): self._brightness = int(brightness) else: _LOGGER.error( - "Received invalid brightness : %s. Expected: 0-255", brightness + "Received invalid brightness : %s for entity %s. Expected: 0-255", + brightness, + self.entity_id, ) self._brightness = None except ValueError: @@ -464,7 +467,9 @@ class LightTemplate(TemplateEntity, LightEntity): self._white_value = int(white_value) else: _LOGGER.error( - "Received invalid white value: %s. Expected: 0-255", white_value + "Received invalid white value: %s for entity %s. Expected: 0-255", + white_value, + self.entity_id, ) self._white_value = None except ValueError: @@ -483,8 +488,9 @@ class LightTemplate(TemplateEntity, LightEntity): if not isinstance(effect_list, list): _LOGGER.error( - "Received invalid effect list: %s. Expected list of strings", + "Received invalid effect list: %s for entity %s. Expected list of strings", effect_list, + self.entity_id, ) self._effect_list = None return @@ -504,8 +510,9 @@ class LightTemplate(TemplateEntity, LightEntity): if effect not in self._effect_list: _LOGGER.error( - "Received invalid effect: %s. Expected one of: %s", + "Received invalid effect: %s for entity %s. Expected one of: %s", effect, + self.entity_id, self._effect_list, ) self._effect = None @@ -533,8 +540,9 @@ class LightTemplate(TemplateEntity, LightEntity): return _LOGGER.error( - "Received invalid light is_on state: %s. Expected: %s", + "Received invalid light is_on state: %s for entity %s. Expected: %s", state, + self.entity_id, ", ".join(_VALID_STATES), ) self._state = None @@ -551,8 +559,9 @@ class LightTemplate(TemplateEntity, LightEntity): self._temperature = temperature else: _LOGGER.error( - "Received invalid color temperature : %s. Expected: %s-%s", + "Received invalid color temperature : %s for entity %s. Expected: %s-%s", temperature, + self.entity_id, self.min_mireds, self.max_mireds, ) @@ -591,13 +600,16 @@ class LightTemplate(TemplateEntity, LightEntity): self._color = (h_str, s_str) elif h_str is not None and s_str is not None: _LOGGER.error( - "Received invalid hs_color : (%s, %s). Expected: (0-360, 0-100)", + "Received invalid hs_color : (%s, %s) for entity %s. Expected: (0-360, 0-100)", h_str, s_str, + self.entity_id, ) self._color = None else: - _LOGGER.error("Received invalid hs_color : (%s)", render) + _LOGGER.error( + "Received invalid hs_color : (%s) for entity %s", render, self.entity_id + ) self._color = None @callback diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 2a004eabc9a..1d350d120c7 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -253,8 +253,9 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): ) else: _LOGGER.error( - "Received invalid fan speed: %s. Expected: %s", + "Received invalid fan speed: %s for entity %s. Expected: %s", fan_speed, + self.entity_id, self._attr_fan_speed_list, ) @@ -298,8 +299,9 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): self._state = None else: _LOGGER.error( - "Received invalid vacuum state: %s. Expected: %s", + "Received invalid vacuum state: %s for entity %s. Expected: %s", result, + self.entity_id, ", ".join(_VALID_STATES), ) self._state = None @@ -312,7 +314,9 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): raise ValueError except ValueError: _LOGGER.error( - "Received invalid battery level: %s. Expected: 0-100", battery_level + "Received invalid battery level: %s for entity %s. Expected: 0-100", + battery_level, + self.entity_id, ) self._attr_battery_level = None return @@ -333,8 +337,9 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): self._attr_fan_speed = None else: _LOGGER.error( - "Received invalid fan speed: %s. Expected: %s", + "Received invalid fan speed: %s for entity %s. Expected: %s", fan_speed, + self.entity_id, self._attr_fan_speed_list, ) self._attr_fan_speed = None diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 2b877c52c4f..642fa5601cf 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -1025,6 +1025,7 @@ async def test_color_action_no_template(hass, start_ha, calls): ((359.9, 99.9), {"replace6": '"{{(359.9, 99.9)}}"'}), (None, {"replace6": '"{{(361, 100)}}"'}), (None, {"replace6": '"{{(360, 101)}}"'}), + (None, {"replace6": '"[{{(360)}},{{null}}]"'}), (None, {"replace6": '"{{x - 12}}"'}), (None, {"replace6": '""'}), (None, {"replace6": '"{{ none }}"'}), From adc2f3d16951578078e1c3b6605ec3c7cc5b02a1 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 30 Apr 2022 15:25:41 +0200 Subject: [PATCH 0081/3516] Update xknx to 0.21.0 (#71108) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 06ccba386d8..f5b1bdc6206 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.20.4"], + "requirements": ["xknx==0.21.0"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 95c1140ffcd..f2ed9354f40 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2445,7 +2445,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.20.4 +xknx==0.21.0 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9838a151c0f..df6be01b0ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1597,7 +1597,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.20.4 +xknx==0.21.0 # homeassistant.components.bluesound # homeassistant.components.fritz From c1bbcfd275da71b2322fa886adbd543e6fd66e24 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 30 Apr 2022 09:33:30 -0700 Subject: [PATCH 0082/3516] Bump gcal_sync to 0.7.0 (#71116) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 0e9a2fe7ddb..46c5844819a 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["auth"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", - "requirements": ["gcal-sync==0.6.3", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==0.7.0", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index f2ed9354f40..bc6f24cd830 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -689,7 +689,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==0.6.3 +gcal-sync==0.7.0 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df6be01b0ce..3ddba1992a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -486,7 +486,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==0.6.3 +gcal-sync==0.7.0 # homeassistant.components.usgs_earthquakes_feed geojson_client==0.6 From 47d19b3967566a5efbed8673754aa240b1457364 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 30 Apr 2022 18:26:14 +0200 Subject: [PATCH 0083/3516] Fix copy paste issue leaving one device trigger with a wrong subtype (#71121) --- homeassistant/components/deconz/device_trigger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index c76aaf481bf..c92ad7f46dc 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -403,7 +403,7 @@ AQARA_OPPLE_4_BUTTONS = { AQARA_OPPLE_6_BUTTONS_MODEL = "lumi.remote.b686opcn01" AQARA_OPPLE_6_BUTTONS = { **AQARA_OPPLE_4_BUTTONS, - (CONF_LONG_PRESS, CONF_DIM_DOWN): {CONF_EVENT: 5001}, + (CONF_LONG_PRESS, CONF_LEFT): {CONF_EVENT: 5001}, (CONF_SHORT_RELEASE, CONF_LEFT): {CONF_EVENT: 5002}, (CONF_LONG_RELEASE, CONF_LEFT): {CONF_EVENT: 5003}, (CONF_DOUBLE_PRESS, CONF_LEFT): {CONF_EVENT: 5004}, From 475135663855b15e787548430f5e3bd75e8d9ebf Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 30 Apr 2022 19:33:47 +0200 Subject: [PATCH 0084/3516] Make deCONZ SSDP discovery more strict by matching on manufacturerURL (#71124) --- homeassistant/components/deconz/config_flow.py | 6 ------ homeassistant/components/deconz/manifest.json | 3 ++- homeassistant/components/deconz/strings.json | 1 - homeassistant/generated/ssdp.py | 3 ++- tests/components/deconz/test_config_flow.py | 16 ---------------- 5 files changed, 4 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index af37ee96878..28205a7382d 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -215,12 +215,6 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered deCONZ bridge.""" - if ( - discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER_URL) - != DECONZ_MANUFACTURERURL - ): - return self.async_abort(reason="not_deconz_bridge") - LOGGER.debug("deCONZ SSDP discovery %s", pformat(discovery_info)) self.bridge_id = normalize_bridge_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index dee41435779..1ce3477db70 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -6,7 +6,8 @@ "requirements": ["pydeconz==91"], "ssdp": [ { - "manufacturer": "Royal Philips Electronics" + "manufacturer": "Royal Philips Electronics", + "manufacturerURL": "http://www.dresden-elektronik.de" } ], "codeowners": ["@Kane610"], diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 4098951d714..55bb86d03f6 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -31,7 +31,6 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "no_bridges": "No deCONZ bridges discovered", "no_hardware_available": "No radio hardware connected to deCONZ", - "not_deconz_bridge": "Not a deCONZ bridge", "updated_instance": "Updated deCONZ instance with new host address" } }, diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 1c0876cd791..851d9b0fd10 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -24,7 +24,8 @@ SSDP = { ], "deconz": [ { - "manufacturer": "Royal Philips Electronics" + "manufacturer": "Royal Philips Electronics", + "manufacturerURL": "http://www.dresden-elektronik.de" } ], "denonavr": [ diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 97b2f7ee164..1a7031a0fd6 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -459,22 +459,6 @@ async def test_flow_ssdp_discovery(hass, aioclient_mock): } -async def test_flow_ssdp_bad_discovery(hass, aioclient_mock): - """Test that SSDP discovery aborts if manufacturer URL is wrong.""" - result = await hass.config_entries.flow.async_init( - DECONZ_DOMAIN, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - upnp={ATTR_UPNP_MANUFACTURER_URL: "other"}, - ), - context={"source": SOURCE_SSDP}, - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "not_deconz_bridge" - - async def test_ssdp_discovery_update_configuration(hass, aioclient_mock): """Test if a discovered bridge is configured but updates with new attributes.""" config_entry = await setup_deconz_integration(hass, aioclient_mock) From 174717dd85a2dd11cbea680520510b0dad175ec4 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 1 May 2022 21:00:38 +0200 Subject: [PATCH 0085/3516] Abort UniFi Network options flow if integration is not setup (#71128) --- homeassistant/components/unifi/config_flow.py | 2 ++ homeassistant/components/unifi/strings.json | 5 ++++- homeassistant/components/unifi/translations/en.json | 3 +++ tests/components/unifi/test_config_flow.py | 11 +++++++++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 6e40798fb7a..d02f3f49a5e 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -256,6 +256,8 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Manage the UniFi Network options.""" + if self.config_entry.entry_id not in self.hass.data[UNIFI_DOMAIN]: + return self.async_abort(reason="integration_not_setup") self.controller = self.hass.data[UNIFI_DOMAIN][self.config_entry.entry_id] self.options[CONF_BLOCK_CLIENT] = self.controller.option_block_clients diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index 476a0bfdd61..8d6df90b704 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -21,11 +21,14 @@ }, "abort": { "already_configured": "UniFi Network site is already configured", - "configuration_updated": "Configuration updated.", + "configuration_updated": "Configuration updated", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "options": { + "abort": { + "integration_not_setup": "UniFi integration is not setup" + }, "step": { "device_tracker": { "data": { diff --git a/homeassistant/components/unifi/translations/en.json b/homeassistant/components/unifi/translations/en.json index 5d08c8b816d..2a1c17cb6e3 100644 --- a/homeassistant/components/unifi/translations/en.json +++ b/homeassistant/components/unifi/translations/en.json @@ -26,6 +26,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "UniFi integration is not setup" + }, "step": { "client_control": { "data": { diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 4c4ff7006fd..321cbdfd9e8 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -542,6 +542,17 @@ async def test_simple_option_flow(hass, aioclient_mock): } +async def test_option_flow_integration_not_setup(hass, aioclient_mock): + """Test advanced config flow options.""" + config_entry = await setup_unifi_integration(hass, aioclient_mock) + + hass.data[UNIFI_DOMAIN].pop(config_entry.entry_id) + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == "abort" + assert result["reason"] == "integration_not_setup" + + async def test_form_ssdp(hass): """Test we get the form with ssdp source.""" From 01b096bb093165971025a8b99f83f11e69302c5d Mon Sep 17 00:00:00 2001 From: Kuba Wolanin Date: Mon, 2 May 2022 01:14:30 +0200 Subject: [PATCH 0086/3516] Add Show logs URL to integration errors notification (#71142) --- homeassistant/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index d5f9eb91b62..5c870918231 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -951,8 +951,9 @@ def async_notify_setup_error( message = "The following integrations and platforms could not be set up:\n\n" for name, link in errors.items(): + show_logs = f"[Show logs](/config/logs?filter={name})" part = f"[{name}]({link})" if link else name - message += f" - {part}\n" + message += f" - {part} ({show_logs})\n" message += "\nPlease check your config and [logs](/config/logs)." From 79cc21632736697052201ae43a853fc1891d42a9 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sun, 1 May 2022 11:00:37 +0200 Subject: [PATCH 0087/3516] Update xknx to 0.21.1 (#71144) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index f5b1bdc6206..a000261ec3a 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.21.0"], + "requirements": ["xknx==0.21.1"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index bc6f24cd830..a1e05d47565 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2445,7 +2445,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.21.0 +xknx==0.21.1 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ddba1992a0..2b4c66e6c5d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1597,7 +1597,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.21.0 +xknx==0.21.1 # homeassistant.components.bluesound # homeassistant.components.fritz From e7bcf839ace2e578b87d6e5a8b808b151d24ae11 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 1 May 2022 15:39:33 +0200 Subject: [PATCH 0088/3516] Bump pysensibo 1.0.14 (#71150) --- homeassistant/components/sensibo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 535824aaa19..308d991e675 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,7 +2,7 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": ["pysensibo==1.0.13"], + "requirements": ["pysensibo==1.0.14"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index a1e05d47565..7856ed8d550 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1783,7 +1783,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.13 +pysensibo==1.0.14 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b4c66e6c5d..cdd024d0da9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1196,7 +1196,7 @@ pyruckus==0.12 pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.13 +pysensibo==1.0.14 # homeassistant.components.serial # homeassistant.components.zha From db53b3cbe0c54014cbf0e2a336f1414a8c53071c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 May 2022 12:00:15 -0500 Subject: [PATCH 0089/3516] Fix missing device info in lutron_caseta (#71156) - There was a missing return due to a bad merge conflict resolution - Fixes #71154 --- .../components/lutron_caseta/__init__.py | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index d0561bc47ab..bb8f94f3abe 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -294,6 +294,8 @@ async def async_unload_entry( class LutronCasetaDevice(Entity): """Common base class for all Lutron Caseta devices.""" + _attr_should_poll = False + def __init__(self, device, bridge, bridge_device): """Set up the base class. @@ -304,6 +306,18 @@ class LutronCasetaDevice(Entity): self._device = device self._smartbridge = bridge self._bridge_device = bridge_device + info = DeviceInfo( + identifiers={(DOMAIN, self.serial)}, + manufacturer=MANUFACTURER, + model=f"{device['model']} ({device['type']})", + name=self.name, + via_device=(DOMAIN, self._bridge_device["serial"]), + configuration_url="https://device-login.lutron.com", + ) + area, _ = _area_and_name_from_name(device["name"]) + if area != UNASSIGNED_AREA: + info[ATTR_SUGGESTED_AREA] = area + self._attr_device_info = info async def async_added_to_hass(self): """Register callbacks.""" @@ -329,28 +343,7 @@ class LutronCasetaDevice(Entity): """Return the unique ID of the device (serial).""" return str(self.serial) - @property - def device_info(self) -> DeviceInfo: - """Return the device info.""" - device = self._device - info = DeviceInfo( - identifiers={(DOMAIN, self.serial)}, - manufacturer=MANUFACTURER, - model=f"{device['model']} ({device['type']})", - name=self.name, - via_device=(DOMAIN, self._bridge_device["serial"]), - configuration_url="https://device-login.lutron.com", - ) - area, _ = _area_and_name_from_name(device["name"]) - if area != UNASSIGNED_AREA: - info[ATTR_SUGGESTED_AREA] = area - @property def extra_state_attributes(self): """Return the state attributes.""" return {"device_id": self.device_id, "zone_id": self._device["zone"]} - - @property - def should_poll(self): - """No polling needed.""" - return False From 1f912e9c98bd8127545f09f46980d208ff98bc5c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 May 2022 12:49:17 -0500 Subject: [PATCH 0090/3516] Bump zeroconf to 0.38.5 (#71160) --- 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 dc4c7c001ae..e1ea2c82b1f 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.38.4"], + "requirements": ["zeroconf==0.38.5"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0cb893be99d..6dd904c858f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 yarl==1.7.2 -zeroconf==0.38.4 +zeroconf==0.38.5 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 7856ed8d550..b1c1023c177 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2480,7 +2480,7 @@ youtube_dl==2021.12.17 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.38.4 +zeroconf==0.38.5 # homeassistant.components.zha zha-quirks==0.0.73 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cdd024d0da9..8ff4285c366 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1620,7 +1620,7 @@ yeelight==0.7.10 youless-api==0.16 # homeassistant.components.zeroconf -zeroconf==0.38.4 +zeroconf==0.38.5 # homeassistant.components.zha zha-quirks==0.0.73 From 0d4947a2d339354e528d6ef7e823d32a03a98f33 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Sun, 1 May 2022 13:36:13 -0700 Subject: [PATCH 0091/3516] update python-smarttub to 0.0.32 (#71164) --- homeassistant/components/smarttub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smarttub/manifest.json b/homeassistant/components/smarttub/manifest.json index 94e8185bb7d..1d7500b9185 100644 --- a/homeassistant/components/smarttub/manifest.json +++ b/homeassistant/components/smarttub/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smarttub", "dependencies": [], "codeowners": ["@mdz"], - "requirements": ["python-smarttub==0.0.31"], + "requirements": ["python-smarttub==0.0.32"], "quality_scale": "platinum", "iot_class": "cloud_polling", "loggers": ["smarttub"] diff --git a/requirements_all.txt b/requirements_all.txt index b1c1023c177..2311d648ada 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1934,7 +1934,7 @@ python-qbittorrent==0.4.2 python-ripple-api==0.0.3 # homeassistant.components.smarttub -python-smarttub==0.0.31 +python-smarttub==0.0.32 # homeassistant.components.songpal python-songpal==0.14.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ff4285c366..1d633f3e664 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1269,7 +1269,7 @@ python-nest==4.2.0 python-picnic-api==1.1.0 # homeassistant.components.smarttub -python-smarttub==0.0.31 +python-smarttub==0.0.32 # homeassistant.components.songpal python-songpal==0.14.1 From c791c52d2813e01e18ff745bce33a707f50b98af Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 1 May 2022 22:04:03 +0200 Subject: [PATCH 0092/3516] Fix template error in sql (#71169) --- homeassistant/components/sql/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 2ae626c67fa..5e748bed55e 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -42,7 +42,7 @@ _QUERY_SCHEME = vol.Schema( vol.Required(CONF_NAME): cv.string, vol.Required(CONF_QUERY): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_VALUE_TEMPLATE): cv.string, } ) From 5c4861011baa3bb1c45606086c28020bd773f408 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 1 May 2022 14:20:44 -0700 Subject: [PATCH 0093/3516] Bump gcal_sync to 0.7.1 to fix calendar API timezone handling (#71173) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 46c5844819a..2cf852fc6af 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["auth"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", - "requirements": ["gcal-sync==0.7.0", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==0.7.1", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 2311d648ada..389712286f7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -689,7 +689,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==0.7.0 +gcal-sync==0.7.1 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d633f3e664..cfdfb1d6d3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -486,7 +486,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==0.7.0 +gcal-sync==0.7.1 # homeassistant.components.usgs_earthquakes_feed geojson_client==0.6 From d4780ac43c1e6bc93a339172d7c2960139c3434a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 1 May 2022 22:50:39 -0600 Subject: [PATCH 0094/3516] Fix issues with SimpliSafe email-based 2FA (#71180) * FIx issues with email-based SimpliSafe 2FA * Bump --- .../components/simplisafe/manifest.json | 2 +- homeassistant/components/simplisafe/strings.json | 2 +- .../components/simplisafe/translations/en.json | 16 +++------------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 8e4c1f5d82b..804523b3390 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==2022.04.1"], + "requirements": ["simplisafe-python==2022.05.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 45e0ef84f5a..85e579fd455 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -32,7 +32,7 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "progress": { - "email_2fa": "Input the two-factor authentication code\nsent to you via email." + "email_2fa": "Check your email for a verification link from Simplisafe." } }, "options": { diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index a3bf0049681..0da6f6442e4 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -3,23 +3,16 @@ "abort": { "already_configured": "This SimpliSafe account is already in use.", "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", - "reauth_successful": "Re-authentication was successful", - "wrong_account": "The user credentials provided do not match this SimpliSafe account." + "reauth_successful": "Re-authentication was successful" }, "error": { - "2fa_timed_out": "Timed out while waiting for two-factor authentication", - "identifier_exists": "Account already registered", "invalid_auth": "Invalid authentication", - "still_awaiting_mfa": "Still awaiting MFA email click", "unknown": "Unexpected error" }, "progress": { - "email_2fa": "Input the two-factor authentication code\nsent to you via email." + "email_2fa": "Check your email for a verification link from Simplisafe." }, "step": { - "mfa": { - "title": "SimpliSafe Multi-Factor Authentication" - }, "reauth_confirm": { "data": { "password": "Password" @@ -35,13 +28,10 @@ }, "user": { "data": { - "auth_code": "Authorization Code", - "code": "Code (used in Home Assistant UI)", "password": "Password", "username": "Username" }, - "description": "Input your username and password.", - "title": "Fill in your information." + "description": "Input your username and password." } } }, diff --git a/requirements_all.txt b/requirements_all.txt index 389712286f7..f50196895c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2153,7 +2153,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.04.1 +simplisafe-python==2022.05.0 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfdfb1d6d3c..ce65bd2814f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1404,7 +1404,7 @@ sharkiq==0.0.1 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2022.04.1 +simplisafe-python==2022.05.0 # homeassistant.components.slack slackclient==2.5.0 From 78439eebb9ecc105629e6f080ece48849cde0558 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 May 2022 21:53:15 -0700 Subject: [PATCH 0095/3516] Bumped version to 2022.5.0b5 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a5db224ee60..e8e281adde2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 03b13f004eb..16fb8c94692 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.5.0b4 +version = 2022.5.0b5 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 7026e5dd110220402f61ca3c3825adca3aa62bfa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 2 May 2022 08:12:32 +0200 Subject: [PATCH 0096/3516] Fix Renault diagnostics (#71186) --- homeassistant/components/renault/diagnostics.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/renault/diagnostics.py b/homeassistant/components/renault/diagnostics.py index f8b0a0d926e..2029ef989d6 100644 --- a/homeassistant/components/renault/diagnostics.py +++ b/homeassistant/components/renault/diagnostics.py @@ -59,7 +59,9 @@ def _get_vehicle_diagnostics(vehicle: RenaultVehicleProxy) -> dict[str, Any]: return { "details": async_redact_data(vehicle.details.raw_data, TO_REDACT), "data": { - key: async_redact_data(coordinator.data.raw_data, TO_REDACT) + key: async_redact_data( + coordinator.data.raw_data if coordinator.data else None, TO_REDACT + ) for key, coordinator in vehicle.coordinators.items() }, } From 5db014666c5f988ad4ce42ee5ce94b6547c086de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 May 2022 02:10:34 -0500 Subject: [PATCH 0097/3516] Avoid recording state_changed events in the events table (#71165) * squash fix mypy * Update homeassistant/components/recorder/models.py Co-authored-by: Paulus Schoutsen * pass all columns * fix commented out code * reduce logbook query complexity * merge * comment Co-authored-by: Paulus Schoutsen --- homeassistant/components/logbook/__init__.py | 135 ++++++++---------- homeassistant/components/recorder/__init__.py | 3 +- .../components/recorder/migration.py | 20 ++- homeassistant/components/recorder/models.py | 45 ++++-- homeassistant/components/recorder/purge.py | 22 +-- tests/components/recorder/test_init.py | 48 +++---- tests/components/recorder/test_models.py | 3 - tests/components/recorder/test_purge.py | 9 +- 8 files changed, 151 insertions(+), 134 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 96e6b975e32..fdf553775b4 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -111,13 +111,28 @@ ALL_EVENT_TYPES = [ *ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, ] + EVENT_COLUMNS = [ - Events.event_type, - Events.event_data, - Events.time_fired, - Events.context_id, - Events.context_user_id, - Events.context_parent_id, + Events.event_type.label("event_type"), + Events.event_data.label("event_data"), + Events.time_fired.label("time_fired"), + Events.context_id.label("context_id"), + Events.context_user_id.label("context_user_id"), + Events.context_parent_id.label("context_parent_id"), +] + +STATE_COLUMNS = [ + States.state.label("state"), + States.entity_id.label("entity_id"), + States.attributes.label("attributes"), + StateAttributes.shared_attrs.label("shared_attrs"), +] + +EMPTY_STATE_COLUMNS = [ + literal(value=None, type_=sqlalchemy.String).label("state"), + literal(value=None, type_=sqlalchemy.String).label("entity_id"), + literal(value=None, type_=sqlalchemy.Text).label("attributes"), + literal(value=None, type_=sqlalchemy.Text).label("shared_attrs"), ] SCRIPT_AUTOMATION_EVENTS = {EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED} @@ -502,43 +517,47 @@ def _get_events( with session_scope(hass=hass) as session: old_state = aliased(States, name="old_state") + query: Query + query = _generate_events_query_without_states(session) + query = _apply_event_time_filter(query, start_day, end_day) + query = _apply_event_types_filter( + hass, query, ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED + ) if entity_ids is not None: - query = _generate_events_query_without_states(session) - query = _apply_event_time_filter(query, start_day, end_day) - query = _apply_event_types_filter( - hass, query, ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED - ) if entity_matches_only: # When entity_matches_only is provided, contexts and events that do not # contain the entity_ids are not included in the logbook response. query = _apply_event_entity_id_matchers(query, entity_ids) query = query.outerjoin(EventData, (Events.data_id == EventData.data_id)) - query = query.union_all( _generate_states_query( session, start_day, end_day, old_state, entity_ids ) ) else: - query = _generate_events_query(session) - query = _apply_event_time_filter(query, start_day, end_day) - query = _apply_events_types_and_states_filter( - hass, query, old_state - ).filter( - (States.last_updated == States.last_changed) - | (Events.event_type != EVENT_STATE_CHANGED) - ) - if filters: - query = query.filter( - filters.entity_filter() | (Events.event_type != EVENT_STATE_CHANGED) # type: ignore[no-untyped-call] - ) - if context_id is not None: query = query.filter(Events.context_id == context_id) - query = query.outerjoin(EventData, (Events.data_id == EventData.data_id)) + states_query = _generate_states_query( + session, start_day, end_day, old_state, entity_ids + ) + if context_id is not None: + # Once all the old `state_changed` events + # are gone from the database this query can + # be simplified to filter only on States.context_id == context_id + states_query = states_query.outerjoin( + Events, (States.event_id == Events.event_id) + ) + states_query = states_query.filter( + (States.context_id == context_id) + | (States.context_id.is_(None) & (Events.context_id == context_id)) + ) + if filters: + states_query = states_query.filter(filters.entity_filter()) # type: ignore[no-untyped-call] + query = query.union_all(states_query) + query = query.order_by(Events.time_fired) return list( @@ -546,36 +565,22 @@ def _get_events( ) -def _generate_events_query(session: Session) -> Query: - return session.query( - *EVENT_COLUMNS, - EventData.shared_data, - States.state, - States.entity_id, - States.attributes, - StateAttributes.shared_attrs, - ) - - def _generate_events_query_without_data(session: Session) -> Query: return session.query( - *EVENT_COLUMNS, + literal(value=EVENT_STATE_CHANGED, type_=sqlalchemy.String).label("event_type"), + literal(value=None, type_=sqlalchemy.Text).label("event_data"), + States.last_changed.label("time_fired"), + States.context_id.label("context_id"), + States.context_user_id.label("context_user_id"), + States.context_parent_id.label("context_parent_id"), literal(value=None, type_=sqlalchemy.Text).label("shared_data"), - States.state, - States.entity_id, - States.attributes, - StateAttributes.shared_attrs, + *STATE_COLUMNS, ) def _generate_events_query_without_states(session: Session) -> Query: return session.query( - *EVENT_COLUMNS, - EventData.shared_data, - literal(value=None, type_=sqlalchemy.String).label("state"), - literal(value=None, type_=sqlalchemy.String).label("entity_id"), - literal(value=None, type_=sqlalchemy.Text).label("attributes"), - literal(value=None, type_=sqlalchemy.Text).label("shared_attrs"), + *EVENT_COLUMNS, EventData.shared_data.label("shared_data"), *EMPTY_STATE_COLUMNS ) @@ -584,41 +589,19 @@ def _generate_states_query( start_day: dt, end_day: dt, old_state: States, - entity_ids: Iterable[str], + entity_ids: Iterable[str] | None, ) -> Query: - return ( + query = ( _generate_events_query_without_data(session) - .outerjoin(Events, (States.event_id == Events.event_id)) .outerjoin(old_state, (States.old_state_id == old_state.state_id)) .filter(_missing_state_matcher(old_state)) .filter(_not_continuous_entity_matcher()) .filter((States.last_updated > start_day) & (States.last_updated < end_day)) - .filter( - (States.last_updated == States.last_changed) - & States.entity_id.in_(entity_ids) - ) - .outerjoin( - StateAttributes, (States.attributes_id == StateAttributes.attributes_id) - ) + .filter(States.last_updated == States.last_changed) ) - - -def _apply_events_types_and_states_filter( - hass: HomeAssistant, query: Query, old_state: States -) -> Query: - events_query = ( - query.outerjoin(States, (Events.event_id == States.event_id)) - .outerjoin(old_state, (States.old_state_id == old_state.state_id)) - .filter( - (Events.event_type != EVENT_STATE_CHANGED) - | _missing_state_matcher(old_state) - ) - .filter( - (Events.event_type != EVENT_STATE_CHANGED) - | _not_continuous_entity_matcher() - ) - ) - return _apply_event_types_filter(hass, events_query, ALL_EVENT_TYPES).outerjoin( + if entity_ids: + query = query.filter(States.entity_id.in_(entity_ids)) + return query.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index d190ebd0a99..486866fc799 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -1223,8 +1223,8 @@ class Recorder(threading.Thread): ] = dbevent_data self.event_session.add(dbevent_data) - self.event_session.add(dbevent) if event.event_type != EVENT_STATE_CHANGED: + self.event_session.add(dbevent) return try: @@ -1272,7 +1272,6 @@ class Recorder(threading.Thread): self._pending_expunge.append(dbstate) else: dbstate.state = None - dbstate.event = dbevent self.event_session.add(dbstate) def _handle_database_error(self, err: Exception) -> bool: diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 2ef51824737..7835f5320b9 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -442,7 +442,7 @@ def _apply_update(instance, new_version, old_version): # noqa: C901 # and we would have to move to something like # sqlalchemy alembic to make that work # - _drop_index(instance, "states", "ix_states_context_id") + # no longer dropping ix_states_context_id since its recreated in 28 _drop_index(instance, "states", "ix_states_context_user_id") # This index won't be there if they were not running # nightly but we don't treat that as a critical issue @@ -652,6 +652,24 @@ def _apply_update(instance, new_version, old_version): # noqa: C901 elif new_version == 27: _add_columns(instance, "events", [f"data_id {big_int}"]) _create_index(instance, "events", "ix_events_data_id") + elif new_version == 28: + _add_columns(instance, "events", ["origin_idx INTEGER"]) + # We never use the user_id or parent_id index + _drop_index(instance, "events", "ix_events_context_user_id") + _drop_index(instance, "events", "ix_events_context_parent_id") + _add_columns( + instance, + "states", + [ + "origin_idx INTEGER", + "context_id VARCHAR(36)", + "context_user_id VARCHAR(36)", + "context_parent_id VARCHAR(36)", + ], + ) + _create_index(instance, "states", "ix_states_context_id") + # Once there are no longer any state_changed events + # in the events table we can drop the index on states.event_id 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 212c0c7e7d4..c528093198d 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -17,6 +17,7 @@ from sqlalchemy import ( Identity, Index, Integer, + SmallInteger, String, Text, distinct, @@ -43,7 +44,7 @@ from .const import ALL_DOMAIN_EXCLUDE_ATTRS, JSON_DUMP # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 27 +SCHEMA_VERSION = 28 _LOGGER = logging.getLogger(__name__) @@ -86,6 +87,8 @@ DOUBLE_TYPE = ( .with_variant(oracle.DOUBLE_PRECISION(), "oracle") .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") ) +EVENT_ORIGIN_ORDER = [EventOrigin.local, EventOrigin.remote] +EVENT_ORIGIN_TO_IDX = {origin: idx for idx, origin in enumerate(EVENT_ORIGIN_ORDER)} class Events(Base): # type: ignore[misc,valid-type] @@ -98,14 +101,15 @@ class Events(Base): # type: ignore[misc,valid-type] {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, ) __tablename__ = TABLE_EVENTS - event_id = Column(Integer, Identity(), primary_key=True) + event_id = Column(Integer, Identity(), primary_key=True) # no longer used event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) - origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) + origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) # no longer used + origin_idx = Column(SmallInteger) time_fired = Column(DATETIME_TYPE, index=True) context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) - context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) - context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) + context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) data_id = Column(Integer, ForeignKey("event_data.data_id"), index=True) event_data_rel = relationship("EventData") @@ -114,7 +118,7 @@ class Events(Base): # type: ignore[misc,valid-type] return ( f"" ) @@ -124,7 +128,7 @@ class Events(Base): # type: ignore[misc,valid-type] return Events( event_type=event.event_type, event_data=None, - origin=str(event.origin.value), + origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), time_fired=event.time_fired, context_id=event.context.id, context_user_id=event.context.user_id, @@ -142,7 +146,9 @@ class Events(Base): # type: ignore[misc,valid-type] return Event( self.event_type, json.loads(self.event_data) if self.event_data else {}, - EventOrigin(self.origin), + EventOrigin(self.origin) + if self.origin + else EVENT_ORIGIN_ORDER[self.origin_idx], process_timestamp(self.time_fired), context=context, ) @@ -222,7 +228,10 @@ class States(Base): # type: ignore[misc,valid-type] attributes_id = Column( Integer, ForeignKey("state_attributes.attributes_id"), index=True ) - event = relationship("Events", uselist=False) + context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) + context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) + origin_idx = Column(SmallInteger) # 0 is local, 1 is remote old_state = relationship("States", remote_side=[state_id]) state_attributes = relationship("StateAttributes") @@ -242,7 +251,14 @@ class States(Base): # type: ignore[misc,valid-type] """Create object from a state_changed event.""" entity_id = event.data["entity_id"] state: State | None = event.data.get("new_state") - dbstate = States(entity_id=entity_id, attributes=None) + dbstate = States( + entity_id=entity_id, + attributes=None, + context_id=event.context.id, + context_user_id=event.context.user_id, + context_parent_id=event.context.parent_id, + origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), + ) # None state means the state was removed from the state machine if state is None: @@ -258,6 +274,11 @@ class States(Base): # type: ignore[misc,valid-type] def to_native(self, validate_entity_id: bool = True) -> State | None: """Convert to an HA state object.""" + context = Context( + id=self.context_id, + user_id=self.context_user_id, + parent_id=self.context_parent_id, + ) try: return State( self.entity_id, @@ -267,9 +288,7 @@ class States(Base): # type: ignore[misc,valid-type] json.loads(self.attributes) if self.attributes else {}, process_timestamp(self.last_changed), process_timestamp(self.last_updated), - # Join the events table on event_id to get the context instead - # as it will always be there for state_changed events - context=Context(id=None), # type: ignore[arg-type] + context=context, validate_entity_id=validate_entity_id, ) except ValueError: diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 4aad3a28a88..dcc535c8177 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -83,7 +83,7 @@ def purge_old_data( if short_term_statistics: _purge_short_term_statistics(session, short_term_statistics) - if event_ids or statistics_runs or short_term_statistics: + if state_ids or 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 @@ -103,27 +103,31 @@ def _select_event_state_attributes_ids_data_ids_to_purge( ) -> tuple[set[int], set[int], set[int], set[int]]: """Return a list of event, state, and attribute ids to purge.""" events = ( - session.query( - Events.event_id, Events.data_id, States.state_id, States.attributes_id - ) - .outerjoin(States, Events.event_id == States.event_id) + session.query(Events.event_id, Events.data_id) .filter(Events.time_fired < purge_before) .limit(MAX_ROWS_TO_PURGE) .all() ) _LOGGER.debug("Selected %s event ids to remove", len(events)) + states = ( + session.query(States.state_id, States.attributes_id) + .filter(States.last_updated < purge_before) + .limit(MAX_ROWS_TO_PURGE) + .all() + ) + _LOGGER.debug("Selected %s state ids to remove", len(states)) event_ids = set() state_ids = set() attributes_ids = set() data_ids = set() for event in events: event_ids.add(event.event_id) - if event.state_id: - state_ids.add(event.state_id) - if event.attributes_id: - attributes_ids.add(event.attributes_id) if event.data_id: data_ids.add(event.data_id) + for state in states: + state_ids.add(state.state_id) + if state.attributes_id: + attributes_ids.add(state.attributes_id) return event_ids, state_ids, attributes_ids, data_ids diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index c3d22255ccc..423e8eb2f48 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -49,7 +49,7 @@ from homeassistant.const import ( STATE_LOCKED, STATE_UNLOCKED, ) -from homeassistant.core import Context, CoreState, Event, HomeAssistant, callback +from homeassistant.core import CoreState, Event, HomeAssistant, callback from homeassistant.setup import async_setup_component, setup_component from homeassistant.util import dt as dt_util @@ -162,7 +162,7 @@ async def test_state_gets_saved_when_set_before_start_event( with session_scope(hass=hass) as session: db_states = list(session.query(States)) assert len(db_states) == 1 - assert db_states[0].event_id > 0 + assert db_states[0].event_id is None async def test_saving_state(hass: HomeAssistant, recorder_mock): @@ -182,9 +182,9 @@ async def test_saving_state(hass: HomeAssistant, recorder_mock): state = db_state.to_native() state.attributes = db_state_attributes.to_native() assert len(db_states) == 1 - assert db_states[0].event_id > 0 + assert db_states[0].event_id is None - assert state == _state_empty_context(hass, entity_id) + assert state == _state_with_context(hass, entity_id) async def test_saving_many_states( @@ -210,7 +210,7 @@ async def test_saving_many_states( with session_scope(hass=hass) as session: db_states = list(session.query(States)) assert len(db_states) == 6 - assert db_states[0].event_id > 0 + assert db_states[0].event_id is None async def test_saving_state_with_intermixed_time_changes( @@ -234,7 +234,7 @@ async def test_saving_state_with_intermixed_time_changes( with session_scope(hass=hass) as session: db_states = list(session.query(States)) assert len(db_states) == 2 - assert db_states[0].event_id > 0 + assert db_states[0].event_id is None def test_saving_state_with_exception(hass, hass_recorder, caplog): @@ -411,7 +411,7 @@ def test_saving_state_with_commit_interval_zero(hass_recorder): with session_scope(hass=hass) as session: db_states = list(session.query(States)) assert len(db_states) == 1 - assert db_states[0].event_id > 0 + assert db_states[0].event_id is None def _add_entities(hass, entity_ids): @@ -454,12 +454,10 @@ def _add_events(hass, events): return events -def _state_empty_context(hass, entity_id): +def _state_with_context(hass, entity_id): # We don't restore context unless we need it by joining the # events table on the event_id for state_changed events - state = hass.states.get(entity_id) - state.context = Context(id=None) - return state + return hass.states.get(entity_id) # pylint: disable=redefined-outer-name,invalid-name @@ -468,7 +466,7 @@ def test_saving_state_include_domains(hass_recorder): hass = hass_recorder({"include": {"domains": "test2"}}) states = _add_entities(hass, ["test.recorder", "test2.recorder"]) assert len(states) == 1 - assert _state_empty_context(hass, "test2.recorder") == states[0] + assert _state_with_context(hass, "test2.recorder") == states[0] def test_saving_state_include_domains_globs(hass_recorder): @@ -480,8 +478,8 @@ def test_saving_state_include_domains_globs(hass_recorder): hass, ["test.recorder", "test2.recorder", "test3.included_entity"] ) assert len(states) == 2 - assert _state_empty_context(hass, "test2.recorder") == states[0] - assert _state_empty_context(hass, "test3.included_entity") == states[1] + assert _state_with_context(hass, "test2.recorder") == states[0] + assert _state_with_context(hass, "test3.included_entity") == states[1] def test_saving_state_incl_entities(hass_recorder): @@ -489,7 +487,7 @@ def test_saving_state_incl_entities(hass_recorder): hass = hass_recorder({"include": {"entities": "test2.recorder"}}) states = _add_entities(hass, ["test.recorder", "test2.recorder"]) assert len(states) == 1 - assert _state_empty_context(hass, "test2.recorder") == states[0] + assert _state_with_context(hass, "test2.recorder") == states[0] def test_saving_event_exclude_event_type(hass_recorder): @@ -518,7 +516,7 @@ def test_saving_state_exclude_domains(hass_recorder): hass = hass_recorder({"exclude": {"domains": "test"}}) states = _add_entities(hass, ["test.recorder", "test2.recorder"]) assert len(states) == 1 - assert _state_empty_context(hass, "test2.recorder") == states[0] + assert _state_with_context(hass, "test2.recorder") == states[0] def test_saving_state_exclude_domains_globs(hass_recorder): @@ -530,7 +528,7 @@ def test_saving_state_exclude_domains_globs(hass_recorder): hass, ["test.recorder", "test2.recorder", "test2.excluded_entity"] ) assert len(states) == 1 - assert _state_empty_context(hass, "test2.recorder") == states[0] + assert _state_with_context(hass, "test2.recorder") == states[0] def test_saving_state_exclude_entities(hass_recorder): @@ -538,7 +536,7 @@ def test_saving_state_exclude_entities(hass_recorder): hass = hass_recorder({"exclude": {"entities": "test.recorder"}}) states = _add_entities(hass, ["test.recorder", "test2.recorder"]) assert len(states) == 1 - assert _state_empty_context(hass, "test2.recorder") == states[0] + assert _state_with_context(hass, "test2.recorder") == states[0] def test_saving_state_exclude_domain_include_entity(hass_recorder): @@ -571,8 +569,8 @@ def test_saving_state_include_domain_exclude_entity(hass_recorder): ) states = _add_entities(hass, ["test.recorder", "test2.recorder", "test.ok"]) assert len(states) == 1 - assert _state_empty_context(hass, "test.ok") == states[0] - assert _state_empty_context(hass, "test.ok").state == "state2" + assert _state_with_context(hass, "test.ok") == states[0] + assert _state_with_context(hass, "test.ok").state == "state2" def test_saving_state_include_domain_glob_exclude_entity(hass_recorder): @@ -587,8 +585,8 @@ def test_saving_state_include_domain_glob_exclude_entity(hass_recorder): hass, ["test.recorder", "test2.recorder", "test.ok", "test2.included_entity"] ) assert len(states) == 1 - assert _state_empty_context(hass, "test.ok") == states[0] - assert _state_empty_context(hass, "test.ok").state == "state2" + assert _state_with_context(hass, "test.ok") == states[0] + assert _state_with_context(hass, "test.ok").state == "state2" def test_saving_state_and_removing_entity(hass, hass_recorder): @@ -1153,8 +1151,8 @@ def test_service_disable_states_not_recording(hass, hass_recorder): with session_scope(hass=hass) as session: db_states = list(session.query(States)) assert len(db_states) == 1 - assert db_states[0].event_id > 0 - assert db_states[0].to_native() == _state_empty_context(hass, "test.two") + assert db_states[0].event_id is None + assert db_states[0].to_native() == _state_with_context(hass, "test.two") def test_service_disable_run_information_recorded(tmpdir): @@ -1257,7 +1255,7 @@ async def test_database_corruption_while_running(hass, tmpdir, caplog): with session_scope(hass=hass) as session: db_states = list(session.query(States)) assert len(db_states) == 1 - assert db_states[0].event_id > 0 + assert db_states[0].event_id is None return db_states[0].to_native() state = await hass.async_add_executor_job(_get_last_state) diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index 3bdb7992c7c..a68d137eb0f 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -39,9 +39,6 @@ def test_from_event_to_db_state(): {"entity_id": "sensor.temperature", "old_state": None, "new_state": state}, context=state.context, ) - # We don't restore context unless we need it by joining the - # events table on the event_id for state_changed events - state.context = ha.Context(id=None) assert state == States.from_event(event).to_native() diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 1f0f3c87ea5..d946d1e2a14 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -64,7 +64,7 @@ async def test_purge_old_states( assert state_attributes.count() == 3 events = session.query(Events).filter(Events.event_type == "state_changed") - assert events.count() == 6 + assert events.count() == 0 assert "test.recorder2" in instance._old_states purge_before = dt_util.utcnow() - timedelta(days=4) @@ -108,7 +108,7 @@ async def test_purge_old_states( assert states[5].old_state_id == states[4].state_id events = session.query(Events).filter(Events.event_type == "state_changed") - assert events.count() == 6 + assert events.count() == 0 assert "test.recorder2" in instance._old_states state_attributes = session.query(StateAttributes) @@ -793,7 +793,6 @@ async def test_purge_filtered_states( assert session.query(StateAttributes).count() == 11 - # Finally make sure we can delete them all except for the ones missing an event_id service_data = {"keep_days": 0} await hass.services.async_call( recorder.DOMAIN, recorder.SERVICE_PURGE, service_data @@ -805,8 +804,8 @@ async def test_purge_filtered_states( remaining = list(session.query(States)) for state in remaining: assert state.event_id is None - assert len(remaining) == 3 - assert session.query(StateAttributes).count() == 1 + assert len(remaining) == 0 + assert session.query(StateAttributes).count() == 0 @pytest.mark.parametrize("use_sqlite", (True, False), indirect=True) From 37b59dfcc0dd2823305a2a9e8ec75bab30a17d8c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 2 May 2022 09:51:19 +0200 Subject: [PATCH 0098/3516] Make sure sensor state value is not None prior to trying to used the scaled value (#71189) --- homeassistant/components/deconz/sensor.py | 8 +++--- tests/components/deconz/test_sensor.py | 30 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index a4dcaed8f18..d3a20fad522 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -112,7 +112,7 @@ ENTITY_DESCRIPTIONS = { DeconzSensorDescription( key="consumption", value_fn=lambda device: device.scaled_consumption - if isinstance(device, Consumption) + if isinstance(device, Consumption) and isinstance(device.consumption, int) else None, update_key="consumption", device_class=SensorDeviceClass.ENERGY, @@ -144,7 +144,7 @@ ENTITY_DESCRIPTIONS = { DeconzSensorDescription( key="humidity", value_fn=lambda device: device.scaled_humidity - if isinstance(device, Humidity) + if isinstance(device, Humidity) and isinstance(device.humidity, int) else None, update_key="humidity", device_class=SensorDeviceClass.HUMIDITY, @@ -156,7 +156,7 @@ ENTITY_DESCRIPTIONS = { DeconzSensorDescription( key="light_level", value_fn=lambda device: device.scaled_light_level - if isinstance(device, LightLevel) + if isinstance(device, LightLevel) and isinstance(device.light_level, int) else None, update_key="lightlevel", device_class=SensorDeviceClass.ILLUMINANCE, @@ -189,7 +189,7 @@ ENTITY_DESCRIPTIONS = { DeconzSensorDescription( key="temperature", value_fn=lambda device: device.scaled_temperature - if isinstance(device, Temperature) + if isinstance(device, Temperature) and isinstance(device.temperature, int) else None, update_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index bd51bab44e4..590ccee25d5 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -785,6 +785,36 @@ async def test_add_new_sensor(hass, aioclient_mock, mock_deconz_websocket): assert hass.states.get("sensor.light_level_sensor").state == "999.8" +BAD_SENSOR_DATA = [ + ("ZHAConsumption", "consumption"), + ("ZHAHumidity", "humidity"), + ("ZHALightLevel", "lightlevel"), + ("ZHATemperature", "temperature"), +] + + +@pytest.mark.parametrize("sensor_type, sensor_property", BAD_SENSOR_DATA) +async def test_dont_add_sensor_if_state_is_none( + hass, aioclient_mock, sensor_type, sensor_property +): + """Test sensor with scaled data is not created if state is None.""" + data = { + "sensors": { + "1": { + "name": "Sensor 1", + "type": sensor_type, + "state": {sensor_property: None}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:00-00", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + await setup_deconz_integration(hass, aioclient_mock) + + assert len(hass.states.async_all()) == 0 + + async def test_add_battery_later(hass, aioclient_mock, mock_deconz_websocket): """Test that a sensor without an initial battery state creates a battery sensor once state exist.""" data = { From 738701a2d628c9d9b32f0fa7d5cbc0c375e0a5e1 Mon Sep 17 00:00:00 2001 From: stegm Date: Mon, 2 May 2022 11:42:18 +0200 Subject: [PATCH 0099/3516] Handle missing kostal plenticore battery option (#65237) Co-authored-by: Martin Hjelmare --- .../components/kostal_plenticore/select.py | 45 +++++++++----- .../components/kostal_plenticore/conftest.py | 61 ++++++++----------- .../kostal_plenticore/test_diagnostics.py | 31 +++++++++- .../kostal_plenticore/test_select.py | 45 ++++++++++++++ 4 files changed, 130 insertions(+), 52 deletions(-) create mode 100644 tests/components/kostal_plenticore/test_select.py diff --git a/homeassistant/components/kostal_plenticore/select.py b/homeassistant/components/kostal_plenticore/select.py index 1844f0894a5..7ac06f2ebef 100644 --- a/homeassistant/components/kostal_plenticore/select.py +++ b/homeassistant/components/kostal_plenticore/select.py @@ -24,6 +24,8 @@ async def async_setup_entry( ) -> None: """Add kostal plenticore Select widget.""" plenticore: Plenticore = hass.data[DOMAIN][entry.entry_id] + + available_settings_data = await plenticore.client.get_settings() select_data_update_coordinator = SelectDataUpdateCoordinator( hass, _LOGGER, @@ -32,23 +34,34 @@ async def async_setup_entry( plenticore, ) - async_add_entities( - PlenticoreDataSelect( - select_data_update_coordinator, - entry_id=entry.entry_id, - platform_name=entry.title, - device_class="kostal_plenticore__battery", - module_id=select.module_id, - data_id=select.data_id, - name=select.name, - current_option="None", - options=select.options, - is_on=select.is_on, - device_info=plenticore.device_info, - unique_id=f"{entry.entry_id}_{select.module_id}", + entities = [] + for select in SELECT_SETTINGS_DATA: + if select.module_id not in available_settings_data: + continue + needed_data_ids = {data_id for data_id in select.options if data_id != "None"} + available_data_ids = { + setting.id for setting in available_settings_data[select.module_id] + } + if not needed_data_ids <= available_data_ids: + continue + entities.append( + PlenticoreDataSelect( + select_data_update_coordinator, + entry_id=entry.entry_id, + platform_name=entry.title, + device_class="kostal_plenticore__battery", + module_id=select.module_id, + data_id=select.data_id, + name=select.name, + current_option="None", + options=select.options, + is_on=select.is_on, + device_info=plenticore.device_info, + unique_id=f"{entry.entry_id}_{select.module_id}", + ) ) - for select in SELECT_SETTINGS_DATA - ) + + async_add_entities(entities) class PlenticoreDataSelect(CoordinatorEntity, SelectEntity, ABC): diff --git a/tests/components/kostal_plenticore/conftest.py b/tests/components/kostal_plenticore/conftest.py index c3ed1b45592..4e789a34198 100644 --- a/tests/components/kostal_plenticore/conftest.py +++ b/tests/components/kostal_plenticore/conftest.py @@ -4,9 +4,10 @@ from __future__ import annotations from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch -from kostal.plenticore import MeData, SettingsData, VersionData +from kostal.plenticore import MeData, VersionData import pytest +from homeassistant.components.kostal_plenticore.helper import Plenticore from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo @@ -14,10 +15,19 @@ from tests.common import MockConfigEntry @pytest.fixture -async def init_integration( - hass: HomeAssistant, -) -> Generator[None, MockConfigEntry, None]: - """Set up Kostal Plenticore integration for testing.""" +def mock_config_entry() -> MockConfigEntry: + """Return a mocked ConfigEntry for testing.""" + return MockConfigEntry( + entry_id="2ab8dd92a62787ddfe213a67e09406bd", + title="scb", + domain="kostal_plenticore", + data={"host": "192.168.1.2", "password": "SecretPassword"}, + ) + + +@pytest.fixture +def mock_plenticore() -> Generator[Plenticore, None, None]: + """Set up a Plenticore mock with some default values.""" with patch( "homeassistant.components.kostal_plenticore.Plenticore", autospec=True ) as mock_api_class: @@ -60,37 +70,20 @@ async def init_integration( ) plenticore.client.get_process_data = AsyncMock() - plenticore.client.get_process_data.return_value = { - "devices:local": ["HomeGrid_P", "HomePv_P"] - } - plenticore.client.get_settings = AsyncMock() - plenticore.client.get_settings.return_value = { - "devices:local": [ - SettingsData( - { - "id": "Battery:MinSoc", - "unit": "%", - "default": "None", - "min": 5, - "max": 100, - "type": "byte", - "access": "readwrite", - } - ) - ] - } - mock_config_entry = MockConfigEntry( - entry_id="2ab8dd92a62787ddfe213a67e09406bd", - title="scb", - domain="kostal_plenticore", - data={"host": "192.168.1.2", "password": "SecretPassword"}, - ) + yield plenticore - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> MockConfigEntry: + """Set up Kostal Plenticore integration for testing.""" - yield mock_config_entry + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/kostal_plenticore/test_diagnostics.py b/tests/components/kostal_plenticore/test_diagnostics.py index 56af8bafe06..1f249aa3798 100644 --- a/tests/components/kostal_plenticore/test_diagnostics.py +++ b/tests/components/kostal_plenticore/test_diagnostics.py @@ -1,7 +1,9 @@ """Test Kostal Plenticore diagnostics.""" from aiohttp import ClientSession +from kostal.plenticore import SettingsData from homeassistant.components.diagnostics import REDACTED +from homeassistant.components.kostal_plenticore.helper import Plenticore from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -9,9 +11,34 @@ from tests.components.diagnostics import get_diagnostics_for_config_entry async def test_entry_diagnostics( - hass: HomeAssistant, hass_client: ClientSession, init_integration: MockConfigEntry -): + hass: HomeAssistant, + hass_client: ClientSession, + mock_plenticore: Plenticore, + init_integration: MockConfigEntry, +) -> None: """Test config entry diagnostics.""" + + # set some test process and settings data for the diagnostics output + mock_plenticore.client.get_process_data.return_value = { + "devices:local": ["HomeGrid_P", "HomePv_P"] + } + + mock_plenticore.client.get_settings.return_value = { + "devices:local": [ + SettingsData( + { + "id": "Battery:MinSoc", + "unit": "%", + "default": "None", + "min": 5, + "max": 100, + "type": "byte", + "access": "readwrite", + } + ) + ] + } + assert await get_diagnostics_for_config_entry( hass, hass_client, init_integration ) == { diff --git a/tests/components/kostal_plenticore/test_select.py b/tests/components/kostal_plenticore/test_select.py new file mode 100644 index 00000000000..6023b015483 --- /dev/null +++ b/tests/components/kostal_plenticore/test_select.py @@ -0,0 +1,45 @@ +"""Test the Kostal Plenticore Solar Inverter select platform.""" +from kostal.plenticore import SettingsData + +from homeassistant.components.kostal_plenticore.helper import Plenticore +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry + +from tests.common import MockConfigEntry + + +async def test_select_battery_charging_usage_available( + hass: HomeAssistant, mock_plenticore: Plenticore, mock_config_entry: MockConfigEntry +) -> None: + """Test that the battery charging usage select entity is added if the settings are available.""" + + mock_plenticore.client.get_settings.return_value = { + "devices:local": [ + SettingsData({"id": "Battery:SmartBatteryControl:Enable"}), + SettingsData({"id": "Battery:TimeControl:Enable"}), + ] + } + + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert entity_registry.async_get(hass).async_is_registered( + "select.battery_charging_usage_mode" + ) + + +async def test_select_battery_charging_usage_not_available( + hass: HomeAssistant, mock_plenticore: Plenticore, mock_config_entry: MockConfigEntry +) -> None: + """Test that the battery charging usage select entity is not added if the settings are unavailable.""" + + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not entity_registry.async_get(hass).async_is_registered( + "select.battery_charging_usage_mode" + ) From 546ba8169d3de57513480e4ac9444b2009879b04 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 2 May 2022 13:15:19 +0200 Subject: [PATCH 0100/3516] Remove entity registry entries when script is removed (#71193) --- homeassistant/components/config/automation.py | 2 +- homeassistant/components/config/scene.py | 2 +- homeassistant/components/config/script.py | 16 ++++++++++++++-- tests/components/config/test_script.py | 19 +++++++++++++++++-- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index f1a2d9aab84..5a39b786e27 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -23,7 +23,7 @@ async def async_setup(hass): if action != ACTION_DELETE: return - ent_reg = await entity_registry.async_get_registry(hass) + ent_reg = entity_registry.async_get(hass) entity_id = ent_reg.async_get_entity_id(DOMAIN, DOMAIN, config_key) diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index 6523ff84158..862c8c46f4d 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -20,7 +20,7 @@ async def async_setup(hass): if action != ACTION_DELETE: return - ent_reg = await entity_registry.async_get_registry(hass) + ent_reg = entity_registry.async_get(hass) entity_id = ent_reg.async_get_entity_id(DOMAIN, HA_DOMAIN, config_key) diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index 7adc766a1ab..45a7a6dc227 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -6,9 +6,9 @@ from homeassistant.components.script.config import ( ) from homeassistant.config import SCRIPT_CONFIG_PATH from homeassistant.const import SERVICE_RELOAD -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er -from . import EditKeyBasedConfigView +from . import ACTION_DELETE, EditKeyBasedConfigView async def async_setup(hass): @@ -18,6 +18,18 @@ async def async_setup(hass): """post_write_hook for Config View that reloads scripts.""" await hass.services.async_call(DOMAIN, SERVICE_RELOAD) + if action != ACTION_DELETE: + return + + ent_reg = er.async_get(hass) + + entity_id = ent_reg.async_get_entity_id(DOMAIN, DOMAIN, config_key) + + if entity_id is None: + return + + ent_reg.async_remove(entity_id) + hass.http.register_view( EditScriptConfigView( DOMAIN, diff --git a/tests/components/config/test_script.py b/tests/components/config/test_script.py index dca9a8aa8a7..4b6ca1bdc8f 100644 --- a/tests/components/config/test_script.py +++ b/tests/components/config/test_script.py @@ -6,21 +6,34 @@ import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components import config +from homeassistant.helpers import entity_registry as er from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 @pytest.fixture(autouse=True) -async def setup_script(hass, stub_blueprint_populate): # noqa: F811 +async def setup_script(hass, script_config, stub_blueprint_populate): # noqa: F811 """Set up script integration.""" - assert await async_setup_component(hass, "script", {}) + assert await async_setup_component(hass, "script", {"script": script_config}) +@pytest.mark.parametrize( + "script_config", + ( + { + "one": {"alias": "Light on", "sequence": []}, + "two": {"alias": "Light off", "sequence": []}, + }, + ), +) async def test_delete_script(hass, hass_client): """Test deleting a script.""" with patch.object(config, "SECTIONS", ["script"]): await async_setup_component(hass, "config", {}) + ent_reg = er.async_get(hass) + assert len(ent_reg.entities) == 2 + client = await hass_client() orig_data = {"one": {}, "two": {}} @@ -46,3 +59,5 @@ async def test_delete_script(hass, hass_client): assert len(written) == 1 assert written[0] == {"one": {}} + + assert len(ent_reg.entities) == 1 From 3f7c6a1ba733c28e9bc73654c994430c6857e0a5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 May 2022 04:35:19 -0700 Subject: [PATCH 0101/3516] Offer visit device for Squeezelite32 devices (#71181) --- homeassistant/components/slimproto/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/slimproto/media_player.py b/homeassistant/components/slimproto/media_player.py index af3eb693478..d41d10432db 100644 --- a/homeassistant/components/slimproto/media_player.py +++ b/homeassistant/components/slimproto/media_player.py @@ -100,8 +100,8 @@ class SlimProtoPlayer(MediaPlayerEntity): name=self.player.name, hw_version=self.player.firmware, ) - # PiCore player has web interface - if "-pCP" in self.player.firmware: + # PiCore + SqueezeESP32 player has web interface + if "-pCP" in self.player.firmware or self.player.device_model == "SqueezeESP32": self._attr_device_info[ "configuration_url" ] = f"http://{self.player.device_address}" From f6c2fb088c8f0c9f9d358e6c81025778dc36129a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 2 May 2022 14:59:58 +0200 Subject: [PATCH 0102/3516] Stop script if sub-script stops or aborts (#71195) --- homeassistant/helpers/script.py | 10 ++++-- tests/helpers/test_script.py | 61 ++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 70f73f065ab..d988b0edd81 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -395,8 +395,14 @@ class _ScriptRun: script_execution_set("finished") except _StopScript: script_execution_set("finished") + # Let the _StopScript bubble up if this is a sub-script + if not self._script.top_level: + raise except _AbortScript: script_execution_set("aborted") + # Let the _AbortScript bubble up if this is a sub-script + if not self._script.top_level: + raise except Exception: script_execution_set("error") raise @@ -1143,7 +1149,7 @@ class Script: hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, partial(_async_stop_scripts_at_shutdown, hass) ) - self._top_level = top_level + self.top_level = top_level if top_level: all_scripts.append( {"instance": self, "started_before_shutdown": not hass.is_stopping} @@ -1431,7 +1437,7 @@ class Script: # If this is a top level Script then make a copy of the variables in case they # are read-only, but more importantly, so as not to leak any variables created # during the run back to the caller. - if self._top_level: + if self.top_level: if self.variables: try: variables = self.variables.async_render( diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index fb3b021daec..b9c968838c9 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -94,7 +94,7 @@ def assert_element(trace_element, expected_element, path): # Check for unexpected items in trace_element assert not set(trace_element._result or {}) - set(expected_result) - if "error_type" in expected_element: + if "error_type" in expected_element and expected_element["error_type"] is not None: assert isinstance(trace_element._error, expected_element["error_type"]) else: assert trace_element._error is None @@ -4485,6 +4485,65 @@ async def test_stop_action(hass, caplog): ) +@pytest.mark.parametrize( + "error,error_type,logmsg,script_execution", + ( + (True, script._AbortScript, "Error", "aborted"), + (False, None, "Stop", "finished"), + ), +) +async def test_stop_action_subscript( + hass, caplog, error, error_type, logmsg, script_execution +): + """Test if automation stops on calling the stop action from a sub-script.""" + event = "test_event" + events = async_capture_events(hass, event) + + alias = "stop step" + sequence = cv.SCRIPT_SCHEMA( + [ + {"event": event}, + { + "if": { + "alias": "if condition", + "condition": "template", + "value_template": "{{ 1 == 1 }}", + }, + "then": { + "alias": alias, + "stop": "In the name of love", + "error": error, + }, + }, + {"event": event}, + ] + ) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert f"{logmsg} script sequence: In the name of love" in caplog.text + caplog.clear() + assert len(events) == 1 + + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{"error_type": error_type, "result": {"choice": "then"}}], + "1/if": [{"result": {"result": True}}], + "1/if/condition/0": [{"result": {"result": True, "entities": []}}], + "1/then/0": [ + { + "error_type": error_type, + "result": {"stop": "In the name of love", "error": error}, + } + ], + }, + expected_script_execution=script_execution, + ) + + async def test_stop_action_with_error(hass, caplog): """Test if automation fails on calling the error action.""" event = "test_event" From 56de00272704ead14c911da2325cad82920aa291 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 May 2022 06:09:49 -0700 Subject: [PATCH 0103/3516] Add media source support to AppleTV (#71185) --- .../components/apple_tv/browse_media.py | 2 +- .../components/apple_tv/media_player.py | 54 +++++++++++++++++-- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/apple_tv/browse_media.py b/homeassistant/components/apple_tv/browse_media.py index 3c0eee8b6ad..8d0a94ca858 100644 --- a/homeassistant/components/apple_tv/browse_media.py +++ b/homeassistant/components/apple_tv/browse_media.py @@ -18,7 +18,7 @@ def build_app_list(app_list): return BrowseMedia( media_class=MEDIA_CLASS_DIRECTORY, - media_content_id=None, + media_content_id="apps", media_content_type=MEDIA_TYPE_APPS, title="Apps", can_play=True, diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index cadf84e8b31..02919043bb2 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -13,11 +13,15 @@ from pyatv.const import ( ) from pyatv.helpers import is_streamable +from homeassistant.components import media_source from homeassistant.components.media_player import ( BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, ) +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_APP, MEDIA_TYPE_MUSIC, @@ -73,7 +77,8 @@ SUPPORT_APPLE_TV = ( # Map features in pyatv to Home Assistant SUPPORT_FEATURE_MAPPING = { - FeatureName.PlayUrl: MediaPlayerEntityFeature.PLAY_MEDIA, + FeatureName.PlayUrl: MediaPlayerEntityFeature.BROWSE_MEDIA + | MediaPlayerEntityFeature.PLAY_MEDIA, FeatureName.StreamFile: MediaPlayerEntityFeature.PLAY_MEDIA, FeatureName.Pause: MediaPlayerEntityFeature.PAUSE, FeatureName.Play: MediaPlayerEntityFeature.PLAY, @@ -276,12 +281,24 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): # RAOP. Otherwise try to play it with regular AirPlay. if media_type == MEDIA_TYPE_APP: await self.atv.apps.launch_app(media_id) - elif self._is_feature_available(FeatureName.StreamFile) and ( - await is_streamable(media_id) or media_type == MEDIA_TYPE_MUSIC + + is_media_source_id = media_source.is_media_source_id(media_id) + + if ( + not is_media_source_id + and self._is_feature_available(FeatureName.StreamFile) + and (await is_streamable(media_id) or media_type == MEDIA_TYPE_MUSIC) ): _LOGGER.debug("Streaming %s via RAOP", media_id) await self.atv.stream.stream_file(media_id) - elif self._is_feature_available(FeatureName.PlayUrl): + + if self._is_feature_available(FeatureName.PlayUrl): + if is_media_source_id: + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + + media_id = async_process_play_media_url(self.hass, media_id) + _LOGGER.debug("Playing %s via AirPlay", media_id) await self.atv.stream.play_url(media_id) else: @@ -380,7 +397,34 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): media_content_id=None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" - return build_app_list(self._app_list) + # If we can't stream URLs, we can't browse media. + # In that case the `BROWSE_MEDIA` feature was added because of AppList/LaunchApp + if not self._is_feature_available(FeatureName.PlayUrl): + return build_app_list(self._app_list) + + if self._app_list: + kwargs = {} + else: + # If it has no apps, assume it has no display + kwargs = { + "content_filter": lambda item: item.media_content_type.startswith( + "audio/" + ), + } + + cur_item = await media_source.async_browse_media( + self.hass, media_content_id, **kwargs + ) + + # If media content id is not None, we're browsing into a media source + if media_content_id is not None: + return cur_item + + # Add app item if we have one + if self._app_list and cur_item.children: + cur_item.children.insert(0, build_app_list(self._app_list)) + + return cur_item async def async_turn_on(self): """Turn the media player on.""" From d6617eba7c4699520b63013ef78377b672621ce9 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 2 May 2022 09:23:17 -0400 Subject: [PATCH 0104/3516] Fix bad ZHA _attr definitions (#71198) --- homeassistant/components/zha/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 0a5fe204648..249034ef068 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -345,7 +345,7 @@ class ElectricalMeasurementFrequency(ElectricalMeasurement, id_suffix="ac_freque """Frequency measurement.""" SENSOR_ATTR = "ac_frequency" - _device_class: SensorDeviceClass = SensorDeviceClass.FREQUENCY + _attr_device_class: SensorDeviceClass = SensorDeviceClass.FREQUENCY _unit = FREQUENCY_HERTZ _div_mul_prefix = "ac_frequency" @@ -360,7 +360,7 @@ class ElectricalMeasurementPowerFactor(ElectricalMeasurement, id_suffix="power_f """Frequency measurement.""" SENSOR_ATTR = "power_factor" - _device_class: SensorDeviceClass = SensorDeviceClass.POWER_FACTOR + _attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER_FACTOR _unit = PERCENTAGE @property @@ -718,8 +718,8 @@ class SinopeHVACAction(ThermostatHVACAction): class RSSISensor(Sensor, id_suffix="rssi"): """RSSI sensor for a device.""" - _state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _device_class: SensorDeviceClass = SensorDeviceClass.SIGNAL_STRENGTH + _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _attr_device_class: SensorDeviceClass = SensorDeviceClass.SIGNAL_STRENGTH _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_registry_enabled_default = False From 1e18307a6678b6f77bf53567c4aa22d005cf190b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 2 May 2022 15:50:13 +0200 Subject: [PATCH 0105/3516] Add reauth flow to Meater (#69895) --- homeassistant/components/meater/__init__.py | 9 +-- .../components/meater/config_flow.py | 58 ++++++++++++++++--- homeassistant/components/meater/strings.json | 9 +++ .../components/meater/translations/en.json | 9 +++ tests/components/meater/test_config_flow.py | 52 ++++++++++++++--- 5 files changed, 119 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/meater/__init__.py b/homeassistant/components/meater/__init__.py index a722928a13f..904d9e412c0 100644 --- a/homeassistant/components/meater/__init__.py +++ b/homeassistant/components/meater/__init__.py @@ -14,7 +14,7 @@ from meater.MeaterApi import MeaterProbe from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -40,8 +40,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (ServiceUnavailableError, TooManyRequestsError) as err: raise ConfigEntryNotReady from err except AuthenticationError as err: - _LOGGER.error("Unable to authenticate with the Meater API: %s", err) - return False + raise ConfigEntryAuthFailed( + f"Unable to authenticate with the Meater API: {err}" + ) from err async def async_update_data() -> dict[str, MeaterProbe]: """Fetch data from API endpoint.""" @@ -51,7 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async with async_timeout.timeout(10): devices: list[MeaterProbe] = await meater_api.get_all_devices() except AuthenticationError as err: - raise UpdateFailed("The API call wasn't authenticated") from err + raise ConfigEntryAuthFailed("The API call wasn't authenticated") from err except TooManyRequestsError as err: raise UpdateFailed( "Too many requests have been made to the API, rate limiting is in place" diff --git a/homeassistant/components/meater/config_flow.py b/homeassistant/components/meater/config_flow.py index 1b1a8a0eca4..07dbd4bd4a5 100644 --- a/homeassistant/components/meater/config_flow.py +++ b/homeassistant/components/meater/config_flow.py @@ -1,14 +1,18 @@ """Config flow for Meater.""" +from __future__ import annotations + from meater import AuthenticationError, MeaterApi, ServiceUnavailableError import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import DOMAIN -FLOW_SCHEMA = vol.Schema( +REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) +USER_SCHEMA = vol.Schema( {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} ) @@ -16,12 +20,17 @@ FLOW_SCHEMA = vol.Schema( class MeaterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Meater Config Flow.""" - async def async_step_user(self, user_input=None): + _data_schema = USER_SCHEMA + _username: str + + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Define the login user step.""" if user_input is None: return self.async_show_form( step_id="user", - data_schema=FLOW_SCHEMA, + data_schema=self._data_schema, ) username: str = user_input[CONF_USERNAME] @@ -31,13 +40,41 @@ class MeaterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): username = user_input[CONF_USERNAME] password = user_input[CONF_PASSWORD] + return await self._try_connect_meater("user", None, username, password) + + async def async_step_reauth(self, data: dict[str, str]) -> FlowResult: + """Handle configuration by re-auth.""" + self._data_schema = REAUTH_SCHEMA + self._username = data[CONF_USERNAME] + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle re-auth completion.""" + placeholders = {"username": self._username} + if not user_input: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=self._data_schema, + description_placeholders=placeholders, + ) + + password = user_input[CONF_PASSWORD] + return await self._try_connect_meater( + "reauth_confirm", placeholders, self._username, password + ) + + async def _try_connect_meater( + self, step_id, placeholders: dict[str, str] | None, username: str, password: str + ) -> FlowResult: session = aiohttp_client.async_get_clientsession(self.hass) api = MeaterApi(session) errors = {} try: - await api.authenticate(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) + await api.authenticate(username, password) except AuthenticationError: errors["base"] = "invalid_auth" except ServiceUnavailableError: @@ -45,13 +82,20 @@ class MeaterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except Exception: # pylint: disable=broad-except errors["base"] = "unknown_auth_error" else: + data = {"username": username, "password": password} + existing_entry = await self.async_set_unique_id(username.lower()) + 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 self.async_create_entry( title="Meater", - data={"username": username, "password": password}, + data=data, ) return self.async_show_form( - step_id="user", - data_schema=FLOW_SCHEMA, + step_id=step_id, + data_schema=self._data_schema, + description_placeholders=placeholders, errors=errors, ) diff --git a/homeassistant/components/meater/strings.json b/homeassistant/components/meater/strings.json index 772e6afd080..635c71d324c 100644 --- a/homeassistant/components/meater/strings.json +++ b/homeassistant/components/meater/strings.json @@ -6,6 +6,15 @@ "data": { "password": "[%key:common::config_flow::data::password%]", "username": "[%key:common::config_flow::data::username%]" + }, + "data_description": { + "username": "Meater Cloud username, typically an email address." + } + }, + "reauth_confirm": { + "description": "Confirm the password for Meater Cloud account {username}.", + "data": { + "password": "[%key:common::config_flow::data::password%]" } } }, diff --git a/homeassistant/components/meater/translations/en.json b/homeassistant/components/meater/translations/en.json index 3ceb94bcef0..707c6dc6ed6 100644 --- a/homeassistant/components/meater/translations/en.json +++ b/homeassistant/components/meater/translations/en.json @@ -6,11 +6,20 @@ "unknown_auth_error": "Unexpected error" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Confirm the password for Meater Cloud account {username}." + }, "user": { "data": { "password": "Password", "username": "Username" }, + "data_description": { + "username": "Meater Cloud username, typically an email address." + }, "description": "Set up your Meater Cloud account." } } diff --git a/tests/components/meater/test_config_flow.py b/tests/components/meater/test_config_flow.py index 597b72c354a..11312111311 100644 --- a/tests/components/meater/test_config_flow.py +++ b/tests/components/meater/test_config_flow.py @@ -4,9 +4,8 @@ from unittest.mock import AsyncMock, patch from meater import AuthenticationError, ServiceUnavailableError import pytest -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.meater import DOMAIN -from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry @@ -35,7 +34,7 @@ async def test_duplicate_error(hass): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=conf + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -48,7 +47,7 @@ async def test_unknown_auth_error(hass, mock_meater): conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"} result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=conf + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) assert result["errors"] == {"base": "unknown_auth_error"} @@ -59,7 +58,7 @@ async def test_invalid_credentials(hass, mock_meater): conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"} result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=conf + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) assert result["errors"] == {"base": "invalid_auth"} @@ -72,7 +71,7 @@ async def test_service_unavailable(hass, mock_meater): conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"} result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=conf + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) assert result["errors"] == {"base": "service_unavailable_error"} @@ -82,7 +81,7 @@ async def test_user_flow(hass, mock_meater): conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"} result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=None + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -106,3 +105,42 @@ async def test_user_flow(hass, mock_meater): CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123", } + + +async def test_reauth_flow(hass, mock_meater): + """Test that the reauth flow works.""" + data = { + CONF_USERNAME: "user@host.com", + CONF_PASSWORD: "password123", + } + mock_config = MockConfigEntry( + domain=DOMAIN, + unique_id="user@host.com", + data=data, + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"password": "passwordabc"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == { + CONF_USERNAME: "user@host.com", + CONF_PASSWORD: "passwordabc", + } From f35e7d1129ccde6994a0969137efdca63a4b0a1e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 2 May 2022 16:41:14 +0200 Subject: [PATCH 0106/3516] Allow cancelling async_at_start helper (#71196) --- homeassistant/helpers/start.py | 15 +++++++-- tests/helpers/test_start.py | 61 +++++++++++++++++++++++++++++++--- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/start.py b/homeassistant/helpers/start.py index 7f919f5351d..6c17ae5be3a 100644 --- a/homeassistant/helpers/start.py +++ b/homeassistant/helpers/start.py @@ -20,8 +20,19 @@ def async_at_start( hass.async_run_hass_job(at_start_job, hass) return lambda: None - async def _matched_event(event: Event) -> None: + unsub: None | CALLBACK_TYPE = None + + @callback + def _matched_event(event: Event) -> None: """Call the callback when Home Assistant started.""" hass.async_run_hass_job(at_start_job, hass) + nonlocal unsub + unsub = None - return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _matched_event) + @callback + def cancel() -> None: + if unsub: + unsub() + + unsub = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _matched_event) + return cancel diff --git a/tests/helpers/test_start.py b/tests/helpers/test_start.py index 55f98cf60eb..bc32ffa35fd 100644 --- a/tests/helpers/test_start.py +++ b/tests/helpers/test_start.py @@ -27,7 +27,7 @@ async def test_at_start_when_running_awaitable(hass): assert len(calls) == 2 -async def test_at_start_when_running_callback(hass): +async def test_at_start_when_running_callback(hass, caplog): """Test at start when already running.""" assert hass.state == core.CoreState.running assert hass.is_running @@ -39,15 +39,19 @@ async def test_at_start_when_running_callback(hass): """Home Assistant is started.""" calls.append(1) - start.async_at_start(hass, cb_at_start) + start.async_at_start(hass, cb_at_start)() assert len(calls) == 1 hass.state = core.CoreState.starting assert hass.is_running - start.async_at_start(hass, cb_at_start) + start.async_at_start(hass, cb_at_start)() assert len(calls) == 2 + # Check the unnecessary cancel did not generate warnings or errors + for record in caplog.records: + assert record.levelname in ("DEBUG", "INFO") + async def test_at_start_when_starting_awaitable(hass): """Test at start when yet to start.""" @@ -69,7 +73,7 @@ async def test_at_start_when_starting_awaitable(hass): assert len(calls) == 1 -async def test_at_start_when_starting_callback(hass): +async def test_at_start_when_starting_callback(hass, caplog): """Test at start when yet to start.""" hass.state = core.CoreState.not_running assert not hass.is_running @@ -81,10 +85,57 @@ async def test_at_start_when_starting_callback(hass): """Home Assistant is started.""" calls.append(1) - start.async_at_start(hass, cb_at_start) + cancel = start.async_at_start(hass, cb_at_start) await hass.async_block_till_done() assert len(calls) == 0 hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() assert len(calls) == 1 + + cancel() + + # Check the unnecessary cancel did not generate warnings or errors + for record in caplog.records: + assert record.levelname in ("DEBUG", "INFO") + + +async def test_cancelling_when_running(hass, caplog): + """Test cancelling at start when already running.""" + assert hass.state == core.CoreState.running + assert hass.is_running + + calls = [] + + async def cb_at_start(hass): + """Home Assistant is started.""" + calls.append(1) + + start.async_at_start(hass, cb_at_start)() + await hass.async_block_till_done() + assert len(calls) == 1 + + # Check the unnecessary cancel did not generate warnings or errors + for record in caplog.records: + assert record.levelname in ("DEBUG", "INFO") + + +async def test_cancelling_when_starting(hass): + """Test cancelling at start when yet to start.""" + hass.state = core.CoreState.not_running + assert not hass.is_running + + calls = [] + + @core.callback + def cb_at_start(hass): + """Home Assistant is started.""" + calls.append(1) + + start.async_at_start(hass, cb_at_start)() + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert len(calls) == 0 From a74f035ae7f16ae62d5d757f7b825ee174b039a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20T=C3=B3th?= <42900504+toth2zoltan@users.noreply.github.com> Date: Mon, 2 May 2022 16:42:23 +0200 Subject: [PATCH 0107/3516] Fix SAJ Solar inverter RecursionError (#71157) --- homeassistant/components/saj/sensor.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 2fb3729d0a8..818ce7ea57a 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -209,14 +209,11 @@ class SAJsensor(SensorEntity): @property def device_class(self): """Return the device class the sensor belongs to.""" - if self.unit_of_measurement == POWER_WATT: + if self.native_unit_of_measurement == POWER_WATT: return SensorDeviceClass.POWER - if self.unit_of_measurement == ENERGY_KILO_WATT_HOUR: + if self.native_unit_of_measurement == ENERGY_KILO_WATT_HOUR: return SensorDeviceClass.ENERGY - if ( - self.unit_of_measurement == TEMP_CELSIUS - or self._sensor.unit == TEMP_FAHRENHEIT - ): + if self.native_unit_of_measurement in (TEMP_CELSIUS, TEMP_FAHRENHEIT): return SensorDeviceClass.TEMPERATURE @property From 5e4e7ed1521c67ba8b07b76279212defd925bb93 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Mon, 2 May 2022 10:42:47 -0400 Subject: [PATCH 0108/3516] Fix Insteon thermostats and reduce logging (#71179) * Bump pyinsteon to 1.1.0 * Load modem aldb if read write mode is unkwown * Correct reference to read_write_mode --- homeassistant/components/insteon/__init__.py | 4 +++- homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 15181cb827c..94c18df9a33 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -4,6 +4,7 @@ from contextlib import suppress import logging from pyinsteon import async_close, async_connect, devices +from pyinsteon.constants import ReadWriteMode from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PLATFORM, EVENT_HOMEASSISTANT_STOP @@ -48,7 +49,8 @@ async def async_get_device_config(hass, config_entry): with suppress(AttributeError): await devices[address].async_status() - await devices.async_load(id_devices=1) + load_aldb = devices.modem.aldb.read_write_mode == ReadWriteMode.UNKNOWN + await devices.async_load(id_devices=1, load_modem_aldb=load_aldb) for addr in devices: device = devices[addr] flags = True diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index fc8ade1ba4d..c69f4f2cdf5 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/insteon", "dependencies": ["http", "websocket_api"], "requirements": [ - "pyinsteon==1.1.0b3", + "pyinsteon==1.1.0", "insteon-frontend-home-assistant==0.1.0" ], "codeowners": ["@teharris1"], diff --git a/requirements_all.txt b/requirements_all.txt index 6b89774f4e7..16177793510 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1550,7 +1550,7 @@ pyialarm==1.9.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.0b3 +pyinsteon==1.1.0 # homeassistant.components.intesishome pyintesishome==1.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fed48ff4175..fde9af6058b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1032,7 +1032,7 @@ pyialarm==1.9.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.0b3 +pyinsteon==1.1.0 # homeassistant.components.ipma pyipma==2.0.5 From a4682ae6e104134b3efc0bc25b0aee3368895dab Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 May 2022 17:35:37 +0200 Subject: [PATCH 0109/3516] Adjust version number in template default deprecation warning (#71203) --- homeassistant/helpers/template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index bded5ab933c..dbc82ce6902 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1331,7 +1331,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 2022.1" + "to render in Home Assistant core 2022.6" ), function, value, From 1aaf78ef9944ded259298afbdbedcc07c90b80b0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 2 May 2022 18:33:16 +0200 Subject: [PATCH 0110/3516] Remove entity category system in favor of hidden_by (#68550) --- homeassistant/const.py | 2 -- homeassistant/helpers/entity.py | 3 -- homeassistant/helpers/entity_platform.py | 2 +- tests/components/alexa/test_entities.py | 10 +----- tests/components/cloud/test_alexa_config.py | 12 +------ tests/components/cloud/test_google_config.py | 17 ++-------- tests/components/energy/test_sensor.py | 34 +++++++++++++++++++ .../google_assistant/test_google_assistant.py | 10 +----- tests/helpers/test_entity.py | 3 +- 9 files changed, 42 insertions(+), 51 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2f9a90032ef..74fe6adfdfd 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -764,11 +764,9 @@ CLOUD_NEVER_EXPOSED_ENTITIES: Final[list[str]] = ["group.all_locks"] # use the EntityCategory enum instead. ENTITY_CATEGORY_CONFIG: Final = "config" ENTITY_CATEGORY_DIAGNOSTIC: Final = "diagnostic" -ENTITY_CATEGORY_SYSTEM: Final = "system" ENTITY_CATEGORIES: Final[list[str]] = [ ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC, - ENTITY_CATEGORY_SYSTEM, ] # The ID of the Home Assistant Media Player Cast App diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index f07f4c1c7b8..c8faad53b0e 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -200,9 +200,6 @@ class EntityCategory(StrEnum): # Diagnostic: An entity exposing some configuration parameter or diagnostics of a device DIAGNOSTIC = "diagnostic" - # System: An entity which is not useful for the user to interact with - SYSTEM = "system" - ENTITY_CATEGORIES_SCHEMA: Final = vol.Coerce(EntityCategory) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 6972dbf7c16..3d57a9c3dc0 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -519,8 +519,8 @@ class EntityPlatform: config_entry=self.config_entry, device_id=device_id, disabled_by=disabled_by, - hidden_by=hidden_by, entity_category=entity.entity_category, + hidden_by=hidden_by, known_object_ids=self.entities.keys(), original_device_class=entity.device_class, original_icon=entity.icon, diff --git a/tests/components/alexa/test_entities.py b/tests/components/alexa/test_entities.py index 5f64879b535..fb364dbf14e 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -43,20 +43,13 @@ async def test_categorized_hidden_entities(hass): entity_category=EntityCategory.DIAGNOSTIC, ) entity_entry3 = entity_registry.async_get_or_create( - "switch", - "test", - "switch_system_id", - suggested_object_id="system_switch", - entity_category=EntityCategory.SYSTEM, - ) - entity_entry4 = entity_registry.async_get_or_create( "switch", "test", "switch_hidden_integration_id", suggested_object_id="hidden_integration_switch", hidden_by=er.RegistryEntryHider.INTEGRATION, ) - entity_entry5 = entity_registry.async_get_or_create( + entity_entry4 = entity_registry.async_get_or_create( "switch", "test", "switch_hidden_user_id", @@ -69,7 +62,6 @@ async def test_categorized_hidden_entities(hass): hass.states.async_set(entity_entry2.entity_id, "something_else") hass.states.async_set(entity_entry3.entity_id, "blah") hass.states.async_set(entity_entry4.entity_id, "foo") - hass.states.async_set(entity_entry5.entity_id, "bar") msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 115d39d3aeb..465ff7dd3d4 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -39,20 +39,13 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub): entity_category=EntityCategory.DIAGNOSTIC, ) entity_entry3 = entity_registry.async_get_or_create( - "light", - "test", - "light_system_id", - suggested_object_id="system_light", - entity_category=EntityCategory.SYSTEM, - ) - entity_entry4 = entity_registry.async_get_or_create( "light", "test", "light_hidden_integration_id", suggested_object_id="hidden_integration_light", hidden_by=er.RegistryEntryHider.INTEGRATION, ) - entity_entry5 = entity_registry.async_get_or_create( + entity_entry4 = entity_registry.async_get_or_create( "light", "test", "light_hidden_user_id", @@ -77,7 +70,6 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub): assert not conf.should_expose(entity_entry2.entity_id) assert not conf.should_expose(entity_entry3.entity_id) assert not conf.should_expose(entity_entry4.entity_id) - assert not conf.should_expose(entity_entry5.entity_id) entity_conf["should_expose"] = True assert conf.should_expose("light.kitchen") @@ -86,7 +78,6 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub): assert not conf.should_expose(entity_entry2.entity_id) assert not conf.should_expose(entity_entry3.entity_id) assert not conf.should_expose(entity_entry4.entity_id) - assert not conf.should_expose(entity_entry5.entity_id) entity_conf["should_expose"] = None assert conf.should_expose("light.kitchen") @@ -95,7 +86,6 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub): assert not conf.should_expose(entity_entry2.entity_id) assert not conf.should_expose(entity_entry3.entity_id) assert not conf.should_expose(entity_entry4.entity_id) - assert not conf.should_expose(entity_entry5.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 95746eb67ae..a86f1f3cf8a 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -298,20 +298,13 @@ async def test_google_config_expose_entity_prefs(hass, mock_conf, cloud_prefs): entity_category=EntityCategory.DIAGNOSTIC, ) entity_entry3 = entity_registry.async_get_or_create( - "light", - "test", - "light_system_id", - suggested_object_id="system_light", - entity_category=EntityCategory.SYSTEM, - ) - entity_entry4 = entity_registry.async_get_or_create( "light", "test", "light_hidden_integration_id", suggested_object_id="hidden_integration_light", hidden_by=er.RegistryEntryHider.INTEGRATION, ) - entity_entry5 = entity_registry.async_get_or_create( + entity_entry4 = entity_registry.async_get_or_create( "light", "test", "light_hidden_user_id", @@ -328,14 +321,12 @@ async def test_google_config_expose_entity_prefs(hass, 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") - state_system = State(entity_entry3.entity_id, "on") - state_hidden_integration = State(entity_entry4.entity_id, "on") - state_hidden_user = State(entity_entry5.entity_id, "on") + state_hidden_integration = State(entity_entry3.entity_id, "on") + state_hidden_user = State(entity_entry4.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) - assert not mock_conf.should_expose(state_system) assert not mock_conf.should_expose(state_hidden_integration) assert not mock_conf.should_expose(state_hidden_user) @@ -344,7 +335,6 @@ async def test_google_config_expose_entity_prefs(hass, mock_conf, cloud_prefs): # categorized and hidden entities should not be exposed assert not mock_conf.should_expose(state_config) assert not mock_conf.should_expose(state_diagnostic) - assert not mock_conf.should_expose(state_system) assert not mock_conf.should_expose(state_hidden_integration) assert not mock_conf.should_expose(state_hidden_user) @@ -353,7 +343,6 @@ async def test_google_config_expose_entity_prefs(hass, mock_conf, cloud_prefs): # categorized and hidden entities should not be exposed assert not mock_conf.should_expose(state_config) assert not mock_conf.should_expose(state_diagnostic) - assert not mock_conf.should_expose(state_system) assert not mock_conf.should_expose(state_hidden_integration) assert not mock_conf.should_expose(state_hidden_user) diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 913a2f44d93..997f60a8899 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -75,6 +75,40 @@ async def test_cost_sensor_no_states(hass, hass_storage, setup_integration) -> N # TODO: No states, should the cost entity refuse to setup? +async def test_cost_sensor_attributes(hass, hass_storage, setup_integration) -> None: + """Test sensor attributes.""" + energy_data = data.EnergyManager.default_preferences() + energy_data["energy_sources"].append( + { + "type": "grid", + "flow_from": [ + { + "stat_energy_from": "sensor.energy_consumption", + "entity_energy_from": "sensor.energy_consumption", + "stat_cost": None, + "entity_energy_price": None, + "number_energy_price": 1, + } + ], + "flow_to": [], + "cost_adjustment_day": 0, + } + ) + + hass_storage[data.STORAGE_KEY] = { + "version": 1, + "data": energy_data, + } + await setup_integration(hass) + + registry = er.async_get(hass) + cost_sensor_entity_id = "sensor.energy_consumption_cost" + entry = registry.async_get(cost_sensor_entity_id) + assert entry.entity_category is None + assert entry.disabled_by is None + assert entry.hidden_by == er.RegistryEntryHider.INTEGRATION + + @pytest.mark.parametrize("initial_energy,initial_cost", [(0, "0.0"), (None, "unknown")]) @pytest.mark.parametrize( "price_entity,fixed_price", [("sensor.energy_price", None), (None, 1)] diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 642ef15451a..8bf0e5573b2 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -147,20 +147,13 @@ async def test_sync_request(hass_fixture, assistant_client, auth_header): entity_category=EntityCategory.DIAGNOSTIC, ) entity_entry3 = entity_registry.async_get_or_create( - "switch", - "test", - "switch_system_id", - suggested_object_id="system_switch", - entity_category=EntityCategory.SYSTEM, - ) - entity_entry4 = entity_registry.async_get_or_create( "switch", "test", "switch_hidden_integration_id", suggested_object_id="hidden_integration_switch", hidden_by=er.RegistryEntryHider.INTEGRATION, ) - entity_entry5 = entity_registry.async_get_or_create( + entity_entry4 = entity_registry.async_get_or_create( "switch", "test", "switch_hidden_user_id", @@ -173,7 +166,6 @@ async def test_sync_request(hass_fixture, assistant_client, auth_header): hass_fixture.states.async_set(entity_entry2.entity_id, "something_else") hass_fixture.states.async_set(entity_entry3.entity_id, "blah") hass_fixture.states.async_set(entity_entry4.entity_id, "foo") - hass_fixture.states.async_set(entity_entry5.entity_id, "bar") reqid = "5711642932632160983" data = {"requestId": reqid, "inputs": [{"intent": "action.devices.SYNC"}]} diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 34cb68403f4..e345d7d7258 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -913,7 +913,6 @@ async def test_entity_category_property(hass): ( ("config", entity.EntityCategory.CONFIG), ("diagnostic", entity.EntityCategory.DIAGNOSTIC), - ("system", entity.EntityCategory.SYSTEM), ), ) def test_entity_category_schema(value, expected): @@ -930,7 +929,7 @@ def test_entity_category_schema_error(value): schema = vol.Schema(entity.ENTITY_CATEGORIES_SCHEMA) with pytest.raises( vol.Invalid, - match=r"expected EntityCategory or one of 'config', 'diagnostic', 'system'", + match=r"expected EntityCategory or one of 'config', 'diagnostic'", ): schema(value) From 0cdcdec80915a0f88f9229304446c2f53f33cea7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 May 2022 11:34:24 -0500 Subject: [PATCH 0111/3516] Speed up and isolate legacy logbook context_id query (#71201) --- homeassistant/components/logbook/__init__.py | 50 ++++++++++++++++---- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index fdf553775b4..7bacbd4cdd7 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -543,20 +543,24 @@ def _get_events( states_query = _generate_states_query( session, start_day, end_day, old_state, entity_ids ) + unions: list[Query] = [] if context_id is not None: # Once all the old `state_changed` events - # are gone from the database this query can - # be simplified to filter only on States.context_id == context_id + # are gone from the database remove the + # _generate_legacy_events_context_id_query + unions.append( + _generate_legacy_events_context_id_query( + session, context_id, start_day, end_day + ) + ) states_query = states_query.outerjoin( Events, (States.event_id == Events.event_id) ) - states_query = states_query.filter( - (States.context_id == context_id) - | (States.context_id.is_(None) & (Events.context_id == context_id)) - ) - if filters: + states_query = states_query.filter(States.context_id == context_id) + elif filters: states_query = states_query.filter(filters.entity_filter()) # type: ignore[no-untyped-call] - query = query.union_all(states_query) + unions.append(states_query) + query = query.union_all(*unions) query = query.order_by(Events.time_fired) @@ -578,6 +582,36 @@ def _generate_events_query_without_data(session: Session) -> Query: ) +def _generate_legacy_events_context_id_query( + session: Session, + context_id: str, + start_day: dt, + end_day: dt, +) -> Query: + """Generate a legacy events context id query that also joins states.""" + # This can be removed once we no longer have event_ids in the states table + legacy_context_id_query = session.query( + *EVENT_COLUMNS, + literal(value=None, type_=sqlalchemy.String).label("shared_data"), + States.state, + States.entity_id, + States.attributes, + StateAttributes.shared_attrs, + ) + legacy_context_id_query = _apply_event_time_filter( + legacy_context_id_query, start_day, end_day + ) + return ( + legacy_context_id_query.filter(Events.context_id == context_id) + .outerjoin(States, (Events.event_id == States.event_id)) + .filter(States.last_updated == States.last_changed) + .filter(_not_continuous_entity_matcher()) + .outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) + ) + + def _generate_events_query_without_states(session: Session) -> Query: return session.query( *EVENT_COLUMNS, EventData.shared_data.label("shared_data"), *EMPTY_STATE_COLUMNS From 0926470ef0f7bf7fd11da09a9d101ea17a4b4c00 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 May 2022 10:51:13 -0700 Subject: [PATCH 0112/3516] Skip signing URL that we know requires no auth (#71208) --- .../components/media_player/browse_media.py | 7 +++++++ tests/components/media_player/test_browse_media.py | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index 60234cd1b38..9327bf68f9f 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -20,6 +20,9 @@ from homeassistant.helpers.network import ( from .const import CONTENT_AUTH_EXPIRY_TIME, MEDIA_CLASS_DIRECTORY +# Paths that we don't need to sign +PATHS_WITHOUT_AUTH = ("/api/tts_proxy/",) + @callback def async_process_play_media_url( @@ -46,6 +49,10 @@ def async_process_play_media_url( logging.getLogger(__name__).debug( "Not signing path for content with query param" ) + elif parsed.path.startswith(PATHS_WITHOUT_AUTH): + # We don't sign this path if it doesn't need auth. Although signing itself can't hurt, + # some devices are unable to handle long URLs and the auth signature might push it over. + pass else: signed_path = async_sign_path( hass, diff --git a/tests/components/media_player/test_browse_media.py b/tests/components/media_player/test_browse_media.py index ea1e3b4fc36..58081c7b3b1 100644 --- a/tests/components/media_player/test_browse_media.py +++ b/tests/components/media_player/test_browse_media.py @@ -73,6 +73,18 @@ async def test_process_play_media_url(hass, mock_sign_path): == "http://192.168.123.123:8123/path?hello=world" ) + # Test skip signing URLs if they are known to require no auth + assert ( + async_process_play_media_url(hass, "/api/tts_proxy/bla") + == "http://example.local:8123/api/tts_proxy/bla" + ) + assert ( + async_process_play_media_url( + hass, "http://example.local:8123/api/tts_proxy/bla" + ) + == "http://example.local:8123/api/tts_proxy/bla" + ) + with pytest.raises(ValueError): async_process_play_media_url(hass, "hello") From 7c46eb5952146812c32d39bcfe77c4be1e2c6d99 Mon Sep 17 00:00:00 2001 From: Yuval Aboulafia Date: Mon, 2 May 2022 20:52:00 +0300 Subject: [PATCH 0113/3516] Add test for incorrect config for Jewish Calendar (#71163) * Loop load Jewish Calendar platforms * Address review * Add test for incorrect config * add test to sensor platform --- .../jewish_calendar/test_binary_sensor.py | 13 +++++++++++++ tests/components/jewish_calendar/test_sensor.py | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index 31cc0095af1..3ecac44b59c 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -4,6 +4,7 @@ from datetime import datetime as dt, timedelta import pytest from homeassistant.components import jewish_calendar +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -302,3 +303,15 @@ async def test_issur_melacha_sensor_update( hass.states.get("binary_sensor.test_issur_melacha_in_effect").state == result[1] ) + + +async def test_no_discovery_info(hass, caplog): + """Test setup without discovery info.""" + assert BINARY_SENSOR_DOMAIN not in hass.config.components + assert await async_setup_component( + hass, + BINARY_SENSOR_DOMAIN, + {BINARY_SENSOR_DOMAIN: {"platform": jewish_calendar.DOMAIN}}, + ) + await hass.async_block_till_done() + assert BINARY_SENSOR_DOMAIN in hass.config.components diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index d7d62a7ac97..a5dae8b4ce7 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -4,6 +4,7 @@ from datetime import datetime as dt, timedelta import pytest from homeassistant.components import jewish_calendar +from homeassistant.components.binary_sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -639,3 +640,15 @@ async def test_dafyomi_sensor(hass, test_time, result): await hass.async_block_till_done() assert hass.states.get("sensor.test_daf_yomi").state == result + + +async def test_no_discovery_info(hass, caplog): + """Test setup without discovery info.""" + assert SENSOR_DOMAIN not in hass.config.components + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + {SENSOR_DOMAIN: {"platform": jewish_calendar.DOMAIN}}, + ) + await hass.async_block_till_done() + assert SENSOR_DOMAIN in hass.config.components From 40cf75844a99794763fbdc3d6915b559bc2e493e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 May 2022 11:02:20 -0700 Subject: [PATCH 0114/3516] Add media source support to Bose Soundtouch (#71209) --- .../components/soundtouch/media_player.py | 20 +++++++++++++++++++ .../soundtouch/test_media_player.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index 7081f4a3a0f..3172eb4aed6 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -1,6 +1,7 @@ """Support for interface with a Bose Soundtouch.""" from __future__ import annotations +from functools import partial import logging import re @@ -8,11 +9,15 @@ from libsoundtouch import soundtouch_device from libsoundtouch.utils import Source import voluptuous as vol +from homeassistant.components import media_source from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, ) +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -190,6 +195,7 @@ class SoundTouchDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.SELECT_SOURCE + | MediaPlayerEntityFeature.BROWSE_MEDIA ) def __init__(self, name, config): @@ -348,6 +354,16 @@ class SoundTouchDevice(MediaPlayerEntity): EVENT_HOMEASSISTANT_START, async_update_on_start ) + async def async_play_media(self, media_type, media_id, **kwargs): + """Play a piece of media.""" + if media_source.is_media_source_id(media_id): + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = async_process_play_media_url(self.hass, play_item.url) + + await self.hass.async_add_executor_job( + partial(self.play_media, media_type, media_id, **kwargs) + ) + def play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" _LOGGER.debug("Starting media with media_id: %s", media_id) @@ -450,6 +466,10 @@ class SoundTouchDevice(MediaPlayerEntity): return attributes + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media(self.hass, media_content_id) + def get_zone_info(self): """Return the current zone info.""" zone_status = self._device.zone_status() diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index 4ed8a648c77..797b5b440d1 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -488,7 +488,7 @@ async def test_media_commands(mocked_status, mocked_volume, hass, one_device): assert mocked_volume.call_count == 2 entity_1_state = hass.states.get("media_player.soundtouch_1") - assert entity_1_state.attributes["supported_features"] == 20413 + assert entity_1_state.attributes["supported_features"] == 151485 @patch("libsoundtouch.device.SoundTouchDevice.power_off") From 188040b8bb7ae1d0776e686504bae6945f8d55c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 May 2022 17:17:21 -0500 Subject: [PATCH 0115/3516] Use lambda_stmt for recorder queries and migrate them to queries module (#71219) --- homeassistant/components/recorder/__init__.py | 47 +- homeassistant/components/recorder/purge.py | 455 +---------------- homeassistant/components/recorder/queries.py | 462 ++++++++++++++++++ tests/components/recorder/test_init.py | 2 - 4 files changed, 481 insertions(+), 485 deletions(-) create mode 100644 homeassistant/components/recorder/queries.py diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 486866fc799..35a664cac65 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -14,19 +14,10 @@ import time from typing import Any, TypeVar, cast from lru import LRU # pylint: disable=no-name-in-module -from sqlalchemy import ( - bindparam, - create_engine, - event as sqlalchemy_event, - exc, - func, - select, -) +from sqlalchemy import create_engine, event as sqlalchemy_event, exc, func, select from sqlalchemy.engine import Engine from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.ext import baked from sqlalchemy.orm import scoped_session, sessionmaker -from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session import voluptuous as vol @@ -90,6 +81,7 @@ from .models import ( process_timestamp, ) from .pool import POOL_SIZE, MutexPool, RecorderPool +from .queries import find_shared_attributes_id, find_shared_data_id from .run_history import RunHistory from .util import ( dburl_to_path, @@ -292,7 +284,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: entity_filter=entity_filter, exclude_t=exclude_t, exclude_attributes_by_domain=exclude_attributes_by_domain, - bakery=baked.bakery(), ) instance.async_initialize() instance.async_register() @@ -614,7 +605,6 @@ class Recorder(threading.Thread): entity_filter: Callable[[str], bool], exclude_t: list[str], exclude_attributes_by_domain: dict[str, set[str]], - bakery: baked.bakery, ) -> None: """Initialize the recorder.""" threading.Thread.__init__(self, name="Recorder") @@ -645,9 +635,6 @@ class Recorder(threading.Thread): self._pending_state_attributes: dict[str, StateAttributes] = {} self._pending_event_data: dict[str, EventData] = {} self._pending_expunge: list[States] = [] - self._bakery = bakery - self._find_shared_attr_query: Query | None = None - self._find_shared_data_query: Query | None = None self.event_session: Session | None = None self.get_session: Callable[[], Session] | None = None self._completed_first_database_setup: bool | None = None @@ -1152,19 +1139,11 @@ class Recorder(threading.Thread): # need to flush before checking the database. # assert self.event_session is not None - if self._find_shared_attr_query is None: - self._find_shared_attr_query = self._bakery( - lambda session: session.query(StateAttributes.attributes_id) - .filter(StateAttributes.hash == bindparam("attr_hash")) - .filter(StateAttributes.shared_attrs == bindparam("shared_attrs")) - ) with self.event_session.no_autoflush: - if ( - attributes := self._find_shared_attr_query(self.event_session) - .params(attr_hash=attr_hash, shared_attrs=shared_attrs) - .first() - ): - return cast(int, attributes[0]) + if attributes_id := self.event_session.execute( + find_shared_attributes_id(attr_hash, shared_attrs) + ).first(): + return cast(int, attributes_id[0]) return None def _find_shared_data_in_db(self, data_hash: int, shared_data: str) -> int | None: @@ -1178,18 +1157,10 @@ class Recorder(threading.Thread): # need to flush before checking the database. # assert self.event_session is not None - if self._find_shared_data_query is None: - self._find_shared_data_query = self._bakery( - lambda session: session.query(EventData.data_id) - .filter(EventData.hash == bindparam("data_hash")) - .filter(EventData.shared_data == bindparam("shared_data")) - ) with self.event_session.no_autoflush: - if ( - data_id := self._find_shared_data_query(self.event_session) - .params(data_hash=data_hash, shared_data=shared_data) - .first() - ): + if data_id := self.event_session.execute( + find_shared_data_id(data_hash, shared_data) + ).first(): return cast(int, data_id[0]) return None diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index dcc535c8177..3a0e2e6e141 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -7,11 +7,9 @@ from itertools import zip_longest import logging from typing import TYPE_CHECKING -from sqlalchemy import func, lambda_stmt, select, union_all +from sqlalchemy import func from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import distinct -from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import Select from homeassistant.const import EVENT_STATE_CHANGED @@ -25,6 +23,7 @@ from .models import ( StatisticsRuns, StatisticsShortTerm, ) +from .queries import attributes_ids_exist_in_states, data_ids_exist_in_events from .repack import repack_database from .util import retryable_database_job, session_scope @@ -131,223 +130,6 @@ def _select_event_state_attributes_ids_data_ids_to_purge( return event_ids, state_ids, attributes_ids, data_ids -def _state_attrs_exist(attr: int | None) -> Select: - """Check if a state attributes id exists in the states table.""" - return select(func.min(States.attributes_id)).where(States.attributes_id == attr) - - -def _generate_find_attr_lambda( - attr1: int, - attr2: int | None, - attr3: int | None, - attr4: int | None, - attr5: int | None, - attr6: int | None, - attr7: int | None, - attr8: int | None, - attr9: int | None, - attr10: int | None, - attr11: int | None, - attr12: int | None, - attr13: int | None, - attr14: int | None, - attr15: int | None, - attr16: int | None, - attr17: int | None, - attr18: int | None, - attr19: int | None, - attr20: int | None, - attr21: int | None, - attr22: int | None, - attr23: int | None, - attr24: int | None, - attr25: int | None, - attr26: int | None, - attr27: int | None, - attr28: int | None, - attr29: int | None, - attr30: int | None, - attr31: int | None, - attr32: int | None, - attr33: int | None, - attr34: int | None, - attr35: int | None, - attr36: int | None, - attr37: int | None, - attr38: int | None, - attr39: int | None, - attr40: int | None, - attr41: int | None, - attr42: int | None, - attr43: int | None, - attr44: int | None, - attr45: int | None, - attr46: int | None, - attr47: int | None, - attr48: int | None, - attr49: int | None, - attr50: int | None, - attr51: int | None, - attr52: int | None, - attr53: int | None, - attr54: int | None, - attr55: int | None, - attr56: int | None, - attr57: int | None, - attr58: int | None, - attr59: int | None, - attr60: int | None, - attr61: int | None, - attr62: int | None, - attr63: int | None, - attr64: int | None, - attr65: int | None, - attr66: int | None, - attr67: int | None, - attr68: int | None, - attr69: int | None, - attr70: int | None, - attr71: int | None, - attr72: int | None, - attr73: int | None, - attr74: int | None, - attr75: int | None, - attr76: int | None, - attr77: int | None, - attr78: int | None, - attr79: int | None, - attr80: int | None, - attr81: int | None, - attr82: int | None, - attr83: int | None, - attr84: int | None, - attr85: int | None, - attr86: int | None, - attr87: int | None, - attr88: int | None, - attr89: int | None, - attr90: int | None, - attr91: int | None, - attr92: int | None, - attr93: int | None, - attr94: int | None, - attr95: int | None, - attr96: int | None, - attr97: int | None, - attr98: int | None, - attr99: int | None, - attr100: int | None, -) -> StatementLambdaElement: - """Generate the find attributes select only once. - - https://docs.sqlalchemy.org/en/14/core/connections.html#quick-guidelines-for-lambdas - """ - return lambda_stmt( - lambda: union_all( - _state_attrs_exist(attr1), - _state_attrs_exist(attr2), - _state_attrs_exist(attr3), - _state_attrs_exist(attr4), - _state_attrs_exist(attr5), - _state_attrs_exist(attr6), - _state_attrs_exist(attr7), - _state_attrs_exist(attr8), - _state_attrs_exist(attr9), - _state_attrs_exist(attr10), - _state_attrs_exist(attr11), - _state_attrs_exist(attr12), - _state_attrs_exist(attr13), - _state_attrs_exist(attr14), - _state_attrs_exist(attr15), - _state_attrs_exist(attr16), - _state_attrs_exist(attr17), - _state_attrs_exist(attr18), - _state_attrs_exist(attr19), - _state_attrs_exist(attr20), - _state_attrs_exist(attr21), - _state_attrs_exist(attr22), - _state_attrs_exist(attr23), - _state_attrs_exist(attr24), - _state_attrs_exist(attr25), - _state_attrs_exist(attr26), - _state_attrs_exist(attr27), - _state_attrs_exist(attr28), - _state_attrs_exist(attr29), - _state_attrs_exist(attr30), - _state_attrs_exist(attr31), - _state_attrs_exist(attr32), - _state_attrs_exist(attr33), - _state_attrs_exist(attr34), - _state_attrs_exist(attr35), - _state_attrs_exist(attr36), - _state_attrs_exist(attr37), - _state_attrs_exist(attr38), - _state_attrs_exist(attr39), - _state_attrs_exist(attr40), - _state_attrs_exist(attr41), - _state_attrs_exist(attr42), - _state_attrs_exist(attr43), - _state_attrs_exist(attr44), - _state_attrs_exist(attr45), - _state_attrs_exist(attr46), - _state_attrs_exist(attr47), - _state_attrs_exist(attr48), - _state_attrs_exist(attr49), - _state_attrs_exist(attr50), - _state_attrs_exist(attr51), - _state_attrs_exist(attr52), - _state_attrs_exist(attr53), - _state_attrs_exist(attr54), - _state_attrs_exist(attr55), - _state_attrs_exist(attr56), - _state_attrs_exist(attr57), - _state_attrs_exist(attr58), - _state_attrs_exist(attr59), - _state_attrs_exist(attr60), - _state_attrs_exist(attr61), - _state_attrs_exist(attr62), - _state_attrs_exist(attr63), - _state_attrs_exist(attr64), - _state_attrs_exist(attr65), - _state_attrs_exist(attr66), - _state_attrs_exist(attr67), - _state_attrs_exist(attr68), - _state_attrs_exist(attr69), - _state_attrs_exist(attr70), - _state_attrs_exist(attr71), - _state_attrs_exist(attr72), - _state_attrs_exist(attr73), - _state_attrs_exist(attr74), - _state_attrs_exist(attr75), - _state_attrs_exist(attr76), - _state_attrs_exist(attr77), - _state_attrs_exist(attr78), - _state_attrs_exist(attr79), - _state_attrs_exist(attr80), - _state_attrs_exist(attr81), - _state_attrs_exist(attr82), - _state_attrs_exist(attr83), - _state_attrs_exist(attr84), - _state_attrs_exist(attr85), - _state_attrs_exist(attr86), - _state_attrs_exist(attr87), - _state_attrs_exist(attr88), - _state_attrs_exist(attr89), - _state_attrs_exist(attr90), - _state_attrs_exist(attr91), - _state_attrs_exist(attr92), - _state_attrs_exist(attr93), - _state_attrs_exist(attr94), - _state_attrs_exist(attr95), - _state_attrs_exist(attr96), - _state_attrs_exist(attr97), - _state_attrs_exist(attr98), - _state_attrs_exist(attr99), - _state_attrs_exist(attr100), - ) - ) - - def _select_unused_attributes_ids( session: Session, attributes_ids: set[int], using_sqlite: bool ) -> set[int]: @@ -402,11 +184,11 @@ def _select_unused_attributes_ids( groups = [iter(attributes_ids)] * 100 for attr_ids in zip_longest(*groups, fillvalue=None): seen_ids |= { - state[0] - for state in session.execute( - _generate_find_attr_lambda(*attr_ids) + attrs_id[0] + for attrs_id in session.execute( + attributes_ids_exist_in_states(*attr_ids) ).all() - if state[0] is not None + if attrs_id[0] is not None } to_remove = attributes_ids - seen_ids _LOGGER.debug( @@ -416,223 +198,6 @@ def _select_unused_attributes_ids( return to_remove -def _event_data_id_exist(data_id: int | None) -> Select: - """Check if a event data id exists in the events table.""" - return select(func.min(Events.data_id)).where(Events.data_id == data_id) - - -def _generate_find_data_id_lambda( - id1: int, - id2: int | None, - id3: int | None, - id4: int | None, - id5: int | None, - id6: int | None, - id7: int | None, - id8: int | None, - id9: int | None, - id10: int | None, - id11: int | None, - id12: int | None, - id13: int | None, - id14: int | None, - id15: int | None, - id16: int | None, - id17: int | None, - id18: int | None, - id19: int | None, - id20: int | None, - id21: int | None, - id22: int | None, - id23: int | None, - id24: int | None, - id25: int | None, - id26: int | None, - id27: int | None, - id28: int | None, - id29: int | None, - id30: int | None, - id31: int | None, - id32: int | None, - id33: int | None, - id34: int | None, - id35: int | None, - id36: int | None, - id37: int | None, - id38: int | None, - id39: int | None, - id40: int | None, - id41: int | None, - id42: int | None, - id43: int | None, - id44: int | None, - id45: int | None, - id46: int | None, - id47: int | None, - id48: int | None, - id49: int | None, - id50: int | None, - id51: int | None, - id52: int | None, - id53: int | None, - id54: int | None, - id55: int | None, - id56: int | None, - id57: int | None, - id58: int | None, - id59: int | None, - id60: int | None, - id61: int | None, - id62: int | None, - id63: int | None, - id64: int | None, - id65: int | None, - id66: int | None, - id67: int | None, - id68: int | None, - id69: int | None, - id70: int | None, - id71: int | None, - id72: int | None, - id73: int | None, - id74: int | None, - id75: int | None, - id76: int | None, - id77: int | None, - id78: int | None, - id79: int | None, - id80: int | None, - id81: int | None, - id82: int | None, - id83: int | None, - id84: int | None, - id85: int | None, - id86: int | None, - id87: int | None, - id88: int | None, - id89: int | None, - id90: int | None, - id91: int | None, - id92: int | None, - id93: int | None, - id94: int | None, - id95: int | None, - id96: int | None, - id97: int | None, - id98: int | None, - id99: int | None, - id100: int | None, -) -> StatementLambdaElement: - """Generate the find event data select only once. - - https://docs.sqlalchemy.org/en/14/core/connections.html#quick-guidelines-for-lambdas - """ - return lambda_stmt( - lambda: union_all( - _event_data_id_exist(id1), - _event_data_id_exist(id2), - _event_data_id_exist(id3), - _event_data_id_exist(id4), - _event_data_id_exist(id5), - _event_data_id_exist(id6), - _event_data_id_exist(id7), - _event_data_id_exist(id8), - _event_data_id_exist(id9), - _event_data_id_exist(id10), - _event_data_id_exist(id11), - _event_data_id_exist(id12), - _event_data_id_exist(id13), - _event_data_id_exist(id14), - _event_data_id_exist(id15), - _event_data_id_exist(id16), - _event_data_id_exist(id17), - _event_data_id_exist(id18), - _event_data_id_exist(id19), - _event_data_id_exist(id20), - _event_data_id_exist(id21), - _event_data_id_exist(id22), - _event_data_id_exist(id23), - _event_data_id_exist(id24), - _event_data_id_exist(id25), - _event_data_id_exist(id26), - _event_data_id_exist(id27), - _event_data_id_exist(id28), - _event_data_id_exist(id29), - _event_data_id_exist(id30), - _event_data_id_exist(id31), - _event_data_id_exist(id32), - _event_data_id_exist(id33), - _event_data_id_exist(id34), - _event_data_id_exist(id35), - _event_data_id_exist(id36), - _event_data_id_exist(id37), - _event_data_id_exist(id38), - _event_data_id_exist(id39), - _event_data_id_exist(id40), - _event_data_id_exist(id41), - _event_data_id_exist(id42), - _event_data_id_exist(id43), - _event_data_id_exist(id44), - _event_data_id_exist(id45), - _event_data_id_exist(id46), - _event_data_id_exist(id47), - _event_data_id_exist(id48), - _event_data_id_exist(id49), - _event_data_id_exist(id50), - _event_data_id_exist(id51), - _event_data_id_exist(id52), - _event_data_id_exist(id53), - _event_data_id_exist(id54), - _event_data_id_exist(id55), - _event_data_id_exist(id56), - _event_data_id_exist(id57), - _event_data_id_exist(id58), - _event_data_id_exist(id59), - _event_data_id_exist(id60), - _event_data_id_exist(id61), - _event_data_id_exist(id62), - _event_data_id_exist(id63), - _event_data_id_exist(id64), - _event_data_id_exist(id65), - _event_data_id_exist(id66), - _event_data_id_exist(id67), - _event_data_id_exist(id68), - _event_data_id_exist(id69), - _event_data_id_exist(id70), - _event_data_id_exist(id71), - _event_data_id_exist(id72), - _event_data_id_exist(id73), - _event_data_id_exist(id74), - _event_data_id_exist(id75), - _event_data_id_exist(id76), - _event_data_id_exist(id77), - _event_data_id_exist(id78), - _event_data_id_exist(id79), - _event_data_id_exist(id80), - _event_data_id_exist(id81), - _event_data_id_exist(id82), - _event_data_id_exist(id83), - _event_data_id_exist(id84), - _event_data_id_exist(id85), - _event_data_id_exist(id86), - _event_data_id_exist(id87), - _event_data_id_exist(id88), - _event_data_id_exist(id89), - _event_data_id_exist(id90), - _event_data_id_exist(id91), - _event_data_id_exist(id92), - _event_data_id_exist(id93), - _event_data_id_exist(id94), - _event_data_id_exist(id95), - _event_data_id_exist(id96), - _event_data_id_exist(id97), - _event_data_id_exist(id98), - _event_data_id_exist(id99), - _event_data_id_exist(id100), - ) - ) - - def _select_unused_event_data_ids( session: Session, data_ids: set[int], using_sqlite: bool ) -> set[int]: @@ -654,11 +219,11 @@ def _select_unused_event_data_ids( groups = [iter(data_ids)] * 100 for data_ids_group in zip_longest(*groups, fillvalue=None): seen_ids |= { - state[0] - for state in session.execute( - _generate_find_data_id_lambda(*data_ids_group) + data_id[0] + for data_id in session.execute( + data_ids_exist_in_events(*data_ids_group) ).all() - if state[0] is not None + if data_id[0] is not None } to_remove = data_ids - seen_ids _LOGGER.debug("Selected %s shared event data to remove", len(to_remove)) diff --git a/homeassistant/components/recorder/queries.py b/homeassistant/components/recorder/queries.py new file mode 100644 index 00000000000..ff4662e27b1 --- /dev/null +++ b/homeassistant/components/recorder/queries.py @@ -0,0 +1,462 @@ +"""Queries for the recorder.""" +from __future__ import annotations + +from sqlalchemy import func, lambda_stmt, select, union_all +from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Select + +from .models import EventData, Events, StateAttributes, States + + +def find_shared_attributes_id( + data_hash: int, shared_attrs: str +) -> StatementLambdaElement: + """Find an attributes_id by hash and shared_attrs.""" + return lambda_stmt( + lambda: select(StateAttributes.attributes_id) + .filter(StateAttributes.hash == data_hash) + .filter(StateAttributes.shared_attrs == shared_attrs) + ) + + +def find_shared_data_id(attr_hash: int, shared_data: str) -> StatementLambdaElement: + """Find a data_id by hash and shared_data.""" + return lambda_stmt( + lambda: select(EventData.data_id) + .filter(EventData.hash == attr_hash) + .filter(EventData.shared_data == shared_data) + ) + + +def _state_attrs_exist(attr: int | None) -> Select: + """Check if a state attributes id exists in the states table.""" + return select(func.min(States.attributes_id)).where(States.attributes_id == attr) + + +def attributes_ids_exist_in_states( + attr1: int, + attr2: int | None, + attr3: int | None, + attr4: int | None, + attr5: int | None, + attr6: int | None, + attr7: int | None, + attr8: int | None, + attr9: int | None, + attr10: int | None, + attr11: int | None, + attr12: int | None, + attr13: int | None, + attr14: int | None, + attr15: int | None, + attr16: int | None, + attr17: int | None, + attr18: int | None, + attr19: int | None, + attr20: int | None, + attr21: int | None, + attr22: int | None, + attr23: int | None, + attr24: int | None, + attr25: int | None, + attr26: int | None, + attr27: int | None, + attr28: int | None, + attr29: int | None, + attr30: int | None, + attr31: int | None, + attr32: int | None, + attr33: int | None, + attr34: int | None, + attr35: int | None, + attr36: int | None, + attr37: int | None, + attr38: int | None, + attr39: int | None, + attr40: int | None, + attr41: int | None, + attr42: int | None, + attr43: int | None, + attr44: int | None, + attr45: int | None, + attr46: int | None, + attr47: int | None, + attr48: int | None, + attr49: int | None, + attr50: int | None, + attr51: int | None, + attr52: int | None, + attr53: int | None, + attr54: int | None, + attr55: int | None, + attr56: int | None, + attr57: int | None, + attr58: int | None, + attr59: int | None, + attr60: int | None, + attr61: int | None, + attr62: int | None, + attr63: int | None, + attr64: int | None, + attr65: int | None, + attr66: int | None, + attr67: int | None, + attr68: int | None, + attr69: int | None, + attr70: int | None, + attr71: int | None, + attr72: int | None, + attr73: int | None, + attr74: int | None, + attr75: int | None, + attr76: int | None, + attr77: int | None, + attr78: int | None, + attr79: int | None, + attr80: int | None, + attr81: int | None, + attr82: int | None, + attr83: int | None, + attr84: int | None, + attr85: int | None, + attr86: int | None, + attr87: int | None, + attr88: int | None, + attr89: int | None, + attr90: int | None, + attr91: int | None, + attr92: int | None, + attr93: int | None, + attr94: int | None, + attr95: int | None, + attr96: int | None, + attr97: int | None, + attr98: int | None, + attr99: int | None, + attr100: int | None, +) -> StatementLambdaElement: + """Generate the find attributes select only once. + + https://docs.sqlalchemy.org/en/14/core/connections.html#quick-guidelines-for-lambdas + """ + return lambda_stmt( + lambda: union_all( + _state_attrs_exist(attr1), + _state_attrs_exist(attr2), + _state_attrs_exist(attr3), + _state_attrs_exist(attr4), + _state_attrs_exist(attr5), + _state_attrs_exist(attr6), + _state_attrs_exist(attr7), + _state_attrs_exist(attr8), + _state_attrs_exist(attr9), + _state_attrs_exist(attr10), + _state_attrs_exist(attr11), + _state_attrs_exist(attr12), + _state_attrs_exist(attr13), + _state_attrs_exist(attr14), + _state_attrs_exist(attr15), + _state_attrs_exist(attr16), + _state_attrs_exist(attr17), + _state_attrs_exist(attr18), + _state_attrs_exist(attr19), + _state_attrs_exist(attr20), + _state_attrs_exist(attr21), + _state_attrs_exist(attr22), + _state_attrs_exist(attr23), + _state_attrs_exist(attr24), + _state_attrs_exist(attr25), + _state_attrs_exist(attr26), + _state_attrs_exist(attr27), + _state_attrs_exist(attr28), + _state_attrs_exist(attr29), + _state_attrs_exist(attr30), + _state_attrs_exist(attr31), + _state_attrs_exist(attr32), + _state_attrs_exist(attr33), + _state_attrs_exist(attr34), + _state_attrs_exist(attr35), + _state_attrs_exist(attr36), + _state_attrs_exist(attr37), + _state_attrs_exist(attr38), + _state_attrs_exist(attr39), + _state_attrs_exist(attr40), + _state_attrs_exist(attr41), + _state_attrs_exist(attr42), + _state_attrs_exist(attr43), + _state_attrs_exist(attr44), + _state_attrs_exist(attr45), + _state_attrs_exist(attr46), + _state_attrs_exist(attr47), + _state_attrs_exist(attr48), + _state_attrs_exist(attr49), + _state_attrs_exist(attr50), + _state_attrs_exist(attr51), + _state_attrs_exist(attr52), + _state_attrs_exist(attr53), + _state_attrs_exist(attr54), + _state_attrs_exist(attr55), + _state_attrs_exist(attr56), + _state_attrs_exist(attr57), + _state_attrs_exist(attr58), + _state_attrs_exist(attr59), + _state_attrs_exist(attr60), + _state_attrs_exist(attr61), + _state_attrs_exist(attr62), + _state_attrs_exist(attr63), + _state_attrs_exist(attr64), + _state_attrs_exist(attr65), + _state_attrs_exist(attr66), + _state_attrs_exist(attr67), + _state_attrs_exist(attr68), + _state_attrs_exist(attr69), + _state_attrs_exist(attr70), + _state_attrs_exist(attr71), + _state_attrs_exist(attr72), + _state_attrs_exist(attr73), + _state_attrs_exist(attr74), + _state_attrs_exist(attr75), + _state_attrs_exist(attr76), + _state_attrs_exist(attr77), + _state_attrs_exist(attr78), + _state_attrs_exist(attr79), + _state_attrs_exist(attr80), + _state_attrs_exist(attr81), + _state_attrs_exist(attr82), + _state_attrs_exist(attr83), + _state_attrs_exist(attr84), + _state_attrs_exist(attr85), + _state_attrs_exist(attr86), + _state_attrs_exist(attr87), + _state_attrs_exist(attr88), + _state_attrs_exist(attr89), + _state_attrs_exist(attr90), + _state_attrs_exist(attr91), + _state_attrs_exist(attr92), + _state_attrs_exist(attr93), + _state_attrs_exist(attr94), + _state_attrs_exist(attr95), + _state_attrs_exist(attr96), + _state_attrs_exist(attr97), + _state_attrs_exist(attr98), + _state_attrs_exist(attr99), + _state_attrs_exist(attr100), + ) + ) + + +def _event_data_id_exist(data_id: int | None) -> Select: + """Check if a event data id exists in the events table.""" + return select(func.min(Events.data_id)).where(Events.data_id == data_id) + + +def data_ids_exist_in_events( + id1: int, + id2: int | None, + id3: int | None, + id4: int | None, + id5: int | None, + id6: int | None, + id7: int | None, + id8: int | None, + id9: int | None, + id10: int | None, + id11: int | None, + id12: int | None, + id13: int | None, + id14: int | None, + id15: int | None, + id16: int | None, + id17: int | None, + id18: int | None, + id19: int | None, + id20: int | None, + id21: int | None, + id22: int | None, + id23: int | None, + id24: int | None, + id25: int | None, + id26: int | None, + id27: int | None, + id28: int | None, + id29: int | None, + id30: int | None, + id31: int | None, + id32: int | None, + id33: int | None, + id34: int | None, + id35: int | None, + id36: int | None, + id37: int | None, + id38: int | None, + id39: int | None, + id40: int | None, + id41: int | None, + id42: int | None, + id43: int | None, + id44: int | None, + id45: int | None, + id46: int | None, + id47: int | None, + id48: int | None, + id49: int | None, + id50: int | None, + id51: int | None, + id52: int | None, + id53: int | None, + id54: int | None, + id55: int | None, + id56: int | None, + id57: int | None, + id58: int | None, + id59: int | None, + id60: int | None, + id61: int | None, + id62: int | None, + id63: int | None, + id64: int | None, + id65: int | None, + id66: int | None, + id67: int | None, + id68: int | None, + id69: int | None, + id70: int | None, + id71: int | None, + id72: int | None, + id73: int | None, + id74: int | None, + id75: int | None, + id76: int | None, + id77: int | None, + id78: int | None, + id79: int | None, + id80: int | None, + id81: int | None, + id82: int | None, + id83: int | None, + id84: int | None, + id85: int | None, + id86: int | None, + id87: int | None, + id88: int | None, + id89: int | None, + id90: int | None, + id91: int | None, + id92: int | None, + id93: int | None, + id94: int | None, + id95: int | None, + id96: int | None, + id97: int | None, + id98: int | None, + id99: int | None, + id100: int | None, +) -> StatementLambdaElement: + """Generate the find event data select only once. + + https://docs.sqlalchemy.org/en/14/core/connections.html#quick-guidelines-for-lambdas + """ + return lambda_stmt( + lambda: union_all( + _event_data_id_exist(id1), + _event_data_id_exist(id2), + _event_data_id_exist(id3), + _event_data_id_exist(id4), + _event_data_id_exist(id5), + _event_data_id_exist(id6), + _event_data_id_exist(id7), + _event_data_id_exist(id8), + _event_data_id_exist(id9), + _event_data_id_exist(id10), + _event_data_id_exist(id11), + _event_data_id_exist(id12), + _event_data_id_exist(id13), + _event_data_id_exist(id14), + _event_data_id_exist(id15), + _event_data_id_exist(id16), + _event_data_id_exist(id17), + _event_data_id_exist(id18), + _event_data_id_exist(id19), + _event_data_id_exist(id20), + _event_data_id_exist(id21), + _event_data_id_exist(id22), + _event_data_id_exist(id23), + _event_data_id_exist(id24), + _event_data_id_exist(id25), + _event_data_id_exist(id26), + _event_data_id_exist(id27), + _event_data_id_exist(id28), + _event_data_id_exist(id29), + _event_data_id_exist(id30), + _event_data_id_exist(id31), + _event_data_id_exist(id32), + _event_data_id_exist(id33), + _event_data_id_exist(id34), + _event_data_id_exist(id35), + _event_data_id_exist(id36), + _event_data_id_exist(id37), + _event_data_id_exist(id38), + _event_data_id_exist(id39), + _event_data_id_exist(id40), + _event_data_id_exist(id41), + _event_data_id_exist(id42), + _event_data_id_exist(id43), + _event_data_id_exist(id44), + _event_data_id_exist(id45), + _event_data_id_exist(id46), + _event_data_id_exist(id47), + _event_data_id_exist(id48), + _event_data_id_exist(id49), + _event_data_id_exist(id50), + _event_data_id_exist(id51), + _event_data_id_exist(id52), + _event_data_id_exist(id53), + _event_data_id_exist(id54), + _event_data_id_exist(id55), + _event_data_id_exist(id56), + _event_data_id_exist(id57), + _event_data_id_exist(id58), + _event_data_id_exist(id59), + _event_data_id_exist(id60), + _event_data_id_exist(id61), + _event_data_id_exist(id62), + _event_data_id_exist(id63), + _event_data_id_exist(id64), + _event_data_id_exist(id65), + _event_data_id_exist(id66), + _event_data_id_exist(id67), + _event_data_id_exist(id68), + _event_data_id_exist(id69), + _event_data_id_exist(id70), + _event_data_id_exist(id71), + _event_data_id_exist(id72), + _event_data_id_exist(id73), + _event_data_id_exist(id74), + _event_data_id_exist(id75), + _event_data_id_exist(id76), + _event_data_id_exist(id77), + _event_data_id_exist(id78), + _event_data_id_exist(id79), + _event_data_id_exist(id80), + _event_data_id_exist(id81), + _event_data_id_exist(id82), + _event_data_id_exist(id83), + _event_data_id_exist(id84), + _event_data_id_exist(id85), + _event_data_id_exist(id86), + _event_data_id_exist(id87), + _event_data_id_exist(id88), + _event_data_id_exist(id89), + _event_data_id_exist(id90), + _event_data_id_exist(id91), + _event_data_id_exist(id92), + _event_data_id_exist(id93), + _event_data_id_exist(id94), + _event_data_id_exist(id95), + _event_data_id_exist(id96), + _event_data_id_exist(id97), + _event_data_id_exist(id98), + _event_data_id_exist(id99), + _event_data_id_exist(id100), + ) + ) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 423e8eb2f48..0b03283547e 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -11,7 +11,6 @@ from unittest.mock import Mock, patch import pytest from sqlalchemy.exc import DatabaseError, OperationalError, SQLAlchemyError -from sqlalchemy.ext import baked from homeassistant.components import recorder from homeassistant.components.recorder import ( @@ -82,7 +81,6 @@ def _default_recorder(hass): entity_filter=CONFIG_SCHEMA({DOMAIN: {}}), exclude_t=[], exclude_attributes_by_domain={}, - bakery=baked.bakery(), ) From ea456893f94c7dc88b0cc28f92dadf240fbb1fe7 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Tue, 3 May 2022 00:18:38 +0200 Subject: [PATCH 0116/3516] Review AndroidTV tests for media player entity (#71168) --- .coveragerc | 1 - tests/components/androidtv/patchers.py | 84 +- .../components/androidtv/test_config_flow.py | 12 +- .../components/androidtv/test_media_player.py | 743 ++++++++---------- 4 files changed, 372 insertions(+), 468 deletions(-) diff --git a/.coveragerc b/.coveragerc index b7433ecf58a..3464b0df1be 100644 --- a/.coveragerc +++ b/.coveragerc @@ -58,7 +58,6 @@ omit = homeassistant/components/amcrest/* homeassistant/components/ampio/* homeassistant/components/android_ip_webcam/* - homeassistant/components/androidtv/__init__.py homeassistant/components/androidtv/diagnostics.py homeassistant/components/anel_pwrctrl/switch.py homeassistant/components/anthemav/media_player.py diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 7cc14bbd7b5..31e9a9c82c3 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -1,8 +1,15 @@ """Define patches used for androidtv tests.""" -from unittest.mock import mock_open, patch +from unittest.mock import patch from androidtv.constants import CMD_DEVICE_PROPERTIES, CMD_MAC_ETH0, CMD_MAC_WLAN0 +from homeassistant.components.androidtv.const import ( + DEFAULT_ADB_SERVER_PORT, + DEVICE_ANDROIDTV, + DEVICE_FIRETV, +) + +ADB_SERVER_HOST = "127.0.0.1" KEY_PYTHON = "python" KEY_SERVER = "server" @@ -36,7 +43,7 @@ class AdbDeviceTcpAsyncFake: class ClientAsyncFakeSuccess: """A fake of the `ClientAsync` class when the connection and shell commands succeed.""" - def __init__(self, host="127.0.0.1", port=5037): + def __init__(self, host=ADB_SERVER_HOST, port=DEFAULT_ADB_SERVER_PORT): """Initialize a `ClientAsyncFakeSuccess` instance.""" self._devices = [] @@ -50,7 +57,7 @@ class ClientAsyncFakeSuccess: class ClientAsyncFakeFail: """A fake of the `ClientAsync` class when the connection and shell commands fail.""" - def __init__(self, host="127.0.0.1", port=5037): + def __init__(self, host=ADB_SERVER_HOST, port=DEFAULT_ADB_SERVER_PORT): """Initialize a `ClientAsyncFakeFail` instance.""" self._devices = [] @@ -143,17 +150,34 @@ def patch_shell(response=None, error=False, mac_eth=False): } -PATCH_ADB_DEVICE_TCP = patch( - "androidtv.adb_manager.adb_manager_async.AdbDeviceTcpAsync", AdbDeviceTcpAsyncFake -) -PATCH_ANDROIDTV_OPEN = patch( - "homeassistant.components.androidtv.media_player.open", mock_open() -) -PATCH_KEYGEN = patch("homeassistant.components.androidtv.keygen") -PATCH_SIGNER = patch( - "homeassistant.components.androidtv.ADBPythonSync.load_adbkey", - return_value="signer for testing", -) +def patch_androidtv_update( + state, + current_app, + running_apps, + device, + is_volume_muted, + volume_level, + hdmi_input, +): + """Patch the `AndroidTV.update()` method.""" + return { + DEVICE_ANDROIDTV: patch( + "androidtv.androidtv.androidtv_async.AndroidTVAsync.update", + return_value=( + state, + current_app, + running_apps, + device, + is_volume_muted, + volume_level, + hdmi_input, + ), + ), + DEVICE_FIRETV: patch( + "androidtv.firetv.firetv_async.FireTVAsync.update", + return_value=(state, current_app, running_apps, hdmi_input), + ), + } def isfile(filepath): @@ -161,32 +185,12 @@ def isfile(filepath): return filepath.endswith("adbkey") -def patch_firetv_update(state, current_app, running_apps, hdmi_input): - """Patch the `FireTV.update()` method.""" - return patch( - "androidtv.firetv.firetv_async.FireTVAsync.update", - return_value=(state, current_app, running_apps, hdmi_input), - ) - - -def patch_androidtv_update( - state, current_app, running_apps, device, is_volume_muted, volume_level, hdmi_input -): - """Patch the `AndroidTV.update()` method.""" - return patch( - "androidtv.androidtv.androidtv_async.AndroidTVAsync.update", - return_value=( - state, - current_app, - running_apps, - device, - is_volume_muted, - volume_level, - hdmi_input, - ), - ) - - +PATCH_SETUP_ENTRY = patch( + "homeassistant.components.androidtv.async_setup_entry", + return_value=True, +) +PATCH_ACCESS = patch("homeassistant.components.androidtv.os.access", return_value=True) +PATCH_ISFILE = patch("homeassistant.components.androidtv.os.path.isfile", isfile) PATCH_LAUNCH_APP = patch("androidtv.basetv.basetv_async.BaseTVAsync.launch_app") PATCH_STOP_APP = patch("androidtv.basetv.basetv_async.BaseTVAsync.stop_app") diff --git a/tests/components/androidtv/test_config_flow.py b/tests/components/androidtv/test_config_flow.py index aca308fd0e5..d5301f7ada3 100644 --- a/tests/components/androidtv/test_config_flow.py +++ b/tests/components/androidtv/test_config_flow.py @@ -36,7 +36,7 @@ from homeassistant.components.androidtv.const import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PORT -from .patchers import isfile +from .patchers import PATCH_ACCESS, PATCH_ISFILE, PATCH_SETUP_ENTRY from tests.common import MockConfigEntry @@ -66,16 +66,6 @@ CONFIG_ADB_SERVER = { CONNECT_METHOD = ( "homeassistant.components.androidtv.config_flow.async_connect_androidtv" ) -PATCH_ACCESS = patch( - "homeassistant.components.androidtv.config_flow.os.access", return_value=True -) -PATCH_ISFILE = patch( - "homeassistant.components.androidtv.config_flow.os.path.isfile", isfile -) -PATCH_SETUP_ENTRY = patch( - "homeassistant.components.androidtv.async_setup_entry", - return_value=True, -) class MockConfigDevice: diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index aa27ee8f564..73f8de55cc9 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -1,8 +1,7 @@ """The tests for the androidtv platform.""" import base64 -import copy import logging -from unittest.mock import patch +from unittest.mock import Mock, patch from androidtv.constants import APPS as ANDROIDTV_APPS, KEYS from androidtv.exceptions import LockNotAcquiredException @@ -14,6 +13,8 @@ from homeassistant.components.androidtv.const import ( CONF_ADBKEY, CONF_APPS, CONF_EXCLUDE_UNNAMED_APPS, + CONF_SCREENCAP, + CONF_STATE_DETECTION_RULES, CONF_TURN_OFF_COMMAND, CONF_TURN_ON_COMMAND, DEFAULT_ADB_SERVER_PORT, @@ -52,6 +53,7 @@ from homeassistant.components.media_player import ( SERVICE_VOLUME_UP, ) from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_COMMAND, ATTR_ENTITY_ID, @@ -72,16 +74,15 @@ from . import patchers from tests.common import MockConfigEntry -CONF_OPTIONS = "options" HOST = "127.0.0.1" ADB_PATCH_KEY = "patch_key" TEST_ENTITY_NAME = "entity_name" -PATCH_ACCESS = patch("homeassistant.components.androidtv.os.access", return_value=True) -PATCH_ISFILE = patch( - "homeassistant.components.androidtv.os.path.isfile", patchers.isfile -) +MSG_RECONNECT = { + patchers.KEY_PYTHON: f"ADB connection to {HOST}:{DEFAULT_PORT} successfully established", + patchers.KEY_SERVER: f"ADB connection to {HOST}:{DEFAULT_PORT} via ADB server {patchers.ADB_SERVER_HOST}:{DEFAULT_ADB_SERVER_PORT} successfully established", +} SHELL_RESPONSE_OFF = "" SHELL_RESPONSE_STANDBY = "1" @@ -107,6 +108,16 @@ CONFIG_ANDROIDTV_PYTHON_ADB_YAML = { }, } +# Android TV device with Python ADB implementation with custom adbkey +CONFIG_ANDROIDTV_PYTHON_ADB_KEY = { + ADB_PATCH_KEY: patchers.KEY_PYTHON, + TEST_ENTITY_NAME: CONFIG_ANDROIDTV_PYTHON_ADB[TEST_ENTITY_NAME], + DOMAIN: { + **CONFIG_ANDROIDTV_PYTHON_ADB[DOMAIN], + CONF_ADBKEY: "user_provided_adbkey", + }, +} + # Android TV device with ADB server CONFIG_ANDROIDTV_ADB_SERVER = { ADB_PATCH_KEY: patchers.KEY_SERVER, @@ -115,7 +126,7 @@ CONFIG_ANDROIDTV_ADB_SERVER = { CONF_HOST: HOST, CONF_PORT: DEFAULT_PORT, CONF_DEVICE_CLASS: DEVICE_ANDROIDTV, - CONF_ADB_SERVER_IP: HOST, + CONF_ADB_SERVER_IP: patchers.ADB_SERVER_HOST, CONF_ADB_SERVER_PORT: DEFAULT_ADB_SERVER_PORT, }, } @@ -139,11 +150,44 @@ CONFIG_FIRETV_ADB_SERVER = { CONF_HOST: HOST, CONF_PORT: DEFAULT_PORT, CONF_DEVICE_CLASS: DEVICE_FIRETV, - CONF_ADB_SERVER_IP: HOST, + CONF_ADB_SERVER_IP: patchers.ADB_SERVER_HOST, CONF_ADB_SERVER_PORT: DEFAULT_ADB_SERVER_PORT, }, } +CONFIG_ANDROIDTV_DEFAULT = CONFIG_ANDROIDTV_PYTHON_ADB +CONFIG_FIRETV_DEFAULT = CONFIG_FIRETV_PYTHON_ADB + + +@pytest.fixture(autouse=True) +def adb_device_tcp_fixture() -> None: + """Patch ADB Device TCP.""" + with patch( + "androidtv.adb_manager.adb_manager_async.AdbDeviceTcpAsync", + patchers.AdbDeviceTcpAsyncFake, + ): + yield + + +@pytest.fixture(autouse=True) +def load_adbkey_fixture() -> None: + """Patch load_adbkey.""" + with patch( + "homeassistant.components.androidtv.ADBPythonSync.load_adbkey", + return_value="signer for testing", + ): + yield + + +@pytest.fixture(autouse=True) +def keygen_fixture() -> None: + """Patch keygen.""" + with patch( + "homeassistant.components.androidtv.keygen", + return_value=Mock(), + ): + yield + def _setup(config): """Perform common setup tasks for the tests.""" @@ -153,7 +197,6 @@ def _setup(config): domain=DOMAIN, data=config[DOMAIN], unique_id="a1:b1:c1:d1:e1:f1", - options=config[DOMAIN].get(CONF_OPTIONS), ) return patch_key, entity_id, config_entry @@ -180,11 +223,9 @@ async def test_reconnect(hass, caplog, config): patch_key, entity_id, config_entry = _setup(config) config_entry.add_to_hass(hass) - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ - patch_key - ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -198,7 +239,7 @@ async def test_reconnect(hass, caplog, config): with patchers.patch_connect(False)[patch_key], patchers.patch_shell(error=True)[ patch_key - ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: + ]: for _ in range(5): await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) @@ -212,23 +253,13 @@ async def test_reconnect(hass, caplog, config): caplog.set_level(logging.DEBUG) with patchers.patch_connect(True)[patch_key], patchers.patch_shell( SHELL_RESPONSE_STANDBY - )[patch_key], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: + )[patch_key]: await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_STANDBY - - if patch_key == "python": - assert ( - "ADB connection to 127.0.0.1:5555 successfully established" - in caplog.record_tuples[2] - ) - else: - assert ( - "ADB connection to 127.0.0.1:5555 via ADB server 127.0.0.1:5037 successfully established" - in caplog.record_tuples[2] - ) + assert MSG_RECONNECT[patch_key] in caplog.record_tuples[2] @pytest.mark.parametrize( @@ -248,11 +279,9 @@ async def test_adb_shell_returns_none(hass, config): patch_key, entity_id, config_entry = _setup(config) config_entry.add_to_hass(hass) - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ - patch_key - ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -263,7 +292,7 @@ async def test_adb_shell_returns_none(hass, config): with patchers.patch_shell(None)[patch_key], patchers.patch_shell(error=True)[ patch_key - ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: + ]: await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None @@ -272,16 +301,12 @@ async def test_adb_shell_returns_none(hass, config): async def test_setup_with_adbkey(hass): """Test that setup succeeds when using an ADB key.""" - config = copy.deepcopy(CONFIG_ANDROIDTV_PYTHON_ADB) - config[DOMAIN][CONF_ADBKEY] = hass.config.path("user_provided_adbkey") - patch_key, entity_id, config_entry = _setup(config) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_PYTHON_ADB_KEY) config_entry.add_to_hass(hass) - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ - patch_key - ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER, PATCH_ISFILE, PATCH_ACCESS: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key], patchers.PATCH_ISFILE: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -292,30 +317,26 @@ async def test_setup_with_adbkey(hass): @pytest.mark.parametrize( - "config0", + "config", [ - CONFIG_ANDROIDTV_ADB_SERVER, - CONFIG_FIRETV_ADB_SERVER, + CONFIG_ANDROIDTV_DEFAULT, + CONFIG_FIRETV_DEFAULT, ], ) -async def test_sources(hass, config0): +async def test_sources(hass, config): """Test that sources (i.e., apps) are handled correctly for Android TV and Fire TV devices.""" - config = copy.deepcopy(config0) - config[DOMAIN].setdefault(CONF_OPTIONS, {}).update( - { - CONF_APPS: { - "com.app.test1": "TEST 1", - "com.app.test3": None, - "com.app.test4": SHELL_RESPONSE_OFF, - } - } - ) + conf_apps = { + "com.app.test1": "TEST 1", + "com.app.test3": None, + "com.app.test4": SHELL_RESPONSE_OFF, + } patch_key, entity_id, config_entry = _setup(config) config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry(config_entry, options={CONF_APPS: conf_apps}) - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -324,25 +345,17 @@ async def test_sources(hass, config0): assert state is not None assert state.state == STATE_OFF - if config[DOMAIN].get(CONF_DEVICE_CLASS) != DEVICE_FIRETV: - patch_update = patchers.patch_androidtv_update( - "playing", - "com.app.test1", - ["com.app.test1", "com.app.test2", "com.app.test3", "com.app.test4"], - "hdmi", - False, - 1, - "HW5", - ) - else: - patch_update = patchers.patch_firetv_update( - "playing", - "com.app.test1", - ["com.app.test1", "com.app.test2", "com.app.test3", "com.app.test4"], - "HW5", - ) + patch_update = patchers.patch_androidtv_update( + "playing", + "com.app.test1", + ["com.app.test1", "com.app.test2", "com.app.test3", "com.app.test4"], + "hdmi", + False, + 1, + "HW5", + ) - with patch_update: + with patch_update[config[DOMAIN][CONF_DEVICE_CLASS]]: await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None @@ -350,25 +363,17 @@ async def test_sources(hass, config0): assert state.attributes["source"] == "TEST 1" assert sorted(state.attributes["source_list"]) == ["TEST 1", "com.app.test2"] - if config[DOMAIN].get(CONF_DEVICE_CLASS) != DEVICE_FIRETV: - patch_update = patchers.patch_androidtv_update( - "playing", - "com.app.test2", - ["com.app.test2", "com.app.test1", "com.app.test3", "com.app.test4"], - "hdmi", - True, - 0, - "HW5", - ) - else: - patch_update = patchers.patch_firetv_update( - "playing", - "com.app.test2", - ["com.app.test2", "com.app.test1", "com.app.test3", "com.app.test4"], - "HW5", - ) + patch_update = patchers.patch_androidtv_update( + "playing", + "com.app.test2", + ["com.app.test2", "com.app.test1", "com.app.test3", "com.app.test4"], + "hdmi", + True, + 0, + "HW5", + ) - with patch_update: + with patch_update[config[DOMAIN][CONF_DEVICE_CLASS]]: await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None @@ -377,24 +382,29 @@ async def test_sources(hass, config0): assert sorted(state.attributes["source_list"]) == ["TEST 1", "com.app.test2"] -async def _test_exclude_sources(hass, config0, expected_sources): +@pytest.mark.parametrize( + ["config", "expected_sources"], + [ + (CONFIG_ANDROIDTV_DEFAULT, ["TEST 1"]), + (CONFIG_FIRETV_DEFAULT, ["TEST 1"]), + ], +) +async def test_exclude_sources(hass, config, expected_sources): """Test that sources (i.e., apps) are handled correctly when the `exclude_unnamed_apps` config parameter is provided.""" - config = copy.deepcopy(config0) - config[DOMAIN].setdefault(CONF_OPTIONS, {}).update( - { - CONF_APPS: { - "com.app.test1": "TEST 1", - "com.app.test3": None, - "com.app.test4": SHELL_RESPONSE_OFF, - } - } - ) + conf_apps = { + "com.app.test1": "TEST 1", + "com.app.test3": None, + "com.app.test4": SHELL_RESPONSE_OFF, + } patch_key, entity_id, config_entry = _setup(config) config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + config_entry, options={CONF_EXCLUDE_UNNAMED_APPS: True, CONF_APPS: conf_apps} + ) - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -403,37 +413,23 @@ async def _test_exclude_sources(hass, config0, expected_sources): assert state is not None assert state.state == STATE_OFF - if config[DOMAIN].get(CONF_DEVICE_CLASS) != DEVICE_FIRETV: - patch_update = patchers.patch_androidtv_update( - "playing", + patch_update = patchers.patch_androidtv_update( + "playing", + "com.app.test1", + [ "com.app.test1", - [ - "com.app.test1", - "com.app.test2", - "com.app.test3", - "com.app.test4", - "com.app.test5", - ], - "hdmi", - False, - 1, - "HW5", - ) - else: - patch_update = patchers.patch_firetv_update( - "playing", - "com.app.test1", - [ - "com.app.test1", - "com.app.test2", - "com.app.test3", - "com.app.test4", - "com.app.test5", - ], - "HW5", - ) + "com.app.test2", + "com.app.test3", + "com.app.test4", + "com.app.test5", + ], + "hdmi", + False, + 1, + "HW5", + ) - with patch_update: + with patch_update[config[DOMAIN][CONF_DEVICE_CLASS]]: await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) assert state is not None @@ -441,41 +437,18 @@ async def _test_exclude_sources(hass, config0, expected_sources): assert state.attributes["source"] == "TEST 1" assert sorted(state.attributes["source_list"]) == expected_sources - return True - -async def test_androidtv_exclude_sources(hass): - """Test that sources (i.e., apps) are handled correctly for Android TV devices when the `exclude_unnamed_apps` config parameter is provided as true.""" - config = copy.deepcopy(CONFIG_ANDROIDTV_ADB_SERVER) - config[DOMAIN][CONF_OPTIONS] = {CONF_EXCLUDE_UNNAMED_APPS: True} - assert await _test_exclude_sources(hass, config, ["TEST 1"]) - - -async def test_firetv_exclude_sources(hass): - """Test that sources (i.e., apps) are handled correctly for Fire TV devices when the `exclude_unnamed_apps` config parameter is provided as true.""" - config = copy.deepcopy(CONFIG_FIRETV_ADB_SERVER) - config[DOMAIN][CONF_OPTIONS] = {CONF_EXCLUDE_UNNAMED_APPS: True} - assert await _test_exclude_sources(hass, config, ["TEST 1"]) - - -async def _test_select_source(hass, config0, source, expected_arg, method_patch): +async def _test_select_source( + hass, config, conf_apps, source, expected_arg, method_patch +): """Test that the methods for launching and stopping apps are called correctly when selecting a source.""" - config = copy.deepcopy(config0) - config[DOMAIN].setdefault(CONF_OPTIONS, {}).update( - { - CONF_APPS: { - "com.app.test1": "TEST 1", - "com.app.test3": None, - "com.youtube.test": "YouTube", - } - } - ) patch_key, entity_id, config_entry = _setup(config) config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry(config_entry, options={CONF_APPS: conf_apps}) - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -484,213 +457,87 @@ async def _test_select_source(hass, config0, source, expected_arg, method_patch) assert state is not None assert state.state == STATE_OFF - with method_patch as method_patch_: + with method_patch as method_patch_used: await hass.services.async_call( MP_DOMAIN, SERVICE_SELECT_SOURCE, {ATTR_ENTITY_ID: entity_id, ATTR_INPUT_SOURCE: source}, blocking=True, ) - method_patch_.assert_called_with(expected_arg) - - return True + method_patch_used.assert_called_with(expected_arg) -async def test_androidtv_select_source_launch_app_id(hass): - """Test that an app can be launched using its app ID.""" - assert await _test_select_source( - hass, - CONFIG_ANDROIDTV_ADB_SERVER, - "com.app.test1", - "com.app.test1", - patchers.PATCH_LAUNCH_APP, - ) - - -async def test_androidtv_select_source_launch_app_name(hass): - """Test that an app can be launched using its friendly name.""" - assert await _test_select_source( - hass, - CONFIG_ANDROIDTV_ADB_SERVER, - "TEST 1", - "com.app.test1", - patchers.PATCH_LAUNCH_APP, - ) - - -async def test_androidtv_select_source_launch_app_id_no_name(hass): - """Test that an app can be launched using its app ID when it has no friendly name.""" - assert await _test_select_source( - hass, - CONFIG_ANDROIDTV_ADB_SERVER, - "com.app.test2", - "com.app.test2", - patchers.PATCH_LAUNCH_APP, - ) - - -async def test_androidtv_select_source_launch_app_hidden(hass): - """Test that an app can be launched using its app ID when it is hidden from the sources list.""" - assert await _test_select_source( - hass, - CONFIG_ANDROIDTV_ADB_SERVER, - "com.app.test3", - "com.app.test3", - patchers.PATCH_LAUNCH_APP, +@pytest.mark.parametrize( + ["source", "expected_arg", "method_patch"], + [ + ("com.app.test1", "com.app.test1", patchers.PATCH_LAUNCH_APP), + ("TEST 1", "com.app.test1", patchers.PATCH_LAUNCH_APP), + ("com.app.test2", "com.app.test2", patchers.PATCH_LAUNCH_APP), + ("com.app.test3", "com.app.test3", patchers.PATCH_LAUNCH_APP), + ("!com.app.test1", "com.app.test1", patchers.PATCH_STOP_APP), + ("!TEST 1", "com.app.test1", patchers.PATCH_STOP_APP), + ("!com.app.test2", "com.app.test2", patchers.PATCH_STOP_APP), + ("!com.app.test3", "com.app.test3", patchers.PATCH_STOP_APP), + ], +) +async def test_select_source_androidtv(hass, source, expected_arg, method_patch): + """Test that an app can be launched for AndroidTV.""" + conf_apps = { + "com.app.test1": "TEST 1", + "com.app.test3": None, + } + await _test_select_source( + hass, CONFIG_ANDROIDTV_DEFAULT, conf_apps, source, expected_arg, method_patch ) async def test_androidtv_select_source_overridden_app_name(hass): """Test that when an app name is overridden via the `apps` configuration parameter, the app is launched correctly.""" # Evidence that the default YouTube app ID will be overridden + conf_apps = { + "com.youtube.test": "YouTube", + } assert "YouTube" in ANDROIDTV_APPS.values() assert "com.youtube.test" not in ANDROIDTV_APPS - assert await _test_select_source( + await _test_select_source( hass, - CONFIG_ANDROIDTV_ADB_SERVER, + CONFIG_ANDROIDTV_PYTHON_ADB, + conf_apps, "YouTube", "com.youtube.test", patchers.PATCH_LAUNCH_APP, ) -async def test_androidtv_select_source_stop_app_id(hass): - """Test that an app can be stopped using its app ID.""" - assert await _test_select_source( - hass, - CONFIG_ANDROIDTV_ADB_SERVER, - "!com.app.test1", - "com.app.test1", - patchers.PATCH_STOP_APP, - ) - - -async def test_androidtv_select_source_stop_app_name(hass): - """Test that an app can be stopped using its friendly name.""" - assert await _test_select_source( - hass, - CONFIG_ANDROIDTV_ADB_SERVER, - "!TEST 1", - "com.app.test1", - patchers.PATCH_STOP_APP, - ) - - -async def test_androidtv_select_source_stop_app_id_no_name(hass): - """Test that an app can be stopped using its app ID when it has no friendly name.""" - assert await _test_select_source( - hass, - CONFIG_ANDROIDTV_ADB_SERVER, - "!com.app.test2", - "com.app.test2", - patchers.PATCH_STOP_APP, - ) - - -async def test_androidtv_select_source_stop_app_hidden(hass): - """Test that an app can be stopped using its app ID when it is hidden from the sources list.""" - assert await _test_select_source( - hass, - CONFIG_ANDROIDTV_ADB_SERVER, - "!com.app.test3", - "com.app.test3", - patchers.PATCH_STOP_APP, - ) - - -async def test_firetv_select_source_launch_app_id(hass): - """Test that an app can be launched using its app ID.""" - assert await _test_select_source( - hass, - CONFIG_FIRETV_ADB_SERVER, - "com.app.test1", - "com.app.test1", - patchers.PATCH_LAUNCH_APP, - ) - - -async def test_firetv_select_source_launch_app_name(hass): - """Test that an app can be launched using its friendly name.""" - assert await _test_select_source( - hass, - CONFIG_FIRETV_ADB_SERVER, - "TEST 1", - "com.app.test1", - patchers.PATCH_LAUNCH_APP, - ) - - -async def test_firetv_select_source_launch_app_id_no_name(hass): - """Test that an app can be launched using its app ID when it has no friendly name.""" - assert await _test_select_source( - hass, - CONFIG_FIRETV_ADB_SERVER, - "com.app.test2", - "com.app.test2", - patchers.PATCH_LAUNCH_APP, - ) - - -async def test_firetv_select_source_launch_app_hidden(hass): - """Test that an app can be launched using its app ID when it is hidden from the sources list.""" - assert await _test_select_source( - hass, - CONFIG_FIRETV_ADB_SERVER, - "com.app.test3", - "com.app.test3", - patchers.PATCH_LAUNCH_APP, - ) - - -async def test_firetv_select_source_stop_app_id(hass): - """Test that an app can be stopped using its app ID.""" - assert await _test_select_source( - hass, - CONFIG_FIRETV_ADB_SERVER, - "!com.app.test1", - "com.app.test1", - patchers.PATCH_STOP_APP, - ) - - -async def test_firetv_select_source_stop_app_name(hass): - """Test that an app can be stopped using its friendly name.""" - assert await _test_select_source( - hass, - CONFIG_FIRETV_ADB_SERVER, - "!TEST 1", - "com.app.test1", - patchers.PATCH_STOP_APP, - ) - - -async def test_firetv_select_source_stop_app_id_no_name(hass): - """Test that an app can be stopped using its app ID when it has no friendly name.""" - assert await _test_select_source( - hass, - CONFIG_FIRETV_ADB_SERVER, - "!com.app.test2", - "com.app.test2", - patchers.PATCH_STOP_APP, - ) - - -async def test_firetv_select_source_stop_hidden(hass): - """Test that an app can be stopped using its app ID when it is hidden from the sources list.""" - assert await _test_select_source( - hass, - CONFIG_FIRETV_ADB_SERVER, - "!com.app.test3", - "com.app.test3", - patchers.PATCH_STOP_APP, +@pytest.mark.parametrize( + ["source", "expected_arg", "method_patch"], + [ + ("com.app.test1", "com.app.test1", patchers.PATCH_LAUNCH_APP), + ("TEST 1", "com.app.test1", patchers.PATCH_LAUNCH_APP), + ("com.app.test2", "com.app.test2", patchers.PATCH_LAUNCH_APP), + ("com.app.test3", "com.app.test3", patchers.PATCH_LAUNCH_APP), + ("!com.app.test1", "com.app.test1", patchers.PATCH_STOP_APP), + ("!TEST 1", "com.app.test1", patchers.PATCH_STOP_APP), + ("!com.app.test2", "com.app.test2", patchers.PATCH_STOP_APP), + ("!com.app.test3", "com.app.test3", patchers.PATCH_STOP_APP), + ], +) +async def test_select_source_firetv(hass, source, expected_arg, method_patch): + """Test that an app can be launched for FireTV.""" + conf_apps = { + "com.app.test1": "TEST 1", + "com.app.test3": None, + } + await _test_select_source( + hass, CONFIG_FIRETV_DEFAULT, conf_apps, source, expected_arg, method_patch ) @pytest.mark.parametrize( "config", [ - CONFIG_ANDROIDTV_PYTHON_ADB, - CONFIG_FIRETV_PYTHON_ADB, + CONFIG_ANDROIDTV_DEFAULT, + CONFIG_FIRETV_DEFAULT, ], ) async def test_setup_fail(hass, config): @@ -698,11 +545,9 @@ async def test_setup_fail(hass, config): patch_key, entity_id, config_entry = _setup(config) config_entry.add_to_hass(hass) - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(False)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ - patch_key - ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: + with patchers.patch_connect(False)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) is False await hass.async_block_till_done() @@ -713,14 +558,14 @@ async def test_setup_fail(hass, config): async def test_adb_command(hass): """Test sending a command via the `androidtv.adb_command` service.""" - patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) command = "test command" response = "test response" - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -742,14 +587,14 @@ async def test_adb_command(hass): async def test_adb_command_unicode_decode_error(hass): """Test sending a command via the `androidtv.adb_command` service that raises a UnicodeDecodeError exception.""" - patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) command = "test command" response = b"test response" - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -771,14 +616,14 @@ async def test_adb_command_unicode_decode_error(hass): async def test_adb_command_key(hass): """Test sending a key command via the `androidtv.adb_command` service.""" - patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) command = "HOME" response = None - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -800,14 +645,14 @@ async def test_adb_command_key(hass): async def test_adb_command_get_properties(hass): """Test sending the "GET_PROPERTIES" command via the `androidtv.adb_command` service.""" - patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) command = "GET_PROPERTIES" response = {"test key": "test value"} - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -830,13 +675,13 @@ async def test_adb_command_get_properties(hass): async def test_learn_sendevent(hass): """Test the `androidtv.learn_sendevent` service.""" - patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) response = "sendevent 1 2 3 4" - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -859,12 +704,12 @@ async def test_learn_sendevent(hass): async def test_update_lock_not_acquired(hass): """Test that the state does not get updated when a `LockNotAcquiredException` is raised.""" - patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -892,14 +737,14 @@ async def test_update_lock_not_acquired(hass): async def test_download(hass): """Test the `androidtv.download` service.""" - patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) device_path = "device/path" local_path = "local/path" - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -938,14 +783,14 @@ async def test_download(hass): async def test_upload(hass): """Test the `androidtv.upload` service.""" - patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) device_path = "device/path" local_path = "local/path" - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -984,12 +829,12 @@ async def test_upload(hass): async def test_androidtv_volume_set(hass): """Test setting the volume for an Android TV device.""" - patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -1011,12 +856,12 @@ async def test_get_image(hass, hass_ws_client): This is based on `test_get_image` in tests/components/media_player/test_init.py. """ - patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -1042,7 +887,7 @@ async def test_get_image(hass, hass_ws_client): with patch( "androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap", - side_effect=RuntimeError, + side_effect=ConnectionResetError, ): await client.send_json( {"id": 6, "type": "media_player_thumbnail", "entity_id": entity_id} @@ -1056,6 +901,39 @@ async def test_get_image(hass, hass_ws_client): assert state.state == STATE_UNAVAILABLE +async def test_get_image_disabled(hass, hass_ws_client): + """Test taking a screen capture with screencap option disabled. + + This is based on `test_get_image` in tests/components/media_player/test_init.py. + """ + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) + config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + config_entry, options={CONF_SCREENCAP: False} + ) + + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + with patchers.patch_shell("11")[patch_key]: + await async_update_entity(hass, entity_id) + + client = await hass_ws_client(hass) + + with patch( + "androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap", return_value=b"image" + ) as screen_cap: + await client.send_json( + {"id": 5, "type": "media_player_thumbnail", "entity_id": entity_id} + ) + + await client.receive_json() + assert not screen_cap.called + + async def _test_service( hass, entity_id, @@ -1088,10 +966,10 @@ async def _test_service( async def test_services_androidtv(hass): """Test media player services for an Android TV device.""" - patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]: + with patchers.patch_connect(True)[patch_key]: with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -1129,15 +1007,17 @@ async def test_services_androidtv(hass): async def test_services_firetv(hass): """Test media player services for a Fire TV device.""" - config = copy.deepcopy(CONFIG_FIRETV_ADB_SERVER) - config[DOMAIN][CONF_OPTIONS] = { - CONF_TURN_OFF_COMMAND: "test off", - CONF_TURN_ON_COMMAND: "test on", - } - patch_key, entity_id, config_entry = _setup(config) + patch_key, entity_id, config_entry = _setup(CONFIG_FIRETV_DEFAULT) config_entry.add_to_hass(hass) + hass.config_entries.async_update_entry( + config_entry, + options={ + CONF_TURN_OFF_COMMAND: "test off", + CONF_TURN_ON_COMMAND: "test on", + }, + ) - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]: + with patchers.patch_connect(True)[patch_key]: with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -1150,10 +1030,10 @@ async def test_services_firetv(hass): async def test_volume_mute(hass): """Test the volume mute service.""" - patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[patch_key]: + with patchers.patch_connect(True)[patch_key]: with patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -1193,18 +1073,16 @@ async def test_volume_mute(hass): async def test_connection_closed_on_ha_stop(hass): """Test that the ADB socket connection is closed when HA stops.""" - patch_key, _, config_entry = _setup(CONFIG_ANDROIDTV_ADB_SERVER) + patch_key, _, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[patch_key]: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - with patch( - "androidtv.androidtv.androidtv_async.AndroidTVAsync.adb_close" - ) as adb_close: + with patch("androidtv.basetv.basetv_async.BaseTVAsync.adb_close") as adb_close: hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() assert adb_close.called @@ -1215,14 +1093,12 @@ async def test_exception(hass): HA will attempt to reconnect on the next update. """ - patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_PYTHON_ADB) + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) - with patchers.PATCH_ADB_DEVICE_TCP, patchers.patch_connect(True)[ - patch_key - ], patchers.patch_shell(SHELL_RESPONSE_OFF)[ - patch_key - ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -1243,3 +1119,38 @@ async def test_exception(hass): state = hass.states.get(entity_id) assert state is not None assert state.state == STATE_OFF + + +async def test_options_reload(hass): + """Test changing an option that will cause integration reload.""" + patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) + config_entry.add_to_hass(hass) + + with patchers.patch_connect(True)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF + )[patch_key]: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + await async_update_entity(hass, entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF + + with patchers.PATCH_SETUP_ENTRY as setup_entry_call: + # change an option that not require integration reload + hass.config_entries.async_update_entry( + config_entry, options={CONF_SCREENCAP: False} + ) + await hass.async_block_till_done() + + assert not setup_entry_call.called + + # change an option that require integration reload + hass.config_entries.async_update_entry( + config_entry, options={CONF_STATE_DETECTION_RULES: {}} + ) + await hass.async_block_till_done() + + assert setup_entry_call.called + assert config_entry.state is ConfigEntryState.LOADED From 75026f9fedcb7c43d166fd4025ec4649179ead8c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 May 2022 17:22:53 -0500 Subject: [PATCH 0117/3516] Separate recorder logic for state_changed and non-state_changed events (#71204) --- homeassistant/components/recorder/__init__.py | 67 ++++++++++--------- tests/components/recorder/test_init.py | 60 +++++++++++++++++ 2 files changed, 97 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 35a664cac65..1d81495c3a5 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -1123,7 +1123,10 @@ class Recorder(threading.Thread): def _process_one_event(self, event: Event) -> None: if not self.enabled: return - self._process_event_into_session(event) + if event.event_type == EVENT_STATE_CHANGED: + self._process_state_changed_event_into_session(event) + else: + self._process_non_state_changed_event_into_session(event) # Commit if the commit interval is zero if not self.commit_interval: self._commit_event_session_or_retry() @@ -1164,40 +1167,44 @@ class Recorder(threading.Thread): return cast(int, data_id[0]) return None - def _process_event_into_session(self, event: Event) -> None: + def _process_non_state_changed_event_into_session(self, event: Event) -> None: + """Process any event into the session except state changed.""" assert self.event_session is not None dbevent = Events.from_event(event) - - if event.event_type != EVENT_STATE_CHANGED and event.data: - try: - shared_data = EventData.shared_data_from_event(event) - except (TypeError, ValueError): - _LOGGER.warning("Event is not JSON serializable: %s", event) - return - - # Matching attributes found in the pending commit - if pending_event_data := self._pending_event_data.get(shared_data): - dbevent.event_data_rel = pending_event_data - # Matching attributes id found in the cache - elif data_id := self._event_data_ids.get(shared_data): - dbevent.data_id = data_id - else: - data_hash = EventData.hash_shared_data(shared_data) - # Matching attributes found in the database - if data_id := self._find_shared_data_in_db(data_hash, shared_data): - self._event_data_ids[shared_data] = dbevent.data_id = data_id - # No matching attributes found, save them in the DB - else: - dbevent_data = EventData(shared_data=shared_data, hash=data_hash) - dbevent.event_data_rel = self._pending_event_data[ - shared_data - ] = dbevent_data - self.event_session.add(dbevent_data) - - if event.event_type != EVENT_STATE_CHANGED: + if not event.data: self.event_session.add(dbevent) return + try: + shared_data = EventData.shared_data_from_event(event) + except (TypeError, ValueError) as ex: + _LOGGER.warning("Event is not JSON serializable: %s: %s", event, ex) + return + + # Matching attributes found in the pending commit + if pending_event_data := self._pending_event_data.get(shared_data): + dbevent.event_data_rel = pending_event_data + # Matching attributes id found in the cache + elif data_id := self._event_data_ids.get(shared_data): + dbevent.data_id = data_id + else: + data_hash = EventData.hash_shared_data(shared_data) + # Matching attributes found in the database + if data_id := self._find_shared_data_in_db(data_hash, shared_data): + self._event_data_ids[shared_data] = dbevent.data_id = data_id + # No matching attributes found, save them in the DB + else: + dbevent_data = EventData(shared_data=shared_data, hash=data_hash) + dbevent.event_data_rel = self._pending_event_data[ + shared_data + ] = dbevent_data + self.event_session.add(dbevent_data) + + self.event_session.add(dbevent) + + def _process_state_changed_event_into_session(self, event: Event) -> None: + """Process a state_changed event into the session.""" + assert self.event_session is not None try: dbstate = States.from_event(event) shared_attrs = StateAttributes.shared_attrs_from_event( diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 0b03283547e..6d4f2e1106a 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1456,3 +1456,63 @@ async def test_database_connection_keep_alive_disabled_on_sqlite( ) await async_wait_recording_done(hass) assert "Sending keepalive" not in caplog.text + + +def test_deduplication_event_data_inside_commit_interval(hass_recorder, caplog): + """Test deduplication of event data inside the commit interval.""" + hass = hass_recorder() + + for _ in range(10): + hass.bus.fire("this_event", {"de": "dupe"}) + wait_recording_done(hass) + for _ in range(10): + hass.bus.fire("this_event", {"de": "dupe"}) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + events = list( + session.query(Events) + .filter(Events.event_type == "this_event") + .outerjoin(EventData, (Events.data_id == EventData.data_id)) + ) + assert len(events) == 20 + first_data_id = events[0].data_id + assert all(event.data_id == first_data_id for event in events) + + +# Patch STATE_ATTRIBUTES_ID_CACHE_SIZE since otherwise +# the CI can fail because the test takes too long to run +@patch("homeassistant.components.recorder.STATE_ATTRIBUTES_ID_CACHE_SIZE", 5) +def test_deduplication_state_attributes_inside_commit_interval(hass_recorder, caplog): + """Test deduplication of state attributes inside the commit interval.""" + hass = hass_recorder() + + entity_id = "test.recorder" + attributes = {"test_attr": 5, "test_attr_10": "nice"} + + hass.states.set(entity_id, "on", attributes) + hass.states.set(entity_id, "off", attributes) + + # Now exaust the cache to ensure we go back to the db + for attr_id in range(5): + hass.states.set(entity_id, "on", {"test_attr": attr_id}) + hass.states.set(entity_id, "off", {"test_attr": attr_id}) + + wait_recording_done(hass) + for _ in range(5): + hass.states.set(entity_id, "on", attributes) + hass.states.set(entity_id, "off", attributes) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + states = list( + session.query(States) + .filter(States.entity_id == entity_id) + .outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) + ) + assert len(states) == 22 + first_attributes_id = states[0].attributes_id + last_attributes_id = states[-1].attributes_id + assert first_attributes_id == last_attributes_id From fbc048f07b64a4158763ae14ee1989b8231ee73c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 May 2022 15:39:05 -0700 Subject: [PATCH 0118/3516] Bump frontend to 20220502.0 (#71221) --- 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 e57f06827a5..18e2fc6ed8f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220429.0"], + "requirements": ["home-assistant-frontend==20220502.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6dd904c858f..f328394b9c1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220429.0 +home-assistant-frontend==20220502.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.1 diff --git a/requirements_all.txt b/requirements_all.txt index 16177793510..82b69db90f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220429.0 +home-assistant-frontend==20220502.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fde9af6058b..a107e19bdf6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -580,7 +580,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220429.0 +home-assistant-frontend==20220502.0 # homeassistant.components.home_connect homeconnect==0.7.0 From 0ec29711e1204dc8e431937fa198dbeeaef60b75 Mon Sep 17 00:00:00 2001 From: stegm Date: Mon, 2 May 2022 11:42:18 +0200 Subject: [PATCH 0119/3516] Handle missing kostal plenticore battery option (#65237) Co-authored-by: Martin Hjelmare --- .../components/kostal_plenticore/select.py | 45 +++++++++----- .../components/kostal_plenticore/conftest.py | 61 ++++++++----------- .../kostal_plenticore/test_diagnostics.py | 31 +++++++++- .../kostal_plenticore/test_select.py | 45 ++++++++++++++ 4 files changed, 130 insertions(+), 52 deletions(-) create mode 100644 tests/components/kostal_plenticore/test_select.py diff --git a/homeassistant/components/kostal_plenticore/select.py b/homeassistant/components/kostal_plenticore/select.py index 1844f0894a5..7ac06f2ebef 100644 --- a/homeassistant/components/kostal_plenticore/select.py +++ b/homeassistant/components/kostal_plenticore/select.py @@ -24,6 +24,8 @@ async def async_setup_entry( ) -> None: """Add kostal plenticore Select widget.""" plenticore: Plenticore = hass.data[DOMAIN][entry.entry_id] + + available_settings_data = await plenticore.client.get_settings() select_data_update_coordinator = SelectDataUpdateCoordinator( hass, _LOGGER, @@ -32,23 +34,34 @@ async def async_setup_entry( plenticore, ) - async_add_entities( - PlenticoreDataSelect( - select_data_update_coordinator, - entry_id=entry.entry_id, - platform_name=entry.title, - device_class="kostal_plenticore__battery", - module_id=select.module_id, - data_id=select.data_id, - name=select.name, - current_option="None", - options=select.options, - is_on=select.is_on, - device_info=plenticore.device_info, - unique_id=f"{entry.entry_id}_{select.module_id}", + entities = [] + for select in SELECT_SETTINGS_DATA: + if select.module_id not in available_settings_data: + continue + needed_data_ids = {data_id for data_id in select.options if data_id != "None"} + available_data_ids = { + setting.id for setting in available_settings_data[select.module_id] + } + if not needed_data_ids <= available_data_ids: + continue + entities.append( + PlenticoreDataSelect( + select_data_update_coordinator, + entry_id=entry.entry_id, + platform_name=entry.title, + device_class="kostal_plenticore__battery", + module_id=select.module_id, + data_id=select.data_id, + name=select.name, + current_option="None", + options=select.options, + is_on=select.is_on, + device_info=plenticore.device_info, + unique_id=f"{entry.entry_id}_{select.module_id}", + ) ) - for select in SELECT_SETTINGS_DATA - ) + + async_add_entities(entities) class PlenticoreDataSelect(CoordinatorEntity, SelectEntity, ABC): diff --git a/tests/components/kostal_plenticore/conftest.py b/tests/components/kostal_plenticore/conftest.py index c3ed1b45592..4e789a34198 100644 --- a/tests/components/kostal_plenticore/conftest.py +++ b/tests/components/kostal_plenticore/conftest.py @@ -4,9 +4,10 @@ from __future__ import annotations from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch -from kostal.plenticore import MeData, SettingsData, VersionData +from kostal.plenticore import MeData, VersionData import pytest +from homeassistant.components.kostal_plenticore.helper import Plenticore from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo @@ -14,10 +15,19 @@ from tests.common import MockConfigEntry @pytest.fixture -async def init_integration( - hass: HomeAssistant, -) -> Generator[None, MockConfigEntry, None]: - """Set up Kostal Plenticore integration for testing.""" +def mock_config_entry() -> MockConfigEntry: + """Return a mocked ConfigEntry for testing.""" + return MockConfigEntry( + entry_id="2ab8dd92a62787ddfe213a67e09406bd", + title="scb", + domain="kostal_plenticore", + data={"host": "192.168.1.2", "password": "SecretPassword"}, + ) + + +@pytest.fixture +def mock_plenticore() -> Generator[Plenticore, None, None]: + """Set up a Plenticore mock with some default values.""" with patch( "homeassistant.components.kostal_plenticore.Plenticore", autospec=True ) as mock_api_class: @@ -60,37 +70,20 @@ async def init_integration( ) plenticore.client.get_process_data = AsyncMock() - plenticore.client.get_process_data.return_value = { - "devices:local": ["HomeGrid_P", "HomePv_P"] - } - plenticore.client.get_settings = AsyncMock() - plenticore.client.get_settings.return_value = { - "devices:local": [ - SettingsData( - { - "id": "Battery:MinSoc", - "unit": "%", - "default": "None", - "min": 5, - "max": 100, - "type": "byte", - "access": "readwrite", - } - ) - ] - } - mock_config_entry = MockConfigEntry( - entry_id="2ab8dd92a62787ddfe213a67e09406bd", - title="scb", - domain="kostal_plenticore", - data={"host": "192.168.1.2", "password": "SecretPassword"}, - ) + yield plenticore - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> MockConfigEntry: + """Set up Kostal Plenticore integration for testing.""" - yield mock_config_entry + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/kostal_plenticore/test_diagnostics.py b/tests/components/kostal_plenticore/test_diagnostics.py index 56af8bafe06..1f249aa3798 100644 --- a/tests/components/kostal_plenticore/test_diagnostics.py +++ b/tests/components/kostal_plenticore/test_diagnostics.py @@ -1,7 +1,9 @@ """Test Kostal Plenticore diagnostics.""" from aiohttp import ClientSession +from kostal.plenticore import SettingsData from homeassistant.components.diagnostics import REDACTED +from homeassistant.components.kostal_plenticore.helper import Plenticore from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -9,9 +11,34 @@ from tests.components.diagnostics import get_diagnostics_for_config_entry async def test_entry_diagnostics( - hass: HomeAssistant, hass_client: ClientSession, init_integration: MockConfigEntry -): + hass: HomeAssistant, + hass_client: ClientSession, + mock_plenticore: Plenticore, + init_integration: MockConfigEntry, +) -> None: """Test config entry diagnostics.""" + + # set some test process and settings data for the diagnostics output + mock_plenticore.client.get_process_data.return_value = { + "devices:local": ["HomeGrid_P", "HomePv_P"] + } + + mock_plenticore.client.get_settings.return_value = { + "devices:local": [ + SettingsData( + { + "id": "Battery:MinSoc", + "unit": "%", + "default": "None", + "min": 5, + "max": 100, + "type": "byte", + "access": "readwrite", + } + ) + ] + } + assert await get_diagnostics_for_config_entry( hass, hass_client, init_integration ) == { diff --git a/tests/components/kostal_plenticore/test_select.py b/tests/components/kostal_plenticore/test_select.py new file mode 100644 index 00000000000..6023b015483 --- /dev/null +++ b/tests/components/kostal_plenticore/test_select.py @@ -0,0 +1,45 @@ +"""Test the Kostal Plenticore Solar Inverter select platform.""" +from kostal.plenticore import SettingsData + +from homeassistant.components.kostal_plenticore.helper import Plenticore +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry + +from tests.common import MockConfigEntry + + +async def test_select_battery_charging_usage_available( + hass: HomeAssistant, mock_plenticore: Plenticore, mock_config_entry: MockConfigEntry +) -> None: + """Test that the battery charging usage select entity is added if the settings are available.""" + + mock_plenticore.client.get_settings.return_value = { + "devices:local": [ + SettingsData({"id": "Battery:SmartBatteryControl:Enable"}), + SettingsData({"id": "Battery:TimeControl:Enable"}), + ] + } + + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert entity_registry.async_get(hass).async_is_registered( + "select.battery_charging_usage_mode" + ) + + +async def test_select_battery_charging_usage_not_available( + hass: HomeAssistant, mock_plenticore: Plenticore, mock_config_entry: MockConfigEntry +) -> None: + """Test that the battery charging usage select entity is not added if the settings are unavailable.""" + + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not entity_registry.async_get(hass).async_is_registered( + "select.battery_charging_usage_mode" + ) From 494902e1852debf18bad2a95e616f9216367683b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 2 May 2022 18:33:16 +0200 Subject: [PATCH 0120/3516] Remove entity category system in favor of hidden_by (#68550) --- homeassistant/const.py | 2 -- homeassistant/helpers/entity.py | 3 -- homeassistant/helpers/entity_platform.py | 2 +- tests/components/alexa/test_entities.py | 10 +----- tests/components/cloud/test_alexa_config.py | 12 +------ tests/components/cloud/test_google_config.py | 17 ++-------- tests/components/energy/test_sensor.py | 34 +++++++++++++++++++ .../google_assistant/test_google_assistant.py | 10 +----- tests/helpers/test_entity.py | 3 +- 9 files changed, 42 insertions(+), 51 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e8e281adde2..22ee6c30e88 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -764,11 +764,9 @@ CLOUD_NEVER_EXPOSED_ENTITIES: Final[list[str]] = ["group.all_locks"] # use the EntityCategory enum instead. ENTITY_CATEGORY_CONFIG: Final = "config" ENTITY_CATEGORY_DIAGNOSTIC: Final = "diagnostic" -ENTITY_CATEGORY_SYSTEM: Final = "system" ENTITY_CATEGORIES: Final[list[str]] = [ ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC, - ENTITY_CATEGORY_SYSTEM, ] # The ID of the Home Assistant Media Player Cast App diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 791a80f7731..32163c4498a 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -200,9 +200,6 @@ class EntityCategory(StrEnum): # Diagnostic: An entity exposing some configuration parameter or diagnostics of a device DIAGNOSTIC = "diagnostic" - # System: An entity which is not useful for the user to interact with - SYSTEM = "system" - ENTITY_CATEGORIES_SCHEMA: Final = vol.Coerce(EntityCategory) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 6972dbf7c16..3d57a9c3dc0 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -519,8 +519,8 @@ class EntityPlatform: config_entry=self.config_entry, device_id=device_id, disabled_by=disabled_by, - hidden_by=hidden_by, entity_category=entity.entity_category, + hidden_by=hidden_by, known_object_ids=self.entities.keys(), original_device_class=entity.device_class, original_icon=entity.icon, diff --git a/tests/components/alexa/test_entities.py b/tests/components/alexa/test_entities.py index 5f64879b535..fb364dbf14e 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -43,20 +43,13 @@ async def test_categorized_hidden_entities(hass): entity_category=EntityCategory.DIAGNOSTIC, ) entity_entry3 = entity_registry.async_get_or_create( - "switch", - "test", - "switch_system_id", - suggested_object_id="system_switch", - entity_category=EntityCategory.SYSTEM, - ) - entity_entry4 = entity_registry.async_get_or_create( "switch", "test", "switch_hidden_integration_id", suggested_object_id="hidden_integration_switch", hidden_by=er.RegistryEntryHider.INTEGRATION, ) - entity_entry5 = entity_registry.async_get_or_create( + entity_entry4 = entity_registry.async_get_or_create( "switch", "test", "switch_hidden_user_id", @@ -69,7 +62,6 @@ async def test_categorized_hidden_entities(hass): hass.states.async_set(entity_entry2.entity_id, "something_else") hass.states.async_set(entity_entry3.entity_id, "blah") hass.states.async_set(entity_entry4.entity_id, "foo") - hass.states.async_set(entity_entry5.entity_id, "bar") msg = await smart_home.async_handle_message(hass, get_default_config(hass), request) diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 115d39d3aeb..465ff7dd3d4 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -39,20 +39,13 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub): entity_category=EntityCategory.DIAGNOSTIC, ) entity_entry3 = entity_registry.async_get_or_create( - "light", - "test", - "light_system_id", - suggested_object_id="system_light", - entity_category=EntityCategory.SYSTEM, - ) - entity_entry4 = entity_registry.async_get_or_create( "light", "test", "light_hidden_integration_id", suggested_object_id="hidden_integration_light", hidden_by=er.RegistryEntryHider.INTEGRATION, ) - entity_entry5 = entity_registry.async_get_or_create( + entity_entry4 = entity_registry.async_get_or_create( "light", "test", "light_hidden_user_id", @@ -77,7 +70,6 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub): assert not conf.should_expose(entity_entry2.entity_id) assert not conf.should_expose(entity_entry3.entity_id) assert not conf.should_expose(entity_entry4.entity_id) - assert not conf.should_expose(entity_entry5.entity_id) entity_conf["should_expose"] = True assert conf.should_expose("light.kitchen") @@ -86,7 +78,6 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub): assert not conf.should_expose(entity_entry2.entity_id) assert not conf.should_expose(entity_entry3.entity_id) assert not conf.should_expose(entity_entry4.entity_id) - assert not conf.should_expose(entity_entry5.entity_id) entity_conf["should_expose"] = None assert conf.should_expose("light.kitchen") @@ -95,7 +86,6 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs, cloud_stub): assert not conf.should_expose(entity_entry2.entity_id) assert not conf.should_expose(entity_entry3.entity_id) assert not conf.should_expose(entity_entry4.entity_id) - assert not conf.should_expose(entity_entry5.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 95746eb67ae..a86f1f3cf8a 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -298,20 +298,13 @@ async def test_google_config_expose_entity_prefs(hass, mock_conf, cloud_prefs): entity_category=EntityCategory.DIAGNOSTIC, ) entity_entry3 = entity_registry.async_get_or_create( - "light", - "test", - "light_system_id", - suggested_object_id="system_light", - entity_category=EntityCategory.SYSTEM, - ) - entity_entry4 = entity_registry.async_get_or_create( "light", "test", "light_hidden_integration_id", suggested_object_id="hidden_integration_light", hidden_by=er.RegistryEntryHider.INTEGRATION, ) - entity_entry5 = entity_registry.async_get_or_create( + entity_entry4 = entity_registry.async_get_or_create( "light", "test", "light_hidden_user_id", @@ -328,14 +321,12 @@ async def test_google_config_expose_entity_prefs(hass, 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") - state_system = State(entity_entry3.entity_id, "on") - state_hidden_integration = State(entity_entry4.entity_id, "on") - state_hidden_user = State(entity_entry5.entity_id, "on") + state_hidden_integration = State(entity_entry3.entity_id, "on") + state_hidden_user = State(entity_entry4.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) - assert not mock_conf.should_expose(state_system) assert not mock_conf.should_expose(state_hidden_integration) assert not mock_conf.should_expose(state_hidden_user) @@ -344,7 +335,6 @@ async def test_google_config_expose_entity_prefs(hass, mock_conf, cloud_prefs): # categorized and hidden entities should not be exposed assert not mock_conf.should_expose(state_config) assert not mock_conf.should_expose(state_diagnostic) - assert not mock_conf.should_expose(state_system) assert not mock_conf.should_expose(state_hidden_integration) assert not mock_conf.should_expose(state_hidden_user) @@ -353,7 +343,6 @@ async def test_google_config_expose_entity_prefs(hass, mock_conf, cloud_prefs): # categorized and hidden entities should not be exposed assert not mock_conf.should_expose(state_config) assert not mock_conf.should_expose(state_diagnostic) - assert not mock_conf.should_expose(state_system) assert not mock_conf.should_expose(state_hidden_integration) assert not mock_conf.should_expose(state_hidden_user) diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 913a2f44d93..997f60a8899 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -75,6 +75,40 @@ async def test_cost_sensor_no_states(hass, hass_storage, setup_integration) -> N # TODO: No states, should the cost entity refuse to setup? +async def test_cost_sensor_attributes(hass, hass_storage, setup_integration) -> None: + """Test sensor attributes.""" + energy_data = data.EnergyManager.default_preferences() + energy_data["energy_sources"].append( + { + "type": "grid", + "flow_from": [ + { + "stat_energy_from": "sensor.energy_consumption", + "entity_energy_from": "sensor.energy_consumption", + "stat_cost": None, + "entity_energy_price": None, + "number_energy_price": 1, + } + ], + "flow_to": [], + "cost_adjustment_day": 0, + } + ) + + hass_storage[data.STORAGE_KEY] = { + "version": 1, + "data": energy_data, + } + await setup_integration(hass) + + registry = er.async_get(hass) + cost_sensor_entity_id = "sensor.energy_consumption_cost" + entry = registry.async_get(cost_sensor_entity_id) + assert entry.entity_category is None + assert entry.disabled_by is None + assert entry.hidden_by == er.RegistryEntryHider.INTEGRATION + + @pytest.mark.parametrize("initial_energy,initial_cost", [(0, "0.0"), (None, "unknown")]) @pytest.mark.parametrize( "price_entity,fixed_price", [("sensor.energy_price", None), (None, 1)] diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 642ef15451a..8bf0e5573b2 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -147,20 +147,13 @@ async def test_sync_request(hass_fixture, assistant_client, auth_header): entity_category=EntityCategory.DIAGNOSTIC, ) entity_entry3 = entity_registry.async_get_or_create( - "switch", - "test", - "switch_system_id", - suggested_object_id="system_switch", - entity_category=EntityCategory.SYSTEM, - ) - entity_entry4 = entity_registry.async_get_or_create( "switch", "test", "switch_hidden_integration_id", suggested_object_id="hidden_integration_switch", hidden_by=er.RegistryEntryHider.INTEGRATION, ) - entity_entry5 = entity_registry.async_get_or_create( + entity_entry4 = entity_registry.async_get_or_create( "switch", "test", "switch_hidden_user_id", @@ -173,7 +166,6 @@ async def test_sync_request(hass_fixture, assistant_client, auth_header): hass_fixture.states.async_set(entity_entry2.entity_id, "something_else") hass_fixture.states.async_set(entity_entry3.entity_id, "blah") hass_fixture.states.async_set(entity_entry4.entity_id, "foo") - hass_fixture.states.async_set(entity_entry5.entity_id, "bar") reqid = "5711642932632160983" data = {"requestId": reqid, "inputs": [{"intent": "action.devices.SYNC"}]} diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 34cb68403f4..e345d7d7258 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -913,7 +913,6 @@ async def test_entity_category_property(hass): ( ("config", entity.EntityCategory.CONFIG), ("diagnostic", entity.EntityCategory.DIAGNOSTIC), - ("system", entity.EntityCategory.SYSTEM), ), ) def test_entity_category_schema(value, expected): @@ -930,7 +929,7 @@ def test_entity_category_schema_error(value): schema = vol.Schema(entity.ENTITY_CATEGORIES_SCHEMA) with pytest.raises( vol.Invalid, - match=r"expected EntityCategory or one of 'config', 'diagnostic', 'system'", + match=r"expected EntityCategory or one of 'config', 'diagnostic'", ): schema(value) From 72eb963c7ac3b721a91519e0ac3abd9b1ef304ba Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 28 Apr 2022 20:36:52 +0200 Subject: [PATCH 0121/3516] Handle situation where mac might not exist in clients (#71016) --- homeassistant/components/unifi/device_tracker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 0ad1c920fa7..2ae8eb2e32b 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -107,11 +107,11 @@ def add_client_entities(controller, async_add_entities, clients): trackers = [] for mac in clients: - if mac in controller.entities[DOMAIN][UniFiClientTracker.TYPE]: + if mac in controller.entities[DOMAIN][UniFiClientTracker.TYPE] or not ( + client := controller.api.clients.get(mac) + ): continue - client = controller.api.clients[mac] - if mac not in controller.wireless_clients: if not controller.option_track_wired_clients: continue From b8e0066ec2790b8608572867f6606861009d7313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20T=C3=B3th?= <42900504+toth2zoltan@users.noreply.github.com> Date: Mon, 2 May 2022 16:42:23 +0200 Subject: [PATCH 0122/3516] Fix SAJ Solar inverter RecursionError (#71157) --- homeassistant/components/saj/sensor.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 2fb3729d0a8..818ce7ea57a 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -209,14 +209,11 @@ class SAJsensor(SensorEntity): @property def device_class(self): """Return the device class the sensor belongs to.""" - if self.unit_of_measurement == POWER_WATT: + if self.native_unit_of_measurement == POWER_WATT: return SensorDeviceClass.POWER - if self.unit_of_measurement == ENERGY_KILO_WATT_HOUR: + if self.native_unit_of_measurement == ENERGY_KILO_WATT_HOUR: return SensorDeviceClass.ENERGY - if ( - self.unit_of_measurement == TEMP_CELSIUS - or self._sensor.unit == TEMP_FAHRENHEIT - ): + if self.native_unit_of_measurement in (TEMP_CELSIUS, TEMP_FAHRENHEIT): return SensorDeviceClass.TEMPERATURE @property From 6bb3957730dcd8226b855bde185a6ed230a61604 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Mon, 2 May 2022 10:42:47 -0400 Subject: [PATCH 0123/3516] Fix Insteon thermostats and reduce logging (#71179) * Bump pyinsteon to 1.1.0 * Load modem aldb if read write mode is unkwown * Correct reference to read_write_mode --- homeassistant/components/insteon/__init__.py | 4 +++- homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 15181cb827c..94c18df9a33 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -4,6 +4,7 @@ from contextlib import suppress import logging from pyinsteon import async_close, async_connect, devices +from pyinsteon.constants import ReadWriteMode from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PLATFORM, EVENT_HOMEASSISTANT_STOP @@ -48,7 +49,8 @@ async def async_get_device_config(hass, config_entry): with suppress(AttributeError): await devices[address].async_status() - await devices.async_load(id_devices=1) + load_aldb = devices.modem.aldb.read_write_mode == ReadWriteMode.UNKNOWN + await devices.async_load(id_devices=1, load_modem_aldb=load_aldb) for addr in devices: device = devices[addr] flags = True diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index fc8ade1ba4d..c69f4f2cdf5 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/insteon", "dependencies": ["http", "websocket_api"], "requirements": [ - "pyinsteon==1.1.0b3", + "pyinsteon==1.1.0", "insteon-frontend-home-assistant==0.1.0" ], "codeowners": ["@teharris1"], diff --git a/requirements_all.txt b/requirements_all.txt index f50196895c8..914c06f40e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1547,7 +1547,7 @@ pyialarm==1.9.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.0b3 +pyinsteon==1.1.0 # homeassistant.components.intesishome pyintesishome==1.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce65bd2814f..70d3ab4a700 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1029,7 +1029,7 @@ pyialarm==1.9.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.0b3 +pyinsteon==1.1.0 # homeassistant.components.ipma pyipma==2.0.5 From b558425333311bcd3ea005da826fab03bd0e99ce Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 May 2022 04:35:19 -0700 Subject: [PATCH 0124/3516] Offer visit device for Squeezelite32 devices (#71181) --- homeassistant/components/slimproto/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/slimproto/media_player.py b/homeassistant/components/slimproto/media_player.py index af3eb693478..d41d10432db 100644 --- a/homeassistant/components/slimproto/media_player.py +++ b/homeassistant/components/slimproto/media_player.py @@ -100,8 +100,8 @@ class SlimProtoPlayer(MediaPlayerEntity): name=self.player.name, hw_version=self.player.firmware, ) - # PiCore player has web interface - if "-pCP" in self.player.firmware: + # PiCore + SqueezeESP32 player has web interface + if "-pCP" in self.player.firmware or self.player.device_model == "SqueezeESP32": self._attr_device_info[ "configuration_url" ] = f"http://{self.player.device_address}" From c07e36283bcf927c5b88b7176ee9a2664e8f75b9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 May 2022 06:09:49 -0700 Subject: [PATCH 0125/3516] Add media source support to AppleTV (#71185) --- .../components/apple_tv/browse_media.py | 2 +- .../components/apple_tv/media_player.py | 54 +++++++++++++++++-- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/apple_tv/browse_media.py b/homeassistant/components/apple_tv/browse_media.py index 3c0eee8b6ad..8d0a94ca858 100644 --- a/homeassistant/components/apple_tv/browse_media.py +++ b/homeassistant/components/apple_tv/browse_media.py @@ -18,7 +18,7 @@ def build_app_list(app_list): return BrowseMedia( media_class=MEDIA_CLASS_DIRECTORY, - media_content_id=None, + media_content_id="apps", media_content_type=MEDIA_TYPE_APPS, title="Apps", can_play=True, diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index cadf84e8b31..02919043bb2 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -13,11 +13,15 @@ from pyatv.const import ( ) from pyatv.helpers import is_streamable +from homeassistant.components import media_source from homeassistant.components.media_player import ( BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, ) +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import ( MEDIA_TYPE_APP, MEDIA_TYPE_MUSIC, @@ -73,7 +77,8 @@ SUPPORT_APPLE_TV = ( # Map features in pyatv to Home Assistant SUPPORT_FEATURE_MAPPING = { - FeatureName.PlayUrl: MediaPlayerEntityFeature.PLAY_MEDIA, + FeatureName.PlayUrl: MediaPlayerEntityFeature.BROWSE_MEDIA + | MediaPlayerEntityFeature.PLAY_MEDIA, FeatureName.StreamFile: MediaPlayerEntityFeature.PLAY_MEDIA, FeatureName.Pause: MediaPlayerEntityFeature.PAUSE, FeatureName.Play: MediaPlayerEntityFeature.PLAY, @@ -276,12 +281,24 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): # RAOP. Otherwise try to play it with regular AirPlay. if media_type == MEDIA_TYPE_APP: await self.atv.apps.launch_app(media_id) - elif self._is_feature_available(FeatureName.StreamFile) and ( - await is_streamable(media_id) or media_type == MEDIA_TYPE_MUSIC + + is_media_source_id = media_source.is_media_source_id(media_id) + + if ( + not is_media_source_id + and self._is_feature_available(FeatureName.StreamFile) + and (await is_streamable(media_id) or media_type == MEDIA_TYPE_MUSIC) ): _LOGGER.debug("Streaming %s via RAOP", media_id) await self.atv.stream.stream_file(media_id) - elif self._is_feature_available(FeatureName.PlayUrl): + + if self._is_feature_available(FeatureName.PlayUrl): + if is_media_source_id: + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + + media_id = async_process_play_media_url(self.hass, media_id) + _LOGGER.debug("Playing %s via AirPlay", media_id) await self.atv.stream.play_url(media_id) else: @@ -380,7 +397,34 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): media_content_id=None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" - return build_app_list(self._app_list) + # If we can't stream URLs, we can't browse media. + # In that case the `BROWSE_MEDIA` feature was added because of AppList/LaunchApp + if not self._is_feature_available(FeatureName.PlayUrl): + return build_app_list(self._app_list) + + if self._app_list: + kwargs = {} + else: + # If it has no apps, assume it has no display + kwargs = { + "content_filter": lambda item: item.media_content_type.startswith( + "audio/" + ), + } + + cur_item = await media_source.async_browse_media( + self.hass, media_content_id, **kwargs + ) + + # If media content id is not None, we're browsing into a media source + if media_content_id is not None: + return cur_item + + # Add app item if we have one + if self._app_list and cur_item.children: + cur_item.children.insert(0, build_app_list(self._app_list)) + + return cur_item async def async_turn_on(self): """Turn the media player on.""" From d97bce2d9da30733672212f81817938961193e87 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 2 May 2022 08:12:32 +0200 Subject: [PATCH 0126/3516] Fix Renault diagnostics (#71186) --- homeassistant/components/renault/diagnostics.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/renault/diagnostics.py b/homeassistant/components/renault/diagnostics.py index f8b0a0d926e..2029ef989d6 100644 --- a/homeassistant/components/renault/diagnostics.py +++ b/homeassistant/components/renault/diagnostics.py @@ -59,7 +59,9 @@ def _get_vehicle_diagnostics(vehicle: RenaultVehicleProxy) -> dict[str, Any]: return { "details": async_redact_data(vehicle.details.raw_data, TO_REDACT), "data": { - key: async_redact_data(coordinator.data.raw_data, TO_REDACT) + key: async_redact_data( + coordinator.data.raw_data if coordinator.data else None, TO_REDACT + ) for key, coordinator in vehicle.coordinators.items() }, } From 35bc81239766590ea33c0ea87197f5dee13d9575 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 2 May 2022 09:51:19 +0200 Subject: [PATCH 0127/3516] Make sure sensor state value is not None prior to trying to used the scaled value (#71189) --- homeassistant/components/deconz/sensor.py | 8 +++--- tests/components/deconz/test_sensor.py | 30 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index a4dcaed8f18..d3a20fad522 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -112,7 +112,7 @@ ENTITY_DESCRIPTIONS = { DeconzSensorDescription( key="consumption", value_fn=lambda device: device.scaled_consumption - if isinstance(device, Consumption) + if isinstance(device, Consumption) and isinstance(device.consumption, int) else None, update_key="consumption", device_class=SensorDeviceClass.ENERGY, @@ -144,7 +144,7 @@ ENTITY_DESCRIPTIONS = { DeconzSensorDescription( key="humidity", value_fn=lambda device: device.scaled_humidity - if isinstance(device, Humidity) + if isinstance(device, Humidity) and isinstance(device.humidity, int) else None, update_key="humidity", device_class=SensorDeviceClass.HUMIDITY, @@ -156,7 +156,7 @@ ENTITY_DESCRIPTIONS = { DeconzSensorDescription( key="light_level", value_fn=lambda device: device.scaled_light_level - if isinstance(device, LightLevel) + if isinstance(device, LightLevel) and isinstance(device.light_level, int) else None, update_key="lightlevel", device_class=SensorDeviceClass.ILLUMINANCE, @@ -189,7 +189,7 @@ ENTITY_DESCRIPTIONS = { DeconzSensorDescription( key="temperature", value_fn=lambda device: device.scaled_temperature - if isinstance(device, Temperature) + if isinstance(device, Temperature) and isinstance(device.temperature, int) else None, update_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index bd51bab44e4..590ccee25d5 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -785,6 +785,36 @@ async def test_add_new_sensor(hass, aioclient_mock, mock_deconz_websocket): assert hass.states.get("sensor.light_level_sensor").state == "999.8" +BAD_SENSOR_DATA = [ + ("ZHAConsumption", "consumption"), + ("ZHAHumidity", "humidity"), + ("ZHALightLevel", "lightlevel"), + ("ZHATemperature", "temperature"), +] + + +@pytest.mark.parametrize("sensor_type, sensor_property", BAD_SENSOR_DATA) +async def test_dont_add_sensor_if_state_is_none( + hass, aioclient_mock, sensor_type, sensor_property +): + """Test sensor with scaled data is not created if state is None.""" + data = { + "sensors": { + "1": { + "name": "Sensor 1", + "type": sensor_type, + "state": {sensor_property: None}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:00-00", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + await setup_deconz_integration(hass, aioclient_mock) + + assert len(hass.states.async_all()) == 0 + + async def test_add_battery_later(hass, aioclient_mock, mock_deconz_websocket): """Test that a sensor without an initial battery state creates a battery sensor once state exist.""" data = { From c37fec67a120b5ada715556e7880431cb1207799 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 2 May 2022 13:15:19 +0200 Subject: [PATCH 0128/3516] Remove entity registry entries when script is removed (#71193) --- homeassistant/components/config/automation.py | 2 +- homeassistant/components/config/scene.py | 2 +- homeassistant/components/config/script.py | 16 ++++++++++++++-- tests/components/config/test_script.py | 19 +++++++++++++++++-- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index f1a2d9aab84..5a39b786e27 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -23,7 +23,7 @@ async def async_setup(hass): if action != ACTION_DELETE: return - ent_reg = await entity_registry.async_get_registry(hass) + ent_reg = entity_registry.async_get(hass) entity_id = ent_reg.async_get_entity_id(DOMAIN, DOMAIN, config_key) diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index 6523ff84158..862c8c46f4d 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -20,7 +20,7 @@ async def async_setup(hass): if action != ACTION_DELETE: return - ent_reg = await entity_registry.async_get_registry(hass) + ent_reg = entity_registry.async_get(hass) entity_id = ent_reg.async_get_entity_id(DOMAIN, HA_DOMAIN, config_key) diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index 7adc766a1ab..45a7a6dc227 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -6,9 +6,9 @@ from homeassistant.components.script.config import ( ) from homeassistant.config import SCRIPT_CONFIG_PATH from homeassistant.const import SERVICE_RELOAD -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er -from . import EditKeyBasedConfigView +from . import ACTION_DELETE, EditKeyBasedConfigView async def async_setup(hass): @@ -18,6 +18,18 @@ async def async_setup(hass): """post_write_hook for Config View that reloads scripts.""" await hass.services.async_call(DOMAIN, SERVICE_RELOAD) + if action != ACTION_DELETE: + return + + ent_reg = er.async_get(hass) + + entity_id = ent_reg.async_get_entity_id(DOMAIN, DOMAIN, config_key) + + if entity_id is None: + return + + ent_reg.async_remove(entity_id) + hass.http.register_view( EditScriptConfigView( DOMAIN, diff --git a/tests/components/config/test_script.py b/tests/components/config/test_script.py index dca9a8aa8a7..4b6ca1bdc8f 100644 --- a/tests/components/config/test_script.py +++ b/tests/components/config/test_script.py @@ -6,21 +6,34 @@ import pytest from homeassistant.bootstrap import async_setup_component from homeassistant.components import config +from homeassistant.helpers import entity_registry as er from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 @pytest.fixture(autouse=True) -async def setup_script(hass, stub_blueprint_populate): # noqa: F811 +async def setup_script(hass, script_config, stub_blueprint_populate): # noqa: F811 """Set up script integration.""" - assert await async_setup_component(hass, "script", {}) + assert await async_setup_component(hass, "script", {"script": script_config}) +@pytest.mark.parametrize( + "script_config", + ( + { + "one": {"alias": "Light on", "sequence": []}, + "two": {"alias": "Light off", "sequence": []}, + }, + ), +) async def test_delete_script(hass, hass_client): """Test deleting a script.""" with patch.object(config, "SECTIONS", ["script"]): await async_setup_component(hass, "config", {}) + ent_reg = er.async_get(hass) + assert len(ent_reg.entities) == 2 + client = await hass_client() orig_data = {"one": {}, "two": {}} @@ -46,3 +59,5 @@ async def test_delete_script(hass, hass_client): assert len(written) == 1 assert written[0] == {"one": {}} + + assert len(ent_reg.entities) == 1 From fed4d0a38d3f90753a7aa03dcae485e6ea37d108 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 2 May 2022 14:59:58 +0200 Subject: [PATCH 0129/3516] Stop script if sub-script stops or aborts (#71195) --- homeassistant/helpers/script.py | 10 ++++-- tests/helpers/test_script.py | 61 ++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 70f73f065ab..d988b0edd81 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -395,8 +395,14 @@ class _ScriptRun: script_execution_set("finished") except _StopScript: script_execution_set("finished") + # Let the _StopScript bubble up if this is a sub-script + if not self._script.top_level: + raise except _AbortScript: script_execution_set("aborted") + # Let the _AbortScript bubble up if this is a sub-script + if not self._script.top_level: + raise except Exception: script_execution_set("error") raise @@ -1143,7 +1149,7 @@ class Script: hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, partial(_async_stop_scripts_at_shutdown, hass) ) - self._top_level = top_level + self.top_level = top_level if top_level: all_scripts.append( {"instance": self, "started_before_shutdown": not hass.is_stopping} @@ -1431,7 +1437,7 @@ class Script: # If this is a top level Script then make a copy of the variables in case they # are read-only, but more importantly, so as not to leak any variables created # during the run back to the caller. - if self._top_level: + if self.top_level: if self.variables: try: variables = self.variables.async_render( diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index fb3b021daec..b9c968838c9 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -94,7 +94,7 @@ def assert_element(trace_element, expected_element, path): # Check for unexpected items in trace_element assert not set(trace_element._result or {}) - set(expected_result) - if "error_type" in expected_element: + if "error_type" in expected_element and expected_element["error_type"] is not None: assert isinstance(trace_element._error, expected_element["error_type"]) else: assert trace_element._error is None @@ -4485,6 +4485,65 @@ async def test_stop_action(hass, caplog): ) +@pytest.mark.parametrize( + "error,error_type,logmsg,script_execution", + ( + (True, script._AbortScript, "Error", "aborted"), + (False, None, "Stop", "finished"), + ), +) +async def test_stop_action_subscript( + hass, caplog, error, error_type, logmsg, script_execution +): + """Test if automation stops on calling the stop action from a sub-script.""" + event = "test_event" + events = async_capture_events(hass, event) + + alias = "stop step" + sequence = cv.SCRIPT_SCHEMA( + [ + {"event": event}, + { + "if": { + "alias": "if condition", + "condition": "template", + "value_template": "{{ 1 == 1 }}", + }, + "then": { + "alias": alias, + "stop": "In the name of love", + "error": error, + }, + }, + {"event": event}, + ] + ) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + assert f"{logmsg} script sequence: In the name of love" in caplog.text + caplog.clear() + assert len(events) == 1 + + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{"error_type": error_type, "result": {"choice": "then"}}], + "1/if": [{"result": {"result": True}}], + "1/if/condition/0": [{"result": {"result": True, "entities": []}}], + "1/then/0": [ + { + "error_type": error_type, + "result": {"stop": "In the name of love", "error": error}, + } + ], + }, + expected_script_execution=script_execution, + ) + + async def test_stop_action_with_error(hass, caplog): """Test if automation fails on calling the error action.""" event = "test_event" From 4dfcc9dcf9f06ff8eaac0688916e3ba720432d10 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 2 May 2022 16:41:14 +0200 Subject: [PATCH 0130/3516] Allow cancelling async_at_start helper (#71196) --- homeassistant/helpers/start.py | 15 +++++++-- tests/helpers/test_start.py | 61 +++++++++++++++++++++++++++++++--- 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/start.py b/homeassistant/helpers/start.py index 7f919f5351d..6c17ae5be3a 100644 --- a/homeassistant/helpers/start.py +++ b/homeassistant/helpers/start.py @@ -20,8 +20,19 @@ def async_at_start( hass.async_run_hass_job(at_start_job, hass) return lambda: None - async def _matched_event(event: Event) -> None: + unsub: None | CALLBACK_TYPE = None + + @callback + def _matched_event(event: Event) -> None: """Call the callback when Home Assistant started.""" hass.async_run_hass_job(at_start_job, hass) + nonlocal unsub + unsub = None - return hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _matched_event) + @callback + def cancel() -> None: + if unsub: + unsub() + + unsub = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _matched_event) + return cancel diff --git a/tests/helpers/test_start.py b/tests/helpers/test_start.py index 55f98cf60eb..bc32ffa35fd 100644 --- a/tests/helpers/test_start.py +++ b/tests/helpers/test_start.py @@ -27,7 +27,7 @@ async def test_at_start_when_running_awaitable(hass): assert len(calls) == 2 -async def test_at_start_when_running_callback(hass): +async def test_at_start_when_running_callback(hass, caplog): """Test at start when already running.""" assert hass.state == core.CoreState.running assert hass.is_running @@ -39,15 +39,19 @@ async def test_at_start_when_running_callback(hass): """Home Assistant is started.""" calls.append(1) - start.async_at_start(hass, cb_at_start) + start.async_at_start(hass, cb_at_start)() assert len(calls) == 1 hass.state = core.CoreState.starting assert hass.is_running - start.async_at_start(hass, cb_at_start) + start.async_at_start(hass, cb_at_start)() assert len(calls) == 2 + # Check the unnecessary cancel did not generate warnings or errors + for record in caplog.records: + assert record.levelname in ("DEBUG", "INFO") + async def test_at_start_when_starting_awaitable(hass): """Test at start when yet to start.""" @@ -69,7 +73,7 @@ async def test_at_start_when_starting_awaitable(hass): assert len(calls) == 1 -async def test_at_start_when_starting_callback(hass): +async def test_at_start_when_starting_callback(hass, caplog): """Test at start when yet to start.""" hass.state = core.CoreState.not_running assert not hass.is_running @@ -81,10 +85,57 @@ async def test_at_start_when_starting_callback(hass): """Home Assistant is started.""" calls.append(1) - start.async_at_start(hass, cb_at_start) + cancel = start.async_at_start(hass, cb_at_start) await hass.async_block_till_done() assert len(calls) == 0 hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() assert len(calls) == 1 + + cancel() + + # Check the unnecessary cancel did not generate warnings or errors + for record in caplog.records: + assert record.levelname in ("DEBUG", "INFO") + + +async def test_cancelling_when_running(hass, caplog): + """Test cancelling at start when already running.""" + assert hass.state == core.CoreState.running + assert hass.is_running + + calls = [] + + async def cb_at_start(hass): + """Home Assistant is started.""" + calls.append(1) + + start.async_at_start(hass, cb_at_start)() + await hass.async_block_till_done() + assert len(calls) == 1 + + # Check the unnecessary cancel did not generate warnings or errors + for record in caplog.records: + assert record.levelname in ("DEBUG", "INFO") + + +async def test_cancelling_when_starting(hass): + """Test cancelling at start when yet to start.""" + hass.state = core.CoreState.not_running + assert not hass.is_running + + calls = [] + + @core.callback + def cb_at_start(hass): + """Home Assistant is started.""" + calls.append(1) + + start.async_at_start(hass, cb_at_start)() + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert len(calls) == 0 From 2b87e03fac61942b2fc0b9a74d6840e98032c21b Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 2 May 2022 09:23:17 -0400 Subject: [PATCH 0131/3516] Fix bad ZHA _attr definitions (#71198) --- homeassistant/components/zha/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 0a5fe204648..249034ef068 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -345,7 +345,7 @@ class ElectricalMeasurementFrequency(ElectricalMeasurement, id_suffix="ac_freque """Frequency measurement.""" SENSOR_ATTR = "ac_frequency" - _device_class: SensorDeviceClass = SensorDeviceClass.FREQUENCY + _attr_device_class: SensorDeviceClass = SensorDeviceClass.FREQUENCY _unit = FREQUENCY_HERTZ _div_mul_prefix = "ac_frequency" @@ -360,7 +360,7 @@ class ElectricalMeasurementPowerFactor(ElectricalMeasurement, id_suffix="power_f """Frequency measurement.""" SENSOR_ATTR = "power_factor" - _device_class: SensorDeviceClass = SensorDeviceClass.POWER_FACTOR + _attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER_FACTOR _unit = PERCENTAGE @property @@ -718,8 +718,8 @@ class SinopeHVACAction(ThermostatHVACAction): class RSSISensor(Sensor, id_suffix="rssi"): """RSSI sensor for a device.""" - _state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _device_class: SensorDeviceClass = SensorDeviceClass.SIGNAL_STRENGTH + _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _attr_device_class: SensorDeviceClass = SensorDeviceClass.SIGNAL_STRENGTH _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_registry_enabled_default = False From 71506bd02bba8a30bb6c697d69859459e85aa7f1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 May 2022 17:35:37 +0200 Subject: [PATCH 0132/3516] Adjust version number in template default deprecation warning (#71203) --- homeassistant/helpers/template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index bded5ab933c..dbc82ce6902 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1331,7 +1331,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 2022.1" + "to render in Home Assistant core 2022.6" ), function, value, From 6cac24887c24e9ea1aac0d4fdb13ccce2c1d20d8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 May 2022 10:51:13 -0700 Subject: [PATCH 0133/3516] Skip signing URL that we know requires no auth (#71208) --- .../components/media_player/browse_media.py | 7 +++++++ tests/components/media_player/test_browse_media.py | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index 60234cd1b38..9327bf68f9f 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -20,6 +20,9 @@ from homeassistant.helpers.network import ( from .const import CONTENT_AUTH_EXPIRY_TIME, MEDIA_CLASS_DIRECTORY +# Paths that we don't need to sign +PATHS_WITHOUT_AUTH = ("/api/tts_proxy/",) + @callback def async_process_play_media_url( @@ -46,6 +49,10 @@ def async_process_play_media_url( logging.getLogger(__name__).debug( "Not signing path for content with query param" ) + elif parsed.path.startswith(PATHS_WITHOUT_AUTH): + # We don't sign this path if it doesn't need auth. Although signing itself can't hurt, + # some devices are unable to handle long URLs and the auth signature might push it over. + pass else: signed_path = async_sign_path( hass, diff --git a/tests/components/media_player/test_browse_media.py b/tests/components/media_player/test_browse_media.py index ea1e3b4fc36..58081c7b3b1 100644 --- a/tests/components/media_player/test_browse_media.py +++ b/tests/components/media_player/test_browse_media.py @@ -73,6 +73,18 @@ async def test_process_play_media_url(hass, mock_sign_path): == "http://192.168.123.123:8123/path?hello=world" ) + # Test skip signing URLs if they are known to require no auth + assert ( + async_process_play_media_url(hass, "/api/tts_proxy/bla") + == "http://example.local:8123/api/tts_proxy/bla" + ) + assert ( + async_process_play_media_url( + hass, "http://example.local:8123/api/tts_proxy/bla" + ) + == "http://example.local:8123/api/tts_proxy/bla" + ) + with pytest.raises(ValueError): async_process_play_media_url(hass, "hello") From d213cc3c8ecc0d22648eac797e5ee3082eeea951 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 May 2022 11:02:20 -0700 Subject: [PATCH 0134/3516] Add media source support to Bose Soundtouch (#71209) --- .../components/soundtouch/media_player.py | 20 +++++++++++++++++++ .../soundtouch/test_media_player.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index 7081f4a3a0f..3172eb4aed6 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -1,6 +1,7 @@ """Support for interface with a Bose Soundtouch.""" from __future__ import annotations +from functools import partial import logging import re @@ -8,11 +9,15 @@ from libsoundtouch import soundtouch_device from libsoundtouch.utils import Source import voluptuous as vol +from homeassistant.components import media_source from homeassistant.components.media_player import ( PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, ) +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -190,6 +195,7 @@ class SoundTouchDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.SELECT_SOURCE + | MediaPlayerEntityFeature.BROWSE_MEDIA ) def __init__(self, name, config): @@ -348,6 +354,16 @@ class SoundTouchDevice(MediaPlayerEntity): EVENT_HOMEASSISTANT_START, async_update_on_start ) + async def async_play_media(self, media_type, media_id, **kwargs): + """Play a piece of media.""" + if media_source.is_media_source_id(media_id): + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = async_process_play_media_url(self.hass, play_item.url) + + await self.hass.async_add_executor_job( + partial(self.play_media, media_type, media_id, **kwargs) + ) + def play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" _LOGGER.debug("Starting media with media_id: %s", media_id) @@ -450,6 +466,10 @@ class SoundTouchDevice(MediaPlayerEntity): return attributes + async def async_browse_media(self, media_content_type=None, media_content_id=None): + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media(self.hass, media_content_id) + def get_zone_info(self): """Return the current zone info.""" zone_status = self._device.zone_status() diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index 4ed8a648c77..797b5b440d1 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -488,7 +488,7 @@ async def test_media_commands(mocked_status, mocked_volume, hass, one_device): assert mocked_volume.call_count == 2 entity_1_state = hass.states.get("media_player.soundtouch_1") - assert entity_1_state.attributes["supported_features"] == 20413 + assert entity_1_state.attributes["supported_features"] == 151485 @patch("libsoundtouch.device.SoundTouchDevice.power_off") From 06f939b0ac4584dc0a4025d278bf45e28ab2e782 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 May 2022 15:39:05 -0700 Subject: [PATCH 0135/3516] Bump frontend to 20220502.0 (#71221) --- 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 e57f06827a5..18e2fc6ed8f 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220429.0"], + "requirements": ["home-assistant-frontend==20220502.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6dd904c858f..f328394b9c1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220429.0 +home-assistant-frontend==20220502.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.1 diff --git a/requirements_all.txt b/requirements_all.txt index 914c06f40e2..2a776b4d883 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220429.0 +home-assistant-frontend==20220502.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70d3ab4a700..3ce220105eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -580,7 +580,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220429.0 +home-assistant-frontend==20220502.0 # homeassistant.components.home_connect homeconnect==0.7.0 From 8ac5dd32a8add0bc0766d444da102f728b3472f0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 2 May 2022 15:40:51 -0700 Subject: [PATCH 0136/3516] Bumped version to 2022.5.0b6 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 22ee6c30e88..a3647a3a27c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 16fb8c94692..84275aca14f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.5.0b5 +version = 2022.5.0b6 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 496260561355c9b4f6d7c9fc78638dda88506b8e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 3 May 2022 01:46:39 +0200 Subject: [PATCH 0137/3516] Fix enumeration of zwave-js device triggers (#71225) * Fix enumeration of zwave-js device triggers * Address review comments --- .../components/zwave_js/device_trigger.py | 6 ++++- homeassistant/components/zwave_js/helpers.py | 23 +++++++++++-------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py index 89379f9a953..1e76f9e73ec 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -266,7 +266,11 @@ async def async_get_triggers( entity_id = async_get_node_status_sensor_entity_id( hass, device_id, ent_reg, dev_reg ) - if (entity := ent_reg.async_get(entity_id)) is not None and not entity.disabled: + if ( + entity_id + and (entity := ent_reg.async_get(entity_id)) is not None + and not entity.disabled + ): triggers.append( {**base_trigger, CONF_TYPE: NODE_STATUS, CONF_ENTITY_ID: entity_id} ) diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index a5a2da23206..17515f9aa99 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -329,13 +329,22 @@ def get_zwave_value_from_config(node: ZwaveNode, config: ConfigType) -> ZwaveVal return node.values[value_id] +def _zwave_js_config_entry(hass: HomeAssistant, device: dr.DeviceEntry) -> str | None: + """Find zwave_js config entry from a device.""" + for entry_id in device.config_entries: + entry = hass.config_entries.async_get_entry(entry_id) + if entry and entry.domain == DOMAIN: + return entry_id + return None + + @callback def async_get_node_status_sensor_entity_id( hass: HomeAssistant, device_id: str, ent_reg: er.EntityRegistry | None = None, dev_reg: dr.DeviceRegistry | None = None, -) -> str: +) -> str | None: """Get the node status sensor entity ID for a given Z-Wave JS device.""" if not ent_reg: ent_reg = er.async_get(hass) @@ -344,20 +353,16 @@ def async_get_node_status_sensor_entity_id( 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) + if not (entry_id := _zwave_js_config_entry(hass, device)): + return None + client = hass.data[DOMAIN][entry_id][DATA_CLIENT] node = async_get_node_from_device_id(hass, device_id, dev_reg) - entity_id = ent_reg.async_get_entity_id( + return ent_reg.async_get_entity_id( SENSOR_DOMAIN, DOMAIN, f"{client.driver.controller.home_id}.{node.node_id}.node_status", ) - if not entity_id: - raise HomeAssistantError( - "Node status sensor entity not found. Device may not be a zwave_js device" - ) - - return entity_id def remove_keys_with_empty_values(config: ConfigType) -> ConfigType: From c594de25f7967bfe65b97f600f789eb675372d74 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 3 May 2022 00:23:56 +0000 Subject: [PATCH 0138/3516] [ci skip] Translation update --- .../application_credentials/translations/no.json | 3 +++ .../components/deconz/translations/no.json | 1 + .../components/dialogflow/translations/fr.json | 2 +- .../components/geofency/translations/fr.json | 2 +- .../components/gpslogger/translations/fr.json | 2 +- .../components/ifttt/translations/fr.json | 2 +- .../components/isy994/translations/no.json | 13 +++++++++++-- .../components/locative/translations/da.json | 2 +- .../components/mailgun/translations/fr.json | 2 +- .../components/meater/translations/de.json | 9 +++++++++ .../components/meater/translations/el.json | 9 +++++++++ .../components/meater/translations/fr.json | 9 +++++++++ .../components/meater/translations/hu.json | 9 +++++++++ .../components/meater/translations/nl.json | 5 +++++ .../components/meater/translations/pl.json | 9 +++++++++ .../components/meater/translations/pt-BR.json | 9 +++++++++ .../components/meater/translations/ru.json | 9 +++++++++ .../components/meater/translations/zh-Hant.json | 9 +++++++++ .../components/owntracks/translations/fr.json | 2 +- .../components/recorder/translations/no.json | 8 ++++++++ .../components/simplisafe/translations/de.json | 2 +- .../components/simplisafe/translations/en.json | 14 ++++++++++++-- .../components/simplisafe/translations/fr.json | 2 +- .../components/simplisafe/translations/no.json | 2 +- .../components/simplisafe/translations/pl.json | 2 +- .../components/simplisafe/translations/pt-BR.json | 2 +- .../simplisafe/translations/zh-Hant.json | 2 +- homeassistant/components/sql/translations/no.json | 4 ++++ .../components/steam_online/translations/no.json | 4 ++-- .../components/traccar/translations/fr.json | 2 +- .../components/unifi/translations/el.json | 3 +++ .../components/unifi/translations/hu.json | 3 +++ .../components/unifi/translations/nl.json | 3 +++ .../components/unifi/translations/no.json | 5 ++++- .../components/unifi/translations/pl.json | 3 +++ .../components/unifi/translations/ru.json | 3 +++ .../components/unifi/translations/zh-Hant.json | 5 ++++- 37 files changed, 155 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/application_credentials/translations/no.json create mode 100644 homeassistant/components/recorder/translations/no.json diff --git a/homeassistant/components/application_credentials/translations/no.json b/homeassistant/components/application_credentials/translations/no.json new file mode 100644 index 00000000000..5e55d3d6538 --- /dev/null +++ b/homeassistant/components/application_credentials/translations/no.json @@ -0,0 +1,3 @@ +{ + "title": "S\u00f8knadslegitimasjon" +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/no.json b/homeassistant/components/deconz/translations/no.json index 06c03b8b585..7682cb15a5d 100644 --- a/homeassistant/components/deconz/translations/no.json +++ b/homeassistant/components/deconz/translations/no.json @@ -9,6 +9,7 @@ "updated_instance": "Oppdatert deCONZ forekomst med ny vertsadresse" }, "error": { + "linking_not_possible": "Kunne ikke koble til gatewayen", "no_key": "Kunne ikke f\u00e5 en API-n\u00f8kkel" }, "flow_title": "{host}", diff --git a/homeassistant/components/dialogflow/translations/fr.json b/homeassistant/components/dialogflow/translations/fr.json index 661d86566b0..56d754d7f0a 100644 --- a/homeassistant/components/dialogflow/translations/fr.json +++ b/homeassistant/components/dialogflow/translations/fr.json @@ -10,7 +10,7 @@ }, "step": { "user": { - "description": "\u00cates-vous s\u00fbr de vouloir configurer Dialogflow?", + "description": "Voulez-vous vraiment configurer Dialogflow\u00a0?", "title": "Configurer le Webhook Dialogflow" } } diff --git a/homeassistant/components/geofency/translations/fr.json b/homeassistant/components/geofency/translations/fr.json index 84da031d50d..0e268eae673 100644 --- a/homeassistant/components/geofency/translations/fr.json +++ b/homeassistant/components/geofency/translations/fr.json @@ -10,7 +10,7 @@ }, "step": { "user": { - "description": "\u00cates-vous s\u00fbr de vouloir configurer le Webhook Geofency ?", + "description": "Voulez-vous vraiment configurer le webhook Geofency\u00a0?", "title": "Configurer le Webhook Geofency" } } diff --git a/homeassistant/components/gpslogger/translations/fr.json b/homeassistant/components/gpslogger/translations/fr.json index 4e207dddfcd..21896884391 100644 --- a/homeassistant/components/gpslogger/translations/fr.json +++ b/homeassistant/components/gpslogger/translations/fr.json @@ -10,7 +10,7 @@ }, "step": { "user": { - "description": "\u00cates-vous s\u00fbr de vouloir configurer le Webhook GPSLogger ?", + "description": "Voulez-vous vraiment configurer le webhook GPSLogger\u00a0?", "title": "Configurer le Webhook GPSLogger" } } diff --git a/homeassistant/components/ifttt/translations/fr.json b/homeassistant/components/ifttt/translations/fr.json index 371bbd65ee0..280c031ea39 100644 --- a/homeassistant/components/ifttt/translations/fr.json +++ b/homeassistant/components/ifttt/translations/fr.json @@ -10,7 +10,7 @@ }, "step": { "user": { - "description": "\u00cates-vous s\u00fbr de vouloir configurer IFTTT?", + "description": "Voulez-vous vraiment configurer IFTTT\u00a0?", "title": "Configurer l'applet IFTTT Webhook" } } diff --git a/homeassistant/components/isy994/translations/no.json b/homeassistant/components/isy994/translations/no.json index 7da3fa4c6fe..e18666d7fc4 100644 --- a/homeassistant/components/isy994/translations/no.json +++ b/homeassistant/components/isy994/translations/no.json @@ -7,10 +7,19 @@ "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", "invalid_host": "Vertsoppf\u00f8ringen var ikke i fullstendig URL-format, for eksempel http://192.168.10.100:80", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "unknown": "Uventet feil" }, "flow_title": "{name} ( {host} )", "step": { + "reauth_confirm": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "P\u00e5loggingsinformasjonen for {host} er ikke lenger gyldig.", + "title": "Autentiser din ISY p\u00e5 nytt" + }, "user": { "data": { "host": "URL", @@ -19,7 +28,7 @@ "username": "Brukernavn" }, "description": "Vertsoppf\u00f8ringen m\u00e5 v\u00e6re i fullstendig URL-format, for eksempel http://192.168.10.100:80", - "title": "Koble til ISY994" + "title": "Koble til din ISY" } } }, @@ -33,7 +42,7 @@ "variable_sensor_string": "Variabel sensorstreng" }, "description": "Angi alternativene for ISY-integrering: \n \u2022 Nodesensorstreng: Alle enheter eller mapper som inneholder NodeSensor String i navnet, behandles som en sensor eller bin\u00e6r sensor. \n \u2022 Ignorer streng: Alle enheter med 'Ignorer streng' i navnet ignoreres. \n \u2022 Variabel sensorstreng: Alle variabler som inneholder \"Variabel sensorstreng\" vil bli lagt til som en sensor. \n \u2022 Gjenopprett lyslysstyrke: Hvis den er aktivert, gjenopprettes den forrige lysstyrken n\u00e5r du sl\u00e5r p\u00e5 et lys i stedet for enhetens innebygde p\u00e5-niv\u00e5.", - "title": "ISY994 Alternativer" + "title": "ISY-alternativer" } } }, diff --git a/homeassistant/components/locative/translations/da.json b/homeassistant/components/locative/translations/da.json index 2401428eff4..3d4395b89dc 100644 --- a/homeassistant/components/locative/translations/da.json +++ b/homeassistant/components/locative/translations/da.json @@ -1,7 +1,7 @@ { "config": { "create_entry": { - "default": "For at sende lokationer til Home Assistant skal du konfigurere webhook funktionen i Locative applicationen.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n \n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + "default": "For at sende lokaliteter til Home Assistant skal du konfigurere webhook-funktionen i Locative-applicationen.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n \n Se [dokumentationen]({docs_url}) for yderligere oplysninger." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/translations/fr.json b/homeassistant/components/mailgun/translations/fr.json index 80c961babba..9d114cec058 100644 --- a/homeassistant/components/mailgun/translations/fr.json +++ b/homeassistant/components/mailgun/translations/fr.json @@ -10,7 +10,7 @@ }, "step": { "user": { - "description": "\u00cates-vous s\u00fbr de vouloir configurer Mailgun?", + "description": "Voulez-vous vraiment configurer Mailgun\u00a0?", "title": "Configurer le Webhook Mailgun" } } diff --git a/homeassistant/components/meater/translations/de.json b/homeassistant/components/meater/translations/de.json index ed143e26b4c..9e85c5e56f8 100644 --- a/homeassistant/components/meater/translations/de.json +++ b/homeassistant/components/meater/translations/de.json @@ -6,11 +6,20 @@ "unknown_auth_error": "Unerwarteter Fehler" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Best\u00e4tige das Passwort f\u00fcr das Meater Cloud-Konto {username} ." + }, "user": { "data": { "password": "Passwort", "username": "Benutzername" }, + "data_description": { + "username": "Meater Cloud-Benutzername, normalerweise eine E-Mail-Adresse." + }, "description": "Richte dein Meater Cloud-Konto ein." } } diff --git a/homeassistant/components/meater/translations/el.json b/homeassistant/components/meater/translations/el.json index 2d02110dd49..3114eb17286 100644 --- a/homeassistant/components/meater/translations/el.json +++ b/homeassistant/components/meater/translations/el.json @@ -6,11 +6,20 @@ "unknown_auth_error": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u0395\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03b9\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc Meater Cloud {username}." + }, "user": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, + "data_description": { + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 Meater Cloud, \u03c3\u03c5\u03bd\u03ae\u03b8\u03c9\u03c2 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 email." + }, "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Meater Cloud." } } diff --git a/homeassistant/components/meater/translations/fr.json b/homeassistant/components/meater/translations/fr.json index 9940cb6e24b..af9b76d722a 100644 --- a/homeassistant/components/meater/translations/fr.json +++ b/homeassistant/components/meater/translations/fr.json @@ -6,11 +6,20 @@ "unknown_auth_error": "Erreur inattendue" }, "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "description": "Confirmez le mot de passe du compte Meater Cloud {username}." + }, "user": { "data": { "password": "Mot de passe", "username": "Nom d'utilisateur" }, + "data_description": { + "username": "Nom d'utilisateur Meater Cloud, g\u00e9n\u00e9ralement une adresse \u00e9lectronique." + }, "description": "Configurez votre compte Meater Cloud." } } diff --git a/homeassistant/components/meater/translations/hu.json b/homeassistant/components/meater/translations/hu.json index fd55bae3bdd..964c240b4e3 100644 --- a/homeassistant/components/meater/translations/hu.json +++ b/homeassistant/components/meater/translations/hu.json @@ -6,11 +6,20 @@ "unknown_auth_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "Er\u0151s\u00edtse meg a Meater Cloud-fi\u00f3k {username} jelszav\u00e1t." + }, "user": { "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, + "data_description": { + "username": "Meater Cloud felhaszn\u00e1l\u00f3n\u00e9v, \u00e1ltal\u00e1ban egy e-mail c\u00edm." + }, "description": "\u00c1ll\u00edtsa be a Meater Cloud fi\u00f3kj\u00e1t." } } diff --git a/homeassistant/components/meater/translations/nl.json b/homeassistant/components/meater/translations/nl.json index a87175c7574..3261272c7f9 100644 --- a/homeassistant/components/meater/translations/nl.json +++ b/homeassistant/components/meater/translations/nl.json @@ -6,6 +6,11 @@ "unknown_auth_error": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + } + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/meater/translations/pl.json b/homeassistant/components/meater/translations/pl.json index 1816069dd34..1afaffb4bb7 100644 --- a/homeassistant/components/meater/translations/pl.json +++ b/homeassistant/components/meater/translations/pl.json @@ -6,11 +6,20 @@ "unknown_auth_error": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Potwierd\u017a has\u0142o do konta Meater Cloud {username}." + }, "user": { "data": { "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" }, + "data_description": { + "username": "Nazwa u\u017cytkownika Meater Cloud, zazwyczaj adres e-mail." + }, "description": "Skonfiguruj swoje konto w Meater Cloud." } } diff --git a/homeassistant/components/meater/translations/pt-BR.json b/homeassistant/components/meater/translations/pt-BR.json index 103f3f76986..4e379482baa 100644 --- a/homeassistant/components/meater/translations/pt-BR.json +++ b/homeassistant/components/meater/translations/pt-BR.json @@ -6,11 +6,20 @@ "unknown_auth_error": "Erro inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "Confirme a senha da conta do Meeater Cloud {username}." + }, "user": { "data": { "password": "Senha", "username": "Usu\u00e1rio" }, + "data_description": { + "username": "Nome de usu\u00e1rio do Meeater Cloud, normalmente um endere\u00e7o de e-mail." + }, "description": "Configure sua conta Meeater Cloud." } } diff --git a/homeassistant/components/meater/translations/ru.json b/homeassistant/components/meater/translations/ru.json index a56f2f8ebbe..182e2d03e33 100644 --- a/homeassistant/components/meater/translations/ru.json +++ b/homeassistant/components/meater/translations/ru.json @@ -6,11 +6,20 @@ "unknown_auth_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Meater Cloud {username}." + }, "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" }, + "data_description": { + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f Meater Cloud, \u043e\u0431\u044b\u0447\u043d\u043e \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b." + }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Meater Cloud." } } diff --git a/homeassistant/components/meater/translations/zh-Hant.json b/homeassistant/components/meater/translations/zh-Hant.json index b04f4a54076..1033f5c993a 100644 --- a/homeassistant/components/meater/translations/zh-Hant.json +++ b/homeassistant/components/meater/translations/zh-Hant.json @@ -6,11 +6,20 @@ "unknown_auth_error": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u78ba\u8a8d Meater Cloud \u5e33\u865f {username} \u5bc6\u78bc\u3002" + }, "user": { "data": { "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, + "data_description": { + "username": "Meater Cloud \u4f7f\u7528\u8005\u540d\u7a31\u3001\u901a\u5e38\u70ba\u96fb\u5b50\u90f5\u4ef6\u4f4d\u5740\u3002" + }, "description": "\u8a2d\u5b9a Meater Cloud \u5e33\u865f\u3002" } } diff --git a/homeassistant/components/owntracks/translations/fr.json b/homeassistant/components/owntracks/translations/fr.json index 9d651c66e94..e2e440cafa5 100644 --- a/homeassistant/components/owntracks/translations/fr.json +++ b/homeassistant/components/owntracks/translations/fr.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "\u00cates-vous s\u00fbr de vouloir configurer OwnTracks?", + "description": "Voulez-vous vraiment configurer OwnTracks\u00a0?", "title": "Configurer OwnTracks" } } diff --git a/homeassistant/components/recorder/translations/no.json b/homeassistant/components/recorder/translations/no.json new file mode 100644 index 00000000000..b1474c13466 --- /dev/null +++ b/homeassistant/components/recorder/translations/no.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "current_recorder_run": "Gjeldende starttid for kj\u00f8ring", + "oldest_recorder_run": "Eldste Run Start Time" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index eb23adaabba..1f7cd74043b 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -14,7 +14,7 @@ "unknown": "Unerwarteter Fehler" }, "progress": { - "email_2fa": "Gib den Code f\u00fcr die Zwei-Faktor-Authentifizierung ein, den du per E-Mail erhalten hast." + "email_2fa": "\u00dcberpr\u00fcfe deine E-Mails auf einen Best\u00e4tigungslink von Simplisafe." }, "step": { "mfa": { diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 0da6f6442e4..0aa7cf4cec1 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -3,16 +3,23 @@ "abort": { "already_configured": "This SimpliSafe account is already in use.", "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", - "reauth_successful": "Re-authentication was successful" + "reauth_successful": "Re-authentication was successful", + "wrong_account": "The user credentials provided do not match this SimpliSafe account." }, "error": { + "2fa_timed_out": "Timed out while waiting for two-factor authentication", + "identifier_exists": "Account already registered", "invalid_auth": "Invalid authentication", + "still_awaiting_mfa": "Still awaiting MFA email click", "unknown": "Unexpected error" }, "progress": { "email_2fa": "Check your email for a verification link from Simplisafe." }, "step": { + "mfa": { + "title": "SimpliSafe Multi-Factor Authentication" + }, "reauth_confirm": { "data": { "password": "Password" @@ -28,10 +35,13 @@ }, "user": { "data": { + "auth_code": "Authorization Code", + "code": "Code (used in Home Assistant UI)", "password": "Password", "username": "Username" }, - "description": "Input your username and password." + "description": "Input your username and password.", + "title": "Fill in your information." } } }, diff --git a/homeassistant/components/simplisafe/translations/fr.json b/homeassistant/components/simplisafe/translations/fr.json index b3456fc35ef..1abfb1c4638 100644 --- a/homeassistant/components/simplisafe/translations/fr.json +++ b/homeassistant/components/simplisafe/translations/fr.json @@ -14,7 +14,7 @@ "unknown": "Erreur inattendue" }, "progress": { - "email_2fa": "Saisissez le code d'authentification \u00e0 deux facteurs qui vous a \u00e9t\u00e9 envoy\u00e9 par courriel." + "email_2fa": "Vous devriez recevoir un lien de v\u00e9rification par courriel envoy\u00e9 par Simplisafe." }, "step": { "mfa": { diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json index d0b743e5ff5..06a7260fd0e 100644 --- a/homeassistant/components/simplisafe/translations/no.json +++ b/homeassistant/components/simplisafe/translations/no.json @@ -14,7 +14,7 @@ "unknown": "Uventet feil" }, "progress": { - "email_2fa": "Skriv inn tofaktorautentiseringskoden\n sendt til deg via e-post." + "email_2fa": "Sjekk e-posten din for en bekreftelseslenke fra Simplisafe." }, "step": { "mfa": { diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json index f4c8f5096cf..ec574217c58 100644 --- a/homeassistant/components/simplisafe/translations/pl.json +++ b/homeassistant/components/simplisafe/translations/pl.json @@ -14,7 +14,7 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "progress": { - "email_2fa": "Wprowad\u017a kod uwierzytelniania dwusk\u0142adnikowego\nwys\u0142any do Ciebie e-mailem." + "email_2fa": "Sprawd\u017a e-mail z linkiem weryfikacyjnym od Simplisafe." }, "step": { "mfa": { diff --git a/homeassistant/components/simplisafe/translations/pt-BR.json b/homeassistant/components/simplisafe/translations/pt-BR.json index 5257950027b..3cc1703e11c 100644 --- a/homeassistant/components/simplisafe/translations/pt-BR.json +++ b/homeassistant/components/simplisafe/translations/pt-BR.json @@ -14,7 +14,7 @@ "unknown": "Erro inesperado" }, "progress": { - "email_2fa": "Insira o c\u00f3digo de autentica\u00e7\u00e3o de dois fatores\n enviado para voc\u00ea por e-mail." + "email_2fa": "Verifique seu e-mail para obter um link de verifica\u00e7\u00e3o do Simplisafe." }, "step": { "mfa": { diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json index 649378c1d2c..e3cc230962c 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/translations/zh-Hant.json @@ -14,7 +14,7 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "progress": { - "email_2fa": "\u8f38\u5165\u90f5\u4ef6\u6240\u6536\u5230\u7684\u5169\u6b65\u9a5f\u9a57\u8b49\u78bc\u3002" + "email_2fa": "\u8f38\u5165\u90f5\u4ef6\u6240\u6536\u5230 \u7684Simplisafe \u9a57\u8b49\u9023\u7d50\u3002" }, "step": { "mfa": { diff --git a/homeassistant/components/sql/translations/no.json b/homeassistant/components/sql/translations/no.json index 292486d3c0f..ab2554c4fb9 100644 --- a/homeassistant/components/sql/translations/no.json +++ b/homeassistant/components/sql/translations/no.json @@ -13,6 +13,7 @@ "data": { "column": "Kolonne", "db_url": "Database URL", + "name": "Navn", "query": "Velg Sp\u00f8rring", "unit_of_measurement": "M\u00e5leenhet", "value_template": "Verdimal" @@ -20,6 +21,7 @@ "data_description": { "column": "Kolonne for returnert foresp\u00f8rsel for \u00e5 presentere som tilstand", "db_url": "Database-URL, la st\u00e5 tom for \u00e5 bruke standard HA-database", + "name": "Navn som vil bli brukt for Config Entry og ogs\u00e5 sensoren", "query": "Sp\u00f8rsm\u00e5let skal kj\u00f8res, m\u00e5 starte med \"SELECT\"", "unit_of_measurement": "M\u00e5leenhet (valgfritt)", "value_template": "Verdimal (valgfritt)" @@ -38,6 +40,7 @@ "data": { "column": "Kolonne", "db_url": "Database URL", + "name": "Navn", "query": "Velg Sp\u00f8rring", "unit_of_measurement": "M\u00e5leenhet", "value_template": "Verdimal" @@ -45,6 +48,7 @@ "data_description": { "column": "Kolonne for returnert foresp\u00f8rsel for \u00e5 presentere som tilstand", "db_url": "Database-URL, la st\u00e5 tom for \u00e5 bruke standard HA-database", + "name": "Navn som vil bli brukt for Config Entry og ogs\u00e5 sensoren", "query": "Sp\u00f8rsm\u00e5let skal kj\u00f8res, m\u00e5 starte med \"SELECT\"", "unit_of_measurement": "M\u00e5leenhet (valgfritt)", "value_template": "Verdimal (valgfritt)" diff --git a/homeassistant/components/steam_online/translations/no.json b/homeassistant/components/steam_online/translations/no.json index 7e0122dfe18..021218823f5 100644 --- a/homeassistant/components/steam_online/translations/no.json +++ b/homeassistant/components/steam_online/translations/no.json @@ -12,7 +12,7 @@ }, "step": { "reauth_confirm": { - "description": "Steam-integrasjonen m\u00e5 re-autentiseres manuelt \n\n Du finner n\u00f8kkelen din her: https://steamcommunity.com/dev/apikey", + "description": "Steam-integrasjonen m\u00e5 re-autentiseres manuelt \n\n Du finner n\u00f8kkelen din her: {api_key_url}", "title": "Godkjenne integrering p\u00e5 nytt" }, "user": { @@ -20,7 +20,7 @@ "account": "Steam-konto-ID", "api_key": "API-n\u00f8kkel" }, - "description": "Bruk https://steamid.io for \u00e5 finne din Steam-konto-ID" + "description": "Bruk {account_id_url} for \u00e5 finne Steam-konto-ID-en din" } } }, diff --git a/homeassistant/components/traccar/translations/fr.json b/homeassistant/components/traccar/translations/fr.json index ab5d254dd98..79e881b674b 100644 --- a/homeassistant/components/traccar/translations/fr.json +++ b/homeassistant/components/traccar/translations/fr.json @@ -10,7 +10,7 @@ }, "step": { "user": { - "description": "\u00cates-vous s\u00fbr de vouloir configurer Traccar?", + "description": "Voulez-vous vraiment configurer Traccar\u00a0?", "title": "Configurer Traccar" } } diff --git a/homeassistant/components/unifi/translations/el.json b/homeassistant/components/unifi/translations/el.json index b017910c279..6c150fe8133 100644 --- a/homeassistant/components/unifi/translations/el.json +++ b/homeassistant/components/unifi/translations/el.json @@ -26,6 +26,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 UniFi \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + }, "step": { "client_control": { "data": { diff --git a/homeassistant/components/unifi/translations/hu.json b/homeassistant/components/unifi/translations/hu.json index ea4c7484d44..f06c30d5343 100644 --- a/homeassistant/components/unifi/translations/hu.json +++ b/homeassistant/components/unifi/translations/hu.json @@ -26,6 +26,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "Az UniFi integr\u00e1ci\u00f3 nincs be\u00e1ll\u00edtva" + }, "step": { "client_control": { "data": { diff --git a/homeassistant/components/unifi/translations/nl.json b/homeassistant/components/unifi/translations/nl.json index 5cadc27117c..8b67bec0364 100644 --- a/homeassistant/components/unifi/translations/nl.json +++ b/homeassistant/components/unifi/translations/nl.json @@ -26,6 +26,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "UniFi-integratie is niet ingesteld" + }, "step": { "client_control": { "data": { diff --git a/homeassistant/components/unifi/translations/no.json b/homeassistant/components/unifi/translations/no.json index ec43a8f295b..2253bbd5023 100644 --- a/homeassistant/components/unifi/translations/no.json +++ b/homeassistant/components/unifi/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "UniFi Network-nettstedet er allerede konfigurert", - "configuration_updated": "Konfigurasjonen er oppdatert.", + "configuration_updated": "Konfigurasjonen er oppdatert", "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { @@ -26,6 +26,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "UniFi-integrasjon er ikke konfigurert" + }, "step": { "client_control": { "data": { diff --git a/homeassistant/components/unifi/translations/pl.json b/homeassistant/components/unifi/translations/pl.json index a52660ac262..40bbc9e47a4 100644 --- a/homeassistant/components/unifi/translations/pl.json +++ b/homeassistant/components/unifi/translations/pl.json @@ -26,6 +26,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "Integracja UniFi nie jest skonfigurowana" + }, "step": { "client_control": { "data": { diff --git a/homeassistant/components/unifi/translations/ru.json b/homeassistant/components/unifi/translations/ru.json index 4d9726ab751..68247ef09e1 100644 --- a/homeassistant/components/unifi/translations/ru.json +++ b/homeassistant/components/unifi/translations/ru.json @@ -26,6 +26,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f UniFi \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430." + }, "step": { "client_control": { "data": { diff --git a/homeassistant/components/unifi/translations/zh-Hant.json b/homeassistant/components/unifi/translations/zh-Hant.json index 1b394bae317..c6cb4724697 100644 --- a/homeassistant/components/unifi/translations/zh-Hant.json +++ b/homeassistant/components/unifi/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "UniFi \u7db2\u8def\u5df2\u7d93\u8a2d\u5b9a", - "configuration_updated": "\u8a2d\u5b9a\u5df2\u66f4\u65b0\u3002", + "configuration_updated": "\u8a2d\u5b9a\u5df2\u66f4\u65b0", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { @@ -26,6 +26,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "UniFi \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a" + }, "step": { "client_control": { "data": { From 29bda196b5e0a90a2bea7e1797742236114afc1c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 2 May 2022 23:53:56 -0500 Subject: [PATCH 0139/3516] Break apart recorder into tasks and core modules (#71222) --- .strict-typing | 2 + homeassistant/components/recorder/__init__.py | 1304 +---------------- homeassistant/components/recorder/const.py | 9 + homeassistant/components/recorder/core.py | 1082 ++++++++++++++ homeassistant/components/recorder/tasks.py | 250 ++++ mypy.ini | 22 + tests/components/history/test_init.py | 25 +- tests/components/recorder/test_history.py | 83 +- tests/components/recorder/test_init.py | 50 +- tests/components/recorder/test_migrate.py | 34 +- tests/components/recorder/test_purge.py | 33 +- tests/components/recorder/test_statistics.py | 12 +- tests/components/recorder/test_util.py | 6 +- .../components/recorder/test_websocket_api.py | 7 +- tests/components/sensor/test_recorder.py | 68 +- 15 files changed, 1583 insertions(+), 1404 deletions(-) create mode 100644 homeassistant/components/recorder/core.py create mode 100644 homeassistant/components/recorder/tasks.py diff --git a/.strict-typing b/.strict-typing index df54a082119..2915f398953 100644 --- a/.strict-typing +++ b/.strict-typing @@ -179,6 +179,7 @@ homeassistant.components.rdw.* homeassistant.components.recollect_waste.* homeassistant.components.recorder homeassistant.components.recorder.const +homeassistant.components.recorder.core homeassistant.components.recorder.backup homeassistant.components.recorder.executor homeassistant.components.recorder.history @@ -189,6 +190,7 @@ homeassistant.components.recorder.repack homeassistant.components.recorder.run_history homeassistant.components.recorder.statistics homeassistant.components.recorder.system_health +homeassistant.components.recorder.tasks homeassistant.components.recorder.util homeassistant.components.recorder.websocket_api homeassistant.components.remote.* diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 1d81495c3a5..f9d462abeea 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -1,55 +1,18 @@ """Support for recording details.""" from __future__ import annotations -import abc -import asyncio -from collections.abc import Callable, Iterable -from dataclasses import dataclass -from datetime import datetime, timedelta import logging -import queue -import sqlite3 -import threading -import time -from typing import Any, TypeVar, cast +from typing import Any -from lru import LRU # pylint: disable=no-name-in-module -from sqlalchemy import create_engine, event as sqlalchemy_event, exc, func, select -from sqlalchemy.engine import Engine -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import scoped_session, sessionmaker -from sqlalchemy.orm.session import Session import voluptuous as vol -from homeassistant.components import persistent_notification -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_EXCLUDE, - EVENT_HOMEASSISTANT_FINAL_WRITE, - EVENT_HOMEASSISTANT_STARTED, - EVENT_HOMEASSISTANT_STOP, - EVENT_STATE_CHANGED, - MATCH_ALL, -) -from homeassistant.core import ( - CALLBACK_TYPE, - CoreState, - Event, - HomeAssistant, - ServiceCall, - callback, -) +from homeassistant.const import CONF_EXCLUDE, EVENT_STATE_CHANGED +from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER, convert_include_exclude_filter, - generate_filter, -) -from homeassistant.helpers.event import ( - async_track_time_change, - async_track_time_interval, - async_track_utc_time_change, ) from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, @@ -57,58 +20,29 @@ from homeassistant.helpers.integration_platform import ( from homeassistant.helpers.service import async_extract_entity_ids from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -import homeassistant.util.dt as dt_util -from . import history, migration, purge, statistics, websocket_api +from . import history, statistics, websocket_api from .const import ( + ATTR_APPLY_FILTER, + ATTR_KEEP_DAYS, + ATTR_REPACK, CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, - DB_WORKER_PREFIX, DOMAIN, - MAX_QUEUE_BACKLOG, + EXCLUDE_ATTRIBUTES, SQLITE_URL_PREFIX, ) -from .executor import DBInterruptibleThreadPoolExecutor -from .models import ( - Base, - EventData, - Events, - StateAttributes, - States, - StatisticData, - StatisticMetaData, - StatisticsRuns, - process_timestamp, -) -from .pool import POOL_SIZE, MutexPool, RecorderPool -from .queries import find_shared_attributes_id, find_shared_data_id -from .run_history import RunHistory -from .util import ( - dburl_to_path, - end_incomplete_runs, - is_second_sunday, - move_away_broken_database, - periodic_db_cleanups, - session_scope, - setup_connection_for_dialect, - validate_or_move_away_sqlite_database, - write_lock_db_sqlite, -) +from .core import Recorder +from .tasks import AddRecorderPlatformTask _LOGGER = logging.getLogger(__name__) -T = TypeVar("T") - -EXCLUDE_ATTRIBUTES = f"{DOMAIN}_exclude_attributes_by_domain" SERVICE_PURGE = "purge" SERVICE_PURGE_ENTITIES = "purge_entities" SERVICE_ENABLE = "enable" SERVICE_DISABLE = "disable" -ATTR_KEEP_DAYS = "keep_days" -ATTR_REPACK = "repack" -ATTR_APPLY_FILTER = "apply_filter" SERVICE_PURGE_SCHEMA = vol.Schema( { @@ -138,26 +72,6 @@ DEFAULT_DB_INTEGRITY_CHECK = True DEFAULT_DB_MAX_RETRIES = 10 DEFAULT_DB_RETRY_WAIT = 3 DEFAULT_COMMIT_INTERVAL = 1 -KEEPALIVE_TIME = 30 - -# Controls how often we clean up -# States and Events objects -EXPIRE_AFTER_COMMITS = 120 - -# The number of attribute ids to cache in memory -# -# Based on: -# - The number of overlapping attributes -# - How frequently states with overlapping attributes will change -# - How much memory our low end hardware has -STATE_ATTRIBUTES_ID_CACHE_SIZE = 2048 -EVENT_DATA_ID_CACHE_SIZE = 2048 - -SHUTDOWN_TASK = object() - - -DB_LOCK_TIMEOUT = 30 -DB_LOCK_QUEUE_CHECK_TIMEOUT = 1 CONF_AUTO_PURGE = "auto_purge" CONF_AUTO_REPACK = "auto_repack" @@ -169,8 +83,6 @@ CONF_PURGE_INTERVAL = "purge_interval" CONF_EVENT_TYPES = "event_types" CONF_COMMIT_INTERVAL = "commit_interval" -INVALIDATED_ERR = "Database connection invalidated" -CONNECTIVITY_ERR = "Error in database connectivity during commit" EXCLUDE_SCHEMA = INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER.extend( {vol.Optional(CONF_EVENT_TYPES): vol.All(cv.ensure_list, [cv.string])} @@ -227,10 +139,6 @@ CONFIG_SCHEMA = vol.Schema( ) -# Pool size must accommodate Recorder thread + All db executors -MAX_DB_EXECUTOR_WORKERS = POOL_SIZE - 1 - - def get_instance(hass: HomeAssistant) -> Recorder: """Get the recorder instance.""" instance: Recorder = hass.data[DATA_INSTANCE] @@ -351,1195 +259,3 @@ def _async_register_services(hass: HomeAssistant, instance: Recorder) -> None: async_handle_disable_service, schema=SERVICE_DISABLE_SCHEMA, ) - - -class RecorderTask(abc.ABC): - """ABC for recorder tasks.""" - - commit_before = True - - @abc.abstractmethod - def run(self, instance: Recorder) -> None: - """Handle the task.""" - - -@dataclass -class ClearStatisticsTask(RecorderTask): - """Object to store statistics_ids which for which to remove statistics.""" - - statistic_ids: list[str] - - def run(self, instance: Recorder) -> None: - """Handle the task.""" - statistics.clear_statistics(instance, self.statistic_ids) - - -@dataclass -class UpdateStatisticsMetadataTask(RecorderTask): - """Object to store statistics_id and unit for update of statistics metadata.""" - - statistic_id: str - unit_of_measurement: str | None - - def run(self, instance: Recorder) -> None: - """Handle the task.""" - statistics.update_statistics_metadata( - instance, self.statistic_id, self.unit_of_measurement - ) - - -@dataclass -class PurgeTask(RecorderTask): - """Object to store information about purge task.""" - - purge_before: datetime - repack: bool - apply_filter: bool - - def run(self, instance: Recorder) -> None: - """Purge the database.""" - assert instance.get_session is not None - - if purge.purge_old_data( - instance, self.purge_before, self.repack, self.apply_filter - ): - with instance.get_session() as session: - instance.run_history.load_from_db(session) - # We always need to do the db cleanups after a purge - # is finished to ensure the WAL checkpoint and other - # tasks happen after a vacuum. - periodic_db_cleanups(instance) - return - # Schedule a new purge task if this one didn't finish - instance.queue.put(PurgeTask(self.purge_before, self.repack, self.apply_filter)) - - -@dataclass -class PurgeEntitiesTask(RecorderTask): - """Object to store entity information about purge task.""" - - entity_filter: Callable[[str], bool] - - def run(self, instance: Recorder) -> None: - """Purge entities from the database.""" - if purge.purge_entity_data(instance, self.entity_filter): - return - # Schedule a new purge task if this one didn't finish - instance.queue.put(PurgeEntitiesTask(self.entity_filter)) - - -@dataclass -class PerodicCleanupTask(RecorderTask): - """An object to insert into the recorder to trigger cleanup tasks when auto purge is disabled.""" - - def run(self, instance: Recorder) -> None: - """Handle the task.""" - periodic_db_cleanups(instance) - - -@dataclass -class StatisticsTask(RecorderTask): - """An object to insert into the recorder queue to run a statistics task.""" - - start: datetime - - def run(self, instance: Recorder) -> None: - """Run statistics task.""" - if statistics.compile_statistics(instance, self.start): - return - # Schedule a new statistics task if this one didn't finish - instance.queue.put(StatisticsTask(self.start)) - - -@dataclass -class ExternalStatisticsTask(RecorderTask): - """An object to insert into the recorder queue to run an external statistics task.""" - - metadata: StatisticMetaData - statistics: Iterable[StatisticData] - - def run(self, instance: Recorder) -> None: - """Run statistics task.""" - if statistics.add_external_statistics(instance, self.metadata, self.statistics): - return - # Schedule a new statistics task if this one didn't finish - instance.queue.put(ExternalStatisticsTask(self.metadata, self.statistics)) - - -@dataclass -class AdjustStatisticsTask(RecorderTask): - """An object to insert into the recorder queue to run an adjust statistics task.""" - - statistic_id: str - start_time: datetime - sum_adjustment: float - - def run(self, instance: Recorder) -> None: - """Run statistics task.""" - if statistics.adjust_statistics( - instance, - self.statistic_id, - self.start_time, - self.sum_adjustment, - ): - return - # Schedule a new adjust statistics task if this one didn't finish - instance.queue.put( - AdjustStatisticsTask( - self.statistic_id, self.start_time, self.sum_adjustment - ) - ) - - -@dataclass -class WaitTask(RecorderTask): - """An object to insert into the recorder queue to tell it set the _queue_watch event.""" - - commit_before = False - - def run(self, instance: Recorder) -> None: - """Handle the task.""" - instance._queue_watch.set() # pylint: disable=[protected-access] - - -@dataclass -class DatabaseLockTask(RecorderTask): - """An object to insert into the recorder queue to prevent writes to the database.""" - - database_locked: asyncio.Event - database_unlock: threading.Event - queue_overflow: bool - - def run(self, instance: Recorder) -> None: - """Handle the task.""" - instance._lock_database(self) # pylint: disable=[protected-access] - - -@dataclass -class StopTask(RecorderTask): - """An object to insert into the recorder queue to stop the event handler.""" - - commit_before = False - - def run(self, instance: Recorder) -> None: - """Handle the task.""" - instance.stop_requested = True - - -@dataclass -class EventTask(RecorderTask): - """An event to be processed.""" - - event: Event - commit_before = False - - def run(self, instance: Recorder) -> None: - """Handle the task.""" - # pylint: disable-next=[protected-access] - instance._process_one_event(self.event) - - -@dataclass -class KeepAliveTask(RecorderTask): - """A keep alive to be sent.""" - - commit_before = False - - def run(self, instance: Recorder) -> None: - """Handle the task.""" - # pylint: disable-next=[protected-access] - instance._send_keep_alive() - - -@dataclass -class CommitTask(RecorderTask): - """Commit the event session.""" - - commit_before = False - - def run(self, instance: Recorder) -> None: - """Handle the task.""" - # pylint: disable-next=[protected-access] - instance._commit_event_session_or_retry() - - -@dataclass -class AddRecorderPlatformTask(RecorderTask): - """Add a recorder platform.""" - - domain: str - platform: Any - commit_before = False - - def run(self, instance: Recorder) -> None: - """Handle the task.""" - hass = instance.hass - domain = self.domain - platform = self.platform - - platforms: dict[str, Any] = hass.data[DOMAIN] - platforms[domain] = platform - if hasattr(self.platform, "exclude_attributes"): - hass.data[EXCLUDE_ATTRIBUTES][domain] = platform.exclude_attributes(hass) - - -COMMIT_TASK = CommitTask() -KEEP_ALIVE_TASK = KeepAliveTask() - - -class Recorder(threading.Thread): - """A threaded recorder class.""" - - stop_requested: bool - - def __init__( - self, - hass: HomeAssistant, - auto_purge: bool, - auto_repack: bool, - keep_days: int, - commit_interval: int, - uri: str, - db_max_retries: int, - db_retry_wait: int, - entity_filter: Callable[[str], bool], - exclude_t: list[str], - exclude_attributes_by_domain: dict[str, set[str]], - ) -> None: - """Initialize the recorder.""" - threading.Thread.__init__(self, name="Recorder") - - self.hass = hass - self.auto_purge = auto_purge - self.auto_repack = auto_repack - self.keep_days = keep_days - self._hass_started: asyncio.Future[object] = asyncio.Future() - self.commit_interval = commit_interval - self.queue: queue.SimpleQueue[RecorderTask] = queue.SimpleQueue() - self.db_url = uri - self.db_max_retries = db_max_retries - self.db_retry_wait = db_retry_wait - self.async_db_ready: asyncio.Future[bool] = asyncio.Future() - self.async_recorder_ready = asyncio.Event() - self._queue_watch = threading.Event() - self.engine: Engine | None = None - self.run_history = RunHistory() - - self.entity_filter = entity_filter - self.exclude_t = exclude_t - - self._commits_without_expire = 0 - self._old_states: dict[str, States] = {} - self._state_attributes_ids: LRU = LRU(STATE_ATTRIBUTES_ID_CACHE_SIZE) - self._event_data_ids: LRU = LRU(EVENT_DATA_ID_CACHE_SIZE) - self._pending_state_attributes: dict[str, StateAttributes] = {} - self._pending_event_data: dict[str, EventData] = {} - self._pending_expunge: list[States] = [] - self.event_session: Session | None = None - self.get_session: Callable[[], Session] | None = None - self._completed_first_database_setup: bool | None = None - self._event_listener: CALLBACK_TYPE | None = None - self.async_migration_event = asyncio.Event() - self.migration_in_progress = False - self._queue_watcher: CALLBACK_TYPE | None = None - self._db_supports_row_number = True - self._database_lock_task: DatabaseLockTask | None = None - self._db_executor: DBInterruptibleThreadPoolExecutor | None = None - self._exclude_attributes_by_domain = exclude_attributes_by_domain - - self._keep_alive_listener: CALLBACK_TYPE | None = None - self._commit_listener: CALLBACK_TYPE | None = None - self._periodic_listener: CALLBACK_TYPE | None = None - self._nightly_listener: CALLBACK_TYPE | None = None - self.enabled = True - - def set_enable(self, enable: bool) -> None: - """Enable or disable recording events and states.""" - self.enabled = enable - - @callback - def async_start_executor(self) -> None: - """Start the executor.""" - self._db_executor = DBInterruptibleThreadPoolExecutor( - thread_name_prefix=DB_WORKER_PREFIX, - max_workers=MAX_DB_EXECUTOR_WORKERS, - shutdown_hook=self._shutdown_pool, - ) - - def _shutdown_pool(self) -> None: - """Close the dbpool connections in the current thread.""" - if self.engine and hasattr(self.engine.pool, "shutdown"): - self.engine.pool.shutdown() - - @callback - def async_initialize(self) -> None: - """Initialize the recorder.""" - self._event_listener = self.hass.bus.async_listen( - MATCH_ALL, self.event_listener, event_filter=self._async_event_filter - ) - self._queue_watcher = async_track_time_interval( - self.hass, self._async_check_queue, timedelta(minutes=10) - ) - - @callback - def _async_keep_alive(self, now: datetime) -> None: - """Queue a keep alive.""" - if self._event_listener: - self.queue.put(KEEP_ALIVE_TASK) - - @callback - def _async_commit(self, now: datetime) -> None: - """Queue a commit.""" - if ( - self._event_listener - and not self._database_lock_task - and self._event_session_has_pending_writes() - ): - self.queue.put(COMMIT_TASK) - - @callback - def async_add_executor_job( - self, target: Callable[..., T], *args: Any - ) -> asyncio.Future[T]: - """Add an executor job from within the event loop.""" - return self.hass.loop.run_in_executor(self._db_executor, target, *args) - - def _stop_executor(self) -> None: - """Stop the executor.""" - assert self._db_executor is not None - self._db_executor.shutdown() - self._db_executor = None - - @callback - def _async_check_queue(self, *_: Any) -> None: - """Periodic check of the queue size to ensure we do not exaust memory. - - The queue grows during migraton or if something really goes wrong. - """ - size = self.queue.qsize() - _LOGGER.debug("Recorder queue size is: %s", size) - if size <= MAX_QUEUE_BACKLOG: - return - _LOGGER.error( - "The recorder backlog queue reached the maximum size of %s events; " - "usually, the system is CPU bound, I/O bound, or the database " - "is corrupt due to a disk problem; The recorder will stop " - "recording events to avoid running out of memory", - MAX_QUEUE_BACKLOG, - ) - self._async_stop_queue_watcher_and_event_listener() - - @callback - def _async_stop_queue_watcher_and_event_listener(self) -> None: - """Stop watching the queue and listening for events.""" - if self._queue_watcher: - self._queue_watcher() - self._queue_watcher = None - if self._event_listener: - self._event_listener() - self._event_listener = None - - @callback - def _async_stop_listeners(self) -> None: - """Stop listeners.""" - self._async_stop_queue_watcher_and_event_listener() - if self._keep_alive_listener: - self._keep_alive_listener() - self._keep_alive_listener = None - if self._commit_listener: - self._commit_listener() - self._commit_listener = None - if self._nightly_listener: - self._nightly_listener() - self._nightly_listener = None - if self._periodic_listener: - self._periodic_listener() - self._periodic_listener = None - - @callback - def _async_event_filter(self, event: Event) -> bool: - """Filter events.""" - if event.event_type in self.exclude_t: - return False - - if (entity_id := event.data.get(ATTR_ENTITY_ID)) is None: - return True - - if isinstance(entity_id, str): - return self.entity_filter(entity_id) - - if isinstance(entity_id, list): - for eid in entity_id: - if self.entity_filter(eid): - return True - return False - - # Unknown what it is. - return True - - def do_adhoc_purge(self, **kwargs: Any) -> None: - """Trigger an adhoc purge retaining keep_days worth of data.""" - keep_days = kwargs.get(ATTR_KEEP_DAYS, self.keep_days) - repack = cast(bool, kwargs[ATTR_REPACK]) - apply_filter = cast(bool, kwargs[ATTR_APPLY_FILTER]) - - purge_before = dt_util.utcnow() - timedelta(days=keep_days) - self.queue.put(PurgeTask(purge_before, repack, apply_filter)) - - def do_adhoc_purge_entities( - self, entity_ids: set[str], domains: list[str], entity_globs: list[str] - ) -> None: - """Trigger an adhoc purge of requested entities.""" - entity_filter = generate_filter(domains, list(entity_ids), [], [], entity_globs) - self.queue.put(PurgeEntitiesTask(entity_filter)) - - def do_adhoc_statistics(self, **kwargs: Any) -> None: - """Trigger an adhoc statistics run.""" - if not (start := kwargs.get("start")): - start = statistics.get_start_time() - self.queue.put(StatisticsTask(start)) - - @callback - def async_register(self) -> None: - """Post connection initialize.""" - - def _empty_queue(event: Event) -> None: - """Empty the queue if its still present at final write.""" - - # If the queue is full of events to be processed because - # the database is so broken that every event results in a retry - # we will never be able to get though the events to shutdown in time. - # - # We drain all the events in the queue and then insert - # an empty one to ensure the next thing the recorder sees - # is a request to shutdown. - while True: - try: - self.queue.get_nowait() - except queue.Empty: - break - self.queue.put(StopTask()) - - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_FINAL_WRITE, _empty_queue) - - async def _async_shutdown(event: Event) -> None: - """Shut down the Recorder.""" - if not self._hass_started.done(): - self._hass_started.set_result(SHUTDOWN_TASK) - self.queue.put(StopTask()) - self._async_stop_listeners() - await self.hass.async_add_executor_job(self.join) - - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_shutdown) - - if self.hass.state == CoreState.running: - self._hass_started.set_result(None) - return - - @callback - def _async_hass_started(event: Event) -> None: - """Notify that hass has started.""" - self._hass_started.set_result(None) - - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, _async_hass_started - ) - - @callback - def async_connection_failed(self) -> None: - """Connect failed tasks.""" - self.async_db_ready.set_result(False) - persistent_notification.async_create( - self.hass, - "The recorder could not start, check [the logs](/config/logs)", - "Recorder", - ) - self._async_stop_listeners() - - @callback - def async_connection_success(self) -> None: - """Connect success tasks.""" - self.async_db_ready.set_result(True) - self.async_start_executor() - - @callback - def _async_recorder_ready(self) -> None: - """Finish start and mark recorder ready.""" - self._async_setup_periodic_tasks() - self.async_recorder_ready.set() - - @callback - def async_nightly_tasks(self, now: datetime) -> None: - """Trigger the purge.""" - if self.auto_purge: - # Purge will schedule the periodic cleanups - # after it completes to ensure it does not happen - # until after the database is vacuumed - repack = self.auto_repack and is_second_sunday(now) - purge_before = dt_util.utcnow() - timedelta(days=self.keep_days) - self.queue.put(PurgeTask(purge_before, repack=repack, apply_filter=False)) - else: - self.queue.put(PerodicCleanupTask()) - - @callback - def async_periodic_statistics(self, now: datetime) -> None: - """Trigger the statistics run. - - Short term statistics run every 5 minutes - """ - start = statistics.get_start_time() - self.queue.put(StatisticsTask(start)) - - @callback - def async_adjust_statistics( - self, statistic_id: str, start_time: datetime, sum_adjustment: float - ) -> None: - """Adjust statistics.""" - self.queue.put(AdjustStatisticsTask(statistic_id, start_time, sum_adjustment)) - - @callback - def async_clear_statistics(self, statistic_ids: list[str]) -> None: - """Clear statistics for a list of statistic_ids.""" - self.queue.put(ClearStatisticsTask(statistic_ids)) - - @callback - def async_update_statistics_metadata( - self, statistic_id: str, unit_of_measurement: str | None - ) -> None: - """Update statistics metadata for a statistic_id.""" - self.queue.put(UpdateStatisticsMetadataTask(statistic_id, unit_of_measurement)) - - @callback - def async_external_statistics( - self, metadata: StatisticMetaData, stats: Iterable[StatisticData] - ) -> None: - """Schedule external statistics.""" - self.queue.put(ExternalStatisticsTask(metadata, stats)) - - @callback - def using_sqlite(self) -> bool: - """Return if recorder uses sqlite as the engine.""" - return bool(self.engine and self.engine.dialect.name == "sqlite") - - @callback - def _async_setup_periodic_tasks(self) -> None: - """Prepare periodic tasks.""" - if self.hass.is_stopping or not self.get_session: - # Home Assistant is shutting down - return - - # If the db is using a socket connection, we need to keep alive - # to prevent errors from unexpected disconnects - if not self.using_sqlite(): - self._keep_alive_listener = async_track_time_interval( - self.hass, self._async_keep_alive, timedelta(seconds=KEEPALIVE_TIME) - ) - - # If the commit interval is not 0, we need to commit periodically - if self.commit_interval: - self._commit_listener = async_track_time_interval( - self.hass, self._async_commit, timedelta(seconds=self.commit_interval) - ) - - # Run nightly tasks at 4:12am - self._nightly_listener = async_track_time_change( - self.hass, self.async_nightly_tasks, hour=4, minute=12, second=0 - ) - - # Compile short term statistics every 5 minutes - self._periodic_listener = async_track_utc_time_change( - self.hass, self.async_periodic_statistics, minute=range(0, 60, 5), second=10 - ) - - async def _async_wait_for_started(self) -> object | None: - """Wait for the hass started future.""" - return await self._hass_started - - def _wait_startup_or_shutdown(self) -> object | None: - """Wait for startup or shutdown before starting.""" - return asyncio.run_coroutine_threadsafe( - self._async_wait_for_started(), self.hass.loop - ).result() - - def run(self) -> None: - """Start processing events to save.""" - current_version = self._setup_recorder() - - if current_version is None: - self.hass.add_job(self.async_connection_failed) - return - - schema_is_current = migration.schema_is_current(current_version) - if schema_is_current: - self._setup_run() - else: - self.migration_in_progress = True - - self.hass.add_job(self.async_connection_success) - - # If shutdown happened before Home Assistant finished starting - if self._wait_startup_or_shutdown() is SHUTDOWN_TASK: - self.migration_in_progress = False - # Make sure we cleanly close the run if - # we restart before startup finishes - self._shutdown() - return - - # We wait to start the migration until startup has finished - # since it can be cpu intensive and we do not want it to compete - # with startup which is also cpu intensive - if not schema_is_current: - if self._migrate_schema_and_setup_run(current_version): - if not self._event_listener: - # If the schema migration takes so long that the end - # queue watcher safety kicks in because MAX_QUEUE_BACKLOG - # is reached, we need to reinitialize the listener. - self.hass.add_job(self.async_initialize) - else: - persistent_notification.create( - self.hass, - "The database migration failed, check [the logs](/config/logs)." - "Database Migration Failed", - "recorder_database_migration", - ) - self._shutdown() - return - - _LOGGER.debug("Recorder processing the queue") - self.hass.add_job(self._async_recorder_ready) - self._run_event_loop() - - def _run_event_loop(self) -> None: - """Run the event loop for the recorder.""" - # Use a session for the event read loop - # with a commit every time the event time - # has changed. This reduces the disk io. - self.stop_requested = False - while not self.stop_requested: - task = self.queue.get() - _LOGGER.debug("Processing task: %s", task) - try: - self._process_one_task_or_recover(task) - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error while processing event %s: %s", task, err) - - self._shutdown() - - def _process_one_task_or_recover(self, task: RecorderTask) -> None: - """Process an event, reconnect, or recover a malformed database.""" - try: - # If its not an event, commit everything - # that is pending before running the task - if task.commit_before: - self._commit_event_session_or_retry() - return task.run(self) - except exc.DatabaseError as err: - if self._handle_database_error(err): - return - _LOGGER.exception( - "Unhandled database error while processing task %s: %s", task, err - ) - except SQLAlchemyError as err: - _LOGGER.exception("SQLAlchemyError error processing task %s: %s", task, err) - - # Reset the session if an SQLAlchemyError (including DatabaseError) - # happens to rollback and recover - self._reopen_event_session() - - def _setup_recorder(self) -> None | int: - """Create connect to the database and get the schema version.""" - tries = 1 - - while tries <= self.db_max_retries: - try: - self._setup_connection() - return migration.get_schema_version(self) - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception( - "Error during connection setup: %s (retrying in %s seconds)", - err, - self.db_retry_wait, - ) - tries += 1 - time.sleep(self.db_retry_wait) - - return None - - @callback - def _async_migration_started(self) -> None: - """Set the migration started event.""" - self.async_migration_event.set() - - def _migrate_schema_and_setup_run(self, current_version: int) -> bool: - """Migrate schema to the latest version.""" - persistent_notification.create( - self.hass, - "System performance will temporarily degrade during the database upgrade. Do not power down or restart the system until the upgrade completes. Integrations that read the database, such as logbook and history, may return inconsistent results until the upgrade completes.", - "Database upgrade in progress", - "recorder_database_migration", - ) - self.hass.add_job(self._async_migration_started) - - try: - migration.migrate_schema(self, current_version) - except exc.DatabaseError as err: - if self._handle_database_error(err): - return True - _LOGGER.exception("Database error during schema migration") - return False - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error during schema migration") - return False - else: - self._setup_run() - return True - finally: - self.migration_in_progress = False - persistent_notification.dismiss(self.hass, "recorder_database_migration") - - def _lock_database(self, task: DatabaseLockTask) -> None: - @callback - def _async_set_database_locked(task: DatabaseLockTask) -> None: - task.database_locked.set() - - with write_lock_db_sqlite(self): - # Notify that lock is being held, wait until database can be used again. - self.hass.add_job(_async_set_database_locked, task) - while not task.database_unlock.wait(timeout=DB_LOCK_QUEUE_CHECK_TIMEOUT): - if self.queue.qsize() > MAX_QUEUE_BACKLOG * 0.9: - _LOGGER.warning( - "Database queue backlog reached more than 90% of maximum queue " - "length while waiting for backup to finish; recorder will now " - "resume writing to database. The backup can not be trusted and " - "must be restarted" - ) - task.queue_overflow = True - break - _LOGGER.info( - "Database queue backlog reached %d entries during backup", - self.queue.qsize(), - ) - - def _process_one_event(self, event: Event) -> None: - if not self.enabled: - return - if event.event_type == EVENT_STATE_CHANGED: - self._process_state_changed_event_into_session(event) - else: - self._process_non_state_changed_event_into_session(event) - # Commit if the commit interval is zero - if not self.commit_interval: - self._commit_event_session_or_retry() - - def _find_shared_attr_in_db(self, attr_hash: int, shared_attrs: str) -> int | None: - """Find shared attributes in the db from the hash and shared_attrs.""" - # - # Avoid the event session being flushed since it will - # commit all the pending events and states to the database. - # - # The lookup has already have checked to see if the data is cached - # or going to be written in the next commit so there is no - # need to flush before checking the database. - # - assert self.event_session is not None - with self.event_session.no_autoflush: - if attributes_id := self.event_session.execute( - find_shared_attributes_id(attr_hash, shared_attrs) - ).first(): - return cast(int, attributes_id[0]) - return None - - def _find_shared_data_in_db(self, data_hash: int, shared_data: str) -> int | None: - """Find shared event data in the db from the hash and shared_attrs.""" - # - # Avoid the event session being flushed since it will - # commit all the pending events and states to the database. - # - # The lookup has already have checked to see if the data is cached - # or going to be written in the next commit so there is no - # need to flush before checking the database. - # - assert self.event_session is not None - with self.event_session.no_autoflush: - if data_id := self.event_session.execute( - find_shared_data_id(data_hash, shared_data) - ).first(): - return cast(int, data_id[0]) - return None - - def _process_non_state_changed_event_into_session(self, event: Event) -> None: - """Process any event into the session except state changed.""" - assert self.event_session is not None - dbevent = Events.from_event(event) - if not event.data: - self.event_session.add(dbevent) - return - - try: - shared_data = EventData.shared_data_from_event(event) - except (TypeError, ValueError) as ex: - _LOGGER.warning("Event is not JSON serializable: %s: %s", event, ex) - return - - # Matching attributes found in the pending commit - if pending_event_data := self._pending_event_data.get(shared_data): - dbevent.event_data_rel = pending_event_data - # Matching attributes id found in the cache - elif data_id := self._event_data_ids.get(shared_data): - dbevent.data_id = data_id - else: - data_hash = EventData.hash_shared_data(shared_data) - # Matching attributes found in the database - if data_id := self._find_shared_data_in_db(data_hash, shared_data): - self._event_data_ids[shared_data] = dbevent.data_id = data_id - # No matching attributes found, save them in the DB - else: - dbevent_data = EventData(shared_data=shared_data, hash=data_hash) - dbevent.event_data_rel = self._pending_event_data[ - shared_data - ] = dbevent_data - self.event_session.add(dbevent_data) - - self.event_session.add(dbevent) - - def _process_state_changed_event_into_session(self, event: Event) -> None: - """Process a state_changed event into the session.""" - assert self.event_session is not None - try: - dbstate = States.from_event(event) - shared_attrs = StateAttributes.shared_attrs_from_event( - event, self._exclude_attributes_by_domain - ) - except (TypeError, ValueError) as ex: - _LOGGER.warning( - "State is not JSON serializable: %s: %s", - event.data.get("new_state"), - ex, - ) - return - - dbstate.attributes = None - # Matching attributes found in the pending commit - if pending_attributes := self._pending_state_attributes.get(shared_attrs): - dbstate.state_attributes = pending_attributes - # Matching attributes id found in the cache - elif attributes_id := self._state_attributes_ids.get(shared_attrs): - dbstate.attributes_id = attributes_id - else: - attr_hash = StateAttributes.hash_shared_attrs(shared_attrs) - # Matching attributes found in the database - if attributes_id := self._find_shared_attr_in_db(attr_hash, shared_attrs): - dbstate.attributes_id = attributes_id - self._state_attributes_ids[shared_attrs] = attributes_id - # No matching attributes found, save them in the DB - else: - dbstate_attributes = StateAttributes( - shared_attrs=shared_attrs, hash=attr_hash - ) - dbstate.state_attributes = dbstate_attributes - self._pending_state_attributes[shared_attrs] = dbstate_attributes - self.event_session.add(dbstate_attributes) - - if old_state := self._old_states.pop(dbstate.entity_id, None): - if old_state.state_id: - dbstate.old_state_id = old_state.state_id - else: - dbstate.old_state = old_state - if event.data.get("new_state"): - self._old_states[dbstate.entity_id] = dbstate - self._pending_expunge.append(dbstate) - else: - dbstate.state = None - self.event_session.add(dbstate) - - def _handle_database_error(self, err: Exception) -> bool: - """Handle a database error that may result in moving away the corrupt db.""" - if isinstance(err.__cause__, sqlite3.DatabaseError): - _LOGGER.exception( - "Unrecoverable sqlite3 database corruption detected: %s", err - ) - self._handle_sqlite_corruption() - return True - return False - - def _event_session_has_pending_writes(self) -> bool: - return bool( - self.event_session and (self.event_session.new or self.event_session.dirty) - ) - - def _commit_event_session_or_retry(self) -> None: - """Commit the event session if there is work to do.""" - if not self._event_session_has_pending_writes(): - return - tries = 1 - while tries <= self.db_max_retries: - try: - self._commit_event_session() - return - except (exc.InternalError, exc.OperationalError) as err: - _LOGGER.error( - "%s: Error executing query: %s. (retrying in %s seconds)", - INVALIDATED_ERR if err.connection_invalidated else CONNECTIVITY_ERR, - err, - self.db_retry_wait, - ) - if tries == self.db_max_retries: - raise - - tries += 1 - time.sleep(self.db_retry_wait) - - def _commit_event_session(self) -> None: - assert self.event_session is not None - self._commits_without_expire += 1 - - if self._pending_expunge: - self.event_session.flush() - for dbstate in self._pending_expunge: - # Expunge the state so its not expired - # until we use it later for dbstate.old_state - if dbstate in self.event_session: - self.event_session.expunge(dbstate) - self._pending_expunge = [] - self.event_session.commit() - - # We just committed the state attributes to the database - # and we now know the attributes_ids. We can save - # many selects for matching attributes by loading them - # into the LRU cache now. - for state_attr in self._pending_state_attributes.values(): - self._state_attributes_ids[ - state_attr.shared_attrs - ] = state_attr.attributes_id - self._pending_state_attributes = {} - for event_data in self._pending_event_data.values(): - self._event_data_ids[event_data.shared_data] = event_data.data_id - self._pending_event_data = {} - - # Expire is an expensive operation (frequently more expensive - # than the flush and commit itself) so we only - # do it after EXPIRE_AFTER_COMMITS commits - if self._commits_without_expire >= EXPIRE_AFTER_COMMITS: - self._commits_without_expire = 0 - self.event_session.expire_all() - - def _handle_sqlite_corruption(self) -> None: - """Handle the sqlite3 database being corrupt.""" - self._close_event_session() - self._close_connection() - move_away_broken_database(dburl_to_path(self.db_url)) - self.run_history.reset() - self._setup_recorder() - self._setup_run() - - def _close_event_session(self) -> None: - """Close the event session.""" - self._old_states = {} - self._state_attributes_ids = {} - self._event_data_ids = {} - self._pending_state_attributes = {} - self._pending_event_data = {} - - if not self.event_session: - return - - try: - self.event_session.rollback() - self.event_session.close() - except SQLAlchemyError as err: - _LOGGER.exception( - "Error while rolling back and closing the event session: %s", err - ) - - def _reopen_event_session(self) -> None: - """Rollback the event session and reopen it after a failure.""" - self._close_event_session() - self._open_event_session() - - def _open_event_session(self) -> None: - """Open the event session.""" - assert self.get_session is not None - self.event_session = self.get_session() - self.event_session.expire_on_commit = False - - def _send_keep_alive(self) -> None: - """Send a keep alive to keep the db connection open.""" - assert self.event_session is not None - _LOGGER.debug("Sending keepalive") - self.event_session.connection().scalar(select([1])) - - @callback - def event_listener(self, event: Event) -> None: - """Listen for new events and put them in the process queue.""" - self.queue.put(EventTask(event)) - - def block_till_done(self) -> None: - """Block till all events processed. - - This is only called in tests. - - This only blocks until the queue is empty - which does not mean the recorder is done. - - Call tests.common's wait_recording_done - after calling this to ensure the data - is in the database. - """ - self._queue_watch.clear() - self.queue.put(WaitTask()) - self._queue_watch.wait() - - async def lock_database(self) -> bool: - """Lock database so it can be backed up safely.""" - if not self.using_sqlite(): - _LOGGER.debug( - "Not a SQLite database or not connected, locking not necessary" - ) - return True - - if self._database_lock_task: - _LOGGER.warning("Database already locked") - return False - - database_locked = asyncio.Event() - task = DatabaseLockTask(database_locked, threading.Event(), False) - self.queue.put(task) - try: - await asyncio.wait_for(database_locked.wait(), timeout=DB_LOCK_TIMEOUT) - except asyncio.TimeoutError as err: - task.database_unlock.set() - raise TimeoutError( - f"Could not lock database within {DB_LOCK_TIMEOUT} seconds." - ) from err - self._database_lock_task = task - return True - - @callback - def unlock_database(self) -> bool: - """Unlock database. - - Returns true if database lock has been held throughout the process. - """ - if not self.using_sqlite(): - _LOGGER.debug( - "Not a SQLite database or not connected, unlocking not necessary" - ) - return True - - if not self._database_lock_task: - _LOGGER.warning("Database currently not locked") - return False - - self._database_lock_task.database_unlock.set() - success = not self._database_lock_task.queue_overflow - - self._database_lock_task = None - - return success - - def _setup_connection(self) -> None: - """Ensure database is ready to fly.""" - kwargs: dict[str, Any] = {} - self._completed_first_database_setup = False - - def setup_recorder_connection( - dbapi_connection: Any, connection_record: Any - ) -> None: - """Dbapi specific connection settings.""" - assert self.engine is not None - setup_connection_for_dialect( - self, - self.engine.dialect.name, - dbapi_connection, - not self._completed_first_database_setup, - ) - self._completed_first_database_setup = True - - if self.db_url == SQLITE_URL_PREFIX or ":memory:" in self.db_url: - kwargs["connect_args"] = {"check_same_thread": False} - kwargs["poolclass"] = MutexPool - MutexPool.pool_lock = threading.RLock() - kwargs["pool_reset_on_return"] = None - elif self.db_url.startswith(SQLITE_URL_PREFIX): - kwargs["poolclass"] = RecorderPool - else: - kwargs["echo"] = False - - if self._using_file_sqlite: - validate_or_move_away_sqlite_database(self.db_url) - - self.engine = create_engine(self.db_url, **kwargs, future=True) - - sqlalchemy_event.listen(self.engine, "connect", setup_recorder_connection) - - Base.metadata.create_all(self.engine) - self.get_session = scoped_session(sessionmaker(bind=self.engine, future=True)) - _LOGGER.debug("Connected to recorder database") - - @property - def _using_file_sqlite(self) -> bool: - """Short version to check if we are using sqlite3 as a file.""" - return self.db_url != SQLITE_URL_PREFIX and self.db_url.startswith( - SQLITE_URL_PREFIX - ) - - def _close_connection(self) -> None: - """Close the connection.""" - assert self.engine is not None - self.engine.dispose() - self.engine = None - self.get_session = None - - def _setup_run(self) -> None: - """Log the start of the current run and schedule any needed jobs.""" - assert self.get_session is not None - with session_scope(session=self.get_session()) as session: - end_incomplete_runs(session, self.run_history.recording_start) - self.run_history.start(session) - self._schedule_compile_missing_statistics(session) - - self._open_event_session() - - def _schedule_compile_missing_statistics(self, session: Session) -> None: - """Add tasks for missing statistics runs.""" - now = dt_util.utcnow() - last_period_minutes = now.minute - now.minute % 5 - last_period = now.replace(minute=last_period_minutes, second=0, microsecond=0) - start = now - timedelta(days=self.keep_days) - start = start.replace(minute=0, second=0, microsecond=0) - - # Find the newest statistics run, if any - if last_run := session.query(func.max(StatisticsRuns.start)).scalar(): - start = max(start, process_timestamp(last_run) + timedelta(minutes=5)) - - # Add tasks - while start < last_period: - end = start + timedelta(minutes=5) - _LOGGER.debug("Compiling missing statistics for %s-%s", start, end) - self.queue.put(StatisticsTask(start)) - start = end - - def _end_session(self) -> None: - """End the recorder session.""" - if self.event_session is None: - return - try: - self.run_history.end(self.event_session) - self._commit_event_session_or_retry() - self.event_session.close() - except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error saving the event session during shutdown: %s", err) - - self.run_history.clear() - - def _shutdown(self) -> None: - """Save end time for current run.""" - self.hass.add_job(self._async_stop_listeners) - self._stop_executor() - self._end_session() - self._close_connection() - - @property - def recording(self) -> bool: - """Return if the recorder is recording.""" - return self._event_listener is not None diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index 593710a10dd..5b623ee5e04 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -28,3 +28,12 @@ DB_WORKER_PREFIX = "DbWorker" JSON_DUMP: Final = partial(json.dumps, cls=JSONEncoder, separators=(",", ":")) ALL_DOMAIN_EXCLUDE_ATTRS = {ATTR_ATTRIBUTION, ATTR_RESTORED, ATTR_SUPPORTED_FEATURES} + +ATTR_KEEP_DAYS = "keep_days" +ATTR_REPACK = "repack" +ATTR_APPLY_FILTER = "apply_filter" + +KEEPALIVE_TIME = 30 + + +EXCLUDE_ATTRIBUTES = f"{DOMAIN}_exclude_attributes_by_domain" diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py new file mode 100644 index 00000000000..b96eee2c67f --- /dev/null +++ b/homeassistant/components/recorder/core.py @@ -0,0 +1,1082 @@ +"""Support for recording details.""" +from __future__ import annotations + +import asyncio +from collections.abc import Callable, Iterable +from datetime import datetime, timedelta +import logging +import queue +import sqlite3 +import threading +import time +from typing import Any, TypeVar, cast + +from lru import LRU # pylint: disable=no-name-in-module +from sqlalchemy import create_engine, event as sqlalchemy_event, exc, func, select +from sqlalchemy.engine import Engine +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.orm.session import Session + +from homeassistant.components import persistent_notification +from homeassistant.const import ( + ATTR_ENTITY_ID, + EVENT_HOMEASSISTANT_FINAL_WRITE, + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, + EVENT_STATE_CHANGED, + MATCH_ALL, +) +from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback +from homeassistant.helpers.entityfilter import generate_filter +from homeassistant.helpers.event import ( + async_track_time_change, + async_track_time_interval, + async_track_utc_time_change, +) +import homeassistant.util.dt as dt_util + +from . import migration, statistics +from .const import ( + ATTR_APPLY_FILTER, + ATTR_KEEP_DAYS, + ATTR_REPACK, + DB_WORKER_PREFIX, + KEEPALIVE_TIME, + MAX_QUEUE_BACKLOG, + SQLITE_URL_PREFIX, +) +from .executor import DBInterruptibleThreadPoolExecutor +from .models import ( + Base, + EventData, + Events, + StateAttributes, + States, + StatisticData, + StatisticMetaData, + StatisticsRuns, + process_timestamp, +) +from .pool import POOL_SIZE, MutexPool, RecorderPool +from .queries import find_shared_attributes_id, find_shared_data_id +from .run_history import RunHistory +from .tasks import ( + AdjustStatisticsTask, + ClearStatisticsTask, + CommitTask, + DatabaseLockTask, + EventTask, + ExternalStatisticsTask, + KeepAliveTask, + PerodicCleanupTask, + PurgeEntitiesTask, + PurgeTask, + RecorderTask, + StatisticsTask, + StopTask, + UpdateStatisticsMetadataTask, + WaitTask, +) +from .util import ( + dburl_to_path, + end_incomplete_runs, + is_second_sunday, + move_away_broken_database, + session_scope, + setup_connection_for_dialect, + validate_or_move_away_sqlite_database, + write_lock_db_sqlite, +) + +_LOGGER = logging.getLogger(__name__) + +T = TypeVar("T") + +DEFAULT_URL = "sqlite:///{hass_config_path}" + +# Controls how often we clean up +# States and Events objects +EXPIRE_AFTER_COMMITS = 120 + +# The number of attribute ids to cache in memory +# +# Based on: +# - The number of overlapping attributes +# - How frequently states with overlapping attributes will change +# - How much memory our low end hardware has +STATE_ATTRIBUTES_ID_CACHE_SIZE = 2048 +EVENT_DATA_ID_CACHE_SIZE = 2048 + +SHUTDOWN_TASK = object() + +COMMIT_TASK = CommitTask() +KEEP_ALIVE_TASK = KeepAliveTask() + +DB_LOCK_TIMEOUT = 30 +DB_LOCK_QUEUE_CHECK_TIMEOUT = 1 + + +INVALIDATED_ERR = "Database connection invalidated" +CONNECTIVITY_ERR = "Error in database connectivity during commit" + +# Pool size must accommodate Recorder thread + All db executors +MAX_DB_EXECUTOR_WORKERS = POOL_SIZE - 1 + + +class Recorder(threading.Thread): + """A threaded recorder class.""" + + stop_requested: bool + + def __init__( + self, + hass: HomeAssistant, + auto_purge: bool, + auto_repack: bool, + keep_days: int, + commit_interval: int, + uri: str, + db_max_retries: int, + db_retry_wait: int, + entity_filter: Callable[[str], bool], + exclude_t: list[str], + exclude_attributes_by_domain: dict[str, set[str]], + ) -> None: + """Initialize the recorder.""" + threading.Thread.__init__(self, name="Recorder") + + self.hass = hass + self.auto_purge = auto_purge + self.auto_repack = auto_repack + self.keep_days = keep_days + self._hass_started: asyncio.Future[object] = asyncio.Future() + self.commit_interval = commit_interval + self.queue: queue.SimpleQueue[RecorderTask] = queue.SimpleQueue() + self.db_url = uri + self.db_max_retries = db_max_retries + self.db_retry_wait = db_retry_wait + self.async_db_ready: asyncio.Future[bool] = asyncio.Future() + self.async_recorder_ready = asyncio.Event() + self._queue_watch = threading.Event() + self.engine: Engine | None = None + self.run_history = RunHistory() + + self.entity_filter = entity_filter + self.exclude_t = exclude_t + + self._commits_without_expire = 0 + self._old_states: dict[str, States] = {} + self._state_attributes_ids: LRU = LRU(STATE_ATTRIBUTES_ID_CACHE_SIZE) + self._event_data_ids: LRU = LRU(EVENT_DATA_ID_CACHE_SIZE) + self._pending_state_attributes: dict[str, StateAttributes] = {} + self._pending_event_data: dict[str, EventData] = {} + self._pending_expunge: list[States] = [] + self.event_session: Session | None = None + self.get_session: Callable[[], Session] | None = None + self._completed_first_database_setup: bool | None = None + self._event_listener: CALLBACK_TYPE | None = None + self.async_migration_event = asyncio.Event() + self.migration_in_progress = False + self._queue_watcher: CALLBACK_TYPE | None = None + self._db_supports_row_number = True + self._database_lock_task: DatabaseLockTask | None = None + self._db_executor: DBInterruptibleThreadPoolExecutor | None = None + self._exclude_attributes_by_domain = exclude_attributes_by_domain + + self._keep_alive_listener: CALLBACK_TYPE | None = None + self._commit_listener: CALLBACK_TYPE | None = None + self._periodic_listener: CALLBACK_TYPE | None = None + self._nightly_listener: CALLBACK_TYPE | None = None + self.enabled = True + + def set_enable(self, enable: bool) -> None: + """Enable or disable recording events and states.""" + self.enabled = enable + + @callback + def async_start_executor(self) -> None: + """Start the executor.""" + self._db_executor = DBInterruptibleThreadPoolExecutor( + thread_name_prefix=DB_WORKER_PREFIX, + max_workers=MAX_DB_EXECUTOR_WORKERS, + shutdown_hook=self._shutdown_pool, + ) + + def _shutdown_pool(self) -> None: + """Close the dbpool connections in the current thread.""" + if self.engine and hasattr(self.engine.pool, "shutdown"): + self.engine.pool.shutdown() + + @callback + def async_initialize(self) -> None: + """Initialize the recorder.""" + self._event_listener = self.hass.bus.async_listen( + MATCH_ALL, self.event_listener, event_filter=self._async_event_filter + ) + self._queue_watcher = async_track_time_interval( + self.hass, self._async_check_queue, timedelta(minutes=10) + ) + + @callback + def _async_keep_alive(self, now: datetime) -> None: + """Queue a keep alive.""" + if self._event_listener: + self.queue.put(KEEP_ALIVE_TASK) + + @callback + def _async_commit(self, now: datetime) -> None: + """Queue a commit.""" + if ( + self._event_listener + and not self._database_lock_task + and self._event_session_has_pending_writes() + ): + self.queue.put(COMMIT_TASK) + + @callback + def async_add_executor_job( + self, target: Callable[..., T], *args: Any + ) -> asyncio.Future[T]: + """Add an executor job from within the event loop.""" + return self.hass.loop.run_in_executor(self._db_executor, target, *args) + + def _stop_executor(self) -> None: + """Stop the executor.""" + assert self._db_executor is not None + self._db_executor.shutdown() + self._db_executor = None + + @callback + def _async_check_queue(self, *_: Any) -> None: + """Periodic check of the queue size to ensure we do not exaust memory. + + The queue grows during migraton or if something really goes wrong. + """ + size = self.queue.qsize() + _LOGGER.debug("Recorder queue size is: %s", size) + if size <= MAX_QUEUE_BACKLOG: + return + _LOGGER.error( + "The recorder backlog queue reached the maximum size of %s events; " + "usually, the system is CPU bound, I/O bound, or the database " + "is corrupt due to a disk problem; The recorder will stop " + "recording events to avoid running out of memory", + MAX_QUEUE_BACKLOG, + ) + self._async_stop_queue_watcher_and_event_listener() + + @callback + def _async_stop_queue_watcher_and_event_listener(self) -> None: + """Stop watching the queue and listening for events.""" + if self._queue_watcher: + self._queue_watcher() + self._queue_watcher = None + if self._event_listener: + self._event_listener() + self._event_listener = None + + @callback + def _async_stop_listeners(self) -> None: + """Stop listeners.""" + self._async_stop_queue_watcher_and_event_listener() + if self._keep_alive_listener: + self._keep_alive_listener() + self._keep_alive_listener = None + if self._commit_listener: + self._commit_listener() + self._commit_listener = None + if self._nightly_listener: + self._nightly_listener() + self._nightly_listener = None + if self._periodic_listener: + self._periodic_listener() + self._periodic_listener = None + + @callback + def _async_event_filter(self, event: Event) -> bool: + """Filter events.""" + if event.event_type in self.exclude_t: + return False + + if (entity_id := event.data.get(ATTR_ENTITY_ID)) is None: + return True + + if isinstance(entity_id, str): + return self.entity_filter(entity_id) + + if isinstance(entity_id, list): + for eid in entity_id: + if self.entity_filter(eid): + return True + return False + + # Unknown what it is. + return True + + def do_adhoc_purge(self, **kwargs: Any) -> None: + """Trigger an adhoc purge retaining keep_days worth of data.""" + keep_days = kwargs.get(ATTR_KEEP_DAYS, self.keep_days) + repack = cast(bool, kwargs[ATTR_REPACK]) + apply_filter = cast(bool, kwargs[ATTR_APPLY_FILTER]) + + purge_before = dt_util.utcnow() - timedelta(days=keep_days) + self.queue.put(PurgeTask(purge_before, repack, apply_filter)) + + def do_adhoc_purge_entities( + self, entity_ids: set[str], domains: list[str], entity_globs: list[str] + ) -> None: + """Trigger an adhoc purge of requested entities.""" + entity_filter = generate_filter(domains, list(entity_ids), [], [], entity_globs) + self.queue.put(PurgeEntitiesTask(entity_filter)) + + def do_adhoc_statistics(self, **kwargs: Any) -> None: + """Trigger an adhoc statistics run.""" + if not (start := kwargs.get("start")): + start = statistics.get_start_time() + self.queue.put(StatisticsTask(start)) + + @callback + def async_register(self) -> None: + """Post connection initialize.""" + + def _empty_queue(event: Event) -> None: + """Empty the queue if its still present at final write.""" + + # If the queue is full of events to be processed because + # the database is so broken that every event results in a retry + # we will never be able to get though the events to shutdown in time. + # + # We drain all the events in the queue and then insert + # an empty one to ensure the next thing the recorder sees + # is a request to shutdown. + while True: + try: + self.queue.get_nowait() + except queue.Empty: + break + self.queue.put(StopTask()) + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_FINAL_WRITE, _empty_queue) + + async def _async_shutdown(event: Event) -> None: + """Shut down the Recorder.""" + if not self._hass_started.done(): + self._hass_started.set_result(SHUTDOWN_TASK) + self.queue.put(StopTask()) + self._async_stop_listeners() + await self.hass.async_add_executor_job(self.join) + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_shutdown) + + if self.hass.state == CoreState.running: + self._hass_started.set_result(None) + return + + @callback + def _async_hass_started(event: Event) -> None: + """Notify that hass has started.""" + self._hass_started.set_result(None) + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, _async_hass_started + ) + + @callback + def async_connection_failed(self) -> None: + """Connect failed tasks.""" + self.async_db_ready.set_result(False) + persistent_notification.async_create( + self.hass, + "The recorder could not start, check [the logs](/config/logs)", + "Recorder", + ) + self._async_stop_listeners() + + @callback + def async_connection_success(self) -> None: + """Connect success tasks.""" + self.async_db_ready.set_result(True) + self.async_start_executor() + + @callback + def _async_recorder_ready(self) -> None: + """Finish start and mark recorder ready.""" + self._async_setup_periodic_tasks() + self.async_recorder_ready.set() + + @callback + def async_nightly_tasks(self, now: datetime) -> None: + """Trigger the purge.""" + if self.auto_purge: + # Purge will schedule the periodic cleanups + # after it completes to ensure it does not happen + # until after the database is vacuumed + repack = self.auto_repack and is_second_sunday(now) + purge_before = dt_util.utcnow() - timedelta(days=self.keep_days) + self.queue.put(PurgeTask(purge_before, repack=repack, apply_filter=False)) + else: + self.queue.put(PerodicCleanupTask()) + + @callback + def async_periodic_statistics(self, now: datetime) -> None: + """Trigger the statistics run. + + Short term statistics run every 5 minutes + """ + start = statistics.get_start_time() + self.queue.put(StatisticsTask(start)) + + @callback + def async_adjust_statistics( + self, statistic_id: str, start_time: datetime, sum_adjustment: float + ) -> None: + """Adjust statistics.""" + self.queue.put(AdjustStatisticsTask(statistic_id, start_time, sum_adjustment)) + + @callback + def async_clear_statistics(self, statistic_ids: list[str]) -> None: + """Clear statistics for a list of statistic_ids.""" + self.queue.put(ClearStatisticsTask(statistic_ids)) + + @callback + def async_update_statistics_metadata( + self, statistic_id: str, unit_of_measurement: str | None + ) -> None: + """Update statistics metadata for a statistic_id.""" + self.queue.put(UpdateStatisticsMetadataTask(statistic_id, unit_of_measurement)) + + @callback + def async_external_statistics( + self, metadata: StatisticMetaData, stats: Iterable[StatisticData] + ) -> None: + """Schedule external statistics.""" + self.queue.put(ExternalStatisticsTask(metadata, stats)) + + @callback + def using_sqlite(self) -> bool: + """Return if recorder uses sqlite as the engine.""" + return bool(self.engine and self.engine.dialect.name == "sqlite") + + @callback + def _async_setup_periodic_tasks(self) -> None: + """Prepare periodic tasks.""" + if self.hass.is_stopping or not self.get_session: + # Home Assistant is shutting down + return + + # If the db is using a socket connection, we need to keep alive + # to prevent errors from unexpected disconnects + if not self.using_sqlite(): + self._keep_alive_listener = async_track_time_interval( + self.hass, self._async_keep_alive, timedelta(seconds=KEEPALIVE_TIME) + ) + + # If the commit interval is not 0, we need to commit periodically + if self.commit_interval: + self._commit_listener = async_track_time_interval( + self.hass, self._async_commit, timedelta(seconds=self.commit_interval) + ) + + # Run nightly tasks at 4:12am + self._nightly_listener = async_track_time_change( + self.hass, self.async_nightly_tasks, hour=4, minute=12, second=0 + ) + + # Compile short term statistics every 5 minutes + self._periodic_listener = async_track_utc_time_change( + self.hass, self.async_periodic_statistics, minute=range(0, 60, 5), second=10 + ) + + async def _async_wait_for_started(self) -> object | None: + """Wait for the hass started future.""" + return await self._hass_started + + def _wait_startup_or_shutdown(self) -> object | None: + """Wait for startup or shutdown before starting.""" + return asyncio.run_coroutine_threadsafe( + self._async_wait_for_started(), self.hass.loop + ).result() + + def run(self) -> None: + """Start processing events to save.""" + current_version = self._setup_recorder() + + if current_version is None: + self.hass.add_job(self.async_connection_failed) + return + + schema_is_current = migration.schema_is_current(current_version) + if schema_is_current: + self._setup_run() + else: + self.migration_in_progress = True + + self.hass.add_job(self.async_connection_success) + + # If shutdown happened before Home Assistant finished starting + if self._wait_startup_or_shutdown() is SHUTDOWN_TASK: + self.migration_in_progress = False + # Make sure we cleanly close the run if + # we restart before startup finishes + self._shutdown() + return + + # We wait to start the migration until startup has finished + # since it can be cpu intensive and we do not want it to compete + # with startup which is also cpu intensive + if not schema_is_current: + if self._migrate_schema_and_setup_run(current_version): + if not self._event_listener: + # If the schema migration takes so long that the end + # queue watcher safety kicks in because MAX_QUEUE_BACKLOG + # is reached, we need to reinitialize the listener. + self.hass.add_job(self.async_initialize) + else: + persistent_notification.create( + self.hass, + "The database migration failed, check [the logs](/config/logs)." + "Database Migration Failed", + "recorder_database_migration", + ) + self._shutdown() + return + + _LOGGER.debug("Recorder processing the queue") + self.hass.add_job(self._async_recorder_ready) + self._run_event_loop() + + def _run_event_loop(self) -> None: + """Run the event loop for the recorder.""" + # Use a session for the event read loop + # with a commit every time the event time + # has changed. This reduces the disk io. + self.stop_requested = False + while not self.stop_requested: + task = self.queue.get() + _LOGGER.debug("Processing task: %s", task) + try: + self._process_one_task_or_recover(task) + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Error while processing event %s: %s", task, err) + + self._shutdown() + + def _process_one_task_or_recover(self, task: RecorderTask) -> None: + """Process an event, reconnect, or recover a malformed database.""" + try: + # If its not an event, commit everything + # that is pending before running the task + if task.commit_before: + self._commit_event_session_or_retry() + return task.run(self) + except exc.DatabaseError as err: + if self._handle_database_error(err): + return + _LOGGER.exception( + "Unhandled database error while processing task %s: %s", task, err + ) + except SQLAlchemyError as err: + _LOGGER.exception("SQLAlchemyError error processing task %s: %s", task, err) + + # Reset the session if an SQLAlchemyError (including DatabaseError) + # happens to rollback and recover + self._reopen_event_session() + + def _setup_recorder(self) -> None | int: + """Create connect to the database and get the schema version.""" + tries = 1 + + while tries <= self.db_max_retries: + try: + self._setup_connection() + return migration.get_schema_version(self) + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception( + "Error during connection setup: %s (retrying in %s seconds)", + err, + self.db_retry_wait, + ) + tries += 1 + time.sleep(self.db_retry_wait) + + return None + + @callback + def _async_migration_started(self) -> None: + """Set the migration started event.""" + self.async_migration_event.set() + + def _migrate_schema_and_setup_run(self, current_version: int) -> bool: + """Migrate schema to the latest version.""" + persistent_notification.create( + self.hass, + "System performance will temporarily degrade during the database upgrade. Do not power down or restart the system until the upgrade completes. Integrations that read the database, such as logbook and history, may return inconsistent results until the upgrade completes.", + "Database upgrade in progress", + "recorder_database_migration", + ) + self.hass.add_job(self._async_migration_started) + + try: + migration.migrate_schema(self, current_version) + except exc.DatabaseError as err: + if self._handle_database_error(err): + return True + _LOGGER.exception("Database error during schema migration") + return False + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error during schema migration") + return False + else: + self._setup_run() + return True + finally: + self.migration_in_progress = False + persistent_notification.dismiss(self.hass, "recorder_database_migration") + + def _lock_database(self, task: DatabaseLockTask) -> None: + @callback + def _async_set_database_locked(task: DatabaseLockTask) -> None: + task.database_locked.set() + + with write_lock_db_sqlite(self): + # Notify that lock is being held, wait until database can be used again. + self.hass.add_job(_async_set_database_locked, task) + while not task.database_unlock.wait(timeout=DB_LOCK_QUEUE_CHECK_TIMEOUT): + if self.queue.qsize() > MAX_QUEUE_BACKLOG * 0.9: + _LOGGER.warning( + "Database queue backlog reached more than 90% of maximum queue " + "length while waiting for backup to finish; recorder will now " + "resume writing to database. The backup can not be trusted and " + "must be restarted" + ) + task.queue_overflow = True + break + _LOGGER.info( + "Database queue backlog reached %d entries during backup", + self.queue.qsize(), + ) + + def _process_one_event(self, event: Event) -> None: + if not self.enabled: + return + if event.event_type == EVENT_STATE_CHANGED: + self._process_state_changed_event_into_session(event) + else: + self._process_non_state_changed_event_into_session(event) + # Commit if the commit interval is zero + if not self.commit_interval: + self._commit_event_session_or_retry() + + def _find_shared_attr_in_db(self, attr_hash: int, shared_attrs: str) -> int | None: + """Find shared attributes in the db from the hash and shared_attrs.""" + # + # Avoid the event session being flushed since it will + # commit all the pending events and states to the database. + # + # The lookup has already have checked to see if the data is cached + # or going to be written in the next commit so there is no + # need to flush before checking the database. + # + assert self.event_session is not None + with self.event_session.no_autoflush: + if attributes_id := self.event_session.execute( + find_shared_attributes_id(attr_hash, shared_attrs) + ).first(): + return cast(int, attributes_id[0]) + return None + + def _find_shared_data_in_db(self, data_hash: int, shared_data: str) -> int | None: + """Find shared event data in the db from the hash and shared_attrs.""" + # + # Avoid the event session being flushed since it will + # commit all the pending events and states to the database. + # + # The lookup has already have checked to see if the data is cached + # or going to be written in the next commit so there is no + # need to flush before checking the database. + # + assert self.event_session is not None + with self.event_session.no_autoflush: + if data_id := self.event_session.execute( + find_shared_data_id(data_hash, shared_data) + ).first(): + return cast(int, data_id[0]) + return None + + def _process_non_state_changed_event_into_session(self, event: Event) -> None: + """Process any event into the session except state changed.""" + assert self.event_session is not None + dbevent = Events.from_event(event) + if not event.data: + self.event_session.add(dbevent) + return + + try: + shared_data = EventData.shared_data_from_event(event) + except (TypeError, ValueError) as ex: + _LOGGER.warning("Event is not JSON serializable: %s: %s", event, ex) + return + + # Matching attributes found in the pending commit + if pending_event_data := self._pending_event_data.get(shared_data): + dbevent.event_data_rel = pending_event_data + # Matching attributes id found in the cache + elif data_id := self._event_data_ids.get(shared_data): + dbevent.data_id = data_id + else: + data_hash = EventData.hash_shared_data(shared_data) + # Matching attributes found in the database + if data_id := self._find_shared_data_in_db(data_hash, shared_data): + self._event_data_ids[shared_data] = dbevent.data_id = data_id + # No matching attributes found, save them in the DB + else: + dbevent_data = EventData(shared_data=shared_data, hash=data_hash) + dbevent.event_data_rel = self._pending_event_data[ + shared_data + ] = dbevent_data + self.event_session.add(dbevent_data) + + self.event_session.add(dbevent) + + def _process_state_changed_event_into_session(self, event: Event) -> None: + """Process a state_changed event into the session.""" + assert self.event_session is not None + try: + dbstate = States.from_event(event) + shared_attrs = StateAttributes.shared_attrs_from_event( + event, self._exclude_attributes_by_domain + ) + except (TypeError, ValueError) as ex: + _LOGGER.warning( + "State is not JSON serializable: %s: %s", + event.data.get("new_state"), + ex, + ) + return + + dbstate.attributes = None + # Matching attributes found in the pending commit + if pending_attributes := self._pending_state_attributes.get(shared_attrs): + dbstate.state_attributes = pending_attributes + # Matching attributes id found in the cache + elif attributes_id := self._state_attributes_ids.get(shared_attrs): + dbstate.attributes_id = attributes_id + else: + attr_hash = StateAttributes.hash_shared_attrs(shared_attrs) + # Matching attributes found in the database + if attributes_id := self._find_shared_attr_in_db(attr_hash, shared_attrs): + dbstate.attributes_id = attributes_id + self._state_attributes_ids[shared_attrs] = attributes_id + # No matching attributes found, save them in the DB + else: + dbstate_attributes = StateAttributes( + shared_attrs=shared_attrs, hash=attr_hash + ) + dbstate.state_attributes = dbstate_attributes + self._pending_state_attributes[shared_attrs] = dbstate_attributes + self.event_session.add(dbstate_attributes) + + if old_state := self._old_states.pop(dbstate.entity_id, None): + if old_state.state_id: + dbstate.old_state_id = old_state.state_id + else: + dbstate.old_state = old_state + if event.data.get("new_state"): + self._old_states[dbstate.entity_id] = dbstate + self._pending_expunge.append(dbstate) + else: + dbstate.state = None + self.event_session.add(dbstate) + + def _handle_database_error(self, err: Exception) -> bool: + """Handle a database error that may result in moving away the corrupt db.""" + if isinstance(err.__cause__, sqlite3.DatabaseError): + _LOGGER.exception( + "Unrecoverable sqlite3 database corruption detected: %s", err + ) + self._handle_sqlite_corruption() + return True + return False + + def _event_session_has_pending_writes(self) -> bool: + return bool( + self.event_session and (self.event_session.new or self.event_session.dirty) + ) + + def _commit_event_session_or_retry(self) -> None: + """Commit the event session if there is work to do.""" + if not self._event_session_has_pending_writes(): + return + tries = 1 + while tries <= self.db_max_retries: + try: + self._commit_event_session() + return + except (exc.InternalError, exc.OperationalError) as err: + _LOGGER.error( + "%s: Error executing query: %s. (retrying in %s seconds)", + INVALIDATED_ERR if err.connection_invalidated else CONNECTIVITY_ERR, + err, + self.db_retry_wait, + ) + if tries == self.db_max_retries: + raise + + tries += 1 + time.sleep(self.db_retry_wait) + + def _commit_event_session(self) -> None: + assert self.event_session is not None + self._commits_without_expire += 1 + + if self._pending_expunge: + self.event_session.flush() + for dbstate in self._pending_expunge: + # Expunge the state so its not expired + # until we use it later for dbstate.old_state + if dbstate in self.event_session: + self.event_session.expunge(dbstate) + self._pending_expunge = [] + self.event_session.commit() + + # We just committed the state attributes to the database + # and we now know the attributes_ids. We can save + # many selects for matching attributes by loading them + # into the LRU cache now. + for state_attr in self._pending_state_attributes.values(): + self._state_attributes_ids[ + state_attr.shared_attrs + ] = state_attr.attributes_id + self._pending_state_attributes = {} + for event_data in self._pending_event_data.values(): + self._event_data_ids[event_data.shared_data] = event_data.data_id + self._pending_event_data = {} + + # Expire is an expensive operation (frequently more expensive + # than the flush and commit itself) so we only + # do it after EXPIRE_AFTER_COMMITS commits + if self._commits_without_expire >= EXPIRE_AFTER_COMMITS: + self._commits_without_expire = 0 + self.event_session.expire_all() + + def _handle_sqlite_corruption(self) -> None: + """Handle the sqlite3 database being corrupt.""" + self._close_event_session() + self._close_connection() + move_away_broken_database(dburl_to_path(self.db_url)) + self.run_history.reset() + self._setup_recorder() + self._setup_run() + + def _close_event_session(self) -> None: + """Close the event session.""" + self._old_states = {} + self._state_attributes_ids = {} + self._event_data_ids = {} + self._pending_state_attributes = {} + self._pending_event_data = {} + + if not self.event_session: + return + + try: + self.event_session.rollback() + self.event_session.close() + except SQLAlchemyError as err: + _LOGGER.exception( + "Error while rolling back and closing the event session: %s", err + ) + + def _reopen_event_session(self) -> None: + """Rollback the event session and reopen it after a failure.""" + self._close_event_session() + self._open_event_session() + + def _open_event_session(self) -> None: + """Open the event session.""" + assert self.get_session is not None + self.event_session = self.get_session() + self.event_session.expire_on_commit = False + + def _send_keep_alive(self) -> None: + """Send a keep alive to keep the db connection open.""" + assert self.event_session is not None + _LOGGER.debug("Sending keepalive") + self.event_session.connection().scalar(select([1])) + + @callback + def event_listener(self, event: Event) -> None: + """Listen for new events and put them in the process queue.""" + self.queue.put(EventTask(event)) + + def block_till_done(self) -> None: + """Block till all events processed. + + This is only called in tests. + + This only blocks until the queue is empty + which does not mean the recorder is done. + + Call tests.common's wait_recording_done + after calling this to ensure the data + is in the database. + """ + self._queue_watch.clear() + self.queue.put(WaitTask()) + self._queue_watch.wait() + + async def lock_database(self) -> bool: + """Lock database so it can be backed up safely.""" + if not self.using_sqlite(): + _LOGGER.debug( + "Not a SQLite database or not connected, locking not necessary" + ) + return True + + if self._database_lock_task: + _LOGGER.warning("Database already locked") + return False + + database_locked = asyncio.Event() + task = DatabaseLockTask(database_locked, threading.Event(), False) + self.queue.put(task) + try: + await asyncio.wait_for(database_locked.wait(), timeout=DB_LOCK_TIMEOUT) + except asyncio.TimeoutError as err: + task.database_unlock.set() + raise TimeoutError( + f"Could not lock database within {DB_LOCK_TIMEOUT} seconds." + ) from err + self._database_lock_task = task + return True + + @callback + def unlock_database(self) -> bool: + """Unlock database. + + Returns true if database lock has been held throughout the process. + """ + if not self.using_sqlite(): + _LOGGER.debug( + "Not a SQLite database or not connected, unlocking not necessary" + ) + return True + + if not self._database_lock_task: + _LOGGER.warning("Database currently not locked") + return False + + self._database_lock_task.database_unlock.set() + success = not self._database_lock_task.queue_overflow + + self._database_lock_task = None + + return success + + def _setup_connection(self) -> None: + """Ensure database is ready to fly.""" + kwargs: dict[str, Any] = {} + self._completed_first_database_setup = False + + def setup_recorder_connection( + dbapi_connection: Any, connection_record: Any + ) -> None: + """Dbapi specific connection settings.""" + assert self.engine is not None + setup_connection_for_dialect( + self, + self.engine.dialect.name, + dbapi_connection, + not self._completed_first_database_setup, + ) + self._completed_first_database_setup = True + + if self.db_url == SQLITE_URL_PREFIX or ":memory:" in self.db_url: + kwargs["connect_args"] = {"check_same_thread": False} + kwargs["poolclass"] = MutexPool + MutexPool.pool_lock = threading.RLock() + kwargs["pool_reset_on_return"] = None + elif self.db_url.startswith(SQLITE_URL_PREFIX): + kwargs["poolclass"] = RecorderPool + else: + kwargs["echo"] = False + + if self._using_file_sqlite: + validate_or_move_away_sqlite_database(self.db_url) + + self.engine = create_engine(self.db_url, **kwargs, future=True) + + sqlalchemy_event.listen(self.engine, "connect", setup_recorder_connection) + + Base.metadata.create_all(self.engine) + self.get_session = scoped_session(sessionmaker(bind=self.engine, future=True)) + _LOGGER.debug("Connected to recorder database") + + @property + def _using_file_sqlite(self) -> bool: + """Short version to check if we are using sqlite3 as a file.""" + return self.db_url != SQLITE_URL_PREFIX and self.db_url.startswith( + SQLITE_URL_PREFIX + ) + + def _close_connection(self) -> None: + """Close the connection.""" + assert self.engine is not None + self.engine.dispose() + self.engine = None + self.get_session = None + + def _setup_run(self) -> None: + """Log the start of the current run and schedule any needed jobs.""" + assert self.get_session is not None + with session_scope(session=self.get_session()) as session: + end_incomplete_runs(session, self.run_history.recording_start) + self.run_history.start(session) + self._schedule_compile_missing_statistics(session) + + self._open_event_session() + + def _schedule_compile_missing_statistics(self, session: Session) -> None: + """Add tasks for missing statistics runs.""" + now = dt_util.utcnow() + last_period_minutes = now.minute - now.minute % 5 + last_period = now.replace(minute=last_period_minutes, second=0, microsecond=0) + start = now - timedelta(days=self.keep_days) + start = start.replace(minute=0, second=0, microsecond=0) + + # Find the newest statistics run, if any + if last_run := session.query(func.max(StatisticsRuns.start)).scalar(): + start = max(start, process_timestamp(last_run) + timedelta(minutes=5)) + + # Add tasks + while start < last_period: + end = start + timedelta(minutes=5) + _LOGGER.debug("Compiling missing statistics for %s-%s", start, end) + self.queue.put(StatisticsTask(start)) + start = end + + def _end_session(self) -> None: + """End the recorder session.""" + if self.event_session is None: + return + try: + self.run_history.end(self.event_session) + self._commit_event_session_or_retry() + self.event_session.close() + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Error saving the event session during shutdown: %s", err) + + self.run_history.clear() + + def _shutdown(self) -> None: + """Save end time for current run.""" + self.hass.add_job(self._async_stop_listeners) + self._stop_executor() + self._end_session() + self._close_connection() + + @property + def recording(self) -> bool: + """Return if the recorder is recording.""" + return self._event_listener is not None diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py new file mode 100644 index 00000000000..fdffa63bcd3 --- /dev/null +++ b/homeassistant/components/recorder/tasks.py @@ -0,0 +1,250 @@ +"""Support for recording details.""" +from __future__ import annotations + +import abc +import asyncio +from collections.abc import Callable, Iterable +from dataclasses import dataclass +from datetime import datetime +import threading +from typing import TYPE_CHECKING, Any + +from homeassistant.core import Event + +from . import purge, statistics +from .const import DOMAIN, EXCLUDE_ATTRIBUTES +from .models import StatisticData, StatisticMetaData +from .util import periodic_db_cleanups + +if TYPE_CHECKING: + from .core import Recorder + + +class RecorderTask(abc.ABC): + """ABC for recorder tasks.""" + + commit_before = True + + @abc.abstractmethod + def run(self, instance: Recorder) -> None: + """Handle the task.""" + + +@dataclass +class ClearStatisticsTask(RecorderTask): + """Object to store statistics_ids which for which to remove statistics.""" + + statistic_ids: list[str] + + def run(self, instance: Recorder) -> None: + """Handle the task.""" + statistics.clear_statistics(instance, self.statistic_ids) + + +@dataclass +class UpdateStatisticsMetadataTask(RecorderTask): + """Object to store statistics_id and unit for update of statistics metadata.""" + + statistic_id: str + unit_of_measurement: str | None + + def run(self, instance: Recorder) -> None: + """Handle the task.""" + statistics.update_statistics_metadata( + instance, self.statistic_id, self.unit_of_measurement + ) + + +@dataclass +class PurgeTask(RecorderTask): + """Object to store information about purge task.""" + + purge_before: datetime + repack: bool + apply_filter: bool + + def run(self, instance: Recorder) -> None: + """Purge the database.""" + assert instance.get_session is not None + + if purge.purge_old_data( + instance, self.purge_before, self.repack, self.apply_filter + ): + with instance.get_session() as session: + instance.run_history.load_from_db(session) + # We always need to do the db cleanups after a purge + # is finished to ensure the WAL checkpoint and other + # tasks happen after a vacuum. + periodic_db_cleanups(instance) + return + # Schedule a new purge task if this one didn't finish + instance.queue.put(PurgeTask(self.purge_before, self.repack, self.apply_filter)) + + +@dataclass +class PurgeEntitiesTask(RecorderTask): + """Object to store entity information about purge task.""" + + entity_filter: Callable[[str], bool] + + def run(self, instance: Recorder) -> None: + """Purge entities from the database.""" + if purge.purge_entity_data(instance, self.entity_filter): + return + # Schedule a new purge task if this one didn't finish + instance.queue.put(PurgeEntitiesTask(self.entity_filter)) + + +@dataclass +class PerodicCleanupTask(RecorderTask): + """An object to insert into the recorder to trigger cleanup tasks when auto purge is disabled.""" + + def run(self, instance: Recorder) -> None: + """Handle the task.""" + periodic_db_cleanups(instance) + + +@dataclass +class StatisticsTask(RecorderTask): + """An object to insert into the recorder queue to run a statistics task.""" + + start: datetime + + def run(self, instance: Recorder) -> None: + """Run statistics task.""" + if statistics.compile_statistics(instance, self.start): + return + # Schedule a new statistics task if this one didn't finish + instance.queue.put(StatisticsTask(self.start)) + + +@dataclass +class ExternalStatisticsTask(RecorderTask): + """An object to insert into the recorder queue to run an external statistics task.""" + + metadata: StatisticMetaData + statistics: Iterable[StatisticData] + + def run(self, instance: Recorder) -> None: + """Run statistics task.""" + if statistics.add_external_statistics(instance, self.metadata, self.statistics): + return + # Schedule a new statistics task if this one didn't finish + instance.queue.put(ExternalStatisticsTask(self.metadata, self.statistics)) + + +@dataclass +class AdjustStatisticsTask(RecorderTask): + """An object to insert into the recorder queue to run an adjust statistics task.""" + + statistic_id: str + start_time: datetime + sum_adjustment: float + + def run(self, instance: Recorder) -> None: + """Run statistics task.""" + if statistics.adjust_statistics( + instance, + self.statistic_id, + self.start_time, + self.sum_adjustment, + ): + return + # Schedule a new adjust statistics task if this one didn't finish + instance.queue.put( + AdjustStatisticsTask( + self.statistic_id, self.start_time, self.sum_adjustment + ) + ) + + +@dataclass +class WaitTask(RecorderTask): + """An object to insert into the recorder queue to tell it set the _queue_watch event.""" + + commit_before = False + + def run(self, instance: Recorder) -> None: + """Handle the task.""" + instance._queue_watch.set() # pylint: disable=[protected-access] + + +@dataclass +class DatabaseLockTask(RecorderTask): + """An object to insert into the recorder queue to prevent writes to the database.""" + + database_locked: asyncio.Event + database_unlock: threading.Event + queue_overflow: bool + + def run(self, instance: Recorder) -> None: + """Handle the task.""" + instance._lock_database(self) # pylint: disable=[protected-access] + + +@dataclass +class StopTask(RecorderTask): + """An object to insert into the recorder queue to stop the event handler.""" + + commit_before = False + + def run(self, instance: Recorder) -> None: + """Handle the task.""" + instance.stop_requested = True + + +@dataclass +class EventTask(RecorderTask): + """An event to be processed.""" + + event: Event + commit_before = False + + def run(self, instance: Recorder) -> None: + """Handle the task.""" + # pylint: disable-next=[protected-access] + instance._process_one_event(self.event) + + +@dataclass +class KeepAliveTask(RecorderTask): + """A keep alive to be sent.""" + + commit_before = False + + def run(self, instance: Recorder) -> None: + """Handle the task.""" + # pylint: disable-next=[protected-access] + instance._send_keep_alive() + + +@dataclass +class CommitTask(RecorderTask): + """Commit the event session.""" + + commit_before = False + + def run(self, instance: Recorder) -> None: + """Handle the task.""" + # pylint: disable-next=[protected-access] + instance._commit_event_session_or_retry() + + +@dataclass +class AddRecorderPlatformTask(RecorderTask): + """Add a recorder platform.""" + + domain: str + platform: Any + commit_before = False + + def run(self, instance: Recorder) -> None: + """Handle the task.""" + hass = instance.hass + domain = self.domain + platform = self.platform + + platforms: dict[str, Any] = hass.data[DOMAIN] + platforms[domain] = platform + if hasattr(self.platform, "exclude_attributes"): + hass.data[EXCLUDE_ATTRIBUTES][domain] = platform.exclude_attributes(hass) diff --git a/mypy.ini b/mypy.ini index a4f7ef80d30..b55d5e74998 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1732,6 +1732,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.recorder.core] +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.recorder.backup] check_untyped_defs = true disallow_incomplete_defs = true @@ -1842,6 +1853,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.recorder.tasks] +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.recorder.util] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 4ae1354464d..630d0b84889 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -459,23 +459,28 @@ def test_get_significant_states_only(hass_history): points.append(start + timedelta(minutes=i)) states = [] - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start + ): set_state("123", attributes={"attribute": 10.64}) with patch( - "homeassistant.components.recorder.dt_util.utcnow", return_value=points[0] + "homeassistant.components.recorder.core.dt_util.utcnow", + return_value=points[0], ): # Attributes are different, state not states.append(set_state("123", attributes={"attribute": 21.42})) with patch( - "homeassistant.components.recorder.dt_util.utcnow", return_value=points[1] + "homeassistant.components.recorder.core.dt_util.utcnow", + return_value=points[1], ): # state is different, attributes not states.append(set_state("32", attributes={"attribute": 21.42})) with patch( - "homeassistant.components.recorder.dt_util.utcnow", return_value=points[2] + "homeassistant.components.recorder.core.dt_util.utcnow", + return_value=points[2], ): # everything is different states.append(set_state("412", attributes={"attribute": 54.23})) @@ -536,7 +541,9 @@ def record_states(hass): four = three + timedelta(seconds=1) states = {therm: [], therm2: [], mp: [], mp2: [], mp3: [], script_c: []} - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=one + ): states[mp].append( set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) ) @@ -553,7 +560,9 @@ def record_states(hass): set_state(therm, 20, attributes={"current_temperature": 19.5}) ) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=two + ): # This state will be skipped only different in time set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)}) # This state will be skipped because domain is excluded @@ -568,7 +577,9 @@ def record_states(hass): set_state(therm2, 20, attributes={"current_temperature": 19}) ) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=three + ): states[mp].append( set_state(mp, "Netflix", attributes={"media_title": str(sentinel.mt4)}) ) diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index 8f22447cd8e..5e29fb092b1 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -52,7 +52,7 @@ async def _async_get_states( def _add_db_entries( hass: ha.HomeAssistant, point: datetime, entity_ids: list[str] ) -> None: - with recorder.session_scope(hass=hass) as session: + with session_scope(hass=hass) as session: for idx, entity_id in enumerate(entity_ids): session.add( Events( @@ -87,7 +87,9 @@ def _setup_get_states(hass): """Set up for testing get_states.""" states = [] now = dt_util.utcnow() - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=now): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=now + ): for i in range(5): state = ha.State( f"test.point_in_time_{i % 5}", @@ -102,7 +104,9 @@ def _setup_get_states(hass): wait_recording_done(hass) future = now + timedelta(seconds=1) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=future): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=future + ): for i in range(5): state = ha.State( f"test.point_in_time_{i % 5}", @@ -122,7 +126,7 @@ def test_get_full_significant_states_with_session_entity_no_matches(hass_recorde hass = hass_recorder() now = dt_util.utcnow() time_before_recorder_ran = now - timedelta(days=1000) - with recorder.session_scope(hass=hass) as session: + with session_scope(hass=hass) as session: assert ( history.get_full_significant_states_with_session( hass, session, time_before_recorder_ran, now, entity_ids=["demo.id"] @@ -148,7 +152,7 @@ def test_significant_states_with_session_entity_minimal_response_no_matches( hass = hass_recorder() now = dt_util.utcnow() time_before_recorder_ran = now - timedelta(days=1000) - with recorder.session_scope(hass=hass) as session: + with session_scope(hass=hass) as session: assert ( history.get_significant_states_with_session( hass, @@ -197,11 +201,15 @@ def test_state_changes_during_period(hass_recorder, attributes, no_attributes, l point = start + timedelta(seconds=1) end = point + timedelta(seconds=1) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start + ): set_state("idle") set_state("YouTube") - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point + ): states = [ set_state("idle"), set_state("Netflix"), @@ -209,7 +217,9 @@ def test_state_changes_during_period(hass_recorder, attributes, no_attributes, l set_state("YouTube"), ] - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=end): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=end + ): set_state("Netflix") set_state("Plex") @@ -235,11 +245,15 @@ def test_state_changes_during_period_descending(hass_recorder): point = start + timedelta(seconds=1) end = point + timedelta(seconds=1) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start + ): set_state("idle") set_state("YouTube") - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point + ): states = [ set_state("idle"), set_state("Netflix"), @@ -247,7 +261,9 @@ def test_state_changes_during_period_descending(hass_recorder): set_state("YouTube"), ] - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=end): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=end + ): set_state("Netflix") set_state("Plex") @@ -277,14 +293,20 @@ def test_get_last_state_changes(hass_recorder): point = start + timedelta(minutes=1) point2 = point + timedelta(minutes=1) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start + ): set_state("1") states = [] - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point + ): states.append(set_state("2")) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point2): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point2 + ): states.append(set_state("3")) hist = history.get_last_state_changes(hass, 2, entity_id) @@ -310,10 +332,14 @@ def test_ensure_state_can_be_copied(hass_recorder): start = dt_util.utcnow() - timedelta(minutes=2) point = start + timedelta(minutes=1) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start + ): set_state("1") - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point + ): set_state("2") hist = history.get_last_state_changes(hass, 2, entity_id) @@ -486,23 +512,28 @@ def test_get_significant_states_only(hass_recorder): points.append(start + timedelta(minutes=i)) states = [] - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start + ): set_state("123", attributes={"attribute": 10.64}) with patch( - "homeassistant.components.recorder.dt_util.utcnow", return_value=points[0] + "homeassistant.components.recorder.core.dt_util.utcnow", + return_value=points[0], ): # Attributes are different, state not states.append(set_state("123", attributes={"attribute": 21.42})) with patch( - "homeassistant.components.recorder.dt_util.utcnow", return_value=points[1] + "homeassistant.components.recorder.core.dt_util.utcnow", + return_value=points[1], ): # state is different, attributes not states.append(set_state("32", attributes={"attribute": 21.42})) with patch( - "homeassistant.components.recorder.dt_util.utcnow", return_value=points[2] + "homeassistant.components.recorder.core.dt_util.utcnow", + return_value=points[2], ): # everything is different states.append(set_state("412", attributes={"attribute": 54.23})) @@ -547,7 +578,9 @@ def record_states(hass): four = three + timedelta(seconds=1) states = {therm: [], therm2: [], mp: [], mp2: [], mp3: [], script_c: []} - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=one + ): states[mp].append( set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) ) @@ -564,7 +597,9 @@ def record_states(hass): set_state(therm, 20, attributes={"current_temperature": 19.5}) ) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=two + ): # This state will be skipped only different in time set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)}) # This state will be skipped because domain is excluded @@ -579,7 +614,9 @@ def record_states(hass): set_state(therm2, 20, attributes={"current_temperature": 19}) ) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=three + ): states[mp].append( set_state(mp, "Netflix", attributes={"media_title": str(sentinel.mt4)}) ) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 6d4f2e1106a..fc2c03e0039 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -20,7 +20,6 @@ from homeassistant.components.recorder import ( CONF_DB_URL, CONFIG_SCHEMA, DOMAIN, - KEEPALIVE_TIME, SERVICE_DISABLE, SERVICE_ENABLE, SERVICE_PURGE, @@ -29,7 +28,7 @@ from homeassistant.components.recorder import ( Recorder, get_instance, ) -from homeassistant.components.recorder.const import DATA_INSTANCE +from homeassistant.components.recorder.const import DATA_INSTANCE, KEEPALIVE_TIME from homeassistant.components.recorder.models import ( EventData, Events, @@ -196,7 +195,7 @@ async def test_saving_many_states( with patch.object( hass.data[DATA_INSTANCE].event_session, "expire_all" - ) as expire_all, patch.object(recorder, "EXPIRE_AFTER_COMMITS", 2): + ) as expire_all, patch.object(recorder.core, "EXPIRE_AFTER_COMMITS", 2): for _ in range(3): hass.states.async_set(entity_id, "on", attributes) await async_wait_recording_done(hass) @@ -611,7 +610,7 @@ def test_saving_state_and_removing_entity(hass, hass_recorder): def test_recorder_setup_failure(hass): """Test some exceptions.""" with patch.object(Recorder, "_setup_connection") as setup, patch( - "homeassistant.components.recorder.time.sleep" + "homeassistant.components.recorder.core.time.sleep" ): setup.side_effect = ImportError("driver not found") rec = _default_recorder(hass) @@ -625,7 +624,7 @@ def test_recorder_setup_failure(hass): def test_recorder_setup_failure_without_event_listener(hass): """Test recorder setup failure when the event listener is not setup.""" with patch.object(Recorder, "_setup_connection") as setup, patch( - "homeassistant.components.recorder.time.sleep" + "homeassistant.components.recorder.core.time.sleep" ): setup.side_effect = ImportError("driver not found") rec = _default_recorder(hass) @@ -685,7 +684,7 @@ def test_auto_purge(hass_recorder): with patch( "homeassistant.components.recorder.purge.purge_old_data", return_value=True ) as purge_old_data, patch( - "homeassistant.components.recorder.periodic_db_cleanups" + "homeassistant.components.recorder.tasks.periodic_db_cleanups" ) as periodic_db_cleanups: # Advance one day, and the purge task should run test_time = test_time + timedelta(days=1) @@ -741,11 +740,11 @@ def test_auto_purge_auto_repack_on_second_sunday(hass_recorder): run_tasks_at_time(hass, test_time) with patch( - "homeassistant.components.recorder.is_second_sunday", return_value=True + "homeassistant.components.recorder.core.is_second_sunday", return_value=True ), patch( "homeassistant.components.recorder.purge.purge_old_data", return_value=True ) as purge_old_data, patch( - "homeassistant.components.recorder.periodic_db_cleanups" + "homeassistant.components.recorder.tasks.periodic_db_cleanups" ) as periodic_db_cleanups: # Advance one day, and the purge task should run test_time = test_time + timedelta(days=1) @@ -779,11 +778,11 @@ def test_auto_purge_auto_repack_disabled_on_second_sunday(hass_recorder): run_tasks_at_time(hass, test_time) with patch( - "homeassistant.components.recorder.is_second_sunday", return_value=True + "homeassistant.components.recorder.core.is_second_sunday", return_value=True ), patch( "homeassistant.components.recorder.purge.purge_old_data", return_value=True ) as purge_old_data, patch( - "homeassistant.components.recorder.periodic_db_cleanups" + "homeassistant.components.recorder.tasks.periodic_db_cleanups" ) as periodic_db_cleanups: # Advance one day, and the purge task should run test_time = test_time + timedelta(days=1) @@ -817,11 +816,12 @@ def test_auto_purge_no_auto_repack_on_not_second_sunday(hass_recorder): run_tasks_at_time(hass, test_time) with patch( - "homeassistant.components.recorder.is_second_sunday", return_value=False + "homeassistant.components.recorder.core.is_second_sunday", + return_value=False, ), patch( "homeassistant.components.recorder.purge.purge_old_data", return_value=True ) as purge_old_data, patch( - "homeassistant.components.recorder.periodic_db_cleanups" + "homeassistant.components.recorder.tasks.periodic_db_cleanups" ) as periodic_db_cleanups: # Advance one day, and the purge task should run test_time = test_time + timedelta(days=1) @@ -856,7 +856,7 @@ def test_auto_purge_disabled(hass_recorder): with patch( "homeassistant.components.recorder.purge.purge_old_data", return_value=True ) as purge_old_data, patch( - "homeassistant.components.recorder.periodic_db_cleanups" + "homeassistant.components.recorder.tasks.periodic_db_cleanups" ) as periodic_db_cleanups: # Advance one day, and the purge task should run test_time = test_time + timedelta(days=1) @@ -924,7 +924,9 @@ def test_auto_statistics(hass_recorder): def test_statistics_runs_initiated(hass_recorder): """Test statistics_runs is initiated when DB is created.""" now = dt_util.utcnow() - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=now): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=now + ): hass = hass_recorder() wait_recording_done(hass) @@ -944,7 +946,9 @@ def test_compile_missing_statistics(tmpdir): test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=now): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=now + ): hass = get_test_home_assistant() setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) @@ -963,7 +967,7 @@ def test_compile_missing_statistics(tmpdir): hass.stop() with patch( - "homeassistant.components.recorder.dt_util.utcnow", + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=now + timedelta(hours=1), ): @@ -1356,8 +1360,8 @@ async def test_database_lock_and_overflow( instance: Recorder = hass.data[DATA_INSTANCE] - with patch.object(recorder, "MAX_QUEUE_BACKLOG", 1), patch.object( - recorder, "DB_LOCK_QUEUE_CHECK_TIMEOUT", 0.1 + with patch.object(recorder.core, "MAX_QUEUE_BACKLOG", 1), patch.object( + recorder.core, "DB_LOCK_QUEUE_CHECK_TIMEOUT", 0.1 ): await instance.lock_database() @@ -1382,7 +1386,7 @@ async def test_database_lock_timeout(hass, recorder_mock): instance: Recorder = hass.data[DATA_INSTANCE] - class BlockQueue(recorder.RecorderTask): + class BlockQueue(recorder.tasks.RecorderTask): event: threading.Event = threading.Event() def run(self, instance: Recorder) -> None: @@ -1390,7 +1394,7 @@ async def test_database_lock_timeout(hass, recorder_mock): block_task = BlockQueue() instance.queue.put(block_task) - with patch.object(recorder, "DB_LOCK_TIMEOUT", 0.1): + with patch.object(recorder.core, "DB_LOCK_TIMEOUT", 0.1): try: with pytest.raises(TimeoutError): await instance.lock_database() @@ -1435,7 +1439,7 @@ async def test_database_connection_keep_alive( await instance.async_recorder_ready.wait() async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=recorder.KEEPALIVE_TIME) + hass, dt_util.utcnow() + timedelta(seconds=recorder.core.KEEPALIVE_TIME) ) await async_wait_recording_done(hass) assert "Sending keepalive" in caplog.text @@ -1452,7 +1456,7 @@ async def test_database_connection_keep_alive_disabled_on_sqlite( await instance.async_recorder_ready.wait() async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=recorder.KEEPALIVE_TIME) + hass, dt_util.utcnow() + timedelta(seconds=recorder.core.KEEPALIVE_TIME) ) await async_wait_recording_done(hass) assert "Sending keepalive" not in caplog.text @@ -1482,7 +1486,7 @@ def test_deduplication_event_data_inside_commit_interval(hass_recorder, caplog): # Patch STATE_ATTRIBUTES_ID_CACHE_SIZE since otherwise # the CI can fail because the test takes too long to run -@patch("homeassistant.components.recorder.STATE_ATTRIBUTES_ID_CACHE_SIZE", 5) +@patch("homeassistant.components.recorder.core.STATE_ATTRIBUTES_ID_CACHE_SIZE", 5) def test_deduplication_state_attributes_inside_commit_interval(hass_recorder, caplog): """Test deduplication of state attributes inside the commit interval.""" hass = hass_recorder() diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 6b963941263..1b84eb5d171 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -44,7 +44,8 @@ async def test_schema_update_calls(hass): assert recorder.util.async_migration_in_progress(hass) is False with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( - "homeassistant.components.recorder.create_engine", new=create_engine_test + "homeassistant.components.recorder.core.create_engine", + new=create_engine_test, ), patch( "homeassistant.components.recorder.migration._apply_update", wraps=migration._apply_update, @@ -67,10 +68,10 @@ async def test_migration_in_progress(hass): """Test that we can check for migration in progress.""" assert recorder.util.async_migration_in_progress(hass) is False - with patch( - "homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", - True, - ), patch("homeassistant.components.recorder.create_engine", new=create_engine_test): + with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True,), patch( + "homeassistant.components.recorder.core.create_engine", + new=create_engine_test, + ): await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) @@ -86,7 +87,8 @@ async def test_database_migration_failed(hass): assert recorder.util.async_migration_in_progress(hass) is False with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( - "homeassistant.components.recorder.create_engine", new=create_engine_test + "homeassistant.components.recorder.core.create_engine", + new=create_engine_test, ), patch( "homeassistant.components.recorder.migration._apply_update", side_effect=ValueError, @@ -125,7 +127,7 @@ async def test_database_migration_encounters_corruption(hass): "homeassistant.components.recorder.migration.migrate_schema", side_effect=sqlite3_exception, ), patch( - "homeassistant.components.recorder.move_away_broken_database" + "homeassistant.components.recorder.core.move_away_broken_database" ) as move_away: await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} @@ -149,7 +151,7 @@ async def test_database_migration_encounters_corruption_not_sqlite(hass): "homeassistant.components.recorder.migration.migrate_schema", side_effect=DatabaseError("statement", {}, []), ), patch( - "homeassistant.components.recorder.move_away_broken_database" + "homeassistant.components.recorder.core.move_away_broken_database" ) as move_away, patch( "homeassistant.components.persistent_notification.create", side_effect=pn.create ) as mock_create, patch( @@ -176,10 +178,10 @@ async def test_events_during_migration_are_queued(hass): assert recorder.util.async_migration_in_progress(hass) is False - with patch( - "homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", - True, - ), patch("homeassistant.components.recorder.create_engine", new=create_engine_test): + with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True,), patch( + "homeassistant.components.recorder.core.create_engine", + new=create_engine_test, + ): await async_setup_component( hass, "recorder", @@ -207,8 +209,9 @@ async def test_events_during_migration_queue_exhausted(hass): assert recorder.util.async_migration_in_progress(hass) is False with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( - "homeassistant.components.recorder.create_engine", new=create_engine_test - ), patch.object(recorder, "MAX_QUEUE_BACKLOG", 1): + "homeassistant.components.recorder.core.create_engine", + new=create_engine_test, + ), patch.object(recorder.core, "MAX_QUEUE_BACKLOG", 1): await async_setup_component( hass, "recorder", @@ -296,7 +299,8 @@ async def test_schema_migrate(hass, start_version): migration_done.set() with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( - "homeassistant.components.recorder.create_engine", new=_create_engine_test + "homeassistant.components.recorder.core.create_engine", + new=_create_engine_test, ), patch( "homeassistant.components.recorder.Recorder._setup_run", side_effect=_mock_setup_run, diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index d946d1e2a14..8ac2f3a783c 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -9,7 +9,6 @@ from sqlalchemy.exc import DatabaseError, OperationalError 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, @@ -20,6 +19,7 @@ from homeassistant.components.recorder.models import ( StatisticsShortTerm, ) from homeassistant.components.recorder.purge import purge_old_data +from homeassistant.components.recorder.tasks import PurgeTask from homeassistant.components.recorder.util import session_scope from homeassistant.const import EVENT_STATE_CHANGED, STATE_ON from homeassistant.core import HomeAssistant @@ -128,7 +128,7 @@ async def test_purge_old_states_encouters_database_corruption( sqlite3_exception.__cause__ = sqlite3.DatabaseError() with patch( - "homeassistant.components.recorder.move_away_broken_database" + "homeassistant.components.recorder.core.move_away_broken_database" ) as move_away, patch( "homeassistant.components.recorder.purge.purge_old_data", side_effect=sqlite3_exception, @@ -406,7 +406,7 @@ async def test_purge_edge_case( """Test states and events are purged even if they occurred shortly before purge_before.""" async def _add_db_entries(hass: HomeAssistant, timestamp: datetime) -> None: - with recorder.session_scope(hass=hass) as session: + with session_scope(hass=hass) as session: session.add( Events( event_id=1001, @@ -477,7 +477,7 @@ async def test_purge_cutoff_date( timestamp_keep = cutoff timestamp_purge = cutoff - timedelta(microseconds=1) - with recorder.session_scope(hass=hass) as session: + with session_scope(hass=hass) as session: session.add( Events( event_id=1000, @@ -626,7 +626,7 @@ async def test_purge_filtered_states( assert instance.entity_filter("sensor.excluded") is False def _add_db_entries(hass: HomeAssistant) -> None: - with recorder.session_scope(hass=hass) as session: + with session_scope(hass=hass) as session: # Add states and state_changed events that should be purged for days in range(1, 4): timestamp = dt_util.utcnow() - timedelta(days=days) @@ -820,7 +820,7 @@ async def test_purge_filtered_states_to_empty( assert instance.entity_filter("sensor.excluded") is False def _add_db_entries(hass: HomeAssistant) -> None: - with recorder.session_scope(hass=hass) as session: + with session_scope(hass=hass) as session: # Add states and state_changed events that should be purged for days in range(1, 4): timestamp = dt_util.utcnow() - timedelta(days=days) @@ -877,7 +877,7 @@ async def test_purge_without_state_attributes_filtered_states_to_empty( assert instance.entity_filter("sensor.old_format") is False def _add_db_entries(hass: HomeAssistant) -> None: - with recorder.session_scope(hass=hass) as session: + with session_scope(hass=hass) as session: # Add states and state_changed events that should be purged # in the legacy format timestamp = dt_util.utcnow() - timedelta(days=5) @@ -944,7 +944,7 @@ async def test_purge_filtered_events( await async_setup_recorder_instance(hass, config) def _add_db_entries(hass: HomeAssistant) -> None: - with recorder.session_scope(hass=hass) as session: + with session_scope(hass=hass) as session: # Add events that should be purged for days in range(1, 4): timestamp = dt_util.utcnow() - timedelta(days=days) @@ -1038,7 +1038,7 @@ async def test_purge_filtered_events_state_changed( assert instance.entity_filter("sensor.excluded") is True def _add_db_entries(hass: HomeAssistant) -> None: - with recorder.session_scope(hass=hass) as session: + with session_scope(hass=hass) as session: # Add states and state_changed events that should be purged for days in range(1, 4): timestamp = dt_util.utcnow() - timedelta(days=days) @@ -1154,7 +1154,7 @@ async def test_purge_entities( await async_wait_purge_done(hass) def _add_purge_records(hass: HomeAssistant) -> None: - with recorder.session_scope(hass=hass) as session: + with session_scope(hass=hass) as session: # Add states and state_changed events that should be purged for days in range(1, 4): timestamp = dt_util.utcnow() - timedelta(days=days) @@ -1186,7 +1186,7 @@ async def test_purge_entities( ) def _add_keep_records(hass: HomeAssistant) -> None: - with recorder.session_scope(hass=hass) as session: + with session_scope(hass=hass) as session: # Add states and state_changed events that should be kept timestamp = dt_util.utcnow() - timedelta(days=2) for event_id in range(200, 210): @@ -1289,7 +1289,8 @@ async def _add_test_states(hass: HomeAssistant): attributes = {"dontpurgeme": True, **base_attributes} with patch( - "homeassistant.components.recorder.dt_util.utcnow", return_value=timestamp + "homeassistant.components.recorder.core.dt_util.utcnow", + return_value=timestamp, ): await set_state("test.recorder2", state, attributes=attributes) @@ -1304,7 +1305,7 @@ async def _add_test_events(hass: HomeAssistant): await hass.async_block_till_done() await async_wait_recording_done(hass) - with recorder.session_scope(hass=hass) as session: + with session_scope(hass=hass) as session: for event_id in range(6): if event_id < 2: timestamp = eleven_days_ago @@ -1335,7 +1336,7 @@ async def _add_test_statistics(hass: HomeAssistant): await hass.async_block_till_done() await async_wait_recording_done(hass) - with recorder.session_scope(hass=hass) as session: + with session_scope(hass=hass) as session: for event_id in range(6): if event_id < 2: timestamp = eleven_days_ago @@ -1364,7 +1365,7 @@ async def _add_test_recorder_runs(hass: HomeAssistant): await hass.async_block_till_done() await async_wait_recording_done(hass) - with recorder.session_scope(hass=hass) as session: + with session_scope(hass=hass) as session: for rec_id in range(6): if rec_id < 2: timestamp = eleven_days_ago @@ -1391,7 +1392,7 @@ async def _add_test_statistics_runs(hass: HomeAssistant): await hass.async_block_till_done() await async_wait_recording_done(hass) - with recorder.session_scope(hass=hass) as session: + with session_scope(hass=hass) as session: for rec_id in range(6): if rec_id < 2: timestamp = eleven_days_ago diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 58f848edc5a..dc13f2abb6a 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -839,7 +839,9 @@ def record_states(hass): four = three + timedelta(seconds=15 * 5) states = {mp: [], sns1: [], sns2: [], sns3: [], sns4: []} - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=one + ): states[mp].append( set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) ) @@ -851,13 +853,17 @@ def record_states(hass): states[sns3].append(set_state(sns3, "10", attributes=sns3_attr)) states[sns4].append(set_state(sns4, "10", attributes=sns4_attr)) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=two + ): states[sns1].append(set_state(sns1, "15", attributes=sns1_attr)) states[sns2].append(set_state(sns2, "15", attributes=sns2_attr)) states[sns3].append(set_state(sns3, "15", attributes=sns3_attr)) states[sns4].append(set_state(sns4, "15", attributes=sns4_attr)) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=three + ): states[sns1].append(set_state(sns1, "20", attributes=sns1_attr)) states[sns2].append(set_state(sns2, "20", attributes=sns2_attr)) states[sns3].append(set_state(sns3, "20", attributes=sns3_attr)) diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index f6d2fa0a99d..07334fef1c3 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -45,7 +45,7 @@ def test_recorder_bad_commit(hass_recorder): session.execute(text("select * from notthere")) with patch( - "homeassistant.components.recorder.time.sleep" + "homeassistant.components.recorder.core.time.sleep" ) as e_mock, util.session_scope(hass=hass) as session: res = util.commit(session, work) assert res is False @@ -66,7 +66,7 @@ def test_recorder_bad_execute(hass_recorder): mck1.to_native = to_native with pytest.raises(SQLAlchemyError), patch( - "homeassistant.components.recorder.time.sleep" + "homeassistant.components.recorder.core.time.sleep" ) as e_mock: util.execute((mck1,), to_native=True) @@ -148,7 +148,7 @@ async def test_last_run_was_recently_clean( "homeassistant.components.recorder.util.last_run_was_recently_clean", wraps=_last_run_was_recently_clean, ) as last_run_was_recently_clean_mock, patch( - "homeassistant.components.recorder.dt_util.utcnow", + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=thirty_min_future_time, ): hass = await async_test_home_assistant(None) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 08427e60911..fe197cb72e6 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -323,9 +323,10 @@ async def test_recorder_info_migration_queue_exhausted(hass, hass_ws_client): with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.Recorder.async_periodic_statistics" ), patch( - "homeassistant.components.recorder.create_engine", new=create_engine_test + "homeassistant.components.recorder.core.create_engine", + new=create_engine_test, ), patch.object( - recorder, "MAX_QUEUE_BACKLOG", 1 + recorder.core, "MAX_QUEUE_BACKLOG", 1 ), patch( "homeassistant.components.recorder.migration.migrate_schema", wraps=stalled_migration, @@ -384,7 +385,7 @@ async def test_backup_start_timeout( # Ensure there are no queued events await async_wait_recording_done(hass) - with patch.object(recorder, "DB_LOCK_TIMEOUT", 0): + with patch.object(recorder.core, "DB_LOCK_TIMEOUT", 0): try: await client.send_json({"id": 1, "type": "backup/start"}) response = await client.receive_json() diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 58a991c5f53..b4abf4d9ec3 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -172,7 +172,9 @@ def test_compile_hourly_statistics_purged_state_changes( mean = min = max = float(hist["sensor.test1"][-1].state) # Purge all states from the database - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=four): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=four + ): hass.services.call("recorder", "purge", {"keep_days": 0}) hass.block_till_done() wait_recording_done(hass) @@ -2747,17 +2749,23 @@ def record_states(hass, zero, entity_id, attributes, seq=None): four = three + timedelta(seconds=10 * 5) states = {entity_id: []} - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=one + ): states[entity_id].append( set_state(entity_id, str(seq[0]), attributes=attributes) ) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=two + ): states[entity_id].append( set_state(entity_id, str(seq[1]), attributes=attributes) ) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=three + ): states[entity_id].append( set_state(entity_id, str(seq[2]), attributes=attributes) ) @@ -3339,35 +3347,53 @@ def record_meter_states(hass, zero, entity_id, _attributes, seq): attributes["last_reset"] = zero.isoformat() states = {entity_id: []} - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=zero): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=zero + ): states[entity_id].append(set_state(entity_id, seq[0], attributes=attributes)) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=one + ): states[entity_id].append(set_state(entity_id, seq[1], attributes=attributes)) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=two + ): states[entity_id].append(set_state(entity_id, seq[2], attributes=attributes)) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=three + ): states[entity_id].append(set_state(entity_id, seq[3], attributes=attributes)) attributes = dict(_attributes) if "last_reset" in _attributes: attributes["last_reset"] = four.isoformat() - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=four): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=four + ): states[entity_id].append(set_state(entity_id, seq[4], attributes=attributes)) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=five): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=five + ): states[entity_id].append(set_state(entity_id, seq[5], attributes=attributes)) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=six): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=six + ): states[entity_id].append(set_state(entity_id, seq[6], attributes=attributes)) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=seven): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=seven + ): states[entity_id].append(set_state(entity_id, seq[7], attributes=attributes)) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=eight): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=eight + ): states[entity_id].append(set_state(entity_id, seq[8], attributes=attributes)) return four, eight, states @@ -3386,7 +3412,9 @@ def record_meter_state(hass, zero, entity_id, attributes, seq): return hass.states.get(entity_id) states = {entity_id: []} - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=zero): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=zero + ): states[entity_id].append(set_state(entity_id, seq[0], attributes=attributes)) return states @@ -3410,13 +3438,19 @@ def record_states_partially_unavailable(hass, zero, entity_id, attributes): four = three + timedelta(seconds=15 * 5) states = {entity_id: []} - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=one + ): states[entity_id].append(set_state(entity_id, "10", attributes=attributes)) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=two + ): states[entity_id].append(set_state(entity_id, "25", attributes=attributes)) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=three + ): states[entity_id].append( set_state(entity_id, STATE_UNAVAILABLE, attributes=attributes) ) From 9b03ef4829bd6591af8f87224573bd57b35b5ac5 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 3 May 2022 01:01:19 -0500 Subject: [PATCH 0140/3516] Improve Sonos terminology for inclusiveness (#71206) * Improve Sonos group terminology * Deprecate Sonos-specific grouping services * Push deprecation back one version * Revert deprecation notice --- homeassistant/components/sonos/speaker.py | 44 +++++++++++------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 4e4661b389b..a57bb4d3206 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -749,18 +749,18 @@ class SonosSpeaker: def _get_soco_group() -> list[str]: """Ask SoCo cache for existing topology.""" coordinator_uid = self.soco.uid - slave_uids = [] + joined_uids = [] with contextlib.suppress(OSError, SoCoException): if self.soco.group and self.soco.group.coordinator: coordinator_uid = self.soco.group.coordinator.uid - slave_uids = [ + joined_uids = [ p.uid for p in self.soco.group.members if p.uid != coordinator_uid and p.is_visible ] - return [coordinator_uid] + slave_uids + return [coordinator_uid] + joined_uids async def _async_extract_group(event: SonosEvent | None) -> list[str]: """Extract group layout from a topology event.""" @@ -814,13 +814,13 @@ class SonosSpeaker: self.sonos_group_entities = sonos_group_entities self.async_write_entity_states() - for slave_uid in group[1:]: - slave = self.hass.data[DATA_SONOS].discovered.get(slave_uid) - if slave: - slave.coordinator = self - slave.sonos_group = sonos_group - slave.sonos_group_entities = sonos_group_entities - slave.async_write_entity_states() + for joined_uid in group[1:]: + joined_speaker = self.hass.data[DATA_SONOS].discovered.get(joined_uid) + if joined_speaker: + joined_speaker.coordinator = self + joined_speaker.sonos_group = sonos_group + joined_speaker.sonos_group_entities = sonos_group_entities + joined_speaker.async_write_entity_states() _LOGGER.debug("Regrouped %s: %s", self.zone_name, self.sonos_group_entities) @@ -838,7 +838,7 @@ class SonosSpeaker: return _async_handle_group_event(event) @soco_error() - def join(self, slaves: list[SonosSpeaker]) -> list[SonosSpeaker]: + def join(self, speakers: list[SonosSpeaker]) -> list[SonosSpeaker]: """Form a group with other players.""" if self.coordinator: self.unjoin() @@ -846,12 +846,12 @@ class SonosSpeaker: else: group = self.sonos_group.copy() - for slave in slaves: - if slave.soco.uid != self.soco.uid: - slave.soco.join(self.soco) - slave.coordinator = self - if slave not in group: - group.append(slave) + for speaker in speakers: + if speaker.soco.uid != self.soco.uid: + speaker.soco.join(self.soco) + speaker.coordinator = self + if speaker not in group: + group.append(speaker) return group @@ -880,11 +880,11 @@ class SonosSpeaker: def _unjoin_all(speakers: list[SonosSpeaker]) -> None: """Sync helper.""" - # Unjoin slaves first to prevent inheritance of queues + # Detach all joined speakers first to prevent inheritance of queues coordinators = [s for s in speakers if s.is_coordinator] - slaves = [s for s in speakers if not s.is_coordinator] + joined_speakers = [s for s in speakers if not s.is_coordinator] - for speaker in slaves + coordinators: + for speaker in joined_speakers + coordinators: speaker.unjoin() async with hass.data[DATA_SONOS].topology_condition: @@ -928,7 +928,7 @@ class SonosSpeaker: assert self.soco_snapshot is not None self.soco_snapshot.restore() except (TypeError, AssertionError, AttributeError, SoCoException) as ex: - # Can happen if restoring a coordinator onto a current slave + # Can happen if restoring a coordinator onto a current group member _LOGGER.warning("Error on restore %s: %s", self.zone_name, ex) self.soco_snapshot = None @@ -1036,7 +1036,7 @@ class SonosSpeaker: if coordinator != current_group[0]: return False - # Test that slaves match + # Test that joined members match if set(group[1:]) != set(current_group[1:]): return False From bbe807c655edcfe2c8b354dd3685221e4a2755a7 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 3 May 2022 01:01:44 -0500 Subject: [PATCH 0141/3516] Deprecate legacy Sonos grouping services (#71226) --- homeassistant/components/sonos/media_player.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index f7f5e2722ff..95834938953 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -127,6 +127,9 @@ async def async_setup_entry( speakers.append(entity.speaker) if service_call.service == SERVICE_JOIN: + _LOGGER.warning( + "Service 'sonos.join' is deprecated and will be removed in 2022.8, please use 'media_player.join'" + ) master = platform.entities.get(service_call.data[ATTR_MASTER]) if master: await SonosSpeaker.join_multi(hass, master.speaker, speakers) # type: ignore[arg-type] @@ -136,6 +139,9 @@ async def async_setup_entry( service_call.data[ATTR_MASTER], ) elif service_call.service == SERVICE_UNJOIN: + _LOGGER.warning( + "Service 'sonos.unjoin' is deprecated and will be removed in 2022.8, please use 'media_player.unjoin'" + ) await SonosSpeaker.unjoin_multi(hass, speakers) # type: ignore[arg-type] elif service_call.service == SERVICE_SNAPSHOT: await SonosSpeaker.snapshot_multi( From a87578998776265d711e9222f15d8beb8010ffbd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 May 2022 00:34:04 -0700 Subject: [PATCH 0142/3516] Bump aioslimproto to 1.0.2 (#71231) --- homeassistant/components/slimproto/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/slimproto/manifest.json b/homeassistant/components/slimproto/manifest.json index b8e00eb3f99..557428919a4 100644 --- a/homeassistant/components/slimproto/manifest.json +++ b/homeassistant/components/slimproto/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "iot_class": "local_push", "documentation": "https://www.home-assistant.io/integrations/slimproto", - "requirements": ["aioslimproto==1.0.0"], + "requirements": ["aioslimproto==1.0.2"], "codeowners": ["@marcelveldt"], "after_dependencies": ["media_source"] } diff --git a/requirements_all.txt b/requirements_all.txt index 82b69db90f8..14154c718b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -244,7 +244,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.slimproto -aioslimproto==1.0.0 +aioslimproto==1.0.2 # homeassistant.components.steamist aiosteamist==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a107e19bdf6..9929fe508d4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -210,7 +210,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.slimproto -aioslimproto==1.0.0 +aioslimproto==1.0.2 # homeassistant.components.steamist aiosteamist==0.3.1 From 1ef060700a02a46ce6de554aaf5e9d4be3ecbd00 Mon Sep 17 00:00:00 2001 From: prokon <67952017+prokon@users.noreply.github.com> Date: Tue, 3 May 2022 09:49:12 +0200 Subject: [PATCH 0143/3516] Add verisure lock method attribute (#70375) Co-authored-by: Martin Hjelmare --- homeassistant/components/verisure/lock.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 0e28298b2e8..8f9556643f8 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -99,6 +99,11 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt """Last change triggered by.""" return self.coordinator.data["locks"][self.serial_number].get("userString") + @property + def changed_method(self) -> str: + """Last change method.""" + return self.coordinator.data["locks"][self.serial_number]["method"] + @property def code_format(self) -> str: """Return the required six digit code.""" @@ -112,6 +117,11 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt == "LOCKED" ) + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return {"method": self.changed_method} + async def async_unlock(self, **kwargs) -> None: """Send unlock command.""" code = kwargs.get( From 1931600eac9d62b00efa2897acb735bfafb73851 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 3 May 2022 10:36:58 +0200 Subject: [PATCH 0144/3516] Isolate parallel subscripts (#71233) --- homeassistant/helpers/script.py | 18 ++-- tests/helpers/test_script.py | 146 +++++++++++++++++++++++++++++++- 2 files changed, 157 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index d988b0edd81..3d43e03ddce 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1133,13 +1133,14 @@ class Script: domain: str, *, # Used in "Running " log message - running_description: str | None = None, change_listener: Callable[..., Any] | None = None, - script_mode: str = DEFAULT_SCRIPT_MODE, - max_runs: int = DEFAULT_MAX, - max_exceeded: str = DEFAULT_MAX_EXCEEDED, - logger: logging.Logger | None = None, + copy_variables: bool = False, log_exceptions: bool = True, + logger: logging.Logger | None = None, + max_exceeded: str = DEFAULT_MAX_EXCEEDED, + max_runs: int = DEFAULT_MAX, + running_description: str | None = None, + script_mode: str = DEFAULT_SCRIPT_MODE, top_level: bool = True, variables: ScriptVariables | None = None, ) -> None: @@ -1192,6 +1193,7 @@ class Script: self._variables_dynamic = template.is_complex(variables) if self._variables_dynamic: template.attach(hass, variables) + self._copy_variables_on_run = copy_variables @property def change_listener(self) -> Callable[..., Any] | None: @@ -1454,7 +1456,10 @@ class Script: variables["context"] = context else: - variables = cast(dict, run_variables) + if self._copy_variables_on_run: + variables = cast(dict, copy(run_variables)) + else: + variables = cast(dict, run_variables) # Prevent non-allowed recursive calls which will cause deadlocks when we try to # stop (restart) or wait for (queued) our own script run. @@ -1671,6 +1676,7 @@ class Script: max_runs=self.max_runs, logger=self._logger, top_level=False, + copy_variables=True, ) parallel_script.change_listener = partial( self._chain_change_listener, parallel_script diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index b9c968838c9..4791dd84cc4 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -3027,7 +3027,7 @@ async def test_parallel(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) - ], "0/parallel/1/sequence/0": [ { - "variables": {"wait": {"remaining": None}}, + "variables": {}, "result": { "event": "test_event", "event_data": {"hello": "from action 2", "what": "world"}, @@ -3047,6 +3047,150 @@ async def test_parallel(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) - assert_action_trace(expected_trace) +async def test_parallel_loop( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test parallel loops do not affect each other.""" + events_loop1 = async_capture_events(hass, "loop1") + events_loop2 = async_capture_events(hass, "loop2") + hass.states.async_set("switch.trigger", "off") + + sequence = cv.SCRIPT_SCHEMA( + { + "parallel": [ + { + "alias": "Loop1", + "sequence": [ + { + "repeat": { + "for_each": ["loop1_a", "loop1_b", "loop1_c"], + "sequence": [ + { + "event": "loop1", + "event_data": {"hello1": "{{ repeat.item }}"}, + } + ], + }, + }, + ], + }, + { + "alias": "Loop2", + "sequence": [ + { + "repeat": { + "for_each": ["loop2_a", "loop2_b", "loop2_c"], + "sequence": [ + { + "event": "loop2", + "event_data": {"hello2": "{{ repeat.item }}"}, + } + ], + }, + }, + ], + }, + ] + } + ) + + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + hass.async_create_task( + script_obj.async_run(MappingProxyType({"what": "world"}), Context()) + ) + await hass.async_block_till_done() + + assert len(events_loop1) == 3 + assert events_loop1[0].data["hello1"] == "loop1_a" + assert events_loop1[1].data["hello1"] == "loop1_b" + assert events_loop1[2].data["hello1"] == "loop1_c" + assert events_loop2[0].data["hello2"] == "loop2_a" + assert events_loop2[1].data["hello2"] == "loop2_b" + assert events_loop2[2].data["hello2"] == "loop2_c" + + expected_trace = { + "0": [{"result": {}}], + "0/parallel/0/sequence/0": [{"result": {}}], + "0/parallel/1/sequence/0": [ + { + "result": {}, + } + ], + "0/parallel/0/sequence/0/repeat/sequence/0": [ + { + "variables": { + "repeat": { + "first": True, + "index": 1, + "last": False, + "item": "loop1_a", + } + }, + "result": {"event": "loop1", "event_data": {"hello1": "loop1_a"}}, + }, + { + "variables": { + "repeat": { + "first": False, + "index": 2, + "last": False, + "item": "loop1_b", + } + }, + "result": {"event": "loop1", "event_data": {"hello1": "loop1_b"}}, + }, + { + "variables": { + "repeat": { + "first": False, + "index": 3, + "last": True, + "item": "loop1_c", + } + }, + "result": {"event": "loop1", "event_data": {"hello1": "loop1_c"}}, + }, + ], + "0/parallel/1/sequence/0/repeat/sequence/0": [ + { + "variables": { + "repeat": { + "first": True, + "index": 1, + "last": False, + "item": "loop2_a", + } + }, + "result": {"event": "loop2", "event_data": {"hello2": "loop2_a"}}, + }, + { + "variables": { + "repeat": { + "first": False, + "index": 2, + "last": False, + "item": "loop2_b", + } + }, + "result": {"event": "loop2", "event_data": {"hello2": "loop2_b"}}, + }, + { + "variables": { + "repeat": { + "first": False, + "index": 3, + "last": True, + "item": "loop2_c", + } + }, + "result": {"event": "loop2", "event_data": {"hello2": "loop2_c"}}, + }, + ], + } + assert_action_trace(expected_trace) + + async def test_parallel_error( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: From 92f1855bcfaad514aaa9614a91a426c2e52402ce Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 3 May 2022 11:28:08 +0200 Subject: [PATCH 0145/3516] Fix script conditions (#71235) --- homeassistant/helpers/script.py | 32 +++++++++++++++++++++++--------- tests/helpers/test_script.py | 5 +---- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 3d43e03ddce..0b755ae0032 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -205,6 +205,10 @@ async def trace_action(hass, script_run, stop, variables): except _AbortScript as ex: trace_element.set_error(ex.__cause__ or ex) raise ex + except _ConditionFail as ex: + # Clear errors which may have been set when evaluating the condition + trace_element.set_error(None) + raise ex except _StopScript as ex: raise ex except Exception as ex: @@ -325,11 +329,19 @@ async def async_validate_action_config( return config -class _AbortScript(Exception): +class _HaltScript(Exception): + """Throw if script needs to stop executing.""" + + +class _AbortScript(_HaltScript): """Throw if script needs to abort because of an unexpected error.""" -class _StopScript(Exception): +class _ConditionFail(_HaltScript): + """Throw if script needs to stop because a condition evaluated to False.""" + + +class _StopScript(_HaltScript): """Throw if script needs to stop.""" @@ -393,16 +405,18 @@ class _ScriptRun: await self._async_step(log_exceptions=False) else: script_execution_set("finished") - except _StopScript: - script_execution_set("finished") - # Let the _StopScript bubble up if this is a sub-script - if not self._script.top_level: - raise except _AbortScript: script_execution_set("aborted") # Let the _AbortScript bubble up if this is a sub-script if not self._script.top_level: raise + except _ConditionFail: + script_execution_set("aborted") + except _StopScript: + script_execution_set("finished") + # Let the _StopScript bubble up if this is a sub-script + if not self._script.top_level: + raise except Exception: script_execution_set("error") raise @@ -450,7 +464,7 @@ class _ScriptRun: def _handle_exception( self, exception: Exception, continue_on_error: bool, log_exceptions: bool ) -> None: - if not isinstance(exception, (_AbortScript, _StopScript)) and log_exceptions: + if not isinstance(exception, _HaltScript) and log_exceptions: self._log_exception(exception) if not continue_on_error: @@ -726,7 +740,7 @@ class _ScriptRun: self._log("Test condition %s: %s", self._script.last_action, check) trace_update_result(result=check) if not check: - raise _AbortScript + raise _ConditionFail def _test_conditions(self, conditions, name, condition_path=None): if condition_path is None: diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 4791dd84cc4..c01f086a12a 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1445,7 +1445,7 @@ async def test_condition_warning(hass, caplog): assert_action_trace( { "0": [{"result": {"event": "test_event", "event_data": {}}}], - "1": [{"error_type": script._AbortScript, "result": {"result": False}}], + "1": [{"result": {"result": False}}], "1/entity_id/0": [{"error_type": ConditionError}], }, expected_script_execution="aborted", @@ -1499,7 +1499,6 @@ async def test_condition_basic(hass, caplog): "0": [{"result": {"event": "test_event", "event_data": {}}}], "1": [ { - "error_type": script._AbortScript, "result": {"entities": ["test.entity"], "result": False}, } ], @@ -1590,7 +1589,6 @@ async def test_shorthand_template_condition(hass, caplog): "0": [{"result": {"event": "test_event", "event_data": {}}}], "1": [ { - "error_type": script._AbortScript, "result": {"entities": ["test.entity"], "result": False}, } ], @@ -1656,7 +1654,6 @@ async def test_condition_validation(hass, caplog): "0": [{"result": {"event": "test_event", "event_data": {}}}], "1": [ { - "error_type": script._AbortScript, "result": {"result": False}, } ], From 71248bcbcef25290afdc727a4182670aa41e1ada Mon Sep 17 00:00:00 2001 From: Sven <85389871+wrt54g@users.noreply.github.com> Date: Tue, 3 May 2022 12:34:57 +0200 Subject: [PATCH 0146/3516] Update images (#71215) --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index cf8323d2e81..05e13695a58 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ of a component, check the `Home Assistant help section Date: Tue, 3 May 2022 13:04:59 +0200 Subject: [PATCH 0147/3516] Indicate disabled steps in script trace (#71237) --- homeassistant/helpers/script.py | 1 + tests/helpers/test_script.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 0b755ae0032..54ae4f456ab 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -439,6 +439,7 @@ class _ScriptRun: self._log( "Skipped disabled step %s", self._action.get(CONF_ALIAS, action) ) + trace_set_result(enabled=False) return try: diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index c01f086a12a..136729e62fc 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -4939,8 +4939,8 @@ async def test_disabled_actions( assert_action_trace( { "0": [{"result": {"event": "test_event", "event_data": {}}}], - "1": [{}], - "2": [{}], + "1": [{"result": {"enabled": False}}], + "2": [{"result": {"enabled": False}}], "3": [{"result": {"event": "test_event", "event_data": {}}}], }, ) From eb10654e011621b4a7c3c5355f44587cf13270ca Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 3 May 2022 13:06:13 +0200 Subject: [PATCH 0148/3516] Add test for failing conditions in sub scripts (#71238) --- tests/helpers/test_script.py | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 136729e62fc..519498f2e08 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1507,6 +1507,55 @@ async def test_condition_basic(hass, caplog): ) +async def test_condition_subscript(hass, caplog): + """Test failing conditions in a subscript don't stop the parent script.""" + event = "test_event" + events = async_capture_events(hass, event) + sequence = cv.SCRIPT_SCHEMA( + [ + {"event": event}, + { + "repeat": { + "until": "{{ 1 == 1 }}", + "sequence": [ + {"condition": "{{ 1 == 2 }}"}, + ], + } + }, + {"event": event}, + ] + ) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + hass.states.async_set("test.entity", "hello") + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + caplog.clear() + assert len(events) == 2 + + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{"result": {}}], + "1/repeat/sequence/0": [ + { + "variables": {"repeat": {"first": True, "index": 1}}, + "result": {"entities": [], "result": False}, + } + ], + "1/repeat": [ + { + "variables": {"repeat": {"first": True, "index": 1}}, + "result": {"result": True}, + } + ], + "1/repeat/until/0": [{"result": {"entities": [], "result": True}}], + "2": [{"result": {"event": "test_event", "event_data": {}}}], + } + ) + + async def test_and_default_condition(hass, caplog): """Test that a list of conditions evaluates as AND.""" alias = "condition step" From b562416eb218c5d96b8ba5f462f70bc9308aa905 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 May 2022 09:33:50 -0500 Subject: [PATCH 0149/3516] Remove humidify_supported and dehumidify_supported attributes from nexia (#71248) These non-standard attributes can already be infered from the dehumidify_setpoint or humidify_setpoint and took up space in the database every time any of the values changes --- homeassistant/components/nexia/climate.py | 9 --------- homeassistant/components/nexia/const.py | 2 -- tests/components/nexia/test_climate.py | 4 ---- 3 files changed, 15 deletions(-) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index fc85f72e503..7eeb04c5676 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -38,9 +38,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ATTR_AIRCLEANER_MODE, ATTR_DEHUMIDIFY_SETPOINT, - ATTR_DEHUMIDIFY_SUPPORTED, ATTR_HUMIDIFY_SETPOINT, - ATTR_HUMIDIFY_SUPPORTED, ATTR_RUN_MODE, ATTR_ZONE_STATUS, DOMAIN, @@ -356,13 +354,6 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): if not self._has_relative_humidity: return data - data.update( - { - ATTR_DEHUMIDIFY_SUPPORTED: self._has_dehumidify_support, - ATTR_HUMIDIFY_SUPPORTED: self._has_humidify_support, - } - ) - if self._has_dehumidify_support: dehumdify_setpoint = percent_conv( self._thermostat.get_dehumidify_setpoint() diff --git a/homeassistant/components/nexia/const.py b/homeassistant/components/nexia/const.py index 4fa3cb022f8..5d84e8a3075 100644 --- a/homeassistant/components/nexia/const.py +++ b/homeassistant/components/nexia/const.py @@ -28,8 +28,6 @@ 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" ATTR_HUMIDIFY_SETPOINT = "humidify_setpoint" ATTR_DEHUMIDIFY_SETPOINT = "dehumidify_setpoint" diff --git a/tests/components/nexia/test_climate.py b/tests/components/nexia/test_climate.py index edd5d56e79a..312d78ac256 100644 --- a/tests/components/nexia/test_climate.py +++ b/tests/components/nexia/test_climate.py @@ -16,11 +16,9 @@ async def test_climate_zones(hass): "current_humidity": 52.0, "current_temperature": 22.8, "dehumidify_setpoint": 45.0, - "dehumidify_supported": True, "fan_mode": "Auto", "fan_modes": ["Auto", "On", "Circulate"], "friendly_name": "Nick Office", - "humidify_supported": False, "humidity": 45.0, "hvac_action": "cooling", "hvac_modes": ["off", "auto", "heat_cool", "heat", "cool"], @@ -51,11 +49,9 @@ async def test_climate_zones(hass): "current_humidity": 36.0, "current_temperature": 25.0, "dehumidify_setpoint": 50.0, - "dehumidify_supported": True, "fan_mode": "Auto", "fan_modes": ["Auto", "On", "Circulate"], "friendly_name": "Kitchen", - "humidify_supported": False, "humidity": 50.0, "hvac_action": "idle", "hvac_modes": ["off", "auto", "heat_cool", "heat", "cool"], From 08b683dafd0bc354880881581666efa981655658 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 May 2022 07:42:06 -0700 Subject: [PATCH 0150/3516] Fix homepod streaming and browsing apps (#71230) --- .../components/apple_tv/media_player.py | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 02919043bb2..6c1b35f70f8 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -79,7 +79,8 @@ SUPPORT_APPLE_TV = ( SUPPORT_FEATURE_MAPPING = { FeatureName.PlayUrl: MediaPlayerEntityFeature.BROWSE_MEDIA | MediaPlayerEntityFeature.PLAY_MEDIA, - FeatureName.StreamFile: MediaPlayerEntityFeature.PLAY_MEDIA, + FeatureName.StreamFile: MediaPlayerEntityFeature.BROWSE_MEDIA + | MediaPlayerEntityFeature.PLAY_MEDIA, FeatureName.Pause: MediaPlayerEntityFeature.PAUSE, FeatureName.Play: MediaPlayerEntityFeature.PLAY, FeatureName.SetPosition: MediaPlayerEntityFeature.SEEK, @@ -282,23 +283,20 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): if media_type == MEDIA_TYPE_APP: await self.atv.apps.launch_app(media_id) - is_media_source_id = media_source.is_media_source_id(media_id) + if media_source.is_media_source_id(media_id): + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + media_type = MEDIA_TYPE_MUSIC - if ( - not is_media_source_id - and self._is_feature_available(FeatureName.StreamFile) - and (await is_streamable(media_id) or media_type == MEDIA_TYPE_MUSIC) + media_id = async_process_play_media_url(self.hass, media_id) + + if self._is_feature_available(FeatureName.StreamFile) and ( + media_type == MEDIA_TYPE_MUSIC or await is_streamable(media_id) ): _LOGGER.debug("Streaming %s via RAOP", media_id) await self.atv.stream.stream_file(media_id) if self._is_feature_available(FeatureName.PlayUrl): - if is_media_source_id: - play_item = await media_source.async_resolve_media(self.hass, media_id) - media_id = play_item.url - - media_id = async_process_play_media_url(self.hass, media_id) - _LOGGER.debug("Playing %s via AirPlay", media_id) await self.atv.stream.play_url(media_id) else: @@ -397,9 +395,12 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): media_content_id=None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" - # If we can't stream URLs, we can't browse media. - # In that case the `BROWSE_MEDIA` feature was added because of AppList/LaunchApp - if not self._is_feature_available(FeatureName.PlayUrl): + if media_content_id == "apps" or ( + # If we can't stream files or URLs, we can't browse media. + # In that case the `BROWSE_MEDIA` feature was added because of AppList/LaunchApp + not self._is_feature_available(FeatureName.PlayUrl) + and not self._is_feature_available(FeatureName.StreamFile) + ): return build_app_list(self._app_list) if self._app_list: From eba125b0939eb2c9acfde97d9ac1aca206d3aac8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 3 May 2022 16:43:44 +0200 Subject: [PATCH 0151/3516] Ensure 'this' variable is always defined for template entities (#70911) --- .../components/template/template_entity.py | 30 ++++- homeassistant/helpers/template.py | 3 +- tests/components/template/test_sensor.py | 114 ++++++++++++++++++ 3 files changed, 140 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index d7d6ab46c62..a6d1cba78e1 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -17,8 +17,9 @@ from homeassistant.const import ( CONF_ICON_TEMPLATE, CONF_NAME, EVENT_HOMEASSISTANT_START, + STATE_UNKNOWN, ) -from homeassistant.core import CoreState, Event, callback +from homeassistant.core import CoreState, Event, State, callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -251,13 +252,28 @@ class TemplateEntity(Entity): self._entity_picture_template = config.get(CONF_PICTURE) self._friendly_name_template = config.get(CONF_NAME) + class DummyState(State): + """None-state for template entities not yet added to the state machine.""" + + def __init__(self) -> None: + """Initialize a new state.""" + super().__init__("unknown.unknown", STATE_UNKNOWN) + self.entity_id = None # type: ignore[assignment] + + @property + def name(self) -> str: + """Name of this state.""" + return "" + + variables = {"this": DummyState()} + # Try to render the name as it can influence the entity ID self._attr_name = fallback_name if self._friendly_name_template: self._friendly_name_template.hass = hass with contextlib.suppress(TemplateError): self._attr_name = self._friendly_name_template.async_render( - parse_result=False + variables=variables, parse_result=False ) # Templates will not render while the entity is unavailable, try to render the @@ -266,13 +282,15 @@ class TemplateEntity(Entity): self._entity_picture_template.hass = hass with contextlib.suppress(TemplateError): self._attr_entity_picture = self._entity_picture_template.async_render( - parse_result=False + variables=variables, parse_result=False ) if self._icon_template: self._icon_template.hass = hass with contextlib.suppress(TemplateError): - self._attr_icon = self._icon_template.async_render(parse_result=False) + self._attr_icon = self._icon_template.async_render( + variables=variables, parse_result=False + ) @callback def _update_available(self, result): @@ -373,10 +391,10 @@ class TemplateEntity(Entity): template_var_tups: list[TrackTemplate] = [] has_availability_template = False - values = {"this": TemplateStateFromEntityId(self.hass, self.entity_id)} + variables = {"this": TemplateStateFromEntityId(self.hass, self.entity_id)} for template, attributes in self._template_attrs.items(): - template_var_tup = TrackTemplate(template, values) + template_var_tup = TrackTemplate(template, variables) is_availability_template = False for attribute in attributes: # pylint: disable-next=protected-access diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index dbc82ce6902..3f084674a1b 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -850,7 +850,8 @@ class TemplateStateFromEntityId(TemplateStateBase): @property def _state(self) -> State: # type: ignore[override] # mypy issue 4125 state = self._hass.states.get(self._entity_id) - assert state + if not state: + state = State(self._entity_id, STATE_UNKNOWN) return state def __repr__(self) -> str: diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 297008e77bb..ddf13c2015b 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -653,6 +653,120 @@ async def test_this_variable(hass, start_ha): assert hass.states.get(TEST_NAME).state == "It Works: " + TEST_NAME +@pytest.mark.parametrize("count,domain", [(1, "template")]) +@pytest.mark.parametrize( + "config", + [ + { + "template": { + "sensor": { + "state": "{{ this.attributes.get('test', 'no-test!') }}: {{ this.entity_id }}", + "icon": "mdi:{% if this.entity_id in states and 'friendly_name' in this.attributes %} {{this.attributes['friendly_name']}} {% else %}{{this.entity_id}}:{{this.entity_id in states}}{% endif %}", + "name": "{% if this.entity_id in states and 'friendly_name' in this.attributes %} {{this.attributes['friendly_name']}} {% else %}{{this.entity_id}}:{{this.entity_id in states}}{% endif %}", + "picture": "{% if this.entity_id in states and 'entity_picture' in this.attributes %} {{this.attributes['entity_picture']}} {% else %}{{this.entity_id}}:{{this.entity_id in states}}{% endif %}", + "attributes": {"test": "{{ this.entity_id }}"}, + }, + }, + }, + ], +) +async def test_this_variable_early_hass_not_running(hass, config, count, domain): + """Test referencing 'this' variable before the entity is in the state machine. + + Hass is not yet started when the entity is added. + Icon, name and picture templates are rendered once in the constructor. + """ + entity_id = "sensor.none_false" + + hass.state = CoreState.not_running + + # Setup template + with assert_setup_component(count, domain): + assert await async_setup_component( + hass, + domain, + config, + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Sensor state not rendered, icon, name and picture + # templates rendered in constructor with entity_id set to None + state = hass.states.get(entity_id) + assert state.state == "unknown" + assert state.attributes == { + "entity_picture": "None:False", + "friendly_name": "None:False", + "icon": "mdi:None:False", + } + + # Signal hass started + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + # Re-render icon, name, pciture + other templates now rendered + state = hass.states.get(entity_id) + assert state.state == "sensor.none_false: sensor.none_false" + assert state.attributes == { + "entity_picture": "sensor.none_false:False", + "friendly_name": "sensor.none_false:False", + "icon": "mdi:sensor.none_false:False", + "test": "sensor.none_false", + } + + +@pytest.mark.parametrize("count,domain", [(1, "template")]) +@pytest.mark.parametrize( + "config", + [ + { + "template": { + "sensor": { + "state": "{{ this.attributes.get('test', 'no-test!') }}: {{ this.entity_id }}", + "icon": "mdi:{% if this.entity_id in states and 'friendly_name' in this.attributes %} {{this.attributes['friendly_name']}} {% else %}{{this.entity_id}}:{{this.entity_id in states}}{% endif %}", + "name": "{% if this.entity_id in states and 'friendly_name' in this.attributes %} {{this.attributes['friendly_name']}} {% else %}{{this.entity_id}}:{{this.entity_id in states}}{% endif %}", + "picture": "{% if this.entity_id in states and 'entity_picture' in this.attributes %} {{this.attributes['entity_picture']}} {% else %}{{this.entity_id}}:{{this.entity_id in states}}{% endif %}", + "attributes": {"test": "{{ this.entity_id }}"}, + }, + }, + }, + ], +) +async def test_this_variable_early_hass_running(hass, config, count, domain): + """Test referencing 'this' variable before the entity is in the state machine. + + Hass is already started when the entity is added. + Icon, name and picture templates are rendered in the constructor, and again + before the entity is added to hass. + """ + + # Start hass + assert hass.state == CoreState.running + await hass.async_start() + await hass.async_block_till_done() + + # Setup template + with assert_setup_component(count, domain): + assert await async_setup_component( + hass, + domain, + config, + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + + entity_id = "sensor.none_false" + # All templated rendered + state = hass.states.get(entity_id) + assert state.state == "sensor.none_false: sensor.none_false" + assert state.attributes == { + "entity_picture": "sensor.none_false:False", + "friendly_name": "sensor.none_false:False", + "icon": "mdi:sensor.none_false:False", + "test": "sensor.none_false", + } + + @pytest.mark.parametrize("count,domain", [(1, sensor.DOMAIN)]) @pytest.mark.parametrize( "config", From 60bfcc6be4d7d3944995182a038075c126221af6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 May 2022 11:47:13 -0500 Subject: [PATCH 0152/3516] Allow hidden entities to be selected in homekit include mode (#71250) --- .../components/homekit/config_flow.py | 10 ++++--- tests/components/homekit/test_config_flow.py | 28 +++++++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index b7a00bc3ade..5f142fdb0fe 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -467,7 +467,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): entity_filter = self.hk_options.get(CONF_FILTER, {}) entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) all_supported_entities = _async_get_matching_entities( - self.hass, domains, include_entity_category=True + self.hass, domains, include_entity_category=True, include_hidden=True ) # In accessory mode we can only have one default_value = next( @@ -508,7 +508,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) all_supported_entities = _async_get_matching_entities( - self.hass, domains, include_entity_category=True + self.hass, domains, include_entity_category=True, include_hidden=True ) if not entities: entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, []) @@ -646,12 +646,13 @@ def _exclude_by_entity_registry( ent_reg: entity_registry.EntityRegistry, entity_id: str, include_entity_category: bool, + include_hidden: bool, ) -> bool: """Filter out hidden entities and ones with entity category (unless specified).""" return bool( (entry := ent_reg.async_get(entity_id)) and ( - entry.hidden_by is not None + (not include_hidden and entry.hidden_by is not None) or (not include_entity_category and entry.entity_category is not None) ) ) @@ -661,6 +662,7 @@ def _async_get_matching_entities( hass: HomeAssistant, domains: list[str] | None = None, include_entity_category: bool = False, + include_hidden: bool = False, ) -> dict[str, str]: """Fetch all entities or entities in the given domains.""" ent_reg = entity_registry.async_get(hass) @@ -671,7 +673,7 @@ def _async_get_matching_entities( key=lambda item: item.entity_id, ) if not _exclude_by_entity_registry( - ent_reg, state.entity_id, include_entity_category + ent_reg, state.entity_id, include_entity_category, include_hidden ) } diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 42ce6779528..ce0bed0ff52 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -1504,7 +1504,7 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( @patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) -async def test_options_flow_include_mode_skips_hidden_entities( +async def test_options_flow_include_mode_allows_hidden_entities( port_mock, hass, mock_get_source_ip, hk_driver, mock_async_zeroconf, entity_reg ): """Ensure include mode does not offer hidden entities.""" @@ -1558,24 +1558,28 @@ async def test_options_flow_include_mode_skips_hidden_entities( assert _get_schema_default(result2["data_schema"].schema, "entities") == [] # sonos_hidden_switch.entity_id is a hidden entity - # so it should not be selectable since it will always be excluded - with pytest.raises(voluptuous.error.MultipleInvalid): - await hass.config_entries.options.async_configure( - result2["flow_id"], - user_input={"entities": [sonos_hidden_switch.entity_id]}, - ) - - result4 = await hass.config_entries.options.async_configure( + # we allow it to be selected in include mode only + result3 = await hass.config_entries.options.async_configure( result2["flow_id"], - user_input={"entities": ["media_player.tv", "switch.other"]}, + user_input={ + "entities": [ + sonos_hidden_switch.entity_id, + "media_player.tv", + "switch.other", + ] + }, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { "exclude_domains": [], "exclude_entities": [], "include_domains": [], - "include_entities": ["media_player.tv", "switch.other"], + "include_entities": [ + sonos_hidden_switch.entity_id, + "media_player.tv", + "switch.other", + ], }, } From 8d40d9df8511e90e73a3a105a4fd1b32b63197c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 May 2022 11:49:52 -0500 Subject: [PATCH 0153/3516] Create ISY auxiliary sensors as sensor entities instead of attributes (#71254) --- homeassistant/components/isy994/__init__.py | 3 +- homeassistant/components/isy994/const.py | 2 + homeassistant/components/isy994/entity.py | 19 +++-- homeassistant/components/isy994/helpers.py | 5 ++ homeassistant/components/isy994/sensor.py | 93 +++++++++++++++++++-- 5 files changed, 104 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index faa2f7cfb5d..e94b8215746 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -41,6 +41,7 @@ from .const import ( MANUFACTURER, PLATFORMS, PROGRAM_PLATFORMS, + SENSOR_AUX, ) from .helpers import _categorize_nodes, _categorize_programs, _categorize_variables from .services import async_setup_services, async_unload_services @@ -120,7 +121,7 @@ async def async_setup_entry( hass.data[DOMAIN][entry.entry_id] = {} hass_isy_data = hass.data[DOMAIN][entry.entry_id] - hass_isy_data[ISY994_NODES] = {} + hass_isy_data[ISY994_NODES] = {SENSOR_AUX: []} for platform in PLATFORMS: hass_isy_data[ISY994_NODES][platform] = [] diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index bc463655f27..ddabe1b9680 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -191,6 +191,8 @@ UOM_INDEX = "25" UOM_ON_OFF = "2" UOM_PERCENTAGE = "51" +SENSOR_AUX = "sensor_aux" + # Do not use the Home Assistant consts for the states here - we're matching exact API # responses, not using them for Home Assistant states # Insteon Types: https://www.universal-devices.com/developers/wsdk/5.0.4/1_fam.xml diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index e5db8de5872..54ee9a2ded5 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -8,6 +8,7 @@ from pyisy.constants import ( EMPTY_TIME, EVENT_PROPS_IGNORED, PROTO_GROUP, + PROTO_INSTEON, PROTO_ZWAVE, ) from pyisy.helpers import EventListener, NodeProperty @@ -35,6 +36,7 @@ class ISYEntity(Entity): """Representation of an ISY994 device.""" _name: str | None = None + _attr_should_poll = False def __init__(self, node: Node) -> None: """Initialize the insteon device.""" @@ -86,7 +88,7 @@ class ISYEntity(Entity): node = self._node url = _async_isy_to_configuration_url(isy) - basename = self.name + basename = self._name or str(self._node.name) if hasattr(self._node, "parent_node") and self._node.parent_node is not None: # This is not the parent node, get the parent node. @@ -151,11 +153,6 @@ class ISYEntity(Entity): """Get the name of the device.""" return self._name or str(self._node.name) - @property - def should_poll(self) -> bool: - """No polling required since we're using the subscription.""" - return False - class ISYNodeEntity(ISYEntity): """Representation of a ISY Nodebase (Node/Group) entity.""" @@ -169,9 +166,13 @@ class ISYNodeEntity(ISYEntity): the combined result are returned as the device state attributes. """ attr = {} - if hasattr(self._node, "aux_properties"): - # Cast as list due to RuntimeError if a new property is added while running. - for name, value in list(self._node.aux_properties.items()): + node = self._node + # Insteon aux_properties are now their own sensors + if ( + hasattr(self._node, "aux_properties") + and getattr(node, "protocol", None) != PROTO_INSTEON + ): + for name, value in self._node.aux_properties.items(): attr_name = COMMAND_FRIENDLY_NAME.get(name, name) attr[attr_name] = str(value.formatted).lower() diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 6d0a1d303bb..7efbb3bade7 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -44,6 +44,7 @@ from .const import ( NODE_FILTERS, PLATFORMS, PROGRAM_PLATFORMS, + SENSOR_AUX, SUBNODE_CLIMATE_COOL, SUBNODE_CLIMATE_HEAT, SUBNODE_EZIO2X4_SENSORS, @@ -295,6 +296,10 @@ def _categorize_nodes( hass_isy_data[ISY994_NODES][ISY_GROUP_PLATFORM].append(node) continue + if getattr(node, "protocol", None) == PROTO_INSTEON: + for control in node.aux_properties: + hass_isy_data[ISY994_NODES][SENSOR_AUX].append((node, control)) + if sensor_identifier in path or sensor_identifier in node.name: # User has specified to treat this as a sensor. First we need to # determine if it should be a binary_sensor. diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index d9751fd707b..2dee990c249 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -3,9 +3,15 @@ from __future__ import annotations from typing import Any, cast -from pyisy.constants import ISY_VALUE_UNKNOWN +from pyisy.constants import COMMAND_FRIENDLY_NAME, ISY_VALUE_UNKNOWN +from pyisy.helpers import NodeProperty +from pyisy.nodes import Node -from homeassistant.components.sensor import DOMAIN as SENSOR, SensorEntity +from homeassistant.components.sensor import ( + DOMAIN as SENSOR, + SensorDeviceClass, + SensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant @@ -16,6 +22,7 @@ from .const import ( DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_VARIABLES, + SENSOR_AUX, UOM_DOUBLE_TEMP, UOM_FRIENDLY_NAME, UOM_INDEX, @@ -25,6 +32,20 @@ from .const import ( from .entity import ISYEntity, ISYNodeEntity from .helpers import convert_isy_value_to_hass, migrate_old_unique_ids +# Disable general purpose and redundant sensors by default +AUX_DISABLED_BY_DEFAULT = ["ERR", "GV", "CLIEMD", "CLIHCS", "DO", "OL", "RR", "ST"] + +ISY_CONTROL_TO_DEVICE_CLASS = { + "BARPRES": SensorDeviceClass.PRESSURE, + "BATLVL": SensorDeviceClass.BATTERY, + "CLIHUM": SensorDeviceClass.HUMIDITY, + "CLITEMP": SensorDeviceClass.TEMPERATURE, + "CO2LVL": SensorDeviceClass.CO2, + "CV": SensorDeviceClass.VOLTAGE, + "LUMIN": SensorDeviceClass.ILLUMINANCE, + "PF": SensorDeviceClass.POWER_FACTOR, +} + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -37,6 +58,13 @@ async def async_setup_entry( _LOGGER.debug("Loading %s", node.name) entities.append(ISYSensorEntity(node)) + for node, control in hass_isy_data[ISY994_NODES][SENSOR_AUX]: + _LOGGER.debug("Loading %s %s", node.name, node.aux_properties[control]) + enabled_default = not any( + control.startswith(match) for match in AUX_DISABLED_BY_DEFAULT + ) + entities.append(ISYAuxSensorEntity(node, control, enabled_default)) + for vname, vobj in hass_isy_data[ISY994_VARIABLES]: entities.append(ISYSensorVariableEntity(vname, vobj)) @@ -47,10 +75,20 @@ async def async_setup_entry( class ISYSensorEntity(ISYNodeEntity, SensorEntity): """Representation of an ISY994 sensor device.""" + @property + def target(self) -> Node | NodeProperty: + """Return target for the sensor.""" + return self._node + + @property + def target_value(self) -> Any: + """Return the target value.""" + return self._node.status + @property def raw_unit_of_measurement(self) -> dict | str | None: """Get the raw unit of measurement for the ISY994 sensor device.""" - uom = self._node.uom + uom = self.target.uom # Backwards compatibility for ISYv4 Firmware: if isinstance(uom, list): @@ -69,7 +107,7 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): @property def native_value(self) -> float | int | str | None: """Get the state of the ISY994 sensor device.""" - if (value := self._node.status) == ISY_VALUE_UNKNOWN: + if (value := self.target_value) == ISY_VALUE_UNKNOWN: return None # Get the translated ISY Unit of Measurement @@ -80,14 +118,14 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): return uom.get(value, value) if uom in (UOM_INDEX, UOM_ON_OFF): - return cast(str, self._node.formatted) + return cast(str, self.target.formatted) # Check if this is an index type and get formatted value - if uom == UOM_INDEX and hasattr(self._node, "formatted"): - return cast(str, self._node.formatted) + if uom == UOM_INDEX and hasattr(self.target, "formatted"): + return cast(str, self.target.formatted) # Handle ISY precision and rounding - value = convert_isy_value_to_hass(value, uom, self._node.prec) + value = convert_isy_value_to_hass(value, uom, self.target.prec) # Convert temperatures to Home Assistant's unit if uom in (TEMP_CELSIUS, TEMP_FAHRENHEIT): @@ -111,6 +149,45 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): return raw_units +class ISYAuxSensorEntity(ISYSensorEntity): + """Representation of an ISY994 aux sensor device.""" + + def __init__(self, node: Node, control: str, enabled_default: bool) -> None: + """Initialize the ISY994 aux sensor.""" + super().__init__(node) + self._control = control + self._attr_entity_registry_enabled_default = enabled_default + + @property + def device_class(self) -> SensorDeviceClass | str | None: + """Return the device class for the sensor.""" + return ISY_CONTROL_TO_DEVICE_CLASS.get(self._control, super().device_class) + + @property + def target(self) -> Node | NodeProperty: + """Return target for the sensor.""" + return cast(NodeProperty, self._node.aux_properties[self._control]) + + @property + def target_value(self) -> Any: + """Return the target value.""" + return self.target.value + + @property + def unique_id(self) -> str | None: + """Get the unique identifier of the device and aux sensor.""" + if not hasattr(self._node, "address"): + return None + return f"{self._node.isy.configuration['uuid']}_{self._node.address}_{self._control}" + + @property + def name(self) -> str: + """Get the name of the device and aux sensor.""" + base_name = self._name or str(self._node.name) + name = COMMAND_FRIENDLY_NAME.get(self._control, self._control) + return f"{base_name} {name.replace('_', ' ').title()}" + + class ISYSensorVariableEntity(ISYEntity, SensorEntity): """Representation of an ISY994 variable as a sensor device.""" From 0580803b7d2c4de0533edc330ed30d36804b1df3 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 3 May 2022 19:38:20 +0200 Subject: [PATCH 0154/3516] Prevent Netgear SSDP from updating host (#71240) --- homeassistant/components/netgear/config_flow.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/netgear/config_flow.py b/homeassistant/components/netgear/config_flow.py index 85c206ce463..79053c712fc 100644 --- a/homeassistant/components/netgear/config_flow.py +++ b/homeassistant/components/netgear/config_flow.py @@ -19,6 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.util.network import is_ipv4_address from .const import ( CONF_CONSIDER_HOME, @@ -129,6 +130,9 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): hostname = cast(str, hostname) updated_data[CONF_HOST] = hostname + if not is_ipv4_address(str(hostname)): + return self.async_abort(reason="not_ipv4_address") + _LOGGER.debug("Netgear ssdp discovery info: %s", discovery_info) await self.async_set_unique_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]) From e1be6dd34f3258bb568b630a67d869b7e98cd438 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 May 2022 12:38:44 -0500 Subject: [PATCH 0155/3516] Move recorder services to services.py (#71249) --- .strict-typing | 1 + homeassistant/components/recorder/__init__.py | 85 +------------- homeassistant/components/recorder/services.py | 106 ++++++++++++++++++ mypy.ini | 11 ++ tests/components/recorder/test_init.py | 10 +- tests/components/recorder/test_purge.py | 66 ++++------- 6 files changed, 147 insertions(+), 132 deletions(-) create mode 100644 homeassistant/components/recorder/services.py diff --git a/.strict-typing b/.strict-typing index 2915f398953..f42bd4a4ab1 100644 --- a/.strict-typing +++ b/.strict-typing @@ -188,6 +188,7 @@ homeassistant.components.recorder.pool homeassistant.components.recorder.purge homeassistant.components.recorder.repack homeassistant.components.recorder.run_history +homeassistant.components.recorder.services homeassistant.components.recorder.statistics homeassistant.components.recorder.system_health homeassistant.components.recorder.tasks diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index f9d462abeea..00f12710c18 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -7,7 +7,7 @@ from typing import Any import voluptuous as vol from homeassistant.const import CONF_EXCLUDE, EVENT_STATE_CHANGED -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, @@ -17,15 +17,11 @@ from homeassistant.helpers.entityfilter import ( from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, ) -from homeassistant.helpers.service import async_extract_entity_ids from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from . import history, statistics, websocket_api from .const import ( - ATTR_APPLY_FILTER, - ATTR_KEEP_DAYS, - ATTR_REPACK, CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, DOMAIN, @@ -33,39 +29,12 @@ from .const import ( SQLITE_URL_PREFIX, ) from .core import Recorder +from .services import async_register_services from .tasks import AddRecorderPlatformTask _LOGGER = logging.getLogger(__name__) -SERVICE_PURGE = "purge" -SERVICE_PURGE_ENTITIES = "purge_entities" -SERVICE_ENABLE = "enable" -SERVICE_DISABLE = "disable" - - -SERVICE_PURGE_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_KEEP_DAYS): cv.positive_int, - vol.Optional(ATTR_REPACK, default=False): cv.boolean, - vol.Optional(ATTR_APPLY_FILTER, default=False): cv.boolean, - } -) - -ATTR_DOMAINS = "domains" -ATTR_ENTITY_GLOBS = "entity_globs" - -SERVICE_PURGE_ENTITIES_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_DOMAINS, default=[]): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ENTITY_GLOBS, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - } -).extend(cv.ENTITY_SERVICE_FIELDS) -SERVICE_ENABLE_SCHEMA = vol.Schema({}) -SERVICE_DISABLE_SCHEMA = vol.Schema({}) - DEFAULT_URL = "sqlite:///{hass_config_path}" DEFAULT_DB_FILE = "home-assistant_v2.db" DEFAULT_DB_INTEGRITY_CHECK = True @@ -196,7 +165,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: instance.async_initialize() instance.async_register() instance.start() - _async_register_services(hass, instance) + async_register_services(hass, instance) history.async_setup(hass) statistics.async_setup(hass) websocket_api.async_setup(hass) @@ -211,51 +180,3 @@ async def _process_recorder_platform( """Process a recorder platform.""" instance: Recorder = hass.data[DATA_INSTANCE] instance.queue.put(AddRecorderPlatformTask(domain, platform)) - - -@callback -def _async_register_services(hass: HomeAssistant, instance: Recorder) -> None: - """Register recorder services.""" - - async def async_handle_purge_service(service: ServiceCall) -> None: - """Handle calls to the purge service.""" - instance.do_adhoc_purge(**service.data) - - hass.services.async_register( - DOMAIN, SERVICE_PURGE, async_handle_purge_service, schema=SERVICE_PURGE_SCHEMA - ) - - async def async_handle_purge_entities_service(service: ServiceCall) -> None: - """Handle calls to the purge entities service.""" - entity_ids = await async_extract_entity_ids(hass, service) - domains = service.data.get(ATTR_DOMAINS, []) - entity_globs = service.data.get(ATTR_ENTITY_GLOBS, []) - - instance.do_adhoc_purge_entities(entity_ids, domains, entity_globs) - - hass.services.async_register( - DOMAIN, - SERVICE_PURGE_ENTITIES, - async_handle_purge_entities_service, - schema=SERVICE_PURGE_ENTITIES_SCHEMA, - ) - - async def async_handle_enable_service(service: ServiceCall) -> None: - instance.set_enable(True) - - hass.services.async_register( - DOMAIN, - SERVICE_ENABLE, - async_handle_enable_service, - schema=SERVICE_ENABLE_SCHEMA, - ) - - async def async_handle_disable_service(service: ServiceCall) -> None: - instance.set_enable(False) - - hass.services.async_register( - DOMAIN, - SERVICE_DISABLE, - async_handle_disable_service, - schema=SERVICE_DISABLE_SCHEMA, - ) diff --git a/homeassistant/components/recorder/services.py b/homeassistant/components/recorder/services.py new file mode 100644 index 00000000000..3da2c63a27c --- /dev/null +++ b/homeassistant/components/recorder/services.py @@ -0,0 +1,106 @@ +"""Support for recorder services.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.core import HomeAssistant, ServiceCall, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.service import async_extract_entity_ids + +from .const import ATTR_APPLY_FILTER, ATTR_KEEP_DAYS, ATTR_REPACK, DOMAIN +from .core import Recorder + +SERVICE_PURGE = "purge" +SERVICE_PURGE_ENTITIES = "purge_entities" +SERVICE_ENABLE = "enable" +SERVICE_DISABLE = "disable" + + +SERVICE_PURGE_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_KEEP_DAYS): cv.positive_int, + vol.Optional(ATTR_REPACK, default=False): cv.boolean, + vol.Optional(ATTR_APPLY_FILTER, default=False): cv.boolean, + } +) + +ATTR_DOMAINS = "domains" +ATTR_ENTITY_GLOBS = "entity_globs" + +SERVICE_PURGE_ENTITIES_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_DOMAINS, default=[]): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_ENTITY_GLOBS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + } +).extend(cv.ENTITY_SERVICE_FIELDS) + +SERVICE_ENABLE_SCHEMA = vol.Schema({}) +SERVICE_DISABLE_SCHEMA = vol.Schema({}) + + +@callback +def _async_register_purge_service(hass: HomeAssistant, instance: Recorder) -> None: + async def async_handle_purge_service(service: ServiceCall) -> None: + """Handle calls to the purge service.""" + instance.do_adhoc_purge(**service.data) + + hass.services.async_register( + DOMAIN, SERVICE_PURGE, async_handle_purge_service, schema=SERVICE_PURGE_SCHEMA + ) + + +@callback +def _async_register_purge_entities_service( + hass: HomeAssistant, instance: Recorder +) -> None: + async def async_handle_purge_entities_service(service: ServiceCall) -> None: + """Handle calls to the purge entities service.""" + entity_ids = await async_extract_entity_ids(hass, service) + domains = service.data.get(ATTR_DOMAINS, []) + entity_globs = service.data.get(ATTR_ENTITY_GLOBS, []) + + instance.do_adhoc_purge_entities(entity_ids, domains, entity_globs) + + hass.services.async_register( + DOMAIN, + SERVICE_PURGE_ENTITIES, + async_handle_purge_entities_service, + schema=SERVICE_PURGE_ENTITIES_SCHEMA, + ) + + +@callback +def _async_register_enable_service(hass: HomeAssistant, instance: Recorder) -> None: + async def async_handle_enable_service(service: ServiceCall) -> None: + instance.set_enable(True) + + hass.services.async_register( + DOMAIN, + SERVICE_ENABLE, + async_handle_enable_service, + schema=SERVICE_ENABLE_SCHEMA, + ) + + +@callback +def _async_register_disable_service(hass: HomeAssistant, instance: Recorder) -> None: + async def async_handle_disable_service(service: ServiceCall) -> None: + instance.set_enable(False) + + hass.services.async_register( + DOMAIN, + SERVICE_DISABLE, + async_handle_disable_service, + schema=SERVICE_DISABLE_SCHEMA, + ) + + +@callback +def async_register_services(hass: HomeAssistant, instance: Recorder) -> None: + """Register recorder services.""" + _async_register_purge_service(hass, instance) + _async_register_purge_entities_service(hass, instance) + _async_register_enable_service(hass, instance) + _async_register_disable_service(hass, instance) diff --git a/mypy.ini b/mypy.ini index b55d5e74998..3d97a716955 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1831,6 +1831,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.recorder.services] +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.recorder.statistics] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index fc2c03e0039..1e8ff89f20d 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -20,10 +20,6 @@ from homeassistant.components.recorder import ( CONF_DB_URL, CONFIG_SCHEMA, DOMAIN, - SERVICE_DISABLE, - SERVICE_ENABLE, - SERVICE_PURGE, - SERVICE_PURGE_ENTITIES, SQLITE_URL_PREFIX, Recorder, get_instance, @@ -38,6 +34,12 @@ from homeassistant.components.recorder.models import ( StatisticsRuns, process_timestamp, ) +from homeassistant.components.recorder.services import ( + SERVICE_DISABLE, + SERVICE_ENABLE, + SERVICE_PURGE, + SERVICE_PURGE_ENTITIES, +) from homeassistant.components.recorder.util import session_scope from homeassistant.const import ( EVENT_HOMEASSISTANT_FINAL_WRITE, diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 8ac2f3a783c..7c3a2adcdc1 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -19,6 +19,10 @@ from homeassistant.components.recorder.models import ( StatisticsShortTerm, ) from homeassistant.components.recorder.purge import purge_old_data +from homeassistant.components.recorder.services import ( + SERVICE_PURGE, + SERVICE_PURGE_ENTITIES, +) from homeassistant.components.recorder.tasks import PurgeTask from homeassistant.components.recorder.util import session_scope from homeassistant.const import EVENT_STATE_CHANGED, STATE_ON @@ -133,9 +137,7 @@ async def test_purge_old_states_encouters_database_corruption( "homeassistant.components.recorder.purge.purge_old_data", side_effect=sqlite3_exception, ): - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, {"keep_days": 0} - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, {"keep_days": 0}) await hass.async_block_till_done() await async_wait_recording_done(hass) @@ -169,9 +171,7 @@ async def test_purge_old_states_encounters_temporary_mysql_error( ), patch.object( instance.engine.dialect, "name", "mysql" ): - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, {"keep_days": 0} - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, {"keep_days": 0}) await hass.async_block_till_done() await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -197,9 +197,7 @@ async def test_purge_old_states_encounters_operational_error( "homeassistant.components.recorder.purge._purge_old_recorder_runs", side_effect=exception, ): - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, {"keep_days": 0} - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, {"keep_days": 0}) await hass.async_block_till_done() await async_wait_recording_done(hass) await async_wait_recording_done(hass) @@ -452,9 +450,7 @@ async def test_purge_edge_case( events = session.query(Events).filter(Events.event_type == "EVENT_TEST_PURGE") assert events.count() == 1 - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await hass.async_block_till_done() await async_recorder_block_till_done(hass) @@ -722,9 +718,7 @@ async def test_purge_filtered_states( assert events_keep.count() == 1 # Normal purge doesn't remove excluded entities - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await hass.async_block_till_done() await async_recorder_block_till_done(hass) @@ -742,9 +736,7 @@ async def test_purge_filtered_states( # Test with 'apply_filter' = True service_data["apply_filter"] = True - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await hass.async_block_till_done() await async_recorder_block_till_done(hass) @@ -780,9 +772,7 @@ async def test_purge_filtered_states( assert session.query(StateAttributes).count() == 11 # Do it again to make sure nothing changes - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -794,9 +784,7 @@ async def test_purge_filtered_states( assert session.query(StateAttributes).count() == 11 service_data = {"keep_days": 0} - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -844,9 +832,7 @@ async def test_purge_filtered_states_to_empty( # Test with 'apply_filter' = True service_data["apply_filter"] = True - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -858,9 +844,7 @@ async def test_purge_filtered_states_to_empty( # Do it again to make sure nothing changes # Why do we do this? Should we check the end result? - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -914,9 +898,7 @@ async def test_purge_without_state_attributes_filtered_states_to_empty( # Test with 'apply_filter' = True service_data["apply_filter"] = True - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -928,9 +910,7 @@ async def test_purge_without_state_attributes_filtered_states_to_empty( # Do it again to make sure nothing changes # Why do we do this? Should we check the end result? - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -985,9 +965,7 @@ async def test_purge_filtered_events( assert states.count() == 10 # Normal purge doesn't remove excluded events - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await hass.async_block_till_done() await async_recorder_block_till_done(hass) @@ -1005,9 +983,7 @@ async def test_purge_filtered_events( # Test with 'apply_filter' = True service_data["apply_filter"] = True - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await hass.async_block_till_done() await async_recorder_block_till_done(hass) @@ -1105,9 +1081,7 @@ async def test_purge_filtered_events_state_changed( assert events_purge.count() == 60 assert states.count() == 63 - await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE, service_data - ) + await hass.services.async_call(recorder.DOMAIN, SERVICE_PURGE, service_data) await hass.async_block_till_done() await async_recorder_block_till_done(hass) @@ -1146,7 +1120,7 @@ async def test_purge_entities( } await hass.services.async_call( - recorder.DOMAIN, recorder.SERVICE_PURGE_ENTITIES, service_data + recorder.DOMAIN, SERVICE_PURGE_ENTITIES, service_data ) await hass.async_block_till_done() From 236d8aa277354870e59c4496fd85adb488ef40bf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 May 2022 13:22:49 -0500 Subject: [PATCH 0156/3516] Avoid recording static attributes for group entities (#71256) --- homeassistant/components/group/__init__.py | 3 ++ homeassistant/components/group/recorder.py | 16 ++++++ tests/components/group/test_recorder.py | 61 ++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 homeassistant/components/group/recorder.py create mode 100644 tests/components/group/test_recorder.py diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 9627ad86734..e6d7e91f035 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -33,6 +33,7 @@ from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.integration_platform import ( + async_process_integration_platform_for_component, async_process_integration_platforms, ) from homeassistant.helpers.reload import async_reload_integration_platforms @@ -265,6 +266,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if DOMAIN not in hass.data: hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) + await async_process_integration_platform_for_component(hass, DOMAIN) + component: EntityComponent = hass.data[DOMAIN] hass.data[REG_KEY] = GroupIntegrationRegistry() diff --git a/homeassistant/components/group/recorder.py b/homeassistant/components/group/recorder.py new file mode 100644 index 00000000000..9138b4ef348 --- /dev/null +++ b/homeassistant/components/group/recorder.py @@ -0,0 +1,16 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + +from . import ATTR_AUTO, ATTR_ENTITY_ID, ATTR_ORDER + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude static attributes from being recorded in the database.""" + return { + ATTR_ENTITY_ID, + ATTR_ORDER, + ATTR_AUTO, + } diff --git a/tests/components/group/test_recorder.py b/tests/components/group/test_recorder.py new file mode 100644 index 00000000000..fb68d9d3d43 --- /dev/null +++ b/tests/components/group/test_recorder.py @@ -0,0 +1,61 @@ +"""The tests for group recorder.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.components import group +from homeassistant.components.group import ATTR_AUTO, ATTR_ENTITY_ID, ATTR_ORDER +from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.util import session_scope +from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_ON +from homeassistant.core import State +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed +from tests.components.recorder.common import async_wait_recording_done + + +async def test_exclude_attributes(hass, recorder_mock): + """Test number registered attributes to be excluded.""" + hass.states.async_set("light.bowl", STATE_ON) + + assert await async_setup_component(hass, "light", {}) + assert await async_setup_component( + hass, + group.DOMAIN, + { + group.DOMAIN: { + "group_zero": {"entities": "light.Bowl", "icon": "mdi:work"}, + "group_one": {"entities": "light.Bowl", "icon": "mdi:work"}, + "group_two": {"entities": "light.Bowl", "icon": "mdi:work"}, + } + }, + ) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + await async_wait_recording_done(hass) + + def _fetch_states() -> list[State]: + with session_scope(hass=hass) as session: + native_states = [] + attr_ids = {} + for db_state_attributes in session.query(StateAttributes): + attr_ids[ + db_state_attributes.attributes_id + ] = db_state_attributes.to_native() + for db_state, db_state_attributes in session.query(States, StateAttributes): + state = db_state.to_native() + state.attributes = attr_ids[db_state.attributes_id] + native_states.append(state) + return native_states + + states: list[State] = await hass.async_add_executor_job(_fetch_states) + assert len(states) > 1 + for state in states: + if state.domain == group.DOMAIN: + assert ATTR_AUTO not in state.attributes + assert ATTR_ENTITY_ID not in state.attributes + assert ATTR_ORDER not in state.attributes + assert ATTR_FRIENDLY_NAME in state.attributes From 3aeda6b18ee79b127f29711a3e9cec84154647f4 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Tue, 3 May 2022 14:34:20 -0400 Subject: [PATCH 0157/3516] Load Insteon modem database on startup if needed (#71261) --- homeassistant/components/insteon/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 94c18df9a33..e2bebf0bb7f 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -49,7 +49,7 @@ async def async_get_device_config(hass, config_entry): with suppress(AttributeError): await devices[address].async_status() - load_aldb = devices.modem.aldb.read_write_mode == ReadWriteMode.UNKNOWN + load_aldb = 2 if devices.modem.aldb.read_write_mode == ReadWriteMode.UNKNOWN else 1 await devices.async_load(id_devices=1, load_modem_aldb=load_aldb) for addr in devices: device = devices[addr] From 92a30a69a351a66b5f9ea0ea631d86dc0614dde3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 May 2022 13:35:38 -0500 Subject: [PATCH 0158/3516] Fix oncue not logging back in when the session expires (#71258) --- homeassistant/components/oncue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oncue/manifest.json b/homeassistant/components/oncue/manifest.json index 9991d35ed18..e0533129d94 100644 --- a/homeassistant/components/oncue/manifest.json +++ b/homeassistant/components/oncue/manifest.json @@ -9,7 +9,7 @@ } ], "documentation": "https://www.home-assistant.io/integrations/oncue", - "requirements": ["aiooncue==0.3.2"], + "requirements": ["aiooncue==0.3.4"], "codeowners": ["@bdraco"], "iot_class": "cloud_polling", "loggers": ["aiooncue"] diff --git a/requirements_all.txt b/requirements_all.txt index 14154c718b1..e9c0844e2d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -211,7 +211,7 @@ aionotify==0.2.0 aionotion==3.0.2 # homeassistant.components.oncue -aiooncue==0.3.2 +aiooncue==0.3.4 # homeassistant.components.acmeda aiopulse==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9929fe508d4..400a60ec180 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -177,7 +177,7 @@ aionanoleaf==0.2.0 aionotion==3.0.2 # homeassistant.components.oncue -aiooncue==0.3.2 +aiooncue==0.3.4 # homeassistant.components.acmeda aiopulse==0.4.3 From c9d49ac9e4915343f53746e0403648ec7861ac9e Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 3 May 2022 13:51:07 -0500 Subject: [PATCH 0159/3516] Bump frontend to 20220503.0 (#71262) --- 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 18e2fc6ed8f..89dd75fa96c 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220502.0"], + "requirements": ["home-assistant-frontend==20220503.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f328394b9c1..fcb63a9426c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220502.0 +home-assistant-frontend==20220503.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.1 diff --git a/requirements_all.txt b/requirements_all.txt index e9c0844e2d3..e212a63b559 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220502.0 +home-assistant-frontend==20220503.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 400a60ec180..c6e1065a52a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -580,7 +580,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220502.0 +home-assistant-frontend==20220503.0 # homeassistant.components.home_connect homeconnect==0.7.0 From 5934167e15c20efec617a40ccaaa83e91ea54f0b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 May 2022 12:16:57 -0700 Subject: [PATCH 0160/3516] Bump aioslimproto to 2.0.0 (#71265) --- homeassistant/components/slimproto/manifest.json | 2 +- homeassistant/components/slimproto/media_player.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/slimproto/manifest.json b/homeassistant/components/slimproto/manifest.json index 557428919a4..eb4ab00f18d 100644 --- a/homeassistant/components/slimproto/manifest.json +++ b/homeassistant/components/slimproto/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "iot_class": "local_push", "documentation": "https://www.home-assistant.io/integrations/slimproto", - "requirements": ["aioslimproto==1.0.2"], + "requirements": ["aioslimproto==2.0.0"], "codeowners": ["@marcelveldt"], "after_dependencies": ["media_source"] } diff --git a/homeassistant/components/slimproto/media_player.py b/homeassistant/components/slimproto/media_player.py index d41d10432db..6b1989830e2 100644 --- a/homeassistant/components/slimproto/media_player.py +++ b/homeassistant/components/slimproto/media_player.py @@ -118,7 +118,7 @@ class SlimProtoPlayer(MediaPlayerEntity): EventType.PLAYER_CONNECTED, EventType.PLAYER_DISCONNECTED, EventType.PLAYER_NAME_RECEIVED, - EventType.PLAYER_RPC_EVENT, + EventType.PLAYER_CLI_EVENT, ), player_filter=self.player.player_id, ) @@ -205,7 +205,7 @@ class SlimProtoPlayer(MediaPlayerEntity): if event.type == EventType.PLAYER_CONNECTED: # player reconnected, update our player object self.player = self.slimserver.get_player(event.player_id) - if event.type == EventType.PLAYER_RPC_EVENT: + if event.type == EventType.PLAYER_CLI_EVENT: # rpc event from player such as a button press, # forward on the eventbus for others to handle dev_id = self.registry_entry.device_id if self.registry_entry else None diff --git a/requirements_all.txt b/requirements_all.txt index e212a63b559..438c951d214 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -244,7 +244,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.slimproto -aioslimproto==1.0.2 +aioslimproto==2.0.0 # homeassistant.components.steamist aiosteamist==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6e1065a52a..b012f795c9e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -210,7 +210,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.slimproto -aioslimproto==1.0.2 +aioslimproto==2.0.0 # homeassistant.components.steamist aiosteamist==0.3.1 From 3717ec8811e41288cd50353e317a1bc411eedb41 Mon Sep 17 00:00:00 2001 From: James Szalay Date: Tue, 3 May 2022 15:17:27 -0400 Subject: [PATCH 0161/3516] Updated vesync component fans list to handle alt ids for models. (#71259) * Updated vesync component fans list to handle alt ids for models. * Lint Co-authored-by: Paulus Schoutsen --- homeassistant/components/vesync/fan.py | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 41ed6109be2..e37a6c8893e 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -20,10 +20,20 @@ _LOGGER = logging.getLogger(__name__) DEV_TYPE_TO_HA = { "LV-PUR131S": "fan", + "LV-RH131S": "fan", # Alt ID Model LV-PUR131S "Core200S": "fan", + "LAP-C201S-AUSR": "fan", # Alt ID Model Core200S + "LAP-C202S-WUSR": "fan", # Alt ID Model Core200S "Core300S": "fan", + "LAP-C301S-WJP": "fan", # Alt ID Model Core300S "Core400S": "fan", + "LAP-C401S-WJP": "fan", # Alt ID Model Core400S + "LAP-C401S-WUSR": "fan", # Alt ID Model Core400S + "LAP-C401S-WAAA": "fan", # Alt ID Model Core400S "Core600S": "fan", + "LAP-C601S-WUS": "fan", # Alt ID Model Core600S + "LAP-C601S-WUSR": "fan", # Alt ID Model Core600S + "LAP-C601S-WEU": "fan", # Alt ID Model Core600S } FAN_MODE_AUTO = "auto" @@ -31,17 +41,37 @@ FAN_MODE_SLEEP = "sleep" PRESET_MODES = { "LV-PUR131S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], + "LV-RH131S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model LV-PUR131S "Core200S": [FAN_MODE_SLEEP], + "LAP-C201S-AUSR": [FAN_MODE_SLEEP], # Alt ID Model Core200S + "LAP-C202S-WUSR": [FAN_MODE_SLEEP], # Alt ID Model Core200S "Core300S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], + "LAP-C301S-WJP": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core300S "Core400S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], + "LAP-C401S-WJP": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core400S + "LAP-C401S-WUSR": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core400S + "LAP-C401S-WAAA": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core400S "Core600S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], + "LAP-C601S-WUS": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core600S + "LAP-C601S-WUSR": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core600S + "LAP-C601S-WEU": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core600S } SPEED_RANGE = { # off is not included "LV-PUR131S": (1, 3), + "LV-RH131S": (1, 3), # ALt ID Model LV-PUR131S "Core200S": (1, 3), + "LAP-C201S-AUSR": (1, 3), # ALt ID Model Core200S + "LAP-C202S-WUSR": (1, 3), # ALt ID Model Core200S "Core300S": (1, 3), + "LAP-C301S-WJP": (1, 3), # ALt ID Model Core300S "Core400S": (1, 4), + "LAP-C401S-WJP": (1, 4), # ALt ID Model Core400S + "LAP-C401S-WUSR": (1, 4), # ALt ID Model Core400S + "LAP-C401S-WAAA": (1, 4), # ALt ID Model Core400S "Core600S": (1, 4), + "LAP-C601S-WUS": (1, 4), # ALt ID Model Core600S + "LAP-C601S-WUSR": (1, 4), # ALt ID Model Core600S + "LAP-C601S-WEU": (1, 4), # ALt ID Model Core600S } From e9abfad3611a8f08b211eb39141e26aefb485609 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 3 May 2022 21:19:43 +0200 Subject: [PATCH 0162/3516] Reject MQTT topics which include control- or non-characters (#71263) --- homeassistant/components/mqtt/util.py | 9 +++++++++ tests/components/mqtt/test_init.py | 28 ++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index b8fca50a153..66eec1bdfe8 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -31,6 +31,15 @@ def valid_topic(value: Any) -> str: ) if "\0" in value: raise vol.Invalid("MQTT topic name/filter must not contain null character.") + if any(char <= "\u001F" for char in value): + raise vol.Invalid("MQTT topic name/filter must not contain control characters.") + if any("\u007f" <= char <= "\u009F" for char in value): + raise vol.Invalid("MQTT topic name/filter must not contain control characters.") + if any("\ufdd0" <= char <= "\ufdef" for char in value): + raise vol.Invalid("MQTT topic name/filter must not contain non-characters.") + if any((ord(char) & 0xFFFF) in (0xFFFE, 0xFFFF) for char in value): + raise vol.Invalid("MQTT topic name/filter must not contain noncharacters.") + return value diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 03874ed331e..763cbfc1b71 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -522,11 +522,29 @@ def test_validate_topic(): # Topics "SHOULD NOT" include these special characters # (not MUST NOT, RFC2119). The receiver MAY close the connection. - mqtt.util.valid_topic("\u0001") - mqtt.util.valid_topic("\u001F") - mqtt.util.valid_topic("\u009F") - mqtt.util.valid_topic("\u009F") - mqtt.util.valid_topic("\uffff") + # We enforce this because mosquitto does: https://github.com/eclipse/mosquitto/commit/94fdc9cb44c829ff79c74e1daa6f7d04283dfffd + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\u0001") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\u001F") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\u007F") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\u009F") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\ufdd0") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\ufdef") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\ufffe") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\ufffe") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\uffff") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\U0001fffe") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\U0001ffff") def test_validate_subscribe_topic(): From 19bff3543797684c9974717e05fe8c43add15c39 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 3 May 2022 16:43:44 +0200 Subject: [PATCH 0163/3516] Ensure 'this' variable is always defined for template entities (#70911) --- .../components/template/template_entity.py | 30 ++++- homeassistant/helpers/template.py | 3 +- tests/components/template/test_sensor.py | 114 ++++++++++++++++++ 3 files changed, 140 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index d7d6ab46c62..a6d1cba78e1 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -17,8 +17,9 @@ from homeassistant.const import ( CONF_ICON_TEMPLATE, CONF_NAME, EVENT_HOMEASSISTANT_START, + STATE_UNKNOWN, ) -from homeassistant.core import CoreState, Event, callback +from homeassistant.core import CoreState, Event, State, callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -251,13 +252,28 @@ class TemplateEntity(Entity): self._entity_picture_template = config.get(CONF_PICTURE) self._friendly_name_template = config.get(CONF_NAME) + class DummyState(State): + """None-state for template entities not yet added to the state machine.""" + + def __init__(self) -> None: + """Initialize a new state.""" + super().__init__("unknown.unknown", STATE_UNKNOWN) + self.entity_id = None # type: ignore[assignment] + + @property + def name(self) -> str: + """Name of this state.""" + return "" + + variables = {"this": DummyState()} + # Try to render the name as it can influence the entity ID self._attr_name = fallback_name if self._friendly_name_template: self._friendly_name_template.hass = hass with contextlib.suppress(TemplateError): self._attr_name = self._friendly_name_template.async_render( - parse_result=False + variables=variables, parse_result=False ) # Templates will not render while the entity is unavailable, try to render the @@ -266,13 +282,15 @@ class TemplateEntity(Entity): self._entity_picture_template.hass = hass with contextlib.suppress(TemplateError): self._attr_entity_picture = self._entity_picture_template.async_render( - parse_result=False + variables=variables, parse_result=False ) if self._icon_template: self._icon_template.hass = hass with contextlib.suppress(TemplateError): - self._attr_icon = self._icon_template.async_render(parse_result=False) + self._attr_icon = self._icon_template.async_render( + variables=variables, parse_result=False + ) @callback def _update_available(self, result): @@ -373,10 +391,10 @@ class TemplateEntity(Entity): template_var_tups: list[TrackTemplate] = [] has_availability_template = False - values = {"this": TemplateStateFromEntityId(self.hass, self.entity_id)} + variables = {"this": TemplateStateFromEntityId(self.hass, self.entity_id)} for template, attributes in self._template_attrs.items(): - template_var_tup = TrackTemplate(template, values) + template_var_tup = TrackTemplate(template, variables) is_availability_template = False for attribute in attributes: # pylint: disable-next=protected-access diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index dbc82ce6902..3f084674a1b 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -850,7 +850,8 @@ class TemplateStateFromEntityId(TemplateStateBase): @property def _state(self) -> State: # type: ignore[override] # mypy issue 4125 state = self._hass.states.get(self._entity_id) - assert state + if not state: + state = State(self._entity_id, STATE_UNKNOWN) return state def __repr__(self) -> str: diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 297008e77bb..ddf13c2015b 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -653,6 +653,120 @@ async def test_this_variable(hass, start_ha): assert hass.states.get(TEST_NAME).state == "It Works: " + TEST_NAME +@pytest.mark.parametrize("count,domain", [(1, "template")]) +@pytest.mark.parametrize( + "config", + [ + { + "template": { + "sensor": { + "state": "{{ this.attributes.get('test', 'no-test!') }}: {{ this.entity_id }}", + "icon": "mdi:{% if this.entity_id in states and 'friendly_name' in this.attributes %} {{this.attributes['friendly_name']}} {% else %}{{this.entity_id}}:{{this.entity_id in states}}{% endif %}", + "name": "{% if this.entity_id in states and 'friendly_name' in this.attributes %} {{this.attributes['friendly_name']}} {% else %}{{this.entity_id}}:{{this.entity_id in states}}{% endif %}", + "picture": "{% if this.entity_id in states and 'entity_picture' in this.attributes %} {{this.attributes['entity_picture']}} {% else %}{{this.entity_id}}:{{this.entity_id in states}}{% endif %}", + "attributes": {"test": "{{ this.entity_id }}"}, + }, + }, + }, + ], +) +async def test_this_variable_early_hass_not_running(hass, config, count, domain): + """Test referencing 'this' variable before the entity is in the state machine. + + Hass is not yet started when the entity is added. + Icon, name and picture templates are rendered once in the constructor. + """ + entity_id = "sensor.none_false" + + hass.state = CoreState.not_running + + # Setup template + with assert_setup_component(count, domain): + assert await async_setup_component( + hass, + domain, + config, + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + + # Sensor state not rendered, icon, name and picture + # templates rendered in constructor with entity_id set to None + state = hass.states.get(entity_id) + assert state.state == "unknown" + assert state.attributes == { + "entity_picture": "None:False", + "friendly_name": "None:False", + "icon": "mdi:None:False", + } + + # Signal hass started + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + # Re-render icon, name, pciture + other templates now rendered + state = hass.states.get(entity_id) + assert state.state == "sensor.none_false: sensor.none_false" + assert state.attributes == { + "entity_picture": "sensor.none_false:False", + "friendly_name": "sensor.none_false:False", + "icon": "mdi:sensor.none_false:False", + "test": "sensor.none_false", + } + + +@pytest.mark.parametrize("count,domain", [(1, "template")]) +@pytest.mark.parametrize( + "config", + [ + { + "template": { + "sensor": { + "state": "{{ this.attributes.get('test', 'no-test!') }}: {{ this.entity_id }}", + "icon": "mdi:{% if this.entity_id in states and 'friendly_name' in this.attributes %} {{this.attributes['friendly_name']}} {% else %}{{this.entity_id}}:{{this.entity_id in states}}{% endif %}", + "name": "{% if this.entity_id in states and 'friendly_name' in this.attributes %} {{this.attributes['friendly_name']}} {% else %}{{this.entity_id}}:{{this.entity_id in states}}{% endif %}", + "picture": "{% if this.entity_id in states and 'entity_picture' in this.attributes %} {{this.attributes['entity_picture']}} {% else %}{{this.entity_id}}:{{this.entity_id in states}}{% endif %}", + "attributes": {"test": "{{ this.entity_id }}"}, + }, + }, + }, + ], +) +async def test_this_variable_early_hass_running(hass, config, count, domain): + """Test referencing 'this' variable before the entity is in the state machine. + + Hass is already started when the entity is added. + Icon, name and picture templates are rendered in the constructor, and again + before the entity is added to hass. + """ + + # Start hass + assert hass.state == CoreState.running + await hass.async_start() + await hass.async_block_till_done() + + # Setup template + with assert_setup_component(count, domain): + assert await async_setup_component( + hass, + domain, + config, + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + + entity_id = "sensor.none_false" + # All templated rendered + state = hass.states.get(entity_id) + assert state.state == "sensor.none_false: sensor.none_false" + assert state.attributes == { + "entity_picture": "sensor.none_false:False", + "friendly_name": "sensor.none_false:False", + "icon": "mdi:sensor.none_false:False", + "test": "sensor.none_false", + } + + @pytest.mark.parametrize("count,domain", [(1, sensor.DOMAIN)]) @pytest.mark.parametrize( "config", From 1a2a061c193eaf09c8763fc459c4166bfb94a450 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 May 2022 07:42:06 -0700 Subject: [PATCH 0164/3516] Fix homepod streaming and browsing apps (#71230) --- .../components/apple_tv/media_player.py | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 02919043bb2..6c1b35f70f8 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -79,7 +79,8 @@ SUPPORT_APPLE_TV = ( SUPPORT_FEATURE_MAPPING = { FeatureName.PlayUrl: MediaPlayerEntityFeature.BROWSE_MEDIA | MediaPlayerEntityFeature.PLAY_MEDIA, - FeatureName.StreamFile: MediaPlayerEntityFeature.PLAY_MEDIA, + FeatureName.StreamFile: MediaPlayerEntityFeature.BROWSE_MEDIA + | MediaPlayerEntityFeature.PLAY_MEDIA, FeatureName.Pause: MediaPlayerEntityFeature.PAUSE, FeatureName.Play: MediaPlayerEntityFeature.PLAY, FeatureName.SetPosition: MediaPlayerEntityFeature.SEEK, @@ -282,23 +283,20 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): if media_type == MEDIA_TYPE_APP: await self.atv.apps.launch_app(media_id) - is_media_source_id = media_source.is_media_source_id(media_id) + if media_source.is_media_source_id(media_id): + play_item = await media_source.async_resolve_media(self.hass, media_id) + media_id = play_item.url + media_type = MEDIA_TYPE_MUSIC - if ( - not is_media_source_id - and self._is_feature_available(FeatureName.StreamFile) - and (await is_streamable(media_id) or media_type == MEDIA_TYPE_MUSIC) + media_id = async_process_play_media_url(self.hass, media_id) + + if self._is_feature_available(FeatureName.StreamFile) and ( + media_type == MEDIA_TYPE_MUSIC or await is_streamable(media_id) ): _LOGGER.debug("Streaming %s via RAOP", media_id) await self.atv.stream.stream_file(media_id) if self._is_feature_available(FeatureName.PlayUrl): - if is_media_source_id: - play_item = await media_source.async_resolve_media(self.hass, media_id) - media_id = play_item.url - - media_id = async_process_play_media_url(self.hass, media_id) - _LOGGER.debug("Playing %s via AirPlay", media_id) await self.atv.stream.play_url(media_id) else: @@ -397,9 +395,12 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): media_content_id=None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" - # If we can't stream URLs, we can't browse media. - # In that case the `BROWSE_MEDIA` feature was added because of AppList/LaunchApp - if not self._is_feature_available(FeatureName.PlayUrl): + if media_content_id == "apps" or ( + # If we can't stream files or URLs, we can't browse media. + # In that case the `BROWSE_MEDIA` feature was added because of AppList/LaunchApp + not self._is_feature_available(FeatureName.PlayUrl) + and not self._is_feature_available(FeatureName.StreamFile) + ): return build_app_list(self._app_list) if self._app_list: From 1f4e9effd8610ccfd5c4030bc51d6ee7d51a92c2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 May 2022 00:34:04 -0700 Subject: [PATCH 0165/3516] Bump aioslimproto to 1.0.2 (#71231) --- homeassistant/components/slimproto/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/slimproto/manifest.json b/homeassistant/components/slimproto/manifest.json index b8e00eb3f99..557428919a4 100644 --- a/homeassistant/components/slimproto/manifest.json +++ b/homeassistant/components/slimproto/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "iot_class": "local_push", "documentation": "https://www.home-assistant.io/integrations/slimproto", - "requirements": ["aioslimproto==1.0.0"], + "requirements": ["aioslimproto==1.0.2"], "codeowners": ["@marcelveldt"], "after_dependencies": ["media_source"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2a776b4d883..2cbc7fa8134 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -244,7 +244,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.slimproto -aioslimproto==1.0.0 +aioslimproto==1.0.2 # homeassistant.components.steamist aiosteamist==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ce220105eb..e824051d07a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -210,7 +210,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.slimproto -aioslimproto==1.0.0 +aioslimproto==1.0.2 # homeassistant.components.steamist aiosteamist==0.3.1 From 8252ba82d113e42a7483e8a69e48565c778a0c8e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 3 May 2022 10:36:58 +0200 Subject: [PATCH 0166/3516] Isolate parallel subscripts (#71233) --- homeassistant/helpers/script.py | 18 ++-- tests/helpers/test_script.py | 146 +++++++++++++++++++++++++++++++- 2 files changed, 157 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index d988b0edd81..3d43e03ddce 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1133,13 +1133,14 @@ class Script: domain: str, *, # Used in "Running " log message - running_description: str | None = None, change_listener: Callable[..., Any] | None = None, - script_mode: str = DEFAULT_SCRIPT_MODE, - max_runs: int = DEFAULT_MAX, - max_exceeded: str = DEFAULT_MAX_EXCEEDED, - logger: logging.Logger | None = None, + copy_variables: bool = False, log_exceptions: bool = True, + logger: logging.Logger | None = None, + max_exceeded: str = DEFAULT_MAX_EXCEEDED, + max_runs: int = DEFAULT_MAX, + running_description: str | None = None, + script_mode: str = DEFAULT_SCRIPT_MODE, top_level: bool = True, variables: ScriptVariables | None = None, ) -> None: @@ -1192,6 +1193,7 @@ class Script: self._variables_dynamic = template.is_complex(variables) if self._variables_dynamic: template.attach(hass, variables) + self._copy_variables_on_run = copy_variables @property def change_listener(self) -> Callable[..., Any] | None: @@ -1454,7 +1456,10 @@ class Script: variables["context"] = context else: - variables = cast(dict, run_variables) + if self._copy_variables_on_run: + variables = cast(dict, copy(run_variables)) + else: + variables = cast(dict, run_variables) # Prevent non-allowed recursive calls which will cause deadlocks when we try to # stop (restart) or wait for (queued) our own script run. @@ -1671,6 +1676,7 @@ class Script: max_runs=self.max_runs, logger=self._logger, top_level=False, + copy_variables=True, ) parallel_script.change_listener = partial( self._chain_change_listener, parallel_script diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index b9c968838c9..4791dd84cc4 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -3027,7 +3027,7 @@ async def test_parallel(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) - ], "0/parallel/1/sequence/0": [ { - "variables": {"wait": {"remaining": None}}, + "variables": {}, "result": { "event": "test_event", "event_data": {"hello": "from action 2", "what": "world"}, @@ -3047,6 +3047,150 @@ async def test_parallel(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) - assert_action_trace(expected_trace) +async def test_parallel_loop( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test parallel loops do not affect each other.""" + events_loop1 = async_capture_events(hass, "loop1") + events_loop2 = async_capture_events(hass, "loop2") + hass.states.async_set("switch.trigger", "off") + + sequence = cv.SCRIPT_SCHEMA( + { + "parallel": [ + { + "alias": "Loop1", + "sequence": [ + { + "repeat": { + "for_each": ["loop1_a", "loop1_b", "loop1_c"], + "sequence": [ + { + "event": "loop1", + "event_data": {"hello1": "{{ repeat.item }}"}, + } + ], + }, + }, + ], + }, + { + "alias": "Loop2", + "sequence": [ + { + "repeat": { + "for_each": ["loop2_a", "loop2_b", "loop2_c"], + "sequence": [ + { + "event": "loop2", + "event_data": {"hello2": "{{ repeat.item }}"}, + } + ], + }, + }, + ], + }, + ] + } + ) + + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + hass.async_create_task( + script_obj.async_run(MappingProxyType({"what": "world"}), Context()) + ) + await hass.async_block_till_done() + + assert len(events_loop1) == 3 + assert events_loop1[0].data["hello1"] == "loop1_a" + assert events_loop1[1].data["hello1"] == "loop1_b" + assert events_loop1[2].data["hello1"] == "loop1_c" + assert events_loop2[0].data["hello2"] == "loop2_a" + assert events_loop2[1].data["hello2"] == "loop2_b" + assert events_loop2[2].data["hello2"] == "loop2_c" + + expected_trace = { + "0": [{"result": {}}], + "0/parallel/0/sequence/0": [{"result": {}}], + "0/parallel/1/sequence/0": [ + { + "result": {}, + } + ], + "0/parallel/0/sequence/0/repeat/sequence/0": [ + { + "variables": { + "repeat": { + "first": True, + "index": 1, + "last": False, + "item": "loop1_a", + } + }, + "result": {"event": "loop1", "event_data": {"hello1": "loop1_a"}}, + }, + { + "variables": { + "repeat": { + "first": False, + "index": 2, + "last": False, + "item": "loop1_b", + } + }, + "result": {"event": "loop1", "event_data": {"hello1": "loop1_b"}}, + }, + { + "variables": { + "repeat": { + "first": False, + "index": 3, + "last": True, + "item": "loop1_c", + } + }, + "result": {"event": "loop1", "event_data": {"hello1": "loop1_c"}}, + }, + ], + "0/parallel/1/sequence/0/repeat/sequence/0": [ + { + "variables": { + "repeat": { + "first": True, + "index": 1, + "last": False, + "item": "loop2_a", + } + }, + "result": {"event": "loop2", "event_data": {"hello2": "loop2_a"}}, + }, + { + "variables": { + "repeat": { + "first": False, + "index": 2, + "last": False, + "item": "loop2_b", + } + }, + "result": {"event": "loop2", "event_data": {"hello2": "loop2_b"}}, + }, + { + "variables": { + "repeat": { + "first": False, + "index": 3, + "last": True, + "item": "loop2_c", + } + }, + "result": {"event": "loop2", "event_data": {"hello2": "loop2_c"}}, + }, + ], + } + assert_action_trace(expected_trace) + + async def test_parallel_error( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: From 6301873d8928d43bdfd66412757b22fa6fd91b9a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 3 May 2022 11:28:08 +0200 Subject: [PATCH 0167/3516] Fix script conditions (#71235) --- homeassistant/helpers/script.py | 32 +++++++++++++++++++++++--------- tests/helpers/test_script.py | 5 +---- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 3d43e03ddce..0b755ae0032 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -205,6 +205,10 @@ async def trace_action(hass, script_run, stop, variables): except _AbortScript as ex: trace_element.set_error(ex.__cause__ or ex) raise ex + except _ConditionFail as ex: + # Clear errors which may have been set when evaluating the condition + trace_element.set_error(None) + raise ex except _StopScript as ex: raise ex except Exception as ex: @@ -325,11 +329,19 @@ async def async_validate_action_config( return config -class _AbortScript(Exception): +class _HaltScript(Exception): + """Throw if script needs to stop executing.""" + + +class _AbortScript(_HaltScript): """Throw if script needs to abort because of an unexpected error.""" -class _StopScript(Exception): +class _ConditionFail(_HaltScript): + """Throw if script needs to stop because a condition evaluated to False.""" + + +class _StopScript(_HaltScript): """Throw if script needs to stop.""" @@ -393,16 +405,18 @@ class _ScriptRun: await self._async_step(log_exceptions=False) else: script_execution_set("finished") - except _StopScript: - script_execution_set("finished") - # Let the _StopScript bubble up if this is a sub-script - if not self._script.top_level: - raise except _AbortScript: script_execution_set("aborted") # Let the _AbortScript bubble up if this is a sub-script if not self._script.top_level: raise + except _ConditionFail: + script_execution_set("aborted") + except _StopScript: + script_execution_set("finished") + # Let the _StopScript bubble up if this is a sub-script + if not self._script.top_level: + raise except Exception: script_execution_set("error") raise @@ -450,7 +464,7 @@ class _ScriptRun: def _handle_exception( self, exception: Exception, continue_on_error: bool, log_exceptions: bool ) -> None: - if not isinstance(exception, (_AbortScript, _StopScript)) and log_exceptions: + if not isinstance(exception, _HaltScript) and log_exceptions: self._log_exception(exception) if not continue_on_error: @@ -726,7 +740,7 @@ class _ScriptRun: self._log("Test condition %s: %s", self._script.last_action, check) trace_update_result(result=check) if not check: - raise _AbortScript + raise _ConditionFail def _test_conditions(self, conditions, name, condition_path=None): if condition_path is None: diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 4791dd84cc4..c01f086a12a 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1445,7 +1445,7 @@ async def test_condition_warning(hass, caplog): assert_action_trace( { "0": [{"result": {"event": "test_event", "event_data": {}}}], - "1": [{"error_type": script._AbortScript, "result": {"result": False}}], + "1": [{"result": {"result": False}}], "1/entity_id/0": [{"error_type": ConditionError}], }, expected_script_execution="aborted", @@ -1499,7 +1499,6 @@ async def test_condition_basic(hass, caplog): "0": [{"result": {"event": "test_event", "event_data": {}}}], "1": [ { - "error_type": script._AbortScript, "result": {"entities": ["test.entity"], "result": False}, } ], @@ -1590,7 +1589,6 @@ async def test_shorthand_template_condition(hass, caplog): "0": [{"result": {"event": "test_event", "event_data": {}}}], "1": [ { - "error_type": script._AbortScript, "result": {"entities": ["test.entity"], "result": False}, } ], @@ -1656,7 +1654,6 @@ async def test_condition_validation(hass, caplog): "0": [{"result": {"event": "test_event", "event_data": {}}}], "1": [ { - "error_type": script._AbortScript, "result": {"result": False}, } ], From d69a08bdf9b45a8c202b6496f1882379d0c81ae7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 May 2022 13:04:59 +0200 Subject: [PATCH 0168/3516] Indicate disabled steps in script trace (#71237) --- homeassistant/helpers/script.py | 1 + tests/helpers/test_script.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 0b755ae0032..54ae4f456ab 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -439,6 +439,7 @@ class _ScriptRun: self._log( "Skipped disabled step %s", self._action.get(CONF_ALIAS, action) ) + trace_set_result(enabled=False) return try: diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index c01f086a12a..136729e62fc 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -4939,8 +4939,8 @@ async def test_disabled_actions( assert_action_trace( { "0": [{"result": {"event": "test_event", "event_data": {}}}], - "1": [{}], - "2": [{}], + "1": [{"result": {"enabled": False}}], + "2": [{"result": {"enabled": False}}], "3": [{"result": {"event": "test_event", "event_data": {}}}], }, ) From f4f5ba93b5a1bf7887b8fa5e2a46859de6399a41 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 3 May 2022 13:06:13 +0200 Subject: [PATCH 0169/3516] Add test for failing conditions in sub scripts (#71238) --- tests/helpers/test_script.py | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 136729e62fc..519498f2e08 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1507,6 +1507,55 @@ async def test_condition_basic(hass, caplog): ) +async def test_condition_subscript(hass, caplog): + """Test failing conditions in a subscript don't stop the parent script.""" + event = "test_event" + events = async_capture_events(hass, event) + sequence = cv.SCRIPT_SCHEMA( + [ + {"event": event}, + { + "repeat": { + "until": "{{ 1 == 1 }}", + "sequence": [ + {"condition": "{{ 1 == 2 }}"}, + ], + } + }, + {"event": event}, + ] + ) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + hass.states.async_set("test.entity", "hello") + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + caplog.clear() + assert len(events) == 2 + + assert_action_trace( + { + "0": [{"result": {"event": "test_event", "event_data": {}}}], + "1": [{"result": {}}], + "1/repeat/sequence/0": [ + { + "variables": {"repeat": {"first": True, "index": 1}}, + "result": {"entities": [], "result": False}, + } + ], + "1/repeat": [ + { + "variables": {"repeat": {"first": True, "index": 1}}, + "result": {"result": True}, + } + ], + "1/repeat/until/0": [{"result": {"entities": [], "result": True}}], + "2": [{"result": {"event": "test_event", "event_data": {}}}], + } + ) + + async def test_and_default_condition(hass, caplog): """Test that a list of conditions evaluates as AND.""" alias = "condition step" From 989fa4274bd9256bf66cd70f5e218688c93691bb Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 3 May 2022 19:38:20 +0200 Subject: [PATCH 0170/3516] Prevent Netgear SSDP from updating host (#71240) --- homeassistant/components/netgear/config_flow.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/netgear/config_flow.py b/homeassistant/components/netgear/config_flow.py index 85c206ce463..79053c712fc 100644 --- a/homeassistant/components/netgear/config_flow.py +++ b/homeassistant/components/netgear/config_flow.py @@ -19,6 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.util.network import is_ipv4_address from .const import ( CONF_CONSIDER_HOME, @@ -129,6 +130,9 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): hostname = cast(str, hostname) updated_data[CONF_HOST] = hostname + if not is_ipv4_address(str(hostname)): + return self.async_abort(reason="not_ipv4_address") + _LOGGER.debug("Netgear ssdp discovery info: %s", discovery_info) await self.async_set_unique_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]) From c9eca4033669133ed8e01135f2b963be062f3acb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 May 2022 11:47:13 -0500 Subject: [PATCH 0171/3516] Allow hidden entities to be selected in homekit include mode (#71250) --- .../components/homekit/config_flow.py | 10 ++++--- tests/components/homekit/test_config_flow.py | 28 +++++++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index b7a00bc3ade..5f142fdb0fe 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -467,7 +467,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): entity_filter = self.hk_options.get(CONF_FILTER, {}) entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) all_supported_entities = _async_get_matching_entities( - self.hass, domains, include_entity_category=True + self.hass, domains, include_entity_category=True, include_hidden=True ) # In accessory mode we can only have one default_value = next( @@ -508,7 +508,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): entities = entity_filter.get(CONF_INCLUDE_ENTITIES, []) all_supported_entities = _async_get_matching_entities( - self.hass, domains, include_entity_category=True + self.hass, domains, include_entity_category=True, include_hidden=True ) if not entities: entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, []) @@ -646,12 +646,13 @@ def _exclude_by_entity_registry( ent_reg: entity_registry.EntityRegistry, entity_id: str, include_entity_category: bool, + include_hidden: bool, ) -> bool: """Filter out hidden entities and ones with entity category (unless specified).""" return bool( (entry := ent_reg.async_get(entity_id)) and ( - entry.hidden_by is not None + (not include_hidden and entry.hidden_by is not None) or (not include_entity_category and entry.entity_category is not None) ) ) @@ -661,6 +662,7 @@ def _async_get_matching_entities( hass: HomeAssistant, domains: list[str] | None = None, include_entity_category: bool = False, + include_hidden: bool = False, ) -> dict[str, str]: """Fetch all entities or entities in the given domains.""" ent_reg = entity_registry.async_get(hass) @@ -671,7 +673,7 @@ def _async_get_matching_entities( key=lambda item: item.entity_id, ) if not _exclude_by_entity_registry( - ent_reg, state.entity_id, include_entity_category + ent_reg, state.entity_id, include_entity_category, include_hidden ) } diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 42ce6779528..ce0bed0ff52 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -1504,7 +1504,7 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( @patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) -async def test_options_flow_include_mode_skips_hidden_entities( +async def test_options_flow_include_mode_allows_hidden_entities( port_mock, hass, mock_get_source_ip, hk_driver, mock_async_zeroconf, entity_reg ): """Ensure include mode does not offer hidden entities.""" @@ -1558,24 +1558,28 @@ async def test_options_flow_include_mode_skips_hidden_entities( assert _get_schema_default(result2["data_schema"].schema, "entities") == [] # sonos_hidden_switch.entity_id is a hidden entity - # so it should not be selectable since it will always be excluded - with pytest.raises(voluptuous.error.MultipleInvalid): - await hass.config_entries.options.async_configure( - result2["flow_id"], - user_input={"entities": [sonos_hidden_switch.entity_id]}, - ) - - result4 = await hass.config_entries.options.async_configure( + # we allow it to be selected in include mode only + result3 = await hass.config_entries.options.async_configure( result2["flow_id"], - user_input={"entities": ["media_player.tv", "switch.other"]}, + user_input={ + "entities": [ + sonos_hidden_switch.entity_id, + "media_player.tv", + "switch.other", + ] + }, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { "exclude_domains": [], "exclude_entities": [], "include_domains": [], - "include_entities": ["media_player.tv", "switch.other"], + "include_entities": [ + sonos_hidden_switch.entity_id, + "media_player.tv", + "switch.other", + ], }, } From 707aa5f684f8d4865fd49fd80c0821f364e47934 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 May 2022 13:35:38 -0500 Subject: [PATCH 0172/3516] Fix oncue not logging back in when the session expires (#71258) --- homeassistant/components/oncue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/oncue/manifest.json b/homeassistant/components/oncue/manifest.json index 9991d35ed18..e0533129d94 100644 --- a/homeassistant/components/oncue/manifest.json +++ b/homeassistant/components/oncue/manifest.json @@ -9,7 +9,7 @@ } ], "documentation": "https://www.home-assistant.io/integrations/oncue", - "requirements": ["aiooncue==0.3.2"], + "requirements": ["aiooncue==0.3.4"], "codeowners": ["@bdraco"], "iot_class": "cloud_polling", "loggers": ["aiooncue"] diff --git a/requirements_all.txt b/requirements_all.txt index 2cbc7fa8134..9429d4be32e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -211,7 +211,7 @@ aionotify==0.2.0 aionotion==3.0.2 # homeassistant.components.oncue -aiooncue==0.3.2 +aiooncue==0.3.4 # homeassistant.components.acmeda aiopulse==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e824051d07a..3db81fe8524 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -177,7 +177,7 @@ aionanoleaf==0.2.0 aionotion==3.0.2 # homeassistant.components.oncue -aiooncue==0.3.2 +aiooncue==0.3.4 # homeassistant.components.acmeda aiopulse==0.4.3 From 9aed63f2d80801d2a55262002ffeb684b83b6e4c Mon Sep 17 00:00:00 2001 From: James Szalay Date: Tue, 3 May 2022 15:17:27 -0400 Subject: [PATCH 0173/3516] Updated vesync component fans list to handle alt ids for models. (#71259) * Updated vesync component fans list to handle alt ids for models. * Lint Co-authored-by: Paulus Schoutsen --- homeassistant/components/vesync/fan.py | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 41ed6109be2..e37a6c8893e 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -20,10 +20,20 @@ _LOGGER = logging.getLogger(__name__) DEV_TYPE_TO_HA = { "LV-PUR131S": "fan", + "LV-RH131S": "fan", # Alt ID Model LV-PUR131S "Core200S": "fan", + "LAP-C201S-AUSR": "fan", # Alt ID Model Core200S + "LAP-C202S-WUSR": "fan", # Alt ID Model Core200S "Core300S": "fan", + "LAP-C301S-WJP": "fan", # Alt ID Model Core300S "Core400S": "fan", + "LAP-C401S-WJP": "fan", # Alt ID Model Core400S + "LAP-C401S-WUSR": "fan", # Alt ID Model Core400S + "LAP-C401S-WAAA": "fan", # Alt ID Model Core400S "Core600S": "fan", + "LAP-C601S-WUS": "fan", # Alt ID Model Core600S + "LAP-C601S-WUSR": "fan", # Alt ID Model Core600S + "LAP-C601S-WEU": "fan", # Alt ID Model Core600S } FAN_MODE_AUTO = "auto" @@ -31,17 +41,37 @@ FAN_MODE_SLEEP = "sleep" PRESET_MODES = { "LV-PUR131S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], + "LV-RH131S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model LV-PUR131S "Core200S": [FAN_MODE_SLEEP], + "LAP-C201S-AUSR": [FAN_MODE_SLEEP], # Alt ID Model Core200S + "LAP-C202S-WUSR": [FAN_MODE_SLEEP], # Alt ID Model Core200S "Core300S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], + "LAP-C301S-WJP": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core300S "Core400S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], + "LAP-C401S-WJP": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core400S + "LAP-C401S-WUSR": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core400S + "LAP-C401S-WAAA": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core400S "Core600S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], + "LAP-C601S-WUS": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core600S + "LAP-C601S-WUSR": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core600S + "LAP-C601S-WEU": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core600S } SPEED_RANGE = { # off is not included "LV-PUR131S": (1, 3), + "LV-RH131S": (1, 3), # ALt ID Model LV-PUR131S "Core200S": (1, 3), + "LAP-C201S-AUSR": (1, 3), # ALt ID Model Core200S + "LAP-C202S-WUSR": (1, 3), # ALt ID Model Core200S "Core300S": (1, 3), + "LAP-C301S-WJP": (1, 3), # ALt ID Model Core300S "Core400S": (1, 4), + "LAP-C401S-WJP": (1, 4), # ALt ID Model Core400S + "LAP-C401S-WUSR": (1, 4), # ALt ID Model Core400S + "LAP-C401S-WAAA": (1, 4), # ALt ID Model Core400S "Core600S": (1, 4), + "LAP-C601S-WUS": (1, 4), # ALt ID Model Core600S + "LAP-C601S-WUSR": (1, 4), # ALt ID Model Core600S + "LAP-C601S-WEU": (1, 4), # ALt ID Model Core600S } From a175943187d759c1b80b38a570f09ab27077c399 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Tue, 3 May 2022 14:34:20 -0400 Subject: [PATCH 0174/3516] Load Insteon modem database on startup if needed (#71261) --- homeassistant/components/insteon/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 94c18df9a33..e2bebf0bb7f 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -49,7 +49,7 @@ async def async_get_device_config(hass, config_entry): with suppress(AttributeError): await devices[address].async_status() - load_aldb = devices.modem.aldb.read_write_mode == ReadWriteMode.UNKNOWN + load_aldb = 2 if devices.modem.aldb.read_write_mode == ReadWriteMode.UNKNOWN else 1 await devices.async_load(id_devices=1, load_modem_aldb=load_aldb) for addr in devices: device = devices[addr] From ad5c2cdf8f09149b9b9edbe10e6cfa26eec2da1c Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 3 May 2022 13:51:07 -0500 Subject: [PATCH 0175/3516] Bump frontend to 20220503.0 (#71262) --- 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 18e2fc6ed8f..89dd75fa96c 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220502.0"], + "requirements": ["home-assistant-frontend==20220503.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f328394b9c1..fcb63a9426c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220502.0 +home-assistant-frontend==20220503.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.1 diff --git a/requirements_all.txt b/requirements_all.txt index 9429d4be32e..900e4047c6d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220502.0 +home-assistant-frontend==20220503.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3db81fe8524..b30a5105fff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -580,7 +580,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220502.0 +home-assistant-frontend==20220503.0 # homeassistant.components.home_connect homeconnect==0.7.0 From 23738d5e91a94e1a00a9ea5dcf9733a68278761d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 3 May 2022 21:19:43 +0200 Subject: [PATCH 0176/3516] Reject MQTT topics which include control- or non-characters (#71263) --- homeassistant/components/mqtt/util.py | 9 +++++++++ tests/components/mqtt/test_init.py | 28 ++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index b8fca50a153..66eec1bdfe8 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -31,6 +31,15 @@ def valid_topic(value: Any) -> str: ) if "\0" in value: raise vol.Invalid("MQTT topic name/filter must not contain null character.") + if any(char <= "\u001F" for char in value): + raise vol.Invalid("MQTT topic name/filter must not contain control characters.") + if any("\u007f" <= char <= "\u009F" for char in value): + raise vol.Invalid("MQTT topic name/filter must not contain control characters.") + if any("\ufdd0" <= char <= "\ufdef" for char in value): + raise vol.Invalid("MQTT topic name/filter must not contain non-characters.") + if any((ord(char) & 0xFFFF) in (0xFFFE, 0xFFFF) for char in value): + raise vol.Invalid("MQTT topic name/filter must not contain noncharacters.") + return value diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 03874ed331e..763cbfc1b71 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -522,11 +522,29 @@ def test_validate_topic(): # Topics "SHOULD NOT" include these special characters # (not MUST NOT, RFC2119). The receiver MAY close the connection. - mqtt.util.valid_topic("\u0001") - mqtt.util.valid_topic("\u001F") - mqtt.util.valid_topic("\u009F") - mqtt.util.valid_topic("\u009F") - mqtt.util.valid_topic("\uffff") + # We enforce this because mosquitto does: https://github.com/eclipse/mosquitto/commit/94fdc9cb44c829ff79c74e1daa6f7d04283dfffd + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\u0001") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\u001F") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\u007F") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\u009F") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\ufdd0") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\ufdef") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\ufffe") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\ufffe") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\uffff") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\U0001fffe") + with pytest.raises(vol.Invalid): + mqtt.util.valid_topic("\U0001ffff") def test_validate_subscribe_topic(): From 269c71d2fbb59f0d6c118416b02f1487052c9229 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 May 2022 12:16:57 -0700 Subject: [PATCH 0177/3516] Bump aioslimproto to 2.0.0 (#71265) --- homeassistant/components/slimproto/manifest.json | 2 +- homeassistant/components/slimproto/media_player.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/slimproto/manifest.json b/homeassistant/components/slimproto/manifest.json index 557428919a4..eb4ab00f18d 100644 --- a/homeassistant/components/slimproto/manifest.json +++ b/homeassistant/components/slimproto/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "iot_class": "local_push", "documentation": "https://www.home-assistant.io/integrations/slimproto", - "requirements": ["aioslimproto==1.0.2"], + "requirements": ["aioslimproto==2.0.0"], "codeowners": ["@marcelveldt"], "after_dependencies": ["media_source"] } diff --git a/homeassistant/components/slimproto/media_player.py b/homeassistant/components/slimproto/media_player.py index d41d10432db..6b1989830e2 100644 --- a/homeassistant/components/slimproto/media_player.py +++ b/homeassistant/components/slimproto/media_player.py @@ -118,7 +118,7 @@ class SlimProtoPlayer(MediaPlayerEntity): EventType.PLAYER_CONNECTED, EventType.PLAYER_DISCONNECTED, EventType.PLAYER_NAME_RECEIVED, - EventType.PLAYER_RPC_EVENT, + EventType.PLAYER_CLI_EVENT, ), player_filter=self.player.player_id, ) @@ -205,7 +205,7 @@ class SlimProtoPlayer(MediaPlayerEntity): if event.type == EventType.PLAYER_CONNECTED: # player reconnected, update our player object self.player = self.slimserver.get_player(event.player_id) - if event.type == EventType.PLAYER_RPC_EVENT: + if event.type == EventType.PLAYER_CLI_EVENT: # rpc event from player such as a button press, # forward on the eventbus for others to handle dev_id = self.registry_entry.device_id if self.registry_entry else None diff --git a/requirements_all.txt b/requirements_all.txt index 900e4047c6d..32caea924ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -244,7 +244,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.slimproto -aioslimproto==1.0.2 +aioslimproto==2.0.0 # homeassistant.components.steamist aiosteamist==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b30a5105fff..40e1252b848 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -210,7 +210,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.slimproto -aioslimproto==1.0.2 +aioslimproto==2.0.0 # homeassistant.components.steamist aiosteamist==0.3.1 From 461ebcc83543602615c4f02ec9e3cb139b63c4e1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 May 2022 12:21:11 -0700 Subject: [PATCH 0178/3516] Bumped version to 2022.5.0b7 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a3647a3a27c..5312d175858 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0b7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 84275aca14f..7115459d9ff 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.5.0b6 +version = 2022.5.0b7 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From e30940ef2adc2a2f4b006e85092f53bcbcb68bfb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 May 2022 15:56:22 -0500 Subject: [PATCH 0179/3516] Move processing of recorder service call arguments into services.py (#71260) --- homeassistant/components/recorder/__init__.py | 2 +- homeassistant/components/recorder/core.py | 170 ++++++++---------- homeassistant/components/recorder/services.py | 18 +- homeassistant/components/recorder/tasks.py | 12 +- .../components/recorder/websocket_api.py | 2 +- tests/components/recorder/test_init.py | 2 +- tests/components/recorder/test_purge.py | 6 +- 7 files changed, 104 insertions(+), 108 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 00f12710c18..ca1ecd8c71a 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -179,4 +179,4 @@ async def _process_recorder_platform( ) -> None: """Process a recorder platform.""" instance: Recorder = hass.data[DATA_INSTANCE] - instance.queue.put(AddRecorderPlatformTask(domain, platform)) + instance.queue_task(AddRecorderPlatformTask(domain, platform)) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index b96eee2c67f..7f07a4483cb 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -28,7 +28,6 @@ from homeassistant.const import ( MATCH_ALL, ) from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback -from homeassistant.helpers.entityfilter import generate_filter from homeassistant.helpers.event import ( async_track_time_change, async_track_time_interval, @@ -38,9 +37,6 @@ import homeassistant.util.dt as dt_util from . import migration, statistics from .const import ( - ATTR_APPLY_FILTER, - ATTR_KEEP_DAYS, - ATTR_REPACK, DB_WORKER_PREFIX, KEEPALIVE_TIME, MAX_QUEUE_BACKLOG, @@ -70,7 +66,6 @@ from .tasks import ( ExternalStatisticsTask, KeepAliveTask, PerodicCleanupTask, - PurgeEntitiesTask, PurgeTask, RecorderTask, StatisticsTask, @@ -112,6 +107,7 @@ SHUTDOWN_TASK = object() COMMIT_TASK = CommitTask() KEEP_ALIVE_TASK = KeepAliveTask() +WAIT_TASK = WaitTask() DB_LOCK_TIMEOUT = 30 DB_LOCK_QUEUE_CHECK_TIMEOUT = 1 @@ -152,7 +148,7 @@ class Recorder(threading.Thread): self.keep_days = keep_days self._hass_started: asyncio.Future[object] = asyncio.Future() self.commit_interval = commit_interval - self.queue: queue.SimpleQueue[RecorderTask] = queue.SimpleQueue() + self._queue: queue.SimpleQueue[RecorderTask] = queue.SimpleQueue() self.db_url = uri self.db_max_retries = db_max_retries self.db_retry_wait = db_retry_wait @@ -175,21 +171,42 @@ class Recorder(threading.Thread): self.event_session: Session | None = None self.get_session: Callable[[], Session] | None = None self._completed_first_database_setup: bool | None = None - self._event_listener: CALLBACK_TYPE | None = None self.async_migration_event = asyncio.Event() self.migration_in_progress = False - self._queue_watcher: CALLBACK_TYPE | None = None self._db_supports_row_number = True self._database_lock_task: DatabaseLockTask | None = None self._db_executor: DBInterruptibleThreadPoolExecutor | None = None self._exclude_attributes_by_domain = exclude_attributes_by_domain + self._event_listener: CALLBACK_TYPE | None = None + self._queue_watcher: CALLBACK_TYPE | None = None self._keep_alive_listener: CALLBACK_TYPE | None = None self._commit_listener: CALLBACK_TYPE | None = None self._periodic_listener: CALLBACK_TYPE | None = None self._nightly_listener: CALLBACK_TYPE | None = None self.enabled = True + @property + def backlog(self) -> int: + """Return the number of items in the recorder backlog.""" + return self._queue.qsize() + + @property + def _using_file_sqlite(self) -> bool: + """Short version to check if we are using sqlite3 as a file.""" + return self.db_url != SQLITE_URL_PREFIX and self.db_url.startswith( + SQLITE_URL_PREFIX + ) + + @property + def recording(self) -> bool: + """Return if the recorder is recording.""" + return self._event_listener is not None + + def queue_task(self, task: RecorderTask) -> None: + """Add a task to the recorder queue.""" + self._queue.put(task) + def set_enable(self, enable: bool) -> None: """Enable or disable recording events and states.""" self.enabled = enable @@ -222,7 +239,7 @@ class Recorder(threading.Thread): def _async_keep_alive(self, now: datetime) -> None: """Queue a keep alive.""" if self._event_listener: - self.queue.put(KEEP_ALIVE_TASK) + self.queue_task(KEEP_ALIVE_TASK) @callback def _async_commit(self, now: datetime) -> None: @@ -232,7 +249,7 @@ class Recorder(threading.Thread): and not self._database_lock_task and self._event_session_has_pending_writes() ): - self.queue.put(COMMIT_TASK) + self.queue_task(COMMIT_TASK) @callback def async_add_executor_job( @@ -253,7 +270,7 @@ class Recorder(threading.Thread): The queue grows during migraton or if something really goes wrong. """ - size = self.queue.qsize() + size = self.backlog _LOGGER.debug("Recorder queue size is: %s", size) if size <= MAX_QUEUE_BACKLOG: return @@ -314,73 +331,52 @@ class Recorder(threading.Thread): # Unknown what it is. return True - def do_adhoc_purge(self, **kwargs: Any) -> None: - """Trigger an adhoc purge retaining keep_days worth of data.""" - keep_days = kwargs.get(ATTR_KEEP_DAYS, self.keep_days) - repack = cast(bool, kwargs[ATTR_REPACK]) - apply_filter = cast(bool, kwargs[ATTR_APPLY_FILTER]) - - purge_before = dt_util.utcnow() - timedelta(days=keep_days) - self.queue.put(PurgeTask(purge_before, repack, apply_filter)) - - def do_adhoc_purge_entities( - self, entity_ids: set[str], domains: list[str], entity_globs: list[str] - ) -> None: - """Trigger an adhoc purge of requested entities.""" - entity_filter = generate_filter(domains, list(entity_ids), [], [], entity_globs) - self.queue.put(PurgeEntitiesTask(entity_filter)) - def do_adhoc_statistics(self, **kwargs: Any) -> None: """Trigger an adhoc statistics run.""" if not (start := kwargs.get("start")): start = statistics.get_start_time() - self.queue.put(StatisticsTask(start)) + self.queue_task(StatisticsTask(start)) + + def _empty_queue(self, event: Event) -> None: + """Empty the queue if its still present at final write.""" + + # If the queue is full of events to be processed because + # the database is so broken that every event results in a retry + # we will never be able to get though the events to shutdown in time. + # + # We drain all the events in the queue and then insert + # an empty one to ensure the next thing the recorder sees + # is a request to shutdown. + while True: + try: + self._queue.get_nowait() + except queue.Empty: + break + self.queue_task(StopTask()) + + async def _async_shutdown(self, event: Event) -> None: + """Shut down the Recorder.""" + if not self._hass_started.done(): + self._hass_started.set_result(SHUTDOWN_TASK) + self.queue_task(StopTask()) + self._async_stop_listeners() + await self.hass.async_add_executor_job(self.join) + + @callback + def _async_hass_started(self, event: Event) -> None: + """Notify that hass has started.""" + self._hass_started.set_result(None) @callback def async_register(self) -> None: """Post connection initialize.""" - - def _empty_queue(event: Event) -> None: - """Empty the queue if its still present at final write.""" - - # If the queue is full of events to be processed because - # the database is so broken that every event results in a retry - # we will never be able to get though the events to shutdown in time. - # - # We drain all the events in the queue and then insert - # an empty one to ensure the next thing the recorder sees - # is a request to shutdown. - while True: - try: - self.queue.get_nowait() - except queue.Empty: - break - self.queue.put(StopTask()) - - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_FINAL_WRITE, _empty_queue) - - async def _async_shutdown(event: Event) -> None: - """Shut down the Recorder.""" - if not self._hass_started.done(): - self._hass_started.set_result(SHUTDOWN_TASK) - self.queue.put(StopTask()) - self._async_stop_listeners() - await self.hass.async_add_executor_job(self.join) - - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_shutdown) - + bus = self.hass.bus + bus.async_listen_once(EVENT_HOMEASSISTANT_FINAL_WRITE, self._empty_queue) + bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown) if self.hass.state == CoreState.running: self._hass_started.set_result(None) return - - @callback - def _async_hass_started(event: Event) -> None: - """Notify that hass has started.""" - self._hass_started.set_result(None) - - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, _async_hass_started - ) + bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, self._async_hass_started) @callback def async_connection_failed(self) -> None: @@ -414,9 +410,9 @@ class Recorder(threading.Thread): # until after the database is vacuumed repack = self.auto_repack and is_second_sunday(now) purge_before = dt_util.utcnow() - timedelta(days=self.keep_days) - self.queue.put(PurgeTask(purge_before, repack=repack, apply_filter=False)) + self.queue_task(PurgeTask(purge_before, repack=repack, apply_filter=False)) else: - self.queue.put(PerodicCleanupTask()) + self.queue_task(PerodicCleanupTask()) @callback def async_periodic_statistics(self, now: datetime) -> None: @@ -425,33 +421,33 @@ class Recorder(threading.Thread): Short term statistics run every 5 minutes """ start = statistics.get_start_time() - self.queue.put(StatisticsTask(start)) + self.queue_task(StatisticsTask(start)) @callback def async_adjust_statistics( self, statistic_id: str, start_time: datetime, sum_adjustment: float ) -> None: """Adjust statistics.""" - self.queue.put(AdjustStatisticsTask(statistic_id, start_time, sum_adjustment)) + self.queue_task(AdjustStatisticsTask(statistic_id, start_time, sum_adjustment)) @callback def async_clear_statistics(self, statistic_ids: list[str]) -> None: """Clear statistics for a list of statistic_ids.""" - self.queue.put(ClearStatisticsTask(statistic_ids)) + self.queue_task(ClearStatisticsTask(statistic_ids)) @callback def async_update_statistics_metadata( self, statistic_id: str, unit_of_measurement: str | None ) -> None: """Update statistics metadata for a statistic_id.""" - self.queue.put(UpdateStatisticsMetadataTask(statistic_id, unit_of_measurement)) + self.queue_task(UpdateStatisticsMetadataTask(statistic_id, unit_of_measurement)) @callback def async_external_statistics( self, metadata: StatisticMetaData, stats: Iterable[StatisticData] ) -> None: """Schedule external statistics.""" - self.queue.put(ExternalStatisticsTask(metadata, stats)) + self.queue_task(ExternalStatisticsTask(metadata, stats)) @callback def using_sqlite(self) -> bool: @@ -553,7 +549,7 @@ class Recorder(threading.Thread): # has changed. This reduces the disk io. self.stop_requested = False while not self.stop_requested: - task = self.queue.get() + task = self._queue.get() _LOGGER.debug("Processing task: %s", task) try: self._process_one_task_or_recover(task) @@ -643,7 +639,7 @@ class Recorder(threading.Thread): # Notify that lock is being held, wait until database can be used again. self.hass.add_job(_async_set_database_locked, task) while not task.database_unlock.wait(timeout=DB_LOCK_QUEUE_CHECK_TIMEOUT): - if self.queue.qsize() > MAX_QUEUE_BACKLOG * 0.9: + if self.backlog > MAX_QUEUE_BACKLOG * 0.9: _LOGGER.warning( "Database queue backlog reached more than 90% of maximum queue " "length while waiting for backup to finish; recorder will now " @@ -654,7 +650,7 @@ class Recorder(threading.Thread): break _LOGGER.info( "Database queue backlog reached %d entries during backup", - self.queue.qsize(), + self.backlog, ) def _process_one_event(self, event: Event) -> None: @@ -908,7 +904,7 @@ class Recorder(threading.Thread): @callback def event_listener(self, event: Event) -> None: """Listen for new events and put them in the process queue.""" - self.queue.put(EventTask(event)) + self.queue_task(EventTask(event)) def block_till_done(self) -> None: """Block till all events processed. @@ -923,7 +919,7 @@ class Recorder(threading.Thread): is in the database. """ self._queue_watch.clear() - self.queue.put(WaitTask()) + self.queue_task(WAIT_TASK) self._queue_watch.wait() async def lock_database(self) -> bool: @@ -940,7 +936,7 @@ class Recorder(threading.Thread): database_locked = asyncio.Event() task = DatabaseLockTask(database_locked, threading.Event(), False) - self.queue.put(task) + self.queue_task(task) try: await asyncio.wait_for(database_locked.wait(), timeout=DB_LOCK_TIMEOUT) except asyncio.TimeoutError as err: @@ -1013,13 +1009,6 @@ class Recorder(threading.Thread): self.get_session = scoped_session(sessionmaker(bind=self.engine, future=True)) _LOGGER.debug("Connected to recorder database") - @property - def _using_file_sqlite(self) -> bool: - """Short version to check if we are using sqlite3 as a file.""" - return self.db_url != SQLITE_URL_PREFIX and self.db_url.startswith( - SQLITE_URL_PREFIX - ) - def _close_connection(self) -> None: """Close the connection.""" assert self.engine is not None @@ -1053,7 +1042,7 @@ class Recorder(threading.Thread): while start < last_period: end = start + timedelta(minutes=5) _LOGGER.debug("Compiling missing statistics for %s-%s", start, end) - self.queue.put(StatisticsTask(start)) + self.queue_task(StatisticsTask(start)) start = end def _end_session(self) -> None: @@ -1075,8 +1064,3 @@ class Recorder(threading.Thread): self._stop_executor() self._end_session() self._close_connection() - - @property - def recording(self) -> bool: - """Return if the recorder is recording.""" - return self._event_listener is not None diff --git a/homeassistant/components/recorder/services.py b/homeassistant/components/recorder/services.py index 3da2c63a27c..14337290c9b 100644 --- a/homeassistant/components/recorder/services.py +++ b/homeassistant/components/recorder/services.py @@ -1,21 +1,26 @@ """Support for recorder services.""" from __future__ import annotations +from datetime import timedelta +from typing import cast + import voluptuous as vol from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entityfilter import generate_filter from homeassistant.helpers.service import async_extract_entity_ids +import homeassistant.util.dt as dt_util from .const import ATTR_APPLY_FILTER, ATTR_KEEP_DAYS, ATTR_REPACK, DOMAIN from .core import Recorder +from .tasks import PurgeEntitiesTask, PurgeTask SERVICE_PURGE = "purge" SERVICE_PURGE_ENTITIES = "purge_entities" SERVICE_ENABLE = "enable" SERVICE_DISABLE = "disable" - SERVICE_PURGE_SCHEMA = vol.Schema( { vol.Optional(ATTR_KEEP_DAYS): cv.positive_int, @@ -44,7 +49,12 @@ SERVICE_DISABLE_SCHEMA = vol.Schema({}) def _async_register_purge_service(hass: HomeAssistant, instance: Recorder) -> None: async def async_handle_purge_service(service: ServiceCall) -> None: """Handle calls to the purge service.""" - instance.do_adhoc_purge(**service.data) + kwargs = service.data + keep_days = kwargs.get(ATTR_KEEP_DAYS, instance.keep_days) + repack = cast(bool, kwargs[ATTR_REPACK]) + apply_filter = cast(bool, kwargs[ATTR_APPLY_FILTER]) + purge_before = dt_util.utcnow() - timedelta(days=keep_days) + instance.queue_task(PurgeTask(purge_before, repack, apply_filter)) hass.services.async_register( DOMAIN, SERVICE_PURGE, async_handle_purge_service, schema=SERVICE_PURGE_SCHEMA @@ -60,8 +70,8 @@ def _async_register_purge_entities_service( entity_ids = await async_extract_entity_ids(hass, service) domains = service.data.get(ATTR_DOMAINS, []) entity_globs = service.data.get(ATTR_ENTITY_GLOBS, []) - - instance.do_adhoc_purge_entities(entity_ids, domains, entity_globs) + entity_filter = generate_filter(domains, list(entity_ids), [], [], entity_globs) + instance.queue_task(PurgeEntitiesTask(entity_filter)) hass.services.async_register( DOMAIN, diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index fdffa63bcd3..bed49e36f16 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -78,7 +78,9 @@ class PurgeTask(RecorderTask): periodic_db_cleanups(instance) return # Schedule a new purge task if this one didn't finish - instance.queue.put(PurgeTask(self.purge_before, self.repack, self.apply_filter)) + instance.queue_task( + PurgeTask(self.purge_before, self.repack, self.apply_filter) + ) @dataclass @@ -92,7 +94,7 @@ class PurgeEntitiesTask(RecorderTask): if purge.purge_entity_data(instance, self.entity_filter): return # Schedule a new purge task if this one didn't finish - instance.queue.put(PurgeEntitiesTask(self.entity_filter)) + instance.queue_task(PurgeEntitiesTask(self.entity_filter)) @dataclass @@ -115,7 +117,7 @@ class StatisticsTask(RecorderTask): if statistics.compile_statistics(instance, self.start): return # Schedule a new statistics task if this one didn't finish - instance.queue.put(StatisticsTask(self.start)) + instance.queue_task(StatisticsTask(self.start)) @dataclass @@ -130,7 +132,7 @@ class ExternalStatisticsTask(RecorderTask): if statistics.add_external_statistics(instance, self.metadata, self.statistics): return # Schedule a new statistics task if this one didn't finish - instance.queue.put(ExternalStatisticsTask(self.metadata, self.statistics)) + instance.queue_task(ExternalStatisticsTask(self.metadata, self.statistics)) @dataclass @@ -151,7 +153,7 @@ class AdjustStatisticsTask(RecorderTask): ): return # Schedule a new adjust statistics task if this one didn't finish - instance.queue.put( + instance.queue_task( AdjustStatisticsTask( self.statistic_id, self.start_time, self.sum_adjustment ) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 585641665af..a851d2681f4 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -148,7 +148,7 @@ def ws_info( """Return status of the recorder.""" instance: Recorder = hass.data[DATA_INSTANCE] - backlog = instance.queue.qsize() if instance and instance.queue else None + backlog = instance.backlog if instance else None migration_in_progress = async_migration_in_progress(hass) recording = instance.recording if instance else False thread_alive = instance.is_alive() if instance else False diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 1e8ff89f20d..9bfca76394b 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1395,7 +1395,7 @@ async def test_database_lock_timeout(hass, recorder_mock): self.event.wait() block_task = BlockQueue() - instance.queue.put(block_task) + instance.queue_task(block_task) with patch.object(recorder.core, "DB_LOCK_TIMEOUT", 0.1): try: with pytest.raises(TimeoutError): diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 7c3a2adcdc1..2d711f17cf3 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -557,7 +557,7 @@ async def test_purge_cutoff_date( assert events.filter(Events.event_type == "PURGE").count() == rows - 1 assert events.filter(Events.event_type == "KEEP").count() == 1 - instance.queue.put(PurgeTask(cutoff, repack=False, apply_filter=False)) + instance.queue_task(PurgeTask(cutoff, repack=False, apply_filter=False)) await hass.async_block_till_done() await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -588,7 +588,7 @@ async def test_purge_cutoff_date( assert events.filter(Events.event_type == "KEEP").count() == 1 # Make sure we can purge everything - instance.queue.put(PurgeTask(dt_util.utcnow(), repack=False, apply_filter=False)) + instance.queue_task(PurgeTask(dt_util.utcnow(), repack=False, apply_filter=False)) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) @@ -599,7 +599,7 @@ async def test_purge_cutoff_date( assert state_attributes.count() == 0 # Make sure we can purge everything when the db is already empty - instance.queue.put(PurgeTask(dt_util.utcnow(), repack=False, apply_filter=False)) + instance.queue_task(PurgeTask(dt_util.utcnow(), repack=False, apply_filter=False)) await async_recorder_block_till_done(hass) await async_wait_purge_done(hass) From 93153b379047c1ebe654a40de796f8e5b729bef0 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Tue, 3 May 2022 22:56:57 +0200 Subject: [PATCH 0180/3516] Add UniqueID to AsusWRT config entry (#70478) --- .../components/asuswrt/config_flow.py | 59 ++++-- .../components/asuswrt/diagnostics.py | 3 +- homeassistant/components/asuswrt/router.py | 19 +- homeassistant/components/asuswrt/sensor.py | 8 +- homeassistant/components/asuswrt/strings.json | 3 +- .../components/asuswrt/translations/en.json | 3 +- tests/components/asuswrt/test_config_flow.py | 187 +++++++++++------- tests/components/asuswrt/test_sensor.py | 19 +- 8 files changed, 201 insertions(+), 100 deletions(-) diff --git a/homeassistant/components/asuswrt/config_flow.py b/homeassistant/components/asuswrt/config_flow.py index ab62b879f75..e1b34cca15b 100644 --- a/homeassistant/components/asuswrt/config_flow.py +++ b/homeassistant/components/asuswrt/config_flow.py @@ -25,6 +25,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.device_registry import format_mac from .const import ( CONF_DNSMASQ, @@ -41,11 +42,13 @@ from .const import ( PROTOCOL_SSH, PROTOCOL_TELNET, ) -from .router import get_api +from .router import get_api, get_nvram_info + +LABEL_MAC = "LABEL_MAC" RESULT_CONN_ERROR = "cannot_connect" -RESULT_UNKNOWN = "unknown" RESULT_SUCCESS = "success" +RESULT_UNKNOWN = "unknown" _LOGGER = logging.getLogger(__name__) @@ -107,7 +110,9 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN): ) @staticmethod - async def _async_check_connection(user_input: dict[str, Any]) -> str: + async def _async_check_connection( + user_input: dict[str, Any] + ) -> tuple[str, str | None]: """Attempt to connect the AsusWrt router.""" host: str = user_input[CONF_HOST] @@ -117,29 +122,37 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN): except OSError: _LOGGER.error("Error connecting to the AsusWrt router at %s", host) - return RESULT_CONN_ERROR + return RESULT_CONN_ERROR, None except Exception: # pylint: disable=broad-except _LOGGER.exception( "Unknown error connecting with AsusWrt router at %s", host ) - return RESULT_UNKNOWN + return RESULT_UNKNOWN, None if not api.is_connected: _LOGGER.error("Error connecting to the AsusWrt router at %s", host) - return RESULT_CONN_ERROR + return RESULT_CONN_ERROR, None + label_mac = await get_nvram_info(api, LABEL_MAC) conf_protocol = user_input[CONF_PROTOCOL] if conf_protocol == PROTOCOL_TELNET: api.connection.disconnect() - return RESULT_SUCCESS + + unique_id = None + if label_mac and "label_mac" in label_mac: + unique_id = format_mac(label_mac["label_mac"]) + return RESULT_SUCCESS, unique_id async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initiated by the user.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") + + # if exist one entry without unique ID, we abort config flow + for unique_id in self._async_current_ids(): + if unique_id is None: + return self.async_abort(reason="not_unique_id_exist") if user_input is None: return self._show_setup_form(user_input) @@ -166,17 +179,27 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_host" if not errors: - result = await self._async_check_connection(user_input) - if result != RESULT_SUCCESS: - errors["base"] = result + result, unique_id = await self._async_check_connection(user_input) + if result == RESULT_SUCCESS: + if unique_id: + await self.async_set_unique_id(unique_id) + # we allow configure a single instance without unique id + elif self._async_current_entries(): + return self.async_abort(reason="invalid_unique_id") + else: + _LOGGER.warning( + "This device do not provide a valid Unique ID." + " Configuration of multiple instance will not be possible" + ) - if errors: - return self._show_setup_form(user_input, errors) + return self.async_create_entry( + title=host, + data=user_input, + ) - return self.async_create_entry( - title=host, - data=user_input, - ) + errors["base"] = result + + return self._show_setup_form(user_input, errors) @staticmethod @callback diff --git a/homeassistant/components/asuswrt/diagnostics.py b/homeassistant/components/asuswrt/diagnostics.py index 16f5468f87d..61de4c866db 100644 --- a/homeassistant/components/asuswrt/diagnostics.py +++ b/homeassistant/components/asuswrt/diagnostics.py @@ -11,6 +11,7 @@ from homeassistant.const import ( ATTR_CONNECTIONS, ATTR_IDENTIFIERS, CONF_PASSWORD, + CONF_UNIQUE_ID, CONF_USERNAME, ) from homeassistant.core import HomeAssistant @@ -19,7 +20,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from .const import DATA_ASUSWRT, DOMAIN from .router import AsusWrtRouter -TO_REDACT = {CONF_PASSWORD, CONF_USERNAME} +TO_REDACT = {CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME} TO_REDACT_DEV = {ATTR_CONNECTIONS, ATTR_IDENTIFIERS} diff --git a/homeassistant/components/asuswrt/router.py b/homeassistant/components/asuswrt/router.py index 2484b9880c3..f68c77a2a66 100644 --- a/homeassistant/components/asuswrt/router.py +++ b/homeassistant/components/asuswrt/router.py @@ -51,6 +51,7 @@ from .const import ( ) CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP] +DEFAULT_NAME = "Asuswrt" KEY_COORDINATOR = "coordinator" KEY_SENSORS = "sensors" @@ -260,10 +261,10 @@ class AsusWrtRouter: raise ConfigEntryNotReady # System - model = await _get_nvram_info(self._api, "MODEL") + model = await get_nvram_info(self._api, "MODEL") if model and "model" in model: self._model = model["model"] - firmware = await _get_nvram_info(self._api, "FIRMWARE") + firmware = await get_nvram_info(self._api, "FIRMWARE") if firmware and "firmver" in firmware and "buildno" in firmware: self._sw_v = f"{firmware['firmver']} (build {firmware['buildno']})" @@ -441,7 +442,7 @@ class AsusWrtRouter: def device_info(self) -> DeviceInfo: """Return the device information.""" return DeviceInfo( - identifiers={(DOMAIN, "AsusWRT")}, + identifiers={(DOMAIN, self.unique_id or "AsusWRT")}, name=self._host, model=self._model, manufacturer="Asus", @@ -464,6 +465,16 @@ class AsusWrtRouter: """Return router hostname.""" return self._host + @property + def unique_id(self) -> str | None: + """Return router unique id.""" + return self._entry.unique_id + + @property + def name(self) -> str: + """Return router name.""" + return self._host if self.unique_id else DEFAULT_NAME + @property def devices(self) -> dict[str, AsusWrtDevInfo]: """Return devices.""" @@ -475,7 +486,7 @@ class AsusWrtRouter: return self._sensors_coordinator -async def _get_nvram_info(api: AsusWrt, info_type: str) -> dict[str, Any]: +async def get_nvram_info(api: AsusWrt, info_type: str) -> dict[str, Any]: """Get AsusWrt router info from nvram.""" info = {} try: diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 5c46b3e693c..e66874e4137 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -43,7 +43,6 @@ class AsusWrtSensorEntityDescription(SensorEntityDescription): precision: int = 2 -DEFAULT_PREFIX = "Asuswrt" UNIT_DEVICES = "Devices" CONNECTION_SENSORS: tuple[AsusWrtSensorEntityDescription, ...] = ( @@ -190,8 +189,11 @@ class AsusWrtSensor(CoordinatorEntity, SensorEntity): super().__init__(coordinator) self.entity_description: AsusWrtSensorEntityDescription = description - self._attr_name = f"{DEFAULT_PREFIX} {description.name}" - self._attr_unique_id = f"{DOMAIN} {self.name}" + self._attr_name = f"{router.name} {description.name}" + if router.unique_id: + self._attr_unique_id = f"{DOMAIN} {router.unique_id} {description.name}" + else: + self._attr_unique_id = f"{DOMAIN} {self.name}" self._attr_device_info = router.device_info self._attr_extra_state_attributes = {"hostname": router.host} diff --git a/homeassistant/components/asuswrt/strings.json b/homeassistant/components/asuswrt/strings.json index 56f3c659c0b..9023f5aa604 100644 --- a/homeassistant/components/asuswrt/strings.json +++ b/homeassistant/components/asuswrt/strings.json @@ -25,7 +25,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "invalid_unique_id": "Impossible to determine a valid unique id for the device", + "not_unique_id_exist": "A device without a valid UniqueID is already configured. Configuration of multiple instance is not possible" } }, "options": { diff --git a/homeassistant/components/asuswrt/translations/en.json b/homeassistant/components/asuswrt/translations/en.json index 4bef898ff35..494156eae45 100644 --- a/homeassistant/components/asuswrt/translations/en.json +++ b/homeassistant/components/asuswrt/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "Already configured. Only a single configuration possible." + "invalid_unique_id": "Impossible to determine a valid unique id for the device", + "not_unique_id_exist": "A device without a valid UniqueID is already configured. Configuration of multiple instance is not possible" }, "error": { "cannot_connect": "Failed to connect", diff --git a/tests/components/asuswrt/test_config_flow.py b/tests/components/asuswrt/test_config_flow.py index a1a37d25460..09c484ff69d 100644 --- a/tests/components/asuswrt/test_config_flow.py +++ b/tests/components/asuswrt/test_config_flow.py @@ -28,6 +28,7 @@ from tests.common import MockConfigEntry HOST = "myrouter.asuswrt.com" IP_ADDRESS = "192.168.1.1" +MAC_ADDR = "a1:b1:c1:d1:e1:f1" SSH_KEY = "1234" CONFIG_DATA = { @@ -39,19 +40,44 @@ CONFIG_DATA = { CONF_MODE: "ap", } +PATCH_GET_HOST = patch( + "homeassistant.components.asuswrt.config_flow.socket.gethostbyname", + return_value=IP_ADDRESS, +) + +PATCH_SETUP_ENTRY = patch( + "homeassistant.components.asuswrt.async_setup_entry", + return_value=True, +) + + +@pytest.fixture(name="mock_unique_id") +def mock_unique_id_fixture(): + """Mock returned unique id.""" + return {} + @pytest.fixture(name="connect") -def mock_controller_connect(): +def mock_controller_connect(mock_unique_id): """Mock a successful connection.""" with patch("homeassistant.components.asuswrt.router.AsusWrt") as service_mock: service_mock.return_value.connection.async_connect = AsyncMock() service_mock.return_value.is_connected = True service_mock.return_value.connection.disconnect = Mock() + service_mock.return_value.async_get_nvram = AsyncMock( + return_value=mock_unique_id + ) yield service_mock -async def test_user(hass, connect): +@pytest.mark.usefixtures("connect") +@pytest.mark.parametrize( + "unique_id", + [{}, {"label_mac": MAC_ADDR}], +) +async def test_user(hass, mock_unique_id, unique_id): """Test user config.""" + mock_unique_id.update(unique_id) flow_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} ) @@ -59,15 +85,10 @@ async def test_user(hass, connect): assert flow_result["step_id"] == "user" # test with all provided - with patch( - "homeassistant.components.asuswrt.async_setup_entry", - return_value=True, - ) as mock_setup_entry, patch( - "homeassistant.components.asuswrt.config_flow.socket.gethostbyname", - return_value=IP_ADDRESS, - ): + with PATCH_GET_HOST, PATCH_SETUP_ENTRY as mock_setup_entry: result = await hass.config_entries.flow.async_configure( - flow_result["flow_id"], user_input=CONFIG_DATA + flow_result["flow_id"], + user_input=CONFIG_DATA, ) await hass.async_block_till_done() @@ -78,10 +99,17 @@ async def test_user(hass, connect): assert len(mock_setup_entry.mock_calls) == 1 -async def test_error_no_password_ssh(hass): - """Test we abort if component is already setup.""" +@pytest.mark.parametrize( + ["config", "error"], + [ + ({CONF_PASSWORD: None}, "pwd_or_ssh"), + ({CONF_SSH_KEY: SSH_KEY}, "pwd_and_ssh"), + ], +) +async def test_error_wrong_password_ssh(hass, config, error): + """Test we abort for wrong password and ssh file combination.""" config_data = CONFIG_DATA.copy() - config_data.pop(CONF_PASSWORD) + config_data.update(config) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True}, @@ -89,36 +117,27 @@ async def test_error_no_password_ssh(hass): ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "pwd_or_ssh"} - - -async def test_error_both_password_ssh(hass): - """Test we abort if component is already setup.""" - config_data = CONFIG_DATA.copy() - config_data[CONF_SSH_KEY] = SSH_KEY - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER, "show_advanced_options": True}, - data=config_data, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "pwd_and_ssh"} + assert result["errors"] == {"base": error} async def test_error_invalid_ssh(hass): - """Test we abort if component is already setup.""" + """Test we abort if invalid ssh file is provided.""" config_data = CONFIG_DATA.copy() config_data.pop(CONF_PASSWORD) config_data[CONF_SSH_KEY] = SSH_KEY - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER, "show_advanced_options": True}, - data=config_data, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "ssh_not_file"} + with patch( + "homeassistant.components.asuswrt.config_flow.os.path.isfile", + return_value=False, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER, "show_advanced_options": True}, + data=config_data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "ssh_not_file"} async def test_error_invalid_host(hass): @@ -137,60 +156,96 @@ async def test_error_invalid_host(hass): assert result["errors"] == {"base": "invalid_host"} -async def test_abort_if_already_setup(hass): - """Test we abort if component is already setup.""" +async def test_abort_if_not_unique_id_setup(hass): + """Test we abort if component without uniqueid is already setup.""" MockConfigEntry( domain=DOMAIN, data=CONFIG_DATA, ).add_to_hass(hass) - with patch( - "homeassistant.components.asuswrt.config_flow.socket.gethostbyname", - return_value=IP_ADDRESS, - ): - # Should fail, same HOST (flow) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=CONFIG_DATA, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "not_unique_id_exist" + + +@pytest.mark.usefixtures("connect") +async def test_update_uniqueid_exist(hass, mock_unique_id): + """Test we update entry if uniqueid is already configured.""" + mock_unique_id.update({"label_mac": MAC_ADDR}) + existing_entry = MockConfigEntry( + domain=DOMAIN, + data={**CONFIG_DATA, CONF_HOST: "10.10.10.10"}, + unique_id=MAC_ADDR, + ) + existing_entry.add_to_hass(hass) + + # test with all provided + with PATCH_GET_HOST, PATCH_SETUP_ENTRY: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER, "show_advanced_options": True}, + data=CONFIG_DATA, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == HOST + assert result["data"] == CONFIG_DATA + prev_entry = hass.config_entries.async_get_entry(existing_entry.entry_id) + assert not prev_entry + + +@pytest.mark.usefixtures("connect") +async def test_abort_invalid_unique_id(hass): + """Test we abort if uniqueid not available.""" + MockConfigEntry( + domain=DOMAIN, + data=CONFIG_DATA, + unique_id=MAC_ADDR, + ).add_to_hass(hass) + + with PATCH_GET_HOST: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_DATA, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "single_instance_allowed" + assert result["reason"] == "invalid_unique_id" -async def test_on_connect_failed(hass): +@pytest.mark.parametrize( + ["side_effect", "error"], + [ + (OSError, "cannot_connect"), + (TypeError, "unknown"), + (None, "cannot_connect"), + ], +) +async def test_on_connect_failed(hass, side_effect, error): """Test when we have errors connecting the router.""" flow_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True}, ) - with patch("homeassistant.components.asuswrt.router.AsusWrt") as asus_wrt: - asus_wrt.return_value.connection.async_connect = AsyncMock() - asus_wrt.return_value.is_connected = False - result = await hass.config_entries.flow.async_configure( - flow_result["flow_id"], user_input=CONFIG_DATA - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "cannot_connect"} - - with patch("homeassistant.components.asuswrt.router.AsusWrt") as asus_wrt: - asus_wrt.return_value.connection.async_connect = AsyncMock(side_effect=OSError) - result = await hass.config_entries.flow.async_configure( - flow_result["flow_id"], user_input=CONFIG_DATA - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "cannot_connect"} - - with patch("homeassistant.components.asuswrt.router.AsusWrt") as asus_wrt: + with PATCH_GET_HOST, patch( + "homeassistant.components.asuswrt.router.AsusWrt" + ) as asus_wrt: asus_wrt.return_value.connection.async_connect = AsyncMock( - side_effect=TypeError + side_effect=side_effect ) + asus_wrt.return_value.is_connected = False + result = await hass.config_entries.flow.async_configure( flow_result["flow_id"], user_input=CONFIG_DATA ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "unknown"} + assert result["errors"] == {"base": error} async def test_options_flow(hass): @@ -202,7 +257,7 @@ async def test_options_flow(hass): ) config_entry.add_to_hass(hass) - with patch("homeassistant.components.asuswrt.async_setup_entry", return_value=True): + with PATCH_SETUP_ENTRY: await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index 3e0c01071c1..483592302bf 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -7,7 +7,7 @@ import pytest from homeassistant.components import device_tracker, sensor from homeassistant.components.asuswrt.const import CONF_INTERFACE, DOMAIN -from homeassistant.components.asuswrt.sensor import DEFAULT_PREFIX +from homeassistant.components.asuswrt.router import DEFAULT_NAME from homeassistant.components.device_tracker.const import CONF_CONSIDER_HOME from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( @@ -39,6 +39,8 @@ CONFIG_DATA = { CONF_MODE: "router", } +MAC_ADDR = "a1:b2:c3:d4:e5:f6" + MOCK_BYTES_TOTAL = [60000000000, 50000000000] MOCK_CURRENT_TRANSFER_RATES = [20000000, 10000000] MOCK_LOAD_AVG = [1.1, 1.2, 1.3] @@ -157,7 +159,7 @@ def mock_controller_connect_sens_fail(): yield service_mock -def _setup_entry(hass): +def _setup_entry(hass, unique_id=None): """Create mock config entry.""" entity_reg = er.async_get(hass) @@ -166,11 +168,11 @@ def _setup_entry(hass): domain=DOMAIN, data=CONFIG_DATA, options={CONF_CONSIDER_HOME: 60}, + unique_id=unique_id, ) # init variable - unique_id = DOMAIN - obj_prefix = slugify(DEFAULT_PREFIX) + obj_prefix = slugify(HOST if unique_id else DEFAULT_NAME) sensor_prefix = f"{sensor.DOMAIN}.{obj_prefix}" # Pre-enable the status sensor @@ -179,7 +181,7 @@ def _setup_entry(hass): entity_reg.async_get_or_create( sensor.DOMAIN, DOMAIN, - f"{unique_id} {DEFAULT_PREFIX} {sensor_name}", + f"{DOMAIN} {unique_id or DEFAULT_NAME} {sensor_name}", suggested_object_id=f"{obj_prefix}_{sensor_id}", disabled_by=None, ) @@ -202,15 +204,20 @@ def _setup_entry(hass): return config_entry, sensor_prefix +@pytest.mark.parametrize( + "entry_unique_id", + [None, MAC_ADDR], +) async def test_sensors( hass, connect, mock_devices, mock_available_temps, create_device_registry_devices, + entry_unique_id, ): """Test creating an AsusWRT sensor.""" - config_entry, sensor_prefix = _setup_entry(hass) + config_entry, sensor_prefix = _setup_entry(hass, entry_unique_id) config_entry.add_to_hass(hass) # initial devices setup From 6e675d6f078088befb2c954d2b9dbd08d78d3119 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 May 2022 14:09:35 -0700 Subject: [PATCH 0181/3516] Fix homekit tests in beta (#71268) --- tests/components/homekit/test_accessories.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 704bb368d64..6d7de6eb696 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -30,6 +30,7 @@ from homeassistant.components.homekit.const import ( MANUFACTURER, SERV_ACCESSORY_INFO, ) +from homeassistant.components.homekit.util import format_version from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, @@ -165,7 +166,7 @@ async def test_home_accessory(hass, hk_driver): serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "light.accessory_that_exceeds_the_maximum_maximum_maximum_maximum" ) - assert hass_version.startswith( + assert format_version(hass_version).startswith( serv.get_characteristic(CHAR_FIRMWARE_REVISION).value ) @@ -217,7 +218,7 @@ async def test_accessory_with_missing_basic_service_info(hass, hk_driver): assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Home Assistant Sensor" assert serv.get_characteristic(CHAR_MODEL).value == "Sensor" assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id - assert hass_version.startswith( + assert format_version(hass_version).startswith( serv.get_characteristic(CHAR_FIRMWARE_REVISION).value ) assert isinstance(acc.to_HAP(), dict) @@ -247,7 +248,7 @@ async def test_accessory_with_hardware_revision(hass, hk_driver): assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Home Assistant Sensor" assert serv.get_characteristic(CHAR_MODEL).value == "Sensor" assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id - assert hass_version.startswith( + assert format_version(hass_version).startswith( serv.get_characteristic(CHAR_FIRMWARE_REVISION).value ) assert serv.get_characteristic(CHAR_HARDWARE_REVISION).value == "1.2.3" @@ -692,7 +693,7 @@ def test_home_bridge(hk_driver): serv = bridge.services[0] # SERV_ACCESSORY_INFO assert serv.display_name == SERV_ACCESSORY_INFO assert serv.get_characteristic(CHAR_NAME).value == BRIDGE_NAME - assert hass_version.startswith( + assert format_version(hass_version).startswith( serv.get_characteristic(CHAR_FIRMWARE_REVISION).value ) assert serv.get_characteristic(CHAR_MANUFACTURER).value == MANUFACTURER From 509dd657b8c452060bcf70b23996c6df61c3f2d1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 4 May 2022 00:22:09 +0000 Subject: [PATCH 0182/3516] [ci skip] Translation update --- .../components/asuswrt/translations/en.json | 3 ++- .../components/asuswrt/translations/fr.json | 2 ++ .../asuswrt/translations/pt-BR.json | 2 ++ .../components/hue/translations/bg.json | 1 + .../components/insteon/translations/bg.json | 4 +++ .../intellifire/translations/bg.json | 14 ++++++++-- .../components/meater/translations/bg.json | 5 ++++ .../components/meater/translations/ca.json | 9 +++++++ .../components/meater/translations/et.json | 9 +++++++ .../components/meater/translations/it.json | 6 +++++ .../components/meater/translations/no.json | 9 +++++++ .../components/qnap_qsw/translations/bg.json | 20 ++++++++++++++ .../components/roon/translations/bg.json | 6 +++++ .../components/senz/translations/bg.json | 3 ++- .../simplisafe/translations/ca.json | 2 +- .../simplisafe/translations/et.json | 2 +- .../components/sql/translations/bg.json | 17 ++++++++++-- .../steam_online/translations/bg.json | 23 ++++++++++++++++ .../components/tautulli/translations/bg.json | 26 +++++++++++++++++++ .../trafikverket_ferry/translations/bg.json | 25 ++++++++++++++++++ .../components/unifi/translations/bg.json | 3 +++ .../components/unifi/translations/ca.json | 5 +++- .../components/unifi/translations/et.json | 5 +++- 23 files changed, 191 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/qnap_qsw/translations/bg.json create mode 100644 homeassistant/components/steam_online/translations/bg.json create mode 100644 homeassistant/components/tautulli/translations/bg.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/bg.json diff --git a/homeassistant/components/asuswrt/translations/en.json b/homeassistant/components/asuswrt/translations/en.json index 494156eae45..6d8dc09aec7 100644 --- a/homeassistant/components/asuswrt/translations/en.json +++ b/homeassistant/components/asuswrt/translations/en.json @@ -2,7 +2,8 @@ "config": { "abort": { "invalid_unique_id": "Impossible to determine a valid unique id for the device", - "not_unique_id_exist": "A device without a valid UniqueID is already configured. Configuration of multiple instance is not possible" + "not_unique_id_exist": "A device without a valid UniqueID is already configured. Configuration of multiple instance is not possible", + "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/asuswrt/translations/fr.json b/homeassistant/components/asuswrt/translations/fr.json index 5c8882d5813..69a7705789e 100644 --- a/homeassistant/components/asuswrt/translations/fr.json +++ b/homeassistant/components/asuswrt/translations/fr.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "invalid_unique_id": "Impossible de d\u00e9terminer un identifiant unique valide pour l'appareil", + "not_unique_id_exist": "Un appareil sans UniqueID valide est d\u00e9j\u00e0 configur\u00e9. La configuration de plusieurs instances n'est pas possible", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { diff --git a/homeassistant/components/asuswrt/translations/pt-BR.json b/homeassistant/components/asuswrt/translations/pt-BR.json index 88999128895..74e48b53ba9 100644 --- a/homeassistant/components/asuswrt/translations/pt-BR.json +++ b/homeassistant/components/asuswrt/translations/pt-BR.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "invalid_unique_id": "Imposs\u00edvel determinar um ID exclusivo v\u00e1lido para o dispositivo", + "not_unique_id_exist": "Um dispositivo sem um ID exclusivo v\u00e1lido j\u00e1 est\u00e1 configurado. A configura\u00e7\u00e3o de v\u00e1rias inst\u00e2ncias n\u00e3o \u00e9 poss\u00edvel", "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { diff --git a/homeassistant/components/hue/translations/bg.json b/homeassistant/components/hue/translations/bg.json index babf0089389..68da5279508 100644 --- a/homeassistant/components/hue/translations/bg.json +++ b/homeassistant/components/hue/translations/bg.json @@ -6,6 +6,7 @@ "already_in_progress": "\u0412 \u043c\u043e\u043c\u0435\u043d\u0442\u0430 \u0442\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f.", "cannot_connect": "\u041d\u0435 \u043c\u043e\u0436\u0430 \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435 \u0441 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f", "discover_timeout": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e \u0435 \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u0431\u0430\u0437\u043e\u0432\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 Philips Hue", + "invalid_host": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u0445\u043e\u0441\u0442", "no_bridges": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u0431\u0430\u0437\u043e\u0432\u0438 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 Philips Hue", "not_hue_bridge": "\u041d\u0435 \u0435 Hue \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" diff --git a/homeassistant/components/insteon/translations/bg.json b/homeassistant/components/insteon/translations/bg.json index 65bf4bad59a..e1adb8657da 100644 --- a/homeassistant/components/insteon/translations/bg.json +++ b/homeassistant/components/insteon/translations/bg.json @@ -8,7 +8,11 @@ "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "select_single": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0435\u0434\u043d\u0430 \u043e\u043f\u0446\u0438\u044f." }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" + }, "hubv1": { "data": { "host": "IP \u0430\u0434\u0440\u0435\u0441", diff --git a/homeassistant/components/intellifire/translations/bg.json b/homeassistant/components/intellifire/translations/bg.json index 1a8dd460097..9926e7bb6a6 100644 --- a/homeassistant/components/intellifire/translations/bg.json +++ b/homeassistant/components/intellifire/translations/bg.json @@ -1,14 +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" + "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": { + "api_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0432\u043b\u0438\u0437\u0430\u043d\u0435", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "iftapi_connect": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435\u0442\u043e \u0441 iftapi.net", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "flow_title": "{serial} ({host})", "step": { + "api_config": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "Email" + } + }, "dhcp_confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {host}\n\u0421\u0435\u0440\u0438\u0435\u043d: {serial}?" }, @@ -21,7 +30,8 @@ "pick_device": { "data": { "host": "\u0425\u043e\u0441\u0442" - } + }, + "title": "\u0418\u0437\u0431\u043e\u0440 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, "user": { "data": { diff --git a/homeassistant/components/meater/translations/bg.json b/homeassistant/components/meater/translations/bg.json index e5396fbc999..cb1a84abf51 100644 --- a/homeassistant/components/meater/translations/bg.json +++ b/homeassistant/components/meater/translations/bg.json @@ -5,6 +5,11 @@ "unknown_auth_error": "\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" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/meater/translations/ca.json b/homeassistant/components/meater/translations/ca.json index 0174767bc52..cd2c626ef8a 100644 --- a/homeassistant/components/meater/translations/ca.json +++ b/homeassistant/components/meater/translations/ca.json @@ -6,11 +6,20 @@ "unknown_auth_error": "Error inesperat" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "Confirma la contrasenya del compte de Meater Cloud {username}." + }, "user": { "data": { "password": "Contrasenya", "username": "Nom d'usuari" }, + "data_description": { + "username": "Nom d'usuari de Meater Cloud, normalment una adre\u00e7a de correu electr\u00f2nic." + }, "description": "Configura el teu compte de Meater Cloud." } } diff --git a/homeassistant/components/meater/translations/et.json b/homeassistant/components/meater/translations/et.json index 55328467d3a..dc5f6f9102f 100644 --- a/homeassistant/components/meater/translations/et.json +++ b/homeassistant/components/meater/translations/et.json @@ -6,11 +6,20 @@ "unknown_auth_error": "Ootamatu t\u00f5rge" }, "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Kinnita Meater Cloudi konto {username} salas\u00f5na." + }, "user": { "data": { "password": "Salas\u00f5na", "username": "Kasutajanimi" }, + "data_description": { + "username": "Meater Cloudi kasutajanimi, tavaliselt e-posti aadress." + }, "description": "Seadista Meater Cloudi konto." } } diff --git a/homeassistant/components/meater/translations/it.json b/homeassistant/components/meater/translations/it.json index fe3bc189ef6..090de20f280 100644 --- a/homeassistant/components/meater/translations/it.json +++ b/homeassistant/components/meater/translations/it.json @@ -6,11 +6,17 @@ "unknown_auth_error": "Errore imprevisto" }, "step": { + "reauth_confirm": { + "description": "Conferma la password per l'account Meater Cloud {username}." + }, "user": { "data": { "password": "Password", "username": "Nome utente" }, + "data_description": { + "username": "Nome utente di Meater Cloud, tipicamente un indirizzo email" + }, "description": "Configura il tuo account Meater Cloud." } } diff --git a/homeassistant/components/meater/translations/no.json b/homeassistant/components/meater/translations/no.json index 60bec4e6864..f97f8ce5f8f 100644 --- a/homeassistant/components/meater/translations/no.json +++ b/homeassistant/components/meater/translations/no.json @@ -6,11 +6,20 @@ "unknown_auth_error": "Uventet feil" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Bekreft passordet for Meater Cloud-kontoen {username} ." + }, "user": { "data": { "password": "Passord", "username": "Brukernavn" }, + "data_description": { + "username": "Meater Cloud brukernavn, vanligvis en e-postadresse." + }, "description": "Sett opp din Meater Cloud-konto." } } diff --git a/homeassistant/components/qnap_qsw/translations/bg.json b/homeassistant/components/qnap_qsw/translations/bg.json new file mode 100644 index 00000000000..33ce2a4028f --- /dev/null +++ b/homeassistant/components/qnap_qsw/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\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" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "url": "URL", + "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/roon/translations/bg.json b/homeassistant/components/roon/translations/bg.json index 36b9a0b4bff..5738012f715 100644 --- a/homeassistant/components/roon/translations/bg.json +++ b/homeassistant/components/roon/translations/bg.json @@ -8,6 +8,12 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "fallback": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/senz/translations/bg.json b/homeassistant/components/senz/translations/bg.json index ddd6040e9cf..916271d14d0 100644 --- a/homeassistant/components/senz/translations/bg.json +++ b/homeassistant/components/senz/translations/bg.json @@ -1,7 +1,8 @@ { "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" + "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430." } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json index 827bd6dceec..82ac0eabbff 100644 --- a/homeassistant/components/simplisafe/translations/ca.json +++ b/homeassistant/components/simplisafe/translations/ca.json @@ -14,7 +14,7 @@ "unknown": "Error inesperat" }, "progress": { - "email_2fa": "Introdueix el codi d'autenticaci\u00f3 de dos factors que s'ha enviat al teu correu." + "email_2fa": "Mira el correu electr\u00f2nic on hauries de trobar l'enlla\u00e7 de verificaci\u00f3 de Simplisafe." }, "step": { "mfa": { diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json index 8931c2250ec..75c908e60c0 100644 --- a/homeassistant/components/simplisafe/translations/et.json +++ b/homeassistant/components/simplisafe/translations/et.json @@ -14,7 +14,7 @@ "unknown": "Tundmatu viga" }, "progress": { - "email_2fa": "Sisesta e-kirjaga saadetud kahefaktoriline autentimiskood." + "email_2fa": "Kontrolli oma meili Simplisafe'i kinnituslingi saamiseks." }, "step": { "mfa": { diff --git a/homeassistant/components/sql/translations/bg.json b/homeassistant/components/sql/translations/bg.json index b6c3577d1d0..5d2b776b0da 100644 --- a/homeassistant/components/sql/translations/bg.json +++ b/homeassistant/components/sql/translations/bg.json @@ -1,9 +1,17 @@ { "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" + }, "step": { "user": { "data": { - "name": "\u0418\u043c\u0435" + "column": "\u041a\u043e\u043b\u043e\u043d\u0430", + "name": "\u0418\u043c\u0435", + "unit_of_measurement": "\u041c\u0435\u0440\u043d\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0430" + }, + "data_description": { + "unit_of_measurement": "\u041c\u0435\u0440\u043d\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0430 (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)" } } } @@ -12,7 +20,12 @@ "step": { "init": { "data": { - "name": "\u0418\u043c\u0435" + "column": "\u041a\u043e\u043b\u043e\u043d\u0430", + "name": "\u0418\u043c\u0435", + "unit_of_measurement": "\u041c\u0435\u0440\u043d\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0430" + }, + "data_description": { + "unit_of_measurement": "\u041c\u0435\u0440\u043d\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0430 (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)" } } } diff --git a/homeassistant/components/steam_online/translations/bg.json b/homeassistant/components/steam_online/translations/bg.json new file mode 100644 index 00000000000..66ae6cc081f --- /dev/null +++ b/homeassistant/components/steam_online/translations/bg.json @@ -0,0 +1,23 @@ +{ + "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", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "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": { + "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/tautulli/translations/bg.json b/homeassistant/components/tautulli/translations/bg.json new file mode 100644 index 00000000000..fb4e836f98d --- /dev/null +++ b/homeassistant/components/tautulli/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", + "single_instance_allowed": "\u0412\u0435\u0447\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\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": { + "reauth_confirm": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/bg.json b/homeassistant/components/trafikverket_ferry/translations/bg.json new file mode 100644 index 00000000000..4e93f090b87 --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/bg.json @@ -0,0 +1,25 @@ +{ + "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", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "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": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447", + "weekday": "\u0420\u0430\u0431\u043e\u0442\u043d\u0438 \u0434\u043d\u0438" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/bg.json b/homeassistant/components/unifi/translations/bg.json index afe2db0cc5b..c1bf205abfe 100644 --- a/homeassistant/components/unifi/translations/bg.json +++ b/homeassistant/components/unifi/translations/bg.json @@ -22,6 +22,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "UniFi \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0435 \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430" + }, "step": { "device_tracker": { "data": { diff --git a/homeassistant/components/unifi/translations/ca.json b/homeassistant/components/unifi/translations/ca.json index 4bb01d82ef6..74129c3c2af 100644 --- a/homeassistant/components/unifi/translations/ca.json +++ b/homeassistant/components/unifi/translations/ca.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El lloc web d'UniFi Network ja est\u00e0 configurat", - "configuration_updated": "S'ha actualitzat la configuraci\u00f3.", + "configuration_updated": "S'ha actualitzat la configuraci\u00f3", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { @@ -26,6 +26,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "La integraci\u00f3 UniFi no est\u00e0 configurada" + }, "step": { "client_control": { "data": { diff --git a/homeassistant/components/unifi/translations/et.json b/homeassistant/components/unifi/translations/et.json index 76d93c95d97..6db9276562e 100644 --- a/homeassistant/components/unifi/translations/et.json +++ b/homeassistant/components/unifi/translations/et.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "UniFi Network on juba seadistatud", - "configuration_updated": "Seaded on v\u00e4rskendatud.", + "configuration_updated": "Seaded on v\u00e4rskendatud", "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { @@ -26,6 +26,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "UniFi sidumine pole seadistatud" + }, "step": { "client_control": { "data": { From c949f010c129bcad73cfa1f02646039ebb733ce1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 May 2022 20:43:58 -0500 Subject: [PATCH 0183/3516] Set entity category for isy auxiliary sensors (#71266) --- homeassistant/components/isy994/sensor.py | 76 ++++++++++++++++++----- 1 file changed, 61 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 2dee990c249..b02050f6f7b 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -3,7 +3,20 @@ from __future__ import annotations from typing import Any, cast -from pyisy.constants import COMMAND_FRIENDLY_NAME, ISY_VALUE_UNKNOWN +from pyisy.constants import ( + COMMAND_FRIENDLY_NAME, + ISY_VALUE_UNKNOWN, + PROP_BATTERY_LEVEL, + PROP_BUSY, + PROP_COMMS_ERROR, + PROP_ENERGY_MODE, + PROP_HEAT_COOL_STATE, + PROP_HUMIDITY, + PROP_ON_LEVEL, + PROP_RAMP_RATE, + PROP_STATUS, + PROP_TEMPERATURE, +) from pyisy.helpers import NodeProperty from pyisy.nodes import Node @@ -11,10 +24,12 @@ from homeassistant.components.sensor import ( DOMAIN as SENSOR, SensorDeviceClass, SensorEntity, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -33,18 +48,34 @@ from .entity import ISYEntity, ISYNodeEntity from .helpers import convert_isy_value_to_hass, migrate_old_unique_ids # Disable general purpose and redundant sensors by default -AUX_DISABLED_BY_DEFAULT = ["ERR", "GV", "CLIEMD", "CLIHCS", "DO", "OL", "RR", "ST"] +AUX_DISABLED_BY_DEFAULT_MATCH = ["GV", "DO"] +AUX_DISABLED_BY_DEFAULT_EXACT = { + PROP_ENERGY_MODE, + PROP_HEAT_COOL_STATE, + PROP_ON_LEVEL, + PROP_RAMP_RATE, + PROP_STATUS, +} +SKIP_AUX_PROPERTIES = {PROP_BUSY, PROP_COMMS_ERROR, PROP_STATUS} ISY_CONTROL_TO_DEVICE_CLASS = { + PROP_BATTERY_LEVEL: SensorDeviceClass.BATTERY, + PROP_HUMIDITY: SensorDeviceClass.HUMIDITY, + PROP_TEMPERATURE: SensorDeviceClass.TEMPERATURE, "BARPRES": SensorDeviceClass.PRESSURE, - "BATLVL": SensorDeviceClass.BATTERY, - "CLIHUM": SensorDeviceClass.HUMIDITY, - "CLITEMP": SensorDeviceClass.TEMPERATURE, "CO2LVL": SensorDeviceClass.CO2, "CV": SensorDeviceClass.VOLTAGE, "LUMIN": SensorDeviceClass.ILLUMINANCE, "PF": SensorDeviceClass.POWER_FACTOR, } +ISY_CONTROL_TO_STATE_CLASS = { + control: SensorStateClass.MEASUREMENT for control in ISY_CONTROL_TO_DEVICE_CLASS +} +ISY_CONTROL_TO_ENTITY_CATEGORY = { + PROP_RAMP_RATE: EntityCategory.CONFIG, + PROP_ON_LEVEL: EntityCategory.CONFIG, + PROP_COMMS_ERROR: EntityCategory.DIAGNOSTIC, +} async def async_setup_entry( @@ -58,13 +89,21 @@ async def async_setup_entry( _LOGGER.debug("Loading %s", node.name) entities.append(ISYSensorEntity(node)) + aux_nodes = set() for node, control in hass_isy_data[ISY994_NODES][SENSOR_AUX]: + aux_nodes.add(node) + if control in SKIP_AUX_PROPERTIES: + continue _LOGGER.debug("Loading %s %s", node.name, node.aux_properties[control]) - enabled_default = not any( - control.startswith(match) for match in AUX_DISABLED_BY_DEFAULT + enabled_default = control not in AUX_DISABLED_BY_DEFAULT_EXACT and not any( + control.startswith(match) for match in AUX_DISABLED_BY_DEFAULT_MATCH ) entities.append(ISYAuxSensorEntity(node, control, enabled_default)) + for node in aux_nodes: + # Any node in SENSOR_AUX can potentially have communication errors + entities.append(ISYAuxSensorEntity(node, PROP_COMMS_ERROR, False)) + for vname, vobj in hass_isy_data[ISY994_VARIABLES]: entities.append(ISYSensorVariableEntity(vname, vobj)) @@ -76,7 +115,7 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): """Representation of an ISY994 sensor device.""" @property - def target(self) -> Node | NodeProperty: + def target(self) -> Node | NodeProperty | None: """Return target for the sensor.""" return self._node @@ -88,6 +127,9 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): @property def raw_unit_of_measurement(self) -> dict | str | None: """Get the raw unit of measurement for the ISY994 sensor device.""" + if self.target is None: + return None + uom = self.target.uom # Backwards compatibility for ISYv4 Firmware: @@ -107,6 +149,9 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): @property def native_value(self) -> float | int | str | None: """Get the state of the ISY994 sensor device.""" + if self.target is None: + return None + if (value := self.target_value) == ISY_VALUE_UNKNOWN: return None @@ -157,21 +202,22 @@ class ISYAuxSensorEntity(ISYSensorEntity): super().__init__(node) self._control = control self._attr_entity_registry_enabled_default = enabled_default + self._attr_entity_category = ISY_CONTROL_TO_ENTITY_CATEGORY.get(control) + self._attr_device_class = ISY_CONTROL_TO_DEVICE_CLASS.get(control) + self._attr_state_class = ISY_CONTROL_TO_STATE_CLASS.get(control) @property - def device_class(self) -> SensorDeviceClass | str | None: - """Return the device class for the sensor.""" - return ISY_CONTROL_TO_DEVICE_CLASS.get(self._control, super().device_class) - - @property - def target(self) -> Node | NodeProperty: + def target(self) -> Node | NodeProperty | None: """Return target for the sensor.""" + if self._control not in self._node.aux_properties: + # Property not yet set (i.e. no errors) + return None return cast(NodeProperty, self._node.aux_properties[self._control]) @property def target_value(self) -> Any: """Return the target value.""" - return self.target.value + return None if self.target is None else self.target.value @property def unique_id(self) -> str | None: From 4408ad82eb26f0b64b775d9ab60c00fd53820067 Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Wed, 4 May 2022 06:14:56 +0200 Subject: [PATCH 0184/3516] Update xknx to 0.21.2 (#71271) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index a000261ec3a..00b4c6cdc5f 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.21.1"], + "requirements": ["xknx==0.21.2"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 438c951d214..8360f4af154 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2448,7 +2448,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.21.1 +xknx==0.21.2 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b012f795c9e..4ca5f854780 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1600,7 +1600,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.21.1 +xknx==0.21.2 # homeassistant.components.bluesound # homeassistant.components.fritz From 2c1d2c323db72d623b58ccbddb54f525f5c627ba Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 4 May 2022 09:22:30 +0200 Subject: [PATCH 0185/3516] Bump pynetgear to 0.10.0 (#71251) --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index a5374f4c315..ae0824c82a5 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.9.4"], + "requirements": ["pynetgear==0.10.0"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 8360f4af154..998f1c8791f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1667,7 +1667,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.9.4 +pynetgear==0.10.0 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ca5f854780..53981018ac1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1119,7 +1119,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.9.4 +pynetgear==0.10.0 # homeassistant.components.nina pynina==0.1.8 From 9e2f0b3af1545ede2e93dca284e8c0635361535b Mon Sep 17 00:00:00 2001 From: Tomasz Date: Wed, 4 May 2022 10:17:43 +0200 Subject: [PATCH 0186/3516] Rename UniqueID to unique id in asuswrt (#71279) * Rename UniqueID to unique id * Update en.json --- homeassistant/components/asuswrt/strings.json | 2 +- homeassistant/components/asuswrt/translations/en.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/asuswrt/strings.json b/homeassistant/components/asuswrt/strings.json index 9023f5aa604..73887fcc4af 100644 --- a/homeassistant/components/asuswrt/strings.json +++ b/homeassistant/components/asuswrt/strings.json @@ -26,7 +26,7 @@ }, "abort": { "invalid_unique_id": "Impossible to determine a valid unique id for the device", - "not_unique_id_exist": "A device without a valid UniqueID is already configured. Configuration of multiple instance is not possible" + "not_unique_id_exist": "A device without a valid unique id is already configured. Configuration of multiple instance is not possible" } }, "options": { diff --git a/homeassistant/components/asuswrt/translations/en.json b/homeassistant/components/asuswrt/translations/en.json index 6d8dc09aec7..4d4ea4d0a30 100644 --- a/homeassistant/components/asuswrt/translations/en.json +++ b/homeassistant/components/asuswrt/translations/en.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Impossible to determine a valid unique id for the device", - "not_unique_id_exist": "A device without a valid UniqueID is already configured. Configuration of multiple instance is not possible", + "not_unique_id_exist": "A device without a valid unique id is already configured. Configuration of multiple instance is not possible", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { @@ -44,4 +44,4 @@ } } } -} \ No newline at end of file +} From ee8eac10c9d8d890acf32c9b02992df5833cfb07 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Wed, 4 May 2022 11:39:55 +0200 Subject: [PATCH 0187/3516] Address late review of AsusWRT unique id PR (#71281) --- homeassistant/components/asuswrt/config_flow.py | 6 +++--- homeassistant/components/asuswrt/strings.json | 2 +- homeassistant/components/asuswrt/translations/en.json | 5 ++--- tests/components/asuswrt/test_config_flow.py | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/asuswrt/config_flow.py b/homeassistant/components/asuswrt/config_flow.py index e1b34cca15b..ec5cccb9a71 100644 --- a/homeassistant/components/asuswrt/config_flow.py +++ b/homeassistant/components/asuswrt/config_flow.py @@ -149,10 +149,10 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle a flow initiated by the user.""" - # if exist one entry without unique ID, we abort config flow + # if there's one entry without unique ID, we abort config flow for unique_id in self._async_current_ids(): if unique_id is None: - return self.async_abort(reason="not_unique_id_exist") + return self.async_abort(reason="no_unique_id") if user_input is None: return self._show_setup_form(user_input) @@ -188,7 +188,7 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="invalid_unique_id") else: _LOGGER.warning( - "This device do not provide a valid Unique ID." + "This device does not provide a valid Unique ID." " Configuration of multiple instance will not be possible" ) diff --git a/homeassistant/components/asuswrt/strings.json b/homeassistant/components/asuswrt/strings.json index 73887fcc4af..bd0c706e74a 100644 --- a/homeassistant/components/asuswrt/strings.json +++ b/homeassistant/components/asuswrt/strings.json @@ -26,7 +26,7 @@ }, "abort": { "invalid_unique_id": "Impossible to determine a valid unique id for the device", - "not_unique_id_exist": "A device without a valid unique id is already configured. Configuration of multiple instance is not possible" + "no_unique_id": "A device without a valid unique id is already configured. Configuration of multiple instance is not possible" } }, "options": { diff --git a/homeassistant/components/asuswrt/translations/en.json b/homeassistant/components/asuswrt/translations/en.json index 4d4ea4d0a30..f93eae80e6d 100644 --- a/homeassistant/components/asuswrt/translations/en.json +++ b/homeassistant/components/asuswrt/translations/en.json @@ -2,8 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Impossible to determine a valid unique id for the device", - "not_unique_id_exist": "A device without a valid unique id is already configured. Configuration of multiple instance is not possible", - "single_instance_allowed": "Already configured. Only a single configuration possible." + "no_unique_id": "A device without a valid unique id is already configured. Configuration of multiple instance is not possible" }, "error": { "cannot_connect": "Failed to connect", @@ -44,4 +43,4 @@ } } } -} +} \ No newline at end of file diff --git a/tests/components/asuswrt/test_config_flow.py b/tests/components/asuswrt/test_config_flow.py index 09c484ff69d..8475bd48f9a 100644 --- a/tests/components/asuswrt/test_config_flow.py +++ b/tests/components/asuswrt/test_config_flow.py @@ -169,7 +169,7 @@ async def test_abort_if_not_unique_id_setup(hass): data=CONFIG_DATA, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "not_unique_id_exist" + assert result["reason"] == "no_unique_id" @pytest.mark.usefixtures("connect") From 6eef3c16f2ed74e3ec1f89a51d88a1ba5c01c6ff Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 4 May 2022 12:14:24 +0200 Subject: [PATCH 0188/3516] Update pylint to 2.13.8 (#71280) --- homeassistant/components/buienradar/util.py | 4 ++-- homeassistant/components/doorbird/__init__.py | 2 +- homeassistant/components/owntracks/messages.py | 2 +- homeassistant/components/smtp/notify.py | 2 +- homeassistant/components/template/cover.py | 2 +- homeassistant/components/vasttrafik/sensor.py | 2 +- homeassistant/components/zwave_js/services.py | 1 - requirements_test.txt | 2 +- 8 files changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py index 3686e2bd3c9..06cd1b32cf5 100644 --- a/homeassistant/components/buienradar/util.py +++ b/homeassistant/components/buienradar/util.py @@ -115,7 +115,7 @@ class BrData: self.load_error_count += 1 threshold_log( self.load_error_count, - "Unable to retrieve json data from Buienradar" "(Msg: %s, status: %s,)", + "Unable to retrieve json data from Buienradar (Msg: %s, status: %s)", content.get(MESSAGE), content.get(STATUS_CODE), ) @@ -135,7 +135,7 @@ class BrData: # unable to get the data threshold_log( self.rain_error_count, - "Unable to retrieve rain data from Buienradar" "(Msg: %s, status: %s)", + "Unable to retrieve rain data from Buienradar (Msg: %s, status: %s)", raincontent.get(MESSAGE), raincontent.get(STATUS_CODE), ) diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 8a2c06c7157..133c7612a89 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -287,7 +287,7 @@ class ConfiguredDoorBird: self.device.change_favorite("http", f"Home Assistant ({event})", url) if not self.webhook_is_registered(url): _LOGGER.warning( - 'Unable to set favorite URL "%s". ' 'Event "%s" will not fire', + 'Unable to set favorite URL "%s". Event "%s" will not fire', url, event, ) diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index b85a37dadf9..4f17c8a5375 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -159,7 +159,7 @@ def encrypt_message(secret, topic, message): if key is None: _LOGGER.warning( - "Unable to encrypt payload because no decryption key known " "for topic %s", + "Unable to encrypt payload because no decryption key known for topic %s", topic, ) return None diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index ef743912bc9..7b8e2dad1ed 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -235,7 +235,7 @@ def _attach_file(atch_name, content_id): attachment = MIMEImage(file_bytes) except TypeError: _LOGGER.warning( - "Attachment %s has an unknown MIME type. " "Falling back to file", + "Attachment %s has an unknown MIME type. Falling back to file", atch_name, ) attachment = MIMEApplication(file_bytes, Name=atch_name) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index e1df61bf4a2..7a20398d0b7 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -242,7 +242,7 @@ class CoverTemplate(TemplateEntity, CoverEntity): if state < 0 or state > 100: self._position = None _LOGGER.error( - "Cover position value must be" " between 0 and 100." " Value was: %.2f", + "Cover position value must be between 0 and 100. Value was: %.2f", state, ) else: diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py index 5e446106312..bec7959f9c8 100644 --- a/homeassistant/components/vasttrafik/sensor.py +++ b/homeassistant/components/vasttrafik/sensor.py @@ -139,7 +139,7 @@ class VasttrafikDepartureSensor(SensorEntity): if not self._departureboard: _LOGGER.debug( - "No departures from departure station %s " "to destination station %s", + "No departures from departure station %s to destination station %s", self._departure["station_name"], self._heading["station_name"] if self._heading else "ANY", ) diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 70099859d39..8577e48e7a3 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -540,7 +540,6 @@ class ZWaveServices: async def async_ping(self, service: ServiceCall) -> None: """Ping node(s).""" - # pylint: disable=no-self-use _LOGGER.warning( "This service is deprecated in favor of the ping button entity. Service " "calls will still work for now but the service will be removed in a " diff --git a/requirements_test.txt b/requirements_test.txt index 744ca7bebcf..0ef6063a1ca 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ freezegun==1.2.1 mock-open==1.4.0 mypy==0.950 pre-commit==2.17.0 -pylint==2.13.7 +pylint==2.13.8 pipdeptree==2.2.1 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 From 309d8d70b1d431078d4e1ef03d7deeadc96be90e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 4 May 2022 14:19:16 +0200 Subject: [PATCH 0189/3516] Update frontend to 20220504.0 (#71284) --- 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 89dd75fa96c..b51219c4f19 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220503.0"], + "requirements": ["home-assistant-frontend==20220504.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fcb63a9426c..a3dec49ddb9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220503.0 +home-assistant-frontend==20220504.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.1 diff --git a/requirements_all.txt b/requirements_all.txt index 998f1c8791f..ce4a5971ecd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220503.0 +home-assistant-frontend==20220504.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 53981018ac1..3772fd7b38a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -580,7 +580,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220503.0 +home-assistant-frontend==20220504.0 # homeassistant.components.home_connect homeconnect==0.7.0 From 03ab9d07a836af6dc3af3ccb745bfc9a025a1c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 4 May 2022 15:47:24 +0200 Subject: [PATCH 0190/3516] Remove more info links for hassio system health (#71286) --- homeassistant/components/hassio/system_health.py | 3 --- tests/components/hassio/test_system_health.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/homeassistant/components/hassio/system_health.py b/homeassistant/components/hassio/system_health.py index 4be94f89218..5ce38b0e121 100644 --- a/homeassistant/components/hassio/system_health.py +++ b/homeassistant/components/hassio/system_health.py @@ -30,7 +30,6 @@ async def system_health_info(hass: HomeAssistant): healthy = { "type": "failed", "error": "Unhealthy", - "more_info": "/hassio/system", } if supervisor_info.get("supported"): @@ -39,7 +38,6 @@ async def system_health_info(hass: HomeAssistant): supported = { "type": "failed", "error": "Unsupported", - "more_info": "/hassio/system", } information = { @@ -63,7 +61,6 @@ async def system_health_info(hass: HomeAssistant): information["version_api"] = system_health.async_check_can_reach_url( hass, f"https://version.home-assistant.io/{info.get('channel')}.json", - "/hassio/system", ) information["installed_addons"] = ", ".join( diff --git a/tests/components/hassio/test_system_health.py b/tests/components/hassio/test_system_health.py index 8fa610b5442..dcf18f7bc13 100644 --- a/tests/components/hassio/test_system_health.py +++ b/tests/components/hassio/test_system_health.py @@ -98,16 +98,13 @@ async def test_hassio_system_health_with_issues(hass, aioclient_mock): assert info["healthy"] == { "error": "Unhealthy", - "more_info": "/hassio/system", "type": "failed", } assert info["supported"] == { "error": "Unsupported", - "more_info": "/hassio/system", "type": "failed", } assert info["version_api"] == { "error": "unreachable", - "more_info": "/hassio/system", "type": "failed", } From fdee8800a0f3de4a45f8d559fa0871def5fdc9f4 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Wed, 4 May 2022 15:51:21 +0200 Subject: [PATCH 0191/3516] Handle empty zeroconf properties in devolo_home_network (#71288) * Handle empty zeroconf properties in devolo_home_network * Change approach * Restore test data --- homeassistant/components/devolo_home_network/manifest.json | 4 +++- homeassistant/generated/zeroconf.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/devolo_home_network/manifest.json b/homeassistant/components/devolo_home_network/manifest.json index a514606a322..445a383ea14 100644 --- a/homeassistant/components/devolo_home_network/manifest.json +++ b/homeassistant/components/devolo_home_network/manifest.json @@ -4,7 +4,9 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/devolo_home_network", "requirements": ["devolo-plc-api==0.7.1"], - "zeroconf": ["_dvl-deviceapi._tcp.local."], + "zeroconf": [ + { "type": "_dvl-deviceapi._tcp.local.", "properties": { "MT": "*" } } + ], "codeowners": ["@2Fake", "@Shutgun"], "quality_scale": "platinum", "iot_class": "local_polling", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 34e24e51fc9..b93d7249211 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -103,7 +103,10 @@ ZEROCONF = { "domain": "devolo_home_control" }, { - "domain": "devolo_home_network" + "domain": "devolo_home_network", + "properties": { + "MT": "*" + } } ], "_easylink._tcp.local.": [ From 1df99badcf04593bb98348c8669e0d066277ae27 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 4 May 2022 15:54:37 +0200 Subject: [PATCH 0192/3516] Allow scripts to turn themselves on (#71289) --- homeassistant/components/script/__init__.py | 9 ++- tests/components/automation/test_init.py | 6 -- tests/components/script/test_init.py | 89 ++++++++++++++++++++- 3 files changed, 95 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 660e7233b33..efad242fbd0 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -42,6 +42,7 @@ from homeassistant.helpers.script import ( CONF_MAX, CONF_MAX_EXCEEDED, Script, + script_stack_cv, ) from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.trace import trace_get, trace_path @@ -398,10 +399,14 @@ class ScriptEntity(ToggleEntity, RestoreEntity): return # Caller does not want to wait for called script to finish so let script run in - # separate Task. However, wait for first state change so we can guarantee that - # it is written to the State Machine before we return. + # separate Task. Make a new empty script stack; scripts are allowed to + # recursively turn themselves on when not waiting. + script_stack_cv.set([]) + self._changed.clear() self.hass.async_create_task(coro) + # Wait for first state change so we can guarantee that + # it is written to the State Machine before we return. await self._changed.wait() async def _async_run(self, variables, context): diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index ccb508c6acc..dbc8f0fc346 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1791,18 +1791,12 @@ async def test_recursive_automation(hass: HomeAssistant, automation_mode, caplog ) service_called = asyncio.Event() - service_called_late = [] async def async_service_handler(service): if service.service == "automation_done": service_called.set() - if service.service == "automation_started_late": - service_called_late.append(service) hass.services.async_register("test", "automation_done", async_service_handler) - hass.services.async_register( - "test", "automation_started_late", async_service_handler - ) hass.bus.async_fire("trigger_automation") await asyncio.wait_for(service_called.wait(), 1) diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 8a5297786f5..ca0cdb97592 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -1,6 +1,7 @@ """The tests for the Script component.""" # pylint: disable=protected-access import asyncio +from datetime import timedelta from unittest.mock import Mock, patch import pytest @@ -33,12 +34,13 @@ from homeassistant.helpers.script import ( SCRIPT_MODE_QUEUED, SCRIPT_MODE_RESTART, SCRIPT_MODE_SINGLE, + _async_stop_scripts_at_shutdown, ) from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_mock_service, mock_restore_cache +from tests.common import async_fire_time_changed, async_mock_service, mock_restore_cache from tests.components.logbook.test_init import MockLazyEventPartialState ENTITY_ID = "script.test" @@ -919,6 +921,91 @@ async def test_recursive_script_indirect(hass, script_mode, warning_msg, caplog) assert warning_msg in caplog.text +@pytest.mark.parametrize( + "script_mode", [SCRIPT_MODE_PARALLEL, SCRIPT_MODE_QUEUED, SCRIPT_MODE_RESTART] +) +async def test_recursive_script_turn_on(hass: HomeAssistant, script_mode, caplog): + """Test script turning itself on. + + - Illegal recursion detection should not be triggered + - Home Assistant should not hang on shut down + - SCRIPT_MODE_SINGLE is not relevant because suca script can't turn itself on + """ + # Make sure we cover all script modes + assert SCRIPT_MODE_CHOICES == [ + SCRIPT_MODE_PARALLEL, + SCRIPT_MODE_QUEUED, + SCRIPT_MODE_RESTART, + SCRIPT_MODE_SINGLE, + ] + stop_scripts_at_shutdown_called = asyncio.Event() + real_stop_scripts_at_shutdown = _async_stop_scripts_at_shutdown + + async def stop_scripts_at_shutdown(*args): + await real_stop_scripts_at_shutdown(*args) + stop_scripts_at_shutdown_called.set() + + with patch( + "homeassistant.helpers.script._async_stop_scripts_at_shutdown", + wraps=stop_scripts_at_shutdown, + ): + assert await async_setup_component( + hass, + script.DOMAIN, + { + script.DOMAIN: { + "script1": { + "mode": script_mode, + "sequence": [ + { + "choose": { + "conditions": { + "condition": "template", + "value_template": "{{ request == 'step_2' }}", + }, + "sequence": {"service": "test.script_done"}, + }, + "default": { + "service": "script.turn_on", + "data": { + "entity_id": "script.script1", + "variables": {"request": "step_2"}, + }, + }, + }, + { + "service": "script.turn_on", + "data": {"entity_id": "script.script1"}, + }, + ], + } + } + }, + ) + + service_called = asyncio.Event() + + async def async_service_handler(service): + if service.service == "script_done": + service_called.set() + + hass.services.async_register("test", "script_done", async_service_handler) + + await hass.services.async_call("script", "script1") + await asyncio.wait_for(service_called.wait(), 1) + + # Trigger 1st stage script shutdown + hass.state = CoreState.stopping + hass.bus.async_fire("homeassistant_stop") + await asyncio.wait_for(stop_scripts_at_shutdown_called.wait(), 1) + + # Trigger 2nd stage script shutdown + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=90)) + await hass.async_block_till_done() + + assert "Disallowed recursion detected" not in caplog.text + + async def test_setup_with_duplicate_scripts( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: From f2d6a06a6a5eb28d045830e3d6da91b6b0755c16 Mon Sep 17 00:00:00 2001 From: Thomas Dietrich Date: Wed, 4 May 2022 15:57:56 +0200 Subject: [PATCH 0193/3516] Add additional characteristics to the statistics integration (#62631) * Improve config checking, add device_class timestamp * Improve warning message --- homeassistant/components/statistics/sensor.py | 44 ++++++++++++++++- tests/components/statistics/test_sensor.py | 48 +++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index bc6acb2732a..ac62b63e8ca 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -64,8 +64,12 @@ STAT_CHANGE = "change" STAT_CHANGE_SAMPLE = "change_sample" STAT_CHANGE_SECOND = "change_second" STAT_COUNT = "count" +STAT_COUNT_BINARY_ON = "count_on" +STAT_COUNT_BINARY_OFF = "count_off" STAT_DATETIME_NEWEST = "datetime_newest" STAT_DATETIME_OLDEST = "datetime_oldest" +STAT_DATETIME_VALUE_MAX = "datetime_value_max" +STAT_DATETIME_VALUE_MIN = "datetime_value_min" STAT_DISTANCE_95P = "distance_95_percent_of_values" STAT_DISTANCE_99P = "distance_99_percent_of_values" STAT_DISTANCE_ABSOLUTE = "distance_absolute" @@ -99,6 +103,8 @@ STATS_NUMERIC_SUPPORT = { STAT_COUNT, STAT_DATETIME_NEWEST, STAT_DATETIME_OLDEST, + STAT_DATETIME_VALUE_MAX, + STAT_DATETIME_VALUE_MIN, STAT_DISTANCE_95P, STAT_DISTANCE_99P, STAT_DISTANCE_ABSOLUTE, @@ -118,18 +124,26 @@ STATS_BINARY_SUPPORT = { STAT_AVERAGE_STEP, STAT_AVERAGE_TIMELESS, STAT_COUNT, + STAT_COUNT_BINARY_ON, + STAT_COUNT_BINARY_OFF, + STAT_DATETIME_NEWEST, + STAT_DATETIME_OLDEST, STAT_MEAN, } STATS_NOT_A_NUMBER = { STAT_DATETIME_NEWEST, STAT_DATETIME_OLDEST, + STAT_DATETIME_VALUE_MAX, + STAT_DATETIME_VALUE_MIN, STAT_QUANTILES, } STATS_DATETIME = { STAT_DATETIME_NEWEST, STAT_DATETIME_OLDEST, + STAT_DATETIME_VALUE_MAX, + STAT_DATETIME_VALUE_MIN, } # Statistics which retain the unit of the source entity @@ -351,7 +365,7 @@ class StatisticsSensor(SensorEntity): except ValueError: self.attributes[STAT_SOURCE_VALUE_VALID] = False _LOGGER.error( - "%s: parsing error, expected number and received %s", + "%s: parsing error. Expected number or binary state, but received '%s'", self.entity_id, new_state.state, ) @@ -370,7 +384,11 @@ class StatisticsSensor(SensorEntity): unit = base_unit elif self._state_characteristic in STATS_NOT_A_NUMBER: unit = None - elif self._state_characteristic == STAT_COUNT: + elif self._state_characteristic in ( + STAT_COUNT, + STAT_COUNT_BINARY_ON, + STAT_COUNT_BINARY_OFF, + ): unit = None elif self._state_characteristic == STAT_VARIANCE: unit = base_unit + "²" @@ -614,6 +632,16 @@ class StatisticsSensor(SensorEntity): return self.ages[0] return None + def _stat_datetime_value_max(self) -> datetime | None: + if len(self.states) > 0: + return self.ages[self.states.index(max(self.states))] + return None + + def _stat_datetime_value_min(self) -> datetime | None: + if len(self.states) > 0: + return self.ages[self.states.index(min(self.states))] + return None + def _stat_distance_95_percent_of_values(self) -> StateType: if len(self.states) >= 2: return 2 * 1.96 * cast(float, self._stat_standard_deviation()) @@ -704,6 +732,18 @@ class StatisticsSensor(SensorEntity): def _stat_binary_count(self) -> StateType: return len(self.states) + def _stat_binary_count_on(self) -> StateType: + return self.states.count(True) + + def _stat_binary_count_off(self) -> StateType: + return self.states.count(False) + + def _stat_binary_datetime_newest(self) -> datetime | None: + return self._stat_datetime_newest() + + def _stat_binary_datetime_oldest(self) -> datetime | None: + return self._stat_datetime_oldest() + def _stat_binary_mean(self) -> StateType: if len(self.states) > 0: return 100.0 / len(self.states) * self.states.count(True) diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index dbcb3b1b8e7..56255216f52 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -687,6 +687,22 @@ async def test_state_characteristics(hass: HomeAssistant): "value_9": (start_datetime + timedelta(minutes=1)).isoformat(), "unit": None, }, + { + "source_sensor_domain": "sensor", + "name": "datetime_value_max", + "value_0": STATE_UNKNOWN, + "value_1": (start_datetime + timedelta(minutes=9)).isoformat(), + "value_9": (start_datetime + timedelta(minutes=2)).isoformat(), + "unit": None, + }, + { + "source_sensor_domain": "sensor", + "name": "datetime_value_min", + "value_0": STATE_UNKNOWN, + "value_1": (start_datetime + timedelta(minutes=9)).isoformat(), + "value_9": (start_datetime + timedelta(minutes=5)).isoformat(), + "unit": None, + }, { "source_sensor_domain": "sensor", "name": "distance_95_percent_of_values", @@ -811,6 +827,38 @@ async def test_state_characteristics(hass: HomeAssistant): "value_9": len(VALUES_BINARY), "unit": None, }, + { + "source_sensor_domain": "binary_sensor", + "name": "count_on", + "value_0": 0, + "value_1": 1, + "value_9": VALUES_BINARY.count("on"), + "unit": None, + }, + { + "source_sensor_domain": "binary_sensor", + "name": "count_off", + "value_0": 0, + "value_1": 0, + "value_9": VALUES_BINARY.count("off"), + "unit": None, + }, + { + "source_sensor_domain": "binary_sensor", + "name": "datetime_newest", + "value_0": STATE_UNKNOWN, + "value_1": (start_datetime + timedelta(minutes=9)).isoformat(), + "value_9": (start_datetime + timedelta(minutes=9)).isoformat(), + "unit": None, + }, + { + "source_sensor_domain": "binary_sensor", + "name": "datetime_oldest", + "value_0": STATE_UNKNOWN, + "value_1": (start_datetime + timedelta(minutes=9)).isoformat(), + "value_9": (start_datetime + timedelta(minutes=1)).isoformat(), + "unit": None, + }, { "source_sensor_domain": "binary_sensor", "name": "mean", From 12bd5fae1a198e8f1c70039c6345fd7d0e21c072 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 4 May 2022 16:13:09 +0200 Subject: [PATCH 0194/3516] Fix meater sensor (#71283) * Fix meater sensor * Cleanup MeaterEntityDescription * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Update sensor.py Co-authored-by: Martin Hjelmare --- homeassistant/components/meater/sensor.py | 29 +++++++++++++---------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index 70582f39d9e..17e8db9e473 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -27,15 +27,18 @@ from .const import DOMAIN @dataclass -class MeaterSensorEntityDescription(SensorEntityDescription): - """Describes meater sensor entity.""" +class MeaterSensorEntityDescriptionMixin: + """Mixin for MeaterSensorEntityDescription.""" - available: Callable[ - [MeaterProbe | None], bool | type[NotImplementedError] - ] = lambda x: NotImplementedError - value: Callable[ - [MeaterProbe], datetime | float | str | None | type[NotImplementedError] - ] = lambda x: NotImplementedError + available: Callable[[MeaterProbe | None], bool] + value: Callable[[MeaterProbe], datetime | float | str | None] + + +@dataclass +class MeaterSensorEntityDescription( + SensorEntityDescription, MeaterSensorEntityDescriptionMixin +): + """Describes meater sensor entity.""" def _elapsed_time_to_timestamp(probe: MeaterProbe) -> datetime | None: @@ -108,7 +111,8 @@ SENSOR_TYPES = ( available=lambda probe: probe is not None and probe.cook is not None, value=lambda probe: probe.cook.peak_temperature if probe.cook else None, ), - # Time since the start of cook in seconds. Default: 0. + # Remaining time in seconds. When unknown/calculating default is used. Default: -1 + # Exposed as a TIMESTAMP sensor where the timestamp is current time + remaining time. MeaterSensorEntityDescription( key="cook_time_remaining", device_class=SensorDeviceClass.TIMESTAMP, @@ -116,7 +120,8 @@ SENSOR_TYPES = ( available=lambda probe: probe is not None and probe.cook is not None, value=_remaining_time_to_timestamp, ), - # Remaining time in seconds. When unknown/calculating default is used. Default: -1 + # Time since the start of cook in seconds. Default: 0. Exposed as a TIMESTAMP sensor + # where the timestamp is current time - elapsed time. MeaterSensorEntityDescription( key="cook_time_elapsed", device_class=SensorDeviceClass.TIMESTAMP, @@ -147,7 +152,7 @@ async def async_setup_entry( # Add entities for temperature probes which we've not yet seen for dev in devices: - if dev in known_probes: + if dev.id in known_probes: continue entities.extend( @@ -156,7 +161,7 @@ async def async_setup_entry( for sensor_description in SENSOR_TYPES ] ) - known_probes.add(dev) + known_probes.add(dev.id) async_add_entities(entities) From 3704b5cf5e535c217ce1fa3c2686d7606ae81b76 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 4 May 2022 16:38:11 +0200 Subject: [PATCH 0195/3516] Bump aioslimproto to 2.0.1 (#71285) --- homeassistant/components/slimproto/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/slimproto/manifest.json b/homeassistant/components/slimproto/manifest.json index eb4ab00f18d..23b3198d7e4 100644 --- a/homeassistant/components/slimproto/manifest.json +++ b/homeassistant/components/slimproto/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "iot_class": "local_push", "documentation": "https://www.home-assistant.io/integrations/slimproto", - "requirements": ["aioslimproto==2.0.0"], + "requirements": ["aioslimproto==2.0.1"], "codeowners": ["@marcelveldt"], "after_dependencies": ["media_source"] } diff --git a/requirements_all.txt b/requirements_all.txt index ce4a5971ecd..16778889248 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -244,7 +244,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.slimproto -aioslimproto==2.0.0 +aioslimproto==2.0.1 # homeassistant.components.steamist aiosteamist==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3772fd7b38a..6d1a1de2cf6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -210,7 +210,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.slimproto -aioslimproto==2.0.0 +aioslimproto==2.0.1 # homeassistant.components.steamist aiosteamist==0.3.1 From d95113c8f26dbdcabe0c801da86b4f68daa89e54 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 4 May 2022 16:56:29 +0200 Subject: [PATCH 0196/3516] Pin grpcio-status to 1.45.0 (#71293) --- homeassistant/package_constraints.txt | 1 + script/gen_requirements_all.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a3dec49ddb9..0a2846a44ad 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -51,6 +51,7 @@ httplib2>=0.19.0 # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. grpcio==1.45.0 +grpcio-status==1.45.0 # libcst >=0.4.0 requires a newer Rust than we currently have available, # thus our wheels builds fail. This pins it to the last working version, diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index ef2b67f3657..1e93b8bba35 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -68,6 +68,7 @@ httplib2>=0.19.0 # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. grpcio==1.45.0 +grpcio-status==1.45.0 # libcst >=0.4.0 requires a newer Rust than we currently have available, # thus our wheels builds fail. This pins it to the last working version, From 08770d015b5413bd56251f7376561148142ba5c2 Mon Sep 17 00:00:00 2001 From: Sean Vig Date: Wed, 4 May 2022 11:15:52 -0400 Subject: [PATCH 0197/3516] Change Amcrest event monitor to non-async (#69640) --- homeassistant/components/amcrest/__init__.py | 128 ++++++++++++------- 1 file changed, 84 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index f2472575259..e3f48263e8b 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -7,6 +7,7 @@ from contextlib import asynccontextmanager, suppress from dataclasses import dataclass from datetime import datetime, timedelta import logging +import threading from typing import Any import aiohttp @@ -30,15 +31,14 @@ from homeassistant.const import ( CONF_USERNAME, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE, - EVENT_HOMEASSISTANT_STOP, HTTP_BASIC_AUTHENTICATION, Platform, ) -from homeassistant.core import Event, HomeAssistant, ServiceCall, callback +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import Unauthorized, UnknownUser from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service import async_extract_entity_ids from homeassistant.helpers.typing import ConfigType @@ -144,10 +144,13 @@ class AmcrestChecker(ApiWrapper): self._hass = hass self._wrap_name = name self._wrap_errors = 0 - self._wrap_lock = asyncio.Lock() + self._wrap_lock = threading.Lock() + self._async_wrap_lock = asyncio.Lock() self._wrap_login_err = False - self._wrap_event_flag = asyncio.Event() + self._wrap_event_flag = threading.Event() self._wrap_event_flag.set() + self._async_wrap_event_flag = asyncio.Event() + self._async_wrap_event_flag.set() self._unsub_recheck: Callable[[], None] | None = None super().__init__( host, @@ -164,12 +167,18 @@ class AmcrestChecker(ApiWrapper): return self._wrap_errors <= MAX_ERRORS and not self._wrap_login_err @property - def available_flag(self) -> asyncio.Event: + def available_flag(self) -> threading.Event: """Return event flag that indicates if camera's API is responding.""" return self._wrap_event_flag + @property + def async_available_flag(self) -> asyncio.Event: + """Return event flag that indicates if camera's API is responding.""" + return self._async_wrap_event_flag + def _start_recovery(self) -> None: - self._wrap_event_flag.clear() + self.available_flag.clear() + self.async_available_flag.clear() async_dispatcher_send( self._hass, service_signal(SERVICE_UPDATE, self._wrap_name) ) @@ -177,9 +186,22 @@ class AmcrestChecker(ApiWrapper): self._hass, self._wrap_test_online, RECHECK_INTERVAL ) + def command(self, *args: Any, **kwargs: Any) -> Any: + """amcrest.ApiWrapper.command wrapper to catch errors.""" + try: + ret = super().command(*args, **kwargs) + except LoginError as ex: + self._handle_offline(ex) + raise + except AmcrestError: + self._handle_error() + raise + self._set_online() + return ret + async def async_command(self, *args: Any, **kwargs: Any) -> httpx.Response: """amcrest.ApiWrapper.command wrapper to catch errors.""" - async with self._command_wrapper(): + async with self._async_command_wrapper(): ret = await super().async_command(*args, **kwargs) return ret @@ -188,35 +210,47 @@ class AmcrestChecker(ApiWrapper): self, *args: Any, **kwargs: Any ) -> AsyncIterator[httpx.Response]: """amcrest.ApiWrapper.command wrapper to catch errors.""" - async with self._command_wrapper(): + async with self._async_command_wrapper(): async with super().async_stream_command(*args, **kwargs) as ret: yield ret @asynccontextmanager - async def _command_wrapper(self) -> AsyncIterator[None]: + async def _async_command_wrapper(self) -> AsyncIterator[None]: try: yield except LoginError as ex: - async with self._wrap_lock: - was_online = self.available - was_login_err = self._wrap_login_err - self._wrap_login_err = True - if not was_login_err: - _LOGGER.error("%s camera offline: Login error: %s", self._wrap_name, ex) - if was_online: - self._start_recovery() + async with self._async_wrap_lock: + self._handle_offline(ex) raise except AmcrestError: - async with self._wrap_lock: - was_online = self.available - errs = self._wrap_errors = self._wrap_errors + 1 - offline = not self.available - _LOGGER.debug("%s camera errs: %i", self._wrap_name, errs) - if was_online and offline: - _LOGGER.error("%s camera offline: Too many errors", self._wrap_name) - self._start_recovery() + async with self._async_wrap_lock: + self._handle_error() raise - async with self._wrap_lock: + async with self._async_wrap_lock: + self._set_online() + + def _handle_offline(self, ex: Exception) -> None: + with self._wrap_lock: + was_online = self.available + was_login_err = self._wrap_login_err + self._wrap_login_err = True + if not was_login_err: + _LOGGER.error("%s camera offline: Login error: %s", self._wrap_name, ex) + if was_online: + self._start_recovery() + + def _handle_error(self) -> None: + with self._wrap_lock: + was_online = self.available + errs = self._wrap_errors = self._wrap_errors + 1 + offline = not self.available + _LOGGER.debug("%s camera errs: %i", self._wrap_name, errs) + if was_online and offline: + _LOGGER.error("%s camera offline: Too many errors", self._wrap_name) + self._start_recovery() + + def _set_online(self) -> None: + with self._wrap_lock: was_offline = not self.available self._wrap_errors = 0 self._wrap_login_err = False @@ -225,7 +259,8 @@ class AmcrestChecker(ApiWrapper): self._unsub_recheck() self._unsub_recheck = None _LOGGER.error("%s camera back online", self._wrap_name) - self._wrap_event_flag.set() + self.available_flag.set() + self.async_available_flag.set() async_dispatcher_send( self._hass, service_signal(SERVICE_UPDATE, self._wrap_name) ) @@ -237,18 +272,18 @@ class AmcrestChecker(ApiWrapper): await self.async_current_time -async def _monitor_events( +def _monitor_events( hass: HomeAssistant, name: str, api: AmcrestChecker, event_codes: set[str], ) -> None: while True: - await api.available_flag.wait() + api.available_flag.wait() try: - async for code, payload in api.async_event_actions("All"): + for code, payload in api.event_actions("All"): event_data = {"camera": name, "event": code, "payload": payload} - hass.bus.async_fire("amcrest", event_data) + hass.bus.fire("amcrest", event_data) if code in event_codes: signal = service_signal(SERVICE_EVENT, name, code) start = any( @@ -256,18 +291,32 @@ async def _monitor_events( for key, val in payload.items() ) _LOGGER.debug("Sending signal: '%s': %s", signal, start) - async_dispatcher_send(hass, signal, start) + dispatcher_send(hass, signal, start) except AmcrestError as error: _LOGGER.warning( "Error while processing events from %s camera: %r", name, error ) +def _start_event_monitor( + hass: HomeAssistant, + name: str, + api: AmcrestChecker, + event_codes: set[str], +) -> None: + thread = threading.Thread( + target=_monitor_events, + name=f"Amcrest {name}", + args=(hass, name, api, event_codes), + daemon=True, + ) + thread.start() + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Amcrest IP Camera component.""" hass.data.setdefault(DATA_AMCREST, {DEVICES: {}, CAMERAS: []}) - monitor_tasks = [] for device in config[DOMAIN]: name: str = device[CONF_NAME] username: str = device[CONF_USERNAME] @@ -328,9 +377,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: and sensor.event_code is not None } - monitor_tasks.append( - asyncio.create_task(_monitor_events(hass, name, api, event_codes)) - ) + _start_event_monitor(hass, name, api, event_codes) if sensors: hass.async_create_task( @@ -354,13 +401,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) ) - @callback - def cancel_monitors(event: Event) -> None: - for monitor_task in monitor_tasks: - monitor_task.cancel() - - hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, cancel_monitors) - if not hass.data[DATA_AMCREST][DEVICES]: return False From 0890f4e51481f44a53e41acc58680f7a408c601e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 May 2022 10:44:20 -0500 Subject: [PATCH 0198/3516] Fix history using pre v25 queries during v26 migration (#71294) --- homeassistant/components/recorder/__init__.py | 5 +++++ homeassistant/components/recorder/history.py | 4 ++-- tests/components/recorder/test_history.py | 6 +++--- tests/components/recorder/test_init.py | 7 +++++++ tests/components/recorder/test_migrate.py | 7 ++++++- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index a0d2f1c8702..1ff7c886c35 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -79,6 +79,7 @@ from .const import ( ) from .executor import DBInterruptibleThreadPoolExecutor from .models import ( + SCHEMA_VERSION, Base, Events, StateAttributes, @@ -634,6 +635,7 @@ class Recorder(threading.Thread): self.entity_filter = entity_filter self.exclude_t = exclude_t + self.schema_version = 0 self._commits_without_expire = 0 self._old_states: dict[str, States] = {} self._state_attributes_ids: LRU = LRU(STATE_ATTRIBUTES_ID_CACHE_SIZE) @@ -973,6 +975,8 @@ class Recorder(threading.Thread): self.hass.add_job(self.async_connection_failed) return + self.schema_version = current_version + schema_is_current = migration.schema_is_current(current_version) if schema_is_current: self._setup_run() @@ -994,6 +998,7 @@ class Recorder(threading.Thread): # with startup which is also cpu intensive if not schema_is_current: if self._migrate_schema_and_setup_run(current_version): + self.schema_version = SCHEMA_VERSION if not self._event_listener: # If the schema migration takes so long that the end # queue watcher safety kicks in because MAX_QUEUE_BACKLOG diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 24fe21f101c..d221ced3a84 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -116,7 +116,7 @@ def query_and_join_attributes( # If we in the process of migrating schema we do # not want to join the state_attributes table as we # do not know if it will be there yet - if recorder.get_instance(hass).migration_in_progress: + if recorder.get_instance(hass).schema_version < 25: return QUERY_STATES_PRE_SCHEMA_25, False # Finally if no migration is in progress and no_attributes # was not requested, we query both attributes columns and @@ -146,7 +146,7 @@ def bake_query_and_join_attributes( # If we in the process of migrating schema we do # not want to join the state_attributes table as we # do not know if it will be there yet - if recorder.get_instance(hass).migration_in_progress: + if recorder.get_instance(hass).schema_version < 25: if include_last_updated: return ( bakery(lambda session: session.query(*QUERY_STATES_PRE_SCHEMA_25)), diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index 8f22447cd8e..c4952d8355b 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -628,7 +628,7 @@ async def test_state_changes_during_period_query_during_migration_to_schema_25( conn.execute(text("drop table state_attributes;")) conn.commit() - with patch.object(instance, "migration_in_progress", True): + with patch.object(instance, "schema_version", 24): no_attributes = True hist = history.state_changes_during_period( hass, start, end, entity_id, no_attributes, include_start_time_state=False @@ -674,7 +674,7 @@ async def test_get_states_query_during_migration_to_schema_25( conn.execute(text("drop table state_attributes;")) conn.commit() - with patch.object(instance, "migration_in_progress", True): + with patch.object(instance, "schema_version", 24): no_attributes = True hist = await _async_get_states( hass, end, [entity_id], no_attributes=no_attributes @@ -723,7 +723,7 @@ async def test_get_states_query_during_migration_to_schema_25_multiple_entities( conn.execute(text("drop table state_attributes;")) conn.commit() - with patch.object(instance, "migration_in_progress", True): + with patch.object(instance, "schema_version", 24): no_attributes = True hist = await _async_get_states( hass, end, entity_ids, no_attributes=no_attributes diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 77bd3992e72..0a0194f4ddf 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -31,6 +31,7 @@ from homeassistant.components.recorder import ( ) from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.models import ( + SCHEMA_VERSION, Events, RecorderRuns, StateAttributes, @@ -438,6 +439,12 @@ def _state_empty_context(hass, entity_id): return state +def test_setup_without_migration(hass_recorder): + """Verify the schema version without a migration.""" + hass = hass_recorder() + assert recorder.get_instance(hass).schema_version == SCHEMA_VERSION + + # pylint: disable=redefined-outer-name,invalid-name def test_saving_state_include_domains(hass_recorder): """Test saving and restoring a state.""" diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 6b963941263..464c3a8b4a3 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -22,7 +22,11 @@ from homeassistant.bootstrap import async_setup_component from homeassistant.components import persistent_notification as pn, recorder from homeassistant.components.recorder import migration, models from homeassistant.components.recorder.const import DATA_INSTANCE -from homeassistant.components.recorder.models import RecorderRuns, States +from homeassistant.components.recorder.models import ( + SCHEMA_VERSION, + RecorderRuns, + States, +) from homeassistant.components.recorder.util import session_scope import homeassistant.util.dt as dt_util @@ -79,6 +83,7 @@ async def test_migration_in_progress(hass): await async_wait_recording_done(hass) assert recorder.util.async_migration_in_progress(hass) is False + assert recorder.get_instance(hass).schema_version == SCHEMA_VERSION async def test_database_migration_failed(hass): From aa0335408aa676ba67caefe937f81a8c9784e6a9 Mon Sep 17 00:00:00 2001 From: Sean Vig Date: Wed, 4 May 2022 11:15:52 -0400 Subject: [PATCH 0199/3516] Change Amcrest event monitor to non-async (#69640) --- homeassistant/components/amcrest/__init__.py | 128 ++++++++++++------- 1 file changed, 84 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index f2472575259..e3f48263e8b 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -7,6 +7,7 @@ from contextlib import asynccontextmanager, suppress from dataclasses import dataclass from datetime import datetime, timedelta import logging +import threading from typing import Any import aiohttp @@ -30,15 +31,14 @@ from homeassistant.const import ( CONF_USERNAME, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE, - EVENT_HOMEASSISTANT_STOP, HTTP_BASIC_AUTHENTICATION, Platform, ) -from homeassistant.core import Event, HomeAssistant, ServiceCall, callback +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import Unauthorized, UnknownUser from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service import async_extract_entity_ids from homeassistant.helpers.typing import ConfigType @@ -144,10 +144,13 @@ class AmcrestChecker(ApiWrapper): self._hass = hass self._wrap_name = name self._wrap_errors = 0 - self._wrap_lock = asyncio.Lock() + self._wrap_lock = threading.Lock() + self._async_wrap_lock = asyncio.Lock() self._wrap_login_err = False - self._wrap_event_flag = asyncio.Event() + self._wrap_event_flag = threading.Event() self._wrap_event_flag.set() + self._async_wrap_event_flag = asyncio.Event() + self._async_wrap_event_flag.set() self._unsub_recheck: Callable[[], None] | None = None super().__init__( host, @@ -164,12 +167,18 @@ class AmcrestChecker(ApiWrapper): return self._wrap_errors <= MAX_ERRORS and not self._wrap_login_err @property - def available_flag(self) -> asyncio.Event: + def available_flag(self) -> threading.Event: """Return event flag that indicates if camera's API is responding.""" return self._wrap_event_flag + @property + def async_available_flag(self) -> asyncio.Event: + """Return event flag that indicates if camera's API is responding.""" + return self._async_wrap_event_flag + def _start_recovery(self) -> None: - self._wrap_event_flag.clear() + self.available_flag.clear() + self.async_available_flag.clear() async_dispatcher_send( self._hass, service_signal(SERVICE_UPDATE, self._wrap_name) ) @@ -177,9 +186,22 @@ class AmcrestChecker(ApiWrapper): self._hass, self._wrap_test_online, RECHECK_INTERVAL ) + def command(self, *args: Any, **kwargs: Any) -> Any: + """amcrest.ApiWrapper.command wrapper to catch errors.""" + try: + ret = super().command(*args, **kwargs) + except LoginError as ex: + self._handle_offline(ex) + raise + except AmcrestError: + self._handle_error() + raise + self._set_online() + return ret + async def async_command(self, *args: Any, **kwargs: Any) -> httpx.Response: """amcrest.ApiWrapper.command wrapper to catch errors.""" - async with self._command_wrapper(): + async with self._async_command_wrapper(): ret = await super().async_command(*args, **kwargs) return ret @@ -188,35 +210,47 @@ class AmcrestChecker(ApiWrapper): self, *args: Any, **kwargs: Any ) -> AsyncIterator[httpx.Response]: """amcrest.ApiWrapper.command wrapper to catch errors.""" - async with self._command_wrapper(): + async with self._async_command_wrapper(): async with super().async_stream_command(*args, **kwargs) as ret: yield ret @asynccontextmanager - async def _command_wrapper(self) -> AsyncIterator[None]: + async def _async_command_wrapper(self) -> AsyncIterator[None]: try: yield except LoginError as ex: - async with self._wrap_lock: - was_online = self.available - was_login_err = self._wrap_login_err - self._wrap_login_err = True - if not was_login_err: - _LOGGER.error("%s camera offline: Login error: %s", self._wrap_name, ex) - if was_online: - self._start_recovery() + async with self._async_wrap_lock: + self._handle_offline(ex) raise except AmcrestError: - async with self._wrap_lock: - was_online = self.available - errs = self._wrap_errors = self._wrap_errors + 1 - offline = not self.available - _LOGGER.debug("%s camera errs: %i", self._wrap_name, errs) - if was_online and offline: - _LOGGER.error("%s camera offline: Too many errors", self._wrap_name) - self._start_recovery() + async with self._async_wrap_lock: + self._handle_error() raise - async with self._wrap_lock: + async with self._async_wrap_lock: + self._set_online() + + def _handle_offline(self, ex: Exception) -> None: + with self._wrap_lock: + was_online = self.available + was_login_err = self._wrap_login_err + self._wrap_login_err = True + if not was_login_err: + _LOGGER.error("%s camera offline: Login error: %s", self._wrap_name, ex) + if was_online: + self._start_recovery() + + def _handle_error(self) -> None: + with self._wrap_lock: + was_online = self.available + errs = self._wrap_errors = self._wrap_errors + 1 + offline = not self.available + _LOGGER.debug("%s camera errs: %i", self._wrap_name, errs) + if was_online and offline: + _LOGGER.error("%s camera offline: Too many errors", self._wrap_name) + self._start_recovery() + + def _set_online(self) -> None: + with self._wrap_lock: was_offline = not self.available self._wrap_errors = 0 self._wrap_login_err = False @@ -225,7 +259,8 @@ class AmcrestChecker(ApiWrapper): self._unsub_recheck() self._unsub_recheck = None _LOGGER.error("%s camera back online", self._wrap_name) - self._wrap_event_flag.set() + self.available_flag.set() + self.async_available_flag.set() async_dispatcher_send( self._hass, service_signal(SERVICE_UPDATE, self._wrap_name) ) @@ -237,18 +272,18 @@ class AmcrestChecker(ApiWrapper): await self.async_current_time -async def _monitor_events( +def _monitor_events( hass: HomeAssistant, name: str, api: AmcrestChecker, event_codes: set[str], ) -> None: while True: - await api.available_flag.wait() + api.available_flag.wait() try: - async for code, payload in api.async_event_actions("All"): + for code, payload in api.event_actions("All"): event_data = {"camera": name, "event": code, "payload": payload} - hass.bus.async_fire("amcrest", event_data) + hass.bus.fire("amcrest", event_data) if code in event_codes: signal = service_signal(SERVICE_EVENT, name, code) start = any( @@ -256,18 +291,32 @@ async def _monitor_events( for key, val in payload.items() ) _LOGGER.debug("Sending signal: '%s': %s", signal, start) - async_dispatcher_send(hass, signal, start) + dispatcher_send(hass, signal, start) except AmcrestError as error: _LOGGER.warning( "Error while processing events from %s camera: %r", name, error ) +def _start_event_monitor( + hass: HomeAssistant, + name: str, + api: AmcrestChecker, + event_codes: set[str], +) -> None: + thread = threading.Thread( + target=_monitor_events, + name=f"Amcrest {name}", + args=(hass, name, api, event_codes), + daemon=True, + ) + thread.start() + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Amcrest IP Camera component.""" hass.data.setdefault(DATA_AMCREST, {DEVICES: {}, CAMERAS: []}) - monitor_tasks = [] for device in config[DOMAIN]: name: str = device[CONF_NAME] username: str = device[CONF_USERNAME] @@ -328,9 +377,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: and sensor.event_code is not None } - monitor_tasks.append( - asyncio.create_task(_monitor_events(hass, name, api, event_codes)) - ) + _start_event_monitor(hass, name, api, event_codes) if sensors: hass.async_create_task( @@ -354,13 +401,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) ) - @callback - def cancel_monitors(event: Event) -> None: - for monitor_task in monitor_tasks: - monitor_task.cancel() - - hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, cancel_monitors) - if not hass.data[DATA_AMCREST][DEVICES]: return False From 444a56341b86531c647fdb658acc1c0f91fabddb Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 4 May 2022 09:22:30 +0200 Subject: [PATCH 0200/3516] Bump pynetgear to 0.10.0 (#71251) --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index a5374f4c315..ae0824c82a5 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.9.4"], + "requirements": ["pynetgear==0.10.0"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 32caea924ed..fd940f1f02c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1664,7 +1664,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.9.4 +pynetgear==0.10.0 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 40e1252b848..f1f64b63f06 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1116,7 +1116,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.9.4 +pynetgear==0.10.0 # homeassistant.components.nina pynina==0.1.8 From d525aad87ee1c7b8bfdaeaa12f51b3e96ac196ec Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 May 2022 14:09:35 -0700 Subject: [PATCH 0201/3516] Fix homekit tests in beta (#71268) --- tests/components/homekit/test_accessories.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 704bb368d64..6d7de6eb696 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -30,6 +30,7 @@ from homeassistant.components.homekit.const import ( MANUFACTURER, SERV_ACCESSORY_INFO, ) +from homeassistant.components.homekit.util import format_version from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, @@ -165,7 +166,7 @@ async def test_home_accessory(hass, hk_driver): serv.get_characteristic(CHAR_SERIAL_NUMBER).value == "light.accessory_that_exceeds_the_maximum_maximum_maximum_maximum" ) - assert hass_version.startswith( + assert format_version(hass_version).startswith( serv.get_characteristic(CHAR_FIRMWARE_REVISION).value ) @@ -217,7 +218,7 @@ async def test_accessory_with_missing_basic_service_info(hass, hk_driver): assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Home Assistant Sensor" assert serv.get_characteristic(CHAR_MODEL).value == "Sensor" assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id - assert hass_version.startswith( + assert format_version(hass_version).startswith( serv.get_characteristic(CHAR_FIRMWARE_REVISION).value ) assert isinstance(acc.to_HAP(), dict) @@ -247,7 +248,7 @@ async def test_accessory_with_hardware_revision(hass, hk_driver): assert serv.get_characteristic(CHAR_MANUFACTURER).value == "Home Assistant Sensor" assert serv.get_characteristic(CHAR_MODEL).value == "Sensor" assert serv.get_characteristic(CHAR_SERIAL_NUMBER).value == entity_id - assert hass_version.startswith( + assert format_version(hass_version).startswith( serv.get_characteristic(CHAR_FIRMWARE_REVISION).value ) assert serv.get_characteristic(CHAR_HARDWARE_REVISION).value == "1.2.3" @@ -692,7 +693,7 @@ def test_home_bridge(hk_driver): serv = bridge.services[0] # SERV_ACCESSORY_INFO assert serv.display_name == SERV_ACCESSORY_INFO assert serv.get_characteristic(CHAR_NAME).value == BRIDGE_NAME - assert hass_version.startswith( + assert format_version(hass_version).startswith( serv.get_characteristic(CHAR_FIRMWARE_REVISION).value ) assert serv.get_characteristic(CHAR_MANUFACTURER).value == MANUFACTURER From bf17bd55fde24d1134aa2444c6d3c5691c3b013b Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Wed, 4 May 2022 06:14:56 +0200 Subject: [PATCH 0202/3516] Update xknx to 0.21.2 (#71271) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index a000261ec3a..00b4c6cdc5f 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.21.1"], + "requirements": ["xknx==0.21.2"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index fd940f1f02c..30114cda0e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2445,7 +2445,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.21.1 +xknx==0.21.2 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1f64b63f06..9d643da34db 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1597,7 +1597,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.21.1 +xknx==0.21.2 # homeassistant.components.bluesound # homeassistant.components.fritz From 44d17a80c33492d484ee47355a78208c8742f80c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 4 May 2022 16:13:09 +0200 Subject: [PATCH 0203/3516] Fix meater sensor (#71283) * Fix meater sensor * Cleanup MeaterEntityDescription * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Update sensor.py Co-authored-by: Martin Hjelmare --- homeassistant/components/meater/sensor.py | 29 +++++++++++++---------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index 70582f39d9e..17e8db9e473 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -27,15 +27,18 @@ from .const import DOMAIN @dataclass -class MeaterSensorEntityDescription(SensorEntityDescription): - """Describes meater sensor entity.""" +class MeaterSensorEntityDescriptionMixin: + """Mixin for MeaterSensorEntityDescription.""" - available: Callable[ - [MeaterProbe | None], bool | type[NotImplementedError] - ] = lambda x: NotImplementedError - value: Callable[ - [MeaterProbe], datetime | float | str | None | type[NotImplementedError] - ] = lambda x: NotImplementedError + available: Callable[[MeaterProbe | None], bool] + value: Callable[[MeaterProbe], datetime | float | str | None] + + +@dataclass +class MeaterSensorEntityDescription( + SensorEntityDescription, MeaterSensorEntityDescriptionMixin +): + """Describes meater sensor entity.""" def _elapsed_time_to_timestamp(probe: MeaterProbe) -> datetime | None: @@ -108,7 +111,8 @@ SENSOR_TYPES = ( available=lambda probe: probe is not None and probe.cook is not None, value=lambda probe: probe.cook.peak_temperature if probe.cook else None, ), - # Time since the start of cook in seconds. Default: 0. + # Remaining time in seconds. When unknown/calculating default is used. Default: -1 + # Exposed as a TIMESTAMP sensor where the timestamp is current time + remaining time. MeaterSensorEntityDescription( key="cook_time_remaining", device_class=SensorDeviceClass.TIMESTAMP, @@ -116,7 +120,8 @@ SENSOR_TYPES = ( available=lambda probe: probe is not None and probe.cook is not None, value=_remaining_time_to_timestamp, ), - # Remaining time in seconds. When unknown/calculating default is used. Default: -1 + # Time since the start of cook in seconds. Default: 0. Exposed as a TIMESTAMP sensor + # where the timestamp is current time - elapsed time. MeaterSensorEntityDescription( key="cook_time_elapsed", device_class=SensorDeviceClass.TIMESTAMP, @@ -147,7 +152,7 @@ async def async_setup_entry( # Add entities for temperature probes which we've not yet seen for dev in devices: - if dev in known_probes: + if dev.id in known_probes: continue entities.extend( @@ -156,7 +161,7 @@ async def async_setup_entry( for sensor_description in SENSOR_TYPES ] ) - known_probes.add(dev) + known_probes.add(dev.id) async_add_entities(entities) From d6e3325ea7fbe024057b3a74872d4abf72bc6d3f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 4 May 2022 14:19:16 +0200 Subject: [PATCH 0204/3516] Update frontend to 20220504.0 (#71284) --- 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 89dd75fa96c..b51219c4f19 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220503.0"], + "requirements": ["home-assistant-frontend==20220504.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fcb63a9426c..a3dec49ddb9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220503.0 +home-assistant-frontend==20220504.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.1 diff --git a/requirements_all.txt b/requirements_all.txt index 30114cda0e4..794797c760d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220503.0 +home-assistant-frontend==20220504.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d643da34db..a817eb3f43d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -580,7 +580,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220503.0 +home-assistant-frontend==20220504.0 # homeassistant.components.home_connect homeconnect==0.7.0 From efa931f6980ce61119f0841702f6eed9a273a5bf Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 4 May 2022 16:38:11 +0200 Subject: [PATCH 0205/3516] Bump aioslimproto to 2.0.1 (#71285) --- homeassistant/components/slimproto/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/slimproto/manifest.json b/homeassistant/components/slimproto/manifest.json index eb4ab00f18d..23b3198d7e4 100644 --- a/homeassistant/components/slimproto/manifest.json +++ b/homeassistant/components/slimproto/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "iot_class": "local_push", "documentation": "https://www.home-assistant.io/integrations/slimproto", - "requirements": ["aioslimproto==2.0.0"], + "requirements": ["aioslimproto==2.0.1"], "codeowners": ["@marcelveldt"], "after_dependencies": ["media_source"] } diff --git a/requirements_all.txt b/requirements_all.txt index 794797c760d..787e4546b0b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -244,7 +244,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.slimproto -aioslimproto==2.0.0 +aioslimproto==2.0.1 # homeassistant.components.steamist aiosteamist==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a817eb3f43d..4f9f4b59b0e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -210,7 +210,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.slimproto -aioslimproto==2.0.0 +aioslimproto==2.0.1 # homeassistant.components.steamist aiosteamist==0.3.1 From deec879a4b4118620e45efa1f83a2c973dfdbe6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 4 May 2022 15:47:24 +0200 Subject: [PATCH 0206/3516] Remove more info links for hassio system health (#71286) --- homeassistant/components/hassio/system_health.py | 3 --- tests/components/hassio/test_system_health.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/homeassistant/components/hassio/system_health.py b/homeassistant/components/hassio/system_health.py index 4be94f89218..5ce38b0e121 100644 --- a/homeassistant/components/hassio/system_health.py +++ b/homeassistant/components/hassio/system_health.py @@ -30,7 +30,6 @@ async def system_health_info(hass: HomeAssistant): healthy = { "type": "failed", "error": "Unhealthy", - "more_info": "/hassio/system", } if supervisor_info.get("supported"): @@ -39,7 +38,6 @@ async def system_health_info(hass: HomeAssistant): supported = { "type": "failed", "error": "Unsupported", - "more_info": "/hassio/system", } information = { @@ -63,7 +61,6 @@ async def system_health_info(hass: HomeAssistant): information["version_api"] = system_health.async_check_can_reach_url( hass, f"https://version.home-assistant.io/{info.get('channel')}.json", - "/hassio/system", ) information["installed_addons"] = ", ".join( diff --git a/tests/components/hassio/test_system_health.py b/tests/components/hassio/test_system_health.py index 8fa610b5442..dcf18f7bc13 100644 --- a/tests/components/hassio/test_system_health.py +++ b/tests/components/hassio/test_system_health.py @@ -98,16 +98,13 @@ async def test_hassio_system_health_with_issues(hass, aioclient_mock): assert info["healthy"] == { "error": "Unhealthy", - "more_info": "/hassio/system", "type": "failed", } assert info["supported"] == { "error": "Unsupported", - "more_info": "/hassio/system", "type": "failed", } assert info["version_api"] == { "error": "unreachable", - "more_info": "/hassio/system", "type": "failed", } From 4e331c331f6fca00d2c9d5c87a5545513c02334a Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Wed, 4 May 2022 15:51:21 +0200 Subject: [PATCH 0207/3516] Handle empty zeroconf properties in devolo_home_network (#71288) * Handle empty zeroconf properties in devolo_home_network * Change approach * Restore test data --- homeassistant/components/devolo_home_network/manifest.json | 4 +++- homeassistant/generated/zeroconf.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/devolo_home_network/manifest.json b/homeassistant/components/devolo_home_network/manifest.json index a514606a322..445a383ea14 100644 --- a/homeassistant/components/devolo_home_network/manifest.json +++ b/homeassistant/components/devolo_home_network/manifest.json @@ -4,7 +4,9 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/devolo_home_network", "requirements": ["devolo-plc-api==0.7.1"], - "zeroconf": ["_dvl-deviceapi._tcp.local."], + "zeroconf": [ + { "type": "_dvl-deviceapi._tcp.local.", "properties": { "MT": "*" } } + ], "codeowners": ["@2Fake", "@Shutgun"], "quality_scale": "platinum", "iot_class": "local_polling", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 34e24e51fc9..b93d7249211 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -103,7 +103,10 @@ ZEROCONF = { "domain": "devolo_home_control" }, { - "domain": "devolo_home_network" + "domain": "devolo_home_network", + "properties": { + "MT": "*" + } } ], "_easylink._tcp.local.": [ From 87a8a82040f6da790633bae61df91d4de9ee0972 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 4 May 2022 15:54:37 +0200 Subject: [PATCH 0208/3516] Allow scripts to turn themselves on (#71289) --- homeassistant/components/script/__init__.py | 9 ++- tests/components/automation/test_init.py | 6 -- tests/components/script/test_init.py | 89 ++++++++++++++++++++- 3 files changed, 95 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 660e7233b33..efad242fbd0 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -42,6 +42,7 @@ from homeassistant.helpers.script import ( CONF_MAX, CONF_MAX_EXCEEDED, Script, + script_stack_cv, ) from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.trace import trace_get, trace_path @@ -398,10 +399,14 @@ class ScriptEntity(ToggleEntity, RestoreEntity): return # Caller does not want to wait for called script to finish so let script run in - # separate Task. However, wait for first state change so we can guarantee that - # it is written to the State Machine before we return. + # separate Task. Make a new empty script stack; scripts are allowed to + # recursively turn themselves on when not waiting. + script_stack_cv.set([]) + self._changed.clear() self.hass.async_create_task(coro) + # Wait for first state change so we can guarantee that + # it is written to the State Machine before we return. await self._changed.wait() async def _async_run(self, variables, context): diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index ccb508c6acc..dbc8f0fc346 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1791,18 +1791,12 @@ async def test_recursive_automation(hass: HomeAssistant, automation_mode, caplog ) service_called = asyncio.Event() - service_called_late = [] async def async_service_handler(service): if service.service == "automation_done": service_called.set() - if service.service == "automation_started_late": - service_called_late.append(service) hass.services.async_register("test", "automation_done", async_service_handler) - hass.services.async_register( - "test", "automation_started_late", async_service_handler - ) hass.bus.async_fire("trigger_automation") await asyncio.wait_for(service_called.wait(), 1) diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 8a5297786f5..ca0cdb97592 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -1,6 +1,7 @@ """The tests for the Script component.""" # pylint: disable=protected-access import asyncio +from datetime import timedelta from unittest.mock import Mock, patch import pytest @@ -33,12 +34,13 @@ from homeassistant.helpers.script import ( SCRIPT_MODE_QUEUED, SCRIPT_MODE_RESTART, SCRIPT_MODE_SINGLE, + _async_stop_scripts_at_shutdown, ) from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_mock_service, mock_restore_cache +from tests.common import async_fire_time_changed, async_mock_service, mock_restore_cache from tests.components.logbook.test_init import MockLazyEventPartialState ENTITY_ID = "script.test" @@ -919,6 +921,91 @@ async def test_recursive_script_indirect(hass, script_mode, warning_msg, caplog) assert warning_msg in caplog.text +@pytest.mark.parametrize( + "script_mode", [SCRIPT_MODE_PARALLEL, SCRIPT_MODE_QUEUED, SCRIPT_MODE_RESTART] +) +async def test_recursive_script_turn_on(hass: HomeAssistant, script_mode, caplog): + """Test script turning itself on. + + - Illegal recursion detection should not be triggered + - Home Assistant should not hang on shut down + - SCRIPT_MODE_SINGLE is not relevant because suca script can't turn itself on + """ + # Make sure we cover all script modes + assert SCRIPT_MODE_CHOICES == [ + SCRIPT_MODE_PARALLEL, + SCRIPT_MODE_QUEUED, + SCRIPT_MODE_RESTART, + SCRIPT_MODE_SINGLE, + ] + stop_scripts_at_shutdown_called = asyncio.Event() + real_stop_scripts_at_shutdown = _async_stop_scripts_at_shutdown + + async def stop_scripts_at_shutdown(*args): + await real_stop_scripts_at_shutdown(*args) + stop_scripts_at_shutdown_called.set() + + with patch( + "homeassistant.helpers.script._async_stop_scripts_at_shutdown", + wraps=stop_scripts_at_shutdown, + ): + assert await async_setup_component( + hass, + script.DOMAIN, + { + script.DOMAIN: { + "script1": { + "mode": script_mode, + "sequence": [ + { + "choose": { + "conditions": { + "condition": "template", + "value_template": "{{ request == 'step_2' }}", + }, + "sequence": {"service": "test.script_done"}, + }, + "default": { + "service": "script.turn_on", + "data": { + "entity_id": "script.script1", + "variables": {"request": "step_2"}, + }, + }, + }, + { + "service": "script.turn_on", + "data": {"entity_id": "script.script1"}, + }, + ], + } + } + }, + ) + + service_called = asyncio.Event() + + async def async_service_handler(service): + if service.service == "script_done": + service_called.set() + + hass.services.async_register("test", "script_done", async_service_handler) + + await hass.services.async_call("script", "script1") + await asyncio.wait_for(service_called.wait(), 1) + + # Trigger 1st stage script shutdown + hass.state = CoreState.stopping + hass.bus.async_fire("homeassistant_stop") + await asyncio.wait_for(stop_scripts_at_shutdown_called.wait(), 1) + + # Trigger 2nd stage script shutdown + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=90)) + await hass.async_block_till_done() + + assert "Disallowed recursion detected" not in caplog.text + + async def test_setup_with_duplicate_scripts( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: From 4e431274ea6f60dfe34b01dd7423598c6eaaca9a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 4 May 2022 16:56:29 +0200 Subject: [PATCH 0209/3516] Pin grpcio-status to 1.45.0 (#71293) --- homeassistant/package_constraints.txt | 1 + script/gen_requirements_all.py | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a3dec49ddb9..0a2846a44ad 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -51,6 +51,7 @@ httplib2>=0.19.0 # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. grpcio==1.45.0 +grpcio-status==1.45.0 # libcst >=0.4.0 requires a newer Rust than we currently have available, # thus our wheels builds fail. This pins it to the last working version, diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index ef2b67f3657..1e93b8bba35 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -68,6 +68,7 @@ httplib2>=0.19.0 # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. grpcio==1.45.0 +grpcio-status==1.45.0 # libcst >=0.4.0 requires a newer Rust than we currently have available, # thus our wheels builds fail. This pins it to the last working version, From d458ac023998f870ae4f249278a0c096e581c75e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 4 May 2022 17:48:27 +0200 Subject: [PATCH 0210/3516] Bumped version to 2022.5.0 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5312d175858..5c7b28e1156 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 7115459d9ff..beab88224e5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.5.0b7 +version = 2022.5.0 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 13ce0a7d6ac7aed9dc50a1bfd7e12339621bc3c5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 May 2022 10:56:50 -0500 Subject: [PATCH 0211/3516] Fix history using pre v25 queries during v26 migration (#71295) --- homeassistant/components/recorder/core.py | 5 +++++ homeassistant/components/recorder/history.py | 4 ++-- tests/components/recorder/test_history.py | 6 +++--- tests/components/recorder/test_init.py | 7 +++++++ tests/components/recorder/test_migrate.py | 7 ++++++- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 7f07a4483cb..84509a1bd53 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -44,6 +44,7 @@ from .const import ( ) from .executor import DBInterruptibleThreadPoolExecutor from .models import ( + SCHEMA_VERSION, Base, EventData, Events, @@ -161,6 +162,7 @@ class Recorder(threading.Thread): self.entity_filter = entity_filter self.exclude_t = exclude_t + self.schema_version = 0 self._commits_without_expire = 0 self._old_states: dict[str, States] = {} self._state_attributes_ids: LRU = LRU(STATE_ATTRIBUTES_ID_CACHE_SIZE) @@ -502,6 +504,8 @@ class Recorder(threading.Thread): self.hass.add_job(self.async_connection_failed) return + self.schema_version = current_version + schema_is_current = migration.schema_is_current(current_version) if schema_is_current: self._setup_run() @@ -523,6 +527,7 @@ class Recorder(threading.Thread): # with startup which is also cpu intensive if not schema_is_current: if self._migrate_schema_and_setup_run(current_version): + self.schema_version = SCHEMA_VERSION if not self._event_listener: # If the schema migration takes so long that the end # queue watcher safety kicks in because MAX_QUEUE_BACKLOG diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 24fe21f101c..d221ced3a84 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -116,7 +116,7 @@ def query_and_join_attributes( # If we in the process of migrating schema we do # not want to join the state_attributes table as we # do not know if it will be there yet - if recorder.get_instance(hass).migration_in_progress: + if recorder.get_instance(hass).schema_version < 25: return QUERY_STATES_PRE_SCHEMA_25, False # Finally if no migration is in progress and no_attributes # was not requested, we query both attributes columns and @@ -146,7 +146,7 @@ def bake_query_and_join_attributes( # If we in the process of migrating schema we do # not want to join the state_attributes table as we # do not know if it will be there yet - if recorder.get_instance(hass).migration_in_progress: + if recorder.get_instance(hass).schema_version < 25: if include_last_updated: return ( bakery(lambda session: session.query(*QUERY_STATES_PRE_SCHEMA_25)), diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index 5e29fb092b1..20b60c3c96d 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -665,7 +665,7 @@ async def test_state_changes_during_period_query_during_migration_to_schema_25( conn.execute(text("drop table state_attributes;")) conn.commit() - with patch.object(instance, "migration_in_progress", True): + with patch.object(instance, "schema_version", 24): no_attributes = True hist = history.state_changes_during_period( hass, start, end, entity_id, no_attributes, include_start_time_state=False @@ -711,7 +711,7 @@ async def test_get_states_query_during_migration_to_schema_25( conn.execute(text("drop table state_attributes;")) conn.commit() - with patch.object(instance, "migration_in_progress", True): + with patch.object(instance, "schema_version", 24): no_attributes = True hist = await _async_get_states( hass, end, [entity_id], no_attributes=no_attributes @@ -760,7 +760,7 @@ async def test_get_states_query_during_migration_to_schema_25_multiple_entities( conn.execute(text("drop table state_attributes;")) conn.commit() - with patch.object(instance, "migration_in_progress", True): + with patch.object(instance, "schema_version", 24): no_attributes = True hist = await _async_get_states( hass, end, entity_ids, no_attributes=no_attributes diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 9bfca76394b..18b74df0189 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -26,6 +26,7 @@ from homeassistant.components.recorder import ( ) from homeassistant.components.recorder.const import DATA_INSTANCE, KEEPALIVE_TIME from homeassistant.components.recorder.models import ( + SCHEMA_VERSION, EventData, Events, RecorderRuns, @@ -459,6 +460,12 @@ def _state_with_context(hass, entity_id): return hass.states.get(entity_id) +def test_setup_without_migration(hass_recorder): + """Verify the schema version without a migration.""" + hass = hass_recorder() + assert recorder.get_instance(hass).schema_version == SCHEMA_VERSION + + # pylint: disable=redefined-outer-name,invalid-name def test_saving_state_include_domains(hass_recorder): """Test saving and restoring a state.""" diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 1b84eb5d171..0a95d174d66 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -22,7 +22,11 @@ from homeassistant.bootstrap import async_setup_component from homeassistant.components import persistent_notification as pn, recorder from homeassistant.components.recorder import migration, models from homeassistant.components.recorder.const import DATA_INSTANCE -from homeassistant.components.recorder.models import RecorderRuns, States +from homeassistant.components.recorder.models import ( + SCHEMA_VERSION, + RecorderRuns, + States, +) from homeassistant.components.recorder.util import session_scope import homeassistant.util.dt as dt_util @@ -80,6 +84,7 @@ async def test_migration_in_progress(hass): await async_wait_recording_done(hass) assert recorder.util.async_migration_in_progress(hass) is False + assert recorder.get_instance(hass).schema_version == SCHEMA_VERSION async def test_database_migration_failed(hass): From eb77f8db8559dba95e5e36c8a9314f89e1ae82b1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 4 May 2022 12:22:50 -0500 Subject: [PATCH 0212/3516] Complete strict typing for recorder (#71274) * Complete strict typing for recorder * update tests * Update tests/components/recorder/test_migrate.py Co-authored-by: Martin Hjelmare * Update tests/components/recorder/test_migrate.py Co-authored-by: Martin Hjelmare * Remove the asserts * remove ignore comments Co-authored-by: Martin Hjelmare --- .strict-typing | 18 +- homeassistant/components/recorder/core.py | 22 +- .../components/recorder/migration.py | 208 ++++++++++-------- homeassistant/components/recorder/purge.py | 4 +- .../components/recorder/statistics.py | 16 +- homeassistant/components/recorder/tasks.py | 2 - mypy.ini | 178 +-------------- tests/components/recorder/test_init.py | 2 + tests/components/recorder/test_migrate.py | 23 +- tests/components/recorder/test_statistics.py | 2 +- 10 files changed, 166 insertions(+), 309 deletions(-) diff --git a/.strict-typing b/.strict-typing index f42bd4a4ab1..67efdfa7953 100644 --- a/.strict-typing +++ b/.strict-typing @@ -177,23 +177,7 @@ homeassistant.components.pure_energie.* homeassistant.components.rainmachine.* homeassistant.components.rdw.* homeassistant.components.recollect_waste.* -homeassistant.components.recorder -homeassistant.components.recorder.const -homeassistant.components.recorder.core -homeassistant.components.recorder.backup -homeassistant.components.recorder.executor -homeassistant.components.recorder.history -homeassistant.components.recorder.models -homeassistant.components.recorder.pool -homeassistant.components.recorder.purge -homeassistant.components.recorder.repack -homeassistant.components.recorder.run_history -homeassistant.components.recorder.services -homeassistant.components.recorder.statistics -homeassistant.components.recorder.system_health -homeassistant.components.recorder.tasks -homeassistant.components.recorder.util -homeassistant.components.recorder.websocket_api +homeassistant.components.recorder.* homeassistant.components.remote.* homeassistant.components.renault.* homeassistant.components.ridwell.* diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 84509a1bd53..af368b909b7 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -171,7 +171,7 @@ class Recorder(threading.Thread): self._pending_event_data: dict[str, EventData] = {} self._pending_expunge: list[States] = [] self.event_session: Session | None = None - self.get_session: Callable[[], Session] | None = None + self._get_session: Callable[[], Session] | None = None self._completed_first_database_setup: bool | None = None self.async_migration_event = asyncio.Event() self.migration_in_progress = False @@ -205,6 +205,12 @@ class Recorder(threading.Thread): """Return if the recorder is recording.""" return self._event_listener is not None + def get_session(self) -> Session: + """Get a new sqlalchemy session.""" + if self._get_session is None: + raise RuntimeError("The database connection has not been established") + return self._get_session() + def queue_task(self, task: RecorderTask) -> None: """Add a task to the recorder queue.""" self._queue.put(task) @@ -459,7 +465,7 @@ class Recorder(threading.Thread): @callback def _async_setup_periodic_tasks(self) -> None: """Prepare periodic tasks.""" - if self.hass.is_stopping or not self.get_session: + if self.hass.is_stopping or not self._get_session: # Home Assistant is shutting down return @@ -591,7 +597,7 @@ class Recorder(threading.Thread): while tries <= self.db_max_retries: try: self._setup_connection() - return migration.get_schema_version(self) + return migration.get_schema_version(self.get_session) except Exception as err: # pylint: disable=broad-except _LOGGER.exception( "Error during connection setup: %s (retrying in %s seconds)", @@ -619,7 +625,9 @@ class Recorder(threading.Thread): self.hass.add_job(self._async_migration_started) try: - migration.migrate_schema(self, current_version) + migration.migrate_schema( + self.hass, self.engine, self.get_session, current_version + ) except exc.DatabaseError as err: if self._handle_database_error(err): return True @@ -896,7 +904,6 @@ class Recorder(threading.Thread): def _open_event_session(self) -> None: """Open the event session.""" - assert self.get_session is not None self.event_session = self.get_session() self.event_session.expire_on_commit = False @@ -1011,7 +1018,7 @@ class Recorder(threading.Thread): sqlalchemy_event.listen(self.engine, "connect", setup_recorder_connection) Base.metadata.create_all(self.engine) - self.get_session = scoped_session(sessionmaker(bind=self.engine, future=True)) + self._get_session = scoped_session(sessionmaker(bind=self.engine, future=True)) _LOGGER.debug("Connected to recorder database") def _close_connection(self) -> None: @@ -1019,11 +1026,10 @@ class Recorder(threading.Thread): assert self.engine is not None self.engine.dispose() self.engine = None - self.get_session = None + self._get_session = None def _setup_run(self) -> None: """Log the start of the current run and schedule any needed jobs.""" - assert self.get_session is not None with session_scope(session=self.get_session()) as session: end_incomplete_runs(session, self.run_history.recording_start) self.run_history.start(session) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 7835f5320b9..b38bb89b5b9 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -1,11 +1,13 @@ """Schema migration helpers.""" +from collections.abc import Callable, Iterable import contextlib from datetime import timedelta import logging -from typing import Any +from typing import cast import sqlalchemy from sqlalchemy import ForeignKeyConstraint, MetaData, Table, func, text +from sqlalchemy.engine import Engine from sqlalchemy.exc import ( DatabaseError, InternalError, @@ -13,9 +15,12 @@ from sqlalchemy.exc import ( ProgrammingError, SQLAlchemyError, ) +from sqlalchemy.orm.session import Session from sqlalchemy.schema import AddConstraint, DropConstraint from sqlalchemy.sql.expression import true +from homeassistant.core import HomeAssistant + from .models import ( SCHEMA_VERSION, TABLE_STATES, @@ -33,7 +38,7 @@ from .util import session_scope _LOGGER = logging.getLogger(__name__) -def raise_if_exception_missing_str(ex, match_substrs): +def raise_if_exception_missing_str(ex: Exception, match_substrs: Iterable[str]) -> None: """Raise an exception if the exception and cause do not contain the match substrs.""" lower_ex_strs = [str(ex).lower(), str(ex.__cause__).lower()] for str_sub in match_substrs: @@ -44,10 +49,9 @@ def raise_if_exception_missing_str(ex, match_substrs): raise ex -def get_schema_version(instance: Any) -> int: +def get_schema_version(session_maker: Callable[[], Session]) -> int: """Get the schema version.""" - assert instance.get_session is not None - with session_scope(session=instance.get_session()) as session: + with session_scope(session=session_maker()) as session: res = ( session.query(SchemaChanges) .order_by(SchemaChanges.change_id.desc()) @@ -61,7 +65,7 @@ def get_schema_version(instance: Any) -> int: "No schema version found. Inspected version: %s", current_version ) - return current_version + return cast(int, current_version) def schema_is_current(current_version: int) -> bool: @@ -69,21 +73,27 @@ def schema_is_current(current_version: int) -> bool: return current_version == SCHEMA_VERSION -def migrate_schema(instance: Any, current_version: int) -> None: +def migrate_schema( + hass: HomeAssistant, + engine: Engine, + session_maker: Callable[[], Session], + current_version: int, +) -> None: """Check if the schema needs to be upgraded.""" - assert instance.get_session is not None _LOGGER.warning("Database is about to upgrade. Schema version: %s", current_version) for version in range(current_version, SCHEMA_VERSION): new_version = version + 1 _LOGGER.info("Upgrading recorder db schema to version %s", new_version) - _apply_update(instance, new_version, current_version) - with session_scope(session=instance.get_session()) as session: + _apply_update(hass, engine, session_maker, new_version, current_version) + with session_scope(session=session_maker()) as session: session.add(SchemaChanges(schema_version=new_version)) _LOGGER.info("Upgrade to version %s done", new_version) -def _create_index(instance, table_name, index_name): +def _create_index( + session_maker: Callable[[], Session], table_name: str, index_name: str +) -> None: """Create an index for the specified table. The index name should match the name given for the index @@ -104,7 +114,7 @@ def _create_index(instance, table_name, index_name): "be patient!", index_name, ) - with session_scope(session=instance.get_session()) as session: + with session_scope(session=session_maker()) as session: try: connection = session.connection() index.create(connection) @@ -117,7 +127,9 @@ def _create_index(instance, table_name, index_name): _LOGGER.debug("Finished creating %s", index_name) -def _drop_index(instance, table_name, index_name): +def _drop_index( + session_maker: Callable[[], Session], table_name: str, index_name: str +) -> None: """Drop an index from a specified table. There is no universal way to do something like `DROP INDEX IF EXISTS` @@ -132,7 +144,7 @@ def _drop_index(instance, table_name, index_name): success = False # Engines like DB2/Oracle - with session_scope(session=instance.get_session()) as session: + with session_scope(session=session_maker()) as session: try: connection = session.connection() connection.execute(text(f"DROP INDEX {index_name}")) @@ -143,7 +155,7 @@ def _drop_index(instance, table_name, index_name): # Engines like SQLite, SQL Server if not success: - with session_scope(session=instance.get_session()) as session: + with session_scope(session=session_maker()) as session: try: connection = session.connection() connection.execute( @@ -160,7 +172,7 @@ def _drop_index(instance, table_name, index_name): if not success: # Engines like MySQL, MS Access - with session_scope(session=instance.get_session()) as session: + with session_scope(session=session_maker()) as session: try: connection = session.connection() connection.execute( @@ -194,7 +206,9 @@ def _drop_index(instance, table_name, index_name): ) -def _add_columns(instance, table_name, columns_def): +def _add_columns( + session_maker: Callable[[], Session], table_name: str, columns_def: list[str] +) -> None: """Add columns to a table.""" _LOGGER.warning( "Adding columns %s to table %s. Note: this can take several " @@ -206,7 +220,7 @@ def _add_columns(instance, table_name, columns_def): columns_def = [f"ADD {col_def}" for col_def in columns_def] - with session_scope(session=instance.get_session()) as session: + with session_scope(session=session_maker()) as session: try: connection = session.connection() connection.execute( @@ -223,7 +237,7 @@ def _add_columns(instance, table_name, columns_def): _LOGGER.info("Unable to use quick column add. Adding 1 by 1") for column_def in columns_def: - with session_scope(session=instance.get_session()) as session: + with session_scope(session=session_maker()) as session: try: connection = session.connection() connection.execute( @@ -242,7 +256,12 @@ def _add_columns(instance, table_name, columns_def): ) -def _modify_columns(instance, engine, table_name, columns_def): +def _modify_columns( + session_maker: Callable[[], Session], + engine: Engine, + table_name: str, + columns_def: list[str], +) -> None: """Modify columns in a table.""" if engine.dialect.name == "sqlite": _LOGGER.debug( @@ -274,7 +293,7 @@ def _modify_columns(instance, engine, table_name, columns_def): else: columns_def = [f"MODIFY {col_def}" for col_def in columns_def] - with session_scope(session=instance.get_session()) as session: + with session_scope(session=session_maker()) as session: try: connection = session.connection() connection.execute( @@ -289,7 +308,7 @@ def _modify_columns(instance, engine, table_name, columns_def): _LOGGER.info("Unable to use quick column modify. Modifying 1 by 1") for column_def in columns_def: - with session_scope(session=instance.get_session()) as session: + with session_scope(session=session_maker()) as session: try: connection = session.connection() connection.execute( @@ -305,7 +324,9 @@ def _modify_columns(instance, engine, table_name, columns_def): ) -def _update_states_table_with_foreign_key_options(instance, engine): +def _update_states_table_with_foreign_key_options( + session_maker: Callable[[], Session], engine: Engine +) -> None: """Add the options to foreign key constraints.""" inspector = sqlalchemy.inspect(engine) alters = [] @@ -333,7 +354,7 @@ def _update_states_table_with_foreign_key_options(instance, engine): ) for alter in alters: - with session_scope(session=instance.get_session()) as session: + with session_scope(session=session_maker()) as session: try: connection = session.connection() connection.execute(DropConstraint(alter["old_fk"])) @@ -346,7 +367,9 @@ def _update_states_table_with_foreign_key_options(instance, engine): ) -def _drop_foreign_key_constraints(instance, engine, table, columns): +def _drop_foreign_key_constraints( + session_maker: Callable[[], Session], engine: Engine, table: str, columns: list[str] +) -> None: """Drop foreign key constraints for a table on specific columns.""" inspector = sqlalchemy.inspect(engine) drops = [] @@ -364,7 +387,7 @@ def _drop_foreign_key_constraints(instance, engine, table, columns): ) for drop in drops: - with session_scope(session=instance.get_session()) as session: + with session_scope(session=session_maker()) as session: try: connection = session.connection() connection.execute(DropConstraint(drop)) @@ -376,19 +399,24 @@ def _drop_foreign_key_constraints(instance, engine, table, columns): ) -def _apply_update(instance, new_version, old_version): # noqa: C901 +def _apply_update( # noqa: C901 + hass: HomeAssistant, + engine: Engine, + session_maker: Callable[[], Session], + new_version: int, + old_version: int, +) -> None: """Perform operations to bring schema up to date.""" - engine = instance.engine dialect = engine.dialect.name big_int = "INTEGER(20)" if dialect == "mysql" else "INTEGER" if new_version == 1: - _create_index(instance, "events", "ix_events_time_fired") + _create_index(session_maker, "events", "ix_events_time_fired") elif new_version == 2: # Create compound start/end index for recorder_runs - _create_index(instance, "recorder_runs", "ix_recorder_runs_start_end") + _create_index(session_maker, "recorder_runs", "ix_recorder_runs_start_end") # Create indexes for states - _create_index(instance, "states", "ix_states_last_updated") + _create_index(session_maker, "states", "ix_states_last_updated") elif new_version == 3: # There used to be a new index here, but it was removed in version 4. pass @@ -398,41 +426,41 @@ def _apply_update(instance, new_version, old_version): # noqa: C901 if old_version == 3: # Remove index that was added in version 3 - _drop_index(instance, "states", "ix_states_created_domain") + _drop_index(session_maker, "states", "ix_states_created_domain") if old_version == 2: # Remove index that was added in version 2 - _drop_index(instance, "states", "ix_states_entity_id_created") + _drop_index(session_maker, "states", "ix_states_entity_id_created") # Remove indexes that were added in version 0 - _drop_index(instance, "states", "states__state_changes") - _drop_index(instance, "states", "states__significant_changes") - _drop_index(instance, "states", "ix_states_entity_id_created") + _drop_index(session_maker, "states", "states__state_changes") + _drop_index(session_maker, "states", "states__significant_changes") + _drop_index(session_maker, "states", "ix_states_entity_id_created") - _create_index(instance, "states", "ix_states_entity_id_last_updated") + _create_index(session_maker, "states", "ix_states_entity_id_last_updated") elif new_version == 5: # Create supporting index for States.event_id foreign key - _create_index(instance, "states", "ix_states_event_id") + _create_index(session_maker, "states", "ix_states_event_id") elif new_version == 6: _add_columns( - instance, + session_maker, "events", ["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"], ) - _create_index(instance, "events", "ix_events_context_id") - _create_index(instance, "events", "ix_events_context_user_id") + _create_index(session_maker, "events", "ix_events_context_id") + _create_index(session_maker, "events", "ix_events_context_user_id") _add_columns( - instance, + session_maker, "states", ["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"], ) - _create_index(instance, "states", "ix_states_context_id") - _create_index(instance, "states", "ix_states_context_user_id") + _create_index(session_maker, "states", "ix_states_context_id") + _create_index(session_maker, "states", "ix_states_context_user_id") elif new_version == 7: - _create_index(instance, "states", "ix_states_entity_id") + _create_index(session_maker, "states", "ix_states_entity_id") elif new_version == 8: - _add_columns(instance, "events", ["context_parent_id CHARACTER(36)"]) - _add_columns(instance, "states", ["old_state_id INTEGER"]) - _create_index(instance, "events", "ix_events_context_parent_id") + _add_columns(session_maker, "events", ["context_parent_id CHARACTER(36)"]) + _add_columns(session_maker, "states", ["old_state_id INTEGER"]) + _create_index(session_maker, "events", "ix_events_context_parent_id") elif new_version == 9: # We now get the context from events with a join # since its always there on state_changed events @@ -443,35 +471,35 @@ def _apply_update(instance, new_version, old_version): # noqa: C901 # sqlalchemy alembic to make that work # # no longer dropping ix_states_context_id since its recreated in 28 - _drop_index(instance, "states", "ix_states_context_user_id") + _drop_index(session_maker, "states", "ix_states_context_user_id") # This index won't be there if they were not running # nightly but we don't treat that as a critical issue - _drop_index(instance, "states", "ix_states_context_parent_id") + _drop_index(session_maker, "states", "ix_states_context_parent_id") # Redundant keys on composite index: # We already have ix_states_entity_id_last_updated - _drop_index(instance, "states", "ix_states_entity_id") - _create_index(instance, "events", "ix_events_event_type_time_fired") - _drop_index(instance, "events", "ix_events_event_type") + _drop_index(session_maker, "states", "ix_states_entity_id") + _create_index(session_maker, "events", "ix_events_event_type_time_fired") + _drop_index(session_maker, "events", "ix_events_event_type") elif new_version == 10: # Now done in step 11 pass elif new_version == 11: - _create_index(instance, "states", "ix_states_old_state_id") - _update_states_table_with_foreign_key_options(instance, engine) + _create_index(session_maker, "states", "ix_states_old_state_id") + _update_states_table_with_foreign_key_options(session_maker, engine) elif new_version == 12: if engine.dialect.name == "mysql": - _modify_columns(instance, engine, "events", ["event_data LONGTEXT"]) - _modify_columns(instance, engine, "states", ["attributes LONGTEXT"]) + _modify_columns(session_maker, engine, "events", ["event_data LONGTEXT"]) + _modify_columns(session_maker, engine, "states", ["attributes LONGTEXT"]) elif new_version == 13: if engine.dialect.name == "mysql": _modify_columns( - instance, + session_maker, engine, "events", ["time_fired DATETIME(6)", "created DATETIME(6)"], ) _modify_columns( - instance, + session_maker, engine, "states", [ @@ -481,12 +509,14 @@ def _apply_update(instance, new_version, old_version): # noqa: C901 ], ) elif new_version == 14: - _modify_columns(instance, engine, "events", ["event_type VARCHAR(64)"]) + _modify_columns(session_maker, engine, "events", ["event_type VARCHAR(64)"]) elif new_version == 15: # This dropped the statistics table, done again in version 18. pass elif new_version == 16: - _drop_foreign_key_constraints(instance, engine, TABLE_STATES, ["old_state_id"]) + _drop_foreign_key_constraints( + session_maker, engine, TABLE_STATES, ["old_state_id"] + ) elif new_version == 17: # This dropped the statistics table, done again in version 18. pass @@ -511,13 +541,13 @@ def _apply_update(instance, new_version, old_version): # noqa: C901 elif new_version == 19: # This adds the statistic runs table, insert a fake run to prevent duplicating # statistics. - with session_scope(session=instance.get_session()) as session: + with session_scope(session=session_maker()) as session: session.add(StatisticsRuns(start=get_start_time())) elif new_version == 20: # This changed the precision of statistics from float to double if engine.dialect.name in ["mysql", "postgresql"]: _modify_columns( - instance, + session_maker, engine, "statistics", [ @@ -539,7 +569,7 @@ def _apply_update(instance, new_version, old_version): # noqa: C901 table, ) with contextlib.suppress(SQLAlchemyError): - with session_scope(session=instance.get_session()) as session: + with session_scope(session=session_maker()) as session: connection = session.connection() connection.execute( # Using LOCK=EXCLUSIVE to prevent the database from corrupting @@ -574,7 +604,7 @@ def _apply_update(instance, new_version, old_version): # noqa: C901 # Block 5-minute statistics for one hour from the last run, or it will overlap # with existing hourly statistics. Don't block on a database with no existing # statistics. - with session_scope(session=instance.get_session()) as session: + with session_scope(session=session_maker()) as session: if session.query(Statistics.id).count() and ( last_run_string := session.query( func.max(StatisticsRuns.start) @@ -590,7 +620,7 @@ def _apply_update(instance, new_version, old_version): # noqa: C901 # When querying the database, be careful to only explicitly query for columns # which were present in schema version 21. If querying the table, SQLAlchemy # will refer to future columns. - with session_scope(session=instance.get_session()) as session: + with session_scope(session=session_maker()) as session: for sum_statistic in session.query(StatisticsMeta.id).filter_by( has_sum=true() ): @@ -617,48 +647,52 @@ def _apply_update(instance, new_version, old_version): # noqa: C901 ) elif new_version == 23: # Add name column to StatisticsMeta - _add_columns(instance, "statistics_meta", ["name VARCHAR(255)"]) + _add_columns(session_maker, "statistics_meta", ["name VARCHAR(255)"]) elif new_version == 24: # Recreate statistics indices to block duplicated statistics - _drop_index(instance, "statistics", "ix_statistics_statistic_id_start") + _drop_index(session_maker, "statistics", "ix_statistics_statistic_id_start") _drop_index( - instance, + session_maker, "statistics_short_term", "ix_statistics_short_term_statistic_id_start", ) try: - _create_index(instance, "statistics", "ix_statistics_statistic_id_start") _create_index( - instance, + session_maker, "statistics", "ix_statistics_statistic_id_start" + ) + _create_index( + session_maker, "statistics_short_term", "ix_statistics_short_term_statistic_id_start", ) except DatabaseError: # There may be duplicated statistics entries, delete duplicated statistics # and try again - with session_scope(session=instance.get_session()) as session: - delete_duplicates(instance, session) - _create_index(instance, "statistics", "ix_statistics_statistic_id_start") + with session_scope(session=session_maker()) as session: + delete_duplicates(hass, session) _create_index( - instance, + session_maker, "statistics", "ix_statistics_statistic_id_start" + ) + _create_index( + session_maker, "statistics_short_term", "ix_statistics_short_term_statistic_id_start", ) elif new_version == 25: - _add_columns(instance, "states", [f"attributes_id {big_int}"]) - _create_index(instance, "states", "ix_states_attributes_id") + _add_columns(session_maker, "states", [f"attributes_id {big_int}"]) + _create_index(session_maker, "states", "ix_states_attributes_id") elif new_version == 26: - _create_index(instance, "statistics_runs", "ix_statistics_runs_start") + _create_index(session_maker, "statistics_runs", "ix_statistics_runs_start") elif new_version == 27: - _add_columns(instance, "events", [f"data_id {big_int}"]) - _create_index(instance, "events", "ix_events_data_id") + _add_columns(session_maker, "events", [f"data_id {big_int}"]) + _create_index(session_maker, "events", "ix_events_data_id") elif new_version == 28: - _add_columns(instance, "events", ["origin_idx INTEGER"]) + _add_columns(session_maker, "events", ["origin_idx INTEGER"]) # We never use the user_id or parent_id index - _drop_index(instance, "events", "ix_events_context_user_id") - _drop_index(instance, "events", "ix_events_context_parent_id") + _drop_index(session_maker, "events", "ix_events_context_user_id") + _drop_index(session_maker, "events", "ix_events_context_parent_id") _add_columns( - instance, + session_maker, "states", [ "origin_idx INTEGER", @@ -667,14 +701,14 @@ def _apply_update(instance, new_version, old_version): # noqa: C901 "context_parent_id VARCHAR(36)", ], ) - _create_index(instance, "states", "ix_states_context_id") + _create_index(session_maker, "states", "ix_states_context_id") # Once there are no longer any state_changed events # in the events table we can drop the index on states.event_id else: raise ValueError(f"No schema migration defined for version {new_version}") -def _inspect_schema_version(session): +def _inspect_schema_version(session: Session) -> int: """Determine the schema version by inspecting the db structure. When the schema version is not present in the db, either db was just @@ -696,4 +730,4 @@ def _inspect_schema_version(session): # Version 1 schema changes not found, this db needs to be migrated. current_version = SchemaChanges(schema_version=0) session.add(current_version) - return current_version.schema_version + return cast(int, current_version.schema_version) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 3a0e2e6e141..b2547d13e45 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -47,7 +47,7 @@ def purge_old_data( ) using_sqlite = instance.using_sqlite() - with session_scope(session=instance.get_session()) as session: # type: ignore[misc] + with session_scope(session=instance.get_session()) as session: # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record ( event_ids, @@ -515,7 +515,7 @@ def _purge_filtered_events( def purge_entity_data(instance: Recorder, entity_filter: Callable[[str], bool]) -> bool: """Purge states and events of specified entities.""" using_sqlite = instance.using_sqlite() - with session_scope(session=instance.get_session()) as session: # type: ignore[misc] + with session_scope(session=instance.get_session()) as session: selected_entity_ids: list[str] = [ entity_id for (entity_id,) in session.query(distinct(States.entity_id)).all() diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 1c993b32bb6..9104fb7e234 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -377,7 +377,7 @@ def _delete_duplicates_from_table( return (total_deleted_rows, all_non_identical_duplicates) -def delete_duplicates(instance: Recorder, session: Session) -> None: +def delete_duplicates(hass: HomeAssistant, session: Session) -> None: """Identify and delete duplicated statistics. A backup will be made of duplicated statistics before it is deleted. @@ -391,7 +391,7 @@ def delete_duplicates(instance: Recorder, session: Session) -> None: if non_identical_duplicates: isotime = dt_util.utcnow().isoformat() backup_file_name = f"deleted_statistics.{isotime}.json" - backup_path = instance.hass.config.path(STORAGE_DIR, backup_file_name) + backup_path = hass.config.path(STORAGE_DIR, backup_file_name) os.makedirs(os.path.dirname(backup_path), exist_ok=True) with open(backup_path, "w", encoding="utf8") as backup_file: @@ -551,7 +551,7 @@ def compile_statistics(instance: Recorder, start: datetime) -> bool: end = start + timedelta(minutes=5) # Return if we already have 5-minute statistics for the requested period - with session_scope(session=instance.get_session()) as session: # type: ignore[misc] + with session_scope(session=instance.get_session()) as session: if session.query(StatisticsRuns).filter_by(start=start).first(): _LOGGER.debug("Statistics already compiled for %s-%s", start, end) return True @@ -578,7 +578,7 @@ def compile_statistics(instance: Recorder, start: datetime) -> bool: # Insert collected statistics in the database with session_scope( - session=instance.get_session(), # type: ignore[misc] + session=instance.get_session(), exception_filter=_filter_unique_constraint_integrity_error(instance), ) as session: for stats in platform_stats: @@ -768,7 +768,7 @@ def _configured_unit(unit: str | None, units: UnitSystem) -> str | None: def clear_statistics(instance: Recorder, statistic_ids: list[str]) -> None: """Clear statistics for a list of statistic_ids.""" - with session_scope(session=instance.get_session()) as session: # type: ignore[misc] + with session_scope(session=instance.get_session()) as session: session.query(StatisticsMeta).filter( StatisticsMeta.statistic_id.in_(statistic_ids) ).delete(synchronize_session=False) @@ -778,7 +778,7 @@ def update_statistics_metadata( instance: Recorder, statistic_id: str, unit_of_measurement: str | None ) -> None: """Update statistics metadata for a statistic_id.""" - with session_scope(session=instance.get_session()) as session: # type: ignore[misc] + with session_scope(session=instance.get_session()) as session: session.query(StatisticsMeta).filter( StatisticsMeta.statistic_id == statistic_id ).update({StatisticsMeta.unit_of_measurement: unit_of_measurement}) @@ -1376,7 +1376,7 @@ def add_external_statistics( """Process an add_external_statistics job.""" with session_scope( - session=instance.get_session(), # type: ignore[misc] + session=instance.get_session(), exception_filter=_filter_unique_constraint_integrity_error(instance), ) as session: old_metadata_dict = get_metadata_with_session( @@ -1403,7 +1403,7 @@ def adjust_statistics( ) -> bool: """Process an add_statistics job.""" - with session_scope(session=instance.get_session()) as session: # type: ignore[misc] + with session_scope(session=instance.get_session()) as session: metadata = get_metadata_with_session( instance.hass, session, statistic_ids=(statistic_id,) ) diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index bed49e36f16..e12526b316a 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -65,8 +65,6 @@ class PurgeTask(RecorderTask): def run(self, instance: Recorder) -> None: """Purge the database.""" - assert instance.get_session is not None - if purge.purge_old_data( instance, self.purge_before, self.repack, self.apply_filter ): diff --git a/mypy.ini b/mypy.ini index 3d97a716955..81677b8d8ff 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1710,183 +1710,7 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.recorder] -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.recorder.const] -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.recorder.core] -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.recorder.backup] -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.recorder.executor] -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.recorder.history] -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.recorder.models] -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.recorder.pool] -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.recorder.purge] -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.recorder.repack] -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.recorder.run_history] -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.recorder.services] -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.recorder.statistics] -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.recorder.system_health] -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.recorder.tasks] -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.recorder.util] -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.recorder.websocket_api] +[mypy-homeassistant.components.recorder.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 18b74df0189..17287151bc1 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -138,6 +138,8 @@ async def test_shutdown_closes_connections(hass, recorder_mock): await hass.async_block_till_done() assert len(pool.shutdown.mock_calls) == 1 + with pytest.raises(RuntimeError): + assert instance.get_session() async def test_state_gets_saved_when_set_before_start_event( diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 0a95d174d66..fcc35938088 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -60,9 +60,12 @@ async def test_schema_update_calls(hass): await async_wait_recording_done(hass) assert recorder.util.async_migration_in_progress(hass) is False + instance = recorder.get_instance(hass) + engine = instance.engine + session_maker = instance.get_session update.assert_has_calls( [ - call(hass.data[DATA_INSTANCE], version + 1, 0) + call(hass, engine, session_maker, version + 1, 0) for version in range(0, models.SCHEMA_VERSION) ] ) @@ -327,10 +330,10 @@ async def test_schema_migrate(hass, start_version): assert recorder.util.async_migration_in_progress(hass) is not True -def test_invalid_update(): +def test_invalid_update(hass): """Test that an invalid new version raises an exception.""" with pytest.raises(ValueError): - migration._apply_update(Mock(), -1, 0) + migration._apply_update(hass, Mock(), Mock(), -1, 0) @pytest.mark.parametrize( @@ -351,7 +354,9 @@ def test_modify_column(engine_type, substr): instance.get_session = Mock(return_value=session) engine = Mock() engine.dialect.name = engine_type - migration._modify_columns(instance, engine, "events", ["event_type VARCHAR(64)"]) + migration._modify_columns( + instance.get_session, engine, "events", ["event_type VARCHAR(64)"] + ) if substr: assert substr in connection.execute.call_args[0][0].text else: @@ -365,8 +370,12 @@ def test_forgiving_add_column(): session.execute(text("CREATE TABLE hello (id int)")) instance = Mock() instance.get_session = Mock(return_value=session) - migration._add_columns(instance, "hello", ["context_id CHARACTER(36)"]) - migration._add_columns(instance, "hello", ["context_id CHARACTER(36)"]) + migration._add_columns( + instance.get_session, "hello", ["context_id CHARACTER(36)"] + ) + migration._add_columns( + instance.get_session, "hello", ["context_id CHARACTER(36)"] + ) def test_forgiving_add_index(): @@ -376,7 +385,7 @@ def test_forgiving_add_index(): with Session(engine) as session: instance = Mock() instance.get_session = Mock(return_value=session) - migration._create_index(instance, "states", "ix_states_context_id") + migration._create_index(instance.get_session, "states", "ix_states_context_id") @pytest.mark.parametrize( diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index dc13f2abb6a..765364a7487 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -740,7 +740,7 @@ def test_delete_duplicates_no_duplicates(hass_recorder, caplog): hass = hass_recorder() wait_recording_done(hass) with session_scope(hass=hass) as session: - delete_duplicates(hass.data[DATA_INSTANCE], session) + delete_duplicates(hass, session) assert "duplicated statistics rows" not in caplog.text assert "Found non identical" not in caplog.text assert "Found duplicated" not in caplog.text From 2b3fc97020460705a9c9164bd9b9e3fccb382d1a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 5 May 2022 00:22:23 +0000 Subject: [PATCH 0213/3516] [ci skip] Translation update --- .../application_credentials/translations/id.json | 3 +++ .../application_credentials/translations/nl.json | 3 +++ .../components/asuswrt/translations/ca.json | 3 +++ .../components/asuswrt/translations/de.json | 3 +++ .../components/asuswrt/translations/el.json | 3 +++ .../components/asuswrt/translations/en.json | 4 +++- .../components/asuswrt/translations/et.json | 3 +++ .../components/asuswrt/translations/fr.json | 3 ++- .../components/asuswrt/translations/hu.json | 2 ++ .../components/asuswrt/translations/id.json | 3 +++ .../components/asuswrt/translations/nl.json | 2 ++ .../components/asuswrt/translations/no.json | 2 ++ .../components/asuswrt/translations/pl.json | 2 ++ .../components/asuswrt/translations/pt-BR.json | 1 + .../components/asuswrt/translations/ru.json | 3 +++ .../components/asuswrt/translations/zh-Hant.json | 2 ++ .../components/deconz/translations/id.json | 1 + .../components/isy994/translations/id.json | 13 +++++++++++-- .../components/meater/translations/id.json | 9 +++++++++ .../components/meater/translations/nl.json | 6 +++++- homeassistant/components/ps4/translations/de.json | 2 +- .../components/recorder/translations/id.json | 8 ++++++++ .../components/recorder/translations/pt-BR.json | 4 ++-- homeassistant/components/senz/translations/bg.json | 5 +++++ homeassistant/components/senz/translations/de.json | 6 +++--- .../components/simplisafe/translations/bg.json | 3 +++ .../components/simplisafe/translations/id.json | 4 ++++ .../components/simplisafe/translations/nl.json | 2 +- .../components/smappee/translations/de.json | 2 +- homeassistant/components/sql/translations/id.json | 4 ++++ .../components/steam_online/translations/id.json | 4 ++-- .../trafikverket_ferry/translations/bg.json | 1 + homeassistant/components/unifi/translations/id.json | 5 ++++- 33 files changed, 105 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/application_credentials/translations/id.json create mode 100644 homeassistant/components/application_credentials/translations/nl.json create mode 100644 homeassistant/components/recorder/translations/id.json diff --git a/homeassistant/components/application_credentials/translations/id.json b/homeassistant/components/application_credentials/translations/id.json new file mode 100644 index 00000000000..b4ce4f295fd --- /dev/null +++ b/homeassistant/components/application_credentials/translations/id.json @@ -0,0 +1,3 @@ +{ + "title": "Kredensial Aplikasi" +} \ No newline at end of file diff --git a/homeassistant/components/application_credentials/translations/nl.json b/homeassistant/components/application_credentials/translations/nl.json new file mode 100644 index 00000000000..c186d95ce27 --- /dev/null +++ b/homeassistant/components/application_credentials/translations/nl.json @@ -0,0 +1,3 @@ +{ + "title": "Applicatiegegevens" +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/ca.json b/homeassistant/components/asuswrt/translations/ca.json index 9149cd1ae7a..93821419fc4 100644 --- a/homeassistant/components/asuswrt/translations/ca.json +++ b/homeassistant/components/asuswrt/translations/ca.json @@ -1,6 +1,9 @@ { "config": { "abort": { + "invalid_unique_id": "No s'ha pogut determinar cap identificador \u00fanic v\u00e0lid del dispositiu", + "no_unique_id": "Ja s'ha configurat un dispositiu sense un identificador \u00fanic v\u00e0lid. La configuraci\u00f3 de diverses inst\u00e0ncies no \u00e9s possible", + "not_unique_id_exist": "Ja s'ha configurat un dispositiu sense un identificador \u00fanic v\u00e0lid. La configuraci\u00f3 de diverses inst\u00e0ncies no \u00e9s possible", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "error": { diff --git a/homeassistant/components/asuswrt/translations/de.json b/homeassistant/components/asuswrt/translations/de.json index af4beb984a4..1a0dc26903d 100644 --- a/homeassistant/components/asuswrt/translations/de.json +++ b/homeassistant/components/asuswrt/translations/de.json @@ -1,6 +1,9 @@ { "config": { "abort": { + "invalid_unique_id": "Unm\u00f6glich, eine g\u00fcltige eindeutige Kennung f\u00fcr das Ger\u00e4t zu ermitteln", + "no_unique_id": "Ein Ger\u00e4t ohne g\u00fcltige, eindeutige ID ist bereits konfiguriert. Die Konfiguration mehrerer Instanzen ist nicht m\u00f6glich", + "not_unique_id_exist": "Ein Ger\u00e4t ohne g\u00fcltige, eindeutige ID ist bereits konfiguriert. Die Konfiguration mehrerer Instanzen ist nicht m\u00f6glich", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { diff --git a/homeassistant/components/asuswrt/translations/el.json b/homeassistant/components/asuswrt/translations/el.json index ea926f10bd2..bb18890091c 100644 --- a/homeassistant/components/asuswrt/translations/el.json +++ b/homeassistant/components/asuswrt/translations/el.json @@ -1,6 +1,9 @@ { "config": { "abort": { + "invalid_unique_id": "\u0391\u03b4\u03cd\u03bd\u03b1\u03c4\u03bf\u03c2 \u03bf \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b5\u03bd\u03cc\u03c2 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c5 \u03bc\u03bf\u03bd\u03b1\u03b4\u03b9\u03ba\u03bf\u03cd \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", + "no_unique_id": "\u039c\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c9\u03c1\u03af\u03c2 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03bc\u03bf\u03bd\u03b1\u03b4\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af. \u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ce\u03bd", + "not_unique_id_exist": "\u039c\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c9\u03c1\u03af\u03c2 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03bc\u03bf\u03bd\u03b1\u03b4\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af. \u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ce\u03bd", "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." }, "error": { diff --git a/homeassistant/components/asuswrt/translations/en.json b/homeassistant/components/asuswrt/translations/en.json index f93eae80e6d..93e8f8869b0 100644 --- a/homeassistant/components/asuswrt/translations/en.json +++ b/homeassistant/components/asuswrt/translations/en.json @@ -2,7 +2,9 @@ "config": { "abort": { "invalid_unique_id": "Impossible to determine a valid unique id for the device", - "no_unique_id": "A device without a valid unique id is already configured. Configuration of multiple instance is not possible" + "no_unique_id": "A device without a valid unique id is already configured. Configuration of multiple instance is not possible", + "not_unique_id_exist": "A device without a valid unique id is already configured. Configuration of multiple instance is not possible", + "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/asuswrt/translations/et.json b/homeassistant/components/asuswrt/translations/et.json index 7b7a0869061..03dc665fcf9 100644 --- a/homeassistant/components/asuswrt/translations/et.json +++ b/homeassistant/components/asuswrt/translations/et.json @@ -1,6 +1,9 @@ { "config": { "abort": { + "invalid_unique_id": "Seadme kehtivat kordumatut ID-d on v\u00f5imatu m\u00e4\u00e4rata", + "no_unique_id": "Seade, millel puudub kehtiv kordumatu ID, on juba seadistatud. Mitme eksemplari seadistamine pole v\u00f5imalik", + "not_unique_id_exist": "Seade, millel puudub kehtiv kordumatu ID, on juba seadistatud. Mitme eksemplari seadistamine pole v\u00f5imalik", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." }, "error": { diff --git a/homeassistant/components/asuswrt/translations/fr.json b/homeassistant/components/asuswrt/translations/fr.json index 69a7705789e..9cb849632b3 100644 --- a/homeassistant/components/asuswrt/translations/fr.json +++ b/homeassistant/components/asuswrt/translations/fr.json @@ -2,7 +2,8 @@ "config": { "abort": { "invalid_unique_id": "Impossible de d\u00e9terminer un identifiant unique valide pour l'appareil", - "not_unique_id_exist": "Un appareil sans UniqueID valide est d\u00e9j\u00e0 configur\u00e9. La configuration de plusieurs instances n'est pas possible", + "no_unique_id": "Un appareil sans identifiant unique valide est d\u00e9j\u00e0 configur\u00e9. La configuration de plusieurs instances n'est pas possible", + "not_unique_id_exist": "Un appareil sans identifiant unique valide est d\u00e9j\u00e0 configur\u00e9. La configuration de plusieurs instances n'est pas possible", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { diff --git a/homeassistant/components/asuswrt/translations/hu.json b/homeassistant/components/asuswrt/translations/hu.json index d8133061380..8cec2c31736 100644 --- a/homeassistant/components/asuswrt/translations/hu.json +++ b/homeassistant/components/asuswrt/translations/hu.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "invalid_unique_id": "Lehetetlen \u00e9rv\u00e9nyes egyedi azonos\u00edt\u00f3t meghat\u00e1rozni az eszk\u00f6zh\u00f6z", + "not_unique_id_exist": "Egy \u00e9rv\u00e9nyes UniqueID azonos\u00edt\u00f3val nem rendelkez\u0151 eszk\u00f6z m\u00e1r konfigur\u00e1lva van. T\u00f6bb p\u00e9ld\u00e1ny konfigur\u00e1l\u00e1sa nem lehets\u00e9ges", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "error": { diff --git a/homeassistant/components/asuswrt/translations/id.json b/homeassistant/components/asuswrt/translations/id.json index 83ade7b6462..249744a1ebe 100644 --- a/homeassistant/components/asuswrt/translations/id.json +++ b/homeassistant/components/asuswrt/translations/id.json @@ -1,6 +1,9 @@ { "config": { "abort": { + "invalid_unique_id": "Penentuan ID unik yang valid untuk perangkat tidak dimungkinkan", + "no_unique_id": "Perangkat tanpa ID unik yang valid sudah dikonfigurasi. Konfigurasi beberapa instans tidak dimungkinkan", + "not_unique_id_exist": "Perangkat tanpa ID unik yang valid sudah dikonfigurasi. Konfigurasi beberapa instans tidak dimungkinkan", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, "error": { diff --git a/homeassistant/components/asuswrt/translations/nl.json b/homeassistant/components/asuswrt/translations/nl.json index e6db5fae621..26d95e16461 100644 --- a/homeassistant/components/asuswrt/translations/nl.json +++ b/homeassistant/components/asuswrt/translations/nl.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "invalid_unique_id": "Onmogelijk om een geldige unieke id voor het apparaat te bepalen", + "not_unique_id_exist": "Een apparaat zonder geldige unieke id is al geconfigureerd. Configuratie van meerdere instanties is niet mogelijk", "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, "error": { diff --git a/homeassistant/components/asuswrt/translations/no.json b/homeassistant/components/asuswrt/translations/no.json index 84ece53ed42..b68e771646f 100644 --- a/homeassistant/components/asuswrt/translations/no.json +++ b/homeassistant/components/asuswrt/translations/no.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "invalid_unique_id": "Umulig \u00e5 bestemme en gyldig unik ID for enheten", + "not_unique_id_exist": "En enhet uten en gyldig UniqueID er allerede konfigurert. Konfigurasjon av flere forekomster er ikke mulig", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { diff --git a/homeassistant/components/asuswrt/translations/pl.json b/homeassistant/components/asuswrt/translations/pl.json index 76d8f30d950..ce0fc53accf 100644 --- a/homeassistant/components/asuswrt/translations/pl.json +++ b/homeassistant/components/asuswrt/translations/pl.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "invalid_unique_id": "Nie mo\u017cna okre\u015bli\u0107 prawid\u0142owego unikalnego identyfikatora urz\u0105dzenia", + "not_unique_id_exist": "Urz\u0105dzenie bez prawid\u0142owego unikatowego identyfikatora jest ju\u017c skonfigurowane. Konfiguracja wielu instancji nie jest mo\u017cliwa", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "error": { diff --git a/homeassistant/components/asuswrt/translations/pt-BR.json b/homeassistant/components/asuswrt/translations/pt-BR.json index 74e48b53ba9..24f2ab51305 100644 --- a/homeassistant/components/asuswrt/translations/pt-BR.json +++ b/homeassistant/components/asuswrt/translations/pt-BR.json @@ -2,6 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Imposs\u00edvel determinar um ID exclusivo v\u00e1lido para o dispositivo", + "no_unique_id": "Um dispositivo sem um ID exclusivo v\u00e1lido j\u00e1 est\u00e1 configurado. A configura\u00e7\u00e3o de v\u00e1rias inst\u00e2ncias n\u00e3o \u00e9 poss\u00edvel", "not_unique_id_exist": "Um dispositivo sem um ID exclusivo v\u00e1lido j\u00e1 est\u00e1 configurado. A configura\u00e7\u00e3o de v\u00e1rias inst\u00e2ncias n\u00e3o \u00e9 poss\u00edvel", "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, diff --git a/homeassistant/components/asuswrt/translations/ru.json b/homeassistant/components/asuswrt/translations/ru.json index 8edc9786f5b..923faf174a5 100644 --- a/homeassistant/components/asuswrt/translations/ru.json +++ b/homeassistant/components/asuswrt/translations/ru.json @@ -1,6 +1,9 @@ { "config": { "abort": { + "invalid_unique_id": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0434\u043b\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", + "no_unique_id": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0431\u0435\u0437 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0433\u043e \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e. \u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u043e\u0432 \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430.", + "not_unique_id_exist": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0431\u0435\u0437 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0433\u043e \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e. \u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u043e\u0432 \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "error": { diff --git a/homeassistant/components/asuswrt/translations/zh-Hant.json b/homeassistant/components/asuswrt/translations/zh-Hant.json index 17c5cd698cd..f78a0466a5c 100644 --- a/homeassistant/components/asuswrt/translations/zh-Hant.json +++ b/homeassistant/components/asuswrt/translations/zh-Hant.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "invalid_unique_id": "\u7121\u6cd5\u78ba\u8a8d\u88dd\u7f6e\u6709\u6548\u552f\u4e00 ID", + "not_unique_id_exist": "\u5df2\u8a2d\u5b9a\u4e0d\u5177\u6709\u6548\u552f\u4e00 ID \u7684\u88dd\u7f6e\uff0c\u7121\u6cd5\u8a2d\u5b9a\u591a\u500b\u5be6\u4f8b", "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { diff --git a/homeassistant/components/deconz/translations/id.json b/homeassistant/components/deconz/translations/id.json index f63261e6e87..e76e8b98d55 100644 --- a/homeassistant/components/deconz/translations/id.json +++ b/homeassistant/components/deconz/translations/id.json @@ -9,6 +9,7 @@ "updated_instance": "Instans deCONZ yang diperbarui dengan alamat host baru" }, "error": { + "linking_not_possible": "Tidak dapat terhubung dengan gateway", "no_key": "Tidak bisa mendapatkan kunci API" }, "flow_title": "{host}", diff --git a/homeassistant/components/isy994/translations/id.json b/homeassistant/components/isy994/translations/id.json index aeea471514c..f27a8c41f5c 100644 --- a/homeassistant/components/isy994/translations/id.json +++ b/homeassistant/components/isy994/translations/id.json @@ -7,10 +7,19 @@ "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", "invalid_host": "Entri host tidak dalam format URL lengkap, misalnya, http://192.168.10.100:80", + "reauth_successful": "Autentikasi ulang berhasil", "unknown": "Kesalahan yang tidak diharapkan" }, "flow_title": "{name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Kredensial untuk {host} tidak lagi valid.", + "title": "Autentikasi ulang ISY Anda" + }, "user": { "data": { "host": "URL", @@ -19,7 +28,7 @@ "username": "Nama Pengguna" }, "description": "Entri host harus dalam format URL lengkap, misalnya, http://192.168.10.100:80", - "title": "Hubungkan ke ISY994 Anda" + "title": "Hubungkan ke ISY Anda" } } }, @@ -33,7 +42,7 @@ "variable_sensor_string": "String Sensor Variabel" }, "description": "Mengatur opsi untuk Integrasi ISY: \n \u2022 String Sensor Node: Setiap perangkat atau folder yang berisi 'String Sensor Node' dalam nama akan diperlakukan sebagai sensor atau sensor biner. \n \u2022 Abaikan String: Setiap perangkat dengan 'Abaikan String' dalam nama akan diabaikan. \n \u2022 String Sensor Variabel: Variabel apa pun yang berisi 'String Sensor Variabel' akan ditambahkan sebagai sensor. \n \u2022 Pulihkan Kecerahan Cahaya: Jika diaktifkan, kecerahan sebelumnya akan dipulihkan saat menyalakan lampu alih-alih bawaan perangkat On-Level.", - "title": "Opsi ISY994" + "title": "Opsi ISY" } } }, diff --git a/homeassistant/components/meater/translations/id.json b/homeassistant/components/meater/translations/id.json index 5d9e28c583c..e27bc97fce5 100644 --- a/homeassistant/components/meater/translations/id.json +++ b/homeassistant/components/meater/translations/id.json @@ -6,11 +6,20 @@ "unknown_auth_error": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Konfirmasikan kata sandi untuk akun Meater Cloud {username}." + }, "user": { "data": { "password": "Kata Sandi", "username": "Nama Pengguna" }, + "data_description": { + "username": "Nama pengguna Meater Cloud, biasanya berupa alamat email." + }, "description": "Siapkan akun Meater Cloud Anda." } } diff --git a/homeassistant/components/meater/translations/nl.json b/homeassistant/components/meater/translations/nl.json index 3261272c7f9..05165dbaa8a 100644 --- a/homeassistant/components/meater/translations/nl.json +++ b/homeassistant/components/meater/translations/nl.json @@ -9,13 +9,17 @@ "reauth_confirm": { "data": { "password": "Wachtwoord" - } + }, + "description": "Bevestig het wachtwoord voor Meater Cloud account {username}." }, "user": { "data": { "password": "Wachtwoord", "username": "Gebruikersnaam" }, + "data_description": { + "username": "Meater Cloud gebruikersnaam, meestal een e-mailadres." + }, "description": "Stel uw Meater Cloud-account in." } } diff --git a/homeassistant/components/ps4/translations/de.json b/homeassistant/components/ps4/translations/de.json index 22a50cdf3e3..94012575c06 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 2nd 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/recorder/translations/id.json b/homeassistant/components/recorder/translations/id.json new file mode 100644 index 00000000000..9d3b4c333ef --- /dev/null +++ b/homeassistant/components/recorder/translations/id.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "current_recorder_run": "Waktu Mulai Jalankan Saat Ini", + "oldest_recorder_run": "Waktu Mulai Lari Terlama" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/pt-BR.json b/homeassistant/components/recorder/translations/pt-BR.json index 78d43d14e52..c44ad73c28b 100644 --- a/homeassistant/components/recorder/translations/pt-BR.json +++ b/homeassistant/components/recorder/translations/pt-BR.json @@ -1,8 +1,8 @@ { "system_health": { "info": { - "current_recorder_run": "Hora atual de in\u00edcio de execu\u00e7\u00e3o", - "oldest_recorder_run": "Hora antiga de in\u00edcio de execu\u00e7\u00e3o" + "current_recorder_run": "Hora de in\u00edcio de execu\u00e7\u00e3o atual", + "oldest_recorder_run": "Hora de in\u00edcio de execu\u00e7\u00e3o mais antiga" } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/bg.json b/homeassistant/components/senz/translations/bg.json index 916271d14d0..a99746433a0 100644 --- a/homeassistant/components/senz/translations/bg.json +++ b/homeassistant/components/senz/translations/bg.json @@ -3,6 +3,11 @@ "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", "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/senz/translations/de.json b/homeassistant/components/senz/translations/de.json index 16c6d8a883d..ffbc7bb458f 100644 --- a/homeassistant/components/senz/translations/de.json +++ b/homeassistant/components/senz/translations/de.json @@ -4,16 +4,16 @@ "already_configured": "Konto wurde bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", - "missing_configuration": "Diese Komponente ist nicht konfiguriert. Bitte in der Anleitung nachlesen.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", "oauth_error": "Ung\u00fcltige Token-Daten empfangen." }, "create_entry": { - "default": "Anmeldung erfolgreich" + "default": "Erfolgreich authentifiziert" }, "step": { "pick_implementation": { - "title": "Art der Anmeldung ausw\u00e4hlen" + "title": "W\u00e4hle die Authentifizierungsmethode" } } } diff --git a/homeassistant/components/simplisafe/translations/bg.json b/homeassistant/components/simplisafe/translations/bg.json index 56cf0e346f1..d193393c534 100644 --- a/homeassistant/components/simplisafe/translations/bg.json +++ b/homeassistant/components/simplisafe/translations/bg.json @@ -8,6 +8,9 @@ "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" }, + "progress": { + "email_2fa": "\u041f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430\u0442\u0430 \u0441\u0438 \u043f\u043e\u0449\u0430 \u0437\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 \u0437\u0430 \u043f\u043e\u0442\u0432\u044a\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u043e\u0442 Simplisafe." + }, "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/simplisafe/translations/id.json b/homeassistant/components/simplisafe/translations/id.json index ad0ad01be63..737831195f1 100644 --- a/homeassistant/components/simplisafe/translations/id.json +++ b/homeassistant/components/simplisafe/translations/id.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Akun SimpliSafe ini sudah digunakan.", + "email_2fa_timed_out": "Tenggang waktu habis ketika menunggu autentikasi dua faktor berbasis email.", "reauth_successful": "Autentikasi ulang berhasil", "wrong_account": "Kredensial pengguna yang diberikan tidak cocok dengan akun SimpliSafe ini." }, @@ -12,6 +13,9 @@ "still_awaiting_mfa": "Masih menunggu pengeklikan dari email MFA", "unknown": "Kesalahan yang tidak diharapkan" }, + "progress": { + "email_2fa": "Periksa email Anda untuk tautan verifikasi dari Simplisafe." + }, "step": { "mfa": { "title": "Autentikasi Multi-Faktor SimpliSafe" diff --git a/homeassistant/components/simplisafe/translations/nl.json b/homeassistant/components/simplisafe/translations/nl.json index 5ed0292b2d0..ff282832e4c 100644 --- a/homeassistant/components/simplisafe/translations/nl.json +++ b/homeassistant/components/simplisafe/translations/nl.json @@ -14,7 +14,7 @@ "unknown": "Onverwachte fout" }, "progress": { - "email_2fa": "Voer de twee-factor authenticatie code in\ndie u per e-mail is toegezonden." + "email_2fa": "Controleer uw e-mail voor een verificatielink van Simplisafe." }, "step": { "mfa": { diff --git a/homeassistant/components/smappee/translations/de.json b/homeassistant/components/smappee/translations/de.json index 127c0f4d725..121b74e9627 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/sql/translations/id.json b/homeassistant/components/sql/translations/id.json index 9cd3574c6c9..8680824e467 100644 --- a/homeassistant/components/sql/translations/id.json +++ b/homeassistant/components/sql/translations/id.json @@ -13,6 +13,7 @@ "data": { "column": "Kolom", "db_url": "URL Database", + "name": "Nama", "query": "Kueri Select", "unit_of_measurement": "Satuan Ukuran", "value_template": "Templat Nilai" @@ -20,6 +21,7 @@ "data_description": { "column": "Kolom pada kueri yang dikembalikan untuk ditampilkan sebagai status", "db_url": "URL database, kosongkan untuk menggunakan database HA default", + "name": "Nama yang akan digunakan untuk Entri Konfigurasi dan juga Sensor", "query": "Kueri untuk dijalankan, perlu dimulai dengan 'SELECT'", "unit_of_measurement": "Satuan Ukuran (opsional)", "value_template": "Template Nilai (opsional)" @@ -38,6 +40,7 @@ "data": { "column": "Kolom", "db_url": "URL Database", + "name": "Nama", "query": "Kueri Select", "unit_of_measurement": "Satuan Ukuran", "value_template": "Templat Nilai" @@ -45,6 +48,7 @@ "data_description": { "column": "Kolom pada kueri yang dikembalikan untuk ditampilkan sebagai status", "db_url": "URL database, kosongkan untuk menggunakan database HA default", + "name": "Nama yang akan digunakan untuk Entri Konfigurasi dan juga Sensor", "query": "Kueri untuk dijalankan, perlu dimulai dengan 'SELECT'", "unit_of_measurement": "Satuan Ukuran (opsional)", "value_template": "Template Nilai (opsional)" diff --git a/homeassistant/components/steam_online/translations/id.json b/homeassistant/components/steam_online/translations/id.json index 07d708067fc..abc61d42194 100644 --- a/homeassistant/components/steam_online/translations/id.json +++ b/homeassistant/components/steam_online/translations/id.json @@ -12,7 +12,7 @@ }, "step": { "reauth_confirm": { - "description": "Integrasi Steam perlu diautentikasi ulang secara manual \n\n Anda dapat menemukan kunci Anda di sini: https://steamcommunity.com/dev/apikey", + "description": "Integrasi Steam perlu diautentikasi ulang secara manual \n\nAnda dapat menemukan kunci Anda di sini: {api_key_url}", "title": "Autentikasi Ulang Integrasi" }, "user": { @@ -20,7 +20,7 @@ "account": "ID akun Steam", "api_key": "Kunci API" }, - "description": "Gunakan https://steamid.io untuk menemukan ID akun Steam Anda" + "description": "Gunakan {account_id_url} untuk menemukan ID akun Steam Anda" } } }, diff --git a/homeassistant/components/trafikverket_ferry/translations/bg.json b/homeassistant/components/trafikverket_ferry/translations/bg.json index 4e93f090b87..72b2aa2cf7b 100644 --- a/homeassistant/components/trafikverket_ferry/translations/bg.json +++ b/homeassistant/components/trafikverket_ferry/translations/bg.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "incorrect_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447 \u0437\u0430 \u0438\u0437\u0431\u0440\u0430\u043d\u0438\u044f \u0430\u043a\u0430\u0443\u043d\u0442", "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": { diff --git a/homeassistant/components/unifi/translations/id.json b/homeassistant/components/unifi/translations/id.json index 4c78c96155c..f3618a31c02 100644 --- a/homeassistant/components/unifi/translations/id.json +++ b/homeassistant/components/unifi/translations/id.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Situs Jaringan UniFi sudah dikonfigurasi", - "configuration_updated": "Konfigurasi diperbarui.", + "configuration_updated": "Konfigurasi diperbarui", "reauth_successful": "Autentikasi ulang berhasil" }, "error": { @@ -26,6 +26,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "Integrasi UniFi tidak disiapkan" + }, "step": { "client_control": { "data": { From d67f19f8a404dffb5c053a7c9232a931ef134647 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 May 2022 01:10:27 -0400 Subject: [PATCH 0214/3516] Fix lutron caseta occupancy sensors (#71309) * Fix lutron_caseta occupancy sensors * Fix lutron_caseta occupancy sensors * Make as service since its a group * merge * Revert "merge" This reverts commit 69d19dc0088bd1b3483cfc481ed2f72e49599cf8. * model and type not present --- .../components/lutron_caseta/__init__.py | 5 ++- .../components/lutron_caseta/binary_sensor.py | 34 ++++++++++++------- .../components/lutron_caseta/const.py | 2 ++ 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index bb8f94f3abe..3d9e07519a8 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -38,6 +38,7 @@ from .const import ( CONF_CA_CERTS, CONF_CERTFILE, CONF_KEYFILE, + CONFIG_URL, DOMAIN, LUTRON_CASETA_BUTTON_EVENT, MANUFACTURER, @@ -306,13 +307,15 @@ class LutronCasetaDevice(Entity): self._device = device self._smartbridge = bridge self._bridge_device = bridge_device + if "serial" not in self._device: + return info = DeviceInfo( identifiers={(DOMAIN, self.serial)}, manufacturer=MANUFACTURER, model=f"{device['model']} ({device['type']})", name=self.name, via_device=(DOMAIN, self._bridge_device["serial"]), - configuration_url="https://device-login.lutron.com", + configuration_url=CONFIG_URL, ) area, _ = _area_and_name_from_name(device["name"]) if area != UNASSIGNED_AREA: diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index 788702f9353..f61e644a331 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -6,11 +6,14 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_SUGGESTED_AREA from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice -from .const import BRIDGE_DEVICE, BRIDGE_LEAP +from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice, _area_and_name_from_name +from .const import BRIDGE_DEVICE, BRIDGE_LEAP, CONFIG_URL, MANUFACTURER, UNASSIGNED_AREA async def async_setup_entry( @@ -39,6 +42,23 @@ async def async_setup_entry( class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): """Representation of a Lutron occupancy group.""" + def __init__(self, device, bridge, bridge_device): + """Init an occupancy sensor.""" + super().__init__(device, bridge, bridge_device) + info = DeviceInfo( + identifiers={(CASETA_DOMAIN, self.unique_id)}, + manufacturer=MANUFACTURER, + model="Lutron Occupancy", + name=self.name, + via_device=(CASETA_DOMAIN, self._bridge_device["serial"]), + configuration_url=CONFIG_URL, + entry_type=DeviceEntryType.SERVICE, + ) + area, _ = _area_and_name_from_name(device["name"]) + if area != UNASSIGNED_AREA: + info[ATTR_SUGGESTED_AREA] = area + self._attr_device_info = info + @property def device_class(self): """Flag supported features.""" @@ -65,16 +85,6 @@ class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): """Return a unique identifier.""" return f"occupancygroup_{self.device_id}" - @property - def device_info(self): - """Return the device info. - - Sensor entities are aggregated from one or more physical - sensors by each room. Therefore, there shouldn't be devices - related to any sensor entities. - """ - return None - @property def extra_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/lutron_caseta/const.py b/homeassistant/components/lutron_caseta/const.py index 56a3821dd64..71d686ba2c8 100644 --- a/homeassistant/components/lutron_caseta/const.py +++ b/homeassistant/components/lutron_caseta/const.py @@ -35,3 +35,5 @@ CONF_SUBTYPE = "subtype" BRIDGE_TIMEOUT = 35 UNASSIGNED_AREA = "Unassigned" + +CONFIG_URL = "https://device-login.lutron.com" From 1da3b5048b4d18f77b6a112636c2cb1871060491 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 May 2022 01:13:23 -0400 Subject: [PATCH 0215/3516] Ensure rachio retries setup later when cloud service is broken (#71300) --- homeassistant/components/rachio/__init__.py | 6 +++++- homeassistant/components/rachio/device.py | 19 +++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index ea8b8fe59cb..e75d7117d73 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -9,7 +9,7 @@ from homeassistant.components import cloud from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from .const import CONF_CLOUDHOOK_URL, CONF_MANUAL_RUN_MINS, CONF_WEBHOOK_ID, DOMAIN @@ -73,6 +73,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Get the API user try: await person.async_setup(hass) + except ConfigEntryAuthFailed as error: + # Reauth is not yet implemented + _LOGGER.error("Authentication failed: %s", error) + return False except ConnectTimeout as error: _LOGGER.error("Could not reach the Rachio API: %s", error) raise ConfigEntryNotReady from error diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index ff7c0535295..911049883d9 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import ServiceCall +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from .const import ( @@ -125,12 +126,18 @@ class RachioPerson: rachio = self.rachio response = rachio.person.info() - assert int(response[0][KEY_STATUS]) == HTTPStatus.OK, "API key error" + if is_invalid_auth_code(int(response[0][KEY_STATUS])): + raise ConfigEntryAuthFailed(f"API key error: {response}") + if int(response[0][KEY_STATUS]) != HTTPStatus.OK: + raise ConfigEntryNotReady(f"API Error: {response}") 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]) == HTTPStatus.OK, "User ID error" + if is_invalid_auth_code(int(data[0][KEY_STATUS])): + raise ConfigEntryAuthFailed(f"User ID error: {data}") + if int(data[0][KEY_STATUS]) != HTTPStatus.OK: + raise ConfigEntryNotReady(f"API Error: {data}") self.username = data[1][KEY_USERNAME] devices = data[1][KEY_DEVICES] for controller in devices: @@ -297,3 +304,11 @@ class RachioIro: """Resume paused watering on this controller.""" self.rachio.device.resume_zone_run(self.controller_id) _LOGGER.debug("Resuming watering on %s", self) + + +def is_invalid_auth_code(http_status_code): + """HTTP status codes that mean invalid auth.""" + if http_status_code in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN): + return True + + return False From db08c04da6f08dbaa29b57e7f8a03bb692ace7b0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 5 May 2022 07:15:24 +0200 Subject: [PATCH 0216/3516] Only test for EncryptedBridge in Samsung J/H models (#71291) --- .../components/samsungtv/__init__.py | 14 +++++----- homeassistant/components/samsungtv/bridge.py | 27 ++++++++++++------- tests/components/samsungtv/conftest.py | 4 +-- .../components/samsungtv/test_config_flow.py | 24 ++++++++--------- 4 files changed, 38 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index dae4033ad4c..a7b8f7d1aec 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -30,7 +30,12 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.typing import ConfigType -from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info +from .bridge import ( + SamsungTVBridge, + async_get_device_info, + mac_from_device_info, + model_requires_encryption, +) from .const import ( CONF_ON_ACTION, CONF_SESSION_ID, @@ -214,11 +219,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -def _model_requires_encryption(model: str | None) -> bool: - """H and J models need pairing with PIN.""" - return model is not None and len(model) > 4 and model[4] in ("H", "J") - - async def _async_create_bridge_with_updated_data( hass: HomeAssistant, entry: ConfigEntry ) -> SamsungTVBridge: @@ -279,7 +279,7 @@ async def _async_create_bridge_with_updated_data( LOGGER.info("Updated model to %s for %s", model, host) updated_data[CONF_MODEL] = model - if _model_requires_encryption(model) and method != METHOD_ENCRYPTED_WEBSOCKET: + if model_requires_encryption(model) and method != METHOD_ENCRYPTED_WEBSOCKET: LOGGER.info( "Detected model %s for %s. Some televisions from H and J series use " "an encrypted protocol but you are using %s which may not be supported", diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 52ab86337dd..c3201a493eb 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -85,6 +85,11 @@ def mac_from_device_info(info: dict[str, Any]) -> str | None: return None +def model_requires_encryption(model: str | None) -> bool: + """H and J models need pairing with PIN.""" + return model is not None and len(model) > 4 and model[4] in ("H", "J") + + async def async_get_device_info( hass: HomeAssistant, host: str, @@ -99,17 +104,19 @@ async def async_get_device_info( port, info, ) - encrypted_bridge = SamsungTVEncryptedBridge( - hass, METHOD_ENCRYPTED_WEBSOCKET, host, ENCRYPTED_WEBSOCKET_PORT - ) - result = await encrypted_bridge.async_try_connect() - if result != RESULT_CANNOT_CONNECT: - return ( - result, - ENCRYPTED_WEBSOCKET_PORT, - METHOD_ENCRYPTED_WEBSOCKET, - info, + # Check the encrypted port if the model requires encryption + if model_requires_encryption(info.get("device", {}).get("modelName")): + encrypted_bridge = SamsungTVEncryptedBridge( + hass, METHOD_ENCRYPTED_WEBSOCKET, host, ENCRYPTED_WEBSOCKET_PORT ) + result = await encrypted_bridge.async_try_connect() + if result != RESULT_CANNOT_CONNECT: + return ( + result, + ENCRYPTED_WEBSOCKET_PORT, + METHOD_ENCRYPTED_WEBSOCKET, + info, + ) return RESULT_SUCCESS, port, METHOD_WEBSOCKET, info # Try legacy port diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index d7f8ed0d1a1..764022f3501 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -22,7 +22,7 @@ from samsungtvws.remote import ChannelEmitCommand from homeassistant.components.samsungtv.const import WEBSOCKET_SSL_PORT import homeassistant.util.dt as dt_util -from .const import SAMPLE_DEVICE_INFO_WIFI +from .const import SAMPLE_DEVICE_INFO_UE48JU6400, SAMPLE_DEVICE_INFO_WIFI @pytest.fixture(autouse=True) @@ -177,7 +177,7 @@ def rest_api_fixture_non_ssl_only() -> Mock: """Mock rest_device_info to fail for ssl and work for non-ssl.""" if self.port == WEBSOCKET_SSL_PORT: raise ResponseError - return SAMPLE_DEVICE_INFO_WIFI + return SAMPLE_DEVICE_INFO_UE48JU6400 with patch( "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index d2a9d10caf2..40397a68d7d 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -340,16 +340,16 @@ async def test_user_encrypted_websocket( ) assert result4["type"] == "create_entry" - assert result4["title"] == "Living Room (82GXARRS)" + assert result4["title"] == "TV-UE48JU6470 (UE48JU6400)" assert result4["data"][CONF_HOST] == "fake_host" - assert result4["data"][CONF_NAME] == "Living Room" + assert result4["data"][CONF_NAME] == "TV-UE48JU6470" assert result4["data"][CONF_MAC] == "aa:bb:ww:ii:ff:ii" assert result4["data"][CONF_MANUFACTURER] == "Samsung" - assert result4["data"][CONF_MODEL] == "82GXARRS" + assert result4["data"][CONF_MODEL] == "UE48JU6400" assert result4["data"][CONF_SSDP_RENDERING_CONTROL_LOCATION] is None assert result4["data"][CONF_TOKEN] == "037739871315caef138547b03e348b72" assert result4["data"][CONF_SESSION_ID] == "1" - assert result4["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + assert result4["result"].unique_id == "223da676-497a-4e06-9507-5e27ec4f0fb3" @pytest.mark.usefixtures("rest_api_failing") @@ -714,19 +714,19 @@ async def test_ssdp_encrypted_websocket_success_populates_mac_address_and_ssdp_l ) assert result4["type"] == "create_entry" - assert result4["title"] == "Living Room (82GXARRS)" + assert result4["title"] == "TV-UE48JU6470 (UE48JU6400)" assert result4["data"][CONF_HOST] == "fake_host" - assert result4["data"][CONF_NAME] == "Living Room" + assert result4["data"][CONF_NAME] == "TV-UE48JU6470" assert result4["data"][CONF_MAC] == "aa:bb:ww:ii:ff:ii" assert result4["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" - assert result4["data"][CONF_MODEL] == "82GXARRS" + assert result4["data"][CONF_MODEL] == "UE48JU6400" assert ( result4["data"][CONF_SSDP_RENDERING_CONTROL_LOCATION] == "https://fake_host:12345/test" ) assert result4["data"][CONF_TOKEN] == "037739871315caef138547b03e348b72" assert result4["data"][CONF_SESSION_ID] == "1" - assert result4["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + assert result4["result"].unique_id == "223da676-497a-4e06-9507-5e27ec4f0fb3" @pytest.mark.usefixtures("rest_api_non_ssl_only") @@ -1036,13 +1036,13 @@ async def test_dhcp_wireless(hass: HomeAssistant) -> None: result["flow_id"], user_input="whatever" ) assert result["type"] == "create_entry" - assert result["title"] == "Living Room (82GXARRS)" + assert result["title"] == "TV-UE48JU6470 (UE48JU6400)" assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_NAME] == "Living Room" + assert result["data"][CONF_NAME] == "TV-UE48JU6470" assert result["data"][CONF_MAC] == "aa:bb:ww:ii:ff:ii" assert result["data"][CONF_MANUFACTURER] == "Samsung" - assert result["data"][CONF_MODEL] == "82GXARRS" - assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + assert result["data"][CONF_MODEL] == "UE48JU6400" + assert result["result"].unique_id == "223da676-497a-4e06-9507-5e27ec4f0fb3" @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") From eebf3acb93507f8f706f8043d57fdfd09942a750 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Thu, 5 May 2022 15:22:15 +1000 Subject: [PATCH 0217/3516] Relax dlna_dmr filtering when browsing media (#69576) * Fix incorrect types of test data structures * Loosen MIME-type filtering for async_browse_media * Add option to not filter results when browsing media Some devices do not report all that they support, and in this case filtering will hide media that's actually playable. Most devices are OK, though, and it's better to hide what they can't play. Add an option, off by default, to show all media. * Fix linting issues --- .../components/dlna_dmr/config_flow.py | 21 ++-- homeassistant/components/dlna_dmr/const.py | 1 + .../components/dlna_dmr/media_player.py | 39 +++++-- .../components/dlna_dmr/strings.json | 3 +- .../components/dlna_dmr/translations/en.json | 1 + tests/components/dlna_dmr/test_config_flow.py | 14 ++- .../components/dlna_dmr/test_media_player.py | 102 ++++++++++++++++++ 7 files changed, 157 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index ac7e3c83253..c7b37537e2b 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -21,6 +21,7 @@ from homeassistant.exceptions import IntegrationError import homeassistant.helpers.config_validation as cv from .const import ( + CONF_BROWSE_UNFILTERED, CONF_CALLBACK_URL_OVERRIDE, CONF_LISTEN_PORT, CONF_POLL_AVAILABILITY, @@ -328,6 +329,7 @@ class DlnaDmrOptionsFlowHandler(config_entries.OptionsFlow): options[CONF_LISTEN_PORT] = listen_port options[CONF_CALLBACK_URL_OVERRIDE] = callback_url_override options[CONF_POLL_AVAILABILITY] = user_input[CONF_POLL_AVAILABILITY] + options[CONF_BROWSE_UNFILTERED] = user_input[CONF_BROWSE_UNFILTERED] # Save if there's no errors, else fall through and show the form again if not errors: @@ -335,9 +337,14 @@ class DlnaDmrOptionsFlowHandler(config_entries.OptionsFlow): fields = {} - def _add_with_suggestion(key: str, validator: Callable) -> None: - """Add a field to with a suggested, not default, value.""" - if (suggested_value := options.get(key)) is None: + def _add_with_suggestion(key: str, validator: Callable | type[bool]) -> None: + """Add a field to with a suggested value. + + For bools, use the existing value as default, or fallback to False. + """ + if validator is bool: + fields[vol.Required(key, default=options.get(key, False))] = validator + elif (suggested_value := options.get(key)) is None: fields[vol.Optional(key)] = validator else: fields[ @@ -347,12 +354,8 @@ class DlnaDmrOptionsFlowHandler(config_entries.OptionsFlow): # listen_port can be blank or 0 for "bind any free port" _add_with_suggestion(CONF_LISTEN_PORT, cv.port) _add_with_suggestion(CONF_CALLBACK_URL_OVERRIDE, str) - fields[ - vol.Required( - CONF_POLL_AVAILABILITY, - default=options.get(CONF_POLL_AVAILABILITY, False), - ) - ] = bool + _add_with_suggestion(CONF_POLL_AVAILABILITY, bool) + _add_with_suggestion(CONF_BROWSE_UNFILTERED, bool) return self.async_show_form( step_id="init", diff --git a/homeassistant/components/dlna_dmr/const.py b/homeassistant/components/dlna_dmr/const.py index a4118a0ce78..4f9982061fb 100644 --- a/homeassistant/components/dlna_dmr/const.py +++ b/homeassistant/components/dlna_dmr/const.py @@ -16,6 +16,7 @@ DOMAIN: Final = "dlna_dmr" CONF_LISTEN_PORT: Final = "listen_port" CONF_CALLBACK_URL_OVERRIDE: Final = "callback_url_override" CONF_POLL_AVAILABILITY: Final = "poll_availability" +CONF_BROWSE_UNFILTERED: Final = "browse_unfiltered" DEFAULT_NAME: Final = "DLNA Digital Media Renderer" diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 9a7eef4bf89..fd1fc9b2bab 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -45,6 +45,7 @@ from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( + CONF_BROWSE_UNFILTERED, CONF_CALLBACK_URL_OVERRIDE, CONF_LISTEN_PORT, CONF_POLL_AVAILABILITY, @@ -108,6 +109,7 @@ async def async_setup_entry( event_callback_url=entry.options.get(CONF_CALLBACK_URL_OVERRIDE), poll_availability=entry.options.get(CONF_POLL_AVAILABILITY, False), location=entry.data[CONF_URL], + browse_unfiltered=entry.options.get(CONF_BROWSE_UNFILTERED, False), ) async_add_entities([entity]) @@ -124,6 +126,8 @@ class DlnaDmrEntity(MediaPlayerEntity): # Last known URL for the device, used when adding this entity to hass to try # to connect before SSDP has rediscovered it, or when SSDP discovery fails. location: str + # Should the async_browse_media function *not* filter out incompatible media? + browse_unfiltered: bool _device_lock: asyncio.Lock # Held when connecting or disconnecting the device _device: DmrDevice | None = None @@ -146,6 +150,7 @@ class DlnaDmrEntity(MediaPlayerEntity): event_callback_url: str | None, poll_availability: bool, location: str, + browse_unfiltered: bool, ) -> None: """Initialize DLNA DMR entity.""" self.udn = udn @@ -154,6 +159,7 @@ class DlnaDmrEntity(MediaPlayerEntity): self._event_addr = EventListenAddr(None, event_port, event_callback_url) self.poll_availability = poll_availability self.location = location + self.browse_unfiltered = browse_unfiltered self._device_lock = asyncio.Lock() async def async_added_to_hass(self) -> None: @@ -275,6 +281,7 @@ class DlnaDmrEntity(MediaPlayerEntity): ) self.location = entry.data[CONF_URL] self.poll_availability = entry.options.get(CONF_POLL_AVAILABILITY, False) + self.browse_unfiltered = entry.options.get(CONF_BROWSE_UNFILTERED, False) new_port = entry.options.get(CONF_LISTEN_PORT) or 0 new_callback_url = entry.options.get(CONF_CALLBACK_URL_OVERRIDE) @@ -762,14 +769,21 @@ class DlnaDmrEntity(MediaPlayerEntity): # media_content_type is ignored; it's the content_type of the current # media_content_id, not the desired content_type of whomever is calling. - content_filter = self._get_content_filter() + if self.browse_unfiltered: + content_filter = None + else: + content_filter = self._get_content_filter() return await media_source.async_browse_media( self.hass, media_content_id, content_filter=content_filter ) def _get_content_filter(self) -> Callable[[BrowseMedia], bool]: - """Return a function that filters media based on what the renderer can play.""" + """Return a function that filters media based on what the renderer can play. + + The filtering is pretty loose; it's better to show something that can't + be played than hide something that can. + """ if not self._device or not self._device.sink_protocol_info: # Nothing is specified by the renderer, so show everything _LOGGER.debug("Get content filter with no device or sink protocol info") @@ -780,18 +794,25 @@ class DlnaDmrEntity(MediaPlayerEntity): # Renderer claims it can handle everything, so show everything return lambda _: True - # Convert list of things like "http-get:*:audio/mpeg:*" to just "audio/mpeg" - content_types: list[str] = [] + # Convert list of things like "http-get:*:audio/mpeg;codecs=mp3:*" + # to just "audio/mpeg" + content_types = set[str]() for protocol_info in self._device.sink_protocol_info: protocol, _, content_format, _ = protocol_info.split(":", 3) + # Transform content_format for better generic matching + content_format = content_format.lower().replace("/x-", "/", 1) + content_format = content_format.partition(";")[0] + if protocol in STREAMABLE_PROTOCOLS: - content_types.append(content_format) + content_types.add(content_format) - def _content_type_filter(item: BrowseMedia) -> bool: - """Filter media items by their content_type.""" - return item.media_content_type in content_types + def _content_filter(item: BrowseMedia) -> bool: + """Filter media items by their media_content_type.""" + content_type = item.media_content_type + content_type = content_type.lower().replace("/x-", "/", 1).partition(";")[0] + return content_type in content_types - return _content_type_filter + return _content_filter @property def media_title(self) -> str | None: diff --git a/homeassistant/components/dlna_dmr/strings.json b/homeassistant/components/dlna_dmr/strings.json index ac77009e0cb..d646f20f7a1 100644 --- a/homeassistant/components/dlna_dmr/strings.json +++ b/homeassistant/components/dlna_dmr/strings.json @@ -44,7 +44,8 @@ "data": { "listen_port": "Event listener port (random if not set)", "callback_url_override": "Event listener callback URL", - "poll_availability": "Poll for device availability" + "poll_availability": "Poll for device availability", + "browse_unfiltered": "Show incompatible media when browsing" } } }, diff --git a/homeassistant/components/dlna_dmr/translations/en.json b/homeassistant/components/dlna_dmr/translations/en.json index 512dfe7f11c..51cbd875211 100644 --- a/homeassistant/components/dlna_dmr/translations/en.json +++ b/homeassistant/components/dlna_dmr/translations/en.json @@ -47,6 +47,7 @@ "step": { "init": { "data": { + "browse_unfiltered": "Show incompatible media when browsing", "callback_url_override": "Event listener callback URL", "listen_port": "Event listener port (random if not set)", "poll_availability": "Poll for device availability" diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index 6112c7c5ed5..7ec25906f99 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -11,6 +11,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp from homeassistant.components.dlna_dmr.const import ( + CONF_BROWSE_UNFILTERED, CONF_CALLBACK_URL_OVERRIDE, CONF_LISTEN_PORT, CONF_POLL_AVAILABILITY, @@ -74,7 +75,7 @@ MOCK_DISCOVERY = ssdp.SsdpServiceInfo( ] }, }, - x_homeassistant_matching_domains=(DLNA_DOMAIN,), + x_homeassistant_matching_domains={DLNA_DOMAIN}, ) @@ -390,7 +391,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: """Test SSDP ignores devices that are missing required services.""" # No service list at all discovery = dataclasses.replace(MOCK_DISCOVERY) - discovery.upnp = discovery.upnp.copy() + discovery.upnp = dict(discovery.upnp) del discovery.upnp[ssdp.ATTR_UPNP_SERVICE_LIST] result = await hass.config_entries.flow.async_init( DLNA_DOMAIN, @@ -414,7 +415,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: # AVTransport service is missing discovery = dataclasses.replace(MOCK_DISCOVERY) - discovery.upnp = discovery.upnp.copy() + discovery.upnp = dict(discovery.upnp) discovery.upnp[ssdp.ATTR_UPNP_SERVICE_LIST] = { "service": [ service @@ -465,7 +466,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: assert result["reason"] == "alternative_integration" discovery = dataclasses.replace(MOCK_DISCOVERY) - discovery.upnp = discovery.upnp.copy() + discovery.upnp = dict(discovery.upnp) discovery.upnp[ ssdp.ATTR_UPNP_DEVICE_TYPE ] = "urn:schemas-upnp-org:device:ZonePlayer:1" @@ -484,7 +485,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: ("Royal Philips Electronics", "Philips TV DMR"), ]: discovery = dataclasses.replace(MOCK_DISCOVERY) - discovery.upnp = discovery.upnp.copy() + discovery.upnp = dict(discovery.upnp) discovery.upnp[ssdp.ATTR_UPNP_MANUFACTURER] = manufacturer discovery.upnp[ssdp.ATTR_UPNP_MODEL_NAME] = model result = await hass.config_entries.flow.async_init( @@ -592,6 +593,7 @@ async def test_options_flow( user_input={ CONF_CALLBACK_URL_OVERRIDE: "Bad url", CONF_POLL_AVAILABILITY: False, + CONF_BROWSE_UNFILTERED: False, }, ) @@ -606,6 +608,7 @@ async def test_options_flow( CONF_LISTEN_PORT: 2222, CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", CONF_POLL_AVAILABILITY: True, + CONF_BROWSE_UNFILTERED: True, }, ) @@ -614,4 +617,5 @@ async def test_options_flow( CONF_LISTEN_PORT: 2222, CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", CONF_POLL_AVAILABILITY: True, + CONF_BROWSE_UNFILTERED: True, } diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index bed89f3db9d..eef5d936396 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -23,6 +23,7 @@ from homeassistant import const as ha_const from homeassistant.components import ssdp from homeassistant.components.dlna_dmr import media_player from homeassistant.components.dlna_dmr.const import ( + CONF_BROWSE_UNFILTERED, CONF_CALLBACK_URL_OVERRIDE, CONF_LISTEN_PORT, CONF_POLL_AVAILABILITY, @@ -997,6 +998,26 @@ async def test_browse_media( # Audio file should appear assert expected_child_audio in response["result"]["children"] + # Device specifies extra parameters in MIME type, uses non-standard "x-" + # prefix, and capitilizes things, all of which should be ignored + dmr_device_mock.sink_protocol_info = [ + "http-get:*:audio/X-MPEG;codecs=mp3:*", + ] + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": mock_entity_id, + } + ) + response = await client.receive_json() + assert response["success"] + # Video file should not be shown + assert expected_child_video not in response["result"]["children"] + # Audio file should appear + assert expected_child_audio in response["result"]["children"] + # Device does not specify what it can play dmr_device_mock.sink_protocol_info = [] client = await hass_ws_client() @@ -1014,6 +1035,87 @@ async def test_browse_media( assert expected_child_audio in response["result"]["children"] +async def test_browse_media_unfiltered( + hass: HomeAssistant, + hass_ws_client, + config_entry_mock: MockConfigEntry, + dmr_device_mock: Mock, + mock_entity_id: str, +) -> None: + """Test the async_browse_media method with filtering turned off and on.""" + # Based on cast's test_entity_browse_media + await async_setup_component(hass, MS_DOMAIN, {MS_DOMAIN: {}}) + await hass.async_block_till_done() + + expected_child_video = { + "title": "Epic Sax Guy 10 Hours.mp4", + "media_class": "video", + "media_content_type": "video/mp4", + "media_content_id": "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4", + "can_play": True, + "can_expand": False, + "thumbnail": None, + "children_media_class": None, + } + expected_child_audio = { + "title": "test.mp3", + "media_class": "music", + "media_content_type": "audio/mpeg", + "media_content_id": "media-source://media_source/local/test.mp3", + "can_play": True, + "can_expand": False, + "thumbnail": None, + "children_media_class": None, + } + + # Device can only play MIME type audio/mpeg and audio/vorbis + dmr_device_mock.sink_protocol_info = [ + "http-get:*:audio/mpeg:*", + "http-get:*:audio/vorbis:*", + ] + + # Filtering turned on by default + assert CONF_BROWSE_UNFILTERED not in config_entry_mock.options + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": mock_entity_id, + } + ) + response = await client.receive_json() + assert response["success"] + # Video file should not be shown + assert expected_child_video not in response["result"]["children"] + # Audio file should appear + assert expected_child_audio in response["result"]["children"] + + # Filtering turned off via config entry + hass.config_entries.async_update_entry( + config_entry_mock, + options={ + CONF_BROWSE_UNFILTERED: True, + }, + ) + await hass.async_block_till_done() + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": mock_entity_id, + } + ) + response = await client.receive_json() + assert response["success"] + # All files should be returned + assert expected_child_video in response["result"]["children"] + assert expected_child_audio in response["result"]["children"] + + async def test_playback_update_state( hass: HomeAssistant, dmr_device_mock: Mock, mock_entity_id: str ) -> None: From a0474633fd944a79731945a0f62ac36ebbd4740b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 4 May 2022 23:52:00 -0700 Subject: [PATCH 0218/3516] Fix apple tv warning (#71321) --- homeassistant/components/apple_tv/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 6c1b35f70f8..5a7298dcbee 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -296,7 +296,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): _LOGGER.debug("Streaming %s via RAOP", media_id) await self.atv.stream.stream_file(media_id) - if self._is_feature_available(FeatureName.PlayUrl): + elif self._is_feature_available(FeatureName.PlayUrl): _LOGGER.debug("Playing %s via AirPlay", media_id) await self.atv.stream.play_url(media_id) else: From e8f0d80fc988d8e43bb1eff27cb45288d446f1c1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 5 May 2022 08:52:20 +0200 Subject: [PATCH 0219/3516] Fix Meater (#71324) --- homeassistant/components/meater/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index 17e8db9e473..8c719d588d8 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -146,13 +146,13 @@ async def async_setup_entry( if not coordinator.last_update_success: return - devices = coordinator.data + devices: dict[str, MeaterProbe] = coordinator.data entities = [] known_probes: set = hass.data[DOMAIN]["known_probes"] # Add entities for temperature probes which we've not yet seen for dev in devices: - if dev.id in known_probes: + if dev in known_probes: continue entities.extend( @@ -161,7 +161,7 @@ async def async_setup_entry( for sensor_description in SENSOR_TYPES ] ) - known_probes.add(dev.id) + known_probes.add(dev) async_add_entities(entities) From 6a35c3f2ab0fbebbdb94a31d26a8babcd133afef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 5 May 2022 09:06:23 +0200 Subject: [PATCH 0220/3516] Update aioairzone to v0.4.3 (#71312) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update aioairzone to v0.4.3 Fixes exception on older local API. Signed-off-by: Álvaro Fernández Rojas * airzone: switch to set_hvac_parameters function Fixes failing airzone tests. Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone/climate.py | 2 +- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airzone/test_climate.py | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index 1ec7dfabcfa..9cff1ff393d 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -123,7 +123,7 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity): } _LOGGER.debug("update_hvac_params=%s", _params) try: - await self.coordinator.airzone.put_hvac(_params) + await self.coordinator.airzone.set_hvac_parameters(_params) except AirzoneError as error: raise HomeAssistantError( f"Failed to set zone {self.name}: {error}" diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index a6ea814fe9c..7a04b3a78b3 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -3,7 +3,7 @@ "name": "Airzone", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airzone", - "requirements": ["aioairzone==0.4.2"], + "requirements": ["aioairzone==0.4.3"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioairzone"] diff --git a/requirements_all.txt b/requirements_all.txt index 16778889248..02b21b18c4b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -110,7 +110,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.2 +aioairzone==0.4.3 # homeassistant.components.ambient_station aioambient==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d1a1de2cf6..2e71bda5cf1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -94,7 +94,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.2 +aioairzone==0.4.3 # homeassistant.components.ambient_station aioambient==2021.11.0 diff --git a/tests/components/airzone/test_climate.py b/tests/components/airzone/test_climate.py index 2128d2818e7..dcb493351ba 100644 --- a/tests/components/airzone/test_climate.py +++ b/tests/components/airzone/test_climate.py @@ -147,7 +147,7 @@ async def test_airzone_climate_turn_on_off(hass: HomeAssistant) -> None: ] } with patch( - "homeassistant.components.airzone.AirzoneLocalApi.http_request", + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", return_value=HVAC_MOCK, ): await hass.services.async_call( @@ -172,7 +172,7 @@ async def test_airzone_climate_turn_on_off(hass: HomeAssistant) -> None: ] } with patch( - "homeassistant.components.airzone.AirzoneLocalApi.http_request", + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", return_value=HVAC_MOCK, ): await hass.services.async_call( @@ -204,7 +204,7 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None: ] } with patch( - "homeassistant.components.airzone.AirzoneLocalApi.http_request", + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", return_value=HVAC_MOCK, ): await hass.services.async_call( @@ -230,7 +230,7 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None: ] } with patch( - "homeassistant.components.airzone.AirzoneLocalApi.http_request", + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", return_value=HVAC_MOCK_2, ): await hass.services.async_call( @@ -263,7 +263,7 @@ async def test_airzone_climate_set_hvac_slave_error(hass: HomeAssistant) -> None await async_init_integration(hass) with patch( - "homeassistant.components.airzone.AirzoneLocalApi.http_request", + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", return_value=HVAC_MOCK, ), pytest.raises(HomeAssistantError): await hass.services.async_call( @@ -296,7 +296,7 @@ async def test_airzone_climate_set_temp(hass: HomeAssistant) -> None: await async_init_integration(hass) with patch( - "homeassistant.components.airzone.AirzoneLocalApi.http_request", + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", return_value=HVAC_MOCK, ): await hass.services.async_call( From e145d3c65bb3401f96903d492fb24e5600fdfedb Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 5 May 2022 10:12:39 +0200 Subject: [PATCH 0221/3516] Bump numpy to 1.21.6 (#71325) --- homeassistant/components/compensation/manifest.json | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index 315d1b705df..213e8888e23 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,7 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.21.4"], + "requirements": ["numpy==1.21.6"], "codeowners": ["@Petro31"], "iot_class": "calculated" } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 50ddeb3bba7..9bb07157b54 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.21.4", "pyiqvia==2022.04.0"], + "requirements": ["numpy==1.21.6", "pyiqvia==2022.04.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyiqvia"] diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 9c1f51c4933..504b83bdaf9 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.21.4", "opencv-python-headless==4.5.2.54"], + "requirements": ["numpy==1.21.6", "opencv-python-headless==4.5.2.54"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 5f1ac406b70..0f53dd61cb4 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.5.0", "tf-models-official==2.5.0", "pycocotools==2.0.1", - "numpy==1.21.4", + "numpy==1.21.6", "pillow==9.1.0" ], "codeowners": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 831e97aed3d..aaae8f7cc54 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.21.4"], + "requirements": ["numpy==1.21.6"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 02b21b18c4b..7faa9dad813 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1114,7 +1114,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.21.4 +numpy==1.21.6 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2e71bda5cf1..89192ad4f2f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -758,7 +758,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.21.4 +numpy==1.21.6 # homeassistant.components.google oauth2client==4.1.3 From 22d258759324a3b16aae7a2f4f4a14ed3dbeedc5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 5 May 2022 11:24:43 +0200 Subject: [PATCH 0222/3516] Tweak Meater typing and variable naming (#71333) --- homeassistant/components/meater/sensor.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index 8c719d588d8..8a6c07bcbc4 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -136,9 +136,9 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the entry.""" - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - "coordinator" - ] + coordinator: DataUpdateCoordinator[dict[str, MeaterProbe]] = hass.data[DOMAIN][ + entry.entry_id + ]["coordinator"] @callback def async_update_data(): @@ -146,22 +146,22 @@ async def async_setup_entry( if not coordinator.last_update_success: return - devices: dict[str, MeaterProbe] = coordinator.data + devices = coordinator.data entities = [] known_probes: set = hass.data[DOMAIN]["known_probes"] # Add entities for temperature probes which we've not yet seen - for dev in devices: - if dev in known_probes: + for device_id in devices: + if device_id in known_probes: continue entities.extend( [ - MeaterProbeTemperature(coordinator, dev, sensor_description) + MeaterProbeTemperature(coordinator, device_id, sensor_description) for sensor_description in SENSOR_TYPES ] ) - known_probes.add(dev) + known_probes.add(device_id) async_add_entities(entities) From 248f01f41f58682abc75a26f8ae16bd1516fba97 Mon Sep 17 00:00:00 2001 From: Markus Bong Date: Thu, 5 May 2022 11:36:00 +0200 Subject: [PATCH 0223/3516] fix reading of battery messages (#70659) --- .../components/devolo_home_control/devolo_device.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/devolo_home_control/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py index b3cb68098e1..6087e07799d 100644 --- a/homeassistant/components/devolo_home_control/devolo_device.py +++ b/homeassistant/components/devolo_home_control/devolo_device.py @@ -7,6 +7,7 @@ from urllib.parse import urlparse from devolo_home_control_api.devices.zwave import Zwave from devolo_home_control_api.homecontrol import HomeControl +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN @@ -71,7 +72,11 @@ class DevoloDeviceEntity(Entity): def _generic_message(self, message: tuple) -> None: """Handle generic messages.""" - if len(message) == 3 and message[2] == "battery_level": + if ( + len(message) == 3 + and message[2] == "battery_level" + and self.device_class == SensorDeviceClass.BATTERY + ): self._value = message[1] elif len(message) == 3 and message[2] == "status": # Maybe the API wants to tell us, that the device went on- or offline. From 191230f535b053ec76ae1164035de8879dfc2750 Mon Sep 17 00:00:00 2001 From: Antoni Czaplicki <56671347+Antoni-Czaplicki@users.noreply.github.com> Date: Thu, 5 May 2022 14:32:36 +0200 Subject: [PATCH 0224/3516] Refactor vulcan integration (#71175) --- homeassistant/components/vulcan/__init__.py | 52 +-- homeassistant/components/vulcan/calendar.py | 167 +++------ .../components/vulcan/config_flow.py | 223 +++++------- homeassistant/components/vulcan/const.py | 1 - homeassistant/components/vulcan/fetch_data.py | 1 + homeassistant/components/vulcan/manifest.json | 5 +- homeassistant/components/vulcan/register.py | 5 +- homeassistant/components/vulcan/strings.json | 20 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/vulcan/test_config_flow.py | 344 +++++++----------- 11 files changed, 293 insertions(+), 529 deletions(-) diff --git a/homeassistant/components/vulcan/__init__.py b/homeassistant/components/vulcan/__init__.py index 430819c8f4c..50f525d9a68 100644 --- a/homeassistant/components/vulcan/__init__.py +++ b/homeassistant/components/vulcan/__init__.py @@ -1,18 +1,15 @@ """The Vulcan component.""" -import logging from aiohttp import ClientConnectorError -from vulcan import Account, Keystore, Vulcan -from vulcan._utils import VulcanAPIException +from vulcan import Account, Keystore, UnauthorizedCertificateException, Vulcan from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) - PLATFORMS = ["calendar"] @@ -22,54 +19,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: keystore = Keystore.load(entry.data["keystore"]) account = Account.load(entry.data["account"]) - client = Vulcan(keystore, account) + client = Vulcan(keystore, account, async_get_clientsession(hass)) await client.select_student() students = await client.get_students() for student in students: if str(student.pupil.id) == str(entry.data["student_id"]): client.student = student break - except VulcanAPIException as err: - if str(err) == "The certificate is not authorized.": - _LOGGER.error( - "The certificate is not authorized, please authorize integration again" - ) - raise ConfigEntryAuthFailed from err - _LOGGER.error("Vulcan API error: %s", err) - return False + except UnauthorizedCertificateException as err: + raise ConfigEntryAuthFailed("The certificate is not authorized.") from err except ClientConnectorError as err: - if "connection_error" not in hass.data[DOMAIN]: - _LOGGER.error( - "Connection error - please check your internet connection: %s", err - ) - hass.data[DOMAIN]["connection_error"] = True - await client.close() - raise ConfigEntryNotReady from err - hass.data[DOMAIN]["students_number"] = len( - hass.config_entries.async_entries(DOMAIN) - ) + raise ConfigEntryNotReady( + f"Connection error - please check your internet connection: {err}" + ) from err hass.data[DOMAIN][entry.entry_id] = client - if not entry.update_listeners: - entry.add_update_listener(_async_update_options) - - for platform in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - await hass.data[DOMAIN][entry.entry_id].close() - for platform in PLATFORMS: - await hass.config_entries.async_forward_entry_unload(entry, platform) + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) - return True - - -async def _async_update_options(hass, entry): - """Update options.""" - await hass.config_entries.async_reload(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/vulcan/calendar.py b/homeassistant/components/vulcan/calendar.py index 2e9e79063f9..ac302940c72 100644 --- a/homeassistant/components/vulcan/calendar.py +++ b/homeassistant/components/vulcan/calendar.py @@ -1,24 +1,25 @@ """Support for Vulcan Calendar platform.""" -import copy +from __future__ import annotations + from datetime import date, datetime, timedelta import logging from aiohttp import ClientConnectorError -from vulcan._utils import VulcanAPIException +from vulcan import UnauthorizedCertificateException -from homeassistant.components.calendar import ENTITY_ID_FORMAT, CalendarEventDevice +from homeassistant.components.calendar import ( + ENTITY_ID_FORMAT, + CalendarEntity, + CalendarEvent, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.template import DATE_STR_FORMAT -from homeassistant.util import Throttle, dt from . import DOMAIN -from .const import DEFAULT_SCAN_INTERVAL from .fetch_data import get_lessons, get_student_info _LOGGER = logging.getLogger(__name__) @@ -29,20 +30,16 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the calendar platform for event devices.""" - VulcanCalendarData.MIN_TIME_BETWEEN_UPDATES = timedelta( - minutes=config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - ) + """Set up the calendar platform for entity.""" client = hass.data[DOMAIN][config_entry.entry_id] data = { "student_info": await get_student_info( client, config_entry.data.get("student_id") ), - "students_number": hass.data[DOMAIN]["students_number"], } async_add_entities( [ - VulcanCalendarEventDevice( + VulcanCalendarEntity( client, data, generate_entity_id( @@ -55,80 +52,33 @@ async def async_setup_entry( ) -class VulcanCalendarEventDevice(CalendarEventDevice): - """A calendar event device.""" +class VulcanCalendarEntity(CalendarEntity): + """A calendar entity.""" - def __init__(self, client, data, entity_id): - """Create the Calendar event device.""" + def __init__(self, client, data, entity_id) -> None: + """Create the Calendar entity.""" self.student_info = data["student_info"] - self.data = VulcanCalendarData( - client, - self.student_info, - self.hass, - ) - self._event = None + self._event: CalendarEvent | None = None + self.client = client self.entity_id = entity_id self._unique_id = f"vulcan_calendar_{self.student_info['id']}" - - if data["students_number"] == 1: - self._attr_name = "Vulcan calendar" - self.device_name = "Calendar" - else: - self._attr_name = f"Vulcan calendar - {self.student_info['full_name']}" - self.device_name = f"{self.student_info['full_name']}: Calendar" + self._attr_name = f"Vulcan calendar - {self.student_info['full_name']}" self._attr_unique_id = f"vulcan_calendar_{self.student_info['id']}" self._attr_device_info = { "identifiers": {(DOMAIN, f"calendar_{self.student_info['id']}")}, "entry_type": DeviceEntryType.SERVICE, - "name": self.device_name, + "name": f"{self.student_info['full_name']}: Calendar", "model": f"{self.student_info['full_name']} - {self.student_info['class']} {self.student_info['school']}", "manufacturer": "Uonet +", "configuration_url": f"https://uonetplus.vulcan.net.pl/{self.student_info['symbol']}", } @property - def event(self): + def event(self) -> CalendarEvent | None: """Return the next upcoming event.""" return self._event - async def async_get_events(self, hass, start_date, end_date): - """Get all events in a specific time frame.""" - return await self.data.async_get_events(hass, start_date, end_date) - - async def async_update(self): - """Update event data.""" - await self.data.async_update() - event = copy.deepcopy(self.data.event) - if event is None: - self._event = event - return - event["start"] = { - "dateTime": datetime.combine(event["date"], event["time"].from_) - .astimezone(dt.DEFAULT_TIME_ZONE) - .isoformat() - } - event["end"] = { - "dateTime": datetime.combine(event["date"], event["time"].to) - .astimezone(dt.DEFAULT_TIME_ZONE) - .isoformat() - } - self._event = event - - -class VulcanCalendarData: - """Class to utilize calendar service object to get next event.""" - - MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=DEFAULT_SCAN_INTERVAL) - - def __init__(self, client, student_info, hass): - """Set up how we are going to search the Vulcan calendar.""" - self.client = client - self.event = None - self.hass = hass - self.student_info = student_info - self._available = True - - async def async_get_events(self, hass, start_date, end_date): + async def async_get_events(self, hass, start_date, end_date) -> list[CalendarEvent]: """Get all events in a specific time frame.""" try: events = await get_lessons( @@ -136,16 +86,12 @@ class VulcanCalendarData: date_from=start_date, date_to=end_date, ) - except VulcanAPIException as err: - if str(err) == "The certificate is not authorized.": - _LOGGER.error( - "The certificate is not authorized, please authorize integration again" - ) - raise ConfigEntryAuthFailed from err - _LOGGER.error("An API error has occurred: %s", err) - events = [] + except UnauthorizedCertificateException as err: + raise ConfigEntryAuthFailed( + "The certificate is not authorized, please authorize integration again" + ) from err except ClientConnectorError as err: - if self._available: + if self.available: _LOGGER.warning( "Connection error - please check your internet connection: %s", err ) @@ -153,37 +99,27 @@ class VulcanCalendarData: event_list = [] for item in events: - event = { - "uid": item["id"], - "start": { - "dateTime": datetime.combine( - item["date"], item["time"].from_ - ).strftime(DATE_STR_FORMAT) - }, - "end": { - "dateTime": datetime.combine( - item["date"], item["time"].to - ).strftime(DATE_STR_FORMAT) - }, - "summary": item["lesson"], - "location": item["room"], - "description": item["teacher"], - } + event = CalendarEvent( + start=datetime.combine(item["date"], item["time"].from_), + end=datetime.combine(item["date"], item["time"].to), + summary=item["lesson"], + location=item["room"], + description=item["teacher"], + ) event_list.append(event) return event_list - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self): + async def async_update(self) -> None: """Get the latest data.""" try: events = await get_lessons(self.client) - if not self._available: + if not self.available: _LOGGER.info("Restored connection with API") - self._available = True + self._attr_available = True if events == []: events = await get_lessons( @@ -191,22 +127,18 @@ class VulcanCalendarData: date_to=date.today() + timedelta(days=7), ) if events == []: - self.event = None + self._event = None return - except VulcanAPIException as err: - if str(err) == "The certificate is not authorized.": - _LOGGER.error( - "The certificate is not authorized, please authorize integration again" - ) - raise ConfigEntryAuthFailed from err - _LOGGER.error("An API error has occurred: %s", err) - return + except UnauthorizedCertificateException as err: + raise ConfigEntryAuthFailed( + "The certificate is not authorized, please authorize integration again" + ) from err except ClientConnectorError as err: - if self._available: + if self.available: _LOGGER.warning( "Connection error - please check your internet connection: %s", err ) - self._available = False + self._attr_available = False return new_event = min( @@ -216,11 +148,10 @@ class VulcanCalendarData: abs(datetime.combine(d["date"], d["time"].to) - datetime.now()), ), ) - self.event = { - "uid": new_event["id"], - "date": new_event["date"], - "time": new_event["time"], - "summary": new_event["lesson"], - "location": new_event["room"], - "description": new_event["teacher"], - } + self._event = CalendarEvent( + start=datetime.combine(new_event["date"], new_event["time"].from_), + end=datetime.combine(new_event["date"], new_event["time"].to), + summary=new_event["lesson"], + location=new_event["room"], + description=new_event["teacher"], + ) diff --git a/homeassistant/components/vulcan/config_flow.py b/homeassistant/components/vulcan/config_flow.py index ef700560d73..09acb13ea27 100644 --- a/homeassistant/components/vulcan/config_flow.py +++ b/homeassistant/components/vulcan/config_flow.py @@ -3,16 +3,22 @@ import logging from aiohttp import ClientConnectionError import voluptuous as vol -from vulcan import Account, Keystore, Vulcan -from vulcan._utils import VulcanAPIException +from vulcan import ( + Account, + ExpiredTokenException, + InvalidPINException, + InvalidSymbolException, + InvalidTokenException, + Keystore, + UnauthorizedCertificateException, + Vulcan, +) from homeassistant import config_entries -from homeassistant.const import CONF_PIN, CONF_REGION, CONF_SCAN_INTERVAL, CONF_TOKEN -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_PIN, CONF_REGION, CONF_TOKEN +from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import DOMAIN -from .const import DEFAULT_SCAN_INTERVAL from .register import register _LOGGER = logging.getLogger(__name__) @@ -29,11 +35,11 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - @staticmethod - @callback - def async_get_options_flow(config_entry): - """Get the options flow for this handler.""" - return VulcanOptionsFlowHandler(config_entry) + def __init__(self): + """Initialize config flow.""" + self.account = None + self.keystore = None + self.students = None async def async_step_user(self, user_input=None): """Handle config flow.""" @@ -53,22 +59,14 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_REGION], user_input[CONF_PIN], ) - except VulcanAPIException as err: - if str(err) == "Invalid token!" or str(err) == "Invalid token.": - errors = {"base": "invalid_token"} - elif str(err) == "Expired token.": - errors = {"base": "expired_token"} - elif str(err) == "Invalid PIN.": - errors = {"base": "invalid_pin"} - else: - errors = {"base": "unknown"} - _LOGGER.error(err) - except RuntimeError as err: - if str(err) == "Internal Server Error (ArgumentException)": - errors = {"base": "invalid_symbol"} - else: - errors = {"base": "unknown"} - _LOGGER.error(err) + except InvalidSymbolException: + errors = {"base": "invalid_symbol"} + except InvalidTokenException: + errors = {"base": "invalid_token"} + except InvalidPINException: + errors = {"base": "invalid_pin"} + except ExpiredTokenException: + errors = {"base": "expired_token"} except ClientConnectionError as err: errors = {"base": "cannot_connect"} _LOGGER.error("Connection error: %s", err) @@ -78,12 +76,10 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not errors: account = credentials["account"] keystore = credentials["keystore"] - client = Vulcan(keystore, account) + client = Vulcan(keystore, account, async_get_clientsession(self.hass)) students = await client.get_students() - await client.close() if len(students) > 1: - # pylint:disable=attribute-defined-outside-init self.account = account self.keystore = keystore self.students = students @@ -109,10 +105,10 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_select_student(self, user_input=None): """Allow user to select student.""" errors = {} - students_list = {} + students = {} if self.students is not None: for student in self.students: - students_list[ + students[ str(student.pupil.id) ] = f"{student.pupil.first_name} {student.pupil.last_name}" if user_input is not None: @@ -120,7 +116,7 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(str(student_id)) self._abort_if_unique_id_configured() return self.async_create_entry( - title=students_list[student_id], + title=students[student_id], data={ "student_id": str(student_id), "keystore": self.keystore.as_dict, @@ -128,37 +124,30 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) - data_schema = { - vol.Required( - "student", - ): vol.In(students_list), - } return self.async_show_form( step_id="select_student", - data_schema=vol.Schema(data_schema), + data_schema=vol.Schema({vol.Required("student"): vol.In(students)}), errors=errors, ) async def async_step_select_saved_credentials(self, user_input=None, errors=None): """Allow user to select saved credentials.""" - credentials_list = {} + + credentials = {} for entry in self.hass.config_entries.async_entries(DOMAIN): - credentials_list[entry.entry_id] = entry.data["account"]["UserName"] + credentials[entry.entry_id] = entry.data["account"]["UserName"] if user_input is not None: entry = self.hass.config_entries.async_get_entry(user_input["credentials"]) keystore = Keystore.load(entry.data["keystore"]) account = Account.load(entry.data["account"]) - client = Vulcan(keystore, account) + client = Vulcan(keystore, account, async_get_clientsession(self.hass)) try: students = await client.get_students() - except VulcanAPIException as err: - if str(err) == "The certificate is not authorized.": - return await self.async_step_auth( - errors={"base": "expired_credentials"} - ) - _LOGGER.error(err) - return await self.async_step_auth(errors={"base": "unknown"}) + except UnauthorizedCertificateException: + return await self.async_step_auth( + errors={"base": "expired_credentials"} + ) except ClientConnectionError as err: _LOGGER.error("Connection error: %s", err) return await self.async_step_select_saved_credentials( @@ -167,8 +156,6 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") return await self.async_step_auth(errors={"base": "unknown"}) - finally: - await client.close() if len(students) == 1: student = students[0] await self.async_set_unique_id(str(student.pupil.id)) @@ -181,7 +168,6 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): "account": account.as_dict, }, ) - # pylint:disable=attribute-defined-outside-init self.account = account self.keystore = keystore self.students = students @@ -190,7 +176,7 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema = { vol.Required( "credentials", - ): vol.In(credentials_list), + ): vol.In(credentials), } return self.async_show_form( step_id="select_saved_credentials", @@ -200,46 +186,46 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_add_next_config_entry(self, user_input=None): """Flow initialized when user is adding next entry of that integration.""" + existing_entries = [] for entry in self.hass.config_entries.async_entries(DOMAIN): existing_entries.append(entry) errors = {} + if user_input is not None: - if user_input["use_saved_credentials"]: - if len(existing_entries) == 1: - keystore = Keystore.load(existing_entries[0].data["keystore"]) - account = Account.load(existing_entries[0].data["account"]) - client = Vulcan(keystore, account) - students = await client.get_students() - await client.close() - new_students = [] - existing_entry_ids = [] - for entry in self.hass.config_entries.async_entries(DOMAIN): - existing_entry_ids.append(entry.data["student_id"]) - for student in students: - if str(student.pupil.id) not in existing_entry_ids: - new_students.append(student) - if not new_students: - return self.async_abort(reason="all_student_already_configured") - if len(new_students) == 1: - await self.async_set_unique_id(str(new_students[0].pupil.id)) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title=f"{new_students[0].pupil.first_name} {new_students[0].pupil.last_name}", - data={ - "student_id": str(new_students[0].pupil.id), - "keystore": keystore.as_dict, - "account": account.as_dict, - }, - ) - # pylint:disable=attribute-defined-outside-init - self.account = account - self.keystore = keystore - self.students = new_students - return await self.async_step_select_student() + if not user_input["use_saved_credentials"]: + return await self.async_step_auth() + if len(existing_entries) > 1: return await self.async_step_select_saved_credentials() - return await self.async_step_auth() + keystore = Keystore.load(existing_entries[0].data["keystore"]) + account = Account.load(existing_entries[0].data["account"]) + client = Vulcan(keystore, account, async_get_clientsession(self.hass)) + students = await client.get_students() + new_students = [] + existing_entry_ids = [] + for entry in self.hass.config_entries.async_entries(DOMAIN): + existing_entry_ids.append(entry.data["student_id"]) + for student in students: + if str(student.pupil.id) not in existing_entry_ids: + new_students.append(student) + if not new_students: + return self.async_abort(reason="all_student_already_configured") + if len(new_students) == 1: + await self.async_set_unique_id(str(new_students[0].pupil.id)) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=f"{new_students[0].pupil.first_name} {new_students[0].pupil.last_name}", + data={ + "student_id": str(new_students[0].pupil.id), + "keystore": keystore.as_dict, + "account": account.as_dict, + }, + ) + self.account = account + self.keystore = keystore + self.students = new_students + return await self.async_step_select_student() data_schema = { vol.Required("use_saved_credentials", default=True): bool, @@ -251,6 +237,10 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_reauth(self, user_input=None): + """Perform reauth upon an API authentication error.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): """Reauthorize integration.""" errors = {} if user_input is not None: @@ -261,22 +251,14 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_REGION], user_input[CONF_PIN], ) - except VulcanAPIException as err: - if str(err) == "Invalid token!" or str(err) == "Invalid token.": - errors["base"] = "invalid_token" - elif str(err) == "Expired token.": - errors["base"] = "expired_token" - elif str(err) == "Invalid PIN.": - errors["base"] = "invalid_pin" - else: - errors["base"] = "unknown" - _LOGGER.error(err) - except RuntimeError as err: - if str(err) == "Internal Server Error (ArgumentException)": - errors["base"] = "invalid_symbol" - else: - errors["base"] = "unknown" - _LOGGER.error(err) + except InvalidSymbolException: + errors = {"base": "invalid_symbol"} + except InvalidTokenException: + errors = {"base": "invalid_token"} + except InvalidPINException: + errors = {"base": "invalid_pin"} + except ExpiredTokenException: + errors = {"base": "expired_token"} except ClientConnectionError as err: errors["base"] = "cannot_connect" _LOGGER.error("Connection error: %s", err) @@ -286,12 +268,12 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not errors: account = credentials["account"] keystore = credentials["keystore"] - client = Vulcan(keystore, account) + client = Vulcan(keystore, account, async_get_clientsession(self.hass)) students = await client.get_students() - await client.close() existing_entries = [] for entry in self.hass.config_entries.async_entries(DOMAIN): existing_entries.append(entry) + matching_entries = False for student in students: for entry in existing_entries: if str(student.pupil.id) == str(entry.data["student_id"]): @@ -305,38 +287,13 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) await self.hass.config_entries.async_reload(entry.entry_id) + matching_entries = True + if not matching_entries: + return self.async_abort(reason="no_matching_entries") return self.async_abort(reason="reauth_successful") return self.async_show_form( - step_id="reauth", + step_id="reauth_confirm", data_schema=vol.Schema(LOGIN_SCHEMA), errors=errors, ) - - -class VulcanOptionsFlowHandler(config_entries.OptionsFlow): - """Config flow options for Uonet+ Vulcan.""" - - def __init__(self, config_entry): - """Initialize options flow.""" - 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) - - options = { - vol.Optional( - CONF_SCAN_INTERVAL, - default=self.config_entry.options.get( - CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL - ), - ): cv.positive_int, - } - - return self.async_show_form( - step_id="init", data_schema=vol.Schema(options), errors=errors - ) diff --git a/homeassistant/components/vulcan/const.py b/homeassistant/components/vulcan/const.py index 938cc4df8cd..4f17d43c342 100644 --- a/homeassistant/components/vulcan/const.py +++ b/homeassistant/components/vulcan/const.py @@ -1,4 +1,3 @@ """Constants for the Vulcan integration.""" DOMAIN = "vulcan" -DEFAULT_SCAN_INTERVAL = 5 diff --git a/homeassistant/components/vulcan/fetch_data.py b/homeassistant/components/vulcan/fetch_data.py index 04da8d125d7..c706bfa805f 100644 --- a/homeassistant/components/vulcan/fetch_data.py +++ b/homeassistant/components/vulcan/fetch_data.py @@ -94,4 +94,5 @@ async def get_student_info(client, student_id): student_info["class"] = student.class_ student_info["school"] = student.school.name student_info["symbol"] = student.symbol + break return student_info diff --git a/homeassistant/components/vulcan/manifest.json b/homeassistant/components/vulcan/manifest.json index 6449f07a3fb..b7f1f5fe4d6 100644 --- a/homeassistant/components/vulcan/manifest.json +++ b/homeassistant/components/vulcan/manifest.json @@ -3,9 +3,8 @@ "name": "Uonet+ Vulcan", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/vulcan", - "requirements": ["vulcan-api==2.0.3"], - "dependencies": [], + "requirements": ["vulcan-api==2.1.1"], "codeowners": ["@Antoni-Czaplicki"], "iot_class": "cloud_polling", - "quality_scale": "platinum" + "quality_scale": "silver" } diff --git a/homeassistant/components/vulcan/register.py b/homeassistant/components/vulcan/register.py index 802805d1db8..67cceb8d7b8 100644 --- a/homeassistant/components/vulcan/register.py +++ b/homeassistant/components/vulcan/register.py @@ -1,13 +1,10 @@ """Support for register Vulcan account.""" -from functools import partial from vulcan import Account, Keystore async def register(hass, token, symbol, pin): """Register integration and save credentials.""" - keystore = await hass.async_add_executor_job( - partial(Keystore.create, device_model="Home Assistant") - ) + keystore = await Keystore.create(device_model="Home Assistant") account = await Account.register(keystore, token, symbol, pin) return {"account": account, "keystore": keystore} diff --git a/homeassistant/components/vulcan/strings.json b/homeassistant/components/vulcan/strings.json index abb3dce7c7f..bb9e1d4d848 100644 --- a/homeassistant/components/vulcan/strings.json +++ b/homeassistant/components/vulcan/strings.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "That student has already been added.", "all_student_already_configured": "All students have already been added.", - "reauth_successful": "Reauth successful" + "reauth_successful": "Reauth successful", + "no_matching_entries": "No matching entries found, please use different account or remove integration with outdated student.." }, "error": { "unknown": "Unknown error occurred", @@ -23,7 +24,7 @@ "pin": "Pin" } }, - "reauth": { + "reauth_confirm": { "description": "Login to your Vulcan Account using mobile app registration page.", "data": { "token": "Token", @@ -50,20 +51,5 @@ } } } - }, - "options": { - "error": { - "error": "Error occurred" - }, - "step": { - "init": { - "data": { - "message_notify": "Show notifications when new message received", - "attendance_notify": "Show notifications about the latest attendance entries", - "grade_notify": "Show notifications about the latest grades", - "scan_interval": "Update interval (in minutes)" - } - } - } } } diff --git a/requirements_all.txt b/requirements_all.txt index 7faa9dad813..53712397c4c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2396,7 +2396,7 @@ vsure==1.7.3 vtjp==0.1.14 # homeassistant.components.vulcan -vulcan-api==2.0.3 +vulcan-api==2.1.1 # homeassistant.components.vultr vultr==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 89192ad4f2f..78f7da53d35 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1563,7 +1563,7 @@ vilfo-api-client==0.3.2 vsure==1.7.3 # homeassistant.components.vulcan -vulcan-api==2.0.3 +vulcan-api==2.1.1 # homeassistant.components.vultr vultr==0.1.2 diff --git a/tests/components/vulcan/test_config_flow.py b/tests/components/vulcan/test_config_flow.py index 91f0dd769c8..20f030bb99f 100644 --- a/tests/components/vulcan/test_config_flow.py +++ b/tests/components/vulcan/test_config_flow.py @@ -3,17 +3,20 @@ import json from unittest import mock from unittest.mock import patch -from vulcan import Account +from vulcan import ( + Account, + ExpiredTokenException, + InvalidPINException, + InvalidSymbolException, + InvalidTokenException, + UnauthorizedCertificateException, +) from vulcan.model import Student from homeassistant import config_entries, data_entry_flow from homeassistant.components.vulcan import config_flow, const, register -from homeassistant.components.vulcan.config_flow import ( - ClientConnectionError, - Keystore, - VulcanAPIException, -) -from homeassistant.const import CONF_PIN, CONF_REGION, CONF_SCAN_INTERVAL, CONF_TOKEN +from homeassistant.components.vulcan.config_flow import ClientConnectionError, Keystore +from homeassistant.const import CONF_PIN, CONF_REGION, CONF_TOKEN from tests.common import MockConfigEntry, load_fixture @@ -56,13 +59,20 @@ async def test_config_flow_auth_success( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "auth" assert result["errors"] is None - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, - ) + + with patch( + "homeassistant.components.vulcan.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, + ) + await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "Jan Kowalski" + assert len(mock_setup_entry.mock_calls) == 1 @mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students") @@ -96,13 +106,18 @@ async def test_config_flow_auth_success_with_multiple_students( assert result["step_id"] == "select_student" assert result["errors"] == {} - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"student": "0"}, - ) + with patch( + "homeassistant.components.vulcan.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"student": "0"}, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "Jan Kowalski" + assert len(mock_setup_entry.mock_calls) == 1 @mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students") @@ -120,14 +135,53 @@ async def test_config_flow_reauth_success( MockConfigEntry( domain=const.DOMAIN, unique_id="0", - data={"student_id": "0", "login": "example@example.com"}, + data={"student_id": "0"}, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {} + + with patch( + "homeassistant.components.vulcan.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert len(mock_setup_entry.mock_calls) == 1 + + +@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students") +@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create") +@mock.patch("homeassistant.components.vulcan.config_flow.Account.register") +async def test_config_flow_reauth_without_matching_entries( + mock_account, mock_keystore, mock_student, hass +): + """Test a aborted config flow reauth caused by leak of matching entries.""" + mock_keystore.return_value = fake_keystore + mock_account.return_value = fake_account + mock_student.return_value = [ + Student.load(load_fixture("fake_student_1.json", "vulcan")) + ] + MockConfigEntry( + domain=const.DOMAIN, + unique_id="0", + data={"student_id": "1"}, + ).add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( @@ -136,7 +190,7 @@ async def test_config_flow_reauth_success( ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "reauth_successful" + assert result["reason"] == "no_matching_entries" @mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create") @@ -149,11 +203,11 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" + assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} with patch( "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=VulcanAPIException("Invalid token."), + side_effect=InvalidTokenException, ): result = await hass.config_entries.flow.async_configure( @@ -162,12 +216,12 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" + assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_token"} with patch( "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=VulcanAPIException("Expired token."), + side_effect=ExpiredTokenException, ): result = await hass.config_entries.flow.async_configure( @@ -176,12 +230,12 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" + assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "expired_token"} with patch( "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=VulcanAPIException("Invalid PIN."), + side_effect=InvalidPINException, ): result = await hass.config_entries.flow.async_configure( @@ -190,12 +244,12 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" + assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_pin"} with patch( "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=VulcanAPIException("Unknown error"), + side_effect=InvalidSymbolException, ): result = await hass.config_entries.flow.async_configure( @@ -204,37 +258,9 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" - assert result["errors"] == {"base": "unknown"} - - with patch( - "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=RuntimeError("Internal Server Error (ArgumentException)"), - ): - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" + assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_symbol"} - with patch( - "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=RuntimeError("Unknown error"), - ): - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" - assert result["errors"] == {"base": "unknown"} - with patch( "homeassistant.components.vulcan.config_flow.Account.register", side_effect=ClientConnectionError, @@ -246,7 +272,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" + assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "cannot_connect"} with patch( @@ -260,7 +286,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" + assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "unknown"} @@ -297,13 +323,18 @@ async def test_multiple_config_entries(mock_account, mock_keystore, mock_student assert result["step_id"] == "auth" assert result["errors"] is None - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, - ) + with patch( + "homeassistant.components.vulcan.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "Jan Kowalski" + assert len(mock_setup_entry.mock_calls) == 2 @mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students") @@ -326,13 +357,18 @@ async def test_multiple_config_entries_using_saved_credentials(mock_student, has assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"use_saved_credentials": True}, - ) + with patch( + "homeassistant.components.vulcan.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"use_saved_credentials": True}, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "Jan Kowalski" + assert len(mock_setup_entry.mock_calls) == 2 @mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students") @@ -364,13 +400,18 @@ async def test_multiple_config_entries_using_saved_credentials_2(mock_student, h assert result["step_id"] == "select_student" assert result["errors"] == {} - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"student": "0"}, - ) + with patch( + "homeassistant.components.vulcan.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"student": "0"}, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "Jan Kowalski" + assert len(mock_setup_entry.mock_calls) == 2 @mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students") @@ -410,13 +451,18 @@ async def test_multiple_config_entries_using_saved_credentials_3(mock_student, h assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"credentials": "123"}, - ) + with patch( + "homeassistant.components.vulcan.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"credentials": "123"}, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "Jan Kowalski" + assert len(mock_setup_entry.mock_calls) == 3 @mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students") @@ -465,13 +511,18 @@ async def test_multiple_config_entries_using_saved_credentials_4(mock_student, h assert result["step_id"] == "select_student" assert result["errors"] == {} - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"student": "0"}, - ) + with patch( + "homeassistant.components.vulcan.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"student": "0"}, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "Jan Kowalski" + assert len(mock_setup_entry.mock_calls) == 3 async def test_multiple_config_entries_without_valid_saved_credentials(hass): @@ -504,7 +555,7 @@ async def test_multiple_config_entries_without_valid_saved_credentials(hass): ) with patch( "homeassistant.components.vulcan.config_flow.Vulcan.get_students", - side_effect=VulcanAPIException("The certificate is not authorized."), + side_effect=UnauthorizedCertificateException, ): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "select_saved_credentials" @@ -614,54 +665,6 @@ async def test_multiple_config_entries_using_saved_credentials_with_unknown_erro assert result["errors"] == {"base": "unknown"} -async def test_multiple_config_entries_using_saved_credentials_with_unknown_api_error( - hass, -): - """Test a unsuccessful config flow for multiple config entries without valid saved credentials.""" - MockConfigEntry( - entry_id="456", - domain=const.DOMAIN, - unique_id="234567", - data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan")) - | {"student_id": "456"}, - ).add_to_hass(hass) - MockConfigEntry( - entry_id="123", - domain=const.DOMAIN, - unique_id="123456", - data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan")), - ).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "add_next_config_entry" - assert result["errors"] == {} - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"use_saved_credentials": True}, - ) - with patch( - "homeassistant.components.vulcan.config_flow.Vulcan.get_students", - side_effect=VulcanAPIException("Unknown error"), - ): - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "select_saved_credentials" - assert result["errors"] is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {"credentials": "123"}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "auth" - assert result["errors"] == {"base": "unknown"} - - @mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students") @mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create") @mock.patch("homeassistant.components.vulcan.config_flow.Account.register") @@ -704,7 +707,7 @@ async def test_config_flow_auth_invalid_token(mock_keystore, hass): mock_keystore.return_value = fake_keystore with patch( "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=VulcanAPIException("Invalid token."), + side_effect=InvalidTokenException, ): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -730,7 +733,7 @@ async def test_config_flow_auth_invalid_region(mock_keystore, hass): mock_keystore.return_value = fake_keystore with patch( "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=RuntimeError("Internal Server Error (ArgumentException)"), + side_effect=InvalidSymbolException, ): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -756,7 +759,7 @@ async def test_config_flow_auth_invalid_pin(mock_keystore, hass): mock_keystore.return_value = fake_keystore with patch( "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=VulcanAPIException("Invalid PIN."), + side_effect=InvalidPINException, ): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -782,7 +785,7 @@ async def test_config_flow_auth_expired_token(mock_keystore, hass): mock_keystore.return_value = fake_keystore with patch( "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=VulcanAPIException("Expired token."), + side_effect=ExpiredTokenException, ): result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -802,58 +805,6 @@ async def test_config_flow_auth_expired_token(mock_keystore, hass): assert result["errors"] == {"base": "expired_token"} -@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create") -async def test_config_flow_auth_api_unknown_error(mock_keystore, hass): - """Test a config flow with unknown API error.""" - mock_keystore.return_value = fake_keystore - with patch( - "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=VulcanAPIException("Unknown error"), - ): - result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "auth" - assert result["errors"] is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "auth" - assert result["errors"] == {"base": "unknown"} - - -@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create") -async def test_config_flow_auth_api_unknown_runtime_error(mock_keystore, hass): - """Test a config flow with runtime error.""" - mock_keystore.return_value = fake_keystore - with patch( - "homeassistant.components.vulcan.config_flow.Account.register", - side_effect=RuntimeError("Unknown error"), - ): - result = await hass.config_entries.flow.async_init( - const.DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "auth" - assert result["errors"] is None - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "auth" - assert result["errors"] == {"base": "unknown"} - - @mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create") async def test_config_flow_auth_connection_error(mock_keystore, hass): """Test a config flow with connection error.""" @@ -904,32 +855,3 @@ async def test_config_flow_auth_unknown_error(mock_keystore, hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "unknown"} - - -@mock.patch("homeassistant.components.vulcan.Vulcan.get_students") -async def test_options_flow(mock_student, hass): - """Test config flow options.""" - mock_student.return_value = [ - Student.load(load_fixture("fake_student_1.json", "vulcan")) - ] - config_entry = MockConfigEntry( - domain=const.DOMAIN, - unique_id="0", - data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan")), - ) - 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={CONF_SCAN_INTERVAL: 2137} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert config_entry.options == {CONF_SCAN_INTERVAL: 2137} From 8c89f68d2c4695dd963d31f9b7b1943059694166 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 5 May 2022 17:40:56 +0200 Subject: [PATCH 0225/3516] Bump library version (#71349) --- 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 2b62500f23b..16231ef0b88 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.2.2"], + "requirements": ["nettigo-air-monitor==1.2.3"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 53712397c4c..4a7f5e25e46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1068,7 +1068,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.2.2 +nettigo-air-monitor==1.2.3 # homeassistant.components.neurio_energy neurio==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 78f7da53d35..8d1f5da4bbb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -730,7 +730,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.2.2 +nettigo-air-monitor==1.2.3 # homeassistant.components.nexia nexia==0.9.13 From e3433008a25d59e057e1d75679babc4ec9932ae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 5 May 2022 19:37:32 +0300 Subject: [PATCH 0226/3516] Upgrade huawei-lte-api to 1.6.0, adapt to it (#71041) * Upgrade huawei-lte-api to 1.6.0, adapt to it https://github.com/Salamek/huawei-lte-api/releases/tag/1.5 https://github.com/Salamek/huawei-lte-api/releases/tag/1.5.1 https://github.com/Salamek/huawei-lte-api/releases/tag/1.5.2 https://github.com/Salamek/huawei-lte-api/releases/tag/1.5.3 https://github.com/Salamek/huawei-lte-api/releases/tag/1.5.4 https://github.com/Salamek/huawei-lte-api/releases/tag/1.6 * Fix logout on config flow Co-authored-by: Antonino Piazza --- homeassistant/components/huawei_lte/__init__.py | 16 +++++++++------- .../components/huawei_lte/config_flow.py | 12 +++++------- .../components/huawei_lte/manifest.json | 2 +- homeassistant/components/huawei_lte/utils.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/huawei_lte/test_config_flow.py | 2 +- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index ca15731641d..d8afbd752a4 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -10,9 +10,9 @@ import logging import time from typing import Any, NamedTuple, cast -from huawei_lte_api.AuthorizedConnection import AuthorizedConnection from huawei_lte_api.Client import Client from huawei_lte_api.Connection import Connection +from huawei_lte_api.enums.device import ControlModeEnum from huawei_lte_api.exceptions import ( ResponseErrorException, ResponseErrorLoginRequiredException, @@ -186,9 +186,12 @@ class Router: try: self.data[key] = func() except ResponseErrorLoginRequiredException: - if isinstance(self.connection, AuthorizedConnection): + if not self.config_entry.options.get(CONF_UNAUTHENTICATED_MODE): _LOGGER.debug("Trying to authorize again") - if self.connection.enforce_authorized_connection(): + if self.client.user.login( + self.config_entry.data.get(CONF_USERNAME, ""), + self.config_entry.data.get(CONF_PASSWORD, ""), + ): _LOGGER.debug( "success, %s will be updated by a future periodic run", key, @@ -276,8 +279,6 @@ class Router: def logout(self) -> None: """Log out router session.""" - if not isinstance(self.connection, AuthorizedConnection): - return try: self.client.user.logout() except ResponseErrorNotSupportedException: @@ -293,6 +294,7 @@ class Router: self.subscriptions.clear() self.logout() + self.connection.requests_session.close() class HuaweiLteData(NamedTuple): @@ -315,7 +317,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("Connecting in authenticated mode, full feature set") username = entry.data.get(CONF_USERNAME) or "" password = entry.data.get(CONF_PASSWORD) or "" - connection = AuthorizedConnection( + connection = Connection( url, username=username, password=password, timeout=CONNECTION_TIMEOUT ) return connection @@ -515,7 +517,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if router.suspended: _LOGGER.debug("%s: ignored, integration suspended", service.service) return - result = router.client.device.reboot() + result = router.client.device.set_control(ControlModeEnum.REBOOT) _LOGGER.debug("%s: %s", service.service, result) elif service.service == SERVICE_RESUME_INTEGRATION: # Login will be handled automatically on demand diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index f4ea7cd86f7..3dfd38d6304 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -5,9 +5,9 @@ import logging from typing import TYPE_CHECKING, Any from urllib.parse import urlparse -from huawei_lte_api.AuthorizedConnection import AuthorizedConnection from huawei_lte_api.Client import Client -from huawei_lte_api.Connection import GetResponseType +from huawei_lte_api.Connection import Connection +from huawei_lte_api.Session import GetResponseType from huawei_lte_api.exceptions import ( LoginErrorPasswordWrongException, LoginErrorUsernamePasswordOverrunException, @@ -108,19 +108,17 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input=user_input, errors=errors ) - conn: AuthorizedConnection - def logout() -> None: try: - conn.user.logout() + conn.user_session.user.logout() # type: ignore[union-attr] except Exception: # pylint: disable=broad-except _LOGGER.debug("Could not logout", exc_info=True) - def try_connect(user_input: dict[str, Any]) -> AuthorizedConnection: + def try_connect(user_input: dict[str, Any]) -> Connection: """Try connecting with given credentials.""" username = user_input.get(CONF_USERNAME) or "" password = user_input.get(CONF_PASSWORD) or "" - conn = AuthorizedConnection( + conn = Connection( user_input[CONF_URL], username=username, password=password, diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 3e7ebf24b16..dd0382d5f55 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ - "huawei-lte-api==1.4.18", + "huawei-lte-api==1.6.0", "stringcase==1.2.0", "url-normalize==1.4.1" ], diff --git a/homeassistant/components/huawei_lte/utils.py b/homeassistant/components/huawei_lte/utils.py index 69b346a58f4..37f9f1a5542 100644 --- a/homeassistant/components/huawei_lte/utils.py +++ b/homeassistant/components/huawei_lte/utils.py @@ -1,7 +1,7 @@ """Utilities for the Huawei LTE integration.""" from __future__ import annotations -from huawei_lte_api.Connection import GetResponseType +from huawei_lte_api.Session import GetResponseType from homeassistant.helpers.device_registry import format_mac diff --git a/requirements_all.txt b/requirements_all.txt index 4a7f5e25e46..eb0b504bdaa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -837,7 +837,7 @@ horimote==0.4.1 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.18 +huawei-lte-api==1.6.0 # homeassistant.components.huisbaasje huisbaasje-client==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d1f5da4bbb..01f4a2d339a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -595,7 +595,7 @@ homepluscontrol==0.0.5 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.18 +huawei-lte-api==1.6.0 # homeassistant.components.huisbaasje huisbaasje-client==0.1.0 diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index 52b46c57b98..b363836cc4f 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -127,7 +127,7 @@ def login_requests_mock(requests_mock): LoginErrorEnum.USERNAME_PWD_WRONG, {CONF_USERNAME: "invalid_auth"}, ), - (LoginErrorEnum.USERNAME_PWD_ORERRUN, {"base": "login_attempts_exceeded"}), + (LoginErrorEnum.USERNAME_PWD_OVERRUN, {"base": "login_attempts_exceeded"}), (ResponseCodeEnum.ERROR_SYSTEM_UNKNOWN, {"base": "response_error"}), ), ) From 8a41370950f951910c89f83089c3ce5285f3b845 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 May 2022 13:13:55 -0400 Subject: [PATCH 0227/3516] Add cache to split_entity_id (#71345) --- homeassistant/core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/core.py b/homeassistant/core.py index d6b1ac85d8b..b181e4c4106 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -133,9 +133,12 @@ SOURCE_YAML = ConfigSource.YAML.value # How long to wait until things that run on startup have to finish. TIMEOUT_EVENT_START = 15 +MAX_EXPECTED_ENTITY_IDS = 16384 + _LOGGER = logging.getLogger(__name__) +@functools.lru_cache(MAX_EXPECTED_ENTITY_IDS) def split_entity_id(entity_id: str) -> tuple[str, str]: """Split a state entity ID into domain and object ID.""" domain, _, object_id = entity_id.partition(".") From 203bebe668cd2cf0b4130c225527691977c9f932 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 5 May 2022 19:16:36 +0200 Subject: [PATCH 0228/3516] Include all non-numeric sensor events in logbook (#71331) --- homeassistant/components/logbook/__init__.py | 48 ++++++++--- tests/components/logbook/test_init.py | 85 +++++++++++++------- 2 files changed, 92 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 7bacbd4cdd7..1c930cf3004 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -26,6 +26,7 @@ from homeassistant.components.history import ( sqlalchemy_filter_from_include_exclude_conf, ) from homeassistant.components.http import HomeAssistantView +from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.models import ( EventData, @@ -36,6 +37,7 @@ from homeassistant.components.recorder.models import ( ) from homeassistant.components.recorder.util import session_scope from homeassistant.components.script import EVENT_SCRIPT_STARTED +from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( ATTR_DOMAIN, ATTR_ENTITY_ID, @@ -58,7 +60,7 @@ from homeassistant.core import ( split_entity_id, ) from homeassistant.exceptions import InvalidEntityFormatError -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, EntityFilter, @@ -79,7 +81,7 @@ DOMAIN_JSON_EXTRACT = re.compile('"domain": ?"([^"]+)"') ICON_JSON_EXTRACT = re.compile('"icon": ?"([^"]+)"') ATTR_MESSAGE = "message" -CONTINUOUS_DOMAINS = {"proximity", "sensor"} +CONTINUOUS_DOMAINS = {PROXIMITY_DOMAIN, SENSOR_DOMAIN} CONTINUOUS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in CONTINUOUS_DOMAINS] DOMAIN = "logbook" @@ -331,7 +333,6 @@ def humanify( """Generate a converted list of events into Entry objects. Will try to group events if possible: - - if 2+ sensor updates in GROUP_BY_MINUTES, show last - if Home Assistant stop and start happen in same minute call it restarted """ external_events = hass.data.get(DOMAIN, {}) @@ -343,8 +344,8 @@ def humanify( events_batch = list(g_events) - # Keep track of last sensor states - last_sensor_event = {} + # Continuous sensors, will be excluded from the logbook + continuous_sensors = {} # Group HA start/stop events # Maps minute of event to 1: stop, 2: stop + start @@ -353,8 +354,13 @@ def humanify( # Process events for event in events_batch: if event.event_type == EVENT_STATE_CHANGED: - if event.domain in CONTINUOUS_DOMAINS: - last_sensor_event[event.entity_id] = event + if event.domain != SENSOR_DOMAIN: + continue + entity_id = event.entity_id + if entity_id in continuous_sensors: + continue + assert entity_id is not None + continuous_sensors[entity_id] = _is_sensor_continuous(hass, entity_id) elif event.event_type == EVENT_HOMEASSISTANT_STOP: if event.time_fired_minute in start_stop_events: @@ -373,15 +379,12 @@ def humanify( if event.event_type == EVENT_STATE_CHANGED: entity_id = event.entity_id domain = event.domain + assert entity_id is not None - if ( - domain in CONTINUOUS_DOMAINS - and event != last_sensor_event[entity_id] - ): - # Skip all but the last sensor state + if domain == SENSOR_DOMAIN and continuous_sensors[entity_id]: + # Skip continuous sensors continue - assert entity_id is not None data = { "when": event.time_fired_isoformat, "name": _entity_name_from_event( @@ -812,6 +815,25 @@ def _entity_name_from_event( ) or split_entity_id(entity_id)[1].replace("_", " ") +def _is_sensor_continuous( + hass: HomeAssistant, + entity_id: str, +) -> bool: + """Determine if a sensor is continuous by checking its state class. + + Sensors with a unit_of_measurement are also considered continuous, but are filtered + already by the SQL query generated by _get_events + """ + registry = er.async_get(hass) + if not (entry := registry.async_get(entity_id)): + # Entity not registered, so can't have a state class + return False + return ( + entry.capabilities is not None + and entry.capabilities.get(ATTR_STATE_CLASS) is not None + ) + + class LazyEventPartialState: """A lazy version of core Event with limited State joined in.""" diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 68b9169224e..2612765584f 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -14,12 +14,14 @@ from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat from homeassistant.components.script import EVENT_SCRIPT_STARTED +from homeassistant.components.sensor import SensorStateClass from homeassistant.const import ( ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_NAME, ATTR_SERVICE, + ATTR_UNIT_OF_MEASUREMENT, CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, @@ -33,6 +35,7 @@ from homeassistant.const import ( STATE_ON, ) import homeassistant.core as ha +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component @@ -157,27 +160,51 @@ async def test_service_call_create_log_book_entry_no_message(hass_): assert len(calls) == 0 -def test_humanify_filter_sensor(hass_): - """Test humanify filter too frequent sensor values.""" - entity_id = "sensor.bla" +async def test_filter_sensor(hass_: ha.HomeAssistant, hass_client): + """Test numeric sensors are filtered.""" - pointA = dt_util.utcnow().replace(minute=2) - pointB = pointA.replace(minute=5) - pointC = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES) - entity_attr_cache = logbook.EntityAttributeCache(hass_) + registry = er.async_get(hass_) - eventA = create_state_changed_event(pointA, entity_id, 10) - eventB = create_state_changed_event(pointB, entity_id, 20) - eventC = create_state_changed_event(pointC, entity_id, 30) + # Unregistered sensor without a unit of measurement - should be in logbook + entity_id1 = "sensor.bla" + attributes_1 = None + # Unregistered sensor with a unit of measurement - should be excluded from logbook + entity_id2 = "sensor.blu" + attributes_2 = {ATTR_UNIT_OF_MEASUREMENT: "cats"} + # Registered sensor with state class - should be excluded from logbook + entity_id3 = registry.async_get_or_create( + "sensor", + "test", + "unique_3", + suggested_object_id="bli", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, + ).entity_id + attributes_3 = None + # Registered sensor without state class or unit - should be in logbook + entity_id4 = registry.async_get_or_create( + "sensor", "test", "unique_4", suggested_object_id="ble" + ).entity_id + attributes_4 = None - entries = list( - logbook.humanify(hass_, (eventA, eventB, eventC), entity_attr_cache, {}) - ) + hass_.states.async_set(entity_id1, None, attributes_1) # Excluded + hass_.states.async_set(entity_id1, 10, attributes_1) # Included + hass_.states.async_set(entity_id2, None, attributes_2) # Excluded + hass_.states.async_set(entity_id2, 10, attributes_2) # Excluded + hass_.states.async_set(entity_id3, None, attributes_3) # Excluded + hass_.states.async_set(entity_id3, 10, attributes_3) # Excluded + hass_.states.async_set(entity_id1, 20, attributes_1) # Included + hass_.states.async_set(entity_id2, 20, attributes_2) # Excluded + hass_.states.async_set(entity_id4, None, attributes_4) # Excluded + hass_.states.async_set(entity_id4, 10, attributes_4) # Included - assert len(entries) == 2 - assert_entry(entries[0], pointB, "bla", entity_id=entity_id) + await async_wait_recording_done(hass_) + client = await hass_client() + entries = await _async_fetch_logbook(client) - assert_entry(entries[1], pointC, "bla", entity_id=entity_id) + assert len(entries) == 3 + _assert_entry(entries[0], name="bla", entity_id=entity_id1, state="10") + _assert_entry(entries[1], name="bla", entity_id=entity_id1, state="20") + _assert_entry(entries[2], name="ble", entity_id=entity_id4, state="10") def test_home_assistant_start_stop_grouped(hass_): @@ -1790,12 +1817,13 @@ async def test_include_exclude_events(hass, hass_client, recorder_mock): client = await hass_client() entries = await _async_fetch_logbook(client) - assert len(entries) == 3 + assert len(entries) == 4 _assert_entry( entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN ) - _assert_entry(entries[1], name="blu", entity_id=entity_id2) - _assert_entry(entries[2], name="keep", entity_id=entity_id4) + _assert_entry(entries[1], name="blu", entity_id=entity_id2, state="10") + _assert_entry(entries[2], name="blu", entity_id=entity_id2, state="20") + _assert_entry(entries[3], name="keep", entity_id=entity_id4, state="10") async def test_include_exclude_events_with_glob_filters( @@ -1849,12 +1877,13 @@ async def test_include_exclude_events_with_glob_filters( client = await hass_client() entries = await _async_fetch_logbook(client) - assert len(entries) == 3 + assert len(entries) == 4 _assert_entry( entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN ) - _assert_entry(entries[1], name="blu", entity_id=entity_id2) - _assert_entry(entries[2], name="included", entity_id=entity_id4) + _assert_entry(entries[1], name="blu", entity_id=entity_id2, state="10") + _assert_entry(entries[2], name="blu", entity_id=entity_id2, state="20") + _assert_entry(entries[3], name="included", entity_id=entity_id4, state="30") async def test_empty_config(hass, hass_client, recorder_mock): @@ -1939,22 +1968,22 @@ def _assert_entry( entry, when=None, name=None, message=None, domain=None, entity_id=None, state=None ): """Assert an entry is what is expected.""" - if when: + if when is not None: assert when.isoformat() == entry["when"] - if name: + if name is not None: assert name == entry["name"] - if message: + if message is not None: assert message == entry["message"] - if domain: + if domain is not None: assert domain == entry["domain"] - if entity_id: + if entity_id is not None: assert entity_id == entry["entity_id"] - if state: + if state is not None: assert state == entry["state"] From d9a7c4a4839ba1468e018ef96861b03c3512a274 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 5 May 2022 20:04:00 +0200 Subject: [PATCH 0229/3516] Only lookup unknown Google Cast models once (#71348) Co-authored-by: Paulus Schoutsen --- homeassistant/components/cast/__init__.py | 4 +- homeassistant/components/cast/discovery.py | 2 +- homeassistant/components/cast/helpers.py | 47 +++++++- homeassistant/components/cast/manifest.json | 2 +- homeassistant/components/cast/media_player.py | 6 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cast/conftest.py | 11 ++ tests/components/cast/test_media_player.py | 109 +++++++++++++++++- 9 files changed, 166 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index d0b7cfc4158..63f0693b01a 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -58,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Cast from a config entry.""" await home_assistant_cast.async_setup_ha_cast(hass, entry) hass.config_entries.async_setup_platforms(entry, PLATFORMS) - hass.data[DOMAIN] = {} + hass.data[DOMAIN] = {"cast_platform": {}, "unknown_models": {}} await async_process_integration_platforms(hass, DOMAIN, _register_cast_platform) return True @@ -107,7 +107,7 @@ async def _register_cast_platform( or not hasattr(platform, "async_play_media") ): raise HomeAssistantError(f"Invalid cast platform {platform}") - hass.data[DOMAIN][integration_domain] = platform + hass.data[DOMAIN]["cast_platform"][integration_domain] = platform async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index bc0f1435f8b..485d2888a41 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -34,7 +34,7 @@ def discover_chromecast( _LOGGER.error("Discovered chromecast without uuid %s", info) return - info = info.fill_out_missing_chromecast_info() + info = info.fill_out_missing_chromecast_info(hass) _LOGGER.debug("Discovered new or updated chromecast %s", info) dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index 5c6f0fee62a..dd98a2bc051 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -15,8 +15,11 @@ from pychromecast import dial from pychromecast.const import CAST_TYPE_GROUP from pychromecast.models import CastInfo +from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client +from .const import DOMAIN + _LOGGER = logging.getLogger(__name__) _PLS_SECTION_PLAYLIST = "playlist" @@ -47,18 +50,50 @@ class ChromecastInfo: """Return the UUID.""" return self.cast_info.uuid - def fill_out_missing_chromecast_info(self) -> ChromecastInfo: + def fill_out_missing_chromecast_info(self, hass: HomeAssistant) -> ChromecastInfo: """Return a new ChromecastInfo object with missing attributes filled in. Uses blocking HTTP / HTTPS. """ cast_info = self.cast_info if self.cast_info.cast_type is None or self.cast_info.manufacturer is None: - # Manufacturer and cast type is not available in mDNS data, get it over http - cast_info = dial.get_cast_type( - cast_info, - zconf=ChromeCastZeroconf.get_zeroconf(), - ) + unknown_models = hass.data[DOMAIN]["unknown_models"] + if self.cast_info.model_name not in unknown_models: + # Manufacturer and cast type is not available in mDNS data, get it over http + cast_info = dial.get_cast_type( + cast_info, + zconf=ChromeCastZeroconf.get_zeroconf(), + ) + unknown_models[self.cast_info.model_name] = ( + cast_info.cast_type, + cast_info.manufacturer, + ) + + report_issue = ( + "create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" + "+label%3A%22integration%3A+cast%22" + ) + + _LOGGER.info( + "Fetched cast details for unknown model '%s' manufacturer: '%s', type: '%s'. Please %s", + cast_info.model_name, + cast_info.manufacturer, + cast_info.cast_type, + report_issue, + ) + else: + cast_type, manufacturer = unknown_models[self.cast_info.model_name] + cast_info = CastInfo( + cast_info.services, + cast_info.uuid, + cast_info.model_name, + cast_info.friendly_name, + cast_info.host, + cast_info.port, + cast_type, + manufacturer, + ) if not self.is_audio_group or self.is_dynamic_group is not None: # We have all information, no need to check HTTP API. diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 389b837f200..7144a3872f5 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==12.0.0"], + "requirements": ["pychromecast==12.1.0"], "after_dependencies": [ "cloud", "http", diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index e63fedd3598..b64c3372c15 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -535,7 +535,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): """Generate root node.""" children = [] # Add media browsers - for platform in self.hass.data[CAST_DOMAIN].values(): + for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values(): children.extend( await platform.async_get_media_browser_root_object( self.hass, self._chromecast.cast_type @@ -587,7 +587,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): if media_content_id is None: return await self._async_root_payload(content_filter) - for platform in self.hass.data[CAST_DOMAIN].values(): + for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values(): browse_media = await platform.async_browse_media( self.hass, media_content_type, @@ -646,7 +646,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): return # Try the cast platforms - for platform in self.hass.data[CAST_DOMAIN].values(): + for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values(): result = await platform.async_play_media( self.hass, self.entity_id, self._chromecast, media_type, media_id ) diff --git a/requirements_all.txt b/requirements_all.txt index eb0b504bdaa..777cd79af74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1402,7 +1402,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==12.0.0 +pychromecast==12.1.0 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 01f4a2d339a..205ac163595 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -941,7 +941,7 @@ pybotvac==0.0.23 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==12.0.0 +pychromecast==12.1.0 # homeassistant.components.climacell pyclimacell==0.18.2 diff --git a/tests/components/cast/conftest.py b/tests/components/cast/conftest.py index 3b96f378906..52152b4a718 100644 --- a/tests/components/cast/conftest.py +++ b/tests/components/cast/conftest.py @@ -14,6 +14,13 @@ def get_multizone_status_mock(): return mock +@pytest.fixture() +def get_cast_type_mock(): + """Mock pychromecast dial.""" + mock = MagicMock(spec_set=pychromecast.dial.get_cast_type) + return mock + + @pytest.fixture() def castbrowser_mock(): """Mock pychromecast CastBrowser.""" @@ -43,6 +50,7 @@ def cast_mock( mz_mock, quick_play_mock, castbrowser_mock, + get_cast_type_mock, get_chromecast_mock, get_multizone_status_mock, ): @@ -52,6 +60,9 @@ def cast_mock( with patch( "homeassistant.components.cast.discovery.pychromecast.discovery.CastBrowser", castbrowser_mock, + ), patch( + "homeassistant.components.cast.helpers.dial.get_cast_type", + get_cast_type_mock, ), patch( "homeassistant.components.cast.helpers.dial.get_multizone_status", get_multizone_status_mock, diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 88cf281c8a5..e4df84f6443 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -64,6 +64,8 @@ FAKE_MDNS_SERVICE = pychromecast.discovery.ServiceInfo( pychromecast.const.SERVICE_TYPE_MDNS, "the-service" ) +UNDEFINED = object() + def get_fake_chromecast(info: ChromecastInfo): """Generate a Fake Chromecast object with the specified arguments.""" @@ -74,7 +76,14 @@ def get_fake_chromecast(info: ChromecastInfo): def get_fake_chromecast_info( - host="192.168.178.42", port=8009, service=None, uuid: UUID | None = FakeUUID + *, + host="192.168.178.42", + port=8009, + service=None, + uuid: UUID | None = FakeUUID, + cast_type=UNDEFINED, + manufacturer=UNDEFINED, + model_name=UNDEFINED, ): """Generate a Fake ChromecastInfo with the specified arguments.""" @@ -82,16 +91,22 @@ def get_fake_chromecast_info( service = pychromecast.discovery.ServiceInfo( pychromecast.const.SERVICE_TYPE_HOST, (host, port) ) + if cast_type is UNDEFINED: + cast_type = CAST_TYPE_GROUP if port != 8009 else CAST_TYPE_CHROMECAST + if manufacturer is UNDEFINED: + manufacturer = "Nabu Casa" + if model_name is UNDEFINED: + model_name = "Chromecast" return ChromecastInfo( cast_info=pychromecast.models.CastInfo( services={service}, uuid=uuid, - model_name="Chromecast", + model_name=model_name, friendly_name="Speaker", host=host, port=port, - cast_type=CAST_TYPE_GROUP if port != 8009 else CAST_TYPE_CHROMECAST, - manufacturer="Nabu Casa", + cast_type=cast_type, + manufacturer=manufacturer, ) ) @@ -342,6 +357,92 @@ async def test_internal_discovery_callback_fill_out_group( get_multizone_status_mock.assert_called_once() +async def test_internal_discovery_callback_fill_out_cast_type_manufacturer( + hass, get_cast_type_mock, caplog +): + """Test internal discovery automatically filling out information.""" + discover_cast, _, _ = await async_setup_cast_internal_discovery(hass) + info = get_fake_chromecast_info( + host="host1", + port=8009, + service=FAKE_MDNS_SERVICE, + cast_type=None, + manufacturer=None, + ) + info2 = get_fake_chromecast_info( + host="host1", + port=8009, + service=FAKE_MDNS_SERVICE, + cast_type=None, + manufacturer=None, + model_name="Model 101", + ) + zconf = get_fake_zconf(host="host1", port=8009) + full_info = attr.evolve( + info, + cast_info=pychromecast.discovery.CastInfo( + services=info.cast_info.services, + uuid=FakeUUID, + model_name="Chromecast", + friendly_name="Speaker", + host=info.cast_info.host, + port=info.cast_info.port, + cast_type="audio", + manufacturer="TrollTech", + ), + is_dynamic_group=None, + ) + full_info2 = attr.evolve( + info2, + cast_info=pychromecast.discovery.CastInfo( + services=info.cast_info.services, + uuid=FakeUUID, + model_name="Model 101", + friendly_name="Speaker", + host=info.cast_info.host, + port=info.cast_info.port, + cast_type="cast", + manufacturer="Cyberdyne Systems", + ), + is_dynamic_group=None, + ) + + get_cast_type_mock.assert_not_called() + get_cast_type_mock.return_value = full_info.cast_info + + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf, + ): + signal = MagicMock() + + async_dispatcher_connect(hass, "cast_discovered", signal) + discover_cast(FAKE_MDNS_SERVICE, info) + await hass.async_block_till_done() + + # when called with incomplete info, it should use HTTP to get missing + get_cast_type_mock.assert_called_once() + assert get_cast_type_mock.call_count == 1 + discover = signal.mock_calls[0][1][0] + assert discover == full_info + assert "Fetched cast details for unknown model 'Chromecast'" in caplog.text + + # Call again, the model name should be fetched from cache + discover_cast(FAKE_MDNS_SERVICE, info) + await hass.async_block_till_done() + assert get_cast_type_mock.call_count == 1 # No additional calls + discover = signal.mock_calls[1][1][0] + assert discover == full_info + + # Call for another model, need to call HTTP again + get_cast_type_mock.return_value = full_info2.cast_info + discover_cast(FAKE_MDNS_SERVICE, info2) + await hass.async_block_till_done() + assert get_cast_type_mock.call_count == 2 + discover = signal.mock_calls[2][1][0] + assert discover == full_info2 + + async def test_stop_discovery_called_on_stop(hass, castbrowser_mock): """Test pychromecast.stop_discovery called on shutdown.""" # start_discovery should be called with empty config From 8f5e61ee8083492c1a6bae307b370976744cf820 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 May 2022 14:48:45 -0400 Subject: [PATCH 0230/3516] Remove logbook split_entity_id caching (#71359) --- homeassistant/components/logbook/__init__.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 1c930cf3004..f29276489a2 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -354,10 +354,11 @@ def humanify( # Process events for event in events_batch: if event.event_type == EVENT_STATE_CHANGED: - if event.domain != SENSOR_DOMAIN: - continue entity_id = event.entity_id - if entity_id in continuous_sensors: + if ( + entity_id in continuous_sensors + or split_entity_id(entity_id)[0] != SENSOR_DOMAIN + ): continue assert entity_id is not None continuous_sensors[entity_id] = _is_sensor_continuous(hass, entity_id) @@ -378,10 +379,9 @@ def humanify( for event in events_batch: if event.event_type == EVENT_STATE_CHANGED: entity_id = event.entity_id - domain = event.domain assert entity_id is not None - if domain == SENSOR_DOMAIN and continuous_sensors[entity_id]: + if continuous_sensors.get(entity_id): # Skip continuous sensors continue @@ -872,14 +872,6 @@ class LazyEventPartialState: self.time_fired_minute: int = self._row.time_fired.minute self._event_data_cache = event_data_cache - @property - def domain(self) -> str | None: - """Return the domain for the state.""" - if self._domain is None: - assert self.entity_id is not None - self._domain = split_entity_id(self.entity_id)[0] - return self._domain - @property def attributes_icon(self) -> str | None: """Extract the icon from the decoded attributes or json.""" From d2c9fa40755cf26e45a0d0ce0192f4ac545acd8e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 5 May 2022 20:55:52 +0200 Subject: [PATCH 0231/3516] Bump pytrafikverket to 0.2.0.1 (#71131) * Bump pytrafikverket to 0.2.0.1 * Use system timezone * Minor review changes * current time * Adjustments timezone --- .../trafikverket_ferry/coordinator.py | 21 ++++++++------ .../trafikverket_ferry/manifest.json | 2 +- .../components/trafikverket_ferry/sensor.py | 15 +++++----- .../trafikverket_train/manifest.json | 2 +- .../components/trafikverket_train/sensor.py | 29 ++++++++----------- .../trafikverket_weatherstation/manifest.json | 2 +- .../trafikverket_weatherstation/sensor.py | 10 +++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 40 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/trafikverket_ferry/coordinator.py b/homeassistant/components/trafikverket_ferry/coordinator.py index 052341cd12e..96a1e58c6c9 100644 --- a/homeassistant/components/trafikverket_ferry/coordinator.py +++ b/homeassistant/components/trafikverket_ferry/coordinator.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_API_KEY, CONF_WEEKDAY, WEEKDAYS from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.util.dt import UTC, as_utc, parse_time +from homeassistant.util import dt from .const import CONF_FROM, CONF_TIME, CONF_TO, DOMAIN @@ -58,21 +58,23 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator): ) self._from: str = entry.data[CONF_FROM] self._to: str = entry.data[CONF_TO] - self._time: time | None = parse_time(entry.data[CONF_TIME]) + self._time: time | None = dt.parse_time(entry.data[CONF_TIME]) self._weekdays: list[str] = entry.data[CONF_WEEKDAY] async def _async_update_data(self) -> dict[str, Any]: """Fetch data from Trafikverket.""" departure_day = next_departuredate(self._weekdays) - currenttime = datetime.now() + current_time = dt.now() when = ( - datetime.combine(departure_day, self._time) + datetime.combine( + departure_day, self._time, dt.get_time_zone(self.hass.config.time_zone) + ) if self._time - else datetime.now() + else dt.now() ) - if currenttime > when: - when = currenttime + if current_time > when: + when = current_time try: routedata: FerryStop = await self._ferry_api.async_get_next_ferry_stop( @@ -84,10 +86,11 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator): ) from error states = { - "departure_time": routedata.departure_time.replace(tzinfo=UTC), + "departure_time": routedata.departure_time, "departure_from": routedata.from_harbor_name, "departure_to": routedata.to_harbor_name, - "departure_modified": as_utc(routedata.modified_time.replace(tzinfo=UTC)), + "departure_modified": routedata.modified_time, "departure_information": routedata.other_information, } + _LOGGER.debug("States: %s", states) return states diff --git a/homeassistant/components/trafikverket_ferry/manifest.json b/homeassistant/components/trafikverket_ferry/manifest.json index 90864c7e358..d333473f169 100644 --- a/homeassistant/components/trafikverket_ferry/manifest.json +++ b/homeassistant/components/trafikverket_ferry/manifest.json @@ -2,7 +2,7 @@ "domain": "trafikverket_ferry", "name": "Trafikverket Ferry", "documentation": "https://www.home-assistant.io/integrations/trafikverket_ferry", - "requirements": ["pytrafikverket==0.1.6.2"], + "requirements": ["pytrafikverket==0.2.0.1"], "codeowners": ["@gjohansson-ST"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/trafikverket_ferry/sensor.py b/homeassistant/components/trafikverket_ferry/sensor.py index 682c2073f76..d93aecd38c1 100644 --- a/homeassistant/components/trafikverket_ferry/sensor.py +++ b/homeassistant/components/trafikverket_ferry/sensor.py @@ -3,8 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from datetime import timedelta -import logging +from datetime import datetime, timedelta from typing import Any from homeassistant.components.sensor import ( @@ -20,12 +19,11 @@ 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 homeassistant.util.dt import as_utc from .const import ATTRIBUTION, DOMAIN from .coordinator import TVDataUpdateCoordinator -_LOGGER = logging.getLogger(__name__) - ATTR_FROM = "from_harbour" ATTR_TO = "to_harbour" ATTR_MODIFIED_TIME = "modified_time" @@ -39,7 +37,7 @@ SCAN_INTERVAL = timedelta(minutes=5) class TrafikverketRequiredKeysMixin: """Mixin for required keys.""" - value_fn: Callable[[dict[str, Any]], StateType] + value_fn: Callable[[dict[str, Any]], StateType | datetime] info_fn: Callable[[dict[str, Any]], StateType | list] @@ -56,7 +54,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Departure Time", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, - value_fn=lambda data: data["departure_time"], + value_fn=lambda data: as_utc(data["departure_time"]), info_fn=lambda data: data["departure_information"], ), TrafikverketSensorEntityDescription( @@ -78,7 +76,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( name="Departure Modified", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, - value_fn=lambda data: data["departure_modified"], + value_fn=lambda data: as_utc(data["departure_modified"]), info_fn=lambda data: data["departure_information"], entity_registry_enabled_default=False, ), @@ -122,7 +120,7 @@ class FerrySensor(CoordinatorEntity[TVDataUpdateCoordinator], SensorEntity): entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, entry_id)}, manufacturer="Trafikverket", - model="v1.2", + model="v2.0", name=name, configuration_url="https://api.trafikinfo.trafikverket.se/", ) @@ -133,6 +131,7 @@ class FerrySensor(CoordinatorEntity[TVDataUpdateCoordinator], SensorEntity): self._attr_native_value = self.entity_description.value_fn( self.coordinator.data ) + self._attr_extra_state_attributes = { "other_information": self.entity_description.info_fn(self.coordinator.data), } diff --git a/homeassistant/components/trafikverket_train/manifest.json b/homeassistant/components/trafikverket_train/manifest.json index b3bf23ce5c4..0432670f15c 100644 --- a/homeassistant/components/trafikverket_train/manifest.json +++ b/homeassistant/components/trafikverket_train/manifest.json @@ -2,7 +2,7 @@ "domain": "trafikverket_train", "name": "Trafikverket Train", "documentation": "https://www.home-assistant.io/integrations/trafikverket_train", - "requirements": ["pytrafikverket==0.1.6.2"], + "requirements": ["pytrafikverket==0.2.0.1"], "codeowners": ["@endor-force", "@gjohansson-ST"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index 7bcd796bb16..6ae6af7d363 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -24,7 +24,7 @@ from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util.dt import as_utc, get_time_zone, parse_time +from homeassistant.util import dt from .const import CONF_FROM, CONF_TIME, CONF_TO, CONF_TRAINS, DOMAIN from .util import create_unique_id @@ -42,7 +42,6 @@ ATTR_DEVIATIONS = "deviations" ICON = "mdi:train" SCAN_INTERVAL = timedelta(minutes=5) -STOCKHOLM_TIMEZONE = get_time_zone("Europe/Stockholm") PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -111,7 +110,9 @@ async def async_setup_entry( ) from error train_time = ( - parse_time(entry.data.get(CONF_TIME, "")) if entry.data.get(CONF_TIME) else None + dt.parse_time(entry.data.get(CONF_TIME, "")) + if entry.data.get(CONF_TIME) + else None ) async_add_entities( @@ -153,7 +154,7 @@ def next_departuredate(departure: list[str]) -> date: def _to_iso_format(traintime: datetime) -> str: """Return isoformatted utc time.""" - return as_utc(traintime.replace(tzinfo=STOCKHOLM_TIMEZONE)).isoformat() + return dt.as_utc(traintime).isoformat() class TrainSensor(SensorEntity): @@ -183,7 +184,7 @@ class TrainSensor(SensorEntity): entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, entry_id)}, manufacturer="Trafikverket", - model="v1.2", + model="v2.0", name=name, configuration_url="https://api.trafikinfo.trafikverket.se/", ) @@ -193,12 +194,12 @@ class TrainSensor(SensorEntity): async def async_update(self) -> None: """Retrieve latest state.""" - when = datetime.now() + when = dt.now() _state: TrainStop | None = None if self._time: departure_day = next_departuredate(self._weekday) - when = datetime.combine(departure_day, self._time).replace( - tzinfo=STOCKHOLM_TIMEZONE + when = datetime.combine( + departure_day, self._time, dt.get_time_zone(self.hass.config.time_zone) ) try: if self._time: @@ -222,17 +223,11 @@ class TrainSensor(SensorEntity): self._attr_available = True # The original datetime doesn't provide a timezone so therefore attaching it here. - self._attr_native_value = _state.advertised_time_at_location.replace( - tzinfo=STOCKHOLM_TIMEZONE - ) + self._attr_native_value = dt.as_utc(_state.advertised_time_at_location) if _state.time_at_location: - self._attr_native_value = _state.time_at_location.replace( - tzinfo=STOCKHOLM_TIMEZONE - ) + self._attr_native_value = dt.as_utc(_state.time_at_location) if _state.estimated_time_at_location: - self._attr_native_value = _state.estimated_time_at_location.replace( - tzinfo=STOCKHOLM_TIMEZONE - ) + self._attr_native_value = dt.as_utc(_state.estimated_time_at_location) self._update_attributes(_state) diff --git a/homeassistant/components/trafikverket_weatherstation/manifest.json b/homeassistant/components/trafikverket_weatherstation/manifest.json index 4001856b703..e7efca9b24a 100644 --- a/homeassistant/components/trafikverket_weatherstation/manifest.json +++ b/homeassistant/components/trafikverket_weatherstation/manifest.json @@ -2,7 +2,7 @@ "domain": "trafikverket_weatherstation", "name": "Trafikverket Weather Station", "documentation": "https://www.home-assistant.io/integrations/trafikverket_weatherstation", - "requirements": ["pytrafikverket==0.1.6.2"], + "requirements": ["pytrafikverket==0.2.0.1"], "codeowners": ["@endor-force", "@gjohansson-ST"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index e659e42f82b..c54c9f67388 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -24,13 +24,11 @@ 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 homeassistant.util.dt import as_utc, get_time_zone +from homeassistant.util.dt import as_utc from .const import ATTRIBUTION, CONF_STATION, DOMAIN, NONE_IS_ZERO_SENSORS from .coordinator import TVDataUpdateCoordinator -STOCKHOLM_TIMEZONE = get_time_zone("Europe/Stockholm") - @dataclass class TrafikverketRequiredKeysMixin: @@ -156,8 +154,8 @@ async def async_setup_entry( def _to_datetime(measuretime: str) -> datetime: """Return isoformatted utc time.""" - time_obj = datetime.strptime(measuretime, "%Y-%m-%dT%H:%M:%S") - return as_utc(time_obj.replace(tzinfo=STOCKHOLM_TIMEZONE)) + time_obj = datetime.strptime(measuretime, "%Y-%m-%dT%H:%M:%S.%f%z") + return as_utc(time_obj) class TrafikverketWeatherStation( @@ -184,7 +182,7 @@ class TrafikverketWeatherStation( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, entry_id)}, manufacturer="Trafikverket", - model="v1.2", + model="v2.0", name=sensor_station, configuration_url="https://api.trafikinfo.trafikverket.se/", ) diff --git a/requirements_all.txt b/requirements_all.txt index 777cd79af74..eae4a2c3920 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1978,7 +1978,7 @@ pytradfri[async]==9.0.0 # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation -pytrafikverket==0.1.6.2 +pytrafikverket==0.2.0.1 # homeassistant.components.usb pyudev==0.22.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 205ac163595..0cb0cf2b69f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1301,7 +1301,7 @@ pytradfri[async]==9.0.0 # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation -pytrafikverket==0.1.6.2 +pytrafikverket==0.2.0.1 # homeassistant.components.usb pyudev==0.22.0 From aadfcc9a6e711f14f3285d6b4763a8d43bc60140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 5 May 2022 20:57:01 +0200 Subject: [PATCH 0232/3516] Lower Airzone unique id migration log to debug (#71362) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address late @MartinHjelmare PR comments. Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index 39f1fc978c3..2882ea18143 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -99,7 +99,7 @@ async def _async_migrate_unique_ids( if entity_unique_id.startswith(entry_id): new_unique_id = f"{unique_id}{entity_unique_id[len(entry_id):]}" - _LOGGER.info( + _LOGGER.debug( "Migrating unique_id from [%s] to [%s]", entity_unique_id, new_unique_id, From 353cc0b8c279ad8d40c78bcd31eddfcf57450ece Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 May 2022 14:33:17 -0700 Subject: [PATCH 0233/3516] Fix importing blueprints (#71365) Co-authored-by: Shay Levy --- homeassistant/helpers/selector.py | 23 ++++++++++++++++++- tests/components/blueprint/test_importer.py | 2 +- .../blueprint/test_websocket_api.py | 12 +++++++--- tests/helpers/test_config_validation.py | 13 ++++++++++- tests/helpers/test_selector.py | 18 ++++++++++----- .../automation/test_event_service.yaml | 2 ++ 6 files changed, 58 insertions(+), 12 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index eecc66c8332..afe74a4d7af 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -5,11 +5,13 @@ from collections.abc import Callable, Sequence from typing import Any, TypedDict, cast import voluptuous as vol +import yaml from homeassistant.backports.enum import StrEnum from homeassistant.const import CONF_MODE, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.util import decorator +from homeassistant.util.yaml.dumper import represent_odict from . import config_validation as cv @@ -71,7 +73,11 @@ class Selector: def serialize(self) -> Any: """Serialize Selector for voluptuous_serialize.""" - return {"selector": {self.selector_type: self.config}} + return {"selector": {self.selector_type: self.serialize_config()}} + + def serialize_config(self) -> Any: + """Serialize config.""" + return self.config SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA = vol.Schema( @@ -623,6 +629,13 @@ class NumberSelector(Selector): """Instantiate a selector.""" super().__init__(config) + def serialize_config(self) -> Any: + """Serialize the selector config.""" + return { + **self.config, + "mode": self.config["mode"].value, + } + def __call__(self, data: Any) -> float: """Validate the passed selection.""" value: float = vol.Coerce(float)(data) @@ -881,3 +894,11 @@ class TimeSelector(Selector): """Validate the passed selection.""" cv.time(data) return cast(str, data) + + +yaml.SafeDumper.add_representer( + Selector, + lambda dumper, value: represent_odict( + dumper, "tag:yaml.org,2002:map", value.serialize() + ), +) diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 0e1e66405e6..806cdb2cb8d 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -198,7 +198,7 @@ async def test_fetch_blueprint_from_github_url(hass, aioclient_mock, url): assert imported_blueprint.blueprint.domain == "automation" assert imported_blueprint.blueprint.inputs == { "service_to_call": None, - "trigger_event": None, + "trigger_event": {"selector": {"text": {}}}, } assert imported_blueprint.suggested_filename == "balloob/motion_light" assert imported_blueprint.blueprint.metadata["source_url"] == url diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index 51b8184354a..40f24d98016 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -30,7 +30,10 @@ async def test_list_blueprints(hass, hass_ws_client): "test_event_service.yaml": { "metadata": { "domain": "automation", - "input": {"service_to_call": None, "trigger_event": None}, + "input": { + "service_to_call": None, + "trigger_event": {"selector": {"text": {}}}, + }, "name": "Call service based on event", }, }, @@ -89,7 +92,10 @@ async def test_import_blueprint(hass, aioclient_mock, hass_ws_client): "blueprint": { "metadata": { "domain": "automation", - "input": {"service_to_call": None, "trigger_event": None}, + "input": { + "service_to_call": None, + "trigger_event": {"selector": {"text": {}}}, + }, "name": "Call service based on event", "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", }, @@ -123,7 +129,7 @@ async def test_save_blueprint(hass, aioclient_mock, hass_ws_client): assert msg["success"] assert write_mock.mock_calls assert write_mock.call_args[0] == ( - "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n entity_id: light.kitchen\n", + "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n selector:\n text: {}\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n entity_id: light.kitchen\n", ) diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 64c838e6c02..a5d2223a3d2 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -11,7 +11,7 @@ import pytest import voluptuous as vol import homeassistant -from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers import config_validation as cv, selector, template def test_boolean(): @@ -720,6 +720,17 @@ def test_string_in_serializer(): } +def test_selector_in_serializer(): + """Test selector with custom_serializer.""" + assert cv.custom_serializer(selector.selector({"text": {}})) == { + "selector": { + "text": { + "multiline": False, + } + } + } + + def test_positive_time_period_dict_in_serializer(): """Test positive_time_period_dict with custom_serializer.""" assert cv.custom_serializer(cv.positive_time_period_dict) == { diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index cb0ad95eb6b..8c94e3d3c56 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -2,7 +2,8 @@ import pytest import voluptuous as vol -from homeassistant.helpers import config_validation as cv, selector +from homeassistant.helpers import selector +from homeassistant.util import yaml FAKE_UUID = "a266a680b608c32770e6c45bfe6b8411" @@ -48,10 +49,12 @@ def _test_selector( converter = default_converter # Validate selector configuration - selector.validate_selector({selector_type: schema}) + config = {selector_type: schema} + selector.validate_selector(config) + selector_instance = selector.selector(config) # Use selector in schema and validate - vol_schema = vol.Schema({"selection": selector.selector({selector_type: schema})}) + vol_schema = vol.Schema({"selection": selector_instance}) for selection in valid_selections: assert vol_schema({"selection": selection}) == { "selection": converter(selection) @@ -62,9 +65,12 @@ def _test_selector( # Serialize selector selector_instance = selector.selector({selector_type: schema}) - assert cv.custom_serializer(selector_instance) == { - "selector": {selector_type: selector_instance.config} - } + assert ( + selector.selector(selector_instance.serialize()["selector"]).config + == selector_instance.config + ) + # Test serialized selector can be dumped to YAML + yaml.dump(selector_instance.serialize()) @pytest.mark.parametrize( diff --git a/tests/testing_config/blueprints/automation/test_event_service.yaml b/tests/testing_config/blueprints/automation/test_event_service.yaml index ab067b004ac..648cef39b96 100644 --- a/tests/testing_config/blueprints/automation/test_event_service.yaml +++ b/tests/testing_config/blueprints/automation/test_event_service.yaml @@ -3,6 +3,8 @@ blueprint: domain: automation input: trigger_event: + selector: + text: service_to_call: trigger: platform: event From c8f95b7dfc357480bdf6295b05cf3aba545699ad Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 May 2022 14:33:37 -0700 Subject: [PATCH 0234/3516] Ignore loading system entity category (#71361) --- homeassistant/helpers/entity_registry.py | 4 ++++ tests/helpers/test_entity_registry.py | 13 ++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index b4dd0820d8c..64ab0323f6c 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -711,6 +711,10 @@ class EntityRegistry: if not valid_entity_id(entity["entity_id"]): continue + # We removed this in 2022.5. Remove this check in 2023.1. + if entity["entity_category"] == "system": + entity["entity_category"] = None + entities[entity["entity_id"]] = RegistryEntry( area_id=entity["area_id"], capabilities=entity["capabilities"], diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 41ac7412d9c..21d29736bd0 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -297,6 +297,12 @@ async def test_loading_extra_values(hass, hass_storage): "unique_id": "invalid-hass", "disabled_by": er.RegistryEntryDisabler.HASS, }, + { + "entity_id": "test.system_entity", + "platform": "super_platform", + "unique_id": "system-entity", + "entity_category": "system", + }, ] }, } @@ -304,7 +310,7 @@ async def test_loading_extra_values(hass, hass_storage): await er.async_load(hass) registry = er.async_get(hass) - assert len(registry.entities) == 4 + assert len(registry.entities) == 5 entry_with_name = registry.async_get_or_create( "test", "super_platform", "with-name" @@ -327,6 +333,11 @@ async def test_loading_extra_values(hass, hass_storage): assert entry_disabled_user.disabled assert entry_disabled_user.disabled_by is er.RegistryEntryDisabler.USER + entry_system_category = registry.async_get_or_create( + "test", "system_entity", "system-entity" + ) + assert entry_system_category.entity_category is None + def test_async_get_entity_id(registry): """Test that entity_id is returned.""" From afe4892ae6e8a714c312efc3bfb702a65cdb6d45 Mon Sep 17 00:00:00 2001 From: Shai Ungar Date: Fri, 6 May 2022 00:34:30 +0300 Subject: [PATCH 0235/3516] Add unique ids to sensors (#71367) --- homeassistant/components/sabnzbd/manifest.json | 2 -- homeassistant/components/sabnzbd/sensor.py | 11 ++++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sabnzbd/manifest.json b/homeassistant/components/sabnzbd/manifest.json index 0702446d217..f112893b5e1 100644 --- a/homeassistant/components/sabnzbd/manifest.json +++ b/homeassistant/components/sabnzbd/manifest.json @@ -3,8 +3,6 @@ "name": "SABnzbd", "documentation": "https://www.home-assistant.io/integrations/sabnzbd", "requirements": ["pysabnzbd==1.1.1"], - "dependencies": ["configurator"], - "after_dependencies": ["discovery"], "codeowners": ["@shaiu"], "iot_class": "local_polling", "config_flow": true, diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index 1d661d90848..dee80945e9d 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -14,8 +14,10 @@ from . import DOMAIN, SIGNAL_SABNZBD_UPDATED from ...config_entries import ConfigEntry from ...const import DATA_GIGABYTES, DATA_MEGABYTES, DATA_RATE_MEGABYTES_PER_SECOND from ...core import HomeAssistant +from ...helpers.device_registry import DeviceEntryType +from ...helpers.entity import DeviceInfo from ...helpers.entity_platform import AddEntitiesCallback -from .const import KEY_API_DATA, KEY_NAME +from .const import DEFAULT_NAME, KEY_API_DATA, KEY_NAME @dataclass @@ -127,9 +129,16 @@ class SabnzbdSensor(SensorEntity): self, sabnzbd_api_data, client_name, description: SabnzbdSensorEntityDescription ): """Initialize the sensor.""" + unique_id = description.key + self._attr_unique_id = unique_id self.entity_description = description self._sabnzbd_api = sabnzbd_api_data self._attr_name = f"{client_name} {description.name}" + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, DOMAIN)}, + name=DEFAULT_NAME, + ) async def async_added_to_hass(self): """Call when entity about to be added to hass.""" From 0bac48864f691b4c883cdbb25228cae321500edf Mon Sep 17 00:00:00 2001 From: Markus Bong Date: Thu, 5 May 2022 11:36:00 +0200 Subject: [PATCH 0236/3516] fix reading of battery messages (#70659) --- .../components/devolo_home_control/devolo_device.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/devolo_home_control/devolo_device.py b/homeassistant/components/devolo_home_control/devolo_device.py index b3cb68098e1..6087e07799d 100644 --- a/homeassistant/components/devolo_home_control/devolo_device.py +++ b/homeassistant/components/devolo_home_control/devolo_device.py @@ -7,6 +7,7 @@ from urllib.parse import urlparse from devolo_home_control_api.devices.zwave import Zwave from devolo_home_control_api.homecontrol import HomeControl +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN @@ -71,7 +72,11 @@ class DevoloDeviceEntity(Entity): def _generic_message(self, message: tuple) -> None: """Handle generic messages.""" - if len(message) == 3 and message[2] == "battery_level": + if ( + len(message) == 3 + and message[2] == "battery_level" + and self.device_class == SensorDeviceClass.BATTERY + ): self._value = message[1] elif len(message) == 3 and message[2] == "status": # Maybe the API wants to tell us, that the device went on- or offline. From 52333bb7203e87ebbe7fefb11d766973a9d16d67 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 5 May 2022 07:15:24 +0200 Subject: [PATCH 0237/3516] Only test for EncryptedBridge in Samsung J/H models (#71291) --- .../components/samsungtv/__init__.py | 14 +++++----- homeassistant/components/samsungtv/bridge.py | 27 ++++++++++++------- tests/components/samsungtv/conftest.py | 4 +-- .../components/samsungtv/test_config_flow.py | 24 ++++++++--------- 4 files changed, 38 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index dae4033ad4c..a7b8f7d1aec 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -30,7 +30,12 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.typing import ConfigType -from .bridge import SamsungTVBridge, async_get_device_info, mac_from_device_info +from .bridge import ( + SamsungTVBridge, + async_get_device_info, + mac_from_device_info, + model_requires_encryption, +) from .const import ( CONF_ON_ACTION, CONF_SESSION_ID, @@ -214,11 +219,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -def _model_requires_encryption(model: str | None) -> bool: - """H and J models need pairing with PIN.""" - return model is not None and len(model) > 4 and model[4] in ("H", "J") - - async def _async_create_bridge_with_updated_data( hass: HomeAssistant, entry: ConfigEntry ) -> SamsungTVBridge: @@ -279,7 +279,7 @@ async def _async_create_bridge_with_updated_data( LOGGER.info("Updated model to %s for %s", model, host) updated_data[CONF_MODEL] = model - if _model_requires_encryption(model) and method != METHOD_ENCRYPTED_WEBSOCKET: + if model_requires_encryption(model) and method != METHOD_ENCRYPTED_WEBSOCKET: LOGGER.info( "Detected model %s for %s. Some televisions from H and J series use " "an encrypted protocol but you are using %s which may not be supported", diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 52ab86337dd..c3201a493eb 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -85,6 +85,11 @@ def mac_from_device_info(info: dict[str, Any]) -> str | None: return None +def model_requires_encryption(model: str | None) -> bool: + """H and J models need pairing with PIN.""" + return model is not None and len(model) > 4 and model[4] in ("H", "J") + + async def async_get_device_info( hass: HomeAssistant, host: str, @@ -99,17 +104,19 @@ async def async_get_device_info( port, info, ) - encrypted_bridge = SamsungTVEncryptedBridge( - hass, METHOD_ENCRYPTED_WEBSOCKET, host, ENCRYPTED_WEBSOCKET_PORT - ) - result = await encrypted_bridge.async_try_connect() - if result != RESULT_CANNOT_CONNECT: - return ( - result, - ENCRYPTED_WEBSOCKET_PORT, - METHOD_ENCRYPTED_WEBSOCKET, - info, + # Check the encrypted port if the model requires encryption + if model_requires_encryption(info.get("device", {}).get("modelName")): + encrypted_bridge = SamsungTVEncryptedBridge( + hass, METHOD_ENCRYPTED_WEBSOCKET, host, ENCRYPTED_WEBSOCKET_PORT ) + result = await encrypted_bridge.async_try_connect() + if result != RESULT_CANNOT_CONNECT: + return ( + result, + ENCRYPTED_WEBSOCKET_PORT, + METHOD_ENCRYPTED_WEBSOCKET, + info, + ) return RESULT_SUCCESS, port, METHOD_WEBSOCKET, info # Try legacy port diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py index d7f8ed0d1a1..764022f3501 100644 --- a/tests/components/samsungtv/conftest.py +++ b/tests/components/samsungtv/conftest.py @@ -22,7 +22,7 @@ from samsungtvws.remote import ChannelEmitCommand from homeassistant.components.samsungtv.const import WEBSOCKET_SSL_PORT import homeassistant.util.dt as dt_util -from .const import SAMPLE_DEVICE_INFO_WIFI +from .const import SAMPLE_DEVICE_INFO_UE48JU6400, SAMPLE_DEVICE_INFO_WIFI @pytest.fixture(autouse=True) @@ -177,7 +177,7 @@ def rest_api_fixture_non_ssl_only() -> Mock: """Mock rest_device_info to fail for ssl and work for non-ssl.""" if self.port == WEBSOCKET_SSL_PORT: raise ResponseError - return SAMPLE_DEVICE_INFO_WIFI + return SAMPLE_DEVICE_INFO_UE48JU6400 with patch( "homeassistant.components.samsungtv.bridge.SamsungTVAsyncRest", diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index d2a9d10caf2..40397a68d7d 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -340,16 +340,16 @@ async def test_user_encrypted_websocket( ) assert result4["type"] == "create_entry" - assert result4["title"] == "Living Room (82GXARRS)" + assert result4["title"] == "TV-UE48JU6470 (UE48JU6400)" assert result4["data"][CONF_HOST] == "fake_host" - assert result4["data"][CONF_NAME] == "Living Room" + assert result4["data"][CONF_NAME] == "TV-UE48JU6470" assert result4["data"][CONF_MAC] == "aa:bb:ww:ii:ff:ii" assert result4["data"][CONF_MANUFACTURER] == "Samsung" - assert result4["data"][CONF_MODEL] == "82GXARRS" + assert result4["data"][CONF_MODEL] == "UE48JU6400" assert result4["data"][CONF_SSDP_RENDERING_CONTROL_LOCATION] is None assert result4["data"][CONF_TOKEN] == "037739871315caef138547b03e348b72" assert result4["data"][CONF_SESSION_ID] == "1" - assert result4["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + assert result4["result"].unique_id == "223da676-497a-4e06-9507-5e27ec4f0fb3" @pytest.mark.usefixtures("rest_api_failing") @@ -714,19 +714,19 @@ async def test_ssdp_encrypted_websocket_success_populates_mac_address_and_ssdp_l ) assert result4["type"] == "create_entry" - assert result4["title"] == "Living Room (82GXARRS)" + assert result4["title"] == "TV-UE48JU6470 (UE48JU6400)" assert result4["data"][CONF_HOST] == "fake_host" - assert result4["data"][CONF_NAME] == "Living Room" + assert result4["data"][CONF_NAME] == "TV-UE48JU6470" assert result4["data"][CONF_MAC] == "aa:bb:ww:ii:ff:ii" assert result4["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" - assert result4["data"][CONF_MODEL] == "82GXARRS" + assert result4["data"][CONF_MODEL] == "UE48JU6400" assert ( result4["data"][CONF_SSDP_RENDERING_CONTROL_LOCATION] == "https://fake_host:12345/test" ) assert result4["data"][CONF_TOKEN] == "037739871315caef138547b03e348b72" assert result4["data"][CONF_SESSION_ID] == "1" - assert result4["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + assert result4["result"].unique_id == "223da676-497a-4e06-9507-5e27ec4f0fb3" @pytest.mark.usefixtures("rest_api_non_ssl_only") @@ -1036,13 +1036,13 @@ async def test_dhcp_wireless(hass: HomeAssistant) -> None: result["flow_id"], user_input="whatever" ) assert result["type"] == "create_entry" - assert result["title"] == "Living Room (82GXARRS)" + assert result["title"] == "TV-UE48JU6470 (UE48JU6400)" assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_NAME] == "Living Room" + assert result["data"][CONF_NAME] == "TV-UE48JU6470" assert result["data"][CONF_MAC] == "aa:bb:ww:ii:ff:ii" assert result["data"][CONF_MANUFACTURER] == "Samsung" - assert result["data"][CONF_MODEL] == "82GXARRS" - assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + assert result["data"][CONF_MODEL] == "UE48JU6400" + assert result["result"].unique_id == "223da676-497a-4e06-9507-5e27ec4f0fb3" @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") From 5a5cde690fdc61d63bcf9e09914f4c5a9368877d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 May 2022 01:13:23 -0400 Subject: [PATCH 0238/3516] Ensure rachio retries setup later when cloud service is broken (#71300) --- homeassistant/components/rachio/__init__.py | 6 +++++- homeassistant/components/rachio/device.py | 19 +++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index ea8b8fe59cb..e75d7117d73 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -9,7 +9,7 @@ from homeassistant.components import cloud from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from .const import CONF_CLOUDHOOK_URL, CONF_MANUAL_RUN_MINS, CONF_WEBHOOK_ID, DOMAIN @@ -73,6 +73,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Get the API user try: await person.async_setup(hass) + except ConfigEntryAuthFailed as error: + # Reauth is not yet implemented + _LOGGER.error("Authentication failed: %s", error) + return False except ConnectTimeout as error: _LOGGER.error("Could not reach the Rachio API: %s", error) raise ConfigEntryNotReady from error diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index ff7c0535295..911049883d9 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import ServiceCall +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from .const import ( @@ -125,12 +126,18 @@ class RachioPerson: rachio = self.rachio response = rachio.person.info() - assert int(response[0][KEY_STATUS]) == HTTPStatus.OK, "API key error" + if is_invalid_auth_code(int(response[0][KEY_STATUS])): + raise ConfigEntryAuthFailed(f"API key error: {response}") + if int(response[0][KEY_STATUS]) != HTTPStatus.OK: + raise ConfigEntryNotReady(f"API Error: {response}") 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]) == HTTPStatus.OK, "User ID error" + if is_invalid_auth_code(int(data[0][KEY_STATUS])): + raise ConfigEntryAuthFailed(f"User ID error: {data}") + if int(data[0][KEY_STATUS]) != HTTPStatus.OK: + raise ConfigEntryNotReady(f"API Error: {data}") self.username = data[1][KEY_USERNAME] devices = data[1][KEY_DEVICES] for controller in devices: @@ -297,3 +304,11 @@ class RachioIro: """Resume paused watering on this controller.""" self.rachio.device.resume_zone_run(self.controller_id) _LOGGER.debug("Resuming watering on %s", self) + + +def is_invalid_auth_code(http_status_code): + """HTTP status codes that mean invalid auth.""" + if http_status_code in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN): + return True + + return False From 7be5eed25cf4134111f5ab1b2c81bc89f4896b0a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 May 2022 01:10:27 -0400 Subject: [PATCH 0239/3516] Fix lutron caseta occupancy sensors (#71309) * Fix lutron_caseta occupancy sensors * Fix lutron_caseta occupancy sensors * Make as service since its a group * merge * Revert "merge" This reverts commit 69d19dc0088bd1b3483cfc481ed2f72e49599cf8. * model and type not present --- .../components/lutron_caseta/__init__.py | 5 ++- .../components/lutron_caseta/binary_sensor.py | 34 ++++++++++++------- .../components/lutron_caseta/const.py | 2 ++ 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index bb8f94f3abe..3d9e07519a8 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -38,6 +38,7 @@ from .const import ( CONF_CA_CERTS, CONF_CERTFILE, CONF_KEYFILE, + CONFIG_URL, DOMAIN, LUTRON_CASETA_BUTTON_EVENT, MANUFACTURER, @@ -306,13 +307,15 @@ class LutronCasetaDevice(Entity): self._device = device self._smartbridge = bridge self._bridge_device = bridge_device + if "serial" not in self._device: + return info = DeviceInfo( identifiers={(DOMAIN, self.serial)}, manufacturer=MANUFACTURER, model=f"{device['model']} ({device['type']})", name=self.name, via_device=(DOMAIN, self._bridge_device["serial"]), - configuration_url="https://device-login.lutron.com", + configuration_url=CONFIG_URL, ) area, _ = _area_and_name_from_name(device["name"]) if area != UNASSIGNED_AREA: diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index 788702f9353..f61e644a331 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -6,11 +6,14 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_SUGGESTED_AREA from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice -from .const import BRIDGE_DEVICE, BRIDGE_LEAP +from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice, _area_and_name_from_name +from .const import BRIDGE_DEVICE, BRIDGE_LEAP, CONFIG_URL, MANUFACTURER, UNASSIGNED_AREA async def async_setup_entry( @@ -39,6 +42,23 @@ async def async_setup_entry( class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): """Representation of a Lutron occupancy group.""" + def __init__(self, device, bridge, bridge_device): + """Init an occupancy sensor.""" + super().__init__(device, bridge, bridge_device) + info = DeviceInfo( + identifiers={(CASETA_DOMAIN, self.unique_id)}, + manufacturer=MANUFACTURER, + model="Lutron Occupancy", + name=self.name, + via_device=(CASETA_DOMAIN, self._bridge_device["serial"]), + configuration_url=CONFIG_URL, + entry_type=DeviceEntryType.SERVICE, + ) + area, _ = _area_and_name_from_name(device["name"]) + if area != UNASSIGNED_AREA: + info[ATTR_SUGGESTED_AREA] = area + self._attr_device_info = info + @property def device_class(self): """Flag supported features.""" @@ -65,16 +85,6 @@ class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): """Return a unique identifier.""" return f"occupancygroup_{self.device_id}" - @property - def device_info(self): - """Return the device info. - - Sensor entities are aggregated from one or more physical - sensors by each room. Therefore, there shouldn't be devices - related to any sensor entities. - """ - return None - @property def extra_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/lutron_caseta/const.py b/homeassistant/components/lutron_caseta/const.py index 56a3821dd64..71d686ba2c8 100644 --- a/homeassistant/components/lutron_caseta/const.py +++ b/homeassistant/components/lutron_caseta/const.py @@ -35,3 +35,5 @@ CONF_SUBTYPE = "subtype" BRIDGE_TIMEOUT = 35 UNASSIGNED_AREA = "Unassigned" + +CONFIG_URL = "https://device-login.lutron.com" From 61a6d13d7965cfe6e4d9d7b2edee2dbc0edc494e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 5 May 2022 09:06:23 +0200 Subject: [PATCH 0240/3516] Update aioairzone to v0.4.3 (#71312) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update aioairzone to v0.4.3 Fixes exception on older local API. Signed-off-by: Álvaro Fernández Rojas * airzone: switch to set_hvac_parameters function Fixes failing airzone tests. Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone/climate.py | 2 +- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airzone/test_climate.py | 12 ++++++------ 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index 1ec7dfabcfa..9cff1ff393d 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -123,7 +123,7 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity): } _LOGGER.debug("update_hvac_params=%s", _params) try: - await self.coordinator.airzone.put_hvac(_params) + await self.coordinator.airzone.set_hvac_parameters(_params) except AirzoneError as error: raise HomeAssistantError( f"Failed to set zone {self.name}: {error}" diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index a6ea814fe9c..7a04b3a78b3 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -3,7 +3,7 @@ "name": "Airzone", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airzone", - "requirements": ["aioairzone==0.4.2"], + "requirements": ["aioairzone==0.4.3"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioairzone"] diff --git a/requirements_all.txt b/requirements_all.txt index 787e4546b0b..46c06635925 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -110,7 +110,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.2 +aioairzone==0.4.3 # homeassistant.components.ambient_station aioambient==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f9f4b59b0e..eb8475ab64f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -94,7 +94,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.2 +aioairzone==0.4.3 # homeassistant.components.ambient_station aioambient==2021.11.0 diff --git a/tests/components/airzone/test_climate.py b/tests/components/airzone/test_climate.py index 2128d2818e7..dcb493351ba 100644 --- a/tests/components/airzone/test_climate.py +++ b/tests/components/airzone/test_climate.py @@ -147,7 +147,7 @@ async def test_airzone_climate_turn_on_off(hass: HomeAssistant) -> None: ] } with patch( - "homeassistant.components.airzone.AirzoneLocalApi.http_request", + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", return_value=HVAC_MOCK, ): await hass.services.async_call( @@ -172,7 +172,7 @@ async def test_airzone_climate_turn_on_off(hass: HomeAssistant) -> None: ] } with patch( - "homeassistant.components.airzone.AirzoneLocalApi.http_request", + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", return_value=HVAC_MOCK, ): await hass.services.async_call( @@ -204,7 +204,7 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None: ] } with patch( - "homeassistant.components.airzone.AirzoneLocalApi.http_request", + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", return_value=HVAC_MOCK, ): await hass.services.async_call( @@ -230,7 +230,7 @@ async def test_airzone_climate_set_hvac_mode(hass: HomeAssistant) -> None: ] } with patch( - "homeassistant.components.airzone.AirzoneLocalApi.http_request", + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", return_value=HVAC_MOCK_2, ): await hass.services.async_call( @@ -263,7 +263,7 @@ async def test_airzone_climate_set_hvac_slave_error(hass: HomeAssistant) -> None await async_init_integration(hass) with patch( - "homeassistant.components.airzone.AirzoneLocalApi.http_request", + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", return_value=HVAC_MOCK, ), pytest.raises(HomeAssistantError): await hass.services.async_call( @@ -296,7 +296,7 @@ async def test_airzone_climate_set_temp(hass: HomeAssistant) -> None: await async_init_integration(hass) with patch( - "homeassistant.components.airzone.AirzoneLocalApi.http_request", + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", return_value=HVAC_MOCK, ): await hass.services.async_call( From c7b24c45ba31701f0145110db35d19e9708d414a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 4 May 2022 23:52:00 -0700 Subject: [PATCH 0241/3516] Fix apple tv warning (#71321) --- homeassistant/components/apple_tv/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 6c1b35f70f8..5a7298dcbee 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -296,7 +296,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): _LOGGER.debug("Streaming %s via RAOP", media_id) await self.atv.stream.stream_file(media_id) - if self._is_feature_available(FeatureName.PlayUrl): + elif self._is_feature_available(FeatureName.PlayUrl): _LOGGER.debug("Playing %s via AirPlay", media_id) await self.atv.stream.play_url(media_id) else: From 6ccd707a652ae827004e919f75bbb8887ae4d243 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 5 May 2022 08:52:20 +0200 Subject: [PATCH 0242/3516] Fix Meater (#71324) --- homeassistant/components/meater/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index 17e8db9e473..8c719d588d8 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -146,13 +146,13 @@ async def async_setup_entry( if not coordinator.last_update_success: return - devices = coordinator.data + devices: dict[str, MeaterProbe] = coordinator.data entities = [] known_probes: set = hass.data[DOMAIN]["known_probes"] # Add entities for temperature probes which we've not yet seen for dev in devices: - if dev.id in known_probes: + if dev in known_probes: continue entities.extend( @@ -161,7 +161,7 @@ async def async_setup_entry( for sensor_description in SENSOR_TYPES ] ) - known_probes.add(dev.id) + known_probes.add(dev) async_add_entities(entities) From b8dccbbbf37fe1d95ee1c742148f792ab4dfb12c Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 5 May 2022 10:12:39 +0200 Subject: [PATCH 0243/3516] Bump numpy to 1.21.6 (#71325) --- homeassistant/components/compensation/manifest.json | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index 315d1b705df..213e8888e23 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,7 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.21.4"], + "requirements": ["numpy==1.21.6"], "codeowners": ["@Petro31"], "iot_class": "calculated" } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 50ddeb3bba7..9bb07157b54 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.21.4", "pyiqvia==2022.04.0"], + "requirements": ["numpy==1.21.6", "pyiqvia==2022.04.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyiqvia"] diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 9c1f51c4933..504b83bdaf9 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.21.4", "opencv-python-headless==4.5.2.54"], + "requirements": ["numpy==1.21.6", "opencv-python-headless==4.5.2.54"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 5f1ac406b70..0f53dd61cb4 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.5.0", "tf-models-official==2.5.0", "pycocotools==2.0.1", - "numpy==1.21.4", + "numpy==1.21.6", "pillow==9.1.0" ], "codeowners": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 831e97aed3d..aaae8f7cc54 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.21.4"], + "requirements": ["numpy==1.21.6"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 46c06635925..5efd953ff3c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1111,7 +1111,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.21.4 +numpy==1.21.6 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eb8475ab64f..aedb93f2d37 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -755,7 +755,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.21.4 +numpy==1.21.6 # homeassistant.components.google oauth2client==4.1.3 From f2a07254a4b49def9712f8aef2115bf9ed1a16bf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 5 May 2022 20:04:00 +0200 Subject: [PATCH 0244/3516] Only lookup unknown Google Cast models once (#71348) Co-authored-by: Paulus Schoutsen --- homeassistant/components/cast/__init__.py | 4 +- homeassistant/components/cast/discovery.py | 2 +- homeassistant/components/cast/helpers.py | 47 +++++++- homeassistant/components/cast/manifest.json | 2 +- homeassistant/components/cast/media_player.py | 6 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/cast/conftest.py | 11 ++ tests/components/cast/test_media_player.py | 109 +++++++++++++++++- 9 files changed, 166 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index d0b7cfc4158..63f0693b01a 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -58,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Cast from a config entry.""" await home_assistant_cast.async_setup_ha_cast(hass, entry) hass.config_entries.async_setup_platforms(entry, PLATFORMS) - hass.data[DOMAIN] = {} + hass.data[DOMAIN] = {"cast_platform": {}, "unknown_models": {}} await async_process_integration_platforms(hass, DOMAIN, _register_cast_platform) return True @@ -107,7 +107,7 @@ async def _register_cast_platform( or not hasattr(platform, "async_play_media") ): raise HomeAssistantError(f"Invalid cast platform {platform}") - hass.data[DOMAIN][integration_domain] = platform + hass.data[DOMAIN]["cast_platform"][integration_domain] = platform async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index bc0f1435f8b..485d2888a41 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -34,7 +34,7 @@ def discover_chromecast( _LOGGER.error("Discovered chromecast without uuid %s", info) return - info = info.fill_out_missing_chromecast_info() + info = info.fill_out_missing_chromecast_info(hass) _LOGGER.debug("Discovered new or updated chromecast %s", info) dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index 5c6f0fee62a..dd98a2bc051 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -15,8 +15,11 @@ from pychromecast import dial from pychromecast.const import CAST_TYPE_GROUP from pychromecast.models import CastInfo +from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client +from .const import DOMAIN + _LOGGER = logging.getLogger(__name__) _PLS_SECTION_PLAYLIST = "playlist" @@ -47,18 +50,50 @@ class ChromecastInfo: """Return the UUID.""" return self.cast_info.uuid - def fill_out_missing_chromecast_info(self) -> ChromecastInfo: + def fill_out_missing_chromecast_info(self, hass: HomeAssistant) -> ChromecastInfo: """Return a new ChromecastInfo object with missing attributes filled in. Uses blocking HTTP / HTTPS. """ cast_info = self.cast_info if self.cast_info.cast_type is None or self.cast_info.manufacturer is None: - # Manufacturer and cast type is not available in mDNS data, get it over http - cast_info = dial.get_cast_type( - cast_info, - zconf=ChromeCastZeroconf.get_zeroconf(), - ) + unknown_models = hass.data[DOMAIN]["unknown_models"] + if self.cast_info.model_name not in unknown_models: + # Manufacturer and cast type is not available in mDNS data, get it over http + cast_info = dial.get_cast_type( + cast_info, + zconf=ChromeCastZeroconf.get_zeroconf(), + ) + unknown_models[self.cast_info.model_name] = ( + cast_info.cast_type, + cast_info.manufacturer, + ) + + report_issue = ( + "create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" + "+label%3A%22integration%3A+cast%22" + ) + + _LOGGER.info( + "Fetched cast details for unknown model '%s' manufacturer: '%s', type: '%s'. Please %s", + cast_info.model_name, + cast_info.manufacturer, + cast_info.cast_type, + report_issue, + ) + else: + cast_type, manufacturer = unknown_models[self.cast_info.model_name] + cast_info = CastInfo( + cast_info.services, + cast_info.uuid, + cast_info.model_name, + cast_info.friendly_name, + cast_info.host, + cast_info.port, + cast_type, + manufacturer, + ) if not self.is_audio_group or self.is_dynamic_group is not None: # We have all information, no need to check HTTP API. diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 389b837f200..7144a3872f5 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==12.0.0"], + "requirements": ["pychromecast==12.1.0"], "after_dependencies": [ "cloud", "http", diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index e63fedd3598..b64c3372c15 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -535,7 +535,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): """Generate root node.""" children = [] # Add media browsers - for platform in self.hass.data[CAST_DOMAIN].values(): + for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values(): children.extend( await platform.async_get_media_browser_root_object( self.hass, self._chromecast.cast_type @@ -587,7 +587,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): if media_content_id is None: return await self._async_root_payload(content_filter) - for platform in self.hass.data[CAST_DOMAIN].values(): + for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values(): browse_media = await platform.async_browse_media( self.hass, media_content_type, @@ -646,7 +646,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): return # Try the cast platforms - for platform in self.hass.data[CAST_DOMAIN].values(): + for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values(): result = await platform.async_play_media( self.hass, self.entity_id, self._chromecast, media_type, media_id ) diff --git a/requirements_all.txt b/requirements_all.txt index 5efd953ff3c..77bedaa4984 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1399,7 +1399,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==12.0.0 +pychromecast==12.1.0 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aedb93f2d37..39f7642ea06 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -938,7 +938,7 @@ pybotvac==0.0.23 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==12.0.0 +pychromecast==12.1.0 # homeassistant.components.climacell pyclimacell==0.18.2 diff --git a/tests/components/cast/conftest.py b/tests/components/cast/conftest.py index 3b96f378906..52152b4a718 100644 --- a/tests/components/cast/conftest.py +++ b/tests/components/cast/conftest.py @@ -14,6 +14,13 @@ def get_multizone_status_mock(): return mock +@pytest.fixture() +def get_cast_type_mock(): + """Mock pychromecast dial.""" + mock = MagicMock(spec_set=pychromecast.dial.get_cast_type) + return mock + + @pytest.fixture() def castbrowser_mock(): """Mock pychromecast CastBrowser.""" @@ -43,6 +50,7 @@ def cast_mock( mz_mock, quick_play_mock, castbrowser_mock, + get_cast_type_mock, get_chromecast_mock, get_multizone_status_mock, ): @@ -52,6 +60,9 @@ def cast_mock( with patch( "homeassistant.components.cast.discovery.pychromecast.discovery.CastBrowser", castbrowser_mock, + ), patch( + "homeassistant.components.cast.helpers.dial.get_cast_type", + get_cast_type_mock, ), patch( "homeassistant.components.cast.helpers.dial.get_multizone_status", get_multizone_status_mock, diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 88cf281c8a5..e4df84f6443 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -64,6 +64,8 @@ FAKE_MDNS_SERVICE = pychromecast.discovery.ServiceInfo( pychromecast.const.SERVICE_TYPE_MDNS, "the-service" ) +UNDEFINED = object() + def get_fake_chromecast(info: ChromecastInfo): """Generate a Fake Chromecast object with the specified arguments.""" @@ -74,7 +76,14 @@ def get_fake_chromecast(info: ChromecastInfo): def get_fake_chromecast_info( - host="192.168.178.42", port=8009, service=None, uuid: UUID | None = FakeUUID + *, + host="192.168.178.42", + port=8009, + service=None, + uuid: UUID | None = FakeUUID, + cast_type=UNDEFINED, + manufacturer=UNDEFINED, + model_name=UNDEFINED, ): """Generate a Fake ChromecastInfo with the specified arguments.""" @@ -82,16 +91,22 @@ def get_fake_chromecast_info( service = pychromecast.discovery.ServiceInfo( pychromecast.const.SERVICE_TYPE_HOST, (host, port) ) + if cast_type is UNDEFINED: + cast_type = CAST_TYPE_GROUP if port != 8009 else CAST_TYPE_CHROMECAST + if manufacturer is UNDEFINED: + manufacturer = "Nabu Casa" + if model_name is UNDEFINED: + model_name = "Chromecast" return ChromecastInfo( cast_info=pychromecast.models.CastInfo( services={service}, uuid=uuid, - model_name="Chromecast", + model_name=model_name, friendly_name="Speaker", host=host, port=port, - cast_type=CAST_TYPE_GROUP if port != 8009 else CAST_TYPE_CHROMECAST, - manufacturer="Nabu Casa", + cast_type=cast_type, + manufacturer=manufacturer, ) ) @@ -342,6 +357,92 @@ async def test_internal_discovery_callback_fill_out_group( get_multizone_status_mock.assert_called_once() +async def test_internal_discovery_callback_fill_out_cast_type_manufacturer( + hass, get_cast_type_mock, caplog +): + """Test internal discovery automatically filling out information.""" + discover_cast, _, _ = await async_setup_cast_internal_discovery(hass) + info = get_fake_chromecast_info( + host="host1", + port=8009, + service=FAKE_MDNS_SERVICE, + cast_type=None, + manufacturer=None, + ) + info2 = get_fake_chromecast_info( + host="host1", + port=8009, + service=FAKE_MDNS_SERVICE, + cast_type=None, + manufacturer=None, + model_name="Model 101", + ) + zconf = get_fake_zconf(host="host1", port=8009) + full_info = attr.evolve( + info, + cast_info=pychromecast.discovery.CastInfo( + services=info.cast_info.services, + uuid=FakeUUID, + model_name="Chromecast", + friendly_name="Speaker", + host=info.cast_info.host, + port=info.cast_info.port, + cast_type="audio", + manufacturer="TrollTech", + ), + is_dynamic_group=None, + ) + full_info2 = attr.evolve( + info2, + cast_info=pychromecast.discovery.CastInfo( + services=info.cast_info.services, + uuid=FakeUUID, + model_name="Model 101", + friendly_name="Speaker", + host=info.cast_info.host, + port=info.cast_info.port, + cast_type="cast", + manufacturer="Cyberdyne Systems", + ), + is_dynamic_group=None, + ) + + get_cast_type_mock.assert_not_called() + get_cast_type_mock.return_value = full_info.cast_info + + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf, + ): + signal = MagicMock() + + async_dispatcher_connect(hass, "cast_discovered", signal) + discover_cast(FAKE_MDNS_SERVICE, info) + await hass.async_block_till_done() + + # when called with incomplete info, it should use HTTP to get missing + get_cast_type_mock.assert_called_once() + assert get_cast_type_mock.call_count == 1 + discover = signal.mock_calls[0][1][0] + assert discover == full_info + assert "Fetched cast details for unknown model 'Chromecast'" in caplog.text + + # Call again, the model name should be fetched from cache + discover_cast(FAKE_MDNS_SERVICE, info) + await hass.async_block_till_done() + assert get_cast_type_mock.call_count == 1 # No additional calls + discover = signal.mock_calls[1][1][0] + assert discover == full_info + + # Call for another model, need to call HTTP again + get_cast_type_mock.return_value = full_info2.cast_info + discover_cast(FAKE_MDNS_SERVICE, info2) + await hass.async_block_till_done() + assert get_cast_type_mock.call_count == 2 + discover = signal.mock_calls[2][1][0] + assert discover == full_info2 + + async def test_stop_discovery_called_on_stop(hass, castbrowser_mock): """Test pychromecast.stop_discovery called on shutdown.""" # start_discovery should be called with empty config From aa69e7646f6c52b9b99216f03f624c6e40f01721 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 5 May 2022 17:40:56 +0200 Subject: [PATCH 0245/3516] Bump library version (#71349) --- 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 2b62500f23b..16231ef0b88 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.2.2"], + "requirements": ["nettigo-air-monitor==1.2.3"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 77bedaa4984..268ef409400 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1065,7 +1065,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.2.2 +nettigo-air-monitor==1.2.3 # homeassistant.components.neurio_energy neurio==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39f7642ea06..aaf2d8217c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -727,7 +727,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.2.2 +nettigo-air-monitor==1.2.3 # homeassistant.components.nexia nexia==0.9.13 From 9f8111cabe0ea85664f8520f58333052b59d7c6e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 May 2022 14:33:37 -0700 Subject: [PATCH 0246/3516] Ignore loading system entity category (#71361) --- homeassistant/helpers/entity_registry.py | 4 ++++ tests/helpers/test_entity_registry.py | 13 ++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index b4dd0820d8c..64ab0323f6c 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -711,6 +711,10 @@ class EntityRegistry: if not valid_entity_id(entity["entity_id"]): continue + # We removed this in 2022.5. Remove this check in 2023.1. + if entity["entity_category"] == "system": + entity["entity_category"] = None + entities[entity["entity_id"]] = RegistryEntry( area_id=entity["area_id"], capabilities=entity["capabilities"], diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 41ac7412d9c..21d29736bd0 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -297,6 +297,12 @@ async def test_loading_extra_values(hass, hass_storage): "unique_id": "invalid-hass", "disabled_by": er.RegistryEntryDisabler.HASS, }, + { + "entity_id": "test.system_entity", + "platform": "super_platform", + "unique_id": "system-entity", + "entity_category": "system", + }, ] }, } @@ -304,7 +310,7 @@ async def test_loading_extra_values(hass, hass_storage): await er.async_load(hass) registry = er.async_get(hass) - assert len(registry.entities) == 4 + assert len(registry.entities) == 5 entry_with_name = registry.async_get_or_create( "test", "super_platform", "with-name" @@ -327,6 +333,11 @@ async def test_loading_extra_values(hass, hass_storage): assert entry_disabled_user.disabled assert entry_disabled_user.disabled_by is er.RegistryEntryDisabler.USER + entry_system_category = registry.async_get_or_create( + "test", "system_entity", "system-entity" + ) + assert entry_system_category.entity_category is None + def test_async_get_entity_id(registry): """Test that entity_id is returned.""" From 61a3873d096fd228fa929371ac126c458511f232 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 May 2022 14:33:17 -0700 Subject: [PATCH 0247/3516] Fix importing blueprints (#71365) Co-authored-by: Shay Levy --- homeassistant/helpers/selector.py | 23 ++++++++++++++++++- tests/components/blueprint/test_importer.py | 2 +- .../blueprint/test_websocket_api.py | 12 +++++++--- tests/helpers/test_config_validation.py | 13 ++++++++++- tests/helpers/test_selector.py | 18 ++++++++++----- .../automation/test_event_service.yaml | 2 ++ 6 files changed, 58 insertions(+), 12 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index eecc66c8332..afe74a4d7af 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -5,11 +5,13 @@ from collections.abc import Callable, Sequence from typing import Any, TypedDict, cast import voluptuous as vol +import yaml from homeassistant.backports.enum import StrEnum from homeassistant.const import CONF_MODE, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.util import decorator +from homeassistant.util.yaml.dumper import represent_odict from . import config_validation as cv @@ -71,7 +73,11 @@ class Selector: def serialize(self) -> Any: """Serialize Selector for voluptuous_serialize.""" - return {"selector": {self.selector_type: self.config}} + return {"selector": {self.selector_type: self.serialize_config()}} + + def serialize_config(self) -> Any: + """Serialize config.""" + return self.config SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA = vol.Schema( @@ -623,6 +629,13 @@ class NumberSelector(Selector): """Instantiate a selector.""" super().__init__(config) + def serialize_config(self) -> Any: + """Serialize the selector config.""" + return { + **self.config, + "mode": self.config["mode"].value, + } + def __call__(self, data: Any) -> float: """Validate the passed selection.""" value: float = vol.Coerce(float)(data) @@ -881,3 +894,11 @@ class TimeSelector(Selector): """Validate the passed selection.""" cv.time(data) return cast(str, data) + + +yaml.SafeDumper.add_representer( + Selector, + lambda dumper, value: represent_odict( + dumper, "tag:yaml.org,2002:map", value.serialize() + ), +) diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 0e1e66405e6..806cdb2cb8d 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -198,7 +198,7 @@ async def test_fetch_blueprint_from_github_url(hass, aioclient_mock, url): assert imported_blueprint.blueprint.domain == "automation" assert imported_blueprint.blueprint.inputs == { "service_to_call": None, - "trigger_event": None, + "trigger_event": {"selector": {"text": {}}}, } assert imported_blueprint.suggested_filename == "balloob/motion_light" assert imported_blueprint.blueprint.metadata["source_url"] == url diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index 51b8184354a..40f24d98016 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -30,7 +30,10 @@ async def test_list_blueprints(hass, hass_ws_client): "test_event_service.yaml": { "metadata": { "domain": "automation", - "input": {"service_to_call": None, "trigger_event": None}, + "input": { + "service_to_call": None, + "trigger_event": {"selector": {"text": {}}}, + }, "name": "Call service based on event", }, }, @@ -89,7 +92,10 @@ async def test_import_blueprint(hass, aioclient_mock, hass_ws_client): "blueprint": { "metadata": { "domain": "automation", - "input": {"service_to_call": None, "trigger_event": None}, + "input": { + "service_to_call": None, + "trigger_event": {"selector": {"text": {}}}, + }, "name": "Call service based on event", "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", }, @@ -123,7 +129,7 @@ async def test_save_blueprint(hass, aioclient_mock, hass_ws_client): assert msg["success"] assert write_mock.mock_calls assert write_mock.call_args[0] == ( - "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n entity_id: light.kitchen\n", + "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n selector:\n text: {}\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n entity_id: light.kitchen\n", ) diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 64c838e6c02..a5d2223a3d2 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -11,7 +11,7 @@ import pytest import voluptuous as vol import homeassistant -from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers import config_validation as cv, selector, template def test_boolean(): @@ -720,6 +720,17 @@ def test_string_in_serializer(): } +def test_selector_in_serializer(): + """Test selector with custom_serializer.""" + assert cv.custom_serializer(selector.selector({"text": {}})) == { + "selector": { + "text": { + "multiline": False, + } + } + } + + def test_positive_time_period_dict_in_serializer(): """Test positive_time_period_dict with custom_serializer.""" assert cv.custom_serializer(cv.positive_time_period_dict) == { diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index cb0ad95eb6b..8c94e3d3c56 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -2,7 +2,8 @@ import pytest import voluptuous as vol -from homeassistant.helpers import config_validation as cv, selector +from homeassistant.helpers import selector +from homeassistant.util import yaml FAKE_UUID = "a266a680b608c32770e6c45bfe6b8411" @@ -48,10 +49,12 @@ def _test_selector( converter = default_converter # Validate selector configuration - selector.validate_selector({selector_type: schema}) + config = {selector_type: schema} + selector.validate_selector(config) + selector_instance = selector.selector(config) # Use selector in schema and validate - vol_schema = vol.Schema({"selection": selector.selector({selector_type: schema})}) + vol_schema = vol.Schema({"selection": selector_instance}) for selection in valid_selections: assert vol_schema({"selection": selection}) == { "selection": converter(selection) @@ -62,9 +65,12 @@ def _test_selector( # Serialize selector selector_instance = selector.selector({selector_type: schema}) - assert cv.custom_serializer(selector_instance) == { - "selector": {selector_type: selector_instance.config} - } + assert ( + selector.selector(selector_instance.serialize()["selector"]).config + == selector_instance.config + ) + # Test serialized selector can be dumped to YAML + yaml.dump(selector_instance.serialize()) @pytest.mark.parametrize( diff --git a/tests/testing_config/blueprints/automation/test_event_service.yaml b/tests/testing_config/blueprints/automation/test_event_service.yaml index ab067b004ac..648cef39b96 100644 --- a/tests/testing_config/blueprints/automation/test_event_service.yaml +++ b/tests/testing_config/blueprints/automation/test_event_service.yaml @@ -3,6 +3,8 @@ blueprint: domain: automation input: trigger_event: + selector: + text: service_to_call: trigger: platform: event From 9a4ce19aff6cb179b6e1549b98f1274d0b912887 Mon Sep 17 00:00:00 2001 From: Shai Ungar Date: Fri, 6 May 2022 00:34:30 +0300 Subject: [PATCH 0248/3516] Add unique ids to sensors (#71367) --- homeassistant/components/sabnzbd/manifest.json | 2 -- homeassistant/components/sabnzbd/sensor.py | 11 ++++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sabnzbd/manifest.json b/homeassistant/components/sabnzbd/manifest.json index 0702446d217..f112893b5e1 100644 --- a/homeassistant/components/sabnzbd/manifest.json +++ b/homeassistant/components/sabnzbd/manifest.json @@ -3,8 +3,6 @@ "name": "SABnzbd", "documentation": "https://www.home-assistant.io/integrations/sabnzbd", "requirements": ["pysabnzbd==1.1.1"], - "dependencies": ["configurator"], - "after_dependencies": ["discovery"], "codeowners": ["@shaiu"], "iot_class": "local_polling", "config_flow": true, diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index 1d661d90848..dee80945e9d 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -14,8 +14,10 @@ from . import DOMAIN, SIGNAL_SABNZBD_UPDATED from ...config_entries import ConfigEntry from ...const import DATA_GIGABYTES, DATA_MEGABYTES, DATA_RATE_MEGABYTES_PER_SECOND from ...core import HomeAssistant +from ...helpers.device_registry import DeviceEntryType +from ...helpers.entity import DeviceInfo from ...helpers.entity_platform import AddEntitiesCallback -from .const import KEY_API_DATA, KEY_NAME +from .const import DEFAULT_NAME, KEY_API_DATA, KEY_NAME @dataclass @@ -127,9 +129,16 @@ class SabnzbdSensor(SensorEntity): self, sabnzbd_api_data, client_name, description: SabnzbdSensorEntityDescription ): """Initialize the sensor.""" + unique_id = description.key + self._attr_unique_id = unique_id self.entity_description = description self._sabnzbd_api = sabnzbd_api_data self._attr_name = f"{client_name} {description.name}" + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, DOMAIN)}, + name=DEFAULT_NAME, + ) async def async_added_to_hass(self): """Call when entity about to be added to hass.""" From e2ae62ea9579bf91798716d9fd6fa16b6da15a19 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 May 2022 14:43:44 -0700 Subject: [PATCH 0249/3516] Bumped version to 2022.5.1 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5c7b28e1156..c2cb8119602 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index beab88224e5..04a68db8ca6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.5.0 +version = 2022.5.1 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 1d95a37eab2907494d1679d3e2d720e524b0420e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 May 2022 15:01:25 -0700 Subject: [PATCH 0250/3516] Bump pychromecast to 12.1.1 (#71377) --- 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 7144a3872f5..cee46913937 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==12.1.0"], + "requirements": ["pychromecast==12.1.1"], "after_dependencies": [ "cloud", "http", diff --git a/requirements_all.txt b/requirements_all.txt index eae4a2c3920..1f68cb43b04 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1402,7 +1402,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==12.1.0 +pychromecast==12.1.1 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0cb0cf2b69f..911f726effd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -941,7 +941,7 @@ pybotvac==0.0.23 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==12.1.0 +pychromecast==12.1.1 # homeassistant.components.climacell pyclimacell==0.18.2 From 4ae596fef296c6db241065a7f034ea499e89bf62 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 5 May 2022 15:01:25 -0700 Subject: [PATCH 0251/3516] Bump pychromecast to 12.1.1 (#71377) --- 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 7144a3872f5..cee46913937 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==12.1.0"], + "requirements": ["pychromecast==12.1.1"], "after_dependencies": [ "cloud", "http", diff --git a/requirements_all.txt b/requirements_all.txt index 268ef409400..b6d512d97be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1399,7 +1399,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==12.1.0 +pychromecast==12.1.1 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aaf2d8217c9..6f93d8c5e82 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -938,7 +938,7 @@ pybotvac==0.0.23 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==12.1.0 +pychromecast==12.1.1 # homeassistant.components.climacell pyclimacell==0.18.2 From 6722d060dd038aad2c9adcd4efa3437b714d9689 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 May 2022 18:49:30 -0400 Subject: [PATCH 0252/3516] Bump yalexs to 1.1.24 (#71372) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 329522bef28..c1156c9aea9 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.23"], + "requirements": ["yalexs==1.1.24"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 1f68cb43b04..f47df3386ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2465,7 +2465,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.august -yalexs==1.1.23 +yalexs==1.1.24 # homeassistant.components.yeelight yeelight==0.7.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 911f726effd..4d7b846f10a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1614,7 +1614,7 @@ xmltodict==0.12.0 yalesmartalarmclient==0.3.8 # homeassistant.components.august -yalexs==1.1.23 +yalexs==1.1.24 # homeassistant.components.yeelight yeelight==0.7.10 From c22cf3b3d2e4d079499473500abf8a73e5b80163 Mon Sep 17 00:00:00 2001 From: Graham Arthur Blair Date: Thu, 5 May 2022 16:12:51 -0700 Subject: [PATCH 0253/3516] Add buttons to Ring chime devices to play ding and motion chimes (#71370) Co-authored-by: Paulus Schoutsen --- homeassistant/components/ring/__init__.py | 1 + homeassistant/components/ring/button.py | 65 +++++++++++++++++++++++ tests/components/ring/test_button.py | 55 +++++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 homeassistant/components/ring/button.py create mode 100644 tests/components/ring/test_button.py diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 5d834976370..2a922a7bb3e 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -32,6 +32,7 @@ DEFAULT_ENTITY_NAMESPACE = "ring" PLATFORMS = [ Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH, diff --git a/homeassistant/components/ring/button.py b/homeassistant/components/ring/button.py new file mode 100644 index 00000000000..04f9f7950f8 --- /dev/null +++ b/homeassistant/components/ring/button.py @@ -0,0 +1,65 @@ +"""This component provides HA button support for Ring Chimes.""" +import logging + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import DOMAIN +from .entity import RingEntityMixin + +_LOGGER = logging.getLogger(__name__) + +BELL_ICON = "mdi:bell-ring" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Create the buttons for the Ring devices.""" + devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] + buttons = [] + + # add one button for each test chime type (ding, motion) + for device in devices["chimes"]: + buttons.append(ChimeButton(config_entry.entry_id, device, "ding")) + buttons.append(ChimeButton(config_entry.entry_id, device, "motion")) + + async_add_entities(buttons) + + +class BaseRingButton(RingEntityMixin, ButtonEntity): + """Represents a Button for controlling an aspect of a ring device.""" + + def __init__(self, config_entry_id, device, button_identifier, button_name): + """Initialize the switch.""" + super().__init__(config_entry_id, device) + self._button_identifier = button_identifier + self._button_name = button_name + self._attr_unique_id = f"{self._device.id}-{self._button_identifier}" + + @property + def name(self): + """Name of the device.""" + return f"{self._device.name} {self._button_name}" + + +class ChimeButton(BaseRingButton): + """Creates a button to play the test chime of a Chime device.""" + + _attr_icon = BELL_ICON + + def __init__(self, config_entry_id, device, kind): + """Initialize the button for a device with a chime.""" + super().__init__( + config_entry_id, device, f"play-chime-{kind}", f"Play chime: {kind}" + ) + self.kind = kind + + def press(self) -> None: + """Send the test chime request.""" + if not self._device.test_sound(kind=self.kind): + _LOGGER.error("Failed to ring chime sound on %s", self.name) diff --git a/tests/components/ring/test_button.py b/tests/components/ring/test_button.py new file mode 100644 index 00000000000..62b2fcd8a78 --- /dev/null +++ b/tests/components/ring/test_button.py @@ -0,0 +1,55 @@ +"""The tests for the Ring button platform.""" + +from homeassistant.const import Platform +from homeassistant.helpers import entity_registry as er + +from .common import setup_platform + + +async def test_entity_registry(hass, requests_mock): + """Tests that the devices are registered in the entity registry.""" + await setup_platform(hass, Platform.BUTTON) + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get("button.downstairs_play_chime_ding") + assert entry.unique_id == "123456-play-chime-ding" + + entry = entity_registry.async_get("button.downstairs_play_chime_motion") + assert entry.unique_id == "123456-play-chime-motion" + + +async def test_play_chime_buttons_report_correctly(hass, requests_mock): + """Tests that the initial state of a device that should be on is correct.""" + await setup_platform(hass, Platform.BUTTON) + + state = hass.states.get("button.downstairs_play_chime_ding") + assert state.attributes.get("friendly_name") == "Downstairs Play chime: ding" + assert state.attributes.get("icon") == "mdi:bell-ring" + + state = hass.states.get("button.downstairs_play_chime_motion") + assert state.attributes.get("friendly_name") == "Downstairs Play chime: motion" + assert state.attributes.get("icon") == "mdi:bell-ring" + + +async def test_chime_can_be_played(hass, requests_mock): + """Tests the play chime request is sent correctly.""" + await setup_platform(hass, Platform.BUTTON) + + # Mocks the response for playing a test sound + requests_mock.post( + "https://api.ring.com/clients_api/chimes/123456/play_sound", + text="SUCCESS", + ) + await hass.services.async_call( + "button", + "press", + {"entity_id": "button.downstairs_play_chime_ding"}, + blocking=True, + ) + + await hass.async_block_till_done() + + assert requests_mock.request_history[-1].url.startswith( + "https://api.ring.com/clients_api/chimes/123456/play_sound?" + ) + assert "kind=ding" in requests_mock.request_history[-1].url From 07706fa62ac4a34fcfa0dd2b5cf5e84ea8b4882f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 6 May 2022 00:22:16 +0000 Subject: [PATCH 0254/3516] [ci skip] Translation update --- .../translations/tr.json | 3 ++ .../components/asuswrt/translations/hu.json | 1 + .../components/asuswrt/translations/no.json | 1 + .../components/asuswrt/translations/sv.json | 3 ++ .../components/asuswrt/translations/tr.json | 3 ++ .../asuswrt/translations/zh-Hant.json | 1 + .../components/axis/translations/sv.json | 2 +- .../components/deconz/translations/sv.json | 1 + .../components/deconz/translations/tr.json | 1 + .../components/dlna_dmr/translations/ca.json | 1 + .../components/dlna_dmr/translations/de.json | 1 + .../components/dlna_dmr/translations/el.json | 1 + .../components/dlna_dmr/translations/et.json | 1 + .../components/dlna_dmr/translations/fr.json | 1 + .../components/dlna_dmr/translations/hu.json | 1 + .../components/dlna_dmr/translations/id.json | 1 + .../components/dlna_dmr/translations/no.json | 1 + .../dlna_dmr/translations/pt-BR.json | 1 + .../components/dlna_dmr/translations/sv.json | 11 ++++++ .../components/dlna_dmr/translations/tr.json | 1 + .../dlna_dmr/translations/zh-Hant.json | 1 + .../components/esphome/translations/sv.json | 1 + .../components/generic/translations/sv.json | 7 ++++ .../components/hassio/translations/ca.json | 4 +-- .../homekit_controller/translations/sv.json | 2 +- .../components/insteon/translations/sv.json | 7 ++++ .../intellifire/translations/tr.json | 2 +- .../components/isy994/translations/sv.json | 11 ++++++ .../components/isy994/translations/tr.json | 13 +++++-- .../components/lovelace/translations/sv.json | 7 ++++ .../components/meater/translations/sv.json | 17 +++++++++ .../components/meater/translations/tr.json | 9 +++++ .../media_player/translations/tr.json | 3 ++ .../components/onewire/translations/sv.json | 9 +++++ .../components/qnap_qsw/translations/sv.json | 8 +++++ .../components/qnap_qsw/translations/tr.json | 21 +++++++++++ .../qnap_qsw/translations/zh-Hans.json | 21 +++++++++++ .../components/recorder/translations/sv.json | 8 +++++ .../components/recorder/translations/tr.json | 8 +++++ .../components/sabnzbd/translations/sv.json | 17 +++++++++ .../components/sabnzbd/translations/tr.json | 18 ++++++++++ .../simplisafe/translations/sv.json | 10 ++++++ .../simplisafe/translations/tr.json | 17 +++++++-- .../components/slimproto/translations/sv.json | 13 +++++++ .../components/slimproto/translations/tr.json | 7 ++++ .../components/sql/translations/sv.json | 23 ++++++++++++ .../components/sql/translations/tr.json | 4 +++ .../steam_online/translations/sv.json | 24 +++++++++++++ .../steam_online/translations/tr.json | 36 +++++++++++++++++++ .../steam_online/translations/zh-Hans.json | 27 ++++++++++++++ .../components/tautulli/translations/sv.json | 9 +++++ .../components/tautulli/translations/tr.json | 30 ++++++++++++++++ .../trafikverket_ferry/translations/tr.json | 30 ++++++++++++++++ .../components/unifi/translations/sv.json | 3 ++ .../components/unifi/translations/tr.json | 5 ++- .../utility_meter/translations/sv.json | 3 ++ .../components/vulcan/translations/ca.json | 9 +++++ .../components/vulcan/translations/el.json | 9 +++++ .../components/vulcan/translations/en.json | 9 +++++ .../components/vulcan/translations/fr.json | 9 +++++ .../components/vulcan/translations/hu.json | 9 +++++ .../components/vulcan/translations/pt-BR.json | 9 +++++ .../components/vulcan/translations/tr.json | 9 +++++ 63 files changed, 524 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/application_credentials/translations/tr.json create mode 100644 homeassistant/components/dlna_dmr/translations/sv.json create mode 100644 homeassistant/components/generic/translations/sv.json create mode 100644 homeassistant/components/insteon/translations/sv.json create mode 100644 homeassistant/components/lovelace/translations/sv.json create mode 100644 homeassistant/components/meater/translations/sv.json create mode 100644 homeassistant/components/qnap_qsw/translations/sv.json create mode 100644 homeassistant/components/qnap_qsw/translations/tr.json create mode 100644 homeassistant/components/qnap_qsw/translations/zh-Hans.json create mode 100644 homeassistant/components/recorder/translations/sv.json create mode 100644 homeassistant/components/recorder/translations/tr.json create mode 100644 homeassistant/components/sabnzbd/translations/sv.json create mode 100644 homeassistant/components/sabnzbd/translations/tr.json create mode 100644 homeassistant/components/slimproto/translations/sv.json create mode 100644 homeassistant/components/slimproto/translations/tr.json create mode 100644 homeassistant/components/sql/translations/sv.json create mode 100644 homeassistant/components/steam_online/translations/sv.json create mode 100644 homeassistant/components/steam_online/translations/tr.json create mode 100644 homeassistant/components/steam_online/translations/zh-Hans.json create mode 100644 homeassistant/components/tautulli/translations/sv.json create mode 100644 homeassistant/components/tautulli/translations/tr.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/tr.json create mode 100644 homeassistant/components/utility_meter/translations/sv.json diff --git a/homeassistant/components/application_credentials/translations/tr.json b/homeassistant/components/application_credentials/translations/tr.json new file mode 100644 index 00000000000..70da28ed814 --- /dev/null +++ b/homeassistant/components/application_credentials/translations/tr.json @@ -0,0 +1,3 @@ +{ + "title": "Uygulama Kimlik Bilgileri" +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/hu.json b/homeassistant/components/asuswrt/translations/hu.json index 8cec2c31736..86339d7274c 100644 --- a/homeassistant/components/asuswrt/translations/hu.json +++ b/homeassistant/components/asuswrt/translations/hu.json @@ -2,6 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Lehetetlen \u00e9rv\u00e9nyes egyedi azonos\u00edt\u00f3t meghat\u00e1rozni az eszk\u00f6zh\u00f6z", + "no_unique_id": "Az \u00e9rv\u00e9nyes egyedi azonos\u00edt\u00f3 n\u00e9lk\u00fcli eszk\u00f6z m\u00e1r konfigur\u00e1lva van. T\u00f6bb p\u00e9ld\u00e1ny konfigur\u00e1l\u00e1sa nem lehets\u00e9ges", "not_unique_id_exist": "Egy \u00e9rv\u00e9nyes UniqueID azonos\u00edt\u00f3val nem rendelkez\u0151 eszk\u00f6z m\u00e1r konfigur\u00e1lva van. T\u00f6bb p\u00e9ld\u00e1ny konfigur\u00e1l\u00e1sa nem lehets\u00e9ges", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, diff --git a/homeassistant/components/asuswrt/translations/no.json b/homeassistant/components/asuswrt/translations/no.json index b68e771646f..5aa48649989 100644 --- a/homeassistant/components/asuswrt/translations/no.json +++ b/homeassistant/components/asuswrt/translations/no.json @@ -2,6 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Umulig \u00e5 bestemme en gyldig unik ID for enheten", + "no_unique_id": "En enhet uten en gyldig unik ID er allerede konfigurert. Konfigurasjon av flere forekomster er ikke mulig", "not_unique_id_exist": "En enhet uten en gyldig UniqueID er allerede konfigurert. Konfigurasjon av flere forekomster er ikke mulig", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, diff --git a/homeassistant/components/asuswrt/translations/sv.json b/homeassistant/components/asuswrt/translations/sv.json index 9189aba6208..ba461c681e3 100644 --- a/homeassistant/components/asuswrt/translations/sv.json +++ b/homeassistant/components/asuswrt/translations/sv.json @@ -1,6 +1,9 @@ { "config": { "abort": { + "invalid_unique_id": "Om\u00f6jligt att fastst\u00e4lla ett giltigt unikt ID f\u00f6r enheten", + "no_unique_id": "En enhet utan ett giltigt unikt ID \u00e4r redan konfigurerad. Konfiguration av flera instanser \u00e4r inte m\u00f6jlig", + "not_unique_id_exist": "En enhet utan ett giltigt unikt ID \u00e4r redan konfigurerad. Konfiguration av flera instanser \u00e4r inte m\u00f6jlig", "single_instance_allowed": "Redan konfigurerad. Bara en konfiguration \u00e4r m\u00f6jlig." }, "error": { diff --git a/homeassistant/components/asuswrt/translations/tr.json b/homeassistant/components/asuswrt/translations/tr.json index 3225c78b40a..10be601fd10 100644 --- a/homeassistant/components/asuswrt/translations/tr.json +++ b/homeassistant/components/asuswrt/translations/tr.json @@ -1,6 +1,9 @@ { "config": { "abort": { + "invalid_unique_id": "Cihaz i\u00e7in ge\u00e7erli bir benzersiz kimlik belirlemek imkans\u0131z", + "no_unique_id": "Ge\u00e7erli bir benzersiz kimli\u011fi olmayan bir cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Birden \u00e7ok \u00f6rne\u011fin yap\u0131land\u0131r\u0131lmas\u0131 m\u00fcmk\u00fcn de\u011fil", + "not_unique_id_exist": "Ge\u00e7erli bir benzersiz kimli\u011fi olmayan bir cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Birden \u00e7ok \u00f6rne\u011fin yap\u0131land\u0131r\u0131lmas\u0131 m\u00fcmk\u00fcn de\u011fil", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, "error": { diff --git a/homeassistant/components/asuswrt/translations/zh-Hant.json b/homeassistant/components/asuswrt/translations/zh-Hant.json index f78a0466a5c..d5955313b93 100644 --- a/homeassistant/components/asuswrt/translations/zh-Hant.json +++ b/homeassistant/components/asuswrt/translations/zh-Hant.json @@ -2,6 +2,7 @@ "config": { "abort": { "invalid_unique_id": "\u7121\u6cd5\u78ba\u8a8d\u88dd\u7f6e\u6709\u6548\u552f\u4e00 ID", + "no_unique_id": "\u5df2\u8a2d\u5b9a\u4e0d\u5177\u6709\u6548\u552f\u4e00 ID \u7684\u88dd\u7f6e\uff0c\u7121\u6cd5\u8a2d\u5b9a\u591a\u500b\u5be6\u4f8b", "not_unique_id_exist": "\u5df2\u8a2d\u5b9a\u4e0d\u5177\u6709\u6548\u552f\u4e00 ID \u7684\u88dd\u7f6e\uff0c\u7121\u6cd5\u8a2d\u5b9a\u591a\u500b\u5be6\u4f8b", "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, diff --git a/homeassistant/components/axis/translations/sv.json b/homeassistant/components/axis/translations/sv.json index 22e9344b64d..e04267cb5d7 100644 --- a/homeassistant/components/axis/translations/sv.json +++ b/homeassistant/components/axis/translations/sv.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad", - "link_local_address": "Link local addresses are not supported", + "link_local_address": "Lokala l\u00e4nkadresser st\u00f6ds inte", "not_axis_device": "Uppt\u00e4ckte enhet som inte \u00e4r en Axis enhet" }, "error": { diff --git a/homeassistant/components/deconz/translations/sv.json b/homeassistant/components/deconz/translations/sv.json index d7ec321ff36..1a867d0c7fb 100644 --- a/homeassistant/components/deconz/translations/sv.json +++ b/homeassistant/components/deconz/translations/sv.json @@ -8,6 +8,7 @@ "updated_instance": "Uppdaterad deCONZ-instans med ny v\u00e4rdadress" }, "error": { + "linking_not_possible": "Det gick inte att l\u00e4nka till gatewayen", "no_key": "Det gick inte att ta emot en API-nyckel" }, "flow_title": "deCONZ Zigbee gateway ({host})", diff --git a/homeassistant/components/deconz/translations/tr.json b/homeassistant/components/deconz/translations/tr.json index 021c4845989..77045776c98 100644 --- a/homeassistant/components/deconz/translations/tr.json +++ b/homeassistant/components/deconz/translations/tr.json @@ -9,6 +9,7 @@ "updated_instance": "DeCONZ yeni ana bilgisayar adresiyle g\u00fcncelle\u015ftirildi" }, "error": { + "linking_not_possible": "A\u011f ge\u00e7idi ile ba\u011flant\u0131 kurulamad\u0131", "no_key": "API anahtar\u0131 al\u0131namad\u0131" }, "flow_title": "{host}", diff --git a/homeassistant/components/dlna_dmr/translations/ca.json b/homeassistant/components/dlna_dmr/translations/ca.json index 944af3bfebc..7c528d5ad29 100644 --- a/homeassistant/components/dlna_dmr/translations/ca.json +++ b/homeassistant/components/dlna_dmr/translations/ca.json @@ -47,6 +47,7 @@ "step": { "init": { "data": { + "browse_unfiltered": "Mostra fitxers multim\u00e8dia incompatibles mentre navegui", "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" diff --git a/homeassistant/components/dlna_dmr/translations/de.json b/homeassistant/components/dlna_dmr/translations/de.json index e37786c401c..baf1a53efca 100644 --- a/homeassistant/components/dlna_dmr/translations/de.json +++ b/homeassistant/components/dlna_dmr/translations/de.json @@ -47,6 +47,7 @@ "step": { "init": { "data": { + "browse_unfiltered": "Inkompatible Medien beim Durchsuchen anzeigen", "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" diff --git a/homeassistant/components/dlna_dmr/translations/el.json b/homeassistant/components/dlna_dmr/translations/el.json index 91a1d353bcc..5dc56ef3e37 100644 --- a/homeassistant/components/dlna_dmr/translations/el.json +++ b/homeassistant/components/dlna_dmr/translations/el.json @@ -47,6 +47,7 @@ "step": { "init": { "data": { + "browse_unfiltered": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03bc\u03b7 \u03c3\u03c5\u03bc\u03b2\u03b1\u03c4\u03ce\u03bd \u03bc\u03ad\u03c3\u03c9\u03bd \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03b5\u03c1\u03b9\u03ae\u03b3\u03b7\u03c3\u03b7", "callback_url_override": "URL \u03ba\u03bb\u03ae\u03c3\u03b7\u03c2 \u03b1\u03ba\u03c1\u03bf\u03b1\u03c4\u03ae \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03bf\u03c2", "listen_port": "\u0398\u03cd\u03c1\u03b1 \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03c9\u03bd (\u03c4\u03c5\u03c7\u03b1\u03af\u03b1 \u03b1\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af)", "poll_availability": "\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03b8\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" diff --git a/homeassistant/components/dlna_dmr/translations/et.json b/homeassistant/components/dlna_dmr/translations/et.json index b5019f5f9a1..1981b6eefde 100644 --- a/homeassistant/components/dlna_dmr/translations/et.json +++ b/homeassistant/components/dlna_dmr/translations/et.json @@ -47,6 +47,7 @@ "step": { "init": { "data": { + "browse_unfiltered": "Kuva sirvimisel \u00fchildumatu meedia", "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" diff --git a/homeassistant/components/dlna_dmr/translations/fr.json b/homeassistant/components/dlna_dmr/translations/fr.json index 6554a3994d6..87686251c4d 100644 --- a/homeassistant/components/dlna_dmr/translations/fr.json +++ b/homeassistant/components/dlna_dmr/translations/fr.json @@ -47,6 +47,7 @@ "step": { "init": { "data": { + "browse_unfiltered": "Afficher les fichiers multim\u00e9dias incompatibles lors de la navigation", "callback_url_override": "URL de rappel de l'\u00e9couteur d'\u00e9v\u00e9nement", "listen_port": "Port d'\u00e9coute d'\u00e9v\u00e9nement (al\u00e9atoire s'il n'est pas d\u00e9fini)", "poll_availability": "Sondage pour la disponibilit\u00e9 de l'appareil" diff --git a/homeassistant/components/dlna_dmr/translations/hu.json b/homeassistant/components/dlna_dmr/translations/hu.json index 5e2ba148602..2773cf2bae4 100644 --- a/homeassistant/components/dlna_dmr/translations/hu.json +++ b/homeassistant/components/dlna_dmr/translations/hu.json @@ -47,6 +47,7 @@ "step": { "init": { "data": { + "browse_unfiltered": "Inkompatibilis m\u00e9dia megjelen\u00edt\u00e9se b\u00f6ng\u00e9sz\u00e9s k\u00f6zben", "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" diff --git a/homeassistant/components/dlna_dmr/translations/id.json b/homeassistant/components/dlna_dmr/translations/id.json index 152c4f73a56..415ab7dc653 100644 --- a/homeassistant/components/dlna_dmr/translations/id.json +++ b/homeassistant/components/dlna_dmr/translations/id.json @@ -47,6 +47,7 @@ "step": { "init": { "data": { + "browse_unfiltered": "Tampilkan media yang tidak kompatibel saat menjelajah", "callback_url_override": "URL panggilan balik pendengar peristiwa", "listen_port": "Port pendengar peristiwa (acak jika tidak diatur)", "poll_availability": "Polling untuk ketersediaan perangkat" diff --git a/homeassistant/components/dlna_dmr/translations/no.json b/homeassistant/components/dlna_dmr/translations/no.json index a1ce1fdce32..04fed89ca78 100644 --- a/homeassistant/components/dlna_dmr/translations/no.json +++ b/homeassistant/components/dlna_dmr/translations/no.json @@ -47,6 +47,7 @@ "step": { "init": { "data": { + "browse_unfiltered": "Vis inkompatible medier n\u00e5r du surfer", "callback_url_override": "URL for tilbakeringing av hendelseslytter", "listen_port": "Hendelseslytterport (tilfeldig hvis den ikke er angitt)", "poll_availability": "Avstemning for tilgjengelighet av enheter" diff --git a/homeassistant/components/dlna_dmr/translations/pt-BR.json b/homeassistant/components/dlna_dmr/translations/pt-BR.json index 0df5a60e565..5caf64af4b1 100644 --- a/homeassistant/components/dlna_dmr/translations/pt-BR.json +++ b/homeassistant/components/dlna_dmr/translations/pt-BR.json @@ -47,6 +47,7 @@ "step": { "init": { "data": { + "browse_unfiltered": "Mostrar m\u00eddia incompat\u00edvel ao navegar", "callback_url_override": "URL de retorno do ouvinte de eventos", "listen_port": "Porta do ouvinte de eventos (aleat\u00f3rio se n\u00e3o estiver definido)", "poll_availability": "Pesquisa de disponibilidade do dispositivo" diff --git a/homeassistant/components/dlna_dmr/translations/sv.json b/homeassistant/components/dlna_dmr/translations/sv.json new file mode 100644 index 00000000000..8f95dda75db --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/sv.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "browse_unfiltered": "Visa inkompatibla media n\u00e4r du surfar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/tr.json b/homeassistant/components/dlna_dmr/translations/tr.json index 59c5221b870..bee4a922a75 100644 --- a/homeassistant/components/dlna_dmr/translations/tr.json +++ b/homeassistant/components/dlna_dmr/translations/tr.json @@ -47,6 +47,7 @@ "step": { "init": { "data": { + "browse_unfiltered": "Tarama s\u0131ras\u0131nda uyumsuz medyay\u0131 g\u00f6ster", "callback_url_override": "Olay dinleyici geri \u00e7a\u011f\u0131rma URL'si", "listen_port": "Olay dinleyici ba\u011flant\u0131 noktas\u0131 (ayarlanmam\u0131\u015fsa rastgele)", "poll_availability": "Cihaz kullan\u0131labilirli\u011fi i\u00e7in anket" diff --git a/homeassistant/components/dlna_dmr/translations/zh-Hant.json b/homeassistant/components/dlna_dmr/translations/zh-Hant.json index f085767565d..c75334df1ae 100644 --- a/homeassistant/components/dlna_dmr/translations/zh-Hant.json +++ b/homeassistant/components/dlna_dmr/translations/zh-Hant.json @@ -47,6 +47,7 @@ "step": { "init": { "data": { + "browse_unfiltered": "\u7576\u700f\u89bd\u6642\u986f\u793a\u4e0d\u76f8\u5bb9\u5a92\u9ad4", "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" diff --git a/homeassistant/components/esphome/translations/sv.json b/homeassistant/components/esphome/translations/sv.json index 40fbbf86bc6..88a9ca92377 100644 --- a/homeassistant/components/esphome/translations/sv.json +++ b/homeassistant/components/esphome/translations/sv.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "ESP \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { diff --git a/homeassistant/components/generic/translations/sv.json b/homeassistant/components/generic/translations/sv.json new file mode 100644 index 00000000000..78033d17c6e --- /dev/null +++ b/homeassistant/components/generic/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga enheter hittades i n\u00e4tverket" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/ca.json b/homeassistant/components/hassio/translations/ca.json index 7813d970d0e..6a6874662b2 100644 --- a/homeassistant/components/hassio/translations/ca.json +++ b/homeassistant/components/hassio/translations/ca.json @@ -2,7 +2,7 @@ "system_health": { "info": { "board": "Placa", - "disk_total": "Total disc", + "disk_total": "Total del disc", "disk_used": "Emmagatzematge utilitzat", "docker_version": "Versi\u00f3 de Docker", "healthy": "Saludable", @@ -11,7 +11,7 @@ "supervisor_api": "API del Supervisor", "supervisor_version": "Versi\u00f3 del Supervisor", "supported": "Compatible", - "update_channel": "Canal d'actualitzaci\u00f3", + "update_channel": "Canal d'actualitzacions", "version_api": "Versi\u00f3 d'APIs" } } diff --git a/homeassistant/components/homekit_controller/translations/sv.json b/homeassistant/components/homekit_controller/translations/sv.json index 71425e0f208..348c0305e03 100644 --- a/homeassistant/components/homekit_controller/translations/sv.json +++ b/homeassistant/components/homekit_controller/translations/sv.json @@ -22,7 +22,7 @@ "data": { "pairing_code": "Parningskod" }, - "description": "Ange din HomeKit-parningskod (i formatet XXX-XX-XXX) f\u00f6r att anv\u00e4nda det h\u00e4r tillbeh\u00f6ret", + "description": "HomeKit Controller kommunicerar med {name} \u00f6ver det lokala n\u00e4tverket med hj\u00e4lp av en s\u00e4ker krypterad anslutning utan en separat HomeKit-kontroller eller iCloud. Ange din HomeKit-kopplingskod (i formatet XXX-XX-XXX) f\u00f6r att anv\u00e4nda detta tillbeh\u00f6r. Denna kod finns vanligtvis p\u00e5 sj\u00e4lva enheten eller i f\u00f6rpackningen.", "title": "Para HomeKit-tillbeh\u00f6r" }, "user": { diff --git a/homeassistant/components/insteon/translations/sv.json b/homeassistant/components/insteon/translations/sv.json new file mode 100644 index 00000000000..b8b6834022c --- /dev/null +++ b/homeassistant/components/insteon/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_insteon_device": "Uppt\u00e4ckt enhet \u00e4r inte en Insteon-enhet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/intellifire/translations/tr.json b/homeassistant/components/intellifire/translations/tr.json index 3a22e7c3680..b5be24edd2f 100644 --- a/homeassistant/components/intellifire/translations/tr.json +++ b/homeassistant/components/intellifire/translations/tr.json @@ -24,7 +24,7 @@ }, "manual_device_entry": { "data": { - "host": "Sunucu (IP Adresi)" + "host": "Ana Bilgisayar (IP Adresi)" }, "description": "Yerel Yap\u0131land\u0131rma" }, diff --git a/homeassistant/components/isy994/translations/sv.json b/homeassistant/components/isy994/translations/sv.json index 23c825f256f..bd14c77dd60 100644 --- a/homeassistant/components/isy994/translations/sv.json +++ b/homeassistant/components/isy994/translations/sv.json @@ -1,6 +1,17 @@ { "config": { + "error": { + "reauth_successful": "\u00c5terautentiseringen lyckades" + }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + }, + "description": "Inloggningsuppgifterna f\u00f6r {host} \u00e4r inte l\u00e4ngre giltiga.", + "title": "Autentisera din ISY igen" + }, "user": { "data": { "username": "Anv\u00e4ndarnamn" diff --git a/homeassistant/components/isy994/translations/tr.json b/homeassistant/components/isy994/translations/tr.json index 78fc66196f1..3999ee16163 100644 --- a/homeassistant/components/isy994/translations/tr.json +++ b/homeassistant/components/isy994/translations/tr.json @@ -7,10 +7,19 @@ "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", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", "unknown": "Beklenmeyen hata" }, "flow_title": "{name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "{host} kimlik bilgileri art\u0131k ge\u00e7erli de\u011fil.", + "title": "ISY'nizi yeniden do\u011frulay\u0131n" + }, "user": { "data": { "host": "URL", @@ -19,7 +28,7 @@ "username": "Kullan\u0131c\u0131 Ad\u0131" }, "description": "Ana bilgisayar giri\u015fi tam URL bi\u00e7iminde olmal\u0131d\u0131r, \u00f6r. http://192.168.10.100:80", - "title": "ISY994'\u00fcn\u00fcze ba\u011flan\u0131n" + "title": "ISY'nize ba\u011flan\u0131n" } } }, @@ -33,7 +42,7 @@ "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.", - "title": "ISY994 Se\u00e7enekleri" + "title": "ISY Se\u00e7enekleri" } } }, diff --git a/homeassistant/components/lovelace/translations/sv.json b/homeassistant/components/lovelace/translations/sv.json new file mode 100644 index 00000000000..b622e036a0f --- /dev/null +++ b/homeassistant/components/lovelace/translations/sv.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "dashboards": "Kontrollpaneler" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/sv.json b/homeassistant/components/meater/translations/sv.json new file mode 100644 index 00000000000..383fbbeb5a6 --- /dev/null +++ b/homeassistant/components/meater/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Bekr\u00e4fta l\u00f6senordet f\u00f6r Meater Cloud-kontot {username}." + }, + "user": { + "data_description": { + "username": "Meater Cloud anv\u00e4ndarnamn, vanligtvis en e-postadress." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/tr.json b/homeassistant/components/meater/translations/tr.json index 03f77d2ed51..14618f6c955 100644 --- a/homeassistant/components/meater/translations/tr.json +++ b/homeassistant/components/meater/translations/tr.json @@ -6,11 +6,20 @@ "unknown_auth_error": "Beklenmeyen hata" }, "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "Meater Bulut hesab\u0131 {username} i\u00e7in parolay\u0131 onaylay\u0131n." + }, "user": { "data": { "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, + "data_description": { + "username": "Meater Cloud kullan\u0131c\u0131 ad\u0131, genellikle bir e-posta adresi." + }, "description": "Meater Cloud hesab\u0131n\u0131z\u0131 kurun." } } diff --git a/homeassistant/components/media_player/translations/tr.json b/homeassistant/components/media_player/translations/tr.json index 5782730f6bf..62958c54fea 100644 --- a/homeassistant/components/media_player/translations/tr.json +++ b/homeassistant/components/media_player/translations/tr.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_buffering": "{entity_name} arabelle\u011fe al\u0131yor", "is_idle": "{entity_name} bo\u015fta", "is_off": "{entity_name} kapal\u0131", "is_on": "{entity_name} a\u00e7\u0131k", @@ -8,6 +9,7 @@ "is_playing": "{entity_name} oynat\u0131l\u0131yor" }, "trigger_type": { + "buffering": "{entity_name} arabelle\u011fe almaya ba\u015flar", "changed_states": "{entity_name} durumlar\u0131 de\u011fi\u015ftirdi", "idle": "{entity_name} bo\u015fta", "paused": "{entity_name} duraklat\u0131ld\u0131", @@ -18,6 +20,7 @@ }, "state": { "_": { + "buffering": "\u00d6n belle\u011fe al\u0131n\u0131yor", "idle": "Bo\u015fta", "off": "Kapal\u0131", "on": "A\u00e7\u0131k", diff --git a/homeassistant/components/onewire/translations/sv.json b/homeassistant/components/onewire/translations/sv.json index e5717348568..3265a54aeaf 100644 --- a/homeassistant/components/onewire/translations/sv.json +++ b/homeassistant/components/onewire/translations/sv.json @@ -7,5 +7,14 @@ } } } + }, + "options": { + "step": { + "device_selection": { + "data": { + "device_selection": "V\u00e4lj enheter att konfigurera" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/sv.json b/homeassistant/components/qnap_qsw/translations/sv.json new file mode 100644 index 00000000000..416ef964cf3 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/sv.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "invalid_id": "Enheten returnerade ett ogiltigt unikt ID" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/tr.json b/homeassistant/components/qnap_qsw/translations/tr.json new file mode 100644 index 00000000000..309f2da3a90 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "invalid_id": "Cihaz ge\u00e7ersiz bir benzersiz kimlik d\u00f6nd\u00fcrd\u00fc" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "url": "URL", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/zh-Hans.json b/homeassistant/components/qnap_qsw/translations/zh-Hans.json new file mode 100644 index 00000000000..84c6b9f4026 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/zh-Hans.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u88ab\u914d\u7f6e", + "invalid_id": "\u8bbe\u5907\u8fd4\u56de\u4e86\u65e0\u6548\u7684\u552f\u4e00 ID" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u51ed\u8bc1\u65e0\u6548" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801", + "url": "\u4e3b\u673a\u5730\u5740", + "username": "\u7528\u6237\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/sv.json b/homeassistant/components/recorder/translations/sv.json new file mode 100644 index 00000000000..bf4d6ccf0a5 --- /dev/null +++ b/homeassistant/components/recorder/translations/sv.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "current_recorder_run": "Aktuell starttid", + "oldest_recorder_run": "\u00c4ldsta starttid" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/tr.json b/homeassistant/components/recorder/translations/tr.json new file mode 100644 index 00000000000..80fd4fd45be --- /dev/null +++ b/homeassistant/components/recorder/translations/tr.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "current_recorder_run": "Mevcut \u00c7al\u0131\u015ft\u0131rma Ba\u015flang\u0131\u00e7 Zaman\u0131", + "oldest_recorder_run": "En Eski \u00c7al\u0131\u015ft\u0131rma Ba\u015flang\u0131\u00e7 Zaman\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/sv.json b/homeassistant/components/sabnzbd/translations/sv.json new file mode 100644 index 00000000000..61847ff0eb4 --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta", + "invalid_api_key": "Ogiltig API-nyckel" + }, + "step": { + "user": { + "data": { + "api_key": "API Nyckel", + "name": "Namn", + "path": "S\u00f6kv\u00e4g" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/tr.json b/homeassistant/components/sabnzbd/translations/tr.json new file mode 100644 index 00000000000..f238072b6da --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "name": "Ad", + "path": "Yol", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/sv.json b/homeassistant/components/simplisafe/translations/sv.json index a4e8e052073..760d092e113 100644 --- a/homeassistant/components/simplisafe/translations/sv.json +++ b/homeassistant/components/simplisafe/translations/sv.json @@ -4,9 +4,19 @@ "already_configured": "Det h\u00e4r SimpliSafe-kontot har redan konfigurerats." }, "error": { + "2fa_timed_out": "Tidsgr\u00e4nsen tog slut i v\u00e4ntan p\u00e5 tv\u00e5faktorsautentisering", "identifier_exists": "Kontot \u00e4r redan registrerat" }, + "progress": { + "email_2fa": "Kontrollera din e-post f\u00f6r en verifieringsl\u00e4nk fr\u00e5n Simplisafe." + }, "step": { + "sms_2fa": { + "data": { + "code": "Kod" + }, + "description": "Ange tv\u00e5faktorsautentiseringskoden som skickats till dig via SMS." + }, "user": { "data": { "password": "L\u00f6senord", diff --git a/homeassistant/components/simplisafe/translations/tr.json b/homeassistant/components/simplisafe/translations/tr.json index 255207833c7..156b3575756 100644 --- a/homeassistant/components/simplisafe/translations/tr.json +++ b/homeassistant/components/simplisafe/translations/tr.json @@ -2,15 +2,20 @@ "config": { "abort": { "already_configured": "Bu SimpliSafe hesab\u0131 zaten kullan\u0131mda.", + "email_2fa_timed_out": "E-posta tabanl\u0131 iki fakt\u00f6rl\u00fc kimlik do\u011frulama i\u00e7in beklerken zaman a\u015f\u0131m\u0131na u\u011frad\u0131.", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", "wrong_account": "Sa\u011flanan kullan\u0131c\u0131 kimlik bilgileri bu SimpliSafe hesab\u0131yla e\u015fle\u015fmiyor." }, "error": { + "2fa_timed_out": "\u0130ki fakt\u00f6rl\u00fc kimlik do\u011frulama i\u00e7in beklerken zaman a\u015f\u0131m\u0131na u\u011frad\u0131", "identifier_exists": "Hesap zaten kay\u0131tl\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "still_awaiting_mfa": "Hala MFA e-posta t\u0131klamas\u0131 bekleniyor", "unknown": "Beklenmeyen hata" }, + "progress": { + "email_2fa": "Simplisafe'den bir do\u011frulama ba\u011flant\u0131s\u0131 i\u00e7in e-postan\u0131z\u0131 kontrol edin." + }, "step": { "mfa": { "title": "SimpliSafe \u00c7ok Fakt\u00f6rl\u00fc Kimlik Do\u011frulama" @@ -19,17 +24,23 @@ "data": { "password": "Parola" }, - "description": "Eri\u015fim kodunuzun s\u00fcresi doldu veya iptal edildi. Hesab\u0131n\u0131z\u0131 yeniden ba\u011flamak i\u00e7in parolan\u0131z\u0131 girin.", + "description": "L\u00fctfen {username} \u015fifresini tekrar girin.", "title": "Entegrasyonu Yeniden Do\u011frula" }, + "sms_2fa": { + "data": { + "code": "Kod" + }, + "description": "Size SMS ile g\u00f6nderilen iki fakt\u00f6rl\u00fc do\u011frulama kodunu girin." + }, "user": { "data": { "auth_code": "Yetkilendirme Kodu", "code": "Kod (Home Assistant kullan\u0131c\u0131 aray\u00fcz\u00fcnde kullan\u0131l\u0131r)", "password": "Parola", - "username": "E-posta" + "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "SimpliSafe, SimpliSafe web uygulamas\u0131 arac\u0131l\u0131\u011f\u0131yla Home Assistant ile kimlik do\u011frulamas\u0131 yapar. Teknik s\u0131n\u0131rlamalar nedeniyle bu i\u015flemin sonunda manuel bir ad\u0131m vard\u0131r; l\u00fctfen ba\u015flamadan \u00f6nce [belgeleri]( {docs_url} \n\n 1. SimpliSafe web uygulamas\u0131n\u0131 a\u00e7mak ve kimlik bilgilerinizi girmek i\u00e7in [buraya]( {url} \n\n 2. Oturum a\u00e7ma i\u015flemi tamamland\u0131\u011f\u0131nda buraya d\u00f6n\u00fcn ve yetkilendirme kodunu a\u015fa\u011f\u0131ya girin.", + "description": "Kullan\u0131c\u0131 ad\u0131n\u0131z\u0131 ve \u015fifrenizi girin.", "title": "Bilgilerinizi doldurun." } } diff --git a/homeassistant/components/slimproto/translations/sv.json b/homeassistant/components/slimproto/translations/sv.json new file mode 100644 index 00000000000..8ca6afbf92f --- /dev/null +++ b/homeassistant/components/slimproto/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "one": "Tom", + "other": "Tom" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/tr.json b/homeassistant/components/slimproto/translations/tr.json new file mode 100644 index 00000000000..a152eb19468 --- /dev/null +++ b/homeassistant/components/slimproto/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/sv.json b/homeassistant/components/sql/translations/sv.json new file mode 100644 index 00000000000..25bd828223f --- /dev/null +++ b/homeassistant/components/sql/translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Namn" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Namn" + }, + "data_description": { + "name": "Namn som kommer att anv\u00e4ndas f\u00f6r Config Entry och \u00e4ven sensorn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/tr.json b/homeassistant/components/sql/translations/tr.json index edbce58d65a..5757f50cdfb 100644 --- a/homeassistant/components/sql/translations/tr.json +++ b/homeassistant/components/sql/translations/tr.json @@ -13,6 +13,7 @@ "data": { "column": "S\u00fctun", "db_url": "Veritaban\u0131 URL'si", + "name": "Ad", "query": "Sorgu Se\u00e7", "unit_of_measurement": "\u00d6l\u00e7\u00fc Birimi", "value_template": "De\u011fer \u015eablonu" @@ -20,6 +21,7 @@ "data_description": { "column": "D\u00f6nd\u00fcr\u00fclen sorgunun durum olarak sunulmas\u0131 i\u00e7in s\u00fctun", "db_url": "Veritaban\u0131 URL'si, varsay\u0131lan HA veritaban\u0131n\u0131 kullanmak i\u00e7in bo\u015f b\u0131rak\u0131n", + "name": "Yap\u0131land\u0131rma Giri\u015fi ve ayr\u0131ca Sens\u00f6r i\u00e7in kullan\u0131lacak ad", "query": "\u00c7al\u0131\u015ft\u0131r\u0131lacak sorgu, 'SE\u00c7' ile ba\u015flamal\u0131d\u0131r", "unit_of_measurement": "\u00d6l\u00e7\u00fc Birimi (iste\u011fe ba\u011fl\u0131)", "value_template": "De\u011fer \u015eablonu (iste\u011fe ba\u011fl\u0131)" @@ -38,6 +40,7 @@ "data": { "column": "S\u00fctun", "db_url": "Veritaban\u0131 URL'si", + "name": "Ad", "query": "Sorgu Se\u00e7", "unit_of_measurement": "\u00d6l\u00e7\u00fc Birimi", "value_template": "De\u011fer \u015eablonu" @@ -45,6 +48,7 @@ "data_description": { "column": "D\u00f6nd\u00fcr\u00fclen sorgunun durum olarak sunulmas\u0131 i\u00e7in s\u00fctun", "db_url": "Veritaban\u0131 URL'si, varsay\u0131lan HA veritaban\u0131n\u0131 kullanmak i\u00e7in bo\u015f b\u0131rak\u0131n", + "name": "Yap\u0131land\u0131rma Giri\u015fi ve ayr\u0131ca Sens\u00f6r i\u00e7in kullan\u0131lacak ad", "query": "\u00c7al\u0131\u015ft\u0131r\u0131lacak sorgu, 'SE\u00c7' ile ba\u015flamal\u0131d\u0131r", "unit_of_measurement": "\u00d6l\u00e7\u00fc Birimi (iste\u011fe ba\u011fl\u0131)", "value_template": "De\u011fer \u015eablonu (iste\u011fe ba\u011fl\u0131)" diff --git a/homeassistant/components/steam_online/translations/sv.json b/homeassistant/components/steam_online/translations/sv.json new file mode 100644 index 00000000000..51c39c12b35 --- /dev/null +++ b/homeassistant/components/steam_online/translations/sv.json @@ -0,0 +1,24 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "Steam-integrationen m\u00e5ste autentiseras p\u00e5 nytt manuellt\n\nDu hittar din nyckel h\u00e4r: {api_key_url}" + }, + "user": { + "data": { + "api_key": "API Nyckel" + }, + "description": "Anv\u00e4nd {account_id_url} f\u00f6r att hitta ditt Steam-konto-ID" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "accounts": "Namn p\u00e5 konton som ska \u00f6vervakas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/tr.json b/homeassistant/components/steam_online/translations/tr.json new file mode 100644 index 00000000000..c378c90855e --- /dev/null +++ b/homeassistant/components/steam_online/translations/tr.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_account": "Ge\u00e7ersiz hesap kimli\u011fi", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "reauth_confirm": { + "description": "Steam entegrasyonunun manuel olarak yeniden do\u011frulanmas\u0131 gerekiyor \n\n Anahtar\u0131n\u0131z\u0131 burada bulabilirsiniz: {api_key_url}", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, + "user": { + "data": { + "account": "Steam hesap kimli\u011fi", + "api_key": "API Anahtar\u0131" + }, + "description": "Steam hesap kimli\u011finizi bulmak i\u00e7in {account_id_url} kullan\u0131n" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "accounts": "\u0130zlenecek hesaplar\u0131n adlar\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/zh-Hans.json b/homeassistant/components/steam_online/translations/zh-Hans.json new file mode 100644 index 00000000000..bf7b93740a0 --- /dev/null +++ b/homeassistant/components/steam_online/translations/zh-Hans.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52a1\u5df2\u88ab\u914d\u7f6e", + "reauth_successful": "\u91cd\u9a8c\u8bc1\u6210\u529f" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_account": "\u5e10\u6237 ID \u65e0\u6548", + "invalid_auth": "\u51ed\u8bc1\u65e0\u6548", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "step": { + "reauth_confirm": { + "description": "Steam \u96c6\u6210\u9700\u8981\u624b\u52a8\u91cd\u65b0\u8ba4\u8bc1\uff1a\n\n\u60a8\u53ef\u4ee5\u5728\u6b64\u5904\u627e\u5230\u60a8\u7684\u5bc6\u94a5\uff1a{api_key_url}", + "title": "\u91cd\u9a8c\u8bc1\u96c6\u6210" + }, + "user": { + "data": { + "account": "Steam \u5e10\u6237 ID", + "api_key": "API \u5bc6\u94a5" + }, + "description": "\u4f7f\u7528 {account_id_url} \u4ee5\u67e5\u627e\u60a8\u7684 Steam \u5e10\u6237 ID" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/sv.json b/homeassistant/components/tautulli/translations/sv.json new file mode 100644 index 00000000000..3354c6053dc --- /dev/null +++ b/homeassistant/components/tautulli/translations/sv.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "F\u00f6r att hitta din API-nyckel, \u00f6ppna Tautullis webbsida och navigera till Inst\u00e4llningar och sedan till webbgr\u00e4nssnitt. API-nyckeln finns l\u00e4ngst ner p\u00e5 sidan." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/tr.json b/homeassistant/components/tautulli/translations/tr.json new file mode 100644 index 00000000000..b52d2a7abad --- /dev/null +++ b/homeassistant/components/tautulli/translations/tr.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API Anahtar\u0131" + }, + "description": "API anahtar\u0131n\u0131z\u0131 bulmak i\u00e7in Tautulli web sayfas\u0131n\u0131 a\u00e7\u0131n ve Ayarlar'a ve ard\u0131ndan Web aray\u00fcz\u00fcne gidin. API anahtar\u0131 o sayfan\u0131n alt\u0131nda olacakt\u0131r.", + "title": "Tautulli'yi yeniden do\u011frulay\u0131n" + }, + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "url": "URL", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" + }, + "description": "API anahtar\u0131n\u0131z\u0131 bulmak i\u00e7in Tautulli web sayfas\u0131n\u0131 a\u00e7\u0131n ve Ayarlar'a ve ard\u0131ndan Web aray\u00fcz\u00fcne gidin. API anahtar\u0131 o sayfan\u0131n alt\u0131nda olacakt\u0131r. \n\n URL \u00f6rne\u011fi: ```http://192.168.0.10:8181``` 8181 varsay\u0131lan ba\u011flant\u0131 noktas\u0131d\u0131r." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/tr.json b/homeassistant/components/trafikverket_ferry/translations/tr.json new file mode 100644 index 00000000000..341de06c8a5 --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/tr.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "incorrect_api_key": "Se\u00e7ilen hesap i\u00e7in ge\u00e7ersiz API anahtar\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_route": "Sa\u011flanan bilgilerle rota bulunamad\u0131" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API Anahtar\u0131" + } + }, + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "from": "\u0130stasyondan", + "time": "Zaman", + "to": "\u0130stasyona", + "weekday": "Hafta i\u00e7i" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/sv.json b/homeassistant/components/unifi/translations/sv.json index 2e4851e70ed..a84558a6b58 100644 --- a/homeassistant/components/unifi/translations/sv.json +++ b/homeassistant/components/unifi/translations/sv.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "UniFi-integration \u00e4r inte konfigurerad" + }, "step": { "client_control": { "title": "UniFi-inst\u00e4llningar 2/3" diff --git a/homeassistant/components/unifi/translations/tr.json b/homeassistant/components/unifi/translations/tr.json index 9b53e712fa5..9e9ff5034d8 100644 --- a/homeassistant/components/unifi/translations/tr.json +++ b/homeassistant/components/unifi/translations/tr.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "UniFi Network sitesi zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "configuration_updated": "Yap\u0131land\u0131rma g\u00fcncellendi.", + "configuration_updated": "Yap\u0131land\u0131rma g\u00fcncellendi", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { @@ -26,6 +26,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "UniFi entegrasyonu kurulmad\u0131" + }, "step": { "client_control": { "data": { diff --git a/homeassistant/components/utility_meter/translations/sv.json b/homeassistant/components/utility_meter/translations/sv.json new file mode 100644 index 00000000000..38d433e7b42 --- /dev/null +++ b/homeassistant/components/utility_meter/translations/sv.json @@ -0,0 +1,3 @@ +{ + "title": "Tj\u00e4nsten \u00e4r redan konfigurerad" +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/ca.json b/homeassistant/components/vulcan/translations/ca.json index 505bcb1d326..a9bfe95310b 100644 --- a/homeassistant/components/vulcan/translations/ca.json +++ b/homeassistant/components/vulcan/translations/ca.json @@ -3,6 +3,7 @@ "abort": { "all_student_already_configured": "Ja s'han afegit tots els alumnes.", "already_configured": "Ja s'ha afegit aquest alumne.", + "no_matching_entries": "No s'han trobat entrades coincidents, utilitza un compte diferent o elimina la integraci\u00f3 amb l'estudiant obsolet.", "reauth_successful": "Re-autenticaci\u00f3 exitosa" }, "error": { @@ -37,6 +38,14 @@ }, "description": "Inicia sessi\u00f3 al teu compte de Vulcan mitjan\u00e7ant la p\u00e0gina de registre de l'aplicaci\u00f3 m\u00f2bil." }, + "reauth_confirm": { + "data": { + "pin": "PIN", + "region": "S\u00edmbol", + "token": "Token" + }, + "description": "Inicia sessi\u00f3 al teu compte de Vulcan mitjan\u00e7ant la p\u00e0gina de registre de l'aplicaci\u00f3 m\u00f2bil." + }, "select_saved_credentials": { "data": { "credentials": "Inici de sessi\u00f3" diff --git a/homeassistant/components/vulcan/translations/el.json b/homeassistant/components/vulcan/translations/el.json index bb77c88f8e5..abd3a264463 100644 --- a/homeassistant/components/vulcan/translations/el.json +++ b/homeassistant/components/vulcan/translations/el.json @@ -3,6 +3,7 @@ "abort": { "all_student_already_configured": "\u038c\u03bb\u03bf\u03b9 \u03bf\u03b9 \u03bc\u03b1\u03b8\u03b7\u03c4\u03ad\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03ae\u03b4\u03b7 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b5\u03b8\u03b5\u03af.", "already_configured": "\u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03bc\u03b1\u03b8\u03b7\u03c4\u03ae\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c0\u03c1\u03bf\u03c3\u03c4\u03b5\u03b8\u03b5\u03af.", + "no_matching_entries": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03ae\u03c3\u03b5\u03b9\u03c2 \u03c0\u03bf\u03c5 \u03bd\u03b1 \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03bf\u03c5\u03bd, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03ae \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03bc\u03b5 \u03c0\u03b1\u03bb\u03b9\u03cc \u03bc\u03b1\u03b8\u03b7\u03c4\u03ae..", "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { @@ -37,6 +38,14 @@ }, "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 Vulcan \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b7 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03b9\u03bd\u03b7\u03c4\u03ac." }, + "reauth_confirm": { + "data": { + "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", + "region": "\u03a3\u03cd\u03bc\u03b2\u03bf\u03bb\u03bf", + "token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc" + }, + "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 Vulcan \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b7 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03b9\u03bd\u03b7\u03c4\u03ac." + }, "select_saved_credentials": { "data": { "credentials": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7" diff --git a/homeassistant/components/vulcan/translations/en.json b/homeassistant/components/vulcan/translations/en.json index 48c054d2c15..8ea9fc59964 100644 --- a/homeassistant/components/vulcan/translations/en.json +++ b/homeassistant/components/vulcan/translations/en.json @@ -3,6 +3,7 @@ "abort": { "all_student_already_configured": "All students have already been added.", "already_configured": "That student has already been added.", + "no_matching_entries": "No matching entries found, please use different account or remove integration with outdated student..", "reauth_successful": "Reauth successful" }, "error": { @@ -37,6 +38,14 @@ }, "description": "Login to your Vulcan Account using mobile app registration page." }, + "reauth_confirm": { + "data": { + "pin": "Pin", + "region": "Symbol", + "token": "Token" + }, + "description": "Login to your Vulcan Account using mobile app registration page." + }, "select_saved_credentials": { "data": { "credentials": "Login" diff --git a/homeassistant/components/vulcan/translations/fr.json b/homeassistant/components/vulcan/translations/fr.json index 812c069223e..9a850d85796 100644 --- a/homeassistant/components/vulcan/translations/fr.json +++ b/homeassistant/components/vulcan/translations/fr.json @@ -3,6 +3,7 @@ "abort": { "all_student_already_configured": "Tous les \u00e9tudiants ont d\u00e9j\u00e0 \u00e9t\u00e9 ajout\u00e9s.", "already_configured": "Cet \u00e9tudiant a d\u00e9j\u00e0 \u00e9t\u00e9 ajout\u00e9.", + "no_matching_entries": "Aucune entr\u00e9e correspondante n'a \u00e9t\u00e9 trouv\u00e9e, veuillez utiliser un autre compte ou retirer l'int\u00e9gration incluant un \u00e9tudiant obsol\u00e8te.", "reauth_successful": "R\u00e9-authentification r\u00e9ussie" }, "error": { @@ -37,6 +38,14 @@ }, "description": "Connectez-vous \u00e0 votre compte Vulcan en utilisant la page d'inscription de l'application mobile." }, + "reauth_confirm": { + "data": { + "pin": "PIN", + "region": "Symbole", + "token": "Jeton" + }, + "description": "Connectez-vous \u00e0 votre compte Vulcan en utilisant la page d'inscription de l'application mobile." + }, "select_saved_credentials": { "data": { "credentials": "Connexion" diff --git a/homeassistant/components/vulcan/translations/hu.json b/homeassistant/components/vulcan/translations/hu.json index 9e138254ebe..90566b69fe8 100644 --- a/homeassistant/components/vulcan/translations/hu.json +++ b/homeassistant/components/vulcan/translations/hu.json @@ -3,6 +3,7 @@ "abort": { "all_student_already_configured": "M\u00e1r minden tanul\u00f3 hozz\u00e1adva.", "already_configured": "Ezt a tanul\u00f3t m\u00e1r hozz\u00e1adt\u00e1k.", + "no_matching_entries": "Nem tal\u00e1ltunk megfelel\u0151 bejegyz\u00e9st, k\u00e9rj\u00fck, haszn\u00e1ljon m\u00e1sik fi\u00f3kot, vagy t\u00e1vol\u00edtsa el az elavult di\u00e1kkal val\u00f3 integr\u00e1ci\u00f3t...", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres" }, "error": { @@ -37,6 +38,14 @@ }, "description": "Jelentkezzen be Vulcan fi\u00f3kj\u00e1ba a mobilalkalmaz\u00e1s regisztr\u00e1ci\u00f3s oldal\u00e1n." }, + "reauth_confirm": { + "data": { + "pin": "PIN", + "region": "Szimb\u00f3lum", + "token": "Token" + }, + "description": "Jelentkezzen be Vulcan fi\u00f3kj\u00e1ba a mobilalkalmaz\u00e1s regisztr\u00e1ci\u00f3s oldal\u00e1n." + }, "select_saved_credentials": { "data": { "credentials": "Bejelentkez\u00e9s" diff --git a/homeassistant/components/vulcan/translations/pt-BR.json b/homeassistant/components/vulcan/translations/pt-BR.json index ecef34ff96b..98aaec3c33f 100644 --- a/homeassistant/components/vulcan/translations/pt-BR.json +++ b/homeassistant/components/vulcan/translations/pt-BR.json @@ -3,6 +3,7 @@ "abort": { "all_student_already_configured": "Todos os alunos j\u00e1 foram adicionados.", "already_configured": "Esse aluno j\u00e1 foi adicionado.", + "no_matching_entries": "Nenhuma entrada correspondente encontrada, use uma conta diferente ou remova a integra\u00e7\u00e3o com o aluno desatualizado.", "reauth_successful": "Autentica\u00e7\u00e3o bem-sucedida" }, "error": { @@ -37,6 +38,14 @@ }, "description": "Fa\u00e7a login na sua conta Vulcan usando a p\u00e1gina de registro do aplicativo m\u00f3vel." }, + "reauth_confirm": { + "data": { + "pin": "C\u00f3digo PIN", + "region": "S\u00edmbolo", + "token": "Token" + }, + "description": "Fa\u00e7a login na sua conta Vulcan usando a p\u00e1gina de registro do aplicativo." + }, "select_saved_credentials": { "data": { "credentials": "Login" diff --git a/homeassistant/components/vulcan/translations/tr.json b/homeassistant/components/vulcan/translations/tr.json index 9f4324dfbcc..e103d8626ab 100644 --- a/homeassistant/components/vulcan/translations/tr.json +++ b/homeassistant/components/vulcan/translations/tr.json @@ -3,6 +3,7 @@ "abort": { "all_student_already_configured": "T\u00fcm \u00f6\u011frenciler zaten eklendi.", "already_configured": "Bu \u00f6\u011frenci zaten eklendi.", + "no_matching_entries": "E\u015fle\u015fen giri\u015f bulunamad\u0131, l\u00fctfen farkl\u0131 bir hesap kullan\u0131n veya eski \u00f6\u011frenci ile entegrasyonu kald\u0131r\u0131n..", "reauth_successful": "Yeniden yetkilendirme ba\u015far\u0131l\u0131" }, "error": { @@ -37,6 +38,14 @@ }, "description": "Mobil uygulama kay\u0131t sayfas\u0131n\u0131 kullanarak Vulcan Hesab\u0131n\u0131za giri\u015f yap\u0131n." }, + "reauth_confirm": { + "data": { + "pin": "Pin", + "region": "Sembol", + "token": "Anahtar" + }, + "description": "Mobil uygulama kay\u0131t sayfas\u0131n\u0131 kullanarak Vulcan Hesab\u0131n\u0131za giri\u015f yap\u0131n." + }, "select_saved_credentials": { "data": { "credentials": "Oturum a\u00e7" From d612b9e0b4d1013e49d8489a3857e110f174cb14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 5 May 2022 23:09:10 -0400 Subject: [PATCH 0255/3516] Reduce event loop overhead for listeners that already queue (#71364) Co-authored-by: Paulus Schoutsen --- homeassistant/components/recorder/core.py | 7 ++++-- .../components/websocket_api/auth.py | 2 +- .../components/websocket_api/commands.py | 16 +++++++++----- .../components/websocket_api/connection.py | 2 +- .../components/websocket_api/http.py | 10 ++++++--- homeassistant/core.py | 22 +++++++++++++++---- tests/test_core.py | 18 +++++++++++++++ 7 files changed, 61 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index af368b909b7..49be0aac705 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -237,7 +237,9 @@ class Recorder(threading.Thread): def async_initialize(self) -> None: """Initialize the recorder.""" self._event_listener = self.hass.bus.async_listen( - MATCH_ALL, self.event_listener, event_filter=self._async_event_filter + MATCH_ALL, + self.event_listener, + run_immediately=True, ) self._queue_watcher = async_track_time_interval( self.hass, self._async_check_queue, timedelta(minutes=10) @@ -916,7 +918,8 @@ class Recorder(threading.Thread): @callback def event_listener(self, event: Event) -> None: """Listen for new events and put them in the process queue.""" - self.queue_task(EventTask(event)) + if self._async_event_filter(event): + self.queue_task(EventTask(event)) def block_till_done(self) -> None: """Block till all events processed. diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py index 794dae77153..9c074588a17 100644 --- a/homeassistant/components/websocket_api/auth.py +++ b/homeassistant/components/websocket_api/auth.py @@ -56,7 +56,7 @@ class AuthPhase: self, logger: WebSocketAdapter, hass: HomeAssistant, - send_message: Callable[[str | dict[str, Any]], None], + send_message: Callable[[str | dict[str, Any] | Callable[[], str]], None], cancel_ws: CALLBACK_TYPE, request: Request, ) -> None: diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 414913be002..edec628fd2c 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -105,17 +105,21 @@ def handle_subscribe_events( ): return - connection.send_message(messages.cached_event_message(msg["id"], event)) + connection.send_message( + lambda: messages.cached_event_message(msg["id"], event) + ) else: @callback def forward_events(event: Event) -> None: """Forward events to websocket.""" - connection.send_message(messages.cached_event_message(msg["id"], event)) + connection.send_message( + lambda: messages.cached_event_message(msg["id"], event) + ) connection.subscriptions[msg["id"]] = hass.bus.async_listen( - event_type, forward_events + event_type, forward_events, run_immediately=True ) connection.send_result(msg["id"]) @@ -286,14 +290,16 @@ def handle_subscribe_entities( if entity_ids and event.data["entity_id"] not in entity_ids: return - connection.send_message(messages.cached_state_diff_message(msg["id"], event)) + connection.send_message( + lambda: messages.cached_state_diff_message(msg["id"], event) + ) # We must never await between sending the states and listening for # state changed events or we will introduce a race condition # where some states are missed states = _async_get_allowed_states(hass, connection) connection.subscriptions[msg["id"]] = hass.bus.async_listen( - "state_changed", forward_entity_changes + EVENT_STATE_CHANGED, forward_entity_changes, run_immediately=True ) connection.send_result(msg["id"]) data: dict[str, dict[str, dict]] = { diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 9b53f358b85..cdcee408070 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -30,7 +30,7 @@ class ActiveConnection: self, logger: WebSocketAdapter, hass: HomeAssistant, - send_message: Callable[[str | dict[str, Any]], None], + send_message: Callable[[str | dict[str, Any] | Callable[[], str]], None], user: User, refresh_token: RefreshToken, ) -> None: diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 44b2aa8579c..a913b81c384 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -72,9 +72,13 @@ class WebSocketHandler: # Exceptions if Socket disconnected or cancelled by connection handler with suppress(RuntimeError, ConnectionResetError, *CANCELLATION_ERRORS): while not self.wsock.closed: - if (message := await self._to_write.get()) is None: + if (process := await self._to_write.get()) is None: break + if not isinstance(process, str): + message: str = process() + else: + message = process self._logger.debug("Sending %s", message) await self.wsock.send_str(message) @@ -84,14 +88,14 @@ class WebSocketHandler: self._peak_checker_unsub = None @callback - def _send_message(self, message: str | dict[str, Any]) -> None: + def _send_message(self, message: str | dict[str, Any] | Callable[[], str]) -> None: """Send a message to the client. Closes connection if the client is not reading the messages. Async friendly. """ - if not isinstance(message, str): + if isinstance(message, dict): message = message_to_json(message) try: diff --git a/homeassistant/core.py b/homeassistant/core.py index b181e4c4106..916dd0c6f72 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -778,6 +778,7 @@ class _FilterableJob(NamedTuple): job: HassJob[None | Awaitable[None]] event_filter: Callable[[Event], bool] | None + run_immediately: bool class EventBus: @@ -845,7 +846,7 @@ class EventBus: if not listeners: return - for job, event_filter in listeners: + for job, event_filter, run_immediately in listeners: if event_filter is not None: try: if not event_filter(event): @@ -853,7 +854,13 @@ class EventBus: except Exception: # pylint: disable=broad-except _LOGGER.exception("Error in event filter") continue - self._hass.async_add_hass_job(job, event) + if run_immediately: + try: + job.target(event) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error running job: %s", job) + else: + self._hass.async_add_hass_job(job, event) def listen( self, @@ -881,6 +888,7 @@ class EventBus: event_type: str, listener: Callable[[Event], None | Awaitable[None]], event_filter: Callable[[Event], bool] | None = None, + run_immediately: bool = False, ) -> CALLBACK_TYPE: """Listen for all events or events of a specific type. @@ -891,12 +899,18 @@ class EventBus: @callback that returns a boolean value, determines if the listener callable should run. + If run_immediately is passed, the callback will be run + right away instead of using call_soon. Only use this if + the callback results in scheduling another task. + This method must be run in the event loop. """ if event_filter is not None and not is_callback(event_filter): raise HomeAssistantError(f"Event filter {event_filter} is not a callback") + if run_immediately and not is_callback(listener): + raise HomeAssistantError(f"Event listener {listener} is not a callback") return self._async_listen_filterable_job( - event_type, _FilterableJob(HassJob(listener), event_filter) + event_type, _FilterableJob(HassJob(listener), event_filter, run_immediately) ) @callback @@ -966,7 +980,7 @@ class EventBus: _onetime_listener, listener, ("__name__", "__qualname__", "__module__"), [] ) - filterable_job = _FilterableJob(HassJob(_onetime_listener), None) + filterable_job = _FilterableJob(HassJob(_onetime_listener), None, False) return self._async_listen_filterable_job(event_type, filterable_job) diff --git a/tests/test_core.py b/tests/test_core.py index 6885063a79a..c870605fc01 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -442,6 +442,24 @@ async def test_eventbus_filtered_listener(hass): unsub() +async def test_eventbus_run_immediately(hass): + """Test we can call events immediately.""" + calls = [] + + @ha.callback + def listener(event): + """Mock listener.""" + calls.append(event) + + unsub = hass.bus.async_listen("test", listener, run_immediately=True) + + hass.bus.async_fire("test", {"event": True}) + # No async_block_till_done here + assert len(calls) == 1 + + unsub() + + async def test_eventbus_unsubscribe_listener(hass): """Test unsubscribe listener from returned function.""" calls = [] From 5931f6598a98af915c7c4435709a76de4b7093f5 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 6 May 2022 09:05:15 +0200 Subject: [PATCH 0256/3516] Add tests for Sensibo (#71148) * Initial commit * Check temperature missing * fix temp is none * Fix parallell * Commit to save * Fix tests * Fix test_init * assert 25 * Adjustments tests * Small removal * Cleanup * no hass.data * Adjustment test_coordinator * Minor change test_coordinator --- .coveragerc | 6 - tests/components/sensibo/__init__.py | 5 + tests/components/sensibo/conftest.py | 45 ++ tests/components/sensibo/response.py | 400 ++++++++++++ .../components/sensibo/test_binary_sensor.py | 52 ++ tests/components/sensibo/test_climate.py | 601 ++++++++++++++++++ tests/components/sensibo/test_coordinator.py | 91 +++ tests/components/sensibo/test_diagnostics.py | 48 ++ tests/components/sensibo/test_entity.py | 132 ++++ tests/components/sensibo/test_init.py | 132 ++++ 10 files changed, 1506 insertions(+), 6 deletions(-) create mode 100644 tests/components/sensibo/conftest.py create mode 100644 tests/components/sensibo/response.py create mode 100644 tests/components/sensibo/test_binary_sensor.py create mode 100644 tests/components/sensibo/test_climate.py create mode 100644 tests/components/sensibo/test_coordinator.py create mode 100644 tests/components/sensibo/test_diagnostics.py create mode 100644 tests/components/sensibo/test_entity.py create mode 100644 tests/components/sensibo/test_init.py diff --git a/.coveragerc b/.coveragerc index 3464b0df1be..353d2f07d06 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1017,12 +1017,6 @@ omit = homeassistant/components/senseme/fan.py homeassistant/components/senseme/light.py homeassistant/components/senseme/switch.py - homeassistant/components/sensibo/__init__.py - homeassistant/components/sensibo/binary_sensor.py - homeassistant/components/sensibo/climate.py - homeassistant/components/sensibo/coordinator.py - homeassistant/components/sensibo/diagnostics.py - homeassistant/components/sensibo/entity.py homeassistant/components/sensibo/number.py homeassistant/components/sensibo/select.py homeassistant/components/sensibo/sensor.py diff --git a/tests/components/sensibo/__init__.py b/tests/components/sensibo/__init__.py index 8dd2ed661bc..da585f8d1e8 100644 --- a/tests/components/sensibo/__init__.py +++ b/tests/components/sensibo/__init__.py @@ -1 +1,6 @@ """Tests for the Sensibo integration.""" +from __future__ import annotations + +from homeassistant.const import CONF_API_KEY + +ENTRY_CONFIG = {CONF_API_KEY: "1234567890"} diff --git a/tests/components/sensibo/conftest.py b/tests/components/sensibo/conftest.py new file mode 100644 index 00000000000..9380ee6ab6b --- /dev/null +++ b/tests/components/sensibo/conftest.py @@ -0,0 +1,45 @@ +"""Fixtures for the Sensibo integration.""" +from __future__ import annotations + +from unittest.mock import patch + +import pytest + +from homeassistant.components.sensibo.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant + +from . import ENTRY_CONFIG +from .response import DATA_FROM_API + +from tests.common import MockConfigEntry + + +@pytest.fixture +async def load_int(hass: HomeAssistant) -> MockConfigEntry: + """Set up the Sensibo integration in Home Assistant.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="username", + version=2, + ) + + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/sensibo/response.py b/tests/components/sensibo/response.py new file mode 100644 index 00000000000..e6b39b81881 --- /dev/null +++ b/tests/components/sensibo/response.py @@ -0,0 +1,400 @@ +"""Test api response for the Sensibo integration.""" +from __future__ import annotations + +from pysensibo.model import MotionSensor, SensiboData, SensiboDevice + +DATA_FROM_API = SensiboData( + raw={ + "status": "success", + "result": [ + { + "id": "ABC999111", + "qrId": "AAAAAAAAAA", + "room": {"uid": "99TT99TT", "name": "Hallway", "icon": "Lounge"}, + "acState": { + "timestamp": { + "time": "2022-04-30T19:58:15.544787Z", + "secondsAgo": 0, + }, + "on": False, + "mode": "fan", + "fanLevel": "high", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + "location": { + "id": "ZZZZZZZZZZZZ", + "name": "Home", + "latLon": [58.9806976, 20.5864297], + "address": ["Sealand 99", "Some county"], + "country": "United Country", + "createTime": { + "time": "2020-03-21T15:44:15Z", + "secondsAgo": 66543240, + }, + "updateTime": None, + "features": [], + "geofenceTriggerRadius": 200, + "subscription": None, + "technician": None, + "shareAnalytics": False, + "occupancy": "n/a", + }, + "accessPoint": {"ssid": "SENSIBO-I-99999", "password": None}, + "macAddress": "00:02:00:B6:00:00", + "autoOffMinutes": None, + "autoOffEnabled": False, + "antiMoldTimer": None, + "antiMoldConfig": None, + } + ], + }, + parsed={ + "ABC999111": SensiboDevice( + id="ABC999111", + mac="00:02:00:B6:00:00", + name="Hallway", + ac_states={ + "timestamp": {"time": "2022-04-30T19:58:15.544787Z", "secondsAgo": 0}, + "on": False, + "mode": "heat", + "fanLevel": "high", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + temp=22.4, + humidity=38, + target_temp=25, + hvac_mode="heat", + device_on=True, + fan_mode="high", + swing_mode="stopped", + horizontal_swing_mode="stopped", + light_mode="on", + available=True, + hvac_modes=["cool", "heat", "dry", "auto", "fan", "off"], + fan_modes=["quiet", "low", "medium"], + swing_modes=[ + "stopped", + "fixedTop", + "fixedMiddleTop", + ], + horizontal_swing_modes=[ + "stopped", + "fixedLeft", + "fixedCenterLeft", + ], + light_modes=["on", "off"], + temp_unit="C", + temp_list=[18, 19, 20], + temp_step=1, + active_features=[ + "timestamp", + "on", + "mode", + "fanLevel", + "swing", + "targetTemperature", + "horizontalSwing", + "light", + ], + full_features={ + "targetTemperature", + "fanLevel", + "swing", + "horizontalSwing", + "light", + }, + state="heat", + fw_ver="SKY30046", + fw_ver_available="SKY30046", + fw_type="esp8266ex", + model="skyv2", + calibration_temp=0.1, + calibration_hum=0.1, + full_capabilities={ + "modes": { + "cool": { + "temperatures": { + "F": { + "isNative": False, + "values": [ + 64, + 66, + 68, + ], + }, + "C": { + "isNative": True, + "values": [ + 18, + 19, + 20, + ], + }, + }, + "fanLevels": [ + "quiet", + "low", + "medium", + ], + "swing": [ + "stopped", + "fixedTop", + "fixedMiddleTop", + ], + "horizontalSwing": [ + "stopped", + "fixedLeft", + "fixedCenterLeft", + ], + "light": ["on", "off"], + }, + "heat": { + "temperatures": { + "F": { + "isNative": False, + "values": [ + 63, + 64, + 66, + ], + }, + "C": { + "isNative": True, + "values": [ + 17, + 18, + 19, + ], + }, + }, + "fanLevels": ["quiet", "low", "medium"], + "swing": [ + "stopped", + "fixedTop", + "fixedMiddleTop", + ], + "horizontalSwing": [ + "stopped", + "fixedLeft", + "fixedCenterLeft", + ], + "light": ["on", "off"], + }, + "dry": { + "temperatures": { + "F": { + "isNative": False, + "values": [ + 64, + 66, + 68, + ], + }, + "C": { + "isNative": True, + "values": [ + 18, + 19, + 20, + ], + }, + }, + "swing": [ + "stopped", + "fixedTop", + "fixedMiddleTop", + ], + "horizontalSwing": [ + "stopped", + "fixedLeft", + "fixedCenterLeft", + ], + "light": ["on", "off"], + }, + "auto": { + "temperatures": { + "F": { + "isNative": False, + "values": [ + 64, + 66, + 68, + ], + }, + "C": { + "isNative": True, + "values": [ + 18, + 19, + 20, + ], + }, + }, + "fanLevels": [ + "quiet", + "low", + "medium", + ], + "swing": [ + "stopped", + "fixedTop", + "fixedMiddleTop", + ], + "horizontalSwing": [ + "stopped", + "fixedLeft", + "fixedCenterLeft", + ], + "light": ["on", "off"], + }, + "fan": { + "temperatures": {}, + "fanLevels": [ + "quiet", + "low", + ], + "swing": [ + "stopped", + "fixedTop", + "fixedMiddleTop", + ], + "horizontalSwing": [ + "stopped", + "fixedLeft", + "fixedCenterLeft", + ], + "light": ["on", "off"], + }, + } + }, + motion_sensors={ + "AABBCC": MotionSensor( + id="AABBCC", + alive=True, + motion=True, + fw_ver="V17", + fw_type="nrf52", + is_main_sensor=True, + battery_voltage=3000, + humidity=57, + temperature=23.9, + model="motion_sensor", + rssi=-72, + ) + }, + pm25=None, + room_occupied=True, + update_available=False, + schedules={}, + pure_boost_enabled=None, + pure_sensitivity=None, + pure_ac_integration=None, + pure_geo_integration=None, + pure_measure_integration=None, + timer_on=False, + timer_id=None, + timer_state_on=None, + timer_time=None, + smart_on=False, + smart_type="temperature", + smart_low_temp_threshold=0.0, + smart_high_temp_threshold=27.5, + smart_low_state={ + "on": True, + "targetTemperature": 21, + "temperatureUnit": "C", + "mode": "heat", + "fanLevel": "low", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + smart_high_state={ + "on": True, + "targetTemperature": 21, + "temperatureUnit": "C", + "mode": "cool", + "fanLevel": "high", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + filter_clean=False, + filter_last_reset="2022-03-12T15:24:26Z", + ), + "AAZZAAZZ": SensiboDevice( + id="AAZZAAZZ", + mac="00:01:00:01:00:01", + name="Kitchen", + ac_states={ + "timestamp": {"time": "2022-04-30T19:58:15.568753Z", "secondsAgo": 0}, + "on": False, + "mode": "fan", + "fanLevel": "low", + "light": "on", + }, + temp=None, + humidity=None, + target_temp=None, + hvac_mode="off", + device_on=False, + fan_mode="low", + swing_mode=None, + horizontal_swing_mode=None, + light_mode="on", + available=True, + hvac_modes=["fan", "off"], + fan_modes=["low", "high"], + swing_modes=None, + horizontal_swing_modes=None, + light_modes=["on", "dim", "off"], + temp_unit="C", + temp_list=[0, 1], + temp_step=1, + active_features=["timestamp", "on", "mode", "fanLevel", "light"], + full_features={"light", "targetTemperature", "fanLevel"}, + state="off", + fw_ver="PUR00111", + fw_ver_available="PUR00111", + fw_type="pure-esp32", + model="pure", + calibration_temp=0.0, + calibration_hum=0.0, + full_capabilities={ + "modes": { + "fan": { + "temperatures": {}, + "fanLevels": ["low", "high"], + "light": ["on", "dim", "off"], + } + } + }, + motion_sensors={}, + pm25=1, + room_occupied=None, + update_available=False, + schedules={}, + pure_boost_enabled=False, + pure_sensitivity="N", + pure_ac_integration=False, + pure_geo_integration=False, + pure_measure_integration=True, + timer_on=None, + timer_id=None, + timer_state_on=None, + timer_time=None, + smart_on=None, + smart_type=None, + smart_low_temp_threshold=None, + smart_high_temp_threshold=None, + smart_low_state=None, + smart_high_state=None, + filter_clean=False, + filter_last_reset="2022-04-23T15:58:45Z", + ), + }, +) diff --git a/tests/components/sensibo/test_binary_sensor.py b/tests/components/sensibo/test_binary_sensor.py new file mode 100644 index 00000000000..cbf38ff27b0 --- /dev/null +++ b/tests/components/sensibo/test_binary_sensor.py @@ -0,0 +1,52 @@ +"""The test for the sensibo binary sensor platform.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +from pytest import MonkeyPatch + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.util import dt + +from .response import DATA_FROM_API + +from tests.common import async_fire_time_changed + + +async def test_binary_sensor( + hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: MonkeyPatch +) -> None: + """Test the Sensibo binary sensor.""" + + state1 = hass.states.get("binary_sensor.hallway_motion_sensor_alive") + state2 = hass.states.get("binary_sensor.hallway_motion_sensor_main_sensor") + state3 = hass.states.get("binary_sensor.hallway_motion_sensor_motion") + state4 = hass.states.get("binary_sensor.hallway_room_occupied") + assert state1.state == "on" + assert state2.state == "on" + assert state3.state == "on" + assert state4.state == "on" + + monkeypatch.setattr( + DATA_FROM_API.parsed["ABC999111"].motion_sensors["AABBCC"], "alive", False + ) + monkeypatch.setattr( + DATA_FROM_API.parsed["ABC999111"].motion_sensors["AABBCC"], "motion", False + ) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("binary_sensor.hallway_motion_sensor_alive") + state3 = hass.states.get("binary_sensor.hallway_motion_sensor_motion") + assert state1.state == "off" + assert state3.state == "off" diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py new file mode 100644 index 00000000000..8b087da5d95 --- /dev/null +++ b/tests/components/sensibo/test_climate.py @@ -0,0 +1,601 @@ +"""The test for the sensibo binary sensor platform.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +import pytest +from voluptuous import MultipleInvalid + +from homeassistant.components.climate.const import ( + ATTR_FAN_MODE, + ATTR_HVAC_MODE, + ATTR_SWING_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_SWING_MODE, + SERVICE_SET_TEMPERATURE, +) +from homeassistant.components.sensibo.climate import SERVICE_ASSUME_STATE +from homeassistant.components.sensibo.const import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_STATE, + ATTR_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import dt + +from .response import DATA_FROM_API + +from tests.common import async_fire_time_changed + + +async def test_climate(hass: HomeAssistant, load_int: ConfigEntry) -> None: + """Test the Sensibo climate.""" + + state1 = hass.states.get("climate.hallway") + state2 = hass.states.get("climate.kitchen") + + assert state1.state == "heat" + assert state1.attributes == { + "hvac_modes": [ + "cool", + "heat", + "dry", + "heat_cool", + "fan_only", + "off", + ], + "min_temp": 18, + "max_temp": 20, + "target_temp_step": 1, + "fan_modes": ["quiet", "low", "medium"], + "swing_modes": [ + "stopped", + "fixedTop", + "fixedMiddleTop", + ], + "current_temperature": 22.4, + "temperature": 25, + "current_humidity": 38, + "fan_mode": "high", + "swing_mode": "stopped", + "friendly_name": "Hallway", + "supported_features": 41, + } + + assert state2.state == "off" + + +async def test_climate_fan( + hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test the Sensibo climate fan service.""" + + state1 = hass.states.get("climate.hallway") + assert state1.attributes["fan_mode"] == "high" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_FAN_MODE: "low"}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["fan_mode"] == "low" + + monkeypatch.setattr( + DATA_FROM_API.parsed["ABC999111"], + "active_features", + [ + "timestamp", + "on", + "mode", + "swing", + "targetTemperature", + "horizontalSwing", + "light", + ], + ) + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + ): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_FAN_MODE: "low"}, + blocking=True, + ) + await hass.async_block_till_done() + + state3 = hass.states.get("climate.hallway") + assert state3.attributes["fan_mode"] == "low" + + +async def test_climate_swing( + hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test the Sensibo climate swing service.""" + + state1 = hass.states.get("climate.hallway") + assert state1.attributes["swing_mode"] == "stopped" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_MODE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_SWING_MODE: "fixedTop"}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["swing_mode"] == "fixedTop" + + monkeypatch.setattr( + DATA_FROM_API.parsed["ABC999111"], + "active_features", + [ + "timestamp", + "on", + "mode", + "targetTemperature", + "horizontalSwing", + "light", + ], + ) + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + ): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_MODE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_SWING_MODE: "fixedTop"}, + blocking=True, + ) + await hass.async_block_till_done() + + state3 = hass.states.get("climate.hallway") + assert state3.attributes["swing_mode"] == "fixedTop" + + +async def test_climate_temperatures( + hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test the Sensibo climate temperature service.""" + + state1 = hass.states.get("climate.hallway") + assert state1.attributes["temperature"] == 25 + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 20}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["temperature"] == 20 + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 15}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["temperature"] == 18 + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + with pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 18.5}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["temperature"] == 18 + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 24}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["temperature"] == 20 + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 20}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["temperature"] == 20 + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + with pytest.raises(MultipleInvalid): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state1.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["temperature"] == 20 + + monkeypatch.setattr( + DATA_FROM_API.parsed["ABC999111"], + "active_features", + [ + "timestamp", + "on", + "mode", + "swing", + "horizontalSwing", + "light", + ], + ) + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 20}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["temperature"] == 20 + + +async def test_climate_temperature_is_none( + hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test the Sensibo climate temperature service no temperature provided.""" + + monkeypatch.setattr( + DATA_FROM_API.parsed["ABC999111"], + "active_features", + [ + "timestamp", + "on", + "mode", + "fanLevel", + "targetTemperature", + "swing", + "horizontalSwing", + "light", + ], + ) + monkeypatch.setattr( + DATA_FROM_API.parsed["ABC999111"], + "target_temp", + 25, + ) + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("climate.hallway") + assert state1.attributes["temperature"] == 25 + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + ): + with pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: state1.entity_id, + ATTR_TARGET_TEMP_HIGH: 30, + ATTR_TARGET_TEMP_LOW: 20, + }, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.attributes["temperature"] == 25 + + +async def test_climate_hvac_mode( + hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test the Sensibo climate hvac mode service.""" + + monkeypatch.setattr( + DATA_FROM_API.parsed["ABC999111"], + "active_features", + [ + "timestamp", + "on", + "mode", + "fanLevel", + "targetTemperature", + "swing", + "horizontalSwing", + "light", + ], + ) + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("climate.hallway") + assert state1.state == "heat" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_HVAC_MODE: "off"}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.state == "off" + + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "device_on", False) + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_HVAC_MODE: "heat"}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.state == "heat" + + +async def test_climate_on_off( + hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test the Sensibo climate on/off service.""" + + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "hvac_mode", "heat") + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "device_on", True) + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("climate.hallway") + assert state1.state == "heat" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: state1.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.state == "off" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: state1.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.state == "heat" + + +async def test_climate_service_failed( + hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test the Sensibo climate service failed.""" + + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "hvac_mode", "heat") + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "device_on", True) + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("climate.hallway") + assert state1.state == "heat" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Error", "failureReason": "Did not work"}}, + ): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: state1.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.state == "heat" + + +async def test_climate_assumed_state( + hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test the Sensibo climate assumed state service.""" + + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "hvac_mode", "heat") + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "device_on", True) + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("climate.hallway") + assert state1.state == "heat" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + DOMAIN, + SERVICE_ASSUME_STATE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_STATE: "off"}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("climate.hallway") + assert state2.state == "off" diff --git a/tests/components/sensibo/test_coordinator.py b/tests/components/sensibo/test_coordinator.py new file mode 100644 index 00000000000..703eb8b184b --- /dev/null +++ b/tests/components/sensibo/test_coordinator.py @@ -0,0 +1,91 @@ +"""The test for the sensibo coordinator.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +from pysensibo.exceptions import AuthenticationError, SensiboError +from pysensibo.model import SensiboData +import pytest + +from homeassistant.components.sensibo.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.util import dt + +from . import ENTRY_CONFIG +from .response import DATA_FROM_API + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_coordinator( + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test the Sensibo coordinator with errors.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="username", + version=2, + ) + + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + ) as mock_data, patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ): + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "hvac_mode", "heat") + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "device_on", True) + mock_data.return_value = DATA_FROM_API + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + mock_data.assert_called_once() + state = hass.states.get("climate.hallway") + assert state.state == "heat" + mock_data.reset_mock() + + mock_data.side_effect = SensiboError("info") + async_fire_time_changed(hass, dt.utcnow() + timedelta(minutes=1)) + await hass.async_block_till_done() + mock_data.assert_called_once() + state = hass.states.get("climate.hallway") + assert state.state == STATE_UNAVAILABLE + mock_data.reset_mock() + + mock_data.return_value = SensiboData(raw={}, parsed={}) + mock_data.side_effect = None + async_fire_time_changed(hass, dt.utcnow() + timedelta(minutes=3)) + await hass.async_block_till_done() + mock_data.assert_called_once() + state = hass.states.get("climate.hallway") + assert state.state == STATE_UNAVAILABLE + mock_data.reset_mock() + + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "hvac_mode", "heat") + monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "device_on", True) + + mock_data.return_value = DATA_FROM_API + mock_data.side_effect = None + async_fire_time_changed(hass, dt.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + mock_data.assert_called_once() + state = hass.states.get("climate.hallway") + assert state.state == "heat" + mock_data.reset_mock() + + mock_data.side_effect = AuthenticationError("info") + async_fire_time_changed(hass, dt.utcnow() + timedelta(minutes=7)) + await hass.async_block_till_done() + mock_data.assert_called_once() + state = hass.states.get("climate.hallway") + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/sensibo/test_diagnostics.py b/tests/components/sensibo/test_diagnostics.py new file mode 100644 index 00000000000..b4e85dad2b4 --- /dev/null +++ b/tests/components/sensibo/test_diagnostics.py @@ -0,0 +1,48 @@ +"""Test Sensibo diagnostics.""" +from __future__ import annotations + +import aiohttp + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, hass_client: aiohttp.client, load_int: ConfigEntry +): + """Test generating diagnostics for a config entry.""" + entry = load_int + + diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) + + assert diag == { + "status": "success", + "result": [ + { + "id": "**REDACTED**", + "qrId": "**REDACTED**", + "room": {"uid": "**REDACTED**", "name": "Hallway", "icon": "Lounge"}, + "acState": { + "timestamp": { + "time": "2022-04-30T19:58:15.544787Z", + "secondsAgo": 0, + }, + "on": False, + "mode": "fan", + "fanLevel": "high", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on", + }, + "location": "**REDACTED**", + "accessPoint": {"ssid": "**REDACTED**", "password": None}, + "macAddress": "**REDACTED**", + "autoOffMinutes": None, + "autoOffEnabled": False, + "antiMoldTimer": None, + "antiMoldConfig": None, + } + ], + } diff --git a/tests/components/sensibo/test_entity.py b/tests/components/sensibo/test_entity.py new file mode 100644 index 00000000000..7df70c7d45e --- /dev/null +++ b/tests/components/sensibo/test_entity.py @@ -0,0 +1,132 @@ +"""The test for the sensibo entity.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +import pytest + +from homeassistant.components.climate.const import ( + ATTR_FAN_MODE, + DOMAIN as CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, +) +from homeassistant.components.number.const import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.components.sensibo.const import SENSIBO_ERRORS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util import dt + +from .response import DATA_FROM_API + +from tests.common import async_fire_time_changed + + +async def test_entity(hass: HomeAssistant, load_int: ConfigEntry) -> None: + """Test the Sensibo climate.""" + + state1 = hass.states.get("climate.hallway") + assert state1 + + dr_reg = dr.async_get(hass) + dr_entries = dr.async_entries_for_config_entry(dr_reg, load_int.entry_id) + dr_entry: dr.DeviceEntry + for dr_entry in dr_entries: + if dr_entry.name == "Hallway": + assert dr_entry.identifiers == {("sensibo", "ABC999111")} + device_id = dr_entry.id + + er_reg = er.async_get(hass) + er_entries = er.async_entries_for_device( + er_reg, device_id, include_disabled_entities=True + ) + er_entry: er.RegistryEntry + for er_entry in er_entries: + if er_entry.name == "Hallway": + assert er_entry.unique_id == "Hallway" + + +@pytest.mark.parametrize("p_error", SENSIBO_ERRORS) +async def test_entity_send_command( + hass: HomeAssistant, p_error: Exception, load_int: ConfigEntry +) -> None: + """Test the Sensibo send command with error.""" + + state = hass.states.get("climate.hallway") + assert state + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_FAN_MODE: "low"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("climate.hallway") + assert state.attributes["fan_mode"] == "low" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + side_effect=p_error, + ): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_FAN_MODE: "low"}, + blocking=True, + ) + + state = hass.states.get("climate.hallway") + assert state.attributes["fan_mode"] == "low" + + +async def test_entity_send_command_calibration( + hass: HomeAssistant, load_int: ConfigEntry +) -> None: + """Test the Sensibo send command for calibration.""" + + registry = er.async_get(hass) + registry.async_update_entity( + "number.hallway_temperature_calibration", disabled_by=None + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state = hass.states.get("number.hallway_temperature_calibration") + assert state.state == "0.1" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_calibration", + return_value={"status": "success"}, + ): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: 0.2}, + blocking=True, + ) + + state = hass.states.get("number.hallway_temperature_calibration") + assert state.state == "0.2" diff --git a/tests/components/sensibo/test_init.py b/tests/components/sensibo/test_init.py new file mode 100644 index 00000000000..b9ad56eaf07 --- /dev/null +++ b/tests/components/sensibo/test_init.py @@ -0,0 +1,132 @@ +"""Test for Sensibo component Init.""" +from __future__ import annotations + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.sensibo.const import DOMAIN +from homeassistant.components.sensibo.util import NoUsernameError +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant + +from . import ENTRY_CONFIG +from .response import DATA_FROM_API + +from tests.common import MockConfigEntry + + +async def test_setup_entry(hass: HomeAssistant) -> None: + """Test setup entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="12", + version=2, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == config_entries.ConfigEntryState.LOADED + + +async def test_migrate_entry(hass: HomeAssistant) -> None: + """Test migrate entry unique id.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="12", + version=1, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == config_entries.ConfigEntryState.LOADED + assert entry.version == 2 + assert entry.unique_id == "username" + + +async def test_migrate_entry_fails(hass: HomeAssistant) -> None: + """Test migrate entry unique id.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="12", + version=1, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + side_effect=NoUsernameError("No username returned"), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == config_entries.ConfigEntryState.MIGRATION_ERROR + assert entry.version == 1 + assert entry.unique_id == "12" + + +async def test_unload_entry(hass: HomeAssistant) -> None: + """Test unload an entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="12", + version="2", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=DATA_FROM_API, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == config_entries.ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED From 118bae6cb40468e4c351d92b49eef434f7befb39 Mon Sep 17 00:00:00 2001 From: Alessandro Di Felice Date: Fri, 6 May 2022 02:27:46 -0500 Subject: [PATCH 0257/3516] Upgrade glances_api to 0.3.5 (#71243) --- homeassistant/components/glances/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/glances/manifest.json b/homeassistant/components/glances/manifest.json index 73f4d2f333f..3c2906f9fd6 100644 --- a/homeassistant/components/glances/manifest.json +++ b/homeassistant/components/glances/manifest.json @@ -3,7 +3,7 @@ "name": "Glances", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/glances", - "requirements": ["glances_api==0.3.4"], + "requirements": ["glances_api==0.3.5"], "codeowners": ["@engrbm87"], "iot_class": "local_polling", "loggers": ["glances_api"] diff --git a/requirements_all.txt b/requirements_all.txt index f47df3386ad..38566ceed22 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -723,7 +723,7 @@ gios==2.1.0 gitterpy==0.1.7 # homeassistant.components.glances -glances_api==0.3.4 +glances_api==0.3.5 # homeassistant.components.goalzero goalzero==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4d7b846f10a..953aeebc8de 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -514,7 +514,7 @@ getmac==0.8.2 gios==2.1.0 # homeassistant.components.glances -glances_api==0.3.4 +glances_api==0.3.5 # homeassistant.components.goalzero goalzero==0.2.1 From 225d41f82abf55f2671e4ca15358e75b7732213c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 6 May 2022 12:07:02 +0200 Subject: [PATCH 0258/3516] Bump numpy to v1.22.3 (#71393) * Bump numpy to v1.22.3 * Fix mypy --- homeassistant/components/compensation/manifest.json | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/iqvia/sensor.py | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index 213e8888e23..1f96ae25953 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,7 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.21.6"], + "requirements": ["numpy==1.22.3"], "codeowners": ["@Petro31"], "iot_class": "calculated" } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 9bb07157b54..f363d7aa91c 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.21.6", "pyiqvia==2022.04.0"], + "requirements": ["numpy==1.22.3", "pyiqvia==2022.04.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyiqvia"] diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 51f2969e9fe..d8c7ea317c8 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -161,7 +161,7 @@ def calculate_trend(indices: list[float]) -> str: """Calculate the "moving average" of a set of indices.""" index_range = np.arange(0, len(indices)) index_array = np.array(indices) - linear_fit = np.polyfit(index_range, index_array, 1) # type: ignore[no-untyped-call] + linear_fit = np.polyfit(index_range, index_array, 1) slope = round(linear_fit[0], 2) if slope > 0: diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 504b83bdaf9..deba05b4065 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.21.6", "opencv-python-headless==4.5.2.54"], + "requirements": ["numpy==1.22.3", "opencv-python-headless==4.5.2.54"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 0f53dd61cb4..cdc3659153d 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.5.0", "tf-models-official==2.5.0", "pycocotools==2.0.1", - "numpy==1.21.6", + "numpy==1.22.3", "pillow==9.1.0" ], "codeowners": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index aaae8f7cc54..d50455aa9ab 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.21.6"], + "requirements": ["numpy==1.22.3"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 38566ceed22..33da79b3234 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1114,7 +1114,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.21.6 +numpy==1.22.3 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 953aeebc8de..398cbb62d92 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -758,7 +758,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.21.6 +numpy==1.22.3 # homeassistant.components.google oauth2client==4.1.3 From f1733236bbffa1f828013f5a4cc89648e53e98dd Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 6 May 2022 12:51:19 +0200 Subject: [PATCH 0259/3516] Revert "Bump numpy to v1.22.3 (#71393)" (#71407) This reverts commit 225d41f82abf55f2671e4ca15358e75b7732213c. --- homeassistant/components/compensation/manifest.json | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/iqvia/sensor.py | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index 1f96ae25953..213e8888e23 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,7 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.22.3"], + "requirements": ["numpy==1.21.6"], "codeowners": ["@Petro31"], "iot_class": "calculated" } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index f363d7aa91c..9bb07157b54 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.22.3", "pyiqvia==2022.04.0"], + "requirements": ["numpy==1.21.6", "pyiqvia==2022.04.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyiqvia"] diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index d8c7ea317c8..51f2969e9fe 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -161,7 +161,7 @@ def calculate_trend(indices: list[float]) -> str: """Calculate the "moving average" of a set of indices.""" index_range = np.arange(0, len(indices)) index_array = np.array(indices) - linear_fit = np.polyfit(index_range, index_array, 1) + linear_fit = np.polyfit(index_range, index_array, 1) # type: ignore[no-untyped-call] slope = round(linear_fit[0], 2) if slope > 0: diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index deba05b4065..504b83bdaf9 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.22.3", "opencv-python-headless==4.5.2.54"], + "requirements": ["numpy==1.21.6", "opencv-python-headless==4.5.2.54"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index cdc3659153d..0f53dd61cb4 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.5.0", "tf-models-official==2.5.0", "pycocotools==2.0.1", - "numpy==1.22.3", + "numpy==1.21.6", "pillow==9.1.0" ], "codeowners": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index d50455aa9ab..aaae8f7cc54 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.22.3"], + "requirements": ["numpy==1.21.6"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 33da79b3234..38566ceed22 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1114,7 +1114,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.22.3 +numpy==1.21.6 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 398cbb62d92..953aeebc8de 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -758,7 +758,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.22.3 +numpy==1.21.6 # homeassistant.components.google oauth2client==4.1.3 From e5619f4af1e1afe28c3f614a79d6756eb4da8729 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 6 May 2022 13:27:12 +0200 Subject: [PATCH 0260/3516] Remove yaml import trafikverket_train (#71410) --- .../trafikverket_train/config_flow.py | 5 -- .../components/trafikverket_train/const.py | 1 - .../components/trafikverket_train/sensor.py | 60 +-------------- .../trafikverket_train/test_config_flow.py | 75 ------------------- 4 files changed, 3 insertions(+), 138 deletions(-) diff --git a/homeassistant/components/trafikverket_train/config_flow.py b/homeassistant/components/trafikverket_train/config_flow.py index 79c5978de2c..823b393f7b1 100644 --- a/homeassistant/components/trafikverket_train/config_flow.py +++ b/homeassistant/components/trafikverket_train/config_flow.py @@ -102,11 +102,6 @@ class TVTrainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, config: dict[str, Any] | None) -> FlowResult: - """Import a configuration from config.yaml.""" - - return await self.async_step_user(user_input=config) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/trafikverket_train/const.py b/homeassistant/components/trafikverket_train/const.py index f0a6a1d6a18..253383b4b5a 100644 --- a/homeassistant/components/trafikverket_train/const.py +++ b/homeassistant/components/trafikverket_train/const.py @@ -5,7 +5,6 @@ DOMAIN = "trafikverket_train" PLATFORMS = [Platform.SENSOR] ATTRIBUTION = "Data provided by Trafikverket" -CONF_TRAINS = "trains" CONF_FROM = "from" CONF_TO = "to" CONF_TIME = "time" diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index 6ae6af7d363..4a419ff3b33 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -7,26 +7,19 @@ from typing import Any from pytrafikverket import TrafikverketTrain from pytrafikverket.trafikverket_train import StationInfo, TrainStop -import voluptuous as vol -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, -) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt -from .const import CONF_FROM, CONF_TIME, CONF_TO, CONF_TRAINS, DOMAIN +from .const import CONF_FROM, CONF_TIME, CONF_TO, DOMAIN from .util import create_unique_id _LOGGER = logging.getLogger(__name__) @@ -43,53 +36,6 @@ ATTR_DEVIATIONS = "deviations" ICON = "mdi:train" SCAN_INTERVAL = timedelta(minutes=5) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_TRAINS): [ - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_TO): cv.string, - vol.Required(CONF_FROM): cv.string, - vol.Optional(CONF_TIME): cv.time, - vol.Optional(CONF_WEEKDAY, default=WEEKDAYS): vol.All( - cv.ensure_list, [vol.In(WEEKDAYS)] - ), - } - ], - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import Trafikverket Train configuration from YAML.""" - _LOGGER.warning( - # Config flow added in Home Assistant Core 2022.3, remove import flow in 2022.7 - "Loading Trafikverket Train via platform setup is deprecated; Please remove it from your configuration" - ) - - for train in config[CONF_TRAINS]: - - new_config = { - CONF_API_KEY: config[CONF_API_KEY], - CONF_FROM: train[CONF_FROM], - CONF_TO: train[CONF_TO], - CONF_TIME: str(train.get(CONF_TIME)), - CONF_WEEKDAY: train.get(CONF_WEEKDAY, WEEKDAYS), - } - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=new_config, - ) - ) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/tests/components/trafikverket_train/test_config_flow.py b/tests/components/trafikverket_train/test_config_flow.py index b8e3548af2e..09539584cbc 100644 --- a/tests/components/trafikverket_train/test_config_flow.py +++ b/tests/components/trafikverket_train/test_config_flow.py @@ -66,81 +66,6 @@ async def test_form(hass: HomeAssistant) -> None: ) -async def test_import_flow_success(hass: HomeAssistant) -> None: - """Test a successful import of yaml.""" - - with patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", - ), patch( - "homeassistant.components.trafikverket_train.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_API_KEY: "1234567890", - CONF_FROM: "Stockholm C", - CONF_TO: "Uppsala C", - CONF_TIME: "10:00", - CONF_WEEKDAY: ["mon", "fri"], - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "Stockholm C to Uppsala C at 10:00" - assert result2["data"] == { - "api_key": "1234567890", - "name": "Stockholm C to Uppsala C at 10:00", - "from": "Stockholm C", - "to": "Uppsala C", - "time": "10:00", - "weekday": ["mon", "fri"], - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_flow_already_exist(hass: HomeAssistant) -> None: - """Test import of yaml already exist.""" - - MockConfigEntry( - domain=DOMAIN, - data={ - CONF_API_KEY: "1234567890", - CONF_NAME: "Stockholm C to Uppsala C", - CONF_FROM: "Stockholm C", - CONF_TO: "Uppsala C", - CONF_TIME: "10:00", - CONF_WEEKDAY: WEEKDAYS, - }, - unique_id=f"stockholmc-uppsalac-10:00-{WEEKDAYS}", - ).add_to_hass(hass) - - with patch( - "homeassistant.components.trafikverket_train.async_setup_entry", - return_value=True, - ), patch( - "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", - ): - result3 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_NAME: "Stockholm C to Uppsala C", - CONF_API_KEY: "1234567890", - CONF_FROM: "Stockholm C", - CONF_TO: "Uppsala C", - CONF_TIME: "10:00", - CONF_WEEKDAY: WEEKDAYS, - }, - ) - await hass.async_block_till_done() - - assert result3["type"] == RESULT_TYPE_ABORT - assert result3["reason"] == "already_configured" - - @pytest.mark.parametrize( "error_message,base_error", [ From 173f14379b98b108d4d8c05fecde57e59b2dc7a9 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 6 May 2022 10:24:08 -0400 Subject: [PATCH 0261/3516] Update Zigpy attribute cache for switch devices that do not report state (#71417) * fix devices that do not report state * whoops --- .../components/zha/core/channels/general.py | 16 ++++++++++++++++ homeassistant/components/zha/switch.py | 8 ++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index e6524c9aad1..2e6093dd4f7 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -308,6 +308,22 @@ class OnOffChannel(ZigbeeChannel): """Return cached value of on/off attribute.""" return self.cluster.get("on_off") + async def turn_on(self) -> bool: + """Turn the on off cluster on.""" + result = await self.on() + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + return False + self.cluster.update_attribute(self.ON_OFF, t.Bool.true) + return True + + async def turn_off(self) -> bool: + """Turn the on off cluster off.""" + result = await self.off() + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + return False + self.cluster.update_attribute(self.ON_OFF, t.Bool.false) + return True + @callback def cluster_command(self, tsn, command_id, args): """Handle commands received to this cluster.""" diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 254e3691da1..76c41093ed6 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -64,15 +64,15 @@ class Switch(ZhaEntity, SwitchEntity): async def async_turn_on(self, **kwargs) -> None: """Turn the entity on.""" - result = await self._on_off_channel.on() - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + result = await self._on_off_channel.turn_on() + if not result: return self.async_write_ha_state() async def async_turn_off(self, **kwargs) -> None: """Turn the entity off.""" - result = await self._on_off_channel.off() - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + result = await self._on_off_channel.turn_off() + if not result: return self.async_write_ha_state() From 6984c56cc6c34e5522823b801bfc8cf6568b164f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 6 May 2022 17:00:01 +0200 Subject: [PATCH 0262/3516] Freeze numpy on wheel build (#71408) --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 950338cdd13..e43c397f3bc 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -96,7 +96,7 @@ jobs: wheels-user: wheels env-file: true apk: "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;cargo" - pip: "Cython;numpy" + pip: "Cython;numpy==1.21.6" skip-binary: aiohttp constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" From 1a00bb9fc4a12b7cacfd1371892b33d06a08ecd8 Mon Sep 17 00:00:00 2001 From: 0bmay <57501269+0bmay@users.noreply.github.com> Date: Fri, 6 May 2022 08:00:48 -0700 Subject: [PATCH 0263/3516] Fix Canary camera stream blocking call (#71369) * fix: Canary stream camera, fix blocker fixes a "detected blocking call to putrequest inside the event loop. This is causing stability issues. Please report issue for canary doing blocking calls at homeassistant/components/canary/camera.py, line 149: self._live_stream_session.live_stream_url, extra_cmd=self._ffmpeg_arguments" from log file. * refactor: black formatting changes tsia --- homeassistant/components/canary/camera.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 46826d80291..5caae6f5cc5 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -144,10 +144,11 @@ class CanaryCamera(CoordinatorEntity[CanaryDataUpdateCoordinator], Camera): if self._live_stream_session is None: return None - stream = CameraMjpeg(self._ffmpeg.binary) - await stream.open_camera( - self._live_stream_session.live_stream_url, extra_cmd=self._ffmpeg_arguments + live_stream_url = await self.hass.async_add_executor_job( + getattr, self._live_stream_session, "live_stream_url" ) + stream = CameraMjpeg(self._ffmpeg.binary) + await stream.open_camera(live_stream_url, extra_cmd=self._ffmpeg_arguments) try: stream_reader = await stream.get_reader() From b1a04302b55dd511f3416cf2ad21a21e78d23bed Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 6 May 2022 11:25:01 -0700 Subject: [PATCH 0264/3516] Stringify enums in selectors (#71441) --- homeassistant/helpers/selector.py | 17 +++-------------- tests/components/automation/test_init.py | 2 ++ tests/components/blueprint/test_importer.py | 1 + .../components/blueprint/test_websocket_api.py | 4 +++- tests/components/trace/test_websocket_api.py | 1 + tests/helpers/test_selector.py | 4 ++++ .../automation/test_event_service.yaml | 4 ++++ 7 files changed, 18 insertions(+), 15 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index afe74a4d7af..1ae0082c06f 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -73,11 +73,7 @@ class Selector: def serialize(self) -> Any: """Serialize Selector for voluptuous_serialize.""" - return {"selector": {self.selector_type: self.serialize_config()}} - - def serialize_config(self) -> Any: - """Serialize config.""" - return self.config + return {"selector": {self.selector_type: self.config}} SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA = vol.Schema( @@ -617,8 +613,8 @@ class NumberSelector(Selector): vol.Coerce(float), vol.Range(min=1e-3) ), vol.Optional(CONF_UNIT_OF_MEASUREMENT): str, - vol.Optional(CONF_MODE, default=NumberSelectorMode.SLIDER): vol.Coerce( - NumberSelectorMode + vol.Optional(CONF_MODE, default=NumberSelectorMode.SLIDER): vol.All( + vol.Coerce(NumberSelectorMode), lambda val: val.value ), } ), @@ -629,13 +625,6 @@ class NumberSelector(Selector): """Instantiate a selector.""" super().__init__(config) - def serialize_config(self) -> Any: - """Serialize the selector config.""" - return { - **self.config, - "mode": self.config["mode"].value, - } - def __call__(self, data: Any) -> float: """Validate the passed selection.""" value: float = vol.Coerce(float)(data) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index dbc8f0fc346..45f719a326f 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1474,6 +1474,7 @@ async def test_blueprint_automation(hass, calls): "input": { "trigger_event": "blueprint_event", "service_to_call": "test.automation", + "a_number": 5, }, } } @@ -1499,6 +1500,7 @@ async def test_blueprint_automation_bad_config(hass, caplog): "input": { "trigger_event": "blueprint_event", "service_to_call": {"dict": "not allowed"}, + "a_number": 5, }, } } diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 806cdb2cb8d..46a98840a80 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -199,6 +199,7 @@ async def test_fetch_blueprint_from_github_url(hass, aioclient_mock, url): assert imported_blueprint.blueprint.inputs == { "service_to_call": None, "trigger_event": {"selector": {"text": {}}}, + "a_number": {"selector": {"number": {"mode": "box", "step": 1.0}}}, } assert imported_blueprint.suggested_filename == "balloob/motion_light" assert imported_blueprint.blueprint.metadata["source_url"] == url diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index 40f24d98016..9376710abee 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -33,6 +33,7 @@ async def test_list_blueprints(hass, hass_ws_client): "input": { "service_to_call": None, "trigger_event": {"selector": {"text": {}}}, + "a_number": {"selector": {"number": {"mode": "box", "step": 1.0}}}, }, "name": "Call service based on event", }, @@ -95,6 +96,7 @@ async def test_import_blueprint(hass, aioclient_mock, hass_ws_client): "input": { "service_to_call": None, "trigger_event": {"selector": {"text": {}}}, + "a_number": {"selector": {"number": {"mode": "box", "step": 1.0}}}, }, "name": "Call service based on event", "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", @@ -129,7 +131,7 @@ async def test_save_blueprint(hass, aioclient_mock, hass_ws_client): assert msg["success"] assert write_mock.mock_calls assert write_mock.call_args[0] == ( - "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n selector:\n text: {}\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n entity_id: light.kitchen\n", + "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n selector:\n text: {}\n service_to_call:\n a_number:\n selector:\n number:\n mode: box\n step: 1.0\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n entity_id: light.kitchen\n", ) diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py index f55999a1e48..21eefb14c1b 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -1539,6 +1539,7 @@ async def test_trace_blueprint_automation( "input": { "trigger_event": "blueprint_event", "service_to_call": "test.automation", + "a_number": 5, }, }, } diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 8c94e3d3c56..ed831026065 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -1,4 +1,6 @@ """Test selectors.""" +from enum import Enum + import pytest import voluptuous as vol @@ -52,6 +54,8 @@ def _test_selector( config = {selector_type: schema} selector.validate_selector(config) selector_instance = selector.selector(config) + # We do not allow enums in the config, as they cannot serialize + assert not any(isinstance(val, Enum) for val in selector_instance.config.values()) # Use selector in schema and validate vol_schema = vol.Schema({"selection": selector_instance}) diff --git a/tests/testing_config/blueprints/automation/test_event_service.yaml b/tests/testing_config/blueprints/automation/test_event_service.yaml index 648cef39b96..ba7462ed2e0 100644 --- a/tests/testing_config/blueprints/automation/test_event_service.yaml +++ b/tests/testing_config/blueprints/automation/test_event_service.yaml @@ -6,6 +6,10 @@ blueprint: selector: text: service_to_call: + a_number: + selector: + number: + mode: "box" trigger: platform: event event_type: !input trigger_event From c54e236416173192d853b89da734fe48bebfb59e Mon Sep 17 00:00:00 2001 From: Alessandro Di Felice Date: Fri, 6 May 2022 02:27:46 -0500 Subject: [PATCH 0265/3516] Upgrade glances_api to 0.3.5 (#71243) --- homeassistant/components/glances/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/glances/manifest.json b/homeassistant/components/glances/manifest.json index 73f4d2f333f..3c2906f9fd6 100644 --- a/homeassistant/components/glances/manifest.json +++ b/homeassistant/components/glances/manifest.json @@ -3,7 +3,7 @@ "name": "Glances", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/glances", - "requirements": ["glances_api==0.3.4"], + "requirements": ["glances_api==0.3.5"], "codeowners": ["@engrbm87"], "iot_class": "local_polling", "loggers": ["glances_api"] diff --git a/requirements_all.txt b/requirements_all.txt index b6d512d97be..f5c09da8eeb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -723,7 +723,7 @@ gios==2.1.0 gitterpy==0.1.7 # homeassistant.components.glances -glances_api==0.3.4 +glances_api==0.3.5 # homeassistant.components.goalzero goalzero==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f93d8c5e82..302e90b5648 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -514,7 +514,7 @@ getmac==0.8.2 gios==2.1.0 # homeassistant.components.glances -glances_api==0.3.4 +glances_api==0.3.5 # homeassistant.components.goalzero goalzero==0.2.1 From 46a36adf268442f55266b069db22bacac81c29fd Mon Sep 17 00:00:00 2001 From: 0bmay <57501269+0bmay@users.noreply.github.com> Date: Fri, 6 May 2022 08:00:48 -0700 Subject: [PATCH 0266/3516] Fix Canary camera stream blocking call (#71369) * fix: Canary stream camera, fix blocker fixes a "detected blocking call to putrequest inside the event loop. This is causing stability issues. Please report issue for canary doing blocking calls at homeassistant/components/canary/camera.py, line 149: self._live_stream_session.live_stream_url, extra_cmd=self._ffmpeg_arguments" from log file. * refactor: black formatting changes tsia --- homeassistant/components/canary/camera.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 46826d80291..5caae6f5cc5 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -144,10 +144,11 @@ class CanaryCamera(CoordinatorEntity[CanaryDataUpdateCoordinator], Camera): if self._live_stream_session is None: return None - stream = CameraMjpeg(self._ffmpeg.binary) - await stream.open_camera( - self._live_stream_session.live_stream_url, extra_cmd=self._ffmpeg_arguments + live_stream_url = await self.hass.async_add_executor_job( + getattr, self._live_stream_session, "live_stream_url" ) + stream = CameraMjpeg(self._ffmpeg.binary) + await stream.open_camera(live_stream_url, extra_cmd=self._ffmpeg_arguments) try: stream_reader = await stream.get_reader() From 2fffac02a3b6fffa51c1618d0ddc6c04aebe7130 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 6 May 2022 10:24:08 -0400 Subject: [PATCH 0267/3516] Update Zigpy attribute cache for switch devices that do not report state (#71417) * fix devices that do not report state * whoops --- .../components/zha/core/channels/general.py | 16 ++++++++++++++++ homeassistant/components/zha/switch.py | 8 ++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index e6524c9aad1..2e6093dd4f7 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -308,6 +308,22 @@ class OnOffChannel(ZigbeeChannel): """Return cached value of on/off attribute.""" return self.cluster.get("on_off") + async def turn_on(self) -> bool: + """Turn the on off cluster on.""" + result = await self.on() + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + return False + self.cluster.update_attribute(self.ON_OFF, t.Bool.true) + return True + + async def turn_off(self) -> bool: + """Turn the on off cluster off.""" + result = await self.off() + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + return False + self.cluster.update_attribute(self.ON_OFF, t.Bool.false) + return True + @callback def cluster_command(self, tsn, command_id, args): """Handle commands received to this cluster.""" diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 254e3691da1..76c41093ed6 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -64,15 +64,15 @@ class Switch(ZhaEntity, SwitchEntity): async def async_turn_on(self, **kwargs) -> None: """Turn the entity on.""" - result = await self._on_off_channel.on() - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + result = await self._on_off_channel.turn_on() + if not result: return self.async_write_ha_state() async def async_turn_off(self, **kwargs) -> None: """Turn the entity off.""" - result = await self._on_off_channel.off() - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + result = await self._on_off_channel.turn_off() + if not result: return self.async_write_ha_state() From dc3e421b3be033c347b0e69cbc5cd55046d297e0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 6 May 2022 11:25:01 -0700 Subject: [PATCH 0268/3516] Stringify enums in selectors (#71441) --- homeassistant/helpers/selector.py | 17 +++-------------- tests/components/automation/test_init.py | 2 ++ tests/components/blueprint/test_importer.py | 1 + .../components/blueprint/test_websocket_api.py | 4 +++- tests/components/trace/test_websocket_api.py | 1 + tests/helpers/test_selector.py | 4 ++++ .../automation/test_event_service.yaml | 4 ++++ 7 files changed, 18 insertions(+), 15 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index afe74a4d7af..1ae0082c06f 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -73,11 +73,7 @@ class Selector: def serialize(self) -> Any: """Serialize Selector for voluptuous_serialize.""" - return {"selector": {self.selector_type: self.serialize_config()}} - - def serialize_config(self) -> Any: - """Serialize config.""" - return self.config + return {"selector": {self.selector_type: self.config}} SINGLE_ENTITY_SELECTOR_CONFIG_SCHEMA = vol.Schema( @@ -617,8 +613,8 @@ class NumberSelector(Selector): vol.Coerce(float), vol.Range(min=1e-3) ), vol.Optional(CONF_UNIT_OF_MEASUREMENT): str, - vol.Optional(CONF_MODE, default=NumberSelectorMode.SLIDER): vol.Coerce( - NumberSelectorMode + vol.Optional(CONF_MODE, default=NumberSelectorMode.SLIDER): vol.All( + vol.Coerce(NumberSelectorMode), lambda val: val.value ), } ), @@ -629,13 +625,6 @@ class NumberSelector(Selector): """Instantiate a selector.""" super().__init__(config) - def serialize_config(self) -> Any: - """Serialize the selector config.""" - return { - **self.config, - "mode": self.config["mode"].value, - } - def __call__(self, data: Any) -> float: """Validate the passed selection.""" value: float = vol.Coerce(float)(data) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index dbc8f0fc346..45f719a326f 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1474,6 +1474,7 @@ async def test_blueprint_automation(hass, calls): "input": { "trigger_event": "blueprint_event", "service_to_call": "test.automation", + "a_number": 5, }, } } @@ -1499,6 +1500,7 @@ async def test_blueprint_automation_bad_config(hass, caplog): "input": { "trigger_event": "blueprint_event", "service_to_call": {"dict": "not allowed"}, + "a_number": 5, }, } } diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 806cdb2cb8d..46a98840a80 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -199,6 +199,7 @@ async def test_fetch_blueprint_from_github_url(hass, aioclient_mock, url): assert imported_blueprint.blueprint.inputs == { "service_to_call": None, "trigger_event": {"selector": {"text": {}}}, + "a_number": {"selector": {"number": {"mode": "box", "step": 1.0}}}, } assert imported_blueprint.suggested_filename == "balloob/motion_light" assert imported_blueprint.blueprint.metadata["source_url"] == url diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index 40f24d98016..9376710abee 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -33,6 +33,7 @@ async def test_list_blueprints(hass, hass_ws_client): "input": { "service_to_call": None, "trigger_event": {"selector": {"text": {}}}, + "a_number": {"selector": {"number": {"mode": "box", "step": 1.0}}}, }, "name": "Call service based on event", }, @@ -95,6 +96,7 @@ async def test_import_blueprint(hass, aioclient_mock, hass_ws_client): "input": { "service_to_call": None, "trigger_event": {"selector": {"text": {}}}, + "a_number": {"selector": {"number": {"mode": "box", "step": 1.0}}}, }, "name": "Call service based on event", "source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml", @@ -129,7 +131,7 @@ async def test_save_blueprint(hass, aioclient_mock, hass_ws_client): assert msg["success"] assert write_mock.mock_calls assert write_mock.call_args[0] == ( - "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n selector:\n text: {}\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n entity_id: light.kitchen\n", + "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n selector:\n text: {}\n service_to_call:\n a_number:\n selector:\n number:\n mode: box\n step: 1.0\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n entity_id: light.kitchen\n", ) diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py index f55999a1e48..21eefb14c1b 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -1539,6 +1539,7 @@ async def test_trace_blueprint_automation( "input": { "trigger_event": "blueprint_event", "service_to_call": "test.automation", + "a_number": 5, }, }, } diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 8c94e3d3c56..ed831026065 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -1,4 +1,6 @@ """Test selectors.""" +from enum import Enum + import pytest import voluptuous as vol @@ -52,6 +54,8 @@ def _test_selector( config = {selector_type: schema} selector.validate_selector(config) selector_instance = selector.selector(config) + # We do not allow enums in the config, as they cannot serialize + assert not any(isinstance(val, Enum) for val in selector_instance.config.values()) # Use selector in schema and validate vol_schema = vol.Schema({"selection": selector_instance}) diff --git a/tests/testing_config/blueprints/automation/test_event_service.yaml b/tests/testing_config/blueprints/automation/test_event_service.yaml index 648cef39b96..ba7462ed2e0 100644 --- a/tests/testing_config/blueprints/automation/test_event_service.yaml +++ b/tests/testing_config/blueprints/automation/test_event_service.yaml @@ -6,6 +6,10 @@ blueprint: selector: text: service_to_call: + a_number: + selector: + number: + mode: "box" trigger: platform: event event_type: !input trigger_event From 4a7710572c2e92ed9ef60ca428c786de4bec403f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 6 May 2022 11:25:53 -0700 Subject: [PATCH 0269/3516] Bumped version to 2022.5.2 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c2cb8119602..5aed0cdaae9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 04a68db8ca6..6d41c8526db 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.5.1 +version = 2022.5.2 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 92f46a48c389ab89d069fbc35ef15586de0dbfa8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 May 2022 16:01:05 -0500 Subject: [PATCH 0270/3516] Bump zeroconf to 0.38.6 (#71447) --- 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 e1ea2c82b1f..8cfc0698dc4 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.38.5"], + "requirements": ["zeroconf==0.38.6"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0a2846a44ad..5c3ef6712b0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 yarl==1.7.2 -zeroconf==0.38.5 +zeroconf==0.38.6 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 38566ceed22..48f71c37f93 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2483,7 +2483,7 @@ youtube_dl==2021.12.17 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.38.5 +zeroconf==0.38.6 # homeassistant.components.zha zha-quirks==0.0.73 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 953aeebc8de..8adb336e488 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1623,7 +1623,7 @@ yeelight==0.7.10 youless-api==0.16 # homeassistant.components.zeroconf -zeroconf==0.38.5 +zeroconf==0.38.6 # homeassistant.components.zha zha-quirks==0.0.73 From 44d8f2f7730f7fb6977e96d0e591323b5682e23e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 7 May 2022 00:20:47 +0000 Subject: [PATCH 0271/3516] [ci skip] Translation update --- homeassistant/components/asuswrt/translations/it.json | 3 +++ homeassistant/components/asuswrt/translations/nl.json | 1 + homeassistant/components/asuswrt/translations/no.json | 2 +- homeassistant/components/asuswrt/translations/pl.json | 3 ++- homeassistant/components/deluge/translations/it.json | 2 +- homeassistant/components/dlna_dmr/translations/it.json | 1 + homeassistant/components/dlna_dmr/translations/nl.json | 1 + homeassistant/components/dlna_dmr/translations/pl.json | 1 + homeassistant/components/dlna_dmr/translations/ru.json | 1 + homeassistant/components/flux_led/translations/it.json | 2 +- homeassistant/components/isy994/translations/it.json | 2 +- homeassistant/components/meater/translations/it.json | 3 +++ homeassistant/components/recorder/translations/nl.json | 8 ++++++++ homeassistant/components/simplisafe/translations/it.json | 2 +- homeassistant/components/vulcan/translations/de.json | 9 +++++++++ homeassistant/components/vulcan/translations/et.json | 9 +++++++++ homeassistant/components/vulcan/translations/it.json | 9 +++++++++ homeassistant/components/vulcan/translations/nl.json | 9 +++++++++ homeassistant/components/vulcan/translations/no.json | 9 +++++++++ homeassistant/components/vulcan/translations/pl.json | 9 +++++++++ homeassistant/components/vulcan/translations/ru.json | 9 +++++++++ .../components/vulcan/translations/zh-Hant.json | 9 +++++++++ 22 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/recorder/translations/nl.json diff --git a/homeassistant/components/asuswrt/translations/it.json b/homeassistant/components/asuswrt/translations/it.json index 0cf06679d83..d2152c85357 100644 --- a/homeassistant/components/asuswrt/translations/it.json +++ b/homeassistant/components/asuswrt/translations/it.json @@ -1,6 +1,9 @@ { "config": { "abort": { + "invalid_unique_id": "Impossibile determinare un ID univoco valido per il dispositivo", + "no_unique_id": "Un dispositivo senza un ID univoco valido \u00e8 gi\u00e0 configurato. La configurazione di pi\u00f9 istanze non \u00e8 possibile", + "not_unique_id_exist": "Un dispositivo senza un ID univoco valido \u00e8 gi\u00e0 configurato. La configurazione di pi\u00f9 istanze non \u00e8 possibile", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "error": { diff --git a/homeassistant/components/asuswrt/translations/nl.json b/homeassistant/components/asuswrt/translations/nl.json index 26d95e16461..4f3611289e5 100644 --- a/homeassistant/components/asuswrt/translations/nl.json +++ b/homeassistant/components/asuswrt/translations/nl.json @@ -2,6 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Onmogelijk om een geldige unieke id voor het apparaat te bepalen", + "no_unique_id": "Een apparaat zonder geldige unieke id is al geconfigureerd. Configuratie van meerdere instanties is niet mogelijk", "not_unique_id_exist": "Een apparaat zonder geldige unieke id is al geconfigureerd. Configuratie van meerdere instanties is niet mogelijk", "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." }, diff --git a/homeassistant/components/asuswrt/translations/no.json b/homeassistant/components/asuswrt/translations/no.json index 5aa48649989..de540484cac 100644 --- a/homeassistant/components/asuswrt/translations/no.json +++ b/homeassistant/components/asuswrt/translations/no.json @@ -3,7 +3,7 @@ "abort": { "invalid_unique_id": "Umulig \u00e5 bestemme en gyldig unik ID for enheten", "no_unique_id": "En enhet uten en gyldig unik ID er allerede konfigurert. Konfigurasjon av flere forekomster er ikke mulig", - "not_unique_id_exist": "En enhet uten en gyldig UniqueID er allerede konfigurert. Konfigurasjon av flere forekomster er ikke mulig", + "not_unique_id_exist": "En enhet uten en gyldig unik ID er allerede konfigurert. Konfigurasjon av flere forekomster er ikke mulig", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { diff --git a/homeassistant/components/asuswrt/translations/pl.json b/homeassistant/components/asuswrt/translations/pl.json index ce0fc53accf..d30a1e9e778 100644 --- a/homeassistant/components/asuswrt/translations/pl.json +++ b/homeassistant/components/asuswrt/translations/pl.json @@ -2,7 +2,8 @@ "config": { "abort": { "invalid_unique_id": "Nie mo\u017cna okre\u015bli\u0107 prawid\u0142owego unikalnego identyfikatora urz\u0105dzenia", - "not_unique_id_exist": "Urz\u0105dzenie bez prawid\u0142owego unikatowego identyfikatora jest ju\u017c skonfigurowane. Konfiguracja wielu instancji nie jest mo\u017cliwa", + "no_unique_id": "Urz\u0105dzenie bez prawid\u0142owego unikalnego identyfikatora jest ju\u017c skonfigurowane. Konfiguracja wielu instancji nie jest mo\u017cliwa.", + "not_unique_id_exist": "Urz\u0105dzenie bez prawid\u0142owego unikalnego identyfikatora jest ju\u017c skonfigurowane. Konfiguracja wielu instancji nie jest mo\u017cliwa.", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "error": { diff --git a/homeassistant/components/deluge/translations/it.json b/homeassistant/components/deluge/translations/it.json index d9407f0c29f..52cfb4a09ae 100644 --- a/homeassistant/components/deluge/translations/it.json +++ b/homeassistant/components/deluge/translations/it.json @@ -16,7 +16,7 @@ "username": "Nome utente", "web_port": "Porta web (per il servizio di visita)" }, - "description": "Per poter utilizzare questa integrazione, devi abilitare la seguente opzione nelle impostazioni di diluvio: Demone > Consenti controlli " + "description": "Per poter utilizzare questa integrazione, devi abilitare la seguente opzione nelle impostazioni di diluvio: Demone > Consenti controlli remoti" } } } diff --git a/homeassistant/components/dlna_dmr/translations/it.json b/homeassistant/components/dlna_dmr/translations/it.json index 545d3cadbcb..f9e3e11d5f0 100644 --- a/homeassistant/components/dlna_dmr/translations/it.json +++ b/homeassistant/components/dlna_dmr/translations/it.json @@ -47,6 +47,7 @@ "step": { "init": { "data": { + "browse_unfiltered": "Mostra file multimediali incompatibili durante la navigazione", "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" diff --git a/homeassistant/components/dlna_dmr/translations/nl.json b/homeassistant/components/dlna_dmr/translations/nl.json index 5331f3340dd..6147fac2007 100644 --- a/homeassistant/components/dlna_dmr/translations/nl.json +++ b/homeassistant/components/dlna_dmr/translations/nl.json @@ -47,6 +47,7 @@ "step": { "init": { "data": { + "browse_unfiltered": "Incompatibele media weergeven tijdens browsen", "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" diff --git a/homeassistant/components/dlna_dmr/translations/pl.json b/homeassistant/components/dlna_dmr/translations/pl.json index 7f831c92f99..23f46c84c38 100644 --- a/homeassistant/components/dlna_dmr/translations/pl.json +++ b/homeassistant/components/dlna_dmr/translations/pl.json @@ -47,6 +47,7 @@ "step": { "init": { "data": { + "browse_unfiltered": "Poka\u017c niezgodne multimedia podczas przegl\u0105dania", "callback_url_override": "Adres Callback URL dla detektora zdarze\u0144", "listen_port": "Port detektora zdarze\u0144 (losowy, je\u015bli nie jest ustawiony)", "poll_availability": "Sondowanie na dost\u0119pno\u015b\u0107 urz\u0105dze\u0144" diff --git a/homeassistant/components/dlna_dmr/translations/ru.json b/homeassistant/components/dlna_dmr/translations/ru.json index d8931e268a0..09c98173b3b 100644 --- a/homeassistant/components/dlna_dmr/translations/ru.json +++ b/homeassistant/components/dlna_dmr/translations/ru.json @@ -47,6 +47,7 @@ "step": { "init": { "data": { + "browse_unfiltered": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0435\u0441\u043e\u0432\u043c\u0435\u0441\u0442\u0438\u043c\u044b\u0435 \u043c\u0435\u0434\u0438\u0430", "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" diff --git a/homeassistant/components/flux_led/translations/it.json b/homeassistant/components/flux_led/translations/it.json index 13b522906ba..f2d20bd45df 100644 --- a/homeassistant/components/flux_led/translations/it.json +++ b/homeassistant/components/flux_led/translations/it.json @@ -27,7 +27,7 @@ "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.", + "custom_effect_transition": "Effetto personalizzato: tipo di transizione tra i colori.", "mode": "La modalit\u00e0 di luminosit\u00e0 scelta." } } diff --git a/homeassistant/components/isy994/translations/it.json b/homeassistant/components/isy994/translations/it.json index 158d9f1d400..dae46ed8275 100644 --- a/homeassistant/components/isy994/translations/it.json +++ b/homeassistant/components/isy994/translations/it.json @@ -41,7 +41,7 @@ "sensor_string": "Stringa Nodo Sensore", "variable_sensor_string": "Stringa Variabile Sensore" }, - "description": "Imposta le opzioni per l'integrazione ISY: \n \u2022 Stringa nodo sensore: qualsiasi dispositivo o cartella che contiene \"Stringa nodo sensore\" nel nome verr\u00e0 trattato come un sensore o un sensore binario. \n \u2022 Ignora stringa: qualsiasi dispositivo con \"Ignora stringa\" nel nome verr\u00e0 ignorato. \n \u2022 Stringa variabile sensore: qualsiasi variabile che contiene \"Stringa variabile sensore\" verr\u00e0 aggiunta come sensore. \n \u2022 Ripristina luminosit\u00e0 luce: se abilitato, verr\u00e0 ripristinata la luminosit\u00e0 precedente quando si accende una luce al posto del livello incorporato nel dispositivo.", + "description": "Imposta le opzioni per l'integrazione ISY: \n \u2022 Stringa nodo sensore: qualsiasi dispositivo o cartella che contiene \"Stringa nodo sensore\" nel nome verr\u00e0 trattato come un sensore o un sensore binario. \n \u2022 Ignora stringa: qualsiasi dispositivo con \"Ignora stringa\" nel nome verr\u00e0 ignorato. \n \u2022 Stringa variabile sensore: qualsiasi variabile che contenga \"Stringa variabile sensore\" sar\u00e0 aggiunta come sensore. \n \u2022 Ripristina luminosit\u00e0 luce: se abilitato, verr\u00e0 ripristinata la luminosit\u00e0 precedente quando si accende una luce al posto del livello incorporato nel dispositivo.", "title": "Opzioni ISY" } } diff --git a/homeassistant/components/meater/translations/it.json b/homeassistant/components/meater/translations/it.json index 090de20f280..d006b2f7e64 100644 --- a/homeassistant/components/meater/translations/it.json +++ b/homeassistant/components/meater/translations/it.json @@ -7,6 +7,9 @@ }, "step": { "reauth_confirm": { + "data": { + "password": "Password" + }, "description": "Conferma la password per l'account Meater Cloud {username}." }, "user": { diff --git a/homeassistant/components/recorder/translations/nl.json b/homeassistant/components/recorder/translations/nl.json new file mode 100644 index 00000000000..5bffcef1bd2 --- /dev/null +++ b/homeassistant/components/recorder/translations/nl.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "current_recorder_run": "Starttijd huidige run", + "oldest_recorder_run": "Starttijd oudste run" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index fb92029795d..17a9a2ceb47 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -14,7 +14,7 @@ "unknown": "Errore imprevisto" }, "progress": { - "email_2fa": "Digita il codice di autenticazione a due fattori\n inviato tramite email." + "email_2fa": "Verifica la presenza nella tua email di un collegamento di verifica da Simplisafe." }, "step": { "mfa": { diff --git a/homeassistant/components/vulcan/translations/de.json b/homeassistant/components/vulcan/translations/de.json index 23d5032f5fa..94f25858119 100644 --- a/homeassistant/components/vulcan/translations/de.json +++ b/homeassistant/components/vulcan/translations/de.json @@ -3,6 +3,7 @@ "abort": { "all_student_already_configured": "Alle Sch\u00fcler wurden bereits hinzugef\u00fcgt.", "already_configured": "Dieser Sch\u00fcler wurde bereits hinzugef\u00fcgt.", + "no_matching_entries": "Keine \u00fcbereinstimmenden Eintr\u00e4ge gefunden, bitte verwende ein anderes Konto oder entferne die Integration mit veraltetem Sch\u00fcler.", "reauth_successful": "Reauth erfolgreich" }, "error": { @@ -37,6 +38,14 @@ }, "description": "Melde dich bei deinem Vulcan-Konto \u00fcber die Registrierungsseite der mobilen App an." }, + "reauth_confirm": { + "data": { + "pin": "PIN", + "region": "Symbol", + "token": "Token" + }, + "description": "Melde dich bei deinem Vulcan-Konto \u00fcber die Registrierungsseite der mobilen App an." + }, "select_saved_credentials": { "data": { "credentials": "Anmelden" diff --git a/homeassistant/components/vulcan/translations/et.json b/homeassistant/components/vulcan/translations/et.json index ab3156cc676..80c97d1c4ed 100644 --- a/homeassistant/components/vulcan/translations/et.json +++ b/homeassistant/components/vulcan/translations/et.json @@ -3,6 +3,7 @@ "abort": { "all_student_already_configured": "K\u00f5ik \u00f5pilased on juba lisatud.", "already_configured": "See \u00f5pilane on juba lisatud.", + "no_matching_entries": "Sobivaid kirjeid ei leitud, kasuta teist kontot v\u00f5i eemalda muudetud \u00f5pilasega sidumine...", "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { @@ -37,6 +38,14 @@ }, "description": "Logi Vulcani kontole sisse mobiilirakenduse registreerimislehe kaudu." }, + "reauth_confirm": { + "data": { + "pin": "PIN kood", + "region": "S\u00fcmbol", + "token": "Token" + }, + "description": "Logi Vulcani kontole sisse mobiilirakenduse registreerimislehe kaudu." + }, "select_saved_credentials": { "data": { "credentials": "Sisselogimine" diff --git a/homeassistant/components/vulcan/translations/it.json b/homeassistant/components/vulcan/translations/it.json index 895f4469eb9..8fe26ddfba6 100644 --- a/homeassistant/components/vulcan/translations/it.json +++ b/homeassistant/components/vulcan/translations/it.json @@ -3,6 +3,7 @@ "abort": { "all_student_already_configured": "Tutti gli studenti sono gi\u00e0 stati aggiunti.", "already_configured": "Quello studente \u00e8 gi\u00e0 stato aggiunto.", + "no_matching_entries": "Nessuna voce corrispondente trovata, utilizza un account diverso o rimuovi l'integrazione con lo studente obsoleto.", "reauth_successful": "Nuova autenticazione avvenuta" }, "error": { @@ -37,6 +38,14 @@ }, "description": "Accedi al tuo account Vulcan utilizzando la pagina di registrazione dell'applicazione mobile." }, + "reauth_confirm": { + "data": { + "pin": "PIN", + "region": "Simbolo", + "token": "Token" + }, + "description": "Accedi al tuo account Vulcan utilizzando la pagina di registrazione dell'applicazione mobile." + }, "select_saved_credentials": { "data": { "credentials": "Accesso" diff --git a/homeassistant/components/vulcan/translations/nl.json b/homeassistant/components/vulcan/translations/nl.json index b05b32937c0..df1990ce1a0 100644 --- a/homeassistant/components/vulcan/translations/nl.json +++ b/homeassistant/components/vulcan/translations/nl.json @@ -3,6 +3,7 @@ "abort": { "all_student_already_configured": "Alle studenten zijn al toegevoegd.", "already_configured": "Die student is al toegevoegd.", + "no_matching_entries": "Geen overeenkomende vermeldingen gevonden, gebruik een ander account of verwijder integratie met verouderde student..", "reauth_successful": "Opnieuw verifi\u00ebren gelukt" }, "error": { @@ -37,6 +38,14 @@ }, "description": "Log in op uw Vulcan-account via de registratiepagina voor mobiele apps." }, + "reauth_confirm": { + "data": { + "pin": "Pincode", + "region": "Symbool", + "token": "Token" + }, + "description": "Log in op je Vulcan Account met behulp van de registratie pagina in de mobiele app." + }, "select_saved_credentials": { "data": { "credentials": "Inloggen" diff --git a/homeassistant/components/vulcan/translations/no.json b/homeassistant/components/vulcan/translations/no.json index 7cb8b68fa63..78444957ff1 100644 --- a/homeassistant/components/vulcan/translations/no.json +++ b/homeassistant/components/vulcan/translations/no.json @@ -3,6 +3,7 @@ "abort": { "all_student_already_configured": "Alle elever er allerede lagt til.", "already_configured": "Den studenten er allerede lagt til.", + "no_matching_entries": "Ingen samsvarende oppf\u00f8ringer funnet, vennligst bruk en annen konto eller fjern integrasjon med utdatert student..", "reauth_successful": "Reauth vellykket" }, "error": { @@ -37,6 +38,14 @@ }, "description": "Logg p\u00e5 Vulcan-kontoen din ved \u00e5 bruke registreringssiden for mobilappen." }, + "reauth_confirm": { + "data": { + "pin": "Pin", + "region": "Symbol", + "token": "Token" + }, + "description": "Logg p\u00e5 Vulcan-kontoen din ved \u00e5 bruke registreringssiden for mobilappen." + }, "select_saved_credentials": { "data": { "credentials": "P\u00e5logging" diff --git a/homeassistant/components/vulcan/translations/pl.json b/homeassistant/components/vulcan/translations/pl.json index acbc51c6754..220b05b6972 100644 --- a/homeassistant/components/vulcan/translations/pl.json +++ b/homeassistant/components/vulcan/translations/pl.json @@ -3,6 +3,7 @@ "abort": { "all_student_already_configured": "Wszyscy uczniowie zostali ju\u017c dodani.", "already_configured": "Ten ucze\u0144 zosta\u0142 ju\u017c dodany.", + "no_matching_entries": "Nie znaleziono pasuj\u0105cych wpis\u00f3w. U\u017cyj innego konta lub usu\u0144 integracj\u0119 z nieaktualnym uczniem.", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { @@ -37,6 +38,14 @@ }, "description": "Zaloguj si\u0119 do swojego konta Vulcan za pomoc\u0105 strony rejestracji aplikacji mobilnej." }, + "reauth_confirm": { + "data": { + "pin": "Kod PIN", + "region": "Symbol", + "token": "Token" + }, + "description": "Zaloguj si\u0119 do swojego konta Vulcan za pomoc\u0105 strony rejestracji aplikacji mobilnej." + }, "select_saved_credentials": { "data": { "credentials": "Login" diff --git a/homeassistant/components/vulcan/translations/ru.json b/homeassistant/components/vulcan/translations/ru.json index 88e64d9ca79..5ce94bd932f 100644 --- a/homeassistant/components/vulcan/translations/ru.json +++ b/homeassistant/components/vulcan/translations/ru.json @@ -3,6 +3,7 @@ "abort": { "all_student_already_configured": "\u0412\u0441\u0435 \u0441\u0442\u0443\u0434\u0435\u043d\u0442\u044b \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u044b.", "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0442\u0443\u0434\u0435\u043d\u0442 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d.", + "no_matching_entries": "\u041f\u043e\u0434\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u0440\u0443\u0433\u0443\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c \u0438\u043b\u0438 \u0443\u0434\u0430\u043b\u0438\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0441 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e \u0441\u0442\u0443\u0434\u0435\u043d\u0442\u0430.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { @@ -37,6 +38,14 @@ }, "description": "\u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Vulcan, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f." }, + "reauth_confirm": { + "data": { + "pin": "PIN-\u043a\u043e\u0434", + "region": "\u0421\u0438\u043c\u0432\u043e\u043b", + "token": "\u0422\u043e\u043a\u0435\u043d" + }, + "description": "\u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Vulcan, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f." + }, "select_saved_credentials": { "data": { "credentials": "\u041b\u043e\u0433\u0438\u043d" diff --git a/homeassistant/components/vulcan/translations/zh-Hant.json b/homeassistant/components/vulcan/translations/zh-Hant.json index 3a1c06ce5ad..b26ef1f5cc5 100644 --- a/homeassistant/components/vulcan/translations/zh-Hant.json +++ b/homeassistant/components/vulcan/translations/zh-Hant.json @@ -3,6 +3,7 @@ "abort": { "all_student_already_configured": "\u6240\u6709\u5b78\u751f\u90fd\u5df2\u7d93\u65b0\u589e\u3002", "already_configured": "\u8a72\u5b78\u751f\u5df2\u7d93\u65b0\u589e\u3002", + "no_matching_entries": "\u627e\u4e0d\u5230\u76f8\u7b26\u7684\u5be6\u9ad4\uff0c\u8acb\u4f7f\u7528\u5176\u4ed6\u5e33\u865f\u6216\u79fb\u9664\u5305\u542b\u904e\u671f\u5b78\u751f\u4e4b\u6574\u5408..", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { @@ -37,6 +38,14 @@ }, "description": "\u4f7f\u7528\u884c\u52d5 App \u8a3b\u518a\u9801\u9762\u767b\u5165 Vulcan \u5e33\u865f\u3002" }, + "reauth_confirm": { + "data": { + "pin": "Pin", + "region": "\u7b26\u865f", + "token": "\u6b0a\u6756" + }, + "description": "\u4f7f\u7528\u884c\u52d5 App \u8a3b\u518a\u9801\u9762\u767b\u5165 Vulcan \u5e33\u865f\u3002" + }, "select_saved_credentials": { "data": { "credentials": "\u767b\u5165" From b86f508ac872d832dc79cb4df53ffc493a381e95 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 08:42:49 -0500 Subject: [PATCH 0272/3516] Use DataUpdateCoordinator generic for data (#71479) --- homeassistant/components/history_stats/coordinator.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/history_stats/coordinator.py b/homeassistant/components/history_stats/coordinator.py index 2a2cc392bb5..7d44da9f5f6 100644 --- a/homeassistant/components/history_stats/coordinator.py +++ b/homeassistant/components/history_stats/coordinator.py @@ -19,11 +19,9 @@ _LOGGER = logging.getLogger(__name__) UPDATE_INTERVAL = timedelta(minutes=1) -class HistoryStatsUpdateCoordinator(DataUpdateCoordinator): +class HistoryStatsUpdateCoordinator(DataUpdateCoordinator[HistoryStatsState]): """DataUpdateCoordinator to gather data for a specific TPLink device.""" - data: HistoryStatsState - def __init__( self, hass: HomeAssistant, From 6866cca929b733a35b4884e4ce156a2e88565093 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 7 May 2022 22:08:20 +0200 Subject: [PATCH 0273/3516] Add timeout (#71499) --- homeassistant/components/brother/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index ce715e991b0..96e2ad069ce 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging +import async_timeout from brother import Brother, DictToObj, SnmpError, UnsupportedModel import pysnmp.hlapi.asyncio as SnmpEngine @@ -76,7 +77,8 @@ class BrotherDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> DictToObj: """Update data via library.""" try: - data = await self.brother.async_update() + async with async_timeout.timeout(20): + data = await self.brother.async_update() except (ConnectionError, SnmpError, UnsupportedModel) as error: raise UpdateFailed(error) from error return data From 21cee3b1c4a844d721b3d3cb3a645de62a822f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 7 May 2022 22:13:12 +0200 Subject: [PATCH 0274/3516] airzone: improve diagnostics (#71488) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- .../components/airzone/diagnostics.py | 18 +++- tests/components/airzone/test_diagnostics.py | 82 +++++++++++++------ 2 files changed, 72 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/airzone/diagnostics.py b/homeassistant/components/airzone/diagnostics.py index 8baa106beb1..f56a5106b25 100644 --- a/homeassistant/components/airzone/diagnostics.py +++ b/homeassistant/components/airzone/diagnostics.py @@ -3,16 +3,25 @@ from __future__ import annotations from typing import Any -from aioairzone.const import AZD_MAC +from aioairzone.const import API_MAC, AZD_MAC from homeassistant.components.diagnostics.util import async_redact_data from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_UNIQUE_ID from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import AirzoneUpdateCoordinator -TO_REDACT = [ +TO_REDACT_API = [ + API_MAC, +] + +TO_REDACT_CONFIG = [ + CONF_UNIQUE_ID, +] + +TO_REDACT_COORD = [ AZD_MAC, ] @@ -24,6 +33,7 @@ async def async_get_config_entry_diagnostics( coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] return { - "info": async_redact_data(config_entry.data, TO_REDACT), - "data": async_redact_data(coordinator.data, TO_REDACT), + "api_data": async_redact_data(coordinator.airzone.raw_data(), TO_REDACT_API), + "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT_CONFIG), + "coord_data": async_redact_data(coordinator.data, TO_REDACT_COORD), } diff --git a/tests/components/airzone/test_diagnostics.py b/tests/components/airzone/test_diagnostics.py index 7425677876f..4f7e2f61a48 100644 --- a/tests/components/airzone/test_diagnostics.py +++ b/tests/components/airzone/test_diagnostics.py @@ -1,20 +1,30 @@ """The diagnostics tests for the Airzone platform.""" +from unittest.mock import patch + from aioairzone.const import ( + API_DATA, + API_MAC, + API_SYSTEM_ID, + API_SYSTEMS, + API_WIFI_RSSI, AZD_ID, AZD_MASTER, AZD_SYSTEM, AZD_SYSTEMS, AZD_ZONES, AZD_ZONES_NUM, + RAW_HVAC, + RAW_WEBSERVER, ) from aiohttp import ClientSession from homeassistant.components.airzone.const import DOMAIN +from homeassistant.components.diagnostics.const import REDACTED from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from .util import CONFIG, async_init_integration +from .util import CONFIG, HVAC_MOCK, HVAC_WEBSERVER_MOCK, async_init_integration from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -27,31 +37,55 @@ async def test_config_entry_diagnostics( assert hass.data[DOMAIN] config_entry = hass.config_entries.async_entries(DOMAIN)[0] + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.raw_data", + return_value={ + RAW_HVAC: HVAC_MOCK, + RAW_WEBSERVER: HVAC_WEBSERVER_MOCK, + }, + ): + diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) - diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + assert ( + diag["api_data"][RAW_HVAC][API_SYSTEMS][0][API_DATA][0].items() + >= { + API_SYSTEM_ID: HVAC_MOCK[API_SYSTEMS][0][API_DATA][0][API_SYSTEM_ID], + }.items() + ) - assert diag["info"][CONF_HOST] == CONFIG[CONF_HOST] - assert diag["info"][CONF_PORT] == CONFIG[CONF_PORT] + assert ( + diag["api_data"][RAW_WEBSERVER].items() + >= { + API_MAC: REDACTED, + API_WIFI_RSSI: HVAC_WEBSERVER_MOCK[API_WIFI_RSSI], + }.items() + ) - assert diag["data"][AZD_SYSTEMS]["1"][AZD_ID] == 1 - assert diag["data"][AZD_SYSTEMS]["1"][AZD_ZONES_NUM] == 5 + assert ( + diag["config_entry"].items() + >= { + "data": { + CONF_HOST: CONFIG[CONF_HOST], + CONF_PORT: CONFIG[CONF_PORT], + }, + "domain": DOMAIN, + "unique_id": REDACTED, + }.items() + ) - assert diag["data"][AZD_ZONES]["1:1"][AZD_ID] == 1 - assert diag["data"][AZD_ZONES]["1:1"][AZD_MASTER] == 1 - assert diag["data"][AZD_ZONES]["1:1"][AZD_SYSTEM] == 1 + assert ( + diag["coord_data"][AZD_SYSTEMS]["1"].items() + >= { + AZD_ID: 1, + AZD_ZONES_NUM: 5, + }.items() + ) - assert diag["data"][AZD_ZONES]["1:2"][AZD_ID] == 2 - assert diag["data"][AZD_ZONES]["1:2"][AZD_MASTER] == 0 - assert diag["data"][AZD_ZONES]["1:2"][AZD_SYSTEM] == 1 - - assert diag["data"][AZD_ZONES]["1:3"][AZD_ID] == 3 - assert diag["data"][AZD_ZONES]["1:3"][AZD_MASTER] == 0 - assert diag["data"][AZD_ZONES]["1:3"][AZD_SYSTEM] == 1 - - assert diag["data"][AZD_ZONES]["1:4"][AZD_ID] == 4 - assert diag["data"][AZD_ZONES]["1:4"][AZD_MASTER] == 0 - assert diag["data"][AZD_ZONES]["1:4"][AZD_SYSTEM] == 1 - - assert diag["data"][AZD_ZONES]["1:5"][AZD_ID] == 5 - assert diag["data"][AZD_ZONES]["1:5"][AZD_MASTER] == 0 - assert diag["data"][AZD_ZONES]["1:5"][AZD_SYSTEM] == 1 + assert ( + diag["coord_data"][AZD_ZONES]["1:1"].items() + >= { + AZD_ID: 1, + AZD_MASTER: True, + AZD_SYSTEM: 1, + }.items() + ) From 50f4c5d347a8d6a95f4fa36d1c95228a56ceefe5 Mon Sep 17 00:00:00 2001 From: Shai Ungar Date: Sat, 7 May 2022 23:16:51 +0300 Subject: [PATCH 0275/3516] fix speed sensor wrong number (#71502) --- homeassistant/components/sabnzbd/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index dee80945e9d..539eaa4f097 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -32,13 +32,15 @@ class SabnzbdSensorEntityDescription(SensorEntityDescription, SabnzbdRequiredKey """Describes Sabnzbd sensor entity.""" +SPEED_KEY = "kbpersec" + SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( SabnzbdSensorEntityDescription( key="status", name="Status", ), SabnzbdSensorEntityDescription( - key="kbpersec", + key=SPEED_KEY, name="Speed", native_unit_of_measurement=DATA_RATE_MEGABYTES_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, @@ -154,7 +156,7 @@ class SabnzbdSensor(SensorEntity): self.entity_description.key ) - if self.entity_description.key == "speed": + if self.entity_description.key == SPEED_KEY: self._attr_native_value = round(float(self._attr_native_value) / 1024, 1) elif "size" in self.entity_description.key: self._attr_native_value = round(float(self._attr_native_value), 2) From 6442c5949b132595bdfc02dcba4fd530d093e795 Mon Sep 17 00:00:00 2001 From: rappenze Date: Sat, 7 May 2022 22:20:30 +0200 Subject: [PATCH 0276/3516] Revert usage of Fibaro Client V5 as it has too many errors (#71477) --- homeassistant/components/fibaro/__init__.py | 23 +++++++-------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index d5a8f94970d..c9b0cb345e1 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -10,10 +10,7 @@ from fiblary3.client.v4.client import ( Client as FibaroClientV4, StateHandler as StateHandlerV4, ) -from fiblary3.client.v5.client import ( - Client as FibaroClientV5, - StateHandler as StateHandlerV5, -) +from fiblary3.client.v5.client import StateHandler as StateHandlerV5 from fiblary3.common.exceptions import HTTPException import voluptuous as vol @@ -141,18 +138,12 @@ class FibaroController: should do that only when you use the FibaroController for login test as only the login and info API's are equal throughout the different versions. """ - if ( - serial_number is None - or serial_number.upper().startswith("HC2") - or serial_number.upper().startswith("HCL") - ): - self._client = FibaroClientV4( - config[CONF_URL], config[CONF_USERNAME], config[CONF_PASSWORD] - ) - else: - self._client = FibaroClientV5( - config[CONF_URL], config[CONF_USERNAME], config[CONF_PASSWORD] - ) + + # Only use V4 API as it works better even for HC3, after the library is fixed, we should + # add here support for the newer library version V5 again. + self._client = FibaroClientV4( + config[CONF_URL], config[CONF_USERNAME], config[CONF_PASSWORD] + ) self._scene_map = None # Whether to import devices from plugins From fe7564813afdc0538348e9a47065adf4e7249abc Mon Sep 17 00:00:00 2001 From: rappenze Date: Sat, 7 May 2022 22:22:41 +0200 Subject: [PATCH 0277/3516] Fix rgb conversion in fibaro light (#71476) --- homeassistant/components/fibaro/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index ad41b2aaed6..9d0309bc4ee 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -185,6 +185,6 @@ class FibaroLight(FibaroDevice, LightEntity): rgbw_list = [int(i) for i in rgbw_s.split(",")][:4] if self._attr_color_mode == ColorMode.RGB: - self._attr_rgb_color = tuple(*rgbw_list[:3]) + self._attr_rgb_color = tuple(rgbw_list[:3]) else: self._attr_rgbw_color = tuple(rgbw_list) From b8c76a416b7c570c514a299e7ef39e40b04899d1 Mon Sep 17 00:00:00 2001 From: 0bmay <57501269+0bmay@users.noreply.github.com> Date: Sat, 7 May 2022 13:28:05 -0700 Subject: [PATCH 0278/3516] Update py-canary to 0.5.2 (#71489) Update py-canary from 0.5.1 to 0.5.2 Github issue #71052 Github Issue #44830 --- homeassistant/components/canary/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/canary/manifest.json b/homeassistant/components/canary/manifest.json index 12b4d54b391..fdae5c83d7b 100644 --- a/homeassistant/components/canary/manifest.json +++ b/homeassistant/components/canary/manifest.json @@ -2,7 +2,7 @@ "domain": "canary", "name": "Canary", "documentation": "https://www.home-assistant.io/integrations/canary", - "requirements": ["py-canary==0.5.1"], + "requirements": ["py-canary==0.5.2"], "dependencies": ["ffmpeg"], "codeowners": [], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 48f71c37f93..a32812c3fd4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1290,7 +1290,7 @@ pushover_complete==1.1.1 pvo==0.2.2 # homeassistant.components.canary -py-canary==0.5.1 +py-canary==0.5.2 # homeassistant.components.cpuspeed py-cpuinfo==8.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8adb336e488..ab43ebd64c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -868,7 +868,7 @@ pushbullet.py==0.11.0 pvo==0.2.2 # homeassistant.components.canary -py-canary==0.5.1 +py-canary==0.5.2 # homeassistant.components.cpuspeed py-cpuinfo==8.0.0 From a01444b6dd0e4fb711da2bc32a8bf71b883183fc Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Sat, 7 May 2022 13:30:36 -0700 Subject: [PATCH 0279/3516] bump total_connect_client to 2022.5 (#71493) --- homeassistant/components/totalconnect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index bc62a5b17af..461bff0cfd0 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -2,7 +2,7 @@ "domain": "totalconnect", "name": "Total Connect", "documentation": "https://www.home-assistant.io/integrations/totalconnect", - "requirements": ["total_connect_client==2022.3"], + "requirements": ["total_connect_client==2022.5"], "dependencies": [], "codeowners": ["@austinmroczek"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index a32812c3fd4..dd253665613 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2319,7 +2319,7 @@ tololib==0.1.0b3 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2022.3 +total_connect_client==2022.5 # homeassistant.components.tplink_lte tp-connected==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ab43ebd64c8..510f001e729 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1504,7 +1504,7 @@ tololib==0.1.0b3 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2022.3 +total_connect_client==2022.5 # homeassistant.components.transmission transmissionrpc==0.11 From 523828c81e98e81cd2f9c2b10de7b80cfa1a4086 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 16:18:40 -0500 Subject: [PATCH 0280/3516] Ensure sql sensors keep working after using the options flow (#71453) * Ensure sql sensors keep working after using the options flow Fixes ``` 2022-05-06 16:17:57 ERROR (MainThread) [homeassistant.components.sensor] Error while setting up sql platform for sensor Traceback (most recent call last): File "/Users/bdraco/home-assistant/homeassistant/helpers/entity_platform.py", line 249, in _async_setup_platform await asyncio.shield(task) File "/Users/bdraco/home-assistant/homeassistant/components/sql/sensor.py", line 97, in async_setup_entry name: str = entry.options[CONF_NAME] KeyError: name ``` * ensure saving the options flow fixes the broken config entry * ensure options changes take effect right away * Add cover to validate the reload --- homeassistant/components/sql/__init__.py | 6 +++ homeassistant/components/sql/config_flow.py | 9 +++- tests/components/sql/test_config_flow.py | 55 +++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sql/__init__.py b/homeassistant/components/sql/__init__.py index 3c83b01b284..2917056b5d4 100644 --- a/homeassistant/components/sql/__init__.py +++ b/homeassistant/components/sql/__init__.py @@ -7,8 +7,14 @@ from homeassistant.core import HomeAssistant from .const import PLATFORMS +async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener for options.""" + await hass.config_entries.async_reload(entry.entry_id) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SQL from a config entry.""" + entry.async_on_unload(entry.add_update_listener(async_update_listener)) hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/sql/config_flow.py b/homeassistant/components/sql/config_flow.py index 9150cb8f63d..07ff610087b 100644 --- a/homeassistant/components/sql/config_flow.py +++ b/homeassistant/components/sql/config_flow.py @@ -165,7 +165,14 @@ class SQLOptionsFlowHandler(config_entries.OptionsFlow): except ValueError: errors["query"] = "query_invalid" else: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry( + title="", + data={ + CONF_NAME: self.entry.title, + **self.entry.options, + **user_input, + }, + ) return self.async_show_form( step_id="init", diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index ec851b491b7..3e065df0ebd 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -214,9 +214,62 @@ async def test_options_flow(hass: HomeAssistant) -> None: assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["data"] == { + "name": "Get Value", "db_url": "sqlite://", "query": "SELECT 5 as size", "column": "size", + "value_template": None, + "unit_of_measurement": "MiB", + } + + +async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None: + """Test options config flow where the name was missing.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + "db_url": "sqlite://", + "query": "SELECT 5 as value", + "column": "value", + "unit_of_measurement": "MiB", + "value_template": None, + }, + title="Get Value Title", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + + with patch( + "homeassistant.components.sql.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "db_url": "sqlite://", + "query": "SELECT 5 as size", + "column": "size", + "unit_of_measurement": "MiB", + }, + ) + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 1 + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "name": "Get Value Title", + "db_url": "sqlite://", + "query": "SELECT 5 as size", + "column": "size", + "value_template": None, "unit_of_measurement": "MiB", } @@ -312,6 +365,8 @@ async def test_options_flow_fails_invalid_query( assert result4["type"] == RESULT_TYPE_CREATE_ENTRY assert result4["data"] == { + "name": "Get Value", + "value_template": None, "db_url": "sqlite://", "query": "SELECT 5 as size", "column": "size", From 3883bad70a4d45fd800792d56a77e689741cb0fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 16:19:01 -0500 Subject: [PATCH 0281/3516] Fix display of multiline queries in sql config flow (#71450) --- homeassistant/components/sql/config_flow.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sql/config_flow.py b/homeassistant/components/sql/config_flow.py index 07ff610087b..ba9a5f7e4dd 100644 --- a/homeassistant/components/sql/config_flow.py +++ b/homeassistant/components/sql/config_flow.py @@ -26,7 +26,9 @@ DATA_SCHEMA = vol.Schema( vol.Required(CONF_NAME, default="Select SQL Query"): selector.TextSelector(), vol.Optional(CONF_DB_URL): selector.TextSelector(), vol.Required(CONF_COLUMN_NAME): selector.TextSelector(), - vol.Required(CONF_QUERY): selector.TextSelector(), + vol.Required(CONF_QUERY): selector.TextSelector( + selector.TextSelectorConfig(multiline=True) + ), vol.Optional(CONF_UNIT_OF_MEASUREMENT): selector.TextSelector(), vol.Optional(CONF_VALUE_TEMPLATE): selector.TemplateSelector(), } @@ -187,7 +189,9 @@ class SQLOptionsFlowHandler(config_entries.OptionsFlow): vol.Required( CONF_QUERY, description={"suggested_value": self.entry.options[CONF_QUERY]}, - ): selector.TextSelector(), + ): selector.TextSelector( + selector.TextSelectorConfig(multiline=True) + ), vol.Required( CONF_COLUMN_NAME, description={ From 55cb35046ec3b2e9072d4bebfbd7259ab2938253 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 7 May 2022 14:19:23 -0700 Subject: [PATCH 0282/3516] Move flexit climate to HVAC action (#71443) --- homeassistant/components/flexit/climate.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index c24acf78221..1c9f752c15b 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -6,7 +6,11 @@ import logging import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate.const import ( + ClimateEntityFeature, + HVACAction, + HVACMode, +) from homeassistant.components.modbus import get_hub from homeassistant.components.modbus.const import ( CALL_TYPE_REGISTER_HOLDING, @@ -69,9 +73,7 @@ class Flexit(ClimateEntity): self._target_temperature = None self._current_temperature = None self._current_fan_mode = None - self._current_operation = None self._fan_modes = ["Off", "Low", "Medium", "High"] - self._current_operation = None self._filter_hours = None self._filter_alarm = None self._heat_recovery = None @@ -124,15 +126,15 @@ class Flexit(ClimateEntity): ) if self._heating: - self._current_operation = "Heating" + self._attr_hvac_action = HVACAction.HEATING elif self._cooling: - self._current_operation = "Cooling" + self._attr_hvac_action = HVACAction.COOLING elif self._heat_recovery: - self._current_operation = "Recovering" + self._attr_hvac_action = HVACAction.IDLE elif actual_air_speed: - self._current_operation = "Fan Only" + self._attr_hvac_action = HVACAction.FAN else: - self._current_operation = "Off" + self._attr_hvac_action = HVACAction.OFF @property def extra_state_attributes(self): @@ -175,7 +177,7 @@ class Flexit(ClimateEntity): @property def hvac_mode(self): """Return current operation ie. heat, cool, idle.""" - return self._current_operation + return HVACMode.COOL @property def hvac_modes(self) -> list[str]: From 12065ad58ec710d79eac7eec1c8865d25b21b3e9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 8 May 2022 00:21:41 +0000 Subject: [PATCH 0283/3516] [ci skip] Translation update --- .../components/isy994/translations/he.json | 7 +++++++ .../components/meater/translations/he.json | 5 +++++ .../components/sql/translations/he.json | 16 ++++++++++++++++ .../components/switch_as_x/translations/pl.json | 4 ++-- .../components/vulcan/translations/he.json | 12 ++++++++++++ 5 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/vulcan/translations/he.json diff --git a/homeassistant/components/isy994/translations/he.json b/homeassistant/components/isy994/translations/he.json index b1874d2675d..e72724785cc 100644 --- a/homeassistant/components/isy994/translations/he.json +++ b/homeassistant/components/isy994/translations/he.json @@ -7,10 +7,17 @@ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "invalid_host": "\u05e2\u05e8\u05da \u05d4\u05beHost \u05dc\u05d0 \u05d4\u05d9\u05d4 \u05d1\u05e4\u05d5\u05e8\u05de\u05d8 URL \u05de\u05dc\u05d0, \u05dc\u05de\u05e9\u05dc, http://192.168.10.100:80", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05dc\u05d0 \u05e6\u05e4\u05d5\u05d9\u05d9\u05d4" }, "flow_title": "{name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, "user": { "data": { "host": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", diff --git a/homeassistant/components/meater/translations/he.json b/homeassistant/components/meater/translations/he.json index f1376b2cf0d..59aa348444f 100644 --- a/homeassistant/components/meater/translations/he.json +++ b/homeassistant/components/meater/translations/he.json @@ -5,6 +5,11 @@ "unknown_auth_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + } + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/sql/translations/he.json b/homeassistant/components/sql/translations/he.json index 937391f9327..9b9bda1ed3f 100644 --- a/homeassistant/components/sql/translations/he.json +++ b/homeassistant/components/sql/translations/he.json @@ -2,6 +2,22 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "step": { + "user": { + "data": { + "name": "\u05e9\u05dd" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "\u05e9\u05dd" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/pl.json b/homeassistant/components/switch_as_x/translations/pl.json index 7491ef0d7fc..53a81fec1a5 100644 --- a/homeassistant/components/switch_as_x/translations/pl.json +++ b/homeassistant/components/switch_as_x/translations/pl.json @@ -12,9 +12,9 @@ "target_domain": "Nowy rodzaj" }, "description": "Wybierz prze\u0142\u0105cznik, kt\u00f3ry chcesz pokaza\u0107 w Home Assistant jako \u015bwiat\u0142o, rolet\u0119 lub cokolwiek innego. Oryginalny prze\u0142\u0105cznik zostanie ukryty.", - "title": "Zmiana typu urz\u0105dzenia prze\u0142\u0105czaj\u0105cego" + "title": "Zmiana typu urz\u0105dzenia prze\u0142\u0105cznika" } } }, - "title": "Zmiana typu urz\u0105dzenia w prze\u0142\u0105czniku" + "title": "Zmiana typu urz\u0105dzenia prze\u0142\u0105cznika" } \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/he.json b/homeassistant/components/vulcan/translations/he.json new file mode 100644 index 00000000000..605830b683e --- /dev/null +++ b/homeassistant/components/vulcan/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "region": "\u05e1\u05de\u05dc", + "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df" + } + } + } + } +} \ No newline at end of file From 00291fb1a6bf0a8cfbd71acebbc01298cb337f2d Mon Sep 17 00:00:00 2001 From: screenagerbe <82099983+screenagerbe@users.noreply.github.com> Date: Sun, 8 May 2022 02:41:09 +0200 Subject: [PATCH 0284/3516] update to caldav v0.9.0 (#71406) --- homeassistant/components/caldav/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index 91f563107ed..e6945effca4 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -2,7 +2,7 @@ "domain": "caldav", "name": "CalDAV", "documentation": "https://www.home-assistant.io/integrations/caldav", - "requirements": ["caldav==0.8.2"], + "requirements": ["caldav==0.9.0"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["caldav", "vobject"] diff --git a/requirements_all.txt b/requirements_all.txt index dd253665613..13bb140e1a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -460,7 +460,7 @@ btsmarthub_devicelist==0.2.0 buienradar==1.0.5 # homeassistant.components.caldav -caldav==0.8.2 +caldav==0.9.0 # homeassistant.components.circuit circuit-webhook==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 510f001e729..792d6287e3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -339,7 +339,7 @@ bsblan==0.5.0 buienradar==1.0.5 # homeassistant.components.caldav -caldav==0.8.2 +caldav==0.9.0 # homeassistant.components.co2signal co2signal==0.4.2 From e35a5a1a28bee57b2ea962759c40d9715318d49b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 7 May 2022 20:57:48 -0700 Subject: [PATCH 0285/3516] Fix other enums in helpers (#71505) --- homeassistant/helpers/selector.py | 8 ++++++-- tests/helpers/test_selector.py | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 1ae0082c06f..87574949f4e 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -704,7 +704,9 @@ class SelectSelector(Selector): vol.Required("options"): vol.All(vol.Any([str], [select_option])), vol.Optional("multiple", default=False): cv.boolean, vol.Optional("custom_value", default=False): cv.boolean, - vol.Optional("mode"): vol.Coerce(SelectSelectorMode), + vol.Optional("mode"): vol.All( + vol.Coerce(SelectSelectorMode), lambda val: val.value + ), } ) @@ -827,7 +829,9 @@ class TextSelector(Selector): vol.Optional("suffix"): str, # The "type" controls the input field in the browser, the resulting # data can be any string so we don't validate it. - vol.Optional("type"): vol.Coerce(TextSelectorType), + vol.Optional("type"): vol.All( + vol.Coerce(TextSelectorType), lambda val: val.value + ), } ) diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index ed831026065..4cb924a520e 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -351,7 +351,7 @@ def test_object_selector_schema(schema, valid_selections, invalid_selections): ( ({}, ("abc123",), (None,)), ({"multiline": True}, (), ()), - ({"multiline": False}, (), ()), + ({"multiline": False, "type": "email"}, (), ()), ), ) def test_text_selector_schema(schema, valid_selections, invalid_selections): @@ -402,7 +402,7 @@ def test_text_selector_schema(schema, valid_selections, invalid_selections): (0, None, ["red"]), ), ( - {"options": [], "custom_value": True, "multiple": True}, + {"options": [], "custom_value": True, "multiple": True, "mode": "list"}, (["red"], ["green", "blue"], []), (0, None, "red"), ), From 49d13b9981a2b92ae09362411e3fcd9b87a235c6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 7 May 2022 20:57:57 -0700 Subject: [PATCH 0286/3516] Bump frontend to 20220504.1 (#71504) --- 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 b51219c4f19..8c475d51abb 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220504.0"], + "requirements": ["home-assistant-frontend==20220504.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5c3ef6712b0..129774d989a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220504.0 +home-assistant-frontend==20220504.1 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.1 diff --git a/requirements_all.txt b/requirements_all.txt index 13bb140e1a5..3c915905d18 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220504.0 +home-assistant-frontend==20220504.1 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 792d6287e3e..9a16053f48f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -580,7 +580,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220504.0 +home-assistant-frontend==20220504.1 # homeassistant.components.home_connect homeconnect==0.7.0 From a8aa0e1cca486ce5f8baf8e09b8c9bd24c47cfa1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 23:02:54 -0500 Subject: [PATCH 0287/3516] Add Estimated Database Size to the recorder system health (#71463) --- homeassistant/components/recorder/const.py | 9 +++ homeassistant/components/recorder/core.py | 20 ++++--- .../components/recorder/migration.py | 15 ++--- homeassistant/components/recorder/purge.py | 10 ++-- homeassistant/components/recorder/repack.py | 8 ++- .../components/recorder/statistics.py | 9 ++- .../components/recorder/strings.json | 3 +- .../components/recorder/system_health.py | 26 -------- .../recorder/system_health/__init__.py | 60 +++++++++++++++++++ .../recorder/system_health/mysql.py | 19 ++++++ .../recorder/system_health/postgresql.py | 15 +++++ .../recorder/system_health/sqlite.py | 17 ++++++ .../components/recorder/translations/en.json | 3 +- homeassistant/components/recorder/util.py | 12 ++-- tests/components/recorder/test_init.py | 4 +- tests/components/recorder/test_purge.py | 8 ++- .../components/recorder/test_system_health.py | 29 ++++++++- 17 files changed, 200 insertions(+), 67 deletions(-) delete mode 100644 homeassistant/components/recorder/system_health.py create mode 100644 homeassistant/components/recorder/system_health/__init__.py create mode 100644 homeassistant/components/recorder/system_health/mysql.py create mode 100644 homeassistant/components/recorder/system_health/postgresql.py create mode 100644 homeassistant/components/recorder/system_health/sqlite.py diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index 5b623ee5e04..5d650ec83e2 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -4,6 +4,7 @@ from functools import partial import json from typing import Final +from homeassistant.backports.enum import StrEnum from homeassistant.const import ATTR_ATTRIBUTION, ATTR_RESTORED, ATTR_SUPPORTED_FEATURES from homeassistant.helpers.json import JSONEncoder @@ -37,3 +38,11 @@ KEEPALIVE_TIME = 30 EXCLUDE_ATTRIBUTES = f"{DOMAIN}_exclude_attributes_by_domain" + + +class SupportedDialect(StrEnum): + """Supported dialects.""" + + SQLITE = "sqlite" + MYSQL = "mysql" + POSTGRESQL = "postgresql" diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 49be0aac705..a008ae6767b 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Iterable +import contextlib from datetime import datetime, timedelta import logging import queue @@ -41,6 +42,7 @@ from .const import ( KEEPALIVE_TIME, MAX_QUEUE_BACKLOG, SQLITE_URL_PREFIX, + SupportedDialect, ) from .executor import DBInterruptibleThreadPoolExecutor from .models import ( @@ -193,6 +195,13 @@ class Recorder(threading.Thread): """Return the number of items in the recorder backlog.""" return self._queue.qsize() + @property + def dialect_name(self) -> SupportedDialect | None: + """Return the dialect the recorder uses.""" + with contextlib.suppress(ValueError): + return SupportedDialect(self.engine.dialect.name) if self.engine else None + return None + @property def _using_file_sqlite(self) -> bool: """Short version to check if we are using sqlite3 as a file.""" @@ -459,11 +468,6 @@ class Recorder(threading.Thread): """Schedule external statistics.""" self.queue_task(ExternalStatisticsTask(metadata, stats)) - @callback - def using_sqlite(self) -> bool: - """Return if recorder uses sqlite as the engine.""" - return bool(self.engine and self.engine.dialect.name == "sqlite") - @callback def _async_setup_periodic_tasks(self) -> None: """Prepare periodic tasks.""" @@ -473,7 +477,7 @@ class Recorder(threading.Thread): # If the db is using a socket connection, we need to keep alive # to prevent errors from unexpected disconnects - if not self.using_sqlite(): + if self.dialect_name != SupportedDialect.SQLITE: self._keep_alive_listener = async_track_time_interval( self.hass, self._async_keep_alive, timedelta(seconds=KEEPALIVE_TIME) ) @@ -939,7 +943,7 @@ class Recorder(threading.Thread): async def lock_database(self) -> bool: """Lock database so it can be backed up safely.""" - if not self.using_sqlite(): + if self.dialect_name != SupportedDialect.SQLITE: _LOGGER.debug( "Not a SQLite database or not connected, locking not necessary" ) @@ -968,7 +972,7 @@ class Recorder(threading.Thread): Returns true if database lock has been held throughout the process. """ - if not self.using_sqlite(): + if self.dialect_name != SupportedDialect.SQLITE: _LOGGER.debug( "Not a SQLite database or not connected, unlocking not necessary" ) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index b38bb89b5b9..5da31f18781 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -21,6 +21,7 @@ from sqlalchemy.sql.expression import true from homeassistant.core import HomeAssistant +from .const import SupportedDialect from .models import ( SCHEMA_VERSION, TABLE_STATES, @@ -263,7 +264,7 @@ def _modify_columns( columns_def: list[str], ) -> None: """Modify columns in a table.""" - if engine.dialect.name == "sqlite": + if engine.dialect.name == SupportedDialect.SQLITE: _LOGGER.debug( "Skipping to modify columns %s in table %s; " "Modifying column length in SQLite is unnecessary, " @@ -281,7 +282,7 @@ def _modify_columns( table_name, ) - if engine.dialect.name == "postgresql": + if engine.dialect.name == SupportedDialect.POSTGRESQL: columns_def = [ "ALTER {column} TYPE {type}".format( **dict(zip(["column", "type"], col_def.split(" ", 1))) @@ -408,7 +409,7 @@ def _apply_update( # noqa: C901 ) -> None: """Perform operations to bring schema up to date.""" dialect = engine.dialect.name - big_int = "INTEGER(20)" if dialect == "mysql" else "INTEGER" + big_int = "INTEGER(20)" if dialect == SupportedDialect.MYSQL else "INTEGER" if new_version == 1: _create_index(session_maker, "events", "ix_events_time_fired") @@ -487,11 +488,11 @@ def _apply_update( # noqa: C901 _create_index(session_maker, "states", "ix_states_old_state_id") _update_states_table_with_foreign_key_options(session_maker, engine) elif new_version == 12: - if engine.dialect.name == "mysql": + if engine.dialect.name == SupportedDialect.MYSQL: _modify_columns(session_maker, engine, "events", ["event_data LONGTEXT"]) _modify_columns(session_maker, engine, "states", ["attributes LONGTEXT"]) elif new_version == 13: - if engine.dialect.name == "mysql": + if engine.dialect.name == SupportedDialect.MYSQL: _modify_columns( session_maker, engine, @@ -545,7 +546,7 @@ def _apply_update( # noqa: C901 session.add(StatisticsRuns(start=get_start_time())) elif new_version == 20: # This changed the precision of statistics from float to double - if engine.dialect.name in ["mysql", "postgresql"]: + if engine.dialect.name in [SupportedDialect.MYSQL, SupportedDialect.POSTGRESQL]: _modify_columns( session_maker, engine, @@ -560,7 +561,7 @@ def _apply_update( # noqa: C901 ) elif new_version == 21: # Try to change the character set of the statistic_meta table - if engine.dialect.name == "mysql": + if engine.dialect.name == SupportedDialect.MYSQL: for table in ("events", "states", "statistics_meta"): _LOGGER.warning( "Updating character set and collation of table %s to utf8mb4. " diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index b2547d13e45..2c6211ca2cd 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -13,7 +13,7 @@ from sqlalchemy.sql.expression import distinct from homeassistant.const import EVENT_STATE_CHANGED -from .const import MAX_ROWS_TO_PURGE +from .const import MAX_ROWS_TO_PURGE, SupportedDialect from .models import ( EventData, Events, @@ -45,7 +45,7 @@ def purge_old_data( "Purging states and events before target %s", purge_before.isoformat(sep=" ", timespec="seconds"), ) - using_sqlite = instance.using_sqlite() + using_sqlite = instance.dialect_name == SupportedDialect.SQLITE with session_scope(session=instance.get_session()) as session: # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record @@ -425,7 +425,7 @@ def _purge_old_recorder_runs( def _purge_filtered_data(instance: Recorder, session: Session) -> bool: """Remove filtered states and events that shouldn't be in the database.""" _LOGGER.debug("Cleanup filtered data") - using_sqlite = instance.using_sqlite() + using_sqlite = instance.dialect_name == SupportedDialect.SQLITE # Check if excluded entity_ids are in database excluded_entity_ids: list[str] = [ @@ -484,7 +484,7 @@ def _purge_filtered_events( instance: Recorder, session: Session, excluded_event_types: list[str] ) -> None: """Remove filtered events and linked states.""" - using_sqlite = instance.using_sqlite() + using_sqlite = instance.dialect_name == SupportedDialect.SQLITE event_ids, data_ids = zip( *( session.query(Events.event_id, Events.data_id) @@ -514,7 +514,7 @@ def _purge_filtered_events( @retryable_database_job("purge") def purge_entity_data(instance: Recorder, entity_filter: Callable[[str], bool]) -> bool: """Purge states and events of specified entities.""" - using_sqlite = instance.using_sqlite() + using_sqlite = instance.dialect_name == SupportedDialect.SQLITE with session_scope(session=instance.get_session()) as session: selected_entity_ids: list[str] = [ entity_id diff --git a/homeassistant/components/recorder/repack.py b/homeassistant/components/recorder/repack.py index c272f2827a0..c05b8390724 100644 --- a/homeassistant/components/recorder/repack.py +++ b/homeassistant/components/recorder/repack.py @@ -6,6 +6,8 @@ from typing import TYPE_CHECKING from sqlalchemy import text +from .const import SupportedDialect + if TYPE_CHECKING: from . import Recorder @@ -18,7 +20,7 @@ def repack_database(instance: Recorder) -> None: dialect_name = instance.engine.dialect.name # Execute sqlite command to free up space on disk - if dialect_name == "sqlite": + if dialect_name == SupportedDialect.SQLITE: _LOGGER.debug("Vacuuming SQL DB to free space") with instance.engine.connect() as conn: conn.execute(text("VACUUM")) @@ -26,7 +28,7 @@ def repack_database(instance: Recorder) -> None: return # Execute postgresql vacuum command to free up space on disk - if dialect_name == "postgresql": + if dialect_name == SupportedDialect.POSTGRESQL: _LOGGER.debug("Vacuuming SQL DB to free space") with instance.engine.connect().execution_options( isolation_level="AUTOCOMMIT" @@ -36,7 +38,7 @@ def repack_database(instance: Recorder) -> None: return # Optimize mysql / mariadb tables to free up space on disk - if dialect_name == "mysql": + if dialect_name == SupportedDialect.MYSQL: _LOGGER.debug("Optimizing SQL DB to free space") with instance.engine.connect() as conn: conn.execute(text("OPTIMIZE TABLE states, events, recorder_runs")) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 9104fb7e234..d8ba415cc7a 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -38,7 +38,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 DATA_INSTANCE, DOMAIN, MAX_ROWS_TO_PURGE +from .const import DATA_INSTANCE, DOMAIN, MAX_ROWS_TO_PURGE, SupportedDialect from .models import ( StatisticData, StatisticMetaData, @@ -1342,10 +1342,13 @@ def _filter_unique_constraint_integrity_error( dialect_name = instance.engine.dialect.name ignore = False - if dialect_name == "sqlite" and "UNIQUE constraint failed" in str(err): + if ( + dialect_name == SupportedDialect.SQLITE + and "UNIQUE constraint failed" in str(err) + ): ignore = True if ( - dialect_name == "postgresql" + dialect_name == SupportedDialect.POSTGRESQL and hasattr(err.orig, "pgcode") and err.orig.pgcode == "23505" ): diff --git a/homeassistant/components/recorder/strings.json b/homeassistant/components/recorder/strings.json index 72fcf322c31..b475b29a16a 100644 --- a/homeassistant/components/recorder/strings.json +++ b/homeassistant/components/recorder/strings.json @@ -2,7 +2,8 @@ "system_health": { "info": { "oldest_recorder_run": "Oldest Run Start Time", - "current_recorder_run": "Current Run Start Time" + "current_recorder_run": "Current Run Start Time", + "estimated_db_size": "Estimated Database Size (MiB)" } } } diff --git a/homeassistant/components/recorder/system_health.py b/homeassistant/components/recorder/system_health.py deleted file mode 100644 index 2a9c536a2d6..00000000000 --- a/homeassistant/components/recorder/system_health.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Provide info to system health.""" - -from typing import Any - -from homeassistant.components import system_health -from homeassistant.core import HomeAssistant, callback - -from . import get_instance - - -@callback -def async_register( - hass: HomeAssistant, register: system_health.SystemHealthRegistration -) -> None: - """Register system health callbacks.""" - register.async_register_info(system_health_info) - - -async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: - """Get info for the info page.""" - instance = get_instance(hass) - run_history = instance.run_history - return { - "oldest_recorder_run": run_history.first.start, - "current_recorder_run": run_history.current.start, - } diff --git a/homeassistant/components/recorder/system_health/__init__.py b/homeassistant/components/recorder/system_health/__init__.py new file mode 100644 index 00000000000..3250aecf356 --- /dev/null +++ b/homeassistant/components/recorder/system_health/__init__.py @@ -0,0 +1,60 @@ +"""Provide info to system health.""" +from __future__ import annotations + +from typing import Any + +from yarl import URL + +from homeassistant.components import system_health +from homeassistant.components.recorder.core import Recorder +from homeassistant.components.recorder.util import session_scope +from homeassistant.core import HomeAssistant, callback + +from .. import get_instance +from ..const import SupportedDialect +from .mysql import db_size_bytes as mysql_db_size_bytes +from .postgresql import db_size_bytes as postgresql_db_size_bytes +from .sqlite import db_size_bytes as sqlite_db_size_bytes + +DIALECT_TO_GET_SIZE = { + SupportedDialect.SQLITE: sqlite_db_size_bytes, + SupportedDialect.MYSQL: mysql_db_size_bytes, + SupportedDialect.POSTGRESQL: postgresql_db_size_bytes, +} + + +@callback +def async_register( + hass: HomeAssistant, register: system_health.SystemHealthRegistration +) -> None: + """Register system health callbacks.""" + register.async_register_info(system_health_info) + + +def _get_db_stats(instance: Recorder, database_name: str) -> dict[str, Any]: + """Get the stats about the database.""" + db_stats: dict[str, Any] = {} + with session_scope(session=instance.get_session()) as session: + if ( + (dialect_name := instance.dialect_name) + and (get_size := DIALECT_TO_GET_SIZE.get(dialect_name)) + and (db_bytes := get_size(session, database_name)) + ): + db_stats["estimated_db_size"] = f"{db_bytes/1024/1024:.2f} MiB" + return db_stats + + +async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: + """Get info for the info page.""" + instance = get_instance(hass) + run_history = instance.run_history + database_name = URL(instance.db_url).path.lstrip("/") + db_stats: dict[str, Any] = {} + if instance.async_db_ready.done(): + db_stats = await instance.async_add_executor_job( + _get_db_stats, instance, database_name + ) + return { + "oldest_recorder_run": run_history.first.start, + "current_recorder_run": run_history.current.start, + } | db_stats diff --git a/homeassistant/components/recorder/system_health/mysql.py b/homeassistant/components/recorder/system_health/mysql.py new file mode 100644 index 00000000000..52ea06f61c3 --- /dev/null +++ b/homeassistant/components/recorder/system_health/mysql.py @@ -0,0 +1,19 @@ +"""Provide info to system health for mysql.""" +from __future__ import annotations + +from sqlalchemy import text +from sqlalchemy.orm.session import Session + + +def db_size_bytes(session: Session, database_name: str) -> float: + """Get the mysql database size.""" + return float( + session.execute( + text( + "SELECT ROUND(SUM(DATA_LENGTH + INDEX_LENGTH), 2) " + "FROM information_schema.TABLES WHERE " + "TABLE_SCHEMA=:database_name" + ), + {"database_name": database_name}, + ).first()[0] + ) diff --git a/homeassistant/components/recorder/system_health/postgresql.py b/homeassistant/components/recorder/system_health/postgresql.py new file mode 100644 index 00000000000..3e0667b1f4f --- /dev/null +++ b/homeassistant/components/recorder/system_health/postgresql.py @@ -0,0 +1,15 @@ +"""Provide info to system health for postgresql.""" +from __future__ import annotations + +from sqlalchemy import text +from sqlalchemy.orm.session import Session + + +def db_size_bytes(session: Session, database_name: str) -> float: + """Get the mysql database size.""" + return float( + session.execute( + text("select pg_database_size(:database_name);"), + {"database_name": database_name}, + ).first()[0] + ) diff --git a/homeassistant/components/recorder/system_health/sqlite.py b/homeassistant/components/recorder/system_health/sqlite.py new file mode 100644 index 00000000000..5a5901d2cb3 --- /dev/null +++ b/homeassistant/components/recorder/system_health/sqlite.py @@ -0,0 +1,17 @@ +"""Provide info to system health for sqlite.""" +from __future__ import annotations + +from sqlalchemy import text +from sqlalchemy.orm.session import Session + + +def db_size_bytes(session: Session, database_name: str) -> float: + """Get the mysql database size.""" + return float( + session.execute( + text( + "SELECT page_count * page_size as size " + "FROM pragma_page_count(), pragma_page_size();" + ) + ).first()[0] + ) diff --git a/homeassistant/components/recorder/translations/en.json b/homeassistant/components/recorder/translations/en.json index a44ecd3c1d6..32d78085999 100644 --- a/homeassistant/components/recorder/translations/en.json +++ b/homeassistant/components/recorder/translations/en.json @@ -2,7 +2,8 @@ "system_health": { "info": { "current_recorder_run": "Current Run Start Time", - "oldest_recorder_run": "Oldest Run Start Time" + "oldest_recorder_run": "Oldest Run Start Time", + "estimated_db_size": "Estimated Database Size" } } } \ No newline at end of file diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index b63bbe740bc..3d9fa7d29e1 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -25,7 +25,7 @@ from typing_extensions import Concatenate, ParamSpec from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from .const import DATA_INSTANCE, SQLITE_URL_PREFIX +from .const import DATA_INSTANCE, SQLITE_URL_PREFIX, SupportedDialect from .models import ( ALL_TABLES, TABLE_RECORDER_RUNS, @@ -353,7 +353,7 @@ def setup_connection_for_dialect( # Returns False if the the connection needs to be setup # on the next connection, returns True if the connection # never needs to be setup again. - if dialect_name == "sqlite": + if dialect_name == SupportedDialect.SQLITE: if first_connection: old_isolation = dbapi_connection.isolation_level dbapi_connection.isolation_level = None @@ -381,7 +381,7 @@ def setup_connection_for_dialect( # enable support for foreign keys execute_on_connection(dbapi_connection, "PRAGMA foreign_keys=ON") - elif dialect_name == "mysql": + elif dialect_name == SupportedDialect.MYSQL: execute_on_connection(dbapi_connection, "SET session wait_timeout=28800") if first_connection: result = query_on_connection(dbapi_connection, "SELECT VERSION()") @@ -408,7 +408,7 @@ def setup_connection_for_dialect( version or version_string, "MySQL", MIN_VERSION_MYSQL ) - elif dialect_name == "postgresql": + elif dialect_name == SupportedDialect.POSTGRESQL: if first_connection: # server_version_num was added in 2006 result = query_on_connection(dbapi_connection, "SHOW server_version") @@ -455,7 +455,7 @@ def retryable_database_job( except OperationalError as err: assert instance.engine is not None if ( - instance.engine.dialect.name == "mysql" + instance.engine.dialect.name == SupportedDialect.MYSQL and err.orig.args[0] in RETRYABLE_MYSQL_ERRORS ): _LOGGER.info( @@ -481,7 +481,7 @@ def periodic_db_cleanups(instance: Recorder) -> None: These cleanups will happen nightly or after any purge. """ assert instance.engine is not None - if instance.engine.dialect.name == "sqlite": + if instance.engine.dialect.name == SupportedDialect.SQLITE: # Execute sqlite to create a wal checkpoint and free up disk space _LOGGER.debug("WAL checkpoint") with instance.engine.connect() as connection: diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 17287151bc1..0b2b7b2dcb8 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1440,9 +1440,7 @@ async def test_database_connection_keep_alive( caplog: pytest.LogCaptureFixture, ): """Test we keep alive socket based dialects.""" - with patch( - "homeassistant.components.recorder.Recorder.using_sqlite", return_value=False - ): + with patch("homeassistant.components.recorder.Recorder.dialect_name"): instance = await async_setup_recorder_instance(hass) # We have to mock this since we don't have a mock # MySQL server available in tests. diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 2d711f17cf3..de9db9d5014 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -9,7 +9,7 @@ from sqlalchemy.exc import DatabaseError, OperationalError from sqlalchemy.orm.session import Session from homeassistant.components import recorder -from homeassistant.components.recorder.const import MAX_ROWS_TO_PURGE +from homeassistant.components.recorder.const import MAX_ROWS_TO_PURGE, SupportedDialect from homeassistant.components.recorder.models import ( Events, RecorderRuns, @@ -43,8 +43,10 @@ from tests.common import SetupRecorderInstanceT def mock_use_sqlite(request): """Pytest fixture to switch purge method.""" with patch( - "homeassistant.components.recorder.Recorder.using_sqlite", - return_value=request.param, + "homeassistant.components.recorder.core.Recorder.dialect_name", + return_value=SupportedDialect.SQLITE + if request.param + else SupportedDialect.MYSQL, ): yield diff --git a/tests/components/recorder/test_system_health.py b/tests/components/recorder/test_system_health.py index 75abc2b6ae1..8a8d9ea4e80 100644 --- a/tests/components/recorder/test_system_health.py +++ b/tests/components/recorder/test_system_health.py @@ -1,8 +1,11 @@ """Test recorder system health.""" -from unittest.mock import patch +from unittest.mock import ANY, Mock, patch + +import pytest from homeassistant.components.recorder import get_instance +from homeassistant.components.recorder.const import SupportedDialect from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -20,6 +23,29 @@ async def test_recorder_system_health(hass, recorder_mock): assert info == { "current_recorder_run": instance.run_history.current.start, "oldest_recorder_run": instance.run_history.first.start, + "estimated_db_size": ANY, + } + + +@pytest.mark.parametrize( + "dialect_name", [SupportedDialect.MYSQL, SupportedDialect.POSTGRESQL] +) +async def test_recorder_system_health_alternate_dbms(hass, recorder_mock, dialect_name): + """Test recorder system health.""" + assert await async_setup_component(hass, "system_health", {}) + await async_wait_recording_done(hass) + with patch( + "homeassistant.components.recorder.core.Recorder.dialect_name", dialect_name + ), patch( + "sqlalchemy.orm.session.Session.execute", + return_value=Mock(first=Mock(return_value=("1048576",))), + ): + info = await get_system_health_info(hass, "recorder") + instance = get_instance(hass) + assert info == { + "current_recorder_run": instance.run_history.current.start, + "oldest_recorder_run": instance.run_history.first.start, + "estimated_db_size": "1.00 MiB", } @@ -35,4 +61,5 @@ async def test_recorder_system_health_crashed_recorder_runs_table( assert info == { "current_recorder_run": instance.run_history.current.start, "oldest_recorder_run": instance.run_history.current.start, + "estimated_db_size": ANY, } From d3e2d2eb40d09c816ba2f5438c08658d74bfa476 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 8 May 2022 11:31:03 +0200 Subject: [PATCH 0288/3516] Remove myself from shiftr codeowners (#71517) --- CODEOWNERS | 1 - homeassistant/components/shiftr/manifest.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b8d737cdfb7..e1f9e82551e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -899,7 +899,6 @@ build.json @home-assistant/supervisor /tests/components/shell_command/ @home-assistant/core /homeassistant/components/shelly/ @balloob @bieniu @thecode @chemelli74 /tests/components/shelly/ @balloob @bieniu @thecode @chemelli74 -/homeassistant/components/shiftr/ @fabaff /homeassistant/components/shodan/ @fabaff /homeassistant/components/sia/ @eavanvalkenburg /tests/components/sia/ @eavanvalkenburg diff --git a/homeassistant/components/shiftr/manifest.json b/homeassistant/components/shiftr/manifest.json index e3d27b6b4fc..932571a977e 100644 --- a/homeassistant/components/shiftr/manifest.json +++ b/homeassistant/components/shiftr/manifest.json @@ -3,7 +3,7 @@ "name": "shiftr.io", "documentation": "https://www.home-assistant.io/integrations/shiftr", "requirements": ["paho-mqtt==1.6.1"], - "codeowners": ["@fabaff"], + "codeowners": [], "iot_class": "cloud_push", "loggers": ["paho"] } From 71e84dfcd1539d984d2bf31c85e0b2bc366502b3 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 8 May 2022 11:31:33 +0200 Subject: [PATCH 0289/3516] Remove myself from volkszaehler codeowners (#71515) --- CODEOWNERS | 1 - homeassistant/components/volkszaehler/manifest.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index e1f9e82551e..dcc98dd29cf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1122,7 +1122,6 @@ build.json @home-assistant/supervisor /tests/components/vizio/ @raman325 /homeassistant/components/vlc_telnet/ @rodripf @MartinHjelmare /tests/components/vlc_telnet/ @rodripf @MartinHjelmare -/homeassistant/components/volkszaehler/ @fabaff /homeassistant/components/volumio/ @OnFreund /tests/components/volumio/ @OnFreund /homeassistant/components/volvooncall/ @molobrakos @decompil3d diff --git a/homeassistant/components/volkszaehler/manifest.json b/homeassistant/components/volkszaehler/manifest.json index 5212593da45..baa9e4a8f14 100644 --- a/homeassistant/components/volkszaehler/manifest.json +++ b/homeassistant/components/volkszaehler/manifest.json @@ -3,7 +3,7 @@ "name": "Volkszaehler", "documentation": "https://www.home-assistant.io/integrations/volkszaehler", "requirements": ["volkszaehler==0.3.2"], - "codeowners": ["@fabaff"], + "codeowners": [], "iot_class": "local_polling", "loggers": ["volkszaehler"] } From d52137cc1a1518bba5b07a08f32182ac7ec260b6 Mon Sep 17 00:00:00 2001 From: Jelte Zeilstra Date: Sun, 8 May 2022 15:54:43 +0200 Subject: [PATCH 0290/3516] Add state class measurement to deCONZ LightLevel sensors (#71516) --- homeassistant/components/deconz/sensor.py | 1 + tests/components/deconz/test_sensor.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index d3a20fad522..bf29e8db478 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -160,6 +160,7 @@ ENTITY_DESCRIPTIONS = { else None, update_key="lightlevel", device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=LIGHT_LUX, ) ], diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 590ccee25d5..658e11da906 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -337,7 +337,7 @@ TEST_DATA = [ "state": "5.0", "entity_category": None, "device_class": SensorDeviceClass.ILLUMINANCE, - "state_class": None, + "state_class": SensorStateClass.MEASUREMENT, "attributes": { "on": True, "dark": True, @@ -345,6 +345,7 @@ TEST_DATA = [ "unit_of_measurement": "lx", "device_class": "illuminance", "friendly_name": "Motion sensor 4", + "state_class": "measurement", }, "websocket_event": {"state": {"lightlevel": 1000}}, "next_state": "1.3", From 2eaaa525f4779ca841ec2f5a5ba10ca6d6ac73d9 Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Sun, 8 May 2022 18:22:20 +0300 Subject: [PATCH 0291/3516] Add Ukraine Alarm integration (#71501) Co-authored-by: J. Nick Koston Co-authored-by: Paulus Schoutsen Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen --- .coveragerc | 3 + CODEOWNERS | 2 + .../components/ukraine_alarm/__init__.py | 79 ++++ .../components/ukraine_alarm/binary_sensor.py | 106 ++++++ .../components/ukraine_alarm/config_flow.py | 154 ++++++++ .../components/ukraine_alarm/const.py | 19 + .../components/ukraine_alarm/manifest.json | 9 + .../components/ukraine_alarm/strings.json | 39 ++ .../ukraine_alarm/translations/en.json | 28 ++ .../ukraine_alarm/translations/ru.json | 28 ++ .../ukraine_alarm/translations/uk.json | 28 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/ukraine_alarm/__init__.py | 1 + .../ukraine_alarm/test_config_flow.py | 354 ++++++++++++++++++ 16 files changed, 857 insertions(+) create mode 100644 homeassistant/components/ukraine_alarm/__init__.py create mode 100644 homeassistant/components/ukraine_alarm/binary_sensor.py create mode 100644 homeassistant/components/ukraine_alarm/config_flow.py create mode 100644 homeassistant/components/ukraine_alarm/const.py create mode 100644 homeassistant/components/ukraine_alarm/manifest.json create mode 100644 homeassistant/components/ukraine_alarm/strings.json create mode 100644 homeassistant/components/ukraine_alarm/translations/en.json create mode 100644 homeassistant/components/ukraine_alarm/translations/ru.json create mode 100644 homeassistant/components/ukraine_alarm/translations/uk.json create mode 100644 tests/components/ukraine_alarm/__init__.py create mode 100644 tests/components/ukraine_alarm/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 353d2f07d06..706122d0a07 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1313,6 +1313,9 @@ omit = homeassistant/components/twitter/notify.py homeassistant/components/ubus/device_tracker.py homeassistant/components/ue_smart_radio/media_player.py + homeassistant/components/ukraine_alarm/__init__.py + homeassistant/components/ukraine_alarm/const.py + homeassistant/components/ukraine_alarm/binary_sensor.py homeassistant/components/unifiled/* homeassistant/components/upb/__init__.py homeassistant/components/upb/const.py diff --git a/CODEOWNERS b/CODEOWNERS index dcc98dd29cf..fe5a460e9ee 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1071,6 +1071,8 @@ build.json @home-assistant/supervisor /tests/components/twentemilieu/ @frenck /homeassistant/components/twinkly/ @dr1rrb @Robbie1221 /tests/components/twinkly/ @dr1rrb @Robbie1221 +/homeassistant/components/ukraine_alarm/ @PaulAnnekov +/tests/components/ukraine_alarm/ @PaulAnnekov /homeassistant/components/unifi/ @Kane610 /tests/components/unifi/ @Kane610 /homeassistant/components/unifiled/ @florisvdk diff --git a/homeassistant/components/ukraine_alarm/__init__.py b/homeassistant/components/ukraine_alarm/__init__.py new file mode 100644 index 00000000000..b2b2ff4162f --- /dev/null +++ b/homeassistant/components/ukraine_alarm/__init__.py @@ -0,0 +1,79 @@ +"""The ukraine_alarm component.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Any + +import aiohttp +from aiohttp import ClientSession +from ukrainealarm.client import Client + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_REGION +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ALERT_TYPES, DOMAIN, PLATFORMS + +_LOGGER = logging.getLogger(__name__) + +UPDATE_INTERVAL = timedelta(seconds=10) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Ukraine Alarm as config entry.""" + api_key = entry.data[CONF_API_KEY] + region_id = entry.data[CONF_REGION] + + websession = async_get_clientsession(hass) + + coordinator = UkraineAlarmDataUpdateCoordinator( + hass, websession, api_key, region_id + ) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class UkraineAlarmDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Class to manage fetching Ukraine Alarm API.""" + + def __init__( + self, + hass: HomeAssistant, + session: ClientSession, + api_key: str, + region_id: str, + ) -> None: + """Initialize.""" + self.region_id = region_id + self.ukrainealarm = Client(session, api_key) + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) + + async def _async_update_data(self) -> dict[str, Any]: + """Update data via library.""" + try: + res = await self.ukrainealarm.get_alerts(self.region_id) + except aiohttp.ClientError as error: + raise UpdateFailed(f"Error fetching alerts from API: {error}") from error + + current = {alert_type: False for alert_type in ALERT_TYPES} + for alert in res[0]["activeAlerts"]: + current[alert["type"]] = True + + return current diff --git a/homeassistant/components/ukraine_alarm/binary_sensor.py b/homeassistant/components/ukraine_alarm/binary_sensor.py new file mode 100644 index 00000000000..b98add95e03 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/binary_sensor.py @@ -0,0 +1,106 @@ +"""binary sensors for Ukraine Alarm integration.""" +from __future__ import annotations + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import UkraineAlarmDataUpdateCoordinator +from .const import ( + ALERT_TYPE_AIR, + ALERT_TYPE_ARTILLERY, + ALERT_TYPE_UNKNOWN, + ALERT_TYPE_URBAN_FIGHTS, + ATTRIBUTION, + DOMAIN, + MANUFACTURER, +) + +BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key=ALERT_TYPE_UNKNOWN, + name="Unknown", + device_class=BinarySensorDeviceClass.SAFETY, + ), + BinarySensorEntityDescription( + key=ALERT_TYPE_AIR, + name="Air", + device_class=BinarySensorDeviceClass.SAFETY, + icon="mdi:cloud", + ), + BinarySensorEntityDescription( + key=ALERT_TYPE_URBAN_FIGHTS, + name="Urban Fights", + device_class=BinarySensorDeviceClass.SAFETY, + icon="mdi:pistol", + ), + BinarySensorEntityDescription( + key=ALERT_TYPE_ARTILLERY, + name="Artillery", + device_class=BinarySensorDeviceClass.SAFETY, + icon="mdi:tank", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Ukraine Alarm binary sensor entities based on a config entry.""" + name = config_entry.data[CONF_NAME] + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + UkraineAlarmSensor( + name, + config_entry.unique_id, + description, + coordinator, + ) + for description in BINARY_SENSOR_TYPES + ) + + +class UkraineAlarmSensor( + CoordinatorEntity[UkraineAlarmDataUpdateCoordinator], BinarySensorEntity +): + """Class for a Ukraine Alarm binary sensor.""" + + _attr_attribution = ATTRIBUTION + + def __init__( + self, + name, + unique_id, + description: BinarySensorEntityDescription, + coordinator: UkraineAlarmDataUpdateCoordinator, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + + self.entity_description = description + + self._attr_name = f"{name} {description.name}" + self._attr_unique_id = f"{unique_id}-{description.key}".lower() + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, unique_id)}, + manufacturer=MANUFACTURER, + name=name, + ) + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self.coordinator.data.get(self.entity_description.key, None) diff --git a/homeassistant/components/ukraine_alarm/config_flow.py b/homeassistant/components/ukraine_alarm/config_flow.py new file mode 100644 index 00000000000..dcf41658dfb --- /dev/null +++ b/homeassistant/components/ukraine_alarm/config_flow.py @@ -0,0 +1,154 @@ +"""Config flow for Ukraine Alarm.""" +from __future__ import annotations + +import asyncio + +import aiohttp +from ukrainealarm.client import Client +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_REGION +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + + +class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for Ukraine Alarm.""" + + VERSION = 1 + + def __init__(self): + """Initialize a new UkraineAlarmConfigFlow.""" + self.api_key = None + self.states = None + self.selected_region = None + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + websession = async_get_clientsession(self.hass) + try: + regions = await Client( + websession, user_input[CONF_API_KEY] + ).get_regions() + except aiohttp.ClientResponseError as ex: + errors["base"] = "invalid_api_key" if ex.status == 401 else "unknown" + except aiohttp.ClientConnectionError: + errors["base"] = "cannot_connect" + except aiohttp.ClientError: + errors["base"] = "unknown" + except asyncio.TimeoutError: + errors["base"] = "timeout" + + if not errors and not regions: + errors["base"] = "unknown" + + if not errors: + self.api_key = user_input[CONF_API_KEY] + self.states = regions["states"] + return await self.async_step_state() + + schema = vol.Schema( + { + vol.Required(CONF_API_KEY): str, + } + ) + + return self.async_show_form( + step_id="user", + data_schema=schema, + description_placeholders={"api_url": "https://api.ukrainealarm.com/"}, + errors=errors, + last_step=False, + ) + + async def async_step_state(self, user_input=None): + """Handle user-chosen state.""" + return await self._handle_pick_region("state", "district", user_input) + + async def async_step_district(self, user_input=None): + """Handle user-chosen district.""" + return await self._handle_pick_region("district", "community", user_input) + + async def async_step_community(self, user_input=None): + """Handle user-chosen community.""" + return await self._handle_pick_region("community", None, user_input, True) + + async def _handle_pick_region( + self, step_id: str, next_step: str | None, user_input, last_step=False + ): + """Handle picking a (sub)region.""" + if self.selected_region: + source = self.selected_region["regionChildIds"] + else: + source = self.states + + if user_input is not None: + # Only offer to browse subchildren if picked region wasn't the previously picked one + if ( + not self.selected_region + or user_input[CONF_REGION] != self.selected_region["regionId"] + ): + self.selected_region = _find(source, user_input[CONF_REGION]) + + if next_step and self.selected_region["regionChildIds"]: + return await getattr(self, f"async_step_{next_step}")() + + return await self._async_finish_flow() + + regions = {} + if self.selected_region: + regions[self.selected_region["regionId"]] = self.selected_region[ + "regionName" + ] + + regions.update(_make_regions_object(source)) + + schema = vol.Schema( + { + vol.Required(CONF_REGION): vol.In(regions), + } + ) + + return self.async_show_form( + step_id=step_id, data_schema=schema, last_step=last_step + ) + + async def _async_finish_flow(self): + """Finish the setup.""" + await self.async_set_unique_id(self.selected_region["regionId"]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=self.selected_region["regionName"], + data={ + CONF_API_KEY: self.api_key, + CONF_REGION: self.selected_region["regionId"], + CONF_NAME: self.selected_region["regionName"], + }, + ) + + +def _find(regions, region_id): + return next((region for region in regions if region["regionId"] == region_id), None) + + +def _make_regions_object(regions): + regions_list = [] + for region in regions: + regions_list.append( + { + "id": region["regionId"], + "name": region["regionName"], + } + ) + regions_list = sorted(regions_list, key=lambda region: region["name"].lower()) + regions_object = {} + for region in regions_list: + regions_object[region["id"]] = region["name"] + + return regions_object diff --git a/homeassistant/components/ukraine_alarm/const.py b/homeassistant/components/ukraine_alarm/const.py new file mode 100644 index 00000000000..cc1ae352967 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/const.py @@ -0,0 +1,19 @@ +"""Consts for the Ukraine Alarm.""" +from __future__ import annotations + +from homeassistant.const import Platform + +DOMAIN = "ukraine_alarm" +ATTRIBUTION = "Data provided by Ukraine Alarm" +MANUFACTURER = "Ukraine Alarm" +ALERT_TYPE_UNKNOWN = "UNKNOWN" +ALERT_TYPE_AIR = "AIR" +ALERT_TYPE_ARTILLERY = "ARTILLERY" +ALERT_TYPE_URBAN_FIGHTS = "URBAN_FIGHTS" +ALERT_TYPES = { + ALERT_TYPE_UNKNOWN, + ALERT_TYPE_AIR, + ALERT_TYPE_ARTILLERY, + ALERT_TYPE_URBAN_FIGHTS, +} +PLATFORMS = [Platform.BINARY_SENSOR] diff --git a/homeassistant/components/ukraine_alarm/manifest.json b/homeassistant/components/ukraine_alarm/manifest.json new file mode 100644 index 00000000000..08dad9960b5 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "ukraine_alarm", + "name": "Ukraine Alarm", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ukraine_alarm", + "requirements": ["ukrainealarm==0.0.1"], + "codeowners": ["@PaulAnnekov"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/ukraine_alarm/strings.json b/homeassistant/components/ukraine_alarm/strings.json new file mode 100644 index 00000000000..79f81e71b08 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/strings.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + }, + "error": { + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "timeout": "[%key:common::config_flow::error::timeout_connect%]" + }, + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "description": "Set up the Ukraine Alarm integration. To generate an API key go to {api_url}" + }, + "state": { + "data": { + "region": "Region" + }, + "description": "Choose state to monitor" + }, + "district": { + "data": { + "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" + }, + "description": "If you want to monitor not only state, choose its specific district" + }, + "community": { + "data": { + "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" + }, + "description": "If you want to monitor not only state and district, choose its specific community" + } + } + } +} diff --git a/homeassistant/components/ukraine_alarm/translations/en.json b/homeassistant/components/ukraine_alarm/translations/en.json new file mode 100644 index 00000000000..2c39945cb87 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/en.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "description": "Set up the Ukraine Alarm integration. To generate an API key go to {api_url}", + "title": "Ukraine Alarm" + }, + "state": { + "data": { + "region": "Region" + }, + "description": "Choose state to monitor" + }, + "district": { + "data": { + "region": "Region" + }, + "description": "If you want to monitor not only state, choose its specific district" + }, + "community": { + "data": { + "region": "Region" + }, + "description": "If you want to monitor not only state and district, choose its specific community" + } + } + } +} diff --git a/homeassistant/components/ukraine_alarm/translations/ru.json b/homeassistant/components/ukraine_alarm/translations/ru.json new file mode 100644 index 00000000000..89c9eb1670a --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/ru.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f\u0020\u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438\u0020\u0441 Ukraine Alarm. \u0414\u043b\u044f\u0020\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f\u0020\u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435\u0020\u043d\u0430 {api_url}.", + "title": "Ukraine Alarm" + }, + "state": { + "data": { + "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435\u0020\u043e\u0431\u043b\u0430\u0441\u0442\u044c\u0020\u0434\u043b\u044f\u0020\u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430" + }, + "district": { + "data": { + "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + }, + "description": "\u0415\u0441\u043b\u0438\u0020\u0432\u044b\u0020\u0436\u0435\u043b\u0430\u0435\u0442\u0435\u0020\u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u0442\u044c\u0020\u043d\u0435\u0020\u0442\u043e\u043b\u044c\u043a\u043e\u0020\u043e\u0431\u043b\u0430\u0441\u0442\u044c\u002c\u0020\u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435\u0020\u0435\u0451\u0020\u0440\u0430\u0439\u043e\u043d" + }, + "community": { + "data": { + "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + }, + "description": "\u0415\u0441\u043b\u0438\u0020\u0432\u044b\u0020\u0436\u0435\u043b\u0430\u0435\u0442\u0435\u0020\u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u0442\u044c\u0020\u043d\u0435\u0020\u0442\u043e\u043b\u044c\u043a\u043e\u0020\u043e\u0431\u043b\u0430\u0441\u0442\u044c\u0020\u0438\u0020\u0440\u0430\u0439\u043e\u043d\u002c\u0020\u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435\u0020\u0435\u0451\u0020\u0433\u0440\u043e\u043c\u0430\u0434\u0443" + } + } + } +} diff --git a/homeassistant/components/ukraine_alarm/translations/uk.json b/homeassistant/components/ukraine_alarm/translations/uk.json new file mode 100644 index 00000000000..2eed983f34f --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/uk.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f\u0020\u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457\u0020\u0437 Ukraine Alarm. \u0414\u043b\u044f\u0020\u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f\u0020\u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c\u0020\u043d\u0430 {api_url}.", + "title": "Ukraine Alarm" + }, + "state": { + "data": { + "region": "\u0420\u0435\u0433\u0456\u043e\u043d" + }, + "description": "\u041e\u0431\u0435\u0440\u0456\u0442\u044c\u0020\u043e\u0431\u043b\u0430\u0441\u0442\u044c\u0020\u0434\u043b\u044f\u0020\u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443" + }, + "district": { + "data": { + "region": "\u0420\u0435\u0433\u0456\u043e\u043d" + }, + "description": "\u042f\u043a\u0449\u043e\u0020\u0432\u0438\u0020\u0431\u0430\u0436\u0430\u0454\u0442\u0435\u0020\u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u0442\u0438\u0020\u043d\u0435\u0020\u043b\u0438\u0448\u0435\u0020\u043e\u0431\u043b\u0430\u0441\u0442\u044c\u002c\u0020\u043e\u0431\u0435\u0440\u0456\u0442\u044c\u0020\u0457\u0457\u0020\u0440\u0430\u0439\u043e\u043d" + }, + "community": { + "data": { + "region": "\u0420\u0435\u0433\u0456\u043e\u043d" + }, + "description": "\u042f\u043a\u0449\u043e\u0020\u0432\u0438\u0020\u0431\u0430\u0436\u0430\u0454\u0442\u0435\u0020\u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u0442\u0438\u0020\u043d\u0435\u0020\u0442\u0456\u043b\u044c\u043a\u0438\u0020\u043e\u0431\u043b\u0430\u0441\u0442\u044c\u0020\u0442\u0430\u0020\u0440\u0430\u0439\u043e\u043d\u002c\u0020\u043e\u0431\u0435\u0440\u0456\u0442\u044c\u0020\u0457\u0457\u0020\u0433\u0440\u043e\u043c\u0430\u0434\u0443" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 710d97f3c34..510adc74e61 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -366,6 +366,7 @@ FLOWS = { "twentemilieu", "twilio", "twinkly", + "ukraine_alarm", "unifi", "unifiprotect", "upb", diff --git a/requirements_all.txt b/requirements_all.txt index 3c915905d18..09a6a61bd11 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2345,6 +2345,9 @@ twitchAPI==2.5.2 # homeassistant.components.rainforest_eagle uEagle==0.0.2 +# homeassistant.components.ukraine_alarm +ukrainealarm==0.0.1 + # homeassistant.components.unifiprotect unifi-discovery==1.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a16053f48f..46daf2c2744 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1527,6 +1527,9 @@ twitchAPI==2.5.2 # homeassistant.components.rainforest_eagle uEagle==0.0.2 +# homeassistant.components.ukraine_alarm +ukrainealarm==0.0.1 + # homeassistant.components.unifiprotect unifi-discovery==1.1.2 diff --git a/tests/components/ukraine_alarm/__init__.py b/tests/components/ukraine_alarm/__init__.py new file mode 100644 index 00000000000..228594b3d0c --- /dev/null +++ b/tests/components/ukraine_alarm/__init__.py @@ -0,0 +1 @@ +"""Tests for the Ukraine Alarm integration.""" diff --git a/tests/components/ukraine_alarm/test_config_flow.py b/tests/components/ukraine_alarm/test_config_flow.py new file mode 100644 index 00000000000..3832e6a9fb6 --- /dev/null +++ b/tests/components/ukraine_alarm/test_config_flow.py @@ -0,0 +1,354 @@ +"""Test the Ukraine Alarm config flow.""" +import asyncio +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +from aiohttp import ClientConnectionError, ClientError, ClientResponseError +import pytest + +from homeassistant import config_entries +from homeassistant.components.ukraine_alarm.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + +MOCK_API_KEY = "mock-api-key" + + +def _region(rid, recurse=0, depth=0): + if depth == 0: + name_prefix = "State" + elif depth == 1: + name_prefix = "District" + else: + name_prefix = "Community" + + name = f"{name_prefix} {rid}" + region = {"regionId": rid, "regionName": name, "regionChildIds": []} + + if not recurse: + return region + + for i in range(1, 4): + region["regionChildIds"].append(_region(f"{rid}.{i}", recurse - 1, depth + 1)) + + return region + + +REGIONS = { + "states": [_region(f"{i}", i - 1) for i in range(1, 4)], +} + + +@pytest.fixture(autouse=True) +def mock_get_regions() -> Generator[None, AsyncMock, None]: + """Mock the get_regions method.""" + + with patch( + "homeassistant.components.ukraine_alarm.config_flow.Client.get_regions", + return_value=REGIONS, + ) as mock_get: + yield mock_get + + +async def test_state(hass: HomeAssistant) -> None: + """Test we can create entry for state.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + with patch( + "homeassistant.components.ukraine_alarm.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "region": "1", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "State 1" + assert result3["data"] == { + "api_key": MOCK_API_KEY, + "region": "1", + "name": result3["title"], + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_state_district(hass: HomeAssistant) -> None: + """Test we can create entry for state + district.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "region": "2", + }, + ) + assert result3["type"] == RESULT_TYPE_FORM + + with patch( + "homeassistant.components.ukraine_alarm.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "region": "2.2", + }, + ) + await hass.async_block_till_done() + + assert result4["type"] == RESULT_TYPE_CREATE_ENTRY + assert result4["title"] == "District 2.2" + assert result4["data"] == { + "api_key": MOCK_API_KEY, + "region": "2.2", + "name": result4["title"], + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_state_district_pick_region(hass: HomeAssistant) -> None: + """Test we can create entry for region which has districts.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "region": "2", + }, + ) + assert result3["type"] == RESULT_TYPE_FORM + + with patch( + "homeassistant.components.ukraine_alarm.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "region": "2", + }, + ) + await hass.async_block_till_done() + + assert result4["type"] == RESULT_TYPE_CREATE_ENTRY + assert result4["title"] == "State 2" + assert result4["data"] == { + "api_key": MOCK_API_KEY, + "region": "2", + "name": result4["title"], + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_state_district_community(hass: HomeAssistant) -> None: + """Test we can create entry for state + district + community.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "region": "3", + }, + ) + assert result3["type"] == RESULT_TYPE_FORM + + result4 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "region": "3.2", + }, + ) + assert result4["type"] == RESULT_TYPE_FORM + + with patch( + "homeassistant.components.ukraine_alarm.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result5 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "region": "3.2.1", + }, + ) + await hass.async_block_till_done() + + assert result5["type"] == RESULT_TYPE_CREATE_ENTRY + assert result5["title"] == "Community 3.2.1" + assert result5["data"] == { + "api_key": MOCK_API_KEY, + "region": "3.2.1", + "name": result5["title"], + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_invalid_api(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: + """Test we can create entry for just region.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + mock_get_regions.side_effect = ClientResponseError(None, None, status=401) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "invalid_api_key"} + + +async def test_server_error(hass: HomeAssistant, mock_get_regions) -> None: + """Test we can create entry for just region.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + mock_get_regions.side_effect = ClientResponseError(None, None, status=500) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "unknown"} + + +async def test_cannot_connect(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: + """Test we can create entry for just region.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + mock_get_regions.side_effect = ClientConnectionError + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_unknown_client_error( + hass: HomeAssistant, mock_get_regions: AsyncMock +) -> None: + """Test we can create entry for just region.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + mock_get_regions.side_effect = ClientError + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "unknown"} + + +async def test_timeout_error(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: + """Test we can create entry for just region.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + mock_get_regions.side_effect = asyncio.TimeoutError + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "timeout"} + + +async def test_no_regions_returned( + hass: HomeAssistant, mock_get_regions: AsyncMock +) -> None: + """Test we can create entry for just region.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + mock_get_regions.return_value = {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "unknown"} From 6abc51b363f9cef2bfd21249b78e905e6b3b0cc7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 7 May 2022 14:19:23 -0700 Subject: [PATCH 0292/3516] Move flexit climate to HVAC action (#71443) --- homeassistant/components/flexit/climate.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index c24acf78221..1c9f752c15b 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -6,7 +6,11 @@ import logging import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity -from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode +from homeassistant.components.climate.const import ( + ClimateEntityFeature, + HVACAction, + HVACMode, +) from homeassistant.components.modbus import get_hub from homeassistant.components.modbus.const import ( CALL_TYPE_REGISTER_HOLDING, @@ -69,9 +73,7 @@ class Flexit(ClimateEntity): self._target_temperature = None self._current_temperature = None self._current_fan_mode = None - self._current_operation = None self._fan_modes = ["Off", "Low", "Medium", "High"] - self._current_operation = None self._filter_hours = None self._filter_alarm = None self._heat_recovery = None @@ -124,15 +126,15 @@ class Flexit(ClimateEntity): ) if self._heating: - self._current_operation = "Heating" + self._attr_hvac_action = HVACAction.HEATING elif self._cooling: - self._current_operation = "Cooling" + self._attr_hvac_action = HVACAction.COOLING elif self._heat_recovery: - self._current_operation = "Recovering" + self._attr_hvac_action = HVACAction.IDLE elif actual_air_speed: - self._current_operation = "Fan Only" + self._attr_hvac_action = HVACAction.FAN else: - self._current_operation = "Off" + self._attr_hvac_action = HVACAction.OFF @property def extra_state_attributes(self): @@ -175,7 +177,7 @@ class Flexit(ClimateEntity): @property def hvac_mode(self): """Return current operation ie. heat, cool, idle.""" - return self._current_operation + return HVACMode.COOL @property def hvac_modes(self) -> list[str]: From b18d64fdac38732e26bf15aed94e3885d232112b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 16:19:01 -0500 Subject: [PATCH 0293/3516] Fix display of multiline queries in sql config flow (#71450) --- homeassistant/components/sql/config_flow.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sql/config_flow.py b/homeassistant/components/sql/config_flow.py index 9150cb8f63d..b18ea406618 100644 --- a/homeassistant/components/sql/config_flow.py +++ b/homeassistant/components/sql/config_flow.py @@ -26,7 +26,9 @@ DATA_SCHEMA = vol.Schema( vol.Required(CONF_NAME, default="Select SQL Query"): selector.TextSelector(), vol.Optional(CONF_DB_URL): selector.TextSelector(), vol.Required(CONF_COLUMN_NAME): selector.TextSelector(), - vol.Required(CONF_QUERY): selector.TextSelector(), + vol.Required(CONF_QUERY): selector.TextSelector( + selector.TextSelectorConfig(multiline=True) + ), vol.Optional(CONF_UNIT_OF_MEASUREMENT): selector.TextSelector(), vol.Optional(CONF_VALUE_TEMPLATE): selector.TemplateSelector(), } @@ -180,7 +182,9 @@ class SQLOptionsFlowHandler(config_entries.OptionsFlow): vol.Required( CONF_QUERY, description={"suggested_value": self.entry.options[CONF_QUERY]}, - ): selector.TextSelector(), + ): selector.TextSelector( + selector.TextSelectorConfig(multiline=True) + ), vol.Required( CONF_COLUMN_NAME, description={ From b2721d659676859e6a7207c930f444eb3cf4e208 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 May 2022 16:18:40 -0500 Subject: [PATCH 0294/3516] Ensure sql sensors keep working after using the options flow (#71453) * Ensure sql sensors keep working after using the options flow Fixes ``` 2022-05-06 16:17:57 ERROR (MainThread) [homeassistant.components.sensor] Error while setting up sql platform for sensor Traceback (most recent call last): File "/Users/bdraco/home-assistant/homeassistant/helpers/entity_platform.py", line 249, in _async_setup_platform await asyncio.shield(task) File "/Users/bdraco/home-assistant/homeassistant/components/sql/sensor.py", line 97, in async_setup_entry name: str = entry.options[CONF_NAME] KeyError: name ``` * ensure saving the options flow fixes the broken config entry * ensure options changes take effect right away * Add cover to validate the reload --- homeassistant/components/sql/__init__.py | 6 +++ homeassistant/components/sql/config_flow.py | 9 +++- tests/components/sql/test_config_flow.py | 55 +++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sql/__init__.py b/homeassistant/components/sql/__init__.py index 3c83b01b284..2917056b5d4 100644 --- a/homeassistant/components/sql/__init__.py +++ b/homeassistant/components/sql/__init__.py @@ -7,8 +7,14 @@ from homeassistant.core import HomeAssistant from .const import PLATFORMS +async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener for options.""" + await hass.config_entries.async_reload(entry.entry_id) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SQL from a config entry.""" + entry.async_on_unload(entry.add_update_listener(async_update_listener)) hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/sql/config_flow.py b/homeassistant/components/sql/config_flow.py index b18ea406618..ba9a5f7e4dd 100644 --- a/homeassistant/components/sql/config_flow.py +++ b/homeassistant/components/sql/config_flow.py @@ -167,7 +167,14 @@ class SQLOptionsFlowHandler(config_entries.OptionsFlow): except ValueError: errors["query"] = "query_invalid" else: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry( + title="", + data={ + CONF_NAME: self.entry.title, + **self.entry.options, + **user_input, + }, + ) return self.async_show_form( step_id="init", diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index ec851b491b7..3e065df0ebd 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -214,9 +214,62 @@ async def test_options_flow(hass: HomeAssistant) -> None: assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["data"] == { + "name": "Get Value", "db_url": "sqlite://", "query": "SELECT 5 as size", "column": "size", + "value_template": None, + "unit_of_measurement": "MiB", + } + + +async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None: + """Test options config flow where the name was missing.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + "db_url": "sqlite://", + "query": "SELECT 5 as value", + "column": "value", + "unit_of_measurement": "MiB", + "value_template": None, + }, + title="Get Value Title", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + + with patch( + "homeassistant.components.sql.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "db_url": "sqlite://", + "query": "SELECT 5 as size", + "column": "size", + "unit_of_measurement": "MiB", + }, + ) + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 1 + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "name": "Get Value Title", + "db_url": "sqlite://", + "query": "SELECT 5 as size", + "column": "size", + "value_template": None, "unit_of_measurement": "MiB", } @@ -312,6 +365,8 @@ async def test_options_flow_fails_invalid_query( assert result4["type"] == RESULT_TYPE_CREATE_ENTRY assert result4["data"] == { + "name": "Get Value", + "value_template": None, "db_url": "sqlite://", "query": "SELECT 5 as size", "column": "size", From 5b4764351dc614c478d59282c70ecb25bcc5cf38 Mon Sep 17 00:00:00 2001 From: rappenze Date: Sat, 7 May 2022 22:22:41 +0200 Subject: [PATCH 0295/3516] Fix rgb conversion in fibaro light (#71476) --- homeassistant/components/fibaro/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index ad41b2aaed6..9d0309bc4ee 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -185,6 +185,6 @@ class FibaroLight(FibaroDevice, LightEntity): rgbw_list = [int(i) for i in rgbw_s.split(",")][:4] if self._attr_color_mode == ColorMode.RGB: - self._attr_rgb_color = tuple(*rgbw_list[:3]) + self._attr_rgb_color = tuple(rgbw_list[:3]) else: self._attr_rgbw_color = tuple(rgbw_list) From 34cbf26e2fed42102eb9dbc7d720da947dd24de3 Mon Sep 17 00:00:00 2001 From: rappenze Date: Sat, 7 May 2022 22:20:30 +0200 Subject: [PATCH 0296/3516] Revert usage of Fibaro Client V5 as it has too many errors (#71477) --- homeassistant/components/fibaro/__init__.py | 23 +++++++-------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index d5a8f94970d..c9b0cb345e1 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -10,10 +10,7 @@ from fiblary3.client.v4.client import ( Client as FibaroClientV4, StateHandler as StateHandlerV4, ) -from fiblary3.client.v5.client import ( - Client as FibaroClientV5, - StateHandler as StateHandlerV5, -) +from fiblary3.client.v5.client import StateHandler as StateHandlerV5 from fiblary3.common.exceptions import HTTPException import voluptuous as vol @@ -141,18 +138,12 @@ class FibaroController: should do that only when you use the FibaroController for login test as only the login and info API's are equal throughout the different versions. """ - if ( - serial_number is None - or serial_number.upper().startswith("HC2") - or serial_number.upper().startswith("HCL") - ): - self._client = FibaroClientV4( - config[CONF_URL], config[CONF_USERNAME], config[CONF_PASSWORD] - ) - else: - self._client = FibaroClientV5( - config[CONF_URL], config[CONF_USERNAME], config[CONF_PASSWORD] - ) + + # Only use V4 API as it works better even for HC3, after the library is fixed, we should + # add here support for the newer library version V5 again. + self._client = FibaroClientV4( + config[CONF_URL], config[CONF_USERNAME], config[CONF_PASSWORD] + ) self._scene_map = None # Whether to import devices from plugins From fccad81227dcb5d9b6a353ab44dc750bada09704 Mon Sep 17 00:00:00 2001 From: 0bmay <57501269+0bmay@users.noreply.github.com> Date: Sat, 7 May 2022 13:28:05 -0700 Subject: [PATCH 0297/3516] Update py-canary to 0.5.2 (#71489) Update py-canary from 0.5.1 to 0.5.2 Github issue #71052 Github Issue #44830 --- homeassistant/components/canary/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/canary/manifest.json b/homeassistant/components/canary/manifest.json index 12b4d54b391..fdae5c83d7b 100644 --- a/homeassistant/components/canary/manifest.json +++ b/homeassistant/components/canary/manifest.json @@ -2,7 +2,7 @@ "domain": "canary", "name": "Canary", "documentation": "https://www.home-assistant.io/integrations/canary", - "requirements": ["py-canary==0.5.1"], + "requirements": ["py-canary==0.5.2"], "dependencies": ["ffmpeg"], "codeowners": [], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index f5c09da8eeb..c7820675fff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1287,7 +1287,7 @@ pushover_complete==1.1.1 pvo==0.2.2 # homeassistant.components.canary -py-canary==0.5.1 +py-canary==0.5.2 # homeassistant.components.cpuspeed py-cpuinfo==8.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 302e90b5648..45f81154a6d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -865,7 +865,7 @@ pushbullet.py==0.11.0 pvo==0.2.2 # homeassistant.components.canary -py-canary==0.5.1 +py-canary==0.5.2 # homeassistant.components.cpuspeed py-cpuinfo==8.0.0 From f817caa7fc15fe597ae0eb658913d3c9221a17a7 Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Sat, 7 May 2022 13:30:36 -0700 Subject: [PATCH 0298/3516] bump total_connect_client to 2022.5 (#71493) --- homeassistant/components/totalconnect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index bc62a5b17af..461bff0cfd0 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -2,7 +2,7 @@ "domain": "totalconnect", "name": "Total Connect", "documentation": "https://www.home-assistant.io/integrations/totalconnect", - "requirements": ["total_connect_client==2022.3"], + "requirements": ["total_connect_client==2022.5"], "dependencies": [], "codeowners": ["@austinmroczek"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index c7820675fff..fd1224ee1c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2316,7 +2316,7 @@ tololib==0.1.0b3 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2022.3 +total_connect_client==2022.5 # homeassistant.components.tplink_lte tp-connected==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45f81154a6d..9a7f419419c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1501,7 +1501,7 @@ tololib==0.1.0b3 toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2022.3 +total_connect_client==2022.5 # homeassistant.components.transmission transmissionrpc==0.11 From 51ba02f14171e875065eee9fec3db146b0c35da5 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 7 May 2022 22:08:20 +0200 Subject: [PATCH 0299/3516] Add timeout (#71499) --- homeassistant/components/brother/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index ce715e991b0..96e2ad069ce 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging +import async_timeout from brother import Brother, DictToObj, SnmpError, UnsupportedModel import pysnmp.hlapi.asyncio as SnmpEngine @@ -76,7 +77,8 @@ class BrotherDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> DictToObj: """Update data via library.""" try: - data = await self.brother.async_update() + async with async_timeout.timeout(20): + data = await self.brother.async_update() except (ConnectionError, SnmpError, UnsupportedModel) as error: raise UpdateFailed(error) from error return data From 9f1d996d9547d8adaaffcda265d8b28ffcaeb829 Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Sun, 8 May 2022 18:22:20 +0300 Subject: [PATCH 0300/3516] Add Ukraine Alarm integration (#71501) Co-authored-by: J. Nick Koston Co-authored-by: Paulus Schoutsen Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen --- .coveragerc | 3 + CODEOWNERS | 2 + .../components/ukraine_alarm/__init__.py | 79 ++++ .../components/ukraine_alarm/binary_sensor.py | 106 ++++++ .../components/ukraine_alarm/config_flow.py | 154 ++++++++ .../components/ukraine_alarm/const.py | 19 + .../components/ukraine_alarm/manifest.json | 9 + .../components/ukraine_alarm/strings.json | 39 ++ .../ukraine_alarm/translations/en.json | 28 ++ .../ukraine_alarm/translations/ru.json | 28 ++ .../ukraine_alarm/translations/uk.json | 28 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/ukraine_alarm/__init__.py | 1 + .../ukraine_alarm/test_config_flow.py | 354 ++++++++++++++++++ 16 files changed, 857 insertions(+) create mode 100644 homeassistant/components/ukraine_alarm/__init__.py create mode 100644 homeassistant/components/ukraine_alarm/binary_sensor.py create mode 100644 homeassistant/components/ukraine_alarm/config_flow.py create mode 100644 homeassistant/components/ukraine_alarm/const.py create mode 100644 homeassistant/components/ukraine_alarm/manifest.json create mode 100644 homeassistant/components/ukraine_alarm/strings.json create mode 100644 homeassistant/components/ukraine_alarm/translations/en.json create mode 100644 homeassistant/components/ukraine_alarm/translations/ru.json create mode 100644 homeassistant/components/ukraine_alarm/translations/uk.json create mode 100644 tests/components/ukraine_alarm/__init__.py create mode 100644 tests/components/ukraine_alarm/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index b7433ecf58a..dc7143aa867 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1320,6 +1320,9 @@ omit = homeassistant/components/twitter/notify.py homeassistant/components/ubus/device_tracker.py homeassistant/components/ue_smart_radio/media_player.py + homeassistant/components/ukraine_alarm/__init__.py + homeassistant/components/ukraine_alarm/const.py + homeassistant/components/ukraine_alarm/binary_sensor.py homeassistant/components/unifiled/* homeassistant/components/upb/__init__.py homeassistant/components/upb/const.py diff --git a/CODEOWNERS b/CODEOWNERS index c3405001f23..8731f2860ce 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1070,6 +1070,8 @@ build.json @home-assistant/supervisor /tests/components/twentemilieu/ @frenck /homeassistant/components/twinkly/ @dr1rrb @Robbie1221 /tests/components/twinkly/ @dr1rrb @Robbie1221 +/homeassistant/components/ukraine_alarm/ @PaulAnnekov +/tests/components/ukraine_alarm/ @PaulAnnekov /homeassistant/components/unifi/ @Kane610 /tests/components/unifi/ @Kane610 /homeassistant/components/unifiled/ @florisvdk diff --git a/homeassistant/components/ukraine_alarm/__init__.py b/homeassistant/components/ukraine_alarm/__init__.py new file mode 100644 index 00000000000..b2b2ff4162f --- /dev/null +++ b/homeassistant/components/ukraine_alarm/__init__.py @@ -0,0 +1,79 @@ +"""The ukraine_alarm component.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Any + +import aiohttp +from aiohttp import ClientSession +from ukrainealarm.client import Client + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_REGION +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ALERT_TYPES, DOMAIN, PLATFORMS + +_LOGGER = logging.getLogger(__name__) + +UPDATE_INTERVAL = timedelta(seconds=10) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Ukraine Alarm as config entry.""" + api_key = entry.data[CONF_API_KEY] + region_id = entry.data[CONF_REGION] + + websession = async_get_clientsession(hass) + + coordinator = UkraineAlarmDataUpdateCoordinator( + hass, websession, api_key, region_id + ) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class UkraineAlarmDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Class to manage fetching Ukraine Alarm API.""" + + def __init__( + self, + hass: HomeAssistant, + session: ClientSession, + api_key: str, + region_id: str, + ) -> None: + """Initialize.""" + self.region_id = region_id + self.ukrainealarm = Client(session, api_key) + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) + + async def _async_update_data(self) -> dict[str, Any]: + """Update data via library.""" + try: + res = await self.ukrainealarm.get_alerts(self.region_id) + except aiohttp.ClientError as error: + raise UpdateFailed(f"Error fetching alerts from API: {error}") from error + + current = {alert_type: False for alert_type in ALERT_TYPES} + for alert in res[0]["activeAlerts"]: + current[alert["type"]] = True + + return current diff --git a/homeassistant/components/ukraine_alarm/binary_sensor.py b/homeassistant/components/ukraine_alarm/binary_sensor.py new file mode 100644 index 00000000000..b98add95e03 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/binary_sensor.py @@ -0,0 +1,106 @@ +"""binary sensors for Ukraine Alarm integration.""" +from __future__ import annotations + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import UkraineAlarmDataUpdateCoordinator +from .const import ( + ALERT_TYPE_AIR, + ALERT_TYPE_ARTILLERY, + ALERT_TYPE_UNKNOWN, + ALERT_TYPE_URBAN_FIGHTS, + ATTRIBUTION, + DOMAIN, + MANUFACTURER, +) + +BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key=ALERT_TYPE_UNKNOWN, + name="Unknown", + device_class=BinarySensorDeviceClass.SAFETY, + ), + BinarySensorEntityDescription( + key=ALERT_TYPE_AIR, + name="Air", + device_class=BinarySensorDeviceClass.SAFETY, + icon="mdi:cloud", + ), + BinarySensorEntityDescription( + key=ALERT_TYPE_URBAN_FIGHTS, + name="Urban Fights", + device_class=BinarySensorDeviceClass.SAFETY, + icon="mdi:pistol", + ), + BinarySensorEntityDescription( + key=ALERT_TYPE_ARTILLERY, + name="Artillery", + device_class=BinarySensorDeviceClass.SAFETY, + icon="mdi:tank", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Ukraine Alarm binary sensor entities based on a config entry.""" + name = config_entry.data[CONF_NAME] + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + UkraineAlarmSensor( + name, + config_entry.unique_id, + description, + coordinator, + ) + for description in BINARY_SENSOR_TYPES + ) + + +class UkraineAlarmSensor( + CoordinatorEntity[UkraineAlarmDataUpdateCoordinator], BinarySensorEntity +): + """Class for a Ukraine Alarm binary sensor.""" + + _attr_attribution = ATTRIBUTION + + def __init__( + self, + name, + unique_id, + description: BinarySensorEntityDescription, + coordinator: UkraineAlarmDataUpdateCoordinator, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + + self.entity_description = description + + self._attr_name = f"{name} {description.name}" + self._attr_unique_id = f"{unique_id}-{description.key}".lower() + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, unique_id)}, + manufacturer=MANUFACTURER, + name=name, + ) + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self.coordinator.data.get(self.entity_description.key, None) diff --git a/homeassistant/components/ukraine_alarm/config_flow.py b/homeassistant/components/ukraine_alarm/config_flow.py new file mode 100644 index 00000000000..dcf41658dfb --- /dev/null +++ b/homeassistant/components/ukraine_alarm/config_flow.py @@ -0,0 +1,154 @@ +"""Config flow for Ukraine Alarm.""" +from __future__ import annotations + +import asyncio + +import aiohttp +from ukrainealarm.client import Client +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_REGION +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + + +class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for Ukraine Alarm.""" + + VERSION = 1 + + def __init__(self): + """Initialize a new UkraineAlarmConfigFlow.""" + self.api_key = None + self.states = None + self.selected_region = None + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + websession = async_get_clientsession(self.hass) + try: + regions = await Client( + websession, user_input[CONF_API_KEY] + ).get_regions() + except aiohttp.ClientResponseError as ex: + errors["base"] = "invalid_api_key" if ex.status == 401 else "unknown" + except aiohttp.ClientConnectionError: + errors["base"] = "cannot_connect" + except aiohttp.ClientError: + errors["base"] = "unknown" + except asyncio.TimeoutError: + errors["base"] = "timeout" + + if not errors and not regions: + errors["base"] = "unknown" + + if not errors: + self.api_key = user_input[CONF_API_KEY] + self.states = regions["states"] + return await self.async_step_state() + + schema = vol.Schema( + { + vol.Required(CONF_API_KEY): str, + } + ) + + return self.async_show_form( + step_id="user", + data_schema=schema, + description_placeholders={"api_url": "https://api.ukrainealarm.com/"}, + errors=errors, + last_step=False, + ) + + async def async_step_state(self, user_input=None): + """Handle user-chosen state.""" + return await self._handle_pick_region("state", "district", user_input) + + async def async_step_district(self, user_input=None): + """Handle user-chosen district.""" + return await self._handle_pick_region("district", "community", user_input) + + async def async_step_community(self, user_input=None): + """Handle user-chosen community.""" + return await self._handle_pick_region("community", None, user_input, True) + + async def _handle_pick_region( + self, step_id: str, next_step: str | None, user_input, last_step=False + ): + """Handle picking a (sub)region.""" + if self.selected_region: + source = self.selected_region["regionChildIds"] + else: + source = self.states + + if user_input is not None: + # Only offer to browse subchildren if picked region wasn't the previously picked one + if ( + not self.selected_region + or user_input[CONF_REGION] != self.selected_region["regionId"] + ): + self.selected_region = _find(source, user_input[CONF_REGION]) + + if next_step and self.selected_region["regionChildIds"]: + return await getattr(self, f"async_step_{next_step}")() + + return await self._async_finish_flow() + + regions = {} + if self.selected_region: + regions[self.selected_region["regionId"]] = self.selected_region[ + "regionName" + ] + + regions.update(_make_regions_object(source)) + + schema = vol.Schema( + { + vol.Required(CONF_REGION): vol.In(regions), + } + ) + + return self.async_show_form( + step_id=step_id, data_schema=schema, last_step=last_step + ) + + async def _async_finish_flow(self): + """Finish the setup.""" + await self.async_set_unique_id(self.selected_region["regionId"]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=self.selected_region["regionName"], + data={ + CONF_API_KEY: self.api_key, + CONF_REGION: self.selected_region["regionId"], + CONF_NAME: self.selected_region["regionName"], + }, + ) + + +def _find(regions, region_id): + return next((region for region in regions if region["regionId"] == region_id), None) + + +def _make_regions_object(regions): + regions_list = [] + for region in regions: + regions_list.append( + { + "id": region["regionId"], + "name": region["regionName"], + } + ) + regions_list = sorted(regions_list, key=lambda region: region["name"].lower()) + regions_object = {} + for region in regions_list: + regions_object[region["id"]] = region["name"] + + return regions_object diff --git a/homeassistant/components/ukraine_alarm/const.py b/homeassistant/components/ukraine_alarm/const.py new file mode 100644 index 00000000000..cc1ae352967 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/const.py @@ -0,0 +1,19 @@ +"""Consts for the Ukraine Alarm.""" +from __future__ import annotations + +from homeassistant.const import Platform + +DOMAIN = "ukraine_alarm" +ATTRIBUTION = "Data provided by Ukraine Alarm" +MANUFACTURER = "Ukraine Alarm" +ALERT_TYPE_UNKNOWN = "UNKNOWN" +ALERT_TYPE_AIR = "AIR" +ALERT_TYPE_ARTILLERY = "ARTILLERY" +ALERT_TYPE_URBAN_FIGHTS = "URBAN_FIGHTS" +ALERT_TYPES = { + ALERT_TYPE_UNKNOWN, + ALERT_TYPE_AIR, + ALERT_TYPE_ARTILLERY, + ALERT_TYPE_URBAN_FIGHTS, +} +PLATFORMS = [Platform.BINARY_SENSOR] diff --git a/homeassistant/components/ukraine_alarm/manifest.json b/homeassistant/components/ukraine_alarm/manifest.json new file mode 100644 index 00000000000..08dad9960b5 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "ukraine_alarm", + "name": "Ukraine Alarm", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ukraine_alarm", + "requirements": ["ukrainealarm==0.0.1"], + "codeowners": ["@PaulAnnekov"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/ukraine_alarm/strings.json b/homeassistant/components/ukraine_alarm/strings.json new file mode 100644 index 00000000000..79f81e71b08 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/strings.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + }, + "error": { + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "timeout": "[%key:common::config_flow::error::timeout_connect%]" + }, + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "description": "Set up the Ukraine Alarm integration. To generate an API key go to {api_url}" + }, + "state": { + "data": { + "region": "Region" + }, + "description": "Choose state to monitor" + }, + "district": { + "data": { + "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" + }, + "description": "If you want to monitor not only state, choose its specific district" + }, + "community": { + "data": { + "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" + }, + "description": "If you want to monitor not only state and district, choose its specific community" + } + } + } +} diff --git a/homeassistant/components/ukraine_alarm/translations/en.json b/homeassistant/components/ukraine_alarm/translations/en.json new file mode 100644 index 00000000000..2c39945cb87 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/en.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "description": "Set up the Ukraine Alarm integration. To generate an API key go to {api_url}", + "title": "Ukraine Alarm" + }, + "state": { + "data": { + "region": "Region" + }, + "description": "Choose state to monitor" + }, + "district": { + "data": { + "region": "Region" + }, + "description": "If you want to monitor not only state, choose its specific district" + }, + "community": { + "data": { + "region": "Region" + }, + "description": "If you want to monitor not only state and district, choose its specific community" + } + } + } +} diff --git a/homeassistant/components/ukraine_alarm/translations/ru.json b/homeassistant/components/ukraine_alarm/translations/ru.json new file mode 100644 index 00000000000..89c9eb1670a --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/ru.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f\u0020\u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438\u0020\u0441 Ukraine Alarm. \u0414\u043b\u044f\u0020\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f\u0020\u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435\u0020\u043d\u0430 {api_url}.", + "title": "Ukraine Alarm" + }, + "state": { + "data": { + "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435\u0020\u043e\u0431\u043b\u0430\u0441\u0442\u044c\u0020\u0434\u043b\u044f\u0020\u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430" + }, + "district": { + "data": { + "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + }, + "description": "\u0415\u0441\u043b\u0438\u0020\u0432\u044b\u0020\u0436\u0435\u043b\u0430\u0435\u0442\u0435\u0020\u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u0442\u044c\u0020\u043d\u0435\u0020\u0442\u043e\u043b\u044c\u043a\u043e\u0020\u043e\u0431\u043b\u0430\u0441\u0442\u044c\u002c\u0020\u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435\u0020\u0435\u0451\u0020\u0440\u0430\u0439\u043e\u043d" + }, + "community": { + "data": { + "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + }, + "description": "\u0415\u0441\u043b\u0438\u0020\u0432\u044b\u0020\u0436\u0435\u043b\u0430\u0435\u0442\u0435\u0020\u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u0442\u044c\u0020\u043d\u0435\u0020\u0442\u043e\u043b\u044c\u043a\u043e\u0020\u043e\u0431\u043b\u0430\u0441\u0442\u044c\u0020\u0438\u0020\u0440\u0430\u0439\u043e\u043d\u002c\u0020\u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435\u0020\u0435\u0451\u0020\u0433\u0440\u043e\u043c\u0430\u0434\u0443" + } + } + } +} diff --git a/homeassistant/components/ukraine_alarm/translations/uk.json b/homeassistant/components/ukraine_alarm/translations/uk.json new file mode 100644 index 00000000000..2eed983f34f --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/uk.json @@ -0,0 +1,28 @@ +{ + "config": { + "step": { + "user": { + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f\u0020\u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457\u0020\u0437 Ukraine Alarm. \u0414\u043b\u044f\u0020\u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f\u0020\u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c\u0020\u043d\u0430 {api_url}.", + "title": "Ukraine Alarm" + }, + "state": { + "data": { + "region": "\u0420\u0435\u0433\u0456\u043e\u043d" + }, + "description": "\u041e\u0431\u0435\u0440\u0456\u0442\u044c\u0020\u043e\u0431\u043b\u0430\u0441\u0442\u044c\u0020\u0434\u043b\u044f\u0020\u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443" + }, + "district": { + "data": { + "region": "\u0420\u0435\u0433\u0456\u043e\u043d" + }, + "description": "\u042f\u043a\u0449\u043e\u0020\u0432\u0438\u0020\u0431\u0430\u0436\u0430\u0454\u0442\u0435\u0020\u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u0442\u0438\u0020\u043d\u0435\u0020\u043b\u0438\u0448\u0435\u0020\u043e\u0431\u043b\u0430\u0441\u0442\u044c\u002c\u0020\u043e\u0431\u0435\u0440\u0456\u0442\u044c\u0020\u0457\u0457\u0020\u0440\u0430\u0439\u043e\u043d" + }, + "community": { + "data": { + "region": "\u0420\u0435\u0433\u0456\u043e\u043d" + }, + "description": "\u042f\u043a\u0449\u043e\u0020\u0432\u0438\u0020\u0431\u0430\u0436\u0430\u0454\u0442\u0435\u0020\u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u0442\u0438\u0020\u043d\u0435\u0020\u0442\u0456\u043b\u044c\u043a\u0438\u0020\u043e\u0431\u043b\u0430\u0441\u0442\u044c\u0020\u0442\u0430\u0020\u0440\u0430\u0439\u043e\u043d\u002c\u0020\u043e\u0431\u0435\u0440\u0456\u0442\u044c\u0020\u0457\u0457\u0020\u0433\u0440\u043e\u043c\u0430\u0434\u0443" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 710d97f3c34..510adc74e61 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -366,6 +366,7 @@ FLOWS = { "twentemilieu", "twilio", "twinkly", + "ukraine_alarm", "unifi", "unifiprotect", "upb", diff --git a/requirements_all.txt b/requirements_all.txt index fd1224ee1c8..783c475e7e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2342,6 +2342,9 @@ twitchAPI==2.5.2 # homeassistant.components.rainforest_eagle uEagle==0.0.2 +# homeassistant.components.ukraine_alarm +ukrainealarm==0.0.1 + # homeassistant.components.unifiprotect unifi-discovery==1.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a7f419419c..c8ad86f64dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1524,6 +1524,9 @@ twitchAPI==2.5.2 # homeassistant.components.rainforest_eagle uEagle==0.0.2 +# homeassistant.components.ukraine_alarm +ukrainealarm==0.0.1 + # homeassistant.components.unifiprotect unifi-discovery==1.1.2 diff --git a/tests/components/ukraine_alarm/__init__.py b/tests/components/ukraine_alarm/__init__.py new file mode 100644 index 00000000000..228594b3d0c --- /dev/null +++ b/tests/components/ukraine_alarm/__init__.py @@ -0,0 +1 @@ +"""Tests for the Ukraine Alarm integration.""" diff --git a/tests/components/ukraine_alarm/test_config_flow.py b/tests/components/ukraine_alarm/test_config_flow.py new file mode 100644 index 00000000000..3832e6a9fb6 --- /dev/null +++ b/tests/components/ukraine_alarm/test_config_flow.py @@ -0,0 +1,354 @@ +"""Test the Ukraine Alarm config flow.""" +import asyncio +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +from aiohttp import ClientConnectionError, ClientError, ClientResponseError +import pytest + +from homeassistant import config_entries +from homeassistant.components.ukraine_alarm.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + +MOCK_API_KEY = "mock-api-key" + + +def _region(rid, recurse=0, depth=0): + if depth == 0: + name_prefix = "State" + elif depth == 1: + name_prefix = "District" + else: + name_prefix = "Community" + + name = f"{name_prefix} {rid}" + region = {"regionId": rid, "regionName": name, "regionChildIds": []} + + if not recurse: + return region + + for i in range(1, 4): + region["regionChildIds"].append(_region(f"{rid}.{i}", recurse - 1, depth + 1)) + + return region + + +REGIONS = { + "states": [_region(f"{i}", i - 1) for i in range(1, 4)], +} + + +@pytest.fixture(autouse=True) +def mock_get_regions() -> Generator[None, AsyncMock, None]: + """Mock the get_regions method.""" + + with patch( + "homeassistant.components.ukraine_alarm.config_flow.Client.get_regions", + return_value=REGIONS, + ) as mock_get: + yield mock_get + + +async def test_state(hass: HomeAssistant) -> None: + """Test we can create entry for state.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + with patch( + "homeassistant.components.ukraine_alarm.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "region": "1", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "State 1" + assert result3["data"] == { + "api_key": MOCK_API_KEY, + "region": "1", + "name": result3["title"], + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_state_district(hass: HomeAssistant) -> None: + """Test we can create entry for state + district.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "region": "2", + }, + ) + assert result3["type"] == RESULT_TYPE_FORM + + with patch( + "homeassistant.components.ukraine_alarm.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "region": "2.2", + }, + ) + await hass.async_block_till_done() + + assert result4["type"] == RESULT_TYPE_CREATE_ENTRY + assert result4["title"] == "District 2.2" + assert result4["data"] == { + "api_key": MOCK_API_KEY, + "region": "2.2", + "name": result4["title"], + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_state_district_pick_region(hass: HomeAssistant) -> None: + """Test we can create entry for region which has districts.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "region": "2", + }, + ) + assert result3["type"] == RESULT_TYPE_FORM + + with patch( + "homeassistant.components.ukraine_alarm.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "region": "2", + }, + ) + await hass.async_block_till_done() + + assert result4["type"] == RESULT_TYPE_CREATE_ENTRY + assert result4["title"] == "State 2" + assert result4["data"] == { + "api_key": MOCK_API_KEY, + "region": "2", + "name": result4["title"], + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_state_district_community(hass: HomeAssistant) -> None: + """Test we can create entry for state + district + community.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "region": "3", + }, + ) + assert result3["type"] == RESULT_TYPE_FORM + + result4 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "region": "3.2", + }, + ) + assert result4["type"] == RESULT_TYPE_FORM + + with patch( + "homeassistant.components.ukraine_alarm.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result5 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "region": "3.2.1", + }, + ) + await hass.async_block_till_done() + + assert result5["type"] == RESULT_TYPE_CREATE_ENTRY + assert result5["title"] == "Community 3.2.1" + assert result5["data"] == { + "api_key": MOCK_API_KEY, + "region": "3.2.1", + "name": result5["title"], + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_invalid_api(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: + """Test we can create entry for just region.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + mock_get_regions.side_effect = ClientResponseError(None, None, status=401) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "invalid_api_key"} + + +async def test_server_error(hass: HomeAssistant, mock_get_regions) -> None: + """Test we can create entry for just region.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + mock_get_regions.side_effect = ClientResponseError(None, None, status=500) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "unknown"} + + +async def test_cannot_connect(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: + """Test we can create entry for just region.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + mock_get_regions.side_effect = ClientConnectionError + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_unknown_client_error( + hass: HomeAssistant, mock_get_regions: AsyncMock +) -> None: + """Test we can create entry for just region.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + mock_get_regions.side_effect = ClientError + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "unknown"} + + +async def test_timeout_error(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: + """Test we can create entry for just region.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + mock_get_regions.side_effect = asyncio.TimeoutError + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "timeout"} + + +async def test_no_regions_returned( + hass: HomeAssistant, mock_get_regions: AsyncMock +) -> None: + """Test we can create entry for just region.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + mock_get_regions.return_value = {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": MOCK_API_KEY, + }, + ) + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "unknown"} From 3ee32e22c153fe4545b2ff5e61cfd3417e5b008d Mon Sep 17 00:00:00 2001 From: Shai Ungar Date: Sat, 7 May 2022 23:16:51 +0300 Subject: [PATCH 0301/3516] fix speed sensor wrong number (#71502) --- homeassistant/components/sabnzbd/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index dee80945e9d..539eaa4f097 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -32,13 +32,15 @@ class SabnzbdSensorEntityDescription(SensorEntityDescription, SabnzbdRequiredKey """Describes Sabnzbd sensor entity.""" +SPEED_KEY = "kbpersec" + SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( SabnzbdSensorEntityDescription( key="status", name="Status", ), SabnzbdSensorEntityDescription( - key="kbpersec", + key=SPEED_KEY, name="Speed", native_unit_of_measurement=DATA_RATE_MEGABYTES_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, @@ -154,7 +156,7 @@ class SabnzbdSensor(SensorEntity): self.entity_description.key ) - if self.entity_description.key == "speed": + if self.entity_description.key == SPEED_KEY: self._attr_native_value = round(float(self._attr_native_value) / 1024, 1) elif "size" in self.entity_description.key: self._attr_native_value = round(float(self._attr_native_value), 2) From f88c643e1cd743814fb777918d2361557d0006ab Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 7 May 2022 20:57:57 -0700 Subject: [PATCH 0302/3516] Bump frontend to 20220504.1 (#71504) --- 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 b51219c4f19..8c475d51abb 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220504.0"], + "requirements": ["home-assistant-frontend==20220504.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0a2846a44ad..43c0d2ad4c4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220504.0 +home-assistant-frontend==20220504.1 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.1 diff --git a/requirements_all.txt b/requirements_all.txt index 783c475e7e3..15ce8f1c8ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -819,7 +819,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220504.0 +home-assistant-frontend==20220504.1 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8ad86f64dc..eeb9f18fbfa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -580,7 +580,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220504.0 +home-assistant-frontend==20220504.1 # homeassistant.components.home_connect homeconnect==0.7.0 From d477546e7602598d5de5302fe0e0a7c12b28e750 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 7 May 2022 20:57:48 -0700 Subject: [PATCH 0303/3516] Fix other enums in helpers (#71505) --- homeassistant/helpers/selector.py | 8 ++++++-- tests/helpers/test_selector.py | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 1ae0082c06f..87574949f4e 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -704,7 +704,9 @@ class SelectSelector(Selector): vol.Required("options"): vol.All(vol.Any([str], [select_option])), vol.Optional("multiple", default=False): cv.boolean, vol.Optional("custom_value", default=False): cv.boolean, - vol.Optional("mode"): vol.Coerce(SelectSelectorMode), + vol.Optional("mode"): vol.All( + vol.Coerce(SelectSelectorMode), lambda val: val.value + ), } ) @@ -827,7 +829,9 @@ class TextSelector(Selector): vol.Optional("suffix"): str, # The "type" controls the input field in the browser, the resulting # data can be any string so we don't validate it. - vol.Optional("type"): vol.Coerce(TextSelectorType), + vol.Optional("type"): vol.All( + vol.Coerce(TextSelectorType), lambda val: val.value + ), } ) diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index ed831026065..4cb924a520e 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -351,7 +351,7 @@ def test_object_selector_schema(schema, valid_selections, invalid_selections): ( ({}, ("abc123",), (None,)), ({"multiline": True}, (), ()), - ({"multiline": False}, (), ()), + ({"multiline": False, "type": "email"}, (), ()), ), ) def test_text_selector_schema(schema, valid_selections, invalid_selections): @@ -402,7 +402,7 @@ def test_text_selector_schema(schema, valid_selections, invalid_selections): (0, None, ["red"]), ), ( - {"options": [], "custom_value": True, "multiple": True}, + {"options": [], "custom_value": True, "multiple": True, "mode": "list"}, (["red"], ["green", "blue"], []), (0, None, "red"), ), From 534eef0b766e500aa3c683804291500bbe00137d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 8 May 2022 08:23:26 -0700 Subject: [PATCH 0304/3516] Bumped version to 2022.5.3 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5aed0cdaae9..569c121f909 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 6d41c8526db..2ec80dd2855 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.5.2 +version = 2022.5.3 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 6922209ddb894d96450c4eb0c8cbcdfa4bba6685 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 May 2022 14:15:06 -0500 Subject: [PATCH 0305/3516] Ensure all mysql tables get optimized (#71538) --- homeassistant/components/recorder/models.py | 13 ++++++++++--- homeassistant/components/recorder/repack.py | 3 ++- homeassistant/components/recorder/util.py | 16 ++-------------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index c528093198d..9cc94b3019f 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -61,12 +61,11 @@ TABLE_STATISTICS_META = "statistics_meta" TABLE_STATISTICS_RUNS = "statistics_runs" TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" -# Only add TABLE_STATE_ATTRIBUTES and TABLE_EVENT_DATA -# to the below list once we want to check for their -# instance in the sanity check. ALL_TABLES = [ TABLE_STATES, + TABLE_STATE_ATTRIBUTES, TABLE_EVENTS, + TABLE_EVENT_DATA, TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES, TABLE_STATISTICS, @@ -75,6 +74,14 @@ ALL_TABLES = [ TABLE_STATISTICS_SHORT_TERM, ] +TABLES_TO_CHECK = [ + TABLE_STATES, + TABLE_EVENTS, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, +] + + EMPTY_JSON_OBJECT = "{}" diff --git a/homeassistant/components/recorder/repack.py b/homeassistant/components/recorder/repack.py index c05b8390724..1b1d59df37e 100644 --- a/homeassistant/components/recorder/repack.py +++ b/homeassistant/components/recorder/repack.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING from sqlalchemy import text from .const import SupportedDialect +from .models import ALL_TABLES if TYPE_CHECKING: from . import Recorder @@ -41,6 +42,6 @@ def repack_database(instance: Recorder) -> None: if dialect_name == SupportedDialect.MYSQL: _LOGGER.debug("Optimizing SQL DB to free space") with instance.engine.connect() as conn: - conn.execute(text("OPTIMIZE TABLE states, events, recorder_runs")) + conn.execute(text(f"OPTIMIZE TABLE {','.join(ALL_TABLES)}")) conn.commit() return diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 3d9fa7d29e1..9f6bef86a29 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -27,13 +27,9 @@ import homeassistant.util.dt as dt_util from .const import DATA_INSTANCE, SQLITE_URL_PREFIX, SupportedDialect from .models import ( - ALL_TABLES, TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES, - TABLE_STATISTICS, - TABLE_STATISTICS_META, - TABLE_STATISTICS_RUNS, - TABLE_STATISTICS_SHORT_TERM, + TABLES_TO_CHECK, RecorderRuns, process_timestamp, ) @@ -213,15 +209,7 @@ def last_run_was_recently_clean(cursor: CursorFetchStrategy) -> bool: def basic_sanity_check(cursor: CursorFetchStrategy) -> bool: """Check tables to make sure select does not fail.""" - for table in ALL_TABLES: - # The statistics tables may not be present in old databases - if table in [ - TABLE_STATISTICS, - TABLE_STATISTICS_META, - TABLE_STATISTICS_RUNS, - TABLE_STATISTICS_SHORT_TERM, - ]: - continue + for table in TABLES_TO_CHECK: if table in (TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES): cursor.execute(f"SELECT * FROM {table};") # nosec # not injection else: From e46310ac0b6415f7e370cc5b5d649acdba5f65bd Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Sun, 8 May 2022 15:29:58 -0400 Subject: [PATCH 0306/3516] Add device class for Mazda pressure sensors (#71539) --- homeassistant/components/mazda/sensor.py | 4 ++++ tests/components/mazda/test_sensor.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py index 99f8c74d64d..7e8b45e0ca1 100644 --- a/homeassistant/components/mazda/sensor.py +++ b/homeassistant/components/mazda/sensor.py @@ -188,6 +188,7 @@ SENSOR_ENTITIES = [ key="front_left_tire_pressure", name_suffix="Front Left Tire Pressure", icon="mdi:car-tire-alert", + device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_PSI, state_class=SensorStateClass.MEASUREMENT, is_supported=_front_left_tire_pressure_supported, @@ -197,6 +198,7 @@ SENSOR_ENTITIES = [ key="front_right_tire_pressure", name_suffix="Front Right Tire Pressure", icon="mdi:car-tire-alert", + device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_PSI, state_class=SensorStateClass.MEASUREMENT, is_supported=_front_right_tire_pressure_supported, @@ -206,6 +208,7 @@ SENSOR_ENTITIES = [ key="rear_left_tire_pressure", name_suffix="Rear Left Tire Pressure", icon="mdi:car-tire-alert", + device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_PSI, state_class=SensorStateClass.MEASUREMENT, is_supported=_rear_left_tire_pressure_supported, @@ -215,6 +218,7 @@ SENSOR_ENTITIES = [ key="rear_right_tire_pressure", name_suffix="Rear Right Tire Pressure", icon="mdi:car-tire-alert", + device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_PSI, state_class=SensorStateClass.MEASUREMENT, is_supported=_rear_right_tire_pressure_supported, diff --git a/tests/components/mazda/test_sensor.py b/tests/components/mazda/test_sensor.py index f7fb379da51..f2e9039397b 100644 --- a/tests/components/mazda/test_sensor.py +++ b/tests/components/mazda/test_sensor.py @@ -75,6 +75,7 @@ async def test_sensors(hass): state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Front Left Tire Pressure" ) assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.state == "35" @@ -90,6 +91,7 @@ async def test_sensors(hass): == "My Mazda3 Front Right Tire Pressure" ) assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.state == "35" @@ -104,6 +106,7 @@ async def test_sensors(hass): state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Left Tire Pressure" ) assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.state == "33" @@ -118,6 +121,7 @@ async def test_sensors(hass): state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Right Tire Pressure" ) assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PSI assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.state == "33" From 7c9c0e911a48e2b99ff15068d9bfbbed5b2cedd2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 May 2022 14:45:57 -0500 Subject: [PATCH 0307/3516] Move do_adhoc_statistics to recorder test helpers (#71544) --- tests/components/history/test_init.py | 7 +- tests/components/recorder/common.py | 11 +- tests/components/recorder/test_statistics.py | 19 ++- .../components/recorder/test_websocket_api.py | 7 +- tests/components/sensor/test_recorder.py | 136 +++++++----------- 5 files changed, 79 insertions(+), 101 deletions(-) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 630d0b84889..143b0c55fab 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -8,7 +8,7 @@ from unittest.mock import patch, sentinel import pytest from pytest import approx -from homeassistant.components import history, recorder +from homeassistant.components import history from homeassistant.components.recorder.history import get_significant_states from homeassistant.components.recorder.models import process_timestamp import homeassistant.core as ha @@ -20,6 +20,7 @@ from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM from tests.components.recorder.common import ( async_recorder_block_till_done, async_wait_recording_done, + do_adhoc_statistics, wait_recording_done, ) @@ -879,7 +880,7 @@ async def test_statistics_during_period( hass.states.async_set("sensor.test", state, attributes=attributes) await async_wait_recording_done(hass) - hass.data[recorder.DATA_INSTANCE].do_adhoc_statistics(start=now) + do_adhoc_statistics(hass, start=now) await async_wait_recording_done(hass) client = await hass_ws_client() @@ -1021,7 +1022,7 @@ async def test_list_statistic_ids( } ] - hass.data[recorder.DATA_INSTANCE].do_adhoc_statistics(start=now) + do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) # Remove the state, statistics will now be fetched from the database hass.states.async_remove("sensor.test") diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index a7472a6289e..b36547309cd 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -2,14 +2,16 @@ from __future__ import annotations from datetime import datetime, timedelta -from typing import cast +from typing import Any, cast from sqlalchemy import create_engine from sqlalchemy.orm.session import Session from homeassistant import core as ha from homeassistant.components import recorder +from homeassistant.components.recorder import get_instance, statistics from homeassistant.components.recorder.models import RecorderRuns +from homeassistant.components.recorder.tasks import StatisticsTask from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -19,6 +21,13 @@ from tests.components.recorder import models_schema_0 DEFAULT_PURGE_TASKS = 3 +def do_adhoc_statistics(hass: HomeAssistant, **kwargs: Any) -> None: + """Trigger an adhoc statistics run.""" + if not (start := kwargs.get("start")): + start = statistics.get_start_time() + get_instance(hass).queue_task(StatisticsTask(start)) + + def wait_recording_done(hass: HomeAssistant) -> None: """Block till recording is done.""" hass.block_till_done() diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 765364a7487..28eba51a4f3 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -30,7 +30,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util -from .common import async_wait_recording_done +from .common import async_wait_recording_done, do_adhoc_statistics from tests.common import mock_registry from tests.components.recorder.common import wait_recording_done @@ -57,8 +57,8 @@ def test_compile_hourly_statistics(hass_recorder): stats = get_last_short_term_statistics(hass, 0, "sensor.test1", True) assert stats == {} - recorder.do_adhoc_statistics(start=zero) - recorder.do_adhoc_statistics(start=four) + do_adhoc_statistics(hass, start=zero) + do_adhoc_statistics(hass, start=four) wait_recording_done(hass) expected_1 = { "statistic_id": "sensor.test1", @@ -197,12 +197,11 @@ def test_compile_periodic_statistics_exception( """Test exception handling when compiling periodic statistics.""" hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) now = dt_util.utcnow() - recorder.do_adhoc_statistics(start=now) - recorder.do_adhoc_statistics(start=now + timedelta(minutes=5)) + do_adhoc_statistics(hass, start=now) + do_adhoc_statistics(hass, start=now + timedelta(minutes=5)) wait_recording_done(hass) expected_1 = { "statistic_id": "sensor.test1", @@ -249,7 +248,6 @@ def test_compile_periodic_statistics_exception( def test_rename_entity(hass_recorder): """Test statistics is migrated when entity_id is changed.""" hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) entity_reg = mock_registry(hass) @@ -277,7 +275,7 @@ def test_rename_entity(hass_recorder): stats = get_last_short_term_statistics(hass, 0, "sensor.test1", True) assert stats == {} - recorder.do_adhoc_statistics(start=zero) + do_adhoc_statistics(hass, start=zero) wait_recording_done(hass) expected_1 = { "statistic_id": "sensor.test1", @@ -317,7 +315,6 @@ def test_rename_entity(hass_recorder): def test_statistics_duplicated(hass_recorder, caplog): """Test statistics with same start time is not compiled.""" hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) zero, four, states = record_states(hass) hist = history.get_significant_states(hass, zero, four) @@ -331,7 +328,7 @@ def test_statistics_duplicated(hass_recorder, caplog): "homeassistant.components.sensor.recorder.compile_statistics", return_value=statistics.PlatformCompiledStatistics([], {}), ) as compile_statistics: - recorder.do_adhoc_statistics(start=zero) + do_adhoc_statistics(hass, start=zero) wait_recording_done(hass) assert compile_statistics.called compile_statistics.reset_mock() @@ -339,7 +336,7 @@ def test_statistics_duplicated(hass_recorder, caplog): assert "Statistics already compiled" not in caplog.text caplog.clear() - recorder.do_adhoc_statistics(start=zero) + do_adhoc_statistics(hass, start=zero) wait_recording_done(hass) assert not compile_statistics.called compile_statistics.reset_mock() diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index fe197cb72e6..55bbb13898e 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -18,6 +18,7 @@ from .common import ( async_recorder_block_till_done, async_wait_recording_done, create_engine_test, + do_adhoc_statistics, ) from tests.common import async_fire_time_changed @@ -84,7 +85,7 @@ async def test_clear_statistics(hass, hass_ws_client, recorder_mock): hass.states.async_set("sensor.test3", state * 3, attributes=attributes) await async_wait_recording_done(hass) - hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) client = await hass_ws_client() @@ -208,7 +209,7 @@ async def test_update_statistics_metadata( hass.states.async_set("sensor.test", state, attributes=attributes) await async_wait_recording_done(hass) - hass.data[DATA_INSTANCE].do_adhoc_statistics(period="hourly", start=now) + do_adhoc_statistics(hass, period="hourly", start=now) await async_recorder_block_till_done(hass) client = await hass_ws_client() @@ -521,7 +522,7 @@ async def test_get_statistics_metadata( } ] - hass.data[recorder.DATA_INSTANCE].do_adhoc_statistics(start=now) + do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) # Remove the state, statistics will now be fetched from the database hass.states.async_remove("sensor.test") diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index b4abf4d9ec3..9165fbc4354 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -1,7 +1,6 @@ """The tests for sensor recorder platform.""" # pylint: disable=protected-access,invalid-name from datetime import timedelta -from functools import partial import math from statistics import mean from unittest.mock import patch @@ -30,6 +29,7 @@ from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM from tests.components.recorder.common import ( async_recorder_block_till_done, async_wait_recording_done, + do_adhoc_statistics, wait_recording_done, ) @@ -101,7 +101,6 @@ def test_compile_hourly_statistics( """Test compiling hourly statistics.""" zero = dt_util.utcnow() hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added attributes = { @@ -113,7 +112,7 @@ def test_compile_hourly_statistics( hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) - recorder.do_adhoc_statistics(start=zero) + do_adhoc_statistics(hass, start=zero) wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ @@ -157,7 +156,6 @@ def test_compile_hourly_statistics_purged_state_changes( """Test compiling hourly statistics.""" zero = dt_util.utcnow() hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added attributes = { @@ -181,7 +179,7 @@ def test_compile_hourly_statistics_purged_state_changes( hist = history.get_significant_states(hass, zero, four) assert not hist - recorder.do_adhoc_statistics(start=zero) + do_adhoc_statistics(hass, start=zero) wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ @@ -218,7 +216,6 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes """Test compiling hourly statistics for unsupported sensor.""" zero = dt_util.utcnow() hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added four, states = record_states(hass, zero, "sensor.test1", attributes) @@ -250,7 +247,7 @@ def test_compile_hourly_statistics_unsupported(hass_recorder, caplog, attributes hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) - recorder.do_adhoc_statistics(start=zero) + do_adhoc_statistics(hass, start=zero) wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ @@ -361,7 +358,6 @@ async def test_compile_hourly_sum_statistics_amount( period2_end = period0 + timedelta(minutes=15) client = await hass_ws_client() hass.config.units = units - recorder = hass.data[DATA_INSTANCE] await async_setup_component(hass, "sensor", {}) # Wait for the sensor recorder platform to be added await async_recorder_block_till_done(hass) @@ -382,17 +378,11 @@ async def test_compile_hourly_sum_statistics_amount( ) assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] - await hass.async_add_executor_job( - partial(recorder.do_adhoc_statistics, start=period0) - ) + do_adhoc_statistics(hass, start=period0) await async_wait_recording_done(hass) - await hass.async_add_executor_job( - partial(recorder.do_adhoc_statistics, start=period1) - ) + do_adhoc_statistics(hass, start=period1) await async_wait_recording_done(hass) - await hass.async_add_executor_job( - partial(recorder.do_adhoc_statistics, start=period2) - ) + do_adhoc_statistics(hass, start=period2) await async_wait_recording_done(hass) statistic_ids = await hass.async_add_executor_job(list_statistic_ids, hass) assert statistic_ids == [ @@ -525,7 +515,6 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( """Test compiling hourly statistics.""" zero = dt_util.utcnow() hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added attributes = { @@ -573,8 +562,8 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( ) assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] - recorder.do_adhoc_statistics(start=zero) - recorder.do_adhoc_statistics(start=zero + timedelta(minutes=5)) + do_adhoc_statistics(hass, start=zero) + do_adhoc_statistics(hass, start=zero + timedelta(minutes=5)) wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ @@ -632,7 +621,6 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( """Test compiling hourly statistics.""" zero = dt_util.utcnow() hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added attributes = { @@ -666,7 +654,7 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( ) assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] - recorder.do_adhoc_statistics(start=zero) + do_adhoc_statistics(hass, start=zero) wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ @@ -712,7 +700,6 @@ def test_compile_hourly_sum_statistics_nan_inf_state( """Test compiling hourly statistics with nan and inf states.""" zero = dt_util.utcnow() hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added attributes = { @@ -742,7 +729,7 @@ def test_compile_hourly_sum_statistics_nan_inf_state( ) assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] - recorder.do_adhoc_statistics(start=zero) + do_adhoc_statistics(hass, start=zero) wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ @@ -817,7 +804,6 @@ def test_compile_hourly_sum_statistics_negative_state( zero = dt_util.utcnow() hass = hass_recorder() hass.data.pop(loader.DATA_CUSTOM_COMPONENTS) - recorder = hass.data[DATA_INSTANCE] platform = getattr(hass.components, "test.sensor") platform.init(empty=True) @@ -855,7 +841,7 @@ def test_compile_hourly_sum_statistics_negative_state( ) assert dict(states)[entity_id] == dict(hist)[entity_id] - recorder.do_adhoc_statistics(start=zero) + do_adhoc_statistics(hass, start=zero) wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert { @@ -911,7 +897,6 @@ def test_compile_hourly_sum_statistics_total_no_reset( period1_end = period2 = period0 + timedelta(minutes=10) period2_end = period0 + timedelta(minutes=15) hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added attributes = { @@ -930,11 +915,11 @@ def test_compile_hourly_sum_statistics_total_no_reset( ) assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] - recorder.do_adhoc_statistics(start=period0) + do_adhoc_statistics(hass, start=period0) wait_recording_done(hass) - recorder.do_adhoc_statistics(start=period1) + do_adhoc_statistics(hass, start=period1) wait_recording_done(hass) - recorder.do_adhoc_statistics(start=period2) + do_adhoc_statistics(hass, start=period2) wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ @@ -1006,7 +991,6 @@ def test_compile_hourly_sum_statistics_total_increasing( period1_end = period2 = period0 + timedelta(minutes=10) period2_end = period0 + timedelta(minutes=15) hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added attributes = { @@ -1025,11 +1009,11 @@ def test_compile_hourly_sum_statistics_total_increasing( ) assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] - recorder.do_adhoc_statistics(start=period0) + do_adhoc_statistics(hass, start=period0) wait_recording_done(hass) - recorder.do_adhoc_statistics(start=period1) + do_adhoc_statistics(hass, start=period1) wait_recording_done(hass) - recorder.do_adhoc_statistics(start=period2) + do_adhoc_statistics(hass, start=period2) wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ @@ -1099,7 +1083,6 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( period1_end = period2 = period0 + timedelta(minutes=10) period2_end = period0 + timedelta(minutes=15) hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added attributes = { @@ -1118,15 +1101,15 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( ) assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] - recorder.do_adhoc_statistics(start=period0) + do_adhoc_statistics(hass, start=period0) wait_recording_done(hass) - recorder.do_adhoc_statistics(start=period1) + do_adhoc_statistics(hass, start=period1) wait_recording_done(hass) assert ( "Entity sensor.test1 has state class total_increasing, but its state is not " "strictly increasing." ) not in caplog.text - recorder.do_adhoc_statistics(start=period2) + do_adhoc_statistics(hass, start=period2) wait_recording_done(hass) state = states["sensor.test1"][6].state previous_state = float(states["sensor.test1"][5].state) @@ -1196,7 +1179,6 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): period1_end = period2 = period0 + timedelta(minutes=10) period2_end = period0 + timedelta(minutes=15) hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added sns1_attr = { @@ -1225,11 +1207,11 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): ) assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] - recorder.do_adhoc_statistics(start=period0) + do_adhoc_statistics(hass, start=period0) wait_recording_done(hass) - recorder.do_adhoc_statistics(start=period1) + do_adhoc_statistics(hass, start=period1) wait_recording_done(hass) - recorder.do_adhoc_statistics(start=period2) + do_adhoc_statistics(hass, start=period2) wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ @@ -1290,7 +1272,6 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): period1_end = period2 = period0 + timedelta(minutes=10) period2_end = period0 + timedelta(minutes=15) hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added sns1_attr = {**ENERGY_SENSOR_ATTRIBUTES, "last_reset": None} @@ -1317,11 +1298,11 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): ) assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] - recorder.do_adhoc_statistics(start=period0) + do_adhoc_statistics(hass, start=period0) wait_recording_done(hass) - recorder.do_adhoc_statistics(start=period1) + do_adhoc_statistics(hass, start=period1) wait_recording_done(hass) - recorder.do_adhoc_statistics(start=period2) + do_adhoc_statistics(hass, start=period2) wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ @@ -1483,7 +1464,6 @@ def test_compile_hourly_statistics_unchanged( """Test compiling hourly statistics, with no changes during the hour.""" zero = dt_util.utcnow() hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added attributes = { @@ -1495,7 +1475,7 @@ def test_compile_hourly_statistics_unchanged( hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) - recorder.do_adhoc_statistics(start=four) + do_adhoc_statistics(hass, start=four) wait_recording_done(hass) stats = statistics_during_period(hass, four, period="5minute") assert stats == { @@ -1520,7 +1500,6 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder, caplog): """Test compiling hourly statistics, with the sensor being partially unavailable.""" zero = dt_util.utcnow() hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added four, states = record_states_partially_unavailable( @@ -1529,7 +1508,7 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder, caplog): hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) - recorder.do_adhoc_statistics(start=zero) + do_adhoc_statistics(hass, start=zero) wait_recording_done(hass) stats = statistics_during_period(hass, zero, period="5minute") assert stats == { @@ -1572,7 +1551,6 @@ def test_compile_hourly_statistics_unavailable( """Test compiling hourly statistics, with the sensor being unavailable.""" zero = dt_util.utcnow() hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added attributes = { @@ -1588,7 +1566,7 @@ def test_compile_hourly_statistics_unavailable( hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) - recorder.do_adhoc_statistics(start=four) + do_adhoc_statistics(hass, start=four) wait_recording_done(hass) stats = statistics_during_period(hass, four, period="5minute") assert stats == { @@ -1613,14 +1591,13 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog): """Test compiling hourly statistics throws.""" zero = dt_util.utcnow() hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added with patch( "homeassistant.components.sensor.recorder.compile_statistics", side_effect=Exception, ): - recorder.do_adhoc_statistics(start=zero) + do_adhoc_statistics(hass, start=zero) wait_recording_done(hass) assert "Error while processing event StatisticsTask" in caplog.text @@ -1737,7 +1714,6 @@ def test_compile_hourly_statistics_changing_units_1( """Test compiling hourly statistics where units change from one hour to the next.""" zero = dt_util.utcnow() hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added attributes = { @@ -1758,7 +1734,7 @@ def test_compile_hourly_statistics_changing_units_1( hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) - recorder.do_adhoc_statistics(start=zero) + do_adhoc_statistics(hass, 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) @@ -1789,7 +1765,7 @@ def test_compile_hourly_statistics_changing_units_1( ] } - recorder.do_adhoc_statistics(start=zero + timedelta(minutes=10)) + do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) wait_recording_done(hass) assert ( "The unit of sensor.test1 (cats) does not match the unit of already compiled " @@ -1840,7 +1816,6 @@ def test_compile_hourly_statistics_changing_units_2( """Test compiling hourly statistics where units change during an hour.""" zero = dt_util.utcnow() hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added attributes = { @@ -1857,7 +1832,7 @@ def test_compile_hourly_statistics_changing_units_2( hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) - recorder.do_adhoc_statistics(start=zero + timedelta(seconds=30 * 5)) + do_adhoc_statistics(hass, start=zero + timedelta(seconds=30 * 5)) wait_recording_done(hass) assert "The unit of sensor.test1 is changing" in caplog.text assert "and matches the unit of already compiled statistics" not in caplog.text @@ -1893,7 +1868,6 @@ def test_compile_hourly_statistics_changing_units_3( """Test compiling hourly statistics where units change from one hour to the next.""" zero = dt_util.utcnow() hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added attributes = { @@ -1914,7 +1888,7 @@ def test_compile_hourly_statistics_changing_units_3( hist = history.get_significant_states(hass, zero, four) assert dict(states) == dict(hist) - recorder.do_adhoc_statistics(start=zero) + do_adhoc_statistics(hass, 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) @@ -1945,7 +1919,7 @@ def test_compile_hourly_statistics_changing_units_3( ] } - recorder.do_adhoc_statistics(start=zero + timedelta(minutes=10)) + do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) wait_recording_done(hass) assert "The unit of sensor.test1 is changing" in caplog.text assert f"matches the unit of already compiled statistics ({unit})" in caplog.text @@ -1991,7 +1965,6 @@ def test_compile_hourly_statistics_changing_device_class_1( """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", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added @@ -2002,7 +1975,7 @@ def test_compile_hourly_statistics_changing_device_class_1( } four, states = record_states(hass, zero, "sensor.test1", attributes) - recorder.do_adhoc_statistics(start=zero) + do_adhoc_statistics(hass, 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) @@ -2047,7 +2020,7 @@ def test_compile_hourly_statistics_changing_device_class_1( 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)) + do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) wait_recording_done(hass) assert ( f"The normalized unit of sensor.test1 ({statistic_unit}) does not match the " @@ -2095,7 +2068,6 @@ def test_compile_hourly_statistics_changing_device_class_2( """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", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added @@ -2107,7 +2079,7 @@ def test_compile_hourly_statistics_changing_device_class_2( } four, states = record_states(hass, zero, "sensor.test1", attributes) - recorder.do_adhoc_statistics(start=zero) + do_adhoc_statistics(hass, 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) @@ -2152,7 +2124,7 @@ def test_compile_hourly_statistics_changing_device_class_2( 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)) + do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) wait_recording_done(hass) assert ( f"The unit of sensor.test1 ({state_unit}) does not match the " @@ -2202,7 +2174,6 @@ def test_compile_hourly_statistics_changing_statistics( period0_end = period1 = period0 + timedelta(minutes=5) period1_end = period0 + timedelta(minutes=10) hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added attributes_1 = { @@ -2216,7 +2187,7 @@ def test_compile_hourly_statistics_changing_statistics( "unit_of_measurement": unit, } four, states = record_states(hass, period0, "sensor.test1", attributes_1) - recorder.do_adhoc_statistics(start=period0) + do_adhoc_statistics(hass, start=period0) wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ @@ -2250,7 +2221,7 @@ def test_compile_hourly_statistics_changing_statistics( hist = history.get_significant_states(hass, period0, four) assert dict(states) == dict(hist) - recorder.do_adhoc_statistics(start=period1) + do_adhoc_statistics(hass, start=period1) wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ @@ -2447,7 +2418,7 @@ def test_compile_statistics_hourly_daily_monthly_summary( # Generate 5-minute statistics for two hours start = zero for i in range(24): - recorder.do_adhoc_statistics(start=start) + do_adhoc_statistics(hass, start=start) wait_recording_done(hass) start += timedelta(minutes=5) @@ -2841,7 +2812,7 @@ async def test_validate_statistics_supported_device_class( # Statistics has run, invalid state - expect error await async_recorder_block_till_done(hass) - hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + do_adhoc_statistics(hass, start=now) hass.states.async_set( "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} ) @@ -2856,7 +2827,7 @@ async def test_validate_statistics_supported_device_class( await assert_validation_result(client, {}) # Valid state, statistic runs again - empty response - hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -2915,7 +2886,7 @@ async def test_validate_statistics_supported_device_class_2( await assert_validation_result(client, {}) # Statistics has run, device class set - expect error - hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) hass.states.async_set("sensor.test", 12, attributes=attributes) await hass.async_block_till_done() @@ -3004,7 +2975,7 @@ async def test_validate_statistics_unsupported_state_class( await assert_validation_result(client, {}) # Statistics has run, empty response - hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -3068,7 +3039,7 @@ async def test_validate_statistics_sensor_no_longer_recorded( await assert_validation_result(client, {}) # Statistics has run, empty response - hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -3141,7 +3112,7 @@ async def test_validate_statistics_sensor_not_recorded( await assert_validation_result(client, expected) # Statistics has run, expect same error - hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) await assert_validation_result(client, expected) @@ -3187,7 +3158,7 @@ async def test_validate_statistics_sensor_removed( await assert_validation_result(client, {}) # Statistics has run, empty response - hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -3243,7 +3214,6 @@ async def test_validate_statistics_unsupported_device_class( await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) client = await hass_ws_client() - rec = hass.data[DATA_INSTANCE] # No statistics, no state - empty response await assert_validation_result(client, {}) @@ -3260,7 +3230,7 @@ async def test_validate_statistics_unsupported_device_class( # Run statistics, no statistics will be generated because of conflicting units await async_recorder_block_till_done(hass) - rec.do_adhoc_statistics(start=now) + do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) await assert_statistic_ids([]) @@ -3272,7 +3242,7 @@ async def test_validate_statistics_unsupported_device_class( # Run statistics one hour later, only the "dogs" state will be considered await async_recorder_block_till_done(hass) - rec.do_adhoc_statistics(start=now + timedelta(hours=1)) + do_adhoc_statistics(hass, start=now + timedelta(hours=1)) await async_recorder_block_till_done(hass) await assert_statistic_ids( [{"statistic_id": "sensor.test", "unit_of_measurement": "dogs"}] @@ -3305,7 +3275,7 @@ async def test_validate_statistics_unsupported_device_class( # Valid state, statistic runs again - empty response await async_recorder_block_till_done(hass) - hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) From 896bf986ebfc5b58344f4e9af02b1b6386caed90 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 May 2022 14:47:12 -0500 Subject: [PATCH 0308/3516] Speed up nightly database purges with lambda_stmt (#71537) --- homeassistant/components/recorder/purge.py | 123 +++++--------- homeassistant/components/recorder/queries.py | 165 ++++++++++++++++++- 2 files changed, 205 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 2c6211ca2cd..f94f3d3d641 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -7,23 +7,32 @@ from itertools import zip_longest import logging from typing import TYPE_CHECKING -from sqlalchemy import func from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import distinct from homeassistant.const import EVENT_STATE_CHANGED from .const import MAX_ROWS_TO_PURGE, SupportedDialect -from .models import ( - EventData, - Events, - RecorderRuns, - StateAttributes, - States, - StatisticsRuns, - StatisticsShortTerm, +from .models import Events, StateAttributes, States +from .queries import ( + attributes_ids_exist_in_states, + attributes_ids_exist_in_states_sqlite, + data_ids_exist_in_events, + data_ids_exist_in_events_sqlite, + delete_event_data_rows, + delete_event_rows, + delete_recorder_runs_rows, + delete_states_attributes_rows, + delete_states_rows, + delete_statistics_runs_rows, + delete_statistics_short_term_rows, + disconnect_states_rows, + find_events_to_purge, + find_latest_statistics_runs_run_id, + find_short_term_statistics_to_purge, + find_states_to_purge, + find_statistics_runs_to_purge, ) -from .queries import attributes_ids_exist_in_states, data_ids_exist_in_events from .repack import repack_database from .util import retryable_database_job, session_scope @@ -101,19 +110,9 @@ def _select_event_state_attributes_ids_data_ids_to_purge( session: Session, purge_before: datetime ) -> tuple[set[int], set[int], set[int], set[int]]: """Return a list of event, state, and attribute ids to purge.""" - events = ( - session.query(Events.event_id, Events.data_id) - .filter(Events.time_fired < purge_before) - .limit(MAX_ROWS_TO_PURGE) - .all() - ) + events = session.execute(find_events_to_purge(purge_before)).all() _LOGGER.debug("Selected %s event ids to remove", len(events)) - states = ( - session.query(States.state_id, States.attributes_id) - .filter(States.last_updated < purge_before) - .limit(MAX_ROWS_TO_PURGE) - .all() - ) + states = session.execute(find_states_to_purge(purge_before)).all() _LOGGER.debug("Selected %s state ids to remove", len(states)) event_ids = set() state_ids = set() @@ -152,9 +151,9 @@ def _select_unused_attributes_ids( # seen_ids = { state[0] - for state in session.query(distinct(States.attributes_id)) - .filter(States.attributes_id.in_(attributes_ids)) - .all() + for state in session.execute( + attributes_ids_exist_in_states_sqlite(attributes_ids) + ).all() } else: # @@ -210,9 +209,9 @@ def _select_unused_event_data_ids( if using_sqlite: seen_ids = { state[0] - for state in session.query(distinct(Events.data_id)) - .filter(Events.data_id.in_(data_ids)) - .all() + for state in session.execute( + data_ids_exist_in_events_sqlite(data_ids) + ).all() } else: seen_ids = set() @@ -234,16 +233,11 @@ 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 = session.execute(find_statistics_runs_to_purge(purge_before)).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() + last_run := session.execute(find_latest_statistics_runs_run_id()).scalar() ) and last_run in statistic_runs_list: statistic_runs_list.remove(last_run) @@ -255,12 +249,9 @@ 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() - ) + statistics = session.execute( + find_short_term_statistics_to_purge(purge_before) + ).all() _LOGGER.debug("Selected %s short term statistics to remove", len(statistics)) return [statistic.id for statistic in statistics] @@ -272,18 +263,10 @@ def _purge_state_ids(instance: Recorder, session: Session, state_ids: set[int]) # the delete does not fail due to a foreign key constraint # since some databases (MSSQL) cannot do the ON DELETE SET NULL # for us. - disconnected_rows = ( - session.query(States) - .filter(States.old_state_id.in_(state_ids)) - .update({"old_state_id": None}, synchronize_session=False) - ) + disconnected_rows = session.execute(disconnect_states_rows(state_ids)) _LOGGER.debug("Updated %s states to remove old_state_id", disconnected_rows) - deleted_rows = ( - session.query(States) - .filter(States.state_id.in_(state_ids)) - .delete(synchronize_session=False) - ) + deleted_rows = session.execute(delete_states_rows(state_ids)) _LOGGER.debug("Deleted %s states", deleted_rows) # Evict eny entries in the old_states cache referring to a purged state @@ -348,12 +331,7 @@ def _purge_attributes_ids( instance: Recorder, session: Session, attributes_ids: set[int] ) -> None: """Delete old attributes ids.""" - - deleted_rows = ( - session.query(StateAttributes) - .filter(StateAttributes.attributes_id.in_(attributes_ids)) - .delete(synchronize_session=False) - ) + deleted_rows = session.execute(delete_states_attributes_rows(attributes_ids)) _LOGGER.debug("Deleted %s attribute states", deleted_rows) # Evict any entries in the state_attributes_ids cache referring to a purged state @@ -365,11 +343,7 @@ def _purge_event_data_ids( ) -> None: """Delete old event data ids.""" - deleted_rows = ( - session.query(EventData) - .filter(EventData.data_id.in_(data_ids)) - .delete(synchronize_session=False) - ) + deleted_rows = session.execute(delete_event_data_rows(data_ids)) _LOGGER.debug("Deleted %s data events", deleted_rows) # Evict any entries in the event_data_ids cache referring to a purged state @@ -378,11 +352,7 @@ def _purge_event_data_ids( 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) - ) + deleted_rows = session.execute(delete_statistics_runs_rows(statistics_runs)) _LOGGER.debug("Deleted %s statistic runs", deleted_rows) @@ -390,21 +360,15 @@ 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) + deleted_rows = session.execute( + delete_statistics_short_term_rows(short_term_statistics) ) _LOGGER.debug("Deleted %s short term statistics", deleted_rows) def _purge_event_ids(session: Session, event_ids: Iterable[int]) -> None: """Delete by event id.""" - deleted_rows = ( - session.query(Events) - .filter(Events.event_id.in_(event_ids)) - .delete(synchronize_session=False) - ) + deleted_rows = session.execute(delete_event_rows(event_ids)) _LOGGER.debug("Deleted %s events", deleted_rows) @@ -413,11 +377,8 @@ def _purge_old_recorder_runs( ) -> None: """Purge all old recorder runs.""" # Recorder runs is small, no need to batch run it - deleted_rows = ( - session.query(RecorderRuns) - .filter(RecorderRuns.start < purge_before) - .filter(RecorderRuns.run_id != instance.run_history.current.run_id) - .delete(synchronize_session=False) + deleted_rows = session.execute( + delete_recorder_runs_rows(purge_before, instance.run_history.current.run_id) ) _LOGGER.debug("Deleted %s recorder_runs", deleted_rows) diff --git a/homeassistant/components/recorder/queries.py b/homeassistant/components/recorder/queries.py index ff4662e27b1..76098663bd7 100644 --- a/homeassistant/components/recorder/queries.py +++ b/homeassistant/components/recorder/queries.py @@ -1,11 +1,23 @@ """Queries for the recorder.""" from __future__ import annotations -from sqlalchemy import func, lambda_stmt, select, union_all +from collections.abc import Iterable +from datetime import datetime + +from sqlalchemy import delete, distinct, func, lambda_stmt, select, union_all, update from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import Select -from .models import EventData, Events, StateAttributes, States +from .const import MAX_ROWS_TO_PURGE +from .models import ( + EventData, + Events, + RecorderRuns, + StateAttributes, + States, + StatisticsRuns, + StatisticsShortTerm, +) def find_shared_attributes_id( @@ -33,6 +45,17 @@ def _state_attrs_exist(attr: int | None) -> Select: return select(func.min(States.attributes_id)).where(States.attributes_id == attr) +def attributes_ids_exist_in_states_sqlite( + attributes_ids: Iterable[int], +) -> StatementLambdaElement: + """Find attributes ids that exist in the states table.""" + return lambda_stmt( + lambda: select(distinct(States.attributes_id)).filter( + States.attributes_id.in_(attributes_ids) + ) + ) + + def attributes_ids_exist_in_states( attr1: int, attr2: int | None, @@ -245,6 +268,15 @@ def attributes_ids_exist_in_states( ) +def data_ids_exist_in_events_sqlite( + data_ids: Iterable[int], +) -> StatementLambdaElement: + """Find data ids that exist in the events table.""" + return lambda_stmt( + lambda: select(distinct(Events.data_id)).filter(Events.data_id.in_(data_ids)) + ) + + def _event_data_id_exist(data_id: int | None) -> Select: """Check if a event data id exists in the events table.""" return select(func.min(Events.data_id)).where(Events.data_id == data_id) @@ -460,3 +492,132 @@ def data_ids_exist_in_events( _event_data_id_exist(id100), ) ) + + +def disconnect_states_rows(state_ids: Iterable[int]) -> StatementLambdaElement: + """Disconnect states rows.""" + return lambda_stmt( + lambda: update(States) + .where(States.old_state_id.in_(state_ids)) + .values(old_state_id=None) + .execution_options(synchronize_session=False) + ) + + +def delete_states_rows(state_ids: Iterable[int]) -> StatementLambdaElement: + """Delete states rows.""" + return lambda_stmt( + lambda: delete(States) + .where(States.state_id.in_(state_ids)) + .execution_options(synchronize_session=False) + ) + + +def delete_event_data_rows(data_ids: Iterable[int]) -> StatementLambdaElement: + """Delete event_data rows.""" + return lambda_stmt( + lambda: delete(EventData) + .where(EventData.data_id.in_(data_ids)) + .execution_options(synchronize_session=False) + ) + + +def delete_states_attributes_rows( + attributes_ids: Iterable[int], +) -> StatementLambdaElement: + """Delete states_attributes rows.""" + return lambda_stmt( + lambda: delete(StateAttributes) + .where(StateAttributes.attributes_id.in_(attributes_ids)) + .execution_options(synchronize_session=False) + ) + + +def delete_statistics_runs_rows( + statistics_runs: Iterable[int], +) -> StatementLambdaElement: + """Delete statistics_runs rows.""" + return lambda_stmt( + lambda: delete(StatisticsRuns) + .where(StatisticsRuns.run_id.in_(statistics_runs)) + .execution_options(synchronize_session=False) + ) + + +def delete_statistics_short_term_rows( + short_term_statistics: Iterable[int], +) -> StatementLambdaElement: + """Delete statistics_short_term rows.""" + return lambda_stmt( + lambda: delete(StatisticsShortTerm) + .where(StatisticsShortTerm.id.in_(short_term_statistics)) + .execution_options(synchronize_session=False) + ) + + +def delete_event_rows( + event_ids: Iterable[int], +) -> StatementLambdaElement: + """Delete statistics_short_term rows.""" + return lambda_stmt( + lambda: delete(Events) + .where(Events.event_id.in_(event_ids)) + .execution_options(synchronize_session=False) + ) + + +def delete_recorder_runs_rows( + purge_before: datetime, current_run_id: int +) -> StatementLambdaElement: + """Delete recorder_runs rows.""" + return lambda_stmt( + lambda: delete(RecorderRuns) + .filter(RecorderRuns.start < purge_before) + .filter(RecorderRuns.run_id != current_run_id) + .execution_options(synchronize_session=False) + ) + + +def find_events_to_purge(purge_before: datetime) -> StatementLambdaElement: + """Find events to purge.""" + return lambda_stmt( + lambda: select(Events.event_id, Events.data_id) + .filter(Events.time_fired < purge_before) + .limit(MAX_ROWS_TO_PURGE) + ) + + +def find_states_to_purge(purge_before: datetime) -> StatementLambdaElement: + """Find states to purge.""" + return lambda_stmt( + lambda: select(States.state_id, States.attributes_id) + .filter(States.last_updated < purge_before) + .limit(MAX_ROWS_TO_PURGE) + ) + + +def find_short_term_statistics_to_purge( + purge_before: datetime, +) -> StatementLambdaElement: + """Find short term statistics to purge.""" + return lambda_stmt( + lambda: select(StatisticsShortTerm.id) + .filter(StatisticsShortTerm.start < purge_before) + .limit(MAX_ROWS_TO_PURGE) + ) + + +def find_statistics_runs_to_purge( + purge_before: datetime, +) -> StatementLambdaElement: + """Find statistics_runs to purge.""" + return lambda_stmt( + lambda: select(StatisticsRuns.run_id) + .filter(StatisticsRuns.start < purge_before) + .limit(MAX_ROWS_TO_PURGE) + ) + + +def find_latest_statistics_runs_run_id() -> StatementLambdaElement: + """Find the latest statistics_runs run_id.""" + return lambda_stmt(lambda: select(func.max(StatisticsRuns.run_id))) From 0b25b44820c6c1ee3a9ff22d46daa6a8e6e77245 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 8 May 2022 15:21:18 -0600 Subject: [PATCH 0309/3516] Bump simplisafe-python to 2022.05.1 (#71545) * Bump simplisafe-python to 2022.05.1 * Trigger Build --- 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 804523b3390..cb1b02e37ae 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==2022.05.0"], + "requirements": ["simplisafe-python==2022.05.1"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 09a6a61bd11..1eb22ec11d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2156,7 +2156,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.05.0 +simplisafe-python==2022.05.1 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 46daf2c2744..8d2061a6187 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1407,7 +1407,7 @@ sharkiq==0.0.1 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2022.05.0 +simplisafe-python==2022.05.1 # homeassistant.components.slack slackclient==2.5.0 From 5e737bfe4fbc5a724f5fdf04ea9319c2224cb114 Mon Sep 17 00:00:00 2001 From: Shawn Saenger Date: Sun, 8 May 2022 15:52:39 -0600 Subject: [PATCH 0310/3516] Add ws66i core integration (#56094) * Add ws66i core integration * Remove all ws66i translations * Update ws66i unit tests to meet minimum code coverage * Update ws66i based on @bdraco review * General improvements after 2nd PR review * Disable entities if amp shutoff, set default source names, set 30sec polling * Add _attr_ and change async_on_unload * Improve entity generation * Implement coordinator * Made options fields required, retry connection on failed attempts, use ZoneStatus for attributes * Refactor WS66i entity properties, raise HomeAssistantError on restore service if no snapshot * Update to pyws66i v1.1 * Add quality scale of silver to manifest * Update config_flow test --- CODEOWNERS | 2 + homeassistant/components/ws66i/__init__.py | 124 ++++ homeassistant/components/ws66i/config_flow.py | 146 ++++ homeassistant/components/ws66i/const.py | 24 + homeassistant/components/ws66i/coordinator.py | 53 ++ homeassistant/components/ws66i/manifest.json | 10 + .../components/ws66i/media_player.py | 213 ++++++ homeassistant/components/ws66i/models.py | 30 + homeassistant/components/ws66i/services.yaml | 15 + homeassistant/components/ws66i/strings.json | 34 + .../components/ws66i/translations/en.json | 35 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/ws66i/__init__.py | 1 + tests/components/ws66i/test_config_flow.py | 152 ++++ tests/components/ws66i/test_media_player.py | 692 ++++++++++++++++++ 17 files changed, 1538 insertions(+) create mode 100644 homeassistant/components/ws66i/__init__.py create mode 100644 homeassistant/components/ws66i/config_flow.py create mode 100644 homeassistant/components/ws66i/const.py create mode 100644 homeassistant/components/ws66i/coordinator.py create mode 100644 homeassistant/components/ws66i/manifest.json create mode 100644 homeassistant/components/ws66i/media_player.py create mode 100644 homeassistant/components/ws66i/models.py create mode 100644 homeassistant/components/ws66i/services.yaml create mode 100644 homeassistant/components/ws66i/strings.json create mode 100644 homeassistant/components/ws66i/translations/en.json create mode 100644 tests/components/ws66i/__init__.py create mode 100644 tests/components/ws66i/test_config_flow.py create mode 100644 tests/components/ws66i/test_media_player.py diff --git a/CODEOWNERS b/CODEOWNERS index fe5a460e9ee..ecb9115480f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1172,6 +1172,8 @@ build.json @home-assistant/supervisor /tests/components/workday/ @fabaff /homeassistant/components/worldclock/ @fabaff /tests/components/worldclock/ @fabaff +/homeassistant/components/ws66i/ @ssaenger +/tests/components/ws66i/ @ssaenger /homeassistant/components/xbox/ @hunterjm /tests/components/xbox/ @hunterjm /homeassistant/components/xbox_live/ @MartinHjelmare diff --git a/homeassistant/components/ws66i/__init__.py b/homeassistant/components/ws66i/__init__.py new file mode 100644 index 00000000000..232c4390f19 --- /dev/null +++ b/homeassistant/components/ws66i/__init__.py @@ -0,0 +1,124 @@ +"""The Soundavo WS66i 6-Zone Amplifier integration.""" +from __future__ import annotations + +import logging + +from pyws66i import WS66i, get_ws66i + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_IP_ADDRESS, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import CONF_SOURCES, DOMAIN +from .coordinator import Ws66iDataUpdateCoordinator +from .models import SourceRep, Ws66iData + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["media_player"] + + +@callback +def _get_sources_from_dict(data) -> SourceRep: + sources_config = data[CONF_SOURCES] + + # Dict index to custom name + source_id_name = {int(index): name for index, name in sources_config.items()} + + # Dict custom name to index + source_name_id = {v: k for k, v in source_id_name.items()} + + # List of custom names + source_names = sorted(source_name_id.keys(), key=lambda v: source_name_id[v]) + + return SourceRep(source_id_name, source_name_id, source_names) + + +def _find_zones(hass: HomeAssistant, ws66i: WS66i) -> list[int]: + """Generate zones list by searching for presence of zones.""" + # Zones 11 - 16 are the master amp + # Zones 21,31 - 26,36 are the daisy-chained amps + zone_list = [] + for amp_num in range(1, 4): + + if amp_num > 1: + # Don't add entities that aren't present + status = ws66i.zone_status(amp_num * 10 + 1) + if status is None: + break + + for zone_num in range(1, 7): + zone_id = (amp_num * 10) + zone_num + zone_list.append(zone_id) + + _LOGGER.info("Detected %d amp(s)", amp_num - 1) + return zone_list + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Soundavo WS66i 6-Zone Amplifier from a config entry.""" + # Get the source names from the options flow + options: dict[str, dict[str, str]] + options = {CONF_SOURCES: entry.options[CONF_SOURCES]} + # Get the WS66i object and open up a connection to it + ws66i = get_ws66i(entry.data[CONF_IP_ADDRESS]) + try: + await hass.async_add_executor_job(ws66i.open) + except ConnectionError as err: + # Amplifier is probably turned off + raise ConfigEntryNotReady("Could not connect to WS66i Amp. Is it off?") from err + + # Create the zone Representation dataclass + source_rep: SourceRep = _get_sources_from_dict(options) + + # Create a list of discovered zones + zones = await hass.async_add_executor_job(_find_zones, hass, ws66i) + + # Create the coordinator for the WS66i + coordinator: Ws66iDataUpdateCoordinator = Ws66iDataUpdateCoordinator( + hass, + ws66i, + zones, + ) + + # Fetch initial data, retry on failed poll + await coordinator.async_config_entry_first_refresh() + + # Create the Ws66iData data class save it to hass + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = Ws66iData( + host_ip=entry.data[CONF_IP_ADDRESS], + device=ws66i, + sources=source_rep, + coordinator=coordinator, + zones=zones, + ) + + def shutdown(event): + """Close the WS66i connection to the amplifier and save snapshots.""" + ws66i.close() + + entry.async_on_unload(entry.add_update_listener(_update_listener)) + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) + ) + + 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: + ws66i: WS66i = hass.data[DOMAIN][entry.entry_id].device + ws66i.close() + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/ws66i/config_flow.py b/homeassistant/components/ws66i/config_flow.py new file mode 100644 index 00000000000..a8f098faadd --- /dev/null +++ b/homeassistant/components/ws66i/config_flow.py @@ -0,0 +1,146 @@ +"""Config flow for WS66i 6-Zone Amplifier integration.""" +import logging + +from pyws66i import WS66i, get_ws66i +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_IP_ADDRESS + +from .const import ( + CONF_SOURCE_1, + CONF_SOURCE_2, + CONF_SOURCE_3, + CONF_SOURCE_4, + CONF_SOURCE_5, + CONF_SOURCE_6, + CONF_SOURCES, + DOMAIN, + INIT_OPTIONS_DEFAULT, +) + +_LOGGER = logging.getLogger(__name__) + +SOURCES = [ + CONF_SOURCE_1, + CONF_SOURCE_2, + CONF_SOURCE_3, + CONF_SOURCE_4, + CONF_SOURCE_5, + CONF_SOURCE_6, +] + +OPTIONS_SCHEMA = {vol.Optional(source): str for source in SOURCES} + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_IP_ADDRESS): str}) + +FIRST_ZONE = 11 + + +@core.callback +def _sources_from_config(data): + sources_config = { + str(idx + 1): data.get(source) for idx, source in enumerate(SOURCES) + } + + return { + index: name.strip() + for index, name in sources_config.items() + if (name is not None and name.strip() != "") + } + + +async def validate_input(hass: core.HomeAssistant, input_data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + ws66i: WS66i = get_ws66i(input_data[CONF_IP_ADDRESS]) + await hass.async_add_executor_job(ws66i.open) + # No exception. run a simple test to make sure we opened correct port + # Test on FIRST_ZONE because this zone will always be valid + ret_val = await hass.async_add_executor_job(ws66i.zone_status, FIRST_ZONE) + if ret_val is None: + ws66i.close() + raise ConnectionError("Not a valid WS66i connection") + + # Validation done. No issues. Close the connection + ws66i.close() + + # Return info that you want to store in the config entry. + return {CONF_IP_ADDRESS: input_data[CONF_IP_ADDRESS]} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for WS66i 6-Zone Amplifier.""" + + 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) + # Data is valid. Add default values for options flow. + return self.async_create_entry( + title="WS66i Amp", + data=info, + options={CONF_SOURCES: INIT_OPTIONS_DEFAULT}, + ) + except ConnectionError: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + @staticmethod + @core.callback + def async_get_options_flow(config_entry): + """Define the config flow to handle options.""" + return Ws66iOptionsFlowHandler(config_entry) + + +@core.callback +def _key_for_source(index, source, previous_sources): + key = vol.Required( + source, description={"suggested_value": previous_sources[str(index)]} + ) + + return key + + +class Ws66iOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a WS66i options flow.""" + + def __init__(self, config_entry): + """Initialize.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry( + title="Source Names", + data={CONF_SOURCES: _sources_from_config(user_input)}, + ) + + # Fill form with previous source names + previous_sources = self.config_entry.options[CONF_SOURCES] + options = { + _key_for_source(idx + 1, source, previous_sources): str + for idx, source in enumerate(SOURCES) + } + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema(options), + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/ws66i/const.py b/homeassistant/components/ws66i/const.py new file mode 100644 index 00000000000..ec4439a690d --- /dev/null +++ b/homeassistant/components/ws66i/const.py @@ -0,0 +1,24 @@ +"""Constants for the Soundavo WS66i 6-Zone Amplifier Media Player component.""" + +DOMAIN = "ws66i" + +CONF_SOURCES = "sources" + +CONF_SOURCE_1 = "source_1" +CONF_SOURCE_2 = "source_2" +CONF_SOURCE_3 = "source_3" +CONF_SOURCE_4 = "source_4" +CONF_SOURCE_5 = "source_5" +CONF_SOURCE_6 = "source_6" + +INIT_OPTIONS_DEFAULT = { + "1": "Source 1", + "2": "Source 2", + "3": "Source 3", + "4": "Source 4", + "5": "Source 5", + "6": "Source 6", +} + +SERVICE_SNAPSHOT = "snapshot" +SERVICE_RESTORE = "restore" diff --git a/homeassistant/components/ws66i/coordinator.py b/homeassistant/components/ws66i/coordinator.py new file mode 100644 index 00000000000..a9a274756b5 --- /dev/null +++ b/homeassistant/components/ws66i/coordinator.py @@ -0,0 +1,53 @@ +"""Coordinator for WS66i.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from pyws66i import WS66i, ZoneStatus + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +_LOGGER = logging.getLogger(__name__) + +POLL_INTERVAL = timedelta(seconds=30) + + +class Ws66iDataUpdateCoordinator(DataUpdateCoordinator): + """DataUpdateCoordinator to gather data for WS66i Zones.""" + + def __init__( + self, + hass: HomeAssistant, + my_api: WS66i, + zones: list[int], + ) -> None: + """Initialize DataUpdateCoordinator to gather data for specific zones.""" + super().__init__( + hass, + _LOGGER, + name="WS66i", + update_interval=POLL_INTERVAL, + ) + self._ws66i = my_api + self._zones = zones + + def _update_all_zones(self) -> list[ZoneStatus]: + """Fetch data for each of the zones.""" + data = [] + for zone_id in self._zones: + data_zone = self._ws66i.zone_status(zone_id) + if data_zone is None: + raise UpdateFailed(f"Failed to update zone {zone_id}") + + data.append(data_zone) + + # HA will call my entity's _handle_coordinator_update() + return data + + async def _async_update_data(self) -> list[ZoneStatus]: + """Fetch data for each of the zones.""" + # HA will call my entity's _handle_coordinator_update() + # The data I pass back here can be accessed through coordinator.data. + return await self.hass.async_add_executor_job(self._update_all_zones) diff --git a/homeassistant/components/ws66i/manifest.json b/homeassistant/components/ws66i/manifest.json new file mode 100644 index 00000000000..abd943eb26b --- /dev/null +++ b/homeassistant/components/ws66i/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "ws66i", + "name": "Soundavo WS66i 6-Zone Amplifier", + "documentation": "https://www.home-assistant.io/integrations/ws66i", + "requirements": ["pyws66i==1.1"], + "codeowners": ["@ssaenger"], + "config_flow": true, + "quality_scale": "silver", + "iot_class": "local_polling" +} diff --git a/homeassistant/components/ws66i/media_player.py b/homeassistant/components/ws66i/media_player.py new file mode 100644 index 00000000000..2f485748c02 --- /dev/null +++ b/homeassistant/components/ws66i/media_player.py @@ -0,0 +1,213 @@ +"""Support for interfacing with WS66i 6 zone home audio controller.""" +from copy import deepcopy +import logging + +from pyws66i import WS66i, ZoneStatus + +from homeassistant.components.media_player import MediaPlayerEntity +from homeassistant.components.media_player.const import ( + SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import ( + AddEntitiesCallback, + async_get_current_platform, +) +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT +from .coordinator import Ws66iDataUpdateCoordinator +from .models import Ws66iData + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 1 + +SUPPORT_WS66I = ( + SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_STEP + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_SELECT_SOURCE +) + +MAX_VOL = 38 + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the WS66i 6-zone amplifier platform from a config entry.""" + ws66i_data: Ws66iData = hass.data[DOMAIN][config_entry.entry_id] + + # Build and add the entities from the data class + async_add_entities( + Ws66iZone( + device=ws66i_data.device, + ws66i_data=ws66i_data, + entry_id=config_entry.entry_id, + zone_id=zone_id, + data_idx=idx, + coordinator=ws66i_data.coordinator, + ) + for idx, zone_id in enumerate(ws66i_data.zones) + ) + + # Set up services + platform = async_get_current_platform() + + platform.async_register_entity_service( + SERVICE_SNAPSHOT, + {}, + "snapshot", + ) + + platform.async_register_entity_service( + SERVICE_RESTORE, + {}, + "async_restore", + ) + + +class Ws66iZone(CoordinatorEntity, MediaPlayerEntity): + """Representation of a WS66i amplifier zone.""" + + def __init__( + self, + device: WS66i, + ws66i_data: Ws66iData, + entry_id: str, + zone_id: int, + data_idx: int, + coordinator: Ws66iDataUpdateCoordinator, + ) -> None: + """Initialize a zone entity.""" + super().__init__(coordinator) + self._ws66i: WS66i = device + self._ws66i_data: Ws66iData = ws66i_data + self._zone_id: int = zone_id + self._zone_id_idx: int = data_idx + self._coordinator = coordinator + self._snapshot: ZoneStatus = None + self._status: ZoneStatus = coordinator.data[data_idx] + self._attr_source_list = ws66i_data.sources.name_list + self._attr_unique_id = f"{entry_id}_{self._zone_id}" + self._attr_name = f"Zone {self._zone_id}" + self._attr_supported_features = SUPPORT_WS66I + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, str(self.unique_id))}, + name=self.name, + manufacturer="Soundavo", + model="WS66i 6-Zone Amplifier", + ) + self._set_attrs_from_status() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + # This will be called for each of the entities after the coordinator + # finishes executing _async_update_data() + + # Save a reference to the zone status that this entity represents + self._status = self.coordinator.data[self._zone_id_idx] + self._set_attrs_from_status() + + # Parent will notify HA of the update + super()._handle_coordinator_update() + + @callback + def _set_attrs_from_status(self) -> None: + status = self._status + sources = self._ws66i_data.sources.id_name + self._attr_state = STATE_ON if status.power else STATE_OFF + self._attr_volume_level = status.volume / float(MAX_VOL) + self._attr_is_volume_muted = status.mute + self._attr_source = self._attr_media_title = sources[status.source] + + @callback + def _async_update_attrs_write_ha_state(self) -> None: + self._set_attrs_from_status() + self.async_write_ha_state() + + @callback + def snapshot(self): + """Save zone's current state.""" + self._snapshot = deepcopy(self._status) + + async def async_restore(self): + """Restore saved state.""" + if not self._snapshot: + raise HomeAssistantError("There is no snapshot to restore") + + await self.hass.async_add_executor_job(self._ws66i.restore_zone, self._snapshot) + self._status = self._snapshot + self._async_update_attrs_write_ha_state() + + async def async_select_source(self, source): + """Set input source.""" + idx = self._ws66i_data.sources.name_id[source] + await self.hass.async_add_executor_job( + self._ws66i.set_source, self._zone_id, idx + ) + self._status.source = idx + self._async_update_attrs_write_ha_state() + + async def async_turn_on(self): + """Turn the media player on.""" + await self.hass.async_add_executor_job( + self._ws66i.set_power, self._zone_id, True + ) + self._status.power = True + self._async_update_attrs_write_ha_state() + + async def async_turn_off(self): + """Turn the media player off.""" + await self.hass.async_add_executor_job( + self._ws66i.set_power, self._zone_id, False + ) + self._status.power = False + self._async_update_attrs_write_ha_state() + + async def async_mute_volume(self, mute): + """Mute (true) or unmute (false) media player.""" + await self.hass.async_add_executor_job( + self._ws66i.set_mute, self._zone_id, mute + ) + self._status.mute = bool(mute) + self._async_update_attrs_write_ha_state() + + async def async_set_volume_level(self, volume): + """Set volume level, range 0..1.""" + await self.hass.async_add_executor_job( + self._ws66i.set_volume, self._zone_id, int(volume * MAX_VOL) + ) + self._status.volume = int(volume * MAX_VOL) + self._async_update_attrs_write_ha_state() + + async def async_volume_up(self): + """Volume up the media player.""" + await self.hass.async_add_executor_job( + self._ws66i.set_volume, self._zone_id, min(self._status.volume + 1, MAX_VOL) + ) + self._status.volume = min(self._status.volume + 1, MAX_VOL) + self._async_update_attrs_write_ha_state() + + async def async_volume_down(self): + """Volume down media player.""" + await self.hass.async_add_executor_job( + self._ws66i.set_volume, self._zone_id, max(self._status.volume - 1, 0) + ) + self._status.volume = max(self._status.volume - 1, 0) + self._async_update_attrs_write_ha_state() diff --git a/homeassistant/components/ws66i/models.py b/homeassistant/components/ws66i/models.py new file mode 100644 index 00000000000..d84ee56a4a1 --- /dev/null +++ b/homeassistant/components/ws66i/models.py @@ -0,0 +1,30 @@ +"""The ws66i integration models.""" +from __future__ import annotations + +from dataclasses import dataclass + +from pyws66i import WS66i + +from .coordinator import Ws66iDataUpdateCoordinator + +# A dataclass is basically a struct in C/C++ + + +@dataclass +class SourceRep: + """Different representations of the amp sources.""" + + id_name: dict[int, str] + name_id: dict[str, int] + name_list: list[str] + + +@dataclass +class Ws66iData: + """Data for the ws66i integration.""" + + host_ip: str + device: WS66i + sources: SourceRep + coordinator: Ws66iDataUpdateCoordinator + zones: list[int] diff --git a/homeassistant/components/ws66i/services.yaml b/homeassistant/components/ws66i/services.yaml new file mode 100644 index 00000000000..cedd1d3546a --- /dev/null +++ b/homeassistant/components/ws66i/services.yaml @@ -0,0 +1,15 @@ +snapshot: + name: Snapshot + description: Take a snapshot of the media player zone. + target: + entity: + integration: ws66i + domain: media_player + +restore: + name: Restore + description: Restore a snapshot of the media player zone. + target: + entity: + integration: ws66i + domain: media_player diff --git a/homeassistant/components/ws66i/strings.json b/homeassistant/components/ws66i/strings.json new file mode 100644 index 00000000000..fcfa64d7e22 --- /dev/null +++ b/homeassistant/components/ws66i/strings.json @@ -0,0 +1,34 @@ +{ + "config": { + "step": { + "user": { + "title": "Connect to the device", + "data": { + "ip_address": "[%key:common::config_flow::data::ip%]" + } + } + }, + "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%]" + } + }, + "options": { + "step": { + "init": { + "title": "Configure sources", + "data": { + "source_1": "Name of source #1", + "source_2": "Name of source #2", + "source_3": "Name of source #3", + "source_4": "Name of source #4", + "source_5": "Name of source #5", + "source_6": "Name of source #6" + } + } + } + } +} diff --git a/homeassistant/components/ws66i/translations/en.json b/homeassistant/components/ws66i/translations/en.json new file mode 100644 index 00000000000..7f6a04c1ea3 --- /dev/null +++ b/homeassistant/components/ws66i/translations/en.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "ip_address": "IP Address" + }, + "title": "Connect to the device" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Name of source #1", + "source_2": "Name of source #2", + "source_3": "Name of source #3", + "source_4": "Name of source #4", + "source_5": "Name of source #5", + "source_6": "Name of source #6" + }, + "title": "Configure sources" + } + } + } +} + diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 510adc74e61..4bcfa0dbbef 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -400,6 +400,7 @@ FLOWS = { "wiz", "wled", "wolflink", + "ws66i", "xbox", "xiaomi_aqara", "xiaomi_miio", diff --git a/requirements_all.txt b/requirements_all.txt index 1eb22ec11d8..9dbb2751aac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2022,6 +2022,9 @@ pywilight==0.0.70 # homeassistant.components.wiz pywizlight==0.5.13 +# homeassistant.components.ws66i +pyws66i==1.1 + # homeassistant.components.xeoma pyxeoma==1.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d2061a6187..8bdfa3f3484 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1336,6 +1336,9 @@ pywilight==0.0.70 # homeassistant.components.wiz pywizlight==0.5.13 +# homeassistant.components.ws66i +pyws66i==1.1 + # homeassistant.components.zerproc pyzerproc==0.4.8 diff --git a/tests/components/ws66i/__init__.py b/tests/components/ws66i/__init__.py new file mode 100644 index 00000000000..3106b858d0c --- /dev/null +++ b/tests/components/ws66i/__init__.py @@ -0,0 +1 @@ +"""Tests for the ws66i component.""" diff --git a/tests/components/ws66i/test_config_flow.py b/tests/components/ws66i/test_config_flow.py new file mode 100644 index 00000000000..d426e62c012 --- /dev/null +++ b/tests/components/ws66i/test_config_flow.py @@ -0,0 +1,152 @@ +"""Test the WS66i 6-Zone Amplifier config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.ws66i.const import ( + CONF_SOURCE_1, + CONF_SOURCE_2, + CONF_SOURCE_3, + CONF_SOURCE_4, + CONF_SOURCE_5, + CONF_SOURCE_6, + CONF_SOURCES, + DOMAIN, + INIT_OPTIONS_DEFAULT, +) +from homeassistant.const import CONF_IP_ADDRESS + +from tests.common import MockConfigEntry +from tests.components.ws66i.test_media_player import AttrDict + +CONFIG = {CONF_IP_ADDRESS: "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} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.ws66i.config_flow.get_ws66i", + ) as mock_ws66i, patch( + "homeassistant.components.ws66i.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + + ws66i_instance = mock_ws66i.return_value + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG + ) + await hass.async_block_till_done() + + ws66i_instance.open.assert_called_once() + ws66i_instance.close.assert_called_once() + + assert result2["type"] == "create_entry" + assert result2["title"] == "WS66i Amp" + assert result2["data"] == {CONF_IP_ADDRESS: CONFIG[CONF_IP_ADDRESS]} + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass): + """Test cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("homeassistant.components.ws66i.config_flow.get_ws66i") as mock_ws66i: + ws66i_instance = mock_ws66i.return_value + ws66i_instance.open.side_effect = ConnectionError + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_wrong_ip(hass): + """Test cannot connect error with bad IP.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("homeassistant.components.ws66i.config_flow.get_ws66i") as mock_ws66i: + ws66i_instance = mock_ws66i.return_value + ws66i_instance.zone_status.return_value = None + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_generic_exception(hass): + """Test generic exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("homeassistant.components.ws66i.config_flow.get_ws66i") as mock_ws66i: + ws66i_instance = mock_ws66i.return_value + ws66i_instance.open.side_effect = Exception + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_options_flow(hass): + """Test config flow options.""" + conf = {CONF_IP_ADDRESS: "1.1.1.1", CONF_SOURCES: INIT_OPTIONS_DEFAULT} + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=conf, + options={CONF_SOURCES: INIT_OPTIONS_DEFAULT}, + ) + config_entry.add_to_hass(hass) + + with patch("homeassistant.components.ws66i.get_ws66i") as mock_ws66i: + ws66i_instance = mock_ws66i.return_value + ws66i_instance.zone_status.return_value = AttrDict( + power=True, volume=0, mute=True, source=1, treble=0, bass=0, balance=10 + ) + 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={ + CONF_SOURCE_1: "one", + CONF_SOURCE_2: "too", + CONF_SOURCE_3: "tree", + CONF_SOURCE_4: "for", + CONF_SOURCE_5: "feeve", + CONF_SOURCE_6: "roku", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options[CONF_SOURCES] == { + "1": "one", + "2": "too", + "3": "tree", + "4": "for", + "5": "feeve", + "6": "roku", + } diff --git a/tests/components/ws66i/test_media_player.py b/tests/components/ws66i/test_media_player.py new file mode 100644 index 00000000000..6fc1e00d827 --- /dev/null +++ b/tests/components/ws66i/test_media_player.py @@ -0,0 +1,692 @@ +"""The tests for WS66i Media player platform.""" +from collections import defaultdict +from unittest.mock import patch + +import pytest + +from homeassistant.components.media_player.const import ( + ATTR_INPUT_SOURCE, + ATTR_INPUT_SOURCE_LIST, + ATTR_MEDIA_VOLUME_LEVEL, + DOMAIN as MEDIA_PLAYER_DOMAIN, + SERVICE_SELECT_SOURCE, + SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, +) +from homeassistant.components.ws66i.const import ( + CONF_SOURCES, + DOMAIN, + INIT_OPTIONS_DEFAULT, + SERVICE_RESTORE, + SERVICE_SNAPSHOT, +) +from homeassistant.const import ( + CONF_IP_ADDRESS, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_SET, + SERVICE_VOLUME_UP, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + +MOCK_SOURCE_DIC = { + "1": "one", + "2": "two", + "3": "three", + "4": "four", + "5": "five", + "6": "six", +} +MOCK_CONFIG = {CONF_IP_ADDRESS: "fake ip"} +MOCK_OPTIONS = {CONF_SOURCES: MOCK_SOURCE_DIC} +MOCK_DEFAULT_OPTIONS = {CONF_SOURCES: INIT_OPTIONS_DEFAULT} + +ZONE_1_ID = "media_player.zone_11" +ZONE_2_ID = "media_player.zone_12" +ZONE_7_ID = "media_player.zone_21" + + +class AttrDict(dict): + """Helper class for mocking attributes.""" + + def __setattr__(self, name, value): + """Set attribute.""" + self[name] = value + + def __getattr__(self, item): + """Get attribute.""" + try: + return self[item] + except KeyError as err: + # The reason for doing this is because of the deepcopy in my code + raise AttributeError(item) from err + + +class MockWs66i: + """Mock for pyws66i object.""" + + def __init__(self, fail_open=False, fail_zone_check=None): + """Init mock object.""" + self.zones = defaultdict( + lambda: AttrDict( + power=True, volume=0, mute=True, source=1, treble=0, bass=0, balance=10 + ) + ) + self.fail_open = fail_open + self.fail_zone_check = fail_zone_check + + def open(self): + """Open socket. Do nothing.""" + if self.fail_open is True: + raise ConnectionError() + + def close(self): + """Close socket. Do nothing.""" + + def zone_status(self, zone_id): + """Get zone status.""" + if self.fail_zone_check is not None and zone_id in self.fail_zone_check: + return None + status = self.zones[zone_id] + status.zone = zone_id + return AttrDict(status) + + def set_source(self, zone_id, source_idx): + """Set source for zone.""" + self.zones[zone_id].source = source_idx + + def set_power(self, zone_id, power): + """Turn zone on/off.""" + self.zones[zone_id].power = power + + def set_mute(self, zone_id, mute): + """Mute/unmute zone.""" + self.zones[zone_id].mute = mute + + def set_volume(self, zone_id, volume): + """Set volume for zone.""" + self.zones[zone_id].volume = volume + + def restore_zone(self, zone): + """Restore zone status.""" + self.zones[zone.zone] = AttrDict(zone) + + +async def test_setup_success(hass): + """Test connection success.""" + with patch( + "homeassistant.components.ws66i.get_ws66i", + new=lambda *a: MockWs66i(), + ): + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.states.get(ZONE_1_ID) is not None + + +async def _setup_ws66i(hass, ws66i) -> MockConfigEntry: + with patch( + "homeassistant.components.ws66i.get_ws66i", + new=lambda *a: ws66i, + ): + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_DEFAULT_OPTIONS + ) + 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 + + +async def _setup_ws66i_with_options(hass, ws66i) -> MockConfigEntry: + with patch( + "homeassistant.components.ws66i.get_ws66i", + new=lambda *a: ws66i, + ): + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS + ) + 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 + + +async def _call_media_player_service(hass, name, data): + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, name, service_data=data, blocking=True + ) + + +async def _call_ws66i_service(hass, name, data): + await hass.services.async_call(DOMAIN, name, service_data=data, blocking=True) + + +async def test_cannot_connect(hass): + """Test connection error.""" + with patch( + "homeassistant.components.ws66i.get_ws66i", + new=lambda *a: MockWs66i(fail_open=True), + ): + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert hass.states.get(ZONE_1_ID) is None + + +async def test_cannot_connect_2(hass): + """Test connection error pt 2.""" + # Another way to test same case as test_cannot_connect + ws66i = MockWs66i() + + with patch.object(MockWs66i, "open", side_effect=ConnectionError): + await _setup_ws66i(hass, ws66i) + assert hass.states.get(ZONE_1_ID) is None + + +async def test_service_calls_with_entity_id(hass): + """Test snapshot save/restore service calls.""" + _ = await _setup_ws66i_with_options(hass, MockWs66i()) + + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} + ) + + # Saving existing values + await _call_ws66i_service(hass, SERVICE_SNAPSHOT, {"entity_id": ZONE_1_ID}) + + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "three"} + ) + await hass.async_block_till_done() + + # Restoring other media player to its previous state + # The zone should not be restored + with pytest.raises(HomeAssistantError): + await _call_ws66i_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_2_ID}) + await hass.async_block_till_done() + + # Checking that values were not (!) restored + state = hass.states.get(ZONE_1_ID) + + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 1.0 + assert state.attributes[ATTR_INPUT_SOURCE] == "three" + + # Restoring media player to its previous state + await _call_ws66i_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID}) + await hass.async_block_till_done() + + state = hass.states.get(ZONE_1_ID) + + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.0 + assert state.attributes[ATTR_INPUT_SOURCE] == "one" + + +async def test_service_calls_with_all_entities(hass): + """Test snapshot save/restore service calls with entity id all.""" + _ = await _setup_ws66i_with_options(hass, MockWs66i()) + + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} + ) + + # Saving existing values + await _call_ws66i_service(hass, SERVICE_SNAPSHOT, {"entity_id": "all"}) + + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "three"} + ) + + # await coordinator.async_refresh() + # await hass.async_block_till_done() + + # Restoring media player to its previous state + await _call_ws66i_service(hass, SERVICE_RESTORE, {"entity_id": "all"}) + await hass.async_block_till_done() + + state = hass.states.get(ZONE_1_ID) + + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.0 + assert state.attributes[ATTR_INPUT_SOURCE] == "one" + + +async def test_service_calls_without_relevant_entities(hass): + """Test snapshot save/restore service calls with bad entity id.""" + config_entry = await _setup_ws66i_with_options(hass, MockWs66i()) + + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} + ) + + ws66i_data = hass.data[DOMAIN][config_entry.entry_id] + coordinator = ws66i_data.coordinator + await coordinator.async_refresh() + await hass.async_block_till_done() + + # Saving existing values + await _call_ws66i_service(hass, SERVICE_SNAPSHOT, {"entity_id": "all"}) + + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "three"} + ) + + await coordinator.async_refresh() + await hass.async_block_till_done() + + # Restoring media player to its previous state + await _call_ws66i_service(hass, SERVICE_RESTORE, {"entity_id": "light.demo"}) + await hass.async_block_till_done() + + state = hass.states.get(ZONE_1_ID) + + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 1.0 + assert state.attributes[ATTR_INPUT_SOURCE] == "three" + + +async def test_restore_without_snapshot(hass): + """Test restore when snapshot wasn't called.""" + await _setup_ws66i(hass, MockWs66i()) + + with patch.object(MockWs66i, "restore_zone") as method_call: + with pytest.raises(HomeAssistantError): + await _call_ws66i_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID}) + await hass.async_block_till_done() + + assert not method_call.called + + +async def test_update(hass): + """Test updating values from ws66i.""" + ws66i = MockWs66i() + config_entry = await _setup_ws66i_with_options(hass, ws66i) + + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} + ) + + ws66i.set_source(11, 3) + ws66i.set_volume(11, 38) + + ws66i_data = hass.data[DOMAIN][config_entry.entry_id] + coordinator = ws66i_data.coordinator + + with patch.object(MockWs66i, "open") as method_call: + await coordinator.async_refresh() + await hass.async_block_till_done() + + assert not method_call.called + + state = hass.states.get(ZONE_1_ID) + + assert hass.states.is_state(ZONE_1_ID, STATE_ON) + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 1.0 + assert state.attributes[ATTR_INPUT_SOURCE] == "three" + + +async def test_failed_update(hass): + """Test updating failure from ws66i.""" + ws66i = MockWs66i() + config_entry = await _setup_ws66i_with_options(hass, ws66i) + + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} + ) + + ws66i.set_source(11, 3) + ws66i.set_volume(11, 38) + ws66i_data = hass.data[DOMAIN][config_entry.entry_id] + coordinator = ws66i_data.coordinator + await coordinator.async_refresh() + await hass.async_block_till_done() + + # Failed update, close called + with patch.object(MockWs66i, "zone_status", return_value=None): + await coordinator.async_refresh() + await hass.async_block_till_done() + + assert hass.states.is_state(ZONE_1_ID, STATE_UNAVAILABLE) + + # A connection re-attempt fails + with patch.object(MockWs66i, "zone_status", return_value=None): + await coordinator.async_refresh() + await hass.async_block_till_done() + + # A connection re-attempt succeeds + await coordinator.async_refresh() + await hass.async_block_till_done() + + # confirm entity is back on + state = hass.states.get(ZONE_1_ID) + + assert hass.states.is_state(ZONE_1_ID, STATE_ON) + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 1.0 + assert state.attributes[ATTR_INPUT_SOURCE] == "three" + + +async def test_supported_features(hass): + """Test supported features property.""" + await _setup_ws66i(hass, MockWs66i()) + + state = hass.states.get(ZONE_1_ID) + assert ( + SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_STEP + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_SELECT_SOURCE + == state.attributes["supported_features"] + ) + + +async def test_source_list(hass): + """Test source list property.""" + await _setup_ws66i(hass, MockWs66i()) + + state = hass.states.get(ZONE_1_ID) + # Note, the list is sorted! + assert state.attributes[ATTR_INPUT_SOURCE_LIST] == list( + INIT_OPTIONS_DEFAULT.values() + ) + + +async def test_source_list_with_options(hass): + """Test source list property.""" + await _setup_ws66i_with_options(hass, MockWs66i()) + + state = hass.states.get(ZONE_1_ID) + # Note, the list is sorted! + assert state.attributes[ATTR_INPUT_SOURCE_LIST] == list(MOCK_SOURCE_DIC.values()) + + +async def test_select_source(hass): + """Test source selection methods.""" + ws66i = MockWs66i() + await _setup_ws66i_with_options(hass, ws66i) + + await _call_media_player_service( + hass, + SERVICE_SELECT_SOURCE, + {"entity_id": ZONE_1_ID, ATTR_INPUT_SOURCE: "three"}, + ) + assert ws66i.zones[11].source == 3 + + +async def test_source_select(hass): + """Test behavior when device has unknown source.""" + ws66i = MockWs66i() + config_entry = await _setup_ws66i_with_options(hass, ws66i) + + ws66i.set_source(11, 5) + + ws66i_data = hass.data[DOMAIN][config_entry.entry_id] + coordinator = ws66i_data.coordinator + await coordinator.async_refresh() + await hass.async_block_till_done() + + state = hass.states.get(ZONE_1_ID) + + assert state.attributes.get(ATTR_INPUT_SOURCE) == "five" + + +async def test_turn_on_off(hass): + """Test turning on the zone.""" + ws66i = MockWs66i() + await _setup_ws66i(hass, ws66i) + + await _call_media_player_service(hass, SERVICE_TURN_OFF, {"entity_id": ZONE_1_ID}) + assert not ws66i.zones[11].power + + await _call_media_player_service(hass, SERVICE_TURN_ON, {"entity_id": ZONE_1_ID}) + assert ws66i.zones[11].power + + +async def test_mute_volume(hass): + """Test mute functionality.""" + ws66i = MockWs66i() + await _setup_ws66i(hass, ws66i) + + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.5} + ) + await _call_media_player_service( + hass, SERVICE_VOLUME_MUTE, {"entity_id": ZONE_1_ID, "is_volume_muted": False} + ) + assert not ws66i.zones[11].mute + + await _call_media_player_service( + hass, SERVICE_VOLUME_MUTE, {"entity_id": ZONE_1_ID, "is_volume_muted": True} + ) + assert ws66i.zones[11].mute + + +async def test_volume_up_down(hass): + """Test increasing volume by one.""" + ws66i = MockWs66i() + config_entry = await _setup_ws66i(hass, ws66i) + + ws66i_data = hass.data[DOMAIN][config_entry.entry_id] + coordinator = ws66i_data.coordinator + + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} + ) + assert ws66i.zones[11].volume == 0 + + await _call_media_player_service( + hass, SERVICE_VOLUME_DOWN, {"entity_id": ZONE_1_ID} + ) + await coordinator.async_refresh() + await hass.async_block_till_done() + # should not go below zero + assert ws66i.zones[11].volume == 0 + + await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID}) + await coordinator.async_refresh() + await hass.async_block_till_done() + assert ws66i.zones[11].volume == 1 + + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} + ) + await coordinator.async_refresh() + await hass.async_block_till_done() + assert ws66i.zones[11].volume == 38 + + await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID}) + + await coordinator.async_refresh() + await hass.async_block_till_done() + # should not go above 38 + assert ws66i.zones[11].volume == 38 + + await _call_media_player_service( + hass, SERVICE_VOLUME_DOWN, {"entity_id": ZONE_1_ID} + ) + assert ws66i.zones[11].volume == 37 + + +async def test_first_run_with_available_zones(hass): + """Test first run with all zones available.""" + ws66i = MockWs66i() + await _setup_ws66i(hass, ws66i) + + registry = er.async_get(hass) + + entry = registry.async_get(ZONE_7_ID) + assert not entry.disabled + + +async def test_first_run_with_failing_zones(hass): + """Test first run with failed zones.""" + ws66i = MockWs66i() + + with patch.object(MockWs66i, "zone_status", return_value=None): + await _setup_ws66i(hass, ws66i) + + registry = er.async_get(hass) + + entry = registry.async_get(ZONE_1_ID) + assert entry is None + + entry = registry.async_get(ZONE_7_ID) + assert entry is None + + +async def test_register_all_entities(hass): + """Test run with all entities registered.""" + ws66i = MockWs66i() + await _setup_ws66i(hass, ws66i) + + registry = er.async_get(hass) + + entry = registry.async_get(ZONE_1_ID) + assert not entry.disabled + + entry = registry.async_get(ZONE_7_ID) + assert not entry.disabled + + +async def test_register_entities_in_1_amp_only(hass): + """Test run with only zones 11-16 registered.""" + ws66i = MockWs66i(fail_zone_check=[21]) + await _setup_ws66i(hass, ws66i) + + registry = er.async_get(hass) + + entry = registry.async_get(ZONE_1_ID) + assert not entry.disabled + + entry = registry.async_get(ZONE_2_ID) + assert not entry.disabled + + entry = registry.async_get(ZONE_7_ID) + assert entry is None + + +async def test_unload_config_entry(hass): + """Test unloading config entry.""" + with patch( + "homeassistant.components.ws66i.get_ws66i", + new=lambda *a: MockWs66i(), + ): + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN][config_entry.entry_id] + + with patch.object(MockWs66i, "close") as method_call: + await config_entry.async_unload(hass) + await hass.async_block_till_done() + + assert method_call.called + + assert not hass.data[DOMAIN] + + +async def test_restore_snapshot_on_reconnect(hass): + """Test restoring a saved snapshot when reconnecting to amp.""" + ws66i = MockWs66i() + config_entry = await _setup_ws66i_with_options(hass, ws66i) + + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} + ) + + # Save a snapshot + await _call_ws66i_service(hass, SERVICE_SNAPSHOT, {"entity_id": ZONE_1_ID}) + + ws66i_data = hass.data[DOMAIN][config_entry.entry_id] + coordinator = ws66i_data.coordinator + + # Failed update, + with patch.object(MockWs66i, "zone_status", return_value=None): + await coordinator.async_refresh() + await hass.async_block_till_done() + + assert hass.states.is_state(ZONE_1_ID, STATE_UNAVAILABLE) + + # A connection re-attempt succeeds + await coordinator.async_refresh() + await hass.async_block_till_done() + + # confirm entity is back on + state = hass.states.get(ZONE_1_ID) + + assert hass.states.is_state(ZONE_1_ID, STATE_ON) + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.0 + assert state.attributes[ATTR_INPUT_SOURCE] == "one" + + # Change states + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "six"} + ) + + # Now confirm that the snapshot before the disconnect works + await _call_ws66i_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID}) + await hass.async_block_till_done() + + state = hass.states.get(ZONE_1_ID) + + assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.0 + assert state.attributes[ATTR_INPUT_SOURCE] == "one" From 9a5e0281db476b598cadd421bfd1e6fc5b0582e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 9 May 2022 01:20:18 +0200 Subject: [PATCH 0311/3516] Add missing AEMET weather units (#70165) --- CODEOWNERS | 2 ++ homeassistant/components/aemet/manifest.json | 2 +- homeassistant/components/aemet/weather.py | 8 +++++--- tests/components/aemet/test_weather.py | 6 +++--- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index ecb9115480f..2e2a8c0584f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -32,6 +32,8 @@ build.json @home-assistant/supervisor /tests/components/adguard/ @frenck /homeassistant/components/advantage_air/ @Bre77 /tests/components/advantage_air/ @Bre77 +/homeassistant/components/aemet/ @Noltari +/tests/components/aemet/ @Noltari /homeassistant/components/agent_dvr/ @ispysoftware /tests/components/agent_dvr/ @ispysoftware /homeassistant/components/air_quality/ @home-assistant/core diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json index 087d5c38820..e530489476e 100644 --- a/homeassistant/components/aemet/manifest.json +++ b/homeassistant/components/aemet/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/aemet", "requirements": ["AEMET-OpenData==0.2.1"], - "codeowners": [], + "codeowners": ["@Noltari"], "iot_class": "cloud_polling", "loggers": ["aemet_opendata"] } diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index 2bbb4d0da55..d05442b621e 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -1,7 +1,7 @@ """Support for the AEMET OpenData service.""" from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -48,6 +48,8 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): _attr_attribution = ATTRIBUTION _attr_temperature_unit = TEMP_CELSIUS + _attr_pressure_unit = PRESSURE_HPA + _attr_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR def __init__( self, @@ -92,10 +94,10 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): @property def wind_bearing(self): - """Return the temperature.""" + """Return the wind bearing.""" return self.coordinator.data[ATTR_API_WIND_BEARING] @property def wind_speed(self): - """Return the temperature.""" + """Return the wind speed.""" return self.coordinator.data[ATTR_API_WIND_SPEED] diff --git a/tests/components/aemet/test_weather.py b/tests/components/aemet/test_weather.py index d1f1889c807..809b61e0bda 100644 --- a/tests/components/aemet/test_weather.py +++ b/tests/components/aemet/test_weather.py @@ -42,10 +42,10 @@ async def test_aemet_weather(hass): assert state.state == ATTR_CONDITION_SNOWY assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_WEATHER_HUMIDITY) == 99.0 - assert state.attributes.get(ATTR_WEATHER_PRESSURE) == 1004.4 + assert state.attributes.get(ATTR_WEATHER_PRESSURE) == 100440.0 assert state.attributes.get(ATTR_WEATHER_TEMPERATURE) == -0.7 assert state.attributes.get(ATTR_WEATHER_WIND_BEARING) == 90.0 - assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 15 + assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 4.17 forecast = state.attributes.get(ATTR_FORECAST)[0] assert forecast.get(ATTR_FORECAST_CONDITION) == ATTR_CONDITION_PARTLYCLOUDY assert forecast.get(ATTR_FORECAST_PRECIPITATION) is None @@ -57,7 +57,7 @@ async def test_aemet_weather(hass): == dt_util.parse_datetime("2021-01-10 00:00:00+00:00").isoformat() ) assert forecast.get(ATTR_FORECAST_WIND_BEARING) == 45.0 - assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 20 + assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 5.56 state = hass.states.get("weather.aemet_hourly") assert state is None From 1d63c2069ea94ea4ddec38967bc678d8fd11ca5b Mon Sep 17 00:00:00 2001 From: Fairesoimeme Date: Mon, 9 May 2022 01:27:09 +0200 Subject: [PATCH 0312/3516] Add ZiGate device on automatic integration USB and ZEROCONF (#68577) Co-authored-by: J. Nick Koston --- homeassistant/components/zha/config_flow.py | 17 +++- homeassistant/components/zha/manifest.json | 16 +++ homeassistant/generated/usb.py | 12 +++ homeassistant/generated/zeroconf.py | 6 ++ tests/components/zha/test_config_flow.py | 103 +++++++++++++++++++- 5 files changed, 148 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index f8f696c56e3..e116954cdcb 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -164,9 +164,15 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle zeroconf discovery.""" # Hostname is format: livingroom.local. local_name = discovery_info.hostname[:-1] + radio_type = discovery_info.properties.get("radio_type") or local_name node_name = local_name[: -len(".local")] host = discovery_info.host - device_path = f"socket://{host}:6638" + if local_name.startswith("tube") or "efr32" in local_name: + # This is hard coded to work with legacy devices + port = 6638 + else: + port = discovery_info.port + device_path = f"socket://{host}:{port}" if current_entry := await self.async_set_unique_id(node_name): self._abort_if_unique_id_configured( @@ -187,9 +193,12 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } self._device_path = device_path - self._radio_type = ( - RadioType.ezsp.name if "efr32" in local_name else RadioType.znp.name - ) + if "efr32" in radio_type: + self._radio_type = RadioType.ezsp.name + elif "zigate" in radio_type: + self._radio_type = RadioType.zigate.name + else: + self._radio_type = RadioType.znp.name return await self.async_step_port_config() diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 8befa039382..d24e29a8ab7 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -57,6 +57,18 @@ "description": "*zigbee*", "known_devices": ["Nortek HUSBZB-1"] }, + { + "vid": "0403", + "pid": "6015", + "description": "*zigate*", + "known_devices": ["ZiGate+"] + }, + { + "vid": "10C4", + "pid": "EA60", + "description": "*zigate*", + "known_devices": ["ZiGate"] + }, { "vid": "10C4", "pid": "8B34", @@ -69,6 +81,10 @@ { "type": "_esphomelib._tcp.local.", "name": "tube*" + }, + { + "type": "_zigate-zigbee-gateway._tcp.local.", + "name": "*zigate*" } ], "after_dependencies": ["usb", "zeroconf"], diff --git a/homeassistant/generated/usb.py b/homeassistant/generated/usb.py index 2e5104ce66d..ef75fbef4d1 100644 --- a/homeassistant/generated/usb.py +++ b/homeassistant/generated/usb.py @@ -77,6 +77,18 @@ USB = [ "pid": "8A2A", "description": "*zigbee*" }, + { + "domain": "zha", + "vid": "0403", + "pid": "6015", + "description": "*zigate*" + }, + { + "domain": "zha", + "vid": "10C4", + "pid": "EA60", + "description": "*zigate*" + }, { "domain": "zha", "vid": "10C4", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index b93d7249211..d1a15356ddc 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -381,6 +381,12 @@ ZEROCONF = { "domain": "kodi" } ], + "_zigate-zigbee-gateway._tcp.local.": [ + { + "domain": "zha", + "name": "*zigate*" + } + ], "_zwave-js-server._tcp.local.": [ { "domain": "zwave_js" diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 0c51ecffe9b..dee04165c1e 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -52,8 +52,8 @@ async def test_discovery(detect_mock, hass): service_info = zeroconf.ZeroconfServiceInfo( host="192.168.1.200", addresses=["192.168.1.200"], - hostname="_tube_zb_gw._tcp.local.", - name="mock_name", + hostname="tube._tube_zb_gw._tcp.local.", + name="tube", port=6053, properties={"name": "tube_123456"}, type="mock_type", @@ -77,6 +77,68 @@ async def test_discovery(detect_mock, hass): } +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +@patch("zigpy_zigate.zigbee.application.ControllerApplication.probe") +async def test_zigate_via_zeroconf(probe_mock, hass): + """Test zeroconf flow -- zigate radio detected.""" + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.1.200", + addresses=["192.168.1.200"], + hostname="_zigate-zigbee-gateway._tcp.local.", + name="any", + port=1234, + properties={"radio_type": "zigate"}, + type="mock_type", + ) + flow = await hass.config_entries.flow.async_init( + "zha", context={"source": SOURCE_ZEROCONF}, data=service_info + ) + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], user_input={} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "socket://192.168.1.200:1234" + assert result["data"] == { + CONF_DEVICE: { + CONF_DEVICE_PATH: "socket://192.168.1.200:1234", + }, + CONF_RADIO_TYPE: "zigate", + } + + +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +@patch("bellows.zigbee.application.ControllerApplication.probe", return_value=True) +async def test_efr32_via_zeroconf(probe_mock, hass): + """Test zeroconf flow -- efr32 radio detected.""" + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.1.200", + addresses=["192.168.1.200"], + hostname="efr32._esphomelib._tcp.local.", + name="efr32", + port=1234, + properties={}, + type="mock_type", + ) + flow = await hass.config_entries.flow.async_init( + "zha", context={"source": SOURCE_ZEROCONF}, data=service_info + ) + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], user_input={"baudrate": 115200} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "socket://192.168.1.200:6638" + assert result["data"] == { + CONF_DEVICE: { + CONF_DEVICE_PATH: "socket://192.168.1.200:6638", + CONF_BAUDRATE: 115200, + CONF_FLOWCONTROL: "software", + }, + CONF_RADIO_TYPE: "ezsp", + } + + @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) @patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) async def test_discovery_via_zeroconf_ip_change(detect_mock, hass): @@ -183,6 +245,43 @@ async def test_discovery_via_usb(detect_mock, hass): } +@patch("zigpy_zigate.zigbee.application.ControllerApplication.probe") +async def test_zigate_discovery_via_usb(detect_mock, hass): + """Test zigate usb flow -- radio detected.""" + discovery_info = usb.UsbServiceInfo( + device="/dev/ttyZIGBEE", + pid="0403", + vid="6015", + serial_number="1234", + description="zigate radio", + manufacturer="test", + ) + result = await hass.config_entries.flow.async_init( + "zha", context={"source": SOURCE_USB}, data=discovery_info + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + + with patch("homeassistant.components.zha.async_setup_entry"): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert ( + "zigate radio - /dev/ttyZIGBEE, s/n: 1234 - test - 6015:0403" + in result2["title"] + ) + assert result2["data"] == { + "device": { + "path": "/dev/ttyZIGBEE", + }, + CONF_RADIO_TYPE: "zigate", + } + + @patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=False) async def test_discovery_via_usb_no_radio(detect_mock, hass): """Test usb flow -- no radio detected.""" From e1fa285640013d0f2ad300d03715e71a8ec59015 Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Mon, 9 May 2022 01:28:33 +0200 Subject: [PATCH 0313/3516] Ezviz dependency bump to 0.2.0.8 (#71512) --- homeassistant/components/ezviz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json index 211e500cc7d..cfdfd6e441d 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.2.0.6"], + "requirements": ["pyezviz==0.2.0.8"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["paho_mqtt", "pyezviz"] diff --git a/requirements_all.txt b/requirements_all.txt index 9dbb2751aac..1d68e4bbfdf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1480,7 +1480,7 @@ pyeverlights==0.1.0 pyevilgenius==2.0.0 # homeassistant.components.ezviz -pyezviz==0.2.0.6 +pyezviz==0.2.0.8 # homeassistant.components.fido pyfido==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8bdfa3f3484..c747ad3587d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -974,7 +974,7 @@ pyeverlights==0.1.0 pyevilgenius==2.0.0 # homeassistant.components.ezviz -pyezviz==0.2.0.6 +pyezviz==0.2.0.8 # homeassistant.components.fido pyfido==2.1.1 From f1dd3b7f89a2990626abc2a4423d46472b002013 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 9 May 2022 00:21:41 +0000 Subject: [PATCH 0314/3516] [ci skip] Translation update --- .../components/monoprice/translations/fr.json | 24 ++++++------ .../components/recorder/translations/ca.json | 1 + .../components/recorder/translations/de.json | 1 + .../components/recorder/translations/el.json | 1 + .../components/recorder/translations/en.json | 4 +- .../components/recorder/translations/et.json | 1 + .../components/recorder/translations/fr.json | 1 + .../components/recorder/translations/hu.json | 1 + .../recorder/translations/pt-BR.json | 1 + .../components/recorder/translations/ru.json | 1 + .../recorder/translations/zh-Hant.json | 1 + .../ukraine_alarm/translations/de.json | 39 +++++++++++++++++++ .../ukraine_alarm/translations/el.json | 39 +++++++++++++++++++ .../ukraine_alarm/translations/en.json | 29 +++++++++----- .../ukraine_alarm/translations/fr.json | 39 +++++++++++++++++++ .../ukraine_alarm/translations/hu.json | 39 +++++++++++++++++++ .../ukraine_alarm/translations/ru.json | 31 ++++++++++----- .../ukraine_alarm/translations/uk.json | 28 ------------- .../components/ws66i/translations/en.json | 3 +- .../components/ws66i/translations/fr.json | 34 ++++++++++++++++ 20 files changed, 255 insertions(+), 63 deletions(-) create mode 100644 homeassistant/components/ukraine_alarm/translations/de.json create mode 100644 homeassistant/components/ukraine_alarm/translations/el.json create mode 100644 homeassistant/components/ukraine_alarm/translations/fr.json create mode 100644 homeassistant/components/ukraine_alarm/translations/hu.json delete mode 100644 homeassistant/components/ukraine_alarm/translations/uk.json create mode 100644 homeassistant/components/ws66i/translations/fr.json diff --git a/homeassistant/components/monoprice/translations/fr.json b/homeassistant/components/monoprice/translations/fr.json index 5489fb4e0e6..7f1a2a7a72e 100644 --- a/homeassistant/components/monoprice/translations/fr.json +++ b/homeassistant/components/monoprice/translations/fr.json @@ -11,12 +11,12 @@ "user": { "data": { "port": "Port", - "source_1": "Nom de la source #1", - "source_2": "Nom de la source #2", - "source_3": "Nom de la source #3", - "source_4": "Nom de la source #4", - "source_5": "Nom de la source #5", - "source_6": "Nom de la source #6" + "source_1": "Nom de la source n\u00ba\u00a01", + "source_2": "Nom de la source n\u00ba\u00a02", + "source_3": "Nom de la source n\u00ba\u00a03", + "source_4": "Nom de la source n\u00ba\u00a04", + "source_5": "Nom de la source n\u00ba\u00a05", + "source_6": "Nom de la source n\u00ba\u00a06" }, "title": "Se connecter \u00e0 l'appareil" } @@ -26,12 +26,12 @@ "step": { "init": { "data": { - "source_1": "Nom de la source #1", - "source_2": "Nom de la source #2", - "source_3": "Nom de la source #3", - "source_4": "Nom de la source #4", - "source_5": "Nom de la source #5", - "source_6": "Nom de la source #6" + "source_1": "Nom de la source n\u00ba\u00a01", + "source_2": "Nom de la source n\u00ba\u00a02", + "source_3": "Nom de la source n\u00ba\u00a03", + "source_4": "Nom de la source n\u00ba\u00a04", + "source_5": "Nom de la source n\u00ba\u00a05", + "source_6": "Nom de la source n\u00ba\u00a06" }, "title": "Configurer les sources" } diff --git a/homeassistant/components/recorder/translations/ca.json b/homeassistant/components/recorder/translations/ca.json index b6e5a549ba7..ee9efb50b21 100644 --- a/homeassistant/components/recorder/translations/ca.json +++ b/homeassistant/components/recorder/translations/ca.json @@ -2,6 +2,7 @@ "system_health": { "info": { "current_recorder_run": "Hora d'inici de l'execuci\u00f3 actual", + "estimated_db_size": "Mida estimada de la base de dades (MiB)", "oldest_recorder_run": "Hora d'inici de l'execuci\u00f3 m\u00e9s antiga" } } diff --git a/homeassistant/components/recorder/translations/de.json b/homeassistant/components/recorder/translations/de.json index 823b4ae62bf..27ba9f1da85 100644 --- a/homeassistant/components/recorder/translations/de.json +++ b/homeassistant/components/recorder/translations/de.json @@ -2,6 +2,7 @@ "system_health": { "info": { "current_recorder_run": "Aktuelle Startzeit der Ausf\u00fchrung", + "estimated_db_size": "Gesch\u00e4tzte Datenbankgr\u00f6\u00dfe (MiB)", "oldest_recorder_run": "\u00c4lteste Startzeit der Ausf\u00fchrung" } } diff --git a/homeassistant/components/recorder/translations/el.json b/homeassistant/components/recorder/translations/el.json index 0ede908d151..6d541820c55 100644 --- a/homeassistant/components/recorder/translations/el.json +++ b/homeassistant/components/recorder/translations/el.json @@ -2,6 +2,7 @@ "system_health": { "info": { "current_recorder_run": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03ce\u03c1\u03b1 \u03ad\u03bd\u03b1\u03c1\u03be\u03b7\u03c2 \u03b5\u03ba\u03c4\u03ad\u03bb\u03b5\u03c3\u03b7\u03c2", + "estimated_db_size": "\u0395\u03ba\u03c4\u03b9\u03bc\u03ce\u03bc\u03b5\u03bd\u03bf \u03bc\u03ad\u03b3\u03b5\u03b8\u03bf\u03c2 \u03b2\u03ac\u03c3\u03b7\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd (MiB)", "oldest_recorder_run": "\u03a0\u03b1\u03bb\u03b1\u03b9\u03cc\u03c4\u03b5\u03c1\u03b7 \u03ce\u03c1\u03b1 \u03ad\u03bd\u03b1\u03c1\u03be\u03b7\u03c2 \u03b5\u03ba\u03c4\u03ad\u03bb\u03b5\u03c3\u03b7\u03c2" } } diff --git a/homeassistant/components/recorder/translations/en.json b/homeassistant/components/recorder/translations/en.json index 32d78085999..fb802ff7ff9 100644 --- a/homeassistant/components/recorder/translations/en.json +++ b/homeassistant/components/recorder/translations/en.json @@ -2,8 +2,8 @@ "system_health": { "info": { "current_recorder_run": "Current Run Start Time", - "oldest_recorder_run": "Oldest Run Start Time", - "estimated_db_size": "Estimated Database Size" + "estimated_db_size": "Estimated Database Size (MiB)", + "oldest_recorder_run": "Oldest Run Start Time" } } } \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/et.json b/homeassistant/components/recorder/translations/et.json index 8455fb5ce7d..0dc6ae3ec62 100644 --- a/homeassistant/components/recorder/translations/et.json +++ b/homeassistant/components/recorder/translations/et.json @@ -2,6 +2,7 @@ "system_health": { "info": { "current_recorder_run": "Praegune k\u00e4ivitamise algusaeg", + "estimated_db_size": "Andmebaasi hinnanguline suurus (MB)", "oldest_recorder_run": "Vanim k\u00e4ivitamise algusaeg" } } diff --git a/homeassistant/components/recorder/translations/fr.json b/homeassistant/components/recorder/translations/fr.json index 31673d970f3..036fbdbb16c 100644 --- a/homeassistant/components/recorder/translations/fr.json +++ b/homeassistant/components/recorder/translations/fr.json @@ -2,6 +2,7 @@ "system_health": { "info": { "current_recorder_run": "Heure de d\u00e9marrage de l'ex\u00e9cution actuelle", + "estimated_db_size": "Taille estim\u00e9e de la base de donn\u00e9es (en Mio)", "oldest_recorder_run": "Heure de d\u00e9marrage de l'ex\u00e9cution la plus ancienne" } } diff --git a/homeassistant/components/recorder/translations/hu.json b/homeassistant/components/recorder/translations/hu.json index 4b0cd6067ad..96bb02c9c20 100644 --- a/homeassistant/components/recorder/translations/hu.json +++ b/homeassistant/components/recorder/translations/hu.json @@ -2,6 +2,7 @@ "system_health": { "info": { "current_recorder_run": "Aktu\u00e1lis futtat\u00e1s kezd\u00e9si id\u0151pontja", + "estimated_db_size": "Az adatb\u00e1zis becs\u00fclt m\u00e9rete (MiB)", "oldest_recorder_run": "Legr\u00e9gebbi futtat\u00e1s kezd\u00e9si id\u0151pontja" } } diff --git a/homeassistant/components/recorder/translations/pt-BR.json b/homeassistant/components/recorder/translations/pt-BR.json index c44ad73c28b..059dcc76779 100644 --- a/homeassistant/components/recorder/translations/pt-BR.json +++ b/homeassistant/components/recorder/translations/pt-BR.json @@ -2,6 +2,7 @@ "system_health": { "info": { "current_recorder_run": "Hora de in\u00edcio de execu\u00e7\u00e3o atual", + "estimated_db_size": "Tamanho estimado do banco de dados (MiB)", "oldest_recorder_run": "Hora de in\u00edcio de execu\u00e7\u00e3o mais antiga" } } diff --git a/homeassistant/components/recorder/translations/ru.json b/homeassistant/components/recorder/translations/ru.json index 6e5c928d77b..bdac6c52a3b 100644 --- a/homeassistant/components/recorder/translations/ru.json +++ b/homeassistant/components/recorder/translations/ru.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "estimated_db_size": "\u041f\u0440\u0435\u0434\u043f\u043e\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u0440\u0430\u0437\u043c\u0435\u0440 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445 (MiB)", "oldest_recorder_run": "\u0412\u0440\u0435\u043c\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u0441\u0430\u043c\u043e\u0433\u043e \u0441\u0442\u0430\u0440\u043e\u0433\u043e \u0446\u0438\u043a\u043b\u0430" } } diff --git a/homeassistant/components/recorder/translations/zh-Hant.json b/homeassistant/components/recorder/translations/zh-Hant.json index 648bade5447..d9525c0826d 100644 --- a/homeassistant/components/recorder/translations/zh-Hant.json +++ b/homeassistant/components/recorder/translations/zh-Hant.json @@ -2,6 +2,7 @@ "system_health": { "info": { "current_recorder_run": "\u76ee\u524d\u57f7\u884c\u958b\u59cb\u6642\u9593", + "estimated_db_size": "\u9810\u4f30\u8cc7\u6599\u5eab\u6a94\u6848\u5927\u5c0f(MiB)", "oldest_recorder_run": "\u6700\u65e9\u57f7\u884c\u958b\u59cb\u6642\u9593" } } diff --git a/homeassistant/components/ukraine_alarm/translations/de.json b/homeassistant/components/ukraine_alarm/translations/de.json new file mode 100644 index 00000000000..75dc7a7856a --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/de.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Standort ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "timeout": "Zeit\u00fcberschreitung beim Verbindungsaufbau", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "community": { + "data": { + "region": "Region" + }, + "description": "Wenn du nicht nur Bundesland und Bezirk \u00fcberwachen m\u00f6chtest, w\u00e4hle die jeweilige Gemeinde aus" + }, + "district": { + "data": { + "region": "Region" + }, + "description": "Wenn du nicht nur das Bundesland \u00fcberwachen m\u00f6chtest, w\u00e4hle seinen bestimmten Bezirk" + }, + "state": { + "data": { + "region": "Region" + }, + "description": "Zu \u00fcberwachendes Bundesland ausw\u00e4hlen" + }, + "user": { + "data": { + "api_key": "API-Schl\u00fcssel" + }, + "description": "Richte die Ukraine Alarm-Integration ein. Um einen API-Schl\u00fcssel zu generieren, gehe zu {api_url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/el.json b/homeassistant/components/ukraine_alarm/translations/el.json new file mode 100644 index 00000000000..57e40095d37 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/el.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API", + "timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "community": { + "data": { + "region": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae" + }, + "description": "\u0395\u03ac\u03bd \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c7\u03b9 \u03bc\u03cc\u03bd\u03bf \u03c4\u03b7\u03bd \u03c0\u03bf\u03bb\u03b9\u03c4\u03b5\u03af\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7\u03bd \u03c0\u03b5\u03c1\u03b9\u03c6\u03ad\u03c1\u03b5\u03b9\u03b1, \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03b7 \u03ba\u03bf\u03b9\u03bd\u03cc\u03c4\u03b7\u03c4\u03ac \u03c4\u03b7\u03c2" + }, + "district": { + "data": { + "region": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae" + }, + "description": "\u0391\u03bd \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c7\u03b9 \u03bc\u03cc\u03bd\u03bf \u03c4\u03b7\u03bd \u03c0\u03bf\u03bb\u03b9\u03c4\u03b5\u03af\u03b1, \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03b7 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae \u03c4\u03b7\u03c2." + }, + "state": { + "data": { + "region": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7" + }, + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c3\u03c5\u03bd\u03b1\u03b3\u03b5\u03c1\u03bc\u03bf\u03cd \u03c4\u03b7\u03c2 \u039f\u03c5\u03ba\u03c1\u03b1\u03bd\u03af\u03b1\u03c2. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {api_url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/en.json b/homeassistant/components/ukraine_alarm/translations/en.json index 2c39945cb87..e7b2a9edcdc 100644 --- a/homeassistant/components/ukraine_alarm/translations/en.json +++ b/homeassistant/components/ukraine_alarm/translations/en.json @@ -1,15 +1,20 @@ { "config": { + "abort": { + "already_configured": "Location is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_api_key": "Invalid API key", + "timeout": "Timeout establishing connection", + "unknown": "Unexpected error" + }, "step": { - "user": { - "description": "Set up the Ukraine Alarm integration. To generate an API key go to {api_url}", - "title": "Ukraine Alarm" - }, - "state": { + "community": { "data": { "region": "Region" }, - "description": "Choose state to monitor" + "description": "If you want to monitor not only state and district, choose its specific community" }, "district": { "data": { @@ -17,12 +22,18 @@ }, "description": "If you want to monitor not only state, choose its specific district" }, - "community": { + "state": { "data": { "region": "Region" }, - "description": "If you want to monitor not only state and district, choose its specific community" + "description": "Choose state to monitor" + }, + "user": { + "data": { + "api_key": "API Key" + }, + "description": "Set up the Ukraine Alarm integration. To generate an API key go to {api_url}" } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/fr.json b/homeassistant/components/ukraine_alarm/translations/fr.json new file mode 100644 index 00000000000..51706dfee5f --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/fr.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_api_key": "Cl\u00e9 d'API non valide", + "timeout": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9", + "unknown": "Erreur inattendue" + }, + "step": { + "community": { + "data": { + "region": "R\u00e9gion" + }, + "description": "Si vous ne d\u00e9sirez pas uniquement surveiller l'\u00c9tat et son district, s\u00e9lectionnez sa communaut\u00e9 sp\u00e9cifique" + }, + "district": { + "data": { + "region": "R\u00e9gion" + }, + "description": "Si vous ne d\u00e9sirez pas uniquement surveiller l'\u00c9tat, s\u00e9lectionnez son district sp\u00e9cifique" + }, + "state": { + "data": { + "region": "R\u00e9gion" + }, + "description": "Choisissez l'\u00c9tat \u00e0 surveiller" + }, + "user": { + "data": { + "api_key": "Cl\u00e9 d'API" + }, + "description": "Configurez l'int\u00e9gration Ukraine Alarm. Pour g\u00e9n\u00e9rer une cl\u00e9 d'API, rendez-vous sur {api_url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/hu.json b/homeassistant/components/ukraine_alarm/translations/hu.json new file mode 100644 index 00000000000..fb00a32a507 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/hu.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", + "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "community": { + "data": { + "region": "R\u00e9gi\u00f3" + }, + "description": "Ha nem csak a megy\u00e9t \u00e9s a ker\u00fcletet szeretn\u00e9 figyelni, v\u00e1lassza ki az adott k\u00f6z\u00f6ss\u00e9get" + }, + "district": { + "data": { + "region": "R\u00e9gi\u00f3" + }, + "description": "Ha nem csak megy\u00e9t szeretne figyelni, v\u00e1lassza ki az adott ker\u00fcletet" + }, + "state": { + "data": { + "region": "R\u00e9gi\u00f3" + }, + "description": "V\u00e1lassza ki a megfigyelni k\u00edv\u00e1nt megy\u00e9t" + }, + "user": { + "data": { + "api_key": "API kulcs" + }, + "description": "\u00c1ll\u00edtsa be az Ukraine Alarm integr\u00e1ci\u00f3t. API-kulcs l\u00e9trehoz\u00e1s\u00e1hoz l\u00e1togasson el ide: {api_url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/ru.json b/homeassistant/components/ukraine_alarm/translations/ru.json index 89c9eb1670a..cd852ad0745 100644 --- a/homeassistant/components/ukraine_alarm/translations/ru.json +++ b/homeassistant/components/ukraine_alarm/translations/ru.json @@ -1,28 +1,39 @@ { "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, "step": { - "user": { - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f\u0020\u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438\u0020\u0441 Ukraine Alarm. \u0414\u043b\u044f\u0020\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f\u0020\u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435\u0020\u043d\u0430 {api_url}.", - "title": "Ukraine Alarm" - }, - "state": { + "community": { "data": { "region": "\u0420\u0435\u0433\u0438\u043e\u043d" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435\u0020\u043e\u0431\u043b\u0430\u0441\u0442\u044c\u0020\u0434\u043b\u044f\u0020\u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430" + "description": "\u0415\u0441\u043b\u0438 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0438\u0442\u044c \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0437\u0430 \u0448\u0442\u0430\u0442\u043e\u043c \u0438 \u043e\u043a\u0440\u0443\u0433\u043e\u043c, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0435\u0433\u043e \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u0441\u0442\u0432\u043e." }, "district": { "data": { "region": "\u0420\u0435\u0433\u0438\u043e\u043d" }, - "description": "\u0415\u0441\u043b\u0438\u0020\u0432\u044b\u0020\u0436\u0435\u043b\u0430\u0435\u0442\u0435\u0020\u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u0442\u044c\u0020\u043d\u0435\u0020\u0442\u043e\u043b\u044c\u043a\u043e\u0020\u043e\u0431\u043b\u0430\u0441\u0442\u044c\u002c\u0020\u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435\u0020\u0435\u0451\u0020\u0440\u0430\u0439\u043e\u043d" + "description": "\u0415\u0441\u043b\u0438 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0438\u0442\u044c \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0437\u0430 \u0448\u0442\u0430\u0442\u043e\u043c, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0435\u0433\u043e \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u0440\u0430\u0439\u043e\u043d." }, - "community": { + "state": { "data": { "region": "\u0420\u0435\u0433\u0438\u043e\u043d" }, - "description": "\u0415\u0441\u043b\u0438\u0020\u0432\u044b\u0020\u0436\u0435\u043b\u0430\u0435\u0442\u0435\u0020\u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u0442\u044c\u0020\u043d\u0435\u0020\u0442\u043e\u043b\u044c\u043a\u043e\u0020\u043e\u0431\u043b\u0430\u0441\u0442\u044c\u0020\u0438\u0020\u0440\u0430\u0439\u043e\u043d\u002c\u0020\u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435\u0020\u0435\u0451\u0020\u0433\u0440\u043e\u043c\u0430\u0434\u0443" + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u0442\u0430\u0442 \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430." + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0427\u0442\u043e\u0431\u044b \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 {api_url}" } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/uk.json b/homeassistant/components/ukraine_alarm/translations/uk.json deleted file mode 100644 index 2eed983f34f..00000000000 --- a/homeassistant/components/ukraine_alarm/translations/uk.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "config": { - "step": { - "user": { - "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f\u0020\u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457\u0020\u0437 Ukraine Alarm. \u0414\u043b\u044f\u0020\u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f\u0020\u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c\u0020\u043d\u0430 {api_url}.", - "title": "Ukraine Alarm" - }, - "state": { - "data": { - "region": "\u0420\u0435\u0433\u0456\u043e\u043d" - }, - "description": "\u041e\u0431\u0435\u0440\u0456\u0442\u044c\u0020\u043e\u0431\u043b\u0430\u0441\u0442\u044c\u0020\u0434\u043b\u044f\u0020\u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443" - }, - "district": { - "data": { - "region": "\u0420\u0435\u0433\u0456\u043e\u043d" - }, - "description": "\u042f\u043a\u0449\u043e\u0020\u0432\u0438\u0020\u0431\u0430\u0436\u0430\u0454\u0442\u0435\u0020\u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u0442\u0438\u0020\u043d\u0435\u0020\u043b\u0438\u0448\u0435\u0020\u043e\u0431\u043b\u0430\u0441\u0442\u044c\u002c\u0020\u043e\u0431\u0435\u0440\u0456\u0442\u044c\u0020\u0457\u0457\u0020\u0440\u0430\u0439\u043e\u043d" - }, - "community": { - "data": { - "region": "\u0420\u0435\u0433\u0456\u043e\u043d" - }, - "description": "\u042f\u043a\u0449\u043e\u0020\u0432\u0438\u0020\u0431\u0430\u0436\u0430\u0454\u0442\u0435\u0020\u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u0442\u0438\u0020\u043d\u0435\u0020\u0442\u0456\u043b\u044c\u043a\u0438\u0020\u043e\u0431\u043b\u0430\u0441\u0442\u044c\u0020\u0442\u0430\u0020\u0440\u0430\u0439\u043e\u043d\u002c\u0020\u043e\u0431\u0435\u0440\u0456\u0442\u044c\u0020\u0457\u0457\u0020\u0433\u0440\u043e\u043c\u0430\u0434\u0443" - } - } - } -} diff --git a/homeassistant/components/ws66i/translations/en.json b/homeassistant/components/ws66i/translations/en.json index 7f6a04c1ea3..30ef1e4205a 100644 --- a/homeassistant/components/ws66i/translations/en.json +++ b/homeassistant/components/ws66i/translations/en.json @@ -31,5 +31,4 @@ } } } -} - +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/fr.json b/homeassistant/components/ws66i/translations/fr.json new file mode 100644 index 00000000000..d4d3f6e7350 --- /dev/null +++ b/homeassistant/components/ws66i/translations/fr.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "ip_address": "Adresse IP" + }, + "title": "Se connecter \u00e0 l'appareil" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Nom de la source n\u00ba\u00a01", + "source_2": "Nom de la source n\u00ba\u00a02", + "source_3": "Nom de la source n\u00ba\u00a03", + "source_4": "Nom de la source n\u00ba\u00a04", + "source_5": "Nom de la source n\u00ba\u00a05", + "source_6": "Nom de la source n\u00ba\u00a06" + }, + "title": "Configurer les sources" + } + } + } +} \ No newline at end of file From 0b8f87169bb2019896069e958aa0f15f42f613cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 May 2022 20:05:19 -0500 Subject: [PATCH 0315/3516] Pretty zha manifest.json (#71556) --- homeassistant/components/zha/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index d24e29a8ab7..d4e71562b07 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -63,7 +63,7 @@ "description": "*zigate*", "known_devices": ["ZiGate+"] }, - { + { "vid": "10C4", "pid": "EA60", "description": "*zigate*", From 6da889326bd51e13752b1511f2f1d46d7c78ab67 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sun, 8 May 2022 22:07:12 -0400 Subject: [PATCH 0316/3516] Fix typer/click incompatibilty for unifiprotect (#71555) Co-authored-by: J. Nick Koston --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index d5dbb51ffc1..3c3b461ed4f 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.4.0", "unifi-discovery==1.1.2"], + "requirements": ["pyunifiprotect==3.4.1", "unifi-discovery==1.1.2"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 1d68e4bbfdf..07772173bd8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1984,7 +1984,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.4.0 +pyunifiprotect==3.4.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c747ad3587d..9cfa8a6945b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1307,7 +1307,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.4.0 +pyunifiprotect==3.4.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 9ef5c23f1ca90ca8242dffc60922fe2a4f0b9b87 Mon Sep 17 00:00:00 2001 From: Andrew Bullock Date: Mon, 9 May 2022 03:10:25 +0100 Subject: [PATCH 0317/3516] Add support to Hunter Douglas for Silhouette Type 23 Tilting (#70775) Co-authored-by: J. Nick Koston --- CODEOWNERS | 4 +- .../hunterdouglas_powerview/cover.py | 170 ++++++++++++++++-- .../hunterdouglas_powerview/manifest.json | 2 +- 3 files changed, 154 insertions(+), 22 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 2e2a8c0584f..19450d86bba 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -455,8 +455,8 @@ build.json @home-assistant/supervisor /tests/components/huisbaasje/ @dennisschroer /homeassistant/components/humidifier/ @home-assistant/core @Shulyaka /tests/components/humidifier/ @home-assistant/core @Shulyaka -/homeassistant/components/hunterdouglas_powerview/ @bdraco -/tests/components/hunterdouglas_powerview/ @bdraco +/homeassistant/components/hunterdouglas_powerview/ @bdraco @trullock +/tests/components/hunterdouglas_powerview/ @bdraco @trullock /homeassistant/components/hvv_departures/ @vigonotion /tests/components/hvv_departures/ @vigonotion /homeassistant/components/hydrawise/ @ptcryan diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 2ef1cc46adf..493b5d53639 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -1,4 +1,5 @@ """Support for hunter douglas shades.""" +from abc import abstractmethod import asyncio from contextlib import suppress import logging @@ -8,12 +9,14 @@ from aiopvapi.resources.shade import ( ATTR_POSKIND1, MAX_POSITION, MIN_POSITION, + Silhouette, factory as PvShade, ) import async_timeout from homeassistant.components.cover import ( ATTR_POSITION, + ATTR_TILT_POSITION, CoverDeviceClass, CoverEntity, CoverEntityFeature, @@ -49,6 +52,12 @@ PARALLEL_UPDATES = 1 RESYNC_DELAY = 60 +POSKIND_NONE = 0 +POSKIND_PRIMARY = 1 +POSKIND_SECONDARY = 2 +POSKIND_VANE = 3 +POSKIND_ERROR = 4 + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -82,24 +91,39 @@ async def async_setup_entry( room_id = shade.raw_data.get(ROOM_ID_IN_SHADE) room_name = room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "") entities.append( - PowerViewShade( + create_powerview_shade_entity( coordinator, device_info, room_name, shade, name_before_refresh ) ) async_add_entities(entities) -def hd_position_to_hass(hd_position): +def create_powerview_shade_entity( + coordinator, device_info, room_name, shade, name_before_refresh +): + """Create a PowerViewShade entity.""" + + if isinstance(shade, Silhouette): + return PowerViewShadeSilhouette( + coordinator, device_info, room_name, shade, name_before_refresh + ) + + return PowerViewShade( + coordinator, device_info, room_name, shade, name_before_refresh + ) + + +def hd_position_to_hass(hd_position, max_val): """Convert hunter douglas position to hass position.""" - return round((hd_position / MAX_POSITION) * 100) + return round((hd_position / max_val) * 100) -def hass_position_to_hd(hass_position): +def hass_position_to_hd(hass_position, max_val): """Convert hass position to hunter douglas position.""" - return int(hass_position / 100 * MAX_POSITION) + return int(hass_position / 100 * max_val) -class PowerViewShade(ShadeEntity, CoverEntity): +class PowerViewShadeBase(ShadeEntity, CoverEntity): """Representation of a powerview shade.""" # The hub frequently reports stale states @@ -113,12 +137,7 @@ class PowerViewShade(ShadeEntity, CoverEntity): self._is_closing = False self._last_action_timestamp = 0 self._scheduled_transition_update = None - self._current_cover_position = MIN_POSITION - self._attr_supported_features = ( - CoverEntityFeature.OPEN - | CoverEntityFeature.CLOSE - | CoverEntityFeature.SET_POSITION - ) + self._current_hd_cover_position = MIN_POSITION if self._device_info[DEVICE_MODEL] != LEGACY_DEVICE_MODEL: self._attr_supported_features |= CoverEntityFeature.STOP self._forced_resync = None @@ -131,7 +150,7 @@ class PowerViewShade(ShadeEntity, CoverEntity): @property def is_closed(self): """Return if the cover is closed.""" - return self._current_cover_position == MIN_POSITION + return self._current_hd_cover_position == MIN_POSITION @property def is_opening(self): @@ -146,7 +165,7 @@ class PowerViewShade(ShadeEntity, CoverEntity): @property def current_cover_position(self): """Return the current position of cover.""" - return hd_position_to_hass(self._current_cover_position) + return hd_position_to_hass(self._current_hd_cover_position, MAX_POSITION) @property def device_class(self): @@ -181,14 +200,18 @@ class PowerViewShade(ShadeEntity, CoverEntity): async def _async_move(self, target_hass_position): """Move the shade to a position.""" - current_hass_position = hd_position_to_hass(self._current_cover_position) + current_hass_position = hd_position_to_hass( + self._current_hd_cover_position, MAX_POSITION + ) steps_to_move = abs(current_hass_position - target_hass_position) self._async_schedule_update_for_transition(steps_to_move) self._async_update_from_command( await self._shade.move( { - ATTR_POSITION1: hass_position_to_hd(target_hass_position), - ATTR_POSKIND1: 1, + ATTR_POSITION1: hass_position_to_hd( + target_hass_position, MAX_POSITION + ), + ATTR_POSKIND1: POSKIND_PRIMARY, } ) ) @@ -218,11 +241,15 @@ class PowerViewShade(ShadeEntity, CoverEntity): """Update the current cover position from the data.""" _LOGGER.debug("Raw data update: %s", self._shade.raw_data) position_data = self._shade.raw_data.get(ATTR_POSITION_DATA, {}) - if ATTR_POSITION1 in position_data: - self._current_cover_position = int(position_data[ATTR_POSITION1]) + self._async_process_updated_position_data(position_data) self._is_opening = False self._is_closing = False + @callback + @abstractmethod + def _async_process_updated_position_data(self, position_data): + """Process position data.""" + @callback def _async_cancel_scheduled_transition_update(self): """Cancel any previous updates.""" @@ -299,3 +326,108 @@ class PowerViewShade(ShadeEntity, CoverEntity): return self._async_process_new_shade_data(self.coordinator.data[self._shade.id]) self.async_write_ha_state() + + +class PowerViewShade(PowerViewShadeBase): + """Represent a standard shade.""" + + _attr_supported_features = ( + CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.SET_POSITION + ) + + @callback + def _async_process_updated_position_data(self, position_data): + """Process position data.""" + if ATTR_POSITION1 in position_data: + self._current_hd_cover_position = int(position_data[ATTR_POSITION1]) + + +class PowerViewShadeWithTilt(PowerViewShade): + """Representation of a PowerView shade with tilt capabilities.""" + + _attr_supported_features = ( + CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.SET_POSITION + | CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT + | CoverEntityFeature.SET_TILT_POSITION + ) + + _max_tilt = MAX_POSITION + _tilt_steps = 10 + + def __init__(self, coordinator, device_info, room_name, shade, name): + """Initialize the shade.""" + super().__init__(coordinator, device_info, room_name, shade, name) + self._attr_current_cover_tilt_position = 0 + + async def async_open_cover_tilt(self, **kwargs): + """Open the cover tilt.""" + current_hass_position = hd_position_to_hass( + self._current_hd_cover_position, MAX_POSITION + ) + steps_to_move = current_hass_position + self._tilt_steps + self._async_schedule_update_for_transition(steps_to_move) + self._async_update_from_command(await self._shade.tilt_open()) + + async def async_close_cover_tilt(self, **kwargs): + """Close the cover tilt.""" + current_hass_position = hd_position_to_hass( + self._current_hd_cover_position, MAX_POSITION + ) + steps_to_move = current_hass_position + self._tilt_steps + self._async_schedule_update_for_transition(steps_to_move) + self._async_update_from_command(await self._shade.tilt_close()) + + async def async_set_cover_tilt_position(self, **kwargs): + """Move the cover tilt to a specific position.""" + target_hass_tilt_position = kwargs[ATTR_TILT_POSITION] + current_hass_position = hd_position_to_hass( + self._current_hd_cover_position, MAX_POSITION + ) + steps_to_move = current_hass_position + self._tilt_steps + + self._async_schedule_update_for_transition(steps_to_move) + self._async_update_from_command( + await self._shade.move( + { + ATTR_POSITION1: hass_position_to_hd( + target_hass_tilt_position, self._max_tilt + ), + ATTR_POSKIND1: POSKIND_VANE, + } + ) + ) + + async def async_stop_cover_tilt(self, **kwargs): + """Stop the cover tilting.""" + # Cancel any previous updates + await self.async_stop_cover() + + @callback + def _async_process_updated_position_data(self, position_data): + """Process position data.""" + if ATTR_POSKIND1 not in position_data: + return + if int(position_data[ATTR_POSKIND1]) == POSKIND_PRIMARY: + self._current_hd_cover_position = int(position_data[ATTR_POSITION1]) + self._attr_current_cover_tilt_position = 0 + if int(position_data[ATTR_POSKIND1]) == POSKIND_VANE: + self._current_hd_cover_position = MIN_POSITION + self._attr_current_cover_tilt_position = hd_position_to_hass( + int(position_data[ATTR_POSITION1]), self._max_tilt + ) + + +class PowerViewShadeSilhouette(PowerViewShadeWithTilt): + """Representation of a Silhouette PowerView shade.""" + + def __init__(self, coordinator, device_info, room_name, shade, name): + """Initialize the shade.""" + super().__init__(coordinator, device_info, room_name, shade, name) + self._max_tilt = 32767 + self._tilt_steps = 4 diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index c34f53f47b4..af6aea17de3 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -3,7 +3,7 @@ "name": "Hunter Douglas PowerView", "documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview", "requirements": ["aiopvapi==1.6.19"], - "codeowners": ["@bdraco"], + "codeowners": ["@bdraco", "@trullock"], "config_flow": true, "homekit": { "models": ["PowerView"] From cec7e533022202cfa15f591d7be2468c0e607940 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 May 2022 21:15:18 -0500 Subject: [PATCH 0318/3516] Fix zeroconf tests (#71557) --- tests/components/zeroconf/test_init.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 3487b57e482..900dbeb8431 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -147,7 +147,17 @@ def get_zeroconf_info_mock_model(model): async def test_setup(hass, mock_async_zeroconf): """Test configured options for a device are loaded via config entry.""" - with patch.object( + mock_zc = { + "_http._tcp.local.": [ + { + "domain": "shelly", + "name": "shelly*", + "properties": {"macaddress": "ffaadd*"}, + } + ], + "_Volumio._tcp.local.": [{"domain": "volumio"}], + } + with patch.dict(zc_gen.ZEROCONF, mock_zc, clear=True,), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock @@ -161,7 +171,7 @@ async def test_setup(hass, mock_async_zeroconf): assert len(mock_service_browser.mock_calls) == 1 expected_flow_calls = 0 - for matching_components in zc_gen.ZEROCONF.values(): + for matching_components in mock_zc.values(): domains = set() for component in matching_components: if len(component) == 1: From 2b30bda6c8371bbf546eeaa4b34393cbe58f9f86 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Mon, 9 May 2022 04:33:20 +0200 Subject: [PATCH 0319/3516] Add binary sensor platform to devolo Home Network (#60301) Co-authored-by: J. Nick Koston --- .../devolo_home_network/binary_sensor.py | 99 +++++++++++++++++++ .../components/devolo_home_network/const.py | 3 +- .../components/devolo_home_network/entity.py | 17 ++-- .../devolo_home_network/__init__.py | 1 + tests/components/devolo_home_network/const.py | 19 +++- .../devolo_home_network/test_binary_sensor.py | 83 ++++++++++++++++ 6 files changed, 210 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/devolo_home_network/binary_sensor.py create mode 100644 tests/components/devolo_home_network/test_binary_sensor.py diff --git a/homeassistant/components/devolo_home_network/binary_sensor.py b/homeassistant/components/devolo_home_network/binary_sensor.py new file mode 100644 index 00000000000..6bc02d802f5 --- /dev/null +++ b/homeassistant/components/devolo_home_network/binary_sensor.py @@ -0,0 +1,99 @@ +"""Platform for binary sensor integration.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from devolo_plc_api.device import Device + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_PLUG, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import CONNECTED_PLC_DEVICES, CONNECTED_TO_ROUTER, DOMAIN +from .entity import DevoloEntity + + +def _is_connected_to_router(entity: DevoloBinarySensorEntity) -> bool: + """Check, if device is attached to the router.""" + return all( + device["attached_to_router"] + for device in entity.coordinator.data["network"]["devices"] + if device["mac_address"] == entity.device.mac + ) + + +@dataclass +class DevoloBinarySensorRequiredKeysMixin: + """Mixin for required keys.""" + + value_func: Callable[[DevoloBinarySensorEntity], bool] + + +@dataclass +class DevoloBinarySensorEntityDescription( + BinarySensorEntityDescription, DevoloBinarySensorRequiredKeysMixin +): + """Describes devolo sensor entity.""" + + +SENSOR_TYPES: dict[str, DevoloBinarySensorEntityDescription] = { + CONNECTED_TO_ROUTER: DevoloBinarySensorEntityDescription( + key=CONNECTED_TO_ROUTER, + device_class=DEVICE_CLASS_PLUG, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:router-network", + name="Connected to router", + value_func=_is_connected_to_router, + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Get all devices and sensors and setup them via config entry.""" + device: Device = hass.data[DOMAIN][entry.entry_id]["device"] + coordinators: dict[str, DataUpdateCoordinator] = hass.data[DOMAIN][entry.entry_id][ + "coordinators" + ] + + entities: list[BinarySensorEntity] = [] + if device.plcnet: + entities.append( + DevoloBinarySensorEntity( + coordinators[CONNECTED_PLC_DEVICES], + SENSOR_TYPES[CONNECTED_TO_ROUTER], + device, + entry.title, + ) + ) + async_add_entities(entities) + + +class DevoloBinarySensorEntity(DevoloEntity, BinarySensorEntity): + """Representation of a devolo binary sensor.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + description: DevoloBinarySensorEntityDescription, + device: Device, + device_name: str, + ) -> None: + """Initialize entity.""" + self.entity_description: DevoloBinarySensorEntityDescription = description + super().__init__(coordinator, device, device_name) + + @property + def is_on(self) -> bool: + """State of the binary sensor.""" + return self.entity_description.value_func(self) diff --git a/homeassistant/components/devolo_home_network/const.py b/homeassistant/components/devolo_home_network/const.py index bd7170bfde5..2dfdd3c1d9a 100644 --- a/homeassistant/components/devolo_home_network/const.py +++ b/homeassistant/components/devolo_home_network/const.py @@ -5,7 +5,7 @@ from datetime import timedelta from homeassistant.const import Platform DOMAIN = "devolo_home_network" -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] PRODUCT = "product" SERIAL_NUMBER = "serial_number" @@ -15,5 +15,6 @@ LONG_UPDATE_INTERVAL = timedelta(minutes=5) SHORT_UPDATE_INTERVAL = timedelta(seconds=15) CONNECTED_PLC_DEVICES = "connected_plc_devices" +CONNECTED_TO_ROUTER = "connected_to_router" CONNECTED_WIFI_CLIENTS = "connected_wifi_clients" NEIGHBORING_WIFI_NETWORKS = "neighboring_wifi_networks" diff --git a/homeassistant/components/devolo_home_network/entity.py b/homeassistant/components/devolo_home_network/entity.py index dbfe0e4035a..dd26324bc2c 100644 --- a/homeassistant/components/devolo_home_network/entity.py +++ b/homeassistant/components/devolo_home_network/entity.py @@ -21,17 +21,14 @@ class DevoloEntity(CoordinatorEntity): """Initialize a devolo home network device.""" super().__init__(coordinator) - self._device = device - self._device_name = device_name + self.device = device self._attr_device_info = DeviceInfo( - configuration_url=f"http://{self._device.ip}", - identifiers={(DOMAIN, str(self._device.serial_number))}, + configuration_url=f"http://{device.ip}", + identifiers={(DOMAIN, str(device.serial_number))}, manufacturer="devolo", - model=self._device.product, - name=self._device_name, - sw_version=self._device.firmware_version, - ) - self._attr_unique_id = ( - f"{self._device.serial_number}_{self.entity_description.key}" + model=device.product, + name=device_name, + sw_version=device.firmware_version, ) + self._attr_unique_id = f"{device.serial_number}_{self.entity_description.key}" diff --git a/tests/components/devolo_home_network/__init__.py b/tests/components/devolo_home_network/__init__.py index 913193be3f7..1c10d7a59ef 100644 --- a/tests/components/devolo_home_network/__init__.py +++ b/tests/components/devolo_home_network/__init__.py @@ -28,5 +28,6 @@ def configure_integration(hass: HomeAssistant) -> MockConfigEntry: async def async_connect(self, session_instance: Any = None): """Give a mocked device the needed properties.""" + self.mac = DISCOVERY_INFO.properties["PlcMacAddress"] self.plcnet = PlcNetApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) self.device = DeviceApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) diff --git a/tests/components/devolo_home_network/const.py b/tests/components/devolo_home_network/const.py index 0e48833a78b..516a19f3421 100644 --- a/tests/components/devolo_home_network/const.py +++ b/tests/components/devolo_home_network/const.py @@ -62,6 +62,12 @@ NEIGHBOR_ACCESS_POINTS = { PLCNET = { "network": { + "devices": [ + { + "mac_address": "AA:BB:CC:DD:EE:FF", + "attached_to_router": False, + } + ], "data_rates": [ { "mac_address_from": "AA:BB:CC:DD:EE:FF", @@ -70,6 +76,17 @@ PLCNET = { "tx_rate": 0.0, }, ], - "devices": [], + } +} + +PLCNET_ATTACHED = { + "network": { + "devices": [ + { + "mac_address": "AA:BB:CC:DD:EE:FF", + "attached_to_router": True, + } + ], + "data_rates": [], } } diff --git a/tests/components/devolo_home_network/test_binary_sensor.py b/tests/components/devolo_home_network/test_binary_sensor.py new file mode 100644 index 00000000000..3d181da6106 --- /dev/null +++ b/tests/components/devolo_home_network/test_binary_sensor.py @@ -0,0 +1,83 @@ +"""Tests for the devolo Home Network sensors.""" +from unittest.mock import AsyncMock, patch + +from devolo_plc_api.exceptions.device import DeviceUnavailable +import pytest + +from homeassistant.components.binary_sensor import DOMAIN +from homeassistant.components.devolo_home_network.const import ( + CONNECTED_TO_ROUTER, + LONG_UPDATE_INTERVAL, +) +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity import EntityCategory +from homeassistant.util import dt + +from . import configure_integration +from .const import PLCNET_ATTACHED + +from tests.common import async_fire_time_changed + + +@pytest.mark.usefixtures("mock_device", "mock_zeroconf") +async def test_binary_sensor_setup(hass: HomeAssistant): + """Test default setup of the binary sensor component.""" + entry = configure_integration(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(f"{DOMAIN}.{CONNECTED_TO_ROUTER}") is None + + await hass.config_entries.async_unload(entry.entry_id) + + +@pytest.mark.usefixtures("mock_device", "mock_zeroconf") +async def test_update_attached_to_router(hass: HomeAssistant): + """Test state change of a attached_to_router binary sensor device.""" + state_key = f"{DOMAIN}.{CONNECTED_TO_ROUTER}" + entry = configure_integration(hass) + + er = entity_registry.async_get(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Enable entity + er.async_update_entity(state_key, disabled_by=None) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_OFF + + assert er.async_get(state_key).entity_category == EntityCategory.DIAGNOSTIC + + # Emulate device failure + with patch( + "devolo_plc_api.plcnet_api.plcnetapi.PlcNetApi.async_get_network_overview", + side_effect=DeviceUnavailable, + ): + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + # Emulate state change + with patch( + "devolo_plc_api.plcnet_api.plcnetapi.PlcNetApi.async_get_network_overview", + new=AsyncMock(return_value=PLCNET_ATTACHED), + ): + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_ON + + await hass.config_entries.async_unload(entry.entry_id) From 15a5878a39a70802a87737d2274c32b1adf75952 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 May 2022 22:38:22 -0500 Subject: [PATCH 0320/3516] Use MediaPlayerEntityFeature in ws66i (#71553) --- .../components/ws66i/media_player.py | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/ws66i/media_player.py b/homeassistant/components/ws66i/media_player.py index 2f485748c02..c0e62fe773c 100644 --- a/homeassistant/components/ws66i/media_player.py +++ b/homeassistant/components/ws66i/media_player.py @@ -1,17 +1,11 @@ """Support for interfacing with WS66i 6 zone home audio controller.""" from copy import deepcopy -import logging from pyws66i import WS66i, ZoneStatus -from homeassistant.components.media_player import MediaPlayerEntity -from homeassistant.components.media_player.const import ( - SUPPORT_SELECT_SOURCE, - SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, +from homeassistant.components.media_player import ( + MediaPlayerEntity, + MediaPlayerEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON @@ -28,19 +22,8 @@ from .const import DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT from .coordinator import Ws66iDataUpdateCoordinator from .models import Ws66iData -_LOGGER = logging.getLogger(__name__) - PARALLEL_UPDATES = 1 -SUPPORT_WS66I = ( - SUPPORT_VOLUME_MUTE - | SUPPORT_VOLUME_SET - | SUPPORT_VOLUME_STEP - | SUPPORT_TURN_ON - | SUPPORT_TURN_OFF - | SUPPORT_SELECT_SOURCE -) - MAX_VOL = 38 @@ -105,7 +88,14 @@ class Ws66iZone(CoordinatorEntity, MediaPlayerEntity): self._attr_source_list = ws66i_data.sources.name_list self._attr_unique_id = f"{entry_id}_{self._zone_id}" self._attr_name = f"Zone {self._zone_id}" - self._attr_supported_features = SUPPORT_WS66I + self._attr_supported_features = ( + MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.VOLUME_STEP + | MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.SELECT_SOURCE + ) self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, str(self.unique_id))}, name=self.name, From 24d7a464e1b09e09b98e8d727adca71825c3ee5f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 8 May 2022 23:47:26 -0500 Subject: [PATCH 0321/3516] Refactor logbook to reduce overhead and complexity (#71509) --- homeassistant/components/logbook/__init__.py | 477 ++++++++---------- homeassistant/scripts/benchmark/__init__.py | 52 -- tests/components/alexa/test_init.py | 64 ++- tests/components/automation/test_init.py | 40 +- tests/components/automation/test_logbook.py | 51 +- tests/components/deconz/test_logbook.py | 149 +++--- .../google_assistant/test_logbook.py | 72 ++- tests/components/homekit/test_init.py | 50 +- tests/components/logbook/common.py | 58 +++ tests/components/logbook/test_init.py | 106 ++-- tests/components/script/test_init.py | 33 +- tests/components/shelly/test_logbook.py | 101 ++-- 12 files changed, 573 insertions(+), 680 deletions(-) create mode 100644 tests/components/logbook/common.py diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index f29276489a2..5ec03b3aaa4 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -326,9 +326,10 @@ class LogbookView(HomeAssistantView): def humanify( hass: HomeAssistant, - events: Generator[LazyEventPartialState, None, None], - entity_attr_cache: EntityAttributeCache, - context_lookup: dict[str | None, LazyEventPartialState | None], + rows: Generator[Row, None, None], + entity_name_cache: EntityNameCache, + event_cache: EventCache, + context_augmenter: ContextAugmenter, ) -> Generator[dict[str, Any], None, None]: """Generate a converted list of events into Entry objects. @@ -336,25 +337,24 @@ def humanify( - if Home Assistant stop and start happen in same minute call it restarted """ external_events = hass.data.get(DOMAIN, {}) + # Continuous sensors, will be excluded from the logbook + continuous_sensors = {} # Group events in batches of GROUP_BY_MINUTES - for _, g_events in groupby( - events, lambda event: event.time_fired_minute // GROUP_BY_MINUTES + for _, g_rows in groupby( + rows, lambda row: row.time_fired.minute // GROUP_BY_MINUTES # type: ignore[no-any-return] ): - events_batch = list(g_events) - - # Continuous sensors, will be excluded from the logbook - continuous_sensors = {} + rows_batch = list(g_rows) # Group HA start/stop events # Maps minute of event to 1: stop, 2: stop + start start_stop_events = {} # Process events - for event in events_batch: - if event.event_type == EVENT_STATE_CHANGED: - entity_id = event.entity_id + for row in rows_batch: + if row.event_type == EVENT_STATE_CHANGED: + entity_id = row.entity_id if ( entity_id in continuous_sensors or split_entity_id(entity_id)[0] != SENSOR_DOMAIN @@ -363,22 +363,22 @@ def humanify( assert entity_id is not None continuous_sensors[entity_id] = _is_sensor_continuous(hass, entity_id) - elif event.event_type == EVENT_HOMEASSISTANT_STOP: - if event.time_fired_minute in start_stop_events: + elif row.event_type == EVENT_HOMEASSISTANT_STOP: + if row.time_fired.minute in start_stop_events: continue - start_stop_events[event.time_fired_minute] = 1 + start_stop_events[row.time_fired.minute] = 1 - elif event.event_type == EVENT_HOMEASSISTANT_START: - if event.time_fired_minute not in start_stop_events: + elif row.event_type == EVENT_HOMEASSISTANT_START: + if row.time_fired.minute not in start_stop_events: continue - start_stop_events[event.time_fired_minute] = 2 + start_stop_events[row.time_fired.minute] = 2 # Yield entries - for event in events_batch: - if event.event_type == EVENT_STATE_CHANGED: - entity_id = event.entity_id + for row in rows_batch: + if row.event_type == EVENT_STATE_CHANGED: + entity_id = row.entity_id assert entity_id is not None if continuous_sensors.get(entity_id): @@ -386,74 +386,59 @@ def humanify( continue data = { - "when": event.time_fired_isoformat, - "name": _entity_name_from_event( - entity_id, event, entity_attr_cache - ), - "state": event.state, + "when": _row_time_fired_isoformat(row), + "name": entity_name_cache.get(entity_id, row), + "state": row.state, "entity_id": entity_id, } - if icon := event.attributes_icon: + if icon := _row_attributes_extract(row, ICON_JSON_EXTRACT): data["icon"] = icon - if event.context_user_id: - data["context_user_id"] = event.context_user_id + if row.context_user_id: + data["context_user_id"] = row.context_user_id - _augment_data_with_context( - data, - entity_id, - event, - context_lookup, - entity_attr_cache, - external_events, - ) + context_augmenter.augment(data, entity_id, row) yield data - elif event.event_type in external_events: - domain, describe_event = external_events[event.event_type] - data = describe_event(event) - data["when"] = event.time_fired_isoformat + elif row.event_type in external_events: + domain, describe_event = external_events[row.event_type] + data = describe_event(event_cache.get(row)) + data["when"] = _row_time_fired_isoformat(row) data["domain"] = domain - if event.context_user_id: - data["context_user_id"] = event.context_user_id + if row.context_user_id: + data["context_user_id"] = row.context_user_id - _augment_data_with_context( - data, - data.get(ATTR_ENTITY_ID), - event, - context_lookup, - entity_attr_cache, - external_events, - ) + entity_id = data.get(ATTR_ENTITY_ID) + context_augmenter.augment(data, entity_id, row) yield data - elif event.event_type == EVENT_HOMEASSISTANT_START: - if start_stop_events.get(event.time_fired_minute) == 2: + elif row.event_type == EVENT_HOMEASSISTANT_START: + if start_stop_events.get(row.time_fired.minute) == 2: continue - yield { - "when": event.time_fired_isoformat, + "when": _row_time_fired_isoformat(row), "name": "Home Assistant", "message": "started", "domain": HA_DOMAIN, } - elif event.event_type == EVENT_HOMEASSISTANT_STOP: - if start_stop_events.get(event.time_fired_minute) == 2: + elif row.event_type == EVENT_HOMEASSISTANT_STOP: + if start_stop_events.get(row.time_fired.minute) == 2: action = "restarted" else: action = "stopped" yield { - "when": event.time_fired_isoformat, + "when": _row_time_fired_isoformat(row), "name": "Home Assistant", "message": action, "domain": HA_DOMAIN, } - elif event.event_type == EVENT_LOGBOOK_ENTRY: + elif row.event_type == EVENT_LOGBOOK_ENTRY: + event = event_cache.get(row) event_data = event.data domain = event_data.get(ATTR_DOMAIN) entity_id = event_data.get(ATTR_ENTITY_ID) @@ -462,25 +447,17 @@ def humanify( domain = split_entity_id(str(entity_id))[0] data = { - "when": event.time_fired_isoformat, + "when": _row_time_fired_isoformat(row), "name": event_data.get(ATTR_NAME), "message": event_data.get(ATTR_MESSAGE), "domain": domain, "entity_id": entity_id, } - if event.context_user_id: - data["context_user_id"] = event.context_user_id - - _augment_data_with_context( - data, - entity_id, - event, - context_lookup, - entity_attr_cache, - external_events, - ) + if row.context_user_id: + data["context_user_id"] = row.context_user_id + context_augmenter.augment(data, entity_id, row) yield data @@ -499,21 +476,24 @@ def _get_events( entity_ids and context_id ), "can't pass in both entity_ids and context_id" - entity_attr_cache = EntityAttributeCache(hass) + entity_name_cache = EntityNameCache(hass) event_data_cache: dict[str, dict[str, Any]] = {} - context_lookup: dict[str | None, LazyEventPartialState | None] = {None: None} + context_lookup: dict[str | None, Row | None] = {None: None} + event_cache = EventCache(event_data_cache) + external_events = hass.data.get(DOMAIN, {}) + context_augmenter = ContextAugmenter( + context_lookup, entity_name_cache, external_events, event_cache + ) - def yield_events(query: Query) -> Generator[LazyEventPartialState, None, None]: + def yield_rows(query: Query) -> Generator[Row, None, None]: """Yield Events that are not filtered away.""" for row in query.yield_per(1000): - event = LazyEventPartialState(row, event_data_cache) - context_lookup.setdefault(event.context_id, event) - if event.event_type == EVENT_CALL_SERVICE: - continue - if event.event_type == EVENT_STATE_CHANGED or _keep_event( - hass, event, entities_filter + context_lookup.setdefault(row.context_id, row) + if row.event_type != EVENT_CALL_SERVICE and ( + row.event_type == EVENT_STATE_CHANGED + or _keep_row(hass, row, entities_filter) ): - yield event + yield row if entity_ids is not None: entities_filter = generate_filter([], entity_ids, [], []) @@ -568,7 +548,13 @@ def _get_events( query = query.order_by(Events.time_fired) return list( - humanify(hass, yield_events(query), entity_attr_cache, context_lookup) + humanify( + hass, + yield_rows(query), + entity_name_cache, + event_cache, + context_augmenter, + ) ) @@ -716,103 +702,108 @@ def _apply_event_entity_id_matchers( return events_query.filter(sqlalchemy.or_(*ors)) -def _keep_event( +def _keep_row( hass: HomeAssistant, - event: LazyEventPartialState, + row: Row, entities_filter: EntityFilter | Callable[[str], bool] | None = None, ) -> bool: - if event.event_type in HOMEASSISTANT_EVENTS: + event_type = row.event_type + if event_type in HOMEASSISTANT_EVENTS: return entities_filter is None or entities_filter(HA_DOMAIN_ENTITY_ID) - if entity_id := event.data_entity_id: + if entity_id := _row_event_data_extract(row, ENTITY_ID_JSON_EXTRACT): return entities_filter is None or entities_filter(entity_id) - if event.event_type in hass.data[DOMAIN]: + if event_type in hass.data[DOMAIN]: # If the entity_id isn't described, use the domain that describes # the event for filtering. - domain = hass.data[DOMAIN][event.event_type][0] + domain = hass.data[DOMAIN][event_type][0] else: - domain = event.data_domain + domain = _row_event_data_extract(row, DOMAIN_JSON_EXTRACT) return domain is not None and ( entities_filter is None or entities_filter(f"{domain}._") ) -def _augment_data_with_context( - data: dict[str, Any], - entity_id: str | None, - event: LazyEventPartialState, - context_lookup: dict[str | None, LazyEventPartialState | None], - entity_attr_cache: EntityAttributeCache, - external_events: dict[ - str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] - ], -) -> None: - if not (context_event := context_lookup.get(event.context_id)): - return +class ContextAugmenter: + """Augment data with context trace.""" - if event == context_event: - # This is the first event with the given ID. Was it directly caused by - # a parent event? - if event.context_parent_id: - context_event = context_lookup.get(event.context_parent_id) - # Ensure the (parent) context_event exists and is not the root cause of - # this log entry. - if not context_event or event == context_event: + def __init__( + self, + context_lookup: dict[str | None, Row | None], + entity_name_cache: EntityNameCache, + external_events: dict[ + str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] + ], + event_cache: EventCache, + ) -> None: + """Init the augmenter.""" + self.context_lookup = context_lookup + self.entity_name_cache = entity_name_cache + self.external_events = external_events + self.event_cache = event_cache + + def augment(self, data: dict[str, Any], entity_id: str | None, row: Row) -> None: + """Augment data from the row and cache.""" + if not (context_row := self.context_lookup.get(row.context_id)): return - event_type = context_event.event_type + if _rows_match(row, context_row): + # This is the first event with the given ID. Was it directly caused by + # a parent event? + if ( + not row.context_parent_id + or (context_row := self.context_lookup.get(row.context_parent_id)) + is None + ): + return + # Ensure the (parent) context_event exists and is not the root cause of + # this log entry. + if _rows_match(row, context_row): + return - # State change - 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 + event_type = context_row.event_type + + # State change + if context_entity_id := context_row.entity_id: + data["context_entity_id"] = context_entity_id + data["context_entity_id_name"] = self.entity_name_cache.get( + context_entity_id, context_row + ) + data["context_event_type"] = event_type + return + + # Call service + if event_type == EVENT_CALL_SERVICE: + event = self.event_cache.get(context_row) + event_data = event.data + data["context_domain"] = event_data.get(ATTR_DOMAIN) + data["context_service"] = event_data.get(ATTR_SERVICE) + data["context_event_type"] = event_type + return + + if not entity_id: + return + + attr_entity_id = _row_event_data_extract(context_row, ENTITY_ID_JSON_EXTRACT) + if attr_entity_id is None or ( + event_type in SCRIPT_AUTOMATION_EVENTS and attr_entity_id == entity_id + ): + return + + data["context_entity_id"] = attr_entity_id + data["context_entity_id_name"] = self.entity_name_cache.get( + attr_entity_id, context_row ) data["context_event_type"] = event_type - return - event_data = context_event.data - - # Call service - if event_type == EVENT_CALL_SERVICE: - event_data = context_event.data - data["context_domain"] = event_data.get(ATTR_DOMAIN) - data["context_service"] = event_data.get(ATTR_SERVICE) - data["context_event_type"] = event_type - return - - if not entity_id or context_event == event: - return - - if (attr_entity_id := context_event.data_entity_id) is None or ( - event_type in SCRIPT_AUTOMATION_EVENTS and attr_entity_id == entity_id - ): - return - - data["context_entity_id"] = attr_entity_id - data["context_entity_id_name"] = _entity_name_from_event( - attr_entity_id, context_event, entity_attr_cache - ) - data["context_event_type"] = event_type - - if event_type in external_events: - domain, describe_event = external_events[event_type] - data["context_domain"] = domain - if name := describe_event(context_event).get(ATTR_NAME): - data["context_name"] = name - - -def _entity_name_from_event( - entity_id: str, - event: LazyEventPartialState, - entity_attr_cache: EntityAttributeCache, -) -> str: - """Extract the entity name from the event using the cache if possible.""" - return entity_attr_cache.get( - entity_id, ATTR_FRIENDLY_NAME, event - ) or split_entity_id(entity_id)[1].replace("_", " ") + if event_type in self.external_events: + domain, describe_event = self.external_events[event_type] + data["context_domain"] = domain + event = self.event_cache.get(context_row) + if name := describe_event(event).get(ATTR_NAME): + data["context_name"] = name def _is_sensor_continuous( @@ -834,23 +825,46 @@ def _is_sensor_continuous( ) +def _rows_match(row: Row, other_row: Row) -> bool: + """Check of rows match by using the same method as Events __hash__.""" + return bool( + row.event_type == other_row.event_type + and row.context_id == other_row.context_id + and row.time_fired == other_row.time_fired + ) + + +def _row_event_data_extract(row: Row, extractor: re.Pattern) -> str | None: + """Extract from event_data row.""" + result = extractor.search(row.shared_data or row.event_data or "") + return result.group(1) if result else None + + +def _row_attributes_extract(row: Row, extractor: re.Pattern) -> str | None: + """Extract from attributes row.""" + result = extractor.search(row.shared_attrs or row.attributes or "") + return result.group(1) if result else None + + +def _row_time_fired_isoformat(row: Row) -> dt | None: + """Convert the row timed_fired to isoformat.""" + return process_timestamp_to_utc_isoformat(row.time_fired) or dt_util.utcnow() + + class LazyEventPartialState: """A lazy version of core Event with limited State joined in.""" __slots__ = [ - "_row", + "row", "_event_data", - "_time_fired_isoformat", - "_attributes", + "_event_data_cache", "event_type", "entity_id", "state", - "_domain", "context_id", "context_user_id", "context_parent_id", - "time_fired_minute", - "_event_data_cache", + "data", ] def __init__( @@ -859,83 +873,28 @@ class LazyEventPartialState: event_data_cache: dict[str, dict[str, Any]], ) -> None: """Init the lazy event.""" - self._row = row + self.row = row self._event_data: dict[str, Any] | None = None - self._time_fired_isoformat: dt | None = None - self._domain: str | None = None - self.event_type: str = self._row.event_type - self.entity_id: str | None = self._row.entity_id - self.state = self._row.state - self.context_id: str | None = self._row.context_id - self.context_user_id: str | None = self._row.context_user_id - self.context_parent_id: str | None = self._row.context_parent_id - self.time_fired_minute: int = self._row.time_fired.minute self._event_data_cache = event_data_cache - - @property - def attributes_icon(self) -> str | None: - """Extract the icon from the decoded attributes or json.""" - result = ICON_JSON_EXTRACT.search( - self._row.shared_attrs or self._row.attributes or "" - ) - return result.group(1) if result else None - - @property - def data_entity_id(self) -> str | None: - """Extract the entity id from the decoded data or json.""" - if self._event_data: - return self._event_data.get(ATTR_ENTITY_ID) - - result = ENTITY_ID_JSON_EXTRACT.search( - self._row.shared_data or self._row.event_data or "" - ) - return result.group(1) if result else None - - @property - def data_domain(self) -> str | None: - """Extract the domain from the decoded data or json.""" - result = DOMAIN_JSON_EXTRACT.search( - self._row.shared_data or self._row.event_data or "" - ) - return result.group(1) if result else None - - @property - def attributes_friendly_name(self) -> str | None: - """Extract the friendly name from the decoded attributes or json.""" - result = FRIENDLY_NAME_JSON_EXTRACT.search( - self._row.shared_attrs or self._row.attributes or "" - ) - return result.group(1) if result else None - - @property - def data(self) -> dict[str, Any]: - """Event data.""" - if self._event_data is None: - source: str = self._row.shared_data or self._row.event_data - if not source: - self._event_data = {} - elif event_data := self._event_data_cache.get(source): - self._event_data = event_data - else: - self._event_data = self._event_data_cache[source] = cast( - dict[str, Any], json.loads(source) - ) - return self._event_data - - @property - def time_fired_isoformat(self) -> dt | None: - """Time event was fired in utc isoformat.""" - if not self._time_fired_isoformat: - self._time_fired_isoformat = ( - process_timestamp_to_utc_isoformat(self._row.time_fired) - or dt_util.utcnow() + self.event_type: str = self.row.event_type + self.entity_id: str | None = self.row.entity_id + self.state = self.row.state + self.context_id: str | None = self.row.context_id + self.context_user_id: str | None = self.row.context_user_id + self.context_parent_id: str | None = self.row.context_parent_id + source: str = self.row.shared_data or self.row.event_data + if not source: + self.data = {} + elif event_data := self._event_data_cache.get(source): + self.data = event_data + else: + self.data = self._event_data_cache[source] = cast( + dict[str, Any], json.loads(source) ) - return self._time_fired_isoformat - -class EntityAttributeCache: - """A cache to lookup static entity_id attribute. +class EntityNameCache: + """A cache to lookup the name for an entity. This class should not be used to lookup attributes that are expected to change state. @@ -944,27 +903,37 @@ class EntityAttributeCache: def __init__(self, hass: HomeAssistant) -> None: """Init the cache.""" self._hass = hass - self._cache: dict[str, dict[str, Any]] = {} + self._names: dict[str, str] = {} - def get(self, entity_id: str, attribute: str, event: LazyEventPartialState) -> Any: - """Lookup an attribute for an entity or get it from the cache.""" - if entity_id in self._cache: - if attribute in self._cache[entity_id]: - return self._cache[entity_id][attribute] + def get(self, entity_id: str, row: Row) -> str: + """Lookup an the friendly name.""" + if entity_id in self._names: + return self._names[entity_id] + if (current_state := self._hass.states.get(entity_id)) and ( + friendly_name := current_state.attributes.get(ATTR_FRIENDLY_NAME) + ): + self._names[entity_id] = friendly_name + elif extracted_name := _row_attributes_extract(row, FRIENDLY_NAME_JSON_EXTRACT): + self._names[entity_id] = extracted_name else: - cache = self._cache[entity_id] = {} + return split_entity_id(entity_id)[1].replace("_", " ") - if current_state := self._hass.states.get(entity_id): - # Try the current state as its faster than decoding the - # attributes - cache[attribute] = current_state.attributes.get(attribute) - else: - # If the entity has been removed, decode the attributes - # instead - if attribute != ATTR_FRIENDLY_NAME: - raise ValueError( - f"{attribute} is not supported by {self.__class__.__name__}" - ) - cache[attribute] = event.attributes_friendly_name + return self._names[entity_id] - return cache[attribute] + +class EventCache: + """Cache LazyEventPartialState by row.""" + + def __init__(self, event_data_cache: dict[str, dict[str, Any]]) -> None: + """Init the cache.""" + self._event_data_cache = event_data_cache + self.event_cache: dict[Row, LazyEventPartialState] = {} + + def get(self, row: Row) -> LazyEventPartialState: + """Get the event from the row.""" + if event := self.event_cache.get(row): + return event + event = self.event_cache[row] = LazyEventPartialState( + row, self._event_data_cache + ) + return event diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index 2062ef231b9..8f95f7db66b 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -16,7 +16,6 @@ from homeassistant.components.websocket_api.const import JSON_DUMP from homeassistant.const import EVENT_STATE_CHANGED from homeassistant.helpers.entityfilter import convert_include_exclude_filter from homeassistant.helpers.json import JSONEncoder -from homeassistant.util import dt as dt_util # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs # mypy: no-warn-return-any @@ -224,57 +223,6 @@ async def state_changed_event_filter_helper(hass): return timer() - start -@benchmark -async def logbook_filtering_state(hass): - """Filter state changes.""" - return await _logbook_filtering(hass, 1, 1) - - -@benchmark -async def logbook_filtering_attributes(hass): - """Filter attribute changes.""" - return await _logbook_filtering(hass, 1, 2) - - -@benchmark -async def _logbook_filtering(hass, last_changed, last_updated): - # pylint: disable=import-outside-toplevel - from homeassistant.components import logbook - - entity_id = "test.entity" - - old_state = {"entity_id": entity_id, "state": "off"} - - new_state = { - "entity_id": entity_id, - "state": "on", - "last_updated": last_updated, - "last_changed": last_changed, - } - - event = _create_state_changed_event_from_old_new( - entity_id, dt_util.utcnow(), old_state, new_state - ) - - entity_attr_cache = logbook.EntityAttributeCache(hass) - - entities_filter = convert_include_exclude_filter( - logbook.INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA({}) - ) - - def yield_events(event): - for _ in range(10**5): - # pylint: disable=protected-access - if logbook._keep_event(hass, event, entities_filter): - yield event - - start = timer() - - list(logbook.humanify(hass, yield_events(event), entity_attr_cache, {})) - - return timer() - start - - @benchmark async def filtering_entity_id(hass): """Run a 100k state changes through entity filter.""" diff --git a/tests/components/alexa/test_init.py b/tests/components/alexa/test_init.py index eac4b32e5ba..9dc47da6256 100644 --- a/tests/components/alexa/test_init.py +++ b/tests/components/alexa/test_init.py @@ -1,9 +1,8 @@ """Tests for alexa.""" -from homeassistant.components import logbook from homeassistant.components.alexa.const import EVENT_ALEXA_SMART_HOME from homeassistant.setup import async_setup_component -from tests.components.logbook.test_init import MockLazyEventPartialState +from tests.components.logbook.common import MockRow, mock_humanify async def test_humanify_alexa_event(hass): @@ -12,40 +11,35 @@ async def test_humanify_alexa_event(hass): await async_setup_component(hass, "alexa", {}) await async_setup_component(hass, "logbook", {}) hass.states.async_set("light.kitchen", "on", {"friendly_name": "Kitchen Light"}) - entity_attr_cache = logbook.EntityAttributeCache(hass) - results = list( - logbook.humanify( - hass, - [ - MockLazyEventPartialState( - EVENT_ALEXA_SMART_HOME, - {"request": {"namespace": "Alexa.Discovery", "name": "Discover"}}, - ), - MockLazyEventPartialState( - EVENT_ALEXA_SMART_HOME, - { - "request": { - "namespace": "Alexa.PowerController", - "name": "TurnOn", - "entity_id": "light.kitchen", - } - }, - ), - MockLazyEventPartialState( - EVENT_ALEXA_SMART_HOME, - { - "request": { - "namespace": "Alexa.PowerController", - "name": "TurnOn", - "entity_id": "light.non_existing", - } - }, - ), - ], - entity_attr_cache, - {}, - ) + results = mock_humanify( + hass, + [ + MockRow( + EVENT_ALEXA_SMART_HOME, + {"request": {"namespace": "Alexa.Discovery", "name": "Discover"}}, + ), + MockRow( + EVENT_ALEXA_SMART_HOME, + { + "request": { + "namespace": "Alexa.PowerController", + "name": "TurnOn", + "entity_id": "light.kitchen", + } + }, + ), + MockRow( + EVENT_ALEXA_SMART_HOME, + { + "request": { + "namespace": "Alexa.PowerController", + "name": "TurnOn", + "entity_id": "light.non_existing", + } + }, + ), + ], ) event1, event2, event3 = results diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 45f719a326f..af88adc00b7 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -6,7 +6,6 @@ from unittest.mock import Mock, patch import pytest -from homeassistant.components import logbook import homeassistant.components.automation as automation from homeassistant.components.automation import ( ATTR_SOURCE, @@ -53,7 +52,7 @@ from tests.common import ( async_mock_service, mock_restore_cache, ) -from tests.components.logbook.test_init import MockLazyEventPartialState +from tests.components.logbook.common import MockRow, mock_humanify @pytest.fixture @@ -1223,28 +1222,23 @@ async def test_logbook_humanify_automation_triggered_event(hass): hass.config.components.add("recorder") await async_setup_component(hass, automation.DOMAIN, {}) await async_setup_component(hass, "logbook", {}) - entity_attr_cache = logbook.EntityAttributeCache(hass) - event1, event2 = list( - logbook.humanify( - hass, - [ - MockLazyEventPartialState( - EVENT_AUTOMATION_TRIGGERED, - {ATTR_ENTITY_ID: "automation.hello", ATTR_NAME: "Hello Automation"}, - ), - MockLazyEventPartialState( - EVENT_AUTOMATION_TRIGGERED, - { - ATTR_ENTITY_ID: "automation.bye", - ATTR_NAME: "Bye Automation", - ATTR_SOURCE: "source of trigger", - }, - ), - ], - entity_attr_cache, - {}, - ) + event1, event2 = mock_humanify( + hass, + [ + MockRow( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_ENTITY_ID: "automation.hello", ATTR_NAME: "Hello Automation"}, + ), + MockRow( + EVENT_AUTOMATION_TRIGGERED, + { + ATTR_ENTITY_ID: "automation.bye", + ATTR_NAME: "Bye Automation", + ATTR_SOURCE: "source of trigger", + }, + ), + ], ) assert event1["name"] == "Hello Automation" diff --git a/tests/components/automation/test_logbook.py b/tests/components/automation/test_logbook.py index e13ebdc17a1..a3299d806ce 100644 --- a/tests/components/automation/test_logbook.py +++ b/tests/components/automation/test_logbook.py @@ -1,9 +1,9 @@ """Test automation logbook.""" -from homeassistant.components import automation, logbook +from homeassistant.components import automation from homeassistant.core import Context from homeassistant.setup import async_setup_component -from tests.components.logbook.test_init import MockLazyEventPartialState +from tests.components.logbook.common import MockRow, mock_humanify async def test_humanify_automation_trigger_event(hass): @@ -11,34 +11,29 @@ async def test_humanify_automation_trigger_event(hass): hass.config.components.add("recorder") assert await async_setup_component(hass, "automation", {}) assert await async_setup_component(hass, "logbook", {}) - entity_attr_cache = logbook.EntityAttributeCache(hass) context = Context() - event1, event2 = list( - logbook.humanify( - hass, - [ - MockLazyEventPartialState( - automation.EVENT_AUTOMATION_TRIGGERED, - { - "name": "Bla", - "entity_id": "automation.bla", - "source": "state change of input_boolean.yo", - }, - context=context, - ), - MockLazyEventPartialState( - automation.EVENT_AUTOMATION_TRIGGERED, - { - "name": "Bla", - "entity_id": "automation.bla", - }, - context=context, - ), - ], - entity_attr_cache, - {}, - ) + event1, event2 = mock_humanify( + hass, + [ + MockRow( + automation.EVENT_AUTOMATION_TRIGGERED, + { + "name": "Bla", + "entity_id": "automation.bla", + "source": "state change of input_boolean.yo", + }, + context=context, + ), + MockRow( + automation.EVENT_AUTOMATION_TRIGGERED, + { + "name": "Bla", + "entity_id": "automation.bla", + }, + context=context, + ), + ], ) assert event1["name"] == "Bla" diff --git a/tests/components/deconz/test_logbook.py b/tests/components/deconz/test_logbook.py index bfd5126c9db..98245a0df20 100644 --- a/tests/components/deconz/test_logbook.py +++ b/tests/components/deconz/test_logbook.py @@ -2,7 +2,6 @@ from unittest.mock import patch -from homeassistant.components import logbook from homeassistant.components.deconz.const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.deconz_event import ( CONF_DECONZ_ALARM_EVENT, @@ -21,7 +20,7 @@ from homeassistant.util import slugify from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.components.logbook.test_init import MockLazyEventPartialState +from tests.components.logbook.common import MockRow, mock_humanify async def test_humanifying_deconz_alarm_event(hass, aioclient_mock): @@ -67,26 +66,21 @@ async def test_humanifying_deconz_alarm_event(hass, aioclient_mock): hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) - entity_attr_cache = logbook.EntityAttributeCache(hass) - events = list( - logbook.humanify( - hass, - [ - MockLazyEventPartialState( - CONF_DECONZ_ALARM_EVENT, - { - CONF_CODE: 1234, - CONF_DEVICE_ID: keypad_entry.id, - CONF_EVENT: STATE_ALARM_ARMED_AWAY, - CONF_ID: keypad_event_id, - CONF_UNIQUE_ID: keypad_serial, - }, - ), - ], - entity_attr_cache, - {}, - ) + events = mock_humanify( + hass, + [ + MockRow( + CONF_DECONZ_ALARM_EVENT, + { + CONF_CODE: 1234, + CONF_DEVICE_ID: keypad_entry.id, + CONF_EVENT: STATE_ALARM_ARMED_AWAY, + CONF_ID: keypad_event_id, + CONF_UNIQUE_ID: keypad_serial, + }, + ), + ], ) assert events[0]["name"] == "Keypad" @@ -161,66 +155,61 @@ async def test_humanifying_deconz_event(hass, aioclient_mock): hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) - entity_attr_cache = logbook.EntityAttributeCache(hass) - events = list( - logbook.humanify( - hass, - [ - # Event without matching device trigger - MockLazyEventPartialState( - CONF_DECONZ_EVENT, - { - CONF_DEVICE_ID: switch_entry.id, - CONF_EVENT: 2000, - CONF_ID: switch_event_id, - CONF_UNIQUE_ID: switch_serial, - }, - ), - # Event with matching device trigger - MockLazyEventPartialState( - CONF_DECONZ_EVENT, - { - CONF_DEVICE_ID: hue_remote_entry.id, - CONF_EVENT: 2001, - CONF_ID: hue_remote_event_id, - CONF_UNIQUE_ID: hue_remote_serial, - }, - ), - # Gesture with matching device trigger - MockLazyEventPartialState( - CONF_DECONZ_EVENT, - { - CONF_DEVICE_ID: xiaomi_cube_entry.id, - CONF_GESTURE: 1, - CONF_ID: xiaomi_cube_event_id, - CONF_UNIQUE_ID: xiaomi_cube_serial, - }, - ), - # Unsupported device trigger - MockLazyEventPartialState( - CONF_DECONZ_EVENT, - { - CONF_DEVICE_ID: xiaomi_cube_entry.id, - CONF_GESTURE: "unsupported_gesture", - CONF_ID: xiaomi_cube_event_id, - CONF_UNIQUE_ID: xiaomi_cube_serial, - }, - ), - # Unknown event - MockLazyEventPartialState( - CONF_DECONZ_EVENT, - { - CONF_DEVICE_ID: faulty_entry.id, - "unknown_event": None, - CONF_ID: faulty_event_id, - CONF_UNIQUE_ID: faulty_serial, - }, - ), - ], - entity_attr_cache, - {}, - ) + events = mock_humanify( + hass, + [ + # Event without matching device trigger + MockRow( + CONF_DECONZ_EVENT, + { + CONF_DEVICE_ID: switch_entry.id, + CONF_EVENT: 2000, + CONF_ID: switch_event_id, + CONF_UNIQUE_ID: switch_serial, + }, + ), + # Event with matching device trigger + MockRow( + CONF_DECONZ_EVENT, + { + CONF_DEVICE_ID: hue_remote_entry.id, + CONF_EVENT: 2001, + CONF_ID: hue_remote_event_id, + CONF_UNIQUE_ID: hue_remote_serial, + }, + ), + # Gesture with matching device trigger + MockRow( + CONF_DECONZ_EVENT, + { + CONF_DEVICE_ID: xiaomi_cube_entry.id, + CONF_GESTURE: 1, + CONF_ID: xiaomi_cube_event_id, + CONF_UNIQUE_ID: xiaomi_cube_serial, + }, + ), + # Unsupported device trigger + MockRow( + CONF_DECONZ_EVENT, + { + CONF_DEVICE_ID: xiaomi_cube_entry.id, + CONF_GESTURE: "unsupported_gesture", + CONF_ID: xiaomi_cube_event_id, + CONF_UNIQUE_ID: xiaomi_cube_serial, + }, + ), + # Unknown event + MockRow( + CONF_DECONZ_EVENT, + { + CONF_DEVICE_ID: faulty_entry.id, + "unknown_event": None, + CONF_ID: faulty_event_id, + CONF_UNIQUE_ID: faulty_serial, + }, + ), + ], ) assert events[0]["name"] == "Switch 1" diff --git a/tests/components/google_assistant/test_logbook.py b/tests/components/google_assistant/test_logbook.py index 09d0b12e417..5875d44adc7 100644 --- a/tests/components/google_assistant/test_logbook.py +++ b/tests/components/google_assistant/test_logbook.py @@ -1,5 +1,4 @@ """The tests for Google Assistant logbook.""" -from homeassistant.components import logbook from homeassistant.components.google_assistant.const import ( DOMAIN, EVENT_COMMAND_RECEIVED, @@ -9,7 +8,7 @@ from homeassistant.components.google_assistant.const import ( from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME from homeassistant.setup import async_setup_component -from tests.components.logbook.test_init import MockLazyEventPartialState +from tests.components.logbook.common import MockRow, mock_humanify async def test_humanify_command_received(hass): @@ -18,48 +17,43 @@ async def test_humanify_command_received(hass): hass.config.components.add("frontend") hass.config.components.add("google_assistant") assert await async_setup_component(hass, "logbook", {}) - entity_attr_cache = logbook.EntityAttributeCache(hass) hass.states.async_set( "light.kitchen", "on", {ATTR_FRIENDLY_NAME: "The Kitchen Lights"} ) - events = list( - logbook.humanify( - hass, - [ - MockLazyEventPartialState( - EVENT_COMMAND_RECEIVED, - { - "request_id": "abcd", - ATTR_ENTITY_ID: ["light.kitchen"], - "execution": [ - { - "command": "action.devices.commands.OnOff", - "params": {"on": True}, - } - ], - "source": SOURCE_LOCAL, - }, - ), - MockLazyEventPartialState( - EVENT_COMMAND_RECEIVED, - { - "request_id": "abcd", - ATTR_ENTITY_ID: ["light.non_existing"], - "execution": [ - { - "command": "action.devices.commands.OnOff", - "params": {"on": False}, - } - ], - "source": SOURCE_CLOUD, - }, - ), - ], - entity_attr_cache, - {}, - ) + events = mock_humanify( + hass, + [ + MockRow( + EVENT_COMMAND_RECEIVED, + { + "request_id": "abcd", + ATTR_ENTITY_ID: ["light.kitchen"], + "execution": [ + { + "command": "action.devices.commands.OnOff", + "params": {"on": True}, + } + ], + "source": SOURCE_LOCAL, + }, + ), + MockRow( + EVENT_COMMAND_RECEIVED, + { + "request_id": "abcd", + ATTR_ENTITY_ID: ["light.non_existing"], + "execution": [ + { + "command": "action.devices.commands.OnOff", + "params": {"on": False}, + } + ], + "source": SOURCE_CLOUD, + }, + ), + ], ) assert len(events) == 2 diff --git a/tests/components/homekit/test_init.py b/tests/components/homekit/test_init.py index 8652f8b032a..17933616fc4 100644 --- a/tests/components/homekit/test_init.py +++ b/tests/components/homekit/test_init.py @@ -1,7 +1,6 @@ """Test HomeKit initialization.""" from unittest.mock import patch -from homeassistant.components import logbook from homeassistant.components.homekit.const import ( ATTR_DISPLAY_NAME, ATTR_VALUE, @@ -11,7 +10,7 @@ from homeassistant.components.homekit.const import ( from homeassistant.const import ATTR_ENTITY_ID, ATTR_SERVICE from homeassistant.setup import async_setup_component -from tests.components.logbook.test_init import MockLazyEventPartialState +from tests.components.logbook.common import MockRow, mock_humanify async def test_humanify_homekit_changed_event(hass, hk_driver, mock_get_source_ip): @@ -20,33 +19,28 @@ async def test_humanify_homekit_changed_event(hass, hk_driver, mock_get_source_i with patch("homeassistant.components.homekit.HomeKit"): assert await async_setup_component(hass, "homekit", {"homekit": {}}) assert await async_setup_component(hass, "logbook", {}) - entity_attr_cache = logbook.EntityAttributeCache(hass) - event1, event2 = list( - logbook.humanify( - hass, - [ - MockLazyEventPartialState( - EVENT_HOMEKIT_CHANGED, - { - ATTR_ENTITY_ID: "lock.front_door", - ATTR_DISPLAY_NAME: "Front Door", - ATTR_SERVICE: "lock", - }, - ), - MockLazyEventPartialState( - EVENT_HOMEKIT_CHANGED, - { - ATTR_ENTITY_ID: "cover.window", - ATTR_DISPLAY_NAME: "Window", - ATTR_SERVICE: "set_cover_position", - ATTR_VALUE: 75, - }, - ), - ], - entity_attr_cache, - {}, - ) + event1, event2 = mock_humanify( + hass, + [ + MockRow( + EVENT_HOMEKIT_CHANGED, + { + ATTR_ENTITY_ID: "lock.front_door", + ATTR_DISPLAY_NAME: "Front Door", + ATTR_SERVICE: "lock", + }, + ), + MockRow( + EVENT_HOMEKIT_CHANGED, + { + ATTR_ENTITY_ID: "cover.window", + ATTR_DISPLAY_NAME: "Window", + ATTR_SERVICE: "set_cover_position", + ATTR_VALUE: 75, + }, + ), + ], ) assert event1["name"] == "HomeKit" diff --git a/tests/components/logbook/common.py b/tests/components/logbook/common.py new file mode 100644 index 00000000000..c5b8c8239e1 --- /dev/null +++ b/tests/components/logbook/common.py @@ -0,0 +1,58 @@ +"""Tests for the logbook component.""" +from __future__ import annotations + +import json +from typing import Any + +from homeassistant.components import logbook +from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat +from homeassistant.core import Context +from homeassistant.helpers.json import JSONEncoder +import homeassistant.util.dt as dt_util + + +class MockRow: + """Minimal row mock.""" + + def __init__( + self, + event_type: str, + data: dict[str, Any] | None = None, + context: Context | None = None, + ): + """Init the fake row.""" + self.event_type = event_type + self.shared_data = json.dumps(data, cls=JSONEncoder) + self.data = data + self.time_fired = dt_util.utcnow() + self.context_parent_id = context.parent_id if context else None + self.context_user_id = context.user_id if context else None + self.context_id = context.id if context else None + self.state = None + self.entity_id = None + + @property + def time_fired_minute(self): + """Minute the event was fired.""" + return self.time_fired.minute + + @property + def time_fired_isoformat(self): + """Time event was fired in utc isoformat.""" + return process_timestamp_to_utc_isoformat(self.time_fired) + + +def mock_humanify(hass_, rows): + """Wrap humanify with mocked logbook objects.""" + event_data_cache = {} + context_lookup = {} + entity_name_cache = logbook.EntityNameCache(hass_) + event_cache = logbook.EventCache(event_data_cache) + context_augmenter = logbook.ContextAugmenter( + context_lookup, entity_name_cache, {}, event_cache + ) + return list( + logbook.humanify( + hass_, rows, entity_name_cache, event_cache, context_augmenter + ), + ) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 2612765584f..ad5423286d9 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -4,6 +4,7 @@ import collections from datetime import datetime, timedelta from http import HTTPStatus import json +from typing import Any from unittest.mock import Mock, patch import pytest @@ -41,6 +42,8 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from .common import mock_humanify + from tests.common import async_capture_events, mock_platform from tests.components.recorder.common import ( async_recorder_block_till_done, @@ -212,16 +215,11 @@ def test_home_assistant_start_stop_grouped(hass_): Events that are occurring in the same minute. """ - entity_attr_cache = logbook.EntityAttributeCache(hass_) - entries = list( - logbook.humanify( - hass_, - ( - MockLazyEventPartialState(EVENT_HOMEASSISTANT_STOP), - MockLazyEventPartialState(EVENT_HOMEASSISTANT_START), - ), - entity_attr_cache, - {}, + entries = mock_humanify( + hass_, + ( + MockRow(EVENT_HOMEASSISTANT_STOP), + MockRow(EVENT_HOMEASSISTANT_START), ), ) @@ -231,30 +229,17 @@ def test_home_assistant_start_stop_grouped(hass_): ) -def test_unsupported_attributes_in_cache_throws(hass): - """Test unsupported attributes in cache.""" - entity_attr_cache = logbook.EntityAttributeCache(hass) - event = MockLazyEventPartialState(EVENT_STATE_CHANGED) - with pytest.raises(ValueError): - entity_attr_cache.get("sensor.xyz", "not_supported", event) - - def test_home_assistant_start(hass_): """Test if HA start is not filtered or converted into a restart.""" entity_id = "switch.bla" pointA = dt_util.utcnow() - entity_attr_cache = logbook.EntityAttributeCache(hass_) - entries = list( - logbook.humanify( - hass_, - ( - MockLazyEventPartialState(EVENT_HOMEASSISTANT_START), - create_state_changed_event(pointA, entity_id, 10), - ), - entity_attr_cache, - {}, - ) + entries = mock_humanify( + hass_, + ( + MockRow(EVENT_HOMEASSISTANT_START), + create_state_changed_event(pointA, entity_id, 10).row, + ), ) assert len(entries) == 2 @@ -267,24 +252,19 @@ def test_process_custom_logbook_entries(hass_): name = "Nice name" message = "has a custom entry" entity_id = "sun.sun" - entity_attr_cache = logbook.EntityAttributeCache(hass_) - entries = list( - logbook.humanify( - hass_, - ( - MockLazyEventPartialState( - logbook.EVENT_LOGBOOK_ENTRY, - { - logbook.ATTR_NAME: name, - logbook.ATTR_MESSAGE: message, - logbook.ATTR_ENTITY_ID: entity_id, - }, - ), + entries = mock_humanify( + hass_, + ( + MockRow( + logbook.EVENT_LOGBOOK_ENTRY, + { + logbook.ATTR_NAME: name, + logbook.ATTR_MESSAGE: message, + logbook.ATTR_ENTITY_ID: entity_id, + }, ), - entity_attr_cache, - {}, - ) + ), ) assert len(entries) == 1 @@ -343,11 +323,13 @@ def create_state_changed_event_from_old_new( "state_id", "old_state_id", "shared_attrs", + "shared_data", ], ) row.event_type = EVENT_STATE_CHANGED row.event_data = "{}" + row.shared_data = "{}" row.attributes = attributes_json row.shared_attrs = attributes_json row.time_fired = event_time_fired @@ -1987,34 +1969,26 @@ def _assert_entry( assert state == entry["state"] -class MockLazyEventPartialState(ha.Event): - """Minimal mock of a Lazy event.""" +class MockRow: + """Minimal row mock.""" - @property - def data_entity_id(self): - """Lookup entity id.""" - return self.data.get(ATTR_ENTITY_ID) - - @property - def data_domain(self): - """Lookup domain.""" - return self.data.get(ATTR_DOMAIN) + def __init__(self, event_type: str, data: dict[str, Any] = None): + """Init the fake row.""" + self.event_type = event_type + self.shared_data = json.dumps(data, cls=JSONEncoder) + self.data = data + self.time_fired = dt_util.utcnow() + self.context_parent_id = None + self.context_user_id = None + self.context_id = None + self.state = None + self.entity_id = None @property def time_fired_minute(self): """Minute the event was fired.""" return self.time_fired.minute - @property - def context_user_id(self): - """Context user id of event.""" - return self.context.user_id - - @property - def context_id(self): - """Context id of event.""" - return self.context.id - @property def time_fired_isoformat(self): """Time event was fired in utc isoformat.""" diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index ca0cdb97592..480bbcbc4f0 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -6,7 +6,7 @@ from unittest.mock import Mock, patch import pytest -from homeassistant.components import logbook, script +from homeassistant.components import script from homeassistant.components.script import DOMAIN, EVENT_SCRIPT_STARTED from homeassistant.const import ( ATTR_ENTITY_ID, @@ -41,7 +41,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed, async_mock_service, mock_restore_cache -from tests.components.logbook.test_init import MockLazyEventPartialState +from tests.components.logbook.common import MockRow, mock_humanify ENTITY_ID = "script.test" @@ -526,24 +526,19 @@ async def test_logbook_humanify_script_started_event(hass): hass.config.components.add("recorder") await async_setup_component(hass, DOMAIN, {}) await async_setup_component(hass, "logbook", {}) - entity_attr_cache = logbook.EntityAttributeCache(hass) - event1, event2 = list( - logbook.humanify( - hass, - [ - MockLazyEventPartialState( - EVENT_SCRIPT_STARTED, - {ATTR_ENTITY_ID: "script.hello", ATTR_NAME: "Hello Script"}, - ), - MockLazyEventPartialState( - EVENT_SCRIPT_STARTED, - {ATTR_ENTITY_ID: "script.bye", ATTR_NAME: "Bye Script"}, - ), - ], - entity_attr_cache, - {}, - ) + event1, event2 = mock_humanify( + hass, + [ + MockRow( + EVENT_SCRIPT_STARTED, + {ATTR_ENTITY_ID: "script.hello", ATTR_NAME: "Hello Script"}, + ), + MockRow( + EVENT_SCRIPT_STARTED, + {ATTR_ENTITY_ID: "script.bye", ATTR_NAME: "Bye Script"}, + ), + ], ) assert event1["name"] == "Hello Script" diff --git a/tests/components/shelly/test_logbook.py b/tests/components/shelly/test_logbook.py index 9ece9590cbb..1ba7ea7ed16 100644 --- a/tests/components/shelly/test_logbook.py +++ b/tests/components/shelly/test_logbook.py @@ -1,5 +1,4 @@ """The tests for Shelly logbook.""" -from homeassistant.components import logbook from homeassistant.components.shelly.const import ( ATTR_CHANNEL, ATTR_CLICK_TYPE, @@ -10,7 +9,7 @@ from homeassistant.components.shelly.const import ( from homeassistant.const import ATTR_DEVICE_ID from homeassistant.setup import async_setup_component -from tests.components.logbook.test_init import MockLazyEventPartialState +from tests.components.logbook.common import MockRow, mock_humanify async def test_humanify_shelly_click_event_block_device(hass, coap_wrapper): @@ -18,34 +17,29 @@ async def test_humanify_shelly_click_event_block_device(hass, coap_wrapper): assert coap_wrapper hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) - entity_attr_cache = logbook.EntityAttributeCache(hass) - event1, event2 = list( - logbook.humanify( - hass, - [ - MockLazyEventPartialState( - EVENT_SHELLY_CLICK, - { - ATTR_DEVICE_ID: coap_wrapper.device_id, - ATTR_DEVICE: "shellyix3-12345678", - ATTR_CLICK_TYPE: "single", - ATTR_CHANNEL: 1, - }, - ), - MockLazyEventPartialState( - EVENT_SHELLY_CLICK, - { - ATTR_DEVICE_ID: "no_device_id", - ATTR_DEVICE: "shellyswitch25-12345678", - ATTR_CLICK_TYPE: "long", - ATTR_CHANNEL: 2, - }, - ), - ], - entity_attr_cache, - {}, - ) + event1, event2 = mock_humanify( + hass, + [ + MockRow( + EVENT_SHELLY_CLICK, + { + ATTR_DEVICE_ID: coap_wrapper.device_id, + ATTR_DEVICE: "shellyix3-12345678", + ATTR_CLICK_TYPE: "single", + ATTR_CHANNEL: 1, + }, + ), + MockRow( + EVENT_SHELLY_CLICK, + { + ATTR_DEVICE_ID: "no_device_id", + ATTR_DEVICE: "shellyswitch25-12345678", + ATTR_CLICK_TYPE: "long", + ATTR_CHANNEL: 2, + }, + ), + ], ) assert event1["name"] == "Shelly" @@ -68,34 +62,29 @@ async def test_humanify_shelly_click_event_rpc_device(hass, rpc_wrapper): assert rpc_wrapper hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) - entity_attr_cache = logbook.EntityAttributeCache(hass) - event1, event2 = list( - logbook.humanify( - hass, - [ - MockLazyEventPartialState( - EVENT_SHELLY_CLICK, - { - ATTR_DEVICE_ID: rpc_wrapper.device_id, - ATTR_DEVICE: "shellyplus1pm-12345678", - ATTR_CLICK_TYPE: "single_push", - ATTR_CHANNEL: 1, - }, - ), - MockLazyEventPartialState( - EVENT_SHELLY_CLICK, - { - ATTR_DEVICE_ID: "no_device_id", - ATTR_DEVICE: "shellypro4pm-12345678", - ATTR_CLICK_TYPE: "btn_down", - ATTR_CHANNEL: 2, - }, - ), - ], - entity_attr_cache, - {}, - ) + event1, event2 = mock_humanify( + hass, + [ + MockRow( + EVENT_SHELLY_CLICK, + { + ATTR_DEVICE_ID: rpc_wrapper.device_id, + ATTR_DEVICE: "shellyplus1pm-12345678", + ATTR_CLICK_TYPE: "single_push", + ATTR_CHANNEL: 1, + }, + ), + MockRow( + EVENT_SHELLY_CLICK, + { + ATTR_DEVICE_ID: "no_device_id", + ATTR_DEVICE: "shellypro4pm-12345678", + ATTR_CLICK_TYPE: "btn_down", + ATTR_CHANNEL: 2, + }, + ), + ], ) assert event1["name"] == "Shelly" From 1be2438ef67c7f523654bdb849cbed5f4c865365 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 9 May 2022 08:15:11 +0200 Subject: [PATCH 0322/3516] Use climate enums in mqtt (#70696) --- homeassistant/components/mqtt/climate.py | 45 +++++++++++------------- tests/components/mqtt/test_climate.py | 39 ++++++++------------ 2 files changed, 36 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 5c53380bb71..deb1021b9d7 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -10,7 +10,6 @@ from homeassistant.components import climate from homeassistant.components.climate import ( PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, ClimateEntity, - ClimateEntityFeature, ) from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, @@ -23,14 +22,13 @@ from homeassistant.components.climate.const import ( FAN_HIGH, FAN_LOW, FAN_MEDIUM, - HVAC_MODE_AUTO, - HVAC_MODE_COOL, - HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_HEAT, - HVAC_MODE_OFF, PRESET_AWAY, PRESET_NONE, + SWING_OFF, + SWING_ON, + ClimateEntityFeature, + HVACAction, + HVACMode, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -43,7 +41,6 @@ from homeassistant.const import ( PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, - STATE_ON, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -272,12 +269,12 @@ _PLATFORM_SCHEMA_BASE = SCHEMA_BASE.extend( vol.Optional( CONF_MODE_LIST, default=[ - HVAC_MODE_AUTO, - HVAC_MODE_OFF, - HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, + HVACMode.AUTO, + HVACMode.OFF, + HVACMode.COOL, + HVACMode.HEAT, + HVACMode.DRY, + HVACMode.FAN_ONLY, ], ): cv.ensure_list, vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, @@ -309,7 +306,7 @@ _PLATFORM_SCHEMA_BASE = SCHEMA_BASE.extend( vol.Optional(CONF_SWING_MODE_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( - CONF_SWING_MODE_LIST, default=[STATE_ON, HVAC_MODE_OFF] + CONF_SWING_MODE_LIST, default=[SWING_ON, SWING_OFF] ): cv.ensure_list, vol.Optional(CONF_SWING_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_SWING_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, @@ -460,9 +457,9 @@ class MqttClimate(MqttEntity, ClimateEntity): if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: self._current_fan_mode = FAN_LOW if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: - self._current_swing_mode = HVAC_MODE_OFF + self._current_swing_mode = SWING_OFF if self._topic[CONF_MODE_STATE_TOPIC] is None: - self._current_operation = HVAC_MODE_OFF + self._current_operation = HVACMode.OFF self._feature_preset_mode = CONF_PRESET_MODE_COMMAND_TOPIC in config if self._feature_preset_mode: self._preset_modes = config[CONF_PRESET_MODES_LIST] @@ -773,17 +770,17 @@ class MqttClimate(MqttEntity, ClimateEntity): return self._target_temp_high @property - def hvac_action(self): + def hvac_action(self) -> HVACAction | None: """Return the current running hvac operation if supported.""" return self._action @property - def hvac_mode(self): + def hvac_mode(self) -> HVACMode: """Return current operation ie. heat, cool, idle.""" return self._current_operation @property - def hvac_modes(self): + def hvac_modes(self) -> list[HVACMode]: """Return the list of available operation modes.""" return self._config[CONF_MODE_LIST] @@ -859,7 +856,7 @@ class MqttClimate(MqttEntity, ClimateEntity): setattr(self, attr, temp) # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if self._send_if_off or self._current_operation != HVAC_MODE_OFF: + if self._send_if_off or self._current_operation != HVACMode.OFF: payload = self._command_templates[cmnd_template](temp) await self._publish(cmnd_topic, payload) @@ -899,7 +896,7 @@ class MqttClimate(MqttEntity, ClimateEntity): async def async_set_swing_mode(self, swing_mode): """Set new swing mode.""" # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if self._send_if_off or self._current_operation != HVAC_MODE_OFF: + if self._send_if_off or self._current_operation != HVACMode.OFF: payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE]( swing_mode ) @@ -912,7 +909,7 @@ class MqttClimate(MqttEntity, ClimateEntity): async def async_set_fan_mode(self, fan_mode): """Set new target temperature.""" # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if self._send_if_off or self._current_operation != HVAC_MODE_OFF: + if self._send_if_off or self._current_operation != HVACMode.OFF: payload = self._command_templates[CONF_FAN_MODE_COMMAND_TEMPLATE](fan_mode) await self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload) @@ -922,7 +919,7 @@ class MqttClimate(MqttEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode) -> None: """Set new operation mode.""" - if hvac_mode == HVAC_MODE_OFF: + if hvac_mode == HVACMode.OFF: await self._publish( CONF_POWER_COMMAND_TOPIC, self._config[CONF_PAYLOAD_OFF] ) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 0c1db16d6fe..af6d65ac490 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -19,23 +19,14 @@ from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_ACTIONS, DOMAIN as CLIMATE_DOMAIN, - HVAC_MODE_AUTO, - HVAC_MODE_COOL, - HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_HEAT, PRESET_AWAY, PRESET_ECO, PRESET_NONE, - SUPPORT_AUX_HEAT, - SUPPORT_FAN_MODE, - SUPPORT_PRESET_MODE, - SUPPORT_SWING_MODE, - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_RANGE, + ClimateEntityFeature, + HVACMode, ) from homeassistant.components.mqtt.climate import MQTT_CLIMATE_ATTRIBUTES_BLOCKED -from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF +from homeassistant.const import ATTR_TEMPERATURE from homeassistant.setup import async_setup_component from .test_common import ( @@ -171,12 +162,12 @@ async def test_supported_features(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) support = ( - SUPPORT_TARGET_TEMPERATURE - | SUPPORT_SWING_MODE - | SUPPORT_FAN_MODE - | SUPPORT_PRESET_MODE - | SUPPORT_AUX_HEAT - | SUPPORT_TARGET_TEMPERATURE_RANGE + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.SWING_MODE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.AUX_HEAT + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE ) assert state.attributes.get("supported_features") == support @@ -190,12 +181,12 @@ async def test_get_hvac_modes(hass, mqtt_mock): state = hass.states.get(ENTITY_CLIMATE) modes = state.attributes.get("hvac_modes") assert [ - HVAC_MODE_AUTO, - STATE_OFF, - HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, + HVACMode.AUTO, + HVACMode.OFF, + HVACMode.COOL, + HVACMode.HEAT, + HVACMode.DRY, + HVACMode.FAN_ONLY, ] == modes From f50681e3d388f952d53bdd35277a6fdcc8cb01ca Mon Sep 17 00:00:00 2001 From: Shai Ungar Date: Mon, 9 May 2022 10:27:23 +0300 Subject: [PATCH 0323/3516] Migrate sabnzbd sensors unique ids (#71455) * Migrate sensors unique ids 1. migrate sensors to have unique id constructed also from entry_id 2. add migration flow in init 3. bump config flow to version 2 4. add tests for migration * move migrate to async_setup_entry * 1. Use the entity registry api in tests 2. Set up the config entry and not use integration directly 3. remove patch for entity registry * fix too many lines * Update tests/components/sabnzbd/test_init.py Co-authored-by: Martin Hjelmare * Update tests/components/sabnzbd/test_init.py Co-authored-by: Martin Hjelmare * Update tests/components/sabnzbd/test_init.py Co-authored-by: Martin Hjelmare * Update tests/components/sabnzbd/test_init.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/sabnzbd/__init__.py | 56 +++++++++++++ homeassistant/components/sabnzbd/sensor.py | 35 +++++--- tests/components/sabnzbd/test_config_flow.py | 1 - tests/components/sabnzbd/test_init.py | 85 ++++++++++++++++++++ 4 files changed, 164 insertions(+), 13 deletions(-) create mode 100644 tests/components/sabnzbd/test_init.py diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index aca50e404a2..3fa5054a5f0 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -1,4 +1,6 @@ """Support for monitoring an SABnzbd NZB client.""" +from __future__ import annotations + from collections.abc import Callable import logging @@ -19,7 +21,9 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import async_get from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType @@ -123,8 +127,56 @@ def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) raise ValueError(f"No api for API key: {call_data_api_key}") +def update_device_identifiers(hass: HomeAssistant, entry: ConfigEntry): + """Update device identifiers to new identifiers.""" + device_registry = async_get(hass) + device_entry = device_registry.async_get_device({(DOMAIN, DOMAIN)}) + if device_entry and entry.entry_id in device_entry.config_entries: + new_identifiers = {(DOMAIN, entry.entry_id)} + _LOGGER.debug( + "Updating device id <%s> with new identifiers <%s>", + device_entry.id, + new_identifiers, + ) + device_registry.async_update_device( + device_entry.id, new_identifiers=new_identifiers + ) + + +async def migrate_unique_id(hass: HomeAssistant, entry: ConfigEntry): + """Migrate entities to new unique ids (with entry_id).""" + + @callback + def async_migrate_callback(entity_entry: RegistryEntry) -> dict | None: + """ + Define a callback to migrate appropriate SabnzbdSensor entities to new unique IDs. + + Old: description.key + New: {entry_id}_description.key + """ + entry_id = entity_entry.config_entry_id + if entry_id is None: + return None + if entity_entry.unique_id.startswith(entry_id): + return None + + new_unique_id = f"{entry_id}_{entity_entry.unique_id}" + + _LOGGER.debug( + "Migrating entity %s from old unique ID '%s' to new unique ID '%s'", + entity_entry.entity_id, + entity_entry.unique_id, + new_unique_id, + ) + + return {"new_unique_id": new_unique_id} + + await async_migrate_entries(hass, entry.entry_id, async_migrate_callback) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the SabNzbd Component.""" + sab_api = await get_client(hass, entry.data) if not sab_api: raise ConfigEntryNotReady @@ -137,6 +189,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: KEY_NAME: entry.data[CONF_NAME], } + await migrate_unique_id(hass, entry) + update_device_identifiers(hass, entry) + @callback def extract_api(func: Callable) -> Callable: """Define a decorator to get the correct api for a service call.""" @@ -188,6 +243,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error(err) async_track_time_interval(hass, async_update_sabnzbd, UPDATE_INTERVAL) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index 539eaa4f097..ebdb9190ed9 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -113,11 +113,16 @@ async def async_setup_entry( ) -> None: """Set up a Sabnzbd sensor entry.""" - sab_api_data = hass.data[DOMAIN][config_entry.entry_id][KEY_API_DATA] - client_name = hass.data[DOMAIN][config_entry.entry_id][KEY_NAME] + entry_id = config_entry.entry_id + + sab_api_data = hass.data[DOMAIN][entry_id][KEY_API_DATA] + client_name = hass.data[DOMAIN][entry_id][KEY_NAME] async_add_entities( - [SabnzbdSensor(sab_api_data, client_name, sensor) for sensor in SENSOR_TYPES] + [ + SabnzbdSensor(sab_api_data, client_name, sensor, entry_id) + for sensor in SENSOR_TYPES + ] ) @@ -128,17 +133,21 @@ class SabnzbdSensor(SensorEntity): _attr_should_poll = False def __init__( - self, sabnzbd_api_data, client_name, description: SabnzbdSensorEntityDescription + self, + sabnzbd_api_data, + client_name, + description: SabnzbdSensorEntityDescription, + entry_id, ): """Initialize the sensor.""" - unique_id = description.key - self._attr_unique_id = unique_id + + self._attr_unique_id = f"{entry_id}_{description.key}" self.entity_description = description self._sabnzbd_api = sabnzbd_api_data self._attr_name = f"{client_name} {description.name}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, DOMAIN)}, + identifiers={(DOMAIN, entry_id)}, name=DEFAULT_NAME, ) @@ -156,9 +165,11 @@ class SabnzbdSensor(SensorEntity): self.entity_description.key ) - if self.entity_description.key == SPEED_KEY: - self._attr_native_value = round(float(self._attr_native_value) / 1024, 1) - elif "size" in self.entity_description.key: - self._attr_native_value = round(float(self._attr_native_value), 2) - + if self._attr_native_value is not None: + if self.entity_description.key == SPEED_KEY: + self._attr_native_value = round( + float(self._attr_native_value) / 1024, 1 + ) + elif "size" in self.entity_description.key: + self._attr_native_value = round(float(self._attr_native_value), 2) self.schedule_update_ha_state() diff --git a/tests/components/sabnzbd/test_config_flow.py b/tests/components/sabnzbd/test_config_flow.py index d04c5b18ab1..bc72dff2535 100644 --- a/tests/components/sabnzbd/test_config_flow.py +++ b/tests/components/sabnzbd/test_config_flow.py @@ -87,7 +87,6 @@ async def test_import_flow(hass) -> None: "homeassistant.components.sabnzbd.sab.SabnzbdApi.check_available", return_value=True, ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, diff --git a/tests/components/sabnzbd/test_init.py b/tests/components/sabnzbd/test_init.py new file mode 100644 index 00000000000..9bdef4119d0 --- /dev/null +++ b/tests/components/sabnzbd/test_init.py @@ -0,0 +1,85 @@ +"""Tests for the SABnzbd Integration.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.sabnzbd import DEFAULT_NAME, DOMAIN, SENSOR_KEYS +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_URL +from homeassistant.helpers.device_registry import DeviceEntryType + +from tests.common import MockConfigEntry, mock_device_registry, mock_registry + +MOCK_ENTRY_ID = "mock_entry_id" + +MOCK_UNIQUE_ID = "someuniqueid" + +MOCK_DEVICE_ID = "somedeviceid" + +MOCK_DATA_VERSION_1 = { + CONF_API_KEY: "api_key", + CONF_URL: "http://127.0.0.1:8080", + CONF_NAME: "name", +} + +MOCK_ENTRY_VERSION_1 = MockConfigEntry( + domain=DOMAIN, data=MOCK_DATA_VERSION_1, entry_id=MOCK_ENTRY_ID, version=1 +) + + +@pytest.fixture +def device_registry(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_registry(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_unique_id_migrate(hass, device_registry, entity_registry): + """Test that config flow entry is migrated correctly.""" + # Start with the config entry at Version 1. + mock_entry = MOCK_ENTRY_VERSION_1 + mock_entry.add_to_hass(hass) + + mock_d_entry = device_registry.async_get_or_create( + config_entry_id=mock_entry.entry_id, + identifiers={(DOMAIN, DOMAIN)}, + name=DEFAULT_NAME, + entry_type=DeviceEntryType.SERVICE, + ) + + entity_id_sensor_key = [] + + for sensor_key in SENSOR_KEYS: + mock_entity_id = f"{SENSOR_DOMAIN}.{DOMAIN}_{sensor_key}" + entity_registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + unique_id=sensor_key, + config_entry=mock_entry, + device_id=mock_d_entry.id, + ) + entity = entity_registry.async_get(mock_entity_id) + assert entity.entity_id == mock_entity_id + assert entity.unique_id == sensor_key + entity_id_sensor_key.append((mock_entity_id, sensor_key)) + + with patch( + "homeassistant.components.sabnzbd.sab.SabnzbdApi.check_available", + return_value=True, + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + + await hass.async_block_till_done() + + for mock_entity_id, sensor_key in entity_id_sensor_key: + entity = entity_registry.async_get(mock_entity_id) + assert entity.unique_id == f"{MOCK_ENTRY_ID}_{sensor_key}" + + assert device_registry.async_get(mock_d_entry.id).identifiers == { + (DOMAIN, MOCK_ENTRY_ID) + } From bb4a5ccc2c05a9e17074ef9509d06dd66f3e12f5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 May 2022 11:03:32 +0200 Subject: [PATCH 0324/3516] Correct device class for meater cook sensors (#71565) --- homeassistant/components/meater/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index 8a6c07bcbc4..f6d06dc2b25 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -176,8 +176,6 @@ class MeaterProbeTemperature( ): """Meater Temperature Sensor Entity.""" - _attr_device_class = SensorDeviceClass.TEMPERATURE - _attr_native_unit_of_measurement = TEMP_CELSIUS entity_description: MeaterSensorEntityDescription def __init__( From 5d9dc8252b5d08fffac3e8f9cdd54246defb1301 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 9 May 2022 11:52:08 +0200 Subject: [PATCH 0325/3516] Use helper for testing an event change (#71579) --- tests/components/mqtt/test_discovery.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index a1ef6ea477a..9215ab651b2 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -24,6 +24,7 @@ from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, + async_capture_events, async_fire_mqtt_message, mock_device_registry, mock_entity_platform, @@ -438,14 +439,7 @@ async def test_rediscover(hass, mqtt_mock, caplog): async def test_rapid_rediscover(hass, mqtt_mock, caplog): """Test immediate rediscover of removed component.""" - events = [] - - @ha.callback - def callback(event): - """Verify event got called.""" - events.append(event) - - hass.bus.async_listen(EVENT_STATE_CHANGED, callback) + events = async_capture_events(hass, EVENT_STATE_CHANGED) async_fire_mqtt_message( hass, From 1cc9800a939327c59b21da849daa5214cb58af57 Mon Sep 17 00:00:00 2001 From: Oscar Calvo <2091582+ocalvo@users.noreply.github.com> Date: Mon, 9 May 2022 03:06:29 -0700 Subject: [PATCH 0326/3516] Support custom baud speed (#68320) Co-authored-by: Franck Nijhof --- homeassistant/components/sms/__init__.py | 24 ++++++++++++++++++--- homeassistant/components/sms/config_flow.py | 20 +++++++++++++---- homeassistant/components/sms/const.py | 24 +++++++++++++++++++++ homeassistant/components/sms/gateway.py | 1 + homeassistant/components/sms/strings.json | 5 ++++- 5 files changed, 66 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index 9b091942556..b1c2703409c 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -1,4 +1,6 @@ """The sms component.""" +import logging + import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -7,13 +9,24 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, SMS_GATEWAY +from .const import CONF_BAUD_SPEED, DEFAULT_BAUD_SPEED, DOMAIN, SMS_GATEWAY from .gateway import create_sms_gateway +_LOGGER = logging.getLogger(__name__) + PLATFORMS = [Platform.SENSOR] +SMS_CONFIG_SCHEMA = {vol.Required(CONF_DEVICE): cv.isdevice} + CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema({vol.Required(CONF_DEVICE): cv.isdevice})}, + { + DOMAIN: vol.Schema( + vol.All( + cv.deprecated(CONF_DEVICE), + SMS_CONFIG_SCHEMA, + ), + ) + }, extra=vol.ALLOW_EXTRA, ) @@ -39,7 +52,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Configure Gammu state machine.""" device = entry.data[CONF_DEVICE] - config = {"Device": device, "Connection": "at"} + connection_mode = "at" + baud_speed = entry.data.get(CONF_BAUD_SPEED, DEFAULT_BAUD_SPEED) + if baud_speed != DEFAULT_BAUD_SPEED: + connection_mode += baud_speed + config = {"Device": device, "Connection": connection_mode} + _LOGGER.debug("Connecting mode:%s", connection_mode) gateway = await create_sms_gateway(config, hass) if not gateway: return False diff --git a/homeassistant/components/sms/config_flow.py b/homeassistant/components/sms/config_flow.py index 37b78ee3ea3..acc33075397 100644 --- a/homeassistant/components/sms/config_flow.py +++ b/homeassistant/components/sms/config_flow.py @@ -6,13 +6,21 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_DEVICE +from homeassistant.helpers import selector -from .const import DOMAIN +from .const import CONF_BAUD_SPEED, DEFAULT_BAUD_SPEED, DEFAULT_BAUD_SPEEDS, DOMAIN from .gateway import create_sms_gateway _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema({vol.Required(CONF_DEVICE): str}) +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_DEVICE): str, + vol.Optional(CONF_BAUD_SPEED, default=DEFAULT_BAUD_SPEED): selector.selector( + {"select": {"options": DEFAULT_BAUD_SPEEDS}} + ), + } +) async def get_imei_from_config(hass: core.HomeAssistant, data): @@ -21,13 +29,17 @@ async def get_imei_from_config(hass: core.HomeAssistant, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ device = data[CONF_DEVICE] - config = {"Device": device, "Connection": "at"} + connection_mode = "at" + baud_speed = data.get(CONF_BAUD_SPEED, DEFAULT_BAUD_SPEED) + if baud_speed != DEFAULT_BAUD_SPEED: + connection_mode += baud_speed + config = {"Device": device, "Connection": connection_mode} gateway = await create_sms_gateway(config, hass) if not gateway: raise CannotConnect try: imei = await gateway.get_imei_async() - except gammu.GSMError as err: + except gammu.GSMError as err: # pylint: disable=no-member raise CannotConnect from err finally: await gateway.terminate_async() diff --git a/homeassistant/components/sms/const.py b/homeassistant/components/sms/const.py index ab2c15a0c49..7c40a04073c 100644 --- a/homeassistant/components/sms/const.py +++ b/homeassistant/components/sms/const.py @@ -3,3 +3,27 @@ DOMAIN = "sms" SMS_GATEWAY = "SMS_GATEWAY" SMS_STATE_UNREAD = "UnRead" +CONF_BAUD_SPEED = "baud_speed" +DEFAULT_BAUD_SPEED = "0" +DEFAULT_BAUD_SPEEDS = [ + {"value": DEFAULT_BAUD_SPEED, "label": "Auto"}, + {"value": "50", "label": "50"}, + {"value": "75", "label": "75"}, + {"value": "110", "label": "110"}, + {"value": "134", "label": "134"}, + {"value": "150", "label": "150"}, + {"value": "200", "label": "200"}, + {"value": "300", "label": "300"}, + {"value": "600", "label": "600"}, + {"value": "1200", "label": "1200"}, + {"value": "1800", "label": "1800"}, + {"value": "2400", "label": "2400"}, + {"value": "4800", "label": "4800"}, + {"value": "9600", "label": "9600"}, + {"value": "19200", "label": "19200"}, + {"value": "28800", "label": "28800"}, + {"value": "38400", "label": "38400"}, + {"value": "57600", "label": "57600"}, + {"value": "76800", "label": "76800"}, + {"value": "115200", "label": "115200"}, +] diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py index bd8d2f365c9..09992600943 100644 --- a/homeassistant/components/sms/gateway.py +++ b/homeassistant/components/sms/gateway.py @@ -16,6 +16,7 @@ class Gateway: def __init__(self, config, hass): """Initialize the sms gateway.""" + _LOGGER.debug("Init with connection mode:%s", config["Connection"]) self._worker = GammuAsyncWorker(self.sms_pull) self._worker.configure(config) self._hass = hass diff --git a/homeassistant/components/sms/strings.json b/homeassistant/components/sms/strings.json index 872cb17cbea..b4a9279845d 100644 --- a/homeassistant/components/sms/strings.json +++ b/homeassistant/components/sms/strings.json @@ -3,7 +3,10 @@ "step": { "user": { "title": "Connect to the modem", - "data": { "device": "Device" } + "data": { + "device": "Device", + "baud_speed": "Baud Speed" + } } }, "error": { From 20d9f2d3b7dc512948bcf5971127231a61c7a47d Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Mon, 9 May 2022 12:11:09 +0200 Subject: [PATCH 0327/3516] Vicare Gas & Power consumption summary sensors (#66458) --- homeassistant/components/vicare/sensor.py | 141 ++++++++++++++++++++-- 1 file changed, 131 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 249cadaee86..60a39b454a2 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -27,6 +27,7 @@ from homeassistant.const import ( POWER_WATT, TEMP_CELSIUS, TIME_HOURS, + VOLUME_CUBIC_METERS, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -132,6 +133,126 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( unit_getter=lambda api: api.getGasConsumptionHeatingUnit(), state_class=SensorStateClass.TOTAL_INCREASING, ), + ViCareSensorEntityDescription( + key="gas_summary_consumption_heating_currentday", + name="Heating gas consumption current day", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + value_getter=lambda api: api.getGasSummaryConsumptionHeatingCurrentDay(), + unit_getter=lambda api: api.getGasSummaryConsumptionHeatingUnit(), + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="gas_summary_consumption_heating_currentmonth", + name="Heating gas consumption current month", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + value_getter=lambda api: api.getGasSummaryConsumptionHeatingCurrentMonth(), + unit_getter=lambda api: api.getGasSummaryConsumptionHeatingUnit(), + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="gas_summary_consumption_heating_currentyear", + name="Heating gas consumption current year", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + value_getter=lambda api: api.getGasSummaryConsumptionHeatingCurrentYear(), + unit_getter=lambda api: api.getGasSummaryConsumptionHeatingUnit(), + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="hotwater_gas_summary_consumption_heating_currentday", + name="Hot water gas consumption current day", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + value_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterCurrentDay(), + unit_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterUnit(), + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="hotwater_gas_summary_consumption_heating_currentmonth", + name="Hot water gas consumption current month", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + value_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterCurrentMonth(), + unit_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterUnit(), + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="hotwater_gas_summary_consumption_heating_currentyear", + name="Hot water gas consumption current year", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + value_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterCurrentYear(), + unit_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterUnit(), + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="hotwater_gas_summary_consumption_heating_lastsevendays", + name="Hot water gas consumption last seven days", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + value_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterLastSevenDays(), + unit_getter=lambda api: api.getGasSummaryConsumptionDomesticHotWaterUnit(), + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="energy_summary_consumption_heating_currentday", + name="Energy consumption of gas heating current day", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getPowerSummaryConsumptionHeatingCurrentDay(), + unit_getter=lambda api: api.getPowerSummaryConsumptionHeatingUnit(), + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="energy_summary_consumption_heating_currentmonth", + name="Energy consumption of gas heating current month", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getPowerSummaryConsumptionHeatingCurrentMonth(), + unit_getter=lambda api: api.getPowerSummaryConsumptionHeatingUnit(), + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="energy_summary_consumption_heating_currentyear", + name="Energy consumption of gas heating current year", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getPowerSummaryConsumptionHeatingCurrentYear(), + unit_getter=lambda api: api.getPowerSummaryConsumptionHeatingUnit(), + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="energy_summary_consumption_heating_lastsevendays", + name="Energy consumption of gas heating last seven days", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getPowerSummaryConsumptionHeatingLastSevenDays(), + unit_getter=lambda api: api.getPowerSummaryConsumptionHeatingUnit(), + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="energy_dhw_summary_consumption_heating_currentday", + name="Energy consumption of hot water gas heating current day", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterCurrentDay(), + unit_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterUnit(), + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="energy_dhw_summary_consumption_heating_currentmonth", + name="Energy consumption of hot water gas heating current month", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterCurrentMonth(), + unit_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterUnit(), + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="energy_dhw_summary_consumption_heating_currentyear", + name="Energy consumption of hot water gas heating current year", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterCurrentYear(), + unit_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterUnit(), + state_class=SensorStateClass.TOTAL_INCREASING, + ), + ViCareSensorEntityDescription( + key="energy_summary_dhw_consumption_heating_lastsevendays", + name="Energy consumption of hot water gas heating last seven days", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + value_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterLastSevenDays(), + unit_getter=lambda api: api.getPowerSummaryConsumptionDomesticHotWaterUnit(), + state_class=SensorStateClass.TOTAL_INCREASING, + ), ViCareSensorEntityDescription( key="power_production_current", name="Power production current", @@ -142,7 +263,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ), ViCareSensorEntityDescription( key="power_production_today", - name="Power production today", + name="Energy production today", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerProductionToday(), device_class=SensorDeviceClass.ENERGY, @@ -158,7 +279,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ), ViCareSensorEntityDescription( key="power_production_this_month", - name="Power production this month", + name="Energy production this month", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerProductionThisMonth(), device_class=SensorDeviceClass.ENERGY, @@ -166,7 +287,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ), ViCareSensorEntityDescription( key="power_production_this_year", - name="Power production this year", + name="Energy production this year", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerProductionThisYear(), device_class=SensorDeviceClass.ENERGY, @@ -190,7 +311,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ), ViCareSensorEntityDescription( key="solar power production today", - name="Solar power production today", + name="Solar energy production today", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getSolarPowerProductionToday(), unit_getter=lambda api: api.getSolarPowerProductionUnit(), @@ -199,7 +320,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ), ViCareSensorEntityDescription( key="solar power production this week", - name="Solar power production this week", + name="Solar energy production this week", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getSolarPowerProductionThisWeek(), unit_getter=lambda api: api.getSolarPowerProductionUnit(), @@ -208,7 +329,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ), ViCareSensorEntityDescription( key="solar power production this month", - name="Solar power production this month", + name="Solar energy production this month", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getSolarPowerProductionThisMonth(), unit_getter=lambda api: api.getSolarPowerProductionUnit(), @@ -217,7 +338,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ), ViCareSensorEntityDescription( key="solar power production this year", - name="Solar power production this year", + name="Solar energy production this year", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getSolarPowerProductionThisYear(), unit_getter=lambda api: api.getSolarPowerProductionUnit(), @@ -226,7 +347,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ), ViCareSensorEntityDescription( key="power consumption today", - name="Power consumption today", + name="Energy consumption today", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerConsumptionToday(), unit_getter=lambda api: api.getPowerConsumptionUnit(), @@ -244,7 +365,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ), ViCareSensorEntityDescription( key="power consumption this month", - name="Power consumption this month", + name="Energy consumption this month", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerConsumptionThisMonth(), unit_getter=lambda api: api.getPowerConsumptionUnit(), @@ -253,7 +374,7 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( ), ViCareSensorEntityDescription( key="power consumption this year", - name="Power consumption this year", + name="Energy consumption this year", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, value_getter=lambda api: api.getPowerConsumptionThisYear(), unit_getter=lambda api: api.getPowerConsumptionUnit(), From 587a29c72327227e9e0169a916571a1ce6e31016 Mon Sep 17 00:00:00 2001 From: Shai Ungar Date: Mon, 9 May 2022 13:34:16 +0300 Subject: [PATCH 0328/3516] Fix SABnzbd config check (#71549) --- homeassistant/components/sabnzbd/__init__.py | 6 ++---- homeassistant/components/sabnzbd/sensor.py | 14 +++++++++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index 3fa5054a5f0..c03d27fefe5 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -16,7 +16,6 @@ from homeassistant.const import ( CONF_PORT, CONF_SENSORS, CONF_SSL, - CONF_URL, ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError @@ -46,7 +45,7 @@ from .const import ( UPDATE_INTERVAL, ) from .sab import get_client -from .sensor import SENSOR_KEYS +from .sensor import OLD_SENSOR_KEYS PLATFORMS = ["sensor"] _LOGGER = logging.getLogger(__name__) @@ -80,12 +79,11 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_API_KEY): str, vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, - vol.Required(CONF_URL): str, vol.Optional(CONF_PATH): str, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_SENSORS): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] + cv.ensure_list, [vol.In(OLD_SENSOR_KEYS)] ), vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, }, diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index ebdb9190ed9..043a344ec7b 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -103,7 +103,19 @@ SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( ), ) -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] +OLD_SENSOR_KEYS = [ + "current_status", + "speed", + "queue_size", + "queue_remaining", + "disk_size", + "disk_free", + "queue_count", + "day_size", + "week_size", + "month_size", + "total_size", +] async def async_setup_entry( From 2dbe910e312dad4c4d7f808850b642f41877f552 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 9 May 2022 12:37:24 +0200 Subject: [PATCH 0329/3516] Adjust warning for missing entites (#71343) --- homeassistant/helpers/service.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 9d446f10913..4cd38aa9768 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -127,7 +127,10 @@ class SelectedEntities: if not parts: return - _LOGGER.warning("Unable to find referenced %s", ", ".join(parts)) + _LOGGER.warning( + "Unable to find referenced %s or it is/they are currently not available", + ", ".join(parts), + ) @bind_hass From 894d0e35377fb993f2fcf536ff78570163f721dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 May 2022 12:38:46 +0200 Subject: [PATCH 0330/3516] Bump docker/login-action from 1.14.1 to 2.0.0 (#71385) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 2a83ed47de0..48c0782dafa 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -122,13 +122,13 @@ jobs: echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE - name: Login to DockerHub - uses: docker/login-action@v1.14.1 + uses: docker/login-action@v2.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.14.1 + uses: docker/login-action@v2.0.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -187,13 +187,13 @@ jobs: fi - name: Login to DockerHub - uses: docker/login-action@v1.14.1 + uses: docker/login-action@v2.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v1.14.1 + uses: docker/login-action@v2.0.0 with: registry: ghcr.io username: ${{ github.repository_owner }} @@ -259,14 +259,14 @@ jobs: - name: Login to DockerHub if: matrix.registry == 'homeassistant' - uses: docker/login-action@v1.14.1 + uses: docker/login-action@v2.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry if: matrix.registry == 'ghcr.io/home-assistant' - uses: docker/login-action@v1.14.1 + uses: docker/login-action@v2.0.0 with: registry: ghcr.io username: ${{ github.repository_owner }} From 30fdfc454f9cd6a1a0abc356a46105042a462cdd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 May 2022 05:48:38 -0500 Subject: [PATCH 0331/3516] Avoid lowercasing entities after template ratelimit recovery (#71415) --- homeassistant/helpers/event.py | 28 ++++++++++--- tests/helpers/test_event.py | 77 ++++++++++++++++++++++++++++++---- 2 files changed, 93 insertions(+), 12 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 686fde89fbb..c1229dc3e7c 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -68,8 +68,8 @@ class TrackStates: """Class for keeping track of states being tracked. all_states: All states on the system are being tracked - entities: Entities to track - domains: Domains to track + entities: Lowercased entities to track + domains: Lowercased domains to track """ all_states: bool @@ -248,7 +248,16 @@ def async_track_state_change_event( """ if not (entity_ids := _async_string_to_lower_list(entity_ids)): return _remove_empty_listener + return _async_track_state_change_event(hass, entity_ids, action) + +@bind_hass +def _async_track_state_change_event( + hass: HomeAssistant, + entity_ids: str | Iterable[str], + action: Callable[[Event], Any], +) -> CALLBACK_TYPE: + """async_track_state_change_event without lowercasing.""" entity_callbacks = hass.data.setdefault(TRACK_STATE_CHANGE_CALLBACKS, {}) if TRACK_STATE_CHANGE_LISTENER not in hass.data: @@ -419,7 +428,16 @@ def async_track_state_added_domain( """Track state change events when an entity is added to domains.""" if not (domains := _async_string_to_lower_list(domains)): return _remove_empty_listener + return _async_track_state_added_domain(hass, domains, action) + +@bind_hass +def _async_track_state_added_domain( + hass: HomeAssistant, + domains: str | Iterable[str], + action: Callable[[Event], Any], +) -> CALLBACK_TYPE: + """async_track_state_added_domain without lowercasing.""" domain_callbacks = hass.data.setdefault(TRACK_STATE_ADDED_DOMAIN_CALLBACKS, {}) if TRACK_STATE_ADDED_DOMAIN_LISTENER not in hass.data: @@ -626,7 +644,7 @@ class _TrackStateChangeFiltered: if not entities: return - self._listeners[_ENTITIES_LISTENER] = async_track_state_change_event( + self._listeners[_ENTITIES_LISTENER] = _async_track_state_change_event( self.hass, entities, self._action ) @@ -643,7 +661,7 @@ class _TrackStateChangeFiltered: if not domains: return - self._listeners[_DOMAINS_LISTENER] = async_track_state_added_domain( + self._listeners[_DOMAINS_LISTENER] = _async_track_state_added_domain( self.hass, domains, self._state_added ) @@ -1217,7 +1235,7 @@ def async_track_same_state( else: async_remove_state_for_cancel = async_track_state_change_event( hass, - [entity_ids] if isinstance(entity_ids, str) else entity_ids, + entity_ids, state_for_cancel_listener, ) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 7644abaa558..9b0a3e1abd5 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -1983,6 +1983,69 @@ async def test_track_template_result_and_conditional(hass): assert specific_runs[2] == "on" +async def test_track_template_result_and_conditional_upper_case(hass): + """Test tracking template with an and conditional with an upper case template.""" + specific_runs = [] + hass.states.async_set("light.a", "off") + hass.states.async_set("light.b", "off") + template_str = '{% if states.light.A.state == "on" and states.light.B.state == "on" %}on{% else %}off{% endif %}' + + template = Template(template_str, hass) + + def specific_run_callback(event, updates): + specific_runs.append(updates.pop().result) + + info = async_track_template_result( + hass, [TrackTemplate(template, None)], specific_run_callback + ) + await hass.async_block_till_done() + assert info.listeners == { + "all": False, + "domains": set(), + "entities": {"light.a"}, + "time": False, + } + + hass.states.async_set("light.b", "on") + await hass.async_block_till_done() + assert len(specific_runs) == 0 + + hass.states.async_set("light.a", "on") + await hass.async_block_till_done() + assert len(specific_runs) == 1 + assert specific_runs[0] == "on" + assert info.listeners == { + "all": False, + "domains": set(), + "entities": {"light.a", "light.b"}, + "time": False, + } + + hass.states.async_set("light.b", "off") + await hass.async_block_till_done() + assert len(specific_runs) == 2 + assert specific_runs[1] == "off" + assert info.listeners == { + "all": False, + "domains": set(), + "entities": {"light.a", "light.b"}, + "time": False, + } + + hass.states.async_set("light.a", "off") + await hass.async_block_till_done() + assert len(specific_runs) == 2 + + hass.states.async_set("light.b", "on") + await hass.async_block_till_done() + assert len(specific_runs) == 2 + + hass.states.async_set("light.a", "on") + await hass.async_block_till_done() + assert len(specific_runs) == 3 + assert specific_runs[2] == "on" + + async def test_track_template_result_iterator(hass): """Test tracking template.""" iterator_runs = [] @@ -2187,7 +2250,7 @@ async def test_track_template_rate_limit(hass): assert refresh_runs == [0] info.async_refresh() assert refresh_runs == [0, 1] - hass.states.async_set("sensor.two", "any") + hass.states.async_set("sensor.TWO", "any") await hass.async_block_till_done() assert refresh_runs == [0, 1] next_time = dt_util.utcnow() + timedelta(seconds=0.125) @@ -2200,7 +2263,7 @@ async def test_track_template_rate_limit(hass): hass.states.async_set("sensor.three", "any") await hass.async_block_till_done() assert refresh_runs == [0, 1, 2] - hass.states.async_set("sensor.four", "any") + hass.states.async_set("sensor.fOuR", "any") await hass.async_block_till_done() assert refresh_runs == [0, 1, 2] next_time = dt_util.utcnow() + timedelta(seconds=0.125 * 2) @@ -2385,7 +2448,7 @@ async def test_track_template_rate_limit_super_3(hass): await hass.async_block_till_done() assert refresh_runs == [] - hass.states.async_set("sensor.one", "any") + hass.states.async_set("sensor.ONE", "any") await hass.async_block_till_done() assert refresh_runs == [] info.async_refresh() @@ -2408,7 +2471,7 @@ async def test_track_template_rate_limit_super_3(hass): hass.states.async_set("sensor.four", "any") await hass.async_block_till_done() assert refresh_runs == [1, 2] - hass.states.async_set("sensor.five", "any") + hass.states.async_set("sensor.FIVE", "any") await hass.async_block_till_done() assert refresh_runs == [1, 2] next_time = dt_util.utcnow() + timedelta(seconds=0.125 * 2) @@ -2453,7 +2516,7 @@ async def test_track_template_rate_limit_suppress_listener(hass): await hass.async_block_till_done() assert refresh_runs == [0] - hass.states.async_set("sensor.one", "any") + hass.states.async_set("sensor.oNe", "any") await hass.async_block_till_done() assert refresh_runs == [0] info.async_refresh() @@ -2482,7 +2545,7 @@ async def test_track_template_rate_limit_suppress_listener(hass): "time": False, } assert refresh_runs == [0, 1, 2] - hass.states.async_set("sensor.three", "any") + hass.states.async_set("sensor.Three", "any") await hass.async_block_till_done() assert refresh_runs == [0, 1, 2] hass.states.async_set("sensor.four", "any") @@ -2509,7 +2572,7 @@ async def test_track_template_rate_limit_suppress_listener(hass): "time": False, } assert refresh_runs == [0, 1, 2, 4] - hass.states.async_set("sensor.five", "any") + hass.states.async_set("sensor.Five", "any") await hass.async_block_till_done() # Rate limit hit and the all listener is shut off assert info.listeners == { From 08856cfab06173bf8957ac1e6334514f062e2bb0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 9 May 2022 13:16:23 +0200 Subject: [PATCH 0332/3516] Remove 1-Wire SysBus (ADR-0019) (#71232) --- .../components/onewire/binary_sensor.py | 24 +- .../components/onewire/config_flow.py | 117 ++-------- homeassistant/components/onewire/const.py | 13 +- .../components/onewire/manifest.json | 4 +- homeassistant/components/onewire/model.py | 16 +- .../components/onewire/onewire_entities.py | 39 +--- .../components/onewire/onewirehub.py | 113 ++-------- homeassistant/components/onewire/sensor.py | 207 ++++-------------- homeassistant/components/onewire/strings.json | 18 +- homeassistant/components/onewire/switch.py | 29 +-- requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/onewire/__init__.py | 29 --- tests/components/onewire/conftest.py | 38 +--- tests/components/onewire/const.py | 140 ------------ .../components/onewire/test_binary_sensor.py | 4 +- tests/components/onewire/test_config_flow.py | 130 +---------- tests/components/onewire/test_diagnostics.py | 1 - tests/components/onewire/test_init.py | 36 +-- tests/components/onewire/test_options_flow.py | 30 +-- tests/components/onewire/test_sensor.py | 53 +---- tests/components/onewire/test_switch.py | 4 +- 22 files changed, 142 insertions(+), 909 deletions(-) diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index 0235b46baa6..307f38ceea0 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -3,7 +3,6 @@ from __future__ import annotations from dataclasses import dataclass import os -from typing import TYPE_CHECKING from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -11,21 +10,18 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( - CONF_TYPE_OWSERVER, DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, DOMAIN, READ_MODE_BOOL, ) -from .model import OWServerDeviceDescription -from .onewire_entities import OneWireEntityDescription, OneWireProxyEntity +from .onewire_entities import OneWireEntity, OneWireEntityDescription from .onewirehub import OneWireHub @@ -98,23 +94,19 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up 1-Wire platform.""" - # Only OWServer implementation works with binary sensors - if config_entry.data[CONF_TYPE] == CONF_TYPE_OWSERVER: - onewirehub = hass.data[DOMAIN][config_entry.entry_id] + onewirehub = hass.data[DOMAIN][config_entry.entry_id] - entities = await hass.async_add_executor_job(get_entities, onewirehub) - async_add_entities(entities, True) + entities = await hass.async_add_executor_job(get_entities, onewirehub) + async_add_entities(entities, True) -def get_entities(onewirehub: OneWireHub) -> list[BinarySensorEntity]: +def get_entities(onewirehub: OneWireHub) -> list[OneWireBinarySensor]: """Get a list of entities.""" if not onewirehub.devices: return [] - entities: list[BinarySensorEntity] = [] + entities: list[OneWireBinarySensor] = [] for device in onewirehub.devices: - if TYPE_CHECKING: - assert isinstance(device, OWServerDeviceDescription) family = device.family device_id = device.id device_type = device.type @@ -130,7 +122,7 @@ def get_entities(onewirehub: OneWireHub) -> list[BinarySensorEntity]: device_file = os.path.join(os.path.split(device.path)[0], description.key) name = f"{device_id} {description.name}" entities.append( - OneWireProxyBinarySensor( + OneWireBinarySensor( description=description, device_id=device_id, device_file=device_file, @@ -143,7 +135,7 @@ def get_entities(onewirehub: OneWireHub) -> list[BinarySensorEntity]: return entities -class OneWireProxyBinarySensor(OneWireProxyEntity, BinarySensorEntity): +class OneWireBinarySensor(OneWireEntity, BinarySensorEntity): """Implementation of a 1-Wire binary sensor.""" entity_description: OneWireBinarySensorEntityDescription diff --git a/homeassistant/components/onewire/config_flow.py b/homeassistant/components/onewire/config_flow.py index 25604473bcf..5e944c51e0a 100644 --- a/homeassistant/components/onewire/config_flow.py +++ b/homeassistant/components/onewire/config_flow.py @@ -6,19 +6,15 @@ from typing import Any import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE +from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.device_registry import DeviceRegistry from .const import ( - CONF_MOUNT_DIR, - CONF_TYPE_OWSERVER, - CONF_TYPE_SYSBUS, - DEFAULT_OWSERVER_HOST, - DEFAULT_OWSERVER_PORT, - DEFAULT_SYSBUS_MOUNT_DIR, + DEFAULT_HOST, + DEFAULT_PORT, DEVICE_SUPPORT_OPTIONS, DOMAIN, INPUT_ENTRY_CLEAR_OPTIONS, @@ -27,31 +23,21 @@ from .const import ( OPTION_ENTRY_SENSOR_PRECISION, PRECISION_MAPPING_FAMILY_28, ) -from .model import OWServerDeviceDescription -from .onewirehub import CannotConnect, InvalidPath, OneWireHub +from .model import OWDeviceDescription +from .onewirehub import CannotConnect, OneWireHub -DATA_SCHEMA_USER = vol.Schema( - {vol.Required(CONF_TYPE): vol.In([CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS])} -) -DATA_SCHEMA_OWSERVER = vol.Schema( +DATA_SCHEMA = vol.Schema( { - vol.Required(CONF_HOST, default=DEFAULT_OWSERVER_HOST): str, - vol.Required(CONF_PORT, default=DEFAULT_OWSERVER_PORT): int, - } -) -DATA_SCHEMA_MOUNTDIR = vol.Schema( - { - vol.Required(CONF_MOUNT_DIR, default=DEFAULT_SYSBUS_MOUNT_DIR): str, + vol.Required(CONF_HOST, default=DEFAULT_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, } ) -async def validate_input_owserver( - hass: HomeAssistant, data: dict[str, Any] -) -> dict[str, str]: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: """Validate the user input allows us to connect. - Data has the keys from DATA_SCHEMA_OWSERVER with values provided by the user. + Data has the keys from DATA_SCHEMA with values provided by the user. """ hub = OneWireHub(hass) @@ -65,24 +51,6 @@ async def validate_input_owserver( return {"title": host} -async def validate_input_mount_dir( - hass: HomeAssistant, data: dict[str, Any] -) -> dict[str, str]: - """Validate the user input allows us to connect. - - Data has the keys from DATA_SCHEMA_MOUNTDIR with values provided by the user. - """ - hub = OneWireHub(hass) - - mount_dir = data[CONF_MOUNT_DIR] - - # Raises InvalidDir exception on failure - await hub.check_mount_dir(mount_dir) - - # Return info that you want to store in the config entry. - return {"title": mount_dir} - - class OneWireFlowHandler(ConfigFlow, domain=DOMAIN): """Handle 1-Wire config flow.""" @@ -100,29 +68,10 @@ class OneWireFlowHandler(ConfigFlow, domain=DOMAIN): Let user manually input configuration. """ errors: dict[str, str] = {} - if user_input is not None: - self.onewire_config.update(user_input) - if CONF_TYPE_OWSERVER == user_input[CONF_TYPE]: - return await self.async_step_owserver() - if CONF_TYPE_SYSBUS == user_input[CONF_TYPE]: - return await self.async_step_mount_dir() - - return self.async_show_form( - step_id="user", - data_schema=DATA_SCHEMA_USER, - errors=errors, - ) - - async def async_step_owserver( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle OWServer configuration.""" - errors = {} if user_input: # Prevent duplicate entries self._async_abort_entries_match( { - CONF_TYPE: CONF_TYPE_OWSERVER, CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT], } @@ -131,7 +80,7 @@ class OneWireFlowHandler(ConfigFlow, domain=DOMAIN): self.onewire_config.update(user_input) try: - info = await validate_input_owserver(self.hass, user_input) + info = await validate_input(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" else: @@ -140,37 +89,8 @@ class OneWireFlowHandler(ConfigFlow, domain=DOMAIN): ) return self.async_show_form( - step_id="owserver", - data_schema=DATA_SCHEMA_OWSERVER, - errors=errors, - ) - - async def async_step_mount_dir( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle SysBus configuration.""" - errors = {} - if user_input: - # Prevent duplicate entries - await self.async_set_unique_id( - f"{CONF_TYPE_SYSBUS}:{user_input[CONF_MOUNT_DIR]}" - ) - self._abort_if_unique_id_configured() - - self.onewire_config.update(user_input) - - try: - info = await validate_input_mount_dir(self.hass, user_input) - except InvalidPath: - errors["base"] = "invalid_path" - else: - return self.async_create_entry( - title=info["title"], data=self.onewire_config - ) - - return self.async_show_form( - step_id="mount_dir", - data_schema=DATA_SCHEMA_MOUNTDIR, + step_id="user", + data_schema=DATA_SCHEMA, errors=errors, ) @@ -188,8 +108,8 @@ class OnewireOptionsFlowHandler(OptionsFlow): """Initialize OneWire Network options flow.""" self.entry_id = config_entry.entry_id self.options = dict(config_entry.options) - self.configurable_devices: dict[str, OWServerDeviceDescription] = {} - self.devices_to_configure: dict[str, OWServerDeviceDescription] = {} + self.configurable_devices: dict[str, OWDeviceDescription] = {} + self.devices_to_configure: dict[str, OWDeviceDescription] = {} self.current_device: str = "" async def async_step_init( @@ -197,12 +117,7 @@ class OnewireOptionsFlowHandler(OptionsFlow): ) -> FlowResult: """Manage the options.""" controller: OneWireHub = self.hass.data[DOMAIN][self.entry_id] - if controller.type == CONF_TYPE_SYSBUS: - return self.async_abort( - reason="SysBus setup does not have any config options." - ) - - all_devices: list[OWServerDeviceDescription] = controller.devices # type: ignore[assignment] + all_devices: list[OWDeviceDescription] = controller.devices # type: ignore[assignment] if not all_devices: return self.async_abort(reason="No configurable devices found.") diff --git a/homeassistant/components/onewire/const.py b/homeassistant/components/onewire/const.py index 7fce90cc012..ed744caee17 100644 --- a/homeassistant/components/onewire/const.py +++ b/homeassistant/components/onewire/const.py @@ -3,14 +3,8 @@ from __future__ import annotations from homeassistant.const import Platform -CONF_MOUNT_DIR = "mount_dir" - -CONF_TYPE_OWSERVER = "OWServer" -CONF_TYPE_SYSBUS = "SysBus" - -DEFAULT_OWSERVER_HOST = "localhost" -DEFAULT_OWSERVER_PORT = 4304 -DEFAULT_SYSBUS_MOUNT_DIR = "/sys/bus/w1/devices/" +DEFAULT_HOST = "localhost" +DEFAULT_PORT = 4304 DOMAIN = "onewire" @@ -18,7 +12,7 @@ DEVICE_KEYS_0_3 = range(4) DEVICE_KEYS_0_7 = range(8) DEVICE_KEYS_A_B = ("A", "B") -DEVICE_SUPPORT_OWSERVER = { +DEVICE_SUPPORT = { "05": (), "10": (), "12": (), @@ -35,7 +29,6 @@ DEVICE_SUPPORT_OWSERVER = { "7E": ("EDS0066", "EDS0068"), "EF": ("HB_HUB", "HB_MOISTURE_METER", "HobbyBoards_EF"), } -DEVICE_SUPPORT_SYSBUS = ["10", "22", "28", "3B", "42"] DEVICE_SUPPORT_OPTIONS = ["28"] diff --git a/homeassistant/components/onewire/manifest.json b/homeassistant/components/onewire/manifest.json index d7b301f9c23..87adc23ae18 100644 --- a/homeassistant/components/onewire/manifest.json +++ b/homeassistant/components/onewire/manifest.json @@ -3,8 +3,8 @@ "name": "1-Wire", "documentation": "https://www.home-assistant.io/integrations/onewire", "config_flow": true, - "requirements": ["pyownet==0.10.0.post1", "pi1wire==0.1.0"], + "requirements": ["pyownet==0.10.0.post1"], "codeowners": ["@garbled1", "@epenet"], "iot_class": "local_polling", - "loggers": ["pi1wire", "pyownet"] + "loggers": ["pyownet"] } diff --git a/homeassistant/components/onewire/model.py b/homeassistant/components/onewire/model.py index 370b26c2530..d3fb2f22f14 100644 --- a/homeassistant/components/onewire/model.py +++ b/homeassistant/components/onewire/model.py @@ -3,29 +3,15 @@ from __future__ import annotations from dataclasses import dataclass -from pi1wire import OneWireInterface - from homeassistant.helpers.entity import DeviceInfo @dataclass class OWDeviceDescription: - """OWDeviceDescription device description class.""" + """1-Wire device description class.""" device_info: DeviceInfo - -@dataclass -class OWDirectDeviceDescription(OWDeviceDescription): - """SysBus device description class.""" - - interface: OneWireInterface - - -@dataclass -class OWServerDeviceDescription(OWDeviceDescription): - """OWServer device description class.""" - family: str id: str path: str diff --git a/homeassistant/components/onewire/onewire_entities.py b/homeassistant/components/onewire/onewire_entities.py index e00733ae387..98287c01ce1 100644 --- a/homeassistant/components/onewire/onewire_entities.py +++ b/homeassistant/components/onewire/onewire_entities.py @@ -23,7 +23,7 @@ class OneWireEntityDescription(EntityDescription): _LOGGER = logging.getLogger(__name__) -class OneWireBaseEntity(Entity): +class OneWireEntity(Entity): """Implementation of a 1-Wire entity.""" entity_description: OneWireEntityDescription @@ -35,6 +35,7 @@ class OneWireBaseEntity(Entity): device_info: DeviceInfo, device_file: str, name: str, + owproxy: protocol._Proxy, ) -> None: """Initialize the entity.""" self.entity_description = description @@ -44,6 +45,7 @@ class OneWireBaseEntity(Entity): self._device_file = device_file self._state: StateType = None self._value_raw: float | None = None + self._owproxy = owproxy @property def extra_state_attributes(self) -> dict[str, Any] | None: @@ -53,44 +55,21 @@ class OneWireBaseEntity(Entity): "raw_value": self._value_raw, } - -class OneWireProxyEntity(OneWireBaseEntity): - """Implementation of a 1-Wire entity connected through owserver.""" - - def __init__( - self, - description: OneWireEntityDescription, - device_id: str, - device_info: DeviceInfo, - device_file: str, - name: str, - owproxy: protocol._Proxy, - ) -> None: - """Initialize the sensor.""" - super().__init__( - description=description, - device_id=device_id, - device_info=device_info, - device_file=device_file, - name=name, - ) - self._owproxy = owproxy - - def _read_value_ownet(self) -> str: - """Read a value from the owserver.""" + def _read_value(self) -> str: + """Read a value from the server.""" read_bytes: bytes = self._owproxy.read(self._device_file) return read_bytes.decode().lstrip() - def _write_value_ownet(self, value: bytes) -> None: - """Write a value to the owserver.""" + def _write_value(self, value: bytes) -> None: + """Write a value to the server.""" self._owproxy.write(self._device_file, value) def update(self) -> None: """Get the latest data from the device.""" try: - self._value_raw = float(self._read_value_ownet()) + self._value_raw = float(self._read_value()) except protocol.Error as exc: - _LOGGER.error("Owserver failure in read(), got: %s", exc) + _LOGGER.error("Failure to read server value, got: %s", exc) self._state = None else: if self.entity_description.read_mode == READ_MODE_INT: diff --git a/homeassistant/components/onewire/onewirehub.py b/homeassistant/components/onewire/onewirehub.py index 63ed3e8cef9..a412f87deaa 100644 --- a/homeassistant/components/onewire/onewirehub.py +++ b/homeassistant/components/onewire/onewirehub.py @@ -5,7 +5,6 @@ import logging import os from typing import TYPE_CHECKING -from pi1wire import Pi1Wire from pyownet import protocol from homeassistant.config_entries import ConfigEntry @@ -17,7 +16,6 @@ from homeassistant.const import ( ATTR_VIA_DEVICE, CONF_HOST, CONF_PORT, - CONF_TYPE, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -25,21 +23,13 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo from .const import ( - CONF_MOUNT_DIR, - CONF_TYPE_OWSERVER, - CONF_TYPE_SYSBUS, - DEVICE_SUPPORT_OWSERVER, - DEVICE_SUPPORT_SYSBUS, + DEVICE_SUPPORT, DOMAIN, MANUFACTURER_EDS, MANUFACTURER_HOBBYBOARDS, MANUFACTURER_MAXIM, ) -from .model import ( - OWDeviceDescription, - OWDirectDeviceDescription, - OWServerDeviceDescription, -) +from .model import OWDeviceDescription DEVICE_COUPLERS = { # Family : [branches] @@ -54,26 +44,24 @@ DEVICE_MANUFACTURER = { _LOGGER = logging.getLogger(__name__) -def _is_known_owserver_device(device_family: str, device_type: str) -> bool: +def _is_known_device(device_family: str, device_type: str) -> bool: """Check if device family/type is known to the library.""" if device_family in ("7E", "EF"): # EDS or HobbyBoard - return device_type in DEVICE_SUPPORT_OWSERVER[device_family] - return device_family in DEVICE_SUPPORT_OWSERVER + return device_type in DEVICE_SUPPORT[device_family] + return device_family in DEVICE_SUPPORT class OneWireHub: - """Hub to communicate with SysBus or OWServer.""" + """Hub to communicate with server.""" def __init__(self, hass: HomeAssistant) -> None: """Initialize.""" self.hass = hass - self.type: str | None = None - self.pi1proxy: Pi1Wire | None = None self.owproxy: protocol._Proxy | None = None self.devices: list[OWDeviceDescription] | None = None async def connect(self, host: str, port: int) -> None: - """Connect to the owserver host.""" + """Connect to the server.""" try: self.owproxy = await self.hass.async_add_executor_job( protocol.proxy, host, port @@ -81,32 +69,12 @@ class OneWireHub: except protocol.ConnError as exc: raise CannotConnect from exc - async def check_mount_dir(self, mount_dir: str) -> None: - """Test that the mount_dir is a valid path.""" - if not await self.hass.async_add_executor_job(os.path.isdir, mount_dir): - raise InvalidPath - self.pi1proxy = Pi1Wire(mount_dir) - async def initialize(self, config_entry: ConfigEntry) -> None: """Initialize a config entry.""" - self.type = config_entry.data[CONF_TYPE] - if self.type == CONF_TYPE_SYSBUS: - mount_dir = config_entry.data[CONF_MOUNT_DIR] - _LOGGER.debug("Initializing using SysBus %s", mount_dir) - _LOGGER.warning( - "Using the 1-Wire integration via SysBus is deprecated and will be removed " - "in Home Assistant Core 2022.6; this integration is being adjusted to comply " - "with Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md " - "Access via OWServer is still supported" - ) - - await self.check_mount_dir(mount_dir) - elif self.type == CONF_TYPE_OWSERVER: - host = config_entry.data[CONF_HOST] - port = config_entry.data[CONF_PORT] - _LOGGER.debug("Initializing using OWServer %s:%s", host, port) - await self.connect(host, port) + host = config_entry.data[CONF_HOST] + port = config_entry.data[CONF_PORT] + _LOGGER.debug("Initializing connection to %s:%s", host, port) + await self.connect(host, port) await self.discover_devices() if TYPE_CHECKING: assert self.devices @@ -126,63 +94,22 @@ class OneWireHub: async def discover_devices(self) -> None: """Discover all devices.""" if self.devices is None: - if self.type == CONF_TYPE_SYSBUS: - self.devices = await self.hass.async_add_executor_job( - 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 - all_sensors = self.pi1proxy.find_all_sensors() - if not all_sensors: - _LOGGER.error( - "No onewire sensor found. Check if dtoverlay=w1-gpio " - "is in your /boot/config.txt. " - "Check the mount_dir parameter if it's defined" + self.devices = await self.hass.async_add_executor_job( + self._discover_devices ) - for interface in all_sensors: - device_family = interface.mac_address[:2] - device_id = f"{device_family}-{interface.mac_address[2:]}" - if device_family not in DEVICE_SUPPORT_SYSBUS: - _LOGGER.warning( - "Ignoring unknown device family (%s) found for device %s", - device_family, - device_id, - ) - continue - device_info: DeviceInfo = { - ATTR_IDENTIFIERS: {(DOMAIN, device_id)}, - ATTR_MANUFACTURER: DEVICE_MANUFACTURER.get( - device_family, MANUFACTURER_MAXIM - ), - ATTR_MODEL: device_family, - ATTR_NAME: device_id, - } - device = OWDirectDeviceDescription( - device_info=device_info, - interface=interface, - ) - devices.append(device) - return devices - def _discover_devices_owserver( + def _discover_devices( self, path: str = "/", parent_id: str | None = None ) -> list[OWDeviceDescription]: - """Discover all owserver devices.""" + """Discover all server 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._get_device_type_owserver(device_path) - if not _is_known_owserver_device(device_family, device_type): + device_type = self._get_device_type(device_path) + if not _is_known_device(device_family, device_type): _LOGGER.warning( "Ignoring unknown device family/type (%s/%s) found for device %s", device_family, @@ -200,7 +127,7 @@ class OneWireHub: } if parent_id: device_info[ATTR_VIA_DEVICE] = (DOMAIN, parent_id) - device = OWServerDeviceDescription( + device = OWDeviceDescription( device_info=device_info, id=device_id, family=device_family, @@ -210,13 +137,13 @@ class OneWireHub: devices.append(device) if device_branches := DEVICE_COUPLERS.get(device_family): for branch in device_branches: - devices += self._discover_devices_owserver( + devices += self._discover_devices( f"{device_path}{branch}", device_id ) return devices - def _get_device_type_owserver(self, device_path: str) -> str: + def _get_device_type(self, device_path: str) -> str: """Get device model.""" if TYPE_CHECKING: assert self.owproxy diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 759b1f9eccf..9f376a6df7a 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -1,16 +1,13 @@ """Support for 1-Wire environment sensors.""" from __future__ import annotations -import asyncio from collections.abc import Callable, Mapping import copy from dataclasses import dataclass import logging import os from types import MappingProxyType -from typing import TYPE_CHECKING, Any - -from pi1wire import InvalidCRCException, OneWireInterface, UnsupportResponseException +from typing import Any from homeassistant.components.sensor import ( SensorDeviceClass, @@ -20,7 +17,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_TYPE, ELECTRIC_POTENTIAL_VOLT, LIGHT_LUX, PERCENTAGE, @@ -29,13 +25,10 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from .const import ( - CONF_TYPE_OWSERVER, - CONF_TYPE_SYSBUS, DEVICE_KEYS_0_3, DEVICE_KEYS_A_B, DOMAIN, @@ -45,12 +38,7 @@ from .const import ( READ_MODE_FLOAT, READ_MODE_INT, ) -from .model import OWDirectDeviceDescription, OWServerDeviceDescription -from .onewire_entities import ( - OneWireBaseEntity, - OneWireEntityDescription, - OneWireProxyEntity, -) +from .onewire_entities import OneWireEntity, OneWireEntityDescription from .onewirehub import OneWireHub @@ -263,8 +251,6 @@ DEVICE_SENSORS: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { } # EF sensors are usually hobbyboards specialized sensors. -# These can only be read by OWFS. Currently this driver only supports them -# via owserver (network protocol) HOBBYBOARD_EF: dict[str, tuple[OneWireSensorEntityDescription, ...]] = { "HobbyBoards_EF": ( @@ -383,109 +369,72 @@ async def async_setup_entry( """Set up 1-Wire platform.""" onewirehub = hass.data[DOMAIN][config_entry.entry_id] entities = await hass.async_add_executor_job( - get_entities, onewirehub, config_entry.data, config_entry.options + get_entities, onewirehub, config_entry.options ) async_add_entities(entities, True) def get_entities( - onewirehub: OneWireHub, - config: MappingProxyType[str, Any], - options: MappingProxyType[str, Any], -) -> list[SensorEntity]: + onewirehub: OneWireHub, options: MappingProxyType[str, Any] +) -> list[OneWireSensor]: """Get a list of entities.""" if not onewirehub.devices: return [] - entities: list[SensorEntity] = [] - conf_type = config[CONF_TYPE] - # We have an owserver on a remote(or local) host/port - if conf_type == CONF_TYPE_OWSERVER: - assert onewirehub.owproxy - for device in onewirehub.devices: - 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 - if "EF" in family: - device_sub_type = "HobbyBoard" - family = device_type - elif "7E" in family: - device_sub_type = "EDS" - family = device_type + entities: list[OneWireSensor] = [] + assert onewirehub.owproxy + for device in onewirehub.devices: + family = device.family + device_type = device.type + device_id = device.id + device_info = device.device_info + device_sub_type = "std" + device_path = device.path + if "EF" in family: + device_sub_type = "HobbyBoard" + family = device_type + elif "7E" in family: + device_sub_type = "EDS" + family = device_type - if family not in get_sensor_types(device_sub_type): - continue - for description in get_sensor_types(device_sub_type)[family]: - if description.key.startswith("moisture/"): - s_id = description.key.split(".")[1] - is_leaf = int( - onewirehub.owproxy.read( - f"{device_path}moisture/is_leaf.{s_id}" - ).decode() - ) - if is_leaf: - description = copy.deepcopy(description) - description.device_class = SensorDeviceClass.HUMIDITY - description.native_unit_of_measurement = PERCENTAGE - description.name = f"Wetness {s_id}" - override_key = None - if description.override_key: - override_key = description.override_key(device_id, options) - device_file = os.path.join( - os.path.split(device.path)[0], - override_key or description.key, + if family not in get_sensor_types(device_sub_type): + continue + for description in get_sensor_types(device_sub_type)[family]: + if description.key.startswith("moisture/"): + s_id = description.key.split(".")[1] + is_leaf = int( + onewirehub.owproxy.read( + f"{device_path}moisture/is_leaf.{s_id}" + ).decode() ) - name = f"{device_id} {description.name}" - entities.append( - OneWireProxySensor( - description=description, - device_id=device_id, - device_file=device_file, - device_info=device_info, - name=name, - owproxy=onewirehub.owproxy, - ) - ) - - # We have a raw GPIO ow sensor on a Pi - elif conf_type == CONF_TYPE_SYSBUS: - 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 - description = SIMPLE_TEMPERATURE_SENSOR_DESCRIPTION - device_file = f"/sys/bus/w1/devices/{device_id}/w1_slave" + if is_leaf: + description = copy.deepcopy(description) + description.device_class = SensorDeviceClass.HUMIDITY + description.native_unit_of_measurement = PERCENTAGE + description.name = f"Wetness {s_id}" + override_key = None + if description.override_key: + override_key = description.override_key(device_id, options) + device_file = os.path.join( + os.path.split(device.path)[0], + override_key or description.key, + ) name = f"{device_id} {description.name}" entities.append( - OneWireDirectSensor( + OneWireSensor( description=description, device_id=device_id, device_file=device_file, device_info=device_info, name=name, - owsensor=p1sensor, + owproxy=onewirehub.owproxy, ) ) - return entities -class OneWireSensor(OneWireBaseEntity, SensorEntity): - """Mixin for sensor specific attributes.""" - - entity_description: OneWireSensorEntityDescription - - -class OneWireProxySensor(OneWireProxyEntity, OneWireSensor): - """Implementation of a 1-Wire sensor connected through owserver.""" +class OneWireSensor(OneWireEntity, SensorEntity): + """Implementation of a 1-Wire sensor.""" entity_description: OneWireSensorEntityDescription @@ -493,69 +442,3 @@ class OneWireProxySensor(OneWireProxyEntity, OneWireSensor): def native_value(self) -> StateType: """Return the state of the entity.""" return self._state - - -class OneWireDirectSensor(OneWireSensor): - """Implementation of a 1-Wire sensor directly connected to RPI GPIO.""" - - def __init__( - self, - description: OneWireSensorEntityDescription, - device_id: str, - device_info: DeviceInfo, - device_file: str, - name: str, - owsensor: OneWireInterface, - ) -> None: - """Initialize the sensor.""" - super().__init__( - description=description, - device_id=device_id, - device_info=device_info, - device_file=device_file, - name=name, - ) - self._attr_unique_id = device_file - self._owsensor = owsensor - - @property - def native_value(self) -> StateType: - """Return the state of the entity.""" - return self._state - - async def get_temperature(self) -> float: - """Get the latest data from the device.""" - attempts = 1 - while True: - try: - return await self.hass.async_add_executor_job( - self._owsensor.get_temperature - ) - except UnsupportResponseException as ex: - _LOGGER.debug( - "Cannot read from sensor %s (retry attempt %s): %s", - self._device_file, - attempts, - ex, - ) - await asyncio.sleep(0.2) - attempts += 1 - if attempts > 10: - raise - - async def async_update(self) -> None: - """Get the latest data from the device.""" - try: - self._value_raw = await self.get_temperature() - self._state = round(self._value_raw, 1) - except ( - FileNotFoundError, - InvalidCRCException, - UnsupportResponseException, - ) as ex: - _LOGGER.warning( - "Cannot read from sensor %s: %s", - self._device_file, - ex, - ) - self._state = None diff --git a/homeassistant/components/onewire/strings.json b/homeassistant/components/onewire/strings.json index b685479d359..734971cb2a1 100644 --- a/homeassistant/components/onewire/strings.json +++ b/homeassistant/components/onewire/strings.json @@ -4,22 +4,15 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_path": "Directory not found." + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "step": { - "owserver": { + "user": { "data": { "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]" }, - "title": "Set owserver details" - }, - "user": { - "data": { - "type": "Connection type" - }, - "title": "Set up 1-Wire" + "title": "Set server details" } } }, @@ -28,11 +21,6 @@ "device_not_selected": "Select devices to configure" }, "step": { - "ack_no_options": { - "data": {}, - "description": "There are no options for the SysBus implementation", - "title": "OneWire SysBus Options" - }, "device_selection": { "data": { "clear_device_options": "Clear all device configurations", diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index a05757936f9..764cc403681 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -3,25 +3,22 @@ from __future__ import annotations from dataclasses import dataclass import os -from typing import TYPE_CHECKING, Any +from typing import Any from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( - CONF_TYPE_OWSERVER, DEVICE_KEYS_0_3, DEVICE_KEYS_0_7, DEVICE_KEYS_A_B, DOMAIN, READ_MODE_BOOL, ) -from .model import OWServerDeviceDescription -from .onewire_entities import OneWireEntityDescription, OneWireProxyEntity +from .onewire_entities import OneWireEntity, OneWireEntityDescription from .onewirehub import OneWireHub @@ -153,24 +150,20 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up 1-Wire platform.""" - # Only OWServer implementation works with switches - if config_entry.data[CONF_TYPE] == CONF_TYPE_OWSERVER: - onewirehub = hass.data[DOMAIN][config_entry.entry_id] + onewirehub = hass.data[DOMAIN][config_entry.entry_id] - entities = await hass.async_add_executor_job(get_entities, onewirehub) - async_add_entities(entities, True) + entities = await hass.async_add_executor_job(get_entities, onewirehub) + async_add_entities(entities, True) -def get_entities(onewirehub: OneWireHub) -> list[SwitchEntity]: +def get_entities(onewirehub: OneWireHub) -> list[OneWireSwitch]: """Get a list of entities.""" if not onewirehub.devices: return [] - entities: list[SwitchEntity] = [] + entities: list[OneWireSwitch] = [] for device in onewirehub.devices: - if TYPE_CHECKING: - assert isinstance(device, OWServerDeviceDescription) family = device.family device_type = device.type device_id = device.id @@ -186,7 +179,7 @@ def get_entities(onewirehub: OneWireHub) -> list[SwitchEntity]: device_file = os.path.join(os.path.split(device.path)[0], description.key) name = f"{device_id} {description.name}" entities.append( - OneWireProxySwitch( + OneWireSwitch( description=description, device_id=device_id, device_file=device_file, @@ -199,7 +192,7 @@ def get_entities(onewirehub: OneWireHub) -> list[SwitchEntity]: return entities -class OneWireProxySwitch(OneWireProxyEntity, SwitchEntity): +class OneWireSwitch(OneWireEntity, SwitchEntity): """Implementation of a 1-Wire switch.""" entity_description: OneWireSwitchEntityDescription @@ -211,8 +204,8 @@ class OneWireProxySwitch(OneWireProxyEntity, SwitchEntity): def turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - self._write_value_ownet(b"1") + self._write_value(b"1") def turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - self._write_value_ownet(b"0") + self._write_value(b"0") diff --git a/requirements_all.txt b/requirements_all.txt index 07772173bd8..644b5c2b74b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1210,9 +1210,6 @@ pexpect==4.6.0 # homeassistant.components.modem_callerid phone_modem==0.1.1 -# homeassistant.components.onewire -pi1wire==0.1.0 - # homeassistant.components.remote_rpi_gpio pigpio==1.78 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9cfa8a6945b..a7859ff3e1e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -812,9 +812,6 @@ pexpect==4.6.0 # homeassistant.components.modem_callerid phone_modem==0.1.1 -# homeassistant.components.onewire -pi1wire==0.1.0 - # homeassistant.components.pilight pilight==0.1.1 diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index 0ab8b8f6917..d189db8af1a 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -7,7 +7,6 @@ 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_IDENTIFIERS, @@ -29,7 +28,6 @@ from .const import ( ATTR_UNIQUE_ID, FIXED_ATTRIBUTES, MOCK_OWPROXY_DEVICES, - MOCK_SYSBUS_DEVICES, ) @@ -181,30 +179,3 @@ def _setup_owproxy_mock_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]]: - """Set up mock for sysbus.""" - glob_result = [] - read_side_effect = [] - - for device_id in device_ids: - mock_device = MOCK_SYSBUS_DEVICES[device_id] - - # Setup directory listing - glob_result += [f"/{DEFAULT_SYSBUS_MOUNT_DIR}/{device_id}"] - - # Setup sub-device reads - device_sensors = mock_device.get(platform, []) - for expected_sensor in device_sensors: - if isinstance(expected_sensor[ATTR_INJECT_READS], list): - read_side_effect += expected_sensor[ATTR_INJECT_READS] - else: - read_side_effect.append(expected_sensor[ATTR_INJECT_READS]) - - # Ensure enough read side effect - read_side_effect.extend([FileNotFoundError("Missing injected value")] * 20) - - return (glob_result, read_side_effect) diff --git a/tests/components/onewire/conftest.py b/tests/components/onewire/conftest.py index ab456c7d7df..9b8b53859ea 100644 --- a/tests/components/onewire/conftest.py +++ b/tests/components/onewire/conftest.py @@ -4,15 +4,9 @@ from unittest.mock import MagicMock, patch from pyownet.protocol import ConnError import pytest -from homeassistant.components.onewire.const import ( - CONF_MOUNT_DIR, - CONF_TYPE_OWSERVER, - CONF_TYPE_SYSBUS, - DEFAULT_SYSBUS_MOUNT_DIR, - DOMAIN, -) +from homeassistant.components.onewire.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE +from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from .const import MOCK_OWPROXY_DEVICES @@ -33,7 +27,6 @@ def get_config_entry(hass: HomeAssistant) -> ConfigEntry: domain=DOMAIN, source=SOURCE_USER, data={ - CONF_TYPE: CONF_TYPE_OWSERVER, CONF_HOST: "1.2.3.4", CONF_PORT: 1234, }, @@ -49,24 +42,6 @@ def get_config_entry(hass: HomeAssistant) -> ConfigEntry: 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, - }, - 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.""" @@ -82,12 +57,3 @@ def get_owproxy_with_connerror() -> MagicMock: 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/const.py b/tests/components/onewire/const.py index d77b374c7c4..c28ec017a9b 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 from homeassistant.components.binary_sensor import BinarySensorDeviceClass @@ -1131,142 +1130,3 @@ MOCK_OWPROXY_DEVICES = { ], }, } - -MOCK_SYSBUS_DEVICES = { - "00-111111111111": { - ATTR_UNKNOWN_DEVICE: True, - }, - "10-111111111111": { - ATTR_DEVICE_INFO: { - ATTR_IDENTIFIERS: {(DOMAIN, "10-111111111111")}, - ATTR_MANUFACTURER: MANUFACTURER_MAXIM, - ATTR_MODEL: "10", - ATTR_NAME: "10-111111111111", - }, - Platform.SENSOR: [ - { - ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, - ATTR_ENTITY_ID: "sensor.10_111111111111_temperature", - ATTR_INJECT_READS: 25.123, - ATTR_STATE: "25.1", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - ATTR_UNIQUE_ID: "/sys/bus/w1/devices/10-111111111111/w1_slave", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, - }, - ], - }, - "22-111111111111": { - ATTR_DEVICE_INFO: { - ATTR_IDENTIFIERS: {(DOMAIN, "22-111111111111")}, - ATTR_MANUFACTURER: MANUFACTURER_MAXIM, - ATTR_MODEL: "22", - ATTR_NAME: "22-111111111111", - }, - Platform.SENSOR: [ - { - ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, - ATTR_ENTITY_ID: "sensor.22_111111111111_temperature", - ATTR_INJECT_READS: FileNotFoundError, - ATTR_STATE: STATE_UNKNOWN, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - ATTR_UNIQUE_ID: "/sys/bus/w1/devices/22-111111111111/w1_slave", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, - }, - ], - }, - "28-111111111111": { - ATTR_DEVICE_INFO: { - ATTR_IDENTIFIERS: {(DOMAIN, "28-111111111111")}, - ATTR_MANUFACTURER: MANUFACTURER_MAXIM, - ATTR_MODEL: "28", - ATTR_NAME: "28-111111111111", - }, - Platform.SENSOR: [ - { - ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, - ATTR_ENTITY_ID: "sensor.28_111111111111_temperature", - ATTR_INJECT_READS: InvalidCRCException, - ATTR_STATE: STATE_UNKNOWN, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - ATTR_UNIQUE_ID: "/sys/bus/w1/devices/28-111111111111/w1_slave", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, - }, - ], - }, - "3B-111111111111": { - ATTR_DEVICE_INFO: { - ATTR_IDENTIFIERS: {(DOMAIN, "3B-111111111111")}, - ATTR_MANUFACTURER: MANUFACTURER_MAXIM, - ATTR_MODEL: "3B", - ATTR_NAME: "3B-111111111111", - }, - Platform.SENSOR: [ - { - ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, - ATTR_ENTITY_ID: "sensor.3b_111111111111_temperature", - ATTR_INJECT_READS: 29.993, - ATTR_STATE: "30.0", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - ATTR_UNIQUE_ID: "/sys/bus/w1/devices/3B-111111111111/w1_slave", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, - }, - ], - }, - "42-111111111111": { - ATTR_DEVICE_INFO: { - ATTR_IDENTIFIERS: {(DOMAIN, "42-111111111111")}, - ATTR_MANUFACTURER: MANUFACTURER_MAXIM, - ATTR_MODEL: "42", - ATTR_NAME: "42-111111111111", - }, - Platform.SENSOR: [ - { - ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, - ATTR_ENTITY_ID: "sensor.42_111111111111_temperature", - ATTR_INJECT_READS: UnsupportResponseException, - ATTR_STATE: STATE_UNKNOWN, - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - ATTR_UNIQUE_ID: "/sys/bus/w1/devices/42-111111111111/w1_slave", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, - }, - ], - }, - "42-111111111112": { - ATTR_DEVICE_INFO: { - ATTR_IDENTIFIERS: {(DOMAIN, "42-111111111112")}, - ATTR_MANUFACTURER: MANUFACTURER_MAXIM, - ATTR_MODEL: "42", - ATTR_NAME: "42-111111111112", - }, - Platform.SENSOR: [ - { - ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, - ATTR_ENTITY_ID: "sensor.42_111111111112_temperature", - ATTR_INJECT_READS: [UnsupportResponseException] * 9 + [27.993], - ATTR_STATE: "28.0", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, - ATTR_UNIQUE_ID: "/sys/bus/w1/devices/42-111111111112/w1_slave", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, - }, - ], - }, - "42-111111111113": { - ATTR_DEVICE_INFO: { - ATTR_IDENTIFIERS: {(DOMAIN, "42-111111111113")}, - ATTR_MANUFACTURER: MANUFACTURER_MAXIM, - ATTR_MODEL: "42", - ATTR_NAME: "42-111111111113", - }, - Platform.SENSOR: [ - { - ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, - ATTR_ENTITY_ID: "sensor.42_111111111113_temperature", - ATTR_INJECT_READS: [UnsupportResponseException] * 10 + [27.993], - ATTR_STATE: STATE_UNKNOWN, - ATTR_STATE_CLASS: SensorStateClass.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 ee55a550cea..636228af143 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -1,4 +1,4 @@ -"""Tests for 1-Wire devices connected on OWServer.""" +"""Tests for 1-Wire binary sensors.""" import logging from unittest.mock import MagicMock, patch @@ -27,7 +27,7 @@ def override_platforms(): yield -async def test_owserver_binary_sensor( +async def test_binary_sensors( hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock, diff --git a/tests/components/onewire/test_config_flow.py b/tests/components/onewire/test_config_flow.py index 7bf7b58ebfa..d599c10ea90 100644 --- a/tests/components/onewire/test_config_flow.py +++ b/tests/components/onewire/test_config_flow.py @@ -4,15 +4,9 @@ from unittest.mock import AsyncMock, patch from pyownet import protocol import pytest -from homeassistant.components.onewire.const import ( - CONF_MOUNT_DIR, - CONF_TYPE_OWSERVER, - CONF_TYPE_SYSBUS, - DEFAULT_SYSBUS_MOUNT_DIR, - DOMAIN, -) +from homeassistant.components.onewire.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE +from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -30,23 +24,14 @@ def override_async_setup_entry() -> AsyncMock: yield mock_setup_entry -async def test_user_owserver(hass: HomeAssistant, mock_setup_entry: AsyncMock): - """Test OWServer user flow.""" +async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock): + """Test user flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == RESULT_TYPE_FORM assert not result["errors"] - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_TYPE: CONF_TYPE_OWSERVER}, - ) - - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "owserver" - assert not result["errors"] - # Invalid server with patch( "homeassistant.components.onewire.onewirehub.protocol.proxy", @@ -58,7 +43,7 @@ async def test_user_owserver(hass: HomeAssistant, mock_setup_entry: AsyncMock): ) assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "owserver" + assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} # Valid server @@ -73,7 +58,6 @@ async def test_user_owserver(hass: HomeAssistant, mock_setup_entry: AsyncMock): assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == "1.2.3.4" assert result["data"] == { - CONF_TYPE: CONF_TYPE_OWSERVER, CONF_HOST: "1.2.3.4", CONF_PORT: 1234, } @@ -81,10 +65,10 @@ async def test_user_owserver(hass: HomeAssistant, mock_setup_entry: AsyncMock): assert len(mock_setup_entry.mock_calls) == 1 -async def test_user_owserver_duplicate( +async def test_user_duplicate( hass: HomeAssistant, config_entry: ConfigEntry, mock_setup_entry: AsyncMock ): - """Test OWServer flow.""" + """Test user duplicate flow.""" 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 @@ -93,15 +77,7 @@ async def test_user_owserver_duplicate( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == RESULT_TYPE_FORM - assert not result["errors"] - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_TYPE: CONF_TYPE_OWSERVER}, - ) - - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "owserver" + assert result["step_id"] == "user" assert not result["errors"] # Duplicate server @@ -113,93 +89,3 @@ async def test_user_owserver_duplicate( assert result["reason"] == "already_configured" await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - - -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} - ) - assert result["type"] == RESULT_TYPE_FORM - assert not result["errors"] - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_TYPE: CONF_TYPE_SYSBUS}, - ) - - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "mount_dir" - assert not result["errors"] - - # Invalid path - with patch( - "homeassistant.components.onewire.onewirehub.os.path.isdir", - return_value=False, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_MOUNT_DIR: "/sys/bus/invalid_directory"}, - ) - - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "mount_dir" - assert result["errors"] == {"base": "invalid_path"} - - # Valid path - with patch( - "homeassistant.components.onewire.onewirehub.os.path.isdir", - return_value=True, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_MOUNT_DIR: "/sys/bus/directory"}, - ) - - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "/sys/bus/directory" - assert result["data"] == { - CONF_TYPE: CONF_TYPE_SYSBUS, - CONF_MOUNT_DIR: "/sys/bus/directory", - } - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_user_sysbus_duplicate( - hass: HomeAssistant, sysbus_config_entry: ConfigEntry, mock_setup_entry: AsyncMock -): - """Test SysBus duplicate flow.""" - 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} - ) - assert result["type"] == RESULT_TYPE_FORM - assert not result["errors"] - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_TYPE: CONF_TYPE_SYSBUS}, - ) - - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "mount_dir" - assert not result["errors"] - - # Valid path - with patch( - "homeassistant.components.onewire.onewirehub.os.path.isdir", - return_value=True, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_MOUNT_DIR: DEFAULT_SYSBUS_MOUNT_DIR}, - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/onewire/test_diagnostics.py b/tests/components/onewire/test_diagnostics.py index ded4811a586..e279e3a2633 100644 --- a/tests/components/onewire/test_diagnostics.py +++ b/tests/components/onewire/test_diagnostics.py @@ -52,7 +52,6 @@ async def test_entry_diagnostics( "data": { "host": REDACTED, "port": 1234, - "type": "OWServer", }, "options": { "device_options": { diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py index 763cdc9c071..fecade521a8 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -1,5 +1,4 @@ """Tests for 1-Wire config flow.""" -import logging from unittest.mock import MagicMock from pyownet import protocol @@ -11,7 +10,7 @@ from homeassistant.core import HomeAssistant @pytest.mark.usefixtures("owproxy_with_connerror") -async def test_owserver_connect_failure(hass: HomeAssistant, config_entry: ConfigEntry): +async def test_connect_failure(hass: HomeAssistant, config_entry: ConfigEntry): """Test connection failure raises ConfigEntryNotReady.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -21,7 +20,7 @@ async def test_owserver_connect_failure(hass: HomeAssistant, config_entry: Confi assert not hass.data.get(DOMAIN) -async def test_owserver_listing_failure( +async def test_listing_failure( hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock ): """Test listing failure raises ConfigEntryNotReady.""" @@ -49,34 +48,3 @@ async def test_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): assert config_entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) - - -@pytest.mark.usefixtures("sysbus") -async def test_warning_no_devices( - hass: HomeAssistant, - sysbus_config_entry: ConfigEntry, - caplog: pytest.LogCaptureFixture, -): - """Test warning is generated when no sysbus devices found.""" - with caplog.at_level(logging.WARNING, logger="homeassistant.components.onewire"): - await hass.config_entries.async_setup(sysbus_config_entry.entry_id) - await hass.async_block_till_done() - assert "No onewire sensor found. Check if dtoverlay=w1-gpio" in caplog.text - - -@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) diff --git a/tests/components/onewire/test_options_flow.py b/tests/components/onewire/test_options_flow.py index 2edb9da7ffc..e27b5a368d9 100644 --- a/tests/components/onewire/test_options_flow.py +++ b/tests/components/onewire/test_options_flow.py @@ -2,8 +2,6 @@ from unittest.mock import MagicMock, patch from homeassistant.components.onewire.const import ( - CONF_TYPE_SYSBUS, - DOMAIN, INPUT_ENTRY_CLEAR_OPTIONS, INPUT_ENTRY_DEVICE_SELECTION, ) @@ -26,13 +24,7 @@ class FakeDevice: name_by_user = "Given Name" -class FakeOWHubSysBus: - """Mock Class for mocking onewire hub.""" - - type = CONF_TYPE_SYSBUS - - -async def test_user_owserver_options_clear( +async def test_user_options_clear( hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock, @@ -61,7 +53,7 @@ async def test_user_owserver_options_clear( assert result["data"] == {} -async def test_user_owserver_options_empty_selection( +async def test_user_options_empty_selection( hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock, @@ -91,7 +83,7 @@ async def test_user_owserver_options_empty_selection( assert result["errors"] == {"base": "device_not_selected"} -async def test_user_owserver_options_set_single( +async def test_user_options_set_single( hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock, @@ -134,7 +126,7 @@ async def test_user_owserver_options_set_single( ) -async def test_user_owserver_options_set_multiple( +async def test_user_options_set_multiple( hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock, @@ -208,7 +200,7 @@ async def test_user_owserver_options_set_multiple( ) -async def test_user_owserver_options_no_devices( +async def test_user_options_no_devices( hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock, @@ -223,15 +215,3 @@ async def test_user_owserver_options_no_devices( await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "No configurable devices found." - - -async def test_user_sysbus_options( - hass: HomeAssistant, - config_entry: ConfigEntry, -): - """Test that SysBus options flow aborts on init.""" - hass.data[DOMAIN] = {config_entry.entry_id: FakeOWHubSysBus()} - result = await hass.config_entries.options.async_init(config_entry.entry_id) - await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "SysBus setup does not have any config options." diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index 6bfc68d85c8..a20714fedfc 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -1,4 +1,4 @@ -"""Tests for 1-Wire sensor platform.""" +"""Tests for 1-Wire sensors.""" import logging from unittest.mock import MagicMock, patch @@ -14,14 +14,8 @@ from . import ( check_device_registry, check_entities, setup_owproxy_mock_devices, - setup_sysbus_mock_devices, -) -from .const import ( - ATTR_DEVICE_INFO, - ATTR_UNKNOWN_DEVICE, - MOCK_OWPROXY_DEVICES, - MOCK_SYSBUS_DEVICES, ) +from .const import ATTR_DEVICE_INFO, ATTR_UNKNOWN_DEVICE, MOCK_OWPROXY_DEVICES from tests.common import mock_device_registry, mock_registry @@ -33,7 +27,7 @@ def override_platforms(): yield -async def test_owserver_sensor( +async def test_sensors( hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock, @@ -73,44 +67,3 @@ async def test_owserver_sensor( await hass.async_block_till_done() check_entities(hass, entity_registry, expected_entities) - - -@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, - caplog: pytest.LogCaptureFixture, -): - """Test that sysbus config entry works correctly.""" - device_registry = mock_device_registry(hass) - entity_registry = mock_registry(hass) - - glob_result, read_side_effect = setup_sysbus_mock_devices( - Platform.SENSOR, [device_id] - ) - - mock_device = MOCK_SYSBUS_DEVICES[device_id] - expected_entities = mock_device.get(Platform.SENSOR, []) - 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", - side_effect=read_side_effect, - ), caplog.at_level( - logging.WARNING, logger="homeassistant.components.onewire" - ), patch( - "homeassistant.components.onewire.sensor.asyncio.sleep" - ): - await hass.config_entries.async_setup(sysbus_config_entry.entry_id) - await hass.async_block_till_done() - assert "No onewire sensor found. Check if dtoverlay=w1-gpio" not in caplog.text - if mock_device.get(ATTR_UNKNOWN_DEVICE): - assert "Ignoring unknown device family" in caplog.text - else: - assert "Ignoring unknown device family" not in caplog.text - - check_device_registry(device_registry, expected_devices) - assert len(entity_registry.entities) == len(expected_entities) - check_entities(hass, entity_registry, expected_entities) diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index 32212f84b34..8b60232330f 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -1,4 +1,4 @@ -"""Tests for 1-Wire devices connected on OWServer.""" +"""Tests for 1-Wire switches.""" import logging from unittest.mock import MagicMock, patch @@ -35,7 +35,7 @@ def override_platforms(): yield -async def test_owserver_switch( +async def test_switches( hass: HomeAssistant, config_entry: ConfigEntry, owproxy: MagicMock, From 4db289ad6e2656aada241f4b70d422f28ebcde84 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 9 May 2022 07:22:51 -0400 Subject: [PATCH 0333/3516] Remove deprecated yaml config from Deluge (#71487) --- .../components/deluge/config_flow.py | 24 +-------- homeassistant/components/deluge/sensor.py | 52 ++----------------- homeassistant/components/deluge/switch.py | 44 ++-------------- tests/components/deluge/__init__.py | 18 +------ tests/components/deluge/test_config_flow.py | 31 +---------- 5 files changed, 12 insertions(+), 157 deletions(-) diff --git a/homeassistant/components/deluge/config_flow.py b/homeassistant/components/deluge/config_flow.py index fc0dd5ff300..2f38d4d447d 100644 --- a/homeassistant/components/deluge/config_flow.py +++ b/homeassistant/components/deluge/config_flow.py @@ -1,7 +1,6 @@ """Config flow for the Deluge integration.""" from __future__ import annotations -import logging import socket from ssl import SSLError from typing import Any @@ -12,8 +11,6 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow from homeassistant.const import ( CONF_HOST, - CONF_MONITORED_VARIABLES, - CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SOURCE, @@ -30,8 +27,6 @@ from .const import ( DOMAIN, ) -_LOGGER = logging.getLogger(__name__) - class DelugeFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Deluge.""" @@ -41,11 +36,8 @@ class DelugeFlowHandler(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle a flow initiated by the user.""" errors = {} - title = None if user_input is not None: - if CONF_NAME in user_input: - title = user_input.pop(CONF_NAME) if (error := await self.validate_input(user_input)) is None: for entry in self._async_current_entries(): if ( @@ -60,7 +52,7 @@ class DelugeFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="reauth_successful") return self.async_abort(reason="already_configured") return self.async_create_entry( - title=title or DEFAULT_NAME, + title=DEFAULT_NAME, data=user_input, ) errors["base"] = error @@ -87,20 +79,6 @@ class DelugeFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a reauthorization flow request.""" return await self.async_step_user() - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Import a config entry from configuration.yaml.""" - if CONF_MONITORED_VARIABLES in config: - config.pop(CONF_MONITORED_VARIABLES) - config[CONF_WEB_PORT] = DEFAULT_WEB_PORT - - for entry in self._async_current_entries(): - if entry.data[CONF_HOST] == config[CONF_HOST]: - _LOGGER.warning( - "Deluge yaml config has been imported. Please remove it" - ) - return self.async_abort(reason="already_configured") - return await self.async_step_user(config) - async def validate_input(self, user_input: dict[str, Any]) -> str | None: """Handle common flow input validation.""" host = user_input[CONF_HOST] diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 99e63e6ef17..bad535def96 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -1,33 +1,19 @@ """Support for monitoring the Deluge BitTorrent client API.""" from __future__ import annotations -import voluptuous as vol - from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_MONITORED_VARIABLES, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - DATA_RATE_KILOBYTES_PER_SECOND, - STATE_IDLE, - Platform, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DATA_RATE_KILOBYTES_PER_SECOND, STATE_IDLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.typing import StateType from . import DelugeEntity -from .const import DEFAULT_NAME, DEFAULT_RPC_PORT, DOMAIN +from .const import DOMAIN from .coordinator import DelugeDataUpdateCoordinator SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( @@ -49,36 +35,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), ) -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] - -# Deprecated in Home Assistant 2022.3 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_RPC_PORT): cv.port, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MONITORED_VARIABLES, default=[]): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ), - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: entity_platform.AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Deluge sensor component.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/deluge/switch.py b/homeassistant/components/deluge/switch.py index d438d236e5c..cc11fcaf86c 100644 --- a/homeassistant/components/deluge/switch.py +++ b/homeassistant/components/deluge/switch.py @@ -3,52 +3,16 @@ from __future__ import annotations from typing import Any -import voluptuous as vol - -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - Platform, -) +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DelugeEntity -from .const import DEFAULT_RPC_PORT, DOMAIN +from .const import DOMAIN from .coordinator import DelugeDataUpdateCoordinator -# Deprecated in Home Assistant 2022.3 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_RPC_PORT): cv.port, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_NAME, default="Deluge Switch"): cv.string, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: entity_platform.AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Deluge sensor component.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - async def async_setup_entry( hass: HomeAssistant, diff --git a/tests/components/deluge/__init__.py b/tests/components/deluge/__init__.py index 47339f8dfd5..4efbe04cf52 100644 --- a/tests/components/deluge/__init__.py +++ b/tests/components/deluge/__init__.py @@ -5,14 +5,7 @@ from homeassistant.components.deluge.const import ( DEFAULT_RPC_PORT, DEFAULT_WEB_PORT, ) -from homeassistant.const import ( - CONF_HOST, - CONF_MONITORED_VARIABLES, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, -) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME CONF_DATA = { CONF_HOST: "1.2.3.4", @@ -21,12 +14,3 @@ CONF_DATA = { CONF_PORT: DEFAULT_RPC_PORT, CONF_WEB_PORT: DEFAULT_WEB_PORT, } - -IMPORT_DATA = { - CONF_HOST: "1.2.3.4", - CONF_NAME: "Deluge Torrent", - CONF_MONITORED_VARIABLES: ["current_status", "download_speed", "upload_speed"], - CONF_USERNAME: "user", - CONF_PASSWORD: "password", - CONF_PORT: DEFAULT_RPC_PORT, -} diff --git a/tests/components/deluge/test_config_flow.py b/tests/components/deluge/test_config_flow.py index 56dbac55674..b56c717e635 100644 --- a/tests/components/deluge/test_config_flow.py +++ b/tests/components/deluge/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest from homeassistant.components.deluge.const import DEFAULT_NAME, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( @@ -13,7 +13,7 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from . import CONF_DATA, IMPORT_DATA +from . import CONF_DATA from tests.common import MockConfigEntry @@ -102,33 +102,6 @@ async def test_flow_user_unknown_error(hass: HomeAssistant, unknown_error): assert result["errors"] == {"base": "unknown"} -async def test_flow_import(hass: HomeAssistant, api): - """Test import step.""" - 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"] == "Deluge Torrent" - assert result["data"] == CONF_DATA - - -async def test_flow_import_already_configured(hass: HomeAssistant, api): - """Test import step already configured.""" - entry = MockConfigEntry( - domain=DOMAIN, - data=CONF_DATA, - ) - - entry.add_to_hass(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, api): """Test reauth step.""" entry = MockConfigEntry( From 524920dd2e69de4030b93a3c90c88a8f0310f56f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 May 2022 13:49:35 +0200 Subject: [PATCH 0334/3516] Add 'toggle' device action to fans (#71570) --- homeassistant/components/fan/device_action.py | 50 +++---------------- tests/components/fan/test_device_action.py | 25 +++++++++- 2 files changed, 29 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/fan/device_action.py b/homeassistant/components/fan/device_action.py index 0482c31b929..fd38c69c3da 100644 --- a/homeassistant/components/fan/device_action.py +++ b/homeassistant/components/fan/device_action.py @@ -3,64 +3,26 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_DEVICE_ID, - CONF_DOMAIN, - CONF_ENTITY_ID, - CONF_TYPE, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, -) +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN from homeassistant.core import Context, HomeAssistant -from homeassistant.helpers import entity_registry -import homeassistant.helpers.config_validation as cv from . import DOMAIN -ACTION_TYPES = {"turn_on", "turn_off"} - -ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( - { - vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), - vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), - } -) +ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) async def async_get_actions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device actions for Fan devices.""" - registry = await entity_registry.async_get_registry(hass) - actions = [] - - # Get all the integrations entities for this device - for entry in entity_registry.async_entries_for_device(registry, device_id): - if entry.domain != DOMAIN: - continue - - base_action = { - CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, - CONF_ENTITY_ID: entry.entity_id, - } - actions += [{**base_action, CONF_TYPE: action} for action in ACTION_TYPES] - - return actions + return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) async def async_call_action_from_config( hass: HomeAssistant, config: dict, variables: dict, context: Context | None ) -> None: """Execute a device action.""" - service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} - - if config[CONF_TYPE] == "turn_on": - service = SERVICE_TURN_ON - elif config[CONF_TYPE] == "turn_off": - service = SERVICE_TURN_OFF - - await hass.services.async_call( - DOMAIN, service, service_data, blocking=True, context=context + await toggle_entity.async_call_action_from_config( + hass, config, variables, context, DOMAIN ) diff --git a/tests/components/fan/test_device_action.py b/tests/components/fan/test_device_action.py index ee300aad39f..8800e61675e 100644 --- a/tests/components/fan/test_device_action.py +++ b/tests/components/fan/test_device_action.py @@ -50,7 +50,7 @@ async def test_get_actions(hass, device_reg, entity_reg): "entity_id": f"{DOMAIN}.test_5678", "metadata": {"secondary": False}, } - for action in ["turn_on", "turn_off"] + for action in ["turn_on", "turn_off", "toggle"] ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id @@ -98,7 +98,7 @@ async def test_get_actions_hidden_auxiliary( "entity_id": f"{DOMAIN}.test_5678", "metadata": {"secondary": True}, } - for action in ["turn_on", "turn_off"] + for action in ["turn_on", "turn_off", "toggle"] ] actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device_entry.id @@ -137,19 +137,40 @@ async def test_action(hass): "type": "turn_on", }, }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_toggle", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "fan.entity", + "type": "toggle", + }, + }, ] }, ) turn_off_calls = async_mock_service(hass, "fan", "turn_off") turn_on_calls = async_mock_service(hass, "fan", "turn_on") + toggle_calls = async_mock_service(hass, "fan", "toggle") hass.bus.async_fire("test_event_turn_off") await hass.async_block_till_done() assert len(turn_off_calls) == 1 assert len(turn_on_calls) == 0 + assert len(toggle_calls) == 0 hass.bus.async_fire("test_event_turn_on") await hass.async_block_till_done() assert len(turn_off_calls) == 1 assert len(turn_on_calls) == 1 + assert len(toggle_calls) == 0 + + hass.bus.async_fire("test_event_toggle") + await hass.async_block_till_done() + assert len(turn_off_calls) == 1 + assert len(turn_on_calls) == 1 + assert len(toggle_calls) == 1 From d284e579bb95955c031db2c41d08d6a36b792c0d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 May 2022 13:57:32 +0200 Subject: [PATCH 0335/3516] Improve Google Cast detection of HLS playlists (#71564) --- homeassistant/components/cast/helpers.py | 17 ++++++++++++++--- tests/components/cast/test_helpers.py | 21 ++++++++++++++++++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index dd98a2bc051..dfeb9fce25b 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -237,12 +237,14 @@ def _is_url(url): return all([result.scheme, result.netloc]) -async def _fetch_playlist(hass, url): +async def _fetch_playlist(hass, url, supported_content_types): """Fetch a playlist from the given url.""" try: session = aiohttp_client.async_get_clientsession(hass, verify_ssl=False) async with session.get(url, timeout=5) as resp: charset = resp.charset or "utf-8" + if resp.content_type in supported_content_types: + raise PlaylistSupported try: playlist_data = (await resp.content.read(64 * 1024)).decode(charset) except ValueError as err: @@ -260,7 +262,16 @@ async def parse_m3u(hass, url): Based on https://github.com/dvndrsn/M3uParser/blob/master/m3uparser.py """ - m3u_data = await _fetch_playlist(hass, url) + # From Mozilla gecko source: https://github.com/mozilla/gecko-dev/blob/c4c1adbae87bf2d128c39832d72498550ee1b4b8/dom/media/DecoderTraits.cpp#L47-L52 + hls_content_types = ( + # https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-10 + "application/vnd.apple.mpegurl", + # Some sites serve these as the informal HLS m3u type. + "application/x-mpegurl", + "audio/mpegurl", + "audio/x-mpegurl", + ) + m3u_data = await _fetch_playlist(hass, url, hls_content_types) m3u_lines = m3u_data.splitlines() playlist = [] @@ -301,7 +312,7 @@ async def parse_pls(hass, url): Based on https://github.com/mariob/plsparser/blob/master/src/plsparser.py """ - pls_data = await _fetch_playlist(hass, url) + pls_data = await _fetch_playlist(hass, url, ()) pls_parser = configparser.ConfigParser() try: diff --git a/tests/components/cast/test_helpers.py b/tests/components/cast/test_helpers.py index 0d7a3b1ff14..d729d36a225 100644 --- a/tests/components/cast/test_helpers.py +++ b/tests/components/cast/test_helpers.py @@ -14,10 +14,25 @@ from homeassistant.components.cast.helpers import ( from tests.common import load_fixture -async def test_hls_playlist_supported(hass, aioclient_mock): +@pytest.mark.parametrize( + "url,fixture,content_type", + ( + ( + "http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_low/ak/bbc_radio_fourfm.m3u8", + "bbc_radio_fourfm.m3u8", + None, + ), + ( + "https://rthkaudio2-lh.akamaihd.net/i/radio2_1@355865/master.m3u8", + "rthkaudio2.m3u8", + "application/vnd.apple.mpegurl", + ), + ), +) +async def test_hls_playlist_supported(hass, aioclient_mock, url, fixture, content_type): """Test playlist parsing of HLS playlist.""" - url = "http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_low/ak/bbc_radio_fourfm.m3u8" - aioclient_mock.get(url, text=load_fixture("bbc_radio_fourfm.m3u8", "cast")) + headers = {"content-type": content_type} + aioclient_mock.get(url, text=load_fixture(fixture, "cast"), headers=headers) with pytest.raises(PlaylistSupported): await parse_playlist(hass, url) From 0842c291090a4d2ed6414cf4075107816396b866 Mon Sep 17 00:00:00 2001 From: Evan Bruhn Date: Mon, 9 May 2022 21:58:26 +1000 Subject: [PATCH 0336/3516] Bump logi_circle to 0.2.3 (#71578) --- homeassistant/components/logi_circle/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/logi_circle/manifest.json b/homeassistant/components/logi_circle/manifest.json index 94c040f3b75..2d8495df576 100644 --- a/homeassistant/components/logi_circle/manifest.json +++ b/homeassistant/components/logi_circle/manifest.json @@ -3,7 +3,7 @@ "name": "Logi Circle", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/logi_circle", - "requirements": ["logi_circle==0.2.2"], + "requirements": ["logi_circle==0.2.3"], "dependencies": ["ffmpeg", "http"], "codeowners": ["@evanjd"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 644b5c2b74b..c6d6b10ad64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -957,7 +957,7 @@ lmnotify==0.0.4 locationsharinglib==4.1.5 # homeassistant.components.logi_circle -logi_circle==0.2.2 +logi_circle==0.2.3 # homeassistant.components.london_underground london-tube-status==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a7859ff3e1e..ecd8fc1f457 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -655,7 +655,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.logi_circle -logi_circle==0.2.2 +logi_circle==0.2.3 # homeassistant.components.recorder lru-dict==1.1.7 From ddd22398f2c49d106930a84ec7e3c90b7838502f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 May 2022 13:59:13 +0200 Subject: [PATCH 0337/3516] Bump pychromecast to 12.1.2 (#71567) --- 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 cee46913937..10edc81e0fc 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==12.1.1"], + "requirements": ["pychromecast==12.1.2"], "after_dependencies": [ "cloud", "http", diff --git a/requirements_all.txt b/requirements_all.txt index c6d6b10ad64..fc76ea2dc35 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1399,7 +1399,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==12.1.1 +pychromecast==12.1.2 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ecd8fc1f457..cf3e5a1217b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -938,7 +938,7 @@ pybotvac==0.0.23 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==12.1.1 +pychromecast==12.1.2 # homeassistant.components.climacell pyclimacell==0.18.2 From 141688e210fd91910e055a27dee4f8e204c60d50 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 9 May 2022 14:17:48 +0200 Subject: [PATCH 0338/3516] Cleanup ServiceInfo compatibility (#60540) Co-authored-by: epenet Co-authored-by: J. Nick Koston --- homeassistant/components/dhcp/__init__.py | 33 ---- homeassistant/components/mqtt/__init__.py | 15 -- homeassistant/components/ssdp/__init__.py | 58 ------- homeassistant/components/usb/__init__.py | 17 +- homeassistant/components/zeroconf/__init__.py | 31 ---- tests/components/dhcp/test_init.py | 25 --- tests/components/mqtt/test_init.py | 20 --- tests/components/ssdp/test_init.py | 162 ------------------ tests/components/usb/test_init.py | 21 --- tests/components/zeroconf/test_init.py | 31 ---- 10 files changed, 1 insertion(+), 412 deletions(-) diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 1756f620f46..8581a5f2241 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -52,14 +52,11 @@ from homeassistant.helpers.event import ( async_track_state_added_domain, async_track_time_interval, ) -from homeassistant.helpers.frame import report from homeassistant.helpers.typing import ConfigType from homeassistant.loader import DHCPMatcher, 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 - if TYPE_CHECKING: from scapy.packet import Packet from scapy.sendrecv import AsyncSniffer @@ -86,36 +83,6 @@ class DhcpServiceInfo(BaseServiceInfo): hostname: str macaddress: str - def __getitem__(self, name: str) -> Any: - """ - Enable method for compatibility reason. - - Deprecated, and will be removed in version 2022.6. - """ - report( - f"accessed discovery_info['{name}'] instead of discovery_info.{name}; " - "this will fail in version 2022.6", - exclude_integrations={DOMAIN}, - error_if_core=False, - ) - return getattr(self, name) - - def get(self, name: str, default: Any = None) -> Any: - """ - Enable method for compatibility reason. - - Deprecated, and will be removed in version 2022.6. - """ - report( - f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; " - "this will fail in version 2022.6", - exclude_integrations={DOMAIN}, - error_if_core=False, - ) - if hasattr(self, name): - return getattr(self, name) - return default - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the dhcp component.""" diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 7c16da7f2aa..162e344f852 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -54,7 +54,6 @@ from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity -from homeassistant.helpers.frame import report from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util @@ -419,20 +418,6 @@ class MqttServiceInfo(BaseServiceInfo): subscribed_topic: str timestamp: dt.datetime - def __getitem__(self, name: str) -> Any: - """ - Allow property access by name for compatibility reason. - - Deprecated, and will be removed in version 2022.6. - """ - report( - f"accessed discovery_info['{name}'] instead of discovery_info.{name}; " - "this will fail in version 2022.6", - exclude_integrations={DOMAIN}, - error_if_core=False, - ) - return getattr(self, name) - def publish( hass: HomeAssistant, diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 8e8663a9734..e854472f21f 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -25,7 +25,6 @@ from homeassistant.data_entry_flow import BaseServiceInfo 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.frame import report from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_ssdp, bind_hass @@ -111,63 +110,6 @@ class SsdpServiceInfo( ): """Prepared info from ssdp/upnp entries.""" - def __getitem__(self, name: str) -> Any: - """ - Allow property access by name for compatibility reason. - - Deprecated, and will be removed in version 2022.6. - """ - report( - f"accessed discovery_info['{name}'] instead of discovery_info.{name}, " - f"discovery_info.upnp['{name}'] " - f"or discovery_info.ssdp_headers['{name}']; " - "this will fail in version 2022.6", - exclude_integrations={DOMAIN}, - error_if_core=False, - ) - # Use a property if it is available, fallback to upnp data - if hasattr(self, name): - return getattr(self, name) - if name in self.ssdp_headers and name not in self.upnp: - return self.ssdp_headers.get(name) - return self.upnp[name] - - def get(self, name: str, default: Any = None) -> Any: - """ - Enable method for compatibility reason. - - Deprecated, and will be removed in version 2022.6. - """ - report( - f"accessed discovery_info.get('{name}') instead of discovery_info.{name}, " - f"discovery_info.upnp.get('{name}') " - f"or discovery_info.ssdp_headers.get('{name}'); " - "this will fail in version 2022.6", - exclude_integrations={DOMAIN}, - error_if_core=False, - ) - if hasattr(self, name): - return getattr(self, name) - return self.upnp.get(name, self.ssdp_headers.get(name, default)) - - def __contains__(self, name: str) -> bool: - """ - Enable method for compatibility reason. - - Deprecated, and will be removed in version 2022.6. - """ - report( - f"accessed discovery_info.__contains__('{name}') " - f"instead of discovery_info.upnp.__contains__('{name}') " - f"or discovery_info.ssdp_headers.__contains__('{name}'); " - "this will fail in version 2022.6", - exclude_integrations={DOMAIN}, - error_if_core=False, - ) - if hasattr(self, name): - return getattr(self, name) is not None - return name in self.upnp or name in self.ssdp_headers - SsdpChange = Enum("SsdpChange", "ALIVE BYEBYE UPDATE") SsdpCallback = Callable[[SsdpServiceInfo, SsdpChange], Awaitable] diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 5ac390ad168..7a56a659d07 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -6,7 +6,7 @@ import fnmatch import logging import os import sys -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from serial.tools.list_ports import comports from serial.tools.list_ports_common import ListPortInfo @@ -20,7 +20,6 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import discovery_flow, system_info from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.frame import report from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_usb @@ -47,20 +46,6 @@ class UsbServiceInfo(BaseServiceInfo): manufacturer: str | None description: str | None - def __getitem__(self, name: str) -> Any: - """ - Allow property access by name for compatibility reason. - - Deprecated, and will be removed in version 2022.6. - """ - report( - f"accessed discovery_info['{name}'] instead of discovery_info.{name}; " - "this will fail in version 2022.6", - exclude_integrations={DOMAIN}, - error_if_core=False, - ) - return getattr(self, name) - def human_readable_device_name( device: str, diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index ffe4140a434..2c16868f9de 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -30,7 +30,6 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import discovery_flow import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.frame import report from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.typing import ConfigType from homeassistant.loader import ( @@ -108,36 +107,6 @@ class ZeroconfServiceInfo(BaseServiceInfo): name: str properties: dict[str, Any] - def __getitem__(self, name: str) -> Any: - """ - Enable method for compatibility reason. - - Deprecated, and will be removed in version 2022.6. - """ - report( - f"accessed discovery_info['{name}'] instead of discovery_info.{name}; " - "this will fail in version 2022.6", - exclude_integrations={DOMAIN}, - error_if_core=False, - ) - return getattr(self, name) - - def get(self, name: str, default: Any = None) -> Any: - """ - Enable method for compatibility reason. - - Deprecated, and will be removed in version 2022.6. - """ - report( - f"accessed discovery_info.get('{name}') instead of discovery_info.{name}; " - "this will fail in version 2022.6", - exclude_integrations={DOMAIN}, - error_if_core=False, - ) - if hasattr(self, name): - return getattr(self, name) - return default - @bind_hass async def async_get_instance(hass: HomeAssistant) -> HaZeroconf: diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index a809d6eb5ab..c6d889eab72 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -3,7 +3,6 @@ import datetime import threading from unittest.mock import MagicMock, patch -import pytest from scapy import arch # pylint: disable=unused-import # noqa: F401 from scapy.error import Scapy_Exception from scapy.layers.dhcp import DHCP @@ -973,27 +972,3 @@ async def test_aiodiscover_finds_new_hosts_after_interval(hass): hostname="connect", macaddress="b8b7f16db533", ) - - -@pytest.mark.usefixtures("mock_integration_frame") -async def test_service_info_compatibility(hass, caplog): - """Test compatibility with old-style dict. - - To be removed in 2022.6 - """ - discovery_info = dhcp.DhcpServiceInfo( - ip="192.168.210.56", - hostname="connect", - macaddress="b8b7f16db533", - ) - - with patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()): - assert discovery_info["ip"] == "192.168.210.56" - assert "Detected integration that accessed discovery_info['ip']" in caplog.text - - with patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()): - assert discovery_info.get("ip") == "192.168.210.56" - assert "Detected integration that accessed discovery_info.get('ip')" in caplog.text - - assert discovery_info.get("ip", "fallback_host") == "192.168.210.56" - assert discovery_info.get("invalid_key", "fallback_host") == "fallback_host" diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 763cbfc1b71..543174653ec 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -2380,26 +2380,6 @@ async def test_publish_json_from_template(hass, mqtt_mock): assert mqtt_mock.async_publish.call_args[0][1] == test_str -@pytest.mark.usefixtures("mock_integration_frame") -async def test_service_info_compatibility(hass, caplog): - """Test compatibility with old-style dict. - - To be removed in 2022.6 - """ - discovery_info = mqtt.MqttServiceInfo( - topic="tasmota/discovery/DC4F220848A2/config", - payload="", - qos=0, - retain=False, - subscribed_topic="tasmota/discovery/#", - timestamp=None, - ) - - with patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()): - assert discovery_info["topic"] == "tasmota/discovery/DC4F220848A2/config" - assert "Detected integration that accessed discovery_info['topic']" in caplog.text - - async def test_subscribe_connection_status(hass, mqtt_mock, mqtt_client_mock): """Test connextion status subscription.""" mqtt_connected_calls = [] diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 5bbc90307b5..bf88f45acf9 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -75,17 +75,6 @@ async def test_ssdp_flow_dispatched_on_st(mock_get_ssdp, hass, caplog, mock_flow assert mock_call_data.x_homeassistant_matching_domains == {"mock-domain"} assert mock_call_data.upnp == {ssdp.ATTR_UPNP_UDN: "uuid:mock-udn"} assert "Failed to fetch ssdp data" not in caplog.text - # Compatibility with old dict access (to be removed after 2022.6) - assert mock_call_data[ssdp.ATTR_SSDP_ST] == "mock-st" - assert mock_call_data[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1" - assert mock_call_data[ssdp.ATTR_SSDP_USN] == "uuid:mock-udn::mock-st" - assert mock_call_data[ssdp.ATTR_SSDP_SERVER] == "mock-server" - assert mock_call_data[ssdp.ATTR_SSDP_EXT] == "" - assert mock_call_data[ssdp.ATTR_UPNP_UDN] == "uuid:mock-udn" - assert mock_call_data[ssdp.ATTR_SSDP_UDN] == ANY - assert mock_call_data["_timestamp"] == ANY - assert mock_call_data[ssdp.ATTR_HA_MATCHING_DOMAINS] == {"mock-domain"} - # End compatibility checks @patch( @@ -129,17 +118,6 @@ async def test_ssdp_flow_dispatched_on_manufacturer_url( assert mock_call_data.x_homeassistant_matching_domains == {"mock-domain"} assert mock_call_data.upnp == {ssdp.ATTR_UPNP_UDN: "uuid:mock-udn"} assert "Failed to fetch ssdp data" not in caplog.text - # Compatibility with old dict access (to be removed after 2022.6) - assert mock_call_data[ssdp.ATTR_SSDP_ST] == "mock-st" - assert mock_call_data[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1" - assert mock_call_data[ssdp.ATTR_SSDP_USN] == "uuid:mock-udn::mock-st" - assert mock_call_data[ssdp.ATTR_SSDP_SERVER] == "mock-server" - assert mock_call_data[ssdp.ATTR_SSDP_EXT] == "" - assert mock_call_data[ssdp.ATTR_UPNP_UDN] == "uuid:mock-udn" - assert mock_call_data[ssdp.ATTR_SSDP_UDN] == ANY - assert mock_call_data["_timestamp"] == ANY - assert mock_call_data[ssdp.ATTR_HA_MATCHING_DOMAINS] == {"mock-domain"} - # End compatibility checks @pytest.mark.usefixtures("mock_get_source_ip") @@ -426,18 +404,6 @@ async def test_discovery_from_advertisement_sets_ssdp_st( ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", ssdp.ATTR_UPNP_UDN: "uuid:mock-udn", } - # Compatibility with old dict access (to be removed after 2022.6) - assert discovery_info[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1" - assert discovery_info[ssdp.ATTR_SSDP_NT] == "mock-st" - # Set by ssdp component, not in original advertisement. - assert discovery_info[ssdp.ATTR_SSDP_ST] == "mock-st" - assert discovery_info[ssdp.ATTR_SSDP_USN] == "uuid:mock-udn::mock-st" - assert discovery_info[ssdp.ATTR_UPNP_UDN] == "uuid:mock-udn" - assert discovery_info[ssdp.ATTR_UPNP_DEVICE_TYPE] == "Paulus" - assert discovery_info[ssdp.ATTR_SSDP_UDN] == ANY - assert discovery_info["nts"] == "ssdp:alive" - assert discovery_info["_timestamp"] == ANY - # End compatibility checks @patch( @@ -547,25 +513,6 @@ async def test_scan_with_registered_callback( ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", } - # Compatibility with old dict access (to be removed after 2022.6) - assert mock_call_data[ssdp.ATTR_UPNP_DEVICE_TYPE] == "Paulus" - assert mock_call_data[ssdp.ATTR_SSDP_EXT] == "" - assert mock_call_data[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1" - assert mock_call_data[ssdp.ATTR_SSDP_SERVER] == "mock-server" - assert mock_call_data[ssdp.ATTR_SSDP_ST] == "mock-st" - assert ( - mock_call_data[ssdp.ATTR_SSDP_USN] - == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::mock-st" - ) - assert ( - mock_call_data[ssdp.ATTR_UPNP_UDN] - == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" - ) - assert mock_call_data["x-rincon-bootseq"] == "55" - assert mock_call_data[ssdp.ATTR_SSDP_UDN] == ANY - assert mock_call_data["_timestamp"] == ANY - assert mock_call_data[ssdp.ATTR_HA_MATCHING_DOMAINS] == set() - # End of compatibility checks assert "Failed to callback info" in caplog.text async_integration_callback_from_cache = AsyncMock() @@ -622,23 +569,6 @@ async def test_getting_existing_headers( ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", } - # Compatibility with old dict access (to be removed after 2022.6) - assert discovery_info_by_st[ssdp.ATTR_SSDP_EXT] == "" - assert discovery_info_by_st[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1" - assert discovery_info_by_st[ssdp.ATTR_SSDP_SERVER] == "mock-server" - assert discovery_info_by_st[ssdp.ATTR_SSDP_ST] == "mock-st" - assert ( - discovery_info_by_st[ssdp.ATTR_SSDP_USN] - == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3" - ) - assert ( - discovery_info_by_st[ssdp.ATTR_UPNP_UDN] - == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" - ) - assert discovery_info_by_st[ssdp.ATTR_UPNP_DEVICE_TYPE] == "Paulus" - assert discovery_info_by_st[ssdp.ATTR_SSDP_UDN] == ANY - assert discovery_info_by_st["_timestamp"] == ANY - # End of compatibility checks discovery_info_by_udn = await ssdp.async_get_discovery_info_by_udn( hass, "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" @@ -658,23 +588,6 @@ async def test_getting_existing_headers( ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", } - # Compatibility with old dict access (to be removed after 2022.6) - assert discovery_info_by_udn[ssdp.ATTR_SSDP_EXT] == "" - assert discovery_info_by_udn[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1" - assert discovery_info_by_udn[ssdp.ATTR_SSDP_SERVER] == "mock-server" - assert discovery_info_by_udn[ssdp.ATTR_SSDP_ST] == "mock-st" - assert ( - discovery_info_by_udn[ssdp.ATTR_SSDP_USN] - == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3" - ) - assert ( - discovery_info_by_udn[ssdp.ATTR_UPNP_UDN] - == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" - ) - assert discovery_info_by_udn[ssdp.ATTR_UPNP_DEVICE_TYPE] == "Paulus" - assert discovery_info_by_udn[ssdp.ATTR_SSDP_UDN] == ANY - assert discovery_info_by_udn["_timestamp"] == ANY - # End of compatibility checks discovery_info_by_udn_st = await ssdp.async_get_discovery_info_by_udn_st( hass, "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", "mock-st" @@ -693,23 +606,6 @@ async def test_getting_existing_headers( ssdp.ATTR_UPNP_DEVICE_TYPE: "Paulus", ssdp.ATTR_UPNP_UDN: "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL", } - # Compatibility with old dict access (to be removed after 2022.6) - assert discovery_info_by_udn_st[ssdp.ATTR_SSDP_EXT] == "" - assert discovery_info_by_udn_st[ssdp.ATTR_SSDP_LOCATION] == "http://1.1.1.1" - assert discovery_info_by_udn_st[ssdp.ATTR_SSDP_SERVER] == "mock-server" - assert discovery_info_by_udn_st[ssdp.ATTR_SSDP_ST] == "mock-st" - assert ( - discovery_info_by_udn_st[ssdp.ATTR_SSDP_USN] - == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL::urn:mdx-netflix-com:service:target:3" - ) - assert ( - discovery_info_by_udn_st[ssdp.ATTR_UPNP_UDN] - == "uuid:TIVRTLSR7ANF-D6E-1557809135086-RETAIL" - ) - assert discovery_info_by_udn_st[ssdp.ATTR_UPNP_DEVICE_TYPE] == "Paulus" - assert discovery_info_by_udn_st[ssdp.ATTR_SSDP_UDN] == ANY - assert discovery_info_by_udn_st["_timestamp"] == ANY - # End of compatibility checks assert ( await ssdp.async_get_discovery_info_by_udn_st(hass, "wrong", "mock-st") is None @@ -845,61 +741,3 @@ async def test_ipv4_does_additional_search_for_sonos( ), ) assert ssdp_listener.async_search.call_args[1] == {} - - -@pytest.mark.usefixtures("mock_integration_frame") -async def test_service_info_compatibility(hass, caplog): - """Test compatibility with old-style dict. - - To be removed in 2022.6 - """ - discovery_info = ssdp.SsdpServiceInfo( - ssdp_st="mock-st", - ssdp_location="http://1.1.1.1", - ssdp_usn="uuid:mock-udn::mock-st", - ssdp_server="mock-server", - ssdp_ext="", - ssdp_headers=_ssdp_headers( - { - "st": "mock-st", - "location": "http://1.1.1.1", - "usn": "uuid:mock-udn::mock-st", - "server": "mock-server", - "ext": "", - } - ), - upnp={ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC"}, - ) - - with patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()): - assert discovery_info["ssdp_st"] == "mock-st" - assert "Detected integration that accessed discovery_info['ssdp_st']" in caplog.text - - with patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()): - assert discovery_info.get("ssdp_location") == "http://1.1.1.1" - assert ( - "Detected integration that accessed discovery_info.get('ssdp_location')" - in caplog.text - ) - - with patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()): - assert "ssdp_usn" in discovery_info - assert ( - "Detected integration that accessed discovery_info.__contains__('ssdp_usn')" - in caplog.text - ) - - # Root item - assert discovery_info["ssdp_usn"] == "uuid:mock-udn::mock-st" - assert discovery_info.get("ssdp_usn") == "uuid:mock-udn::mock-st" - assert "ssdp_usn" in discovery_info - - # SSDP header - assert discovery_info["st"] == "mock-st" - assert discovery_info.get("st") == "mock-st" - assert "st" in discovery_info - - # UPnP item - assert discovery_info[ssdp.ATTR_UPNP_DEVICE_TYPE] == "ABC" - assert discovery_info.get(ssdp.ATTR_UPNP_DEVICE_TYPE) == "ABC" - assert ssdp.ATTR_UPNP_DEVICE_TYPE in discovery_info diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index f1cf67bfb78..f4245a0e0d6 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -833,24 +833,3 @@ def test_human_readable_device_name(): assert "Silicon Labs" in name assert "10C4" in name assert "8A2A" in name - - -@pytest.mark.usefixtures("mock_integration_frame") -async def test_service_info_compatibility(hass, caplog): - """Test compatibility with old-style dict. - - To be removed in 2022.6 - """ - discovery_info = usb.UsbServiceInfo( - device=slae_sh_device.device, - vid=12345, - pid=12345, - serial_number=slae_sh_device.serial_number, - manufacturer=slae_sh_device.manufacturer, - description=slae_sh_device.description, - ) - - # Ensure first call get logged - with patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()): - assert discovery_info["vid"] == 12345 - assert "Detected integration that accessed discovery_info['vid']" in caplog.text diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 900dbeb8431..88dceb9d464 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -3,7 +3,6 @@ from ipaddress import ip_address from typing import Any from unittest.mock import call, patch -import pytest from zeroconf import InterfaceChoice, IPVersion, ServiceStateChange from zeroconf.asyncio import AsyncServiceInfo @@ -1116,33 +1115,3 @@ async def test_no_name(hass, mock_async_zeroconf): register_call = mock_async_zeroconf.async_register_service.mock_calls[-1] info = register_call.args[0] assert info.name == "Home._home-assistant._tcp.local." - - -@pytest.mark.usefixtures("mock_integration_frame") -async def test_service_info_compatibility(hass, caplog): - """Test compatibility with old-style dict. - - To be removed in 2022.6 - """ - discovery_info = zeroconf.ZeroconfServiceInfo( - host="mock_host", - addresses=["mock_host"], - port=None, - hostname="mock_hostname", - type="mock_type", - name="mock_name", - properties={}, - ) - - with patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()): - assert discovery_info["host"] == "mock_host" - assert "Detected integration that accessed discovery_info['host']" in caplog.text - - with patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()): - assert discovery_info.get("host") == "mock_host" - assert ( - "Detected integration that accessed discovery_info.get('host')" in caplog.text - ) - - assert discovery_info.get("host", "fallback_host") == "mock_host" - assert discovery_info.get("invalid_key", "fallback_host") == "fallback_host" From a599325c2dd3a812f4c344a311c951b538f15e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 9 May 2022 14:44:14 +0200 Subject: [PATCH 0339/3516] Add QNAP QSW Binary Sensor platform (#70868) --- homeassistant/components/qnap_qsw/__init__.py | 51 +--------- .../components/qnap_qsw/binary_sensor.py | 88 ++++++++++++++++++ homeassistant/components/qnap_qsw/const.py | 1 + homeassistant/components/qnap_qsw/entity.py | 93 +++++++++++++++++++ homeassistant/components/qnap_qsw/sensor.py | 26 +----- .../components/qnap_qsw/test_binary_sensor.py | 17 ++++ 6 files changed, 206 insertions(+), 70 deletions(-) create mode 100644 homeassistant/components/qnap_qsw/binary_sensor.py create mode 100644 homeassistant/components/qnap_qsw/entity.py create mode 100644 tests/components/qnap_qsw/test_binary_sensor.py diff --git a/homeassistant/components/qnap_qsw/__init__.py b/homeassistant/components/qnap_qsw/__init__.py index c1b96a3298e..838a567199d 100644 --- a/homeassistant/components/qnap_qsw/__init__.py +++ b/homeassistant/components/qnap_qsw/__init__.py @@ -1,64 +1,17 @@ """The QNAP QSW integration.""" from __future__ import annotations -from typing import Any - -from aioqsw.const import ( - QSD_FIRMWARE, - QSD_FIRMWARE_INFO, - QSD_MAC, - QSD_PRODUCT, - QSD_SYSTEM_BOARD, -) from aioqsw.localapi import ConnectionOptions, QnapQswApi from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, MANUFACTURER +from .const import DOMAIN from .coordinator import QswUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.SENSOR] - - -class QswEntity(CoordinatorEntity[QswUpdateCoordinator]): - """Define an QNAP QSW entity.""" - - def __init__( - self, - coordinator: QswUpdateCoordinator, - entry: ConfigEntry, - ) -> None: - """Initialize.""" - super().__init__(coordinator) - - self._attr_device_info = DeviceInfo( - configuration_url=entry.data[CONF_URL], - connections={ - ( - CONNECTION_NETWORK_MAC, - self.get_device_value(QSD_SYSTEM_BOARD, QSD_MAC), - ) - }, - manufacturer=MANUFACTURER, - model=self.get_device_value(QSD_SYSTEM_BOARD, QSD_PRODUCT), - name=self.get_device_value(QSD_SYSTEM_BOARD, QSD_PRODUCT), - sw_version=self.get_device_value(QSD_FIRMWARE_INFO, QSD_FIRMWARE), - ) - - def get_device_value(self, key: str, subkey: str) -> Any: - """Return device value by key.""" - value = None - if key in self.coordinator.data: - data = self.coordinator.data[key] - if subkey in data: - value = data[subkey] - return value +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/qnap_qsw/binary_sensor.py b/homeassistant/components/qnap_qsw/binary_sensor.py new file mode 100644 index 00000000000..467a3314070 --- /dev/null +++ b/homeassistant/components/qnap_qsw/binary_sensor.py @@ -0,0 +1,88 @@ +"""Support for the QNAP QSW binary sensors.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Final + +from aioqsw.const import QSD_ANOMALY, QSD_FIRMWARE_CONDITION, QSD_MESSAGE + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ATTR_MESSAGE, DOMAIN +from .coordinator import QswUpdateCoordinator +from .entity import QswEntityDescription, QswSensorEntity + + +@dataclass +class QswBinarySensorEntityDescription( + BinarySensorEntityDescription, QswEntityDescription +): + """A class that describes QNAP QSW binary sensor entities.""" + + attributes: dict[str, list[str]] | None = None + + +BINARY_SENSOR_TYPES: Final[tuple[QswBinarySensorEntityDescription, ...]] = ( + QswBinarySensorEntityDescription( + attributes={ + ATTR_MESSAGE: [QSD_FIRMWARE_CONDITION, QSD_MESSAGE], + }, + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + key=QSD_FIRMWARE_CONDITION, + name="Anomaly", + subkey=QSD_ANOMALY, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add QNAP QSW binary sensors from a config_entry.""" + coordinator: QswUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + QswBinarySensor(coordinator, description, entry) + for description in BINARY_SENSOR_TYPES + if ( + description.key in coordinator.data + and description.subkey in coordinator.data[description.key] + ) + ) + + +class QswBinarySensor(QswSensorEntity, BinarySensorEntity): + """Define a QNAP QSW binary sensor.""" + + entity_description: QswBinarySensorEntityDescription + + def __init__( + self, + coordinator: QswUpdateCoordinator, + description: QswBinarySensorEntityDescription, + entry: ConfigEntry, + ) -> None: + """Initialize.""" + super().__init__(coordinator, entry) + self._attr_name = f"{self.product} {description.name}" + self._attr_unique_id = ( + f"{entry.unique_id}_{description.key}_{description.subkey}" + ) + self.entity_description = description + self._async_update_attrs() + + @callback + def _async_update_attrs(self) -> None: + """Update binary sensor attributes.""" + self._attr_is_on = self.get_device_value( + self.entity_description.key, self.entity_description.subkey + ) + super()._async_update_attrs() diff --git a/homeassistant/components/qnap_qsw/const.py b/homeassistant/components/qnap_qsw/const.py index b55a817927f..a6cacfd1c40 100644 --- a/homeassistant/components/qnap_qsw/const.py +++ b/homeassistant/components/qnap_qsw/const.py @@ -3,6 +3,7 @@ from typing import Final ATTR_MAX: Final = "max" +ATTR_MESSAGE: Final = "message" DOMAIN: Final = "qnap_qsw" MANUFACTURER: Final = "QNAP" diff --git a/homeassistant/components/qnap_qsw/entity.py b/homeassistant/components/qnap_qsw/entity.py new file mode 100644 index 00000000000..c3550610d83 --- /dev/null +++ b/homeassistant/components/qnap_qsw/entity.py @@ -0,0 +1,93 @@ +"""Entity classes for the QNAP QSW integration.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from aioqsw.const import ( + QSD_FIRMWARE, + QSD_FIRMWARE_INFO, + QSD_MAC, + QSD_PRODUCT, + QSD_SYSTEM_BOARD, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_URL +from homeassistant.core import callback +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import MANUFACTURER +from .coordinator import QswUpdateCoordinator + + +class QswEntity(CoordinatorEntity[QswUpdateCoordinator]): + """Define an QNAP QSW entity.""" + + def __init__( + self, + coordinator: QswUpdateCoordinator, + entry: ConfigEntry, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self.product = self.get_device_value(QSD_SYSTEM_BOARD, QSD_PRODUCT) + self._attr_device_info = DeviceInfo( + configuration_url=entry.data[CONF_URL], + connections={ + ( + CONNECTION_NETWORK_MAC, + self.get_device_value(QSD_SYSTEM_BOARD, QSD_MAC), + ) + }, + manufacturer=MANUFACTURER, + model=self.product, + name=self.product, + sw_version=self.get_device_value(QSD_FIRMWARE_INFO, QSD_FIRMWARE), + ) + + def get_device_value(self, key: str, subkey: str) -> Any: + """Return device value by key.""" + value = None + if key in self.coordinator.data: + data = self.coordinator.data[key] + if subkey in data: + value = data[subkey] + return value + + +@dataclass +class QswEntityDescriptionMixin: + """Mixin to describe a QSW entity.""" + + subkey: str + + +class QswEntityDescription(EntityDescription, QswEntityDescriptionMixin): + """Class to describe a QSW entity.""" + + attributes: dict[str, list[str]] | None = None + + +class QswSensorEntity(QswEntity): + """Base class for QSW sensor entities.""" + + entity_description: QswEntityDescription + + @callback + def _handle_coordinator_update(self) -> None: + """Update attributes when the coordinator updates.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Update attributes.""" + if self.entity_description.attributes: + self._attr_extra_state_attributes = { + key: self.get_device_value(val[0], val[1]) + for key, val in self.entity_description.attributes.items() + } diff --git a/homeassistant/components/qnap_qsw/sensor.py b/homeassistant/components/qnap_qsw/sensor.py index 8453232ce6f..0de8ec4a39e 100644 --- a/homeassistant/components/qnap_qsw/sensor.py +++ b/homeassistant/components/qnap_qsw/sensor.py @@ -7,8 +7,6 @@ from typing import Final from aioqsw.const import ( QSD_FAN1_SPEED, QSD_FAN2_SPEED, - QSD_PRODUCT, - QSD_SYSTEM_BOARD, QSD_SYSTEM_SENSOR, QSD_SYSTEM_TIME, QSD_TEMP, @@ -28,17 +26,16 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import QswEntity from .const import ATTR_MAX, DOMAIN, RPM from .coordinator import QswUpdateCoordinator +from .entity import QswEntityDescription, QswSensorEntity @dataclass -class QswSensorEntityDescription(SensorEntityDescription): +class QswSensorEntityDescription(SensorEntityDescription, QswEntityDescription): """A class that describes QNAP QSW sensor entities.""" attributes: dict[str, list[str]] | None = None - subkey: str = "" SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( @@ -96,7 +93,7 @@ async def async_setup_entry( ) -class QswSensor(QswEntity, SensorEntity): +class QswSensor(QswSensorEntity, SensorEntity): """Define a QNAP QSW sensor.""" entity_description: QswSensorEntityDescription @@ -109,30 +106,17 @@ class QswSensor(QswEntity, SensorEntity): ) -> None: """Initialize.""" super().__init__(coordinator, entry) - self._attr_name = ( - f"{self.get_device_value(QSD_SYSTEM_BOARD, QSD_PRODUCT)} {description.name}" - ) + self._attr_name = f"{self.product} {description.name}" self._attr_unique_id = ( f"{entry.unique_id}_{description.key}_{description.subkey}" ) self.entity_description = description self._async_update_attrs() - @callback - def _handle_coordinator_update(self) -> None: - """Update attributes when the coordinator updates.""" - self._async_update_attrs() - super()._handle_coordinator_update() - @callback def _async_update_attrs(self) -> None: """Update sensor attributes.""" self._attr_native_value = self.get_device_value( self.entity_description.key, self.entity_description.subkey ) - - if self.entity_description.attributes: - self._attr_extra_state_attributes = { - key: self.get_device_value(val[0], val[1]) - for key, val in self.entity_description.attributes.items() - } + super()._async_update_attrs() diff --git a/tests/components/qnap_qsw/test_binary_sensor.py b/tests/components/qnap_qsw/test_binary_sensor.py new file mode 100644 index 00000000000..a36a34f02be --- /dev/null +++ b/tests/components/qnap_qsw/test_binary_sensor.py @@ -0,0 +1,17 @@ +"""The binary sensor tests for the QNAP QSW platform.""" + +from homeassistant.components.qnap_qsw.const import ATTR_MESSAGE +from homeassistant.const import STATE_OFF +from homeassistant.core import HomeAssistant + +from .util import async_init_integration + + +async def test_qnap_qsw_create_binary_sensors(hass: HomeAssistant) -> None: + """Test creation of binary sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("binary_sensor.qsw_m408_4c_anomaly") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_MESSAGE) is None From 539ce7ff0e9d9bc59cd8f028f245c09f802c89cb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 9 May 2022 06:05:31 -0700 Subject: [PATCH 0340/3516] Allow mobile app to disable entities by default (#71562) --- .../components/mobile_app/binary_sensor.py | 12 +----- homeassistant/components/mobile_app/const.py | 1 + homeassistant/components/mobile_app/entity.py | 43 ++++++------------- homeassistant/components/mobile_app/sensor.py | 17 +------- .../components/mobile_app/webhook.py | 27 ++++++++++-- tests/components/mobile_app/test_sensor.py | 33 ++++++++++++++ 6 files changed, 74 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index 4d40e42a47e..fd8545b1f98 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -3,14 +3,13 @@ from typing import Any from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, CONF_WEBHOOK_ID, STATE_ON +from homeassistant.const import CONF_WEBHOOK_ID, STATE_ON from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( - ATTR_DEVICE_NAME, ATTR_SENSOR_ATTRIBUTES, ATTR_SENSOR_DEVICE_CLASS, ATTR_SENSOR_ENTITY_CATEGORY, @@ -22,7 +21,7 @@ from .const import ( ATTR_SENSOR_UNIQUE_ID, DOMAIN, ) -from .entity import MobileAppEntity, unique_id +from .entity import MobileAppEntity async def async_setup_entry( @@ -59,13 +58,6 @@ async def async_setup_entry( if data[CONF_WEBHOOK_ID] != webhook_id: return - data[CONF_UNIQUE_ID] = unique_id( - data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID] - ) - data[ - CONF_NAME - ] = f"{config_entry.data[ATTR_DEVICE_NAME]} {data[ATTR_SENSOR_NAME]}" - async_add_entities([MobileAppBinarySensor(data, config_entry)]) async_dispatcher_connect( diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index ba81a0484cf..e6a26430f11 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -67,6 +67,7 @@ ERR_INVALID_FORMAT = "invalid_format" ATTR_SENSOR_ATTRIBUTES = "attributes" ATTR_SENSOR_DEVICE_CLASS = "device_class" +ATTR_SENSOR_DEFAULT_DISABLED = "default_disabled" ATTR_SENSOR_ENTITY_CATEGORY = "entity_category" ATTR_SENSOR_ICON = "icon" ATTR_SENSOR_NAME = "name" diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index 0cb6cfc6fcc..b38774d56d6 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -2,46 +2,35 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ICON, - CONF_NAME, - CONF_UNIQUE_ID, - CONF_WEBHOOK_ID, - STATE_UNAVAILABLE, -) +from homeassistant.const import ATTR_ICON, CONF_NAME, CONF_UNIQUE_ID, STATE_UNAVAILABLE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity from .const import ( ATTR_SENSOR_ATTRIBUTES, + ATTR_SENSOR_DEFAULT_DISABLED, ATTR_SENSOR_DEVICE_CLASS, ATTR_SENSOR_ENTITY_CATEGORY, ATTR_SENSOR_ICON, ATTR_SENSOR_STATE, - ATTR_SENSOR_TYPE, - ATTR_SENSOR_UNIQUE_ID, SIGNAL_SENSOR_UPDATE, ) from .helpers import device_info -def unique_id(webhook_id, sensor_unique_id): - """Return a unique sensor ID.""" - return f"{webhook_id}_{sensor_unique_id}" - - class MobileAppEntity(RestoreEntity): """Representation of an mobile app entity.""" + _attr_should_poll = False + def __init__(self, config: dict, entry: ConfigEntry) -> None: """Initialize the entity.""" self._config = config self._entry = entry self._registration = entry.data - self._unique_id = config[CONF_UNIQUE_ID] - self._entity_type = config[ATTR_SENSOR_TYPE] - self._name = config[CONF_NAME] + self._attr_unique_id = config[CONF_UNIQUE_ID] + self._name = self._config[CONF_NAME] async def async_added_to_hass(self): """Register callbacks.""" @@ -67,16 +56,16 @@ class MobileAppEntity(RestoreEntity): if ATTR_ICON in last_state.attributes: self._config[ATTR_SENSOR_ICON] = last_state.attributes[ATTR_ICON] - @property - def should_poll(self) -> bool: - """Declare that this entity pushes its state to HA.""" - return False - @property def name(self): """Return the name of the mobile app sensor.""" return self._name + @property + def entity_registry_enabled_default(self) -> bool: + """Return if entity should be enabled by default.""" + return not self._config.get(ATTR_SENSOR_DEFAULT_DISABLED) + @property def device_class(self): """Return the device class.""" @@ -97,11 +86,6 @@ class MobileAppEntity(RestoreEntity): """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.""" - return self._unique_id - @property def device_info(self): """Return device registry information for this entity.""" @@ -113,10 +97,9 @@ class MobileAppEntity(RestoreEntity): return self._config.get(ATTR_SENSOR_STATE) != STATE_UNAVAILABLE @callback - def _handle_update(self, data): + def _handle_update(self, incoming_id, data): """Handle async event updates.""" - incoming_id = unique_id(data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID]) - if incoming_id != self._unique_id: + if incoming_id != self._attr_unique_id: return self._config = {**self._config, **data} diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index 45bc4acd6a2..d7cfc9545f6 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -5,12 +5,7 @@ from typing import Any from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_NAME, - CONF_UNIQUE_ID, - CONF_WEBHOOK_ID, - STATE_UNKNOWN, -) +from homeassistant.const import CONF_WEBHOOK_ID, STATE_UNKNOWN from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -18,7 +13,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util from .const import ( - ATTR_DEVICE_NAME, ATTR_SENSOR_ATTRIBUTES, ATTR_SENSOR_DEVICE_CLASS, ATTR_SENSOR_ENTITY_CATEGORY, @@ -32,7 +26,7 @@ from .const import ( ATTR_SENSOR_UOM, DOMAIN, ) -from .entity import MobileAppEntity, unique_id +from .entity import MobileAppEntity async def async_setup_entry( @@ -70,13 +64,6 @@ async def async_setup_entry( if data[CONF_WEBHOOK_ID] != webhook_id: return - data[CONF_UNIQUE_ID] = unique_id( - data[CONF_WEBHOOK_ID], data[ATTR_SENSOR_UNIQUE_ID] - ) - data[ - CONF_NAME - ] = f"{config_entry.data[ATTR_DEVICE_NAME]} {data[ATTR_SENSOR_NAME]}" - async_add_entities([MobileAppSensor(data, config_entry)]) async_dispatcher_connect( diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index f0c7c628976..bc67076b005 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -34,6 +34,8 @@ from homeassistant.const import ( ATTR_SERVICE, ATTR_SERVICE_DATA, ATTR_SUPPORTED_FEATURES, + CONF_NAME, + CONF_UNIQUE_ID, CONF_WEBHOOK_ID, ) from homeassistant.core import EventOrigin, HomeAssistant @@ -62,6 +64,7 @@ from .const import ( ATTR_NO_LEGACY_ENCRYPTION, ATTR_OS_VERSION, ATTR_SENSOR_ATTRIBUTES, + ATTR_SENSOR_DEFAULT_DISABLED, ATTR_SENSOR_DEVICE_CLASS, ATTR_SENSOR_ENTITY_CATEGORY, ATTR_SENSOR_ICON, @@ -431,6 +434,11 @@ def _validate_state_class_sensor(value: dict): return value +def _gen_unique_id(webhook_id, sensor_unique_id): + """Return a unique sensor ID.""" + return f"{webhook_id}_{sensor_unique_id}" + + @WEBHOOK_COMMANDS.register("register_sensor") @validate_schema( vol.All( @@ -449,6 +457,7 @@ def _validate_state_class_sensor(value: dict): 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), + vol.Optional(ATTR_SENSOR_DEFAULT_DISABLED): bool, }, _validate_state_class_sensor, ) @@ -459,7 +468,7 @@ async def webhook_register_sensor(hass, config_entry, data): unique_id = data[ATTR_SENSOR_UNIQUE_ID] device_name = config_entry.data[ATTR_DEVICE_NAME] - unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}" + unique_store_key = _gen_unique_id(config_entry.data[CONF_WEBHOOK_ID], unique_id) entity_registry = er.async_get(hass) existing_sensor = entity_registry.async_get_entity_id( entity_type, DOMAIN, unique_store_key @@ -493,8 +502,13 @@ async def webhook_register_sensor(hass, config_entry, data): if changes: entity_registry.async_update_entity(existing_sensor, **changes) - async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, data) + async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, unique_store_key, data) else: + data[CONF_UNIQUE_ID] = unique_store_key + data[ + CONF_NAME + ] = f"{config_entry.data[ATTR_DEVICE_NAME]} {data[ATTR_SENSOR_NAME]}" + register_signal = f"{DOMAIN}_{data[ATTR_SENSOR_TYPE]}_register" async_dispatcher_send(hass, register_signal, data) @@ -543,7 +557,7 @@ async def webhook_update_sensor_states(hass, config_entry, data): unique_id = sensor[ATTR_SENSOR_UNIQUE_ID] - unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}" + unique_store_key = _gen_unique_id(config_entry.data[CONF_WEBHOOK_ID], unique_id) entity_registry = er.async_get(hass) if not entity_registry.async_get_entity_id( @@ -578,7 +592,12 @@ async def webhook_update_sensor_states(hass, config_entry, data): continue sensor[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID] - async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, sensor) + async_dispatcher_send( + hass, + SIGNAL_SENSOR_UPDATE, + unique_store_key, + sensor, + ) resp[unique_id] = {"success": True} diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index 7eb99df8d8f..301c49381f7 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -340,3 +340,36 @@ async def test_sensor_datetime( assert entity.attributes["device_class"] == device_class assert entity.domain == "sensor" assert entity.state == state_value + + +async def test_default_disabling_entity(hass, create_registrations, webhook_client): + """Test that sensors can be disabled by default upon registration.""" + 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", + "type": "sensor", + "unique_id": "battery_state", + "default_disabled": True, + }, + }, + ) + + assert reg_resp.status == HTTPStatus.CREATED + + json = await reg_resp.json() + assert json == {"success": True} + await hass.async_block_till_done() + + entity = hass.states.get("sensor.test_1_battery_state") + assert entity is None + + assert ( + er.async_get(hass).async_get("sensor.test_1_battery_state").disabled_by + == er.RegistryEntryDisabler.INTEGRATION + ) From 88c2c5c36cfe1ac89a5b26836d0bdd34a0d72021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 9 May 2022 16:07:11 +0200 Subject: [PATCH 0341/3516] Add Binary Sensors for Airzone Systems (#69736) --- homeassistant/components/airzone/__init__.py | 62 +---------- .../components/airzone/binary_sensor.py | 47 +++++++- homeassistant/components/airzone/climate.py | 2 +- homeassistant/components/airzone/entity.py | 105 ++++++++++++++++++ homeassistant/components/airzone/sensor.py | 2 +- .../components/airzone/test_binary_sensor.py | 5 + tests/components/airzone/util.py | 18 ++- 7 files changed, 176 insertions(+), 65 deletions(-) create mode 100644 homeassistant/components/airzone/entity.py diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index 2882ea18143..cf57a28a5e1 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -4,17 +4,7 @@ from __future__ import annotations import logging from typing import Any -from aioairzone.const import ( - AZD_ID, - AZD_MAC, - AZD_NAME, - AZD_SYSTEM, - AZD_THERMOSTAT_FW, - AZD_THERMOSTAT_MODEL, - AZD_WEBSERVER, - AZD_ZONES, - DEFAULT_SYSTEM_ID, -) +from aioairzone.const import AZD_MAC, AZD_WEBSERVER, DEFAULT_SYSTEM_ID from aioairzone.localapi import AirzoneLocalApi, ConnectionOptions from homeassistant.config_entries import ConfigEntry @@ -25,10 +15,8 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, MANUFACTURER +from .const import DOMAIN from .coordinator import AirzoneUpdateCoordinator PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR] @@ -36,52 +24,6 @@ PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform. _LOGGER = logging.getLogger(__name__) -class AirzoneEntity(CoordinatorEntity[AirzoneUpdateCoordinator]): - """Define an Airzone entity.""" - - def get_airzone_value(self, key) -> Any: - """Return Airzone entity value by key.""" - raise NotImplementedError() - - -class AirzoneZoneEntity(AirzoneEntity): - """Define an Airzone Zone entity.""" - - def __init__( - self, - coordinator: AirzoneUpdateCoordinator, - entry: ConfigEntry, - system_zone_id: str, - zone_data: dict[str, Any], - ) -> None: - """Initialize.""" - super().__init__(coordinator) - - self.system_id = zone_data[AZD_SYSTEM] - self.system_zone_id = system_zone_id - self.zone_id = zone_data[AZD_ID] - - self._attr_device_info: DeviceInfo = { - "identifiers": {(DOMAIN, f"{entry.entry_id}_{system_zone_id}")}, - "manufacturer": MANUFACTURER, - "model": self.get_airzone_value(AZD_THERMOSTAT_MODEL), - "name": f"Airzone [{system_zone_id}] {zone_data[AZD_NAME]}", - "sw_version": self.get_airzone_value(AZD_THERMOSTAT_FW), - } - self._attr_unique_id = ( - entry.entry_id if entry.unique_id is None else entry.unique_id - ) - - def get_airzone_value(self, key) -> Any: - """Return zone value by key.""" - value = None - if self.system_zone_id in self.coordinator.data[AZD_ZONES]: - zone = self.coordinator.data[AZD_ZONES][self.system_zone_id] - if key in zone: - value = zone[key] - return value - - async def _async_migrate_unique_ids( hass: HomeAssistant, entry: ConfigEntry, diff --git a/homeassistant/components/airzone/binary_sensor.py b/homeassistant/components/airzone/binary_sensor.py index dd0e4a5b768..4dbfc764664 100644 --- a/homeassistant/components/airzone/binary_sensor.py +++ b/homeassistant/components/airzone/binary_sensor.py @@ -12,6 +12,7 @@ from aioairzone.const import ( AZD_FLOOR_DEMAND, AZD_NAME, AZD_PROBLEMS, + AZD_SYSTEMS, AZD_ZONES, ) @@ -25,9 +26,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AirzoneEntity, AirzoneZoneEntity from .const import DOMAIN from .coordinator import AirzoneUpdateCoordinator +from .entity import AirzoneEntity, AirzoneSystemEntity, AirzoneZoneEntity @dataclass @@ -37,6 +38,18 @@ class AirzoneBinarySensorEntityDescription(BinarySensorEntityDescription): attributes: dict[str, str] | None = None +SYSTEM_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = ( + AirzoneBinarySensorEntityDescription( + attributes={ + "errors": AZD_ERRORS, + }, + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + key=AZD_PROBLEMS, + name="Problem", + ), +) + ZONE_BINARY_SENSOR_TYPES: Final[tuple[AirzoneBinarySensorEntityDescription, ...]] = ( AirzoneBinarySensorEntityDescription( device_class=BinarySensorDeviceClass.RUNNING, @@ -72,6 +85,20 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][entry.entry_id] binary_sensors: list[AirzoneBinarySensor] = [] + + for system_id, system_data in coordinator.data[AZD_SYSTEMS].items(): + for description in SYSTEM_BINARY_SENSOR_TYPES: + if description.key in system_data: + binary_sensors.append( + AirzoneSystemBinarySensor( + coordinator, + description, + entry, + system_id, + system_data, + ) + ) + for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items(): for description in ZONE_BINARY_SENSOR_TYPES: if description.key in zone_data: @@ -109,6 +136,24 @@ class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity): return self.get_airzone_value(self.entity_description.key) +class AirzoneSystemBinarySensor(AirzoneSystemEntity, AirzoneBinarySensor): + """Define an Airzone System binary sensor.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + description: AirzoneBinarySensorEntityDescription, + entry: ConfigEntry, + system_id: str, + system_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator, entry, system_data) + self._attr_name = f"System {system_id} {description.name}" + self._attr_unique_id = f"{self._attr_unique_id}_{system_id}_{description.key}" + self.entity_description = description + + class AirzoneZoneBinarySensor(AirzoneZoneEntity, AirzoneBinarySensor): """Define an Airzone Zone binary sensor.""" diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index 9cff1ff393d..21bb08f08c9 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -39,9 +39,9 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AirzoneZoneEntity from .const import API_TEMPERATURE_STEP, DOMAIN, TEMP_UNIT_LIB_TO_HASS from .coordinator import AirzoneUpdateCoordinator +from .entity import AirzoneZoneEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/airzone/entity.py b/homeassistant/components/airzone/entity.py new file mode 100644 index 00000000000..bf1512f3801 --- /dev/null +++ b/homeassistant/components/airzone/entity.py @@ -0,0 +1,105 @@ +"""Entity classes for the Airzone integration.""" +from __future__ import annotations + +from typing import Any + +from aioairzone.const import ( + AZD_FIRMWARE, + AZD_FULL_NAME, + AZD_ID, + AZD_MODEL, + AZD_NAME, + AZD_SYSTEM, + AZD_SYSTEMS, + AZD_THERMOSTAT_FW, + AZD_THERMOSTAT_MODEL, + AZD_ZONES, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MANUFACTURER +from .coordinator import AirzoneUpdateCoordinator + + +class AirzoneEntity(CoordinatorEntity[AirzoneUpdateCoordinator]): + """Define an Airzone entity.""" + + def get_airzone_value(self, key) -> Any: + """Return Airzone entity value by key.""" + raise NotImplementedError() + + +class AirzoneSystemEntity(AirzoneEntity): + """Define an Airzone System entity.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + entry: ConfigEntry, + system_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self.system_id = system_data[AZD_ID] + + self._attr_device_info: DeviceInfo = { + "identifiers": {(DOMAIN, f"{entry.entry_id}_{self.system_id}")}, + "manufacturer": MANUFACTURER, + "model": self.get_airzone_value(AZD_MODEL), + "name": self.get_airzone_value(AZD_FULL_NAME), + "sw_version": self.get_airzone_value(AZD_FIRMWARE), + "via_device": (DOMAIN, f"{entry.entry_id}_ws"), + } + self._attr_unique_id = ( + entry.entry_id if entry.unique_id is None else entry.unique_id + ) + + def get_airzone_value(self, key) -> Any: + """Return system value by key.""" + value = None + if system := self.coordinator.data[AZD_SYSTEMS].get(self.system_id): + if key in system: + value = system[key] + return value + + +class AirzoneZoneEntity(AirzoneEntity): + """Define an Airzone Zone entity.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + entry: ConfigEntry, + system_zone_id: str, + zone_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self.system_id = zone_data[AZD_SYSTEM] + self.system_zone_id = system_zone_id + self.zone_id = zone_data[AZD_ID] + + self._attr_device_info: DeviceInfo = { + "identifiers": {(DOMAIN, f"{entry.entry_id}_{system_zone_id}")}, + "manufacturer": MANUFACTURER, + "model": self.get_airzone_value(AZD_THERMOSTAT_MODEL), + "name": f"Airzone [{system_zone_id}] {zone_data[AZD_NAME]}", + "sw_version": self.get_airzone_value(AZD_THERMOSTAT_FW), + "via_device": (DOMAIN, f"{entry.entry_id}_{self.system_id}"), + } + self._attr_unique_id = ( + entry.entry_id if entry.unique_id is None else entry.unique_id + ) + + def get_airzone_value(self, key) -> Any: + """Return zone value by key.""" + value = None + if zone := self.coordinator.data[AZD_ZONES].get(self.system_zone_id): + if key in zone: + value = zone[key] + return value diff --git a/homeassistant/components/airzone/sensor.py b/homeassistant/components/airzone/sensor.py index f41add5053a..81dcea5098e 100644 --- a/homeassistant/components/airzone/sensor.py +++ b/homeassistant/components/airzone/sensor.py @@ -16,9 +16,9 @@ from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AirzoneEntity, AirzoneZoneEntity from .const import DOMAIN, TEMP_UNIT_LIB_TO_HASS from .coordinator import AirzoneUpdateCoordinator +from .entity import AirzoneEntity, AirzoneZoneEntity ZONE_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( SensorEntityDescription( diff --git a/tests/components/airzone/test_binary_sensor.py b/tests/components/airzone/test_binary_sensor.py index d6a3fdcb115..138801d1dc0 100644 --- a/tests/components/airzone/test_binary_sensor.py +++ b/tests/components/airzone/test_binary_sensor.py @@ -13,6 +13,11 @@ async def test_airzone_create_binary_sensors(hass: HomeAssistant) -> None: await async_init_integration(hass) + # Systems + state = hass.states.get("binary_sensor.system_1_problem") + assert state.state == STATE_OFF + + # Zones state = hass.states.get("binary_sensor.despacho_air_demand") assert state.state == STATE_OFF diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index 52f15dd1476..ede4bcc3b53 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -19,9 +19,12 @@ from aioairzone.const import ( API_MODES, API_NAME, API_ON, + API_POWER, API_ROOM_TEMP, API_SET_POINT, + API_SYSTEM_FIRMWARE, API_SYSTEM_ID, + API_SYSTEM_TYPE, API_SYSTEMS, API_THERMOS_FIRMWARE, API_THERMOS_RADIO, @@ -31,7 +34,7 @@ from aioairzone.const import ( API_WIFI_RSSI, API_ZONE_ID, ) -from aioairzone.exceptions import InvalidMethod, SystemOutOfRange +from aioairzone.exceptions import InvalidMethod from homeassistant.components.airzone import DOMAIN from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT @@ -178,6 +181,17 @@ HVAC_MOCK = { ] } +HVAC_SYSTEMS_MOCK = { + API_SYSTEMS: [ + { + API_SYSTEM_ID: 1, + API_POWER: 0, + API_SYSTEM_FIRMWARE: "3.31", + API_SYSTEM_TYPE: 1, + } + ] +} + HVAC_WEBSERVER_MOCK = { API_MAC: "11:22:33:44:55:66", API_WIFI_CHANNEL: 6, @@ -202,7 +216,7 @@ async def async_init_integration( return_value=HVAC_MOCK, ), patch( "homeassistant.components.airzone.AirzoneLocalApi.get_hvac_systems", - side_effect=SystemOutOfRange, + return_value=HVAC_SYSTEMS_MOCK, ), patch( "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", side_effect=InvalidMethod, From e5870c65ee928b37fbc07bf7ea1ab4aaa527678e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 May 2022 18:41:25 +0200 Subject: [PATCH 0342/3516] Add missing cast test fixture (#71595) --- tests/components/cast/fixtures/rthkaudio2.m3u8 | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 tests/components/cast/fixtures/rthkaudio2.m3u8 diff --git a/tests/components/cast/fixtures/rthkaudio2.m3u8 b/tests/components/cast/fixtures/rthkaudio2.m3u8 new file mode 100644 index 00000000000..388c115635f --- /dev/null +++ b/tests/components/cast/fixtures/rthkaudio2.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=54000,CODECS="mp4a.40.2" +https://rthkaudio2-lh.akamaihd.net/i/radio2_1@355865/index_56_a-p.m3u8?sd=10&rebase=on +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=54000,CODECS="mp4a.40.2" +https://rthkaudio2-lh.akamaihd.net/i/radio2_1@355865/index_56_a-b.m3u8?sd=10&rebase=on From 75ce66e8bdbc4b968c9609b0d4f31d863b0ff932 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 9 May 2022 18:42:18 +0200 Subject: [PATCH 0343/3516] Migrate wemo light to ColorMode (#70857) --- homeassistant/components/wemo/light.py | 60 +++++++++++++--------- tests/components/wemo/test_light_bridge.py | 11 +++- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 34ec7cab08d..87ebbd9e2c3 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -11,9 +11,7 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + ColorMode, LightEntity, LightEntityFeature, ) @@ -29,13 +27,6 @@ from .const import DOMAIN as WEMO_DOMAIN from .entity import WemoBinaryStateEntity, WemoEntity from .wemo_device import DeviceCoordinator -SUPPORT_WEMO = ( - SUPPORT_BRIGHTNESS - | SUPPORT_COLOR_TEMP - | SUPPORT_COLOR - | LightEntityFeature.TRANSITION -) - # The WEMO_ constants below come from pywemo itself WEMO_OFF = 0 @@ -94,6 +85,8 @@ def async_setup_bridge( class WemoLight(WemoEntity, LightEntity): """Representation of a WeMo light.""" + _attr_supported_features = LightEntityFeature.TRANSITION + def __init__(self, coordinator: DeviceCoordinator, light: bridge.Light) -> None: """Initialize the WeMo light.""" super().__init__(coordinator) @@ -133,27 +126,48 @@ class WemoLight(WemoEntity, LightEntity): return cast(int, self.light.state.get("level", 255)) @property - def hs_color(self) -> tuple[float, float] | None: - """Return the hs color values of this light.""" - if xy_color := self.light.state.get("color_xy"): - return color_util.color_xy_to_hs(*xy_color) - return None + def xy_color(self) -> tuple[float, float] | None: + """Return the xy color value [float, float].""" + return self.light.state.get("color_xy") # type:ignore[no-any-return] @property def color_temp(self) -> int | None: """Return the color temperature of this light in mireds.""" return cast(Optional[int], self.light.state.get("temperature_mireds")) + @property + def color_mode(self) -> ColorMode: + """Return the color mode of the light.""" + if ( + "colorcontrol" in self.light.capabilities + and self.light.state.get("color_xy") is not None + ): + return ColorMode.XY + if "colortemperature" in self.light.capabilities: + return ColorMode.COLOR_TEMP + if "levelcontrol" in self.light.capabilities: + return ColorMode.BRIGHTNESS + return ColorMode.ONOFF + + @property + def supported_color_modes(self) -> set[ColorMode]: + """Flag supported color modes.""" + modes: set[ColorMode] = set() + if "colorcontrol" in self.light.capabilities: + modes.add(ColorMode.XY) + if "colortemperature" in self.light.capabilities: + modes.add(ColorMode.COLOR_TEMP) + if "levelcontrol" in self.light.capabilities and not modes: + modes.add(ColorMode.BRIGHTNESS) + if not modes: + modes.add(ColorMode.ONOFF) + return modes + @property def is_on(self) -> bool: """Return true if device is on.""" return cast(int, self.light.state.get("onoff")) != WEMO_OFF - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORT_WEMO - def turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" xy_color = None @@ -194,10 +208,8 @@ class WemoLight(WemoEntity, LightEntity): class WemoDimmer(WemoBinaryStateEntity, LightEntity): """Representation of a WeMo dimmer.""" - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORT_BRIGHTNESS + _attr_supported_color_modes = {ColorMode.BRIGHTNESS} + _attr_color_mode = ColorMode.BRIGHTNESS @property def brightness(self) -> int: diff --git a/tests/components/wemo/test_light_bridge.py b/tests/components/wemo/test_light_bridge.py index 3184335b173..afcdb37cba9 100644 --- a/tests/components/wemo/test_light_bridge.py +++ b/tests/components/wemo/test_light_bridge.py @@ -8,7 +8,13 @@ from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) -from homeassistant.components.light import ATTR_COLOR_TEMP, DOMAIN as LIGHT_DOMAIN +from homeassistant.components.light import ( + ATTR_COLOR_MODE, + ATTR_COLOR_TEMP, + ATTR_SUPPORTED_COLOR_MODES, + DOMAIN as LIGHT_DOMAIN, + ColorMode, +) from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component @@ -32,6 +38,7 @@ def pywemo_bridge_light_fixture(pywemo_device): light.name = pywemo_device.name light.bridge = pywemo_device light.state = {"onoff": 0, "available": True} + light.capabilities = ["onoff", "levelcontrol", "colortemperature"] pywemo_device.Lights = {pywemo_device.serialnumber: light} return light @@ -102,6 +109,8 @@ async def test_light_update_entity( ) state = hass.states.get(wemo_entity.entity_id) assert state.attributes.get(ATTR_COLOR_TEMP) == 432 + assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [ColorMode.COLOR_TEMP] + assert state.attributes.get(ATTR_COLOR_MODE) == ColorMode.COLOR_TEMP assert state.state == STATE_ON # Off state. From b9b83c05e9d53963891bb50ec9ed14c2bb07f4b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 9 May 2022 19:56:59 +0200 Subject: [PATCH 0344/3516] Add Airzone to strict typing (#71604) --- .strict-typing | 1 + .../components/airzone/binary_sensor.py | 28 ++++++++----------- homeassistant/components/airzone/climate.py | 2 +- .../components/airzone/coordinator.py | 8 ++++-- homeassistant/components/airzone/entity.py | 6 ++-- homeassistant/components/airzone/sensor.py | 12 ++++---- mypy.ini | 11 ++++++++ 7 files changed, 40 insertions(+), 28 deletions(-) diff --git a/.strict-typing b/.strict-typing index 67efdfa7953..2aa336afb9e 100644 --- a/.strict-typing +++ b/.strict-typing @@ -43,6 +43,7 @@ homeassistant.components.aftership.* homeassistant.components.air_quality.* homeassistant.components.airly.* homeassistant.components.airvisual.* +homeassistant.components.airzone.* homeassistant.components.aladdin_connect.* homeassistant.components.alarm_control_panel.* homeassistant.components.amazon_polly.* diff --git a/homeassistant/components/airzone/binary_sensor.py b/homeassistant/components/airzone/binary_sensor.py index 4dbfc764664..d1885e0edf9 100644 --- a/homeassistant/components/airzone/binary_sensor.py +++ b/homeassistant/components/airzone/binary_sensor.py @@ -1,7 +1,6 @@ """Support for the Airzone sensors.""" from __future__ import annotations -from collections.abc import Mapping from dataclasses import dataclass from typing import Any, Final @@ -22,7 +21,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -120,20 +119,15 @@ class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity): entity_description: AirzoneBinarySensorEntityDescription - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return state attributes.""" - if not self.entity_description.attributes: - return None - return { - key: self.get_airzone_value(val) - for key, val in self.entity_description.attributes.items() - } - - @property - def is_on(self) -> bool | None: - """Return true if the binary sensor is on.""" - return self.get_airzone_value(self.entity_description.key) + @callback + def _async_update_attrs(self) -> None: + """Update binary sensor attributes.""" + self._attr_is_on = self.get_airzone_value(self.entity_description.key) + if self.entity_description.attributes: + self._attr_extra_state_attributes = { + key: self.get_airzone_value(val) + for key, val in self.entity_description.attributes.items() + } class AirzoneSystemBinarySensor(AirzoneSystemEntity, AirzoneBinarySensor): @@ -152,6 +146,7 @@ class AirzoneSystemBinarySensor(AirzoneSystemEntity, AirzoneBinarySensor): self._attr_name = f"System {system_id} {description.name}" self._attr_unique_id = f"{self._attr_unique_id}_{system_id}_{description.key}" self.entity_description = description + self._async_update_attrs() class AirzoneZoneBinarySensor(AirzoneZoneEntity, AirzoneBinarySensor): @@ -173,3 +168,4 @@ class AirzoneZoneBinarySensor(AirzoneZoneEntity, AirzoneBinarySensor): f"{self._attr_unique_id}_{system_zone_id}_{description.key}" ) self.entity_description = description + self._async_update_attrs() diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index 21bb08f08c9..e264df18e0c 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -162,7 +162,7 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity): params[API_ON] = 1 await self._async_update_hvac_params(params) - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: dict[str, Any]) -> None: """Set new target temperature.""" params = { API_SET_POINT: kwargs.get(ATTR_TEMPERATURE), diff --git a/homeassistant/components/airzone/coordinator.py b/homeassistant/components/airzone/coordinator.py index 9e1dc44bb6c..f9f7322a3eb 100644 --- a/homeassistant/components/airzone/coordinator.py +++ b/homeassistant/components/airzone/coordinator.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any, cast from aioairzone.exceptions import AirzoneError from aioairzone.localapi import AirzoneLocalApi @@ -18,7 +19,7 @@ SCAN_INTERVAL = timedelta(seconds=60) _LOGGER = logging.getLogger(__name__) -class AirzoneUpdateCoordinator(DataUpdateCoordinator): +class AirzoneUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Class to manage fetching data from the Airzone device.""" def __init__(self, hass: HomeAssistant, airzone: AirzoneLocalApi) -> None: @@ -30,13 +31,14 @@ class AirzoneUpdateCoordinator(DataUpdateCoordinator): _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL, + update_method=self._async_update, ) - async def _async_update_data(self): + async def _async_update(self) -> dict[str, Any]: """Update data via library.""" async with async_timeout.timeout(AIOAIRZONE_DEVICE_TIMEOUT_SEC): try: await self.airzone.update() except AirzoneError as error: raise UpdateFailed(error) from error - return self.airzone.data() + return cast(dict[str, Any], self.airzone.data()) diff --git a/homeassistant/components/airzone/entity.py b/homeassistant/components/airzone/entity.py index bf1512f3801..687d7873ece 100644 --- a/homeassistant/components/airzone/entity.py +++ b/homeassistant/components/airzone/entity.py @@ -27,7 +27,7 @@ from .coordinator import AirzoneUpdateCoordinator class AirzoneEntity(CoordinatorEntity[AirzoneUpdateCoordinator]): """Define an Airzone entity.""" - def get_airzone_value(self, key) -> Any: + def get_airzone_value(self, key: str) -> Any: """Return Airzone entity value by key.""" raise NotImplementedError() @@ -58,7 +58,7 @@ class AirzoneSystemEntity(AirzoneEntity): entry.entry_id if entry.unique_id is None else entry.unique_id ) - def get_airzone_value(self, key) -> Any: + def get_airzone_value(self, key: str) -> Any: """Return system value by key.""" value = None if system := self.coordinator.data[AZD_SYSTEMS].get(self.system_id): @@ -96,7 +96,7 @@ class AirzoneZoneEntity(AirzoneEntity): entry.entry_id if entry.unique_id is None else entry.unique_id ) - def get_airzone_value(self, key) -> Any: + def get_airzone_value(self, key: str) -> Any: """Return zone value by key.""" value = None if zone := self.coordinator.data[AZD_ZONES].get(self.system_zone_id): diff --git a/homeassistant/components/airzone/sensor.py b/homeassistant/components/airzone/sensor.py index 81dcea5098e..3e81f5df094 100644 --- a/homeassistant/components/airzone/sensor.py +++ b/homeassistant/components/airzone/sensor.py @@ -13,7 +13,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, TEMP_UNIT_LIB_TO_HASS @@ -64,10 +64,10 @@ async def async_setup_entry( class AirzoneSensor(AirzoneEntity, SensorEntity): """Define an Airzone sensor.""" - @property - def native_value(self): - """Return the state.""" - return self.get_airzone_value(self.entity_description.key) + @callback + def _async_update_attrs(self) -> None: + """Update sensor attributes.""" + self._attr_native_value = self.get_airzone_value(self.entity_description.key) class AirzoneZoneSensor(AirzoneZoneEntity, AirzoneSensor): @@ -94,3 +94,5 @@ class AirzoneZoneSensor(AirzoneZoneEntity, AirzoneSensor): self._attr_native_unit_of_measurement = TEMP_UNIT_LIB_TO_HASS.get( self.get_airzone_value(AZD_TEMP_UNIT) ) + + self._async_update_attrs() diff --git a/mypy.ini b/mypy.ini index 81677b8d8ff..0232e1eec15 100644 --- a/mypy.ini +++ b/mypy.ini @@ -236,6 +236,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.airzone.*] +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.aladdin_connect.*] check_untyped_defs = true disallow_incomplete_defs = true From d8e4f6d6e6965159d6fdad9f059e8c367a2e9b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 9 May 2022 19:57:27 +0200 Subject: [PATCH 0345/3516] Add QNAP QSW to strict typing (#71603) --- .strict-typing | 1 + homeassistant/components/qnap_qsw/coordinator.py | 8 +++++--- mypy.ini | 11 +++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.strict-typing b/.strict-typing index 2aa336afb9e..f7264591c83 100644 --- a/.strict-typing +++ b/.strict-typing @@ -175,6 +175,7 @@ homeassistant.components.powerwall.* homeassistant.components.proximity.* homeassistant.components.pvoutput.* homeassistant.components.pure_energie.* +homeassistant.components.qnap_qsw.* homeassistant.components.rainmachine.* homeassistant.components.rdw.* homeassistant.components.recollect_waste.* diff --git a/homeassistant/components/qnap_qsw/coordinator.py b/homeassistant/components/qnap_qsw/coordinator.py index 064953b1446..e3ca95076d0 100644 --- a/homeassistant/components/qnap_qsw/coordinator.py +++ b/homeassistant/components/qnap_qsw/coordinator.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any, cast from aioqsw.exceptions import QswError from aioqsw.localapi import QnapQswApi @@ -18,7 +19,7 @@ SCAN_INTERVAL = timedelta(seconds=60) _LOGGER = logging.getLogger(__name__) -class QswUpdateCoordinator(DataUpdateCoordinator): +class QswUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Class to manage fetching data from the QNAP QSW device.""" def __init__(self, hass: HomeAssistant, qsw: QnapQswApi) -> None: @@ -30,13 +31,14 @@ class QswUpdateCoordinator(DataUpdateCoordinator): _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL, + update_method=self._async_update, ) - async def _async_update_data(self): + async def _async_update(self) -> dict[str, Any]: """Update data via library.""" async with async_timeout.timeout(QSW_TIMEOUT_SEC): try: await self.qsw.update() except QswError as error: raise UpdateFailed(error) from error - return self.qsw.data() + return cast(dict[str, Any], self.qsw.data()) diff --git a/mypy.ini b/mypy.ini index 0232e1eec15..ab4ba77c98b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1688,6 +1688,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.qnap_qsw.*] +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.rainmachine.*] check_untyped_defs = true disallow_incomplete_defs = true From 37c4318d336cc19db333ffbba8dfd24d8119df9f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 May 2022 12:58:42 -0500 Subject: [PATCH 0346/3516] Fix merge conflict with master to dev in sabnzbd (CI fix) (#71605) --- tests/components/sabnzbd/test_init.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/sabnzbd/test_init.py b/tests/components/sabnzbd/test_init.py index 9bdef4119d0..f140c332778 100644 --- a/tests/components/sabnzbd/test_init.py +++ b/tests/components/sabnzbd/test_init.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from homeassistant.components.sabnzbd import DEFAULT_NAME, DOMAIN, SENSOR_KEYS +from homeassistant.components.sabnzbd import DEFAULT_NAME, DOMAIN, OLD_SENSOR_KEYS from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_URL from homeassistant.helpers.device_registry import DeviceEntryType @@ -54,7 +54,7 @@ async def test_unique_id_migrate(hass, device_registry, entity_registry): entity_id_sensor_key = [] - for sensor_key in SENSOR_KEYS: + for sensor_key in OLD_SENSOR_KEYS: mock_entity_id = f"{SENSOR_DOMAIN}.{DOMAIN}_{sensor_key}" entity_registry.async_get_or_create( SENSOR_DOMAIN, From 1a45e543240470db952f4f1dc9d632e6c4dc2d8e Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Mon, 9 May 2022 14:20:45 -0400 Subject: [PATCH 0347/3516] Fix Insteon issue with dimmer default on level (#71426) --- homeassistant/components/insteon/insteon_entity.py | 5 ++--- homeassistant/components/insteon/light.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index 60935f3f951..67d30ba8cad 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -153,10 +153,9 @@ class InsteonEntity(Entity): def get_device_property(self, name: str): """Get a single Insteon device property value (raw).""" - value = None if (prop := self._insteon_device.properties.get(name)) is not None: - value = prop.value if prop.new_value is None else prop.new_value - return value + return prop.value + return None def _get_label(self): """Get the device label for grouped devices.""" diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py index 05ad9794042..bf8b693b103 100644 --- a/homeassistant/components/insteon/light.py +++ b/homeassistant/components/insteon/light.py @@ -58,9 +58,9 @@ class InsteonDimmerEntity(InsteonEntity, LightEntity): """Turn light on.""" if ATTR_BRIGHTNESS in kwargs: brightness = int(kwargs[ATTR_BRIGHTNESS]) - else: + elif self._insteon_device_group.group == 1: brightness = self.get_device_property(ON_LEVEL) - if brightness is not None: + if brightness: await self._insteon_device.async_on( on_level=brightness, group=self._insteon_device_group.group ) From 0fcdca2d71578b257df2938cf350e9b07c17a176 Mon Sep 17 00:00:00 2001 From: TheHolyRoger <39387497+TheHolyRoger@users.noreply.github.com> Date: Mon, 9 May 2022 20:40:15 +0100 Subject: [PATCH 0348/3516] Add optional base64 decoding of mqtt camera image (#71223) Add unittest for b64 decoding of camera, fix linting --- homeassistant/components/mqtt/camera.py | 8 +++++-- tests/components/mqtt/test_camera.py | 29 +++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 0e387023a39..176e8e86b6b 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -1,6 +1,7 @@ """Camera that loads a picture from an MQTT topic.""" from __future__ import annotations +from base64 import b64decode import functools import voluptuous as vol @@ -16,7 +17,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription from .. import mqtt -from .const import CONF_QOS, CONF_TOPIC +from .const import CONF_ENCODING, CONF_QOS, CONF_TOPIC from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, @@ -102,7 +103,10 @@ class MqttCamera(MqttEntity, Camera): @log_messages(self.hass, self.entity_id) def message_received(msg): """Handle new MQTT messages.""" - self._last_image = msg.payload + if self._config[CONF_ENCODING] == "b64": + self._last_image = b64decode(msg.payload) + else: + self._last_image = msg.payload self._sub_state = subscription.async_prepare_subscribe_topics( self.hass, diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 07fd7dc2c14..48f73a504bf 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 base64 import b64encode from http import HTTPStatus import json from unittest.mock import patch @@ -64,6 +65,34 @@ async def test_run_camera_setup(hass, hass_client_no_auth, mqtt_mock): assert body == "beer" +async def test_run_camera_b64_encoded(hass, hass_client_no_auth, mqtt_mock): + """Test that it fetches the given encoded payload.""" + topic = "test/camera" + await async_setup_component( + hass, + "camera", + { + "camera": { + "platform": "mqtt", + "topic": topic, + "name": "Test Camera", + "encoding": "b64", + } + }, + ) + await hass.async_block_till_done() + + url = hass.states.get("camera.test_camera").attributes["entity_picture"] + + async_fire_mqtt_message(hass, topic, b64encode(b"grass")) + + client = await hass_client_no_auth() + resp = await client.get(url) + assert resp.status == HTTPStatus.OK + body = await resp.text() + assert body == "grass" + + async def test_availability_when_connection_lost(hass, mqtt_mock): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( From 287bc5e3dcfea2fc04ed30bdc8719dd580f1b4a1 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 9 May 2022 22:07:47 +0200 Subject: [PATCH 0349/3516] Add release url to Fritz!Tools update entity (#71606) * add release url to update entity * fix test --- homeassistant/components/fritz/common.py | 28 ++++++++++++++++++------ homeassistant/components/fritz/update.py | 5 +++++ tests/components/fritz/const.py | 5 ++++- tests/components/fritz/test_update.py | 12 +++++++--- 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index d7b8d916803..4a01367cc20 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -174,6 +174,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): self._current_firmware: str | None = None self._latest_firmware: str | None = None self._update_available: bool = False + self._release_url: str | None = None async def async_setup( self, options: MappingProxyType[str, Any] | None = None @@ -224,7 +225,11 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): self._model = info.get("NewModelName") self._current_firmware = info.get("NewSoftwareVersion") - self._update_available, self._latest_firmware = self._update_device_info() + ( + self._update_available, + self._latest_firmware, + self._release_url, + ) = self._update_device_info() if "Layer3Forwarding1" in self.connection.services: if connection_type := self.connection.call_action( "Layer3Forwarding1", "GetDefaultConnectionService" @@ -274,6 +279,11 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): """Return if new SW version is available.""" return self._update_available + @property + def release_url(self) -> str | None: + """Return the info URL for latest firmware.""" + return self._release_url + @property def mac(self) -> str: """Return device Mac address.""" @@ -305,12 +315,12 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): raise HomeAssistantError("Error refreshing hosts info") from ex return [] - def _update_device_info(self) -> tuple[bool, str | None]: + def _update_device_info(self) -> tuple[bool, str | None, str | None]: """Retrieve latest device information from the FRITZ!Box.""" - version = self.connection.call_action("UserInterface1", "GetInfo").get( - "NewX_AVM-DE_Version" - ) - return bool(version), version + info = self.connection.call_action("UserInterface1", "GetInfo") + version = info.get("NewX_AVM-DE_Version") + release_url = info.get("NewX_AVM-DE_InfoURL") + return bool(version), version, release_url def _get_wan_access(self, ip_address: str) -> bool | None: """Get WAN access rule for given IP address.""" @@ -361,7 +371,11 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): return _LOGGER.debug("Checking host info for FRITZ!Box device %s", self.host) - self._update_available, self._latest_firmware = self._update_device_info() + ( + self._update_available, + self._latest_firmware, + self._release_url, + ) = self._update_device_info() _LOGGER.debug("Checking devices for FRITZ!Box device %s", self.host) _default_consider_home = DEFAULT_CONSIDER_HOME.total_seconds() diff --git a/homeassistant/components/fritz/update.py b/homeassistant/components/fritz/update.py index 620b932999e..03cffc3cae6 100644 --- a/homeassistant/components/fritz/update.py +++ b/homeassistant/components/fritz/update.py @@ -55,6 +55,11 @@ class FritzBoxUpdateEntity(FritzBoxBaseEntity, UpdateEntity): return self._avm_wrapper.latest_firmware return self._avm_wrapper.current_firmware + @property + def release_url(self) -> str | None: + """URL to the full release notes of the latest version available.""" + return self._avm_wrapper.release_url + async def async_install( self, version: str | None, backup: bool, **kwargs: Any ) -> None: diff --git a/tests/components/fritz/const.py b/tests/components/fritz/const.py index 39533d07a93..f8f6f8370d7 100644 --- a/tests/components/fritz/const.py +++ b/tests/components/fritz/const.py @@ -30,8 +30,11 @@ MOCK_IPS = {"fritz.box": "192.168.178.1", "printer": "192.168.178.2"} MOCK_MODELNAME = "FRITZ!Box 7530 AX" MOCK_FIRMWARE = "256.07.29" MOCK_FIRMWARE_AVAILABLE = "256.07.50" +MOCK_FIRMWARE_RELEASE_URL = ( + "http://download.avm.de/fritzbox/fritzbox-7530-ax/deutschland/fritz.os/info_de.txt" +) MOCK_SERIAL_NUMBER = "fake_serial_number" -MOCK_FIRMWARE_INFO = [True, "1.1.1"] +MOCK_FIRMWARE_INFO = [True, "1.1.1", "some-release-url"] MOCK_MESH_SSID = "TestSSID" MOCK_MESH_MASTER_MAC = "1C:ED:6F:12:34:11" MOCK_MESH_MASTER_WIFI1_MAC = "1C:ED:6F:12:34:12" diff --git a/tests/components/fritz/test_update.py b/tests/components/fritz/test_update.py index f43b35755e8..2261ef3ef9b 100644 --- a/tests/components/fritz/test_update.py +++ b/tests/components/fritz/test_update.py @@ -10,7 +10,12 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from .const import MOCK_FIRMWARE, MOCK_FIRMWARE_AVAILABLE, MOCK_USER_DATA +from .const import ( + MOCK_FIRMWARE, + MOCK_FIRMWARE_AVAILABLE, + MOCK_FIRMWARE_RELEASE_URL, + MOCK_USER_DATA, +) from tests.common import MockConfigEntry @@ -38,7 +43,7 @@ async def test_update_available( with patch( "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", - return_value=(True, MOCK_FIRMWARE_AVAILABLE), + return_value=(True, MOCK_FIRMWARE_AVAILABLE, MOCK_FIRMWARE_RELEASE_URL), ): entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) entry.add_to_hass(hass) @@ -52,6 +57,7 @@ async def test_update_available( assert update.state == "on" assert update.attributes.get("installed_version") == MOCK_FIRMWARE assert update.attributes.get("latest_version") == MOCK_FIRMWARE_AVAILABLE + assert update.attributes.get("release_url") == MOCK_FIRMWARE_RELEASE_URL async def test_no_update_available( @@ -80,7 +86,7 @@ async def test_available_update_can_be_installed( with patch( "homeassistant.components.fritz.common.FritzBoxTools._update_device_info", - return_value=(True, MOCK_FIRMWARE_AVAILABLE), + return_value=(True, MOCK_FIRMWARE_AVAILABLE, MOCK_FIRMWARE_RELEASE_URL), ), patch( "homeassistant.components.fritz.common.FritzBoxTools.async_trigger_firmware_update", return_value=True, From bb052679df609f1f0c0382e0d3328d791b022479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Mon, 9 May 2022 22:08:22 +0200 Subject: [PATCH 0350/3516] Update adax lib (#71609) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Hjelseth Høyer --- homeassistant/components/adax/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/adax/manifest.json b/homeassistant/components/adax/manifest.json index b5c19269f09..408c099b8ac 100644 --- a/homeassistant/components/adax/manifest.json +++ b/homeassistant/components/adax/manifest.json @@ -3,7 +3,7 @@ "name": "Adax", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adax", - "requirements": ["adax==0.2.0", "Adax-local==0.1.3"], + "requirements": ["adax==0.2.0", "Adax-local==0.1.4"], "codeowners": ["@danielhiversen"], "iot_class": "local_polling", "loggers": ["adax", "adax_local"] diff --git a/requirements_all.txt b/requirements_all.txt index fc76ea2dc35..72c2d54e44c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.adax -Adax-local==0.1.3 +Adax-local==0.1.4 # homeassistant.components.homekit HAP-python==4.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf3e5a1217b..0f67c0adfa7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.adax -Adax-local==0.1.3 +Adax-local==0.1.4 # homeassistant.components.homekit HAP-python==4.4.0 From bec3c6e66ae3376affd0047d61c35ca6e50d7009 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 May 2022 22:09:43 +0200 Subject: [PATCH 0351/3516] Add 'toggle' device action translation to fan (#71590) --- homeassistant/components/fan/strings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index 403a63f99d5..fdd95a822de 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -11,6 +11,7 @@ "turned_off": "{entity_name} turned off" }, "action_type": { + "toggle": "Toggle {entity_name}", "turn_on": "Turn on {entity_name}", "turn_off": "Turn off {entity_name}" } From 347193055e2d78716a6fc8b45d3e776a7b1d7c81 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 May 2022 22:10:26 +0200 Subject: [PATCH 0352/3516] Rename logbook humanify function (#71597) Co-authored-by: J. Nick Koston --- homeassistant/components/logbook/__init__.py | 4 ++-- tests/components/logbook/common.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 5ec03b3aaa4..d3ede5303fb 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -324,7 +324,7 @@ class LogbookView(HomeAssistantView): ) -def humanify( +def _humanify( hass: HomeAssistant, rows: Generator[Row, None, None], entity_name_cache: EntityNameCache, @@ -548,7 +548,7 @@ def _get_events( query = query.order_by(Events.time_fired) return list( - humanify( + _humanify( hass, yield_rows(query), entity_name_cache, diff --git a/tests/components/logbook/common.py b/tests/components/logbook/common.py index c5b8c8239e1..896add3104e 100644 --- a/tests/components/logbook/common.py +++ b/tests/components/logbook/common.py @@ -52,7 +52,7 @@ def mock_humanify(hass_, rows): context_lookup, entity_name_cache, {}, event_cache ) return list( - logbook.humanify( + logbook._humanify( hass_, rows, entity_name_cache, event_cache, context_augmenter ), ) From 1c841590aac3cd5fbef4a04d66c3b729de717b08 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 9 May 2022 13:12:48 -0700 Subject: [PATCH 0353/3516] Reduce mobile app error to debug (#71601) --- homeassistant/components/mobile_app/webhook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index bc67076b005..cdf6fc874d9 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -563,7 +563,7 @@ async def webhook_update_sensor_states(hass, config_entry, data): if not entity_registry.async_get_entity_id( entity_type, DOMAIN, unique_store_key ): - _LOGGER.error( + _LOGGER.debug( "Refusing to update %s non-registered sensor: %s", device_name, unique_store_key, From f859cb6b2ed638f45a53063cce907ad7ece576c2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 May 2022 22:19:00 +0200 Subject: [PATCH 0354/3516] Remove unneeded bind_hass from device_automation code (#71599) --- homeassistant/components/device_automation/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 73f28c54657..d7b75d4fcd4 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -26,7 +26,7 @@ from homeassistant.helpers import ( entity_registry as er, ) from homeassistant.helpers.typing import ConfigType -from homeassistant.loader import IntegrationNotFound, bind_hass +from homeassistant.loader import IntegrationNotFound from homeassistant.requirements import async_get_integration_with_requirements from .exceptions import DeviceNotFound, InvalidDeviceAutomationConfig @@ -212,7 +212,6 @@ async def _async_get_device_automations_from_domain( ) -@bind_hass async def async_get_device_automations( hass: HomeAssistant, automation_type: DeviceAutomationType, From 5430b513589a50d48fba33ea14b13e0d45060599 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 9 May 2022 22:19:22 +0200 Subject: [PATCH 0355/3516] Update pre-commit to 2.19.0 (#71592) Co-authored-by: Shay Levy --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 0ef6063a1ca..b6fb5272abd 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,7 +12,7 @@ coverage==6.3.2 freezegun==1.2.1 mock-open==1.4.0 mypy==0.950 -pre-commit==2.17.0 +pre-commit==2.19.0 pylint==2.13.8 pipdeptree==2.2.1 pylint-strict-informational==0.1 From 2560d35f1c4de7eaa9ff9623b1060a2f9f9fa8ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 May 2022 15:21:21 -0500 Subject: [PATCH 0356/3516] Always show the start and stop event in logbook (#71600) --- homeassistant/components/logbook/__init__.py | 181 +++++++------------ tests/components/logbook/test_init.py | 14 +- 2 files changed, 71 insertions(+), 124 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index d3ede5303fb..53063d36fc0 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -5,7 +5,6 @@ from collections.abc import Callable, Generator, Iterable from contextlib import suppress from datetime import datetime as dt, timedelta from http import HTTPStatus -from itertools import groupby import json import re from typing import Any, cast @@ -338,127 +337,76 @@ def _humanify( """ external_events = hass.data.get(DOMAIN, {}) # Continuous sensors, will be excluded from the logbook - continuous_sensors = {} + continuous_sensors: dict[str, bool] = {} - # Group events in batches of GROUP_BY_MINUTES - for _, g_rows in groupby( - rows, lambda row: row.time_fired.minute // GROUP_BY_MINUTES # type: ignore[no-any-return] - ): + # Process events + for row in rows: + event_type = row.event_type + if event_type == EVENT_STATE_CHANGED: + entity_id = row.entity_id + assert entity_id is not None + # Skip continuous sensors + if ( + is_continuous := continuous_sensors.get(entity_id) + ) is None and split_entity_id(entity_id)[0] == SENSOR_DOMAIN: + is_continuous = _is_sensor_continuous(hass, entity_id) + continuous_sensors[entity_id] = is_continuous + if is_continuous: + continue - rows_batch = list(g_rows) + data = { + "when": _row_time_fired_isoformat(row), + "name": entity_name_cache.get(entity_id, row), + "state": row.state, + "entity_id": entity_id, + } + if icon := _row_attributes_extract(row, ICON_JSON_EXTRACT): + data["icon"] = icon - # Group HA start/stop events - # Maps minute of event to 1: stop, 2: stop + start - start_stop_events = {} + context_augmenter.augment(data, entity_id, row) + yield data - # Process events - for row in rows_batch: - if row.event_type == EVENT_STATE_CHANGED: - entity_id = row.entity_id - if ( - entity_id in continuous_sensors - or split_entity_id(entity_id)[0] != SENSOR_DOMAIN - ): - continue - assert entity_id is not None - continuous_sensors[entity_id] = _is_sensor_continuous(hass, entity_id) + elif event_type in external_events: + domain, describe_event = external_events[event_type] + data = describe_event(event_cache.get(row)) + data["when"] = _row_time_fired_isoformat(row) + data["domain"] = domain + context_augmenter.augment(data, data.get(ATTR_ENTITY_ID), row) + yield data - elif row.event_type == EVENT_HOMEASSISTANT_STOP: - if row.time_fired.minute in start_stop_events: - continue + elif event_type == EVENT_HOMEASSISTANT_START: + yield { + "when": _row_time_fired_isoformat(row), + "name": "Home Assistant", + "message": "started", + "domain": HA_DOMAIN, + } + elif event_type == EVENT_HOMEASSISTANT_STOP: + yield { + "when": _row_time_fired_isoformat(row), + "name": "Home Assistant", + "message": "stopped", + "domain": HA_DOMAIN, + } - start_stop_events[row.time_fired.minute] = 1 + elif event_type == EVENT_LOGBOOK_ENTRY: + event = event_cache.get(row) + event_data = event.data + domain = event_data.get(ATTR_DOMAIN) + entity_id = event_data.get(ATTR_ENTITY_ID) + if domain is None and entity_id is not None: + with suppress(IndexError): + domain = split_entity_id(str(entity_id))[0] - elif row.event_type == EVENT_HOMEASSISTANT_START: - if row.time_fired.minute not in start_stop_events: - continue - - start_stop_events[row.time_fired.minute] = 2 - - # Yield entries - for row in rows_batch: - if row.event_type == EVENT_STATE_CHANGED: - entity_id = row.entity_id - assert entity_id is not None - - if continuous_sensors.get(entity_id): - # Skip continuous sensors - continue - - data = { - "when": _row_time_fired_isoformat(row), - "name": entity_name_cache.get(entity_id, row), - "state": row.state, - "entity_id": entity_id, - } - - if icon := _row_attributes_extract(row, ICON_JSON_EXTRACT): - data["icon"] = icon - - if row.context_user_id: - data["context_user_id"] = row.context_user_id - - context_augmenter.augment(data, entity_id, row) - - yield data - - elif row.event_type in external_events: - domain, describe_event = external_events[row.event_type] - data = describe_event(event_cache.get(row)) - data["when"] = _row_time_fired_isoformat(row) - data["domain"] = domain - if row.context_user_id: - data["context_user_id"] = row.context_user_id - - entity_id = data.get(ATTR_ENTITY_ID) - context_augmenter.augment(data, entity_id, row) - yield data - - elif row.event_type == EVENT_HOMEASSISTANT_START: - if start_stop_events.get(row.time_fired.minute) == 2: - continue - yield { - "when": _row_time_fired_isoformat(row), - "name": "Home Assistant", - "message": "started", - "domain": HA_DOMAIN, - } - - elif row.event_type == EVENT_HOMEASSISTANT_STOP: - if start_stop_events.get(row.time_fired.minute) == 2: - action = "restarted" - else: - action = "stopped" - - yield { - "when": _row_time_fired_isoformat(row), - "name": "Home Assistant", - "message": action, - "domain": HA_DOMAIN, - } - - elif row.event_type == EVENT_LOGBOOK_ENTRY: - event = event_cache.get(row) - event_data = event.data - domain = event_data.get(ATTR_DOMAIN) - entity_id = event_data.get(ATTR_ENTITY_ID) - if domain is None and entity_id is not None: - with suppress(IndexError): - domain = split_entity_id(str(entity_id))[0] - - data = { - "when": _row_time_fired_isoformat(row), - "name": event_data.get(ATTR_NAME), - "message": event_data.get(ATTR_MESSAGE), - "domain": domain, - "entity_id": entity_id, - } - - if row.context_user_id: - data["context_user_id"] = row.context_user_id - - context_augmenter.augment(data, entity_id, row) - yield data + data = { + "when": _row_time_fired_isoformat(row), + "name": event_data.get(ATTR_NAME), + "message": event_data.get(ATTR_MESSAGE), + "domain": domain, + "entity_id": entity_id, + } + context_augmenter.augment(data, entity_id, row) + yield data def _get_events( @@ -746,6 +694,9 @@ class ContextAugmenter: def augment(self, data: dict[str, Any], entity_id: str | None, row: Row) -> None: """Augment data from the row and cache.""" + if context_user_id := row.context_user_id: + data["context_user_id"] = context_user_id + if not (context_row := self.context_lookup.get(row.context_id)): return diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index ad5423286d9..e55fca1133b 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -210,11 +210,8 @@ async def test_filter_sensor(hass_: ha.HomeAssistant, hass_client): _assert_entry(entries[2], name="ble", entity_id=entity_id4, state="10") -def test_home_assistant_start_stop_grouped(hass_): - """Test if HA start and stop events are grouped. - - Events that are occurring in the same minute. - """ +def test_home_assistant_start_stop_not_grouped(hass_): + """Test if HA start and stop events are no longer grouped.""" entries = mock_humanify( hass_, ( @@ -223,10 +220,9 @@ def test_home_assistant_start_stop_grouped(hass_): ), ) - assert len(entries) == 1 - assert_entry( - entries[0], name="Home Assistant", message="restarted", domain=ha.DOMAIN - ) + assert len(entries) == 2 + assert_entry(entries[0], name="Home Assistant", message="stopped", domain=ha.DOMAIN) + assert_entry(entries[1], name="Home Assistant", message="started", domain=ha.DOMAIN) def test_home_assistant_start(hass_): From 222baa53dd9adb24a352e9a3974e69c19d2d1466 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 May 2022 15:22:08 -0500 Subject: [PATCH 0357/3516] Make database access in the eventloop raise an exception (#71547) --- homeassistant/components/recorder/pool.py | 15 +++++++++++- homeassistant/util/async_.py | 13 +++++++--- tests/common.py | 7 +++--- tests/components/recorder/test_init.py | 30 ++++++++++++++--------- tests/components/recorder/test_pool.py | 20 +++++++++------ tests/components/recorder/test_util.py | 6 ++++- tests/conftest.py | 7 +++--- 7 files changed, 66 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py index 027b9bfbc25..f2f952121af 100644 --- a/homeassistant/components/recorder/pool.py +++ b/homeassistant/components/recorder/pool.py @@ -8,6 +8,7 @@ from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.pool import NullPool, SingletonThreadPool, StaticPool from homeassistant.helpers.frame import report +from homeassistant.util.async_ import check_loop from .const import DB_WORKER_PREFIX @@ -19,6 +20,10 @@ DEBUG_MUTEX_POOL_TRACE = False POOL_SIZE = 5 +ADVISE_MSG = ( + "Use homeassistant.components.recorder.get_instance(hass).async_add_executor_job()" +) + class RecorderPool(SingletonThreadPool, NullPool): # type: ignore[misc] """A hybrid of NullPool and SingletonThreadPool. @@ -62,9 +67,17 @@ class RecorderPool(SingletonThreadPool, NullPool): # type: ignore[misc] def _do_get(self) -> Any: if self.recorder_or_dbworker: return super()._do_get() + check_loop( + self._do_get_db_connection_protected, + strict=True, + advise_msg=ADVISE_MSG, + ) + return self._do_get_db_connection_protected() + + def _do_get_db_connection_protected(self) -> Any: report( "accesses the database without the database executor; " - "Use homeassistant.components.recorder.get_instance(hass).async_add_executor_job() " + f"{ADVISE_MSG} " "for faster database operations", exclude_integrations={"recorder"}, error_if_core=False, diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 4e7d05f7c2e..8b96e85664d 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -94,8 +94,14 @@ def run_callback_threadsafe( return future -def check_loop(func: Callable[..., Any], strict: bool = True) -> None: - """Warn if called inside the event loop. Raise if `strict` is True.""" +def check_loop( + func: Callable[..., Any], strict: bool = True, advise_msg: str | None = None +) -> None: + """Warn if called inside the event loop. Raise if `strict` is True. + + The default advisory message is 'Use `await hass.async_add_executor_job()' + Set `advise_msg` to an alternate message if the the solution differs. + """ try: get_running_loop() in_loop = True @@ -134,6 +140,7 @@ def check_loop(func: Callable[..., Any], strict: bool = True) -> None: if found_frame is None: raise RuntimeError( f"Detected blocking call to {func.__name__} inside the event loop. " + f"{advise_msg or 'Use `await hass.async_add_executor_job()`'}; " "This is causing stability issues. Please report issue" ) @@ -160,7 +167,7 @@ def check_loop(func: Callable[..., Any], strict: bool = True) -> None: if strict: raise RuntimeError( "Blocking calls must be done in the executor or a separate thread; " - "Use `await hass.async_add_executor_job()` " + f"{advise_msg or 'Use `await hass.async_add_executor_job()`'}; " f"at {found_frame.filename[index:]}, line {found_frame.lineno}: {(found_frame.line or '?').strip()}" ) diff --git a/tests/common.py b/tests/common.py index 73625bcfe07..bd0b828737b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -905,10 +905,9 @@ def init_recorder_component(hass, add_config=None): if recorder.CONF_COMMIT_INTERVAL not in config: config[recorder.CONF_COMMIT_INTERVAL] = 0 - with patch( - "homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", - True, - ), patch("homeassistant.components.recorder.migration.migrate_schema"): + with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( + "homeassistant.components.recorder.migration.migrate_schema" + ): assert setup_component(hass, recorder.DOMAIN, {recorder.DOMAIN: config}) assert recorder.DOMAIN in hass.config.components _LOGGER.info( diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 0b2b7b2dcb8..49133dd67bd 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1319,7 +1319,9 @@ def test_entity_id_filter(hass_recorder): async def test_database_lock_and_unlock( - hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT, tmp_path + hass: HomeAssistant, + async_setup_recorder_instance: SetupRecorderInstanceT, + tmp_path, ): """Test writing events during lock getting written after unlocking.""" # Use file DB, in memory DB cannot do write locks. @@ -1330,6 +1332,10 @@ async def test_database_lock_and_unlock( await async_setup_recorder_instance(hass, config) await hass.async_block_till_done() + def _get_db_events(): + with session_scope(hass=hass) as session: + return list(session.query(Events).filter_by(event_type=event_type)) + instance: Recorder = hass.data[DATA_INSTANCE] assert await instance.lock_database() @@ -1344,21 +1350,20 @@ async def test_database_lock_and_unlock( # Recording can't be finished while lock is held with pytest.raises(asyncio.TimeoutError): await asyncio.wait_for(asyncio.shield(task), timeout=1) - - with session_scope(hass=hass) as session: - db_events = list(session.query(Events).filter_by(event_type=event_type)) + db_events = await hass.async_add_executor_job(_get_db_events) assert len(db_events) == 0 assert instance.unlock_database() await task - with session_scope(hass=hass) as session: - db_events = list(session.query(Events).filter_by(event_type=event_type)) - assert len(db_events) == 1 + db_events = await hass.async_add_executor_job(_get_db_events) + assert len(db_events) == 1 async def test_database_lock_and_overflow( - hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT, tmp_path + hass: HomeAssistant, + async_setup_recorder_instance: SetupRecorderInstanceT, + tmp_path, ): """Test writing events during lock leading to overflow the queue causes the database to unlock.""" # Use file DB, in memory DB cannot do write locks. @@ -1369,6 +1374,10 @@ async def test_database_lock_and_overflow( await async_setup_recorder_instance(hass, config) await hass.async_block_till_done() + def _get_db_events(): + with session_scope(hass=hass) as session: + return list(session.query(Events).filter_by(event_type=event_type)) + instance: Recorder = hass.data[DATA_INSTANCE] with patch.object(recorder.core, "MAX_QUEUE_BACKLOG", 1), patch.object( @@ -1384,9 +1393,8 @@ async def test_database_lock_and_overflow( # even before unlocking. await async_wait_recording_done(hass) - with session_scope(hass=hass) as session: - db_events = list(session.query(Events).filter_by(event_type=event_type)) - assert len(db_events) == 1 + db_events = await hass.async_add_executor_job(_get_db_events) + assert len(db_events) == 1 assert not instance.unlock_database() diff --git a/tests/components/recorder/test_pool.py b/tests/components/recorder/test_pool.py index ca6a88d84a7..aa47ce5eb3c 100644 --- a/tests/components/recorder/test_pool.py +++ b/tests/components/recorder/test_pool.py @@ -1,6 +1,7 @@ """Test pool.""" import threading +import pytest from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker @@ -8,6 +9,13 @@ from homeassistant.components.recorder.const import DB_WORKER_PREFIX from homeassistant.components.recorder.pool import RecorderPool +async def test_recorder_pool_called_from_event_loop(): + """Test we raise an exception when calling from the event loop.""" + engine = create_engine("sqlite://", poolclass=RecorderPool) + with pytest.raises(RuntimeError): + sessionmaker(bind=engine)().connection() + + def test_recorder_pool(caplog): """Test RecorderPool gives the same connection in the creating thread.""" @@ -28,30 +36,26 @@ def test_recorder_pool(caplog): connections.append(session.connection().connection.connection) session.close() - _get_connection_twice() - assert "accesses the database without the database executor" in caplog.text - assert connections[0] != connections[1] - caplog.clear() new_thread = threading.Thread(target=_get_connection_twice) new_thread.start() new_thread.join() assert "accesses the database without the database executor" in caplog.text - assert connections[2] != connections[3] + assert connections[0] != connections[1] caplog.clear() new_thread = threading.Thread(target=_get_connection_twice, name=DB_WORKER_PREFIX) new_thread.start() new_thread.join() assert "accesses the database without the database executor" not in caplog.text - assert connections[4] == connections[5] + assert connections[2] == connections[3] caplog.clear() new_thread = threading.Thread(target=_get_connection_twice, name="Recorder") new_thread.start() new_thread.join() assert "accesses the database without the database executor" not in caplog.text - assert connections[6] == connections[7] + assert connections[4] == connections[5] shutdown = True caplog.clear() @@ -59,4 +63,4 @@ def test_recorder_pool(caplog): new_thread.start() new_thread.join() assert "accesses the database without the database executor" not in caplog.text - assert connections[8] != connections[9] + assert connections[6] != connections[7] diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 07334fef1c3..6b1093ee038 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -597,8 +597,12 @@ def test_periodic_db_cleanups(hass_recorder): assert str(text_obj) == "PRAGMA wal_checkpoint(TRUNCATE);" +@patch("homeassistant.components.recorder.pool.check_loop") async def test_write_lock_db( - hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT, tmp_path + skip_check_loop, + hass: HomeAssistant, + async_setup_recorder_instance: SetupRecorderInstanceT, + tmp_path, ): """Test database write lock.""" from sqlalchemy.exc import OperationalError diff --git a/tests/conftest.py b/tests/conftest.py index 0155a965fe6..37bdb05faf7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -692,10 +692,9 @@ async def _async_init_recorder_component(hass, add_config=None): if recorder.CONF_COMMIT_INTERVAL not in config: config[recorder.CONF_COMMIT_INTERVAL] = 0 - with patch( - "homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", - True, - ), patch("homeassistant.components.recorder.migration.migrate_schema"): + with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( + "homeassistant.components.recorder.migration.migrate_schema" + ): assert await async_setup_component( hass, recorder.DOMAIN, {recorder.DOMAIN: config} ) From d8336a521652b7fe7c0c0a78bced558b1d35dd73 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 May 2022 15:22:27 -0500 Subject: [PATCH 0358/3516] Fix missing context_id in script logbook entries (#71602) --- homeassistant/components/script/logbook.py | 1 + tests/components/logbook/test_init.py | 62 ++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/homeassistant/components/script/logbook.py b/homeassistant/components/script/logbook.py index f75584540d3..250b7231b32 100644 --- a/homeassistant/components/script/logbook.py +++ b/homeassistant/components/script/logbook.py @@ -17,6 +17,7 @@ def async_describe_events(hass, async_describe_event): "name": data.get(ATTR_NAME), "message": "started", "entity_id": data.get(ATTR_ENTITY_ID), + "context_id": event.context_id, } async_describe_event(DOMAIN, EVENT_SCRIPT_STARTED, async_describe_logbook_event) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index e55fca1133b..4f961dcd2c1 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -1002,6 +1002,68 @@ async def test_logbook_entity_context_id(hass, recorder_mock, hass_client): assert json_dict[7]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" +async def test_logbook_context_id_automation_script_started_manually( + hass, recorder_mock, hass_client +): + """Test the logbook populates context_ids for scripts and automations started manually.""" + await async_setup_component(hass, "logbook", {}) + await async_setup_component(hass, "automation", {}) + await async_setup_component(hass, "script", {}) + + await async_recorder_block_till_done(hass) + + # An Automation + automation_entity_id_test = "automation.alarm" + automation_context = ha.Context( + id="fc5bd62de45711eaaeb351041eec8dd9", + user_id="f400facee45711eaa9308bfd3d19e474", + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation", ATTR_ENTITY_ID: automation_entity_id_test}, + context=automation_context, + ) + script_context = ha.Context( + id="ac5bd62de45711eaaeb351041eec8dd9", + user_id="b400facee45711eaa9308bfd3d19e474", + ) + hass.bus.async_fire( + EVENT_SCRIPT_STARTED, + {ATTR_NAME: "Mock script", ATTR_ENTITY_ID: "script.mock_script"}, + context=script_context, + ) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + await async_wait_recording_done(hass) + + client = await hass_client() + + # Today time 00:00:00 + start = dt_util.utcnow().date() + start_date = datetime(start.year, start.month, start.day) + + # Test today entries with filter by end_time + end_time = start + timedelta(hours=24) + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}" + ) + assert response.status == HTTPStatus.OK + json_dict = await response.json() + + assert json_dict[0]["entity_id"] == "automation.alarm" + assert "context_entity_id" not in json_dict[0] + assert json_dict[0]["context_user_id"] == "f400facee45711eaa9308bfd3d19e474" + assert json_dict[0]["context_id"] == "fc5bd62de45711eaaeb351041eec8dd9" + + assert json_dict[1]["entity_id"] == "script.mock_script" + assert "context_entity_id" not in json_dict[1] + assert json_dict[1]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + assert json_dict[1]["context_id"] == "ac5bd62de45711eaaeb351041eec8dd9" + + assert json_dict[2]["domain"] == "homeassistant" + + async def test_logbook_entity_context_parent_id(hass, hass_client, recorder_mock): """Test the logbook view links events via context parent_id.""" await async_setup_component(hass, "logbook", {}) From 64636a4310f4765c7477e3588b025b85a8d4586a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 9 May 2022 14:45:53 -0700 Subject: [PATCH 0359/3516] Add service entity context (#71558) Co-authored-by: Shay Levy --- homeassistant/helpers/service.py | 11 +++++++++++ tests/helpers/test_service.py | 16 +++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 4cd38aa9768..975b05067b2 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable, Iterable +from contextvars import ContextVar import dataclasses from functools import partial, wraps import logging @@ -63,6 +64,15 @@ _LOGGER = logging.getLogger(__name__) SERVICE_DESCRIPTION_CACHE = "service_description_cache" +_current_entity: ContextVar[str | None] = ContextVar("current_entity", default=None) + + +@callback +def async_get_current_entity() -> str | None: + """Get the current entity on which the service is called.""" + return _current_entity.get() + + class ServiceParams(TypedDict): """Type for service call parameters.""" @@ -706,6 +716,7 @@ async def _handle_entity_call( ) -> None: """Handle calling service method.""" entity.async_set_context(context) + _current_entity.set(entity.entity_id) if isinstance(func, str): result = hass.async_run_job(partial(getattr(entity, func), **data)) # type: ignore[arg-type] diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 1b8de6ca6e2..cf87377dd8f 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -19,12 +19,12 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.helpers import ( + config_validation as cv, device_registry as dev_reg, entity_registry as ent_reg, service, template, ) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import EntityCategory from homeassistant.setup import async_setup_component @@ -1205,3 +1205,17 @@ async def test_async_extract_config_entry_ids(hass): ) assert await service.async_extract_config_entry_ids(hass, call) == {"abc"} + + +async def test_current_entity_context(hass, mock_entities): + """Test we set the current entity context var.""" + + async def mock_service(entity, call): + assert entity.entity_id == service.async_get_current_entity() + + await service.entity_service_call( + hass, + [Mock(entities=mock_entities)], + mock_service, + ha.ServiceCall("test_domain", "test_service", {"entity_id": "light.kitchen"}), + ) From 3a00c95113eb86c0b58c04509a9ff32bc6688836 Mon Sep 17 00:00:00 2001 From: Andre Richter Date: Tue, 10 May 2022 00:00:31 +0200 Subject: [PATCH 0360/3516] Add device_info and entity_category to Vallox (#67353) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add device_info and entity_category to Vallox * Fix DeviceInfo * Address review comments 1 * vallox suggested changes Co-authored-by: Sebastian Lövdahl Co-authored-by: J. Nick Koston --- homeassistant/components/vallox/__init__.py | 56 ++++++++++++++++--- .../components/vallox/binary_sensor.py | 15 ++--- homeassistant/components/vallox/fan.py | 10 ++-- homeassistant/components/vallox/sensor.py | 13 ++--- tests/components/vallox/conftest.py | 26 ++++++++- tests/components/vallox/test_sensor.py | 10 ++-- 6 files changed, 93 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index 23e5cb53f97..a69542596c8 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -5,14 +5,16 @@ from dataclasses import dataclass, field from datetime import date import ipaddress import logging -from typing import Any, NamedTuple +from typing import Any, NamedTuple, cast from uuid import UUID from vallox_websocket_api import PROFILE as VALLOX_PROFILE, Vallox from vallox_websocket_api.exceptions import ValloxApiException from vallox_websocket_api.vallox import ( - get_next_filter_change_date as calculate_next_filter_change_date, - get_uuid as calculate_uuid, + get_model as _api_get_model, + get_next_filter_change_date as _api_get_next_filter_change_date, + get_sw_version as _api_get_sw_version, + get_uuid as _api_get_uuid, ) import voluptuous as vol @@ -20,8 +22,13 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, Platform from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType, StateType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import ( DEFAULT_FAN_SPEED_AWAY, @@ -114,16 +121,32 @@ class ValloxState: return value - def get_uuid(self) -> UUID | None: + @property + def model(self) -> str | None: + """Return the model, if any.""" + model = cast(str, _api_get_model(self.metric_cache)) + + if model == "Unknown": + return None + + return model + + @property + def sw_version(self) -> str: + """Return the SW version.""" + return cast(str, _api_get_sw_version(self.metric_cache)) + + @property + def uuid(self) -> UUID | None: """Return cached UUID value.""" - uuid = calculate_uuid(self.metric_cache) + uuid = _api_get_uuid(self.metric_cache) if not isinstance(uuid, UUID): raise ValueError return uuid def get_next_filter_change_date(self) -> date | None: """Return the next filter change date.""" - next_filter_change_date = calculate_next_filter_change_date(self.metric_cache) + next_filter_change_date = _api_get_next_filter_change_date(self.metric_cache) if not isinstance(next_filter_change_date, date): return None @@ -291,3 +314,22 @@ class ValloxServiceHandler: # be observed by all parties involved. if result: await self._coordinator.async_request_refresh() + + +class ValloxEntity(CoordinatorEntity[ValloxDataUpdateCoordinator]): + """Representation of a Vallox entity.""" + + def __init__(self, name: str, coordinator: ValloxDataUpdateCoordinator) -> None: + """Initialize a Vallox entity.""" + super().__init__(coordinator) + + self._device_uuid = self.coordinator.data.uuid + assert self.coordinator.config_entry is not None + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, str(self._device_uuid))}, + manufacturer=DEFAULT_NAME, + model=self.coordinator.data.model, + name=name, + sw_version=self.coordinator.data.sw_version, + configuration_url=f"http://{self.coordinator.config_entry.data[CONF_HOST]}", + ) diff --git a/homeassistant/components/vallox/binary_sensor.py b/homeassistant/components/vallox/binary_sensor.py index 348bad97158..762b63c0c1d 100644 --- a/homeassistant/components/vallox/binary_sensor.py +++ b/homeassistant/components/vallox/binary_sensor.py @@ -9,19 +9,18 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import ValloxDataUpdateCoordinator +from . import ValloxDataUpdateCoordinator, ValloxEntity from .const import DOMAIN -class ValloxBinarySensor( - CoordinatorEntity[ValloxDataUpdateCoordinator], BinarySensorEntity -): +class ValloxBinarySensor(ValloxEntity, BinarySensorEntity): """Representation of a Vallox binary sensor.""" entity_description: ValloxBinarySensorEntityDescription + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__( self, @@ -30,14 +29,12 @@ class ValloxBinarySensor( description: ValloxBinarySensorEntityDescription, ) -> None: """Initialize the Vallox binary sensor.""" - super().__init__(coordinator) + super().__init__(name, coordinator) self.entity_description = description self._attr_name = f"{name} {description.name}" - - uuid = self.coordinator.data.get_uuid() - self._attr_unique_id = f"{uuid}-{description.key}" + self._attr_unique_id = f"{self._device_uuid}-{description.key}" @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index 85b3f501c85..6872acbb5b7 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -17,9 +17,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import ValloxDataUpdateCoordinator +from . import ValloxDataUpdateCoordinator, ValloxEntity from .const import ( DOMAIN, METRIC_KEY_MODE, @@ -80,7 +79,7 @@ async def async_setup_entry( async_add_entities([device]) -class ValloxFan(CoordinatorEntity[ValloxDataUpdateCoordinator], FanEntity): +class ValloxFan(ValloxEntity, FanEntity): """Representation of the fan.""" _attr_supported_features = FanEntityFeature.PRESET_MODE @@ -92,13 +91,12 @@ class ValloxFan(CoordinatorEntity[ValloxDataUpdateCoordinator], FanEntity): coordinator: ValloxDataUpdateCoordinator, ) -> None: """Initialize the fan.""" - super().__init__(coordinator) + super().__init__(name, coordinator) self._client = client self._attr_name = name - - self._attr_unique_id = str(self.coordinator.data.get_uuid()) + self._attr_unique_id = str(self._device_uuid) @property def preset_modes(self) -> list[str]: diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 92f0bc32e76..54a010c3e3d 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -17,12 +17,12 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt -from . import ValloxDataUpdateCoordinator +from . import ValloxDataUpdateCoordinator, ValloxEntity from .const import ( DOMAIN, METRIC_KEY_MODE, @@ -32,10 +32,11 @@ from .const import ( ) -class ValloxSensor(CoordinatorEntity[ValloxDataUpdateCoordinator], SensorEntity): +class ValloxSensor(ValloxEntity, SensorEntity): """Representation of a Vallox sensor.""" entity_description: ValloxSensorEntityDescription + _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__( self, @@ -44,14 +45,12 @@ class ValloxSensor(CoordinatorEntity[ValloxDataUpdateCoordinator], SensorEntity) description: ValloxSensorEntityDescription, ) -> None: """Initialize the Vallox sensor.""" - super().__init__(coordinator) + super().__init__(name, coordinator) self.entity_description = description self._attr_name = f"{name} {description.name}" - - uuid = self.coordinator.data.get_uuid() - self._attr_unique_id = f"{uuid}-{description.key}" + self._attr_unique_id = f"{self._device_uuid}-{description.key}" @property def native_value(self) -> StateType | datetime: diff --git a/tests/components/vallox/conftest.py b/tests/components/vallox/conftest.py index e7ea6ee6d6e..ef9fd2a0e4b 100644 --- a/tests/components/vallox/conftest.py +++ b/tests/components/vallox/conftest.py @@ -50,10 +50,30 @@ def patch_profile_home(): @pytest.fixture(autouse=True) -def patch_uuid(): - """Patch the Vallox entity UUID.""" +def patch_model(): + """Patch the Vallox model response.""" with patch( - "homeassistant.components.vallox.calculate_uuid", + "homeassistant.components.vallox._api_get_model", + return_value="Vallox Testmodel", + ): + yield + + +@pytest.fixture(autouse=True) +def patch_sw_version(): + """Patch the Vallox SW version response.""" + with patch( + "homeassistant.components.vallox._api_get_sw_version", + return_value="0.1.2", + ): + yield + + +@pytest.fixture(autouse=True) +def patch_uuid(): + """Patch the Vallox UUID response.""" + with patch( + "homeassistant.components.vallox._api_get_uuid", return_value=_random_uuid(), ): yield diff --git a/tests/components/vallox/test_sensor.py b/tests/components/vallox/test_sensor.py index dea3f79fbd8..9c96f6f0a68 100644 --- a/tests/components/vallox/test_sensor.py +++ b/tests/components/vallox/test_sensor.py @@ -51,7 +51,7 @@ async def test_remaining_filter_returns_timestamp( """Test that the remaining time for filter sensor returns a timestamp.""" # Act with patch( - "homeassistant.components.vallox.calculate_next_filter_change_date", + "homeassistant.components.vallox._api_get_next_filter_change_date", return_value=dt.now().date(), ), patch_metrics(metrics={}): await hass.config_entries.async_setup(mock_entry.entry_id) @@ -68,7 +68,7 @@ async def test_remaining_time_for_filter_none_returned_from_vallox( """Test that the remaining time for filter sensor returns 'unknown' when Vallox returns None.""" # Act with patch( - "homeassistant.components.vallox.calculate_next_filter_change_date", + "homeassistant.components.vallox._api_get_next_filter_change_date", return_value=None, ), patch_metrics(metrics={}): await hass.config_entries.async_setup(mock_entry.entry_id) @@ -98,7 +98,7 @@ async def test_remaining_time_for_filter_in_the_future( # Act with patch( - "homeassistant.components.vallox.calculate_next_filter_change_date", + "homeassistant.components.vallox._api_get_next_filter_change_date", return_value=mocked_filter_end_date, ), patch_metrics(metrics={}): await hass.config_entries.async_setup(mock_entry.entry_id) @@ -122,7 +122,7 @@ async def test_remaining_time_for_filter_today( # Act with patch( - "homeassistant.components.vallox.calculate_next_filter_change_date", + "homeassistant.components.vallox._api_get_next_filter_change_date", return_value=mocked_filter_end_date, ), patch_metrics(metrics={}): await hass.config_entries.async_setup(mock_entry.entry_id) @@ -146,7 +146,7 @@ async def test_remaining_time_for_filter_in_the_past( # Act with patch( - "homeassistant.components.vallox.calculate_next_filter_change_date", + "homeassistant.components.vallox._api_get_next_filter_change_date", return_value=mocked_filter_end_date, ), patch_metrics(metrics={}): await hass.config_entries.async_setup(mock_entry.entry_id) From b731b7b69c64fff6f8ec89e2bf961f342d29a4ef Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 10 May 2022 00:03:03 +0200 Subject: [PATCH 0361/3516] Bump pydeconz to v92 (#71613) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 1ce3477db70..2a4a5ccf253 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==91"], + "requirements": ["pydeconz==92"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 72c2d54e44c..c8ee7b0f29c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1432,7 +1432,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==91 +pydeconz==92 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f67c0adfa7..930a6ff4b29 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -953,7 +953,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==91 +pydeconz==92 # homeassistant.components.dexcom pydexcom==0.2.3 From 7243f787f97bb6c6d2dece8e751721eba67b1735 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 10 May 2022 00:11:50 +0200 Subject: [PATCH 0362/3516] Bump nam backend library to version 1.2.4 (#71584) --- 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 16231ef0b88..a842af46f84 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.2.3"], + "requirements": ["nettigo-air-monitor==1.2.4"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index c8ee7b0f29c..731e7e1ee2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1068,7 +1068,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.2.3 +nettigo-air-monitor==1.2.4 # homeassistant.components.neurio_energy neurio==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 930a6ff4b29..ca41a5ca264 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -730,7 +730,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.2.3 +nettigo-air-monitor==1.2.4 # homeassistant.components.nexia nexia==0.9.13 From 3de7ffde54103e51e92ea2d812e7ae972d0c1160 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 10 May 2022 00:23:19 +0000 Subject: [PATCH 0363/3516] [ci skip] Translation update --- .../components/airthings/translations/ko.json | 11 ++++++ .../binary_sensor/translations/ko.json | 3 ++ .../crownstone/translations/ko.json | 9 +++++ .../components/deconz/translations/ja.json | 1 + .../components/deluge/translations/it.json | 2 +- .../components/fan/translations/el.json | 1 + .../components/fan/translations/en.json | 1 + .../components/fan/translations/pt-BR.json | 1 + .../fjaraskupan/translations/ko.json | 7 ++++ .../components/fronius/translations/ko.json | 9 +++++ .../components/goodwe/translations/ko.json | 9 +++++ .../components/iotawatt/translations/ko.json | 15 +++++++ .../components/isy994/translations/ja.json | 7 ++++ .../components/isy994/translations/ru.json | 2 +- .../components/knx/translations/ko.json | 11 ++++++ .../components/meater/translations/ja.json | 5 +++ .../media_player/translations/ja.json | 3 ++ .../components/mill/translations/ko.json | 3 ++ .../components/nam/translations/ko.json | 12 ++++++ .../components/nanoleaf/translations/ko.json | 14 +++++++ .../components/nest/translations/ko.json | 4 ++ .../components/notion/translations/ko.json | 3 ++ .../components/onewire/translations/ca.json | 4 +- .../components/onewire/translations/de.json | 4 +- .../components/onewire/translations/el.json | 2 + .../components/onewire/translations/en.json | 4 +- .../components/onewire/translations/fr.json | 4 +- .../components/onewire/translations/hu.json | 2 + .../components/onewire/translations/id.json | 4 +- .../components/onewire/translations/it.json | 4 +- .../components/onewire/translations/ja.json | 2 + .../components/onewire/translations/nl.json | 4 +- .../components/onewire/translations/pl.json | 4 +- .../onewire/translations/pt-BR.json | 4 +- .../onewire/translations/zh-Hant.json | 4 +- .../opengarage/translations/ko.json | 11 ++++++ .../p1_monitor/translations/ko.json | 8 ++++ .../components/qnap_qsw/translations/ja.json | 13 +++++++ .../rainforest_eagle/translations/ko.json | 10 +++++ .../components/recorder/translations/id.json | 1 + .../components/recorder/translations/it.json | 1 + .../components/recorder/translations/nl.json | 1 + .../components/recorder/translations/no.json | 1 + .../components/recorder/translations/pl.json | 1 + .../simplisafe/translations/ru.json | 2 +- .../components/sms/translations/ca.json | 1 + .../components/sms/translations/de.json | 1 + .../components/sms/translations/el.json | 1 + .../components/sms/translations/en.json | 1 + .../components/sms/translations/et.json | 1 + .../components/sms/translations/fr.json | 1 + .../components/sms/translations/hu.json | 1 + .../components/sms/translations/id.json | 1 + .../components/sms/translations/it.json | 1 + .../components/sms/translations/ja.json | 1 + .../components/sms/translations/nl.json | 1 + .../components/sms/translations/no.json | 1 + .../components/sms/translations/pl.json | 1 + .../components/sms/translations/pt-BR.json | 1 + .../components/sms/translations/zh-Hant.json | 1 + .../components/sql/translations/ja.json | 39 +++++++++++++++++++ .../steam_online/translations/ja.json | 11 ++++++ .../steam_online/translations/nl.json | 4 +- .../steam_online/translations/ru.json | 4 +- .../components/tailscale/translations/ko.json | 14 +++++++ .../components/tautulli/translations/ja.json | 27 +++++++++++++ .../totalconnect/translations/ko.json | 1 + .../components/tplink/translations/ko.json | 3 ++ .../tractive/translations/sensor.ko.json | 9 +++++ .../trafikverket_ferry/translations/ja.json | 26 +++++++++++++ .../translations/ko.json | 15 +++++++ .../tuya/translations/select.ko.json | 12 ++++++ .../ukraine_alarm/translations/ca.json | 39 +++++++++++++++++++ .../ukraine_alarm/translations/et.json | 39 +++++++++++++++++++ .../ukraine_alarm/translations/id.json | 39 +++++++++++++++++++ .../ukraine_alarm/translations/it.json | 39 +++++++++++++++++++ .../ukraine_alarm/translations/ja.json | 35 +++++++++++++++++ .../ukraine_alarm/translations/nl.json | 39 +++++++++++++++++++ .../ukraine_alarm/translations/no.json | 39 +++++++++++++++++++ .../ukraine_alarm/translations/pl.json | 35 +++++++++++++++++ .../ukraine_alarm/translations/pt-BR.json | 39 +++++++++++++++++++ .../ukraine_alarm/translations/zh-Hant.json | 39 +++++++++++++++++++ .../components/unifi/translations/nl.json | 2 +- .../components/vulcan/translations/id.json | 9 +++++ .../components/vulcan/translations/ja.json | 7 ++++ .../components/wallbox/translations/ko.json | 7 ++++ .../components/ws66i/translations/ca.json | 34 ++++++++++++++++ .../components/ws66i/translations/de.json | 34 ++++++++++++++++ .../components/ws66i/translations/el.json | 34 ++++++++++++++++ .../components/ws66i/translations/et.json | 34 ++++++++++++++++ .../components/ws66i/translations/hu.json | 34 ++++++++++++++++ .../components/ws66i/translations/id.json | 34 ++++++++++++++++ .../components/ws66i/translations/it.json | 34 ++++++++++++++++ .../components/ws66i/translations/ja.json | 18 +++++++++ .../components/ws66i/translations/nl.json | 34 ++++++++++++++++ .../components/ws66i/translations/no.json | 34 ++++++++++++++++ .../components/ws66i/translations/pl.json | 34 ++++++++++++++++ .../components/ws66i/translations/pt-BR.json | 34 ++++++++++++++++ .../components/ws66i/translations/ru.json | 34 ++++++++++++++++ .../ws66i/translations/zh-Hant.json | 34 ++++++++++++++++ .../yale_smart_alarm/translations/ko.json | 11 ++++++ .../components/zwave_js/translations/ko.json | 7 ++++ 102 files changed, 1275 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/airthings/translations/ko.json create mode 100644 homeassistant/components/crownstone/translations/ko.json create mode 100644 homeassistant/components/fjaraskupan/translations/ko.json create mode 100644 homeassistant/components/fronius/translations/ko.json create mode 100644 homeassistant/components/goodwe/translations/ko.json create mode 100644 homeassistant/components/iotawatt/translations/ko.json create mode 100644 homeassistant/components/knx/translations/ko.json create mode 100644 homeassistant/components/nam/translations/ko.json create mode 100644 homeassistant/components/nanoleaf/translations/ko.json create mode 100644 homeassistant/components/opengarage/translations/ko.json create mode 100644 homeassistant/components/p1_monitor/translations/ko.json create mode 100644 homeassistant/components/qnap_qsw/translations/ja.json create mode 100644 homeassistant/components/rainforest_eagle/translations/ko.json create mode 100644 homeassistant/components/sql/translations/ja.json create mode 100644 homeassistant/components/steam_online/translations/ja.json create mode 100644 homeassistant/components/tailscale/translations/ko.json create mode 100644 homeassistant/components/tautulli/translations/ja.json create mode 100644 homeassistant/components/tractive/translations/sensor.ko.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/ja.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/ko.json create mode 100644 homeassistant/components/tuya/translations/select.ko.json create mode 100644 homeassistant/components/ukraine_alarm/translations/ca.json create mode 100644 homeassistant/components/ukraine_alarm/translations/et.json create mode 100644 homeassistant/components/ukraine_alarm/translations/id.json create mode 100644 homeassistant/components/ukraine_alarm/translations/it.json create mode 100644 homeassistant/components/ukraine_alarm/translations/ja.json create mode 100644 homeassistant/components/ukraine_alarm/translations/nl.json create mode 100644 homeassistant/components/ukraine_alarm/translations/no.json create mode 100644 homeassistant/components/ukraine_alarm/translations/pl.json create mode 100644 homeassistant/components/ukraine_alarm/translations/pt-BR.json create mode 100644 homeassistant/components/ukraine_alarm/translations/zh-Hant.json create mode 100644 homeassistant/components/wallbox/translations/ko.json create mode 100644 homeassistant/components/ws66i/translations/ca.json create mode 100644 homeassistant/components/ws66i/translations/de.json create mode 100644 homeassistant/components/ws66i/translations/el.json create mode 100644 homeassistant/components/ws66i/translations/et.json create mode 100644 homeassistant/components/ws66i/translations/hu.json create mode 100644 homeassistant/components/ws66i/translations/id.json create mode 100644 homeassistant/components/ws66i/translations/it.json create mode 100644 homeassistant/components/ws66i/translations/ja.json create mode 100644 homeassistant/components/ws66i/translations/nl.json create mode 100644 homeassistant/components/ws66i/translations/no.json create mode 100644 homeassistant/components/ws66i/translations/pl.json create mode 100644 homeassistant/components/ws66i/translations/pt-BR.json create mode 100644 homeassistant/components/ws66i/translations/ru.json create mode 100644 homeassistant/components/ws66i/translations/zh-Hant.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/ko.json diff --git a/homeassistant/components/airthings/translations/ko.json b/homeassistant/components/airthings/translations/ko.json new file mode 100644 index 00000000000..27863f2e0e4 --- /dev/null +++ b/homeassistant/components/airthings/translations/ko.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "secret": "\ubcf4\uc548\ud0a4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/ko.json b/homeassistant/components/binary_sensor/translations/ko.json index 7a725fc6719..3d26bdd0193 100644 --- a/homeassistant/components/binary_sensor/translations/ko.json +++ b/homeassistant/components/binary_sensor/translations/ko.json @@ -89,6 +89,9 @@ "vibration": "{entity_name}\uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uae30 \uc2dc\uc791\ud588\uc744 \ub54c" } }, + "device_class": { + "co": "\uc77c\uc0b0\ud654\ud0c4\uc18c" + }, "state": { "_": { "off": "\uaebc\uc9d0", diff --git a/homeassistant/components/crownstone/translations/ko.json b/homeassistant/components/crownstone/translations/ko.json new file mode 100644 index 00000000000..22a5729e256 --- /dev/null +++ b/homeassistant/components/crownstone/translations/ko.json @@ -0,0 +1,9 @@ +{ + "options": { + "step": { + "usb_sphere_config": { + "title": "\ud06c\ub77c\uc6b4\uc2a4\ud1a4 USB \uc2a4\ud53c\uc5b4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/ja.json b/homeassistant/components/deconz/translations/ja.json index a074be8d8cd..ce1cab4dea6 100644 --- a/homeassistant/components/deconz/translations/ja.json +++ b/homeassistant/components/deconz/translations/ja.json @@ -9,6 +9,7 @@ "updated_instance": "\u65b0\u3057\u3044\u30db\u30b9\u30c8\u30a2\u30c9\u30ec\u30b9\u3067deCONZ\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f" }, "error": { + "linking_not_possible": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3068\u30ea\u30f3\u30af\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", "no_key": "API\u30ad\u30fc\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" }, "flow_title": "{host}", diff --git a/homeassistant/components/deluge/translations/it.json b/homeassistant/components/deluge/translations/it.json index 52cfb4a09ae..a7b03fc75ed 100644 --- a/homeassistant/components/deluge/translations/it.json +++ b/homeassistant/components/deluge/translations/it.json @@ -16,7 +16,7 @@ "username": "Nome utente", "web_port": "Porta web (per il servizio di visita)" }, - "description": "Per poter utilizzare questa integrazione, devi abilitare la seguente opzione nelle impostazioni di diluvio: Demone > Consenti controlli remoti" + "description": "Per poter utilizzare questa integrazione, devi abilitare la seguente opzione nelle impostazioni di diluvio: Daemon > Consenti controlli remoti" } } } diff --git a/homeassistant/components/fan/translations/el.json b/homeassistant/components/fan/translations/el.json index b9c4962a7a6..b0ff671f737 100644 --- a/homeassistant/components/fan/translations/el.json +++ b/homeassistant/components/fan/translations/el.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "\u0395\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae {entity_name}", "turn_off": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 {entity_name}", "turn_on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 {entity_name}" }, diff --git a/homeassistant/components/fan/translations/en.json b/homeassistant/components/fan/translations/en.json index f4f27e8fd2d..55feb3dad9f 100644 --- a/homeassistant/components/fan/translations/en.json +++ b/homeassistant/components/fan/translations/en.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "Toggle {entity_name}", "turn_off": "Turn off {entity_name}", "turn_on": "Turn on {entity_name}" }, diff --git a/homeassistant/components/fan/translations/pt-BR.json b/homeassistant/components/fan/translations/pt-BR.json index 97145dd8a60..14eafd36f40 100644 --- a/homeassistant/components/fan/translations/pt-BR.json +++ b/homeassistant/components/fan/translations/pt-BR.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "Alternar {entity_name}", "turn_off": "Desligar {entity_name}", "turn_on": "Ligar {entity_name}" }, diff --git a/homeassistant/components/fjaraskupan/translations/ko.json b/homeassistant/components/fjaraskupan/translations/ko.json new file mode 100644 index 00000000000..0a4087d0200 --- /dev/null +++ b/homeassistant/components/fjaraskupan/translations/ko.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c \uc548\uc5d0 \ubc1c\uacac\ub41c \ub514\ubc14\uc774\uc2a4 \uc5c6\uc74c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/ko.json b/homeassistant/components/fronius/translations/ko.json new file mode 100644 index 00000000000..5e3238d1be1 --- /dev/null +++ b/homeassistant/components/fronius/translations/ko.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm_discovery": { + "description": "{device} \ub97c \ud648\uc5b4\uc2dc\uc2a4\ud134\ud2b8\uc5d0 \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goodwe/translations/ko.json b/homeassistant/components/goodwe/translations/ko.json new file mode 100644 index 00000000000..9aaa9eacf20 --- /dev/null +++ b/homeassistant/components/goodwe/translations/ko.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "\uc778\ubc84\ud130\uc5d0 \uc5f0\uacb0" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iotawatt/translations/ko.json b/homeassistant/components/iotawatt/translations/ko.json new file mode 100644 index 00000000000..f3fac4e1e74 --- /dev/null +++ b/homeassistant/components/iotawatt/translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + }, + "step": { + "auth": { + "data": { + "password": "\uc554\ud638" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/ja.json b/homeassistant/components/isy994/translations/ja.json index 271cba9e0c4..51c56f035d7 100644 --- a/homeassistant/components/isy994/translations/ja.json +++ b/homeassistant/components/isy994/translations/ja.json @@ -7,10 +7,17 @@ "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_host": "\u30db\u30b9\u30c8\u30a8\u30f3\u30c8\u30ea\u306f\u304d\u3061\u3093\u3068\u3057\u305fURL\u5f62\u5f0f\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f \u4f8b: http://192.168.10.100:80", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + }, "user": { "data": { "host": "URL", diff --git a/homeassistant/components/isy994/translations/ru.json b/homeassistant/components/isy994/translations/ru.json index 61e2c79831a..ea7d5c6e36c 100644 --- a/homeassistant/components/isy994/translations/ru.json +++ b/homeassistant/components/isy994/translations/ru.json @@ -42,7 +42,7 @@ "variable_sensor_string": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0443\u044e \u043a\u0430\u043a \u0441\u0435\u043d\u0441\u043e\u0440" }, "description": "\u041e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432:\n \u2022 \u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0443\u0437\u0435\u043b \u043a\u0430\u043a \u0441\u0435\u043d\u0441\u043e\u0440: \u043b\u044e\u0431\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438\u043b\u0438 \u043f\u0430\u043f\u043a\u0430, \u0432 \u0438\u043c\u0435\u043d\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u0430\u044f \u0441\u0442\u0440\u043e\u043a\u0430, \u0431\u0443\u0434\u0435\u0442 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043e \u043a\u0430\u043a \u0441\u0435\u043d\u0441\u043e\u0440 \u0438\u043b\u0438 \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440.\n \u2022 \u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0443\u044e \u043a\u0430\u043a \u0441\u0435\u043d\u0441\u043e\u0440: \u043b\u044e\u0431\u0430\u044f \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u0430\u044f, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u0443\u044e \u0441\u0442\u0440\u043e\u043a\u0443, \u0431\u0443\u0434\u0435\u0442 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430 \u043a\u0430\u043a \u0441\u0435\u043d\u0441\u043e\u0440.\n \u2022 \u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c: \u043b\u044e\u0431\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0432 \u0438\u043c\u0435\u043d\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u0430\u044f \u0441\u0442\u0440\u043e\u043a\u0430, \u0431\u0443\u0434\u0435\u0442 \u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f.\n \u2022 \u0412\u043e\u0441\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c \u0441\u0432\u0435\u0442\u0430: \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u044f\u0440\u043a\u043e\u0441\u0442\u0438, \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0435 \u0434\u043e \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 ISY994" + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 ISY" } } }, diff --git a/homeassistant/components/knx/translations/ko.json b/homeassistant/components/knx/translations/ko.json new file mode 100644 index 00000000000..676037b15e4 --- /dev/null +++ b/homeassistant/components/knx/translations/ko.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "manual_tunnel": { + "data": { + "route_back": "\ub77c\uc6b0\ud2b8 \ubc31 / NAT \ubaa8\ub4dc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/ja.json b/homeassistant/components/meater/translations/ja.json index db4dd2e9c6b..e24fc9199cc 100644 --- a/homeassistant/components/meater/translations/ja.json +++ b/homeassistant/components/meater/translations/ja.json @@ -6,6 +6,11 @@ "unknown_auth_error": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", diff --git a/homeassistant/components/media_player/translations/ja.json b/homeassistant/components/media_player/translations/ja.json index c553308a3e7..a11bc516b38 100644 --- a/homeassistant/components/media_player/translations/ja.json +++ b/homeassistant/components/media_player/translations/ja.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_buffering": "{entity_name} \u306f\u3001\u30d0\u30c3\u30d5\u30a1\u30ea\u30f3\u30b0\u4e2d\u3067\u3059", "is_idle": "{entity_name} \u306f\u3001\u30a2\u30a4\u30c9\u30eb\u72b6\u614b\u3067\u3059", "is_off": "{entity_name} \u306f\u30aa\u30d5\u3067\u3059", "is_on": "{entity_name} \u304c\u30aa\u30f3\u3067\u3059", @@ -8,6 +9,7 @@ "is_playing": "{entity_name} \u304c\u518d\u751f\u3055\u308c\u3066\u3044\u307e\u3059" }, "trigger_type": { + "buffering": "{entity_name} \u306e\u30d0\u30c3\u30d5\u30a1\u30ea\u30f3\u30b0\u3092\u958b\u59cb\u3057\u307e\u3059", "changed_states": "{entity_name} \u306e\u72b6\u614b\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f", "idle": "{entity_name} \u304c\u30a2\u30a4\u30c9\u30eb\u72b6\u614b\u306b\u306a\u308a\u307e\u3059", "paused": "{entity_name} \u306f\u3001\u4e00\u6642\u505c\u6b62\u3057\u3066\u3044\u307e\u3059", @@ -18,6 +20,7 @@ }, "state": { "_": { + "buffering": "\u30d0\u30c3\u30d5\u30a1\u30ea\u30f3\u30b0", "idle": "\u30a2\u30a4\u30c9\u30eb", "off": "\u30aa\u30d5", "on": "\u30aa\u30f3", diff --git a/homeassistant/components/mill/translations/ko.json b/homeassistant/components/mill/translations/ko.json index 48c8cdc6eaa..1041727d84a 100644 --- a/homeassistant/components/mill/translations/ko.json +++ b/homeassistant/components/mill/translations/ko.json @@ -7,6 +7,9 @@ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { + "local": { + "description": "\ub85c\uceec IP \uc8fc\uc18c" + }, "user": { "data": { "password": "\ube44\ubc00\ubc88\ud638", diff --git a/homeassistant/components/nam/translations/ko.json b/homeassistant/components/nam/translations/ko.json new file mode 100644 index 00000000000..a4cc4c01810 --- /dev/null +++ b/homeassistant/components/nam/translations/ko.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "reauth_unsuccessful": "\uc7ac\uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. \uad6c\uc131\uc744 \uc81c\uac70\ud558\uace0 \ub2e4\uc2dc \uc124\uc815\ud558\uc2ed\uc2dc\uc624." + }, + "step": { + "reauth_confirm": { + "description": "\ud638\uc2a4\ud2b8\uc5d0 \ub300\ud55c \uc62c\ubc14\ub978 \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \uc554\ud638\ub97c \uc785\ub825\ud558\uc2ed\uc2dc\uc624: {host}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/ko.json b/homeassistant/components/nanoleaf/translations/ko.json new file mode 100644 index 00000000000..e540d903647 --- /dev/null +++ b/homeassistant/components/nanoleaf/translations/ko.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\ub514\ubc14\uc774\uc2a4\uac00 \uc774\ubbf8 \uc124\uc815\ub418\uc5b4 \uc788\uc74c", + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "invalid_token": "\uc798\ubabb\ub41c \uc811\uc18d \ud1a0\ud070", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ko.json b/homeassistant/components/nest/translations/ko.json index 048a0c7d100..1b4563c800f 100644 --- a/homeassistant/components/nest/translations/ko.json +++ b/homeassistant/components/nest/translations/ko.json @@ -35,6 +35,10 @@ "pick_implementation": { "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" }, + "pubsub": { + "description": "[Cloud Console]( {url} )\uc744 \ubc29\ubb38\ud558\uc5ec Google Cloud \ud504\ub85c\uc81d\ud2b8 ID\ub97c \ucc3e\uc73c\uc138\uc694.", + "title": "\uad6c\uae00 \ud074\ub77c\uc6b0\ub4dc \uad6c\uc131" + }, "reauth_confirm": { "description": "Nest \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \uacc4\uc815\uc744 \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4", "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" diff --git a/homeassistant/components/notion/translations/ko.json b/homeassistant/components/notion/translations/ko.json index b5c7cadbe9b..c81805446f9 100644 --- a/homeassistant/components/notion/translations/ko.json +++ b/homeassistant/components/notion/translations/ko.json @@ -8,6 +8,9 @@ "no_devices": "\uacc4\uc815\uc5d0 \ub4f1\ub85d\ub41c \uae30\uae30\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" }, "step": { + "reauth_confirm": { + "description": "{username} \uc758 \ube44\ubc00\ubc88\ud638\ub97c \ub2e4\uc2dc \uc785\ub825\ud558\uc138\uc694." + }, "user": { "data": { "password": "\ube44\ubc00\ubc88\ud638", diff --git a/homeassistant/components/onewire/translations/ca.json b/homeassistant/components/onewire/translations/ca.json index b50e3abc1f6..a8f4da41b6b 100644 --- a/homeassistant/components/onewire/translations/ca.json +++ b/homeassistant/components/onewire/translations/ca.json @@ -17,9 +17,11 @@ }, "user": { "data": { + "host": "Amfitri\u00f3", + "port": "Port", "type": "Tipus de connexi\u00f3" }, - "title": "Configuraci\u00f3 d'1-Wire" + "title": "Detalls del servidor" } } }, diff --git a/homeassistant/components/onewire/translations/de.json b/homeassistant/components/onewire/translations/de.json index a6347b12d75..14f321c8004 100644 --- a/homeassistant/components/onewire/translations/de.json +++ b/homeassistant/components/onewire/translations/de.json @@ -17,9 +17,11 @@ }, "user": { "data": { + "host": "Host", + "port": "Port", "type": "Verbindungstyp" }, - "title": "1-Wire einrichten" + "title": "Serverdetails festlegen" } } }, diff --git a/homeassistant/components/onewire/translations/el.json b/homeassistant/components/onewire/translations/el.json index 3d358decfe5..b0d37d679bc 100644 --- a/homeassistant/components/onewire/translations/el.json +++ b/homeassistant/components/onewire/translations/el.json @@ -17,6 +17,8 @@ }, "user": { "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1", "type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 1-Wire" diff --git a/homeassistant/components/onewire/translations/en.json b/homeassistant/components/onewire/translations/en.json index df61b436f65..d8e2c157e0f 100644 --- a/homeassistant/components/onewire/translations/en.json +++ b/homeassistant/components/onewire/translations/en.json @@ -17,9 +17,11 @@ }, "user": { "data": { + "host": "Host", + "port": "Port", "type": "Connection type" }, - "title": "Set up 1-Wire" + "title": "Set server details" } } }, diff --git a/homeassistant/components/onewire/translations/fr.json b/homeassistant/components/onewire/translations/fr.json index 08e3d3cf18b..3d9e01916c3 100644 --- a/homeassistant/components/onewire/translations/fr.json +++ b/homeassistant/components/onewire/translations/fr.json @@ -17,9 +17,11 @@ }, "user": { "data": { + "host": "H\u00f4te", + "port": "Port", "type": "Type de connexion" }, - "title": "Configurer 1-Wire" + "title": "Renseigner les informations du serveur" } } }, diff --git a/homeassistant/components/onewire/translations/hu.json b/homeassistant/components/onewire/translations/hu.json index 7034f2eaa29..e26721758cd 100644 --- a/homeassistant/components/onewire/translations/hu.json +++ b/homeassistant/components/onewire/translations/hu.json @@ -17,6 +17,8 @@ }, "user": { "data": { + "host": "C\u00edm", + "port": "Port", "type": "Kapcsolat t\u00edpusa" }, "title": "A 1-Wire be\u00e1ll\u00edt\u00e1sa" diff --git a/homeassistant/components/onewire/translations/id.json b/homeassistant/components/onewire/translations/id.json index a1cde1b35bd..203a38d1a4c 100644 --- a/homeassistant/components/onewire/translations/id.json +++ b/homeassistant/components/onewire/translations/id.json @@ -17,9 +17,11 @@ }, "user": { "data": { + "host": "Host", + "port": "Port", "type": "Jenis koneksi" }, - "title": "Siapkan 1-Wire" + "title": "Setel detail server" } } }, diff --git a/homeassistant/components/onewire/translations/it.json b/homeassistant/components/onewire/translations/it.json index ec2edddc275..77f64381ee7 100644 --- a/homeassistant/components/onewire/translations/it.json +++ b/homeassistant/components/onewire/translations/it.json @@ -17,9 +17,11 @@ }, "user": { "data": { + "host": "Host", + "port": "Porta", "type": "Tipo di connessione" }, - "title": "Configurazione 1-Wire" + "title": "Imposta i dettagli del server" } } }, diff --git a/homeassistant/components/onewire/translations/ja.json b/homeassistant/components/onewire/translations/ja.json index c948a9567b0..b4acd54cd41 100644 --- a/homeassistant/components/onewire/translations/ja.json +++ b/homeassistant/components/onewire/translations/ja.json @@ -17,6 +17,8 @@ }, "user": { "data": { + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8", "type": "\u63a5\u7d9a\u30bf\u30a4\u30d7" }, "title": "1-Wire\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" diff --git a/homeassistant/components/onewire/translations/nl.json b/homeassistant/components/onewire/translations/nl.json index 3ea1fd8e13c..e6af2cebef3 100644 --- a/homeassistant/components/onewire/translations/nl.json +++ b/homeassistant/components/onewire/translations/nl.json @@ -17,9 +17,11 @@ }, "user": { "data": { + "host": "Host", + "port": "Poort", "type": "Verbindingstype" }, - "title": "Stel 1-Wire in" + "title": "Serverdetails instellen" } } }, diff --git a/homeassistant/components/onewire/translations/pl.json b/homeassistant/components/onewire/translations/pl.json index fe0e1d54eb5..743a1b28522 100644 --- a/homeassistant/components/onewire/translations/pl.json +++ b/homeassistant/components/onewire/translations/pl.json @@ -17,9 +17,11 @@ }, "user": { "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port", "type": "Rodzaj po\u0142\u0105czenia" }, - "title": "Konfiguracja 1-Wire" + "title": "Konfiguracja serwera" } } }, diff --git a/homeassistant/components/onewire/translations/pt-BR.json b/homeassistant/components/onewire/translations/pt-BR.json index 61405f5089a..74fe2918222 100644 --- a/homeassistant/components/onewire/translations/pt-BR.json +++ b/homeassistant/components/onewire/translations/pt-BR.json @@ -17,9 +17,11 @@ }, "user": { "data": { + "host": "Host", + "port": "Porta", "type": "Tipo de conex\u00e3o" }, - "title": "Configurar 1-Wire" + "title": "Definir detalhes do servidor" } } }, diff --git a/homeassistant/components/onewire/translations/zh-Hant.json b/homeassistant/components/onewire/translations/zh-Hant.json index 8b11692a3f5..a77b86ea416 100644 --- a/homeassistant/components/onewire/translations/zh-Hant.json +++ b/homeassistant/components/onewire/translations/zh-Hant.json @@ -17,9 +17,11 @@ }, "user": { "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0", "type": "\u9023\u7dda\u985e\u5225" }, - "title": "\u8a2d\u5b9a 1-Wire" + "title": "\u8a2d\u5b9a\u4f3a\u670d\u5668\u8a73\u7d30\u8cc7\u8a0a" } } }, diff --git a/homeassistant/components/opengarage/translations/ko.json b/homeassistant/components/opengarage/translations/ko.json new file mode 100644 index 00000000000..b297ac33af6 --- /dev/null +++ b/homeassistant/components/opengarage/translations/ko.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "device_key": "\uc7a5\uce58 \ud0a4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/p1_monitor/translations/ko.json b/homeassistant/components/p1_monitor/translations/ko.json new file mode 100644 index 00000000000..40f9f3b7152 --- /dev/null +++ b/homeassistant/components/p1_monitor/translations/ko.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/ja.json b/homeassistant/components/qnap_qsw/translations/ja.json new file mode 100644 index 00000000000..38e13174ac8 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/ja.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "url": "URL", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainforest_eagle/translations/ko.json b/homeassistant/components/rainforest_eagle/translations/ko.json new file mode 100644 index 00000000000..2da0f2c8ee1 --- /dev/null +++ b/homeassistant/components/rainforest_eagle/translations/ko.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "\ub514\ubc14\uc774\uc2a4\uac00 \uc774\ubbf8 \uc124\uc815\ub418\uc5b4 \uc788\uc74c" + }, + "error": { + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/id.json b/homeassistant/components/recorder/translations/id.json index 9d3b4c333ef..ec9e87b39ab 100644 --- a/homeassistant/components/recorder/translations/id.json +++ b/homeassistant/components/recorder/translations/id.json @@ -2,6 +2,7 @@ "system_health": { "info": { "current_recorder_run": "Waktu Mulai Jalankan Saat Ini", + "estimated_db_size": "Perkiraan Ukuran Basis Data (MiB)", "oldest_recorder_run": "Waktu Mulai Lari Terlama" } } diff --git a/homeassistant/components/recorder/translations/it.json b/homeassistant/components/recorder/translations/it.json index 82f456981a8..ab0ac13772a 100644 --- a/homeassistant/components/recorder/translations/it.json +++ b/homeassistant/components/recorder/translations/it.json @@ -2,6 +2,7 @@ "system_health": { "info": { "current_recorder_run": "Ora di inizio esecuzione corrente", + "estimated_db_size": "Dimensione stimata del database (MiB)", "oldest_recorder_run": "Ora di inizio esecuzione meno recente" } } diff --git a/homeassistant/components/recorder/translations/nl.json b/homeassistant/components/recorder/translations/nl.json index 5bffcef1bd2..bb28428cc48 100644 --- a/homeassistant/components/recorder/translations/nl.json +++ b/homeassistant/components/recorder/translations/nl.json @@ -2,6 +2,7 @@ "system_health": { "info": { "current_recorder_run": "Starttijd huidige run", + "estimated_db_size": "Geschatte databasegrootte (MiB)", "oldest_recorder_run": "Starttijd oudste run" } } diff --git a/homeassistant/components/recorder/translations/no.json b/homeassistant/components/recorder/translations/no.json index b1474c13466..982354a6e34 100644 --- a/homeassistant/components/recorder/translations/no.json +++ b/homeassistant/components/recorder/translations/no.json @@ -2,6 +2,7 @@ "system_health": { "info": { "current_recorder_run": "Gjeldende starttid for kj\u00f8ring", + "estimated_db_size": "Estimert databasest\u00f8rrelse (MiB)", "oldest_recorder_run": "Eldste Run Start Time" } } diff --git a/homeassistant/components/recorder/translations/pl.json b/homeassistant/components/recorder/translations/pl.json index 1a31b3f7ec5..a91f92207f2 100644 --- a/homeassistant/components/recorder/translations/pl.json +++ b/homeassistant/components/recorder/translations/pl.json @@ -2,6 +2,7 @@ "system_health": { "info": { "current_recorder_run": "Pocz\u0105tek aktualnej sesji rejestratora", + "estimated_db_size": "Szacowany rozmiar bazy danych (MiB)", "oldest_recorder_run": "Pocz\u0105tek najstarszej sesji rejestratora" } } diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index 1788a9ca206..0d2a36085e4 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -14,7 +14,7 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "progress": { - "email_2fa": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u0412\u0430\u043c \u043f\u043e \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u0435." + "email_2fa": "\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 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f \u043e\u0442 Simplisafe." }, "step": { "mfa": { diff --git a/homeassistant/components/sms/translations/ca.json b/homeassistant/components/sms/translations/ca.json index f7640befed4..f6491d0dd46 100644 --- a/homeassistant/components/sms/translations/ca.json +++ b/homeassistant/components/sms/translations/ca.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "baud_speed": "Velocitat en Baudis", "device": "Dispositiu" }, "title": "Connexi\u00f3 al m\u00f2dem" diff --git a/homeassistant/components/sms/translations/de.json b/homeassistant/components/sms/translations/de.json index 3c5cc3c0490..f17e30789c2 100644 --- a/homeassistant/components/sms/translations/de.json +++ b/homeassistant/components/sms/translations/de.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "baud_speed": "Baud-Geschwindigkeit", "device": "Ger\u00e4t" }, "title": "Verbinden mit dem Modem" diff --git a/homeassistant/components/sms/translations/el.json b/homeassistant/components/sms/translations/el.json index 372cdfed20f..6484f847523 100644 --- a/homeassistant/components/sms/translations/el.json +++ b/homeassistant/components/sms/translations/el.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "baud_speed": "\u03a4\u03b1\u03c7\u03cd\u03c4\u03b7\u03c4\u03b1 Baud", "device": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" }, "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf \u03bc\u03cc\u03bd\u03c4\u03b5\u03bc" diff --git a/homeassistant/components/sms/translations/en.json b/homeassistant/components/sms/translations/en.json index dbbac1871c7..12d4e82c648 100644 --- a/homeassistant/components/sms/translations/en.json +++ b/homeassistant/components/sms/translations/en.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "baud_speed": "Baud Speed", "device": "Device" }, "title": "Connect to the modem" diff --git a/homeassistant/components/sms/translations/et.json b/homeassistant/components/sms/translations/et.json index 70a378d591e..e3f67f1ac1a 100644 --- a/homeassistant/components/sms/translations/et.json +++ b/homeassistant/components/sms/translations/et.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "baud_speed": "Pordi kiirus", "device": "Seade" }, "title": "Modemiga \u00fchenduse loomine" diff --git a/homeassistant/components/sms/translations/fr.json b/homeassistant/components/sms/translations/fr.json index ebfa3c1da08..3caec67daaa 100644 --- a/homeassistant/components/sms/translations/fr.json +++ b/homeassistant/components/sms/translations/fr.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "baud_speed": "Vitesse en bauds", "device": "Appareil" }, "title": "Se connecter au modem" diff --git a/homeassistant/components/sms/translations/hu.json b/homeassistant/components/sms/translations/hu.json index 6fa524b18ab..3ebf7fbd948 100644 --- a/homeassistant/components/sms/translations/hu.json +++ b/homeassistant/components/sms/translations/hu.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "baud_speed": "Baud sebess\u00e9g", "device": "Eszk\u00f6z" }, "title": "Csatlakoz\u00e1s a modemhez" diff --git a/homeassistant/components/sms/translations/id.json b/homeassistant/components/sms/translations/id.json index 63ebb088521..d2b190eb3d0 100644 --- a/homeassistant/components/sms/translations/id.json +++ b/homeassistant/components/sms/translations/id.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "baud_speed": "Kecepatan Baud", "device": "Perangkat" }, "title": "Hubungkan ke modem" diff --git a/homeassistant/components/sms/translations/it.json b/homeassistant/components/sms/translations/it.json index 9d2ac87d833..48524b72f24 100644 --- a/homeassistant/components/sms/translations/it.json +++ b/homeassistant/components/sms/translations/it.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "baud_speed": "Velocit\u00e0 Baud", "device": "Dispositivo" }, "title": "Connettiti al modem" diff --git a/homeassistant/components/sms/translations/ja.json b/homeassistant/components/sms/translations/ja.json index 248427ad9bf..ddfb644d90f 100644 --- a/homeassistant/components/sms/translations/ja.json +++ b/homeassistant/components/sms/translations/ja.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "baud_speed": "\u30dc\u30fc\u30ec\u30fc\u30c8", "device": "\u30c7\u30d0\u30a4\u30b9" }, "title": "\u30e2\u30c7\u30e0\u306b\u63a5\u7d9a" diff --git a/homeassistant/components/sms/translations/nl.json b/homeassistant/components/sms/translations/nl.json index ddcc54d239f..7c2bcb0568c 100644 --- a/homeassistant/components/sms/translations/nl.json +++ b/homeassistant/components/sms/translations/nl.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "baud_speed": "Baud snelheid", "device": "Apparaat" }, "title": "Maak verbinding met de modem" diff --git a/homeassistant/components/sms/translations/no.json b/homeassistant/components/sms/translations/no.json index ab692ff8fa6..e8abd1ab220 100644 --- a/homeassistant/components/sms/translations/no.json +++ b/homeassistant/components/sms/translations/no.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "baud_speed": "Overf\u00f8ringshastighet", "device": "Enhet" }, "title": "Koble til modemet" diff --git a/homeassistant/components/sms/translations/pl.json b/homeassistant/components/sms/translations/pl.json index 615f08e3e06..c3aae889b7e 100644 --- a/homeassistant/components/sms/translations/pl.json +++ b/homeassistant/components/sms/translations/pl.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "baud_speed": "Szybko\u015b\u0107 transmisji (Baud)", "device": "Urz\u0105dzenie" }, "title": "Po\u0142\u0105czenie z modemem" diff --git a/homeassistant/components/sms/translations/pt-BR.json b/homeassistant/components/sms/translations/pt-BR.json index 05e211760f2..708d64e33de 100644 --- a/homeassistant/components/sms/translations/pt-BR.json +++ b/homeassistant/components/sms/translations/pt-BR.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "baud_speed": "Velocidade de transmiss\u00e3o", "device": "Dispositivo" }, "title": "Conectado ao modem" diff --git a/homeassistant/components/sms/translations/zh-Hant.json b/homeassistant/components/sms/translations/zh-Hant.json index b6e08ffa7ec..0b84e7910f7 100644 --- a/homeassistant/components/sms/translations/zh-Hant.json +++ b/homeassistant/components/sms/translations/zh-Hant.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "baud_speed": "\u9b91\u7387", "device": "\u88dd\u7f6e" }, "title": "\u9023\u7dda\u81f3\u6578\u64da\u6a5f" diff --git a/homeassistant/components/sql/translations/ja.json b/homeassistant/components/sql/translations/ja.json new file mode 100644 index 00000000000..cf03616fbb9 --- /dev/null +++ b/homeassistant/components/sql/translations/ja.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "db_url_invalid": "\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306eURL\u304c\u7121\u52b9\u3067\u3059", + "query_invalid": "SQL\u30af\u30a8\u30ea\u304c\u7121\u52b9\u3067\u3059", + "value_template_invalid": "\u5024\u306e\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8\u304c\u7121\u52b9\u3067\u3059" + }, + "step": { + "user": { + "data": { + "column": "\u30b3\u30e9\u30e0", + "db_url": "\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306eURL", + "name": "\u540d\u524d", + "query": "\u30af\u30a8\u30ea\u3092\u9078\u629e", + "unit_of_measurement": "\u6e2c\u5b9a\u5358\u4f4d", + "value_template": "\u5024\u306e\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8" + }, + "data_description": { + "name": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u3068\u30bb\u30f3\u30b5\u30fc\u306b\u4f7f\u7528\u3055\u308c\u308b\u540d\u524d" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "\u540d\u524d" + }, + "data_description": { + "name": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u3068\u30bb\u30f3\u30b5\u30fc\u306b\u4f7f\u7528\u3055\u308c\u308b\u540d\u524d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/ja.json b/homeassistant/components/steam_online/translations/ja.json new file mode 100644 index 00000000000..07c813bb0b1 --- /dev/null +++ b/homeassistant/components/steam_online/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/nl.json b/homeassistant/components/steam_online/translations/nl.json index 20600463f6a..d9281038e76 100644 --- a/homeassistant/components/steam_online/translations/nl.json +++ b/homeassistant/components/steam_online/translations/nl.json @@ -12,7 +12,7 @@ }, "step": { "reauth_confirm": { - "description": "De Steam-integratie moet handmatig opnieuw worden geauthenticeerd.\n\nU vind uw API-sleutel hier: https://steamcommunity.com/dev/apikey", + "description": "De Steam integratie moet handmatig opnieuw geauthenticeerd worden\n\nU kunt uw sleutel hier vinden: {api_key_url}", "title": "Verifieer de integratie opnieuw" }, "user": { @@ -20,7 +20,7 @@ "account": "Steam Account-ID", "api_key": "API-sleutel" }, - "description": "Gebruik https://steamid.io om je Steam Account-ID te vinden." + "description": "Gebruik {account_id_url} om uw Steam account ID te vinden" } } }, diff --git a/homeassistant/components/steam_online/translations/ru.json b/homeassistant/components/steam_online/translations/ru.json index 8a6ea715455..0da65eda473 100644 --- a/homeassistant/components/steam_online/translations/ru.json +++ b/homeassistant/components/steam_online/translations/ru.json @@ -12,7 +12,7 @@ }, "step": { "reauth_confirm": { - "description": "\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0439\u0442\u0438 \u0441\u0432\u043e\u0439 \u043a\u043b\u044e\u0447 \u0437\u0434\u0435\u0441\u044c: https://steamcommunity.com/dev/apikey", + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f. \n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0439\u0442\u0438 \u0441\u0432\u043e\u0439 \u043a\u043b\u044e\u0447 \u0437\u0434\u0435\u0441\u044c: {api_key_url}", "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": { @@ -20,7 +20,7 @@ "account": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Steam", "api_key": "\u041a\u043b\u044e\u0447 API" }, - "description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 https://steamid.io, \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0439\u0442\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0441\u0432\u043e\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Steam." + "description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 {account_id_url}, \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0439\u0442\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0441\u0432\u043e\u0435\u0433\u043e \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 Steam." } } }, diff --git a/homeassistant/components/tailscale/translations/ko.json b/homeassistant/components/tailscale/translations/ko.json new file mode 100644 index 00000000000..cd273d60ca0 --- /dev/null +++ b/homeassistant/components/tailscale/translations/ko.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "Tailscale API \ud1a0\ud070\uc740 90\uc77c \ub3d9\uc548 \uc720\ud6a8\ud569\ub2c8\ub2e4. https://login.tailscale.com/admin/settings/authkeys\uc5d0\uc11c \uc0c8\ub85c\uc6b4 Tailscale API \ud0a4\ub97c \uc0dd\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "user": { + "data": { + "tailnet": "\ud14c\uc77c\ub137" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/ja.json b/homeassistant/components/tautulli/translations/ja.json new file mode 100644 index 00000000000..854dc593a55 --- /dev/null +++ b/homeassistant/components/tautulli/translations/ja.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API\u30ad\u30fc" + } + }, + "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "url": "URL", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/ko.json b/homeassistant/components/totalconnect/translations/ko.json index 354522154b5..246b62d5500 100644 --- a/homeassistant/components/totalconnect/translations/ko.json +++ b/homeassistant/components/totalconnect/translations/ko.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "no_locations": "\uc774 \uc0ac\uc6a9\uc790\uac00 \uc0ac\uc6a9\ud560 \uc218 \uc788\ub294 \uc704\uce58\uac00 \uc5c6\uc2b5\ub2c8\ub2e4. TotalConnect \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694.", "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "error": { diff --git a/homeassistant/components/tplink/translations/ko.json b/homeassistant/components/tplink/translations/ko.json index a1bfb59ca07..01ae48031e4 100644 --- a/homeassistant/components/tplink/translations/ko.json +++ b/homeassistant/components/tplink/translations/ko.json @@ -7,6 +7,9 @@ "step": { "confirm": { "description": "TP-Link \uc2a4\ub9c8\ud2b8 \uae30\uae30\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + }, + "discovery_confirm": { + "description": "{name} {model} ( {host} )\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } } diff --git a/homeassistant/components/tractive/translations/sensor.ko.json b/homeassistant/components/tractive/translations/sensor.ko.json new file mode 100644 index 00000000000..459ec08bec5 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.ko.json @@ -0,0 +1,9 @@ +{ + "state": { + "tractive__tracker_state": { + "operational": "\uc6b4\uc601", + "system_shutdown_user": "\uc2dc\uc2a4\ud15c \uc885\ub8cc \uc0ac\uc6a9\uc790", + "system_startup": "\uc2dc\uc2a4\ud15c \uc2dc\uc791" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/ja.json b/homeassistant/components/trafikverket_ferry/translations/ja.json new file mode 100644 index 00000000000..2377e33a60c --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/ja.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API\u30ad\u30fc" + } + }, + "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "time": "\u6642\u9593", + "weekday": "\u5e73\u65e5" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/ko.json b/homeassistant/components/trafikverket_weatherstation/translations/ko.json new file mode 100644 index 00000000000..97d9717113c --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "invalid_station": "\uc9c0\uc815\ub41c \uc774\ub984\uc758 \uae30\uc0c1 \uad00\uce21\uc18c\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "more_stations": "\uc9c0\uc815\ub41c \uc774\ub984\uc744 \uac00\uc9c4 \uc5ec\ub7ec \uae30\uc0c1 \uad00\uce21\uc18c\ub97c \ucc3e\uc558\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "station": "\uc2a4\ud14c\uc774\uc158" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.ko.json b/homeassistant/components/tuya/translations/select.ko.json new file mode 100644 index 00000000000..022505e76e5 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.ko.json @@ -0,0 +1,12 @@ +{ + "state": { + "tuya__basic_anti_flickr": { + "1": "50Hz", + "2": "60Hz" + }, + "tuya__fingerbot_mode": { + "click": "\ud478\uc2dc", + "switch": "\uc2a4\uc704\uce58" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/ca.json b/homeassistant/components/ukraine_alarm/translations/ca.json new file mode 100644 index 00000000000..bee49c8e4a1 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/ca.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_api_key": "Clau API inv\u00e0lida", + "timeout": "Temps m\u00e0xim d'espera per establir la connexi\u00f3 esgotat", + "unknown": "Error inesperat" + }, + "step": { + "community": { + "data": { + "region": "Regi\u00f3" + }, + "description": "Si vols monitorar no nom\u00e9s l'estat i el districte, tria la comunitat espec\u00edfica" + }, + "district": { + "data": { + "region": "Regi\u00f3" + }, + "description": "Si vols monitorar no nom\u00e9s l'estat, tria el districte espec\u00edfic" + }, + "state": { + "data": { + "region": "Regi\u00f3" + }, + "description": "Escull l'estat a monitorar" + }, + "user": { + "data": { + "api_key": "Clau API" + }, + "description": "Configura la integraci\u00f3 d'Alarma Ucraina. Per generar la clau API, v\u00e9s a {api_url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/et.json b/homeassistant/components/ukraine_alarm/translations/et.json new file mode 100644 index 00000000000..45649336837 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/et.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Asukoht on juba m\u00e4\u00e4ratud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_api_key": "Vigane API v\u00f5ti", + "timeout": "\u00dchendamise ajal\u00f5pp", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "community": { + "data": { + "region": "Piirkond" + }, + "description": "Kui soovid j\u00e4lgida mitte ainult oblastit ja piirkonda vali konkreetne asukoht" + }, + "district": { + "data": { + "region": "Piirkond" + }, + "description": "Kui soovid j\u00e4lgida mitte ainult oblastit vali konkreetne piirkond" + }, + "state": { + "data": { + "region": "Piirkond" + }, + "description": "Vali j\u00e4lgitav oblast" + }, + "user": { + "data": { + "api_key": "API v\u00f5ti" + }, + "description": "Loo Ukraina h\u00e4ire integreerimine. API-v\u00f5tme loomiseks ava {api_url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/id.json b/homeassistant/components/ukraine_alarm/translations/id.json new file mode 100644 index 00000000000..6c10857bc80 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/id.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Lokasi sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_api_key": "Kunci API tidak valid", + "timeout": "Tenggang waktu membuat koneksi habis", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "community": { + "data": { + "region": "Wilayah" + }, + "description": "Jika ingin memantau tidak hanya negara bagian dan distrik, pilih komunitas tertentu" + }, + "district": { + "data": { + "region": "Wilayah" + }, + "description": "Jika ingin memantau tidak hanya negara bagian, pilih distrik tertentu" + }, + "state": { + "data": { + "region": "Wilayah" + }, + "description": "Pilih negara bagian untuk dipantau" + }, + "user": { + "data": { + "api_key": "Kunci API" + }, + "description": "Siapkan integrasi Alarm Ukraina. Untuk membuat kunci API, buka {api_url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/it.json b/homeassistant/components/ukraine_alarm/translations/it.json new file mode 100644 index 00000000000..23d0f2a12a5 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/it.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "La posizione \u00e8 gi\u00e0 configurata" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_api_key": "Chiave API non valida", + "timeout": "Tempo scaduto per stabile la connessione.", + "unknown": "Errore imprevisto" + }, + "step": { + "community": { + "data": { + "region": "Regione" + }, + "description": "Se vuoi monitorare non solo stato e distretto, scegli la sua comunit\u00e0 specifica" + }, + "district": { + "data": { + "region": "Regione" + }, + "description": "Se vuoi monitorare non solo lo stato, scegli il suo distretto specifico" + }, + "state": { + "data": { + "region": "Regione" + }, + "description": "Scegli lo stato da monitorare" + }, + "user": { + "data": { + "api_key": "Chiave API" + }, + "description": "Imposta l'integrazione di Ukraine Alarm. Per generare una chiave API vai su {api_url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/ja.json b/homeassistant/components/ukraine_alarm/translations/ja.json new file mode 100644 index 00000000000..519bf3df072 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/ja.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", + "timeout": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "community": { + "data": { + "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" + } + }, + "district": { + "data": { + "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" + } + }, + "state": { + "data": { + "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" + } + }, + "user": { + "data": { + "api_key": "API\u30ad\u30fc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/nl.json b/homeassistant/components/ukraine_alarm/translations/nl.json new file mode 100644 index 00000000000..6ec93b0526d --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/nl.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Locatie is al geconfigureerd." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_api_key": "Ongeldige API-sleutel", + "timeout": "Time-out bij het maken van verbinding", + "unknown": "Onverwachte fout" + }, + "step": { + "community": { + "data": { + "region": "Regio" + }, + "description": "Als u niet alleen de staat en het district wilt volgen, kiest u de specifieke gemeenschap ervan" + }, + "district": { + "data": { + "region": "Regio" + }, + "description": "Als u niet alleen de staat wilt controleren, kies dan het specifieke district" + }, + "state": { + "data": { + "region": "Regio" + }, + "description": "Kies staat om te monitoren" + }, + "user": { + "data": { + "api_key": "API-sleutel" + }, + "description": "Stel de Oekra\u00efne Alarm integratie in. Om een API sleutel te genereren ga naar {api_url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/no.json b/homeassistant/components/ukraine_alarm/translations/no.json new file mode 100644 index 00000000000..b7ee3275920 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/no.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Plasseringen er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_api_key": "Ugyldig API-n\u00f8kkel", + "timeout": "Tidsavbrudd oppretter forbindelse", + "unknown": "Uventet feil" + }, + "step": { + "community": { + "data": { + "region": "Region" + }, + "description": "Hvis du ikke bare vil overv\u00e5ke stat og distrikt, velg dets spesifikke fellesskap" + }, + "district": { + "data": { + "region": "Region" + }, + "description": "Hvis du ikke bare vil overv\u00e5ke staten, velg dens spesifikke distrikt" + }, + "state": { + "data": { + "region": "Region" + }, + "description": "Velg tilstand \u00e5 overv\u00e5ke" + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "description": "Sett opp Ukraina Alarm-integrasjonen. For \u00e5 generere en API-n\u00f8kkel, g\u00e5 til {api_url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/pl.json b/homeassistant/components/ukraine_alarm/translations/pl.json new file mode 100644 index 00000000000..1cccde50083 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/pl.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_api_key": "Nieprawid\u0142owy klucz API", + "timeout": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "community": { + "data": { + "region": "Region" + } + }, + "district": { + "data": { + "region": "Region" + } + }, + "state": { + "data": { + "region": "Region" + } + }, + "user": { + "data": { + "api_key": "Klucz API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/pt-BR.json b/homeassistant/components/ukraine_alarm/translations/pt-BR.json new file mode 100644 index 00000000000..92c5eb20b6d --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/pt-BR.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "O local j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_api_key": "Chave de API inv\u00e1lida", + "timeout": "Tempo limite estabelecendo conex\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "community": { + "data": { + "region": "Regi\u00e3o" + }, + "description": "Se voc\u00ea deseja monitorar n\u00e3o apenas estado e distrito, escolha sua comunidade espec\u00edfica" + }, + "district": { + "data": { + "region": "Regi\u00e3o" + }, + "description": "Se voc\u00ea deseja monitorar n\u00e3o apenas o estado, escolha seu distrito espec\u00edfico" + }, + "state": { + "data": { + "region": "Regi\u00e3o" + }, + "description": "Escolha o estado para monitorar" + }, + "user": { + "data": { + "api_key": "Chave de API" + }, + "description": "Configure a integra\u00e7\u00e3o do Alarme da Ucr\u00e2nia. Para gerar uma chave de API, acesse {api_url}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/zh-Hant.json b/homeassistant/components/ukraine_alarm/translations/zh-Hant.json new file mode 100644 index 00000000000..0704a45dd56 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/zh-Hant.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_api_key": "API \u91d1\u9470\u7121\u6548", + "timeout": "\u5efa\u7acb\u9023\u7dda\u903e\u6642", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "community": { + "data": { + "region": "\u5340\u57df" + }, + "description": "\u5047\u5982\u4f60\u4e0d\u53ea\u8981\u76e3\u770b\u72c0\u614b\u8207\u5340\u57df\u3001\u8acb\u9078\u64c7\u7279\u5b9a\u793e\u5340" + }, + "district": { + "data": { + "region": "\u5340\u57df" + }, + "description": "\u5047\u5982\u4f60\u4e0d\u53ea\u8981\u76e3\u770b\u72c0\u614b\u3001\u8acb\u9078\u64c7\u7279\u5b9a\u5340\u57df" + }, + "state": { + "data": { + "region": "\u5340\u57df" + }, + "description": "\u9078\u64c7\u6240\u8981\u76e3\u8996\u7684\u72c0\u614b" + }, + "user": { + "data": { + "api_key": "API \u91d1\u9470" + }, + "description": "\u8a2d\u5b9a\u70cf\u514b\u862d\u8b66\u5831\u6574\u5408\uff0c\u8acb\u9023\u7dda\u81f3 {api_url} \u4ee5\u53d6\u5f97 API \u91d1\u9470\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/nl.json b/homeassistant/components/unifi/translations/nl.json index 8b67bec0364..637dbd480f3 100644 --- a/homeassistant/components/unifi/translations/nl.json +++ b/homeassistant/components/unifi/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Unifi Network site is al geconfigureerd", - "configuration_updated": "Configuratie bijgewerkt.", + "configuration_updated": "Configuratie bijgewerkt", "reauth_successful": "Herauthenticatie was succesvol" }, "error": { diff --git a/homeassistant/components/vulcan/translations/id.json b/homeassistant/components/vulcan/translations/id.json index a15ed1772d2..c372ca0ebe5 100644 --- a/homeassistant/components/vulcan/translations/id.json +++ b/homeassistant/components/vulcan/translations/id.json @@ -3,6 +3,7 @@ "abort": { "all_student_already_configured": "Semua siswa telah ditambahkan.", "already_configured": "Siswa tersebut telah ditambahkan.", + "no_matching_entries": "Tidak ditemukan entri yang cocok, harap gunakan akun lain atau hapus integrasi dengan siswa yang ketinggalan zaman..", "reauth_successful": "Otorisasi ulang berhasil" }, "error": { @@ -37,6 +38,14 @@ }, "description": "Masuk ke Akun Vulcan Anda menggunakan halaman pendaftaran aplikasi seluler." }, + "reauth_confirm": { + "data": { + "pin": "PIN", + "region": "Simbol", + "token": "Token" + }, + "description": "Masuk ke Akun Vulcan Anda menggunakan halaman pendaftaran aplikasi seluler." + }, "select_saved_credentials": { "data": { "credentials": "Masuk" diff --git a/homeassistant/components/vulcan/translations/ja.json b/homeassistant/components/vulcan/translations/ja.json index 98363f12f13..1f97968e9ae 100644 --- a/homeassistant/components/vulcan/translations/ja.json +++ b/homeassistant/components/vulcan/translations/ja.json @@ -37,6 +37,13 @@ }, "description": "\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u767b\u9332\u30da\u30fc\u30b8\u3092\u4f7f\u7528\u3057\u3066\u3001Vulcan\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3059\u3002" }, + "reauth_confirm": { + "data": { + "pin": "\u30d4\u30f3", + "region": "\u30b7\u30f3\u30dc\u30eb", + "token": "\u30c8\u30fc\u30af\u30f3" + } + }, "select_saved_credentials": { "data": { "credentials": "\u30ed\u30b0\u30a4\u30f3" diff --git a/homeassistant/components/wallbox/translations/ko.json b/homeassistant/components/wallbox/translations/ko.json new file mode 100644 index 00000000000..7c532be8890 --- /dev/null +++ b/homeassistant/components/wallbox/translations/ko.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "reauth_invalid": "\uc7ac\uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. \uc2dc\ub9ac\uc5bc \ubc88\ud638\uac00 \uc6d0\ubcf8\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/ca.json b/homeassistant/components/ws66i/translations/ca.json new file mode 100644 index 00000000000..789edb86fb2 --- /dev/null +++ b/homeassistant/components/ws66i/translations/ca.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "ip_address": "Adre\u00e7a IP" + }, + "title": "Connexi\u00f3 amb el dispositiu" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Nom de la font #1", + "source_2": "Nom de la font #2", + "source_3": "Nom de la font #3", + "source_4": "Nom de la font #4", + "source_5": "Nom de la font #5", + "source_6": "Nom de la font #6" + }, + "title": "Configuraci\u00f3 de les fonts" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/de.json b/homeassistant/components/ws66i/translations/de.json new file mode 100644 index 00000000000..cab3e062d8e --- /dev/null +++ b/homeassistant/components/ws66i/translations/de.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-Adresse" + }, + "title": "Verbinden mit dem Ger\u00e4t" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Name der Quelle #1", + "source_2": "Name der Quelle #2", + "source_3": "Name der Quelle #3", + "source_4": "Name der Quelle #4", + "source_5": "Name der Quelle #5", + "source_6": "Name der Quelle #6" + }, + "title": "Quellen konfigurieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/el.json b/homeassistant/components/ws66i/translations/el.json new file mode 100644 index 00000000000..4a1365c3a77 --- /dev/null +++ b/homeassistant/components/ws66i/translations/el.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" + }, + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b7\u03b3\u03ae\u03c2 #1", + "source_2": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b7\u03b3\u03ae\u03c2 #2", + "source_3": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b7\u03b3\u03ae\u03c2 #3", + "source_4": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b7\u03b3\u03ae\u03c2 #4", + "source_5": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b7\u03b3\u03ae\u03c2 #5", + "source_6": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03b7\u03b3\u03ae\u03c2 #6" + }, + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c0\u03b7\u03b3\u03ce\u03bd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/et.json b/homeassistant/components/ws66i/translations/et.json new file mode 100644 index 00000000000..83b238d74f5 --- /dev/null +++ b/homeassistant/components/ws66i/translations/et.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "ip_address": "IP aadress" + }, + "title": "\u00dchendu seadmega" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Sisendi nr 1 nimi", + "source_2": "Sisendi nr 2 nimi", + "source_3": "Sisendi nr 3 nimi", + "source_4": "Sisendi nr 4 nimi", + "source_5": "Sisendi nr 5 nimi", + "source_6": "Sisendi nr 6 nimi" + }, + "title": "Seadista sisendid" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/hu.json b/homeassistant/components/ws66i/translations/hu.json new file mode 100644 index 00000000000..9d6585e8f6b --- /dev/null +++ b/homeassistant/components/ws66i/translations/hu.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "ip_address": "IP c\u00edm" + }, + "title": "Csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Forr\u00e1s neve #1", + "source_2": "Forr\u00e1s neve #2", + "source_3": "Forr\u00e1s neve #3", + "source_4": "Forr\u00e1s neve #4", + "source_5": "Forr\u00e1s neve #5", + "source_6": "Forr\u00e1s neve #6" + }, + "title": "Forr\u00e1sok konfigur\u00e1l\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/id.json b/homeassistant/components/ws66i/translations/id.json new file mode 100644 index 00000000000..54cb3043a11 --- /dev/null +++ b/homeassistant/components/ws66i/translations/id.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "ip_address": "Alamat IP" + }, + "title": "Hubungkan ke perangkat" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Nama sumber #1", + "source_2": "Nama sumber #2", + "source_3": "Nama sumber #3", + "source_4": "Nama sumber #4", + "source_5": "Nama sumber #5", + "source_6": "Nama sumber #6" + }, + "title": "Konfigurasikan sumber" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/it.json b/homeassistant/components/ws66i/translations/it.json new file mode 100644 index 00000000000..c98714c98ad --- /dev/null +++ b/homeassistant/components/ws66i/translations/it.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "ip_address": "Indirizzo IP" + }, + "title": "Connettiti al dispositivo" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Nome della fonte n. 1", + "source_2": "Nome della fonte n. 2", + "source_3": "Nome della fonte n. 3", + "source_4": "Nome della fonte n. 4", + "source_5": "Nome della fonte n. 5", + "source_6": "Nome della fonte n. 6" + }, + "title": "Configura le fonti" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/ja.json b/homeassistant/components/ws66i/translations/ja.json new file mode 100644 index 00000000000..2f93c93954b --- /dev/null +++ b/homeassistant/components/ws66i/translations/ja.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "ip_address": "IP\u30a2\u30c9\u30ec\u30b9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/nl.json b/homeassistant/components/ws66i/translations/nl.json new file mode 100644 index 00000000000..0ff24636b88 --- /dev/null +++ b/homeassistant/components/ws66i/translations/nl.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-adres" + }, + "title": "Verbinding maken met het apparaat" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Naam van bron #1", + "source_2": "Naam van bron #2", + "source_3": "Naam van bron #3", + "source_4": "Naam van bron #4", + "source_5": "Naam van bron #5", + "source_6": "Naam van bron #6" + }, + "title": "Configureer bronnen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/no.json b/homeassistant/components/ws66i/translations/no.json new file mode 100644 index 00000000000..fa132ab681a --- /dev/null +++ b/homeassistant/components/ws66i/translations/no.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresse" + }, + "title": "Koble til enheten" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Navn p\u00e5 kilde #1", + "source_2": "Navn p\u00e5 kilde #2", + "source_3": "Navn p\u00e5 kilde #3", + "source_4": "Navn p\u00e5 kilde #4", + "source_5": "Navn p\u00e5 kilde #5", + "source_6": "Navn p\u00e5 kilde #6" + }, + "title": "Konfigurer kilder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/pl.json b/homeassistant/components/ws66i/translations/pl.json new file mode 100644 index 00000000000..1fc8c8b1cc9 --- /dev/null +++ b/homeassistant/components/ws66i/translations/pl.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "ip_address": "Adres IP" + }, + "title": "Po\u0142\u0105czenie z urz\u0105dzeniem" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Nazwa \u017ar\u00f3d\u0142a #1", + "source_2": "Nazwa \u017ar\u00f3d\u0142a #2", + "source_3": "Nazwa \u017ar\u00f3d\u0142a #3", + "source_4": "Nazwa \u017ar\u00f3d\u0142a #4", + "source_5": "Nazwa \u017ar\u00f3d\u0142a #5", + "source_6": "Nazwa \u017ar\u00f3d\u0142a #6" + }, + "title": "Konfiguracja \u017ar\u00f3de\u0142" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/pt-BR.json b/homeassistant/components/ws66i/translations/pt-BR.json new file mode 100644 index 00000000000..d440aab3aa4 --- /dev/null +++ b/homeassistant/components/ws66i/translations/pt-BR.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP" + }, + "title": "Conecte-se ao dispositivo" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Nome da fonte #1", + "source_2": "Nome da fonte #2", + "source_3": "Nome da fonte #3", + "source_4": "Nome da fonte #4", + "source_5": "Nome da fonte #5", + "source_6": "Nome da fonte #6" + }, + "title": "Configurar fontes" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/ru.json b/homeassistant/components/ws66i/translations/ru.json new file mode 100644 index 00000000000..b7e244cf2b0 --- /dev/null +++ b/homeassistant/components/ws66i/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": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441" + }, + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 \u21161", + "source_2": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 \u21162", + "source_3": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 \u21163", + "source_4": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 \u21164", + "source_5": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 \u21165", + "source_6": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 \u21166" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/zh-Hant.json b/homeassistant/components/ws66i/translations/zh-Hant.json new file mode 100644 index 00000000000..a583ac2217f --- /dev/null +++ b/homeassistant/components/ws66i/translations/zh-Hant.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u4f4d\u5740" + }, + "title": "\u9023\u7dda\u81f3\u88dd\u7f6e" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "\u4f86\u6e90 #1 \u540d\u7a31", + "source_2": "\u4f86\u6e90 #2 \u540d\u7a31", + "source_3": "\u4f86\u6e90 #3 \u540d\u7a31", + "source_4": "\u4f86\u6e90 #4 \u540d\u7a31", + "source_5": "\u4f86\u6e90 #5 \u540d\u7a31", + "source_6": "\u4f86\u6e90 #6 \u540d\u7a31" + }, + "title": "\u8a2d\u5b9a\u4f86\u6e90" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/ko.json b/homeassistant/components/yale_smart_alarm/translations/ko.json new file mode 100644 index 00000000000..b213fdda32c --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/ko.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "lock_code_digits": "\ud540\ucf54\ub4dc \uc22b\uc790 \uac1c\uc218" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/ko.json b/homeassistant/components/zwave_js/translations/ko.json index 08e3dc8c7d7..2c14397177e 100644 --- a/homeassistant/components/zwave_js/translations/ko.json +++ b/homeassistant/components/zwave_js/translations/ko.json @@ -51,5 +51,12 @@ } } }, + "device_automation": { + "action_type": { + "set_config_parameter": "\uad6c\uc131 \ub9e4\uac1c\ubcc0\uc218 {subtype} \uc758 \uac12 \uc124\uc815", + "set_lock_usercode": "{entity_name} \uc5d0 \uc0ac\uc6a9\uc790 \ucf54\ub4dc \uc124\uc815", + "set_value": "Z-Wave Value\uc758 \uc124\uc815\uac12" + } + }, "title": "Z-Wave JS" } \ No newline at end of file From bf77c000eaf4b3e8ae6e607481f6a5a75cad9376 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 May 2022 23:00:19 -0500 Subject: [PATCH 0364/3516] Complete baked query conversion for recorder.history (#71618) --- homeassistant/components/recorder/history.py | 246 ++++++++++--------- 1 file changed, 131 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index d221ced3a84..a061bcd1329 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -11,6 +11,8 @@ from typing import Any, cast from sqlalchemy import Column, Text, and_, bindparam, func, or_ from sqlalchemy.ext import baked +from sqlalchemy.ext.baked import BakedQuery +from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal @@ -104,26 +106,6 @@ QUERY_STATES_NO_LAST_UPDATED = [ HISTORY_BAKERY = "recorder_history_bakery" -def query_and_join_attributes( - hass: HomeAssistant, no_attributes: bool -) -> tuple[list[Column], bool]: - """Return the query keys and if StateAttributes should be joined.""" - # If no_attributes was requested we do the query - # without the attributes fields and do not join the - # state_attributes table - if no_attributes: - return QUERY_STATE_NO_ATTR, False - # If we in the process of migrating schema we do - # not want to join the state_attributes table as we - # do not know if it will be there yet - if recorder.get_instance(hass).schema_version < 25: - return QUERY_STATES_PRE_SCHEMA_25, False - # Finally if no migration is in progress and no_attributes - # was not requested, we query both attributes columns and - # join state_attributes - return QUERY_STATES, True - - def bake_query_and_join_attributes( hass: HomeAssistant, no_attributes: bool, include_last_updated: bool = True ) -> tuple[Any, bool]: @@ -138,9 +120,9 @@ def bake_query_and_join_attributes( # state_attributes table if no_attributes: if include_last_updated: - return bakery(lambda session: session.query(*QUERY_STATE_NO_ATTR)), False + return bakery(lambda s: s.query(*QUERY_STATE_NO_ATTR)), False return ( - bakery(lambda session: session.query(*QUERY_STATE_NO_ATTR_NO_LAST_UPDATED)), + bakery(lambda s: s.query(*QUERY_STATE_NO_ATTR_NO_LAST_UPDATED)), False, ) # If we in the process of migrating schema we do @@ -149,23 +131,19 @@ def bake_query_and_join_attributes( if recorder.get_instance(hass).schema_version < 25: if include_last_updated: return ( - bakery(lambda session: session.query(*QUERY_STATES_PRE_SCHEMA_25)), + bakery(lambda s: s.query(*QUERY_STATES_PRE_SCHEMA_25)), False, ) return ( - bakery( - lambda session: session.query( - *QUERY_STATES_PRE_SCHEMA_25_NO_LAST_UPDATED - ) - ), + bakery(lambda s: s.query(*QUERY_STATES_PRE_SCHEMA_25_NO_LAST_UPDATED)), False, ) # Finally if no migration is in progress and no_attributes # was not requested, we query both attributes columns and # join state_attributes if include_last_updated: - return bakery(lambda session: session.query(*QUERY_STATES)), True - return bakery(lambda session: session.query(*QUERY_STATES_NO_LAST_UPDATED)), True + return bakery(lambda s: s.query(*QUERY_STATES)), True + return bakery(lambda s: s.query(*QUERY_STATES_NO_LAST_UPDATED)), True def async_setup(hass: HomeAssistant) -> None: @@ -200,6 +178,18 @@ def get_significant_states( ) +def _ignore_domains_filter(query: Query) -> Query: + """Add a filter to ignore domains we do not fetch history for.""" + return query.filter( + and_( + *[ + ~States.entity_id.like(entity_domain) + for entity_domain in IGNORE_DOMAINS_ENTITY_ID_LIKE + ] + ) + ) + + def _query_significant_states_with_session( hass: HomeAssistant, session: Session, @@ -243,14 +233,7 @@ def _query_significant_states_with_session( States.entity_id.in_(bindparam("entity_ids", expanding=True)) ) else: - baked_query += lambda q: q.filter( - and_( - *[ - ~States.entity_id.like(entity_domain) - for entity_domain in IGNORE_DOMAINS_ENTITY_ID_LIKE - ] - ) - ) + baked_query += _ignore_domains_filter if filters: filters.bake(baked_query) @@ -470,6 +453,94 @@ def get_last_state_changes( ) +def _most_recent_state_ids_entities_subquery(query: Query) -> Query: + """Query to find the most recent state id for specific entities.""" + # We got an include-list of entities, accelerate the query by filtering already + # in the inner query. + most_recent_state_ids = ( + query.session.query(func.max(States.state_id).label("max_state_id")) + .filter( + (States.last_updated >= bindparam("run_start")) + & (States.last_updated < bindparam("utc_point_in_time")) + ) + .filter(States.entity_id.in_(bindparam("entity_ids", expanding=True))) + .group_by(States.entity_id) + .subquery() + ) + return query.join( + most_recent_state_ids, + States.state_id == most_recent_state_ids.c.max_state_id, + ) + + +def _get_states_baked_query_for_entites( + hass: HomeAssistant, + no_attributes: bool = False, +) -> BakedQuery: + """Baked query to get states for specific entities.""" + baked_query, join_attributes = bake_query_and_join_attributes(hass, no_attributes) + baked_query += _most_recent_state_ids_entities_subquery + if join_attributes: + baked_query += lambda q: q.outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) + return baked_query + + +def _most_recent_state_ids_subquery(query: Query) -> Query: + """Find the most recent state ids for all entiites.""" + # We did not get an include-list of entities, query all states in the inner + # query, then filter out unwanted domains as well as applying the custom filter. + # This filtering can't be done in the inner query because the domain column is + # not indexed and we can't control what's in the custom filter. + most_recent_states_by_date = ( + query.session.query( + States.entity_id.label("max_entity_id"), + func.max(States.last_updated).label("max_last_updated"), + ) + .filter( + (States.last_updated >= bindparam("run_start")) + & (States.last_updated < bindparam("utc_point_in_time")) + ) + .group_by(States.entity_id) + .subquery() + ) + most_recent_state_ids = ( + query.session.query(func.max(States.state_id).label("max_state_id")) + .join( + most_recent_states_by_date, + and_( + States.entity_id == most_recent_states_by_date.c.max_entity_id, + States.last_updated == most_recent_states_by_date.c.max_last_updated, + ), + ) + .group_by(States.entity_id) + .subquery() + ) + return query.join( + most_recent_state_ids, + States.state_id == most_recent_state_ids.c.max_state_id, + ) + + +def _get_states_baked_query_for_all( + hass: HomeAssistant, + filters: Any | None = None, + no_attributes: bool = False, +) -> BakedQuery: + """Baked query to get states for all entities.""" + baked_query, join_attributes = bake_query_and_join_attributes(hass, no_attributes) + baked_query += _most_recent_state_ids_subquery + baked_query += _ignore_domains_filter + if filters: + filters.bake(baked_query) + if join_attributes: + baked_query += lambda q: q.outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) + return baked_query + + def _get_states_with_session( hass: HomeAssistant, session: Session, @@ -494,77 +565,22 @@ def _get_states_with_session( # We have more than one entity to look at so we need to do a query on states # since the last recorder run started. - query_keys, join_attributes = query_and_join_attributes(hass, no_attributes) - query = session.query(*query_keys) - if entity_ids: - # We got an include-list of entities, accelerate the query by filtering already - # in the inner query. - most_recent_state_ids = ( - session.query( - func.max(States.state_id).label("max_state_id"), - ) - .filter( - (States.last_updated >= run.start) - & (States.last_updated < utc_point_in_time) - ) - .filter(States.entity_id.in_(entity_ids)) - ) - most_recent_state_ids = most_recent_state_ids.group_by(States.entity_id) - most_recent_state_ids = most_recent_state_ids.subquery() - query = query.join( - most_recent_state_ids, - States.state_id == most_recent_state_ids.c.max_state_id, - ) - if join_attributes: - query = query.outerjoin( - StateAttributes, (States.attributes_id == StateAttributes.attributes_id) - ) + baked_query = _get_states_baked_query_for_entites(hass, no_attributes) else: - # We did not get an include-list of entities, query all states in the inner - # query, then filter out unwanted domains as well as applying the custom filter. - # This filtering can't be done in the inner query because the domain column is - # not indexed and we can't control what's in the custom filter. - most_recent_states_by_date = ( - session.query( - States.entity_id.label("max_entity_id"), - func.max(States.last_updated).label("max_last_updated"), - ) - .filter( - (States.last_updated >= run.start) - & (States.last_updated < utc_point_in_time) - ) - .group_by(States.entity_id) - .subquery() - ) - most_recent_state_ids = ( - session.query(func.max(States.state_id).label("max_state_id")) - .join( - most_recent_states_by_date, - and_( - States.entity_id == most_recent_states_by_date.c.max_entity_id, - States.last_updated - == most_recent_states_by_date.c.max_last_updated, - ), - ) - .group_by(States.entity_id) - .subquery() - ) - query = query.join( - most_recent_state_ids, - States.state_id == most_recent_state_ids.c.max_state_id, - ) - for entity_domain in IGNORE_DOMAINS_ENTITY_ID_LIKE: - query = query.filter(~States.entity_id.like(entity_domain)) - if filters: - query = filters.apply(query) - if join_attributes: - query = query.outerjoin( - StateAttributes, (States.attributes_id == StateAttributes.attributes_id) - ) + baked_query = _get_states_baked_query_for_all(hass, filters, no_attributes) attr_cache: dict[str, dict[str, Any]] = {} - return [LazyState(row, attr_cache) for row in execute(query)] + return [ + LazyState(row, attr_cache) + for row in execute( + baked_query(session).params( + run_start=run.start, + utc_point_in_time=utc_point_in_time, + entity_ids=entity_ids, + ) + ) + ] def _get_single_entity_states_with_session( @@ -585,8 +601,7 @@ def _get_single_entity_states_with_session( baked_query += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) - baked_query += lambda q: q.order_by(States.last_updated.desc()) - baked_query += lambda q: q.limit(1) + baked_query += lambda q: q.order_by(States.last_updated.desc()).limit(1) query = baked_query(session).params( utc_point_in_time=utc_point_in_time, entity_id=entity_id @@ -634,8 +649,8 @@ def _sorted_states_to_dict( filters=filters, no_attributes=no_attributes, ): - state.last_changed = start_time state.last_updated = start_time + state.last_changed = start_time result[state.entity_id].append(state) if _LOGGER.isEnabledFor(logging.DEBUG): @@ -671,31 +686,32 @@ def _sorted_states_to_dict( continue ent_results.append(LazyState(first_state, attr_cache)) - prev_state = ent_results[-1] - assert isinstance(prev_state, LazyState) + assert isinstance(ent_results[-1], State) + prev_state: Column | str = ent_results[-1].state initial_state_count = len(ent_results) + db_state = None for db_state in group: # With minimal response we do not care about attribute # changes so we can filter out duplicate states - if db_state.state == prev_state.state: + if (state := db_state.state) == prev_state: continue ent_results.append( { - STATE_KEY: db_state.state, + STATE_KEY: state, LAST_CHANGED_KEY: _process_timestamp_to_utc_isoformat( db_state.last_changed ), } ) - prev_state = db_state + prev_state = state - if prev_state and len(ent_results) != initial_state_count: + if db_state and len(ent_results) != initial_state_count: # There was at least one state change # replace the last minimal state with # a full state - ent_results[-1] = LazyState(prev_state, attr_cache) + ent_results[-1] = LazyState(db_state, attr_cache) # Filter out the empty lists if some states had 0 results. return {key: val for key, val in result.items() if val} From 0f41f569985944a6972d06da626fcace70ff8bdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 10 May 2022 09:59:10 +0200 Subject: [PATCH 0365/3516] Address late QNAP QSW strict typing comments (#71628) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/qnap_qsw/coordinator.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/qnap_qsw/coordinator.py b/homeassistant/components/qnap_qsw/coordinator.py index e3ca95076d0..7aa1b6f0a85 100644 --- a/homeassistant/components/qnap_qsw/coordinator.py +++ b/homeassistant/components/qnap_qsw/coordinator.py @@ -31,10 +31,9 @@ class QswUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL, - update_method=self._async_update, ) - async def _async_update(self) -> dict[str, Any]: + async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" async with async_timeout.timeout(QSW_TIMEOUT_SEC): try: From 46becd40235fe2077f4493c6c0511a1a2176448c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 10 May 2022 10:00:38 +0200 Subject: [PATCH 0366/3516] Address late Airzone strict typing comments (#71627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone/climate.py | 2 +- homeassistant/components/airzone/coordinator.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index e264df18e0c..ce67142547f 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -162,7 +162,7 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity): params[API_ON] = 1 await self._async_update_hvac_params(params) - async def async_set_temperature(self, **kwargs: dict[str, Any]) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" params = { API_SET_POINT: kwargs.get(ATTR_TEMPERATURE), diff --git a/homeassistant/components/airzone/coordinator.py b/homeassistant/components/airzone/coordinator.py index f9f7322a3eb..5ce9dcc45ef 100644 --- a/homeassistant/components/airzone/coordinator.py +++ b/homeassistant/components/airzone/coordinator.py @@ -31,10 +31,9 @@ class AirzoneUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL, - update_method=self._async_update, ) - async def _async_update(self) -> dict[str, Any]: + async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" async with async_timeout.timeout(AIOAIRZONE_DEVICE_TIMEOUT_SEC): try: From 275a90a2e84c07687f8f0fa7a1550b23bdd90aea Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 10 May 2022 12:46:02 +0200 Subject: [PATCH 0367/3516] Check state attributes in template light tests (#71608) --- tests/components/template/test_light.py | 388 ++++++++++++++++++++---- 1 file changed, 322 insertions(+), 66 deletions(-) diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 642fa5601cf..0888511c583 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -1,5 +1,4 @@ """The tests for the Template light platform.""" -import logging import pytest @@ -11,6 +10,12 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + SUPPORT_TRANSITION, + SUPPORT_WHITE_VALUE, + ColorMode, LightEntityFeature, ) from homeassistant.const import ( @@ -22,13 +27,15 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) -_LOGGER = logging.getLogger(__name__) - # Represent for light's availability _STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state" @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize( + "supported_features,supported_color_modes", + [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], +) @pytest.mark.parametrize( "config", [ @@ -59,12 +66,22 @@ _STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state" }, ], ) -async def test_template_state_invalid(hass, start_ha): +async def test_template_state_invalid( + hass, supported_features, supported_color_modes, start_ha +): """Test template state with render error.""" - assert hass.states.get("light.test_template_light").state == STATE_OFF + state = hass.states.get("light.test_template_light") + assert state.state == STATE_OFF + assert "color_mode" not in state.attributes + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize( + "supported_features,supported_color_modes", + [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], +) @pytest.mark.parametrize( "config", [ @@ -95,20 +112,39 @@ async def test_template_state_invalid(hass, start_ha): }, ], ) -async def test_template_state_text(hass, start_ha): +async def test_template_state_text( + hass, supported_features, supported_color_modes, start_ha +): """Test the state text of a template.""" - for set_state in [STATE_ON, STATE_OFF]: - hass.states.async_set("light.test_state", set_state) - await hass.async_block_till_done() - assert hass.states.get("light.test_template_light").state == set_state + set_state = STATE_ON + hass.states.async_set("light.test_state", set_state) + await hass.async_block_till_done() + state = hass.states.get("light.test_template_light") + assert state.state == set_state + assert state.attributes["color_mode"] == ColorMode.UNKNOWN # Brightness is None + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features + + set_state = STATE_OFF + hass.states.async_set("light.test_state", set_state) + await hass.async_block_till_done() + state = hass.states.get("light.test_template_light") + assert state.state == set_state + assert "color_mode" not in state.attributes + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) @pytest.mark.parametrize( - "config_addon,expected_state", + "supported_features,supported_color_modes", + [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], +) +@pytest.mark.parametrize( + "config_addon,expected_state,expected_color_mode", [ - ({"replace1": '"{{ 1 == 1 }}"'}, STATE_ON), - ({"replace1": '"{{ 1 == 2 }}"'}, STATE_OFF), + ({"replace1": '"{{ 1 == 1 }}"'}, STATE_ON, ColorMode.UNKNOWN), + ({"replace1": '"{{ 1 == 2 }}"'}, STATE_OFF, None), ], ) @pytest.mark.parametrize( @@ -141,9 +177,20 @@ async def test_template_state_text(hass, start_ha): }""", ], ) -async def test_templatex_state_boolean(hass, expected_state, start_ha): +async def test_templatex_state_boolean( + hass, + expected_color_mode, + expected_state, + supported_features, + supported_color_modes, + start_ha, +): """Test the setting of the state with boolean on.""" - assert hass.states.get("light.test_template_light").state == expected_state + state = hass.states.get("light.test_template_light") + assert state.state == expected_state + assert state.attributes.get("color_mode") == expected_color_mode + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features @pytest.mark.parametrize("count,domain", [(0, light.DOMAIN)]) @@ -247,6 +294,10 @@ async def test_missing_key(hass, count, start_ha): @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize( + "supported_features,supported_color_modes", + [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], +) @pytest.mark.parametrize( "config", [ @@ -274,13 +325,18 @@ async def test_missing_key(hass, count, start_ha): }, ], ) -async def test_on_action(hass, start_ha, calls): +async def test_on_action( + hass, start_ha, calls, supported_features, supported_color_modes +): """Test on action.""" hass.states.async_set("light.test_state", STATE_OFF) await hass.async_block_till_done() state = hass.states.get("light.test_template_light") assert state.state == STATE_OFF + assert "color_mode" not in state.attributes + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features await hass.services.async_call( light.DOMAIN, @@ -291,8 +347,17 @@ async def test_on_action(hass, start_ha, calls): assert len(calls) == 1 + assert state.state == STATE_OFF + assert "color_mode" not in state.attributes + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features + @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize( + "supported_features,supported_color_modes", + [(SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION, [ColorMode.BRIGHTNESS])], +) @pytest.mark.parametrize( "config", [ @@ -327,13 +392,18 @@ async def test_on_action(hass, start_ha, calls): }, ], ) -async def test_on_action_with_transition(hass, start_ha, calls): +async def test_on_action_with_transition( + hass, start_ha, calls, supported_features, supported_color_modes +): """Test on action with transition.""" hass.states.async_set("light.test_state", STATE_OFF) await hass.async_block_till_done() state = hass.states.get("light.test_template_light") assert state.state == STATE_OFF + assert "color_mode" not in state.attributes + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features await hass.services.async_call( light.DOMAIN, @@ -345,8 +415,17 @@ async def test_on_action_with_transition(hass, start_ha, calls): assert len(calls) == 1 assert calls[0].data["transition"] == 5 + assert state.state == STATE_OFF + assert "color_mode" not in state.attributes + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features + @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize( + "supported_features,supported_color_modes,expected_color_mode", + [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS], ColorMode.BRIGHTNESS)], +) @pytest.mark.parametrize( "config", [ @@ -373,13 +452,23 @@ async def test_on_action_with_transition(hass, start_ha, calls): }, ], ) -async def test_on_action_optimistic(hass, start_ha, calls): +async def test_on_action_optimistic( + hass, + start_ha, + calls, + supported_features, + supported_color_modes, + expected_color_mode, +): """Test on action with optimistic state.""" hass.states.async_set("light.test_state", STATE_OFF) await hass.async_block_till_done() state = hass.states.get("light.test_template_light") assert state.state == STATE_OFF + assert "color_mode" not in state.attributes + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features await hass.services.async_call( light.DOMAIN, @@ -391,9 +480,30 @@ async def test_on_action_optimistic(hass, start_ha, calls): state = hass.states.get("light.test_template_light") assert len(calls) == 1 assert state.state == STATE_ON + assert state.attributes["color_mode"] == ColorMode.UNKNOWN # Brightness is None + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_template_light", ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + + state = hass.states.get("light.test_template_light") + assert len(calls) == 1 + assert state.state == STATE_ON + assert state.attributes["color_mode"] == expected_color_mode + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize( + "supported_features,supported_color_modes", + [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], +) @pytest.mark.parametrize( "config", [ @@ -423,13 +533,18 @@ async def test_on_action_optimistic(hass, start_ha, calls): }, ], ) -async def test_off_action(hass, start_ha, calls): +async def test_off_action( + hass, start_ha, calls, supported_features, supported_color_modes +): """Test off action.""" hass.states.async_set("light.test_state", STATE_ON) await hass.async_block_till_done() state = hass.states.get("light.test_template_light") assert state.state == STATE_ON + assert state.attributes["color_mode"] == ColorMode.UNKNOWN # Brightness is None + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features await hass.services.async_call( light.DOMAIN, @@ -439,9 +554,17 @@ async def test_off_action(hass, start_ha, calls): ) assert len(calls) == 1 + assert state.state == STATE_ON + assert state.attributes["color_mode"] == ColorMode.UNKNOWN # Brightness is None + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize( + "supported_features,supported_color_modes", + [(SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION, [ColorMode.BRIGHTNESS])], +) @pytest.mark.parametrize( "config", [ @@ -476,13 +599,18 @@ async def test_off_action(hass, start_ha, calls): }, ], ) -async def test_off_action_with_transition(hass, start_ha, calls): +async def test_off_action_with_transition( + hass, start_ha, calls, supported_features, supported_color_modes +): """Test off action with transition.""" hass.states.async_set("light.test_state", STATE_ON) await hass.async_block_till_done() state = hass.states.get("light.test_template_light") assert state.state == STATE_ON + assert state.attributes["color_mode"] == ColorMode.UNKNOWN # Brightness is None + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features await hass.services.async_call( light.DOMAIN, @@ -493,9 +621,17 @@ async def test_off_action_with_transition(hass, start_ha, calls): assert len(calls) == 1 assert calls[0].data["transition"] == 2 + assert state.state == STATE_ON + assert state.attributes["color_mode"] == ColorMode.UNKNOWN # Brightness is None + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize( + "supported_features,supported_color_modes", + [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], +) @pytest.mark.parametrize( "config", [ @@ -522,10 +658,15 @@ async def test_off_action_with_transition(hass, start_ha, calls): }, ], ) -async def test_off_action_optimistic(hass, start_ha, calls): +async def test_off_action_optimistic( + hass, start_ha, calls, supported_features, supported_color_modes +): """Test off action with optimistic state.""" state = hass.states.get("light.test_template_light") assert state.state == STATE_OFF + assert "color_mode" not in state.attributes + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features await hass.services.async_call( light.DOMAIN, @@ -537,9 +678,16 @@ async def test_off_action_optimistic(hass, start_ha, calls): assert len(calls) == 1 state = hass.states.get("light.test_template_light") assert state.state == STATE_OFF + assert "color_mode" not in state.attributes + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize( + "supported_features,supported_color_modes,expected_color_mode", + [(SUPPORT_WHITE_VALUE, [ColorMode.RGBW], ColorMode.UNKNOWN)], +) @pytest.mark.parametrize( "config", [ @@ -570,7 +718,14 @@ async def test_off_action_optimistic(hass, start_ha, calls): }, ], ) -async def test_white_value_action_no_template(hass, start_ha, calls): +async def test_white_value_action_no_template( + hass, + start_ha, + calls, + supported_color_modes, + supported_features, + expected_color_mode, +): """Test setting white value with optimistic template.""" state = hass.states.get("light.test_template_light") assert state.attributes.get("white_value") is None @@ -586,8 +741,11 @@ async def test_white_value_action_no_template(hass, start_ha, calls): assert calls[0].data["white_value"] == 124 state = hass.states.get("light.test_template_light") - assert state is not None assert state.attributes.get("white_value") == 124 + assert state.state == STATE_ON + assert state.attributes["color_mode"] == expected_color_mode # hs_color is None + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features @pytest.mark.parametrize( @@ -601,6 +759,10 @@ async def test_white_value_action_no_template(hass, start_ha, calls): ], ) @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize( + "supported_features,supported_color_modes,expected_color_mode", + [(SUPPORT_WHITE_VALUE, [ColorMode.RGBW], ColorMode.UNKNOWN)], +) @pytest.mark.parametrize( "config", [ @@ -617,14 +779,29 @@ async def test_white_value_action_no_template(hass, start_ha, calls): }}}}""", ], ) -async def test_white_value_template(hass, expected_white_value, start_ha): +async def test_white_value_template( + hass, + expected_white_value, + start_ha, + supported_features, + supported_color_modes, + expected_color_mode, +): """Test the template for the white value.""" state = hass.states.get("light.test_template_light") assert state is not None assert state.attributes.get("white_value") == expected_white_value + assert state.state == STATE_ON + assert state.attributes["color_mode"] == expected_color_mode # hs_color is None + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize( + "supported_features,supported_color_modes,expected_color_mode", + [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS], ColorMode.BRIGHTNESS)], +) @pytest.mark.parametrize( "config", [ @@ -655,7 +832,14 @@ async def test_white_value_template(hass, expected_white_value, start_ha): }, ], ) -async def test_level_action_no_template(hass, start_ha, calls): +async def test_level_action_no_template( + hass, + start_ha, + calls, + supported_features, + supported_color_modes, + expected_color_mode, +): """Test setting brightness with optimistic template.""" state = hass.states.get("light.test_template_light") assert state.attributes.get("brightness") is None @@ -671,23 +855,33 @@ async def test_level_action_no_template(hass, start_ha, calls): assert calls[0].data["brightness"] == 124 state = hass.states.get("light.test_template_light") - _LOGGER.info(str(state.attributes)) - assert state is not None - assert state.attributes.get("brightness") == 124 + assert state.state == STATE_ON + assert state.attributes["brightness"] == 124 + assert state.attributes["color_mode"] == expected_color_mode + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) @pytest.mark.parametrize( - "expected_level,config_addon", + "expected_level,config_addon,expected_color_mode", [ - (255, {"replace4": '"{{255}}"'}), - (None, {"replace4": '"{{256}}"'}), - (None, {"replace4": '"{{x - 12}}"'}), - (None, {"replace4": '"{{ none }}"'}), - (None, {"replace4": '""'}), - (None, {"replace4": "\"{{ state_attr('light.nolight', 'brightness') }}\""}), + (255, {"replace4": '"{{255}}"'}, ColorMode.BRIGHTNESS), + (None, {"replace4": '"{{256}}"'}, ColorMode.UNKNOWN), + (None, {"replace4": '"{{x - 12}}"'}, ColorMode.UNKNOWN), + (None, {"replace4": '"{{ none }}"'}, ColorMode.UNKNOWN), + (None, {"replace4": '""'}, ColorMode.UNKNOWN), + ( + None, + {"replace4": "\"{{ state_attr('light.nolight', 'brightness') }}\""}, + ColorMode.UNKNOWN, + ), ], ) +@pytest.mark.parametrize( + "supported_features,supported_color_modes", + [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], +) @pytest.mark.parametrize( "config", [ @@ -702,25 +896,39 @@ async def test_level_action_no_template(hass, start_ha, calls): }}}}""", ], ) -async def test_level_template(hass, expected_level, start_ha): +async def test_level_template( + hass, + expected_level, + start_ha, + supported_features, + supported_color_modes, + expected_color_mode, +): """Test the template for the level.""" state = hass.states.get("light.test_template_light") - assert state is not None assert state.attributes.get("brightness") == expected_level + assert state.state == STATE_ON + assert state.attributes["color_mode"] == expected_color_mode + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) @pytest.mark.parametrize( - "expected_temp,config_addon", + "expected_temp,config_addon,expected_color_mode", [ - (500, {"replace5": '"{{500}}"'}), - (None, {"replace5": '"{{501}}"'}), - (None, {"replace5": '"{{x - 12}}"'}), - (None, {"replace5": '"None"'}), - (None, {"replace5": '"{{ none }}"'}), - (None, {"replace5": '""'}), + (500, {"replace5": '"{{500}}"'}, ColorMode.COLOR_TEMP), + (None, {"replace5": '"{{501}}"'}, ColorMode.UNKNOWN), + (None, {"replace5": '"{{x - 12}}"'}, ColorMode.UNKNOWN), + (None, {"replace5": '"None"'}, ColorMode.UNKNOWN), + (None, {"replace5": '"{{ none }}"'}, ColorMode.UNKNOWN), + (None, {"replace5": '""'}, ColorMode.UNKNOWN), ], ) +@pytest.mark.parametrize( + "supported_features,supported_color_modes", + [(SUPPORT_COLOR_TEMP, [ColorMode.COLOR_TEMP])], +) @pytest.mark.parametrize( "config", [ @@ -752,14 +960,28 @@ async def test_level_template(hass, expected_level, start_ha): }""" ], ) -async def test_temperature_template(hass, expected_temp, start_ha): +async def test_temperature_template( + hass, + expected_temp, + start_ha, + supported_features, + supported_color_modes, + expected_color_mode, +): """Test the template for the temperature.""" state = hass.states.get("light.test_template_light") - assert state is not None assert state.attributes.get("color_temp") == expected_temp + assert state.state == STATE_ON + assert state.attributes["color_mode"] == expected_color_mode + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize( + "supported_features,supported_color_modes,expected_color_mode", + [(SUPPORT_COLOR_TEMP, [ColorMode.COLOR_TEMP], ColorMode.COLOR_TEMP)], +) @pytest.mark.parametrize( "config", [ @@ -790,7 +1012,14 @@ async def test_temperature_template(hass, expected_temp, start_ha): }, ], ) -async def test_temperature_action_no_template(hass, start_ha, calls): +async def test_temperature_action_no_template( + hass, + start_ha, + calls, + supported_features, + supported_color_modes, + expected_color_mode, +): """Test setting temperature with optimistic template.""" state = hass.states.get("light.test_template_light") assert state.attributes.get("color_template") is None @@ -806,9 +1035,12 @@ async def test_temperature_action_no_template(hass, start_ha, calls): assert calls[0].data["color_temp"] == 345 state = hass.states.get("light.test_template_light") - _LOGGER.info(str(state.attributes)) assert state is not None assert state.attributes.get("color_temp") == 345 + assert state.state == STATE_ON + assert state.attributes["color_mode"] == expected_color_mode + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) @@ -949,6 +1181,10 @@ async def test_entity_picture_template(hass, start_ha): @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize( + "supported_features,supported_color_modes, expected_color_mode", + [(SUPPORT_COLOR, [ColorMode.HS], ColorMode.UNKNOWN)], +) @pytest.mark.parametrize( "config", [ @@ -990,7 +1226,14 @@ async def test_entity_picture_template(hass, start_ha): }, ], ) -async def test_color_action_no_template(hass, start_ha, calls): +async def test_color_action_no_template( + hass, + start_ha, + calls, + supported_features, + supported_color_modes, + expected_color_mode, +): """Test setting color with optimistic template.""" state = hass.states.get("light.test_template_light") assert state.attributes.get("hs_color") is None @@ -1009,28 +1252,31 @@ async def test_color_action_no_template(hass, start_ha, calls): assert calls[1].data["s"] == 50 state = hass.states.get("light.test_template_light") - _LOGGER.info(str(state.attributes)) - assert state is not None - assert calls[0].data["h"] == 40 - assert calls[0].data["s"] == 50 - assert calls[1].data["h"] == 40 - assert calls[1].data["s"] == 50 + assert state.state == STATE_ON + assert state.attributes["color_mode"] == expected_color_mode # hs_color is None + assert "hs_color" not in state.attributes + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) @pytest.mark.parametrize( - "expected_hs,config_addon", + "expected_hs,config_addon,expected_color_mode", [ - ((360, 100), {"replace6": '"{{(360, 100)}}"'}), - ((359.9, 99.9), {"replace6": '"{{(359.9, 99.9)}}"'}), - (None, {"replace6": '"{{(361, 100)}}"'}), - (None, {"replace6": '"{{(360, 101)}}"'}), - (None, {"replace6": '"[{{(360)}},{{null}}]"'}), - (None, {"replace6": '"{{x - 12}}"'}), - (None, {"replace6": '""'}), - (None, {"replace6": '"{{ none }}"'}), + ((360, 100), {"replace6": '"{{(360, 100)}}"'}, ColorMode.HS), + ((359.9, 99.9), {"replace6": '"{{(359.9, 99.9)}}"'}, ColorMode.HS), + (None, {"replace6": '"{{(361, 100)}}"'}, ColorMode.UNKNOWN), + (None, {"replace6": '"{{(360, 101)}}"'}, ColorMode.UNKNOWN), + (None, {"replace6": '"[{{(360)}},{{null}}]"'}, ColorMode.UNKNOWN), + (None, {"replace6": '"{{x - 12}}"'}, ColorMode.UNKNOWN), + (None, {"replace6": '""'}, ColorMode.UNKNOWN), + (None, {"replace6": '"{{ none }}"'}, ColorMode.UNKNOWN), ], ) +@pytest.mark.parametrize( + "supported_features,supported_color_modes", + [(SUPPORT_COLOR, [ColorMode.HS])], +) @pytest.mark.parametrize( "config", [ @@ -1045,11 +1291,21 @@ async def test_color_action_no_template(hass, start_ha, calls): }}}}""" ], ) -async def test_color_template(hass, expected_hs, start_ha): +async def test_color_template( + hass, + expected_hs, + start_ha, + supported_features, + supported_color_modes, + expected_color_mode, +): """Test the template for the color.""" state = hass.states.get("light.test_template_light") - assert state is not None assert state.attributes.get("hs_color") == expected_hs + assert state.state == STATE_ON + assert state.attributes["color_mode"] == expected_color_mode + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) From 054ea77b45cbcb58db842b49f0629d493073cc1a Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Tue, 10 May 2022 13:00:00 +0200 Subject: [PATCH 0368/3516] Bump devolo-plc-api to 0.8.0 (#71633) --- homeassistant/components/devolo_home_network/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/devolo_home_network/manifest.json b/homeassistant/components/devolo_home_network/manifest.json index 445a383ea14..94f26c8a615 100644 --- a/homeassistant/components/devolo_home_network/manifest.json +++ b/homeassistant/components/devolo_home_network/manifest.json @@ -3,7 +3,7 @@ "name": "devolo Home Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/devolo_home_network", - "requirements": ["devolo-plc-api==0.7.1"], + "requirements": ["devolo-plc-api==0.8.0"], "zeroconf": [ { "type": "_dvl-deviceapi._tcp.local.", "properties": { "MT": "*" } } ], diff --git a/requirements_all.txt b/requirements_all.txt index 731e7e1ee2d..69fed66566e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -542,7 +542,7 @@ denonavr==0.10.11 devolo-home-control-api==0.18.1 # homeassistant.components.devolo_home_network -devolo-plc-api==0.7.1 +devolo-plc-api==0.8.0 # homeassistant.components.directv directv==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ca41a5ca264..6bc7b31420d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -397,7 +397,7 @@ denonavr==0.10.11 devolo-home-control-api==0.18.1 # homeassistant.components.devolo_home_network -devolo-plc-api==0.7.1 +devolo-plc-api==0.8.0 # homeassistant.components.directv directv==0.4.0 From 68c2b63ca1e92b0d041a2b0f53eeaf441f8ee83d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 10 May 2022 15:22:12 +0200 Subject: [PATCH 0369/3516] Fix issue creation links in log messages (#71638) --- homeassistant/components/edl21/sensor.py | 4 ++-- homeassistant/components/recorder/statistics.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index 278ac004121..7614dc54ac4 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -329,9 +329,9 @@ class EDL21: self._registered_obis.add((electricity_id, obis)) elif obis not in self._OBIS_BLACKLIST: _LOGGER.warning( - "Unhandled sensor %s detected. Please report at " - 'https://github.com/home-assistant/core/issues?q=is%%3Aissue+label%%3A"integration%%3A+edl21"+', + "Unhandled sensor %s detected. Please report at %s", obis, + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+edl21%22", ) self._OBIS_BLACKLIST.add(obis) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index d8ba415cc7a..732c042d9c2 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -415,8 +415,8 @@ def delete_duplicates(hass: HomeAssistant, session: Session) -> None: ) if deleted_short_term_statistics_rows: _LOGGER.warning( - "Deleted duplicated short term statistic rows, please report at " - 'https://github.com/home-assistant/core/issues?q=is%%3Aissue+label%%3A"integration%%3A+recorder"+' + "Deleted duplicated short term statistic rows, please report at %s", + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+recorder%22", ) @@ -1360,8 +1360,8 @@ def _filter_unique_constraint_integrity_error( if ignore: _LOGGER.warning( - "Blocked attempt to insert duplicated statistic rows, please report at " - 'https://github.com/home-assistant/core/issues?q=is%%3Aissue+label%%3A"integration%%3A+recorder"+', + "Blocked attempt to insert duplicated statistic rows, please report at %s", + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+recorder%22", exc_info=err, ) From 26177bd080b4eb6d11cfd9fbdd158be36f4983d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 10 May 2022 08:23:13 -0500 Subject: [PATCH 0370/3516] Convert logbook to use lambda_stmt (#71624) --- homeassistant/components/logbook/__init__.py | 247 ++++++++++--------- tests/components/logbook/test_init.py | 53 ++++ 2 files changed, 179 insertions(+), 121 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 53063d36fc0..a877cba4ff2 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -11,11 +11,13 @@ from typing import Any, cast from aiohttp import web import sqlalchemy +from sqlalchemy import lambda_stmt, select from sqlalchemy.engine.row import Row from sqlalchemy.orm import aliased from sqlalchemy.orm.query import Query -from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal +from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Select import voluptuous as vol from homeassistant.components import frontend @@ -85,8 +87,6 @@ CONTINUOUS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in CONTINUOUS_DOMAINS] DOMAIN = "logbook" -GROUP_BY_MINUTES = 15 - EMPTY_JSON_OBJECT = "{}" UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%" @@ -435,70 +435,43 @@ def _get_events( def yield_rows(query: Query) -> Generator[Row, None, None]: """Yield Events that are not filtered away.""" - for row in query.yield_per(1000): + if entity_ids or context_id: + rows = query.all() + else: + rows = query.yield_per(1000) + for row in rows: context_lookup.setdefault(row.context_id, row) - if row.event_type != EVENT_CALL_SERVICE and ( - row.event_type == EVENT_STATE_CHANGED - or _keep_row(hass, row, entities_filter) + event_type = row.event_type + if event_type != EVENT_CALL_SERVICE and ( + event_type == EVENT_STATE_CHANGED + or _keep_row(hass, event_type, row, entities_filter) ): yield row if entity_ids is not None: entities_filter = generate_filter([], entity_ids, [], []) + event_types = [ + *ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, + *hass.data.get(DOMAIN, {}), + ] + entity_filter = None + if entity_ids is None and filters: + entity_filter = filters.entity_filter() # type: ignore[no-untyped-call] + stmt = _generate_logbook_query( + start_day, + end_day, + event_types, + entity_ids, + entity_filter, + entity_matches_only, + context_id, + ) with session_scope(hass=hass) as session: - old_state = aliased(States, name="old_state") - query: Query - query = _generate_events_query_without_states(session) - query = _apply_event_time_filter(query, start_day, end_day) - query = _apply_event_types_filter( - hass, query, ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED - ) - - if entity_ids is not None: - if entity_matches_only: - # When entity_matches_only is provided, contexts and events that do not - # contain the entity_ids are not included in the logbook response. - query = _apply_event_entity_id_matchers(query, entity_ids) - query = query.outerjoin(EventData, (Events.data_id == EventData.data_id)) - query = query.union_all( - _generate_states_query( - session, start_day, end_day, old_state, entity_ids - ) - ) - else: - if context_id is not None: - query = query.filter(Events.context_id == context_id) - query = query.outerjoin(EventData, (Events.data_id == EventData.data_id)) - - states_query = _generate_states_query( - session, start_day, end_day, old_state, entity_ids - ) - unions: list[Query] = [] - if context_id is not None: - # Once all the old `state_changed` events - # are gone from the database remove the - # _generate_legacy_events_context_id_query - unions.append( - _generate_legacy_events_context_id_query( - session, context_id, start_day, end_day - ) - ) - states_query = states_query.outerjoin( - Events, (States.event_id == Events.event_id) - ) - states_query = states_query.filter(States.context_id == context_id) - elif filters: - states_query = states_query.filter(filters.entity_filter()) # type: ignore[no-untyped-call] - unions.append(states_query) - query = query.union_all(*unions) - - query = query.order_by(Events.time_fired) - return list( _humanify( hass, - yield_rows(query), + yield_rows(session.execute(stmt)), entity_name_cache, event_cache, context_augmenter, @@ -506,8 +479,72 @@ def _get_events( ) -def _generate_events_query_without_data(session: Session) -> Query: - return session.query( +def _generate_logbook_query( + start_day: dt, + end_day: dt, + event_types: list[str], + entity_ids: list[str] | None = None, + entity_filter: Any | None = None, + entity_matches_only: bool = False, + context_id: str | None = None, +) -> StatementLambdaElement: + """Generate a logbook query lambda_stmt.""" + stmt = lambda_stmt( + lambda: _generate_events_query_without_states() + .where((Events.time_fired > start_day) & (Events.time_fired < end_day)) + .where(Events.event_type.in_(event_types)) + .outerjoin(EventData, (Events.data_id == EventData.data_id)) + ) + if entity_ids is not None: + if entity_matches_only: + # When entity_matches_only is provided, contexts and events that do not + # contain the entity_ids are not included in the logbook response. + stmt.add_criteria( + lambda s: s.where(_apply_event_entity_id_matchers(entity_ids)), + track_on=entity_ids, + ) + stmt += lambda s: s.union_all( + _generate_states_query() + .filter((States.last_updated > start_day) & (States.last_updated < end_day)) + .where(States.entity_id.in_(entity_ids)) + ) + else: + if context_id is not None: + # Once all the old `state_changed` events + # are gone from the database remove the + # union_all(_generate_legacy_events_context_id_query()....) + stmt += lambda s: s.where(Events.context_id == context_id).union_all( + _generate_legacy_events_context_id_query() + .where((Events.time_fired > start_day) & (Events.time_fired < end_day)) + .where(Events.context_id == context_id), + _generate_states_query() + .where( + (States.last_updated > start_day) & (States.last_updated < end_day) + ) + .outerjoin(Events, (States.event_id == Events.event_id)) + .where(States.context_id == context_id), + ) + elif entity_filter is not None: + stmt += lambda s: s.union_all( + _generate_states_query() + .where( + (States.last_updated > start_day) & (States.last_updated < end_day) + ) + .where(entity_filter) + ) + else: + stmt += lambda s: s.union_all( + _generate_states_query().where( + (States.last_updated > start_day) & (States.last_updated < end_day) + ) + ) + + stmt += lambda s: s.order_by(Events.time_fired) + return stmt + + +def _generate_events_query_without_data() -> Select: + return select( literal(value=EVENT_STATE_CHANGED, type_=sqlalchemy.String).label("event_type"), literal(value=None, type_=sqlalchemy.Text).label("event_data"), States.last_changed.label("time_fired"), @@ -519,65 +556,48 @@ def _generate_events_query_without_data(session: Session) -> Query: ) -def _generate_legacy_events_context_id_query( - session: Session, - context_id: str, - start_day: dt, - end_day: dt, -) -> Query: +def _generate_legacy_events_context_id_query() -> Select: """Generate a legacy events context id query that also joins states.""" # This can be removed once we no longer have event_ids in the states table - legacy_context_id_query = session.query( - *EVENT_COLUMNS, - literal(value=None, type_=sqlalchemy.String).label("shared_data"), - States.state, - States.entity_id, - States.attributes, - StateAttributes.shared_attrs, - ) - legacy_context_id_query = _apply_event_time_filter( - legacy_context_id_query, start_day, end_day - ) return ( - legacy_context_id_query.filter(Events.context_id == context_id) + select( + *EVENT_COLUMNS, + literal(value=None, type_=sqlalchemy.String).label("shared_data"), + States.state, + States.entity_id, + States.attributes, + StateAttributes.shared_attrs, + ) .outerjoin(States, (Events.event_id == States.event_id)) - .filter(States.last_updated == States.last_changed) - .filter(_not_continuous_entity_matcher()) + .where(States.last_updated == States.last_changed) + .where(_not_continuous_entity_matcher()) .outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) ) -def _generate_events_query_without_states(session: Session) -> Query: - return session.query( +def _generate_events_query_without_states() -> Select: + return select( *EVENT_COLUMNS, EventData.shared_data.label("shared_data"), *EMPTY_STATE_COLUMNS ) -def _generate_states_query( - session: Session, - start_day: dt, - end_day: dt, - old_state: States, - entity_ids: Iterable[str] | None, -) -> Query: - query = ( - _generate_events_query_without_data(session) +def _generate_states_query() -> Select: + old_state = aliased(States, name="old_state") + return ( + _generate_events_query_without_data() .outerjoin(old_state, (States.old_state_id == old_state.state_id)) - .filter(_missing_state_matcher(old_state)) - .filter(_not_continuous_entity_matcher()) - .filter((States.last_updated > start_day) & (States.last_updated < end_day)) - .filter(States.last_updated == States.last_changed) - ) - if entity_ids: - query = query.filter(States.entity_id.in_(entity_ids)) - return query.outerjoin( - StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + .where(_missing_state_matcher(old_state)) + .where(_not_continuous_entity_matcher()) + .where(States.last_updated == States.last_changed) + .outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) ) -def _missing_state_matcher(old_state: States) -> Any: +def _missing_state_matcher(old_state: States) -> sqlalchemy.and_: # The below removes state change events that do not have # and old_state or the old_state is missing (newly added entities) # or the new_state is missing (removed entities) @@ -588,7 +608,7 @@ def _missing_state_matcher(old_state: States) -> Any: ) -def _not_continuous_entity_matcher() -> Any: +def _not_continuous_entity_matcher() -> sqlalchemy.or_: """Match non continuous entities.""" return sqlalchemy.or_( _not_continuous_domain_matcher(), @@ -598,7 +618,7 @@ def _not_continuous_entity_matcher() -> Any: ) -def _not_continuous_domain_matcher() -> Any: +def _not_continuous_domain_matcher() -> sqlalchemy.and_: """Match not continuous domains.""" return sqlalchemy.and_( *[ @@ -608,7 +628,7 @@ def _not_continuous_domain_matcher() -> Any: ).self_group() -def _continuous_domain_matcher() -> Any: +def _continuous_domain_matcher() -> sqlalchemy.or_: """Match continuous domains.""" return sqlalchemy.or_( *[ @@ -625,37 +645,22 @@ def _not_uom_attributes_matcher() -> Any: ) | ~States.attributes.like(UNIT_OF_MEASUREMENT_JSON_LIKE) -def _apply_event_time_filter(events_query: Query, start_day: dt, end_day: dt) -> Query: - return events_query.filter( - (Events.time_fired > start_day) & (Events.time_fired < end_day) - ) - - -def _apply_event_types_filter( - hass: HomeAssistant, query: Query, event_types: list[str] -) -> Query: - return query.filter( - Events.event_type.in_(event_types + list(hass.data.get(DOMAIN, {}))) - ) - - -def _apply_event_entity_id_matchers( - events_query: Query, entity_ids: Iterable[str] -) -> Query: +def _apply_event_entity_id_matchers(entity_ids: Iterable[str]) -> sqlalchemy.or_: + """Create matchers for the entity_id in the event_data.""" ors = [] for entity_id in entity_ids: like = ENTITY_ID_JSON_TEMPLATE.format(entity_id) ors.append(Events.event_data.like(like)) ors.append(EventData.shared_data.like(like)) - return events_query.filter(sqlalchemy.or_(*ors)) + return sqlalchemy.or_(*ors) def _keep_row( hass: HomeAssistant, + event_type: str, row: Row, entities_filter: EntityFilter | Callable[[str], bool] | None = None, ) -> bool: - event_type = row.event_type if event_type in HOMEASSISTANT_EVENTS: return entities_filter is None or entities_filter(HA_DOMAIN_ENTITY_ID) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 4f961dcd2c1..76319ba5a6e 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -1390,6 +1390,59 @@ async def test_logbook_entity_matches_only(hass, hass_client, recorder_mock): assert json_dict[1]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" +async def test_logbook_entity_matches_only_multiple_calls( + hass, hass_client, recorder_mock +): + """Test the logbook view with a single entity and entity_matches_only called multiple times.""" + await async_setup_component(hass, "logbook", {}) + await async_setup_component(hass, "automation", {}) + + await async_recorder_block_till_done(hass) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + for automation_id in range(5): + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + { + ATTR_NAME: f"Mock automation {automation_id}", + ATTR_ENTITY_ID: f"automation.mock_{automation_id}_automation", + }, + ) + await async_wait_recording_done(hass) + client = await hass_client() + + # Today time 00:00:00 + start = dt_util.utcnow().date() + start_date = datetime(start.year, start.month, start.day) + end_time = start + timedelta(hours=24) + + for automation_id in range(5): + # Test today entries with filter by end_time + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=automation.mock_{automation_id}_automation&entity_matches_only" + ) + assert response.status == HTTPStatus.OK + json_dict = await response.json() + + assert len(json_dict) == 1 + assert ( + json_dict[0]["entity_id"] == f"automation.mock_{automation_id}_automation" + ) + + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=automation.mock_0_automation,automation.mock_1_automation,automation.mock_2_automation&entity_matches_only" + ) + assert response.status == HTTPStatus.OK + json_dict = await response.json() + + assert len(json_dict) == 3 + assert json_dict[0]["entity_id"] == "automation.mock_0_automation" + assert json_dict[1]["entity_id"] == "automation.mock_1_automation" + assert json_dict[2]["entity_id"] == "automation.mock_2_automation" + + async def test_custom_log_entry_discoverable_via_entity_matches_only( hass, hass_client, recorder_mock ): From ef16e6c129485f654a765fb3dbad27a605e4f0c4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 10 May 2022 15:28:44 +0200 Subject: [PATCH 0371/3516] Fix Plugwise recovering from aiohttp client error (#71642) --- homeassistant/components/plugwise/gateway.py | 3 ++- tests/components/plugwise/test_init.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index f540c6d0b9c..648765155e4 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from typing import Any +from aiohttp import ClientConnectionError from plugwise.exceptions import InvalidAuthentication, PlugwiseException from plugwise.smile import Smile @@ -44,7 +45,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: except InvalidAuthentication: LOGGER.error("Invalid username or Smile ID") return False - except PlugwiseException as err: + except (ClientConnectionError, PlugwiseException) as err: raise ConfigEntryNotReady( f"Error while communicating to device {api.smile_name}" ) from err diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index 811018db7e2..557680cb060 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -2,6 +2,7 @@ import asyncio from unittest.mock import MagicMock +import aiohttp from plugwise.exceptions import ( ConnectionFailedError, PlugwiseException, @@ -49,6 +50,7 @@ async def test_load_unload_config_entry( (PlugwiseException), (XMLDataMissingError), (asyncio.TimeoutError), + (aiohttp.ClientConnectionError), ], ) async def test_config_entry_not_ready( From c994d06967975a63a9b43ef0f1dc30cfd49432c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 10 May 2022 16:48:26 +0200 Subject: [PATCH 0372/3516] Update aioqsw to 0.0.8 (#71640) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements PEP 561, which allows to remove coordinator cast. Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/qnap_qsw/coordinator.py | 4 ++-- homeassistant/components/qnap_qsw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/qnap_qsw/coordinator.py b/homeassistant/components/qnap_qsw/coordinator.py index 7aa1b6f0a85..c018c1f3848 100644 --- a/homeassistant/components/qnap_qsw/coordinator.py +++ b/homeassistant/components/qnap_qsw/coordinator.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any, cast +from typing import Any from aioqsw.exceptions import QswError from aioqsw.localapi import QnapQswApi @@ -40,4 +40,4 @@ class QswUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): await self.qsw.update() except QswError as error: raise UpdateFailed(error) from error - return cast(dict[str, Any], self.qsw.data()) + return self.qsw.data() diff --git a/homeassistant/components/qnap_qsw/manifest.json b/homeassistant/components/qnap_qsw/manifest.json index 0624edd000c..9331a7df468 100644 --- a/homeassistant/components/qnap_qsw/manifest.json +++ b/homeassistant/components/qnap_qsw/manifest.json @@ -3,7 +3,7 @@ "name": "QNAP QSW", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/qnap_qsw", - "requirements": ["aioqsw==0.0.7"], + "requirements": ["aioqsw==0.0.8"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioqsw"] diff --git a/requirements_all.txt b/requirements_all.txt index 69fed66566e..9f3f4b00e33 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -226,7 +226,7 @@ aiopvpc==3.0.0 aiopyarr==22.2.2 # homeassistant.components.qnap_qsw -aioqsw==0.0.7 +aioqsw==0.0.8 # homeassistant.components.recollect_waste aiorecollect==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6bc7b31420d..8104911a6ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -192,7 +192,7 @@ aiopvpc==3.0.0 aiopyarr==22.2.2 # homeassistant.components.qnap_qsw -aioqsw==0.0.7 +aioqsw==0.0.8 # homeassistant.components.recollect_waste aiorecollect==1.0.8 From 45290c4c0920e64a2faca5adb05d8440d215d44c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 10 May 2022 16:49:40 +0200 Subject: [PATCH 0373/3516] Update aioairzone to 0.4.4 (#71641) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements PEP 561, which allows to remove coordinator cast for strict typing. Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone/coordinator.py | 4 ++-- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/airzone/coordinator.py b/homeassistant/components/airzone/coordinator.py index 5ce9dcc45ef..ba0296557a1 100644 --- a/homeassistant/components/airzone/coordinator.py +++ b/homeassistant/components/airzone/coordinator.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any, cast +from typing import Any from aioairzone.exceptions import AirzoneError from aioairzone.localapi import AirzoneLocalApi @@ -40,4 +40,4 @@ class AirzoneUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): await self.airzone.update() except AirzoneError as error: raise UpdateFailed(error) from error - return cast(dict[str, Any], self.airzone.data()) + return self.airzone.data() diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index 7a04b3a78b3..e0d3bd6df09 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -3,7 +3,7 @@ "name": "Airzone", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airzone", - "requirements": ["aioairzone==0.4.3"], + "requirements": ["aioairzone==0.4.4"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioairzone"] diff --git a/requirements_all.txt b/requirements_all.txt index 9f3f4b00e33..8f80e5026f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -110,7 +110,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.3 +aioairzone==0.4.4 # homeassistant.components.ambient_station aioambient==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8104911a6ec..6849524c94c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -94,7 +94,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.3 +aioairzone==0.4.4 # homeassistant.components.ambient_station aioambient==2021.11.0 From 220589877168e8b0c59f8cd8d7fdcd55072184a8 Mon Sep 17 00:00:00 2001 From: rappenze Date: Tue, 10 May 2022 22:33:40 +0200 Subject: [PATCH 0374/3516] Fix wrong brightness level change visible in UI (#71655) --- homeassistant/components/fibaro/light.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index 9d0309bc4ee..08a9e651668 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -112,6 +112,7 @@ class FibaroLight(FibaroDevice, LightEntity): if ATTR_BRIGHTNESS in kwargs: self._attr_brightness = kwargs[ATTR_BRIGHTNESS] self.set_level(scaleto99(self._attr_brightness)) + return if ATTR_RGB_COLOR in kwargs: # Update based on parameters From 945eba9aa384f2348e082ddb981f897a9465040f Mon Sep 17 00:00:00 2001 From: Graham Arthur Blair Date: Tue, 10 May 2022 13:49:38 -0700 Subject: [PATCH 0375/3516] Change Ring Chime play sound Buttons to a Siren (#71449) --- homeassistant/components/ring/__init__.py | 2 +- homeassistant/components/ring/button.py | 65 ----------- homeassistant/components/ring/siren.py | 51 +++++++++ tests/components/ring/test_button.py | 55 --------- tests/components/ring/test_siren.py | 132 ++++++++++++++++++++++ 5 files changed, 184 insertions(+), 121 deletions(-) delete mode 100644 homeassistant/components/ring/button.py create mode 100644 homeassistant/components/ring/siren.py delete mode 100644 tests/components/ring/test_button.py create mode 100644 tests/components/ring/test_siren.py diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 2a922a7bb3e..a1ed1ac017b 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -32,11 +32,11 @@ DEFAULT_ENTITY_NAMESPACE = "ring" PLATFORMS = [ Platform.BINARY_SENSOR, - Platform.BUTTON, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH, Platform.CAMERA, + Platform.SIREN, ] diff --git a/homeassistant/components/ring/button.py b/homeassistant/components/ring/button.py deleted file mode 100644 index 04f9f7950f8..00000000000 --- a/homeassistant/components/ring/button.py +++ /dev/null @@ -1,65 +0,0 @@ -"""This component provides HA button support for Ring Chimes.""" -import logging - -from homeassistant.components.button import ButtonEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from . import DOMAIN -from .entity import RingEntityMixin - -_LOGGER = logging.getLogger(__name__) - -BELL_ICON = "mdi:bell-ring" - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Create the buttons for the Ring devices.""" - devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] - buttons = [] - - # add one button for each test chime type (ding, motion) - for device in devices["chimes"]: - buttons.append(ChimeButton(config_entry.entry_id, device, "ding")) - buttons.append(ChimeButton(config_entry.entry_id, device, "motion")) - - async_add_entities(buttons) - - -class BaseRingButton(RingEntityMixin, ButtonEntity): - """Represents a Button for controlling an aspect of a ring device.""" - - def __init__(self, config_entry_id, device, button_identifier, button_name): - """Initialize the switch.""" - super().__init__(config_entry_id, device) - self._button_identifier = button_identifier - self._button_name = button_name - self._attr_unique_id = f"{self._device.id}-{self._button_identifier}" - - @property - def name(self): - """Name of the device.""" - return f"{self._device.name} {self._button_name}" - - -class ChimeButton(BaseRingButton): - """Creates a button to play the test chime of a Chime device.""" - - _attr_icon = BELL_ICON - - def __init__(self, config_entry_id, device, kind): - """Initialize the button for a device with a chime.""" - super().__init__( - config_entry_id, device, f"play-chime-{kind}", f"Play chime: {kind}" - ) - self.kind = kind - - def press(self) -> None: - """Send the test chime request.""" - if not self._device.test_sound(kind=self.kind): - _LOGGER.error("Failed to ring chime sound on %s", self.name) diff --git a/homeassistant/components/ring/siren.py b/homeassistant/components/ring/siren.py new file mode 100644 index 00000000000..b83d3e7b2ae --- /dev/null +++ b/homeassistant/components/ring/siren.py @@ -0,0 +1,51 @@ +"""This component provides HA Siren support for Ring Chimes.""" +import logging +from typing import Any + +from ring_doorbell.const import CHIME_TEST_SOUND_KINDS, KIND_DING + +from homeassistant.components.siren import ATTR_TONE, SirenEntity, SirenEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import DOMAIN +from .entity import RingEntityMixin + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Create the sirens for the Ring devices.""" + devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] + sirens = [] + + for device in devices["chimes"]: + sirens.append(RingChimeSiren(config_entry, device)) + + async_add_entities(sirens) + + +class RingChimeSiren(RingEntityMixin, SirenEntity): + """Creates a siren to play the test chimes of a Chime device.""" + + def __init__(self, config_entry: ConfigEntry, device) -> None: + """Initialize a Ring Chime siren.""" + super().__init__(config_entry.entry_id, device) + # Entity class attributes + self._attr_name = f"{self._device.name} Siren" + self._attr_unique_id = f"{self._device.id}-siren" + self._attr_available_tones = CHIME_TEST_SOUND_KINDS + self._attr_supported_features = ( + SirenEntityFeature.TURN_ON | SirenEntityFeature.TONES + ) + + def turn_on(self, **kwargs: Any) -> None: + """Play the test sound on a Ring Chime device.""" + tone = kwargs.get(ATTR_TONE) or KIND_DING + + self._device.test_sound(kind=tone) diff --git a/tests/components/ring/test_button.py b/tests/components/ring/test_button.py deleted file mode 100644 index 62b2fcd8a78..00000000000 --- a/tests/components/ring/test_button.py +++ /dev/null @@ -1,55 +0,0 @@ -"""The tests for the Ring button platform.""" - -from homeassistant.const import Platform -from homeassistant.helpers import entity_registry as er - -from .common import setup_platform - - -async def test_entity_registry(hass, requests_mock): - """Tests that the devices are registered in the entity registry.""" - await setup_platform(hass, Platform.BUTTON) - entity_registry = er.async_get(hass) - - entry = entity_registry.async_get("button.downstairs_play_chime_ding") - assert entry.unique_id == "123456-play-chime-ding" - - entry = entity_registry.async_get("button.downstairs_play_chime_motion") - assert entry.unique_id == "123456-play-chime-motion" - - -async def test_play_chime_buttons_report_correctly(hass, requests_mock): - """Tests that the initial state of a device that should be on is correct.""" - await setup_platform(hass, Platform.BUTTON) - - state = hass.states.get("button.downstairs_play_chime_ding") - assert state.attributes.get("friendly_name") == "Downstairs Play chime: ding" - assert state.attributes.get("icon") == "mdi:bell-ring" - - state = hass.states.get("button.downstairs_play_chime_motion") - assert state.attributes.get("friendly_name") == "Downstairs Play chime: motion" - assert state.attributes.get("icon") == "mdi:bell-ring" - - -async def test_chime_can_be_played(hass, requests_mock): - """Tests the play chime request is sent correctly.""" - await setup_platform(hass, Platform.BUTTON) - - # Mocks the response for playing a test sound - requests_mock.post( - "https://api.ring.com/clients_api/chimes/123456/play_sound", - text="SUCCESS", - ) - await hass.services.async_call( - "button", - "press", - {"entity_id": "button.downstairs_play_chime_ding"}, - blocking=True, - ) - - await hass.async_block_till_done() - - assert requests_mock.request_history[-1].url.startswith( - "https://api.ring.com/clients_api/chimes/123456/play_sound?" - ) - assert "kind=ding" in requests_mock.request_history[-1].url diff --git a/tests/components/ring/test_siren.py b/tests/components/ring/test_siren.py new file mode 100644 index 00000000000..bb282769e57 --- /dev/null +++ b/tests/components/ring/test_siren.py @@ -0,0 +1,132 @@ +"""The tests for the Ring button platform.""" + +from homeassistant.const import Platform +from homeassistant.helpers import entity_registry as er + +from .common import setup_platform + + +async def test_entity_registry(hass, requests_mock): + """Tests that the devices are registered in the entity registry.""" + await setup_platform(hass, Platform.SIREN) + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get("siren.downstairs_siren") + assert entry.unique_id == "123456-siren" + + +async def test_sirens_report_correctly(hass, requests_mock): + """Tests that the initial state of a device that should be on is correct.""" + await setup_platform(hass, Platform.SIREN) + + state = hass.states.get("siren.downstairs_siren") + assert state.attributes.get("friendly_name") == "Downstairs Siren" + assert state.state == "unknown" + + +async def test_default_ding_chime_can_be_played(hass, requests_mock): + """Tests the play chime request is sent correctly.""" + await setup_platform(hass, Platform.SIREN) + + # Mocks the response for playing a test sound + requests_mock.post( + "https://api.ring.com/clients_api/chimes/123456/play_sound", + text="SUCCESS", + ) + await hass.services.async_call( + "siren", + "turn_on", + {"entity_id": "siren.downstairs_siren"}, + blocking=True, + ) + + await hass.async_block_till_done() + + assert requests_mock.request_history[-1].url.startswith( + "https://api.ring.com/clients_api/chimes/123456/play_sound?" + ) + assert "kind=ding" in requests_mock.request_history[-1].url + + state = hass.states.get("siren.downstairs_siren") + assert state.state == "unknown" + + +async def test_toggle_plays_default_chime(hass, requests_mock): + """Tests the play chime request is sent correctly when toggled.""" + await setup_platform(hass, Platform.SIREN) + + # Mocks the response for playing a test sound + requests_mock.post( + "https://api.ring.com/clients_api/chimes/123456/play_sound", + text="SUCCESS", + ) + await hass.services.async_call( + "siren", + "toggle", + {"entity_id": "siren.downstairs_siren"}, + blocking=True, + ) + + await hass.async_block_till_done() + + assert requests_mock.request_history[-1].url.startswith( + "https://api.ring.com/clients_api/chimes/123456/play_sound?" + ) + assert "kind=ding" in requests_mock.request_history[-1].url + + state = hass.states.get("siren.downstairs_siren") + assert state.state == "unknown" + + +async def test_explicit_ding_chime_can_be_played(hass, requests_mock): + """Tests the play chime request is sent correctly.""" + await setup_platform(hass, Platform.SIREN) + + # Mocks the response for playing a test sound + requests_mock.post( + "https://api.ring.com/clients_api/chimes/123456/play_sound", + text="SUCCESS", + ) + await hass.services.async_call( + "siren", + "turn_on", + {"entity_id": "siren.downstairs_siren", "tone": "ding"}, + blocking=True, + ) + + await hass.async_block_till_done() + + assert requests_mock.request_history[-1].url.startswith( + "https://api.ring.com/clients_api/chimes/123456/play_sound?" + ) + assert "kind=ding" in requests_mock.request_history[-1].url + + state = hass.states.get("siren.downstairs_siren") + assert state.state == "unknown" + + +async def test_motion_chime_can_be_played(hass, requests_mock): + """Tests the play chime request is sent correctly.""" + await setup_platform(hass, Platform.SIREN) + + # Mocks the response for playing a test sound + requests_mock.post( + "https://api.ring.com/clients_api/chimes/123456/play_sound", + text="SUCCESS", + ) + await hass.services.async_call( + "siren", + "turn_on", + {"entity_id": "siren.downstairs_siren", "tone": "motion"}, + blocking=True, + ) + + await hass.async_block_till_done() + + assert requests_mock.request_history[-1].url.startswith( + "https://api.ring.com/clients_api/chimes/123456/play_sound?" + ) + assert "kind=motion" in requests_mock.request_history[-1].url + + state = hass.states.get("siren.downstairs_siren") + assert state.state == "unknown" From dcbac86fc04c864e3b6b4bb592134d192e5ccbf5 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 10 May 2022 19:21:46 -0400 Subject: [PATCH 0376/3516] Bump up ZHA dependencies (#71663) --- 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 d4e71562b07..8ea9e9d8d4a 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.29.0", + "bellows==0.30.0", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.73", diff --git a/requirements_all.txt b/requirements_all.txt index 8f80e5026f2..f3204e93502 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -394,7 +394,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.29.0 +bellows==0.30.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.8.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6849524c94c..f4b109b6b07 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -303,7 +303,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.29.0 +bellows==0.30.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.8.12 From 3d2b0a17cedbac417ac29c3e2f0f3110f7d83044 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 11 May 2022 00:22:31 +0000 Subject: [PATCH 0377/3516] [ci skip] Translation update --- .../components/dlna_dmr/translations/ja.json | 1 + .../components/fan/translations/ca.json | 1 + .../components/fan/translations/de.json | 1 + .../components/fan/translations/et.json | 1 + .../components/fan/translations/fr.json | 1 + .../components/fan/translations/hu.json | 1 + .../components/fan/translations/id.json | 1 + .../components/fan/translations/ja.json | 1 + .../components/fan/translations/nl.json | 1 + .../components/fan/translations/pl.json | 1 + .../components/fan/translations/ru.json | 1 + .../components/fan/translations/zh-Hant.json | 1 + .../components/hue/translations/ja.json | 1 + .../humidifier/translations/fr.json | 2 +- .../components/insteon/translations/ja.json | 1 + .../components/onewire/translations/bg.json | 2 ++ .../components/onewire/translations/et.json | 4 ++- .../components/onewire/translations/ru.json | 2 ++ .../components/qnap_qsw/translations/ja.json | 8 +++++ .../components/recorder/translations/ja.json | 7 ++++ .../components/sabnzbd/translations/ja.json | 18 ++++++++++ .../components/slimproto/translations/ja.json | 7 ++++ .../components/sms/translations/ru.json | 1 + .../components/sql/translations/ja.json | 11 +++++- .../steam_online/translations/ja.json | 16 ++++++++- .../components/tautulli/translations/ja.json | 3 +- .../trafikverket_ferry/translations/ja.json | 3 ++ .../ukraine_alarm/translations/bg.json | 34 +++++++++++++++++++ .../ukraine_alarm/translations/ca.json | 2 +- .../ukraine_alarm/translations/ja.json | 3 +- .../components/ws66i/translations/bg.json | 18 ++++++++++ .../components/ws66i/translations/ja.json | 18 +++++++++- .../translations/select.fr.json | 2 +- 33 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/recorder/translations/ja.json create mode 100644 homeassistant/components/sabnzbd/translations/ja.json create mode 100644 homeassistant/components/slimproto/translations/ja.json create mode 100644 homeassistant/components/ukraine_alarm/translations/bg.json create mode 100644 homeassistant/components/ws66i/translations/bg.json diff --git a/homeassistant/components/dlna_dmr/translations/ja.json b/homeassistant/components/dlna_dmr/translations/ja.json index 9edb9156534..4c71d57b468 100644 --- a/homeassistant/components/dlna_dmr/translations/ja.json +++ b/homeassistant/components/dlna_dmr/translations/ja.json @@ -47,6 +47,7 @@ "step": { "init": { "data": { + "browse_unfiltered": "\u30d6\u30e9\u30a6\u30ba\u6642\u306b\u4e92\u63db\u6027\u306e\u306a\u3044\u30e1\u30c7\u30a3\u30a2\u3092\u8868\u793a\u3059\u308b", "callback_url_override": "\u30a4\u30d9\u30f3\u30c8\u30ea\u30b9\u30ca\u30fc\u306e\u30b3\u30fc\u30eb\u30d0\u30c3\u30afURL", "listen_port": "\u30a4\u30d9\u30f3\u30c8\u30ea\u30b9\u30ca\u30fc\u30dd\u30fc\u30c8(\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u30e9\u30f3\u30c0\u30e0)", "poll_availability": "\u30c7\u30d0\u30a4\u30b9\u306e\u53ef\u7528\u6027\u3092\u30dd\u30fc\u30ea\u30f3\u30b0" diff --git a/homeassistant/components/fan/translations/ca.json b/homeassistant/components/fan/translations/ca.json index 274ddddbbaf..206933b1c9a 100644 --- a/homeassistant/components/fan/translations/ca.json +++ b/homeassistant/components/fan/translations/ca.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "Commuta {entity_name}", "turn_off": "Apaga {entity_name}", "turn_on": "Enc\u00e9n {entity_name}" }, diff --git a/homeassistant/components/fan/translations/de.json b/homeassistant/components/fan/translations/de.json index 43a7a42f808..5048a576bee 100644 --- a/homeassistant/components/fan/translations/de.json +++ b/homeassistant/components/fan/translations/de.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "{entity_name} umschalten", "turn_off": "Schalte {entity_name} aus.", "turn_on": "Schalte {entity_name} ein." }, diff --git a/homeassistant/components/fan/translations/et.json b/homeassistant/components/fan/translations/et.json index 4a46384ae3a..d168ca75158 100644 --- a/homeassistant/components/fan/translations/et.json +++ b/homeassistant/components/fan/translations/et.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "Muuda {entity_name} olekut", "turn_off": "L\u00fclita {entity_name} v\u00e4lja", "turn_on": "L\u00fclita {entity_name} sisse" }, diff --git a/homeassistant/components/fan/translations/fr.json b/homeassistant/components/fan/translations/fr.json index e9cf666b725..1ff0ed5eea9 100644 --- a/homeassistant/components/fan/translations/fr.json +++ b/homeassistant/components/fan/translations/fr.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "Basculer {entity_name}", "turn_off": "\u00c9teindre {entity_name}", "turn_on": "Allumer {entity_name}" }, diff --git a/homeassistant/components/fan/translations/hu.json b/homeassistant/components/fan/translations/hu.json index 50e659b001a..e7ef69d8800 100644 --- a/homeassistant/components/fan/translations/hu.json +++ b/homeassistant/components/fan/translations/hu.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "{entity_name} kapcsol\u00e1sa", "turn_off": "{entity_name} kikapcsol\u00e1sa", "turn_on": "{entity_name} bekapcsol\u00e1sa" }, diff --git a/homeassistant/components/fan/translations/id.json b/homeassistant/components/fan/translations/id.json index c21b90a495a..85fa77539c3 100644 --- a/homeassistant/components/fan/translations/id.json +++ b/homeassistant/components/fan/translations/id.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "Nyala/matikan {entity_name}", "turn_off": "Matikan {entity_name}", "turn_on": "Nyalakan {entity_name}" }, diff --git a/homeassistant/components/fan/translations/ja.json b/homeassistant/components/fan/translations/ja.json index 62b37a0b5df..75d528404a2 100644 --- a/homeassistant/components/fan/translations/ja.json +++ b/homeassistant/components/fan/translations/ja.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "\u30c8\u30b0\u30eb {entity_name}", "turn_off": "\u30aa\u30d5\u306b\u3059\u308b {entity_name}", "turn_on": "\u30aa\u30f3\u306b\u3059\u308b {entity_name}" }, diff --git a/homeassistant/components/fan/translations/nl.json b/homeassistant/components/fan/translations/nl.json index aa4474a782f..bdc2f64195b 100644 --- a/homeassistant/components/fan/translations/nl.json +++ b/homeassistant/components/fan/translations/nl.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "Schakel {entity_name}", "turn_off": "Schakel {entity_name} uit", "turn_on": "Schakel {entity_name} in" }, diff --git a/homeassistant/components/fan/translations/pl.json b/homeassistant/components/fan/translations/pl.json index 9e4f6b45341..9cfc6fa9b28 100644 --- a/homeassistant/components/fan/translations/pl.json +++ b/homeassistant/components/fan/translations/pl.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "prze\u0142\u0105cz {entity_name}", "turn_off": "wy\u0142\u0105cz {entity_name}", "turn_on": "w\u0142\u0105cz {entity_name}" }, diff --git a/homeassistant/components/fan/translations/ru.json b/homeassistant/components/fan/translations/ru.json index ec667b78ee1..5a4571f2dad 100644 --- a/homeassistant/components/fan/translations/ru.json +++ b/homeassistant/components/fan/translations/ru.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "{entity_name}: \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c", "turn_off": "{entity_name}: \u0432\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c", "turn_on": "{entity_name}: \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c" }, diff --git a/homeassistant/components/fan/translations/zh-Hant.json b/homeassistant/components/fan/translations/zh-Hant.json index 571040c820a..ba68f7c5158 100644 --- a/homeassistant/components/fan/translations/zh-Hant.json +++ b/homeassistant/components/fan/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "\u5207\u63db{entity_name}", "turn_off": "\u95dc\u9589{entity_name}", "turn_on": "\u958b\u555f{entity_name}" }, diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index 7a99db7e498..ec88173adca 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -6,6 +6,7 @@ "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "discover_timeout": "Hue bridge\u3092\u767a\u898b(\u63a2\u308a\u5f53\u3066)\u3067\u304d\u307e\u305b\u3093", + "invalid_host": "\u7121\u52b9\u306a\u30db\u30b9\u30c8", "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": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/humidifier/translations/fr.json b/homeassistant/components/humidifier/translations/fr.json index 84b24b96904..4caf57d81b5 100644 --- a/homeassistant/components/humidifier/translations/fr.json +++ b/homeassistant/components/humidifier/translations/fr.json @@ -3,7 +3,7 @@ "action_type": { "set_humidity": "R\u00e9gler l'humidit\u00e9 pour {nom_entit\u00e9}", "set_mode": "Changer le mode sur {nom_entit\u00e9}.", - "toggle": "Inverser {nom_entit\u00e9}", + "toggle": "Basculer {entity_name}", "turn_off": "\u00c9teindre {entity_name}", "turn_on": "Allumer {entity_name}" }, diff --git a/homeassistant/components/insteon/translations/ja.json b/homeassistant/components/insteon/translations/ja.json index 0b8d93518bd..81b7f246e41 100644 --- a/homeassistant/components/insteon/translations/ja.json +++ b/homeassistant/components/insteon/translations/ja.json @@ -2,6 +2,7 @@ "config": { "abort": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "not_insteon_device": "\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306f\u3001Insteon\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u306a\u3044", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { diff --git a/homeassistant/components/onewire/translations/bg.json b/homeassistant/components/onewire/translations/bg.json index e45a2db7197..26dd6f15686 100644 --- a/homeassistant/components/onewire/translations/bg.json +++ b/homeassistant/components/onewire/translations/bg.json @@ -15,6 +15,8 @@ }, "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", "type": "\u0412\u0438\u0434 \u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0430\u0442\u0430" } } diff --git a/homeassistant/components/onewire/translations/et.json b/homeassistant/components/onewire/translations/et.json index bb00ce8fbbc..7e57911127e 100644 --- a/homeassistant/components/onewire/translations/et.json +++ b/homeassistant/components/onewire/translations/et.json @@ -17,9 +17,11 @@ }, "user": { "data": { + "host": "Host", + "port": "Port", "type": "\u00dchenduse t\u00fc\u00fcp" }, - "title": "Seadista 1-wire sidumine" + "title": "M\u00e4\u00e4ra serveri \u00fcksikasjad" } } }, diff --git a/homeassistant/components/onewire/translations/ru.json b/homeassistant/components/onewire/translations/ru.json index 4bc24f85f19..d0ab8b8a282 100644 --- a/homeassistant/components/onewire/translations/ru.json +++ b/homeassistant/components/onewire/translations/ru.json @@ -17,6 +17,8 @@ }, "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", "type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" }, "title": "1-Wire" diff --git a/homeassistant/components/qnap_qsw/translations/ja.json b/homeassistant/components/qnap_qsw/translations/ja.json index 38e13174ac8..0658405fd3f 100644 --- a/homeassistant/components/qnap_qsw/translations/ja.json +++ b/homeassistant/components/qnap_qsw/translations/ja.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "invalid_id": "\u30c7\u30d0\u30a4\u30b9\u304c\u7121\u52b9\u306a\u30e6\u30cb\u30fc\u30afID\u3092\u8fd4\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/recorder/translations/ja.json b/homeassistant/components/recorder/translations/ja.json new file mode 100644 index 00000000000..2aadb11ac33 --- /dev/null +++ b/homeassistant/components/recorder/translations/ja.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "estimated_db_size": "\u63a8\u5b9a\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u30b5\u30a4\u30ba(MiB)" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/ja.json b/homeassistant/components/sabnzbd/translations/ja.json new file mode 100644 index 00000000000..737bbfe4140 --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/ja.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc" + }, + "step": { + "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "name": "\u540d\u524d", + "path": "\u30d1\u30b9", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/ja.json b/homeassistant/components/slimproto/translations/ja.json new file mode 100644 index 00000000000..4b8d0691b68 --- /dev/null +++ b/homeassistant/components/slimproto/translations/ja.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sms/translations/ru.json b/homeassistant/components/sms/translations/ru.json index 2200b582123..e5daa2005c0 100644 --- a/homeassistant/components/sms/translations/ru.json +++ b/homeassistant/components/sms/translations/ru.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "baud_speed": "\u0421\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0434\u0430\u043d\u043d\u044b\u0445", "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" diff --git a/homeassistant/components/sql/translations/ja.json b/homeassistant/components/sql/translations/ja.json index cf03616fbb9..6fa037efc5f 100644 --- a/homeassistant/components/sql/translations/ja.json +++ b/homeassistant/components/sql/translations/ja.json @@ -19,16 +19,25 @@ "value_template": "\u5024\u306e\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8" }, "data_description": { + "column": "\u8fd4\u3055\u308c\u305f\u30af\u30a8\u30ea\u3092\u72b6\u614b\u3068\u3057\u3066\u8868\u793a\u3059\u308b\u305f\u3081\u306e\u30ab\u30e9\u30e0", "name": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u3068\u30bb\u30f3\u30b5\u30fc\u306b\u4f7f\u7528\u3055\u308c\u308b\u540d\u524d" } } } }, "options": { + "error": { + "db_url_invalid": "\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306eURL\u304c\u7121\u52b9\u3067\u3059", + "query_invalid": "SQL\u30af\u30a8\u30ea\u304c\u7121\u52b9\u3067\u3059", + "value_template_invalid": "\u5024\u306e\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8\u304c\u7121\u52b9\u3067\u3059" + }, "step": { "init": { "data": { - "name": "\u540d\u524d" + "column": "\u30b3\u30e9\u30e0", + "db_url": "\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306eURL", + "name": "\u540d\u524d", + "query": "\u30af\u30a8\u30ea\u3092\u9078\u629e" }, "data_description": { "name": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u3068\u30bb\u30f3\u30b5\u30fc\u306b\u4f7f\u7528\u3055\u308c\u308b\u540d\u524d" diff --git a/homeassistant/components/steam_online/translations/ja.json b/homeassistant/components/steam_online/translations/ja.json index 07c813bb0b1..5e3b98684e8 100644 --- a/homeassistant/components/steam_online/translations/ja.json +++ b/homeassistant/components/steam_online/translations/ja.json @@ -5,7 +5,21 @@ "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_account": "\u30a2\u30ab\u30a6\u30f3\u30c8ID\u304c\u7121\u52b9", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "reauth_confirm": { + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + }, + "user": { + "data": { + "account": "Steam\u30a2\u30ab\u30a6\u30f3\u30c8ID", + "api_key": "API\u30ad\u30fc" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/ja.json b/homeassistant/components/tautulli/translations/ja.json index 854dc593a55..f6bec08752a 100644 --- a/homeassistant/components/tautulli/translations/ja.json +++ b/homeassistant/components/tautulli/translations/ja.json @@ -13,7 +13,8 @@ "reauth_confirm": { "data": { "api_key": "API\u30ad\u30fc" - } + }, + "title": "Tautulli\u3092\u518d\u8a8d\u8a3c\u3057\u307e\u3059" }, "user": { "data": { diff --git a/homeassistant/components/trafikverket_ferry/translations/ja.json b/homeassistant/components/trafikverket_ferry/translations/ja.json index 2377e33a60c..8081c3a9198 100644 --- a/homeassistant/components/trafikverket_ferry/translations/ja.json +++ b/homeassistant/components/trafikverket_ferry/translations/ja.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "incorrect_api_key": "\u9078\u629e\u3057\u305f\u30a2\u30ab\u30a6\u30f3\u30c8\u306eAPI\u30ad\u30fc\u304c\u7121\u52b9\u3067\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { @@ -17,7 +18,9 @@ "user": { "data": { "api_key": "API\u30ad\u30fc", + "from": "\u6e2f\u304b\u3089", "time": "\u6642\u9593", + "to": "\u6e2f\u3078", "weekday": "\u5e73\u65e5" } } diff --git a/homeassistant/components/ukraine_alarm/translations/bg.json b/homeassistant/components/ukraine_alarm/translations/bg.json new file mode 100644 index 00000000000..fbedf14b7cd --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/bg.json @@ -0,0 +1,34 @@ +{ + "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\u0448\u043d\u043e \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": { + "community": { + "data": { + "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + } + }, + "district": { + "data": { + "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + } + }, + "state": { + "data": { + "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + } + }, + "user": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/ca.json b/homeassistant/components/ukraine_alarm/translations/ca.json index bee49c8e4a1..cd5cda1cc8b 100644 --- a/homeassistant/components/ukraine_alarm/translations/ca.json +++ b/homeassistant/components/ukraine_alarm/translations/ca.json @@ -32,7 +32,7 @@ "data": { "api_key": "Clau API" }, - "description": "Configura la integraci\u00f3 d'Alarma Ucraina. Per generar la clau API, v\u00e9s a {api_url}" + "description": "Configura la integraci\u00f3 d'Alarma Ucra\u00efna. Per generar la clau API, v\u00e9s a {api_url}" } } } diff --git a/homeassistant/components/ukraine_alarm/translations/ja.json b/homeassistant/components/ukraine_alarm/translations/ja.json index 519bf3df072..602d63a1de7 100644 --- a/homeassistant/components/ukraine_alarm/translations/ja.json +++ b/homeassistant/components/ukraine_alarm/translations/ja.json @@ -23,7 +23,8 @@ "state": { "data": { "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" - } + }, + "description": "\u30e2\u30cb\u30bf\u30fc\u3059\u308b\u72b6\u614b\u3092\u9078\u629e" }, "user": { "data": { diff --git a/homeassistant/components/ws66i/translations/bg.json b/homeassistant/components/ws66i/translations/bg.json new file mode 100644 index 00000000000..ba078ae5f11 --- /dev/null +++ b/homeassistant/components/ws66i/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\u0448\u043d\u043e \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": { + "ip_address": "IP \u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/ja.json b/homeassistant/components/ws66i/translations/ja.json index 2f93c93954b..2ae21b3916c 100644 --- a/homeassistant/components/ws66i/translations/ja.json +++ b/homeassistant/components/ws66i/translations/ja.json @@ -11,7 +11,23 @@ "user": { "data": { "ip_address": "IP\u30a2\u30c9\u30ec\u30b9" - } + }, + "title": "\u30c7\u30d0\u30a4\u30b9\u306b\u63a5\u7d9a" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "\u30bd\u30fc\u30b9#1\u306e\u540d\u524d", + "source_2": "\u30bd\u30fc\u30b9#2\u306e\u540d\u524d", + "source_3": "\u30bd\u30fc\u30b9#3\u306e\u540d\u524d", + "source_4": "\u30bd\u30fc\u30b9#4\u306e\u540d\u524d", + "source_5": "\u30bd\u30fc\u30b9#5\u306e\u540d\u524d", + "source_6": "\u30bd\u30fc\u30b9#6\u306e\u540d\u524d" + }, + "title": "\u30bd\u30fc\u30b9\u306e\u8a2d\u5b9a" } } } diff --git a/homeassistant/components/yamaha_musiccast/translations/select.fr.json b/homeassistant/components/yamaha_musiccast/translations/select.fr.json index f8ce2da31a7..a12e6c3f26c 100644 --- a/homeassistant/components/yamaha_musiccast/translations/select.fr.json +++ b/homeassistant/components/yamaha_musiccast/translations/select.fr.json @@ -41,7 +41,7 @@ "dts_neo6_cinema": "DTS Neo:6 Cin\u00e9ma", "dts_neo6_music": "DTS Neo:6 Musique", "dts_neural_x": "DTS Neural:X", - "toggle": "Permuter" + "toggle": "Basculer" }, "yamaha_musiccast__zone_tone_control_mode": { "auto": "Auto", From f18d7942934c95ef3fec7af00f7828344621a911 Mon Sep 17 00:00:00 2001 From: dacwe Date: Wed, 11 May 2022 02:34:03 +0200 Subject: [PATCH 0378/3516] Bump yalexs to 1.1.25 for handling locks in "secure locked mode" as locked (#71666) --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index c1156c9aea9..34bd4843f32 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.24"], + "requirements": ["yalexs==1.1.25"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index f3204e93502..94dfcbd4aa0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2468,7 +2468,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.august -yalexs==1.1.24 +yalexs==1.1.25 # homeassistant.components.yeelight yeelight==0.7.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4b109b6b07..e3b856b10d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1617,7 +1617,7 @@ xmltodict==0.12.0 yalesmartalarmclient==0.3.8 # homeassistant.components.august -yalexs==1.1.24 +yalexs==1.1.25 # homeassistant.components.yeelight yeelight==0.7.10 From fb6cdb5a38463809104ac91edc55bca395ed4e15 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 11 May 2022 09:57:54 +0200 Subject: [PATCH 0379/3516] Optimistically set hs_color in template light (#71629) * Optimistically set hs_color in template light * Update light.py * Update test --- homeassistant/components/template/light.py | 18 ++- tests/components/template/test_light.py | 133 ++++++++++++++++++++- 2 files changed, 145 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 0ebdc3fb93a..1988e77d223 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -352,25 +352,37 @@ class LightTemplate(TemplateEntity, LightEntity): optimistic_set = True if self._level_template is None and ATTR_BRIGHTNESS in kwargs: - _LOGGER.info( + _LOGGER.debug( "Optimistically setting brightness to %s", kwargs[ATTR_BRIGHTNESS] ) self._brightness = kwargs[ATTR_BRIGHTNESS] optimistic_set = True if self._white_value_template is None and ATTR_WHITE_VALUE in kwargs: - _LOGGER.info( + _LOGGER.debug( "Optimistically setting white value to %s", kwargs[ATTR_WHITE_VALUE] ) self._white_value = kwargs[ATTR_WHITE_VALUE] optimistic_set = True if self._temperature_template is None and ATTR_COLOR_TEMP in kwargs: - _LOGGER.info( + _LOGGER.debug( "Optimistically setting color temperature to %s", kwargs[ATTR_COLOR_TEMP], ) self._temperature = kwargs[ATTR_COLOR_TEMP] + if self._color_template is None: + self._color = None + optimistic_set = True + + if self._color_template is None and ATTR_HS_COLOR in kwargs: + _LOGGER.debug( + "Optimistically setting color to %s", + kwargs[ATTR_HS_COLOR], + ) + self._color = kwargs[ATTR_HS_COLOR] + if self._temperature_template is None: + self._temperature = None optimistic_set = True common_params = {} diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 0888511c583..924a5c25538 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -1183,7 +1183,7 @@ async def test_entity_picture_template(hass, start_ha): @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) @pytest.mark.parametrize( "supported_features,supported_color_modes, expected_color_mode", - [(SUPPORT_COLOR, [ColorMode.HS], ColorMode.UNKNOWN)], + [(SUPPORT_COLOR, [ColorMode.HS], ColorMode.HS)], ) @pytest.mark.parametrize( "config", @@ -1253,8 +1253,8 @@ async def test_color_action_no_template( state = hass.states.get("light.test_template_light") assert state.state == STATE_ON - assert state.attributes["color_mode"] == expected_color_mode # hs_color is None - assert "hs_color" not in state.attributes + assert state.attributes["color_mode"] == expected_color_mode + assert state.attributes.get("hs_color") == (40, 50) assert state.attributes["supported_color_modes"] == supported_color_modes assert state.attributes["supported_features"] == supported_features @@ -1308,6 +1308,133 @@ async def test_color_template( assert state.attributes["supported_features"] == supported_features +@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize( + "supported_features,supported_color_modes", + [(SUPPORT_COLOR | SUPPORT_COLOR_TEMP, [ColorMode.COLOR_TEMP, ColorMode.HS])], +) +@pytest.mark.parametrize( + "config", + [ + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{1 == 1}}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_color": [ + { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "h": "{{h}}", + "s": "{{s}}", + }, + }, + ], + "set_temperature": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "color_temp": "{{color_temp}}", + }, + }, + } + }, + } + }, + ], +) +async def test_color_and_temperature_actions_no_template( + hass, start_ha, calls, supported_features, supported_color_modes +): + """Test setting color and color temperature with optimistic template.""" + state = hass.states.get("light.test_template_light") + assert state.attributes.get("hs_color") is None + + # Optimistically set color, light should be in hs_color mode + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_template_light", ATTR_HS_COLOR: (40, 50)}, + blocking=True, + ) + + assert len(calls) == 1 + assert calls[-1].data["h"] == 40 + assert calls[-1].data["s"] == 50 + + state = hass.states.get("light.test_template_light") + assert state.attributes["color_mode"] == ColorMode.HS + assert "color_temp" not in state.attributes + assert state.attributes["hs_color"] == (40, 50) + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features + + # Optimistically set color temp, light should be in color temp mode + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_template_light", ATTR_COLOR_TEMP: 123}, + blocking=True, + ) + + assert len(calls) == 2 + assert calls[-1].data["color_temp"] == 123 + + state = hass.states.get("light.test_template_light") + assert state.attributes["color_mode"] == ColorMode.COLOR_TEMP + assert state.attributes["color_temp"] == 123 + assert "hs_color" in state.attributes # Color temp represented as hs_color + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features + + # Optimistically set color, light should again be in hs_color mode + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_template_light", ATTR_HS_COLOR: (10, 20)}, + blocking=True, + ) + + assert len(calls) == 3 + assert calls[-1].data["h"] == 10 + assert calls[-1].data["s"] == 20 + + state = hass.states.get("light.test_template_light") + assert state.attributes["color_mode"] == ColorMode.HS + assert "color_temp" not in state.attributes + assert state.attributes["hs_color"] == (10, 20) + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features + + # Optimistically set color temp, light should again be in color temp mode + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_template_light", ATTR_COLOR_TEMP: 234}, + blocking=True, + ) + + assert len(calls) == 4 + assert calls[-1].data["color_temp"] == 234 + + state = hass.states.get("light.test_template_light") + assert state.attributes["color_mode"] == ColorMode.COLOR_TEMP + assert state.attributes["color_temp"] == 234 + assert "hs_color" in state.attributes # Color temp represented as hs_color + assert state.attributes["supported_color_modes"] == supported_color_modes + assert state.attributes["supported_features"] == supported_features + + @pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) @pytest.mark.parametrize( "config", From 2d5a82d10e23d87106081fade29763822c737f5f Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Wed, 11 May 2022 10:48:13 +0200 Subject: [PATCH 0380/3516] Refactor Plugwise select and add regulation_mode selector (#69210) Co-authored-by: Franck Nijhof --- homeassistant/components/plugwise/select.py | 99 +++++++++++++++------ 1 file changed, 74 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index b3cfb366889..cac9c3e5637 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -1,64 +1,113 @@ """Plugwise Select component for Home Assistant.""" from __future__ import annotations -from homeassistant.components.select import SelectEntity +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any + +from plugwise import Smile + +from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, THERMOSTAT_CLASSES +from .const import DOMAIN from .coordinator import PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity +@dataclass +class PlugwiseSelectDescriptionMixin: + """Mixin values for Plugwise Select entities.""" + + command: Callable[[Smile, str, str], Awaitable[Any]] + current_option: str + options: str + + +@dataclass +class PlugwiseSelectEntityDescription( + SelectEntityDescription, PlugwiseSelectDescriptionMixin +): + """Class describing Plugwise Number entities.""" + + +SELECT_TYPES = ( + PlugwiseSelectEntityDescription( + key="select_schedule", + name="Thermostat Schedule", + icon="mdi:calendar-clock", + command=lambda api, loc, opt: api.set_schedule_state(loc, opt, STATE_ON), + current_option="selected_schedule", + options="available_schedules", + ), + PlugwiseSelectEntityDescription( + key="select_regulation_mode", + name="Regulation Mode", + icon="mdi:hvac", + entity_category=EntityCategory.CONFIG, + command=lambda api, loc, opt: api.set_regulation_mode(opt), + current_option="regulation_mode", + options="regulation_modes", + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Smile selector from a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities( - PlugwiseSelectEntity(coordinator, device_id) - for device_id, device in coordinator.data.devices.items() - if device["class"] in THERMOSTAT_CLASSES - and len(device.get("available_schedules")) > 1 - ) + coordinator: PlugwiseDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + entities: list[PlugwiseSelectEntity] = [] + for device_id, device in coordinator.data.devices.items(): + for description in SELECT_TYPES: + if description.options in device and len(device[description.options]) > 1: + entities.append( + PlugwiseSelectEntity(coordinator, device_id, description) + ) + + async_add_entities(entities) class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity): """Represent Smile selector.""" + entity_description: PlugwiseSelectEntityDescription + def __init__( self, coordinator: PlugwiseDataUpdateCoordinator, device_id: str, + entity_description: PlugwiseSelectEntityDescription, ) -> None: """Initialise the selector.""" super().__init__(coordinator, device_id) - self._attr_unique_id = f"{device_id}-select_schedule" - self._attr_name = (f"{self.device.get('name', '')} Select Schedule").lstrip() + self.entity_description = entity_description + self._attr_unique_id = f"{device_id}-{entity_description.key}" + self._attr_name = (f"{self.device['name']} {entity_description.name}").lstrip() @property - def current_option(self) -> str | None: + def current_option(self) -> str: """Return the selected entity option to represent the entity state.""" - return self.device.get("selected_schedule") + return self.device[self.entity_description.current_option] @property def options(self) -> list[str]: - """Return a set of selectable options.""" - return self.device.get("available_schedules", []) + """Return the selectable entity options.""" + return self.device[self.entity_description.options] async def async_select_option(self, option: str) -> None: - """Change the selected option.""" - if not ( - await self.coordinator.api.set_schedule_state( - self.device.get("location"), - option, - STATE_ON, - ) - ): - raise HomeAssistantError(f"Failed to change to schedule {option}") + """Change to the selected entity option.""" + await self.entity_description.command( + self.coordinator.api, self.device["location"], option + ) + await self.coordinator.async_request_refresh() From 1304808f8963e58dccda62fd176d8d38d3dbac1a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 May 2022 06:09:55 -0500 Subject: [PATCH 0381/3516] Add additional test cover for history_stats (#71648) --- tests/components/history_stats/test_sensor.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index bfa0c8f415e..297e3a5a363 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -1387,3 +1387,85 @@ async def test_measure_cet(hass, recorder_mock): assert hass.states.get("sensor.sensor2").state == "0.83" assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor4").state == "83.3" + + +async def test_end_time_with_microseconds_zeroed(hass, recorder_mock): + """Test the history statistics sensor that has the end time microseconds zeroed out.""" + hass.config.set_time_zone("Europe/Berlin") + start_of_today = dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) + start_time = start_of_today + timedelta(minutes=60) + t0 = start_time + timedelta(minutes=20) + t1 = t0 + timedelta(minutes=10) + t2 = t1 + timedelta(minutes=10) + time_200 = start_of_today + timedelta(hours=2) + + def _fake_states(*args, **kwargs): + return { + "binary_sensor.heatpump_compressor_state": [ + ha.State( + "binary_sensor.heatpump_compressor_state", "on", last_changed=t0 + ), + ha.State( + "binary_sensor.heatpump_compressor_state", + "off", + last_changed=t1, + ), + ha.State( + "binary_sensor.heatpump_compressor_state", "on", last_changed=t2 + ), + ] + } + + with freeze_time(time_200), patch( + "homeassistant.components.recorder.history.state_changes_during_period", + _fake_states, + ): + await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "history_stats", + "entity_id": "binary_sensor.heatpump_compressor_state", + "name": "heatpump_compressor_today", + "state": "on", + "start": "{{ now().replace(hour=0, minute=0, second=0, microsecond=0) }}", + "end": "{{ now().replace(microsecond=0) }}", + "type": "time", + }, + ] + }, + ) + await hass.async_block_till_done() + await async_update_entity(hass, "sensor.heatpump_compressor_today") + await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83" + async_fire_time_changed(hass, time_200) + await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83" + hass.states.async_set("binary_sensor.heatpump_compressor_state", "off") + await hass.async_block_till_done() + + time_400 = start_of_today + timedelta(hours=4) + with freeze_time(time_400): + async_fire_time_changed(hass, time_400) + await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83" + hass.states.async_set("binary_sensor.heatpump_compressor_state", "on") + await hass.async_block_till_done() + time_600 = start_of_today + timedelta(hours=6) + with freeze_time(time_600): + async_fire_time_changed(hass, time_600) + await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "3.83" + rolled_to_next_day = start_of_today + timedelta(days=1) + assert rolled_to_next_day.hour == 0 + assert rolled_to_next_day.minute == 0 + assert rolled_to_next_day.second == 0 + assert rolled_to_next_day.microsecond == 0 + + with freeze_time(rolled_to_next_day): + async_fire_time_changed(hass, rolled_to_next_day) + await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "0.0" From 557cba118f8ee68cdd2f1f6ca29c30a6facbf3a6 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Wed, 11 May 2022 13:10:34 +0200 Subject: [PATCH 0382/3516] Bump devolo_home_control (#71639) --- homeassistant/components/devolo_home_control/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/devolo_home_control/manifest.json b/homeassistant/components/devolo_home_control/manifest.json index 75cca12d99e..0de3cd7f07d 100644 --- a/homeassistant/components/devolo_home_control/manifest.json +++ b/homeassistant/components/devolo_home_control/manifest.json @@ -2,7 +2,7 @@ "domain": "devolo_home_control", "name": "devolo Home Control", "documentation": "https://www.home-assistant.io/integrations/devolo_home_control", - "requirements": ["devolo-home-control-api==0.18.1"], + "requirements": ["devolo-home-control-api==0.18.2"], "after_dependencies": ["zeroconf"], "config_flow": true, "codeowners": ["@2Fake", "@Shutgun"], diff --git a/requirements_all.txt b/requirements_all.txt index 94dfcbd4aa0..44d44e0602c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -539,7 +539,7 @@ deluge-client==1.7.1 denonavr==0.10.11 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.18.1 +devolo-home-control-api==0.18.2 # homeassistant.components.devolo_home_network devolo-plc-api==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3b856b10d9..cfe9ad40cda 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -394,7 +394,7 @@ deluge-client==1.7.1 denonavr==0.10.11 # homeassistant.components.devolo_home_control -devolo-home-control-api==0.18.1 +devolo-home-control-api==0.18.2 # homeassistant.components.devolo_home_network devolo-plc-api==0.8.0 From 34d4eb7c151e962fdcb5821d37598353c7e18436 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 11 May 2022 07:12:44 -0400 Subject: [PATCH 0383/3516] Improve eight sleep code quality and fix bug (#71622) --- .../components/eight_sleep/__init__.py | 2 - .../components/eight_sleep/binary_sensor.py | 3 +- .../components/eight_sleep/sensor.py | 100 ++++++------------ 3 files changed, 33 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index eece6e69ed5..e986a7f6d60 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -156,14 +156,12 @@ class EightSleepBaseEntity(CoordinatorEntity[DataUpdateCoordinator]): eight: EightSleep, user_id: str | None, sensor: str, - units: str | None = None, ) -> None: """Initialize the data object.""" super().__init__(coordinator) self._eight = eight self._user_id = user_id self._sensor = sensor - self._units = units self._user_obj: EightUser | None = None if self._user_id: self._user_obj = self._eight.users[user_id] diff --git a/homeassistant/components/eight_sleep/binary_sensor.py b/homeassistant/components/eight_sleep/binary_sensor.py index a63389ef3f9..868a5177cfe 100644 --- a/homeassistant/components/eight_sleep/binary_sensor.py +++ b/homeassistant/components/eight_sleep/binary_sensor.py @@ -45,6 +45,8 @@ async def async_setup_platform( class EightHeatSensor(EightSleepBaseEntity, BinarySensorEntity): """Representation of a Eight Sleep heat-based sensor.""" + _attr_device_class = BinarySensorDeviceClass.OCCUPANCY + def __init__( self, coordinator: DataUpdateCoordinator, @@ -54,7 +56,6 @@ class EightHeatSensor(EightSleepBaseEntity, BinarySensorEntity): ) -> None: """Initialize the sensor.""" super().__init__(coordinator, eight, user_id, sensor) - self._attr_device_class = BinarySensorDeviceClass.OCCUPANCY assert self._user_obj _LOGGER.debug( "Presence Sensor: %s, Side: %s, User: %s", diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index 145292d6728..b405617e276 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -6,8 +6,8 @@ from typing import Any from pyeight.eight import EightSleep -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -68,24 +68,19 @@ async def async_setup_platform( heat_coordinator: DataUpdateCoordinator = hass.data[DOMAIN][DATA_HEAT] user_coordinator: DataUpdateCoordinator = hass.data[DOMAIN][DATA_USER] - if hass.config.units.is_metric: - units = "si" - else: - units = "us" - all_sensors: list[SensorEntity] = [] for obj in eight.users.values(): for sensor in EIGHT_USER_SENSORS: all_sensors.append( - EightUserSensor(user_coordinator, eight, obj.userid, sensor, units) + EightUserSensor(user_coordinator, eight, obj.userid, sensor) ) for sensor in EIGHT_HEAT_SENSORS: all_sensors.append( EightHeatSensor(heat_coordinator, eight, obj.userid, sensor) ) for sensor in EIGHT_ROOM_SENSORS: - all_sensors.append(EightRoomSensor(user_coordinator, eight, sensor, units)) + all_sensors.append(EightRoomSensor(user_coordinator, eight, sensor)) async_add_entities(all_sensors) @@ -93,6 +88,8 @@ async def async_setup_platform( class EightHeatSensor(EightSleepBaseEntity, SensorEntity): """Representation of an eight sleep heat-based sensor.""" + _attr_native_unit_of_measurement = PERCENTAGE + def __init__( self, coordinator: DataUpdateCoordinator, @@ -102,7 +99,6 @@ class EightHeatSensor(EightSleepBaseEntity, SensorEntity): ) -> None: """Initialize the sensor.""" super().__init__(coordinator, eight, user_id, sensor) - self._attr_native_unit_of_measurement = PERCENTAGE assert self._user_obj _LOGGER.debug( @@ -139,6 +135,13 @@ def _get_breakdown_percent( return 0 +def _get_rounded_value(attr: dict[str, Any], key: str) -> int | float | None: + """Get rounded value for given key.""" + if (val := attr.get(key)) is None: + return None + return round(val, 2) + + class EightUserSensor(EightSleepBaseEntity, SensorEntity): """Representation of an eight sleep user-based sensor.""" @@ -148,14 +151,17 @@ class EightUserSensor(EightSleepBaseEntity, SensorEntity): eight: EightSleep, user_id: str, sensor: str, - units: str, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, eight, user_id, sensor, units) + super().__init__(coordinator, eight, user_id, sensor) assert self._user_obj if self._sensor == "bed_temperature": self._attr_icon = "mdi:thermometer" + self._attr_device_class = SensorDeviceClass.TEMPERATURE + self._attr_native_unit_of_measurement = TEMP_CELSIUS + elif self._sensor in ("current_sleep", "last_sleep", "current_sleep_fitness"): + self._attr_native_unit_of_measurement = "Score" _LOGGER.debug( "User Sensor: %s, Side: %s, User: %s", @@ -179,41 +185,13 @@ class EightUserSensor(EightSleepBaseEntity, SensorEntity): return self._user_obj.last_sleep_score if self._sensor == "bed_temperature": - temp = self._user_obj.current_values["bed_temp"] - try: - if self._units == "si": - return round(temp, 2) - return round((temp * 1.8) + 32, 2) - except TypeError: - return None + return self._user_obj.current_values["bed_temp"] if self._sensor == "sleep_stage": return self._user_obj.current_values["stage"] return None - @property - def native_unit_of_measurement(self) -> str | None: - """Return the unit the value is expressed in.""" - if self._sensor in ("current_sleep", "last_sleep", "current_sleep_fitness"): - return "Score" - if self._sensor == "bed_temperature": - if self._units == "si": - return TEMP_CELSIUS - return TEMP_FAHRENHEIT - return None - - def _get_rounded_value( - self, attr: dict[str, Any], key: str, use_units: bool = True - ) -> int | float | None: - """Get rounded value based on units for given key.""" - try: - if self._units == "si" or not use_units: - return round(attr["room_temp"], 2) - return round((attr["room_temp"] * 1.8) + 32, 2) - except TypeError: - return None - @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return device state attributes.""" @@ -255,26 +233,18 @@ class EightUserSensor(EightSleepBaseEntity, SensorEntity): ) state_attr[ATTR_REM_PERC] = _get_breakdown_percent(attr, "rem", sleep_time) - room_temp = self._get_rounded_value(attr, "room_temp") - bed_temp = self._get_rounded_value(attr, "bed_temp") + room_temp = _get_rounded_value(attr, "room_temp") + bed_temp = _get_rounded_value(attr, "bed_temp") if "current" in self._sensor: - state_attr[ATTR_RESP_RATE] = self._get_rounded_value( - attr, "resp_rate", False - ) - state_attr[ATTR_HEART_RATE] = self._get_rounded_value( - attr, "heart_rate", False - ) + state_attr[ATTR_RESP_RATE] = _get_rounded_value(attr, "resp_rate") + state_attr[ATTR_HEART_RATE] = _get_rounded_value(attr, "heart_rate") state_attr[ATTR_SLEEP_STAGE] = attr["stage"] state_attr[ATTR_ROOM_TEMP] = room_temp state_attr[ATTR_BED_TEMP] = bed_temp elif "last" in self._sensor: - state_attr[ATTR_AVG_RESP_RATE] = self._get_rounded_value( - attr, "resp_rate", False - ) - state_attr[ATTR_AVG_HEART_RATE] = self._get_rounded_value( - attr, "heart_rate", False - ) + state_attr[ATTR_AVG_RESP_RATE] = _get_rounded_value(attr, "resp_rate") + state_attr[ATTR_AVG_HEART_RATE] = _get_rounded_value(attr, "heart_rate") state_attr[ATTR_AVG_ROOM_TEMP] = room_temp state_attr[ATTR_AVG_BED_TEMP] = bed_temp @@ -284,28 +254,20 @@ class EightUserSensor(EightSleepBaseEntity, SensorEntity): class EightRoomSensor(EightSleepBaseEntity, SensorEntity): """Representation of an eight sleep room sensor.""" + _attr_icon = "mdi:thermometer" + _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_native_unit_of_measurement = TEMP_CELSIUS + def __init__( self, coordinator: DataUpdateCoordinator, eight: EightSleep, sensor: str, - units: str, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, eight, None, sensor, units) - - self._attr_icon = "mdi:thermometer" - self._attr_native_unit_of_measurement: str = ( - TEMP_CELSIUS if self._units == "si" else TEMP_FAHRENHEIT - ) + super().__init__(coordinator, eight, None, sensor) @property def native_value(self) -> int | float | None: """Return the state of the sensor.""" - temp = self._eight.room_temperature() - try: - if self._units == "si": - return round(temp, 2) - return round((temp * 1.8) + 32, 2) - except TypeError: - return None + return self._eight.room_temperature() From 04e3cee6c363f9c580e11c0b9b2d5b9a35cfa06e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 May 2022 06:14:11 -0500 Subject: [PATCH 0384/3516] Bump flux_led to 0.28.29 (#71665) --- 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 e28b55869c7..d2eb4e1e2e0 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.28.28"], + "requirements": ["flux_led==0.28.29"], "quality_scale": "platinum", "codeowners": ["@icemanch", "@bdraco"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 44d44e0602c..4f8bf564915 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -657,7 +657,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.28 +flux_led==0.28.29 # homeassistant.components.homekit # homeassistant.components.recorder diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfe9ad40cda..e274844449b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -460,7 +460,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.28 +flux_led==0.28.29 # homeassistant.components.homekit # homeassistant.components.recorder From b9f3f1af6c1d10d16189dab04c4e68969568f381 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 11 May 2022 13:18:20 +0200 Subject: [PATCH 0385/3516] Streamline setup of deCONZ fan platform (#71658) --- homeassistant/components/deconz/fan.py | 30 +++++++------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index 8fcc7c14f9d..93daae55ccf 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any, Literal +from pydeconz.models.event import EventType from pydeconz.models.light.fan import ( FAN_SPEED_25_PERCENT, FAN_SPEED_50_PERCENT, @@ -15,7 +16,6 @@ from pydeconz.models.light.fan import ( from homeassistant.components.fan import DOMAIN, FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( ordered_list_item_to_percentage, @@ -43,33 +43,19 @@ async def async_setup_entry( gateway.entities[DOMAIN] = set() @callback - def async_add_fan(lights: list[Fan] | None = None) -> None: + def async_add_fan(_: EventType, fan_id: str) -> None: """Add fan from deCONZ.""" - entities = [] - - if lights is None: - lights = list(gateway.api.lights.fans.values()) - - for light in lights: - - if ( - isinstance(light, Fan) - and light.unique_id not in gateway.entities[DOMAIN] - ): - entities.append(DeconzFan(light, gateway)) - - if entities: - async_add_entities(entities) + fan = gateway.api.lights.fans[fan_id] + async_add_entities([DeconzFan(fan, gateway)]) config_entry.async_on_unload( - async_dispatcher_connect( - hass, - gateway.signal_new_light, + gateway.api.lights.fans.subscribe( async_add_fan, + EventType.ADDED, ) ) - - async_add_fan() + for fan_id in gateway.api.lights.fans: + async_add_fan(EventType.ADDED, fan_id) class DeconzFan(DeconzDevice, FanEntity): From 920c0d166732e2059027239f09a924f992151844 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 11 May 2022 13:19:28 +0200 Subject: [PATCH 0386/3516] Streamline setup of deCONZ cover platform (#71656) --- homeassistant/components/deconz/cover.py | 29 +++++++----------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index d5656d31bee..3d5dabb73c1 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any, cast +from pydeconz.models.event import EventType from pydeconz.models.light.cover import Cover from homeassistant.components.cover import ( @@ -15,7 +16,6 @@ from homeassistant.components.cover import ( ) 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 .deconz_device import DeconzDevice @@ -38,32 +38,19 @@ async def async_setup_entry( gateway.entities[DOMAIN] = set() @callback - def async_add_cover(lights: list[Cover] | None = None) -> None: + def async_add_cover(_: EventType, cover_id: str) -> None: """Add cover from deCONZ.""" - entities = [] - - if lights is None: - lights = list(gateway.api.lights.covers.values()) - - for light in lights: - if ( - isinstance(light, Cover) - and light.unique_id not in gateway.entities[DOMAIN] - ): - entities.append(DeconzCover(light, gateway)) - - if entities: - async_add_entities(entities) + cover = gateway.api.lights.covers[cover_id] + async_add_entities([DeconzCover(cover, gateway)]) config_entry.async_on_unload( - async_dispatcher_connect( - hass, - gateway.signal_new_light, + gateway.api.lights.covers.subscribe( async_add_cover, + EventType.ADDED, ) ) - - async_add_cover() + for cover_id in gateway.api.lights.covers: + async_add_cover(EventType.ADDED, cover_id) class DeconzCover(DeconzDevice, CoverEntity): From db17d7aecfcecb82f16d2f5cd615ddb0a845cbb6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 11 May 2022 13:20:09 +0200 Subject: [PATCH 0387/3516] Streamline setup of deCONZ lock from light platform (#71659) --- homeassistant/components/deconz/lock.py | 28 +++++++------------------ 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index 51c2d4fc0fc..1ecc1802b74 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Any +from pydeconz.models.event import EventType from pydeconz.models.light.lock import Lock from pydeconz.models.sensor.door_lock import DoorLock @@ -27,31 +28,19 @@ async def async_setup_entry( gateway.entities[DOMAIN] = set() @callback - def async_add_lock_from_light(lights: list[Lock] | None = None) -> None: + def async_add_lock_from_light(_: EventType, lock_id: str) -> None: """Add lock from deCONZ.""" - entities = [] - - if lights is None: - lights = list(gateway.api.lights.locks.values()) - - for light in lights: - - if ( - isinstance(light, Lock) - and light.unique_id not in gateway.entities[DOMAIN] - ): - entities.append(DeconzLock(light, gateway)) - - if entities: - async_add_entities(entities) + lock = gateway.api.lights.locks[lock_id] + async_add_entities([DeconzLock(lock, gateway)]) config_entry.async_on_unload( - async_dispatcher_connect( - hass, - gateway.signal_new_light, + gateway.api.lights.locks.subscribe( async_add_lock_from_light, + EventType.ADDED, ) ) + for lock_id in gateway.api.lights.locks: + async_add_lock_from_light(EventType.ADDED, lock_id) @callback def async_add_lock_from_sensor(sensors: list[DoorLock] | None = None) -> None: @@ -80,7 +69,6 @@ async def async_setup_entry( ) ) - async_add_lock_from_light() async_add_lock_from_sensor() From 44f8c555a6ff94cc82d587a7ce274d2e44538340 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 11 May 2022 13:30:16 +0200 Subject: [PATCH 0388/3516] Streamline setup of deCONZ siren platform (#71660) --- homeassistant/components/deconz/siren.py | 30 +++++++----------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/deconz/siren.py b/homeassistant/components/deconz/siren.py index c279afae696..2a11cd5346a 100644 --- a/homeassistant/components/deconz/siren.py +++ b/homeassistant/components/deconz/siren.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any +from pydeconz.models.event import EventType from pydeconz.models.light.siren import Siren from homeassistant.components.siren import ( @@ -13,7 +14,6 @@ from homeassistant.components.siren import ( ) 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 .deconz_device import DeconzDevice @@ -30,33 +30,19 @@ async def async_setup_entry( gateway.entities[DOMAIN] = set() @callback - def async_add_siren(lights: list[Siren] | None = None) -> None: + def async_add_siren(_: EventType, siren_id: str) -> None: """Add siren from deCONZ.""" - entities = [] - - if lights is None: - lights = list(gateway.api.lights.sirens.values()) - - for light in lights: - - if ( - isinstance(light, Siren) - and light.unique_id not in gateway.entities[DOMAIN] - ): - entities.append(DeconzSiren(light, gateway)) - - if entities: - async_add_entities(entities) + siren = gateway.api.lights.sirens[siren_id] + async_add_entities([DeconzSiren(siren, gateway)]) config_entry.async_on_unload( - async_dispatcher_connect( - hass, - gateway.signal_new_light, + gateway.api.lights.sirens.subscribe( async_add_siren, + EventType.ADDED, ) ) - - async_add_siren() + for siren_id in gateway.api.lights.sirens: + async_add_siren(EventType.ADDED, siren_id) class DeconzSiren(DeconzDevice, SirenEntity): From 554f079b0244a582e4244ec6ad7654c617ffbcb3 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 11 May 2022 13:31:16 +0200 Subject: [PATCH 0389/3516] Streamline setup of deCONZ switch platform (#71661) --- homeassistant/components/deconz/switch.py | 32 +++++++---------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index f1ff95183dd..6789b20b3c7 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -4,12 +4,12 @@ from __future__ import annotations from typing import Any +from pydeconz.models.event import EventType from pydeconz.models.light.light import Light from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import POWER_PLUGS @@ -30,33 +30,21 @@ async def async_setup_entry( gateway.entities[DOMAIN] = set() @callback - def async_add_switch(lights: list[Light] | None = None) -> None: + def async_add_switch(_: EventType, light_id: str) -> None: """Add switch from deCONZ.""" - entities = [] - - if lights is None: - lights = list(gateway.api.lights.lights.values()) - - for light in lights: - - if ( - light.type in POWER_PLUGS - and light.unique_id not in gateway.entities[DOMAIN] - ): - entities.append(DeconzPowerPlug(light, gateway)) - - if entities: - async_add_entities(entities) + light = gateway.api.lights.lights[light_id] + if light.type not in POWER_PLUGS: + return + async_add_entities([DeconzPowerPlug(light, gateway)]) config_entry.async_on_unload( - async_dispatcher_connect( - hass, - gateway.signal_new_light, + gateway.api.lights.lights.subscribe( async_add_switch, + EventType.ADDED, ) ) - - async_add_switch() + for light_id in gateway.api.lights.lights: + async_add_switch(EventType.ADDED, light_id) class DeconzPowerPlug(DeconzDevice, SwitchEntity): From 49491bcda55125369319fe1f0b6036ddfc4a2a2a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 11 May 2022 19:36:30 +0200 Subject: [PATCH 0390/3516] Sensibo test data adjustment from late review (#71482) * Sensibo test data * Separate pytest fixture for json load * Scope to session --- tests/components/sensibo/conftest.py | 34 +- tests/components/sensibo/fixtures/data.json | 551 ++++++++++++++++++ .../components/sensibo/test_binary_sensor.py | 14 +- tests/components/sensibo/test_climate.py | 107 ++-- tests/components/sensibo/test_coordinator.py | 15 +- tests/components/sensibo/test_diagnostics.py | 36 +- tests/components/sensibo/test_entity.py | 16 +- tests/components/sensibo/test_init.py | 17 +- 8 files changed, 688 insertions(+), 102 deletions(-) create mode 100644 tests/components/sensibo/fixtures/data.json diff --git a/tests/components/sensibo/conftest.py b/tests/components/sensibo/conftest.py index 9380ee6ab6b..48c9317a5cb 100644 --- a/tests/components/sensibo/conftest.py +++ b/tests/components/sensibo/conftest.py @@ -1,8 +1,12 @@ """Fixtures for the Sensibo integration.""" from __future__ import annotations +import json +from typing import Any from unittest.mock import patch +from pysensibo import SensiboClient +from pysensibo.model import SensiboData import pytest from homeassistant.components.sensibo.const import DOMAIN @@ -10,13 +14,13 @@ from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from . import ENTRY_CONFIG -from .response import DATA_FROM_API -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker @pytest.fixture -async def load_int(hass: HomeAssistant) -> MockConfigEntry: +async def load_int(hass: HomeAssistant, get_data: SensiboData) -> MockConfigEntry: """Set up the Sensibo integration in Home Assistant.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -31,7 +35,7 @@ async def load_int(hass: HomeAssistant) -> MockConfigEntry: with patch( "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=DATA_FROM_API, + return_value=get_data, ), patch( "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, @@ -43,3 +47,25 @@ async def load_int(hass: HomeAssistant) -> MockConfigEntry: await hass.async_block_till_done() return config_entry + + +@pytest.fixture(name="get_data") +async def get_data_from_library( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, load_json: dict[str, Any] +) -> SensiboData: + """Retrieve data from upstream Sensibo library.""" + + client = SensiboClient("123467890", aioclient_mock.create_session(hass.loop)) + with patch("pysensibo.SensiboClient.async_get_devices", return_value=load_json): + output = await client.async_get_devices_data() + await client._session.close() # pylint: disable=protected-access + return output + + +@pytest.fixture(name="load_json", scope="session") +def load_json_from_fixture() -> SensiboData: + """Load fixture with json data and return.""" + + data_fixture = load_fixture("data.json", "sensibo") + json_data: dict[str, Any] = json.loads(data_fixture) + return json_data diff --git a/tests/components/sensibo/fixtures/data.json b/tests/components/sensibo/fixtures/data.json new file mode 100644 index 00000000000..c75423fe464 --- /dev/null +++ b/tests/components/sensibo/fixtures/data.json @@ -0,0 +1,551 @@ +{ + "status": "success", + "result": [ + { + "isGeofenceOnEnterEnabledForThisUser": false, + "isClimateReactGeofenceOnEnterEnabledForThisUser": false, + "isMotionGeofenceOnEnterEnabled": false, + "isOwner": true, + "id": "ABC999111", + "qrId": "AAAAAAAAAA", + "temperatureUnit": "C", + "room": { + "uid": "99TT99TT", + "name": "Hallway", + "icon": "Lounge" + }, + "acState": { + "timestamp": { + "time": "2022-04-30T11:23:30.019722Z", + "secondsAgo": -1 + }, + "on": true, + "mode": "heat", + "fanLevel": "high", + "targetTemperature": 25, + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on" + }, + "lastStateChange": { + "time": "2022-04-30T11:21:30Z", + "secondsAgo": 119 + }, + "lastStateChangeToOn": { + "time": "2022-04-30T11:20:28Z", + "secondsAgo": 181 + }, + "lastStateChangeToOff": { + "time": "2022-04-30T11:21:30Z", + "secondsAgo": 119 + }, + "location": { + "id": "ZZZZZZZZZZZZ", + "name": "Home", + "latLon": [58.9806976, 20.5864297], + "address": ["Sealand 99", "Some county"], + "country": "United Country", + "createTime": { + "time": "2020-03-21T15:44:15Z", + "secondsAgo": 66543240 + } + }, + "connectionStatus": { + "isAlive": true, + "lastSeen": { + "time": "2022-04-30T11:22:57.894846Z", + "secondsAgo": 32 + } + }, + "firmwareVersion": "SKY30046", + "firmwareType": "esp8266ex", + "productModel": "skyv2", + "configGroup": "stable", + "currentlyAvailableFirmwareVersion": "SKY30046", + "cleanFiltersNotificationEnabled": false, + "shouldShowFilterCleaningNotification": false, + "isGeofenceOnExitEnabled": false, + "isClimateReactGeofenceOnExitEnabled": false, + "isMotionGeofenceOnExitEnabled": false, + "serial": "1234567890", + "sensorsCalibration": { + "temperature": 0.1, + "humidity": 0.0 + }, + "motionSensors": [ + { + "configGroup": "stable", + "connectionStatus": { + "isAlive": true, + "lastSeen": { + "secondsAgo": 86, + "time": "2022-01-01T18:59:48.665878Z" + } + }, + "firmwareType": "nrf52", + "firmwareVersion": "V17", + "id": "AABBCC", + "isMainSensor": true, + "macAddress": "00:04:00:B6:00:00", + "measurements": { + "batteryVoltage": 3000, + "humidity": 57, + "motion": true, + "rssi": -72, + "temperature": 23.9, + "time": { + "secondsAgo": 86, + "time": "2022-01-01T18:59:48.665878Z" + } + }, + "parentDeviceUid": "ABC999111", + "productModel": "motion_sensor", + "qrId": "AAZZAAZZ", + "serial": "12123434" + } + ], + "tags": [], + "timer": null, + "schedules": [ + { + "id": "11", + "isEnabled": false, + "acState": { + "on": false, + "targetTemperature": 21, + "temperatureUnit": "C", + "mode": "heat", + "fanLevel": "low", + "swing": "stopped", + "extra": { + "scheduler": { + "climate_react": null, + "motion": null, + "on": false, + "climate_react_settings": null, + "pure_boost": null + } + }, + "horizontalSwing": "stopped", + "light": "on" + }, + "createTime": "2022-04-17T15:41:05", + "createTimeSecondsAgo": 1107745, + "recurringDays": ["Wednesday", "Thursday"], + "targetTimeLocal": "17:40", + "timezone": "Europe/Stockholm", + "podUid": "ABC999111", + "nextTime": "2022-05-04T15:40:00", + "nextTimeSecondsFromNow": 360989 + } + ], + "motionConfig": { + "enabled": true, + "onEnterACChange": false, + "onEnterACState": null, + "onEnterCRChange": true, + "onExitACChange": true, + "onExitACState": null, + "onExitCRChange": true, + "onExitDelayMinutes": 20 + }, + "filtersCleaning": { + "acOnSecondsSinceLastFiltersClean": 667991, + "filtersCleanSecondsThreshold": 1080000, + "lastFiltersCleanTime": { + "time": "2022-03-12T15:24:26Z", + "secondsAgo": 4219143 + }, + "shouldCleanFilters": false + }, + "serviceSubscriptions": [], + "roomIsOccupied": true, + "mainMeasurementsSensor": { + "configGroup": "stable", + "connectionStatus": { + "isAlive": true, + "lastSeen": { + "secondsAgo": 86, + "time": "2022-01-01T18:59:48.665878Z" + } + }, + "firmwareType": "nrf52", + "firmwareVersion": "V17", + "id": "AABBCC", + "isMainSensor": true, + "macAddress": "00:04:00:B6:00:00", + "measurements": { + "batteryVoltage": 3000, + "humidity": 32.9, + "motion": false, + "rssi": -72, + "temperature": 21.2, + "time": { + "secondsAgo": 86, + "time": "2022-01-01T18:59:48.665878Z" + } + }, + "parentDeviceUid": "ABC999111", + "productModel": "motion_sensor", + "qrId": "AAZZAAZZ", + "serial": "12123434" + }, + "pureBoostConfig": null, + "warrantyEligible": "no", + "warrantyEligibleUntil": { + "time": "2020-04-18T15:43:08Z", + "secondsAgo": 64093221 + }, + "features": ["softShowPlus", "optimusTrial"], + "runningHealthcheck": null, + "lastHealthcheck": null, + "lastACStateChange": { + "id": "11", + "time": { + "time": "2022-04-30T11:21:29Z", + "secondsAgo": 120 + }, + "status": "Success", + "acState": { + "timestamp": { + "time": "2022-04-30T11:23:30.062645Z", + "secondsAgo": -1 + }, + "on": false, + "mode": "fan", + "fanLevel": "high", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on" + }, + "resultingAcState": { + "timestamp": { + "time": "2022-04-30T11:23:30.062688Z", + "secondsAgo": -1 + }, + "on": false, + "mode": "fan", + "fanLevel": "high", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on" + }, + "changedProperties": ["on"], + "reason": "UserRequest", + "failureReason": null, + "resolveTime": { + "time": "2022-04-30T11:21:30Z", + "secondsAgo": 119 + }, + "causedByScheduleId": null, + "causedByScheduleType": null + }, + "homekitSupported": false, + "remoteCapabilities": { + "modes": { + "cool": { + "temperatures": { + "F": { + "isNative": false, + "values": [64, 66, 68] + }, + "C": { + "isNative": true, + "values": [18, 19, 20] + } + }, + "fanLevels": ["quiet", "low", "medium"], + "swing": ["stopped", "fixedTop", "fixedMiddleTop"], + "horizontalSwing": ["stopped", "fixedLeft", "fixedCenterLeft"], + "light": ["on", "off"] + }, + "heat": { + "temperatures": { + "F": { + "isNative": false, + "values": [63, 64, 66] + }, + "C": { + "isNative": true, + "values": [17, 18, 19, 20] + } + }, + "fanLevels": ["quiet", "low", "medium"], + "swing": ["stopped", "fixedTop", "fixedMiddleTop"], + "horizontalSwing": ["stopped", "fixedLeft", "fixedCenterLeft"], + "light": ["on", "off"] + }, + "dry": { + "temperatures": { + "F": { + "isNative": false, + "values": [64, 66, 68] + }, + "C": { + "isNative": true, + "values": [18, 19, 20] + } + }, + "swing": ["stopped", "fixedTop", "fixedMiddleTop"], + "horizontalSwing": ["stopped", "fixedLeft", "fixedCenterLeft"], + "light": ["on", "off"] + }, + "auto": { + "temperatures": { + "F": { + "isNative": false, + "values": [64, 66, 68] + }, + "C": { + "isNative": true, + "values": [18, 19, 20, 21] + } + }, + "fanLevels": ["quiet", "low", "medium"], + "swing": ["stopped", "fixedTop", "fixedMiddleTop"], + "horizontalSwing": ["stopped", "fixedLeft", "fixedCenterLeft"], + "light": ["on", "off"] + }, + "fan": { + "temperatures": {}, + "fanLevels": ["quiet", "low", "medium"], + "swing": ["stopped", "fixedTop", "fixedMiddleTop"], + "horizontalSwing": ["stopped", "fixedLeft", "fixedCenterLeft"], + "light": ["on", "off"] + } + } + }, + "remote": { + "toggle": false, + "window": false + }, + "remoteFlavor": "Curious Sea Cucumber", + "remoteAlternatives": ["_mitsubishi2_night_heat"], + "smartMode": { + "enabled": false, + "type": "temperature", + "deviceUid": "ABC999111", + "lowTemperatureThreshold": 0.0, + "highTemperatureThreshold": 27.5, + "lowTemperatureState": { + "on": true, + "targetTemperature": 21, + "temperatureUnit": "C", + "mode": "heat", + "fanLevel": "low", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on" + }, + "highTemperatureState": { + "on": true, + "targetTemperature": 21, + "temperatureUnit": "C", + "mode": "cool", + "fanLevel": "high", + "swing": "stopped", + "horizontalSwing": "stopped", + "light": "on" + }, + "lowTemperatureWebhook": null, + "highTemperatureWebhook": null + }, + "measurements": { + "time": { + "time": "2022-04-30T11:22:57.894846Z", + "secondsAgo": 32 + }, + "temperature": 21.2, + "humidity": 32.9, + "motion": true, + "roomIsOccupied": true, + "feelsLike": 21.2, + "rssi": -45 + }, + "accessPoint": { + "ssid": "Sensibo-1234567890", + "password": null + }, + "macAddress": "00:02:00:B6:00:00", + "autoOffMinutes": null, + "autoOffEnabled": false, + "antiMoldTimer": null, + "antiMoldConfig": null + }, + { + "isGeofenceOnEnterEnabledForThisUser": false, + "isClimateReactGeofenceOnEnterEnabledForThisUser": false, + "isMotionGeofenceOnEnterEnabled": false, + "isOwner": true, + "id": "AAZZAAZZ", + "qrId": "AAAAAAAABB", + "temperatureUnit": "C", + "room": { + "uid": "99TT99ZZ", + "name": "Kitchen", + "icon": "Diningroom" + }, + "acState": { + "timestamp": { + "time": "2022-04-30T11:23:30.067312Z", + "secondsAgo": -1 + }, + "on": false, + "mode": "fan", + "fanLevel": "low", + "light": "on" + }, + "lastStateChange": { + "time": "2022-04-30T11:21:41Z", + "secondsAgo": 108 + }, + "lastStateChangeToOn": { + "time": "2022-04-30T09:43:26Z", + "secondsAgo": 6003 + }, + "lastStateChangeToOff": { + "time": "2022-04-30T11:21:37Z", + "secondsAgo": 112 + }, + "location": { + "id": "ZZZZZZZZZZZZ", + "name": "Home", + "latLon": [58.9806976, 20.5864297], + "address": ["Sealand 99", "Some county"], + "country": "United Country", + "createTime": { + "time": "2020-03-21T15:44:15Z", + "secondsAgo": 66543240 + } + }, + "connectionStatus": { + "isAlive": true, + "lastSeen": { + "time": "2022-04-30T11:23:20.642798Z", + "secondsAgo": 9 + } + }, + "firmwareVersion": "PUR00111", + "firmwareType": "pure-esp32", + "productModel": "pure", + "configGroup": "stable", + "currentlyAvailableFirmwareVersion": "PUR00111", + "cleanFiltersNotificationEnabled": false, + "shouldShowFilterCleaningNotification": false, + "isGeofenceOnExitEnabled": false, + "isClimateReactGeofenceOnExitEnabled": false, + "isMotionGeofenceOnExitEnabled": false, + "serial": "0987654321", + "sensorsCalibration": { + "temperature": 0.0, + "humidity": 0.0 + }, + "motionSensors": [], + "tags": [], + "timer": null, + "schedules": [], + "motionConfig": null, + "filtersCleaning": { + "acOnSecondsSinceLastFiltersClean": 415560, + "filtersCleanSecondsThreshold": 14256000, + "lastFiltersCleanTime": { + "time": "2022-04-23T15:58:45Z", + "secondsAgo": 588284 + }, + "shouldCleanFilters": false + }, + "serviceSubscriptions": [], + "roomIsOccupied": null, + "mainMeasurementsSensor": null, + "pureBoostConfig": { + "enabled": false, + "sensitivity": "N", + "measurements_integration": true, + "ac_integration": false, + "geo_integration": false, + "prime_integration": false + }, + "warrantyEligible": "no", + "warrantyEligibleUntil": { + "time": "2022-04-10T09:58:58Z", + "secondsAgo": 1733071 + }, + "features": ["optimusTrial", "softShowPlus"], + "runningHealthcheck": null, + "lastHealthcheck": null, + "lastACStateChange": { + "id": "AA22", + "time": { + "time": "2022-04-30T11:21:37Z", + "secondsAgo": 112 + }, + "status": "Success", + "acState": { + "timestamp": { + "time": "2022-04-30T11:23:30.090144Z", + "secondsAgo": -1 + }, + "on": false, + "mode": "fan", + "fanLevel": "low", + "light": "on" + }, + "resultingAcState": { + "timestamp": { + "time": "2022-04-30T11:23:30.090185Z", + "secondsAgo": -1 + }, + "on": false, + "mode": "fan", + "fanLevel": "low", + "light": "on" + }, + "changedProperties": ["on"], + "reason": "UserRequest", + "failureReason": null, + "resolveTime": { + "time": "2022-04-30T11:21:37Z", + "secondsAgo": 112 + }, + "causedByScheduleId": null, + "causedByScheduleType": null + }, + "homekitSupported": true, + "remoteCapabilities": { + "modes": { + "fan": { + "temperatures": {}, + "fanLevels": ["low", "high"], + "light": ["on", "dim", "off"] + } + } + }, + "remote": { + "toggle": false, + "window": false + }, + "remoteFlavor": "Eccentric Eagle", + "remoteAlternatives": [], + "smartMode": null, + "measurements": { + "time": { + "time": "2022-04-30T11:23:20.642798Z", + "secondsAgo": 9 + }, + "rssi": -58, + "pm25": 1, + "motion": false, + "roomIsOccupied": null + }, + "accessPoint": { + "ssid": "Sensibo-09876", + "password": null + }, + "macAddress": "00:01:00:01:00:01", + "autoOffMinutes": null, + "autoOffEnabled": false, + "antiMoldTimer": null, + "antiMoldConfig": null + } + ] +} diff --git a/tests/components/sensibo/test_binary_sensor.py b/tests/components/sensibo/test_binary_sensor.py index cbf38ff27b0..3a84dc99ca5 100644 --- a/tests/components/sensibo/test_binary_sensor.py +++ b/tests/components/sensibo/test_binary_sensor.py @@ -4,19 +4,21 @@ from __future__ import annotations from datetime import timedelta from unittest.mock import patch +from pysensibo.model import SensiboData from pytest import MonkeyPatch from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.util import dt -from .response import DATA_FROM_API - from tests.common import async_fire_time_changed async def test_binary_sensor( - hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: MonkeyPatch + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: MonkeyPatch, + get_data: SensiboData, ) -> None: """Test the Sensibo binary sensor.""" @@ -30,15 +32,15 @@ async def test_binary_sensor( assert state4.state == "on" monkeypatch.setattr( - DATA_FROM_API.parsed["ABC999111"].motion_sensors["AABBCC"], "alive", False + get_data.parsed["ABC999111"].motion_sensors["AABBCC"], "alive", False ) monkeypatch.setattr( - DATA_FROM_API.parsed["ABC999111"].motion_sensors["AABBCC"], "motion", False + get_data.parsed["ABC999111"].motion_sensors["AABBCC"], "motion", False ) with patch( "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=DATA_FROM_API, + return_value=get_data, ): async_fire_time_changed( hass, diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index 8b087da5d95..0b6c043240c 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta from unittest.mock import patch +from pysensibo.model import SensiboData import pytest from voluptuous import MultipleInvalid @@ -33,12 +34,12 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.util import dt -from .response import DATA_FROM_API - from tests.common import async_fire_time_changed -async def test_climate(hass: HomeAssistant, load_int: ConfigEntry) -> None: +async def test_climate( + hass: HomeAssistant, load_int: ConfigEntry, get_data: SensiboData +) -> None: """Test the Sensibo climate.""" state1 = hass.states.get("climate.hallway") @@ -54,7 +55,7 @@ async def test_climate(hass: HomeAssistant, load_int: ConfigEntry) -> None: "fan_only", "off", ], - "min_temp": 18, + "min_temp": 17, "max_temp": 20, "target_temp_step": 1, "fan_modes": ["quiet", "low", "medium"], @@ -63,9 +64,9 @@ async def test_climate(hass: HomeAssistant, load_int: ConfigEntry) -> None: "fixedTop", "fixedMiddleTop", ], - "current_temperature": 22.4, + "current_temperature": 21.2, "temperature": 25, - "current_humidity": 38, + "current_humidity": 32.9, "fan_mode": "high", "swing_mode": "stopped", "friendly_name": "Hallway", @@ -76,7 +77,10 @@ async def test_climate(hass: HomeAssistant, load_int: ConfigEntry) -> None: async def test_climate_fan( - hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: pytest.MonkeyPatch, + get_data: SensiboData, ) -> None: """Test the Sensibo climate fan service.""" @@ -99,7 +103,7 @@ async def test_climate_fan( assert state2.attributes["fan_mode"] == "low" monkeypatch.setattr( - DATA_FROM_API.parsed["ABC999111"], + get_data.parsed["ABC999111"], "active_features", [ "timestamp", @@ -113,7 +117,7 @@ async def test_climate_fan( ) with patch( "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=DATA_FROM_API, + return_value=get_data, ): async_fire_time_changed( hass, @@ -138,7 +142,10 @@ async def test_climate_fan( async def test_climate_swing( - hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: pytest.MonkeyPatch, + get_data: SensiboData, ) -> None: """Test the Sensibo climate swing service.""" @@ -161,7 +168,7 @@ async def test_climate_swing( assert state2.attributes["swing_mode"] == "fixedTop" monkeypatch.setattr( - DATA_FROM_API.parsed["ABC999111"], + get_data.parsed["ABC999111"], "active_features", [ "timestamp", @@ -174,7 +181,7 @@ async def test_climate_swing( ) with patch( "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=DATA_FROM_API, + return_value=get_data, ): async_fire_time_changed( hass, @@ -199,7 +206,10 @@ async def test_climate_swing( async def test_climate_temperatures( - hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: pytest.MonkeyPatch, + get_data: SensiboData, ) -> None: """Test the Sensibo climate temperature service.""" @@ -234,7 +244,7 @@ async def test_climate_temperatures( await hass.async_block_till_done() state2 = hass.states.get("climate.hallway") - assert state2.attributes["temperature"] == 18 + assert state2.attributes["temperature"] == 17 with patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", @@ -250,7 +260,7 @@ async def test_climate_temperatures( await hass.async_block_till_done() state2 = hass.states.get("climate.hallway") - assert state2.attributes["temperature"] == 18 + assert state2.attributes["temperature"] == 17 with patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", @@ -299,7 +309,7 @@ async def test_climate_temperatures( assert state2.attributes["temperature"] == 20 monkeypatch.setattr( - DATA_FROM_API.parsed["ABC999111"], + get_data.parsed["ABC999111"], "active_features", [ "timestamp", @@ -312,7 +322,7 @@ async def test_climate_temperatures( ) with patch( "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=DATA_FROM_API, + return_value=get_data, ): async_fire_time_changed( hass, @@ -338,12 +348,15 @@ async def test_climate_temperatures( async def test_climate_temperature_is_none( - hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: pytest.MonkeyPatch, + get_data: SensiboData, ) -> None: """Test the Sensibo climate temperature service no temperature provided.""" monkeypatch.setattr( - DATA_FROM_API.parsed["ABC999111"], + get_data.parsed["ABC999111"], "active_features", [ "timestamp", @@ -357,13 +370,13 @@ async def test_climate_temperature_is_none( ], ) monkeypatch.setattr( - DATA_FROM_API.parsed["ABC999111"], + get_data.parsed["ABC999111"], "target_temp", 25, ) with patch( "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=DATA_FROM_API, + return_value=get_data, ): async_fire_time_changed( hass, @@ -395,12 +408,15 @@ async def test_climate_temperature_is_none( async def test_climate_hvac_mode( - hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: pytest.MonkeyPatch, + get_data: SensiboData, ) -> None: """Test the Sensibo climate hvac mode service.""" monkeypatch.setattr( - DATA_FROM_API.parsed["ABC999111"], + get_data.parsed["ABC999111"], "active_features", [ "timestamp", @@ -415,7 +431,7 @@ async def test_climate_hvac_mode( ) with patch( "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=DATA_FROM_API, + return_value=get_data, ): async_fire_time_changed( hass, @@ -428,7 +444,7 @@ async def test_climate_hvac_mode( with patch( "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=DATA_FROM_API, + return_value=get_data, ), patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", return_value={"result": {"status": "Success"}}, @@ -444,10 +460,10 @@ async def test_climate_hvac_mode( state2 = hass.states.get("climate.hallway") assert state2.state == "off" - monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "device_on", False) + monkeypatch.setattr(get_data.parsed["ABC999111"], "device_on", False) with patch( "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=DATA_FROM_API, + return_value=get_data, ): async_fire_time_changed( hass, @@ -457,7 +473,7 @@ async def test_climate_hvac_mode( with patch( "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=DATA_FROM_API, + return_value=get_data, ), patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", return_value={"result": {"status": "Success"}}, @@ -475,15 +491,18 @@ async def test_climate_hvac_mode( async def test_climate_on_off( - hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: pytest.MonkeyPatch, + get_data: SensiboData, ) -> None: """Test the Sensibo climate on/off service.""" - monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "hvac_mode", "heat") - monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "device_on", True) + monkeypatch.setattr(get_data.parsed["ABC999111"], "hvac_mode", "heat") + monkeypatch.setattr(get_data.parsed["ABC999111"], "device_on", True) with patch( "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=DATA_FROM_API, + return_value=get_data, ): async_fire_time_changed( hass, @@ -526,15 +545,18 @@ async def test_climate_on_off( async def test_climate_service_failed( - hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: pytest.MonkeyPatch, + get_data: SensiboData, ) -> None: """Test the Sensibo climate service failed.""" - monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "hvac_mode", "heat") - monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "device_on", True) + monkeypatch.setattr(get_data.parsed["ABC999111"], "hvac_mode", "heat") + monkeypatch.setattr(get_data.parsed["ABC999111"], "device_on", True) with patch( "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=DATA_FROM_API, + return_value=get_data, ): async_fire_time_changed( hass, @@ -563,15 +585,18 @@ async def test_climate_service_failed( async def test_climate_assumed_state( - hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: pytest.MonkeyPatch + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: pytest.MonkeyPatch, + get_data: SensiboData, ) -> None: """Test the Sensibo climate assumed state service.""" - monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "hvac_mode", "heat") - monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "device_on", True) + monkeypatch.setattr(get_data.parsed["ABC999111"], "hvac_mode", "heat") + monkeypatch.setattr(get_data.parsed["ABC999111"], "device_on", True) with patch( "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=DATA_FROM_API, + return_value=get_data, ): async_fire_time_changed( hass, @@ -584,7 +609,7 @@ async def test_climate_assumed_state( with patch( "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=DATA_FROM_API, + return_value=get_data, ), patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", return_value={"result": {"status": "Success"}}, diff --git a/tests/components/sensibo/test_coordinator.py b/tests/components/sensibo/test_coordinator.py index 703eb8b184b..f0b9d60f112 100644 --- a/tests/components/sensibo/test_coordinator.py +++ b/tests/components/sensibo/test_coordinator.py @@ -15,13 +15,12 @@ from homeassistant.core import HomeAssistant from homeassistant.util import dt from . import ENTRY_CONFIG -from .response import DATA_FROM_API from tests.common import MockConfigEntry, async_fire_time_changed async def test_coordinator( - hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch + hass: HomeAssistant, monkeypatch: pytest.MonkeyPatch, get_data: SensiboData ) -> None: """Test the Sensibo coordinator with errors.""" config_entry = MockConfigEntry( @@ -44,9 +43,9 @@ async def test_coordinator( "homeassistant.components.sensibo.util.SensiboClient.async_get_me", return_value={"result": {"username": "username"}}, ): - monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "hvac_mode", "heat") - monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "device_on", True) - mock_data.return_value = DATA_FROM_API + monkeypatch.setattr(get_data.parsed["ABC999111"], "hvac_mode", "heat") + monkeypatch.setattr(get_data.parsed["ABC999111"], "device_on", True) + mock_data.return_value = get_data await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() mock_data.assert_called_once() @@ -71,10 +70,10 @@ async def test_coordinator( assert state.state == STATE_UNAVAILABLE mock_data.reset_mock() - monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "hvac_mode", "heat") - monkeypatch.setattr(DATA_FROM_API.parsed["ABC999111"], "device_on", True) + monkeypatch.setattr(get_data.parsed["ABC999111"], "hvac_mode", "heat") + monkeypatch.setattr(get_data.parsed["ABC999111"], "device_on", True) - mock_data.return_value = DATA_FROM_API + mock_data.return_value = get_data mock_data.side_effect = None async_fire_time_changed(hass, dt.utcnow() + timedelta(minutes=5)) await hass.async_block_till_done() diff --git a/tests/components/sensibo/test_diagnostics.py b/tests/components/sensibo/test_diagnostics.py index b4e85dad2b4..636fefa5054 100644 --- a/tests/components/sensibo/test_diagnostics.py +++ b/tests/components/sensibo/test_diagnostics.py @@ -17,32 +17,10 @@ async def test_diagnostics( diag = await get_diagnostics_for_config_entry(hass, hass_client, entry) - assert diag == { - "status": "success", - "result": [ - { - "id": "**REDACTED**", - "qrId": "**REDACTED**", - "room": {"uid": "**REDACTED**", "name": "Hallway", "icon": "Lounge"}, - "acState": { - "timestamp": { - "time": "2022-04-30T19:58:15.544787Z", - "secondsAgo": 0, - }, - "on": False, - "mode": "fan", - "fanLevel": "high", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", - }, - "location": "**REDACTED**", - "accessPoint": {"ssid": "**REDACTED**", "password": None}, - "macAddress": "**REDACTED**", - "autoOffMinutes": None, - "autoOffEnabled": False, - "antiMoldTimer": None, - "antiMoldConfig": None, - } - ], - } + assert diag["status"] == "success" + for device in diag["result"]: + assert device["id"] == "**REDACTED**" + assert device["qrId"] == "**REDACTED**" + assert device["macAddress"] == "**REDACTED**" + assert device["location"] == "**REDACTED**" + assert device["productModel"] in ["skyv2", "pure"] diff --git a/tests/components/sensibo/test_entity.py b/tests/components/sensibo/test_entity.py index 7df70c7d45e..27e0f4df772 100644 --- a/tests/components/sensibo/test_entity.py +++ b/tests/components/sensibo/test_entity.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta from unittest.mock import patch +from pysensibo.model import SensiboData import pytest from homeassistant.components.climate.const import ( @@ -24,12 +25,12 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import dt -from .response import DATA_FROM_API - from tests.common import async_fire_time_changed -async def test_entity(hass: HomeAssistant, load_int: ConfigEntry) -> None: +async def test_entity( + hass: HomeAssistant, load_int: ConfigEntry, get_data: SensiboData +) -> None: """Test the Sensibo climate.""" state1 = hass.states.get("climate.hallway") @@ -55,7 +56,10 @@ async def test_entity(hass: HomeAssistant, load_int: ConfigEntry) -> None: @pytest.mark.parametrize("p_error", SENSIBO_ERRORS) async def test_entity_send_command( - hass: HomeAssistant, p_error: Exception, load_int: ConfigEntry + hass: HomeAssistant, + p_error: Exception, + load_int: ConfigEntry, + get_data: SensiboData, ) -> None: """Test the Sensibo send command with error.""" @@ -94,7 +98,7 @@ async def test_entity_send_command( async def test_entity_send_command_calibration( - hass: HomeAssistant, load_int: ConfigEntry + hass: HomeAssistant, load_int: ConfigEntry, get_data: SensiboData ) -> None: """Test the Sensibo send command for calibration.""" @@ -106,7 +110,7 @@ async def test_entity_send_command_calibration( with patch( "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=DATA_FROM_API, + return_value=get_data, ): async_fire_time_changed( hass, diff --git a/tests/components/sensibo/test_init.py b/tests/components/sensibo/test_init.py index b9ad56eaf07..505816e3f41 100644 --- a/tests/components/sensibo/test_init.py +++ b/tests/components/sensibo/test_init.py @@ -3,6 +3,8 @@ from __future__ import annotations from unittest.mock import patch +from pysensibo.model import SensiboData + from homeassistant import config_entries from homeassistant.components.sensibo.const import DOMAIN from homeassistant.components.sensibo.util import NoUsernameError @@ -10,12 +12,11 @@ from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from . import ENTRY_CONFIG -from .response import DATA_FROM_API from tests.common import MockConfigEntry -async def test_setup_entry(hass: HomeAssistant) -> None: +async def test_setup_entry(hass: HomeAssistant, get_data: SensiboData) -> None: """Test setup entry.""" entry = MockConfigEntry( domain=DOMAIN, @@ -29,7 +30,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: with patch( "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=DATA_FROM_API, + return_value=get_data, ), patch( "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, @@ -43,7 +44,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None: assert entry.state == config_entries.ConfigEntryState.LOADED -async def test_migrate_entry(hass: HomeAssistant) -> None: +async def test_migrate_entry(hass: HomeAssistant, get_data: SensiboData) -> None: """Test migrate entry unique id.""" entry = MockConfigEntry( domain=DOMAIN, @@ -57,7 +58,7 @@ async def test_migrate_entry(hass: HomeAssistant) -> None: with patch( "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=DATA_FROM_API, + return_value=get_data, ), patch( "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, @@ -73,7 +74,7 @@ async def test_migrate_entry(hass: HomeAssistant) -> None: assert entry.unique_id == "username" -async def test_migrate_entry_fails(hass: HomeAssistant) -> None: +async def test_migrate_entry_fails(hass: HomeAssistant, get_data: SensiboData) -> None: """Test migrate entry unique id.""" entry = MockConfigEntry( domain=DOMAIN, @@ -101,7 +102,7 @@ async def test_migrate_entry_fails(hass: HomeAssistant) -> None: assert entry.unique_id == "12" -async def test_unload_entry(hass: HomeAssistant) -> None: +async def test_unload_entry(hass: HomeAssistant, get_data: SensiboData) -> None: """Test unload an entry.""" entry = MockConfigEntry( domain=DOMAIN, @@ -115,7 +116,7 @@ async def test_unload_entry(hass: HomeAssistant) -> None: with patch( "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=DATA_FROM_API, + return_value=get_data, ), patch( "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, From 074dfd5a21de0e205a723f892238be7b00657398 Mon Sep 17 00:00:00 2001 From: Trevor North Date: Wed, 11 May 2022 23:05:42 +0100 Subject: [PATCH 0391/3516] Allow RTMP sources for RTSPtoWeb (#71695) --- homeassistant/components/camera/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 2079e962459..57019f7c68d 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -103,7 +103,7 @@ class CameraEntityFeature(IntEnum): SUPPORT_ON_OFF: Final = 1 SUPPORT_STREAM: Final = 2 -RTSP_PREFIXES = {"rtsp://", "rtsps://"} +RTSP_PREFIXES = {"rtsp://", "rtsps://", "rtmp://"} DEFAULT_CONTENT_TYPE: Final = "image/jpeg" ENTITY_IMAGE_URL: Final = "/api/camera_proxy/{0}?token={1}" From 81e8d2ab862b78b7936a0ba8b12a3643e76ccd94 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 May 2022 17:27:02 -0500 Subject: [PATCH 0392/3516] Significantly improve logbook performance when selecting entities (#71657) --- homeassistant/components/logbook/__init__.py | 277 ++------------ homeassistant/components/logbook/queries.py | 377 +++++++++++++++++++ tests/components/logbook/test_init.py | 96 ++++- 3 files changed, 480 insertions(+), 270 deletions(-) create mode 100644 homeassistant/components/logbook/queries.py diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index a877cba4ff2..9cf3ad99e57 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -1,23 +1,18 @@ """Event parser and human readable log generator.""" from __future__ import annotations -from collections.abc import Callable, Generator, Iterable +from collections.abc import Callable, Generator from contextlib import suppress from datetime import datetime as dt, timedelta from http import HTTPStatus import json +import logging import re from typing import Any, cast from aiohttp import web -import sqlalchemy -from sqlalchemy import lambda_stmt, select from sqlalchemy.engine.row import Row -from sqlalchemy.orm import aliased from sqlalchemy.orm.query import Query -from sqlalchemy.sql.expression import literal -from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import Select import voluptuous as vol from homeassistant.components import frontend @@ -27,15 +22,8 @@ from homeassistant.components.history import ( sqlalchemy_filter_from_include_exclude_conf, ) from homeassistant.components.http import HomeAssistantView -from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN from homeassistant.components.recorder import get_instance -from homeassistant.components.recorder.models import ( - EventData, - Events, - StateAttributes, - States, - process_timestamp_to_utc_isoformat, -) +from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat from homeassistant.components.recorder.util import session_scope from homeassistant.components.script import EVENT_SCRIPT_STARTED from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN @@ -75,66 +63,32 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util -ENTITY_ID_JSON_TEMPLATE = '%"entity_id":"{}"%' +from .queries import statement_for_request + +_LOGGER = logging.getLogger(__name__) + + FRIENDLY_NAME_JSON_EXTRACT = re.compile('"friendly_name": ?"([^"]+)"') ENTITY_ID_JSON_EXTRACT = re.compile('"entity_id": ?"([^"]+)"') DOMAIN_JSON_EXTRACT = re.compile('"domain": ?"([^"]+)"') ICON_JSON_EXTRACT = re.compile('"icon": ?"([^"]+)"') ATTR_MESSAGE = "message" -CONTINUOUS_DOMAINS = {PROXIMITY_DOMAIN, SENSOR_DOMAIN} -CONTINUOUS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in CONTINUOUS_DOMAINS] - DOMAIN = "logbook" -EMPTY_JSON_OBJECT = "{}" -UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' -UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%" HA_DOMAIN_ENTITY_ID = f"{HA_DOMAIN}._" CONFIG_SCHEMA = vol.Schema( {DOMAIN: INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA}, extra=vol.ALLOW_EXTRA ) -HOMEASSISTANT_EVENTS = [ - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, -] +HOMEASSISTANT_EVENTS = {EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP} -ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED = [ +ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED = ( EVENT_LOGBOOK_ENTRY, EVENT_CALL_SERVICE, *HOMEASSISTANT_EVENTS, -] - -ALL_EVENT_TYPES = [ - EVENT_STATE_CHANGED, - *ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, -] - - -EVENT_COLUMNS = [ - Events.event_type.label("event_type"), - Events.event_data.label("event_data"), - Events.time_fired.label("time_fired"), - Events.context_id.label("context_id"), - Events.context_user_id.label("context_user_id"), - Events.context_parent_id.label("context_parent_id"), -] - -STATE_COLUMNS = [ - States.state.label("state"), - States.entity_id.label("entity_id"), - States.attributes.label("attributes"), - StateAttributes.shared_attrs.label("shared_attrs"), -] - -EMPTY_STATE_COLUMNS = [ - literal(value=None, type_=sqlalchemy.String).label("state"), - literal(value=None, type_=sqlalchemy.String).label("entity_id"), - literal(value=None, type_=sqlalchemy.Text).label("attributes"), - literal(value=None, type_=sqlalchemy.Text).label("shared_attrs"), -] +) SCRIPT_AUTOMATION_EVENTS = {EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED} @@ -295,7 +249,6 @@ class LogbookView(HomeAssistantView): hass = request.app["hass"] - entity_matches_only = "entity_matches_only" in request.query context_id = request.query.get("context_id") if entity_ids and context_id: @@ -313,7 +266,6 @@ class LogbookView(HomeAssistantView): entity_ids, self.filters, self.entities_filter, - entity_matches_only, context_id, ) ) @@ -416,7 +368,6 @@ def _get_events( entity_ids: list[str] | None = None, filters: Filters | None = None, entities_filter: EntityFilter | Callable[[str], bool] | None = None, - entity_matches_only: bool = False, context_id: str | None = None, ) -> list[dict[str, Any]]: """Get events for a period of time.""" @@ -428,10 +379,13 @@ def _get_events( event_data_cache: dict[str, dict[str, Any]] = {} context_lookup: dict[str | None, Row | None] = {None: None} event_cache = EventCache(event_data_cache) - external_events = hass.data.get(DOMAIN, {}) + external_events: dict[ + str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] + ] = hass.data.get(DOMAIN, {}) context_augmenter = ContextAugmenter( context_lookup, entity_name_cache, external_events, event_cache ) + event_types = (*ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, *external_events) def yield_rows(query: Query) -> Generator[Row, None, None]: """Yield Events that are not filtered away.""" @@ -441,6 +395,8 @@ def _get_events( rows = query.yield_per(1000) for row in rows: context_lookup.setdefault(row.context_id, row) + if row.context_only: + continue event_type = row.event_type if event_type != EVENT_CALL_SERVICE and ( event_type == EVENT_STATE_CHANGED @@ -451,22 +407,15 @@ def _get_events( if entity_ids is not None: entities_filter = generate_filter([], entity_ids, [], []) - event_types = [ - *ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, - *hass.data.get(DOMAIN, {}), - ] - entity_filter = None - if entity_ids is None and filters: - entity_filter = filters.entity_filter() # type: ignore[no-untyped-call] - stmt = _generate_logbook_query( - start_day, - end_day, - event_types, - entity_ids, - entity_filter, - entity_matches_only, - context_id, + stmt = statement_for_request( + start_day, end_day, event_types, entity_ids, filters, context_id ) + if _LOGGER.isEnabledFor(logging.DEBUG): + _LOGGER.debug( + "Literal statement: %s", + stmt.compile(compile_kwargs={"literal_binds": True}), + ) + with session_scope(hass=hass) as session: return list( _humanify( @@ -479,182 +428,6 @@ def _get_events( ) -def _generate_logbook_query( - start_day: dt, - end_day: dt, - event_types: list[str], - entity_ids: list[str] | None = None, - entity_filter: Any | None = None, - entity_matches_only: bool = False, - context_id: str | None = None, -) -> StatementLambdaElement: - """Generate a logbook query lambda_stmt.""" - stmt = lambda_stmt( - lambda: _generate_events_query_without_states() - .where((Events.time_fired > start_day) & (Events.time_fired < end_day)) - .where(Events.event_type.in_(event_types)) - .outerjoin(EventData, (Events.data_id == EventData.data_id)) - ) - if entity_ids is not None: - if entity_matches_only: - # When entity_matches_only is provided, contexts and events that do not - # contain the entity_ids are not included in the logbook response. - stmt.add_criteria( - lambda s: s.where(_apply_event_entity_id_matchers(entity_ids)), - track_on=entity_ids, - ) - stmt += lambda s: s.union_all( - _generate_states_query() - .filter((States.last_updated > start_day) & (States.last_updated < end_day)) - .where(States.entity_id.in_(entity_ids)) - ) - else: - if context_id is not None: - # Once all the old `state_changed` events - # are gone from the database remove the - # union_all(_generate_legacy_events_context_id_query()....) - stmt += lambda s: s.where(Events.context_id == context_id).union_all( - _generate_legacy_events_context_id_query() - .where((Events.time_fired > start_day) & (Events.time_fired < end_day)) - .where(Events.context_id == context_id), - _generate_states_query() - .where( - (States.last_updated > start_day) & (States.last_updated < end_day) - ) - .outerjoin(Events, (States.event_id == Events.event_id)) - .where(States.context_id == context_id), - ) - elif entity_filter is not None: - stmt += lambda s: s.union_all( - _generate_states_query() - .where( - (States.last_updated > start_day) & (States.last_updated < end_day) - ) - .where(entity_filter) - ) - else: - stmt += lambda s: s.union_all( - _generate_states_query().where( - (States.last_updated > start_day) & (States.last_updated < end_day) - ) - ) - - stmt += lambda s: s.order_by(Events.time_fired) - return stmt - - -def _generate_events_query_without_data() -> Select: - return select( - literal(value=EVENT_STATE_CHANGED, type_=sqlalchemy.String).label("event_type"), - literal(value=None, type_=sqlalchemy.Text).label("event_data"), - States.last_changed.label("time_fired"), - States.context_id.label("context_id"), - States.context_user_id.label("context_user_id"), - States.context_parent_id.label("context_parent_id"), - literal(value=None, type_=sqlalchemy.Text).label("shared_data"), - *STATE_COLUMNS, - ) - - -def _generate_legacy_events_context_id_query() -> Select: - """Generate a legacy events context id query that also joins states.""" - # This can be removed once we no longer have event_ids in the states table - return ( - select( - *EVENT_COLUMNS, - literal(value=None, type_=sqlalchemy.String).label("shared_data"), - States.state, - States.entity_id, - States.attributes, - StateAttributes.shared_attrs, - ) - .outerjoin(States, (Events.event_id == States.event_id)) - .where(States.last_updated == States.last_changed) - .where(_not_continuous_entity_matcher()) - .outerjoin( - StateAttributes, (States.attributes_id == StateAttributes.attributes_id) - ) - ) - - -def _generate_events_query_without_states() -> Select: - return select( - *EVENT_COLUMNS, EventData.shared_data.label("shared_data"), *EMPTY_STATE_COLUMNS - ) - - -def _generate_states_query() -> Select: - old_state = aliased(States, name="old_state") - return ( - _generate_events_query_without_data() - .outerjoin(old_state, (States.old_state_id == old_state.state_id)) - .where(_missing_state_matcher(old_state)) - .where(_not_continuous_entity_matcher()) - .where(States.last_updated == States.last_changed) - .outerjoin( - StateAttributes, (States.attributes_id == StateAttributes.attributes_id) - ) - ) - - -def _missing_state_matcher(old_state: States) -> sqlalchemy.and_: - # The below removes state change events that do not have - # and old_state or the old_state is missing (newly added entities) - # or the new_state is missing (removed entities) - return sqlalchemy.and_( - old_state.state_id.isnot(None), - (States.state != old_state.state), - States.state.isnot(None), - ) - - -def _not_continuous_entity_matcher() -> sqlalchemy.or_: - """Match non continuous entities.""" - return sqlalchemy.or_( - _not_continuous_domain_matcher(), - sqlalchemy.and_( - _continuous_domain_matcher, _not_uom_attributes_matcher() - ).self_group(), - ) - - -def _not_continuous_domain_matcher() -> sqlalchemy.and_: - """Match not continuous domains.""" - return sqlalchemy.and_( - *[ - ~States.entity_id.like(entity_domain) - for entity_domain in CONTINUOUS_ENTITY_ID_LIKE - ], - ).self_group() - - -def _continuous_domain_matcher() -> sqlalchemy.or_: - """Match continuous domains.""" - return sqlalchemy.or_( - *[ - States.entity_id.like(entity_domain) - for entity_domain in CONTINUOUS_ENTITY_ID_LIKE - ], - ).self_group() - - -def _not_uom_attributes_matcher() -> Any: - """Prefilter ATTR_UNIT_OF_MEASUREMENT as its much faster in sql.""" - return ~StateAttributes.shared_attrs.like( - UNIT_OF_MEASUREMENT_JSON_LIKE - ) | ~States.attributes.like(UNIT_OF_MEASUREMENT_JSON_LIKE) - - -def _apply_event_entity_id_matchers(entity_ids: Iterable[str]) -> sqlalchemy.or_: - """Create matchers for the entity_id in the event_data.""" - ors = [] - for entity_id in entity_ids: - like = ENTITY_ID_JSON_TEMPLATE.format(entity_id) - ors.append(Events.event_data.like(like)) - ors.append(EventData.shared_data.like(like)) - return sqlalchemy.or_(*ors) - - def _keep_row( hass: HomeAssistant, event_type: str, diff --git a/homeassistant/components/logbook/queries.py b/homeassistant/components/logbook/queries.py new file mode 100644 index 00000000000..8c13719a1cf --- /dev/null +++ b/homeassistant/components/logbook/queries.py @@ -0,0 +1,377 @@ +"""Queries for logbook.""" +from __future__ import annotations + +from collections.abc import Iterable +from datetime import datetime as dt +from typing import Any + +import sqlalchemy +from sqlalchemy import lambda_stmt, select +from sqlalchemy.orm import aliased +from sqlalchemy.sql.expression import literal +from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Select + +from homeassistant.components.history import Filters +from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN +from homeassistant.components.recorder.models import ( + EventData, + Events, + StateAttributes, + States, +) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import EVENT_STATE_CHANGED + +ENTITY_ID_JSON_TEMPLATE = '%"entity_id":"{}"%' + +CONTINUOUS_DOMAINS = {PROXIMITY_DOMAIN, SENSOR_DOMAIN} +CONTINUOUS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in CONTINUOUS_DOMAINS] + +UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' +UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%" + + +EVENT_COLUMNS = ( + Events.event_type.label("event_type"), + Events.event_data.label("event_data"), + Events.time_fired.label("time_fired"), + Events.context_id.label("context_id"), + Events.context_user_id.label("context_user_id"), + Events.context_parent_id.label("context_parent_id"), +) + +STATE_COLUMNS = ( + States.state.label("state"), + States.entity_id.label("entity_id"), + States.attributes.label("attributes"), + StateAttributes.shared_attrs.label("shared_attrs"), +) + +EMPTY_STATE_COLUMNS = ( + literal(value=None, type_=sqlalchemy.String).label("state"), + literal(value=None, type_=sqlalchemy.String).label("entity_id"), + literal(value=None, type_=sqlalchemy.Text).label("attributes"), + literal(value=None, type_=sqlalchemy.Text).label("shared_attrs"), +) + +EVENT_ROWS_NO_STATES = ( + *EVENT_COLUMNS, + EventData.shared_data.label("shared_data"), + *EMPTY_STATE_COLUMNS, +) + +# Virtual column to tell logbook if it should avoid processing +# the event as its only used to link contexts +CONTEXT_ONLY = literal("1").label("context_only") +NOT_CONTEXT_ONLY = literal(None).label("context_only") + + +def statement_for_request( + start_day: dt, + end_day: dt, + event_types: tuple[str, ...], + entity_ids: list[str] | None = None, + filters: Filters | None = None, + context_id: str | None = None, +) -> StatementLambdaElement: + """Generate the logbook statement for a logbook request.""" + + # No entities: logbook sends everything for the timeframe + # limited by the context_id and the yaml configured filter + if not entity_ids: + entity_filter = filters.entity_filter() if filters else None # type: ignore[no-untyped-call] + return _all_stmt(start_day, end_day, event_types, entity_filter, context_id) + + # Multiple entities: logbook sends everything for the timeframe for the entities + # + # This is the least efficient query because we use + # like matching which means part of the query has to be built each + # time when the entity_ids are not in the cache + if len(entity_ids) > 1: + return _entities_stmt(start_day, end_day, event_types, entity_ids) + + # Single entity: logbook sends everything for the timeframe for the entity + entity_id = entity_ids[0] + entity_like = ENTITY_ID_JSON_TEMPLATE.format(entity_id) + return _single_entity_stmt(start_day, end_day, event_types, entity_id, entity_like) + + +def _select_events_context_id_subquery( + start_day: dt, + end_day: dt, + event_types: tuple[str, ...], +) -> Select: + """Generate the select for a context_id subquery.""" + return ( + select(Events.context_id) + .where((Events.time_fired > start_day) & (Events.time_fired < end_day)) + .where(Events.event_type.in_(event_types)) + .outerjoin(EventData, (Events.data_id == EventData.data_id)) + ) + + +def _select_entities_context_ids_sub_query( + start_day: dt, + end_day: dt, + event_types: tuple[str, ...], + entity_ids: list[str], +) -> Select: + """Generate a subquery to find context ids for multiple entities.""" + return ( + _select_events_context_id_subquery(start_day, end_day, event_types) + .where(_apply_event_entity_id_matchers(entity_ids)) + .union_all( + select(States.context_id) + .filter((States.last_updated > start_day) & (States.last_updated < end_day)) + .where(States.entity_id.in_(entity_ids)) + ) + .subquery() + ) + + +def _select_events_context_only() -> Select: + """Generate an events query that mark them as for context_only. + + By marking them as context_only we know they are only for + linking context ids and we can avoid processing them. + """ + return select(*EVENT_ROWS_NO_STATES, CONTEXT_ONLY).outerjoin( + EventData, (Events.data_id == EventData.data_id) + ) + + +def _entities_stmt( + start_day: dt, + end_day: dt, + event_types: tuple[str, ...], + entity_ids: list[str], +) -> StatementLambdaElement: + """Generate a logbook query for multiple entities.""" + stmt = lambda_stmt( + lambda: _select_events_without_states(start_day, end_day, event_types) + ) + stmt = stmt.add_criteria( + lambda s: s.where(_apply_event_entity_id_matchers(entity_ids)).union_all( + _select_states(start_day, end_day).where(States.entity_id.in_(entity_ids)), + _select_events_context_only().where( + Events.context_id.in_( + _select_entities_context_ids_sub_query( + start_day, + end_day, + event_types, + entity_ids, + ) + ) + ), + ), + # Since _apply_event_entity_id_matchers generates multiple + # like statements we need to use the entity_ids in the + # the cache key since the sql can change based on the + # likes. + track_on=(str(entity_ids),), + ) + stmt += lambda s: s.order_by(Events.time_fired) + return stmt + + +def _select_entity_context_ids_sub_query( + start_day: dt, + end_day: dt, + event_types: tuple[str, ...], + entity_id: str, + entity_id_like: str, +) -> Select: + """Generate a subquery to find context ids for a single entity.""" + return ( + _select_events_context_id_subquery(start_day, end_day, event_types) + .where( + Events.event_data.like(entity_id_like) + | EventData.shared_data.like(entity_id_like) + ) + .union_all( + select(States.context_id) + .filter((States.last_updated > start_day) & (States.last_updated < end_day)) + .where(States.entity_id == entity_id) + ) + .subquery() + ) + + +def _single_entity_stmt( + start_day: dt, + end_day: dt, + event_types: tuple[str, ...], + entity_id: str, + entity_id_like: str, +) -> StatementLambdaElement: + """Generate a logbook query for a single entity.""" + stmt = lambda_stmt( + lambda: _select_events_without_states(start_day, end_day, event_types) + .where( + Events.event_data.like(entity_id_like) + | EventData.shared_data.like(entity_id_like) + ) + .union_all( + _select_states(start_day, end_day).where(States.entity_id == entity_id), + _select_events_context_only().where( + Events.context_id.in_( + _select_entity_context_ids_sub_query( + start_day, end_day, event_types, entity_id, entity_id_like + ) + ) + ), + ) + .order_by(Events.time_fired) + ) + return stmt + + +def _all_stmt( + start_day: dt, + end_day: dt, + event_types: tuple[str, ...], + entity_filter: Any | None = None, + context_id: str | None = None, +) -> StatementLambdaElement: + """Generate a logbook query for all entities.""" + stmt = lambda_stmt( + lambda: _select_events_without_states(start_day, end_day, event_types) + ) + if context_id is not None: + # Once all the old `state_changed` events + # are gone from the database remove the + # _legacy_select_events_context_id() + stmt += lambda s: s.where(Events.context_id == context_id).union_all( + _select_states(start_day, end_day).where(States.context_id == context_id), + _legacy_select_events_context_id(start_day, end_day, context_id), + ) + elif entity_filter is not None: + stmt += lambda s: s.union_all( + _select_states(start_day, end_day).where(entity_filter) + ) + else: + stmt += lambda s: s.union_all(_select_states(start_day, end_day)) + stmt += lambda s: s.order_by(Events.time_fired) + return stmt + + +def _legacy_select_events_context_id( + start_day: dt, end_day: dt, context_id: str +) -> Select: + """Generate a legacy events context id select that also joins states.""" + # This can be removed once we no longer have event_ids in the states table + return ( + select( + *EVENT_COLUMNS, + literal(value=None, type_=sqlalchemy.String).label("shared_data"), + *STATE_COLUMNS, + NOT_CONTEXT_ONLY, + ) + .outerjoin(States, (Events.event_id == States.event_id)) + .where(States.last_updated == States.last_changed) + .where(_not_continuous_entity_matcher()) + .outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) + .where((Events.time_fired > start_day) & (Events.time_fired < end_day)) + .where(Events.context_id == context_id) + ) + + +def _select_events_without_states( + start_day: dt, end_day: dt, event_types: tuple[str, ...] +) -> Select: + """Generate an events select that does not join states.""" + return ( + select(*EVENT_ROWS_NO_STATES, NOT_CONTEXT_ONLY) + .where((Events.time_fired > start_day) & (Events.time_fired < end_day)) + .where(Events.event_type.in_(event_types)) + .outerjoin(EventData, (Events.data_id == EventData.data_id)) + ) + + +def _select_states(start_day: dt, end_day: dt) -> Select: + """Generate a states select that formats the states table as event rows.""" + old_state = aliased(States, name="old_state") + return ( + select( + literal(value=EVENT_STATE_CHANGED, type_=sqlalchemy.String).label( + "event_type" + ), + literal(value=None, type_=sqlalchemy.Text).label("event_data"), + States.last_changed.label("time_fired"), + States.context_id.label("context_id"), + States.context_user_id.label("context_user_id"), + States.context_parent_id.label("context_parent_id"), + literal(value=None, type_=sqlalchemy.Text).label("shared_data"), + *STATE_COLUMNS, + NOT_CONTEXT_ONLY, + ) + .filter((States.last_updated > start_day) & (States.last_updated < end_day)) + .outerjoin(old_state, (States.old_state_id == old_state.state_id)) + .where(_missing_state_matcher(old_state)) + .where(_not_continuous_entity_matcher()) + .where(States.last_updated == States.last_changed) + .outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) + ) + + +def _missing_state_matcher(old_state: States) -> sqlalchemy.and_: + # The below removes state change events that do not have + # and old_state or the old_state is missing (newly added entities) + # or the new_state is missing (removed entities) + return sqlalchemy.and_( + old_state.state_id.isnot(None), + (States.state != old_state.state), + States.state.isnot(None), + ) + + +def _not_continuous_entity_matcher() -> sqlalchemy.or_: + """Match non continuous entities.""" + return sqlalchemy.or_( + _not_continuous_domain_matcher(), + sqlalchemy.and_( + _continuous_domain_matcher, _not_uom_attributes_matcher() + ).self_group(), + ) + + +def _not_continuous_domain_matcher() -> sqlalchemy.and_: + """Match not continuous domains.""" + return sqlalchemy.and_( + *[ + ~States.entity_id.like(entity_domain) + for entity_domain in CONTINUOUS_ENTITY_ID_LIKE + ], + ).self_group() + + +def _continuous_domain_matcher() -> sqlalchemy.or_: + """Match continuous domains.""" + return sqlalchemy.or_( + *[ + States.entity_id.like(entity_domain) + for entity_domain in CONTINUOUS_ENTITY_ID_LIKE + ], + ).self_group() + + +def _not_uom_attributes_matcher() -> Any: + """Prefilter ATTR_UNIT_OF_MEASUREMENT as its much faster in sql.""" + return ~StateAttributes.shared_attrs.like( + UNIT_OF_MEASUREMENT_JSON_LIKE + ) | ~States.attributes.like(UNIT_OF_MEASUREMENT_JSON_LIKE) + + +def _apply_event_entity_id_matchers(entity_ids: Iterable[str]) -> sqlalchemy.or_: + """Create matchers for the entity_id in the event_data.""" + ors = [] + for entity_id in entity_ids: + like = ENTITY_ID_JSON_TEMPLATE.format(entity_id) + ors.append(Events.event_data.like(like)) + ors.append(EventData.shared_data.like(like)) + return sqlalchemy.or_(*ors) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 76319ba5a6e..cc10346fc07 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -1321,8 +1321,8 @@ async def test_logbook_context_from_template(hass, hass_client, recorder_mock): assert json_dict[5]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" -async def test_logbook_entity_matches_only(hass, hass_client, recorder_mock): - """Test the logbook view with a single entity and entity_matches_only.""" +async def test_logbook_(hass, hass_client, recorder_mock): + """Test the logbook view with a single entity and .""" await async_setup_component(hass, "logbook", {}) assert await async_setup_component( hass, @@ -1377,7 +1377,7 @@ async def test_logbook_entity_matches_only(hass, hass_client, recorder_mock): # Test today entries with filter by end_time end_time = start + timedelta(hours=24) response = await client.get( - f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=switch.test_state&entity_matches_only" + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=switch.test_state" ) assert response.status == HTTPStatus.OK json_dict = await response.json() @@ -1390,10 +1390,8 @@ async def test_logbook_entity_matches_only(hass, hass_client, recorder_mock): assert json_dict[1]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" -async def test_logbook_entity_matches_only_multiple_calls( - hass, hass_client, recorder_mock -): - """Test the logbook view with a single entity and entity_matches_only called multiple times.""" +async def test_logbook_many_entities_multiple_calls(hass, hass_client, recorder_mock): + """Test the logbook view with a many entities called multiple times.""" await async_setup_component(hass, "logbook", {}) await async_setup_component(hass, "automation", {}) @@ -1421,7 +1419,7 @@ async def test_logbook_entity_matches_only_multiple_calls( for automation_id in range(5): # Test today entries with filter by end_time response = await client.get( - f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=automation.mock_{automation_id}_automation&entity_matches_only" + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=automation.mock_{automation_id}_automation" ) assert response.status == HTTPStatus.OK json_dict = await response.json() @@ -1432,7 +1430,7 @@ async def test_logbook_entity_matches_only_multiple_calls( ) response = await client.get( - f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=automation.mock_0_automation,automation.mock_1_automation,automation.mock_2_automation&entity_matches_only" + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=automation.mock_0_automation,automation.mock_1_automation,automation.mock_2_automation" ) assert response.status == HTTPStatus.OK json_dict = await response.json() @@ -1442,11 +1440,28 @@ async def test_logbook_entity_matches_only_multiple_calls( assert json_dict[1]["entity_id"] == "automation.mock_1_automation" assert json_dict[2]["entity_id"] == "automation.mock_2_automation" + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=automation.mock_4_automation,automation.mock_2_automation,automation.mock_0_automation,automation.mock_1_automation" + ) + assert response.status == HTTPStatus.OK + json_dict = await response.json() -async def test_custom_log_entry_discoverable_via_entity_matches_only( - hass, hass_client, recorder_mock -): - """Test if a custom log entry is later discoverable via entity_matches_only.""" + assert len(json_dict) == 4 + assert json_dict[0]["entity_id"] == "automation.mock_0_automation" + assert json_dict[1]["entity_id"] == "automation.mock_1_automation" + assert json_dict[2]["entity_id"] == "automation.mock_2_automation" + assert json_dict[3]["entity_id"] == "automation.mock_4_automation" + + response = await client.get( + f"/api/logbook/{end_time.isoformat()}?end_time={end_time}&entity=automation.mock_4_automation,automation.mock_2_automation,automation.mock_0_automation,automation.mock_1_automation" + ) + assert response.status == HTTPStatus.OK + json_dict = await response.json() + assert len(json_dict) == 0 + + +async def test_custom_log_entry_discoverable_via_(hass, hass_client, recorder_mock): + """Test if a custom log entry is later discoverable via .""" await async_setup_component(hass, "logbook", {}) await async_recorder_block_till_done(hass) @@ -1468,7 +1483,7 @@ async def test_custom_log_entry_discoverable_via_entity_matches_only( # Test today entries with filter by end_time end_time = start + timedelta(hours=24) response = await client.get( - f"/api/logbook/{start_date.isoformat()}?end_time={end_time.isoformat()}&entity=switch.test_switch&entity_matches_only" + f"/api/logbook/{start_date.isoformat()}?end_time={end_time.isoformat()}&entity=switch.test_switch" ) assert response.status == HTTPStatus.OK json_dict = await response.json() @@ -1480,8 +1495,8 @@ async def test_custom_log_entry_discoverable_via_entity_matches_only( assert json_dict[0]["entity_id"] == "switch.test_switch" -async def test_logbook_entity_matches_only_multiple(hass, hass_client, recorder_mock): - """Test the logbook view with a multiple entities and entity_matches_only.""" +async def test_logbook_multiple_entities(hass, hass_client, recorder_mock): + """Test the logbook view with a multiple entities.""" await async_setup_component(hass, "logbook", {}) assert await async_setup_component( hass, @@ -1513,12 +1528,14 @@ async def test_logbook_entity_matches_only_multiple(hass, hass_client, recorder_ # Entity added (should not be logged) hass.states.async_set("switch.test_state", STATE_ON) hass.states.async_set("light.test_state", STATE_ON) + hass.states.async_set("binary_sensor.test_state", STATE_ON) await hass.async_block_till_done() # First state change (should be logged) hass.states.async_set("switch.test_state", STATE_OFF) hass.states.async_set("light.test_state", STATE_OFF) + hass.states.async_set("binary_sensor.test_state", STATE_OFF) await hass.async_block_till_done() @@ -1530,6 +1547,9 @@ async def test_logbook_entity_matches_only_multiple(hass, hass_client, recorder_ "switch.test_state", STATE_ON, context=switch_turn_off_context ) hass.states.async_set("light.test_state", STATE_ON, context=switch_turn_off_context) + hass.states.async_set( + "binary_sensor.test_state", STATE_ON, context=switch_turn_off_context + ) await async_wait_recording_done(hass) client = await hass_client() @@ -1541,7 +1561,7 @@ async def test_logbook_entity_matches_only_multiple(hass, hass_client, recorder_ # Test today entries with filter by end_time end_time = start + timedelta(hours=24) response = await client.get( - f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=switch.test_state,light.test_state&entity_matches_only" + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=switch.test_state,light.test_state" ) assert response.status == HTTPStatus.OK json_dict = await response.json() @@ -1558,6 +1578,46 @@ async def test_logbook_entity_matches_only_multiple(hass, hass_client, recorder_ assert json_dict[3]["entity_id"] == "light.test_state" assert json_dict[3]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" + # Test today entries with filter by end_time + end_time = start + timedelta(hours=24) + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=binary_sensor.test_state,light.test_state" + ) + assert response.status == HTTPStatus.OK + json_dict = await response.json() + + assert len(json_dict) == 4 + + assert json_dict[0]["entity_id"] == "light.test_state" + + assert json_dict[1]["entity_id"] == "binary_sensor.test_state" + + assert json_dict[2]["entity_id"] == "light.test_state" + assert json_dict[2]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" + + assert json_dict[3]["entity_id"] == "binary_sensor.test_state" + assert json_dict[3]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" + + # Test today entries with filter by end_time + end_time = start + timedelta(hours=24) + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=light.test_state,binary_sensor.test_state" + ) + assert response.status == HTTPStatus.OK + json_dict = await response.json() + + assert len(json_dict) == 4 + + assert json_dict[0]["entity_id"] == "light.test_state" + + assert json_dict[1]["entity_id"] == "binary_sensor.test_state" + + assert json_dict[2]["entity_id"] == "light.test_state" + assert json_dict[2]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" + + assert json_dict[3]["entity_id"] == "binary_sensor.test_state" + assert json_dict[3]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" + async def test_logbook_invalid_entity(hass, hass_client, recorder_mock): """Test the logbook view with requesting an invalid entity.""" @@ -1572,7 +1632,7 @@ async def test_logbook_invalid_entity(hass, hass_client, recorder_mock): # Test today entries with filter by end_time end_time = start + timedelta(hours=24) response = await client.get( - f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=invalid&entity_matches_only" + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=invalid" ) assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR From e2cef55162793dfacd06559dc994bc868861eb0f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 May 2022 17:52:22 -0500 Subject: [PATCH 0393/3516] Add history/history_during_period websocket endpoint (#71688) --- homeassistant/components/history/__init__.py | 85 +++- homeassistant/components/recorder/history.py | 117 +++-- homeassistant/components/recorder/models.py | 91 ++-- .../components/websocket_api/const.py | 6 + .../components/websocket_api/messages.py | 13 +- tests/components/history/test_init.py | 406 ++++++++++++++++++ tests/components/recorder/test_history.py | 17 +- tests/components/recorder/test_models.py | 8 +- 8 files changed, 655 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 8740f352dee..a0d1f2fa76b 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -1,12 +1,12 @@ """Provide pre-made queries on top of the recorder component.""" from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Iterable, MutableMapping from datetime import datetime as dt, timedelta from http import HTTPStatus import logging import time -from typing import cast +from typing import Any, cast from aiohttp import web from sqlalchemy import not_, or_ @@ -25,7 +25,7 @@ from homeassistant.components.recorder.statistics import ( ) from homeassistant.components.recorder.util import session_scope from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, State import homeassistant.helpers.config_validation as cv from homeassistant.helpers.deprecation import deprecated_class, deprecated_function from homeassistant.helpers.entityfilter import ( @@ -40,6 +40,7 @@ import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) DOMAIN = "history" +HISTORY_FILTERS = "history_filters" CONF_ORDER = "use_include_order" GLOB_TO_SQL_CHARS = { @@ -83,7 +84,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the history hooks.""" conf = config.get(DOMAIN, {}) - filters = sqlalchemy_filter_from_include_exclude_conf(conf) + hass.data[HISTORY_FILTERS] = filters = sqlalchemy_filter_from_include_exclude_conf( + conf + ) use_include_order = conf.get(CONF_ORDER) @@ -91,6 +94,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: frontend.async_register_built_in_panel(hass, "history", "history", "hass:chart-box") websocket_api.async_register_command(hass, ws_get_statistics_during_period) websocket_api.async_register_command(hass, ws_get_list_statistic_ids) + websocket_api.async_register_command(hass, ws_get_history_during_period) return True @@ -163,6 +167,79 @@ async def ws_get_list_statistic_ids( connection.send_result(msg["id"], statistic_ids) +@websocket_api.websocket_command( + { + vol.Required("type"): "history/history_during_period", + vol.Required("start_time"): str, + vol.Optional("end_time"): str, + vol.Optional("entity_ids"): [str], + vol.Optional("include_start_time_state", default=True): bool, + vol.Optional("significant_changes_only", default=True): bool, + vol.Optional("minimal_response", default=False): bool, + vol.Optional("no_attributes", default=False): bool, + } +) +@websocket_api.async_response +async def ws_get_history_during_period( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Handle history during period websocket command.""" + start_time_str = msg["start_time"] + end_time_str = msg.get("end_time") + + if start_time := dt_util.parse_datetime(start_time_str): + start_time = dt_util.as_utc(start_time) + else: + connection.send_error(msg["id"], "invalid_start_time", "Invalid start_time") + return + + if end_time_str: + if end_time := dt_util.parse_datetime(end_time_str): + end_time = dt_util.as_utc(end_time) + else: + connection.send_error(msg["id"], "invalid_end_time", "Invalid end_time") + return + else: + end_time = None + + if start_time > dt_util.utcnow(): + connection.send_result(msg["id"], {}) + return + + entity_ids = msg.get("entity_ids") + include_start_time_state = msg["include_start_time_state"] + + if ( + not include_start_time_state + and entity_ids + and not _entities_may_have_state_changes_after(hass, entity_ids, start_time) + ): + connection.send_result(msg["id"], {}) + return + + significant_changes_only = msg["significant_changes_only"] + no_attributes = msg["no_attributes"] + minimal_response = msg["minimal_response"] + compressed_state_format = True + + history_during_period: MutableMapping[ + str, list[State | dict[str, Any]] + ] = await get_instance(hass).async_add_executor_job( + history.get_significant_states, + hass, + start_time, + end_time, + entity_ids, + hass.data[HISTORY_FILTERS], + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + compressed_state_format, + ) + connection.send_result(msg["id"], history_during_period) + + class HistoryPeriodView(HomeAssistantView): """Handle history period requests.""" diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index a061bcd1329..179d25b9f5b 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections import defaultdict -from collections.abc import Iterable, Iterator, MutableMapping +from collections.abc import Callable, Iterable, Iterator, MutableMapping from datetime import datetime from itertools import groupby import logging @@ -10,6 +10,7 @@ import time from typing import Any, cast from sqlalchemy import Column, Text, and_, bindparam, func, or_ +from sqlalchemy.engine.row import Row from sqlalchemy.ext import baked from sqlalchemy.ext.baked import BakedQuery from sqlalchemy.orm.query import Query @@ -17,6 +18,10 @@ from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal from homeassistant.components import recorder +from homeassistant.components.websocket_api.const import ( + COMPRESSED_STATE_LAST_CHANGED, + COMPRESSED_STATE_STATE, +) from homeassistant.core import HomeAssistant, State, split_entity_id import homeassistant.util.dt as dt_util @@ -25,8 +30,10 @@ from .models import ( RecorderRuns, StateAttributes, States, + process_datetime_to_timestamp, process_timestamp, process_timestamp_to_utc_isoformat, + row_to_compressed_state, ) from .util import execute, session_scope @@ -161,6 +168,7 @@ def get_significant_states( significant_changes_only: bool = True, minimal_response: bool = False, no_attributes: bool = False, + compressed_state_format: bool = False, ) -> MutableMapping[str, list[State | dict[str, Any]]]: """Wrap get_significant_states_with_session with an sql session.""" with session_scope(hass=hass) as session: @@ -175,6 +183,7 @@ def get_significant_states( significant_changes_only, minimal_response, no_attributes, + compressed_state_format, ) @@ -199,7 +208,7 @@ def _query_significant_states_with_session( filters: Any = None, significant_changes_only: bool = True, no_attributes: bool = False, -) -> list[States]: +) -> list[Row]: """Query the database for significant state changes.""" if _LOGGER.isEnabledFor(logging.DEBUG): timer_start = time.perf_counter() @@ -271,6 +280,7 @@ def get_significant_states_with_session( significant_changes_only: bool = True, minimal_response: bool = False, no_attributes: bool = False, + compressed_state_format: bool = False, ) -> MutableMapping[str, list[State | dict[str, Any]]]: """ Return states changes during UTC period start_time - end_time. @@ -304,6 +314,7 @@ def get_significant_states_with_session( include_start_time_state, minimal_response, no_attributes, + compressed_state_format, ) @@ -541,7 +552,7 @@ def _get_states_baked_query_for_all( return baked_query -def _get_states_with_session( +def _get_rows_with_session( hass: HomeAssistant, session: Session, utc_point_in_time: datetime, @@ -549,7 +560,7 @@ def _get_states_with_session( run: RecorderRuns | None = None, filters: Any | None = None, no_attributes: bool = False, -) -> list[State]: +) -> list[Row]: """Return the states at a specific point in time.""" if entity_ids and len(entity_ids) == 1: return _get_single_entity_states_with_session( @@ -570,17 +581,13 @@ def _get_states_with_session( else: baked_query = _get_states_baked_query_for_all(hass, filters, no_attributes) - attr_cache: dict[str, dict[str, Any]] = {} - return [ - LazyState(row, attr_cache) - for row in execute( - baked_query(session).params( - run_start=run.start, - utc_point_in_time=utc_point_in_time, - entity_ids=entity_ids, - ) + return execute( + baked_query(session).params( + run_start=run.start, + utc_point_in_time=utc_point_in_time, + entity_ids=entity_ids, ) - ] + ) def _get_single_entity_states_with_session( @@ -589,7 +596,7 @@ def _get_single_entity_states_with_session( utc_point_in_time: datetime, entity_id: str, no_attributes: bool = False, -) -> list[State]: +) -> list[Row]: # Use an entirely different (and extremely fast) query if we only # have a single entity id baked_query, join_attributes = bake_query_and_join_attributes(hass, no_attributes) @@ -607,19 +614,20 @@ def _get_single_entity_states_with_session( utc_point_in_time=utc_point_in_time, entity_id=entity_id ) - return [LazyState(row) for row in execute(query)] + return execute(query) def _sorted_states_to_dict( hass: HomeAssistant, session: Session, - states: Iterable[States], + states: Iterable[Row], start_time: datetime, entity_ids: list[str] | None, filters: Any = None, include_start_time_state: bool = True, minimal_response: bool = False, no_attributes: bool = False, + compressed_state_format: bool = False, ) -> MutableMapping[str, list[State | dict[str, Any]]]: """Convert SQL results into JSON friendly data structure. @@ -632,6 +640,19 @@ def _sorted_states_to_dict( each list of states, otherwise our graphs won't start on the Y axis correctly. """ + if compressed_state_format: + state_class = row_to_compressed_state + _process_timestamp: Callable[ + [datetime], float | str + ] = process_datetime_to_timestamp + attr_last_changed = COMPRESSED_STATE_LAST_CHANGED + attr_state = COMPRESSED_STATE_STATE + else: + state_class = LazyState # type: ignore[assignment] + _process_timestamp = process_timestamp_to_utc_isoformat + attr_last_changed = LAST_CHANGED_KEY + attr_state = STATE_KEY + result: dict[str, list[State | dict[str, Any]]] = defaultdict(list) # Set all entity IDs to empty lists in result set to maintain the order if entity_ids is not None: @@ -640,27 +661,24 @@ def _sorted_states_to_dict( # Get the states at the start time timer_start = time.perf_counter() + initial_states: dict[str, Row] = {} if include_start_time_state: - for state in _get_states_with_session( - hass, - session, - start_time, - entity_ids, - filters=filters, - no_attributes=no_attributes, - ): - state.last_updated = start_time - state.last_changed = start_time - result[state.entity_id].append(state) + initial_states = { + row.entity_id: row + for row in _get_rows_with_session( + hass, + session, + start_time, + entity_ids, + filters=filters, + no_attributes=no_attributes, + ) + } if _LOGGER.isEnabledFor(logging.DEBUG): elapsed = time.perf_counter() - timer_start _LOGGER.debug("getting %d first datapoints took %fs", len(result), elapsed) - # Called in a tight loop so cache the function - # here - _process_timestamp_to_utc_isoformat = process_timestamp_to_utc_isoformat - if entity_ids and len(entity_ids) == 1: states_iter: Iterable[tuple[str | Column, Iterator[States]]] = ( (entity_ids[0], iter(states)), @@ -670,11 +688,15 @@ def _sorted_states_to_dict( # Append all changes to it for ent_id, group in states_iter: - ent_results = result[ent_id] attr_cache: dict[str, dict[str, Any]] = {} + prev_state: Column | str + ent_results = result[ent_id] + if row := initial_states.pop(ent_id, None): + prev_state = row.state + ent_results.append(state_class(row, attr_cache, start_time)) if not minimal_response or split_entity_id(ent_id)[0] in NEED_ATTRIBUTE_DOMAINS: - ent_results.extend(LazyState(db_state, attr_cache) for db_state in group) + ent_results.extend(state_class(db_state, attr_cache) for db_state in group) continue # With minimal response we only provide a native @@ -684,34 +706,35 @@ def _sorted_states_to_dict( if not ent_results: if (first_state := next(group, None)) is None: continue - ent_results.append(LazyState(first_state, attr_cache)) + prev_state = first_state.state + ent_results.append(state_class(first_state, attr_cache)) - assert isinstance(ent_results[-1], State) - prev_state: Column | str = ent_results[-1].state initial_state_count = len(ent_results) - - db_state = None - for db_state in group: + row = None + for row in group: # With minimal response we do not care about attribute # changes so we can filter out duplicate states - if (state := db_state.state) == prev_state: + if (state := row.state) == prev_state: continue ent_results.append( { - STATE_KEY: state, - LAST_CHANGED_KEY: _process_timestamp_to_utc_isoformat( - db_state.last_changed - ), + attr_state: state, + attr_last_changed: _process_timestamp(row.last_changed), } ) prev_state = state - if db_state and len(ent_results) != initial_state_count: + if row and len(ent_results) != initial_state_count: # There was at least one state change # replace the last minimal state with # a full state - ent_results[-1] = LazyState(db_state, attr_cache) + ent_results[-1] = state_class(row, attr_cache) + + # If there are no states beyond the initial state, + # the state a was never popped from initial_states + for ent_id, row in initial_states.items(): + result[ent_id].append(state_class(row, {}, start_time)) # Filter out the empty lists if some states had 0 results. return {key: val for key, val in result.items() if val} diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 9cc94b3019f..38d6b3319aa 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -28,6 +28,12 @@ from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.orm.session import Session +from homeassistant.components.websocket_api.const import ( + COMPRESSED_STATE_ATTRIBUTES, + COMPRESSED_STATE_LAST_CHANGED, + COMPRESSED_STATE_LAST_UPDATED, + COMPRESSED_STATE_STATE, +) from homeassistant.const import ( MAX_LENGTH_EVENT_CONTEXT_ID, MAX_LENGTH_EVENT_EVENT_TYPE, @@ -612,6 +618,13 @@ def process_timestamp_to_utc_isoformat(ts: datetime | None) -> str | None: return ts.astimezone(dt_util.UTC).isoformat() +def process_datetime_to_timestamp(ts: datetime) -> float: + """Process a timestamp into a unix timestamp.""" + if ts.tzinfo == dt_util.UTC: + return ts.timestamp() + return ts.replace(tzinfo=dt_util.UTC).timestamp() + + class LazyState(State): """A lazy version of core State.""" @@ -621,45 +634,30 @@ class LazyState(State): "_last_changed", "_last_updated", "_context", - "_attr_cache", + "attr_cache", ] def __init__( # pylint: disable=super-init-not-called - self, row: Row, attr_cache: dict[str, dict[str, Any]] | None = None + self, + row: Row, + attr_cache: dict[str, dict[str, Any]], + start_time: datetime | None = None, ) -> None: """Init the lazy state.""" self._row = row self.entity_id: str = self._row.entity_id self.state = self._row.state or "" self._attributes: dict[str, Any] | None = None - self._last_changed: datetime | None = None - self._last_updated: datetime | None = None + self._last_changed: datetime | None = start_time + self._last_updated: datetime | None = start_time self._context: Context | None = None - self._attr_cache = attr_cache + self.attr_cache = attr_cache @property # type: ignore[override] def attributes(self) -> dict[str, Any]: # type: ignore[override] """State attributes.""" if self._attributes is None: - source = self._row.shared_attrs or self._row.attributes - if self._attr_cache is not None and ( - attributes := self._attr_cache.get(source) - ): - self._attributes = attributes - return attributes - if source == EMPTY_JSON_OBJECT or source is None: - self._attributes = {} - return self._attributes - try: - self._attributes = json.loads(source) - except ValueError: - # When json.loads fails - _LOGGER.exception( - "Error converting row to state attributes: %s", self._row - ) - self._attributes = {} - if self._attr_cache is not None: - self._attr_cache[source] = self._attributes + self._attributes = decode_attributes_from_row(self._row, self.attr_cache) return self._attributes @attributes.setter @@ -748,3 +746,48 @@ class LazyState(State): and self.state == other.state and self.attributes == other.attributes ) + + +def decode_attributes_from_row( + row: Row, attr_cache: dict[str, dict[str, Any]] +) -> dict[str, Any]: + """Decode attributes from a database row.""" + source: str = row.shared_attrs or row.attributes + if (attributes := attr_cache.get(source)) is not None: + return attributes + if not source or source == EMPTY_JSON_OBJECT: + return {} + try: + attr_cache[source] = attributes = json.loads(source) + except ValueError: + _LOGGER.exception("Error converting row to state attributes: %s", source) + attr_cache[source] = attributes = {} + return attributes + + +def row_to_compressed_state( + row: Row, + attr_cache: dict[str, dict[str, Any]], + start_time: datetime | None = None, +) -> dict[str, Any]: + """Convert a database row to a compressed state.""" + if start_time: + last_changed = last_updated = start_time.timestamp() + else: + row_changed_changed: datetime = row.last_changed + if ( + not (row_last_updated := row.last_updated) + or row_last_updated == row_changed_changed + ): + last_changed = last_updated = process_datetime_to_timestamp( + row_changed_changed + ) + else: + last_changed = process_datetime_to_timestamp(row_changed_changed) + last_updated = process_datetime_to_timestamp(row_last_updated) + return { + COMPRESSED_STATE_STATE: row.state, + COMPRESSED_STATE_ATTRIBUTES: decode_attributes_from_row(row, attr_cache), + COMPRESSED_STATE_LAST_CHANGED: last_changed, + COMPRESSED_STATE_LAST_UPDATED: last_updated, + } diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 6c5615ad253..107cf6d0270 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -56,3 +56,9 @@ DATA_CONNECTIONS: Final = f"{DOMAIN}.connections" JSON_DUMP: Final = partial( json.dumps, cls=JSONEncoder, allow_nan=False, separators=(",", ":") ) + +COMPRESSED_STATE_STATE = "s" +COMPRESSED_STATE_ATTRIBUTES = "a" +COMPRESSED_STATE_CONTEXT = "c" +COMPRESSED_STATE_LAST_CHANGED = "lc" +COMPRESSED_STATE_LAST_UPDATED = "lu" diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index eac40c9510b..1c09bc1d567 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -16,6 +16,13 @@ from homeassistant.util.json import ( from homeassistant.util.yaml.loader import JSON_TYPE from . import const +from .const import ( + COMPRESSED_STATE_ATTRIBUTES, + COMPRESSED_STATE_CONTEXT, + COMPRESSED_STATE_LAST_CHANGED, + COMPRESSED_STATE_LAST_UPDATED, + COMPRESSED_STATE_STATE, +) _LOGGER: Final = logging.getLogger(__name__) @@ -31,12 +38,6 @@ BASE_COMMAND_MESSAGE_SCHEMA: Final = vol.Schema({vol.Required("id"): cv.positive IDEN_TEMPLATE: Final = "__IDEN__" IDEN_JSON_TEMPLATE: Final = '"__IDEN__"' -COMPRESSED_STATE_STATE = "s" -COMPRESSED_STATE_ATTRIBUTES = "a" -COMPRESSED_STATE_CONTEXT = "c" -COMPRESSED_STATE_LAST_CHANGED = "lc" -COMPRESSED_STATE_LAST_UPDATED = "lu" - STATE_DIFF_ADDITIONS = "+" STATE_DIFF_REMOVALS = "-" diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 143b0c55fab..1dc18e5cc73 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -1070,3 +1070,409 @@ async def test_list_statistic_ids( response = await client.receive_json() assert response["success"] assert response["result"] == [] + + +async def test_history_during_period(hass, hass_ws_client, recorder_mock): + """Test history_during_period.""" + now = dt_util.utcnow() + + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "changed"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "again"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_wait_recording_done(hass) + + do_adhoc_statistics(hass, start=now) + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "entity_ids": ["sensor.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + await client.send_json( + { + "id": 2, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "entity_ids": ["sensor.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 2 + + sensor_test_history = response["result"]["sensor.test"] + assert len(sensor_test_history) == 3 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {} + assert isinstance(sensor_test_history[0]["lu"], float) + assert isinstance(sensor_test_history[0]["lc"], float) + + assert "a" not in sensor_test_history[1] + assert sensor_test_history[1]["s"] == "off" + assert isinstance(sensor_test_history[1]["lc"], float) + + assert sensor_test_history[2]["s"] == "on" + assert sensor_test_history[2]["a"] == {} + + await client.send_json( + { + "id": 3, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "entity_ids": ["sensor.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 3 + sensor_test_history = response["result"]["sensor.test"] + + assert len(sensor_test_history) == 5 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {"any": "attr"} + assert isinstance(sensor_test_history[0]["lu"], float) + assert isinstance(sensor_test_history[0]["lc"], float) + + assert sensor_test_history[1]["s"] == "off" + assert isinstance(sensor_test_history[1]["lc"], float) + assert sensor_test_history[1]["a"] == {"any": "attr"} + + assert sensor_test_history[4]["s"] == "on" + assert sensor_test_history[4]["a"] == {"any": "attr"} + + await client.send_json( + { + "id": 4, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "entity_ids": ["sensor.test"], + "include_start_time_state": True, + "significant_changes_only": True, + "no_attributes": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 4 + sensor_test_history = response["result"]["sensor.test"] + + assert len(sensor_test_history) == 3 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {"any": "attr"} + assert isinstance(sensor_test_history[0]["lu"], float) + assert isinstance(sensor_test_history[0]["lc"], float) + + assert sensor_test_history[1]["s"] == "off" + assert isinstance(sensor_test_history[1]["lc"], float) + assert sensor_test_history[1]["a"] == {"any": "attr"} + + assert sensor_test_history[2]["s"] == "on" + assert sensor_test_history[2]["a"] == {"any": "attr"} + + +async def test_history_during_period_impossible_conditions( + hass, hass_ws_client, recorder_mock +): + """Test history_during_period returns when condition cannot be true.""" + now = dt_util.utcnow() + + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "changed"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "again"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_wait_recording_done(hass) + + do_adhoc_statistics(hass, start=now) + await async_wait_recording_done(hass) + + after = dt_util.utcnow() + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/history_during_period", + "start_time": after.isoformat(), + "end_time": after.isoformat(), + "entity_ids": ["sensor.test"], + "include_start_time_state": False, + "significant_changes_only": False, + "no_attributes": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["result"] == {} + + future = dt_util.utcnow() + timedelta(hours=10) + + await client.send_json( + { + "id": 2, + "type": "history/history_during_period", + "start_time": future.isoformat(), + "entity_ids": ["sensor.test"], + "include_start_time_state": True, + "significant_changes_only": True, + "no_attributes": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 2 + assert response["result"] == {} + + +@pytest.mark.parametrize( + "time_zone", ["UTC", "Europe/Berlin", "America/Chicago", "US/Hawaii"] +) +async def test_history_during_period_significant_domain( + time_zone, hass, hass_ws_client, recorder_mock +): + """Test history_during_period with climate domain.""" + hass.config.set_time_zone(time_zone) + now = dt_util.utcnow() + + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "on", attributes={"temperature": "1"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "off", attributes={"temperature": "2"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "off", attributes={"temperature": "3"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "off", attributes={"temperature": "4"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "on", attributes={"temperature": "5"}) + await async_wait_recording_done(hass) + + do_adhoc_statistics(hass, start=now) + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + await client.send_json( + { + "id": 2, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 2 + + sensor_test_history = response["result"]["climate.test"] + assert len(sensor_test_history) == 5 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {} + assert isinstance(sensor_test_history[0]["lu"], float) + assert isinstance(sensor_test_history[0]["lc"], float) + + assert "a" in sensor_test_history[1] + assert sensor_test_history[1]["s"] == "off" + assert isinstance(sensor_test_history[1]["lc"], float) + + assert sensor_test_history[4]["s"] == "on" + assert sensor_test_history[4]["a"] == {} + + await client.send_json( + { + "id": 3, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 3 + sensor_test_history = response["result"]["climate.test"] + + assert len(sensor_test_history) == 5 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {"temperature": "1"} + assert isinstance(sensor_test_history[0]["lu"], float) + assert isinstance(sensor_test_history[0]["lc"], float) + + assert sensor_test_history[1]["s"] == "off" + assert isinstance(sensor_test_history[1]["lc"], float) + assert sensor_test_history[1]["a"] == {"temperature": "2"} + + assert sensor_test_history[4]["s"] == "on" + assert sensor_test_history[4]["a"] == {"temperature": "5"} + + await client.send_json( + { + "id": 4, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": True, + "no_attributes": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 4 + sensor_test_history = response["result"]["climate.test"] + + assert len(sensor_test_history) == 5 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {"temperature": "1"} + assert isinstance(sensor_test_history[0]["lu"], float) + assert isinstance(sensor_test_history[0]["lc"], float) + + assert sensor_test_history[1]["s"] == "off" + assert isinstance(sensor_test_history[1]["lc"], float) + assert sensor_test_history[1]["a"] == {"temperature": "2"} + + assert sensor_test_history[2]["s"] == "off" + assert sensor_test_history[2]["a"] == {"temperature": "3"} + + assert sensor_test_history[3]["s"] == "off" + assert sensor_test_history[3]["a"] == {"temperature": "4"} + + assert sensor_test_history[4]["s"] == "on" + assert sensor_test_history[4]["a"] == {"temperature": "5"} + + # Test we impute the state time state + later = dt_util.utcnow() + await client.send_json( + { + "id": 5, + "type": "history/history_during_period", + "start_time": later.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": True, + "no_attributes": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 5 + sensor_test_history = response["result"]["climate.test"] + + assert len(sensor_test_history) == 1 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {"temperature": "5"} + assert sensor_test_history[0]["lu"] == later.timestamp() + assert sensor_test_history[0]["lc"] == later.timestamp() + + +async def test_history_during_period_bad_start_time( + hass, hass_ws_client, recorder_mock +): + """Test history_during_period bad state time.""" + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/history_during_period", + "start_time": "cats", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_start_time" + + +async def test_history_during_period_bad_end_time(hass, hass_ws_client, recorder_mock): + """Test history_during_period bad end time.""" + now = dt_util.utcnow() + + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "end_time": "dogs", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_end_time" diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index 20b60c3c96d..a98712ef282 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -14,6 +14,7 @@ from homeassistant.components import recorder from homeassistant.components.recorder import history from homeassistant.components.recorder.models import ( Events, + LazyState, RecorderRuns, StateAttributes, States, @@ -40,9 +41,19 @@ async def _async_get_states( def _get_states_with_session(): with session_scope(hass=hass) as session: - return history._get_states_with_session( - hass, session, utc_point_in_time, entity_ids, run, None, no_attributes - ) + attr_cache = {} + return [ + LazyState(row, attr_cache) + for row in history._get_rows_with_session( + hass, + session, + utc_point_in_time, + entity_ids, + run, + None, + no_attributes, + ) + ] return await recorder.get_instance(hass).async_add_executor_job( _get_states_with_session diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index a68d137eb0f..874d84ef2ad 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -247,7 +247,7 @@ async def test_lazy_state_handles_include_json(caplog): entity_id="sensor.invalid", shared_attrs="{INVALID_JSON}", ) - assert LazyState(row).attributes == {} + assert LazyState(row, {}).attributes == {} assert "Error converting row to state attributes" in caplog.text @@ -258,7 +258,7 @@ async def test_lazy_state_prefers_shared_attrs_over_attrs(caplog): shared_attrs='{"shared":true}', attributes='{"shared":false}', ) - assert LazyState(row).attributes == {"shared": True} + assert LazyState(row, {}).attributes == {"shared": True} async def test_lazy_state_handles_different_last_updated_and_last_changed(caplog): @@ -271,7 +271,7 @@ async def test_lazy_state_handles_different_last_updated_and_last_changed(caplog last_updated=now, last_changed=now - timedelta(seconds=60), ) - lstate = LazyState(row) + lstate = LazyState(row, {}) assert lstate.as_dict() == { "attributes": {"shared": True}, "entity_id": "sensor.valid", @@ -300,7 +300,7 @@ async def test_lazy_state_handles_same_last_updated_and_last_changed(caplog): last_updated=now, last_changed=now, ) - lstate = LazyState(row) + lstate = LazyState(row, {}) assert lstate.as_dict() == { "attributes": {"shared": True}, "entity_id": "sensor.valid", From 69a8232b4560e028525928c00b1d650e9ff3b7d7 Mon Sep 17 00:00:00 2001 From: Nic Jansma Date: Wed, 11 May 2022 20:22:54 -0400 Subject: [PATCH 0394/3516] Add missing Coinbase RATEs (#65101) --- homeassistant/components/coinbase/const.py | 95 +++++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/coinbase/const.py b/homeassistant/components/coinbase/const.py index 85535613851..08773745f09 100644 --- a/homeassistant/components/coinbase/const.py +++ b/homeassistant/components/coinbase/const.py @@ -278,21 +278,31 @@ WALLETS = { RATES = { "1INCH": "1INCH", "AAVE": "AAVE", + "ACH": "ACH", "ADA": "ADA", "AED": "AED", "AFN": "AFN", + "AGLD": "AGLD", + "ALCX": "ALCX", "ALGO": "ALGO", "ALL": "ALL", "AMD": "AMD", + "AMP": "AMP", "ANG": "ANG", "ANKR": "ANKR", "AOA": "AOA", + "API3": "API3", + "ARPA": "ARPA", "ARS": "ARS", + "ASM": "ASM", "ATOM": "ATOM", "AUCTION": "AUCTION", "AUD": "AUD", + "AVAX": "AVAX", "AWG": "AWG", + "AXS": "AXS", "AZN": "AZN", + "BADGER": "BADGER", "BAL": "BAL", "BAM": "BAM", "BAND": "BAND", @@ -302,16 +312,20 @@ RATES = { "BDT": "BDT", "BGN": "BGN", "BHD": "BHD", + "BICO": "BICO", "BIF": "BIF", + "BLZ": "BLZ", "BMD": "BMD", "BND": "BND", "BNT": "BNT", "BOB": "BOB", + "BOND": "BOND", "BRL": "BRL", "BSD": "BSD", "BSV": "BSV", "BTC": "BTC", "BTN": "BTN", + "BTRST": "BTRST", "BWP": "BWP", "BYN": "BYN", "BYR": "BYR", @@ -320,6 +334,7 @@ RATES = { "CDF": "CDF", "CGLD": "CGLD", "CHF": "CHF", + "CHZ": "CHZ", "CLF": "CLF", "CLP": "CLP", "CLV": "CLV", @@ -327,21 +342,32 @@ RATES = { "CNY": "CNY", "COMP": "COMP", "COP": "COP", + "COTI": "COTI", + "COVAL": "COVAL", "CRC": "CRC", + "CRO": "CRO", "CRV": "CRV", + "CTSI": "CTSI", + "CTX": "CTX", "CUC": "CUC", "CVC": "CVC", "CVE": "CVE", "CZK": "CZK", "DAI": "DAI", "DASH": "DASH", + "DDX": "DDX", + "DESO": "DESO", + "DIA": "DIA", "DJF": "DJF", "DKK": "DKK", "DNT": "DNT", + "DOGE": "DOGE", "DOP": "DOP", + "DOT": "DOT", "DZD": "DZD", "EGP": "EGP", "ENJ": "ENJ", + "ENS": "ENS", "EOS": "EOS", "ERN": "ERN", "ETB": "ETB", @@ -349,50 +375,69 @@ RATES = { "ETH": "ETH", "ETH2": "ETH2", "EUR": "EUR", + "FARM": "FARM", "FET": "FET", "FIL": "FIL", "FJD": "FJD", "FKP": "FKP", "FORTH": "FORTH", + "FOX": "FOX", + "FX": "FX", + "GALA": "GALA", "GBP": "GBP", "GBX": "GBX", "GEL": "GEL", + "GFI": "GFI", "GGP": "GGP", "GHS": "GHS", "GIP": "GIP", "GMD": "GMD", "GNF": "GNF", + "GODS": "GODS", "GRT": "GRT", + "GTC": "GTC", "GTQ": "GTQ", "GYD": "GYD", + "GYEN": "GYEN", "HKD": "HKD", "HNL": "HNL", "HRK": "HRK", "HTG": "HTG", "HUF": "HUF", + "ICP": "ICP", + "IDEX": "IDEX", "IDR": "IDR", "ILS": "ILS", "IMP": "IMP", + "IMX": "IMX", "INR": "INR", + "INV": "INV", + "IOTX": "IOTX", "IQD": "IQD", "ISK": "ISK", + "JASMY": "JASMY", "JEP": "JEP", "JMD": "JMD", "JOD": "JOD", "JPY": "JPY", + "KEEP": "KEEP", "KES": "KES", "KGS": "KGS", "KHR": "KHR", "KMF": "KMF", "KNC": "KNC", + "KRL": "KRL", "KRW": "KRW", "KWD": "KWD", "KYD": "KYD", "KZT": "KZT", "LAK": "LAK", "LBP": "LBP", + "LCX": "LCX", "LINK": "LINK", "LKR": "LKR", + "LPT": "LPT", + "LQTY": "LQTY", "LRC": "LRC", "LRD": "LRD", "LSL": "LSL", @@ -400,23 +445,31 @@ RATES = { "LYD": "LYD", "MAD": "MAD", "MANA": "MANA", + "MASK": "MASK", "MATIC": "MATIC", + "MCO2": "MCO2", "MDL": "MDL", + "MDT": "MDT", "MGA": "MGA", + "MIR": "MIR", "MKD": "MKD", "MKR": "MKR", + "MLN": "MLN", "MMK": "MMK", "MNT": "MNT", "MOP": "MOP", + "MPL": "MPL", "MRO": "MRO", "MTL": "MTL", "MUR": "MUR", + "MUSD": "MUSD", "MVR": "MVR", "MWK": "MWK", "MXN": "MXN", "MYR": "MYR", "MZN": "MZN", "NAD": "NAD", + "NCT": "NCT", "NGN": "NGN", "NIO": "NIO", "NKN": "NKN", @@ -428,19 +481,37 @@ RATES = { "OGN": "OGN", "OMG": "OMG", "OMR": "OMR", + "ORN": "ORN", "OXT": "OXT", "PAB": "PAB", + "PAX": "PAX", "PEN": "PEN", + "PERP": "PERP", "PGK": "PGK", "PHP": "PHP", "PKR": "PKR", + "PLA": "PLA", "PLN": "PLN", + "PLU": "PLU", + "POLS": "POLS", "POLY": "POLY", + "POWR": "POWR", + "PRO": "PRO", "PYG": "PYG", "QAR": "QAR", - "RLY": "RLY", + "QNT": "QNT", + "QUICK": "QUICK", + "RAD": "RAD", + "RAI": "RAI", + "RARI": "RARI", + "RBN": "RBN", "REN": "REN", "REP": "REP", + "REPV2": "REPV2", + "REQ": "REQ", + "RGT": "RGT", + "RLC": "RLC", + "RLY": "RLY", "RON": "RON", "RSD": "RSD", "RUB": "RUB", @@ -452,22 +523,34 @@ RATES = { "SGD": "SGD", "SHIB": "SHIB", "SHP": "SHP", + "SHPING": "SHPING", + "SKK": "SKK", "SKL": "SKL", "SLL": "SLL", "SNX": "SNX", + "SOL": "SOL", "SOS": "SOS", + "SPELL": "SPELL", "SRD": "SRD", "SSP": "SSP", "STD": "STD", "STORJ": "STORJ", + "STX": "STX", + "SUKU": "SUKU", + "SUPER": "SUPER", "SUSHI": "SUSHI", "SVC": "SVC", "SZL": "SZL", "THB": "THB", "TJS": "TJS", + "TMM": "TMM", "TMT": "TMT", "TND": "TND", "TOP": "TOP", + "TRAC": "TRAC", + "TRB": "TRB", + "TRIBE": "TRIBE", + "TRU": "TRU", "TRY": "TRY", "TTD": "TTD", "TWD": "TWD", @@ -475,15 +558,21 @@ RATES = { "UAH": "UAH", "UGX": "UGX", "UMA": "UMA", + "UNFI": "UNFI", "UNI": "UNI", "USD": "USD", "USDC": "USDC", + "USDT": "USDT", + "UST": "UST", "UYU": "UYU", "UZS": "UZS", "VES": "VES", + "VGX": "VGX", "VND": "VND", "VUV": "VUV", "WBTC": "WBTC", + "WCFG": "WCFG", + "WLUNA": "WLUNA", "WST": "WST", "XAF": "XAF", "XAG": "XAG", @@ -495,11 +584,15 @@ RATES = { "XPD": "XPD", "XPF": "XPF", "XPT": "XPT", + "XRP": "XRP", "XTZ": "XTZ", + "XYO": "XYO", "YER": "YER", "YFI": "YFI", + "YFII": "YFII", "ZAR": "ZAR", "ZEC": "ZEC", + "ZEN": "ZEN", "ZMW": "ZMW", "ZRX": "ZRX", "ZWL": "ZWL", From 08851d8366e0860619fc605637267ee4a99a652d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 12 May 2022 02:32:02 +0200 Subject: [PATCH 0395/3516] Remove YAML configuration from International Space Station (ISS) (#71693) --- homeassistant/components/iss/binary_sensor.py | 45 ++----------------- homeassistant/components/iss/config_flow.py | 9 ---- tests/components/iss/test_config_flow.py | 20 +-------- 3 files changed, 5 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/iss/binary_sensor.py b/homeassistant/components/iss/binary_sensor.py index 12a8a7514b2..e2034fb48f9 100644 --- a/homeassistant/components/iss/binary_sensor.py +++ b/homeassistant/components/iss/binary_sensor.py @@ -7,24 +7,14 @@ import logging import pyiss import requests from requests.exceptions import HTTPError -import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - ATTR_LATITUDE, - ATTR_LONGITUDE, - CONF_NAME, - CONF_SHOW_ON_MAP, -) +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle -from .const import DOMAIN - _LOGGER = logging.getLogger(__name__) ATTR_ISS_NEXT_RISE = "next_rise" @@ -35,35 +25,6 @@ DEFAULT_DEVICE_CLASS = "visible" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean, - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Import ISS configuration from yaml.""" - _LOGGER.warning( - "Configuration of the iss platform in YAML is deprecated and will be " - "removed in Home Assistant 2022.5; 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=config, - ) - ) - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/iss/config_flow.py b/homeassistant/components/iss/config_flow.py index dc80126bd14..b43949daadc 100644 --- a/homeassistant/components/iss/config_flow.py +++ b/homeassistant/components/iss/config_flow.py @@ -43,15 +43,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user") - async def async_step_import(self, conf: dict) -> FlowResult: - """Import a configuration from configuration.yaml.""" - return await self.async_step_user( - user_input={ - CONF_NAME: conf[CONF_NAME], - CONF_SHOW_ON_MAP: conf[CONF_SHOW_ON_MAP], - } - ) - class OptionsFlowHandler(config_entries.OptionsFlow): """Config flow options handler for iss.""" diff --git a/tests/components/iss/test_config_flow.py b/tests/components/iss/test_config_flow.py index 09f7f391b89..3260b76432f 100644 --- a/tests/components/iss/test_config_flow.py +++ b/tests/components/iss/test_config_flow.py @@ -2,31 +2,15 @@ from unittest.mock import patch from homeassistant import data_entry_flow -from homeassistant.components.iss.binary_sensor import DEFAULT_NAME from homeassistant.components.iss.const import DOMAIN from homeassistant.config import async_process_ha_core_config -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import CONF_NAME, CONF_SHOW_ON_MAP +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -async def test_import(hass: HomeAssistant): - """Test entry will be imported.""" - - imported_config = {CONF_NAME: DEFAULT_NAME, CONF_SHOW_ON_MAP: False} - - with patch("homeassistant.components.iss.async_setup_entry", return_value=True): - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=imported_config - ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result.get("result").title == DEFAULT_NAME - assert result.get("result").options == {CONF_SHOW_ON_MAP: False} - - async def test_create_entry(hass: HomeAssistant): """Test we can finish a config flow.""" From 19168227ebac3861ac38ecffd93379ff8bf006c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 May 2022 21:30:36 -0500 Subject: [PATCH 0396/3516] Fix sqlalchemy warning about logbook query being converted from subquery (#71710) --- homeassistant/components/logbook/queries.py | 34 ++++++++++----------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/logbook/queries.py b/homeassistant/components/logbook/queries.py index 8c13719a1cf..08f6d759d25 100644 --- a/homeassistant/components/logbook/queries.py +++ b/homeassistant/components/logbook/queries.py @@ -6,7 +6,7 @@ from datetime import datetime as dt from typing import Any import sqlalchemy -from sqlalchemy import lambda_stmt, select +from sqlalchemy import lambda_stmt, select, union_all from sqlalchemy.orm import aliased from sqlalchemy.sql.expression import literal from sqlalchemy.sql.lambdas import StatementLambdaElement @@ -118,15 +118,15 @@ def _select_entities_context_ids_sub_query( entity_ids: list[str], ) -> Select: """Generate a subquery to find context ids for multiple entities.""" - return ( - _select_events_context_id_subquery(start_day, end_day, event_types) - .where(_apply_event_entity_id_matchers(entity_ids)) - .union_all( + return select( + union_all( + _select_events_context_id_subquery(start_day, end_day, event_types).where( + _apply_event_entity_id_matchers(entity_ids) + ), select(States.context_id) .filter((States.last_updated > start_day) & (States.last_updated < end_day)) - .where(States.entity_id.in_(entity_ids)) - ) - .subquery() + .where(States.entity_id.in_(entity_ids)), + ).c.context_id ) @@ -183,18 +183,16 @@ def _select_entity_context_ids_sub_query( entity_id_like: str, ) -> Select: """Generate a subquery to find context ids for a single entity.""" - return ( - _select_events_context_id_subquery(start_day, end_day, event_types) - .where( - Events.event_data.like(entity_id_like) - | EventData.shared_data.like(entity_id_like) - ) - .union_all( + return select( + union_all( + _select_events_context_id_subquery(start_day, end_day, event_types).where( + Events.event_data.like(entity_id_like) + | EventData.shared_data.like(entity_id_like) + ), select(States.context_id) .filter((States.last_updated > start_day) & (States.last_updated < end_day)) - .where(States.entity_id == entity_id) - ) - .subquery() + .where(States.entity_id == entity_id), + ).c.context_id ) From a03a4b0d131ddb6842b7ca09b6bd436c2fd7b983 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Wed, 11 May 2022 22:32:19 -0400 Subject: [PATCH 0397/3516] ElkM1 integration updates for new version of base library (#71508) Co-authored-by: J. Nick Koston --- .../components/elkm1/alarm_control_panel.py | 28 ++++---- homeassistant/components/elkm1/climate.py | 67 +++++++++--------- homeassistant/components/elkm1/manifest.json | 2 +- homeassistant/components/elkm1/sensor.py | 69 ++++++++----------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 82 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index 49aa6f68a09..6b6a5b44d55 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -205,18 +205,18 @@ class ElkArea(ElkAttachedEntity, AlarmControlPanelEntity, RestoreEntity): def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None: elk_state_to_hass_state = { - ArmedStatus.DISARMED.value: STATE_ALARM_DISARMED, - ArmedStatus.ARMED_AWAY.value: STATE_ALARM_ARMED_AWAY, - ArmedStatus.ARMED_STAY.value: STATE_ALARM_ARMED_HOME, - ArmedStatus.ARMED_STAY_INSTANT.value: STATE_ALARM_ARMED_HOME, - ArmedStatus.ARMED_TO_NIGHT.value: STATE_ALARM_ARMED_NIGHT, - ArmedStatus.ARMED_TO_NIGHT_INSTANT.value: STATE_ALARM_ARMED_NIGHT, - ArmedStatus.ARMED_TO_VACATION.value: STATE_ALARM_ARMED_AWAY, + ArmedStatus.DISARMED: STATE_ALARM_DISARMED, + ArmedStatus.ARMED_AWAY: STATE_ALARM_ARMED_AWAY, + ArmedStatus.ARMED_STAY: STATE_ALARM_ARMED_HOME, + ArmedStatus.ARMED_STAY_INSTANT: STATE_ALARM_ARMED_HOME, + ArmedStatus.ARMED_TO_NIGHT: STATE_ALARM_ARMED_NIGHT, + ArmedStatus.ARMED_TO_NIGHT_INSTANT: STATE_ALARM_ARMED_NIGHT, + ArmedStatus.ARMED_TO_VACATION: STATE_ALARM_ARMED_AWAY, } if self._element.alarm_state is None: self._state = None - elif self._element.alarm_state >= AlarmState.FIRE_ALARM.value: + elif self._element.in_alarm_state(): # Area is in alarm state self._state = STATE_ALARM_TRIGGERED elif self._entry_exit_timer_is_running(): @@ -239,32 +239,32 @@ class ElkArea(ElkAttachedEntity, AlarmControlPanelEntity, RestoreEntity): async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" if code is not None: - self._element.arm(ArmLevel.ARMED_STAY.value, int(code)) + self._element.arm(ArmLevel.ARMED_STAY, int(code)) async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" if code is not None: - self._element.arm(ArmLevel.ARMED_AWAY.value, int(code)) + self._element.arm(ArmLevel.ARMED_AWAY, int(code)) async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" if code is not None: - self._element.arm(ArmLevel.ARMED_NIGHT.value, int(code)) + self._element.arm(ArmLevel.ARMED_NIGHT, int(code)) async def async_alarm_arm_home_instant(self, code: str | None = None) -> None: """Send arm stay instant command.""" if code is not None: - self._element.arm(ArmLevel.ARMED_STAY_INSTANT.value, int(code)) + self._element.arm(ArmLevel.ARMED_STAY_INSTANT, int(code)) async def async_alarm_arm_night_instant(self, code: str | None = None) -> None: """Send arm night instant command.""" if code is not None: - self._element.arm(ArmLevel.ARMED_NIGHT_INSTANT.value, int(code)) + self._element.arm(ArmLevel.ARMED_NIGHT_INSTANT, int(code)) async def async_alarm_arm_vacation(self, code: str | None = None) -> None: """Send arm vacation command.""" if code is not None: - self._element.arm(ArmLevel.ARMED_VACATION.value, int(code)) + self._element.arm(ArmLevel.ARMED_VACATION, int(code)) async def async_display_message( self, clear: int, beep: bool, timeout: int, line1: str, line2: str diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index 7b6de739d4f..9f6dc359f6f 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -33,26 +33,26 @@ SUPPORT_HVAC = [ HVACMode.FAN_ONLY, ] HASS_TO_ELK_HVAC_MODES = { - HVACMode.OFF: (ThermostatMode.OFF.value, ThermostatFan.AUTO.value), - HVACMode.HEAT: (ThermostatMode.HEAT.value, None), - HVACMode.COOL: (ThermostatMode.COOL.value, None), - HVACMode.HEAT_COOL: (ThermostatMode.AUTO.value, None), - HVACMode.FAN_ONLY: (ThermostatMode.OFF.value, ThermostatFan.ON.value), + HVACMode.OFF: (ThermostatMode.OFF, ThermostatFan.AUTO), + HVACMode.HEAT: (ThermostatMode.HEAT, None), + HVACMode.COOL: (ThermostatMode.COOL, None), + HVACMode.HEAT_COOL: (ThermostatMode.AUTO, None), + HVACMode.FAN_ONLY: (ThermostatMode.OFF, ThermostatFan.ON), } ELK_TO_HASS_HVAC_MODES = { - ThermostatMode.OFF.value: HVACMode.OFF, - ThermostatMode.COOL.value: HVACMode.COOL, - ThermostatMode.HEAT.value: HVACMode.HEAT, - ThermostatMode.EMERGENCY_HEAT.value: HVACMode.HEAT, - ThermostatMode.AUTO.value: HVACMode.HEAT_COOL, + ThermostatMode.OFF: HVACMode.OFF, + ThermostatMode.COOL: HVACMode.COOL, + ThermostatMode.HEAT: HVACMode.HEAT, + ThermostatMode.EMERGENCY_HEAT: HVACMode.HEAT, + ThermostatMode.AUTO: HVACMode.HEAT_COOL, } HASS_TO_ELK_FAN_MODES = { - FAN_AUTO: (None, ThermostatFan.AUTO.value), - FAN_ON: (None, ThermostatFan.ON.value), + FAN_AUTO: (None, ThermostatFan.AUTO), + FAN_ON: (None, ThermostatFan.ON), } ELK_TO_HASS_FAN_MODES = { - ThermostatFan.AUTO.value: FAN_AUTO, - ThermostatFan.ON.value: FAN_ON, + ThermostatFan.AUTO: FAN_AUTO, + ThermostatFan.ON: FAN_ON, } @@ -84,7 +84,7 @@ class ElkThermostat(ElkEntity, ClimateEntity): def __init__(self, element: Element, elk: Elk, elk_data: dict[str, Any]) -> None: """Initialize climate entity.""" super().__init__(element, elk, elk_data) - self._state: str = HVACMode.OFF + self._state: str | None = None @property def temperature_unit(self) -> str: @@ -100,11 +100,11 @@ class ElkThermostat(ElkEntity, ClimateEntity): def target_temperature(self) -> float | None: """Return the temperature we are trying to reach.""" if self._element.mode in ( - ThermostatMode.HEAT.value, - ThermostatMode.EMERGENCY_HEAT.value, + ThermostatMode.HEAT, + ThermostatMode.EMERGENCY_HEAT, ): return self._element.heat_setpoint - if self._element.mode == ThermostatMode.COOL.value: + if self._element.mode == ThermostatMode.COOL: return self._element.cool_setpoint return None @@ -129,7 +129,7 @@ class ElkThermostat(ElkEntity, ClimateEntity): return self._element.humidity @property - def hvac_mode(self) -> str: + def hvac_mode(self) -> str | None: """Return current operation ie. heat, cool, idle.""" return self._state @@ -146,7 +146,7 @@ class ElkThermostat(ElkEntity, ClimateEntity): @property def is_aux_heat(self) -> bool: """Return if aux heater is on.""" - return self._element.mode == ThermostatMode.EMERGENCY_HEAT.value + return self._element.mode == ThermostatMode.EMERGENCY_HEAT @property def min_temp(self) -> float: @@ -159,15 +159,17 @@ class ElkThermostat(ElkEntity, ClimateEntity): return 99 @property - def fan_mode(self) -> str: + def fan_mode(self) -> str | None: """Return the fan setting.""" + if self._element.fan is None: + return None return ELK_TO_HASS_FAN_MODES[self._element.fan] - def _elk_set(self, mode: int | None, fan: int | None) -> None: + def _elk_set(self, mode: ThermostatMode | None, fan: ThermostatFan | None) -> None: if mode is not None: - self._element.set(ThermostatSetting.MODE.value, mode) + self._element.set(ThermostatSetting.MODE, mode) if fan is not None: - self._element.set(ThermostatSetting.FAN.value, fan) + self._element.set(ThermostatSetting.FAN, fan) async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set thermostat operation mode.""" @@ -176,11 +178,11 @@ class ElkThermostat(ElkEntity, ClimateEntity): async def async_turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" - self._elk_set(ThermostatMode.EMERGENCY_HEAT.value, None) + self._elk_set(ThermostatMode.EMERGENCY_HEAT, None) async def async_turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" - self._elk_set(ThermostatMode.HEAT.value, None) + self._elk_set(ThermostatMode.HEAT, None) @property def fan_modes(self) -> list[str]: @@ -197,11 +199,14 @@ class ElkThermostat(ElkEntity, ClimateEntity): low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) if low_temp is not None: - self._element.set(ThermostatSetting.HEAT_SETPOINT.value, round(low_temp)) + self._element.set(ThermostatSetting.HEAT_SETPOINT, round(low_temp)) if high_temp is not None: - self._element.set(ThermostatSetting.COOL_SETPOINT.value, round(high_temp)) + self._element.set(ThermostatSetting.COOL_SETPOINT, round(high_temp)) def _element_changed(self, element: Element, changeset: Any) -> None: - self._state = ELK_TO_HASS_HVAC_MODES[self._element.mode] - if self._state == HVACMode.OFF and self._element.fan == ThermostatFan.ON.value: - self._state = HVACMode.FAN_ONLY + if self._element.mode is None: + self._state = None + else: + self._state = ELK_TO_HASS_HVAC_MODES[self._element.mode] + if self._state == HVACMode.OFF and self._element.fan == ThermostatFan.ON: + self._state = HVACMode.FAN_ONLY diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index ad5634c1e9f..13f8ba8401f 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -2,7 +2,7 @@ "domain": "elkm1", "name": "Elk-M1 Control", "documentation": "https://www.home-assistant.io/integrations/elkm1", - "requirements": ["elkm1-lib==1.3.5"], + "requirements": ["elkm1-lib==2.0.0"], "dhcp": [{ "registered_devices": true }, { "macaddress": "00409D*" }], "codeowners": ["@gwww", "@bdraco"], "dependencies": ["network"], diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index e41d09e2e76..57f989d5cb5 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -3,12 +3,7 @@ from __future__ import annotations from typing import Any -from elkm1_lib.const import ( - SettingFormat, - ZoneLogicalStatus, - ZonePhysicalStatus, - ZoneType, -) +from elkm1_lib.const import SettingFormat, ZoneType from elkm1_lib.counters import Counter from elkm1_lib.elements import Element from elkm1_lib.elk import Elk @@ -237,25 +232,25 @@ class ElkZone(ElkSensor): def icon(self) -> str: """Icon to use in the frontend.""" zone_icons = { - ZoneType.FIRE_ALARM.value: "fire", - ZoneType.FIRE_VERIFIED.value: "fire", - ZoneType.FIRE_SUPERVISORY.value: "fire", - ZoneType.KEYFOB.value: "key", - ZoneType.NON_ALARM.value: "alarm-off", - ZoneType.MEDICAL_ALARM.value: "medical-bag", - ZoneType.POLICE_ALARM.value: "alarm-light", - ZoneType.POLICE_NO_INDICATION.value: "alarm-light", - ZoneType.KEY_MOMENTARY_ARM_DISARM.value: "power", - ZoneType.KEY_MOMENTARY_ARM_AWAY.value: "power", - ZoneType.KEY_MOMENTARY_ARM_STAY.value: "power", - ZoneType.KEY_MOMENTARY_DISARM.value: "power", - ZoneType.KEY_ON_OFF.value: "toggle-switch", - ZoneType.MUTE_AUDIBLES.value: "volume-mute", - ZoneType.POWER_SUPERVISORY.value: "power-plug", - ZoneType.TEMPERATURE.value: "thermometer-lines", - ZoneType.ANALOG_ZONE.value: "speedometer", - ZoneType.PHONE_KEY.value: "phone-classic", - ZoneType.INTERCOM_KEY.value: "deskphone", + ZoneType.FIRE_ALARM: "fire", + ZoneType.FIRE_VERIFIED: "fire", + ZoneType.FIRE_SUPERVISORY: "fire", + ZoneType.KEYFOB: "key", + ZoneType.NON_ALARM: "alarm-off", + ZoneType.MEDICAL_ALARM: "medical-bag", + ZoneType.POLICE_ALARM: "alarm-light", + ZoneType.POLICE_NO_INDICATION: "alarm-light", + ZoneType.KEY_MOMENTARY_ARM_DISARM: "power", + ZoneType.KEY_MOMENTARY_ARM_AWAY: "power", + ZoneType.KEY_MOMENTARY_ARM_STAY: "power", + ZoneType.KEY_MOMENTARY_DISARM: "power", + ZoneType.KEY_ON_OFF: "toggle-switch", + ZoneType.MUTE_AUDIBLES: "volume-mute", + ZoneType.POWER_SUPERVISORY: "power-plug", + ZoneType.TEMPERATURE: "thermometer-lines", + ZoneType.ANALOG_ZONE: "speedometer", + ZoneType.PHONE_KEY: "phone-classic", + ZoneType.INTERCOM_KEY: "deskphone", } return f"mdi:{zone_icons.get(self._element.definition, 'alarm-bell')}" @@ -263,13 +258,9 @@ class ElkZone(ElkSensor): def extra_state_attributes(self) -> dict[str, Any]: """Attributes of the sensor.""" attrs: dict[str, Any] = self.initial_attrs() - attrs["physical_status"] = ZonePhysicalStatus( - self._element.physical_status - ).name.lower() - attrs["logical_status"] = ZoneLogicalStatus( - self._element.logical_status - ).name.lower() - attrs["definition"] = ZoneType(self._element.definition).name.lower() + attrs["physical_status"] = self._element.physical_status.name.lower() + attrs["logical_status"] = self._element.logical_status.name.lower() + attrs["definition"] = self._element.definition.name.lower() attrs["area"] = self._element.area + 1 attrs["triggered_alarm"] = self._element.triggered_alarm return attrs @@ -277,27 +268,25 @@ class ElkZone(ElkSensor): @property def temperature_unit(self) -> str | None: """Return the temperature unit.""" - if self._element.definition == ZoneType.TEMPERATURE.value: + if self._element.definition == ZoneType.TEMPERATURE: return self._temperature_unit return None @property def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" - if self._element.definition == ZoneType.TEMPERATURE.value: + if self._element.definition == ZoneType.TEMPERATURE: return self._temperature_unit - if self._element.definition == ZoneType.ANALOG_ZONE.value: + if self._element.definition == ZoneType.ANALOG_ZONE: return ELECTRIC_POTENTIAL_VOLT return None def _element_changed(self, _: Element, changeset: Any) -> None: - if self._element.definition == ZoneType.TEMPERATURE.value: + if self._element.definition == ZoneType.TEMPERATURE: self._state = temperature_to_state( self._element.temperature, UNDEFINED_TEMPERATURE ) - elif self._element.definition == ZoneType.ANALOG_ZONE.value: + elif self._element.definition == ZoneType.ANALOG_ZONE: self._state = f"{self._element.voltage}" else: - self._state = pretty_const( - ZoneLogicalStatus(self._element.logical_status).name - ) + self._state = pretty_const(self._element.logical_status.name) diff --git a/requirements_all.txt b/requirements_all.txt index 4f8bf564915..c2fc83e847b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -584,7 +584,7 @@ elgato==3.0.0 eliqonline==1.2.2 # homeassistant.components.elkm1 -elkm1-lib==1.3.5 +elkm1-lib==2.0.0 # homeassistant.components.elmax elmax_api==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e274844449b..2fe34d6a33e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -418,7 +418,7 @@ dynalite_devices==0.1.46 elgato==3.0.0 # homeassistant.components.elkm1 -elkm1-lib==1.3.5 +elkm1-lib==2.0.0 # homeassistant.components.elmax elmax_api==0.0.2 From b9f7d1f54c091943348ef2bf7d0993b9e2280e94 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 11 May 2022 22:55:12 -0400 Subject: [PATCH 0398/3516] Fix zwave_js device automation bug (#71715) --- .../zwave_js/device_automation_helpers.py | 24 +++++++++++++++++++ .../components/zwave_js/device_condition.py | 6 ++--- .../components/zwave_js/device_trigger.py | 6 ++--- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zwave_js/device_automation_helpers.py b/homeassistant/components/zwave_js/device_automation_helpers.py index 906efb2c4f9..f17ddccf03c 100644 --- a/homeassistant/components/zwave_js/device_automation_helpers.py +++ b/homeassistant/components/zwave_js/device_automation_helpers.py @@ -8,6 +8,12 @@ from zwave_js_server.const import ConfigurationValueType from zwave_js_server.model.node import Node from zwave_js_server.model.value import ConfigurationValue +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr + +from .const import DOMAIN + NODE_STATUSES = ["asleep", "awake", "dead", "alive"] CONF_SUBTYPE = "subtype" @@ -41,3 +47,21 @@ def generate_config_parameter_subtype(config_value: ConfigurationValue) -> str: parameter = f"{parameter}[{hex(config_value.property_key)}]" return f"{parameter} ({config_value.property_name})" + + +@callback +def async_bypass_dynamic_config_validation(hass: HomeAssistant, device_id: str) -> bool: + """Return whether device's config entries are not loaded.""" + dev_reg = dr.async_get(hass) + if (device := dev_reg.async_get(device_id)) is None: + raise ValueError(f"Device {device_id} not found") + entry = next( + ( + config_entry + for config_entry in hass.config_entries.async_entries(DOMAIN) + if config_entry.entry_id in device.config_entries + and config_entry.state == ConfigEntryState.LOADED + ), + None, + ) + return not entry diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py index c70371d6f8a..549319d23f4 100644 --- a/homeassistant/components/zwave_js/device_condition.py +++ b/homeassistant/components/zwave_js/device_condition.py @@ -29,12 +29,12 @@ from .device_automation_helpers import ( CONF_SUBTYPE, CONF_VALUE_ID, NODE_STATUSES, + async_bypass_dynamic_config_validation, generate_config_parameter_subtype, get_config_parameter_value_schema, ) from .helpers import ( async_get_node_from_device_id, - async_is_device_config_entry_not_loaded, check_type_schema_map, get_zwave_value_from_config, remove_keys_with_empty_values, @@ -101,7 +101,7 @@ async def async_validate_condition_config( # We return early if the config entry for this device is not ready because we can't # validate the value without knowing the state of the device try: - device_config_entry_not_loaded = async_is_device_config_entry_not_loaded( + bypass_dynamic_config_validation = async_bypass_dynamic_config_validation( hass, config[CONF_DEVICE_ID] ) except ValueError as err: @@ -109,7 +109,7 @@ async def async_validate_condition_config( f"Device {config[CONF_DEVICE_ID]} not found" ) from err - if device_config_entry_not_loaded: + if bypass_dynamic_config_validation: return config if config[CONF_TYPE] == VALUE_TYPE: diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py index 1e76f9e73ec..75a9647a5ca 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -53,12 +53,12 @@ from .const import ( from .device_automation_helpers import ( CONF_SUBTYPE, NODE_STATUSES, + async_bypass_dynamic_config_validation, generate_config_parameter_subtype, ) from .helpers import ( async_get_node_from_device_id, async_get_node_status_sensor_entity_id, - async_is_device_config_entry_not_loaded, check_type_schema_map, copy_available_params, get_value_state_schema, @@ -215,7 +215,7 @@ async def async_validate_trigger_config( # We return early if the config entry for this device is not ready because we can't # validate the value without knowing the state of the device try: - device_config_entry_not_loaded = async_is_device_config_entry_not_loaded( + bypass_dynamic_config_validation = async_bypass_dynamic_config_validation( hass, config[CONF_DEVICE_ID] ) except ValueError as err: @@ -223,7 +223,7 @@ async def async_validate_trigger_config( f"Device {config[CONF_DEVICE_ID]} not found" ) from err - if device_config_entry_not_loaded: + if bypass_dynamic_config_validation: return config trigger_type = config[CONF_TYPE] From 04af9698d3783c84ba8f6684badb40b96d63b13d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 May 2022 22:28:06 -0500 Subject: [PATCH 0399/3516] Add logbook/get_events websocket endpoint (#71706) Co-authored-by: Paulus Schoutsen --- homeassistant/components/logbook/__init__.py | 94 ++++++++- tests/components/logbook/common.py | 7 +- tests/components/logbook/test_init.py | 193 ++++++++++++++++--- 3 files changed, 260 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 9cf3ad99e57..df8143d39fb 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -15,7 +15,7 @@ from sqlalchemy.engine.row import Row from sqlalchemy.orm.query import Query import voluptuous as vol -from homeassistant.components import frontend +from homeassistant.components import frontend, websocket_api from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED from homeassistant.components.history import ( Filters, @@ -23,7 +23,10 @@ from homeassistant.components.history import ( ) from homeassistant.components.http import HomeAssistantView from homeassistant.components.recorder import get_instance -from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat +from homeassistant.components.recorder.models import ( + process_datetime_to_timestamp, + process_timestamp_to_utc_isoformat, +) from homeassistant.components.recorder.util import session_scope from homeassistant.components.script import EVENT_SCRIPT_STARTED from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN @@ -102,6 +105,10 @@ LOG_MESSAGE_SCHEMA = vol.Schema( ) +LOGBOOK_FILTERS = "logbook_filters" +LOGBOOK_ENTITIES_FILTER = "entities_filter" + + @bind_hass def log_entry( hass: HomeAssistant, @@ -168,7 +175,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: filters = None entities_filter = None + hass.data[LOGBOOK_FILTERS] = filters + hass.data[LOGBOOK_ENTITIES_FILTER] = entities_filter + hass.http.register_view(LogbookView(conf, filters, entities_filter)) + websocket_api.async_register_command(hass, ws_get_events) hass.services.async_register(DOMAIN, "log", log_message, schema=LOG_MESSAGE_SCHEMA) @@ -194,6 +205,61 @@ async def _process_logbook_platform( platform.async_describe_events(hass, _async_describe_event) +@websocket_api.websocket_command( + { + vol.Required("type"): "logbook/get_events", + vol.Required("start_time"): str, + vol.Optional("end_time"): str, + vol.Optional("entity_ids"): [str], + vol.Optional("context_id"): str, + } +) +@websocket_api.async_response +async def ws_get_events( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Handle logbook get events websocket command.""" + start_time_str = msg["start_time"] + end_time_str = msg.get("end_time") + utc_now = dt_util.utcnow() + + if start_time := dt_util.parse_datetime(start_time_str): + start_time = dt_util.as_utc(start_time) + else: + connection.send_error(msg["id"], "invalid_start_time", "Invalid start_time") + return + + if not end_time_str: + end_time = utc_now + elif parsed_end_time := dt_util.parse_datetime(end_time_str): + end_time = dt_util.as_utc(parsed_end_time) + else: + connection.send_error(msg["id"], "invalid_end_time", "Invalid end_time") + return + + if start_time > utc_now: + connection.send_result(msg["id"], {}) + return + + entity_ids = msg.get("entity_ids") + context_id = msg.get("context_id") + + logbook_events: list[dict[str, Any]] = await get_instance( + hass + ).async_add_executor_job( + _get_events, + hass, + start_time, + end_time, + entity_ids, + hass.data[LOGBOOK_FILTERS], + hass.data[LOGBOOK_ENTITIES_FILTER], + context_id, + True, + ) + connection.send_result(msg["id"], logbook_events) + + class LogbookView(HomeAssistantView): """Handle logbook view requests.""" @@ -267,6 +333,7 @@ class LogbookView(HomeAssistantView): self.filters, self.entities_filter, context_id, + False, ) ) @@ -281,6 +348,7 @@ def _humanify( entity_name_cache: EntityNameCache, event_cache: EventCache, context_augmenter: ContextAugmenter, + format_time: Callable[[Row], Any], ) -> Generator[dict[str, Any], None, None]: """Generate a converted list of events into Entry objects. @@ -307,7 +375,7 @@ def _humanify( continue data = { - "when": _row_time_fired_isoformat(row), + "when": format_time(row), "name": entity_name_cache.get(entity_id, row), "state": row.state, "entity_id": entity_id, @@ -321,21 +389,21 @@ def _humanify( elif event_type in external_events: domain, describe_event = external_events[event_type] data = describe_event(event_cache.get(row)) - data["when"] = _row_time_fired_isoformat(row) + data["when"] = format_time(row) data["domain"] = domain context_augmenter.augment(data, data.get(ATTR_ENTITY_ID), row) yield data elif event_type == EVENT_HOMEASSISTANT_START: yield { - "when": _row_time_fired_isoformat(row), + "when": format_time(row), "name": "Home Assistant", "message": "started", "domain": HA_DOMAIN, } elif event_type == EVENT_HOMEASSISTANT_STOP: yield { - "when": _row_time_fired_isoformat(row), + "when": format_time(row), "name": "Home Assistant", "message": "stopped", "domain": HA_DOMAIN, @@ -351,7 +419,7 @@ def _humanify( domain = split_entity_id(str(entity_id))[0] data = { - "when": _row_time_fired_isoformat(row), + "when": format_time(row), "name": event_data.get(ATTR_NAME), "message": event_data.get(ATTR_MESSAGE), "domain": domain, @@ -369,6 +437,7 @@ def _get_events( filters: Filters | None = None, entities_filter: EntityFilter | Callable[[str], bool] | None = None, context_id: str | None = None, + timestamp: bool = False, ) -> list[dict[str, Any]]: """Get events for a period of time.""" assert not ( @@ -386,6 +455,7 @@ def _get_events( context_lookup, entity_name_cache, external_events, event_cache ) event_types = (*ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, *external_events) + format_time = _row_time_fired_timestamp if timestamp else _row_time_fired_isoformat def yield_rows(query: Query) -> Generator[Row, None, None]: """Yield Events that are not filtered away.""" @@ -424,6 +494,7 @@ def _get_events( entity_name_cache, event_cache, context_augmenter, + format_time, ) ) @@ -575,9 +646,14 @@ def _row_attributes_extract(row: Row, extractor: re.Pattern) -> str | None: return result.group(1) if result else None -def _row_time_fired_isoformat(row: Row) -> dt | None: +def _row_time_fired_isoformat(row: Row) -> str: """Convert the row timed_fired to isoformat.""" - return process_timestamp_to_utc_isoformat(row.time_fired) or dt_util.utcnow() + return process_timestamp_to_utc_isoformat(row.time_fired or dt_util.utcnow()) + + +def _row_time_fired_timestamp(row: Row) -> float: + """Convert the row timed_fired to timestamp.""" + return process_datetime_to_timestamp(row.time_fired or dt_util.utcnow()) class LazyEventPartialState: diff --git a/tests/components/logbook/common.py b/tests/components/logbook/common.py index 896add3104e..32273f04c05 100644 --- a/tests/components/logbook/common.py +++ b/tests/components/logbook/common.py @@ -53,6 +53,11 @@ def mock_humanify(hass_, rows): ) return list( logbook._humanify( - hass_, rows, entity_name_cache, event_cache, context_augmenter + hass_, + rows, + entity_name_cache, + event_cache, + context_augmenter, + logbook._row_time_fired_isoformat, ), ) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index cc10346fc07..d382506a3db 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -4,7 +4,6 @@ import collections from datetime import datetime, timedelta from http import HTTPStatus import json -from typing import Any from unittest.mock import Mock, patch import pytest @@ -13,7 +12,6 @@ import voluptuous as vol from homeassistant.components import logbook from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED -from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat from homeassistant.components.script import EVENT_SCRIPT_STARTED from homeassistant.components.sensor import SensorStateClass from homeassistant.const import ( @@ -42,7 +40,7 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .common import mock_humanify +from .common import MockRow, mock_humanify from tests.common import async_capture_events, mock_platform from tests.components.recorder.common import ( @@ -2140,27 +2138,174 @@ def _assert_entry( assert state == entry["state"] -class MockRow: - """Minimal row mock.""" +async def test_get_events(hass, hass_ws_client, recorder_mock): + """Test logbook get_events.""" + now = dt_util.utcnow() + await async_setup_component(hass, "logbook", {}) + await async_recorder_block_till_done(hass) - def __init__(self, event_type: str, data: dict[str, Any] = None): - """Init the fake row.""" - self.event_type = event_type - self.shared_data = json.dumps(data, cls=JSONEncoder) - self.data = data - self.time_fired = dt_util.utcnow() - self.context_parent_id = None - self.context_user_id = None - self.context_id = None - self.state = None - self.entity_id = None + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - @property - def time_fired_minute(self): - """Minute the event was fired.""" - return self.time_fired.minute + hass.states.async_set("light.kitchen", STATE_OFF) + await hass.async_block_till_done() + hass.states.async_set("light.kitchen", STATE_ON, {"brightness": 100}) + await hass.async_block_till_done() + hass.states.async_set("light.kitchen", STATE_ON, {"brightness": 200}) + await hass.async_block_till_done() + hass.states.async_set("light.kitchen", STATE_ON, {"brightness": 300}) + await hass.async_block_till_done() + hass.states.async_set("light.kitchen", STATE_ON, {"brightness": 400}) + await hass.async_block_till_done() + context = ha.Context( + id="ac5bd62de45711eaaeb351041eec8dd9", + user_id="b400facee45711eaa9308bfd3d19e474", + ) - @property - def time_fired_isoformat(self): - """Time event was fired in utc isoformat.""" - return process_timestamp_to_utc_isoformat(self.time_fired) + hass.states.async_set("light.kitchen", STATE_OFF, context=context) + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "logbook/get_events", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "entity_ids": ["light.kitchen"], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [] + + await client.send_json( + { + "id": 2, + "type": "logbook/get_events", + "start_time": now.isoformat(), + "entity_ids": ["sensor.test"], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 2 + assert response["result"] == [] + + await client.send_json( + { + "id": 3, + "type": "logbook/get_events", + "start_time": now.isoformat(), + "entity_ids": ["light.kitchen"], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 3 + + results = response["result"] + assert results[0]["entity_id"] == "light.kitchen" + assert results[0]["state"] == "on" + assert results[1]["entity_id"] == "light.kitchen" + assert results[1]["state"] == "off" + + await client.send_json( + { + "id": 4, + "type": "logbook/get_events", + "start_time": now.isoformat(), + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 4 + + results = response["result"] + assert len(results) == 3 + assert results[0]["message"] == "started" + assert results[1]["entity_id"] == "light.kitchen" + assert results[1]["state"] == "on" + assert isinstance(results[1]["when"], float) + assert results[2]["entity_id"] == "light.kitchen" + assert results[2]["state"] == "off" + assert isinstance(results[2]["when"], float) + + await client.send_json( + { + "id": 5, + "type": "logbook/get_events", + "start_time": now.isoformat(), + "context_id": "ac5bd62de45711eaaeb351041eec8dd9", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 5 + + results = response["result"] + assert len(results) == 1 + assert results[0]["entity_id"] == "light.kitchen" + assert results[0]["state"] == "off" + assert isinstance(results[0]["when"], float) + + +async def test_get_events_future_start_time(hass, hass_ws_client, recorder_mock): + """Test get_events with a future start time.""" + await async_setup_component(hass, "logbook", {}) + await async_recorder_block_till_done(hass) + future = dt_util.utcnow() + timedelta(hours=10) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "logbook/get_events", + "start_time": future.isoformat(), + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + + results = response["result"] + assert len(results) == 0 + + +async def test_get_events_bad_start_time(hass, hass_ws_client, recorder_mock): + """Test get_events bad start time.""" + await async_setup_component(hass, "logbook", {}) + await async_recorder_block_till_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "logbook/get_events", + "start_time": "cats", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_start_time" + + +async def test_get_events_bad_end_time(hass, hass_ws_client, recorder_mock): + """Test get_events bad end time.""" + now = dt_util.utcnow() + await async_setup_component(hass, "logbook", {}) + await async_recorder_block_till_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "logbook/get_events", + "start_time": now.isoformat(), + "end_time": "dogs", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_end_time" From 1dc15bb7c806c496ba6e848b31818d7b58fd900b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 May 2022 22:44:35 -0500 Subject: [PATCH 0400/3516] Prevent history_stats from rejecting states when microseconds differ (#71704) --- .../components/history_stats/data.py | 6 +++- tests/components/history_stats/test_sensor.py | 36 +++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 3f22f4cc32b..3d21cca6b6d 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -96,7 +96,11 @@ class HistoryStats: new_data = False if event and event.data["new_state"] is not None: new_state: State = event.data["new_state"] - if current_period_start <= new_state.last_changed <= current_period_end: + if ( + current_period_start_timestamp + <= floored_timestamp(new_state.last_changed) + <= current_period_end_timestamp + ): self._history_current_period.append(new_state) new_data = True if not new_data and current_period_end_timestamp < now_timestamp: diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 297e3a5a363..4f56edaa291 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -1389,9 +1389,10 @@ async def test_measure_cet(hass, recorder_mock): assert hass.states.get("sensor.sensor4").state == "83.3" -async def test_end_time_with_microseconds_zeroed(hass, recorder_mock): +@pytest.mark.parametrize("time_zone", ["Europe/Berlin", "America/Chicago", "US/Hawaii"]) +async def test_end_time_with_microseconds_zeroed(time_zone, hass, recorder_mock): """Test the history statistics sensor that has the end time microseconds zeroed out.""" - hass.config.set_time_zone("Europe/Berlin") + hass.config.set_time_zone(time_zone) start_of_today = dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) start_time = start_of_today + timedelta(minutes=60) t0 = start_time + timedelta(minutes=20) @@ -1459,6 +1460,7 @@ async def test_end_time_with_microseconds_zeroed(hass, recorder_mock): async_fire_time_changed(hass, time_600) await hass.async_block_till_done() assert hass.states.get("sensor.heatpump_compressor_today").state == "3.83" + rolled_to_next_day = start_of_today + timedelta(days=1) assert rolled_to_next_day.hour == 0 assert rolled_to_next_day.minute == 0 @@ -1469,3 +1471,33 @@ async def test_end_time_with_microseconds_zeroed(hass, recorder_mock): async_fire_time_changed(hass, rolled_to_next_day) await hass.async_block_till_done() assert hass.states.get("sensor.heatpump_compressor_today").state == "0.0" + + rolled_to_next_day_plus_12 = start_of_today + timedelta( + days=1, hours=12, microseconds=0 + ) + with freeze_time(rolled_to_next_day_plus_12): + async_fire_time_changed(hass, rolled_to_next_day_plus_12) + await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "12.0" + + rolled_to_next_day_plus_14 = start_of_today + timedelta( + days=1, hours=14, microseconds=0 + ) + with freeze_time(rolled_to_next_day_plus_14): + async_fire_time_changed(hass, rolled_to_next_day_plus_14) + await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "14.0" + + rolled_to_next_day_plus_16_860000 = start_of_today + timedelta( + days=1, hours=16, microseconds=860000 + ) + with freeze_time(rolled_to_next_day_plus_16_860000): + hass.states.async_set("binary_sensor.heatpump_compressor_state", "off") + async_fire_time_changed(hass, rolled_to_next_day_plus_16_860000) + await hass.async_block_till_done() + + rolled_to_next_day_plus_18 = start_of_today + timedelta(days=1, hours=18) + with freeze_time(rolled_to_next_day_plus_18): + async_fire_time_changed(hass, rolled_to_next_day_plus_18) + await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "16.0" From 9cd81db5b31165130ec99227230fd9cd753e608f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 May 2022 22:45:16 -0500 Subject: [PATCH 0401/3516] Add device_id and logbook descriptions to lutron_caseta (#71713) --- .../components/lutron_caseta/__init__.py | 5 +- .../components/lutron_caseta/logbook.py | 42 +++++++++++ .../components/lutron_caseta/test_logbook.py | 73 +++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/lutron_caseta/logbook.py create mode 100644 tests/components/lutron_caseta/test_logbook.py diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 3d9e07519a8..e9ada82bb54 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -12,7 +12,7 @@ from pylutron_caseta.smartbridge import Smartbridge import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import ATTR_SUGGESTED_AREA, CONF_HOST, Platform +from homeassistant.const import ATTR_DEVICE_ID, ATTR_SUGGESTED_AREA, CONF_HOST, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -229,6 +229,7 @@ def _async_subscribe_pico_remote_events( button_devices_by_id: dict[int, dict], ): """Subscribe to lutron events.""" + dev_reg = dr.async_get(hass) @callback def _async_button_event(button_id, event_type): @@ -257,6 +258,7 @@ def _async_subscribe_pico_remote_events( ) return lip_button_number = sub_type_to_lip_button[sub_type] + hass_device = dev_reg.async_get_device({(DOMAIN, device["serial"])}) hass.bus.async_fire( LUTRON_CASETA_BUTTON_EVENT, @@ -266,6 +268,7 @@ def _async_subscribe_pico_remote_events( ATTR_BUTTON_NUMBER: lip_button_number, ATTR_LEAP_BUTTON_NUMBER: button_number, ATTR_DEVICE_NAME: name, + ATTR_DEVICE_ID: hass_device.id, ATTR_AREA_NAME: area, ATTR_ACTION: action, }, diff --git a/homeassistant/components/lutron_caseta/logbook.py b/homeassistant/components/lutron_caseta/logbook.py new file mode 100644 index 00000000000..28342090a21 --- /dev/null +++ b/homeassistant/components/lutron_caseta/logbook.py @@ -0,0 +1,42 @@ +"""Describe lutron_caseta logbook events.""" +from __future__ import annotations + +from collections.abc import Callable + +from homeassistant.core import Event, HomeAssistant, callback + +from .const import ( + ATTR_ACTION, + ATTR_AREA_NAME, + ATTR_DEVICE_NAME, + ATTR_LEAP_BUTTON_NUMBER, + ATTR_TYPE, + DOMAIN, + LUTRON_CASETA_BUTTON_EVENT, +) +from .device_trigger import LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP + + +@callback +def async_describe_events( + hass: HomeAssistant, + async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None], +) -> None: + """Describe logbook events.""" + + @callback + def async_describe_button_event(event: Event) -> dict[str, str]: + """Describe lutron_caseta_button_event logbook event.""" + data = event.data + device_type = data[ATTR_TYPE] + leap_button_number = data[ATTR_LEAP_BUTTON_NUMBER] + button_map = LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP[device_type] + button_description = button_map[leap_button_number] + return { + "name": f"{data[ATTR_AREA_NAME]} {data[ATTR_DEVICE_NAME]}", + "message": f"{data[ATTR_ACTION]} {button_description}", + } + + async_describe_event( + DOMAIN, LUTRON_CASETA_BUTTON_EVENT, async_describe_button_event + ) diff --git a/tests/components/lutron_caseta/test_logbook.py b/tests/components/lutron_caseta/test_logbook.py new file mode 100644 index 00000000000..3a202eadf58 --- /dev/null +++ b/tests/components/lutron_caseta/test_logbook.py @@ -0,0 +1,73 @@ +"""The tests for lutron caseta logbook.""" +from unittest.mock import patch + +from homeassistant.components.lutron_caseta.const import ( + ATTR_ACTION, + ATTR_AREA_NAME, + ATTR_BUTTON_NUMBER, + ATTR_DEVICE_NAME, + ATTR_LEAP_BUTTON_NUMBER, + ATTR_SERIAL, + ATTR_TYPE, + CONF_CA_CERTS, + CONF_CERTFILE, + CONF_KEYFILE, + DOMAIN, + LUTRON_CASETA_BUTTON_EVENT, +) +from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST +from homeassistant.setup import async_setup_component + +from . import MockBridge + +from tests.common import MockConfigEntry +from tests.components.logbook.common import MockRow, mock_humanify + + +async def test_humanify_lutron_caseta_button_event(hass): + """Test humanifying lutron_caseta_button_events.""" + hass.config.components.add("recorder") + assert await async_setup_component(hass, "logbook", {}) + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_KEYFILE: "", + CONF_CERTFILE: "", + CONF_CA_CERTS: "", + }, + unique_id="abc", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.lutron_caseta.Smartbridge.create_tls", + return_value=MockBridge(can_connect=True), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + await hass.async_block_till_done() + + (event1,) = mock_humanify( + hass, + [ + MockRow( + LUTRON_CASETA_BUTTON_EVENT, + { + ATTR_SERIAL: "123", + ATTR_DEVICE_ID: "1234", + ATTR_TYPE: "Pico3ButtonRaiseLower", + ATTR_LEAP_BUTTON_NUMBER: 3, + ATTR_BUTTON_NUMBER: 3, + ATTR_DEVICE_NAME: "Pico", + ATTR_AREA_NAME: "Living Room", + ATTR_ACTION: "press", + }, + ), + ], + ) + + assert event1["name"] == "Living Room Pico" + assert event1["domain"] == DOMAIN + assert event1["message"] == "press raise" From 18bdc70185cd9751a0a2bdf83729ea0c717897c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 May 2022 22:45:47 -0500 Subject: [PATCH 0402/3516] Update sql to prepare for sqlalchemy 2.0 (#71532) * Update sql to prepare for sqlalchemy 2.0 * config flow as well --- homeassistant/components/sql/config_flow.py | 6 +++--- homeassistant/components/sql/sensor.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sql/config_flow.py b/homeassistant/components/sql/config_flow.py index ba9a5f7e4dd..9a6013e1d62 100644 --- a/homeassistant/components/sql/config_flow.py +++ b/homeassistant/components/sql/config_flow.py @@ -45,15 +45,15 @@ def validate_sql_select(value: str) -> str | None: def validate_query(db_url: str, query: str, column: str) -> bool: """Validate SQL query.""" try: - engine = sqlalchemy.create_engine(db_url) - sessmaker = scoped_session(sessionmaker(bind=engine)) + engine = sqlalchemy.create_engine(db_url, future=True) + sessmaker = scoped_session(sessionmaker(bind=engine, future=True)) except SQLAlchemyError as error: raise error sess: scoped_session = sessmaker() try: - result: Result = sess.execute(query) + result: Result = sess.execute(sqlalchemy.text(query)) for res in result.mappings(): data = res[column] _LOGGER.debug("Return value from query: %s", data) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 5e748bed55e..4e3f9d15846 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -111,8 +111,8 @@ async def async_setup_entry( value_template.hass = hass try: - engine = sqlalchemy.create_engine(db_url) - sessmaker = scoped_session(sessionmaker(bind=engine)) + engine = sqlalchemy.create_engine(db_url, future=True) + sessmaker = scoped_session(sessionmaker(bind=engine, future=True)) except SQLAlchemyError as err: _LOGGER.error("Can not open database %s", {redact_credentials(str(err))}) return @@ -179,7 +179,7 @@ class SQLSensor(SensorEntity): self._attr_extra_state_attributes = {} sess: scoped_session = self.sessionmaker() try: - result = sess.execute(self._query) + result = sess.execute(sqlalchemy.text(self._query)) except SQLAlchemyError as err: _LOGGER.error( "Error executing query %s: %s", From 684fe242d9c2afdd0b5d21d7b459f86b300a9ee6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 11 May 2022 23:51:10 -0400 Subject: [PATCH 0403/3516] Set PARALLEL_UPDATES to 0 for all zwave_js platforms (#71626) --- homeassistant/components/zwave_js/binary_sensor.py | 2 ++ homeassistant/components/zwave_js/button.py | 2 ++ homeassistant/components/zwave_js/climate.py | 2 ++ homeassistant/components/zwave_js/cover.py | 2 ++ homeassistant/components/zwave_js/fan.py | 2 ++ homeassistant/components/zwave_js/humidifier.py | 2 ++ homeassistant/components/zwave_js/light.py | 2 ++ homeassistant/components/zwave_js/lock.py | 2 ++ homeassistant/components/zwave_js/number.py | 2 ++ homeassistant/components/zwave_js/select.py | 2 ++ homeassistant/components/zwave_js/sensor.py | 2 ++ homeassistant/components/zwave_js/siren.py | 2 ++ homeassistant/components/zwave_js/switch.py | 2 ++ 13 files changed, 26 insertions(+) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 88ab7221600..8a86898239a 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -27,6 +27,8 @@ from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity +PARALLEL_UPDATES = 0 + LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave_js/button.py b/homeassistant/components/zwave_js/button.py index ea987b56258..49c2a76cccf 100644 --- a/homeassistant/components/zwave_js/button.py +++ b/homeassistant/components/zwave_js/button.py @@ -14,6 +14,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DOMAIN, LOGGER from .helpers import get_device_id, get_valueless_base_unique_id +PARALLEL_UPDATES = 0 + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 188e627bb5e..67def698fc2 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -52,6 +52,8 @@ from .discovery_data_template import DynamicCurrentTempClimateDataTemplate from .entity import ZWaveBaseEntity from .helpers import get_value_of_zwave_value +PARALLEL_UPDATES = 0 + # Map Z-Wave HVAC Mode to Home Assistant value # Note: We treat "auto" as "heat_cool" as most Z-Wave devices # report auto_changeover as auto without schedule support. diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 9281f0bc21c..c7ba50ee7e7 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -35,6 +35,8 @@ from .discovery import ZwaveDiscoveryInfo from .discovery_data_template import CoverTiltDataTemplate from .entity import ZWaveBaseEntity +PARALLEL_UPDATES = 0 + LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index 623ee072f3a..f571884ac80 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -35,6 +35,8 @@ from .discovery_data_template import FanValueMapping, FanValueMappingDataTemplat from .entity import ZWaveBaseEntity from .helpers import get_value_of_zwave_value +PARALLEL_UPDATES = 0 + DEFAULT_SPEED_RANGE = (1, 99) # off is not included ATTR_FAN_STATE = "fan_state" diff --git a/homeassistant/components/zwave_js/humidifier.py b/homeassistant/components/zwave_js/humidifier.py index 44d7bc19bbb..8cf0b6aec7c 100644 --- a/homeassistant/components/zwave_js/humidifier.py +++ b/homeassistant/components/zwave_js/humidifier.py @@ -32,6 +32,8 @@ from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity +PARALLEL_UPDATES = 0 + @dataclass class ZwaveHumidifierEntityDescriptionRequiredKeys: diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index ca51bc83986..534a86f1c86 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -45,6 +45,8 @@ from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity +PARALLEL_UPDATES = 0 + LOGGER = logging.getLogger(__name__) MULTI_COLOR_MAP = { diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py index d70b6ef2009..3781821e4c7 100644 --- a/homeassistant/components/zwave_js/lock.py +++ b/homeassistant/components/zwave_js/lock.py @@ -34,6 +34,8 @@ from .const import ( from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity +PARALLEL_UPDATES = 0 + LOGGER = logging.getLogger(__name__) STATE_TO_ZWAVE_MAP: dict[int, dict[str, int | bool]] = { diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py index 16434b51108..fb9266ac071 100644 --- a/homeassistant/components/zwave_js/number.py +++ b/homeassistant/components/zwave_js/number.py @@ -14,6 +14,8 @@ from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity +PARALLEL_UPDATES = 0 + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/zwave_js/select.py b/homeassistant/components/zwave_js/select.py index c6bc11a4804..085c694fc0e 100644 --- a/homeassistant/components/zwave_js/select.py +++ b/homeassistant/components/zwave_js/select.py @@ -18,6 +18,8 @@ from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity +PARALLEL_UPDATES = 0 + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index e60a0793608..71e6bc48952 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -63,6 +63,8 @@ from .discovery_data_template import ( from .entity import ZWaveBaseEntity from .helpers import get_device_id, get_valueless_base_unique_id +PARALLEL_UPDATES = 0 + LOGGER = logging.getLogger(__name__) STATUS_ICON: dict[NodeStatus, str] = { diff --git a/homeassistant/components/zwave_js/siren.py b/homeassistant/components/zwave_js/siren.py index e7443f33dae..e686c5446ca 100644 --- a/homeassistant/components/zwave_js/siren.py +++ b/homeassistant/components/zwave_js/siren.py @@ -21,6 +21,8 @@ from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity +PARALLEL_UPDATES = 0 + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py index a680a8fb04a..115f90b8e11 100644 --- a/homeassistant/components/zwave_js/switch.py +++ b/homeassistant/components/zwave_js/switch.py @@ -20,6 +20,8 @@ from .const import DATA_CLIENT, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity +PARALLEL_UPDATES = 0 + LOGGER = logging.getLogger(__name__) From ca52aa47aaf1af3299c18dd69a24839266938e06 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 12 May 2022 03:05:35 -0400 Subject: [PATCH 0404/3516] Enable sentry reporting for zwave_js (#71719) * Enable sentry reporting for zwave_js * Fix test --- homeassistant/components/zwave_js/helpers.py | 1 + tests/components/zwave_js/test_api.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 17515f9aa99..c4eb0396287 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -95,6 +95,7 @@ def get_value_of_zwave_value(value: ZwaveValue | None) -> Any | None: async def async_enable_statistics(client: ZwaveClient) -> None: """Enable statistics on the driver.""" await client.driver.async_enable_statistics("Home Assistant", HA_VERSION) + await client.driver.async_enable_error_reporting() @callback diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 7ec7d98217b..f4ffa0876d9 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -3172,10 +3172,12 @@ async def test_data_collection(hass, client, integration, hass_ws_client): result = msg["result"] assert result is None - assert len(client.async_send_command.call_args_list) == 1 - args = client.async_send_command.call_args[0][0] + assert len(client.async_send_command.call_args_list) == 2 + args = client.async_send_command.call_args_list[0][0][0] assert args["command"] == "driver.enable_statistics" assert args["applicationName"] == "Home Assistant" + args = client.async_send_command.call_args_list[1][0][0] + assert args["command"] == "driver.enable_error_reporting" assert entry.data[CONF_DATA_COLLECTION_OPTED_IN] client.async_send_command.reset_mock() From 0d69adb4046d6487dabe07277b93e7af81205cfc Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 12 May 2022 03:07:11 -0400 Subject: [PATCH 0405/3516] Send initial message for certain zwave_js ws subscriptions (#71723) * Send initial message for certain zwave_js ws subscriptions * Be consistent * fix tests and bugs --- homeassistant/components/zwave_js/api.py | 39 ++++++++++++++++++++---- tests/components/zwave_js/test_api.py | 24 +++++++++++++-- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 5608679fe90..7d1c4622d50 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -1848,9 +1848,17 @@ async def websocket_subscribe_firmware_update_status( connection.subscriptions[msg["id"]] = async_cleanup progress = node.firmware_update_progress - connection.send_result( - msg[ID], _get_firmware_update_progress_dict(progress) if progress else None - ) + connection.send_result(msg[ID]) + if progress: + connection.send_message( + websocket_api.event_message( + msg[ID], + { + "event": "firmware update progress", + **_get_firmware_update_progress_dict(progress), + }, + ) + ) class FirmwareUploadView(HomeAssistantView): @@ -2011,8 +2019,16 @@ async def websocket_subscribe_controller_statistics( ] connection.subscriptions[msg["id"]] = async_cleanup - connection.send_result( - msg[ID], _get_controller_statistics_dict(controller.statistics) + connection.send_result(msg[ID]) + connection.send_message( + websocket_api.event_message( + msg[ID], + { + "event": "statistics updated", + "source": "controller", + **_get_controller_statistics_dict(controller.statistics), + }, + ) ) @@ -2069,7 +2085,18 @@ async def websocket_subscribe_node_statistics( msg[DATA_UNSUBSCRIBE] = unsubs = [node.on("statistics updated", forward_stats)] connection.subscriptions[msg["id"]] = async_cleanup - connection.send_result(msg[ID], _get_node_statistics_dict(node.statistics)) + connection.send_result(msg[ID]) + connection.send_message( + websocket_api.event_message( + msg[ID], + { + "event": "statistics updated", + "source": "node", + "nodeId": node.node_id, + **_get_node_statistics_dict(node.statistics), + }, + ) + ) @websocket_api.require_admin diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index f4ffa0876d9..d8a36fc6509 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -3479,7 +3479,14 @@ async def test_subscribe_firmware_update_status_initial_value( msg = await ws_client.receive_json() assert msg["success"] - assert msg["result"] == {"sent_fragments": 1, "total_fragments": 10} + assert msg["result"] is None + + msg = await ws_client.receive_json() + assert msg["event"] == { + "event": "firmware update progress", + "sent_fragments": 1, + "total_fragments": 10, + } async def test_subscribe_firmware_update_status_failures( @@ -3688,7 +3695,12 @@ async def test_subscribe_controller_statistics( msg = await ws_client.receive_json() assert msg["success"] - assert msg["result"] == { + assert msg["result"] is None + + msg = await ws_client.receive_json() + assert msg["event"] == { + "event": "statistics updated", + "source": "controller", "messages_tx": 0, "messages_rx": 0, "messages_dropped_tx": 0, @@ -3783,7 +3795,13 @@ async def test_subscribe_node_statistics( msg = await ws_client.receive_json() assert msg["success"] - assert msg["result"] == { + assert msg["result"] is None + + msg = await ws_client.receive_json() + assert msg["event"] == { + "source": "node", + "event": "statistics updated", + "nodeId": multisensor_6.node_id, "commands_tx": 0, "commands_rx": 0, "commands_dropped_tx": 0, From 533257021cddf56312092cff3ff71947c7086f77 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 12 May 2022 03:07:58 -0400 Subject: [PATCH 0406/3516] Parallelize zwave_js service calls (#71662) * Parallelize zwave_js service calls to speed them up and handle exceptions properly * Fix bug * Add tests * Fix comments * Additional comment fixes Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/services.py | 149 ++++++++++----- tests/components/zwave_js/test_services.py | 169 +++++++++++++++++- 2 files changed, 274 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 8577e48e7a3..af8f3c4c4e7 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -2,13 +2,14 @@ from __future__ import annotations import asyncio +from collections.abc import Generator import logging -from typing import Any, cast +from typing import Any import voluptuous as vol from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import CommandClass, CommandStatus -from zwave_js_server.exceptions import FailedCommand, SetValueFailed +from zwave_js_server.exceptions import SetValueFailed from zwave_js_server.model.endpoint import Endpoint from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import get_value_id @@ -38,6 +39,12 @@ from .helpers import ( _LOGGER = logging.getLogger(__name__) +SET_VALUE_FAILED_EXC = SetValueFailed( + "Unable to set value, refer to " + "https://zwave-js.github.io/node-zwave-js/#/api/node?id=setvalue for " + "possible reasons" +) + def parameter_name_does_not_need_bitmask( val: dict[str, int | str | list[str]] @@ -64,6 +71,33 @@ def broadcast_command(val: dict[str, Any]) -> dict[str, Any]: ) +def get_valid_responses_from_results( + zwave_objects: set[ZwaveNode | Endpoint], results: tuple[Any, ...] +) -> Generator[tuple[ZwaveNode | Endpoint, Any], None, None]: + """Return valid responses from a list of results.""" + for zwave_object, result in zip(zwave_objects, results): + if not isinstance(result, Exception): + yield zwave_object, result + + +def raise_exceptions_from_results( + zwave_objects: set[ZwaveNode | Endpoint] | tuple[ZwaveNode | str, ...], + results: tuple[Any, ...], +) -> None: + """Raise list of exceptions from a list of results.""" + if errors := [ + tup for tup in zip(zwave_objects, results) if isinstance(tup[1], Exception) + ]: + lines = ( + f"{len(errors)} error(s):", + *( + f"{zwave_object} - {error.__class__.__name__}: {error.args[0]}" + for zwave_object, error in errors + ), + ) + raise HomeAssistantError("\n".join(lines)) + + class ZWaveServices: """Class that holds our services (Zwave Commands) that should be published to hass.""" @@ -371,14 +405,21 @@ class ZWaveServices: property_key = service.data.get(const.ATTR_CONFIG_PARAMETER_BITMASK) new_value = service.data[const.ATTR_CONFIG_VALUE] - for node in nodes: - zwave_value, cmd_status = await async_set_config_parameter( - node, - new_value, - property_or_property_name, - property_key=property_key, - ) - + results = await asyncio.gather( + *( + async_set_config_parameter( + node, + new_value, + property_or_property_name, + property_key=property_key, + ) + for node in nodes + ), + return_exceptions=True, + ) + for node, result in get_valid_responses_from_results(nodes, results): + zwave_value = result[0] + cmd_status = result[1] if cmd_status == CommandStatus.ACCEPTED: msg = "Set configuration parameter %s on Node %s with value %s" else: @@ -386,8 +427,8 @@ class ZWaveServices: "Added command to queue to set configuration parameter %s on Node " "%s with value %s. Parameter will be set when the device wakes up" ) - _LOGGER.info(msg, zwave_value, node, new_value) + raise_exceptions_from_results(nodes, results) async def async_bulk_set_partial_config_parameters( self, service: ServiceCall @@ -397,23 +438,31 @@ class ZWaveServices: property_ = service.data[const.ATTR_CONFIG_PARAMETER] new_value = service.data[const.ATTR_CONFIG_VALUE] - for node in nodes: - cmd_status = await async_bulk_set_partial_config_parameters( - node, - property_, - new_value, - ) + results = await asyncio.gather( + *( + async_bulk_set_partial_config_parameters( + node, + property_, + new_value, + ) + for node in nodes + ), + return_exceptions=True, + ) + for node, cmd_status in get_valid_responses_from_results(nodes, results): if cmd_status == CommandStatus.ACCEPTED: msg = "Bulk set partials for configuration parameter %s on Node %s" else: msg = ( - "Added command to queue to bulk set partials for configuration " - "parameter %s on Node %s" + "Queued command to bulk set partials for configuration parameter " + "%s on Node %s" ) _LOGGER.info(msg, property_, node) + raise_exceptions_from_results(nodes, results) + async def async_poll_value(self, service: ServiceCall) -> None: """Poll value on a node.""" for entity_id in service.data[ATTR_ENTITY_ID]: @@ -436,6 +485,7 @@ class ZWaveServices: wait_for_result = service.data.get(const.ATTR_WAIT_FOR_RESULT) options = service.data.get(const.ATTR_OPTIONS) + coros = [] for node in nodes: value_id = get_value_id( node, @@ -455,19 +505,29 @@ class ZWaveServices: new_value_ = str(new_value) else: new_value_ = new_value - success = await node.async_set_value( - value_id, - new_value_, - options=options, - wait_for_result=wait_for_result, + coros.append( + node.async_set_value( + value_id, + new_value_, + options=options, + wait_for_result=wait_for_result, + ) ) + results = await asyncio.gather(*coros, return_exceptions=True) + # multiple set_values my fail so we will track the entire list + set_value_failed_nodes_list = [] + for node, success in get_valid_responses_from_results(nodes, results): if success is False: - raise HomeAssistantError( - "Unable to set value, refer to " - "https://zwave-js.github.io/node-zwave-js/#/api/node?id=setvalue " - "for possible reasons" - ) from SetValueFailed + # If we failed to set a value, add node to SetValueFailed exception list + set_value_failed_nodes_list.append(node) + + # Add the SetValueFailed exception to the results and the nodes to the node + # list. No-op if there are no SetValueFailed exceptions + raise_exceptions_from_results( + (*nodes, *set_value_failed_nodes_list), + (*results, *([SET_VALUE_FAILED_EXC] * len(set_value_failed_nodes_list))), + ) async def async_multicast_set_value(self, service: ServiceCall) -> None: """Set a value via multicast to multiple nodes.""" @@ -556,24 +616,29 @@ class ZWaveServices: async def _async_invoke_cc_api(endpoints: set[Endpoint]) -> None: """Invoke the CC API on a node endpoint.""" - errors: list[str] = [] - for endpoint in endpoints: + results = await asyncio.gather( + *( + endpoint.async_invoke_cc_api( + command_class, method_name, *parameters + ) + for endpoint in endpoints + ), + return_exceptions=True, + ) + for endpoint, result in get_valid_responses_from_results( + endpoints, results + ): _LOGGER.info( - "Invoking %s CC API method %s on endpoint %s", + ( + "Invoked %s CC API method %s on endpoint %s with the following " + "result: %s" + ), command_class.name, method_name, endpoint, + result, ) - try: - await endpoint.async_invoke_cc_api( - command_class, method_name, *parameters - ) - except FailedCommand as err: - errors.append(cast(str, err.args[0])) - if errors: - raise HomeAssistantError( - "\n".join([f"{len(errors)} error(s):", *errors]) - ) + raise_exceptions_from_results(endpoints, results) # If an endpoint is provided, we assume the user wants to call the CC API on # that endpoint for all target nodes diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 67f3320b5f7..e04ec569c9f 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -518,6 +518,71 @@ async def test_set_config_parameter(hass, client, multisensor_6, integration): ) +async def test_set_config_parameter_gather( + hass, + client, + multisensor_6, + climate_radio_thermostat_ct100_plus_different_endpoints, + integration, +): + """Test the set_config_parameter service gather functionality.""" + # Test setting config parameter by property and validate that the first node + # which triggers an error doesn't prevent the second one to be called. + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_CONFIG_PARAMETER, + { + ATTR_ENTITY_ID: [ + AIR_TEMPERATURE_SENSOR, + CLIMATE_RADIO_THERMOSTAT_ENTITY, + ], + ATTR_CONFIG_PARAMETER: 1, + ATTR_CONFIG_VALUE: 1, + }, + blocking=True, + ) + + assert len(client.async_send_command_no_wait.call_args_list) == 0 + 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"] == 26 + assert args["valueId"] == { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Temperature Reporting Threshold", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "description": "Reporting threshold for changes in the ambient temperature", + "label": "Temperature Reporting Threshold", + "default": 2, + "min": 0, + "max": 4, + "states": { + "0": "Disabled", + "1": "0.5\u00b0 F", + "2": "1.0\u00b0 F", + "3": "1.5\u00b0 F", + "4": "2.0\u00b0 F", + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": False, + "isFromConfig": True, + }, + "value": 1, + } + assert args["value"] == 1 + + client.async_send_command.reset_mock() + + async def test_bulk_set_config_parameters(hass, client, multisensor_6, integration): """Test the bulk_set_partial_config_parameters service.""" dev_reg = async_get_dev_reg(hass) @@ -726,6 +791,45 @@ async def test_bulk_set_config_parameters(hass, client, multisensor_6, integrati client.async_send_command.reset_mock() +async def test_bulk_set_config_parameters_gather( + hass, + client, + multisensor_6, + climate_radio_thermostat_ct100_plus_different_endpoints, + integration, +): + """Test the bulk_set_partial_config_parameters service gather functionality.""" + # Test bulk setting config parameter by property and validate that the first node + # which triggers an error doesn't prevent the second one to be called. + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, + { + ATTR_ENTITY_ID: [ + CLIMATE_RADIO_THERMOSTAT_ENTITY, + AIR_TEMPERATURE_SENSOR, + ], + ATTR_CONFIG_PARAMETER: 102, + ATTR_CONFIG_VALUE: 241, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 0 + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 52 + assert args["valueId"] == { + "commandClass": 112, + "property": 102, + } + assert args["value"] == 241 + + client.async_send_command_no_wait.reset_mock() + + async def test_refresh_value( hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, integration ): @@ -1126,6 +1230,66 @@ async def test_set_value_options(hass, client, aeon_smart_switch_6, integration) client.async_send_command.reset_mock() +async def test_set_value_gather( + hass, + client, + multisensor_6, + climate_radio_thermostat_ct100_plus_different_endpoints, + integration, +): + """Test the set_value service gather functionality.""" + # Test setting value by property and validate that the first node + # which triggers an error doesn't prevent the second one to be called. + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: [ + CLIMATE_RADIO_THERMOSTAT_ENTITY, + AIR_TEMPERATURE_SENSOR, + ], + ATTR_COMMAND_CLASS: 112, + ATTR_PROPERTY: 102, + ATTR_PROPERTY_KEY: 1, + ATTR_VALUE: 1, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 0 + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 52 + assert args["valueId"] == { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyKey": 1, + "propertyName": "Group 2: Send battery reports", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": True, + "label": "Group 2: Send battery reports", + "description": "Include battery information in periodic reports to Group 2", + "isFromConfig": True, + }, + "value": 0, + } + assert args["value"] == 1 + + client.async_send_command_no_wait.reset_mock() + + async def test_multicast_set_value( hass, client, @@ -1728,7 +1892,8 @@ async def test_invoke_cc_api( client.async_send_command.reset_mock() client.async_send_command_no_wait.reset_mock() - # Test failed invoke_cc_api call on one node + # Test failed invoke_cc_api call on one node. We return the error on + # the first node in the call to make sure that gather works as expected client.async_send_command.return_value = {"response": True} client.async_send_command_no_wait.side_effect = FailedZWaveCommand( "test", 12, "test" @@ -1740,8 +1905,8 @@ async def test_invoke_cc_api( SERVICE_INVOKE_CC_API, { ATTR_DEVICE_ID: [ - device_radio_thermostat.id, device_danfoss.id, + device_radio_thermostat.id, ], ATTR_COMMAND_CLASS: 132, ATTR_ENDPOINT: 0, From 3ce19cd6f870b91f162c69d027a52f2f795f899d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 12 May 2022 09:39:49 +0200 Subject: [PATCH 0407/3516] Remove YAML configuration from DuneHD (#71694) --- .../components/dunehd/config_flow.py | 28 +----------- .../components/dunehd/media_player.py | 39 +--------------- tests/components/dunehd/test_config_flow.py | 45 +------------------ 3 files changed, 4 insertions(+), 108 deletions(-) diff --git a/homeassistant/components/dunehd/config_flow.py b/homeassistant/components/dunehd/config_flow.py index 434bbc7bd84..b5a656716b0 100644 --- a/homeassistant/components/dunehd/config_flow.py +++ b/homeassistant/components/dunehd/config_flow.py @@ -2,9 +2,8 @@ from __future__ import annotations import ipaddress -import logging import re -from typing import Any, Final +from typing import Any from pdunehd import DuneHDPlayer import voluptuous as vol @@ -15,8 +14,6 @@ from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN -_LOGGER: Final = logging.getLogger(__name__) - def host_valid(host: str) -> bool: """Return True if hostname or IP address is valid.""" @@ -72,29 +69,6 @@ class DuneHDConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import( - self, user_input: dict[str, str] | None = None - ) -> FlowResult: - """Handle configuration by yaml file.""" - _LOGGER.warning( - "Configuration of the Dune HD integration in YAML is deprecated and will be " - "removed in Home Assistant 2022.6; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - assert user_input is not None - host: str = user_input[CONF_HOST] - - self._async_abort_entries_match({CONF_HOST: host}) - - try: - await self.init_device(host) - except CannotConnect: - _LOGGER.error("Import aborted, cannot connect to %s", host) - return self.async_abort(reason="cannot_connect") - else: - return self.async_create_entry(title=host, data=user_input) - def host_already_configured(self, host: str) -> bool: """See if we already have a dunehd entry matching user input configured.""" existing_hosts = { diff --git a/homeassistant/components/dunehd/media_player.py b/homeassistant/components/dunehd/media_player.py index d39d148e5a5..f6d90b7b1d9 100644 --- a/homeassistant/components/dunehd/media_player.py +++ b/homeassistant/components/dunehd/media_player.py @@ -4,40 +4,21 @@ from __future__ import annotations from typing import Any, Final from pdunehd import DuneHDPlayer -import voluptuous as vol from homeassistant.components.media_player import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, MediaPlayerEntity, MediaPlayerEntityFeature, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - STATE_OFF, - STATE_ON, - STATE_PAUSED, - STATE_PLAYING, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING 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 .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN CONF_SOURCES: Final = "sources" -PLATFORM_SCHEMA: Final = PARENT_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_SOURCES): vol.Schema({cv.string: cv.string}), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - DUNEHD_PLAYER_SUPPORT: Final[int] = ( MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.TURN_ON @@ -48,22 +29,6 @@ DUNEHD_PLAYER_SUPPORT: Final[int] = ( ) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Dune HD media player platform.""" - host: str = config[CONF_HOST] - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_HOST: host} - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: diff --git a/tests/components/dunehd/test_config_flow.py b/tests/components/dunehd/test_config_flow.py index 73732236077..76585ac73a1 100644 --- a/tests/components/dunehd/test_config_flow.py +++ b/tests/components/dunehd/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.dunehd.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST from tests.common import MockConfigEntry @@ -14,49 +14,6 @@ CONFIG_IP = {CONF_HOST: "10.10.10.12"} DUNEHD_STATE = {"protocol_version": "4", "player_state": "navigator"} -async def test_import(hass): - """Test that the import works.""" - with patch("homeassistant.components.dunehd.async_setup_entry"), patch( - "pdunehd.DuneHDPlayer.update_state", return_value=DUNEHD_STATE - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=CONFIG_HOSTNAME - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "dunehd-host" - assert result["data"] == {CONF_HOST: "dunehd-host"} - - -async def test_import_cannot_connect(hass): - """Test that errors are shown when cannot connect to the host during import.""" - with patch("pdunehd.DuneHDPlayer.update_state", return_value={}): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=CONFIG_HOSTNAME - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "cannot_connect" - - -async def test_import_duplicate_error(hass): - """Test that errors are shown when duplicates are added during import.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_HOST: "dunehd-host"}, - title="dunehd-host", - ) - config_entry.add_to_hass(hass) - - with patch("pdunehd.DuneHDPlayer.update_state", return_value=DUNEHD_STATE): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=CONFIG_HOSTNAME - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - async def test_user_invalid_host(hass): """Test that errors are shown when the host is invalid.""" result = await hass.config_entries.flow.async_init( From 135326a4a6c152d2d2a3d96b3781cb21d2bdb921 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 12 May 2022 10:31:42 +0200 Subject: [PATCH 0408/3516] Remove YAML configuration from filesize (#71692) --- homeassistant/components/filesize/__init__.py | 3 -- .../components/filesize/config_flow.py | 4 -- homeassistant/components/filesize/const.py | 2 - homeassistant/components/filesize/sensor.py | 37 +------------- tests/components/filesize/test_config_flow.py | 23 +-------- tests/components/filesize/test_init.py | 48 +------------------ tests/components/filesize/test_sensor.py | 22 --------- 7 files changed, 6 insertions(+), 133 deletions(-) diff --git a/homeassistant/components/filesize/__init__.py b/homeassistant/components/filesize/__init__.py index 8f5b098d221..b3292ed9c9f 100644 --- a/homeassistant/components/filesize/__init__.py +++ b/homeassistant/components/filesize/__init__.py @@ -1,7 +1,6 @@ """The filesize component.""" from __future__ import annotations -import logging import pathlib from homeassistant.config_entries import ConfigEntry @@ -11,8 +10,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from .const import PLATFORMS -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" diff --git a/homeassistant/components/filesize/config_flow.py b/homeassistant/components/filesize/config_flow.py index 7838353fa12..ed2d4ab0940 100644 --- a/homeassistant/components/filesize/config_flow.py +++ b/homeassistant/components/filesize/config_flow.py @@ -67,10 +67,6 @@ class FilesizeConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Handle import from configuration.yaml.""" - return await self.async_step_user(user_input) - class NotValidError(Exception): """Path is not valid error.""" diff --git a/homeassistant/components/filesize/const.py b/homeassistant/components/filesize/const.py index a47f1f99d38..a3180f762c0 100644 --- a/homeassistant/components/filesize/const.py +++ b/homeassistant/components/filesize/const.py @@ -4,5 +4,3 @@ from homeassistant.const import Platform DOMAIN = "filesize" PLATFORMS = [Platform.SENSOR] - -CONF_FILE_PATHS = "file_paths" diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 22b8cd60d79..70896bcacad 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -6,23 +6,18 @@ import logging import os import pathlib -import voluptuous as vol - from homeassistant.components.sensor import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_FILE_PATH, DATA_BYTES, DATA_MEGABYTES from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -30,7 +25,7 @@ from homeassistant.helpers.update_coordinator import ( ) import homeassistant.util.dt as dt_util -from .const import CONF_FILE_PATHS, DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -64,34 +59,6 @@ SENSOR_TYPES = ( ), ) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( - {vol.Required(CONF_FILE_PATHS): vol.All(cv.ensure_list, [cv.isfile])} -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the file size sensor.""" - _LOGGER.warning( - # Filesize config flow added in 2022.4 and should be removed in 2022.6 - "Configuration of the Filesize sensor platform in YAML is deprecated and " - "will be removed in Home Assistant 2022.6; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - for path in config[CONF_FILE_PATHS]: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_FILE_PATH: path}, - ) - ) - async def async_setup_entry( hass: HomeAssistant, diff --git a/tests/components/filesize/test_config_flow.py b/tests/components/filesize/test_config_flow.py index 0209444ec42..f6873e64128 100644 --- a/tests/components/filesize/test_config_flow.py +++ b/tests/components/filesize/test_config_flow.py @@ -1,10 +1,8 @@ """Tests for the Filesize config flow.""" from unittest.mock import patch -import pytest - from homeassistant.components.filesize.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_FILE_PATH from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( @@ -40,39 +38,22 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: assert result2.get("data") == {CONF_FILE_PATH: TEST_FILE} -@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT]) async def test_unique_path( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - source: str, ) -> None: """Test we abort if already setup.""" hass.config.allowlist_external_dirs = {TEST_DIR} mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": source}, data={CONF_FILE_PATH: TEST_FILE} + DOMAIN, context={"source": SOURCE_USER}, data={CONF_FILE_PATH: TEST_FILE} ) assert result.get("type") == RESULT_TYPE_ABORT assert result.get("reason") == "already_configured" -async def test_import_flow(hass: HomeAssistant) -> None: - """Test the import configuration flow.""" - create_file(TEST_FILE) - hass.config.allowlist_external_dirs = {TEST_DIR} - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_FILE_PATH: TEST_FILE}, - ) - - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY - assert result.get("title") == TEST_FILE_NAME - assert result.get("data") == {CONF_FILE_PATH: TEST_FILE} - - async def test_flow_fails_on_validation(hass: HomeAssistant) -> None: """Test config flow errors.""" create_file(TEST_FILE) diff --git a/tests/components/filesize/test_init.py b/tests/components/filesize/test_init.py index fecd3033c91..99285e56b6f 100644 --- a/tests/components/filesize/test_init.py +++ b/tests/components/filesize/test_init.py @@ -1,21 +1,10 @@ """Tests for the Filesize integration.""" -from unittest.mock import AsyncMock - -from homeassistant.components.filesize.const import CONF_FILE_PATHS, DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.filesize.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_FILE_PATH from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component -from . import ( - TEST_DIR, - TEST_FILE, - TEST_FILE2, - TEST_FILE_NAME, - TEST_FILE_NAME2, - create_file, -) +from . import create_file from tests.common import MockConfigEntry @@ -75,36 +64,3 @@ async def test_not_valid_path_to_file( await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY - - -async def test_import_config( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, -) -> None: - """Test Filesize being set up from config via import.""" - create_file(TEST_FILE) - create_file(TEST_FILE2) - hass.config.allowlist_external_dirs = {TEST_DIR} - assert await async_setup_component( - hass, - SENSOR_DOMAIN, - { - SENSOR_DOMAIN: { - "platform": DOMAIN, - CONF_FILE_PATHS: [TEST_FILE, TEST_FILE2], - } - }, - ) - await hass.async_block_till_done() - - config_entries = hass.config_entries.async_entries(DOMAIN) - assert len(config_entries) == 2 - - entry = config_entries[0] - assert entry.title == TEST_FILE_NAME - assert entry.unique_id == TEST_FILE - assert entry.data == {CONF_FILE_PATH: TEST_FILE} - entry2 = config_entries[1] - assert entry2.title == TEST_FILE_NAME2 - assert entry2.unique_id == TEST_FILE2 - assert entry2.data == {CONF_FILE_PATH: TEST_FILE2} diff --git a/tests/components/filesize/test_sensor.py b/tests/components/filesize/test_sensor.py index 6f21119f95f..803aa96610d 100644 --- a/tests/components/filesize/test_sensor.py +++ b/tests/components/filesize/test_sensor.py @@ -1,11 +1,9 @@ """The tests for the filesize sensor.""" import os -from homeassistant.components.filesize.const import DOMAIN from homeassistant.const import CONF_FILE_PATH, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import async_update_entity -from homeassistant.setup import async_setup_component from . import TEST_FILE, TEST_FILE_NAME, create_file @@ -71,23 +69,3 @@ async def test_state_unavailable( state = hass.states.get("sensor.file_txt_size") assert state.state == STATE_UNAVAILABLE - - -async def test_import_query(hass: HomeAssistant, tmpdir: str) -> None: - """Test import from yaml.""" - testfile = f"{tmpdir}/file.txt" - create_file(testfile) - hass.config.allowlist_external_dirs = {tmpdir} - config = { - "sensor": { - "platform": "filesize", - "file_paths": [testfile], - } - } - - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() - - assert hass.config_entries.async_entries(DOMAIN) - data = hass.config_entries.async_entries(DOMAIN)[0].data - assert data[CONF_FILE_PATH] == testfile From 577b8cd97638124f93f8f4dc18809214d0240bdd Mon Sep 17 00:00:00 2001 From: Rudolf Offereins Date: Thu, 12 May 2022 12:12:47 +0200 Subject: [PATCH 0409/3516] Add Geocaching integration (#50284) Co-authored-by: Paulus Schoutsen Co-authored-by: Reinder Reinders Co-authored-by: Franck Nijhof --- .coveragerc | 5 + .strict-typing | 1 + CODEOWNERS | 2 + .../components/geocaching/__init__.py | 81 ++++++ .../components/geocaching/config_flow.py | 56 ++++ homeassistant/components/geocaching/const.py | 27 ++ .../components/geocaching/coordinator.py | 47 ++++ .../components/geocaching/manifest.json | 10 + homeassistant/components/geocaching/models.py | 9 + homeassistant/components/geocaching/oauth.py | 77 ++++++ homeassistant/components/geocaching/sensor.py | 126 +++++++++ .../components/geocaching/strings.json | 25 ++ .../geocaching/translations/en.json | 25 ++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/geocaching/__init__.py | 5 + tests/components/geocaching/conftest.py | 50 ++++ .../components/geocaching/test_config_flow.py | 256 ++++++++++++++++++ 20 files changed, 820 insertions(+) create mode 100644 homeassistant/components/geocaching/__init__.py create mode 100644 homeassistant/components/geocaching/config_flow.py create mode 100644 homeassistant/components/geocaching/const.py create mode 100644 homeassistant/components/geocaching/coordinator.py create mode 100644 homeassistant/components/geocaching/manifest.json create mode 100644 homeassistant/components/geocaching/models.py create mode 100644 homeassistant/components/geocaching/oauth.py create mode 100644 homeassistant/components/geocaching/sensor.py create mode 100644 homeassistant/components/geocaching/strings.json create mode 100644 homeassistant/components/geocaching/translations/en.json create mode 100644 tests/components/geocaching/__init__.py create mode 100644 tests/components/geocaching/conftest.py create mode 100644 tests/components/geocaching/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 706122d0a07..78f63d30cbe 100644 --- a/.coveragerc +++ b/.coveragerc @@ -411,6 +411,11 @@ omit = homeassistant/components/garages_amsterdam/sensor.py homeassistant/components/gc100/* homeassistant/components/geniushub/* + homeassistant/components/geocaching/__init__.py + homeassistant/components/geocaching/const.py + homeassistant/components/geocaching/coordinator.py + homeassistant/components/geocaching/oauth.py + homeassistant/components/geocaching/sensor.py homeassistant/components/github/__init__.py homeassistant/components/github/coordinator.py homeassistant/components/github/sensor.py diff --git a/.strict-typing b/.strict-typing index f7264591c83..8c580dfa1aa 100644 --- a/.strict-typing +++ b/.strict-typing @@ -97,6 +97,7 @@ homeassistant.components.fronius.* homeassistant.components.frontend.* homeassistant.components.fritz.* homeassistant.components.geo_location.* +homeassistant.components.geocaching.* homeassistant.components.gios.* homeassistant.components.goalzero.* homeassistant.components.greeneye_monitor.* diff --git a/CODEOWNERS b/CODEOWNERS index 19450d86bba..8be3146b432 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -374,6 +374,8 @@ build.json @home-assistant/supervisor /tests/components/geo_location/ @home-assistant/core /homeassistant/components/geo_rss_events/ @exxamalte /tests/components/geo_rss_events/ @exxamalte +/homeassistant/components/geocaching/ @Sholofly @reinder83 +/tests/components/geocaching/ @Sholofly @reinder83 /homeassistant/components/geonetnz_quakes/ @exxamalte /tests/components/geonetnz_quakes/ @exxamalte /homeassistant/components/geonetnz_volcano/ @exxamalte diff --git a/homeassistant/components/geocaching/__init__.py b/homeassistant/components/geocaching/__init__.py new file mode 100644 index 00000000000..f04fbbd608f --- /dev/null +++ b/homeassistant/components/geocaching/__init__.py @@ -0,0 +1,81 @@ +"""The Geocaching integration.""" +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.config_entry_oauth2_flow import ( + OAuth2Session, + async_get_config_entry_implementation, +) +from homeassistant.helpers.typing import ConfigType + +from .config_flow import GeocachingFlowHandler +from .const import DOMAIN +from .coordinator import GeocachingDataUpdateCoordinator +from .oauth import GeocachingOAuth2Implementation + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Geocaching component.""" + if DOMAIN not in config: + return True + + GeocachingFlowHandler.async_register_implementation( + hass, + GeocachingOAuth2Implementation( + hass, + client_id=config[DOMAIN][CONF_CLIENT_ID], + client_secret=config[DOMAIN][CONF_CLIENT_SECRET], + name="Geocaching", + ), + ) + + # When manual configuration is done, discover the integration. + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_INTEGRATION_DISCOVERY} + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Geocaching from a config entry.""" + implementation = await async_get_config_entry_implementation(hass, entry) + + oauth_session = OAuth2Session(hass, entry, implementation) + coordinator = GeocachingDataUpdateCoordinator( + hass, entry=entry, session=oauth_session + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + del hass.data[DOMAIN][entry.entry_id] + return unload_ok diff --git a/homeassistant/components/geocaching/config_flow.py b/homeassistant/components/geocaching/config_flow.py new file mode 100644 index 00000000000..83c9ed17586 --- /dev/null +++ b/homeassistant/components/geocaching/config_flow.py @@ -0,0 +1,56 @@ +"""Config flow for Geocaching.""" +from __future__ import annotations + +import logging +from typing import Any + +from geocachingapi.geocachingapi import GeocachingApi + +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler + +from .const import DOMAIN, ENVIRONMENT + + +class GeocachingFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): + """Config flow to handle Geocaching OAuth2 authentication.""" + + DOMAIN = DOMAIN + VERSION = 1 + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + async def async_step_reauth(self, user_input: dict[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + return await self.async_step_reauth_confirm(user_input=user_input) + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | 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") + return await self.async_step_user() + + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: + """Create an oauth config entry or update existing entry for reauth.""" + api = GeocachingApi( + environment=ENVIRONMENT, + token=data["token"]["access_token"], + session=async_get_clientsession(self.hass), + ) + status = await api.update() + if not status.user or not status.user.username: + return self.async_abort(reason="oauth_error") + + if existing_entry := await self.async_set_unique_id( + status.user.username.lower() + ): + 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 self.async_create_entry(title=status.user.username, data=data) diff --git a/homeassistant/components/geocaching/const.py b/homeassistant/components/geocaching/const.py new file mode 100644 index 00000000000..13b42b318c0 --- /dev/null +++ b/homeassistant/components/geocaching/const.py @@ -0,0 +1,27 @@ +"""Constants for the Geocaching integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Final + +from geocachingapi.models import GeocachingApiEnvironment + +from .models import GeocachingOAuthApiUrls + +DOMAIN: Final = "geocaching" +LOGGER = logging.getLogger(__package__) +UPDATE_INTERVAL = timedelta(hours=1) + +ENVIRONMENT_URLS = { + GeocachingApiEnvironment.Staging: GeocachingOAuthApiUrls( + authorize_url="https://staging.geocaching.com/oauth/authorize.aspx", + token_url="https://oauth-staging.geocaching.com/token", + ), + GeocachingApiEnvironment.Production: GeocachingOAuthApiUrls( + authorize_url="https://www.geocaching.com/oauth/authorize.aspx", + token_url="https://oauth.geocaching.com/token", + ), +} + +ENVIRONMENT = GeocachingApiEnvironment.Production diff --git a/homeassistant/components/geocaching/coordinator.py b/homeassistant/components/geocaching/coordinator.py new file mode 100644 index 00000000000..f02cccf544b --- /dev/null +++ b/homeassistant/components/geocaching/coordinator.py @@ -0,0 +1,47 @@ +"""Provides the Geocaching DataUpdateCoordinator.""" +from __future__ import annotations + +from geocachingapi.exceptions import GeocachingApiError +from geocachingapi.geocachingapi import GeocachingApi +from geocachingapi.models import GeocachingStatus + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, ENVIRONMENT, LOGGER, UPDATE_INTERVAL + + +class GeocachingDataUpdateCoordinator(DataUpdateCoordinator[GeocachingStatus]): + """Class to manage fetching Geocaching data from single endpoint.""" + + def __init__( + self, hass: HomeAssistant, *, entry: ConfigEntry, session: OAuth2Session + ) -> None: + """Initialize global Geocaching data updater.""" + self.session = session + self.entry = entry + + async def async_token_refresh() -> str: + await session.async_ensure_token_valid() + token = session.token["access_token"] + LOGGER.debug(str(token)) + return str(token) + + client_session = async_get_clientsession(hass) + self.geocaching = GeocachingApi( + environment=ENVIRONMENT, + token=session.token["access_token"], + session=client_session, + token_refresh_method=async_token_refresh, + ) + + super().__init__(hass, LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) + + async def _async_update_data(self) -> GeocachingStatus: + try: + return await self.geocaching.update() + except GeocachingApiError as error: + raise UpdateFailed(f"Invalid response from API: {error}") from error diff --git a/homeassistant/components/geocaching/manifest.json b/homeassistant/components/geocaching/manifest.json new file mode 100644 index 00000000000..683a23d474a --- /dev/null +++ b/homeassistant/components/geocaching/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "geocaching", + "name": "Geocaching", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/geocaching", + "requirements": ["geocachingapi==0.2.1"], + "dependencies": ["auth"], + "codeowners": ["@Sholofly", "@reinder83"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/geocaching/models.py b/homeassistant/components/geocaching/models.py new file mode 100644 index 00000000000..60ee4e05978 --- /dev/null +++ b/homeassistant/components/geocaching/models.py @@ -0,0 +1,9 @@ +"""Models for the Geocaching integration.""" +from typing import TypedDict + + +class GeocachingOAuthApiUrls(TypedDict): + """oAuth2 urls for a single environment.""" + + authorize_url: str + token_url: str diff --git a/homeassistant/components/geocaching/oauth.py b/homeassistant/components/geocaching/oauth.py new file mode 100644 index 00000000000..29371eb793c --- /dev/null +++ b/homeassistant/components/geocaching/oauth.py @@ -0,0 +1,77 @@ +"""oAuth2 functions and classes for Geocaching API integration.""" +from __future__ import annotations + +from typing import Any, cast + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN, ENVIRONMENT, ENVIRONMENT_URLS + + +class GeocachingOAuth2Implementation( + config_entry_oauth2_flow.LocalOAuth2Implementation +): + """Local OAuth2 implementation for Geocaching.""" + + def __init__( + self, hass: HomeAssistant, client_id: str, client_secret: str, name: str + ) -> None: + """Local Geocaching Oauth Implementation.""" + self._name = name + super().__init__( + hass=hass, + client_id=client_id, + client_secret=client_secret, + domain=DOMAIN, + authorize_url=ENVIRONMENT_URLS[ENVIRONMENT]["authorize_url"], + token_url=ENVIRONMENT_URLS[ENVIRONMENT]["token_url"], + ) + + @property + def name(self) -> str: + """Name of the implementation.""" + return f"{self._name}" + + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return {"scope": "*", "response_type": "code"} + + async def async_resolve_external_data(self, external_data: Any) -> dict: + """Initialize local Geocaching API auth implementation.""" + redirect_uri = external_data["state"]["redirect_uri"] + data = { + "grant_type": "authorization_code", + "code": external_data["code"], + "redirect_uri": redirect_uri, + } + token = await self._token_request(data) + # Store the redirect_uri (Needed for refreshing token, but not according to oAuth2 spec!) + token["redirect_uri"] = redirect_uri + return token + + async def _async_refresh_token(self, token: dict) -> dict: + """Refresh tokens.""" + data = { + "client_id": self.client_id, + "client_secret": self.client_secret, + "grant_type": "refresh_token", + "refresh_token": token["refresh_token"], + # Add previously stored redirect_uri (Mandatory, but not according to oAuth2 spec!) + "redirect_uri": token["redirect_uri"], + } + + new_token = await self._token_request(data) + return {**token, **new_token} + + async def _token_request(self, data: dict) -> dict: + """Make a token request.""" + data["client_id"] = self.client_id + if self.client_secret is not None: + data["client_secret"] = self.client_secret + session = async_get_clientsession(self.hass) + resp = await session.post(ENVIRONMENT_URLS[ENVIRONMENT]["token_url"], data=data) + resp.raise_for_status() + return cast(dict, await resp.json()) diff --git a/homeassistant/components/geocaching/sensor.py b/homeassistant/components/geocaching/sensor.py new file mode 100644 index 00000000000..82353a81484 --- /dev/null +++ b/homeassistant/components/geocaching/sensor.py @@ -0,0 +1,126 @@ +"""Platform for sensor integration.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import cast + +from geocachingapi.models import GeocachingStatus + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import GeocachingDataUpdateCoordinator + + +@dataclass +class GeocachingRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[GeocachingStatus], str | int | None] + + +@dataclass +class GeocachingSensorEntityDescription( + SensorEntityDescription, GeocachingRequiredKeysMixin +): + """Define Sensor entity description class.""" + + +SENSORS: tuple[GeocachingSensorEntityDescription, ...] = ( + GeocachingSensorEntityDescription( + key="username", + name="username", + icon="mdi:account", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda status: status.user.username, + ), + GeocachingSensorEntityDescription( + key="find_count", + name="Total finds", + icon="mdi:notebook-edit-outline", + native_unit_of_measurement="caches", + value_fn=lambda status: status.user.find_count, + ), + GeocachingSensorEntityDescription( + key="hide_count", + name="Total hides", + icon="mdi:eye-off-outline", + native_unit_of_measurement="caches", + entity_registry_visible_default=False, + value_fn=lambda status: status.user.hide_count, + ), + GeocachingSensorEntityDescription( + key="favorite_points", + name="Favorite points", + icon="mdi:heart-outline", + native_unit_of_measurement="points", + entity_registry_visible_default=False, + value_fn=lambda status: status.user.favorite_points, + ), + GeocachingSensorEntityDescription( + key="souvenir_count", + name="Total souvenirs", + icon="mdi:license", + native_unit_of_measurement="souvenirs", + value_fn=lambda status: status.user.souvenir_count, + ), + GeocachingSensorEntityDescription( + key="awarded_favorite_points", + name="Awarded favorite points", + icon="mdi:heart", + native_unit_of_measurement="points", + entity_registry_visible_default=False, + value_fn=lambda status: status.user.awarded_favorite_points, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up a Geocaching sensor entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + GeocachingSensor(coordinator, description) for description in SENSORS + ) + + +class GeocachingSensor( + CoordinatorEntity[GeocachingDataUpdateCoordinator], SensorEntity +): + """Representation of a Sensor.""" + + entity_description: GeocachingSensorEntityDescription + + def __init__( + self, + coordinator: GeocachingDataUpdateCoordinator, + description: GeocachingSensorEntityDescription, + ) -> None: + """Initialize the Geocaching sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_name = ( + f"Geocaching {coordinator.data.user.username} {description.name}" + ) + self._attr_unique_id = ( + f"{coordinator.data.user.reference_code}_{description.key}" + ) + self._attr_device_info = DeviceInfo( + name=f"Geocaching {coordinator.data.user.username}", + identifiers={(DOMAIN, cast(str, coordinator.data.user.reference_code))}, + entry_type=DeviceEntryType.SERVICE, + manufacturer="Groundspeak, Inc.", + ) + + @property + def native_value(self) -> str | int | None: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/geocaching/strings.json b/homeassistant/components/geocaching/strings.json new file mode 100644 index 00000000000..7c8547805d1 --- /dev/null +++ b/homeassistant/components/geocaching/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Geocaching integration needs to re-authenticate your account" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + }, + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" + } + } +} diff --git a/homeassistant/components/geocaching/translations/en.json b/homeassistant/components/geocaching/translations/en.json new file mode 100644 index 00000000000..7f76bce4f3d --- /dev/null +++ b/homeassistant/components/geocaching/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured", + "already_in_progress": "Configuration flow is already in progress", + "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})", + "oauth_error": "Received invalid token data.", + "reauth_successful": "Re-authentication was successful" + }, + "create_entry": { + "default": "Successfully authenticated" + }, + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + }, + "reauth_confirm": { + "description": "The Geocaching integration needs to re-authenticate your account", + "title": "Reauthenticate Integration" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 4bcfa0dbbef..70451b22001 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -121,6 +121,7 @@ FLOWS = { "garages_amsterdam", "gdacs", "generic", + "geocaching", "geofency", "geonetnz_quakes", "geonetnz_volcano", diff --git a/mypy.ini b/mypy.ini index ab4ba77c98b..532c4526372 100644 --- a/mypy.ini +++ b/mypy.ini @@ -830,6 +830,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.geocaching.*] +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.gios.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index c2fc83e847b..3ecdceca859 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -694,6 +694,9 @@ gcal-sync==0.7.1 # homeassistant.components.geniushub geniushub-client==0.6.30 +# homeassistant.components.geocaching +geocachingapi==0.2.1 + # homeassistant.components.usgs_earthquakes_feed geojson_client==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2fe34d6a33e..c23b9d58593 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -488,6 +488,9 @@ garages-amsterdam==3.0.0 # homeassistant.components.google gcal-sync==0.7.1 +# homeassistant.components.geocaching +geocachingapi==0.2.1 + # homeassistant.components.usgs_earthquakes_feed geojson_client==0.6 diff --git a/tests/components/geocaching/__init__.py b/tests/components/geocaching/__init__.py new file mode 100644 index 00000000000..8bc72aa3799 --- /dev/null +++ b/tests/components/geocaching/__init__.py @@ -0,0 +1,5 @@ +"""Tests for the Geocaching integration.""" + +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" +REDIRECT_URI = "https://example.com/auth/external/callback" diff --git a/tests/components/geocaching/conftest.py b/tests/components/geocaching/conftest.py new file mode 100644 index 00000000000..f59f428118e --- /dev/null +++ b/tests/components/geocaching/conftest.py @@ -0,0 +1,50 @@ +"""Fixtures for the Geocaching integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +from geocachingapi import GeocachingStatus +import pytest + +from homeassistant.components.geocaching.const import DOMAIN + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="1234AB 1", + domain=DOMAIN, + data={ + "id": "mock_user", + "auth_implementation": DOMAIN, + }, + unique_id="mock_user", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.geocaching.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup + + +@pytest.fixture +def mock_geocaching_config_flow() -> Generator[None, MagicMock, None]: + """Return a mocked Geocaching API client.""" + + mock_status = GeocachingStatus() + mock_status.user.username = "mock_user" + + with patch( + "homeassistant.components.geocaching.config_flow.GeocachingApi", autospec=True + ) as geocaching_mock: + geocachingapi = geocaching_mock.return_value + geocachingapi.update.return_value = mock_status + yield geocachingapi diff --git a/tests/components/geocaching/test_config_flow.py b/tests/components/geocaching/test_config_flow.py new file mode 100644 index 00000000000..0f5d182b2db --- /dev/null +++ b/tests/components/geocaching/test_config_flow.py @@ -0,0 +1,256 @@ +"""Test the Geocaching config flow.""" +from collections.abc import Awaitable, Callable +from http import HTTPStatus +from unittest.mock import MagicMock + +from aiohttp.test_utils import TestClient + +from homeassistant.components.geocaching.const import ( + DOMAIN, + ENVIRONMENT, + ENVIRONMENT_URLS, +) +from homeassistant.config_entries import ( + DEFAULT_DISCOVERY_UNIQUE_ID, + SOURCE_INTEGRATION_DISCOVERY, + SOURCE_REAUTH, + SOURCE_USER, +) +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_EXTERNAL_STEP +from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.setup import async_setup_component + +from . import CLIENT_ID, CLIENT_SECRET, REDIRECT_URI + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + +CURRENT_ENVIRONMENT_URLS = ENVIRONMENT_URLS[ENVIRONMENT] + + +async def setup_geocaching_component(hass: HomeAssistant) -> bool: + """Set up the Geocaching component.""" + return await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_CLIENT_ID: CLIENT_ID, + CONF_CLIENT_SECRET: CLIENT_SECRET, + }, + }, + ) + + +async def test_full_flow( + hass: HomeAssistant, + hass_client_no_auth: Callable[[], Awaitable[TestClient]], + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + mock_geocaching_config_flow: MagicMock, + mock_setup_entry: MagicMock, +) -> None: + """Check full flow.""" + assert await setup_geocaching_component(hass) + + # Ensure integration is discovered when manual implementation is configured + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert "context" in flows[0] + assert flows[0]["context"]["source"] == SOURCE_INTEGRATION_DISCOVERY + assert flows[0]["context"]["unique_id"] == DEFAULT_DISCOVERY_UNIQUE_ID + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert "flow_id" in result + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": REDIRECT_URI, + }, + ) + + assert result.get("type") == RESULT_TYPE_EXTERNAL_STEP + assert result.get("step_id") == "auth" + assert result.get("url") == ( + f"{CURRENT_ENVIRONMENT_URLS['authorize_url']}?response_type=code&client_id={CLIENT_ID}" + f"&redirect_uri={REDIRECT_URI}" + f"&state={state}&scope=*" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + CURRENT_ENVIRONMENT_URLS["token_url"], + json={ + "access_token": "mock-access-token", + "token_type": "bearer", + "expires_in": 3599, + "refresh_token": "mock-refresh_token", + }, + ) + + await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_existing_entry( + hass: HomeAssistant, + hass_client_no_auth: Callable[[], Awaitable[TestClient]], + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + mock_geocaching_config_flow: MagicMock, + mock_setup_entry: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Check existing entry.""" + assert await setup_geocaching_component(hass) + mock_config_entry.add_to_hass(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert "flow_id" in result + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": REDIRECT_URI, + }, + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + CURRENT_ENVIRONMENT_URLS["token_url"], + json={ + "access_token": "mock-access-token", + "token_type": "bearer", + "expires_in": 3599, + "refresh_token": "mock-refresh_token", + }, + ) + + await hass.config_entries.flow.async_configure(result["flow_id"]) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + +async def test_oauth_error( + hass: HomeAssistant, + hass_client_no_auth: Callable[[], Awaitable[TestClient]], + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + mock_geocaching_config_flow: MagicMock, + mock_setup_entry: MagicMock, +) -> None: + """Check if aborted when oauth error occurs.""" + assert await setup_geocaching_component(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert "flow_id" in result + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": REDIRECT_URI, + }, + ) + assert result.get("type") == RESULT_TYPE_EXTERNAL_STEP + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == HTTPStatus.OK + + # No user information is returned from API + mock_geocaching_config_flow.update.return_value.user = None + + aioclient_mock.post( + CURRENT_ENVIRONMENT_URLS["token_url"], + json={ + "access_token": "mock-access-token", + "token_type": "bearer", + "expires_in": 3599, + "refresh_token": "mock-refresh_token", + }, + ) + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("reason") == "oauth_error" + + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_reauthentication( + hass: HomeAssistant, + hass_client_no_auth: Callable[[], Awaitable[TestClient]], + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + mock_geocaching_config_flow: MagicMock, + mock_setup_entry: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test Geocaching reauthentication.""" + mock_config_entry.add_to_hass(hass) + assert await setup_geocaching_component(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_REAUTH} + ) + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert "flow_id" in flows[0] + + result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) + assert "flow_id" in result + + # pylint: disable=protected-access + 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 == HTTPStatus.OK + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + CURRENT_ENVIRONMENT_URLS["token_url"], + json={ + "access_token": "mock-access-token", + "token_type": "bearer", + "expires_in": 3599, + "refresh_token": "mock-refresh_token", + }, + ) + + await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup_entry.mock_calls) == 1 From bf314970ea8f89f32c25872f381530544e61d4f0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 12 May 2022 13:14:33 +0200 Subject: [PATCH 0410/3516] Remove username entity from Geocaching (#71728) --- homeassistant/components/geocaching/sensor.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/homeassistant/components/geocaching/sensor.py b/homeassistant/components/geocaching/sensor.py index 82353a81484..0c719c463a4 100644 --- a/homeassistant/components/geocaching/sensor.py +++ b/homeassistant/components/geocaching/sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.sensor import SensorEntity, SensorEntityDescriptio from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -34,13 +34,6 @@ class GeocachingSensorEntityDescription( SENSORS: tuple[GeocachingSensorEntityDescription, ...] = ( - GeocachingSensorEntityDescription( - key="username", - name="username", - icon="mdi:account", - entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda status: status.user.username, - ), GeocachingSensorEntityDescription( key="find_count", name="Total finds", From 39313057c4f8384cc44bc95a8262a49c986777c1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 12 May 2022 23:14:52 +1200 Subject: [PATCH 0411/3516] Add amperage limit number to JuiceNet (#71716) * Add amperage limit number to JuiceNet * coverage exception * Use mixin dataclass --- .coveragerc | 1 + homeassistant/components/juicenet/__init__.py | 2 +- homeassistant/components/juicenet/entity.py | 15 ++- homeassistant/components/juicenet/number.py | 98 +++++++++++++++++++ 4 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/juicenet/number.py diff --git a/.coveragerc b/.coveragerc index 78f63d30cbe..922c13e550c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -579,6 +579,7 @@ omit = homeassistant/components/juicenet/const.py homeassistant/components/juicenet/device.py homeassistant/components/juicenet/entity.py + homeassistant/components/juicenet/number.py homeassistant/components/juicenet/sensor.py homeassistant/components/juicenet/switch.py homeassistant/components/kaiterra/* diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index 92d8de8bc0a..60377d7e85b 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -20,7 +20,7 @@ from .device import JuiceNetApi _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.SENSOR, Platform.SWITCH, Platform.NUMBER] CONFIG_SCHEMA = vol.Schema( vol.All( diff --git a/homeassistant/components/juicenet/entity.py b/homeassistant/components/juicenet/entity.py index 4b4e5764a5e..c0151a0cd00 100644 --- a/homeassistant/components/juicenet/entity.py +++ b/homeassistant/components/juicenet/entity.py @@ -1,7 +1,12 @@ """Adapter to wrap the pyjuicenet api for home assistant.""" +from pyjuicenet import Charger + from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import DOMAIN @@ -9,16 +14,18 @@ from .const import DOMAIN class JuiceNetDevice(CoordinatorEntity): """Represent a base JuiceNet device.""" - def __init__(self, device, sensor_type, coordinator): + def __init__( + self, device: Charger, key: str, coordinator: DataUpdateCoordinator + ) -> None: """Initialise the sensor.""" super().__init__(coordinator) self.device = device - self.type = sensor_type + self.key = key @property def unique_id(self): """Return a unique ID.""" - return f"{self.device.id}-{self.type}" + return f"{self.device.id}-{self.key}" @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/juicenet/number.py b/homeassistant/components/juicenet/number.py new file mode 100644 index 00000000000..24b0ba4f42b --- /dev/null +++ b/homeassistant/components/juicenet/number.py @@ -0,0 +1,98 @@ +"""Support for controlling juicenet/juicepoint/juicebox based EVSE numbers.""" +from __future__ import annotations + +from dataclasses import dataclass + +from pyjuicenet import Api, Charger + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.components.number.const import DEFAULT_MAX_VALUE +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 .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR +from .entity import JuiceNetDevice + + +@dataclass +class JuiceNetNumberEntityDescriptionMixin: + """Mixin for required keys.""" + + setter_key: str + + +@dataclass +class JuiceNetNumberEntityDescription( + NumberEntityDescription, JuiceNetNumberEntityDescriptionMixin +): + """An entity description for a JuiceNetNumber.""" + + max_value_key: str | None = None + + +NUMBER_TYPES: tuple[JuiceNetNumberEntityDescription, ...] = ( + JuiceNetNumberEntityDescription( + name="Amperage Limit", + key="current_charging_amperage_limit", + min_value=6, + max_value_key="max_charging_amperage", + step=1, + setter_key="set_charging_amperage_limit", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the JuiceNet Numbers.""" + juicenet_data = hass.data[DOMAIN][config_entry.entry_id] + api: Api = juicenet_data[JUICENET_API] + coordinator = juicenet_data[JUICENET_COORDINATOR] + + entities = [ + JuiceNetNumber(device, description, coordinator) + for device in api.devices + for description in NUMBER_TYPES + ] + async_add_entities(entities) + + +class JuiceNetNumber(JuiceNetDevice, NumberEntity): + """Implementation of a JuiceNet number.""" + + entity_description: JuiceNetNumberEntityDescription + + def __init__( + self, + device: Charger, + description: JuiceNetNumberEntityDescription, + coordinator: DataUpdateCoordinator, + ) -> None: + """Initialise the number.""" + super().__init__(device, description.key, coordinator) + self.entity_description = description + + self._attr_name = f"{self.device.name} {description.name}" + + @property + def value(self) -> float | None: + """Return the value of the entity.""" + return getattr(self.device, self.entity_description.key, None) + + @property + def max_value(self) -> float: + """Return the maximum value.""" + if self.entity_description.max_value_key is not None: + return getattr(self.device, self.entity_description.max_value_key) + if self.entity_description.max_value is not None: + return self.entity_description.max_value + return DEFAULT_MAX_VALUE + + async def async_set_value(self, value: float) -> None: + """Update the current value.""" + await getattr(self.device, self.entity_description.setter_key)(value) From 8f50a70ff584e0e68425afac9e0ddb6f9c21c02f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 12 May 2022 14:09:43 +0200 Subject: [PATCH 0412/3516] Tweak template alarm tests (#71730) --- .../template/test_alarm_control_panel.py | 351 +++++------------- 1 file changed, 93 insertions(+), 258 deletions(-) diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index cd29794db8d..669effca3e9 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -1,7 +1,12 @@ """The tests for the Template alarm control panel platform.""" import pytest +from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN from homeassistant.const import ( + ATTR_DOMAIN, + ATTR_ENTITY_ID, + ATTR_SERVICE_DATA, + EVENT_CALL_SERVICE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, @@ -10,13 +15,62 @@ from homeassistant.const import ( STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) - -from tests.components.alarm_control_panel import common +from homeassistant.core import callback TEMPLATE_NAME = "alarm_control_panel.test_template_panel" PANEL_NAME = "alarm_control_panel.test" +@pytest.fixture +def service_calls(hass): + """Track service call events for alarm_control_panel.test.""" + events = [] + entity_id = "alarm_control_panel.test" + + @callback + def capture_events(event): + print(event.data) + if event.data[ATTR_DOMAIN] != ALARM_DOMAIN: + return + if event.data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID] != [entity_id]: + return + events.append(event) + + hass.bus.async_listen(EVENT_CALL_SERVICE, capture_events) + + return events + + +OPTIMISTIC_TEMPLATE_ALARM_CONFIG = { + "arm_away": { + "service": "alarm_control_panel.alarm_arm_away", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "arm_home": { + "service": "alarm_control_panel.alarm_arm_home", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "arm_night": { + "service": "alarm_control_panel.alarm_arm_night", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, + "disarm": { + "service": "alarm_control_panel.alarm_disarm", + "entity_id": "alarm_control_panel.test", + "data": {"code": "1234"}, + }, +} + + +TEMPLATE_ALARM_CONFIG = { + "value_template": "{{ states('alarm_control_panel.test') }}", + **OPTIMISTIC_TEMPLATE_ALARM_CONFIG, +} + + @pytest.mark.parametrize("count,domain", [(1, "alarm_control_panel")]) @pytest.mark.parametrize( "config", @@ -24,31 +78,7 @@ PANEL_NAME = "alarm_control_panel.test" { "alarm_control_panel": { "platform": "template", - "panels": { - "test_template_panel": { - "value_template": "{{ states('alarm_control_panel.test') }}", - "arm_away": { - "service": "alarm_control_panel.alarm_arm_away", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "arm_home": { - "service": "alarm_control_panel.alarm_arm_home", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "arm_night": { - "service": "alarm_control_panel.alarm_arm_night", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "disarm": { - "service": "alarm_control_panel.alarm_disarm", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - } - }, + "panels": {"test_template_panel": TEMPLATE_ALARM_CONFIG}, } }, ], @@ -83,30 +113,7 @@ async def test_template_state_text(hass, start_ha): { "alarm_control_panel": { "platform": "template", - "panels": { - "test_template_panel": { - "arm_away": { - "service": "alarm_control_panel.alarm_arm_away", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "arm_home": { - "service": "alarm_control_panel.alarm_arm_home", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "arm_night": { - "service": "alarm_control_panel.alarm_arm_night", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "disarm": { - "service": "alarm_control_panel.alarm_disarm", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - } - }, + "panels": {"test_template_panel": OPTIMISTIC_TEMPLATE_ALARM_CONFIG}, } }, ], @@ -118,13 +125,15 @@ async def test_optimistic_states(hass, start_ha): await hass.async_block_till_done() assert state.state == "unknown" - for func, set_state in [ - (common.async_alarm_arm_away, STATE_ALARM_ARMED_AWAY), - (common.async_alarm_arm_home, STATE_ALARM_ARMED_HOME), - (common.async_alarm_arm_night, STATE_ALARM_ARMED_NIGHT), - (common.async_alarm_disarm, STATE_ALARM_DISARMED), + for service, set_state in [ + ("alarm_arm_away", STATE_ALARM_ARMED_AWAY), + ("alarm_arm_home", STATE_ALARM_ARMED_HOME), + ("alarm_arm_night", STATE_ALARM_ARMED_NIGHT), + ("alarm_disarm", STATE_ALARM_DISARMED), ]: - await func(hass, entity_id=TEMPLATE_NAME) + await hass.services.async_call( + ALARM_DOMAIN, service, {"entity_id": TEMPLATE_NAME}, blocking=True + ) await hass.async_block_till_done() assert hass.states.get(TEMPLATE_NAME).state == set_state @@ -140,26 +149,7 @@ async def test_optimistic_states(hass, start_ha): "panels": { "test_template_panel": { "value_template": "{% if blah %}", - "arm_away": { - "service": "alarm_control_panel.alarm_arm_away", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "arm_home": { - "service": "alarm_control_panel.alarm_arm_home", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "arm_night": { - "service": "alarm_control_panel.alarm_arm_night", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "disarm": { - "service": "alarm_control_panel.alarm_disarm", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, + **OPTIMISTIC_TEMPLATE_ALARM_CONFIG, } }, } @@ -173,26 +163,7 @@ async def test_optimistic_states(hass, start_ha): "panels": { "bad name here": { "value_template": "disarmed", - "arm_away": { - "service": "alarm_control_panel.alarm_arm_away", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "arm_home": { - "service": "alarm_control_panel.alarm_arm_home", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "arm_night": { - "service": "alarm_control_panel.alarm_arm_night", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "disarm": { - "service": "alarm_control_panel.alarm_disarm", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, + **OPTIMISTIC_TEMPLATE_ALARM_CONFIG, } }, } @@ -221,26 +192,7 @@ async def test_optimistic_states(hass, start_ha): "panels": { "test_template_panel": { "value_template": "disarmed", - "arm_away": { - "service": "alarm_control_panel.alarm_arm_away", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "arm_home": { - "service": "alarm_control_panel.alarm_arm_home", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "arm_night": { - "service": "alarm_control_panel.alarm_arm_night", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "disarm": { - "service": "alarm_control_panel.alarm_disarm", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, + **OPTIMISTIC_TEMPLATE_ALARM_CONFIG, "code_format": "bad_format", } }, @@ -267,26 +219,7 @@ async def test_template_syntax_error(hass, msg, start_ha, caplog_setup_text): "test_template_panel": { "name": "Template Alarm Panel", "value_template": "disarmed", - "arm_away": { - "service": "alarm_control_panel.alarm_arm_away", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "arm_home": { - "service": "alarm_control_panel.alarm_arm_home", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "arm_night": { - "service": "alarm_control_panel.alarm_arm_night", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "disarm": { - "service": "alarm_control_panel.alarm_disarm", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, + **OPTIMISTIC_TEMPLATE_ALARM_CONFIG, } }, } @@ -302,131 +235,33 @@ async def test_name(hass, start_ha): @pytest.mark.parametrize("count,domain", [(1, "alarm_control_panel")]) @pytest.mark.parametrize( - "config,func", + "config", [ - ( - { - "alarm_control_panel": { - "platform": "template", - "panels": { - "test_template_panel": { - "value_template": "{{ states('alarm_control_panel.test') }}", - "arm_away": { - "service": "alarm_control_panel.alarm_arm_home", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "arm_home": {"service": "test.automation"}, - "arm_night": { - "service": "alarm_control_panel.alarm_arm_home", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "disarm": { - "service": "alarm_control_panel.alarm_disarm", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - } - }, - } - }, - common.async_alarm_arm_home, - ), - ( - { - "alarm_control_panel": { - "platform": "template", - "panels": { - "test_template_panel": { - "value_template": "{{ states('alarm_control_panel.test') }}", - "arm_home": { - "service": "alarm_control_panel.alarm_arm_home", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "arm_away": {"service": "test.automation"}, - "arm_night": { - "service": "alarm_control_panel.alarm_arm_home", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "disarm": { - "service": "alarm_control_panel.alarm_disarm", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - } - }, - }, - }, - common.async_alarm_arm_away, - ), - ( - { - "alarm_control_panel": { - "platform": "template", - "panels": { - "test_template_panel": { - "value_template": "{{ states('alarm_control_panel.test') }}", - "arm_home": { - "service": "alarm_control_panel.alarm_arm_home", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "arm_night": {"service": "test.automation"}, - "arm_away": { - "service": "alarm_control_panel.alarm_arm_home", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "disarm": { - "service": "alarm_control_panel.alarm_disarm", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - } - }, - } - }, - common.async_alarm_arm_night, - ), - ( - { - "alarm_control_panel": { - "platform": "template", - "panels": { - "test_template_panel": { - "value_template": "{{ states('alarm_control_panel.test') }}", - "arm_home": { - "service": "alarm_control_panel.alarm_arm_home", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "disarm": {"service": "test.automation"}, - "arm_away": { - "service": "alarm_control_panel.alarm_arm_home", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - "arm_night": { - "service": "alarm_control_panel.alarm_disarm", - "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, - }, - } - }, - } - }, - common.async_alarm_disarm, - ), + { + "alarm_control_panel": { + "platform": "template", + "panels": {"test_template_panel": TEMPLATE_ALARM_CONFIG}, + } + }, ], ) -async def test_arm_home_action(hass, func, start_ha, calls): - """Test arm home action.""" - await func(hass, entity_id=TEMPLATE_NAME) +@pytest.mark.parametrize( + "service", + [ + "alarm_arm_home", + "alarm_arm_away", + "alarm_arm_night", + "alarm_disarm", + ], +) +async def test_actions(hass, service, start_ha, service_calls): + """Test alarm actions.""" + await hass.services.async_call( + ALARM_DOMAIN, service, {"entity_id": TEMPLATE_NAME}, blocking=True + ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 + assert service_calls[0].data["service"] == service @pytest.mark.parametrize("count,domain", [(1, "alarm_control_panel")]) From a0f741778aa561c30962648206db09e292be8261 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 12 May 2022 14:12:21 +0200 Subject: [PATCH 0413/3516] Use HVACAction in mqtt (#71726) --- homeassistant/components/mqtt/climate.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index deb1021b9d7..c095054ae9b 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -15,7 +15,6 @@ from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - CURRENT_HVAC_ACTIONS, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, FAN_AUTO, @@ -529,21 +528,23 @@ class MqttClimate(MqttEntity, ClimateEntity): def handle_action_received(msg): """Handle receiving action via MQTT.""" payload = render_template(msg, CONF_ACTION_TEMPLATE) - if payload in CURRENT_HVAC_ACTIONS: - self._action = payload - self.async_write_ha_state() - elif not payload or payload == PAYLOAD_NONE: + if not payload or payload == PAYLOAD_NONE: _LOGGER.debug( "Invalid %s action: %s, ignoring", - CURRENT_HVAC_ACTIONS, + [e.value for e in HVACAction], payload, ) - else: + return + try: + self._action = HVACAction(payload) + except ValueError: _LOGGER.warning( "Invalid %s action: %s", - CURRENT_HVAC_ACTIONS, + [e.value for e in HVACAction], payload, ) + return + self.async_write_ha_state() add_subscription(topics, CONF_ACTION_TOPIC, handle_action_received) From 5a7624bfd49798a6e316f3e7215f4457c449da7b Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Thu, 12 May 2022 15:19:50 +0200 Subject: [PATCH 0414/3516] Bump plugwise to v0.18.2 (#71731) --- homeassistant/components/plugwise/climate.py | 6 +- homeassistant/components/plugwise/const.py | 44 ++-- homeassistant/components/plugwise/entity.py | 4 +- homeassistant/components/plugwise/gateway.py | 2 +- .../components/plugwise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../all_data.json | 193 +++++++----------- .../fixtures/anna_heatpump/all_data.json | 27 +-- .../fixtures/p1v3_full_option/all_data.json | 6 +- .../fixtures/stretch_v31/all_data.json | 71 +++---- tests/components/plugwise/test_diagnostics.py | 193 +++++++----------- 12 files changed, 210 insertions(+), 342 deletions(-) diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 91a56d09ee5..09f5181090c 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -31,7 +31,7 @@ async def async_setup_entry( async_add_entities( PlugwiseClimateEntity(coordinator, device_id) for device_id, device in coordinator.data.devices.items() - if device["class"] in THERMOSTAT_CLASSES + if device["dev_class"] in THERMOSTAT_CLASSES ) @@ -53,9 +53,9 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): # Determine preset modes self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE - if presets := self.device.get("presets"): + if presets := self.device.get("preset_modes"): self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE - self._attr_preset_modes = list(presets) + self._attr_preset_modes = presets # Determine hvac modes and current hvac mode self._attr_hvac_modes = [HVACMode.HEAT] diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index 3bcad0b88aa..63bd2a6d8f1 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -1,51 +1,53 @@ """Constants for Plugwise component.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Final from homeassistant.const import Platform -DOMAIN = "plugwise" +DOMAIN: Final = "plugwise" LOGGER = logging.getLogger(__package__) -API = "api" -FLOW_SMILE = "smile (Adam/Anna/P1)" -FLOW_STRETCH = "stretch (Stretch)" -FLOW_TYPE = "flow_type" -GATEWAY = "gateway" -PW_TYPE = "plugwise_type" -SMILE = "smile" -STRETCH = "stretch" -STRETCH_USERNAME = "stretch" -UNIT_LUMEN = "lm" +API: Final = "api" +FLOW_SMILE: Final = "smile (Adam/Anna/P1)" +FLOW_STRETCH: Final = "stretch (Stretch)" +FLOW_TYPE: Final = "flow_type" +GATEWAY: Final = "gateway" +PW_TYPE: Final = "plugwise_type" +SMILE: Final = "smile" +STRETCH: Final = "stretch" +STRETCH_USERNAME: Final = "stretch" +UNIT_LUMEN: Final = "lm" -PLATFORMS_GATEWAY = [ +PLATFORMS_GATEWAY: Final[list[str]] = [ Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR, - Platform.SWITCH, Platform.SELECT, + Platform.SWITCH, ] -ZEROCONF_MAP = { +ZEROCONF_MAP: Final[dict[str, str]] = { "smile": "P1", "smile_thermo": "Anna", "smile_open_therm": "Adam", "stretch": "Stretch", } - # Default directives -DEFAULT_MAX_TEMP = 30 -DEFAULT_MIN_TEMP = 4 -DEFAULT_PORT = 80 -DEFAULT_SCAN_INTERVAL = { +DEFAULT_MAX_TEMP: Final = 30 +DEFAULT_MIN_TEMP: Final = 4 +DEFAULT_PORT: Final = 80 +DEFAULT_SCAN_INTERVAL: Final[dict[str, timedelta]] = { "power": timedelta(seconds=10), "stretch": timedelta(seconds=60), "thermostat": timedelta(seconds=60), } -DEFAULT_USERNAME = "smile" +DEFAULT_USERNAME: Final = "smile" -THERMOSTAT_CLASSES = [ +THERMOSTAT_CLASSES: Final[list[str]] = [ "thermostat", "thermostatic_radiator_valve", "zone_thermometer", diff --git a/homeassistant/components/plugwise/entity.py b/homeassistant/components/plugwise/entity.py index b0896c3cd6d..491eb7c7db8 100644 --- a/homeassistant/components/plugwise/entity.py +++ b/homeassistant/components/plugwise/entity.py @@ -45,8 +45,8 @@ class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]): manufacturer=data.get("vendor"), model=data.get("model"), name=f"Smile {coordinator.data.gateway['smile_name']}", - sw_version=data.get("fw"), - hw_version=data.get("hw"), + sw_version=data.get("firmware"), + hw_version=data.get("hardware"), ) if device_id != coordinator.data.gateway["gateway_id"]: diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index 648765155e4..7eb2b1371d5 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -113,7 +113,7 @@ def migrate_sensor_entities( # Migrating opentherm_outdoor_temperature to opentherm_outdoor_air_temperature sensor for device_id, device in coordinator.data.devices.items(): - if device["class"] != "heater_central": + if device["dev_class"] != "heater_central": continue old_unique_id = f"{device_id}-outdoor_temperature" diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 68ed6d65647..abf6f27b3fa 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.17.3"], + "requirements": ["plugwise==0.18.2"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 3ecdceca859..ac6727a95aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1242,7 +1242,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.17.3 +plugwise==0.18.2 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c23b9d58593..3289356d23a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -838,7 +838,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.17.3 +plugwise==0.18.2 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json index 4c4776f4063..85e5abb0b17 100644 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json +++ b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json @@ -12,26 +12,19 @@ }, { "df4a4a8169904cdb9c03d61a21f42140": { - "class": "zone_thermostat", - "fw": "2016-10-27T02:00:00+02:00", - "hw": "255", + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", "location": "12493538af164a409c6a1c79e38afe1c", - "mac_address": null, "model": "Lisa", "name": "Zone Lisa Bios", + "zigbee_mac_address": "ABCD012345670A06", "vendor": "Plugwise", "lower_bound": 0.0, "upper_bound": 99.9, "resolution": 0.01, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "away", - "presets": { - "home": [20.0, 22.0], - "asleep": [17.0, 24.0], - "away": [15.0, 25.0], - "vacation": [15.0, 28.0], - "no_frost": [10.0, 30.0] - }, "available_schedules": [ "CV Roan", "Bios Schema met Film Avond", @@ -41,7 +34,7 @@ ], "selected_schedule": "None", "last_used": "Badkamer Schema", - "schedule_temperature": 15.0, + "schedule_temperature": 0.0, "mode": "heat", "sensors": { "temperature": 16.5, @@ -50,13 +43,13 @@ } }, "b310b72a0e354bfab43089919b9a88bf": { - "class": "thermo_sensor", - "fw": "2019-03-27T01:00:00+01:00", - "hw": "1", + "dev_class": "thermo_sensor", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", "location": "c50f167537524366a5af7aa3942feb1e", - "mac_address": null, "model": "Tom/Floor", "name": "Floor kraan", + "zigbee_mac_address": "ABCD012345670A02", "vendor": "Plugwise", "lower_bound": 0.0, "upper_bound": 100.0, @@ -69,13 +62,13 @@ } }, "a2c3583e0a6349358998b760cea82d2a": { - "class": "thermo_sensor", - "fw": "2019-03-27T01:00:00+01:00", - "hw": "1", + "dev_class": "thermo_sensor", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", "location": "12493538af164a409c6a1c79e38afe1c", - "mac_address": null, "model": "Tom/Floor", "name": "Bios Cv Thermostatic Radiator ", + "zigbee_mac_address": "ABCD012345670A09", "vendor": "Plugwise", "lower_bound": 0.0, "upper_bound": 100.0, @@ -89,26 +82,19 @@ } }, "b59bcebaf94b499ea7d46e4a66fb62d8": { - "class": "zone_thermostat", - "fw": "2016-08-02T02:00:00+02:00", - "hw": "255", + "dev_class": "zone_thermostat", + "firmware": "2016-08-02T02:00:00+02:00", + "hardware": "255", "location": "c50f167537524366a5af7aa3942feb1e", - "mac_address": null, "model": "Lisa", "name": "Zone Lisa WK", + "zigbee_mac_address": "ABCD012345670A07", "vendor": "Plugwise", "lower_bound": 0.0, "upper_bound": 99.9, "resolution": 0.01, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "home", - "presets": { - "home": [20.0, 22.0], - "asleep": [17.0, 24.0], - "away": [15.0, 25.0], - "vacation": [15.0, 28.0], - "no_frost": [10.0, 30.0] - }, "available_schedules": [ "CV Roan", "Bios Schema met Film Avond", @@ -118,7 +104,7 @@ ], "selected_schedule": "GF7 Woonkamer", "last_used": "GF7 Woonkamer", - "schedule_temperature": 20.0, + "schedule_temperature": 15.0, "mode": "auto", "sensors": { "temperature": 20.9, @@ -127,15 +113,15 @@ } }, "fe799307f1624099878210aa0b9f1475": { - "class": "gateway", - "fw": "3.0.15", - "hw": "AME Smile 2.0 board", + "dev_class": "gateway", + "firmware": "3.0.15", + "hardware": "AME Smile 2.0 board", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", "mac_address": "012345670001", "model": "Adam", "name": "Adam", - "vendor": "Plugwise B.V.", "zigbee_mac_address": "ABCD012345670101", + "vendor": "Plugwise B.V.", "regulation_mode": "heating", "regulation_modes": [], "binary_sensors": { @@ -146,13 +132,13 @@ } }, "d3da73bde12a47d5a6b8f9dad971f2ec": { - "class": "thermo_sensor", - "fw": "2019-03-27T01:00:00+01:00", - "hw": "1", + "dev_class": "thermo_sensor", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", "location": "82fa13f017d240daa0d0ea1775420f24", - "mac_address": null, "model": "Tom/Floor", "name": "Thermostatic Radiator Jessie", + "zigbee_mac_address": "ABCD012345670A10", "vendor": "Plugwise", "lower_bound": 0.0, "upper_bound": 100.0, @@ -166,15 +152,13 @@ } }, "21f2b542c49845e6bb416884c55778d6": { - "class": "game_console", - "fw": "2019-06-21T02:00:00+02:00", - "hw": null, + "dev_class": "game_console", + "firmware": "2019-06-21T02:00:00+02:00", "location": "cd143c07248f491493cea0533bc3d669", - "mac_address": null, "model": "Plug", "name": "Playstation Smart Plug", - "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A12", + "vendor": "Plugwise", "sensors": { "electricity_consumed": 82.6, "electricity_consumed_interval": 8.6, @@ -187,15 +171,13 @@ } }, "78d1126fc4c743db81b61c20e88342a7": { - "class": "central_heating_pump", - "fw": "2019-06-21T02:00:00+02:00", - "hw": null, + "dev_class": "central_heating_pump", + "firmware": "2019-06-21T02:00:00+02:00", "location": "c50f167537524366a5af7aa3942feb1e", - "mac_address": null, "model": "Plug", "name": "CV Pomp", - "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A05", + "vendor": "Plugwise", "sensors": { "electricity_consumed": 35.6, "electricity_consumed_interval": 7.37, @@ -207,14 +189,10 @@ } }, "90986d591dcd426cae3ec3e8111ff730": { - "class": "heater_central", - "fw": null, - "hw": null, + "dev_class": "heater_central", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", - "mac_address": null, "model": "Unknown", "name": "OnOff", - "vendor": null, "binary_sensors": { "heating_state": true }, @@ -225,15 +203,13 @@ } }, "cd0ddb54ef694e11ac18ed1cbce5dbbd": { - "class": "vcr", - "fw": "2019-06-21T02:00:00+02:00", - "hw": null, + "dev_class": "vcr", + "firmware": "2019-06-21T02:00:00+02:00", "location": "cd143c07248f491493cea0533bc3d669", - "mac_address": null, "model": "Plug", "name": "NAS", - "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A14", + "vendor": "Plugwise", "sensors": { "electricity_consumed": 16.5, "electricity_consumed_interval": 0.5, @@ -246,15 +222,13 @@ } }, "4a810418d5394b3f82727340b91ba740": { - "class": "router", - "fw": "2019-06-21T02:00:00+02:00", - "hw": null, + "dev_class": "router", + "firmware": "2019-06-21T02:00:00+02:00", "location": "cd143c07248f491493cea0533bc3d669", - "mac_address": null, "model": "Plug", "name": "USG Smart Plug", - "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A16", + "vendor": "Plugwise", "sensors": { "electricity_consumed": 8.5, "electricity_consumed_interval": 0.0, @@ -267,15 +241,13 @@ } }, "02cf28bfec924855854c544690a609ef": { - "class": "vcr", - "fw": "2019-06-21T02:00:00+02:00", - "hw": null, + "dev_class": "vcr", + "firmware": "2019-06-21T02:00:00+02:00", "location": "cd143c07248f491493cea0533bc3d669", - "mac_address": null, "model": "Plug", "name": "NVR", - "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A15", + "vendor": "Plugwise", "sensors": { "electricity_consumed": 34.0, "electricity_consumed_interval": 9.15, @@ -288,15 +260,13 @@ } }, "a28f588dc4a049a483fd03a30361ad3a": { - "class": "settop", - "fw": "2019-06-21T02:00:00+02:00", - "hw": null, + "dev_class": "settop", + "firmware": "2019-06-21T02:00:00+02:00", "location": "cd143c07248f491493cea0533bc3d669", - "mac_address": null, "model": "Plug", "name": "Fibaro HC2", - "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A13", + "vendor": "Plugwise", "sensors": { "electricity_consumed": 12.5, "electricity_consumed_interval": 3.8, @@ -309,26 +279,19 @@ } }, "6a3bf693d05e48e0b460c815a4fdd09d": { - "class": "zone_thermostat", - "fw": "2016-10-27T02:00:00+02:00", - "hw": "255", + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", "location": "82fa13f017d240daa0d0ea1775420f24", - "mac_address": null, "model": "Lisa", "name": "Zone Thermostat Jessie", + "zigbee_mac_address": "ABCD012345670A03", "vendor": "Plugwise", "lower_bound": 0.0, "upper_bound": 99.9, "resolution": 0.01, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "asleep", - "presets": { - "home": [20.0, 22.0], - "asleep": [17.0, 24.0], - "away": [15.0, 25.0], - "vacation": [15.0, 28.0], - "no_frost": [10.0, 30.0] - }, "available_schedules": [ "CV Roan", "Bios Schema met Film Avond", @@ -347,13 +310,13 @@ } }, "680423ff840043738f42cc7f1ff97a36": { - "class": "thermo_sensor", - "fw": "2019-03-27T01:00:00+01:00", - "hw": "1", + "dev_class": "thermo_sensor", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", "location": "08963fec7c53423ca5680aa4cb502c63", - "mac_address": null, "model": "Tom/Floor", "name": "Thermostatic Radiator Badkamer", + "zigbee_mac_address": "ABCD012345670A17", "vendor": "Plugwise", "lower_bound": 0.0, "upper_bound": 100.0, @@ -367,26 +330,19 @@ } }, "f1fee6043d3642a9b0a65297455f008e": { - "class": "zone_thermostat", - "fw": "2016-10-27T02:00:00+02:00", - "hw": "255", + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", "location": "08963fec7c53423ca5680aa4cb502c63", - "mac_address": null, "model": "Lisa", "name": "Zone Thermostat Badkamer", + "zigbee_mac_address": "ABCD012345670A08", "vendor": "Plugwise", "lower_bound": 0.0, "upper_bound": 99.9, "resolution": 0.01, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "away", - "presets": { - "home": [20.0, 22.0], - "asleep": [17.0, 24.0], - "away": [15.0, 25.0], - "vacation": [15.0, 28.0], - "no_frost": [10.0, 30.0] - }, "available_schedules": [ "CV Roan", "Bios Schema met Film Avond", @@ -405,15 +361,13 @@ } }, "675416a629f343c495449970e2ca37b5": { - "class": "router", - "fw": "2019-06-21T02:00:00+02:00", - "hw": null, + "dev_class": "router", + "firmware": "2019-06-21T02:00:00+02:00", "location": "cd143c07248f491493cea0533bc3d669", - "mac_address": null, "model": "Plug", "name": "Ziggo Modem", - "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A01", + "vendor": "Plugwise", "sensors": { "electricity_consumed": 12.2, "electricity_consumed_interval": 2.97, @@ -426,26 +380,19 @@ } }, "e7693eb9582644e5b865dba8d4447cf1": { - "class": "thermostatic_radiator_valve", - "fw": "2019-03-27T01:00:00+01:00", - "hw": "1", + "dev_class": "thermostatic_radiator_valve", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", "location": "446ac08dd04d4eff8ac57489757b7314", - "mac_address": null, "model": "Tom/Floor", "name": "CV Kraan Garage", + "zigbee_mac_address": "ABCD012345670A11", "vendor": "Plugwise", "lower_bound": 0.0, "upper_bound": 100.0, "resolution": 0.01, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "no_frost", - "presets": { - "home": [20.0, 22.0], - "asleep": [17.0, 24.0], - "away": [15.0, 25.0], - "vacation": [15.0, 28.0], - "no_frost": [10.0, 30.0] - }, "available_schedules": [ "CV Roan", "Bios Schema met Film Avond", @@ -455,7 +402,7 @@ ], "selected_schedule": "None", "last_used": "Badkamer Schema", - "schedule_temperature": 15.0, + "schedule_temperature": 0.0, "mode": "heat", "sensors": { "temperature": 15.6, diff --git a/tests/components/plugwise/fixtures/anna_heatpump/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump/all_data.json index efc95494cad..cc3f8d8f385 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump/all_data.json @@ -8,19 +8,16 @@ }, { "1cbf783bb11e4a7c8a6843dee3a86927": { - "class": "heater_central", - "fw": null, - "hw": null, + "dev_class": "heater_central", "location": "a57efe5f145f498c9be62a9b63626fbf", - "mac_address": null, "model": "Generic heater", "name": "OpenTherm", "vendor": "Techneco", "maximum_boiler_temperature": 60.0, - "compressor_state": true, "binary_sensors": { "dhw_state": false, "heating_state": true, + "compressor_state": true, "cooling_state": false, "slave_boiler_state": false, "flame_state": false @@ -38,9 +35,9 @@ } }, "015ae9ea3f964e668e490fa39da3870b": { - "class": "gateway", - "fw": "4.0.15", - "hw": "AME Smile 2.0 board", + "dev_class": "gateway", + "firmware": "4.0.15", + "hardware": "AME Smile 2.0 board", "location": "a57efe5f145f498c9be62a9b63626fbf", "mac_address": "012345670001", "model": "Anna", @@ -54,11 +51,10 @@ } }, "3cb70739631c4d17a86b8b12e8a5161b": { - "class": "thermostat", - "fw": "2018-02-08T11:15:53+01:00", - "hw": "6539-1301-5002", + "dev_class": "thermostat", + "firmware": "2018-02-08T11:15:53+01:00", + "hardware": "6539-1301-5002", "location": "c784ee9fdab44e1395b8dee7d7a497d5", - "mac_address": null, "model": "Anna", "name": "Anna", "vendor": "Plugwise", @@ -67,13 +63,6 @@ "resolution": 0.1, "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], "active_preset": "home", - "presets": { - "no_frost": [10.0, 30.0], - "home": [21.0, 22.0], - "away": [20.0, 25.0], - "asleep": [20.5, 24.0], - "vacation": [17.0, 28.0] - }, "available_schedules": ["None"], "selected_schedule": "None", "last_used": null, diff --git a/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json b/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json index f186335fcc9..fbf5aa63a5f 100644 --- a/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json +++ b/tests/components/plugwise/fixtures/p1v3_full_option/all_data.json @@ -6,9 +6,9 @@ }, { "e950c7d5e1ee407a858e2a8b5016c8b3": { - "class": "gateway", - "fw": "3.3.9", - "hw": "AME Smile 2.0 board", + "dev_class": "gateway", + "firmware": "3.3.9", + "hardware": "AME Smile 2.0 board", "location": "cd3e822288064775a7c4afcdd70bdda2", "mac_address": "012345670001", "model": "P1", diff --git a/tests/components/plugwise/fixtures/stretch_v31/all_data.json b/tests/components/plugwise/fixtures/stretch_v31/all_data.json index 08ebab7b148..1ff62e9e619 100644 --- a/tests/components/plugwise/fixtures/stretch_v31/all_data.json +++ b/tests/components/plugwise/fixtures/stretch_v31/all_data.json @@ -6,26 +6,24 @@ }, { "0000aaaa0000aaaa0000aaaa0000aa00": { - "class": "gateway", - "fw": "3.1.11", - "hw": null, - "mac_address": "01:23:45:67:89:AB", + "dev_class": "gateway", + "firmware": "3.1.11", "location": "0000aaaa0000aaaa0000aaaa0000aa00", - "vendor": "Plugwise B.V.", + "mac_address": "01:23:45:67:89:AB", "model": "Stretch", "name": "Stretch", + "vendor": "Plugwise B.V.", "zigbee_mac_address": "ABCD012345670101" }, "5871317346d045bc9f6b987ef25ee638": { - "class": "water_heater_vessel", - "fw": "2011-06-27T10:52:18+02:00", - "hw": "6539-0701-4028", + "dev_class": "water_heater_vessel", + "firmware": "2011-06-27T10:52:18+02:00", + "hardware": "6539-0701-4028", "location": "0000aaaa0000aaaa0000aaaa0000aa00", - "mac_address": null, "model": "Circle type F", "name": "Boiler (1EB31)", - "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A07", + "vendor": "Plugwise", "sensors": { "electricity_consumed": 1.19, "electricity_consumed_interval": 0.0, @@ -37,13 +35,13 @@ } }, "e1c884e7dede431dadee09506ec4f859": { - "class": "refrigerator", - "fw": "2011-06-27T10:47:37+02:00", - "hw": "6539-0700-7330", + "dev_class": "refrigerator", + "firmware": "2011-06-27T10:47:37+02:00", + "hardware": "6539-0700-7330", "location": "0000aaaa0000aaaa0000aaaa0000aa00", - "mac_address": null, "model": "Circle+ type F", "name": "Koelkast (92C4A)", + "zigbee_mac_address": "0123456789AB", "vendor": "Plugwise", "sensors": { "electricity_consumed": 50.5, @@ -56,15 +54,14 @@ } }, "aac7b735042c4832ac9ff33aae4f453b": { - "class": "dishwasher", - "fw": "2011-06-27T10:52:18+02:00", - "hw": "6539-0701-4022", + "dev_class": "dishwasher", + "firmware": "2011-06-27T10:52:18+02:00", + "hardware": "6539-0701-4022", "location": "0000aaaa0000aaaa0000aaaa0000aa00", - "mac_address": null, "model": "Circle type F", "name": "Vaatwasser (2a1ab)", - "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A02", + "vendor": "Plugwise", "sensors": { "electricity_consumed": 0.0, "electricity_consumed_interval": 0.71, @@ -76,15 +73,14 @@ } }, "cfe95cf3de1948c0b8955125bf754614": { - "class": "dryer", - "fw": "2011-06-27T10:52:18+02:00", - "hw": "0000-0440-0107", + "dev_class": "dryer", + "firmware": "2011-06-27T10:52:18+02:00", + "hardware": "0000-0440-0107", "location": "0000aaaa0000aaaa0000aaaa0000aa00", - "mac_address": null, "model": "Circle type F", "name": "Droger (52559)", - "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A04", + "vendor": "Plugwise", "sensors": { "electricity_consumed": 0.0, "electricity_consumed_interval": 0.0, @@ -96,15 +92,14 @@ } }, "059e4d03c7a34d278add5c7a4a781d19": { - "class": "washingmachine", - "fw": "2011-06-27T10:52:18+02:00", - "hw": "0000-0440-0107", + "dev_class": "washingmachine", + "firmware": "2011-06-27T10:52:18+02:00", + "hardware": "0000-0440-0107", "location": "0000aaaa0000aaaa0000aaaa0000aa00", - "mac_address": null, "model": "Circle type F", "name": "Wasmachine (52AC1)", - "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A01", + "vendor": "Plugwise", "sensors": { "electricity_consumed": 0.0, "electricity_consumed_interval": 0.0, @@ -116,22 +111,16 @@ } }, "71e1944f2a944b26ad73323e399efef0": { - "class": "switching", - "fw": null, - "location": null, + "dev_class": "switching", "model": "Switchgroup", "name": "Test", "members": ["5ca521ac179d468e91d772eeeb8a2117"], - "types": ["switch_group"], - "vendor": null, "switches": { "relay": true } }, "d950b314e9d8499f968e6db8d82ef78c": { - "class": "report", - "fw": null, - "location": null, + "dev_class": "report", "model": "Switchgroup", "name": "Stroomvreters", "members": [ @@ -141,24 +130,18 @@ "cfe95cf3de1948c0b8955125bf754614", "e1c884e7dede431dadee09506ec4f859" ], - "types": ["switch_group"], - "vendor": null, "switches": { "relay": true } }, "d03738edfcc947f7b8f4573571d90d2d": { - "class": "switching", - "fw": null, - "location": null, + "dev_class": "switching", "model": "Switchgroup", "name": "Schakel", "members": [ "059e4d03c7a34d278add5c7a4a781d19", "cfe95cf3de1948c0b8955125bf754614" ], - "types": ["switch_group"], - "vendor": null, "switches": { "relay": true } diff --git a/tests/components/plugwise/test_diagnostics.py b/tests/components/plugwise/test_diagnostics.py index d6d1eebaf72..c2bb91746ae 100644 --- a/tests/components/plugwise/test_diagnostics.py +++ b/tests/components/plugwise/test_diagnostics.py @@ -32,26 +32,19 @@ async def test_diagnostics( }, "devices": { "df4a4a8169904cdb9c03d61a21f42140": { - "class": "zone_thermostat", - "fw": "2016-10-27T02:00:00+02:00", - "hw": "255", + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", "location": "12493538af164a409c6a1c79e38afe1c", - "mac_address": None, "model": "Lisa", "name": "Zone Lisa Bios", + "zigbee_mac_address": "ABCD012345670A06", "vendor": "Plugwise", "lower_bound": 0.0, "upper_bound": 99.9, "resolution": 0.01, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "away", - "presets": { - "home": [20.0, 22.0], - "asleep": [17.0, 24.0], - "away": [15.0, 25.0], - "vacation": [15.0, 28.0], - "no_frost": [10.0, 30.0], - }, "available_schedules": [ "CV Roan", "Bios Schema met Film Avond", @@ -61,18 +54,18 @@ async def test_diagnostics( ], "selected_schedule": "None", "last_used": "Badkamer Schema", - "schedule_temperature": 15.0, + "schedule_temperature": 0.0, "mode": "heat", "sensors": {"temperature": 16.5, "setpoint": 13.0, "battery": 67}, }, "b310b72a0e354bfab43089919b9a88bf": { - "class": "thermo_sensor", - "fw": "2019-03-27T01:00:00+01:00", - "hw": "1", + "dev_class": "thermo_sensor", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", "location": "c50f167537524366a5af7aa3942feb1e", - "mac_address": None, "model": "Tom/Floor", "name": "Floor kraan", + "zigbee_mac_address": "ABCD012345670A02", "vendor": "Plugwise", "lower_bound": 0.0, "upper_bound": 100.0, @@ -85,13 +78,13 @@ async def test_diagnostics( }, }, "a2c3583e0a6349358998b760cea82d2a": { - "class": "thermo_sensor", - "fw": "2019-03-27T01:00:00+01:00", - "hw": "1", + "dev_class": "thermo_sensor", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", "location": "12493538af164a409c6a1c79e38afe1c", - "mac_address": None, "model": "Tom/Floor", "name": "Bios Cv Thermostatic Radiator ", + "zigbee_mac_address": "ABCD012345670A09", "vendor": "Plugwise", "lower_bound": 0.0, "upper_bound": 100.0, @@ -105,26 +98,19 @@ async def test_diagnostics( }, }, "b59bcebaf94b499ea7d46e4a66fb62d8": { - "class": "zone_thermostat", - "fw": "2016-08-02T02:00:00+02:00", - "hw": "255", + "dev_class": "zone_thermostat", + "firmware": "2016-08-02T02:00:00+02:00", + "hardware": "255", "location": "c50f167537524366a5af7aa3942feb1e", - "mac_address": None, "model": "Lisa", "name": "Zone Lisa WK", + "zigbee_mac_address": "ABCD012345670A07", "vendor": "Plugwise", "lower_bound": 0.0, "upper_bound": 99.9, "resolution": 0.01, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "home", - "presets": { - "home": [20.0, 22.0], - "asleep": [17.0, 24.0], - "away": [15.0, 25.0], - "vacation": [15.0, 28.0], - "no_frost": [10.0, 30.0], - }, "available_schedules": [ "CV Roan", "Bios Schema met Film Avond", @@ -134,33 +120,33 @@ async def test_diagnostics( ], "selected_schedule": "GF7 Woonkamer", "last_used": "GF7 Woonkamer", - "schedule_temperature": 20.0, + "schedule_temperature": 15.0, "mode": "auto", "sensors": {"temperature": 20.9, "setpoint": 21.5, "battery": 34}, }, "fe799307f1624099878210aa0b9f1475": { - "class": "gateway", - "fw": "3.0.15", - "hw": "AME Smile 2.0 board", + "dev_class": "gateway", + "firmware": "3.0.15", + "hardware": "AME Smile 2.0 board", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", "mac_address": "012345670001", "model": "Adam", "name": "Adam", - "vendor": "Plugwise B.V.", "zigbee_mac_address": "ABCD012345670101", + "vendor": "Plugwise B.V.", "regulation_mode": "heating", "regulation_modes": [], "binary_sensors": {"plugwise_notification": True}, "sensors": {"outdoor_temperature": 7.81}, }, "d3da73bde12a47d5a6b8f9dad971f2ec": { - "class": "thermo_sensor", - "fw": "2019-03-27T01:00:00+01:00", - "hw": "1", + "dev_class": "thermo_sensor", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", "location": "82fa13f017d240daa0d0ea1775420f24", - "mac_address": None, "model": "Tom/Floor", "name": "Thermostatic Radiator Jessie", + "zigbee_mac_address": "ABCD012345670A10", "vendor": "Plugwise", "lower_bound": 0.0, "upper_bound": 100.0, @@ -174,15 +160,13 @@ async def test_diagnostics( }, }, "21f2b542c49845e6bb416884c55778d6": { - "class": "game_console", - "fw": "2019-06-21T02:00:00+02:00", - "hw": None, + "dev_class": "game_console", + "firmware": "2019-06-21T02:00:00+02:00", "location": "cd143c07248f491493cea0533bc3d669", - "mac_address": None, "model": "Plug", "name": "Playstation Smart Plug", - "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A12", + "vendor": "Plugwise", "sensors": { "electricity_consumed": 82.6, "electricity_consumed_interval": 8.6, @@ -192,15 +176,13 @@ async def test_diagnostics( "switches": {"relay": True, "lock": False}, }, "78d1126fc4c743db81b61c20e88342a7": { - "class": "central_heating_pump", - "fw": "2019-06-21T02:00:00+02:00", - "hw": None, + "dev_class": "central_heating_pump", + "firmware": "2019-06-21T02:00:00+02:00", "location": "c50f167537524366a5af7aa3942feb1e", - "mac_address": None, "model": "Plug", "name": "CV Pomp", - "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A05", + "vendor": "Plugwise", "sensors": { "electricity_consumed": 35.6, "electricity_consumed_interval": 7.37, @@ -210,14 +192,10 @@ async def test_diagnostics( "switches": {"relay": True}, }, "90986d591dcd426cae3ec3e8111ff730": { - "class": "heater_central", - "fw": None, - "hw": None, + "dev_class": "heater_central", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d", - "mac_address": None, "model": "Unknown", "name": "OnOff", - "vendor": None, "binary_sensors": {"heating_state": True}, "sensors": { "water_temperature": 70.0, @@ -226,15 +204,13 @@ async def test_diagnostics( }, }, "cd0ddb54ef694e11ac18ed1cbce5dbbd": { - "class": "vcr", - "fw": "2019-06-21T02:00:00+02:00", - "hw": None, + "dev_class": "vcr", + "firmware": "2019-06-21T02:00:00+02:00", "location": "cd143c07248f491493cea0533bc3d669", - "mac_address": None, "model": "Plug", "name": "NAS", - "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A14", + "vendor": "Plugwise", "sensors": { "electricity_consumed": 16.5, "electricity_consumed_interval": 0.5, @@ -244,15 +220,13 @@ async def test_diagnostics( "switches": {"relay": True, "lock": True}, }, "4a810418d5394b3f82727340b91ba740": { - "class": "router", - "fw": "2019-06-21T02:00:00+02:00", - "hw": None, + "dev_class": "router", + "firmware": "2019-06-21T02:00:00+02:00", "location": "cd143c07248f491493cea0533bc3d669", - "mac_address": None, "model": "Plug", "name": "USG Smart Plug", - "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A16", + "vendor": "Plugwise", "sensors": { "electricity_consumed": 8.5, "electricity_consumed_interval": 0.0, @@ -262,15 +236,13 @@ async def test_diagnostics( "switches": {"relay": True, "lock": True}, }, "02cf28bfec924855854c544690a609ef": { - "class": "vcr", - "fw": "2019-06-21T02:00:00+02:00", - "hw": None, + "dev_class": "vcr", + "firmware": "2019-06-21T02:00:00+02:00", "location": "cd143c07248f491493cea0533bc3d669", - "mac_address": None, "model": "Plug", "name": "NVR", - "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A15", + "vendor": "Plugwise", "sensors": { "electricity_consumed": 34.0, "electricity_consumed_interval": 9.15, @@ -280,15 +252,13 @@ async def test_diagnostics( "switches": {"relay": True, "lock": True}, }, "a28f588dc4a049a483fd03a30361ad3a": { - "class": "settop", - "fw": "2019-06-21T02:00:00+02:00", - "hw": None, + "dev_class": "settop", + "firmware": "2019-06-21T02:00:00+02:00", "location": "cd143c07248f491493cea0533bc3d669", - "mac_address": None, "model": "Plug", "name": "Fibaro HC2", - "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A13", + "vendor": "Plugwise", "sensors": { "electricity_consumed": 12.5, "electricity_consumed_interval": 3.8, @@ -298,26 +268,19 @@ async def test_diagnostics( "switches": {"relay": True, "lock": True}, }, "6a3bf693d05e48e0b460c815a4fdd09d": { - "class": "zone_thermostat", - "fw": "2016-10-27T02:00:00+02:00", - "hw": "255", + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", "location": "82fa13f017d240daa0d0ea1775420f24", - "mac_address": None, "model": "Lisa", "name": "Zone Thermostat Jessie", + "zigbee_mac_address": "ABCD012345670A03", "vendor": "Plugwise", "lower_bound": 0.0, "upper_bound": 99.9, "resolution": 0.01, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "asleep", - "presets": { - "home": [20.0, 22.0], - "asleep": [17.0, 24.0], - "away": [15.0, 25.0], - "vacation": [15.0, 28.0], - "no_frost": [10.0, 30.0], - }, "available_schedules": [ "CV Roan", "Bios Schema met Film Avond", @@ -332,13 +295,13 @@ async def test_diagnostics( "sensors": {"temperature": 17.2, "setpoint": 15.0, "battery": 37}, }, "680423ff840043738f42cc7f1ff97a36": { - "class": "thermo_sensor", - "fw": "2019-03-27T01:00:00+01:00", - "hw": "1", + "dev_class": "thermo_sensor", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", "location": "08963fec7c53423ca5680aa4cb502c63", - "mac_address": None, "model": "Tom/Floor", "name": "Thermostatic Radiator Badkamer", + "zigbee_mac_address": "ABCD012345670A17", "vendor": "Plugwise", "lower_bound": 0.0, "upper_bound": 100.0, @@ -352,26 +315,19 @@ async def test_diagnostics( }, }, "f1fee6043d3642a9b0a65297455f008e": { - "class": "zone_thermostat", - "fw": "2016-10-27T02:00:00+02:00", - "hw": "255", + "dev_class": "zone_thermostat", + "firmware": "2016-10-27T02:00:00+02:00", + "hardware": "255", "location": "08963fec7c53423ca5680aa4cb502c63", - "mac_address": None, "model": "Lisa", "name": "Zone Thermostat Badkamer", + "zigbee_mac_address": "ABCD012345670A08", "vendor": "Plugwise", "lower_bound": 0.0, "upper_bound": 99.9, "resolution": 0.01, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "away", - "presets": { - "home": [20.0, 22.0], - "asleep": [17.0, 24.0], - "away": [15.0, 25.0], - "vacation": [15.0, 28.0], - "no_frost": [10.0, 30.0], - }, "available_schedules": [ "CV Roan", "Bios Schema met Film Avond", @@ -386,15 +342,13 @@ async def test_diagnostics( "sensors": {"temperature": 18.9, "setpoint": 14.0, "battery": 92}, }, "675416a629f343c495449970e2ca37b5": { - "class": "router", - "fw": "2019-06-21T02:00:00+02:00", - "hw": None, + "dev_class": "router", + "firmware": "2019-06-21T02:00:00+02:00", "location": "cd143c07248f491493cea0533bc3d669", - "mac_address": None, "model": "Plug", "name": "Ziggo Modem", - "vendor": "Plugwise", "zigbee_mac_address": "ABCD012345670A01", + "vendor": "Plugwise", "sensors": { "electricity_consumed": 12.2, "electricity_consumed_interval": 2.97, @@ -404,26 +358,19 @@ async def test_diagnostics( "switches": {"relay": True, "lock": True}, }, "e7693eb9582644e5b865dba8d4447cf1": { - "class": "thermostatic_radiator_valve", - "fw": "2019-03-27T01:00:00+01:00", - "hw": "1", + "dev_class": "thermostatic_radiator_valve", + "firmware": "2019-03-27T01:00:00+01:00", + "hardware": "1", "location": "446ac08dd04d4eff8ac57489757b7314", - "mac_address": None, "model": "Tom/Floor", "name": "CV Kraan Garage", + "zigbee_mac_address": "ABCD012345670A11", "vendor": "Plugwise", "lower_bound": 0.0, "upper_bound": 100.0, "resolution": 0.01, - "preset_modes": ["home", "asleep", "away", "vacation", "no_frost"], + "preset_modes": ["home", "asleep", "away", "no_frost"], "active_preset": "no_frost", - "presets": { - "home": [20.0, 22.0], - "asleep": [17.0, 24.0], - "away": [15.0, 25.0], - "vacation": [15.0, 28.0], - "no_frost": [10.0, 30.0], - }, "available_schedules": [ "CV Roan", "Bios Schema met Film Avond", @@ -433,7 +380,7 @@ async def test_diagnostics( ], "selected_schedule": "None", "last_used": "Badkamer Schema", - "schedule_temperature": 15.0, + "schedule_temperature": 0.0, "mode": "heat", "sensors": { "temperature": 15.6, From 1ef3800844236a389df93c481b4e5724becf4c2f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 12 May 2022 15:22:57 +0200 Subject: [PATCH 0415/3516] Tweak template cover tests (#71732) --- tests/components/template/test_cover.py | 269 +++++++++--------------- 1 file changed, 98 insertions(+), 171 deletions(-) diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index b4bc00ee6a2..92842dc5bfe 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -4,7 +4,10 @@ import pytest from homeassistant import setup from homeassistant.components.cover import ATTR_POSITION, ATTR_TILT_POSITION, DOMAIN from homeassistant.const import ( + ATTR_DOMAIN, ATTR_ENTITY_ID, + ATTR_SERVICE_DATA, + EVENT_CALL_SERVICE, SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, SERVICE_OPEN_COVER, @@ -22,12 +25,45 @@ from homeassistant.const import ( STATE_OPENING, STATE_UNAVAILABLE, ) +from homeassistant.core import callback from tests.common import assert_setup_component ENTITY_COVER = "cover.test_template_cover" +@pytest.fixture +def service_calls(hass): + """Track service call events for cover.test_state.""" + events = [] + entity_id = "cover.test_state" + + @callback + def capture_events(event): + print(event.data) + if event.data[ATTR_DOMAIN] != DOMAIN: + return + if event.data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID] != [entity_id]: + return + events.append(event) + + hass.bus.async_listen(EVENT_CALL_SERVICE, capture_events) + + return events + + +OPEN_CLOSE_COVER_CONFIG = { + "open_cover": { + "service": "cover.open_cover", + "entity_id": "cover.test_state", + }, + "close_cover": { + "service": "cover.close_cover", + "entity_id": "cover.test_state", + }, +} + + @pytest.mark.parametrize("count,domain", [(1, DOMAIN)]) @pytest.mark.parametrize( "config, states", @@ -38,15 +74,8 @@ ENTITY_COVER = "cover.test_template_cover" "platform": "template", "covers": { "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, "value_template": "{{ states.cover.test_state.state }}", - "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test_state", - }, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", - }, } }, } @@ -90,16 +119,9 @@ ENTITY_COVER = "cover.test_template_cover" "platform": "template", "covers": { "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, "position_template": "{{ states.cover.test.attributes.position }}", "value_template": "{{ states.cover.test_state.state }}", - "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test_state", - }, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", - }, } }, } @@ -148,15 +170,8 @@ async def test_template_state_text(hass, states, start_ha, caplog): "platform": "template", "covers": { "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, "value_template": "{{ 1 == 1 }}", - "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test_state", - }, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", - }, } }, } @@ -178,15 +193,8 @@ async def test_template_state_boolean(hass, start_ha): "platform": "template", "covers": { "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, "position_template": "{{ states.cover.test.attributes.position }}", - "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test", - }, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test", - }, } }, } @@ -202,11 +210,8 @@ async def test_template_position(hass, start_ha): (STATE_CLOSED, 42, STATE_OPEN), (STATE_OPEN, 0.0, STATE_CLOSED), ]: - state = hass.states.async_set("cover.test", set_state) - await hass.async_block_till_done() - entity = hass.states.get("cover.test") attrs["position"] = pos - hass.states.async_set(entity.entity_id, entity.state, attributes=attrs) + hass.states.async_set("cover.test", set_state, attributes=attrs) await hass.async_block_till_done() state = hass.states.get("cover.test_template_cover") assert state.attributes.get("current_position") == pos @@ -222,16 +227,9 @@ async def test_template_position(hass, start_ha): "platform": "template", "covers": { "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, "value_template": "{{ 1 == 1 }}", "tilt_template": "{{ 42 }}", - "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test_state", - }, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", - }, } }, } @@ -253,16 +251,9 @@ async def test_template_tilt(hass, start_ha): "platform": "template", "covers": { "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, "position_template": "{{ -1 }}", "tilt_template": "{{ 110 }}", - "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test_state", - }, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", - }, } }, } @@ -272,20 +263,13 @@ async def test_template_tilt(hass, start_ha): "platform": "template", "covers": { "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, "position_template": "{{ on }}", "tilt_template": "{% if states.cover.test_state.state %}" "on" "{% else %}" "off" "{% endif %}", - "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test_state", - }, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", - }, }, }, } @@ -340,19 +324,15 @@ async def test_template_open_or_position(hass, start_ha, caplog_setup_text): "platform": "template", "covers": { "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, "position_template": "{{ 0 }}", - "open_cover": {"service": "test.automation"}, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", - }, } }, } }, ], ) -async def test_open_action(hass, start_ha, calls): +async def test_open_action(hass, start_ha, service_calls): """Test the open_cover command.""" state = hass.states.get("cover.test_template_cover") assert state.state == STATE_CLOSED @@ -362,7 +342,8 @@ async def test_open_action(hass, start_ha, calls): ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 + assert service_calls[0].data["service"] == "open_cover" @pytest.mark.parametrize("count,domain", [(1, DOMAIN)]) @@ -374,20 +355,19 @@ async def test_open_action(hass, start_ha, calls): "platform": "template", "covers": { "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, "position_template": "{{ 100 }}", - "open_cover": { - "service": "cover.open_cover", + "stop_cover": { + "service": "cover.stop_cover", "entity_id": "cover.test_state", }, - "close_cover": {"service": "test.automation"}, - "stop_cover": {"service": "test.automation"}, } }, } }, ], ) -async def test_close_stop_action(hass, start_ha, calls): +async def test_close_stop_action(hass, start_ha, service_calls): """Test the close-cover and stop_cover commands.""" state = hass.states.get("cover.test_template_cover") assert state.state == STATE_OPEN @@ -402,7 +382,9 @@ async def test_close_stop_action(hass, start_ha, calls): ) await hass.async_block_till_done() - assert len(calls) == 2 + assert len(service_calls) == 2 + assert service_calls[0].data["service"] == "close_cover" + assert service_calls[1].data["service"] == "stop_cover" @pytest.mark.parametrize("count,domain", [(1, "input_number")]) @@ -412,7 +394,7 @@ async def test_close_stop_action(hass, start_ha, calls): {"input_number": {"test": {"min": "0", "max": "100", "initial": "42"}}}, ], ) -async def test_set_position(hass, start_ha, calls): +async def test_set_position(hass, start_ha, service_calls): """Test the set_position command.""" with assert_setup_component(1, "cover"): assert await setup.async_setup_component( @@ -423,11 +405,10 @@ async def test_set_position(hass, start_ha, calls): "platform": "template", "covers": { "test_template_cover": { - "position_template": "{{ states.input_number.test.state | int }}", "set_cover_position": { - "service": "input_number.set_value", - "entity_id": "input_number.test", - "data_template": {"value": "{{ position }}"}, + "service": "cover.set_cover_position", + "entity_id": "cover.test_state", + "data_template": {"position": "{{ position }}"}, }, } }, @@ -450,6 +431,9 @@ async def test_set_position(hass, start_ha, calls): await hass.async_block_till_done() state = hass.states.get("cover.test_template_cover") assert state.attributes.get("current_position") == 100.0 + assert len(service_calls) == 1 + assert service_calls[-1].data["service"] == "set_cover_position" + assert service_calls[-1].data["service_data"]["position"] == 100 await hass.services.async_call( DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True @@ -457,6 +441,9 @@ async def test_set_position(hass, start_ha, calls): await hass.async_block_till_done() state = hass.states.get("cover.test_template_cover") assert state.attributes.get("current_position") == 0.0 + assert len(service_calls) == 2 + assert service_calls[-1].data["service"] == "set_cover_position" + assert service_calls[-1].data["service_data"]["position"] == 0 await hass.services.async_call( DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True @@ -464,6 +451,9 @@ async def test_set_position(hass, start_ha, calls): await hass.async_block_till_done() state = hass.states.get("cover.test_template_cover") assert state.attributes.get("current_position") == 100.0 + assert len(service_calls) == 3 + assert service_calls[-1].data["service"] == "set_cover_position" + assert service_calls[-1].data["service_data"]["position"] == 100 await hass.services.async_call( DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True @@ -471,6 +461,9 @@ async def test_set_position(hass, start_ha, calls): await hass.async_block_till_done() state = hass.states.get("cover.test_template_cover") assert state.attributes.get("current_position") == 0.0 + assert len(service_calls) == 4 + assert service_calls[-1].data["service"] == "set_cover_position" + assert service_calls[-1].data["service_data"]["position"] == 0 await hass.services.async_call( DOMAIN, @@ -481,6 +474,9 @@ async def test_set_position(hass, start_ha, calls): await hass.async_block_till_done() state = hass.states.get("cover.test_template_cover") assert state.attributes.get("current_position") == 25.0 + assert len(service_calls) == 5 + assert service_calls[-1].data["service"] == "set_cover_position" + assert service_calls[-1].data["service_data"]["position"] == 25 @pytest.mark.parametrize("count,domain", [(1, DOMAIN)]) @@ -492,16 +488,12 @@ async def test_set_position(hass, start_ha, calls): "platform": "template", "covers": { "test_template_cover": { - "position_template": "{{ 100 }}", - "open_cover": { - "service": "cover.open_cover", + **OPEN_CLOSE_COVER_CONFIG, + "set_cover_tilt_position": { + "service": "cover.set_cover_tilt_position", "entity_id": "cover.test_state", + "data_template": {"tilt_position": "{{ tilt }}"}, }, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", - }, - "set_cover_tilt_position": {"service": "test.automation"}, } }, } @@ -509,17 +501,20 @@ async def test_set_position(hass, start_ha, calls): ], ) @pytest.mark.parametrize( - "service,attr", + "service,attr,tilt_position", [ ( SERVICE_SET_COVER_TILT_POSITION, {ATTR_ENTITY_ID: ENTITY_COVER, ATTR_TILT_POSITION: 42}, + 42, ), - (SERVICE_OPEN_COVER_TILT, {ATTR_ENTITY_ID: ENTITY_COVER}), - (SERVICE_CLOSE_COVER_TILT, {ATTR_ENTITY_ID: ENTITY_COVER}), + (SERVICE_OPEN_COVER_TILT, {ATTR_ENTITY_ID: ENTITY_COVER}, 100), + (SERVICE_CLOSE_COVER_TILT, {ATTR_ENTITY_ID: ENTITY_COVER}, 0), ], ) -async def test_set_tilt_position(hass, service, attr, start_ha, calls): +async def test_set_tilt_position( + hass, service, attr, start_ha, service_calls, tilt_position +): """Test the set_tilt_position command.""" await hass.services.async_call( DOMAIN, @@ -529,7 +524,9 @@ async def test_set_tilt_position(hass, service, attr, start_ha, calls): ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 + assert service_calls[-1].data["service"] == "set_cover_tilt_position" + assert service_calls[-1].data["service_data"]["tilt_position"] == tilt_position @pytest.mark.parametrize("count,domain", [(1, DOMAIN)]) @@ -633,15 +630,8 @@ async def test_set_tilt_position_optimistic(hass, start_ha, calls): "platform": "template", "covers": { "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, "value_template": "{{ states.cover.test_state.state }}", - "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test_state", - }, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", - }, "icon_template": "{% if states.cover.test_state.state %}" "mdi:check" "{% endif %}", @@ -673,15 +663,8 @@ async def test_icon_template(hass, start_ha): "platform": "template", "covers": { "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, "value_template": "{{ states.cover.test_state.state }}", - "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test_state", - }, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", - }, "entity_picture_template": "{% if states.cover.test_state.state %}" "/local/cover.png" "{% endif %}", @@ -713,15 +696,8 @@ async def test_entity_picture_template(hass, start_ha): "platform": "template", "covers": { "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, "value_template": "open", - "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test_state", - }, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", - }, "availability_template": "{{ is_state('availability_state.state','on') }}", } }, @@ -751,15 +727,8 @@ async def test_availability_template(hass, start_ha): "platform": "template", "covers": { "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, "value_template": "open", - "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test_state", - }, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", - }, } }, } @@ -781,16 +750,9 @@ async def test_availability_without_availability_template(hass, start_ha): "platform": "template", "covers": { "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, "availability_template": "{{ x - 12 }}", "value_template": "open", - "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test_state", - }, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", - }, } }, } @@ -814,16 +776,9 @@ async def test_invalid_availability_template_keeps_component_available( "platform": "template", "covers": { "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, "value_template": "{{ states.cover.test_state.state }}", "device_class": "door", - "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test_state", - }, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", - }, } }, } @@ -845,16 +800,9 @@ async def test_device_class(hass, start_ha): "platform": "template", "covers": { "test_template_cover": { + **OPEN_CLOSE_COVER_CONFIG, "value_template": "{{ states.cover.test_state.state }}", "device_class": "barnacle_bill", - "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test_state", - }, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", - }, } }, } @@ -876,28 +824,14 @@ async def test_invalid_device_class(hass, start_ha): "platform": "template", "covers": { "test_template_cover_01": { + **OPEN_CLOSE_COVER_CONFIG, "unique_id": "not-so-unique-anymore", "value_template": "{{ true }}", - "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test_state", - }, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", - }, }, "test_template_cover_02": { + **OPEN_CLOSE_COVER_CONFIG, "unique_id": "not-so-unique-anymore", "value_template": "{{ false }}", - "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test_state", - }, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", - }, }, }, } @@ -918,16 +852,9 @@ async def test_unique_id(hass, start_ha): "platform": "template", "covers": { "garage_door": { + **OPEN_CLOSE_COVER_CONFIG, "friendly_name": "Garage Door", "value_template": "{{ is_state('binary_sensor.garage_door_sensor', 'off') }}", - "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test_state", - }, - "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", - }, }, }, } From b70e97e949ca73fe57849625c0b0c51f0b8796f7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 12 May 2022 16:04:01 +0200 Subject: [PATCH 0416/3516] Remove unused calls fixture from template tests (#71735) --- tests/components/template/test_button.py | 18 +++++------------- tests/components/template/test_fan.py | 2 +- tests/components/template/test_number.py | 21 +++++---------------- tests/components/template/test_select.py | 24 ++++++------------------ 4 files changed, 17 insertions(+), 48 deletions(-) diff --git a/tests/components/template/test_button.py b/tests/components/template/test_button.py index aa671bebd89..25bced16ed2 100644 --- a/tests/components/template/test_button.py +++ b/tests/components/template/test_button.py @@ -2,8 +2,6 @@ import datetime as dt from unittest.mock import patch -import pytest - from homeassistant import setup from homeassistant.components.button.const import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS from homeassistant.components.template.button import DEFAULT_NAME @@ -16,19 +14,13 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity_registry import async_get -from tests.common import assert_setup_component, async_mock_service +from tests.common import assert_setup_component _TEST_BUTTON = "button.template_button" _TEST_OPTIONS_BUTTON = "button.test" -@pytest.fixture -def calls(hass): - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - -async def test_missing_optional_config(hass, calls): +async def test_missing_optional_config(hass): """Test: missing optional template is ok.""" with assert_setup_component(1, "template"): assert await setup.async_setup_component( @@ -50,7 +42,7 @@ async def test_missing_optional_config(hass, calls): _verify(hass, STATE_UNKNOWN) -async def test_missing_required_keys(hass, calls): +async def test_missing_required_keys(hass): """Test: missing required fields will fail.""" with assert_setup_component(0, "template"): assert await setup.async_setup_component( @@ -128,7 +120,7 @@ async def test_all_optional_config(hass, calls): assert er.async_get_entity_id("button", "template", "test-test") -async def test_name_template(hass, calls): +async def test_name_template(hass): """Test: name template.""" with assert_setup_component(1, "template"): assert await setup.async_setup_component( @@ -158,7 +150,7 @@ async def test_name_template(hass, calls): ) -async def test_unique_id(hass, calls): +async def test_unique_id(hass): """Test: unique id is ok.""" with assert_setup_component(1, "template"): assert await setup.async_setup_component( diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index ccd273571b5..30d5222024d 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -396,7 +396,7 @@ async def test_on_off(hass): _verify(hass, state, 0, None, None, None) -async def test_set_invalid_direction_from_initial_stage(hass, calls): +async def test_set_invalid_direction_from_initial_stage(hass): """Test set invalid direction when fan is in initial state.""" await _register_components(hass) diff --git a/tests/components/template/test_number.py b/tests/components/template/test_number.py index 98460335047..6c47fdd2abc 100644 --- a/tests/components/template/test_number.py +++ b/tests/components/template/test_number.py @@ -1,5 +1,4 @@ """The tests for the Template number platform.""" -import pytest from homeassistant import setup from homeassistant.components.input_number import ( @@ -19,11 +18,7 @@ from homeassistant.const import ATTR_ICON, CONF_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import Context from homeassistant.helpers.entity_registry import async_get -from tests.common import ( - assert_setup_component, - async_capture_events, - async_mock_service, -) +from tests.common import assert_setup_component, async_capture_events _TEST_NUMBER = "number.template_number" # Represent for number's value @@ -47,13 +42,7 @@ _VALUE_INPUT_NUMBER_CONFIG = { } -@pytest.fixture -def calls(hass): - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - -async def test_missing_optional_config(hass, calls): +async def test_missing_optional_config(hass): """Test: missing optional template is ok.""" with assert_setup_component(1, "template"): assert await setup.async_setup_component( @@ -77,7 +66,7 @@ async def test_missing_optional_config(hass, calls): _verify(hass, 4, 1, 0.0, 100.0) -async def test_missing_required_keys(hass, calls): +async def test_missing_required_keys(hass): """Test: missing required fields will fail.""" with assert_setup_component(0, "template"): assert await setup.async_setup_component( @@ -112,7 +101,7 @@ async def test_missing_required_keys(hass, calls): assert hass.states.async_all("number") == [] -async def test_all_optional_config(hass, calls): +async def test_all_optional_config(hass): """Test: including all optional templates is ok.""" with assert_setup_component(1, "template"): assert await setup.async_setup_component( @@ -138,7 +127,7 @@ async def test_all_optional_config(hass, calls): _verify(hass, 4, 1, 3, 5) -async def test_templates_with_entities(hass, calls): +async def test_templates_with_entities(hass): """Test templates with values from other entities.""" with assert_setup_component(4, "input_number"): assert await setup.async_setup_component( diff --git a/tests/components/template/test_select.py b/tests/components/template/test_select.py index 66f67d93754..de41ccedbb1 100644 --- a/tests/components/template/test_select.py +++ b/tests/components/template/test_select.py @@ -1,6 +1,4 @@ """The tests for the Template select platform.""" -import pytest - from homeassistant import setup from homeassistant.components.input_select import ( ATTR_OPTION as INPUT_SELECT_ATTR_OPTION, @@ -19,24 +17,14 @@ from homeassistant.const import ATTR_ICON, CONF_ENTITY_ID, STATE_UNKNOWN from homeassistant.core import Context from homeassistant.helpers.entity_registry import async_get -from tests.common import ( - assert_setup_component, - async_capture_events, - async_mock_service, -) +from tests.common import assert_setup_component, async_capture_events _TEST_SELECT = "select.template_select" # Represent for select's current_option _OPTION_INPUT_SELECT = "input_select.option" -@pytest.fixture -def calls(hass): - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") - - -async def test_missing_optional_config(hass, calls): +async def test_missing_optional_config(hass): """Test: missing optional template is ok.""" with assert_setup_component(1, "template"): assert await setup.async_setup_component( @@ -60,7 +48,7 @@ async def test_missing_optional_config(hass, calls): _verify(hass, "a", ["a", "b"]) -async def test_multiple_configs(hass, calls): +async def test_multiple_configs(hass): """Test: multiple select entities get created.""" with assert_setup_component(1, "template"): assert await setup.async_setup_component( @@ -92,7 +80,7 @@ async def test_multiple_configs(hass, calls): _verify(hass, "a", ["a", "b"], f"{_TEST_SELECT}_2") -async def test_missing_required_keys(hass, calls): +async def test_missing_required_keys(hass): """Test: missing required fields will fail.""" with assert_setup_component(0, "template"): assert await setup.async_setup_component( @@ -143,7 +131,7 @@ async def test_missing_required_keys(hass, calls): assert hass.states.async_all("select") == [] -async def test_templates_with_entities(hass, calls): +async def test_templates_with_entities(hass): """Test templates with values from other entities.""" with assert_setup_component(1, "input_select"): assert await setup.async_setup_component( @@ -290,7 +278,7 @@ def _verify(hass, expected_current_option, expected_options, entity_name=_TEST_S assert attributes.get(SELECT_ATTR_OPTIONS) == expected_options -async def test_template_icon_with_entities(hass, calls): +async def test_template_icon_with_entities(hass): """Test templates with values from other entities.""" with assert_setup_component(1, "input_select"): assert await setup.async_setup_component( From 1cb00cbb79a69a2ca44849226ced5275cc695847 Mon Sep 17 00:00:00 2001 From: Marvin ROGER Date: Thu, 12 May 2022 16:23:18 +0200 Subject: [PATCH 0417/3516] Fix timezone issue on onvif integration (#70473) --- homeassistant/components/onvif/device.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index f45362c6e6c..d376b7fe258 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -165,17 +165,13 @@ class ONVIFDevice: ) return + tzone = dt_util.DEFAULT_TIME_ZONE + cdate = device_time.LocalDateTime if device_time.UTCDateTime: tzone = dt_util.UTC cdate = device_time.UTCDateTime - else: - tzone = ( - dt_util.get_time_zone( - device_time.TimeZone or str(dt_util.DEFAULT_TIME_ZONE) - ) - or dt_util.DEFAULT_TIME_ZONE - ) - cdate = device_time.LocalDateTime + elif device_time.TimeZone: + tzone = dt_util.get_time_zone(device_time.TimeZone.TZ) or tzone if cdate is None: LOGGER.warning("Could not retrieve date/time on this camera") From 35e4f11e0bc7c57f7da10d99c26bba3901969b89 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 12 May 2022 16:29:48 +0200 Subject: [PATCH 0418/3516] Tweak template lock tests (#71734) --- tests/components/template/test_lock.py | 143 +++++++++++-------------- 1 file changed, 62 insertions(+), 81 deletions(-) diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index 80c83e0885a..e53f6660162 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -3,7 +3,48 @@ import pytest from homeassistant import setup from homeassistant.components import lock -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_DOMAIN, + ATTR_ENTITY_ID, + ATTR_SERVICE_DATA, + EVENT_CALL_SERVICE, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import callback + + +@pytest.fixture +def service_calls(hass): + """Track service call events for switch.test_state.""" + events = [] + entity_id = "switch.test_state" + + @callback + def capture_events(event): + if event.data[ATTR_DOMAIN] != "switch": + return + if event.data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID] != [entity_id]: + return + events.append(event) + + hass.bus.async_listen(EVENT_CALL_SERVICE, capture_events) + + return events + + +OPTIMISTIC_LOCK_CONFIG = { + "platform": "template", + "lock": { + "service": "switch.turn_on", + "entity_id": "switch.test_state", + }, + "unlock": { + "service": "switch.turn_off", + "entity_id": "switch.test_state", + }, +} @pytest.mark.parametrize("count,domain", [(1, lock.DOMAIN)]) @@ -12,17 +53,9 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVA [ { lock.DOMAIN: { - "platform": "template", + **OPTIMISTIC_LOCK_CONFIG, "name": "Test template lock", "value_template": "{{ states.switch.test_state.state }}", - "lock": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "unlock": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, } }, ], @@ -48,16 +81,8 @@ async def test_template_state(hass, start_ha): [ { lock.DOMAIN: { - "platform": "template", + **OPTIMISTIC_LOCK_CONFIG, "value_template": "{{ 1 == 1 }}", - "lock": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "unlock": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, } }, ], @@ -74,16 +99,8 @@ async def test_template_state_boolean_on(hass, start_ha): [ { lock.DOMAIN: { - "platform": "template", + **OPTIMISTIC_LOCK_CONFIG, "value_template": "{{ 1 == 2 }}", - "lock": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "unlock": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, } }, ], @@ -155,16 +172,8 @@ async def test_template_syntax_error(hass, start_ha): [ { lock.DOMAIN: { - "platform": "template", + **OPTIMISTIC_LOCK_CONFIG, "value_template": "{{ 1 + 1 }}", - "lock": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "unlock": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, } }, ], @@ -186,19 +195,15 @@ async def test_template_static(hass, start_ha): [ { lock.DOMAIN: { - "platform": "template", + **OPTIMISTIC_LOCK_CONFIG, "value_template": "{{ states.switch.test_state.state }}", - "lock": {"service": "test.automation"}, - "unlock": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, } }, ], ) -async def test_lock_action(hass, start_ha, calls): +async def test_lock_action(hass, start_ha, service_calls): """Test lock action.""" + await setup.async_setup_component(hass, "switch", {}) hass.states.async_set("switch.test_state", STATE_OFF) await hass.async_block_till_done() @@ -210,7 +215,8 @@ async def test_lock_action(hass, start_ha, calls): ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 + assert service_calls[-1].data["service"] == "turn_on" @pytest.mark.parametrize("count,domain", [(1, lock.DOMAIN)]) @@ -219,19 +225,15 @@ async def test_lock_action(hass, start_ha, calls): [ { lock.DOMAIN: { - "platform": "template", + **OPTIMISTIC_LOCK_CONFIG, "value_template": "{{ states.switch.test_state.state }}", - "lock": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "unlock": {"service": "test.automation"}, } }, ], ) -async def test_unlock_action(hass, start_ha, calls): +async def test_unlock_action(hass, start_ha, service_calls): """Test unlock action.""" + await setup.async_setup_component(hass, "switch", {}) hass.states.async_set("switch.test_state", STATE_ON) await hass.async_block_till_done() @@ -243,7 +245,8 @@ async def test_unlock_action(hass, start_ha, calls): ) await hass.async_block_till_done() - assert len(calls) == 1 + assert len(service_calls) == 1 + assert service_calls[-1].data["service"] == "turn_off" @pytest.mark.parametrize("count,domain", [(1, lock.DOMAIN)]) @@ -252,10 +255,8 @@ async def test_unlock_action(hass, start_ha, calls): [ { lock.DOMAIN: { - "platform": "template", + **OPTIMISTIC_LOCK_CONFIG, "value_template": "{{ states.input_select.test_state.state }}", - "lock": {"service": "test.automation"}, - "unlock": {"service": "test.automation"}, } }, ], @@ -264,7 +265,7 @@ async def test_unlock_action(hass, start_ha, calls): "test_state", [lock.STATE_UNLOCKING, lock.STATE_LOCKING, lock.STATE_JAMMED] ) async def test_lock_state(hass, test_state, start_ha): - """Test unlocking.""" + """Test value template.""" hass.states.async_set("input_select.test_state", test_state) await hass.async_block_till_done() @@ -278,13 +279,8 @@ async def test_lock_state(hass, test_state, start_ha): [ { lock.DOMAIN: { - "platform": "template", + **OPTIMISTIC_LOCK_CONFIG, "value_template": "{{ states('switch.test_state') }}", - "lock": {"service": "switch.turn_on", "entity_id": "switch.test_state"}, - "unlock": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, "availability_template": "{{ is_state('availability_state.state', 'on') }}", } }, @@ -313,14 +309,9 @@ async def test_available_template_with_entities(hass, start_ha): [ { lock.DOMAIN: { - "platform": "template", + **OPTIMISTIC_LOCK_CONFIG, "value_template": "{{ 1 + 1 }}", "availability_template": "{{ x - 12 }}", - "lock": {"service": "switch.turn_on", "entity_id": "switch.test_state"}, - "unlock": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, } }, ], @@ -339,15 +330,10 @@ async def test_invalid_availability_template_keeps_component_available( [ { lock.DOMAIN: { - "platform": "template", + **OPTIMISTIC_LOCK_CONFIG, "name": "test_template_lock_01", "unique_id": "not-so-unique-anymore", "value_template": "{{ true }}", - "lock": {"service": "switch.turn_on", "entity_id": "switch.test_state"}, - "unlock": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, } }, ], @@ -359,15 +345,10 @@ async def test_unique_id(hass, start_ha): lock.DOMAIN, { "lock": { - "platform": "template", + **OPTIMISTIC_LOCK_CONFIG, "name": "test_template_lock_02", "unique_id": "not-so-unique-anymore", "value_template": "{{ false }}", - "lock": {"service": "switch.turn_on", "entity_id": "switch.test_state"}, - "unlock": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, }, }, ) From 22930457c5512c7a8320267ba724b6f506e2b82a Mon Sep 17 00:00:00 2001 From: Marvin ROGER Date: Thu, 12 May 2022 16:23:18 +0200 Subject: [PATCH 0419/3516] Fix timezone issue on onvif integration (#70473) --- homeassistant/components/onvif/device.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index f45362c6e6c..d376b7fe258 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -165,17 +165,13 @@ class ONVIFDevice: ) return + tzone = dt_util.DEFAULT_TIME_ZONE + cdate = device_time.LocalDateTime if device_time.UTCDateTime: tzone = dt_util.UTC cdate = device_time.UTCDateTime - else: - tzone = ( - dt_util.get_time_zone( - device_time.TimeZone or str(dt_util.DEFAULT_TIME_ZONE) - ) - or dt_util.DEFAULT_TIME_ZONE - ) - cdate = device_time.LocalDateTime + elif device_time.TimeZone: + tzone = dt_util.get_time_zone(device_time.TimeZone.TZ) or tzone if cdate is None: LOGGER.warning("Could not retrieve date/time on this camera") From 4a544254266292e8cb892b80595b05b7ebb395ef Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Mon, 9 May 2022 14:20:45 -0400 Subject: [PATCH 0420/3516] Fix Insteon issue with dimmer default on level (#71426) --- homeassistant/components/insteon/insteon_entity.py | 5 ++--- homeassistant/components/insteon/light.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index 60935f3f951..67d30ba8cad 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -153,10 +153,9 @@ class InsteonEntity(Entity): def get_device_property(self, name: str): """Get a single Insteon device property value (raw).""" - value = None if (prop := self._insteon_device.properties.get(name)) is not None: - value = prop.value if prop.new_value is None else prop.new_value - return value + return prop.value + return None def _get_label(self): """Get the device label for grouped devices.""" diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py index 05ad9794042..bf8b693b103 100644 --- a/homeassistant/components/insteon/light.py +++ b/homeassistant/components/insteon/light.py @@ -58,9 +58,9 @@ class InsteonDimmerEntity(InsteonEntity, LightEntity): """Turn light on.""" if ATTR_BRIGHTNESS in kwargs: brightness = int(kwargs[ATTR_BRIGHTNESS]) - else: + elif self._insteon_device_group.group == 1: brightness = self.get_device_property(ON_LEVEL) - if brightness is not None: + if brightness: await self._insteon_device.async_on( on_level=brightness, group=self._insteon_device_group.group ) From b213f221b5eaeb7f105350556bd65af2aa589301 Mon Sep 17 00:00:00 2001 From: Shai Ungar Date: Mon, 9 May 2022 10:27:23 +0300 Subject: [PATCH 0421/3516] Migrate sabnzbd sensors unique ids (#71455) * Migrate sensors unique ids 1. migrate sensors to have unique id constructed also from entry_id 2. add migration flow in init 3. bump config flow to version 2 4. add tests for migration * move migrate to async_setup_entry * 1. Use the entity registry api in tests 2. Set up the config entry and not use integration directly 3. remove patch for entity registry * fix too many lines * Update tests/components/sabnzbd/test_init.py Co-authored-by: Martin Hjelmare * Update tests/components/sabnzbd/test_init.py Co-authored-by: Martin Hjelmare * Update tests/components/sabnzbd/test_init.py Co-authored-by: Martin Hjelmare * Update tests/components/sabnzbd/test_init.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/sabnzbd/__init__.py | 56 +++++++++++++ homeassistant/components/sabnzbd/sensor.py | 35 +++++--- tests/components/sabnzbd/test_config_flow.py | 1 - tests/components/sabnzbd/test_init.py | 85 ++++++++++++++++++++ 4 files changed, 164 insertions(+), 13 deletions(-) create mode 100644 tests/components/sabnzbd/test_init.py diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index aca50e404a2..3fa5054a5f0 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -1,4 +1,6 @@ """Support for monitoring an SABnzbd NZB client.""" +from __future__ import annotations + from collections.abc import Callable import logging @@ -19,7 +21,9 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import async_get from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType @@ -123,8 +127,56 @@ def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) raise ValueError(f"No api for API key: {call_data_api_key}") +def update_device_identifiers(hass: HomeAssistant, entry: ConfigEntry): + """Update device identifiers to new identifiers.""" + device_registry = async_get(hass) + device_entry = device_registry.async_get_device({(DOMAIN, DOMAIN)}) + if device_entry and entry.entry_id in device_entry.config_entries: + new_identifiers = {(DOMAIN, entry.entry_id)} + _LOGGER.debug( + "Updating device id <%s> with new identifiers <%s>", + device_entry.id, + new_identifiers, + ) + device_registry.async_update_device( + device_entry.id, new_identifiers=new_identifiers + ) + + +async def migrate_unique_id(hass: HomeAssistant, entry: ConfigEntry): + """Migrate entities to new unique ids (with entry_id).""" + + @callback + def async_migrate_callback(entity_entry: RegistryEntry) -> dict | None: + """ + Define a callback to migrate appropriate SabnzbdSensor entities to new unique IDs. + + Old: description.key + New: {entry_id}_description.key + """ + entry_id = entity_entry.config_entry_id + if entry_id is None: + return None + if entity_entry.unique_id.startswith(entry_id): + return None + + new_unique_id = f"{entry_id}_{entity_entry.unique_id}" + + _LOGGER.debug( + "Migrating entity %s from old unique ID '%s' to new unique ID '%s'", + entity_entry.entity_id, + entity_entry.unique_id, + new_unique_id, + ) + + return {"new_unique_id": new_unique_id} + + await async_migrate_entries(hass, entry.entry_id, async_migrate_callback) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the SabNzbd Component.""" + sab_api = await get_client(hass, entry.data) if not sab_api: raise ConfigEntryNotReady @@ -137,6 +189,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: KEY_NAME: entry.data[CONF_NAME], } + await migrate_unique_id(hass, entry) + update_device_identifiers(hass, entry) + @callback def extract_api(func: Callable) -> Callable: """Define a decorator to get the correct api for a service call.""" @@ -188,6 +243,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error(err) async_track_time_interval(hass, async_update_sabnzbd, UPDATE_INTERVAL) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index 539eaa4f097..ebdb9190ed9 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -113,11 +113,16 @@ async def async_setup_entry( ) -> None: """Set up a Sabnzbd sensor entry.""" - sab_api_data = hass.data[DOMAIN][config_entry.entry_id][KEY_API_DATA] - client_name = hass.data[DOMAIN][config_entry.entry_id][KEY_NAME] + entry_id = config_entry.entry_id + + sab_api_data = hass.data[DOMAIN][entry_id][KEY_API_DATA] + client_name = hass.data[DOMAIN][entry_id][KEY_NAME] async_add_entities( - [SabnzbdSensor(sab_api_data, client_name, sensor) for sensor in SENSOR_TYPES] + [ + SabnzbdSensor(sab_api_data, client_name, sensor, entry_id) + for sensor in SENSOR_TYPES + ] ) @@ -128,17 +133,21 @@ class SabnzbdSensor(SensorEntity): _attr_should_poll = False def __init__( - self, sabnzbd_api_data, client_name, description: SabnzbdSensorEntityDescription + self, + sabnzbd_api_data, + client_name, + description: SabnzbdSensorEntityDescription, + entry_id, ): """Initialize the sensor.""" - unique_id = description.key - self._attr_unique_id = unique_id + + self._attr_unique_id = f"{entry_id}_{description.key}" self.entity_description = description self._sabnzbd_api = sabnzbd_api_data self._attr_name = f"{client_name} {description.name}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, DOMAIN)}, + identifiers={(DOMAIN, entry_id)}, name=DEFAULT_NAME, ) @@ -156,9 +165,11 @@ class SabnzbdSensor(SensorEntity): self.entity_description.key ) - if self.entity_description.key == SPEED_KEY: - self._attr_native_value = round(float(self._attr_native_value) / 1024, 1) - elif "size" in self.entity_description.key: - self._attr_native_value = round(float(self._attr_native_value), 2) - + if self._attr_native_value is not None: + if self.entity_description.key == SPEED_KEY: + self._attr_native_value = round( + float(self._attr_native_value) / 1024, 1 + ) + elif "size" in self.entity_description.key: + self._attr_native_value = round(float(self._attr_native_value), 2) self.schedule_update_ha_state() diff --git a/tests/components/sabnzbd/test_config_flow.py b/tests/components/sabnzbd/test_config_flow.py index d04c5b18ab1..bc72dff2535 100644 --- a/tests/components/sabnzbd/test_config_flow.py +++ b/tests/components/sabnzbd/test_config_flow.py @@ -87,7 +87,6 @@ async def test_import_flow(hass) -> None: "homeassistant.components.sabnzbd.sab.SabnzbdApi.check_available", return_value=True, ): - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, diff --git a/tests/components/sabnzbd/test_init.py b/tests/components/sabnzbd/test_init.py new file mode 100644 index 00000000000..9bdef4119d0 --- /dev/null +++ b/tests/components/sabnzbd/test_init.py @@ -0,0 +1,85 @@ +"""Tests for the SABnzbd Integration.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.sabnzbd import DEFAULT_NAME, DOMAIN, SENSOR_KEYS +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_URL +from homeassistant.helpers.device_registry import DeviceEntryType + +from tests.common import MockConfigEntry, mock_device_registry, mock_registry + +MOCK_ENTRY_ID = "mock_entry_id" + +MOCK_UNIQUE_ID = "someuniqueid" + +MOCK_DEVICE_ID = "somedeviceid" + +MOCK_DATA_VERSION_1 = { + CONF_API_KEY: "api_key", + CONF_URL: "http://127.0.0.1:8080", + CONF_NAME: "name", +} + +MOCK_ENTRY_VERSION_1 = MockConfigEntry( + domain=DOMAIN, data=MOCK_DATA_VERSION_1, entry_id=MOCK_ENTRY_ID, version=1 +) + + +@pytest.fixture +def device_registry(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_registry(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_unique_id_migrate(hass, device_registry, entity_registry): + """Test that config flow entry is migrated correctly.""" + # Start with the config entry at Version 1. + mock_entry = MOCK_ENTRY_VERSION_1 + mock_entry.add_to_hass(hass) + + mock_d_entry = device_registry.async_get_or_create( + config_entry_id=mock_entry.entry_id, + identifiers={(DOMAIN, DOMAIN)}, + name=DEFAULT_NAME, + entry_type=DeviceEntryType.SERVICE, + ) + + entity_id_sensor_key = [] + + for sensor_key in SENSOR_KEYS: + mock_entity_id = f"{SENSOR_DOMAIN}.{DOMAIN}_{sensor_key}" + entity_registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + unique_id=sensor_key, + config_entry=mock_entry, + device_id=mock_d_entry.id, + ) + entity = entity_registry.async_get(mock_entity_id) + assert entity.entity_id == mock_entity_id + assert entity.unique_id == sensor_key + entity_id_sensor_key.append((mock_entity_id, sensor_key)) + + with patch( + "homeassistant.components.sabnzbd.sab.SabnzbdApi.check_available", + return_value=True, + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + + await hass.async_block_till_done() + + for mock_entity_id, sensor_key in entity_id_sensor_key: + entity = entity_registry.async_get(mock_entity_id) + assert entity.unique_id == f"{MOCK_ENTRY_ID}_{sensor_key}" + + assert device_registry.async_get(mock_d_entry.id).identifiers == { + (DOMAIN, MOCK_ENTRY_ID) + } From 6c70a518ebdbeef49ffd8da930c308e6dc513d73 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 8 May 2022 15:21:18 -0600 Subject: [PATCH 0422/3516] Bump simplisafe-python to 2022.05.1 (#71545) * Bump simplisafe-python to 2022.05.1 * Trigger Build --- 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 804523b3390..cb1b02e37ae 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==2022.05.0"], + "requirements": ["simplisafe-python==2022.05.1"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 15ce8f1c8ce..d869142ce86 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2153,7 +2153,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.05.0 +simplisafe-python==2022.05.1 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eeb9f18fbfa..7ab7fa4a5ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1404,7 +1404,7 @@ sharkiq==0.0.1 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2022.05.0 +simplisafe-python==2022.05.1 # homeassistant.components.slack slackclient==2.5.0 From 18d440cc6f92c530d2955e8d0b5a83639dc9661f Mon Sep 17 00:00:00 2001 From: Shai Ungar Date: Mon, 9 May 2022 13:34:16 +0300 Subject: [PATCH 0423/3516] Fix SABnzbd config check (#71549) --- homeassistant/components/sabnzbd/__init__.py | 6 ++---- homeassistant/components/sabnzbd/sensor.py | 14 +++++++++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index 3fa5054a5f0..c03d27fefe5 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -16,7 +16,6 @@ from homeassistant.const import ( CONF_PORT, CONF_SENSORS, CONF_SSL, - CONF_URL, ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError @@ -46,7 +45,7 @@ from .const import ( UPDATE_INTERVAL, ) from .sab import get_client -from .sensor import SENSOR_KEYS +from .sensor import OLD_SENSOR_KEYS PLATFORMS = ["sensor"] _LOGGER = logging.getLogger(__name__) @@ -80,12 +79,11 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_API_KEY): str, vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, - vol.Required(CONF_URL): str, vol.Optional(CONF_PATH): str, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_SENSORS): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] + cv.ensure_list, [vol.In(OLD_SENSOR_KEYS)] ), vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, }, diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index ebdb9190ed9..043a344ec7b 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -103,7 +103,19 @@ SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = ( ), ) -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] +OLD_SENSOR_KEYS = [ + "current_status", + "speed", + "queue_size", + "queue_remaining", + "disk_size", + "disk_free", + "queue_count", + "day_size", + "week_size", + "month_size", + "total_size", +] async def async_setup_entry( From 2caa92b1ebefb1e72ec42a4a2901af9f4f392c93 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sun, 8 May 2022 22:07:12 -0400 Subject: [PATCH 0424/3516] Fix typer/click incompatibilty for unifiprotect (#71555) Co-authored-by: J. Nick Koston --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index d5dbb51ffc1..3c3b461ed4f 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.4.0", "unifi-discovery==1.1.2"], + "requirements": ["pyunifiprotect==3.4.1", "unifi-discovery==1.1.2"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index d869142ce86..e72c54c7129 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1981,7 +1981,7 @@ pytrafikverket==0.1.6.2 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.4.0 +pyunifiprotect==3.4.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ab7fa4a5ed..1ec8844dad7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1304,7 +1304,7 @@ pytrafikverket==0.1.6.2 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.4.0 +pyunifiprotect==3.4.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 4b28d522a8d7aa4eeb7234c1f65dccbeacd0d821 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 May 2022 13:57:32 +0200 Subject: [PATCH 0425/3516] Improve Google Cast detection of HLS playlists (#71564) --- homeassistant/components/cast/helpers.py | 17 ++++++++++++++--- tests/components/cast/test_helpers.py | 21 ++++++++++++++++++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index dd98a2bc051..dfeb9fce25b 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -237,12 +237,14 @@ def _is_url(url): return all([result.scheme, result.netloc]) -async def _fetch_playlist(hass, url): +async def _fetch_playlist(hass, url, supported_content_types): """Fetch a playlist from the given url.""" try: session = aiohttp_client.async_get_clientsession(hass, verify_ssl=False) async with session.get(url, timeout=5) as resp: charset = resp.charset or "utf-8" + if resp.content_type in supported_content_types: + raise PlaylistSupported try: playlist_data = (await resp.content.read(64 * 1024)).decode(charset) except ValueError as err: @@ -260,7 +262,16 @@ async def parse_m3u(hass, url): Based on https://github.com/dvndrsn/M3uParser/blob/master/m3uparser.py """ - m3u_data = await _fetch_playlist(hass, url) + # From Mozilla gecko source: https://github.com/mozilla/gecko-dev/blob/c4c1adbae87bf2d128c39832d72498550ee1b4b8/dom/media/DecoderTraits.cpp#L47-L52 + hls_content_types = ( + # https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-10 + "application/vnd.apple.mpegurl", + # Some sites serve these as the informal HLS m3u type. + "application/x-mpegurl", + "audio/mpegurl", + "audio/x-mpegurl", + ) + m3u_data = await _fetch_playlist(hass, url, hls_content_types) m3u_lines = m3u_data.splitlines() playlist = [] @@ -301,7 +312,7 @@ async def parse_pls(hass, url): Based on https://github.com/mariob/plsparser/blob/master/src/plsparser.py """ - pls_data = await _fetch_playlist(hass, url) + pls_data = await _fetch_playlist(hass, url, ()) pls_parser = configparser.ConfigParser() try: diff --git a/tests/components/cast/test_helpers.py b/tests/components/cast/test_helpers.py index 0d7a3b1ff14..d729d36a225 100644 --- a/tests/components/cast/test_helpers.py +++ b/tests/components/cast/test_helpers.py @@ -14,10 +14,25 @@ from homeassistant.components.cast.helpers import ( from tests.common import load_fixture -async def test_hls_playlist_supported(hass, aioclient_mock): +@pytest.mark.parametrize( + "url,fixture,content_type", + ( + ( + "http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_low/ak/bbc_radio_fourfm.m3u8", + "bbc_radio_fourfm.m3u8", + None, + ), + ( + "https://rthkaudio2-lh.akamaihd.net/i/radio2_1@355865/master.m3u8", + "rthkaudio2.m3u8", + "application/vnd.apple.mpegurl", + ), + ), +) +async def test_hls_playlist_supported(hass, aioclient_mock, url, fixture, content_type): """Test playlist parsing of HLS playlist.""" - url = "http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_low/ak/bbc_radio_fourfm.m3u8" - aioclient_mock.get(url, text=load_fixture("bbc_radio_fourfm.m3u8", "cast")) + headers = {"content-type": content_type} + aioclient_mock.get(url, text=load_fixture(fixture, "cast"), headers=headers) with pytest.raises(PlaylistSupported): await parse_playlist(hass, url) From 0099560a8d22102cba703dd8e9e5a098c757b44a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 May 2022 11:03:32 +0200 Subject: [PATCH 0426/3516] Correct device class for meater cook sensors (#71565) --- homeassistant/components/meater/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index 8c719d588d8..fecde148a5e 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -176,8 +176,6 @@ class MeaterProbeTemperature( ): """Meater Temperature Sensor Entity.""" - _attr_device_class = SensorDeviceClass.TEMPERATURE - _attr_native_unit_of_measurement = TEMP_CELSIUS entity_description: MeaterSensorEntityDescription def __init__( From 8f5677f523bba7b35a3da4dbc82153635bae460d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 May 2022 13:59:13 +0200 Subject: [PATCH 0427/3516] Bump pychromecast to 12.1.2 (#71567) --- 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 cee46913937..10edc81e0fc 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==12.1.1"], + "requirements": ["pychromecast==12.1.2"], "after_dependencies": [ "cloud", "http", diff --git a/requirements_all.txt b/requirements_all.txt index e72c54c7129..b8f7a452499 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1399,7 +1399,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==12.1.1 +pychromecast==12.1.2 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ec8844dad7..f678c7ff2ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -938,7 +938,7 @@ pybotvac==0.0.23 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==12.1.1 +pychromecast==12.1.2 # homeassistant.components.climacell pyclimacell==0.18.2 From 596039ff4a47b35ec1d23a7a0e155ee612e669e2 Mon Sep 17 00:00:00 2001 From: Evan Bruhn Date: Mon, 9 May 2022 21:58:26 +1000 Subject: [PATCH 0428/3516] Bump logi_circle to 0.2.3 (#71578) --- homeassistant/components/logi_circle/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/logi_circle/manifest.json b/homeassistant/components/logi_circle/manifest.json index 94c040f3b75..2d8495df576 100644 --- a/homeassistant/components/logi_circle/manifest.json +++ b/homeassistant/components/logi_circle/manifest.json @@ -3,7 +3,7 @@ "name": "Logi Circle", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/logi_circle", - "requirements": ["logi_circle==0.2.2"], + "requirements": ["logi_circle==0.2.3"], "dependencies": ["ffmpeg", "http"], "codeowners": ["@evanjd"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index b8f7a452499..fc87c3b3601 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -957,7 +957,7 @@ lmnotify==0.0.4 locationsharinglib==4.1.5 # homeassistant.components.logi_circle -logi_circle==0.2.2 +logi_circle==0.2.3 # homeassistant.components.london_underground london-tube-status==0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f678c7ff2ed..973a42c0628 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -655,7 +655,7 @@ librouteros==3.2.0 libsoundtouch==0.8 # homeassistant.components.logi_circle -logi_circle==0.2.2 +logi_circle==0.2.3 # homeassistant.components.recorder lru-dict==1.1.7 From cd69b5708f984f36484f41d43a4deb50e840c470 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 10 May 2022 00:11:50 +0200 Subject: [PATCH 0429/3516] Bump nam backend library to version 1.2.4 (#71584) --- 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 16231ef0b88..a842af46f84 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.2.3"], + "requirements": ["nettigo-air-monitor==1.2.4"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index fc87c3b3601..74f7b42a900 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1065,7 +1065,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.2.3 +nettigo-air-monitor==1.2.4 # homeassistant.components.neurio_energy neurio==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 973a42c0628..e1b17dd97ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -727,7 +727,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.2.3 +nettigo-air-monitor==1.2.4 # homeassistant.components.nexia nexia==0.9.13 From fabea6aacb5fe9f42809eecb3db98d7b9d344c54 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 10 May 2022 00:03:03 +0200 Subject: [PATCH 0430/3516] Bump pydeconz to v92 (#71613) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 1ce3477db70..2a4a5ccf253 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==91"], + "requirements": ["pydeconz==92"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 74f7b42a900..0cb659a32d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1432,7 +1432,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==91 +pydeconz==92 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e1b17dd97ca..9a905e42fae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -953,7 +953,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==91 +pydeconz==92 # homeassistant.components.dexcom pydexcom==0.2.3 From aa3012243414ee89f669d66988c856e04cf6735b Mon Sep 17 00:00:00 2001 From: rappenze Date: Tue, 10 May 2022 22:33:40 +0200 Subject: [PATCH 0431/3516] Fix wrong brightness level change visible in UI (#71655) --- homeassistant/components/fibaro/light.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index 9d0309bc4ee..08a9e651668 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -112,6 +112,7 @@ class FibaroLight(FibaroDevice, LightEntity): if ATTR_BRIGHTNESS in kwargs: self._attr_brightness = kwargs[ATTR_BRIGHTNESS] self.set_level(scaleto99(self._attr_brightness)) + return if ATTR_RGB_COLOR in kwargs: # Update based on parameters From 1f53786e1b230b279fe399ef01d210b43f659ec6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 May 2022 22:44:35 -0500 Subject: [PATCH 0432/3516] Prevent history_stats from rejecting states when microseconds differ (#71704) --- .../components/history_stats/data.py | 6 +- tests/components/history_stats/test_sensor.py | 114 ++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 3f22f4cc32b..3d21cca6b6d 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -96,7 +96,11 @@ class HistoryStats: new_data = False if event and event.data["new_state"] is not None: new_state: State = event.data["new_state"] - if current_period_start <= new_state.last_changed <= current_period_end: + if ( + current_period_start_timestamp + <= floored_timestamp(new_state.last_changed) + <= current_period_end_timestamp + ): self._history_current_period.append(new_state) new_data = True if not new_data and current_period_end_timestamp < now_timestamp: diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index bfa0c8f415e..4f56edaa291 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -1387,3 +1387,117 @@ async def test_measure_cet(hass, recorder_mock): assert hass.states.get("sensor.sensor2").state == "0.83" assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor4").state == "83.3" + + +@pytest.mark.parametrize("time_zone", ["Europe/Berlin", "America/Chicago", "US/Hawaii"]) +async def test_end_time_with_microseconds_zeroed(time_zone, hass, recorder_mock): + """Test the history statistics sensor that has the end time microseconds zeroed out.""" + hass.config.set_time_zone(time_zone) + start_of_today = dt_util.now().replace(hour=0, minute=0, second=0, microsecond=0) + start_time = start_of_today + timedelta(minutes=60) + t0 = start_time + timedelta(minutes=20) + t1 = t0 + timedelta(minutes=10) + t2 = t1 + timedelta(minutes=10) + time_200 = start_of_today + timedelta(hours=2) + + def _fake_states(*args, **kwargs): + return { + "binary_sensor.heatpump_compressor_state": [ + ha.State( + "binary_sensor.heatpump_compressor_state", "on", last_changed=t0 + ), + ha.State( + "binary_sensor.heatpump_compressor_state", + "off", + last_changed=t1, + ), + ha.State( + "binary_sensor.heatpump_compressor_state", "on", last_changed=t2 + ), + ] + } + + with freeze_time(time_200), patch( + "homeassistant.components.recorder.history.state_changes_during_period", + _fake_states, + ): + await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "history_stats", + "entity_id": "binary_sensor.heatpump_compressor_state", + "name": "heatpump_compressor_today", + "state": "on", + "start": "{{ now().replace(hour=0, minute=0, second=0, microsecond=0) }}", + "end": "{{ now().replace(microsecond=0) }}", + "type": "time", + }, + ] + }, + ) + await hass.async_block_till_done() + await async_update_entity(hass, "sensor.heatpump_compressor_today") + await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83" + async_fire_time_changed(hass, time_200) + await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83" + hass.states.async_set("binary_sensor.heatpump_compressor_state", "off") + await hass.async_block_till_done() + + time_400 = start_of_today + timedelta(hours=4) + with freeze_time(time_400): + async_fire_time_changed(hass, time_400) + await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "1.83" + hass.states.async_set("binary_sensor.heatpump_compressor_state", "on") + await hass.async_block_till_done() + time_600 = start_of_today + timedelta(hours=6) + with freeze_time(time_600): + async_fire_time_changed(hass, time_600) + await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "3.83" + + rolled_to_next_day = start_of_today + timedelta(days=1) + assert rolled_to_next_day.hour == 0 + assert rolled_to_next_day.minute == 0 + assert rolled_to_next_day.second == 0 + assert rolled_to_next_day.microsecond == 0 + + with freeze_time(rolled_to_next_day): + async_fire_time_changed(hass, rolled_to_next_day) + await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "0.0" + + rolled_to_next_day_plus_12 = start_of_today + timedelta( + days=1, hours=12, microseconds=0 + ) + with freeze_time(rolled_to_next_day_plus_12): + async_fire_time_changed(hass, rolled_to_next_day_plus_12) + await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "12.0" + + rolled_to_next_day_plus_14 = start_of_today + timedelta( + days=1, hours=14, microseconds=0 + ) + with freeze_time(rolled_to_next_day_plus_14): + async_fire_time_changed(hass, rolled_to_next_day_plus_14) + await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "14.0" + + rolled_to_next_day_plus_16_860000 = start_of_today + timedelta( + days=1, hours=16, microseconds=860000 + ) + with freeze_time(rolled_to_next_day_plus_16_860000): + hass.states.async_set("binary_sensor.heatpump_compressor_state", "off") + async_fire_time_changed(hass, rolled_to_next_day_plus_16_860000) + await hass.async_block_till_done() + + rolled_to_next_day_plus_18 = start_of_today + timedelta(days=1, hours=18) + with freeze_time(rolled_to_next_day_plus_18): + async_fire_time_changed(hass, rolled_to_next_day_plus_18) + await hass.async_block_till_done() + assert hass.states.get("sensor.heatpump_compressor_today").state == "16.0" From a19a88db639344b4201ce9025b79979fdafc6a2f Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 11 May 2022 22:55:12 -0400 Subject: [PATCH 0433/3516] Fix zwave_js device automation bug (#71715) --- .../zwave_js/device_automation_helpers.py | 24 +++++++++++++++++++ .../components/zwave_js/device_condition.py | 6 ++--- .../components/zwave_js/device_trigger.py | 6 ++--- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zwave_js/device_automation_helpers.py b/homeassistant/components/zwave_js/device_automation_helpers.py index 906efb2c4f9..f17ddccf03c 100644 --- a/homeassistant/components/zwave_js/device_automation_helpers.py +++ b/homeassistant/components/zwave_js/device_automation_helpers.py @@ -8,6 +8,12 @@ from zwave_js_server.const import ConfigurationValueType from zwave_js_server.model.node import Node from zwave_js_server.model.value import ConfigurationValue +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr + +from .const import DOMAIN + NODE_STATUSES = ["asleep", "awake", "dead", "alive"] CONF_SUBTYPE = "subtype" @@ -41,3 +47,21 @@ def generate_config_parameter_subtype(config_value: ConfigurationValue) -> str: parameter = f"{parameter}[{hex(config_value.property_key)}]" return f"{parameter} ({config_value.property_name})" + + +@callback +def async_bypass_dynamic_config_validation(hass: HomeAssistant, device_id: str) -> bool: + """Return whether device's config entries are not loaded.""" + dev_reg = dr.async_get(hass) + if (device := dev_reg.async_get(device_id)) is None: + raise ValueError(f"Device {device_id} not found") + entry = next( + ( + config_entry + for config_entry in hass.config_entries.async_entries(DOMAIN) + if config_entry.entry_id in device.config_entries + and config_entry.state == ConfigEntryState.LOADED + ), + None, + ) + return not entry diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py index c70371d6f8a..549319d23f4 100644 --- a/homeassistant/components/zwave_js/device_condition.py +++ b/homeassistant/components/zwave_js/device_condition.py @@ -29,12 +29,12 @@ from .device_automation_helpers import ( CONF_SUBTYPE, CONF_VALUE_ID, NODE_STATUSES, + async_bypass_dynamic_config_validation, generate_config_parameter_subtype, get_config_parameter_value_schema, ) from .helpers import ( async_get_node_from_device_id, - async_is_device_config_entry_not_loaded, check_type_schema_map, get_zwave_value_from_config, remove_keys_with_empty_values, @@ -101,7 +101,7 @@ async def async_validate_condition_config( # We return early if the config entry for this device is not ready because we can't # validate the value without knowing the state of the device try: - device_config_entry_not_loaded = async_is_device_config_entry_not_loaded( + bypass_dynamic_config_validation = async_bypass_dynamic_config_validation( hass, config[CONF_DEVICE_ID] ) except ValueError as err: @@ -109,7 +109,7 @@ async def async_validate_condition_config( f"Device {config[CONF_DEVICE_ID]} not found" ) from err - if device_config_entry_not_loaded: + if bypass_dynamic_config_validation: return config if config[CONF_TYPE] == VALUE_TYPE: diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py index 89379f9a953..0b6369654fe 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -53,12 +53,12 @@ from .const import ( from .device_automation_helpers import ( CONF_SUBTYPE, NODE_STATUSES, + async_bypass_dynamic_config_validation, generate_config_parameter_subtype, ) from .helpers import ( async_get_node_from_device_id, async_get_node_status_sensor_entity_id, - async_is_device_config_entry_not_loaded, check_type_schema_map, copy_available_params, get_value_state_schema, @@ -215,7 +215,7 @@ async def async_validate_trigger_config( # We return early if the config entry for this device is not ready because we can't # validate the value without knowing the state of the device try: - device_config_entry_not_loaded = async_is_device_config_entry_not_loaded( + bypass_dynamic_config_validation = async_bypass_dynamic_config_validation( hass, config[CONF_DEVICE_ID] ) except ValueError as err: @@ -223,7 +223,7 @@ async def async_validate_trigger_config( f"Device {config[CONF_DEVICE_ID]} not found" ) from err - if device_config_entry_not_loaded: + if bypass_dynamic_config_validation: return config trigger_type = config[CONF_TYPE] From c9543d8665ec0ed7f72bca1286bda039ef9a343a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 12 May 2022 07:34:25 -0700 Subject: [PATCH 0434/3516] Bumped version to 2022.5.4 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 569c121f909..5eb59e819da 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 2ec80dd2855..b492bd0a240 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.5.3 +version = 2022.5.4 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 11cc1feb853bcfd9633ebfc44eae142c10a7f983 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 12 May 2022 17:08:21 +0200 Subject: [PATCH 0435/3516] Tweak template switch tests (#71738) --- tests/components/template/test_switch.py | 194 ++++++++--------------- 1 file changed, 64 insertions(+), 130 deletions(-) diff --git a/tests/components/template/test_switch.py b/tests/components/template/test_switch.py index 2628b0afa49..93e0f8540bf 100644 --- a/tests/components/template/test_switch.py +++ b/tests/components/template/test_switch.py @@ -5,28 +5,51 @@ import pytest from homeassistant import setup from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( + ATTR_DOMAIN, ATTR_ENTITY_ID, + ATTR_SERVICE_DATA, + EVENT_CALL_SERVICE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import CoreState, State +from homeassistant.core import CoreState, State, callback from homeassistant.setup import async_setup_component -from tests.common import ( - assert_setup_component, - async_mock_service, - mock_component, - mock_restore_cache, -) +from tests.common import assert_setup_component, mock_component, mock_restore_cache @pytest.fixture -def calls(hass): - """Track calls to a mock service.""" - return async_mock_service(hass, "test", "automation") +def service_calls(hass): + """Track service call events for switch.test_state.""" + events = [] + entity_id = "switch.test_state" + + @callback + def capture_events(event): + if event.data[ATTR_DOMAIN] != "switch": + return + if event.data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID] != [entity_id]: + return + events.append(event) + + hass.bus.async_listen(EVENT_CALL_SERVICE, capture_events) + + return events + + +OPTIMISTIC_SWITCH_CONFIG = { + "turn_on": { + "service": "switch.turn_on", + "entity_id": "switch.test_state", + }, + "turn_off": { + "service": "switch.turn_off", + "entity_id": "switch.test_state", + }, +} async def test_template_state_text(hass): @@ -40,15 +63,8 @@ async def test_template_state_text(hass): "platform": "template", "switches": { "test_template_switch": { + **OPTIMISTIC_SWITCH_CONFIG, "value_template": "{{ states.switch.test_state.state }}", - "turn_on": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "turn_off": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, } }, } @@ -83,15 +99,8 @@ async def test_template_state_boolean_on(hass): "platform": "template", "switches": { "test_template_switch": { + **OPTIMISTIC_SWITCH_CONFIG, "value_template": "{{ 1 == 1 }}", - "turn_on": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "turn_off": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, } }, } @@ -117,15 +126,8 @@ async def test_template_state_boolean_off(hass): "platform": "template", "switches": { "test_template_switch": { + **OPTIMISTIC_SWITCH_CONFIG, "value_template": "{{ 1 == 2 }}", - "turn_on": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "turn_off": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, } }, } @@ -151,15 +153,8 @@ async def test_icon_template(hass): "platform": "template", "switches": { "test_template_switch": { + **OPTIMISTIC_SWITCH_CONFIG, "value_template": "{{ states.switch.test_state.state }}", - "turn_on": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "turn_off": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, "icon_template": "{% if states.switch.test_state.state %}" "mdi:check" "{% endif %}", @@ -194,15 +189,8 @@ async def test_entity_picture_template(hass): "platform": "template", "switches": { "test_template_switch": { + **OPTIMISTIC_SWITCH_CONFIG, "value_template": "{{ states.switch.test_state.state }}", - "turn_on": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "turn_off": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, "entity_picture_template": "{% if states.switch.test_state.state %}" "/local/switch.png" "{% endif %}", @@ -237,15 +225,8 @@ async def test_template_syntax_error(hass): "platform": "template", "switches": { "test_template_switch": { + **OPTIMISTIC_SWITCH_CONFIG, "value_template": "{% if rubbish %}", - "turn_on": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "turn_off": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, } }, } @@ -270,15 +251,8 @@ async def test_invalid_name_does_not_create(hass): "platform": "template", "switches": { "test INVALID switch": { + **OPTIMISTIC_SWITCH_CONFIG, "value_template": "{{ rubbish }", - "turn_on": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "turn_off": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, } }, } @@ -393,7 +367,7 @@ async def test_missing_off_does_not_create(hass): assert hass.states.async_all("switch") == [] -async def test_on_action(hass, calls): +async def test_on_action(hass, service_calls): """Test on action.""" assert await async_setup_component( hass, @@ -403,12 +377,8 @@ async def test_on_action(hass, calls): "platform": "template", "switches": { "test_template_switch": { + **OPTIMISTIC_SWITCH_CONFIG, "value_template": "{{ states.switch.test_state.state }}", - "turn_on": {"service": "test.automation"}, - "turn_off": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, } }, } @@ -432,10 +402,11 @@ async def test_on_action(hass, calls): blocking=True, ) - assert len(calls) == 1 + assert len(service_calls) == 1 + assert service_calls[-1].data["service"] == "turn_on" -async def test_on_action_optimistic(hass, calls): +async def test_on_action_optimistic(hass, service_calls): """Test on action in optimistic mode.""" assert await async_setup_component( hass, @@ -445,11 +416,7 @@ async def test_on_action_optimistic(hass, calls): "platform": "template", "switches": { "test_template_switch": { - "turn_on": {"service": "test.automation"}, - "turn_off": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, + **OPTIMISTIC_SWITCH_CONFIG, } }, } @@ -473,11 +440,13 @@ async def test_on_action_optimistic(hass, calls): ) state = hass.states.get("switch.test_template_switch") - assert len(calls) == 1 assert state.state == STATE_ON + assert len(service_calls) == 1 + assert service_calls[-1].data["service"] == "turn_on" -async def test_off_action(hass, calls): + +async def test_off_action(hass, service_calls): """Test off action.""" assert await async_setup_component( hass, @@ -487,12 +456,8 @@ async def test_off_action(hass, calls): "platform": "template", "switches": { "test_template_switch": { + **OPTIMISTIC_SWITCH_CONFIG, "value_template": "{{ states.switch.test_state.state }}", - "turn_on": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "turn_off": {"service": "test.automation"}, } }, } @@ -516,10 +481,11 @@ async def test_off_action(hass, calls): blocking=True, ) - assert len(calls) == 1 + assert len(service_calls) == 1 + assert service_calls[-1].data["service"] == "turn_off" -async def test_off_action_optimistic(hass, calls): +async def test_off_action_optimistic(hass, service_calls): """Test off action in optimistic mode.""" assert await async_setup_component( hass, @@ -529,11 +495,7 @@ async def test_off_action_optimistic(hass, calls): "platform": "template", "switches": { "test_template_switch": { - "turn_off": {"service": "test.automation"}, - "turn_on": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, + **OPTIMISTIC_SWITCH_CONFIG, } }, } @@ -557,9 +519,11 @@ async def test_off_action_optimistic(hass, calls): ) state = hass.states.get("switch.test_template_switch") - assert len(calls) == 1 assert state.state == STATE_OFF + assert len(service_calls) == 1 + assert service_calls[-1].data["service"] == "turn_off" + async def test_restore_state(hass): """Test state restoration.""" @@ -582,12 +546,10 @@ async def test_restore_state(hass): "platform": "template", "switches": { "s1": { - "turn_on": {"service": "test.automation"}, - "turn_off": {"service": "test.automation"}, + **OPTIMISTIC_SWITCH_CONFIG, }, "s2": { - "turn_on": {"service": "test.automation"}, - "turn_off": {"service": "test.automation"}, + **OPTIMISTIC_SWITCH_CONFIG, }, }, } @@ -614,15 +576,8 @@ async def test_available_template_with_entities(hass): "platform": "template", "switches": { "test_template_switch": { + **OPTIMISTIC_SWITCH_CONFIG, "value_template": "{{ 1 == 1 }}", - "turn_on": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "turn_off": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, "availability_template": "{{ is_state('availability_state.state', 'on') }}", } }, @@ -655,15 +610,8 @@ async def test_invalid_availability_template_keeps_component_available(hass, cap "platform": "template", "switches": { "test_template_switch": { + **OPTIMISTIC_SWITCH_CONFIG, "value_template": "{{ true }}", - "turn_on": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "turn_off": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, "availability_template": "{{ x - 12 }}", } }, @@ -689,28 +637,14 @@ async def test_unique_id(hass): "platform": "template", "switches": { "test_template_switch_01": { + **OPTIMISTIC_SWITCH_CONFIG, "unique_id": "not-so-unique-anymore", "value_template": "{{ true }}", - "turn_on": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "turn_off": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, }, "test_template_switch_02": { + **OPTIMISTIC_SWITCH_CONFIG, "unique_id": "not-so-unique-anymore", "value_template": "{{ false }}", - "turn_on": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", - }, - "turn_off": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", - }, }, }, } From 3332c853c4dcbea146a9c52c02bb0df219718f00 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 12 May 2022 18:34:26 +0200 Subject: [PATCH 0436/3516] Remove prints from template tests (#71746) --- tests/components/template/test_alarm_control_panel.py | 1 - tests/components/template/test_cover.py | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index 669effca3e9..9ec93073911 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -29,7 +29,6 @@ def service_calls(hass): @callback def capture_events(event): - print(event.data) if event.data[ATTR_DOMAIN] != ALARM_DOMAIN: return if event.data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID] != [entity_id]: diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 92842dc5bfe..18db5482141 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -40,7 +40,6 @@ def service_calls(hass): @callback def capture_events(event): - print(event.data) if event.data[ATTR_DOMAIN] != DOMAIN: return if event.data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID] != [entity_id]: From ae89a1243a563ea757a8251310101c6956c19976 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Thu, 12 May 2022 12:15:59 -0600 Subject: [PATCH 0437/3516] Refactor litterrobot to use SensorEntityDescription (#71224) --- .../components/litterrobot/sensor.py | 113 ++++++++++-------- tests/components/litterrobot/conftest.py | 6 + tests/components/litterrobot/test_sensor.py | 32 ++--- 3 files changed, 84 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index de3126986d1..751d16551c3 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -1,11 +1,19 @@ """Support for Litter-Robot sensors.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from datetime import datetime +from typing import Any from pylitterbot.robot import Robot -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity, StateType +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + StateType, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant @@ -27,56 +35,64 @@ def icon_for_gauge_level(gauge_level: int | None = None, offset: int = 0) -> str return "mdi:gauge-low" -class LitterRobotPropertySensor(LitterRobotEntity, SensorEntity): - """Litter-Robot property sensor.""" +@dataclass +class LitterRobotSensorEntityDescription(SensorEntityDescription): + """A class that describes Litter-Robot sensor entities.""" + + icon_fn: Callable[[Any], str | None] = lambda _: None + should_report: Callable[[Robot], bool] = lambda _: True + + +class LitterRobotSensorEntity(LitterRobotEntity, SensorEntity): + """Litter-Robot sensor entity.""" + + entity_description: LitterRobotSensorEntityDescription def __init__( - self, robot: Robot, entity_type: str, hub: LitterRobotHub, sensor_attribute: str + self, + robot: Robot, + hub: LitterRobotHub, + description: LitterRobotSensorEntityDescription, ) -> None: - """Pass robot, entity_type and hub to LitterRobotEntity.""" - super().__init__(robot, entity_type, hub) - self.sensor_attribute = sensor_attribute + """Initialize a Litter-Robot sensor entity.""" + assert description.name + super().__init__(robot, description.name, hub) + self.entity_description = description @property def native_value(self) -> StateType | datetime: """Return the state.""" - return getattr(self.robot, self.sensor_attribute) - - -class LitterRobotWasteSensor(LitterRobotPropertySensor): - """Litter-Robot waste sensor.""" - - @property - def native_unit_of_measurement(self) -> str: - """Return unit of measurement.""" - return PERCENTAGE - - @property - def icon(self) -> str: - """Return the icon to use in the frontend, if any.""" - return icon_for_gauge_level(self.state, 10) - - -class LitterRobotSleepTimeSensor(LitterRobotPropertySensor): - """Litter-Robot sleep time sensor.""" - - @property - def native_value(self) -> StateType | datetime: - """Return the state.""" - if self.robot.sleep_mode_enabled: - return super().native_value + if self.entity_description.should_report(self.robot): + return getattr(self.robot, self.entity_description.key) return None @property - def device_class(self) -> str: - """Return the device class, if any.""" - return SensorDeviceClass.TIMESTAMP + def icon(self) -> str | None: + """Return the icon to use in the frontend, if any.""" + if (icon := self.entity_description.icon_fn(self.state)) is not None: + return icon + return super().icon -ROBOT_SENSORS: list[tuple[type[LitterRobotPropertySensor], str, str]] = [ - (LitterRobotWasteSensor, "Waste Drawer", "waste_drawer_level"), - (LitterRobotSleepTimeSensor, "Sleep Mode Start Time", "sleep_mode_start_time"), - (LitterRobotSleepTimeSensor, "Sleep Mode End Time", "sleep_mode_end_time"), +ROBOT_SENSORS = [ + LitterRobotSensorEntityDescription( + name="Waste Drawer", + key="waste_drawer_level", + native_unit_of_measurement=PERCENTAGE, + icon_fn=lambda state: icon_for_gauge_level(state, 10), + ), + LitterRobotSensorEntityDescription( + name="Sleep Mode Start Time", + key="sleep_mode_start_time", + device_class=SensorDeviceClass.TIMESTAMP, + should_report=lambda robot: robot.sleep_mode_enabled, + ), + LitterRobotSensorEntityDescription( + name="Sleep Mode End Time", + key="sleep_mode_end_time", + device_class=SensorDeviceClass.TIMESTAMP, + should_report=lambda robot: robot.sleep_mode_enabled, + ), ] @@ -87,17 +103,8 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot sensors using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - - entities = [] - for robot in hub.account.robots: - for (sensor_class, entity_type, sensor_attribute) in ROBOT_SENSORS: - entities.append( - sensor_class( - robot=robot, - entity_type=entity_type, - hub=hub, - sensor_attribute=sensor_attribute, - ) - ) - - async_add_entities(entities) + async_add_entities( + LitterRobotSensorEntity(robot=robot, hub=hub, description=description) + for description in ROBOT_SENSORS + for robot in hub.account.robots + ) diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py index 839ffe2952d..e8ec5324ae6 100644 --- a/tests/components/litterrobot/conftest.py +++ b/tests/components/litterrobot/conftest.py @@ -67,6 +67,12 @@ def mock_account_with_sleeping_robot() -> MagicMock: return create_mock_account({"sleepModeActive": "102:00:00"}) +@pytest.fixture +def mock_account_with_sleep_disabled_robot() -> MagicMock: + """Mock a Litter-Robot account with a robot that has sleep mode disabled.""" + return create_mock_account({"sleepModeActive": "0"}) + + @pytest.fixture def mock_account_with_robot_not_recently_seen() -> MagicMock: """Mock a Litter-Robot account with a sleeping robot.""" diff --git a/tests/components/litterrobot/test_sensor.py b/tests/components/litterrobot/test_sensor.py index e3e62d5f5e4..ce91541f0d7 100644 --- a/tests/components/litterrobot/test_sensor.py +++ b/tests/components/litterrobot/test_sensor.py @@ -1,16 +1,19 @@ """Test the Litter-Robot sensor entity.""" -from unittest.mock import Mock +from unittest.mock import MagicMock -from homeassistant.components.litterrobot.sensor import LitterRobotSleepTimeSensor from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN, SensorDeviceClass -from homeassistant.const import PERCENTAGE +from homeassistant.const import PERCENTAGE, STATE_UNKNOWN +from homeassistant.core import HomeAssistant -from .conftest import create_mock_robot, setup_integration +from .conftest import setup_integration WASTE_DRAWER_ENTITY_ID = "sensor.test_waste_drawer" +SLEEP_START_TIME_ENTITY_ID = "sensor.test_sleep_mode_start_time" -async def test_waste_drawer_sensor(hass, mock_account): +async def test_waste_drawer_sensor( + hass: HomeAssistant, mock_account: MagicMock +) -> None: """Tests the waste drawer sensor entity was set up.""" await setup_integration(hass, mock_account, PLATFORM_DOMAIN) @@ -20,20 +23,21 @@ async def test_waste_drawer_sensor(hass, mock_account): assert sensor.attributes["unit_of_measurement"] == PERCENTAGE -async def test_sleep_time_sensor_with_none_state(hass): - """Tests the sleep mode start time sensor where sleep mode is inactive.""" - robot = create_mock_robot({"sleepModeActive": "0"}) - sensor = LitterRobotSleepTimeSensor( - robot, "Sleep Mode Start Time", Mock(), "sleep_mode_start_time" +async def test_sleep_time_sensor_with_sleep_disabled( + hass: HomeAssistant, mock_account_with_sleep_disabled_robot: MagicMock +) -> None: + """Tests the sleep mode start time sensor where sleep mode is disabled.""" + await setup_integration( + hass, mock_account_with_sleep_disabled_robot, PLATFORM_DOMAIN ) - sensor.hass = hass + sensor = hass.states.get(SLEEP_START_TIME_ENTITY_ID) assert sensor - assert sensor.state is None - assert sensor.device_class is SensorDeviceClass.TIMESTAMP + assert sensor.state == STATE_UNKNOWN + assert sensor.attributes["device_class"] == SensorDeviceClass.TIMESTAMP -async def test_gauge_icon(): +async def test_gauge_icon() -> None: """Test icon generator for gauge sensor.""" from homeassistant.components.litterrobot.sensor import icon_for_gauge_level From a746d7c1d7b8f1944e9caf923cd95b89bf9e9696 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 13 May 2022 01:40:00 +0200 Subject: [PATCH 0438/3516] Improve code quality in sql integration (#71705) --- homeassistant/components/sql/config_flow.py | 20 ++++++--------- homeassistant/components/sql/sensor.py | 22 ++++++++-------- homeassistant/components/sql/strings.json | 6 ++--- .../components/sql/translations/en.json | 6 ++--- tests/components/sql/test_config_flow.py | 1 - tests/components/sql/test_sensor.py | 25 ++++++++++++++++++- 6 files changed, 46 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/sql/config_flow.py b/homeassistant/components/sql/config_flow.py index 9a6013e1d62..dc3a839ef1d 100644 --- a/homeassistant/components/sql/config_flow.py +++ b/homeassistant/components/sql/config_flow.py @@ -13,7 +13,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.recorder import CONF_DB_URL, DEFAULT_DB_FILE, DEFAULT_URL from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import selector @@ -44,24 +44,23 @@ def validate_sql_select(value: str) -> str | None: def validate_query(db_url: str, query: str, column: str) -> bool: """Validate SQL query.""" - try: - engine = sqlalchemy.create_engine(db_url, future=True) - sessmaker = scoped_session(sessionmaker(bind=engine, future=True)) - except SQLAlchemyError as error: - raise error + engine = sqlalchemy.create_engine(db_url, future=True) + sessmaker = scoped_session(sessionmaker(bind=engine, future=True)) sess: scoped_session = sessmaker() try: result: Result = sess.execute(sqlalchemy.text(query)) - for res in result.mappings(): - data = res[column] - _LOGGER.debug("Return value from query: %s", data) except SQLAlchemyError as error: + _LOGGER.debug("Execution error %s", error) if sess: sess.close() raise ValueError(error) from error + for res in result.mappings(): + data = res[column] + _LOGGER.debug("Return value from query: %s", data) + if sess: sess.close() @@ -73,9 +72,6 @@ class SQLConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - entry: config_entries.ConfigEntry - hass: HomeAssistant - @staticmethod @callback def async_get_options_flow( diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 4e3f9d15846..33ddafe2c0a 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -6,6 +6,7 @@ import decimal import logging import sqlalchemy +from sqlalchemy.engine import Result from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import scoped_session, sessionmaker import voluptuous as vol @@ -73,11 +74,11 @@ async def async_setup_platform( for query in config[CONF_QUERIES]: new_config = { CONF_DB_URL: config.get(CONF_DB_URL, default_db_url), - CONF_NAME: query.get(CONF_NAME), - CONF_QUERY: query.get(CONF_QUERY), + CONF_NAME: query[CONF_NAME], + CONF_QUERY: query[CONF_QUERY], CONF_UNIT_OF_MEASUREMENT: query.get(CONF_UNIT_OF_MEASUREMENT), CONF_VALUE_TEMPLATE: query.get(CONF_VALUE_TEMPLATE), - CONF_COLUMN_NAME: query.get(CONF_COLUMN_NAME), + CONF_COLUMN_NAME: query[CONF_COLUMN_NAME], } hass.async_create_task( hass.config_entries.flow.async_init( @@ -119,11 +120,10 @@ async def async_setup_entry( # MSSQL uses TOP and not LIMIT if not ("LIMIT" in query_str.upper() or "SELECT TOP" in query_str.upper()): - query_str = ( - query_str.replace("SELECT", "SELECT TOP 1") - if "mssql" in db_url - else query_str.replace(";", " LIMIT 1;") - ) + if "mssql" in db_url: + query_str = query_str.upper().replace("SELECT", "SELECT TOP 1") + else: + query_str = query_str.replace(";", "") + " LIMIT 1;" async_add_entities( [ @@ -179,7 +179,7 @@ class SQLSensor(SensorEntity): self._attr_extra_state_attributes = {} sess: scoped_session = self.sessionmaker() try: - result = sess.execute(sqlalchemy.text(self._query)) + result: Result = sess.execute(sqlalchemy.text(self._query)) except SQLAlchemyError as err: _LOGGER.error( "Error executing query %s: %s", @@ -188,10 +188,8 @@ class SQLSensor(SensorEntity): ) return - _LOGGER.debug("Result %s, ResultMapping %s", result, result.mappings()) - for res in result.mappings(): - _LOGGER.debug("result = %s", res.items()) + _LOGGER.debug("Query %s result in %s", self._query, res.items()) data = res[self._column_name] for key, value in res.items(): if isinstance(value, decimal.Decimal): diff --git a/homeassistant/components/sql/strings.json b/homeassistant/components/sql/strings.json index 8d3a194ac3e..2a300f75b3e 100644 --- a/homeassistant/components/sql/strings.json +++ b/homeassistant/components/sql/strings.json @@ -5,8 +5,7 @@ }, "error": { "db_url_invalid": "Database URL invalid", - "query_invalid": "SQL Query invalid", - "value_template_invalid": "Value Template invalid" + "query_invalid": "SQL Query invalid" }, "step": { "user": { @@ -52,8 +51,7 @@ }, "error": { "db_url_invalid": "[%key:component::sql::config::error::db_url_invalid%]", - "query_invalid": "[%key:component::sql::config::error::query_invalid%]", - "value_template_invalid": "[%key:component::sql::config::error::value_template_invalid%]" + "query_invalid": "[%key:component::sql::config::error::query_invalid%]" } } } diff --git a/homeassistant/components/sql/translations/en.json b/homeassistant/components/sql/translations/en.json index 4b72d024df4..9d05f771853 100644 --- a/homeassistant/components/sql/translations/en.json +++ b/homeassistant/components/sql/translations/en.json @@ -5,8 +5,7 @@ }, "error": { "db_url_invalid": "Database URL invalid", - "query_invalid": "SQL Query invalid", - "value_template_invalid": "Value Template invalid" + "query_invalid": "SQL Query invalid" }, "step": { "user": { @@ -32,8 +31,7 @@ "options": { "error": { "db_url_invalid": "Database URL invalid", - "query_invalid": "SQL Query invalid", - "value_template_invalid": "Value Template invalid" + "query_invalid": "SQL Query invalid" }, "step": { "init": { diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index 3e065df0ebd..47957ead98e 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -42,7 +42,6 @@ async def test_form(hass: HomeAssistant) -> None: ENTRY_CONFIG, ) await hass.async_block_till_done() - print(ENTRY_CONFIG) assert result2["type"] == RESULT_TYPE_CREATE_ENTRY assert result2["title"] == "Get Value" diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index 0eb3bf70683..588e1c824b7 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -115,7 +115,30 @@ async def test_query_no_value( state = hass.states.get("sensor.count_tables") assert state.state == STATE_UNKNOWN - text = "SELECT 5 as value where 1=2 returned no results" + text = "SELECT 5 as value where 1=2 LIMIT 1; returned no results" + assert text in caplog.text + + +async def test_query_mssql_no_result( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test the SQL sensor with a query that returns no value.""" + config = { + "db_url": "mssql://", + "query": "SELECT 5 as value where 1=2", + "column": "value", + "name": "count_tables", + } + with patch("homeassistant.components.sql.sensor.sqlalchemy"), patch( + "homeassistant.components.sql.sensor.sqlalchemy.text", + return_value="SELECT TOP 1 5 as value where 1=2", + ): + await init_integration(hass, config) + + state = hass.states.get("sensor.count_tables") + assert state.state == STATE_UNKNOWN + + text = "SELECT TOP 1 5 AS VALUE WHERE 1=2 returned no results" assert text in caplog.text From 316bfc89f27aca3f175abfbdb8510368fb73a784 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 May 2022 12:58:42 -0500 Subject: [PATCH 0439/3516] Fix merge conflict with master to dev in sabnzbd (CI fix) (#71605) --- tests/components/sabnzbd/test_init.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/sabnzbd/test_init.py b/tests/components/sabnzbd/test_init.py index 9bdef4119d0..f140c332778 100644 --- a/tests/components/sabnzbd/test_init.py +++ b/tests/components/sabnzbd/test_init.py @@ -3,7 +3,7 @@ from unittest.mock import patch import pytest -from homeassistant.components.sabnzbd import DEFAULT_NAME, DOMAIN, SENSOR_KEYS +from homeassistant.components.sabnzbd import DEFAULT_NAME, DOMAIN, OLD_SENSOR_KEYS from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_URL from homeassistant.helpers.device_registry import DeviceEntryType @@ -54,7 +54,7 @@ async def test_unique_id_migrate(hass, device_registry, entity_registry): entity_id_sensor_key = [] - for sensor_key in SENSOR_KEYS: + for sensor_key in OLD_SENSOR_KEYS: mock_entity_id = f"{SENSOR_DOMAIN}.{DOMAIN}_{sensor_key}" entity_registry.async_get_or_create( SENSOR_DOMAIN, From 7e49ae64105aeb7d422fe8c999f86cddfa1c653c Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 13 May 2022 07:43:24 +0800 Subject: [PATCH 0440/3516] Add use_wallclock_as_timestamps option to generic (#71245) Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- homeassistant/components/generic/camera.py | 5 ++++ .../components/generic/config_flow.py | 20 ++++++++++++- homeassistant/components/generic/const.py | 6 +++- homeassistant/components/generic/strings.json | 4 +++ .../components/generic/translations/en.json | 6 ++-- tests/components/generic/test_config_flow.py | 30 +++++++++++++++++++ 6 files changed, 67 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index d20032e2607..197890efad3 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -37,6 +37,7 @@ from .const import ( CONF_RTSP_TRANSPORT, CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE, + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, DEFAULT_NAME, FFMPEG_OPTION_MAP, GET_IMAGE_TIMEOUT, @@ -160,6 +161,10 @@ class GenericCamera(Camera): CONF_RTSP_TRANSPORT ] self._auth = generate_auth(device_info) + if device_info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): + self.stream_options[ + FFMPEG_OPTION_MAP[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] + ] = "1" self._last_url = None self._last_image = None diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 086262aa0a1..0a49393d9cc 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -41,6 +41,7 @@ from .const import ( CONF_RTSP_TRANSPORT, CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE, + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, DEFAULT_NAME, DOMAIN, FFMPEG_OPTION_MAP, @@ -64,6 +65,7 @@ SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml", "webp"} def build_schema( user_input: dict[str, Any] | MappingProxyType[str, Any], is_options_flow: bool = False, + show_advanced_options=False, ): """Create schema for camera config setup.""" spec = { @@ -106,6 +108,13 @@ def build_schema( default=user_input.get(CONF_LIMIT_REFETCH_TO_URL_CHANGE, False), ) ] = bool + if show_advanced_options: + spec[ + vol.Required( + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, + default=user_input.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS, False), + ) + ] = bool return vol.Schema(spec) @@ -199,6 +208,8 @@ async def async_test_stream(hass, info) -> dict[str, str]: } if rtsp_transport := info.get(CONF_RTSP_TRANSPORT): stream_options[FFMPEG_OPTION_MAP[CONF_RTSP_TRANSPORT]] = rtsp_transport + if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): + stream_options[FFMPEG_OPTION_MAP[CONF_USE_WALLCLOCK_AS_TIMESTAMPS]] = "1" _LOGGER.debug("Attempting to open stream %s", stream_source) container = await hass.async_add_executor_job( partial( @@ -356,6 +367,9 @@ class GenericOptionsFlowHandler(OptionsFlow): ], CONF_FRAMERATE: user_input[CONF_FRAMERATE], CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL], + CONF_USE_WALLCLOCK_AS_TIMESTAMPS: user_input.get( + CONF_USE_WALLCLOCK_AS_TIMESTAMPS + ), } return self.async_create_entry( title=title, @@ -363,6 +377,10 @@ class GenericOptionsFlowHandler(OptionsFlow): ) return self.async_show_form( step_id="init", - data_schema=build_schema(user_input or self.config_entry.options, True), + data_schema=build_schema( + user_input or self.config_entry.options, + True, + self.show_advanced_options, + ), errors=errors, ) diff --git a/homeassistant/components/generic/const.py b/homeassistant/components/generic/const.py index 60b4cec61a6..8ae5f16c4c4 100644 --- a/homeassistant/components/generic/const.py +++ b/homeassistant/components/generic/const.py @@ -8,7 +8,11 @@ CONF_STILL_IMAGE_URL = "still_image_url" CONF_STREAM_SOURCE = "stream_source" CONF_FRAMERATE = "framerate" CONF_RTSP_TRANSPORT = "rtsp_transport" -FFMPEG_OPTION_MAP = {CONF_RTSP_TRANSPORT: "rtsp_transport"} +CONF_USE_WALLCLOCK_AS_TIMESTAMPS = "use_wallclock_as_timestamps" +FFMPEG_OPTION_MAP = { + CONF_RTSP_TRANSPORT: "rtsp_transport", + CONF_USE_WALLCLOCK_AS_TIMESTAMPS: "use_wallclock_as_timestamps", +} RTSP_TRANSPORTS = { "tcp": "TCP", "udp": "UDP", diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index 01b1fe48a82..0954656f71d 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -55,9 +55,13 @@ "authentication": "[%key:component::generic::config::step::user::data::authentication%]", "limit_refetch_to_url_change": "[%key:component::generic::config::step::user::data::limit_refetch_to_url_change%]", "password": "[%key:common::config_flow::data::password%]", + "use_wallclock_as_timestamps": "Use wallclock as timestamps", "username": "[%key:common::config_flow::data::username%]", "framerate": "[%key:component::generic::config::step::user::data::framerate%]", "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + }, + "data_description": { + "use_wallclock_as_timestamps": "This option may correct segmenting or crashing issues arising from buggy timestamp implementations on some cameras" } }, "content_type": { diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json index b158488f178..b552c780d29 100644 --- a/homeassistant/components/generic/translations/en.json +++ b/homeassistant/components/generic/translations/en.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "Authentication", - "content_type": "Content Type", "framerate": "Frame Rate (Hz)", "limit_refetch_to_url_change": "Limit refetch to url change", "password": "Password", @@ -72,15 +71,18 @@ "init": { "data": { "authentication": "Authentication", - "content_type": "Content Type", "framerate": "Frame Rate (Hz)", "limit_refetch_to_url_change": "Limit refetch to url change", "password": "Password", "rtsp_transport": "RTSP transport protocol", "still_image_url": "Still Image URL (e.g. http://...)", "stream_source": "Stream Source URL (e.g. rtsp://...)", + "use_wallclock_as_timestamps": "Use wallclock as timestamps", "username": "Username", "verify_ssl": "Verify SSL certificate" + }, + "data_description": { + "use_wallclock_as_timestamps": "This option may correct segmenting or crashing issues arising from buggy timestamp implementations on some cameras" } } } diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 457cac26aa5..dd53cb8548e 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -18,6 +18,7 @@ from homeassistant.components.generic.const import ( CONF_RTSP_TRANSPORT, CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE, + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, DOMAIN, ) from homeassistant.const import ( @@ -653,3 +654,32 @@ async def test_migrate_existing_ids(hass) -> None: entity_entry = registry.async_get(entity_id) assert entity_entry.unique_id == new_unique_id + + +@respx.mock +async def test_use_wallclock_as_timestamps_option(hass, fakeimg_png, mock_av_open): + """Test the use_wallclock_as_timestamps option flow.""" + + mock_entry = MockConfigEntry( + title="Test Camera", + domain=DOMAIN, + data={}, + options=TESTDATA, + ) + + with mock_av_open: + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + mock_entry.entry_id, context={"show_advanced_options": True} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY From d76ff7d5a272419ad5329211c621f4d6218f1c63 Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Fri, 13 May 2022 02:45:39 +0300 Subject: [PATCH 0441/3516] Changed API for Ukraine Alarm (#71754) --- .../components/ukraine_alarm/__init__.py | 14 +- .../components/ukraine_alarm/config_flow.py | 67 ++++---- .../components/ukraine_alarm/manifest.json | 2 +- .../components/ukraine_alarm/strings.json | 17 +- .../ukraine_alarm/translations/en.json | 15 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../ukraine_alarm/test_config_flow.py | 161 ++++++------------ 8 files changed, 99 insertions(+), 181 deletions(-) diff --git a/homeassistant/components/ukraine_alarm/__init__.py b/homeassistant/components/ukraine_alarm/__init__.py index b2b2ff4162f..587854a3a7e 100644 --- a/homeassistant/components/ukraine_alarm/__init__.py +++ b/homeassistant/components/ukraine_alarm/__init__.py @@ -7,10 +7,10 @@ from typing import Any import aiohttp from aiohttp import ClientSession -from ukrainealarm.client import Client +from uasiren.client import Client from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_REGION +from homeassistant.const import CONF_REGION from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -24,14 +24,11 @@ UPDATE_INTERVAL = timedelta(seconds=10) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ukraine Alarm as config entry.""" - api_key = entry.data[CONF_API_KEY] region_id = entry.data[CONF_REGION] websession = async_get_clientsession(hass) - coordinator = UkraineAlarmDataUpdateCoordinator( - hass, websession, api_key, region_id - ) + coordinator = UkraineAlarmDataUpdateCoordinator(hass, websession, region_id) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator @@ -56,19 +53,18 @@ class UkraineAlarmDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): self, hass: HomeAssistant, session: ClientSession, - api_key: str, region_id: str, ) -> None: """Initialize.""" self.region_id = region_id - self.ukrainealarm = Client(session, api_key) + self.uasiren = Client(session) super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" try: - res = await self.ukrainealarm.get_alerts(self.region_id) + res = await self.uasiren.get_alerts(self.region_id) except aiohttp.ClientError as error: raise UpdateFailed(f"Error fetching alerts from API: {error}") from error diff --git a/homeassistant/components/ukraine_alarm/config_flow.py b/homeassistant/components/ukraine_alarm/config_flow.py index dcf41658dfb..4f1e1c5cf23 100644 --- a/homeassistant/components/ukraine_alarm/config_flow.py +++ b/homeassistant/components/ukraine_alarm/config_flow.py @@ -2,17 +2,20 @@ from __future__ import annotations import asyncio +import logging import aiohttp -from ukrainealarm.client import Client +from uasiren.client import Client import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_REGION +from homeassistant.const import CONF_NAME, CONF_REGION from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) + class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for Ukraine Alarm.""" @@ -21,54 +24,47 @@ class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize a new UkraineAlarmConfigFlow.""" - self.api_key = None self.states = None self.selected_region = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" - errors = {} - if user_input is not None: + if len(self._async_current_entries()) == 5: + return self.async_abort(reason="max_regions") + + if not self.states: websession = async_get_clientsession(self.hass) + reason = None + unknown_err_msg = None try: - regions = await Client( - websession, user_input[CONF_API_KEY] - ).get_regions() + regions = await Client(websession).get_regions() except aiohttp.ClientResponseError as ex: - errors["base"] = "invalid_api_key" if ex.status == 401 else "unknown" + if ex.status == 429: + reason = "rate_limit" + else: + reason = "unknown" + unknown_err_msg = str(ex) except aiohttp.ClientConnectionError: - errors["base"] = "cannot_connect" - except aiohttp.ClientError: - errors["base"] = "unknown" + reason = "cannot_connect" + except aiohttp.ClientError as ex: + reason = "unknown" + unknown_err_msg = str(ex) except asyncio.TimeoutError: - errors["base"] = "timeout" + reason = "timeout" - if not errors and not regions: - errors["base"] = "unknown" + if not reason and not regions: + reason = "unknown" + unknown_err_msg = "no regions returned" - if not errors: - self.api_key = user_input[CONF_API_KEY] - self.states = regions["states"] - return await self.async_step_state() + if unknown_err_msg: + _LOGGER.error("Failed to connect to the service: %s", unknown_err_msg) - schema = vol.Schema( - { - vol.Required(CONF_API_KEY): str, - } - ) + if reason: + return self.async_abort(reason=reason) + self.states = regions["states"] - return self.async_show_form( - step_id="user", - data_schema=schema, - description_placeholders={"api_url": "https://api.ukrainealarm.com/"}, - errors=errors, - last_step=False, - ) - - async def async_step_state(self, user_input=None): - """Handle user-chosen state.""" - return await self._handle_pick_region("state", "district", user_input) + return await self._handle_pick_region("user", "district", user_input) async def async_step_district(self, user_input=None): """Handle user-chosen district.""" @@ -126,7 +122,6 @@ class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=self.selected_region["regionName"], data={ - CONF_API_KEY: self.api_key, CONF_REGION: self.selected_region["regionId"], CONF_NAME: self.selected_region["regionName"], }, diff --git a/homeassistant/components/ukraine_alarm/manifest.json b/homeassistant/components/ukraine_alarm/manifest.json index 08dad9960b5..5592ac774a4 100644 --- a/homeassistant/components/ukraine_alarm/manifest.json +++ b/homeassistant/components/ukraine_alarm/manifest.json @@ -3,7 +3,7 @@ "name": "Ukraine Alarm", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ukraine_alarm", - "requirements": ["ukrainealarm==0.0.1"], + "requirements": ["uasiren==0.0.1"], "codeowners": ["@PaulAnnekov"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ukraine_alarm/strings.json b/homeassistant/components/ukraine_alarm/strings.json index 79f81e71b08..6831d66adb3 100644 --- a/homeassistant/components/ukraine_alarm/strings.json +++ b/homeassistant/components/ukraine_alarm/strings.json @@ -1,22 +1,15 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" - }, - "error": { - "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "max_regions": "Max 5 regions can be configured", + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]", + "rate_limit": "Too much requests", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]", "timeout": "[%key:common::config_flow::error::timeout_connect%]" }, "step": { "user": { - "data": { - "api_key": "[%key:common::config_flow::data::api_key%]" - }, - "description": "Set up the Ukraine Alarm integration. To generate an API key go to {api_url}" - }, - "state": { "data": { "region": "Region" }, @@ -24,13 +17,13 @@ }, "district": { "data": { - "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" + "region": "[%key:component::ukraine_alarm::config::step::user::data::region%]" }, "description": "If you want to monitor not only state, choose its specific district" }, "community": { "data": { - "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" + "region": "[%key:component::ukraine_alarm::config::step::user::data::region%]" }, "description": "If you want to monitor not only state and district, choose its specific community" } diff --git a/homeassistant/components/ukraine_alarm/translations/en.json b/homeassistant/components/ukraine_alarm/translations/en.json index e7b2a9edcdc..857311ea3e7 100644 --- a/homeassistant/components/ukraine_alarm/translations/en.json +++ b/homeassistant/components/ukraine_alarm/translations/en.json @@ -1,11 +1,10 @@ { "config": { "abort": { - "already_configured": "Location is already configured" - }, - "error": { + "already_configured": "Location is already configured", "cannot_connect": "Failed to connect", - "invalid_api_key": "Invalid API key", + "max_regions": "Max 5 regions can be configured", + "rate_limit": "Too much requests", "timeout": "Timeout establishing connection", "unknown": "Unexpected error" }, @@ -22,17 +21,11 @@ }, "description": "If you want to monitor not only state, choose its specific district" }, - "state": { + "user": { "data": { "region": "Region" }, "description": "Choose state to monitor" - }, - "user": { - "data": { - "api_key": "API Key" - }, - "description": "Set up the Ukraine Alarm integration. To generate an API key go to {api_url}" } } } diff --git a/requirements_all.txt b/requirements_all.txt index ac6727a95aa..e0c864fea9d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2349,7 +2349,7 @@ twitchAPI==2.5.2 uEagle==0.0.2 # homeassistant.components.ukraine_alarm -ukrainealarm==0.0.1 +uasiren==0.0.1 # homeassistant.components.unifiprotect unifi-discovery==1.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3289356d23a..e0f5459d43a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1531,7 +1531,7 @@ twitchAPI==2.5.2 uEagle==0.0.2 # homeassistant.components.ukraine_alarm -ukrainealarm==0.0.1 +uasiren==0.0.1 # homeassistant.components.unifiprotect unifi-discovery==1.1.2 diff --git a/tests/components/ukraine_alarm/test_config_flow.py b/tests/components/ukraine_alarm/test_config_flow.py index 3832e6a9fb6..7369816fdc7 100644 --- a/tests/components/ukraine_alarm/test_config_flow.py +++ b/tests/components/ukraine_alarm/test_config_flow.py @@ -3,15 +3,20 @@ import asyncio from collections.abc import Generator from unittest.mock import AsyncMock, patch -from aiohttp import ClientConnectionError, ClientError, ClientResponseError +from aiohttp import ClientConnectionError, ClientError, ClientResponseError, RequestInfo import pytest +from yarl import URL from homeassistant import config_entries from homeassistant.components.ukraine_alarm.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) -MOCK_API_KEY = "mock-api-key" +from tests.common import MockConfigEntry def _region(rid, recurse=0, depth=0): @@ -57,12 +62,7 @@ async def test_state(hass: HomeAssistant) -> None: ) assert result["type"] == RESULT_TYPE_FORM - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result2["type"] == RESULT_TYPE_FORM with patch( @@ -80,7 +80,6 @@ async def test_state(hass: HomeAssistant) -> None: assert result3["type"] == RESULT_TYPE_CREATE_ENTRY assert result3["title"] == "State 1" assert result3["data"] == { - "api_key": MOCK_API_KEY, "region": "1", "name": result3["title"], } @@ -94,12 +93,7 @@ async def test_state_district(hass: HomeAssistant) -> None: ) assert result["type"] == RESULT_TYPE_FORM - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result2["type"] == RESULT_TYPE_FORM result3 = await hass.config_entries.flow.async_configure( @@ -125,7 +119,6 @@ async def test_state_district(hass: HomeAssistant) -> None: assert result4["type"] == RESULT_TYPE_CREATE_ENTRY assert result4["title"] == "District 2.2" assert result4["data"] == { - "api_key": MOCK_API_KEY, "region": "2.2", "name": result4["title"], } @@ -139,12 +132,7 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: ) assert result["type"] == RESULT_TYPE_FORM - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result2["type"] == RESULT_TYPE_FORM result3 = await hass.config_entries.flow.async_configure( @@ -170,7 +158,6 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: assert result4["type"] == RESULT_TYPE_CREATE_ENTRY assert result4["title"] == "State 2" assert result4["data"] == { - "api_key": MOCK_API_KEY, "region": "2", "name": result4["title"], } @@ -186,9 +173,6 @@ async def test_state_district_community(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, ) assert result2["type"] == RESULT_TYPE_FORM @@ -223,132 +207,89 @@ async def test_state_district_community(hass: HomeAssistant) -> None: assert result5["type"] == RESULT_TYPE_CREATE_ENTRY assert result5["title"] == "Community 3.2.1" assert result5["data"] == { - "api_key": MOCK_API_KEY, "region": "3.2.1", "name": result5["title"], } assert len(mock_setup_entry.mock_calls) == 1 -async def test_invalid_api(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: - """Test we can create entry for just region.""" +async def test_max_regions(hass: HomeAssistant) -> None: + """Test max regions config.""" + for i in range(5): + MockConfigEntry( + domain=DOMAIN, + unique_id=i, + ).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 - mock_get_regions.side_effect = ClientResponseError(None, None, status=401) + assert result["type"] == "abort" + assert result["reason"] == "max_regions" - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, + +async def test_rate_limit(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: + """Test rate limit error.""" + mock_get_regions.side_effect = ClientResponseError(None, None, status=429) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "invalid_api_key"} + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "rate_limit" async def test_server_error(hass: HomeAssistant, mock_get_regions) -> None: - """Test we can create entry for just region.""" + """Test server error.""" + mock_get_regions.side_effect = ClientResponseError( + RequestInfo(None, None, None, real_url=URL("/regions")), None, status=500 + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - - mock_get_regions.side_effect = ClientResponseError(None, None, status=500) - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "unknown"} + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" async def test_cannot_connect(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: - """Test we can create entry for just region.""" + """Test connection error.""" + mock_get_regions.side_effect = ClientConnectionError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - - mock_get_regions.side_effect = ClientConnectionError - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "cannot_connect"} + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" async def test_unknown_client_error( hass: HomeAssistant, mock_get_regions: AsyncMock ) -> None: - """Test we can create entry for just region.""" + """Test client error.""" + mock_get_regions.side_effect = ClientError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - - mock_get_regions.side_effect = ClientError - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "unknown"} + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" async def test_timeout_error(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: - """Test we can create entry for just region.""" + """Test timeout error.""" + mock_get_regions.side_effect = asyncio.TimeoutError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - - mock_get_regions.side_effect = asyncio.TimeoutError - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "timeout"} + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "timeout" async def test_no_regions_returned( hass: HomeAssistant, mock_get_regions: AsyncMock ) -> None: - """Test we can create entry for just region.""" + """Test regions not returned.""" + mock_get_regions.return_value = {} result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - - mock_get_regions.return_value = {} - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "unknown"} + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" From 2500cc6132d7e5a2eb09cface512b92faba0f88d Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 13 May 2022 07:43:24 +0800 Subject: [PATCH 0442/3516] Add use_wallclock_as_timestamps option to generic (#71245) Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- homeassistant/components/generic/camera.py | 5 ++++ .../components/generic/config_flow.py | 20 ++++++++++++- homeassistant/components/generic/const.py | 6 +++- homeassistant/components/generic/strings.json | 4 +++ .../components/generic/translations/en.json | 6 ++-- tests/components/generic/test_config_flow.py | 30 +++++++++++++++++++ 6 files changed, 67 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index d20032e2607..197890efad3 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -37,6 +37,7 @@ from .const import ( CONF_RTSP_TRANSPORT, CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE, + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, DEFAULT_NAME, FFMPEG_OPTION_MAP, GET_IMAGE_TIMEOUT, @@ -160,6 +161,10 @@ class GenericCamera(Camera): CONF_RTSP_TRANSPORT ] self._auth = generate_auth(device_info) + if device_info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): + self.stream_options[ + FFMPEG_OPTION_MAP[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] + ] = "1" self._last_url = None self._last_image = None diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 086262aa0a1..0a49393d9cc 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -41,6 +41,7 @@ from .const import ( CONF_RTSP_TRANSPORT, CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE, + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, DEFAULT_NAME, DOMAIN, FFMPEG_OPTION_MAP, @@ -64,6 +65,7 @@ SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml", "webp"} def build_schema( user_input: dict[str, Any] | MappingProxyType[str, Any], is_options_flow: bool = False, + show_advanced_options=False, ): """Create schema for camera config setup.""" spec = { @@ -106,6 +108,13 @@ def build_schema( default=user_input.get(CONF_LIMIT_REFETCH_TO_URL_CHANGE, False), ) ] = bool + if show_advanced_options: + spec[ + vol.Required( + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, + default=user_input.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS, False), + ) + ] = bool return vol.Schema(spec) @@ -199,6 +208,8 @@ async def async_test_stream(hass, info) -> dict[str, str]: } if rtsp_transport := info.get(CONF_RTSP_TRANSPORT): stream_options[FFMPEG_OPTION_MAP[CONF_RTSP_TRANSPORT]] = rtsp_transport + if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): + stream_options[FFMPEG_OPTION_MAP[CONF_USE_WALLCLOCK_AS_TIMESTAMPS]] = "1" _LOGGER.debug("Attempting to open stream %s", stream_source) container = await hass.async_add_executor_job( partial( @@ -356,6 +367,9 @@ class GenericOptionsFlowHandler(OptionsFlow): ], CONF_FRAMERATE: user_input[CONF_FRAMERATE], CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL], + CONF_USE_WALLCLOCK_AS_TIMESTAMPS: user_input.get( + CONF_USE_WALLCLOCK_AS_TIMESTAMPS + ), } return self.async_create_entry( title=title, @@ -363,6 +377,10 @@ class GenericOptionsFlowHandler(OptionsFlow): ) return self.async_show_form( step_id="init", - data_schema=build_schema(user_input or self.config_entry.options, True), + data_schema=build_schema( + user_input or self.config_entry.options, + True, + self.show_advanced_options, + ), errors=errors, ) diff --git a/homeassistant/components/generic/const.py b/homeassistant/components/generic/const.py index 60b4cec61a6..8ae5f16c4c4 100644 --- a/homeassistant/components/generic/const.py +++ b/homeassistant/components/generic/const.py @@ -8,7 +8,11 @@ CONF_STILL_IMAGE_URL = "still_image_url" CONF_STREAM_SOURCE = "stream_source" CONF_FRAMERATE = "framerate" CONF_RTSP_TRANSPORT = "rtsp_transport" -FFMPEG_OPTION_MAP = {CONF_RTSP_TRANSPORT: "rtsp_transport"} +CONF_USE_WALLCLOCK_AS_TIMESTAMPS = "use_wallclock_as_timestamps" +FFMPEG_OPTION_MAP = { + CONF_RTSP_TRANSPORT: "rtsp_transport", + CONF_USE_WALLCLOCK_AS_TIMESTAMPS: "use_wallclock_as_timestamps", +} RTSP_TRANSPORTS = { "tcp": "TCP", "udp": "UDP", diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index 01b1fe48a82..0954656f71d 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -55,9 +55,13 @@ "authentication": "[%key:component::generic::config::step::user::data::authentication%]", "limit_refetch_to_url_change": "[%key:component::generic::config::step::user::data::limit_refetch_to_url_change%]", "password": "[%key:common::config_flow::data::password%]", + "use_wallclock_as_timestamps": "Use wallclock as timestamps", "username": "[%key:common::config_flow::data::username%]", "framerate": "[%key:component::generic::config::step::user::data::framerate%]", "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + }, + "data_description": { + "use_wallclock_as_timestamps": "This option may correct segmenting or crashing issues arising from buggy timestamp implementations on some cameras" } }, "content_type": { diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json index b158488f178..b552c780d29 100644 --- a/homeassistant/components/generic/translations/en.json +++ b/homeassistant/components/generic/translations/en.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "Authentication", - "content_type": "Content Type", "framerate": "Frame Rate (Hz)", "limit_refetch_to_url_change": "Limit refetch to url change", "password": "Password", @@ -72,15 +71,18 @@ "init": { "data": { "authentication": "Authentication", - "content_type": "Content Type", "framerate": "Frame Rate (Hz)", "limit_refetch_to_url_change": "Limit refetch to url change", "password": "Password", "rtsp_transport": "RTSP transport protocol", "still_image_url": "Still Image URL (e.g. http://...)", "stream_source": "Stream Source URL (e.g. rtsp://...)", + "use_wallclock_as_timestamps": "Use wallclock as timestamps", "username": "Username", "verify_ssl": "Verify SSL certificate" + }, + "data_description": { + "use_wallclock_as_timestamps": "This option may correct segmenting or crashing issues arising from buggy timestamp implementations on some cameras" } } } diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 457cac26aa5..dd53cb8548e 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -18,6 +18,7 @@ from homeassistant.components.generic.const import ( CONF_RTSP_TRANSPORT, CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE, + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, DOMAIN, ) from homeassistant.const import ( @@ -653,3 +654,32 @@ async def test_migrate_existing_ids(hass) -> None: entity_entry = registry.async_get(entity_id) assert entity_entry.unique_id == new_unique_id + + +@respx.mock +async def test_use_wallclock_as_timestamps_option(hass, fakeimg_png, mock_av_open): + """Test the use_wallclock_as_timestamps option flow.""" + + mock_entry = MockConfigEntry( + title="Test Camera", + domain=DOMAIN, + data={}, + options=TESTDATA, + ) + + with mock_av_open: + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init( + mock_entry.entry_id, context={"show_advanced_options": True} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY From f65eca9c195f340751c2092b9ea719bbb6fb8f65 Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Fri, 13 May 2022 02:45:39 +0300 Subject: [PATCH 0443/3516] Changed API for Ukraine Alarm (#71754) --- .../components/ukraine_alarm/__init__.py | 14 +- .../components/ukraine_alarm/config_flow.py | 67 ++++---- .../components/ukraine_alarm/manifest.json | 2 +- .../components/ukraine_alarm/strings.json | 17 +- .../ukraine_alarm/translations/en.json | 22 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../ukraine_alarm/test_config_flow.py | 161 ++++++------------ 8 files changed, 108 insertions(+), 179 deletions(-) diff --git a/homeassistant/components/ukraine_alarm/__init__.py b/homeassistant/components/ukraine_alarm/__init__.py index b2b2ff4162f..587854a3a7e 100644 --- a/homeassistant/components/ukraine_alarm/__init__.py +++ b/homeassistant/components/ukraine_alarm/__init__.py @@ -7,10 +7,10 @@ from typing import Any import aiohttp from aiohttp import ClientSession -from ukrainealarm.client import Client +from uasiren.client import Client from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_REGION +from homeassistant.const import CONF_REGION from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -24,14 +24,11 @@ UPDATE_INTERVAL = timedelta(seconds=10) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ukraine Alarm as config entry.""" - api_key = entry.data[CONF_API_KEY] region_id = entry.data[CONF_REGION] websession = async_get_clientsession(hass) - coordinator = UkraineAlarmDataUpdateCoordinator( - hass, websession, api_key, region_id - ) + coordinator = UkraineAlarmDataUpdateCoordinator(hass, websession, region_id) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator @@ -56,19 +53,18 @@ class UkraineAlarmDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): self, hass: HomeAssistant, session: ClientSession, - api_key: str, region_id: str, ) -> None: """Initialize.""" self.region_id = region_id - self.ukrainealarm = Client(session, api_key) + self.uasiren = Client(session) super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" try: - res = await self.ukrainealarm.get_alerts(self.region_id) + res = await self.uasiren.get_alerts(self.region_id) except aiohttp.ClientError as error: raise UpdateFailed(f"Error fetching alerts from API: {error}") from error diff --git a/homeassistant/components/ukraine_alarm/config_flow.py b/homeassistant/components/ukraine_alarm/config_flow.py index dcf41658dfb..4f1e1c5cf23 100644 --- a/homeassistant/components/ukraine_alarm/config_flow.py +++ b/homeassistant/components/ukraine_alarm/config_flow.py @@ -2,17 +2,20 @@ from __future__ import annotations import asyncio +import logging import aiohttp -from ukrainealarm.client import Client +from uasiren.client import Client import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_REGION +from homeassistant.const import CONF_NAME, CONF_REGION from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) + class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for Ukraine Alarm.""" @@ -21,54 +24,47 @@ class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize a new UkraineAlarmConfigFlow.""" - self.api_key = None self.states = None self.selected_region = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" - errors = {} - if user_input is not None: + if len(self._async_current_entries()) == 5: + return self.async_abort(reason="max_regions") + + if not self.states: websession = async_get_clientsession(self.hass) + reason = None + unknown_err_msg = None try: - regions = await Client( - websession, user_input[CONF_API_KEY] - ).get_regions() + regions = await Client(websession).get_regions() except aiohttp.ClientResponseError as ex: - errors["base"] = "invalid_api_key" if ex.status == 401 else "unknown" + if ex.status == 429: + reason = "rate_limit" + else: + reason = "unknown" + unknown_err_msg = str(ex) except aiohttp.ClientConnectionError: - errors["base"] = "cannot_connect" - except aiohttp.ClientError: - errors["base"] = "unknown" + reason = "cannot_connect" + except aiohttp.ClientError as ex: + reason = "unknown" + unknown_err_msg = str(ex) except asyncio.TimeoutError: - errors["base"] = "timeout" + reason = "timeout" - if not errors and not regions: - errors["base"] = "unknown" + if not reason and not regions: + reason = "unknown" + unknown_err_msg = "no regions returned" - if not errors: - self.api_key = user_input[CONF_API_KEY] - self.states = regions["states"] - return await self.async_step_state() + if unknown_err_msg: + _LOGGER.error("Failed to connect to the service: %s", unknown_err_msg) - schema = vol.Schema( - { - vol.Required(CONF_API_KEY): str, - } - ) + if reason: + return self.async_abort(reason=reason) + self.states = regions["states"] - return self.async_show_form( - step_id="user", - data_schema=schema, - description_placeholders={"api_url": "https://api.ukrainealarm.com/"}, - errors=errors, - last_step=False, - ) - - async def async_step_state(self, user_input=None): - """Handle user-chosen state.""" - return await self._handle_pick_region("state", "district", user_input) + return await self._handle_pick_region("user", "district", user_input) async def async_step_district(self, user_input=None): """Handle user-chosen district.""" @@ -126,7 +122,6 @@ class UkraineAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=self.selected_region["regionName"], data={ - CONF_API_KEY: self.api_key, CONF_REGION: self.selected_region["regionId"], CONF_NAME: self.selected_region["regionName"], }, diff --git a/homeassistant/components/ukraine_alarm/manifest.json b/homeassistant/components/ukraine_alarm/manifest.json index 08dad9960b5..5592ac774a4 100644 --- a/homeassistant/components/ukraine_alarm/manifest.json +++ b/homeassistant/components/ukraine_alarm/manifest.json @@ -3,7 +3,7 @@ "name": "Ukraine Alarm", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ukraine_alarm", - "requirements": ["ukrainealarm==0.0.1"], + "requirements": ["uasiren==0.0.1"], "codeowners": ["@PaulAnnekov"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ukraine_alarm/strings.json b/homeassistant/components/ukraine_alarm/strings.json index 79f81e71b08..6831d66adb3 100644 --- a/homeassistant/components/ukraine_alarm/strings.json +++ b/homeassistant/components/ukraine_alarm/strings.json @@ -1,22 +1,15 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" - }, - "error": { - "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "max_regions": "Max 5 regions can be configured", + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]", + "rate_limit": "Too much requests", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]", "timeout": "[%key:common::config_flow::error::timeout_connect%]" }, "step": { "user": { - "data": { - "api_key": "[%key:common::config_flow::data::api_key%]" - }, - "description": "Set up the Ukraine Alarm integration. To generate an API key go to {api_url}" - }, - "state": { "data": { "region": "Region" }, @@ -24,13 +17,13 @@ }, "district": { "data": { - "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" + "region": "[%key:component::ukraine_alarm::config::step::user::data::region%]" }, "description": "If you want to monitor not only state, choose its specific district" }, "community": { "data": { - "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" + "region": "[%key:component::ukraine_alarm::config::step::user::data::region%]" }, "description": "If you want to monitor not only state and district, choose its specific community" } diff --git a/homeassistant/components/ukraine_alarm/translations/en.json b/homeassistant/components/ukraine_alarm/translations/en.json index 2c39945cb87..857311ea3e7 100644 --- a/homeassistant/components/ukraine_alarm/translations/en.json +++ b/homeassistant/components/ukraine_alarm/translations/en.json @@ -1,15 +1,19 @@ { "config": { + "abort": { + "already_configured": "Location is already configured", + "cannot_connect": "Failed to connect", + "max_regions": "Max 5 regions can be configured", + "rate_limit": "Too much requests", + "timeout": "Timeout establishing connection", + "unknown": "Unexpected error" + }, "step": { - "user": { - "description": "Set up the Ukraine Alarm integration. To generate an API key go to {api_url}", - "title": "Ukraine Alarm" - }, - "state": { + "community": { "data": { "region": "Region" }, - "description": "Choose state to monitor" + "description": "If you want to monitor not only state and district, choose its specific community" }, "district": { "data": { @@ -17,12 +21,12 @@ }, "description": "If you want to monitor not only state, choose its specific district" }, - "community": { + "user": { "data": { "region": "Region" }, - "description": "If you want to monitor not only state and district, choose its specific community" + "description": "Choose state to monitor" } } } -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 0cb659a32d0..ef48c8117f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2343,7 +2343,7 @@ twitchAPI==2.5.2 uEagle==0.0.2 # homeassistant.components.ukraine_alarm -ukrainealarm==0.0.1 +uasiren==0.0.1 # homeassistant.components.unifiprotect unifi-discovery==1.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9a905e42fae..cb683ae0bab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1525,7 +1525,7 @@ twitchAPI==2.5.2 uEagle==0.0.2 # homeassistant.components.ukraine_alarm -ukrainealarm==0.0.1 +uasiren==0.0.1 # homeassistant.components.unifiprotect unifi-discovery==1.1.2 diff --git a/tests/components/ukraine_alarm/test_config_flow.py b/tests/components/ukraine_alarm/test_config_flow.py index 3832e6a9fb6..7369816fdc7 100644 --- a/tests/components/ukraine_alarm/test_config_flow.py +++ b/tests/components/ukraine_alarm/test_config_flow.py @@ -3,15 +3,20 @@ import asyncio from collections.abc import Generator from unittest.mock import AsyncMock, patch -from aiohttp import ClientConnectionError, ClientError, ClientResponseError +from aiohttp import ClientConnectionError, ClientError, ClientResponseError, RequestInfo import pytest +from yarl import URL from homeassistant import config_entries from homeassistant.components.ukraine_alarm.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) -MOCK_API_KEY = "mock-api-key" +from tests.common import MockConfigEntry def _region(rid, recurse=0, depth=0): @@ -57,12 +62,7 @@ async def test_state(hass: HomeAssistant) -> None: ) assert result["type"] == RESULT_TYPE_FORM - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result2["type"] == RESULT_TYPE_FORM with patch( @@ -80,7 +80,6 @@ async def test_state(hass: HomeAssistant) -> None: assert result3["type"] == RESULT_TYPE_CREATE_ENTRY assert result3["title"] == "State 1" assert result3["data"] == { - "api_key": MOCK_API_KEY, "region": "1", "name": result3["title"], } @@ -94,12 +93,7 @@ async def test_state_district(hass: HomeAssistant) -> None: ) assert result["type"] == RESULT_TYPE_FORM - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result2["type"] == RESULT_TYPE_FORM result3 = await hass.config_entries.flow.async_configure( @@ -125,7 +119,6 @@ async def test_state_district(hass: HomeAssistant) -> None: assert result4["type"] == RESULT_TYPE_CREATE_ENTRY assert result4["title"] == "District 2.2" assert result4["data"] == { - "api_key": MOCK_API_KEY, "region": "2.2", "name": result4["title"], } @@ -139,12 +132,7 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: ) assert result["type"] == RESULT_TYPE_FORM - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result2["type"] == RESULT_TYPE_FORM result3 = await hass.config_entries.flow.async_configure( @@ -170,7 +158,6 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: assert result4["type"] == RESULT_TYPE_CREATE_ENTRY assert result4["title"] == "State 2" assert result4["data"] == { - "api_key": MOCK_API_KEY, "region": "2", "name": result4["title"], } @@ -186,9 +173,6 @@ async def test_state_district_community(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, ) assert result2["type"] == RESULT_TYPE_FORM @@ -223,132 +207,89 @@ async def test_state_district_community(hass: HomeAssistant) -> None: assert result5["type"] == RESULT_TYPE_CREATE_ENTRY assert result5["title"] == "Community 3.2.1" assert result5["data"] == { - "api_key": MOCK_API_KEY, "region": "3.2.1", "name": result5["title"], } assert len(mock_setup_entry.mock_calls) == 1 -async def test_invalid_api(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: - """Test we can create entry for just region.""" +async def test_max_regions(hass: HomeAssistant) -> None: + """Test max regions config.""" + for i in range(5): + MockConfigEntry( + domain=DOMAIN, + unique_id=i, + ).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 - mock_get_regions.side_effect = ClientResponseError(None, None, status=401) + assert result["type"] == "abort" + assert result["reason"] == "max_regions" - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, + +async def test_rate_limit(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: + """Test rate limit error.""" + mock_get_regions.side_effect = ClientResponseError(None, None, status=429) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "invalid_api_key"} + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "rate_limit" async def test_server_error(hass: HomeAssistant, mock_get_regions) -> None: - """Test we can create entry for just region.""" + """Test server error.""" + mock_get_regions.side_effect = ClientResponseError( + RequestInfo(None, None, None, real_url=URL("/regions")), None, status=500 + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - - mock_get_regions.side_effect = ClientResponseError(None, None, status=500) - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "unknown"} + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" async def test_cannot_connect(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: - """Test we can create entry for just region.""" + """Test connection error.""" + mock_get_regions.side_effect = ClientConnectionError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - - mock_get_regions.side_effect = ClientConnectionError - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "cannot_connect"} + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" async def test_unknown_client_error( hass: HomeAssistant, mock_get_regions: AsyncMock ) -> None: - """Test we can create entry for just region.""" + """Test client error.""" + mock_get_regions.side_effect = ClientError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - - mock_get_regions.side_effect = ClientError - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "unknown"} + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" async def test_timeout_error(hass: HomeAssistant, mock_get_regions: AsyncMock) -> None: - """Test we can create entry for just region.""" + """Test timeout error.""" + mock_get_regions.side_effect = asyncio.TimeoutError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - - mock_get_regions.side_effect = asyncio.TimeoutError - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "timeout"} + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "timeout" async def test_no_regions_returned( hass: HomeAssistant, mock_get_regions: AsyncMock ) -> None: - """Test we can create entry for just region.""" + """Test regions not returned.""" + mock_get_regions.return_value = {} result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - - mock_get_regions.return_value = {} - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "api_key": MOCK_API_KEY, - }, - ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "unknown"} + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" From 8ab27f26b90bbb12898ecb10ff08e7225898b5e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 May 2022 20:11:43 -0400 Subject: [PATCH 0444/3516] Use ciso8601 for parsing datetimes with sqlalchemy sqlite dialect (#71766) --- homeassistant/components/recorder/models.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 38d6b3319aa..6edee252ea3 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -6,6 +6,7 @@ import json import logging from typing import Any, TypedDict, cast, overload +import ciso8601 from fnvhash import fnv1a_32 from sqlalchemy import ( BigInteger, @@ -22,7 +23,7 @@ from sqlalchemy import ( Text, distinct, ) -from sqlalchemy.dialects import mysql, oracle, postgresql +from sqlalchemy.dialects import mysql, oracle, postgresql, sqlite from sqlalchemy.engine.row import Row from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import declarative_base, relationship @@ -91,8 +92,18 @@ TABLES_TO_CHECK = [ EMPTY_JSON_OBJECT = "{}" -DATETIME_TYPE = DateTime(timezone=True).with_variant( - mysql.DATETIME(timezone=True, fsp=6), "mysql" +class FAST_PYSQLITE_DATETIME(sqlite.DATETIME): # type: ignore[misc] + """Use ciso8601 to parse datetimes instead of sqlalchemy built-in regex.""" + + def result_processor(self, dialect, coltype): # type: ignore[no-untyped-def] + """Offload the datetime parsing to ciso8601.""" + return lambda value: None if value is None else ciso8601.parse_datetime(value) + + +DATETIME_TYPE = ( + DateTime(timezone=True) + .with_variant(mysql.DATETIME(timezone=True, fsp=6), "mysql") + .with_variant(FAST_PYSQLITE_DATETIME(), "sqlite") ) DOUBLE_TYPE = ( Float() From 1d9fb4bca871f97109684419f0f9526a0c151f2d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 May 2022 20:12:50 -0400 Subject: [PATCH 0445/3516] Fix process_datetime_to_timestamp and add test coverage (#71755) --- homeassistant/components/recorder/models.py | 24 +++++-- tests/components/recorder/test_models.py | 72 +++++++++++++++++++++ 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 6edee252ea3..38b03eb824e 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -55,6 +55,10 @@ SCHEMA_VERSION = 28 _LOGGER = logging.getLogger(__name__) +# EPOCHORDINAL is not exposed as a constant +# https://github.com/python/cpython/blob/3.10/Lib/zoneinfo/_zoneinfo.py#L12 +EPOCHORDINAL = datetime(1970, 1, 1).toordinal() + DB_TIMEZONE = "+00:00" TABLE_EVENTS = "events" @@ -630,10 +634,22 @@ def process_timestamp_to_utc_isoformat(ts: datetime | None) -> str | None: def process_datetime_to_timestamp(ts: datetime) -> float: - """Process a timestamp into a unix timestamp.""" - if ts.tzinfo == dt_util.UTC: - return ts.timestamp() - return ts.replace(tzinfo=dt_util.UTC).timestamp() + """Process a datebase datetime to epoch. + + Mirrors the behavior of process_timestamp_to_utc_isoformat + except it returns the epoch time. + """ + if ts.tzinfo is None: + # Taken from + # https://github.com/python/cpython/blob/3.10/Lib/zoneinfo/_zoneinfo.py#L185 + return ( + (ts.toordinal() - EPOCHORDINAL) * 86400 + + ts.hour * 3600 + + ts.minute * 60 + + ts.second + + (ts.microsecond / 1000000) + ) + return ts.timestamp() class LazyState(State): diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index 874d84ef2ad..ca68d5951d8 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta from unittest.mock import PropertyMock +from freezegun import freeze_time import pytest from sqlalchemy import create_engine from sqlalchemy.orm import scoped_session, sessionmaker @@ -14,6 +15,7 @@ from homeassistant.components.recorder.models import ( RecorderRuns, StateAttributes, States, + process_datetime_to_timestamp, process_timestamp, process_timestamp_to_utc_isoformat, ) @@ -333,3 +335,73 @@ async def test_lazy_state_handles_same_last_updated_and_last_changed(caplog): "last_updated": "2020-06-12T03:04:01.000323+00:00", "state": "off", } + + +@pytest.mark.parametrize( + "time_zone", ["Europe/Berlin", "America/Chicago", "US/Hawaii", "UTC"] +) +def test_process_datetime_to_timestamp(time_zone, hass): + """Test we can handle processing database datatimes to timestamps.""" + hass.config.set_time_zone(time_zone) + utc_now = dt_util.utcnow() + assert process_datetime_to_timestamp(utc_now) == utc_now.timestamp() + now = dt_util.now() + assert process_datetime_to_timestamp(now) == now.timestamp() + + +@pytest.mark.parametrize( + "time_zone", ["Europe/Berlin", "America/Chicago", "US/Hawaii", "UTC"] +) +def test_process_datetime_to_timestamp_freeze_time(time_zone, hass): + """Test we can handle processing database datatimes to timestamps. + + This test freezes time to make sure everything matches. + """ + hass.config.set_time_zone(time_zone) + utc_now = dt_util.utcnow() + with freeze_time(utc_now): + epoch = utc_now.timestamp() + assert process_datetime_to_timestamp(dt_util.utcnow()) == epoch + now = dt_util.now() + assert process_datetime_to_timestamp(now) == epoch + + +@pytest.mark.parametrize( + "time_zone", ["Europe/Berlin", "America/Chicago", "US/Hawaii", "UTC"] +) +async def test_process_datetime_to_timestamp_mirrors_utc_isoformat_behavior( + time_zone, hass +): + """Test process_datetime_to_timestamp mirrors process_timestamp_to_utc_isoformat.""" + hass.config.set_time_zone(time_zone) + datetime_with_tzinfo = datetime(2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC) + datetime_without_tzinfo = datetime(2016, 7, 9, 11, 0, 0) + est = dt_util.get_time_zone("US/Eastern") + datetime_est_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=est) + est = dt_util.get_time_zone("US/Eastern") + datetime_est_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=est) + nst = dt_util.get_time_zone("Canada/Newfoundland") + datetime_nst_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=nst) + hst = dt_util.get_time_zone("US/Hawaii") + datetime_hst_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=hst) + + assert ( + process_datetime_to_timestamp(datetime_with_tzinfo) + == dt_util.parse_datetime("2016-07-09T11:00:00+00:00").timestamp() + ) + assert ( + process_datetime_to_timestamp(datetime_without_tzinfo) + == dt_util.parse_datetime("2016-07-09T11:00:00+00:00").timestamp() + ) + assert ( + process_datetime_to_timestamp(datetime_est_timezone) + == dt_util.parse_datetime("2016-07-09T15:00:00+00:00").timestamp() + ) + assert ( + process_datetime_to_timestamp(datetime_nst_timezone) + == dt_util.parse_datetime("2016-07-09T13:30:00+00:00").timestamp() + ) + assert ( + process_datetime_to_timestamp(datetime_hst_timezone) + == dt_util.parse_datetime("2016-07-09T21:00:00+00:00").timestamp() + ) From 24a0007785e65e1043e0bdd55e7524ef0a7ccdad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 May 2022 20:21:14 -0400 Subject: [PATCH 0446/3516] Add additional context data to logbook events (#71721) --- .../components/automation/logbook.py | 2 +- homeassistant/components/deconz/logbook.py | 10 +- homeassistant/components/doorbird/logbook.py | 2 +- .../components/homeassistant/logbook.py | 39 +++++ homeassistant/components/logbook/__init__.py | 135 +++++++-------- homeassistant/components/logbook/queries.py | 4 + homeassistant/components/shelly/logbook.py | 2 +- tests/components/automation/test_init.py | 4 +- tests/components/automation/test_logbook.py | 4 +- tests/components/deconz/test_logbook.py | 12 +- tests/components/logbook/test_init.py | 156 ++++++++++++++---- tests/components/shelly/test_logbook.py | 8 +- 12 files changed, 254 insertions(+), 124 deletions(-) create mode 100644 homeassistant/components/homeassistant/logbook.py diff --git a/homeassistant/components/automation/logbook.py b/homeassistant/components/automation/logbook.py index 86fb797ea31..97a859d25b0 100644 --- a/homeassistant/components/automation/logbook.py +++ b/homeassistant/components/automation/logbook.py @@ -15,7 +15,7 @@ def async_describe_events(hass: HomeAssistant, async_describe_event): # type: i def async_describe_logbook_event(event: LazyEventPartialState): # type: ignore[no-untyped-def] """Describe a logbook event.""" data = event.data - message = "has been triggered" + message = "triggered" if ATTR_SOURCE in data: message = f"{message} by {data[ATTR_SOURCE]}" diff --git a/homeassistant/components/deconz/logbook.py b/homeassistant/components/deconz/logbook.py index 3dedeb4bfac..e67e07d2222 100644 --- a/homeassistant/components/deconz/logbook.py +++ b/homeassistant/components/deconz/logbook.py @@ -136,7 +136,7 @@ def async_describe_events( return { "name": f"{deconz_alarm_event.device.name}", - "message": f"fired event '{data}'.", + "message": f"fired event '{data}'", } @callback @@ -158,26 +158,26 @@ def async_describe_events( if not data: return { "name": f"{deconz_event.device.name}", - "message": "fired an unknown event.", + "message": "fired an unknown event", } # No device event match if not action: return { "name": f"{deconz_event.device.name}", - "message": f"fired event '{data}'.", + "message": f"fired event '{data}'", } # Gesture event if not interface: return { "name": f"{deconz_event.device.name}", - "message": f"fired event '{ACTIONS[action]}'.", + "message": f"fired event '{ACTIONS[action]}'", } return { "name": f"{deconz_event.device.name}", - "message": f"'{ACTIONS[action]}' event for '{INTERFACES[interface]}' was fired.", + "message": f"'{ACTIONS[action]}' event for '{INTERFACES[interface]}' was fired", } async_describe_event( diff --git a/homeassistant/components/doorbird/logbook.py b/homeassistant/components/doorbird/logbook.py index a4889360d81..fbd6c670a8d 100644 --- a/homeassistant/components/doorbird/logbook.py +++ b/homeassistant/components/doorbird/logbook.py @@ -17,7 +17,7 @@ def async_describe_events(hass, async_describe_event): return { "name": "Doorbird", - "message": f"Event {event.event_type} was fired.", + "message": f"Event {event.event_type} was fired", "entity_id": hass.data[DOMAIN][DOOR_STATION_EVENT_ENTITY_IDS].get( doorbird_event, event.data.get(ATTR_ENTITY_ID) ), diff --git a/homeassistant/components/homeassistant/logbook.py b/homeassistant/components/homeassistant/logbook.py new file mode 100644 index 00000000000..229fb24cb27 --- /dev/null +++ b/homeassistant/components/homeassistant/logbook.py @@ -0,0 +1,39 @@ +"""Describe homeassistant logbook events.""" +from __future__ import annotations + +from collections.abc import Callable + +from homeassistant.components.logbook import ( + LOGBOOK_ENTRY_ICON, + LOGBOOK_ENTRY_MESSAGE, + LOGBOOK_ENTRY_NAME, +) +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, HomeAssistant, callback + +from . import DOMAIN + +EVENT_TO_NAME = { + EVENT_HOMEASSISTANT_STOP: "stopped", + EVENT_HOMEASSISTANT_START: "started", +} + + +@callback +def async_describe_events( + hass: HomeAssistant, + async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None], +) -> None: + """Describe logbook events.""" + + @callback + def async_describe_hass_event(event: Event) -> dict[str, str]: + """Describe homeassisant logbook event.""" + return { + LOGBOOK_ENTRY_NAME: "Home Assistant", + LOGBOOK_ENTRY_MESSAGE: EVENT_TO_NAME[event.event_type], + LOGBOOK_ENTRY_ICON: "mdi:home-assistant", + } + + async_describe_event(DOMAIN, EVENT_HOMEASSISTANT_STOP, async_describe_hass_event) + async_describe_event(DOMAIN, EVENT_HOMEASSISTANT_START, async_describe_hass_event) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index df8143d39fb..80748768c55 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -37,13 +37,10 @@ from homeassistant.const import ( ATTR_NAME, ATTR_SERVICE, EVENT_CALL_SERVICE, - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, ) from homeassistant.core import ( - DOMAIN as HA_DOMAIN, Context, Event, HomeAssistant, @@ -70,7 +67,6 @@ from .queries import statement_for_request _LOGGER = logging.getLogger(__name__) - FRIENDLY_NAME_JSON_EXTRACT = re.compile('"friendly_name": ?"([^"]+)"') ENTITY_ID_JSON_EXTRACT = re.compile('"entity_id": ?"([^"]+)"') DOMAIN_JSON_EXTRACT = re.compile('"domain": ?"([^"]+)"') @@ -79,19 +75,28 @@ ATTR_MESSAGE = "message" DOMAIN = "logbook" -HA_DOMAIN_ENTITY_ID = f"{HA_DOMAIN}._" - CONFIG_SCHEMA = vol.Schema( {DOMAIN: INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA}, extra=vol.ALLOW_EXTRA ) -HOMEASSISTANT_EVENTS = {EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP} +CONTEXT_USER_ID = "context_user_id" +CONTEXT_ENTITY_ID = "context_entity_id" +CONTEXT_ENTITY_ID_NAME = "context_entity_id_name" +CONTEXT_EVENT_TYPE = "context_event_type" +CONTEXT_DOMAIN = "context_domain" +CONTEXT_SERVICE = "context_service" +CONTEXT_NAME = "context_name" +CONTEXT_MESSAGE = "context_message" -ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED = ( - EVENT_LOGBOOK_ENTRY, - EVENT_CALL_SERVICE, - *HOMEASSISTANT_EVENTS, -) +LOGBOOK_ENTRY_DOMAIN = "domain" +LOGBOOK_ENTRY_ENTITY_ID = "entity_id" +LOGBOOK_ENTRY_ICON = "icon" +LOGBOOK_ENTRY_MESSAGE = "message" +LOGBOOK_ENTRY_NAME = "name" +LOGBOOK_ENTRY_STATE = "state" +LOGBOOK_ENTRY_WHEN = "when" + +ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED = {EVENT_LOGBOOK_ENTRY, EVENT_CALL_SERVICE} SCRIPT_AUTOMATION_EVENTS = {EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED} @@ -133,12 +138,12 @@ def async_log_entry( context: Context | None = None, ) -> None: """Add an entry to the logbook.""" - data = {ATTR_NAME: name, ATTR_MESSAGE: message} + data = {LOGBOOK_ENTRY_NAME: name, LOGBOOK_ENTRY_MESSAGE: message} if domain is not None: - data[ATTR_DOMAIN] = domain + data[LOGBOOK_ENTRY_DOMAIN] = domain if entity_id is not None: - data[ATTR_ENTITY_ID] = entity_id + data[LOGBOOK_ENTRY_ENTITY_ID] = entity_id hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data, context=context) @@ -162,7 +167,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: message.hass = hass message = message.async_render(parse_result=False) - async_log_entry(hass, name, message, domain, entity_id) + async_log_entry(hass, name, message, domain, entity_id, service.context) frontend.async_register_built_in_panel( hass, "logbook", "logbook", "hass:format-list-bulleted-type" @@ -375,13 +380,13 @@ def _humanify( continue data = { - "when": format_time(row), - "name": entity_name_cache.get(entity_id, row), - "state": row.state, - "entity_id": entity_id, + LOGBOOK_ENTRY_WHEN: format_time(row), + LOGBOOK_ENTRY_NAME: entity_name_cache.get(entity_id, row), + LOGBOOK_ENTRY_STATE: row.state, + LOGBOOK_ENTRY_ENTITY_ID: entity_id, } if icon := _row_attributes_extract(row, ICON_JSON_EXTRACT): - data["icon"] = icon + data[LOGBOOK_ENTRY_ICON] = icon context_augmenter.augment(data, entity_id, row) yield data @@ -389,26 +394,11 @@ def _humanify( elif event_type in external_events: domain, describe_event = external_events[event_type] data = describe_event(event_cache.get(row)) - data["when"] = format_time(row) - data["domain"] = domain + data[LOGBOOK_ENTRY_WHEN] = format_time(row) + data[LOGBOOK_ENTRY_DOMAIN] = domain context_augmenter.augment(data, data.get(ATTR_ENTITY_ID), row) yield data - elif event_type == EVENT_HOMEASSISTANT_START: - yield { - "when": format_time(row), - "name": "Home Assistant", - "message": "started", - "domain": HA_DOMAIN, - } - elif event_type == EVENT_HOMEASSISTANT_STOP: - yield { - "when": format_time(row), - "name": "Home Assistant", - "message": "stopped", - "domain": HA_DOMAIN, - } - elif event_type == EVENT_LOGBOOK_ENTRY: event = event_cache.get(row) event_data = event.data @@ -419,11 +409,11 @@ def _humanify( domain = split_entity_id(str(entity_id))[0] data = { - "when": format_time(row), - "name": event_data.get(ATTR_NAME), - "message": event_data.get(ATTR_MESSAGE), - "domain": domain, - "entity_id": entity_id, + LOGBOOK_ENTRY_WHEN: format_time(row), + LOGBOOK_ENTRY_NAME: event_data.get(ATTR_NAME), + LOGBOOK_ENTRY_MESSAGE: event_data.get(ATTR_MESSAGE), + LOGBOOK_ENTRY_DOMAIN: domain, + LOGBOOK_ENTRY_ENTITY_ID: entity_id, } context_augmenter.augment(data, entity_id, row) yield data @@ -505,9 +495,6 @@ def _keep_row( row: Row, entities_filter: EntityFilter | Callable[[str], bool] | None = None, ) -> bool: - if event_type in HOMEASSISTANT_EVENTS: - return entities_filter is None or entities_filter(HA_DOMAIN_ENTITY_ID) - if entity_id := _row_event_data_extract(row, ENTITY_ID_JSON_EXTRACT): return entities_filter is None or entities_filter(entity_id) @@ -544,7 +531,7 @@ class ContextAugmenter: def augment(self, data: dict[str, Any], entity_id: str | None, row: Row) -> None: """Augment data from the row and cache.""" if context_user_id := row.context_user_id: - data["context_user_id"] = context_user_id + data[CONTEXT_USER_ID] = context_user_id if not (context_row := self.context_lookup.get(row.context_id)): return @@ -567,43 +554,40 @@ class ContextAugmenter: # State change if context_entity_id := context_row.entity_id: - data["context_entity_id"] = context_entity_id - data["context_entity_id_name"] = self.entity_name_cache.get( + data[CONTEXT_ENTITY_ID] = context_entity_id + data[CONTEXT_ENTITY_ID_NAME] = self.entity_name_cache.get( context_entity_id, context_row ) - data["context_event_type"] = event_type + data[CONTEXT_EVENT_TYPE] = event_type return # Call service if event_type == EVENT_CALL_SERVICE: event = self.event_cache.get(context_row) event_data = event.data - data["context_domain"] = event_data.get(ATTR_DOMAIN) - data["context_service"] = event_data.get(ATTR_SERVICE) - data["context_event_type"] = event_type + data[CONTEXT_DOMAIN] = event_data.get(ATTR_DOMAIN) + data[CONTEXT_SERVICE] = event_data.get(ATTR_SERVICE) + data[CONTEXT_EVENT_TYPE] = event_type return - if not entity_id: + if event_type not in self.external_events: return - attr_entity_id = _row_event_data_extract(context_row, ENTITY_ID_JSON_EXTRACT) - if attr_entity_id is None or ( - event_type in SCRIPT_AUTOMATION_EVENTS and attr_entity_id == entity_id - ): + domain, describe_event = self.external_events[event_type] + data[CONTEXT_EVENT_TYPE] = event_type + data[CONTEXT_DOMAIN] = domain + event = self.event_cache.get(context_row) + described = describe_event(event) + if name := described.get(ATTR_NAME): + data[CONTEXT_NAME] = name + if message := described.get(ATTR_MESSAGE): + data[CONTEXT_MESSAGE] = message + if not (attr_entity_id := described.get(ATTR_ENTITY_ID)): return - - data["context_entity_id"] = attr_entity_id - data["context_entity_id_name"] = self.entity_name_cache.get( + data[CONTEXT_ENTITY_ID] = attr_entity_id + data[CONTEXT_ENTITY_ID_NAME] = self.entity_name_cache.get( attr_entity_id, context_row ) - data["context_event_type"] = event_type - - if event_type in self.external_events: - domain, describe_event = self.external_events[event_type] - data["context_domain"] = domain - event = self.event_cache.get(context_row) - if name := describe_event(event).get(ATTR_NAME): - data["context_name"] = name def _is_sensor_continuous( @@ -627,11 +611,14 @@ def _is_sensor_continuous( def _rows_match(row: Row, other_row: Row) -> bool: """Check of rows match by using the same method as Events __hash__.""" - return bool( - row.event_type == other_row.event_type - and row.context_id == other_row.context_id - and row.time_fired == other_row.time_fired - ) + if ( + (state_id := row.state_id) is not None + and state_id == other_row.state_id + or (event_id := row.event_id) is not None + and event_id == other_row.event_id + ): + return True + return False def _row_event_data_extract(row: Row, extractor: re.Pattern) -> str | None: diff --git a/homeassistant/components/logbook/queries.py b/homeassistant/components/logbook/queries.py index 08f6d759d25..2456c73fe22 100644 --- a/homeassistant/components/logbook/queries.py +++ b/homeassistant/components/logbook/queries.py @@ -33,6 +33,7 @@ UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%" EVENT_COLUMNS = ( + Events.event_id.label("event_id"), Events.event_type.label("event_type"), Events.event_data.label("event_data"), Events.time_fired.label("time_fired"), @@ -42,6 +43,7 @@ EVENT_COLUMNS = ( ) STATE_COLUMNS = ( + States.state_id.label("state_id"), States.state.label("state"), States.entity_id.label("entity_id"), States.attributes.label("attributes"), @@ -49,6 +51,7 @@ STATE_COLUMNS = ( ) EMPTY_STATE_COLUMNS = ( + literal(value=None, type_=sqlalchemy.String).label("state_id"), literal(value=None, type_=sqlalchemy.String).label("state"), literal(value=None, type_=sqlalchemy.String).label("entity_id"), literal(value=None, type_=sqlalchemy.Text).label("attributes"), @@ -294,6 +297,7 @@ def _select_states(start_day: dt, end_day: dt) -> Select: old_state = aliased(States, name="old_state") return ( select( + literal(value=None, type_=sqlalchemy.Text).label("event_id"), literal(value=EVENT_STATE_CHANGED, type_=sqlalchemy.String).label( "event_type" ), diff --git a/homeassistant/components/shelly/logbook.py b/homeassistant/components/shelly/logbook.py index d4278e3e98e..504dfe90791 100644 --- a/homeassistant/components/shelly/logbook.py +++ b/homeassistant/components/shelly/logbook.py @@ -49,7 +49,7 @@ def async_describe_events( return { "name": "Shelly", - "message": f"'{click_type}' click event for {input_name} Input was fired.", + "message": f"'{click_type}' click event for {input_name} Input was fired", } async_describe_event(DOMAIN, EVENT_SHELLY_CLICK, async_describe_shelly_click_event) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index af88adc00b7..bcbcf382892 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1243,12 +1243,12 @@ async def test_logbook_humanify_automation_triggered_event(hass): assert event1["name"] == "Hello Automation" assert event1["domain"] == "automation" - assert event1["message"] == "has been triggered" + assert event1["message"] == "triggered" assert event1["entity_id"] == "automation.hello" assert event2["name"] == "Bye Automation" assert event2["domain"] == "automation" - assert event2["message"] == "has been triggered by source of trigger" + assert event2["message"] == "triggered by source of trigger" assert event2["entity_id"] == "automation.bye" diff --git a/tests/components/automation/test_logbook.py b/tests/components/automation/test_logbook.py index a3299d806ce..1f726f478bd 100644 --- a/tests/components/automation/test_logbook.py +++ b/tests/components/automation/test_logbook.py @@ -37,13 +37,13 @@ async def test_humanify_automation_trigger_event(hass): ) assert event1["name"] == "Bla" - assert event1["message"] == "has been triggered by state change of input_boolean.yo" + assert event1["message"] == "triggered by state change of input_boolean.yo" assert event1["source"] == "state change of input_boolean.yo" assert event1["context_id"] == context.id assert event1["entity_id"] == "automation.bla" assert event2["name"] == "Bla" - assert event2["message"] == "has been triggered" + assert event2["message"] == "triggered" assert event2["source"] is None assert event2["context_id"] == context.id assert event2["entity_id"] == "automation.bla" diff --git a/tests/components/deconz/test_logbook.py b/tests/components/deconz/test_logbook.py index 98245a0df20..a78e795573a 100644 --- a/tests/components/deconz/test_logbook.py +++ b/tests/components/deconz/test_logbook.py @@ -85,7 +85,7 @@ async def test_humanifying_deconz_alarm_event(hass, aioclient_mock): assert events[0]["name"] == "Keypad" assert events[0]["domain"] == "deconz" - assert events[0]["message"] == "fired event 'armed_away'." + assert events[0]["message"] == "fired event 'armed_away'" async def test_humanifying_deconz_event(hass, aioclient_mock): @@ -214,20 +214,20 @@ async def test_humanifying_deconz_event(hass, aioclient_mock): assert events[0]["name"] == "Switch 1" assert events[0]["domain"] == "deconz" - assert events[0]["message"] == "fired event '2000'." + assert events[0]["message"] == "fired event '2000'" assert events[1]["name"] == "Hue remote" assert events[1]["domain"] == "deconz" - assert events[1]["message"] == "'Long press' event for 'Dim up' was fired." + assert events[1]["message"] == "'Long press' event for 'Dim up' was fired" assert events[2]["name"] == "Xiaomi cube" assert events[2]["domain"] == "deconz" - assert events[2]["message"] == "fired event 'Shake'." + assert events[2]["message"] == "fired event 'Shake'" assert events[3]["name"] == "Xiaomi cube" assert events[3]["domain"] == "deconz" - assert events[3]["message"] == "fired event 'unsupported_gesture'." + assert events[3]["message"] == "fired event 'unsupported_gesture'" assert events[4]["name"] == "Faulty event" assert events[4]["domain"] == "deconz" - assert events[4]["message"] == "fired an unknown event." + assert events[4]["message"] == "fired an unknown event" diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index d382506a3db..82857e6735c 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -1,5 +1,6 @@ """The tests for the logbook component.""" # pylint: disable=protected-access,invalid-name +import asyncio import collections from datetime import datetime, timedelta from http import HTTPStatus @@ -208,8 +209,10 @@ async def test_filter_sensor(hass_: ha.HomeAssistant, hass_client): _assert_entry(entries[2], name="ble", entity_id=entity_id4, state="10") -def test_home_assistant_start_stop_not_grouped(hass_): +async def test_home_assistant_start_stop_not_grouped(hass_): """Test if HA start and stop events are no longer grouped.""" + await async_setup_component(hass_, "homeassistant", {}) + await hass_.async_block_till_done() entries = mock_humanify( hass_, ( @@ -223,8 +226,10 @@ def test_home_assistant_start_stop_not_grouped(hass_): assert_entry(entries[1], name="Home Assistant", message="started", domain=ha.DOMAIN) -def test_home_assistant_start(hass_): +async def test_home_assistant_start(hass_): """Test if HA start is not filtered or converted into a restart.""" + await async_setup_component(hass_, "homeassistant", {}) + await hass_.async_block_till_done() entity_id = "switch.bla" pointA = dt_util.utcnow() @@ -604,9 +609,12 @@ async def test_logbook_view_end_time_entity(hass, hass_client, recorder_mock): async def test_logbook_entity_filter_with_automations(hass, hass_client, recorder_mock): """Test the logbook view with end_time and entity with automations and scripts.""" - await async_setup_component(hass, "logbook", {}) - await async_setup_component(hass, "automation", {}) - await async_setup_component(hass, "script", {}) + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) await async_recorder_block_till_done(hass) @@ -749,7 +757,12 @@ async def test_filter_continuous_sensor_values( async def test_exclude_new_entities(hass, hass_client, recorder_mock, set_utc): """Test if events are excluded on first update.""" - await async_setup_component(hass, "logbook", {}) + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook") + ] + ) await async_recorder_block_till_done(hass) entity_id = "climate.bla" @@ -781,7 +794,12 @@ async def test_exclude_new_entities(hass, hass_client, recorder_mock, set_utc): async def test_exclude_removed_entities(hass, hass_client, recorder_mock, set_utc): """Test if events are excluded on last update.""" - await async_setup_component(hass, "logbook", {}) + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook") + ] + ) await async_recorder_block_till_done(hass) entity_id = "climate.bla" @@ -820,7 +838,12 @@ async def test_exclude_removed_entities(hass, hass_client, recorder_mock, set_ut async def test_exclude_attribute_changes(hass, hass_client, recorder_mock, set_utc): """Test if events of attribute changes are filtered.""" - await async_setup_component(hass, "logbook", {}) + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook") + ] + ) await async_recorder_block_till_done(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -855,9 +878,12 @@ async def test_exclude_attribute_changes(hass, hass_client, recorder_mock, set_u async def test_logbook_entity_context_id(hass, recorder_mock, hass_client): """Test the logbook view with end_time and entity with automations and scripts.""" - await async_setup_component(hass, "logbook", {}) - await async_setup_component(hass, "automation", {}) - await async_setup_component(hass, "script", {}) + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) await async_recorder_block_till_done(hass) @@ -1004,9 +1030,12 @@ async def test_logbook_context_id_automation_script_started_manually( hass, recorder_mock, hass_client ): """Test the logbook populates context_ids for scripts and automations started manually.""" - await async_setup_component(hass, "logbook", {}) - await async_setup_component(hass, "automation", {}) - await async_setup_component(hass, "script", {}) + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) await async_recorder_block_till_done(hass) @@ -1032,6 +1061,19 @@ async def test_logbook_context_id_automation_script_started_manually( ) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + + script_2_context = ha.Context( + id="1234", + user_id="b400facee45711eaa9308bfd3d19e474", + ) + hass.bus.async_fire( + EVENT_SCRIPT_STARTED, + {ATTR_NAME: "Mock script"}, + context=script_2_context, + ) + hass.states.async_set("switch.new", STATE_ON, context=script_2_context) + hass.states.async_set("switch.new", STATE_OFF, context=script_2_context) + await hass.async_block_till_done() await async_wait_recording_done(hass) @@ -1061,12 +1103,28 @@ async def test_logbook_context_id_automation_script_started_manually( assert json_dict[2]["domain"] == "homeassistant" + assert json_dict[3]["entity_id"] is None + assert json_dict[3]["name"] == "Mock script" + assert "context_entity_id" not in json_dict[1] + assert json_dict[3]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + assert json_dict[3]["context_id"] == "1234" + + assert json_dict[4]["entity_id"] == "switch.new" + assert json_dict[4]["state"] == "off" + assert "context_entity_id" not in json_dict[1] + assert json_dict[4]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + assert json_dict[4]["context_event_type"] == "script_started" + assert json_dict[4]["context_domain"] == "script" + async def test_logbook_entity_context_parent_id(hass, hass_client, recorder_mock): """Test the logbook view links events via context parent_id.""" - await async_setup_component(hass, "logbook", {}) - await async_setup_component(hass, "automation", {}) - await async_setup_component(hass, "script", {}) + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) await async_recorder_block_till_done(hass) @@ -1240,7 +1298,13 @@ async def test_logbook_entity_context_parent_id(hass, hass_client, recorder_mock async def test_logbook_context_from_template(hass, hass_client, recorder_mock): """Test the logbook view with end_time and entity with automations and scripts.""" - await async_setup_component(hass, "logbook", {}) + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook") + ] + ) + assert await async_setup_component( hass, "switch", @@ -1637,7 +1701,13 @@ async def test_logbook_invalid_entity(hass, hass_client, recorder_mock): async def test_icon_and_state(hass, hass_client, recorder_mock): """Test to ensure state and custom icons are returned.""" - await async_setup_component(hass, "logbook", {}) + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook") + ] + ) + await async_recorder_block_till_done(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -1705,6 +1775,7 @@ async def test_exclude_events_domain(hass, hass_client, recorder_mock): entity_id = "switch.bla" entity_id2 = "sensor.blu" + await async_setup_component(hass, "homeassistant", {}) config = logbook.CONFIG_SCHEMA( { ha.DOMAIN: {}, @@ -1750,7 +1821,10 @@ async def test_exclude_events_domain_glob(hass, hass_client, recorder_mock): }, } ) - await async_setup_component(hass, "logbook", config) + await asyncio.gather( + async_setup_component(hass, "homeassistant", {}), + async_setup_component(hass, "logbook", config), + ) await async_recorder_block_till_done(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -1789,7 +1863,10 @@ async def test_include_events_entity(hass, hass_client, recorder_mock): }, } ) - await async_setup_component(hass, "logbook", config) + await asyncio.gather( + async_setup_component(hass, "homeassistant", {}), + async_setup_component(hass, "logbook", config), + ) await async_recorder_block_till_done(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -1821,7 +1898,10 @@ async def test_exclude_events_entity(hass, hass_client, recorder_mock): logbook.DOMAIN: {CONF_EXCLUDE: {CONF_ENTITIES: [entity_id]}}, } ) - await async_setup_component(hass, "logbook", config) + await asyncio.gather( + async_setup_component(hass, "homeassistant", {}), + async_setup_component(hass, "logbook", config), + ) await async_recorder_block_till_done(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -1854,7 +1934,10 @@ async def test_include_events_domain(hass, hass_client, recorder_mock): }, } ) - await async_setup_component(hass, "logbook", config) + await asyncio.gather( + async_setup_component(hass, "homeassistant", {}), + async_setup_component(hass, "logbook", config), + ) await async_recorder_block_till_done(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -1897,7 +1980,10 @@ async def test_include_events_domain_glob(hass, hass_client, recorder_mock): }, } ) - await async_setup_component(hass, "logbook", config) + await asyncio.gather( + async_setup_component(hass, "homeassistant", {}), + async_setup_component(hass, "logbook", config), + ) await async_recorder_block_till_done(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -1948,7 +2034,10 @@ async def test_include_exclude_events(hass, hass_client, recorder_mock): }, } ) - await async_setup_component(hass, "logbook", config) + await asyncio.gather( + async_setup_component(hass, "homeassistant", {}), + async_setup_component(hass, "logbook", config), + ) await async_recorder_block_till_done(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -2004,7 +2093,10 @@ async def test_include_exclude_events_with_glob_filters( }, } ) - await async_setup_component(hass, "logbook", config) + await asyncio.gather( + async_setup_component(hass, "homeassistant", {}), + async_setup_component(hass, "logbook", config), + ) await async_recorder_block_till_done(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -2047,7 +2139,10 @@ async def test_empty_config(hass, hass_client, recorder_mock): logbook.DOMAIN: {}, } ) - await async_setup_component(hass, "logbook", config) + await asyncio.gather( + async_setup_component(hass, "homeassistant", {}), + async_setup_component(hass, "logbook", config), + ) await async_recorder_block_till_done(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -2141,7 +2236,12 @@ def _assert_entry( async def test_get_events(hass, hass_ws_client, recorder_mock): """Test logbook get_events.""" now = dt_util.utcnow() - await async_setup_component(hass, "logbook", {}) + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook") + ] + ) await async_recorder_block_till_done(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) diff --git a/tests/components/shelly/test_logbook.py b/tests/components/shelly/test_logbook.py index 1ba7ea7ed16..5e267dcfd8f 100644 --- a/tests/components/shelly/test_logbook.py +++ b/tests/components/shelly/test_logbook.py @@ -46,14 +46,14 @@ async def test_humanify_shelly_click_event_block_device(hass, coap_wrapper): assert event1["domain"] == DOMAIN assert ( event1["message"] - == "'single' click event for Test name channel 1 Input was fired." + == "'single' click event for Test name channel 1 Input was fired" ) assert event2["name"] == "Shelly" assert event2["domain"] == DOMAIN assert ( event2["message"] - == "'long' click event for shellyswitch25-12345678 channel 2 Input was fired." + == "'long' click event for shellyswitch25-12345678 channel 2 Input was fired" ) @@ -91,12 +91,12 @@ async def test_humanify_shelly_click_event_rpc_device(hass, rpc_wrapper): assert event1["domain"] == DOMAIN assert ( event1["message"] - == "'single_push' click event for test switch_0 Input was fired." + == "'single_push' click event for test switch_0 Input was fired" ) assert event2["name"] == "Shelly" assert event2["domain"] == DOMAIN assert ( event2["message"] - == "'btn_down' click event for shellypro4pm-12345678 channel 2 Input was fired." + == "'btn_down' click event for shellypro4pm-12345678 channel 2 Input was fired" ) From c7e8428daa00da507752721ef45b233724958b96 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 13 May 2022 00:26:47 +0000 Subject: [PATCH 0447/3516] [ci skip] Translation update --- .../components/androidtv/translations/ja.json | 4 +-- .../translations/ja.json | 3 +++ .../components/asuswrt/translations/ja.json | 3 +++ .../derivative/translations/ru.json | 14 ++++++++--- .../components/fan/translations/it.json | 1 + .../components/fan/translations/no.json | 1 + .../components/flux_led/translations/ja.json | 2 +- .../components/generic/translations/en.json | 2 ++ .../geocaching/translations/ca.json | 25 +++++++++++++++++++ .../geocaching/translations/de.json | 25 +++++++++++++++++++ .../geocaching/translations/el.json | 25 +++++++++++++++++++ .../geocaching/translations/et.json | 25 +++++++++++++++++++ .../geocaching/translations/fr.json | 25 +++++++++++++++++++ .../geocaching/translations/hu.json | 25 +++++++++++++++++++ .../geocaching/translations/id.json | 25 +++++++++++++++++++ .../geocaching/translations/it.json | 25 +++++++++++++++++++ .../geocaching/translations/pt-BR.json | 25 +++++++++++++++++++ .../geocaching/translations/zh-Hant.json | 25 +++++++++++++++++++ .../components/gios/translations/hu.json | 2 +- .../components/ipp/translations/bg.json | 1 + .../components/isy994/translations/ja.json | 3 ++- .../components/knx/translations/ja.json | 8 +++--- .../components/kodi/translations/ja.json | 2 +- .../components/mysensors/translations/ja.json | 6 ++--- .../components/onewire/translations/bg.json | 3 ++- .../components/onewire/translations/it.json | 2 +- .../components/onewire/translations/no.json | 4 ++- .../components/ps4/translations/ja.json | 2 +- .../components/recorder/translations/ja.json | 4 ++- .../components/recorder/translations/ru.json | 1 + .../components/rfxtrx/translations/bg.json | 3 +++ .../components/sabnzbd/translations/bg.json | 1 + .../simplisafe/translations/ja.json | 5 ++++ .../components/sonarr/translations/bg.json | 1 + .../components/sql/translations/en.json | 6 +++-- .../components/sql/translations/ja.json | 17 ++++++++++--- .../components/steamist/translations/ja.json | 2 +- .../components/threshold/translations/ja.json | 4 +-- .../components/tplink/translations/ja.json | 2 +- .../components/tuya/translations/ja.json | 2 +- .../tuya/translations/select.ru.json | 10 ++++++++ .../tuya/translations/sensor.ru.json | 3 ++- .../ukraine_alarm/translations/bg.json | 3 ++- .../ukraine_alarm/translations/en.json | 13 ++++++++++ .../ukraine_alarm/translations/ja.json | 9 ++++--- .../ukraine_alarm/translations/pl.json | 12 ++++++--- .../components/unifi/translations/ja.json | 3 +++ .../utility_meter/translations/ja.json | 2 +- .../xiaomi_aqara/translations/ja.json | 2 +- .../components/yeelight/translations/ja.json | 4 +-- .../components/zwave_js/translations/ja.json | 4 +-- 51 files changed, 379 insertions(+), 47 deletions(-) create mode 100644 homeassistant/components/application_credentials/translations/ja.json create mode 100644 homeassistant/components/geocaching/translations/ca.json create mode 100644 homeassistant/components/geocaching/translations/de.json create mode 100644 homeassistant/components/geocaching/translations/el.json create mode 100644 homeassistant/components/geocaching/translations/et.json create mode 100644 homeassistant/components/geocaching/translations/fr.json create mode 100644 homeassistant/components/geocaching/translations/hu.json create mode 100644 homeassistant/components/geocaching/translations/id.json create mode 100644 homeassistant/components/geocaching/translations/it.json create mode 100644 homeassistant/components/geocaching/translations/pt-BR.json create mode 100644 homeassistant/components/geocaching/translations/zh-Hant.json diff --git a/homeassistant/components/androidtv/translations/ja.json b/homeassistant/components/androidtv/translations/ja.json index f3139df9819..365a6078366 100644 --- a/homeassistant/components/androidtv/translations/ja.json +++ b/homeassistant/components/androidtv/translations/ja.json @@ -14,9 +14,9 @@ "step": { "user": { "data": { - "adb_server_ip": "ADB\u30b5\u30fc\u30d0\u30fc\u306eIP\u30a2\u30c9\u30ec\u30b9(\u4f7f\u7528\u3057\u306a\u3044\u5834\u5408\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", + "adb_server_ip": "ADB\u30b5\u30fc\u30d0\u30fc\u306eIP\u30a2\u30c9\u30ec\u30b9(\u4f7f\u7528\u3057\u306a\u3044\u5834\u5408\u306f\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", "adb_server_port": "ADB\u30b5\u30fc\u30d0\u30fc\u306e\u30dd\u30fc\u30c8", - "adbkey": "ADB\u30ad\u30fc\u30d5\u30a1\u30a4\u30eb\u3078\u306e\u30d1\u30b9(\u7a7a\u306b\u3059\u308b\u3068\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3059)", + "adbkey": "ADB\u30ad\u30fc\u30d5\u30a1\u30a4\u30eb\u3078\u306e\u30d1\u30b9(\u7a7a\u767d\u306b\u3059\u308b\u3068\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3059)", "device_class": "\u30c7\u30d0\u30a4\u30b9\u306e\u7a2e\u985e", "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8" diff --git a/homeassistant/components/application_credentials/translations/ja.json b/homeassistant/components/application_credentials/translations/ja.json new file mode 100644 index 00000000000..ee195408093 --- /dev/null +++ b/homeassistant/components/application_credentials/translations/ja.json @@ -0,0 +1,3 @@ +{ + "title": "\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8a8d\u8a3c" +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/ja.json b/homeassistant/components/asuswrt/translations/ja.json index ab253e324ce..0ae15e8b86b 100644 --- a/homeassistant/components/asuswrt/translations/ja.json +++ b/homeassistant/components/asuswrt/translations/ja.json @@ -1,6 +1,9 @@ { "config": { "abort": { + "invalid_unique_id": "\u30c7\u30d0\u30a4\u30b9\u306e\u6709\u52b9\u306a\u30e6\u30cb\u30fc\u30afID\u3092\u6c7a\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093", + "no_unique_id": "\u6709\u52b9\u306a\u30e6\u30cb\u30fc\u30afID\u3092\u6301\u305f\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8907\u6570\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u69cb\u6210\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093", + "not_unique_id_exist": "\u6709\u52b9\u306a\u30e6\u30cb\u30fc\u30afID\u3092\u6301\u305f\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8907\u6570\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u69cb\u6210\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "error": { diff --git a/homeassistant/components/derivative/translations/ru.json b/homeassistant/components/derivative/translations/ru.json index d7c62f070e1..f3ff7cf0e83 100644 --- a/homeassistant/components/derivative/translations/ru.json +++ b/homeassistant/components/derivative/translations/ru.json @@ -11,8 +11,11 @@ "unit_time": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438" }, "data_description": { - "round": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439." + "round": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439.", + "time_window": "\u0415\u0441\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e, \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0432\u043d\u043e \u0432\u0437\u0432\u0435\u0448\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043a\u043e\u043b\u044c\u0437\u044f\u0449\u0435\u043c\u0443 \u0441\u0440\u0435\u0434\u043d\u0435\u043c\u0443 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u044b\u0445 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u044d\u0442\u043e\u0433\u043e \u043e\u043a\u043d\u0430.", + "unit_prefix": "\u0414\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0438 \u0441 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u043c \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \u043c\u0435\u0442\u0440\u0438\u043a\u0438 \u0438 \u0435\u0434\u0438\u043d\u0438\u0446\u0435\u0439 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u043e\u0439." }, + "description": "\u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u0441\u0435\u043d\u0441\u043e\u0440, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u0443\u044e \u0441\u0435\u043d\u0441\u043e\u0440\u0430.", "title": "\u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u0430\u044f" } } @@ -30,7 +33,8 @@ }, "data_description": { "round": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439.", - "unit_prefix": "." + "time_window": "\u0415\u0441\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e, \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0432\u043d\u043e \u0432\u0437\u0432\u0435\u0448\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043a\u043e\u043b\u044c\u0437\u044f\u0449\u0435\u043c\u0443 \u0441\u0440\u0435\u0434\u043d\u0435\u043c\u0443 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u044b\u0445 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u044d\u0442\u043e\u0433\u043e \u043e\u043a\u043d\u0430.", + "unit_prefix": "\u0414\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0438 \u0441 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u043c \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \u043c\u0435\u0442\u0440\u0438\u043a\u0438 \u0438 \u0435\u0434\u0438\u043d\u0438\u0446\u0435\u0439 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u043e\u0439.." } }, "options": { @@ -44,8 +48,10 @@ }, "data_description": { "round": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439.", - "unit_prefix": "." - } + "time_window": "\u0415\u0441\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e, \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0432\u043d\u043e \u0432\u0437\u0432\u0435\u0448\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043a\u043e\u043b\u044c\u0437\u044f\u0449\u0435\u043c\u0443 \u0441\u0440\u0435\u0434\u043d\u0435\u043c\u0443 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u044b\u0445 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u044d\u0442\u043e\u0433\u043e \u043e\u043a\u043d\u0430.", + "unit_prefix": "\u0414\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0438 \u0441 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u043c \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \u043c\u0435\u0442\u0440\u0438\u043a\u0438 \u0438 \u0435\u0434\u0438\u043d\u0438\u0446\u0435\u0439 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u043e\u0439.." + }, + "description": "\u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u0441\u0435\u043d\u0441\u043e\u0440, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u0443\u044e \u0441\u0435\u043d\u0441\u043e\u0440\u0430." } } }, diff --git a/homeassistant/components/fan/translations/it.json b/homeassistant/components/fan/translations/it.json index 563b24b7c44..c74f477ea73 100644 --- a/homeassistant/components/fan/translations/it.json +++ b/homeassistant/components/fan/translations/it.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "Attiva/disattiva {entity_name}", "turn_off": "Spegnere {entity_name}", "turn_on": "Accendere {entity_name}" }, diff --git a/homeassistant/components/fan/translations/no.json b/homeassistant/components/fan/translations/no.json index b576e2aca7d..d9fdf8c5ea5 100644 --- a/homeassistant/components/fan/translations/no.json +++ b/homeassistant/components/fan/translations/no.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "Veksle {entity_name}", "turn_off": "Sl\u00e5 av {entity_name}", "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" }, diff --git a/homeassistant/components/flux_led/translations/ja.json b/homeassistant/components/flux_led/translations/ja.json index 3b6a34d7e5b..248df586ee9 100644 --- a/homeassistant/components/flux_led/translations/ja.json +++ b/homeassistant/components/flux_led/translations/ja.json @@ -17,7 +17,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "\u30db\u30b9\u30c8\u3092\u7a7a\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc\u3092\u4f7f\u3063\u3066\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u7d22\u3057\u307e\u3059\u3002" + "description": "\u30db\u30b9\u30c8\u3092\u7a7a\u767d\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc\u3092\u4f7f\u3063\u3066\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u7d22\u3057\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json index b552c780d29..334f00fab6f 100644 --- a/homeassistant/components/generic/translations/en.json +++ b/homeassistant/components/generic/translations/en.json @@ -32,6 +32,7 @@ "user": { "data": { "authentication": "Authentication", + "content_type": "Content Type", "framerate": "Frame Rate (Hz)", "limit_refetch_to_url_change": "Limit refetch to url change", "password": "Password", @@ -71,6 +72,7 @@ "init": { "data": { "authentication": "Authentication", + "content_type": "Content Type", "framerate": "Frame Rate (Hz)", "limit_refetch_to_url_change": "Limit refetch to url change", "password": "Password", diff --git a/homeassistant/components/geocaching/translations/ca.json b/homeassistant/components/geocaching/translations/ca.json new file mode 100644 index 00000000000..09e241d8f2a --- /dev/null +++ b/homeassistant/components/geocaching/translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "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.", + "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", + "oauth_error": "S'han rebut dades token inv\u00e0lides.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa" + }, + "step": { + "pick_implementation": { + "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + }, + "reauth_confirm": { + "description": "La integraci\u00f3 Geocaching ha de tornar a autenticar-se amb el teu compte", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geocaching/translations/de.json b/homeassistant/components/geocaching/translations/de.json new file mode 100644 index 00000000000..1712153fbce --- /dev/null +++ b/homeassistant/components/geocaching/translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", + "oauth_error": "Ung\u00fcltige Token-Daten empfangen.", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "create_entry": { + "default": "Erfolgreich authentifiziert" + }, + "step": { + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + }, + "reauth_confirm": { + "description": "Die Geocaching-Integration muss dein Konto erneut authentifizieren", + "title": "Integration erneut authentifizieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geocaching/translations/el.json b/homeassistant/components/geocaching/translations/el.json new file mode 100644 index 00000000000..e07b121ceee --- /dev/null +++ b/homeassistant/components/geocaching/translations/el.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2.", + "missing_configuration": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", + "no_url_available": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL. \u0393\u03b9\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1, [\u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b2\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1\u03c2] ( {docs_url} )", + "oauth_error": "\u039b\u03ae\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd.", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + }, + "create_entry": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "step": { + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "reauth_confirm": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Geocaching \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geocaching/translations/et.json b/homeassistant/components/geocaching/translations/et.json new file mode 100644 index 00000000000..075e41d1f24 --- /dev/null +++ b/homeassistant/components/geocaching/translations/et.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp", + "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.", + "no_url_available": "URL pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [spikrijaotis]({docs_url})", + "oauth_error": "Saadi sobimatud loaandmed.", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "create_entry": { + "default": "Tuvastamine \u00f5nnestus" + }, + "step": { + "pick_implementation": { + "title": "Vali tuvastusmeetod" + }, + "reauth_confirm": { + "description": "Geocachingu sidumine peab konto taastuvastama", + "title": "Taastuvasta sidumine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geocaching/translations/fr.json b/homeassistant/components/geocaching/translations/fr.json new file mode 100644 index 00000000000..33bd549c762 --- /dev/null +++ b/homeassistant/components/geocaching/translations/fr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", + "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", + "oauth_error": "Des donn\u00e9es de jeton non valides ont \u00e9t\u00e9 re\u00e7ues.", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "create_entry": { + "default": "Authentification r\u00e9ussie" + }, + "step": { + "pick_implementation": { + "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + }, + "reauth_confirm": { + "description": "L'int\u00e9gration Geocaching doit r\u00e9-authentifier votre compte", + "title": "R\u00e9-authentifier l'int\u00e9gration" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geocaching/translations/hu.json b/homeassistant/components/geocaching/translations/hu.json new file mode 100644 index 00000000000..b7bbe55e397 --- /dev/null +++ b/homeassistant/components/geocaching/translations/hu.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "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\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", + "oauth_error": "\u00c9rv\u00e9nytelen token adatok \u00e9rkeztek.", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + }, + "create_entry": { + "default": "Sikeres hiteles\u00edt\u00e9s" + }, + "step": { + "pick_implementation": { + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" + }, + "reauth_confirm": { + "description": "A Geocaching integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie a fi\u00f3kj\u00e1t.", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geocaching/translations/id.json b/homeassistant/components/geocaching/translations/id.json new file mode 100644 index 00000000000..c52366419d5 --- /dev/null +++ b/homeassistant/components/geocaching/translations/id.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", + "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})", + "oauth_error": "Menerima respons token yang tidak valid.", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "create_entry": { + "default": "Berhasil diautentikasi" + }, + "step": { + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + }, + "reauth_confirm": { + "description": "Integrasi Geocaching perlu mengautentikasi ulang akun Anda", + "title": "Autentikasi Ulang Integrasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geocaching/translations/it.json b/homeassistant/components/geocaching/translations/it.json new file mode 100644 index 00000000000..2758f190756 --- /dev/null +++ b/homeassistant/components/geocaching/translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", + "oauth_error": "Ricevuti dati token non validi.", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "create_entry": { + "default": "Autenticazione riuscita" + }, + "step": { + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + }, + "reauth_confirm": { + "description": "L'integrazione Geocaching deve autenticare nuovamente il tuo account", + "title": "Autentica nuovamente l'integrazione" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geocaching/translations/pt-BR.json b/homeassistant/components/geocaching/translations/pt-BR.json new file mode 100644 index 00000000000..4a6c20919d0 --- /dev/null +++ b/homeassistant/components/geocaching/translations/pt-BR.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 est\u00e1 configurada", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_url_available": "Nenhuma URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre este erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})", + "oauth_error": "Dados de token inv\u00e1lidos recebidos.", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "description": "A integra\u00e7\u00e3o Geocaching precisa reautenticar sua conta", + "title": "Reautenticar Integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geocaching/translations/zh-Hant.json b/homeassistant/components/geocaching/translations/zh-Hant.json new file mode 100644 index 00000000000..80c501be734 --- /dev/null +++ b/homeassistant/components/geocaching/translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", + "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", + "oauth_error": "\u6536\u5230\u7121\u6548\u7684\u6b0a\u6756\u8cc7\u6599\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" + }, + "step": { + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, + "reauth_confirm": { + "description": "Geocaching \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/hu.json b/homeassistant/components/gios/translations/hu.json index 1de37cee96a..9ad0e244eaf 100644 --- a/homeassistant/components/gios/translations/hu.json +++ b/homeassistant/components/gios/translations/hu.json @@ -14,7 +14,7 @@ "name": "Elnevez\u00e9s", "station_id": "A m\u00e9r\u0151\u00e1llom\u00e1s azonos\u00edt\u00f3ja" }, - "description": "A GIO\u015a (lengyel k\u00f6rnyezetv\u00e9delmi f\u0151fel\u00fcgyel\u0151) leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Ha seg\u00edts\u00e9gre van sz\u00fcks\u00e9ged a konfigur\u00e1ci\u00f3val kapcsolatban, l\u00e1togass ide: https://www.home-assistant.io/integrations/gios", + "description": "A GIO\u015a (lengyel k\u00f6rnyezetv\u00e9delmi f\u0151fel\u00fcgyel\u0151) leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Ha seg\u00edts\u00e9gre van sz\u00fcks\u00e9ge a konfigur\u00e1ci\u00f3val kapcsolatban, l\u00e1togasson ide: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Lengyel K\u00f6rnyezetv\u00e9delmi F\u0151fel\u00fcgyel\u0151s\u00e9g)" } } diff --git a/homeassistant/components/ipp/translations/bg.json b/homeassistant/components/ipp/translations/bg.json index 4983c9a14b2..d454fe68170 100644 --- a/homeassistant/components/ipp/translations/bg.json +++ b/homeassistant/components/ipp/translations/bg.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "base_path": "\u041e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u0435\u043d \u043f\u044a\u0442 \u0434\u043e \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/isy994/translations/ja.json b/homeassistant/components/isy994/translations/ja.json index 51c56f035d7..0245baf01a0 100644 --- a/homeassistant/components/isy994/translations/ja.json +++ b/homeassistant/components/isy994/translations/ja.json @@ -16,7 +16,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } + }, + "title": "ISY\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/knx/translations/ja.json b/homeassistant/components/knx/translations/ja.json index 52eed8780b8..475958af9a0 100644 --- a/homeassistant/components/knx/translations/ja.json +++ b/homeassistant/components/knx/translations/ja.json @@ -16,7 +16,7 @@ "data": { "host": "\u30db\u30b9\u30c8", "individual_address": "\u63a5\u7d9a\u7528\u306e\u500b\u5225\u30a2\u30c9\u30ec\u30b9", - "local_ip": "\u30ed\u30fc\u30ab\u30ebIP(\u4e0d\u660e\u306a\u5834\u5408\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", + "local_ip": "\u30ed\u30fc\u30ab\u30ebIP(\u4e0d\u660e\u306a\u5834\u5408\u306f\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", "port": "\u30dd\u30fc\u30c8", "route_back": "\u30eb\u30fc\u30c8\u30d0\u30c3\u30af / NAT\u30e2\u30fc\u30c9", "tunneling_type": "KNX\u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u30bf\u30a4\u30d7" @@ -31,7 +31,7 @@ "routing": { "data": { "individual_address": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u63a5\u7d9a\u306e\u500b\u5225\u306e\u30a2\u30c9\u30ec\u30b9", - "local_ip": "\u30ed\u30fc\u30ab\u30ebIP(\u4e0d\u660e\u306a\u5834\u5408\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", + "local_ip": "\u30ed\u30fc\u30ab\u30ebIP(\u4e0d\u660e\u306a\u5834\u5408\u306f\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", "multicast_group": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u306b\u4f7f\u7528\u3059\u308b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8\u30b0\u30eb\u30fc\u30d7", "multicast_port": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u306b\u4f7f\u7528\u3059\u308b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8\u30dd\u30fc\u30c8" }, @@ -92,7 +92,7 @@ "data": { "connection_type": "KNX\u63a5\u7d9a\u30bf\u30a4\u30d7", "individual_address": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u500b\u5225\u30a2\u30c9\u30ec\u30b9", - "local_ip": "\u30ed\u30fc\u30ab\u30ebIP(\u4e0d\u660e\u306a\u5834\u5408\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", + "local_ip": "\u30ed\u30fc\u30ab\u30ebIP(\u4e0d\u660e\u306a\u5834\u5408\u306f\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", "multicast_group": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3068\u691c\u51fa(discovery)\u306b\u4f7f\u7528\u3055\u308c\u308b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8\u30b0\u30eb\u30fc\u30d7", "multicast_port": "\u30eb\u30fc\u30c6\u30a3\u30f3\u30b0\u3068\u691c\u51fa(discovery)\u306b\u4f7f\u7528\u3055\u308c\u308b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8\u30dd\u30fc\u30c8", "rate_limit": "1 \u79d2\u3042\u305f\u308a\u306e\u6700\u5927\u9001\u4fe1\u96fb\u5831(telegrams )\u6570", @@ -110,7 +110,7 @@ "tunnel": { "data": { "host": "\u30db\u30b9\u30c8", - "local_ip": "\u30ed\u30fc\u30ab\u30ebIP(\u4e0d\u660e\u306a\u5834\u5408\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", + "local_ip": "\u30ed\u30fc\u30ab\u30ebIP(\u4e0d\u660e\u306a\u5834\u5408\u306f\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", "port": "\u30dd\u30fc\u30c8", "route_back": "\u30eb\u30fc\u30c8\u30d0\u30c3\u30af / NAT\u30e2\u30fc\u30c9", "tunneling_type": "KNX\u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u30bf\u30a4\u30d7" diff --git a/homeassistant/components/kodi/translations/ja.json b/homeassistant/components/kodi/translations/ja.json index c7cf892bb2b..88855ff4d77 100644 --- a/homeassistant/components/kodi/translations/ja.json +++ b/homeassistant/components/kodi/translations/ja.json @@ -37,7 +37,7 @@ "data": { "ws_port": "\u30dd\u30fc\u30c8" }, - "description": "WebSocket\u30dd\u30fc\u30c8(Kodi\u3067\u306fTCP\u30dd\u30fc\u30c8\u3068\u547c\u3070\u308c\u308b\u3053\u3068\u3082\u3042\u308a\u307e\u3059)\u3002WebSocket\u3092\u4ecb\u3057\u3066\u63a5\u7d9a\u3059\u308b\u306b\u306f\u3001\u30b7\u30b9\u30c6\u30e0/\u8a2d\u5b9a/\u30cd\u30c3\u30c8\u30ef\u30fc\u30af/\u30b5\u30fc\u30d3\u30b9\u306b\u3042\u308b \"\u30d7\u30ed\u30b0\u30e9\u30e0\u306bKodi\u306e\u5236\u5fa1\u3092\u8a31\u53ef\u3059\u308b\" \u3092\u6709\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002WebSocket\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u30dd\u30fc\u30c8\u3092\u524a\u9664\u3057\u3066\u7a7a\u306e\u307e\u307e\u306b\u3057\u307e\u3059\u3002" + "description": "WebSocket\u30dd\u30fc\u30c8(Kodi\u3067\u306fTCP\u30dd\u30fc\u30c8\u3068\u547c\u3070\u308c\u308b\u3053\u3068\u3082\u3042\u308a\u307e\u3059)\u3002WebSocket\u3092\u4ecb\u3057\u3066\u63a5\u7d9a\u3059\u308b\u306b\u306f\u3001\u30b7\u30b9\u30c6\u30e0/\u8a2d\u5b9a/\u30cd\u30c3\u30c8\u30ef\u30fc\u30af/\u30b5\u30fc\u30d3\u30b9\u306b\u3042\u308b \"\u30d7\u30ed\u30b0\u30e9\u30e0\u306bKodi\u306e\u5236\u5fa1\u3092\u8a31\u53ef\u3059\u308b\" \u3092\u6709\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002WebSocket\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u30dd\u30fc\u30c8\u3092\u524a\u9664\u3057\u3066\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/mysensors/translations/ja.json b/homeassistant/components/mysensors/translations/ja.json index 9842d241ad1..b178d136009 100644 --- a/homeassistant/components/mysensors/translations/ja.json +++ b/homeassistant/components/mysensors/translations/ja.json @@ -42,7 +42,7 @@ "step": { "gw_mqtt": { "data": { - "persistence_file": "\u6c38\u7d9a(persistence)\u30d5\u30a1\u30a4\u30eb(\u7a7a\u306b\u3059\u308b\u3068\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3059)", + "persistence_file": "\u6c38\u7d9a(persistence)\u30d5\u30a1\u30a4\u30eb(\u7a7a\u767d\u306b\u3059\u308b\u3068\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3059)", "retain": "mqtt retain(\u4fdd\u6301)", "topic_in_prefix": "\u30a4\u30f3\u30d7\u30c3\u30c8 \u30c8\u30d4\u30c3\u30af\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9(topic_in_prefix)", "topic_out_prefix": "\u30a2\u30a6\u30c8\u30d7\u30c3\u30c8 \u30c8\u30d4\u30c3\u30af\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9(topic_out_prefix)", @@ -54,7 +54,7 @@ "data": { "baud_rate": "\u30dc\u30fc\u30ec\u30fc\u30c8", "device": "\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8", - "persistence_file": "\u6c38\u7d9a(persistence)\u30d5\u30a1\u30a4\u30eb(\u7a7a\u306b\u3059\u308b\u3068\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3059)", + "persistence_file": "\u6c38\u7d9a(persistence)\u30d5\u30a1\u30a4\u30eb(\u7a7a\u767d\u306b\u3059\u308b\u3068\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3059)", "version": "MySensors\u306e\u30d0\u30fc\u30b8\u30e7\u30f3" }, "description": "\u30b7\u30ea\u30a2\u30eb\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" @@ -62,7 +62,7 @@ "gw_tcp": { "data": { "device": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306eIP\u30a2\u30c9\u30ec\u30b9", - "persistence_file": "\u6c38\u7d9a(persistence)\u30d5\u30a1\u30a4\u30eb(\u7a7a\u306b\u3059\u308b\u3068\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3059)", + "persistence_file": "\u6c38\u7d9a(persistence)\u30d5\u30a1\u30a4\u30eb(\u7a7a\u767d\u306b\u3059\u308b\u3068\u81ea\u52d5\u751f\u6210\u3055\u308c\u307e\u3059)", "tcp_port": "\u30dd\u30fc\u30c8", "version": "MySensors\u306e\u30d0\u30fc\u30b8\u30e7\u30f3" }, diff --git a/homeassistant/components/onewire/translations/bg.json b/homeassistant/components/onewire/translations/bg.json index 26dd6f15686..a14f7fa00a2 100644 --- a/homeassistant/components/onewire/translations/bg.json +++ b/homeassistant/components/onewire/translations/bg.json @@ -4,7 +4,8 @@ "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_path": "\u0414\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u044f\u0442\u0430 \u043d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0430." }, "step": { "owserver": { diff --git a/homeassistant/components/onewire/translations/it.json b/homeassistant/components/onewire/translations/it.json index 77f64381ee7..a6bffe1e5fc 100644 --- a/homeassistant/components/onewire/translations/it.json +++ b/homeassistant/components/onewire/translations/it.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Impossibile connettersi", - "invalid_path": "Directory non trovata." + "invalid_path": "Cartella non trovata." }, "step": { "owserver": { diff --git a/homeassistant/components/onewire/translations/no.json b/homeassistant/components/onewire/translations/no.json index 62bd0ac4634..89529aa7810 100644 --- a/homeassistant/components/onewire/translations/no.json +++ b/homeassistant/components/onewire/translations/no.json @@ -17,9 +17,11 @@ }, "user": { "data": { + "host": "Vert", + "port": "Port", "type": "Tilkoblingstype" }, - "title": "Sett opp 1-Wire" + "title": "Angi serverdetaljer" } } }, diff --git a/homeassistant/components/ps4/translations/ja.json b/homeassistant/components/ps4/translations/ja.json index 92be62f7229..40ac55264a9 100644 --- a/homeassistant/components/ps4/translations/ja.json +++ b/homeassistant/components/ps4/translations/ja.json @@ -33,7 +33,7 @@ }, "mode": { "data": { - "ip_address": "IP\u30a2\u30c9\u30ec\u30b9(\u81ea\u52d5\u691c\u51fa\u3092\u4f7f\u7528\u3059\u308b\u5834\u5408\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u307e\u3059)", + "ip_address": "IP\u30a2\u30c9\u30ec\u30b9(\u81ea\u52d5\u691c\u51fa\u3092\u4f7f\u7528\u3059\u308b\u5834\u5408\u306f\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u307e\u3059)", "mode": "\u30b3\u30f3\u30d5\u30a3\u30b0\u30e2\u30fc\u30c9" }, "data_description": { diff --git a/homeassistant/components/recorder/translations/ja.json b/homeassistant/components/recorder/translations/ja.json index 2aadb11ac33..8b54e4f544f 100644 --- a/homeassistant/components/recorder/translations/ja.json +++ b/homeassistant/components/recorder/translations/ja.json @@ -1,7 +1,9 @@ { "system_health": { "info": { - "estimated_db_size": "\u63a8\u5b9a\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u30b5\u30a4\u30ba(MiB)" + "current_recorder_run": "\u73fe\u5728\u306e\u5b9f\u884c\u958b\u59cb\u6642\u9593", + "estimated_db_size": "\u63a8\u5b9a\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u30b5\u30a4\u30ba(MiB)", + "oldest_recorder_run": "\u6700\u3082\u53e4\u3044\u5b9f\u884c\u958b\u59cb\u6642\u9593" } } } \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/ru.json b/homeassistant/components/recorder/translations/ru.json index bdac6c52a3b..a637d37f720 100644 --- a/homeassistant/components/recorder/translations/ru.json +++ b/homeassistant/components/recorder/translations/ru.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "current_recorder_run": "\u0412\u0440\u0435\u043c\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u0446\u0438\u043a\u043b\u0430", "estimated_db_size": "\u041f\u0440\u0435\u0434\u043f\u043e\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u0440\u0430\u0437\u043c\u0435\u0440 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445 (MiB)", "oldest_recorder_run": "\u0412\u0440\u0435\u043c\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u0441\u0430\u043c\u043e\u0433\u043e \u0441\u0442\u0430\u0440\u043e\u0433\u043e \u0446\u0438\u043a\u043b\u0430" } diff --git a/homeassistant/components/rfxtrx/translations/bg.json b/homeassistant/components/rfxtrx/translations/bg.json index 9a7983491d6..c03ec99553e 100644 --- a/homeassistant/components/rfxtrx/translations/bg.json +++ b/homeassistant/components/rfxtrx/translations/bg.json @@ -19,6 +19,9 @@ "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" + }, + "setup_serial_manual_path": { + "title": "\u041f\u044a\u0442" } } }, diff --git a/homeassistant/components/sabnzbd/translations/bg.json b/homeassistant/components/sabnzbd/translations/bg.json index d3fcfb0789d..a9d07e52b39 100644 --- a/homeassistant/components/sabnzbd/translations/bg.json +++ b/homeassistant/components/sabnzbd/translations/bg.json @@ -9,6 +9,7 @@ "data": { "api_key": "API \u043a\u043b\u044e\u0447", "name": "\u0418\u043c\u0435", + "path": "\u041f\u044a\u0442", "url": "URL" } } diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index 74b0b20c65e..2d3f53b0710 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -22,6 +22,11 @@ "description": "\u30a2\u30af\u30bb\u30b9\u306e\u6709\u52b9\u671f\u9650\u304c\u5207\u308c\u3066\u3044\u308b\u304b\u3001\u53d6\u308a\u6d88\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u30ea\u30f3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, + "sms_2fa": { + "data": { + "code": "\u30b3\u30fc\u30c9" + } + }, "user": { "data": { "auth_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9", diff --git a/homeassistant/components/sonarr/translations/bg.json b/homeassistant/components/sonarr/translations/bg.json index c6ab1b205e9..d866b1d1813 100644 --- a/homeassistant/components/sonarr/translations/bg.json +++ b/homeassistant/components/sonarr/translations/bg.json @@ -17,6 +17,7 @@ "user": { "data": { "api_key": "API \u043a\u043b\u044e\u0447", + "base_path": "\u041f\u044a\u0442 \u0434\u043e API", "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442", "url": "URL" diff --git a/homeassistant/components/sql/translations/en.json b/homeassistant/components/sql/translations/en.json index 9d05f771853..4b72d024df4 100644 --- a/homeassistant/components/sql/translations/en.json +++ b/homeassistant/components/sql/translations/en.json @@ -5,7 +5,8 @@ }, "error": { "db_url_invalid": "Database URL invalid", - "query_invalid": "SQL Query invalid" + "query_invalid": "SQL Query invalid", + "value_template_invalid": "Value Template invalid" }, "step": { "user": { @@ -31,7 +32,8 @@ "options": { "error": { "db_url_invalid": "Database URL invalid", - "query_invalid": "SQL Query invalid" + "query_invalid": "SQL Query invalid", + "value_template_invalid": "Value Template invalid" }, "step": { "init": { diff --git a/homeassistant/components/sql/translations/ja.json b/homeassistant/components/sql/translations/ja.json index 6fa037efc5f..3b9fdfc8eaa 100644 --- a/homeassistant/components/sql/translations/ja.json +++ b/homeassistant/components/sql/translations/ja.json @@ -20,7 +20,11 @@ }, "data_description": { "column": "\u8fd4\u3055\u308c\u305f\u30af\u30a8\u30ea\u3092\u72b6\u614b\u3068\u3057\u3066\u8868\u793a\u3059\u308b\u305f\u3081\u306e\u30ab\u30e9\u30e0", - "name": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u3068\u30bb\u30f3\u30b5\u30fc\u306b\u4f7f\u7528\u3055\u308c\u308b\u540d\u524d" + "db_url": "\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9URL\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u306eHA\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u3092\u4f7f\u7528\u3059\u308b\u306b\u306f\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u307e\u3059", + "name": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u3068\u30bb\u30f3\u30b5\u30fc\u306b\u4f7f\u7528\u3055\u308c\u308b\u540d\u524d", + "query": "\u5b9f\u884c\u3059\u308b\u30af\u30a8\u30ea\u306f\u3001 'SELECT' \u3067\u59cb\u3081\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "unit_of_measurement": "\u6e2c\u5b9a\u5358\u4f4d(\u30aa\u30d7\u30b7\u30e7\u30f3)", + "value_template": "\u5024\u306e\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8(\u30aa\u30d7\u30b7\u30e7\u30f3)" } } } @@ -37,10 +41,17 @@ "column": "\u30b3\u30e9\u30e0", "db_url": "\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306eURL", "name": "\u540d\u524d", - "query": "\u30af\u30a8\u30ea\u3092\u9078\u629e" + "query": "\u30af\u30a8\u30ea\u3092\u9078\u629e", + "unit_of_measurement": "\u6e2c\u5b9a\u5358\u4f4d", + "value_template": "\u5024\u306e\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8" }, "data_description": { - "name": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u3068\u30bb\u30f3\u30b5\u30fc\u306b\u4f7f\u7528\u3055\u308c\u308b\u540d\u524d" + "column": "\u8fd4\u3055\u308c\u305f\u30af\u30a8\u30ea\u3092\u72b6\u614b\u3068\u3057\u3066\u8868\u793a\u3059\u308b\u305f\u3081\u306e\u30ab\u30e9\u30e0", + "db_url": "\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9URL\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u306eHA\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u3092\u4f7f\u7528\u3059\u308b\u306b\u306f\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u307e\u3059", + "name": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u3068\u30bb\u30f3\u30b5\u30fc\u306b\u4f7f\u7528\u3055\u308c\u308b\u540d\u524d", + "query": "\u5b9f\u884c\u3059\u308b\u30af\u30a8\u30ea\u306f\u3001 'SELECT' \u3067\u59cb\u3081\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "unit_of_measurement": "\u6e2c\u5b9a\u5358\u4f4d(\u30aa\u30d7\u30b7\u30e7\u30f3)", + "value_template": "\u5024\u306e\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8(\u30aa\u30d7\u30b7\u30e7\u30f3)" } } } diff --git a/homeassistant/components/steamist/translations/ja.json b/homeassistant/components/steamist/translations/ja.json index 36658f87577..b37135e9c44 100644 --- a/homeassistant/components/steamist/translations/ja.json +++ b/homeassistant/components/steamist/translations/ja.json @@ -25,7 +25,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "\u30db\u30b9\u30c8\u3092\u7a7a\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc(discovery)\u3092\u4f7f\u3063\u3066\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u7d22\u3057\u307e\u3059\u3002" + "description": "\u30db\u30b9\u30c8\u3092\u7a7a\u767d\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc(discovery)\u3092\u4f7f\u3063\u3066\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u7d22\u3057\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/threshold/translations/ja.json b/homeassistant/components/threshold/translations/ja.json index 7d5a6cc2ce7..75330ac830c 100644 --- a/homeassistant/components/threshold/translations/ja.json +++ b/homeassistant/components/threshold/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "error": { - "need_lower_upper": "\u4e0b\u9650\u3068\u4e0a\u9650\u306e\u4e21\u65b9\u3092\u7a7a\u306b\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093" + "need_lower_upper": "\u4e0b\u9650\u3068\u4e0a\u9650\u306e\u4e21\u65b9\u3092\u7a7a\u767d\u306b\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093" }, "step": { "user": { @@ -20,7 +20,7 @@ }, "options": { "error": { - "need_lower_upper": "\u4e0b\u9650\u3068\u4e0a\u9650\u306e\u4e21\u65b9\u3092\u7a7a\u306b\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093" + "need_lower_upper": "\u4e0b\u9650\u3068\u4e0a\u9650\u306e\u4e21\u65b9\u3092\u7a7a\u767d\u306b\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093" }, "step": { "init": { diff --git a/homeassistant/components/tplink/translations/ja.json b/homeassistant/components/tplink/translations/ja.json index f78e4adec9c..45e9854b431 100644 --- a/homeassistant/components/tplink/translations/ja.json +++ b/homeassistant/components/tplink/translations/ja.json @@ -25,7 +25,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "\u30db\u30b9\u30c8\u3092\u7a7a\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc(discovery)\u3092\u4f7f\u3063\u3066\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u7d22\u3057\u307e\u3059\u3002" + "description": "\u30db\u30b9\u30c8\u3092\u7a7a\u767d\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc(discovery)\u3092\u4f7f\u3063\u3066\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u7d22\u3057\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/tuya/translations/ja.json b/homeassistant/components/tuya/translations/ja.json index 6733e70b472..1b5d29584cc 100644 --- a/homeassistant/components/tuya/translations/ja.json +++ b/homeassistant/components/tuya/translations/ja.json @@ -71,7 +71,7 @@ "init": { "data": { "discovery_interval": "\u30c7\u30d0\u30a4\u30b9\u691c\u51fa\u306e\u30dd\u30fc\u30ea\u30f3\u30b0\u9593\u9694(\u79d2\u5358\u4f4d)", - "list_devices": "\u8a2d\u5b9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3059\u308b\u304b\u3001\u7a7a\u6b04\u306e\u307e\u307e\u306b\u3057\u3066\u8a2d\u5b9a\u3092\u4fdd\u5b58\u3057\u307e\u3059", + "list_devices": "\u8a2d\u5b9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3059\u308b\u304b\u3001\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u3066\u8a2d\u5b9a\u3092\u4fdd\u5b58\u3057\u307e\u3059", "query_device": "\u30b9\u30c6\u30fc\u30bf\u30b9\u306e\u66f4\u65b0\u3092\u9ad8\u901f\u5316\u3059\u308b\u305f\u3081\u306b\u30af\u30a8\u30ea\u306e\u65b9\u6cd5(query method)\u3092\u4f7f\u7528\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u307e\u3059", "query_interval": "\u30af\u30a8\u30ea\u30c7\u30d0\u30a4\u30b9\u306e\u30dd\u30fc\u30ea\u30f3\u30b0\u9593\u9694(\u79d2)" }, diff --git a/homeassistant/components/tuya/translations/select.ru.json b/homeassistant/components/tuya/translations/select.ru.json index 99f7f02771d..c2138e2490b 100644 --- a/homeassistant/components/tuya/translations/select.ru.json +++ b/homeassistant/components/tuya/translations/select.ru.json @@ -52,8 +52,17 @@ "level_8": "\u0423\u0440\u043e\u0432\u0435\u043d\u044c 8", "level_9": "\u0423\u0440\u043e\u0432\u0435\u043d\u044c 9" }, + "tuya__humidifier_moodlighting": { + "1": "\u041d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u0435 1", + "2": "\u041d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u0435 2", + "3": "\u041d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u0435 3", + "4": "\u041d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u0435 4", + "5": "\u041d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u0435 5" + }, "tuya__humidifier_spray_mode": { "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438", + "health": "\u0421\u0442\u0430\u0442\u0443\u0441", + "humidity": "\u0412\u043b\u0430\u0436\u043d\u043e\u0441\u0442\u044c", "sleep": "\u0421\u043e\u043d", "work": "\u0420\u0430\u0431\u043e\u0442\u0430" }, @@ -108,6 +117,7 @@ "part": "\u0427\u0430\u0441\u0442\u044c", "pick_zone": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0437\u043e\u043d\u0443", "point": "\u0422\u043e\u0447\u043a\u0430", + "pose": "\u041f\u043e\u0437\u0438\u0446\u0438\u044f", "random": "\u0421\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u0439", "right_bow": "\u041e\u0433\u0438\u0431\u0430\u0442\u044c \u0441\u043f\u0440\u0430\u0432\u0430", "right_spiral": "\u0421\u043f\u0438\u0440\u0430\u043b\u044c \u0432\u043f\u0440\u0430\u0432\u043e", diff --git a/homeassistant/components/tuya/translations/sensor.ru.json b/homeassistant/components/tuya/translations/sensor.ru.json index 9ec7eac14c0..8b356e13db0 100644 --- a/homeassistant/components/tuya/translations/sensor.ru.json +++ b/homeassistant/components/tuya/translations/sensor.ru.json @@ -3,7 +3,8 @@ "tuya__air_quality": { "good": "\u0425\u043e\u0440\u043e\u0448\u0435\u0435", "great": "\u041e\u0442\u043b\u0438\u0447\u043d\u043e\u0435", - "mild": "\u0423\u043c\u0435\u0440\u0435\u043d\u043d\u043e\u0435" + "mild": "\u0423\u043c\u0435\u0440\u0435\u043d\u043d\u043e\u0435", + "severe": "\u0421\u0435\u0440\u044c\u0435\u0437\u043d\u044b\u0439" }, "tuya__status": { "boiling_temp": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043a\u0438\u043f\u0435\u043d\u0438\u044f", diff --git a/homeassistant/components/ukraine_alarm/translations/bg.json b/homeassistant/components/ukraine_alarm/translations/bg.json index fbedf14b7cd..68cd63e86b1 100644 --- a/homeassistant/components/ukraine_alarm/translations/bg.json +++ b/homeassistant/components/ukraine_alarm/translations/bg.json @@ -27,7 +27,8 @@ "user": { "data": { "api_key": "API \u043a\u043b\u044e\u0447" - } + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 Ukraine Alarm. \u0417\u0430 \u0434\u0430 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u0442\u0435 API \u043a\u043b\u044e\u0447, \u043e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 {api_url}" } } } diff --git a/homeassistant/components/ukraine_alarm/translations/en.json b/homeassistant/components/ukraine_alarm/translations/en.json index 857311ea3e7..6397f652aee 100644 --- a/homeassistant/components/ukraine_alarm/translations/en.json +++ b/homeassistant/components/ukraine_alarm/translations/en.json @@ -8,6 +8,12 @@ "timeout": "Timeout establishing connection", "unknown": "Unexpected error" }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_api_key": "Invalid API key", + "timeout": "Timeout establishing connection", + "unknown": "Unexpected error" + }, "step": { "community": { "data": { @@ -21,8 +27,15 @@ }, "description": "If you want to monitor not only state, choose its specific district" }, + "state": { + "data": { + "region": "Region" + }, + "description": "Choose state to monitor" + }, "user": { "data": { + "api_key": "API Key", "region": "Region" }, "description": "Choose state to monitor" diff --git a/homeassistant/components/ukraine_alarm/translations/ja.json b/homeassistant/components/ukraine_alarm/translations/ja.json index 602d63a1de7..303ac7df051 100644 --- a/homeassistant/components/ukraine_alarm/translations/ja.json +++ b/homeassistant/components/ukraine_alarm/translations/ja.json @@ -13,12 +13,14 @@ "community": { "data": { "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" - } + }, + "description": "\u5dde\u3084\u5730\u533a\u3060\u3051\u3067\u306a\u304f\u3001\u7279\u5b9a\u306e\u5730\u57df\u3092\u76e3\u8996\u3057\u305f\u3044\u5834\u5408\u306f\u3001\u305d\u306e\u5730\u57df\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" }, "district": { "data": { "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" - } + }, + "description": "\u5dde\u3060\u3051\u3067\u306a\u304f\u3001\u7279\u5b9a\u306e\u5730\u533a\u3092\u76e3\u8996\u3057\u305f\u3044\u5834\u5408\u306f\u3001\u305d\u306e\u5730\u533a\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" }, "state": { "data": { @@ -29,7 +31,8 @@ "user": { "data": { "api_key": "API\u30ad\u30fc" - } + }, + "description": "\u30a6\u30af\u30e9\u30a4\u30ca\u306e\u8b66\u5831\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001 {api_url} \u306b\u79fb\u52d5\u3057\u3066\u304f\u3060\u3055\u3044" } } } diff --git a/homeassistant/components/ukraine_alarm/translations/pl.json b/homeassistant/components/ukraine_alarm/translations/pl.json index 1cccde50083..9ed67385f75 100644 --- a/homeassistant/components/ukraine_alarm/translations/pl.json +++ b/homeassistant/components/ukraine_alarm/translations/pl.json @@ -13,22 +13,26 @@ "community": { "data": { "region": "Region" - } + }, + "description": "Je\u015bli nie chcesz monitorowa\u0107 tylko rejonu i dystryktu, wybierz jego konkretn\u0105 spo\u0142eczno\u015b\u0107" }, "district": { "data": { "region": "Region" - } + }, + "description": "Je\u015bli nie chcesz monitorowa\u0107 tylko rejonu, wybierz jego konkretny dystrykt" }, "state": { "data": { "region": "Region" - } + }, + "description": "Wybierz rejon do monitorowania" }, "user": { "data": { "api_key": "Klucz API" - } + }, + "description": "Skonfiguruj integracj\u0119 Alarmy w Ukrainie. Aby wygenerowa\u0107 klucz API, przejd\u017a do {api_url}." } } } diff --git a/homeassistant/components/unifi/translations/ja.json b/homeassistant/components/unifi/translations/ja.json index 1f77f15d519..fba092385c1 100644 --- a/homeassistant/components/unifi/translations/ja.json +++ b/homeassistant/components/unifi/translations/ja.json @@ -26,6 +26,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "UniFi\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + }, "step": { "client_control": { "data": { diff --git a/homeassistant/components/utility_meter/translations/ja.json b/homeassistant/components/utility_meter/translations/ja.json index 90c175f16ec..ec12f2310be 100644 --- a/homeassistant/components/utility_meter/translations/ja.json +++ b/homeassistant/components/utility_meter/translations/ja.json @@ -15,7 +15,7 @@ "delta_values": "\u30bd\u30fc\u30b9\u5024\u304c\u7d76\u5bfe\u5024\u3067\u306f\u306a\u304f\u3001\u6700\u5f8c\u306e\u8aad\u307f\u53d6\u308a\u4ee5\u964d\u304c\u30c7\u30eb\u30bf\u5024\u3067\u3042\u308b\u5834\u5408\u306f\u3001\u6709\u52b9\u306b\u3057\u307e\u3059\u3002", "net_consumption": "\u30bd\u30fc\u30b9\u304c\u30cd\u30c3\u30c8\u30e1\u30fc\u30bf(net meter)\u3001\u3064\u307e\u308a\u5897\u52a0\u3082\u3059\u308b\u3057\u6e1b\u5c11\u3082\u3059\u308b\u5834\u5408\u306f\u3001\u6709\u52b9\u306b\u3057\u307e\u3059\u3002", "offset": "\u6bce\u6708\u306e\u30e1\u30fc\u30bf\u30fc\u30ea\u30bb\u30c3\u30c8\u306e\u65e5\u3092\u30aa\u30d5\u30bb\u30c3\u30c8\u3057\u307e\u3059\u3002", - "tariffs": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u6599\u91d1(Tariff)\u306e\u30ea\u30b9\u30c8\u3002\u5358\u4e00\u306e\u6599\u91d1\u8868\u306e\u307f\u304c\u5fc5\u8981\u306a\u5834\u5408\u306f\u7a7a\u306e\u307e\u307e\u306b\u3057\u307e\u3059\u3002" + "tariffs": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u6599\u91d1(Tariff)\u306e\u30ea\u30b9\u30c8\u3002\u5358\u4e00\u306e\u6599\u91d1\u8868\u306e\u307f\u304c\u5fc5\u8981\u306a\u5834\u5408\u306f\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u307e\u3059\u3002" }, "description": "\u8a2d\u5b9a\u3055\u308c\u305f\u671f\u9593\u3001\u901a\u5e38\u306f\u6bce\u6708\u3001\u69d8\u3005\u306a\u30e6\u30fc\u30c6\u30a3\u30ea\u30c6\u30a3(\u30a8\u30cd\u30eb\u30ae\u30fc\u3001\u30ac\u30b9\u3001\u6c34\u3001\u6696\u623f\u306a\u3069)\u306e\u6d88\u8cbb\u91cf\u3092\u8ffd\u8de1\u3059\u308b\u30bb\u30f3\u30b5\u30fc\u3092\u4f5c\u6210\u3057\u307e\u3059\u3002\u30e6\u30fc\u30c6\u30a3\u30ea\u30c6\u30a3\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u30aa\u30d7\u30b7\u30e7\u30f3\u3068\u3057\u3066\u3001\u6599\u91d1\u8868\u306b\u3088\u308b\u6d88\u8cbb\u91cf\u306e\u5206\u5272\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u307e\u3059\u3002\u3053\u306e\u5834\u5408\u3001\u5404\u6599\u91d1\u8868\u306e\u305f\u3081\u306e1\u3064\u306e\u30bb\u30f3\u30b5\u30fc\u3068\u3001\u73fe\u5728\u306e\u6599\u91d1(Tariff)\u3092\u9078\u629e\u3059\u308b\u305f\u3081\u306e\u9078\u629e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u304c\u4f5c\u6210\u3055\u308c\u307e\u3059\u3002", "title": "\u65b0\u3057\u3044\u30e6\u30fc\u30c6\u30a3\u30ea\u30c6\u30a3\u30e1\u30fc\u30bf\u30fc" diff --git a/homeassistant/components/xiaomi_aqara/translations/ja.json b/homeassistant/components/xiaomi_aqara/translations/ja.json index 87e757a2c5d..65e1870efe2 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ja.json +++ b/homeassistant/components/xiaomi_aqara/translations/ja.json @@ -35,7 +35,7 @@ "interface": "\u4f7f\u7528\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9", "mac": "Mac\u30a2\u30c9\u30ec\u30b9 (\u30aa\u30d7\u30b7\u30e7\u30f3)" }, - "description": "Xiaomi Aqara Gateway\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002IP\u30a2\u30c9\u30ec\u30b9\u3068MAC\u30a2\u30c9\u30ec\u30b9\u304c\u7a7a\u306e\u307e\u307e\u306e\u5834\u5408\u3001\u81ea\u52d5\u691c\u51fa\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059", + "description": "Xiaomi Aqara Gateway\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002IP\u30a2\u30c9\u30ec\u30b9\u3068MAC\u30a2\u30c9\u30ec\u30b9\u304c\u7a7a\u767d\u306e\u307e\u307e\u306e\u5834\u5408\u3001\u81ea\u52d5\u691c\u51fa\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059", "title": "Xiaomi Aqara Gateway" } } diff --git a/homeassistant/components/yeelight/translations/ja.json b/homeassistant/components/yeelight/translations/ja.json index e032979dcee..001cde41977 100644 --- a/homeassistant/components/yeelight/translations/ja.json +++ b/homeassistant/components/yeelight/translations/ja.json @@ -21,7 +21,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "\u30db\u30b9\u30c8\u3092\u7a7a\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc\u3092\u4f7f\u3063\u3066\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u7d22\u3057\u307e\u3059\u3002" + "description": "\u30db\u30b9\u30c8\u3092\u7a7a\u767d\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc\u3092\u4f7f\u3063\u3066\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u7d22\u3057\u307e\u3059\u3002" } } }, @@ -35,7 +35,7 @@ "transition": "\u9077\u79fb\u6642\u9593(Transition Time)(ms)", "use_music_mode": "\u97f3\u697d\u30e2\u30fc\u30c9\u3092\u6709\u52b9\u306b\u3059\u308b" }, - "description": "\u30e2\u30c7\u30eb\u3092\u7a7a\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u81ea\u52d5\u7684\u306b\u691c\u51fa\u3055\u308c\u307e\u3059\u3002" + "description": "\u30e2\u30c7\u30eb\u3092\u7a7a\u767d\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u81ea\u52d5\u7684\u306b\u691c\u51fa\u3055\u308c\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index 6df591dd0ab..9b00f062993 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -33,7 +33,7 @@ "s2_unauthenticated_key": "S2\u8a8d\u8a3c\u3055\u308c\u3066\u3044\u306a\u3044\u30ad\u30fc", "usb_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" }, - "description": "\u3053\u308c\u3089\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u304c\u7a7a\u306e\u307e\u307e\u306e\u5834\u5408\u3001\u30a2\u30c9\u30aa\u30f3\u306f\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30ad\u30fc\u3092\u751f\u6210\u3057\u307e\u3059\u3002", + "description": "\u3053\u308c\u3089\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u304c\u7a7a\u767d\u306e\u307e\u307e\u306e\u5834\u5408\u3001\u30a2\u30c9\u30aa\u30f3\u306f\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30ad\u30fc\u3092\u751f\u6210\u3057\u307e\u3059\u3002", "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u8a2d\u5b9a\u3092\u5165\u529b" }, "hassio_confirm": { @@ -124,7 +124,7 @@ "s2_unauthenticated_key": "S2\u8a8d\u8a3c\u3055\u308c\u3066\u3044\u306a\u3044\u30ad\u30fc", "usb_path": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" }, - "description": "\u3053\u308c\u3089\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u304c\u7a7a\u306e\u307e\u307e\u306e\u5834\u5408\u3001\u30a2\u30c9\u30aa\u30f3\u306f\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30ad\u30fc\u3092\u751f\u6210\u3057\u307e\u3059\u3002", + "description": "\u3053\u308c\u3089\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u304c\u7a7a\u767d\u306e\u307e\u307e\u306e\u5834\u5408\u3001\u30a2\u30c9\u30aa\u30f3\u306f\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30ad\u30fc\u3092\u751f\u6210\u3057\u307e\u3059\u3002", "title": "Z-Wave JS\u306e\u30a2\u30c9\u30aa\u30f3\u304c\u59cb\u307e\u308a\u307e\u3059\u3002" }, "install_addon": { From 99c5070591128542c1f9ea05cfd69a4fe8a8ea23 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 9 May 2022 18:41:25 +0200 Subject: [PATCH 0448/3516] Add missing cast test fixture (#71595) --- tests/components/cast/fixtures/rthkaudio2.m3u8 | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 tests/components/cast/fixtures/rthkaudio2.m3u8 diff --git a/tests/components/cast/fixtures/rthkaudio2.m3u8 b/tests/components/cast/fixtures/rthkaudio2.m3u8 new file mode 100644 index 00000000000..388c115635f --- /dev/null +++ b/tests/components/cast/fixtures/rthkaudio2.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=54000,CODECS="mp4a.40.2" +https://rthkaudio2-lh.akamaihd.net/i/radio2_1@355865/index_56_a-p.m3u8?sd=10&rebase=on +#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=54000,CODECS="mp4a.40.2" +https://rthkaudio2-lh.akamaihd.net/i/radio2_1@355865/index_56_a-b.m3u8?sd=10&rebase=on From 7ab4960b1e9311e515e2a7771deb68d6cbc95adb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 May 2022 21:14:02 -0400 Subject: [PATCH 0449/3516] Restore v23 stats migration tests (#71743) --- .../models_schema_23_with_newer_columns.py | 613 +++++++++++++++++ .../recorder/test_statistics_v23_migration.py | 618 ++++++++++++++++++ 2 files changed, 1231 insertions(+) create mode 100644 tests/components/recorder/models_schema_23_with_newer_columns.py create mode 100644 tests/components/recorder/test_statistics_v23_migration.py diff --git a/tests/components/recorder/models_schema_23_with_newer_columns.py b/tests/components/recorder/models_schema_23_with_newer_columns.py new file mode 100644 index 00000000000..a086aa588d4 --- /dev/null +++ b/tests/components/recorder/models_schema_23_with_newer_columns.py @@ -0,0 +1,613 @@ +"""Models for SQLAlchemy. + +This file contains the model definitions for schema version 23 +used by Home Assistant Core 2021.11.0, which adds the name column +to statistics_meta. + +The v23 schema as been slightly modified to add the EventData table to +allow the recorder to startup successfully. + +It is used to test the schema migration logic. +""" +from __future__ import annotations + +from datetime import datetime, timedelta +import json +import logging +from typing import TypedDict, overload + +from sqlalchemy import ( + BigInteger, + Boolean, + Column, + DateTime, + Float, + ForeignKey, + Identity, + Index, + Integer, + SmallInteger, + String, + Text, + distinct, +) +from sqlalchemy.dialects import mysql, oracle, postgresql +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.orm import declarative_base, relationship +from sqlalchemy.orm.session import Session + +from homeassistant.const import ( + MAX_LENGTH_EVENT_CONTEXT_ID, + MAX_LENGTH_EVENT_EVENT_TYPE, + MAX_LENGTH_EVENT_ORIGIN, + MAX_LENGTH_STATE_DOMAIN, + MAX_LENGTH_STATE_ENTITY_ID, + MAX_LENGTH_STATE_STATE, +) +from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id +from homeassistant.helpers.json import JSONEncoder +import homeassistant.util.dt as dt_util + +# SQLAlchemy Schema +# pylint: disable=invalid-name +Base = declarative_base() + +SCHEMA_VERSION = 23 + +_LOGGER = logging.getLogger(__name__) + +DB_TIMEZONE = "+00:00" + +TABLE_EVENTS = "events" +TABLE_STATES = "states" +TABLE_RECORDER_RUNS = "recorder_runs" +TABLE_SCHEMA_CHANGES = "schema_changes" +TABLE_STATISTICS = "statistics" +TABLE_STATISTICS_META = "statistics_meta" +TABLE_STATISTICS_RUNS = "statistics_runs" +TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" +TABLE_EVENT_DATA = "event_data" + +ALL_TABLES = [ + TABLE_STATES, + TABLE_EVENTS, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + TABLE_STATISTICS, + TABLE_STATISTICS_META, + TABLE_STATISTICS_RUNS, + TABLE_STATISTICS_SHORT_TERM, +] + +DATETIME_TYPE = DateTime(timezone=True).with_variant( + mysql.DATETIME(timezone=True, fsp=6), "mysql" +) +DOUBLE_TYPE = ( + Float() + .with_variant(mysql.DOUBLE(asdecimal=False), "mysql") + .with_variant(oracle.DOUBLE_PRECISION(), "oracle") + .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") +) + + +class Events(Base): # type: ignore + """Event history data.""" + + __table_args__ = ( + # Used for fetching events at a specific time + # see logbook + Index("ix_events_event_type_time_fired", "event_type", "time_fired"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_EVENTS + event_id = Column(Integer, Identity(), primary_key=True) + event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) + event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) + origin_idx = Column( + SmallInteger + ) # *** Not originally in v23, only added for recorder to startup ok + time_fired = Column(DATETIME_TYPE, index=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + data_id = Column( + Integer, ForeignKey("event_data.data_id"), index=True + ) # *** Not originally in v23, only added for recorder to startup ok + event_data_rel = relationship( + "EventData" + ) # *** Not originally in v23, only added for recorder to startup ok + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event, event_data=None): + """Create an event database object from a native event.""" + return Events( + event_type=event.event_type, + event_data=event_data + or json.dumps(event.data, cls=JSONEncoder, separators=(",", ":")), + origin=str(event.origin.value), + time_fired=event.time_fired, + context_id=event.context.id, + context_user_id=event.context.user_id, + context_parent_id=event.context.parent_id, + ) + + def to_native(self, validate_entity_id=True): + """Convert to a native HA Event.""" + context = Context( + id=self.context_id, + user_id=self.context_user_id, + parent_id=self.context_parent_id, + ) + try: + return Event( + self.event_type, + json.loads(self.event_data), + EventOrigin(self.origin), + process_timestamp(self.time_fired), + context=context, + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting to event: %s", self) + return None + + +# *** Not originally in v23, only added for recorder to startup ok +# This is not being tested by the v23 statistics migration tests +class EventData(Base): # type: ignore[misc,valid-type] + """Event data history.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_EVENT_DATA + data_id = Column(Integer, Identity(), primary_key=True) + hash = Column(BigInteger, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + + +class States(Base): # type: ignore + """State change history.""" + + __table_args__ = ( + # Used for fetching the state of entities at a specific time + # (get_states in history.py) + Index("ix_states_entity_id_last_updated", "entity_id", "last_updated"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATES + state_id = Column(Integer, Identity(), primary_key=True) + domain = Column(String(MAX_LENGTH_STATE_DOMAIN)) + entity_id = Column(String(MAX_LENGTH_STATE_ENTITY_ID)) + state = Column(String(MAX_LENGTH_STATE_STATE)) + attributes = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + event_id = Column( + Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True + ) + last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) + last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) + event = relationship("Events", uselist=False) + old_state = relationship("States", remote_side=[state_id]) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event): + """Create object from a state_changed event.""" + entity_id = event.data["entity_id"] + state = event.data.get("new_state") + + dbstate = States(entity_id=entity_id) + + # State got deleted + if state is None: + dbstate.state = "" + dbstate.domain = split_entity_id(entity_id)[0] + dbstate.attributes = "{}" + dbstate.last_changed = event.time_fired + dbstate.last_updated = event.time_fired + else: + dbstate.domain = state.domain + dbstate.state = state.state + dbstate.attributes = json.dumps( + dict(state.attributes), cls=JSONEncoder, separators=(",", ":") + ) + dbstate.last_changed = state.last_changed + dbstate.last_updated = state.last_updated + + return dbstate + + def to_native(self, validate_entity_id=True): + """Convert to an HA state object.""" + try: + return State( + self.entity_id, + self.state, + json.loads(self.attributes), + process_timestamp(self.last_changed), + process_timestamp(self.last_updated), + # Join the events table on event_id to get the context instead + # as it will always be there for state_changed events + context=Context(id=None), + validate_entity_id=validate_entity_id, + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state: %s", self) + return None + + +class StatisticResult(TypedDict): + """Statistic result data class. + + Allows multiple datapoints for the same statistic_id. + """ + + meta: StatisticMetaData + stat: StatisticData + + +class StatisticDataBase(TypedDict): + """Mandatory fields for statistic data class.""" + + start: datetime + + +class StatisticData(StatisticDataBase, total=False): + """Statistic data class.""" + + mean: float + min: float + max: float + last_reset: datetime | None + state: float + sum: float + + +class StatisticsBase: + """Statistics base class.""" + + id = Column(Integer, Identity(), primary_key=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + + @declared_attr + def metadata_id(self): + """Define the metadata_id column for sub classes.""" + return Column( + Integer, + ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), + index=True, + ) + + start = Column(DATETIME_TYPE, index=True) + mean = Column(DOUBLE_TYPE) + min = Column(DOUBLE_TYPE) + max = Column(DOUBLE_TYPE) + last_reset = Column(DATETIME_TYPE) + state = Column(DOUBLE_TYPE) + sum = Column(DOUBLE_TYPE) + + @classmethod + def from_stats(cls, metadata_id: int, stats: StatisticData): + """Create object from a statistics.""" + return cls( # type: ignore + metadata_id=metadata_id, + **stats, + ) + + +class Statistics(Base, StatisticsBase): # type: ignore + """Long term statistics.""" + + duration = timedelta(hours=1) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index("ix_statistics_statistic_id_start", "metadata_id", "start"), + ) + __tablename__ = TABLE_STATISTICS + + +class StatisticsShortTerm(Base, StatisticsBase): # type: ignore + """Short term statistics.""" + + duration = timedelta(minutes=5) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index("ix_statistics_short_term_statistic_id_start", "metadata_id", "start"), + ) + __tablename__ = TABLE_STATISTICS_SHORT_TERM + + +class StatisticMetaData(TypedDict): + """Statistic meta data class.""" + + has_mean: bool + has_sum: bool + name: str | None + source: str + statistic_id: str + unit_of_measurement: str | None + + +class StatisticsMeta(Base): # type: ignore + """Statistics meta data.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATISTICS_META + id = Column(Integer, Identity(), primary_key=True) + statistic_id = Column(String(255), index=True) + source = Column(String(32)) + unit_of_measurement = Column(String(255)) + has_mean = Column(Boolean) + has_sum = Column(Boolean) + name = Column(String(255)) + + @staticmethod + def from_meta(meta: StatisticMetaData) -> StatisticsMeta: + """Create object from meta data.""" + return StatisticsMeta(**meta) + + +class RecorderRuns(Base): # type: ignore + """Representation of recorder run.""" + + __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) + __tablename__ = TABLE_RECORDER_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True), default=dt_util.utcnow) + end = Column(DateTime(timezone=True)) + closed_incorrect = Column(Boolean, default=False) + created = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + end = ( + f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None + ) + return ( + f"" + ) + + def entity_ids(self, point_in_time=None): + """Return the entity ids that existed in this run. + + Specify point_in_time if you want to know which existed at that point + in time inside the run. + """ + session = Session.object_session(self) + + assert session is not None, "RecorderRuns need to be persisted" + + query = session.query(distinct(States.entity_id)).filter( + States.last_updated >= self.start + ) + + if point_in_time is not None: + query = query.filter(States.last_updated < point_in_time) + elif self.end is not None: + query = query.filter(States.last_updated < self.end) + + return [row[0] for row in query] + + def to_native(self, validate_entity_id=True): + """Return self, native format is this model.""" + return self + + +class SchemaChanges(Base): # type: ignore + """Representation of schema version changes.""" + + __tablename__ = TABLE_SCHEMA_CHANGES + change_id = Column(Integer, Identity(), primary_key=True) + schema_version = Column(Integer) + changed = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +class StatisticsRuns(Base): # type: ignore + """Representation of statistics run.""" + + __tablename__ = TABLE_STATISTICS_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True)) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +@overload +def process_timestamp(ts: None) -> None: + ... + + +@overload +def process_timestamp(ts: datetime) -> datetime: + ... + + +def process_timestamp(ts: datetime | None) -> datetime | None: + """Process a timestamp into datetime object.""" + if ts is None: + return None + if ts.tzinfo is None: + return ts.replace(tzinfo=dt_util.UTC) + + return dt_util.as_utc(ts) + + +@overload +def process_timestamp_to_utc_isoformat(ts: None) -> None: + ... + + +@overload +def process_timestamp_to_utc_isoformat(ts: datetime) -> str: + ... + + +def process_timestamp_to_utc_isoformat(ts: datetime | None) -> str | None: + """Process a timestamp into UTC isotime.""" + if ts is None: + return None + if ts.tzinfo == dt_util.UTC: + return ts.isoformat() + if ts.tzinfo is None: + return f"{ts.isoformat()}{DB_TIMEZONE}" + return ts.astimezone(dt_util.UTC).isoformat() + + +class LazyState(State): + """A lazy version of core State.""" + + __slots__ = [ + "_row", + "entity_id", + "state", + "_attributes", + "_last_changed", + "_last_updated", + "_context", + ] + + def __init__(self, row): # pylint: disable=super-init-not-called + """Init the lazy state.""" + self._row = row + self.entity_id = self._row.entity_id + self.state = self._row.state or "" + self._attributes = None + self._last_changed = None + self._last_updated = None + self._context = None + + @property # type: ignore + def attributes(self): + """State attributes.""" + if not self._attributes: + try: + self._attributes = json.loads(self._row.attributes) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state: %s", self._row) + self._attributes = {} + return self._attributes + + @attributes.setter + def attributes(self, value): + """Set attributes.""" + self._attributes = value + + @property # type: ignore + def context(self): + """State context.""" + if not self._context: + self._context = Context(id=None) + return self._context + + @context.setter + def context(self, value): + """Set context.""" + self._context = value + + @property # type: ignore + def last_changed(self): + """Last changed datetime.""" + if not self._last_changed: + self._last_changed = process_timestamp(self._row.last_changed) + return self._last_changed + + @last_changed.setter + def last_changed(self, value): + """Set last changed datetime.""" + self._last_changed = value + + @property # type: ignore + def last_updated(self): + """Last updated datetime.""" + if not self._last_updated: + self._last_updated = process_timestamp(self._row.last_updated) + return self._last_updated + + @last_updated.setter + def last_updated(self, value): + """Set last updated datetime.""" + self._last_updated = value + + def as_dict(self): + """Return a dict representation of the LazyState. + + Async friendly. + + To be used for JSON serialization. + """ + if self._last_changed: + last_changed_isoformat = self._last_changed.isoformat() + else: + last_changed_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_changed + ) + if self._last_updated: + last_updated_isoformat = self._last_updated.isoformat() + else: + last_updated_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_updated + ) + return { + "entity_id": self.entity_id, + "state": self.state, + "attributes": self._attributes or self.attributes, + "last_changed": last_changed_isoformat, + "last_updated": last_updated_isoformat, + } + + def __eq__(self, other): + """Return the comparison.""" + return ( + other.__class__ in [self.__class__, State] + and self.entity_id == other.entity_id + and self.state == other.state + and self.attributes == other.attributes + ) diff --git a/tests/components/recorder/test_statistics_v23_migration.py b/tests/components/recorder/test_statistics_v23_migration.py new file mode 100644 index 00000000000..d487743a87f --- /dev/null +++ b/tests/components/recorder/test_statistics_v23_migration.py @@ -0,0 +1,618 @@ +"""The tests for sensor recorder platform migrating statistics from v23. + +The v23 schema used for these tests has been slightly modified to add the +EventData table to allow the recorder to startup successfully. +""" +# pylint: disable=protected-access,invalid-name +import importlib +import json +import sys +from unittest.mock import patch + +import pytest +from sqlalchemy import create_engine +from sqlalchemy.orm import Session + +from homeassistant.components import recorder +from homeassistant.components.recorder import SQLITE_URL_PREFIX, statistics +from homeassistant.components.recorder.util import session_scope +from homeassistant.setup import setup_component +import homeassistant.util.dt as dt_util + +from tests.common import get_test_home_assistant +from tests.components.recorder.common import wait_recording_done + +ORIG_TZ = dt_util.DEFAULT_TIME_ZONE + +CREATE_ENGINE_TARGET = "homeassistant.components.recorder.core.create_engine" +SCHEMA_MODULE = "tests.components.recorder.models_schema_23_with_newer_columns" + + +def _create_engine_test(*args, **kwargs): + """Test version of create_engine that initializes with old schema. + + This simulates an existing db with the old schema. + """ + importlib.import_module(SCHEMA_MODULE) + old_models = sys.modules[SCHEMA_MODULE] + engine = create_engine(*args, **kwargs) + old_models.Base.metadata.create_all(engine) + with Session(engine) as session: + session.add(recorder.models.StatisticsRuns(start=statistics.get_start_time())) + session.add( + recorder.models.SchemaChanges(schema_version=old_models.SCHEMA_VERSION) + ) + session.commit() + return engine + + +def test_delete_duplicates(caplog, tmpdir): + """Test removal of duplicated statistics.""" + test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") + dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + + importlib.import_module(SCHEMA_MODULE) + old_models = sys.modules[SCHEMA_MODULE] + + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + + external_energy_statistics_1 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 2, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 4, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + ) + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_statistics_2 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 20, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 30, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 40, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + ) + external_energy_metadata_2 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_2", + "unit_of_measurement": "kWh", + } + external_co2_statistics = ( + { + "start": period1, + "last_reset": None, + "mean": 10, + }, + { + "start": period2, + "last_reset": None, + "mean": 30, + }, + { + "start": period3, + "last_reset": None, + "mean": 60, + }, + { + "start": period4, + "last_reset": None, + "mean": 90, + }, + ) + external_co2_metadata = { + "has_mean": True, + "has_sum": False, + "name": "Fossil percentage", + "source": "test", + "statistic_id": "test:fossil_percentage", + "unit_of_measurement": "%", + } + + # Create some duplicated statistics with schema version 23 + with patch.object(recorder, "models", old_models), patch.object( + recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION + ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): + hass = get_test_home_assistant() + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + wait_recording_done(hass) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) + ) + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) + ) + session.add(recorder.models.StatisticsMeta.from_meta(external_co2_metadata)) + with session_scope(hass=hass) as session: + for stat in external_energy_statistics_1: + session.add(recorder.models.Statistics.from_stats(1, stat)) + for stat in external_energy_statistics_2: + session.add(recorder.models.Statistics.from_stats(2, stat)) + for stat in external_co2_statistics: + session.add(recorder.models.Statistics.from_stats(3, stat)) + + hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ + + # Test that the duplicates are removed during migration from schema 23 + hass = get_test_home_assistant() + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + hass.start() + wait_recording_done(hass) + wait_recording_done(hass) + hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ + + assert "Deleted 2 duplicated statistics rows" in caplog.text + assert "Found non identical" not in caplog.text + assert "Found duplicated" not in caplog.text + + +def test_delete_duplicates_many(caplog, tmpdir): + """Test removal of duplicated statistics.""" + test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") + dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + + importlib.import_module(SCHEMA_MODULE) + old_models = sys.modules[SCHEMA_MODULE] + + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + + external_energy_statistics_1 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 2, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 4, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + ) + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_statistics_2 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 20, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 30, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 40, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + ) + external_energy_metadata_2 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_2", + "unit_of_measurement": "kWh", + } + external_co2_statistics = ( + { + "start": period1, + "last_reset": None, + "mean": 10, + }, + { + "start": period2, + "last_reset": None, + "mean": 30, + }, + { + "start": period3, + "last_reset": None, + "mean": 60, + }, + { + "start": period4, + "last_reset": None, + "mean": 90, + }, + ) + external_co2_metadata = { + "has_mean": True, + "has_sum": False, + "name": "Fossil percentage", + "source": "test", + "statistic_id": "test:fossil_percentage", + "unit_of_measurement": "%", + } + + # Create some duplicated statistics with schema version 23 + with patch.object(recorder, "models", old_models), patch.object( + recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION + ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): + hass = get_test_home_assistant() + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + wait_recording_done(hass) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) + ) + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) + ) + session.add(recorder.models.StatisticsMeta.from_meta(external_co2_metadata)) + with session_scope(hass=hass) as session: + for stat in external_energy_statistics_1: + session.add(recorder.models.Statistics.from_stats(1, stat)) + for _ in range(3000): + session.add( + recorder.models.Statistics.from_stats( + 1, external_energy_statistics_1[-1] + ) + ) + for stat in external_energy_statistics_2: + session.add(recorder.models.Statistics.from_stats(2, stat)) + for stat in external_co2_statistics: + session.add(recorder.models.Statistics.from_stats(3, stat)) + + hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ + + # Test that the duplicates are removed during migration from schema 23 + hass = get_test_home_assistant() + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + hass.start() + wait_recording_done(hass) + wait_recording_done(hass) + hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ + + assert "Deleted 3002 duplicated statistics rows" in caplog.text + assert "Found non identical" not in caplog.text + assert "Found duplicated" not in caplog.text + + +@pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") +def test_delete_duplicates_non_identical(caplog, tmpdir): + """Test removal of duplicated statistics.""" + test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") + dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + + importlib.import_module(SCHEMA_MODULE) + old_models = sys.modules[SCHEMA_MODULE] + + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + + external_energy_statistics_1 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 2, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 4, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 6, + }, + ) + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_statistics_2 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 20, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 30, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 40, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + ) + external_energy_metadata_2 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_2", + "unit_of_measurement": "kWh", + } + + # Create some duplicated statistics with schema version 23 + with patch.object(recorder, "models", old_models), patch.object( + recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION + ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): + hass = get_test_home_assistant() + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + wait_recording_done(hass) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) + ) + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) + ) + with session_scope(hass=hass) as session: + for stat in external_energy_statistics_1: + session.add(recorder.models.Statistics.from_stats(1, stat)) + for stat in external_energy_statistics_2: + session.add(recorder.models.Statistics.from_stats(2, stat)) + + hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ + + # Test that the duplicates are removed during migration from schema 23 + hass = get_test_home_assistant() + hass.config.config_dir = tmpdir + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + hass.start() + wait_recording_done(hass) + wait_recording_done(hass) + hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ + + assert "Deleted 2 duplicated statistics rows" in caplog.text + assert "Deleted 1 non identical" in caplog.text + assert "Found duplicated" not in caplog.text + + isotime = dt_util.utcnow().isoformat() + backup_file_name = f".storage/deleted_statistics.{isotime}.json" + + with open(hass.config.path(backup_file_name)) as backup_file: + backup = json.load(backup_file) + + assert backup == [ + { + "duplicate": { + "created": "2021-08-01T00:00:00", + "id": 4, + "last_reset": None, + "max": None, + "mean": None, + "metadata_id": 1, + "min": None, + "start": "2021-10-31T23:00:00", + "state": 3.0, + "sum": 5.0, + }, + "original": { + "created": "2021-08-01T00:00:00", + "id": 5, + "last_reset": None, + "max": None, + "mean": None, + "metadata_id": 1, + "min": None, + "start": "2021-10-31T23:00:00", + "state": 3.0, + "sum": 6.0, + }, + } + ] + + +def test_delete_duplicates_short_term(caplog, tmpdir): + """Test removal of duplicated statistics.""" + test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") + dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + + importlib.import_module(SCHEMA_MODULE) + old_models = sys.modules[SCHEMA_MODULE] + + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + statistic_row = { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + } + + # Create some duplicated statistics with schema version 23 + with patch.object(recorder, "models", old_models), patch.object( + recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION + ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): + hass = get_test_home_assistant() + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + wait_recording_done(hass) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) + ) + with session_scope(hass=hass) as session: + session.add( + recorder.models.StatisticsShortTerm.from_stats(1, statistic_row) + ) + session.add( + recorder.models.StatisticsShortTerm.from_stats(1, statistic_row) + ) + + hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ + + # Test that the duplicates are removed during migration from schema 23 + hass = get_test_home_assistant() + hass.config.config_dir = tmpdir + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + hass.start() + wait_recording_done(hass) + wait_recording_done(hass) + hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ + + assert "duplicated statistics rows" not in caplog.text + assert "Found non identical" not in caplog.text + assert "Deleted duplicated short term statistic" in caplog.text From 32e4046435428567cba74c8dbc615e339f33cf70 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 12 May 2022 19:33:52 -0700 Subject: [PATCH 0450/3516] Prepare google calendar integration for Application Credentials (#71748) * Prepare google calendar integration for Application Credentials Update google calendar integration to have fewer dependencies on yaml configuration data to prepare for supporting application credentials, which means setup can happen without configuration.yaml at all. This pre-factoring will allow the following PR adding application credentials support to be more focused. * Add test coverage for device auth checks --- homeassistant/components/google/__init__.py | 33 ++++++++++------- homeassistant/components/google/api.py | 35 ++++++++++++++----- .../components/google/config_flow.py | 28 ++++++++++++--- homeassistant/components/google/const.py | 3 ++ tests/components/google/test_config_flow.py | 29 ++++++++++++++- 5 files changed, 103 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 98eadead101..c5f62c0034f 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -40,7 +40,7 @@ from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.typing import ConfigType from . import config_flow -from .api import ApiAuthImpl, DeviceAuth +from .api import ApiAuthImpl, DeviceAuth, get_feature_access from .const import ( CONF_CALENDAR_ACCESS, DATA_CONFIG, @@ -172,7 +172,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # a ConfigEntry managed by home assistant. storage = Storage(hass.config.path(TOKEN_FILE)) creds = await hass.async_add_executor_job(storage.get) - if creds and conf[CONF_CALENDAR_ACCESS].scope in creds.scopes: + if creds and get_feature_access(hass).scope in creds.scopes: _LOGGER.debug("Importing configuration entry with credentials") hass.async_create_task( hass.config_entries.flow.async_init( @@ -210,8 +210,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except aiohttp.ClientError as err: raise ConfigEntryNotReady from err - required_scope = hass.data[DOMAIN][DATA_CONFIG][CONF_CALENDAR_ACCESS].scope - if required_scope not in session.token.get("scope", []): + access = get_feature_access(hass) + if access.scope not in session.token.get("scope", []): raise ConfigEntryAuthFailed( "Required scopes are not available, reauth required" ) @@ -220,7 +220,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data[DOMAIN][DATA_SERVICE] = calendar_service - await async_setup_services(hass, hass.data[DOMAIN][DATA_CONFIG], calendar_service) + track_new = hass.data[DOMAIN][DATA_CONFIG].get(CONF_TRACK_NEW, True) + await async_setup_services(hass, track_new, calendar_service) + # Only expose the add event service if we have the correct permissions + if access is FeatureAccess.read_write: + await async_setup_add_event_service(hass, calendar_service) hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -234,7 +238,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_services( hass: HomeAssistant, - config: ConfigType, + track_new: bool, calendar_service: GoogleCalendarService, ) -> None: """Set up the service listeners.""" @@ -274,7 +278,7 @@ async def async_setup_services( tasks = [] for calendar_item in result.items: calendar = calendar_item.dict(exclude_unset=True) - calendar[CONF_TRACK] = config[CONF_TRACK_NEW] + calendar[CONF_TRACK] = track_new tasks.append( hass.services.async_call(DOMAIN, SERVICE_FOUND_CALENDARS, calendar) ) @@ -282,6 +286,13 @@ async def async_setup_services( hass.services.async_register(DOMAIN, SERVICE_SCAN_CALENDARS, _scan_for_calendars) + +async def async_setup_add_event_service( + hass: HomeAssistant, + calendar_service: GoogleCalendarService, +) -> None: + """Add the service to add events.""" + async def _add_event(call: ServiceCall) -> None: """Add a new event to calendar.""" start: DateOrDatetime | None = None @@ -333,11 +344,9 @@ async def async_setup_services( ), ) - # Only expose the add event service if we have the correct permissions - if config.get(CONF_CALENDAR_ACCESS) is FeatureAccess.read_write: - hass.services.async_register( - DOMAIN, SERVICE_ADD_EVENT, _add_event, schema=ADD_EVENT_SERVICE_SCHEMA - ) + hass.services.async_register( + DOMAIN, SERVICE_ADD_EVENT, _add_event, schema=ADD_EVENT_SERVICE_SCHEMA + ) def get_calendar_info( diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index 5de563393e1..bb32d46f0e4 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -19,18 +19,25 @@ from oauth2client.client import ( OAuth2WebServerFlow, ) -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import dt -from .const import CONF_CALENDAR_ACCESS, DATA_CONFIG, DEVICE_AUTH_IMPL, DOMAIN +from .const import ( + CONF_CALENDAR_ACCESS, + DATA_CONFIG, + DEFAULT_FEATURE_ACCESS, + DEVICE_AUTH_IMPL, + DOMAIN, + FeatureAccess, +) _LOGGER = logging.getLogger(__name__) EVENT_PAGE_SIZE = 100 EXCHANGE_TIMEOUT_SECONDS = 60 +DEVICE_AUTH_CREDS = "creds" class OAuthError(Exception): @@ -53,7 +60,7 @@ class DeviceAuth(config_entry_oauth2_flow.LocalOAuth2Implementation): async def async_resolve_external_data(self, external_data: Any) -> dict: """Resolve a Google API Credentials object to Home Assistant token.""" - creds: Credentials = external_data["creds"] + creds: Credentials = external_data[DEVICE_AUTH_CREDS] return { "access_token": creds.access_token, "refresh_token": creds.refresh_token, @@ -132,13 +139,25 @@ class DeviceFlow: ) -async def async_create_device_flow(hass: HomeAssistant) -> DeviceFlow: +def get_feature_access(hass: HomeAssistant) -> FeatureAccess: + """Return the desired calendar feature access.""" + # This may be called during config entry setup without integration setup running when there + # is no google entry in configuration.yaml + return ( + hass.data.get(DOMAIN, {}) + .get(DATA_CONFIG, {}) + .get(CONF_CALENDAR_ACCESS, DEFAULT_FEATURE_ACCESS) + ) + + +async def async_create_device_flow( + hass: HomeAssistant, client_id: str, client_secret: str, access: FeatureAccess +) -> DeviceFlow: """Create a new Device flow.""" - conf = hass.data[DOMAIN][DATA_CONFIG] oauth_flow = OAuth2WebServerFlow( - client_id=conf[CONF_CLIENT_ID], - client_secret=conf[CONF_CLIENT_SECRET], - scope=conf[CONF_CALENDAR_ACCESS].scope, + client_id=client_id, + client_secret=client_secret, + scope=access.scope, redirect_uri="", ) try: diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index 8bbd2a6c2b1..f5e567a5272 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -9,7 +9,14 @@ from oauth2client.client import Credentials from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow -from .api import DeviceFlow, OAuthError, async_create_device_flow +from .api import ( + DEVICE_AUTH_CREDS, + DeviceAuth, + DeviceFlow, + OAuthError, + async_create_device_flow, + get_feature_access, +) from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -67,15 +74,28 @@ class OAuth2FlowHandler( if not self._device_flow: _LOGGER.debug("Creating DeviceAuth flow") + if not isinstance(self.flow_impl, DeviceAuth): + _LOGGER.error( + "Unexpected OAuth implementation does not support device auth: %s", + self.flow_impl, + ) + return self.async_abort(reason="oauth_error") try: - device_flow = await async_create_device_flow(self.hass) + device_flow = await async_create_device_flow( + self.hass, + self.flow_impl.client_id, + self.flow_impl.client_secret, + get_feature_access(self.hass), + ) except OAuthError as err: _LOGGER.error("Error initializing device flow: %s", str(err)) return self.async_abort(reason="oauth_error") self._device_flow = device_flow async def _exchange_finished(creds: Credentials | None) -> None: - self.external_data = {"creds": creds} # is None on timeout/expiration + self.external_data = { + DEVICE_AUTH_CREDS: creds + } # is None on timeout/expiration self.hass.async_create_task( self.hass.config_entries.flow.async_configure( flow_id=self.flow_id, user_input={} @@ -97,7 +117,7 @@ class OAuth2FlowHandler( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle external yaml configuration.""" - if self.external_data.get("creds") is None: + if self.external_data.get(DEVICE_AUTH_CREDS) is None: return self.async_abort(reason="code_expired") return await super().async_step_creation(user_input) diff --git a/homeassistant/components/google/const.py b/homeassistant/components/google/const.py index d5cdabb0638..c01ff1ea48b 100644 --- a/homeassistant/components/google/const.py +++ b/homeassistant/components/google/const.py @@ -28,3 +28,6 @@ class FeatureAccess(Enum): def scope(self) -> str: """Google calendar scope for the feature.""" return self._scope + + +DEFAULT_FEATURE_ACCESS = FeatureAccess.read_write diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index e96a4c3fd5f..0991f4e5194 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -13,6 +13,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.google.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util.dt import utcnow from .conftest import ComponentSetup, YieldFixture @@ -254,7 +255,7 @@ async def test_existing_config_entry( async def test_missing_configuration( hass: HomeAssistant, ) -> None: - """Test can't configure when config entry already exists.""" + """Test can't configure when no authentication source is available.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -262,6 +263,32 @@ async def test_missing_configuration( assert result.get("reason") == "missing_configuration" +async def test_wrong_configuration( + hass: HomeAssistant, +) -> None: + """Test can't use the wrong type of authentication.""" + + # Google calendar flow currently only supports device auth + config_entry_oauth2_flow.async_register_implementation( + hass, + DOMAIN, + config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + DOMAIN, + "client-id", + "client-secret", + "http://example/authorize", + "http://example/token", + ), + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "abort" + assert result.get("reason") == "oauth_error" + + async def test_import_config_entry_from_existing_token( hass: HomeAssistant, mock_token_read: None, From 75058e63a4a9dbe5facf66532134684bcc9e082e Mon Sep 17 00:00:00 2001 From: Jelte Zeilstra Date: Fri, 13 May 2022 09:17:41 +0200 Subject: [PATCH 0451/3516] Create Update entities for Ubiquiti network devices (#71700) Co-authored-by: Robert Svensson --- homeassistant/components/unifi/controller.py | 2 +- .../components/unifi/device_tracker.py | 10 ++ homeassistant/components/unifi/update.py | 128 ++++++++++++++ tests/components/unifi/test_device_tracker.py | 1 + tests/components/unifi/test_diagnostics.py | 4 + tests/components/unifi/test_update.py | 167 ++++++++++++++++++ 6 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/unifi/update.py create mode 100644 tests/components/unifi/test_update.py diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index be59a25f69f..fa92568b477 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -74,7 +74,7 @@ from .switch import BLOCK_SWITCH, POE_SWITCH RETRY_TIMER = 15 CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1) -PLATFORMS = [Platform.DEVICE_TRACKER, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.DEVICE_TRACKER, Platform.SENSOR, Platform.SWITCH, Platform.UPDATE] CLIENT_CONNECTED = ( WIRED_CLIENT_CONNECTED, diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 2ae8eb2e32b..947e4523531 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -429,6 +429,16 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): return attributes + @property + def ip_address(self) -> str: + """Return the primary ip address of the device.""" + return self.device.ip + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self.device.mac + async def options_updated(self) -> None: """Config entry options are updated, remove entity if option is disabled.""" if not self.controller.option_track_devices: diff --git a/homeassistant/components/unifi/update.py b/homeassistant/components/unifi/update.py new file mode 100644 index 00000000000..09720f15f84 --- /dev/null +++ b/homeassistant/components/unifi/update.py @@ -0,0 +1,128 @@ +"""Update entities for Ubiquiti network devices.""" +from __future__ import annotations + +import logging + +from homeassistant.components.update import ( + DOMAIN, + UpdateDeviceClass, + UpdateEntity, + UpdateEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_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 homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN +from .unifi_entity_base import UniFiBase + +LOGGER = logging.getLogger(__name__) + +DEVICE_UPDATE = "device_update" + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up update entities for UniFi Network integration.""" + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + controller.entities[DOMAIN] = {DEVICE_UPDATE: set()} + + @callback + def items_added( + clients: set = controller.api.clients, devices: set = controller.api.devices + ) -> None: + """Add device update entities.""" + add_device_update_entities(controller, async_add_entities, devices) + + for signal in (controller.signal_update, controller.signal_options_update): + config_entry.async_on_unload( + async_dispatcher_connect(hass, signal, items_added) + ) + + items_added() + + +@callback +def add_device_update_entities(controller, async_add_entities, devices): + """Add new device update entities from the controller.""" + entities = [] + + for mac in devices: + if mac in controller.entities[DOMAIN][UniFiDeviceUpdateEntity.TYPE]: + continue + + device = controller.api.devices[mac] + entities.append(UniFiDeviceUpdateEntity(device, controller)) + + if entities: + async_add_entities(entities) + + +class UniFiDeviceUpdateEntity(UniFiBase, UpdateEntity): + """Update entity for a UniFi network infrastructure device.""" + + DOMAIN = DOMAIN + TYPE = DEVICE_UPDATE + _attr_device_class = UpdateDeviceClass.FIRMWARE + _attr_supported_features = UpdateEntityFeature.PROGRESS + + def __init__(self, device, controller): + """Set up device update entity.""" + super().__init__(device, controller) + + self.device = self._item + + @property + def name(self) -> str: + """Return the name of the device.""" + return self.device.name or self.device.model + + @property + def unique_id(self) -> str: + """Return a unique identifier for this device.""" + return f"{self.TYPE}-{self.device.mac}" + + @property + def available(self) -> bool: + """Return if controller is available.""" + return not self.device.disabled and self.controller.available + + @property + def in_progress(self) -> bool: + """Update installation in progress.""" + return self.device.state == 4 + + @property + def installed_version(self) -> str | None: + """Version currently in use.""" + return self.device.version + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + return self.device.upgrade_to_firmware or self.device.version + + @property + def device_info(self) -> DeviceInfo: + """Return a device description for device registry.""" + info = DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, self.device.mac)}, + manufacturer=ATTR_MANUFACTURER, + model=self.device.model, + sw_version=self.device.version, + ) + + if self.device.name: + info[ATTR_NAME] = self.device.name + + return info + + async def options_updated(self) -> None: + """No action needed.""" diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 532f19c35ae..736e85d1b8c 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -499,6 +499,7 @@ async def test_option_track_devices(hass, aioclient_mock, mock_device_registry): "board_rev": 3, "device_id": "mock-id", "last_seen": 1562600145, + "ip": "10.0.1.1", "mac": "00:00:00:00:01:01", "model": "US16P150", "name": "Device", diff --git a/tests/components/unifi/test_diagnostics.py b/tests/components/unifi/test_diagnostics.py index ccec7fcb48a..6584b947293 100644 --- a/tests/components/unifi/test_diagnostics.py +++ b/tests/components/unifi/test_diagnostics.py @@ -14,6 +14,7 @@ from homeassistant.components.unifi.switch import ( OUTLET_SWITCH, POE_SWITCH, ) +from homeassistant.components.unifi.update import DEVICE_UPDATE from homeassistant.const import Platform from .test_controller import setup_unifi_integration @@ -161,6 +162,9 @@ async def test_entry_diagnostics(hass, hass_client, aioclient_mock): POE_SWITCH: ["00:00:00:00:00:00"], OUTLET_SWITCH: [], }, + str(Platform.UPDATE): { + DEVICE_UPDATE: ["00:00:00:00:00:01"], + }, }, "clients": { "00:00:00:00:00:00": { diff --git a/tests/components/unifi/test_update.py b/tests/components/unifi/test_update.py new file mode 100644 index 00000000000..b5eb9c1d02e --- /dev/null +++ b/tests/components/unifi/test_update.py @@ -0,0 +1,167 @@ +"""The tests for the UniFi Network update platform.""" + +from aiounifi.controller import MESSAGE_DEVICE +from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING + +from homeassistant.components.update import ( + ATTR_IN_PROGRESS, + ATTR_INSTALLED_VERSION, + ATTR_LATEST_VERSION, + DOMAIN as UPDATE_DOMAIN, + UpdateDeviceClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) + +from .test_controller import setup_unifi_integration + + +async def test_no_entities(hass, aioclient_mock): + """Test the update_clients function when no clients are found.""" + await setup_unifi_integration(hass, aioclient_mock) + + assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 0 + + +async def test_device_updates( + hass, aioclient_mock, mock_unifi_websocket, mock_device_registry +): + """Test the update_items function with some devices.""" + device_1 = { + "board_rev": 3, + "device_id": "mock-id", + "has_fan": True, + "fan_level": 0, + "ip": "10.0.1.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device 1", + "next_interval": 20, + "overheating": True, + "state": 1, + "type": "usw", + "upgradable": True, + "version": "4.0.42.10433", + "upgrade_to_firmware": "4.3.17.11279", + } + device_2 = { + "board_rev": 3, + "device_id": "mock-id", + "has_fan": True, + "ip": "10.0.1.2", + "mac": "00:00:00:00:01:02", + "model": "US16P150", + "name": "Device 2", + "next_interval": 20, + "state": 0, + "type": "usw", + "version": "4.0.42.10433", + } + await setup_unifi_integration( + hass, + aioclient_mock, + devices_response=[device_1, device_2], + ) + + assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 2 + + device_1_state = hass.states.get("update.device_1") + assert device_1_state.state == STATE_ON + assert device_1_state.attributes[ATTR_INSTALLED_VERSION] == "4.0.42.10433" + assert device_1_state.attributes[ATTR_LATEST_VERSION] == "4.3.17.11279" + assert device_1_state.attributes[ATTR_IN_PROGRESS] is False + assert device_1_state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + + device_2_state = hass.states.get("update.device_2") + assert device_2_state.state == STATE_OFF + assert device_2_state.attributes[ATTR_INSTALLED_VERSION] == "4.0.42.10433" + assert device_2_state.attributes[ATTR_LATEST_VERSION] == "4.0.42.10433" + assert device_2_state.attributes[ATTR_IN_PROGRESS] is False + assert device_2_state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + + # Simulate start of update + + device_1["state"] = 4 + mock_unifi_websocket( + data={ + "meta": {"message": MESSAGE_DEVICE}, + "data": [device_1], + } + ) + await hass.async_block_till_done() + + device_1_state = hass.states.get("update.device_1") + assert device_1_state.state == STATE_ON + assert device_1_state.attributes[ATTR_INSTALLED_VERSION] == "4.0.42.10433" + assert device_1_state.attributes[ATTR_LATEST_VERSION] == "4.3.17.11279" + assert device_1_state.attributes[ATTR_IN_PROGRESS] is True + + # Simulate update finished + + device_1["state"] = "0" + device_1["version"] = "4.3.17.11279" + device_1["upgradable"] = False + del device_1["upgrade_to_firmware"] + mock_unifi_websocket( + data={ + "meta": {"message": MESSAGE_DEVICE}, + "data": [device_1], + } + ) + await hass.async_block_till_done() + + device_1_state = hass.states.get("update.device_1") + assert device_1_state.state == STATE_OFF + assert device_1_state.attributes[ATTR_INSTALLED_VERSION] == "4.3.17.11279" + assert device_1_state.attributes[ATTR_LATEST_VERSION] == "4.3.17.11279" + assert device_1_state.attributes[ATTR_IN_PROGRESS] is False + + +async def test_controller_state_change( + hass, aioclient_mock, mock_unifi_websocket, mock_device_registry +): + """Verify entities state reflect on controller becoming unavailable.""" + device = { + "board_rev": 3, + "device_id": "mock-id", + "has_fan": True, + "fan_level": 0, + "ip": "10.0.1.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device", + "next_interval": 20, + "overheating": True, + "state": 1, + "type": "usw", + "upgradable": True, + "version": "4.0.42.10433", + "upgrade_to_firmware": "4.3.17.11279", + } + + await setup_unifi_integration( + hass, + aioclient_mock, + devices_response=[device], + ) + + assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 + assert hass.states.get("update.device").state == STATE_ON + + # Controller unavailable + mock_unifi_websocket(state=STATE_DISCONNECTED) + await hass.async_block_till_done() + + assert hass.states.get("update.device").state == STATE_UNAVAILABLE + + # Controller available + mock_unifi_websocket(state=STATE_RUNNING) + await hass.async_block_till_done() + + assert hass.states.get("update.device").state == STATE_ON From bed2d1e37b967f857aecb4ca54b5da9be09d8b93 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 13 May 2022 09:51:19 +0200 Subject: [PATCH 0452/3516] Streamline setup of deCONZ climate platform (#71708) --- homeassistant/components/deconz/climate.py | 50 +++++++++++----------- homeassistant/components/deconz/gateway.py | 2 + 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index aaedc7bae90..bdc366ff7db 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any +from pydeconz.models.event import EventType from pydeconz.models.sensor.thermostat import ( THERMOSTAT_FAN_MODE_AUTO, THERMOSTAT_FAN_MODE_HIGH, @@ -91,45 +92,42 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the deCONZ climate devices. - - Thermostats are based on the same device class as sensors in deCONZ. - """ + """Set up the deCONZ climate devices.""" gateway = get_gateway_from_config_entry(hass, config_entry) gateway.entities[DOMAIN] = set() @callback - def async_add_climate(sensors: list[Thermostat] | None = None) -> None: - """Add climate devices from deCONZ.""" - entities: list[DeconzThermostat] = [] + def async_add_climate(_: EventType, climate_id: str) -> None: + """Add climate from deCONZ.""" + climate = gateway.api.sensors.thermostat[climate_id] + if not gateway.option_allow_clip_sensor and climate.type.startswith("CLIP"): + return + async_add_entities([DeconzThermostat(climate, gateway)]) - if sensors is None: - sensors = list(gateway.api.sensors.thermostat.values()) + config_entry.async_on_unload( + gateway.api.sensors.thermostat.subscribe( + async_add_climate, + EventType.ADDED, + ) + ) + for climate_id in gateway.api.sensors.thermostat: + async_add_climate(EventType.ADDED, climate_id) - for sensor in sensors: - - if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): - continue - - if ( - isinstance(sensor, Thermostat) - and sensor.unique_id not in gateway.entities[DOMAIN] - ): - entities.append(DeconzThermostat(sensor, gateway)) - - if entities: - async_add_entities(entities) + @callback + def async_reload_clip_sensors() -> None: + """Load clip climate sensors from deCONZ.""" + for climate_id, climate in gateway.api.sensors.thermostat.items(): + if climate.type.startswith("CLIP"): + async_add_climate(EventType.ADDED, climate_id) config_entry.async_on_unload( async_dispatcher_connect( hass, - gateway.signal_new_sensor, - async_add_climate, + gateway.signal_reload_clip_sensors, + async_reload_clip_sensors, ) ) - async_add_climate() - class DeconzThermostat(DeconzDevice, ClimateEntity): """Representation of a deCONZ thermostat.""" diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index f54cd4076d3..0e880fa22c1 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -63,6 +63,7 @@ class DeconzGateway: self.signal_reachable = f"deconz-reachable-{config_entry.entry_id}" self.signal_reload_groups = f"deconz_reload_group_{config_entry.entry_id}" + self.signal_reload_clip_sensors = f"deconz_reload_clip_{config_entry.entry_id}" self.signal_new_light = f"deconz_new_light_{config_entry.entry_id}" self.signal_new_sensor = f"deconz_new_sensor_{config_entry.entry_id}" @@ -212,6 +213,7 @@ class DeconzGateway: if self.option_allow_clip_sensor: self.async_add_device_callback(ResourceGroup.SENSOR.value) + async_dispatcher_send(self.hass, self.signal_reload_clip_sensors) else: deconz_ids += [ From 2e568771a92f27c4e97c3bbcf2c09ebdb69a3cfc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 13 May 2022 10:16:56 +0200 Subject: [PATCH 0453/3516] Update coverage to 6.3.3 (#71772) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index b6fb5272abd..49615d104a7 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.3.2 +coverage==6.3.3 freezegun==1.2.1 mock-open==1.4.0 mypy==0.950 From e6d7170fd8c227342cd217c89c997cd957ad0264 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 13 May 2022 10:18:16 +0200 Subject: [PATCH 0454/3516] Remove deprecated WLED update button (#71775) --- homeassistant/components/wled/button.py | 73 +------------- tests/components/wled/test_button.py | 127 +----------------------- 2 files changed, 3 insertions(+), 197 deletions(-) diff --git a/homeassistant/components/wled/button.py b/homeassistant/components/wled/button.py index 2967067ef44..97877053163 100644 --- a/homeassistant/components/wled/button.py +++ b/homeassistant/components/wled/button.py @@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, LOGGER +from .const import DOMAIN from .coordinator import WLEDDataUpdateCoordinator from .helpers import wled_exception_handler from .models import WLEDEntity @@ -20,12 +20,7 @@ async def async_setup_entry( ) -> None: """Set up WLED button based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - [ - WLEDRestartButton(coordinator), - WLEDUpdateButton(coordinator), - ] - ) + async_add_entities([WLEDRestartButton(coordinator)]) class WLEDRestartButton(WLEDEntity, ButtonEntity): @@ -44,67 +39,3 @@ class WLEDRestartButton(WLEDEntity, ButtonEntity): async def async_press(self) -> None: """Send out a restart command.""" await self.coordinator.wled.reset() - - -class WLEDUpdateButton(WLEDEntity, ButtonEntity): - """Defines a WLED update button.""" - - _attr_device_class = ButtonDeviceClass.UPDATE - _attr_entity_category = EntityCategory.CONFIG - - # Disabled by default, as this entity is deprecated. - _attr_entity_registry_enabled_default = False - - def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: - """Initialize the button entity.""" - super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Update" - self._attr_unique_id = f"{coordinator.data.info.mac_address}_update" - - @property - def available(self) -> bool: - """Return if the entity and an update is available.""" - current = self.coordinator.data.info.version - beta = self.coordinator.data.info.version_latest_beta - stable = self.coordinator.data.info.version_latest_stable - - # If we already run a pre-release, allow upgrading to a newer - # pre-release offer a normal upgrade otherwise. - return ( - super().available - and current is not None - and ( - (stable is not None and stable > current) - or ( - beta is not None - and (current.alpha or current.beta or current.release_candidate) - and beta > current - ) - ) - ) - - @wled_exception_handler - async def async_press(self) -> None: - """Send out a update command.""" - LOGGER.warning( - "The WLED update button '%s' is deprecated, please " - "use the new update entity as a replacement", - self.entity_id, - ) - current = self.coordinator.data.info.version - beta = self.coordinator.data.info.version_latest_beta - stable = self.coordinator.data.info.version_latest_stable - - # If we already run a pre-release, allow update to a newer - # pre-release or newer stable, otherwise, offer a normal stable updates. - version = stable - if ( - current is not None - and beta is not None - and (current.alpha or current.beta or current.release_candidate) - and beta > current - and beta > stable - ): - version = beta - - await self.coordinator.wled.upgrade(version=str(version)) diff --git a/tests/components/wled/test_button.py b/tests/components/wled/test_button.py index a09c7e2aaa3..3cfa4f762ad 100644 --- a/tests/components/wled/test_button.py +++ b/tests/components/wled/test_button.py @@ -1,5 +1,5 @@ """Tests for the WLED button platform.""" -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import MagicMock from freezegun import freeze_time import pytest @@ -95,128 +95,3 @@ async def test_button_connection_error( assert state assert state.state == STATE_UNAVAILABLE assert "Error communicating with API" in caplog.text - - -async def test_button_update_stay_stable( - hass: HomeAssistant, - entity_registry_enabled_by_default: AsyncMock, - init_integration: MockConfigEntry, - mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test the update button. - - There is both an update for beta and stable available, however, the device - is currently running a stable version. Therefore, the update button should - update the the next stable (even though beta is newer). - """ - entity_registry = er.async_get(hass) - - entry = entity_registry.async_get("button.wled_rgb_light_update") - assert entry - assert entry.unique_id == "aabbccddeeff_update" - assert entry.entity_category is EntityCategory.CONFIG - - state = hass.states.get("button.wled_rgb_light_update") - assert state - assert state.state == STATE_UNKNOWN - assert state.attributes[ATTR_DEVICE_CLASS] == ButtonDeviceClass.UPDATE - - await hass.services.async_call( - BUTTON_DOMAIN, - SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.wled_rgb_light_update"}, - blocking=True, - ) - await hass.async_block_till_done() - assert mock_wled.upgrade.call_count == 1 - mock_wled.upgrade.assert_called_with(version="0.12.0") - assert ( - "The WLED update button 'button.wled_rgb_light_update' is deprecated" - in caplog.text - ) - - -@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True) -async def test_button_update_beta_to_stable( - hass: HomeAssistant, - entity_registry_enabled_by_default: AsyncMock, - init_integration: MockConfigEntry, - mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test the update button. - - There is both an update for beta and stable available the device - is currently a beta, however, a newer stable is available. Therefore, the - update button should update to the next stable. - """ - await hass.services.async_call( - BUTTON_DOMAIN, - SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.wled_rgbw_light_update"}, - blocking=True, - ) - await hass.async_block_till_done() - assert mock_wled.upgrade.call_count == 1 - mock_wled.upgrade.assert_called_with(version="0.8.6") - assert ( - "The WLED update button 'button.wled_rgbw_light_update' is deprecated" - in caplog.text - ) - - -@pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True) -async def test_button_update_stay_beta( - hass: HomeAssistant, - entity_registry_enabled_by_default: AsyncMock, - init_integration: MockConfigEntry, - mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test the update button. - - There is an update for beta and the device is currently a beta. Therefore, - the update button should update to the next beta. - """ - await hass.services.async_call( - BUTTON_DOMAIN, - SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.wled_rgb_light_update"}, - blocking=True, - ) - await hass.async_block_till_done() - assert mock_wled.upgrade.call_count == 1 - mock_wled.upgrade.assert_called_with(version="0.8.6b2") - assert ( - "The WLED update button 'button.wled_rgb_light_update' is deprecated" - in caplog.text - ) - - -@pytest.mark.parametrize("mock_wled", ["wled/rgb_websocket.json"], indirect=True) -async def test_button_no_update_available( - hass: HomeAssistant, - entity_registry_enabled_by_default: AsyncMock, - init_integration: MockConfigEntry, - mock_wled: MagicMock, -) -> None: - """Test the update button. There is no update available.""" - state = hass.states.get("button.wled_websocket_update") - assert state - assert state.state == STATE_UNAVAILABLE - - -async def test_disabled_by_default( - hass: HomeAssistant, init_integration: MockConfigEntry -) -> None: - """Test that the update button is disabled by default.""" - registry = er.async_get(hass) - - state = hass.states.get("button.wled_rgb_light_update") - assert state is None - - entry = registry.async_get("button.wled_rgb_light_update") - assert entry - assert entry.disabled - assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION From 3d05a9d31fcb39a55f73e0c2388d999f24750275 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 13 May 2022 10:38:36 +0200 Subject: [PATCH 0455/3516] Streamline setup of deCONZ lock from sensor platform (#71707) --- homeassistant/components/deconz/lock.py | 29 ++++++------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index 1ecc1802b74..5b8c9d87e67 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -11,7 +11,6 @@ from pydeconz.models.sensor.door_lock import DoorLock from homeassistant.components.lock import DOMAIN, LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .deconz_device import DeconzDevice @@ -43,33 +42,19 @@ async def async_setup_entry( async_add_lock_from_light(EventType.ADDED, lock_id) @callback - def async_add_lock_from_sensor(sensors: list[DoorLock] | None = None) -> None: + def async_add_lock_from_sensor(_: EventType, lock_id: str) -> None: """Add lock from deCONZ.""" - entities = [] - - if sensors is None: - sensors = list(gateway.api.sensors.door_lock.values()) - - for sensor in sensors: - - if ( - isinstance(sensor, DoorLock) - and sensor.unique_id not in gateway.entities[DOMAIN] - ): - entities.append(DeconzLock(sensor, gateway)) - - if entities: - async_add_entities(entities) + lock = gateway.api.sensors.door_lock[lock_id] + async_add_entities([DeconzLock(lock, gateway)]) config_entry.async_on_unload( - async_dispatcher_connect( - hass, - gateway.signal_new_sensor, + gateway.api.sensors.door_lock.subscribe( async_add_lock_from_sensor, + EventType.ADDED, ) ) - - async_add_lock_from_sensor() + for lock_id in gateway.api.sensors.door_lock: + async_add_lock_from_sensor(EventType.ADDED, lock_id) class DeconzLock(DeconzDevice, LockEntity): From d3c25bf4507e166ee98d283598c63b26afec64b4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 13 May 2022 10:40:14 +0200 Subject: [PATCH 0456/3516] Adjust pylint plugin for climate HVACAction (#70760) --- pylint/plugins/hass_imports.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index ae9961b49dd..afbd930ec30 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -68,6 +68,10 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { ), ], "homeassistant.components.climate.const": [ + ObsoleteImportMatch( + reason="replaced by HVACAction enum", + constant=re.compile(r"^CURRENT_HVAC_(\w*)$"), + ), ObsoleteImportMatch( reason="replaced by ClimateEntityFeature enum", constant=re.compile(r"^SUPPORT_(\w*)$"), From 3adaad73816a7df12b18c13fbf4de9aab2a85142 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 13 May 2022 11:40:22 +0200 Subject: [PATCH 0457/3516] Migrate limitlessled light to color_mode (#69430) --- .../components/limitlessled/light.py | 86 +++++++++---------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/limitlessled/light.py b/homeassistant/components/limitlessled/light.py index 15b814775e6..41668470dfb 100644 --- a/homeassistant/components/limitlessled/light.py +++ b/homeassistant/components/limitlessled/light.py @@ -24,9 +24,7 @@ from homeassistant.components.light import ( EFFECT_WHITE, FLASH_LONG, PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + ColorMode, LightEntity, LightEntityFeature, ) @@ -60,27 +58,17 @@ MIN_SATURATION = 10 WHITE = [0, 0] -SUPPORT_LIMITLESSLED_WHITE = ( - SUPPORT_BRIGHTNESS - | SUPPORT_COLOR_TEMP - | LightEntityFeature.EFFECT - | LightEntityFeature.TRANSITION -) -SUPPORT_LIMITLESSLED_DIMMER = SUPPORT_BRIGHTNESS | LightEntityFeature.TRANSITION +COLOR_MODES_LIMITLESS_WHITE = {ColorMode.COLOR_TEMP} +SUPPORT_LIMITLESSLED_WHITE = LightEntityFeature.EFFECT | LightEntityFeature.TRANSITION +COLOR_MODES_LIMITLESS_DIMMER = {ColorMode.BRIGHTNESS} +SUPPORT_LIMITLESSLED_DIMMER = LightEntityFeature.TRANSITION +COLOR_MODES_LIMITLESS_RGB = {ColorMode.HS} SUPPORT_LIMITLESSLED_RGB = ( - SUPPORT_BRIGHTNESS - | LightEntityFeature.EFFECT - | LightEntityFeature.FLASH - | SUPPORT_COLOR - | LightEntityFeature.TRANSITION + LightEntityFeature.EFFECT | LightEntityFeature.FLASH | LightEntityFeature.TRANSITION ) +COLOR_MODES_LIMITLESS_RGBWW = {ColorMode.COLOR_TEMP, ColorMode.HS} SUPPORT_LIMITLESSLED_RGBWW = ( - SUPPORT_BRIGHTNESS - | SUPPORT_COLOR_TEMP - | LightEntityFeature.EFFECT - | LightEntityFeature.FLASH - | SUPPORT_COLOR - | LightEntityFeature.TRANSITION + LightEntityFeature.EFFECT | LightEntityFeature.FLASH | LightEntityFeature.TRANSITION ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -219,18 +207,31 @@ class LimitlessLEDGroup(LightEntity, RestoreEntity): """Initialize a group.""" if isinstance(group, WhiteGroup): - self._supported = SUPPORT_LIMITLESSLED_WHITE + self._attr_supported_color_modes = COLOR_MODES_LIMITLESS_WHITE + self._attr_supported_features = SUPPORT_LIMITLESSLED_WHITE self._effect_list = [EFFECT_NIGHT] elif isinstance(group, DimmerGroup): - self._supported = SUPPORT_LIMITLESSLED_DIMMER + self._attr_supported_color_modes = COLOR_MODES_LIMITLESS_DIMMER + self._attr_supported_features = SUPPORT_LIMITLESSLED_DIMMER self._effect_list = [] elif isinstance(group, RgbwGroup): - self._supported = SUPPORT_LIMITLESSLED_RGB + self._attr_supported_color_modes = COLOR_MODES_LIMITLESS_RGB + self._attr_supported_features = SUPPORT_LIMITLESSLED_RGB self._effect_list = [EFFECT_COLORLOOP, EFFECT_NIGHT, EFFECT_WHITE] elif isinstance(group, RgbwwGroup): - self._supported = SUPPORT_LIMITLESSLED_RGBWW + self._attr_supported_color_modes = COLOR_MODES_LIMITLESS_RGBWW + self._attr_supported_features = SUPPORT_LIMITLESSLED_RGBWW self._effect_list = [EFFECT_COLORLOOP, EFFECT_NIGHT, EFFECT_WHITE] + self._fixed_color_mode = None + if len(self._attr_supported_color_modes) == 1: + self._fixed_color_mode = next(iter(self._attr_supported_color_modes)) + else: + assert self._attr_supported_color_modes == { + ColorMode.COLOR_TEMP, + ColorMode.HS, + } + self.group = group self.config = config self._is_on = False @@ -286,29 +287,27 @@ class LimitlessLEDGroup(LightEntity, RestoreEntity): """Return the warmest color_temp that this light supports.""" return 370 + @property + def color_mode(self) -> str | None: + """Return the color mode of the light.""" + if self._fixed_color_mode: + return self._fixed_color_mode + + # The light supports both hs and white with adjustable color temperature + if self._effect == EFFECT_NIGHT or self._color is None or self._color[1] == 0: + return ColorMode.COLOR_TEMP + return ColorMode.HS + @property def color_temp(self): """Return the temperature property.""" - if self.hs_color is not None: - return None return self._temperature @property def hs_color(self): """Return the color property.""" - if self._effect == EFFECT_NIGHT: - return None - - if self._color is None or self._color[1] == 0: - return None - return self._color - @property - def supported_features(self): - """Flag supported features.""" - return self._supported - @property def effect(self): """Return the current effect for this light.""" @@ -349,7 +348,7 @@ class LimitlessLEDGroup(LightEntity, RestoreEntity): self._brightness = kwargs[ATTR_BRIGHTNESS] args["brightness"] = self.limitlessled_brightness() - if ATTR_HS_COLOR in kwargs and self._supported & SUPPORT_COLOR: + if ATTR_HS_COLOR in kwargs: self._color = kwargs[ATTR_HS_COLOR] # White is a special case. if self._color[1] < MIN_SATURATION: @@ -359,18 +358,17 @@ class LimitlessLEDGroup(LightEntity, RestoreEntity): args["color"] = self.limitlessled_color() if ATTR_COLOR_TEMP in kwargs: - if self._supported & SUPPORT_COLOR: + if ColorMode.HS in self.supported_color_modes: pipeline.white() self._color = WHITE - if self._supported & SUPPORT_COLOR_TEMP: - self._temperature = kwargs[ATTR_COLOR_TEMP] - args["temperature"] = self.limitlessled_temperature() + self._temperature = kwargs[ATTR_COLOR_TEMP] + args["temperature"] = self.limitlessled_temperature() if args: pipeline.transition(transition_time, **args) # Flash. - if ATTR_FLASH in kwargs and self._supported & LightEntityFeature.FLASH: + if ATTR_FLASH in kwargs and self.supported_features & LightEntityFeature.FLASH: duration = 0 if kwargs[ATTR_FLASH] == FLASH_LONG: duration = 1 From c8d171c4754efa0c08ae2df8f43f1ac283bc660f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 13 May 2022 12:13:26 +0200 Subject: [PATCH 0458/3516] Remove deprecated Raspberry Pi GPIO integration (#71777) --- .coveragerc | 1 - .github/workflows/wheels.yml | 1 - homeassistant/components/rpi_gpio/__init__.py | 68 --------- .../components/rpi_gpio/binary_sensor.py | 109 -------------- homeassistant/components/rpi_gpio/cover.py | 139 ------------------ .../components/rpi_gpio/manifest.json | 9 -- .../components/rpi_gpio/services.yaml | 3 - homeassistant/components/rpi_gpio/switch.py | 88 ----------- machine/raspberrypi | 6 +- machine/raspberrypi2 | 6 +- machine/raspberrypi3 | 3 +- machine/raspberrypi3-64 | 3 +- machine/raspberrypi4 | 3 +- machine/raspberrypi4-64 | 3 +- requirements_all.txt | 3 - script/gen_requirements_all.py | 1 - 16 files changed, 6 insertions(+), 440 deletions(-) delete mode 100644 homeassistant/components/rpi_gpio/__init__.py delete mode 100644 homeassistant/components/rpi_gpio/binary_sensor.py delete mode 100644 homeassistant/components/rpi_gpio/cover.py delete mode 100644 homeassistant/components/rpi_gpio/manifest.json delete mode 100644 homeassistant/components/rpi_gpio/services.yaml delete mode 100644 homeassistant/components/rpi_gpio/switch.py diff --git a/.coveragerc b/.coveragerc index 922c13e550c..ef7cd7b847c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -993,7 +993,6 @@ omit = homeassistant/components/route53/* homeassistant/components/rova/sensor.py homeassistant/components/rpi_camera/* - homeassistant/components/rpi_gpio/* homeassistant/components/rtorrent/sensor.py homeassistant/components/russound_rio/media_player.py homeassistant/components/russound_rnet/media_player.py diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e43c397f3bc..70b6534df5e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -134,7 +134,6 @@ jobs: sed -i "s|# pybluez|pybluez|g" ${requirement_file} sed -i "s|# bluepy|bluepy|g" ${requirement_file} sed -i "s|# beacontools|beacontools|g" ${requirement_file} - sed -i "s|# RPi.GPIO|RPi.GPIO|g" ${requirement_file} sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file} sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file} sed -i "s|# evdev|evdev|g" ${requirement_file} diff --git a/homeassistant/components/rpi_gpio/__init__.py b/homeassistant/components/rpi_gpio/__init__.py deleted file mode 100644 index 95e3ded1c64..00000000000 --- a/homeassistant/components/rpi_gpio/__init__.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Support for controlling GPIO pins of a Raspberry Pi.""" -import logging - -from RPi import GPIO # pylint: disable=import-error - -from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, - Platform, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType - -DOMAIN = "rpi_gpio" -PLATFORMS = [ - Platform.BINARY_SENSOR, - Platform.COVER, - Platform.SWITCH, -] - -_LOGGER = logging.getLogger(__name__) - - -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Raspberry PI GPIO component.""" - _LOGGER.warning( - "The Raspberry Pi GPIO integration is deprecated and will be removed " - "in Home Assistant Core 2022.6; this integration is removed under " - "Architectural Decision Record 0019, more information can be found here: " - "https://github.com/home-assistant/architecture/blob/master/adr/0019-GPIO.md" - ) - - def cleanup_gpio(event): - """Stuff to do before stopping.""" - GPIO.cleanup() - - def prepare_gpio(event): - """Stuff to do when Home Assistant starts.""" - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) - - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) - GPIO.setmode(GPIO.BCM) - return True - - -def setup_output(port): - """Set up a GPIO as output.""" - GPIO.setup(port, GPIO.OUT) - - -def setup_input(port, pull_mode): - """Set up a GPIO as input.""" - GPIO.setup(port, GPIO.IN, GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP) - - -def write_output(port, value): - """Write a value to a GPIO.""" - GPIO.output(port, value) - - -def read_input(port): - """Read a value from a GPIO.""" - return GPIO.input(port) - - -def edge_detect(port, event_callback, bounce): - """Add detection for RISING and FALLING events.""" - GPIO.add_event_detect(port, GPIO.BOTH, callback=event_callback, bouncetime=bounce) diff --git a/homeassistant/components/rpi_gpio/binary_sensor.py b/homeassistant/components/rpi_gpio/binary_sensor.py deleted file mode 100644 index e183b463e45..00000000000 --- a/homeassistant/components/rpi_gpio/binary_sensor.py +++ /dev/null @@ -1,109 +0,0 @@ -"""Support for binary sensor using RPi GPIO.""" -from __future__ import annotations - -import asyncio - -import voluptuous as vol - -from homeassistant.components import rpi_gpio -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorEntity -from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import setup_reload_service -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import DOMAIN, PLATFORMS - -CONF_BOUNCETIME = "bouncetime" -CONF_INVERT_LOGIC = "invert_logic" -CONF_PORTS = "ports" -CONF_PULL_MODE = "pull_mode" - -DEFAULT_BOUNCETIME = 50 -DEFAULT_INVERT_LOGIC = False -DEFAULT_PULL_MODE = "UP" - -_SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_PORTS): _SENSORS_SCHEMA, - vol.Optional(CONF_BOUNCETIME, default=DEFAULT_BOUNCETIME): cv.positive_int, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - vol.Optional(CONF_PULL_MODE, default=DEFAULT_PULL_MODE): cv.string, - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Raspberry PI GPIO devices.""" - setup_reload_service(hass, DOMAIN, PLATFORMS) - - pull_mode = config[CONF_PULL_MODE] - bouncetime = config[CONF_BOUNCETIME] - invert_logic = config[CONF_INVERT_LOGIC] - - binary_sensors = [] - ports = config[CONF_PORTS] - for port_num, port_name in ports.items(): - binary_sensors.append( - RPiGPIOBinarySensor( - port_name, port_num, pull_mode, bouncetime, invert_logic - ) - ) - add_entities(binary_sensors, True) - - -class RPiGPIOBinarySensor(BinarySensorEntity): - """Represent a binary sensor that uses Raspberry Pi GPIO.""" - - async def async_read_gpio(self): - """Read state from GPIO.""" - await asyncio.sleep(float(self._bouncetime) / 1000) - self._state = await self.hass.async_add_executor_job( - rpi_gpio.read_input, self._port - ) - self.async_write_ha_state() - - def __init__(self, name, port, pull_mode, bouncetime, invert_logic): - """Initialize the RPi binary sensor.""" - self._name = name or DEVICE_DEFAULT_NAME - self._port = port - self._pull_mode = pull_mode - self._bouncetime = bouncetime - self._invert_logic = invert_logic - self._state = None - - rpi_gpio.setup_input(self._port, self._pull_mode) - - def edge_detected(port): - """Edge detection handler.""" - self.hass.add_job(self.async_read_gpio) - - rpi_gpio.edge_detect(self._port, edge_detected, self._bouncetime) - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): - """Return the state of the entity.""" - return self._state != self._invert_logic - - def update(self): - """Update the GPIO state.""" - self._state = rpi_gpio.read_input(self._port) diff --git a/homeassistant/components/rpi_gpio/cover.py b/homeassistant/components/rpi_gpio/cover.py deleted file mode 100644 index e4b07d3c577..00000000000 --- a/homeassistant/components/rpi_gpio/cover.py +++ /dev/null @@ -1,139 +0,0 @@ -"""Support for controlling a Raspberry Pi cover.""" -from __future__ import annotations - -from time import sleep - -import voluptuous as vol - -from homeassistant.components import rpi_gpio -from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity -from homeassistant.const import CONF_COVERS, CONF_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import setup_reload_service -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import DOMAIN, PLATFORMS - -CONF_RELAY_PIN = "relay_pin" -CONF_RELAY_TIME = "relay_time" -CONF_STATE_PIN = "state_pin" -CONF_STATE_PULL_MODE = "state_pull_mode" -CONF_INVERT_STATE = "invert_state" -CONF_INVERT_RELAY = "invert_relay" - -DEFAULT_RELAY_TIME = 0.2 -DEFAULT_STATE_PULL_MODE = "UP" -DEFAULT_INVERT_STATE = False -DEFAULT_INVERT_RELAY = False -_COVERS_SCHEMA = vol.All( - cv.ensure_list, - [ - vol.Schema( - { - CONF_NAME: cv.string, - CONF_RELAY_PIN: cv.positive_int, - CONF_STATE_PIN: cv.positive_int, - } - ) - ], -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_COVERS): _COVERS_SCHEMA, - vol.Optional(CONF_STATE_PULL_MODE, default=DEFAULT_STATE_PULL_MODE): cv.string, - vol.Optional(CONF_RELAY_TIME, default=DEFAULT_RELAY_TIME): cv.positive_int, - vol.Optional(CONF_INVERT_STATE, default=DEFAULT_INVERT_STATE): cv.boolean, - vol.Optional(CONF_INVERT_RELAY, default=DEFAULT_INVERT_RELAY): cv.boolean, - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the RPi cover platform.""" - setup_reload_service(hass, DOMAIN, PLATFORMS) - - relay_time = config[CONF_RELAY_TIME] - state_pull_mode = config[CONF_STATE_PULL_MODE] - invert_state = config[CONF_INVERT_STATE] - invert_relay = config[CONF_INVERT_RELAY] - covers = [] - covers_conf = config[CONF_COVERS] - - for cover in covers_conf: - covers.append( - RPiGPIOCover( - cover[CONF_NAME], - cover[CONF_RELAY_PIN], - cover[CONF_STATE_PIN], - state_pull_mode, - relay_time, - invert_state, - invert_relay, - ) - ) - add_entities(covers) - - -class RPiGPIOCover(CoverEntity): - """Representation of a Raspberry GPIO cover.""" - - def __init__( - self, - name, - relay_pin, - state_pin, - state_pull_mode, - relay_time, - invert_state, - invert_relay, - ): - """Initialize the cover.""" - self._name = name - self._state = False - self._relay_pin = relay_pin - self._state_pin = state_pin - self._state_pull_mode = state_pull_mode - self._relay_time = relay_time - self._invert_state = invert_state - self._invert_relay = invert_relay - rpi_gpio.setup_output(self._relay_pin) - rpi_gpio.setup_input(self._state_pin, self._state_pull_mode) - rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1) - - @property - def name(self): - """Return the name of the cover if any.""" - return self._name - - def update(self): - """Update the state of the cover.""" - self._state = rpi_gpio.read_input(self._state_pin) - - @property - def is_closed(self): - """Return true if cover is closed.""" - return self._state != self._invert_state - - def _trigger(self): - """Trigger the cover.""" - rpi_gpio.write_output(self._relay_pin, 1 if self._invert_relay else 0) - sleep(self._relay_time) - rpi_gpio.write_output(self._relay_pin, 0 if self._invert_relay else 1) - - def close_cover(self, **kwargs): - """Close the cover.""" - if not self.is_closed: - self._trigger() - - def open_cover(self, **kwargs): - """Open the cover.""" - if self.is_closed: - self._trigger() diff --git a/homeassistant/components/rpi_gpio/manifest.json b/homeassistant/components/rpi_gpio/manifest.json deleted file mode 100644 index f8db41b1a31..00000000000 --- a/homeassistant/components/rpi_gpio/manifest.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "domain": "rpi_gpio", - "name": "Raspberry Pi GPIO", - "documentation": "https://www.home-assistant.io/integrations/rpi_gpio", - "requirements": ["RPi.GPIO==0.7.1a4"], - "codeowners": [], - "iot_class": "local_push", - "loggers": ["RPi"] -} diff --git a/homeassistant/components/rpi_gpio/services.yaml b/homeassistant/components/rpi_gpio/services.yaml deleted file mode 100644 index 1858c5a9fa2..00000000000 --- a/homeassistant/components/rpi_gpio/services.yaml +++ /dev/null @@ -1,3 +0,0 @@ -reload: - name: Reload - description: Reload all rpi_gpio entities. diff --git a/homeassistant/components/rpi_gpio/switch.py b/homeassistant/components/rpi_gpio/switch.py deleted file mode 100644 index 040edd912c5..00000000000 --- a/homeassistant/components/rpi_gpio/switch.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Allows to configure a switch using RPi GPIO.""" -from __future__ import annotations - -import voluptuous as vol - -from homeassistant.components import rpi_gpio -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import setup_reload_service -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import DOMAIN, PLATFORMS - -CONF_PULL_MODE = "pull_mode" -CONF_PORTS = "ports" -CONF_INVERT_LOGIC = "invert_logic" - -DEFAULT_INVERT_LOGIC = False - -_SWITCHES_SCHEMA = vol.Schema({cv.positive_int: cv.string}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_PORTS): _SWITCHES_SCHEMA, - vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Raspberry PI GPIO devices.""" - setup_reload_service(hass, DOMAIN, PLATFORMS) - - invert_logic = config[CONF_INVERT_LOGIC] - - switches = [] - ports = config[CONF_PORTS] - for port, name in ports.items(): - switches.append(RPiGPIOSwitch(name, port, invert_logic)) - add_entities(switches) - - -class RPiGPIOSwitch(SwitchEntity): - """Representation of a Raspberry Pi GPIO.""" - - def __init__(self, name, port, invert_logic): - """Initialize the pin.""" - self._name = name or DEVICE_DEFAULT_NAME - self._port = port - self._invert_logic = invert_logic - self._state = False - rpi_gpio.setup_output(self._port) - rpi_gpio.write_output(self._port, 1 if self._invert_logic else 0) - - @property - def name(self): - """Return the name of the switch.""" - return self._name - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - def turn_on(self, **kwargs): - """Turn the device on.""" - rpi_gpio.write_output(self._port, 0 if self._invert_logic else 1) - self._state = True - self.schedule_update_ha_state() - - def turn_off(self, **kwargs): - """Turn the device off.""" - rpi_gpio.write_output(self._port, 1 if self._invert_logic else 0) - self._state = False - self.schedule_update_ha_state() diff --git a/machine/raspberrypi b/machine/raspberrypi index 960e343792d..8ea2d08bba7 100644 --- a/machine/raspberrypi +++ b/machine/raspberrypi @@ -4,11 +4,7 @@ FROM homeassistant/armhf-homeassistant:$BUILD_VERSION RUN apk --no-cache add \ raspberrypi \ raspberrypi-libs \ - usbutils \ - && sed -i "s|# RPi.GPIO|RPi.GPIO|g" /usr/src/homeassistant/requirements_all.txt \ - && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - RPi.GPIO -c /usr/src/homeassistant/requirements_all.txt \ - --use-deprecated=legacy-resolver + usbutils ## # Set symlinks for raspberry pi camera binaries. diff --git a/machine/raspberrypi2 b/machine/raspberrypi2 index 225c45423a1..45f7a9c80d1 100644 --- a/machine/raspberrypi2 +++ b/machine/raspberrypi2 @@ -4,11 +4,7 @@ FROM homeassistant/armv7-homeassistant:$BUILD_VERSION RUN apk --no-cache add \ raspberrypi \ raspberrypi-libs \ - usbutils \ - && sed -i "s|# RPi.GPIO|RPi.GPIO|g" /usr/src/homeassistant/requirements_all.txt \ - && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - RPi.GPIO -c /usr/src/homeassistant/requirements_all.txt \ - --use-deprecated=legacy-resolver + usbutils ## # Set symlinks for raspberry pi binaries. diff --git a/machine/raspberrypi3 b/machine/raspberrypi3 index 6315cc3e885..9985b4a3b7a 100644 --- a/machine/raspberrypi3 +++ b/machine/raspberrypi3 @@ -5,9 +5,8 @@ RUN apk --no-cache add \ raspberrypi \ raspberrypi-libs \ usbutils \ - && sed -i "s|# RPi.GPIO|RPi.GPIO|g" /usr/src/homeassistant/requirements_all.txt \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - RPi.GPIO bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt \ + bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt \ --use-deprecated=legacy-resolver ## diff --git a/machine/raspberrypi3-64 b/machine/raspberrypi3-64 index 51f41d68320..35c6eec77de 100644 --- a/machine/raspberrypi3-64 +++ b/machine/raspberrypi3-64 @@ -5,9 +5,8 @@ RUN apk --no-cache add \ raspberrypi \ raspberrypi-libs \ usbutils \ - && sed -i "s|# RPi.GPIO|RPi.GPIO|g" /usr/src/homeassistant/requirements_all.txt \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - RPi.GPIO bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt \ + bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt \ --use-deprecated=legacy-resolver ## diff --git a/machine/raspberrypi4 b/machine/raspberrypi4 index 6315cc3e885..9985b4a3b7a 100644 --- a/machine/raspberrypi4 +++ b/machine/raspberrypi4 @@ -5,9 +5,8 @@ RUN apk --no-cache add \ raspberrypi \ raspberrypi-libs \ usbutils \ - && sed -i "s|# RPi.GPIO|RPi.GPIO|g" /usr/src/homeassistant/requirements_all.txt \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - RPi.GPIO bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt \ + bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt \ --use-deprecated=legacy-resolver ## diff --git a/machine/raspberrypi4-64 b/machine/raspberrypi4-64 index 51f41d68320..35c6eec77de 100644 --- a/machine/raspberrypi4-64 +++ b/machine/raspberrypi4-64 @@ -5,9 +5,8 @@ RUN apk --no-cache add \ raspberrypi \ raspberrypi-libs \ usbutils \ - && sed -i "s|# RPi.GPIO|RPi.GPIO|g" /usr/src/homeassistant/requirements_all.txt \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - RPi.GPIO bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt \ + bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt \ --use-deprecated=legacy-resolver ## diff --git a/requirements_all.txt b/requirements_all.txt index e0c864fea9d..b572dca4f6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -49,9 +49,6 @@ PyViCare==2.16.1 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 -# homeassistant.components.rpi_gpio -# RPi.GPIO==0.7.1a4 - # homeassistant.components.remember_the_milk RtmAPI==0.7.2 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 1e93b8bba35..be94fdac22b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -32,7 +32,6 @@ COMMENT_REQUIREMENTS = ( "python-gammu", "python-lirc", "pyuserinput", - "RPi.GPIO", "tensorflow", "tf-models-official", ) From f301de98e47bc45548f41c0a01d172d5d2a34f64 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Fri, 13 May 2022 12:15:49 +0200 Subject: [PATCH 0459/3516] Add deprecation warning to Somfy integration (#71653) --- homeassistant/components/somfy/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index ae12c4ea266..ed6c58bc0a0 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -77,6 +77,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Somfy from a config entry.""" + + _LOGGER.warning( + "The Somfy integration is deprecated and will be removed " + "in Home Assistant Core 2022.7; due to the Somfy Open API deprecation." + "The Somfy Open API will shutdown June 21st 2022, migrate to the " + "Overkiz integration to control your Somfy devices" + ) + # Backwards compat if "auth_implementation" not in entry.data: hass.config_entries.async_update_entry( From dba2f5ab1c5cd67b98dbfce27939115018f503c3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 13 May 2022 12:17:40 +0200 Subject: [PATCH 0460/3516] Support this variable in template alarm actions (#71744) --- .../template/alarm_control_panel.py | 4 +++- .../components/template/template_entity.py | 21 ++++++++++++++++++- .../template/test_alarm_control_panel.py | 9 ++++---- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 9d81dce28fe..a8a88c57bd3 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -230,7 +230,9 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): self._state = state optimistic_set = True - await script.async_run({ATTR_CODE: code}, context=self._context) + await self.async_run_script( + script, run_variables={ATTR_CODE: code}, context=self._context + ) if optimistic_set: self.async_write_ha_state() diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index a6d1cba78e1..6e0b7f6f48f 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -19,7 +19,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, ) -from homeassistant.core import CoreState, Event, State, callback +from homeassistant.core import Context, CoreState, Event, State, callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -28,6 +28,7 @@ from homeassistant.helpers.event import ( TrackTemplateResult, async_track_template_result, ) +from homeassistant.helpers.script import Script, _VarsType from homeassistant.helpers.template import ( Template, TemplateStateFromEntityId, @@ -455,3 +456,21 @@ class TemplateEntity(Entity): async def async_update(self) -> None: """Call for forced update.""" self._async_update() + + async def async_run_script( + self, + script: Script, + *, + run_variables: _VarsType | None = None, + context: Context | None = None, + ) -> None: + """Run an action script.""" + if run_variables is None: + run_variables = {} + return await script.async_run( + run_variables={ + "this": TemplateStateFromEntityId(self.hass, self.entity_id), + **run_variables, + }, + context=context, + ) diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index 9ec93073911..8f9ab39f7c0 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -44,22 +44,22 @@ OPTIMISTIC_TEMPLATE_ALARM_CONFIG = { "arm_away": { "service": "alarm_control_panel.alarm_arm_away", "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, + "data": {"code": "{{ this.entity_id }}"}, }, "arm_home": { "service": "alarm_control_panel.alarm_arm_home", "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, + "data": {"code": "{{ this.entity_id }}"}, }, "arm_night": { "service": "alarm_control_panel.alarm_arm_night", "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, + "data": {"code": "{{ this.entity_id }}"}, }, "disarm": { "service": "alarm_control_panel.alarm_disarm", "entity_id": "alarm_control_panel.test", - "data": {"code": "1234"}, + "data": {"code": "{{ this.entity_id }}"}, }, } @@ -261,6 +261,7 @@ async def test_actions(hass, service, start_ha, service_calls): await hass.async_block_till_done() assert len(service_calls) == 1 assert service_calls[0].data["service"] == service + assert service_calls[0].data["service_data"]["code"] == TEMPLATE_NAME @pytest.mark.parametrize("count,domain", [(1, "alarm_control_panel")]) From 6cff2f8571005ac795540839a69ba6ef1446f863 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 13 May 2022 12:22:34 +0200 Subject: [PATCH 0461/3516] Tweak template light tests (#71729) --- tests/components/template/conftest.py | 14 +- tests/components/template/test_light.py | 1491 +++++++++-------------- 2 files changed, 592 insertions(+), 913 deletions(-) diff --git a/tests/components/template/conftest.py b/tests/components/template/conftest.py index 410ce21e4c6..5ccc9e6479a 100644 --- a/tests/components/template/conftest.py +++ b/tests/components/template/conftest.py @@ -1,6 +1,4 @@ """template conftest.""" -import json - import pytest from homeassistant.setup import async_setup_component @@ -15,18 +13,8 @@ def calls(hass): @pytest.fixture -def config_addon(): - """Add entra configuration items.""" - return None - - -@pytest.fixture -async def start_ha(hass, count, domain, config_addon, config, caplog): +async def start_ha(hass, count, domain, config, caplog): """Do setup of integration.""" - if config_addon: - for key, value in config_addon.items(): - config = config.replace(key, value) - config = json.loads(config) with assert_setup_component(count, domain): assert await async_setup_component( hass, diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 924a5c25538..394eb1cb9ba 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -26,48 +26,90 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.setup import async_setup_component + +from tests.common import assert_setup_component # Represent for light's availability _STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state" -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +OPTIMISTIC_ON_OFF_LIGHT_CONFIG = { + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, +} + + +OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG = { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + }, + }, +} + + +OPTIMISTIC_WHITE_VALUE_LIGHT_CONFIG = { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "set_white_value": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "white_value": "{{white_value}}", + }, + }, +} + + +async def async_setup_light(hass, count, light_config): + """Do setup of light integration.""" + config = {"light": {"platform": "template", "lights": light_config}} + + with assert_setup_component(count, light.DOMAIN): + assert await async_setup_component( + hass, + light.DOMAIN, + config, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + +@pytest.fixture +async def setup_light(hass, count, light_config): + """Do setup of light integration.""" + await async_setup_light(hass, count, light_config) + + +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( "supported_features,supported_color_modes", [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], ) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "value_template": "{{states.test['big.fat...']}}", - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", - }, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - }, - }, - } - }, + "test_template_light": { + **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, + "value_template": "{{states.test['big.fat...']}}", } }, ], ) async def test_template_state_invalid( - hass, supported_features, supported_color_modes, start_ha + hass, supported_features, supported_color_modes, setup_light ): """Test template state with render error.""" state = hass.states.get("light.test_template_light") @@ -77,43 +119,24 @@ async def test_template_state_invalid( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( "supported_features,supported_color_modes", [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], ) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "value_template": "{{ states.light.test_state.state }}", - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", - }, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - }, - }, - } - }, + "test_template_light": { + **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, + "value_template": "{{ states.light.test_state.state }}", } }, ], ) async def test_template_state_text( - hass, supported_features, supported_color_modes, start_ha + hass, supported_features, supported_color_modes, setup_light ): """Test the state text of a template.""" set_state = STATE_ON @@ -135,46 +158,24 @@ async def test_template_state_text( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( "supported_features,supported_color_modes", [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], ) @pytest.mark.parametrize( - "config_addon,expected_state,expected_color_mode", + "value_template,expected_state,expected_color_mode", [ - ({"replace1": '"{{ 1 == 1 }}"'}, STATE_ON, ColorMode.UNKNOWN), - ({"replace1": '"{{ 1 == 2 }}"'}, STATE_OFF, None), - ], -) -@pytest.mark.parametrize( - "config", - [ - """{ - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "value_template": replace1, - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state" - }, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state" - }, - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}" - } - } - } - } - } - }""", + ( + "{{ 1 == 1 }}", + STATE_ON, + ColorMode.UNKNOWN, + ), + ( + "{{ 1 == 2 }}", + STATE_OFF, + None, + ), ], ) async def test_templatex_state_boolean( @@ -183,9 +184,17 @@ async def test_templatex_state_boolean( expected_state, supported_features, supported_color_modes, - start_ha, + count, + value_template, ): """Test the setting of the state with boolean on.""" + light_config = { + "test_template_light": { + **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, + "value_template": value_template, + } + } + await async_setup_light(hass, count, light_config) state = hass.states.get("light.test_template_light") assert state.state == expected_state assert state.attributes.get("color_mode") == expected_color_mode @@ -193,99 +202,55 @@ async def test_templatex_state_boolean( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize("count,domain", [(0, light.DOMAIN)]) +@pytest.mark.parametrize("count", [0]) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "value_template": "{%- if false -%}", - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", - }, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - }, - }, - } - }, + "test_template_light": { + **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, + "value_template": "{%- if false -%}", } }, { - "light": { - "platform": "template", - "lights": { - "bad name here": { - "value_template": "{{ 1== 1}}", - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", - }, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - }, - }, - } - }, - } - }, - { - "light": { - "platform": "template", - "switches": {"test_template_light": "Invalid"}, + "bad name here": { + **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, + "value_template": "{{ 1== 1}}", } }, + {"test_template_light": "Invalid"}, ], ) -async def test_template_syntax_error(hass, start_ha): +async def test_template_syntax_error(hass, setup_light): """Test templating syntax error.""" assert hass.states.async_all("light") == [] -SET_VAL1 = '"value_template": "{{ 1== 1}}",' -SET_VAL2 = '"turn_on": {"service": "light.turn_on","entity_id": "light.test_state"},' -SET_VAL3 = '"turn_off": {"service": "light.turn_off","entity_id": "light.test_state"},' - - -@pytest.mark.parametrize("domain", [light.DOMAIN]) @pytest.mark.parametrize( - "config_addon, count", + "light_config, count", [ - ({"replace2": f"{SET_VAL2}{SET_VAL3}"}, 1), - ({"replace2": f"{SET_VAL1}{SET_VAL2}"}, 0), - ({"replace2": f"{SET_VAL2}{SET_VAL3}"}, 1), + ( + { + "light_one": { + "value_template": "{{ 1== 1}}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + }, + }, + } + }, + 0, + ), ], ) -@pytest.mark.parametrize( - "config", - [ - """{"light": {"platform": "template", "lights": { - "light_one": { - replace2 - "set_level": {"service": "light.turn_on", - "data_template": {"entity_id": "light.test_state","brightness": "{{brightness}}" - }}}}}}""" - ], -) -async def test_missing_key(hass, count, start_ha): +async def test_missing_key(hass, count, setup_light): """Test missing template.""" if count: assert hass.states.async_all("light") != [] @@ -293,40 +258,25 @@ async def test_missing_key(hass, count, start_ha): assert hass.states.async_all("light") == [] -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( "supported_features,supported_color_modes", [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], ) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "value_template": "{{states.light.test_state.state}}", - "turn_on": {"service": "test.automation"}, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - }, - }, - } - }, + "test_template_light": { + **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, + "value_template": "{{states.light.test_state.state}}", + "turn_on": {"service": "test.automation"}, } }, ], ) async def test_on_action( - hass, start_ha, calls, supported_features, supported_color_modes + hass, setup_light, calls, supported_features, supported_color_modes ): """Test on action.""" hass.states.async_set("light.test_state", STATE_OFF) @@ -353,47 +303,42 @@ async def test_on_action( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( "supported_features,supported_color_modes", [(SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION, [ColorMode.BRIGHTNESS])], ) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "value_template": "{{states.light.test_state.state}}", - "turn_on": { - "service": "test.automation", - "data_template": { - "transition": "{{transition}}", - }, - }, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - "supports_transition_template": "{{true}}", - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - "transition": "{{transition}}", - }, - }, - } + "test_template_light": { + "value_template": "{{states.light.test_state.state}}", + "turn_on": { + "service": "test.automation", + "data_template": { + "transition": "{{transition}}", + }, + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "supports_transition_template": "{{true}}", + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + "transition": "{{transition}}", + }, }, } }, ], ) async def test_on_action_with_transition( - hass, start_ha, calls, supported_features, supported_color_modes + hass, setup_light, calls, supported_features, supported_color_modes ): """Test on action with transition.""" hass.states.async_set("light.test_state", STATE_OFF) @@ -421,40 +366,25 @@ async def test_on_action_with_transition( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( "supported_features,supported_color_modes,expected_color_mode", [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS], ColorMode.BRIGHTNESS)], ) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "turn_on": {"service": "test.automation"}, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - }, - }, - } - }, + "test_template_light": { + **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, + "turn_on": {"service": "test.automation"}, } }, ], ) async def test_on_action_optimistic( hass, - start_ha, + setup_light, calls, supported_features, supported_color_modes, @@ -499,42 +429,25 @@ async def test_on_action_optimistic( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( "supported_features,supported_color_modes", [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], ) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "value_template": "{{states.light.test_state.state}}", - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", - }, - "turn_off": { - "service": "test.automation", - }, - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - }, - }, - } - }, + "test_template_light": { + **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, + "value_template": "{{states.light.test_state.state}}", + "turn_off": {"service": "test.automation"}, } }, ], ) async def test_off_action( - hass, start_ha, calls, supported_features, supported_color_modes + hass, setup_light, calls, supported_features, supported_color_modes ): """Test off action.""" hass.states.async_set("light.test_state", STATE_ON) @@ -560,47 +473,42 @@ async def test_off_action( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [(1)]) @pytest.mark.parametrize( "supported_features,supported_color_modes", [(SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION, [ColorMode.BRIGHTNESS])], ) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "value_template": "{{states.light.test_state.state}}", - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", - }, - "turn_off": { - "service": "test.automation", - "data_template": { - "transition": "{{transition}}", - }, - }, - "supports_transition_template": "{{true}}", - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - "transition": "{{transition}}", - }, - }, - } + "test_template_light": { + "value_template": "{{states.light.test_state.state}}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "test.automation", + "data_template": { + "transition": "{{transition}}", + }, + }, + "supports_transition_template": "{{true}}", + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + "transition": "{{transition}}", + }, }, } }, ], ) async def test_off_action_with_transition( - hass, start_ha, calls, supported_features, supported_color_modes + hass, setup_light, calls, supported_features, supported_color_modes ): """Test off action with transition.""" hass.states.async_set("light.test_state", STATE_ON) @@ -627,39 +535,24 @@ async def test_off_action_with_transition( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( "supported_features,supported_color_modes", [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], ) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", - }, - "turn_off": {"service": "test.automation"}, - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - }, - }, - } - }, + "test_template_light": { + **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, + "turn_off": {"service": "test.automation"}, } }, ], ) async def test_off_action_optimistic( - hass, start_ha, calls, supported_features, supported_color_modes + hass, setup_light, calls, supported_features, supported_color_modes ): """Test off action with optimistic state.""" state = hass.states.get("light.test_template_light") @@ -683,36 +576,24 @@ async def test_off_action_optimistic( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( "supported_features,supported_color_modes,expected_color_mode", [(SUPPORT_WHITE_VALUE, [ColorMode.RGBW], ColorMode.UNKNOWN)], ) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "value_template": "{{1 == 1}}", - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", - }, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - "set_white_value": { - "service": "test.automation", - "data_template": { - "entity_id": "test.test_state", - "white_value": "{{white_value}}", - }, - }, - } + "test_template_light": { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "value_template": "{{1 == 1}}", + "set_white_value": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "white_value": "{{white_value}}", + }, }, } }, @@ -720,7 +601,7 @@ async def test_off_action_optimistic( ) async def test_white_value_action_no_template( hass, - start_ha, + setup_light, calls, supported_color_modes, supported_features, @@ -748,46 +629,40 @@ async def test_white_value_action_no_template( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize( - "expected_white_value,config_addon", - [ - (255, {"replace3": "{{255}}"}), - (None, {"replace3": "{{256}}"}), - (None, {"replace3": "{{x - 12}}"}), - (None, {"replace3": "{{ none }}"}), - (None, {"replace3": ""}), - ], -) -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( "supported_features,supported_color_modes,expected_color_mode", [(SUPPORT_WHITE_VALUE, [ColorMode.RGBW], ColorMode.UNKNOWN)], ) @pytest.mark.parametrize( - "config", + "expected_white_value,white_value_template", [ - """{ - "light": {"platform": "template","lights": { - "test_template_light": { - "value_template": "{{ 1 == 1 }}", - "turn_on": {"service": "light.turn_on","entity_id": "light.test_state"}, - "turn_off": {"service": "light.turn_off","entity_id": "light.test_state"}, - "set_white_value": {"service": "light.turn_on", - "data_template": {"entity_id": "light.test_state", - "white_value": "{{white_value}}"}}, - "white_value_template": "replace3" - }}}}""", + (255, "{{255}}"), + (None, "{{256}}"), + (None, "{{x-12}}"), + (None, "{{ none }}"), + (None, ""), ], ) async def test_white_value_template( hass, expected_white_value, - start_ha, supported_features, supported_color_modes, expected_color_mode, + count, + white_value_template, ): """Test the template for the white value.""" + light_config = { + "test_template_light": { + **OPTIMISTIC_WHITE_VALUE_LIGHT_CONFIG, + "value_template": "{{ 1 == 1 }}", + "white_value_template": white_value_template, + } + } + await async_setup_light(hass, count, light_config) + state = hass.states.get("light.test_template_light") assert state is not None assert state.attributes.get("white_value") == expected_white_value @@ -797,36 +672,24 @@ async def test_white_value_template( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( "supported_features,supported_color_modes,expected_color_mode", [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS], ColorMode.BRIGHTNESS)], ) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "value_template": "{{1 == 1}}", - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", - }, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - "set_level": { - "service": "test.automation", - "data_template": { - "entity_id": "test.test_state", - "brightness": "{{brightness}}", - }, - }, - } + "test_template_light": { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "value_template": "{{1 == 1}}", + "set_level": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "brightness": "{{brightness}}", + }, }, } }, @@ -834,7 +697,7 @@ async def test_white_value_template( ) async def test_level_action_no_template( hass, - start_ha, + setup_light, calls, supported_features, supported_color_modes, @@ -862,18 +725,18 @@ async def test_level_action_no_template( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "expected_level,config_addon,expected_color_mode", + "expected_level,level_template,expected_color_mode", [ - (255, {"replace4": '"{{255}}"'}, ColorMode.BRIGHTNESS), - (None, {"replace4": '"{{256}}"'}, ColorMode.UNKNOWN), - (None, {"replace4": '"{{x - 12}}"'}, ColorMode.UNKNOWN), - (None, {"replace4": '"{{ none }}"'}, ColorMode.UNKNOWN), - (None, {"replace4": '""'}, ColorMode.UNKNOWN), + (255, "{{255}}", ColorMode.BRIGHTNESS), + (None, "{{256}}", ColorMode.UNKNOWN), + (None, "{{x - 12}}", ColorMode.UNKNOWN), + (None, "{{ none }}", ColorMode.UNKNOWN), + (None, "", ColorMode.UNKNOWN), ( None, - {"replace4": "\"{{ state_attr('light.nolight', 'brightness') }}\""}, + "{{ state_attr('light.nolight', 'brightness') }}", ColorMode.UNKNOWN, ), ], @@ -882,29 +745,31 @@ async def test_level_action_no_template( "supported_features,supported_color_modes", [(SUPPORT_BRIGHTNESS, [ColorMode.BRIGHTNESS])], ) -@pytest.mark.parametrize( - "config", - [ - """{"light": {"platform": "template", "lights": { - "test_template_light": { - "value_template": "{{ 1 == 1 }}", - "turn_on": {"service": "light.turn_on","entity_id": "light.test_state"}, - "turn_off": {"service": "light.turn_off","entity_id": "light.test_state"}, - "set_level": {"service": "light.turn_on","data_template": { - "entity_id": "light.test_state","brightness": "{{brightness}}"}}, - "level_template": replace4 - }}}}""", - ], -) async def test_level_template( hass, expected_level, - start_ha, supported_features, supported_color_modes, expected_color_mode, + count, + level_template, ): """Test the template for the level.""" + light_config = { + "test_template_light": { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "value_template": "{{ 1 == 1 }}", + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + }, + }, + "level_template": level_template, + } + } + await async_setup_light(hass, count, light_config) state = hass.states.get("light.test_template_light") assert state.attributes.get("brightness") == expected_level assert state.state == STATE_ON @@ -913,62 +778,47 @@ async def test_level_template( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "expected_temp,config_addon,expected_color_mode", + "expected_temp,temperature_template,expected_color_mode", [ - (500, {"replace5": '"{{500}}"'}, ColorMode.COLOR_TEMP), - (None, {"replace5": '"{{501}}"'}, ColorMode.UNKNOWN), - (None, {"replace5": '"{{x - 12}}"'}, ColorMode.UNKNOWN), - (None, {"replace5": '"None"'}, ColorMode.UNKNOWN), - (None, {"replace5": '"{{ none }}"'}, ColorMode.UNKNOWN), - (None, {"replace5": '""'}, ColorMode.UNKNOWN), + (500, "{{500}}", ColorMode.COLOR_TEMP), + (None, "{{501}}", ColorMode.UNKNOWN), + (None, "{{x - 12}}", ColorMode.UNKNOWN), + (None, "None", ColorMode.UNKNOWN), + (None, "{{ none }}", ColorMode.UNKNOWN), + (None, "", ColorMode.UNKNOWN), ], ) @pytest.mark.parametrize( "supported_features,supported_color_modes", [(SUPPORT_COLOR_TEMP, [ColorMode.COLOR_TEMP])], ) -@pytest.mark.parametrize( - "config", - [ - """{ - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "value_template": "{{ 1 == 1 }}", - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state" - }, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state" - }, - "set_temperature": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "color_temp": "{{color_temp}}" - } - }, - "temperature_template": replace5 - } - } - } - }""" - ], -) async def test_temperature_template( hass, expected_temp, - start_ha, supported_features, supported_color_modes, expected_color_mode, + count, + temperature_template, ): """Test the template for the temperature.""" + light_config = { + "test_template_light": { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "value_template": "{{ 1 == 1 }}", + "set_temperature": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "color_temp": "{{color_temp}}", + }, + }, + "temperature_template": temperature_template, + } + } + await async_setup_light(hass, count, light_config) state = hass.states.get("light.test_template_light") assert state.attributes.get("color_temp") == expected_temp assert state.state == STATE_ON @@ -977,36 +827,24 @@ async def test_temperature_template( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( "supported_features,supported_color_modes,expected_color_mode", [(SUPPORT_COLOR_TEMP, [ColorMode.COLOR_TEMP], ColorMode.COLOR_TEMP)], ) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "value_template": "{{1 == 1}}", - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", - }, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - "set_temperature": { - "service": "test.automation", - "data_template": { - "entity_id": "test.test_state", - "color_temp": "{{color_temp}}", - }, - }, - } + "test_template_light": { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "value_template": "{{1 == 1}}", + "set_temperature": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "color_temp": "{{color_temp}}", + }, }, } }, @@ -1014,7 +852,7 @@ async def test_temperature_template( ) async def test_temperature_action_no_template( hass, - start_ha, + setup_light, calls, supported_features, supported_color_modes, @@ -1043,39 +881,20 @@ async def test_temperature_action_no_template( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "friendly_name": "Template light", - "value_template": "{{ 1 == 1 }}", - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", - }, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - }, - }, - } - }, + "test_template_light": { + **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, + "friendly_name": "Template light", + "value_template": "{{ 1 == 1 }}", } }, ], ) -async def test_friendly_name(hass, start_ha): +async def test_friendly_name(hass, setup_light): """Test the accessibility of the friendly_name attribute.""" state = hass.states.get("light.test_template_light") @@ -1084,42 +903,23 @@ async def test_friendly_name(hass, start_ha): assert state.attributes.get("friendly_name") == "Template light" -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "friendly_name": "Template light", - "value_template": "{{ 1 == 1 }}", - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", - }, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - }, - }, - "icon_template": "{% if states.light.test_state.state %}" - "mdi:check" - "{% endif %}", - } - }, + "test_template_light": { + **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, + "friendly_name": "Template light", + "value_template": "{{ 1 == 1 }}", + "icon_template": "{% if states.light.test_state.state %}" + "mdi:check" + "{% endif %}", } }, ], ) -async def test_icon_template(hass, start_ha): +async def test_icon_template(hass, setup_light): """Test icon template.""" state = hass.states.get("light.test_template_light") assert state.attributes.get("icon") == "" @@ -1132,42 +932,23 @@ async def test_icon_template(hass, start_ha): assert state.attributes["icon"] == "mdi:check" -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "friendly_name": "Template light", - "value_template": "{{ 1 == 1 }}", - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", - }, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - }, - }, - "entity_picture_template": "{% if states.light.test_state.state %}" - "/local/light.png" - "{% endif %}", - } - }, + "test_template_light": { + **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, + "friendly_name": "Template light", + "value_template": "{{ 1 == 1 }}", + "entity_picture_template": "{% if states.light.test_state.state %}" + "/local/light.png" + "{% endif %}", } }, ], ) -async def test_entity_picture_template(hass, start_ha): +async def test_entity_picture_template(hass, setup_light): """Test entity_picture template.""" state = hass.states.get("light.test_template_light") assert state.attributes.get("entity_picture") == "" @@ -1180,55 +961,35 @@ async def test_entity_picture_template(hass, start_ha): assert state.attributes["entity_picture"] == "/local/light.png" -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( "supported_features,supported_color_modes, expected_color_mode", [(SUPPORT_COLOR, [ColorMode.HS], ColorMode.HS)], ) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "value_template": "{{1 == 1}}", - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", + "test_template_light": { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "value_template": "{{1 == 1}}", + "set_color": [ + { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "s": "{{s}}", + "h": "{{h}}", }, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - "set_color": [ - { - "service": "test.automation", - "data_template": { - "entity_id": "test.test_state", - "h": "{{h}}", - "s": "{{s}}", - }, - }, - { - "service": "test.automation", - "data_template": { - "entity_id": "test.test_state", - "s": "{{s}}", - "h": "{{h}}", - }, - }, - ], - } - }, + }, + ], } }, ], ) async def test_color_action_no_template( hass, - start_ha, + setup_light, calls, supported_features, supported_color_modes, @@ -1245,11 +1006,9 @@ async def test_color_action_no_template( blocking=True, ) - assert len(calls) == 2 + assert len(calls) == 1 assert calls[0].data["h"] == 40 assert calls[0].data["s"] == 50 - assert calls[1].data["h"] == 40 - assert calls[1].data["s"] == 50 state = hass.states.get("light.test_template_light") assert state.state == STATE_ON @@ -1259,47 +1018,51 @@ async def test_color_action_no_template( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "expected_hs,config_addon,expected_color_mode", + "expected_hs,color_template,expected_color_mode", [ - ((360, 100), {"replace6": '"{{(360, 100)}}"'}, ColorMode.HS), - ((359.9, 99.9), {"replace6": '"{{(359.9, 99.9)}}"'}, ColorMode.HS), - (None, {"replace6": '"{{(361, 100)}}"'}, ColorMode.UNKNOWN), - (None, {"replace6": '"{{(360, 101)}}"'}, ColorMode.UNKNOWN), - (None, {"replace6": '"[{{(360)}},{{null}}]"'}, ColorMode.UNKNOWN), - (None, {"replace6": '"{{x - 12}}"'}, ColorMode.UNKNOWN), - (None, {"replace6": '""'}, ColorMode.UNKNOWN), - (None, {"replace6": '"{{ none }}"'}, ColorMode.UNKNOWN), + ((360, 100), "{{(360, 100)}}", ColorMode.HS), + ((359.9, 99.9), "{{(359.9, 99.9)}}", ColorMode.HS), + (None, "{{(361, 100)}}", ColorMode.UNKNOWN), + (None, "{{(360, 101)}}", ColorMode.UNKNOWN), + (None, "[{{(360)}},{{null}}]", ColorMode.UNKNOWN), + (None, "{{x - 12}}", ColorMode.UNKNOWN), + (None, "", ColorMode.UNKNOWN), + (None, "{{ none }}", ColorMode.UNKNOWN), ], ) @pytest.mark.parametrize( "supported_features,supported_color_modes", [(SUPPORT_COLOR, [ColorMode.HS])], ) -@pytest.mark.parametrize( - "config", - [ - """{"light": {"platform": "template","lights": {"test_template_light": { - "value_template": "{{ 1 == 1 }}", - "turn_on": {"service": "light.turn_on","entity_id": "light.test_state"}, - "turn_off": {"service": "light.turn_off","entity_id": "light.test_state"}, - "set_color": [{"service": "input_number.set_value", - "data_template": {"entity_id": "input_number.h","color_temp": "{{h}}" - }}], - "color_template": replace6 - }}}}""" - ], -) async def test_color_template( hass, expected_hs, - start_ha, supported_features, supported_color_modes, expected_color_mode, + count, + color_template, ): """Test the template for the color.""" + light_config = { + "test_template_light": { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "value_template": "{{ 1 == 1 }}", + "set_color": [ + { + "service": "input_number.set_value", + "data_template": { + "entity_id": "input_number.h", + "color_temp": "{{h}}", + }, + } + ], + "color_template": color_template, + } + } + await async_setup_light(hass, count, light_config) state = hass.states.get("light.test_template_light") assert state.attributes.get("hs_color") == expected_hs assert state.state == STATE_ON @@ -1308,53 +1071,41 @@ async def test_color_template( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( "supported_features,supported_color_modes", [(SUPPORT_COLOR | SUPPORT_COLOR_TEMP, [ColorMode.COLOR_TEMP, ColorMode.HS])], ) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "value_template": "{{1 == 1}}", - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", + "test_template_light": { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "value_template": "{{1 == 1}}", + "set_color": [ + { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "h": "{{h}}", + "s": "{{s}}", }, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - "set_color": [ - { - "service": "test.automation", - "data_template": { - "entity_id": "test.test_state", - "h": "{{h}}", - "s": "{{s}}", - }, - }, - ], - "set_temperature": { - "service": "test.automation", - "data_template": { - "entity_id": "test.test_state", - "color_temp": "{{color_temp}}", - }, - }, - } + }, + ], + "set_temperature": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "color_temp": "{{color_temp}}", + }, }, } }, ], ) async def test_color_and_temperature_actions_no_template( - hass, start_ha, calls, supported_features, supported_color_modes + hass, setup_light, calls, supported_features, supported_color_modes ): """Test setting color and color temperature with optimistic template.""" state = hass.states.get("light.test_template_light") @@ -1435,44 +1186,35 @@ async def test_color_and_temperature_actions_no_template( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "value_template": "{{true}}", - "turn_on": {"service": "test.automation"}, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - }, - }, - "set_effect": { - "service": "test.automation", - "data_template": { - "entity_id": "test.test_state", - "effect": "{{effect}}", - }, - }, - "effect_list_template": "{{ ['Disco', 'Police'] }}", - "effect_template": "{{ 'Disco' }}", - } + "test_template_light": { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "value_template": "{{true}}", + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + }, }, + "set_effect": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "effect": "{{effect}}", + }, + }, + "effect_list_template": "{{ ['Disco', 'Police'] }}", + "effect_template": "{{ 'Disco' }}", } }, ], ) -async def test_effect_action_valid_effect(hass, start_ha, calls): +async def test_effect_action_valid_effect(hass, setup_light, calls): """Test setting valid effect with template.""" state = hass.states.get("light.test_template_light") assert state is not None @@ -1492,44 +1234,35 @@ async def test_effect_action_valid_effect(hass, start_ha, calls): assert state.attributes.get("effect") == "Disco" -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "value_template": "{{true}}", - "turn_on": {"service": "test.automation"}, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - }, - }, - "set_effect": { - "service": "test.automation", - "data_template": { - "entity_id": "test.test_state", - "effect": "{{effect}}", - }, - }, - "effect_list_template": "{{ ['Disco', 'Police'] }}", - "effect_template": "{{ None }}", - } + "test_template_light": { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "value_template": "{{true}}", + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + }, }, + "set_effect": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "effect": "{{effect}}", + }, + }, + "effect_list_template": "{{ ['Disco', 'Police'] }}", + "effect_template": "{{ None }}", } }, ], ) -async def test_effect_action_invalid_effect(hass, start_ha, calls): +async def test_effect_action_invalid_effect(hass, setup_light, calls): """Test setting invalid effect with template.""" state = hass.states.get("light.test_template_light") assert state is not None @@ -1549,176 +1282,191 @@ async def test_effect_action_invalid_effect(hass, start_ha, calls): assert state.attributes.get("effect") is None -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "expected_effect_list,config_addon", + "expected_effect_list,effect_list_template", [ ( ["Strobe color", "Police", "Christmas", "RGB", "Random Loop"], - { - "replace7": "\"{{ ['Strobe color', 'Police', 'Christmas', 'RGB', 'Random Loop'] }}\"" - }, + "{{ ['Strobe color', 'Police', 'Christmas', 'RGB', 'Random Loop'] }}", ), ( ["Police", "RGB", "Random Loop"], - {"replace7": "\"{{ ['Police', 'RGB', 'Random Loop'] }}\""}, + "{{ ['Police', 'RGB', 'Random Loop'] }}", ), - (None, {"replace7": '"{{ [] }}"'}), - (None, {"replace7": "\"{{ '[]' }}\""}), - (None, {"replace7": '"{{ 124 }}"'}), - (None, {"replace7": "\"{{ '124' }}\""}), - (None, {"replace7": '"{{ none }}"'}), - (None, {"replace7": '""'}), + (None, "{{ [] }}"), + (None, "{{ '[]' }}"), + (None, "{{ 124 }}"), + (None, "{{ '124' }}"), + (None, "{{ none }}"), + (None, ""), ], ) -@pytest.mark.parametrize( - "config", - [ - """{"light": {"platform": "template","lights": {"test_template_light": { - "value_template": "{{ 1 == 1 }}", - "turn_on": {"service": "light.turn_on","entity_id": "light.test_state"}, - "turn_off": {"service": "light.turn_off","entity_id": "light.test_state"}, - "set_effect": {"service": "test.automation", - "data_template": {"entity_id": "test.test_state","effect": "{{effect}}"}}, - "effect_template": "{{ None }}", - "effect_list_template": replace7 - }}}}""", - ], -) -async def test_effect_list_template(hass, expected_effect_list, start_ha): +async def test_effect_list_template( + hass, expected_effect_list, count, effect_list_template +): """Test the template for the effect list.""" + light_config = { + "test_template_light": { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "value_template": "{{ 1 == 1 }}", + "set_effect": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "effect": "{{effect}}", + }, + }, + "effect_template": "{{ None }}", + "effect_list_template": effect_list_template, + } + } + await async_setup_light(hass, count, light_config) state = hass.states.get("light.test_template_light") assert state is not None assert state.attributes.get("effect_list") == expected_effect_list -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "expected_effect,config_addon", + "expected_effect,effect_template", [ - (None, {"replace8": '"Disco"'}), - (None, {"replace8": '"None"'}), - (None, {"replace8": '"{{ None }}"'}), - ("Police", {"replace8": '"Police"'}), - ("Strobe color", {"replace8": "\"{{ 'Strobe color' }}\""}), + (None, "Disco"), + (None, "None"), + (None, "{{ None }}"), + ("Police", "Police"), + ("Strobe color", "{{ 'Strobe color' }}"), ], ) -@pytest.mark.parametrize( - "config", - [ - """{"light": {"platform": "template","lights": {"test_template_light": { - "value_template": "{{ 1 == 1 }}", - "turn_on": {"service": "light.turn_on","entity_id": "light.test_state"}, - "turn_off": {"service": "light.turn_off","entity_id": "light.test_state"}, - "set_effect": {"service": "test.automation","data_template": { - "entity_id": "test.test_state","effect": "{{effect}}"}}, - "effect_list_template": "{{ ['Strobe color', 'Police', 'Christmas', 'RGB', 'Random Loop'] }}", - "effect_template": replace8 - }}}}""", - ], -) -async def test_effect_template(hass, expected_effect, start_ha): +async def test_effect_template(hass, expected_effect, count, effect_template): """Test the template for the effect.""" + light_config = { + "test_template_light": { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "value_template": "{{ 1 == 1 }}", + "set_effect": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "effect": "{{effect}}", + }, + }, + "effect_list_template": "{{ ['Strobe color', 'Police', 'Christmas', 'RGB', 'Random Loop'] }}", + "effect_template": effect_template, + } + } + await async_setup_light(hass, count, light_config) state = hass.states.get("light.test_template_light") assert state is not None assert state.attributes.get("effect") == expected_effect -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "expected_min_mireds,config_addon", + "expected_min_mireds,min_mireds_template", [ - (118, {"replace9": '"{{118}}"'}), - (153, {"replace9": '"{{x - 12}}"'}), - (153, {"replace9": '"None"'}), - (153, {"replace9": '"{{ none }}"'}), - (153, {"replace9": '""'}), - (153, {"replace9": "\"{{ 'a' }}\""}), + (118, "{{118}}"), + (153, "{{x - 12}}"), + (153, "None"), + (153, "{{ none }}"), + (153, ""), + (153, "{{ 'a' }}"), ], ) -@pytest.mark.parametrize( - "config", - [ - """{"light": {"platform": "template","lights": {"test_template_light": { - "value_template": "{{ 1 == 1 }}", - "turn_on": {"service": "light.turn_on","entity_id": "light.test_state"}, - "turn_off": {"service": "light.turn_off","entity_id": "light.test_state"}, - "set_temperature": {"service": "light.turn_on","data_template": { - "entity_id": "light.test_state","color_temp": "{{color_temp}}"}}, - "temperature_template": "{{200}}", - "min_mireds_template": replace9 - }}}}""", - ], -) -async def test_min_mireds_template(hass, expected_min_mireds, start_ha): +async def test_min_mireds_template( + hass, expected_min_mireds, count, min_mireds_template +): """Test the template for the min mireds.""" + light_config = { + "test_template_light": { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "value_template": "{{ 1 == 1 }}", + "set_temperature": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "color_temp": "{{color_temp}}", + }, + }, + "temperature_template": "{{200}}", + "min_mireds_template": min_mireds_template, + } + } + await async_setup_light(hass, count, light_config) state = hass.states.get("light.test_template_light") assert state is not None assert state.attributes.get("min_mireds") == expected_min_mireds -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "expected_max_mireds,config_addon", + "expected_max_mireds,max_mireds_template", [ - (488, {"template1": '"{{488}}"'}), - (500, {"template1": '"{{x - 12}}"'}), - (500, {"template1": '"None"'}), - (500, {"template1": '"{{ none }}"'}), - (500, {"template1": '""'}), - (500, {"template1": "\"{{ 'a' }}\""}), + (488, "{{488}}"), + (500, "{{x - 12}}"), + (500, "None"), + (500, "{{ none }}"), + (500, ""), + (500, "{{ 'a' }}"), ], ) -@pytest.mark.parametrize( - "config", - [ - """{"light": {"platform": "template","lights": {"test_template_light": { - "value_template": "{{ 1 == 1 }}", - "turn_on": {"service": "light.turn_on","entity_id": "light.test_state"}, - "turn_off": {"service": "light.turn_off","entity_id": "light.test_state"}, - "set_temperature": {"service": "light.turn_on","data_template": { - "entity_id": "light.test_state","color_temp": "{{color_temp}}"}}, - "temperature_template": "{{200}}", - "max_mireds_template": template1 - }}}}""", - ], -) -async def test_max_mireds_template(hass, expected_max_mireds, start_ha): +async def test_max_mireds_template( + hass, expected_max_mireds, count, max_mireds_template +): """Test the template for the max mireds.""" + light_config = { + "test_template_light": { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "value_template": "{{ 1 == 1 }}", + "set_temperature": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "color_temp": "{{color_temp}}", + }, + }, + "temperature_template": "{{200}}", + "max_mireds_template": max_mireds_template, + } + } + await async_setup_light(hass, count, light_config) state = hass.states.get("light.test_template_light") assert state is not None assert state.attributes.get("max_mireds") == expected_max_mireds -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "expected_supports_transition,config_addon", + "expected_supports_transition,supports_transition_template", [ - (True, {"template2": '"{{true}}"'}), - (True, {"template2": '"{{1 == 1}}"'}), - (False, {"template2": '"{{false}}"'}), - (False, {"template2": '"{{ none }}"'}), - (False, {"template2": '""'}), - (False, {"template2": '"None"'}), - ], -) -@pytest.mark.parametrize( - "config", - [ - """{"light": {"platform": "template","lights": {"test_template_light": { - "value_template": "{{ 1 == 1 }}", - "turn_on": {"service": "light.turn_on","entity_id": "light.test_state"}, - "turn_off": {"service": "light.turn_off","entity_id": "light.test_state"}, - "set_temperature": {"service": "light.turn_on","data_template": { - "entity_id": "light.test_state","color_temp": "{{color_temp}}"}}, - "supports_transition_template": template2 - }}}}""", + (True, "{{true}}"), + (True, "{{1 == 1}}"), + (False, "{{false}}"), + (False, "{{ none }}"), + (False, ""), + (False, "None"), ], ) async def test_supports_transition_template( - hass, expected_supports_transition, start_ha + hass, expected_supports_transition, count, supports_transition_template ): """Test the template for the supports transition.""" + light_config = { + "test_template_light": { + "value_template": "{{ 1 == 1 }}", + "turn_on": {"service": "light.turn_on", "entity_id": "light.test_state"}, + "turn_off": {"service": "light.turn_off", "entity_id": "light.test_state"}, + "set_temperature": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "color_temp": "{{color_temp}}", + }, + }, + "supports_transition_template": supports_transition_template, + } + } + await async_setup_light(hass, count, light_config) state = hass.states.get("light.test_template_light") expected_value = 1 @@ -1732,38 +1480,19 @@ async def test_supports_transition_template( ) != expected_value -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "availability_template": "{{ is_state('availability_boolean.state', 'on') }}", - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", - }, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - }, - }, - } - }, + "test_template_light": { + **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, + "availability_template": "{{ is_state('availability_boolean.state', 'on') }}", } }, ], ) -async def test_available_template_with_entities(hass, start_ha): +async def test_available_template_with_entities(hass, setup_light): """Test availability templates with values from other entities.""" # When template returns true.. hass.states.async_set(_STATE_AVAILABILITY_BOOLEAN, STATE_ON) @@ -1780,80 +1509,42 @@ async def test_available_template_with_entities(hass, start_ha): assert hass.states.get("light.test_template_light").state == STATE_UNAVAILABLE -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light": { - "availability_template": "{{ x - 12 }}", - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", - }, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - }, - }, - } - }, + "test_template_light": { + **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, + "availability_template": "{{ x - 12 }}", } }, ], ) async def test_invalid_availability_template_keeps_component_available( - hass, start_ha, caplog_setup_text + hass, setup_light, caplog_setup_text ): """Test that an invalid availability keeps the device available.""" assert hass.states.get("light.test_template_light").state != STATE_UNAVAILABLE assert ("UndefinedError: 'x' is undefined") in caplog_setup_text -@pytest.mark.parametrize("count,domain", [(1, light.DOMAIN)]) +@pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( - "config", + "light_config", [ { - "light": { - "platform": "template", - "lights": { - "test_template_light_01": { - "unique_id": "not-so-unique-anymore", - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", - }, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - }, - "test_template_light_02": { - "unique_id": "not-so-unique-anymore", - "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", - }, - "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", - }, - }, - }, - } + "test_template_light_01": { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "unique_id": "not-so-unique-anymore", + }, + "test_template_light_02": { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "unique_id": "not-so-unique-anymore", + }, }, ], ) -async def test_unique_id(hass, start_ha): +async def test_unique_id(hass, setup_light): """Test unique_id option only creates one light per id.""" assert len(hass.states.async_all("light")) == 1 From 80d332ddf1ccddea42cbab38a1ee85812a835134 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Fri, 13 May 2022 12:36:08 +0200 Subject: [PATCH 0462/3516] =?UTF-8?q?Don=E2=80=99t=20send=20None=20value?= =?UTF-8?q?=20within=20Command=20parameter=20value=20in=20Overkiz=20integr?= =?UTF-8?q?ation=20(#71582)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/overkiz/executor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/overkiz/executor.py b/homeassistant/components/overkiz/executor.py index 127851db36a..9bf7ef43b02 100644 --- a/homeassistant/components/overkiz/executor.py +++ b/homeassistant/components/overkiz/executor.py @@ -68,17 +68,18 @@ class OverkizExecutor: async def async_execute_command(self, command_name: str, *args: Any) -> None: """Execute device command in async context.""" + parameters = [arg for arg in args if arg is not None] # Set the execution duration to 0 seconds for RTS devices on supported commands # Default execution duration is 30 seconds and will block consecutive commands if ( self.device.protocol == Protocol.RTS and command_name not in COMMANDS_WITHOUT_DELAY ): - args = args + (0,) + parameters.append(0) exec_id = await self.coordinator.client.execute_command( self.device.device_url, - Command(command_name, list(args)), + Command(command_name, parameters), "Home Assistant", ) From f487c04e02d3c08bbb507dd28cd845bf80c5f6a0 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 13 May 2022 21:30:44 +1000 Subject: [PATCH 0463/3516] Remove LIFX bulb discovery from the inflight list if it fails to connect (#71673) Remove the bulb discovery from the inflight list if it fails to connect Signed-off-by: Avi Miller --- homeassistant/components/lifx/light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 188959adc76..31e973874d9 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -416,6 +416,8 @@ class LIFXManager: if color_resp is None or version_resp is None: _LOGGER.error("Failed to connect to %s", bulb.ip_addr) bulb.registered = False + if bulb.mac_addr in self.discoveries_inflight: + self.discoveries_inflight.pop(bulb.mac_addr) else: bulb.timeout = MESSAGE_TIMEOUT bulb.retry_count = MESSAGE_RETRIES From 0a9a86f973c6eff07c1c6c21f5afc95a1b603a0f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 13 May 2022 13:38:20 +0200 Subject: [PATCH 0464/3516] Update jinja2 to 3.1.2 (#71780) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 129774d989a..9c9354c584b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ hass-nabucasa==0.54.0 home-assistant-frontend==20220504.1 httpx==0.22.0 ifaddr==0.1.7 -jinja2==3.1.1 +jinja2==3.1.2 lru-dict==1.1.7 paho-mqtt==1.6.1 pillow==9.1.0 diff --git a/requirements.txt b/requirements.txt index ed129d0d944..2c96c00fdb3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 httpx==0.22.0 ifaddr==0.1.7 -jinja2==3.1.1 +jinja2==3.1.2 PyJWT==2.3.0 cryptography==36.0.2 pip>=21.0,<22.1 diff --git a/setup.cfg b/setup.cfg index 51226c30924..47bf617755a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,7 +44,7 @@ install_requires = # httpcore, anyio, and h11 in gen_requirements_all httpx==0.22.0 ifaddr==0.1.7 - jinja2==3.1.1 + jinja2==3.1.2 PyJWT==2.3.0 # PyJWT has loose dependency. We want the latest one. cryptography==36.0.2 From 184421dae68c8f97d7a9f796e2a26137089de562 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 13 May 2022 16:44:27 +0200 Subject: [PATCH 0465/3516] Support this variable in template switch actions (#71799) --- homeassistant/components/template/switch.py | 4 +- tests/components/template/test_switch.py | 68 ++++++++------------- 2 files changed, 29 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index 8a3955db007..ac01bc66812 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -156,14 +156,14 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): async def async_turn_on(self, **kwargs): """Fire the on action.""" - await self._on_script.async_run(context=self._context) + await self.async_run_script(self._on_script, context=self._context) if self._template is None: self._state = True self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Fire the off action.""" - await self._off_script.async_run(context=self._context) + await self.async_run_script(self._off_script, context=self._context) if self._template is None: self._state = False self.async_write_ha_state() diff --git a/tests/components/template/test_switch.py b/tests/components/template/test_switch.py index 93e0f8540bf..3c9c22154c9 100644 --- a/tests/components/template/test_switch.py +++ b/tests/components/template/test_switch.py @@ -1,53 +1,35 @@ """The tests for the Template switch platform.""" -import pytest from homeassistant import setup from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( - ATTR_DOMAIN, ATTR_ENTITY_ID, - ATTR_SERVICE_DATA, - EVENT_CALL_SERVICE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.core import CoreState, State, callback +from homeassistant.core import CoreState, State from homeassistant.setup import async_setup_component from tests.common import assert_setup_component, mock_component, mock_restore_cache - -@pytest.fixture -def service_calls(hass): - """Track service call events for switch.test_state.""" - events = [] - entity_id = "switch.test_state" - - @callback - def capture_events(event): - if event.data[ATTR_DOMAIN] != "switch": - return - if event.data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID] != [entity_id]: - return - events.append(event) - - hass.bus.async_listen(EVENT_CALL_SERVICE, capture_events) - - return events - - OPTIMISTIC_SWITCH_CONFIG = { "turn_on": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", + "service": "test.automation", + "data_template": { + "action": "turn_on", + "caller": "{{ this.entity_id }}", + }, }, "turn_off": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", + "service": "test.automation", + "data_template": { + "action": "turn_off", + "caller": "{{ this.entity_id }}", + }, }, } @@ -367,7 +349,7 @@ async def test_missing_off_does_not_create(hass): assert hass.states.async_all("switch") == [] -async def test_on_action(hass, service_calls): +async def test_on_action(hass, calls): """Test on action.""" assert await async_setup_component( hass, @@ -402,11 +384,12 @@ async def test_on_action(hass, service_calls): blocking=True, ) - assert len(service_calls) == 1 - assert service_calls[-1].data["service"] == "turn_on" + assert len(calls) == 1 + assert calls[-1].data["action"] == "turn_on" + assert calls[-1].data["caller"] == "switch.test_template_switch" -async def test_on_action_optimistic(hass, service_calls): +async def test_on_action_optimistic(hass, calls): """Test on action in optimistic mode.""" assert await async_setup_component( hass, @@ -442,11 +425,12 @@ async def test_on_action_optimistic(hass, service_calls): state = hass.states.get("switch.test_template_switch") assert state.state == STATE_ON - assert len(service_calls) == 1 - assert service_calls[-1].data["service"] == "turn_on" + assert len(calls) == 1 + assert calls[-1].data["action"] == "turn_on" + assert calls[-1].data["caller"] == "switch.test_template_switch" -async def test_off_action(hass, service_calls): +async def test_off_action(hass, calls): """Test off action.""" assert await async_setup_component( hass, @@ -481,11 +465,12 @@ async def test_off_action(hass, service_calls): blocking=True, ) - assert len(service_calls) == 1 - assert service_calls[-1].data["service"] == "turn_off" + assert len(calls) == 1 + assert calls[-1].data["action"] == "turn_off" + assert calls[-1].data["caller"] == "switch.test_template_switch" -async def test_off_action_optimistic(hass, service_calls): +async def test_off_action_optimistic(hass, calls): """Test off action in optimistic mode.""" assert await async_setup_component( hass, @@ -521,8 +506,9 @@ async def test_off_action_optimistic(hass, service_calls): state = hass.states.get("switch.test_template_switch") assert state.state == STATE_OFF - assert len(service_calls) == 1 - assert service_calls[-1].data["service"] == "turn_off" + assert len(calls) == 1 + assert calls[-1].data["action"] == "turn_off" + assert calls[-1].data["caller"] == "switch.test_template_switch" async def test_restore_state(hass): From 042321be6019366fc5449e3015f6234ff6513424 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 13 May 2022 17:28:36 +0200 Subject: [PATCH 0466/3516] Support this variable in template button actions (#71792) --- homeassistant/components/template/button.py | 2 +- tests/components/template/test_button.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/template/button.py b/homeassistant/components/template/button.py index d6f41649734..ac83f76ca91 100644 --- a/homeassistant/components/template/button.py +++ b/homeassistant/components/template/button.py @@ -93,4 +93,4 @@ class TemplateButtonEntity(TemplateEntity, ButtonEntity): async def async_press(self) -> None: """Press the button.""" - await self._command_press.async_run(context=self._context) + await self.async_run_script(self._command_press, context=self._context) diff --git a/tests/components/template/test_button.py b/tests/components/template/test_button.py index 25bced16ed2..48bedd8e928 100644 --- a/tests/components/template/test_button.py +++ b/tests/components/template/test_button.py @@ -68,7 +68,10 @@ async def test_all_optional_config(hass, calls): "template": { "unique_id": "test", "button": { - "press": {"service": "test.automation"}, + "press": { + "service": "test.automation", + "data_template": {"caller": "{{ this.entity_id }}"}, + }, "device_class": "restart", "unique_id": "test", "name": "test", @@ -104,6 +107,7 @@ async def test_all_optional_config(hass, calls): ) assert len(calls) == 1 + assert calls[0].data["caller"] == _TEST_OPTIONS_BUTTON _verify( hass, From 28560e76e9f952f005315c84e2171c90558a53fd Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 13 May 2022 17:29:56 +0200 Subject: [PATCH 0467/3516] Fix mixing string references in Motion Blinds translations (#71806) --- homeassistant/components/motion_blinds/strings.json | 2 +- homeassistant/components/motion_blinds/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/motion_blinds/strings.json b/homeassistant/components/motion_blinds/strings.json index 13a4d117344..0b1482883aa 100644 --- a/homeassistant/components/motion_blinds/strings.json +++ b/homeassistant/components/motion_blinds/strings.json @@ -26,7 +26,7 @@ "discovery_error": "Failed to discover a Motion Gateway" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%], connection settings are updated", + "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%]" } diff --git a/homeassistant/components/motion_blinds/translations/en.json b/homeassistant/components/motion_blinds/translations/en.json index 92931ee27ab..7a6d6763fdc 100644 --- a/homeassistant/components/motion_blinds/translations/en.json +++ b/homeassistant/components/motion_blinds/translations/en.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Device is already configured, connection settings are updated", + "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "connection_error": "Failed to connect" }, From e7e45209ec0c8a245dc2388d50b546a9426bbe5c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 May 2022 12:07:26 -0400 Subject: [PATCH 0468/3516] Update stale docstring in logbook (#71814) --- homeassistant/components/logbook/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 80748768c55..d850df847a0 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -355,11 +355,7 @@ def _humanify( context_augmenter: ContextAugmenter, format_time: Callable[[Row], Any], ) -> Generator[dict[str, Any], None, None]: - """Generate a converted list of events into Entry objects. - - Will try to group events if possible: - - if Home Assistant stop and start happen in same minute call it restarted - """ + """Generate a converted list of events into entries.""" external_events = hass.data.get(DOMAIN, {}) # Continuous sensors, will be excluded from the logbook continuous_sensors: dict[str, bool] = {} From d215cdc563d21b74b26c38ec9f95d63341f09347 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 May 2022 12:10:43 -0400 Subject: [PATCH 0469/3516] Avoid buffering logbook rows unless we are selecting less than a days worth (#71809) --- homeassistant/components/logbook/__init__.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index d850df847a0..592831badf1 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -445,10 +445,22 @@ def _get_events( def yield_rows(query: Query) -> Generator[Row, None, None]: """Yield Events that are not filtered away.""" - if entity_ids or context_id: + # end_day - start_day intentionally checks .days and not .total_seconds() + # since we don't want to switch over to buffered if they go + # over one day by a few hours since the UI makes it so easy to do that. + if entity_ids or context_id or (end_day - start_day).days <= 1: rows = query.all() else: - rows = query.yield_per(1000) + # Only buffer rows to reduce memory pressure + # if we expect the result set is going to be very large. + # What is considered very large is going to differ + # based on the hardware Home Assistant is running on. + # + # sqlalchemy suggests that is at least 10k, but for + # even and RPi3 that number seems higher in testing + # so we don't switch over until we request > 1 day+ of data. + # + rows = query.yield_per(1024) for row in rows: context_lookup.setdefault(row.context_id, row) if row.context_only: From 9eca91afc97be0f9a58953dce31fd1876a5e8152 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 13 May 2022 18:31:37 +0200 Subject: [PATCH 0470/3516] Support this variable in template light actions (#71805) --- homeassistant/components/template/light.py | 37 +++-- tests/components/template/test_light.py | 176 ++++++++++----------- 2 files changed, 104 insertions(+), 109 deletions(-) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 1988e77d223..eafb0b3f4d0 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -396,14 +396,18 @@ class LightTemplate(TemplateEntity, LightEntity): if ATTR_COLOR_TEMP in kwargs and self._temperature_script: common_params["color_temp"] = kwargs[ATTR_COLOR_TEMP] - await self._temperature_script.async_run( - common_params, context=self._context + await self.async_run_script( + self._temperature_script, + run_variables=common_params, + context=self._context, ) elif ATTR_WHITE_VALUE in kwargs and self._white_value_script: common_params["white_value"] = kwargs[ATTR_WHITE_VALUE] - await self._white_value_script.async_run( - common_params, context=self._context + await self.async_run_script( + self._white_value_script, + run_variables=common_params, + context=self._context, ) elif ATTR_EFFECT in kwargs and self._effect_script: effect = kwargs[ATTR_EFFECT] @@ -418,21 +422,26 @@ class LightTemplate(TemplateEntity, LightEntity): common_params["effect"] = effect - await self._effect_script.async_run(common_params, context=self._context) + await self.async_run_script( + self._effect_script, run_variables=common_params, context=self._context + ) elif ATTR_HS_COLOR in kwargs and self._color_script: hs_value = kwargs[ATTR_HS_COLOR] common_params["hs"] = hs_value common_params["h"] = int(hs_value[0]) common_params["s"] = int(hs_value[1]) - await self._color_script.async_run( - common_params, - context=self._context, + await self.async_run_script( + self._color_script, run_variables=common_params, context=self._context ) elif ATTR_BRIGHTNESS in kwargs and self._level_script: - await self._level_script.async_run(common_params, context=self._context) + await self.async_run_script( + self._level_script, run_variables=common_params, context=self._context + ) else: - await self._on_script.async_run(common_params, context=self._context) + await self.async_run_script( + self._on_script, run_variables=common_params, context=self._context + ) if optimistic_set: self.async_write_ha_state() @@ -440,11 +449,13 @@ class LightTemplate(TemplateEntity, LightEntity): async def async_turn_off(self, **kwargs): """Turn the light off.""" if ATTR_TRANSITION in kwargs and self._supports_transition is True: - await self._off_script.async_run( - {"transition": kwargs[ATTR_TRANSITION]}, context=self._context + await self.async_run_script( + self._off_script, + run_variables={"transition": kwargs[ATTR_TRANSITION]}, + context=self._context, ) else: - await self._off_script.async_run(context=self._context) + await self.async_run_script(self._off_script, context=self._context) if self._template is None: self._state = False self.async_write_ha_state() diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 394eb1cb9ba..ca03c6f5d82 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -36,12 +36,18 @@ _STATE_AVAILABILITY_BOOLEAN = "availability_boolean.state" OPTIMISTIC_ON_OFF_LIGHT_CONFIG = { "turn_on": { - "service": "light.turn_on", - "entity_id": "light.test_state", + "service": "test.automation", + "data_template": { + "action": "turn_on", + "caller": "{{ this.entity_id }}", + }, }, "turn_off": { - "service": "light.turn_off", - "entity_id": "light.test_state", + "service": "test.automation", + "data_template": { + "action": "turn_off", + "caller": "{{ this.entity_id }}", + }, }, } @@ -49,10 +55,38 @@ OPTIMISTIC_ON_OFF_LIGHT_CONFIG = { OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG = { **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, "set_level": { - "service": "light.turn_on", + "service": "test.automation", "data_template": { - "entity_id": "light.test_state", + "action": "set_level", "brightness": "{{brightness}}", + "caller": "{{ this.entity_id }}", + }, + }, +} + + +OPTIMISTIC_COLOR_TEMP_LIGHT_CONFIG = { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "set_temperature": { + "service": "test.automation", + "data_template": { + "action": "set_temperature", + "caller": "{{ this.entity_id }}", + "color_temp": "{{color_temp}}", + }, + }, +} + + +OPTIMISTIC_HS_COLOR_LIGHT_CONFIG = { + **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + "set_color": { + "service": "test.automation", + "data_template": { + "action": "set_color", + "caller": "{{ this.entity_id }}", + "s": "{{s}}", + "h": "{{h}}", }, }, } @@ -61,9 +95,10 @@ OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG = { OPTIMISTIC_WHITE_VALUE_LIGHT_CONFIG = { **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, "set_white_value": { - "service": "light.turn_on", + "service": "test.automation", "data_template": { - "entity_id": "light.test_state", + "action": "set_white_value", + "caller": "{{ this.entity_id }}", "white_value": "{{white_value}}", }, }, @@ -270,7 +305,6 @@ async def test_missing_key(hass, count, setup_light): "test_template_light": { **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, "value_template": "{{states.light.test_state.state}}", - "turn_on": {"service": "test.automation"}, } }, ], @@ -296,6 +330,8 @@ async def test_on_action( ) assert len(calls) == 1 + assert calls[-1].data["action"] == "turn_on" + assert calls[-1].data["caller"] == "light.test_template_light" assert state.state == STATE_OFF assert "color_mode" not in state.attributes @@ -377,7 +413,6 @@ async def test_on_action_with_transition( { "test_template_light": { **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, - "turn_on": {"service": "test.automation"}, } }, ], @@ -409,6 +444,8 @@ async def test_on_action_optimistic( state = hass.states.get("light.test_template_light") assert len(calls) == 1 + assert calls[-1].data["action"] == "turn_on" + assert calls[-1].data["caller"] == "light.test_template_light" assert state.state == STATE_ON assert state.attributes["color_mode"] == ColorMode.UNKNOWN # Brightness is None assert state.attributes["supported_color_modes"] == supported_color_modes @@ -422,7 +459,10 @@ async def test_on_action_optimistic( ) state = hass.states.get("light.test_template_light") - assert len(calls) == 1 + assert len(calls) == 2 + assert calls[-1].data["action"] == "set_level" + assert calls[-1].data["brightness"] == 100 + assert calls[-1].data["caller"] == "light.test_template_light" assert state.state == STATE_ON assert state.attributes["color_mode"] == expected_color_mode assert state.attributes["supported_color_modes"] == supported_color_modes @@ -441,7 +481,6 @@ async def test_on_action_optimistic( "test_template_light": { **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, "value_template": "{{states.light.test_state.state}}", - "turn_off": {"service": "test.automation"}, } }, ], @@ -467,6 +506,8 @@ async def test_off_action( ) assert len(calls) == 1 + assert calls[-1].data["action"] == "turn_off" + assert calls[-1].data["caller"] == "light.test_template_light" assert state.state == STATE_ON assert state.attributes["color_mode"] == ColorMode.UNKNOWN # Brightness is None assert state.attributes["supported_color_modes"] == supported_color_modes @@ -546,7 +587,6 @@ async def test_off_action_with_transition( { "test_template_light": { **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, - "turn_off": {"service": "test.automation"}, } }, ], @@ -586,15 +626,8 @@ async def test_off_action_optimistic( [ { "test_template_light": { - **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + **OPTIMISTIC_WHITE_VALUE_LIGHT_CONFIG, "value_template": "{{1 == 1}}", - "set_white_value": { - "service": "test.automation", - "data_template": { - "entity_id": "test.test_state", - "white_value": "{{white_value}}", - }, - }, } }, ], @@ -619,7 +652,9 @@ async def test_white_value_action_no_template( ) assert len(calls) == 1 - assert calls[0].data["white_value"] == 124 + assert calls[-1].data["action"] == "set_white_value" + assert calls[-1].data["caller"] == "light.test_template_light" + assert calls[-1].data["white_value"] == 124 state = hass.states.get("light.test_template_light") assert state.attributes.get("white_value") == 124 @@ -682,15 +717,8 @@ async def test_white_value_template( [ { "test_template_light": { - **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, "value_template": "{{1 == 1}}", - "set_level": { - "service": "test.automation", - "data_template": { - "entity_id": "test.test_state", - "brightness": "{{brightness}}", - }, - }, } }, ], @@ -715,7 +743,9 @@ async def test_level_action_no_template( ) assert len(calls) == 1 - assert calls[0].data["brightness"] == 124 + assert calls[-1].data["action"] == "set_level" + assert calls[-1].data["brightness"] == 124 + assert calls[-1].data["caller"] == "light.test_template_light" state = hass.states.get("light.test_template_light") assert state.state == STATE_ON @@ -757,15 +787,8 @@ async def test_level_template( """Test the template for the level.""" light_config = { "test_template_light": { - **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, "value_template": "{{ 1 == 1 }}", - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - }, - }, "level_template": level_template, } } @@ -806,15 +829,8 @@ async def test_temperature_template( """Test the template for the temperature.""" light_config = { "test_template_light": { - **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + **OPTIMISTIC_COLOR_TEMP_LIGHT_CONFIG, "value_template": "{{ 1 == 1 }}", - "set_temperature": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "color_temp": "{{color_temp}}", - }, - }, "temperature_template": temperature_template, } } @@ -837,15 +853,8 @@ async def test_temperature_template( [ { "test_template_light": { - **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + **OPTIMISTIC_COLOR_TEMP_LIGHT_CONFIG, "value_template": "{{1 == 1}}", - "set_temperature": { - "service": "test.automation", - "data_template": { - "entity_id": "test.test_state", - "color_temp": "{{color_temp}}", - }, - }, } }, ], @@ -870,7 +879,9 @@ async def test_temperature_action_no_template( ) assert len(calls) == 1 - assert calls[0].data["color_temp"] == 345 + assert calls[-1].data["action"] == "set_temperature" + assert calls[-1].data["caller"] == "light.test_template_light" + assert calls[-1].data["color_temp"] == 345 state = hass.states.get("light.test_template_light") assert state is not None @@ -971,18 +982,8 @@ async def test_entity_picture_template(hass, setup_light): [ { "test_template_light": { - **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + **OPTIMISTIC_HS_COLOR_LIGHT_CONFIG, "value_template": "{{1 == 1}}", - "set_color": [ - { - "service": "test.automation", - "data_template": { - "entity_id": "test.test_state", - "s": "{{s}}", - "h": "{{h}}", - }, - }, - ], } }, ], @@ -1007,8 +1008,10 @@ async def test_color_action_no_template( ) assert len(calls) == 1 - assert calls[0].data["h"] == 40 - assert calls[0].data["s"] == 50 + assert calls[-1].data["action"] == "set_color" + assert calls[-1].data["caller"] == "light.test_template_light" + assert calls[-1].data["h"] == 40 + assert calls[-1].data["s"] == 50 state = hass.states.get("light.test_template_light") assert state.state == STATE_ON @@ -1048,17 +1051,8 @@ async def test_color_template( """Test the template for the color.""" light_config = { "test_template_light": { - **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + **OPTIMISTIC_HS_COLOR_LIGHT_CONFIG, "value_template": "{{ 1 == 1 }}", - "set_color": [ - { - "service": "input_number.set_value", - "data_template": { - "entity_id": "input_number.h", - "color_temp": "{{h}}", - }, - } - ], "color_template": color_template, } } @@ -1192,18 +1186,13 @@ async def test_color_and_temperature_actions_no_template( [ { "test_template_light": { - **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, "value_template": "{{true}}", - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - }, - }, "set_effect": { "service": "test.automation", "data_template": { + "action": "set_effect", + "caller": "{{ this.entity_id }}", "entity_id": "test.test_state", "effect": "{{effect}}", }, @@ -1227,7 +1216,9 @@ async def test_effect_action_valid_effect(hass, setup_light, calls): ) assert len(calls) == 1 - assert calls[0].data["effect"] == "Disco" + assert calls[-1].data["action"] == "set_effect" + assert calls[-1].data["caller"] == "light.test_template_light" + assert calls[-1].data["effect"] == "Disco" state = hass.states.get("light.test_template_light") assert state is not None @@ -1240,15 +1231,8 @@ async def test_effect_action_valid_effect(hass, setup_light, calls): [ { "test_template_light": { - **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, + **OPTIMISTIC_BRIGHTNESS_LIGHT_CONFIG, "value_template": "{{true}}", - "set_level": { - "service": "light.turn_on", - "data_template": { - "entity_id": "light.test_state", - "brightness": "{{brightness}}", - }, - }, "set_effect": { "service": "test.automation", "data_template": { From 6f7a4653471909aa6e47ad58a3a5d651b37f35ad Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 13 May 2022 18:32:19 +0200 Subject: [PATCH 0471/3516] Support this variable in template vacuum actions (#71800) --- homeassistant/components/template/vacuum.py | 18 ++- tests/components/template/test_vacuum.py | 166 +++++++++++++++----- 2 files changed, 141 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 1d350d120c7..4b278ef6aec 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -204,42 +204,42 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): async def async_start(self): """Start or resume the cleaning task.""" - await self._start_script.async_run(context=self._context) + await self.async_run_script(self._start_script, context=self._context) async def async_pause(self): """Pause the cleaning task.""" if self._pause_script is None: return - await self._pause_script.async_run(context=self._context) + await self.async_run_script(self._pause_script, context=self._context) async def async_stop(self, **kwargs): """Stop the cleaning task.""" if self._stop_script is None: return - await self._stop_script.async_run(context=self._context) + await self.async_run_script(self._stop_script, context=self._context) async def async_return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" if self._return_to_base_script is None: return - await self._return_to_base_script.async_run(context=self._context) + await self.async_run_script(self._return_to_base_script, context=self._context) async def async_clean_spot(self, **kwargs): """Perform a spot clean-up.""" if self._clean_spot_script is None: return - await self._clean_spot_script.async_run(context=self._context) + await self.async_run_script(self._clean_spot_script, context=self._context) async def async_locate(self, **kwargs): """Locate the vacuum cleaner.""" if self._locate_script is None: return - await self._locate_script.async_run(context=self._context) + await self.async_run_script(self._locate_script, context=self._context) async def async_set_fan_speed(self, fan_speed, **kwargs): """Set fan speed.""" @@ -248,8 +248,10 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): if fan_speed in self._attr_fan_speed_list: self._attr_fan_speed = fan_speed - await self._set_fan_speed_script.async_run( - {ATTR_FAN_SPEED: fan_speed}, context=self._context + await self.async_run_script( + self._set_fan_speed_script, + run_variables={ATTR_FAN_SPEED: fan_speed}, + context=self._context, ) else: _LOGGER.error( diff --git a/tests/components/template/test_vacuum.py b/tests/components/template/test_vacuum.py index 1fd875f2df8..e454696d12a 100644 --- a/tests/components/template/test_vacuum.py +++ b/tests/components/template/test_vacuum.py @@ -342,7 +342,7 @@ async def test_unused_services(hass): _verify(hass, STATE_UNKNOWN, None) -async def test_state_services(hass): +async def test_state_services(hass, calls): """Test state services.""" await _register_components(hass) @@ -353,6 +353,9 @@ async def test_state_services(hass): # verify assert hass.states.get(_STATE_INPUT_SELECT).state == STATE_CLEANING _verify(hass, STATE_CLEANING, None) + assert len(calls) == 1 + assert calls[-1].data["action"] == "start" + assert calls[-1].data["caller"] == _TEST_VACUUM # Pause vacuum await common.async_pause(hass, _TEST_VACUUM) @@ -361,6 +364,9 @@ async def test_state_services(hass): # verify assert hass.states.get(_STATE_INPUT_SELECT).state == STATE_PAUSED _verify(hass, STATE_PAUSED, None) + assert len(calls) == 2 + assert calls[-1].data["action"] == "pause" + assert calls[-1].data["caller"] == _TEST_VACUUM # Stop vacuum await common.async_stop(hass, _TEST_VACUUM) @@ -369,6 +375,9 @@ async def test_state_services(hass): # verify assert hass.states.get(_STATE_INPUT_SELECT).state == STATE_IDLE _verify(hass, STATE_IDLE, None) + assert len(calls) == 3 + assert calls[-1].data["action"] == "stop" + assert calls[-1].data["caller"] == _TEST_VACUUM # Return vacuum to base await common.async_return_to_base(hass, _TEST_VACUUM) @@ -377,9 +386,12 @@ async def test_state_services(hass): # verify assert hass.states.get(_STATE_INPUT_SELECT).state == STATE_RETURNING _verify(hass, STATE_RETURNING, None) + assert len(calls) == 4 + assert calls[-1].data["action"] == "return_to_base" + assert calls[-1].data["caller"] == _TEST_VACUUM -async def test_clean_spot_service(hass): +async def test_clean_spot_service(hass, calls): """Test clean spot service.""" await _register_components(hass) @@ -389,9 +401,12 @@ async def test_clean_spot_service(hass): # verify assert hass.states.get(_SPOT_CLEANING_INPUT_BOOLEAN).state == STATE_ON + assert len(calls) == 1 + assert calls[-1].data["action"] == "clean_spot" + assert calls[-1].data["caller"] == _TEST_VACUUM -async def test_locate_service(hass): +async def test_locate_service(hass, calls): """Test locate service.""" await _register_components(hass) @@ -401,9 +416,12 @@ async def test_locate_service(hass): # verify assert hass.states.get(_LOCATING_INPUT_BOOLEAN).state == STATE_ON + assert len(calls) == 1 + assert calls[-1].data["action"] == "locate" + assert calls[-1].data["caller"] == _TEST_VACUUM -async def test_set_fan_speed(hass): +async def test_set_fan_speed(hass, calls): """Test set valid fan speed.""" await _register_components(hass) @@ -413,6 +431,10 @@ async def test_set_fan_speed(hass): # verify assert hass.states.get(_FAN_SPEED_INPUT_SELECT).state == "high" + assert len(calls) == 1 + assert calls[-1].data["action"] == "set_fan_speed" + assert calls[-1].data["caller"] == _TEST_VACUUM + assert calls[-1].data["option"] == "high" # Set fan's speed to medium await common.async_set_fan_speed(hass, "medium", _TEST_VACUUM) @@ -420,9 +442,13 @@ async def test_set_fan_speed(hass): # verify assert hass.states.get(_FAN_SPEED_INPUT_SELECT).state == "medium" + assert len(calls) == 2 + assert calls[-1].data["action"] == "set_fan_speed" + assert calls[-1].data["caller"] == _TEST_VACUUM + assert calls[-1].data["option"] == "medium" -async def test_set_invalid_fan_speed(hass): +async def test_set_invalid_fan_speed(hass, calls): """Test set invalid fan speed when fan has valid speed.""" await _register_components(hass) @@ -522,37 +548,107 @@ async def _register_components(hass): test_vacuum_config = { "value_template": "{{ states('input_select.state') }}", "fan_speed_template": "{{ states('input_select.fan_speed') }}", - "start": { - "service": "input_select.select_option", - "data": {"entity_id": _STATE_INPUT_SELECT, "option": STATE_CLEANING}, - }, - "pause": { - "service": "input_select.select_option", - "data": {"entity_id": _STATE_INPUT_SELECT, "option": STATE_PAUSED}, - }, - "stop": { - "service": "input_select.select_option", - "data": {"entity_id": _STATE_INPUT_SELECT, "option": STATE_IDLE}, - }, - "return_to_base": { - "service": "input_select.select_option", - "data": {"entity_id": _STATE_INPUT_SELECT, "option": STATE_RETURNING}, - }, - "clean_spot": { - "service": "input_boolean.turn_on", - "entity_id": _SPOT_CLEANING_INPUT_BOOLEAN, - }, - "locate": { - "service": "input_boolean.turn_on", - "entity_id": _LOCATING_INPUT_BOOLEAN, - }, - "set_fan_speed": { - "service": "input_select.select_option", - "data_template": { - "entity_id": _FAN_SPEED_INPUT_SELECT, - "option": "{{ fan_speed }}", + "start": [ + { + "service": "input_select.select_option", + "data": { + "entity_id": _STATE_INPUT_SELECT, + "option": STATE_CLEANING, + }, }, - }, + { + "service": "test.automation", + "data_template": { + "action": "start", + "caller": "{{ this.entity_id }}", + }, + }, + ], + "pause": [ + { + "service": "input_select.select_option", + "data": {"entity_id": _STATE_INPUT_SELECT, "option": STATE_PAUSED}, + }, + { + "service": "test.automation", + "data_template": { + "action": "pause", + "caller": "{{ this.entity_id }}", + }, + }, + ], + "stop": [ + { + "service": "input_select.select_option", + "data": {"entity_id": _STATE_INPUT_SELECT, "option": STATE_IDLE}, + }, + { + "service": "test.automation", + "data_template": { + "action": "stop", + "caller": "{{ this.entity_id }}", + }, + }, + ], + "return_to_base": [ + { + "service": "input_select.select_option", + "data": { + "entity_id": _STATE_INPUT_SELECT, + "option": STATE_RETURNING, + }, + }, + { + "service": "test.automation", + "data_template": { + "action": "return_to_base", + "caller": "{{ this.entity_id }}", + }, + }, + ], + "clean_spot": [ + { + "service": "input_boolean.turn_on", + "entity_id": _SPOT_CLEANING_INPUT_BOOLEAN, + }, + { + "service": "test.automation", + "data_template": { + "action": "clean_spot", + "caller": "{{ this.entity_id }}", + }, + }, + ], + "locate": [ + { + "service": "input_boolean.turn_on", + "entity_id": _LOCATING_INPUT_BOOLEAN, + }, + { + "service": "test.automation", + "data_template": { + "action": "locate", + "caller": "{{ this.entity_id }}", + }, + }, + ], + "set_fan_speed": [ + { + "service": "input_select.select_option", + "data_template": { + "entity_id": _FAN_SPEED_INPUT_SELECT, + "option": "{{ fan_speed }}", + }, + }, + { + "service": "test.automation", + "data_template": { + "action": "set_fan_speed", + "caller": "{{ this.entity_id }}", + "option": "{{ fan_speed }}", + }, + }, + ], "fan_speeds": ["low", "medium", "high"], "attribute_templates": { "test_attribute": "It {{ states.sensor.test_state.state }}." From adde9130a1d7db0ad9cc1c6054f07616bc1dc288 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 13 May 2022 18:32:45 +0200 Subject: [PATCH 0472/3516] Support this variable in template select actions (#71798) --- homeassistant/components/template/select.py | 6 +++-- tests/components/template/test_select.py | 30 ++++++++++++++++----- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/template/select.py b/homeassistant/components/template/select.py index fa2668c1e81..19f17096178 100644 --- a/homeassistant/components/template/select.py +++ b/homeassistant/components/template/select.py @@ -133,8 +133,10 @@ class TemplateSelect(TemplateEntity, SelectEntity): if self._optimistic: self._attr_current_option = option self.async_write_ha_state() - await self._command_select_option.async_run( - {ATTR_OPTION: option}, context=self._context + await self.async_run_script( + self._command_select_option, + run_variables={ATTR_OPTION: option}, + context=self._context, ) diff --git a/tests/components/template/test_select.py b/tests/components/template/test_select.py index de41ccedbb1..a58de63f186 100644 --- a/tests/components/template/test_select.py +++ b/tests/components/template/test_select.py @@ -131,7 +131,7 @@ async def test_missing_required_keys(hass): assert hass.states.async_all("select") == [] -async def test_templates_with_entities(hass): +async def test_templates_with_entities(hass, calls): """Test templates with values from other entities.""" with assert_setup_component(1, "input_select"): assert await setup.async_setup_component( @@ -158,13 +158,23 @@ async def test_templates_with_entities(hass): "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_template": { - "entity_id": _OPTION_INPUT_SELECT, - "option": "{{ option }}", + "select_option": [ + { + "service": "input_select.select_option", + "data_template": { + "entity_id": _OPTION_INPUT_SELECT, + "option": "{{ option }}", + }, }, - }, + { + "service": "test.automation", + "data_template": { + "action": "select_option", + "caller": "{{ this.entity_id }}", + "option": "{{ option }}", + }, + }, + ], "optimistic": True, "unique_id": "a", }, @@ -212,6 +222,12 @@ async def test_templates_with_entities(hass): ) _verify(hass, "c", ["a", "b", "c"]) + # Check this variable can be used in set_value script + assert len(calls) == 1 + assert calls[-1].data["action"] == "select_option" + assert calls[-1].data["caller"] == _TEST_SELECT + assert calls[-1].data["option"] == "c" + async def test_trigger_select(hass): """Test trigger based template select.""" From 66ec4564f4b5c07debaac4e164d5bcae3a7bfe81 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 13 May 2022 18:33:00 +0200 Subject: [PATCH 0473/3516] Support this variable in template number actions (#71797) --- homeassistant/components/template/number.py | 6 +++-- tests/components/template/test_number.py | 30 ++++++++++++++++----- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index 89adb45a6d0..54131990a26 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -157,8 +157,10 @@ class TemplateNumber(TemplateEntity, NumberEntity): if self._optimistic: self._attr_value = value self.async_write_ha_state() - await self._command_set_value.async_run( - {ATTR_VALUE: value}, context=self._context + await self.async_run_script( + self._command_set_value, + run_variables={ATTR_VALUE: value}, + context=self._context, ) diff --git a/tests/components/template/test_number.py b/tests/components/template/test_number.py index 6c47fdd2abc..ea29bb303df 100644 --- a/tests/components/template/test_number.py +++ b/tests/components/template/test_number.py @@ -127,7 +127,7 @@ async def test_all_optional_config(hass): _verify(hass, 4, 1, 3, 5) -async def test_templates_with_entities(hass): +async def test_templates_with_entities(hass, calls): """Test templates with values from other entities.""" with assert_setup_component(4, "input_number"): assert await setup.async_setup_component( @@ -173,13 +173,23 @@ async def test_templates_with_entities(hass): "step": f"{{{{ states('{_STEP_INPUT_NUMBER}') }}}}", "min": f"{{{{ states('{_MINIMUM_INPUT_NUMBER}') }}}}", "max": f"{{{{ states('{_MAXIMUM_INPUT_NUMBER}') }}}}", - "set_value": { - "service": "input_number.set_value", - "data_template": { - "entity_id": _VALUE_INPUT_NUMBER, - "value": "{{ value }}", + "set_value": [ + { + "service": "input_number.set_value", + "data_template": { + "entity_id": _VALUE_INPUT_NUMBER, + "value": "{{ value }}", + }, }, - }, + { + "service": "test.automation", + "data_template": { + "action": "set_value", + "caller": "{{ this.entity_id }}", + "value": "{{ value }}", + }, + }, + ], "optimistic": True, "unique_id": "a", }, @@ -247,6 +257,12 @@ async def test_templates_with_entities(hass): ) _verify(hass, 2, 2, 2, 6) + # Check this variable can be used in set_value script + assert len(calls) == 1 + assert calls[-1].data["action"] == "set_value" + assert calls[-1].data["caller"] == _TEST_NUMBER + assert calls[-1].data["value"] == 2 + async def test_trigger_number(hass): """Test trigger based template number.""" From 8b412acc9851e6197b333086f919d22b5e1782c3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 13 May 2022 18:33:25 +0200 Subject: [PATCH 0474/3516] Support this variable in template lock actions (#71796) --- homeassistant/components/template/lock.py | 4 +- tests/components/template/test_lock.py | 59 ++++++++--------------- 2 files changed, 21 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 78141c0d25e..1d94194be63 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -141,11 +141,11 @@ class TemplateLock(TemplateEntity, LockEntity): if self._optimistic: self._state = True self.async_write_ha_state() - await self._command_lock.async_run(context=self._context) + await self.async_run_script(self._command_lock, context=self._context) async def async_unlock(self, **kwargs): """Unlock the device.""" if self._optimistic: self._state = False self.async_write_ha_state() - await self._command_unlock.async_run(context=self._context) + await self.async_run_script(self._command_unlock, context=self._context) diff --git a/tests/components/template/test_lock.py b/tests/components/template/test_lock.py index e53f6660162..1ed5296f681 100644 --- a/tests/components/template/test_lock.py +++ b/tests/components/template/test_lock.py @@ -3,46 +3,23 @@ import pytest from homeassistant import setup from homeassistant.components import lock -from homeassistant.const import ( - ATTR_DOMAIN, - ATTR_ENTITY_ID, - ATTR_SERVICE_DATA, - EVENT_CALL_SERVICE, - STATE_OFF, - STATE_ON, - STATE_UNAVAILABLE, -) -from homeassistant.core import callback - - -@pytest.fixture -def service_calls(hass): - """Track service call events for switch.test_state.""" - events = [] - entity_id = "switch.test_state" - - @callback - def capture_events(event): - if event.data[ATTR_DOMAIN] != "switch": - return - if event.data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID] != [entity_id]: - return - events.append(event) - - hass.bus.async_listen(EVENT_CALL_SERVICE, capture_events) - - return events - +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE OPTIMISTIC_LOCK_CONFIG = { "platform": "template", "lock": { - "service": "switch.turn_on", - "entity_id": "switch.test_state", + "service": "test.automation", + "data_template": { + "action": "lock", + "caller": "{{ this.entity_id }}", + }, }, "unlock": { - "service": "switch.turn_off", - "entity_id": "switch.test_state", + "service": "test.automation", + "data_template": { + "action": "unlock", + "caller": "{{ this.entity_id }}", + }, }, } @@ -201,7 +178,7 @@ async def test_template_static(hass, start_ha): }, ], ) -async def test_lock_action(hass, start_ha, service_calls): +async def test_lock_action(hass, start_ha, calls): """Test lock action.""" await setup.async_setup_component(hass, "switch", {}) hass.states.async_set("switch.test_state", STATE_OFF) @@ -215,8 +192,9 @@ async def test_lock_action(hass, start_ha, service_calls): ) await hass.async_block_till_done() - assert len(service_calls) == 1 - assert service_calls[-1].data["service"] == "turn_on" + assert len(calls) == 1 + assert calls[0].data["action"] == "lock" + assert calls[0].data["caller"] == "lock.template_lock" @pytest.mark.parametrize("count,domain", [(1, lock.DOMAIN)]) @@ -231,7 +209,7 @@ async def test_lock_action(hass, start_ha, service_calls): }, ], ) -async def test_unlock_action(hass, start_ha, service_calls): +async def test_unlock_action(hass, start_ha, calls): """Test unlock action.""" await setup.async_setup_component(hass, "switch", {}) hass.states.async_set("switch.test_state", STATE_ON) @@ -245,8 +223,9 @@ async def test_unlock_action(hass, start_ha, service_calls): ) await hass.async_block_till_done() - assert len(service_calls) == 1 - assert service_calls[-1].data["service"] == "turn_off" + assert len(calls) == 1 + assert calls[0].data["action"] == "unlock" + assert calls[0].data["caller"] == "lock.template_lock" @pytest.mark.parametrize("count,domain", [(1, lock.DOMAIN)]) From a17fa6d6d530cdb74dd8d9eceee7054e17ef61ad Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 13 May 2022 18:33:44 +0200 Subject: [PATCH 0475/3516] Support this variable in template fan actions (#71795) --- homeassistant/components/template/fan.py | 31 ++-- tests/components/template/test_fan.py | 178 +++++++++++++++++------ 2 files changed, 152 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 6b0fdefc2f9..2c8d7247967 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -251,8 +251,9 @@ class TemplateFan(TemplateEntity, FanEntity): **kwargs, ) -> None: """Turn on the fan.""" - await self._on_script.async_run( - { + await self.async_run_script( + self._on_script, + run_variables={ ATTR_PERCENTAGE: percentage, ATTR_PRESET_MODE: preset_mode, }, @@ -267,7 +268,7 @@ class TemplateFan(TemplateEntity, FanEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the fan.""" - await self._off_script.async_run(context=self._context) + await self.async_run_script(self._off_script, context=self._context) self._state = STATE_OFF async def async_set_percentage(self, percentage: int) -> None: @@ -277,8 +278,10 @@ class TemplateFan(TemplateEntity, FanEntity): self._preset_mode = None if self._set_percentage_script: - await self._set_percentage_script.async_run( - {ATTR_PERCENTAGE: self._percentage}, context=self._context + await self.async_run_script( + self._set_percentage_script, + run_variables={ATTR_PERCENTAGE: self._percentage}, + context=self._context, ) async def async_set_preset_mode(self, preset_mode: str) -> None: @@ -297,8 +300,10 @@ class TemplateFan(TemplateEntity, FanEntity): self._percentage = None if self._set_preset_mode_script: - await self._set_preset_mode_script.async_run( - {ATTR_PRESET_MODE: self._preset_mode}, context=self._context + await self.async_run_script( + self._set_preset_mode_script, + run_variables={ATTR_PRESET_MODE: self._preset_mode}, + context=self._context, ) async def async_oscillate(self, oscillating: bool) -> None: @@ -307,8 +312,10 @@ class TemplateFan(TemplateEntity, FanEntity): return self._oscillating = oscillating - await self._set_oscillating_script.async_run( - {ATTR_OSCILLATING: oscillating}, context=self._context + await self.async_run_script( + self._set_oscillating_script, + run_variables={ATTR_OSCILLATING: self.oscillating}, + context=self._context, ) async def async_set_direction(self, direction: str) -> None: @@ -318,8 +325,10 @@ class TemplateFan(TemplateEntity, FanEntity): if direction in _VALID_DIRECTIONS: self._direction = direction - await self._set_direction_script.async_run( - {ATTR_DIRECTION: direction}, context=self._context + await self.async_run_script( + self._set_direction_script, + run_variables={ATTR_DIRECTION: direction}, + context=self._context, ) else: _LOGGER.error( diff --git a/tests/components/template/test_fan.py b/tests/components/template/test_fan.py index 30d5222024d..f7805ae41d6 100644 --- a/tests/components/template/test_fan.py +++ b/tests/components/template/test_fan.py @@ -383,20 +383,25 @@ async def test_invalid_availability_template_keeps_component_available( assert "x" in caplog_setup_text -async def test_on_off(hass): +async def test_on_off(hass, calls): """Test turn on and turn off.""" await _register_components(hass) + expected_calls = 0 - for func, state in [ - (common.async_turn_on, STATE_ON), - (common.async_turn_off, STATE_OFF), + for func, state, action in [ + (common.async_turn_on, STATE_ON, "turn_on"), + (common.async_turn_off, STATE_OFF, "turn_off"), ]: await func(hass, _TEST_FAN) assert hass.states.get(_STATE_INPUT_BOOLEAN).state == state _verify(hass, state, 0, None, None, None) + expected_calls += 1 + assert len(calls) == expected_calls + assert calls[-1].data["action"] == action + assert calls[-1].data["caller"] == _TEST_FAN -async def test_set_invalid_direction_from_initial_stage(hass): +async def test_set_invalid_direction_from_initial_stage(hass, calls): """Test set invalid direction when fan is in initial state.""" await _register_components(hass) @@ -407,29 +412,43 @@ async def test_set_invalid_direction_from_initial_stage(hass): _verify(hass, STATE_ON, 0, None, None, None) -async def test_set_osc(hass): +async def test_set_osc(hass, calls): """Test set oscillating.""" await _register_components(hass) + expected_calls = 0 await common.async_turn_on(hass, _TEST_FAN) + expected_calls += 1 for state in [True, False]: await common.async_oscillate(hass, _TEST_FAN, state) assert hass.states.get(_OSC_INPUT).state == str(state) _verify(hass, STATE_ON, 0, state, None, None) + expected_calls += 1 + assert len(calls) == expected_calls + assert calls[-1].data["action"] == "set_oscillating" + assert calls[-1].data["caller"] == _TEST_FAN + assert calls[-1].data["option"] == state -async def test_set_direction(hass): +async def test_set_direction(hass, calls): """Test set valid direction.""" await _register_components(hass) + expected_calls = 0 await common.async_turn_on(hass, _TEST_FAN) + expected_calls += 1 for cmd in [DIRECTION_FORWARD, DIRECTION_REVERSE]: await common.async_set_direction(hass, _TEST_FAN, cmd) assert hass.states.get(_DIRECTION_INPUT_SELECT).state == cmd _verify(hass, STATE_ON, 0, None, cmd, None) + expected_calls += 1 + assert len(calls) == expected_calls + assert calls[-1].data["action"] == "set_direction" + assert calls[-1].data["caller"] == _TEST_FAN + assert calls[-1].data["option"] == cmd -async def test_set_invalid_direction(hass): +async def test_set_invalid_direction(hass, calls): """Test set invalid direction when fan has valid direction.""" await _register_components(hass) @@ -440,30 +459,36 @@ async def test_set_invalid_direction(hass): _verify(hass, STATE_ON, 0, None, DIRECTION_FORWARD, None) -async def test_preset_modes(hass): +async def test_preset_modes(hass, calls): """Test preset_modes.""" await _register_components( hass, ["off", "low", "medium", "high", "auto", "smart"], ["auto", "smart"] ) await common.async_turn_on(hass, _TEST_FAN) - for extra, state in [ - ("auto", "auto"), - ("smart", "smart"), - ("invalid", "smart"), + for extra, state, expected_calls in [ + ("auto", "auto", 2), + ("smart", "smart", 3), + ("invalid", "smart", 3), ]: await common.async_set_preset_mode(hass, _TEST_FAN, extra) assert hass.states.get(_PRESET_MODE_INPUT_SELECT).state == state + assert len(calls) == expected_calls + assert calls[-1].data["action"] == "set_preset_mode" + assert calls[-1].data["caller"] == _TEST_FAN + assert calls[-1].data["option"] == state await common.async_turn_on(hass, _TEST_FAN, preset_mode="auto") assert hass.states.get(_PRESET_MODE_INPUT_SELECT).state == "auto" -async def test_set_percentage(hass): +async def test_set_percentage(hass, calls): """Test set valid speed percentage.""" await _register_components(hass) + expected_calls = 0 await common.async_turn_on(hass, _TEST_FAN) + expected_calls += 1 for state, value in [ (STATE_ON, 100), (STATE_ON, 66), @@ -472,13 +497,18 @@ async def test_set_percentage(hass): await common.async_set_percentage(hass, _TEST_FAN, value) assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == value _verify(hass, state, value, None, None, None) + expected_calls += 1 + assert len(calls) == expected_calls + assert calls[-1].data["action"] == "set_value" + assert calls[-1].data["caller"] == _TEST_FAN + assert calls[-1].data["value"] == value await common.async_turn_on(hass, _TEST_FAN, percentage=50) assert int(float(hass.states.get(_PERCENTAGE_INPUT_NUMBER).state)) == 50 _verify(hass, STATE_ON, 50, None, None, None) -async def test_increase_decrease_speed(hass): +async def test_increase_decrease_speed(hass, calls): """Test set valid increase and decrease speed.""" await _register_components(hass, speed_count=3) @@ -495,7 +525,7 @@ async def test_increase_decrease_speed(hass): _verify(hass, state, value, None, None, None) -async def test_increase_decrease_speed_default_speed_count(hass): +async def test_increase_decrease_speed_default_speed_count(hass, calls): """Test set valid increase and decrease speed.""" await _register_components(hass) @@ -512,7 +542,7 @@ async def test_increase_decrease_speed_default_speed_count(hass): _verify(hass, state, value, None, None, None) -async def test_set_invalid_osc_from_initial_state(hass): +async def test_set_invalid_osc_from_initial_state(hass, calls): """Test set invalid oscillating when fan is in initial state.""" await _register_components(hass) @@ -523,7 +553,7 @@ async def test_set_invalid_osc_from_initial_state(hass): _verify(hass, STATE_ON, 0, None, None, None) -async def test_set_invalid_osc(hass): +async def test_set_invalid_osc(hass, calls): """Test set invalid oscillating when fan has valid osc.""" await _register_components(hass) @@ -616,10 +646,19 @@ async def _register_components( "percentage_template": "{{ states('input_number.percentage') }}", "oscillating_template": "{{ states('input_select.osc') }}", "direction_template": "{{ states('input_select.direction') }}", - "turn_on": { - "service": "input_boolean.turn_on", - "entity_id": _STATE_INPUT_BOOLEAN, - }, + "turn_on": [ + { + "service": "input_boolean.turn_on", + "entity_id": _STATE_INPUT_BOOLEAN, + }, + { + "service": "test.automation", + "data_template": { + "action": "turn_on", + "caller": "{{ this.entity_id }}", + }, + }, + ], "turn_off": [ { "service": "input_boolean.turn_off", @@ -632,35 +671,82 @@ async def _register_components( "value": 0, }, }, + { + "service": "test.automation", + "data_template": { + "action": "turn_off", + "caller": "{{ this.entity_id }}", + }, + }, ], - "set_preset_mode": { - "service": "input_select.select_option", - "data_template": { - "entity_id": _PRESET_MODE_INPUT_SELECT, - "option": "{{ preset_mode }}", + "set_preset_mode": [ + { + "service": "input_select.select_option", + "data_template": { + "entity_id": _PRESET_MODE_INPUT_SELECT, + "option": "{{ preset_mode }}", + }, }, - }, - "set_percentage": { - "service": "input_number.set_value", - "data_template": { - "entity_id": _PERCENTAGE_INPUT_NUMBER, - "value": "{{ percentage }}", + { + "service": "test.automation", + "data_template": { + "action": "set_preset_mode", + "caller": "{{ this.entity_id }}", + "option": "{{ preset_mode }}", + }, }, - }, - "set_oscillating": { - "service": "input_select.select_option", - "data_template": { - "entity_id": _OSC_INPUT, - "option": "{{ oscillating }}", + ], + "set_percentage": [ + { + "service": "input_number.set_value", + "data_template": { + "entity_id": _PERCENTAGE_INPUT_NUMBER, + "value": "{{ percentage }}", + }, }, - }, - "set_direction": { - "service": "input_select.select_option", - "data_template": { - "entity_id": _DIRECTION_INPUT_SELECT, - "option": "{{ direction }}", + { + "service": "test.automation", + "data_template": { + "action": "set_value", + "caller": "{{ this.entity_id }}", + "value": "{{ percentage }}", + }, }, - }, + ], + "set_oscillating": [ + { + "service": "input_select.select_option", + "data_template": { + "entity_id": _OSC_INPUT, + "option": "{{ oscillating }}", + }, + }, + { + "service": "test.automation", + "data_template": { + "action": "set_oscillating", + "caller": "{{ this.entity_id }}", + "option": "{{ oscillating }}", + }, + }, + ], + "set_direction": [ + { + "service": "input_select.select_option", + "data_template": { + "entity_id": _DIRECTION_INPUT_SELECT, + "option": "{{ direction }}", + }, + }, + { + "service": "test.automation", + "data_template": { + "action": "set_direction", + "caller": "{{ this.entity_id }}", + "option": "{{ direction }}", + }, + }, + ], } if preset_modes: From 83080dbba8c87f43871d1c8f77166588c56f0663 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 13 May 2022 18:34:00 +0200 Subject: [PATCH 0476/3516] Support this variable in template cover actions (#71793) --- homeassistant/components/template/cover.py | 42 ++++--- tests/components/template/test_cover.py | 134 +++++++++++---------- 2 files changed, 95 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 7a20398d0b7..82c5cc2578c 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -323,10 +323,12 @@ class CoverTemplate(TemplateEntity, CoverEntity): async def async_open_cover(self, **kwargs): """Move the cover up.""" if self._open_script: - await self._open_script.async_run(context=self._context) + await self.async_run_script(self._open_script, context=self._context) elif self._position_script: - await self._position_script.async_run( - {"position": 100}, context=self._context + await self.async_run_script( + self._position_script, + run_variables={"position": 100}, + context=self._context, ) if self._optimistic: self._position = 100 @@ -335,10 +337,12 @@ class CoverTemplate(TemplateEntity, CoverEntity): async def async_close_cover(self, **kwargs): """Move the cover down.""" if self._close_script: - await self._close_script.async_run(context=self._context) + await self.async_run_script(self._close_script, context=self._context) elif self._position_script: - await self._position_script.async_run( - {"position": 0}, context=self._context + await self.async_run_script( + self._position_script, + run_variables={"position": 0}, + context=self._context, ) if self._optimistic: self._position = 0 @@ -347,13 +351,15 @@ class CoverTemplate(TemplateEntity, CoverEntity): async def async_stop_cover(self, **kwargs): """Fire the stop action.""" if self._stop_script: - await self._stop_script.async_run(context=self._context) + await self.async_run_script(self._stop_script, context=self._context) async def async_set_cover_position(self, **kwargs): """Set cover position.""" self._position = kwargs[ATTR_POSITION] - await self._position_script.async_run( - {"position": self._position}, context=self._context + await self.async_run_script( + self._position_script, + run_variables={"position": self._position}, + context=self._context, ) if self._optimistic: self.async_write_ha_state() @@ -361,8 +367,10 @@ class CoverTemplate(TemplateEntity, CoverEntity): async def async_open_cover_tilt(self, **kwargs): """Tilt the cover open.""" self._tilt_value = 100 - await self._tilt_script.async_run( - {"tilt": self._tilt_value}, context=self._context + await self.async_run_script( + self._tilt_script, + run_variables={"tilt": self._tilt_value}, + context=self._context, ) if self._tilt_optimistic: self.async_write_ha_state() @@ -370,8 +378,10 @@ class CoverTemplate(TemplateEntity, CoverEntity): async def async_close_cover_tilt(self, **kwargs): """Tilt the cover closed.""" self._tilt_value = 0 - await self._tilt_script.async_run( - {"tilt": self._tilt_value}, context=self._context + await self.async_run_script( + self._tilt_script, + run_variables={"tilt": self._tilt_value}, + context=self._context, ) if self._tilt_optimistic: self.async_write_ha_state() @@ -379,8 +389,10 @@ class CoverTemplate(TemplateEntity, CoverEntity): async def async_set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" self._tilt_value = kwargs[ATTR_TILT_POSITION] - await self._tilt_script.async_run( - {"tilt": self._tilt_value}, context=self._context + await self.async_run_script( + self._tilt_script, + run_variables={"tilt": self._tilt_value}, + context=self._context, ) if self._tilt_optimistic: self.async_write_ha_state() diff --git a/tests/components/template/test_cover.py b/tests/components/template/test_cover.py index 18db5482141..d08e8587703 100644 --- a/tests/components/template/test_cover.py +++ b/tests/components/template/test_cover.py @@ -4,10 +4,7 @@ import pytest from homeassistant import setup from homeassistant.components.cover import ATTR_POSITION, ATTR_TILT_POSITION, DOMAIN from homeassistant.const import ( - ATTR_DOMAIN, ATTR_ENTITY_ID, - ATTR_SERVICE_DATA, - EVENT_CALL_SERVICE, SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, SERVICE_OPEN_COVER, @@ -25,40 +22,26 @@ from homeassistant.const import ( STATE_OPENING, STATE_UNAVAILABLE, ) -from homeassistant.core import callback from tests.common import assert_setup_component ENTITY_COVER = "cover.test_template_cover" -@pytest.fixture -def service_calls(hass): - """Track service call events for cover.test_state.""" - events = [] - entity_id = "cover.test_state" - - @callback - def capture_events(event): - if event.data[ATTR_DOMAIN] != DOMAIN: - return - if event.data[ATTR_SERVICE_DATA][ATTR_ENTITY_ID] != [entity_id]: - return - events.append(event) - - hass.bus.async_listen(EVENT_CALL_SERVICE, capture_events) - - return events - - OPEN_CLOSE_COVER_CONFIG = { "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test_state", + "service": "test.automation", + "data_template": { + "action": "open_cover", + "caller": "{{ this.entity_id }}", + }, }, "close_cover": { - "service": "cover.close_cover", - "entity_id": "cover.test_state", + "service": "test.automation", + "data_template": { + "action": "close_cover", + "caller": "{{ this.entity_id }}", + }, }, } @@ -299,8 +282,11 @@ async def test_template_out_of_bounds(hass, start_ha): "test_template_cover": { "value_template": "{{ 1 == 1 }}", "open_cover": { - "service": "cover.open_cover", - "entity_id": "cover.test_state", + "service": "test.automation", + "data_template": { + "action": "open_cover", + "caller": "{{ this.entity_id }}", + }, }, } }, @@ -331,7 +317,7 @@ async def test_template_open_or_position(hass, start_ha, caplog_setup_text): }, ], ) -async def test_open_action(hass, start_ha, service_calls): +async def test_open_action(hass, start_ha, calls): """Test the open_cover command.""" state = hass.states.get("cover.test_template_cover") assert state.state == STATE_CLOSED @@ -341,8 +327,9 @@ async def test_open_action(hass, start_ha, service_calls): ) await hass.async_block_till_done() - assert len(service_calls) == 1 - assert service_calls[0].data["service"] == "open_cover" + assert len(calls) == 1 + assert calls[0].data["action"] == "open_cover" + assert calls[0].data["caller"] == "cover.test_template_cover" @pytest.mark.parametrize("count,domain", [(1, DOMAIN)]) @@ -357,8 +344,11 @@ async def test_open_action(hass, start_ha, service_calls): **OPEN_CLOSE_COVER_CONFIG, "position_template": "{{ 100 }}", "stop_cover": { - "service": "cover.stop_cover", - "entity_id": "cover.test_state", + "service": "test.automation", + "data_template": { + "action": "stop_cover", + "caller": "{{ this.entity_id }}", + }, }, } }, @@ -366,7 +356,7 @@ async def test_open_action(hass, start_ha, service_calls): }, ], ) -async def test_close_stop_action(hass, start_ha, service_calls): +async def test_close_stop_action(hass, start_ha, calls): """Test the close-cover and stop_cover commands.""" state = hass.states.get("cover.test_template_cover") assert state.state == STATE_OPEN @@ -381,9 +371,11 @@ async def test_close_stop_action(hass, start_ha, service_calls): ) await hass.async_block_till_done() - assert len(service_calls) == 2 - assert service_calls[0].data["service"] == "close_cover" - assert service_calls[1].data["service"] == "stop_cover" + assert len(calls) == 2 + assert calls[0].data["action"] == "close_cover" + assert calls[0].data["caller"] == "cover.test_template_cover" + assert calls[1].data["action"] == "stop_cover" + assert calls[1].data["caller"] == "cover.test_template_cover" @pytest.mark.parametrize("count,domain", [(1, "input_number")]) @@ -393,7 +385,7 @@ async def test_close_stop_action(hass, start_ha, service_calls): {"input_number": {"test": {"min": "0", "max": "100", "initial": "42"}}}, ], ) -async def test_set_position(hass, start_ha, service_calls): +async def test_set_position(hass, start_ha, calls): """Test the set_position command.""" with assert_setup_component(1, "cover"): assert await setup.async_setup_component( @@ -405,9 +397,12 @@ async def test_set_position(hass, start_ha, service_calls): "covers": { "test_template_cover": { "set_cover_position": { - "service": "cover.set_cover_position", - "entity_id": "cover.test_state", - "data_template": {"position": "{{ position }}"}, + "service": "test.automation", + "data_template": { + "action": "set_cover_position", + "caller": "{{ this.entity_id }}", + "position": "{{ position }}", + }, }, } }, @@ -430,9 +425,10 @@ async def test_set_position(hass, start_ha, service_calls): await hass.async_block_till_done() state = hass.states.get("cover.test_template_cover") assert state.attributes.get("current_position") == 100.0 - assert len(service_calls) == 1 - assert service_calls[-1].data["service"] == "set_cover_position" - assert service_calls[-1].data["service_data"]["position"] == 100 + assert len(calls) == 1 + assert calls[-1].data["action"] == "set_cover_position" + assert calls[-1].data["caller"] == "cover.test_template_cover" + assert calls[-1].data["position"] == 100 await hass.services.async_call( DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True @@ -440,9 +436,10 @@ async def test_set_position(hass, start_ha, service_calls): await hass.async_block_till_done() state = hass.states.get("cover.test_template_cover") assert state.attributes.get("current_position") == 0.0 - assert len(service_calls) == 2 - assert service_calls[-1].data["service"] == "set_cover_position" - assert service_calls[-1].data["service_data"]["position"] == 0 + assert len(calls) == 2 + assert calls[-1].data["action"] == "set_cover_position" + assert calls[-1].data["caller"] == "cover.test_template_cover" + assert calls[-1].data["position"] == 0 await hass.services.async_call( DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True @@ -450,9 +447,10 @@ async def test_set_position(hass, start_ha, service_calls): await hass.async_block_till_done() state = hass.states.get("cover.test_template_cover") assert state.attributes.get("current_position") == 100.0 - assert len(service_calls) == 3 - assert service_calls[-1].data["service"] == "set_cover_position" - assert service_calls[-1].data["service_data"]["position"] == 100 + assert len(calls) == 3 + assert calls[-1].data["action"] == "set_cover_position" + assert calls[-1].data["caller"] == "cover.test_template_cover" + assert calls[-1].data["position"] == 100 await hass.services.async_call( DOMAIN, SERVICE_TOGGLE, {ATTR_ENTITY_ID: ENTITY_COVER}, blocking=True @@ -460,9 +458,10 @@ async def test_set_position(hass, start_ha, service_calls): await hass.async_block_till_done() state = hass.states.get("cover.test_template_cover") assert state.attributes.get("current_position") == 0.0 - assert len(service_calls) == 4 - assert service_calls[-1].data["service"] == "set_cover_position" - assert service_calls[-1].data["service_data"]["position"] == 0 + assert len(calls) == 4 + assert calls[-1].data["action"] == "set_cover_position" + assert calls[-1].data["caller"] == "cover.test_template_cover" + assert calls[-1].data["position"] == 0 await hass.services.async_call( DOMAIN, @@ -473,9 +472,10 @@ async def test_set_position(hass, start_ha, service_calls): await hass.async_block_till_done() state = hass.states.get("cover.test_template_cover") assert state.attributes.get("current_position") == 25.0 - assert len(service_calls) == 5 - assert service_calls[-1].data["service"] == "set_cover_position" - assert service_calls[-1].data["service_data"]["position"] == 25 + assert len(calls) == 5 + assert calls[-1].data["action"] == "set_cover_position" + assert calls[-1].data["caller"] == "cover.test_template_cover" + assert calls[-1].data["position"] == 25 @pytest.mark.parametrize("count,domain", [(1, DOMAIN)]) @@ -489,9 +489,12 @@ async def test_set_position(hass, start_ha, service_calls): "test_template_cover": { **OPEN_CLOSE_COVER_CONFIG, "set_cover_tilt_position": { - "service": "cover.set_cover_tilt_position", - "entity_id": "cover.test_state", - "data_template": {"tilt_position": "{{ tilt }}"}, + "service": "test.automation", + "data_template": { + "action": "set_cover_tilt_position", + "caller": "{{ this.entity_id }}", + "tilt_position": "{{ tilt }}", + }, }, } }, @@ -511,9 +514,7 @@ async def test_set_position(hass, start_ha, service_calls): (SERVICE_CLOSE_COVER_TILT, {ATTR_ENTITY_ID: ENTITY_COVER}, 0), ], ) -async def test_set_tilt_position( - hass, service, attr, start_ha, service_calls, tilt_position -): +async def test_set_tilt_position(hass, service, attr, start_ha, calls, tilt_position): """Test the set_tilt_position command.""" await hass.services.async_call( DOMAIN, @@ -523,9 +524,10 @@ async def test_set_tilt_position( ) await hass.async_block_till_done() - assert len(service_calls) == 1 - assert service_calls[-1].data["service"] == "set_cover_tilt_position" - assert service_calls[-1].data["service_data"]["tilt_position"] == tilt_position + assert len(calls) == 1 + assert calls[-1].data["action"] == "set_cover_tilt_position" + assert calls[-1].data["caller"] == "cover.test_template_cover" + assert calls[-1].data["tilt_position"] == tilt_position @pytest.mark.parametrize("count,domain", [(1, DOMAIN)]) From 4885331509eeffe50f42d76b234996467b06170f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 13 May 2022 18:46:49 +0200 Subject: [PATCH 0477/3516] Fail template functions when no default specified (#71687) --- homeassistant/helpers/template.py | 90 +++---- .../fixtures/broken_configuration.yaml | 4 +- .../fixtures/sensor_configuration.yaml | 4 +- tests/helpers/test_event.py | 2 +- tests/helpers/test_template.py | 226 ++++++++++++++---- 5 files changed, 213 insertions(+), 113 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 3f084674a1b..d1ed9d06db9 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1325,21 +1325,12 @@ def utcnow(hass: HomeAssistant) -> datetime: return dt_util.utcnow() -def warn_no_default(function, value, default): +def raise_no_default(function, value): """Log warning if no default is specified.""" template, action = template_cv.get() or ("", "rendering or compiling") - _LOGGER.warning( - ( - "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 2022.6" - ), - function, - value, - action, - template, - function, - default, + raise ValueError( + f"Template error: {function} got invalid input '{value}' when {action} template '{template}' " + "but no default was specified" ) @@ -1361,8 +1352,7 @@ def forgiving_round(value, precision=0, method="common", default=_SENTINEL): except (ValueError, TypeError): # If value can't be converted to float if default is _SENTINEL: - warn_no_default("round", value, value) - return value + raise_no_default("round", value) return default @@ -1373,20 +1363,25 @@ def multiply(value, amount, default=_SENTINEL): except (ValueError, TypeError): # If value can't be converted to float if default is _SENTINEL: - warn_no_default("multiply", value, value) - return value + raise_no_default("multiply", value) return default def logarithm(value, base=math.e, default=_SENTINEL): """Filter and function to get logarithm of the value with a specific base.""" try: - return math.log(float(value), float(base)) + value_float = float(value) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("log", value, value) - return value + raise_no_default("log", value) return default + try: + base_float = float(base) + except (ValueError, TypeError): + if default is _SENTINEL: + raise_no_default("log", base) + return default + return math.log(value_float, base_float) def sine(value, default=_SENTINEL): @@ -1395,8 +1390,7 @@ def sine(value, default=_SENTINEL): return math.sin(float(value)) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("sin", value, value) - return value + raise_no_default("sin", value) return default @@ -1406,8 +1400,7 @@ def cosine(value, default=_SENTINEL): return math.cos(float(value)) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("cos", value, value) - return value + raise_no_default("cos", value) return default @@ -1417,8 +1410,7 @@ def tangent(value, default=_SENTINEL): return math.tan(float(value)) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("tan", value, value) - return value + raise_no_default("tan", value) return default @@ -1428,8 +1420,7 @@ def arc_sine(value, default=_SENTINEL): return math.asin(float(value)) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("asin", value, value) - return value + raise_no_default("asin", value) return default @@ -1439,8 +1430,7 @@ def arc_cosine(value, default=_SENTINEL): return math.acos(float(value)) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("acos", value, value) - return value + raise_no_default("acos", value) return default @@ -1450,8 +1440,7 @@ def arc_tangent(value, default=_SENTINEL): return math.atan(float(value)) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("atan", value, value) - return value + raise_no_default("atan", value) return default @@ -1474,8 +1463,7 @@ def arc_tangent2(*args, default=_SENTINEL): return math.atan2(float(args[0]), float(args[1])) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("atan2", args, args) - return args + raise_no_default("atan2", args) return default @@ -1485,8 +1473,7 @@ def square_root(value, default=_SENTINEL): return math.sqrt(float(value)) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("sqrt", value, value) - return value + raise_no_default("sqrt", value) return default @@ -1502,8 +1489,7 @@ def timestamp_custom(value, date_format=DATE_STR_FORMAT, local=True, default=_SE except (ValueError, TypeError): # If timestamp can't be converted if default is _SENTINEL: - warn_no_default("timestamp_custom", value, value) - return value + raise_no_default("timestamp_custom", value) return default @@ -1514,8 +1500,7 @@ def timestamp_local(value, default=_SENTINEL): except (ValueError, TypeError): # If timestamp can't be converted if default is _SENTINEL: - warn_no_default("timestamp_local", value, value) - return value + raise_no_default("timestamp_local", value) return default @@ -1526,8 +1511,7 @@ def timestamp_utc(value, default=_SENTINEL): except (ValueError, TypeError): # If timestamp can't be converted if default is _SENTINEL: - warn_no_default("timestamp_utc", value, value) - return value + raise_no_default("timestamp_utc", value) return default @@ -1537,8 +1521,7 @@ def forgiving_as_timestamp(value, default=_SENTINEL): return dt_util.as_timestamp(value) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("as_timestamp", value, None) - return None + raise_no_default("as_timestamp", value) return default @@ -1558,8 +1541,7 @@ def strptime(string, fmt, default=_SENTINEL): return datetime.strptime(string, fmt) except (ValueError, AttributeError, TypeError): if default is _SENTINEL: - warn_no_default("strptime", string, string) - return string + raise_no_default("strptime", string) return default @@ -1618,8 +1600,7 @@ def forgiving_float(value, default=_SENTINEL): return float(value) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("float", value, value) - return value + raise_no_default("float", value) return default @@ -1629,26 +1610,23 @@ def forgiving_float_filter(value, default=_SENTINEL): return float(value) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("float", value, 0) - return 0 + raise_no_default("float", value) return default def forgiving_int(value, default=_SENTINEL, base=10): - """Try to convert value to an int, and warn if it fails.""" + """Try to convert value to an int, and raise 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 + raise_no_default("int", value) return result def forgiving_int_filter(value, default=_SENTINEL, base=10): - """Try to convert value to an int, and warn if it fails.""" + """Try to convert value to an int, and raise 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 + raise_no_default("int", value) return result diff --git a/tests/components/template/fixtures/broken_configuration.yaml b/tests/components/template/fixtures/broken_configuration.yaml index de0c8aebd9d..00f773e2fd3 100644 --- a/tests/components/template/fixtures/broken_configuration.yaml +++ b/tests/components/template/fixtures/broken_configuration.yaml @@ -7,8 +7,8 @@ sensor: friendly_name: Combined Sense Energy Usage unit_of_measurement: kW value_template: - "{{ ((states('sensor.energy_usage') | float) + (states('sensor.energy_usage_2') - | float)) / 1000 }}" + "{{ ((states('sensor.energy_usage') | float(default=0)) + (states('sensor.energy_usage_2') + | float(default=0))) / 1000 }}" watching_tv_in_master_bedroom: friendly_name: Watching TV in Master Bedroom value_template: diff --git a/tests/components/template/fixtures/sensor_configuration.yaml b/tests/components/template/fixtures/sensor_configuration.yaml index 02c8cc373f4..f1afef26b87 100644 --- a/tests/components/template/fixtures/sensor_configuration.yaml +++ b/tests/components/template/fixtures/sensor_configuration.yaml @@ -14,8 +14,8 @@ sensor: friendly_name: Combined Sense Energy Usage unit_of_measurement: kW value_template: - "{{ ((states('sensor.energy_usage') | float) + (states('sensor.energy_usage_2') - | float)) / 1000 }}" + "{{ ((states('sensor.energy_usage') | float(default=0)) + (states('sensor.energy_usage_2') + | float(default=0))) / 1000 }}" watching_tv_in_master_bedroom: friendly_name: Watching TV in Master Bedroom value_template: diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 9b0a3e1abd5..d0355bba5a8 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -1026,7 +1026,7 @@ async def test_track_template_result_none(hass): template_condition = Template("{{state_attr('sensor.test', 'battery')}}", hass) template_condition_var = Template( - "{{(state_attr('sensor.test', 'battery')|int) + test }}", hass + "{{(state_attr('sensor.test', 'battery')|int(default=0)) + test }}", hass ) def specific_run_callback(event, updates): diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index d3a63460ec1..b2448bc1b5c 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -233,11 +233,11 @@ def test_float_function(hass): is True ) - assert ( + # Test handling of invalid input + with pytest.raises(TemplateError): template.Template("{{ float('forgiving') }}", hass).async_render() - == "forgiving" - ) + # Test handling of default return value assert render(hass, "{{ float('bad', 1) }}") == 1 assert render(hass, "{{ float('bad', default=1) }}") == 1 @@ -248,7 +248,12 @@ def test_float_filter(hass): assert render(hass, "{{ states.sensor.temperature.state | float }}") == 12.0 assert render(hass, "{{ states.sensor.temperature.state | float > 11 }}") is True - assert render(hass, "{{ 'bad' | float }}") == 0 + + # Test handling of invalid input + with pytest.raises(TemplateError): + render(hass, "{{ 'bad' | float }}") + + # Test handling of default return value assert render(hass, "{{ 'bad' | float(1) }}") == 1 assert render(hass, "{{ 'bad' | float(default=1) }}") == 1 @@ -262,7 +267,11 @@ 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, "{{ 'bad' | int }}") == 0 + # Test handling of invalid input + with pytest.raises(TemplateError): + render(hass, "{{ 'bad' | int }}") + + # Test handling of default return value assert render(hass, "{{ 'bad' | int(1) }}") == 1 assert render(hass, "{{ 'bad' | int(default=1) }}") == 1 @@ -276,7 +285,11 @@ 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('bad') }}") == "bad" + # Test handling of invalid input + with pytest.raises(TemplateError): + render(hass, "{{ int('bad') }}") + + # Test handling of default return value assert render(hass, "{{ int('bad', 1) }}") == 1 assert render(hass, "{{ int('bad', default=1) }}") == 1 @@ -364,12 +377,12 @@ def test_rounding_value(hass): def test_rounding_value_on_error(hass): """Test rounding value handling of error.""" - assert template.Template("{{ None | round }}", hass).async_render() is None + # Test handling of invalid input + with pytest.raises(TemplateError): + template.Template("{{ None | round }}", hass).async_render() - assert ( + with pytest.raises(TemplateError): template.Template('{{ "no_number" | round }}', hass).async_render() - == "no_number" - ) # Test handling of default return value assert render(hass, "{{ 'no_number' | round(default=1) }}") == 1 @@ -377,7 +390,7 @@ def test_rounding_value_on_error(hass): def test_multiply(hass): """Test multiply.""" - tests = {None: None, 10: 100, '"abcd"': "abcd"} + tests = {10: 100} for inp, out in tests.items(): assert ( @@ -387,6 +400,10 @@ def test_multiply(hass): == out ) + # Test handling of invalid input + with pytest.raises(TemplateError): + template.Template("{{ abcd | multiply(10) }}", hass).async_render() + # Test handling of default return value assert render(hass, "{{ 'no_number' | multiply(10, 1) }}") == 1 assert render(hass, "{{ 'no_number' | multiply(10, default=1) }}") == 1 @@ -397,9 +414,7 @@ def test_logarithm(hass): tests = [ (4, 2, 2.0), (1000, 10, 3.0), - (math.e, "", 1.0), - ('"invalid"', "_", "invalid"), - (10, '"invalid"', 10.0), + (math.e, "", 1.0), # The "" means the default base (e) will be used ] for value, base, expected in tests: @@ -417,6 +432,16 @@ def test_logarithm(hass): == expected ) + # Test handling of invalid input + with pytest.raises(TemplateError): + template.Template("{{ invalid | log(_) }}", hass).async_render() + with pytest.raises(TemplateError): + template.Template("{{ log(invalid, _) }}", hass).async_render() + with pytest.raises(TemplateError): + template.Template("{{ 10 | log(invalid) }}", hass).async_render() + with pytest.raises(TemplateError): + template.Template("{{ log(10, invalid) }}", hass).async_render() + # Test handling of default return value assert render(hass, "{{ 'no_number' | log(10, 1) }}") == 1 assert render(hass, "{{ 'no_number' | log(10, default=1) }}") == 1 @@ -432,7 +457,6 @@ def test_sine(hass): (math.pi, 0.0), (math.pi * 1.5, -1.0), (math.pi / 10, 0.309), - ('"duck"', "duck"), ] for value, expected in tests: @@ -442,6 +466,12 @@ def test_sine(hass): ) assert render(hass, f"{{{{ sin({value}) | round(3) }}}}") == expected + # Test handling of invalid input + with pytest.raises(TemplateError): + template.Template("{{ 'duck' | sin }}", hass).async_render() + with pytest.raises(TemplateError): + template.Template("{{ invalid | sin('duck') }}", hass).async_render() + # Test handling of default return value assert render(hass, "{{ 'no_number' | sin(1) }}") == 1 assert render(hass, "{{ 'no_number' | sin(default=1) }}") == 1 @@ -457,7 +487,6 @@ def test_cos(hass): (math.pi, -1.0), (math.pi * 1.5, -0.0), (math.pi / 10, 0.951), - ("'error'", "error"), ] for value, expected in tests: @@ -467,11 +496,17 @@ def test_cos(hass): ) assert render(hass, f"{{{{ cos({value}) | round(3) }}}}") == expected + # Test handling of invalid input + with pytest.raises(TemplateError): + template.Template("{{ 'error' | cos }}", hass).async_render() + with pytest.raises(TemplateError): + template.Template("{{ invalid | cos('error') }}", hass).async_render() + # Test handling of default return value - assert render(hass, "{{ 'no_number' | sin(1) }}") == 1 - assert render(hass, "{{ 'no_number' | sin(default=1) }}") == 1 - assert render(hass, "{{ sin('no_number', 1) }}") == 1 - assert render(hass, "{{ sin('no_number', default=1) }}") == 1 + assert render(hass, "{{ 'no_number' | cos(1) }}") == 1 + assert render(hass, "{{ 'no_number' | cos(default=1) }}") == 1 + assert render(hass, "{{ cos('no_number', 1) }}") == 1 + assert render(hass, "{{ cos('no_number', default=1) }}") == 1 def test_tan(hass): @@ -482,7 +517,6 @@ def test_tan(hass): (math.pi / 180 * 45, 1.0), (math.pi / 180 * 90, "1.633123935319537e+16"), (math.pi / 180 * 135, -1.0), - ("'error'", "error"), ] for value, expected in tests: @@ -492,6 +526,12 @@ def test_tan(hass): ) assert render(hass, f"{{{{ tan({value}) | round(3) }}}}") == expected + # Test handling of invalid input + with pytest.raises(TemplateError): + template.Template("{{ 'error' | tan }}", hass).async_render() + with pytest.raises(TemplateError): + template.Template("{{ invalid | tan('error') }}", hass).async_render() + # Test handling of default return value assert render(hass, "{{ 'no_number' | tan(1) }}") == 1 assert render(hass, "{{ 'no_number' | tan(default=1) }}") == 1 @@ -507,7 +547,6 @@ def test_sqrt(hass): (2, 1.414), (10, 3.162), (100, 10.0), - ("'error'", "error"), ] for value, expected in tests: @@ -517,6 +556,12 @@ def test_sqrt(hass): ) assert render(hass, f"{{{{ sqrt({value}) | round(3) }}}}") == expected + # Test handling of invalid input + with pytest.raises(TemplateError): + template.Template("{{ 'error' | sqrt }}", hass).async_render() + with pytest.raises(TemplateError): + template.Template("{{ invalid | sqrt('error') }}", hass).async_render() + # Test handling of default return value assert render(hass, "{{ 'no_number' | sqrt(1) }}") == 1 assert render(hass, "{{ 'no_number' | sqrt(default=1) }}") == 1 @@ -527,14 +572,11 @@ def test_sqrt(hass): def test_arc_sine(hass): """Test arcus sine.""" tests = [ - (-2.0, -2.0), # value error (-1.0, -1.571), (-0.5, -0.524), (0.0, 0.0), (0.5, 0.524), (1.0, 1.571), - (2.0, 2.0), # value error - ('"error"', "error"), ] for value, expected in tests: @@ -544,6 +586,19 @@ def test_arc_sine(hass): ) assert render(hass, f"{{{{ asin({value}) | round(3) }}}}") == expected + # Test handling of invalid input + invalid_tests = [ + -2.0, # value error + 2.0, # value error + '"error"', + ] + + for value in invalid_tests: + with pytest.raises(TemplateError): + template.Template("{{ %s | asin | round(3) }}" % value, hass).async_render() + with pytest.raises(TemplateError): + assert render(hass, f"{{{{ asin({value}) | round(3) }}}}") + # Test handling of default return value assert render(hass, "{{ 'no_number' | asin(1) }}") == 1 assert render(hass, "{{ 'no_number' | asin(default=1) }}") == 1 @@ -554,14 +609,11 @@ def test_arc_sine(hass): def test_arc_cos(hass): """Test arcus cosine.""" tests = [ - (-2.0, -2.0), # value error (-1.0, 3.142), (-0.5, 2.094), (0.0, 1.571), (0.5, 1.047), (1.0, 0.0), - (2.0, 2.0), # value error - ('"error"', "error"), ] for value, expected in tests: @@ -571,6 +623,19 @@ def test_arc_cos(hass): ) assert render(hass, f"{{{{ acos({value}) | round(3) }}}}") == expected + # Test handling of invalid input + invalid_tests = [ + -2.0, # value error + 2.0, # value error + '"error"', + ] + + for value in invalid_tests: + with pytest.raises(TemplateError): + template.Template("{{ %s | acos | round(3) }}" % value, hass).async_render() + with pytest.raises(TemplateError): + assert render(hass, f"{{{{ acos({value}) | round(3) }}}}") + # Test handling of default return value assert render(hass, "{{ 'no_number' | acos(1) }}") == 1 assert render(hass, "{{ 'no_number' | acos(default=1) }}") == 1 @@ -590,7 +655,6 @@ def test_arc_tan(hass): (1.0, 0.785), (2.0, 1.107), (10.0, 1.471), - ('"error"', "error"), ] for value, expected in tests: @@ -600,6 +664,12 @@ def test_arc_tan(hass): ) assert render(hass, f"{{{{ atan({value}) | round(3) }}}}") == expected + # Test handling of invalid input + with pytest.raises(TemplateError): + template.Template("{{ 'error' | atan }}", hass).async_render() + with pytest.raises(TemplateError): + template.Template("{{ invalid | atan('error') }}", hass).async_render() + # Test handling of default return value assert render(hass, "{{ 'no_number' | atan(1) }}") == 1 assert render(hass, "{{ 'no_number' | atan(default=1) }}") == 1 @@ -622,7 +692,6 @@ def test_arc_tan2(hass): (-4.0, 3.0, -0.927), (-1.0, 2.0, -0.464), (2.0, 1.0, 1.107), - ('"duck"', '"goose"', ("duck", "goose")), ] for y, x, expected in tests: @@ -639,6 +708,12 @@ def test_arc_tan2(hass): == expected ) + # Test handling of invalid input + with pytest.raises(TemplateError): + template.Template("{{ ('duck', 'goose') | atan2 }}", hass).async_render() + with pytest.raises(TemplateError): + template.Template("{{ atan2('duck', 'goose') }}", hass).async_render() + # Test handling of default return value assert render(hass, "{{ ('duck', 'goose') | atan2(1) }}") == 1 assert render(hass, "{{ ('duck', 'goose') | atan2(default=1) }}") == 1 @@ -655,8 +730,6 @@ def test_strptime(hass): ("2016-10-19", "%Y-%m-%d", None), ("2016", "%Y", None), ("15:22:05", "%H:%M:%S", None), - ("1469119144", "%Y", 1469119144), - ("invalid", "%Y", "invalid"), ] for inp, fmt, expected in tests: @@ -667,6 +740,18 @@ def test_strptime(hass): assert template.Template(temp, hass).async_render() == expected + # Test handling of invalid input + invalid_tests = [ + ("1469119144", "%Y"), + ("invalid", "%Y"), + ] + + for inp, fmt in invalid_tests: + temp = f"{{{{ strptime('{inp}', '{fmt}') }}}}" + + with pytest.raises(TemplateError): + template.Template(temp, hass).async_render() + # Test handling of default return value assert render(hass, "{{ strptime('invalid', '%Y', 1) }}") == 1 assert render(hass, "{{ strptime('invalid', '%Y', default=1) }}") == 1 @@ -677,7 +762,6 @@ def test_timestamp_custom(hass): hass.config.set_time_zone("UTC") now = dt_util.utcnow() tests = [ - (None, None, None, None), (1469119144, None, True, "2016-07-21 16:39:04"), (1469119144, "%Y", True, 2016), (1469119144, "invalid", True, "invalid"), @@ -694,6 +778,22 @@ def test_timestamp_custom(hass): assert template.Template(f"{{{{ {inp} | {fil} }}}}", hass).async_render() == out + # Test handling of invalid input + invalid_tests = [ + (None, None, None), + ] + + for inp, fmt, local in invalid_tests: + if fmt: + fil = f"timestamp_custom('{fmt}')" + elif fmt and local: + fil = f"timestamp_custom('{fmt}', {local})" + else: + fil = "timestamp_custom" + + with pytest.raises(TemplateError): + template.Template(f"{{{{ {inp} | {fil} }}}}", hass).async_render() + # Test handling of default return value assert render(hass, "{{ None | timestamp_custom('invalid', True, 1) }}") == 1 assert render(hass, "{{ None | timestamp_custom(default=1) }}") == 1 @@ -702,14 +802,25 @@ def test_timestamp_custom(hass): def test_timestamp_local(hass): """Test the timestamps to local filter.""" hass.config.set_time_zone("UTC") - tests = {None: None, 1469119144: "2016-07-21T16:39:04+00:00"} + tests = [ + (1469119144, "2016-07-21T16:39:04+00:00"), + ] - for inp, out in tests.items(): + for inp, out in tests: assert ( template.Template("{{ %s | timestamp_local }}" % inp, hass).async_render() == out ) + # Test handling of invalid input + invalid_tests = [ + None, + ] + + for inp in invalid_tests: + with pytest.raises(TemplateError): + template.Template("{{ %s | timestamp_local }}" % inp, hass).async_render() + # Test handling of default return value assert render(hass, "{{ None | timestamp_local(1) }}") == 1 assert render(hass, "{{ None | timestamp_local(default=1) }}") == 1 @@ -1002,18 +1113,26 @@ def test_ordinal(hass): def test_timestamp_utc(hass): """Test the timestamps to local filter.""" now = dt_util.utcnow() - tests = { - None: None, - 1469119144: "2016-07-21T16:39:04+00:00", - dt_util.as_timestamp(now): now.isoformat(), - } + tests = [ + (1469119144, "2016-07-21T16:39:04+00:00"), + (dt_util.as_timestamp(now), now.isoformat()), + ] - for inp, out in tests.items(): + for inp, out in tests: assert ( template.Template("{{ %s | timestamp_utc }}" % inp, hass).async_render() == out ) + # Test handling of invalid input + invalid_tests = [ + None, + ] + + for inp in invalid_tests: + with pytest.raises(TemplateError): + template.Template("{{ %s | timestamp_utc }}" % inp, hass).async_render() + # Test handling of default return value assert render(hass, "{{ None | timestamp_utc(1) }}") == 1 assert render(hass, "{{ None | timestamp_utc(default=1) }}") == 1 @@ -1021,14 +1140,12 @@ def test_timestamp_utc(hass): def test_as_timestamp(hass): """Test the as_timestamp function.""" - assert ( - template.Template('{{ as_timestamp("invalid") }}', hass).async_render() is None - ) - hass.mock = None - assert ( - template.Template("{{ as_timestamp(states.mock) }}", hass).async_render() - is None - ) + with pytest.raises(TemplateError): + template.Template('{{ as_timestamp("invalid") }}', hass).async_render() + + hass.states.async_set("test.object", None) + with pytest.raises(TemplateError): + template.Template("{{ as_timestamp(states.test.object) }}", hass).async_render() tpl = ( '{{ as_timestamp(strptime("2024-02-03T09:10:24+0000", ' @@ -1822,7 +1939,8 @@ def test_distance_function_return_none_if_invalid_state(hass): """Test distance function return None if invalid state.""" hass.states.async_set("test.object_2", "happy", {"latitude": 10}) tpl = template.Template("{{ distance(states.test.object_2) | round }}", hass) - assert tpl.async_render() is None + with pytest.raises(TemplateError): + tpl.async_render() def test_distance_function_return_none_if_invalid_coord(hass): @@ -3359,7 +3477,11 @@ async def test_protected_blocked(hass): async def test_demo_template(hass): """Test the demo template works as expected.""" - hass.states.async_set("sun.sun", "above", {"elevation": 50, "next_rising": "later"}) + hass.states.async_set( + "sun.sun", + "above", + {"elevation": 50, "next_rising": "2022-05-12T03:00:08.503651+00:00"}, + ) for i in range(2): hass.states.async_set(f"sensor.sensor{i}", "on") @@ -3375,7 +3497,7 @@ The temperature is {{ my_test_json.temperature }} {{ my_test_json.unit }}. {% if is_state("sun.sun", "above_horizon") -%} The sun rose {{ relative_time(states.sun.sun.last_changed) }} ago. {%- else -%} - The sun will rise at {{ as_timestamp(strptime(state_attr("sun.sun", "next_rising"), "")) | timestamp_local }}. + The sun will rise at {{ as_timestamp(state_attr("sun.sun", "next_rising")) | timestamp_local }}. {%- endif %} For loop example getting 3 entity values: From 9bd508c0bfc00405cb6c4e17f3378f8edce112ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 May 2022 13:17:54 -0400 Subject: [PATCH 0478/3516] Generate json for history and logbook websocket responses in the executor (#71813) --- homeassistant/components/history/__init__.py | 129 ++++++++++++++----- homeassistant/components/logbook/__init__.py | 55 ++++++-- 2 files changed, 138 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index a0d1f2fa76b..17b983e11c1 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -1,12 +1,12 @@ """Provide pre-made queries on top of the recorder component.""" from __future__ import annotations -from collections.abc import Iterable, MutableMapping +from collections.abc import Iterable from datetime import datetime as dt, timedelta from http import HTTPStatus import logging import time -from typing import Any, cast +from typing import Any, Literal, cast from aiohttp import web from sqlalchemy import not_, or_ @@ -24,8 +24,10 @@ from homeassistant.components.recorder.statistics import ( statistics_during_period, ) from homeassistant.components.recorder.util import session_scope +from homeassistant.components.websocket_api import messages +from homeassistant.components.websocket_api.const import JSON_DUMP from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE -from homeassistant.core import HomeAssistant, State +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.deprecation import deprecated_class, deprecated_function from homeassistant.helpers.entityfilter import ( @@ -104,6 +106,23 @@ class LazyState(history_models.LazyState): """A lazy version of core State.""" +def _ws_get_statistics_during_period( + hass: HomeAssistant, + msg_id: int, + start_time: dt, + end_time: dt | None = None, + statistic_ids: list[str] | None = None, + period: Literal["5minute", "day", "hour", "month"] = "hour", +) -> str: + """Fetch statistics and convert them to json in the executor.""" + return JSON_DUMP( + messages.result_message( + msg_id, + statistics_during_period(hass, start_time, end_time, statistic_ids, period), + ) + ) + + @websocket_api.websocket_command( { vol.Required("type"): "history/statistics_during_period", @@ -136,15 +155,28 @@ async def ws_get_statistics_during_period( else: end_time = None - statistics = await get_instance(hass).async_add_executor_job( - statistics_during_period, - hass, - start_time, - end_time, - msg.get("statistic_ids"), - msg.get("period"), + connection.send_message( + await get_instance(hass).async_add_executor_job( + _ws_get_statistics_during_period, + hass, + msg["id"], + start_time, + end_time, + msg.get("statistic_ids"), + msg.get("period"), + ) + ) + + +def _ws_get_list_statistic_ids( + hass: HomeAssistant, + msg_id: int, + statistic_type: Literal["mean"] | Literal["sum"] | None = None, +) -> str: + """Fetch a list of available statistic_id and convert them to json in the executor.""" + return JSON_DUMP( + messages.result_message(msg_id, list_statistic_ids(hass, None, statistic_type)) ) - connection.send_result(msg["id"], statistics) @websocket_api.websocket_command( @@ -158,13 +190,46 @@ async def ws_get_list_statistic_ids( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Fetch a list of available statistic_id.""" - statistic_ids = await get_instance(hass).async_add_executor_job( - list_statistic_ids, - hass, - None, - msg.get("statistic_type"), + connection.send_message( + await get_instance(hass).async_add_executor_job( + _ws_get_list_statistic_ids, + hass, + msg["id"], + msg.get("statistic_type"), + ) + ) + + +def _ws_get_significant_states( + hass: HomeAssistant, + msg_id: int, + start_time: dt, + end_time: dt | None = None, + entity_ids: list[str] | None = None, + filters: Any | None = None, + include_start_time_state: bool = True, + significant_changes_only: bool = True, + minimal_response: bool = False, + no_attributes: bool = False, +) -> str: + """Fetch history significant_states and convert them to json in the executor.""" + return JSON_DUMP( + messages.result_message( + msg_id, + history.get_significant_states( + hass, + start_time, + end_time, + entity_ids, + filters, + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + True, + ), + ) ) - connection.send_result(msg["id"], statistic_ids) @websocket_api.websocket_command( @@ -220,24 +285,22 @@ async def ws_get_history_during_period( significant_changes_only = msg["significant_changes_only"] no_attributes = msg["no_attributes"] minimal_response = msg["minimal_response"] - compressed_state_format = True - history_during_period: MutableMapping[ - str, list[State | dict[str, Any]] - ] = await get_instance(hass).async_add_executor_job( - history.get_significant_states, - hass, - start_time, - end_time, - entity_ids, - hass.data[HISTORY_FILTERS], - include_start_time_state, - significant_changes_only, - minimal_response, - no_attributes, - compressed_state_format, + connection.send_message( + await get_instance(hass).async_add_executor_job( + _ws_get_significant_states, + hass, + msg["id"], + start_time, + end_time, + entity_ids, + hass.data[HISTORY_FILTERS], + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + ) ) - connection.send_result(msg["id"], history_during_period) class HistoryPeriodView(HomeAssistantView): diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 592831badf1..4b40e398f6e 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -30,6 +30,8 @@ from homeassistant.components.recorder.models import ( from homeassistant.components.recorder.util import session_scope from homeassistant.components.script import EVENT_SCRIPT_STARTED from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN +from homeassistant.components.websocket_api import messages +from homeassistant.components.websocket_api.const import JSON_DUMP from homeassistant.const import ( ATTR_DOMAIN, ATTR_ENTITY_ID, @@ -210,6 +212,34 @@ async def _process_logbook_platform( platform.async_describe_events(hass, _async_describe_event) +def _ws_formatted_get_events( + hass: HomeAssistant, + msg_id: int, + start_day: dt, + end_day: dt, + entity_ids: list[str] | None = None, + filters: Filters | None = None, + entities_filter: EntityFilter | Callable[[str], bool] | None = None, + context_id: str | None = None, +) -> str: + """Fetch events and convert them to json in the executor.""" + return JSON_DUMP( + messages.result_message( + msg_id, + _get_events( + hass, + start_day, + end_day, + entity_ids, + filters, + entities_filter, + context_id, + True, + ), + ) + ) + + @websocket_api.websocket_command( { vol.Required("type"): "logbook/get_events", @@ -249,20 +279,19 @@ async def ws_get_events( entity_ids = msg.get("entity_ids") context_id = msg.get("context_id") - logbook_events: list[dict[str, Any]] = await get_instance( - hass - ).async_add_executor_job( - _get_events, - hass, - start_time, - end_time, - entity_ids, - hass.data[LOGBOOK_FILTERS], - hass.data[LOGBOOK_ENTITIES_FILTER], - context_id, - True, + connection.send_message( + await get_instance(hass).async_add_executor_job( + _ws_formatted_get_events, + hass, + msg["id"], + start_time, + end_time, + entity_ids, + hass.data[LOGBOOK_FILTERS], + hass.data[LOGBOOK_ENTITIES_FILTER], + context_id, + ) ) - connection.send_result(msg["id"], logbook_events) class LogbookView(HomeAssistantView): From 807df530bc9713099f6eb015255f4591dd45fa74 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Fri, 13 May 2022 18:27:08 +0100 Subject: [PATCH 0479/3516] Add diagnostics file export to generic camera (#71492) --- .../components/generic/diagnostics.py | 49 +++++++++++++++++++ tests/components/generic/conftest.py | 35 +++++++++++++ tests/components/generic/test_diagnostics.py | 24 +++++++++ 3 files changed, 108 insertions(+) create mode 100644 homeassistant/components/generic/diagnostics.py create mode 100644 tests/components/generic/test_diagnostics.py diff --git a/homeassistant/components/generic/diagnostics.py b/homeassistant/components/generic/diagnostics.py new file mode 100644 index 00000000000..00be287f053 --- /dev/null +++ b/homeassistant/components/generic/diagnostics.py @@ -0,0 +1,49 @@ +"""Diagnostics support for generic (IP camera).""" +from __future__ import annotations + +from typing import Any + +import yarl + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from .const import CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE + +TO_REDACT = { + CONF_PASSWORD, + CONF_USERNAME, +} + + +# A very similar redact function is in components.sql. Possible to be made common. +def redact_url(data: str) -> str: + """Redact credentials from string url.""" + url_in = yarl.URL(data) + if url_in.user: + url = url_in.with_user("****") + if url_in.password: + url = url.with_password("****") + if url_in.path: + url = url.with_path("****") + if url_in.query_string: + url = url.with_query("****=****") + return str(url) + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + options = async_redact_data(entry.options, TO_REDACT) + for key in (CONF_STREAM_SOURCE, CONF_STILL_IMAGE_URL): + if (value := options.get(key)) is not None: + options[key] = redact_url(value) + + return { + "title": entry.title, + "data": async_redact_data(entry.data, TO_REDACT), + "options": options, + } diff --git a/tests/components/generic/conftest.py b/tests/components/generic/conftest.py index 9daa3574e6e..dc5c545869b 100644 --- a/tests/components/generic/conftest.py +++ b/tests/components/generic/conftest.py @@ -10,6 +10,8 @@ import respx from homeassistant import config_entries, setup from homeassistant.components.generic.const import DOMAIN +from tests.common import MockConfigEntry + @pytest.fixture(scope="package") def fakeimgbytes_png(): @@ -79,3 +81,36 @@ async def user_flow(hass): assert result["errors"] == {} return result + + +@pytest.fixture(name="config_entry") +def config_entry_fixture(hass): + """Define a config entry fixture.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Test Camera", + unique_id="abc123", + data={}, + options={ + "still_image_url": "http://joebloggs:letmein1@example.com/secret1/file.jpg?pw=qwerty", + "stream_source": "http://janebloggs:letmein2@example.com/stream", + "username": "johnbloggs", + "password": "letmein123", + "limit_refetch_to_url_change": False, + "authentication": "basic", + "framerate": 2.0, + "verify_ssl": True, + "content_type": "image/jpeg", + }, + version=1, + ) + entry.add_to_hass(hass) + return entry + + +@pytest.fixture +async def setup_entry(hass, config_entry): + """Set up a config entry ready to be used in tests.""" + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + return config_entry diff --git a/tests/components/generic/test_diagnostics.py b/tests/components/generic/test_diagnostics.py new file mode 100644 index 00000000000..2d4e4c536d8 --- /dev/null +++ b/tests/components/generic/test_diagnostics.py @@ -0,0 +1,24 @@ +"""Test generic (IP camera) diagnostics.""" +from homeassistant.components.diagnostics import REDACTED + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_entry_diagnostics(hass, hass_client, setup_entry): + """Test config entry diagnostics.""" + + assert await get_diagnostics_for_config_entry(hass, hass_client, setup_entry) == { + "title": "Test Camera", + "data": {}, + "options": { + "still_image_url": "http://****:****@example.com/****?****=****", + "stream_source": "http://****:****@example.com/****", + "username": REDACTED, + "password": REDACTED, + "limit_refetch_to_url_change": False, + "authentication": "basic", + "framerate": 2.0, + "verify_ssl": True, + "content_type": "image/jpeg", + }, + } From 08ee276277234e0fa155e4392b3c104a51b11942 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 13 May 2022 21:03:21 +0200 Subject: [PATCH 0480/3516] Add tilt support to Tasmota covers (#71789) * Add tilt support to Tasmota covers * Bump hatasmota to 0.5.0 --- homeassistant/components/tasmota/cover.py | 48 ++++++- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_cover.py | 136 ++++++++++++++---- 5 files changed, 157 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/tasmota/cover.py b/homeassistant/components/tasmota/cover.py index 172e460c29b..4b851a86406 100644 --- a/homeassistant/components/tasmota/cover.py +++ b/homeassistant/components/tasmota/cover.py @@ -9,6 +9,7 @@ from hatasmota.models import DiscoveryHashType from homeassistant.components.cover import ( ATTR_POSITION, + ATTR_TILT_POSITION, DOMAIN as COVER_DOMAIN, CoverEntity, CoverEntityFeature, @@ -55,23 +56,32 @@ class TasmotaCover( ): """Representation of a Tasmota cover.""" - _attr_supported_features = ( - CoverEntityFeature.OPEN - | CoverEntityFeature.CLOSE - | CoverEntityFeature.STOP - | CoverEntityFeature.SET_POSITION - ) _tasmota_entity: tasmota_shutter.TasmotaShutter def __init__(self, **kwds: Any) -> None: """Initialize the Tasmota cover.""" self._direction: int | None = None self._position: int | None = None + self._tilt_position: int | None = None super().__init__( **kwds, ) + self._attr_supported_features = ( + CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.STOP + | CoverEntityFeature.SET_POSITION + ) + if self._tasmota_entity.supports_tilt: + self._attr_supported_features |= ( + CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT + | CoverEntityFeature.SET_TILT_POSITION + ) + async def async_added_to_hass(self) -> None: """Subscribe to MQTT events.""" self._tasmota_entity.set_on_state_callback(self.cover_state_updated) @@ -82,6 +92,7 @@ class TasmotaCover( """Handle state updates.""" self._direction = kwargs["direction"] self._position = kwargs["position"] + self._tilt_position = kwargs["tilt"] self.async_write_ha_state() @property @@ -92,6 +103,14 @@ class TasmotaCover( """ return self._position + @property + def current_cover_tilt_position(self) -> int | None: + """Return current tilt position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + return self._tilt_position + @property def is_opening(self) -> bool: """Return if the cover is opening or not.""" @@ -125,3 +144,20 @@ class TasmotaCover( async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self._tasmota_entity.stop() + + async def async_open_cover_tilt(self, **kwargs: Any) -> None: + """Open the cover tilt.""" + await self._tasmota_entity.open_tilt() + + async def async_close_cover_tilt(self, **kwargs: Any) -> None: + """Close cover tilt.""" + await self._tasmota_entity.close_tilt() + + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: + """Move the cover tilt to a specific position.""" + tilt = kwargs[ATTR_TILT_POSITION] + await self._tasmota_entity.set_tilt_position(tilt) + + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: + """Stop the cover tilt.""" + await self._tasmota_entity.stop() diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 6a743683d94..772105043fe 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.4.1"], + "requirements": ["hatasmota==0.5.0"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index b572dca4f6f..79ee89d3cb7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -792,7 +792,7 @@ hass-nabucasa==0.54.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.4.1 +hatasmota==0.5.0 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e0f5459d43a..85077aa295a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -565,7 +565,7 @@ hangups==0.4.18 hass-nabucasa==0.54.0 # homeassistant.components.tasmota -hatasmota==0.4.1 +hatasmota==0.5.0 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py index e88df08a80c..843fd72ecf1 100644 --- a/tests/components/tasmota/test_cover.py +++ b/tests/components/tasmota/test_cover.py @@ -30,6 +30,19 @@ from .test_common import ( from tests.common import async_fire_mqtt_message +COVER_SUPPORT = ( + cover.SUPPORT_OPEN + | cover.SUPPORT_CLOSE + | cover.SUPPORT_STOP + | cover.SUPPORT_SET_POSITION +) +TILT_SUPPORT = ( + cover.SUPPORT_OPEN_TILT + | cover.SUPPORT_CLOSE_TILT + | cover.SUPPORT_STOP_TILT + | cover.SUPPORT_SET_TILT_POSITION +) + async def test_missing_relay(hass, mqtt_mock, setup_tasmota): """Test no cover is discovered if relays are missing.""" @@ -64,11 +77,46 @@ async def test_multiple_covers( assert len(hass.states.async_all("cover")) == num_covers +async def test_tilt_support(hass, mqtt_mock, setup_tasmota): + """Test tilt support detection.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"] = [3, 3, 3, 3, 3, 3, 3, 3] + config["sht"] = [ + [0, 0, 0], # Default settings, no tilt + [-90, 90, 24], # Tilt configured + [-90, 90, 0], # Duration 0, no tilt + [-90, -90, 24], # min+max same, no tilt + ] + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all("cover")) == 4 + + state = hass.states.get("cover.tasmota_cover_1") + assert state.attributes["supported_features"] == COVER_SUPPORT + + state = hass.states.get("cover.tasmota_cover_2") + assert state.attributes["supported_features"] == COVER_SUPPORT | TILT_SUPPORT + + state = hass.states.get("cover.tasmota_cover_3") + assert state.attributes["supported_features"] == COVER_SUPPORT + + state = hass.states.get("cover.tasmota_cover_4") + assert state.attributes["supported_features"] == COVER_SUPPORT + + async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) config["rl"][0] = 3 config["rl"][1] = 3 + config["sht"] = [[-90, 90, 24]] mac = config["mac"] async_fire_mqtt_message( @@ -86,40 +134,39 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): await hass.async_block_till_done() state = hass.states.get("cover.tasmota_cover_1") assert state.state == STATE_UNKNOWN - assert ( - state.attributes["supported_features"] - == cover.SUPPORT_OPEN - | cover.SUPPORT_CLOSE - | cover.SUPPORT_STOP - | cover.SUPPORT_SET_POSITION - ) + assert state.attributes["supported_features"] == COVER_SUPPORT | TILT_SUPPORT assert not state.attributes.get(ATTR_ASSUMED_STATE) # Periodic updates async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/SENSOR", - '{"Shutter1":{"Position":54,"Direction":-1}}', + '{"Shutter1":{"Position":54,"Direction":-1,"Tilt":-90}}', ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "closing" assert state.attributes["current_position"] == 54 + assert state.attributes["current_tilt_position"] == 0 async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/SENSOR", - '{"Shutter1":{"Position":100,"Direction":1}}', + '{"Shutter1":{"Position":100,"Direction":1,"Tilt":90}}', ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "opening" assert state.attributes["current_position"] == 100 + assert state.attributes["current_tilt_position"] == 100 async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/SENSOR", '{"Shutter1":{"Position":0,"Direction":0}}' + hass, + "tasmota_49A3BC/tele/SENSOR", + '{"Shutter1":{"Position":0,"Direction":0,"Tilt":0}}', ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "closed" assert state.attributes["current_position"] == 0 + assert state.attributes["current_tilt_position"] == 50 async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/SENSOR", '{"Shutter1":{"Position":1,"Direction":0}}' @@ -141,29 +188,32 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"Shutter1":{"Position":54,"Direction":-1}}}', + '{"StatusSNS":{"Shutter1":{"Position":54,"Direction":-1,"Tilt":-90}}}', ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "closing" assert state.attributes["current_position"] == 54 + assert state.attributes["current_tilt_position"] == 0 async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"Shutter1":{"Position":100,"Direction":1}}}', + '{"StatusSNS":{"Shutter1":{"Position":100,"Direction":1,"Tilt":90}}}', ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "opening" assert state.attributes["current_position"] == 100 + assert state.attributes["current_tilt_position"] == 100 async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"Shutter1":{"Position":0,"Direction":0}}}', + '{"StatusSNS":{"Shutter1":{"Position":0,"Direction":0,"Tilt":0}}}', ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "closed" assert state.attributes["current_position"] == 0 + assert state.attributes["current_tilt_position"] == 50 async_fire_mqtt_message( hass, @@ -187,27 +237,32 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", - '{"Shutter1":{"Position":54,"Direction":-1}}', + '{"Shutter1":{"Position":54,"Direction":-1,"Tilt":-90}}', ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "closing" assert state.attributes["current_position"] == 54 + assert state.attributes["current_tilt_position"] == 0 async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", - '{"Shutter1":{"Position":100,"Direction":1}}', + '{"Shutter1":{"Position":100,"Direction":1,"Tilt":90}}', ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "opening" assert state.attributes["current_position"] == 100 + assert state.attributes["current_tilt_position"] == 100 async_fire_mqtt_message( - hass, "tasmota_49A3BC/stat/RESULT", '{"Shutter1":{"Position":0,"Direction":0}}' + hass, + "tasmota_49A3BC/stat/RESULT", + '{"Shutter1":{"Position":0,"Direction":0,"Tilt":0}}', ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "closed" assert state.attributes["current_position"] == 0 + assert state.attributes["current_tilt_position"] == 50 async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", '{"Shutter1":{"Position":1,"Direction":0}}' @@ -249,14 +304,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot await hass.async_block_till_done() state = hass.states.get("cover.tasmota_cover_1") assert state.state == STATE_UNKNOWN - assert ( - state.attributes["supported_features"] - == cover.SUPPORT_OPEN - | cover.SUPPORT_CLOSE - | cover.SUPPORT_STOP - | cover.SUPPORT_SET_POSITION - ) - assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes["supported_features"] == COVER_SUPPORT # Periodic updates async_fire_mqtt_message( @@ -405,6 +453,7 @@ async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota): config["dn"] = "Test" config["rl"][0] = 3 config["rl"][1] = 3 + config["sht"] = [[-90, 90, 24]] mac = config["mac"] async_fire_mqtt_message( @@ -461,6 +510,45 @@ async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() + # Close the cover tilt and verify MQTT message is sent + await call_service(hass, "cover.test_cover_1", "close_cover_tilt") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/ShutterTilt1", "CLOSE", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Open the cover tilt and verify MQTT message is sent + await call_service(hass, "cover.test_cover_1", "open_cover_tilt") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/ShutterTilt1", "OPEN", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Stop the cover tilt and verify MQTT message is sent + await call_service(hass, "cover.test_cover_1", "stop_cover_tilt") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/ShutterStop1", "", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set tilt position and verify MQTT message is sent + await call_service( + hass, "cover.test_cover_1", "set_cover_tilt_position", tilt_position=0 + ) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/ShutterTilt1", "-90", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set tilt position and verify MQTT message is sent + await call_service( + hass, "cover.test_cover_1", "set_cover_tilt_position", tilt_position=100 + ) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/ShutterTilt1", "90", 0, False + ) + mqtt_mock.async_publish.reset_mock() + async def test_sending_mqtt_commands_inverted(hass, mqtt_mock, setup_tasmota): """Test the sending MQTT commands.""" From 2a2a7a62c525e1feda6557c48a881c997133eef7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 May 2022 16:16:33 -0400 Subject: [PATCH 0481/3516] Avoid matching entity_id/domain attributes in logbook when there is no entities_filter (#71825) --- homeassistant/components/logbook/__init__.py | 57 ++++++++++---------- tests/components/logbook/test_init.py | 19 ++++++- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 4b40e398f6e..fae979445fd 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -426,7 +426,8 @@ def _humanify( elif event_type == EVENT_LOGBOOK_ENTRY: event = event_cache.get(row) - event_data = event.data + if not (event_data := event.data): + continue domain = event_data.get(ATTR_DOMAIN) entity_id = event_data.get(ATTR_ENTITY_ID) if domain is None and entity_id is not None: @@ -474,6 +475,22 @@ def _get_events( def yield_rows(query: Query) -> Generator[Row, None, None]: """Yield Events that are not filtered away.""" + + def _keep_row(row: Row, event_type: str) -> bool: + """Check if the entity_filter rejects a row.""" + assert entities_filter is not None + if entity_id := _row_event_data_extract(row, ENTITY_ID_JSON_EXTRACT): + return entities_filter(entity_id) + + if event_type in external_events: + # If the entity_id isn't described, use the domain that describes + # the event for filtering. + domain: str | None = external_events[event_type][0] + else: + domain = _row_event_data_extract(row, DOMAIN_JSON_EXTRACT) + + return domain is not None and entities_filter(f"{domain}._") + # end_day - start_day intentionally checks .days and not .total_seconds() # since we don't want to switch over to buffered if they go # over one day by a few hours since the UI makes it so easy to do that. @@ -490,16 +507,17 @@ def _get_events( # so we don't switch over until we request > 1 day+ of data. # rows = query.yield_per(1024) + for row in rows: context_lookup.setdefault(row.context_id, row) - if row.context_only: - continue - event_type = row.event_type - if event_type != EVENT_CALL_SERVICE and ( - event_type == EVENT_STATE_CHANGED - or _keep_row(hass, event_type, row, entities_filter) - ): - yield row + if not row.context_only: + event_type = row.event_type + if event_type != EVENT_CALL_SERVICE and ( + entities_filter is None + or event_type == EVENT_STATE_CHANGED + or _keep_row(row, event_type) + ): + yield row if entity_ids is not None: entities_filter = generate_filter([], entity_ids, [], []) @@ -526,27 +544,6 @@ def _get_events( ) -def _keep_row( - hass: HomeAssistant, - event_type: str, - row: Row, - entities_filter: EntityFilter | Callable[[str], bool] | None = None, -) -> bool: - if entity_id := _row_event_data_extract(row, ENTITY_ID_JSON_EXTRACT): - return entities_filter is None or entities_filter(entity_id) - - if event_type in hass.data[DOMAIN]: - # If the entity_id isn't described, use the domain that describes - # the event for filtering. - domain = hass.data[DOMAIN][event_type][0] - else: - domain = _row_event_data_extract(row, DOMAIN_JSON_EXTRACT) - - return domain is not None and ( - entities_filter is None or entities_filter(f"{domain}._") - ) - - class ContextAugmenter: """Augment data with context trace.""" diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 82857e6735c..eb58a61835e 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -1761,13 +1761,21 @@ async def test_fire_logbook_entries(hass, hass_client, recorder_mock): logbook.EVENT_LOGBOOK_ENTRY, {}, ) + hass.bus.async_fire( + logbook.EVENT_LOGBOOK_ENTRY, + { + logbook.ATTR_NAME: "Alarm", + logbook.ATTR_MESSAGE: "is triggered", + logbook.ATTR_DOMAIN: "switch", + }, + ) await async_wait_recording_done(hass) client = await hass_client() response_json = await _async_fetch_logbook(client) # The empty events should be skipped - assert len(response_json) == 10 + assert len(response_json) == 11 async def test_exclude_events_domain(hass, hass_client, recorder_mock): @@ -1986,6 +1994,15 @@ async def test_include_events_domain_glob(hass, hass_client, recorder_mock): ) await async_recorder_block_till_done(hass) + # Should get excluded by domain + hass.bus.async_fire( + logbook.EVENT_LOGBOOK_ENTRY, + { + logbook.ATTR_NAME: "Alarm", + logbook.ATTR_MESSAGE: "is triggered", + logbook.ATTR_DOMAIN: "switch", + }, + ) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire( From 535ae56fe77f72d8950f1cba7632d459721fe9c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 May 2022 16:17:41 -0400 Subject: [PATCH 0482/3516] Remove unused entity_id argument in logbook context augmenter (#71829) --- homeassistant/components/logbook/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index fae979445fd..351b121a166 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -413,7 +413,7 @@ def _humanify( if icon := _row_attributes_extract(row, ICON_JSON_EXTRACT): data[LOGBOOK_ENTRY_ICON] = icon - context_augmenter.augment(data, entity_id, row) + context_augmenter.augment(data, row) yield data elif event_type in external_events: @@ -421,7 +421,7 @@ def _humanify( data = describe_event(event_cache.get(row)) data[LOGBOOK_ENTRY_WHEN] = format_time(row) data[LOGBOOK_ENTRY_DOMAIN] = domain - context_augmenter.augment(data, data.get(ATTR_ENTITY_ID), row) + context_augmenter.augment(data, row) yield data elif event_type == EVENT_LOGBOOK_ENTRY: @@ -441,7 +441,7 @@ def _humanify( LOGBOOK_ENTRY_DOMAIN: domain, LOGBOOK_ENTRY_ENTITY_ID: entity_id, } - context_augmenter.augment(data, entity_id, row) + context_augmenter.augment(data, row) yield data @@ -562,7 +562,7 @@ class ContextAugmenter: self.external_events = external_events self.event_cache = event_cache - def augment(self, data: dict[str, Any], entity_id: str | None, row: Row) -> None: + def augment(self, data: dict[str, Any], row: Row) -> None: """Augment data from the row and cache.""" if context_user_id := row.context_user_id: data[CONTEXT_USER_ID] = context_user_id From e06ea5e03e2cf4b97d19c9ea08e3d0cd40716dec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 May 2022 16:19:26 -0400 Subject: [PATCH 0483/3516] Remove deprecated history function entry points (#71815) --- homeassistant/components/history/__init__.py | 28 -------------------- 1 file changed, 28 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 17b983e11c1..e5b6c99eb2a 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -29,7 +29,6 @@ from homeassistant.components.websocket_api.const import JSON_DUMP from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.deprecation import deprecated_class, deprecated_function from homeassistant.helpers.entityfilter import ( CONF_ENTITY_GLOBS, INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, @@ -60,28 +59,6 @@ CONFIG_SCHEMA = vol.Schema( ) -@deprecated_function("homeassistant.components.recorder.history.get_significant_states") -def get_significant_states(hass, *args, **kwargs): - """Wrap get_significant_states_with_session with an sql session.""" - return history.get_significant_states(hass, *args, **kwargs) - - -@deprecated_function( - "homeassistant.components.recorder.history.state_changes_during_period" -) -def state_changes_during_period(hass, start_time, end_time=None, entity_id=None): - """Return states changes during UTC period start_time - end_time.""" - return history.state_changes_during_period( - hass, start_time, end_time=None, entity_id=None - ) - - -@deprecated_function("homeassistant.components.recorder.history.get_last_state_changes") -def get_last_state_changes(hass, number_of_states, entity_id): - """Return the last number_of_states.""" - return history.get_last_state_changes(hass, number_of_states, entity_id) - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the history hooks.""" conf = config.get(DOMAIN, {}) @@ -101,11 +78,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -@deprecated_class("homeassistant.components.recorder.models.LazyState") -class LazyState(history_models.LazyState): - """A lazy version of core State.""" - - def _ws_get_statistics_during_period( hass: HomeAssistant, msg_id: int, From 663f6f83409f0c415380086b6fefd976327adbb8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 May 2022 17:56:16 -0400 Subject: [PATCH 0484/3516] Complete refactoring of logbook humanify (#71830) --- homeassistant/components/logbook/__init__.py | 148 +++++++++---------- tests/components/logbook/common.py | 20 +-- tests/components/logbook/test_init.py | 2 + 3 files changed, 85 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 351b121a166..809f13ca598 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -377,21 +377,54 @@ class LogbookView(HomeAssistantView): def _humanify( - hass: HomeAssistant, rows: Generator[Row, None, None], + entities_filter: EntityFilter | Callable[[str], bool] | None, + ent_reg: er.EntityRegistry, + external_events: dict[ + str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] + ], entity_name_cache: EntityNameCache, - event_cache: EventCache, - context_augmenter: ContextAugmenter, format_time: Callable[[Row], Any], ) -> Generator[dict[str, Any], None, None]: """Generate a converted list of events into entries.""" - external_events = hass.data.get(DOMAIN, {}) # Continuous sensors, will be excluded from the logbook continuous_sensors: dict[str, bool] = {} + event_data_cache: dict[str, dict[str, Any]] = {} + context_lookup: dict[str | None, Row | None] = {None: None} + event_cache = EventCache(event_data_cache) + context_augmenter = ContextAugmenter( + context_lookup, entity_name_cache, external_events, event_cache + ) - # Process events + def _keep_row(row: Row, event_type: str) -> bool: + """Check if the entity_filter rejects a row.""" + assert entities_filter is not None + if entity_id := _row_event_data_extract(row, ENTITY_ID_JSON_EXTRACT): + return entities_filter(entity_id) + + if event_type in external_events: + # If the entity_id isn't described, use the domain that describes + # the event for filtering. + domain: str | None = external_events[event_type][0] + else: + domain = _row_event_data_extract(row, DOMAIN_JSON_EXTRACT) + + return domain is not None and entities_filter(f"{domain}._") + + # Process rows for row in rows: + context_id = row.context_id + context_lookup.setdefault(context_id, row) + if row.context_only: + continue event_type = row.event_type + if event_type == EVENT_CALL_SERVICE or ( + event_type != EVENT_STATE_CHANGED + and entities_filter is not None + and not _keep_row(row, event_type) + ): + continue + if event_type == EVENT_STATE_CHANGED: entity_id = row.entity_id assert entity_id is not None @@ -399,7 +432,7 @@ def _humanify( if ( is_continuous := continuous_sensors.get(entity_id) ) is None and split_entity_id(entity_id)[0] == SENSOR_DOMAIN: - is_continuous = _is_sensor_continuous(hass, entity_id) + is_continuous = _is_sensor_continuous(ent_reg, entity_id) continuous_sensors[entity_id] = is_continuous if is_continuous: continue @@ -413,7 +446,7 @@ def _humanify( if icon := _row_attributes_extract(row, ICON_JSON_EXTRACT): data[LOGBOOK_ENTRY_ICON] = icon - context_augmenter.augment(data, row) + context_augmenter.augment(data, row, context_id) yield data elif event_type in external_events: @@ -421,27 +454,27 @@ def _humanify( data = describe_event(event_cache.get(row)) data[LOGBOOK_ENTRY_WHEN] = format_time(row) data[LOGBOOK_ENTRY_DOMAIN] = domain - context_augmenter.augment(data, row) + context_augmenter.augment(data, row, context_id) yield data elif event_type == EVENT_LOGBOOK_ENTRY: event = event_cache.get(row) if not (event_data := event.data): continue - domain = event_data.get(ATTR_DOMAIN) - entity_id = event_data.get(ATTR_ENTITY_ID) - if domain is None and entity_id is not None: + entry_domain = event_data.get(ATTR_DOMAIN) + entry_entity_id = event_data.get(ATTR_ENTITY_ID) + if entry_domain is None and entry_entity_id is not None: with suppress(IndexError): - domain = split_entity_id(str(entity_id))[0] + entry_domain = split_entity_id(str(entry_entity_id))[0] data = { LOGBOOK_ENTRY_WHEN: format_time(row), LOGBOOK_ENTRY_NAME: event_data.get(ATTR_NAME), LOGBOOK_ENTRY_MESSAGE: event_data.get(ATTR_MESSAGE), - LOGBOOK_ENTRY_DOMAIN: domain, - LOGBOOK_ENTRY_ENTITY_ID: entity_id, + LOGBOOK_ENTRY_DOMAIN: entry_domain, + LOGBOOK_ENTRY_ENTITY_ID: entry_entity_id, } - context_augmenter.augment(data, row) + context_augmenter.augment(data, row, context_id) yield data @@ -460,67 +493,34 @@ def _get_events( entity_ids and context_id ), "can't pass in both entity_ids and context_id" - entity_name_cache = EntityNameCache(hass) - event_data_cache: dict[str, dict[str, Any]] = {} - context_lookup: dict[str | None, Row | None] = {None: None} - event_cache = EventCache(event_data_cache) external_events: dict[ str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] ] = hass.data.get(DOMAIN, {}) - context_augmenter = ContextAugmenter( - context_lookup, entity_name_cache, external_events, event_cache - ) event_types = (*ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, *external_events) format_time = _row_time_fired_timestamp if timestamp else _row_time_fired_isoformat + entity_name_cache = EntityNameCache(hass) + ent_reg = er.async_get(hass) + + if entity_ids is not None: + entities_filter = generate_filter([], entity_ids, [], []) def yield_rows(query: Query) -> Generator[Row, None, None]: - """Yield Events that are not filtered away.""" - - def _keep_row(row: Row, event_type: str) -> bool: - """Check if the entity_filter rejects a row.""" - assert entities_filter is not None - if entity_id := _row_event_data_extract(row, ENTITY_ID_JSON_EXTRACT): - return entities_filter(entity_id) - - if event_type in external_events: - # If the entity_id isn't described, use the domain that describes - # the event for filtering. - domain: str | None = external_events[event_type][0] - else: - domain = _row_event_data_extract(row, DOMAIN_JSON_EXTRACT) - - return domain is not None and entities_filter(f"{domain}._") - + """Yield rows from the database.""" # end_day - start_day intentionally checks .days and not .total_seconds() # since we don't want to switch over to buffered if they go # over one day by a few hours since the UI makes it so easy to do that. if entity_ids or context_id or (end_day - start_day).days <= 1: - rows = query.all() - else: - # Only buffer rows to reduce memory pressure - # if we expect the result set is going to be very large. - # What is considered very large is going to differ - # based on the hardware Home Assistant is running on. - # - # sqlalchemy suggests that is at least 10k, but for - # even and RPi3 that number seems higher in testing - # so we don't switch over until we request > 1 day+ of data. - # - rows = query.yield_per(1024) - - for row in rows: - context_lookup.setdefault(row.context_id, row) - if not row.context_only: - event_type = row.event_type - if event_type != EVENT_CALL_SERVICE and ( - entities_filter is None - or event_type == EVENT_STATE_CHANGED - or _keep_row(row, event_type) - ): - yield row - - if entity_ids is not None: - entities_filter = generate_filter([], entity_ids, [], []) + return query.all() # type: ignore[no-any-return] + # Only buffer rows to reduce memory pressure + # if we expect the result set is going to be very large. + # What is considered very large is going to differ + # based on the hardware Home Assistant is running on. + # + # sqlalchemy suggests that is at least 10k, but for + # even and RPi3 that number seems higher in testing + # so we don't switch over until we request > 1 day+ of data. + # + return query.yield_per(1024) # type: ignore[no-any-return] stmt = statement_for_request( start_day, end_day, event_types, entity_ids, filters, context_id @@ -534,11 +534,11 @@ def _get_events( with session_scope(hass=hass) as session: return list( _humanify( - hass, yield_rows(session.execute(stmt)), + entities_filter, + ent_reg, + external_events, entity_name_cache, - event_cache, - context_augmenter, format_time, ) ) @@ -562,12 +562,12 @@ class ContextAugmenter: self.external_events = external_events self.event_cache = event_cache - def augment(self, data: dict[str, Any], row: Row) -> None: + def augment(self, data: dict[str, Any], row: Row, context_id: str) -> None: """Augment data from the row and cache.""" if context_user_id := row.context_user_id: data[CONTEXT_USER_ID] = context_user_id - if not (context_row := self.context_lookup.get(row.context_id)): + if not (context_row := self.context_lookup.get(context_id)): return if _rows_match(row, context_row): @@ -624,17 +624,13 @@ class ContextAugmenter: ) -def _is_sensor_continuous( - hass: HomeAssistant, - entity_id: str, -) -> bool: +def _is_sensor_continuous(ent_reg: er.EntityRegistry, entity_id: str) -> bool: """Determine if a sensor is continuous by checking its state class. Sensors with a unit_of_measurement are also considered continuous, but are filtered already by the SQL query generated by _get_events """ - registry = er.async_get(hass) - if not (entry := registry.async_get(entity_id)): + if not (entry := ent_reg.async_get(entity_id)): # Entity not registered, so can't have a state class return False return ( diff --git a/tests/components/logbook/common.py b/tests/components/logbook/common.py index 32273f04c05..9d6f35eb702 100644 --- a/tests/components/logbook/common.py +++ b/tests/components/logbook/common.py @@ -7,6 +7,7 @@ from typing import Any from homeassistant.components import logbook from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat from homeassistant.core import Context +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util @@ -30,6 +31,11 @@ class MockRow: self.context_id = context.id if context else None self.state = None self.entity_id = None + self.state_id = None + self.event_id = None + self.shared_attrs = None + self.attributes = None + self.context_only = False @property def time_fired_minute(self): @@ -44,20 +50,16 @@ class MockRow: def mock_humanify(hass_, rows): """Wrap humanify with mocked logbook objects.""" - event_data_cache = {} - context_lookup = {} entity_name_cache = logbook.EntityNameCache(hass_) - event_cache = logbook.EventCache(event_data_cache) - context_augmenter = logbook.ContextAugmenter( - context_lookup, entity_name_cache, {}, event_cache - ) + ent_reg = er.async_get(hass_) + external_events = hass_.data.get(logbook.DOMAIN, {}) return list( logbook._humanify( - hass_, rows, + None, + ent_reg, + external_events, entity_name_cache, - event_cache, - context_augmenter, logbook._row_time_fired_isoformat, ), ) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index eb58a61835e..a515afdf16e 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -323,6 +323,7 @@ def create_state_changed_event_from_old_new( "old_state_id", "shared_attrs", "shared_data", + "context_only", ], ) @@ -335,6 +336,7 @@ def create_state_changed_event_from_old_new( row.state = new_state and new_state.get("state") row.entity_id = entity_id row.domain = entity_id and ha.split_entity_id(entity_id)[0] + row.context_only = False row.context_id = None row.context_user_id = None row.context_parent_id = None From f6600bbc209130cffee3b8b115e8309488eafc73 Mon Sep 17 00:00:00 2001 From: mkmer Date: Fri, 13 May 2022 18:41:01 -0400 Subject: [PATCH 0485/3516] Add Aladdin connect config flow (#68304) * Adding flow and async * Fixes to init * Lint and type * Fixed coveragerc file * Added Test Coverage * Added Update Listener and removed unused code * Wrong integration name in init. * Nothing * Added yaml import flow * Added YAML import functionality * Added back aladdin_connect files to coverage rc * Removed commented code * Clean up error message * Update homeassistant/components/aladdin_connect/__init__.py Co-authored-by: G Johansson * Update homeassistant/components/aladdin_connect/__init__.py Co-authored-by: G Johansson * Update homeassistant/components/aladdin_connect/config_flow.py Co-authored-by: G Johansson * Updated Documentation errors * recommended change broke cover.py - backed out * Cleaned up unused defenitions * implimented recommended changes from gjohansson * Dev environment cleanup * Raised errors for better recovery, replaced removed update files, utilized PLATFORM vars to init platform * Added back removal * Added Code Owner * Fixed more comment errors and import duplicates * Added test coverage and formated code * Added test coverage for model and init * Added test_cover for full testing coverage * Added await to async call * Added missing asserts to failure tests * Updated tranlsation * Fixed wording in yaml import function, white space in const.py, return from validate_input. * Update homeassistant/components/aladdin_connect/config_flow.py Co-authored-by: Robert Svensson * "too much" whitespace * Added back mising strings.json errors * Added ConfigFlowReconfig and tests * Finished up reauth config flow and associated tests * Added reauth to strings, removed username from reauth * recommended changes, ran script.translations, added auth test to reauth * put back self.entry.data unpack. * Cleanup for error message, fixed missing "asserts" in tests * Added yaml import assertions * Fixed documentation errors in test_cover. * remove unused string. * revised tests and wording for yaml import * Documentation cleanup. * Changed sideeffect names Co-authored-by: G Johansson Co-authored-by: Robert Svensson --- .coveragerc | 2 - CODEOWNERS | 2 + .../components/aladdin_connect/__init__.py | 37 ++ .../components/aladdin_connect/config_flow.py | 134 ++++++++ .../components/aladdin_connect/const.py | 1 + .../components/aladdin_connect/cover.py | 58 ++-- .../components/aladdin_connect/manifest.json | 5 +- .../components/aladdin_connect/strings.json | 29 ++ .../aladdin_connect/translations/en.json | 27 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/aladdin_connect/__init__.py | 1 + .../aladdin_connect/test_config_flow.py | 323 ++++++++++++++++++ .../components/aladdin_connect/test_cover.py | 272 +++++++++++++++ tests/components/aladdin_connect/test_init.py | 76 +++++ .../components/aladdin_connect/test_model.py | 18 + 16 files changed, 962 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/aladdin_connect/config_flow.py create mode 100644 homeassistant/components/aladdin_connect/strings.json create mode 100644 homeassistant/components/aladdin_connect/translations/en.json create mode 100644 tests/components/aladdin_connect/__init__.py create mode 100644 tests/components/aladdin_connect/test_config_flow.py create mode 100644 tests/components/aladdin_connect/test_cover.py create mode 100644 tests/components/aladdin_connect/test_init.py create mode 100644 tests/components/aladdin_connect/test_model.py diff --git a/.coveragerc b/.coveragerc index ef7cd7b847c..7ce0e5ee299 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,5 @@ [run] source = homeassistant - omit = homeassistant/__main__.py homeassistant/helpers/signal.py @@ -42,7 +41,6 @@ omit = homeassistant/components/airtouch4/const.py homeassistant/components/airvisual/__init__.py homeassistant/components/airvisual/sensor.py - homeassistant/components/aladdin_connect/* homeassistant/components/alarmdecoder/__init__.py homeassistant/components/alarmdecoder/alarm_control_panel.py homeassistant/components/alarmdecoder/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 8be3146b432..d07ce028ba9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -50,6 +50,8 @@ build.json @home-assistant/supervisor /tests/components/airvisual/ @bachya /homeassistant/components/airzone/ @Noltari /tests/components/airzone/ @Noltari +/homeassistant/components/aladdin_connect/ @mkmer +/tests/components/aladdin_connect/ @mkmer /homeassistant/components/alarm_control_panel/ @home-assistant/core /tests/components/alarm_control_panel/ @home-assistant/core /homeassistant/components/alert/ @home-assistant/core diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index 90196616dc5..cbd4a195a3a 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -1 +1,38 @@ """The aladdin_connect component.""" +import logging +from typing import Final + +from aladdin_connect import AladdinConnectClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady + +from .const import DOMAIN + +_LOGGER: Final = logging.getLogger(__name__) + +PLATFORMS: list[Platform] = [Platform.COVER] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up platform from a ConfigEntry.""" + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + acc = AladdinConnectClient(username, password) + try: + if not await hass.async_add_executor_job(acc.login): + raise ConfigEntryAuthFailed("Incorrect Password") + except (TypeError, KeyError, NameError, ValueError) as ex: + _LOGGER.error("%s", ex) + raise ConfigEntryNotReady from ex + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = acc + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +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/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py new file mode 100644 index 00000000000..f912d36a3f0 --- /dev/null +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -0,0 +1,134 @@ +"""Config flow for Aladdin Connect cover integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from aladdin_connect import AladdinConnectClient +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + +REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + acc = AladdinConnectClient(data[CONF_USERNAME], data[CONF_PASSWORD]) + try: + login = await hass.async_add_executor_job(acc.login) + except (TypeError, KeyError, NameError, ValueError) as ex: + raise ConnectionError from ex + else: + if not login: + raise InvalidAuth + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Aladdin Connect.""" + + VERSION = 1 + entry: config_entries.ConfigEntry | None + + async def async_step_reauth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle re-authentication with Aladdin Connect.""" + + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm re-authentication with Aladdin Connect.""" + errors: dict[str, str] = {} + + if user_input: + assert self.entry is not None + password = user_input[CONF_PASSWORD] + data = { + CONF_USERNAME: self.entry.data[CONF_USERNAME], + CONF_PASSWORD: password, + } + + try: + await validate_input(self.hass, data) + except ConnectionError: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + else: + + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_PASSWORD: password, + }, + ) + await 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", + data_schema=REAUTH_SCHEMA, + errors=errors, + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + try: + await validate_input(self.hass, user_input) + except ConnectionError: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + + else: + await self.async_set_unique_id( + user_input["username"].lower(), raise_on_progress=False + ) + self._abort_if_unique_id_configured() + return self.async_create_entry(title="Aladdin Connect", data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_import( + self, import_data: dict[str, Any] | None = None + ) -> FlowResult: + """Import Aladin Connect config from configuration.yaml.""" + return await self.async_step_user(import_data) + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/aladdin_connect/const.py b/homeassistant/components/aladdin_connect/const.py index 3069680c753..7a11cf63a9e 100644 --- a/homeassistant/components/aladdin_connect/const.py +++ b/homeassistant/components/aladdin_connect/const.py @@ -16,4 +16,5 @@ STATES_MAP: Final[dict[str, str]] = { "closing": STATE_CLOSING, } +DOMAIN = "aladdin_connect" SUPPORTED_FEATURES: Final = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 05c24fc9a37..9e18abe21f6 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -7,12 +7,12 @@ from typing import Any, Final from aladdin_connect import AladdinConnectClient import voluptuous as vol -from homeassistant.components import persistent_notification from homeassistant.components.cover import ( PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, CoverDeviceClass, CoverEntity, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, @@ -21,11 +21,12 @@ from homeassistant.const import ( STATE_OPENING, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import NOTIFICATION_ID, NOTIFICATION_TITLE, STATES_MAP, SUPPORTED_FEATURES +from .const import DOMAIN, STATES_MAP, SUPPORTED_FEATURES from .model import DoorDevice _LOGGER: Final = logging.getLogger(__name__) @@ -35,33 +36,44 @@ PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Aladdin Connect platform.""" - - username: str = config[CONF_USERNAME] - password: str = config[CONF_PASSWORD] - acc = AladdinConnectClient(username, password) - - try: - if not acc.login(): - raise ValueError("Username or Password is incorrect") - add_entities( - (AladdinDevice(acc, door) for door in acc.get_doors()), - update_before_add=True, + """Set up Aladdin Connect devices yaml depreciated.""" + _LOGGER.warning( + "Configuring Aladdin Connect through yaml is deprecated" + "Please remove it from your configuration as it has already been imported to a config entry" + ) + await hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, ) + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Aladdin Connect platform.""" + acc = hass.data[DOMAIN][config_entry.entry_id] + try: + doors = await hass.async_add_executor_job(acc.get_doors) + except (TypeError, KeyError, NameError, ValueError) as ex: _LOGGER.error("%s", ex) - persistent_notification.create( - hass, - f"Error: {ex}
You will need to restart hass after fixing.", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) + raise ConfigEntryNotReady from ex + + async_add_entities( + (AladdinDevice(acc, door) for door in doors), + update_before_add=True, + ) class AladdinDevice(CoverEntity): @@ -71,7 +83,7 @@ class AladdinDevice(CoverEntity): _attr_supported_features = SUPPORTED_FEATURES def __init__(self, acc: AladdinConnectClient, device: DoorDevice) -> None: - """Initialize the cover.""" + """Initialize the Aladdin Connect cover.""" self._acc = acc self._device_id = device["device_id"] self._number = device["door_number"] diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index e28b2adff42..b9ea214d996 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -3,7 +3,8 @@ "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", "requirements": ["aladdin_connect==0.4"], - "codeowners": [], + "codeowners": ["@mkmer"], "iot_class": "cloud_polling", - "loggers": ["aladdin_connect"] + "loggers": ["aladdin_connect"], + "config_flow": true } diff --git a/homeassistant/components/aladdin_connect/strings.json b/homeassistant/components/aladdin_connect/strings.json new file mode 100644 index 00000000000..ff42ca14bc3 --- /dev/null +++ b/homeassistant/components/aladdin_connect/strings.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Aladdin Connect integration needs to re-authenticate your account", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + + "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/aladdin_connect/translations/en.json b/homeassistant/components/aladdin_connect/translations/en.json new file mode 100644 index 00000000000..959e88fb2ae --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "The Aladdin Connect integration needs to re-authenticate your account", + "title": "Reauthenticate Integration" + }, + "user": { + "data": { + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 70451b22001..ae2ab6339aa 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -21,6 +21,7 @@ FLOWS = { "airtouch4", "airvisual", "airzone", + "aladdin_connect", "alarmdecoder", "almond", "ambee", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 85077aa295a..96ef2b55568 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -248,6 +248,9 @@ airthings_cloud==0.1.0 # homeassistant.components.airtouch4 airtouch4pyapi==1.0.5 +# homeassistant.components.aladdin_connect +aladdin_connect==0.4 + # homeassistant.components.ambee ambee==0.4.0 diff --git a/tests/components/aladdin_connect/__init__.py b/tests/components/aladdin_connect/__init__.py new file mode 100644 index 00000000000..6e108ed88df --- /dev/null +++ b/tests/components/aladdin_connect/__init__.py @@ -0,0 +1 @@ +"""The tests for Aladdin Connect platforms.""" diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py new file mode 100644 index 00000000000..37ec64ba6f0 --- /dev/null +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -0,0 +1,323 @@ +"""Test the Aladdin Connect config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.aladdin_connect.config_flow import InvalidAuth +from homeassistant.components.aladdin_connect.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +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_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Aladdin Connect" + assert result2["data"] == { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +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.aladdin_connect.config_flow.AladdinConnectClient.login", + side_effect=ConnectionError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + with patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", + side_effect=TypeError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + with patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", + return_value=False, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_already_configured(hass): + """Test we handle already configured error.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_USERNAME: "test-username", CONF_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.aladdin_connect.config_flow.AladdinConnectClient.login", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + +async def test_import_flow_success(hass: HomeAssistant) -> None: + """Test a successful import of yaml.""" + + with patch( + "homeassistant.components.aladdin_connect.cover.async_setup_platform", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.cover.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_USERNAME: "test-user", + CONF_PASSWORD: "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Aladdin Connect" + assert result2["data"] == { + CONF_USERNAME: "test-user", + CONF_PASSWORD: "test-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth_flow(hass: HomeAssistant) -> None: + """Test a successful reauth flow.""" + + 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_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={"username": "test-username", "password": "new-password"}, + ) + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.aladdin_connect.cover.async_setup_platform", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.cover.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + assert mock_entry.data == { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "new-password", + } + + +async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None: + """Test a successful reauth flow.""" + + 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_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={"username": "test-username", "password": "new-password"}, + ) + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.aladdin_connect.cover.async_setup_platform", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", + return_value=False, + ), patch( + "homeassistant.components.aladdin_connect.cover.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_reauth_flow_other_error(hass: HomeAssistant) -> None: + """Test an unsuccessful reauth flow.""" + + 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_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={"username": "test-username", "password": "new-password"}, + ) + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.aladdin_connect.cover.async_setup_platform", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", + side_effect=ValueError, + ), patch( + "homeassistant.components.aladdin_connect.cover.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/aladdin_connect/test_cover.py b/tests/components/aladdin_connect/test_cover.py new file mode 100644 index 00000000000..4904d904ad4 --- /dev/null +++ b/tests/components/aladdin_connect/test_cover.py @@ -0,0 +1,272 @@ +"""Test the Aladdin Connect Cover.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.aladdin_connect.const import DOMAIN +import homeassistant.components.aladdin_connect.cover as cover +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + CONF_PASSWORD, + CONF_USERNAME, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +YAML_CONFIG = {"username": "test-user", "password": "test-password"} + +DEVICE_CONFIG_OPEN = { + "device_id": 533255, + "door_number": 1, + "name": "home", + "status": "open", + "link_status": "Connected", +} + +DEVICE_CONFIG_OPENING = { + "device_id": 533255, + "door_number": 1, + "name": "home", + "status": "opening", + "link_status": "Connected", +} + +DEVICE_CONFIG_CLOSED = { + "device_id": 533255, + "door_number": 1, + "name": "home", + "status": "closed", + "link_status": "Connected", +} + +DEVICE_CONFIG_CLOSING = { + "device_id": 533255, + "door_number": 1, + "name": "home", + "status": "closing", + "link_status": "Connected", +} + +DEVICE_CONFIG_DISCONNECTED = { + "device_id": 533255, + "door_number": 1, + "name": "home", + "status": "open", + "link_status": "Disconnected", +} + +DEVICE_CONFIG_BAD = { + "device_id": 533255, + "door_number": 1, + "name": "home", + "status": "open", +} +DEVICE_CONFIG_BAD_NO_DOOR = { + "device_id": 533255, + "door_number": 2, + "name": "home", + "status": "open", + "link_status": "Disconnected", +} + + +@pytest.mark.parametrize( + "side_effect", + [ + (TypeError), + (KeyError), + (NameError), + (ValueError), + ], +) +async def test_setup_get_doors_errors( + hass: HomeAssistant, side_effect: Exception +) -> None: + """Test component setup Get Doors Errors.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=YAML_CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + side_effect=side_effect, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) is True + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + + +@pytest.mark.parametrize( + "side_effect", + [ + (TypeError), + (KeyError), + (NameError), + (ValueError), + ], +) +async def test_setup_login_error(hass: HomeAssistant, side_effect: Exception) -> None: + """Test component setup Login Errors.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=YAML_CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", + side_effect=side_effect, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) is False + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + + +async def test_setup_component_noerror(hass: HomeAssistant) -> None: + """Test component setup No Error.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=YAML_CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", + return_value=True, + ): + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + +async def test_cover_operation(hass: HomeAssistant) -> None: + """Test component setup open cover, close cover.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=YAML_CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + + assert await async_setup_component(hass, "homeassistant", {}) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=[DEVICE_CONFIG_OPEN], + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert COVER_DOMAIN in hass.config.components + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.open_door", + return_value=True, + ): + await hass.services.async_call( + "cover", "open_cover", {"entity_id": "cover.home"}, blocking=True + ) + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.close_door", + return_value=True, + ): + await hass.services.async_call( + "cover", "close_cover", {"entity_id": "cover.home"}, blocking=True + ) + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=[DEVICE_CONFIG_CLOSED], + ): + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True + ) + assert hass.states.get("cover.home").state == STATE_CLOSED + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=[DEVICE_CONFIG_OPEN], + ): + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True + ) + assert hass.states.get("cover.home").state == STATE_OPEN + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=[DEVICE_CONFIG_OPENING], + ): + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True + ) + assert hass.states.get("cover.home").state == STATE_OPENING + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=[DEVICE_CONFIG_CLOSING], + ): + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True + ) + assert hass.states.get("cover.home").state == STATE_CLOSING + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=[DEVICE_CONFIG_BAD], + ): + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True + ) + assert hass.states.get("cover.home").state + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=[DEVICE_CONFIG_BAD_NO_DOOR], + ): + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True + ) + assert hass.states.get("cover.home").state + + +async def test_yaml_import(hass: HomeAssistant, caplog: pytest.LogCaptureFixture): + """Test setup YAML import.""" + assert COVER_DOMAIN not in hass.config.components + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=[DEVICE_CONFIG_CLOSED], + ): + await cover.async_setup_platform(hass, YAML_CONFIG, None) + await hass.async_block_till_done() + + assert "Configuring Aladdin Connect through yaml is deprecated" in caplog.text + + assert hass.config_entries.async_entries(DOMAIN) + config_data = hass.config_entries.async_entries(DOMAIN)[0].data + assert config_data[CONF_USERNAME] == "test-user" + assert config_data[CONF_PASSWORD] == "test-password" diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py new file mode 100644 index 00000000000..c9814adb051 --- /dev/null +++ b/tests/components/aladdin_connect/test_init.py @@ -0,0 +1,76 @@ +"""Test for Aladdin Connect init logic.""" +from unittest.mock import patch + +from homeassistant.components.aladdin_connect.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +YAML_CONFIG = {"username": "test-user", "password": "test-password"} + + +async def test_unload_entry(hass: HomeAssistant): + """Test successful unload of entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={"username": "test-user", "password": "test-password"}, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", + return_value=True, + ): + + assert (await async_setup_component(hass, DOMAIN, entry)) is True + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state is 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 + + +async def test_entry_password_fail(hass: HomeAssistant): + """Test successful unload of entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={"username": "test-user", "password": "test-password"}, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", + return_value=False, + ): + + assert (await async_setup_component(hass, DOMAIN, entry)) is True + assert entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_load_and_unload(hass: HomeAssistant) -> None: + """Test loading and unloading Aladdin Connect entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=YAML_CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", + return_value=True, + ): + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + assert await config_entry.async_unload(hass) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.NOT_LOADED diff --git a/tests/components/aladdin_connect/test_model.py b/tests/components/aladdin_connect/test_model.py new file mode 100644 index 00000000000..e802ae53b74 --- /dev/null +++ b/tests/components/aladdin_connect/test_model.py @@ -0,0 +1,18 @@ +"""Test the Aladdin Connect model class.""" +from homeassistant.components.aladdin_connect.model import DoorDevice +from homeassistant.core import HomeAssistant + + +async def test_model(hass: HomeAssistant) -> None: + """Test model for Aladdin Connect Model.""" + test_values = { + "device_id": "1", + "door_number": "2", + "name": "my door", + "status": "good", + } + result2 = DoorDevice(test_values) + assert result2["device_id"] == "1" + assert result2["door_number"] == "2" + assert result2["name"] == "my door" + assert result2["status"] == "good" From 3e386064cfe3d81acf396f4ea0530b23b0861fb2 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Fri, 13 May 2022 18:42:33 -0400 Subject: [PATCH 0486/3516] Fix handling package detection for latest UniFi Protect beta (#71821) Co-authored-by: J. Nick Koston --- homeassistant/components/unifiprotect/config_flow.py | 8 ++++++-- homeassistant/components/unifiprotect/manifest.json | 2 +- homeassistant/components/unifiprotect/select.py | 2 +- homeassistant/components/unifiprotect/switch.py | 9 +++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifiprotect/test_switch.py | 10 +++++++--- 7 files changed, 26 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 0cfce44a6ca..27108d24eaa 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -143,7 +143,9 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_VERIFY_SSL] = False nvr_data, errors = await self._async_get_nvr_data(user_input) if nvr_data and not errors: - return self._async_create_entry(nvr_data.name, user_input) + return self._async_create_entry( + nvr_data.name or nvr_data.type, user_input + ) placeholders = { "name": discovery_info["hostname"] @@ -289,7 +291,9 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(nvr_data.mac) self._abort_if_unique_id_configured() - return self._async_create_entry(nvr_data.name, user_input) + return self._async_create_entry( + nvr_data.name or nvr_data.type, user_input + ) user_input = user_input or {} return self.async_show_form( diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 3c3b461ed4f..3efad51db31 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.4.1", "unifi-discovery==1.1.2"], + "requirements": ["pyunifiprotect==3.5.1", "unifi-discovery==1.1.2"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index b7b53ff81c8..f0500ea54e5 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -140,7 +140,7 @@ def _get_doorbell_options(api: ProtectApiClient) -> list[dict[str, Any]]: def _get_paired_camera_options(api: ProtectApiClient) -> list[dict[str, Any]]: options = [{"id": TYPE_EMPTY_VALUE, "name": "Not Paired"}] for camera in api.bootstrap.cameras.values(): - options.append({"id": camera.id, "name": camera.name}) + options.append({"id": camera.id, "name": camera.name or camera.type}) return options diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 2257f399f75..85a089994f8 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -159,6 +159,15 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_value="is_face_detection_on", ufp_set_method="set_face_detection", ), + ProtectSwitchEntityDescription( + key="smart_package", + name="Detections: Package", + icon="mdi:package-variant-closed", + entity_category=EntityCategory.CONFIG, + ufp_required_field="can_detect_package", + ufp_value="is_package_detection_on", + ufp_set_method="set_package_detection", + ), ) SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( diff --git a/requirements_all.txt b/requirements_all.txt index 79ee89d3cb7..2e8c89e5797 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1981,7 +1981,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.4.1 +pyunifiprotect==3.5.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 96ef2b55568..30510f08910 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1310,7 +1310,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.4.1 +pyunifiprotect==3.5.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 0a3ac92076e..c54d04a8cb7 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -26,9 +26,13 @@ from .conftest import ( ids_from_device_description, ) -CAMERA_SWITCHES_NO_FACE = [d for d in CAMERA_SWITCHES if d.name != "Detections: Face"] +CAMERA_SWITCHES_BASIC = [ + d + for d in CAMERA_SWITCHES + if d.name != "Detections: Face" and d.name != "Detections: Package" +] CAMERA_SWITCHES_NO_EXTRA = [ - d for d in CAMERA_SWITCHES_NO_FACE if d.name not in ("High FPS", "Privacy Mode") + d for d in CAMERA_SWITCHES_BASIC if d.name not in ("High FPS", "Privacy Mode") ] @@ -253,7 +257,7 @@ async def test_switch_setup_camera_all( entity_registry = er.async_get(hass) - for description in CAMERA_SWITCHES_NO_FACE: + for description in CAMERA_SWITCHES_BASIC: unique_id, entity_id = ids_from_device_description( Platform.SWITCH, camera, description ) From 72a65b6a211262d4690dda5067e9100d5ff4f253 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Fri, 13 May 2022 17:03:25 -0600 Subject: [PATCH 0487/3516] Add last seen and status code diagnostic sensors to litterrobot (#71760) --- .../components/litterrobot/sensor.py | 17 ++++++++++- .../litterrobot/strings.sensor.json | 28 +++++++++++++++++++ .../litterrobot/translations/sensor.en.json | 28 +++++++++++++++++++ .../components/litterrobot/vacuum.py | 3 -- 4 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/litterrobot/strings.sensor.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.en.json diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 751d16551c3..01deaa302cf 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -17,6 +17,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -63,7 +64,9 @@ class LitterRobotSensorEntity(LitterRobotEntity, SensorEntity): def native_value(self) -> StateType | datetime: """Return the state.""" if self.entity_description.should_report(self.robot): - return getattr(self.robot, self.entity_description.key) + if isinstance(val := getattr(self.robot, self.entity_description.key), str): + return val.lower() + return val return None @property @@ -93,6 +96,18 @@ ROBOT_SENSORS = [ device_class=SensorDeviceClass.TIMESTAMP, should_report=lambda robot: robot.sleep_mode_enabled, ), + LitterRobotSensorEntityDescription( + name="Last Seen", + key="last_seen", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + ), + LitterRobotSensorEntityDescription( + name="Status Code", + key="status_code", + device_class="litterrobot__status_code", + entity_category=EntityCategory.DIAGNOSTIC, + ), ] diff --git a/homeassistant/components/litterrobot/strings.sensor.json b/homeassistant/components/litterrobot/strings.sensor.json new file mode 100644 index 00000000000..d9ad141cf21 --- /dev/null +++ b/homeassistant/components/litterrobot/strings.sensor.json @@ -0,0 +1,28 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "Bonnet Removed", + "ccc": "Clean Cycle Complete", + "ccp": "Clean Cycle In Progress", + "csf": "Cat Sensor Fault", + "csi": "Cat Sensor Interrupted", + "cst": "Cat Sensor Timing", + "df1": "Drawer Almost Full - 2 Cycles Left", + "df2": "Drawer Almost Full - 1 Cycle Left", + "dfs": "Drawer Full", + "dhf": "Dump + Home Position Fault", + "dpf": "Dump Position Fault", + "ec": "Empty Cycle", + "hpf": "Home Position Fault", + "off": "[%key:common::state::off%]", + "offline": "Offline", + "otf": "Over Torque Fault", + "p": "[%key:common::state::paused%]", + "pd": "Pinch Detect", + "rdy": "Ready", + "scf": "Cat Sensor Fault At Startup", + "sdf": "Drawer Full At Startup", + "spf": "Pinch Detect At Startup" + } + } +} diff --git a/homeassistant/components/litterrobot/translations/sensor.en.json b/homeassistant/components/litterrobot/translations/sensor.en.json new file mode 100644 index 00000000000..5491a6a835f --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.en.json @@ -0,0 +1,28 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "Bonnet Removed", + "ccc": "Clean Cycle Complete", + "ccp": "Clean Cycle In Progress", + "csf": "Cat Sensor Fault", + "csi": "Cat Sensor Interrupted", + "cst": "Cat Sensor Timing", + "df1": "Drawer Almost Full - 2 Cycles Left", + "df2": "Drawer Almost Full - 1 Cycle Left", + "dfs": "Drawer Full", + "dhf": "Dump + Home Position Fault", + "dpf": "Dump Position Fault", + "ec": "Empty Cycle", + "hpf": "Home Position Fault", + "off": "Off", + "offline": "Offline", + "otf": "Over Torque Fault", + "p": "Paused", + "pd": "Pinch Detect", + "rdy": "Ready", + "scf": "Cat Sensor Fault At Startup", + "sdf": "Drawer Full At Startup", + "spf": "Pinch Detect At Startup" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 0d9a2b1dc3d..dbe51270857 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -162,11 +162,8 @@ class LitterRobotCleaner(LitterRobotControlEntity, StateVacuumEntity): def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" return { - "clean_cycle_wait_time_minutes": self.robot.clean_cycle_wait_time_minutes, "is_sleeping": self.robot.is_sleeping, "sleep_mode_enabled": self.robot.sleep_mode_enabled, "power_status": self.robot.power_status, - "status_code": self.robot.status_code, - "last_seen": self.robot.last_seen, "status": self.status, } From c5460ce4b9d90934825a7e8da9c712c51ddb1fc4 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 13 May 2022 18:04:58 -0500 Subject: [PATCH 0488/3516] Fix Sonos idle states (#71756) --- homeassistant/components/sonos/media.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/sonos/media.py b/homeassistant/components/sonos/media.py index e661a8320dc..e3d8f043d4b 100644 --- a/homeassistant/components/sonos/media.py +++ b/homeassistant/components/sonos/media.py @@ -120,6 +120,8 @@ class SonosMedia: self.clear() track_info = self.poll_track_info() + if not track_info["uri"]: + return self.uri = track_info["uri"] audio_source = self.soco.music_source_from_uri(self.uri) From 21b1667de9c38edf43f4ce73d50caab194780361 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 14 May 2022 01:57:19 +0200 Subject: [PATCH 0489/3516] Adjust Fan Modes in insteon (#71804) --- homeassistant/components/insteon/climate.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/insteon/climate.py b/homeassistant/components/insteon/climate.py index 833180583e2..97e21a02f6f 100644 --- a/homeassistant/components/insteon/climate.py +++ b/homeassistant/components/insteon/climate.py @@ -9,8 +9,7 @@ from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, DOMAIN as CLIMATE_DOMAIN, - HVAC_MODE_AUTO, - HVAC_MODE_FAN_ONLY, + FAN_AUTO, ClimateEntityFeature, HVACAction, HVACMode, @@ -25,6 +24,8 @@ from .const import SIGNAL_ADD_ENTITIES from .insteon_entity import InsteonEntity from .utils import async_add_insteon_entities +FAN_ONLY = "fan_only" + COOLING = 1 HEATING = 2 DEHUMIDIFYING = 3 @@ -46,7 +47,7 @@ HVAC_MODES = { 2: HVACMode.COOL, 3: HVACMode.HEAT_COOL, } -FAN_MODES = {4: HVAC_MODE_AUTO, 8: HVAC_MODE_FAN_ONLY} +FAN_MODES = {4: FAN_AUTO, 8: FAN_ONLY} async def async_setup_entry( From 4ea6e5dfc0f50adff04e5676edab5c713e472808 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 13 May 2022 20:05:06 -0400 Subject: [PATCH 0490/3516] Add config flow to Slack integration (#69880) --- .coveragerc | 1 + CODEOWNERS | 4 +- homeassistant/components/slack/__init__.py | 64 +++++++- homeassistant/components/slack/config_flow.py | 87 +++++++++++ homeassistant/components/slack/const.py | 16 ++ homeassistant/components/slack/manifest.json | 3 +- homeassistant/components/slack/notify.py | 71 ++++----- homeassistant/components/slack/strings.json | 29 ++++ .../components/slack/translations/en.json | 29 ++++ homeassistant/generated/config_flows.py | 1 + tests/components/slack/__init__.py | 73 ++++++++- .../components/slack/fixtures/auth_test.json | 10 ++ tests/components/slack/test_config_flow.py | 140 ++++++++++++++++++ tests/components/slack/test_init.py | 39 +++++ tests/components/slack/test_notify.py | 97 +++--------- 15 files changed, 537 insertions(+), 127 deletions(-) create mode 100644 homeassistant/components/slack/config_flow.py create mode 100644 homeassistant/components/slack/const.py create mode 100644 homeassistant/components/slack/strings.json create mode 100644 homeassistant/components/slack/translations/en.json create mode 100644 tests/components/slack/fixtures/auth_test.json create mode 100644 tests/components/slack/test_config_flow.py create mode 100644 tests/components/slack/test_init.py diff --git a/.coveragerc b/.coveragerc index 7ce0e5ee299..898a14b9c86 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1054,6 +1054,7 @@ omit = homeassistant/components/sky_hub/* homeassistant/components/skybeacon/sensor.py homeassistant/components/skybell/* + homeassistant/components/slack/__init__.py homeassistant/components/slack/notify.py homeassistant/components/sia/__init__.py homeassistant/components/sia/alarm_control_panel.py diff --git a/CODEOWNERS b/CODEOWNERS index d07ce028ba9..68f9017363e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -919,8 +919,8 @@ build.json @home-assistant/supervisor /tests/components/siren/ @home-assistant/core @raman325 /homeassistant/components/sisyphus/ @jkeljo /homeassistant/components/sky_hub/ @rogerselwyn -/homeassistant/components/slack/ @bachya -/tests/components/slack/ @bachya +/homeassistant/components/slack/ @bachya @tkdrob +/tests/components/slack/ @bachya @tkdrob /homeassistant/components/sleepiq/ @mfugate1 @kbickar /tests/components/sleepiq/ @mfugate1 @kbickar /homeassistant/components/slide/ @ualex73 diff --git a/homeassistant/components/slack/__init__.py b/homeassistant/components/slack/__init__.py index e3ae111f2ea..ae52013621f 100644 --- a/homeassistant/components/slack/__init__.py +++ b/homeassistant/components/slack/__init__.py @@ -1,2 +1,62 @@ -"""The slack component.""" -DOMAIN = "slack" +"""The slack integration.""" +import logging + +from aiohttp.client_exceptions import ClientError +from slack import WebClient +from slack.errors import SlackApiError + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_PLATFORM, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client, discovery +from homeassistant.helpers.typing import ConfigType + +from .const import DATA_CLIENT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [Platform.NOTIFY] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Slack component.""" + # Iterate all entries for notify to only get Slack + if Platform.NOTIFY in config: + for entry in config[Platform.NOTIFY]: + if entry[CONF_PLATFORM] == DOMAIN: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Slack from a config entry.""" + session = aiohttp_client.async_get_clientsession(hass) + slack = WebClient(token=entry.data[CONF_API_KEY], run_async=True, session=session) + + try: + await slack.auth_test() + except (SlackApiError, ClientError) as ex: + if isinstance(ex, SlackApiError) and ex.response["error"] == "invalid_auth": + _LOGGER.error("Invalid API key") + return False + raise ConfigEntryNotReady("Error while setting up integration") from ex + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry.data | {DATA_CLIENT: slack} + + hass.async_create_task( + discovery.async_load_platform( + hass, + Platform.NOTIFY, + DOMAIN, + hass.data[DOMAIN][entry.entry_id], + hass.data[DOMAIN], + ) + ) + + return True diff --git a/homeassistant/components/slack/config_flow.py b/homeassistant/components/slack/config_flow.py new file mode 100644 index 00000000000..253750a2310 --- /dev/null +++ b/homeassistant/components/slack/config_flow.py @@ -0,0 +1,87 @@ +"""Config flow for Slack integration.""" +from __future__ import annotations + +import logging + +from slack import WebClient +from slack.errors import SlackApiError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_ICON, CONF_NAME, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import aiohttp_client + +from .const import CONF_DEFAULT_CHANNEL, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Required(CONF_DEFAULT_CHANNEL): str, + vol.Optional(CONF_ICON): str, + vol.Optional(CONF_USERNAME): str, + } +) + + +class SlackFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Slack.""" + + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle a flow initiated by the user.""" + errors = {} + + if user_input is not None: + error, info = await self._async_try_connect(user_input[CONF_API_KEY]) + if error is not None: + errors["base"] = error + elif info is not None: + await self.async_set_unique_id(info["team_id"].lower()) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input.get(CONF_NAME, info["team"]), + data={CONF_NAME: user_input.get(CONF_NAME, info["team"])} + | user_input, + ) + + user_input = user_input or {} + return self.async_show_form( + step_id="user", + data_schema=CONFIG_SCHEMA, + errors=errors, + ) + + async def async_step_import(self, import_config: dict[str, str]) -> FlowResult: + """Import a config entry from configuration.yaml.""" + _LOGGER.warning( + "Configuration of the Slack integration in YAML is deprecated and " + "will be removed in a future release; Your existing configuration " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file" + ) + entries = self._async_current_entries() + if any(x.data[CONF_API_KEY] == import_config[CONF_API_KEY] for x in entries): + return self.async_abort(reason="already_configured") + return await self.async_step_user(import_config) + + async def _async_try_connect( + self, token: str + ) -> tuple[str, None] | tuple[None, dict[str, str]]: + """Try connecting to Slack.""" + session = aiohttp_client.async_get_clientsession(self.hass) + client = WebClient(token=token, run_async=True, session=session) + + try: + info = await client.auth_test() + except SlackApiError as ex: + if ex.response["error"] == "invalid_auth": + return "invalid_auth", None + return "cannot_connect", None + except Exception as ex: # pylint:disable=broad-except + _LOGGER.exception("Unexpected exception: %s", ex) + return "unknown", None + return None, info diff --git a/homeassistant/components/slack/const.py b/homeassistant/components/slack/const.py new file mode 100644 index 00000000000..b7b5707aeeb --- /dev/null +++ b/homeassistant/components/slack/const.py @@ -0,0 +1,16 @@ +"""Constants for the Slack integration.""" +from typing import Final + +ATTR_BLOCKS = "blocks" +ATTR_BLOCKS_TEMPLATE = "blocks_template" +ATTR_FILE = "file" +ATTR_PASSWORD = "password" +ATTR_PATH = "path" +ATTR_URL = "url" +ATTR_USERNAME = "username" + +CONF_DEFAULT_CHANNEL = "default_channel" + +DATA_CLIENT = "client" +DEFAULT_TIMEOUT_SECONDS = 15 +DOMAIN: Final = "slack" diff --git a/homeassistant/components/slack/manifest.json b/homeassistant/components/slack/manifest.json index d54bb9e0ec6..57c1690e647 100644 --- a/homeassistant/components/slack/manifest.json +++ b/homeassistant/components/slack/manifest.json @@ -1,9 +1,10 @@ { "domain": "slack", "name": "Slack", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/slack", "requirements": ["slackclient==2.5.0"], - "codeowners": ["@bachya"], + "codeowners": ["@bachya", "@tkdrob"], "iot_class": "cloud_push", "loggers": ["slack"] } diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index 4dfacda266c..bfcf79ef676 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -20,26 +20,32 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import ATTR_ICON, CONF_API_KEY, CONF_ICON, CONF_USERNAME +from homeassistant.const import ( + ATTR_ICON, + CONF_API_KEY, + CONF_ICON, + CONF_PATH, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client, config_validation as cv, template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import ( + ATTR_BLOCKS, + ATTR_BLOCKS_TEMPLATE, + ATTR_FILE, + ATTR_PASSWORD, + ATTR_PATH, + ATTR_URL, + ATTR_USERNAME, + CONF_DEFAULT_CHANNEL, + DATA_CLIENT, +) + _LOGGER = logging.getLogger(__name__) -ATTR_BLOCKS = "blocks" -ATTR_BLOCKS_TEMPLATE = "blocks_template" -ATTR_FILE = "file" -ATTR_PASSWORD = "password" -ATTR_PATH = "path" -ATTR_URL = "url" -ATTR_USERNAME = "username" - -CONF_DEFAULT_CHANNEL = "default_channel" - -DEFAULT_TIMEOUT_SECONDS = 15 - -FILE_PATH_SCHEMA = vol.Schema({vol.Required(ATTR_PATH): cv.isfile}) +FILE_PATH_SCHEMA = vol.Schema({vol.Required(CONF_PATH): cv.isfile}) FILE_URL_SCHEMA = vol.Schema( { @@ -66,6 +72,7 @@ DATA_SCHEMA = vol.All( cv.ensure_list, [vol.Any(DATA_FILE_SCHEMA, DATA_TEXT_ONLY_SCHEMA)] ) +# Deprecated in Home Assistant 2022.5 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, @@ -109,27 +116,13 @@ async def async_get_service( discovery_info: DiscoveryInfoType | None = None, ) -> SlackNotificationService | None: """Set up the Slack notification service.""" - session = aiohttp_client.async_get_clientsession(hass) - client = WebClient(token=config[CONF_API_KEY], run_async=True, session=session) - - try: - await client.auth_test() - except SlackApiError as err: - _LOGGER.error("Error while setting up integration: %r", err) + if discovery_info is None: return None - except ClientError as err: - _LOGGER.warning( - "Error testing connection to slack: %r " - "Continuing setup anyway, but notify service might not work", - err, - ) return SlackNotificationService( hass, - client, - config[CONF_DEFAULT_CHANNEL], - username=config.get(CONF_USERNAME), - icon=config.get(CONF_ICON), + discovery_info.pop(DATA_CLIENT), + discovery_info, ) @@ -153,16 +146,12 @@ class SlackNotificationService(BaseNotificationService): self, hass: HomeAssistant, client: WebClient, - default_channel: str, - username: str | None, - icon: str | None, + config: dict[str, str], ) -> None: """Initialize.""" - self._client = client - self._default_channel = default_channel self._hass = hass - self._icon = icon - self._username = username + self._client = client + self._config = config async def _async_send_local_file_message( self, @@ -294,7 +283,7 @@ class SlackNotificationService(BaseNotificationService): title = kwargs.get(ATTR_TITLE) targets = _async_sanitize_channel_names( - kwargs.get(ATTR_TARGET, [self._default_channel]) + kwargs.get(ATTR_TARGET, [self._config[CONF_DEFAULT_CHANNEL]]) ) # Message Type 1: A text-only message @@ -312,8 +301,8 @@ class SlackNotificationService(BaseNotificationService): targets, message, title, - username=data.get(ATTR_USERNAME, self._username), - icon=data.get(ATTR_ICON, self._icon), + username=data.get(ATTR_USERNAME, self._config.get(ATTR_USERNAME)), + icon=data.get(ATTR_ICON, self._config.get(ATTR_ICON)), blocks=blocks, ) diff --git a/homeassistant/components/slack/strings.json b/homeassistant/components/slack/strings.json new file mode 100644 index 00000000000..f14129cf156 --- /dev/null +++ b/homeassistant/components/slack/strings.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "description": "Refer to the documentation on getting your Slack API key.", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "default_channel": "Default Channel", + "icon": "Icon", + "username": "[%key:common::config_flow::data::username%]" + }, + "data_description": { + "api_key": "The Slack API token to use for sending Slack messages.", + "default_channel": "The channel to post to if no channel is specified when sending a message.", + "icon": "Use one of the Slack emojis as an Icon for the supplied username.", + "username": "Home Assistant will post to Slack using the username specified." + } + } + }, + "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_service%]" + } + } +} diff --git a/homeassistant/components/slack/translations/en.json b/homeassistant/components/slack/translations/en.json new file mode 100644 index 00000000000..5a97fb1938e --- /dev/null +++ b/homeassistant/components/slack/translations/en.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "default_channel": "Default Channel", + "icon": "Icon", + "username": "Username" + }, + "description": "Refer to the documentation on getting your Slack API key.", + "data_description": { + "api_key": "The Slack API token to use for sending Slack messages.", + "default_channel": "The channel to post to if no channel is specified when sending a message.", + "icon": "Use one of the Slack emojis as an Icon for the supplied username.", + "username": "Home Assistant will post to Slack using the username specified." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ae2ab6339aa..89a9e4e489a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -306,6 +306,7 @@ FLOWS = { "shopping_list", "sia", "simplisafe", + "slack", "sleepiq", "slimproto", "sma", diff --git a/tests/components/slack/__init__.py b/tests/components/slack/__init__.py index b32ec5ef7b1..6a258ce9027 100644 --- a/tests/components/slack/__init__.py +++ b/tests/components/slack/__init__.py @@ -1 +1,72 @@ -"""Slack notification tests.""" +"""Tests for the Slack integration.""" +from __future__ import annotations + +import json + +from homeassistant.components.slack.const import CONF_DEFAULT_CHANNEL, DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + +AUTH_URL = "https://www.slack.com/api/auth.test" + +TOKEN = "abc123" +TEAM_NAME = "Test Team" +TEAM_ID = "abc123def" + +CONF_INPUT = {CONF_API_KEY: TOKEN, CONF_DEFAULT_CHANNEL: "test_channel"} + +CONF_DATA = CONF_INPUT | {CONF_NAME: TEAM_NAME} + + +def create_entry(hass: HomeAssistant) -> ConfigEntry: + """Add config entry in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=CONF_DATA, + unique_id=TEAM_ID, + ) + entry.add_to_hass(hass) + return entry + + +def mock_connection( + aioclient_mock: AiohttpClientMocker, error: str | None = None +) -> None: + """Mock connection.""" + if error is not None: + if error == "invalid_auth": + aioclient_mock.post( + AUTH_URL, + text=json.dumps({"ok": False, "error": "invalid_auth"}), + ) + else: + aioclient_mock.post( + AUTH_URL, + text=json.dumps({"ok": False, "error": "cannot_connect"}), + ) + else: + aioclient_mock.post( + AUTH_URL, + text=load_fixture("slack/auth_test.json"), + ) + + +async def async_init_integration( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + skip_setup: bool = False, + error: str | None = None, +) -> ConfigEntry: + """Set up the Slack integration in Home Assistant.""" + entry = create_entry(hass) + mock_connection(aioclient_mock, error) + + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/slack/fixtures/auth_test.json b/tests/components/slack/fixtures/auth_test.json new file mode 100644 index 00000000000..b7b6ee3e9bd --- /dev/null +++ b/tests/components/slack/fixtures/auth_test.json @@ -0,0 +1,10 @@ +{ + "ok": true, + "url": "https://newscorp-tech.slack.com/", + "team": "Test Team", + "user": "user.name", + "team_id": "ABC123DEF", + "user_id": "ABCDEF12345", + "enterprise_id": "123ABCDEF", + "is_enterprise_install": false +} diff --git a/tests/components/slack/test_config_flow.py b/tests/components/slack/test_config_flow.py new file mode 100644 index 00000000000..850690783e8 --- /dev/null +++ b/tests/components/slack/test_config_flow.py @@ -0,0 +1,140 @@ +"""Test Slack config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.slack.const import DOMAIN +from homeassistant.core import HomeAssistant + +from . import CONF_DATA, CONF_INPUT, TEAM_NAME, create_entry, mock_connection + +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_flow_user( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test user initialized flow.""" + mock_connection(aioclient_mock) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_INPUT, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == TEAM_NAME + assert result["data"] == CONF_DATA + + +async def test_flow_user_already_configured( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test user initialized flow with duplicate server.""" + create_entry(hass) + mock_connection(aioclient_mock) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_INPUT, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_user_invalid_auth( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test user initialized flow with invalid token.""" + mock_connection(aioclient_mock, "invalid_auth") + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=CONF_DATA, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_flow_user_cannot_connect( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test user initialized flow with unreachable server.""" + mock_connection(aioclient_mock, "cannot_connect") + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=CONF_DATA, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: + """Test user initialized flow with unreachable server.""" + with patch( + "homeassistant.components.slack.config_flow.WebClient.auth_test" + ) as mock: + mock.side_effect = Exception + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=CONF_DATA, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "unknown"} + + +async def test_flow_import( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test an import flow.""" + mock_connection(aioclient_mock) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=CONF_DATA, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == TEAM_NAME + assert result["data"] == CONF_DATA + + +async def test_flow_import_no_name( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test import flow with no name in config.""" + mock_connection(aioclient_mock) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=CONF_INPUT, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == TEAM_NAME + assert result["data"] == CONF_DATA + + +async def test_flow_import_already_configured( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test an import flow already configured.""" + create_entry(hass) + mock_connection(aioclient_mock) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=CONF_DATA, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/slack/test_init.py b/tests/components/slack/test_init.py new file mode 100644 index 00000000000..487a65b8ef0 --- /dev/null +++ b/tests/components/slack/test_init.py @@ -0,0 +1,39 @@ +"""Test Slack integration.""" +from homeassistant.components.slack.const import DOMAIN +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import CONF_DATA, async_init_integration + +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: + """Test Slack setup.""" + entry: ConfigEntry = await async_init_integration(hass, aioclient_mock) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.data == CONF_DATA + + +async def test_async_setup_entry_not_ready( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" + entry: ConfigEntry = await async_init_integration( + hass, aioclient_mock, error="cannot_connect" + ) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_async_setup_entry_invalid_auth( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test invalid auth during setup.""" + entry: ConfigEntry = await async_init_integration( + hass, aioclient_mock, error="invalid_auth" + ) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_ERROR diff --git a/tests/components/slack/test_notify.py b/tests/components/slack/test_notify.py index f10673cced4..b5fa08fa54f 100644 --- a/tests/components/slack/test_notify.py +++ b/tests/components/slack/test_notify.py @@ -1,13 +1,10 @@ """Test slack notifications.""" from __future__ import annotations -import copy import logging -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, Mock from _pytest.logging import LogCaptureFixture -import aiohttp -from slack.errors import SlackApiError from homeassistant.components import notify from homeassistant.components.slack import DOMAIN @@ -15,15 +12,9 @@ from homeassistant.components.slack.notify import ( CONF_DEFAULT_CHANNEL, SlackNotificationService, ) -from homeassistant.const import ( - CONF_API_KEY, - CONF_ICON, - CONF_NAME, - CONF_PLATFORM, - CONF_USERNAME, -) -from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component +from homeassistant.const import ATTR_ICON, CONF_API_KEY, CONF_NAME, CONF_PLATFORM + +from . import CONF_DATA MODULE_PATH = "homeassistant.components.slack.notify" SERVICE_NAME = f"notify_{DOMAIN}" @@ -47,74 +38,14 @@ def filter_log_records(caplog: LogCaptureFixture) -> list[logging.LogRecord]: ] -async def test_setup(hass: HomeAssistant, caplog: LogCaptureFixture): - """Test setup slack notify.""" - config = DEFAULT_CONFIG - - with patch( - MODULE_PATH + ".aiohttp_client", - **{"async_get_clientsession.return_value": (session := Mock())}, - ), patch( - MODULE_PATH + ".WebClient", - return_value=(client := AsyncMock()), - ) as mock_client: - - await async_setup_component(hass, notify.DOMAIN, config) - await hass.async_block_till_done() - assert hass.services.has_service(notify.DOMAIN, SERVICE_NAME) - caplog_records_slack = filter_log_records(caplog) - assert len(caplog_records_slack) == 0 - mock_client.assert_called_with(token="12345", run_async=True, session=session) - client.auth_test.assert_called_once_with() - - -async def test_setup_clientError(hass: HomeAssistant, caplog: LogCaptureFixture): - """Test setup slack notify with aiohttp.ClientError exception.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config[notify.DOMAIN][0].update({CONF_USERNAME: "user", CONF_ICON: "icon"}) - - with patch( - MODULE_PATH + ".aiohttp_client", - **{"async_get_clientsession.return_value": Mock()}, - ), patch(MODULE_PATH + ".WebClient", return_value=(client := AsyncMock())): - - client.auth_test.side_effect = [aiohttp.ClientError] - await async_setup_component(hass, notify.DOMAIN, config) - await hass.async_block_till_done() - assert hass.services.has_service(notify.DOMAIN, SERVICE_NAME) - caplog_records_slack = filter_log_records(caplog) - assert len(caplog_records_slack) == 1 - record = caplog_records_slack[0] - assert record.levelno == logging.WARNING - assert aiohttp.ClientError.__qualname__ in record.message - - -async def test_setup_slackApiError(hass: HomeAssistant, caplog: LogCaptureFixture): - """Test setup slack notify with SlackApiError exception.""" - config = DEFAULT_CONFIG - - with patch( - MODULE_PATH + ".aiohttp_client", - **{"async_get_clientsession.return_value": Mock()}, - ), patch(MODULE_PATH + ".WebClient", return_value=(client := AsyncMock())): - - client.auth_test.side_effect = [err := SlackApiError("msg", "resp")] - await async_setup_component(hass, notify.DOMAIN, config) - await hass.async_block_till_done() - assert hass.services.has_service(notify.DOMAIN, SERVICE_NAME) is False - caplog_records_slack = filter_log_records(caplog) - assert len(caplog_records_slack) == 1 - record = caplog_records_slack[0] - assert record.levelno == logging.ERROR - assert err.__class__.__qualname__ in record.message - - async def test_message_includes_default_emoji(): """Tests that default icon is used when no message icon is given.""" mock_client = Mock() mock_client.chat_postMessage = AsyncMock() expected_icon = ":robot_face:" - service = SlackNotificationService(None, mock_client, "_", "_", expected_icon) + service = SlackNotificationService( + None, mock_client, CONF_DATA | {ATTR_ICON: expected_icon} + ) await service.async_send_message("test") @@ -128,7 +59,9 @@ async def test_message_emoji_overrides_default(): """Tests that overriding the default icon emoji when sending a message works.""" mock_client = Mock() mock_client.chat_postMessage = AsyncMock() - service = SlackNotificationService(None, mock_client, "_", "_", "default_icon") + service = SlackNotificationService( + None, mock_client, CONF_DATA | {ATTR_ICON: "default_icon"} + ) expected_icon = ":new:" await service.async_send_message("test", data={"icon": expected_icon}) @@ -144,7 +77,9 @@ async def test_message_includes_default_icon_url(): mock_client = Mock() mock_client.chat_postMessage = AsyncMock() expected_icon = "https://example.com/hass.png" - service = SlackNotificationService(None, mock_client, "_", "_", expected_icon) + service = SlackNotificationService( + None, mock_client, CONF_DATA | {ATTR_ICON: expected_icon} + ) await service.async_send_message("test") @@ -158,10 +93,12 @@ async def test_message_icon_url_overrides_default(): """Tests that overriding the default icon url when sending a message works.""" mock_client = Mock() mock_client.chat_postMessage = AsyncMock() - service = SlackNotificationService(None, mock_client, "_", "_", "default_icon") + service = SlackNotificationService( + None, mock_client, CONF_DATA | {ATTR_ICON: "default_icon"} + ) expected_icon = "https://example.com/hass.png" - await service.async_send_message("test", data={"icon": expected_icon}) + await service.async_send_message("test", data={ATTR_ICON: expected_icon}) mock_fn = mock_client.chat_postMessage mock_fn.assert_called_once() From e8a8d352898bd025f0fd57f5c574b5f6ef4c0309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 14 May 2022 02:23:18 +0200 Subject: [PATCH 0491/3516] Add Sensors for Airzone WebServer (#69748) --- homeassistant/components/airzone/entity.py | 67 +++++++++++++++------- homeassistant/components/airzone/sensor.py | 61 +++++++++++++++++++- tests/components/airzone/test_sensor.py | 11 +++- tests/components/airzone/util.py | 3 +- 4 files changed, 116 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/airzone/entity.py b/homeassistant/components/airzone/entity.py index 687d7873ece..f697a364bc8 100644 --- a/homeassistant/components/airzone/entity.py +++ b/homeassistant/components/airzone/entity.py @@ -7,16 +7,19 @@ from aioairzone.const import ( AZD_FIRMWARE, AZD_FULL_NAME, AZD_ID, + AZD_MAC, AZD_MODEL, AZD_NAME, AZD_SYSTEM, AZD_SYSTEMS, AZD_THERMOSTAT_FW, AZD_THERMOSTAT_MODEL, + AZD_WEBSERVER, AZD_ZONES, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -46,17 +49,15 @@ class AirzoneSystemEntity(AirzoneEntity): self.system_id = system_data[AZD_ID] - self._attr_device_info: DeviceInfo = { - "identifiers": {(DOMAIN, f"{entry.entry_id}_{self.system_id}")}, - "manufacturer": MANUFACTURER, - "model": self.get_airzone_value(AZD_MODEL), - "name": self.get_airzone_value(AZD_FULL_NAME), - "sw_version": self.get_airzone_value(AZD_FIRMWARE), - "via_device": (DOMAIN, f"{entry.entry_id}_ws"), - } - self._attr_unique_id = ( - entry.entry_id if entry.unique_id is None else entry.unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{entry.entry_id}_{self.system_id}")}, + manufacturer=MANUFACTURER, + model=self.get_airzone_value(AZD_MODEL), + name=self.get_airzone_value(AZD_FULL_NAME), + sw_version=self.get_airzone_value(AZD_FIRMWARE), + via_device=(DOMAIN, f"{entry.entry_id}_ws"), ) + self._attr_unique_id = entry.unique_id or entry.entry_id def get_airzone_value(self, key: str) -> Any: """Return system value by key.""" @@ -67,6 +68,34 @@ class AirzoneSystemEntity(AirzoneEntity): return value +class AirzoneWebServerEntity(AirzoneEntity): + """Define an Airzone WebServer entity.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + entry: ConfigEntry, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + mac = self.get_airzone_value(AZD_MAC) + + self._attr_device_info = DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, mac)}, + identifiers={(DOMAIN, f"{entry.entry_id}_ws")}, + manufacturer=MANUFACTURER, + model=self.get_airzone_value(AZD_MODEL), + name=self.get_airzone_value(AZD_FULL_NAME), + sw_version=self.get_airzone_value(AZD_FIRMWARE), + ) + self._attr_unique_id = entry.unique_id or entry.entry_id + + def get_airzone_value(self, key: str) -> Any: + """Return system value by key.""" + return self.coordinator.data[AZD_WEBSERVER].get(key) + + class AirzoneZoneEntity(AirzoneEntity): """Define an Airzone Zone entity.""" @@ -84,17 +113,15 @@ class AirzoneZoneEntity(AirzoneEntity): self.system_zone_id = system_zone_id self.zone_id = zone_data[AZD_ID] - self._attr_device_info: DeviceInfo = { - "identifiers": {(DOMAIN, f"{entry.entry_id}_{system_zone_id}")}, - "manufacturer": MANUFACTURER, - "model": self.get_airzone_value(AZD_THERMOSTAT_MODEL), - "name": f"Airzone [{system_zone_id}] {zone_data[AZD_NAME]}", - "sw_version": self.get_airzone_value(AZD_THERMOSTAT_FW), - "via_device": (DOMAIN, f"{entry.entry_id}_{self.system_id}"), - } - self._attr_unique_id = ( - entry.entry_id if entry.unique_id is None else entry.unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{entry.entry_id}_{system_zone_id}")}, + manufacturer=MANUFACTURER, + model=self.get_airzone_value(AZD_THERMOSTAT_MODEL), + name=f"Airzone [{system_zone_id}] {zone_data[AZD_NAME]}", + sw_version=self.get_airzone_value(AZD_THERMOSTAT_FW), + via_device=(DOMAIN, f"{entry.entry_id}_{self.system_id}"), ) + self._attr_unique_id = entry.unique_id or entry.entry_id def get_airzone_value(self, key: str) -> Any: """Return zone value by key.""" diff --git a/homeassistant/components/airzone/sensor.py b/homeassistant/components/airzone/sensor.py index 3e81f5df094..1671eb919a7 100644 --- a/homeassistant/components/airzone/sensor.py +++ b/homeassistant/components/airzone/sensor.py @@ -3,7 +3,15 @@ from __future__ import annotations from typing import Any, Final -from aioairzone.const import AZD_HUMIDITY, AZD_NAME, AZD_TEMP, AZD_TEMP_UNIT, AZD_ZONES +from aioairzone.const import ( + AZD_HUMIDITY, + AZD_NAME, + AZD_TEMP, + AZD_TEMP_UNIT, + AZD_WEBSERVER, + AZD_WIFI_RSSI, + AZD_ZONES, +) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -12,13 +20,30 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import ( + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, TEMP_UNIT_LIB_TO_HASS from .coordinator import AirzoneUpdateCoordinator -from .entity import AirzoneEntity, AirzoneZoneEntity +from .entity import AirzoneEntity, AirzoneWebServerEntity, AirzoneZoneEntity + +WEBSERVER_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( + SensorEntityDescription( + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + key=AZD_WIFI_RSSI, + name="RSSI", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + ), +) ZONE_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = ( SensorEntityDescription( @@ -45,6 +70,19 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][entry.entry_id] sensors: list[AirzoneSensor] = [] + + if AZD_WEBSERVER in coordinator.data: + ws_data = coordinator.data[AZD_WEBSERVER] + for description in WEBSERVER_SENSOR_TYPES: + if description.key in ws_data: + sensors.append( + AirzoneWebServerSensor( + coordinator, + description, + entry, + ) + ) + for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items(): for description in ZONE_SENSOR_TYPES: if description.key in zone_data: @@ -70,6 +108,23 @@ class AirzoneSensor(AirzoneEntity, SensorEntity): self._attr_native_value = self.get_airzone_value(self.entity_description.key) +class AirzoneWebServerSensor(AirzoneWebServerEntity, AirzoneSensor): + """Define an Airzone WebServer sensor.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + description: SensorEntityDescription, + entry: ConfigEntry, + ) -> None: + """Initialize.""" + super().__init__(coordinator, entry) + self._attr_name = f"WebServer {description.name}" + self._attr_unique_id = f"{self._attr_unique_id}_ws_{description.key}" + self.entity_description = description + self._async_update_attrs() + + class AirzoneZoneSensor(AirzoneZoneEntity, AirzoneSensor): """Define an Airzone Zone sensor.""" diff --git a/tests/components/airzone/test_sensor.py b/tests/components/airzone/test_sensor.py index c68be2abbab..bd57129cae0 100644 --- a/tests/components/airzone/test_sensor.py +++ b/tests/components/airzone/test_sensor.py @@ -1,15 +1,24 @@ """The sensor tests for the Airzone platform.""" +from unittest.mock import AsyncMock + from homeassistant.core import HomeAssistant from .util import async_init_integration -async def test_airzone_create_sensors(hass: HomeAssistant) -> None: +async def test_airzone_create_sensors( + hass: HomeAssistant, entity_registry_enabled_by_default: AsyncMock +) -> None: """Test creation of sensors.""" await async_init_integration(hass) + # WebServer + state = hass.states.get("sensor.webserver_rssi") + assert state.state == "-42" + + # Zones state = hass.states.get("sensor.despacho_temperature") assert state.state == "21.2" diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index ede4bcc3b53..6b81c493eb6 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -34,7 +34,6 @@ from aioairzone.const import ( API_WIFI_RSSI, API_ZONE_ID, ) -from aioairzone.exceptions import InvalidMethod from homeassistant.components.airzone import DOMAIN from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT @@ -219,7 +218,7 @@ async def async_init_integration( return_value=HVAC_SYSTEMS_MOCK, ), patch( "homeassistant.components.airzone.AirzoneLocalApi.get_webserver", - side_effect=InvalidMethod, + return_value=HVAC_WEBSERVER_MOCK, ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From abe78b1212602d8b19562d6acc0adf9361302327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 14 May 2022 02:24:29 +0200 Subject: [PATCH 0492/3516] Add QNAP QSW Button platform (#70980) Co-authored-by: J. Nick Koston --- homeassistant/components/qnap_qsw/__init__.py | 2 +- homeassistant/components/qnap_qsw/button.py | 77 +++++++++++++++++++ homeassistant/components/qnap_qsw/const.py | 1 + tests/components/qnap_qsw/test_button.py | 37 +++++++++ tests/components/qnap_qsw/util.py | 6 ++ 5 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/qnap_qsw/button.py create mode 100644 tests/components/qnap_qsw/test_button.py diff --git a/homeassistant/components/qnap_qsw/__init__.py b/homeassistant/components/qnap_qsw/__init__.py index 838a567199d..26ed8066686 100644 --- a/homeassistant/components/qnap_qsw/__init__.py +++ b/homeassistant/components/qnap_qsw/__init__.py @@ -11,7 +11,7 @@ from homeassistant.helpers import aiohttp_client from .const import DOMAIN from .coordinator import QswUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/qnap_qsw/button.py b/homeassistant/components/qnap_qsw/button.py new file mode 100644 index 00000000000..1c13310fe05 --- /dev/null +++ b/homeassistant/components/qnap_qsw/button.py @@ -0,0 +1,77 @@ +"""Support for the QNAP QSW buttons.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Final + +from aioqsw.localapi import QnapQswApi + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, QSW_REBOOT +from .coordinator import QswUpdateCoordinator +from .entity import QswEntity + + +@dataclass +class QswButtonDescriptionMixin: + """Mixin to describe a Button entity.""" + + press_action: Callable[[QnapQswApi], Awaitable[bool]] + + +@dataclass +class QswButtonDescription(ButtonEntityDescription, QswButtonDescriptionMixin): + """Class to describe a Button entity.""" + + +BUTTON_TYPES: Final[tuple[QswButtonDescription, ...]] = ( + QswButtonDescription( + device_class=ButtonDeviceClass.RESTART, + entity_category=EntityCategory.CONFIG, + key=QSW_REBOOT, + name="Reboot", + press_action=lambda qsw: qsw.reboot(), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add QNAP QSW buttons from a config_entry.""" + coordinator: QswUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + QswButton(coordinator, description, entry) for description in BUTTON_TYPES + ) + + +class QswButton(QswEntity, ButtonEntity): + """Define a QNAP QSW button.""" + + entity_description: QswButtonDescription + + def __init__( + self, + coordinator: QswUpdateCoordinator, + description: QswButtonDescription, + entry: ConfigEntry, + ) -> None: + """Initialize.""" + super().__init__(coordinator, entry) + self._attr_name = f"{self.product} {description.name}" + self._attr_unique_id = f"{entry.unique_id}_{description.key}" + self.entity_description = description + + async def async_press(self) -> None: + """Triggers the QNAP QSW button action.""" + await self.entity_description.press_action(self.coordinator.qsw) diff --git a/homeassistant/components/qnap_qsw/const.py b/homeassistant/components/qnap_qsw/const.py index a6cacfd1c40..e583c0250f4 100644 --- a/homeassistant/components/qnap_qsw/const.py +++ b/homeassistant/components/qnap_qsw/const.py @@ -10,4 +10,5 @@ MANUFACTURER: Final = "QNAP" RPM: Final = "rpm" +QSW_REBOOT = "reboot" QSW_TIMEOUT_SEC: Final = 25 diff --git a/tests/components/qnap_qsw/test_button.py b/tests/components/qnap_qsw/test_button.py new file mode 100644 index 00000000000..5423c9686d4 --- /dev/null +++ b/tests/components/qnap_qsw/test_button.py @@ -0,0 +1,37 @@ +"""The sensor tests for the QNAP QSW platform.""" + +from unittest.mock import patch + +from homeassistant.components.button.const import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant + +from .util import SYSTEM_COMMAND_MOCK, USERS_VERIFICATION_MOCK, async_init_integration + + +async def test_qnap_buttons(hass: HomeAssistant) -> None: + """Test buttons.""" + + await async_init_integration(hass) + + state = hass.states.get("button.qsw_m408_4c_reboot") + assert state + assert state.state == STATE_UNKNOWN + + with patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_users_verification", + return_value=USERS_VERIFICATION_MOCK, + ) as mock_users_verification, patch( + "homeassistant.components.qnap_qsw.QnapQswApi.post_system_command", + return_value=SYSTEM_COMMAND_MOCK, + ) as mock_post_system_command: + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.qsw_m408_4c_reboot"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_users_verification.assert_called_once() + mock_post_system_command.assert_called_once() diff --git a/tests/components/qnap_qsw/util.py b/tests/components/qnap_qsw/util.py index 57b7b61a59d..28e7f7881d5 100644 --- a/tests/components/qnap_qsw/util.py +++ b/tests/components/qnap_qsw/util.py @@ -90,6 +90,12 @@ FIRMWARE_INFO_MOCK = { }, } +SYSTEM_COMMAND_MOCK = { + API_ERROR_CODE: 200, + API_ERROR_MESSAGE: "OK", + API_RESULT: "None", +} + SYSTEM_SENSOR_MOCK = { API_ERROR_CODE: 200, API_ERROR_MESSAGE: "OK", From a8f1dda004c2f32f2c29e95238f08a7bf3c1b7f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 May 2022 20:26:09 -0400 Subject: [PATCH 0493/3516] Use ciso8601 for parsing MySQLdb datetimes (#71818) * Use ciso8601 for parsing MySQLDB datetimes The default parser is this: https://github.com/PyMySQL/mysqlclient/blob/5340191feb16b1f99a7b43fe7c74a2f690138cb1/MySQLdb/times.py#L66 * tweak * tweak * add coverage for building the MySQLdb connect conv param --- homeassistant/components/recorder/const.py | 1 + homeassistant/components/recorder/core.py | 10 +++++++++ homeassistant/components/recorder/util.py | 25 ++++++++++++++++++++++ tests/components/recorder/test_util.py | 19 +++++++++++++++- 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index 5d650ec83e2..e558d19b530 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -10,6 +10,7 @@ from homeassistant.helpers.json import JSONEncoder DATA_INSTANCE = "recorder_instance" SQLITE_URL_PREFIX = "sqlite://" +MYSQLDB_URL_PREFIX = "mysql://" DOMAIN = "recorder" CONF_DB_INTEGRITY_CHECK = "db_integrity_check" diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index a008ae6767b..ba0e5ba17d1 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -41,6 +41,7 @@ from .const import ( DB_WORKER_PREFIX, KEEPALIVE_TIME, MAX_QUEUE_BACKLOG, + MYSQLDB_URL_PREFIX, SQLITE_URL_PREFIX, SupportedDialect, ) @@ -77,6 +78,7 @@ from .tasks import ( WaitTask, ) from .util import ( + build_mysqldb_conv, dburl_to_path, end_incomplete_runs, is_second_sunday, @@ -1014,6 +1016,14 @@ class Recorder(threading.Thread): kwargs["pool_reset_on_return"] = None elif self.db_url.startswith(SQLITE_URL_PREFIX): kwargs["poolclass"] = RecorderPool + elif self.db_url.startswith(MYSQLDB_URL_PREFIX): + # If they have configured MySQLDB but don't have + # the MySQLDB module installed this will throw + # an ImportError which we suppress here since + # sqlalchemy will give them a better error when + # it tried to import it below. + with contextlib.suppress(ImportError): + kwargs["connect_args"] = {"conv": build_mysqldb_conv()} else: kwargs["echo"] = False diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 9f6bef86a29..f48a6126ea9 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -15,6 +15,7 @@ from awesomeversion import ( AwesomeVersionException, AwesomeVersionStrategy, ) +import ciso8601 from sqlalchemy import text from sqlalchemy.engine.cursor import CursorFetchStrategy from sqlalchemy.exc import OperationalError, SQLAlchemyError @@ -331,6 +332,30 @@ def _extract_version_from_server_response( return None +def _datetime_or_none(value: str) -> datetime | None: + """Fast version of mysqldb DateTime_or_None. + + https://github.com/PyMySQL/mysqlclient/blob/v2.1.0/MySQLdb/times.py#L66 + """ + try: + return ciso8601.parse_datetime(value) + except ValueError: + return None + + +def build_mysqldb_conv() -> dict: + """Build a MySQLDB conv dict that uses cisco8601 to parse datetimes.""" + # Late imports since we only call this if they are using mysqldb + from MySQLdb.constants import ( # pylint: disable=import-outside-toplevel,import-error + FIELD_TYPE, + ) + from MySQLdb.converters import ( # pylint: disable=import-outside-toplevel,import-error + conversions, + ) + + return {**conversions, FIELD_TYPE.DATETIME: _datetime_or_none} + + def setup_connection_for_dialect( instance: Recorder, dialect_name: str, diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 6b1093ee038..237030c1186 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta import os import sqlite3 -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, Mock, patch import pytest from sqlalchemy import text @@ -641,3 +641,20 @@ def test_is_second_sunday(): assert is_second_sunday(datetime(2022, 5, 8, 0, 0, 0, tzinfo=dt_util.UTC)) is True assert is_second_sunday(datetime(2022, 1, 10, 0, 0, 0, tzinfo=dt_util.UTC)) is False + + +def test_build_mysqldb_conv(): + """Test building the MySQLdb connect conv param.""" + mock_converters = Mock(conversions={"original": "preserved"}) + mock_constants = Mock(FIELD_TYPE=Mock(DATETIME="DATETIME")) + with patch.dict( + "sys.modules", + **{"MySQLdb.constants": mock_constants, "MySQLdb.converters": mock_converters}, + ): + conv = util.build_mysqldb_conv() + + assert conv["original"] == "preserved" + assert conv["DATETIME"]("INVALID") is None + assert conv["DATETIME"]("2022-05-13T22:33:12.741") == datetime( + 2022, 5, 13, 22, 33, 12, 741000, tzinfo=None + ) From 0bc843c133468f535f0168eef88850358dc1730a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 14 May 2022 02:40:34 +0200 Subject: [PATCH 0494/3516] Add remaining tests for Sensibo (#71764) * Sensibo remaining tests * Use fixture for enable entity --- .coveragerc | 4 - tests/components/sensibo/fixtures/data.json | 2 +- tests/components/sensibo/response.py | 400 -------------------- tests/components/sensibo/test_entity.py | 27 +- tests/components/sensibo/test_number.py | 101 +++++ tests/components/sensibo/test_select.py | 163 ++++++++ tests/components/sensibo/test_sensor.py | 50 +++ tests/components/sensibo/test_update.py | 47 +++ 8 files changed, 367 insertions(+), 427 deletions(-) delete mode 100644 tests/components/sensibo/response.py create mode 100644 tests/components/sensibo/test_number.py create mode 100644 tests/components/sensibo/test_select.py create mode 100644 tests/components/sensibo/test_sensor.py create mode 100644 tests/components/sensibo/test_update.py diff --git a/.coveragerc b/.coveragerc index 898a14b9c86..2a8e6cdec9a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1020,10 +1020,6 @@ omit = homeassistant/components/senseme/fan.py homeassistant/components/senseme/light.py homeassistant/components/senseme/switch.py - homeassistant/components/sensibo/number.py - homeassistant/components/sensibo/select.py - homeassistant/components/sensibo/sensor.py - homeassistant/components/sensibo/update.py homeassistant/components/senz/__init__.py homeassistant/components/senz/api.py homeassistant/components/senz/climate.py diff --git a/tests/components/sensibo/fixtures/data.json b/tests/components/sensibo/fixtures/data.json index c75423fe464..c787ea5592c 100644 --- a/tests/components/sensibo/fixtures/data.json +++ b/tests/components/sensibo/fixtures/data.json @@ -61,7 +61,7 @@ "firmwareType": "esp8266ex", "productModel": "skyv2", "configGroup": "stable", - "currentlyAvailableFirmwareVersion": "SKY30046", + "currentlyAvailableFirmwareVersion": "SKY30048", "cleanFiltersNotificationEnabled": false, "shouldShowFilterCleaningNotification": false, "isGeofenceOnExitEnabled": false, diff --git a/tests/components/sensibo/response.py b/tests/components/sensibo/response.py deleted file mode 100644 index e6b39b81881..00000000000 --- a/tests/components/sensibo/response.py +++ /dev/null @@ -1,400 +0,0 @@ -"""Test api response for the Sensibo integration.""" -from __future__ import annotations - -from pysensibo.model import MotionSensor, SensiboData, SensiboDevice - -DATA_FROM_API = SensiboData( - raw={ - "status": "success", - "result": [ - { - "id": "ABC999111", - "qrId": "AAAAAAAAAA", - "room": {"uid": "99TT99TT", "name": "Hallway", "icon": "Lounge"}, - "acState": { - "timestamp": { - "time": "2022-04-30T19:58:15.544787Z", - "secondsAgo": 0, - }, - "on": False, - "mode": "fan", - "fanLevel": "high", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", - }, - "location": { - "id": "ZZZZZZZZZZZZ", - "name": "Home", - "latLon": [58.9806976, 20.5864297], - "address": ["Sealand 99", "Some county"], - "country": "United Country", - "createTime": { - "time": "2020-03-21T15:44:15Z", - "secondsAgo": 66543240, - }, - "updateTime": None, - "features": [], - "geofenceTriggerRadius": 200, - "subscription": None, - "technician": None, - "shareAnalytics": False, - "occupancy": "n/a", - }, - "accessPoint": {"ssid": "SENSIBO-I-99999", "password": None}, - "macAddress": "00:02:00:B6:00:00", - "autoOffMinutes": None, - "autoOffEnabled": False, - "antiMoldTimer": None, - "antiMoldConfig": None, - } - ], - }, - parsed={ - "ABC999111": SensiboDevice( - id="ABC999111", - mac="00:02:00:B6:00:00", - name="Hallway", - ac_states={ - "timestamp": {"time": "2022-04-30T19:58:15.544787Z", "secondsAgo": 0}, - "on": False, - "mode": "heat", - "fanLevel": "high", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", - }, - temp=22.4, - humidity=38, - target_temp=25, - hvac_mode="heat", - device_on=True, - fan_mode="high", - swing_mode="stopped", - horizontal_swing_mode="stopped", - light_mode="on", - available=True, - hvac_modes=["cool", "heat", "dry", "auto", "fan", "off"], - fan_modes=["quiet", "low", "medium"], - swing_modes=[ - "stopped", - "fixedTop", - "fixedMiddleTop", - ], - horizontal_swing_modes=[ - "stopped", - "fixedLeft", - "fixedCenterLeft", - ], - light_modes=["on", "off"], - temp_unit="C", - temp_list=[18, 19, 20], - temp_step=1, - active_features=[ - "timestamp", - "on", - "mode", - "fanLevel", - "swing", - "targetTemperature", - "horizontalSwing", - "light", - ], - full_features={ - "targetTemperature", - "fanLevel", - "swing", - "horizontalSwing", - "light", - }, - state="heat", - fw_ver="SKY30046", - fw_ver_available="SKY30046", - fw_type="esp8266ex", - model="skyv2", - calibration_temp=0.1, - calibration_hum=0.1, - full_capabilities={ - "modes": { - "cool": { - "temperatures": { - "F": { - "isNative": False, - "values": [ - 64, - 66, - 68, - ], - }, - "C": { - "isNative": True, - "values": [ - 18, - 19, - 20, - ], - }, - }, - "fanLevels": [ - "quiet", - "low", - "medium", - ], - "swing": [ - "stopped", - "fixedTop", - "fixedMiddleTop", - ], - "horizontalSwing": [ - "stopped", - "fixedLeft", - "fixedCenterLeft", - ], - "light": ["on", "off"], - }, - "heat": { - "temperatures": { - "F": { - "isNative": False, - "values": [ - 63, - 64, - 66, - ], - }, - "C": { - "isNative": True, - "values": [ - 17, - 18, - 19, - ], - }, - }, - "fanLevels": ["quiet", "low", "medium"], - "swing": [ - "stopped", - "fixedTop", - "fixedMiddleTop", - ], - "horizontalSwing": [ - "stopped", - "fixedLeft", - "fixedCenterLeft", - ], - "light": ["on", "off"], - }, - "dry": { - "temperatures": { - "F": { - "isNative": False, - "values": [ - 64, - 66, - 68, - ], - }, - "C": { - "isNative": True, - "values": [ - 18, - 19, - 20, - ], - }, - }, - "swing": [ - "stopped", - "fixedTop", - "fixedMiddleTop", - ], - "horizontalSwing": [ - "stopped", - "fixedLeft", - "fixedCenterLeft", - ], - "light": ["on", "off"], - }, - "auto": { - "temperatures": { - "F": { - "isNative": False, - "values": [ - 64, - 66, - 68, - ], - }, - "C": { - "isNative": True, - "values": [ - 18, - 19, - 20, - ], - }, - }, - "fanLevels": [ - "quiet", - "low", - "medium", - ], - "swing": [ - "stopped", - "fixedTop", - "fixedMiddleTop", - ], - "horizontalSwing": [ - "stopped", - "fixedLeft", - "fixedCenterLeft", - ], - "light": ["on", "off"], - }, - "fan": { - "temperatures": {}, - "fanLevels": [ - "quiet", - "low", - ], - "swing": [ - "stopped", - "fixedTop", - "fixedMiddleTop", - ], - "horizontalSwing": [ - "stopped", - "fixedLeft", - "fixedCenterLeft", - ], - "light": ["on", "off"], - }, - } - }, - motion_sensors={ - "AABBCC": MotionSensor( - id="AABBCC", - alive=True, - motion=True, - fw_ver="V17", - fw_type="nrf52", - is_main_sensor=True, - battery_voltage=3000, - humidity=57, - temperature=23.9, - model="motion_sensor", - rssi=-72, - ) - }, - pm25=None, - room_occupied=True, - update_available=False, - schedules={}, - pure_boost_enabled=None, - pure_sensitivity=None, - pure_ac_integration=None, - pure_geo_integration=None, - pure_measure_integration=None, - timer_on=False, - timer_id=None, - timer_state_on=None, - timer_time=None, - smart_on=False, - smart_type="temperature", - smart_low_temp_threshold=0.0, - smart_high_temp_threshold=27.5, - smart_low_state={ - "on": True, - "targetTemperature": 21, - "temperatureUnit": "C", - "mode": "heat", - "fanLevel": "low", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", - }, - smart_high_state={ - "on": True, - "targetTemperature": 21, - "temperatureUnit": "C", - "mode": "cool", - "fanLevel": "high", - "swing": "stopped", - "horizontalSwing": "stopped", - "light": "on", - }, - filter_clean=False, - filter_last_reset="2022-03-12T15:24:26Z", - ), - "AAZZAAZZ": SensiboDevice( - id="AAZZAAZZ", - mac="00:01:00:01:00:01", - name="Kitchen", - ac_states={ - "timestamp": {"time": "2022-04-30T19:58:15.568753Z", "secondsAgo": 0}, - "on": False, - "mode": "fan", - "fanLevel": "low", - "light": "on", - }, - temp=None, - humidity=None, - target_temp=None, - hvac_mode="off", - device_on=False, - fan_mode="low", - swing_mode=None, - horizontal_swing_mode=None, - light_mode="on", - available=True, - hvac_modes=["fan", "off"], - fan_modes=["low", "high"], - swing_modes=None, - horizontal_swing_modes=None, - light_modes=["on", "dim", "off"], - temp_unit="C", - temp_list=[0, 1], - temp_step=1, - active_features=["timestamp", "on", "mode", "fanLevel", "light"], - full_features={"light", "targetTemperature", "fanLevel"}, - state="off", - fw_ver="PUR00111", - fw_ver_available="PUR00111", - fw_type="pure-esp32", - model="pure", - calibration_temp=0.0, - calibration_hum=0.0, - full_capabilities={ - "modes": { - "fan": { - "temperatures": {}, - "fanLevels": ["low", "high"], - "light": ["on", "dim", "off"], - } - } - }, - motion_sensors={}, - pm25=1, - room_occupied=None, - update_available=False, - schedules={}, - pure_boost_enabled=False, - pure_sensitivity="N", - pure_ac_integration=False, - pure_geo_integration=False, - pure_measure_integration=True, - timer_on=None, - timer_id=None, - timer_state_on=None, - timer_time=None, - smart_on=None, - smart_type=None, - smart_low_temp_threshold=None, - smart_high_temp_threshold=None, - smart_low_state=None, - smart_high_state=None, - filter_clean=False, - filter_last_reset="2022-04-23T15:58:45Z", - ), - }, -) diff --git a/tests/components/sensibo/test_entity.py b/tests/components/sensibo/test_entity.py index 27e0f4df772..bf512f9f220 100644 --- a/tests/components/sensibo/test_entity.py +++ b/tests/components/sensibo/test_entity.py @@ -1,8 +1,7 @@ """The test for the sensibo entity.""" from __future__ import annotations -from datetime import timedelta -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from pysensibo.model import SensiboData import pytest @@ -23,9 +22,6 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.util import dt - -from tests.common import async_fire_time_changed async def test_entity( @@ -98,26 +94,13 @@ async def test_entity_send_command( async def test_entity_send_command_calibration( - hass: HomeAssistant, load_int: ConfigEntry, get_data: SensiboData + hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, + load_int: ConfigEntry, + get_data: SensiboData, ) -> None: """Test the Sensibo send command for calibration.""" - registry = er.async_get(hass) - registry.async_update_entity( - "number.hallway_temperature_calibration", disabled_by=None - ) - await hass.async_block_till_done() - - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() - state = hass.states.get("number.hallway_temperature_calibration") assert state.state == "0.1" diff --git a/tests/components/sensibo/test_number.py b/tests/components/sensibo/test_number.py new file mode 100644 index 00000000000..ed60d6653e9 --- /dev/null +++ b/tests/components/sensibo/test_number.py @@ -0,0 +1,101 @@ +"""The test for the sensibo number platform.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from pysensibo.model import SensiboData +import pytest +from pytest import MonkeyPatch + +from homeassistant.components.number.const import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import dt + +from tests.common import async_fire_time_changed + + +async def test_number( + hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, + load_int: ConfigEntry, + monkeypatch: MonkeyPatch, + get_data: SensiboData, +) -> None: + """Test the Sensibo number.""" + + state1 = hass.states.get("number.hallway_temperature_calibration") + state2 = hass.states.get("number.hallway_humidity_calibration") + assert state1.state == "0.1" + assert state2.state == "0.0" + + monkeypatch.setattr(get_data.parsed["ABC999111"], "calibration_temp", 0.2) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("number.hallway_temperature_calibration") + assert state1.state == "0.2" + + +async def test_number_set_value( + hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, + load_int: ConfigEntry, + get_data: SensiboData, +) -> None: + """Test the Sensibo number service.""" + + state1 = hass.states.get("number.hallway_temperature_calibration") + assert state1.state == "0.1" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_calibration", + return_value={"status": "failure"}, + ): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_VALUE: "0.2"}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("number.hallway_temperature_calibration") + assert state2.state == "0.1" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_calibration", + return_value={"status": "success"}, + ): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_VALUE: "0.2"}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("number.hallway_temperature_calibration") + assert state2.state == "0.2" diff --git a/tests/components/sensibo/test_select.py b/tests/components/sensibo/test_select.py new file mode 100644 index 00000000000..ce361e224c9 --- /dev/null +++ b/tests/components/sensibo/test_select.py @@ -0,0 +1,163 @@ +"""The test for the sensibo select platform.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +from pysensibo.model import SensiboData +import pytest +from pytest import MonkeyPatch + +from homeassistant.components.select.const import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import dt + +from tests.common import async_fire_time_changed + + +async def test_select( + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: MonkeyPatch, + get_data: SensiboData, +) -> None: + """Test the Sensibo select.""" + + state1 = hass.states.get("select.hallway_horizontal_swing") + assert state1.state == "stopped" + + monkeypatch.setattr( + get_data.parsed["ABC999111"], "horizontal_swing_mode", "fixedLeft" + ) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("select.hallway_horizontal_swing") + assert state1.state == "fixedLeft" + + +async def test_select_set_option( + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: MonkeyPatch, + get_data: SensiboData, +) -> None: + """Test the Sensibo select service.""" + + monkeypatch.setattr( + get_data.parsed["ABC999111"], + "active_features", + [ + "timestamp", + "on", + "mode", + "targetTemperature", + "light", + ], + ) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("select.hallway_horizontal_swing") + assert state1.state == "stopped" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "failed"}}, + ): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_OPTION: "fixedLeft"}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("select.hallway_horizontal_swing") + assert state2.state == "stopped" + + monkeypatch.setattr( + get_data.parsed["ABC999111"], + "active_features", + [ + "timestamp", + "on", + "mode", + "targetTemperature", + "horizontalSwing", + "light", + ], + ) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Failed", "failureReason": "No connection"}}, + ): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_OPTION: "fixedLeft"}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("select.hallway_horizontal_swing") + assert state2.state == "stopped" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", + return_value={"result": {"status": "Success"}}, + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_OPTION: "fixedLeft"}, + blocking=True, + ) + await hass.async_block_till_done() + + state2 = hass.states.get("select.hallway_horizontal_swing") + assert state2.state == "fixedLeft" diff --git a/tests/components/sensibo/test_sensor.py b/tests/components/sensibo/test_sensor.py new file mode 100644 index 00000000000..413b62f6b9f --- /dev/null +++ b/tests/components/sensibo/test_sensor.py @@ -0,0 +1,50 @@ +"""The test for the sensibo select platform.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +from pysensibo.model import SensiboData +from pytest import MonkeyPatch + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.util import dt + +from tests.common import async_fire_time_changed + + +async def test_sensor( + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: MonkeyPatch, + get_data: SensiboData, +) -> None: + """Test the Sensibo sensor.""" + + state1 = hass.states.get("sensor.hallway_motion_sensor_battery_voltage") + state2 = hass.states.get("sensor.kitchen_pm2_5") + assert state1.state == "3000" + assert state2.state == "1" + assert state2.attributes == { + "state_class": "measurement", + "unit_of_measurement": "µg/m³", + "device_class": "pm25", + "icon": "mdi:air-filter", + "friendly_name": "Kitchen PM2.5", + } + + monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pm25", 2) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("sensor.kitchen_pm2_5") + assert state1.state == "2" diff --git a/tests/components/sensibo/test_update.py b/tests/components/sensibo/test_update.py new file mode 100644 index 00000000000..11b019d111e --- /dev/null +++ b/tests/components/sensibo/test_update.py @@ -0,0 +1,47 @@ +"""The test for the sensibo update platform.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +from pysensibo.model import SensiboData +from pytest import MonkeyPatch + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.util import dt + +from tests.common import async_fire_time_changed + + +async def test_select( + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: MonkeyPatch, + get_data: SensiboData, +) -> None: + """Test the Sensibo update.""" + + state1 = hass.states.get("update.hallway_update_available") + state2 = hass.states.get("update.kitchen_update_available") + assert state1.state == STATE_ON + assert state1.attributes["installed_version"] == "SKY30046" + assert state1.attributes["latest_version"] == "SKY30048" + assert state1.attributes["title"] == "skyv2" + assert state2.state == STATE_OFF + + monkeypatch.setattr(get_data.parsed["ABC999111"], "fw_ver", "SKY30048") + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("update.hallway_update_available") + assert state1.state == STATE_OFF From d84c6af55df12700aebfc18963e2b0640aa59728 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 14 May 2022 02:42:11 +0200 Subject: [PATCH 0495/3516] Add multiple departures to Trafikverket Ferry (#71484) --- .../trafikverket_ferry/coordinator.py | 18 +++++++----- .../components/trafikverket_ferry/sensor.py | 29 ++++++++++++++++--- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/trafikverket_ferry/coordinator.py b/homeassistant/components/trafikverket_ferry/coordinator.py index 96a1e58c6c9..7c2c64d49f0 100644 --- a/homeassistant/components/trafikverket_ferry/coordinator.py +++ b/homeassistant/components/trafikverket_ferry/coordinator.py @@ -77,8 +77,10 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator): when = current_time try: - routedata: FerryStop = await self._ferry_api.async_get_next_ferry_stop( - self._from, self._to, when + routedata: list[ + FerryStop + ] = await self._ferry_api.async_get_next_ferry_stops( + self._from, self._to, when, 3 ) except ValueError as error: raise UpdateFailed( @@ -86,11 +88,13 @@ class TVDataUpdateCoordinator(DataUpdateCoordinator): ) from error states = { - "departure_time": routedata.departure_time, - "departure_from": routedata.from_harbor_name, - "departure_to": routedata.to_harbor_name, - "departure_modified": routedata.modified_time, - "departure_information": routedata.other_information, + "departure_time": routedata[0].departure_time, + "departure_from": routedata[0].from_harbor_name, + "departure_to": routedata[0].to_harbor_name, + "departure_modified": routedata[0].modified_time, + "departure_information": routedata[0].other_information, + "departure_time_next": routedata[1].departure_time, + "departure_time_next_next": routedata[2].departure_time, } _LOGGER.debug("States: %s", states) return states diff --git a/homeassistant/components/trafikverket_ferry/sensor.py b/homeassistant/components/trafikverket_ferry/sensor.py index d93aecd38c1..bab73d72210 100644 --- a/homeassistant/components/trafikverket_ferry/sensor.py +++ b/homeassistant/components/trafikverket_ferry/sensor.py @@ -38,7 +38,7 @@ class TrafikverketRequiredKeysMixin: """Mixin for required keys.""" value_fn: Callable[[dict[str, Any]], StateType | datetime] - info_fn: Callable[[dict[str, Any]], StateType | list] + info_fn: Callable[[dict[str, Any]], StateType | list] | None @dataclass @@ -80,6 +80,24 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( info_fn=lambda data: data["departure_information"], entity_registry_enabled_default=False, ), + TrafikverketSensorEntityDescription( + key="departure_time_next", + name="Departure Time Next", + icon="mdi:clock", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda data: as_utc(data["departure_time_next"]), + info_fn=None, + entity_registry_enabled_default=False, + ), + TrafikverketSensorEntityDescription( + key="departure_time_next_next", + name="Departure Time Next After", + icon="mdi:clock", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda data: as_utc(data["departure_time_next_next"]), + info_fn=None, + entity_registry_enabled_default=False, + ), ) @@ -132,9 +150,12 @@ class FerrySensor(CoordinatorEntity[TVDataUpdateCoordinator], SensorEntity): self.coordinator.data ) - self._attr_extra_state_attributes = { - "other_information": self.entity_description.info_fn(self.coordinator.data), - } + if self.entity_description.info_fn: + self._attr_extra_state_attributes = { + "other_information": self.entity_description.info_fn( + self.coordinator.data + ), + } @callback def _handle_coordinator_update(self) -> None: From ef9d8944f1a45cdb9d10fe1c6f9a149620f80ca8 Mon Sep 17 00:00:00 2001 From: "Clifford W. Hansen" Date: Sat, 14 May 2022 05:09:09 +0200 Subject: [PATCH 0496/3516] Update sonarr sensor (#71576) Fixed issue when episodeFileCount/episodeCount might not exist --- homeassistant/components/sonarr/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sonarr/sensor.py b/homeassistant/components/sonarr/sensor.py index c9a946758c5..adc588f6951 100644 --- a/homeassistant/components/sonarr/sensor.py +++ b/homeassistant/components/sonarr/sensor.py @@ -237,7 +237,7 @@ class SonarrSensor(SonarrEntity, SensorEntity): stats = item.statistics attrs[ item.title - ] = f"{stats.episodeFileCount}/{stats.episodeCount} Episodes" + ] = f"{getattr(stats,'episodeFileCount', 0)}/{getattr(stats, 'episodeCount', 0)} Episodes" elif key == "upcoming" and self.data.get(key) is not None: for episode in self.data[key]: identifier = f"S{episode.seasonNumber:02d}E{episode.episodeNumber:02d}" From 991f0b40f2d43431d0c44656127cfe0bf07ef02b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 14 May 2022 05:55:02 +0200 Subject: [PATCH 0497/3516] Remove YAML configuration from Discord (#71696) --- homeassistant/components/discord/__init__.py | 20 +------- .../components/discord/config_flow.py | 18 +------- homeassistant/components/discord/notify.py | 8 +--- tests/components/discord/__init__.py | 6 +-- tests/components/discord/test_config_flow.py | 46 ------------------- 5 files changed, 6 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/discord/__init__.py b/homeassistant/components/discord/__init__.py index ec41ac9d073..ae06447f741 100644 --- a/homeassistant/components/discord/__init__.py +++ b/homeassistant/components/discord/__init__.py @@ -2,33 +2,17 @@ from aiohttp.client_exceptions import ClientConnectorError import nextcord -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_API_TOKEN, CONF_PLATFORM, Platform +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_TOKEN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import discovery -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN PLATFORMS = [Platform.NOTIFY] -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Discord component.""" - # Iterate all entries for notify to only get Discord - if Platform.NOTIFY in config: - for entry in config[Platform.NOTIFY]: - if entry[CONF_PLATFORM] == DOMAIN: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=entry - ) - ) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Discord from a config entry.""" nextcord.VoiceClient.warn_nacl = False diff --git a/homeassistant/components/discord/config_flow.py b/homeassistant/components/discord/config_flow.py index 8abd2a6be37..bce27feced3 100644 --- a/homeassistant/components/discord/config_flow.py +++ b/homeassistant/components/discord/config_flow.py @@ -8,7 +8,7 @@ import nextcord import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_API_TOKEN, CONF_NAME, CONF_TOKEN +from homeassistant.const import CONF_API_TOKEN, CONF_NAME from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN, URL_PLACEHOLDER @@ -69,7 +69,7 @@ class DiscordFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return self.async_create_entry( title=info.name, - data=user_input | {CONF_NAME: user_input.get(CONF_NAME, info.name)}, + data=user_input | {CONF_NAME: info.name}, ) user_input = user_input or {} @@ -80,20 +80,6 @@ class DiscordFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, import_config: dict[str, str]) -> FlowResult: - """Import a config entry from configuration.yaml.""" - _LOGGER.warning( - "Configuration of the Discord integration in YAML is deprecated and " - "will be removed in Home Assistant 2022.6; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - for entry in self._async_current_entries(): - if entry.data[CONF_API_TOKEN] == import_config[CONF_TOKEN]: - return self.async_abort(reason="already_configured") - import_config[CONF_API_TOKEN] = import_config.pop(CONF_TOKEN) - return await self.async_step_user(import_config) - async def _async_try_connect(token: str) -> tuple[str | None, nextcord.AppInfo | None]: """Try connecting to Discord.""" diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index 098857876a1..299919472cf 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -7,17 +7,14 @@ from typing import Any, cast import nextcord from nextcord.abc import Messageable -import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, - PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import CONF_API_TOKEN, CONF_TOKEN +from homeassistant.const import CONF_API_TOKEN from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -33,9 +30,6 @@ ATTR_EMBED_THUMBNAIL = "thumbnail" ATTR_EMBED_URL = "url" ATTR_IMAGES = "images" -# Deprecated in Home Assistant 2022.4 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_TOKEN): cv.string}) - async def async_get_service( hass: HomeAssistant, diff --git a/tests/components/discord/__init__.py b/tests/components/discord/__init__.py index ebc23360555..bf7c188b7b5 100644 --- a/tests/components/discord/__init__.py +++ b/tests/components/discord/__init__.py @@ -6,7 +6,7 @@ import nextcord from homeassistant.components.discord.const import DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_TOKEN, CONF_NAME, CONF_TOKEN +from homeassistant.const import CONF_API_TOKEN, CONF_NAME from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -21,10 +21,6 @@ CONF_DATA = { CONF_NAME: NAME, } -CONF_IMPORT_DATA_NO_NAME = {CONF_TOKEN: TOKEN} - -CONF_IMPORT_DATA = CONF_IMPORT_DATA_NO_NAME | {CONF_NAME: NAME} - def create_entry(hass: HomeAssistant) -> ConfigEntry: """Add config entry in Home Assistant.""" diff --git a/tests/components/discord/test_config_flow.py b/tests/components/discord/test_config_flow.py index 64588b052fe..59030187866 100644 --- a/tests/components/discord/test_config_flow.py +++ b/tests/components/discord/test_config_flow.py @@ -1,6 +1,5 @@ """Test Discord config flow.""" import nextcord -from pytest import LogCaptureFixture from homeassistant import config_entries, data_entry_flow from homeassistant.components.discord.const import DOMAIN @@ -9,8 +8,6 @@ from homeassistant.core import HomeAssistant from . import ( CONF_DATA, - CONF_IMPORT_DATA, - CONF_IMPORT_DATA_NO_NAME, CONF_INPUT, NAME, create_entry, @@ -121,49 +118,6 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: assert result["data"] == CONF_DATA -async def test_flow_import(hass: HomeAssistant, caplog: LogCaptureFixture) -> None: - """Test an import flow.""" - with mocked_discord_info(), patch_discord_login(): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=CONF_IMPORT_DATA.copy(), - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == NAME - assert result["data"] == CONF_DATA - assert "Discord integration in YAML" in caplog.text - - -async def test_flow_import_no_name(hass: HomeAssistant) -> None: - """Test import flow with no name in config.""" - with mocked_discord_info(), patch_discord_login(): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=CONF_IMPORT_DATA_NO_NAME, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == NAME - assert result["data"] == CONF_DATA - - -async def test_flow_import_already_configured(hass: HomeAssistant) -> None: - """Test an import flow already configured.""" - create_entry(hass) - with mocked_discord_info(), patch_discord_login(): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=CONF_IMPORT_DATA, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - async def test_flow_reauth(hass: HomeAssistant) -> None: """Test a reauth flow.""" entry = create_entry(hass) From c0ae31d86c841107930cf471fd60d65b5c163f16 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 14 May 2022 13:16:22 +0200 Subject: [PATCH 0498/3516] Code quality Filesize (#71768) --- homeassistant/components/filesize/__init__.py | 10 +++++-- .../components/filesize/config_flow.py | 27 ++++++++++--------- homeassistant/components/filesize/sensor.py | 15 +++++------ tests/components/filesize/__init__.py | 9 ++++++- tests/components/filesize/test_config_flow.py | 25 +++++++++-------- tests/components/filesize/test_init.py | 6 ++--- tests/components/filesize/test_sensor.py | 6 ++--- 7 files changed, 54 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/filesize/__init__.py b/homeassistant/components/filesize/__init__.py index b3292ed9c9f..61ef26f0a66 100644 --- a/homeassistant/components/filesize/__init__.py +++ b/homeassistant/components/filesize/__init__.py @@ -11,13 +11,19 @@ from homeassistant.exceptions import ConfigEntryNotReady from .const import PLATFORMS +def check_path(path: pathlib.Path) -> bool: + """Check path.""" + return path.exists() and path.is_file() + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" path = entry.data[CONF_FILE_PATH] - get_path = await hass.async_add_executor_job(pathlib.Path, path) + get_path = pathlib.Path(path) - if not get_path.exists() and not get_path.is_file(): + check_file = await hass.async_add_executor_job(check_path, get_path) + if not check_file: raise ConfigEntryNotReady(f"Can not access file {path}") if not hass.config.is_allowed_path(path): diff --git a/homeassistant/components/filesize/config_flow.py b/homeassistant/components/filesize/config_flow.py index ed2d4ab0940..3f58e636b0e 100644 --- a/homeassistant/components/filesize/config_flow.py +++ b/homeassistant/components/filesize/config_flow.py @@ -11,6 +11,7 @@ from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_FILE_PATH from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN @@ -19,19 +20,20 @@ DATA_SCHEMA = vol.Schema({vol.Required(CONF_FILE_PATH): str}) _LOGGER = logging.getLogger(__name__) -def validate_path(hass: HomeAssistant, path: str) -> pathlib.Path: +def validate_path(hass: HomeAssistant, path: str) -> str: """Validate path.""" - try: - get_path = pathlib.Path(path) - except OSError as error: - _LOGGER.error("Can not access file %s, error %s", path, error) - raise NotValidError from error + get_path = pathlib.Path(path) + if not get_path.exists() or not get_path.is_file(): + _LOGGER.error("Can not access file %s", path) + raise NotValidError if not hass.config.is_allowed_path(path): - _LOGGER.error("Filepath %s is not valid or allowed", path) + _LOGGER.error("Filepath %s is not allowed", path) raise NotAllowedError - return get_path + full_path = get_path.absolute() + + return str(full_path) class FilesizeConfigFlow(ConfigFlow, domain=DOMAIN): @@ -47,14 +49,13 @@ class FilesizeConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: try: - get_path = validate_path(self.hass, user_input[CONF_FILE_PATH]) + full_path = validate_path(self.hass, user_input[CONF_FILE_PATH]) except NotValidError: errors["base"] = "not_valid" except NotAllowedError: errors["base"] = "not_allowed" else: - fullpath = str(get_path.absolute()) - await self.async_set_unique_id(fullpath) + await self.async_set_unique_id(full_path) self._abort_if_unique_id_configured() name = str(user_input[CONF_FILE_PATH]).rsplit("/", maxsplit=1)[-1] @@ -68,9 +69,9 @@ class FilesizeConfigFlow(ConfigFlow, domain=DOMAIN): ) -class NotValidError(Exception): +class NotValidError(HomeAssistantError): """Path is not valid error.""" -class NotAllowedError(Exception): +class NotAllowedError(HomeAssistantError): """Path is not allowed error.""" diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 70896bcacad..6c52fcbdd5a 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -74,13 +74,10 @@ async def async_setup_entry( coordinator = FileSizeCoordinator(hass, fullpath) await coordinator.async_config_entry_first_refresh() - if get_path.exists() and get_path.is_file(): - async_add_entities( - [ - FilesizeEntity(description, fullpath, entry.entry_id, coordinator) - for description in SENSOR_TYPES - ] - ) + async_add_entities( + FilesizeEntity(description, fullpath, entry.entry_id, coordinator) + for description in SENSOR_TYPES + ) class FileSizeCoordinator(DataUpdateCoordinator): @@ -119,7 +116,7 @@ class FileSizeCoordinator(DataUpdateCoordinator): class FilesizeEntity(CoordinatorEntity[FileSizeCoordinator], SensorEntity): - """Encapsulates file size information.""" + """Filesize sensor.""" entity_description: SensorEntityDescription @@ -130,7 +127,7 @@ class FilesizeEntity(CoordinatorEntity[FileSizeCoordinator], SensorEntity): entry_id: str, coordinator: FileSizeCoordinator, ) -> None: - """Initialize the data object.""" + """Initialize the Filesize sensor.""" super().__init__(coordinator) base_name = path.split("/")[-1] self._attr_name = f"{base_name} {description.name}" diff --git a/tests/components/filesize/__init__.py b/tests/components/filesize/__init__.py index d7aa8dd2481..3e09a745387 100644 --- a/tests/components/filesize/__init__.py +++ b/tests/components/filesize/__init__.py @@ -1,6 +1,8 @@ """Tests for the filesize component.""" import os +from homeassistant.core import HomeAssistant + TEST_DIR = os.path.join(os.path.dirname(__file__)) TEST_FILE_NAME = "mock_file_test_filesize.txt" TEST_FILE_NAME2 = "mock_file_test_filesize2.txt" @@ -8,7 +10,12 @@ TEST_FILE = os.path.join(TEST_DIR, TEST_FILE_NAME) TEST_FILE2 = os.path.join(TEST_DIR, TEST_FILE_NAME2) -def create_file(path) -> None: +async def async_create_file(hass: HomeAssistant, path: str) -> None: """Create a test file.""" + await hass.async_add_executor_job(create_file, path) + + +def create_file(path: str) -> None: + """Create the test file.""" with open(path, "w", encoding="utf-8") as test_file: test_file.write("test") diff --git a/tests/components/filesize/test_config_flow.py b/tests/components/filesize/test_config_flow.py index f6873e64128..16f0ab38dc1 100644 --- a/tests/components/filesize/test_config_flow.py +++ b/tests/components/filesize/test_config_flow.py @@ -11,14 +11,14 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from . import TEST_DIR, TEST_FILE, TEST_FILE_NAME, create_file +from . import TEST_DIR, TEST_FILE, TEST_FILE_NAME, async_create_file from tests.common import MockConfigEntry async def test_full_user_flow(hass: HomeAssistant) -> None: """Test the full user configuration flow.""" - create_file(TEST_FILE) + await async_create_file(hass, TEST_FILE) hass.config.allowlist_external_dirs = {TEST_DIR} result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -43,6 +43,7 @@ async def test_unique_path( mock_config_entry: MockConfigEntry, ) -> None: """Test we abort if already setup.""" + await async_create_file(hass, TEST_FILE) hass.config.allowlist_external_dirs = {TEST_DIR} mock_config_entry.add_to_hass(hass) @@ -56,7 +57,7 @@ async def test_unique_path( async def test_flow_fails_on_validation(hass: HomeAssistant) -> None: """Test config flow errors.""" - create_file(TEST_FILE) + hass.config.allowlist_external_dirs = {} result = await hass.config_entries.flow.async_init( @@ -66,19 +67,17 @@ async def test_flow_fails_on_validation(hass: HomeAssistant) -> None: assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == SOURCE_USER - with patch( - "homeassistant.components.filesize.config_flow.pathlib.Path", - side_effect=OSError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_FILE_PATH: TEST_FILE, - }, - ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_FILE_PATH: TEST_FILE, + }, + ) assert result2["errors"] == {"base": "not_valid"} + await async_create_file(hass, TEST_FILE) + with patch("homeassistant.components.filesize.config_flow.pathlib.Path",), patch( "homeassistant.components.filesize.async_setup_entry", return_value=True, diff --git a/tests/components/filesize/test_init.py b/tests/components/filesize/test_init.py index 99285e56b6f..effab8f75d8 100644 --- a/tests/components/filesize/test_init.py +++ b/tests/components/filesize/test_init.py @@ -4,7 +4,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_FILE_PATH from homeassistant.core import HomeAssistant -from . import create_file +from . import async_create_file from tests.common import MockConfigEntry @@ -14,7 +14,7 @@ async def test_load_unload_config_entry( ) -> None: """Test the Filesize configuration entry loading/unloading.""" testfile = f"{tmpdir}/file.txt" - create_file(testfile) + await async_create_file(hass, testfile) hass.config.allowlist_external_dirs = {tmpdir} mock_config_entry.add_to_hass(hass) hass.config_entries.async_update_entry( @@ -54,7 +54,7 @@ async def test_not_valid_path_to_file( ) -> None: """Test that an invalid path is caught.""" testfile = f"{tmpdir}/file.txt" - create_file(testfile) + await async_create_file(hass, testfile) mock_config_entry.add_to_hass(hass) hass.config_entries.async_update_entry( mock_config_entry, unique_id=testfile, data={CONF_FILE_PATH: testfile} diff --git a/tests/components/filesize/test_sensor.py b/tests/components/filesize/test_sensor.py index 803aa96610d..5b072769f56 100644 --- a/tests/components/filesize/test_sensor.py +++ b/tests/components/filesize/test_sensor.py @@ -5,7 +5,7 @@ from homeassistant.const import CONF_FILE_PATH, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import async_update_entity -from . import TEST_FILE, TEST_FILE_NAME, create_file +from . import TEST_FILE, TEST_FILE_NAME, async_create_file from tests.common import MockConfigEntry @@ -28,7 +28,7 @@ async def test_valid_path( ) -> None: """Test for a valid path.""" testfile = f"{tmpdir}/file.txt" - create_file(testfile) + await async_create_file(hass, testfile) hass.config.allowlist_external_dirs = {tmpdir} mock_config_entry.add_to_hass(hass) hass.config_entries.async_update_entry( @@ -50,7 +50,7 @@ async def test_state_unavailable( ) -> None: """Verify we handle state unavailable.""" testfile = f"{tmpdir}/file.txt" - create_file(testfile) + await async_create_file(hass, testfile) hass.config.allowlist_external_dirs = {tmpdir} mock_config_entry.add_to_hass(hass) hass.config_entries.async_update_entry( From ba7d3977045fc50af4b357fb09f85ab3a6464e69 Mon Sep 17 00:00:00 2001 From: eyager1 <44526531+eyager1@users.noreply.github.com> Date: Sat, 14 May 2022 11:14:35 -0400 Subject: [PATCH 0499/3516] Improve reliability of VLC metadata parsing (#71856) * Improve reliability of metadata parsing. * Remove media_album_name property * Apply suggestions from code review Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/vlc_telnet/media_player.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 7a0a796f665..89fa1a3c323 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -162,8 +162,16 @@ class VlcDevice(MediaPlayerEntity): 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._attr_media_album_name = data.get("data", {}).get("album") + self._media_artist = data.get("data", {}).get("artist") + self._media_title = data.get("data", {}).get("title") + now_playing = data.get("data", {}).get("now_playing") + + # Many radio streams put artist/title/album in now_playing and title is the station name. + if now_playing: + if not self._media_artist: + self._media_artist = self._media_title + self._media_title = now_playing if self._media_title: return From fdc8830dd362e6d0b08b3ea1e139cf83f82d853c Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sat, 14 May 2022 12:07:17 -0400 Subject: [PATCH 0500/3516] Remove ssh switch from unsupported devices for UniFi Protect (#71859) --- .../components/unifiprotect/switch.py | 28 +++++++++++++++---- tests/components/unifiprotect/test_switch.py | 25 +++++++++-------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 85a089994f8..6051e4e596f 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -43,7 +43,7 @@ async def _set_highfps(obj: Camera, value: bool) -> None: await obj.set_video_mode(VideoMode.DEFAULT) -ALL_DEVICES_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( +CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ProtectSwitchEntityDescription( key="ssh", name="SSH Enabled", @@ -53,9 +53,6 @@ ALL_DEVICES_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_value="is_ssh_enabled", ufp_set_method="set_ssh", ), -) - -CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ProtectSwitchEntityDescription( key="status_light", name="Status Light On", @@ -222,6 +219,15 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( LIGHT_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( + ProtectSwitchEntityDescription( + key="ssh", + name="SSH Enabled", + icon="mdi:lock", + entity_registry_enabled_default=False, + entity_category=EntityCategory.CONFIG, + ufp_value="is_ssh_enabled", + ufp_set_method="set_ssh", + ), ProtectSwitchEntityDescription( key="status_light", name="Status Light On", @@ -243,6 +249,18 @@ DOORLOCK_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ) +VIEWER_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( + ProtectSwitchEntityDescription( + key="ssh", + name="SSH Enabled", + icon="mdi:lock", + entity_registry_enabled_default=False, + entity_category=EntityCategory.CONFIG, + ufp_value="is_ssh_enabled", + ufp_set_method="set_ssh", + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -254,11 +272,11 @@ async def async_setup_entry( entities: list[ProtectDeviceEntity] = async_all_device_entities( data, ProtectSwitch, - all_descs=ALL_DEVICES_SWITCHES, camera_descs=CAMERA_SWITCHES, light_descs=LIGHT_SWITCHES, sense_descs=SENSE_SWITCHES, lock_descs=DOORLOCK_SWITCHES, + viewer_descs=VIEWER_SWITCHES, ) async_add_entities(entities) diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index c54d04a8cb7..498f4c6b3b7 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -10,7 +10,6 @@ from pyunifiprotect.data.types import RecordingMode, SmartDetectObjectType, Vide from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.switch import ( - ALL_DEVICES_SWITCHES, CAMERA_SWITCHES, LIGHT_SWITCHES, ProtectSwitchEntityDescription, @@ -29,7 +28,9 @@ from .conftest import ( CAMERA_SWITCHES_BASIC = [ d for d in CAMERA_SWITCHES - if d.name != "Detections: Face" and d.name != "Detections: Package" + if d.name != "Detections: Face" + and d.name != "Detections: Package" + and d.name != "SSH Enabled" ] CAMERA_SWITCHES_NO_EXTRA = [ d for d in CAMERA_SWITCHES_BASIC if d.name not in ("High FPS", "Privacy Mode") @@ -215,7 +216,7 @@ async def test_switch_setup_light( entity_registry = er.async_get(hass) - description = LIGHT_SWITCHES[0] + description = LIGHT_SWITCHES[1] unique_id, entity_id = ids_from_device_description( Platform.SWITCH, light, description @@ -230,7 +231,7 @@ async def test_switch_setup_light( assert state.state == STATE_OFF assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION - description = ALL_DEVICES_SWITCHES[0] + description = LIGHT_SWITCHES[0] unique_id = f"{light.id}_{description.key}" entity_id = f"switch.test_light_{description.name.lower().replace(' ', '_')}" @@ -271,7 +272,7 @@ async def test_switch_setup_camera_all( assert state.state == STATE_OFF assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION - description = ALL_DEVICES_SWITCHES[0] + description = CAMERA_SWITCHES[0] description_entity_name = ( description.name.lower().replace(":", "").replace(" ", "_") @@ -301,7 +302,7 @@ async def test_switch_setup_camera_none( entity_registry = er.async_get(hass) - for description in CAMERA_SWITCHES: + for description in CAMERA_SWITCHES_BASIC: if description.ufp_required_field is not None: continue @@ -318,7 +319,7 @@ async def test_switch_setup_camera_none( assert state.state == STATE_OFF assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION - description = ALL_DEVICES_SWITCHES[0] + description = CAMERA_SWITCHES[0] description_entity_name = ( description.name.lower().replace(":", "").replace(" ", "_") @@ -342,7 +343,7 @@ async def test_switch_setup_camera_none( async def test_switch_light_status(hass: HomeAssistant, light: Light): """Tests status light switch for lights.""" - description = LIGHT_SWITCHES[0] + description = LIGHT_SWITCHES[1] light.__fields__["set_status_light"] = Mock() light.set_status_light = AsyncMock() @@ -367,7 +368,7 @@ async def test_switch_camera_ssh( ): """Tests SSH switch for cameras.""" - description = ALL_DEVICES_SWITCHES[0] + description = CAMERA_SWITCHES[0] camera.__fields__["set_ssh"] = Mock() camera.set_ssh = AsyncMock() @@ -418,7 +419,7 @@ async def test_switch_camera_simple( async def test_switch_camera_highfps(hass: HomeAssistant, camera: Camera): """Tests High FPS switch for cameras.""" - description = CAMERA_SWITCHES[2] + description = CAMERA_SWITCHES[3] camera.__fields__["set_video_mode"] = Mock() camera.set_video_mode = AsyncMock() @@ -441,7 +442,7 @@ async def test_switch_camera_highfps(hass: HomeAssistant, camera: Camera): async def test_switch_camera_privacy(hass: HomeAssistant, camera: Camera): """Tests Privacy Mode switch for cameras.""" - description = CAMERA_SWITCHES[3] + description = CAMERA_SWITCHES[4] camera.__fields__["set_privacy"] = Mock() camera.set_privacy = AsyncMock() @@ -468,7 +469,7 @@ async def test_switch_camera_privacy_already_on( ): """Tests Privacy Mode switch for cameras with privacy mode defaulted on.""" - description = CAMERA_SWITCHES[3] + description = CAMERA_SWITCHES[4] camera_privacy.__fields__["set_privacy"] = Mock() camera_privacy.set_privacy = AsyncMock() From 656e88faecea8e9d2ae389a67c99a4235d51c725 Mon Sep 17 00:00:00 2001 From: AlainH Date: Sat, 14 May 2022 18:10:15 +0200 Subject: [PATCH 0501/3516] Update pyRFXtrx dependency to 0.29.0 (#71852) * rfxtrx: update pyRFXtrx dependency to 0.29.0 * Update requirements_all.txt / requirements_test_all.txt --- homeassistant/components/rfxtrx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index edbf5c8556c..cfe1049c888 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -2,7 +2,7 @@ "domain": "rfxtrx", "name": "RFXCOM RFXtrx", "documentation": "https://www.home-assistant.io/integrations/rfxtrx", - "requirements": ["pyRFXtrx==0.28.0"], + "requirements": ["pyRFXtrx==0.29.0"], "codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 2e8c89e5797..fe73d53bee5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1324,7 +1324,7 @@ pyMetEireann==2021.8.0 pyMetno==0.9.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.28.0 +pyRFXtrx==0.29.0 # homeassistant.components.switchmate # pySwitchmate==0.4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30510f08910..5bf3a4aa4eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -899,7 +899,7 @@ pyMetEireann==2021.8.0 pyMetno==0.9.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.28.0 +pyRFXtrx==0.29.0 # homeassistant.components.tibber pyTibber==0.22.3 From 355445db2d5c94dff91f3ea9799ec0ebe2581e4f Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 14 May 2022 10:27:47 -0700 Subject: [PATCH 0502/3516] Add application credentials platform for google calendar integration (#71808) * Add google application_credentials platform * Further simplify custom auth implementation overrides * Add test coverage in application_credentials * Simplify wording in a comment * Remove unused imports accidentally left from merge * Wrap lines that are too long for style guide * Move application credential loading to only where it is needed * Leave CLIENT_ID and CLIENT_SECRET as required. --- .../application_credentials/__init__.py | 62 +++++++++---- homeassistant/components/google/__init__.py | 25 ++++-- homeassistant/components/google/api.py | 16 +--- .../google/application_credentials.py | 23 +++++ homeassistant/components/google/manifest.json | 2 +- .../generated/application_credentials.py | 1 + .../application_credentials/test_init.py | 49 ++++++++++- tests/components/google/conftest.py | 5 +- tests/components/google/test_config_flow.py | 88 ++++++++++++++++++- tests/components/google/test_init.py | 34 +++++++ 10 files changed, 258 insertions(+), 47 deletions(-) create mode 100644 homeassistant/components/google/application_credentials.py diff --git a/homeassistant/components/application_credentials/__init__.py b/homeassistant/components/application_credentials/__init__.py index b5c828762c1..0dda775774f 100644 --- a/homeassistant/components/application_credentials/__init__.py +++ b/homeassistant/components/application_credentials/__init__.py @@ -151,7 +151,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_import_client_credential( - hass: HomeAssistant, domain: str, credential: ClientCredential + hass: HomeAssistant, + domain: str, + credential: ClientCredential, + auth_domain: str = None, ) -> None: """Import an existing credential from configuration.yaml.""" if DOMAIN not in hass.data: @@ -161,7 +164,7 @@ async def async_import_client_credential( CONF_DOMAIN: domain, CONF_CLIENT_ID: credential.client_id, CONF_CLIENT_SECRET: credential.client_secret, - CONF_AUTH_DOMAIN: domain, + CONF_AUTH_DOMAIN: auth_domain if auth_domain else domain, } await storage_collection.async_import_item(item) @@ -169,6 +172,23 @@ async def async_import_client_credential( class AuthImplementation(config_entry_oauth2_flow.LocalOAuth2Implementation): """Application Credentials local oauth2 implementation.""" + def __init__( + self, + hass: HomeAssistant, + auth_domain: str, + credential: ClientCredential, + authorization_server: AuthorizationServer, + ) -> None: + """Initialize AuthImplementation.""" + super().__init__( + hass, + auth_domain, + credential.client_id, + credential.client_secret, + authorization_server.authorize_url, + authorization_server.token_url, + ) + @property def name(self) -> str: """Name of the implementation.""" @@ -184,29 +204,38 @@ async def _async_provide_implementation( if not platform: return [] - authorization_server = await platform.async_get_authorization_server(hass) storage_collection = hass.data[DOMAIN][DATA_STORAGE] credentials = storage_collection.async_client_credentials(domain) + if hasattr(platform, "async_get_auth_implementation"): + return [ + await platform.async_get_auth_implementation(hass, auth_domain, credential) + for auth_domain, credential in credentials.items() + ] + authorization_server = await platform.async_get_authorization_server(hass) return [ - AuthImplementation( - hass, - auth_domain, - credential.client_id, - credential.client_secret, - authorization_server.authorize_url, - authorization_server.token_url, - ) + AuthImplementation(hass, auth_domain, credential, authorization_server) for auth_domain, credential in credentials.items() ] class ApplicationCredentialsProtocol(Protocol): - """Define the format that application_credentials platforms can have.""" + """Define the format that application_credentials platforms may have. + + Most platforms typically just implement async_get_authorization_server, and + the default oauth implementation will be used. Otherwise a platform may + implement async_get_auth_implementation to give their use a custom + AbstractOAuth2Implementation. + """ async def async_get_authorization_server( self, hass: HomeAssistant ) -> AuthorizationServer: - """Return authorization server.""" + """Return authorization server, for the default auth implementation.""" + + async def async_get_auth_implementation( + self, hass: HomeAssistant, auth_domain: str, credential: ClientCredential + ) -> config_entry_oauth2_flow.AbstractOAuth2Implementation: + """Return a custom auth implementation.""" async def _get_platform( @@ -227,9 +256,12 @@ async def _get_platform( err, ) return None - if not hasattr(platform, "async_get_authorization_server"): + if not hasattr(platform, "async_get_authorization_server") and not hasattr( + platform, "async_get_auth_implementation" + ): raise ValueError( - f"Integration '{integration_domain}' platform application_credentials did not implement 'async_get_authorization_server'" + f"Integration '{integration_domain}' platform {DOMAIN} did not " + f"implement 'async_get_authorization_server' or 'async_get_auth_implementation'" ) return platform diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index c5f62c0034f..61b25a9c027 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -17,6 +17,10 @@ from voluptuous.error import Error as VoluptuousError import yaml from homeassistant import config_entries +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, @@ -39,12 +43,12 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.typing import ConfigType -from . import config_flow -from .api import ApiAuthImpl, DeviceAuth, get_feature_access +from .api import ApiAuthImpl, get_feature_access from .const import ( CONF_CALENDAR_ACCESS, DATA_CONFIG, DATA_SERVICE, + DEVICE_AUTH_IMPL, DISCOVER_CALENDAR, DOMAIN, FeatureAccess, @@ -159,14 +163,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Google component.""" conf = config.get(DOMAIN, {}) hass.data[DOMAIN] = {DATA_CONFIG: conf} - config_flow.OAuth2FlowHandler.async_register_implementation( - hass, - DeviceAuth( + + if CONF_CLIENT_ID in conf and CONF_CLIENT_SECRET in conf: + await async_import_client_credential( hass, - conf[CONF_CLIENT_ID], - conf[CONF_CLIENT_SECRET], - ), - ) + DOMAIN, + ClientCredential( + conf[CONF_CLIENT_ID], + conf[CONF_CLIENT_SECRET], + ), + DEVICE_AUTH_IMPL, + ) # Import credentials from the old token file into the new way as # a ConfigEntry managed by home assistant. diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index bb32d46f0e4..dceeb6ba6a4 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -10,7 +10,6 @@ from typing import Any import aiohttp from gcal_sync.auth import AbstractAuth -import oauth2client from oauth2client.client import ( Credentials, DeviceFlowInfo, @@ -19,6 +18,7 @@ from oauth2client.client import ( OAuth2WebServerFlow, ) +from homeassistant.components.application_credentials import AuthImplementation from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.event import async_track_time_interval @@ -28,7 +28,6 @@ from .const import ( CONF_CALENDAR_ACCESS, DATA_CONFIG, DEFAULT_FEATURE_ACCESS, - DEVICE_AUTH_IMPL, DOMAIN, FeatureAccess, ) @@ -44,20 +43,9 @@ class OAuthError(Exception): """OAuth related error.""" -class DeviceAuth(config_entry_oauth2_flow.LocalOAuth2Implementation): +class DeviceAuth(AuthImplementation): """OAuth implementation for Device Auth.""" - def __init__(self, hass: HomeAssistant, client_id: str, client_secret: str) -> None: - """Initialize InstalledAppAuth.""" - super().__init__( - hass, - DEVICE_AUTH_IMPL, - client_id, - client_secret, - oauth2client.GOOGLE_AUTH_URI, - oauth2client.GOOGLE_TOKEN_URI, - ) - async def async_resolve_external_data(self, external_data: Any) -> dict: """Resolve a Google API Credentials object to Home Assistant token.""" creds: Credentials = external_data[DEVICE_AUTH_CREDS] diff --git a/homeassistant/components/google/application_credentials.py b/homeassistant/components/google/application_credentials.py new file mode 100644 index 00000000000..2f1fcba8084 --- /dev/null +++ b/homeassistant/components/google/application_credentials.py @@ -0,0 +1,23 @@ +"""application_credentials platform for nest.""" + +import oauth2client + +from homeassistant.components.application_credentials import ( + AuthorizationServer, + ClientCredential, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow + +from .api import DeviceAuth + +AUTHORIZATION_SERVER = AuthorizationServer( + oauth2client.GOOGLE_AUTH_URI, oauth2client.GOOGLE_TOKEN_URI +) + + +async def async_get_auth_implementation( + hass: HomeAssistant, auth_domain: str, credential: ClientCredential +) -> config_entry_oauth2_flow.AbstractOAuth2Implementation: + """Return auth implementation.""" + return DeviceAuth(hass, auth_domain, credential, AUTHORIZATION_SERVER) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 2cf852fc6af..87069bc463a 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -2,7 +2,7 @@ "domain": "google", "name": "Google Calendars", "config_flow": true, - "dependencies": ["auth"], + "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", "requirements": ["gcal-sync==0.7.1", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index ec6c1886e0a..3bde9e4681f 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -6,5 +6,6 @@ To update, run python3 -m script.hassfest # fmt: off APPLICATION_CREDENTIALS = [ + "google", "xbox" ] diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py index 8929d8f9c54..b5f51a9b837 100644 --- a/tests/components/application_credentials/test_init.py +++ b/tests/components/application_credentials/test_init.py @@ -14,6 +14,7 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components.application_credentials import ( CONF_AUTH_DOMAIN, DOMAIN, + AuthImplementation, AuthorizationServer, ClientCredential, async_import_client_credential, @@ -64,12 +65,14 @@ async def setup_application_credentials_integration( ) -> None: """Set up a fake application_credentials integration.""" hass.config.components.add(domain) + mock_platform_impl = Mock( + async_get_authorization_server=AsyncMock(return_value=authorization_server), + ) + del mock_platform_impl.async_get_auth_implementation # return False on hasattr mock_platform( hass, f"{domain}.application_credentials", - Mock( - async_get_authorization_server=AsyncMock(return_value=authorization_server), - ), + mock_platform_impl, ) @@ -585,6 +588,7 @@ async def test_websocket_without_authorization_server( # Platform does not implemenent async_get_authorization_server platform = Mock() del platform.async_get_authorization_server + del platform.async_get_auth_implementation mock_platform( hass, f"{TEST_DOMAIN}.application_credentials", @@ -611,6 +615,45 @@ async def test_websocket_without_authorization_server( ) +@pytest.mark.parametrize("config_credential", [DEVELOPER_CREDENTIAL]) +async def test_platform_with_auth_implementation( + hass, + hass_client_no_auth, + aioclient_mock, + oauth_fixture, + config_credential, + import_config_credential, + authorization_server, +): + """Test config flow with custom OAuth2 implementation.""" + + assert await async_setup_component(hass, "application_credentials", {}) + hass.config.components.add(TEST_DOMAIN) + + async def get_auth_impl( + hass: HomeAssistant, auth_domain: str, credential: ClientCredential + ) -> config_entry_oauth2_flow.AbstractOAuth2Implementation: + return AuthImplementation(hass, auth_domain, credential, authorization_server) + + mock_platform_impl = Mock( + async_get_auth_implementation=get_auth_impl, + ) + del mock_platform_impl.async_get_authorization_server + mock_platform( + hass, + f"{TEST_DOMAIN}.application_credentials", + mock_platform_impl, + ) + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + result = await oauth_fixture.complete_external_step(result) + # Uses the imported auth domain for compatibility + assert result["data"].get("auth_implementation") == TEST_DOMAIN + + async def test_websocket_integration_list(ws_client: ClientFixture): """Test websocket integration list command.""" client = await ws_client() diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 9594963008f..f48996de72a 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -171,7 +171,8 @@ def config_entry_token_expiry(token_expiry: datetime.datetime) -> float: @pytest.fixture def config_entry( - token_scopes: list[str], config_entry_token_expiry: float + token_scopes: list[str], + config_entry_token_expiry: float, ) -> MockConfigEntry: """Fixture to create a config entry for the integration.""" return MockConfigEntry( @@ -291,7 +292,7 @@ def google_config(google_config_track_new: bool | None) -> dict[str, Any]: @pytest.fixture def config(google_config: dict[str, Any]) -> dict[str, Any]: """Fixture for overriding component config.""" - return {DOMAIN: google_config} + return {DOMAIN: google_config} if google_config else {} @pytest.fixture diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index 0991f4e5194..f061aeb3057 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -1,6 +1,7 @@ """Test the google config flow.""" import datetime +from typing import Any from unittest.mock import Mock, patch from oauth2client.client import ( @@ -11,6 +12,10 @@ from oauth2client.client import ( import pytest from homeassistant import config_entries +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.components.google.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow @@ -65,7 +70,7 @@ async def fire_alarm(hass, point_in_time): await hass.async_block_till_done() -async def test_full_flow( +async def test_full_flow_yaml_creds( hass: HomeAssistant, mock_code_flow: Mock, mock_exchange: Mock, @@ -94,7 +99,7 @@ async def test_full_flow( ) assert result.get("type") == "create_entry" - assert result.get("title") == "Configuration.yaml" + assert result.get("title") == "client-id" assert "data" in result data = result["data"] assert "token" in data @@ -121,6 +126,68 @@ async def test_full_flow( assert len(entries) == 1 +@pytest.mark.parametrize("google_config", [None]) +async def test_full_flow_application_creds( + hass: HomeAssistant, + mock_code_flow: Mock, + mock_exchange: Mock, + config: dict[str, Any], + component_setup: ComponentSetup, +) -> None: + """Test successful creds setup.""" + assert await component_setup() + + await async_import_client_credential( + hass, DOMAIN, ClientCredential("client-id", "client-secret"), "imported-cred" + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "progress" + assert result.get("step_id") == "auth" + assert "description_placeholders" in result + assert "url" in result["description_placeholders"] + + with patch( + "homeassistant.components.google.async_setup_entry", return_value=True + ) as mock_setup: + # Run one tick to invoke the credential exchange check + now = utcnow() + await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"] + ) + + assert result.get("type") == "create_entry" + assert result.get("title") == "client-id" + assert "data" in result + data = result["data"] + assert "token" in data + assert 0 < data["token"]["expires_in"] < 8 * 86400 + assert ( + datetime.datetime.now().timestamp() + <= data["token"]["expires_at"] + < (datetime.datetime.now() + datetime.timedelta(days=8)).timestamp() + ) + data["token"].pop("expires_at") + data["token"].pop("expires_in") + assert data == { + "auth_implementation": "imported-cred", + "token": { + "access_token": "ACCESS_TOKEN", + "refresh_token": "REFRESH_TOKEN", + "scope": "https://www.googleapis.com/auth/calendar", + "token_type": "Bearer", + }, + } + + assert len(mock_setup.mock_calls) == 1 + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + + async def test_code_error( hass: HomeAssistant, mock_code_flow: Mock, @@ -211,7 +278,7 @@ async def test_exchange_error( ) assert result.get("type") == "create_entry" - assert result.get("title") == "Configuration.yaml" + assert result.get("title") == "client-id" assert "data" in result data = result["data"] assert "token" in data @@ -263,6 +330,21 @@ async def test_missing_configuration( assert result.get("reason") == "missing_configuration" +@pytest.mark.parametrize("google_config", [None]) +async def test_missing_configuration_yaml_empty( + hass: HomeAssistant, + component_setup: ComponentSetup, +) -> None: + """Test setup with an empty yaml configuration and no credentials.""" + assert await component_setup() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "abort" + assert result.get("reason") == "missing_configuration" + + async def test_wrong_configuration( hass: HomeAssistant, ) -> None: diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index c536ef7ea00..511e8545b40 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -10,6 +10,10 @@ from unittest.mock import patch import pytest +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.components.google import ( DOMAIN, SERVICE_ADD_EVENT, @@ -18,6 +22,7 @@ from homeassistant.components.google import ( from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant, State +from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from .conftest import ( @@ -224,6 +229,35 @@ async def test_found_calendar_from_api( assert not hass.states.get(TEST_YAML_ENTITY) +@pytest.mark.parametrize("calendars_config,google_config", [([], {})]) +async def test_load_application_credentials( + hass: HomeAssistant, + component_setup: ComponentSetup, + mock_calendars_yaml: None, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + mock_events_list: ApiResult, + setup_config_entry: MockConfigEntry, +) -> None: + """Test loading an application credentials and a config entry.""" + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, DOMAIN, ClientCredential("client-id", "client-secret"), "device_auth" + ) + + mock_calendars_list({"items": [test_api_calendar]}) + mock_events_list({}) + assert await component_setup() + + state = hass.states.get(TEST_API_ENTITY) + assert state + assert state.name == TEST_API_ENTITY_NAME + assert state.state == STATE_OFF + + # No yaml config loaded that overwrites the entity name + assert not hass.states.get(TEST_YAML_ENTITY) + + @pytest.mark.parametrize( "calendars_config_track,expected_state", [ From 532b3d780f58df7178cb3278513c451f7450ada1 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 14 May 2022 13:40:26 -0500 Subject: [PATCH 0503/3516] Rework Sonos battery and ping activity tracking (#70942) --- homeassistant/components/sonos/__init__.py | 13 +--- homeassistant/components/sonos/exception.py | 4 ++ homeassistant/components/sonos/speaker.py | 73 ++++++++++++--------- 3 files changed, 48 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 114c2815a56..c775b475dc4 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -40,6 +40,7 @@ from .const import ( SONOS_VANISHED, UPNP_ST, ) +from .exception import SonosUpdateError from .favorites import SonosFavorites from .speaker import SonosSpeaker @@ -264,19 +265,11 @@ class SonosDiscoveryManager: self._create_visible_speakers(ip_addr) elif not known_speaker.available: try: - known_speaker.soco.renderingControl.GetVolume( - [("InstanceID", 0), ("Channel", "Master")], timeout=1 - ) - except OSError: + known_speaker.ping() + except SonosUpdateError: _LOGGER.debug( "Manual poll to %s failed, keeping unavailable", ip_addr ) - else: - dispatcher_send( - self.hass, - f"{SONOS_SPEAKER_ACTIVITY}-{known_speaker.uid}", - "manual rediscovery", - ) self.data.hosts_heartbeat = call_later( self.hass, DISCOVERY_INTERVAL.total_seconds(), self._poll_manual_hosts diff --git a/homeassistant/components/sonos/exception.py b/homeassistant/components/sonos/exception.py index bce1e3233c1..dd2d30796cc 100644 --- a/homeassistant/components/sonos/exception.py +++ b/homeassistant/components/sonos/exception.py @@ -9,3 +9,7 @@ class UnknownMediaType(BrowseError): class SonosUpdateError(HomeAssistantError): """Update failed.""" + + +class S1BatteryMissing(SonosUpdateError): + """Battery update failed on S1 firmware.""" diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index a57bb4d3206..5d4199ec905 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -57,6 +57,7 @@ from .const import ( SONOS_VANISHED, SUBSCRIPTION_TIMEOUT, ) +from .exception import S1BatteryMissing, SonosUpdateError from .favorites import SonosFavorites from .helpers import soco_error from .media import SonosMedia @@ -83,16 +84,6 @@ UNUSED_DEVICE_KEYS = ["SPID", "TargetRoomName"] _LOGGER = logging.getLogger(__name__) -def fetch_battery_info_or_none(soco: SoCo) -> dict[str, Any] | None: - """Fetch battery_info from the given SoCo object. - - Returns None if the device doesn't support battery info - or if the device is offline. - """ - with contextlib.suppress(ConnectionError, TimeoutError, SoCoException): - return soco.get_battery_info() - - class SonosSpeaker: """Representation of a Sonos speaker.""" @@ -207,8 +198,11 @@ class SonosSpeaker: self.hass, SONOS_CREATE_AUDIO_FORMAT_SENSOR, self, audio_format ) - if battery_info := fetch_battery_info_or_none(self.soco): - self.battery_info = battery_info + try: + self.battery_info = self.fetch_battery_info() + except SonosUpdateError: + _LOGGER.debug("No battery available for %s", self.zone_name) + else: # Battery events can be infrequent, polling is still necessary self._battery_poll_timer = track_time_interval( self.hass, self.async_poll_battery, BATTERY_SCAN_INTERVAL @@ -530,6 +524,13 @@ class SonosSpeaker: # # Speaker availability methods # + @soco_error() + def ping(self) -> None: + """Test device availability. Failure will raise SonosUpdateError.""" + self.soco.renderingControl.GetVolume( + [("InstanceID", 0), ("Channel", "Master")], timeout=1 + ) + @callback def speaker_activity(self, source): """Track the last activity on this speaker, set availability and resubscribe.""" @@ -560,23 +561,13 @@ class SonosSpeaker: return try: - # Make a short-timeout call as a final check - # before marking this speaker as unavailable - await self.hass.async_add_executor_job( - partial( - self.soco.renderingControl.GetVolume, - [("InstanceID", 0), ("Channel", "Master")], - timeout=1, - ) - ) - except OSError: + await self.hass.async_add_executor_job(self.ping) + except SonosUpdateError: _LOGGER.warning( "No recent activity and cannot reach %s, marking unavailable", self.zone_name, ) await self.async_offline() - else: - self.speaker_activity("timeout poll") async def async_offline(self) -> None: """Handle removal of speaker when unavailable.""" @@ -619,6 +610,15 @@ class SonosSpeaker: # # Battery management # + @soco_error() + def fetch_battery_info(self) -> dict[str, Any]: + """Fetch battery_info for the speaker.""" + battery_info = self.soco.get_battery_info() + if not battery_info: + # S1 firmware returns an empty payload + raise S1BatteryMissing + return battery_info + async def async_update_battery_info(self, more_info: str) -> None: """Update battery info using a SonosEvent payload value.""" battery_dict = dict(x.split(":") for x in more_info.split(",")) @@ -658,11 +658,17 @@ class SonosSpeaker: if is_charging == self.charging: self.battery_info.update({"Level": int(battery_dict["BattPct"])}) + elif not is_charging: + # Avoid polling the speaker if possible + self.battery_info["PowerSource"] = "BATTERY" else: - if battery_info := await self.hass.async_add_executor_job( - fetch_battery_info_or_none, self.soco - ): - self.battery_info = battery_info + # Poll to obtain current power source not provided by event + try: + self.battery_info = await self.hass.async_add_executor_job( + self.fetch_battery_info + ) + except SonosUpdateError as err: + _LOGGER.debug("Could not request current power source: %s", err) @property def power_source(self) -> str | None: @@ -692,10 +698,13 @@ class SonosSpeaker: ): return - if battery_info := await self.hass.async_add_executor_job( - fetch_battery_info_or_none, self.soco - ): - self.battery_info = battery_info + try: + self.battery_info = await self.hass.async_add_executor_job( + self.fetch_battery_info + ) + except SonosUpdateError as err: + _LOGGER.debug("Could not poll battery info: %s", err) + else: self.async_write_entity_states() # From 8c2743bb67dca45796c596814e7978905477cd38 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 May 2022 15:06:31 -0400 Subject: [PATCH 0504/3516] Avoid storing last_changed in the database if its the same as last_updated (#71843) --- homeassistant/components/logbook/queries.py | 10 ++- homeassistant/components/recorder/history.py | 78 +++++++++++------ homeassistant/components/recorder/models.py | 74 +++++++++------- tests/components/recorder/test_history.py | 92 +++++++++++++++++++- tests/components/recorder/test_models.py | 2 +- 5 files changed, 189 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/logbook/queries.py b/homeassistant/components/logbook/queries.py index 2456c73fe22..ba1138c2c26 100644 --- a/homeassistant/components/logbook/queries.py +++ b/homeassistant/components/logbook/queries.py @@ -270,7 +270,9 @@ def _legacy_select_events_context_id( NOT_CONTEXT_ONLY, ) .outerjoin(States, (Events.event_id == States.event_id)) - .where(States.last_updated == States.last_changed) + .where( + (States.last_updated == States.last_changed) | States.last_changed.is_(None) + ) .where(_not_continuous_entity_matcher()) .outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) @@ -302,7 +304,7 @@ def _select_states(start_day: dt, end_day: dt) -> Select: "event_type" ), literal(value=None, type_=sqlalchemy.Text).label("event_data"), - States.last_changed.label("time_fired"), + States.last_updated.label("time_fired"), States.context_id.label("context_id"), States.context_user_id.label("context_user_id"), States.context_parent_id.label("context_parent_id"), @@ -314,7 +316,9 @@ def _select_states(start_day: dt, end_day: dt) -> Select: .outerjoin(old_state, (States.old_state_id == old_state.state_id)) .where(_missing_state_matcher(old_state)) .where(_not_continuous_entity_matcher()) - .where(States.last_updated == States.last_changed) + .where( + (States.last_updated == States.last_changed) | States.last_changed.is_(None) + ) .outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 179d25b9f5b..f434c1d5fe2 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -68,19 +68,19 @@ BASE_STATES = [ States.last_changed, States.last_updated, ] -BASE_STATES_NO_LAST_UPDATED = [ +BASE_STATES_NO_LAST_CHANGED = [ States.entity_id, States.state, - States.last_changed, - literal(value=None, type_=Text).label("last_updated"), + literal(value=None, type_=Text).label("last_changed"), + States.last_updated, ] QUERY_STATE_NO_ATTR = [ *BASE_STATES, literal(value=None, type_=Text).label("attributes"), literal(value=None, type_=Text).label("shared_attrs"), ] -QUERY_STATE_NO_ATTR_NO_LAST_UPDATED = [ - *BASE_STATES_NO_LAST_UPDATED, +QUERY_STATE_NO_ATTR_NO_LAST_CHANGED = [ + *BASE_STATES_NO_LAST_CHANGED, literal(value=None, type_=Text).label("attributes"), literal(value=None, type_=Text).label("shared_attrs"), ] @@ -92,8 +92,8 @@ QUERY_STATES_PRE_SCHEMA_25 = [ States.attributes, literal(value=None, type_=Text).label("shared_attrs"), ] -QUERY_STATES_PRE_SCHEMA_25_NO_LAST_UPDATED = [ - *BASE_STATES_NO_LAST_UPDATED, +QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED = [ + *BASE_STATES_NO_LAST_CHANGED, States.attributes, literal(value=None, type_=Text).label("shared_attrs"), ] @@ -103,8 +103,8 @@ QUERY_STATES = [ States.attributes, StateAttributes.shared_attrs, ] -QUERY_STATES_NO_LAST_UPDATED = [ - *BASE_STATES_NO_LAST_UPDATED, +QUERY_STATES_NO_LAST_CHANGED = [ + *BASE_STATES_NO_LAST_CHANGED, # Remove States.attributes once all attributes are in StateAttributes.shared_attrs States.attributes, StateAttributes.shared_attrs, @@ -114,7 +114,7 @@ HISTORY_BAKERY = "recorder_history_bakery" def bake_query_and_join_attributes( - hass: HomeAssistant, no_attributes: bool, include_last_updated: bool = True + hass: HomeAssistant, no_attributes: bool, include_last_changed: bool = True ) -> tuple[Any, bool]: """Return the initial backed query and if StateAttributes should be joined. @@ -126,31 +126,31 @@ def bake_query_and_join_attributes( # without the attributes fields and do not join the # state_attributes table if no_attributes: - if include_last_updated: + if include_last_changed: return bakery(lambda s: s.query(*QUERY_STATE_NO_ATTR)), False return ( - bakery(lambda s: s.query(*QUERY_STATE_NO_ATTR_NO_LAST_UPDATED)), + bakery(lambda s: s.query(*QUERY_STATE_NO_ATTR_NO_LAST_CHANGED)), False, ) # If we in the process of migrating schema we do # not want to join the state_attributes table as we # do not know if it will be there yet if recorder.get_instance(hass).schema_version < 25: - if include_last_updated: + if include_last_changed: return ( bakery(lambda s: s.query(*QUERY_STATES_PRE_SCHEMA_25)), False, ) return ( - bakery(lambda s: s.query(*QUERY_STATES_PRE_SCHEMA_25_NO_LAST_UPDATED)), + bakery(lambda s: s.query(*QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED)), False, ) # Finally if no migration is in progress and no_attributes # was not requested, we query both attributes columns and # join state_attributes - if include_last_updated: + if include_last_changed: return bakery(lambda s: s.query(*QUERY_STATES)), True - return bakery(lambda s: s.query(*QUERY_STATES_NO_LAST_UPDATED)), True + return bakery(lambda s: s.query(*QUERY_STATES_NO_LAST_CHANGED)), True def async_setup(hass: HomeAssistant) -> None: @@ -213,7 +213,9 @@ def _query_significant_states_with_session( if _LOGGER.isEnabledFor(logging.DEBUG): timer_start = time.perf_counter() - baked_query, join_attributes = bake_query_and_join_attributes(hass, no_attributes) + baked_query, join_attributes = bake_query_and_join_attributes( + hass, no_attributes, include_last_changed=True + ) if entity_ids is not None and len(entity_ids) == 1: if ( @@ -221,10 +223,11 @@ def _query_significant_states_with_session( and split_entity_id(entity_ids[0])[0] not in SIGNIFICANT_DOMAINS ): baked_query, join_attributes = bake_query_and_join_attributes( - hass, no_attributes, include_last_updated=False + hass, no_attributes, include_last_changed=False ) baked_query += lambda q: q.filter( - States.last_changed == States.last_updated + (States.last_changed == States.last_updated) + | States.last_changed.is_(None) ) elif significant_changes_only: baked_query += lambda q: q.filter( @@ -233,7 +236,10 @@ def _query_significant_states_with_session( States.entity_id.like(entity_domain) for entity_domain in SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE ], - (States.last_changed == States.last_updated), + ( + (States.last_changed == States.last_updated) + | States.last_changed.is_(None) + ), ) ) @@ -360,11 +366,14 @@ def state_changes_during_period( """Return states changes during UTC period start_time - end_time.""" with session_scope(hass=hass) as session: baked_query, join_attributes = bake_query_and_join_attributes( - hass, no_attributes, include_last_updated=False + hass, no_attributes, include_last_changed=False ) baked_query += lambda q: q.filter( - (States.last_changed == States.last_updated) + ( + (States.last_changed == States.last_updated) + | States.last_changed.is_(None) + ) & (States.last_updated > bindparam("start_time")) ) @@ -424,10 +433,12 @@ def get_last_state_changes( with session_scope(hass=hass) as session: baked_query, join_attributes = bake_query_and_join_attributes( - hass, False, include_last_updated=False + hass, False, include_last_changed=False ) - baked_query += lambda q: q.filter(States.last_changed == States.last_updated) + baked_query += lambda q: q.filter( + (States.last_changed == States.last_updated) | States.last_changed.is_(None) + ) if entity_id is not None: baked_query += lambda q: q.filter_by(entity_id=bindparam("entity_id")) @@ -489,7 +500,9 @@ def _get_states_baked_query_for_entites( no_attributes: bool = False, ) -> BakedQuery: """Baked query to get states for specific entities.""" - baked_query, join_attributes = bake_query_and_join_attributes(hass, no_attributes) + baked_query, join_attributes = bake_query_and_join_attributes( + hass, no_attributes, include_last_changed=True + ) baked_query += _most_recent_state_ids_entities_subquery if join_attributes: baked_query += lambda q: q.outerjoin( @@ -540,7 +553,9 @@ def _get_states_baked_query_for_all( no_attributes: bool = False, ) -> BakedQuery: """Baked query to get states for all entities.""" - baked_query, join_attributes = bake_query_and_join_attributes(hass, no_attributes) + baked_query, join_attributes = bake_query_and_join_attributes( + hass, no_attributes, include_last_changed=True + ) baked_query += _most_recent_state_ids_subquery baked_query += _ignore_domains_filter if filters: @@ -599,7 +614,9 @@ def _get_single_entity_states_with_session( ) -> list[Row]: # Use an entirely different (and extremely fast) query if we only # have a single entity id - baked_query, join_attributes = bake_query_and_join_attributes(hass, no_attributes) + baked_query, join_attributes = bake_query_and_join_attributes( + hass, no_attributes, include_last_changed=True + ) baked_query += lambda q: q.filter( States.last_updated < bindparam("utc_point_in_time"), States.entity_id == bindparam("entity_id"), @@ -720,7 +737,12 @@ def _sorted_states_to_dict( ent_results.append( { attr_state: state, - attr_last_changed: _process_timestamp(row.last_changed), + # + # minimal_response only makes sense with last_updated == last_updated + # + # We use last_updated for for last_changed since its the same + # + attr_last_changed: _process_timestamp(row.last_updated), } ) prev_state = state diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 38b03eb824e..9faedbbdb1e 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -250,7 +250,7 @@ class States(Base): # type: ignore[misc,valid-type] event_id = Column( Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True ) - last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) + last_changed = Column(DATETIME_TYPE) last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) attributes_id = Column( @@ -291,12 +291,16 @@ class States(Base): # type: ignore[misc,valid-type] # None state means the state was removed from the state machine if state is None: dbstate.state = "" - dbstate.last_changed = event.time_fired dbstate.last_updated = event.time_fired + dbstate.last_changed = None + return dbstate + + dbstate.state = state.state + dbstate.last_updated = state.last_updated + if state.last_updated == state.last_changed: + dbstate.last_changed = None else: - dbstate.state = state.state dbstate.last_changed = state.last_changed - dbstate.last_updated = state.last_updated return dbstate @@ -308,21 +312,27 @@ class States(Base): # type: ignore[misc,valid-type] parent_id=self.context_parent_id, ) try: - return State( - self.entity_id, - self.state, - # Join the state_attributes table on attributes_id to get the attributes - # for newer states - json.loads(self.attributes) if self.attributes else {}, - process_timestamp(self.last_changed), - process_timestamp(self.last_updated), - context=context, - validate_entity_id=validate_entity_id, - ) + attrs = json.loads(self.attributes) if self.attributes else {} except ValueError: # When json.loads fails _LOGGER.exception("Error converting row to state: %s", self) return None + if self.last_changed is None or self.last_changed == self.last_updated: + last_changed = last_updated = process_timestamp(self.last_updated) + else: + last_updated = process_timestamp(self.last_updated) + last_changed = process_timestamp(self.last_changed) + return State( + self.entity_id, + self.state, + # Join the state_attributes table on attributes_id to get the attributes + # for newer states + attrs, + last_changed, + last_updated, + context=context, + validate_entity_id=validate_entity_id, + ) class StateAttributes(Base): # type: ignore[misc,valid-type] @@ -708,7 +718,10 @@ class LazyState(State): def last_changed(self) -> datetime: # type: ignore[override] """Last changed datetime.""" if self._last_changed is None: - self._last_changed = process_timestamp(self._row.last_changed) + if (last_changed := self._row.last_changed) is not None: + self._last_changed = process_timestamp(last_changed) + else: + self._last_changed = self.last_updated return self._last_changed @last_changed.setter @@ -720,10 +733,7 @@ class LazyState(State): def last_updated(self) -> datetime: # type: ignore[override] """Last updated datetime.""" if self._last_updated is None: - if (last_updated := self._row.last_updated) is not None: - self._last_updated = process_timestamp(last_updated) - else: - self._last_updated = self.last_changed + self._last_updated = process_timestamp(self._row.last_updated) return self._last_updated @last_updated.setter @@ -739,24 +749,24 @@ class LazyState(State): To be used for JSON serialization. """ if self._last_changed is None and self._last_updated is None: - last_changed_isoformat = process_timestamp_to_utc_isoformat( - self._row.last_changed + last_updated_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_updated ) if ( - self._row.last_updated is None + self._row.last_changed is None or self._row.last_changed == self._row.last_updated ): - last_updated_isoformat = last_changed_isoformat + last_changed_isoformat = last_updated_isoformat else: - last_updated_isoformat = process_timestamp_to_utc_isoformat( - self._row.last_updated + last_changed_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_changed ) else: - last_changed_isoformat = self.last_changed.isoformat() + last_updated_isoformat = self.last_updated.isoformat() if self.last_changed == self.last_updated: - last_updated_isoformat = last_changed_isoformat + last_changed_isoformat = last_updated_isoformat else: - last_updated_isoformat = self.last_updated.isoformat() + last_changed_isoformat = self.last_changed.isoformat() return { "entity_id": self.entity_id, "state": self.state, @@ -801,13 +811,13 @@ def row_to_compressed_state( if start_time: last_changed = last_updated = start_time.timestamp() else: - row_changed_changed: datetime = row.last_changed + row_last_updated: datetime = row.last_updated if ( - not (row_last_updated := row.last_updated) + not (row_changed_changed := row.last_changed) or row_last_updated == row_changed_changed ): last_changed = last_updated = process_datetime_to_timestamp( - row_changed_changed + row_last_updated ) else: last_changed = process_datetime_to_timestamp(row_changed_changed) diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index a98712ef282..1d59893745b 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -22,12 +22,15 @@ from homeassistant.components.recorder.models import ( ) from homeassistant.components.recorder.util import session_scope import homeassistant.core as ha -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, State from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util from tests.common import SetupRecorderInstanceT, mock_state_change_event -from tests.components.recorder.common import wait_recording_done +from tests.components.recorder.common import ( + async_wait_recording_done, + wait_recording_done, +) async def _async_get_states( @@ -79,7 +82,7 @@ def _add_db_entries( entity_id=entity_id, state="on", attributes='{"name":"the light"}', - last_changed=point, + last_changed=None, last_updated=point, event_id=1001 + idx, attributes_id=1002 + idx, @@ -785,3 +788,86 @@ async def test_get_states_query_during_migration_to_schema_25_multiple_entities( ) assert hist[0].attributes == {"name": "the light"} assert hist[1].attributes == {"name": "the light"} + + +async def test_get_full_significant_states_handles_empty_last_changed( + hass: ha.HomeAssistant, + async_setup_recorder_instance: SetupRecorderInstanceT, +): + """Test getting states when last_changed is null.""" + await async_setup_recorder_instance(hass, {}) + + now = dt_util.utcnow() + hass.states.async_set("sensor.one", "on", {"attr": "original"}) + state0 = hass.states.get("sensor.one") + await hass.async_block_till_done() + hass.states.async_set("sensor.one", "on", {"attr": "new"}) + state1 = hass.states.get("sensor.one") + + assert state0.last_changed == state1.last_changed + assert state0.last_updated != state1.last_updated + await async_wait_recording_done(hass) + + def _get_entries(): + with session_scope(hass=hass) as session: + return history.get_full_significant_states_with_session( + hass, + session, + now, + dt_util.utcnow(), + entity_ids=["sensor.one"], + significant_changes_only=False, + ) + + states = await recorder.get_instance(hass).async_add_executor_job(_get_entries) + sensor_one_states: list[State] = states["sensor.one"] + assert sensor_one_states[0] == state0 + assert sensor_one_states[1] == state1 + assert sensor_one_states[0].last_changed == sensor_one_states[1].last_changed + assert sensor_one_states[0].last_updated != sensor_one_states[1].last_updated + + def _fetch_native_states() -> list[State]: + with session_scope(hass=hass) as session: + native_states = [] + db_state_attributes = { + state_attributes.attributes_id: state_attributes + for state_attributes in session.query(StateAttributes) + } + for db_state in session.query(States): + state = db_state.to_native() + state.attributes = db_state_attributes[ + db_state.attributes_id + ].to_native() + native_states.append(state) + return native_states + + native_sensor_one_states = await recorder.get_instance(hass).async_add_executor_job( + _fetch_native_states + ) + assert native_sensor_one_states[0] == state0 + assert native_sensor_one_states[1] == state1 + assert ( + native_sensor_one_states[0].last_changed + == native_sensor_one_states[1].last_changed + ) + assert ( + native_sensor_one_states[0].last_updated + != native_sensor_one_states[1].last_updated + ) + + def _fetch_db_states() -> list[State]: + with session_scope(hass=hass) as session: + states = list(session.query(States)) + session.expunge_all() + return states + + db_sensor_one_states = await recorder.get_instance(hass).async_add_executor_job( + _fetch_db_states + ) + assert db_sensor_one_states[0].last_changed is None + assert ( + process_timestamp(db_sensor_one_states[1].last_changed) == state0.last_changed + ) + assert db_sensor_one_states[0].last_updated is not None + assert db_sensor_one_states[1].last_updated is not None + assert db_sensor_one_states[0].last_updated != db_sensor_one_states[1].last_updated diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index ca68d5951d8..9d07c33a17a 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -79,7 +79,7 @@ def test_from_event_to_delete_state(): assert db_state.entity_id == "sensor.temperature" assert db_state.state == "" - assert db_state.last_changed == event.time_fired + assert db_state.last_changed is None assert db_state.last_updated == event.time_fired From ebce5660e3f80ceb95c21d8fe231f792ad0dfd7f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 May 2022 15:12:08 -0400 Subject: [PATCH 0505/3516] Sync event timed_fired and the context ulid time (#71854) --- homeassistant/components/recorder/models.py | 16 +- homeassistant/core.py | 10 +- homeassistant/util/dt.py | 17 + homeassistant/util/ulid.py | 9 +- .../generic_hygrostat/test_humidifier.py | 8 +- .../generic_thermostat/test_climate.py | 12 +- tests/test_core.py | 328 +++++++++++++++++- tests/util/test_dt.py | 6 + 8 files changed, 370 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 9faedbbdb1e..2f71324fac0 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -55,10 +55,6 @@ SCHEMA_VERSION = 28 _LOGGER = logging.getLogger(__name__) -# EPOCHORDINAL is not exposed as a constant -# https://github.com/python/cpython/blob/3.10/Lib/zoneinfo/_zoneinfo.py#L12 -EPOCHORDINAL = datetime(1970, 1, 1).toordinal() - DB_TIMEZONE = "+00:00" TABLE_EVENTS = "events" @@ -649,16 +645,8 @@ def process_datetime_to_timestamp(ts: datetime) -> float: Mirrors the behavior of process_timestamp_to_utc_isoformat except it returns the epoch time. """ - if ts.tzinfo is None: - # Taken from - # https://github.com/python/cpython/blob/3.10/Lib/zoneinfo/_zoneinfo.py#L185 - return ( - (ts.toordinal() - EPOCHORDINAL) * 86400 - + ts.hour * 3600 - + ts.minute * 60 - + ts.second - + (ts.microsecond / 1000000) - ) + if ts.tzinfo is None or ts.tzinfo == dt_util.UTC: + return dt_util.utc_to_timestamp(ts) return ts.timestamp() diff --git a/homeassistant/core.py b/homeassistant/core.py index 916dd0c6f72..061695ade6a 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -734,7 +734,9 @@ class Event: self.data = data or {} self.origin = origin self.time_fired = time_fired or dt_util.utcnow() - self.context: Context = context or Context() + self.context: Context = context or Context( + id=ulid_util.ulid(dt_util.utc_to_timestamp(self.time_fired)) + ) def __hash__(self) -> int: """Make hashable.""" @@ -1363,11 +1365,11 @@ class StateMachine: if same_state and same_attr: return - if context is None: - context = Context() - now = dt_util.utcnow() + if context is None: + context = Context(id=ulid_util.ulid(dt_util.utc_to_timestamp(now))) + state = State( entity_id, new_state, diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 4b4b798a2d8..7c0a4923e71 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -14,6 +14,10 @@ DATE_STR_FORMAT = "%Y-%m-%d" UTC = dt.timezone.utc DEFAULT_TIME_ZONE: dt.tzinfo = dt.timezone.utc +# EPOCHORDINAL is not exposed as a constant +# https://github.com/python/cpython/blob/3.10/Lib/zoneinfo/_zoneinfo.py#L12 +EPOCHORDINAL = dt.datetime(1970, 1, 1).toordinal() + # Copyright (c) Django Software Foundation and individual contributors. # All rights reserved. # https://github.com/django/django/blob/master/LICENSE @@ -98,6 +102,19 @@ def utc_from_timestamp(timestamp: float) -> dt.datetime: return dt.datetime.utcfromtimestamp(timestamp).replace(tzinfo=UTC) +def utc_to_timestamp(utc_dt: dt.datetime) -> float: + """Fast conversion of a datetime in UTC to a timestamp.""" + # Taken from + # https://github.com/python/cpython/blob/3.10/Lib/zoneinfo/_zoneinfo.py#L185 + return ( + (utc_dt.toordinal() - EPOCHORDINAL) * 86400 + + utc_dt.hour * 3600 + + utc_dt.minute * 60 + + utc_dt.second + + (utc_dt.microsecond / 1000000) + ) + + def start_of_local_day(dt_or_d: dt.date | dt.datetime | None = None) -> dt.datetime: """Return local datetime object of start of day from date or datetime.""" if dt_or_d is None: diff --git a/homeassistant/util/ulid.py b/homeassistant/util/ulid.py index c38d95bd169..d40b0f48e16 100644 --- a/homeassistant/util/ulid.py +++ b/homeassistant/util/ulid.py @@ -1,4 +1,5 @@ """Helpers to generate ulids.""" +from __future__ import annotations from random import getrandbits import time @@ -17,7 +18,7 @@ def ulid_hex() -> str: return f"{int(time.time()*1000):012x}{getrandbits(80):020x}" -def ulid() -> str: +def ulid(timestamp: float | None = None) -> str: """Generate a ULID. This ulid should not be used for cryptographically secure @@ -34,9 +35,9 @@ def ulid() -> str: import ulid ulid.parse(ulid_util.ulid()) """ - ulid_bytes = int(time.time() * 1000).to_bytes(6, byteorder="big") + int( - getrandbits(80) - ).to_bytes(10, byteorder="big") + ulid_bytes = int((timestamp or time.time()) * 1000).to_bytes( + 6, byteorder="big" + ) + int(getrandbits(80)).to_bytes(10, byteorder="big") # This is base32 crockford encoding with the loop unrolled for performance # diff --git a/tests/components/generic_hygrostat/test_humidifier.py b/tests/components/generic_hygrostat/test_humidifier.py index d0412731c78..b27b6324f86 100644 --- a/tests/components/generic_hygrostat/test_humidifier.py +++ b/tests/components/generic_hygrostat/test_humidifier.py @@ -811,7 +811,7 @@ async def test_humidity_change_dry_trigger_on_not_long_enough(hass, setup_comp_4 async def test_humidity_change_dry_trigger_on_long_enough(hass, setup_comp_4): """Test if humidity change turn dry on.""" fake_changed = datetime.datetime( - 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc + 1970, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc ) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed @@ -845,7 +845,7 @@ async def test_humidity_change_dry_trigger_off_not_long_enough(hass, setup_comp_ async def test_humidity_change_dry_trigger_off_long_enough(hass, setup_comp_4): """Test if humidity change turn dry on.""" fake_changed = datetime.datetime( - 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc + 1970, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc ) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed @@ -967,7 +967,7 @@ async def test_humidity_change_humidifier_trigger_on_not_long_enough( async def test_humidity_change_humidifier_trigger_on_long_enough(hass, setup_comp_6): """Test if humidity change turn humidifier on after min cycle.""" fake_changed = datetime.datetime( - 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc + 1970, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc ) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed @@ -989,7 +989,7 @@ async def test_humidity_change_humidifier_trigger_on_long_enough(hass, setup_com async def test_humidity_change_humidifier_trigger_off_long_enough(hass, setup_comp_6): """Test if humidity change turn humidifier off after min cycle.""" fake_changed = datetime.datetime( - 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc + 1970, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc ) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index d607c6dbb61..c4d23b41579 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -749,7 +749,7 @@ async def test_temp_change_ac_trigger_on_not_long_enough(hass, setup_comp_4): async def test_temp_change_ac_trigger_on_long_enough(hass, setup_comp_4): """Test if temperature change turn ac on.""" - fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) + fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): @@ -775,7 +775,7 @@ async def test_temp_change_ac_trigger_off_not_long_enough(hass, setup_comp_4): async def test_temp_change_ac_trigger_off_long_enough(hass, setup_comp_4): """Test if temperature change turn ac on.""" - fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) + fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): @@ -855,7 +855,7 @@ async def test_temp_change_ac_trigger_on_not_long_enough_2(hass, setup_comp_5): async def test_temp_change_ac_trigger_on_long_enough_2(hass, setup_comp_5): """Test if temperature change turn ac on.""" - fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) + fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): @@ -881,7 +881,7 @@ async def test_temp_change_ac_trigger_off_not_long_enough_2(hass, setup_comp_5): async def test_temp_change_ac_trigger_off_long_enough_2(hass, setup_comp_5): """Test if temperature change turn ac on.""" - fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) + fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): @@ -969,7 +969,7 @@ async def test_temp_change_heater_trigger_on_not_long_enough(hass, setup_comp_6) async def test_temp_change_heater_trigger_on_long_enough(hass, setup_comp_6): """Test if temperature change turn heater on after min cycle.""" - fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) + fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): @@ -986,7 +986,7 @@ async def test_temp_change_heater_trigger_on_long_enough(hass, setup_comp_6): async def test_temp_change_heater_trigger_off_long_enough(hass, setup_comp_6): """Test if temperature change turn heater off after min cycle.""" - fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) + fake_changed = datetime.datetime(1970, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): diff --git a/tests/test_core.py b/tests/test_core.py index c870605fc01..104828f64e1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,5 +1,8 @@ """Test to verify that Home Assistant core works.""" +from __future__ import annotations + # pylint: disable=protected-access +import array import asyncio from datetime import datetime, timedelta import functools @@ -28,6 +31,7 @@ from homeassistant.const import ( __version__, ) import homeassistant.core as ha +from homeassistant.core import State from homeassistant.exceptions import ( InvalidEntityFormatError, InvalidStateError, @@ -1489,9 +1493,300 @@ async def test_reserving_states(hass): assert hass.states.async_available("light.bedroom") is True -async def test_state_change_events_match_state_time(hass): - """Test last_updated and timed_fired only call utcnow once.""" +def _ulid_timestamp(ulid: str) -> int: + encoded = ulid[:10].encode("ascii") + # This unpacks the time from the ulid + # Copied from + # https://github.com/ahawker/ulid/blob/06289583e9de4286b4d80b4ad000d137816502ca/ulid/base32.py#L296 + decoding = array.array( + "B", + ( + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x01, + 0x02, + 0x03, + 0x04, + 0x05, + 0x06, + 0x07, + 0x08, + 0x09, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x0A, + 0x0B, + 0x0C, + 0x0D, + 0x0E, + 0x0F, + 0x10, + 0x11, + 0x01, + 0x12, + 0x13, + 0x01, + 0x14, + 0x15, + 0x00, + 0x16, + 0x17, + 0x18, + 0x19, + 0x1A, + 0xFF, + 0x1B, + 0x1C, + 0x1D, + 0x1E, + 0x1F, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x0A, + 0x0B, + 0x0C, + 0x0D, + 0x0E, + 0x0F, + 0x10, + 0x11, + 0x01, + 0x12, + 0x13, + 0x01, + 0x14, + 0x15, + 0x00, + 0x16, + 0x17, + 0x18, + 0x19, + 0x1A, + 0xFF, + 0x1B, + 0x1C, + 0x1D, + 0x1E, + 0x1F, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + ), + ) + return int.from_bytes( + bytes( + ( + ((decoding[encoded[0]] << 5) | decoding[encoded[1]]) & 0xFF, + ((decoding[encoded[2]] << 3) | (decoding[encoded[3]] >> 2)) & 0xFF, + ( + (decoding[encoded[3]] << 6) + | (decoding[encoded[4]] << 1) + | (decoding[encoded[5]] >> 4) + ) + & 0xFF, + ((decoding[encoded[5]] << 4) | (decoding[encoded[6]] >> 1)) & 0xFF, + ( + (decoding[encoded[6]] << 7) + | (decoding[encoded[7]] << 2) + | (decoding[encoded[8]] >> 3) + ) + & 0xFF, + ((decoding[encoded[8]] << 5) | (decoding[encoded[9]])) & 0xFF, + ) + ), + byteorder="big", + ) + + +async def test_state_change_events_context_id_match_state_time(hass): + """Test last_updated, timed_fired, and the ulid all have the same time.""" events = [] @ha.callback @@ -1502,6 +1797,31 @@ async def test_state_change_events_match_state_time(hass): hass.states.async_set("light.bedroom", "on") await hass.async_block_till_done() - state = hass.states.get("light.bedroom") - + state: State = hass.states.get("light.bedroom") assert state.last_updated == events[0].time_fired + assert len(state.context.id) == 26 + # ULIDs store time to 3 decimal places compared to python timestamps + assert _ulid_timestamp(state.context.id) == int( + state.last_updated.timestamp() * 1000 + ) + + +async def test_state_firing_event_matches_context_id_ulid_time(hass): + """Test timed_fired and the ulid have the same time.""" + events = [] + + @ha.callback + def _event_listener(event): + events.append(event) + + hass.bus.async_listen(EVENT_HOMEASSISTANT_STARTED, _event_listener) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + event = events[0] + assert len(event.context.id) == 26 + # ULIDs store time to 3 decimal places compared to python timestamps + assert _ulid_timestamp(event.context.id) == int( + events[0].time_fired.timestamp() * 1000 + ) diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index d2c453f070d..6e499e6e6f1 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -106,6 +106,12 @@ def test_utc_from_timestamp(): ) +def test_timestamp_to_utc(): + """Test we can convert a utc datetime to a timestamp.""" + utc_now = dt_util.utcnow() + assert dt_util.utc_to_timestamp(utc_now) == utc_now.timestamp() + + def test_as_timestamp(): """Test as_timestamp method.""" ts = 1462401234 From 4e9bc9eaffd464f192d187a01771a86699b2f932 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 May 2022 15:13:32 -0400 Subject: [PATCH 0506/3516] Small cleanups to find_next_time_expression and addition of tests (#71845) --- homeassistant/util/dt.py | 8 +++---- tests/util/test_dt.py | 50 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 7c0a4923e71..c7073c0306f 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -339,8 +339,8 @@ def find_next_time_expression_time( now += dt.timedelta(seconds=1) continue - now_is_ambiguous = _datetime_ambiguous(now) - result_is_ambiguous = _datetime_ambiguous(result) + if not _datetime_ambiguous(now): + return result # When leaving DST and clocks are turned backward. # Then there are wall clock times that are ambiguous i.e. exist with DST and without DST @@ -348,7 +348,7 @@ def find_next_time_expression_time( # in a day. # Example: on 2021.10.31 02:00:00 in CET timezone clocks are turned backward an hour - if now_is_ambiguous and result_is_ambiguous: + if _datetime_ambiguous(result): # `now` and `result` are both ambiguous, so the next match happens # _within_ the current fold. @@ -357,7 +357,7 @@ def find_next_time_expression_time( # 2. 2021.10.31 02:00:00+01:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 return result.replace(fold=now.fold) - if now_is_ambiguous and now.fold == 0 and not result_is_ambiguous: + if now.fold == 0: # `now` is in the first fold, but result is not ambiguous (meaning it no longer matches # within the fold). # -> Check if result matches in the next fold. If so, emit that match diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 6e499e6e6f1..c7992e05068 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -641,3 +641,53 @@ def test_find_next_time_expression_time_leave_dst_chicago_past_the_fold_ahead_2_ assert dt_util.as_utc(next_time) == datetime( 2021, 11, 7, 8, 20, 1, tzinfo=dt_util.UTC ) + + +def test_find_next_time_expression_microseconds(): + """Test finding next time expression with microsecond clock drift.""" + hour_minute_second = (None, "5", "10") + test_time = datetime(2022, 5, 13, 0, 5, 9, tzinfo=dt_util.UTC) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *hour_minute_second + ) + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2022, 5, 13, 0, 5, 10, tzinfo=dt_util.UTC) + next_time_last_microsecond_plus_one = next_time.replace( + microsecond=999999 + ) + timedelta(seconds=1) + time_after = dt_util.find_next_time_expression_time( + next_time_last_microsecond_plus_one, + matching_seconds, + matching_minutes, + matching_hours, + ) + assert time_after == datetime(2022, 5, 13, 1, 5, 10, tzinfo=dt_util.UTC) + + +def test_find_next_time_expression_tenth_second_pattern_does_not_drift_entering_dst(): + """Test finding next time expression tenth second pattern does not drift entering dst.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + tenth_second_pattern = (None, None, "10") + # Entering DST, clocks go forward + test_time = datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz, fold=0) + matching_hours, matching_minutes, matching_seconds = _get_matches( + *tenth_second_pattern + ) + next_time = dt_util.find_next_time_expression_time( + test_time, matching_seconds, matching_minutes, matching_hours + ) + assert next_time == datetime(2021, 3, 15, 2, 30, 10, tzinfo=tz) + prev_target = next_time + for i in range(1000): + next_target = dt_util.find_next_time_expression_time( + prev_target.replace(microsecond=999999) + timedelta(seconds=1), + matching_seconds, + matching_minutes, + matching_hours, + ) + assert (next_target - prev_target).total_seconds() == 60 + assert next_target.second == 10 + prev_target = next_target From cd2898886bef9938e472d6d0f9170e8745a6954d Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 14 May 2022 12:24:52 -0700 Subject: [PATCH 0507/3516] Upgrade grpcio to 1.46.1 (#71865) --- homeassistant/package_constraints.txt | 4 ++-- script/gen_requirements_all.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9c9354c584b..ec3ac5de9b3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -50,8 +50,8 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.45.0 -grpcio-status==1.45.0 +grpcio==1.46.1 +grpcio-status==1.46.1 # libcst >=0.4.0 requires a newer Rust than we currently have available, # thus our wheels builds fail. This pins it to the last working version, diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index be94fdac22b..369045e5124 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -66,8 +66,8 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.45.0 -grpcio-status==1.45.0 +grpcio==1.46.1 +grpcio-status==1.46.1 # libcst >=0.4.0 requires a newer Rust than we currently have available, # thus our wheels builds fail. This pins it to the last working version, From 68632cb2671c26a8d367c1dafb6ac2e6c2904b95 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 May 2022 15:37:35 -0400 Subject: [PATCH 0508/3516] Implement use_include_order in the history websocket api (#71839) --- homeassistant/components/history/__init__.py | 121 ++++++++++--------- homeassistant/components/logbook/queries.py | 2 +- tests/components/history/test_init.py | 58 +++++++++ 3 files changed, 124 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index e5b6c99eb2a..2ebe6405a7a 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -10,6 +10,8 @@ from typing import Any, Literal, cast from aiohttp import web from sqlalchemy import not_, or_ +from sqlalchemy.ext.baked import BakedQuery +from sqlalchemy.orm import Query import voluptuous as vol from homeassistant.components import frontend, websocket_api @@ -36,12 +38,12 @@ from homeassistant.helpers.entityfilter import ( from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util -# mypy: allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) DOMAIN = "history" HISTORY_FILTERS = "history_filters" +HISTORY_USE_INCLUDE_ORDER = "history_use_include_order" + CONF_ORDER = "use_include_order" GLOB_TO_SQL_CHARS = { @@ -66,8 +68,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[HISTORY_FILTERS] = filters = sqlalchemy_filter_from_include_exclude_conf( conf ) - - use_include_order = conf.get(CONF_ORDER) + hass.data[HISTORY_USE_INCLUDE_ORDER] = use_include_order = conf.get(CONF_ORDER) hass.http.register_view(HistoryPeriodView(filters, use_include_order)) frontend.async_register_built_in_panel(hass, "history", "history", "hass:chart-box") @@ -176,30 +177,41 @@ def _ws_get_significant_states( hass: HomeAssistant, msg_id: int, start_time: dt, - end_time: dt | None = None, - entity_ids: list[str] | None = None, - filters: Any | None = None, - include_start_time_state: bool = True, - significant_changes_only: bool = True, - minimal_response: bool = False, - no_attributes: bool = False, + end_time: dt | None, + entity_ids: list[str] | None, + filters: Filters | None, + use_include_order: bool | None, + include_start_time_state: bool, + significant_changes_only: bool, + minimal_response: bool, + no_attributes: bool, ) -> str: """Fetch history significant_states and convert them to json in the executor.""" + states = history.get_significant_states( + hass, + start_time, + end_time, + entity_ids, + filters, + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + True, + ) + + if not use_include_order or not filters: + return JSON_DUMP(messages.result_message(msg_id, states)) + return JSON_DUMP( messages.result_message( msg_id, - history.get_significant_states( - hass, - start_time, - end_time, - entity_ids, - filters, - include_start_time_state, - significant_changes_only, - minimal_response, - no_attributes, - True, - ), + { + order_entity: states.pop(order_entity) + for order_entity in filters.included_entities + if order_entity in states + } + | states, ) ) @@ -267,6 +279,7 @@ async def ws_get_history_during_period( end_time, entity_ids, hass.data[HISTORY_FILTERS], + hass.data[HISTORY_USE_INCLUDE_ORDER], include_start_time_state, significant_changes_only, minimal_response, @@ -351,20 +364,20 @@ class HistoryPeriodView(HomeAssistantView): def _sorted_significant_states_json( self, - hass, - start_time, - end_time, - entity_ids, - include_start_time_state, - significant_changes_only, - minimal_response, - no_attributes, - ): + hass: HomeAssistant, + start_time: dt, + end_time: dt, + entity_ids: list[str] | None, + include_start_time_state: bool, + significant_changes_only: bool, + minimal_response: bool, + no_attributes: bool, + ) -> web.Response: """Fetch significant stats from the database as json.""" timer_start = time.perf_counter() with session_scope(hass=hass) as session: - result = history.get_significant_states_with_session( + states = history.get_significant_states_with_session( hass, session, start_time, @@ -377,25 +390,24 @@ class HistoryPeriodView(HomeAssistantView): no_attributes, ) - result = list(result.values()) if _LOGGER.isEnabledFor(logging.DEBUG): elapsed = time.perf_counter() - timer_start - _LOGGER.debug("Extracted %d states in %fs", sum(map(len, result)), elapsed) + _LOGGER.debug( + "Extracted %d states in %fs", sum(map(len, states.values())), elapsed + ) # Optionally reorder the result to respect the ordering given # by any entities explicitly included in the configuration. - if self.filters and self.use_include_order: - sorted_result = [] - for order_entity in self.filters.included_entities: - for state_list in result: - if state_list[0].entity_id == order_entity: - sorted_result.append(state_list) - result.remove(state_list) - break - sorted_result.extend(result) - result = sorted_result + if not self.filters or not self.use_include_order: + return self.json(list(states.values())) - return self.json(result) + sorted_result = [ + states.pop(order_entity) + for order_entity in self.filters.included_entities + if order_entity in states + ] + sorted_result.extend(list(states.values())) + return self.json(sorted_result) def sqlalchemy_filter_from_include_exclude_conf(conf: ConfigType) -> Filters | None: @@ -426,7 +438,7 @@ class Filters: self.included_domains: list[str] = [] self.included_entity_globs: list[str] = [] - def apply(self, query): + def apply(self, query: Query) -> Query: """Apply the entity filter.""" if not self.has_config: return query @@ -434,21 +446,18 @@ class Filters: return query.filter(self.entity_filter()) @property - def has_config(self): + def has_config(self) -> bool: """Determine if there is any filter configuration.""" - if ( + return bool( self.excluded_entities or self.excluded_domains or self.excluded_entity_globs or self.included_entities or self.included_domains or self.included_entity_globs - ): - return True + ) - return False - - def bake(self, baked_query): + def bake(self, baked_query: BakedQuery) -> None: """Update a baked query. Works the same as apply on a baked_query. @@ -458,7 +467,7 @@ class Filters: baked_query += lambda q: q.filter(self.entity_filter()) - def entity_filter(self): + def entity_filter(self) -> Any: """Generate the entity filter query.""" includes = [] if self.included_domains: @@ -502,7 +511,7 @@ class Filters: return or_(*includes) & not_(or_(*excludes)) -def _glob_to_like(glob_str): +def _glob_to_like(glob_str: str) -> Any: """Translate glob to sql.""" return history_models.States.entity_id.like(glob_str.translate(GLOB_TO_SQL_CHARS)) diff --git a/homeassistant/components/logbook/queries.py b/homeassistant/components/logbook/queries.py index ba1138c2c26..1cea6599327 100644 --- a/homeassistant/components/logbook/queries.py +++ b/homeassistant/components/logbook/queries.py @@ -83,7 +83,7 @@ def statement_for_request( # No entities: logbook sends everything for the timeframe # limited by the context_id and the yaml configured filter if not entity_ids: - entity_filter = filters.entity_filter() if filters else None # type: ignore[no-untyped-call] + entity_filter = filters.entity_filter() if filters else None return _all_stmt(start_day, end_day, event_types, entity_filter, context_id) # Multiple entities: logbook sends everything for the timeframe for the entities diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 1dc18e5cc73..bcbab1e21ca 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -11,6 +11,7 @@ from pytest import approx from homeassistant.components import history from homeassistant.components.recorder.history import get_significant_states from homeassistant.components.recorder.models import process_timestamp +from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES import homeassistant.core as ha from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component @@ -1476,3 +1477,60 @@ async def test_history_during_period_bad_end_time(hass, hass_ws_client, recorder response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == "invalid_end_time" + + +async def test_history_during_period_with_use_include_order( + hass, hass_ws_client, recorder_mock +): + """Test history_during_period.""" + now = dt_util.utcnow() + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + history.CONF_INCLUDE: { + CONF_ENTITIES: sort_order, + CONF_DOMAINS: ["sensor"], + }, + } + }, + ) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.three", "off", attributes={"any": "changed"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.four", "off", attributes={"any": "again"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + do_adhoc_statistics(hass, start=now) + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + + assert list(response["result"]) == [ + *sort_order, + "sensor.three", + ] From 0584e84c30903aae07cf16898138ce4e1e8b6be7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 May 2022 17:01:36 -0500 Subject: [PATCH 0509/3516] Add MySQL index hints to logbook (#71864) * Add MySQL index hints to logbook * fix mysql query planner --- homeassistant/components/logbook/queries.py | 102 ++++++++++++++------ homeassistant/components/recorder/models.py | 4 +- 2 files changed, 76 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/logbook/queries.py b/homeassistant/components/logbook/queries.py index 1cea6599327..e8e691f2787 100644 --- a/homeassistant/components/logbook/queries.py +++ b/homeassistant/components/logbook/queries.py @@ -7,7 +7,7 @@ from typing import Any import sqlalchemy from sqlalchemy import lambda_stmt, select, union_all -from sqlalchemy.orm import aliased +from sqlalchemy.orm import Query, aliased from sqlalchemy.sql.expression import literal from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import Select @@ -15,6 +15,8 @@ from sqlalchemy.sql.selectable import Select from homeassistant.components.history import Filters from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN from homeassistant.components.recorder.models import ( + ENTITY_ID_LAST_UPDATED_INDEX, + LAST_UPDATED_INDEX, EventData, Events, StateAttributes, @@ -31,6 +33,8 @@ CONTINUOUS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in CONTINUOUS_DOMAINS] UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%" +OLD_STATE = aliased(States, name="old_state") + EVENT_COLUMNS = ( Events.event_id.label("event_id"), @@ -126,7 +130,7 @@ def _select_entities_context_ids_sub_query( _select_events_context_id_subquery(start_day, end_day, event_types).where( _apply_event_entity_id_matchers(entity_ids) ), - select(States.context_id) + _apply_entities_hints(select(States.context_id)) .filter((States.last_updated > start_day) & (States.last_updated < end_day)) .where(States.entity_id.in_(entity_ids)), ).c.context_id @@ -156,7 +160,7 @@ def _entities_stmt( ) stmt = stmt.add_criteria( lambda s: s.where(_apply_event_entity_id_matchers(entity_ids)).union_all( - _select_states(start_day, end_day).where(States.entity_id.in_(entity_ids)), + _states_query_for_entitiy_ids(start_day, end_day, entity_ids), _select_events_context_only().where( Events.context_id.in_( _select_entities_context_ids_sub_query( @@ -192,7 +196,7 @@ def _select_entity_context_ids_sub_query( Events.event_data.like(entity_id_like) | EventData.shared_data.like(entity_id_like) ), - select(States.context_id) + _apply_entities_hints(select(States.context_id)) .filter((States.last_updated > start_day) & (States.last_updated < end_day)) .where(States.entity_id == entity_id), ).c.context_id @@ -214,7 +218,7 @@ def _single_entity_stmt( | EventData.shared_data.like(entity_id_like) ) .union_all( - _select_states(start_day, end_day).where(States.entity_id == entity_id), + _states_query_for_entitiy_id(start_day, end_day, entity_id), _select_events_context_only().where( Events.context_id.in_( _select_entity_context_ids_sub_query( @@ -244,15 +248,15 @@ def _all_stmt( # are gone from the database remove the # _legacy_select_events_context_id() stmt += lambda s: s.where(Events.context_id == context_id).union_all( - _select_states(start_day, end_day).where(States.context_id == context_id), + _states_query_for_context_id(start_day, end_day, context_id), _legacy_select_events_context_id(start_day, end_day, context_id), ) elif entity_filter is not None: stmt += lambda s: s.union_all( - _select_states(start_day, end_day).where(entity_filter) + _states_query_for_all(start_day, end_day).where(entity_filter) ) else: - stmt += lambda s: s.union_all(_select_states(start_day, end_day)) + stmt += lambda s: s.union_all(_states_query_for_all(start_day, end_day)) stmt += lambda s: s.order_by(Events.time_fired) return stmt @@ -294,27 +298,67 @@ def _select_events_without_states( ) -def _select_states(start_day: dt, end_day: dt) -> Select: +def _states_query_for_context_id(start_day: dt, end_day: dt, context_id: str) -> Query: + return _apply_states_filters(_select_states(), start_day, end_day).where( + States.context_id == context_id + ) + + +def _states_query_for_entitiy_id(start_day: dt, end_day: dt, entity_id: str) -> Query: + return _apply_states_filters( + _apply_entities_hints(_select_states()), start_day, end_day + ).where(States.entity_id == entity_id) + + +def _states_query_for_entitiy_ids( + start_day: dt, end_day: dt, entity_ids: list[str] +) -> Query: + return _apply_states_filters( + _apply_entities_hints(_select_states()), start_day, end_day + ).where(States.entity_id.in_(entity_ids)) + + +def _states_query_for_all(start_day: dt, end_day: dt) -> Query: + return _apply_states_filters(_apply_all_hints(_select_states()), start_day, end_day) + + +def _select_states() -> Select: """Generate a states select that formats the states table as event rows.""" - old_state = aliased(States, name="old_state") + return select( + literal(value=None, type_=sqlalchemy.Text).label("event_id"), + literal(value=EVENT_STATE_CHANGED, type_=sqlalchemy.String).label("event_type"), + literal(value=None, type_=sqlalchemy.Text).label("event_data"), + States.last_updated.label("time_fired"), + States.context_id.label("context_id"), + States.context_user_id.label("context_user_id"), + States.context_parent_id.label("context_parent_id"), + literal(value=None, type_=sqlalchemy.Text).label("shared_data"), + *STATE_COLUMNS, + NOT_CONTEXT_ONLY, + ) + + +def _apply_all_hints(query: Query) -> Query: + """Force mysql to use the right index on large selects.""" + return query.with_hint( + States, f"FORCE INDEX ({LAST_UPDATED_INDEX})", dialect_name="mysql" + ) + + +def _apply_entities_hints(query: Query) -> Query: + """Force mysql to use the right index on large selects.""" + return query.with_hint( + States, f"FORCE INDEX ({ENTITY_ID_LAST_UPDATED_INDEX})", dialect_name="mysql" + ) + + +def _apply_states_filters(query: Query, start_day: dt, end_day: dt) -> Query: return ( - select( - literal(value=None, type_=sqlalchemy.Text).label("event_id"), - literal(value=EVENT_STATE_CHANGED, type_=sqlalchemy.String).label( - "event_type" - ), - literal(value=None, type_=sqlalchemy.Text).label("event_data"), - States.last_updated.label("time_fired"), - States.context_id.label("context_id"), - States.context_user_id.label("context_user_id"), - States.context_parent_id.label("context_parent_id"), - literal(value=None, type_=sqlalchemy.Text).label("shared_data"), - *STATE_COLUMNS, - NOT_CONTEXT_ONLY, + query.filter( + (States.last_updated > start_day) & (States.last_updated < end_day) ) - .filter((States.last_updated > start_day) & (States.last_updated < end_day)) - .outerjoin(old_state, (States.old_state_id == old_state.state_id)) - .where(_missing_state_matcher(old_state)) + .outerjoin(OLD_STATE, (States.old_state_id == OLD_STATE.state_id)) + .where(_missing_state_matcher()) .where(_not_continuous_entity_matcher()) .where( (States.last_updated == States.last_changed) | States.last_changed.is_(None) @@ -325,13 +369,13 @@ def _select_states(start_day: dt, end_day: dt) -> Select: ) -def _missing_state_matcher(old_state: States) -> sqlalchemy.and_: +def _missing_state_matcher() -> sqlalchemy.and_: # The below removes state change events that do not have # and old_state or the old_state is missing (newly added entities) # or the new_state is missing (removed entities) return sqlalchemy.and_( - old_state.state_id.isnot(None), - (States.state != old_state.state), + OLD_STATE.state_id.isnot(None), + (States.state != OLD_STATE.state), States.state.isnot(None), ) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 2f71324fac0..4c3832c4fc0 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -88,6 +88,8 @@ TABLES_TO_CHECK = [ TABLE_SCHEMA_CHANGES, ] +LAST_UPDATED_INDEX = "ix_states_last_updated" +ENTITY_ID_LAST_UPDATED_INDEX = "ix_states_entity_id_last_updated" EMPTY_JSON_OBJECT = "{}" @@ -235,7 +237,7 @@ class States(Base): # type: ignore[misc,valid-type] __table_args__ = ( # Used for fetching the state of entities at a specific time # (get_states in history.py) - Index("ix_states_entity_id_last_updated", "entity_id", "last_updated"), + Index(ENTITY_ID_LAST_UPDATED_INDEX, "entity_id", "last_updated"), {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, ) __tablename__ = TABLE_STATES From 51c6a68036acb91bce72beb94f366d583d5a2b6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 May 2022 17:22:47 -0500 Subject: [PATCH 0510/3516] Add Big Ass Fans integration (#71498) Co-authored-by: Paulus Schoutsen --- .coveragerc | 3 + .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/components/baf/__init__.py | 46 +++++ homeassistant/components/baf/config_flow.py | 120 +++++++++++++ homeassistant/components/baf/const.py | 19 +++ homeassistant/components/baf/entity.py | 48 ++++++ homeassistant/components/baf/fan.py | 97 +++++++++++ homeassistant/components/baf/manifest.json | 13 ++ homeassistant/components/baf/models.py | 25 +++ homeassistant/components/baf/strings.json | 23 +++ .../components/baf/translations/en.json | 23 +++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 14 ++ mypy.ini | 11 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/baf/__init__.py | 1 + tests/components/baf/test_config_flow.py | 158 ++++++++++++++++++ 19 files changed, 611 insertions(+) create mode 100644 homeassistant/components/baf/__init__.py create mode 100644 homeassistant/components/baf/config_flow.py create mode 100644 homeassistant/components/baf/const.py create mode 100644 homeassistant/components/baf/entity.py create mode 100644 homeassistant/components/baf/fan.py create mode 100644 homeassistant/components/baf/manifest.json create mode 100644 homeassistant/components/baf/models.py create mode 100644 homeassistant/components/baf/strings.json create mode 100644 homeassistant/components/baf/translations/en.json create mode 100644 tests/components/baf/__init__.py create mode 100644 tests/components/baf/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 2a8e6cdec9a..15b9544dad5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -93,6 +93,9 @@ omit = homeassistant/components/azure_devops/const.py homeassistant/components/azure_devops/sensor.py homeassistant/components/azure_service_bus/* + homeassistant/components/baf/__init__.py + homeassistant/components/baf/entity.py + homeassistant/components/baf/fan.py homeassistant/components/baidu/tts.py homeassistant/components/balboa/__init__.py homeassistant/components/beewi_smartclim/sensor.py diff --git a/.strict-typing b/.strict-typing index 8c580dfa1aa..36ed1685e9f 100644 --- a/.strict-typing +++ b/.strict-typing @@ -55,6 +55,7 @@ homeassistant.components.aseko_pool_live.* homeassistant.components.asuswrt.* homeassistant.components.automation.* homeassistant.components.backup.* +homeassistant.components.baf.* homeassistant.components.binary_sensor.* homeassistant.components.bluetooth_tracker.* homeassistant.components.bmw_connected_drive.* diff --git a/CODEOWNERS b/CODEOWNERS index 68f9017363e..0805b17cff5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -120,6 +120,8 @@ build.json @home-assistant/supervisor /homeassistant/components/azure_service_bus/ @hfurubotten /homeassistant/components/backup/ @home-assistant/core /tests/components/backup/ @home-assistant/core +/homeassistant/components/baf/ @bdraco @jfroy +/tests/components/baf/ @bdraco @jfroy /homeassistant/components/balboa/ @garbled1 /tests/components/balboa/ @garbled1 /homeassistant/components/beewi_smartclim/ @alemuro diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py new file mode 100644 index 00000000000..4327eb9ddb9 --- /dev/null +++ b/homeassistant/components/baf/__init__.py @@ -0,0 +1,46 @@ +"""The Big Ass Fans integration.""" +from __future__ import annotations + +import asyncio + +from aiobafi6 import Device, Service +from aiobafi6.discovery import PORT + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_IP_ADDRESS, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN, QUERY_INTERVAL, RUN_TIMEOUT +from .models import BAFData + +PLATFORMS: list[Platform] = [Platform.FAN] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Big Ass Fans from a config entry.""" + ip_address = entry.data[CONF_IP_ADDRESS] + + service = Service(ip_addresses=[ip_address], uuid=entry.unique_id, port=PORT) + device = Device(service, query_interval_seconds=QUERY_INTERVAL) + run_future = device.async_run() + + try: + await asyncio.wait_for(device.async_wait_available(), timeout=RUN_TIMEOUT) + except asyncio.TimeoutError as ex: + run_future.cancel() + raise ConfigEntryNotReady(f"Timed out connecting to {ip_address}") from ex + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BAFData(device, run_future) + 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): + data: BAFData = hass.data[DOMAIN].pop(entry.entry_id) + data.run_future.cancel() + + return unload_ok diff --git a/homeassistant/components/baf/config_flow.py b/homeassistant/components/baf/config_flow.py new file mode 100644 index 00000000000..1c2873eb759 --- /dev/null +++ b/homeassistant/components/baf/config_flow.py @@ -0,0 +1,120 @@ +"""Config flow for baf.""" +from __future__ import annotations + +import asyncio +import logging +from typing import Any + +from aiobafi6 import Device, Service +from aiobafi6.discovery import PORT +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import zeroconf +from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.data_entry_flow import FlowResult +from homeassistant.util.network import is_ipv6_address + +from .const import DOMAIN, RUN_TIMEOUT +from .models import BAFDiscovery + +_LOGGER = logging.getLogger(__name__) + + +async def async_try_connect(ip_address: str) -> Device: + """Validate we can connect to a device.""" + device = Device(Service(ip_addresses=[ip_address], port=PORT)) + run_future = device.async_run() + try: + await asyncio.wait_for(device.async_wait_available(), timeout=RUN_TIMEOUT) + except asyncio.TimeoutError as ex: + raise CannotConnect from ex + finally: + run_future.cancel() + return device + + +class BAFFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle BAF discovery config flow.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the BAF config flow.""" + self.discovery: BAFDiscovery | None = None + + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: + """Handle zeroconf discovery.""" + properties = discovery_info.properties + ip_address = discovery_info.host + if is_ipv6_address(ip_address): + return self.async_abort(reason="ipv6_not_supported") + uuid = properties["uuid"] + model = properties["model"] + name = properties["name"] + await self.async_set_unique_id(uuid, raise_on_progress=False) + self._abort_if_unique_id_configured(updates={CONF_IP_ADDRESS: ip_address}) + self.discovery = BAFDiscovery(ip_address, name, uuid, model) + return await self.async_step_discovery_confirm() + + async def async_step_discovery_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self.discovery is not None + discovery = self.discovery + if user_input is not None: + return self.async_create_entry( + title=discovery.name, + data={CONF_IP_ADDRESS: discovery.ip_address}, + ) + placeholders = { + "name": discovery.name, + "model": discovery.model, + "ip_address": discovery.ip_address, + } + self.context["title_placeholders"] = placeholders + self._set_confirm_only() + return self.async_show_form( + step_id="discovery_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + ip_address = (user_input or {}).get(CONF_IP_ADDRESS, "") + if user_input is not None: + try: + device = await async_try_connect(ip_address) + except CannotConnect: + errors[CONF_IP_ADDRESS] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Unknown exception during connection test to %s", ip_address + ) + errors["base"] = "unknown" + else: + await self.async_set_unique_id(device.dns_sd_uuid) + self._abort_if_unique_id_configured( + updates={CONF_IP_ADDRESS: ip_address} + ) + return self.async_create_entry( + title=device.name, + data={CONF_IP_ADDRESS: ip_address}, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_IP_ADDRESS, default=ip_address): str} + ), + errors=errors, + ) + + +class CannotConnect(Exception): + """Exception to raise when we cannot connect.""" diff --git a/homeassistant/components/baf/const.py b/homeassistant/components/baf/const.py new file mode 100644 index 00000000000..9876d7ffec3 --- /dev/null +++ b/homeassistant/components/baf/const.py @@ -0,0 +1,19 @@ +"""Constants for the Big Ass Fans integration.""" + +DOMAIN = "baf" + +# Most properties are pushed, only the +# query every 5 minutes so we keep the RPM +# sensors up to date +QUERY_INTERVAL = 300 + +RUN_TIMEOUT = 20 + +PRESET_MODE_AUTO = "Auto" + +SPEED_COUNT = 7 +SPEED_RANGE = (1, SPEED_COUNT) + +ONE_MIN_SECS = 60 +ONE_DAY_SECS = 86400 +HALF_DAY_SECS = 43200 diff --git a/homeassistant/components/baf/entity.py b/homeassistant/components/baf/entity.py new file mode 100644 index 00000000000..22054d0b16d --- /dev/null +++ b/homeassistant/components/baf/entity.py @@ -0,0 +1,48 @@ +"""The baf integration entities.""" +from __future__ import annotations + +from aiobafi6 import Device + +from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.entity import DeviceInfo, Entity + + +class BAFEntity(Entity): + """Base class for baf entities.""" + + _attr_should_poll = False + + def __init__(self, device: Device, name: str) -> None: + """Initialize the entity.""" + self._device = device + self._attr_unique_id = format_mac(self._device.mac_address) + self._attr_name = name + self._attr_device_info = DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self._device.mac_address)}, + name=self._device.name, + manufacturer="Big Ass Fans", + model=self._device.model, + sw_version=self._device.firmware_version, + ) + self._async_update_attrs() + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + self._attr_available = self._device.available + + @callback + def _async_update_from_device(self, device: Device) -> None: + """Process an update from the device.""" + self._async_update_attrs() + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Add data updated listener after this object has been initialized.""" + self._device.add_callback(self._async_update_from_device) + + async def async_will_remove_from_hass(self) -> None: + """Remove data updated listener after this object has been initialized.""" + self._device.remove_callback(self._async_update_from_device) diff --git a/homeassistant/components/baf/fan.py b/homeassistant/components/baf/fan.py new file mode 100644 index 00000000000..360926363a5 --- /dev/null +++ b/homeassistant/components/baf/fan.py @@ -0,0 +1,97 @@ +"""Support for Big Ass Fans fan.""" +from __future__ import annotations + +import math +from typing import Any + +from aiobafi6 import OffOnAuto + +from homeassistant import config_entries +from homeassistant.components.fan import ( + DIRECTION_FORWARD, + DIRECTION_REVERSE, + FanEntity, + FanEntityFeature, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) + +from .const import DOMAIN, PRESET_MODE_AUTO, SPEED_COUNT, SPEED_RANGE +from .entity import BAFEntity +from .models import BAFData + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up SenseME fans.""" + data: BAFData = hass.data[DOMAIN][entry.entry_id] + if data.device.has_fan: + async_add_entities([BAFFan(data.device, data.device.name)]) + + +class BAFFan(BAFEntity, FanEntity): + """BAF ceiling fan component.""" + + _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION + _attr_preset_modes = [PRESET_MODE_AUTO] + _attr_speed_count = SPEED_COUNT + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + self._attr_is_on = self._device.fan_mode == OffOnAuto.ON + self._attr_current_direction = DIRECTION_FORWARD + if self._device.reverse_enable: + self._attr_current_direction = DIRECTION_REVERSE + if self._device.speed is not None: + self._attr_percentage = ranged_value_to_percentage( + SPEED_RANGE, self._device.speed + ) + else: + self._attr_percentage = None + auto = self._device.fan_mode == OffOnAuto.AUTO + self._attr_preset_mode = PRESET_MODE_AUTO if auto else None + super()._async_update_attrs() + + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed of the fan, as a percentage.""" + device = self._device + if device.fan_mode != OffOnAuto.ON: + device.fan_mode = OffOnAuto.ON + device.speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + + async def async_turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Turn the fan on with a percentage or preset mode.""" + if preset_mode is not None: + await self.async_set_preset_mode(preset_mode) + return + if percentage is None: + self._device.fan_mode = OffOnAuto.ON + return + await self.async_set_percentage(percentage) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the fan off.""" + self._device.fan_mode = OffOnAuto.OFF + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the fan.""" + if preset_mode != PRESET_MODE_AUTO: + raise ValueError(f"Invalid preset mode: {preset_mode}") + self._device.fan_mode = OffOnAuto.AUTO + + async def async_set_direction(self, direction: str) -> None: + """Set the direction of the fan.""" + self._device.reverse_enable = direction == DIRECTION_REVERSE diff --git a/homeassistant/components/baf/manifest.json b/homeassistant/components/baf/manifest.json new file mode 100644 index 00000000000..9dfc35685e3 --- /dev/null +++ b/homeassistant/components/baf/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "baf", + "name": "Big Ass Fans", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/baf", + "requirements": ["aiobafi6==0.3.0"], + "codeowners": ["@bdraco", "@jfroy"], + "iot_class": "local_push", + "zeroconf": [ + { "type": "_api._tcp.local.", "properties": { "model": "haiku*" } }, + { "type": "_api._tcp.local.", "properties": { "model": "i6*" } } + ] +} diff --git a/homeassistant/components/baf/models.py b/homeassistant/components/baf/models.py new file mode 100644 index 00000000000..de5c4a3498b --- /dev/null +++ b/homeassistant/components/baf/models.py @@ -0,0 +1,25 @@ +"""The baf integration models.""" +from __future__ import annotations + +import asyncio +from dataclasses import dataclass + +from aiobafi6 import Device + + +@dataclass +class BAFData: + """Data for the baf integration.""" + + device: Device + run_future: asyncio.Future + + +@dataclass +class BAFDiscovery: + """A BAF Discovery.""" + + ip_address: str + name: str + uuid: str + model: str diff --git a/homeassistant/components/baf/strings.json b/homeassistant/components/baf/strings.json new file mode 100644 index 00000000000..a26e3152326 --- /dev/null +++ b/homeassistant/components/baf/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "flow_title": "{name} - {model} ({ip_address})", + "step": { + "user": { + "data": { + "ip_address": "[%key:common::config_flow::data::ip%]" + } + }, + "discovery_confirm": { + "description": "Do you want to setup {name} - {model} ({ip_address})?" + } + }, + "abort": { + "ipv6_not_supported": "IPv6 is not supported.", + "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/baf/translations/en.json b/homeassistant/components/baf/translations/en.json new file mode 100644 index 00000000000..4bb7256a692 --- /dev/null +++ b/homeassistant/components/baf/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "ipv6_not_supported": "IPv6 is not supported." + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "flow_title": "{name} - {model} ({ip_address})", + "step": { + "discovery_confirm": { + "description": "Do you want to setup {name} - {model} ({ip_address})?" + }, + "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 89a9e4e489a..77146689b8c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -42,6 +42,7 @@ FLOWS = { "axis", "azure_devops", "azure_event_hub", + "baf", "balboa", "blebox", "blink", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index d1a15356ddc..415e2746c6d 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -42,6 +42,20 @@ ZEROCONF = { "domain": "apple_tv" } ], + "_api._tcp.local.": [ + { + "domain": "baf", + "properties": { + "model": "haiku*" + } + }, + { + "domain": "baf", + "properties": { + "model": "i6*" + } + } + ], "_api._udp.local.": [ { "domain": "guardian" diff --git a/mypy.ini b/mypy.ini index 532c4526372..7784acd9fe6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -368,6 +368,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.baf.*] +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.binary_sensor.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index fe73d53bee5..e1bde3fb6ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -121,6 +121,9 @@ aioasuswrt==1.4.0 # homeassistant.components.azure_devops aioazuredevops==1.3.5 +# homeassistant.components.baf +aiobafi6==0.3.0 + # homeassistant.components.aws aiobotocore==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5bf3a4aa4eb..e2d7267149f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -108,6 +108,9 @@ aioasuswrt==1.4.0 # homeassistant.components.azure_devops aioazuredevops==1.3.5 +# homeassistant.components.baf +aiobafi6==0.3.0 + # homeassistant.components.aws aiobotocore==2.1.0 diff --git a/tests/components/baf/__init__.py b/tests/components/baf/__init__.py new file mode 100644 index 00000000000..e0432ed643a --- /dev/null +++ b/tests/components/baf/__init__.py @@ -0,0 +1 @@ +"""Tests for the Big Ass Fans integration.""" diff --git a/tests/components/baf/test_config_flow.py b/tests/components/baf/test_config_flow.py new file mode 100644 index 00000000000..687a871ed4c --- /dev/null +++ b/tests/components/baf/test_config_flow.py @@ -0,0 +1,158 @@ +"""Test the baf config flow.""" +import asyncio +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components import zeroconf +from homeassistant.components.baf.const import DOMAIN +from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + + +async def test_form_user(hass): + """Test we get the user form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch("homeassistant.components.baf.config_flow.Device.async_run",), patch( + "homeassistant.components.baf.config_flow.Device.async_wait_available", + ), patch( + "homeassistant.components.baf.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_IP_ADDRESS: "127.0.0.1"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "127.0.0.1" + assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.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} + ) + + with patch("homeassistant.components.baf.config_flow.Device.async_run",), patch( + "homeassistant.components.baf.config_flow.Device.async_wait_available", + side_effect=asyncio.TimeoutError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_IP_ADDRESS: "127.0.0.1"}, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {CONF_IP_ADDRESS: "cannot_connect"} + + +async def test_form_unknown_exception(hass): + """Test we handle unknown exceptions.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("homeassistant.components.baf.config_flow.Device.async_run",), patch( + "homeassistant.components.baf.config_flow.Device.async_wait_available", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_IP_ADDRESS: "127.0.0.1"}, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_zeroconf_discovery(hass): + """Test we can setup from zeroconf discovery.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="127.0.0.1", + addresses=["127.0.0.1"], + hostname="mock_hostname", + name="testfan", + port=None, + properties={"name": "My Fan", "model": "Haiku", "uuid": "1234"}, + type="mock_type", + ), + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.baf.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"] == "My Fan" + assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.1"} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_zeroconf_updates_existing_ip(hass): + """Test we can setup from zeroconf discovery.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_IP_ADDRESS: "127.0.0.2"}, unique_id="1234" + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="127.0.0.1", + addresses=["127.0.0.1"], + hostname="mock_hostname", + name="testfan", + port=None, + properties={"name": "My Fan", "model": "Haiku", "uuid": "1234"}, + type="mock_type", + ), + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_IP_ADDRESS] == "127.0.0.1" + + +async def test_zeroconf_rejects_ipv6(hass): + """Test zeroconf discovery rejects ipv6.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="fd00::b27c:63bb:cc85:4ea0", + addresses=["fd00::b27c:63bb:cc85:4ea0"], + hostname="mock_hostname", + name="testfan", + port=None, + properties={"name": "My Fan", "model": "Haiku", "uuid": "1234"}, + type="mock_type", + ), + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "ipv6_not_supported" From 10624e93c802dadb624e92d58faf617cbffa11ea Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 15 May 2022 00:30:16 +0000 Subject: [PATCH 0511/3516] [ci skip] Translation update --- .../aladdin_connect/translations/ca.json | 26 +++++++++++ .../aladdin_connect/translations/de.json | 27 +++++++++++ .../aladdin_connect/translations/el.json | 27 +++++++++++ .../aladdin_connect/translations/et.json | 27 +++++++++++ .../aladdin_connect/translations/fr.json | 27 +++++++++++ .../aladdin_connect/translations/hu.json | 27 +++++++++++ .../aladdin_connect/translations/ja.json | 27 +++++++++++ .../aladdin_connect/translations/nl.json | 27 +++++++++++ .../aladdin_connect/translations/pt-BR.json | 27 +++++++++++ .../aladdin_connect/translations/ru.json | 27 +++++++++++ .../aladdin_connect/translations/zh-Hans.json | 26 +++++++++++ .../aladdin_connect/translations/zh-Hant.json | 27 +++++++++++ .../components/apple_tv/translations/es.json | 2 +- .../components/baf/translations/nl.json | 23 ++++++++++ .../components/bosch_shc/translations/ja.json | 2 +- .../components/canary/translations/es.json | 2 +- .../components/generic/translations/ca.json | 4 ++ .../components/generic/translations/de.json | 4 ++ .../components/generic/translations/el.json | 4 ++ .../components/generic/translations/et.json | 4 ++ .../components/generic/translations/fr.json | 4 ++ .../components/generic/translations/hu.json | 4 ++ .../components/generic/translations/id.json | 4 ++ .../components/generic/translations/it.json | 4 ++ .../components/generic/translations/ja.json | 4 ++ .../components/generic/translations/nl.json | 4 ++ .../components/generic/translations/no.json | 4 ++ .../components/generic/translations/pl.json | 4 ++ .../generic/translations/pt-BR.json | 4 ++ .../components/generic/translations/ru.json | 4 ++ .../generic/translations/zh-Hans.json | 11 +++++ .../generic/translations/zh-Hant.json | 4 ++ .../geocaching/translations/ja.json | 25 +++++++++++ .../geocaching/translations/nl.json | 25 +++++++++++ .../geocaching/translations/no.json | 25 +++++++++++ .../geocaching/translations/pl.json | 25 +++++++++++ .../geocaching/translations/ru.json | 25 +++++++++++ .../geocaching/translations/zh-Hans.json | 21 +++++++++ .../components/google/translations/ja.json | 2 +- .../components/isy994/translations/ja.json | 1 + .../litterrobot/translations/sensor.ca.json | 9 ++++ .../litterrobot/translations/sensor.de.json | 28 ++++++++++++ .../litterrobot/translations/sensor.el.json | 28 ++++++++++++ .../litterrobot/translations/sensor.et.json | 28 ++++++++++++ .../litterrobot/translations/sensor.fr.json | 28 ++++++++++++ .../litterrobot/translations/sensor.hu.json | 28 ++++++++++++ .../litterrobot/translations/sensor.ja.json | 28 ++++++++++++ .../litterrobot/translations/sensor.nl.json | 25 +++++++++++ .../translations/sensor.pt-BR.json | 28 ++++++++++++ .../litterrobot/translations/sensor.ru.json | 28 ++++++++++++ .../translations/sensor.zh-Hans.json | 10 +++++ .../translations/sensor.zh-Hant.json | 28 ++++++++++++ .../components/meater/translations/ja.json | 6 ++- .../min_max/translations/zh-Hans.json | 4 +- .../motion_blinds/translations/ca.json | 2 +- .../motion_blinds/translations/de.json | 2 +- .../motion_blinds/translations/et.json | 2 +- .../motion_blinds/translations/fr.json | 2 +- .../motion_blinds/translations/id.json | 2 +- .../motion_blinds/translations/it.json | 2 +- .../motion_blinds/translations/nl.json | 2 +- .../motion_blinds/translations/pt-BR.json | 2 +- .../motion_blinds/translations/ru.json | 2 +- .../motion_blinds/translations/zh-Hant.json | 2 +- .../components/nest/translations/ja.json | 2 +- .../components/netatmo/translations/ja.json | 2 +- .../components/onewire/translations/es.json | 1 + .../simplisafe/translations/ja.json | 8 +++- .../components/slack/translations/ca.json | 22 +++++++++ .../components/slack/translations/de.json | 29 ++++++++++++ .../components/slack/translations/el.json | 29 ++++++++++++ .../components/slack/translations/en.json | 4 +- .../components/slack/translations/et.json | 29 ++++++++++++ .../components/slack/translations/fr.json | 29 ++++++++++++ .../components/slack/translations/hu.json | 29 ++++++++++++ .../components/slack/translations/ja.json | 29 ++++++++++++ .../components/slack/translations/nl.json | 29 ++++++++++++ .../components/slack/translations/pt-BR.json | 29 ++++++++++++ .../components/slack/translations/ru.json | 29 ++++++++++++ .../slack/translations/zh-Hans.json | 29 ++++++++++++ .../slack/translations/zh-Hant.json | 29 ++++++++++++ .../components/smarttub/translations/ja.json | 2 +- .../steam_online/translations/ja.json | 11 +++++ .../components/tautulli/translations/ja.json | 4 +- .../totalconnect/translations/ja.json | 2 +- .../trafikverket_ferry/translations/ja.json | 3 +- .../ukraine_alarm/translations/ca.json | 12 +++-- .../ukraine_alarm/translations/de.json | 12 +++-- .../ukraine_alarm/translations/el.json | 10 ++++- .../ukraine_alarm/translations/es.json | 11 +++++ .../ukraine_alarm/translations/et.json | 12 +++-- .../ukraine_alarm/translations/fr.json | 12 +++-- .../ukraine_alarm/translations/hu.json | 10 ++++- .../ukraine_alarm/translations/id.json | 12 +++-- .../ukraine_alarm/translations/it.json | 12 +++-- .../ukraine_alarm/translations/ja.json | 10 ++++- .../ukraine_alarm/translations/nl.json | 12 +++-- .../ukraine_alarm/translations/no.json | 12 +++-- .../ukraine_alarm/translations/pl.json | 12 +++-- .../ukraine_alarm/translations/pt-BR.json | 12 +++-- .../ukraine_alarm/translations/ru.json | 24 ++++++---- .../ukraine_alarm/translations/uk.json | 45 +++++++++++++++++++ .../ukraine_alarm/translations/zh-Hans.json | 18 ++++++++ .../ukraine_alarm/translations/zh-Hant.json | 12 +++-- .../components/vulcan/translations/ja.json | 4 +- 105 files changed, 1466 insertions(+), 75 deletions(-) create mode 100644 homeassistant/components/aladdin_connect/translations/ca.json create mode 100644 homeassistant/components/aladdin_connect/translations/de.json create mode 100644 homeassistant/components/aladdin_connect/translations/el.json create mode 100644 homeassistant/components/aladdin_connect/translations/et.json create mode 100644 homeassistant/components/aladdin_connect/translations/fr.json create mode 100644 homeassistant/components/aladdin_connect/translations/hu.json create mode 100644 homeassistant/components/aladdin_connect/translations/ja.json create mode 100644 homeassistant/components/aladdin_connect/translations/nl.json create mode 100644 homeassistant/components/aladdin_connect/translations/pt-BR.json create mode 100644 homeassistant/components/aladdin_connect/translations/ru.json create mode 100644 homeassistant/components/aladdin_connect/translations/zh-Hans.json create mode 100644 homeassistant/components/aladdin_connect/translations/zh-Hant.json create mode 100644 homeassistant/components/baf/translations/nl.json create mode 100644 homeassistant/components/generic/translations/zh-Hans.json create mode 100644 homeassistant/components/geocaching/translations/ja.json create mode 100644 homeassistant/components/geocaching/translations/nl.json create mode 100644 homeassistant/components/geocaching/translations/no.json create mode 100644 homeassistant/components/geocaching/translations/pl.json create mode 100644 homeassistant/components/geocaching/translations/ru.json create mode 100644 homeassistant/components/geocaching/translations/zh-Hans.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.ca.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.de.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.el.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.et.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.fr.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.hu.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.ja.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.nl.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.pt-BR.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.ru.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.zh-Hans.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.zh-Hant.json create mode 100644 homeassistant/components/slack/translations/ca.json create mode 100644 homeassistant/components/slack/translations/de.json create mode 100644 homeassistant/components/slack/translations/el.json create mode 100644 homeassistant/components/slack/translations/et.json create mode 100644 homeassistant/components/slack/translations/fr.json create mode 100644 homeassistant/components/slack/translations/hu.json create mode 100644 homeassistant/components/slack/translations/ja.json create mode 100644 homeassistant/components/slack/translations/nl.json create mode 100644 homeassistant/components/slack/translations/pt-BR.json create mode 100644 homeassistant/components/slack/translations/ru.json create mode 100644 homeassistant/components/slack/translations/zh-Hans.json create mode 100644 homeassistant/components/slack/translations/zh-Hant.json create mode 100644 homeassistant/components/ukraine_alarm/translations/es.json create mode 100644 homeassistant/components/ukraine_alarm/translations/uk.json create mode 100644 homeassistant/components/ukraine_alarm/translations/zh-Hans.json diff --git a/homeassistant/components/aladdin_connect/translations/ca.json b/homeassistant/components/aladdin_connect/translations/ca.json new file mode 100644 index 00000000000..c7115a32099 --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/ca.json @@ -0,0 +1,26 @@ +{ + "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" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/de.json b/homeassistant/components/aladdin_connect/translations/de.json new file mode 100644 index 00000000000..057f31e9078 --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/de.json @@ -0,0 +1,27 @@ +{ + "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" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Die Aladdin Connect-Integration muss dein Konto erneut authentifizieren", + "title": "Integration erneut authentifizieren" + }, + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/el.json b/homeassistant/components/aladdin_connect/translations/el.json new file mode 100644 index 00000000000..6fa2af154f7 --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/el.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "reauth_successful": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Aladdin Connect \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c4\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03c3\u03b1\u03c2", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, + "user": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/et.json b/homeassistant/components/aladdin_connect/translations/et.json new file mode 100644 index 00000000000..4eb34a1d8bf --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/et.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Aladdin Connecti sidumine peab konto uuesti autentima", + "title": "Taastuvasta sidumine" + }, + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/fr.json b/homeassistant/components/aladdin_connect/translations/fr.json new file mode 100644 index 00000000000..5586c751335 --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "description": "L'int\u00e9gration Aladdin Connect doit r\u00e9-authentifier votre compte", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/hu.json b/homeassistant/components/aladdin_connect/translations/hu.json new file mode 100644 index 00000000000..8d0f979e05a --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/hu.json @@ -0,0 +1,27 @@ +{ + "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" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "The Aladdin Connect integration needs to re-authenticate your account", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/ja.json b/homeassistant/components/aladdin_connect/translations/ja.json new file mode 100644 index 00000000000..196b7f1d066 --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/ja.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "Aladdin Connect\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + }, + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/nl.json b/homeassistant/components/aladdin_connect/translations/nl.json new file mode 100644 index 00000000000..314df5712f8 --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/nl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "description": "De Aladdin Connect-integratie moet uw account opnieuw verifi\u00ebren", + "title": "Verifieer de integratie opnieuw" + }, + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/pt-BR.json b/homeassistant/components/aladdin_connect/translations/pt-BR.json new file mode 100644 index 00000000000..2d709bf1125 --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/pt-BR.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "A integra\u00e7\u00e3o do Aladdin Connect precisa autenticar novamente sua conta", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "password": "Senha", + "username": "Nome de usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/ru.json b/homeassistant/components/aladdin_connect/translations/ru.json new file mode 100644 index 00000000000..d2db9c388ce --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/ru.json @@ -0,0 +1,27 @@ +{ + "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." + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Aladdin Connect.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "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/aladdin_connect/translations/zh-Hans.json b/homeassistant/components/aladdin_connect/translations/zh-Hans.json new file mode 100644 index 00000000000..d7088d39951 --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/zh-Hans.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86", + "reauth_successful": "\u91cd\u65b0\u8ba4\u8bc1\u6210\u529f" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u7801" + }, + "title": "\u4f7f\u96c6\u6210\u91cd\u65b0\u8fdb\u884c\u8eab\u4efd\u8ba4\u8bc1" + }, + "user": { + "data": { + "password": "\u5bc6\u7801", + "username": "\u7528\u6237\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/zh-Hant.json b/homeassistant/components/aladdin_connect/translations/zh-Hant.json new file mode 100644 index 00000000000..1dcca5b6f9e --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "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" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "Aladdin \u9023\u7dda\u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/es.json b/homeassistant/components/apple_tv/translations/es.json index f72b7ec4cff..65f096ca6ae 100644 --- a/homeassistant/components/apple_tv/translations/es.json +++ b/homeassistant/components/apple_tv/translations/es.json @@ -24,7 +24,7 @@ "flow_title": "Apple TV: {name}", "step": { "confirm": { - "description": "Est\u00e1s a punto de a\u00f1adir el Apple TV con nombre `{name}` a Home Assistant.\n\n**Para completar el proceso, puede que tengas que introducir varios c\u00f3digos PIN.**\n\nTen en cuenta que *no* podr\u00e1s apagar tu Apple TV con esta integraci\u00f3n. \u00a1S\u00f3lo se apagar\u00e1 el reproductor de medios de Home Assistant!", + "description": "Est\u00e1s a punto de a\u00f1adir `{name}` con el tipo `{type}` en Home Assistant.\n\n**Para completar el proceso, puede que tengas que introducir varios c\u00f3digos PIN.**\n\nTen en cuenta que *no* podr\u00e1s apagar tu Apple TV con esta integraci\u00f3n. \u00a1S\u00f3lo se apagar\u00e1 el reproductor de medios de Home Assistant!", "title": "Confirma la adici\u00f3n del Apple TV" }, "pair_no_pin": { diff --git a/homeassistant/components/baf/translations/nl.json b/homeassistant/components/baf/translations/nl.json new file mode 100644 index 00000000000..9b619390165 --- /dev/null +++ b/homeassistant/components/baf/translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "ipv6_not_supported": "IPv6 wordt niet ondersteund" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "flow_title": "{name} - {model} ({ip_address})", + "step": { + "discovery_confirm": { + "description": "Wilt u {name} - {model} ({ip_address}) instellen?" + }, + "user": { + "data": { + "ip_address": "IP-adres" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/ja.json b/homeassistant/components/bosch_shc/translations/ja.json index 5146f81301c..0a624bd2eb0 100644 --- a/homeassistant/components/bosch_shc/translations/ja.json +++ b/homeassistant/components/bosch_shc/translations/ja.json @@ -22,7 +22,7 @@ } }, "reauth_confirm": { - "description": "bosch_shc\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "description": "Bosch_shc\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, "user": { diff --git a/homeassistant/components/canary/translations/es.json b/homeassistant/components/canary/translations/es.json index 018a1f30b08..76a439da49d 100644 --- a/homeassistant/components/canary/translations/es.json +++ b/homeassistant/components/canary/translations/es.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "No se pudo conectar" }, - "flow_title": "Canary: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/generic/translations/ca.json b/homeassistant/components/generic/translations/ca.json index 3c2f5055ba5..7d69e837130 100644 --- a/homeassistant/components/generic/translations/ca.json +++ b/homeassistant/components/generic/translations/ca.json @@ -79,8 +79,12 @@ "rtsp_transport": "Protocol de transport RTSP", "still_image_url": "URL d'imatge fixa (p. ex. http://...)", "stream_source": "URL origen del flux (p. ex. rtsp://...)", + "use_wallclock_as_timestamps": "Utilitza el rellotge de paret com a marca de temps", "username": "Nom d'usuari", "verify_ssl": "Verifica el certificat SSL" + }, + "data_description": { + "use_wallclock_as_timestamps": "Aquesta opci\u00f3 pot corregir problemes de segmentaci\u00f3 o bloqueig en algunes c\u00e0meres derivats d'implementacions de marca de temps amb errors" } } } diff --git a/homeassistant/components/generic/translations/de.json b/homeassistant/components/generic/translations/de.json index 849555dcc5e..8d40216001a 100644 --- a/homeassistant/components/generic/translations/de.json +++ b/homeassistant/components/generic/translations/de.json @@ -79,8 +79,12 @@ "rtsp_transport": "RTSP-Transportprotokoll", "still_image_url": "Standbild-URL (z.B. http://...)", "stream_source": "Stream-Quell-URL (z.B. rtsp://...)", + "use_wallclock_as_timestamps": "Wanduhr als Zeitstempel verwenden", "username": "Benutzername", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + }, + "data_description": { + "use_wallclock_as_timestamps": "Diese Option kann Segmentierungs- oder Absturzprobleme beheben, die durch fehlerhafte Zeitstempel-Implementierungen auf einigen Kameras entstehen" } } } diff --git a/homeassistant/components/generic/translations/el.json b/homeassistant/components/generic/translations/el.json index f3fae37f5ca..8b15c7de851 100644 --- a/homeassistant/components/generic/translations/el.json +++ b/homeassistant/components/generic/translations/el.json @@ -79,8 +79,12 @@ "rtsp_transport": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP", "still_image_url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 (\u03c0.\u03c7. http://...)", "stream_source": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c0\u03b7\u03b3\u03ae\u03c2 \u03c1\u03bf\u03ae\u03c2 (\u03c0.\u03c7. rtsp://...)", + "use_wallclock_as_timestamps": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03c1\u03bf\u03bb\u03bf\u03b3\u03b9\u03bf\u03cd \u03c4\u03bf\u03af\u03c7\u03bf\u03c5 \u03c9\u03c2 \u03c7\u03c1\u03bf\u03bd\u03bf\u03c3\u03c6\u03c1\u03b1\u03b3\u03af\u03b4\u03b5\u03c2", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" + }, + "data_description": { + "use_wallclock_as_timestamps": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03b9 \u03b6\u03b7\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1 \u03c4\u03bc\u03b7\u03bc\u03b1\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03ae \u03c3\u03c6\u03b1\u03bb\u03bc\u03ac\u03c4\u03c9\u03bd \u03c0\u03bf\u03c5 \u03c0\u03c1\u03bf\u03ba\u03cd\u03c0\u03c4\u03bf\u03c5\u03bd \u03b1\u03c0\u03cc \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03c2 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03ce\u03bd \u03c3\u03c6\u03c1\u03b1\u03b3\u03af\u03b4\u03c9\u03bd \u03bc\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1\u03c4\u03b1 \u03c3\u03b5 \u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b5\u03c2" } } } diff --git a/homeassistant/components/generic/translations/et.json b/homeassistant/components/generic/translations/et.json index ae6f5aa75f8..0a4bdc0731f 100644 --- a/homeassistant/components/generic/translations/et.json +++ b/homeassistant/components/generic/translations/et.json @@ -79,8 +79,12 @@ "rtsp_transport": "RTSP transpordiprotokoll", "still_image_url": "Pildi URL (nt http://...)", "stream_source": "Voo allikas URL (nt rtsp://...)", + "use_wallclock_as_timestamps": "Wallclocki kasutamine ajatemplitena", "username": "Kasutajanimi", "verify_ssl": "Kontrolli SSL sertifikaati" + }, + "data_description": { + "use_wallclock_as_timestamps": "See suvand v\u00f5ib parandada segmentimis- v\u00f5i krahhiprobleeme, mis tulenevad m\u00f5ne kaamera vigastest ajatemplirakendustest" } } } diff --git a/homeassistant/components/generic/translations/fr.json b/homeassistant/components/generic/translations/fr.json index 4905afb64e7..454f9b0de03 100644 --- a/homeassistant/components/generic/translations/fr.json +++ b/homeassistant/components/generic/translations/fr.json @@ -79,8 +79,12 @@ "rtsp_transport": "Protocole de transport RTSP", "still_image_url": "URL d'image fixe (par exemple, http://\u2026)", "stream_source": "URL de la source du flux (par exemple, rtsp://\u2026)", + "use_wallclock_as_timestamps": "Utiliser l'horloge murale comme horodatage", "username": "Nom d'utilisateur", "verify_ssl": "V\u00e9rifier le certificat SSL" + }, + "data_description": { + "use_wallclock_as_timestamps": "Cette option peut corriger des probl\u00e8mes de segmentation ou de plantage dus \u00e0 des impl\u00e9mentations d'horodatage d\u00e9fectueuses sur certaines cam\u00e9ras" } } } diff --git a/homeassistant/components/generic/translations/hu.json b/homeassistant/components/generic/translations/hu.json index 4e97e594d1e..bc8172caaf9 100644 --- a/homeassistant/components/generic/translations/hu.json +++ b/homeassistant/components/generic/translations/hu.json @@ -79,8 +79,12 @@ "rtsp_transport": "RTSP protokoll", "still_image_url": "\u00c1ll\u00f3k\u00e9p URL (pl. http://...)", "stream_source": "Mozg\u00f3k\u00e9p adatfolyam URL (pl. rtsp://...)", + "use_wallclock_as_timestamps": "Wallclock haszn\u00e1lata id\u0151b\u00e9lyegz\u0151k\u00e9nt", "username": "Felhaszn\u00e1l\u00f3n\u00e9v", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + }, + "data_description": { + "use_wallclock_as_timestamps": "Ez az opci\u00f3 korrig\u00e1lhatja az egyes kamer\u00e1k hib\u00e1s id\u0151b\u00e9lyegz\u0151 implement\u00e1ci\u00f3j\u00e1b\u00f3l ered\u0151 szegment\u00e1l\u00e1si vagy \u00f6sszeoml\u00e1si probl\u00e9m\u00e1kat." } } } diff --git a/homeassistant/components/generic/translations/id.json b/homeassistant/components/generic/translations/id.json index 4b3106ad1df..7c4be981e4a 100644 --- a/homeassistant/components/generic/translations/id.json +++ b/homeassistant/components/generic/translations/id.json @@ -79,8 +79,12 @@ "rtsp_transport": "Protokol transportasi RTSP", "still_image_url": "URL Gambar Diam (mis. http://...)", "stream_source": "URL Sumber Streaming (mis. rtsp://...)", + "use_wallclock_as_timestamps": "Gunakan jam dinding sebagai stempel waktu", "username": "Nama Pengguna", "verify_ssl": "Verifikasi sertifikat SSL" + }, + "data_description": { + "use_wallclock_as_timestamps": "Opsi ini dapat memperbaiki masalah segmentasi atau mogok yang timbul dari implementasi stempel waktu bermasalah pada beberapa kamera" } } } diff --git a/homeassistant/components/generic/translations/it.json b/homeassistant/components/generic/translations/it.json index 880fcff500b..e5e41e50285 100644 --- a/homeassistant/components/generic/translations/it.json +++ b/homeassistant/components/generic/translations/it.json @@ -79,8 +79,12 @@ "rtsp_transport": "Protocollo di trasporto RTSP", "still_image_url": "URL immagine fissa (ad es. http://...)", "stream_source": "URL sorgente del flusso (ad es. rtsp://...)", + "use_wallclock_as_timestamps": "Usa wallclock come marca temporale", "username": "Nome utente", "verify_ssl": "Verifica il certificato SSL" + }, + "data_description": { + "use_wallclock_as_timestamps": "Questa opzione pu\u00f2 correggere problemi di segmentazione o di arresto anomalo derivanti da implementazioni difettose di marche temporali su alcune telecamere" } } } diff --git a/homeassistant/components/generic/translations/ja.json b/homeassistant/components/generic/translations/ja.json index 916142125e6..97c1a76ab36 100644 --- a/homeassistant/components/generic/translations/ja.json +++ b/homeassistant/components/generic/translations/ja.json @@ -79,8 +79,12 @@ "rtsp_transport": "RTSP\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30d7\u30ed\u30c8\u30b3\u30eb", "still_image_url": "\u9759\u6b62\u753b\u50cf\u306eURL(\u4f8b: http://...)", "stream_source": "\u30b9\u30c8\u30ea\u30fc\u30e0\u30bd\u30fc\u30b9\u306eURL(\u4f8b: rtsp://...)", + "use_wallclock_as_timestamps": "\u30bf\u30a4\u30e0\u30b9\u30bf\u30f3\u30d7\u306bwallclock\u3092\u4f7f\u7528", "username": "\u30e6\u30fc\u30b6\u30fc\u540d", "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" + }, + "data_description": { + "use_wallclock_as_timestamps": "\u3053\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u306f\u3001\u4e00\u90e8\u306e\u30ab\u30e1\u30e9\u306e\u30bf\u30a4\u30e0\u30b9\u30bf\u30f3\u30d7\u5b9f\u88c5\u306e\u30d0\u30b0\u304b\u3089\u767a\u751f\u3059\u308b\u30bb\u30b0\u30e1\u30f3\u30c6\u30fc\u30b7\u30e7\u30f3\u3084\u30af\u30e9\u30c3\u30b7\u30e5\u306e\u554f\u984c\u3092\u4fee\u6b63\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059" } } } diff --git a/homeassistant/components/generic/translations/nl.json b/homeassistant/components/generic/translations/nl.json index 167374c7aeb..5c45b783c4f 100644 --- a/homeassistant/components/generic/translations/nl.json +++ b/homeassistant/components/generic/translations/nl.json @@ -79,8 +79,12 @@ "rtsp_transport": "RTSP-transportprotocol", "still_image_url": "URL van stilstaande afbeelding (bijv. http://...)", "stream_source": "Url van streambron (bijv. rtsp://...)", + "use_wallclock_as_timestamps": "Gebruik de wandklok als tijdstempel", "username": "Gebruikersnaam", "verify_ssl": "SSL-certificaat verifi\u00ebren" + }, + "data_description": { + "use_wallclock_as_timestamps": "Deze optie kan problemen met segmenteren of crashen verhelpen die het gevolg zijn van buggy timestamp implementaties op sommige camera's" } } } diff --git a/homeassistant/components/generic/translations/no.json b/homeassistant/components/generic/translations/no.json index 1f9eedaced4..78b5618b073 100644 --- a/homeassistant/components/generic/translations/no.json +++ b/homeassistant/components/generic/translations/no.json @@ -79,8 +79,12 @@ "rtsp_transport": "RTSP transportprotokoll", "still_image_url": "Stillbilde-URL (f.eks. http://...)", "stream_source": "Str\u00f8mkilde-URL (f.eks. rtsp://...)", + "use_wallclock_as_timestamps": "Bruk veggklokke som tidsstempler", "username": "Brukernavn", "verify_ssl": "Verifisere SSL-sertifikat" + }, + "data_description": { + "use_wallclock_as_timestamps": "Dette alternativet kan korrigere segmenterings- eller krasjproblemer som oppst\u00e5r fra buggy-tidsstempelimplementeringer p\u00e5 enkelte kameraer" } } } diff --git a/homeassistant/components/generic/translations/pl.json b/homeassistant/components/generic/translations/pl.json index a791a56c065..16a912587e5 100644 --- a/homeassistant/components/generic/translations/pl.json +++ b/homeassistant/components/generic/translations/pl.json @@ -79,8 +79,12 @@ "rtsp_transport": "Protok\u00f3\u0142 transportowy RTSP", "still_image_url": "Adres URL obrazu nieruchomego (np. http://...)", "stream_source": "Adres URL strumienia (np. rtsp://...)", + "use_wallclock_as_timestamps": "U\u017cyj wallclock jako znacznika czasu", "username": "Nazwa u\u017cytkownika", "verify_ssl": "Weryfikacja certyfikatu SSL" + }, + "data_description": { + "use_wallclock_as_timestamps": "Ta opcja mo\u017ce rozwi\u0105za\u0107 problemy z segmentacj\u0105 lub awariami wynikaj\u0105ce z wadliwych implementacji znacznik\u00f3w czasu w niekt\u00f3rych kamerach." } } } diff --git a/homeassistant/components/generic/translations/pt-BR.json b/homeassistant/components/generic/translations/pt-BR.json index 0e3ce2bd75d..ec093f3ff1a 100644 --- a/homeassistant/components/generic/translations/pt-BR.json +++ b/homeassistant/components/generic/translations/pt-BR.json @@ -79,8 +79,12 @@ "rtsp_transport": "Protocolo RTSP", "still_image_url": "URL da imagem est\u00e1tica (por exemplo, http://...)", "stream_source": "URL de origem da Stream (por exemplo, rtsp://...)", + "use_wallclock_as_timestamps": "Use o rel\u00f3gio de parede como data/hora", "username": "Usu\u00e1rio", "verify_ssl": "Verifique o certificado SSL" + }, + "data_description": { + "use_wallclock_as_timestamps": "Esta op\u00e7\u00e3o pode corrigir problemas de segmenta\u00e7\u00e3o ou travamento decorrentes de implementa\u00e7\u00f5es de data/hora com erros em algumas c\u00e2meras" } } } diff --git a/homeassistant/components/generic/translations/ru.json b/homeassistant/components/generic/translations/ru.json index 06f41613a50..e88f9f9cd1e 100644 --- a/homeassistant/components/generic/translations/ru.json +++ b/homeassistant/components/generic/translations/ru.json @@ -79,8 +79,12 @@ "rtsp_transport": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP", "still_image_url": "URL-\u0430\u0434\u0440\u0435\u0441 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, http://...)", "stream_source": "URL-\u0430\u0434\u0440\u0435\u0441 \u043f\u043e\u0442\u043e\u043a\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, rtsp://...)", + "use_wallclock_as_timestamps": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0435\u043d\u043d\u044b\u0435 \u0447\u0430\u0441\u044b \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445 \u043c\u0435\u0442\u043e\u043a", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "data_description": { + "use_wallclock_as_timestamps": "\u042d\u0442\u043e\u0442 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0441 \u0441\u0435\u0433\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439 \u0438\u043b\u0438 \u0441\u0431\u043e\u0435\u043c, \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u044e\u0449\u0438\u0435 \u0438\u0437-\u0437\u0430 \u043e\u0448\u0438\u0431\u043e\u0447\u043d\u043e\u0439 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445 \u043c\u0435\u0442\u043e\u043a \u043d\u0430 \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u043a\u0430\u043c\u0435\u0440\u0430\u0445." } } } diff --git a/homeassistant/components/generic/translations/zh-Hans.json b/homeassistant/components/generic/translations/zh-Hans.json new file mode 100644 index 00000000000..f13ba39c5b8 --- /dev/null +++ b/homeassistant/components/generic/translations/zh-Hans.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data_description": { + "use_wallclock_as_timestamps": "\u6b64\u9009\u9879\u53ef\u80fd\u4f1a\u7ea0\u6b63\u7531\u4e8e\u67d0\u4e9b\u6444\u50cf\u5934\u4e0a\u7684\u9519\u8bef\u65f6\u95f4\u6233\u5f15\u8d77\u7684\u5206\u6bb5\u6216\u5d29\u6e83\u95ee\u9898" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/zh-Hant.json b/homeassistant/components/generic/translations/zh-Hant.json index 1b72dcb0ce4..e37ee2d9235 100644 --- a/homeassistant/components/generic/translations/zh-Hant.json +++ b/homeassistant/components/generic/translations/zh-Hant.json @@ -79,8 +79,12 @@ "rtsp_transport": "RTSP \u50b3\u8f38\u5354\u5b9a", "still_image_url": "\u975c\u614b\u5f71\u50cf URL\uff08\u4f8b\u5982 http://...\uff09", "stream_source": "\u4e32\u6d41\u4f86\u6e90 URL\uff08\u4f8b\u5982 rtsp://...\uff09", + "use_wallclock_as_timestamps": "\u4f7f\u7528\u639b\u9418\u4f5c\u70ba\u6642\u9593\u6233", "username": "\u4f7f\u7528\u8005\u540d\u7a31", "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + }, + "data_description": { + "use_wallclock_as_timestamps": "\u6b64\u9078\u9805\u53ef\u80fd\u4fee\u6b63\u67d0\u4fee\u93e1\u982d\u4e0a\u932f\u8aa4\u7684\u6642\u9593\u6233\u5c0e\u81f4\u7684\u5206\u6bb5\u6216\u7576\u6a5f\u554f\u984c" } } } diff --git a/homeassistant/components/geocaching/translations/ja.json b/homeassistant/components/geocaching/translations/ja.json new file mode 100644 index 00000000000..41a238aa637 --- /dev/null +++ b/homeassistant/components/geocaching/translations/ja.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", + "oauth_error": "\u7121\u52b9\u306a\u30c8\u30fc\u30af\u30f3\u30c7\u30fc\u30bf\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f\u3002", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "create_entry": { + "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" + }, + "step": { + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + }, + "reauth_confirm": { + "description": "Geocaching\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geocaching/translations/nl.json b/homeassistant/components/geocaching/translations/nl.json new file mode 100644 index 00000000000..82d40849146 --- /dev/null +++ b/homeassistant/components/geocaching/translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", + "oauth_error": "Ongeldige token data ontvangen.", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "create_entry": { + "default": "Succesvol geauthenticeerd" + }, + "step": { + "pick_implementation": { + "title": "Kies een authenticatie methode" + }, + "reauth_confirm": { + "description": "De Geocaching integratie moet uw account herauthenticeren", + "title": "Verifieer de integratie opnieuw" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geocaching/translations/no.json b/homeassistant/components/geocaching/translations/no.json new file mode 100644 index 00000000000..f0b0b724861 --- /dev/null +++ b/homeassistant/components/geocaching/translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", + "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", + "oauth_error": "Mottatt ugyldige token data.", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "create_entry": { + "default": "Vellykket godkjenning" + }, + "step": { + "pick_implementation": { + "title": "Velg godkjenningsmetode" + }, + "reauth_confirm": { + "description": "Geocaching-integrasjonen m\u00e5 autentisere kontoen din p\u00e5 nytt", + "title": "Godkjenne integrering p\u00e5 nytt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geocaching/translations/pl.json b/homeassistant/components/geocaching/translations/pl.json new file mode 100644 index 00000000000..f38f7e137f6 --- /dev/null +++ b/homeassistant/components/geocaching/translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", + "oauth_error": "Otrzymano nieprawid\u0142owe dane tokena.", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono" + }, + "step": { + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + }, + "reauth_confirm": { + "description": "Integracja Geocaching wymaga ponownego uwierzytelnienia Twojego konta", + "title": "Ponownie uwierzytelnij integracj\u0119" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geocaching/translations/ru.json b/homeassistant/components/geocaching/translations/ru.json new file mode 100644 index 00000000000..96bc2b576a1 --- /dev/null +++ b/homeassistant/components/geocaching/translations/ru.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "already_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.", + "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", + "oauth_error": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u044b \u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0442\u043e\u043a\u0435\u043d\u0430.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "create_entry": { + "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "reauth_confirm": { + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Geocaching.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geocaching/translations/zh-Hans.json b/homeassistant/components/geocaching/translations/zh-Hans.json new file mode 100644 index 00000000000..21e284119ed --- /dev/null +++ b/homeassistant/components/geocaching/translations/zh-Hans.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "no_url_available": "\u6ca1\u6709\u53ef\u7528\u7684\u7f51\u5740\u3002\u6709\u5173\u6b64\u9519\u8bef\u7684\u4fe1\u606f\uff0c\u8bf7[\u68c0\u67e5\u5e2e\u52a9\u90e8\u5206]\uff08{docs_url}\uff09", + "oauth_error": "\u6536\u5230\u7684 token \u6570\u636e\u65e0\u6548\u3002", + "reauth_successful": "\u91cd\u65b0\u8ba4\u8bc1\u6210\u529f" + }, + "create_entry": { + "default": "\u8ba4\u8bc1\u6210\u529f" + }, + "step": { + "pick_implementation": { + "title": "\u8bf7\u9009\u62e9\u8ba4\u8bc1\u65b9\u6cd5" + }, + "reauth_confirm": { + "description": "Geocaching \u96c6\u6210\u9700\u8981\u91cd\u65b0\u9a8c\u8bc1\u60a8\u7684\u5e10\u6237", + "title": "\u4f7f\u96c6\u6210\u91cd\u65b0\u8fdb\u884c\u8eab\u4efd\u8ba4\u8bc1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/ja.json b/homeassistant/components/google/translations/ja.json index 7eab5abc6f6..81f2d941a73 100644 --- a/homeassistant/components/google/translations/ja.json +++ b/homeassistant/components/google/translations/ja.json @@ -23,7 +23,7 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "Nest\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "description": "Nest\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" } } diff --git a/homeassistant/components/isy994/translations/ja.json b/homeassistant/components/isy994/translations/ja.json index 0245baf01a0..f1a447901b9 100644 --- a/homeassistant/components/isy994/translations/ja.json +++ b/homeassistant/components/isy994/translations/ja.json @@ -17,6 +17,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, + "description": "{host} \u306e\u8cc7\u683c\u60c5\u5831\u304c\u7121\u52b9\u306b\u306a\u308a\u307e\u3057\u305f\u3002", "title": "ISY\u306e\u518d\u8a8d\u8a3c" }, "user": { diff --git a/homeassistant/components/litterrobot/translations/sensor.ca.json b/homeassistant/components/litterrobot/translations/sensor.ca.json new file mode 100644 index 00000000000..72a6906f03a --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.ca.json @@ -0,0 +1,9 @@ +{ + "state": { + "litterrobot__status_code": { + "off": "OFF", + "offline": "Fora de l\u00ednia", + "p": "Pausat/ada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.de.json b/homeassistant/components/litterrobot/translations/sensor.de.json new file mode 100644 index 00000000000..2901b0e5c55 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.de.json @@ -0,0 +1,28 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "Haube entfernt", + "ccc": "Reinigungszyklus abgeschlossen", + "ccp": "Reinigungszyklus l\u00e4uft", + "csf": "Katzensensor Fehler", + "csi": "Katzensensor unterbrochen", + "cst": "Katzensensor Timing", + "df1": "Schublade fast voll - 2 Zyklen \u00fcbrig", + "df2": "Schublade fast voll - 1 Zyklus \u00fcbrig", + "dfs": "Schublade voll", + "dhf": "Fehler in Leerungs- und Grundstellung", + "dpf": "Fehler in der Leerungsstellung", + "ec": "Leerungszyklus", + "hpf": "Fehler in der Grundstellung", + "off": "Aus", + "offline": "Offline", + "otf": "\u00dcberdrehungsfehler", + "p": "Pausiert", + "pd": "Einklemmen erkennen", + "rdy": "Bereit", + "scf": "Katzen-Sensorfehler beim Start", + "sdf": "Schublade voll beim Start", + "spf": "Einklemmerkennung beim Start" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.el.json b/homeassistant/components/litterrobot/translations/sensor.el.json new file mode 100644 index 00000000000..f34d6078495 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.el.json @@ -0,0 +1,28 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "\u03a4\u03bf \u03ba\u03b1\u03c0\u03cc \u03b1\u03c6\u03b1\u03b9\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5", + "ccc": "\u039f\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03bf \u03ba\u03cd\u03ba\u03bb\u03bf\u03c2 \u03ba\u03b1\u03b8\u03b1\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd", + "ccp": "\u039a\u03cd\u03ba\u03bb\u03bf\u03c2 \u03ba\u03b1\u03b8\u03b1\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "csf": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b3\u03ac\u03c4\u03b1\u03c2", + "csi": "\u0394\u03b9\u03b1\u03ba\u03bf\u03c0\u03ae \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b3\u03ac\u03c4\u03b1\u03c2", + "cst": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b3\u03ac\u03c4\u03b1\u03c2", + "df1": "\u03a3\u03c5\u03c1\u03c4\u03ac\u03c1\u03b9 \u03c3\u03c7\u03b5\u03b4\u03cc\u03bd \u03b3\u03b5\u03bc\u03ac\u03c4\u03bf - \u0391\u03c0\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5\u03bd 2 \u03ba\u03cd\u03ba\u03bb\u03bf\u03b9", + "df2": "\u03a3\u03c5\u03c1\u03c4\u03ac\u03c1\u03b9 \u03c3\u03c7\u03b5\u03b4\u03cc\u03bd \u03b3\u03b5\u03bc\u03ac\u03c4\u03bf - \u0391\u03c0\u03bf\u03bc\u03ad\u03bd\u03b5\u03b9 1 \u03ba\u03cd\u03ba\u03bb\u03bf\u03c2", + "dfs": "\u03a3\u03c5\u03c1\u03c4\u03ac\u03c1\u03b9 \u03b3\u03b5\u03bc\u03ac\u03c4\u03bf", + "dhf": "\u0392\u03bb\u03ac\u03b2\u03b7 \u03b1\u03c1\u03c7\u03b9\u03ba\u03ae\u03c2 \u03b8\u03ad\u03c3\u03b7\u03c2 \u03ba\u03b1\u03b9 \u03b8\u03ad\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc\u03c1\u03c1\u03b9\u03c8\u03b7\u03c2", + "dpf": "\u0392\u03bb\u03ac\u03b2\u03b7 \u03b8\u03ad\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc\u03c1\u03c1\u03b9\u03c8\u03b7\u03c2", + "ec": "\u0386\u03b4\u03b5\u03b9\u03bf\u03c2 \u03ba\u03cd\u03ba\u03bb\u03bf\u03c2", + "hpf": "\u0392\u03bb\u03ac\u03b2\u03b7 \u03c4\u03b7\u03c2 \u03b1\u03c1\u03c7\u03b9\u03ba\u03ae\u03c2 \u03b8\u03ad\u03c3\u03b7\u03c2", + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "offline": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "otf": "\u0392\u03bb\u03ac\u03b2\u03b7 \u03c5\u03c0\u03b5\u03c1\u03b2\u03bf\u03bb\u03b9\u03ba\u03ae\u03c2 \u03c1\u03bf\u03c0\u03ae\u03c2", + "p": "\u03a3\u03b5 \u03c0\u03b1\u03cd\u03c3\u03b7", + "pd": "\u0391\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7 \u03c4\u03c3\u03b9\u03bc\u03c0\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2", + "rdy": "\u0388\u03c4\u03bf\u03b9\u03bc\u03bf", + "scf": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b3\u03ac\u03c4\u03b1\u03c2 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7", + "sdf": "\u0393\u03b5\u03bc\u03ac\u03c4\u03bf \u03c3\u03c5\u03c1\u03c4\u03ac\u03c1\u03b9 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7", + "spf": "\u0391\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7 \u03c4\u03c3\u03b9\u03bc\u03c0\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.et.json b/homeassistant/components/litterrobot/translations/sensor.et.json new file mode 100644 index 00000000000..98b04dfd207 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.et.json @@ -0,0 +1,28 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "Kaast eemaldatud", + "ccc": "Puhastusts\u00fckkel on l\u00f5ppenud", + "ccp": "Puhastusts\u00fckkel on pooleli", + "csf": "Kassianduri viga", + "csi": "Kassianduri h\u00e4iring", + "cst": "Kassianduri ajastus", + "df1": "Sahtli peaaegu t\u00e4is - 2 ts\u00fcklit j\u00e4\u00e4nud", + "df2": "Sahtli peaaegu t\u00e4is - 1 ts\u00fckkel j\u00e4\u00e4nud", + "dfs": "Sahtli t\u00e4is", + "dhf": "Pr\u00fcgikasti + koduasendi t\u00f5rge", + "dpf": "T\u00fchjenduspunkti viga", + "ec": "T\u00fchjendusts\u00fckkel", + "hpf": "Koduasendi viga", + "off": "V\u00e4ljas", + "offline": "\u00dchenduseta", + "otf": "\u00dclekoornuse viga", + "p": "Ootel", + "pd": "Pinch Detect", + "rdy": "Valmis", + "scf": "Kassianduri viga k\u00e4ivitamisel", + "sdf": "Sahtel t\u00e4is k\u00e4ivitamisel", + "spf": "Pinch Detect k\u00e4ivitamisel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.fr.json b/homeassistant/components/litterrobot/translations/sensor.fr.json new file mode 100644 index 00000000000..fba0796e2b0 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.fr.json @@ -0,0 +1,28 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "Capot retir\u00e9", + "ccc": "Cycle de nettoyage termin\u00e9", + "ccp": "Cycle de nettoyage en cours", + "csf": "D\u00e9faut du capteur de chat", + "csi": "Interruption du capteur de chat", + "cst": "Minutage du capteur de chat", + "df1": "Tiroir presque plein \u2013\u00a02\u00a0cycles restants", + "df2": "Tiroir presque plein \u2013\u00a01\u00a0cycle restant", + "dfs": "Tiroir plein", + "dhf": "D\u00e9faut de position de vidage +\u00a0origine", + "dpf": "D\u00e9faut de position de vidage", + "ec": "Cycle de vidage", + "hpf": "D\u00e9faut de position d'origine", + "off": "D\u00e9sactiv\u00e9", + "offline": "Hors ligne", + "otf": "D\u00e9faut de surcouple", + "p": "En pause", + "pd": "D\u00e9tection de pincement", + "rdy": "Pr\u00eat", + "scf": "D\u00e9faut du capteur de chat au d\u00e9marrage", + "sdf": "Tiroir plein au d\u00e9marrage", + "spf": "D\u00e9tection de pincement au d\u00e9marrage" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.hu.json b/homeassistant/components/litterrobot/translations/sensor.hu.json new file mode 100644 index 00000000000..6c68a231bf4 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.hu.json @@ -0,0 +1,28 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "Tet\u0151 elt\u00e1vol\u00edtva", + "ccc": "Tiszt\u00edt\u00e1s befejez\u0151d\u00f6tt", + "ccp": "Tiszt\u00edt\u00e1s folyamatban", + "csf": "Macska\u00e9rz\u00e9kel\u0151 hiba", + "csi": "Macska\u00e9rz\u00e9kel\u0151 megszak\u00edtva", + "cst": "Macska\u00e9rz\u00e9kel\u0151 id\u0151z\u00edt\u00e9se", + "df1": "A fi\u00f3k majdnem megtelt \u2013 2 ciklus van h\u00e1tra", + "df2": "A fi\u00f3k majdnem megtelt \u2013 1 ciklus maradt", + "dfs": "Fi\u00f3k tele", + "dhf": "Dump + Home poz\u00edci\u00f3 hiba", + "dpf": "Dump poz\u00edci\u00f3 hiba", + "ec": "\u00dcres ciklus", + "hpf": "Home poz\u00edci\u00f3 hiba", + "off": "Ki", + "offline": "Offline", + "otf": "T\u00falzott nyomat\u00e9k hiba", + "p": "Sz\u00fcnetel", + "pd": "Pinch Detect", + "rdy": "K\u00e9sz", + "scf": "Macska\u00e9rz\u00e9kel\u0151 hiba ind\u00edt\u00e1skor", + "sdf": "Ind\u00edt\u00e1skor megtelt a fi\u00f3k", + "spf": "Pinch Detect ind\u00edt\u00e1skor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.ja.json b/homeassistant/components/litterrobot/translations/sensor.ja.json new file mode 100644 index 00000000000..4191e47ff4a --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.ja.json @@ -0,0 +1,28 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "\u30dc\u30f3\u30cd\u30c3\u30c8\u304c\u53d6\u308a\u5916\u3055\u308c\u307e\u3057\u305f", + "ccc": "\u30af\u30ea\u30fc\u30f3\u30b5\u30a4\u30af\u30eb\u5b8c\u4e86", + "ccp": "\u30af\u30ea\u30fc\u30f3\u30b5\u30a4\u30af\u30eb\u304c\u9032\u884c\u4e2d", + "csf": "Cat\u30bb\u30f3\u30b5\u30fc\u4e0d\u826f", + "csi": "Cat\u30bb\u30f3\u30b5\u30fc\u304c\u4e2d\u65ad\u3055\u308c\u307e\u3057\u305f", + "cst": "Cat\u30bb\u30f3\u30b5\u30fc\u30bf\u30a4\u30df\u30f3\u30b0", + "df1": "\u30c9\u30ed\u30ef\u30fc\u307b\u307c\u6e80\u676f - \u6b8b\u308a2\u30b5\u30a4\u30af\u30eb", + "df2": "\u30c9\u30ed\u30ef\u30fc\u307b\u307c\u6e80\u676f - \u6b8b\u308a1\u30b5\u30a4\u30af\u30eb", + "dfs": "\u30c9\u30ed\u30ef\u30fc\u30d5\u30eb", + "dhf": "\u30c0\u30f3\u30d7+\u30db\u30fc\u30e0\u30dd\u30b8\u30b7\u30e7\u30f3\u4e0d\u826f", + "dpf": "\u30c0\u30f3\u30d7\u30dd\u30b8\u30b7\u30e7\u30f3\u4e0d\u826f", + "ec": "\u7a7a\u306e\u30b5\u30a4\u30af\u30eb", + "hpf": "\u30db\u30fc\u30e0\u30dd\u30b8\u30b7\u30e7\u30f3\u4e0d\u826f", + "off": "\u30aa\u30d5", + "offline": "\u30aa\u30d5\u30e9\u30a4\u30f3", + "otf": "\u30aa\u30fc\u30d0\u30fc\u30c8\u30eb\u30af\u4e0d\u826f", + "p": "\u4e00\u6642\u505c\u6b62", + "pd": "\u30d4\u30f3\u30c1\u691c\u51fa", + "rdy": "\u6e96\u5099", + "scf": "\u8d77\u52d5\u6642\u306bCat\u30bb\u30f3\u30b5\u30fc\u4e0d\u826f", + "sdf": "\u8d77\u52d5\u6642\u306b\u30c9\u30ed\u30ef\u30fc\u6e80\u676f", + "spf": "\u8d77\u52d5\u6642\u306b\u30d4\u30f3\u30c1\u691c\u51fa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.nl.json b/homeassistant/components/litterrobot/translations/sensor.nl.json new file mode 100644 index 00000000000..6a383627aee --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.nl.json @@ -0,0 +1,25 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "Motorkap verwijderd", + "ccc": "Reinigingscyclus voltooid", + "ccp": "Reinigingscyclus in uitvoering", + "csf": "Kattensensor fout", + "csi": "Kattensensor onderbroken", + "cst": "Timing kattensensor", + "df1": "Lade bijna vol - nog 2 cycli over", + "df2": "Lade bijna vol - nog 1 cyclus over", + "dfs": "Lade vol", + "dhf": "Dump + Home positie fout", + "dpf": "Dump Positie Fout", + "ec": "Lege cyclus", + "hpf": "Home Positie Fout", + "off": "Uit", + "offline": "Offline", + "p": "Gepauzeerd", + "rdy": "Gereed", + "scf": "Kattensensorfout bij opstarten", + "sdf": "Lade vol bij opstarten" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.pt-BR.json b/homeassistant/components/litterrobot/translations/sensor.pt-BR.json new file mode 100644 index 00000000000..9eb1dd8be8b --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.pt-BR.json @@ -0,0 +1,28 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "Cap\u00f4 removido", + "ccc": "Ciclo de limpeza conclu\u00eddo", + "ccp": "Ciclo de limpeza em andamento", + "csf": "Falha do Sensor Cat", + "csi": "Sensor Cat interrompido", + "cst": "Sincroniza\u00e7\u00e3o do Sensor Cat", + "df1": "Gaveta quase cheia - 2 ciclos restantes", + "df2": "Gaveta quase cheia - 1 ciclo \u00e0 esquerda", + "dfs": "Gaveta cheia", + "dhf": "Despejo + Falha de Posi\u00e7\u00e3o Inicial", + "dpf": "Falha de posi\u00e7\u00e3o de despejo", + "ec": "Ciclo vazio", + "hpf": "Falha de posi\u00e7\u00e3o inicial", + "off": "Desligado", + "offline": "Offline", + "otf": "Falha de sobretorque", + "p": "Pausado", + "pd": "Detec\u00e7\u00e3o de pin\u00e7a", + "rdy": "Pronto", + "scf": "Falha do sensor Cat na inicializa\u00e7\u00e3o", + "sdf": "Gaveta cheia na inicializa\u00e7\u00e3o", + "spf": "Detec\u00e7\u00e3o de pin\u00e7a na inicializa\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.ru.json b/homeassistant/components/litterrobot/translations/sensor.ru.json new file mode 100644 index 00000000000..3a28cfe08ec --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.ru.json @@ -0,0 +1,28 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "\u041a\u043e\u0436\u0443\u0445 \u0441\u043d\u044f\u0442", + "ccc": "\u0426\u0438\u043a\u043b \u043e\u0447\u0438\u0441\u0442\u043a\u0438 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d", + "ccp": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u0446\u0438\u043a\u043b \u043e\u0447\u0438\u0441\u0442\u043a\u0438", + "csf": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u043e\u0441\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0430", + "csi": "\u0414\u0430\u0442\u0447\u0438\u043a \u043f\u0435\u0440\u0435\u043a\u0440\u044b\u0442", + "cst": "\u0412\u0440\u0435\u043c\u044f \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u043d\u0438\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u0430", + "df1": "\u042f\u0449\u0438\u043a \u043f\u043e\u0447\u0442\u0438 \u043f\u043e\u043b\u043e\u043d \u2014 \u043e\u0441\u0442\u0430\u043b\u043e\u0441\u044c 2 \u0446\u0438\u043a\u043b\u0430", + "df2": "\u042f\u0449\u0438\u043a \u043f\u043e\u0447\u0442\u0438 \u043f\u043e\u043b\u043e\u043d \u2014 \u043e\u0441\u0442\u0430\u043b\u0441\u044f 1 \u0446\u0438\u043a\u043b", + "dfs": "\u042f\u0449\u0438\u043a \u043f\u043e\u043b\u043d\u044b\u0439", + "dhf": "\u0421\u0431\u0440\u043e\u0441 + \u043e\u0448\u0438\u0431\u043a\u0430 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f", + "dpf": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0441\u0431\u0440\u043e\u0441\u0430", + "ec": "\u041f\u0443\u0441\u0442\u043e\u0439 \u0446\u0438\u043a\u043b", + "hpf": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f", + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "offline": "\u041d\u0435 \u0432 \u0441\u0435\u0442\u0438", + "otf": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u0438\u044f \u043a\u0440\u0443\u0442\u044f\u0449\u0435\u0433\u043e \u043c\u043e\u043c\u0435\u043d\u0442\u0430", + "p": "\u041f\u0430\u0443\u0437\u0430", + "pd": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u0437\u0430\u0449\u0435\u043c\u043b\u0435\u043d\u0438\u044f", + "rdy": "\u0413\u043e\u0442\u043e\u0432", + "scf": "\u041d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u043e\u0441\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435", + "sdf": "\u042f\u0449\u0438\u043a \u043f\u043e\u043b\u043d\u044b\u0439 \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435", + "spf": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435 \u0437\u0430\u0449\u0435\u043c\u043b\u0435\u043d\u0438\u044f \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.zh-Hans.json b/homeassistant/components/litterrobot/translations/sensor.zh-Hans.json new file mode 100644 index 00000000000..400c2b81dcb --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.zh-Hans.json @@ -0,0 +1,10 @@ +{ + "state": { + "litterrobot__status_code": { + "off": "\u5173\u95ed", + "offline": "\u79bb\u7ebf", + "p": "\u5df2\u6682\u505c", + "rdy": "\u5c31\u7eea" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.zh-Hant.json b/homeassistant/components/litterrobot/translations/sensor.zh-Hant.json new file mode 100644 index 00000000000..f51c03f9c8c --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.zh-Hant.json @@ -0,0 +1,28 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "\u4e0a\u84cb\u906d\u958b\u555f", + "ccc": "\u8c93\u7802\u6e05\u7406\u5b8c\u6210", + "ccp": "\u8c93\u7802\u6e05\u7406\u4e2d", + "csf": "\u8c93\u54aa\u611f\u6e2c\u5668\u6545\u969c", + "csi": "\u8c93\u54aa\u611f\u6e2c\u5668\u906d\u4e2d\u65b7", + "cst": "\u8c93\u54aa\u611f\u6e2c\u5668\u8a08\u6642", + "df1": "\u6392\u5ee2\u76d2\u5feb\u6eff\u4e86 - \u5269\u4e0b 2 \u6b21\u6e05\u7406", + "df2": "\u6392\u5ee2\u76d2\u5feb\u6eff\u4e86 - \u5269\u4e0b 1 \u6b21\u6e05\u7406", + "dfs": "\u6392\u5ee2\u76d2\u5df2\u6eff", + "dhf": "\u50be\u5012\u8207\u539f\u9ede\u4f4d\u7f6e\u6545\u969c", + "dpf": "\u50be\u5012\u4f4d\u7f6e\u6545\u969c", + "ec": "\u6574\u5e73", + "hpf": "\u539f\u9ede\u5b9a\u4f4d\u6545\u969c", + "off": "\u95dc\u9589", + "offline": "\u96e2\u7dda", + "otf": "\u8f49\u52d5\u5931\u6557", + "p": "\u5df2\u66ab\u505c", + "pd": "\u7570\u7269\u5075\u6e2c", + "rdy": "\u6e96\u5099\u5c31\u7dd2", + "scf": "\u555f\u52d5\u6642\u8c93\u54aa\u611f\u6e2c\u5668\u5931\u6548", + "sdf": "\u555f\u52d5\u6642\u6392\u5ee2\u76d2\u5df2\u6eff", + "spf": "\u555f\u52d5\u6642\u5075\u6e2c\u5230\u7570\u7269" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/ja.json b/homeassistant/components/meater/translations/ja.json index e24fc9199cc..72912036ddd 100644 --- a/homeassistant/components/meater/translations/ja.json +++ b/homeassistant/components/meater/translations/ja.json @@ -9,13 +9,17 @@ "reauth_confirm": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - } + }, + "description": "Meater Cloud\u30a2\u30ab\u30a6\u30f3\u30c8 {username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, + "data_description": { + "username": "Meater Cloud\u306e\u30e6\u30fc\u30b6\u30fc\u540d\u3001\u901a\u5e38\u306f\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3067\u3059\u3002" + }, "description": "Meater Cloud\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } diff --git a/homeassistant/components/min_max/translations/zh-Hans.json b/homeassistant/components/min_max/translations/zh-Hans.json index ca5555bbd0a..9fb15dafd2a 100644 --- a/homeassistant/components/min_max/translations/zh-Hans.json +++ b/homeassistant/components/min_max/translations/zh-Hans.json @@ -11,7 +11,7 @@ "data_description": { "round_digits": "\u5f53\u7edf\u8ba1\u9879\u4e3a\u5e73\u5747\u503c\u6216\u4e2d\u4f4d\u6570\u65f6\uff0c\u63a7\u5236\u8f93\u51fa\u7684\u5c0f\u6570\u4f4d\u6570\u3002" }, - "description": "\u521b\u5efa\u4f20\u611f\u5668\u6765\u8ba1\u7b97\u591a\u4e2a\u8f93\u5165\u4f20\u611f\u5668\u503c\u7684\u6700\u5927\u503c\u3001\u6700\u5c0f\u503c\u3001\u5e73\u5747\u503c\u3001\u6216\u4e2d\u4f4d\u6570\u3002", + "description": "\u521b\u5efa\u4f20\u611f\u5668\u6765\u8ba1\u7b97\u591a\u4e2a\u8f93\u5165\u4f20\u611f\u5668\u503c\u7684\u6700\u5927\u503c\u3001\u6700\u5c0f\u503c\u3001\u5e73\u5747\u503c\u6216\u4e2d\u4f4d\u6570\u3002", "title": "\u6dfb\u52a0\u6700\u5927\u503c/\u6700\u5c0f\u503c/\u5e73\u5747\u503c/\u4e2d\u4f4d\u6570\u4f20\u611f\u5668" } } @@ -34,7 +34,7 @@ "round_digits": "\u7cbe\u5ea6", "type": "\u7edf\u8ba1\u9879" }, - "description": "\u521b\u5efa\u4f20\u611f\u5668\u6765\u8ba1\u7b97\u591a\u4e2a\u8f93\u5165\u4f20\u611f\u5668\u503c\u7684\u6700\u5927\u503c\u3001\u6700\u5c0f\u503c\u3001\u5e73\u5747\u503c\u3001\u6216\u4e2d\u4f4d\u6570\u3002" + "description": "\u521b\u5efa\u4f20\u611f\u5668\u6765\u8ba1\u7b97\u591a\u4e2a\u8f93\u5165\u4f20\u611f\u5668\u503c\u7684\u6700\u5927\u503c\u3001\u6700\u5c0f\u503c\u3001\u5e73\u5747\u503c\u6216\u4e2d\u4f4d\u6570\u3002" } } }, diff --git a/homeassistant/components/motion_blinds/translations/ca.json b/homeassistant/components/motion_blinds/translations/ca.json index 18cc8f892d6..4ec912aa25c 100644 --- a/homeassistant/components/motion_blinds/translations/ca.json +++ b/homeassistant/components/motion_blinds/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat, la configuraci\u00f3 de connexi\u00f3 s'ha actualitzat", + "already_configured": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "connection_error": "Ha fallat la connexi\u00f3" }, diff --git a/homeassistant/components/motion_blinds/translations/de.json b/homeassistant/components/motion_blinds/translations/de.json index c5ced3ddd55..d3b6b219516 100644 --- a/homeassistant/components/motion_blinds/translations/de.json +++ b/homeassistant/components/motion_blinds/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert, Verbindungseinstellungen werden aktualisiert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "connection_error": "Verbindung fehlgeschlagen" }, diff --git a/homeassistant/components/motion_blinds/translations/et.json b/homeassistant/components/motion_blinds/translations/et.json index 935e345ea3a..715208cd9f4 100644 --- a/homeassistant/components/motion_blinds/translations/et.json +++ b/homeassistant/components/motion_blinds/translations/et.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud, \u00fchenduse s\u00e4tted on v\u00e4rskendatud", + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Seadistamine on juba k\u00e4imas", "connection_error": "\u00dchendamine nurjus" }, diff --git a/homeassistant/components/motion_blinds/translations/fr.json b/homeassistant/components/motion_blinds/translations/fr.json index 09cf93fde60..17df2654026 100644 --- a/homeassistant/components/motion_blinds/translations/fr.json +++ b/homeassistant/components/motion_blinds/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9, les param\u00e8tres de connexion ont \u00e9t\u00e9 mis \u00e0 jour", + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "connection_error": "\u00c9chec de connexion" }, diff --git a/homeassistant/components/motion_blinds/translations/id.json b/homeassistant/components/motion_blinds/translations/id.json index d576ced8c0a..42b9b8d127d 100644 --- a/homeassistant/components/motion_blinds/translations/id.json +++ b/homeassistant/components/motion_blinds/translations/id.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi, pengaturan koneksi diperbarui", + "already_configured": "Perangkat sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", "connection_error": "Gagal terhubung" }, diff --git a/homeassistant/components/motion_blinds/translations/it.json b/homeassistant/components/motion_blinds/translations/it.json index 0871ce07bf8..ddb29c2c042 100644 --- a/homeassistant/components/motion_blinds/translations/it.json +++ b/homeassistant/components/motion_blinds/translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato, le impostazioni di connessione sono aggiornate", + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "connection_error": "Impossibile connettersi" }, diff --git a/homeassistant/components/motion_blinds/translations/nl.json b/homeassistant/components/motion_blinds/translations/nl.json index 6cb69b06b87..c837e656c56 100644 --- a/homeassistant/components/motion_blinds/translations/nl.json +++ b/homeassistant/components/motion_blinds/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd, verbindingsinstellingen zijn bijgewerkt", + "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratiestroom is al aan de gang", "connection_error": "Kan geen verbinding maken" }, diff --git a/homeassistant/components/motion_blinds/translations/pt-BR.json b/homeassistant/components/motion_blinds/translations/pt-BR.json index 01658cea852..afbd117d260 100644 --- a/homeassistant/components/motion_blinds/translations/pt-BR.json +++ b/homeassistant/components/motion_blinds/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado, as configura\u00e7\u00f5es de conex\u00e3o s\u00e3o atualizadas", + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "connection_error": "Falha ao conectar" }, diff --git a/homeassistant/components/motion_blinds/translations/ru.json b/homeassistant/components/motion_blinds/translations/ru.json index e7a4492bd54..0ab743f8895 100644 --- a/homeassistant/components/motion_blinds/translations/ru.json +++ b/homeassistant/components/motion_blinds/translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant. \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u044b.", + "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.", "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, diff --git a/homeassistant/components/motion_blinds/translations/zh-Hant.json b/homeassistant/components/motion_blinds/translations/zh-Hant.json index e7fa565ba36..ccfe6e782ed 100644 --- a/homeassistant/components/motion_blinds/translations/zh-Hant.json +++ b/homeassistant/components/motion_blinds/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u9023\u7dda\u8a2d\u5b9a\u5df2\u66f4\u65b0", + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "connection_error": "\u9023\u7dda\u5931\u6557" }, diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index 752e847336f..c2994de0532 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -54,7 +54,7 @@ "title": "Google Cloud\u306e\u8a2d\u5b9a" }, "reauth_confirm": { - "description": "Nest\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "description": "Nest\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" } } diff --git a/homeassistant/components/netatmo/translations/ja.json b/homeassistant/components/netatmo/translations/ja.json index ecb21ffe039..b9f2f17960a 100644 --- a/homeassistant/components/netatmo/translations/ja.json +++ b/homeassistant/components/netatmo/translations/ja.json @@ -15,7 +15,7 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "Netatmo\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "description": "Netatmo\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" } } diff --git a/homeassistant/components/onewire/translations/es.json b/homeassistant/components/onewire/translations/es.json index d789e46875f..9879ccb0a00 100644 --- a/homeassistant/components/onewire/translations/es.json +++ b/homeassistant/components/onewire/translations/es.json @@ -17,6 +17,7 @@ }, "user": { "data": { + "port": "Puerto", "type": "Tipo de conexi\u00f3n" }, "title": "Configurar 1 cable" diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index 2d3f53b0710..67e1630469e 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -2,15 +2,20 @@ "config": { "abort": { "already_configured": "\u3053\u306eSimpliSafe account\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002", + "email_2fa_timed_out": "\u96fb\u5b50\u30e1\u30fc\u30eb\u306b\u3088\u308b2\u8981\u7d20\u8a8d\u8a3c\u306e\u5f85\u6a5f\u4e2d\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "wrong_account": "\u63d0\u4f9b\u3055\u308c\u305f\u30e6\u30fc\u30b6\u30fc\u8a8d\u8a3c\u60c5\u5831\u304c\u3001\u3053\u306eSimpliSafe\u30a2\u30ab\u30a6\u30f3\u30c8\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" }, "error": { + "2fa_timed_out": "\u4e8c\u8981\u7d20\u8a8d\u8a3c\u306e\u5f85\u6a5f\u4e2d\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f", "identifier_exists": "\u30a2\u30ab\u30a6\u30f3\u30c8\u767b\u9332\u6e08\u307f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "still_awaiting_mfa": "MFA email click\u3092\u307e\u3060\u5f85\u3063\u3066\u3044\u307e\u3059", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, + "progress": { + "email_2fa": "Simplisafe\u304b\u3089\u306e\u78ba\u8a8d\u30ea\u30f3\u30af\u304c\u306a\u3044\u304b\u30e1\u30fc\u30eb\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "step": { "mfa": { "title": "SimpliSafe\u591a\u8981\u7d20\u8a8d\u8a3c" @@ -25,7 +30,8 @@ "sms_2fa": { "data": { "code": "\u30b3\u30fc\u30c9" - } + }, + "description": "SMS\u3067\u9001\u3089\u308c\u3066\u304d\u305f2\u8981\u7d20\u8a8d\u8a3c\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u307e\u3059\u3002" }, "user": { "data": { diff --git a/homeassistant/components/slack/translations/ca.json b/homeassistant/components/slack/translations/ca.json new file mode 100644 index 00000000000..58ff126ae27 --- /dev/null +++ b/homeassistant/components/slack/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "default_channel": "Canal predeterminat", + "icon": "Icona", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/de.json b/homeassistant/components/slack/translations/de.json new file mode 100644 index 00000000000..d87383ffac7 --- /dev/null +++ b/homeassistant/components/slack/translations/de.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "default_channel": "Standardkanal", + "icon": "Symbol", + "username": "Benutzername" + }, + "data_description": { + "api_key": "Der Slack-API-Token, das zum Senden von Slack-Nachrichten verwendet werden soll.", + "default_channel": "Der Kanal, an den gepostet werden soll, wenn beim Senden einer Nachricht kein Kanal angegeben wird.", + "icon": "Verwende eines der Slack-Emojis als Symbol f\u00fcr den bereitgestellten Benutzernamen.", + "username": "Home Assistant postet unter Verwendung des angegebenen Benutzernamens in Slack." + }, + "description": "Lies in der Dokumentation nach, wie du deinen Slack-API-Schl\u00fcssel erh\u00e4ltst." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/el.json b/homeassistant/components/slack/translations/el.json new file mode 100644 index 00000000000..8628dde5821 --- /dev/null +++ b/homeassistant/components/slack/translations/el.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "default_channel": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03ba\u03b1\u03bd\u03ac\u03bb\u03b9", + "icon": "\u0395\u03b9\u03ba\u03bf\u03bd\u03af\u03b4\u03b9\u03bf", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "data_description": { + "api_key": "\u03a4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc Slack API \u03b3\u03b9\u03b1 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae \u03bc\u03b7\u03bd\u03c5\u03bc\u03ac\u03c4\u03c9\u03bd Slack.", + "default_channel": "\u03a4\u03bf \u03ba\u03b1\u03bd\u03ac\u03bb\u03b9 \u03c3\u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03b8\u03b1 \u03b4\u03b7\u03bc\u03bf\u03c3\u03b9\u03b5\u03cd\u03c3\u03b5\u03c4\u03b5 \u03b1\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03ba\u03b1\u03bd\u03ac\u03bb\u03b9 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae \u03b5\u03bd\u03cc\u03c2 \u03bc\u03b7\u03bd\u03cd\u03bc\u03b1\u03c4\u03bf\u03c2.", + "icon": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c0\u03cc \u03c4\u03b1 Slack emoji \u03c9\u03c2 \u03b5\u03b9\u03ba\u03bf\u03bd\u03af\u03b4\u03b9\u03bf \u03b3\u03b9\u03b1 \u03c4\u03bf \u03c0\u03b1\u03c1\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7.", + "username": "\u03a4\u03bf Home Assistant \u03b8\u03b1 \u03b4\u03b7\u03bc\u03bf\u03c3\u03b9\u03b5\u03cd\u03b5\u03b9 \u03c3\u03c4\u03bf Slack \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7." + }, + "description": "\u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03bb\u03ae\u03c8\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd Slack API." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/en.json b/homeassistant/components/slack/translations/en.json index 5a97fb1938e..23e5830764e 100644 --- a/homeassistant/components/slack/translations/en.json +++ b/homeassistant/components/slack/translations/en.json @@ -16,13 +16,13 @@ "icon": "Icon", "username": "Username" }, - "description": "Refer to the documentation on getting your Slack API key.", "data_description": { "api_key": "The Slack API token to use for sending Slack messages.", "default_channel": "The channel to post to if no channel is specified when sending a message.", "icon": "Use one of the Slack emojis as an Icon for the supplied username.", "username": "Home Assistant will post to Slack using the username specified." - } + }, + "description": "Refer to the documentation on getting your Slack API key." } } } diff --git a/homeassistant/components/slack/translations/et.json b/homeassistant/components/slack/translations/et.json new file mode 100644 index 00000000000..bc76c6d848a --- /dev/null +++ b/homeassistant/components/slack/translations/et.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "default_channel": "Vaikimisi kanal", + "icon": "Ikoon", + "username": "Kasutajanimi" + }, + "data_description": { + "api_key": "Slacki API v\u00f5ti mida kasutatakse Slacki s\u00f5numite saatmiseks.", + "default_channel": "Kanal, kuhu postitada kui s\u00f5numi saatmisel ei ole kanalit m\u00e4\u00e4ratud.", + "icon": "Kasuta \u00fchte Slacki emotikoni esitatud kasutajanime ikoonina.", + "username": "Home Assistant postitab Slacki kasutades m\u00e4\u00e4ratud kasutajanime." + }, + "description": "Vaata oma Slack API v\u00f5tme hankimise dokumentatsiooni." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/fr.json b/homeassistant/components/slack/translations/fr.json new file mode 100644 index 00000000000..0573e8c81c2 --- /dev/null +++ b/homeassistant/components/slack/translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "default_channel": "Canal par d\u00e9faut", + "icon": "Ic\u00f4ne", + "username": "Nom d'utilisateur" + }, + "data_description": { + "api_key": "Le jeton d'API Slack \u00e0 utiliser pour l'envoi des messages sur Slack.", + "default_channel": "Le canal sur lequel poster si aucun canal n'est sp\u00e9cifi\u00e9 lors de l'envoi d'un message.", + "icon": "Utilisez l'un des \u00e9mojis de Slack en tant qu'ic\u00f4ne pour le nom d'utilisateur fourni.", + "username": "Home Assistant postera sur Slack en utilisant le nom d'utilisateur sp\u00e9cifi\u00e9." + }, + "description": "Consultez la documentation pour obtenir votre cl\u00e9 d'API Slack." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/hu.json b/homeassistant/components/slack/translations/hu.json new file mode 100644 index 00000000000..c2737dd6e01 --- /dev/null +++ b/homeassistant/components/slack/translations/hu.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s 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": { + "api_key": "API kulcs", + "default_channel": "Alap\u00e9rtelmezett csatorna", + "icon": "Ikon", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "data_description": { + "api_key": "A Slack API token a Slack \u00fczenetek k\u00fcld\u00e9s\u00e9re.", + "default_channel": "Az a csatorna, amelyre k\u00f6zz\u00e9tehet, ha nincs megadva csatorna az \u00fczenet k\u00fcld\u00e9sekor.", + "icon": "Haszn\u00e1lja a Slack hangulatjelek egyik\u00e9t ikonk\u00e9nt a megadott felhaszn\u00e1l\u00f3n\u00e9vhez.", + "username": "A Home Assistant a megadott felhaszn\u00e1l\u00f3n\u00e9vvel fog \u00fczenetet k\u00fcldeni a Slackre." + }, + "description": "L\u00e1sd a Slack API-kulcs megszerz\u00e9s\u00e9nek dokument\u00e1ci\u00f3j\u00e1t." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/ja.json b/homeassistant/components/slack/translations/ja.json new file mode 100644 index 00000000000..c6a7f478db6 --- /dev/null +++ b/homeassistant/components/slack/translations/ja.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "default_channel": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30c1\u30e3\u30f3\u30cd\u30eb", + "icon": "\u30a2\u30a4\u30b3\u30f3", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + }, + "data_description": { + "api_key": "Slack\u30e1\u30c3\u30bb\u30fc\u30b8\u306e\u9001\u4fe1\u306b\u4f7f\u7528\u3059\u308b\u3001Slack API\u30c8\u30fc\u30af\u30f3\u3002", + "default_channel": "\u30e1\u30c3\u30bb\u30fc\u30b8\u306e\u9001\u4fe1\u6642\u306b\u3001\u30c1\u30e3\u30f3\u30cd\u30eb\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u306a\u304b\u3063\u305f\u5834\u5408\u306b\u6295\u7a3f\u3059\u308b\u30c1\u30e3\u30f3\u30cd\u30eb\u3002", + "icon": "Slack\u306e\u7d75\u6587\u5b57\u3092\u3001\u6307\u5b9a\u3055\u308c\u305f\u30e6\u30fc\u30b6\u30fc\u540d\u306e\u30a2\u30a4\u30b3\u30f3\u3068\u3057\u3066\u4f7f\u7528\u3057\u307e\u3059\u3002", + "username": "Home Assistant\u306f\u3001\u6307\u5b9a\u3055\u308c\u305f\u30e6\u30fc\u30b6\u30fc\u540d\u3092\u4f7f\u7528\u3057\u3066Slack\u306b\u6295\u7a3f\u3057\u307e\u3059\u3002" + }, + "description": "Slack API\u30ad\u30fc\u306e\u53d6\u5f97\u306b\u3064\u3044\u3066\u306f\u3001\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/nl.json b/homeassistant/components/slack/translations/nl.json new file mode 100644 index 00000000000..8022048798e --- /dev/null +++ b/homeassistant/components/slack/translations/nl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Service is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "api_key": "API-sleutel", + "default_channel": "Standaard kanaal", + "icon": "Icoon", + "username": "Gebruikersnaam" + }, + "data_description": { + "api_key": "Het Slack API-token dat moet worden gebruikt voor het verzenden van Slack-berichten.", + "default_channel": "Het kanaal waarnaar moet worden gepost als er geen kanaal is opgegeven bij het verzenden van een bericht.", + "icon": "Gebruik een van de Slack emoji's als pictogram voor de opgegeven gebruikersnaam.", + "username": "Home Assistant plaatst berichten op Slack met de opgegeven gebruikersnaam." + }, + "description": "Raadpleeg de documentatie over het verkrijgen van uw Slack API-sleutel." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/pt-BR.json b/homeassistant/components/slack/translations/pt-BR.json new file mode 100644 index 00000000000..a6299848bfc --- /dev/null +++ b/homeassistant/components/slack/translations/pt-BR.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave de API", + "default_channel": "Canal padr\u00e3o", + "icon": "\u00cdcone", + "username": "Nome de usu\u00e1rio" + }, + "data_description": { + "api_key": "O token da API do Slack a ser usado para enviar mensagens do Slack.", + "default_channel": "O canal para postar se nenhum canal for especificado ao enviar uma mensagem.", + "icon": "Use um dos emojis do Slack como \u00edcone para o nome de usu\u00e1rio fornecido.", + "username": "O Home Assistant postar\u00e1 no Slack usando o nome de usu\u00e1rio especificado." + }, + "description": "Consulte a documenta\u00e7\u00e3o sobre como obter sua chave de API do Slack." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/ru.json b/homeassistant/components/slack/translations/ru.json new file mode 100644 index 00000000000..dd25d00e367 --- /dev/null +++ b/homeassistant/components/slack/translations/ru.json @@ -0,0 +1,29 @@ +{ + "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." + }, + "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", + "default_channel": "\u041a\u0430\u043d\u0430\u043b \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e", + "icon": "\u0418\u043a\u043e\u043d\u043a\u0430", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "data_description": { + "api_key": "\u0422\u043e\u043a\u0435\u043d API Slack, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439.", + "default_channel": "\u041a\u0430\u043d\u0430\u043b, \u043d\u0430 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435, \u0435\u0441\u043b\u0438 \u043f\u0440\u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u043a\u0430\u043d\u0430\u043b \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d.", + "icon": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0434\u0438\u043d \u0438\u0437 \u0441\u043c\u0430\u0439\u043b\u0438\u043a\u043e\u0432 Slack \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0437\u043d\u0430\u0447\u043a\u0430 \u0434\u043b\u044f \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0438\u043c\u0435\u043d\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.", + "username": "Home Assistant \u0431\u0443\u0434\u0435\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0432 Slack, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f." + }, + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439 \u043f\u043e \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044e \u043a\u043b\u044e\u0447\u0430 API Slack." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/zh-Hans.json b/homeassistant/components/slack/translations/zh-Hans.json new file mode 100644 index 00000000000..0f9177e8a5c --- /dev/null +++ b/homeassistant/components/slack/translations/zh-Hans.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52a1\u5df2\u914d\u7f6e" + }, + "error": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u8eab\u4efd\u9a8c\u8bc1\u65e0\u6548", + "unknown": "\u610f\u5916\u7684\u9519\u8bef" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u94a5", + "default_channel": "\u9ed8\u8ba4\u901a\u9053", + "icon": "\u56fe\u6807", + "username": "\u7528\u6237\u540d" + }, + "data_description": { + "api_key": "\u7528\u4e8e\u53d1\u9001 Slack \u6d88\u606f\u7684 Slack API token\u3002", + "default_channel": "\u5728\u53d1\u9001\u6d88\u606f\u672a\u6307\u5b9a\u901a\u9053\u65f6\u8981\u53d1\u5e03\u5230\u7684\u901a\u9053\u3002", + "icon": "\u4f7f\u7528\u5176\u4e2d\u4e00\u4e2a Slack \u8868\u60c5\u7b26\u53f7\u4f5c\u4e3a\u63d0\u4f9b\u7684\u7528\u6237\u540d\u7684\u56fe\u6807\u3002", + "username": "Home Assistant \u5c06\u4f7f\u7528\u6307\u5b9a\u7684\u7528\u6237\u540d\u53d1\u5e03\u5230 Slack\u3002" + }, + "description": "\u8bf7\u53c2\u9605\u6709\u5173\u83b7\u53d6 Slack API \u5bc6\u94a5\u7684\u6587\u6863\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/zh-Hant.json b/homeassistant/components/slack/translations/zh-Hant.json new file mode 100644 index 00000000000..083f5a9ffeb --- /dev/null +++ b/homeassistant/components/slack/translations/zh-Hant.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\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": { + "api_key": "API \u91d1\u9470", + "default_channel": "\u9810\u8a2d\u983b\u9053", + "icon": "\u5716\u793a", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "data_description": { + "api_key": "\u4f7f\u7528\u65bc\u50b3\u9001 Slack \u8a0a\u606f\u7684 Slack API \u6b0a\u6756\u3002", + "default_channel": "\u767c\u9001\u8a0a\u606f\u6642\u3001\u82e5\u7121\u6307\u5b9a\u983b\u9053\u6642\uff0c\u6240\u8981\u5f35\u8cbc\u7684\u983b\u9053\u3002", + "icon": "\u4f7f\u7528 Slack \u8868\u60c5\u7b26\u865f\u4f5c\u70ba\u63d0\u4f9b\u4f7f\u7528\u8005\u540d\u7a31\u4e4b\u5716\u793a\u3002", + "username": "Home Assistant \u5c07\u6703\u4f7f\u7528\u6307\u5b9a\u4f7f\u7528\u8005\u540d\u7a31\u5f35\u8cbc\u81f3 Slack\u3002" + }, + "description": "\u8acb\u53c3\u8003\u6587\u4ef6\u4ee5\u53d6\u5f97 API \u91d1\u9470\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarttub/translations/ja.json b/homeassistant/components/smarttub/translations/ja.json index dfe2ef1ab26..44d753fa22e 100644 --- a/homeassistant/components/smarttub/translations/ja.json +++ b/homeassistant/components/smarttub/translations/ja.json @@ -9,7 +9,7 @@ }, "step": { "reauth_confirm": { - "description": "SmartTub\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "description": "SmartTub\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, "user": { diff --git a/homeassistant/components/steam_online/translations/ja.json b/homeassistant/components/steam_online/translations/ja.json index 5e3b98684e8..935c975c801 100644 --- a/homeassistant/components/steam_online/translations/ja.json +++ b/homeassistant/components/steam_online/translations/ja.json @@ -12,12 +12,23 @@ }, "step": { "reauth_confirm": { + "description": "Steam\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306f\u624b\u52d5\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\n\n\u3053\u3053\u3067\u3042\u306a\u305f\u306e\u30ad\u30fc\u3092\u898b\u3064\u3051\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059: {api_key_url}", "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { "account": "Steam\u30a2\u30ab\u30a6\u30f3\u30c8ID", "api_key": "API\u30ad\u30fc" + }, + "description": "{account_id_url} \u3092\u4f7f\u7528\u3057\u3066\u3001\u3042\u306a\u305f\u306eSteam\u306e\u30a2\u30ab\u30a6\u30f3\u30c8ID\u3092\u898b\u3064\u3051\u307e\u3059" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "accounts": "\u76e3\u8996\u5bfe\u8c61\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u540d" } } } diff --git a/homeassistant/components/tautulli/translations/ja.json b/homeassistant/components/tautulli/translations/ja.json index f6bec08752a..6f733e1cad4 100644 --- a/homeassistant/components/tautulli/translations/ja.json +++ b/homeassistant/components/tautulli/translations/ja.json @@ -14,6 +14,7 @@ "data": { "api_key": "API\u30ad\u30fc" }, + "description": "API\u30ad\u30fc\u3092\u898b\u3064\u3051\u308b\u306b\u306f\u3001Tautulli Web\u30da\u30fc\u30b8\u3092\u958b\u304d\u3001Settings\u306b\u79fb\u52d5\u3057\u3066\u304b\u3089\u3001Web interface\u306b\u79fb\u52d5\u3057\u307e\u3059\u3002API\u30ad\u30fc\u306f\u3001\u305d\u306e\u30da\u30fc\u30b8\u306e\u4e00\u756a\u4e0b\u306b\u3042\u308a\u307e\u3059\u3002", "title": "Tautulli\u3092\u518d\u8a8d\u8a3c\u3057\u307e\u3059" }, "user": { @@ -21,7 +22,8 @@ "api_key": "API\u30ad\u30fc", "url": "URL", "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" - } + }, + "description": "API\u30ad\u30fc\u3092\u898b\u3064\u3051\u308b\u306b\u306f\u3001Tautulli Web\u30da\u30fc\u30b8\u3092\u958b\u304d\u3001Settings\u306b\u79fb\u52d5\u3057\u3066\u304b\u3089\u3001Web interface\u306b\u79fb\u52d5\u3057\u307e\u3059\u3002API\u30ad\u30fc\u306f\u3001\u305d\u306e\u30da\u30fc\u30b8\u306e\u4e00\u756a\u4e0b\u306b\u3042\u308a\u307e\u3059\u3002\n\nURL\u306e\u4f8b: ```http://192.168.0.10:8181``` \u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30dd\u30fc\u30c8\u306f\u30018181 \u3067\u3059\u3002" } } } diff --git a/homeassistant/components/totalconnect/translations/ja.json b/homeassistant/components/totalconnect/translations/ja.json index d6bffce6dae..7eefbc3a873 100644 --- a/homeassistant/components/totalconnect/translations/ja.json +++ b/homeassistant/components/totalconnect/translations/ja.json @@ -19,7 +19,7 @@ "title": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9" }, "reauth_confirm": { - "description": "Total Connect\u306f\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "description": "Total Connect\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" }, "user": { diff --git a/homeassistant/components/trafikverket_ferry/translations/ja.json b/homeassistant/components/trafikverket_ferry/translations/ja.json index 8081c3a9198..251117bc773 100644 --- a/homeassistant/components/trafikverket_ferry/translations/ja.json +++ b/homeassistant/components/trafikverket_ferry/translations/ja.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "incorrect_api_key": "\u9078\u629e\u3057\u305f\u30a2\u30ab\u30a6\u30f3\u30c8\u306eAPI\u30ad\u30fc\u304c\u7121\u52b9\u3067\u3059", - "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_route": "\u63d0\u4f9b\u3055\u308c\u305f\u60c5\u5831\u3067\u30eb\u30fc\u30c8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/ukraine_alarm/translations/ca.json b/homeassistant/components/ukraine_alarm/translations/ca.json index cd5cda1cc8b..c650423723d 100644 --- a/homeassistant/components/ukraine_alarm/translations/ca.json +++ b/homeassistant/components/ukraine_alarm/translations/ca.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada", + "cannot_connect": "Ha fallat la connexi\u00f3", + "max_regions": "Es poden configurar un m\u00e0xim de 5 regions", + "rate_limit": "Massa peticions", + "timeout": "Temps m\u00e0xim d'espera per establir la connexi\u00f3 esgotat", + "unknown": "Error inesperat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -30,9 +35,10 @@ }, "user": { "data": { - "api_key": "Clau API" + "api_key": "Clau API", + "region": "Regi\u00f3" }, - "description": "Configura la integraci\u00f3 d'Alarma Ucra\u00efna. Per generar la clau API, v\u00e9s a {api_url}" + "description": "Escull l'estat a monitorar" } } } diff --git a/homeassistant/components/ukraine_alarm/translations/de.json b/homeassistant/components/ukraine_alarm/translations/de.json index 75dc7a7856a..2eb99ee692f 100644 --- a/homeassistant/components/ukraine_alarm/translations/de.json +++ b/homeassistant/components/ukraine_alarm/translations/de.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "Standort ist bereits konfiguriert" + "already_configured": "Standort ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "max_regions": "Es k\u00f6nnen maximal 5 Regionen konfiguriert werden", + "rate_limit": "Zu viele Anfragen", + "timeout": "Zeit\u00fcberschreitung beim Verbindungsaufbau", + "unknown": "Unerwarteter Fehler" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -30,9 +35,10 @@ }, "user": { "data": { - "api_key": "API-Schl\u00fcssel" + "api_key": "API-Schl\u00fcssel", + "region": "Region" }, - "description": "Richte die Ukraine Alarm-Integration ein. Um einen API-Schl\u00fcssel zu generieren, gehe zu {api_url}" + "description": "Zu \u00fcberwachendes Bundesland ausw\u00e4hlen" } } } diff --git a/homeassistant/components/ukraine_alarm/translations/el.json b/homeassistant/components/ukraine_alarm/translations/el.json index 57e40095d37..61b40412415 100644 --- a/homeassistant/components/ukraine_alarm/translations/el.json +++ b/homeassistant/components/ukraine_alarm/translations/el.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + "already_configured": "\u0397 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "max_regions": "\u039c\u03c0\u03bf\u03c1\u03bf\u03cd\u03bd \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03bf\u03cd\u03bd \u03ad\u03c9\u03c2 5 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ad\u03c2", + "rate_limit": "\u03a0\u03ac\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03ac \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1", + "timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", @@ -30,7 +35,8 @@ }, "user": { "data": { - "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "region": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae\u03c2" }, "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c3\u03c5\u03bd\u03b1\u03b3\u03b5\u03c1\u03bc\u03bf\u03cd \u03c4\u03b7\u03c2 \u039f\u03c5\u03ba\u03c1\u03b1\u03bd\u03af\u03b1\u03c2. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {api_url}" } diff --git a/homeassistant/components/ukraine_alarm/translations/es.json b/homeassistant/components/ukraine_alarm/translations/es.json new file mode 100644 index 00000000000..7fa891bde3e --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/es.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "region": "Regi\u00f3n" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/et.json b/homeassistant/components/ukraine_alarm/translations/et.json index 45649336837..452b9d8cd11 100644 --- a/homeassistant/components/ukraine_alarm/translations/et.json +++ b/homeassistant/components/ukraine_alarm/translations/et.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "Asukoht on juba m\u00e4\u00e4ratud" + "already_configured": "Asukoht on juba m\u00e4\u00e4ratud", + "cannot_connect": "\u00dchendamine nurjus", + "max_regions": "Seadistada saab kuni 5 piirkonda", + "rate_limit": "Liiga palju taotlusi", + "timeout": "\u00dchenduse ajal\u00f5pp", + "unknown": "Ootamatu t\u00f5rge" }, "error": { "cannot_connect": "\u00dchendamine nurjus", @@ -30,9 +35,10 @@ }, "user": { "data": { - "api_key": "API v\u00f5ti" + "api_key": "API v\u00f5ti", + "region": "Piirkond" }, - "description": "Loo Ukraina h\u00e4ire integreerimine. API-v\u00f5tme loomiseks ava {api_url}" + "description": "Vali j\u00e4lgitav oblast" } } } diff --git a/homeassistant/components/ukraine_alarm/translations/fr.json b/homeassistant/components/ukraine_alarm/translations/fr.json index 51706dfee5f..6cafde0e4d0 100644 --- a/homeassistant/components/ukraine_alarm/translations/fr.json +++ b/homeassistant/components/ukraine_alarm/translations/fr.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", + "max_regions": "Un maximum de cinq r\u00e9gions peuvent \u00eatre configur\u00e9es", + "rate_limit": "Trop de demandes", + "timeout": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9", + "unknown": "Erreur inattendue" }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -30,9 +35,10 @@ }, "user": { "data": { - "api_key": "Cl\u00e9 d'API" + "api_key": "Cl\u00e9 d'API", + "region": "R\u00e9gion" }, - "description": "Configurez l'int\u00e9gration Ukraine Alarm. Pour g\u00e9n\u00e9rer une cl\u00e9 d'API, rendez-vous sur {api_url}" + "description": "Choisissez l'\u00c9tat \u00e0 surveiller" } } } diff --git a/homeassistant/components/ukraine_alarm/translations/hu.json b/homeassistant/components/ukraine_alarm/translations/hu.json index fb00a32a507..e72d6140c01 100644 --- a/homeassistant/components/ukraine_alarm/translations/hu.json +++ b/homeassistant/components/ukraine_alarm/translations/hu.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "max_regions": "Legfeljebb 5 r\u00e9gi\u00f3 konfigur\u00e1lhat\u00f3", + "rate_limit": "T\u00fal sok k\u00e9r\u00e9s", + "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -30,7 +35,8 @@ }, "user": { "data": { - "api_key": "API kulcs" + "api_key": "API kulcs", + "region": "R\u00e9gi\u00f3" }, "description": "\u00c1ll\u00edtsa be az Ukraine Alarm integr\u00e1ci\u00f3t. API-kulcs l\u00e9trehoz\u00e1s\u00e1hoz l\u00e1togasson el ide: {api_url}" } diff --git a/homeassistant/components/ukraine_alarm/translations/id.json b/homeassistant/components/ukraine_alarm/translations/id.json index 6c10857bc80..bd453d78e06 100644 --- a/homeassistant/components/ukraine_alarm/translations/id.json +++ b/homeassistant/components/ukraine_alarm/translations/id.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "Lokasi sudah dikonfigurasi" + "already_configured": "Lokasi sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "max_regions": "Maksimal 5 wilayah dapat dikonfigurasi", + "rate_limit": "Terlalu banyak permintaan", + "timeout": "Tenggang waktu membuat koneksi habis", + "unknown": "Kesalahan yang tidak diharapkan" }, "error": { "cannot_connect": "Gagal terhubung", @@ -30,9 +35,10 @@ }, "user": { "data": { - "api_key": "Kunci API" + "api_key": "Kunci API", + "region": "Wilayah" }, - "description": "Siapkan integrasi Alarm Ukraina. Untuk membuat kunci API, buka {api_url}" + "description": "Pilih negara bagian untuk dipantau" } } } diff --git a/homeassistant/components/ukraine_alarm/translations/it.json b/homeassistant/components/ukraine_alarm/translations/it.json index 23d0f2a12a5..4994cf45d33 100644 --- a/homeassistant/components/ukraine_alarm/translations/it.json +++ b/homeassistant/components/ukraine_alarm/translations/it.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "La posizione \u00e8 gi\u00e0 configurata" + "already_configured": "La posizione \u00e8 gi\u00e0 configurata", + "cannot_connect": "Impossibile connettersi", + "max_regions": "\u00c8 possibile configurare al massimo 5 regioni", + "rate_limit": "Troppe richieste", + "timeout": "Tempo scaduto per stabile la connessione.", + "unknown": "Errore imprevisto" }, "error": { "cannot_connect": "Impossibile connettersi", @@ -30,9 +35,10 @@ }, "user": { "data": { - "api_key": "Chiave API" + "api_key": "Chiave API", + "region": "Regione" }, - "description": "Imposta l'integrazione di Ukraine Alarm. Per generare una chiave API vai su {api_url}" + "description": "Scegli lo stato da monitorare" } } } diff --git a/homeassistant/components/ukraine_alarm/translations/ja.json b/homeassistant/components/ukraine_alarm/translations/ja.json index 303ac7df051..b879cf4541e 100644 --- a/homeassistant/components/ukraine_alarm/translations/ja.json +++ b/homeassistant/components/ukraine_alarm/translations/ja.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "max_regions": "\u6700\u59275\u3064\u306e\u30ea\u30fc\u30b8\u30e7\u30f3\u3092\u8a2d\u5b9a\u53ef\u80fd", + "rate_limit": "\u30ea\u30af\u30a8\u30b9\u30c8\u304c\u591a\u3059\u304e\u307e\u3059", + "timeout": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", @@ -30,7 +35,8 @@ }, "user": { "data": { - "api_key": "API\u30ad\u30fc" + "api_key": "API\u30ad\u30fc", + "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" }, "description": "\u30a6\u30af\u30e9\u30a4\u30ca\u306e\u8b66\u5831\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001 {api_url} \u306b\u79fb\u52d5\u3057\u3066\u304f\u3060\u3055\u3044" } diff --git a/homeassistant/components/ukraine_alarm/translations/nl.json b/homeassistant/components/ukraine_alarm/translations/nl.json index 6ec93b0526d..ab9973a0618 100644 --- a/homeassistant/components/ukraine_alarm/translations/nl.json +++ b/homeassistant/components/ukraine_alarm/translations/nl.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd." + "already_configured": "Locatie is al geconfigureerd.", + "cannot_connect": "Kan geen verbinding maken", + "max_regions": "Er kunnen maximaal 5 regio's worden geconfigureerd", + "rate_limit": "Te veel verzoeken", + "timeout": "Time-out bij het maken van verbinding", + "unknown": "Onverwachte fout" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -30,9 +35,10 @@ }, "user": { "data": { - "api_key": "API-sleutel" + "api_key": "API-sleutel", + "region": "Regio" }, - "description": "Stel de Oekra\u00efne Alarm integratie in. Om een API sleutel te genereren ga naar {api_url}" + "description": "Kies staat om te monitoren" } } } diff --git a/homeassistant/components/ukraine_alarm/translations/no.json b/homeassistant/components/ukraine_alarm/translations/no.json index b7ee3275920..10a717c72ec 100644 --- a/homeassistant/components/ukraine_alarm/translations/no.json +++ b/homeassistant/components/ukraine_alarm/translations/no.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "Plasseringen er allerede konfigurert" + "already_configured": "Plasseringen er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", + "max_regions": "Maks 5 regioner kan konfigureres", + "rate_limit": "For mange foresp\u00f8rsler", + "timeout": "Tidsavbrudd oppretter forbindelse", + "unknown": "Uventet feil" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -30,9 +35,10 @@ }, "user": { "data": { - "api_key": "API-n\u00f8kkel" + "api_key": "API-n\u00f8kkel", + "region": "Region" }, - "description": "Sett opp Ukraina Alarm-integrasjonen. For \u00e5 generere en API-n\u00f8kkel, g\u00e5 til {api_url}" + "description": "Velg tilstand \u00e5 overv\u00e5ke" } } } diff --git a/homeassistant/components/ukraine_alarm/translations/pl.json b/homeassistant/components/ukraine_alarm/translations/pl.json index 9ed67385f75..6b970b759ed 100644 --- a/homeassistant/components/ukraine_alarm/translations/pl.json +++ b/homeassistant/components/ukraine_alarm/translations/pl.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "max_regions": "Mo\u017cna skonfigurowa\u0107 maksymalnie 5 region\u00f3w", + "rate_limit": "Zbyt wiele \u017c\u0105da\u0144", + "timeout": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -30,9 +35,10 @@ }, "user": { "data": { - "api_key": "Klucz API" + "api_key": "Klucz API", + "region": "Region" }, - "description": "Skonfiguruj integracj\u0119 Alarmy w Ukrainie. Aby wygenerowa\u0107 klucz API, przejd\u017a do {api_url}." + "description": "Wybierz rejon do monitorowania" } } } diff --git a/homeassistant/components/ukraine_alarm/translations/pt-BR.json b/homeassistant/components/ukraine_alarm/translations/pt-BR.json index 92c5eb20b6d..316a8e96563 100644 --- a/homeassistant/components/ukraine_alarm/translations/pt-BR.json +++ b/homeassistant/components/ukraine_alarm/translations/pt-BR.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "O local j\u00e1 est\u00e1 configurado" + "already_configured": "O local j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falhou ao conectar", + "max_regions": "M\u00e1ximo de 5 regi\u00f5es podem ser configuradas", + "rate_limit": "Excesso de pedidos", + "timeout": "Tempo limite estabelecendo conex\u00e3o", + "unknown": "Erro inesperado" }, "error": { "cannot_connect": "Falhou ao conectar", @@ -30,9 +35,10 @@ }, "user": { "data": { - "api_key": "Chave de API" + "api_key": "Chave de API", + "region": "Regi\u00e3o" }, - "description": "Configure a integra\u00e7\u00e3o do Alarme da Ucr\u00e2nia. Para gerar uma chave de API, acesse {api_url}" + "description": "Escolha o estado para monitorar" } } } diff --git a/homeassistant/components/ukraine_alarm/translations/ru.json b/homeassistant/components/ukraine_alarm/translations/ru.json index cd852ad0745..fdb9d4b3f23 100644 --- a/homeassistant/components/ukraine_alarm/translations/ru.json +++ b/homeassistant/components/ukraine_alarm/translations/ru.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0440\u0435\u0433\u0438\u043e\u043d\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "max_regions": "\u041c\u043e\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c 5 \u0440\u0435\u0433\u0438\u043e\u043d\u043e\u0432.", + "rate_limit": "\u0421\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432.", + "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -12,27 +17,28 @@ "step": { "community": { "data": { - "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + "region": "\u0420\u0430\u0439\u043e\u043d" }, - "description": "\u0415\u0441\u043b\u0438 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0438\u0442\u044c \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0437\u0430 \u0448\u0442\u0430\u0442\u043e\u043c \u0438 \u043e\u043a\u0440\u0443\u0433\u043e\u043c, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0435\u0433\u043e \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0435 \u0441\u043e\u043e\u0431\u0449\u0435\u0441\u0442\u0432\u043e." + "description": "\u0415\u0441\u043b\u0438 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0438\u0442\u044c \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0437\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u044c\u044e \u0438 \u0433\u0440\u043e\u043c\u0430\u0434\u043e\u0439, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0435\u0451 \u0440\u0430\u0439\u043e\u043d" }, "district": { "data": { - "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + "region": "\u0413\u0440\u043e\u043c\u0430\u0434\u0430" }, - "description": "\u0415\u0441\u043b\u0438 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0438\u0442\u044c \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0437\u0430 \u0448\u0442\u0430\u0442\u043e\u043c, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0435\u0433\u043e \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u0440\u0430\u0439\u043e\u043d." + "description": "\u0415\u0441\u043b\u0438 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0438\u0442\u044c \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0437\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u044c\u044e, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0435\u0451 \u0433\u0440\u043e\u043c\u0430\u0434\u0443" }, "state": { "data": { - "region": "\u0420\u0435\u0433\u0438\u043e\u043d" + "region": "\u041e\u0431\u043b\u0430\u0441\u0442\u044c" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u0442\u0430\u0442 \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430." + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u0434\u043b\u044f \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f" }, "user": { "data": { - "api_key": "\u041a\u043b\u044e\u0447 API" + "api_key": "\u041a\u043b\u044e\u0447 API", + "region": "\u041e\u0431\u043b\u0430\u0441\u0442\u044c" }, - "description": "\u0427\u0442\u043e\u0431\u044b \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 {api_url}" + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u0434\u043b\u044f \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f" } } } diff --git a/homeassistant/components/ukraine_alarm/translations/uk.json b/homeassistant/components/ukraine_alarm/translations/uk.json new file mode 100644 index 00000000000..2d5e881e41b --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/uk.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u0440\u0435\u0433\u0456\u043e\u043d\u0443 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "max_regions": "\u041c\u043e\u0436\u043d\u0430 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c 5 \u0440\u0435\u0433\u0456\u043e\u043d\u0456\u0432", + "rate_limit": "\u0417\u0430\u0431\u0430\u0433\u0430\u0442\u043e \u0437\u0430\u043f\u0438\u0442\u0456\u0432", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0437\u2019\u0454\u0434\u043d\u0430\u043d\u043d\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0437\u2019\u0454\u0434\u043d\u0430\u043d\u043d\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "community": { + "data": { + "region": "\u0420\u0430\u0439\u043e\u043d" + }, + "description": "\u042f\u043a\u0449\u043e \u0432\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0432\u0430\u0442\u0438 \u043d\u0435 \u0442\u0456\u043b\u044c\u043a\u0438 \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u0442\u0430 \u0433\u0440\u043e\u043c\u0430\u0434\u0443, \u043e\u0431\u0435\u0440\u0456\u0442\u044c \u0457\u0457 \u0440\u0430\u0439\u043e\u043d" + }, + "district": { + "data": { + "region": "\u0413\u0440\u043e\u043c\u0430\u0434\u0430" + }, + "description": "\u042f\u043a\u0449\u043e \u0432\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0432\u0430\u0442\u0438 \u043d\u0435 \u0442\u0456\u043b\u044c\u043a\u0438 \u043e\u0431\u043b\u0430\u0441\u0442\u044c, \u043e\u0431\u0435\u0440\u0456\u0442\u044c \u0457\u0457 \u0433\u0440\u043e\u043c\u0430\u0434\u0443" + }, + "state": { + "data": { + "region": "\u041e\u0431\u043b\u0430\u0441\u0442\u044c" + }, + "description": "\u041e\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u0434\u043b\u044f \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f" + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "region": "\u041e\u0431\u043b\u0430\u0441\u0442\u044c" + }, + "description": "\u041e\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u0434\u043b\u044f \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/zh-Hans.json b/homeassistant/components/ukraine_alarm/translations/zh-Hans.json new file mode 100644 index 00000000000..e4f7da73890 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/zh-Hans.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "max_regions": "\u6700\u591a\u53ef\u914d\u7f6e 5 \u4e2a\u533a\u57df", + "rate_limit": "\u8bf7\u6c42\u8fc7\u591a", + "timeout": "\u5efa\u7acb\u8fde\u63a5\u8d85\u65f6", + "unknown": "\u610f\u5916\u7684\u9519\u8bef" + }, + "step": { + "user": { + "data": { + "region": "\u5730\u533a" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/zh-Hant.json b/homeassistant/components/ukraine_alarm/translations/zh-Hant.json index 0704a45dd56..33e75d23df0 100644 --- a/homeassistant/components/ukraine_alarm/translations/zh-Hant.json +++ b/homeassistant/components/ukraine_alarm/translations/zh-Hant.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "max_regions": "\u6700\u9ad8\u53ef\u8a2d\u5b9a 5 \u500b\u5340\u57df", + "rate_limit": "\u8acb\u6c42\u6b21\u6578\u904e\u591a", + "timeout": "\u5efa\u7acb\u9023\u7dda\u903e\u6642", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -30,9 +35,10 @@ }, "user": { "data": { - "api_key": "API \u91d1\u9470" + "api_key": "API \u91d1\u9470", + "region": "\u5340\u57df" }, - "description": "\u8a2d\u5b9a\u70cf\u514b\u862d\u8b66\u5831\u6574\u5408\uff0c\u8acb\u9023\u7dda\u81f3 {api_url} \u4ee5\u53d6\u5f97 API \u91d1\u9470\u3002" + "description": "\u9078\u64c7\u6240\u8981\u76e3\u8996\u7684\u72c0\u614b" } } } diff --git a/homeassistant/components/vulcan/translations/ja.json b/homeassistant/components/vulcan/translations/ja.json index 1f97968e9ae..2f83f44ab53 100644 --- a/homeassistant/components/vulcan/translations/ja.json +++ b/homeassistant/components/vulcan/translations/ja.json @@ -3,6 +3,7 @@ "abort": { "all_student_already_configured": "\u3059\u3079\u3066\u306e\u751f\u5f92\u306f\u3059\u3067\u306b\u8ffd\u52a0\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "already_configured": "\u305d\u306e\u5b66\u751f\u306f\u3059\u3067\u306b\u8ffd\u52a0\u3055\u308c\u3066\u3044\u307e\u3059\u3002", + "no_matching_entries": "\u4e00\u81f4\u3059\u308b\u30a8\u30f3\u30c8\u30ea\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002\u5225\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u4f7f\u7528\u3059\u308b\u304b\u3001outdated student\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044..", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f" }, "error": { @@ -42,7 +43,8 @@ "pin": "\u30d4\u30f3", "region": "\u30b7\u30f3\u30dc\u30eb", "token": "\u30c8\u30fc\u30af\u30f3" - } + }, + "description": "\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u767b\u9332\u30da\u30fc\u30b8\u3092\u4f7f\u7528\u3057\u3066\u3001Vulcan\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3059\u3002" }, "select_saved_credentials": { "data": { From 3d2f4e31afb8fb739534400afb6d94b2d0f614a5 Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Sat, 14 May 2022 19:06:05 -0700 Subject: [PATCH 0512/3516] Add diagnostics for TotalConnect (#71506) --- .../components/totalconnect/diagnostics.py | 113 ++++++++++++++++++ tests/components/totalconnect/common.py | 25 ++++ .../totalconnect/test_diagnostics.py | 32 +++++ 3 files changed, 170 insertions(+) create mode 100644 homeassistant/components/totalconnect/diagnostics.py create mode 100644 tests/components/totalconnect/test_diagnostics.py diff --git a/homeassistant/components/totalconnect/diagnostics.py b/homeassistant/components/totalconnect/diagnostics.py new file mode 100644 index 00000000000..4a9a73c89a9 --- /dev/null +++ b/homeassistant/components/totalconnect/diagnostics.py @@ -0,0 +1,113 @@ +"""Provides diagnostics for TotalConnect.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +TO_REDACT = [ + "username", + "Password", + "Usercode", + "UserID", + "Serial Number", + "serial_number", + "sensor_serial_number", +] + +# Private variable access needed for diagnostics +# pylint: disable=protected-access + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + client = hass.data[DOMAIN][config_entry.entry_id].client + + data: dict[str, Any] = {} + data["client"] = { + "auto_bypass_low_battery": client.auto_bypass_low_battery, + "module_flags": client._module_flags, + "retry_delay": client.retry_delay, + "invalid_credentials": client._invalid_credentials, + } + + data["user"] = { + "master": client._user._master_user, + "user_admin": client._user._user_admin, + "config_admin": client._user._config_admin, + "security_problem": client._user.security_problem(), + "features": client._user._features, + } + + data["locations"] = [] + for location in client.locations.values(): + new_location = { + "location_id": location.location_id, + "name": location.location_name, + "module_flags": location._module_flags, + "security_device_id": location.security_device_id, + "ac_loss": location.ac_loss, + "low_battery": location.low_battery, + "auto_bypass_low_battery": location.auto_bypass_low_battery, + "cover_tampered": location.cover_tampered, + "arming_state": location.arming_state, + } + + new_location["devices"] = [] + for device in location.devices.values(): + new_device = { + "device_id": device.deviceid, + "name": device.name, + "class_id": device.class_id, + "serial_number": device.serial_number, + "security_panel_type_id": device.security_panel_type_id, + "serial_text": device.serial_text, + "flags": device.flags, + } + new_location["devices"].append(new_device) + + new_location["partitions"] = [] + for partition in location.partitions.values(): + new_partition = { + "partition_id": partition.partitionid, + "name": partition.name, + "is_stay_armed": partition.is_stay_armed, + "is_fire_enabled": partition.is_fire_enabled, + "is_common_enabled": partition.is_common_enabled, + "is_locked": partition.is_locked, + "is_new_partition": partition.is_new_partition, + "is_night_stay_enabled": partition.is_night_stay_enabled, + "exit_delay_timer": partition.exit_delay_timer, + } + new_location["partitions"].append(new_partition) + + new_location["zones"] = [] + for zone in location.zones.values(): + new_zone = { + "zone_id": zone.zoneid, + "description": zone.description, + "partition": zone.partition, + "status": zone.status, + "zone_type_id": zone.zone_type_id, + "can_be_bypassed": zone.can_be_bypassed, + "battery_level": zone.battery_level, + "signal_strength": zone.signal_strength, + "sensor_serial_number": zone.sensor_serial_number, + "loop_number": zone.loop_number, + "response_type": zone.response_type, + "alarm_report_state": zone.alarm_report_state, + "supervision_type": zone.supervision_type, + "chime_state": zone.chime_state, + "device_type": zone.device_type, + } + new_location["zones"].append(new_zone) + + data["locations"].append(new_location) + + return async_redact_data(data, TO_REDACT) diff --git a/tests/components/totalconnect/common.py b/tests/components/totalconnect/common.py index ec0c182895f..65b10718fd5 100644 --- a/tests/components/totalconnect/common.py +++ b/tests/components/totalconnect/common.py @@ -345,3 +345,28 @@ async def setup_platform(hass, platform): await hass.async_block_till_done() return mock_entry + + +async def init_integration(hass): + """Set up the TotalConnect integration.""" + # first set up a config entry and add it to hass + mock_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_DATA) + mock_entry.add_to_hass(hass) + + responses = [ + RESPONSE_AUTHENTICATE, + RESPONSE_PARTITION_DETAILS, + RESPONSE_GET_ZONE_DETAILS_SUCCESS, + RESPONSE_DISARMED, + RESPONSE_DISARMED, + ] + + with patch( + TOTALCONNECT_REQUEST, + side_effect=responses, + ) as mock_request: + await hass.config_entries.async_setup(mock_entry.entry_id) + assert mock_request.call_count == 5 + await hass.async_block_till_done() + + return mock_entry diff --git a/tests/components/totalconnect/test_diagnostics.py b/tests/components/totalconnect/test_diagnostics.py new file mode 100644 index 00000000000..9c6b1975097 --- /dev/null +++ b/tests/components/totalconnect/test_diagnostics.py @@ -0,0 +1,32 @@ +"""Test TotalConnect diagnostics.""" + +from homeassistant.components.diagnostics import REDACTED + +from .common import LOCATION_ID, init_integration + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_entry_diagnostics(hass, hass_client): + """Test config entry diagnostics.""" + entry = await init_integration(hass) + + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) + + client = result["client"] + assert client["invalid_credentials"] is False + + user = result["user"] + assert user["master"] is False + + location = result["locations"][0] + assert location["location_id"] == LOCATION_ID + + device = location["devices"][0] + assert device["serial_number"] == REDACTED + + partition = location["partitions"][0] + assert partition["name"] == "Test1" + + zone = location["zones"][0] + assert zone["zone_id"] == "1" From 8f3343750786017a2ecf9783b86af2ca601a4709 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 May 2022 22:04:31 -0500 Subject: [PATCH 0513/3516] Correct typo in internal logbook function names (#71882) --- homeassistant/components/logbook/queries.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/logbook/queries.py b/homeassistant/components/logbook/queries.py index e8e691f2787..29dac31a432 100644 --- a/homeassistant/components/logbook/queries.py +++ b/homeassistant/components/logbook/queries.py @@ -160,7 +160,7 @@ def _entities_stmt( ) stmt = stmt.add_criteria( lambda s: s.where(_apply_event_entity_id_matchers(entity_ids)).union_all( - _states_query_for_entitiy_ids(start_day, end_day, entity_ids), + _states_query_for_entity_ids(start_day, end_day, entity_ids), _select_events_context_only().where( Events.context_id.in_( _select_entities_context_ids_sub_query( @@ -218,7 +218,7 @@ def _single_entity_stmt( | EventData.shared_data.like(entity_id_like) ) .union_all( - _states_query_for_entitiy_id(start_day, end_day, entity_id), + _states_query_for_entity_id(start_day, end_day, entity_id), _select_events_context_only().where( Events.context_id.in_( _select_entity_context_ids_sub_query( @@ -304,13 +304,13 @@ def _states_query_for_context_id(start_day: dt, end_day: dt, context_id: str) -> ) -def _states_query_for_entitiy_id(start_day: dt, end_day: dt, entity_id: str) -> Query: +def _states_query_for_entity_id(start_day: dt, end_day: dt, entity_id: str) -> Query: return _apply_states_filters( _apply_entities_hints(_select_states()), start_day, end_day ).where(States.entity_id == entity_id) -def _states_query_for_entitiy_ids( +def _states_query_for_entity_ids( start_day: dt, end_day: dt, entity_ids: list[str] ) -> Query: return _apply_states_filters( From 6a6d31180b0107242657edbfac741f4ef5dd6db6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 15 May 2022 05:08:30 +0200 Subject: [PATCH 0514/3516] Motion blinds restore angle (#71790) * restore tilt for TiltDevices * add tilt option to set_absolute_position * improve set_absolute_position service * fix styling --- .../components/motion_blinds/cover.py | 18 ++++++++++++++++-- .../components/motion_blinds/manifest.json | 2 +- .../components/motion_blinds/services.yaml | 12 +++++++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index ccd4043faee..e09bb531208 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -71,6 +71,7 @@ TDBU_DEVICE_MAP = { SET_ABSOLUTE_POSITION_SCHEMA = { vol.Required(ATTR_ABSOLUTE_POSITION): vol.All(cv.positive_int, vol.Range(max=100)), + vol.Optional(ATTR_TILT_POSITION): vol.All(cv.positive_int, vol.Range(max=100)), vol.Optional(ATTR_WIDTH): vol.All(cv.positive_int, vol.Range(max=100)), } @@ -163,6 +164,8 @@ async def async_setup_entry( class MotionPositionDevice(CoordinatorEntity, CoverEntity): """Representation of a Motion Blind Device.""" + _restore_tilt = False + def __init__(self, coordinator, blind, device_class, sw_version): """Initialize the blind.""" super().__init__(coordinator) @@ -287,16 +290,25 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): position = kwargs[ATTR_POSITION] async with self._api_lock: await self.hass.async_add_executor_job( - self._blind.Set_position, 100 - position + self._blind.Set_position, + 100 - position, + None, + self._restore_tilt, ) await self.async_request_position_till_stop() async def async_set_absolute_position(self, **kwargs): """Move the cover to a specific absolute position (see TDBU).""" position = kwargs[ATTR_ABSOLUTE_POSITION] + angle = kwargs.get(ATTR_TILT_POSITION) + if angle is not None: + angle = angle * 180 / 100 async with self._api_lock: await self.hass.async_add_executor_job( - self._blind.Set_position, 100 - position + self._blind.Set_position, + 100 - position, + angle, + self._restore_tilt, ) await self.async_request_position_till_stop() @@ -309,6 +321,8 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): class MotionTiltDevice(MotionPositionDevice): """Representation of a Motion Blind Device.""" + _restore_tilt = True + @property def current_cover_tilt_position(self): """ diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 5ec39d8c638..1e8ad0eb0a1 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,7 +3,7 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.6.5"], + "requirements": ["motionblinds==0.6.7"], "dependencies": ["network"], "dhcp": [ { "registered_devices": true }, diff --git a/homeassistant/components/motion_blinds/services.yaml b/homeassistant/components/motion_blinds/services.yaml index 1ee60923332..d37d6bb5be8 100644 --- a/homeassistant/components/motion_blinds/services.yaml +++ b/homeassistant/components/motion_blinds/services.yaml @@ -14,8 +14,17 @@ set_absolute_position: required: true selector: number: - min: 1 + min: 0 max: 100 + unit_of_measurement: "%" + tilt_position: + name: Tilt position + description: Tilt position to move to. + selector: + number: + min: 0 + max: 100 + unit_of_measurement: "%" width: name: Width description: Specify the width that is covered, only for TDBU Combined entities. @@ -23,3 +32,4 @@ set_absolute_position: number: min: 1 max: 100 + unit_of_measurement: "%" diff --git a/requirements_all.txt b/requirements_all.txt index e1bde3fb6ca..457c2e9ec1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1035,7 +1035,7 @@ mitemp_bt==0.0.5 moehlenhoff-alpha2==1.1.2 # homeassistant.components.motion_blinds -motionblinds==0.6.5 +motionblinds==0.6.7 # homeassistant.components.motioneye motioneye-client==0.3.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2d7267149f..dbb54b45643 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -712,7 +712,7 @@ minio==5.0.10 moehlenhoff-alpha2==1.1.2 # homeassistant.components.motion_blinds -motionblinds==0.6.5 +motionblinds==0.6.7 # homeassistant.components.motioneye motioneye-client==0.3.12 From 65f44bd80b148c802b5ff21a911f13b77b0ab967 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 May 2022 01:03:56 -0500 Subject: [PATCH 0515/3516] Exclude last_changed when same as last_updated for history websocket api (#71886) --- homeassistant/components/recorder/history.py | 8 ++--- homeassistant/components/recorder/models.py | 32 +++++++++----------- tests/components/history/test_init.py | 31 +++++++++++-------- 3 files changed, 37 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index f434c1d5fe2..7def35ce3ac 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -19,7 +19,7 @@ from sqlalchemy.sql.expression import literal from homeassistant.components import recorder from homeassistant.components.websocket_api.const import ( - COMPRESSED_STATE_LAST_CHANGED, + COMPRESSED_STATE_LAST_UPDATED, COMPRESSED_STATE_STATE, ) from homeassistant.core import HomeAssistant, State, split_entity_id @@ -662,12 +662,12 @@ def _sorted_states_to_dict( _process_timestamp: Callable[ [datetime], float | str ] = process_datetime_to_timestamp - attr_last_changed = COMPRESSED_STATE_LAST_CHANGED + attr_time = COMPRESSED_STATE_LAST_UPDATED attr_state = COMPRESSED_STATE_STATE else: state_class = LazyState # type: ignore[assignment] _process_timestamp = process_timestamp_to_utc_isoformat - attr_last_changed = LAST_CHANGED_KEY + attr_time = LAST_CHANGED_KEY attr_state = STATE_KEY result: dict[str, list[State | dict[str, Any]]] = defaultdict(list) @@ -742,7 +742,7 @@ def _sorted_states_to_dict( # # We use last_updated for for last_changed since its the same # - attr_last_changed: _process_timestamp(row.last_updated), + attr_time: _process_timestamp(row.last_updated), } ) prev_state = state diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 4c3832c4fc0..d64d85f3ce4 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -798,23 +798,21 @@ def row_to_compressed_state( start_time: datetime | None = None, ) -> dict[str, Any]: """Convert a database row to a compressed state.""" - if start_time: - last_changed = last_updated = start_time.timestamp() - else: - row_last_updated: datetime = row.last_updated - if ( - not (row_changed_changed := row.last_changed) - or row_last_updated == row_changed_changed - ): - last_changed = last_updated = process_datetime_to_timestamp( - row_last_updated - ) - else: - last_changed = process_datetime_to_timestamp(row_changed_changed) - last_updated = process_datetime_to_timestamp(row_last_updated) - return { + comp_state = { COMPRESSED_STATE_STATE: row.state, COMPRESSED_STATE_ATTRIBUTES: decode_attributes_from_row(row, attr_cache), - COMPRESSED_STATE_LAST_CHANGED: last_changed, - COMPRESSED_STATE_LAST_UPDATED: last_updated, } + if start_time: + comp_state[COMPRESSED_STATE_LAST_UPDATED] = start_time.timestamp() + else: + row_last_updated: datetime = row.last_updated + comp_state[COMPRESSED_STATE_LAST_UPDATED] = process_datetime_to_timestamp( + row_last_updated + ) + if ( + row_changed_changed := row.last_changed + ) and row_last_updated != row_changed_changed: + comp_state[COMPRESSED_STATE_LAST_CHANGED] = process_datetime_to_timestamp( + row_changed_changed + ) + return comp_state diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index bcbab1e21ca..23e0550d6aa 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -1133,11 +1133,12 @@ async def test_history_during_period(hass, hass_ws_client, recorder_mock): assert sensor_test_history[0]["s"] == "on" assert sensor_test_history[0]["a"] == {} assert isinstance(sensor_test_history[0]["lu"], float) - assert isinstance(sensor_test_history[0]["lc"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) assert "a" not in sensor_test_history[1] assert sensor_test_history[1]["s"] == "off" - assert isinstance(sensor_test_history[1]["lc"], float) + assert isinstance(sensor_test_history[1]["lu"], float) + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) assert sensor_test_history[2]["s"] == "on" assert sensor_test_history[2]["a"] == {} @@ -1163,10 +1164,11 @@ async def test_history_during_period(hass, hass_ws_client, recorder_mock): assert sensor_test_history[0]["s"] == "on" assert sensor_test_history[0]["a"] == {"any": "attr"} assert isinstance(sensor_test_history[0]["lu"], float) - assert isinstance(sensor_test_history[0]["lc"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) assert sensor_test_history[1]["s"] == "off" - assert isinstance(sensor_test_history[1]["lc"], float) + assert isinstance(sensor_test_history[1]["lu"], float) + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) assert sensor_test_history[1]["a"] == {"any": "attr"} assert sensor_test_history[4]["s"] == "on" @@ -1193,10 +1195,11 @@ async def test_history_during_period(hass, hass_ws_client, recorder_mock): assert sensor_test_history[0]["s"] == "on" assert sensor_test_history[0]["a"] == {"any": "attr"} assert isinstance(sensor_test_history[0]["lu"], float) - assert isinstance(sensor_test_history[0]["lc"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) assert sensor_test_history[1]["s"] == "off" - assert isinstance(sensor_test_history[1]["lc"], float) + assert isinstance(sensor_test_history[1]["lu"], float) + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) assert sensor_test_history[1]["a"] == {"any": "attr"} assert sensor_test_history[2]["s"] == "on" @@ -1331,11 +1334,11 @@ async def test_history_during_period_significant_domain( assert sensor_test_history[0]["s"] == "on" assert sensor_test_history[0]["a"] == {} assert isinstance(sensor_test_history[0]["lu"], float) - assert isinstance(sensor_test_history[0]["lc"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) assert "a" in sensor_test_history[1] assert sensor_test_history[1]["s"] == "off" - assert isinstance(sensor_test_history[1]["lc"], float) + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) assert sensor_test_history[4]["s"] == "on" assert sensor_test_history[4]["a"] == {} @@ -1361,10 +1364,11 @@ async def test_history_during_period_significant_domain( assert sensor_test_history[0]["s"] == "on" assert sensor_test_history[0]["a"] == {"temperature": "1"} assert isinstance(sensor_test_history[0]["lu"], float) - assert isinstance(sensor_test_history[0]["lc"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) assert sensor_test_history[1]["s"] == "off" - assert isinstance(sensor_test_history[1]["lc"], float) + assert isinstance(sensor_test_history[1]["lu"], float) + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) assert sensor_test_history[1]["a"] == {"temperature": "2"} assert sensor_test_history[4]["s"] == "on" @@ -1391,10 +1395,11 @@ async def test_history_during_period_significant_domain( assert sensor_test_history[0]["s"] == "on" assert sensor_test_history[0]["a"] == {"temperature": "1"} assert isinstance(sensor_test_history[0]["lu"], float) - assert isinstance(sensor_test_history[0]["lc"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) assert sensor_test_history[1]["s"] == "off" - assert isinstance(sensor_test_history[1]["lc"], float) + assert isinstance(sensor_test_history[1]["lu"], float) + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) assert sensor_test_history[1]["a"] == {"temperature": "2"} assert sensor_test_history[2]["s"] == "off" @@ -1429,7 +1434,7 @@ async def test_history_during_period_significant_domain( assert sensor_test_history[0]["s"] == "on" assert sensor_test_history[0]["a"] == {"temperature": "5"} assert sensor_test_history[0]["lu"] == later.timestamp() - assert sensor_test_history[0]["lc"] == later.timestamp() + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) async def test_history_during_period_bad_start_time( From 1f753ecd88d4453edf02154c70fc408741fc993f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 May 2022 01:04:23 -0500 Subject: [PATCH 0516/3516] Relocate sqlalchemy filter builder to recorder/filters.py (#71883) --- homeassistant/components/history/__init__.py | 129 +------------------ homeassistant/components/logbook/__init__.py | 6 +- homeassistant/components/logbook/queries.py | 8 +- homeassistant/components/recorder/filters.py | 119 +++++++++++++++++ homeassistant/components/recorder/history.py | 15 ++- tests/components/history/conftest.py | 13 +- tests/components/history/test_init.py | 70 +++++----- 7 files changed, 177 insertions(+), 183 deletions(-) create mode 100644 homeassistant/components/recorder/filters.py diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 2ebe6405a7a..27acff54f99 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -6,20 +6,17 @@ from datetime import datetime as dt, timedelta from http import HTTPStatus import logging import time -from typing import Any, Literal, cast +from typing import Literal, cast from aiohttp import web -from sqlalchemy import not_, or_ -from sqlalchemy.ext.baked import BakedQuery -from sqlalchemy.orm import Query import voluptuous as vol from homeassistant.components import frontend, websocket_api from homeassistant.components.http import HomeAssistantView -from homeassistant.components.recorder import ( - get_instance, - history, - models as history_models, +from homeassistant.components.recorder import get_instance, history +from homeassistant.components.recorder.filters import ( + Filters, + sqlalchemy_filter_from_include_exclude_conf, ) from homeassistant.components.recorder.statistics import ( list_statistic_ids, @@ -28,13 +25,9 @@ from homeassistant.components.recorder.statistics import ( from homeassistant.components.recorder.util import session_scope from homeassistant.components.websocket_api import messages from homeassistant.components.websocket_api.const import JSON_DUMP -from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entityfilter import ( - CONF_ENTITY_GLOBS, - INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, -) +from homeassistant.helpers.entityfilter import INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util @@ -46,10 +39,6 @@ HISTORY_USE_INCLUDE_ORDER = "history_use_include_order" CONF_ORDER = "use_include_order" -GLOB_TO_SQL_CHARS = { - 42: "%", # * - 46: "_", # . -} CONFIG_SCHEMA = vol.Schema( { @@ -410,112 +399,6 @@ class HistoryPeriodView(HomeAssistantView): return self.json(sorted_result) -def sqlalchemy_filter_from_include_exclude_conf(conf: ConfigType) -> Filters | None: - """Build a sql filter from config.""" - filters = Filters() - 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, []) - 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, []) - - return filters if filters.has_config else None - - -class Filters: - """Container for the configured include and exclude filters.""" - - def __init__(self) -> None: - """Initialise the include and exclude filters.""" - self.excluded_entities: list[str] = [] - self.excluded_domains: list[str] = [] - self.excluded_entity_globs: list[str] = [] - - self.included_entities: list[str] = [] - self.included_domains: list[str] = [] - self.included_entity_globs: list[str] = [] - - def apply(self, query: Query) -> Query: - """Apply the entity filter.""" - if not self.has_config: - return query - - return query.filter(self.entity_filter()) - - @property - def has_config(self) -> bool: - """Determine if there is any filter configuration.""" - return bool( - self.excluded_entities - or self.excluded_domains - or self.excluded_entity_globs - or self.included_entities - or self.included_domains - or self.included_entity_globs - ) - - def bake(self, baked_query: BakedQuery) -> None: - """Update a baked query. - - Works the same as apply on a baked_query. - """ - if not self.has_config: - return - - baked_query += lambda q: q.filter(self.entity_filter()) - - def entity_filter(self) -> Any: - """Generate the entity filter query.""" - includes = [] - if self.included_domains: - includes.append( - or_( - *[ - history_models.States.entity_id.like(f"{domain}.%") - for domain in self.included_domains - ] - ).self_group() - ) - if self.included_entities: - includes.append(history_models.States.entity_id.in_(self.included_entities)) - for glob in self.included_entity_globs: - includes.append(_glob_to_like(glob)) - - excludes = [] - if self.excluded_domains: - excludes.append( - or_( - *[ - history_models.States.entity_id.like(f"{domain}.%") - for domain in self.excluded_domains - ] - ).self_group() - ) - if self.excluded_entities: - excludes.append(history_models.States.entity_id.in_(self.excluded_entities)) - for glob in self.excluded_entity_globs: - excludes.append(_glob_to_like(glob)) - - if not includes and not excludes: - return None - - if includes and not excludes: - return or_(*includes) - - if not includes and excludes: - return not_(or_(*excludes)) - - return or_(*includes) & not_(or_(*excludes)) - - -def _glob_to_like(glob_str: str) -> Any: - """Translate glob to sql.""" - return history_models.States.entity_id.like(glob_str.translate(GLOB_TO_SQL_CHARS)) - - def _entities_may_have_state_changes_after( hass: HomeAssistant, entity_ids: Iterable, start_time: dt ) -> bool: diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 809f13ca598..4bef1f1a23d 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -17,12 +17,12 @@ import voluptuous as vol from homeassistant.components import frontend, websocket_api from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED -from homeassistant.components.history import ( +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.recorder import get_instance +from homeassistant.components.recorder.filters import ( Filters, sqlalchemy_filter_from_include_exclude_conf, ) -from homeassistant.components.http import HomeAssistantView -from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.models import ( process_datetime_to_timestamp, process_timestamp_to_utc_isoformat, diff --git a/homeassistant/components/logbook/queries.py b/homeassistant/components/logbook/queries.py index 29dac31a432..89c530aec43 100644 --- a/homeassistant/components/logbook/queries.py +++ b/homeassistant/components/logbook/queries.py @@ -3,17 +3,17 @@ from __future__ import annotations from collections.abc import Iterable from datetime import datetime as dt -from typing import Any import sqlalchemy from sqlalchemy import lambda_stmt, select, union_all from sqlalchemy.orm import Query, aliased +from sqlalchemy.sql.elements import ClauseList from sqlalchemy.sql.expression import literal from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import Select -from homeassistant.components.history import Filters from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN +from homeassistant.components.recorder.filters import Filters from homeassistant.components.recorder.models import ( ENTITY_ID_LAST_UPDATED_INDEX, LAST_UPDATED_INDEX, @@ -236,7 +236,7 @@ def _all_stmt( start_day: dt, end_day: dt, event_types: tuple[str, ...], - entity_filter: Any | None = None, + entity_filter: ClauseList | None = None, context_id: str | None = None, ) -> StatementLambdaElement: """Generate a logbook query for all entities.""" @@ -410,7 +410,7 @@ def _continuous_domain_matcher() -> sqlalchemy.or_: ).self_group() -def _not_uom_attributes_matcher() -> Any: +def _not_uom_attributes_matcher() -> ClauseList: """Prefilter ATTR_UNIT_OF_MEASUREMENT as its much faster in sql.""" return ~StateAttributes.shared_attrs.like( UNIT_OF_MEASUREMENT_JSON_LIKE diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py new file mode 100644 index 00000000000..bb19dfc6d62 --- /dev/null +++ b/homeassistant/components/recorder/filters.py @@ -0,0 +1,119 @@ +"""Provide pre-made queries on top of the recorder component.""" +from __future__ import annotations + +from sqlalchemy import not_, or_ +from sqlalchemy.ext.baked import BakedQuery +from sqlalchemy.sql.elements import ClauseList + +from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE +from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS +from homeassistant.helpers.typing import ConfigType + +from .models import States + +DOMAIN = "history" +HISTORY_FILTERS = "history_filters" + +GLOB_TO_SQL_CHARS = { + 42: "%", # * + 46: "_", # . +} + + +def sqlalchemy_filter_from_include_exclude_conf(conf: ConfigType) -> Filters | None: + """Build a sql filter from config.""" + filters = Filters() + 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, []) + 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, []) + + return filters if filters.has_config else None + + +class Filters: + """Container for the configured include and exclude filters.""" + + def __init__(self) -> None: + """Initialise the include and exclude filters.""" + self.excluded_entities: list[str] = [] + self.excluded_domains: list[str] = [] + self.excluded_entity_globs: list[str] = [] + + self.included_entities: list[str] = [] + self.included_domains: list[str] = [] + self.included_entity_globs: list[str] = [] + + @property + def has_config(self) -> bool: + """Determine if there is any filter configuration.""" + return bool( + self.excluded_entities + or self.excluded_domains + or self.excluded_entity_globs + or self.included_entities + or self.included_domains + or self.included_entity_globs + ) + + def bake(self, baked_query: BakedQuery) -> BakedQuery: + """Update a baked query. + + Works the same as apply on a baked_query. + """ + if not self.has_config: + return + + baked_query += lambda q: q.filter(self.entity_filter()) + + def entity_filter(self) -> ClauseList: + """Generate the entity filter query.""" + includes = [] + if self.included_domains: + includes.append( + or_( + *[ + States.entity_id.like(f"{domain}.%") + for domain in self.included_domains + ] + ).self_group() + ) + if self.included_entities: + includes.append(States.entity_id.in_(self.included_entities)) + for glob in self.included_entity_globs: + includes.append(_glob_to_like(glob)) + + excludes = [] + if self.excluded_domains: + excludes.append( + or_( + *[ + States.entity_id.like(f"{domain}.%") + for domain in self.excluded_domains + ] + ).self_group() + ) + if self.excluded_entities: + excludes.append(States.entity_id.in_(self.excluded_entities)) + for glob in self.excluded_entity_globs: + excludes.append(_glob_to_like(glob)) + + if not includes and not excludes: + return None + + if includes and not excludes: + return or_(*includes) + + if not includes and excludes: + return not_(or_(*excludes)) + + return or_(*includes) & not_(or_(*excludes)) + + +def _glob_to_like(glob_str: str) -> ClauseList: + """Translate glob to sql.""" + return States.entity_id.like(glob_str.translate(GLOB_TO_SQL_CHARS)) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 7def35ce3ac..316e5ab27c8 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -25,6 +25,7 @@ from homeassistant.components.websocket_api.const import ( from homeassistant.core import HomeAssistant, State, split_entity_id import homeassistant.util.dt as dt_util +from .filters import Filters from .models import ( LazyState, RecorderRuns, @@ -163,7 +164,7 @@ def get_significant_states( start_time: datetime, end_time: datetime | None = None, entity_ids: list[str] | None = None, - filters: Any | None = None, + filters: Filters | None = None, include_start_time_state: bool = True, significant_changes_only: bool = True, minimal_response: bool = False, @@ -205,7 +206,7 @@ def _query_significant_states_with_session( start_time: datetime, end_time: datetime | None = None, entity_ids: list[str] | None = None, - filters: Any = None, + filters: Filters | None = None, significant_changes_only: bool = True, no_attributes: bool = False, ) -> list[Row]: @@ -281,7 +282,7 @@ def get_significant_states_with_session( start_time: datetime, end_time: datetime | None = None, entity_ids: list[str] | None = None, - filters: Any = None, + filters: Filters | None = None, include_start_time_state: bool = True, significant_changes_only: bool = True, minimal_response: bool = False, @@ -330,7 +331,7 @@ def get_full_significant_states_with_session( start_time: datetime, end_time: datetime | None = None, entity_ids: list[str] | None = None, - filters: Any = None, + filters: Filters | None = None, include_start_time_state: bool = True, significant_changes_only: bool = True, no_attributes: bool = False, @@ -549,7 +550,7 @@ def _most_recent_state_ids_subquery(query: Query) -> Query: def _get_states_baked_query_for_all( hass: HomeAssistant, - filters: Any | None = None, + filters: Filters | None = None, no_attributes: bool = False, ) -> BakedQuery: """Baked query to get states for all entities.""" @@ -573,7 +574,7 @@ def _get_rows_with_session( utc_point_in_time: datetime, entity_ids: list[str] | None = None, run: RecorderRuns | None = None, - filters: Any | None = None, + filters: Filters | None = None, no_attributes: bool = False, ) -> list[Row]: """Return the states at a specific point in time.""" @@ -640,7 +641,7 @@ def _sorted_states_to_dict( states: Iterable[Row], start_time: datetime, entity_ids: list[str] | None, - filters: Any = None, + filters: Filters | None = None, include_start_time_state: bool = True, minimal_response: bool = False, no_attributes: bool = False, diff --git a/tests/components/history/conftest.py b/tests/components/history/conftest.py index 5e81b444393..a2916153acc 100644 --- a/tests/components/history/conftest.py +++ b/tests/components/history/conftest.py @@ -2,6 +2,7 @@ import pytest from homeassistant.components import history +from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE from homeassistant.setup import setup_component @@ -13,13 +14,13 @@ def hass_history(hass_recorder): config = history.CONFIG_SCHEMA( { history.DOMAIN: { - history.CONF_INCLUDE: { - history.CONF_DOMAINS: ["media_player"], - history.CONF_ENTITIES: ["thermostat.test"], + CONF_INCLUDE: { + CONF_DOMAINS: ["media_player"], + CONF_ENTITIES: ["thermostat.test"], }, - history.CONF_EXCLUDE: { - history.CONF_DOMAINS: ["thermostat"], - history.CONF_ENTITIES: ["media_player.test"], + CONF_EXCLUDE: { + CONF_DOMAINS: ["thermostat"], + CONF_ENTITIES: ["media_player.test"], }, } } diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 23e0550d6aa..0425c9bc2e7 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -11,7 +11,7 @@ from pytest import approx from homeassistant.components import history from homeassistant.components.recorder.history import get_significant_states from homeassistant.components.recorder.models import process_timestamp -from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES +from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE import homeassistant.core as ha from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component @@ -186,9 +186,7 @@ def test_get_significant_states_exclude_domain(hass_history): config = history.CONFIG_SCHEMA( { ha.DOMAIN: {}, - history.DOMAIN: { - history.CONF_EXCLUDE: {history.CONF_DOMAINS: ["media_player"]} - }, + history.DOMAIN: {CONF_EXCLUDE: {CONF_DOMAINS: ["media_player"]}}, } ) check_significant_states(hass, zero, four, states, config) @@ -207,9 +205,7 @@ def test_get_significant_states_exclude_entity(hass_history): config = history.CONFIG_SCHEMA( { ha.DOMAIN: {}, - history.DOMAIN: { - history.CONF_EXCLUDE: {history.CONF_ENTITIES: ["media_player.test"]} - }, + history.DOMAIN: {CONF_EXCLUDE: {CONF_ENTITIES: ["media_player.test"]}}, } ) check_significant_states(hass, zero, four, states, config) @@ -230,9 +226,9 @@ def test_get_significant_states_exclude(hass_history): { ha.DOMAIN: {}, history.DOMAIN: { - history.CONF_EXCLUDE: { - history.CONF_DOMAINS: ["thermostat"], - history.CONF_ENTITIES: ["media_player.test"], + CONF_EXCLUDE: { + CONF_DOMAINS: ["thermostat"], + CONF_ENTITIES: ["media_player.test"], } }, } @@ -257,10 +253,8 @@ def test_get_significant_states_exclude_include_entity(hass_history): { ha.DOMAIN: {}, history.DOMAIN: { - history.CONF_INCLUDE: { - history.CONF_ENTITIES: ["media_player.test", "thermostat.test"] - }, - history.CONF_EXCLUDE: {history.CONF_DOMAINS: ["thermostat"]}, + CONF_INCLUDE: {CONF_ENTITIES: ["media_player.test", "thermostat.test"]}, + CONF_EXCLUDE: {CONF_DOMAINS: ["thermostat"]}, }, } ) @@ -282,9 +276,7 @@ def test_get_significant_states_include_domain(hass_history): config = history.CONFIG_SCHEMA( { ha.DOMAIN: {}, - history.DOMAIN: { - history.CONF_INCLUDE: {history.CONF_DOMAINS: ["thermostat", "script"]} - }, + history.DOMAIN: {CONF_INCLUDE: {CONF_DOMAINS: ["thermostat", "script"]}}, } ) check_significant_states(hass, zero, four, states, config) @@ -306,9 +298,7 @@ def test_get_significant_states_include_entity(hass_history): config = history.CONFIG_SCHEMA( { ha.DOMAIN: {}, - history.DOMAIN: { - history.CONF_INCLUDE: {history.CONF_ENTITIES: ["media_player.test"]} - }, + history.DOMAIN: {CONF_INCLUDE: {CONF_ENTITIES: ["media_player.test"]}}, } ) check_significant_states(hass, zero, four, states, config) @@ -330,9 +320,9 @@ def test_get_significant_states_include(hass_history): { ha.DOMAIN: {}, history.DOMAIN: { - history.CONF_INCLUDE: { - history.CONF_DOMAINS: ["thermostat"], - history.CONF_ENTITIES: ["media_player.test"], + CONF_INCLUDE: { + CONF_DOMAINS: ["thermostat"], + CONF_ENTITIES: ["media_player.test"], } }, } @@ -359,8 +349,8 @@ def test_get_significant_states_include_exclude_domain(hass_history): { ha.DOMAIN: {}, history.DOMAIN: { - history.CONF_INCLUDE: {history.CONF_DOMAINS: ["media_player"]}, - history.CONF_EXCLUDE: {history.CONF_DOMAINS: ["media_player"]}, + CONF_INCLUDE: {CONF_DOMAINS: ["media_player"]}, + CONF_EXCLUDE: {CONF_DOMAINS: ["media_player"]}, }, } ) @@ -386,8 +376,8 @@ def test_get_significant_states_include_exclude_entity(hass_history): { ha.DOMAIN: {}, history.DOMAIN: { - history.CONF_INCLUDE: {history.CONF_ENTITIES: ["media_player.test"]}, - history.CONF_EXCLUDE: {history.CONF_ENTITIES: ["media_player.test"]}, + CONF_INCLUDE: {CONF_ENTITIES: ["media_player.test"]}, + CONF_EXCLUDE: {CONF_ENTITIES: ["media_player.test"]}, }, } ) @@ -410,13 +400,13 @@ def test_get_significant_states_include_exclude(hass_history): { ha.DOMAIN: {}, history.DOMAIN: { - history.CONF_INCLUDE: { - history.CONF_DOMAINS: ["media_player"], - history.CONF_ENTITIES: ["thermostat.test"], + CONF_INCLUDE: { + CONF_DOMAINS: ["media_player"], + CONF_ENTITIES: ["thermostat.test"], }, - history.CONF_EXCLUDE: { - history.CONF_DOMAINS: ["thermostat"], - history.CONF_ENTITIES: ["media_player.test"], + CONF_EXCLUDE: { + CONF_DOMAINS: ["thermostat"], + CONF_ENTITIES: ["media_player.test"], }, }, } @@ -503,14 +493,14 @@ def test_get_significant_states_only(hass_history): def check_significant_states(hass, zero, four, states, config): """Check if significant states are retrieved.""" filters = history.Filters() - exclude = config[history.DOMAIN].get(history.CONF_EXCLUDE) + exclude = config[history.DOMAIN].get(CONF_EXCLUDE) if exclude: - filters.excluded_entities = exclude.get(history.CONF_ENTITIES, []) - filters.excluded_domains = exclude.get(history.CONF_DOMAINS, []) - include = config[history.DOMAIN].get(history.CONF_INCLUDE) + filters.excluded_entities = exclude.get(CONF_ENTITIES, []) + filters.excluded_domains = exclude.get(CONF_DOMAINS, []) + include = config[history.DOMAIN].get(CONF_INCLUDE) if include: - filters.included_entities = include.get(history.CONF_ENTITIES, []) - filters.included_domains = include.get(history.CONF_DOMAINS, []) + filters.included_entities = include.get(CONF_ENTITIES, []) + filters.included_domains = include.get(CONF_DOMAINS, []) hist = get_significant_states(hass, zero, four, filters=filters) assert states == hist @@ -1496,7 +1486,7 @@ async def test_history_during_period_with_use_include_order( { history.DOMAIN: { history.CONF_ORDER: True, - history.CONF_INCLUDE: { + CONF_INCLUDE: { CONF_ENTITIES: sort_order, CONF_DOMAINS: ["sensor"], }, From e0bf1fba8d9fb37364af422575eeac855ab406cc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 May 2022 01:05:56 -0500 Subject: [PATCH 0517/3516] Add logbook descriptions for mobile app zone enter and exit (#71749) --- .../components/mobile_app/logbook.py | 54 +++++++++++++++++++ tests/components/mobile_app/test_logbook.py | 50 +++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 homeassistant/components/mobile_app/logbook.py create mode 100644 tests/components/mobile_app/test_logbook.py diff --git a/homeassistant/components/mobile_app/logbook.py b/homeassistant/components/mobile_app/logbook.py new file mode 100644 index 00000000000..6dd4d007e3e --- /dev/null +++ b/homeassistant/components/mobile_app/logbook.py @@ -0,0 +1,54 @@ +"""Describe mobile_app logbook events.""" +from __future__ import annotations + +from collections.abc import Callable + +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON +from homeassistant.core import Event, HomeAssistant, callback + +from .const import DOMAIN + +IOS_EVENT_ZONE_ENTERED = "ios.zone_entered" +IOS_EVENT_ZONE_EXITED = "ios.zone_exited" + +ATTR_ZONE = "zone" +ATTR_SOURCE_DEVICE_NAME = "sourceDeviceName" +ATTR_SOURCE_DEVICE_ID = "sourceDeviceID" +EVENT_TO_DESCRIPTION = { + IOS_EVENT_ZONE_ENTERED: "entered zone", + IOS_EVENT_ZONE_EXITED: "exited zone", +} + + +@callback +def async_describe_events( + hass: HomeAssistant, + async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None], +) -> None: + """Describe logbook events.""" + + @callback + def async_describe_zone_event(event: Event) -> dict[str, str]: + """Describe mobile_app logbook event.""" + data = event.data + event_description = EVENT_TO_DESCRIPTION[event.event_type] + zone_entity_id = data.get(ATTR_ZONE) + source_device_name = data.get( + ATTR_SOURCE_DEVICE_NAME, data.get(ATTR_SOURCE_DEVICE_ID) + ) + zone_name = None + zone_icon = None + if zone_entity_id and (zone_state := hass.states.get(zone_entity_id)): + zone_name = zone_state.attributes.get(ATTR_FRIENDLY_NAME) + zone_icon = zone_state.attributes.get(ATTR_ICON) + description = { + "name": source_device_name, + "message": f"{event_description} {zone_name or zone_entity_id}", + "icon": zone_icon or "mdi:crosshairs-gps", + } + if zone_entity_id: + description["entity_id"] = zone_entity_id + return description + + async_describe_event(DOMAIN, IOS_EVENT_ZONE_ENTERED, async_describe_zone_event) + async_describe_event(DOMAIN, IOS_EVENT_ZONE_EXITED, async_describe_zone_event) diff --git a/tests/components/mobile_app/test_logbook.py b/tests/components/mobile_app/test_logbook.py new file mode 100644 index 00000000000..b151bd11a26 --- /dev/null +++ b/tests/components/mobile_app/test_logbook.py @@ -0,0 +1,50 @@ +"""The tests for mobile_app logbook.""" + +from homeassistant.components.mobile_app.logbook import ( + DOMAIN, + IOS_EVENT_ZONE_ENTERED, + IOS_EVENT_ZONE_EXITED, +) +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON +from homeassistant.setup import async_setup_component + +from tests.components.logbook.common import MockRow, mock_humanify + + +async def test_humanify_ios_events(hass): + """Test humanifying ios events.""" + hass.config.components.add("recorder") + assert await async_setup_component(hass, "logbook", {}) + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + hass.states.async_set( + "zone.bad_place", + "0", + {ATTR_FRIENDLY_NAME: "passport control", ATTR_ICON: "mdi:airplane-marker"}, + ) + await hass.async_block_till_done() + + (event1, event2) = mock_humanify( + hass, + [ + MockRow( + IOS_EVENT_ZONE_ENTERED, + {"sourceDeviceName": "test_phone", "zone": "zone.happy_place"}, + ), + MockRow( + IOS_EVENT_ZONE_EXITED, + {"sourceDeviceName": "test_phone", "zone": "zone.bad_place"}, + ), + ], + ) + + assert event1["name"] == "test_phone" + assert event1["domain"] == DOMAIN + assert event1["message"] == "entered zone zone.happy_place" + assert event1["icon"] == "mdi:crosshairs-gps" + assert event1["entity_id"] == "zone.happy_place" + + assert event2["name"] == "test_phone" + assert event2["domain"] == DOMAIN + assert event2["message"] == "exited zone passport control" + assert event2["icon"] == "mdi:airplane-marker" + assert event2["entity_id"] == "zone.bad_place" From 617b0d04dcab521ce3b68b4f4956c1f341f6ea60 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sun, 15 May 2022 14:31:18 +0800 Subject: [PATCH 0518/3516] Decouple stream options from PyAV options (#71247) Co-authored-by: Allen Porter --- homeassistant/components/generic/camera.py | 19 ++++---- .../components/generic/config_flow.py | 17 ++++---- homeassistant/components/generic/const.py | 12 ------ homeassistant/components/onvif/__init__.py | 11 ++--- homeassistant/components/onvif/camera.py | 2 +- homeassistant/components/onvif/config_flow.py | 15 ++----- homeassistant/components/onvif/const.py | 3 -- homeassistant/components/stream/__init__.py | 43 ++++++++++++++++--- homeassistant/components/stream/const.py | 11 +++++ homeassistant/components/stream/worker.py | 4 ++ tests/components/generic/test_config_flow.py | 6 ++- tests/components/onvif/test_config_flow.py | 4 +- 12 files changed, 83 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 197890efad3..4886e3a0693 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -12,6 +12,11 @@ from homeassistant.components.camera import ( Camera, CameraEntityFeature, ) +from homeassistant.components.stream.const import ( + CONF_RTSP_TRANSPORT, + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, + RTSP_TRANSPORTS, +) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_AUTHENTICATION, @@ -34,14 +39,10 @@ from .const import ( CONF_CONTENT_TYPE, CONF_FRAMERATE, CONF_LIMIT_REFETCH_TO_URL_CHANGE, - CONF_RTSP_TRANSPORT, CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE, - CONF_USE_WALLCLOCK_AS_TIMESTAMPS, DEFAULT_NAME, - FFMPEG_OPTION_MAP, GET_IMAGE_TIMEOUT, - RTSP_TRANSPORTS, ) _LOGGER = logging.getLogger(__name__) @@ -63,7 +64,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( cv.small_float, cv.positive_int ), vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - vol.Optional(CONF_RTSP_TRANSPORT): vol.In(RTSP_TRANSPORTS.keys()), + vol.Optional(CONF_RTSP_TRANSPORT): vol.In(RTSP_TRANSPORTS), } ) @@ -157,14 +158,10 @@ class GenericCamera(Camera): self.content_type = device_info[CONF_CONTENT_TYPE] self.verify_ssl = device_info[CONF_VERIFY_SSL] if device_info.get(CONF_RTSP_TRANSPORT): - self.stream_options[FFMPEG_OPTION_MAP[CONF_RTSP_TRANSPORT]] = device_info[ - CONF_RTSP_TRANSPORT - ] + self.stream_options[CONF_RTSP_TRANSPORT] = device_info[CONF_RTSP_TRANSPORT] self._auth = generate_auth(device_info) if device_info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): - self.stream_options[ - FFMPEG_OPTION_MAP[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] - ] = "1" + self.stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True self._last_url = None self._last_image = None diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 0a49393d9cc..bf9499c07df 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -16,7 +16,12 @@ from httpx import HTTPStatusError, RequestError, TimeoutException import voluptuous as vol import yarl -from homeassistant.components.stream.const import SOURCE_TIMEOUT +from homeassistant.components.stream.const import ( + CONF_RTSP_TRANSPORT, + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, + RTSP_TRANSPORTS, + SOURCE_TIMEOUT, +) from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import ( CONF_AUTHENTICATION, @@ -38,15 +43,11 @@ from .const import ( CONF_CONTENT_TYPE, CONF_FRAMERATE, CONF_LIMIT_REFETCH_TO_URL_CHANGE, - CONF_RTSP_TRANSPORT, CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE, - CONF_USE_WALLCLOCK_AS_TIMESTAMPS, DEFAULT_NAME, DOMAIN, - FFMPEG_OPTION_MAP, GET_IMAGE_TIMEOUT, - RTSP_TRANSPORTS, ) _LOGGER = logging.getLogger(__name__) @@ -200,16 +201,16 @@ async def async_test_stream(hass, info) -> dict[str, str]: # For RTSP streams, prefer TCP. This code is duplicated from # homeassistant.components.stream.__init__.py:create_stream() # It may be possible & better to call create_stream() directly. - stream_options: dict[str, str] = {} + stream_options: dict[str, bool | str] = {} if isinstance(stream_source, str) and stream_source[:7] == "rtsp://": stream_options = { "rtsp_flags": "prefer_tcp", "stimeout": "5000000", } if rtsp_transport := info.get(CONF_RTSP_TRANSPORT): - stream_options[FFMPEG_OPTION_MAP[CONF_RTSP_TRANSPORT]] = rtsp_transport + stream_options[CONF_RTSP_TRANSPORT] = rtsp_transport if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): - stream_options[FFMPEG_OPTION_MAP[CONF_USE_WALLCLOCK_AS_TIMESTAMPS]] = "1" + stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True _LOGGER.debug("Attempting to open stream %s", stream_source) container = await hass.async_add_executor_job( partial( diff --git a/homeassistant/components/generic/const.py b/homeassistant/components/generic/const.py index 8ae5f16c4c4..eb0d81d493c 100644 --- a/homeassistant/components/generic/const.py +++ b/homeassistant/components/generic/const.py @@ -7,18 +7,6 @@ CONF_LIMIT_REFETCH_TO_URL_CHANGE = "limit_refetch_to_url_change" CONF_STILL_IMAGE_URL = "still_image_url" CONF_STREAM_SOURCE = "stream_source" CONF_FRAMERATE = "framerate" -CONF_RTSP_TRANSPORT = "rtsp_transport" -CONF_USE_WALLCLOCK_AS_TIMESTAMPS = "use_wallclock_as_timestamps" -FFMPEG_OPTION_MAP = { - CONF_RTSP_TRANSPORT: "rtsp_transport", - CONF_USE_WALLCLOCK_AS_TIMESTAMPS: "use_wallclock_as_timestamps", -} -RTSP_TRANSPORTS = { - "tcp": "TCP", - "udp": "UDP", - "udp_multicast": "UDP Multicast", - "http": "HTTP", -} GET_IMAGE_TIMEOUT = 10 DEFAULT_USERNAME = None diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 8956f0ae2f9..d966549a36f 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -2,6 +2,7 @@ from onvif.exceptions import ONVIFAuthError, ONVIFError, ONVIFTimeoutError from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS +from homeassistant.components.stream.const import CONF_RTSP_TRANSPORT, RTSP_TRANSPORTS from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, @@ -12,13 +13,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import ( - CONF_RTSP_TRANSPORT, - CONF_SNAPSHOT_AUTH, - DEFAULT_ARGUMENTS, - DOMAIN, - RTSP_TRANS_PROTOCOLS, -) +from .const import CONF_SNAPSHOT_AUTH, DEFAULT_ARGUMENTS, DOMAIN from .device import ONVIFDevice @@ -99,7 +94,7 @@ async def async_populate_options(hass, entry): """Populate default options for device.""" options = { CONF_EXTRA_ARGUMENTS: DEFAULT_ARGUMENTS, - CONF_RTSP_TRANSPORT: RTSP_TRANS_PROTOCOLS[0], + CONF_RTSP_TRANSPORT: next(iter(RTSP_TRANSPORTS)), } hass.config_entries.async_update_entry(entry, options=options) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 3475df241b0..6b61a37eb16 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -9,6 +9,7 @@ from yarl import URL from homeassistant.components import ffmpeg from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, get_ffmpeg_manager +from homeassistant.components.stream.const import CONF_RTSP_TRANSPORT from homeassistant.config_entries import ConfigEntry from homeassistant.const import HTTP_BASIC_AUTHENTICATION from homeassistant.core import HomeAssistant @@ -27,7 +28,6 @@ from .const import ( ATTR_SPEED, ATTR_TILT, ATTR_ZOOM, - CONF_RTSP_TRANSPORT, CONF_SNAPSHOT_AUTH, CONTINUOUS_MOVE, DIR_DOWN, diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index c4579702675..894f2fee3df 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -13,6 +13,7 @@ from zeep.exceptions import Fault from homeassistant import config_entries from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS +from homeassistant.components.stream import CONF_RTSP_TRANSPORT, RTSP_TRANSPORTS from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -22,15 +23,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback -from .const import ( - CONF_DEVICE_ID, - CONF_RTSP_TRANSPORT, - DEFAULT_ARGUMENTS, - DEFAULT_PORT, - DOMAIN, - LOGGER, - RTSP_TRANS_PROTOCOLS, -) +from .const import CONF_DEVICE_ID, DEFAULT_ARGUMENTS, DEFAULT_PORT, DOMAIN, LOGGER from .device import get_device CONF_MANUAL_INPUT = "Manually configure ONVIF device" @@ -294,9 +287,9 @@ class OnvifOptionsFlowHandler(config_entries.OptionsFlow): vol.Optional( CONF_RTSP_TRANSPORT, default=self.config_entry.options.get( - CONF_RTSP_TRANSPORT, RTSP_TRANS_PROTOCOLS[0] + CONF_RTSP_TRANSPORT, next(iter(RTSP_TRANSPORTS)) ), - ): vol.In(RTSP_TRANS_PROTOCOLS), + ): vol.In(RTSP_TRANSPORTS), } ), ) diff --git a/homeassistant/components/onvif/const.py b/homeassistant/components/onvif/const.py index 3a2e802a5a0..410088f28df 100644 --- a/homeassistant/components/onvif/const.py +++ b/homeassistant/components/onvif/const.py @@ -9,11 +9,8 @@ DEFAULT_PORT = 80 DEFAULT_ARGUMENTS = "-pred 1" CONF_DEVICE_ID = "deviceid" -CONF_RTSP_TRANSPORT = "rtsp_transport" CONF_SNAPSHOT_AUTH = "snapshot_auth" -RTSP_TRANS_PROTOCOLS = ["tcp", "udp", "udp_multicast", "http"] - ATTR_PAN = "pan" ATTR_TILT = "tilt" ATTR_ZOOM = "zoom" diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index abaf367486d..fbdfd97f9b2 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -23,7 +23,7 @@ import secrets import threading import time from types import MappingProxyType -from typing import Any, cast +from typing import Any, Final, cast import voluptuous as vol @@ -39,12 +39,15 @@ from .const import ( ATTR_STREAMS, CONF_LL_HLS, CONF_PART_DURATION, + CONF_RTSP_TRANSPORT, CONF_SEGMENT_DURATION, + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, DOMAIN, HLS_PROVIDER, MAX_SEGMENTS, OUTPUT_IDLE_TIMEOUT, RECORDER_PROVIDER, + RTSP_TRANSPORTS, SEGMENT_DURATION_ADJUSTER, STREAM_RESTART_INCREMENT, STREAM_RESTART_RESET_TIME, @@ -72,28 +75,32 @@ def redact_credentials(data: str) -> str: def create_stream( hass: HomeAssistant, stream_source: str, - options: dict[str, str], + options: dict[str, Any], stream_label: str | None = None, ) -> Stream: """Create a stream with the specified identfier based on the source url. The stream_source is typically an rtsp url (though any url accepted by ffmpeg is fine) and - options are passed into pyav / ffmpeg as options. + options (see STREAM_OPTIONS_SCHEMA) are converted and passed into pyav / ffmpeg. The stream_label is a string used as an additional message in logging. """ if DOMAIN not in hass.config.components: raise HomeAssistantError("Stream integration is not set up.") + # Convert extra stream options into PyAV options + pyav_options = convert_stream_options(options) # For RTSP streams, prefer TCP if isinstance(stream_source, str) and stream_source[:7] == "rtsp://": - options = { + pyav_options = { "rtsp_flags": "prefer_tcp", "stimeout": "5000000", - **options, + **pyav_options, } - stream = Stream(hass, stream_source, options=options, stream_label=stream_label) + stream = Stream( + hass, stream_source, options=pyav_options, stream_label=stream_label + ) hass.data[DOMAIN][ATTR_STREAMS].append(stream) return stream @@ -464,3 +471,27 @@ class Stream: def _should_retry() -> bool: """Return true if worker failures should be retried, for disabling during tests.""" return True + + +STREAM_OPTIONS_SCHEMA: Final = vol.Schema( + { + vol.Optional(CONF_RTSP_TRANSPORT): vol.In(RTSP_TRANSPORTS), + vol.Optional(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): bool, + } +) + + +def convert_stream_options(stream_options: dict[str, Any]) -> dict[str, str]: + """Convert options from stream options into PyAV options.""" + pyav_options: dict[str, str] = {} + try: + STREAM_OPTIONS_SCHEMA(stream_options) + except vol.Invalid as exc: + raise HomeAssistantError("Invalid stream options") from exc + + if rtsp_transport := stream_options.get(CONF_RTSP_TRANSPORT): + pyav_options["rtsp_transport"] = rtsp_transport + if stream_options.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): + pyav_options["use_wallclock_as_timestamps"] = "1" + + return pyav_options diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py index 50ae43df0d0..f8c9ba85d59 100644 --- a/homeassistant/components/stream/const.py +++ b/homeassistant/components/stream/const.py @@ -42,3 +42,14 @@ STREAM_RESTART_RESET_TIME = 300 # Reset wait_timeout after this many seconds CONF_LL_HLS = "ll_hls" CONF_PART_DURATION = "part_duration" CONF_SEGMENT_DURATION = "segment_duration" + +CONF_PREFER_TCP = "prefer_tcp" +CONF_RTSP_TRANSPORT = "rtsp_transport" +# The first dict entry below may be used as the default when populating options +RTSP_TRANSPORTS = { + "tcp": "TCP", + "udp": "UDP", + "udp_multicast": "UDP Multicast", + "http": "HTTP", +} +CONF_USE_WALLCLOCK_AS_TIMESTAMPS = "use_wallclock_as_timestamps" diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index bde5ab0fb05..f8d12c1cb44 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -452,6 +452,10 @@ def stream_worker( ) -> None: """Handle consuming streams.""" + if av.library_versions["libavformat"][0] >= 59 and "stimeout" in options: + # the stimeout option was renamed to timeout as of ffmpeg 5.0 + options["timeout"] = options["stimeout"] + del options["stimeout"] try: container = av.open(source, options=options, timeout=SOURCE_TIMEOUT) except av.AVError as err: diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index dd53cb8548e..82cf41c6e91 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -15,12 +15,14 @@ from homeassistant.components.generic.const import ( CONF_CONTENT_TYPE, CONF_FRAMERATE, CONF_LIMIT_REFETCH_TO_URL_CHANGE, - CONF_RTSP_TRANSPORT, CONF_STILL_IMAGE_URL, CONF_STREAM_SOURCE, - CONF_USE_WALLCLOCK_AS_TIMESTAMPS, DOMAIN, ) +from homeassistant.components.stream.const import ( + CONF_RTSP_TRANSPORT, + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, +) from homeassistant.const import ( CONF_AUTHENTICATION, CONF_NAME, diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index 0760ead2ba1..1e4dabfe7cf 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -323,12 +323,12 @@ async def test_option_flow(hass): result["flow_id"], user_input={ config_flow.CONF_EXTRA_ARGUMENTS: "", - config_flow.CONF_RTSP_TRANSPORT: config_flow.RTSP_TRANS_PROTOCOLS[1], + config_flow.CONF_RTSP_TRANSPORT: list(config_flow.RTSP_TRANSPORTS)[1], }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == { config_flow.CONF_EXTRA_ARGUMENTS: "", - config_flow.CONF_RTSP_TRANSPORT: config_flow.RTSP_TRANS_PROTOCOLS[1], + config_flow.CONF_RTSP_TRANSPORT: list(config_flow.RTSP_TRANSPORTS)[1], } From 8549af38554084605b4edd62ee80d3a12ee67749 Mon Sep 17 00:00:00 2001 From: RadekHvizdos <10856567+RadekHvizdos@users.noreply.github.com> Date: Sun, 15 May 2022 11:29:35 +0200 Subject: [PATCH 0519/3516] Add missing Shelly Cover sensors bugfix (#71831) Switching Shelly Plus 2PM from switch to cover mode results in missing sensors for Power, Voltage, Energy and Temperature. These parameters are still available in the API, but need to be accessed via "cover" key instead of "switch" key. This change adds the missing sensors. --- homeassistant/components/shelly/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index df5a75a7ed9..77c09283fbf 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -297,6 +297,9 @@ def get_rpc_key_instances(keys_dict: dict[str, Any], key: str) -> list[str]: if key in keys_dict: return [key] + if key == "switch" and "cover:0" in keys_dict: + key = "cover" + keys_list: list[str] = [] for i in range(MAX_RPC_KEY_INSTANCES): key_inst = f"{key}:{i}" From 2b637f71faaeb16fa855af578c4ab6a6b22190cb Mon Sep 17 00:00:00 2001 From: rappenze Date: Sun, 15 May 2022 15:02:05 +0200 Subject: [PATCH 0520/3516] Limit parallel requests in fibaro light (#71762) --- homeassistant/components/fibaro/light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index 08a9e651668..0115e0301c3 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -23,6 +23,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import FIBARO_DEVICES, FibaroDevice from .const import DOMAIN +PARALLEL_UPDATES = 2 + def scaleto255(value: int | None) -> int: """Scale the input value from 0-100 to 0-255.""" From 8ea5ec6f0814326b11339f56655448d7cf036af3 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 15 May 2022 15:56:45 +0200 Subject: [PATCH 0521/3516] Streamline setup of deCONZ number platform (#71840) --- homeassistant/components/deconz/number.py | 53 +++++++---------------- 1 file changed, 16 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index 12ff768cad2..81886fd3c7c 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -5,6 +5,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from pydeconz.models.event import EventType from pydeconz.models.sensor.presence import PRESENCE_DELAY, Presence from homeassistant.components.number import ( @@ -14,7 +15,6 @@ from homeassistant.components.number import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -62,48 +62,27 @@ async def async_setup_entry( gateway.entities[DOMAIN] = set() @callback - def async_add_sensor(sensors: list[Presence] | None = None) -> None: - """Add number config sensor from deCONZ.""" - entities = [] - - if sensors is None: - sensors = list(gateway.api.sensors.presence.values()) - - for sensor in sensors: - - if sensor.type.startswith("CLIP"): + def async_add_sensor(_: EventType, sensor_id: str) -> None: + """Add sensor from deCONZ.""" + sensor = gateway.api.sensors.presence[sensor_id] + if sensor.type.startswith("CLIP"): + return + for description in ENTITY_DESCRIPTIONS.get(type(sensor), []): + if ( + not hasattr(sensor, description.key) + or description.value_fn(sensor) is None + ): continue - - known_entities = set(gateway.entities[DOMAIN]) - for description in ENTITY_DESCRIPTIONS.get(type(sensor), []): - - if ( - not hasattr(sensor, description.key) - or description.value_fn(sensor) is None - ): - continue - - new_entity = DeconzNumber(sensor, gateway, description) - if new_entity.unique_id not in known_entities: - entities.append(new_entity) - - if entities: - async_add_entities(entities) + async_add_entities([DeconzNumber(sensor, gateway, description)]) config_entry.async_on_unload( - async_dispatcher_connect( - hass, - gateway.signal_new_sensor, + gateway.api.sensors.presence.subscribe( async_add_sensor, + EventType.ADDED, ) ) - - async_add_sensor( - [ - gateway.api.sensors.presence[key] - for key in sorted(gateway.api.sensors.presence, key=int) - ] - ) + for sensor_id in gateway.api.sensors.presence: + async_add_sensor(EventType.ADDED, sensor_id) class DeconzNumber(DeconzDevice, NumberEntity): From 98809675ff8cc87dbb5e165ad3096d75e69f3b44 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 May 2022 10:47:29 -0500 Subject: [PATCH 0522/3516] Convert history queries to use lambda_stmt (#71870) Co-authored-by: Paulus Schoutsen --- homeassistant/components/recorder/__init__.py | 3 +- homeassistant/components/recorder/filters.py | 11 - homeassistant/components/recorder/history.py | 454 ++++++++---------- homeassistant/components/recorder/util.py | 46 +- tests/components/recorder/test_util.py | 61 ++- 5 files changed, 313 insertions(+), 262 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index ca1ecd8c71a..4063e443e8b 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers.integration_platform import ( from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -from . import history, statistics, websocket_api +from . import statistics, websocket_api from .const import ( CONF_DB_INTEGRITY_CHECK, DATA_INSTANCE, @@ -166,7 +166,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: instance.async_register() instance.start() async_register_services(hass, instance) - history.async_setup(hass) statistics.async_setup(hass) websocket_api.async_setup(hass) await async_process_integration_platforms(hass, DOMAIN, _process_recorder_platform) diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index bb19dfc6d62..adc746379e6 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -2,7 +2,6 @@ from __future__ import annotations from sqlalchemy import not_, or_ -from sqlalchemy.ext.baked import BakedQuery from sqlalchemy.sql.elements import ClauseList from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE @@ -60,16 +59,6 @@ class Filters: or self.included_entity_globs ) - def bake(self, baked_query: BakedQuery) -> BakedQuery: - """Update a baked query. - - Works the same as apply on a baked_query. - """ - if not self.has_config: - return - - baked_query += lambda q: q.filter(self.entity_filter()) - def entity_filter(self) -> ClauseList: """Generate the entity filter query.""" includes = [] diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 316e5ab27c8..3df444faccc 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -9,13 +9,12 @@ import logging import time from typing import Any, cast -from sqlalchemy import Column, Text, and_, bindparam, func, or_ +from sqlalchemy import Column, Text, and_, func, lambda_stmt, or_, select from sqlalchemy.engine.row import Row -from sqlalchemy.ext import baked -from sqlalchemy.ext.baked import BakedQuery from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal +from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components import recorder from homeassistant.components.websocket_api.const import ( @@ -36,7 +35,7 @@ from .models import ( process_timestamp_to_utc_isoformat, row_to_compressed_state, ) -from .util import execute, session_scope +from .util import execute_stmt_lambda_element, session_scope # mypy: allow-untyped-defs, no-check-untyped-defs @@ -111,52 +110,48 @@ QUERY_STATES_NO_LAST_CHANGED = [ StateAttributes.shared_attrs, ] -HISTORY_BAKERY = "recorder_history_bakery" + +def _schema_version(hass: HomeAssistant) -> int: + return recorder.get_instance(hass).schema_version -def bake_query_and_join_attributes( - hass: HomeAssistant, no_attributes: bool, include_last_changed: bool = True -) -> tuple[Any, bool]: - """Return the initial backed query and if StateAttributes should be joined. +def lambda_stmt_and_join_attributes( + schema_version: int, no_attributes: bool, include_last_changed: bool = True +) -> tuple[StatementLambdaElement, bool]: + """Return the lambda_stmt and if StateAttributes should be joined. - Because these are baked queries the values inside the lambdas need + Because these are lambda_stmt the values inside the lambdas need to be explicitly written out to avoid caching the wrong values. """ - bakery: baked.bakery = hass.data[HISTORY_BAKERY] # If no_attributes was requested we do the query # without the attributes fields and do not join the # state_attributes table if no_attributes: if include_last_changed: - return bakery(lambda s: s.query(*QUERY_STATE_NO_ATTR)), False + return lambda_stmt(lambda: select(*QUERY_STATE_NO_ATTR)), False return ( - bakery(lambda s: s.query(*QUERY_STATE_NO_ATTR_NO_LAST_CHANGED)), + lambda_stmt(lambda: select(*QUERY_STATE_NO_ATTR_NO_LAST_CHANGED)), False, ) # If we in the process of migrating schema we do # not want to join the state_attributes table as we # do not know if it will be there yet - if recorder.get_instance(hass).schema_version < 25: + if schema_version < 25: if include_last_changed: return ( - bakery(lambda s: s.query(*QUERY_STATES_PRE_SCHEMA_25)), + lambda_stmt(lambda: select(*QUERY_STATES_PRE_SCHEMA_25)), False, ) return ( - bakery(lambda s: s.query(*QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED)), + lambda_stmt(lambda: select(*QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED)), False, ) # Finally if no migration is in progress and no_attributes # was not requested, we query both attributes columns and # join state_attributes if include_last_changed: - return bakery(lambda s: s.query(*QUERY_STATES)), True - return bakery(lambda s: s.query(*QUERY_STATES_NO_LAST_CHANGED)), True - - -def async_setup(hass: HomeAssistant) -> None: - """Set up the history hooks.""" - hass.data[HISTORY_BAKERY] = baked.bakery() + return lambda_stmt(lambda: select(*QUERY_STATES)), True + return lambda_stmt(lambda: select(*QUERY_STATES_NO_LAST_CHANGED)), True def get_significant_states( @@ -200,38 +195,30 @@ def _ignore_domains_filter(query: Query) -> Query: ) -def _query_significant_states_with_session( - hass: HomeAssistant, - session: Session, +def _significant_states_stmt( + schema_version: int, start_time: datetime, - end_time: datetime | None = None, - entity_ids: list[str] | None = None, - filters: Filters | None = None, - significant_changes_only: bool = True, - no_attributes: bool = False, -) -> list[Row]: + end_time: datetime | None, + entity_ids: list[str] | None, + filters: Filters | None, + significant_changes_only: bool, + no_attributes: bool, +) -> StatementLambdaElement: """Query the database for significant state changes.""" - if _LOGGER.isEnabledFor(logging.DEBUG): - timer_start = time.perf_counter() - - baked_query, join_attributes = bake_query_and_join_attributes( - hass, no_attributes, include_last_changed=True + stmt, join_attributes = lambda_stmt_and_join_attributes( + schema_version, no_attributes, include_last_changed=not significant_changes_only ) - - if entity_ids is not None and len(entity_ids) == 1: - if ( - significant_changes_only - and split_entity_id(entity_ids[0])[0] not in SIGNIFICANT_DOMAINS - ): - baked_query, join_attributes = bake_query_and_join_attributes( - hass, no_attributes, include_last_changed=False - ) - baked_query += lambda q: q.filter( - (States.last_changed == States.last_updated) - | States.last_changed.is_(None) - ) + if ( + entity_ids + and len(entity_ids) == 1 + and significant_changes_only + and split_entity_id(entity_ids[0])[0] not in SIGNIFICANT_DOMAINS + ): + stmt += lambda q: q.filter( + (States.last_changed == States.last_updated) | States.last_changed.is_(None) + ) elif significant_changes_only: - baked_query += lambda q: q.filter( + stmt += lambda q: q.filter( or_( *[ States.entity_id.like(entity_domain) @@ -244,36 +231,24 @@ def _query_significant_states_with_session( ) ) - if entity_ids is not None: - baked_query += lambda q: q.filter( - States.entity_id.in_(bindparam("entity_ids", expanding=True)) - ) + if entity_ids: + stmt += lambda q: q.filter(States.entity_id.in_(entity_ids)) else: - baked_query += _ignore_domains_filter - if filters: - filters.bake(baked_query) + stmt += _ignore_domains_filter + if filters and filters.has_config: + entity_filter = filters.entity_filter() + stmt += lambda q: q.filter(entity_filter) - baked_query += lambda q: q.filter(States.last_updated > bindparam("start_time")) - if end_time is not None: - baked_query += lambda q: q.filter(States.last_updated < bindparam("end_time")) + stmt += lambda q: q.filter(States.last_updated > start_time) + if end_time: + stmt += lambda q: q.filter(States.last_updated < end_time) if join_attributes: - baked_query += lambda q: q.outerjoin( + stmt += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) - baked_query += lambda q: q.order_by(States.entity_id, States.last_updated) - - states = execute( - baked_query(session).params( - start_time=start_time, end_time=end_time, entity_ids=entity_ids - ) - ) - - if _LOGGER.isEnabledFor(logging.DEBUG): - elapsed = time.perf_counter() - timer_start - _LOGGER.debug("get_significant_states took %fs", elapsed) - - return states + stmt += lambda q: q.order_by(States.entity_id, States.last_updated) + return stmt def get_significant_states_with_session( @@ -301,9 +276,8 @@ def get_significant_states_with_session( as well as all states from certain domains (for instance thermostat so that we get current temperature in our graphs). """ - states = _query_significant_states_with_session( - hass, - session, + stmt = _significant_states_stmt( + _schema_version(hass), start_time, end_time, entity_ids, @@ -311,6 +285,9 @@ def get_significant_states_with_session( significant_changes_only, no_attributes, ) + states = execute_stmt_lambda_element( + session, stmt, None if entity_ids else start_time, end_time + ) return _sorted_states_to_dict( hass, session, @@ -354,6 +331,38 @@ def get_full_significant_states_with_session( ) +def _state_changed_during_period_stmt( + schema_version: int, + start_time: datetime, + end_time: datetime | None, + entity_id: str | None, + no_attributes: bool, + descending: bool, + limit: int | None, +) -> StatementLambdaElement: + stmt, join_attributes = lambda_stmt_and_join_attributes( + schema_version, no_attributes, include_last_changed=False + ) + stmt += lambda q: q.filter( + ((States.last_changed == States.last_updated) | States.last_changed.is_(None)) + & (States.last_updated > start_time) + ) + if end_time: + stmt += lambda q: q.filter(States.last_updated < end_time) + stmt += lambda q: q.filter(States.entity_id == entity_id) + if join_attributes: + stmt += lambda q: q.outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ) + if descending: + stmt += lambda q: q.order_by(States.entity_id, States.last_updated.desc()) + else: + stmt += lambda q: q.order_by(States.entity_id, States.last_updated) + if limit: + stmt += lambda q: q.limit(limit) + return stmt + + def state_changes_during_period( hass: HomeAssistant, start_time: datetime, @@ -365,52 +374,21 @@ def state_changes_during_period( include_start_time_state: bool = True, ) -> MutableMapping[str, list[State]]: """Return states changes during UTC period start_time - end_time.""" + entity_id = entity_id.lower() if entity_id is not None else None + with session_scope(hass=hass) as session: - baked_query, join_attributes = bake_query_and_join_attributes( - hass, no_attributes, include_last_changed=False + stmt = _state_changed_during_period_stmt( + _schema_version(hass), + start_time, + end_time, + entity_id, + no_attributes, + descending, + limit, ) - - baked_query += lambda q: q.filter( - ( - (States.last_changed == States.last_updated) - | States.last_changed.is_(None) - ) - & (States.last_updated > bindparam("start_time")) + states = execute_stmt_lambda_element( + session, stmt, None if entity_id else start_time, end_time ) - - if end_time is not None: - baked_query += lambda q: q.filter( - States.last_updated < bindparam("end_time") - ) - - if entity_id is not None: - baked_query += lambda q: q.filter_by(entity_id=bindparam("entity_id")) - entity_id = entity_id.lower() - - if join_attributes: - baked_query += lambda q: q.outerjoin( - StateAttributes, States.attributes_id == StateAttributes.attributes_id - ) - - if descending: - baked_query += lambda q: q.order_by( - States.entity_id, States.last_updated.desc() - ) - else: - baked_query += lambda q: q.order_by(States.entity_id, States.last_updated) - - if limit: - baked_query += lambda q: q.limit(bindparam("limit")) - - states = execute( - baked_query(session).params( - start_time=start_time, - end_time=end_time, - entity_id=entity_id, - limit=limit, - ) - ) - entity_ids = [entity_id] if entity_id is not None else None return cast( @@ -426,41 +404,37 @@ def state_changes_during_period( ) +def _get_last_state_changes_stmt( + schema_version: int, number_of_states: int, entity_id: str +) -> StatementLambdaElement: + stmt, join_attributes = lambda_stmt_and_join_attributes( + schema_version, False, include_last_changed=False + ) + stmt += lambda q: q.filter( + (States.last_changed == States.last_updated) | States.last_changed.is_(None) + ).filter(States.entity_id == entity_id) + if join_attributes: + stmt += lambda q: q.outerjoin( + StateAttributes, States.attributes_id == StateAttributes.attributes_id + ) + stmt += lambda q: q.order_by(States.entity_id, States.last_updated.desc()).limit( + number_of_states + ) + return stmt + + def get_last_state_changes( hass: HomeAssistant, number_of_states: int, entity_id: str ) -> MutableMapping[str, list[State]]: """Return the last number_of_states.""" start_time = dt_util.utcnow() + entity_id = entity_id.lower() if entity_id is not None else None with session_scope(hass=hass) as session: - baked_query, join_attributes = bake_query_and_join_attributes( - hass, False, include_last_changed=False + stmt = _get_last_state_changes_stmt( + _schema_version(hass), number_of_states, entity_id ) - - baked_query += lambda q: q.filter( - (States.last_changed == States.last_updated) | States.last_changed.is_(None) - ) - - if entity_id is not None: - baked_query += lambda q: q.filter_by(entity_id=bindparam("entity_id")) - entity_id = entity_id.lower() - - if join_attributes: - baked_query += lambda q: q.outerjoin( - StateAttributes, States.attributes_id == StateAttributes.attributes_id - ) - baked_query += lambda q: q.order_by( - States.entity_id, States.last_updated.desc() - ) - - baked_query += lambda q: q.limit(bindparam("number_of_states")) - - states = execute( - baked_query(session).params( - number_of_states=number_of_states, entity_id=entity_id - ) - ) - + states = list(execute_stmt_lambda_element(session, stmt)) entity_ids = [entity_id] if entity_id is not None else None return cast( @@ -476,96 +450,91 @@ def get_last_state_changes( ) -def _most_recent_state_ids_entities_subquery(query: Query) -> Query: - """Query to find the most recent state id for specific entities.""" +def _get_states_for_entites_stmt( + schema_version: int, + run_start: datetime, + utc_point_in_time: datetime, + entity_ids: list[str], + no_attributes: bool, +) -> StatementLambdaElement: + """Baked query to get states for specific entities.""" + stmt, join_attributes = lambda_stmt_and_join_attributes( + schema_version, no_attributes, include_last_changed=True + ) # We got an include-list of entities, accelerate the query by filtering already # in the inner query. - most_recent_state_ids = ( - query.session.query(func.max(States.state_id).label("max_state_id")) - .filter( - (States.last_updated >= bindparam("run_start")) - & (States.last_updated < bindparam("utc_point_in_time")) - ) - .filter(States.entity_id.in_(bindparam("entity_ids", expanding=True))) - .group_by(States.entity_id) - .subquery() + stmt += lambda q: q.where( + States.state_id + == ( + select(func.max(States.state_id).label("max_state_id")) + .filter( + (States.last_updated >= run_start) + & (States.last_updated < utc_point_in_time) + ) + .filter(States.entity_id.in_(entity_ids)) + .group_by(States.entity_id) + .subquery() + ).c.max_state_id ) - return query.join( - most_recent_state_ids, - States.state_id == most_recent_state_ids.c.max_state_id, - ) - - -def _get_states_baked_query_for_entites( - hass: HomeAssistant, - no_attributes: bool = False, -) -> BakedQuery: - """Baked query to get states for specific entities.""" - baked_query, join_attributes = bake_query_and_join_attributes( - hass, no_attributes, include_last_changed=True - ) - baked_query += _most_recent_state_ids_entities_subquery if join_attributes: - baked_query += lambda q: q.outerjoin( + stmt += lambda q: q.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) - return baked_query + return stmt -def _most_recent_state_ids_subquery(query: Query) -> Query: - """Find the most recent state ids for all entiites.""" +def _get_states_for_all_stmt( + schema_version: int, + run_start: datetime, + utc_point_in_time: datetime, + filters: Filters | None, + no_attributes: bool, +) -> StatementLambdaElement: + """Baked query to get states for all entities.""" + stmt, join_attributes = lambda_stmt_and_join_attributes( + schema_version, no_attributes, include_last_changed=True + ) # We did not get an include-list of entities, query all states in the inner # query, then filter out unwanted domains as well as applying the custom filter. # This filtering can't be done in the inner query because the domain column is # not indexed and we can't control what's in the custom filter. most_recent_states_by_date = ( - query.session.query( + select( States.entity_id.label("max_entity_id"), func.max(States.last_updated).label("max_last_updated"), ) .filter( - (States.last_updated >= bindparam("run_start")) - & (States.last_updated < bindparam("utc_point_in_time")) + (States.last_updated >= run_start) + & (States.last_updated < utc_point_in_time) ) .group_by(States.entity_id) .subquery() ) - most_recent_state_ids = ( - query.session.query(func.max(States.state_id).label("max_state_id")) - .join( - most_recent_states_by_date, - and_( - States.entity_id == most_recent_states_by_date.c.max_entity_id, - States.last_updated == most_recent_states_by_date.c.max_last_updated, - ), - ) - .group_by(States.entity_id) - .subquery() + stmt += lambda q: q.where( + States.state_id + == ( + select(func.max(States.state_id).label("max_state_id")) + .join( + most_recent_states_by_date, + and_( + States.entity_id == most_recent_states_by_date.c.max_entity_id, + States.last_updated + == most_recent_states_by_date.c.max_last_updated, + ), + ) + .group_by(States.entity_id) + .subquery() + ).c.max_state_id, ) - return query.join( - most_recent_state_ids, - States.state_id == most_recent_state_ids.c.max_state_id, - ) - - -def _get_states_baked_query_for_all( - hass: HomeAssistant, - filters: Filters | None = None, - no_attributes: bool = False, -) -> BakedQuery: - """Baked query to get states for all entities.""" - baked_query, join_attributes = bake_query_and_join_attributes( - hass, no_attributes, include_last_changed=True - ) - baked_query += _most_recent_state_ids_subquery - baked_query += _ignore_domains_filter - if filters: - filters.bake(baked_query) + stmt += _ignore_domains_filter + if filters and filters.has_config: + entity_filter = filters.entity_filter() + stmt += lambda q: q.filter(entity_filter) if join_attributes: - baked_query += lambda q: q.outerjoin( + stmt += lambda q: q.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) - return baked_query + return stmt def _get_rows_with_session( @@ -576,11 +545,15 @@ def _get_rows_with_session( run: RecorderRuns | None = None, filters: Filters | None = None, no_attributes: bool = False, -) -> list[Row]: +) -> Iterable[Row]: """Return the states at a specific point in time.""" + schema_version = _schema_version(hass) if entity_ids and len(entity_ids) == 1: - return _get_single_entity_states_with_session( - hass, session, utc_point_in_time, entity_ids[0], no_attributes + return execute_stmt_lambda_element( + session, + _get_single_entity_states_stmt( + schema_version, utc_point_in_time, entity_ids[0], no_attributes + ), ) if run is None: @@ -593,46 +566,41 @@ def _get_rows_with_session( # We have more than one entity to look at so we need to do a query on states # since the last recorder run started. if entity_ids: - baked_query = _get_states_baked_query_for_entites(hass, no_attributes) - else: - baked_query = _get_states_baked_query_for_all(hass, filters, no_attributes) - - return execute( - baked_query(session).params( - run_start=run.start, - utc_point_in_time=utc_point_in_time, - entity_ids=entity_ids, + stmt = _get_states_for_entites_stmt( + schema_version, run.start, utc_point_in_time, entity_ids, no_attributes ) - ) + else: + stmt = _get_states_for_all_stmt( + schema_version, run.start, utc_point_in_time, filters, no_attributes + ) + + return execute_stmt_lambda_element(session, stmt) -def _get_single_entity_states_with_session( - hass: HomeAssistant, - session: Session, +def _get_single_entity_states_stmt( + schema_version: int, utc_point_in_time: datetime, entity_id: str, no_attributes: bool = False, -) -> list[Row]: +) -> StatementLambdaElement: # Use an entirely different (and extremely fast) query if we only # have a single entity id - baked_query, join_attributes = bake_query_and_join_attributes( - hass, no_attributes, include_last_changed=True + stmt, join_attributes = lambda_stmt_and_join_attributes( + schema_version, no_attributes, include_last_changed=True ) - baked_query += lambda q: q.filter( - States.last_updated < bindparam("utc_point_in_time"), - States.entity_id == bindparam("entity_id"), + stmt += ( + lambda q: q.filter( + States.last_updated < utc_point_in_time, + States.entity_id == entity_id, + ) + .order_by(States.last_updated.desc()) + .limit(1) ) if join_attributes: - baked_query += lambda q: q.outerjoin( + stmt += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) - baked_query += lambda q: q.order_by(States.last_updated.desc()).limit(1) - - query = baked_query(session).params( - utc_point_in_time=utc_point_in_time, entity_id=entity_id - ) - - return execute(query) + return stmt def _sorted_states_to_dict( diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index f48a6126ea9..eb99c304808 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -1,7 +1,7 @@ """SQLAlchemy util functions.""" from __future__ import annotations -from collections.abc import Callable, Generator +from collections.abc import Callable, Generator, Iterable from contextlib import contextmanager from datetime import date, datetime, timedelta import functools @@ -18,9 +18,12 @@ from awesomeversion import ( import ciso8601 from sqlalchemy import text from sqlalchemy.engine.cursor import CursorFetchStrategy +from sqlalchemy.engine.row import Row from sqlalchemy.exc import OperationalError, SQLAlchemyError +from sqlalchemy.ext.baked import Result from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session +from sqlalchemy.sql.lambdas import StatementLambdaElement from typing_extensions import Concatenate, ParamSpec from homeassistant.core import HomeAssistant @@ -46,6 +49,7 @@ _LOGGER = logging.getLogger(__name__) RETRIES = 3 QUERY_RETRY_WAIT = 0.1 SQLITE3_POSTFIXES = ["", "-wal", "-shm"] +DEFAULT_YIELD_STATES_ROWS = 32768 MIN_VERSION_MARIA_DB = AwesomeVersion("10.3.0", AwesomeVersionStrategy.SIMPLEVER) MIN_VERSION_MARIA_DB_ROWNUM = AwesomeVersion("10.2.0", AwesomeVersionStrategy.SIMPLEVER) @@ -119,8 +123,10 @@ def commit(session: Session, work: Any) -> bool: def execute( - qry: Query, to_native: bool = False, validate_entity_ids: bool = True -) -> list: + qry: Query | Result, + to_native: bool = False, + validate_entity_ids: bool = True, +) -> list[Row]: """Query the database and convert the objects to HA native form. This method also retries a few times in the case of stale connections. @@ -163,7 +169,39 @@ def execute( raise time.sleep(QUERY_RETRY_WAIT) - assert False # unreachable + assert False # unreachable # pragma: no cover + + +def execute_stmt_lambda_element( + session: Session, + stmt: StatementLambdaElement, + start_time: datetime | None = None, + end_time: datetime | None = None, + yield_per: int | None = DEFAULT_YIELD_STATES_ROWS, +) -> Iterable[Row]: + """Execute a StatementLambdaElement. + + If the time window passed is greater than one day + the execution method will switch to yield_per to + reduce memory pressure. + + It is not recommended to pass a time window + when selecting non-ranged rows (ie selecting + specific entities) since they are usually faster + with .all(). + """ + executed = session.execute(stmt) + use_all = not start_time or ((end_time or dt_util.utcnow()) - start_time).days <= 1 + for tryno in range(0, RETRIES): + try: + return executed.all() if use_all else executed.yield_per(yield_per) # type: ignore[no-any-return] + except SQLAlchemyError as err: + _LOGGER.error("Error executing query: %s", err) + if tryno == RETRIES - 1: + raise + time.sleep(QUERY_RETRY_WAIT) + + assert False # unreachable # pragma: no cover def validate_or_move_away_sqlite_database(dburl: str) -> bool: diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 237030c1186..c650ec20fb0 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -6,10 +6,13 @@ from unittest.mock import MagicMock, Mock, patch import pytest from sqlalchemy import text +from sqlalchemy.engine.result import ChunkedIteratorResult +from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.sql.elements import TextClause +from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components import recorder -from homeassistant.components.recorder import util +from homeassistant.components.recorder import history, util from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX from homeassistant.components.recorder.models import RecorderRuns from homeassistant.components.recorder.util import ( @@ -24,6 +27,7 @@ from homeassistant.util import dt as dt_util from .common import corrupt_db_file, run_information_with_session from tests.common import SetupRecorderInstanceT, async_test_home_assistant +from tests.components.recorder.common import wait_recording_done def test_session_scope_not_setup(hass_recorder): @@ -510,8 +514,10 @@ def test_basic_sanity_check(hass_recorder): def test_combined_checks(hass_recorder, caplog): """Run Checks on the open database.""" hass = hass_recorder() + instance = recorder.get_instance(hass) + instance.db_retry_wait = 0 - cursor = hass.data[DATA_INSTANCE].engine.raw_connection().cursor() + cursor = instance.engine.raw_connection().cursor() assert util.run_checks_on_open_db("fake_db_path", cursor) is None assert "could not validate that the sqlite3 database" in caplog.text @@ -658,3 +664,54 @@ def test_build_mysqldb_conv(): assert conv["DATETIME"]("2022-05-13T22:33:12.741") == datetime( 2022, 5, 13, 22, 33, 12, 741000, tzinfo=None ) + + +@patch("homeassistant.components.recorder.util.QUERY_RETRY_WAIT", 0) +def test_execute_stmt_lambda_element(hass_recorder): + """Test executing with execute_stmt_lambda_element.""" + hass = hass_recorder() + instance = recorder.get_instance(hass) + hass.states.set("sensor.on", "on") + new_state = hass.states.get("sensor.on") + wait_recording_done(hass) + now = dt_util.utcnow() + tomorrow = now + timedelta(days=1) + one_week_from_now = now + timedelta(days=7) + + class MockExecutor: + def __init__(self, stmt): + assert isinstance(stmt, StatementLambdaElement) + self.calls = 0 + + def all(self): + self.calls += 1 + if self.calls == 2: + return ["mock_row"] + raise SQLAlchemyError + + with session_scope(hass=hass) as session: + # No time window, we always get a list + stmt = history._get_single_entity_states_stmt( + instance.schema_version, dt_util.utcnow(), "sensor.on", False + ) + rows = util.execute_stmt_lambda_element(session, stmt) + assert isinstance(rows, list) + assert rows[0].state == new_state.state + assert rows[0].entity_id == new_state.entity_id + + # Time window >= 2 days, we get a ChunkedIteratorResult + rows = util.execute_stmt_lambda_element(session, stmt, now, one_week_from_now) + assert isinstance(rows, ChunkedIteratorResult) + row = next(rows) + assert row.state == new_state.state + assert row.entity_id == new_state.entity_id + + # Time window < 2 days, we get a list + rows = util.execute_stmt_lambda_element(session, stmt, now, tomorrow) + assert isinstance(rows, list) + assert rows[0].state == new_state.state + assert rows[0].entity_id == new_state.entity_id + + with patch.object(session, "execute", MockExecutor): + rows = util.execute_stmt_lambda_element(session, stmt, now, tomorrow) + assert rows == ["mock_row"] From 221b77297e2021b4b337594ff644ab5139507764 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sun, 15 May 2022 23:58:57 +0800 Subject: [PATCH 0523/3516] Declare exports from stream explicitly (#71898) --- homeassistant/components/camera/__init__.py | 8 ++++++-- homeassistant/components/camera/media_source.py | 2 +- homeassistant/components/generic/camera.py | 2 +- homeassistant/components/generic/config_flow.py | 2 +- homeassistant/components/onvif/__init__.py | 2 +- homeassistant/components/onvif/camera.py | 2 +- homeassistant/components/stream/__init__.py | 15 +++++++++++++++ mypy.ini | 1 + script/hassfest/mypy_config.py | 1 + tests/components/camera/test_media_source.py | 2 +- tests/components/generic/test_config_flow.py | 2 +- tests/components/roku/test_media_player.py | 2 +- 12 files changed, 31 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 57019f7c68d..bff0a07a9be 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -29,8 +29,12 @@ from homeassistant.components.media_player.const import ( DOMAIN as DOMAIN_MP, SERVICE_PLAY_MEDIA, ) -from homeassistant.components.stream import Stream, create_stream -from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE, OUTPUT_FORMATS +from homeassistant.components.stream import ( + FORMAT_CONTENT_TYPE, + OUTPUT_FORMATS, + Stream, + create_stream, +) from homeassistant.components.websocket_api import ActiveConnection from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( diff --git a/homeassistant/components/camera/media_source.py b/homeassistant/components/camera/media_source.py index ffa1962f9ef..733efb3a430 100644 --- a/homeassistant/components/camera/media_source.py +++ b/homeassistant/components/camera/media_source.py @@ -15,7 +15,7 @@ from homeassistant.components.media_source.models import ( MediaSourceItem, PlayMedia, ) -from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE, HLS_PROVIDER +from homeassistant.components.stream import FORMAT_CONTENT_TYPE, HLS_PROVIDER from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_component import EntityComponent diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 4886e3a0693..edc51430f0d 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -12,7 +12,7 @@ from homeassistant.components.camera import ( Camera, CameraEntityFeature, ) -from homeassistant.components.stream.const import ( +from homeassistant.components.stream import ( CONF_RTSP_TRANSPORT, CONF_USE_WALLCLOCK_AS_TIMESTAMPS, RTSP_TRANSPORTS, diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index bf9499c07df..435fdf6f729 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -16,7 +16,7 @@ from httpx import HTTPStatusError, RequestError, TimeoutException import voluptuous as vol import yarl -from homeassistant.components.stream.const import ( +from homeassistant.components.stream import ( CONF_RTSP_TRANSPORT, CONF_USE_WALLCLOCK_AS_TIMESTAMPS, RTSP_TRANSPORTS, diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index d966549a36f..7922d59ca53 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -2,7 +2,7 @@ from onvif.exceptions import ONVIFAuthError, ONVIFError, ONVIFTimeoutError from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS -from homeassistant.components.stream.const import CONF_RTSP_TRANSPORT, RTSP_TRANSPORTS +from homeassistant.components.stream import CONF_RTSP_TRANSPORT, RTSP_TRANSPORTS from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 6b61a37eb16..e46e1a2ac59 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -9,7 +9,7 @@ from yarl import URL from homeassistant.components import ffmpeg from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, get_ffmpeg_manager -from homeassistant.components.stream.const import CONF_RTSP_TRANSPORT +from homeassistant.components.stream import CONF_RTSP_TRANSPORT from homeassistant.config_entries import ConfigEntry from homeassistant.const import HTTP_BASIC_AUTHENTICATION from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index fbdfd97f9b2..95e9afe6c36 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -43,12 +43,15 @@ from .const import ( CONF_SEGMENT_DURATION, CONF_USE_WALLCLOCK_AS_TIMESTAMPS, DOMAIN, + FORMAT_CONTENT_TYPE, HLS_PROVIDER, MAX_SEGMENTS, + OUTPUT_FORMATS, OUTPUT_IDLE_TIMEOUT, RECORDER_PROVIDER, RTSP_TRANSPORTS, SEGMENT_DURATION_ADJUSTER, + SOURCE_TIMEOUT, STREAM_RESTART_INCREMENT, STREAM_RESTART_RESET_TIME, TARGET_SEGMENT_DURATION_NON_LL_HLS, @@ -57,6 +60,18 @@ from .core import PROVIDERS, IdleTimer, KeyFrameConverter, StreamOutput, StreamS from .diagnostics import Diagnostics from .hls import HlsStreamOutput, async_setup_hls +__all__ = [ + "CONF_RTSP_TRANSPORT", + "CONF_USE_WALLCLOCK_AS_TIMESTAMPS", + "FORMAT_CONTENT_TYPE", + "HLS_PROVIDER", + "OUTPUT_FORMATS", + "RTSP_TRANSPORTS", + "SOURCE_TIMEOUT", + "Stream", + "create_stream", +] + _LOGGER = logging.getLogger(__name__) STREAM_SOURCE_REDACT_PATTERN = [ diff --git a/mypy.ini b/mypy.ini index 7784acd9fe6..dc023da8d01 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2017,6 +2017,7 @@ disallow_untyped_defs = true no_implicit_optional = true warn_return_any = true warn_unreachable = true +no_implicit_reexport = true [mypy-homeassistant.components.sun.*] check_untyped_defs = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index d21dc0faf7d..2b7c1e00f94 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -200,6 +200,7 @@ IGNORED_MODULES: Final[list[str]] = [ NO_IMPLICIT_REEXPORT_MODULES: set[str] = { "homeassistant.components", "homeassistant.components.diagnostics.*", + "homeassistant.components.stream.*", } HEADER: Final = """ diff --git a/tests/components/camera/test_media_source.py b/tests/components/camera/test_media_source.py index f684d81a2b1..b7c273bb23a 100644 --- a/tests/components/camera/test_media_source.py +++ b/tests/components/camera/test_media_source.py @@ -5,7 +5,7 @@ import pytest from homeassistant.components import media_source from homeassistant.components.camera.const import StreamType -from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE +from homeassistant.components.stream import FORMAT_CONTENT_TYPE from homeassistant.setup import async_setup_component diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 82cf41c6e91..b7f3bf73527 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -19,7 +19,7 @@ from homeassistant.components.generic.const import ( CONF_STREAM_SOURCE, DOMAIN, ) -from homeassistant.components.stream.const import ( +from homeassistant.components.stream import ( CONF_RTSP_TRANSPORT, CONF_USE_WALLCLOCK_AS_TIMESTAMPS, ) diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 21fd2e861b6..c95eda2288a 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -52,7 +52,7 @@ from homeassistant.components.roku.const import ( DOMAIN, SERVICE_SEARCH, ) -from homeassistant.components.stream.const import FORMAT_CONTENT_TYPE, HLS_PROVIDER +from homeassistant.components.stream import FORMAT_CONTENT_TYPE, HLS_PROVIDER from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( From f2da1fceb2857f209ae61930cf32d45240195126 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 15 May 2022 10:33:46 -0700 Subject: [PATCH 0524/3516] Bump gcal_sync to 0.8.0 (#71900) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 87069bc463a..fde04d96baf 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", - "requirements": ["gcal-sync==0.7.1", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==0.8.0", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 457c2e9ec1d..1c4f2ec0860 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -689,7 +689,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==0.7.1 +gcal-sync==0.8.0 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dbb54b45643..7e9f5a5011c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -492,7 +492,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==0.7.1 +gcal-sync==0.8.0 # homeassistant.components.geocaching geocachingapi==0.2.1 From b6c7422607511f4896d7d2e28cca689413903da1 Mon Sep 17 00:00:00 2001 From: moritzbeck01 <48137462+moritzbeck01@users.noreply.github.com> Date: Sun, 15 May 2022 19:37:24 +0200 Subject: [PATCH 0525/3516] Add timer to the the helper category (#71837) --- homeassistant/components/timer/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/timer/manifest.json b/homeassistant/components/timer/manifest.json index 19748332221..160c96f9664 100644 --- a/homeassistant/components/timer/manifest.json +++ b/homeassistant/components/timer/manifest.json @@ -1,6 +1,7 @@ { "domain": "timer", "name": "Timer", + "integration_type": "helper", "documentation": "https://www.home-assistant.io/integrations/timer", "codeowners": [], "quality_scale": "internal" From 690fb2ae86cfe59907a6e42931165be1bf59ae16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Drobni=C4=8D?= Date: Sun, 15 May 2022 20:31:22 +0200 Subject: [PATCH 0526/3516] Add back description and location to calendar endpoint (#71887) --- homeassistant/components/calendar/__init__.py | 2 ++ homeassistant/components/demo/calendar.py | 4 ++++ tests/components/caldav/test_calendar.py | 2 ++ tests/components/calendar/test_init.py | 2 ++ tests/components/twentemilieu/test_calendar.py | 2 ++ 5 files changed, 12 insertions(+) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 0f122fea55f..432b6943473 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -341,6 +341,8 @@ class CalendarEventView(http.HomeAssistantView): [ { "summary": event.summary, + "description": event.description, + "location": event.location, "start": _get_api_date(event.start), "end": _get_api_date(event.end), } diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index 42ec04e42f2..3a8b909bd0c 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -39,6 +39,8 @@ def calendar_data_future() -> CalendarEvent: start=one_hour_from_now, end=one_hour_from_now + datetime.timedelta(minutes=60), summary="Future Event", + description="Future Description", + location="Future Location", ) @@ -90,6 +92,8 @@ class LegacyDemoCalendar(CalendarEventDevice): ).isoformat() }, "summary": "Future Event", + "description": "Future Description", + "location": "Future Location", } @property diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index 2131eebe997..f35fa609e4a 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -937,5 +937,7 @@ async def test_get_events_custom_calendars(hass, calendar, get_api_events): "end": {"dateTime": "2017-11-27T10:00:00-08:00"}, "start": {"dateTime": "2017-11-27T09:00:00-08:00"}, "summary": "This is a normal event", + "location": "Hamburg", + "description": "Surprisingly rainy", } ] diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 26f9320a196..0a91f58b0b2 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -57,3 +57,5 @@ async def test_events_http_api_shim(hass, hass_client): assert response.status == HTTPStatus.OK events = await response.json() assert events[0]["summary"] == "Future Event" + assert events[0]["description"] == "Future Description" + assert events[0]["location"] == "Future Location" diff --git a/tests/components/twentemilieu/test_calendar.py b/tests/components/twentemilieu/test_calendar.py index 0a0f32be212..b9f1dd9247d 100644 --- a/tests/components/twentemilieu/test_calendar.py +++ b/tests/components/twentemilieu/test_calendar.py @@ -79,4 +79,6 @@ async def test_api_events( "start": {"date": "2022-01-06"}, "end": {"date": "2022-01-06"}, "summary": "Christmas Tree Pickup", + "description": None, + "location": None, } From 904be03d72b78c043d553ab02d6392b571f442c7 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 15 May 2022 20:36:57 +0200 Subject: [PATCH 0527/3516] Revert changing `pysnmp` to `pysnmplib` (#71901) --- homeassistant/components/brother/manifest.json | 2 +- homeassistant/components/snmp/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index c230373ea36..aaf1af72db9 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.2.0"], + "requirements": ["brother==1.1.0"], "zeroconf": [ { "type": "_printer._tcp.local.", diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index ef0213e82dc..76df9e18606 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -2,7 +2,7 @@ "domain": "snmp", "name": "SNMP", "documentation": "https://www.home-assistant.io/integrations/snmp", - "requirements": ["pysnmplib==5.0.10"], + "requirements": ["pysnmp==4.4.12"], "codeowners": [], "iot_class": "local_polling", "loggers": ["pyasn1", "pysmi", "pysnmp"] diff --git a/requirements_all.txt b/requirements_all.txt index 1c4f2ec0860..5b35a595083 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -436,7 +436,7 @@ bravia-tv==1.0.11 broadlink==0.18.1 # homeassistant.components.brother -brother==1.2.0 +brother==1.1.0 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 @@ -1832,7 +1832,7 @@ pysmarty==0.8 pysml==0.0.7 # homeassistant.components.snmp -pysnmplib==5.0.10 +pysnmp==4.4.12 # homeassistant.components.soma pysoma==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7e9f5a5011c..888cd30a056 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -333,7 +333,7 @@ bravia-tv==1.0.11 broadlink==0.18.1 # homeassistant.components.brother -brother==1.2.0 +brother==1.1.0 # homeassistant.components.brunt brunt==1.2.0 From 73f2f2ad1ba6cb86ed4961009e2277301274c529 Mon Sep 17 00:00:00 2001 From: moritzbeck01 <48137462+moritzbeck01@users.noreply.github.com> Date: Sun, 15 May 2022 21:19:18 +0200 Subject: [PATCH 0528/3516] Add counter to the the helper category (#71838) --- homeassistant/components/counter/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/counter/manifest.json b/homeassistant/components/counter/manifest.json index ab1a4bf0438..6db4a0a7a97 100644 --- a/homeassistant/components/counter/manifest.json +++ b/homeassistant/components/counter/manifest.json @@ -1,6 +1,7 @@ { "domain": "counter", "name": "Counter", + "integration_type": "helper", "documentation": "https://www.home-assistant.io/integrations/counter", "codeowners": ["@fabaff"], "quality_scale": "internal" From 37f81b261dcfdf78951ad3a51957c93e84394509 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 16 May 2022 00:24:28 +0000 Subject: [PATCH 0529/3516] [ci skip] Translation update --- .../components/adax/translations/sk.json | 7 + .../components/adguard/translations/es.json | 6 +- .../components/airthings/translations/es.json | 2 +- .../components/airvisual/translations/es.json | 4 +- .../airvisual/translations/sensor.sk.json | 7 + .../components/airzone/translations/es.json | 20 +++ .../aladdin_connect/translations/ca.json | 1 + .../aladdin_connect/translations/es.json | 26 ++++ .../aladdin_connect/translations/it.json | 27 ++++ .../aladdin_connect/translations/sk.json | 11 ++ .../aladdin_connect/translations/tr.json | 27 ++++ .../alarm_control_panel/translations/es.json | 2 +- .../components/almond/translations/es.json | 4 +- .../components/apple_tv/translations/es.json | 4 +- .../translations/es.json | 3 + .../aseko_pool_live/translations/es.json | 2 +- .../components/asuswrt/translations/es.json | 3 +- .../components/atag/translations/sk.json | 3 + .../components/august/translations/es.json | 2 +- .../aussie_broadband/translations/es.json | 23 ++- .../aussie_broadband/translations/sk.json | 10 ++ .../components/awair/translations/es.json | 2 +- .../azure_devops/translations/es.json | 4 +- .../azure_event_hub/translations/sk.json | 7 + .../components/baf/translations/ca.json | 23 +++ .../components/baf/translations/es.json | 23 +++ .../components/baf/translations/et.json | 23 +++ .../components/baf/translations/fr.json | 23 +++ .../components/baf/translations/it.json | 23 +++ .../components/baf/translations/pt-BR.json | 23 +++ .../components/baf/translations/sk.json | 19 +++ .../components/baf/translations/tr.json | 23 +++ .../binary_sensor/translations/es.json | 7 +- .../binary_sensor/translations/sk.json | 4 + .../components/blebox/translations/es.json | 2 +- .../components/braviatv/translations/es.json | 2 +- .../components/brother/translations/es.json | 2 +- .../components/brunt/translations/es.json | 2 +- .../components/brunt/translations/sk.json | 7 + .../components/bsblan/translations/es.json | 2 +- .../components/button/translations/sk.json | 7 + .../components/cast/translations/es.json | 2 +- .../components/climacell/translations/es.json | 2 +- .../climacell/translations/sensor.sk.json | 7 + .../components/climate/translations/es.json | 2 +- .../components/coinbase/translations/es.json | 4 +- .../components/coinbase/translations/sk.json | 5 + .../configurator/translations/es.json | 2 +- .../components/cover/translations/es.json | 2 +- .../crownstone/translations/es.json | 2 +- .../components/deconz/translations/es.json | 5 +- .../components/deluge/translations/es.json | 19 +++ .../components/deluge/translations/sk.json | 12 ++ .../components/denonavr/translations/es.json | 2 +- .../derivative/translations/es.json | 43 ++++++ .../derivative/translations/sk.json | 11 ++ .../device_tracker/translations/es.json | 4 +- .../devolo_home_control/translations/es.json | 2 +- .../components/dexcom/translations/es.json | 2 +- .../dialogflow/translations/es.json | 1 + .../components/directv/translations/es.json | 2 +- .../components/discord/translations/es.json | 23 +++ .../components/dlna_dmr/translations/es.json | 1 + .../components/dlna_dms/translations/es.json | 5 + .../components/elgato/translations/es.json | 8 +- .../components/elkm1/translations/es.json | 5 +- .../emulated_roku/translations/es.json | 2 +- .../components/esphome/translations/es.json | 2 +- .../components/ezviz/translations/es.json | 2 +- .../faa_delays/translations/sk.json | 7 + .../components/fan/translations/es.json | 1 + .../components/fan/translations/tr.json | 1 + .../components/fibaro/translations/es.json | 16 ++ .../components/fibaro/translations/sk.json | 11 ++ .../components/filesize/translations/es.json | 7 + .../fireservicerota/translations/es.json | 2 +- .../components/fivem/translations/es.json | 2 +- .../flick_electric/translations/es.json | 2 +- .../flunearyou/translations/es.json | 2 +- .../forecast_solar/translations/es.json | 1 + .../forked_daapd/translations/es.json | 4 +- .../components/freebox/translations/es.json | 4 +- .../components/fritz/translations/es.json | 1 + .../components/fritzbox/translations/es.json | 1 + .../fritzbox_callmonitor/translations/es.json | 2 +- .../components/generic/translations/es.json | 68 +++++++++ .../components/generic/translations/tr.json | 4 + .../geocaching/translations/es.json | 24 +++ .../geocaching/translations/tr.json | 25 ++++ .../components/github/translations/es.json | 16 ++ .../components/goalzero/translations/es.json | 2 +- .../components/google/translations/es.json | 10 +- .../components/gpslogger/translations/es.json | 1 + .../components/group/translations/es.json | 141 +++++++++++++++++- .../components/group/translations/sk.json | 18 +++ .../components/guardian/translations/es.json | 2 +- .../components/hangouts/translations/es.json | 6 +- .../components/harmony/translations/es.json | 4 +- .../hisense_aehw4a1/translations/es.json | 4 +- .../components/hive/translations/es.json | 2 +- .../home_connect/translations/es.json | 2 +- .../home_plus_control/translations/es.json | 4 +- .../homeassistant/translations/es.json | 2 +- .../components/homekit/translations/es.json | 20 ++- .../homekit_controller/translations/es.json | 4 +- .../translations/select.es.json | 8 + .../homekit_controller/translations/sk.json | 5 + .../homematicip_cloud/translations/es.json | 2 +- .../homewizard/translations/es.json | 2 +- .../components/honeywell/translations/es.json | 9 ++ .../components/honeywell/translations/sk.json | 7 + .../huawei_lte/translations/es.json | 4 +- .../components/hue/translations/es.json | 5 +- .../components/hue/translations/sk.json | 5 + .../components/hyperion/translations/es.json | 2 +- .../components/iaqualink/translations/es.json | 4 +- .../components/icloud/translations/es.json | 4 +- .../input_datetime/translations/es.json | 2 +- .../input_number/translations/es.json | 2 +- .../input_number/translations/tr.json | 2 +- .../components/insteon/translations/es.json | 7 +- .../components/insteon/translations/sk.json | 3 + .../integration/translations/es.json | 36 +++++ .../integration/translations/sk.json | 20 +++ .../intellifire/translations/es.json | 43 ++++++ .../components/ios/translations/es.json | 4 +- .../components/iss/translations/es.json | 3 +- .../components/isy994/translations/es.json | 10 +- .../components/jellyfin/translations/sk.json | 7 + .../components/juicenet/translations/es.json | 2 +- .../kaleidescape/translations/es.json | 20 +++ .../components/knx/translations/es.json | 56 +++++-- .../components/knx/translations/sk.json | 10 ++ .../components/konnected/translations/es.json | 6 +- .../components/kraken/translations/sk.json | 11 ++ .../components/life360/translations/es.json | 2 +- .../components/lifx/translations/es.json | 4 +- .../litterrobot/translations/sensor.ca.json | 21 ++- .../litterrobot/translations/sensor.es.json | 14 ++ .../litterrobot/translations/sensor.it.json | 28 ++++ .../litterrobot/translations/sensor.sk.json | 8 + .../litterrobot/translations/sensor.tr.json | 28 ++++ .../components/locative/translations/es.json | 1 + .../logi_circle/translations/es.json | 4 +- .../components/luftdaten/translations/es.json | 2 +- .../lutron_caseta/translations/es.json | 4 +- .../components/lyric/translations/es.json | 2 +- .../components/mailgun/translations/es.json | 1 + .../components/mazda/translations/es.json | 2 +- .../components/meater/translations/es.json | 19 +++ .../components/meater/translations/sk.json | 7 + .../media_player/translations/es.json | 5 +- .../components/melcloud/translations/es.json | 2 +- .../meteo_france/translations/es.json | 2 +- .../components/metoffice/translations/es.json | 2 +- .../components/mill/translations/es.json | 2 +- .../components/min_max/translations/es.json | 39 +++++ .../moehlenhoff_alpha2/translations/es.json | 2 +- .../motion_blinds/translations/tr.json | 2 +- .../components/myq/translations/es.json | 2 +- .../components/nam/translations/es.json | 2 +- .../components/nanoleaf/translations/es.json | 5 + .../components/nanoleaf/translations/sk.json | 5 + .../components/neato/translations/es.json | 2 +- .../components/nest/translations/es.json | 4 +- .../components/nest/translations/sk.json | 3 + .../components/netatmo/translations/es.json | 6 +- .../components/netatmo/translations/tr.json | 2 +- .../components/netgear/translations/es.json | 2 +- .../components/nexia/translations/es.json | 2 +- .../nmap_tracker/translations/es.json | 4 +- .../components/notion/translations/es.json | 2 +- .../components/nzbget/translations/es.json | 2 +- .../components/oncue/translations/sk.json | 3 +- .../ondilo_ico/translations/es.json | 2 +- .../components/onewire/translations/es.json | 19 +++ .../components/onewire/translations/tr.json | 4 +- .../opentherm_gw/translations/es.json | 2 +- .../components/openuv/translations/es.json | 2 +- .../openweathermap/translations/es.json | 4 +- .../components/overkiz/translations/es.json | 5 +- .../overkiz/translations/select.sk.json | 7 + .../overkiz/translations/sensor.sk.json | 10 ++ .../components/overkiz/translations/sk.json | 7 + .../ovo_energy/translations/es.json | 4 +- .../components/owntracks/translations/es.json | 1 + .../components/peco/translations/es.json | 14 ++ .../components/person/translations/es.json | 2 +- .../components/plaato/translations/es.json | 9 +- .../components/plex/translations/es.json | 4 +- .../components/plugwise/translations/es.json | 6 +- .../plum_lightpad/translations/es.json | 2 +- .../components/point/translations/es.json | 2 +- .../components/poolsense/translations/es.json | 2 +- .../components/powerwall/translations/es.json | 3 + .../components/ps4/translations/es.json | 2 +- .../pure_energie/translations/es.json | 2 +- .../pvpc_hourly_pricing/translations/es.json | 4 +- .../components/qnap_qsw/translations/es.json | 21 +++ .../components/rachio/translations/es.json | 6 +- .../radio_browser/translations/es.json | 5 + .../rainmachine/translations/es.json | 2 +- .../components/recorder/translations/es.json | 8 + .../components/recorder/translations/tr.json | 1 + .../components/renault/translations/es.json | 2 +- .../components/rfxtrx/translations/es.json | 1 + .../components/risco/translations/es.json | 2 +- .../components/risco/translations/sk.json | 9 ++ .../components/roku/translations/es.json | 2 +- .../components/roomba/translations/es.json | 4 +- .../components/roon/translations/es.json | 9 +- .../ruckus_unleashed/translations/sk.json | 7 + .../components/sabnzbd/translations/es.json | 18 +++ .../components/sabnzbd/translations/sk.json | 11 ++ .../components/samsungtv/translations/es.json | 9 +- .../season/translations/sensor.sk.json | 7 + .../components/sense/translations/es.json | 3 +- .../components/senseme/translations/sk.json | 7 + .../components/sensibo/translations/es.json | 11 +- .../components/sensibo/translations/sk.json | 3 + .../components/sensor/translations/sk.json | 9 ++ .../components/senz/translations/es.json | 20 +++ .../components/shelly/translations/es.json | 2 +- .../components/sia/translations/sk.json | 3 +- .../simplisafe/translations/es.json | 14 +- .../components/slack/translations/ca.json | 9 +- .../components/slack/translations/es.json | 29 ++++ .../components/slack/translations/it.json | 29 ++++ .../components/slack/translations/sk.json | 20 +++ .../components/slack/translations/tr.json | 29 ++++ .../components/sleepiq/translations/es.json | 6 +- .../components/slimproto/translations/es.json | 7 + .../components/slimproto/translations/tr.json | 6 + .../components/smappee/translations/es.json | 2 +- .../components/smarttub/translations/es.json | 2 +- .../components/smhi/translations/es.json | 3 + .../components/sms/translations/es.json | 1 + .../components/sms/translations/tr.json | 1 + .../components/solax/translations/es.json | 8 + .../components/soma/translations/es.json | 4 +- .../components/somfy/translations/es.json | 4 +- .../components/sonarr/translations/es.json | 2 +- .../components/sonos/translations/es.json | 2 +- .../speedtestdotnet/translations/es.json | 2 +- .../components/sql/translations/es.json | 55 +++++++ .../steam_online/translations/es.json | 34 +++++ .../components/subaru/translations/es.json | 18 ++- .../surepetcare/translations/es.json | 2 +- .../surepetcare/translations/sk.json | 7 + .../components/switch/translations/es.json | 7 + .../switch_as_x/translations/es.json | 13 ++ .../components/syncthru/translations/es.json | 2 +- .../synology_dsm/translations/es.json | 3 +- .../synology_dsm/translations/sk.json | 6 + .../system_bridge/translations/sk.json | 3 +- .../components/tado/translations/es.json | 2 +- .../tankerkoenig/translations/es.json | 33 ++++ .../tankerkoenig/translations/sk.json | 11 ++ .../components/tautulli/translations/es.json | 25 ++++ .../tellduslive/translations/es.json | 2 +- .../components/threshold/translations/es.json | 24 +++ .../components/tile/translations/es.json | 2 +- .../components/tod/translations/es.json | 25 ++++ .../tomorrowio/translations/es.json | 29 ++++ .../tomorrowio/translations/sensor.es.json | 16 ++ .../tomorrowio/translations/sensor.sk.json | 7 + .../tomorrowio/translations/sk.json | 12 ++ .../components/toon/translations/es.json | 2 +- .../totalconnect/translations/es.json | 2 +- .../components/traccar/translations/es.json | 6 +- .../components/tradfri/translations/es.json | 4 +- .../trafikverket_ferry/translations/es.json | 26 ++++ .../trafikverket_train/translations/es.json | 17 +++ .../components/tuya/translations/es.json | 6 +- .../tuya/translations/select.es.json | 9 ++ .../tuya/translations/select.sk.json | 18 +++ .../tuya/translations/sensor.sk.json | 7 + .../twentemilieu/translations/es.json | 4 +- .../components/twilio/translations/es.json | 3 +- .../components/twinkly/translations/es.json | 2 +- .../ukraine_alarm/translations/es.json | 27 +++- .../ukraine_alarm/translations/tr.json | 45 ++++++ .../components/unifi/translations/es.json | 11 +- .../unifiprotect/translations/es.json | 9 +- .../components/upb/translations/sk.json | 7 + .../components/update/translations/es.json | 7 + .../components/upnp/translations/es.json | 2 +- .../uptimerobot/translations/es.json | 3 +- .../uptimerobot/translations/sensor.es.json | 9 ++ .../utility_meter/translations/es.json | 29 ++++ .../components/velbus/translations/es.json | 4 +- .../components/verisure/translations/es.json | 2 +- .../components/vulcan/translations/es.json | 51 +++++++ .../components/vulcan/translations/sk.json | 9 ++ .../components/weather/translations/es.json | 2 +- .../components/weather/translations/sk.json | 2 +- .../components/webostv/translations/es.json | 5 + .../components/wemo/translations/es.json | 4 +- .../components/whois/translations/es.json | 4 + .../components/whois/translations/sk.json | 11 ++ .../components/wiffi/translations/es.json | 2 +- .../components/wilight/translations/es.json | 4 +- .../components/withings/translations/es.json | 8 +- .../components/wiz/translations/sk.json | 3 + .../components/wled/translations/es.json | 2 +- .../components/ws66i/translations/es.json | 34 +++++ .../components/ws66i/translations/tr.json | 34 +++++ .../components/xbox/translations/es.json | 2 +- .../xiaomi_aqara/translations/es.json | 4 +- .../xiaomi_miio/translations/es.json | 6 +- .../xiaomi_miio/translations/select.sk.json | 7 + .../yale_smart_alarm/translations/es.json | 3 +- .../yale_smart_alarm/translations/sk.json | 3 +- .../yamaha_musiccast/translations/sk.json | 7 + .../components/zha/translations/es.json | 2 +- .../components/zwave_js/translations/es.json | 4 + .../components/zwave_me/translations/es.json | 2 +- 317 files changed, 2608 insertions(+), 261 deletions(-) create mode 100644 homeassistant/components/airvisual/translations/sensor.sk.json create mode 100644 homeassistant/components/airzone/translations/es.json create mode 100644 homeassistant/components/aladdin_connect/translations/es.json create mode 100644 homeassistant/components/aladdin_connect/translations/it.json create mode 100644 homeassistant/components/aladdin_connect/translations/sk.json create mode 100644 homeassistant/components/aladdin_connect/translations/tr.json create mode 100644 homeassistant/components/application_credentials/translations/es.json create mode 100644 homeassistant/components/azure_event_hub/translations/sk.json create mode 100644 homeassistant/components/baf/translations/ca.json create mode 100644 homeassistant/components/baf/translations/es.json create mode 100644 homeassistant/components/baf/translations/et.json create mode 100644 homeassistant/components/baf/translations/fr.json create mode 100644 homeassistant/components/baf/translations/it.json create mode 100644 homeassistant/components/baf/translations/pt-BR.json create mode 100644 homeassistant/components/baf/translations/sk.json create mode 100644 homeassistant/components/baf/translations/tr.json create mode 100644 homeassistant/components/button/translations/sk.json create mode 100644 homeassistant/components/climacell/translations/sensor.sk.json create mode 100644 homeassistant/components/deluge/translations/es.json create mode 100644 homeassistant/components/deluge/translations/sk.json create mode 100644 homeassistant/components/derivative/translations/es.json create mode 100644 homeassistant/components/derivative/translations/sk.json create mode 100644 homeassistant/components/discord/translations/es.json create mode 100644 homeassistant/components/faa_delays/translations/sk.json create mode 100644 homeassistant/components/fibaro/translations/es.json create mode 100644 homeassistant/components/fibaro/translations/sk.json create mode 100644 homeassistant/components/filesize/translations/es.json create mode 100644 homeassistant/components/generic/translations/es.json create mode 100644 homeassistant/components/geocaching/translations/es.json create mode 100644 homeassistant/components/geocaching/translations/tr.json create mode 100644 homeassistant/components/github/translations/es.json create mode 100644 homeassistant/components/homekit_controller/translations/select.es.json create mode 100644 homeassistant/components/integration/translations/es.json create mode 100644 homeassistant/components/integration/translations/sk.json create mode 100644 homeassistant/components/intellifire/translations/es.json create mode 100644 homeassistant/components/kaleidescape/translations/es.json create mode 100644 homeassistant/components/kraken/translations/sk.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.es.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.it.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.sk.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.tr.json create mode 100644 homeassistant/components/meater/translations/es.json create mode 100644 homeassistant/components/meater/translations/sk.json create mode 100644 homeassistant/components/min_max/translations/es.json create mode 100644 homeassistant/components/overkiz/translations/select.sk.json create mode 100644 homeassistant/components/overkiz/translations/sensor.sk.json create mode 100644 homeassistant/components/peco/translations/es.json create mode 100644 homeassistant/components/qnap_qsw/translations/es.json create mode 100644 homeassistant/components/recorder/translations/es.json create mode 100644 homeassistant/components/sabnzbd/translations/es.json create mode 100644 homeassistant/components/sabnzbd/translations/sk.json create mode 100644 homeassistant/components/season/translations/sensor.sk.json create mode 100644 homeassistant/components/senseme/translations/sk.json create mode 100644 homeassistant/components/senz/translations/es.json create mode 100644 homeassistant/components/slack/translations/es.json create mode 100644 homeassistant/components/slack/translations/it.json create mode 100644 homeassistant/components/slack/translations/sk.json create mode 100644 homeassistant/components/slack/translations/tr.json create mode 100644 homeassistant/components/slimproto/translations/es.json create mode 100644 homeassistant/components/sql/translations/es.json create mode 100644 homeassistant/components/steam_online/translations/es.json create mode 100644 homeassistant/components/switch_as_x/translations/es.json create mode 100644 homeassistant/components/tankerkoenig/translations/es.json create mode 100644 homeassistant/components/tankerkoenig/translations/sk.json create mode 100644 homeassistant/components/tautulli/translations/es.json create mode 100644 homeassistant/components/threshold/translations/es.json create mode 100644 homeassistant/components/tod/translations/es.json create mode 100644 homeassistant/components/tomorrowio/translations/es.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.es.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.sk.json create mode 100644 homeassistant/components/tomorrowio/translations/sk.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/es.json create mode 100644 homeassistant/components/trafikverket_train/translations/es.json create mode 100644 homeassistant/components/tuya/translations/select.sk.json create mode 100644 homeassistant/components/tuya/translations/sensor.sk.json create mode 100644 homeassistant/components/ukraine_alarm/translations/tr.json create mode 100644 homeassistant/components/upb/translations/sk.json create mode 100644 homeassistant/components/uptimerobot/translations/sensor.es.json create mode 100644 homeassistant/components/utility_meter/translations/es.json create mode 100644 homeassistant/components/vulcan/translations/es.json create mode 100644 homeassistant/components/vulcan/translations/sk.json create mode 100644 homeassistant/components/whois/translations/sk.json create mode 100644 homeassistant/components/ws66i/translations/es.json create mode 100644 homeassistant/components/ws66i/translations/tr.json create mode 100644 homeassistant/components/xiaomi_miio/translations/select.sk.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/sk.json diff --git a/homeassistant/components/adax/translations/sk.json b/homeassistant/components/adax/translations/sk.json index 2c3ed1dd930..8bacfc145e2 100644 --- a/homeassistant/components/adax/translations/sk.json +++ b/homeassistant/components/adax/translations/sk.json @@ -5,6 +5,13 @@ }, "error": { "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "password": "Heslo" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/es.json b/homeassistant/components/adguard/translations/es.json index 5750808ab76..7fc2f972fa1 100644 --- a/homeassistant/components/adguard/translations/es.json +++ b/homeassistant/components/adguard/translations/es.json @@ -10,16 +10,16 @@ "step": { "hassio_confirm": { "description": "\u00bfDesea configurar Home Assistant para conectarse al AdGuard Home proporcionado por el complemento Supervisor: {addon} ?", - "title": "AdGuard Home a trav\u00e9s del complemento Supervisor" + "title": "AdGuard Home v\u00eda complemento de Home Assistant" }, "user": { "data": { "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", - "ssl": "AdGuard Home utiliza un certificado SSL", + "ssl": "Utiliza un certificado SSL", "username": "Usuario", - "verify_ssl": "AdGuard Home utiliza un certificado apropiado" + "verify_ssl": "Verificar certificado SSL" }, "description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control." } diff --git a/homeassistant/components/airthings/translations/es.json b/homeassistant/components/airthings/translations/es.json index 55dd0c10026..5feddd20875 100644 --- a/homeassistant/components/airthings/translations/es.json +++ b/homeassistant/components/airthings/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada" + "already_configured": "La cuenta ya est\u00e1 configurada" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/airvisual/translations/es.json b/homeassistant/components/airvisual/translations/es.json index 6e7ea6e6903..b51d3035fe5 100644 --- a/homeassistant/components/airvisual/translations/es.json +++ b/homeassistant/components/airvisual/translations/es.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "No se pudo conectar", "general_error": "Se ha producido un error desconocido.", - "invalid_api_key": "Se proporciona una clave API no v\u00e1lida.", + "invalid_api_key": "Clave API no v\u00e1lida", "location_not_found": "Ubicaci\u00f3n no encontrada" }, "step": { @@ -32,7 +32,7 @@ }, "node_pro": { "data": { - "ip_address": "Direcci\u00f3n IP/Nombre de host de la Unidad", + "ip_address": "Host", "password": "Contrase\u00f1a" }, "description": "Monitorizar una unidad personal AirVisual. La contrase\u00f1a puede ser recuperada desde la interfaz de la unidad.", diff --git a/homeassistant/components/airvisual/translations/sensor.sk.json b/homeassistant/components/airvisual/translations/sensor.sk.json new file mode 100644 index 00000000000..6d640d965b9 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.sk.json @@ -0,0 +1,7 @@ +{ + "state": { + "airvisual__pollutant_level": { + "good": "Dobr\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/es.json b/homeassistant/components/airzone/translations/es.json new file mode 100644 index 00000000000..fbb00d71bf0 --- /dev/null +++ b/homeassistant/components/airzone/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "invalid_system_id": "ID de sistema Airzone inv\u00e1lido" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "description": "Configura la integraci\u00f3n Airzone." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/ca.json b/homeassistant/components/aladdin_connect/translations/ca.json index c7115a32099..41cb5c550fa 100644 --- a/homeassistant/components/aladdin_connect/translations/ca.json +++ b/homeassistant/components/aladdin_connect/translations/ca.json @@ -13,6 +13,7 @@ "data": { "password": "Contrasenya" }, + "description": "La integraci\u00f3 d'Aladdin Connect ha de tornar a autenticar-se amb el teu compte", "title": "Reautenticaci\u00f3 de la integraci\u00f3" }, "user": { diff --git a/homeassistant/components/aladdin_connect/translations/es.json b/homeassistant/components/aladdin_connect/translations/es.json new file mode 100644 index 00000000000..67e509e2626 --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + }, + "error": { + "cannot_connect": "Error al conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "title": "Reautenticaci\u00f3n de la integraci\u00f3n" + }, + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/it.json b/homeassistant/components/aladdin_connect/translations/it.json new file mode 100644 index 00000000000..997d7f36e12 --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/it.json @@ -0,0 +1,27 @@ +{ + "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" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "L'integrazione Aladdin Connect deve autenticare nuovamente il tuo account", + "title": "Autentica nuovamente l'integrazione" + }, + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/sk.json b/homeassistant/components/aladdin_connect/translations/sk.json new file mode 100644 index 00000000000..c9a7b4c204a --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/tr.json b/homeassistant/components/aladdin_connect/translations/tr.json new file mode 100644 index 00000000000..21e063e21ee --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/tr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "Aladdin Connect entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/translations/es.json b/homeassistant/components/alarm_control_panel/translations/es.json index a76c6bd5af9..6255f8cc574 100644 --- a/homeassistant/components/alarm_control_panel/translations/es.json +++ b/homeassistant/components/alarm_control_panel/translations/es.json @@ -40,5 +40,5 @@ "triggered": "Disparada" } }, - "title": "Panel de control de alarmas" + "title": "Panel de control de alarma" } \ No newline at end of file diff --git a/homeassistant/components/almond/translations/es.json b/homeassistant/components/almond/translations/es.json index 4dc5e4ee1c0..83e78317741 100644 --- a/homeassistant/components/almond/translations/es.json +++ b/homeassistant/components/almond/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "cannot_connect": "No se puede conectar al servidor Almond.", - "missing_configuration": "Consulte la documentaci\u00f3n sobre c\u00f3mo configurar Almond.", + "cannot_connect": "No se pudo conectar", + "missing_configuration": "El componente no est\u00e1 configurado. Mira su documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, diff --git a/homeassistant/components/apple_tv/translations/es.json b/homeassistant/components/apple_tv/translations/es.json index 65f096ca6ae..3f22edd8d77 100644 --- a/homeassistant/components/apple_tv/translations/es.json +++ b/homeassistant/components/apple_tv/translations/es.json @@ -21,7 +21,7 @@ "no_usable_service": "Se encontr\u00f3 un dispositivo, pero no se pudo identificar ninguna manera de establecer una conexi\u00f3n con \u00e9l. Si sigues viendo este mensaje, intenta especificar su direcci\u00f3n IP o reiniciar el Apple TV.", "unknown": "Error inesperado" }, - "flow_title": "Apple TV: {name}", + "flow_title": "{name} ({type})", "step": { "confirm": { "description": "Est\u00e1s a punto de a\u00f1adir `{name}` con el tipo `{type}` en Home Assistant.\n\n**Para completar el proceso, puede que tengas que introducir varios c\u00f3digos PIN.**\n\nTen en cuenta que *no* podr\u00e1s apagar tu Apple TV con esta integraci\u00f3n. \u00a1S\u00f3lo se apagar\u00e1 el reproductor de medios de Home Assistant!", @@ -47,7 +47,7 @@ "title": "No es posible el emparejamiento" }, "reconfigure": { - "description": "Este Apple TV est\u00e1 experimentando algunos problemas de conexi\u00f3n y debe ser reconfigurado.", + "description": "Vuelve a configurar este dispositivo para restablecer su funcionamiento.", "title": "Reconfiguraci\u00f3n del dispositivo" }, "service_problem": { diff --git a/homeassistant/components/application_credentials/translations/es.json b/homeassistant/components/application_credentials/translations/es.json new file mode 100644 index 00000000000..bf7462ead66 --- /dev/null +++ b/homeassistant/components/application_credentials/translations/es.json @@ -0,0 +1,3 @@ +{ + "title": "Credenciales de la aplicaci\u00f3n" +} \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/es.json b/homeassistant/components/aseko_pool_live/translations/es.json index 419359a942c..79c199d012d 100644 --- a/homeassistant/components/aseko_pool_live/translations/es.json +++ b/homeassistant/components/aseko_pool_live/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada" + "already_configured": "La cuenta ya est\u00e1 configurada" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/asuswrt/translations/es.json b/homeassistant/components/asuswrt/translations/es.json index c5792babf00..8a7876bb45b 100644 --- a/homeassistant/components/asuswrt/translations/es.json +++ b/homeassistant/components/asuswrt/translations/es.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "invalid_unique_id": "No se pudo determinar ning\u00fan identificador \u00fanico v\u00e1lido del dispositivo", "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { @@ -18,7 +19,7 @@ "mode": "Modo", "name": "Nombre", "password": "Contrase\u00f1a", - "port": "Puerto", + "port": "Puerto (d\u00e9jalo vac\u00edo por el predeterminado del protocolo)", "protocol": "Protocolo de comunicaci\u00f3n a utilizar", "ssh_key": "Ruta de acceso a su archivo de clave SSH (en lugar de la contrase\u00f1a)", "username": "Nombre de usuario" diff --git a/homeassistant/components/atag/translations/sk.json b/homeassistant/components/atag/translations/sk.json index 892b8b2cd91..5b7f75998d7 100644 --- a/homeassistant/components/atag/translations/sk.json +++ b/homeassistant/components/atag/translations/sk.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Zariadenie je u\u017e nakonfigurovan\u00e9" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/august/translations/es.json b/homeassistant/components/august/translations/es.json index a660d11f996..cd50020bb2a 100644 --- a/homeassistant/components/august/translations/es.json +++ b/homeassistant/components/august/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/aussie_broadband/translations/es.json b/homeassistant/components/aussie_broadband/translations/es.json index ff13c88c598..e0df929fe9e 100644 --- a/homeassistant/components/aussie_broadband/translations/es.json +++ b/homeassistant/components/aussie_broadband/translations/es.json @@ -1,11 +1,17 @@ { "config": { "abort": { - "no_services_found": "No se han encontrado servicios para esta cuenta" + "no_services_found": "No se han encontrado servicios para esta cuenta", + "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + }, + "error": { + "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "reauth": { - "description": "Actualizar la contrase\u00f1a de {username}" + "description": "Actualizar la contrase\u00f1a de {username}", + "title": "Reautenticaci\u00f3n de la integraci\u00f3n" }, "reauth_confirm": { "data": { @@ -19,6 +25,19 @@ "services": "Servicios" }, "title": "Seleccionar Servicios" + }, + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "Selecciona servicios" } } } diff --git a/homeassistant/components/aussie_broadband/translations/sk.json b/homeassistant/components/aussie_broadband/translations/sk.json index a8cf7db8bbf..2d028b5f36c 100644 --- a/homeassistant/components/aussie_broadband/translations/sk.json +++ b/homeassistant/components/aussie_broadband/translations/sk.json @@ -5,11 +5,21 @@ }, "error": { "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "service": { + "title": "Vyberte slu\u017eby" + } } }, "options": { "abort": { "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "init": { + "title": "Vyberte slu\u017eby" + } } } } \ No newline at end of file diff --git a/homeassistant/components/awair/translations/es.json b/homeassistant/components/awair/translations/es.json index e87ce031b95..1a3240d3802 100644 --- a/homeassistant/components/awair/translations/es.json +++ b/homeassistant/components/awair/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "no_devices_found": "No se encontraron dispositivos en la red", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, diff --git a/homeassistant/components/azure_devops/translations/es.json b/homeassistant/components/azure_devops/translations/es.json index 1055fdebf41..c71fc22b1bd 100644 --- a/homeassistant/components/azure_devops/translations/es.json +++ b/homeassistant/components/azure_devops/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", - "reauth_successful": "Token de acceso actualizado correctamente " + "already_configured": "La cuenta ya est\u00e1 configurada", + "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/azure_event_hub/translations/sk.json b/homeassistant/components/azure_event_hub/translations/sk.json new file mode 100644 index 00000000000..3f20d345b26 --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/ca.json b/homeassistant/components/baf/translations/ca.json new file mode 100644 index 00000000000..f471d071017 --- /dev/null +++ b/homeassistant/components/baf/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "ipv6_not_supported": "IPv6 no est\u00e0 suportat." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "flow_title": "{name} - {model} ({ip_address})", + "step": { + "discovery_confirm": { + "description": "Vols configurar {name} - {model} ({ip_address})?" + }, + "user": { + "data": { + "ip_address": "Adre\u00e7a IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/es.json b/homeassistant/components/baf/translations/es.json new file mode 100644 index 00000000000..4e2800090d2 --- /dev/null +++ b/homeassistant/components/baf/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "ipv6_not_supported": "IPv6 no est\u00e1 soportado." + }, + "error": { + "cannot_connect": "Error al conectar", + "unknown": "Error Inesperado" + }, + "flow_title": "{name} - {model} ({ip_address})", + "step": { + "discovery_confirm": { + "description": "\u00bfQuieres configurar {name} - {model} ({ip_address})?" + }, + "user": { + "data": { + "ip_address": "Direcci\u00f3n IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/et.json b/homeassistant/components/baf/translations/et.json new file mode 100644 index 00000000000..e958eb4545d --- /dev/null +++ b/homeassistant/components/baf/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "ipv6_not_supported": "IPv6 ei ole toetatud." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "{name} - {model} ({ip_address})", + "step": { + "discovery_confirm": { + "description": "Kas seadistada {name} \u2013 {model} ( {ip_address} )?" + }, + "user": { + "data": { + "ip_address": "IP aadress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/fr.json b/homeassistant/components/baf/translations/fr.json new file mode 100644 index 00000000000..8088de223c3 --- /dev/null +++ b/homeassistant/components/baf/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "ipv6_not_supported": "IPv6 n'est pas pris en charge." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "flow_title": "{name} - {model} ({ip_address})", + "step": { + "discovery_confirm": { + "description": "Voulez-vous configurer {name} - {model} ({ip_address})\u00a0?" + }, + "user": { + "data": { + "ip_address": "Adresse IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/it.json b/homeassistant/components/baf/translations/it.json new file mode 100644 index 00000000000..86fcdab723b --- /dev/null +++ b/homeassistant/components/baf/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "ipv6_not_supported": "IPv6 non \u00e8 supportato." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "flow_title": "{name} - {model} ({ip_address})", + "step": { + "discovery_confirm": { + "description": "Vuoi configurare {name} - {model} ({ip_address})?" + }, + "user": { + "data": { + "ip_address": "Indirizzo IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/pt-BR.json b/homeassistant/components/baf/translations/pt-BR.json new file mode 100644 index 00000000000..72ce0dd06fc --- /dev/null +++ b/homeassistant/components/baf/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "ipv6_not_supported": "IPv6 n\u00e3o \u00e9 suportado." + }, + "error": { + "cannot_connect": "Falhou ao se conectar", + "unknown": "Erro inesperado" + }, + "flow_title": "{name} - {model} ({ip_address})", + "step": { + "discovery_confirm": { + "description": "Deseja configurar {name} - {model} ({ip_address})?" + }, + "user": { + "data": { + "ip_address": "Endere\u00e7o IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/sk.json b/homeassistant/components/baf/translations/sk.json new file mode 100644 index 00000000000..5ca1b9820a9 --- /dev/null +++ b/homeassistant/components/baf/translations/sk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie je u\u017e nakonfigurovan\u00e9", + "ipv6_not_supported": "IPv6 nie je podporovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/tr.json b/homeassistant/components/baf/translations/tr.json new file mode 100644 index 00000000000..ffa458b7366 --- /dev/null +++ b/homeassistant/components/baf/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "ipv6_not_supported": "IPv6 desteklenmiyor." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "{name} - {model} ({ip_address})", + "step": { + "discovery_confirm": { + "description": "{name} - {model} ( {ip_address} ) kurulumunu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "ip_address": "IP Adresi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/es.json b/homeassistant/components/binary_sensor/translations/es.json index 1e328150c39..5875804175e 100644 --- a/homeassistant/components/binary_sensor/translations/es.json +++ b/homeassistant/components/binary_sensor/translations/es.json @@ -83,7 +83,7 @@ "not_moist": "{entity_name} se sec\u00f3", "not_moving": "{entity_name} dej\u00f3 de moverse", "not_occupied": "{entity_name} no est\u00e1 ocupado", - "not_opened": "{entity_name} cerrado", + "not_opened": "{entity_name} se cierra", "not_plugged_in": "{entity_name} desconectado", "not_powered": "{entity_name} no est\u00e1 activado", "not_present": "{entity_name} no est\u00e1 presente", @@ -92,7 +92,7 @@ "not_unsafe": "{entity_name} se volvi\u00f3 seguro", "occupied": "{entity_name} se convirti\u00f3 en ocupado", "opened": "{entity_name} abierto", - "plugged_in": "{entity_name} conectado", + "plugged_in": "{entity_name} se ha enchufado", "powered": "{entity_name} alimentado", "present": "{entity_name} presente", "problem": "{entity_name} empez\u00f3 a detectar problemas", @@ -135,6 +135,7 @@ "on": "Cargando" }, "carbon_monoxide": { + "off": "Libre", "on": "Detectado" }, "co": { @@ -198,7 +199,7 @@ "on": "Enchufado" }, "presence": { - "off": "Fuera de casa", + "off": "Fuera", "on": "En casa" }, "problem": { diff --git a/homeassistant/components/binary_sensor/translations/sk.json b/homeassistant/components/binary_sensor/translations/sk.json index 5cff82615ae..89e6288a979 100644 --- a/homeassistant/components/binary_sensor/translations/sk.json +++ b/homeassistant/components/binary_sensor/translations/sk.json @@ -1,4 +1,8 @@ { + "device_class": { + "motion": "pohyb", + "vibration": "vibr\u00e1cia" + }, "state": { "_": { "off": "Neakt\u00edvny", diff --git a/homeassistant/components/blebox/translations/es.json b/homeassistant/components/blebox/translations/es.json index a415edd9809..c62caa44eec 100644 --- a/homeassistant/components/blebox/translations/es.json +++ b/homeassistant/components/blebox/translations/es.json @@ -9,7 +9,7 @@ "unknown": "Error inesperado", "unsupported_version": "El dispositivo BleBox tiene un firmware anticuado. Por favor, actual\u00edzalo primero." }, - "flow_title": "Dispositivo BleBox: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/es.json b/homeassistant/components/braviatv/translations/es.json index 2cc3b05010c..d095deea33b 100644 --- a/homeassistant/components/braviatv/translations/es.json +++ b/homeassistant/components/braviatv/translations/es.json @@ -15,7 +15,7 @@ "pin": "C\u00f3digo PIN" }, "description": "Introduce el c\u00f3digo PIN que se muestra en el televisor Sony Bravia.\n\nSi no se muestra ning\u00fan c\u00f3digo PIN, necesitas eliminar el registro de Home Assistant de tu televisor, ve a: Configuraci\u00f3n -> Red -> Configuraci\u00f3n del dispositivo remoto -> Eliminar el dispositivo remoto.", - "title": "Autorizar televisor Sony Bravia" + "title": "Autorizaci\u00f3n del televisor Sony Bravia" }, "user": { "data": { diff --git a/homeassistant/components/brother/translations/es.json b/homeassistant/components/brother/translations/es.json index bc6aa749445..9c327a2887a 100644 --- a/homeassistant/components/brother/translations/es.json +++ b/homeassistant/components/brother/translations/es.json @@ -9,7 +9,7 @@ "snmp_error": "El servidor SNMP est\u00e1 apagado o la impresora no es compatible.", "wrong_host": "Nombre del host o direcci\u00f3n IP no v\u00e1lidos." }, - "flow_title": "Impresora Brother: {model} {serial_number}", + "flow_title": "{model} {serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/brunt/translations/es.json b/homeassistant/components/brunt/translations/es.json index 61370b2791f..362ef22b838 100644 --- a/homeassistant/components/brunt/translations/es.json +++ b/homeassistant/components/brunt/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/brunt/translations/sk.json b/homeassistant/components/brunt/translations/sk.json index 71a7aea5018..3f00701d0ba 100644 --- a/homeassistant/components/brunt/translations/sk.json +++ b/homeassistant/components/brunt/translations/sk.json @@ -5,6 +5,13 @@ }, "error": { "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "username": "U\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/es.json b/homeassistant/components/bsblan/translations/es.json index 691136f5441..830752dd863 100644 --- a/homeassistant/components/bsblan/translations/es.json +++ b/homeassistant/components/bsblan/translations/es.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "No se pudo conectar" }, - "flow_title": "BSB-Lan: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/button/translations/sk.json b/homeassistant/components/button/translations/sk.json new file mode 100644 index 00000000000..5dfc88234c6 --- /dev/null +++ b/homeassistant/components/button/translations/sk.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "press": "Stla\u010dte tla\u010didlo {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/translations/es.json b/homeassistant/components/cast/translations/es.json index d38067588a8..39e34975af8 100644 --- a/homeassistant/components/cast/translations/es.json +++ b/homeassistant/components/cast/translations/es.json @@ -9,7 +9,7 @@ "step": { "config": { "data": { - "known_hosts": "Lista opcional de hosts conocidos si el descubrimiento mDNS no funciona." + "known_hosts": "Anfitriones conocidos" }, "description": "Introduce la configuraci\u00f3n de Google Cast.", "title": "Configuraci\u00f3n de Google Cast" diff --git a/homeassistant/components/climacell/translations/es.json b/homeassistant/components/climacell/translations/es.json index 4e709f03ad1..dba1957dd21 100644 --- a/homeassistant/components/climacell/translations/es.json +++ b/homeassistant/components/climacell/translations/es.json @@ -26,7 +26,7 @@ "timestep": "Min. Entre pron\u00f3sticos de NowCast" }, "description": "Si elige habilitar la entidad de pron\u00f3stico \"nowcast\", puede configurar el n\u00famero de minutos entre cada pron\u00f3stico. El n\u00famero de pron\u00f3sticos proporcionados depende del n\u00famero de minutos elegidos entre los pron\u00f3sticos.", - "title": "Actualizar las opciones ClimaCell" + "title": "Actualizaci\u00f3n de opciones de ClimaCell" } } }, diff --git a/homeassistant/components/climacell/translations/sensor.sk.json b/homeassistant/components/climacell/translations/sensor.sk.json new file mode 100644 index 00000000000..843169b2f3b --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.sk.json @@ -0,0 +1,7 @@ +{ + "state": { + "climacell__health_concern": { + "unhealthy": "Nezdrav\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/translations/es.json b/homeassistant/components/climate/translations/es.json index a7258609d59..09806ddf5ef 100644 --- a/homeassistant/components/climate/translations/es.json +++ b/homeassistant/components/climate/translations/es.json @@ -17,7 +17,7 @@ "state": { "_": { "auto": "Autom\u00e1tico", - "cool": "Fr\u00edo", + "cool": "Enfr\u00eda", "dry": "Seco", "fan_only": "Solo ventilador", "heat": "Calor", diff --git a/homeassistant/components/coinbase/translations/es.json b/homeassistant/components/coinbase/translations/es.json index 311fcdb8546..d8961ee6b3a 100644 --- a/homeassistant/components/coinbase/translations/es.json +++ b/homeassistant/components/coinbase/translations/es.json @@ -26,6 +26,7 @@ "options": { "error": { "currency_unavaliable": "La API de Coinbase no proporciona uno o m\u00e1s de los saldos de divisas solicitados.", + "exchange_rate_unavailable": "El API de Coinbase no proporciona alguno/s de los tipos de cambio que has solicitado.", "exchange_rate_unavaliable": "Coinbase no proporciona uno o m\u00e1s de los tipos de cambio solicitados.", "unknown": "Error inesperado" }, @@ -34,7 +35,8 @@ "data": { "account_balance_currencies": "Saldos de la cartera para informar.", "exchange_base": "Moneda base para sensores de tipo de cambio.", - "exchange_rate_currencies": "Tipos de cambio a informar." + "exchange_rate_currencies": "Tipos de cambio a informar.", + "exchnage_rate_precision": "N\u00famero de posiciones decimales para los tipos de cambio." }, "description": "Ajustar las opciones de Coinbase" } diff --git a/homeassistant/components/coinbase/translations/sk.json b/homeassistant/components/coinbase/translations/sk.json index ff853127803..f161003be52 100644 --- a/homeassistant/components/coinbase/translations/sk.json +++ b/homeassistant/components/coinbase/translations/sk.json @@ -10,5 +10,10 @@ } } } + }, + "options": { + "error": { + "currency_unavailable": "Jeden alebo viacero po\u017eadovan\u00fdch zostatkov mien nie s\u00fa poskytovan\u00e9 Va\u0161\u00edm Coinbase API." + } } } \ No newline at end of file diff --git a/homeassistant/components/configurator/translations/es.json b/homeassistant/components/configurator/translations/es.json index dffb90e6d49..50ea2f68a7c 100644 --- a/homeassistant/components/configurator/translations/es.json +++ b/homeassistant/components/configurator/translations/es.json @@ -1,7 +1,7 @@ { "state": { "_": { - "configure": "Configurar", + "configure": "Configura", "configured": "Configurado" } }, diff --git a/homeassistant/components/cover/translations/es.json b/homeassistant/components/cover/translations/es.json index 181cda45fb5..550c9d368e9 100644 --- a/homeassistant/components/cover/translations/es.json +++ b/homeassistant/components/cover/translations/es.json @@ -35,5 +35,5 @@ "stopped": "Detenido" } }, - "title": "Persiana" + "title": "Cubierta" } \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/es.json b/homeassistant/components/crownstone/translations/es.json index f52b0074322..b71c69db33c 100644 --- a/homeassistant/components/crownstone/translations/es.json +++ b/homeassistant/components/crownstone/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "usb_setup_complete": "Configuraci\u00f3n USB de Crownstone completa.", "usb_setup_unsuccessful": "La configuraci\u00f3n del USB de Crownstone no tuvo \u00e9xito." }, diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index 6822b71d564..0174c4cf76d 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -2,20 +2,21 @@ "config": { "abort": { "already_configured": "La pasarela ya est\u00e1 configurada", - "already_in_progress": "El flujo de configuraci\u00f3n para la pasarela ya est\u00e1 en marcha.", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "no_bridges": "No se han descubierto pasarelas deCONZ", "no_hardware_available": "No hay hardware de radio conectado a deCONZ", "not_deconz_bridge": "No es una pasarela deCONZ", "updated_instance": "Instancia deCONZ actualizada con nueva direcci\u00f3n de host" }, "error": { + "linking_not_possible": "No se ha podido enlazar con la pasarela", "no_key": "No se pudo obtener una clave API" }, "flow_title": "{host}", "step": { "hassio_confirm": { "description": "\u00bfQuieres configurar Home Assistant para que se conecte al gateway de deCONZ proporcionado por el add-on {addon} de Supervisor?", - "title": "Add-on deCONZ Zigbee v\u00eda Supervisor" + "title": "Pasarela de enlace de CONZ Zigbee v\u00eda complemento de Home Assistant" }, "link": { "description": "Desbloquea tu gateway de deCONZ para registrarte con Home Assistant.\n\n1. Dir\u00edgete a deCONZ Settings -> Gateway -> Advanced\n2. Pulsa el bot\u00f3n \"Authenticate app\"", diff --git a/homeassistant/components/deluge/translations/es.json b/homeassistant/components/deluge/translations/es.json new file mode 100644 index 00000000000..7ba1b8c3bf9 --- /dev/null +++ b/homeassistant/components/deluge/translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Usuario", + "web_port": "Puerto web (para el servicio de visita)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/sk.json b/homeassistant/components/deluge/translations/sk.json new file mode 100644 index 00000000000..0fbba9ccd6b --- /dev/null +++ b/homeassistant/components/deluge/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port", + "username": "U\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/es.json b/homeassistant/components/denonavr/translations/es.json index 04bcb55ace5..33051047719 100644 --- a/homeassistant/components/denonavr/translations/es.json +++ b/homeassistant/components/denonavr/translations/es.json @@ -10,7 +10,7 @@ "error": { "discovery_error": "Error detectando un Receptor AVR Denon en Red" }, - "flow_title": "Receptor AVR Denon en Red: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Por favor confirma la adici\u00f3n del receptor", diff --git a/homeassistant/components/derivative/translations/es.json b/homeassistant/components/derivative/translations/es.json new file mode 100644 index 00000000000..114c2102678 --- /dev/null +++ b/homeassistant/components/derivative/translations/es.json @@ -0,0 +1,43 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nombre", + "source": "Sensor de entrada", + "time_window": "Ventana de tiempo", + "unit_prefix": "Prefijo m\u00e9trico", + "unit_time": "Unidad de tiempo" + }, + "description": "Crea un sensor que ama la derivada de otro sensor.", + "title": "A\u00f1ade sensor derivativo" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Nombre", + "round": "Precisi\u00f3n", + "unit_prefix": "Prefijo m\u00e9trico", + "unit_time": "Unidad de tiempo" + }, + "data_description": { + "round": "Controla el n\u00famero de d\u00edgitos decimales en la salida.", + "unit_prefix": "a salida se escalar\u00e1 seg\u00fan el prefijo m\u00e9trico y la unidad de tiempo de la derivada seleccionados." + } + }, + "options": { + "data": { + "name": "Nombre", + "time_window": "Ventana de tiempo", + "unit_prefix": "Prefijo m\u00e9trico", + "unit_time": "Unidad de tiempo" + }, + "description": "Crea un sensor que ama la derivada de otro sensor." + } + } + }, + "title": "Sensor derivatiu" +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/sk.json b/homeassistant/components/derivative/translations/sk.json new file mode 100644 index 00000000000..cc4a1cc7cb9 --- /dev/null +++ b/homeassistant/components/derivative/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Meno" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/translations/es.json b/homeassistant/components/device_tracker/translations/es.json index eccef30e765..47948da66ec 100644 --- a/homeassistant/components/device_tracker/translations/es.json +++ b/homeassistant/components/device_tracker/translations/es.json @@ -12,8 +12,8 @@ "state": { "_": { "home": "En casa", - "not_home": "Fuera de casa" + "not_home": "Fuera" } }, - "title": "Rastreador de dispositivo" + "title": "Seguimiento de dispositivos" } \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/es.json b/homeassistant/components/devolo_home_control/translations/es.json index b4a7a873aaa..91a06530273 100644 --- a/homeassistant/components/devolo_home_control/translations/es.json +++ b/homeassistant/components/devolo_home_control/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/dexcom/translations/es.json b/homeassistant/components/dexcom/translations/es.json index 934c40be05e..24d06db3ee6 100644 --- a/homeassistant/components/dexcom/translations/es.json +++ b/homeassistant/components/dexcom/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada" + "already_configured": "La cuenta ya est\u00e1 configurada" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/dialogflow/translations/es.json b/homeassistant/components/dialogflow/translations/es.json index ca2370f9086..8c2c5b4993e 100644 --- a/homeassistant/components/dialogflow/translations/es.json +++ b/homeassistant/components/dialogflow/translations/es.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "No conectado a Home Assistant Cloud.", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, diff --git a/homeassistant/components/directv/translations/es.json b/homeassistant/components/directv/translations/es.json index f1d896e698b..92cf160462c 100644 --- a/homeassistant/components/directv/translations/es.json +++ b/homeassistant/components/directv/translations/es.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Error al conectar" }, - "flow_title": "DirecTV: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "\u00bfQuieres configurar {name}?" diff --git a/homeassistant/components/discord/translations/es.json b/homeassistant/components/discord/translations/es.json new file mode 100644 index 00000000000..768afb877f3 --- /dev/null +++ b/homeassistant/components/discord/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + }, + "error": { + "cannot_connect": "Fall\u00f3 la conexi\u00f3n" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "Token API" + }, + "description": "Consulta la documentaci\u00f3n para obtener tu clave de bote de Discord.\n\n{url}" + }, + "user": { + "data": { + "api_token": "Token API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/es.json b/homeassistant/components/dlna_dmr/translations/es.json index 6c7bd0ae4e3..6878a05b766 100644 --- a/homeassistant/components/dlna_dmr/translations/es.json +++ b/homeassistant/components/dlna_dmr/translations/es.json @@ -47,6 +47,7 @@ "step": { "init": { "data": { + "browse_unfiltered": "Muestra archivos multimedia incompatibles mientras navega", "callback_url_override": "URL de devoluci\u00f3n de llamada del detector de eventos", "listen_port": "Puerto de escucha de eventos (aleatorio si no se establece)", "poll_availability": "Sondeo para la disponibilidad del dispositivo" diff --git a/homeassistant/components/dlna_dms/translations/es.json b/homeassistant/components/dlna_dms/translations/es.json index b4bc90a666a..1ce13967ea5 100644 --- a/homeassistant/components/dlna_dms/translations/es.json +++ b/homeassistant/components/dlna_dms/translations/es.json @@ -1,10 +1,15 @@ { "config": { "abort": { + "bad_ssdp": "Falta un valor necesario en los datos SSDP", "no_devices_found": "No se han encontrado dispositivos en la red" }, + "flow_title": "{name}", "step": { "user": { + "data": { + "host": "Host" + }, "description": "Escoge un dispositivo a configurar" } } diff --git a/homeassistant/components/elgato/translations/es.json b/homeassistant/components/elgato/translations/es.json index 940272f5562..2cdbd7e6ae1 100644 --- a/homeassistant/components/elgato/translations/es.json +++ b/homeassistant/components/elgato/translations/es.json @@ -1,20 +1,20 @@ { "config": { "abort": { - "already_configured": "Este dispositivo Elgato Key Light ya est\u00e1 configurado.", + "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar" }, "error": { "cannot_connect": "No se pudo conectar" }, - "flow_title": "Elgato Key Light: {serial_number}", + "flow_title": "{serial_number}", "step": { "user": { "data": { - "host": "Host o direcci\u00f3n IP", + "host": "Host", "port": "Puerto" }, - "description": "Configura tu Elgato Key Light para integrarlo con Home Assistant." + "description": "Configura la integraci\u00f3n de Elgato Light con Home Assistant." }, "zeroconf_confirm": { "description": "\u00bfDesea a\u00f1adir Elgato Key Light con el n\u00famero de serie `{serial_number}` a Home Assistant?", diff --git a/homeassistant/components/elkm1/translations/es.json b/homeassistant/components/elkm1/translations/es.json index 46e25dd288f..2bd75f83e2f 100644 --- a/homeassistant/components/elkm1/translations/es.json +++ b/homeassistant/components/elkm1/translations/es.json @@ -4,7 +4,8 @@ "address_already_configured": "Ya est\u00e1 configurado un Elk-M1 con esta direcci\u00f3n", "already_configured": "Ya est\u00e1 configurado un Elk-M1 con este prefijo", "already_in_progress": "La configuraci\u00f3n ya se encuentra en proceso", - "cannot_connect": "Error al conectar" + "cannot_connect": "Error al conectar", + "unknown": "Error inesperado" }, "error": { "cannot_connect": "No se pudo conectar", @@ -20,7 +21,7 @@ "temperature_unit": "La unidad de temperatura que el ElkM1 usa.", "username": "Usuario" }, - "description": "Con\u00e9ctese al sistema detectado: {mac_address} ({host})", + "description": "Con\u00e9ctate al sistema descubierto: {mac_address} ({host})", "title": "Conectar con Control Elk-M1" }, "manual_connection": { diff --git a/homeassistant/components/emulated_roku/translations/es.json b/homeassistant/components/emulated_roku/translations/es.json index 1bc53c03f19..abbcac75479 100644 --- a/homeassistant/components/emulated_roku/translations/es.json +++ b/homeassistant/components/emulated_roku/translations/es.json @@ -7,7 +7,7 @@ "user": { "data": { "advertise_ip": "IP para anunciar", - "advertise_port": "Puerto para anunciar", + "advertise_port": "Puerto de advertencias", "host_ip": "Direcci\u00f3n IP del host", "listen_port": "Puerto de escucha", "name": "Nombre", diff --git a/homeassistant/components/esphome/translations/es.json b/homeassistant/components/esphome/translations/es.json index f7fd73cd227..c682ddab294 100644 --- a/homeassistant/components/esphome/translations/es.json +++ b/homeassistant/components/esphome/translations/es.json @@ -11,7 +11,7 @@ "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}", + "flow_title": "{name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/ezviz/translations/es.json b/homeassistant/components/ezviz/translations/es.json index a0f624bf8df..eef8343c17a 100644 --- a/homeassistant/components/ezviz/translations/es.json +++ b/homeassistant/components/ezviz/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured_account": "La cuenta ya ha sido configurada", + "already_configured_account": "La cuenta ya est\u00e1 configurada", "ezviz_cloud_account_missing": "Falta la cuenta de Ezviz Cloud. Por favor, reconfigura la cuenta de Ezviz Cloud", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/faa_delays/translations/sk.json b/homeassistant/components/faa_delays/translations/sk.json new file mode 100644 index 00000000000..bbadecd391b --- /dev/null +++ b/homeassistant/components/faa_delays/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_airport": "K\u00f3d letiska je neplatn\u00fd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/translations/es.json b/homeassistant/components/fan/translations/es.json index c4edae6f9ee..85cc347f82d 100644 --- a/homeassistant/components/fan/translations/es.json +++ b/homeassistant/components/fan/translations/es.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "Cambiar {entity_name}", "turn_off": "Desactivar {entity_name}", "turn_on": "Activar {entity_name}" }, diff --git a/homeassistant/components/fan/translations/tr.json b/homeassistant/components/fan/translations/tr.json index 37091db264c..b4f7a9b5291 100644 --- a/homeassistant/components/fan/translations/tr.json +++ b/homeassistant/components/fan/translations/tr.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "{entity_name} de\u011fi\u015ftir", "turn_off": "{entity_name} kapat", "turn_on": "{entity_name} a\u00e7\u0131n" }, diff --git a/homeassistant/components/fibaro/translations/es.json b/homeassistant/components/fibaro/translations/es.json new file mode 100644 index 00000000000..00a7eeb8ece --- /dev/null +++ b/homeassistant/components/fibaro/translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Fall\u00f3 la conexi\u00f3n" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "url": "URL en el format http://HOST/api/", + "username": "Usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/sk.json b/homeassistant/components/fibaro/translations/sk.json new file mode 100644 index 00000000000..649a1e186ee --- /dev/null +++ b/homeassistant/components/fibaro/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "U\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/filesize/translations/es.json b/homeassistant/components/filesize/translations/es.json new file mode 100644 index 00000000000..e2bc079b961 --- /dev/null +++ b/homeassistant/components/filesize/translations/es.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "not_allowed": "Ruta no permitida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/es.json b/homeassistant/components/fireservicerota/translations/es.json index 9f27181dfe5..085bce8f343 100644 --- a/homeassistant/components/fireservicerota/translations/es.json +++ b/homeassistant/components/fireservicerota/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "create_entry": { diff --git a/homeassistant/components/fivem/translations/es.json b/homeassistant/components/fivem/translations/es.json index d8b3f3c6e1c..873119cf4b9 100644 --- a/homeassistant/components/fivem/translations/es.json +++ b/homeassistant/components/fivem/translations/es.json @@ -12,7 +12,7 @@ "step": { "user": { "data": { - "host": "Anfitri\u00f3n", + "host": "Host", "name": "Nombre", "port": "Puerto" } diff --git a/homeassistant/components/flick_electric/translations/es.json b/homeassistant/components/flick_electric/translations/es.json index 435ead31d9a..424c73da24f 100644 --- a/homeassistant/components/flick_electric/translations/es.json +++ b/homeassistant/components/flick_electric/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada" + "already_configured": "La cuenta ya est\u00e1 configurada" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/flunearyou/translations/es.json b/homeassistant/components/flunearyou/translations/es.json index 5d0c05c0c54..5d7b8fb6a6f 100644 --- a/homeassistant/components/flunearyou/translations/es.json +++ b/homeassistant/components/flunearyou/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Estas coordenadas ya est\u00e1n registradas." + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada" }, "error": { "unknown": "Error inesperado" diff --git a/homeassistant/components/forecast_solar/translations/es.json b/homeassistant/components/forecast_solar/translations/es.json index d688c577024..d82bd944202 100644 --- a/homeassistant/components/forecast_solar/translations/es.json +++ b/homeassistant/components/forecast_solar/translations/es.json @@ -22,6 +22,7 @@ "azimuth": "Azimut (360 grados, 0 = Norte, 90 = Este, 180 = Sur, 270 = Oeste)", "damping": "Factor de amortiguaci\u00f3n: ajusta los resultados por la ma\u00f1ana y por la noche", "declination": "Declinaci\u00f3n (0 = Horizontal, 90 = Vertical)", + "inverter_size": "Potencia del inversor (Watts)", "modules power": "Potencia pico total en vatios de tus m\u00f3dulos solares" }, "description": "Estos valores permiten ajustar el resultado de Solar.Forecast. Consulte la documentaci\u00f3n si un campo no est\u00e1 claro." diff --git a/homeassistant/components/forked_daapd/translations/es.json b/homeassistant/components/forked_daapd/translations/es.json index 7ec30e72b1a..54b1adb479f 100644 --- a/homeassistant/components/forked_daapd/translations/es.json +++ b/homeassistant/components/forked_daapd/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado.", + "already_configured": "El dispositivo ya est\u00e1 configurado", "not_forked_daapd": "El dispositivo no es un servidor forked-daapd." }, "error": { @@ -12,7 +12,7 @@ "wrong_password": "Contrase\u00f1a incorrecta.", "wrong_server_type": "La integraci\u00f3n forked-daapd requiere un servidor forked-daapd con versi\u00f3n >= 27.0." }, - "flow_title": "Servidor forked-daapd: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/freebox/translations/es.json b/homeassistant/components/freebox/translations/es.json index fb2da18c6c6..bbd691c9764 100644 --- a/homeassistant/components/freebox/translations/es.json +++ b/homeassistant/components/freebox/translations/es.json @@ -4,9 +4,9 @@ "already_configured": "El dispositivo ya est\u00e1 configurado." }, "error": { - "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", + "cannot_connect": "Fall\u00f3 la conexi\u00f3n", "register_failed": "No se pudo registrar, int\u00e9ntalo de nuevo", - "unknown": "Error desconocido: por favor, int\u00e9ntalo de nuevo m\u00e1s" + "unknown": "Error inesperado" }, "step": { "link": { diff --git a/homeassistant/components/fritz/translations/es.json b/homeassistant/components/fritz/translations/es.json index 7decdcd01f5..e8337dbef60 100644 --- a/homeassistant/components/fritz/translations/es.json +++ b/homeassistant/components/fritz/translations/es.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "ignore_ip6_link_local": "El enlace con direcciones IPv6 locales no est\u00e1 permitido", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/fritzbox/translations/es.json b/homeassistant/components/fritzbox/translations/es.json index fcb240deb77..77c8bb64ee5 100644 --- a/homeassistant/components/fritzbox/translations/es.json +++ b/homeassistant/components/fritzbox/translations/es.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Este AVM FRITZ!Box ya est\u00e1 configurado.", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "ignore_ip6_link_local": "El enlace con direcciones IPv6 locales no est\u00e1 permitido", "no_devices_found": "No se encontraron dispositivos en la red", "not_supported": "Conectado a AVM FRITZ!Box pero no es capaz de controlar dispositivos Smart Home.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" diff --git a/homeassistant/components/fritzbox_callmonitor/translations/es.json b/homeassistant/components/fritzbox_callmonitor/translations/es.json index d6891db5ef9..61e295d3b99 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/es.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/es.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, - "flow_title": "Monitor de llamadas de AVM FRITZ! Box: {name}", + "flow_title": "{name}", "step": { "phonebook": { "data": { diff --git a/homeassistant/components/generic/translations/es.json b/homeassistant/components/generic/translations/es.json new file mode 100644 index 00000000000..90bff434f6d --- /dev/null +++ b/homeassistant/components/generic/translations/es.json @@ -0,0 +1,68 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "error": { + "already_exists": "Ya hay una c\u00e1mara con esa URL de configuraci\u00f3n.", + "invalid_still_image": "La URL no ha devuelto una imagen fija v\u00e1lida", + "no_still_image_or_stream_url": "Tienes que especificar al menos una imagen una URL de flujo", + "stream_http_not_found": "HTTP 404 'Not found' al intentar conectarse al flujo de datos ('stream')", + "stream_no_route_to_host": "No se pudo encontrar el anfitri\u00f3n mientras intentaba conectar al flujo de datos", + "stream_no_video": "El flujo no contiene v\u00eddeo", + "stream_unauthorised": "La autorizaci\u00f3n ha fallado mientras se intentaba conectar con el flujo de datos", + "timeout": "El tiempo m\u00e1ximo de carga de la URL ha expirado", + "unknown": "Error inesperado" + }, + "step": { + "content_type": { + "data": { + "content_type": "Tipos de contenido" + }, + "description": "Especifique el tipo de contenido para el flujo de datos (stream)." + }, + "user": { + "data": { + "authentication": "Autenticaci\u00f3n", + "content_type": "Tipo de contenido", + "framerate": "Frecuencia de visualizaci\u00f3n (Hz)", + "limit_refetch_to_url_change": "Limita la lectura al cambio de URL", + "still_image_url": "URL de imagen fija (ej. http://...)", + "stream_source": "URL origen del flux (p. ex. rtsp://...)", + "username": "Usuario", + "verify_ssl": "Verifica el certificat SSL" + } + } + } + }, + "options": { + "error": { + "already_exists": "Ya hay una c\u00e1mara con esa URL de configuraci\u00f3n.", + "stream_no_route_to_host": "No se pudo encontrar el anfitri\u00f3n mientras intentaba conectar al flujo de datos", + "stream_unauthorised": "La autorizaci\u00f3n ha fallado mientras se intentaba conectar con el flujo de datos", + "timeout": "El tiempo m\u00e1ximo de carga de la URL ha expirado", + "unknown": "Error inesperado" + }, + "step": { + "content_type": { + "data": { + "content_type": "Tipos de contenido" + }, + "description": "Especifique el tipo de contenido para el flujo de datos (stream)." + }, + "init": { + "data": { + "authentication": "Autenticaci\u00f3n", + "content_type": "Tipo de contenido", + "framerate": "Frecuencia de visualizaci\u00f3n (Hz)", + "limit_refetch_to_url_change": "Limita la lectura al cambio de URL", + "password": "Contrase\u00f1a", + "rtsp_transport": "Protocolo de transporte RTSP", + "still_image_url": "URL de imagen fija (ej. http://...)", + "username": "Usuario", + "verify_ssl": "Verifica el certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/tr.json b/homeassistant/components/generic/translations/tr.json index c2fb343f227..d6ba8601beb 100644 --- a/homeassistant/components/generic/translations/tr.json +++ b/homeassistant/components/generic/translations/tr.json @@ -79,8 +79,12 @@ "rtsp_transport": "RTSP aktar\u0131m protokol\u00fc", "still_image_url": "Hareketsiz G\u00f6r\u00fcnt\u00fc URL'si (\u00f6rne\u011fin http://...)", "stream_source": "Ak\u0131\u015f Kayna\u011f\u0131 URL'si (\u00f6r. rtsp://...)", + "use_wallclock_as_timestamps": "Wallclock'u zaman damgas\u0131 olarak kullan\u0131n", "username": "Kullan\u0131c\u0131 Ad\u0131", "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" + }, + "data_description": { + "use_wallclock_as_timestamps": "Bu se\u00e7enek, baz\u0131 kameralarda hatal\u0131 zaman damgas\u0131 uygulamalar\u0131ndan kaynaklanan segmentlere ay\u0131rma veya kilitlenme sorunlar\u0131n\u0131 d\u00fczeltebilir" } } } diff --git a/homeassistant/components/geocaching/translations/es.json b/homeassistant/components/geocaching/translations/es.json new file mode 100644 index 00000000000..8b03adca234 --- /dev/null +++ b/homeassistant/components/geocaching/translations/es.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Mira su documentaci\u00f3n.", + "oauth_error": "Se han recibido datos token inv\u00e1lidos.", + "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + }, + "create_entry": { + "default": "Autenticaci\u00f3n exitosa" + }, + "step": { + "pick_implementation": { + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" + }, + "reauth_confirm": { + "description": "La integraci\u00f3n Geocaching debe volver a autenticarse con tu cuenta", + "title": "Reautenticaci\u00f3n de la integraci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geocaching/translations/tr.json b/homeassistant/components/geocaching/translations/tr.json new file mode 100644 index 00000000000..b2c7a1031a8 --- /dev/null +++ b/homeassistant/components/geocaching/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})", + "oauth_error": "Ge\u00e7ersiz anahtar verileri al\u0131nd\u0131.", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, + "step": { + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + }, + "reauth_confirm": { + "description": "Geocaching entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor", + "title": "Entegrasyonu Yeniden Do\u011frula" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/github/translations/es.json b/homeassistant/components/github/translations/es.json new file mode 100644 index 00000000000..d53004c3cb5 --- /dev/null +++ b/homeassistant/components/github/translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", + "could_not_register": "No se pudo registrar la integraci\u00f3n con GitHub" + }, + "step": { + "repositories": { + "data": { + "repositories": "Seleccione los repositorios a seguir." + }, + "title": "Configuraci\u00f3n de repositorios" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/es.json b/homeassistant/components/goalzero/translations/es.json index fa54d6d6afc..921941d1c16 100644 --- a/homeassistant/components/goalzero/translations/es.json +++ b/homeassistant/components/goalzero/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/google/translations/es.json b/homeassistant/components/google/translations/es.json index 8072ac95d4b..c6d990c2caa 100644 --- a/homeassistant/components/google/translations/es.json +++ b/homeassistant/components/google/translations/es.json @@ -1,11 +1,19 @@ { "config": { "abort": { - "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n." + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", + "oauth_error": "Se han recibido datos token inv\u00e1lidos.", + "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + }, + "create_entry": { + "default": "Autenticaci\u00f3n exitosa" }, "step": { "auth": { "title": "Vincular cuenta de Google" + }, + "pick_implementation": { + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" } } } diff --git a/homeassistant/components/gpslogger/translations/es.json b/homeassistant/components/gpslogger/translations/es.json index 733871c5ee7..ea3221ee2f5 100644 --- a/homeassistant/components/gpslogger/translations/es.json +++ b/homeassistant/components/gpslogger/translations/es.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "No conectado a Home Assistant Cloud.", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, diff --git a/homeassistant/components/group/translations/es.json b/homeassistant/components/group/translations/es.json index e74adda486d..719d42c772a 100644 --- a/homeassistant/components/group/translations/es.json +++ b/homeassistant/components/group/translations/es.json @@ -1,9 +1,17 @@ { "config": { "step": { + "binary_sensor": { + "data": { + "hide_members": "Esconde miembros" + }, + "title": "Agregar grupo" + }, "cover": { "data": { - "name": "Nombre del Grupo" + "hide_members": "Esconde miembros", + "name": "Nombre del Grupo", + "title": "Agregar grupo" }, "description": "Seleccionar opciones de grupo" }, @@ -11,6 +19,135 @@ "data": { "entities": "Miembros del grupo" } + }, + "fan": { + "data": { + "entities": "Miembros", + "hide_members": "Esconde miembros", + "title": "Agregar grupo" + }, + "description": "Selecciona las opciones del grupo", + "title": "Agregar grupo" + }, + "fan_options": { + "data": { + "entities": "Miembros del grupo" + } + }, + "init": { + "data": { + "group_type": "Tipo de grupo" + }, + "description": "Selecciona el tipo de grupo" + }, + "light": { + "data": { + "entities": "Miembros", + "hide_members": "Esconde miembros", + "title": "Agregar grupo" + }, + "title": "Agregar grupo" + }, + "light_options": { + "data": { + "entities": "Miembros del grupo" + }, + "description": "Selecciona las opciones del grupo" + }, + "lock": { + "data": { + "entities": "Miembros", + "hide_members": "Esconde miembros" + }, + "title": "Agregar grupo" + }, + "media_player": { + "data": { + "entities": "Miembros", + "hide_members": "Esconde miembros", + "name": "Nombre" + }, + "description": "Selecciona las opciones del grupo", + "title": "Agregar grupo" + }, + "media_player_options": { + "description": "Selecciona las opciones del grupo" + }, + "switch": { + "data": { + "entities": "Miembros" + } + }, + "user": { + "menu_options": { + "binary_sensor": "Grupo de sensores binarios", + "cover": "Grupo de cubiertas", + "fan": "Grupo de ventiladores", + "switch": "Grupo de conmutadores" + } + } + } + }, + "options": { + "step": { + "binary_sensor": { + "data": { + "all": "Todas las entidades", + "hide_members": "Esconde miembros" + } + }, + "binary_sensor_options": { + "data": { + "all": "Todas las entidades" + } + }, + "cover": { + "data": { + "hide_members": "Esconde miembros" + } + }, + "cover_options": { + "data": { + "entities": "Miembros" + } + }, + "fan": { + "data": { + "hide_members": "Esconde miembros" + } + }, + "fan_options": { + "data": { + "entities": "Miembros" + } + }, + "light": { + "data": { + "entities": "Miembros", + "hide_members": "Esconde miembros" + } + }, + "light_options": { + "data": { + "all": "Todas las entidades", + "entities": "Miembros" + } + }, + "lock": { + "data": { + "entities": "Miembros" + } + }, + "media_player": { + "data": { + "entities": "Miembros", + "hide_members": "Esconde miembros" + } + }, + "media_player_options": { + "data": { + "entities": "Miembros" + } } } }, @@ -19,7 +156,7 @@ "closed": "Cerrado", "home": "En casa", "locked": "Bloqueado", - "not_home": "Fuera de casa", + "not_home": "Fuera", "off": "Apagado", "ok": "OK", "on": "Encendido", diff --git a/homeassistant/components/group/translations/sk.json b/homeassistant/components/group/translations/sk.json index 151cc1c47b0..51759a9dc86 100644 --- a/homeassistant/components/group/translations/sk.json +++ b/homeassistant/components/group/translations/sk.json @@ -1,4 +1,22 @@ { + "config": { + "step": { + "media_player": { + "data": { + "name": "Meno" + } + } + } + }, + "options": { + "step": { + "light": { + "data": { + "entities": "\u010clenovia" + } + } + } + }, "state": { "_": { "closed": "Zatvoren\u00e1", diff --git a/homeassistant/components/guardian/translations/es.json b/homeassistant/components/guardian/translations/es.json index 981534cca9b..531d4ac5774 100644 --- a/homeassistant/components/guardian/translations/es.json +++ b/homeassistant/components/guardian/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Este dispositivo Guardian ya ha sido configurado", + "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "La configuraci\u00f3n del dispositivo Guardian ya est\u00e1 en proceso.", "cannot_connect": "No se pudo conectar" }, diff --git a/homeassistant/components/hangouts/translations/es.json b/homeassistant/components/hangouts/translations/es.json index 692df44c5bc..a2aba99c24c 100644 --- a/homeassistant/components/hangouts/translations/es.json +++ b/homeassistant/components/hangouts/translations/es.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Google Hangouts ya est\u00e1 configurado", + "already_configured": "El servicio ya est\u00e1 configurado", "unknown": "Error desconocido" }, "error": { "invalid_2fa": "Autenticaci\u00f3n de 2 factores no v\u00e1lida, por favor, int\u00e9ntelo de nuevo.", - "invalid_2fa_method": "M\u00e9todo 2FA no v\u00e1lido (verificar en el tel\u00e9fono).", + "invalid_2fa_method": "M\u00e9todo 2FA inv\u00e1lido (verificar en el tel\u00e9fono).", "invalid_login": "Inicio de sesi\u00f3n no v\u00e1lido, por favor, int\u00e9ntalo de nuevo." }, "step": { @@ -24,7 +24,7 @@ "password": "Contrase\u00f1a" }, "description": "Vac\u00edo", - "title": "Iniciar sesi\u00f3n en Google Hangouts" + "title": "Inicio de sesi\u00f3n de Google Chat" } } } diff --git a/homeassistant/components/harmony/translations/es.json b/homeassistant/components/harmony/translations/es.json index 39305d30680..527cd8433e8 100644 --- a/homeassistant/components/harmony/translations/es.json +++ b/homeassistant/components/harmony/translations/es.json @@ -4,10 +4,10 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo", + "cannot_connect": "Fall\u00f3 la conexi\u00f3n", "unknown": "Error inesperado" }, - "flow_title": "Logitech Harmony Hub {name}", + "flow_title": "{name}", "step": { "link": { "description": "\u00bfQuieres configurar {name} ({host})?", diff --git a/homeassistant/components/hisense_aehw4a1/translations/es.json b/homeassistant/components/hisense_aehw4a1/translations/es.json index 1a3a8ca221d..8eb81391ec4 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/es.json +++ b/homeassistant/components/hisense_aehw4a1/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "No se encontraron dispositivos Hisense AEH-W4A1 en la red.", - "single_instance_allowed": "Solo es posible una \u00fanica configuraci\u00f3n de Hisense AEH-W4A1." + "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": { diff --git a/homeassistant/components/hive/translations/es.json b/homeassistant/components/hive/translations/es.json index 727a33ec66e..09acc273536 100644 --- a/homeassistant/components/hive/translations/es.json +++ b/homeassistant/components/hive/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown_entry": "No se puede encontrar una entrada existente." }, diff --git a/homeassistant/components/home_connect/translations/es.json b/homeassistant/components/home_connect/translations/es.json index 10b49c96926..9ee5769583b 100644 --- a/homeassistant/components/home_connect/translations/es.json +++ b/homeassistant/components/home_connect/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "missing_configuration": "El componente Home Connect no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Mira su documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" }, "create_entry": { diff --git a/homeassistant/components/home_plus_control/translations/es.json b/homeassistant/components/home_plus_control/translations/es.json index 3c471ffc75e..82144e727ab 100644 --- a/homeassistant/components/home_plus_control/translations/es.json +++ b/homeassistant/components/home_plus_control/translations/es.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", - "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." diff --git a/homeassistant/components/homeassistant/translations/es.json b/homeassistant/components/homeassistant/translations/es.json index 0a9342afa69..6b00a1b3b51 100644 --- a/homeassistant/components/homeassistant/translations/es.json +++ b/homeassistant/components/homeassistant/translations/es.json @@ -6,7 +6,7 @@ "docker": "Docker", "hassio": "Supervisor", "installation_type": "Tipo de instalaci\u00f3n", - "os_name": "Nombre del Sistema Operativo", + "os_name": "Familia del sistema operativo", "os_version": "Versi\u00f3n del Sistema Operativo", "python_version": "Versi\u00f3n de Python", "timezone": "Zona horaria", diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index ef60610a8a1..c673a4e9749 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "port_name_in_use": "Ya est\u00e1 configurada una pasarela con el mismo nombre o puerto." + "port_name_in_use": "Ya existe un enlace o accesorio configurado con ese nombre o puerto." }, "step": { "pairing": { "description": "Para completar el emparejamiento, sigue las instrucciones en \"Notificaciones\" en \"Emparejamiento HomeKit\".", - "title": "Vincular pasarela Homekit" + "title": "Vinculaci\u00f3n HomeKit" }, "user": { "data": { @@ -19,6 +19,11 @@ }, "options": { "step": { + "accessory": { + "data": { + "entities": "Entidad" + } + }, "advanced": { "data": { "auto_start": "Arranque autom\u00e1tico (desactivado si se utiliza Z-Wave u otro sistema de arranque retardado)", @@ -36,16 +41,23 @@ "title": "Seleccione el c\u00f3dec de video de la c\u00e1mara." }, "exclude": { + "data": { + "entities": "Entidades" + }, + "title": "Selecciona las entidades a excluir" + }, + "include": { "data": { "entities": "Entidades" } }, "init": { "data": { - "mode": "Modo" + "domains": "Dominios a incluir", + "mode": "Mode de HomeKit" }, "description": "Las entidades de los \"Dominios que se van a incluir\" se establecer\u00e1n en HomeKit. Podr\u00e1 seleccionar qu\u00e9 entidades excluir de esta lista en la siguiente pantalla.", - "title": "Seleccione los dominios que desea establecer un puente." + "title": "Selecciona el modo y dominios." }, "yaml": { "description": "Esta entrada se controla a trav\u00e9s de YAML", diff --git a/homeassistant/components/homekit_controller/translations/es.json b/homeassistant/components/homekit_controller/translations/es.json index 52b295ecf21..a528d66ea06 100644 --- a/homeassistant/components/homekit_controller/translations/es.json +++ b/homeassistant/components/homekit_controller/translations/es.json @@ -18,7 +18,7 @@ "unable_to_pair": "No se ha podido emparejar, por favor int\u00e9ntelo de nuevo.", "unknown_error": "El dispositivo report\u00f3 un error desconocido. La vinculaci\u00f3n ha fallado." }, - "flow_title": "Accesorio HomeKit: {name}", + "flow_title": "{name}", "step": { "busy_error": { "description": "Interrumpe el emparejamiento en todos los controladores o intenta reiniciar el dispositivo y luego contin\u00faa con el emparejamiento.", @@ -69,5 +69,5 @@ "single_press": "\"{subtype}\" pulsado" } }, - "title": "Accesorio HomeKit" + "title": "Controlador HomeKit" } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/select.es.json b/homeassistant/components/homekit_controller/translations/select.es.json new file mode 100644 index 00000000000..0cbfbc71373 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.es.json @@ -0,0 +1,8 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "Afuera", + "sleep": "Durmiendo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sk.json b/homeassistant/components/homekit_controller/translations/sk.json index bee0999420f..8ebd0e1e08e 100644 --- a/homeassistant/components/homekit_controller/translations/sk.json +++ b/homeassistant/components/homekit_controller/translations/sk.json @@ -2,6 +2,11 @@ "config": { "abort": { "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" + }, + "step": { + "user": { + "title": "V\u00fdber zariadenia" + } } } } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/es.json b/homeassistant/components/homematicip_cloud/translations/es.json index 28a084a7ee9..454fa8f9f2a 100644 --- a/homeassistant/components/homematicip_cloud/translations/es.json +++ b/homeassistant/components/homematicip_cloud/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El punto de acceso ya est\u00e1 configurado", - "connection_aborted": "No se pudo conectar al servidor HMIP", + "connection_aborted": "Fall\u00f3 la conexi\u00f3n", "unknown": "Se ha producido un error desconocido." }, "error": { diff --git a/homeassistant/components/homewizard/translations/es.json b/homeassistant/components/homewizard/translations/es.json index 92faffd15c8..898d37fed09 100644 --- a/homeassistant/components/homewizard/translations/es.json +++ b/homeassistant/components/homewizard/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "api_not_enabled": "La API no est\u00e1 habilitada. Habilite la API en la aplicaci\u00f3n HomeWizard Energy en configuraci\u00f3n", "device_not_supported": "Este dispositivo no es compatible", - "invalid_discovery_parameters": "unsupported_api_version", + "invalid_discovery_parameters": "Versi\u00f3n de API no compatible detectada", "unknown_error": "Error inesperado" }, "step": { diff --git a/homeassistant/components/honeywell/translations/es.json b/homeassistant/components/honeywell/translations/es.json index bdae26b8794..f30e9606a4d 100644 --- a/homeassistant/components/honeywell/translations/es.json +++ b/homeassistant/components/honeywell/translations/es.json @@ -12,5 +12,14 @@ "description": "Por favor, introduzca las credenciales utilizadas para iniciar sesi\u00f3n en mytotalconnectcomfort.com." } } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "Temperatura fria, modo fuera" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/sk.json b/homeassistant/components/honeywell/translations/sk.json index 5ada995aa6e..1b1e671c054 100644 --- a/homeassistant/components/honeywell/translations/sk.json +++ b/homeassistant/components/honeywell/translations/sk.json @@ -2,6 +2,13 @@ "config": { "error": { "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "password": "Heslo" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/es.json b/homeassistant/components/huawei_lte/translations/es.json index 5d5e72e70c3..09a63bb1f62 100644 --- a/homeassistant/components/huawei_lte/translations/es.json +++ b/homeassistant/components/huawei_lte/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Este dispositivo ya ha sido configurado", - "already_in_progress": "Este dispositivo ya se est\u00e1 configurando", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "not_huawei_lte": "No es un dispositivo Huawei LTE" }, "error": { @@ -15,7 +15,7 @@ "response_error": "Error desconocido del dispositivo", "unknown": "Error inesperado" }, - "flow_title": "Huawei LTE: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/hue/translations/es.json b/homeassistant/components/hue/translations/es.json index e8611cbe690..7017cca99c1 100644 --- a/homeassistant/components/hue/translations/es.json +++ b/homeassistant/components/hue/translations/es.json @@ -3,11 +3,12 @@ "abort": { "all_configured": "Ya se han configurado todas las pasarelas Philips Hue", "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n para la pasarela ya est\u00e1 en marcha.", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "cannot_connect": "No se pudo conectar", "discover_timeout": "Imposible encontrar pasarelas Philips Hue", + "invalid_host": "Host inv\u00e1lido", "no_bridges": "No se han encontrado pasarelas Philips Hue.", - "not_hue_bridge": "No es una pasarela Hue", + "not_hue_bridge": "No es un enlace Hue", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/hue/translations/sk.json b/homeassistant/components/hue/translations/sk.json index 424ac0d9252..10605f27ce1 100644 --- a/homeassistant/components/hue/translations/sk.json +++ b/homeassistant/components/hue/translations/sk.json @@ -20,5 +20,10 @@ "description": "Pre registr\u00e1ciu Philips Hue s Home Assistant stla\u010dte tla\u010didlo na Philips Hue bridge.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)" } } + }, + "device_automation": { + "trigger_subtype": { + "1": "Prv\u00e9 tla\u010didlo" + } } } \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/es.json b/homeassistant/components/hyperion/translations/es.json index 5b4534069dd..496064b1543 100644 --- a/homeassistant/components/hyperion/translations/es.json +++ b/homeassistant/components/hyperion/translations/es.json @@ -23,7 +23,7 @@ "description": "Configurar autorizaci\u00f3n a tu servidor Hyperion Ambilight" }, "confirm": { - "description": "\u00bfQuieres a\u00f1adir este Hyperion Ambilight a Home Assistant?\n\n**Host:** {host}\n**Puerto:** {port}\n**Identificaci\u00f3n**: {id}", + "description": "\u00bfQuieres a\u00f1adir el siguiente Hyperion Ambilight en Home Assistant?\n\n**Host:** {host}\n**Puerto:** {port}\n**Identificaci\u00f3n**: {id}", "title": "Confirmar la adici\u00f3n del servicio Hyperion Ambilight" }, "create_token": { diff --git a/homeassistant/components/iaqualink/translations/es.json b/homeassistant/components/iaqualink/translations/es.json index 71dc95c82f5..c95f9d51927 100644 --- a/homeassistant/components/iaqualink/translations/es.json +++ b/homeassistant/components/iaqualink/translations/es.json @@ -13,8 +13,8 @@ "password": "Contrase\u00f1a", "username": "Usuario" }, - "description": "Por favor, introduzca el nombre de usuario y la contrase\u00f1a de su cuenta de iAqualink.", - "title": "Con\u00e9ctese a iAqualink" + "description": "Introduce el nombre de usuario y contrase\u00f1a de tu cuenta de iAqualink.", + "title": "Conexi\u00f3n con iAqualink" } } } diff --git a/homeassistant/components/icloud/translations/es.json b/homeassistant/components/icloud/translations/es.json index 593f2753d99..31db2283f66 100644 --- a/homeassistant/components/icloud/translations/es.json +++ b/homeassistant/components/icloud/translations/es.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "no_device": "Ninguno de tus dispositivos tiene activado \"Buscar mi iPhone\"", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "send_verification_code": "Error al enviar el c\u00f3digo de verificaci\u00f3n", - "validate_verification_code": "No se pudo verificar el c\u00f3digo de verificaci\u00f3n, elegir un dispositivo de confianza e iniciar la verificaci\u00f3n de nuevo" + "validate_verification_code": "No se ha podido verificar el c\u00f3digo de verificaci\u00f3n, vuelve a intentarlo" }, "step": { "reauth": { diff --git a/homeassistant/components/input_datetime/translations/es.json b/homeassistant/components/input_datetime/translations/es.json index 025943ee4fb..570c7b17592 100644 --- a/homeassistant/components/input_datetime/translations/es.json +++ b/homeassistant/components/input_datetime/translations/es.json @@ -1,3 +1,3 @@ { - "title": "Entrada de fecha" + "title": "Entrada de data i hora" } \ No newline at end of file diff --git a/homeassistant/components/input_number/translations/es.json b/homeassistant/components/input_number/translations/es.json index e05a9130580..e31e0ccc72f 100644 --- a/homeassistant/components/input_number/translations/es.json +++ b/homeassistant/components/input_number/translations/es.json @@ -1,3 +1,3 @@ { - "title": "Entrada de n\u00famero" + "title": "Entrada num\u00e9rica" } \ No newline at end of file diff --git a/homeassistant/components/input_number/translations/tr.json b/homeassistant/components/input_number/translations/tr.json index c1805180884..8d85abf5c15 100644 --- a/homeassistant/components/input_number/translations/tr.json +++ b/homeassistant/components/input_number/translations/tr.json @@ -1,3 +1,3 @@ { - "title": "Numara giriniz" + "title": "Numara giri\u015fi" } \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/es.json b/homeassistant/components/insteon/translations/es.json index 31088b3bf60..b7b72cefa28 100644 --- a/homeassistant/components/insteon/translations/es.json +++ b/homeassistant/components/insteon/translations/es.json @@ -1,14 +1,19 @@ { "config": { "abort": { - "cannot_connect": "No se puede conectar al m\u00f3dem Insteon", + "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "not_insteon_device": "El dispositivo descubierto no es un dispositivo Insteon", "single_instance_allowed": "Ya esta configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { "cannot_connect": "No se conect\u00f3 al m\u00f3dem Insteon, por favor, int\u00e9ntelo de nuevo.", "select_single": "Seleccione una opci\u00f3n." }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "\u00bfQuieres configurar {name}?" + }, "hubv1": { "data": { "host": "Direcci\u00f3n IP", diff --git a/homeassistant/components/insteon/translations/sk.json b/homeassistant/components/insteon/translations/sk.json index c563a509f07..b3711644c03 100644 --- a/homeassistant/components/insteon/translations/sk.json +++ b/homeassistant/components/insteon/translations/sk.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { "hubv1": { "data": { diff --git a/homeassistant/components/integration/translations/es.json b/homeassistant/components/integration/translations/es.json new file mode 100644 index 00000000000..8c903976d1d --- /dev/null +++ b/homeassistant/components/integration/translations/es.json @@ -0,0 +1,36 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "M\u00e9todo de integraci\u00f3n", + "round": "Precisi\u00f3n", + "source": "Sensor de entrada", + "unit_prefix": "Prefijo m\u00e9trico", + "unit_time": "Unidad de tiempo" + }, + "data_description": { + "round": "Controla el n\u00famero de d\u00edgitos decimales en la salida.", + "unit_prefix": "La salida se escalar\u00e1 seg\u00fan el prefijo m\u00e9trico seleccionado.", + "unit_time": "La salida se escalar\u00e1 seg\u00fan la unidad de tiempo seleccionada." + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "Precisi\u00f3n" + }, + "data_description": { + "round": "Controla el n\u00famero de d\u00edgitos decimales en la salida." + } + }, + "options": { + "description": "La precisi\u00f3n controla el n\u00famero de d\u00edgitos decimales en la salida." + } + } + }, + "title": "Integraci\u00f3n - Sensor integral de suma de Riemann" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/sk.json b/homeassistant/components/integration/translations/sk.json new file mode 100644 index 00000000000..aec16fc71b8 --- /dev/null +++ b/homeassistant/components/integration/translations/sk.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "round": "Presnos\u0165" + } + } + } + }, + "options": { + "step": { + "options": { + "data": { + "round": "Presnos\u0165" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/intellifire/translations/es.json b/homeassistant/components/intellifire/translations/es.json new file mode 100644 index 00000000000..e848376bdfb --- /dev/null +++ b/homeassistant/components/intellifire/translations/es.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "not_intellifire_device": "No es un dispositivo IntelliFire.", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "api_error": "Ha Fallado el inicio de sesi\u00f3n", + "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "iftapi_connect": "Se ha producido un error al conectar a iftapi.net" + }, + "flow_title": "{serial} ({host})", + "step": { + "api_config": { + "data": { + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico" + } + }, + "dhcp_confirm": { + "description": "\u00bfQuieres configurar {host} \nSerie: {serial}?" + }, + "manual_device_entry": { + "data": { + "host": "Host (direcci\u00f3n IP)" + } + }, + "pick_device": { + "data": { + "host": "Host" + }, + "description": "Se han descubierto los siguientes dispositivos IntelliFire. Selecciona lo que quieras configurar.", + "title": "Selecci\u00f3n de dispositivo" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/translations/es.json b/homeassistant/components/ios/translations/es.json index 3c4064280e7..8adb4041b20 100644 --- a/homeassistant/components/ios/translations/es.json +++ b/homeassistant/components/ios/translations/es.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Solo se necesita una \u00fanica configuraci\u00f3n de Home Assistant iOS." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "step": { "confirm": { - "description": "\u00bfQuieres configurar el componente iOS de Home Assistant?" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" } } } diff --git a/homeassistant/components/iss/translations/es.json b/homeassistant/components/iss/translations/es.json index 2cc46fefdb1..a0456431ad8 100644 --- a/homeassistant/components/iss/translations/es.json +++ b/homeassistant/components/iss/translations/es.json @@ -7,7 +7,8 @@ "user": { "data": { "show_on_map": "\u00bfMostrar en el mapa?" - } + }, + "description": "\u00bfQuieres configurar Estaci\u00f3n Espacial Internacional (ISS)?" } } }, diff --git a/homeassistant/components/isy994/translations/es.json b/homeassistant/components/isy994/translations/es.json index e5e15fc9c13..b9b2c1f1ed5 100644 --- a/homeassistant/components/isy994/translations/es.json +++ b/homeassistant/components/isy994/translations/es.json @@ -11,6 +11,14 @@ }, "flow_title": "{name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "description": "Las credenciales de {host} ya no son v\u00e1lidas.", + "title": "Re-autenticaci\u00f3n de ISY" + }, "user": { "data": { "host": "URL", @@ -19,7 +27,7 @@ "username": "Usuario" }, "description": "La entrada del host debe estar en formato URL completo, por ejemplo, http://192.168.10.100:80", - "title": "Conectar con tu ISY994" + "title": "Conexi\u00f3n con ISY" } } }, diff --git a/homeassistant/components/jellyfin/translations/sk.json b/homeassistant/components/jellyfin/translations/sk.json index 5ada995aa6e..1b1e671c054 100644 --- a/homeassistant/components/jellyfin/translations/sk.json +++ b/homeassistant/components/jellyfin/translations/sk.json @@ -2,6 +2,13 @@ "config": { "error": { "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "password": "Heslo" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/es.json b/homeassistant/components/juicenet/translations/es.json index 12b5159c99c..2fa2b61cd60 100644 --- a/homeassistant/components/juicenet/translations/es.json +++ b/homeassistant/components/juicenet/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada" + "already_configured": "La cuenta ya est\u00e1 configurada" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/kaleidescape/translations/es.json b/homeassistant/components/kaleidescape/translations/es.json new file mode 100644 index 00000000000..6586a20f202 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "unsupported": "Dispositiu no compatible" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "\u00bfQuieres configurar el reproductor {name} modelo {model}?" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/es.json b/homeassistant/components/knx/translations/es.json index b0256167cce..4e1c3130d64 100644 --- a/homeassistant/components/knx/translations/es.json +++ b/homeassistant/components/knx/translations/es.json @@ -5,28 +5,58 @@ "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { - "cannot_connect": "Error al conectar" + "cannot_connect": "Error al conectar", + "invalid_individual_address": "El valor no coincide con el patr\u00f3n de direcci\u00f3n KNX individual. 'area.line.device'", + "invalid_ip_address": "Direcci\u00f3n IPv4 inv\u00e1lida." }, "step": { "manual_tunnel": { "data": { "host": "Host", "individual_address": "Direcci\u00f3n individual para la conexi\u00f3n", - "local_ip": "IP local de Home Assistant (d\u00e9jela vac\u00eda para la detecci\u00f3n autom\u00e1tica)", + "local_ip": "IP local de Home Assistant", "port": "Puerto", "route_back": "Modo Route Back / NAT" }, + "data_description": { + "local_ip": "D\u00e9jalo en blanco para utilizar el descubrimiento autom\u00e1tico.", + "port": "Puerto del dispositivo de tunelizaci\u00f3n KNX/IP.Puerto del dispositivo de tunelizaci\u00f3n KNX/IP." + }, "description": "Introduzca la informaci\u00f3n de conexi\u00f3n de su dispositivo de tunelizaci\u00f3n." }, "routing": { "data": { - "individual_address": "Direcci\u00f3n individual para la conexi\u00f3n de enrutamiento", - "local_ip": "IP local de Home Assistant (d\u00e9jela vac\u00eda para la detecci\u00f3n autom\u00e1tica)", + "individual_address": "Direcci\u00f3n individual", + "local_ip": "IP local de Home Assistant", "multicast_group": "El grupo de multidifusi\u00f3n utilizado para el enrutamiento", "multicast_port": "El puerto de multidifusi\u00f3n utilizado para el enrutamiento" }, "description": "Por favor, configure las opciones de enrutamiento." }, + "secure_knxkeys": { + "data": { + "knxkeys_password": "Contrase\u00f1a para descifrar el archivo `.knxkeys`." + }, + "data_description": { + "knxkeys_password": "Se ha definido durante la exportaci\u00f3n del archivo desde ETS.Se ha definido durante la exportaci\u00f3n del archivo desde ETS." + }, + "description": "Introduce la informaci\u00f3n de tu archivo `.knxkeys`." + }, + "secure_manual": { + "data": { + "user_id": "ID de usuario" + }, + "data_description": { + "user_id": "A menudo, es el n\u00famero del t\u00fanel +1. Por tanto, 'T\u00fanel 2' tendr\u00eda el ID de usuario '3'." + }, + "description": "Introduce la informaci\u00f3n de seguridad IP (IP Secure)." + }, + "secure_tunneling": { + "description": "Selecciona c\u00f3mo quieres configurar KNX/IP Secure.", + "menu_options": { + "secure_manual": "Configura manualmente las claves de seguridad IP (IP Secure)" + } + }, "tunnel": { "data": { "gateway": "Conexi\u00f3n de t\u00fanel KNX" @@ -47,11 +77,15 @@ "data": { "connection_type": "Tipo de conexi\u00f3n KNX", "individual_address": "Direcci\u00f3n individual predeterminada", - "local_ip": "IP local del Asistente Hogar (utilice 0.0.0.0 para la detecci\u00f3n autom\u00e1tica)", - "multicast_group": "Grupo de multidifusi\u00f3n utilizado para enrutamiento y descubrimiento", - "multicast_port": "Puerto de multidifusi\u00f3n utilizado para enrutamiento y descubrimiento", - "rate_limit": "M\u00e1ximo de telegramas salientes por segundo", - "state_updater": "Habilitar globalmente la lectura de estados del Bus KNX" + "local_ip": "IP local de Home Assistant", + "multicast_group": "Grupo multidifusi\u00f3n", + "multicast_port": "Puerto multidifusi\u00f3n", + "rate_limit": "Frecuencia m\u00e1xima", + "state_updater": "Actualizador de estado" + }, + "data_description": { + "individual_address": "Direcci\u00f3n KNX para utilizar con Home Assistant, ej. `0.0.4`", + "rate_limit": "Telegramas de salida m\u00e1ximos por segundo. \nRecomendado: de 20 a 40" } }, "tunnel": { @@ -60,6 +94,10 @@ "local_ip": "IP local (d\u00e9jelo en blanco si no est\u00e1 seguro)", "port": "Puerto", "route_back": "Modo Route Back / NAT" + }, + "data_description": { + "host": "Direcci\u00f3n IP del dispositivo de tunelizaci\u00f3n KNX/IP.", + "port": "Puerto del dispositivo de tunelizaci\u00f3n KNX/IP." } } } diff --git a/homeassistant/components/knx/translations/sk.json b/homeassistant/components/knx/translations/sk.json index 6668aaa92fb..347dbdfbcef 100644 --- a/homeassistant/components/knx/translations/sk.json +++ b/homeassistant/components/knx/translations/sk.json @@ -8,11 +8,21 @@ "data": { "port": "Port" } + }, + "secure_manual": { + "data": { + "device_authentication": "Heslo na overenie zariadenia" + } } } }, "options": { "step": { + "init": { + "data": { + "local_ip": "Lok\u00e1lna IP adresa Home Assistant-a" + } + }, "tunnel": { "data": { "port": "Port" diff --git a/homeassistant/components/konnected/translations/es.json b/homeassistant/components/konnected/translations/es.json index 25c79d9f5c7..1074711901c 100644 --- a/homeassistant/components/konnected/translations/es.json +++ b/homeassistant/components/konnected/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ya est\u00e1 en marcha.", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "cannot_connect": "No se pudo conectar", "not_konn_panel": "No es un dispositivo Konnected.io reconocido", "unknown": "Se produjo un error desconocido" @@ -33,7 +33,7 @@ "not_konn_panel": "No es un dispositivo Konnected.io reconocido" }, "error": { - "bad_host": "URL del host de la API de invalidaci\u00f3n no v\u00e1lida" + "bad_host": "La URL de sustituci\u00f3n del host de la API es inv\u00e1lida" }, "step": { "options_binary": { @@ -84,7 +84,7 @@ }, "options_misc": { "data": { - "api_host": "Invalidar la direcci\u00f3n URL del host de la API (opcional)", + "api_host": "Sustituye la URL del host de la API (opcional)", "blink": "Parpadea el LED del panel cuando se env\u00eda un cambio de estado", "discovery": "Responde a las solicitudes de descubrimiento en tu red", "override_api_host": "Reemplazar la URL predeterminada del panel host de la API de Home Assistant" diff --git a/homeassistant/components/kraken/translations/sk.json b/homeassistant/components/kraken/translations/sk.json new file mode 100644 index 00000000000..ba5e13223a7 --- /dev/null +++ b/homeassistant/components/kraken/translations/sk.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Interval aktualiz\u00e1cie" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/es.json b/homeassistant/components/life360/translations/es.json index 019d2bc88d8..02a4a349ee9 100644 --- a/homeassistant/components/life360/translations/es.json +++ b/homeassistant/components/life360/translations/es.json @@ -8,7 +8,7 @@ "default": "Para configurar las opciones avanzadas, consulta la [documentaci\u00f3n de Life360]({docs_url})." }, "error": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "invalid_username": "Nombre de usuario no v\u00e1lido", "unknown": "Error inesperado" diff --git a/homeassistant/components/lifx/translations/es.json b/homeassistant/components/lifx/translations/es.json index c5b157a1499..484d59ba55f 100644 --- a/homeassistant/components/lifx/translations/es.json +++ b/homeassistant/components/lifx/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "No se encontraron dispositivos LIFX en la red.", - "single_instance_allowed": "S\u00f3lo es posible una \u00fanica configuraci\u00f3n de LIFX." + "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": { diff --git a/homeassistant/components/litterrobot/translations/sensor.ca.json b/homeassistant/components/litterrobot/translations/sensor.ca.json index 72a6906f03a..dd8731b78c5 100644 --- a/homeassistant/components/litterrobot/translations/sensor.ca.json +++ b/homeassistant/components/litterrobot/translations/sensor.ca.json @@ -1,9 +1,28 @@ { "state": { "litterrobot__status_code": { + "br": "Bossa extreta", + "ccc": "Cicle de neteja completat", + "ccp": "Cicle de neteja en curs", + "csf": "Error del sensor de gat", + "csi": "Sensor de gat interromput", + "cst": "Temps del sensor de gats", + "df1": "Dip\u00f2sit gaireb\u00e9 ple - Queden 2 cicles", + "df2": "Dip\u00f2sit gaireb\u00e9 ple - Queda 1 cicle", + "dfs": "Dip\u00f2sit ple", + "dhf": "Error de posici\u00f3 d'abocament + inici", + "dpf": "Error de posici\u00f3 d'abocament", + "ec": "Cicle de buidatge", + "hpf": "Error de posici\u00f3 d'inici", "off": "OFF", "offline": "Fora de l\u00ednia", - "p": "Pausat/ada" + "otf": "Error per sobre-parell", + "p": "Pausat/ada", + "pd": "Detecci\u00f3 de pessigada", + "rdy": "A punt", + "scf": "Error del sensor de gat a l'arrencada", + "sdf": "Dip\u00f2sit ple a l'arrencada", + "spf": "Detecci\u00f3 de pessigada a l'arrencada" } } } \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.es.json b/homeassistant/components/litterrobot/translations/sensor.es.json new file mode 100644 index 00000000000..1bf023d68bc --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.es.json @@ -0,0 +1,14 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "Bolsa extra\u00edda", + "ccc": "Ciclo de limpieza completado", + "ccp": "Ciclo de limpieza en curso", + "dhf": "Error de posici\u00f3n de vertido + inicio", + "ec": "Ciclo vac\u00edo", + "off": "Apagado", + "offline": "Desconectado", + "rdy": "Listo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.it.json b/homeassistant/components/litterrobot/translations/sensor.it.json new file mode 100644 index 00000000000..926cb5d68d7 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.it.json @@ -0,0 +1,28 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "Coperchio rimosso", + "ccc": "Ciclo di pulizia completato", + "ccp": "Ciclo di pulizia in corso", + "csf": "Errore del sensore Gatto", + "csi": "Sensore Gatto interrotto", + "cst": "Temporizzazione del sensore Gatto", + "df1": "Cassetto quasi pieno - 2 cicli rimasti", + "df2": "Cassetto quasi pieno - 1 ciclo rimasto", + "dfs": "Cassetto pieno", + "dhf": "Errore di scarico + posizione iniziale", + "dpf": "Errore di posizione di scarico", + "ec": "Ciclo vuoto", + "hpf": "Errore di posizione iniziale", + "off": "Spento", + "offline": "Non in linea", + "otf": "Errore di sovracoppia", + "p": "In pausa", + "pd": "Antipresa", + "rdy": "Pronto", + "scf": "Errore del sensore Gatto all'avvio", + "sdf": "Cassetto pieno all'avvio", + "spf": "Antipresa all'avvio" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.sk.json b/homeassistant/components/litterrobot/translations/sensor.sk.json new file mode 100644 index 00000000000..b4c5c43292d --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.sk.json @@ -0,0 +1,8 @@ +{ + "state": { + "litterrobot__status_code": { + "off": "Vypnut\u00fd", + "rdy": "Pripraven\u00fd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.tr.json b/homeassistant/components/litterrobot/translations/sensor.tr.json new file mode 100644 index 00000000000..2db5e574f7e --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.tr.json @@ -0,0 +1,28 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "Kapak \u00c7\u0131kar\u0131ld\u0131", + "ccc": "Temizleme Tamamland\u0131", + "ccp": "Temizleme Devam Ediyor", + "csf": "Kedi Sens\u00f6r\u00fc Hatas\u0131", + "csi": "Kedi Sens\u00f6r\u00fc Kesildi", + "cst": "Kedi Sens\u00f6r Zamanlamas\u0131", + "df1": "Hazne Neredeyse Dolu - 2 kez daha s\u00fcp\u00fcrebilir", + "df2": "Hazne Neredeyse Dolu - 1 kez daha s\u00fcp\u00fcrebilir", + "dfs": "Hazne Dolu", + "dhf": "Bo\u015faltma + Ana Konum Hatas\u0131", + "dpf": "Bo\u015faltma Konumu Hatas\u0131", + "ec": "Bo\u015f D\u00f6ng\u00fc", + "hpf": "Ev Konumu Hatas\u0131", + "off": "Kapal\u0131", + "offline": "\u00c7evrimd\u0131\u015f\u0131", + "otf": "A\u015f\u0131r\u0131 Tork Ar\u0131zas\u0131", + "p": "Durduruldu", + "pd": "S\u0131k\u0131\u015fma Alg\u0131lama", + "rdy": "Haz\u0131r", + "scf": "Ba\u015flang\u0131\u00e7ta Cat Sens\u00f6r\u00fc Hatas\u0131", + "sdf": "Ba\u015flang\u0131\u00e7ta Hazne Dolu", + "spf": "Ba\u015flang\u0131\u00e7ta S\u0131k\u0131\u015fma Alg\u0131lama" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/translations/es.json b/homeassistant/components/locative/translations/es.json index 9fa02248d42..e3c63d7b35d 100644 --- a/homeassistant/components/locative/translations/es.json +++ b/homeassistant/components/locative/translations/es.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "No conectado a Home Assistant Cloud.", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, diff --git a/homeassistant/components/logi_circle/translations/es.json b/homeassistant/components/logi_circle/translations/es.json index 6bd99289230..7fe62a28d05 100644 --- a/homeassistant/components/logi_circle/translations/es.json +++ b/homeassistant/components/logi_circle/translations/es.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "external_error": "Se produjo una excepci\u00f3n de otro flujo.", "external_setup": "Logi Circle se ha configurado correctamente a partir de otro flujo.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n." }, "error": { - "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", "follow_link": "Accede al enlace e identif\u00edcate antes de pulsar Enviar.", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, diff --git a/homeassistant/components/luftdaten/translations/es.json b/homeassistant/components/luftdaten/translations/es.json index bc33a13d870..99e5f7d5058 100644 --- a/homeassistant/components/luftdaten/translations/es.json +++ b/homeassistant/components/luftdaten/translations/es.json @@ -9,7 +9,7 @@ "user": { "data": { "show_on_map": "Mostrar en el mapa", - "station_id": "Sensro ID de Luftdaten" + "station_id": "ID del sensor" }, "title": "Definir Luftdaten" } diff --git a/homeassistant/components/lutron_caseta/translations/es.json b/homeassistant/components/lutron_caseta/translations/es.json index 098a90377d8..d13fded562e 100644 --- a/homeassistant/components/lutron_caseta/translations/es.json +++ b/homeassistant/components/lutron_caseta/translations/es.json @@ -8,10 +8,10 @@ "error": { "cannot_connect": "No se pudo conectar" }, - "flow_title": "Lutron Cas\u00e9ta {name} ({host})", + "flow_title": "{name} ({host})", "step": { "import_failed": { - "description": "No se puede configurar bridge (host: {host}) importado desde configuration.yaml.", + "description": "No se ha podido configurar el enlace (anfitri\u00f3n: {host}) importado de configuration.yaml.", "title": "Error al importar la configuraci\u00f3n del bridge Cas\u00e9ta." }, "link": { diff --git a/homeassistant/components/lyric/translations/es.json b/homeassistant/components/lyric/translations/es.json index 404a812e676..12692849ce3 100644 --- a/homeassistant/components/lyric/translations/es.json +++ b/homeassistant/components/lyric/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, diff --git a/homeassistant/components/mailgun/translations/es.json b/homeassistant/components/mailgun/translations/es.json index 50290059f54..1fcd54a5266 100644 --- a/homeassistant/components/mailgun/translations/es.json +++ b/homeassistant/components/mailgun/translations/es.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "No conectado a Home Assistant Cloud.", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, diff --git a/homeassistant/components/mazda/translations/es.json b/homeassistant/components/mazda/translations/es.json index f0ba0f4da49..01140eb3aad 100644 --- a/homeassistant/components/mazda/translations/es.json +++ b/homeassistant/components/mazda/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/meater/translations/es.json b/homeassistant/components/meater/translations/es.json new file mode 100644 index 00000000000..39d35b38d4a --- /dev/null +++ b/homeassistant/components/meater/translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "service_unavailable_error": "La API no est\u00e1 disponible actualmente, vuelva a intentarlo m\u00e1s tarde.", + "unknown_auth_error": "Error inesperado" + }, + "step": { + "reauth_confirm": { + "description": "Confirma la contrase\u00f1a de la cuenta de Meater Cloud {username}." + }, + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/sk.json b/homeassistant/components/meater/translations/sk.json new file mode 100644 index 00000000000..a52d3b46e7c --- /dev/null +++ b/homeassistant/components/meater/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "service_unavailable_error": "Rozhranie API je moment\u00e1lne nedostupn\u00e9, sk\u00faste to pros\u00edm nesk\u00f4r." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/translations/es.json b/homeassistant/components/media_player/translations/es.json index 0dfc063a035..fd60d09f562 100644 --- a/homeassistant/components/media_player/translations/es.json +++ b/homeassistant/components/media_player/translations/es.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_buffering": "{entity_name} se est\u00e1 cargando en memoria", "is_idle": "{entity_name} est\u00e1 inactivo", "is_off": "{entity_name} est\u00e1 apagado", "is_on": "{entity_name} est\u00e1 activado", @@ -8,6 +9,7 @@ "is_playing": "{entity_name} est\u00e1 reproduciendo" }, "trigger_type": { + "buffering": "{entity_name} comienza a cargarse en memoria", "changed_states": "{entity_name} ha cambiado de estado", "idle": "{entity_name} est\u00e1 inactivo", "paused": "{entity_name} est\u00e1 en pausa", @@ -18,12 +20,13 @@ }, "state": { "_": { + "buffering": "Cargando", "idle": "Inactivo", "off": "Apagado", "on": "Encendido", "paused": "En pausa", "playing": "Reproduciendo", - "standby": "Apagado" + "standby": "En espera" } }, "title": "Reproductor multimedia" diff --git a/homeassistant/components/melcloud/translations/es.json b/homeassistant/components/melcloud/translations/es.json index caba17be17a..be4f6cabe6c 100644 --- a/homeassistant/components/melcloud/translations/es.json +++ b/homeassistant/components/melcloud/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "Integraci\u00f3n mELCloud ya configurada para este correo electr\u00f3nico. Se ha actualizado el token de acceso." }, "error": { - "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntelo de nuevo.", + "cannot_connect": "Fall\u00f3 la conexi\u00f3n", "invalid_auth": "Autentificaci\u00f3n inv\u00e1lida", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/meteo_france/translations/es.json b/homeassistant/components/meteo_france/translations/es.json index 8843d301779..cd6d2d80812 100644 --- a/homeassistant/components/meteo_france/translations/es.json +++ b/homeassistant/components/meteo_france/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La ciudad ya est\u00e1 configurada", + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada", "unknown": "Error desconocido: por favor, vuelva a intentarlo m\u00e1s tarde" }, "error": { diff --git a/homeassistant/components/metoffice/translations/es.json b/homeassistant/components/metoffice/translations/es.json index 098e8fb0335..5751db1f760 100644 --- a/homeassistant/components/metoffice/translations/es.json +++ b/homeassistant/components/metoffice/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" + "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/mill/translations/es.json b/homeassistant/components/mill/translations/es.json index 4f2ddd29651..104c9d37d84 100644 --- a/homeassistant/components/mill/translations/es.json +++ b/homeassistant/components/mill/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada" + "already_configured": "La cuenta ya est\u00e1 configurada" }, "error": { "cannot_connect": "No se pudo conectar" diff --git a/homeassistant/components/min_max/translations/es.json b/homeassistant/components/min_max/translations/es.json new file mode 100644 index 00000000000..ffe5217b1f9 --- /dev/null +++ b/homeassistant/components/min_max/translations/es.json @@ -0,0 +1,39 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "Entidades de entrada", + "name": "Nombre", + "type": "Caracter\u00edstica estad\u00edstica" + }, + "data_description": { + "round_digits": "Controla el n\u00famero de d\u00edgitos decimales cuando la caracter\u00edstica estad\u00edstica es la media o mediana." + }, + "title": "Agregar sensor m\u00edn / m\u00e1x / media / mediana" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "Entidades de entrada", + "round_digits": "Precisi\u00f3n", + "type": "Caracter\u00edstica estad\u00edstica" + }, + "data_description": { + "round_digits": "Controla el n\u00famero de d\u00edgitos decimales cuando la caracter\u00edstica estad\u00edstica es la media o mediana." + } + }, + "options": { + "data": { + "entity_ids": "Entidades de entrada", + "round_digits": "Precisi\u00f3n", + "type": "Caracter\u00edstica estad\u00edstica" + } + } + } + }, + "title": "Sensor m\u00edn / m\u00e1x / media / mediana" +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/es.json b/homeassistant/components/moehlenhoff_alpha2/translations/es.json index 81e3d96b7bc..12821ae906d 100644 --- a/homeassistant/components/moehlenhoff_alpha2/translations/es.json +++ b/homeassistant/components/moehlenhoff_alpha2/translations/es.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Anfitri\u00f3n" + "host": "Host" } } } diff --git a/homeassistant/components/motion_blinds/translations/tr.json b/homeassistant/components/motion_blinds/translations/tr.json index 824569ce173..f53b251a8ec 100644 --- a/homeassistant/components/motion_blinds/translations/tr.json +++ b/homeassistant/components/motion_blinds/translations/tr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f , ba\u011flant\u0131 ayarlar\u0131 g\u00fcncellendi", + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "connection_error": "Ba\u011flanma hatas\u0131" }, diff --git a/homeassistant/components/myq/translations/es.json b/homeassistant/components/myq/translations/es.json index 08d2ccbee73..98d02b56280 100644 --- a/homeassistant/components/myq/translations/es.json +++ b/homeassistant/components/myq/translations/es.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/nam/translations/es.json b/homeassistant/components/nam/translations/es.json index 81e83c4376b..feeff2015f9 100644 --- a/homeassistant/components/nam/translations/es.json +++ b/homeassistant/components/nam/translations/es.json @@ -14,7 +14,7 @@ "flow_title": "{host}", "step": { "confirm_discovery": { - "description": "\u00bfQuieres configurar Nettigo Air Monitor en {host} ?" + "description": "\u00bfQuieres configurar Nettigo Air Monitor en {host}?" }, "credentials": { "data": { diff --git a/homeassistant/components/nanoleaf/translations/es.json b/homeassistant/components/nanoleaf/translations/es.json index 605997c6439..187517ed478 100644 --- a/homeassistant/components/nanoleaf/translations/es.json +++ b/homeassistant/components/nanoleaf/translations/es.json @@ -24,5 +24,10 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Desliza hacia abajo" + } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/sk.json b/homeassistant/components/nanoleaf/translations/sk.json index c2f015fe339..c34ad96714a 100644 --- a/homeassistant/components/nanoleaf/translations/sk.json +++ b/homeassistant/components/nanoleaf/translations/sk.json @@ -3,5 +3,10 @@ "abort": { "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" } + }, + "device_automation": { + "trigger_type": { + "swipe_right": "Potiahnite prstom doprava" + } } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/es.json b/homeassistant/components/neato/translations/es.json index 64c58279614..bcb9dc3619e 100644 --- a/homeassistant/components/neato/translations/es.json +++ b/homeassistant/components/neato/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index 2921b0272f8..ea65d2fc78a 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -18,7 +18,7 @@ "invalid_pin": "C\u00f3digo PIN no v\u00e1lido", "subscriber_error": "Error de abonado desconocido, ver registros", "timeout": "Tiempo de espera agotado validando el c\u00f3digo", - "unknown": "Error desconocido validando el c\u00f3digo", + "unknown": "Error inesperado", "wrong_project_id": "Por favor, introduzca un ID de proyecto Cloud v\u00e1lido (encontr\u00f3 el ID de proyecto de acceso al dispositivo)" }, "step": { @@ -33,7 +33,7 @@ "data": { "flow_impl": "Proveedor" }, - "description": "Elija a trav\u00e9s de qu\u00e9 proveedor de autenticaci\u00f3n desea autenticarse con Nest.", + "description": "Selecciona el m\u00e9todo de autenticaci\u00f3n", "title": "Proveedor de autenticaci\u00f3n" }, "link": { diff --git a/homeassistant/components/nest/translations/sk.json b/homeassistant/components/nest/translations/sk.json index 029432864bf..43b57c320d4 100644 --- a/homeassistant/components/nest/translations/sk.json +++ b/homeassistant/components/nest/translations/sk.json @@ -6,6 +6,9 @@ "create_entry": { "default": "\u00daspe\u0161ne overen\u00e9" }, + "error": { + "invalid_pin": "Nespr\u00e1vny PIN" + }, "step": { "auth": { "data": { diff --git a/homeassistant/components/netatmo/translations/es.json b/homeassistant/components/netatmo/translations/es.json index 15b67d4b32b..dd9fcf18d60 100644 --- a/homeassistant/components/netatmo/translations/es.json +++ b/homeassistant/components/netatmo/translations/es.json @@ -47,9 +47,9 @@ "public_weather": { "data": { "area_name": "Nombre del \u00e1rea", - "lat_ne": "Latitud esquina Noreste", - "lat_sw": "Latitud esquina Suroeste", - "lon_ne": "Longitud esquina Noreste", + "lat_ne": "Longitud esquina noreste", + "lat_sw": "Latitud esquina suroeste", + "lon_ne": "Longitud esquina noreste", "lon_sw": "Longitud esquina Suroeste", "mode": "C\u00e1lculo", "show_on_map": "Mostrar en el mapa" diff --git a/homeassistant/components/netatmo/translations/tr.json b/homeassistant/components/netatmo/translations/tr.json index d40de61e36a..807497e9d60 100644 --- a/homeassistant/components/netatmo/translations/tr.json +++ b/homeassistant/components/netatmo/translations/tr.json @@ -60,7 +60,7 @@ "public_weather_areas": { "data": { "new_area": "Alan ad\u0131", - "weather_areas": "Hava alanlar\u0131" + "weather_areas": "Hava b\u00f6lgeleri" }, "description": "Genel hava durumu sens\u00f6rlerini yap\u0131land\u0131r\u0131n.", "title": "Netatmo genel hava durumu sens\u00f6r\u00fc" diff --git a/homeassistant/components/netgear/translations/es.json b/homeassistant/components/netgear/translations/es.json index 7aadc51efb1..e1edb726d52 100644 --- a/homeassistant/components/netgear/translations/es.json +++ b/homeassistant/components/netgear/translations/es.json @@ -15,7 +15,7 @@ "ssl": "Utiliza un certificado SSL", "username": "Usuario (Opcional)" }, - "description": "Host predeterminado: {host}\nPuerto predeterminado: {port}\nNombre de usuario predeterminado: {username}", + "description": "Host predeterminado: {host} \nNombre de usuario predeterminado: {username}", "title": "Netgear" } } diff --git a/homeassistant/components/nexia/translations/es.json b/homeassistant/components/nexia/translations/es.json index 1698b8db1d1..4306a8795b8 100644 --- a/homeassistant/components/nexia/translations/es.json +++ b/homeassistant/components/nexia/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "Este nexia home ya est\u00e1 configurado" }, "error": { - "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", + "cannot_connect": "Fall\u00f3 la conexi\u00f3n", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/nmap_tracker/translations/es.json b/homeassistant/components/nmap_tracker/translations/es.json index 212b56a9606..e68d8ff59ab 100644 --- a/homeassistant/components/nmap_tracker/translations/es.json +++ b/homeassistant/components/nmap_tracker/translations/es.json @@ -11,7 +11,7 @@ "data": { "exclude": "Direcciones de red (separadas por comas) para excluir del escaneo", "home_interval": "N\u00famero m\u00ednimo de minutos entre los escaneos de los dispositivos activos (preservar la bater\u00eda)", - "hosts": "Direcciones de red (separadas por comas) para escanear", + "hosts": "Direcciones de red a escanear (separadas por comas)", "scan_options": "Opciones de escaneo configurables sin procesar para Nmap" }, "description": "Configure los hosts que ser\u00e1n escaneados por Nmap. Las direcciones de red y los excluidos pueden ser direcciones IP (192.168.1.1), redes IP (192.168.0.0/24) o rangos IP (192.168.1.0-32)." @@ -28,7 +28,7 @@ "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", + "hosts": "Direcciones de red a escanear (separadas por comas)", "interval_seconds": "Intervalo de exploraci\u00f3n", "scan_options": "Opciones de escaneo configurables sin procesar para Nmap", "track_new_devices": "Seguimiento de nuevos dispositivos" diff --git a/homeassistant/components/notion/translations/es.json b/homeassistant/components/notion/translations/es.json index b2012363f63..b537bd5bd3b 100644 --- a/homeassistant/components/notion/translations/es.json +++ b/homeassistant/components/notion/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/nzbget/translations/es.json b/homeassistant/components/nzbget/translations/es.json index cc89bdd2469..e3b1a595c90 100644 --- a/homeassistant/components/nzbget/translations/es.json +++ b/homeassistant/components/nzbget/translations/es.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "No se pudo conectar" }, - "flow_title": "NZBGet: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/oncue/translations/sk.json b/homeassistant/components/oncue/translations/sk.json index 5ada995aa6e..82a91905e7c 100644 --- a/homeassistant/components/oncue/translations/sk.json +++ b/homeassistant/components/oncue/translations/sk.json @@ -1,7 +1,8 @@ { "config": { "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" } } } \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/es.json b/homeassistant/components/ondilo_ico/translations/es.json index db8d744d176..1de8399aced 100644 --- a/homeassistant/components/ondilo_ico/translations/es.json +++ b/homeassistant/components/ondilo_ico/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n." }, "create_entry": { diff --git a/homeassistant/components/onewire/translations/es.json b/homeassistant/components/onewire/translations/es.json index 9879ccb0a00..cb13e1992bd 100644 --- a/homeassistant/components/onewire/translations/es.json +++ b/homeassistant/components/onewire/translations/es.json @@ -17,6 +17,7 @@ }, "user": { "data": { + "host": "Host", "port": "Puerto", "type": "Tipo de conexi\u00f3n" }, @@ -27,6 +28,24 @@ "options": { "error": { "device_not_selected": "Seleccionar los dispositivos a configurar" + }, + "step": { + "ack_no_options": { + "title": "Opciones SysBus de OneWire" + }, + "configure_device": { + "data": { + "precision": "Precisi\u00f3n del sensor" + }, + "description": "Selecciona la precisi\u00f3n del sensor {sensor_id}" + }, + "device_selection": { + "data": { + "clear_device_options": "Borra todas las configuraciones de dispositivo" + }, + "description": "Seleccione los pasos de configuraci\u00f3n a procesar", + "title": "Opciones de dispositivo OneWire" + } } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/tr.json b/homeassistant/components/onewire/translations/tr.json index 6fea23cfc76..4859b5ec865 100644 --- a/homeassistant/components/onewire/translations/tr.json +++ b/homeassistant/components/onewire/translations/tr.json @@ -17,9 +17,11 @@ }, "user": { "data": { + "host": "Sunucu", + "port": "Port", "type": "Ba\u011flant\u0131 t\u00fcr\u00fc" }, - "title": "1-Wire'\u0131 kurun" + "title": "Sunucu ayr\u0131nt\u0131lar\u0131n\u0131 ayarla" } } }, diff --git a/homeassistant/components/opentherm_gw/translations/es.json b/homeassistant/components/opentherm_gw/translations/es.json index d780548a8fa..8c773c8c1e2 100644 --- a/homeassistant/components/opentherm_gw/translations/es.json +++ b/homeassistant/components/opentherm_gw/translations/es.json @@ -1,7 +1,7 @@ { "config": { "error": { - "already_configured": "Gateway ya configurado", + "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", "id_exists": "El ID del Gateway ya existe" }, diff --git a/homeassistant/components/openuv/translations/es.json b/homeassistant/components/openuv/translations/es.json index 014eba04f52..bb5f36499ba 100644 --- a/homeassistant/components/openuv/translations/es.json +++ b/homeassistant/components/openuv/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Estas coordenadas ya est\u00e1n registradas." + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada" }, "error": { "invalid_api_key": "Clave API no v\u00e1lida" diff --git a/homeassistant/components/openweathermap/translations/es.json b/homeassistant/components/openweathermap/translations/es.json index 8a44668e8ac..5d276f60cb6 100644 --- a/homeassistant/components/openweathermap/translations/es.json +++ b/homeassistant/components/openweathermap/translations/es.json @@ -15,9 +15,9 @@ "latitude": "Latitud", "longitude": "Longitud", "mode": "Modo", - "name": "Nombre de la integraci\u00f3n" + "name": "Nombre" }, - "description": "Configurar la integraci\u00f3n de OpenWeatherMap. Para generar la clave API, ve a https://openweathermap.org/appid", + "description": "Para generar la clave API, ve a https://openweathermap.org/appid", "title": "OpenWeatherMap" } } diff --git a/homeassistant/components/overkiz/translations/es.json b/homeassistant/components/overkiz/translations/es.json index f1702a6d703..817864deba8 100644 --- a/homeassistant/components/overkiz/translations/es.json +++ b/homeassistant/components/overkiz/translations/es.json @@ -1,12 +1,15 @@ { "config": { "abort": { - "already_configured": "La cuenta ya est\u00e1 configurada" + "already_configured": "La cuenta ya est\u00e1 configurada", + "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente", + "reauth_wrong_account": "S\u00f3lo puedes volver a autenticar esta entrada con la misma cuenta y hub de Overkiz" }, "error": { "cannot_connect": "Error al conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "server_in_maintenance": "El servidor est\u00e1 inactivo por mantenimiento", + "too_many_attempts": "Demasiados intentos con un 'token' inv\u00e1lido, bloqueado temporalmente", "too_many_requests": "Demasiadas solicitudes, int\u00e9ntalo de nuevo m\u00e1s tarde.", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/overkiz/translations/select.sk.json b/homeassistant/components/overkiz/translations/select.sk.json new file mode 100644 index 00000000000..460b1d67415 --- /dev/null +++ b/homeassistant/components/overkiz/translations/select.sk.json @@ -0,0 +1,7 @@ +{ + "state": { + "overkiz__open_closed_pedestrian": { + "pedestrian": "Chodec" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.sk.json b/homeassistant/components/overkiz/translations/sensor.sk.json new file mode 100644 index 00000000000..cfa849319eb --- /dev/null +++ b/homeassistant/components/overkiz/translations/sensor.sk.json @@ -0,0 +1,10 @@ +{ + "state": { + "overkiz__discrete_rssi_level": { + "good": "Dobr\u00e1" + }, + "overkiz__priority_lock_originator": { + "wind": "Vietor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sk.json b/homeassistant/components/overkiz/translations/sk.json index 71a7aea5018..93eb8dcc1b5 100644 --- a/homeassistant/components/overkiz/translations/sk.json +++ b/homeassistant/components/overkiz/translations/sk.json @@ -5,6 +5,13 @@ }, "error": { "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "password": "Heslo" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/es.json b/homeassistant/components/ovo_energy/translations/es.json index aa9708c60d0..549f3af16ca 100644 --- a/homeassistant/components/ovo_energy/translations/es.json +++ b/homeassistant/components/ovo_energy/translations/es.json @@ -5,7 +5,7 @@ "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, - "flow_title": "OVO Energy: {username}", + "flow_title": "{username}", "step": { "reauth": { "data": { @@ -20,7 +20,7 @@ "username": "Usuario" }, "description": "Configurar una instancia de OVO Energy para acceder a su consumo de energ\u00eda.", - "title": "A\u00f1adir OVO Energy" + "title": "A\u00f1adir cuenta de OVO Energy" } } } diff --git a/homeassistant/components/owntracks/translations/es.json b/homeassistant/components/owntracks/translations/es.json index 1832e701559..dc420a806f4 100644 --- a/homeassistant/components/owntracks/translations/es.json +++ b/homeassistant/components/owntracks/translations/es.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "No conectado a Home Assistant Cloud.", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "create_entry": { diff --git a/homeassistant/components/peco/translations/es.json b/homeassistant/components/peco/translations/es.json new file mode 100644 index 00000000000..ecfb586561c --- /dev/null +++ b/homeassistant/components/peco/translations/es.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "county": "Condado" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/person/translations/es.json b/homeassistant/components/person/translations/es.json index 98fca470569..6576d02bb5e 100644 --- a/homeassistant/components/person/translations/es.json +++ b/homeassistant/components/person/translations/es.json @@ -2,7 +2,7 @@ "state": { "_": { "home": "En casa", - "not_home": "Fuera de casa" + "not_home": "Fuera" } }, "title": "Persona" diff --git a/homeassistant/components/plaato/translations/es.json b/homeassistant/components/plaato/translations/es.json index d38bf2a8265..693c6bf99c7 100644 --- a/homeassistant/components/plaato/translations/es.json +++ b/homeassistant/components/plaato/translations/es.json @@ -1,12 +1,13 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", + "cloud_not_connected": "No conectado a Home Assistant Cloud.", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, "create_entry": { - "default": "\u00a1Tu Plaato {device_type} con nombre **{device_name}** se configur\u00f3 correctamente!" + "default": "\u00a1El dispositivo Plaato {device_type} con nombre **{device_name}** se ha configurado correctamente!" }, "error": { "invalid_webhook_device": "Has seleccionado un dispositivo que no admite el env\u00edo de datos a un webhook. Solo est\u00e1 disponible para Airlock", @@ -27,8 +28,8 @@ "device_name": "Nombre de su dispositivo", "device_type": "Tipo de dispositivo Plaato" }, - "description": "\u00bfEst\u00e1s seguro de que quieres configurar el Airlock de Plaato?", - "title": "Configurar el webhook de Plaato" + "description": "\u00bfQuieres empezar la configuraci\u00f3n?", + "title": "Configura dispositivos Plaato" }, "webhook": { "description": "Para enviar eventos a Home Assistant, deber\u00e1 configurar la funci\u00f3n de webhook en Plaato Airlock. \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\n Consulte [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s detalles.", diff --git a/homeassistant/components/plex/translations/es.json b/homeassistant/components/plex/translations/es.json index c31dcf26e07..38f57b91c7b 100644 --- a/homeassistant/components/plex/translations/es.json +++ b/homeassistant/components/plex/translations/es.json @@ -3,10 +3,10 @@ "abort": { "all_configured": "Todos los servidores vinculados ya configurados", "already_configured": "Este servidor Plex ya est\u00e1 configurado", - "already_in_progress": "Plex se est\u00e1 configurando", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "token_request_timeout": "Tiempo de espera agotado para la obtenci\u00f3n del token", - "unknown": "Fall\u00f3 por razones desconocidas" + "unknown": "Error inesperado" }, "error": { "faulty_credentials": "La autorizaci\u00f3n ha fallado, verifica el token", diff --git a/homeassistant/components/plugwise/translations/es.json b/homeassistant/components/plugwise/translations/es.json index 8f3ce70cb88..e1d1bf3359b 100644 --- a/homeassistant/components/plugwise/translations/es.json +++ b/homeassistant/components/plugwise/translations/es.json @@ -4,17 +4,17 @@ "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { - "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntelo de nuevo.", + "cannot_connect": "Fall\u00f3 la conexi\u00f3n", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida, comprueba los 8 caracteres de tu Smile ID", "unknown": "Error inesperado" }, - "flow_title": "Smile: {name}", + "flow_title": "{name}", "step": { "user": { "data": { "flow_type": "Tipo de conexi\u00f3n" }, - "description": "Detalles", + "description": "Producto:", "title": "Conectarse a Smile" }, "user_gateway": { diff --git a/homeassistant/components/plum_lightpad/translations/es.json b/homeassistant/components/plum_lightpad/translations/es.json index b5f3c8b1439..c22321bc0b7 100644 --- a/homeassistant/components/plum_lightpad/translations/es.json +++ b/homeassistant/components/plum_lightpad/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada" + "already_configured": "La cuenta ya est\u00e1 configurada" }, "error": { "cannot_connect": "No se pudo conectar" diff --git a/homeassistant/components/point/translations/es.json b/homeassistant/components/point/translations/es.json index c495a4fe3bd..3d1f47c2ceb 100644 --- a/homeassistant/components/point/translations/es.json +++ b/homeassistant/components/point/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", - "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", "external_setup": "Point se ha configurado correctamente a partir de otro flujo.", "no_flows": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." diff --git a/homeassistant/components/poolsense/translations/es.json b/homeassistant/components/poolsense/translations/es.json index e2e5272f7ac..2d38dd670d3 100644 --- a/homeassistant/components/poolsense/translations/es.json +++ b/homeassistant/components/poolsense/translations/es.json @@ -12,7 +12,7 @@ "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a" }, - "description": "[%key:common::config_flow::description%]", + "description": "\u00bfQuieres empezar la configuraci\u00f3n?", "title": "PoolSense" } } diff --git a/homeassistant/components/powerwall/translations/es.json b/homeassistant/components/powerwall/translations/es.json index 5d82ecda8e5..c0f73c2ac99 100644 --- a/homeassistant/components/powerwall/translations/es.json +++ b/homeassistant/components/powerwall/translations/es.json @@ -12,6 +12,9 @@ }, "flow_title": "Powerwall de Tesla ({ip_address})", "step": { + "confirm_discovery": { + "description": "\u00bfQuieres configurar {name} ({ip_address})?" + }, "reauth_confim": { "data": { "password": "Contrase\u00f1a" diff --git a/homeassistant/components/ps4/translations/es.json b/homeassistant/components/ps4/translations/es.json index f65a27c61b6..7a807953b5e 100644 --- a/homeassistant/components/ps4/translations/es.json +++ b/homeassistant/components/ps4/translations/es.json @@ -20,7 +20,7 @@ }, "link": { "data": { - "code": "PIN", + "code": "C\u00f3digo PIN", "ip_address": "Direcci\u00f3n IP", "name": "Nombre", "region": "Regi\u00f3n" diff --git a/homeassistant/components/pure_energie/translations/es.json b/homeassistant/components/pure_energie/translations/es.json index eb5d98c7ebf..9725448be66 100644 --- a/homeassistant/components/pure_energie/translations/es.json +++ b/homeassistant/components/pure_energie/translations/es.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Anfitri\u00f3n" + "host": "Host" } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/es.json b/homeassistant/components/pvpc_hourly_pricing/translations/es.json index 4af0de8f594..ec5634a160e 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/es.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/es.json @@ -9,10 +9,10 @@ "name": "Nombre del sensor", "power": "Potencia contratada (kW)", "power_p3": "Potencia contratada para el per\u00edodo valle P3 (kW)", - "tariff": "Tarifa contratada (1, 2 o 3 per\u00edodos)" + "tariff": "Tarifa aplicable por zona geogr\u00e1fica" }, "description": "Este sensor utiliza la API oficial para obtener [el precio horario de la electricidad (PVPC)](https://www.esios.ree.es/es/pvpc) en Espa\u00f1a.\nPara obtener una explicaci\u00f3n m\u00e1s precisa, visita los [documentos de la integraci\u00f3n](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSelecciona la tarifa contratada en funci\u00f3n del n\u00famero de per\u00edodos de facturaci\u00f3n por d\u00eda:\n- 1 per\u00edodo: normal\n- 2 per\u00edodos: discriminaci\u00f3n (tarifa nocturna)\n- 3 per\u00edodos: coche el\u00e9ctrico (tarifa nocturna de 3 per\u00edodos)", - "title": "Selecci\u00f3n de tarifa" + "title": "Configuraci\u00f3n del sensor" } } }, diff --git a/homeassistant/components/qnap_qsw/translations/es.json b/homeassistant/components/qnap_qsw/translations/es.json new file mode 100644 index 00000000000..b58fcb71fc7 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "invalid_id": "El dispositivo ha devuelto un ID \u00fanico inv\u00e1lido" + }, + "error": { + "cannot_connect": "Ha fallado la conexi\u00f3n", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "url": "URL", + "username": "Nombre de usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/translations/es.json b/homeassistant/components/rachio/translations/es.json index 7e4a03c138a..f3821b7aa5c 100644 --- a/homeassistant/components/rachio/translations/es.json +++ b/homeassistant/components/rachio/translations/es.json @@ -4,8 +4,8 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "unknown": "Error inesperado" }, "step": { @@ -22,7 +22,7 @@ "step": { "init": { "data": { - "manual_run_mins": "Durante cu\u00e1nto tiempo, en minutos, permanece encendida una estaci\u00f3n cuando el interruptor est\u00e1 activado." + "manual_run_mins": "Duraci\u00f3n en minutos a ejecutar cuando se active un interruptor de zona" } } } diff --git a/homeassistant/components/radio_browser/translations/es.json b/homeassistant/components/radio_browser/translations/es.json index 6bb377d2d28..aa8e6336550 100644 --- a/homeassistant/components/radio_browser/translations/es.json +++ b/homeassistant/components/radio_browser/translations/es.json @@ -2,6 +2,11 @@ "config": { "abort": { "single_instance_allowed": "Ya est\u00e1 configurado. Solamente una configuraci\u00f3n es posible." + }, + "step": { + "user": { + "description": "\u00bfQuieres a\u00f1adir el navegador radio a Home Assistant?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/es.json b/homeassistant/components/rainmachine/translations/es.json index 317339ed39f..3e13d925b34 100644 --- a/homeassistant/components/rainmachine/translations/es.json +++ b/homeassistant/components/rainmachine/translations/es.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "ip_address": "Nombre de host o direcci\u00f3n IP", + "ip_address": "Nombre del host o direcci\u00f3n IP", "password": "Contrase\u00f1a", "port": "Puerto" }, diff --git a/homeassistant/components/recorder/translations/es.json b/homeassistant/components/recorder/translations/es.json new file mode 100644 index 00000000000..81bcf29d548 --- /dev/null +++ b/homeassistant/components/recorder/translations/es.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "current_recorder_run": "Hora de inicio de la ejecuci\u00f3n actual", + "estimated_db_size": "Mida estimada de la base de datos (MiB)" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/tr.json b/homeassistant/components/recorder/translations/tr.json index 80fd4fd45be..1fdc0e408b3 100644 --- a/homeassistant/components/recorder/translations/tr.json +++ b/homeassistant/components/recorder/translations/tr.json @@ -2,6 +2,7 @@ "system_health": { "info": { "current_recorder_run": "Mevcut \u00c7al\u0131\u015ft\u0131rma Ba\u015flang\u0131\u00e7 Zaman\u0131", + "estimated_db_size": "Tahmini Veritaban\u0131 Boyutu (MB)", "oldest_recorder_run": "En Eski \u00c7al\u0131\u015ft\u0131rma Ba\u015flang\u0131\u00e7 Zaman\u0131" } } diff --git a/homeassistant/components/renault/translations/es.json b/homeassistant/components/renault/translations/es.json index cf0f88983e0..1d8b5f447e3 100644 --- a/homeassistant/components/renault/translations/es.json +++ b/homeassistant/components/renault/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "kamereon_no_account": "No se pudo encontrar la cuenta de Kamereon.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, diff --git a/homeassistant/components/rfxtrx/translations/es.json b/homeassistant/components/rfxtrx/translations/es.json index fa45fe8a777..ec7777ee0d4 100644 --- a/homeassistant/components/rfxtrx/translations/es.json +++ b/homeassistant/components/rfxtrx/translations/es.json @@ -61,6 +61,7 @@ "debug": "Activar la depuraci\u00f3n", "device": "Seleccionar dispositivo para configurar", "event_code": "Introducir el c\u00f3digo de evento para a\u00f1adir", + "protocols": "Protocolos", "remove_device": "Selecciona el dispositivo a eliminar" }, "title": "Opciones de Rfxtrx" diff --git a/homeassistant/components/risco/translations/es.json b/homeassistant/components/risco/translations/es.json index 68e7031c102..6f85fe0c4ab 100644 --- a/homeassistant/components/risco/translations/es.json +++ b/homeassistant/components/risco/translations/es.json @@ -33,7 +33,7 @@ "init": { "data": { "code_arm_required": "Requiere un c\u00f3digo PIN para armar", - "code_disarm_required": "Requiere un c\u00f3digo PIN para desarmar", + "code_disarm_required": "Requiere un c\u00f3digo PIN para desactivar", "scan_interval": "Con qu\u00e9 frecuencia sondear Risco (en segundos)" }, "title": "Configurar opciones" diff --git a/homeassistant/components/risco/translations/sk.json b/homeassistant/components/risco/translations/sk.json index 5ada995aa6e..3f464b4046d 100644 --- a/homeassistant/components/risco/translations/sk.json +++ b/homeassistant/components/risco/translations/sk.json @@ -3,5 +3,14 @@ "error": { "invalid_auth": "Neplatn\u00e9 overenie" } + }, + "options": { + "step": { + "risco_to_ha": { + "data": { + "A": "Skupina A" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/roku/translations/es.json b/homeassistant/components/roku/translations/es.json index 189a4aec179..202c7f6c8ff 100644 --- a/homeassistant/components/roku/translations/es.json +++ b/homeassistant/components/roku/translations/es.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Fallo al conectar" }, - "flow_title": "Roku: {name}", + "flow_title": "{name}", "step": { "discovery_confirm": { "description": "\u00bfQuieres configurar {name} ?", diff --git a/homeassistant/components/roomba/translations/es.json b/homeassistant/components/roomba/translations/es.json index 315c8bda096..c8760f75fdd 100644 --- a/homeassistant/components/roomba/translations/es.json +++ b/homeassistant/components/roomba/translations/es.json @@ -16,7 +16,7 @@ "host": "Host" }, "description": "Selecciona una Roomba o Braava.", - "title": "Conectar autom\u00e1ticamente con el dispositivo" + "title": "Conexi\u00f3n autom\u00e1tica con el dispositivo" }, "link": { "description": "Mant\u00e9n pulsado el bot\u00f3n Inicio en {name} hasta que el dispositivo genere un sonido (aproximadamente dos segundos).", @@ -46,7 +46,7 @@ "password": "Contrase\u00f1a" }, "description": "Actualmente recuperar el BLID y la contrase\u00f1a es un proceso manual. Sigue los pasos descritos en la documentaci\u00f3n en: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", - "title": "Conectarse al dispositivo" + "title": "Conexi\u00f3n autom\u00e1tica con el dispositivo" } } }, diff --git a/homeassistant/components/roon/translations/es.json b/homeassistant/components/roon/translations/es.json index e38b7691f40..45038d8e7dd 100644 --- a/homeassistant/components/roon/translations/es.json +++ b/homeassistant/components/roon/translations/es.json @@ -8,6 +8,13 @@ "unknown": "Error inesperado" }, "step": { + "fallback": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "description": "No se ha podrido descubrir el servidor Roon, introduce el anfitri\u00f3n y el puerto." + }, "link": { "description": "Debes autorizar Home Assistant en Roon. Despu\u00e9s de pulsar en Enviar, ve a la aplicaci\u00f3n Roon Core, abre Configuraci\u00f3n y activa HomeAssistant en la pesta\u00f1a Extensiones.", "title": "Autorizar HomeAssistant en Roon" @@ -16,7 +23,7 @@ "data": { "host": "Host" }, - "description": "Introduce el nombre de Host o IP del servidor Roon." + "description": "No se ha podido descubrir el servidor Roon, introduce el nombre de host o la IP." } } } diff --git a/homeassistant/components/ruckus_unleashed/translations/sk.json b/homeassistant/components/ruckus_unleashed/translations/sk.json index 5ada995aa6e..dbe0480911b 100644 --- a/homeassistant/components/ruckus_unleashed/translations/sk.json +++ b/homeassistant/components/ruckus_unleashed/translations/sk.json @@ -2,6 +2,13 @@ "config": { "error": { "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "username": "U\u017e\u00edvate\u013esk\u00e9 meno" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/es.json b/homeassistant/components/sabnzbd/translations/es.json new file mode 100644 index 00000000000..f38e28a3287 --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallado la conexi\u00f3n", + "invalid_api_key": "Clave API inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "name": "Nombre", + "path": "Ruta", + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/sk.json b/homeassistant/components/sabnzbd/translations/sk.json new file mode 100644 index 00000000000..9d5ee388dc3 --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json index 42b0f794e7a..922ee0da4fe 100644 --- a/homeassistant/components/samsungtv/translations/es.json +++ b/homeassistant/components/samsungtv/translations/es.json @@ -12,7 +12,8 @@ "unknown": "Error inesperado" }, "error": { - "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este televisor Samsung. Revisa la configuraci\u00f3n de tu televisor para autorizar a Home Assistant." + "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este televisor Samsung. Revisa la configuraci\u00f3n de tu televisor para autorizar a Home Assistant.", + "invalid_pin": "El PIN es inv\u00e1lido; vuelve a intentarlo." }, "flow_title": "{device}", "step": { @@ -20,9 +21,15 @@ "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" }, + "encrypted_pairing": { + "description": "Introduce el PIN que se muestra en {device}." + }, "reauth_confirm": { "description": "Despu\u00e9s de enviarlo, acepte la ventana emergente en {device} solicitando autorizaci\u00f3n dentro de los 30 segundos." }, + "reauth_confirm_encrypted": { + "description": "Introduce el PIN que se muestra en {device}." + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/season/translations/sensor.sk.json b/homeassistant/components/season/translations/sensor.sk.json new file mode 100644 index 00000000000..6a1f5dd293b --- /dev/null +++ b/homeassistant/components/season/translations/sensor.sk.json @@ -0,0 +1,7 @@ +{ + "state": { + "season__season__": { + "spring": "Jar" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/es.json b/homeassistant/components/sense/translations/es.json index ca77c97900b..5621988c60b 100644 --- a/homeassistant/components/sense/translations/es.json +++ b/homeassistant/components/sense/translations/es.json @@ -12,7 +12,8 @@ "reauth_validate": { "data": { "password": "Contrase\u00f1a" - } + }, + "description": "La integraci\u00f3n Sense debe volver a autenticar la cuenta {email}." }, "user": { "data": { diff --git a/homeassistant/components/senseme/translations/sk.json b/homeassistant/components/senseme/translations/sk.json new file mode 100644 index 00000000000..ffc3e13321e --- /dev/null +++ b/homeassistant/components/senseme/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/es.json b/homeassistant/components/sensibo/translations/es.json index f4ff854c502..c5033bce962 100644 --- a/homeassistant/components/sensibo/translations/es.json +++ b/homeassistant/components/sensibo/translations/es.json @@ -4,9 +4,18 @@ "already_configured": "La cuenta ya est\u00e1 configurada" }, "error": { - "cannot_connect": "No se pudo conectar" + "cannot_connect": "No se pudo conectar", + "incorrect_api_key": "Clave API inv\u00e1lida para la cuenta seleccionada", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "no_devices": "No se ha descubierto ning\u00fan dispositivo", + "no_username": "No se pudo obtener el nombre de usuario" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Clave API" + } + }, "user": { "data": { "api_key": "Clave API", diff --git a/homeassistant/components/sensibo/translations/sk.json b/homeassistant/components/sensibo/translations/sk.json index 694f006218b..c3bf532e7a3 100644 --- a/homeassistant/components/sensibo/translations/sk.json +++ b/homeassistant/components/sensibo/translations/sk.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "incorrect_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d pre vybran\u00fd \u00fa\u010det" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/sensor/translations/sk.json b/homeassistant/components/sensor/translations/sk.json index 8f3507f56c1..f86600559a4 100644 --- a/homeassistant/components/sensor/translations/sk.json +++ b/homeassistant/components/sensor/translations/sk.json @@ -1,4 +1,13 @@ { + "device_automation": { + "condition_type": { + "is_voltage": "S\u00fa\u010dasn\u00e9 nap\u00e4tie {entity_name}" + }, + "trigger_type": { + "current": "{entity_name} pr\u00fad sa zmen\u00ed", + "voltage": "{entity_name} zmen\u00ed nap\u00e4tie" + } + }, "state": { "_": { "off": "Neakt\u00edvny", diff --git a/homeassistant/components/senz/translations/es.json b/homeassistant/components/senz/translations/es.json new file mode 100644 index 00000000000..f81af28f4d9 --- /dev/null +++ b/homeassistant/components/senz/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "no_url_available": "No hay ninguna URL disponible. Para m\u00e1s informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "oauth_error": "Se han recibido datos token inv\u00e1lidos." + }, + "create_entry": { + "default": "Autenticaci\u00f3n exitosa" + }, + "step": { + "pick_implementation": { + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index 6f5c86417d4..25cbb8eb30b 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/translations/es.json @@ -42,7 +42,7 @@ "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_push": "{subtype} pulsado durante un rato", "long_single": "Pulsaci\u00f3n larga de {subtype} seguida de una pulsaci\u00f3n simple", "single": "Pulsaci\u00f3n simple de {subtype}", "single_long": "Pulsaci\u00f3n simple de {subtype} seguida de una pulsaci\u00f3n larga", diff --git a/homeassistant/components/sia/translations/sk.json b/homeassistant/components/sia/translations/sk.json index 892b8b2cd91..d703643c7de 100644 --- a/homeassistant/components/sia/translations/sk.json +++ b/homeassistant/components/sia/translations/sk.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "port": "Port" + "port": "Port", + "protocol": "Protokol" } } } diff --git a/homeassistant/components/simplisafe/translations/es.json b/homeassistant/components/simplisafe/translations/es.json index d4b066890f1..632fc50e4c0 100644 --- a/homeassistant/components/simplisafe/translations/es.json +++ b/homeassistant/components/simplisafe/translations/es.json @@ -6,11 +6,15 @@ "wrong_account": "Las credenciales de usuario proporcionadas no coinciden con esta cuenta de SimpliSafe." }, "error": { + "2fa_timed_out": "Se ha agotado el tiempo de espera m\u00e1ximo durante la autenticaci\u00f3n de dos factores", "identifier_exists": "Cuenta ya registrada", "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "still_awaiting_mfa": "Esperando todav\u00eda el clic en el correo electr\u00f3nico de MFA", "unknown": "Error inesperado" }, + "progress": { + "email_2fa": "Mira el correo electr\u00f3nico donde deber\u00edas encontrar el enlace de verificaci\u00f3n de Simplisafe." + }, "step": { "mfa": { "title": "Autenticaci\u00f3n Multi-Factor SimpliSafe" @@ -19,8 +23,14 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Tu token de acceso ha expirado o ha sido revocado. Introduce tu contrase\u00f1a para volver a vincular tu cuenta.", - "title": "Volver a vincular la Cuenta SimpliSafe" + "description": "Vuelve a introducir la contrase\u00f1a de {username}", + "title": "Reautenticaci\u00f3n de la integraci\u00f3n" + }, + "sms_2fa": { + "data": { + "code": "C\u00f3digo" + }, + "description": "Introduce el c\u00f3digo de autenticaci\u00f3n de dos factores enviado por SMS." }, "user": { "data": { diff --git a/homeassistant/components/slack/translations/ca.json b/homeassistant/components/slack/translations/ca.json index 58ff126ae27..ca107cede26 100644 --- a/homeassistant/components/slack/translations/ca.json +++ b/homeassistant/components/slack/translations/ca.json @@ -15,7 +15,14 @@ "default_channel": "Canal predeterminat", "icon": "Icona", "username": "Nom d'usuari" - } + }, + "data_description": { + "api_key": "'Token' API de Slack que s'utilitza per enviar els missatges de Slack.", + "default_channel": "Canal al qual publicar en cas que no s'especifiqui cap canal en enviar un missatge.", + "icon": "Utilitza un dels emojis de Slack com a icona per al nom d'usuari proporcionat.", + "username": "Home Assistant publicar\u00e0 a Slack amb el nom d'usuari especificat." + }, + "description": "Consulta la documentaci\u00f3 per obtenir la teva clau API de Slack." } } } diff --git a/homeassistant/components/slack/translations/es.json b/homeassistant/components/slack/translations/es.json new file mode 100644 index 00000000000..52aee38b4f9 --- /dev/null +++ b/homeassistant/components/slack/translations/es.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Error al conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "default_channel": "Canal por defecto", + "icon": "Icono", + "username": "Nombre de usuario" + }, + "data_description": { + "api_key": "El token de la API de Slack que se usar\u00e1 para enviar mensajes de Slack.", + "default_channel": "Canal al que publicar en caso de que no se especifique ning\u00fan canal al enviar un mensaje.", + "icon": "Utilice uno de los emojis de Slack como icono para el nombre de usuario proporcionado.", + "username": "Home Assistant publicar\u00e1 en Slack con el nombre de usuario especificado." + }, + "description": "Consulte la documentaci\u00f3n sobre c\u00f3mo obtener su clave API de Slack." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/it.json b/homeassistant/components/slack/translations/it.json new file mode 100644 index 00000000000..dbd6a44de23 --- /dev/null +++ b/homeassistant/components/slack/translations/it.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "default_channel": "Canale predefinito", + "icon": "Icona", + "username": "Nome utente" + }, + "data_description": { + "api_key": "Il token API Slack da utilizzare per inviare messaggi Slack.", + "default_channel": "Il canale su cui inviare se non viene specificato alcun canale durante l'invio di un messaggio.", + "icon": "Usa uno degli emoji Slack come icona per il nome utente fornito.", + "username": "Home Assistant pubblicher\u00e0 su Slack utilizzando il nome utente specificato." + }, + "description": "Fai riferimento alla documentazione su come ottenere la chiave API Slack." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/sk.json b/homeassistant/components/slack/translations/sk.json new file mode 100644 index 00000000000..4e2a5fa0ed7 --- /dev/null +++ b/homeassistant/components/slack/translations/sk.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "icon": "Ikona", + "username": "U\u017e\u00edvate\u013esk\u00e9 meno" + }, + "data_description": { + "username": "Home Assistant odo\u0161le pr\u00edspevok na Slack pomocou zadan\u00e9ho pou\u017e\u00edvate\u013esk\u00e9ho mena." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/tr.json b/homeassistant/components/slack/translations/tr.json new file mode 100644 index 00000000000..4fd5a28f790 --- /dev/null +++ b/homeassistant/components/slack/translations/tr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "default_channel": "Varsay\u0131lan Kanal", + "icon": "Simge", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "data_description": { + "api_key": "Slack mesajlar\u0131 g\u00f6ndermek i\u00e7in kullan\u0131lacak Slack API belirteci.", + "default_channel": "Mesaj g\u00f6nderirken kanal belirtilmemi\u015fse g\u00f6nderi yap\u0131lacak kanal.", + "icon": "Sa\u011flanan kullan\u0131c\u0131 ad\u0131 i\u00e7in Slack emojilerinden birini simge olarak kullan\u0131n.", + "username": "Home Assistant, belirtilen kullan\u0131c\u0131 ad\u0131n\u0131 kullanarak Slack'e g\u00f6nderecektir." + }, + "description": "Slack API anahtar\u0131n\u0131z\u0131 almayla ilgili belgelere bak\u0131n." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/es.json b/homeassistant/components/sleepiq/translations/es.json index ae6059aa227..57b8f421844 100644 --- a/homeassistant/components/sleepiq/translations/es.json +++ b/homeassistant/components/sleepiq/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "La cuenta ya est\u00e1 configurada" + "already_configured": "La cuenta ya est\u00e1 configurada", + "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" }, "error": { "cannot_connect": "Error al conectar", @@ -11,7 +12,8 @@ "reauth_confirm": { "data": { "password": "Contrase\u00f1a" - } + }, + "title": "Reautenticaci\u00f3n de la integraci\u00f3n" }, "user": { "data": { diff --git a/homeassistant/components/slimproto/translations/es.json b/homeassistant/components/slimproto/translations/es.json new file mode 100644 index 00000000000..352f82f605f --- /dev/null +++ b/homeassistant/components/slimproto/translations/es.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya configurado. S\u00f3lo es posible una sola configuraci\u00f3n." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/tr.json b/homeassistant/components/slimproto/translations/tr.json index a152eb19468..b04fdc4468d 100644 --- a/homeassistant/components/slimproto/translations/tr.json +++ b/homeassistant/components/slimproto/translations/tr.json @@ -2,6 +2,12 @@ "config": { "abort": { "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "user": { + "one": "Bo\u015f", + "other": "Bo\u015f" + } } } } \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/es.json b/homeassistant/components/smappee/translations/es.json index a1124557876..891e9642d53 100644 --- a/homeassistant/components/smappee/translations/es.json +++ b/homeassistant/components/smappee/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured_device": "El dispositivo ya est\u00e1 configurado", "already_configured_local_device": "Los dispositivos locales ya est\u00e1n configurados. Elim\u00ednelos primero antes de configurar un dispositivo en la nube.", - "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", "cannot_connect": "No se pudo conectar", "invalid_mdns": "Dispositivo no compatible para la integraci\u00f3n de Smappee.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", diff --git a/homeassistant/components/smarttub/translations/es.json b/homeassistant/components/smarttub/translations/es.json index 20a57210448..8f2eb153cb7 100644 --- a/homeassistant/components/smarttub/translations/es.json +++ b/homeassistant/components/smarttub/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_configured": "La cuenta ya est\u00e1 configurada", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/smhi/translations/es.json b/homeassistant/components/smhi/translations/es.json index d9902ba7d08..ab34438b12d 100644 --- a/homeassistant/components/smhi/translations/es.json +++ b/homeassistant/components/smhi/translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada" + }, "error": { "name_exists": "Nombre ya existe", "wrong_location": "Ubicaci\u00f3n Suecia solamente" diff --git a/homeassistant/components/sms/translations/es.json b/homeassistant/components/sms/translations/es.json index f2f36d426f9..27669a2b52f 100644 --- a/homeassistant/components/sms/translations/es.json +++ b/homeassistant/components/sms/translations/es.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "baud_speed": "Velocidad en Baudis", "device": "Dispositivo" }, "title": "Conectar con el m\u00f3dem" diff --git a/homeassistant/components/sms/translations/tr.json b/homeassistant/components/sms/translations/tr.json index 0488390beed..1558fe5a58e 100644 --- a/homeassistant/components/sms/translations/tr.json +++ b/homeassistant/components/sms/translations/tr.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "baud_speed": "Baud H\u0131z\u0131", "device": "Cihaz" }, "title": "Modeme ba\u011flan\u0131n" diff --git a/homeassistant/components/solax/translations/es.json b/homeassistant/components/solax/translations/es.json index 4728aed6395..f6658c63353 100644 --- a/homeassistant/components/solax/translations/es.json +++ b/homeassistant/components/solax/translations/es.json @@ -1,7 +1,15 @@ { "config": { "error": { + "cannot_connect": "Fall\u00f3 la conexi\u00f3n", "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "port": "Puerto" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/soma/translations/es.json b/homeassistant/components/soma/translations/es.json index 6f6b47275dd..0ae2d26adec 100644 --- a/homeassistant/components/soma/translations/es.json +++ b/homeassistant/components/soma/translations/es.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_setup": "S\u00f3lo puede configurar una cuenta de Soma.", - "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", "connection_error": "No se ha podido conectar a SOMA Connect.", "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, leer la documentaci\u00f3n.", "result_error": "SOMA Connect respondi\u00f3 con un error." }, "create_entry": { - "default": "Autenticado con \u00e9xito con Soma." + "default": "Autenticaci\u00f3n exitosa" }, "step": { "user": { diff --git a/homeassistant/components/somfy/translations/es.json b/homeassistant/components/somfy/translations/es.json index 1fd22ebda3f..2f8b35f5af8 100644 --- a/homeassistant/components/somfy/translations/es.json +++ b/homeassistant/components/somfy/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n", - "missing_configuration": "El componente Somfy no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, diff --git a/homeassistant/components/sonarr/translations/es.json b/homeassistant/components/sonarr/translations/es.json index b2f073297d9..f7d55cc5353 100644 --- a/homeassistant/components/sonarr/translations/es.json +++ b/homeassistant/components/sonarr/translations/es.json @@ -13,7 +13,7 @@ "step": { "reauth_confirm": { "description": "La integraci\u00f3n de Sonarr necesita volver a autenticarse manualmente con la API de Sonarr alojada en: {host}", - "title": "Volver a autenticarse con Sonarr" + "title": "Reautenticaci\u00f3n de la integraci\u00f3n" }, "user": { "data": { diff --git a/homeassistant/components/sonos/translations/es.json b/homeassistant/components/sonos/translations/es.json index 3560280c90e..e6af9ac0e44 100644 --- a/homeassistant/components/sonos/translations/es.json +++ b/homeassistant/components/sonos/translations/es.json @@ -3,7 +3,7 @@ "abort": { "no_devices_found": "No se encontraron dispositivos en la red", "not_sonos_device": "El dispositivo descubierto no es un dispositivo Sonos", - "single_instance_allowed": "S\u00f3lo se necesita una \u00fanica configuraci\u00f3n de Sonos." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "step": { "confirm": { diff --git a/homeassistant/components/speedtestdotnet/translations/es.json b/homeassistant/components/speedtestdotnet/translations/es.json index 1ccca471c7e..1727f4d2a6c 100644 --- a/homeassistant/components/speedtestdotnet/translations/es.json +++ b/homeassistant/components/speedtestdotnet/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", - "wrong_server_id": "Id del servidor no v\u00e1lido" + "wrong_server_id": "El identificador del servidor no es v\u00e1lido" }, "step": { "user": { diff --git a/homeassistant/components/sql/translations/es.json b/homeassistant/components/sql/translations/es.json new file mode 100644 index 00000000000..3d794607feb --- /dev/null +++ b/homeassistant/components/sql/translations/es.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada" + }, + "error": { + "db_url_invalid": "URL de la base de datos inv\u00e1lido" + }, + "step": { + "user": { + "data": { + "name": "Nombre", + "query": "Selecciona la consulta", + "unit_of_measurement": "Unidad de medida", + "value_template": "Plantilla de valor" + }, + "data_description": { + "column": "Columna de respuesta de la consulta para presentar como estado", + "db_url": "URL de la base de datos, d\u00e9jalo en blanco para utilizar la predeterminada de HA", + "name": "Nombre que se utilizar\u00e1 para la entrada de configuraci\u00f3n y tambi\u00e9n para el sensor", + "query": "Consulta a ejecutar, debe empezar por 'SELECT'", + "unit_of_measurement": "Unidad de medida (opcional)", + "value_template": "Plantilla de valor (opcional)" + } + } + } + }, + "options": { + "error": { + "db_url_invalid": "URL de la base de datos inv\u00e1lido", + "query_invalid": "Consulta SQL inv\u00e1lida", + "value_template_invalid": "Plantilla de valor inv\u00e1lida" + }, + "step": { + "init": { + "data": { + "column": "Columna", + "db_url": "URL de la base de datos", + "name": "Nombre", + "query": "Selecciona la consulta", + "unit_of_measurement": "Unidad de medida", + "value_template": "Plantilla de valor" + }, + "data_description": { + "column": "Columna de respuesta de la consulta para presentar como estado", + "db_url": "URL de la base de datos, d\u00e9jalo en blanco para utilizar la predeterminada de HA", + "name": "Nombre que se utilizar\u00e1 para la entrada de configuraci\u00f3n y tambi\u00e9n para el sensor", + "query": "Consulta a ejecutar, debe empezar por 'SELECT'", + "unit_of_measurement": "Unidad de medida (opcional)", + "value_template": "Plantilla de valor (opcional)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/es.json b/homeassistant/components/steam_online/translations/es.json new file mode 100644 index 00000000000..9636fc04a54 --- /dev/null +++ b/homeassistant/components/steam_online/translations/es.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", + "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + }, + "error": { + "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "invalid_account": "ID de la cuenta inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "title": "Volver a autenticar la integraci\u00f3n" + }, + "user": { + "data": { + "account": "ID de la cuenta de Steam", + "api_key": "Clave API" + }, + "description": "Utiliza {account_id_url} para encontrar el ID de tu cuenta de Steam" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "accounts": "Nombres de las cuentas a controlar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/es.json b/homeassistant/components/subaru/translations/es.json index 42a13a37a7a..170983e8df4 100644 --- a/homeassistant/components/subaru/translations/es.json +++ b/homeassistant/components/subaru/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "cannot_connect": "No se pudo conectar" }, "error": { @@ -18,11 +18,25 @@ "description": "Por favor, introduzca su PIN de MySubaru\nNOTA: Todos los veh\u00edculos de la cuenta deben tener el mismo PIN", "title": "Configuraci\u00f3n de Subaru Starlink" }, + "two_factor": { + "data": { + "contact_method": "Selecciona un m\u00e9todo de contacto:" + }, + "description": "Autenticaci\u00f3n de dos factores requerida", + "title": "Configuraci\u00f3n de Subaru Starlink" + }, + "two_factor_validate": { + "data": { + "validation_code": "C\u00f3digo de validaci\u00f3n" + }, + "description": "Introduce el c\u00f3digo de validaci\u00f3n recibido", + "title": "Configuraci\u00f3n de Subaru Starlink" + }, "user": { "data": { "country": "Seleccionar pa\u00eds", "password": "Contrase\u00f1a", - "username": "Nombre de usuario" + "username": "Usuario" }, "description": "Por favor, introduzca sus credenciales de MySubaru\nNOTA: La configuraci\u00f3n inicial puede tardar hasta 30 segundos", "title": "Configuraci\u00f3n de Subaru Starlink" diff --git a/homeassistant/components/surepetcare/translations/es.json b/homeassistant/components/surepetcare/translations/es.json index 3d3945748cb..13f2eb38bef 100644 --- a/homeassistant/components/surepetcare/translations/es.json +++ b/homeassistant/components/surepetcare/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada" + "already_configured": "La cuenta ya est\u00e1 configurada" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/surepetcare/translations/sk.json b/homeassistant/components/surepetcare/translations/sk.json index 5ada995aa6e..1b1e671c054 100644 --- a/homeassistant/components/surepetcare/translations/sk.json +++ b/homeassistant/components/surepetcare/translations/sk.json @@ -2,6 +2,13 @@ "config": { "error": { "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "password": "Heslo" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/switch/translations/es.json b/homeassistant/components/switch/translations/es.json index 95a60ab55ea..2c1f6050ce6 100644 --- a/homeassistant/components/switch/translations/es.json +++ b/homeassistant/components/switch/translations/es.json @@ -1,4 +1,11 @@ { + "config": { + "step": { + "init": { + "description": "Seleccione el interruptor de la l\u00e1mpara." + } + } + }, "device_automation": { "action_type": { "toggle": "Alternar {entity_name}", diff --git a/homeassistant/components/switch_as_x/translations/es.json b/homeassistant/components/switch_as_x/translations/es.json new file mode 100644 index 00000000000..7e91d3217a4 --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_id": "Conmutador", + "target_domain": "Nuevo tipo" + } + } + } + }, + "title": "Cambia el tipo de dispositivo de un conmutador" +} \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/es.json b/homeassistant/components/syncthru/translations/es.json index 513cbf17fc0..da3002496db 100644 --- a/homeassistant/components/syncthru/translations/es.json +++ b/homeassistant/components/syncthru/translations/es.json @@ -8,7 +8,7 @@ "syncthru_not_supported": "El dispositivo no es compatible con SyncThru", "unknown_state": "Estado de la impresora desconocido, verifica la URL y la conectividad de la red" }, - "flow_title": "Impresora 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 134f99cd75d..382797dc81e 100644 --- a/homeassistant/components/synology_dsm/translations/es.json +++ b/homeassistant/components/synology_dsm/translations/es.json @@ -10,7 +10,7 @@ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "missing_data": "Faltan datos: por favor, vuelva a intentarlo m\u00e1s tarde o pruebe con otra configuraci\u00f3n", "otp_failed": "La autenticaci\u00f3n de dos pasos fall\u00f3, vuelva a intentar con un nuevo c\u00f3digo de acceso", - "unknown": "Error desconocido: por favor, consulta logs para obtener m\u00e1s detalles" + "unknown": "Error inesperado" }, "flow_title": "{name} ({host})", "step": { @@ -64,6 +64,7 @@ "init": { "data": { "scan_interval": "Minutos entre escaneos", + "snap_profile_type": "Calidad de las fotos de la c\u00e1mara (0:alta, 1:media, 2:baja)", "timeout": "Tiempo de espera (segundos)" } } diff --git a/homeassistant/components/synology_dsm/translations/sk.json b/homeassistant/components/synology_dsm/translations/sk.json index e9c37059842..59f0e333d63 100644 --- a/homeassistant/components/synology_dsm/translations/sk.json +++ b/homeassistant/components/synology_dsm/translations/sk.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Zariadenie je u\u017e nakonfigurovan\u00e9", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { @@ -12,6 +13,11 @@ "port": "Port" } }, + "reauth": { + "data": { + "password": "Heslo" + } + }, "user": { "data": { "port": "Port" diff --git a/homeassistant/components/system_bridge/translations/sk.json b/homeassistant/components/system_bridge/translations/sk.json index 276eac51dd4..4a6f823bbe6 100644 --- a/homeassistant/components/system_bridge/translations/sk.json +++ b/homeassistant/components/system_bridge/translations/sk.json @@ -4,7 +4,8 @@ "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "invalid_auth": "Neplatn\u00e9 overenie" + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { "authenticate": { diff --git a/homeassistant/components/tado/translations/es.json b/homeassistant/components/tado/translations/es.json index c9a26a1a9ab..db71d41f789 100644 --- a/homeassistant/components/tado/translations/es.json +++ b/homeassistant/components/tado/translations/es.json @@ -23,7 +23,7 @@ "step": { "init": { "data": { - "fallback": "Activar modo de salvaguarda." + "fallback": "Elige el modo alternativo." }, "description": "El modo de salvaguarda volver\u00e1 a la Planificaci\u00f3n Inteligente en el siguiente cambio de programaci\u00f3n despu\u00e9s de ajustar manualmente una zona.", "title": "Ajustar las opciones de Tado" diff --git a/homeassistant/components/tankerkoenig/translations/es.json b/homeassistant/components/tankerkoenig/translations/es.json new file mode 100644 index 00000000000..ec97b5886d3 --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/es.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "no_stations": "No se pudo encontrar ninguna estaci\u00f3n al alcance." + }, + "step": { + "select_station": { + "data": { + "stations": "Estaciones" + }, + "title": "Selecciona las estaciones a a\u00f1adir" + }, + "user": { + "data": { + "name": "Nombre de la regi\u00f3n" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Muestra las estaciones en el mapa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/sk.json b/homeassistant/components/tankerkoenig/translations/sk.json new file mode 100644 index 00000000000..06c74b52725 --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "radius": "Polomer vyh\u013ead\u00e1vania" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/es.json b/homeassistant/components/tautulli/translations/es.json new file mode 100644 index 00000000000..0d6fd1d3b9e --- /dev/null +++ b/homeassistant/components/tautulli/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Clave API" + }, + "title": "Re-autenticaci\u00f3n de Tautulli" + }, + "user": { + "data": { + "url": "URL", + "verify_ssl": "Verifica el certificat SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/es.json b/homeassistant/components/tellduslive/translations/es.json index 882e553d46e..5971c89f43e 100644 --- a/homeassistant/components/tellduslive/translations/es.json +++ b/homeassistant/components/tellduslive/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "TelldusLive ya est\u00e1 configurado", + "already_configured": "El servicio ya est\u00e1 configurado", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n", "unknown": "Se produjo un error desconocido", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." diff --git a/homeassistant/components/threshold/translations/es.json b/homeassistant/components/threshold/translations/es.json new file mode 100644 index 00000000000..0200c840243 --- /dev/null +++ b/homeassistant/components/threshold/translations/es.json @@ -0,0 +1,24 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_id": "Sensor de entrada", + "hysteresis": "Hist\u00e9resis", + "lower": "L\u00edmite inferior", + "upper": "L\u00edmite superior" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "hysteresis": "Hist\u00e9resis" + } + } + } + }, + "title": "Sensor de umbral" +} \ No newline at end of file diff --git a/homeassistant/components/tile/translations/es.json b/homeassistant/components/tile/translations/es.json index 42eb3527a6f..c13488183d3 100644 --- a/homeassistant/components/tile/translations/es.json +++ b/homeassistant/components/tile/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "reauth_successful": "La reautenticaci\u00f3n fue exitosa" }, "error": { diff --git a/homeassistant/components/tod/translations/es.json b/homeassistant/components/tod/translations/es.json new file mode 100644 index 00000000000..899b4ce18f0 --- /dev/null +++ b/homeassistant/components/tod/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "user": { + "data": { + "after_time": "Tiempo de activaci\u00f3n", + "before_time": "Tiempo de desactivaci\u00f3n" + }, + "description": "Crea un sensor binario que se activa o desactiva en funci\u00f3n de la hora.", + "title": "A\u00f1ade sensor tiempo del d\u00eda" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after_time": "Tiempo de activaci\u00f3n" + }, + "description": "Crea un sensor binario que se activa o desactiva en funci\u00f3n de la hora." + } + } + }, + "title": "Sensor tiempo del d\u00eda" +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/es.json b/homeassistant/components/tomorrowio/translations/es.json new file mode 100644 index 00000000000..9ad330dab15 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/es.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "invalid_api_key": "Clave API inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "latitude": "Latitud", + "location": "Ubicaci\u00f3n", + "name": "Nombre" + }, + "description": "Para obtener una clave API, reg\u00edstrate en [Tomorrow.io](https://app.tomorrow.io/signup)" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. entre previsiones de NowCast" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.es.json b/homeassistant/components/tomorrowio/translations/sensor.es.json new file mode 100644 index 00000000000..03820d30265 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.es.json @@ -0,0 +1,16 @@ +{ + "state": { + "tomorrowio__health_concern": { + "unhealthy": "Poco saludable", + "unhealthy_for_sensitive_groups": "No saludable para grupos sensibles", + "very_unhealthy": "Nada saludable" + }, + "tomorrowio__pollen_index": { + "medium": "Medio", + "very_low": "Muy bajo" + }, + "tomorrowio__precipitation_type": { + "snow": "Nieve" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.sk.json b/homeassistant/components/tomorrowio/translations/sensor.sk.json new file mode 100644 index 00000000000..3dd3dede27b --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.sk.json @@ -0,0 +1,7 @@ +{ + "state": { + "tomorrowio__pollen_index": { + "low": "N\u00edzka" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sk.json b/homeassistant/components/tomorrowio/translations/sk.json new file mode 100644 index 00000000000..7f480c9778c --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API k\u013e\u00fa\u010d", + "name": "Meno" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/es.json b/homeassistant/components/toon/translations/es.json index d048e53ec90..423d81dde0c 100644 --- a/homeassistant/components/toon/translations/es.json +++ b/homeassistant/components/toon/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El acuerdo seleccionado ya est\u00e1 configurado.", - "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_agreements": "Esta cuenta no tiene pantallas Toon.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", diff --git a/homeassistant/components/totalconnect/translations/es.json b/homeassistant/components/totalconnect/translations/es.json index 5c402fb76af..97822a10300 100644 --- a/homeassistant/components/totalconnect/translations/es.json +++ b/homeassistant/components/totalconnect/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "no_locations": "No hay ubicaciones disponibles para este usuario, compruebe la configuraci\u00f3n de TotalConnect", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, diff --git a/homeassistant/components/traccar/translations/es.json b/homeassistant/components/traccar/translations/es.json index a87e7aae2c1..851984b0024 100644 --- a/homeassistant/components/traccar/translations/es.json +++ b/homeassistant/components/traccar/translations/es.json @@ -5,12 +5,12 @@ "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, "create_entry": { - "default": "Para enviar eventos a Home Assistant, necesitar\u00e1 configurar la funci\u00f3n de webhook en Traccar.\n\nUtilice la siguiente url: ``{webhook_url}``\n\nConsulte la [documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." + "default": "Para enviar eventos a Home Assistant, tendr\u00e1s que configurar la opci\u00f3n webhook de Traccar.\n\nUtilice la siguiente url: `{webhook_url}`\n\nConsulte la [documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." }, "step": { "user": { - "description": "\u00bfEst\u00e1 seguro de querer configurar Traccar?", - "title": "Configurar Traccar" + "description": "\u00bfEst\u00e1s seguro de que quieres configurar Traccar?", + "title": "Configura Traccar" } } } diff --git a/homeassistant/components/tradfri/translations/es.json b/homeassistant/components/tradfri/translations/es.json index 1f342c4630e..531b11cc0a1 100644 --- a/homeassistant/components/tradfri/translations/es.json +++ b/homeassistant/components/tradfri/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "La pasarela ya est\u00e1 configurada", - "already_in_progress": "La configuraci\u00f3n de la pasarela ya est\u00e1 en marcha." + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso" }, "error": { "cannot_authenticate": "No se puede autenticar, \u00bfGateway est\u00e1 emparejado con otro servidor como, por ejemplo, Homekit?", diff --git a/homeassistant/components/trafikverket_ferry/translations/es.json b/homeassistant/components/trafikverket_ferry/translations/es.json new file mode 100644 index 00000000000..26532fbce5e --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + }, + "error": { + "incorrect_api_key": "Clave API inv\u00e1lida para la cuenta seleccionada", + "invalid_route": "No se pudo encontrar la ruta con la informaci\u00f3n proporcionada" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Clave API" + } + }, + "user": { + "data": { + "api_key": "Clave API", + "from": "Des del puerto", + "time": "Hora", + "to": "Al puerto" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/es.json b/homeassistant/components/trafikverket_train/translations/es.json new file mode 100644 index 00000000000..4ce1da04b02 --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/es.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "incorrect_api_key": "Clave API inv\u00e1lida para la cuenta seleccionada", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "from": "Desde la estaci\u00f3n", + "time": "Hora (opcional)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/es.json b/homeassistant/components/tuya/translations/es.json index bcd83f85af9..235f87d5374 100644 --- a/homeassistant/components/tuya/translations/es.json +++ b/homeassistant/components/tuya/translations/es.json @@ -30,13 +30,13 @@ "access_secret": "Tuya IoT Access Secret", "country_code": "C\u00f3digo de pais de tu cuenta (por ejemplo, 1 para USA o 86 para China)", "password": "Contrase\u00f1a", - "platform": "La aplicaci\u00f3n en la cual registraste tu cuenta", + "platform": "La aplicaci\u00f3n donde se registra tu cuenta", "region": "Regi\u00f3n", "tuya_project_type": "Tipo de proyecto en la nube de Tuya", "username": "Usuario" }, - "description": "Introduce tu credencial Tuya.", - "title": "Tuya" + "description": "Introduce tus credencial de Tuya", + "title": "Integraci\u00f3n Tuya" } } }, diff --git a/homeassistant/components/tuya/translations/select.es.json b/homeassistant/components/tuya/translations/select.es.json index 7dc4cf1067b..e29ccdc381f 100644 --- a/homeassistant/components/tuya/translations/select.es.json +++ b/homeassistant/components/tuya/translations/select.es.json @@ -27,6 +27,10 @@ "0": "Sensibilidad baja", "1": "Sensibilidad alta" }, + "tuya__fan_angle": { + "30": "30\u00b0", + "90": "90\u00b0" + }, "tuya__fingerbot_mode": { "click": "Push", "switch": "Interruptor" @@ -43,7 +47,12 @@ "level_8": "Nivel 8", "level_9": "Nivel 9" }, + "tuya__humidifier_moodlighting": { + "3": "Estado 3", + "4": "Estado 4" + }, "tuya__humidifier_spray_mode": { + "auto": "Autom\u00e1tico", "health": "Salud", "humidity": "Humedad", "sleep": "Dormir", diff --git a/homeassistant/components/tuya/translations/select.sk.json b/homeassistant/components/tuya/translations/select.sk.json new file mode 100644 index 00000000000..493576472b9 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.sk.json @@ -0,0 +1,18 @@ +{ + "state": { + "tuya__countdown": { + "3h": "3 hodiny" + }, + "tuya__decibel_sensitivity": { + "1": "Vysok\u00e1 citlivos\u0165" + }, + "tuya__motion_sensitivity": { + "0": "N\u00edzka citlivos\u0165", + "1": "Stredn\u00e1 citlivos\u0165", + "2": "Vysok\u00e1 citlivos\u0165" + }, + "tuya__vacuum_mode": { + "random": "N\u00e1hodn\u00fd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sensor.sk.json b/homeassistant/components/tuya/translations/sensor.sk.json new file mode 100644 index 00000000000..4f80ab106ad --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.sk.json @@ -0,0 +1,7 @@ +{ + "state": { + "tuya__status": { + "boiling_temp": "Teplota varu" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/translations/es.json b/homeassistant/components/twentemilieu/translations/es.json index 259d202a9c3..93effca8aad 100644 --- a/homeassistant/components/twentemilieu/translations/es.json +++ b/homeassistant/components/twentemilieu/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_address": "Direcci\u00f3n no encontrada en el \u00e1rea de servicio de Twente Milieu." + "invalid_address": "No se ha encontrado la direcci\u00f3n en el \u00e1rea de servicio de Twente Milieu." }, "step": { "user": { @@ -14,7 +14,7 @@ "house_number": "N\u00famero de casa", "post_code": "C\u00f3digo postal" }, - "description": "Configure Twente Milieu proporcionando informaci\u00f3n sobre la recolecci\u00f3n de residuos en su direcci\u00f3n." + "description": "Configura Twente Milieu con informaci\u00f3n de la recogida de residuos en tu direcci\u00f3n." } } } diff --git a/homeassistant/components/twilio/translations/es.json b/homeassistant/components/twilio/translations/es.json index 46499faca59..41057c5f4a0 100644 --- a/homeassistant/components/twilio/translations/es.json +++ b/homeassistant/components/twilio/translations/es.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "No conectado a Home Assistant Cloud.", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, @@ -9,7 +10,7 @@ }, "step": { "user": { - "description": "\u00bfEst\u00e1s seguro de que quieres configurar Twilio?", + "description": "\u00bfQuieres empezar la configuraci\u00f3n?", "title": "Configurar el Webhook de Twilio" } } diff --git a/homeassistant/components/twinkly/translations/es.json b/homeassistant/components/twinkly/translations/es.json index e18d54adb9e..a45be8da295 100644 --- a/homeassistant/components/twinkly/translations/es.json +++ b/homeassistant/components/twinkly/translations/es.json @@ -12,7 +12,7 @@ }, "user": { "data": { - "host": "Host (o direcci\u00f3n IP) de tu dispositivo Twinkly" + "host": "Host" } } } diff --git a/homeassistant/components/ukraine_alarm/translations/es.json b/homeassistant/components/ukraine_alarm/translations/es.json index 7fa891bde3e..7f8db3877c0 100644 --- a/homeassistant/components/ukraine_alarm/translations/es.json +++ b/homeassistant/components/ukraine_alarm/translations/es.json @@ -1,10 +1,33 @@ { "config": { + "abort": { + "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "max_regions": "Se pueden configurar un m\u00e1ximo de 5 regiones", + "timeout": "Tiempo m\u00e1ximo de espera para establecer la conexi\u00f3n agotado", + "unknown": "Error inesperado" + }, + "error": { + "unknown": "Error inesperado" + }, "step": { - "user": { + "district": { "data": { "region": "Regi\u00f3n" - } + }, + "description": "Si quieres monitorear no s\u00f3lo el estado, elige el distrito espec\u00edfico" + }, + "state": { + "data": { + "region": "Regi\u00f3n" + }, + "description": "Escoja el estado a monitorear" + }, + "user": { + "data": { + "api_key": "Clave API", + "region": "Regi\u00f3n" + }, + "description": "Escoja el estado a monitorear" } } } diff --git a/homeassistant/components/ukraine_alarm/translations/tr.json b/homeassistant/components/ukraine_alarm/translations/tr.json new file mode 100644 index 00000000000..3662a32b1fa --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/tr.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "max_regions": "En fazla 5 b\u00f6lge yap\u0131land\u0131r\u0131labilir", + "rate_limit": "\u00c7ok fazla istek", + "timeout": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "timeout": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "community": { + "data": { + "region": "B\u00f6lge" + }, + "description": "Yaln\u0131zca eyalet ve il\u00e7eyi izlemek istemiyorsan\u0131z, belirli toplulu\u011funu se\u00e7in" + }, + "district": { + "data": { + "region": "B\u00f6lge" + }, + "description": "Sadece eyaleti izlemek istemiyorsan\u0131z, belirli bir b\u00f6lgeyi se\u00e7in" + }, + "state": { + "data": { + "region": "B\u00f6lge" + }, + "description": "\u0130zlenecek durumu se\u00e7in" + }, + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "region": "B\u00f6lge" + }, + "description": "\u0130zlenecek durumu se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/es.json b/homeassistant/components/unifi/translations/es.json index a5963d7019e..ebaf1d3c127 100644 --- a/homeassistant/components/unifi/translations/es.json +++ b/homeassistant/components/unifi/translations/es.json @@ -21,11 +21,14 @@ "username": "Usuario", "verify_ssl": "Controlador usando el certificado adecuado" }, - "title": "Configurar el controlador UniFi" + "title": "Configuraci\u00f3n de UniFi Network" } } }, "options": { + "abort": { + "integration_not_setup": "La integraci\u00f3n UniFi no est\u00e1 configurada" + }, "step": { "client_control": { "data": { @@ -39,7 +42,7 @@ "device_tracker": { "data": { "detection_time": "Tiempo en segundos desde la \u00faltima vez que se vio hasta considerarlo desconectado", - "ignore_wired_bug": "Desactivar la l\u00f3gica para el bug cableado de UniFi", + "ignore_wired_bug": "Desactiva la l\u00f3gica de errores de UniFi Network", "ssid_filter": "Seleccione los SSIDs para realizar seguimiento de clientes inal\u00e1mbricos", "track_clients": "Seguimiento de los clientes de red", "track_devices": "Rastree dispositivos de red (dispositivos Ubiquiti)", @@ -54,7 +57,7 @@ "track_clients": "Rastree clientes de red", "track_devices": "Rastree dispositivos de red (dispositivos Ubiquiti)" }, - "description": "Configurar la integraci\u00f3n de UniFi" + "description": "Configura la integraci\u00f3n UniFi Network" }, "statistics_sensors": { "data": { @@ -62,7 +65,7 @@ "allow_uptime_sensors": "Sensores de tiempo de actividad para clientes de la red" }, "description": "Configurar estad\u00edsticas de los sensores", - "title": "Opciones UniFi 3/3" + "title": "Opciones de UniFi Network 3/3" } } } diff --git a/homeassistant/components/unifiprotect/translations/es.json b/homeassistant/components/unifiprotect/translations/es.json index bcd7efe6cdf..901f1707de5 100644 --- a/homeassistant/components/unifiprotect/translations/es.json +++ b/homeassistant/components/unifiprotect/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado", + "discovery_started": "Se ha iniciado el descubrimiento" }, "error": { "cannot_connect": "No se pudo conectar", @@ -10,6 +11,12 @@ "unknown": "Error inesperado" }, "step": { + "discovery_confirm": { + "data": { + "username": "Usuario", + "verify_ssl": "Verifica el certificado SSL" + } + }, "reauth_confirm": { "data": { "host": "IP/Host del servidor UniFi Protect", diff --git a/homeassistant/components/upb/translations/sk.json b/homeassistant/components/upb/translations/sk.json new file mode 100644 index 00000000000..3f20d345b26 --- /dev/null +++ b/homeassistant/components/upb/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/update/translations/es.json b/homeassistant/components/update/translations/es.json index 79993c92b20..ee92c01c938 100644 --- a/homeassistant/components/update/translations/es.json +++ b/homeassistant/components/update/translations/es.json @@ -1,3 +1,10 @@ { + "device_automation": { + "trigger_type": { + "changed_states": "La disponibilidad de la actualizaci\u00f3n de {entity_name} cambie", + "turned_off": "{entity_name} se actualice", + "turned_on": "{entity_name} tenga una actualizaci\u00f3n disponible" + } + }, "title": "Actualizar" } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/es.json b/homeassistant/components/upnp/translations/es.json index 356376e2e07..7a76ab44bf0 100644 --- a/homeassistant/components/upnp/translations/es.json +++ b/homeassistant/components/upnp/translations/es.json @@ -9,7 +9,7 @@ "one": "UNO", "other": "OTRO" }, - "flow_title": "UPnP / IGD: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "\u00bfQuieres configurar este dispositivo UPnP/IGD?" diff --git a/homeassistant/components/uptimerobot/translations/es.json b/homeassistant/components/uptimerobot/translations/es.json index 455c96cd644..e78a90b4176 100644 --- a/homeassistant/components/uptimerobot/translations/es.json +++ b/homeassistant/components/uptimerobot/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, elimine la integraci\u00f3n y config\u00farela nuevamente.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" @@ -9,6 +9,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_api_key": "Clave API no v\u00e1lida", + "not_main_key": "Se ha detectado un tipo de clave API incorrecta, utiliza la clave API 'principal'", "reauth_failed_matching_account": "La clave de API que has proporcionado no coincide con el ID de cuenta para la configuraci\u00f3n existente.", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/uptimerobot/translations/sensor.es.json b/homeassistant/components/uptimerobot/translations/sensor.es.json new file mode 100644 index 00000000000..1f037738b42 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.es.json @@ -0,0 +1,9 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "not_checked_yet": "No comprobado", + "pause": "En pausa", + "up": "Funcionante" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/es.json b/homeassistant/components/utility_meter/translations/es.json new file mode 100644 index 00000000000..bea05df125d --- /dev/null +++ b/homeassistant/components/utility_meter/translations/es.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "Ciclo de reinicio del contador", + "delta_values": "Valores delta", + "source": "Sensor de entrada", + "tariffs": "Tarifas soportadas" + }, + "data_description": { + "net_consumption": "Act\u00edvalo si es un contador limpio, es decir, puede aumentar y disminuir.", + "offset": "Desplaza el d\u00eda de restablecimiento mensual del contador.", + "tariffs": "Lista de tarifas admitidas, d\u00e9jala en blanco si utilizas una \u00fanica tarifa." + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "Sensor de entrada" + } + } + } + }, + "title": "Contador" +} \ No newline at end of file diff --git a/homeassistant/components/velbus/translations/es.json b/homeassistant/components/velbus/translations/es.json index cb600d577e7..ed2585a03e9 100644 --- a/homeassistant/components/velbus/translations/es.json +++ b/homeassistant/components/velbus/translations/es.json @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "name": "El nombre de esta conexi\u00f3n velbus", + "name": "Nombre de la conexi\u00f3n Velbus", "port": "Cadena de conexi\u00f3n" }, - "title": "Definir el tipo de conexi\u00f3n velbus" + "title": "Tipo de conexi\u00f3n Velbus" } } } diff --git a/homeassistant/components/verisure/translations/es.json b/homeassistant/components/verisure/translations/es.json index 7ec2812d964..19517bb7ed0 100644 --- a/homeassistant/components/verisure/translations/es.json +++ b/homeassistant/components/verisure/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/vulcan/translations/es.json b/homeassistant/components/vulcan/translations/es.json new file mode 100644 index 00000000000..fa303806a76 --- /dev/null +++ b/homeassistant/components/vulcan/translations/es.json @@ -0,0 +1,51 @@ +{ + "config": { + "abort": { + "already_configured": "Ya se ha a\u00f1adido a este alumno.", + "reauth_successful": "Re-autenticaci\u00f3n exitosa" + }, + "error": { + "expired_token": "Token caducado, genera un nuevo token", + "invalid_symbol": "S\u00edmbolo inv\u00e1lido", + "invalid_token": "Token inv\u00e1lido" + }, + "step": { + "auth": { + "data": { + "pin": "PIN" + } + }, + "reauth": { + "data": { + "pin": "PIN", + "region": "S\u00edmbolo", + "token": "Token" + }, + "description": "Inicia sesi\u00f3n en tu cuenta de Vulcan mediante la p\u00e1gina de registro de la aplicaci\u00f3n m\u00f3vil." + }, + "reauth_confirm": { + "data": { + "region": "S\u00edmbolo" + } + }, + "select_student": { + "data": { + "student_name": "Selecciona al alumno" + } + } + } + }, + "options": { + "error": { + "error": "Se ha producido un error" + }, + "step": { + "init": { + "data": { + "message_notify": "Muestra notificaciones cuando se reciba un mensaje nuevo", + "scan_interval": "Intervalo de actualizaci\u00f3n (minutos)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/sk.json b/homeassistant/components/vulcan/translations/sk.json new file mode 100644 index 00000000000..c16ed208d24 --- /dev/null +++ b/homeassistant/components/vulcan/translations/sk.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "add_next_config_entry": { + "description": "Prida\u0165 \u010fal\u0161ieho \u0161tudenta." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/weather/translations/es.json b/homeassistant/components/weather/translations/es.json index 2457f68cf92..a6a7336e612 100644 --- a/homeassistant/components/weather/translations/es.json +++ b/homeassistant/components/weather/translations/es.json @@ -9,7 +9,7 @@ "lightning": "Rel\u00e1mpagos", "lightning-rainy": "Rel\u00e1mpagos, lluvioso", "partlycloudy": "Parcialmente nublado", - "pouring": "Torrencial", + "pouring": "Lluvia", "rainy": "Lluvioso", "snowy": "Nevado", "snowy-rainy": "Nevado, lluvioso", diff --git a/homeassistant/components/weather/translations/sk.json b/homeassistant/components/weather/translations/sk.json index 12c3e530e9e..b174efccf4f 100644 --- a/homeassistant/components/weather/translations/sk.json +++ b/homeassistant/components/weather/translations/sk.json @@ -11,7 +11,7 @@ "partlycloudy": "\u010ciasto\u010dne zamra\u010den\u00e9", "pouring": "Lej\u00faco", "rainy": "Da\u017edivo", - "snowy": "Zasne\u017eeno", + "snowy": "Zasne\u017een\u00fd", "snowy-rainy": "Zasne\u017eeno, da\u017edivo", "sunny": "slne\u010dno", "windy": "Veterno", diff --git a/homeassistant/components/webostv/translations/es.json b/homeassistant/components/webostv/translations/es.json index d15f31b514c..712de905ddc 100644 --- a/homeassistant/components/webostv/translations/es.json +++ b/homeassistant/components/webostv/translations/es.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "error_pairing": "Conectado a LG webOS TV pero no emparejado" }, "error": { @@ -13,6 +15,9 @@ "title": "Emparejamiento de webOS TV" }, "user": { + "data": { + "name": "Nombre" + }, "description": "Encienda la televisi\u00f3n, rellene los siguientes campos y haga clic en enviar", "title": "Conectarse a webOS TV" } diff --git a/homeassistant/components/wemo/translations/es.json b/homeassistant/components/wemo/translations/es.json index 4c176762d04..29dc3d8db46 100644 --- a/homeassistant/components/wemo/translations/es.json +++ b/homeassistant/components/wemo/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "No se encontraron dispositivos Wemo en la red.", - "single_instance_allowed": "Solo es posible una \u00fanica configuraci\u00f3n de Wemo." + "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": { diff --git a/homeassistant/components/whois/translations/es.json b/homeassistant/components/whois/translations/es.json index 712c85865cd..0da233e02ad 100644 --- a/homeassistant/components/whois/translations/es.json +++ b/homeassistant/components/whois/translations/es.json @@ -3,6 +3,10 @@ "abort": { "already_configured": "El servicio ya est\u00e1 configurado" }, + "error": { + "unknown_date_format": "Formato de fecha desconocido en la respuesta del servidor whois", + "whois_command_failed": "El comando whois ha fallado: no se pudo obtener la informaci\u00f3n whois" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/whois/translations/sk.json b/homeassistant/components/whois/translations/sk.json new file mode 100644 index 00000000000..102f110d9ba --- /dev/null +++ b/homeassistant/components/whois/translations/sk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "domain": "Dom\u00e9nov\u00e9 meno" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/es.json b/homeassistant/components/wiffi/translations/es.json index 392392e0f31..dbcec6f2dc9 100644 --- a/homeassistant/components/wiffi/translations/es.json +++ b/homeassistant/components/wiffi/translations/es.json @@ -8,7 +8,7 @@ "step": { "user": { "data": { - "port": "Puerto del servidor" + "port": "Puerto" }, "title": "Configurar servidor TCP para dispositivos WIFFI" } diff --git a/homeassistant/components/wilight/translations/es.json b/homeassistant/components/wilight/translations/es.json index b0104e6e44c..23dfdfcc728 100644 --- a/homeassistant/components/wilight/translations/es.json +++ b/homeassistant/components/wilight/translations/es.json @@ -5,10 +5,10 @@ "not_supported_device": "Este WiLight no es compatible actualmente", "not_wilight_device": "Este dispositivo no es un Wilight" }, - "flow_title": "WiLight: {name}", + "flow_title": "{name}", "step": { "confirm": { - "description": "\u00bfQuieres configurar WiLight {name} ? \n\n Es compatible con: {components}", + "description": "Se admiten los siguientes componentes: {componentes}", "title": "WiLight" } } diff --git a/homeassistant/components/withings/translations/es.json b/homeassistant/components/withings/translations/es.json index 7f83e45c8c9..d3a3f60b12f 100644 --- a/homeassistant/components/withings/translations/es.json +++ b/homeassistant/components/withings/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Configuraci\u00f3n actualizada para el perfil.", - "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" }, @@ -10,16 +10,16 @@ "default": "Autenticado correctamente con Withings." }, "error": { - "already_configured": "La cuenta ya ha sido configurada" + "already_configured": "La cuenta ya est\u00e1 configurada" }, - "flow_title": "Withings: {profile}", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" }, "profile": { "data": { - "profile": "Perfil" + "profile": "Nombre de perfil" }, "description": "\u00bfQu\u00e9 perfil seleccion\u00f3 en el sitio web de Withings? Es importante que los perfiles coincidan, de lo contrario los datos se etiquetar\u00e1n incorrectamente.", "title": "Perfil de usuario." diff --git a/homeassistant/components/wiz/translations/sk.json b/homeassistant/components/wiz/translations/sk.json index af15f92c2f2..f97ee8e1837 100644 --- a/homeassistant/components/wiz/translations/sk.json +++ b/homeassistant/components/wiz/translations/sk.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/wled/translations/es.json b/homeassistant/components/wled/translations/es.json index d883ba6ff23..07ebd6c2e02 100644 --- a/homeassistant/components/wled/translations/es.json +++ b/homeassistant/components/wled/translations/es.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "No se pudo conectar" }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/ws66i/translations/es.json b/homeassistant/components/ws66i/translations/es.json new file mode 100644 index 00000000000..d19017fdb95 --- /dev/null +++ b/homeassistant/components/ws66i/translations/es.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Ha fallado la conexi\u00f3n", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "ip_address": "Direcci\u00f3n IP" + }, + "title": "Conexi\u00f3n con el dispositivo" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Nombre de la fuente #1", + "source_2": "Nombre de la fuente #2", + "source_3": "Nombre de la fuente #3", + "source_4": "Nombre de la fuente #4", + "source_5": "Nombre de la fuente #5", + "source_6": "Nombre de la fuente #6" + }, + "title": "Configuraci\u00f3n de las fuentes" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/tr.json b/homeassistant/components/ws66i/translations/tr.json new file mode 100644 index 00000000000..5baea0cee9d --- /dev/null +++ b/homeassistant/components/ws66i/translations/tr.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "ip_address": "IP Adresi" + }, + "title": "Cihaza ba\u011flan\u0131n" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Kaynak #1 ad\u0131", + "source_2": "Kaynak #2 ad\u0131", + "source_3": "Kaynak #3 ad\u0131", + "source_4": "Kaynak #4 ad\u0131", + "source_5": "Kaynak #5 ad\u0131", + "source_6": "Kaynak #6 ad\u0131" + }, + "title": "Kaynaklar\u0131 yap\u0131land\u0131r" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/es.json b/homeassistant/components/xbox/translations/es.json index 52fb998dfd3..27cbeaec139 100644 --- a/homeassistant/components/xbox/translations/es.json +++ b/homeassistant/components/xbox/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, diff --git a/homeassistant/components/xiaomi_aqara/translations/es.json b/homeassistant/components/xiaomi_aqara/translations/es.json index 04da1a8caf5..671797b914f 100644 --- a/homeassistant/components/xiaomi_aqara/translations/es.json +++ b/homeassistant/components/xiaomi_aqara/translations/es.json @@ -16,7 +16,7 @@ "step": { "select": { "data": { - "select_ip": "IP del gateway" + "select_ip": "Direcci\u00f3n IP" }, "description": "Ejecuta la configuraci\u00f3n de nuevo si deseas conectar gateways adicionales", "title": "Selecciona el Xiaomi Aqara Gateway que quieres conectar" @@ -27,7 +27,7 @@ "name": "Nombre del Gateway" }, "description": "La clave (contrase\u00f1a) se puede obtener con este tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Si no se proporciona la clave solo se podr\u00e1 acceder a los sensores", - "title": "Xiaomi Aqara Gateway, configuraciones opcionales" + "title": "Configuraciones opcionales" }, "user": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/es.json b/homeassistant/components/xiaomi_miio/translations/es.json index ae59404a793..2c1dad04111 100644 --- a/homeassistant/components/xiaomi_miio/translations/es.json +++ b/homeassistant/components/xiaomi_miio/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n para este dispositivo Xiaomi Miio ya est\u00e1 en marcha.", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "incomplete_info": "Informaci\u00f3n incompleta para configurar el dispositivo, no se ha suministrado ning\u00fan host o token.", "not_xiaomi_miio": "El dispositivo no es (todav\u00eda) compatible con Xiaomi Miio.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" @@ -10,13 +10,13 @@ "error": { "cannot_connect": "No se pudo conectar", "cloud_credentials_incomplete": "Las credenciales de la nube est\u00e1n incompletas, por favor, rellene el nombre de usuario, la contrase\u00f1a y el pa\u00eds", - "cloud_login_error": "No se ha podido iniciar sesi\u00f3n en Xioami Miio Cloud, comprueba las credenciales.", + "cloud_login_error": "No se ha podido iniciar sesi\u00f3n en Xiaomi Miio Cloud, comprueba las credenciales.", "cloud_no_devices": "No se han encontrado dispositivos en esta cuenta de Xiaomi Miio.", "no_device_selected": "No se ha seleccionado ning\u00fan dispositivo, por favor, seleccione un dispositivo.", "unknown_device": "No se conoce el modelo del dispositivo, no se puede configurar el dispositivo mediante el flujo de configuraci\u00f3n.", "wrong_token": "Error de suma de comprobaci\u00f3n, token err\u00f3neo" }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "cloud": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/select.sk.json b/homeassistant/components/xiaomi_miio/translations/select.sk.json new file mode 100644 index 00000000000..8745c700fe6 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.sk.json @@ -0,0 +1,7 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "off": "Vypnut\u00e9" + } + } +} \ 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 4df58fda1b7..e9c24aab7f2 100644 --- a/homeassistant/components/yale_smart_alarm/translations/es.json +++ b/homeassistant/components/yale_smart_alarm/translations/es.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "La cuenta ya ha sido configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "reauth_successful": "La reautenticaci\u00f3n fue exitosa" }, "error": { + "cannot_connect": "Fall\u00f3 la conexi\u00f3n", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { diff --git a/homeassistant/components/yale_smart_alarm/translations/sk.json b/homeassistant/components/yale_smart_alarm/translations/sk.json index 00dddc88d1d..6130380de31 100644 --- a/homeassistant/components/yale_smart_alarm/translations/sk.json +++ b/homeassistant/components/yale_smart_alarm/translations/sk.json @@ -9,7 +9,8 @@ "step": { "reauth_confirm": { "data": { - "name": "N\u00e1zov" + "name": "N\u00e1zov", + "password": "Heslo" } }, "user": { diff --git a/homeassistant/components/yamaha_musiccast/translations/sk.json b/homeassistant/components/yamaha_musiccast/translations/sk.json new file mode 100644 index 00000000000..f74ba4b46d2 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/sk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie je u\u017e nakonfigurovan\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 36ab27c53b5..d8162401f3d 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "No se pudo conectar" }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "\u00bfQuieres configurar {name} ?" diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json index 1002d0ad0b3..dfe8c1de296 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -59,6 +59,10 @@ }, "usb_confirm": { "description": "\u00bfQuieres configurar {name} con el complemento Z-Wave JS?" + }, + "zeroconf_confirm": { + "description": "\u00bfQuieres a\u00f1adir el servidor Z-Wave JS con ID {home_id} que se encuentra en {url} en Home Assistant?", + "title": "Servidor Z-Wave JS descubierto" } } }, diff --git a/homeassistant/components/zwave_me/translations/es.json b/homeassistant/components/zwave_me/translations/es.json index eab23bbd0ff..2443547e59d 100644 --- a/homeassistant/components/zwave_me/translations/es.json +++ b/homeassistant/components/zwave_me/translations/es.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "token": "Token", + "token": "Token API", "url": "URL" }, "description": "Direcci\u00f3n IP de entrada del servidor Z-Way y token de acceso Z-Way. La direcci\u00f3n IP se puede prefijar con wss:// si se debe usar HTTPS en lugar de HTTP. Para obtener el token, vaya a la interfaz de usuario de Z-Way > Configuraci\u00f3n de > de men\u00fa > token de API de > de usuario. Se sugiere crear un nuevo usuario para Home Assistant y conceder acceso a los dispositivos que necesita controlar desde Home Assistant. Tambi\u00e9n es posible utilizar el acceso remoto a trav\u00e9s de find.z-wave.me para conectar un Z-Way remoto. Ingrese wss://find.z-wave.me en el campo IP y copie el token con alcance global (inicie sesi\u00f3n en Z-Way a trav\u00e9s de find.z-wave.me para esto)." From 089eb9960a835be6e0f336b644d2b62f5c2feaad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 May 2022 22:48:57 -0500 Subject: [PATCH 0530/3516] Reduce logbook websocket payload size and parse json attributes via the DBM (#71895) --- homeassistant/components/logbook/__init__.py | 45 +++++++++++--------- homeassistant/components/logbook/queries.py | 38 ++++++++++++++--- homeassistant/components/recorder/models.py | 3 ++ tests/components/logbook/test_init.py | 8 ++-- 4 files changed, 64 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 4bef1f1a23d..35fa17bb3a4 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -40,7 +40,6 @@ from homeassistant.const import ( ATTR_SERVICE, EVENT_CALL_SERVICE, EVENT_LOGBOOK_ENTRY, - EVENT_STATE_CHANGED, ) from homeassistant.core import ( Context, @@ -65,14 +64,12 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util -from .queries import statement_for_request +from .queries import PSUEDO_EVENT_STATE_CHANGED, statement_for_request _LOGGER = logging.getLogger(__name__) -FRIENDLY_NAME_JSON_EXTRACT = re.compile('"friendly_name": ?"([^"]+)"') ENTITY_ID_JSON_EXTRACT = re.compile('"entity_id": ?"([^"]+)"') DOMAIN_JSON_EXTRACT = re.compile('"domain": ?"([^"]+)"') -ICON_JSON_EXTRACT = re.compile('"icon": ?"([^"]+)"') ATTR_MESSAGE = "message" DOMAIN = "logbook" @@ -235,6 +232,7 @@ def _ws_formatted_get_events( entities_filter, context_id, True, + False, ), ) ) @@ -368,6 +366,7 @@ class LogbookView(HomeAssistantView): self.entities_filter, context_id, False, + True, ) ) @@ -385,6 +384,7 @@ def _humanify( ], entity_name_cache: EntityNameCache, format_time: Callable[[Row], Any], + include_entity_name: bool = True, ) -> Generator[dict[str, Any], None, None]: """Generate a converted list of events into entries.""" # Continuous sensors, will be excluded from the logbook @@ -419,13 +419,13 @@ def _humanify( continue event_type = row.event_type if event_type == EVENT_CALL_SERVICE or ( - event_type != EVENT_STATE_CHANGED + event_type is not PSUEDO_EVENT_STATE_CHANGED and entities_filter is not None and not _keep_row(row, event_type) ): continue - if event_type == EVENT_STATE_CHANGED: + if event_type is PSUEDO_EVENT_STATE_CHANGED: entity_id = row.entity_id assert entity_id is not None # Skip continuous sensors @@ -439,14 +439,15 @@ def _humanify( data = { LOGBOOK_ENTRY_WHEN: format_time(row), - LOGBOOK_ENTRY_NAME: entity_name_cache.get(entity_id, row), LOGBOOK_ENTRY_STATE: row.state, LOGBOOK_ENTRY_ENTITY_ID: entity_id, } - if icon := _row_attributes_extract(row, ICON_JSON_EXTRACT): + if include_entity_name: + data[LOGBOOK_ENTRY_NAME] = entity_name_cache.get(entity_id, row) + if icon := row.icon or row.old_format_icon: data[LOGBOOK_ENTRY_ICON] = icon - context_augmenter.augment(data, row, context_id) + context_augmenter.augment(data, row, context_id, include_entity_name) yield data elif event_type in external_events: @@ -454,7 +455,7 @@ def _humanify( data = describe_event(event_cache.get(row)) data[LOGBOOK_ENTRY_WHEN] = format_time(row) data[LOGBOOK_ENTRY_DOMAIN] = domain - context_augmenter.augment(data, row, context_id) + context_augmenter.augment(data, row, context_id, include_entity_name) yield data elif event_type == EVENT_LOGBOOK_ENTRY: @@ -474,7 +475,7 @@ def _humanify( LOGBOOK_ENTRY_DOMAIN: entry_domain, LOGBOOK_ENTRY_ENTITY_ID: entry_entity_id, } - context_augmenter.augment(data, row, context_id) + context_augmenter.augment(data, row, context_id, include_entity_name) yield data @@ -487,6 +488,7 @@ def _get_events( entities_filter: EntityFilter | Callable[[str], bool] | None = None, context_id: str | None = None, timestamp: bool = False, + include_entity_name: bool = True, ) -> list[dict[str, Any]]: """Get events for a period of time.""" assert not ( @@ -540,6 +542,7 @@ def _get_events( external_events, entity_name_cache, format_time, + include_entity_name, ) ) @@ -562,7 +565,9 @@ class ContextAugmenter: self.external_events = external_events self.event_cache = event_cache - def augment(self, data: dict[str, Any], row: Row, context_id: str) -> None: + def augment( + self, data: dict[str, Any], row: Row, context_id: str, include_entity_name: bool + ) -> None: """Augment data from the row and cache.""" if context_user_id := row.context_user_id: data[CONTEXT_USER_ID] = context_user_id @@ -589,9 +594,10 @@ class ContextAugmenter: # State change if context_entity_id := context_row.entity_id: data[CONTEXT_ENTITY_ID] = context_entity_id - data[CONTEXT_ENTITY_ID_NAME] = self.entity_name_cache.get( - context_entity_id, context_row - ) + if include_entity_name: + data[CONTEXT_ENTITY_ID_NAME] = self.entity_name_cache.get( + context_entity_id, context_row + ) data[CONTEXT_EVENT_TYPE] = event_type return @@ -619,9 +625,10 @@ class ContextAugmenter: if not (attr_entity_id := described.get(ATTR_ENTITY_ID)): return data[CONTEXT_ENTITY_ID] = attr_entity_id - data[CONTEXT_ENTITY_ID_NAME] = self.entity_name_cache.get( - attr_entity_id, context_row - ) + if include_entity_name: + data[CONTEXT_ENTITY_ID_NAME] = self.entity_name_cache.get( + attr_entity_id, context_row + ) def _is_sensor_continuous(ent_reg: er.EntityRegistry, entity_id: str) -> bool: @@ -735,8 +742,6 @@ class EntityNameCache: friendly_name := current_state.attributes.get(ATTR_FRIENDLY_NAME) ): self._names[entity_id] = friendly_name - elif extracted_name := _row_attributes_extract(row, FRIENDLY_NAME_JSON_EXTRACT): - self._names[entity_id] = extracted_name else: return split_entity_id(entity_id)[1].replace("_", " ") diff --git a/homeassistant/components/logbook/queries.py b/homeassistant/components/logbook/queries.py index 89c530aec43..6fe20bfc561 100644 --- a/homeassistant/components/logbook/queries.py +++ b/homeassistant/components/logbook/queries.py @@ -5,7 +5,7 @@ from collections.abc import Iterable from datetime import datetime as dt import sqlalchemy -from sqlalchemy import lambda_stmt, select, union_all +from sqlalchemy import JSON, lambda_stmt, select, type_coerce, union_all from sqlalchemy.orm import Query, aliased from sqlalchemy.sql.elements import ClauseList from sqlalchemy.sql.expression import literal @@ -16,6 +16,7 @@ from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN from homeassistant.components.recorder.filters import Filters from homeassistant.components.recorder.models import ( ENTITY_ID_LAST_UPDATED_INDEX, + JSON_VARIENT_CAST, LAST_UPDATED_INDEX, EventData, Events, @@ -23,7 +24,6 @@ from homeassistant.components.recorder.models import ( States, ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import EVENT_STATE_CHANGED ENTITY_ID_JSON_TEMPLATE = '%"entity_id":"{}"%' @@ -36,6 +36,22 @@ UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%" OLD_STATE = aliased(States, name="old_state") +SHARED_ATTRS_JSON = type_coerce( + StateAttributes.shared_attrs.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) +) +OLD_FORMAT_ATTRS_JSON = type_coerce( + States.attributes.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) +) + + +PSUEDO_EVENT_STATE_CHANGED = None +# Since we don't store event_types and None +# and we don't store state_changed in events +# we use a NULL for state_changed events +# when we synthesize them from the states table +# since it avoids another column being sent +# in the payload + EVENT_COLUMNS = ( Events.event_id.label("event_id"), Events.event_type.label("event_type"), @@ -50,18 +66,20 @@ STATE_COLUMNS = ( States.state_id.label("state_id"), States.state.label("state"), States.entity_id.label("entity_id"), - States.attributes.label("attributes"), - StateAttributes.shared_attrs.label("shared_attrs"), + SHARED_ATTRS_JSON["icon"].as_string().label("icon"), + OLD_FORMAT_ATTRS_JSON["icon"].as_string().label("old_format_icon"), ) + EMPTY_STATE_COLUMNS = ( literal(value=None, type_=sqlalchemy.String).label("state_id"), literal(value=None, type_=sqlalchemy.String).label("state"), literal(value=None, type_=sqlalchemy.String).label("entity_id"), - literal(value=None, type_=sqlalchemy.Text).label("attributes"), - literal(value=None, type_=sqlalchemy.Text).label("shared_attrs"), + literal(value=None, type_=sqlalchemy.String).label("icon"), + literal(value=None, type_=sqlalchemy.String).label("old_format_icon"), ) + EVENT_ROWS_NO_STATES = ( *EVENT_COLUMNS, EventData.shared_data.label("shared_data"), @@ -326,7 +344,13 @@ def _select_states() -> Select: """Generate a states select that formats the states table as event rows.""" return select( literal(value=None, type_=sqlalchemy.Text).label("event_id"), - literal(value=EVENT_STATE_CHANGED, type_=sqlalchemy.String).label("event_type"), + # We use PSUEDO_EVENT_STATE_CHANGED aka None for + # state_changed events since it takes up less + # space in the response and every row has to be + # marked with the event_type + literal(value=PSUEDO_EVENT_STATE_CHANGED, type_=sqlalchemy.String).label( + "event_type" + ), literal(value=None, type_=sqlalchemy.Text).label("event_data"), States.last_updated.label("time_fired"), States.context_id.label("context_id"), diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index d64d85f3ce4..f5498e941d3 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -102,6 +102,9 @@ class FAST_PYSQLITE_DATETIME(sqlite.DATETIME): # type: ignore[misc] return lambda value: None if value is None else ciso8601.parse_datetime(value) +JSON_VARIENT_CAST = Text().with_variant( + postgresql.JSON(none_as_null=True), "postgresql" +) DATETIME_TYPE = ( DateTime(timezone=True) .with_variant(mysql.DATETIME(timezone=True, fsp=6), "mysql") diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index a515afdf16e..ed95b4d10bc 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -30,7 +30,6 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, - EVENT_STATE_CHANGED, STATE_OFF, STATE_ON, ) @@ -327,7 +326,7 @@ def create_state_changed_event_from_old_new( ], ) - row.event_type = EVENT_STATE_CHANGED + row.event_type = logbook.PSUEDO_EVENT_STATE_CHANGED row.event_data = "{}" row.shared_data = "{}" row.attributes = attributes_json @@ -338,6 +337,9 @@ def create_state_changed_event_from_old_new( row.domain = entity_id and ha.split_entity_id(entity_id)[0] row.context_only = False row.context_id = None + row.friendly_name = None + row.icon = None + row.old_format_icon = None row.context_user_id = None row.context_parent_id = None row.old_state_id = old_state and 1 @@ -719,7 +721,7 @@ async def test_logbook_entity_no_longer_in_state_machine( ) assert response.status == HTTPStatus.OK json_dict = await response.json() - assert json_dict[0]["name"] == "Alarm Control Panel" + assert json_dict[0]["name"] == "area 001" async def test_filter_continuous_sensor_values( From a70e2a33dcd85608f1145d8fc2e89a87620f4ef3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 May 2022 23:25:07 -0500 Subject: [PATCH 0531/3516] Fixing purging legacy rows and improve performance (#71916) --- homeassistant/components/recorder/models.py | 10 +- homeassistant/components/recorder/purge.py | 289 ++++++++++++++---- homeassistant/components/recorder/queries.py | 19 ++ tests/components/recorder/test_purge.py | 293 +++++++++++++++++-- 4 files changed, 530 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index f5498e941d3..d7bb59bdeb1 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -130,10 +130,10 @@ class Events(Base): # type: ignore[misc,valid-type] {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, ) __tablename__ = TABLE_EVENTS - event_id = Column(Integer, Identity(), primary_key=True) # no longer used + event_id = Column(Integer, Identity(), primary_key=True) event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) - origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) # no longer used + origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) # no longer used for new rows origin_idx = Column(SmallInteger) time_fired = Column(DATETIME_TYPE, index=True) context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) @@ -247,8 +247,10 @@ class States(Base): # type: ignore[misc,valid-type] state_id = Column(Integer, Identity(), primary_key=True) entity_id = Column(String(MAX_LENGTH_STATE_ENTITY_ID)) state = Column(String(MAX_LENGTH_STATE_STATE)) - attributes = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) - event_id = Column( + attributes = Column( + Text().with_variant(mysql.LONGTEXT, "mysql") + ) # no longer used for new rows + event_id = Column( # no longer used for new rows Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True ) last_changed = Column(DATETIME_TYPE) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index f94f3d3d641..432e3993add 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -3,9 +3,10 @@ from __future__ import annotations from collections.abc import Callable, Iterable from datetime import datetime -from itertools import zip_longest +from functools import partial +from itertools import islice, zip_longest import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import distinct @@ -29,6 +30,8 @@ from .queries import ( disconnect_states_rows, find_events_to_purge, find_latest_statistics_runs_run_id, + find_legacy_event_state_and_attributes_and_data_ids_to_purge, + find_legacy_row, find_short_term_statistics_to_purge, find_states_to_purge, find_statistics_runs_to_purge, @@ -42,9 +45,34 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) +DEFAULT_STATES_BATCHES_PER_PURGE = 20 # We expect ~95% de-dupe rate +DEFAULT_EVENTS_BATCHES_PER_PURGE = 15 # We expect ~92% de-dupe rate + + +def take(take_num: int, iterable: Iterable) -> list[Any]: + """Return first n items of the iterable as a list. + + From itertools recipes + """ + return list(islice(iterable, take_num)) + + +def chunked(iterable: Iterable, chunked_num: int) -> Iterable[Any]: + """Break *iterable* into lists of length *n*. + + From more-itertools + """ + return iter(partial(take, chunked_num, iter(iterable)), []) + + @retryable_database_job("purge") def purge_old_data( - instance: Recorder, purge_before: datetime, repack: bool, apply_filter: bool = False + instance: Recorder, + purge_before: datetime, + repack: bool, + apply_filter: bool = False, + events_batch_size: int = DEFAULT_EVENTS_BATCHES_PER_PURGE, + states_batch_size: int = DEFAULT_STATES_BATCHES_PER_PURGE, ) -> bool: """Purge events and states older than purge_before. @@ -58,40 +86,37 @@ def purge_old_data( with session_scope(session=instance.get_session()) as session: # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record - ( - event_ids, - state_ids, - attributes_ids, - data_ids, - ) = _select_event_state_attributes_ids_data_ids_to_purge(session, purge_before) + has_more_to_purge = False + if _purging_legacy_format(session): + _LOGGER.debug( + "Purge running in legacy format as there are states with event_id remaining" + ) + has_more_to_purge |= _purge_legacy_format( + instance, session, purge_before, using_sqlite + ) + else: + _LOGGER.debug( + "Purge running in new format as there are NO states with event_id remaining" + ) + # Once we are done purging legacy rows, we use the new method + has_more_to_purge |= _purge_states_and_attributes_ids( + instance, session, states_batch_size, purge_before, using_sqlite + ) + has_more_to_purge |= _purge_events_and_data_ids( + instance, session, events_batch_size, purge_before, using_sqlite + ) + 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 unused_attribute_ids_set := _select_unused_attributes_ids( - session, attributes_ids, using_sqlite - ): - _purge_attributes_ids(instance, session, unused_attribute_ids_set) - - if event_ids: - _purge_event_ids(session, event_ids) - - if unused_data_ids_set := _select_unused_event_data_ids( - session, data_ids, using_sqlite - ): - _purge_event_data_ids(instance, session, unused_data_ids_set) - if statistics_runs: _purge_statistics_runs(session, statistics_runs) if short_term_statistics: _purge_short_term_statistics(session, short_term_statistics) - if state_ids or event_ids or statistics_runs or short_term_statistics: + if has_more_to_purge 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 @@ -106,27 +131,132 @@ def purge_old_data( return True -def _select_event_state_attributes_ids_data_ids_to_purge( +def _purging_legacy_format(session: Session) -> bool: + """Check if there are any legacy event_id linked states rows remaining.""" + return bool(session.execute(find_legacy_row()).scalar()) + + +def _purge_legacy_format( + instance: Recorder, session: Session, purge_before: datetime, using_sqlite: bool +) -> bool: + """Purge rows that are still linked by the event_ids.""" + ( + event_ids, + state_ids, + attributes_ids, + data_ids, + ) = _select_legacy_event_state_and_attributes_and_data_ids_to_purge( + session, purge_before + ) + if state_ids: + _purge_state_ids(instance, session, state_ids) + _purge_unused_attributes_ids(instance, session, attributes_ids, using_sqlite) + if event_ids: + _purge_event_ids(session, event_ids) + _purge_unused_data_ids(instance, session, data_ids, using_sqlite) + return bool(event_ids or state_ids or attributes_ids or data_ids) + + +def _purge_states_and_attributes_ids( + instance: Recorder, + session: Session, + states_batch_size: int, + purge_before: datetime, + using_sqlite: bool, +) -> bool: + """Purge states and linked attributes id in a batch. + + Returns true if there are more states to purge. + """ + has_remaining_state_ids_to_purge = True + # There are more states relative to attributes_ids so + # we purge enough state_ids to try to generate a full + # size batch of attributes_ids that will be around the size + # MAX_ROWS_TO_PURGE + attributes_ids_batch: set[int] = set() + for _ in range(states_batch_size): + state_ids, attributes_ids = _select_state_attributes_ids_to_purge( + session, purge_before + ) + if not state_ids: + has_remaining_state_ids_to_purge = False + break + _purge_state_ids(instance, session, state_ids) + attributes_ids_batch = attributes_ids_batch | attributes_ids + + _purge_unused_attributes_ids(instance, session, attributes_ids_batch, using_sqlite) + _LOGGER.debug( + "After purging states and attributes_ids remaining=%s", + has_remaining_state_ids_to_purge, + ) + return has_remaining_state_ids_to_purge + + +def _purge_events_and_data_ids( + instance: Recorder, + session: Session, + events_batch_size: int, + purge_before: datetime, + using_sqlite: bool, +) -> bool: + """Purge states and linked attributes id in a batch. + + Returns true if there are more states to purge. + """ + has_remaining_event_ids_to_purge = True + # There are more events relative to data_ids so + # we purge enough event_ids to try to generate a full + # size batch of data_ids that will be around the size + # MAX_ROWS_TO_PURGE + data_ids_batch: set[int] = set() + for _ in range(events_batch_size): + event_ids, data_ids = _select_event_data_ids_to_purge(session, purge_before) + if not event_ids: + has_remaining_event_ids_to_purge = False + break + _purge_event_ids(session, event_ids) + data_ids_batch = data_ids_batch | data_ids + + _purge_unused_data_ids(instance, session, data_ids_batch, using_sqlite) + _LOGGER.debug( + "After purging event and data_ids remaining=%s", + has_remaining_event_ids_to_purge, + ) + return has_remaining_event_ids_to_purge + + +def _select_state_attributes_ids_to_purge( session: Session, purge_before: datetime -) -> tuple[set[int], set[int], set[int], set[int]]: - """Return a list of event, state, and attribute ids to purge.""" - events = session.execute(find_events_to_purge(purge_before)).all() - _LOGGER.debug("Selected %s event ids to remove", len(events)) - states = session.execute(find_states_to_purge(purge_before)).all() - _LOGGER.debug("Selected %s state ids to remove", len(states)) - event_ids = set() +) -> tuple[set[int], set[int]]: + """Return sets of state and attribute ids to purge.""" state_ids = set() attributes_ids = set() - data_ids = set() - for event in events: - event_ids.add(event.event_id) - if event.data_id: - data_ids.add(event.data_id) - for state in states: + for state in session.execute(find_states_to_purge(purge_before)).all(): state_ids.add(state.state_id) if state.attributes_id: attributes_ids.add(state.attributes_id) - return event_ids, state_ids, attributes_ids, data_ids + _LOGGER.debug( + "Selected %s state ids and %s attributes_ids to remove", + len(state_ids), + len(attributes_ids), + ) + return state_ids, attributes_ids + + +def _select_event_data_ids_to_purge( + session: Session, purge_before: datetime +) -> tuple[set[int], set[int]]: + """Return sets of event and data ids to purge.""" + event_ids = set() + data_ids = set() + for event in session.execute(find_events_to_purge(purge_before)).all(): + event_ids.add(event.event_id) + if event.data_id: + data_ids.add(event.data_id) + _LOGGER.debug( + "Selected %s event ids and %s data_ids to remove", len(event_ids), len(data_ids) + ) + return event_ids, data_ids def _select_unused_attributes_ids( @@ -197,6 +327,18 @@ def _select_unused_attributes_ids( return to_remove +def _purge_unused_attributes_ids( + instance: Recorder, + session: Session, + attributes_ids_batch: set[int], + using_sqlite: bool, +) -> None: + if unused_attribute_ids_set := _select_unused_attributes_ids( + session, attributes_ids_batch, using_sqlite + ): + _purge_batch_attributes_ids(instance, session, unused_attribute_ids_set) + + def _select_unused_event_data_ids( session: Session, data_ids: set[int], using_sqlite: bool ) -> set[int]: @@ -229,6 +371,16 @@ def _select_unused_event_data_ids( return to_remove +def _purge_unused_data_ids( + instance: Recorder, session: Session, data_ids_batch: set[int], using_sqlite: bool +) -> None: + + if unused_data_ids_set := _select_unused_event_data_ids( + session, data_ids_batch, using_sqlite + ): + _purge_batch_data_ids(instance, session, unused_data_ids_set) + + def _select_statistics_runs_to_purge( session: Session, purge_before: datetime ) -> list[int]: @@ -256,6 +408,34 @@ def _select_short_term_statistics_to_purge( return [statistic.id for statistic in statistics] +def _select_legacy_event_state_and_attributes_and_data_ids_to_purge( + session: Session, purge_before: datetime +) -> tuple[set[int], set[int], set[int], set[int]]: + """Return a list of event, state, and attribute ids to purge that are linked by the event_id. + + We do not link these anymore since state_change events + do not exist in the events table anymore, however we + still need to be able to purge them. + """ + events = session.execute( + find_legacy_event_state_and_attributes_and_data_ids_to_purge(purge_before) + ).all() + _LOGGER.debug("Selected %s event ids to remove", len(events)) + event_ids = set() + state_ids = set() + attributes_ids = set() + data_ids = set() + for event in events: + event_ids.add(event.event_id) + if event.state_id: + state_ids.add(event.state_id) + if event.attributes_id: + attributes_ids.add(event.attributes_id) + if event.data_id: + data_ids.add(event.data_id) + return event_ids, state_ids, attributes_ids, data_ids + + def _purge_state_ids(instance: Recorder, session: Session, state_ids: set[int]) -> None: """Disconnect states and delete by state id.""" @@ -327,24 +507,27 @@ def _evict_purged_attributes_from_attributes_cache( ) -def _purge_attributes_ids( +def _purge_batch_attributes_ids( instance: Recorder, session: Session, attributes_ids: set[int] ) -> None: - """Delete old attributes ids.""" - deleted_rows = session.execute(delete_states_attributes_rows(attributes_ids)) - _LOGGER.debug("Deleted %s attribute states", deleted_rows) + """Delete old attributes ids in batches of MAX_ROWS_TO_PURGE.""" + for attributes_ids_chunk in chunked(attributes_ids, MAX_ROWS_TO_PURGE): + deleted_rows = session.execute( + delete_states_attributes_rows(attributes_ids_chunk) + ) + _LOGGER.debug("Deleted %s attribute states", deleted_rows) # Evict any entries in the state_attributes_ids cache referring to a purged state _evict_purged_attributes_from_attributes_cache(instance, attributes_ids) -def _purge_event_data_ids( +def _purge_batch_data_ids( instance: Recorder, session: Session, data_ids: set[int] ) -> None: - """Delete old event data ids.""" - - deleted_rows = session.execute(delete_event_data_rows(data_ids)) - _LOGGER.debug("Deleted %s data events", deleted_rows) + """Delete old event data ids in batches of MAX_ROWS_TO_PURGE.""" + for data_ids_chunk in chunked(data_ids, MAX_ROWS_TO_PURGE): + deleted_rows = session.execute(delete_event_data_rows(data_ids_chunk)) + _LOGGER.debug("Deleted %s data events", deleted_rows) # Evict any entries in the event_data_ids cache referring to a purged state _evict_purged_data_from_data_cache(instance, data_ids) @@ -438,7 +621,7 @@ def _purge_filtered_states( unused_attribute_ids_set = _select_unused_attributes_ids( session, {id_ for id_ in attributes_ids if id_ is not None}, using_sqlite ) - _purge_attributes_ids(instance, session, unused_attribute_ids_set) + _purge_batch_attributes_ids(instance, session, unused_attribute_ids_set) def _purge_filtered_events( @@ -466,7 +649,7 @@ def _purge_filtered_events( if unused_data_ids_set := _select_unused_event_data_ids( session, set(data_ids), using_sqlite ): - _purge_event_data_ids(instance, session, unused_data_ids_set) + _purge_batch_data_ids(instance, session, unused_data_ids_set) if EVENT_STATE_CHANGED in excluded_event_types: session.query(StateAttributes).delete(synchronize_session=False) instance._state_attributes_ids = {} # pylint: disable=protected-access diff --git a/homeassistant/components/recorder/queries.py b/homeassistant/components/recorder/queries.py index 76098663bd7..5532c5c0703 100644 --- a/homeassistant/components/recorder/queries.py +++ b/homeassistant/components/recorder/queries.py @@ -621,3 +621,22 @@ def find_statistics_runs_to_purge( def find_latest_statistics_runs_run_id() -> StatementLambdaElement: """Find the latest statistics_runs run_id.""" return lambda_stmt(lambda: select(func.max(StatisticsRuns.run_id))) + + +def find_legacy_event_state_and_attributes_and_data_ids_to_purge( + purge_before: datetime, +) -> StatementLambdaElement: + """Find the latest row in the legacy format to purge.""" + return lambda_stmt( + lambda: select( + Events.event_id, Events.data_id, States.state_id, States.attributes_id + ) + .join(States, Events.event_id == States.event_id) + .filter(Events.time_fired < purge_before) + .limit(MAX_ROWS_TO_PURGE) + ) + + +def find_legacy_row() -> StatementLambdaElement: + """Check if there are still states in the table with an event_id.""" + return lambda_stmt(lambda: select(func.max(States.event_id))) diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index de9db9d5014..c8ba5e9d076 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -11,6 +11,7 @@ from sqlalchemy.orm.session import Session from homeassistant.components import recorder from homeassistant.components.recorder.const import MAX_ROWS_TO_PURGE, SupportedDialect from homeassistant.components.recorder.models import ( + EventData, Events, RecorderRuns, StateAttributes, @@ -76,7 +77,13 @@ async def test_purge_old_states( purge_before = dt_util.utcnow() - timedelta(days=4) # run purge_old_data() - finished = purge_old_data(instance, purge_before, repack=False) + finished = purge_old_data( + instance, + purge_before, + states_batch_size=1, + events_batch_size=1, + repack=False, + ) assert not finished assert states.count() == 2 assert state_attributes.count() == 1 @@ -96,7 +103,13 @@ async def test_purge_old_states( # run purge_old_data again purge_before = dt_util.utcnow() - finished = purge_old_data(instance, purge_before, repack=False) + finished = purge_old_data( + instance, + purge_before, + states_batch_size=1, + events_batch_size=1, + repack=False, + ) assert not finished assert states.count() == 0 assert state_attributes.count() == 0 @@ -223,12 +236,24 @@ async def test_purge_old_events( purge_before = dt_util.utcnow() - timedelta(days=4) # run purge_old_data() - finished = purge_old_data(instance, purge_before, repack=False) + finished = purge_old_data( + instance, + purge_before, + repack=False, + events_batch_size=1, + states_batch_size=1, + ) assert not finished assert events.count() == 2 # we should only have 2 events left - finished = purge_old_data(instance, purge_before, repack=False) + finished = purge_old_data( + instance, + purge_before, + repack=False, + events_batch_size=1, + states_batch_size=1, + ) assert finished assert events.count() == 2 @@ -249,10 +274,22 @@ async def test_purge_old_recorder_runs( purge_before = dt_util.utcnow() # run purge_old_data() - finished = purge_old_data(instance, purge_before, repack=False) + finished = purge_old_data( + instance, + purge_before, + repack=False, + events_batch_size=1, + states_batch_size=1, + ) assert not finished - finished = purge_old_data(instance, purge_before, repack=False) + finished = purge_old_data( + instance, + purge_before, + repack=False, + events_batch_size=1, + states_batch_size=1, + ) assert finished assert recorder_runs.count() == 1 @@ -1271,7 +1308,7 @@ async def _add_test_states(hass: HomeAssistant): await set_state("test.recorder2", state, attributes=attributes) -async def _add_test_events(hass: HomeAssistant): +async def _add_test_events(hass: HomeAssistant, iterations: int = 1): """Add a few events for testing.""" utcnow = dt_util.utcnow() five_days_ago = utcnow - timedelta(days=5) @@ -1282,25 +1319,64 @@ async def _add_test_events(hass: HomeAssistant): await async_wait_recording_done(hass) with session_scope(hass=hass) as session: - for event_id in range(6): - if event_id < 2: - timestamp = eleven_days_ago - event_type = "EVENT_TEST_AUTOPURGE" - elif event_id < 4: - timestamp = five_days_ago - event_type = "EVENT_TEST_PURGE" - else: - timestamp = utcnow - event_type = "EVENT_TEST" + for _ in range(iterations): + for event_id in range(6): + if event_id < 2: + timestamp = eleven_days_ago + event_type = "EVENT_TEST_AUTOPURGE" + elif event_id < 4: + timestamp = five_days_ago + event_type = "EVENT_TEST_PURGE" + else: + timestamp = utcnow + event_type = "EVENT_TEST" - session.add( - Events( - event_type=event_type, - event_data=json.dumps(event_data), - origin="LOCAL", - time_fired=timestamp, + session.add( + Events( + event_type=event_type, + event_data=json.dumps(event_data), + origin="LOCAL", + time_fired=timestamp, + ) + ) + + +async def _add_events_with_event_data(hass: HomeAssistant, iterations: int = 1): + """Add a few events with linked event_data for testing.""" + utcnow = dt_util.utcnow() + five_days_ago = utcnow - timedelta(days=5) + eleven_days_ago = utcnow - timedelta(days=11) + event_data = {"test_attr": 5, "test_attr_10": "nice"} + + await hass.async_block_till_done() + await async_wait_recording_done(hass) + + with session_scope(hass=hass) as session: + for _ in range(iterations): + for event_id in range(6): + if event_id < 2: + timestamp = eleven_days_ago + event_type = "EVENT_TEST_AUTOPURGE_WITH_EVENT_DATA" + shared_data = '{"type":{"EVENT_TEST_AUTOPURGE_WITH_EVENT_DATA"}' + elif event_id < 4: + timestamp = five_days_ago + event_type = "EVENT_TEST_PURGE_WITH_EVENT_DATA" + shared_data = '{"type":{"EVENT_TEST_PURGE_WITH_EVENT_DATA"}' + else: + timestamp = utcnow + event_type = "EVENT_TEST_WITH_EVENT_DATA" + shared_data = '{"type":{"EVENT_TEST_WITH_EVENT_DATA"}' + + event_data = EventData(hash=1234, shared_data=shared_data) + + session.add( + Events( + event_type=event_type, + origin="LOCAL", + time_fired=timestamp, + event_data_rel=event_data, + ) ) - ) async def _add_test_statistics(hass: HomeAssistant): @@ -1384,6 +1460,29 @@ async def _add_test_statistics_runs(hass: HomeAssistant): ) +def _add_state_without_event_linkage( + session: Session, + entity_id: str, + state: str, + timestamp: datetime, +): + state_attrs = StateAttributes( + hash=1234, shared_attrs=json.dumps({entity_id: entity_id}) + ) + session.add(state_attrs) + session.add( + States( + entity_id=entity_id, + state=state, + attributes=None, + last_changed=timestamp, + last_updated=timestamp, + event_id=None, + state_attributes=state_attrs, + ) + ) + + def _add_state_and_state_changed_event( session: Session, entity_id: str, @@ -1416,3 +1515,149 @@ def _add_state_and_state_changed_event( time_fired=timestamp, ) ) + + +async def test_purge_many_old_events( + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT +): + """Test deleting old events.""" + instance = await async_setup_recorder_instance(hass) + + await _add_test_events(hass, MAX_ROWS_TO_PURGE) + + with session_scope(hass=hass) as session: + events = session.query(Events).filter(Events.event_type.like("EVENT_TEST%")) + event_datas = session.query(EventData) + assert events.count() == MAX_ROWS_TO_PURGE * 6 + assert event_datas.count() == 5 + + purge_before = dt_util.utcnow() - timedelta(days=4) + + # run purge_old_data() + finished = purge_old_data( + instance, + purge_before, + repack=False, + states_batch_size=3, + events_batch_size=3, + ) + assert not finished + assert events.count() == MAX_ROWS_TO_PURGE * 3 + assert event_datas.count() == 5 + + # we should only have 2 groups of events left + finished = purge_old_data( + instance, + purge_before, + repack=False, + states_batch_size=3, + events_batch_size=3, + ) + assert finished + assert events.count() == MAX_ROWS_TO_PURGE * 2 + assert event_datas.count() == 5 + + # we should now purge everything + finished = purge_old_data( + instance, + dt_util.utcnow(), + repack=False, + states_batch_size=20, + events_batch_size=20, + ) + assert finished + assert events.count() == 0 + assert event_datas.count() == 0 + + +async def test_purge_can_mix_legacy_and_new_format( + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT +): + """Test purging with legacy a new events.""" + instance = await async_setup_recorder_instance(hass) + utcnow = dt_util.utcnow() + eleven_days_ago = utcnow - timedelta(days=11) + with session_scope(hass=hass) as session: + broken_state_no_time = States( + event_id=None, + entity_id="orphened.state", + last_updated=None, + last_changed=None, + ) + session.add(broken_state_no_time) + start_id = 50000 + for event_id in range(start_id, start_id + 50): + _add_state_and_state_changed_event( + session, + "sensor.excluded", + "purgeme", + eleven_days_ago, + event_id, + ) + await _add_test_events(hass, 50) + await _add_events_with_event_data(hass, 50) + with session_scope(hass=hass) as session: + for _ in range(50): + _add_state_without_event_linkage( + session, "switch.random", "on", eleven_days_ago + ) + states_with_event_id = session.query(States).filter( + States.event_id.is_not(None) + ) + states_without_event_id = session.query(States).filter( + States.event_id.is_(None) + ) + + assert states_with_event_id.count() == 50 + assert states_without_event_id.count() == 51 + + purge_before = dt_util.utcnow() - timedelta(days=4) + finished = purge_old_data( + instance, + purge_before, + repack=False, + ) + assert not finished + assert states_with_event_id.count() == 0 + assert states_without_event_id.count() == 51 + # At this point all the legacy states are gone + # and we switch methods + purge_before = dt_util.utcnow() - timedelta(days=4) + finished = purge_old_data( + instance, + purge_before, + repack=False, + events_batch_size=1, + states_batch_size=1, + ) + # Since we only allow one iteration, we won't + # check if we are finished this loop similar + # to the legacy method + assert not finished + assert states_with_event_id.count() == 0 + assert states_without_event_id.count() == 1 + finished = purge_old_data( + instance, + purge_before, + repack=False, + events_batch_size=100, + states_batch_size=100, + ) + assert finished + assert states_with_event_id.count() == 0 + assert states_without_event_id.count() == 1 + _add_state_without_event_linkage( + session, "switch.random", "on", eleven_days_ago + ) + assert states_with_event_id.count() == 0 + assert states_without_event_id.count() == 2 + finished = purge_old_data( + instance, + purge_before, + repack=False, + ) + assert finished + # The broken state without a timestamp + # does not prevent future purges. Its ignored. + assert states_with_event_id.count() == 0 + assert states_without_event_id.count() == 1 From 2613c865f7720ede45ab287d4a9aff8a94517439 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 16 May 2022 00:31:14 -0700 Subject: [PATCH 0532/3516] Add Spotify application_credentials platform (#71871) --- homeassistant/components/spotify/__init__.py | 45 ++++++++++++------- .../spotify/application_credentials.py | 12 +++++ .../components/spotify/manifest.json | 2 +- .../generated/application_credentials.py | 1 + 4 files changed, 43 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/spotify/application_credentials.py diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index c057ea240c0..bf4e9d8deae 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta +import logging from typing import Any import aiohttp @@ -10,6 +11,10 @@ import requests from spotipy import Spotify, SpotifyException import voluptuous as vol +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CREDENTIALS, @@ -19,7 +24,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, async_get_config_entry_implementation, @@ -27,7 +32,6 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from . import config_flow from .browse_media import async_browse_media from .const import DOMAIN, LOGGER, SPOTIFY_SCOPES from .util import ( @@ -36,15 +40,20 @@ from .util import ( spotify_uri_from_media_browser_url, ) +_LOGGER = logging.getLogger(__name__) + CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Inclusive(CONF_CLIENT_ID, ATTR_CREDENTIALS): cv.string, - vol.Inclusive(CONF_CLIENT_SECRET, ATTR_CREDENTIALS): cv.string, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Inclusive(CONF_CLIENT_ID, ATTR_CREDENTIALS): cv.string, + vol.Inclusive(CONF_CLIENT_SECRET, ATTR_CREDENTIALS): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -76,17 +85,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True if CONF_CLIENT_ID in config[DOMAIN]: - config_flow.SpotifyFlowHandler.async_register_implementation( + await async_import_client_credential( hass, - config_entry_oauth2_flow.LocalOAuth2Implementation( - hass, - DOMAIN, + DOMAIN, + ClientCredential( config[DOMAIN][CONF_CLIENT_ID], config[DOMAIN][CONF_CLIENT_SECRET], - "https://accounts.spotify.com/authorize", - "https://accounts.spotify.com/api/token", ), ) + _LOGGER.warning( + "Configuration of Spotify integration in YAML is deprecated and " + "will be removed in a future release; Your existing OAuth " + "Application Credentials have been imported into the UI " + "automatically and can be safely removed from your " + "configuration.yaml file" + ) return True diff --git a/homeassistant/components/spotify/application_credentials.py b/homeassistant/components/spotify/application_credentials.py new file mode 100644 index 00000000000..203028ba7d2 --- /dev/null +++ b/homeassistant/components/spotify/application_credentials.py @@ -0,0 +1,12 @@ +"""Application credentials platform for spotify.""" + +from homeassistant.components.application_credentials import AuthorizationServer +from homeassistant.core import HomeAssistant + + +async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: + """Return authorization server.""" + return AuthorizationServer( + authorize_url="https://accounts.spotify.com/authorize", + token_url="https://accounts.spotify.com/api/token", + ) diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index dd18a05bdc7..979f262e54a 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/spotify", "requirements": ["spotipy==2.19.0"], "zeroconf": ["_spotify-connect._tcp.local."], - "dependencies": ["auth"], + "dependencies": ["application_credentials"], "codeowners": ["@frenck"], "config_flow": true, "quality_scale": "silver", diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index 3bde9e4681f..3e283dcfeee 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -7,5 +7,6 @@ To update, run python3 -m script.hassfest APPLICATION_CREDENTIALS = [ "google", + "spotify", "xbox" ] From a32321aa9f335b9f1e880d5312882f6e55895f02 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 16 May 2022 00:35:29 -0700 Subject: [PATCH 0533/3516] Mark xbox configuration.yaml as deprecated after app creds import (#71908) --- homeassistant/components/xbox/__init__.py | 26 ++++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index 2b5772dd0ba..19bbec8bbf5 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -38,14 +38,17 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -71,6 +74,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: config[DOMAIN][CONF_CLIENT_ID], config[DOMAIN][CONF_CLIENT_SECRET] ), ) + _LOGGER.warning( + "Configuration of Xbox integration in YAML is deprecated and " + "will be removed in a future release; Your existing configuration " + "(including OAuth Application Credentials) has been imported into " + "the UI automatically and can be safely removed from your " + "configuration.yaml file" + ) return True From aa35b878844ba4077f598c4b083c72be4b3babc2 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 16 May 2022 00:37:36 -0700 Subject: [PATCH 0534/3516] Improve error handling for application credentials deletion (#71868) --- homeassistant/components/application_credentials/__init__.py | 5 ++++- tests/components/application_credentials/test_init.py | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/application_credentials/__init__.py b/homeassistant/components/application_credentials/__init__.py index 0dda775774f..9117a91c33d 100644 --- a/homeassistant/components/application_credentials/__init__.py +++ b/homeassistant/components/application_credentials/__init__.py @@ -17,6 +17,7 @@ from homeassistant.components import websocket_api from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DOMAIN, CONF_ID from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, config_entry_oauth2_flow import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import Store @@ -98,7 +99,9 @@ class ApplicationCredentialsStorageCollection(collection.StorageCollection): entries = self.hass.config_entries.async_entries(current[CONF_DOMAIN]) for entry in entries: if entry.data.get("auth_implementation") == item_id: - raise ValueError("Cannot delete credential in use by an integration") + raise HomeAssistantError( + f"Cannot delete credential in use by integration {entry.domain}" + ) await super().async_delete_item(item_id) diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py index b5f51a9b837..b62f8a0139c 100644 --- a/tests/components/application_credentials/test_init.py +++ b/tests/components/application_credentials/test_init.py @@ -445,6 +445,10 @@ async def test_config_flow( assert not resp.get("success") assert "error" in resp assert resp["error"].get("code") == "unknown_error" + assert ( + resp["error"].get("message") + == "Cannot delete credential in use by integration fake_integration" + ) async def test_config_flow_multiple_entries( From 7c68278482aa79817012af78f6cd49fd7fc51ffd Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 16 May 2022 00:57:25 -0700 Subject: [PATCH 0535/3516] Add application_credentials platform to geocaching integration (#71880) --- .../components/geocaching/__init__.py | 46 +------------------ .../geocaching/application_credentials.py | 14 ++++++ .../components/geocaching/manifest.json | 2 +- homeassistant/components/geocaching/oauth.py | 34 +++++++------- .../generated/application_credentials.py | 1 + .../components/geocaching/test_config_flow.py | 41 ++++++----------- 6 files changed, 48 insertions(+), 90 deletions(-) create mode 100644 homeassistant/components/geocaching/application_credentials.py diff --git a/homeassistant/components/geocaching/__init__.py b/homeassistant/components/geocaching/__init__.py index f04fbbd608f..430cbc9a8d0 100644 --- a/homeassistant/components/geocaching/__init__.py +++ b/homeassistant/components/geocaching/__init__.py @@ -1,61 +1,19 @@ """The Geocaching integration.""" -import voluptuous as vol -from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, async_get_config_entry_implementation, ) -from homeassistant.helpers.typing import ConfigType -from .config_flow import GeocachingFlowHandler from .const import DOMAIN from .coordinator import GeocachingDataUpdateCoordinator -from .oauth import GeocachingOAuth2Implementation - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) PLATFORMS = [Platform.SENSOR] -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Geocaching component.""" - if DOMAIN not in config: - return True - - GeocachingFlowHandler.async_register_implementation( - hass, - GeocachingOAuth2Implementation( - hass, - client_id=config[DOMAIN][CONF_CLIENT_ID], - client_secret=config[DOMAIN][CONF_CLIENT_SECRET], - name="Geocaching", - ), - ) - - # When manual configuration is done, discover the integration. - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_INTEGRATION_DISCOVERY} - ) - ) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Geocaching from a config entry.""" implementation = await async_get_config_entry_implementation(hass, entry) diff --git a/homeassistant/components/geocaching/application_credentials.py b/homeassistant/components/geocaching/application_credentials.py new file mode 100644 index 00000000000..e3d35f57a81 --- /dev/null +++ b/homeassistant/components/geocaching/application_credentials.py @@ -0,0 +1,14 @@ +"""application_credentials platform for Geocaching.""" + +from homeassistant.components.application_credentials import ClientCredential +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow + +from .oauth import GeocachingOAuth2Implementation + + +async def async_get_auth_implementation( + hass: HomeAssistant, auth_domain: str, credential: ClientCredential +) -> config_entry_oauth2_flow.AbstractOAuth2Implementation: + """Return auth implementation.""" + return GeocachingOAuth2Implementation(hass, auth_domain, credential) diff --git a/homeassistant/components/geocaching/manifest.json b/homeassistant/components/geocaching/manifest.json index 683a23d474a..59c3da45cf5 100644 --- a/homeassistant/components/geocaching/manifest.json +++ b/homeassistant/components/geocaching/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/geocaching", "requirements": ["geocachingapi==0.2.1"], - "dependencies": ["auth"], + "dependencies": ["application_credentials"], "codeowners": ["@Sholofly", "@reinder83"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/geocaching/oauth.py b/homeassistant/components/geocaching/oauth.py index 29371eb793c..e0120344cdb 100644 --- a/homeassistant/components/geocaching/oauth.py +++ b/homeassistant/components/geocaching/oauth.py @@ -3,37 +3,37 @@ from __future__ import annotations from typing import Any, cast +from homeassistant.components.application_credentials import ( + AuthImplementation, + AuthorizationServer, + ClientCredential, +) from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN, ENVIRONMENT, ENVIRONMENT_URLS +from .const import ENVIRONMENT, ENVIRONMENT_URLS -class GeocachingOAuth2Implementation( - config_entry_oauth2_flow.LocalOAuth2Implementation -): +class GeocachingOAuth2Implementation(AuthImplementation): """Local OAuth2 implementation for Geocaching.""" def __init__( - self, hass: HomeAssistant, client_id: str, client_secret: str, name: str + self, + hass: HomeAssistant, + auth_domain: str, + credential: ClientCredential, ) -> None: """Local Geocaching Oauth Implementation.""" - self._name = name super().__init__( hass=hass, - client_id=client_id, - client_secret=client_secret, - domain=DOMAIN, - authorize_url=ENVIRONMENT_URLS[ENVIRONMENT]["authorize_url"], - token_url=ENVIRONMENT_URLS[ENVIRONMENT]["token_url"], + auth_domain=auth_domain, + credential=credential, + authorization_server=AuthorizationServer( + authorize_url=ENVIRONMENT_URLS[ENVIRONMENT]["authorize_url"], + token_url=ENVIRONMENT_URLS[ENVIRONMENT]["token_url"], + ), ) - @property - def name(self) -> str: - """Name of the implementation.""" - return f"{self._name}" - @property def extra_authorize_data(self) -> dict: """Extra data that needs to be appended to the authorize url.""" diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index 3e283dcfeee..56340ccc44e 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -6,6 +6,7 @@ To update, run python3 -m script.hassfest # fmt: off APPLICATION_CREDENTIALS = [ + "geocaching", "google", "spotify", "xbox" diff --git a/tests/components/geocaching/test_config_flow.py b/tests/components/geocaching/test_config_flow.py index 0f5d182b2db..56a301e2f3c 100644 --- a/tests/components/geocaching/test_config_flow.py +++ b/tests/components/geocaching/test_config_flow.py @@ -4,19 +4,18 @@ from http import HTTPStatus from unittest.mock import MagicMock from aiohttp.test_utils import TestClient +import pytest +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.components.geocaching.const import ( DOMAIN, ENVIRONMENT, ENVIRONMENT_URLS, ) -from homeassistant.config_entries import ( - DEFAULT_DISCOVERY_UNIQUE_ID, - SOURCE_INTEGRATION_DISCOVERY, - SOURCE_REAUTH, - SOURCE_USER, -) -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_EXTERNAL_STEP from homeassistant.helpers import config_entry_oauth2_flow @@ -30,17 +29,14 @@ from tests.test_util.aiohttp import AiohttpClientMocker CURRENT_ENVIRONMENT_URLS = ENVIRONMENT_URLS[ENVIRONMENT] -async def setup_geocaching_component(hass: HomeAssistant) -> bool: - """Set up the Geocaching component.""" - return await async_setup_component( +@pytest.fixture(autouse=True) +async def setup_credentials(hass: HomeAssistant) -> None: + """Fixture to setup credentials.""" + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( hass, DOMAIN, - { - DOMAIN: { - CONF_CLIENT_ID: CLIENT_ID, - CONF_CLIENT_SECRET: CLIENT_SECRET, - }, - }, + ClientCredential(CLIENT_ID, CLIENT_SECRET), ) @@ -53,15 +49,6 @@ async def test_full_flow( mock_setup_entry: MagicMock, ) -> None: """Check full flow.""" - assert await setup_geocaching_component(hass) - - # Ensure integration is discovered when manual implementation is configured - flows = hass.config_entries.flow.async_progress() - assert len(flows) == 1 - assert "context" in flows[0] - assert flows[0]["context"]["source"] == SOURCE_INTEGRATION_DISCOVERY - assert flows[0]["context"]["unique_id"] == DEFAULT_DISCOVERY_UNIQUE_ID - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -113,9 +100,9 @@ async def test_existing_entry( mock_geocaching_config_flow: MagicMock, mock_setup_entry: MagicMock, mock_config_entry: MockConfigEntry, + setup_credentials: None, ) -> None: """Check existing entry.""" - assert await setup_geocaching_component(hass) mock_config_entry.add_to_hass(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -161,7 +148,6 @@ async def test_oauth_error( mock_setup_entry: MagicMock, ) -> None: """Check if aborted when oauth error occurs.""" - assert await setup_geocaching_component(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -213,7 +199,6 @@ async def test_reauthentication( ) -> None: """Test Geocaching reauthentication.""" mock_config_entry.add_to_hass(hass) - assert await setup_geocaching_component(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH} From 77d39f9c4f4b10bfab25eb850b5e26542d8cbcdc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 May 2022 10:07:39 +0200 Subject: [PATCH 0536/3516] Add missing title translation for the Siren domain (#71924) --- homeassistant/components/siren/strings.json | 3 +++ homeassistant/components/siren/translations/en.json | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 homeassistant/components/siren/strings.json create mode 100644 homeassistant/components/siren/translations/en.json diff --git a/homeassistant/components/siren/strings.json b/homeassistant/components/siren/strings.json new file mode 100644 index 00000000000..c8e60e91ce0 --- /dev/null +++ b/homeassistant/components/siren/strings.json @@ -0,0 +1,3 @@ +{ + "title": "Siren" +} diff --git a/homeassistant/components/siren/translations/en.json b/homeassistant/components/siren/translations/en.json new file mode 100644 index 00000000000..549bad8914b --- /dev/null +++ b/homeassistant/components/siren/translations/en.json @@ -0,0 +1,3 @@ +{ + "title": "Siren" +} \ No newline at end of file From e30a9d1fc015c6de92e71821d8ad36c1e3dcbcd5 Mon Sep 17 00:00:00 2001 From: Ethan Madden Date: Mon, 16 May 2022 01:30:49 -0700 Subject: [PATCH 0537/3516] Fix VeSync air_quality fan attribute (#71771) * Refactor attribute inclusion for VeSync fans. A recent change to pyvesync (introduced in 2.2) changed `air_quality` to refer to air quality as an integer representation of perceived air quality rather than a direct reading of the PM2.5 sensor. With 2.3 the PM2.5 sensor access was restored as `air_quality_value`. Unfortunately, `air_quality_value` was not added as an attribute on the fan object, and rather only exists in the `details` dictionary on the fan object. * Update homeassistant/components/vesync/fan.py Co-authored-by: Martin Hjelmare * Rename `air_quality_value` attribute to `pm25` This should make it more clear what the attribute actually represents * `air_quality` attribute reports `air_quality_value` This restores previous behavior for this integration to what it was before the `pyvesync==2.02` upgrade, using the `air_quality` attribute to report pm2.5 concentrations (formerly `air_quality`) rather the vague measurement now reported by `air_quality`. Co-authored-by: Martin Hjelmare --- homeassistant/components/vesync/fan.py | 4 ++-- homeassistant/components/vesync/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index e37a6c8893e..f16a785ee1e 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -171,8 +171,8 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): if hasattr(self.smartfan, "night_light"): attr["night_light"] = self.smartfan.night_light - if hasattr(self.smartfan, "air_quality"): - attr["air_quality"] = self.smartfan.air_quality + if self.smartfan.details.get("air_quality_value") is not None: + attr["air_quality"] = self.smartfan.details["air_quality_value"] if hasattr(self.smartfan, "mode"): attr["mode"] = self.smartfan.mode diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json index c93e070a484..49be473b748 100644 --- a/homeassistant/components/vesync/manifest.json +++ b/homeassistant/components/vesync/manifest.json @@ -3,7 +3,7 @@ "name": "VeSync", "documentation": "https://www.home-assistant.io/integrations/vesync", "codeowners": ["@markperdue", "@webdjoe", "@thegardenmonkey"], - "requirements": ["pyvesync==2.0.2"], + "requirements": ["pyvesync==2.0.3"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["pyvesync"] diff --git a/requirements_all.txt b/requirements_all.txt index 5b35a595083..b0281677308 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1999,7 +1999,7 @@ pyvera==0.3.13 pyversasense==0.0.6 # homeassistant.components.vesync -pyvesync==2.0.2 +pyvesync==2.0.3 # homeassistant.components.vizio pyvizio==0.1.57 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 888cd30a056..c56bf1aeb98 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1322,7 +1322,7 @@ pyuptimerobot==22.2.0 pyvera==0.3.13 # homeassistant.components.vesync -pyvesync==2.0.2 +pyvesync==2.0.3 # homeassistant.components.vizio pyvizio==0.1.57 From f7a2a6ea216f4beeca4e03064217609794503419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 16 May 2022 12:26:54 +0200 Subject: [PATCH 0538/3516] Bump awesomeversion from 22.2.0 to 22.5.1 (#71933) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ec3ac5de9b3..d2f88767d0c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ async-upnp-client==0.29.0 async_timeout==4.0.2 atomicwrites==1.4.0 attrs==21.2.0 -awesomeversion==22.2.0 +awesomeversion==22.5.1 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements.txt b/requirements.txt index 2c96c00fdb3..0cd6c1fc6ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ astral==2.2 async_timeout==4.0.2 attrs==21.2.0 atomicwrites==1.4.0 -awesomeversion==22.2.0 +awesomeversion==22.5.1 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/setup.cfg b/setup.cfg index 47bf617755a..e14915c4998 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,7 +36,7 @@ install_requires = async_timeout==4.0.2 attrs==21.2.0 atomicwrites==1.4.0 - awesomeversion==22.2.0 + awesomeversion==22.5.1 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 From 0ee838b25e850204fc4df971c2520f8e725b96b4 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 16 May 2022 12:50:03 +0200 Subject: [PATCH 0539/3516] Properly handle Shelly gen2 device disconnect (#71937) --- 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 d29584c4e83..41a9e68fbdd 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -812,7 +812,7 @@ class RpcPollingWrapper(update_coordinator.DataUpdateCoordinator): LOGGER.debug("Polling Shelly RPC Device - %s", self.name) async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): await self.device.update_status() - except OSError as err: + except (OSError, aioshelly.exceptions.RPCTimeout) as err: raise update_coordinator.UpdateFailed("Device disconnected") from err @property From 732082b7700cf52f04c6841d3ac058c3c6a88688 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 May 2022 12:54:24 +0200 Subject: [PATCH 0540/3516] Update apprise to 0.9.8.3 (#71934) --- homeassistant/components/apprise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index 21e2ed7c94d..b4422a49ef4 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -2,7 +2,7 @@ "domain": "apprise", "name": "Apprise", "documentation": "https://www.home-assistant.io/integrations/apprise", - "requirements": ["apprise==0.9.7"], + "requirements": ["apprise==0.9.8.3"], "codeowners": ["@caronc"], "iot_class": "cloud_push", "loggers": ["apprise"] diff --git a/requirements_all.txt b/requirements_all.txt index b0281677308..1fef7fae54b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -313,7 +313,7 @@ anthemav==1.2.0 apcaccess==0.0.13 # homeassistant.components.apprise -apprise==0.9.7 +apprise==0.9.8.3 # homeassistant.components.aprs aprslib==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c56bf1aeb98..a20ea91d92e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -267,7 +267,7 @@ ambiclimate==0.2.1 androidtv[async]==0.0.67 # homeassistant.components.apprise -apprise==0.9.7 +apprise==0.9.8.3 # homeassistant.components.aprs aprslib==0.7.0 From fcd9fcffe6c96b31990f143c574531d02d34058e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 May 2022 12:55:21 +0200 Subject: [PATCH 0541/3516] Update watchdog to 2.1.8 (#71927) --- 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 918492c82e9..f7562633ba0 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.7"], + "requirements": ["watchdog==2.1.8"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 1fef7fae54b..fa529f22320 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2418,7 +2418,7 @@ wallbox==0.4.4 waqiasync==1.0.0 # homeassistant.components.folder_watcher -watchdog==2.1.7 +watchdog==2.1.8 # homeassistant.components.waterfurnace waterfurnace==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a20ea91d92e..c8b37a3c73d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1588,7 +1588,7 @@ wakeonlan==2.0.1 wallbox==0.4.4 # homeassistant.components.folder_watcher -watchdog==2.1.7 +watchdog==2.1.8 # homeassistant.components.whirlpool whirlpool-sixth-sense==0.15.1 From ace2f18697b7254f81dfcc1891989a367e1ba756 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 May 2022 13:58:54 +0200 Subject: [PATCH 0542/3516] Update pyupgrade to v2.32.1 (#71939) --- .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 4a012441a6b..539308c08f1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.32.0 + rev: v2.32.1 hooks: - id: pyupgrade args: [--py39-plus] diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index bed60c11c53..047dcbf90ad 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -12,5 +12,5 @@ mccabe==0.6.1 pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 -pyupgrade==2.32.0 +pyupgrade==2.32.1 yamllint==1.26.3 From a90289905803b4d88d38651fac4961c6605ba995 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 May 2022 14:44:07 +0200 Subject: [PATCH 0543/3516] Remove auto_start translation from HomeKit (#71938) --- homeassistant/components/homekit/strings.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json index d2b6951a698..2c0fad0290e 100644 --- a/homeassistant/components/homekit/strings.json +++ b/homeassistant/components/homekit/strings.json @@ -44,8 +44,7 @@ }, "advanced": { "data": { - "devices": "Devices (Triggers)", - "auto_start": "Autostart (disable if you are calling the homekit.start service manually)" + "devices": "Devices (Triggers)" }, "description": "Programmable switches are created for each selected device. When a device trigger fires, HomeKit can be configured to run an automation or scene.", "title": "Advanced Configuration" From 2d7723169ae463d7cfe9052a0a6f05de857ca56e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 16 May 2022 15:01:31 +0200 Subject: [PATCH 0544/3516] Update pylint to 2.13.9 (#71941) * Update pylint to 2.13.9 * Small change --- homeassistant/components/sms/config_flow.py | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sms/config_flow.py b/homeassistant/components/sms/config_flow.py index acc33075397..9128b6187c1 100644 --- a/homeassistant/components/sms/config_flow.py +++ b/homeassistant/components/sms/config_flow.py @@ -39,7 +39,7 @@ async def get_imei_from_config(hass: core.HomeAssistant, data): raise CannotConnect try: imei = await gateway.get_imei_async() - except gammu.GSMError as err: # pylint: disable=no-member + except gammu.GSMError as err: raise CannotConnect from err finally: await gateway.terminate_async() diff --git a/requirements_test.txt b/requirements_test.txt index 49615d104a7..f0cdac24e5f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ freezegun==1.2.1 mock-open==1.4.0 mypy==0.950 pre-commit==2.19.0 -pylint==2.13.8 +pylint==2.13.9 pipdeptree==2.2.1 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 From e74794d50020b8c54c4a791755ef4f21bdb9bdb4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 May 2022 09:10:18 -0500 Subject: [PATCH 0545/3516] Add sensor platform to Big Ass Fans (#71877) --- .coveragerc | 1 + homeassistant/components/baf/__init__.py | 2 +- homeassistant/components/baf/sensor.py | 132 +++++++++++++++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/baf/sensor.py diff --git a/.coveragerc b/.coveragerc index 15b9544dad5..12aa3972f1a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -96,6 +96,7 @@ omit = homeassistant/components/baf/__init__.py homeassistant/components/baf/entity.py homeassistant/components/baf/fan.py + homeassistant/components/baf/sensor.py homeassistant/components/baidu/tts.py homeassistant/components/balboa/__init__.py homeassistant/components/beewi_smartclim/sensor.py diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 4327eb9ddb9..f6401cbbc40 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -14,7 +14,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN, QUERY_INTERVAL, RUN_TIMEOUT from .models import BAFData -PLATFORMS: list[Platform] = [Platform.FAN] +PLATFORMS: list[Platform] = [Platform.FAN, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/baf/sensor.py b/homeassistant/components/baf/sensor.py new file mode 100644 index 00000000000..0f4239962cf --- /dev/null +++ b/homeassistant/components/baf/sensor.py @@ -0,0 +1,132 @@ +"""Support for Big Ass Fans sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Optional, cast + +from aiobafi6 import Device + +from homeassistant import config_entries +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import BAFEntity +from .models import BAFData + + +@dataclass +class BAFSensorDescriptionMixin: + """Required values for BAF sensors.""" + + value_fn: Callable[[Device], int | float | str | None] + + +@dataclass +class BAFSensorDescription( + SensorEntityDescription, + BAFSensorDescriptionMixin, +): + """Class describing BAF sensor entities.""" + + +BASE_SENSORS = ( + BAFSensorDescription( + key="temperature", + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda device: cast(Optional[float], device.temperature), + ), +) + +DEFINED_ONLY_SENSORS = ( + BAFSensorDescription( + key="humidity", + name="Humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda device: cast(Optional[float], device.humidity), + ), +) + +FAN_SENSORS = ( + BAFSensorDescription( + key="current_rpm", + name="Current RPM", + native_unit_of_measurement="RPM", + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: cast(Optional[int], device.current_rpm), + ), + BAFSensorDescription( + key="target_rpm", + name="Target RPM", + native_unit_of_measurement="RPM", + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: cast(Optional[int], device.target_rpm), + ), + BAFSensorDescription( + key="wifi_ssid", + name="WiFi SSID", + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: cast(Optional[int], device.wifi_ssid), + ), + BAFSensorDescription( + key="ip_address", + name="IP Address", + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: cast(Optional[str], device.ip_address), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up BAF fan sensors.""" + data: BAFData = hass.data[DOMAIN][entry.entry_id] + device = data.device + sensors_descriptions = list(BASE_SENSORS) + for description in DEFINED_ONLY_SENSORS: + if getattr(device, description.key): + sensors_descriptions.append(description) + if device.has_fan: + sensors_descriptions.extend(FAN_SENSORS) + async_add_entities( + BAFSensor(device, description) for description in sensors_descriptions + ) + + +class BAFSensor(BAFEntity, SensorEntity): + """BAF sensor.""" + + entity_description: BAFSensorDescription + + def __init__(self, device: Device, description: BAFSensorDescription) -> None: + """Initialize the entity.""" + self.entity_description = description + super().__init__(device, f"{device.name} {description.name}") + self._attr_unique_id = f"{self._device.mac_address}-{description.key}" + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + description = self.entity_description + self._attr_native_value = description.value_fn(self._device) From 514e7708b3e9d7bcacec8dc2f279f161854d5e8f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 May 2022 16:38:01 +0200 Subject: [PATCH 0546/3516] Update PyJWT to 2.4.0 (#71928) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d2f88767d0c..b9d08a07591 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,4 +1,4 @@ -PyJWT==2.3.0 +PyJWT==2.4.0 PyNaCl==1.5.0 aiodiscover==1.4.11 aiohttp==3.8.1 diff --git a/requirements.txt b/requirements.txt index 0cd6c1fc6ba..f2308908a30 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ ciso8601==2.2.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.2 -PyJWT==2.3.0 +PyJWT==2.4.0 cryptography==36.0.2 pip>=21.0,<22.1 python-slugify==4.0.1 diff --git a/setup.cfg b/setup.cfg index e14915c4998..9d4fad881b6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,7 +45,7 @@ install_requires = httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.2 - PyJWT==2.3.0 + PyJWT==2.4.0 # PyJWT has loose dependency. We want the latest one. cryptography==36.0.2 pip>=21.0,<22.1 From 5d32659d17f4a42f995a67ab647dfe1838f3ddca Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 16 May 2022 08:11:09 -0700 Subject: [PATCH 0547/3516] Update scaffold script to use application_credentials platform (#71881) --- script/scaffold/generate.py | 2 +- .../integration/__init__.py | 49 ++----------------- .../integration/application_credentials.py | 16 ++++++ .../tests/test_config_flow.py | 31 ++++++++---- 4 files changed, 42 insertions(+), 56 deletions(-) create mode 100644 script/scaffold/templates/config_flow_oauth2/integration/application_credentials.py diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index a86c4e3a015..7f418868463 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -173,7 +173,7 @@ def _custom_tasks(template, info: Info) -> None: ) elif template == "config_flow_oauth2": - info.update_manifest(config_flow=True, dependencies=["auth"]) + info.update_manifest(config_flow=True, dependencies=["application_credentials"]) info.update_strings( config={ "step": { diff --git a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py index b580e609bba..24dcee48ccd 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/__init__.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/__init__.py @@ -1,60 +1,19 @@ """The NEW_NAME integration.""" from __future__ import annotations -import voluptuous as vol - from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import ( - aiohttp_client, - config_entry_oauth2_flow, - config_validation as cv, -) -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow -from . import api, config_flow -from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) +from . import api +from .const import DOMAIN # TODO List the platforms that you want to support. # For your initial PR, limit it to 1 platform. PLATFORMS: list[Platform] = [Platform.LIGHT] -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the NEW_NAME component.""" - hass.data[DOMAIN] = {} - - if DOMAIN not in config: - return True - - config_flow.OAuth2FlowHandler.async_register_implementation( - hass, - config_entry_oauth2_flow.LocalOAuth2Implementation( - hass, - DOMAIN, - config[DOMAIN][CONF_CLIENT_ID], - config[DOMAIN][CONF_CLIENT_SECRET], - OAUTH2_AUTHORIZE, - OAUTH2_TOKEN, - ), - ) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up NEW_NAME from a config entry.""" implementation = ( diff --git a/script/scaffold/templates/config_flow_oauth2/integration/application_credentials.py b/script/scaffold/templates/config_flow_oauth2/integration/application_credentials.py new file mode 100644 index 00000000000..51ef70b1885 --- /dev/null +++ b/script/scaffold/templates/config_flow_oauth2/integration/application_credentials.py @@ -0,0 +1,16 @@ +"""application_credentials platform the NEW_NAME integration.""" + +from homeassistant.components.application_credentials import AuthorizationServer +from homeassistant.core import HomeAssistant + +# TODO Update with your own urls +OAUTH2_AUTHORIZE = "https://www.example.com/auth/authorize" +OAUTH2_TOKEN = "https://www.example.com/auth/token" + + +async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: + """Return authorization server.""" + return AuthorizationServer( + authorize_url=OAUTH2_AUTHORIZE, + token_url=OAUTH2_TOKEN, + ) diff --git a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py index 3ed8d9d293f..bc087119c6e 100644 --- a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py @@ -1,35 +1,46 @@ """Test the NEW_NAME config flow.""" + from unittest.mock import patch -from homeassistant import config_entries, setup +import pytest + +from homeassistant import config_entries from homeassistant.components.NEW_DOMAIN.const import ( DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN, ) +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.setup import async_setup_component CLIENT_ID = "1234" CLIENT_SECRET = "5678" +@pytest.fixture +async def setup_credentials(hass: HomeAssistant) -> None: + """Fixture to setup credentials.""" + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential(CLIENT_ID, CLIENT_SECRET), + ) + + async def test_full_flow( hass: HomeAssistant, hass_client_no_auth, aioclient_mock, current_request_with_host, + setup_credentials, ) -> None: """Check full flow.""" - assert await setup.async_setup_component( - hass, - "NEW_DOMAIN", - { - "NEW_DOMAIN": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, - "http": {"base_url": "https://example.com"}, - }, - ) - result = await hass.config_entries.flow.async_init( "NEW_DOMAIN", context={"source": config_entries.SOURCE_USER} ) From 32b3ce5727d2a63b167652330198d1b561d6b673 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 17 May 2022 00:06:54 +0800 Subject: [PATCH 0548/3516] Clean up use_wallclock_as_timestamps in generic (#71940) --- homeassistant/components/camera/__init__.py | 2 +- .../components/generic/config_flow.py | 20 ++++++++++++------- homeassistant/components/stream/__init__.py | 5 +++-- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index bff0a07a9be..31dede33e7a 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -454,7 +454,7 @@ class Camera(Entity): def __init__(self) -> None: """Initialize a camera.""" self.stream: Stream | None = None - self.stream_options: dict[str, str] = {} + self.stream_options: dict[str, str | bool] = {} self.content_type: str = DEFAULT_CONTENT_TYPE self.access_tokens: collections.deque = collections.deque([], 2) self._warned_old_signature = False diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 435fdf6f729..651ed87da39 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -21,6 +21,7 @@ from homeassistant.components.stream import ( CONF_USE_WALLCLOCK_AS_TIMESTAMPS, RTSP_TRANSPORTS, SOURCE_TIMEOUT, + convert_stream_options, ) from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import ( @@ -202,21 +203,23 @@ async def async_test_stream(hass, info) -> dict[str, str]: # homeassistant.components.stream.__init__.py:create_stream() # It may be possible & better to call create_stream() directly. stream_options: dict[str, bool | str] = {} - if isinstance(stream_source, str) and stream_source[:7] == "rtsp://": - stream_options = { - "rtsp_flags": "prefer_tcp", - "stimeout": "5000000", - } if rtsp_transport := info.get(CONF_RTSP_TRANSPORT): stream_options[CONF_RTSP_TRANSPORT] = rtsp_transport if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True + pyav_options = convert_stream_options(stream_options) + if isinstance(stream_source, str) and stream_source[:7] == "rtsp://": + pyav_options = { + "rtsp_flags": "prefer_tcp", + "stimeout": "5000000", + **pyav_options, + } _LOGGER.debug("Attempting to open stream %s", stream_source) container = await hass.async_add_executor_job( partial( av.open, stream_source, - options=stream_options, + options=pyav_options, timeout=SOURCE_TIMEOUT, ) ) @@ -369,7 +372,10 @@ class GenericOptionsFlowHandler(OptionsFlow): CONF_FRAMERATE: user_input[CONF_FRAMERATE], CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL], CONF_USE_WALLCLOCK_AS_TIMESTAMPS: user_input.get( - CONF_USE_WALLCLOCK_AS_TIMESTAMPS + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, + self.config_entry.options.get( + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, False + ), ), } return self.async_create_entry( diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 95e9afe6c36..895bdaf3201 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -90,7 +90,7 @@ def redact_credentials(data: str) -> str: def create_stream( hass: HomeAssistant, stream_source: str, - options: dict[str, Any], + options: dict[str, str | bool], stream_label: str | None = None, ) -> Stream: """Create a stream with the specified identfier based on the source url. @@ -496,7 +496,7 @@ STREAM_OPTIONS_SCHEMA: Final = vol.Schema( ) -def convert_stream_options(stream_options: dict[str, Any]) -> dict[str, str]: +def convert_stream_options(stream_options: dict[str, str | bool]) -> dict[str, str]: """Convert options from stream options into PyAV options.""" pyav_options: dict[str, str] = {} try: @@ -505,6 +505,7 @@ def convert_stream_options(stream_options: dict[str, Any]) -> dict[str, str]: raise HomeAssistantError("Invalid stream options") from exc if rtsp_transport := stream_options.get(CONF_RTSP_TRANSPORT): + assert isinstance(rtsp_transport, str) pyav_options["rtsp_transport"] = rtsp_transport if stream_options.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): pyav_options["use_wallclock_as_timestamps"] = "1" From 57c94c0350a5765bc6f93b396c5e53766f7f12eb Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 16 May 2022 13:04:32 -0400 Subject: [PATCH 0549/3516] Add additional configuration entities for ZHA lights (#70597) * Add more configuration entities for ZHA lights * fix typing circular imports * enhance filter in entity factory * fix read attribute chunking * add test * add exception test --- .../components/zha/core/channels/base.py | 2 +- .../components/zha/core/channels/general.py | 8 + homeassistant/components/zha/number.py | 153 +++++++++++++++ tests/components/zha/test_discover.py | 10 +- tests/components/zha/test_number.py | 175 +++++++++++++++++- 5 files changed, 341 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index 7beefe2f0d0..7ba28a52116 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -446,7 +446,7 @@ class ZigbeeChannel(LogMixin): try: self.debug("Reading attributes in chunks: %s", chunk) read, _ = await self.cluster.read_attributes( - attributes, + chunk, allow_cache=from_cache, only_cache=only_cache, manufacturer=manufacturer, diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 2e6093dd4f7..8b67c81db44 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -222,6 +222,14 @@ class LevelControlChannel(ZigbeeChannel): CURRENT_LEVEL = 0 REPORT_CONFIG = ({"attr": "current_level", "config": REPORT_CONFIG_ASAP},) + ZCL_INIT_ATTRS = { + "on_off_transition_time": True, + "on_level": True, + "on_transition_time": True, + "off_transition_time": True, + "default_move_rate": True, + "start_up_current_level": True, + } @property def current_level(self) -> int | None: diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index e1191b4ece4..22d086891ca 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -1,17 +1,25 @@ """Support for ZHA AnalogOutput cluster.""" +from __future__ import annotations + import functools import logging +from typing import TYPE_CHECKING + +import zigpy.exceptions +from zigpy.zcl.foundation import Status from homeassistant.components.number import NumberEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .core import discovery from .core.const import ( CHANNEL_ANALOG_OUTPUT, + CHANNEL_LEVEL, DATA_ZHA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, @@ -19,9 +27,16 @@ from .core.const import ( from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity +if TYPE_CHECKING: + from .core.channels.base import ZigbeeChannel + from .core.device import ZHADevice + _LOGGER = logging.getLogger(__name__) STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.NUMBER) +CONFIG_DIAGNOSTIC_MATCH = functools.partial( + ZHA_ENTITIES.config_diagnostic_match, Platform.NUMBER +) UNITS = { @@ -342,3 +357,141 @@ class ZhaNumber(ZhaEntity, NumberEntity): "present_value", from_cache=False ) _LOGGER.debug("read value=%s", value) + + +class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity): + """Representation of a ZHA number configuration entity.""" + + _attr_entity_category = EntityCategory.CONFIG + _attr_step: float = 1.0 + _zcl_attribute: str + + @classmethod + def create_entity( + cls, + unique_id: str, + zha_device: ZHADevice, + channels: list[ZigbeeChannel], + **kwargs, + ) -> ZhaEntity | None: + """Entity Factory. + + Return entity if it is a supported configuration, otherwise return None + """ + channel = channels[0] + if ( + cls._zcl_attribute in channel.cluster.unsupported_attributes + or channel.cluster.get(cls._zcl_attribute) is None + ): + _LOGGER.debug( + "%s is not supported - skipping %s entity creation", + cls._zcl_attribute, + cls.__name__, + ) + return None + + return cls(unique_id, zha_device, channels, **kwargs) + + def __init__( + self, + unique_id: str, + zha_device: ZHADevice, + channels: list[ZigbeeChannel], + **kwargs, + ) -> None: + """Init this number configuration entity.""" + self._channel: ZigbeeChannel = channels[0] + super().__init__(unique_id, zha_device, channels, **kwargs) + + @property + def value(self) -> float: + """Return the current value.""" + return self._channel.cluster.get(self._zcl_attribute) + + async def async_set_value(self, value: float) -> None: + """Update the current value from HA.""" + try: + res = await self._channel.cluster.write_attributes( + {self._zcl_attribute: int(value)} + ) + except zigpy.exceptions.ZigbeeException as ex: + self.error("Could not set value: %s", ex) + return + if not isinstance(res, Exception) and all( + record.status == Status.SUCCESS for record in res[0] + ): + self.async_write_ha_state() + + async def async_update(self) -> None: + """Attempt to retrieve the state of the entity.""" + await super().async_update() + _LOGGER.debug("polling current state") + if self._channel: + value = await self._channel.get_attribute_value( + self._zcl_attribute, from_cache=False + ) + _LOGGER.debug("read value=%s", value) + + +@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL) +class OnOffTransitionTimeConfigurationEntity( + ZHANumberConfigurationEntity, id_suffix="on_off_transition_time" +): + """Representation of a ZHA on off transition time configuration entity.""" + + _attr_min_value: float = 0x0000 + _attr_max_value: float = 0xFFFF + _zcl_attribute: str = "on_off_transition_time" + + +@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL) +class OnLevelConfigurationEntity(ZHANumberConfigurationEntity, id_suffix="on_level"): + """Representation of a ZHA on level configuration entity.""" + + _attr_min_value: float = 0x00 + _attr_max_value: float = 0xFF + _zcl_attribute: str = "on_level" + + +@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL) +class OnTransitionTimeConfigurationEntity( + ZHANumberConfigurationEntity, id_suffix="on_transition_time" +): + """Representation of a ZHA on transition time configuration entity.""" + + _attr_min_value: float = 0x0000 + _attr_max_value: float = 0xFFFE + _zcl_attribute: str = "on_transition_time" + + +@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL) +class OffTransitionTimeConfigurationEntity( + ZHANumberConfigurationEntity, id_suffix="off_transition_time" +): + """Representation of a ZHA off transition time configuration entity.""" + + _attr_min_value: float = 0x0000 + _attr_max_value: float = 0xFFFE + _zcl_attribute: str = "off_transition_time" + + +@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL) +class DefaultMoveRateConfigurationEntity( + ZHANumberConfigurationEntity, id_suffix="default_move_rate" +): + """Representation of a ZHA default move rate configuration entity.""" + + _attr_min_value: float = 0x00 + _attr_max_value: float = 0xFE + _zcl_attribute: str = "default_move_rate" + + +@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL) +class StartUpCurrentLevelConfigurationEntity( + ZHANumberConfigurationEntity, id_suffix="start_up_current_level" +): + """Representation of a ZHA startup current level configuration entity.""" + + _attr_min_value: float = 0x00 + _attr_max_value: float = 0xFF + _zcl_attribute: str = "start_up_current_level" diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index 149c77314a1..4de251fda8b 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -44,7 +44,15 @@ from .zha_devices_list import ( NO_TAIL_ID = re.compile("_\\d$") UNIQUE_ID_HD = re.compile(r"^(([\da-fA-F]{2}:){7}[\da-fA-F]{2}-\d{1,3})", re.X) -IGNORE_SUFFIXES = [zigpy.zcl.clusters.general.OnOff.StartUpOnOff.__name__] +IGNORE_SUFFIXES = [ + zigpy.zcl.clusters.general.OnOff.StartUpOnOff.__name__, + "on_off_transition_time", + "on_level", + "on_transition_time", + "off_transition_time", + "default_move_rate", + "start_up_current_level", +] def contains_ignored_suffix(unique_id: str) -> bool: diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index 336800f9ccb..01946c05f1a 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -2,13 +2,14 @@ from unittest.mock import call, patch import pytest -import zigpy.profiles.zha -import zigpy.types +from zigpy.exceptions import ZigbeeException +from zigpy.profiles import zha import zigpy.zcl.clusters.general as general import zigpy.zcl.foundation as zcl_f from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN -from homeassistant.const import STATE_UNAVAILABLE, Platform +from homeassistant.const import ENTITY_CATEGORY_CONFIG, STATE_UNAVAILABLE, Platform +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from .common import ( @@ -18,7 +19,7 @@ from .common import ( send_attributes_report, update_attribute_cache, ) -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 from tests.common import mock_coro @@ -29,7 +30,7 @@ def zigpy_analog_output_device(zigpy_device_mock): endpoints = { 1: { - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.LEVEL_CONTROL_SWITCH, + SIG_EP_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, SIG_EP_INPUT: [general.AnalogOutput.cluster_id, general.Basic.cluster_id], SIG_EP_OUTPUT: [], } @@ -37,6 +38,30 @@ def zigpy_analog_output_device(zigpy_device_mock): return zigpy_device_mock(endpoints) +@pytest.fixture +async def light(zigpy_device_mock): + """Siren fixture.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_PROFILE: zha.PROFILE_ID, + SIG_EP_TYPE: zha.DeviceType.ON_OFF_LIGHT, + SIG_EP_INPUT: [ + general.Basic.cluster_id, + general.Identify.cluster_id, + general.OnOff.cluster_id, + general.LevelControl.cluster_id, + ], + SIG_EP_OUTPUT: [general.Ota.cluster_id], + } + }, + node_descriptor=b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", + ) + + return zigpy_device + + async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_device): """Test zha number platform.""" @@ -139,3 +164,143 @@ async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_devi assert hass.states.get(entity_id).state == "40.0" assert cluster.read_attributes.call_count == 10 assert "present_value" in cluster.read_attributes.call_args[0][0] + + +@pytest.mark.parametrize( + "attr, initial_value, new_value", + ( + ("on_off_transition_time", 20, 5), + ("on_level", 255, 50), + ("on_transition_time", 5, 1), + ("off_transition_time", 5, 1), + ("default_move_rate", 1, 5), + ("start_up_current_level", 254, 125), + ), +) +async def test_level_control_number( + hass, light, zha_device_joined, attr, initial_value, new_value +): + """Test zha level control number entities - new join.""" + + entity_registry = er.async_get(hass) + level_control_cluster = light.endpoints[1].level + level_control_cluster.PLUGGED_ATTR_READS = { + attr: initial_value, + } + zha_device = await zha_device_joined(light) + + entity_id = await find_entity_id( + Platform.NUMBER, + zha_device, + hass, + qualifier=attr, + ) + assert entity_id is not None + + assert level_control_cluster.read_attributes.call_count == 3 + assert ( + call( + [ + "on_off_transition_time", + "on_level", + "on_transition_time", + "off_transition_time", + "default_move_rate", + ], + allow_cache=True, + only_cache=False, + manufacturer=None, + ) + in level_control_cluster.read_attributes.call_args_list + ) + + assert ( + call( + ["start_up_current_level"], + allow_cache=True, + only_cache=False, + manufacturer=None, + ) + in level_control_cluster.read_attributes.call_args_list + ) + + assert ( + call( + [ + "current_level", + ], + allow_cache=False, + only_cache=False, + manufacturer=None, + ) + in level_control_cluster.read_attributes.call_args_list + ) + + state = hass.states.get(entity_id) + assert state + assert state.state == str(initial_value) + + entity_entry = entity_registry.async_get(entity_id) + assert entity_entry + assert entity_entry.entity_category == ENTITY_CATEGORY_CONFIG + + # Test number set_value + await hass.services.async_call( + "number", + "set_value", + { + "entity_id": entity_id, + "value": new_value, + }, + blocking=True, + ) + + assert level_control_cluster.write_attributes.call_count == 1 + assert level_control_cluster.write_attributes.call_args[0][0] == { + attr: new_value, + } + + state = hass.states.get(entity_id) + assert state + assert state.state == str(new_value) + + level_control_cluster.read_attributes.reset_mock() + await async_setup_component(hass, "homeassistant", {}) + await hass.async_block_till_done() + + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + # the mocking doesn't update the attr cache so this flips back to initial value + assert hass.states.get(entity_id).state == str(initial_value) + assert level_control_cluster.read_attributes.call_count == 1 + assert ( + call( + [ + attr, + ], + allow_cache=False, + only_cache=False, + manufacturer=None, + ) + in level_control_cluster.read_attributes.call_args_list + ) + + level_control_cluster.write_attributes.reset_mock() + level_control_cluster.write_attributes.side_effect = ZigbeeException + + await hass.services.async_call( + "number", + "set_value", + { + "entity_id": entity_id, + "value": new_value, + }, + blocking=True, + ) + + assert level_control_cluster.write_attributes.call_count == 1 + assert level_control_cluster.write_attributes.call_args[0][0] == { + attr: new_value, + } + assert hass.states.get(entity_id).state == str(initial_value) From bac2dce5ee3b15e604ec8a165983f436c3014c0f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 May 2022 20:14:04 +0200 Subject: [PATCH 0550/3516] Update twentemilieu to 0.6.1 (#71953) --- homeassistant/components/twentemilieu/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/twentemilieu/manifest.json b/homeassistant/components/twentemilieu/manifest.json index e8e280dcd3d..c7dd2508fd8 100644 --- a/homeassistant/components/twentemilieu/manifest.json +++ b/homeassistant/components/twentemilieu/manifest.json @@ -3,7 +3,7 @@ "name": "Twente Milieu", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/twentemilieu", - "requirements": ["twentemilieu==0.6.0"], + "requirements": ["twentemilieu==0.6.1"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index fa529f22320..ede1f0cb075 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2337,7 +2337,7 @@ ttls==1.4.3 tuya-iot-py-sdk==0.6.6 # homeassistant.components.twentemilieu -twentemilieu==0.6.0 +twentemilieu==0.6.1 # homeassistant.components.twilio twilio==6.32.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8b37a3c73d..a1b49258def 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1525,7 +1525,7 @@ ttls==1.4.3 tuya-iot-py-sdk==0.6.6 # homeassistant.components.twentemilieu -twentemilieu==0.6.0 +twentemilieu==0.6.1 # homeassistant.components.twilio twilio==6.32.0 From fb7aead7567181087a23caa0221918bf6c8079d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 May 2022 14:15:04 -0500 Subject: [PATCH 0551/3516] Guard expensive `cast`s in performance sensitive spots with `if TYPE_CHECKING` (#71960) --- homeassistant/core.py | 44 +++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 061695ade6a..973a940ffbc 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -406,7 +406,12 @@ class HomeAssistant: if asyncio.iscoroutine(target): return self.async_create_task(target) - target = cast(Callable[..., Union[Coroutine[Any, Any, _R], _R]], target) + # This code path is performance sensitive and uses + # if TYPE_CHECKING to avoid the overhead of constructing + # the type used for the cast. For history see: + # https://github.com/home-assistant/core/pull/71960 + if TYPE_CHECKING: + target = cast(Callable[..., Union[Coroutine[Any, Any, _R], _R]], target) return self.async_add_hass_job(HassJob(target), *args) @overload @@ -434,17 +439,25 @@ class HomeAssistant: args: parameters for method to call. """ task: asyncio.Future[_R] + # This code path is performance sensitive and uses + # if TYPE_CHECKING to avoid the overhead of constructing + # the type used for the cast. For history see: + # https://github.com/home-assistant/core/pull/71960 if hassjob.job_type == HassJobType.Coroutinefunction: - task = self.loop.create_task( - cast(Callable[..., Coroutine[Any, Any, _R]], hassjob.target)(*args) - ) + if TYPE_CHECKING: + hassjob.target = cast( + Callable[..., Coroutine[Any, Any, _R]], hassjob.target + ) + task = self.loop.create_task(hassjob.target(*args)) elif hassjob.job_type == HassJobType.Callback: - self.loop.call_soon(cast(Callable[..., _R], hassjob.target), *args) + if TYPE_CHECKING: + hassjob.target = cast(Callable[..., _R], hassjob.target) + self.loop.call_soon(hassjob.target, *args) return None else: - task = self.loop.run_in_executor( - None, cast(Callable[..., _R], hassjob.target), *args - ) + if TYPE_CHECKING: + hassjob.target = cast(Callable[..., _R], hassjob.target) + task = self.loop.run_in_executor(None, hassjob.target, *args) # If a task is scheduled if self._track_task: @@ -522,8 +535,14 @@ class HomeAssistant: hassjob: HassJob args: parameters for method to call. """ + # This code path is performance sensitive and uses + # if TYPE_CHECKING to avoid the overhead of constructing + # the type used for the cast. For history see: + # https://github.com/home-assistant/core/pull/71960 if hassjob.job_type == HassJobType.Callback: - cast(Callable[..., _R], hassjob.target)(*args) + if TYPE_CHECKING: + hassjob.target = cast(Callable[..., _R], hassjob.target) + hassjob.target(*args) return None return self.async_add_hass_job(hassjob, *args) @@ -565,7 +584,12 @@ class HomeAssistant: if asyncio.iscoroutine(target): return self.async_create_task(target) - target = cast(Callable[..., Union[Coroutine[Any, Any, _R], _R]], target) + # This code path is performance sensitive and uses + # if TYPE_CHECKING to avoid the overhead of constructing + # the type used for the cast. For history see: + # https://github.com/home-assistant/core/pull/71960 + if TYPE_CHECKING: + target = cast(Callable[..., Union[Coroutine[Any, Any, _R], _R]], target) return self.async_run_hass_job(HassJob(target), *args) def block_till_done(self) -> None: From d22472208f34e4ee2c248cbae019d8f182c26ead Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 16 May 2022 21:41:43 +0200 Subject: [PATCH 0552/3516] Update frontend to 20220516.0 (#71964) --- 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 8c475d51abb..0040dece159 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220504.1"], + "requirements": ["home-assistant-frontend==20220516.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b9d08a07591..db6433c65b7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220504.1 +home-assistant-frontend==20220516.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index ede1f0cb075..2f6bd5cac3f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -822,7 +822,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220504.1 +home-assistant-frontend==20220516.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a1b49258def..752788718e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -589,7 +589,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220504.1 +home-assistant-frontend==20220516.0 # homeassistant.components.home_connect homeconnect==0.7.0 From 03e98a9a32f87590da803fcdc6c3898a3429c09b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 16 May 2022 22:13:43 +0200 Subject: [PATCH 0553/3516] Update sentry-sdk to 1.5.12 (#71930) * Update sentry-sdk to 1.5.12 * Remove now unneeded mypy ignore --- homeassistant/components/sentry/__init__.py | 2 +- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index c02b16fcfb0..ab3b80414dd 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -80,7 +80,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } # pylint: disable-next=abstract-class-instantiated - sentry_sdk.init( # type: ignore[abstract] + sentry_sdk.init( dsn=entry.data[CONF_DSN], environment=entry.options.get(CONF_ENVIRONMENT), integrations=[sentry_logging, AioHttpIntegration(), SqlalchemyIntegration()], diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 1096edd4e36..b76318f046d 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.5.10"], + "requirements": ["sentry-sdk==1.5.12"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 2f6bd5cac3f..e5a091ee65d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2141,7 +2141,7 @@ sendgrid==6.8.2 sense_energy==0.10.4 # homeassistant.components.sentry -sentry-sdk==1.5.10 +sentry-sdk==1.5.12 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 752788718e4..2623f133464 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1407,7 +1407,7 @@ securetar==2022.2.0 sense_energy==0.10.4 # homeassistant.components.sentry -sentry-sdk==1.5.10 +sentry-sdk==1.5.12 # homeassistant.components.sharkiq sharkiq==0.0.1 From 007c6d22366b39c166bc4c511a145382d8fd7551 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 17 May 2022 00:04:57 +0200 Subject: [PATCH 0554/3516] Streamline setup of deCONZ binary sensor platform (#71820) --- .../components/deconz/alarm_control_panel.py | 2 +- .../components/deconz/binary_sensor.py | 59 ++++++++++--------- homeassistant/components/deconz/climate.py | 4 +- homeassistant/components/deconz/cover.py | 2 +- .../components/deconz/deconz_event.py | 4 +- homeassistant/components/deconz/fan.py | 2 +- homeassistant/components/deconz/gateway.py | 45 ++++++++++++-- homeassistant/components/deconz/light.py | 2 +- homeassistant/components/deconz/lock.py | 4 +- homeassistant/components/deconz/number.py | 2 +- homeassistant/components/deconz/services.py | 1 + homeassistant/components/deconz/siren.py | 2 +- homeassistant/components/deconz/switch.py | 14 ++--- 13 files changed, 89 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py index a29c439123e..2e825dafd65 100644 --- a/homeassistant/components/deconz/alarm_control_panel.py +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -81,7 +81,7 @@ async def async_setup_entry( config_entry.async_on_unload( gateway.api.sensors.ancillary_control.subscribe( - async_add_sensor, + gateway.evaluate_add_device(async_add_sensor), EventType.ADDED, ) ) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index c3b16e509cb..e57f0bde3a6 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -5,6 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass from pydeconz.interfaces.sensors import SensorResources +from pydeconz.models.event import EventType from pydeconz.models.sensor.alarm import Alarm from pydeconz.models.sensor.carbon_monoxide import CarbonMonoxide from pydeconz.models.sensor.fire import Fire @@ -188,48 +189,48 @@ async def async_setup_entry( gateway.entities[DOMAIN] = set() @callback - def async_add_sensor(sensors: list[SensorResources] | None = None) -> None: - """Add binary sensor from deCONZ.""" - entities: list[DeconzBinarySensor] = [] + def async_add_sensor(_: EventType, sensor_id: str) -> None: + """Add sensor from deCONZ.""" + sensor = gateway.api.sensors[sensor_id] - if sensors is None: - sensors = gateway.api.sensors.values() + if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): + return - for sensor in sensors: - - if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): + for description in ( + ENTITY_DESCRIPTIONS.get(type(sensor), []) + BINARY_SENSOR_DESCRIPTIONS + ): + if ( + not hasattr(sensor, description.key) + or description.value_fn(sensor) is None + ): continue - known_entities = set(gateway.entities[DOMAIN]) - for description in ( - ENTITY_DESCRIPTIONS.get(type(sensor), []) + BINARY_SENSOR_DESCRIPTIONS - ): + async_add_entities([DeconzBinarySensor(sensor, gateway, description)]) - if ( - not hasattr(sensor, description.key) - or description.value_fn(sensor) is None - ): - continue + config_entry.async_on_unload( + gateway.api.sensors.subscribe( + gateway.evaluate_add_device(async_add_sensor), + EventType.ADDED, + ) + ) + for sensor_id in gateway.api.sensors: + async_add_sensor(EventType.ADDED, sensor_id) - new_sensor = DeconzBinarySensor(sensor, gateway, description) - if new_sensor.unique_id not in known_entities: - entities.append(new_sensor) - - if entities: - async_add_entities(entities) + @callback + def async_reload_clip_sensors() -> None: + """Load clip sensor sensors from deCONZ.""" + for sensor_id, sensor in gateway.api.sensors.items(): + if sensor.type.startswith("CLIP"): + async_add_sensor(EventType.ADDED, sensor_id) config_entry.async_on_unload( async_dispatcher_connect( hass, - gateway.signal_new_sensor, - async_add_sensor, + gateway.signal_reload_clip_sensors, + async_reload_clip_sensors, ) ) - async_add_sensor( - [gateway.api.sensors[key] for key in sorted(gateway.api.sensors, key=int)] - ) - class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): """Representation of a deCONZ binary sensor.""" diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index bdc366ff7db..1d57288d275 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -106,7 +106,7 @@ async def async_setup_entry( config_entry.async_on_unload( gateway.api.sensors.thermostat.subscribe( - async_add_climate, + gateway.evaluate_add_device(async_add_climate), EventType.ADDED, ) ) @@ -115,7 +115,7 @@ async def async_setup_entry( @callback def async_reload_clip_sensors() -> None: - """Load clip climate sensors from deCONZ.""" + """Load clip sensors from deCONZ.""" for climate_id, climate in gateway.api.sensors.thermostat.items(): if climate.type.startswith("CLIP"): async_add_climate(EventType.ADDED, climate_id) diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 3d5dabb73c1..1931ac78389 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -45,7 +45,7 @@ async def async_setup_entry( config_entry.async_on_unload( gateway.api.lights.covers.subscribe( - async_add_cover, + gateway.evaluate_add_device(async_add_cover), EventType.ADDED, ) ) diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 4f04aa34fe2..cfd60a63ec9 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -66,14 +66,14 @@ async def async_setup_events(gateway: DeconzGateway) -> None: gateway.config_entry.async_on_unload( gateway.api.sensors.ancillary_control.subscribe( - async_add_sensor, + gateway.evaluate_add_device(async_add_sensor), EventType.ADDED, ) ) gateway.config_entry.async_on_unload( gateway.api.sensors.switch.subscribe( - async_add_sensor, + gateway.evaluate_add_device(async_add_sensor), EventType.ADDED, ) ) diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index 93daae55ccf..b0d6d7755a2 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -50,7 +50,7 @@ async def async_setup_entry( config_entry.async_on_unload( gateway.api.lights.fans.subscribe( - async_add_fan, + gateway.evaluate_add_device(async_add_fan), EventType.ADDED, ) ) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 0e880fa22c1..d59c5e2d160 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from collections.abc import Callable from types import MappingProxyType from typing import TYPE_CHECKING, Any, cast @@ -10,6 +11,7 @@ import async_timeout from pydeconz import DeconzSession, errors from pydeconz.models import ResourceGroup from pydeconz.models.alarm_system import AlarmSystem as DeconzAlarmSystem +from pydeconz.models.event import EventType from pydeconz.models.group import Group as DeconzGroup from pydeconz.models.light import LightBase as DeconzLight from pydeconz.models.sensor import SensorBase as DeconzSensor @@ -76,10 +78,14 @@ class DeconzGateway: self.deconz_ids: dict[str, str] = {} self.entities: dict[str, set[str]] = {} self.events: list[DeconzAlarmEvent | DeconzEvent] = [] + self.ignored_devices: set[tuple[Callable[[EventType, str], None], str]] = set() self._option_allow_deconz_groups = self.config_entry.options.get( CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS ) + self.option_allow_new_devices = self.config_entry.options.get( + CONF_ALLOW_NEW_DEVICES, DEFAULT_ALLOW_NEW_DEVICES + ) @property def bridgeid(self) -> str: @@ -112,12 +118,31 @@ class DeconzGateway: CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS ) - @property - def option_allow_new_devices(self) -> bool: - """Allow automatic adding of new devices.""" - return self.config_entry.options.get( - CONF_ALLOW_NEW_DEVICES, DEFAULT_ALLOW_NEW_DEVICES - ) + @callback + def evaluate_add_device( + self, add_device_callback: Callable[[EventType, str], None] + ) -> Callable[[EventType, str], None]: + """Wrap add_device_callback to check allow_new_devices option.""" + + def async_add_device(event: EventType, device_id: str) -> None: + """Add device or add it to ignored_devices set. + + If ignore_state_updates is True means device_refresh service is used. + Device_refresh is expected to load new devices. + """ + if not self.option_allow_new_devices and not self.ignore_state_updates: + self.ignored_devices.add((add_device_callback, device_id)) + return + add_device_callback(event, device_id) + + return async_add_device + + @callback + def load_ignored_devices(self) -> None: + """Load previously ignored devices.""" + for add_entities, device_id in self.ignored_devices: + add_entities(EventType.ADDED, device_id) + self.ignored_devices.clear() # Callbacks @@ -230,6 +255,14 @@ class DeconzGateway: self._option_allow_deconz_groups = self.option_allow_deconz_groups + option_allow_new_devices = self.config_entry.options.get( + CONF_ALLOW_NEW_DEVICES, DEFAULT_ALLOW_NEW_DEVICES + ) + if option_allow_new_devices and not self.option_allow_new_devices: + self.load_ignored_devices() + + self.option_allow_new_devices = option_allow_new_devices + entity_registry = er.async_get(self.hass) for entity_id, deconz_id in self.deconz_ids.items(): diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 2b03bb0ddb4..0f23ff02831 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -95,7 +95,7 @@ async def async_setup_entry( config_entry.async_on_unload( gateway.api.lights.lights.subscribe( - async_add_light, + gateway.evaluate_add_device(async_add_light), EventType.ADDED, ) ) diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index 5b8c9d87e67..dc3da3cbe8c 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -34,7 +34,7 @@ async def async_setup_entry( config_entry.async_on_unload( gateway.api.lights.locks.subscribe( - async_add_lock_from_light, + gateway.evaluate_add_device(async_add_lock_from_light), EventType.ADDED, ) ) @@ -49,7 +49,7 @@ async def async_setup_entry( config_entry.async_on_unload( gateway.api.sensors.door_lock.subscribe( - async_add_lock_from_sensor, + gateway.evaluate_add_device(async_add_lock_from_sensor), EventType.ADDED, ) ) diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index 81886fd3c7c..5d555797372 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -77,7 +77,7 @@ async def async_setup_entry( config_entry.async_on_unload( gateway.api.sensors.presence.subscribe( - async_add_sensor, + gateway.evaluate_add_device(async_add_sensor), EventType.ADDED, ) ) diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 3d12a293c39..a9a5172d72e 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -144,6 +144,7 @@ async def async_refresh_devices_service(gateway: DeconzGateway) -> None: """Refresh available devices from deCONZ.""" gateway.ignore_state_updates = True await gateway.api.refresh_state() + gateway.load_ignored_devices() gateway.ignore_state_updates = False for resource_type in gateway.deconz_resource_type_to_signal_new_device: diff --git a/homeassistant/components/deconz/siren.py b/homeassistant/components/deconz/siren.py index 2a11cd5346a..0c5b3010626 100644 --- a/homeassistant/components/deconz/siren.py +++ b/homeassistant/components/deconz/siren.py @@ -37,7 +37,7 @@ async def async_setup_entry( config_entry.async_on_unload( gateway.api.lights.sirens.subscribe( - async_add_siren, + gateway.evaluate_add_device(async_add_siren), EventType.ADDED, ) ) diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index 6789b20b3c7..d6e12365407 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -30,21 +30,21 @@ async def async_setup_entry( gateway.entities[DOMAIN] = set() @callback - def async_add_switch(_: EventType, light_id: str) -> None: + def async_add_switch(_: EventType, switch_id: str) -> None: """Add switch from deCONZ.""" - light = gateway.api.lights.lights[light_id] - if light.type not in POWER_PLUGS: + switch = gateway.api.lights.lights[switch_id] + if switch.type not in POWER_PLUGS: return - async_add_entities([DeconzPowerPlug(light, gateway)]) + async_add_entities([DeconzPowerPlug(switch, gateway)]) config_entry.async_on_unload( gateway.api.lights.lights.subscribe( - async_add_switch, + gateway.evaluate_add_device(async_add_switch), EventType.ADDED, ) ) - for light_id in gateway.api.lights.lights: - async_add_switch(EventType.ADDED, light_id) + for switch_id in gateway.api.lights.lights: + async_add_switch(EventType.ADDED, switch_id) class DeconzPowerPlug(DeconzDevice, SwitchEntity): From f4a2afeb375483f952a0e142fb6c4ef2a7b4d21a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 May 2022 17:48:14 -0500 Subject: [PATCH 0555/3516] Guard against recorder pool current connection disappearing during global destruction (#71971) --- homeassistant/components/recorder/pool.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py index f2f952121af..52b6b74dfa1 100644 --- a/homeassistant/components/recorder/pool.py +++ b/homeassistant/components/recorder/pool.py @@ -55,7 +55,12 @@ class RecorderPool(SingletonThreadPool, NullPool): # type: ignore[misc] def shutdown(self) -> None: """Close the connection.""" - if self.recorder_or_dbworker and self._conn and (conn := self._conn.current()): + if ( + self.recorder_or_dbworker + and self._conn + and hasattr(self._conn, "current") + and (conn := self._conn.current()) + ): conn.close() def dispose(self) -> None: From 8fcee01db0543271cdbf0ef46ab028fa3eed1326 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 May 2022 18:03:02 -0500 Subject: [PATCH 0556/3516] Remove unnecessary flush from recorder (#71910) --- homeassistant/components/recorder/core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index ba0e5ba17d1..91dc0a27ead 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -847,15 +847,14 @@ class Recorder(threading.Thread): assert self.event_session is not None self._commits_without_expire += 1 + self.event_session.commit() if self._pending_expunge: - self.event_session.flush() for dbstate in self._pending_expunge: # Expunge the state so its not expired # until we use it later for dbstate.old_state if dbstate in self.event_session: self.event_session.expunge(dbstate) self._pending_expunge = [] - self.event_session.commit() # We just committed the state attributes to the database # and we now know the attributes_ids. We can save From e339f433221547baa6218e75783acd743efdefce Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 16 May 2022 18:03:39 -0500 Subject: [PATCH 0557/3516] Add a timeout during Sonos speaker setup (#71973) --- homeassistant/components/sonos/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index c775b475dc4..1d7acd8d8dc 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -204,7 +204,7 @@ class SonosDiscoveryManager: def _add_speaker(self, soco: SoCo) -> None: """Create and set up a new SonosSpeaker instance.""" try: - speaker_info = soco.get_speaker_info(True) + speaker_info = soco.get_speaker_info(True, timeout=7) if soco.uid not in self.data.boot_counts: self.data.boot_counts[soco.uid] = soco.boot_seqnum _LOGGER.debug("Adding new speaker: %s", speaker_info) From 9092dcaceaaa0396aaf175edac3a56a2aeadf33c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 May 2022 18:04:05 -0500 Subject: [PATCH 0558/3516] Use async_capture_events for core tests (#71970) --- tests/test_core.py | 34 ++++------------------------------ 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 104828f64e1..1ac7002cb7b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1055,13 +1055,7 @@ def test_config_is_allowed_external_url(): async def test_event_on_update(hass): """Test that event is fired on update.""" - events = [] - - @ha.callback - def callback(event): - events.append(event) - - hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, callback) + events = async_capture_events(hass, EVENT_CORE_CONFIG_UPDATE) assert hass.config.latitude != 12 @@ -1141,13 +1135,7 @@ async def test_service_executed_with_subservices(hass): async def test_service_call_event_contains_original_data(hass): """Test that service call event contains original data.""" - events = [] - - @ha.callback - def callback(event): - events.append(event) - - hass.bus.async_listen(EVENT_CALL_SERVICE, callback) + events = async_capture_events(hass, EVENT_CALL_SERVICE) calls = async_mock_service( hass, "test", "service", vol.Schema({"number": vol.Coerce(int)}) @@ -1787,14 +1775,7 @@ def _ulid_timestamp(ulid: str) -> int: async def test_state_change_events_context_id_match_state_time(hass): """Test last_updated, timed_fired, and the ulid all have the same time.""" - events = [] - - @ha.callback - def _event_listener(event): - events.append(event) - - hass.bus.async_listen(ha.EVENT_STATE_CHANGED, _event_listener) - + events = async_capture_events(hass, ha.EVENT_STATE_CHANGED) hass.states.async_set("light.bedroom", "on") await hass.async_block_till_done() state: State = hass.states.get("light.bedroom") @@ -1808,14 +1789,7 @@ async def test_state_change_events_context_id_match_state_time(hass): async def test_state_firing_event_matches_context_id_ulid_time(hass): """Test timed_fired and the ulid have the same time.""" - events = [] - - @ha.callback - def _event_listener(event): - events.append(event) - - hass.bus.async_listen(EVENT_HOMEASSISTANT_STARTED, _event_listener) - + events = async_capture_events(hass, EVENT_HOMEASSISTANT_STARTED) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() From 17470618206e05561bd381017a6476bb53356670 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Tue, 17 May 2022 01:51:30 +0200 Subject: [PATCH 0559/3516] Enable NUT strict typing (#71913) --- .strict-typing | 1 + homeassistant/components/nut/__init__.py | 81 ++++++++++++--------- homeassistant/components/nut/config_flow.py | 62 +++++++++------- homeassistant/components/nut/sensor.py | 35 ++++++++- mypy.ini | 11 +++ 5 files changed, 126 insertions(+), 64 deletions(-) diff --git a/.strict-typing b/.strict-typing index 36ed1685e9f..4ec4fbff5ee 100644 --- a/.strict-typing +++ b/.strict-typing @@ -165,6 +165,7 @@ homeassistant.components.no_ip.* homeassistant.components.notify.* homeassistant.components.notion.* homeassistant.components.number.* +homeassistant.components.nut.* homeassistant.components.oncue.* homeassistant.components.onewire.* homeassistant.components.open_meteo.* diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 775c512f946..28c41ccda3a 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -1,4 +1,7 @@ """The nut component.""" +from __future__ import annotations + +from dataclasses import dataclass from datetime import timedelta import logging @@ -7,9 +10,6 @@ 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, @@ -59,7 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data = PyNUTData(host, port, alias, username, password) - async def async_update_data(): + async def async_update_data() -> dict[str, str]: """Fetch data from NUT.""" async with async_timeout.timeout(10): await hass.async_add_executor_job(data.update) @@ -98,9 +98,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: config_entry_id=entry.entry_id, identifiers={(DOMAIN, unique_id)}, name=data.name.title(), - manufacturer=data.device_info.get(ATTR_MANUFACTURER), - model=data.device_info.get(ATTR_MODEL), - sw_version=data.device_info.get(ATTR_SW_VERSION), + manufacturer=data.device_info.manufacturer, + model=data.device_info.model, + sw_version=data.device_info.firmware, ) hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -120,7 +120,7 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> Non await hass.config_entries.async_reload(entry.entry_id) -def _manufacturer_from_status(status): +def _manufacturer_from_status(status: dict[str, str]) -> str | None: """Find the best manufacturer value from the status.""" return ( status.get("device.mfr") @@ -130,7 +130,7 @@ def _manufacturer_from_status(status): ) -def _model_from_status(status): +def _model_from_status(status: dict[str, str]) -> str | None: """Find the best model value from the status.""" return ( status.get("device.model") @@ -139,12 +139,12 @@ def _model_from_status(status): ) -def _firmware_from_status(status): +def _firmware_from_status(status: dict[str, str]) -> str | None: """Find the best firmware value from the status.""" return status.get("ups.firmware") or status.get("ups.firmware.aux") -def _serial_from_status(status): +def _serial_from_status(status: dict[str, str]) -> str | None: """Find the best serialvalue from the status.""" serial = status.get("device.serial") or status.get("ups.serial") if serial and ( @@ -154,7 +154,7 @@ def _serial_from_status(status): return serial -def _unique_id_from_status(status): +def _unique_id_from_status(status: dict[str, str]) -> str | None: """Find the best unique id value from the status.""" serial = _serial_from_status(status) # We must have a serial for this to be unique @@ -174,6 +174,15 @@ def _unique_id_from_status(status): return "_".join(unique_id_group) +@dataclass +class NUTDeviceInfo: + """Device information for NUT.""" + + manufacturer: str | None = None + model: str | None = None + firmware: str | None = None + + class PyNUTData: """Stores the data retrieved from NUT. @@ -181,7 +190,14 @@ class PyNUTData: updates from the server. """ - def __init__(self, host, port, alias, username, password): + def __init__( + self, + host: str, + port: int, + alias: str | None, + username: str | None, + password: str | None, + ) -> None: """Initialize the data object.""" self._host = host @@ -190,29 +206,29 @@ class PyNUTData: # Establish client with persistent=False to open/close connection on # each update call. This is more reliable with async. self._client = PyNUTClient(self._host, port, username, password, 5, False) - self.ups_list = None - self._status = None - self._device_info = None + self.ups_list: dict[str, str] | None = None + self._status: dict[str, str] | None = None + self._device_info: NUTDeviceInfo | None = None @property - def status(self): + def status(self) -> dict[str, str] | None: """Get latest update if throttle allows. Return status.""" return self._status @property - def name(self): + def name(self) -> str: """Return the name of the ups.""" - return self._alias + return self._alias or f"Nut-{self._host}" @property - def device_info(self): + def device_info(self) -> NUTDeviceInfo: """Return the device info for the ups.""" - return self._device_info or {} + return self._device_info or NUTDeviceInfo() - def _get_alias(self): + def _get_alias(self) -> str | None: """Get the ups alias from NUT.""" try: - ups_list = self._client.list_ups() + ups_list: dict[str, str] = self._client.list_ups() except PyNUTError as err: _LOGGER.error("Failure getting NUT ups alias, %s", err) return None @@ -224,7 +240,7 @@ class PyNUTData: self.ups_list = ups_list return list(ups_list)[0] - def _get_device_info(self): + def _get_device_info(self) -> NUTDeviceInfo | None: """Get the ups device info from NUT.""" if not self._status: return None @@ -232,27 +248,24 @@ class PyNUTData: 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 + device_info = NUTDeviceInfo(manufacturer, model, firmware) + return device_info - def _get_status(self): + def _get_status(self) -> dict[str, str] | None: """Get the ups status from NUT.""" if self._alias is None: self._alias = self._get_alias() try: - return self._client.list_vars(self._alias) + status: dict[str, str] = self._client.list_vars(self._alias) except (PyNUTError, ConnectionResetError) as err: _LOGGER.debug("Error getting NUT vars for host %s: %s", self._host, err) return None - def update(self): + return status + + def update(self) -> None: """Fetch the latest status from NUT.""" self._status = self._get_status() if self._device_info is None: diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py index fb0a2210a69..5ba8024878a 100644 --- a/homeassistant/components/nut/config_flow.py +++ b/homeassistant/components/nut/config_flow.py @@ -2,11 +2,13 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import exceptions from homeassistant.components import zeroconf +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import ( CONF_ALIAS, CONF_BASE, @@ -16,7 +18,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_USERNAME, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from . import PyNUTData @@ -42,12 +44,12 @@ def _base_schema(discovery_info: zeroconf.ZeroconfServiceInfo | None) -> vol.Sch return vol.Schema(base_schema) -def _ups_schema(ups_list): +def _ups_schema(ups_list: dict[str, str]) -> vol.Schema: """UPS selection schema.""" return vol.Schema({vol.Required(CONF_ALIAS): vol.In(ups_list)}) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from _base_schema with values provided by the user. @@ -59,15 +61,15 @@ async def validate_input(hass: core.HomeAssistant, data): username = data.get(CONF_USERNAME) password = data.get(CONF_PASSWORD) - data = PyNUTData(host, port, alias, username, password) - await hass.async_add_executor_job(data.update) - if not (status := data.status): + nut_data = PyNUTData(host, port, alias, username, password) + await hass.async_add_executor_job(nut_data.update) + if not (status := nut_data.status): raise CannotConnect - return {"ups_list": data.ups_list, "available_resources": status} + return {"ups_list": nut_data.ups_list, "available_resources": status} -def _format_host_port_alias(user_input): +def _format_host_port_alias(user_input: dict[str, Any]) -> str: """Format a host, port, and alias so it can be used for comparison or display.""" host = user_input[CONF_HOST] port = user_input[CONF_PORT] @@ -77,17 +79,17 @@ def _format_host_port_alias(user_input): return f"{host}:{port}" -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class NutConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Network UPS Tools (NUT).""" VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize the nut config flow.""" - self.nut_config = {} + self.nut_config: dict[str, Any] = {} self.discovery_info: zeroconf.ZeroconfServiceInfo | None = None - self.ups_list = None - self.title = None + self.ups_list: dict[str, str] | None = None + self.title: str | None = None async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo @@ -101,9 +103,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } return await self.async_step_user() - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the user input.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: if self.discovery_info: user_input.update( @@ -129,9 +133,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=_base_schema(self.discovery_info), errors=errors ) - async def async_step_ups(self, user_input=None): + async def async_step_ups( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the picking the ups.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: self.nut_config.update(user_input) @@ -144,20 +150,22 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="ups", - data_schema=_ups_schema(self.ups_list), + data_schema=_ups_schema(self.ups_list or {}), errors=errors, ) - def _host_port_alias_already_configured(self, user_input): + def _host_port_alias_already_configured(self, user_input: dict[str, Any]) -> bool: """See if we already have a nut entry matching user input configured.""" existing_host_port_aliases = { - _format_host_port_alias(entry.data) + _format_host_port_alias(dict(entry.data)) for entry in self._async_current_entries() if CONF_HOST in entry.data } return _format_host_port_alias(user_input) in existing_host_port_aliases - async def _async_validate_or_error(self, config): + async def _async_validate_or_error( + self, config: dict[str, Any] + ) -> tuple[dict[str, Any], dict[str, str]]: errors = {} info = {} try: @@ -171,19 +179,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle a option flow for nut.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle options flow.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 1992ba49635..6992e41366e 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -1,11 +1,18 @@ """Provides a sensor to track various status aspects of a UPS.""" from __future__ import annotations +from dataclasses import asdict import logging +from typing import cast from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_UNKNOWN +from homeassistant.const import ( + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_SW_VERSION, + STATE_UNKNOWN, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -26,9 +33,27 @@ from .const import ( STATE_TYPES, ) +NUT_DEV_INFO_TO_DEV_INFO: dict[str, str] = { + "manufacturer": ATTR_MANUFACTURER, + "model": ATTR_MODEL, + "firmware": ATTR_SW_VERSION, +} + _LOGGER = logging.getLogger(__name__) +def _get_nut_device_info(data: PyNUTData) -> DeviceInfo: + """Return a DeviceInfo object filled with NUT device info.""" + nut_dev_infos = asdict(data.device_info) + nut_infos = { + info_key: nut_dev_infos[nut_key] + for nut_key, info_key in NUT_DEV_INFO_TO_DEV_INFO.items() + if nut_dev_infos[nut_key] is not None + } + + return cast(DeviceInfo, nut_infos) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -64,6 +89,8 @@ async def async_setup_entry( class NUTSensor(CoordinatorEntity, SensorEntity): """Representation of a sensor entity for NUT status values.""" + coordinator: DataUpdateCoordinator[dict[str, str]] + def __init__( self, coordinator: DataUpdateCoordinator, @@ -82,10 +109,10 @@ class NUTSensor(CoordinatorEntity, SensorEntity): identifiers={(DOMAIN, unique_id)}, name=device_name, ) - self._attr_device_info.update(data.device_info) + self._attr_device_info.update(_get_nut_device_info(data)) @property - def native_value(self): + def native_value(self) -> str | None: """Return entity state from ups.""" status = self.coordinator.data if self.entity_description.key == KEY_STATUS_DISPLAY: @@ -93,7 +120,7 @@ class NUTSensor(CoordinatorEntity, SensorEntity): return status.get(self.entity_description.key) -def _format_display_state(status): +def _format_display_state(status: dict[str, str]) -> str: """Return UPS display state.""" try: return " ".join(STATE_TYPES[state] for state in status[KEY_STATUS].split()) diff --git a/mypy.ini b/mypy.ini index dc023da8d01..898c6b860f0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1578,6 +1578,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.nut.*] +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.oncue.*] check_untyped_defs = true disallow_incomplete_defs = true From 1a6ccdbf597787be11d2e456ae7da3daf4d5b5dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 May 2022 18:52:05 -0500 Subject: [PATCH 0560/3516] Bump unifi-discovery to 1.1.3 (#71975) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 3efad51db31..36b6011e8fc 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.5.1", "unifi-discovery==1.1.2"], + "requirements": ["pyunifiprotect==3.5.1", "unifi-discovery==1.1.3"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index e5a091ee65d..027b6406b2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2352,7 +2352,7 @@ uEagle==0.0.2 uasiren==0.0.1 # homeassistant.components.unifiprotect -unifi-discovery==1.1.2 +unifi-discovery==1.1.3 # homeassistant.components.unifiled unifiled==0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2623f133464..c9949ba3b2c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1540,7 +1540,7 @@ uEagle==0.0.2 uasiren==0.0.1 # homeassistant.components.unifiprotect -unifi-discovery==1.1.2 +unifi-discovery==1.1.3 # homeassistant.components.upb upb_lib==0.4.12 From 3b88c6c0126c8b019322fecd9e8547cc2b6e8b60 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 16 May 2022 17:10:34 -0700 Subject: [PATCH 0561/3516] Inverse parallel updates default check, follow sync "update" method (#71720) --- homeassistant/helpers/entity_platform.py | 6 +++--- tests/helpers/test_entity_platform.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 3d57a9c3dc0..ecf2125962a 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -120,7 +120,7 @@ class EntityPlatform: @callback def _get_parallel_updates_semaphore( - self, entity_has_async_update: bool + self, entity_has_sync_update: bool ) -> asyncio.Semaphore | None: """Get or create a semaphore for parallel updates. @@ -138,7 +138,7 @@ class EntityPlatform: parallel_updates = getattr(self.platform, "PARALLEL_UPDATES", None) - if parallel_updates is None and not entity_has_async_update: + if parallel_updates is None and entity_has_sync_update: parallel_updates = 1 if parallel_updates == 0: @@ -422,7 +422,7 @@ class EntityPlatform: entity.add_to_platform_start( self.hass, self, - self._get_parallel_updates_semaphore(hasattr(entity, "async_update")), + self._get_parallel_updates_semaphore(hasattr(entity, "update")), ) # Update properties before we generate the entity_id diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 3f18d565e83..933669ebc53 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -332,6 +332,25 @@ async def test_parallel_updates_sync_platform(hass): assert entity.parallel_updates._value == 1 +async def test_parallel_updates_no_update_method(hass): + """Test platform parallel_updates default set to 0.""" + platform = MockPlatform() + + mock_entity_platform(hass, "test_domain.platform", platform) + + component = EntityComponent(_LOGGER, DOMAIN, hass) + component._platforms = {} + + await component.async_setup({DOMAIN: {"platform": "platform"}}) + await hass.async_block_till_done() + + handle = list(component._platforms.values())[-1] + + entity = MockEntity() + await handle.async_add_entities([entity]) + assert entity.parallel_updates is None + + async def test_parallel_updates_sync_platform_with_constant(hass): """Test sync platform can set parallel_updates limit.""" platform = MockPlatform() From 2d1a612976e59a40211356bfdfd2564f2b6679a6 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 17 May 2022 00:23:03 +0000 Subject: [PATCH 0562/3516] [ci skip] Translation update --- .../aladdin_connect/translations/bg.json | 26 +++++++++++++++++ .../aladdin_connect/translations/no.json | 27 +++++++++++++++++ .../aladdin_connect/translations/pl.json | 26 +++++++++++++++++ .../components/aurora/translations/nl.json | 2 +- .../components/baf/translations/bg.json | 23 +++++++++++++++ .../components/baf/translations/de.json | 23 +++++++++++++++ .../components/baf/translations/el.json | 23 +++++++++++++++ .../components/baf/translations/hu.json | 23 +++++++++++++++ .../components/baf/translations/ja.json | 23 +++++++++++++++ .../components/baf/translations/ko.json | 13 +++++++++ .../components/baf/translations/no.json | 23 +++++++++++++++ .../components/baf/translations/pl.json | 18 ++++++++++++ .../components/baf/translations/zh-Hant.json | 23 +++++++++++++++ .../derivative/translations/nl.json | 22 +++++++------- .../geocaching/translations/bg.json | 17 +++++++++++ .../components/group/translations/nl.json | 6 ++-- .../components/hassio/translations/it.json | 6 ++-- .../homeassistant/translations/it.json | 6 ++-- .../components/knx/translations/it.json | 2 +- .../litterrobot/translations/sensor.bg.json | 8 +++++ .../litterrobot/translations/sensor.no.json | 28 ++++++++++++++++++ .../litterrobot/translations/sensor.pl.json | 9 ++++++ .../motion_blinds/translations/no.json | 2 +- .../season/translations/sensor.fy.json | 7 +++++ .../components/siren/translations/bg.json | 3 ++ .../components/siren/translations/ca.json | 3 ++ .../components/siren/translations/de.json | 3 ++ .../components/siren/translations/el.json | 3 ++ .../components/siren/translations/et.json | 3 ++ .../components/siren/translations/fr.json | 3 ++ .../components/siren/translations/it.json | 3 ++ .../components/siren/translations/ja.json | 3 ++ .../components/siren/translations/ko.json | 3 ++ .../components/siren/translations/nl.json | 3 ++ .../components/siren/translations/pt-BR.json | 3 ++ .../components/siren/translations/tr.json | 3 ++ .../components/slack/translations/bg.json | 22 ++++++++++++++ .../components/slack/translations/ko.json | 9 ++++++ .../components/slack/translations/no.json | 29 +++++++++++++++++++ .../components/slack/translations/pl.json | 20 +++++++++++++ .../components/threshold/translations/nl.json | 4 +-- .../ukraine_alarm/translations/bg.json | 8 +++-- .../components/zha/translations/fy.json | 7 +++++ .../components/zwave_js/translations/it.json | 10 +++---- 44 files changed, 499 insertions(+), 32 deletions(-) create mode 100644 homeassistant/components/aladdin_connect/translations/bg.json create mode 100644 homeassistant/components/aladdin_connect/translations/no.json create mode 100644 homeassistant/components/aladdin_connect/translations/pl.json create mode 100644 homeassistant/components/baf/translations/bg.json create mode 100644 homeassistant/components/baf/translations/de.json create mode 100644 homeassistant/components/baf/translations/el.json create mode 100644 homeassistant/components/baf/translations/hu.json create mode 100644 homeassistant/components/baf/translations/ja.json create mode 100644 homeassistant/components/baf/translations/ko.json create mode 100644 homeassistant/components/baf/translations/no.json create mode 100644 homeassistant/components/baf/translations/pl.json create mode 100644 homeassistant/components/baf/translations/zh-Hant.json create mode 100644 homeassistant/components/geocaching/translations/bg.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.bg.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.no.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.pl.json create mode 100644 homeassistant/components/season/translations/sensor.fy.json create mode 100644 homeassistant/components/siren/translations/bg.json create mode 100644 homeassistant/components/siren/translations/ca.json create mode 100644 homeassistant/components/siren/translations/de.json create mode 100644 homeassistant/components/siren/translations/el.json create mode 100644 homeassistant/components/siren/translations/et.json create mode 100644 homeassistant/components/siren/translations/fr.json create mode 100644 homeassistant/components/siren/translations/it.json create mode 100644 homeassistant/components/siren/translations/ja.json create mode 100644 homeassistant/components/siren/translations/ko.json create mode 100644 homeassistant/components/siren/translations/nl.json create mode 100644 homeassistant/components/siren/translations/pt-BR.json create mode 100644 homeassistant/components/siren/translations/tr.json create mode 100644 homeassistant/components/slack/translations/bg.json create mode 100644 homeassistant/components/slack/translations/ko.json create mode 100644 homeassistant/components/slack/translations/no.json create mode 100644 homeassistant/components/slack/translations/pl.json create mode 100644 homeassistant/components/zha/translations/fy.json diff --git a/homeassistant/components/aladdin_connect/translations/bg.json b/homeassistant/components/aladdin_connect/translations/bg.json new file mode 100644 index 00000000000..d5babb02c1c --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/bg.json @@ -0,0 +1,26 @@ +{ + "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\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" + }, + "step": { + "reauth_confirm": { + "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 \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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/no.json b/homeassistant/components/aladdin_connect/translations/no.json new file mode 100644 index 00000000000..c6e6e8413b3 --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/no.json @@ -0,0 +1,27 @@ +{ + "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" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Aladdin Connect-integrasjonen m\u00e5 autentisere kontoen din p\u00e5 nytt", + "title": "Godkjenne integrering p\u00e5 nytt" + }, + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/pl.json b/homeassistant/components/aladdin_connect/translations/pl.json new file mode 100644 index 00000000000..3ebc9a4ff92 --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/pl.json @@ -0,0 +1,26 @@ +{ + "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" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "title": "Ponownie uwierzytelnij integracj\u0119" + }, + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/nl.json b/homeassistant/components/aurora/translations/nl.json index fe7b4809f13..423e722921d 100644 --- a/homeassistant/components/aurora/translations/nl.json +++ b/homeassistant/components/aurora/translations/nl.json @@ -17,7 +17,7 @@ "step": { "init": { "data": { - "threshold": "Drempel (%)" + "threshold": "Drempelwaarde (%)" } } } diff --git a/homeassistant/components/baf/translations/bg.json b/homeassistant/components/baf/translations/bg.json new file mode 100644 index 00000000000..c5e33f4c079 --- /dev/null +++ b/homeassistant/components/baf/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", + "ipv6_not_supported": "IPv6 \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \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} - {model} ({ip_address})", + "step": { + "discovery_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name} - {model} ({ip_address})?" + }, + "user": { + "data": { + "ip_address": "IP \u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/de.json b/homeassistant/components/baf/translations/de.json new file mode 100644 index 00000000000..8e5344cf9b0 --- /dev/null +++ b/homeassistant/components/baf/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "ipv6_not_supported": "IPv6 wird nicht unterst\u00fctzt." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "{name} - {model} ({ip_address})", + "step": { + "discovery_confirm": { + "description": "M\u00f6chtest du {name} - {model} ({ip_address}) einrichten?" + }, + "user": { + "data": { + "ip_address": "IP-Adresse" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/el.json b/homeassistant/components/baf/translations/el.json new file mode 100644 index 00000000000..a73fe3aa490 --- /dev/null +++ b/homeassistant/components/baf/translations/el.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "ipv6_not_supported": "\u03a4\u03bf IPv6 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9." + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "flow_title": "{name} - {model} ({ip_address})", + "step": { + "discovery_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} - {model} ({ip_address});" + }, + "user": { + "data": { + "ip_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/hu.json b/homeassistant/components/baf/translations/hu.json new file mode 100644 index 00000000000..82e7d6aae75 --- /dev/null +++ b/homeassistant/components/baf/translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "ipv6_not_supported": "Az IPv6 nem t\u00e1mogatott." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "{name} - {model} ({ip_address})", + "step": { + "discovery_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} - {model} ({ip_address})?" + }, + "user": { + "data": { + "ip_address": "IP c\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/ja.json b/homeassistant/components/baf/translations/ja.json new file mode 100644 index 00000000000..0e256a2f92c --- /dev/null +++ b/homeassistant/components/baf/translations/ja.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "ipv6_not_supported": "IPv6\u306b\u306f\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "flow_title": "{name} - {model} ({ip_address})", + "step": { + "discovery_confirm": { + "description": "{name} - {model} ({ip_address}) \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "ip_address": "IP\u30a2\u30c9\u30ec\u30b9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/ko.json b/homeassistant/components/baf/translations/ko.json new file mode 100644 index 00000000000..a8884c0403e --- /dev/null +++ b/homeassistant/components/baf/translations/ko.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "ipv6_not_supported": "IPv6\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." + }, + "flow_title": "{name} - {model} ( {ip_address} )", + "step": { + "discovery_confirm": { + "description": "{name} - {model} ({ip_address}) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/no.json b/homeassistant/components/baf/translations/no.json new file mode 100644 index 00000000000..fa65c5ca21f --- /dev/null +++ b/homeassistant/components/baf/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "ipv6_not_supported": "IPv6 st\u00f8ttes ikke." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "flow_title": "{name} \u2013 {model} ( {ip_address} )", + "step": { + "discovery_confirm": { + "description": "Vil du konfigurere {name} - {model} ( {ip_address} )?" + }, + "user": { + "data": { + "ip_address": "IP adresse" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/pl.json b/homeassistant/components/baf/translations/pl.json new file mode 100644 index 00000000000..5c64afd30a8 --- /dev/null +++ b/homeassistant/components/baf/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "ip_address": "Adres IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/zh-Hant.json b/homeassistant/components/baf/translations/zh-Hant.json new file mode 100644 index 00000000000..4d191cb6d87 --- /dev/null +++ b/homeassistant/components/baf/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "ipv6_not_supported": "\u4e0d\u652f\u63f4 IPv6\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{name} - {model} ({ip_address})", + "step": { + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} - {model} ({ip_address})\uff1f" + }, + "user": { + "data": { + "ip_address": "IP \u4f4d\u5740" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/nl.json b/homeassistant/components/derivative/translations/nl.json index 8b7cf5a9402..aa8eadc214c 100644 --- a/homeassistant/components/derivative/translations/nl.json +++ b/homeassistant/components/derivative/translations/nl.json @@ -4,19 +4,19 @@ "user": { "data": { "name": "Naam", - "round": "Precisie", - "source": "Invoer sensor", + "round": "Nauwkeurigheid", + "source": "Invoersensor", "time_window": "Tijdsvenster", "unit_prefix": "Metrisch voorvoegsel", "unit_time": "Tijdseenheid" }, "data_description": { - "round": "Regelt het aantal decimale cijfers in de uitvoer.", + "round": "Regelt het aantal decimalen in de uitvoer.", "time_window": "Indien ingesteld, is de waarde van de sensor een tijdgewogen voortschrijdend gemiddelde van de afgeleiden binnen dit venster.", "unit_prefix": "De uitvoer wordt geschaald volgens het geselecteerde metrische voorvoegsel en de tijdseenheid van de afgeleide" }, "description": "Maak een sensor die de afgeleide van een sensor schat.", - "title": "Voeg afgeleide sensor toe" + "title": "Voeg afgeleidesensor toe" } } }, @@ -25,14 +25,14 @@ "init": { "data": { "name": "Naam", - "round": "Precisie", - "source": "Invoer sensor", + "round": "Nauwkeurigheid", + "source": "Invoersensor", "time_window": "Tijdsvenster", "unit_prefix": "Metrisch voorvoegsel", "unit_time": "Tijdseenheid" }, "data_description": { - "round": "Regelt het aantal decimale cijfers in de uitvoer.", + "round": "Regelt het aantal decimalen in de uitvoer.", "time_window": "Indien ingesteld, is de waarde van de sensor een tijdgewogen voortschrijdend gemiddelde van de afgeleiden binnen dit venster.", "unit_prefix": "De uitvoer wordt geschaald volgens het geselecteerde metrische voorvoegsel en de tijdseenheid van de afgeleide." } @@ -40,14 +40,14 @@ "options": { "data": { "name": "Naam", - "round": "Precisie", - "source": "Invoer sensor", + "round": "Nauwkeurigheid", + "source": "Invoersensor", "time_window": "Tijdsvenster", "unit_prefix": "Metrisch voorvoegsel", "unit_time": "Tijdseenheid" }, "data_description": { - "round": "Regelt het aantal decimale cijfers in de uitvoer.", + "round": "Regelt het aantal decimalen in de uitvoer.", "time_window": "Indien ingesteld, is de waarde van de sensor een tijdgewogen voortschrijdend gemiddelde van de afgeleiden binnen dit venster.", "unit_prefix": "De uitvoer wordt geschaald volgens het geselecteerde metrische voorvoegsel en de tijdseenheid van de afgeleide." }, @@ -55,5 +55,5 @@ } } }, - "title": "Derivatieve sensor" + "title": "Afgeleide" } \ No newline at end of file diff --git a/homeassistant/components/geocaching/translations/bg.json b/homeassistant/components/geocaching/translations/bg.json new file mode 100644 index 00000000000..00c237dd077 --- /dev/null +++ b/homeassistant/components/geocaching/translations/bg.json @@ -0,0 +1,17 @@ +{ + "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", + "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": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\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" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/nl.json b/homeassistant/components/group/translations/nl.json index e670e2e853a..1343279c365 100644 --- a/homeassistant/components/group/translations/nl.json +++ b/homeassistant/components/group/translations/nl.json @@ -104,13 +104,13 @@ }, "description": "Met groepen kunt u een nieuwe entiteit cre\u00ebren die meerdere entiteiten van hetzelfde type vertegenwoordigt.", "menu_options": { - "binary_sensor": "Binaire sensorgroep", + "binary_sensor": "Binaire-sensorgroep", "cover": "Rolluikgroep", "fan": "Ventilatorgroep", "light": "Lichtgroep", - "lock": "Groep vergrendelen", + "lock": "Slotgroep", "media_player": "Mediaspelergroep", - "switch": "Groep wisselen" + "switch": "Schakelaargroep" }, "title": "Groep toevoegen" } diff --git a/homeassistant/components/hassio/translations/it.json b/homeassistant/components/hassio/translations/it.json index b601a11241d..44499d3f002 100644 --- a/homeassistant/components/hassio/translations/it.json +++ b/homeassistant/components/hassio/translations/it.json @@ -6,10 +6,10 @@ "disk_used": "Disco utilizzato", "docker_version": "Versione Docker", "healthy": "Integrit\u00e0", - "host_os": "Sistema operativo dell'host", + "host_os": "Sistema Operativo Host", "installed_addons": "Componenti aggiuntivi installati", - "supervisor_api": "API supervisore", - "supervisor_version": "Versione supervisore", + "supervisor_api": "API Supervisor", + "supervisor_version": "Versione Supervisor", "supported": "Supportato", "update_channel": "Canale di aggiornamento", "version_api": "Versione API" diff --git a/homeassistant/components/homeassistant/translations/it.json b/homeassistant/components/homeassistant/translations/it.json index b85f3072620..3052a536338 100644 --- a/homeassistant/components/homeassistant/translations/it.json +++ b/homeassistant/components/homeassistant/translations/it.json @@ -4,10 +4,10 @@ "arch": "Architettura della CPU", "dev": "Sviluppo", "docker": "Docker", - "hassio": "Supervisore", + "hassio": "Supervisor", "installation_type": "Tipo di installazione", - "os_name": "Famiglia del sistema operativo", - "os_version": "Versione del sistema operativo", + "os_name": "Famiglia del Sistema Operativo", + "os_version": "Versione del Sistema Operativo", "python_version": "Versione Python", "timezone": "Fuso orario", "user": "Utente", diff --git a/homeassistant/components/knx/translations/it.json b/homeassistant/components/knx/translations/it.json index c05bdba3944..e47d93a27a5 100644 --- a/homeassistant/components/knx/translations/it.json +++ b/homeassistant/components/knx/translations/it.json @@ -47,7 +47,7 @@ "knxkeys_password": "La password per decifrare il file `.knxkeys`" }, "data_description": { - "knxkeys_filename": "Il file dovrebbe essere trovato nella tua cartella di configurazione in `.storage/knx/`.\n Nel sistema operativo Home Assistant questo sarebbe `/config/.storage/knx/`\n Esempio: `mio_progetto.knxkeys`", + "knxkeys_filename": "Il file dovrebbe essere trovato nella tua cartella di configurazione in `.storage/knx/`.\n Nel Sistema Operativo di Home Assistant questo sarebbe `/config/.storage/knx/`\n Esempio: `mio_progetto.knxkeys`", "knxkeys_password": "Questo \u00e8 stato impostato durante l'esportazione del file da ETS." }, "description": "Inserisci le informazioni per il tuo file `.knxkeys`." diff --git a/homeassistant/components/litterrobot/translations/sensor.bg.json b/homeassistant/components/litterrobot/translations/sensor.bg.json new file mode 100644 index 00000000000..3081fb9b285 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.bg.json @@ -0,0 +1,8 @@ +{ + "state": { + "litterrobot__status_code": { + "off": "\u0418\u0437\u043a\u043b.", + "offline": "\u041e\u0444\u043b\u0430\u0439\u043d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.no.json b/homeassistant/components/litterrobot/translations/sensor.no.json new file mode 100644 index 00000000000..2997930e53b --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.no.json @@ -0,0 +1,28 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "Panser fjernet", + "ccc": "Rengj\u00f8ringssyklus fullf\u00f8rt", + "ccp": "Rengj\u00f8ringssyklus p\u00e5g\u00e5r", + "csf": "Feil p\u00e5 kattesensor", + "csi": "Kattesensor avbrutt", + "cst": "Tidsberegning for kattesensor", + "df1": "Skuff nesten full - 2 sykluser igjen", + "df2": "Skuff nesten full - 1 syklus til venstre", + "dfs": "Skuff full", + "dhf": "Dump + Hjemmeposisjonsfeil", + "dpf": "Dumpposisjonsfeil", + "ec": "Tom syklus", + "hpf": "Hjemmeposisjonsfeil", + "off": "Av", + "offline": "Frakoblet", + "otf": "Over dreiemomentfeil", + "p": "Pauset", + "pd": "Knip gjenkjenning", + "rdy": "Klar", + "scf": "Kattesensorfeil ved oppstart", + "sdf": "Skuff full ved oppstart", + "spf": "Knipegjenkjenning ved oppstart" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.pl.json b/homeassistant/components/litterrobot/translations/sensor.pl.json new file mode 100644 index 00000000000..4f12cd1f30c --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.pl.json @@ -0,0 +1,9 @@ +{ + "state": { + "litterrobot__status_code": { + "off": "wy\u0142.", + "offline": "Offline", + "p": "wstrzymany" + } + } +} \ 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 d6c4708ea8c..655e574cab9 100644 --- a/homeassistant/components/motion_blinds/translations/no.json +++ b/homeassistant/components/motion_blinds/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert , tilkoblingsinnstillingene er oppdatert", + "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "connection_error": "Tilkobling mislyktes" }, diff --git a/homeassistant/components/season/translations/sensor.fy.json b/homeassistant/components/season/translations/sensor.fy.json new file mode 100644 index 00000000000..f0990e02749 --- /dev/null +++ b/homeassistant/components/season/translations/sensor.fy.json @@ -0,0 +1,7 @@ +{ + "state": { + "season__season__": { + "summer": "Simmer" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/siren/translations/bg.json b/homeassistant/components/siren/translations/bg.json new file mode 100644 index 00000000000..bd7725464a7 --- /dev/null +++ b/homeassistant/components/siren/translations/bg.json @@ -0,0 +1,3 @@ +{ + "title": "\u0421\u0438\u0440\u0435\u043d\u0430" +} \ No newline at end of file diff --git a/homeassistant/components/siren/translations/ca.json b/homeassistant/components/siren/translations/ca.json new file mode 100644 index 00000000000..f3c1468be31 --- /dev/null +++ b/homeassistant/components/siren/translations/ca.json @@ -0,0 +1,3 @@ +{ + "title": "Sirena" +} \ No newline at end of file diff --git a/homeassistant/components/siren/translations/de.json b/homeassistant/components/siren/translations/de.json new file mode 100644 index 00000000000..bcea16c56f1 --- /dev/null +++ b/homeassistant/components/siren/translations/de.json @@ -0,0 +1,3 @@ +{ + "title": "Sirene" +} \ No newline at end of file diff --git a/homeassistant/components/siren/translations/el.json b/homeassistant/components/siren/translations/el.json new file mode 100644 index 00000000000..c5a968c2fde --- /dev/null +++ b/homeassistant/components/siren/translations/el.json @@ -0,0 +1,3 @@ +{ + "title": "\u03a3\u03b5\u03b9\u03c1\u03ae\u03bd\u03b1" +} \ No newline at end of file diff --git a/homeassistant/components/siren/translations/et.json b/homeassistant/components/siren/translations/et.json new file mode 100644 index 00000000000..c0bd3d03ec7 --- /dev/null +++ b/homeassistant/components/siren/translations/et.json @@ -0,0 +1,3 @@ +{ + "title": "Sireen" +} \ No newline at end of file diff --git a/homeassistant/components/siren/translations/fr.json b/homeassistant/components/siren/translations/fr.json new file mode 100644 index 00000000000..2dad0e1b3b8 --- /dev/null +++ b/homeassistant/components/siren/translations/fr.json @@ -0,0 +1,3 @@ +{ + "title": "Sir\u00e8ne" +} \ No newline at end of file diff --git a/homeassistant/components/siren/translations/it.json b/homeassistant/components/siren/translations/it.json new file mode 100644 index 00000000000..f3c1468be31 --- /dev/null +++ b/homeassistant/components/siren/translations/it.json @@ -0,0 +1,3 @@ +{ + "title": "Sirena" +} \ No newline at end of file diff --git a/homeassistant/components/siren/translations/ja.json b/homeassistant/components/siren/translations/ja.json new file mode 100644 index 00000000000..c83a8107246 --- /dev/null +++ b/homeassistant/components/siren/translations/ja.json @@ -0,0 +1,3 @@ +{ + "title": "\u30b5\u30a4\u30ec\u30f3" +} \ No newline at end of file diff --git a/homeassistant/components/siren/translations/ko.json b/homeassistant/components/siren/translations/ko.json new file mode 100644 index 00000000000..4fe08e488ee --- /dev/null +++ b/homeassistant/components/siren/translations/ko.json @@ -0,0 +1,3 @@ +{ + "title": "\uc0ac\uc774\ub80c" +} \ No newline at end of file diff --git a/homeassistant/components/siren/translations/nl.json b/homeassistant/components/siren/translations/nl.json new file mode 100644 index 00000000000..bcea16c56f1 --- /dev/null +++ b/homeassistant/components/siren/translations/nl.json @@ -0,0 +1,3 @@ +{ + "title": "Sirene" +} \ No newline at end of file diff --git a/homeassistant/components/siren/translations/pt-BR.json b/homeassistant/components/siren/translations/pt-BR.json new file mode 100644 index 00000000000..bcea16c56f1 --- /dev/null +++ b/homeassistant/components/siren/translations/pt-BR.json @@ -0,0 +1,3 @@ +{ + "title": "Sirene" +} \ No newline at end of file diff --git a/homeassistant/components/siren/translations/tr.json b/homeassistant/components/siren/translations/tr.json new file mode 100644 index 00000000000..549bad8914b --- /dev/null +++ b/homeassistant/components/siren/translations/tr.json @@ -0,0 +1,3 @@ +{ + "title": "Siren" +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/bg.json b/homeassistant/components/slack/translations/bg.json new file mode 100644 index 00000000000..35a5c7dd36d --- /dev/null +++ b/homeassistant/components/slack/translations/bg.json @@ -0,0 +1,22 @@ +{ + "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\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": { + "api_key": "API \u043a\u043b\u044e\u0447", + "default_channel": "\u041a\u0430\u043d\u0430\u043b \u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435", + "icon": "\u0418\u043a\u043e\u043d\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/slack/translations/ko.json b/homeassistant/components/slack/translations/ko.json new file mode 100644 index 00000000000..ffc01a9bbcb --- /dev/null +++ b/homeassistant/components/slack/translations/ko.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Slack API \ud0a4\ub97c \uac00\uc838\uc624\ub294 \ubc29\ubc95\uc5d0 \ub300\ud55c \uc124\uba85\uc11c\ub97c \ucc38\uc870\ud558\uc2ed\uc2dc\uc624." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/no.json b/homeassistant/components/slack/translations/no.json new file mode 100644 index 00000000000..850d18ff5e0 --- /dev/null +++ b/homeassistant/components/slack/translations/no.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "default_channel": "Standard kanal", + "icon": "Ikon", + "username": "Brukernavn" + }, + "data_description": { + "api_key": "Slack API-tokenet som skal brukes til \u00e5 sende Slack-meldinger.", + "default_channel": "Kanalen det skal legges inn p\u00e5 hvis ingen kanal er angitt n\u00e5r du sender en melding.", + "icon": "Bruk en av Slack-emojiene som et ikon for det oppgitte brukernavnet.", + "username": "Home Assistant vil legge ut p\u00e5 Slack ved \u00e5 bruke det spesifiserte brukernavnet." + }, + "description": "Se dokumentasjonen for \u00e5 f\u00e5 Slack API-n\u00f8kkelen din." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/pl.json b/homeassistant/components/slack/translations/pl.json new file mode 100644 index 00000000000..3ce8f4bb311 --- /dev/null +++ b/homeassistant/components/slack/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + }, + "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", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/nl.json b/homeassistant/components/threshold/translations/nl.json index 73af8fcc8b7..c49b00eee0d 100644 --- a/homeassistant/components/threshold/translations/nl.json +++ b/homeassistant/components/threshold/translations/nl.json @@ -14,7 +14,7 @@ "upper": "Bovengrens" }, "description": "Maak een binaire sensor die aan en uit gaat afhankelijk van de waarde van een sensor\n\nAlleen ondergrens geconfigureerd - Schakelt in wanneer de waarde van de ingangssensor kleiner is dan de ondergrens.\nAlleen bovengrens geconfigureerd - Schakelt in wanneer de waarde van de ingangssensor groter is dan de bovengrens.\nZowel ondergrens als bovengrens ingesteld - Schakelt in wanneer de waarde van de ingangssensor binnen het bereik [ondergrens ... bovengrens] ligt.", - "title": "Drempelsensor toevoegen" + "title": "Drempelwaardesensor toevoegen" } } }, @@ -36,5 +36,5 @@ } } }, - "title": "Drempelsensor" + "title": "Drempelwaardesensor" } \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/bg.json b/homeassistant/components/ukraine_alarm/translations/bg.json index 68cd63e86b1..181f28745b9 100644 --- a/homeassistant/components/ukraine_alarm/translations/bg.json +++ b/homeassistant/components/ukraine_alarm/translations/bg.json @@ -1,7 +1,10 @@ { "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" + "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", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "max_regions": "\u041c\u043e\u0433\u0430\u0442 \u0434\u0430 \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442 \u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c 5 \u0440\u0435\u0433\u0438\u043e\u043d\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\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", @@ -26,7 +29,8 @@ }, "user": { "data": { - "api_key": "API \u043a\u043b\u044e\u0447" + "api_key": "API \u043a\u043b\u044e\u0447", + "region": "\u0420\u0435\u0433\u0438\u043e\u043d" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 Ukraine Alarm. \u0417\u0430 \u0434\u0430 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u0442\u0435 API \u043a\u043b\u044e\u0447, \u043e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 {api_url}" } diff --git a/homeassistant/components/zha/translations/fy.json b/homeassistant/components/zha/translations/fy.json new file mode 100644 index 00000000000..790851a469e --- /dev/null +++ b/homeassistant/components/zha/translations/fy.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "trigger_subtype": { + "turn_on": "Ynskeakelje" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json index 5922601921a..e82ccfe8476 100644 --- a/homeassistant/components/zwave_js/translations/it.json +++ b/homeassistant/components/zwave_js/translations/it.json @@ -9,7 +9,7 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "cannot_connect": "Impossibile connettersi", - "discovery_requires_supervisor": "Il rilevamento richiede il supervisore.", + "discovery_requires_supervisor": "Il rilevamento richiede il Supervisor.", "not_zwave_device": "Il dispositivo rilevato non \u00e8 un dispositivo Z-Wave." }, "error": { @@ -49,9 +49,9 @@ }, "on_supervisor": { "data": { - "use_addon": "Usa il componente aggiuntivo Z-Wave JS del supervisore" + "use_addon": "Usa il componente aggiuntivo Z-Wave JS del Supervisor" }, - "description": "Desideri utilizzare il componente aggiuntivo Z-Wave JS del supervisore?", + "description": "Desideri utilizzare il componente aggiuntivo Z-Wave JS del Supervisor?", "title": "Seleziona il metodo di connessione" }, "start_addon": { @@ -137,9 +137,9 @@ }, "on_supervisor": { "data": { - "use_addon": "Usa il componente aggiuntivo Z-Wave JS del supervisore" + "use_addon": "Usa il componente aggiuntivo Z-Wave JS di Supervisor" }, - "description": "Desideri utilizzare il componente aggiuntivo Z-Wave JS del supervisore?", + "description": "Desideri utilizzare il componente aggiuntivo Z-Wave JS del Supervisor?", "title": "Seleziona il metodo di connessione" }, "start_addon": { From 3de31939d8d7ff2626d3b52f40bd9dd0def6727b Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Mon, 16 May 2022 23:51:13 -0400 Subject: [PATCH 0563/3516] Refactor button code to allow for other button types for UniFi Protect (#71911) Co-authored-by: J. Nick Koston --- .../components/unifiprotect/__init__.py | 58 ++++++++++ .../components/unifiprotect/button.py | 61 ++++++---- tests/components/unifiprotect/test_button.py | 2 +- tests/components/unifiprotect/test_init.py | 105 +++++++++++++++++- 4 files changed, 205 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 97fdc6eac20..c28f2639e00 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -17,9 +17,11 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_create_clientsession from .const import ( @@ -27,6 +29,7 @@ from .const import ( CONF_OVERRIDE_CHOST, DEFAULT_SCAN_INTERVAL, DEVICES_FOR_SUBSCRIBE, + DEVICES_THAT_ADOPT, DOMAIN, MIN_REQUIRED_PROTECT_V, OUTDATED_LOG_MESSAGE, @@ -41,6 +44,60 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=DEFAULT_SCAN_INTERVAL) +async def _async_migrate_data( + hass: HomeAssistant, entry: ConfigEntry, protect: ProtectApiClient +) -> None: + + registry = er.async_get(hass) + to_migrate = [] + for entity in er.async_entries_for_config_entry(registry, entry.entry_id): + if entity.domain == Platform.BUTTON and "_" not in entity.unique_id: + _LOGGER.debug("Button %s needs migration", entity.entity_id) + to_migrate.append(entity) + + if len(to_migrate) == 0: + _LOGGER.debug("No entities need migration") + return + + _LOGGER.info("Migrating %s reboot button entities ", len(to_migrate)) + bootstrap = await protect.get_bootstrap() + count = 0 + for button in to_migrate: + device = None + for model in DEVICES_THAT_ADOPT: + attr = f"{model.value}s" + device = getattr(bootstrap, attr).get(button.unique_id) + if device is not None: + break + + if device is None: + continue + + new_unique_id = f"{device.id}_reboot" + _LOGGER.debug( + "Migrating entity %s (old unique_id: %s, new unique_id: %s)", + button.entity_id, + button.unique_id, + new_unique_id, + ) + try: + registry.async_update_entity(button.entity_id, new_unique_id=new_unique_id) + except ValueError: + _LOGGER.warning( + "Could not migrate entity %s (old unique_id: %s, new unique_id: %s)", + button.entity_id, + button.unique_id, + new_unique_id, + ) + else: + count += 1 + + if count < len(to_migrate): + _LOGGER.warning("Failed to migate %s reboot buttons", len(to_migrate) - count) + else: + _LOGGER.info("Migrated %s reboot button entities", count) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the UniFi Protect config entries.""" @@ -75,6 +132,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False + await _async_migrate_data(hass, entry, protect) if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=nvr_info.mac) diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 3940c85d21a..731b1eaf86a 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -1,20 +1,47 @@ """Support for Ubiquiti's UniFi Protect NVR.""" from __future__ import annotations -import logging +from dataclasses import dataclass +from typing import Final from pyunifiprotect.data.base import ProtectAdoptableDeviceModel -from homeassistant.components.button import ButtonDeviceClass, ButtonEntity +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DEVICES_THAT_ADOPT, DOMAIN +from .const import DOMAIN from .data import ProtectData -from .entity import ProtectDeviceEntity +from .entity import ProtectDeviceEntity, async_all_device_entities +from .models import ProtectSetableKeysMixin, T -_LOGGER = logging.getLogger(__name__) + +@dataclass +class ProtectButtonEntityDescription( + ProtectSetableKeysMixin[T], ButtonEntityDescription +): + """Describes UniFi Protect Button entity.""" + + ufp_press: str | None = None + + +DEVICE_CLASS_CHIME_BUTTON: Final = "unifiprotect__chime_button" + + +ALL_DEVICE_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( + ProtectButtonEntityDescription( + key="reboot", + entity_registry_enabled_default=False, + device_class=ButtonDeviceClass.RESTART, + name="Reboot Device", + ufp_press="reboot", + ), +) async def async_setup_entry( @@ -25,34 +52,30 @@ async def async_setup_entry( """Discover devices on a UniFi Protect NVR.""" data: ProtectData = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - [ - ProtectButton( - data, - device, - ) - for device in data.get_by_types(DEVICES_THAT_ADOPT) - ] + entities: list[ProtectDeviceEntity] = async_all_device_entities( + data, ProtectButton, all_descs=ALL_DEVICE_BUTTONS ) + async_add_entities(entities) + class ProtectButton(ProtectDeviceEntity, ButtonEntity): """A Ubiquiti UniFi Protect Reboot button.""" - _attr_entity_registry_enabled_default = False - _attr_device_class = ButtonDeviceClass.RESTART + entity_description: ProtectButtonEntityDescription def __init__( self, data: ProtectData, device: ProtectAdoptableDeviceModel, + description: ProtectButtonEntityDescription, ) -> None: """Initialize an UniFi camera.""" - super().__init__(data, device) - self._attr_name = f"{self.device.name} Reboot Device" + super().__init__(data, device, description) + self._attr_name = f"{self.device.name} {self.entity_description.name}" async def async_press(self) -> None: """Press the button.""" - _LOGGER.debug("Rebooting %s with id %s", self.device.model, self.device.id) - await self.device.reboot() + if self.entity_description.ufp_press is not None: + await getattr(self.device, self.entity_description.ufp_press)() diff --git a/tests/components/unifiprotect/test_button.py b/tests/components/unifiprotect/test_button.py index 0064781c6ce..64677dd1d77 100644 --- a/tests/components/unifiprotect/test_button.py +++ b/tests/components/unifiprotect/test_button.py @@ -49,7 +49,7 @@ async def test_button( mock_entry.api.reboot_device = AsyncMock() - unique_id = f"{camera[0].id}" + unique_id = f"{camera[0].id}_reboot" entity_id = camera[1] entity_registry = er.async_get(hass) diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 77bf900d87e..53588984e25 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -1,14 +1,17 @@ """Test the UniFi Protect setup flow.""" +# pylint: disable=protected-access from __future__ import annotations from unittest.mock import AsyncMock, patch from pyunifiprotect import NotAuthorized, NvrError -from pyunifiprotect.data import NVR +from pyunifiprotect.data import NVR, Light from homeassistant.components.unifiprotect.const import CONF_DISABLE_RTSP, DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from . import _patch_discovery from .conftest import MockBootstrap, MockEntityFixture @@ -175,3 +178,103 @@ async def test_setup_starts_discovery( assert mock_entry.entry.state == ConfigEntryState.LOADED await hass.async_block_till_done() assert len(hass.config_entries.flow.async_progress_by_handler(DOMAIN)) == 1 + + +async def test_migrate_reboot_button( + hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light +): + """Test migrating unique ID of reboot button.""" + + light1 = mock_light.copy() + light1._api = mock_entry.api + light1.name = "Test Light 1" + light1.id = "lightid1" + + light2 = mock_light.copy() + light2._api = mock_entry.api + light2.name = "Test Light 2" + light2.id = "lightid2" + mock_entry.api.bootstrap.lights = { + light1.id: light1, + light2.id: light2, + } + mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) + + registry = er.async_get(hass) + registry.async_get_or_create( + Platform.BUTTON, Platform.BUTTON, light1.id, config_entry=mock_entry.entry + ) + registry.async_get_or_create( + Platform.BUTTON, + Platform.BUTTON, + f"{light2.id}_reboot", + config_entry=mock_entry.entry, + ) + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + + assert mock_entry.entry.state == ConfigEntryState.LOADED + assert mock_entry.api.update.called + assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac + + assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device_2") is None + light = registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device") + assert light is not None + assert light.unique_id == f"{light1.id}_reboot" + + assert registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device_2") is None + light = registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device") + assert light is not None + assert light.unique_id == f"{light2.id}_reboot" + + buttons = [] + for entity in er.async_entries_for_config_entry( + registry, mock_entry.entry.entry_id + ): + if entity.platform == Platform.BUTTON.value: + buttons.append(entity) + assert len(buttons) == 2 + + +async def test_migrate_reboot_button_fail( + hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light +): + """Test migrating unique ID of reboot button.""" + + light1 = mock_light.copy() + light1._api = mock_entry.api + light1.name = "Test Light 1" + light1.id = "lightid1" + + mock_entry.api.bootstrap.lights = { + light1.id: light1, + } + mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) + + registry = er.async_get(hass) + registry.async_get_or_create( + Platform.BUTTON, + Platform.BUTTON, + light1.id, + config_entry=mock_entry.entry, + suggested_object_id=light1.name, + ) + registry.async_get_or_create( + Platform.BUTTON, + Platform.BUTTON, + f"{light1.id}_reboot", + config_entry=mock_entry.entry, + suggested_object_id=light1.name, + ) + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + + assert mock_entry.entry.state == ConfigEntryState.LOADED + assert mock_entry.api.update.called + assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac + + light = registry.async_get(f"{Platform.BUTTON}.test_light_1") + assert light is not None + assert light.unique_id == f"{light1.id}" From 0608506bac7a57be28fd274cfaaba8785fbbf96f Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 17 May 2022 11:52:48 +0800 Subject: [PATCH 0564/3516] Reuse codec_context on stream thread restart (#71942) --- homeassistant/components/stream/core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 8db6a239818..8c0b867752e 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -395,6 +395,9 @@ class KeyFrameConverter: This is run by the worker thread and will only be called once per worker. """ + if self._codec_context: + return + # Keep import here so that we can import stream integration without installing reqs # pylint: disable=import-outside-toplevel from av import CodecContext From 99f68ab85848cd89ee2aae0a7570d06bf9ee02ed Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 May 2022 06:04:59 +0200 Subject: [PATCH 0565/3516] Update dsmr_parser to v0.33 (#71946) --- homeassistant/components/dsmr/const.py | 2 +- homeassistant/components/dsmr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/dsmr/test_sensor.py | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index 2533aa8d025..43c0e66e945 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -271,7 +271,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( - key=obis_references.BELGIUM_HOURLY_GAS_METER_READING, + key=obis_references.BELGIUM_5MIN_GAS_METER_READING, name="Gas Consumption", dsmr_versions={"5B"}, is_gas=True, diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index e15a7c3b80a..b086944a4d2 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -2,7 +2,7 @@ "domain": "dsmr", "name": "DSMR Slimme Meter", "documentation": "https://www.home-assistant.io/integrations/dsmr", - "requirements": ["dsmr_parser==0.32"], + "requirements": ["dsmr_parser==0.33"], "codeowners": ["@Robbie1221", "@frenck"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 027b6406b2a..a7c59f1853d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -560,7 +560,7 @@ doorbirdpy==2.1.0 dovado==0.4.1 # homeassistant.components.dsmr -dsmr_parser==0.32 +dsmr_parser==0.33 # homeassistant.components.dwd_weather_warnings dwdwfsapi==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9949ba3b2c..f9926797e6f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -415,7 +415,7 @@ discovery30303==0.2.1 doorbirdpy==2.1.0 # homeassistant.components.dsmr -dsmr_parser==0.32 +dsmr_parser==0.33 # homeassistant.components.dynalite dynalite_devices==0.1.46 diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 93dd78034cc..4502f61586a 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -393,7 +393,7 @@ async def test_belgian_meter(hass, dsmr_connection_fixture): (connection_factory, transport, protocol) = dsmr_connection_fixture from dsmr_parser.obis_references import ( - BELGIUM_HOURLY_GAS_METER_READING, + BELGIUM_5MIN_GAS_METER_READING, ELECTRICITY_ACTIVE_TARIFF, ) from dsmr_parser.objects import CosemObject, MBusObject @@ -411,7 +411,7 @@ async def test_belgian_meter(hass, dsmr_connection_fixture): } telegram = { - BELGIUM_HOURLY_GAS_METER_READING: MBusObject( + BELGIUM_5MIN_GAS_METER_READING: MBusObject( [ {"value": datetime.datetime.fromtimestamp(1551642213)}, {"value": Decimal(745.695), "unit": "m3"}, From ddecf76f6f301f0240c012ed73adf3699ec2d6a5 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 16 May 2022 21:09:48 -0700 Subject: [PATCH 0566/3516] Add application_credentials platform for netatmo (#71884) --- homeassistant/components/netatmo/__init__.py | 43 +++++++++++-------- .../netatmo/application_credentials.py | 14 ++++++ .../components/netatmo/manifest.json | 2 +- .../generated/application_credentials.py | 1 + 4 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/netatmo/application_credentials.py diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index f6e43b29653..a3a9d22e017 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -11,6 +11,10 @@ import pyatmo import voluptuous as vol from homeassistant.components import cloud +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.components.webhook import ( async_generate_url as webhook_generate_url, async_register as webhook_register, @@ -35,7 +39,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType -from . import api, config_flow +from . import api from .const import ( AUTH, CONF_CLOUDHOOK_URL, @@ -48,8 +52,6 @@ from .const import ( DATA_SCHEDULES, DOMAIN, NETATMO_SCOPES, - OAUTH2_AUTHORIZE, - OAUTH2_TOKEN, PLATFORMS, WEBHOOK_DEACTIVATION, WEBHOOK_PUSH_TYPE, @@ -60,14 +62,17 @@ from .webhook import async_handle_webhook _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -88,17 +93,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if DOMAIN not in config: return True - config_flow.NetatmoFlowHandler.async_register_implementation( + await async_import_client_credential( hass, - config_entry_oauth2_flow.LocalOAuth2Implementation( - hass, - DOMAIN, + DOMAIN, + ClientCredential( config[DOMAIN][CONF_CLIENT_ID], config[DOMAIN][CONF_CLIENT_SECRET], - OAUTH2_AUTHORIZE, - OAUTH2_TOKEN, ), ) + _LOGGER.warning( + "Configuration of Netatmo integration in YAML is deprecated and " + "will be removed in a future release; Your existing configuration " + "(including OAuth Application Credentials) have been imported into " + "the UI automatically and can be safely removed from your " + "configuration.yaml file" + ) return True diff --git a/homeassistant/components/netatmo/application_credentials.py b/homeassistant/components/netatmo/application_credentials.py new file mode 100644 index 00000000000..5536343ebe6 --- /dev/null +++ b/homeassistant/components/netatmo/application_credentials.py @@ -0,0 +1,14 @@ +"""Application credentials platform for Netatmo.""" + +from homeassistant.components.application_credentials import AuthorizationServer +from homeassistant.core import HomeAssistant + +from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN + + +async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: + """Return authorization server.""" + return AuthorizationServer( + authorize_url=OAUTH2_AUTHORIZE, + token_url=OAUTH2_TOKEN, + ) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 565bd42594a..2081f9bd274 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": ["pyatmo==6.2.4"], "after_dependencies": ["cloud", "media_source"], - "dependencies": ["auth", "webhook"], + "dependencies": ["application_credentials", "webhook"], "codeowners": ["@cgtobi"], "config_flow": true, "homekit": { diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index 56340ccc44e..40f451de153 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -8,6 +8,7 @@ To update, run python3 -m script.hassfest APPLICATION_CREDENTIALS = [ "geocaching", "google", + "netatmo", "spotify", "xbox" ] From 78f0716574dbb7f6619b570489f9d34e939a4031 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 May 2022 01:22:56 -0500 Subject: [PATCH 0567/3516] Add support for specifying the integrations manifest/list fetches (#71982) * Add support for specifying the integrations manifest/list fetches See https://github.com/home-assistant/core/pull/71979 for the motovation * Update homeassistant/components/websocket_api/commands.py Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- .../components/websocket_api/commands.py | 10 +++++++--- .../components/websocket_api/test_commands.py | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index edec628fd2c..61bcb8badf0 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -359,15 +359,19 @@ def handle_get_config( connection.send_result(msg["id"], hass.config.as_dict()) -@decorators.websocket_command({vol.Required("type"): "manifest/list"}) +@decorators.websocket_command( + {vol.Required("type"): "manifest/list", vol.Optional("integrations"): [str]} +) @decorators.async_response async def handle_manifest_list( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Handle integrations command.""" - loaded_integrations = async_get_loaded_integrations(hass) + wanted_integrations = msg.get("integrations") + if wanted_integrations is None: + wanted_integrations = async_get_loaded_integrations(hass) integrations = await asyncio.gather( - *(async_get_integration(hass, domain) for domain in loaded_integrations) + *(async_get_integration(hass, domain) for domain in wanted_integrations) ) connection.send_result( msg["id"], [integration.manifest for integration in integrations] diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 007e130ff6f..4d3302f7c13 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1351,6 +1351,25 @@ async def test_manifest_list(hass, websocket_client): ] +async def test_manifest_list_specific_integrations(hass, websocket_client): + """Test loading manifests for specific integrations.""" + websocket_api = await async_get_integration(hass, "websocket_api") + + await websocket_client.send_json( + {"id": 5, "type": "manifest/list", "integrations": ["hue", "websocket_api"]} + ) + hue = await async_get_integration(hass, "hue") + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert sorted(msg["result"], key=lambda manifest: manifest["domain"]) == [ + hue.manifest, + websocket_api.manifest, + ] + + async def test_manifest_get(hass, websocket_client): """Test getting a manifest.""" hue = await async_get_integration(hass, "hue") From a614ddca287c8a7ebf2a2c0336bfb5a8b55d20ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 May 2022 01:23:11 -0500 Subject: [PATCH 0568/3516] Support requesting translations for multiple integrations in a single request (#71979) --- homeassistant/components/frontend/__init__.py | 2 +- homeassistant/components/onboarding/views.py | 5 +- homeassistant/helpers/translation.py | 13 +++-- tests/components/frontend/test_init.py | 54 ++++++++++++++++++- tests/helpers/test_translation.py | 29 +++++++--- 5 files changed, 88 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 0c540a477ef..91b9c9a126f 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -667,7 +667,7 @@ def websocket_get_themes( "type": "frontend/get_translations", vol.Required("language"): str, vol.Required("category"): str, - vol.Optional("integration"): str, + vol.Optional("integration"): vol.All(cv.ensure_list, [str]), vol.Optional("config_flow"): bool, } ) diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index b277bd97edf..07b3dfa9cc2 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -15,6 +15,7 @@ from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView from homeassistant.core import callback from homeassistant.helpers.system_info import async_get_system_info +from homeassistant.helpers.translation import async_get_translations from .const import ( DEFAULT_AREAS, @@ -147,8 +148,8 @@ class UserOnboardingView(_BaseOnboardingView): await person.async_create_person(hass, data["name"], user_id=user.id) # Create default areas using the users supplied language. - translations = await hass.helpers.translation.async_get_translations( - data["language"], "area", DOMAIN + translations = await async_get_translations( + hass, data["language"], "area", {DOMAIN} ) area_registry = await hass.helpers.area_registry.async_get_registry() diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 976c66dda56..cda50de535b 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from collections import ChainMap -from collections.abc import Mapping +from collections.abc import Iterable, Mapping import logging from typing import Any @@ -286,7 +286,7 @@ async def async_get_translations( hass: HomeAssistant, language: str, category: str, - integration: str | None = None, + integrations: Iterable[str] | None = None, config_flow: bool | None = None, ) -> dict[str, Any]: """Return all backend translations. @@ -297,8 +297,8 @@ async def async_get_translations( """ lock = hass.data.setdefault(TRANSLATION_LOAD_LOCK, asyncio.Lock()) - if integration is not None: - components = {integration} + if integrations is not None: + components = set(integrations) elif config_flow: components = (await async_get_config_flows(hass)) - hass.config.components elif category == "state": @@ -310,7 +310,10 @@ async def async_get_translations( } async with lock: - cache = hass.data.setdefault(TRANSLATION_FLATTEN_CACHE, _TranslationCache(hass)) + if TRANSLATION_FLATTEN_CACHE in hass.data: + cache = hass.data[TRANSLATION_FLATTEN_CACHE] + else: + cache = hass.data[TRANSLATION_FLATTEN_CACHE] = _TranslationCache(hass) cached = await cache.async_fetch(language, category, components) return dict(ChainMap(*cached)) diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 8c8fd3bf671..84ca04df3ba 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -424,7 +424,7 @@ async def test_get_translations(hass, ws_client): """Test get_translations command.""" with patch( "homeassistant.components.frontend.async_get_translations", - side_effect=lambda hass, lang, category, integration, config_flow: { + side_effect=lambda hass, lang, category, integrations, config_flow: { "lang": lang }, ): @@ -444,6 +444,58 @@ async def test_get_translations(hass, ws_client): assert msg["result"] == {"resources": {"lang": "nl"}} +async def test_get_translations_for_integrations(hass, ws_client): + """Test get_translations for integrations command.""" + with patch( + "homeassistant.components.frontend.async_get_translations", + side_effect=lambda hass, lang, category, integration, config_flow: { + "lang": lang, + "integration": integration, + }, + ): + await ws_client.send_json( + { + "id": 5, + "type": "frontend/get_translations", + "integration": ["frontend", "http"], + "language": "nl", + "category": "lang", + } + ) + msg = await ws_client.receive_json() + + assert msg["id"] == 5 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + assert set(msg["result"]["resources"]["integration"]) == {"frontend", "http"} + + +async def test_get_translations_for_single_integration(hass, ws_client): + """Test get_translations for integration command.""" + with patch( + "homeassistant.components.frontend.async_get_translations", + side_effect=lambda hass, lang, category, integrations, config_flow: { + "lang": lang, + "integration": integrations, + }, + ): + await ws_client.send_json( + { + "id": 5, + "type": "frontend/get_translations", + "integration": "http", + "language": "nl", + "category": "lang", + } + ) + msg = await ws_client.receive_json() + + assert msg["id"] == 5 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + assert msg["result"] == {"resources": {"lang": "nl", "integration": ["http"]}} + + async def test_auth_load(hass): """Test auth component loaded by default.""" frontend = await async_get_integration(hass, "frontend") diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 5520269ca9d..2e30a649a7b 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -290,12 +290,29 @@ async def test_translation_merging_loaded_apart(hass, caplog): assert "component.sensor.state.moon__phase.first_quarter" in translations translations = await translation.async_get_translations( - hass, "en", "state", integration="sensor" + hass, "en", "state", integrations={"sensor"} ) assert "component.sensor.state.moon__phase.first_quarter" in translations +async def test_translation_merging_loaded_together(hass, caplog): + """Test we merge translations of two integrations when they are loaded at the same time.""" + hass.config.components.add("hue") + hass.config.components.add("homekit") + hue_translations = await translation.async_get_translations( + hass, "en", "config", integrations={"hue"} + ) + homekit_translations = await translation.async_get_translations( + hass, "en", "config", integrations={"homekit"} + ) + + translations = await translation.async_get_translations( + hass, "en", "config", integrations={"hue", "homekit"} + ) + assert translations == hue_translations | homekit_translations + + async def test_caching(hass): """Test we cache data.""" hass.config.components.add("sensor") @@ -320,14 +337,14 @@ async def test_caching(hass): ) load_sensor_only = await translation.async_get_translations( - hass, "en", "state", integration="sensor" + hass, "en", "state", integrations={"sensor"} ) assert load_sensor_only for key in load_sensor_only: assert key.startswith("component.sensor.state.") load_light_only = await translation.async_get_translations( - hass, "en", "state", integration="light" + hass, "en", "state", integrations={"light"} ) assert load_light_only for key in load_light_only: @@ -341,7 +358,7 @@ async def test_caching(hass): side_effect=translation._build_resources, ) as mock_build: load_sensor_only = await translation.async_get_translations( - hass, "en", "title", integration="sensor" + hass, "en", "title", integrations={"sensor"} ) assert load_sensor_only for key in load_sensor_only: @@ -349,12 +366,12 @@ async def test_caching(hass): assert len(mock_build.mock_calls) == 0 assert await translation.async_get_translations( - hass, "en", "title", integration="sensor" + hass, "en", "title", integrations={"sensor"} ) assert len(mock_build.mock_calls) == 0 load_light_only = await translation.async_get_translations( - hass, "en", "title", integration="media_player" + hass, "en", "title", integrations={"media_player"} ) assert load_light_only for key in load_light_only: From 513e276bbaba4c1d750cad4253db5c79fb341ec5 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 17 May 2022 08:32:28 +0200 Subject: [PATCH 0569/3516] Avoid polling fjaraskupan if no broadcast is received (#71969) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Switch to subclassed coordinator * Avoid polling fjäråskupan only do on request We still want to timeout the availability if no data was received. * Remove unused variable * Update homeassistant/components/fjaraskupan/__init__.py Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- .../components/fjaraskupan/__init__.py | 91 ++++++++++++------- .../components/fjaraskupan/binary_sensor.py | 21 ++--- homeassistant/components/fjaraskupan/fan.py | 17 ++-- homeassistant/components/fjaraskupan/light.py | 21 ++--- .../components/fjaraskupan/number.py | 19 ++-- .../components/fjaraskupan/sensor.py | 21 ++--- 6 files changed, 92 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 64962a746f7..488139b080b 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -20,7 +20,7 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DISPATCH_DETECTION, DOMAIN @@ -35,13 +35,48 @@ PLATFORMS = [ _LOGGER = logging.getLogger(__name__) -@dataclass -class DeviceState: - """Store state of a device.""" +class Coordinator(DataUpdateCoordinator[State]): + """Update coordinator for each device.""" - device: Device - coordinator: DataUpdateCoordinator[State] - device_info: DeviceInfo + def __init__( + self, hass: HomeAssistant, device: Device, device_info: DeviceInfo + ) -> None: + """Initialize the coordinator.""" + self.device = device + self.device_info = device_info + self._refresh_was_scheduled = False + + super().__init__( + hass, _LOGGER, name="Fjäråskupan", update_interval=timedelta(seconds=120) + ) + + async def _async_refresh( + self, + log_failures: bool = True, + raise_on_auth_failed: bool = False, + scheduled: bool = False, + ) -> None: + self._refresh_was_scheduled = scheduled + await super()._async_refresh( + log_failures=log_failures, + raise_on_auth_failed=raise_on_auth_failed, + scheduled=scheduled, + ) + + async def _async_update_data(self) -> State: + """Handle an explicit update request.""" + if self._refresh_was_scheduled: + raise UpdateFailed("No data received within schedule.") + + await self.device.update() + return self.device.state + + def detection_callback( + self, ble_device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a new announcement of data.""" + self.device.detection_callback(ble_device, advertisement_data) + self.async_set_updated_data(self.device.state) @dataclass @@ -49,7 +84,7 @@ class EntryState: """Store state of config entry.""" scanner: BleakScanner - devices: dict[str, DeviceState] + coordinators: dict[str, Coordinator] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -64,13 +99,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def detection_callback( ble_device: BLEDevice, advertisement_data: AdvertisementData ) -> None: - if data := state.devices.get(ble_device.address): + if data := state.coordinators.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) + data.detection_callback(ble_device, advertisement_data) else: if not device_filter(ble_device, advertisement_data): return @@ -80,31 +114,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) device = Device(ble_device) - device.detection_callback(ble_device, advertisement_data) - - async def async_update_data(): - """Handle an explicit update request.""" - await device.update() - return device.state - - coordinator: DataUpdateCoordinator[State] = DataUpdateCoordinator( - hass, - logger=_LOGGER, - name="Fjaraskupan Updater", - update_interval=timedelta(seconds=120), - update_method=async_update_data, - ) - coordinator.async_set_updated_data(device.state) - 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 + + coordinator: Coordinator = Coordinator(hass, device, device_info) + coordinator.detection_callback(ble_device, advertisement_data) + + state.coordinators[ble_device.address] = coordinator async_dispatcher_send( - hass, f"{DISPATCH_DETECTION}.{entry.entry_id}", device_state + hass, f"{DISPATCH_DETECTION}.{entry.entry_id}", coordinator ) scanner.register_detection_callback(detection_callback) @@ -119,20 +140,20 @@ def async_setup_entry_platform( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - constructor: Callable[[DeviceState], list[Entity]], + constructor: Callable[[Coordinator], list[Entity]], ) -> None: """Set up a platform with added entities.""" entry_state: EntryState = hass.data[DOMAIN][entry.entry_id] async_add_entities( entity - for device_state in entry_state.devices.values() - for entity in constructor(device_state) + for coordinator in entry_state.coordinators.values() + for entity in constructor(coordinator) ) @callback - def _detection(device_state: DeviceState) -> None: - async_add_entities(constructor(device_state)) + def _detection(coordinator: Coordinator) -> None: + async_add_entities(constructor(coordinator)) entry.async_on_unload( async_dispatcher_connect( diff --git a/homeassistant/components/fjaraskupan/binary_sensor.py b/homeassistant/components/fjaraskupan/binary_sensor.py index f1672530c45..ef4f64c5ecd 100644 --- a/homeassistant/components/fjaraskupan/binary_sensor.py +++ b/homeassistant/components/fjaraskupan/binary_sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from fjaraskupan import Device, State +from fjaraskupan import Device from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -15,12 +15,9 @@ from homeassistant.config_entries import ConfigEntry 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 homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DeviceState, async_setup_entry_platform +from . import Coordinator, async_setup_entry_platform @dataclass @@ -53,12 +50,12 @@ async def async_setup_entry( ) -> None: """Set up sensors dynamically through discovery.""" - def _constructor(device_state: DeviceState) -> list[Entity]: + def _constructor(coordinator: Coordinator) -> list[Entity]: return [ BinarySensor( - device_state.coordinator, - device_state.device, - device_state.device_info, + coordinator, + coordinator.device, + coordinator.device_info, entity_description, ) for entity_description in SENSORS @@ -67,14 +64,14 @@ async def async_setup_entry( async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) -class BinarySensor(CoordinatorEntity[DataUpdateCoordinator[State]], BinarySensorEntity): +class BinarySensor(CoordinatorEntity[Coordinator], BinarySensorEntity): """Grease filter sensor.""" entity_description: EntityDescription def __init__( self, - coordinator: DataUpdateCoordinator[State], + coordinator: Coordinator, device: Device, device_info: DeviceInfo, entity_description: EntityDescription, diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index a8f8e13f3da..fcd95090400 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -16,16 +16,13 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.percentage import ( ordered_list_item_to_percentage, percentage_to_ordered_list_item, ) -from . import DeviceState, async_setup_entry_platform +from . import Coordinator, async_setup_entry_platform ORDERED_NAMED_FAN_SPEEDS = ["1", "2", "3", "4", "5", "6", "7", "8"] @@ -58,22 +55,20 @@ async def async_setup_entry( ) -> None: """Set up sensors dynamically through discovery.""" - def _constructor(device_state: DeviceState): - return [ - Fan(device_state.coordinator, device_state.device, device_state.device_info) - ] + def _constructor(coordinator: Coordinator): + return [Fan(coordinator, coordinator.device, coordinator.device_info)] async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) -class Fan(CoordinatorEntity[DataUpdateCoordinator[State]], FanEntity): +class Fan(CoordinatorEntity[Coordinator], FanEntity): """Fan entity.""" _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE def __init__( self, - coordinator: DataUpdateCoordinator[State], + coordinator: Coordinator, device: Device, device_info: DeviceInfo, ) -> None: diff --git a/homeassistant/components/fjaraskupan/light.py b/homeassistant/components/fjaraskupan/light.py index 6c3e6e458f8..9d52c5cac82 100644 --- a/homeassistant/components/fjaraskupan/light.py +++ b/homeassistant/components/fjaraskupan/light.py @@ -1,19 +1,16 @@ """Support for lights.""" from __future__ import annotations -from fjaraskupan import COMMAND_LIGHT_ON_OFF, Device, State +from fjaraskupan import COMMAND_LIGHT_ON_OFF, Device from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DeviceState, async_setup_entry_platform +from . import Coordinator, async_setup_entry_platform async def async_setup_entry( @@ -23,22 +20,18 @@ async def async_setup_entry( ) -> None: """Set up tuya sensors dynamically through tuya discovery.""" - def _constructor(device_state: DeviceState) -> list[Entity]: - return [ - Light( - device_state.coordinator, device_state.device, device_state.device_info - ) - ] + def _constructor(coordinator: Coordinator) -> list[Entity]: + return [Light(coordinator, coordinator.device, coordinator.device_info)] async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) -class Light(CoordinatorEntity[DataUpdateCoordinator[State]], LightEntity): +class Light(CoordinatorEntity[Coordinator], LightEntity): """Light device.""" def __init__( self, - coordinator: DataUpdateCoordinator[State], + coordinator: Coordinator, device: Device, device_info: DeviceInfo, ) -> None: diff --git a/homeassistant/components/fjaraskupan/number.py b/homeassistant/components/fjaraskupan/number.py index bbde9bd8898..6314b9c9cc1 100644 --- a/homeassistant/components/fjaraskupan/number.py +++ b/homeassistant/components/fjaraskupan/number.py @@ -1,7 +1,7 @@ """Support for sensors.""" from __future__ import annotations -from fjaraskupan import Device, State +from fjaraskupan import Device from homeassistant.components.number import NumberEntity from homeassistant.config_entries import ConfigEntry @@ -9,12 +9,9 @@ from homeassistant.const import TIME_MINUTES from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, Entity, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DeviceState, async_setup_entry_platform +from . import Coordinator, async_setup_entry_platform async def async_setup_entry( @@ -24,19 +21,17 @@ async def async_setup_entry( ) -> None: """Set up number entities dynamically through discovery.""" - def _constructor(device_state: DeviceState) -> list[Entity]: + def _constructor(coordinator: Coordinator) -> list[Entity]: return [ PeriodicVentingTime( - device_state.coordinator, device_state.device, device_state.device_info + coordinator, coordinator.device, coordinator.device_info ), ] async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) -class PeriodicVentingTime( - CoordinatorEntity[DataUpdateCoordinator[State]], NumberEntity -): +class PeriodicVentingTime(CoordinatorEntity[Coordinator], NumberEntity): """Periodic Venting.""" _attr_max_value: float = 59 @@ -47,7 +42,7 @@ class PeriodicVentingTime( def __init__( self, - coordinator: DataUpdateCoordinator[State], + coordinator: Coordinator, device: Device, device_info: DeviceInfo, ) -> None: diff --git a/homeassistant/components/fjaraskupan/sensor.py b/homeassistant/components/fjaraskupan/sensor.py index fbd9d5f6d08..e4dbffab38e 100644 --- a/homeassistant/components/fjaraskupan/sensor.py +++ b/homeassistant/components/fjaraskupan/sensor.py @@ -1,7 +1,7 @@ """Support for sensors.""" from __future__ import annotations -from fjaraskupan import Device, State +from fjaraskupan import Device from homeassistant.components.sensor import ( SensorDeviceClass, @@ -14,12 +14,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, Entity, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DeviceState, async_setup_entry_platform +from . import Coordinator, async_setup_entry_platform async def async_setup_entry( @@ -29,22 +26,18 @@ async def async_setup_entry( ) -> None: """Set up sensors dynamically through discovery.""" - def _constructor(device_state: DeviceState) -> list[Entity]: - return [ - RssiSensor( - device_state.coordinator, device_state.device, device_state.device_info - ) - ] + def _constructor(coordinator: Coordinator) -> list[Entity]: + return [RssiSensor(coordinator, coordinator.device, coordinator.device_info)] async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) -class RssiSensor(CoordinatorEntity[DataUpdateCoordinator[State]], SensorEntity): +class RssiSensor(CoordinatorEntity[Coordinator], SensorEntity): """Sensor device.""" def __init__( self, - coordinator: DataUpdateCoordinator[State], + coordinator: Coordinator, device: Device, device_info: DeviceInfo, ) -> None: From e0154d6fb1983a9d3156c8cb410a2eff8b61c4dd Mon Sep 17 00:00:00 2001 From: Matrix Date: Tue, 17 May 2022 15:59:39 +0800 Subject: [PATCH 0570/3516] Add YoLink product integration (#69167) * add yolink integration with door sensor * Add test flow and update .coveragerc * Yolink integration Bug fix * resovle test flow * issues resolve * issues resolve * issues resolve * resolve issues * issues resolve * issues resolve * change .coveragerc and test_flow * Update yolink api version * add test for config entry * change config flow and add test cases * remove config entry data * Add token check for re-auth test flow * fix test flow issues * Add application credentials * Add alias for application_credentials * support application credentials and cloud account linking * fix suggest change --- .coveragerc | 6 + CODEOWNERS | 2 + homeassistant/components/yolink/__init__.py | 64 ++++++ homeassistant/components/yolink/api.py | 30 +++ .../yolink/application_credentials.py | 14 ++ .../components/yolink/config_flow.py | 63 ++++++ homeassistant/components/yolink/const.py | 16 ++ .../components/yolink/coordinator.py | 109 +++++++++ homeassistant/components/yolink/entity.py | 52 +++++ homeassistant/components/yolink/manifest.json | 10 + homeassistant/components/yolink/sensor.py | 104 +++++++++ homeassistant/components/yolink/strings.json | 25 +++ .../components/yolink/translations/en.json | 25 +++ .../generated/application_credentials.py | 3 +- homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/yolink/__init__.py | 1 + tests/components/yolink/test_config_flow.py | 210 ++++++++++++++++++ 19 files changed, 740 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/yolink/__init__.py create mode 100644 homeassistant/components/yolink/api.py create mode 100644 homeassistant/components/yolink/application_credentials.py create mode 100644 homeassistant/components/yolink/config_flow.py create mode 100644 homeassistant/components/yolink/const.py create mode 100644 homeassistant/components/yolink/coordinator.py create mode 100644 homeassistant/components/yolink/entity.py create mode 100644 homeassistant/components/yolink/manifest.json create mode 100644 homeassistant/components/yolink/sensor.py create mode 100644 homeassistant/components/yolink/strings.json create mode 100644 homeassistant/components/yolink/translations/en.json create mode 100644 tests/components/yolink/__init__.py create mode 100644 tests/components/yolink/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 12aa3972f1a..da12b41d222 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1467,6 +1467,12 @@ omit = homeassistant/components/yandex_transport/* homeassistant/components/yeelightsunflower/light.py homeassistant/components/yi/camera.py + homeassistant/components/yolink/__init__.py + homeassistant/components/yolink/api.py + homeassistant/components/yolink/const.py + homeassistant/components/yolink/coordinator.py + homeassistant/components/yolink/entity.py + homeassistant/components/yolink/sensor.py homeassistant/components/youless/__init__.py homeassistant/components/youless/const.py homeassistant/components/youless/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 0805b17cff5..a1d329cbee0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1201,6 +1201,8 @@ build.json @home-assistant/supervisor /tests/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015 /homeassistant/components/yeelightsunflower/ @lindsaymarkward /homeassistant/components/yi/ @bachya +/homeassistant/components/yolink/ @YoSmart-Inc +/tests/components/yolink/ @YoSmart-Inc /homeassistant/components/youless/ @gjong /tests/components/youless/ @gjong /homeassistant/components/zengge/ @emontnemery diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py new file mode 100644 index 00000000000..d31a082f82f --- /dev/null +++ b/homeassistant/components/yolink/__init__.py @@ -0,0 +1,64 @@ +"""The yolink integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from yolink.client import YoLinkClient +from yolink.mqtt_client import MqttClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow + +from . import api +from .const import ATTR_CLIENT, ATTR_COORDINATOR, ATTR_MQTT_CLIENT, DOMAIN +from .coordinator import YoLinkCoordinator + +SCAN_INTERVAL = timedelta(minutes=5) + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up yolink from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + ) + + session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + + auth_mgr = api.ConfigEntryAuth( + hass, aiohttp_client.async_get_clientsession(hass), session + ) + + yolink_http_client = YoLinkClient(auth_mgr) + yolink_mqtt_client = MqttClient(auth_mgr) + coordinator = YoLinkCoordinator(hass, yolink_http_client, yolink_mqtt_client) + await coordinator.init_coordinator() + try: + await coordinator.async_config_entry_first_refresh() + except ConfigEntryNotReady as ex: + _LOGGER.error("Fetching initial data failed: %s", ex) + + hass.data[DOMAIN][entry.entry_id] = { + ATTR_CLIENT: yolink_http_client, + ATTR_MQTT_CLIENT: yolink_mqtt_client, + ATTR_COORDINATOR: coordinator, + } + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/yolink/api.py b/homeassistant/components/yolink/api.py new file mode 100644 index 00000000000..0991baed23f --- /dev/null +++ b/homeassistant/components/yolink/api.py @@ -0,0 +1,30 @@ +"""API for yolink bound to Home Assistant OAuth.""" +from aiohttp import ClientSession +from yolink.auth_mgr import YoLinkAuthMgr + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow + + +class ConfigEntryAuth(YoLinkAuthMgr): + """Provide yolink authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + hass: HomeAssistant, + websession: ClientSession, + oauth2Session: config_entry_oauth2_flow.OAuth2Session, + ) -> None: + """Initialize yolink Auth.""" + self.hass = hass + self.oauth_session = oauth2Session + super().__init__(websession) + + def access_token(self) -> str: + """Return the access token.""" + return self.oauth_session.token["access_token"] + + async def check_and_refresh_token(self) -> str: + """Check the token.""" + await self.oauth_session.async_ensure_token_valid() + return self.access_token() diff --git a/homeassistant/components/yolink/application_credentials.py b/homeassistant/components/yolink/application_credentials.py new file mode 100644 index 00000000000..f8378299952 --- /dev/null +++ b/homeassistant/components/yolink/application_credentials.py @@ -0,0 +1,14 @@ +"""Application credentials platform for yolink.""" + +from yolink.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN + +from homeassistant.components.application_credentials import AuthorizationServer +from homeassistant.core import HomeAssistant + + +async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: + """Return authorization server.""" + return AuthorizationServer( + authorize_url=OAUTH2_AUTHORIZE, + token_url=OAUTH2_TOKEN, + ) diff --git a/homeassistant/components/yolink/config_flow.py b/homeassistant/components/yolink/config_flow.py new file mode 100644 index 00000000000..35a4c4ebea8 --- /dev/null +++ b/homeassistant/components/yolink/config_flow.py @@ -0,0 +1,63 @@ +"""Config flow for yolink.""" +from __future__ import annotations + +import logging +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_entry_oauth2_flow + +from .const import DOMAIN + + +class OAuth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle yolink OAuth2 authentication.""" + + DOMAIN = DOMAIN + _reauth_entry: ConfigEntry | None = None + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + scopes = ["create"] + return {"scope": " ".join(scopes)} + + async def async_step_reauth(self, user_input=None) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=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") + 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.""" + if existing_entry := self._reauth_entry: + self.hass.config_entries.async_update_entry( + existing_entry, data=existing_entry.data | data + ) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + return self.async_create_entry(title="YoLink", data=data) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow start.""" + existing_entry = await self.async_set_unique_id(DOMAIN) + if existing_entry and not self._reauth_entry: + return self.async_abort(reason="already_configured") + return await super().async_step_user(user_input) diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py new file mode 100644 index 00000000000..1804838e8b3 --- /dev/null +++ b/homeassistant/components/yolink/const.py @@ -0,0 +1,16 @@ +"""Constants for the yolink integration.""" + +DOMAIN = "yolink" +MANUFACTURER = "YoLink" +HOME_ID = "homeId" +HOME_SUBSCRIPTION = "home_subscription" +ATTR_PLATFORM_SENSOR = "sensor" +ATTR_COORDINATOR = "coordinator" +ATTR_DEVICE = "devices" +ATTR_DEVICE_TYPE = "type" +ATTR_DEVICE_NAME = "name" +ATTR_DEVICE_STATE = "state" +ATTR_CLIENT = "client" +ATTR_MQTT_CLIENT = "mqtt_client" +ATTR_DEVICE_ID = "deviceId" +ATTR_DEVICE_DOOR_SENSOR = "DoorSensor" diff --git a/homeassistant/components/yolink/coordinator.py b/homeassistant/components/yolink/coordinator.py new file mode 100644 index 00000000000..e5578eae4b2 --- /dev/null +++ b/homeassistant/components/yolink/coordinator.py @@ -0,0 +1,109 @@ +"""YoLink DataUpdateCoordinator.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +import logging + +import async_timeout +from yolink.client import YoLinkClient +from yolink.device import YoLinkDevice +from yolink.exception import YoLinkAuthFailError, YoLinkClientError +from yolink.model import BRDP +from yolink.mqtt_client import MqttClient + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ATTR_DEVICE, ATTR_DEVICE_STATE, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class YoLinkCoordinator(DataUpdateCoordinator[dict]): + """YoLink DataUpdateCoordinator.""" + + def __init__( + self, hass: HomeAssistant, yl_client: YoLinkClient, yl_mqtt_client: MqttClient + ) -> None: + """Init YoLink DataUpdateCoordinator. + + fetch state every 30 minutes base on yolink device heartbeat interval + data is None before the first successful update, but we need to use data at first update + """ + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=30) + ) + self._client = yl_client + self._mqtt_client = yl_mqtt_client + self.yl_devices: list[YoLinkDevice] = [] + self.data = {} + + def on_message_callback(self, message: tuple[str, BRDP]): + """On message callback.""" + data = message[1] + if data.event is None: + return + event_param = data.event.split(".") + event_type = event_param[len(event_param) - 1] + if event_type not in ( + "Report", + "Alert", + "StatusChange", + "getState", + ): + return + resolved_state = data.data + if resolved_state is None: + return + self.data[message[0]] = resolved_state + self.async_set_updated_data(self.data) + + async def init_coordinator(self): + """Init coordinator.""" + try: + async with async_timeout.timeout(10): + home_info = await self._client.get_general_info() + await self._mqtt_client.init_home_connection( + home_info.data["id"], self.on_message_callback + ) + async with async_timeout.timeout(10): + device_response = await self._client.get_auth_devices() + + except YoLinkAuthFailError as yl_auth_err: + raise ConfigEntryAuthFailed from yl_auth_err + + except (YoLinkClientError, asyncio.TimeoutError) as err: + raise ConfigEntryNotReady from err + + yl_devices: list[YoLinkDevice] = [] + + for device_info in device_response.data[ATTR_DEVICE]: + yl_devices.append(YoLinkDevice(device_info, self._client)) + + self.yl_devices = yl_devices + + async def fetch_device_state(self, device: YoLinkDevice): + """Fetch Device State.""" + try: + async with async_timeout.timeout(10): + device_state_resp = await device.fetch_state_with_api() + if ATTR_DEVICE_STATE in device_state_resp.data: + self.data[device.device_id] = device_state_resp.data[ + ATTR_DEVICE_STATE + ] + except YoLinkAuthFailError as yl_auth_err: + raise ConfigEntryAuthFailed from yl_auth_err + except YoLinkClientError as yl_client_err: + raise UpdateFailed( + f"Error communicating with API: {yl_client_err}" + ) from yl_client_err + + async def _async_update_data(self) -> dict: + fetch_tasks = [] + for yl_device in self.yl_devices: + fetch_tasks.append(self.fetch_device_state(yl_device)) + if fetch_tasks: + await asyncio.gather(*fetch_tasks) + return self.data diff --git a/homeassistant/components/yolink/entity.py b/homeassistant/components/yolink/entity.py new file mode 100644 index 00000000000..6954b117728 --- /dev/null +++ b/homeassistant/components/yolink/entity.py @@ -0,0 +1,52 @@ +"""Support for YoLink Device.""" +from __future__ import annotations + +from abc import abstractmethod + +from yolink.device import YoLinkDevice + +from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MANUFACTURER +from .coordinator import YoLinkCoordinator + + +class YoLinkEntity(CoordinatorEntity[YoLinkCoordinator]): + """YoLink Device Basic Entity.""" + + def __init__( + self, + coordinator: YoLinkCoordinator, + device_info: YoLinkDevice, + ) -> None: + """Init YoLink Entity.""" + super().__init__(coordinator) + self.device = device_info + + @property + def device_id(self) -> str: + """Return the device id of the YoLink device.""" + return self.device.device_id + + @callback + def _handle_coordinator_update(self) -> None: + data = self.coordinator.data.get(self.device.device_id) + if data is not None: + self.update_entity_state(data) + + @property + def device_info(self) -> DeviceInfo: + """Return the device info for HA.""" + return DeviceInfo( + identifiers={(DOMAIN, self.device.device_id)}, + manufacturer=MANUFACTURER, + model=self.device.device_type, + name=self.device.device_name, + ) + + @callback + @abstractmethod + def update_entity_state(self, state: dict) -> None: + """Parse and update entity state, should be overridden.""" diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json new file mode 100644 index 00000000000..d0072145c19 --- /dev/null +++ b/homeassistant/components/yolink/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "yolink", + "name": "YoLink", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/yolink", + "requirements": ["yolink-api==0.0.5"], + "dependencies": ["auth", "application_credentials"], + "codeowners": ["@YoSmart-Inc"], + "iot_class": "cloud_push" +} diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py new file mode 100644 index 00000000000..075cfc24179 --- /dev/null +++ b/homeassistant/components/yolink/sensor.py @@ -0,0 +1,104 @@ +"""YoLink Binary Sensor.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from yolink.device import YoLinkDevice + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import percentage + +from .const import ATTR_COORDINATOR, ATTR_DEVICE_DOOR_SENSOR, DOMAIN +from .coordinator import YoLinkCoordinator +from .entity import YoLinkEntity + + +@dataclass +class YoLinkSensorEntityDescriptionMixin: + """Mixin for device type.""" + + exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True + + +@dataclass +class YoLinkSensorEntityDescription( + YoLinkSensorEntityDescriptionMixin, SensorEntityDescription +): + """YoLink SensorEntityDescription.""" + + value: Callable = lambda state: state + + +SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( + YoLinkSensorEntityDescription( + key="battery", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + name="Battery", + state_class=SensorStateClass.MEASUREMENT, + value=lambda value: percentage.ordered_list_item_to_percentage( + [1, 2, 3, 4], value + ), + exists_fn=lambda device: device.device_type in [ATTR_DEVICE_DOOR_SENSOR], + ), +) + +SENSOR_DEVICE_TYPE = [ATTR_DEVICE_DOOR_SENSOR] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up YoLink Sensor from a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATOR] + sensor_devices = [ + device + for device in coordinator.yl_devices + if device.device_type in SENSOR_DEVICE_TYPE + ] + entities = [] + for sensor_device in sensor_devices: + for description in SENSOR_TYPES: + if description.exists_fn(sensor_device): + entities.append( + YoLinkSensorEntity(coordinator, description, sensor_device) + ) + async_add_entities(entities) + + +class YoLinkSensorEntity(YoLinkEntity, SensorEntity): + """YoLink Sensor Entity.""" + + entity_description: YoLinkSensorEntityDescription + + def __init__( + self, + coordinator: YoLinkCoordinator, + description: YoLinkSensorEntityDescription, + device: YoLinkDevice, + ) -> None: + """Init YoLink Sensor.""" + super().__init__(coordinator, device) + self.entity_description = description + self._attr_unique_id = f"{device.device_id} {self.entity_description.key}" + self._attr_name = f"{device.device_name} ({self.entity_description.name})" + + @callback + def update_entity_state(self, state: dict) -> None: + """Update HA Entity State.""" + self._attr_native_value = self.entity_description.value( + state[self.entity_description.key] + ) + self.async_write_ha_state() diff --git a/homeassistant/components/yolink/strings.json b/homeassistant/components/yolink/strings.json new file mode 100644 index 00000000000..94fe5dc09aa --- /dev/null +++ b/homeassistant/components/yolink/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The yolink integration needs to re-authenticate your account" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + }, + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" + } + } +} diff --git a/homeassistant/components/yolink/translations/en.json b/homeassistant/components/yolink/translations/en.json new file mode 100644 index 00000000000..d1817fbe011 --- /dev/null +++ b/homeassistant/components/yolink/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured", + "already_in_progress": "Configuration flow is already in progress", + "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})", + "oauth_error": "Received invalid token data.", + "reauth_successful": "Re-authentication was successful" + }, + "create_entry": { + "default": "Successfully authenticated" + }, + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + }, + "reauth_confirm": { + "description": "The yolink integration needs to re-authenticate your account", + "title": "Reauthenticate Integration" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index 40f451de153..0c3e60b8b8d 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -10,5 +10,6 @@ APPLICATION_CREDENTIALS = [ "google", "netatmo", "spotify", - "xbox" + "xbox", + "yolink" ] diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 77146689b8c..7f0059b2da9 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -411,6 +411,7 @@ FLOWS = { "yale_smart_alarm", "yamaha_musiccast", "yeelight", + "yolink", "youless", "zerproc", "zha", diff --git a/requirements_all.txt b/requirements_all.txt index a7c59f1853d..687962e94b6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2479,6 +2479,9 @@ yeelight==0.7.10 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 +# homeassistant.components.yolink +yolink-api==0.0.5 + # homeassistant.components.youless youless-api==0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f9926797e6f..05a7207c01a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1631,6 +1631,9 @@ yalexs==1.1.25 # homeassistant.components.yeelight yeelight==0.7.10 +# homeassistant.components.yolink +yolink-api==0.0.5 + # homeassistant.components.youless youless-api==0.16 diff --git a/tests/components/yolink/__init__.py b/tests/components/yolink/__init__.py new file mode 100644 index 00000000000..a72667661b9 --- /dev/null +++ b/tests/components/yolink/__init__.py @@ -0,0 +1 @@ +"""Tests for the yolink integration.""" diff --git a/tests/components/yolink/test_config_flow.py b/tests/components/yolink/test_config_flow.py new file mode 100644 index 00000000000..4dd347f4076 --- /dev/null +++ b/tests/components/yolink/test_config_flow.py @@ -0,0 +1,210 @@ +"""Test yolink config flow.""" +import asyncio +from http import HTTPStatus +from unittest.mock import patch + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components import application_credentials +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow + +from tests.common import MockConfigEntry + +CLIENT_ID = "12345" +CLIENT_SECRET = "6789" +YOLINK_HOST = "api.yosmart.com" +YOLINK_HTTP_HOST = f"http://{YOLINK_HOST}" +DOMAIN = "yolink" +OAUTH2_AUTHORIZE = f"{YOLINK_HTTP_HOST}/oauth/v2/authorization.htm" +OAUTH2_TOKEN = f"{YOLINK_HTTP_HOST}/open/yolink/token" + + +async def test_abort_if_no_configuration(hass): + """Check flow abort when no configuration.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "missing_configuration" + + +async def test_abort_if_existing_entry(hass: HomeAssistant): + """Check flow abort when an entry already exist.""" + MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN).add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_full_flow( + hass, hass_client_no_auth, aioclient_mock, current_request_with_host +): + """Check full flow.""" + assert await setup.async_setup_component( + hass, + DOMAIN, + {}, + ) + await application_credentials.async_import_client_credential( + hass, + DOMAIN, + application_credentials.ClientCredential(CLIENT_ID, CLIENT_SECRET), + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=create" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch("homeassistant.components.yolink.api.ConfigEntryAuth"), patch( + "homeassistant.components.yolink.async_setup_entry", return_value=True + ) as mock_setup: + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["data"]["auth_implementation"] == DOMAIN + + result["data"]["token"].pop("expires_at") + assert result["data"]["token"] == { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + } + + assert DOMAIN in hass.config.components + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.state is config_entries.ConfigEntryState.LOADED + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 + + +async def test_abort_if_authorization_timeout(hass, current_request_with_host): + """Check yolink authorization timeout.""" + assert await setup.async_setup_component( + hass, + DOMAIN, + {}, + ) + await application_credentials.async_import_client_credential( + hass, + DOMAIN, + application_credentials.ClientCredential(CLIENT_ID, CLIENT_SECRET), + ) + with patch( + "homeassistant.components.yolink.config_entry_oauth2_flow." + "LocalOAuth2Implementation.async_generate_authorize_url", + side_effect=asyncio.TimeoutError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "authorize_url_timeout" + + +async def test_reauthentication( + hass, hass_client_no_auth, aioclient_mock, current_request_with_host +): + """Test yolink reauthentication.""" + await setup.async_setup_component( + hass, + DOMAIN, + {}, + ) + + await application_credentials.async_import_client_credential( + hass, + DOMAIN, + application_credentials.ClientCredential(CLIENT_ID, CLIENT_SECRET), + ) + + old_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=DOMAIN, + version=1, + data={ + "refresh_token": "outdated_fresh_token", + "access_token": "outdated_access_token", + }, + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) + + # pylint: disable=protected-access + 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() + await client.get(f"/auth/external/callback?code=abcd&state={state}") + + 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.yolink.api.ConfigEntryAuth"): + with patch( + "homeassistant.components.yolink.async_setup_entry", return_value=True + ) as mock_setup: + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + token_data = old_entry.data["token"] + assert token_data["access_token"] == "mock-access-token" + assert token_data["refresh_token"] == "mock-refresh-token" + assert token_data["type"] == "Bearer" + assert token_data["expires_in"] == 60 + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert len(mock_setup.mock_calls) == 1 From 7d26be2d4d0711796c0870d7917d68751aad7a66 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 17 May 2022 01:48:31 -0700 Subject: [PATCH 0571/3516] Add Home Connect application_credentials platform and deprecate configuration.yaml (#71988) Add Home Connect application_credentials platform --- .../components/home_connect/__init__.py | 43 ++++++++++++------- .../home_connect/application_credentials.py | 14 ++++++ .../components/home_connect/manifest.json | 2 +- .../generated/application_credentials.py | 1 + 4 files changed, 43 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/home_connect/application_credentials.py diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index 26448893438..345eeeddaaa 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -6,6 +6,10 @@ import logging from requests import HTTPError import voluptuous as vol +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform from homeassistant.core import HomeAssistant @@ -13,22 +17,25 @@ from homeassistant.helpers import config_entry_oauth2_flow, config_validation as from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle -from . import api, config_flow -from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN +from . import api +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=1) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -42,17 +49,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if DOMAIN not in config: return True - config_flow.OAuth2FlowHandler.async_register_implementation( + await async_import_client_credential( hass, - config_entry_oauth2_flow.LocalOAuth2Implementation( - hass, - DOMAIN, + DOMAIN, + ClientCredential( config[DOMAIN][CONF_CLIENT_ID], config[DOMAIN][CONF_CLIENT_SECRET], - OAUTH2_AUTHORIZE, - OAUTH2_TOKEN, ), ) + _LOGGER.warning( + "Configuration of Home Connect integration in YAML is deprecated and " + "will be removed in a future release; Your existing OAuth " + "Application Credentials have been imported into the UI " + "automatically and can be safely removed from your " + "configuration.yaml file" + ) return True diff --git a/homeassistant/components/home_connect/application_credentials.py b/homeassistant/components/home_connect/application_credentials.py new file mode 100644 index 00000000000..3d5a407b487 --- /dev/null +++ b/homeassistant/components/home_connect/application_credentials.py @@ -0,0 +1,14 @@ +"""Application credentials platform for Home Connect.""" + +from homeassistant.components.application_credentials import AuthorizationServer +from homeassistant.core import HomeAssistant + +from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN + + +async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: + """Return authorization server.""" + return AuthorizationServer( + authorize_url=OAUTH2_AUTHORIZE, + token_url=OAUTH2_TOKEN, + ) diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index e50053d2d1b..0a055c971c5 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "home_connect", "name": "Home Connect", "documentation": "https://www.home-assistant.io/integrations/home_connect", - "dependencies": ["auth"], + "dependencies": ["application_credentials"], "codeowners": ["@DavidMStraub"], "requirements": ["homeconnect==0.7.0"], "config_flow": true, diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index 0c3e60b8b8d..7521c5c30b8 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -8,6 +8,7 @@ To update, run python3 -m script.hassfest APPLICATION_CREDENTIALS = [ "geocaching", "google", + "home_connect", "netatmo", "spotify", "xbox", From 39b27e4d38b91498245e19ded3c5fba6641cbcd4 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Tue, 17 May 2022 13:08:38 +0200 Subject: [PATCH 0572/3516] Improve NUT typing (#72002) --- homeassistant/components/nut/config_flow.py | 5 +++-- homeassistant/components/nut/sensor.py | 6 ++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py index 5ba8024878a..917f004ce32 100644 --- a/homeassistant/components/nut/config_flow.py +++ b/homeassistant/components/nut/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Network UPS Tools (NUT) integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -69,7 +70,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, return {"ups_list": nut_data.ups_list, "available_resources": status} -def _format_host_port_alias(user_input: dict[str, Any]) -> str: +def _format_host_port_alias(user_input: Mapping[str, Any]) -> str: """Format a host, port, and alias so it can be used for comparison or display.""" host = user_input[CONF_HOST] port = user_input[CONF_PORT] @@ -157,7 +158,7 @@ class NutConfigFlow(ConfigFlow, domain=DOMAIN): def _host_port_alias_already_configured(self, user_input: dict[str, Any]) -> bool: """See if we already have a nut entry matching user input configured.""" existing_host_port_aliases = { - _format_host_port_alias(dict(entry.data)) + _format_host_port_alias(entry.data) for entry in self._async_current_entries() if CONF_HOST in entry.data } diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 6992e41366e..6c68f2b6c6b 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -86,14 +86,12 @@ async def async_setup_entry( async_add_entities(entities, True) -class NUTSensor(CoordinatorEntity, SensorEntity): +class NUTSensor(CoordinatorEntity[DataUpdateCoordinator[dict[str, str]]], SensorEntity): """Representation of a sensor entity for NUT status values.""" - coordinator: DataUpdateCoordinator[dict[str, str]] - def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[dict[str, str]], sensor_description: SensorEntityDescription, data: PyNUTData, unique_id: str, From 7d2deae5921989fc4a9add9aef910160ab0ac358 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 May 2022 13:40:19 +0200 Subject: [PATCH 0573/3516] Clean up use of deprecated async_get_registry methods (#72001) --- homeassistant/components/alarm_control_panel/device_action.py | 2 +- .../components/alarm_control_panel/device_condition.py | 2 +- .../components/alarm_control_panel/device_trigger.py | 2 +- homeassistant/components/arcam_fmj/device_trigger.py | 2 +- homeassistant/components/climate/device_action.py | 2 +- homeassistant/components/climate/device_condition.py | 2 +- homeassistant/components/climate/device_trigger.py | 2 +- homeassistant/components/cover/device_action.py | 2 +- homeassistant/components/cover/device_condition.py | 2 +- homeassistant/components/cover/device_trigger.py | 2 +- homeassistant/components/device_tracker/device_condition.py | 2 +- homeassistant/components/device_tracker/device_trigger.py | 2 +- homeassistant/components/fan/device_condition.py | 2 +- homeassistant/components/geofency/device_tracker.py | 2 +- homeassistant/components/gpslogger/device_tracker.py | 2 +- homeassistant/components/huawei_lte/device_tracker.py | 2 +- homeassistant/components/humidifier/device_action.py | 2 +- homeassistant/components/humidifier/device_condition.py | 2 +- homeassistant/components/humidifier/device_trigger.py | 2 +- homeassistant/components/keenetic_ndms2/device_tracker.py | 2 +- homeassistant/components/kodi/device_trigger.py | 2 +- homeassistant/components/kodi/media_player.py | 2 +- homeassistant/components/lock/device_action.py | 2 +- homeassistant/components/lock/device_condition.py | 2 +- homeassistant/components/lock/device_trigger.py | 2 +- homeassistant/components/media_player/device_condition.py | 2 +- homeassistant/components/media_player/device_trigger.py | 2 +- homeassistant/components/mikrotik/device_tracker.py | 2 +- homeassistant/components/netatmo/device_trigger.py | 2 +- homeassistant/components/number/device_action.py | 2 +- homeassistant/components/owntracks/device_tracker.py | 2 +- homeassistant/components/ps4/__init__.py | 2 +- homeassistant/components/ps4/media_player.py | 4 ++-- homeassistant/components/ruckus_unleashed/__init__.py | 2 +- homeassistant/components/ruckus_unleashed/device_tracker.py | 2 +- homeassistant/components/select/device_action.py | 2 +- homeassistant/components/select/device_condition.py | 2 +- homeassistant/components/select/device_trigger.py | 2 +- homeassistant/components/shelly/climate.py | 2 +- homeassistant/components/shelly/entity.py | 2 +- homeassistant/components/traccar/device_tracker.py | 2 +- homeassistant/components/vacuum/device_action.py | 2 +- homeassistant/components/vacuum/device_condition.py | 2 +- homeassistant/components/vacuum/device_trigger.py | 2 +- homeassistant/components/water_heater/device_action.py | 2 +- homeassistant/config_entries.py | 2 +- homeassistant/helpers/device_registry.py | 2 +- .../templates/device_action/integration/device_action.py | 2 +- .../device_condition/integration/device_condition.py | 2 +- .../templates/device_trigger/integration/device_trigger.py | 2 +- 50 files changed, 51 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py index 2680b033b03..ff14b851df8 100644 --- a/homeassistant/components/alarm_control_panel/device_action.py +++ b/homeassistant/components/alarm_control_panel/device_action.py @@ -57,7 +57,7 @@ async def async_get_actions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device actions for Alarm control panel devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) actions = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/alarm_control_panel/device_condition.py b/homeassistant/components/alarm_control_panel/device_condition.py index a378cf262ed..4764d5cfcbe 100644 --- a/homeassistant/components/alarm_control_panel/device_condition.py +++ b/homeassistant/components/alarm_control_panel/device_condition.py @@ -64,7 +64,7 @@ async def async_get_conditions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device conditions for Alarm control panel devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) conditions = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py index ce53596fc8d..18508661034 100644 --- a/homeassistant/components/alarm_control_panel/device_trigger.py +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -60,7 +60,7 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, Any]]: """List device triggers for Alarm control panel devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) triggers: list[dict[str, str]] = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/arcam_fmj/device_trigger.py b/homeassistant/components/arcam_fmj/device_trigger.py index 2b1a3bf3a19..d1fca811fcc 100644 --- a/homeassistant/components/arcam_fmj/device_trigger.py +++ b/homeassistant/components/arcam_fmj/device_trigger.py @@ -37,7 +37,7 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, Any]]: """List device triggers for Arcam FMJ Receiver control devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) triggers = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/climate/device_action.py b/homeassistant/components/climate/device_action.py index 81189417c2b..45160eed8eb 100644 --- a/homeassistant/components/climate/device_action.py +++ b/homeassistant/components/climate/device_action.py @@ -43,7 +43,7 @@ async def async_get_actions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device actions for Climate devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) actions = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py index dd5842cd2a8..56bac123d28 100644 --- a/homeassistant/components/climate/device_condition.py +++ b/homeassistant/components/climate/device_condition.py @@ -45,7 +45,7 @@ async def async_get_conditions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device conditions for Climate devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) conditions = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index 6bd6f4c3e02..e40cf32f2a9 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -67,7 +67,7 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, Any]]: """List device triggers for Climate devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) triggers = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/cover/device_action.py b/homeassistant/components/cover/device_action.py index debb2368cf2..9bdf21f168e 100644 --- a/homeassistant/components/cover/device_action.py +++ b/homeassistant/components/cover/device_action.py @@ -63,7 +63,7 @@ async def async_get_actions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device actions for Cover devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) actions = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py index cca608187a2..bb66d54b79b 100644 --- a/homeassistant/components/cover/device_condition.py +++ b/homeassistant/components/cover/device_condition.py @@ -66,7 +66,7 @@ async def async_get_conditions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device conditions for Cover devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) conditions: list[dict[str, str]] = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/cover/device_trigger.py b/homeassistant/components/cover/device_trigger.py index f960fcdcce6..f7b2f54f4dd 100644 --- a/homeassistant/components/cover/device_trigger.py +++ b/homeassistant/components/cover/device_trigger.py @@ -76,7 +76,7 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, Any]]: """List device triggers for Cover devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) triggers = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/device_tracker/device_condition.py b/homeassistant/components/device_tracker/device_condition.py index 0703d3c7646..1a6adabda63 100644 --- a/homeassistant/components/device_tracker/device_condition.py +++ b/homeassistant/components/device_tracker/device_condition.py @@ -33,7 +33,7 @@ async def async_get_conditions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device conditions for Device tracker devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) conditions = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/device_tracker/device_trigger.py b/homeassistant/components/device_tracker/device_trigger.py index 4350f03cefc..134e9d8dca3 100644 --- a/homeassistant/components/device_tracker/device_trigger.py +++ b/homeassistant/components/device_tracker/device_trigger.py @@ -41,7 +41,7 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, Any]]: """List device triggers for Device Tracker devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) triggers = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/fan/device_condition.py b/homeassistant/components/fan/device_condition.py index b0882137d7f..7e27ea29f98 100644 --- a/homeassistant/components/fan/device_condition.py +++ b/homeassistant/components/fan/device_condition.py @@ -34,7 +34,7 @@ async def async_get_conditions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device conditions for Fan devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) conditions = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index c92b2905be2..bd4b2852019 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -35,7 +35,7 @@ async def async_setup_entry( ] = async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) # Restore previously loaded devices - dev_reg = await device_registry.async_get_registry(hass) + dev_reg = device_registry.async_get(hass) dev_ids = { identifier[1] for device in dev_reg.devices.values() diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index fb0e1385fba..ea648ed7495 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -45,7 +45,7 @@ async def async_setup_entry( ] = async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) # Restore previously loaded devices - dev_reg = await device_registry.async_get_registry(hass) + dev_reg = device_registry.async_get(hass) dev_ids = { identifier[1] for device in dev_reg.devices.values() diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index c8e9a5d7a0d..ee643c59b12 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -66,7 +66,7 @@ async def async_setup_entry( # Initialize already tracked entities tracked: set[str] = set() - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) known_entities: list[Entity] = [] track_wired_clients = router.config_entry.options.get( CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS diff --git a/homeassistant/components/humidifier/device_action.py b/homeassistant/components/humidifier/device_action.py index db51ab34baa..03f0bb1fea1 100644 --- a/homeassistant/components/humidifier/device_action.py +++ b/homeassistant/components/humidifier/device_action.py @@ -49,7 +49,7 @@ async def async_get_actions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device actions for Humidifier devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) actions = await toggle_entity.async_get_actions(hass, device_id, DOMAIN) # Get all the integrations entities for this device diff --git a/homeassistant/components/humidifier/device_condition.py b/homeassistant/components/humidifier/device_condition.py index c58c247d569..b8e8d2a13ae 100644 --- a/homeassistant/components/humidifier/device_condition.py +++ b/homeassistant/components/humidifier/device_condition.py @@ -41,7 +41,7 @@ async def async_get_conditions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device conditions for Humidifier devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) conditions = await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) # Get all the integrations entities for this device diff --git a/homeassistant/components/humidifier/device_trigger.py b/homeassistant/components/humidifier/device_trigger.py index e4b0440e869..a2d15097335 100644 --- a/homeassistant/components/humidifier/device_trigger.py +++ b/homeassistant/components/humidifier/device_trigger.py @@ -61,7 +61,7 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, Any]]: """List device triggers for Humidifier devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) triggers = await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) # Get all the integrations entities for this device diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index 5a92d04e498..b625cabbbc1 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -40,7 +40,7 @@ async def async_setup_entry( update_from_router() - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) # Restore devices that are not a part of active clients list. restored = [] for entity_entry in registry.entities.values(): diff --git a/homeassistant/components/kodi/device_trigger.py b/homeassistant/components/kodi/device_trigger.py index 68735bfa386..e4fe6cfa103 100644 --- a/homeassistant/components/kodi/device_trigger.py +++ b/homeassistant/components/kodi/device_trigger.py @@ -38,7 +38,7 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, Any]]: """List device triggers for Kodi devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) triggers = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 66bb582e6e0..cea3adcde00 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -426,7 +426,7 @@ class KodiEntity(MediaPlayerEntity): version = (await self._kodi.get_application_properties(["version"]))["version"] sw_version = f"{version['major']}.{version['minor']}" - dev_reg = await device_registry.async_get_registry(self.hass) + dev_reg = device_registry.async_get(self.hass) device = dev_reg.async_get_device({(DOMAIN, self.unique_id)}) dev_reg.async_update_device(device.id, sw_version=sw_version) diff --git a/homeassistant/components/lock/device_action.py b/homeassistant/components/lock/device_action.py index 092aff8878d..038c2f1c0da 100644 --- a/homeassistant/components/lock/device_action.py +++ b/homeassistant/components/lock/device_action.py @@ -34,7 +34,7 @@ async def async_get_actions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device actions for Lock devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) actions = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/lock/device_condition.py b/homeassistant/components/lock/device_condition.py index a818a2b5fa4..cdaa02de618 100644 --- a/homeassistant/components/lock/device_condition.py +++ b/homeassistant/components/lock/device_condition.py @@ -45,7 +45,7 @@ async def async_get_conditions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device conditions for Lock devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) conditions = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/lock/device_trigger.py b/homeassistant/components/lock/device_trigger.py index 75415bbf3e1..d875cadb446 100644 --- a/homeassistant/components/lock/device_trigger.py +++ b/homeassistant/components/lock/device_trigger.py @@ -45,7 +45,7 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, Any]]: """List device triggers for Lock devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) triggers = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/media_player/device_condition.py b/homeassistant/components/media_player/device_condition.py index 5f57bcea48e..2f398790ac3 100644 --- a/homeassistant/components/media_player/device_condition.py +++ b/homeassistant/components/media_player/device_condition.py @@ -45,7 +45,7 @@ async def async_get_conditions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device conditions for Media player devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) conditions = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/media_player/device_trigger.py b/homeassistant/components/media_player/device_trigger.py index aeed2fd646a..9644e5e0010 100644 --- a/homeassistant/components/media_player/device_trigger.py +++ b/homeassistant/components/media_player/device_trigger.py @@ -57,7 +57,7 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, Any]]: """List device triggers for Media player entities.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) triggers = await entity.async_get_triggers(hass, device_id, DOMAIN) # Get all the integration entities for this device diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 166415caf39..dee4a5de08d 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -34,7 +34,7 @@ async def async_setup_entry( tracked: dict[str, MikrotikHubTracker] = {} - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) # Restore clients that is not a part of active clients list. for entity in registry.entities.values(): diff --git a/homeassistant/components/netatmo/device_trigger.py b/homeassistant/components/netatmo/device_trigger.py index e3173fde160..7a9acac2f96 100644 --- a/homeassistant/components/netatmo/device_trigger.py +++ b/homeassistant/components/netatmo/device_trigger.py @@ -93,7 +93,7 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, Any]]: """List device triggers for Netatmo devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) device_registry = await hass.helpers.device_registry.async_get_registry() triggers = [] diff --git a/homeassistant/components/number/device_action.py b/homeassistant/components/number/device_action.py index 917bcdce74f..3d449ffc213 100644 --- a/homeassistant/components/number/device_action.py +++ b/homeassistant/components/number/device_action.py @@ -32,7 +32,7 @@ async def async_get_actions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device actions for Number.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) actions: list[dict[str, str]] = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index ae4d88df718..92f34617462 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -26,7 +26,7 @@ async def async_setup_entry( ) -> None: """Set up OwnTracks based off an entry.""" # Restore previously loaded devices - dev_reg = await device_registry.async_get_registry(hass) + dev_reg = device_registry.async_get(hass) dev_ids = { identifier[1] for device in dev_reg.devices.values() diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index e6c22552fc2..7e215060d73 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -116,7 +116,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Migrate Version 2 -> Version 3: Update identifier format. if version == 2: # Prevent changing entity_id. Updates entity registry. - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) for entity_id, e_entry in registry.entities.items(): if e_entry.config_entry_id == entry.entry_id: diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index ef6cc3c364b..1a1c93e210a 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -333,8 +333,8 @@ class PS4Device(MediaPlayerEntity): # If cannot get status on startup, assume info from registry. if status is None: _LOGGER.info("Assuming status from registry") - e_registry = await entity_registry.async_get_registry(self.hass) - d_registry = await device_registry.async_get_registry(self.hass) + e_registry = entity_registry.async_get(self.hass) + d_registry = device_registry.async_get(self.hass) for entity_id, entry in e_registry.entities.items(): if entry.config_entry_id == self._entry_id: self._unique_id = entry.unique_id diff --git a/homeassistant/components/ruckus_unleashed/__init__.py b/homeassistant/components/ruckus_unleashed/__init__.py index 6ea3b736dcd..2c8c8bb108d 100644 --- a/homeassistant/components/ruckus_unleashed/__init__.py +++ b/homeassistant/components/ruckus_unleashed/__init__.py @@ -44,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: system_info = await hass.async_add_executor_job(ruckus.system_info) - registry = await device_registry.async_get_registry(hass) + registry = device_registry.async_get(hass) ap_info = await hass.async_add_executor_job(ruckus.ap_info) for device in ap_info[API_AP][API_ID].values(): registry.async_get_or_create( diff --git a/homeassistant/components/ruckus_unleashed/device_tracker.py b/homeassistant/components/ruckus_unleashed/device_tracker.py index 562f4835547..67c86f3bb51 100644 --- a/homeassistant/components/ruckus_unleashed/device_tracker.py +++ b/homeassistant/components/ruckus_unleashed/device_tracker.py @@ -38,7 +38,7 @@ async def async_setup_entry( coordinator.async_add_listener(router_update) ) - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) restore_entities(registry, coordinator, entry, async_add_entities, tracked) diff --git a/homeassistant/components/select/device_action.py b/homeassistant/components/select/device_action.py index f4c4d7883d0..f55a12da62a 100644 --- a/homeassistant/components/select/device_action.py +++ b/homeassistant/components/select/device_action.py @@ -34,7 +34,7 @@ async def async_get_actions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device actions for Select devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) return [ { CONF_DEVICE_ID: device_id, diff --git a/homeassistant/components/select/device_condition.py b/homeassistant/components/select/device_condition.py index ed48f2afb15..6e6a3c704b3 100644 --- a/homeassistant/components/select/device_condition.py +++ b/homeassistant/components/select/device_condition.py @@ -38,7 +38,7 @@ async def async_get_conditions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device conditions for Select devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) return [ { CONF_CONDITION: "device", diff --git a/homeassistant/components/select/device_trigger.py b/homeassistant/components/select/device_trigger.py index 79e1c4a221f..6d0378946e8 100644 --- a/homeassistant/components/select/device_trigger.py +++ b/homeassistant/components/select/device_trigger.py @@ -49,7 +49,7 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, Any]]: """List device triggers for Select devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) return [ { CONF_PLATFORM: "device", diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index ffd5e012ef7..a042254fdc6 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -87,7 +87,7 @@ async def async_restore_climate_entities( ) -> None: """Restore sleeping climate devices.""" - ent_reg = await entity_registry.async_get_registry(hass) + ent_reg = entity_registry.async_get(hass) entries = entity_registry.async_entries_for_config_entry( ent_reg, config_entry.entry_id ) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index f544770722f..de7d3dd9437 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -134,7 +134,7 @@ async def async_restore_block_attribute_entities( """Restore block attributes entities.""" entities = [] - ent_reg = await entity_registry.async_get_registry(hass) + ent_reg = entity_registry.async_get(hass) entries = entity_registry.async_entries_for_config_entry( ent_reg, config_entry.entry_id ) diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 74ce3ead901..0ed13bceefa 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -144,7 +144,7 @@ async def async_setup_entry( ] = async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) # Restore previously loaded devices - dev_reg = await device_registry.async_get_registry(hass) + dev_reg = device_registry.async_get(hass) dev_ids = { identifier[1] for device in dev_reg.devices.values() diff --git a/homeassistant/components/vacuum/device_action.py b/homeassistant/components/vacuum/device_action.py index 702f3fe7439..e8dac646153 100644 --- a/homeassistant/components/vacuum/device_action.py +++ b/homeassistant/components/vacuum/device_action.py @@ -30,7 +30,7 @@ async def async_get_actions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device actions for Vacuum devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) actions = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/vacuum/device_condition.py b/homeassistant/components/vacuum/device_condition.py index 7a973c93694..fa76dd800ec 100644 --- a/homeassistant/components/vacuum/device_condition.py +++ b/homeassistant/components/vacuum/device_condition.py @@ -32,7 +32,7 @@ async def async_get_conditions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device conditions for Vacuum devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) conditions = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/vacuum/device_trigger.py b/homeassistant/components/vacuum/device_trigger.py index 25a874a1e69..502f9f01410 100644 --- a/homeassistant/components/vacuum/device_trigger.py +++ b/homeassistant/components/vacuum/device_trigger.py @@ -40,7 +40,7 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, Any]]: """List device triggers for Vacuum devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) triggers = [] # Get all the integrations entities for this device diff --git a/homeassistant/components/water_heater/device_action.py b/homeassistant/components/water_heater/device_action.py index dae9e4d579b..f200498c350 100644 --- a/homeassistant/components/water_heater/device_action.py +++ b/homeassistant/components/water_heater/device_action.py @@ -32,7 +32,7 @@ async def async_get_actions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device actions for Water Heater devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) actions = [] for entry in entity_registry.async_entries_for_device(registry, device_id): diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 97278f3b2e3..7dfbb131c1b 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1549,7 +1549,7 @@ class EntityRegistryDisabledHandler: async def _handle_entry_updated(self, event: Event) -> None: """Handle entity registry entry update.""" if self.registry is None: - self.registry = await entity_registry.async_get_registry(self.hass) + self.registry = entity_registry.async_get(self.hass) entity_entry = self.registry.async_get(event.data["entity_id"]) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index c45b7d5f37b..7299da43958 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -823,7 +823,7 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: async def cleanup() -> None: """Cleanup.""" - ent_reg = await entity_registry.async_get_registry(hass) + ent_reg = entity_registry.async_get(hass) async_cleanup(hass, dev_reg, ent_reg) debounced_cleanup = Debouncer( diff --git a/script/scaffold/templates/device_action/integration/device_action.py b/script/scaffold/templates/device_action/integration/device_action.py index 5eb5249211b..a9d77853e55 100644 --- a/script/scaffold/templates/device_action/integration/device_action.py +++ b/script/scaffold/templates/device_action/integration/device_action.py @@ -33,7 +33,7 @@ async def async_get_actions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device actions for NEW_NAME devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) actions = [] # TODO Read this comment and remove it. diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index 6f129289af8..cc5ad765885 100644 --- a/script/scaffold/templates/device_condition/integration/device_condition.py +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -35,7 +35,7 @@ async def async_get_conditions( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device conditions for NEW_NAME devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) conditions = [] # Get all the integrations entities for this device diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py index 9082d27953a..a03e27394e2 100644 --- a/script/scaffold/templates/device_trigger/integration/device_trigger.py +++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py @@ -41,7 +41,7 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, Any]]: """List device triggers for NEW_NAME devices.""" - registry = await entity_registry.async_get_registry(hass) + registry = entity_registry.async_get(hass) triggers = [] # TODO Read this comment and remove it. From c67bbd06d4416ea63d6c7efb23cd519bdf25b06a Mon Sep 17 00:00:00 2001 From: rhadamantys <46837767+rhadamantys@users.noreply.github.com> Date: Tue, 17 May 2022 13:43:36 +0200 Subject: [PATCH 0574/3516] Provide unique id for enocean devices (#71774) Co-authored-by: Martin Hjelmare --- .../components/enocean/binary_sensor.py | 2 + homeassistant/components/enocean/light.py | 2 + homeassistant/components/enocean/sensor.py | 39 +++++++++++++++---- homeassistant/components/enocean/switch.py | 2 + 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/enocean/binary_sensor.py b/homeassistant/components/enocean/binary_sensor.py index 3afa7110657..e18542241da 100644 --- a/homeassistant/components/enocean/binary_sensor.py +++ b/homeassistant/components/enocean/binary_sensor.py @@ -1,6 +1,7 @@ """Support for EnOcean binary sensors.""" from __future__ import annotations +from enocean.utils import combine_hex import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -57,6 +58,7 @@ class EnOceanBinarySensor(EnOceanEntity, BinarySensorEntity): self._device_class = device_class self.which = -1 self.onoff = -1 + self._attr_unique_id = f"{combine_hex(dev_id)}-{device_class}" @property def name(self): diff --git a/homeassistant/components/enocean/light.py b/homeassistant/components/enocean/light.py index c7bf076c738..8ecc3f63831 100644 --- a/homeassistant/components/enocean/light.py +++ b/homeassistant/components/enocean/light.py @@ -3,6 +3,7 @@ from __future__ import annotations import math +from enocean.utils import combine_hex import voluptuous as vol from homeassistant.components.light import ( @@ -58,6 +59,7 @@ class EnOceanLight(EnOceanEntity, LightEntity): self._on_state = False self._brightness = 50 self._sender_id = sender_id + self._attr_unique_id = f"{combine_hex(dev_id)}" @property def name(self): diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index e48f117648a..06ea50d4cdb 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -1,6 +1,10 @@ """Support for EnOcean sensors.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass + +from enocean.utils import combine_hex import voluptuous as vol from homeassistant.components.sensor import ( @@ -40,37 +44,56 @@ SENSOR_TYPE_POWER = "powersensor" SENSOR_TYPE_TEMPERATURE = "temperature" SENSOR_TYPE_WINDOWHANDLE = "windowhandle" -SENSOR_DESC_TEMPERATURE = SensorEntityDescription( + +@dataclass +class EnOceanSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + unique_id: Callable[[list[int]], str | None] + + +@dataclass +class EnOceanSensorEntityDescription( + SensorEntityDescription, EnOceanSensorEntityDescriptionMixin +): + """Describes EnOcean sensor entity.""" + + +SENSOR_DESC_TEMPERATURE = EnOceanSensorEntityDescription( key=SENSOR_TYPE_TEMPERATURE, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, icon="mdi:thermometer", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, + unique_id=lambda dev_id: f"{combine_hex(dev_id)}-{SENSOR_TYPE_TEMPERATURE}", ) -SENSOR_DESC_HUMIDITY = SensorEntityDescription( +SENSOR_DESC_HUMIDITY = EnOceanSensorEntityDescription( key=SENSOR_TYPE_HUMIDITY, name="Humidity", native_unit_of_measurement=PERCENTAGE, icon="mdi:water-percent", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, + unique_id=lambda dev_id: f"{combine_hex(dev_id)}-{SENSOR_TYPE_HUMIDITY}", ) -SENSOR_DESC_POWER = SensorEntityDescription( +SENSOR_DESC_POWER = EnOceanSensorEntityDescription( key=SENSOR_TYPE_POWER, name="Power", native_unit_of_measurement=POWER_WATT, icon="mdi:power-plug", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + unique_id=lambda dev_id: f"{combine_hex(dev_id)}-{SENSOR_TYPE_POWER}", ) -SENSOR_DESC_WINDOWHANDLE = SensorEntityDescription( +SENSOR_DESC_WINDOWHANDLE = EnOceanSensorEntityDescription( key=SENSOR_TYPE_WINDOWHANDLE, name="WindowHandle", - icon="mdi:window", + icon="mdi:window-open-variant", + unique_id=lambda dev_id: f"{combine_hex(dev_id)}-{SENSOR_TYPE_WINDOWHANDLE}", ) @@ -132,11 +155,12 @@ def setup_platform( class EnOceanSensor(EnOceanEntity, RestoreEntity, SensorEntity): """Representation of an EnOcean sensor device such as a power meter.""" - def __init__(self, dev_id, dev_name, description: SensorEntityDescription): + def __init__(self, dev_id, dev_name, description: EnOceanSensorEntityDescription): """Initialize the EnOcean sensor device.""" super().__init__(dev_id, dev_name) self.entity_description = description self._attr_name = f"{description.name} {dev_name}" + self._attr_unique_id = description.unique_id(dev_id) async def async_added_to_hass(self): """Call when entity about to be added to hass.""" @@ -194,7 +218,7 @@ class EnOceanTemperatureSensor(EnOceanSensor): self, dev_id, dev_name, - description: SensorEntityDescription, + description: EnOceanSensorEntityDescription, *, scale_min, scale_max, @@ -248,7 +272,6 @@ class EnOceanWindowHandle(EnOceanSensor): def value_changed(self, packet): """Update the internal state of the sensor.""" - action = (packet.data[1] & 0x70) >> 4 if action == 0x07: diff --git a/homeassistant/components/enocean/switch.py b/homeassistant/components/enocean/switch.py index fc788e88d72..a53f691df19 100644 --- a/homeassistant/components/enocean/switch.py +++ b/homeassistant/components/enocean/switch.py @@ -1,6 +1,7 @@ """Support for EnOcean switches.""" from __future__ import annotations +from enocean.utils import combine_hex import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity @@ -48,6 +49,7 @@ class EnOceanSwitch(EnOceanEntity, SwitchEntity): self._on_state = False self._on_state2 = False self.channel = channel + self._attr_unique_id = f"{combine_hex(dev_id)}" @property def is_on(self): From cba2fda93d27ca45e015247b1f146cf3d7ad723d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 May 2022 14:13:21 +0200 Subject: [PATCH 0575/3516] Fix clear config entry from registry in Samsung TV migration (#72004) * Fix clear config entry from device registry * Fix clear config entry from entity registry --- homeassistant/components/samsungtv/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index a7b8f7d1aec..49963734e83 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -314,10 +314,10 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> # 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(config_entry.entry_id) en_reg = await hass.helpers.entity_registry.async_get_registry() - en_reg.async_clear_config_entry(config_entry) + en_reg.async_clear_config_entry(config_entry.entry_id) version = config_entry.version = 2 hass.config_entries.async_update_entry(config_entry) From 81259f4eefe3149ca84c3a066e835af7740cafbe Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 17 May 2022 15:08:21 +0200 Subject: [PATCH 0576/3516] Update xknx to 0.21.3 (#72006) --- homeassistant/components/knx/cover.py | 46 ++++------------------ homeassistant/components/knx/manifest.json | 2 +- homeassistant/components/knx/strings.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 11 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index b3096a75df5..ca18ad4835a 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -2,11 +2,10 @@ from __future__ import annotations from collections.abc import Callable -from datetime import datetime from typing import Any from xknx import XKNX -from xknx.devices import Cover as XknxCover, Device as XknxDevice +from xknx.devices import Cover as XknxCover from homeassistant import config_entries from homeassistant.components.cover import ( @@ -22,9 +21,8 @@ from homeassistant.const import ( CONF_NAME, Platform, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_utc_time_change from homeassistant.helpers.typing import ConfigType from .const import DATA_KNX_CONFIG, DOMAIN @@ -104,13 +102,6 @@ class KNXCover(KnxEntity, CoverEntity): f"{self._device.position_target.group_address}" ) - @callback - async def after_update_callback(self, device: XknxDevice) -> None: - """Call after device was updated.""" - self.async_write_ha_state() - if self._device.is_traveling(): - self.start_auto_updater() - @property def current_cover_position(self) -> int | None: """Return the current position of the cover. @@ -118,8 +109,9 @@ class KNXCover(KnxEntity, CoverEntity): None is unknown, 0 is closed, 100 is fully open. """ # In KNX 0 is open, 100 is closed. - pos = self._device.current_position() - return 100 - pos if pos is not None else None + if (pos := self._device.current_position()) is not None: + return 100 - pos + return None @property def is_closed(self) -> bool | None: @@ -155,14 +147,12 @@ class KNXCover(KnxEntity, CoverEntity): async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self._device.stop() - self.stop_auto_updater() @property def current_cover_tilt_position(self) -> int | None: """Return current tilt position of cover.""" - if self._device.supports_angle: - ang = self._device.current_angle() - return 100 - ang if ang is not None else None + if (angle := self._device.current_angle()) is not None: + return 100 - angle return None async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: @@ -181,25 +171,3 @@ class KNXCover(KnxEntity, CoverEntity): async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the cover tilt.""" await self._device.stop() - self.stop_auto_updater() - - def start_auto_updater(self) -> None: - """Start the autoupdater to update Home Assistant while cover is moving.""" - if self._unsubscribe_auto_updater is None: - self._unsubscribe_auto_updater = async_track_utc_time_change( - self.hass, self.auto_updater_hook - ) - - def stop_auto_updater(self) -> None: - """Stop the autoupdater.""" - if self._unsubscribe_auto_updater is not None: - self._unsubscribe_auto_updater() - self._unsubscribe_auto_updater = None - - @callback - def auto_updater_hook(self, now: datetime) -> None: - """Call for the autoupdater.""" - self.async_write_ha_state() - if self._device.position_reached(): - self.hass.async_create_task(self._device.auto_stop_if_necessary()) - self.stop_auto_updater() diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 00b4c6cdc5f..b8f9bdcbd30 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.21.2"], + "requirements": ["xknx==0.21.3"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/homeassistant/components/knx/strings.json b/homeassistant/components/knx/strings.json index 018db071adf..c8161462d66 100644 --- a/homeassistant/components/knx/strings.json +++ b/homeassistant/components/knx/strings.json @@ -101,7 +101,7 @@ "multicast_group": "Used for routing and discovery. Default: `224.0.23.12`", "multicast_port": "Used for routing and discovery. Default: `3671`", "local_ip": "Use `0.0.0.0` for auto-discovery.", - "state_updater": "Globally enable or disable reading states from the KNX Bus. When disabled, Home Assistant will not actively retrieve states from the KNX Bus, `sync_state` entity options will have no effect.", + "state_updater": "Set default for reading states from the KNX Bus. When disabled, Home Assistant will not actively retrieve entity states from the KNX Bus. Can be overridden by `sync_state` entity options.", "rate_limit": "Maximum outgoing telegrams per second.\nRecommended: 20 to 40" } }, diff --git a/requirements_all.txt b/requirements_all.txt index 687962e94b6..3fa07679b1a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2454,7 +2454,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.21.2 +xknx==0.21.3 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 05a7207c01a..9195dc04ff4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1612,7 +1612,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.21.2 +xknx==0.21.3 # homeassistant.components.bluesound # homeassistant.components.fritz From c7b4aca998936da50ce4dabe8db138aebe75f33c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 May 2022 15:29:22 +0200 Subject: [PATCH 0577/3516] Add more to no implicit reexport modules (#71947) --- mypy.ini | 7 +++++++ script/hassfest/mypy_config.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/mypy.ini b/mypy.ini index 898c6b860f0..0f698bda35d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2271,6 +2271,7 @@ disallow_untyped_defs = true no_implicit_optional = true warn_return_any = true warn_unreachable = true +no_implicit_reexport = true [mypy-homeassistant.components.uptime.*] check_untyped_defs = true @@ -2514,6 +2515,12 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.application_credentials.*] +no_implicit_reexport = true + +[mypy-homeassistant.components.spotify.*] +no_implicit_reexport = true + [mypy-homeassistant.components.diagnostics.*] no_implicit_reexport = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 2b7c1e00f94..3517307548b 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -199,8 +199,11 @@ IGNORED_MODULES: Final[list[str]] = [ # Component modules which should set no_implicit_reexport = true. NO_IMPLICIT_REEXPORT_MODULES: set[str] = { "homeassistant.components", + "homeassistant.components.application_credentials.*", "homeassistant.components.diagnostics.*", + "homeassistant.components.spotify.*", "homeassistant.components.stream.*", + "homeassistant.components.update.*", } HEADER: Final = """ From 69cc6ab5f1d58adc586c3b300a4f7f0cde2cd0c2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 May 2022 16:40:45 +0200 Subject: [PATCH 0578/3516] Clean up accessing entity_registry.async_get_registry helper via hass (#72005) --- homeassistant/auth/auth_store.py | 9 ++++----- .../components/binary_sensor/device_trigger.py | 7 +++---- homeassistant/components/device_automation/entity.py | 7 +++---- .../components/device_automation/toggle_entity.py | 11 +++++++---- homeassistant/components/emulated_kasa/__init__.py | 6 +++--- homeassistant/components/evohome/__init__.py | 3 ++- homeassistant/components/geniushub/__init__.py | 4 ++-- homeassistant/components/heos/__init__.py | 8 ++++---- homeassistant/components/homekit/aidmanager.py | 10 ++++------ homeassistant/components/nest/__init__.py | 4 ++-- homeassistant/components/risco/sensor.py | 5 ++--- homeassistant/components/samsungtv/__init__.py | 5 +++-- homeassistant/components/sensor/device_trigger.py | 7 +++---- homeassistant/components/seventeentrack/sensor.py | 8 ++++++-- homeassistant/components/shelly/utils.py | 4 ++-- homeassistant/components/unifi/switch.py | 8 +++++--- homeassistant/components/vera/config_flow.py | 6 ++---- homeassistant/components/withings/common.py | 7 ++----- homeassistant/helpers/service.py | 2 +- tests/auth/test_auth_store.py | 4 ++-- tests/components/kraken/test_sensor.py | 3 ++- tests/components/lcn/test_cover.py | 3 ++- tests/components/lcn/test_light.py | 3 ++- tests/components/lcn/test_switch.py | 3 ++- tests/components/nws/test_sensor.py | 5 +++-- tests/components/octoprint/test_binary_sensor.py | 5 +++-- tests/components/picnic/test_sensor.py | 5 ++--- tests/components/plex/test_device_handling.py | 9 +++++---- tests/components/prosegur/test_alarm_control_panel.py | 4 ++-- tests/components/subaru/test_lock.py | 3 ++- .../totalconnect/test_alarm_control_panel.py | 3 ++- 31 files changed, 89 insertions(+), 82 deletions(-) diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 398a4ec839a..d2c3db79726 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -9,6 +9,7 @@ from logging import getLogger from typing import Any from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import dt as dt_util from . import models @@ -303,11 +304,9 @@ class AuthStore: async def _async_load_task(self) -> None: """Load the users.""" - [ent_reg, dev_reg, data] = await asyncio.gather( - self.hass.helpers.entity_registry.async_get_registry(), - self.hass.helpers.device_registry.async_get_registry(), - self._store.async_load(), - ) + dev_reg = dr.async_get(self.hass) + ent_reg = er.async_get(self.hass) + data = await self._store.async_load() # Make sure that we're not overriding data if 2 loads happened at the # same time diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index 9989e415242..5b20b991403 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -8,9 +8,8 @@ from homeassistant.components.device_automation.const import ( ) from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import CONF_ENTITY_ID, CONF_FOR, CONF_TYPE -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity import get_device_class -from homeassistant.helpers.entity_registry import async_entries_for_device from . import DOMAIN, BinarySensorDeviceClass @@ -280,11 +279,11 @@ async def async_attach_trigger(hass, config, action, automation_info): async def async_get_triggers(hass, device_id): """List device triggers.""" triggers = [] - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(hass) entries = [ entry - for entry in async_entries_for_device(entity_registry, device_id) + for entry in er.async_entries_for_device(entity_registry, device_id) if entry.domain == DOMAIN ] diff --git a/homeassistant/components/device_automation/entity.py b/homeassistant/components/device_automation/entity.py index b27a8c67561..9fa878ea038 100644 --- a/homeassistant/components/device_automation/entity.py +++ b/homeassistant/components/device_automation/entity.py @@ -12,8 +12,7 @@ from homeassistant.components.automation import ( from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import CONF_ENTITY_ID, CONF_FOR, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.typing import ConfigType from . import DEVICE_TRIGGER_BASE_SCHEMA @@ -68,11 +67,11 @@ async def _async_get_automations( ) -> list[dict[str, str]]: """List device automations.""" automations: list[dict[str, str]] = [] - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(hass) entries = [ entry - for entry in async_entries_for_device(entity_registry, device_id) + for entry in er.async_entries_for_device(entity_registry, device_id) if entry.domain == domain ] diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 4559e88f9d3..4ae32927bf4 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -20,8 +20,11 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback -from homeassistant.helpers import condition, config_validation as cv -from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers import ( + condition, + config_validation as cv, + entity_registry as er, +) from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DEVICE_TRIGGER_BASE_SCHEMA, entity @@ -187,11 +190,11 @@ async def _async_get_automations( ) -> list[dict[str, str]]: """List device automations.""" automations: list[dict[str, str]] = [] - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(hass) entries = [ entry - for entry in async_entries_for_device(entity_registry, device_id) + for entry in er.async_entries_for_device(entity_registry, device_id) if entry.domain == domain ] diff --git a/homeassistant/components/emulated_kasa/__init__.py b/homeassistant/components/emulated_kasa/__init__.py index 9643962ecb4..41198f922cf 100644 --- a/homeassistant/components/emulated_kasa/__init__.py +++ b/homeassistant/components/emulated_kasa/__init__.py @@ -14,8 +14,8 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.template import Template, is_template_string from homeassistant.helpers.typing import ConfigType @@ -79,7 +79,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: 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() + entity_registry = er.async_get(hass) for entity_id, entity_config in entity_configs.items(): if (state := hass.states.get(entity_id)) is None: _LOGGER.debug("Entity not found: %s", entity_id) @@ -108,7 +108,7 @@ async def validate_configs(hass, entity_configs): _LOGGER.debug("No power value defined for: %s", entity_id) -def get_system_unique_id(entity: RegistryEntry): +def get_system_unique_id(entity: er.RegistryEntry): """Determine the system wide unique_id for an entity.""" return f"{entity.platform}.{entity.domain}.{entity.unique_id}" diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 5f83caa8648..0085eb701d0 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -24,6 +24,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -297,7 +298,7 @@ def setup_service_functions(hass: HomeAssistant, broker): """Set the zone override (setpoint).""" entity_id = call.data[ATTR_ENTITY_ID] - registry = await hass.helpers.entity_registry.async_get_registry() + registry = er.async_get(hass) registry_entry = registry.async_get(entity_id) if registry_entry is None or registry_entry.platform != DOMAIN: diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 947dd325064..3c5cc22af81 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -21,7 +21,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import ( @@ -146,7 +146,7 @@ def setup_service_functions(hass: HomeAssistant, broker): """Set the system mode.""" entity_id = call.data[ATTR_ENTITY_ID] - registry = await hass.helpers.entity_registry.async_get_registry() + registry = er.async_get(hass) registry_entry = registry.async_get(entity_id) if registry_entry is None or registry_entry.platform != DOMAIN: diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index dbd66e28307..a7f56e91368 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -12,6 +12,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.helpers import device_registry as dr, entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -167,10 +168,9 @@ class ControllerManager: async def connect_listeners(self): """Subscribe to events of interest.""" - self._device_registry, self._entity_registry = await asyncio.gather( - self._hass.helpers.device_registry.async_get_registry(), - self._hass.helpers.entity_registry.async_get_registry(), - ) + self._device_registry = dr.async_get(self._hass) + self._entity_registry = er.async_get(self._hass) + # Handle controller events self._signals.append( self.controller.dispatcher.connect( diff --git a/homeassistant/components/homekit/aidmanager.py b/homeassistant/components/homekit/aidmanager.py index ddba9d02bcd..27bd6234d46 100644 --- a/homeassistant/components/homekit/aidmanager.py +++ b/homeassistant/components/homekit/aidmanager.py @@ -17,7 +17,7 @@ import random from fnvhash import fnv1a_32 from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_registry import EntityRegistry, RegistryEntry +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.storage import Store from .util import get_aid_storage_filename_for_entry_id @@ -34,7 +34,7 @@ AID_MIN = 2 AID_MAX = 18446744073709551615 -def get_system_unique_id(entity: RegistryEntry) -> str: +def get_system_unique_id(entity: er.RegistryEntry) -> str: """Determine the system wide unique_id for an entity.""" return f"{entity.platform}.{entity.domain}.{entity.unique_id}" @@ -74,13 +74,11 @@ class AccessoryAidStorage: self.allocated_aids: set[int] = set() self._entry_id = entry_id self.store: Store | None = None - self._entity_registry: EntityRegistry | None = None + self._entity_registry: er.EntityRegistry | None = None async def async_initialize(self) -> None: """Load the latest AID data.""" - self._entity_registry = ( - await self.hass.helpers.entity_registry.async_get_registry() - ) + self._entity_registry = er.async_get(self.hass) aidstore = get_aid_storage_filename_for_entry_id(self._entry_id) self.store = Store(self.hass, AID_MANAGER_STORAGE_VERSION, aidstore) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 076b9a58814..5a34df1d74b 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -42,7 +42,7 @@ from homeassistant.exceptions import ( HomeAssistantError, Unauthorized, ) -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.typing import ConfigType @@ -266,7 +266,7 @@ class NestEventViewBase(HomeAssistantView, ABC): ) -> web.StreamResponse: """Start a GET request.""" user = request[KEY_HASS_USER] - entity_registry = await self.hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(self.hass) for entry in async_entries_for_device(entity_registry, device_id): if not user.permissions.check_entity(entry.entity_id, POLICY_READ): raise Unauthorized(entity_id=entry.entity_id) diff --git a/homeassistant/components/risco/sensor.py b/homeassistant/components/risco/sensor.py index 3f3dc221fac..6038c2911c9 100644 --- a/homeassistant/components/risco/sensor.py +++ b/homeassistant/components/risco/sensor.py @@ -3,6 +3,7 @@ from homeassistant.components.binary_sensor import DOMAIN as BS_DOMAIN from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util @@ -75,9 +76,7 @@ class RiscoSensor(CoordinatorEntity, SensorEntity): async def async_added_to_hass(self): """When entity is added to hass.""" - self._entity_registry = ( - await self.hass.helpers.entity_registry.async_get_registry() - ) + self._entity_registry = er.async_get(self.hass) self.async_on_remove( self.coordinator.async_add_listener(self._refresh_from_coordinator) ) diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 49963734e83..b870aab62d4 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -26,6 +26,7 @@ from homeassistant.const import ( ) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr, entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.typing import ConfigType @@ -313,10 +314,10 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> # 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 = dr.async_get(hass) dev_reg.async_clear_config_entry(config_entry.entry_id) - en_reg = await hass.helpers.entity_registry.async_get_registry() + en_reg = er.async_get(hass) en_reg.async_clear_config_entry(config_entry.entry_id) version = config_entry.version = 2 diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index f90022cf5f3..d760b92b31c 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -16,13 +16,12 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity import ( get_capability, get_device_class, get_unit_of_measurement, ) -from homeassistant.helpers.entity_registry import async_entries_for_device from . import ATTR_STATE_CLASS, DOMAIN, SensorDeviceClass @@ -159,11 +158,11 @@ async def async_attach_trigger(hass, config, action, automation_info): async def async_get_triggers(hass, device_id): """List device triggers.""" triggers = [] - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(hass) entries = [ entry - for entry in async_entries_for_device(entity_registry, device_id) + for entry in er.async_entries_for_device(entity_registry, device_id) if entry.domain == DOMAIN ] diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index 36fabaa7337..12cdcc0680c 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -19,7 +19,11 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers import ( + aiohttp_client, + config_validation as cv, + entity_registry as er, +) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -234,7 +238,7 @@ class SeventeenTrackPackageSensor(SensorEntity): """Remove entity itself.""" await self.async_remove(force_remove=True) - reg = await self.hass.helpers.entity_registry.async_get_registry() + reg = er.async_get(self.hass) entity_id = reg.async_get_entity_id( "sensor", "seventeentrack", diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 77c09283fbf..0398250df71 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -11,7 +11,7 @@ from aioshelly.rpc_device import RpcDevice from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry, singleton +from homeassistant.helpers import device_registry, entity_registry, singleton from homeassistant.helpers.typing import EventType from homeassistant.util.dt import utcnow @@ -34,7 +34,7 @@ async def async_remove_shelly_entity( hass: HomeAssistant, domain: str, unique_id: str ) -> None: """Remove a Shelly entity.""" - entity_reg = await hass.helpers.entity_registry.async_get_registry() + entity_reg = entity_registry.async_get(hass) entity_id = entity_reg.async_get_entity_id(domain, DOMAIN, unique_id) if entity_id: LOGGER.debug("Removing entity: %s", entity_id) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 9151c81543a..67c4d5c4544 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -20,6 +20,7 @@ from homeassistant.components.switch import DOMAIN, SwitchDeviceClass, SwitchEnt from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_NAME from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, DeviceEntryType, @@ -27,7 +28,6 @@ from homeassistant.helpers.device_registry import ( from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.helpers.restore_state import RestoreEntity from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN @@ -65,8 +65,10 @@ async def async_setup_entry( # Store previously known POE control entities in case their POE are turned off. known_poe_clients = [] - entity_registry = await hass.helpers.entity_registry.async_get_registry() - for entry in async_entries_for_config_entry(entity_registry, config_entry.entry_id): + entity_registry = er.async_get(hass) + for entry in er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ): if not entry.unique_id.startswith(POE_SWITCH): continue diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index b2c4fe5e5db..319dcd031d0 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -14,7 +14,7 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import callback -from homeassistant.helpers.entity_registry import EntityRegistry +from homeassistant.helpers import entity_registry as er from .const import CONF_CONTROLLER, CONF_LEGACY_UNIQUE_ID, DOMAIN @@ -119,9 +119,7 @@ class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # If there are entities with the legacy unique_id, then this imported config # should also use the legacy unique_id for entity creation. - entity_registry: EntityRegistry = ( - await self.hass.helpers.entity_registry.async_get_registry() - ) + entity_registry = er.async_get(self.hass) use_legacy_unique_id = ( len( [ diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index c0dadb47924..78ae375b4bc 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -41,7 +41,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers import config_entry_oauth2_flow, entity_registry as er from homeassistant.helpers.config_entry_oauth2_flow import ( AUTH_CALLBACK_PATH, AbstractOAuth2Implementation, @@ -49,7 +49,6 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, ) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.helpers.network import get_url from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util import dt @@ -918,9 +917,7 @@ async def async_get_entity_id( hass: HomeAssistant, attribute: WithingsAttribute, user_id: int ) -> str | None: """Get an entity id for a user's attribute.""" - entity_registry: EntityRegistry = ( - await hass.helpers.entity_registry.async_get_registry() - ) + entity_registry = er.async_get(hass) unique_id = get_attribute_unique_id(attribute, user_id) entity_id = entity_registry.async_get_entity_id( diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 975b05067b2..9a1e6caa27e 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -791,7 +791,7 @@ def verify_domain_control( user_id=call.context.user_id, ) - reg = await hass.helpers.entity_registry.async_get_registry() + reg = entity_registry.async_get(hass) authorized = False diff --git a/tests/auth/test_auth_store.py b/tests/auth/test_auth_store.py index 0c650adba3c..e52a0dc88f5 100644 --- a/tests/auth/test_auth_store.py +++ b/tests/auth/test_auth_store.py @@ -236,9 +236,9 @@ async def test_loading_race_condition(hass): """Test only one storage load called when concurrent loading occurred .""" store = auth_store.AuthStore(hass) with patch( - "homeassistant.helpers.entity_registry.async_get_registry" + "homeassistant.helpers.entity_registry.async_get" ) as mock_ent_registry, patch( - "homeassistant.helpers.device_registry.async_get_registry" + "homeassistant.helpers.device_registry.async_get" ) as mock_dev_registry, patch( "homeassistant.helpers.storage.Store.async_load", return_value=None ) as mock_load: diff --git a/tests/components/kraken/test_sensor.py b/tests/components/kraken/test_sensor.py index bc37b67e676..cbfda938f99 100644 --- a/tests/components/kraken/test_sensor.py +++ b/tests/components/kraken/test_sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.kraken.const import ( DOMAIN, ) from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryType import homeassistant.util.dt as dt_util @@ -52,7 +53,7 @@ async def test_sensor(hass): ) entry.add_to_hass(hass) - registry = await hass.helpers.entity_registry.async_get_registry() + registry = er.async_get(hass) # Pre-create registry entries for disabled by default sensors registry.async_get_or_create( diff --git a/tests/components/lcn/test_cover.py b/tests/components/lcn/test_cover.py index f89cfa41071..8c6b814e525 100644 --- a/tests/components/lcn/test_cover.py +++ b/tests/components/lcn/test_cover.py @@ -18,6 +18,7 @@ from homeassistant.const import ( STATE_OPENING, STATE_UNAVAILABLE, ) +from homeassistant.helpers import entity_registry as er from .conftest import MockModuleConnection @@ -35,7 +36,7 @@ async def test_setup_lcn_cover(hass, entry, lcn_connection): async def test_entity_attributes(hass, entry, lcn_connection): """Test the attributes of an entity.""" - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(hass) entity_outputs = entity_registry.async_get("cover.cover_outputs") diff --git a/tests/components/lcn/test_light.py b/tests/components/lcn/test_light.py index c74ecd2beb9..efde0daa68f 100644 --- a/tests/components/lcn/test_light.py +++ b/tests/components/lcn/test_light.py @@ -23,6 +23,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.helpers import entity_registry as er from .conftest import MockModuleConnection @@ -54,7 +55,7 @@ async def test_entity_state(hass, lcn_connection): async def test_entity_attributes(hass, entry, lcn_connection): """Test the attributes of an entity.""" - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(hass) entity_output = entity_registry.async_get("light.light_output1") diff --git a/tests/components/lcn/test_switch.py b/tests/components/lcn/test_switch.py index aee063f310d..8c4fb1ff0a8 100644 --- a/tests/components/lcn/test_switch.py +++ b/tests/components/lcn/test_switch.py @@ -15,6 +15,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.helpers import entity_registry as er from .conftest import MockModuleConnection @@ -34,7 +35,7 @@ async def test_setup_lcn_switch(hass, lcn_connection): async def test_entity_attributes(hass, entry, lcn_connection): """Test the attributes of an entity.""" - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(hass) entity_output = entity_registry.async_get("switch.switch_output1") diff --git a/tests/components/nws/test_sensor.py b/tests/components/nws/test_sensor.py index aa5ca3bf66c..5a55907e53b 100644 --- a/tests/components/nws/test_sensor.py +++ b/tests/components/nws/test_sensor.py @@ -4,6 +4,7 @@ import pytest from homeassistant.components.nws.const import ATTRIBUTION, DOMAIN, SENSOR_TYPES from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNKNOWN +from homeassistant.helpers import entity_registry as er from homeassistant.util import slugify from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM @@ -33,7 +34,7 @@ async def test_imperial_metric( hass, units, result_observation, result_forecast, mock_simple_nws, no_weather ): """Test with imperial and metric units.""" - registry = await hass.helpers.entity_registry.async_get_registry() + registry = er.async_get(hass) for description in SENSOR_TYPES: registry.async_get_or_create( @@ -66,7 +67,7 @@ async def test_none_values(hass, mock_simple_nws, no_weather): instance = mock_simple_nws.return_value instance.observation = NONE_OBSERVATION - registry = await hass.helpers.entity_registry.async_get_registry() + registry = er.async_get(hass) for description in SENSOR_TYPES: registry.async_get_or_create( diff --git a/tests/components/octoprint/test_binary_sensor.py b/tests/components/octoprint/test_binary_sensor.py index 55e240eb282..e4a028f346a 100644 --- a/tests/components/octoprint/test_binary_sensor.py +++ b/tests/components/octoprint/test_binary_sensor.py @@ -1,6 +1,7 @@ """The tests for Octoptint binary sensor module.""" from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.helpers import entity_registry as er from . import init_integration @@ -16,7 +17,7 @@ async def test_sensors(hass): } await init_integration(hass, "binary_sensor", printer=printer) - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(hass) state = hass.states.get("binary_sensor.octoprint_printing") assert state is not None @@ -37,7 +38,7 @@ 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() + entity_registry = er.async_get(hass) state = hass.states.get("binary_sensor.octoprint_printing") assert state is not None diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py index 7b1bdeb1d12..808f1ce6f41 100644 --- a/tests/components/picnic/test_sensor.py +++ b/tests/components/picnic/test_sensor.py @@ -17,6 +17,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.util import dt @@ -97,9 +98,7 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): """Set up things to be run when tests are started.""" self.hass = await async_test_home_assistant(None) - self.entity_registry = ( - await self.hass.helpers.entity_registry.async_get_registry() - ) + self.entity_registry = er.async_get(self.hass) # Patch the api client self.picnic_patcher = patch("homeassistant.components.picnic.PicnicAPI") diff --git a/tests/components/plex/test_device_handling.py b/tests/components/plex/test_device_handling.py index 2c23ee7cb09..6d2b7524d34 100644 --- a/tests/components/plex/test_device_handling.py +++ b/tests/components/plex/test_device_handling.py @@ -2,14 +2,15 @@ from homeassistant.components.plex.const import DOMAIN from homeassistant.const import Platform +from homeassistant.helpers import device_registry as dr, entity_registry as er async def test_cleanup_orphaned_devices(hass, entry, setup_plex_server): """Test cleaning up orphaned devices on startup.""" test_device_id = {(DOMAIN, "temporary_device_123")} - device_registry = await hass.helpers.device_registry.async_get_registry() - entity_registry = await hass.helpers.entity_registry.async_get_registry() + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) test_device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, @@ -44,8 +45,8 @@ async def test_migrate_transient_devices( non_plexweb_device_id = {(DOMAIN, "1234567890123456-com-plexapp-android")} plex_client_service_device_id = {(DOMAIN, "plex.tv-clients")} - device_registry = await hass.helpers.device_registry.async_get_registry() - entity_registry = await hass.helpers.entity_registry.async_get_registry() + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) # Pre-create devices and entities to test device migration plexweb_device = device_registry.async_get_or_create( diff --git a/tests/components/prosegur/test_alarm_control_panel.py b/tests/components/prosegur/test_alarm_control_panel.py index 9ab0c0d37de..8a50319047e 100644 --- a/tests/components/prosegur/test_alarm_control_panel.py +++ b/tests/components/prosegur/test_alarm_control_panel.py @@ -18,7 +18,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_UNAVAILABLE, ) -from homeassistant.helpers import entity_component +from homeassistant.helpers import entity_component, entity_registry as er from .common import CONTRACT, setup_platform @@ -49,7 +49,7 @@ def mock_status(request): async def test_entity_registry(hass, mock_auth, mock_status): """Tests that the devices are registered in the entity registry.""" await setup_platform(hass) - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(hass) entry = entity_registry.async_get(PROSEGUR_ALARM_ENTITY) # Prosegur alarm device unique_id is the contract id associated to the alarm account diff --git a/tests/components/subaru/test_lock.py b/tests/components/subaru/test_lock.py index 19918ba205c..7ccc1e5fdf5 100644 --- a/tests/components/subaru/test_lock.py +++ b/tests/components/subaru/test_lock.py @@ -13,6 +13,7 @@ from homeassistant.components.subaru.const import ( ) from homeassistant.const import ATTR_ENTITY_ID, SERVICE_LOCK, SERVICE_UNLOCK from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er from .conftest import MOCK_API @@ -23,7 +24,7 @@ DEVICE_ID = "lock.test_vehicle_2_door_locks" async def test_device_exists(hass, ev_entry): """Test subaru lock entity exists.""" - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(hass) entry = entity_registry.async_get(DEVICE_ID) assert entry diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py index 3066dfff172..07cb5f3d40a 100644 --- a/tests/components/totalconnect/test_alarm_control_panel.py +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -30,6 +30,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er from homeassistant.util import dt from .common import ( @@ -76,7 +77,7 @@ async def test_attributes(hass: HomeAssistant) -> None: mock_request.assert_called_once() assert state.attributes.get(ATTR_FRIENDLY_NAME) == "test" - entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_registry = er.async_get(hass) entry = entity_registry.async_get(ENTITY_ID) # TotalConnect partition #1 alarm device unique_id is the location_id assert entry.unique_id == LOCATION_ID From 4d8593402ef033108c04f69e02674608306fa3c6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 May 2022 17:35:03 +0200 Subject: [PATCH 0579/3516] Fix no-implicit-reexport sorting issue (#72015) --- mypy.ini | 4 ++-- script/hassfest/mypy_config.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mypy.ini b/mypy.ini index 0f698bda35d..8f0b1868bce 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2518,10 +2518,10 @@ warn_unreachable = true [mypy-homeassistant.components.application_credentials.*] no_implicit_reexport = true -[mypy-homeassistant.components.spotify.*] +[mypy-homeassistant.components.diagnostics.*] no_implicit_reexport = true -[mypy-homeassistant.components.diagnostics.*] +[mypy-homeassistant.components.spotify.*] no_implicit_reexport = true [mypy-tests.*] diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 3517307548b..0b705fab983 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -367,7 +367,9 @@ def generate_and_validate(config: Config) -> str: if strict_module in NO_IMPLICIT_REEXPORT_MODULES: mypy_config.set(strict_section, "no_implicit_reexport", "true") - for reexport_module in NO_IMPLICIT_REEXPORT_MODULES.difference(strict_modules): + for reexport_module in sorted( + NO_IMPLICIT_REEXPORT_MODULES.difference(strict_modules) + ): reexport_section = f"mypy-{reexport_module}" mypy_config.add_section(reexport_section) mypy_config.set(reexport_section, "no_implicit_reexport", "true") From e4573273dcc91035fcb06ff4afd051c957de6963 Mon Sep 17 00:00:00 2001 From: Elad Bar <3207137+elad-bar@users.noreply.github.com> Date: Tue, 17 May 2022 18:57:19 +0300 Subject: [PATCH 0580/3516] Add Tuya Multi-functional Sensor (dgnbj) (#71778) Co-authored-by: Franck Nijhof --- .../components/tuya/binary_sensor.py | 73 +++++++++++++++++ homeassistant/components/tuya/number.py | 9 +++ homeassistant/components/tuya/select.py | 9 +++ homeassistant/components/tuya/sensor.py | 78 +++++++++++++++++++ homeassistant/components/tuya/siren.py | 8 ++ 5 files changed, 177 insertions(+) diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 2c61151bfaf..d5e4a9b22b0 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -46,6 +46,79 @@ TAMPER_BINARY_SENSOR = TuyaBinarySensorEntityDescription( # end up being a binary sensor. # https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { + # Multi-functional Sensor + # https://developer.tuya.com/en/docs/iot/categorydgnbj?id=Kaiuz3yorvzg3 + "dgnbj": ( + TuyaBinarySensorEntityDescription( + key=DPCode.GAS_SENSOR_STATE, + name="Gas", + icon="mdi:gas-cylinder", + device_class=BinarySensorDeviceClass.SAFETY, + on_value="alarm", + ), + TuyaBinarySensorEntityDescription( + key=DPCode.CH4_SENSOR_STATE, + name="Methane", + device_class=BinarySensorDeviceClass.GAS, + on_value="alarm", + ), + TuyaBinarySensorEntityDescription( + key=DPCode.VOC_STATE, + name="Volatile Organic Compound", + device_class=BinarySensorDeviceClass.SAFETY, + on_value="alarm", + ), + TuyaBinarySensorEntityDescription( + key=DPCode.PM25_STATE, + name="Particulate Matter 2.5 µm", + device_class=BinarySensorDeviceClass.SAFETY, + on_value="alarm", + ), + TuyaBinarySensorEntityDescription( + key=DPCode.CO_STATE, + name="Carbon Monoxide", + icon="mdi:molecule-co", + device_class=BinarySensorDeviceClass.SAFETY, + on_value="alarm", + ), + TuyaBinarySensorEntityDescription( + key=DPCode.CO2_STATE, + icon="mdi:molecule-co2", + name="Carbon Dioxide", + device_class=BinarySensorDeviceClass.SAFETY, + on_value="alarm", + ), + TuyaBinarySensorEntityDescription( + key=DPCode.CH2O_STATE, + name="Formaldehyde", + device_class=BinarySensorDeviceClass.SAFETY, + on_value="alarm", + ), + TuyaBinarySensorEntityDescription( + key=DPCode.DOORCONTACT_STATE, + name="Door", + device_class=BinarySensorDeviceClass.DOOR, + ), + TuyaBinarySensorEntityDescription( + key=DPCode.WATERSENSOR_STATE, + name="Water Leak", + device_class=BinarySensorDeviceClass.MOISTURE, + on_value="alarm", + ), + TuyaBinarySensorEntityDescription( + key=DPCode.PRESSURE_STATE, + name="Pressure", + on_value="alarm", + ), + TuyaBinarySensorEntityDescription( + key=DPCode.SMOKE_SENSOR_STATE, + name="Smoke", + icon="mdi:smoke-detector", + device_class=BinarySensorDeviceClass.SMOKE, + on_value="alarm", + ), + TAMPER_BINARY_SENSOR, + ), # CO2 Detector # https://developer.tuya.com/en/docs/iot/categoryco2bj?id=Kaiuz3wes7yuy "co2bj": ( diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index d9cde61a276..35efd78871f 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -18,6 +18,15 @@ from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, DPType # 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, ...]] = { + # Multi-functional Sensor + # https://developer.tuya.com/en/docs/iot/categorydgnbj?id=Kaiuz3yorvzg3 + "dgnbj": ( + NumberEntityDescription( + key=DPCode.ALARM_TIME, + name="Time", + entity_category=EntityCategory.CONFIG, + ), + ), # Smart Kettle # https://developer.tuya.com/en/docs/iot/fbh?id=K9gf484m21yq7 "bh": ( diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index d9103b916f4..974268c109f 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -18,6 +18,15 @@ from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, DPType, TuyaDeviceClass # 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, ...]] = { + # Multi-functional Sensor + # https://developer.tuya.com/en/docs/iot/categorydgnbj?id=Kaiuz3yorvzg3 + "dgnbj": ( + SelectEntityDescription( + key=DPCode.ALARM_VOLUME, + name="Volume", + entity_category=EntityCategory.CONFIG, + ), + ), # Coffee maker # https://developer.tuya.com/en/docs/iot/categorykfj?id=Kaiuz2p12pc7f "kfj": ( diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 3ee88d2d57b..acb2ffe7987 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -82,6 +82,84 @@ BATTERY_SENSORS: tuple[TuyaSensorEntityDescription, ...] = ( # end up being a sensor. # https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { + # Multi-functional Sensor + # https://developer.tuya.com/en/docs/iot/categorydgnbj?id=Kaiuz3yorvzg3 + "dgnbj": ( + TuyaSensorEntityDescription( + key=DPCode.GAS_SENSOR_VALUE, + name="Gas", + icon="mdi:gas-cylinder", + device_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.CH4_SENSOR_VALUE, + name="Methane", + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.VOC_VALUE, + name="Volatile Organic Compound", + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.PM25_VALUE, + name="Particulate Matter 2.5 µm", + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.CO_VALUE, + name="Carbon Monoxide", + icon="mdi:molecule-co", + device_class=SensorDeviceClass.CO, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.CO2_VALUE, + name="Carbon Dioxide", + icon="mdi:molecule-co2", + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.CH2O_VALUE, + name="Formaldehyde", + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.BRIGHT_STATE, + name="Luminosity", + icon="mdi:brightness-6", + ), + TuyaSensorEntityDescription( + key=DPCode.BRIGHT_VALUE, + name="Luminosity", + icon="mdi:brightness-6", + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.TEMP_CURRENT, + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.HUMIDITY_VALUE, + name="Humidity", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.SMOKE_SENSOR_VALUE, + name="Smoke Amount", + icon="mdi:smoke-detector", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorStateClass.MEASUREMENT, + ), + *BATTERY_SENSORS, + ), # Smart Kettle # https://developer.tuya.com/en/docs/iot/fbh?id=K9gf484m21yq7 "bh": ( diff --git a/homeassistant/components/tuya/siren.py b/homeassistant/components/tuya/siren.py index dcb26871bb2..a60e24eca86 100644 --- a/homeassistant/components/tuya/siren.py +++ b/homeassistant/components/tuya/siren.py @@ -22,6 +22,14 @@ 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, ...]] = { + # Multi-functional Sensor + # https://developer.tuya.com/en/docs/iot/categorydgnbj?id=Kaiuz3yorvzg3 + "dgnbj": ( + SirenEntityDescription( + key=DPCode.ALARM_SWITCH, + name="Siren", + ), + ), # Siren Alarm # https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu "sgbj": ( From 0b09376360eef91f95c227f4d2b958010729662d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 May 2022 09:05:49 -0700 Subject: [PATCH 0581/3516] Mobile app to notify when sensor is disabled (#71561) * Mobile app to notify when sensor is disabled * Add entity status to get_config * Allow overriding enabled/disabled --- homeassistant/components/mobile_app/const.py | 2 +- homeassistant/components/mobile_app/entity.py | 4 +- .../components/mobile_app/webhook.py | 47 +++++++++++-- tests/components/mobile_app/test_sensor.py | 69 ++++++++++++++++++- tests/components/mobile_app/test_webhook.py | 53 ++++++++++++-- 5 files changed, 162 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index e6a26430f11..efc105a80ea 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -67,7 +67,7 @@ ERR_INVALID_FORMAT = "invalid_format" ATTR_SENSOR_ATTRIBUTES = "attributes" ATTR_SENSOR_DEVICE_CLASS = "device_class" -ATTR_SENSOR_DEFAULT_DISABLED = "default_disabled" +ATTR_SENSOR_DISABLED = "disabled" ATTR_SENSOR_ENTITY_CATEGORY = "entity_category" ATTR_SENSOR_ICON = "icon" ATTR_SENSOR_NAME = "name" diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index b38774d56d6..d4c4374b8d9 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -9,8 +9,8 @@ from homeassistant.helpers.restore_state import RestoreEntity from .const import ( ATTR_SENSOR_ATTRIBUTES, - ATTR_SENSOR_DEFAULT_DISABLED, ATTR_SENSOR_DEVICE_CLASS, + ATTR_SENSOR_DISABLED, ATTR_SENSOR_ENTITY_CATEGORY, ATTR_SENSOR_ICON, ATTR_SENSOR_STATE, @@ -64,7 +64,7 @@ class MobileAppEntity(RestoreEntity): @property def entity_registry_enabled_default(self) -> bool: """Return if entity should be enabled by default.""" - return not self._config.get(ATTR_SENSOR_DEFAULT_DISABLED) + return not self._config.get(ATTR_SENSOR_DISABLED) @property def device_class(self): diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index cdf6fc874d9..97d386691d1 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -64,8 +64,8 @@ from .const import ( ATTR_NO_LEGACY_ENCRYPTION, ATTR_OS_VERSION, ATTR_SENSOR_ATTRIBUTES, - ATTR_SENSOR_DEFAULT_DISABLED, ATTR_SENSOR_DEVICE_CLASS, + ATTR_SENSOR_DISABLED, ATTR_SENSOR_ENTITY_CATEGORY, ATTR_SENSOR_ICON, ATTR_SENSOR_NAME, @@ -439,6 +439,11 @@ def _gen_unique_id(webhook_id, sensor_unique_id): return f"{webhook_id}_{sensor_unique_id}" +def _extract_sensor_unique_id(webhook_id, unique_id): + """Return a unique sensor ID.""" + return unique_id[len(webhook_id) + 1 :] + + @WEBHOOK_COMMANDS.register("register_sensor") @validate_schema( vol.All( @@ -457,7 +462,7 @@ def _gen_unique_id(webhook_id, sensor_unique_id): 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), - vol.Optional(ATTR_SENSOR_DEFAULT_DISABLED): bool, + vol.Optional(ATTR_SENSOR_DISABLED): bool, }, _validate_state_class_sensor, ) @@ -490,6 +495,15 @@ async def webhook_register_sensor(hass, config_entry, data): ) != entry.original_name: changes["original_name"] = new_name + if ( + should_be_disabled := data.get(ATTR_SENSOR_DISABLED) + ) is None or should_be_disabled == entry.disabled: + pass + elif should_be_disabled: + changes["disabled_by"] = er.RegistryEntryDisabler.INTEGRATION + else: + changes["disabled_by"] = None + for ent_reg_key, data_key in ( ("device_class", ATTR_SENSOR_DEVICE_CLASS), ("unit_of_measurement", ATTR_SENSOR_UOM), @@ -551,6 +565,7 @@ async def webhook_update_sensor_states(hass, config_entry, data): device_name = config_entry.data[ATTR_DEVICE_NAME] resp = {} + entity_registry = er.async_get(hass) for sensor in data: entity_type = sensor[ATTR_SENSOR_TYPE] @@ -559,9 +574,10 @@ async def webhook_update_sensor_states(hass, config_entry, data): unique_store_key = _gen_unique_id(config_entry.data[CONF_WEBHOOK_ID], unique_id) - entity_registry = er.async_get(hass) - if not entity_registry.async_get_entity_id( - entity_type, DOMAIN, unique_store_key + if not ( + entity_id := entity_registry.async_get_entity_id( + entity_type, DOMAIN, unique_store_key + ) ): _LOGGER.debug( "Refusing to update %s non-registered sensor: %s", @@ -601,6 +617,12 @@ async def webhook_update_sensor_states(hass, config_entry, data): resp[unique_id] = {"success": True} + # Check if disabled + entry = entity_registry.async_get(entity_id) + + if entry.disabled_by: + resp[unique_id]["is_disabled"] = True + return webhook_response(resp, registration=config_entry.data) @@ -637,6 +659,21 @@ async def webhook_get_config(hass, config_entry, data): with suppress(hass.components.cloud.CloudNotAvailable): resp[CONF_REMOTE_UI_URL] = cloud.async_remote_ui_url(hass) + webhook_id = config_entry.data[CONF_WEBHOOK_ID] + + entities = {} + for entry in er.async_entries_for_config_entry( + er.async_get(hass), config_entry.entry_id + ): + if entry.domain in ("binary_sensor", "sensor"): + unique_id = _extract_sensor_unique_id(webhook_id, entry.unique_id) + else: + unique_id = entry.unique_id + + entities[unique_id] = {"disabled": entry.disabled} + + resp["entities"] = entities + return webhook_response(resp, registration=config_entry.data) diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index 301c49381f7..c0f7f126a49 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -355,7 +355,7 @@ async def test_default_disabling_entity(hass, create_registrations, webhook_clie "name": "Battery State", "type": "sensor", "unique_id": "battery_state", - "default_disabled": True, + "disabled": True, }, }, ) @@ -373,3 +373,70 @@ async def test_default_disabling_entity(hass, create_registrations, webhook_clie er.async_get(hass).async_get("sensor.test_1_battery_state").disabled_by == er.RegistryEntryDisabler.INTEGRATION ) + + +async def test_updating_disabled_sensor(hass, create_registrations, webhook_client): + """Test that sensors return error if disabled in instance.""" + 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": None, + "type": "sensor", + "unique_id": "battery_state", + }, + }, + ) + + assert reg_resp.status == HTTPStatus.CREATED + + update_resp = await webhook_client.post( + webhook_url, + json={ + "type": "update_sensor_states", + "data": [ + { + "icon": "mdi:battery-unknown", + "state": 123, + "type": "sensor", + "unique_id": "battery_state", + }, + ], + }, + ) + + assert update_resp.status == HTTPStatus.OK + + json = await update_resp.json() + assert json["battery_state"]["success"] is True + assert "is_disabled" not in json["battery_state"] + + er.async_get(hass).async_update_entity( + "sensor.test_1_battery_state", disabled_by=er.RegistryEntryDisabler.USER + ) + + update_resp = await webhook_client.post( + webhook_url, + json={ + "type": "update_sensor_states", + "data": [ + { + "icon": "mdi:battery-unknown", + "state": 123, + "type": "sensor", + "unique_id": "battery_state", + }, + ], + }, + ) + + assert update_resp.status == HTTPStatus.OK + + json = await update_resp.json() + assert json["battery_state"]["success"] is True + assert json["battery_state"]["is_disabled"] is True diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 3eac2d97b19..0bc237b1c11 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -254,10 +254,30 @@ async def test_webhook_handle_get_zones(hass, create_registrations, webhook_clie async def test_webhook_handle_get_config(hass, create_registrations, webhook_client): """Test that we can get config properly.""" - resp = await webhook_client.post( - "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), - json={"type": "get_config"}, - ) + webhook_id = create_registrations[1]["webhook_id"] + webhook_url = f"/api/webhook/{webhook_id}" + + # Create two entities + for sensor in ( + { + "name": "Battery State", + "type": "sensor", + "unique_id": "battery-state-id", + }, + { + "name": "Battery Charging", + "type": "sensor", + "unique_id": "battery-charging-id", + "disabled": True, + }, + ): + reg_resp = await webhook_client.post( + webhook_url, + json={"type": "register_sensor", "data": sensor}, + ) + assert reg_resp.status == HTTPStatus.CREATED + + resp = await webhook_client.post(webhook_url, json={"type": "get_config"}) assert resp.status == HTTPStatus.OK @@ -279,6 +299,11 @@ async def test_webhook_handle_get_config(hass, create_registrations, webhook_cli "components": hass_config["components"], "version": hass_config["version"], "theme_color": "#03A9F4", # Default frontend theme color + "entities": { + "mock-device-id": {"disabled": False}, + "battery-state-id": {"disabled": False}, + "battery-charging-id": {"disabled": True}, + }, } assert expected_dict == json @@ -902,6 +927,7 @@ async def test_reregister_sensor(hass, create_registrations, webhook_client): assert entry.unit_of_measurement is None assert entry.entity_category is None assert entry.original_icon == "mdi:cellphone" + assert entry.disabled_by is None reg_resp = await webhook_client.post( webhook_url, @@ -917,6 +943,7 @@ async def test_reregister_sensor(hass, create_registrations, webhook_client): "entity_category": "diagnostic", "icon": "mdi:new-icon", "unit_of_measurement": "%", + "disabled": True, }, }, ) @@ -928,3 +955,21 @@ async def test_reregister_sensor(hass, create_registrations, webhook_client): assert entry.unit_of_measurement == "%" assert entry.entity_category == "diagnostic" assert entry.original_icon == "mdi:new-icon" + assert entry.disabled_by == er.RegistryEntryDisabler.INTEGRATION + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "name": "New Name", + "type": "sensor", + "unique_id": "abcd", + "disabled": False, + }, + }, + ) + + assert reg_resp.status == HTTPStatus.CREATED + entry = ent_reg.async_get("sensor.test_1_battery_state") + assert entry.disabled_by is None From c8f700c80319cef81a9a817c1b9111887ea98b1a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 May 2022 18:41:36 +0200 Subject: [PATCH 0582/3516] Clean up accessing dispatcher helpers via hass (#72014) Clean up accessing ditpatcher helpers via hass --- .../components/alarmdecoder/__init__.py | 11 ++++---- .../components/aqualogic/__init__.py | 3 ++- .../components/arcam_fmj/__init__.py | 11 +++----- .../components/arcam_fmj/media_player.py | 13 +++------- homeassistant/components/enocean/device.py | 4 +-- homeassistant/components/enocean/dongle.py | 4 +-- .../homekit_controller/connection.py | 5 ++-- .../components/mobile_app/webhook.py | 4 +-- .../components/owntracks/__init__.py | 9 ++++--- .../components/qwikswitch/__init__.py | 9 ++++--- homeassistant/components/rfxtrx/__init__.py | 7 +++-- .../components/tellstick/__init__.py | 9 ++++--- .../components/waterfurnace/__init__.py | 3 ++- .../components/websocket_api/http.py | 9 +++---- .../components/arcam_fmj/test_media_player.py | 26 ++++++------------- tests/components/cloud/test_binary_sensor.py | 5 ++-- 16 files changed, 64 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index 3be3d67e32b..c196ebb8fbc 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.util import dt as dt_util from .const import ( @@ -81,23 +82,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def handle_message(sender, message): """Handle message from AlarmDecoder.""" - hass.helpers.dispatcher.dispatcher_send(SIGNAL_PANEL_MESSAGE, message) + dispatcher_send(hass, SIGNAL_PANEL_MESSAGE, message) def handle_rfx_message(sender, message): """Handle RFX message from AlarmDecoder.""" - hass.helpers.dispatcher.dispatcher_send(SIGNAL_RFX_MESSAGE, message) + dispatcher_send(hass, SIGNAL_RFX_MESSAGE, message) def zone_fault_callback(sender, zone): """Handle zone fault from AlarmDecoder.""" - hass.helpers.dispatcher.dispatcher_send(SIGNAL_ZONE_FAULT, zone) + dispatcher_send(hass, SIGNAL_ZONE_FAULT, zone) def zone_restore_callback(sender, zone): """Handle zone restore from AlarmDecoder.""" - hass.helpers.dispatcher.dispatcher_send(SIGNAL_ZONE_RESTORE, zone) + dispatcher_send(hass, SIGNAL_ZONE_RESTORE, zone) def handle_rel_message(sender, message): """Handle relay or zone expander message from AlarmDecoder.""" - hass.helpers.dispatcher.dispatcher_send(SIGNAL_REL_MESSAGE, message) + dispatcher_send(hass, SIGNAL_REL_MESSAGE, message) baud = ad_connection.get(CONF_DEVICE_BAUD) if protocol == PROTOCOL_SOCKET: diff --git a/homeassistant/components/aqualogic/__init__.py b/homeassistant/components/aqualogic/__init__.py index 0c4ecaa1683..94941b30713 100644 --- a/homeassistant/components/aqualogic/__init__.py +++ b/homeassistant/components/aqualogic/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -70,7 +71,7 @@ class AquaLogicProcessor(threading.Thread): def data_changed(self, panel): """Aqualogic data changed callback.""" - self._hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC) + dispatcher_send(self._hass, UPDATE_TOPIC) def run(self): """Event thread.""" diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index ee86fc8c4b5..a82e0239842 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -11,6 +11,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType from .const import ( @@ -81,7 +82,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _run_client(hass, client, interval): def _listen(_): - hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_CLIENT_DATA, client.host) + async_dispatcher_send(hass, SIGNAL_CLIENT_DATA, client.host) while True: try: @@ -89,9 +90,7 @@ async def _run_client(hass, client, interval): await client.start() _LOGGER.debug("Client connected %s", client.host) - hass.helpers.dispatcher.async_dispatcher_send( - SIGNAL_CLIENT_STARTED, client.host - ) + async_dispatcher_send(hass, SIGNAL_CLIENT_STARTED, client.host) try: with client.listen(_listen): @@ -100,9 +99,7 @@ async def _run_client(hass, client, interval): await client.stop() _LOGGER.debug("Client disconnected %s", client.host) - hass.helpers.dispatcher.async_dispatcher_send( - SIGNAL_CLIENT_STOPPED, client.host - ) + async_dispatcher_send(hass, SIGNAL_CLIENT_STOPPED, client.host) except ConnectionFailed: await asyncio.sleep(interval) diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 245e309af9c..731eb0b0352 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -18,6 +18,7 @@ from homeassistant.components.media_player.errors import BrowseError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -127,21 +128,15 @@ class ArcamFmj(MediaPlayerEntity): self.async_schedule_update_ha_state(force_refresh=True) self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_CLIENT_DATA, _data - ) + async_dispatcher_connect(self.hass, SIGNAL_CLIENT_DATA, _data) ) self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_CLIENT_STARTED, _started - ) + async_dispatcher_connect(self.hass, SIGNAL_CLIENT_STARTED, _started) ) self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_CLIENT_STOPPED, _stopped - ) + async_dispatcher_connect(self.hass, SIGNAL_CLIENT_STOPPED, _stopped) ) async def async_update(self): diff --git a/homeassistant/components/enocean/device.py b/homeassistant/components/enocean/device.py index b57b053f4a7..0bd084742b5 100644 --- a/homeassistant/components/enocean/device.py +++ b/homeassistant/components/enocean/device.py @@ -2,7 +2,7 @@ from enocean.protocol.packet import Packet from enocean.utils import combine_hex -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity from .const import SIGNAL_RECEIVE_MESSAGE, SIGNAL_SEND_MESSAGE @@ -37,4 +37,4 @@ class EnOceanEntity(Entity): """Send a command via the EnOcean dongle.""" packet = Packet(packet_type, data=data, optional=optional) - self.hass.helpers.dispatcher.dispatcher_send(SIGNAL_SEND_MESSAGE, packet) + dispatcher_send(self.hass, SIGNAL_SEND_MESSAGE, packet) diff --git a/homeassistant/components/enocean/dongle.py b/homeassistant/components/enocean/dongle.py index 63ab3e86925..9ccaeba504c 100644 --- a/homeassistant/components/enocean/dongle.py +++ b/homeassistant/components/enocean/dongle.py @@ -7,7 +7,7 @@ from enocean.communicators import SerialCommunicator from enocean.protocol.packet import RadioPacket import serial -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from .const import SIGNAL_RECEIVE_MESSAGE, SIGNAL_SEND_MESSAGE @@ -58,7 +58,7 @@ class EnOceanDongle: if isinstance(packet, RadioPacket): _LOGGER.debug("Received radio packet: %s", packet) - self.hass.helpers.dispatcher.dispatcher_send(SIGNAL_RECEIVE_MESSAGE, packet) + dispatcher_send(self.hass, SIGNAL_RECEIVE_MESSAGE, packet) def detect(): diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 9642d5d3bc5..7a85e234807 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -19,6 +19,7 @@ from aiohomekit.model.services import Service from homeassistant.const import ATTR_VIA_DEVICE from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.event import async_track_time_interval @@ -162,7 +163,7 @@ class HKDevice: if self.available == available: return self.available = available - self.hass.helpers.dispatcher.async_dispatcher_send(self.signal_state_updated) + async_dispatcher_send(self.hass, self.signal_state_updated) async def async_setup(self) -> bool: """Prepare to use a paired HomeKit device in Home Assistant.""" @@ -568,7 +569,7 @@ class HKDevice: # For now we update both self.entity_map.process_changes(new_values_dict) - self.hass.helpers.dispatcher.async_dispatcher_send(self.signal_state_updated) + async_dispatcher_send(self.hass, self.signal_state_updated) async def get_characteristics(self, *args, **kwargs) -> dict[str, Any]: """Read latest state from homekit accessory.""" diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 97d386691d1..61b69ebad5c 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -353,8 +353,8 @@ async def webhook_render_template(hass, config_entry, data): ) async def webhook_update_location(hass, config_entry, data): """Handle an update location webhook.""" - hass.helpers.dispatcher.async_dispatcher_send( - SIGNAL_LOCATION_UPDATE.format(config_entry.entry_id), data + async_dispatcher_send( + hass, SIGNAL_LOCATION_UPDATE.format(config_entry.entry_id), data ) return empty_okay_response() diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index a9f89d26238..5a21862c767 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -18,7 +18,10 @@ 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 +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_when_setup @@ -140,7 +143,7 @@ async def async_connect_mqtt(hass, component): return message["topic"] = msg.topic - hass.helpers.dispatcher.async_dispatcher_send(DOMAIN, hass, context, message) + async_dispatcher_send(hass, DOMAIN, hass, context, message) await mqtt.async_subscribe(hass, context.mqtt_topic, async_handle_mqtt_message, 1) @@ -179,7 +182,7 @@ async def handle_webhook(hass, webhook_id, request): # Keep it as a 200 response so the incorrect packet is discarded return json_response([]) - hass.helpers.dispatcher.async_dispatcher_send(DOMAIN, hass, context, message) + async_dispatcher_send(hass, DOMAIN, hass, context, message) response = [] diff --git a/homeassistant/components/qwikswitch/__init__.py b/homeassistant/components/qwikswitch/__init__.py index e529483d0cc..2df4a2ab73e 100644 --- a/homeassistant/components/qwikswitch/__init__.py +++ b/homeassistant/components/qwikswitch/__init__.py @@ -21,7 +21,10 @@ from homeassistant.core import HomeAssistant, callback 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.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType @@ -150,7 +153,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: def callback_value_changed(_qsd, qsid, _val): """Update entity values based on device change.""" _LOGGER.debug("Dispatch %s (update from devices)", qsid) - hass.helpers.dispatcher.async_dispatcher_send(qsid, None) + async_dispatcher_send(hass, qsid, None) session = async_get_clientsession(hass) qsusb = QSUsb( @@ -221,7 +224,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if qspacket[QS_ID] in sensor_ids: _LOGGER.debug("Dispatch %s ((%s))", qspacket[QS_ID], qspacket) - hass.helpers.dispatcher.async_dispatcher_send(qspacket[QS_ID], qspacket) + async_dispatcher_send(hass, qspacket[QS_ID], qspacket) # Update all ha_objects hass.async_add_job(qsusb.update_from_devices) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index c10075bbb79..89521aba9f2 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -30,7 +30,10 @@ from homeassistant.helpers.device_registry import ( DeviceEntry, DeviceRegistry, ) -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -214,7 +217,7 @@ async def async_setup_internal(hass, entry: ConfigEntry): event_data[ATTR_DEVICE_ID] = device_entry.id # Callback to HA registered components. - hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_EVENT, event, device_id) + async_dispatcher_send(hass, SIGNAL_EVENT, event, device_id) # Signal event to any other listeners hass.bus.async_fire(EVENT_RFXTRX_EVENT, event_data) diff --git a/homeassistant/components/tellstick/__init__.py b/homeassistant/components/tellstick/__init__.py index 8611c99b654..1007867362a 100644 --- a/homeassistant/components/tellstick/__init__.py +++ b/homeassistant/components/tellstick/__init__.py @@ -17,7 +17,10 @@ from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType @@ -147,8 +150,8 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: @callback def async_handle_callback(tellcore_id, tellcore_command, tellcore_data, cid): """Handle the actual callback from Tellcore.""" - hass.helpers.dispatcher.async_dispatcher_send( - SIGNAL_TELLCORE_CALLBACK, tellcore_id, tellcore_command, tellcore_data + async_dispatcher_send( + hass, SIGNAL_TELLCORE_CALLBACK, tellcore_id, tellcore_command, tellcore_data ) # Register callback diff --git a/homeassistant/components/waterfurnace/__init__.py b/homeassistant/components/waterfurnace/__init__.py index 1da170f2b75..107ae7b9d67 100644 --- a/homeassistant/components/waterfurnace/__init__.py +++ b/homeassistant/components/waterfurnace/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -156,5 +157,5 @@ class WaterFurnaceData(threading.Thread): self._reconnect() else: - self.hass.helpers.dispatcher.dispatcher_send(UPDATE_TOPIC) + dispatcher_send(self.hass, UPDATE_TOPIC) time.sleep(SCAN_INTERVAL.total_seconds()) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index a913b81c384..e8972a227c8 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -14,6 +14,7 @@ import async_timeout from homeassistant.components.http import HomeAssistantView from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later from .auth import AuthPhase, auth_required_message @@ -203,9 +204,7 @@ class WebSocketHandler: self.hass.data[DATA_CONNECTIONS] = ( self.hass.data.get(DATA_CONNECTIONS, 0) + 1 ) - self.hass.helpers.dispatcher.async_dispatcher_send( - SIGNAL_WEBSOCKET_CONNECTED - ) + async_dispatcher_send(self.hass, SIGNAL_WEBSOCKET_CONNECTED) # Command phase while not wsock.closed: @@ -258,8 +257,6 @@ class WebSocketHandler: if connection is not None: self.hass.data[DATA_CONNECTIONS] -= 1 - self.hass.helpers.dispatcher.async_dispatcher_send( - SIGNAL_WEBSOCKET_DISCONNECTED - ) + async_dispatcher_send(self.hass, SIGNAL_WEBSOCKET_DISCONNECTED) return wsock diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index 81f6a5a935f..d3b221a05fd 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -1,6 +1,6 @@ """Tests for arcam fmj receivers.""" from math import isclose -from unittest.mock import ANY, MagicMock, PropertyMock, patch +from unittest.mock import ANY, PropertyMock, patch from arcam.fmj import DecodeMode2CH, DecodeModeMCH, SourceCodes import pytest @@ -303,22 +303,12 @@ async def test_added_to_hass(player, state): SIGNAL_CLIENT_STOPPED, ) - connectors = {} + with patch( + "homeassistant.components.arcam_fmj.media_player.async_dispatcher_connect" + ) as connect: + await player.async_added_to_hass() - def _connect(signal, fun): - connectors[signal] = fun - - player.hass = MagicMock() - player.hass.helpers.dispatcher.async_dispatcher_connect.side_effects = _connect - - await player.async_added_to_hass() state.start.assert_called_with() - player.hass.helpers.dispatcher.async_dispatcher_connect.assert_any_call( - SIGNAL_CLIENT_DATA, ANY - ) - player.hass.helpers.dispatcher.async_dispatcher_connect.assert_any_call( - SIGNAL_CLIENT_STARTED, ANY - ) - player.hass.helpers.dispatcher.async_dispatcher_connect.assert_any_call( - SIGNAL_CLIENT_STOPPED, ANY - ) + connect.assert_any_call(player.hass, SIGNAL_CLIENT_DATA, ANY) + connect.assert_any_call(player.hass, SIGNAL_CLIENT_STARTED, ANY) + connect.assert_any_call(player.hass, SIGNAL_CLIENT_STOPPED, ANY) diff --git a/tests/components/cloud/test_binary_sensor.py b/tests/components/cloud/test_binary_sensor.py index c9c9d53981e..0aacb189d72 100644 --- a/tests/components/cloud/test_binary_sensor.py +++ b/tests/components/cloud/test_binary_sensor.py @@ -2,6 +2,7 @@ from unittest.mock import Mock, patch from homeassistant.components.cloud.const import DISPATCHER_REMOTE_UPDATE +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component @@ -29,14 +30,14 @@ async def test_remote_connection_sensor(hass): with patch("homeassistant.components.cloud.binary_sensor.WAIT_UNTIL_CHANGE", 0): cloud.remote.is_connected = False cloud.remote.certificate = object() - hass.helpers.dispatcher.async_dispatcher_send(DISPATCHER_REMOTE_UPDATE, {}) + async_dispatcher_send(hass, DISPATCHER_REMOTE_UPDATE, {}) await hass.async_block_till_done() state = hass.states.get("binary_sensor.remote_ui") assert state.state == "off" cloud.remote.is_connected = True - hass.helpers.dispatcher.async_dispatcher_send(DISPATCHER_REMOTE_UPDATE, {}) + async_dispatcher_send(hass, DISPATCHER_REMOTE_UPDATE, {}) await hass.async_block_till_done() state = hass.states.get("binary_sensor.remote_ui") From 5f44d0f8f9db03371164ff596f7b09615a5f6efe Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 May 2022 18:45:57 +0200 Subject: [PATCH 0583/3516] Clean up accessing storage.Store helper via hass (#72009) --- homeassistant/auth/auth_store.py | 7 ++--- homeassistant/auth/mfa_modules/notify.py | 9 ++++--- homeassistant/auth/mfa_modules/totp.py | 9 ++++--- homeassistant/auth/providers/homeassistant.py | 12 ++++++--- homeassistant/components/alexa/auth.py | 3 ++- .../components/ambiclimate/climate.py | 3 ++- .../components/ambiclimate/config_flow.py | 3 ++- .../components/analytics/analytics.py | 2 +- homeassistant/components/camera/prefs.py | 7 +++-- homeassistant/components/cloud/prefs.py | 3 ++- homeassistant/components/evohome/__init__.py | 3 ++- homeassistant/components/freebox/router.py | 3 ++- homeassistant/components/frontend/__init__.py | 8 +++--- homeassistant/components/frontend/storage.py | 6 +++-- homeassistant/components/hassio/__init__.py | 3 ++- homeassistant/components/http/auth.py | 5 ++-- homeassistant/components/icloud/__init__.py | 3 ++- .../components/icloud/config_flow.py | 9 +++---- .../components/mobile_app/__init__.py | 7 +++-- homeassistant/components/network/network.py | 5 ++-- .../components/smartthings/smartapp.py | 7 ++--- homeassistant/components/unifi/__init__.py | 3 ++- homeassistant/components/zha/core/store.py | 3 ++- homeassistant/core.py | 26 +++++++++++++++---- homeassistant/helpers/area_registry.py | 7 +++-- 25 files changed, 101 insertions(+), 55 deletions(-) diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index d2c3db79726..baf5a8bf3b3 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -10,6 +10,7 @@ from typing import Any from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.storage import Store from homeassistant.util import dt as dt_util from . import models @@ -45,8 +46,8 @@ class AuthStore: self._users: dict[str, models.User] | None = None self._groups: dict[str, models.Group] | None = None self._perm_lookup: PermissionLookup | None = None - self._store = hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True + self._store = Store( + hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) self._lock = asyncio.Lock() @@ -315,7 +316,7 @@ class AuthStore: self._perm_lookup = perm_lookup = PermissionLookup(ent_reg, dev_reg) - if data is None: + if data is None or not isinstance(data, dict): self._set_defaults() return diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 37b35a5087d..3872257a205 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -17,6 +17,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import ServiceNotFound from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.storage import Store from . import ( MULTI_FACTOR_AUTH_MODULE_SCHEMA, @@ -99,8 +100,8 @@ class NotifyAuthModule(MultiFactorAuthModule): """Initialize the user data store.""" super().__init__(hass, config) self._user_settings: _UsersDict | None = None - self._user_store = hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True + self._user_store = Store( + hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) self._include = config.get(CONF_INCLUDE, []) self._exclude = config.get(CONF_EXCLUDE, []) @@ -118,7 +119,9 @@ class NotifyAuthModule(MultiFactorAuthModule): if self._user_settings is not None: return - if (data := await self._user_store.async_load()) is None: + if (data := await self._user_store.async_load()) is None or not isinstance( + data, dict + ): data = {STORAGE_USERS: {}} self._user_settings = { diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index bb5fc47469f..e503198f08b 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.auth.models import User from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.storage import Store from . import ( MULTI_FACTOR_AUTH_MODULE_SCHEMA, @@ -76,8 +77,8 @@ class TotpAuthModule(MultiFactorAuthModule): """Initialize the user data store.""" super().__init__(hass, config) self._users: dict[str, str] | None = None - self._user_store = hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True + self._user_store = Store( + hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) self._init_lock = asyncio.Lock() @@ -92,7 +93,9 @@ class TotpAuthModule(MultiFactorAuthModule): if self._users is not None: return - if (data := await self._user_store.async_load()) is None: + if (data := await self._user_store.async_load()) is None or not isinstance( + data, dict + ): data = {STORAGE_USERS: {}} self._users = data.get(STORAGE_USERS, {}) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 3971cb43080..cb95907c9b2 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -14,6 +14,7 @@ from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.storage import Store from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta @@ -60,8 +61,8 @@ class Data: def __init__(self, hass: HomeAssistant) -> None: """Initialize the user data store.""" self.hass = hass - self._store = hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True + self._store = Store( + hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) self._data: dict[str, Any] | None = None # Legacy mode will allow usernames to start/end with whitespace @@ -79,7 +80,9 @@ class Data: async def async_load(self) -> None: """Load stored data.""" - if (data := await self._store.async_load()) is None: + if (data := await self._store.async_load()) is None or not isinstance( + data, dict + ): data = {"users": []} seen: set[str] = set() @@ -203,7 +206,8 @@ class Data: async def async_save(self) -> None: """Save data.""" - await self._store.async_save(self._data) + if self._data is not None: + await self._store.async_save(self._data) @AUTH_PROVIDERS.register("homeassistant") diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index d888b91a39e..1ce20d154bd 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -11,6 +11,7 @@ import async_timeout from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import callback from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.storage import Store from homeassistant.util import dt _LOGGER = logging.getLogger(__name__) @@ -37,7 +38,7 @@ class Auth: self.client_secret = client_secret self._prefs = None - self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) self._get_token_lock = asyncio.Lock() diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index 0bf4ec35526..93d9348655f 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -23,6 +23,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( @@ -63,7 +64,7 @@ async def async_setup_entry( """Set up the Ambiclimate device from config entry.""" config = entry.data websession = async_get_clientsession(hass) - store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + store = Store(hass, STORAGE_VERSION, STORAGE_KEY) token_info = await store.async_load() oauth = ambiclimate.AmbiclimateOAuth( diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index e93c21ce5ba..bde5e41e392 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -10,6 +10,7 @@ from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.network import get_url +from homeassistant.helpers.storage import Store from .const import ( AUTH_CALLBACK_NAME, @@ -102,7 +103,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.error("Failed to get access token", exc_info=True) return None - store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + store = Store(self.hass, STORAGE_VERSION, STORAGE_KEY) await store.async_save(token_info) return token_info diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 39c813b2696..802aa33585a 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -71,7 +71,7 @@ class Analytics: ATTR_ONBOARDED: False, ATTR_UUID: None, } - self._store: Store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) @property def preferences(self) -> dict: diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index 53a149ff7d8..3d54c10d09a 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Final from homeassistant.core import HomeAssistant +from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import UNDEFINED, UndefinedType from .const import DOMAIN, PREF_PRELOAD_STREAM @@ -35,12 +36,14 @@ class CameraPreferences: def __init__(self, hass: HomeAssistant) -> None: """Initialize camera prefs.""" self._hass = hass - self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) self._prefs: dict[str, dict[str, bool]] | None = None async def async_initialize(self) -> None: """Finish initializing the preferences.""" - if (prefs := await self._store.async_load()) is None: + if (prefs := await self._store.async_load()) is None or not isinstance( + prefs, dict + ): prefs = {} self._prefs = prefs diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index e6747c42c45..275c2a56326 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -5,6 +5,7 @@ from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.auth.models import User from homeassistant.components import webhook from homeassistant.core import callback +from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import UNDEFINED from homeassistant.util.logging import async_create_catching_coro @@ -46,7 +47,7 @@ class CloudPreferences: def __init__(self, hass): """Initialize cloud prefs.""" self._hass = hass - self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) self._prefs = None self._listeners = [] diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 0085eb701d0..aa09bb666f4 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -35,6 +35,7 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_call_later from homeassistant.helpers.service import verify_domain_control +from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util @@ -198,7 +199,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: user_data = tokens.pop(USER_DATA, None) return (tokens, user_data) - store = hass.helpers.storage.Store(STORAGE_VER, STORAGE_KEY) + store = Store(hass, STORAGE_VER, STORAGE_KEY) tokens, user_data = await load_auth_tokens(store) client_v2 = evohomeasync2.EvohomeClient( diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 0d20545fdcd..70fc7b86a40 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -18,6 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.storage import Store from homeassistant.util import slugify from .const import ( @@ -32,7 +33,7 @@ from .const import ( async def get_api(hass: HomeAssistant, host: str) -> Freepybox: """Get the Freebox API.""" - freebox_path = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY).path + freebox_path = Store(hass, STORAGE_VERSION, STORAGE_KEY).path if not os.path.exists(freebox_path): await hass.async_add_executor_job(os.makedirs, freebox_path) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 91b9c9a126f..c1deb02fc6a 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -22,6 +22,7 @@ from homeassistant.const import CONF_MODE, CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import service import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.storage import Store from homeassistant.helpers.translation import async_get_translations from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration, bind_hass @@ -389,11 +390,12 @@ async def _async_setup_themes( """Set up themes data and services.""" hass.data[DATA_THEMES] = themes or {} - store = hass.data[DATA_THEMES_STORE] = hass.helpers.storage.Store( - THEMES_STORAGE_VERSION, THEMES_STORAGE_KEY + store = hass.data[DATA_THEMES_STORE] = Store( + hass, THEMES_STORAGE_VERSION, THEMES_STORAGE_KEY ) - theme_data = await store.async_load() or {} + if not (theme_data := await store.async_load()) or not isinstance(theme_data, dict): + theme_data = {} theme_name = theme_data.get(DATA_DEFAULT_THEME, DEFAULT_THEME) dark_theme_name = theme_data.get(DATA_DEFAULT_DARK_THEME) diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py index e5100ca3ab2..bfda566de55 100644 --- a/homeassistant/components/frontend/storage.py +++ b/homeassistant/components/frontend/storage.py @@ -35,8 +35,10 @@ def with_store(orig_func: Callable) -> Callable: user_id = connection.user.id 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}" + store = stores[user_id] = Store( + hass, + STORAGE_VERSION_USER_DATA, + f"frontend.user_data_{connection.user.id}", ) if user_id not in data: diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index a3689f61746..34773987014 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -41,6 +41,7 @@ from homeassistant.helpers.device_registry import ( async_get_registry, ) from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.loader import bind_hass @@ -519,7 +520,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: if not await hassio.is_connected(): _LOGGER.warning("Not connected with the supervisor / system too busy!") - store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + store = Store(hass, STORAGE_VERSION, STORAGE_KEY) if (data := await store.async_load()) is None: data = {} diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 4788680a6fa..dab6abede4c 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -17,6 +17,7 @@ from homeassistant.auth.const import GROUP_ID_READ_ONLY from homeassistant.auth.models import User from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.storage import Store from homeassistant.util import dt as dt_util from homeassistant.util.network import is_local @@ -108,8 +109,8 @@ def async_user_not_allowed_do_auth( async def async_setup_auth(hass: HomeAssistant, app: Application) -> None: """Create auth middleware for the app.""" - store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - if (data := await store.async_load()) is None: + store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + if (data := await store.async_load()) is None or not isinstance(data, dict): data = {} refresh_token = None diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 17cc15b195a..8175cf43f27 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -5,6 +5,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.storage import Store from homeassistant.util import slugify from .account import IcloudAccount @@ -81,7 +82,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=username) - icloud_dir = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + icloud_dir = Store(hass, STORAGE_VERSION, STORAGE_KEY) account = IcloudAccount( hass, diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py index f3630abfdaa..a4c29acff75 100644 --- a/homeassistant/components/icloud/config_flow.py +++ b/homeassistant/components/icloud/config_flow.py @@ -13,6 +13,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.storage import Store from .const import ( CONF_GPS_ACCURACY_THRESHOLD, @@ -113,7 +114,7 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): PyiCloudService, self._username, self._password, - self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY).path, + Store(self.hass, STORAGE_VERSION, STORAGE_KEY).path, True, None, self._with_family, @@ -162,7 +163,7 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initiated by the user.""" errors = {} - icloud_dir = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + icloud_dir = Store(self.hass, STORAGE_VERSION, STORAGE_KEY) if not os.path.exists(icloud_dir.path): await self.hass.async_add_executor_job(os.makedirs, icloud_dir.path) @@ -276,9 +277,7 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): PyiCloudService, self._username, self._password, - self.hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY - ).path, + Store(self.hass, STORAGE_VERSION, STORAGE_KEY).path, True, None, self._with_family, diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 9f92285625b..1b308705624 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -10,6 +10,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_DEVICE_ID, CONF_WEBHOOK_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, discovery +from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType from . import websocket_api @@ -37,8 +38,10 @@ PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the mobile app component.""" - store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - if (app_config := await store.async_load()) is None: + store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + if (app_config := await store.async_load()) is None or not isinstance( + app_config, dict + ): app_config = { DATA_CONFIG_ENTRIES: {}, DATA_DELETED_IDS: [], diff --git a/homeassistant/components/network/network.py b/homeassistant/components/network/network.py index 6ec9941da3c..b2caf6438bd 100644 --- a/homeassistant/components/network/network.py +++ b/homeassistant/components/network/network.py @@ -6,6 +6,7 @@ from typing import Any, cast from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.singleton import singleton +from homeassistant.helpers.storage import Store from .const import ( ATTR_CONFIGURED_ADAPTERS, @@ -37,9 +38,7 @@ class Network: def __init__(self, hass: HomeAssistant) -> None: """Initialize the Network class.""" - self._store = hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY, atomic_writes=True - ) + self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True) self._data: dict[str, Any] = {} self.adapters: list[Adapter] = [] diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index a6d35c40335..28b60b57447 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -32,6 +32,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.network import NoURLAvailableError, get_url +from homeassistant.helpers.storage import Store from .const import ( APP_NAME_PREFIX, @@ -210,8 +211,8 @@ async def setup_smartapp_endpoint(hass: HomeAssistant): return # Get/create config to store a unique id for this hass instance. - store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - if not (config := await store.async_load()): + store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + if not (config := await store.async_load()) or not isinstance(config, dict): # Create config config = { CONF_INSTANCE_ID: str(uuid4()), @@ -282,7 +283,7 @@ async def unload_smartapp_endpoint(hass: HomeAssistant): if cloudhook_url and cloud.async_is_logged_in(hass): await cloud.async_delete_cloudhook(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]) # Remove cloudhook from storage - store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + store = Store(hass, STORAGE_VERSION, STORAGE_KEY) await store.async_save( { CONF_INSTANCE_ID: hass.data[DOMAIN][CONF_INSTANCE_ID], diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index a396f8ce38f..7a874aff993 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -4,6 +4,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType from .const import ( @@ -106,7 +107,7 @@ class UnifiWirelessClients: """Set up client storage.""" self.hass = hass self.data = {} - self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) async def async_load(self): """Load data from file.""" diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index 5d33375ace2..28983bdb427 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -10,6 +10,7 @@ from typing import cast import attr from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.storage import Store from homeassistant.loader import bind_hass from .typing import ZhaDeviceType @@ -38,7 +39,7 @@ class ZhaStorage: """Initialize the zha device storage.""" self.hass: HomeAssistant = hass self.devices: MutableMapping[str, ZhaDeviceEntry] = {} - self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) @callback def async_create_device(self, device: ZhaDeviceType) -> ZhaDeviceEntry: diff --git a/homeassistant/core.py b/homeassistant/core.py index 973a940ffbc..7fcf07d9c66 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1895,11 +1895,19 @@ class Config: async def async_load(self) -> None: """Load [homeassistant] core config.""" - store = self.hass.helpers.storage.Store( - CORE_STORAGE_VERSION, CORE_STORAGE_KEY, private=True, atomic_writes=True + # Circular dep + # pylint: disable=import-outside-toplevel + from .helpers.storage import Store + + store = Store( + self.hass, + CORE_STORAGE_VERSION, + CORE_STORAGE_KEY, + private=True, + atomic_writes=True, ) - if not (data := await store.async_load()): + if not (data := await store.async_load()) or not isinstance(data, dict): return # In 2021.9 we fixed validation to disallow a path (because that's never correct) @@ -1931,6 +1939,10 @@ class Config: async def async_store(self) -> None: """Store [homeassistant] core config.""" + # Circular dep + # pylint: disable=import-outside-toplevel + from .helpers.storage import Store + data = { "latitude": self.latitude, "longitude": self.longitude, @@ -1943,7 +1955,11 @@ class Config: "currency": self.currency, } - store = self.hass.helpers.storage.Store( - CORE_STORAGE_VERSION, CORE_STORAGE_KEY, private=True, atomic_writes=True + store = Store( + self.hass, + CORE_STORAGE_VERSION, + CORE_STORAGE_KEY, + private=True, + atomic_writes=True, ) await store.async_save(data) diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index 660c433d8c7..ad4930825c8 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -12,6 +12,7 @@ from homeassistant.loader import bind_hass from homeassistant.util import slugify from . import device_registry as dr, entity_registry as er +from .storage import Store from .typing import UNDEFINED, UndefinedType DATA_REGISTRY = "area_registry" @@ -47,9 +48,7 @@ class AreaRegistry: """Initialize the area registry.""" self.hass = hass self.areas: MutableMapping[str, AreaEntry] = {} - self._store = hass.helpers.storage.Store( - STORAGE_VERSION, STORAGE_KEY, atomic_writes=True - ) + self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True) self._normalized_name_area_idx: dict[str, str] = {} @callback @@ -176,7 +175,7 @@ class AreaRegistry: areas: MutableMapping[str, AreaEntry] = OrderedDict() - if data is not None: + if isinstance(data, dict): for area in data["areas"]: normalized_name = normalize_area_name(area["name"]) areas[area["id"]] = AreaEntry( From 993e76a44dcc38647197e1b6c49be1bd78ae7a61 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 May 2022 19:36:02 +0200 Subject: [PATCH 0584/3516] Increase timeout for running full suite tests (#72024) --- .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 50a809a5d21..0fb77fdc789 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -783,7 +783,7 @@ jobs: echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" - name: Run pytest (fully) if: needs.changes.outputs.test_full_suite == 'true' - timeout-minutes: 45 + timeout-minutes: 60 run: | . venv/bin/activate python --version From 8f4caf414124f380a8f5e1d54aedb54a8f6c5c05 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 May 2022 19:36:29 +0200 Subject: [PATCH 0585/3516] Clean up accessing event helpers via hass (#72011) --- .../components/alarmdecoder/__init__.py | 5 ++- .../components/alexa/state_report.py | 5 +-- homeassistant/components/camera/__init__.py | 3 +- homeassistant/components/evohome/__init__.py | 6 +-- homeassistant/components/freedns/__init__.py | 5 +-- .../google_assistant/report_state.py | 6 +-- .../components/google_domains/__init__.py | 3 +- homeassistant/components/hassio/__init__.py | 5 ++- homeassistant/components/homematic/entity.py | 7 ++-- homeassistant/components/no_ip/__init__.py | 3 +- .../components/universal/media_player.py | 10 +++-- homeassistant/scripts/benchmark/__init__.py | 16 ++++---- .../components/bayesian/test_binary_sensor.py | 9 ++-- .../components/universal/test_media_player.py | 5 ++- tests/helpers/test_event.py | 41 ++++++++----------- 15 files changed, 65 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index c196ebb8fbc..d5c1b88e08e 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import async_track_point_in_time from homeassistant.util import dt as dt_util from .const import ( @@ -65,8 +66,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.async_add_executor_job(controller.open, baud) except NoDeviceError: _LOGGER.debug("Failed to connect. Retrying in 5 seconds") - hass.helpers.event.async_track_point_in_time( - open_connection, dt_util.utcnow() + timedelta(seconds=5) + async_track_point_in_time( + hass, open_connection, dt_util.utcnow() + timedelta(seconds=5) ) return _LOGGER.debug("Established a connection with the alarmdecoder") diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 161ac4072b6..d3e476c2e8c 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -12,6 +12,7 @@ import async_timeout from homeassistant.const import MATCH_ALL, STATE_ON from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.significant_change import create_checker import homeassistant.util.dt as dt_util @@ -102,9 +103,7 @@ async def async_enable_proactive_mode(hass, smart_home_config): hass, smart_home_config, alexa_changed_entity, alexa_properties ) - return hass.helpers.event.async_track_state_change( - MATCH_ALL, async_entity_state_listener - ) + return async_track_state_change(hass, MATCH_ALL, async_entity_state_listener) async def async_send_changereport_message( diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 31dede33e7a..4a6e1546f46 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -54,6 +54,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.network import get_url from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass @@ -398,7 +399,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: entity.async_update_token() entity.async_write_ha_state() - hass.helpers.event.async_track_time_interval(update_tokens, TOKEN_CHANGE_INTERVAL) + async_track_time_interval(hass, update_tokens, TOKEN_CHANGE_INTERVAL) component.async_register_entity_service( SERVICE_ENABLE_MOTION, {}, "async_enable_motion_detection" diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index aa09bb666f4..908dff48aef 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -33,7 +33,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.event import async_call_later, async_track_time_interval from homeassistant.helpers.service import verify_domain_control from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType @@ -259,8 +259,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async_load_platform(hass, Platform.WATER_HEATER, DOMAIN, {}, config) ) - hass.helpers.event.async_track_time_interval( - broker.async_update, config[DOMAIN][CONF_SCAN_INTERVAL] + async_track_time_interval( + hass, broker.async_update, config[DOMAIN][CONF_SCAN_INTERVAL] ) setup_service_functions(hass, broker) diff --git a/homeassistant/components/freedns/__init__.py b/homeassistant/components/freedns/__init__.py index bc651e726ec..a5c507c3857 100644 --- a/homeassistant/components/freedns/__init__.py +++ b/homeassistant/components/freedns/__init__.py @@ -11,6 +11,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL, CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -56,9 +57,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Update the FreeDNS entry.""" await _update_freedns(hass, session, url, auth_token) - hass.helpers.event.async_track_time_interval( - update_domain_callback, update_interval - ) + async_track_time_interval(hass, update_domain_callback, update_interval) return True diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index c3f8ba3bffd..4e8ac1624cc 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -6,7 +6,7 @@ import logging from homeassistant.const import MATCH_ALL from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback -from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.event import async_call_later, async_track_state_change from homeassistant.helpers.significant_change import create_checker from .const import DOMAIN @@ -136,9 +136,7 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig await google_config.async_report_state_all({"devices": {"states": entities}}) - unsub = hass.helpers.event.async_track_state_change( - MATCH_ALL, async_entity_state_listener - ) + unsub = async_track_state_change(hass, MATCH_ALL, async_entity_state_listener) unsub = async_call_later(hass, INITIAL_REPORT_DELAY, initial_report) diff --git a/homeassistant/components/google_domains/__init__.py b/homeassistant/components/google_domains/__init__.py index 8ccc9d78c64..c7f7e632bd6 100644 --- a/homeassistant/components/google_domains/__init__.py +++ b/homeassistant/components/google_domains/__init__.py @@ -11,6 +11,7 @@ from homeassistant.const import CONF_DOMAIN, CONF_PASSWORD, CONF_TIMEOUT, CONF_U from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -56,7 +57,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Update the Google Domains entry.""" await _update_google_domains(hass, session, domain, user, password, timeout) - hass.helpers.event.async_track_time_interval(update_domain_interval, INTERVAL) + async_track_time_interval(hass, update_domain_interval, INTERVAL) return True diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 34773987014..3ded34bd6f1 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -41,6 +41,7 @@ from homeassistant.helpers.device_registry import ( async_get_registry, ) from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -638,8 +639,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: except HassioAPIError as 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 + async_track_point_in_utc_time( + hass, update_info_data, utcnow() + HASSIO_UPDATE_INTERVAL ) # Fetch data diff --git a/homeassistant/components/homematic/entity.py b/homeassistant/components/homematic/entity.py index 1fe3799bbd9..fee68caf7ed 100644 --- a/homeassistant/components/homematic/entity.py +++ b/homeassistant/components/homematic/entity.py @@ -11,6 +11,7 @@ from pyhomematic.devicetypes.generic import HMGeneric from homeassistant.const import ATTR_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, EntityDescription +from homeassistant.helpers.event import track_time_interval from .const import ( ATTR_ADDRESS, @@ -222,12 +223,10 @@ class HMHub(Entity): self._state = None # Load data - self.hass.helpers.event.track_time_interval(self._update_hub, SCAN_INTERVAL_HUB) + track_time_interval(self.hass, self._update_hub, SCAN_INTERVAL_HUB) self.hass.add_job(self._update_hub, None) - self.hass.helpers.event.track_time_interval( - self._update_variables, SCAN_INTERVAL_VARIABLES - ) + track_time_interval(self.hass, self._update_variables, SCAN_INTERVAL_VARIABLES) self.hass.add_job(self._update_variables, None) @property diff --git a/homeassistant/components/no_ip/__init__.py b/homeassistant/components/no_ip/__init__.py index 5340e926e62..a903b1af5b6 100644 --- a/homeassistant/components/no_ip/__init__.py +++ b/homeassistant/components/no_ip/__init__.py @@ -16,6 +16,7 @@ from homeassistant.helpers.aiohttp_client import ( async_get_clientsession, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -76,7 +77,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Update the NO-IP entry.""" await _update_no_ip(hass, session, domain, auth_str, timeout) - hass.helpers.event.async_track_time_interval(update_domain_interval, INTERVAL) + async_track_time_interval(hass, update_domain_interval, INTERVAL) return True diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index e29a18f285f..7ffd8b9d13d 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -79,7 +79,11 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import TrackTemplate, async_track_template_result +from homeassistant.helpers.event import ( + TrackTemplate, + async_track_state_change_event, + async_track_template_result, +) from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.service import async_call_from_config from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -202,8 +206,8 @@ class UniversalMediaPlayer(MediaPlayerEntity): depend.append(entity[0]) self.async_on_remove( - self.hass.helpers.event.async_track_state_change_event( - list(set(depend)), _async_on_dependency_update + async_track_state_change_event( + self.hass, list(set(depend)), _async_on_dependency_update ) ) diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index 8f95f7db66b..a681b3e210d 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -15,6 +15,10 @@ from homeassistant import core from homeassistant.components.websocket_api.const import JSON_DUMP from homeassistant.const import EVENT_STATE_CHANGED from homeassistant.helpers.entityfilter import convert_include_exclude_filter +from homeassistant.helpers.event import ( + async_track_state_change, + async_track_state_change_event, +) from homeassistant.helpers.json import JSONEncoder # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -134,9 +138,7 @@ async def state_changed_helper(hass): event.set() for idx in range(1000): - hass.helpers.event.async_track_state_change( - f"{entity_id}{idx}", listener, "off", "on" - ) + async_track_state_change(hass, f"{entity_id}{idx}", listener, "off", "on") event_data = { "entity_id": f"{entity_id}0", "old_state": core.State(entity_id, "off"), @@ -166,8 +168,8 @@ async def state_changed_event_helper(hass): nonlocal count count += 1 - hass.helpers.event.async_track_state_change_event( - [f"{entity_id}{idx}" for idx in range(1000)], listener + async_track_state_change_event( + hass, [f"{entity_id}{idx}" for idx in range(1000)], listener ) event_data = { @@ -201,8 +203,8 @@ async def state_changed_event_filter_helper(hass): nonlocal count count += 1 - hass.helpers.event.async_track_state_change_event( - [f"{entity_id}{idx}" for idx in range(1000)], listener + async_track_state_change_event( + hass, [f"{entity_id}{idx}" for idx in range(1000)], listener ) event_data = { diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index c2f289b0697..2f45e0e475e 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import Context, callback +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.setup import async_setup_component from tests.common import get_fixture_path @@ -709,8 +710,8 @@ async def test_template_triggers(hass): assert hass.states.get("binary_sensor.test_binary").state == STATE_OFF events = [] - hass.helpers.event.async_track_state_change_event( - "binary_sensor.test_binary", callback(lambda event: events.append(event)) + async_track_state_change_event( + hass, "binary_sensor.test_binary", callback(lambda event: events.append(event)) ) context = Context() @@ -748,8 +749,8 @@ async def test_state_triggers(hass): assert hass.states.get("binary_sensor.test_binary").state == STATE_OFF events = [] - hass.helpers.event.async_track_state_change_event( - "binary_sensor.test_binary", callback(lambda event: events.append(event)) + async_track_state_change_event( + hass, "binary_sensor.test_binary", callback(lambda event: events.append(event)) ) context = Context() diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 4a1838aba12..d4fe2ce64ae 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -21,6 +21,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import Context, callback +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.setup import async_setup_component from tests.common import async_mock_service, get_fixture_path @@ -1136,8 +1137,8 @@ async def test_master_state_with_template(hass): events = [] - hass.helpers.event.async_track_state_change_event( - "media_player.tv", callback(lambda event: events.append(event)) + async_track_state_change_event( + hass, "media_player.tv", callback(lambda event: events.append(event)) ) context = Context() diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index d0355bba5a8..57b89e64cce 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -21,6 +21,7 @@ from homeassistant.helpers.event import ( TrackTemplate, TrackTemplateResult, async_call_later, + async_track_entity_registry_updated_event, async_track_point_in_time, async_track_point_in_utc_time, async_track_same_state, @@ -4230,12 +4231,10 @@ async def test_track_point_in_utc_time_cancel(hass): with pytest.raises(TypeError): track_point_in_utc_time("nothass", run_callback, utc_now) - unsub1 = hass.helpers.event.track_point_in_utc_time( - run_callback, utc_now + timedelta(seconds=0.1) - ) - hass.helpers.event.track_point_in_utc_time( - run_callback, utc_now + timedelta(seconds=0.1) + unsub1 = track_point_in_utc_time( + hass, run_callback, utc_now + timedelta(seconds=0.1) ) + track_point_in_utc_time(hass, run_callback, utc_now + timedelta(seconds=0.1)) unsub1() @@ -4262,12 +4261,10 @@ async def test_async_track_point_in_time_cancel(hass): utc_now = dt_util.utcnow() hst_now = utc_now.astimezone(hst_tz) - unsub1 = hass.helpers.event.async_track_point_in_time( - run_callback, hst_now + timedelta(seconds=0.1) - ) - hass.helpers.event.async_track_point_in_time( - run_callback, hst_now + timedelta(seconds=0.1) + unsub1 = async_track_point_in_time( + hass, run_callback, hst_now + timedelta(seconds=0.1) ) + async_track_point_in_time(hass, run_callback, hst_now + timedelta(seconds=0.1)) unsub1() @@ -4292,11 +4289,9 @@ async def test_async_track_entity_registry_updated_event(hass): def run_callback(event): event_data.append(event.data) - unsub1 = hass.helpers.event.async_track_entity_registry_updated_event( - entity_id, run_callback - ) - unsub2 = hass.helpers.event.async_track_entity_registry_updated_event( - new_entity_id, run_callback + unsub1 = async_track_entity_registry_updated_event(hass, entity_id, run_callback) + unsub2 = async_track_entity_registry_updated_event( + hass, new_entity_id, run_callback ) hass.bus.async_fire( EVENT_ENTITY_REGISTRY_UPDATED, {"action": "create", "entity_id": entity_id} @@ -4362,12 +4357,10 @@ async def test_async_track_entity_registry_updated_event_with_a_callback_that_th def failing_callback(event): raise ValueError - unsub1 = hass.helpers.event.async_track_entity_registry_updated_event( - entity_id, failing_callback - ) - unsub2 = hass.helpers.event.async_track_entity_registry_updated_event( - entity_id, run_callback + unsub1 = async_track_entity_registry_updated_event( + hass, entity_id, failing_callback ) + unsub2 = async_track_entity_registry_updated_event(hass, entity_id, run_callback) hass.bus.async_fire( EVENT_ENTITY_REGISTRY_UPDATED, {"action": "create", "entity_id": entity_id} ) @@ -4380,11 +4373,11 @@ async def test_async_track_entity_registry_updated_event_with_a_callback_that_th async def test_async_track_entity_registry_updated_event_with_empty_list(hass): """Test async_track_entity_registry_updated_event passing an empty list of entities.""" - unsub_single = hass.helpers.event.async_track_entity_registry_updated_event( - [], ha.callback(lambda event: None) + unsub_single = async_track_entity_registry_updated_event( + hass, [], ha.callback(lambda event: None) ) - unsub_single2 = hass.helpers.event.async_track_entity_registry_updated_event( - [], ha.callback(lambda event: None) + unsub_single2 = async_track_entity_registry_updated_event( + hass, [], ha.callback(lambda event: None) ) unsub_single2() From 5433c0a5355f3167309e4aa7a8baeb7488ccd6cf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 May 2022 10:43:58 -0700 Subject: [PATCH 0586/3516] Make sure empty get_events results is always a list (#72021) --- homeassistant/components/logbook/__init__.py | 2 +- tests/components/logbook/test_init.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 35fa17bb3a4..549518e875a 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -271,7 +271,7 @@ async def ws_get_events( return if start_time > utc_now: - connection.send_result(msg["id"], {}) + connection.send_result(msg["id"], []) return entity_ids = msg.get("entity_ids") diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index ed95b4d10bc..dddbbc61134 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -2391,6 +2391,7 @@ async def test_get_events_future_start_time(hass, hass_ws_client, recorder_mock) assert response["id"] == 1 results = response["result"] + assert isinstance(results, list) assert len(results) == 0 From c0da97b038ac43bbe4801cb9c3f56d341a2ab7c6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 May 2022 19:56:57 +0200 Subject: [PATCH 0587/3516] Clean up accessing service helpers via hass (#72013) --- .../components/cast/home_assistant_cast.py | 4 +++- homeassistant/components/cloud/__init__.py | 9 ++++---- .../components/homeassistant/__init__.py | 23 +++++++++++-------- .../components/homeassistant/scene.py | 5 ++-- homeassistant/components/homekit/__init__.py | 8 +++++-- .../components/huawei_lte/__init__.py | 4 +++- homeassistant/components/isy994/services.py | 21 +++++++++-------- homeassistant/components/template/__init__.py | 5 ++-- tests/components/script/test_init.py | 2 +- tests/helpers/test_service.py | 19 +++++++-------- 10 files changed, 55 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index 2f9583f329c..010e4a046b4 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -9,6 +9,7 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, dispatcher from homeassistant.helpers.network import NoURLAvailableError, get_url +from homeassistant.helpers.service import async_register_admin_service from .const import DOMAIN, SIGNAL_HASS_CAST_SHOW_VIEW @@ -65,7 +66,8 @@ async def async_setup_ha_cast( call.data.get(ATTR_URL_PATH), ) - hass.helpers.service.async_register_admin_service( + async_register_admin_service( + hass, DOMAIN, SERVICE_SHOW_VIEW, handle_show_view, diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 360e726c89e..559453fe02b 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -26,6 +26,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.aiohttp import MockRequest @@ -250,11 +251,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: elif service.service == SERVICE_REMOTE_DISCONNECT: await prefs.async_update(remote_enabled=False) - hass.helpers.service.async_register_admin_service( - DOMAIN, SERVICE_REMOTE_CONNECT, _service_handler - ) - hass.helpers.service.async_register_admin_service( - DOMAIN, SERVICE_REMOTE_DISCONNECT, _service_handler + async_register_admin_service(hass, DOMAIN, SERVICE_REMOTE_CONNECT, _service_handler) + async_register_admin_service( + hass, DOMAIN, SERVICE_REMOTE_DISCONNECT, _service_handler ) loaded = False diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index f79213e2484..81d79b03c10 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -27,6 +27,7 @@ from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.service import ( async_extract_config_entry_ids, async_extract_referenced_entity_ids, + async_register_admin_service, ) from homeassistant.helpers.typing import ConfigType @@ -206,14 +207,14 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no if tasks: await asyncio.wait(tasks) - hass.helpers.service.async_register_admin_service( - ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service + async_register_admin_service( + hass, ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service ) - hass.helpers.service.async_register_admin_service( - ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service + async_register_admin_service( + hass, ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service ) - hass.helpers.service.async_register_admin_service( - ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service + async_register_admin_service( + hass, ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service ) hass.services.async_register( ha.DOMAIN, @@ -233,8 +234,8 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no # auth only processed during startup await conf_util.async_process_ha_core_config(hass, conf.get(ha.DOMAIN) or {}) - hass.helpers.service.async_register_admin_service( - ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config + async_register_admin_service( + hass, ha.DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config ) async def async_set_location(call: ha.ServiceCall) -> None: @@ -243,7 +244,8 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no latitude=call.data[ATTR_LATITUDE], longitude=call.data[ATTR_LONGITUDE] ) - hass.helpers.service.async_register_admin_service( + async_register_admin_service( + hass, ha.DOMAIN, SERVICE_SET_LOCATION, async_set_location, @@ -265,7 +267,8 @@ async def async_setup(hass: ha.HomeAssistant, config: ConfigType) -> bool: # no ) ) - hass.helpers.service.async_register_admin_service( + async_register_admin_service( + hass, ha.DOMAIN, SERVICE_RELOAD_CONFIG_ENTRY, async_handle_reload_config_entry, diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 505dc0027d6..bf8affd902e 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -35,6 +35,7 @@ from homeassistant.helpers import ( entity_platform, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.state import async_reproduce_state from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.loader import async_get_integration @@ -206,9 +207,7 @@ async def async_setup_platform( hass.bus.async_fire(EVENT_SCENE_RELOADED, context=call.context) - hass.helpers.service.async_register_admin_service( - SCENE_DOMAIN, SERVICE_RELOAD, reload_config - ) + async_register_admin_service(hass, SCENE_DOMAIN, SERVICE_RELOAD, reload_config) async def apply_service(call: ServiceCall) -> None: """Apply a scene.""" diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 48f8f336fe7..1cba9746609 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -54,7 +54,10 @@ from homeassistant.helpers.entityfilter import ( EntityFilter, ) from homeassistant.helpers.reload import async_integration_yaml_config -from homeassistant.helpers.service import async_extract_referenced_entity_ids +from homeassistant.helpers.service import ( + async_extract_referenced_entity_ids, + async_register_admin_service, +) from homeassistant.helpers.typing import ConfigType from homeassistant.loader import IntegrationNotFound, async_get_integration @@ -461,7 +464,8 @@ def _async_register_events_and_services(hass: HomeAssistant) -> None: await asyncio.gather(*reload_tasks) - hass.helpers.service.async_register_admin_service( + async_register_admin_service( + hass, DOMAIN, SERVICE_RELOAD, _handle_homekit_reload, diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index d8afbd752a4..601a3b9af8d 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -47,6 +47,7 @@ from homeassistant.helpers import ( from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType from .const import ( @@ -531,7 +532,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.error("%s: unsupported service", service.service) for service in ADMIN_SERVICES: - hass.helpers.service.async_register_admin_service( + async_register_admin_service( + hass, DOMAIN, service, service_handler, diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 8323394803f..654b65309fc 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -19,6 +19,7 @@ from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import async_get_platforms import homeassistant.helpers.entity_registry as er +from homeassistant.helpers.service import entity_service_call from .const import ( _LOGGER, @@ -373,8 +374,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 ) async def _async_send_raw_node_command(call: ServiceCall) -> None: - await hass.helpers.service.entity_service_call( - async_get_platforms(hass, DOMAIN), "async_send_raw_node_command", call + await entity_service_call( + hass, async_get_platforms(hass, DOMAIN), "async_send_raw_node_command", call ) hass.services.async_register( @@ -385,8 +386,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 ) async def _async_send_node_command(call: ServiceCall) -> None: - await hass.helpers.service.entity_service_call( - async_get_platforms(hass, DOMAIN), "async_send_node_command", call + await entity_service_call( + hass, async_get_platforms(hass, DOMAIN), "async_send_node_command", call ) hass.services.async_register( @@ -397,8 +398,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 ) async def _async_get_zwave_parameter(call: ServiceCall) -> None: - await hass.helpers.service.entity_service_call( - async_get_platforms(hass, DOMAIN), "async_get_zwave_parameter", call + await entity_service_call( + hass, async_get_platforms(hass, DOMAIN), "async_get_zwave_parameter", call ) hass.services.async_register( @@ -409,8 +410,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 ) async def _async_set_zwave_parameter(call: ServiceCall) -> None: - await hass.helpers.service.entity_service_call( - async_get_platforms(hass, DOMAIN), "async_set_zwave_parameter", call + await entity_service_call( + hass, async_get_platforms(hass, DOMAIN), "async_set_zwave_parameter", call ) hass.services.async_register( @@ -421,8 +422,8 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 ) async def _async_rename_node(call: ServiceCall) -> None: - await hass.helpers.service.entity_service_call( - async_get_platforms(hass, DOMAIN), "async_rename_node", call + await entity_service_call( + hass, async_get_platforms(hass, DOMAIN), "async_rename_node", call ) hass.services.async_register( diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index a1231708b91..47b51853bcd 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -19,6 +19,7 @@ from homeassistant.helpers import ( update_coordinator, ) from homeassistant.helpers.reload import async_reload_integration_platforms +from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration @@ -54,9 +55,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.bus.async_fire(f"event_{DOMAIN}_reloaded", context=call.context) - hass.helpers.service.async_register_admin_service( - DOMAIN, SERVICE_RELOAD, _reload_config - ) + async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, _reload_config) return True diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 480bbcbc4f0..4ef4cd9d2b0 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -377,7 +377,7 @@ async def test_async_get_descriptions_script(hass): } await async_setup_component(hass, DOMAIN, script_config) - descriptions = await hass.helpers.service.async_get_all_descriptions() + descriptions = await async_get_all_descriptions(hass) assert descriptions[DOMAIN]["test1"]["description"] == "" assert not descriptions[DOMAIN]["test1"]["fields"] diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index cf87377dd8f..1e947a9353f 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -819,8 +819,9 @@ async def test_register_admin_service(hass, hass_read_only_user, hass_admin_user async def mock_service(call): calls.append(call) - hass.helpers.service.async_register_admin_service("test", "test", mock_service) - hass.helpers.service.async_register_admin_service( + service.async_register_admin_service(hass, "test", "test", mock_service) + service.async_register_admin_service( + hass, "test", "test2", mock_service, @@ -887,7 +888,7 @@ async def test_domain_control_not_async(hass, mock_entities): calls.append(call) with pytest.raises(exceptions.HomeAssistantError): - hass.helpers.service.verify_domain_control("test_domain")(mock_service_log) + service.verify_domain_control(hass, "test_domain")(mock_service_log) async def test_domain_control_unknown(hass, mock_entities): @@ -902,9 +903,9 @@ async def test_domain_control_unknown(hass, mock_entities): "homeassistant.helpers.entity_registry.async_get_registry", return_value=Mock(entities=mock_entities), ): - protected_mock_service = hass.helpers.service.verify_domain_control( - "test_domain" - )(mock_service_log) + protected_mock_service = service.verify_domain_control(hass, "test_domain")( + mock_service_log + ) hass.services.async_register( "test_domain", "test_service", protected_mock_service, schema=None @@ -940,7 +941,7 @@ async def test_domain_control_unauthorized(hass, hass_read_only_user): """Define a protected service.""" calls.append(call) - protected_mock_service = hass.helpers.service.verify_domain_control("test_domain")( + protected_mock_service = service.verify_domain_control(hass, "test_domain")( mock_service_log ) @@ -979,7 +980,7 @@ async def test_domain_control_admin(hass, hass_admin_user): """Define a protected service.""" calls.append(call) - protected_mock_service = hass.helpers.service.verify_domain_control("test_domain")( + protected_mock_service = service.verify_domain_control(hass, "test_domain")( mock_service_log ) @@ -1017,7 +1018,7 @@ async def test_domain_control_no_user(hass): """Define a protected service.""" calls.append(call) - protected_mock_service = hass.helpers.service.verify_domain_control("test_domain")( + protected_mock_service = service.verify_domain_control(hass, "test_domain")( mock_service_log ) From 0d94324d58dd049cf2520d041a769e50368c8662 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Tue, 17 May 2022 20:57:41 +0300 Subject: [PATCH 0588/3516] Clean up Shelly async methods that are not awaiting (#72026) --- homeassistant/components/shelly/__init__.py | 9 ++++---- .../components/shelly/binary_sensor.py | 8 +++---- homeassistant/components/shelly/climate.py | 12 +++++----- homeassistant/components/shelly/cover.py | 10 ++++---- homeassistant/components/shelly/entity.py | 23 +++++++++++-------- homeassistant/components/shelly/light.py | 14 ++++++----- homeassistant/components/shelly/number.py | 2 +- homeassistant/components/shelly/sensor.py | 8 +++---- homeassistant/components/shelly/switch.py | 14 ++++++----- homeassistant/components/shelly/utils.py | 3 ++- 10 files changed, 58 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 41a9e68fbdd..4551fee5590 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -182,7 +182,7 @@ async def async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bo data["model"] = device.settings["device"]["type"] hass.config_entries.async_update_entry(entry, data=data) - hass.async_create_task(async_block_device_setup(hass, entry, device)) + async_block_device_setup(hass, entry, device) if sleep_period == 0: # Not a sleeping device, finish setup @@ -197,7 +197,7 @@ async def async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bo except OSError as err: raise ConfigEntryNotReady(str(err) or "Error during device setup") from err - await async_block_device_setup(hass, entry, device) + async_block_device_setup(hass, entry, device) elif sleep_period is None or device_entry is None: # Need to get sleep info or first time sleeping device setup, wait for device hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id][DEVICE] = device @@ -208,12 +208,13 @@ async def async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bo else: # Restore sensors for sleeping device LOGGER.debug("Setting up offline block device %s", entry.title) - await async_block_device_setup(hass, entry, device) + async_block_device_setup(hass, entry, device) return True -async def async_block_device_setup( +@callback +def async_block_device_setup( hass: HomeAssistant, entry: ConfigEntry, device: BlockDevice ) -> None: """Set up a block based device that is online.""" diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index a6cde0c4670..b33947ad6b7 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -235,12 +235,12 @@ async def async_setup_entry( ) -> None: """Set up sensors for device.""" if get_device_entry_gen(config_entry) == 2: - return await async_setup_entry_rpc( + return async_setup_entry_rpc( hass, config_entry, async_add_entities, RPC_SENSORS, RpcBinarySensor ) if config_entry.data[CONF_SLEEP_PERIOD]: - await async_setup_entry_attribute_entities( + async_setup_entry_attribute_entities( hass, config_entry, async_add_entities, @@ -249,7 +249,7 @@ async def async_setup_entry( _build_block_description, ) else: - await async_setup_entry_attribute_entities( + async_setup_entry_attribute_entities( hass, config_entry, async_add_entities, @@ -257,7 +257,7 @@ async def async_setup_entry( BlockBinarySensor, _build_block_description, ) - await async_setup_entry_rest( + async_setup_entry_rest( hass, config_entry, async_add_entities, diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index a042254fdc6..453d67f39a7 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -50,14 +50,13 @@ async def async_setup_entry( ][BLOCK] if wrapper.device.initialized: - await async_setup_climate_entities(async_add_entities, wrapper) + async_setup_climate_entities(async_add_entities, wrapper) else: - await async_restore_climate_entities( - hass, config_entry, async_add_entities, wrapper - ) + async_restore_climate_entities(hass, config_entry, async_add_entities, wrapper) -async def async_setup_climate_entities( +@callback +def async_setup_climate_entities( async_add_entities: AddEntitiesCallback, wrapper: BlockDeviceWrapper, ) -> None: @@ -79,7 +78,8 @@ async def async_setup_climate_entities( async_add_entities([BlockSleepingClimate(wrapper, sensor_block, device_block)]) -async def async_restore_climate_entities( +@callback +def async_restore_climate_entities( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, diff --git a/homeassistant/components/shelly/cover.py b/homeassistant/components/shelly/cover.py index 9bc61f9415f..e28fe22a528 100644 --- a/homeassistant/components/shelly/cover.py +++ b/homeassistant/components/shelly/cover.py @@ -28,12 +28,13 @@ async def async_setup_entry( ) -> None: """Set up switches for device.""" if get_device_entry_gen(config_entry) == 2: - return await async_setup_rpc_entry(hass, config_entry, async_add_entities) + return async_setup_rpc_entry(hass, config_entry, async_add_entities) - return await async_setup_block_entry(hass, config_entry, async_add_entities) + return async_setup_block_entry(hass, config_entry, async_add_entities) -async def async_setup_block_entry( +@callback +def async_setup_block_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, @@ -48,7 +49,8 @@ async def async_setup_block_entry( async_add_entities(BlockShellyCover(wrapper, block) for block in blocks) -async def async_setup_rpc_entry( +@callback +def async_setup_rpc_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index de7d3dd9437..8cba4d2804e 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -46,7 +46,8 @@ from .utils import ( ) -async def async_setup_entry_attribute_entities( +@callback +def async_setup_entry_attribute_entities( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, @@ -62,11 +63,11 @@ async def async_setup_entry_attribute_entities( ][BLOCK] if wrapper.device.initialized: - await async_setup_block_attribute_entities( + async_setup_block_attribute_entities( hass, async_add_entities, wrapper, sensors, sensor_class ) else: - await async_restore_block_attribute_entities( + async_restore_block_attribute_entities( hass, config_entry, async_add_entities, @@ -77,7 +78,8 @@ async def async_setup_entry_attribute_entities( ) -async def async_setup_block_attribute_entities( +@callback +def async_setup_block_attribute_entities( hass: HomeAssistant, async_add_entities: AddEntitiesCallback, wrapper: BlockDeviceWrapper, @@ -105,7 +107,7 @@ async def async_setup_block_attribute_entities( ): domain = sensor_class.__module__.split(".")[-1] unique_id = f"{wrapper.mac}-{block.description}-{sensor_id}" - await async_remove_shelly_entity(hass, domain, unique_id) + async_remove_shelly_entity(hass, domain, unique_id) else: blocks.append((block, sensor_id, description)) @@ -120,7 +122,8 @@ async def async_setup_block_attribute_entities( ) -async def async_restore_block_attribute_entities( +@callback +def async_restore_block_attribute_entities( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, @@ -158,7 +161,8 @@ async def async_restore_block_attribute_entities( async_add_entities(entities) -async def async_setup_entry_rpc( +@callback +def async_setup_entry_rpc( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, @@ -192,7 +196,7 @@ async def async_setup_entry_rpc( ): domain = sensor_class.__module__.split(".")[-1] unique_id = f"{wrapper.mac}-{key}-{sensor_id}" - await async_remove_shelly_entity(hass, domain, unique_id) + async_remove_shelly_entity(hass, domain, unique_id) else: if description.use_polling_wrapper: entities.append( @@ -207,7 +211,8 @@ async def async_setup_entry_rpc( async_add_entities(entities) -async def async_setup_entry_rest( +@callback +def async_setup_entry_rest( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 295f64b01f6..79db9c509f4 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -65,12 +65,13 @@ async def async_setup_entry( ) -> None: """Set up lights for device.""" if get_device_entry_gen(config_entry) == 2: - return await async_setup_rpc_entry(hass, config_entry, async_add_entities) + return async_setup_rpc_entry(hass, config_entry, async_add_entities) - return await async_setup_block_entry(hass, config_entry, async_add_entities) + return async_setup_block_entry(hass, config_entry, async_add_entities) -async def async_setup_block_entry( +@callback +def async_setup_block_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, @@ -92,7 +93,7 @@ async def async_setup_block_entry( blocks.append(block) assert wrapper.device.shelly unique_id = f"{wrapper.mac}-{block.type}_{block.channel}" - await async_remove_shelly_entity(hass, "switch", unique_id) + async_remove_shelly_entity(hass, "switch", unique_id) if not blocks: return @@ -100,7 +101,8 @@ async def async_setup_block_entry( async_add_entities(BlockShellyLight(wrapper, block) for block in blocks) -async def async_setup_rpc_entry( +@callback +def async_setup_rpc_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, @@ -116,7 +118,7 @@ async def async_setup_rpc_entry( switch_ids.append(id_) unique_id = f"{wrapper.mac}-switch:{id_}" - await async_remove_shelly_entity(hass, "switch", unique_id) + async_remove_shelly_entity(hass, "switch", unique_id) if not switch_ids: return diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index bfac4cd4033..dcedc32602d 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -81,7 +81,7 @@ async def async_setup_entry( return if config_entry.data[CONF_SLEEP_PERIOD]: - await async_setup_entry_attribute_entities( + async_setup_entry_attribute_entities( hass, config_entry, async_add_entities, diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 7e19b9724d7..92c19734414 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -394,12 +394,12 @@ async def async_setup_entry( ) -> None: """Set up sensors for device.""" if get_device_entry_gen(config_entry) == 2: - return await async_setup_entry_rpc( + return async_setup_entry_rpc( hass, config_entry, async_add_entities, RPC_SENSORS, RpcSensor ) if config_entry.data[CONF_SLEEP_PERIOD]: - await async_setup_entry_attribute_entities( + async_setup_entry_attribute_entities( hass, config_entry, async_add_entities, @@ -408,7 +408,7 @@ async def async_setup_entry( _build_block_description, ) else: - await async_setup_entry_attribute_entities( + async_setup_entry_attribute_entities( hass, config_entry, async_add_entities, @@ -416,7 +416,7 @@ async def async_setup_entry( BlockSensor, _build_block_description, ) - await async_setup_entry_rest( + async_setup_entry_rest( hass, config_entry, async_add_entities, REST_SENSORS, RestSensor ) diff --git a/homeassistant/components/shelly/switch.py b/homeassistant/components/shelly/switch.py index 9114587a910..d65568d0a2a 100644 --- a/homeassistant/components/shelly/switch.py +++ b/homeassistant/components/shelly/switch.py @@ -29,12 +29,13 @@ async def async_setup_entry( ) -> None: """Set up switches for device.""" if get_device_entry_gen(config_entry) == 2: - return await async_setup_rpc_entry(hass, config_entry, async_add_entities) + return async_setup_rpc_entry(hass, config_entry, async_add_entities) - return await async_setup_block_entry(hass, config_entry, async_add_entities) + return async_setup_block_entry(hass, config_entry, async_add_entities) -async def async_setup_block_entry( +@callback +def async_setup_block_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, @@ -59,7 +60,7 @@ async def async_setup_block_entry( relay_blocks.append(block) unique_id = f"{wrapper.mac}-{block.type}_{block.channel}" - await async_remove_shelly_entity(hass, "light", unique_id) + async_remove_shelly_entity(hass, "light", unique_id) if not relay_blocks: return @@ -67,7 +68,8 @@ async def async_setup_block_entry( async_add_entities(BlockRelaySwitch(wrapper, block) for block in relay_blocks) -async def async_setup_rpc_entry( +@callback +def async_setup_rpc_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, @@ -84,7 +86,7 @@ async def async_setup_rpc_entry( switch_ids.append(id_) unique_id = f"{wrapper.mac}-switch:{id_}" - await async_remove_shelly_entity(hass, "light", unique_id) + async_remove_shelly_entity(hass, "light", unique_id) if not switch_ids: return diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 0398250df71..6dfc2fb3be8 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -30,7 +30,8 @@ from .const import ( ) -async def async_remove_shelly_entity( +@callback +def async_remove_shelly_entity( hass: HomeAssistant, domain: str, unique_id: str ) -> None: """Remove a Shelly entity.""" From 69e622b3278c4d26a180969d746b9381756f7d15 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 May 2022 20:33:51 +0200 Subject: [PATCH 0589/3516] Clean up accessing intent helpers via hass (#72028) --- homeassistant/components/cover/intent.py | 10 ++++++---- .../components/hangouts/hangouts_bot.py | 11 +++++++--- homeassistant/components/humidifier/intent.py | 11 +++++----- homeassistant/components/intent/__init__.py | 15 ++++++++------ homeassistant/components/light/intent.py | 6 +++--- tests/components/humidifier/test_intent.py | 20 ++++++++++++------- tests/components/light/test_intent.py | 11 ++++++---- 7 files changed, 52 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/cover/intent.py b/homeassistant/components/cover/intent.py index 1fa353151ba..9174e8399f3 100644 --- a/homeassistant/components/cover/intent.py +++ b/homeassistant/components/cover/intent.py @@ -11,13 +11,15 @@ INTENT_CLOSE_COVER = "HassCloseCover" async def async_setup_intents(hass: HomeAssistant) -> None: """Set up the cover intents.""" - hass.helpers.intent.async_register( + intent.async_register( + hass, intent.ServiceIntentHandler( INTENT_OPEN_COVER, DOMAIN, SERVICE_OPEN_COVER, "Opened {}" - ) + ), ) - hass.helpers.intent.async_register( + intent.async_register( + hass, intent.ServiceIntentHandler( INTENT_CLOSE_COVER, DOMAIN, SERVICE_CLOSE_COVER, "Closed {}" - ) + ), ) diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 11181c5b0ff..c3c363ef55e 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -187,11 +187,16 @@ class HangoutsBot: if not (match := matcher.match(text)): continue if intent_type == INTENT_HELP: - return await self.hass.helpers.intent.async_handle( - DOMAIN, intent_type, {"conv_id": {"value": conv_id}}, text + return await intent.async_handle( + self.hass, + DOMAIN, + intent_type, + {"conv_id": {"value": conv_id}}, + text, ) - return await self.hass.helpers.intent.async_handle( + return await intent.async_handle( + self.hass, DOMAIN, intent_type, {"conv_id": {"value": conv_id}} diff --git a/homeassistant/components/humidifier/intent.py b/homeassistant/components/humidifier/intent.py index aeeb18cceec..57f42f58fe0 100644 --- a/homeassistant/components/humidifier/intent.py +++ b/homeassistant/components/humidifier/intent.py @@ -22,8 +22,8 @@ INTENT_MODE = "HassHumidifierMode" async def async_setup_intents(hass: HomeAssistant) -> None: """Set up the humidifier intents.""" - hass.helpers.intent.async_register(HumidityHandler()) - hass.helpers.intent.async_register(SetModeHandler()) + intent.async_register(hass, HumidityHandler()) + intent.async_register(hass, SetModeHandler()) class HumidityHandler(intent.IntentHandler): @@ -39,8 +39,8 @@ class HumidityHandler(intent.IntentHandler): """Handle the hass intent.""" hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - state = hass.helpers.intent.async_match_state( - slots["name"]["value"], hass.states.async_all(DOMAIN) + state = intent.async_match_state( + hass, slots["name"]["value"], hass.states.async_all(DOMAIN) ) service_data = {ATTR_ENTITY_ID: state.entity_id} @@ -83,7 +83,8 @@ class SetModeHandler(intent.IntentHandler): """Handle the hass intent.""" hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - state = hass.helpers.intent.async_match_state( + state = intent.async_match_state( + hass, slots["name"]["value"], hass.states.async_all(DOMAIN), ) diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index d626daa8c3b..c43643b0244 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -19,20 +19,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, DOMAIN, _async_process_intent ) - hass.helpers.intent.async_register( + intent.async_register( + hass, intent.ServiceIntentHandler( intent.INTENT_TURN_ON, HA_DOMAIN, SERVICE_TURN_ON, "Turned {} on" - ) + ), ) - hass.helpers.intent.async_register( + intent.async_register( + hass, intent.ServiceIntentHandler( intent.INTENT_TURN_OFF, HA_DOMAIN, SERVICE_TURN_OFF, "Turned {} off" - ) + ), ) - hass.helpers.intent.async_register( + intent.async_register( + hass, intent.ServiceIntentHandler( intent.INTENT_TOGGLE, HA_DOMAIN, SERVICE_TOGGLE, "Toggled {}" - ) + ), ) return True diff --git a/homeassistant/components/light/intent.py b/homeassistant/components/light/intent.py index 261cbbd973d..2ffd73a9792 100644 --- a/homeassistant/components/light/intent.py +++ b/homeassistant/components/light/intent.py @@ -21,7 +21,7 @@ INTENT_SET = "HassLightSet" async def async_setup_intents(hass: HomeAssistant) -> None: """Set up the light intents.""" - hass.helpers.intent.async_register(SetIntentHandler()) + intent.async_register(hass, SetIntentHandler()) def _test_supports_color(state: State) -> None: @@ -56,8 +56,8 @@ class SetIntentHandler(intent.IntentHandler): """Handle the hass intent.""" hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - state = hass.helpers.intent.async_match_state( - slots["name"]["value"], hass.states.async_all(DOMAIN) + state = intent.async_match_state( + hass, slots["name"]["value"], hass.states.async_all(DOMAIN) ) service_data = {ATTR_ENTITY_ID: state.entity_id} diff --git a/tests/components/humidifier/test_intent.py b/tests/components/humidifier/test_intent.py index 66ff62872f3..ed207231bc6 100644 --- a/tests/components/humidifier/test_intent.py +++ b/tests/components/humidifier/test_intent.py @@ -15,7 +15,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.helpers.intent import IntentHandleError +from homeassistant.helpers.intent import IntentHandleError, async_handle from tests.common import async_mock_service @@ -29,7 +29,8 @@ async def test_intent_set_humidity(hass): turn_on_calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) await intent.async_setup_intents(hass) - result = await hass.helpers.intent.async_handle( + result = await async_handle( + hass, "test", intent.INTENT_HUMIDITY, {"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}}, @@ -56,7 +57,8 @@ async def test_intent_set_humidity_and_turn_on(hass): turn_on_calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) await intent.async_setup_intents(hass) - result = await hass.helpers.intent.async_handle( + result = await async_handle( + hass, "test", intent.INTENT_HUMIDITY, {"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}}, @@ -97,7 +99,8 @@ async def test_intent_set_mode(hass): turn_on_calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) await intent.async_setup_intents(hass) - result = await hass.helpers.intent.async_handle( + result = await async_handle( + hass, "test", intent.INTENT_MODE, {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, @@ -134,7 +137,8 @@ async def test_intent_set_mode_and_turn_on(hass): turn_on_calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) await intent.async_setup_intents(hass) - result = await hass.helpers.intent.async_handle( + result = await async_handle( + hass, "test", intent.INTENT_MODE, {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, @@ -168,7 +172,8 @@ async def test_intent_set_mode_tests_feature(hass): await intent.async_setup_intents(hass) try: - await hass.helpers.intent.async_handle( + await async_handle( + hass, "test", intent.INTENT_MODE, {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, @@ -196,7 +201,8 @@ async def test_intent_set_unknown_mode(hass): await intent.async_setup_intents(hass) try: - await hass.helpers.intent.async_handle( + await async_handle( + hass, "test", intent.INTENT_MODE, {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "eco"}}, diff --git a/tests/components/light/test_intent.py b/tests/components/light/test_intent.py index 19b25efa772..0c837a49c42 100644 --- a/tests/components/light/test_intent.py +++ b/tests/components/light/test_intent.py @@ -2,7 +2,7 @@ from homeassistant.components import light from homeassistant.components.light import ATTR_SUPPORTED_COLOR_MODES, ColorMode, intent from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON -from homeassistant.helpers.intent import IntentHandleError +from homeassistant.helpers.intent import IntentHandleError, async_handle from tests.common import async_mock_service @@ -16,7 +16,8 @@ async def test_intent_set_color(hass): calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) await intent.async_setup_intents(hass) - result = await hass.helpers.intent.async_handle( + result = await async_handle( + hass, "test", intent.INTENT_SET, {"name": {"value": "Hello"}, "color": {"value": "blue"}}, @@ -40,7 +41,8 @@ async def test_intent_set_color_tests_feature(hass): await intent.async_setup_intents(hass) try: - await hass.helpers.intent.async_handle( + await async_handle( + hass, "test", intent.INTENT_SET, {"name": {"value": "Hello"}, "color": {"value": "blue"}}, @@ -61,7 +63,8 @@ async def test_intent_set_color_and_brightness(hass): calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) await intent.async_setup_intents(hass) - result = await hass.helpers.intent.async_handle( + result = await async_handle( + hass, "test", intent.INTENT_SET, { From c3d19f38274935ab8f784e6fbeb7c5959a551074 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 May 2022 21:22:15 +0200 Subject: [PATCH 0590/3516] Clean up accessing device_registry helpers via hass (#72031) --- homeassistant/components/guardian/__init__.py | 2 +- homeassistant/components/hue/v1/hue_event.py | 5 ++--- homeassistant/components/insteon/api/aldb.py | 3 ++- .../components/insteon/api/device.py | 3 ++- homeassistant/components/insteon/utils.py | 3 ++- homeassistant/components/mikrotik/__init__.py | 4 ++-- homeassistant/components/nest/__init__.py | 8 +++++-- .../components/nest/device_trigger.py | 6 ++--- .../components/netatmo/device_trigger.py | 22 ++++++++++++++----- .../components/netatmo/netatmo_entity_base.py | 7 +++--- homeassistant/components/netatmo/sensor.py | 6 ++--- homeassistant/components/plex/__init__.py | 4 ++-- homeassistant/components/rfxtrx/__init__.py | 15 ++++--------- homeassistant/components/starline/__init__.py | 3 ++- .../components/tellduslive/__init__.py | 4 ++-- homeassistant/components/tradfri/__init__.py | 2 +- homeassistant/components/zha/__init__.py | 8 ++++--- homeassistant/components/zha/core/helpers.py | 3 ++- tests/components/deconz/test_deconz_event.py | 17 ++++++++------ tests/components/deconz/test_logbook.py | 5 +++-- .../home_plus_control/test_switch.py | 13 +++++------ tests/components/kaleidescape/test_init.py | 3 ++- .../kaleidescape/test_media_player.py | 3 ++- tests/components/kraken/test_sensor.py | 7 +++--- tests/components/mikrotik/test_init.py | 2 +- tests/components/picnic/test_sensor.py | 7 +++--- tests/components/unifi/test_init.py | 7 +++--- tests/components/unifi/test_services.py | 20 ++++++++--------- 28 files changed, 105 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index b971a428a76..25e0df913d8 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -363,7 +363,7 @@ class PairedSensorManager: # Remove the paired sensor device from the device registry (which will # clean up entities and the entity registry): - dev_reg = await self._hass.helpers.device_registry.async_get_registry() + dev_reg = dr.async_get(self._hass) device = dev_reg.async_get_or_create( config_entry_id=self._entry.entry_id, identifiers={(DOMAIN, uid)} ) diff --git a/homeassistant/components/hue/v1/hue_event.py b/homeassistant/components/hue/v1/hue_event.py index 9074baaaa88..b3faf88c2d9 100644 --- a/homeassistant/components/hue/v1/hue_event.py +++ b/homeassistant/components/hue/v1/hue_event.py @@ -10,6 +10,7 @@ from aiohue.v1.sensors import ( from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_UNIQUE_ID from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr from homeassistant.util import dt as dt_util, slugify from ..const import ATTR_HUE_EVENT @@ -89,9 +90,7 @@ class HueEvent(GenericHueDevice): async def async_update_device_registry(self): """Update device registry.""" - device_registry = ( - await self.bridge.hass.helpers.device_registry.async_get_registry() - ) + device_registry = dr.async_get(self.bridge.hass) entry = device_registry.async_get_or_create( config_entry_id=self.bridge.config_entry.entry_id, **self.device_info diff --git a/homeassistant/components/insteon/api/aldb.py b/homeassistant/components/insteon/api/aldb.py index a3132570ccc..a119707b03d 100644 --- a/homeassistant/components/insteon/api/aldb.py +++ b/homeassistant/components/insteon/api/aldb.py @@ -12,6 +12,7 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr from ..const import DEVICE_ADDRESS, ID, INSTEON_DEVICE_NOT_FOUND, TYPE from .device import async_device_name, notify_device_not_found @@ -82,7 +83,7 @@ async def websocket_get_aldb( aldb.update(device.aldb.pending_changes) changed_records = list(device.aldb.pending_changes.keys()) - dev_registry = await hass.helpers.device_registry.async_get_registry() + dev_registry = dr.async_get(hass) records = [ await async_aldb_record_to_dict( diff --git a/homeassistant/components/insteon/api/device.py b/homeassistant/components/insteon/api/device.py index beef78394fa..ea6bade4835 100644 --- a/homeassistant/components/insteon/api/device.py +++ b/homeassistant/components/insteon/api/device.py @@ -6,6 +6,7 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr from ..const import ( DEVICE_ADDRESS, @@ -68,7 +69,7 @@ async def websocket_get_device( msg: dict, ) -> None: """Get an Insteon device.""" - dev_registry = await hass.helpers.device_registry.async_get_registry() + dev_registry = dr.async_get(hass) if not (ha_device := dev_registry.async_get(msg[DEVICE_ID])): notify_device_not_found(connection, msg, HA_DEVICE_NOT_FOUND) return diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py index 03647559345..e8e34ee8e62 100644 --- a/homeassistant/components/insteon/utils.py +++ b/homeassistant/components/insteon/utils.py @@ -28,6 +28,7 @@ from homeassistant.const import ( ENTITY_MATCH_ALL, ) from homeassistant.core import ServiceCall, callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -294,7 +295,7 @@ def async_register_services(hass): """Remove the device and all entities from hass.""" signal = f"{address.id}_{SIGNAL_REMOVE_ENTITY}" async_dispatcher_send(hass, signal) - dev_registry = await hass.helpers.device_registry.async_get_registry() + dev_registry = dr.async_get(hass) device = dev_registry.async_get_device(identifiers={(DOMAIN, str(address))}) if device: dev_registry.async_remove_device(device.id) diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index f0dc249d26a..1ef250a3f4e 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -11,7 +11,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.typing import ConfigType from .const import ( @@ -76,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return False hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = hub - 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={(DOMAIN, hub.serial_num)}, diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 5a34df1d74b..7d8353738ff 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -42,7 +42,11 @@ from homeassistant.exceptions import ( HomeAssistantError, Unauthorized, ) -from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.typing import ConfigType @@ -145,7 +149,7 @@ class SignalUpdateCallback: 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() + device_registry = dr.async_get(self._hass) device_entry = device_registry.async_get_device({(DOMAIN, device_id)}) if not device_entry: return diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index 28eb444b91a..67b361e76d8 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -16,7 +16,7 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant -from homeassistant.helpers.device_registry import DeviceRegistry +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import ConfigType from .const import DATA_SUBSCRIBER, DOMAIN @@ -35,9 +35,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_get_nest_device_id(hass: HomeAssistant, device_id: str) -> str | None: """Get the nest API device_id from the HomeAssistant device_id.""" - device_registry: DeviceRegistry = ( - await hass.helpers.device_registry.async_get_registry() - ) + device_registry = dr.async_get(hass) if device := device_registry.async_get(device_id): for (domain, unique_id) in device.identifiers: if domain == DOMAIN: diff --git a/homeassistant/components/netatmo/device_trigger.py b/homeassistant/components/netatmo/device_trigger.py index 7a9acac2f96..ae62956b691 100644 --- a/homeassistant/components/netatmo/device_trigger.py +++ b/homeassistant/components/netatmo/device_trigger.py @@ -23,7 +23,11 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant -from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry, +) from homeassistant.helpers.typing import ConfigType from .climate import STATE_NETATMO_AWAY, STATE_NETATMO_HG, STATE_NETATMO_SCHEDULE @@ -74,9 +78,14 @@ async def async_validate_trigger_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]) + if not device: + raise InvalidDeviceAutomationConfig( + f"Trigger invalid, device with ID {config[CONF_DEVICE_ID]} not found" + ) + trigger = config[CONF_TYPE] if ( @@ -94,11 +103,14 @@ async def async_get_triggers( ) -> list[dict[str, Any]]: """List device triggers for Netatmo devices.""" registry = entity_registry.async_get(hass) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) triggers = [] for entry in entity_registry.async_entries_for_device(registry, device_id): - device = device_registry.async_get(device_id) + if ( + device := device_registry.async_get(device_id) + ) is None or device.model is None: + continue for trigger in DEVICES.get(device.model, []): if trigger in SUBTYPES: @@ -134,7 +146,7 @@ async def async_attach_trigger( automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" - 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]) if not device: diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index 56f25e04906..decedbbdfbd 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -3,6 +3,7 @@ from __future__ import annotations from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( @@ -65,9 +66,9 @@ class NetatmoBase(Entity): if sub is None: await self.data_handler.unregister_data_class(signal_name, None) - registry = await self.hass.helpers.device_registry.async_get_registry() - device = registry.async_get_device({(DOMAIN, self._id)}) - self.hass.data[DOMAIN][DATA_DEVICE_IDS][self._id] = device.id + registry = dr.async_get(self.hass) + if device := registry.async_get_device({(DOMAIN, self._id)}): + self.hass.data[DOMAIN][DATA_DEVICE_IDS][self._id] = device.id self.async_update_callback() diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 41ae27b2992..217b2146cc9 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -29,7 +29,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.device_registry import async_entries_for_config_entry +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -393,13 +393,13 @@ async def async_setup_entry( async_add_entities(await find_entities(data_class_name), True) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) async def add_public_entities(update: bool = True) -> None: """Retrieve Netatmo public weather entities.""" entities = { device.name: device.id - for device in async_entries_for_config_entry( + for device in dr.async_entries_for_config_entry( device_registry, entry.entry_id ) if device.model == "Public Weather stations" diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 4ca83d98242..dbfe55077d7 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -299,14 +299,14 @@ def async_cleanup_plex_devices(hass, entry): device_registry = dev_reg.async_get(hass) entity_registry = ent_reg.async_get(hass) - device_entries = hass.helpers.device_registry.async_entries_for_config_entry( + device_entries = dev_reg.async_entries_for_config_entry( device_registry, entry.entry_id ) for device_entry in device_entries: if ( len( - hass.helpers.entity_registry.async_entries_for_device( + ent_reg.async_entries_for_device( entity_registry, device_entry.id, include_disabled_entities=True ) ) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 89521aba9f2..b517abafc86 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -24,12 +24,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import Event, HomeAssistant, ServiceCall, callback -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import ( - EVENT_DEVICE_REGISTRY_UPDATED, - DeviceEntry, - DeviceRegistry, -) +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -174,9 +169,7 @@ async def async_setup_internal(hass, entry: ConfigEntry): devices = _get_device_lookup(config[CONF_DEVICES]) pt2262_devices: list[str] = [] - device_registry: DeviceRegistry = ( - await hass.helpers.device_registry.async_get_registry() - ) + device_registry = dr.async_get(hass) # Declare the Handle event @callback @@ -268,7 +261,7 @@ async def async_setup_internal(hass, entry: ConfigEntry): _remove_device(device_id) entry.async_on_unload( - hass.bus.async_listen(EVENT_DEVICE_REGISTRY_UPDATED, _updated_device) + hass.bus.async_listen(dr.EVENT_DEVICE_REGISTRY_UPDATED, _updated_device) ) def _shutdown_rfxtrx(event): @@ -448,7 +441,7 @@ def get_device_tuple_from_identifiers( async def async_remove_config_entry_device( - hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry ) -> bool: """Remove config entry from a device. diff --git a/homeassistant/components/starline/__init__.py b/homeassistant/components/starline/__init__.py index 7d05c0d47ee..886a583ae93 100644 --- a/homeassistant/components/starline/__init__.py +++ b/homeassistant/components/starline/__init__.py @@ -7,6 +7,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from .account import StarlineAccount from .const import ( @@ -33,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = {} hass.data[DOMAIN][entry.entry_id] = account - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) for device in account.api.devices.values(): device_registry.async_get_or_create( config_entry_id=entry.entry_id, **account.device_info(device) diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index 572950b816b..92feb69c60c 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -10,7 +10,7 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_SCAN_INTERVAL from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType @@ -84,7 +84,7 @@ async def async_new_client(hass, session, entry): _LOGGER.debug("Update interval %s seconds", interval) client = TelldusLiveClient(hass, entry, session, interval) hass.data[DOMAIN] = client - dev_reg = await hass.helpers.device_registry.async_get_registry() + dev_reg = dr.async_get(hass) for hub in await client.async_get_hubs(): _LOGGER.debug("Connected hub %s", hub["name"]) dev_reg.async_get_or_create( diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index e4d13568e6d..e79cd8396e1 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -84,7 +84,7 @@ async def async_setup_entry( await factory.shutdown() raise ConfigEntryNotReady from exc - dev_reg = await hass.helpers.device_registry.async_get_registry() + dev_reg = dr.async_get(hass) dev_reg.async_get_or_create( config_entry_id=entry.entry_id, connections=set(), diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index c512e104ae8..0e11d992a25 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -9,8 +9,8 @@ from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from homeassistant import const as ha_const from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType @@ -106,10 +106,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b coro = hass.config_entries.async_forward_entry_setup(config_entry, platform) zha_data[DATA_ZHA_PLATFORM_LOADED].append(hass.async_create_task(coro)) - 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_ZIGBEE, str(zha_gateway.application_controller.ieee))}, + connections={ + (dr.CONNECTION_ZIGBEE, str(zha_gateway.application_controller.ieee)) + }, identifiers={(DOMAIN, str(zha_gateway.application_controller.ieee))}, name="Zigbee Coordinator", manufacturer="ZHA", diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index fcd29c1619f..19a1d3fef9a 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -25,6 +25,7 @@ import zigpy.zdo.types as zdo_types from homeassistant.config_entries import ConfigEntry from homeassistant.core import State, callback +from homeassistant.helpers import device_registry as dr from .const import ( CLUSTER_TYPE_IN, @@ -161,7 +162,7 @@ def async_cluster_exists(hass, cluster_id): async def async_get_zha_device(hass, device_id): """Get a ZHA device for the given device registry id.""" - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) registry_device = device_registry.async_get(device_id) zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee_address = list(list(registry_device.identifiers)[0])[1] diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index 5b04b459278..e697edd5a9a 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -22,7 +22,7 @@ from homeassistant.const import ( CONF_UNIQUE_ID, STATE_UNAVAILABLE, ) -from homeassistant.helpers.device_registry import async_entries_for_config_entry +from homeassistant.helpers import device_registry as dr from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration @@ -73,12 +73,13 @@ async def test_deconz_events(hass, aioclient_mock, mock_deconz_websocket): with patch.dict(DECONZ_WEB_REQUEST, data): config_entry = await setup_deconz_integration(hass, aioclient_mock) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) assert len(hass.states.async_all()) == 3 # 5 switches + 2 additional devices for deconz service and host assert ( - len(async_entries_for_config_entry(device_registry, config_entry.entry_id)) == 7 + len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + == 7 ) assert hass.states.get("sensor.switch_2_battery").state == "100" assert hass.states.get("sensor.switch_3_battery").state == "100" @@ -267,12 +268,13 @@ async def test_deconz_alarm_events(hass, aioclient_mock, mock_deconz_websocket): with patch.dict(DECONZ_WEB_REQUEST, data): config_entry = await setup_deconz_integration(hass, aioclient_mock) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) assert len(hass.states.async_all()) == 4 # 1 alarm control device + 2 additional devices for deconz service and host assert ( - len(async_entries_for_config_entry(device_registry, config_entry.entry_id)) == 3 + len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + == 3 ) captured_events = async_capture_events(hass, CONF_DECONZ_ALARM_EVENT) @@ -435,9 +437,10 @@ async def test_deconz_events_bad_unique_id(hass, aioclient_mock, mock_deconz_web with patch.dict(DECONZ_WEB_REQUEST, data): config_entry = await setup_deconz_integration(hass, aioclient_mock) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) assert len(hass.states.async_all()) == 1 assert ( - len(async_entries_for_config_entry(device_registry, config_entry.entry_id)) == 2 + len(dr.async_entries_for_config_entry(device_registry, config_entry.entry_id)) + == 2 ) diff --git a/tests/components/deconz/test_logbook.py b/tests/components/deconz/test_logbook.py index a78e795573a..9ba0799d04e 100644 --- a/tests/components/deconz/test_logbook.py +++ b/tests/components/deconz/test_logbook.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_UNIQUE_ID, STATE_ALARM_ARMED_AWAY, ) +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from homeassistant.util import slugify @@ -56,7 +57,7 @@ async def test_humanifying_deconz_alarm_event(hass, aioclient_mock): with patch.dict(DECONZ_WEB_REQUEST, data): await setup_deconz_integration(hass, aioclient_mock) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) keypad_event_id = slugify(data["sensors"]["1"]["name"]) keypad_serial = data["sensors"]["1"]["uniqueid"].split("-", 1)[0] @@ -127,7 +128,7 @@ async def test_humanifying_deconz_event(hass, aioclient_mock): with patch.dict(DECONZ_WEB_REQUEST, data): await setup_deconz_integration(hass, aioclient_mock) - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) switch_event_id = slugify(data["sensors"]["1"]["name"]) switch_serial = data["sensors"]["1"]["uniqueid"].split("-", 1)[0] diff --git a/tests/components/home_plus_control/test_switch.py b/tests/components/home_plus_control/test_switch.py index 75d416ba2b1..44a969392bf 100644 --- a/tests/components/home_plus_control/test_switch.py +++ b/tests/components/home_plus_control/test_switch.py @@ -16,6 +16,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import async_fire_time_changed from tests.components.home_plus_control.conftest import ( @@ -33,8 +34,8 @@ def entity_assertions( expected_devices=None, ): """Assert number of entities and devices.""" - entity_reg = hass.helpers.entity_registry.async_get(hass) - device_reg = hass.helpers.device_registry.async_get(hass) + entity_reg = er.async_get(hass) + device_reg = dr.async_get(hass) if num_exp_devices is None: num_exp_devices = num_exp_entities @@ -53,13 +54,11 @@ def entity_assertions( def one_entity_state(hass, device_uid): """Assert the presence of an entity and return its state.""" - entity_reg = hass.helpers.entity_registry.async_get(hass) - device_reg = hass.helpers.device_registry.async_get(hass) + entity_reg = er.async_get(hass) + device_reg = dr.async_get(hass) device_id = device_reg.async_get_device({(DOMAIN, device_uid)}).id - entity_entries = hass.helpers.entity_registry.async_entries_for_device( - entity_reg, device_id - ) + entity_entries = er.async_entries_for_device(entity_reg, device_id) assert len(entity_entries) == 1 entity_entry = entity_entries[0] diff --git a/tests/components/kaleidescape/test_init.py b/tests/components/kaleidescape/test_init.py index 876c02ba5a6..d0826f4714a 100644 --- a/tests/components/kaleidescape/test_init.py +++ b/tests/components/kaleidescape/test_init.py @@ -5,6 +5,7 @@ from unittest.mock import AsyncMock from homeassistant.components.kaleidescape.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from . import MOCK_SERIAL @@ -50,7 +51,7 @@ async def test_device( mock_integration: MockConfigEntry, ) -> None: """Test device.""" - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get_device( identifiers={("kaleidescape", MOCK_SERIAL)} ) diff --git a/tests/components/kaleidescape/test_media_player.py b/tests/components/kaleidescape/test_media_player.py index 94ba7f82fe8..11f5d5f5f2b 100644 --- a/tests/components/kaleidescape/test_media_player.py +++ b/tests/components/kaleidescape/test_media_player.py @@ -21,6 +21,7 @@ from homeassistant.const import ( STATE_PLAYING, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from . import MOCK_SERIAL @@ -173,7 +174,7 @@ async def test_device( mock_integration: MockConfigEntry, ) -> None: """Test device attributes.""" - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get_device( identifiers={("kaleidescape", MOCK_SERIAL)} ) diff --git a/tests/components/kraken/test_sensor.py b/tests/components/kraken/test_sensor.py index cbfda938f99..1d6b9e6518d 100644 --- a/tests/components/kraken/test_sensor.py +++ b/tests/components/kraken/test_sensor.py @@ -11,8 +11,7 @@ from homeassistant.components.kraken.const import ( DOMAIN, ) from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START -from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers import device_registry as dr, entity_registry as er import homeassistant.util.dt as dt_util from .const import ( @@ -249,13 +248,13 @@ async def test_sensors_available_after_restart(hass): }, ) - 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=entry.entry_id, identifiers={(DOMAIN, "XBT_USD")}, name="XBT USD", manufacturer="Kraken.com", - entry_type=DeviceEntryType.SERVICE, + entry_type=dr.DeviceEntryType.SERVICE, ) entry.add_to_hass(hass) diff --git a/tests/components/mikrotik/test_init.py b/tests/components/mikrotik/test_init.py index 281b70e36be..30fa1a0a89f 100644 --- a/tests/components/mikrotik/test_init.py +++ b/tests/components/mikrotik/test_init.py @@ -25,7 +25,7 @@ async def test_successful_config_entry(hass): mock_registry = Mock() with patch.object(mikrotik, "MikrotikHub") as mock_hub, patch( - "homeassistant.helpers.device_registry.async_get_registry", + "homeassistant.components.mikrotik.dr.async_get", return_value=mock_registry, ): mock_hub.return_value.async_setup = AsyncMock(return_value=True) diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py index 808f1ce6f41..8d680327bc8 100644 --- a/tests/components/picnic/test_sensor.py +++ b/tests/components/picnic/test_sensor.py @@ -17,8 +17,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util import dt from tests.common import ( @@ -497,13 +496,13 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): # Setup platform and default mock responses await self._setup_platform(use_default_responses=True) - device_registry = await self.hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(self.hass) picnic_service = device_registry.async_get_device( identifiers={(const.DOMAIN, DEFAULT_USER_RESPONSE["user_id"])} ) assert picnic_service.model == DEFAULT_USER_RESPONSE["user_id"] assert picnic_service.name == "Picnic: Commonstreet 123a" - assert picnic_service.entry_type is DeviceEntryType.SERVICE + assert picnic_service.entry_type is dr.DeviceEntryType.SERVICE async def test_auth_token_is_saved_on_update(self): """Test that auth-token changes in the session object are reflected by the config entry.""" diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index b2d37ad7ee3..f183e1c22ff 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, patch from homeassistant.components import unifi from homeassistant.components.unifi import async_flatten_entry_data from homeassistant.components.unifi.const import CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from .test_controller import ( @@ -53,9 +53,10 @@ async def test_controller_mac(hass): assert len(mock_controller.mock_calls) == 2 - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device = device_registry.async_get_or_create( - config_entry_id=entry.entry_id, connections={(CONNECTION_NETWORK_MAC, "mac1")} + config_entry_id=entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "mac1")}, ) assert device.configuration_url == "https://123:443" assert device.manufacturer == "Ubiquiti Networks" diff --git a/tests/components/unifi/test_services.py b/tests/components/unifi/test_services.py index 27e4ddea930..0c6f20869c8 100644 --- a/tests/components/unifi/test_services.py +++ b/tests/components/unifi/test_services.py @@ -9,7 +9,7 @@ from homeassistant.components.unifi.services import ( SUPPORTED_SERVICES, ) from homeassistant.const import ATTR_DEVICE_ID -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers import device_registry as dr from .test_controller import setup_unifi_integration @@ -62,10 +62,10 @@ async def test_reconnect_client(hass, aioclient_mock): f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr", ) - device_registry = await hass.helpers.device_registry.async_get_registry() + 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, clients[0]["mac"])}, + connections={(dr.CONNECTION_NETWORK_MAC, clients[0]["mac"])}, ) await hass.services.async_call( @@ -98,7 +98,7 @@ async def test_reconnect_device_without_mac(hass, aioclient_mock): aioclient_mock.clear_requests() - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry = dr.async_get(hass) device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, connections={("other connection", "not mac")}, @@ -132,10 +132,10 @@ async def test_reconnect_client_controller_unavailable(hass, aioclient_mock): f"https://{controller.host}:1234/api/s/{controller.site}/cmd/stamgr", ) - device_registry = await hass.helpers.device_registry.async_get_registry() + 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, clients[0]["mac"])}, + connections={(dr.CONNECTION_NETWORK_MAC, clients[0]["mac"])}, ) await hass.services.async_call( @@ -153,10 +153,10 @@ async def test_reconnect_client_unknown_mac(hass, aioclient_mock): aioclient_mock.clear_requests() - device_registry = await hass.helpers.device_registry.async_get_registry() + 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, "mac unknown to controller")}, + connections={(dr.CONNECTION_NETWORK_MAC, "mac unknown to controller")}, ) await hass.services.async_call( @@ -182,10 +182,10 @@ async def test_reconnect_wired_client(hass, aioclient_mock): aioclient_mock.clear_requests() - device_registry = await hass.helpers.device_registry.async_get_registry() + 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, clients[0]["mac"])}, + connections={(dr.CONNECTION_NETWORK_MAC, clients[0]["mac"])}, ) await hass.services.async_call( From a78f183b640b7f59ece8429c95aae21bd3120b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 17 May 2022 21:47:28 +0200 Subject: [PATCH 0591/3516] Fix Airzone sensor and binary sensor updates (#72025) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit airzone: fix sensor and binary sensor updates This regression was introduced in b9b83c05e9d53963891bb50ec9ed14c2bb07f4b5 along with the modifications for the strict typing. Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone/binary_sensor.py | 6 ++++++ homeassistant/components/airzone/sensor.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/homeassistant/components/airzone/binary_sensor.py b/homeassistant/components/airzone/binary_sensor.py index d1885e0edf9..7ad3254c080 100644 --- a/homeassistant/components/airzone/binary_sensor.py +++ b/homeassistant/components/airzone/binary_sensor.py @@ -119,6 +119,12 @@ class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity): entity_description: AirzoneBinarySensorEntityDescription + @callback + def _handle_coordinator_update(self) -> None: + """Update attributes when the coordinator updates.""" + self._async_update_attrs() + super()._handle_coordinator_update() + @callback def _async_update_attrs(self) -> None: """Update binary sensor attributes.""" diff --git a/homeassistant/components/airzone/sensor.py b/homeassistant/components/airzone/sensor.py index 1671eb919a7..86d88aa5a55 100644 --- a/homeassistant/components/airzone/sensor.py +++ b/homeassistant/components/airzone/sensor.py @@ -102,6 +102,12 @@ async def async_setup_entry( class AirzoneSensor(AirzoneEntity, SensorEntity): """Define an Airzone sensor.""" + @callback + def _handle_coordinator_update(self) -> None: + """Update attributes when the coordinator updates.""" + self._async_update_attrs() + super()._handle_coordinator_update() + @callback def _async_update_attrs(self) -> None: """Update sensor attributes.""" From afe2b71b2e694bbc08138ad7dd2c6c24d3125049 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 May 2022 21:49:19 +0200 Subject: [PATCH 0592/3516] Update model info from SSDP in SamsungTV (#71992) * Update model info from SSDP in SamsungTV * Add tests --- .../components/samsungtv/config_flow.py | 6 ++++ .../components/samsungtv/test_config_flow.py | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index adcec0e8b2b..02414d3f476 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -167,6 +167,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): updates = {CONF_HOST: self._host} if self._mac: updates[CONF_MAC] = self._mac + if self._model: + updates[CONF_MODEL] = self._model if self._ssdp_rendering_control_location: updates[ CONF_SSDP_RENDERING_CONTROL_LOCATION @@ -380,10 +382,12 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): != self._ssdp_main_tv_agent_location ) update_mac = self._mac and not data.get(CONF_MAC) + update_model = self._model and not data.get(CONF_MODEL) if ( update_ssdp_rendering_control_location or update_ssdp_main_tv_agent_location or update_mac + or update_model ): if update_ssdp_rendering_control_location: data[ @@ -395,6 +399,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ] = self._ssdp_main_tv_agent_location if update_mac: data[CONF_MAC] = self._mac + if update_model: + data[CONF_MODEL] = self._model entry_kw_args["data"] = data if not entry_kw_args: return None diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 40397a68d7d..11aaf12d9ee 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -1420,6 +1420,36 @@ async def test_update_missing_mac_unique_id_added_from_zeroconf( assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" +@pytest.mark.usefixtures("remote", "rest_api_failing") +async def test_update_missing_model_added_from_ssdp(hass: HomeAssistant) -> None: + """Test missing model added via ssdp on legacy models.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_OLD_ENTRY, + unique_id=None, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.samsungtv.async_setup", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.samsungtv.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=MOCK_SSDP_DATA, + ) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MODEL] == "fake_model" + + @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_ssdp_location_added_from_ssdp( hass: HomeAssistant, From d1d6c6b9231f23455e763b89bb95de1b665b6b24 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 17 May 2022 21:50:03 +0200 Subject: [PATCH 0593/3516] Cleanup SamsungTV log message (#71987) --- homeassistant/components/samsungtv/bridge.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index c3201a493eb..fe0b102647a 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -307,9 +307,7 @@ class SamsungTVLegacyBridge(SamsungTVBridge): if self._remote is None: # We need to create a new instance to reconnect. try: - LOGGER.debug( - "Create SamsungTVLegacyBridge for %s (%s)", CONF_NAME, self.host - ) + LOGGER.debug("Create SamsungTVLegacyBridge for %s", self.host) self._remote = Remote(self.config.copy()) # This is only happening when the auth was switched to DENY # A removed auth will lead to socket timeout because waiting for auth popup is just an open socket From bfb47eb212ea1a00aa42140b3d898419ebd553bf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 May 2022 22:42:37 +0200 Subject: [PATCH 0594/3516] Final clean up of helpers accessed via hass (#72032) * Final clean up of helpers accessed via hass * Fix circular dep * Fix import --- homeassistant/components/cloud/__init__.py | 13 ++----- homeassistant/components/demo/__init__.py | 5 +-- homeassistant/components/homekit/__init__.py | 4 +- homeassistant/components/ombi/__init__.py | 3 +- homeassistant/components/onboarding/views.py | 3 +- .../components/proxmoxve/__init__.py | 5 +-- homeassistant/components/sentry/__init__.py | 9 +++-- homeassistant/components/zeroconf/__init__.py | 4 +- homeassistant/helpers/device_registry.py | 12 ++++-- tests/components/cloud/test_binary_sensor.py | 5 +-- .../components/config/test_config_entries.py | 6 +-- tests/components/fan/test_reproduce_state.py | 38 ++++++++++--------- tests/components/homekit/test_homekit.py | 8 ++-- tests/components/plex/test_device_handling.py | 18 +++------ tests/helpers/test_instance_id.py | 6 ++- tests/helpers/test_integration_platform.py | 9 ++--- tests/helpers/test_system_info.py | 9 +++-- 17 files changed, 75 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 559453fe02b..aa31f796491 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -22,6 +22,7 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entityfilter from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -267,15 +268,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return loaded = True - await hass.helpers.discovery.async_load_platform( - Platform.BINARY_SENSOR, DOMAIN, {}, config - ) - await hass.helpers.discovery.async_load_platform( - Platform.STT, DOMAIN, {}, config - ) - await hass.helpers.discovery.async_load_platform( - Platform.TTS, DOMAIN, {}, config - ) + await async_load_platform(hass, Platform.BINARY_SENSOR, DOMAIN, {}, config) + await async_load_platform(hass, Platform.STT, DOMAIN, {}, config) + await async_load_platform(hass, Platform.TTS, DOMAIN, {}, config) async_dispatcher_send( hass, SIGNAL_CLOUD_CONNECTION_STATE, CloudConnectionState.CLOUD_CONNECTED diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 9c399c67f35..d6c5a5d3afc 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( ) import homeassistant.core as ha from homeassistant.core import HomeAssistant +from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util @@ -71,9 +72,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Set up demo platforms for platform in COMPONENTS_WITH_DEMO_PLATFORM: - hass.async_create_task( - hass.helpers.discovery.async_load_platform(platform, DOMAIN, {}, config) - ) + hass.async_create_task(async_load_platform(hass, platform, DOMAIN, {}, config)) config.setdefault(ha.DOMAIN, {}) config.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 1cba9746609..5d7e647e466 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -46,7 +46,7 @@ from homeassistant.const import ( ) from homeassistant.core import CoreState, HomeAssistant, ServiceCall, State, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized -from homeassistant.helpers import device_registry, entity_registry +from homeassistant.helpers import device_registry, entity_registry, instance_id import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import ( BASE_FILTER_SCHEMA, @@ -726,7 +726,7 @@ class HomeKit: return self.status = STATUS_WAIT async_zc_instance = await zeroconf.async_get_async_instance(self.hass) - uuid = await self.hass.helpers.instance_id.async_get() + uuid = await instance_id.async_get(self.hass) await self.hass.async_add_executor_job(self.setup, async_zc_instance, uuid) self.aid_storage = AccessoryAidStorage(self.hass, self._entry_id) await self.aid_storage.async_initialize() diff --git a/homeassistant/components/ombi/__init__.py b/homeassistant/components/ombi/__init__.py index bdf8334ff02..b67b097dfbf 100644 --- a/homeassistant/components/ombi/__init__.py +++ b/homeassistant/components/ombi/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.typing import ConfigType from .const import ( @@ -152,6 +153,6 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: submit_tv_request, schema=SUBMIT_TV_REQUEST_SERVICE_SCHEMA, ) - hass.helpers.discovery.load_platform(Platform.SENSOR, DOMAIN, {}, config) + load_platform(hass, Platform.SENSOR, DOMAIN, {}, config) return True diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index 07b3dfa9cc2..7f40ad87e84 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -14,6 +14,7 @@ from homeassistant.components.http.const import KEY_HASS_REFRESH_TOKEN_ID from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView from homeassistant.core import callback +from homeassistant.helpers import area_registry as ar from homeassistant.helpers.system_info import async_get_system_info from homeassistant.helpers.translation import async_get_translations @@ -152,7 +153,7 @@ class UserOnboardingView(_BaseOnboardingView): hass, data["language"], "area", {DOMAIN} ) - area_registry = await hass.helpers.area_registry.async_get_registry() + area_registry = ar.async_get(hass) for area in DEFAULT_AREAS: area_registry.async_create( diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index d10e64772ae..1334a66cfa0 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -21,6 +21,7 @@ from homeassistant.const import ( ) 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 from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -178,9 +179,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: for component in PLATFORMS: await hass.async_create_task( - hass.helpers.discovery.async_load_platform( - component, DOMAIN, {"config": config}, config - ) + async_load_platform(hass, component, DOMAIN, {"config": config}, config) ) return True diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index ab3b80414dd..3037f1dc374 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -16,8 +16,9 @@ from homeassistant.const import ( __version__ as current_version, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers import config_validation as cv, entity_platform, instance_id from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.system_info import async_get_system_info from homeassistant.loader import Integration, async_get_custom_components from .const import ( @@ -67,8 +68,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Additional/extra data collection channel = get_channel(current_version) - huuid = await hass.helpers.instance_id.async_get() - system_info = await hass.helpers.system_info.async_get_system_info() + huuid = await instance_id.async_get(hass) + system_info = await async_get_system_info(hass) custom_components = await async_get_custom_components(hass) tracing = {} @@ -100,7 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def update_system_info(now): nonlocal system_info - system_info = await hass.helpers.system_info.async_get_system_info() + system_info = await async_get_system_info(hass) # Update system info every hour async_call_later(hass, 3600, update_system_info) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 2c16868f9de..29afd5fc236 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -28,7 +28,7 @@ from homeassistant.const import ( ) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.data_entry_flow import BaseServiceInfo -from homeassistant.helpers import discovery_flow +from homeassistant.helpers import discovery_flow, instance_id import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import NoURLAvailableError, get_url from homeassistant.helpers.typing import ConfigType @@ -198,7 +198,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: Wait till started or otherwise HTTP is not up and running. """ - uuid = await hass.helpers.instance_id.async_get() + uuid = await instance_id.async_get(hass) await _async_register_hass_zc_service(hass, aio_zc, uuid) async def _async_zeroconf_hass_stop(_event: Event) -> None: diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 7299da43958..901242607e3 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -420,6 +420,10 @@ class DeviceRegistry: via_device_id: str | None | UndefinedType = UNDEFINED, ) -> DeviceEntry | None: """Update device attributes.""" + # Circular dep + # pylint: disable=import-outside-toplevel + from . import area_registry as ar + old = self.devices[device_id] new_values: dict[str, Any] = {} # Dict with new key/value pairs @@ -442,13 +446,13 @@ class DeviceRegistry: disabled_by = DeviceEntryDisabler(disabled_by) if ( - suggested_area not in (UNDEFINED, None, "") + suggested_area is not None + and suggested_area is not UNDEFINED + and suggested_area != "" and area_id is UNDEFINED and old.area_id is None ): - area = self.hass.helpers.area_registry.async_get( - self.hass - ).async_get_or_create(suggested_area) + area = ar.async_get(self.hass).async_get_or_create(suggested_area) area_id = area.id if ( diff --git a/tests/components/cloud/test_binary_sensor.py b/tests/components/cloud/test_binary_sensor.py index 0aacb189d72..fb2ca562715 100644 --- a/tests/components/cloud/test_binary_sensor.py +++ b/tests/components/cloud/test_binary_sensor.py @@ -2,6 +2,7 @@ from unittest.mock import Mock, patch from homeassistant.components.cloud.const import DISPATCHER_REMOTE_UPDATE +from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component @@ -14,9 +15,7 @@ async def test_remote_connection_sensor(hass): assert hass.states.get("binary_sensor.remote_ui") is None # Fake connection/discovery - await hass.helpers.discovery.async_load_platform( - "binary_sensor", "cloud", {}, {"cloud": {}} - ) + await async_load_platform(hass, "binary_sensor", "cloud", {}, {"cloud": {}}) # Mock test env cloud = hass.data["cloud"] = Mock() diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 6366eca4c6d..bb0d67fc306 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -12,7 +12,7 @@ from homeassistant.components.config import config_entries from homeassistant.config_entries import HANDLERS, ConfigFlow from homeassistant.core import callback from homeassistant.generated import config_flows -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_entry_flow, config_validation as cv from homeassistant.setup import async_setup_component from tests.common import ( @@ -68,9 +68,7 @@ async def test_get_entries(hass, client, clear_handlers): """Return options flow support for this handler.""" return True - hass.helpers.config_entry_flow.register_discovery_flow( - "comp2", "Comp 2", lambda: None - ) + config_entry_flow.register_discovery_flow("comp2", "Comp 2", lambda: None) entry = MockConfigEntry( domain="comp1", diff --git a/tests/components/fan/test_reproduce_state.py b/tests/components/fan/test_reproduce_state.py index cca56d3e128..e33f1005d75 100644 --- a/tests/components/fan/test_reproduce_state.py +++ b/tests/components/fan/test_reproduce_state.py @@ -183,8 +183,8 @@ async def test_modern_turn_on_invalid(hass, start_state): set_preset_mode = async_mock_service(hass, "fan", "set_preset_mode") # Turn on with an invalid config (speed, percentage, preset_modes all None) - await hass.helpers.state.async_reproduce_state( - [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_INVALID_STATE)] + await async_reproduce_state( + hass, [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_INVALID_STATE)] ) assert len(turn_on_calls) == 1 @@ -227,8 +227,8 @@ async def test_modern_turn_on_percentage_from_different_speed(hass, start_state) set_percentage_mode = async_mock_service(hass, "fan", "set_percentage") set_preset_mode = async_mock_service(hass, "fan", "set_preset_mode") - await hass.helpers.state.async_reproduce_state( - [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_PERCENTAGE15_STATE)] + await async_reproduce_state( + hass, [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_PERCENTAGE15_STATE)] ) assert len(turn_on_calls) == 1 @@ -256,8 +256,8 @@ async def test_modern_turn_on_percentage_from_same_speed(hass): set_percentage_mode = async_mock_service(hass, "fan", "set_percentage") set_preset_mode = async_mock_service(hass, "fan", "set_preset_mode") - await hass.helpers.state.async_reproduce_state( - [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_PERCENTAGE15_STATE)] + await async_reproduce_state( + hass, [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_PERCENTAGE15_STATE)] ) assert len(turn_on_calls) == 1 @@ -293,8 +293,8 @@ async def test_modern_turn_on_preset_mode_from_different_speed(hass, start_state set_percentage_mode = async_mock_service(hass, "fan", "set_percentage") set_preset_mode = async_mock_service(hass, "fan", "set_preset_mode") - await hass.helpers.state.async_reproduce_state( - [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_PRESET_MODE_AUTO_STATE)] + await async_reproduce_state( + hass, [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_PRESET_MODE_AUTO_STATE)] ) assert len(turn_on_calls) == 1 @@ -324,8 +324,8 @@ async def test_modern_turn_on_preset_mode_from_same_speed(hass): set_percentage_mode = async_mock_service(hass, "fan", "set_percentage") set_preset_mode = async_mock_service(hass, "fan", "set_preset_mode") - await hass.helpers.state.async_reproduce_state( - [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_PRESET_MODE_AUTO_STATE)] + await async_reproduce_state( + hass, [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_PRESET_MODE_AUTO_STATE)] ) assert len(turn_on_calls) == 1 @@ -361,8 +361,9 @@ async def test_modern_turn_on_preset_mode_reverse(hass, start_state): set_percentage_mode = async_mock_service(hass, "fan", "set_percentage") set_preset_mode = async_mock_service(hass, "fan", "set_preset_mode") - await hass.helpers.state.async_reproduce_state( - [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_PRESET_MODE_AUTO_REVERSE_STATE)] + await async_reproduce_state( + hass, + [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_PRESET_MODE_AUTO_REVERSE_STATE)], ) assert len(turn_on_calls) == 1 @@ -403,8 +404,8 @@ async def test_modern_to_preset(hass, start_state): set_percentage_mode = async_mock_service(hass, "fan", "set_percentage") set_preset_mode = async_mock_service(hass, "fan", "set_preset_mode") - await hass.helpers.state.async_reproduce_state( - [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_PRESET_MODE_AUTO_STATE)] + await async_reproduce_state( + hass, [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_PRESET_MODE_AUTO_STATE)] ) assert len(turn_on_calls) == 0 @@ -439,8 +440,8 @@ async def test_modern_to_percentage(hass, start_state): set_percentage_mode = async_mock_service(hass, "fan", "set_percentage") set_preset_mode = async_mock_service(hass, "fan", "set_preset_mode") - await hass.helpers.state.async_reproduce_state( - [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_PERCENTAGE15_STATE)] + await async_reproduce_state( + hass, [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_PERCENTAGE15_STATE)] ) assert len(turn_on_calls) == 0 @@ -467,8 +468,9 @@ async def test_modern_direction(hass): set_percentage_mode = async_mock_service(hass, "fan", "set_percentage") set_preset_mode = async_mock_service(hass, "fan", "set_preset_mode") - await hass.helpers.state.async_reproduce_state( - [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_PRESET_MODE_AUTO_REVERSE_STATE)] + await async_reproduce_state( + hass, + [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_PRESET_MODE_AUTO_REVERSE_STATE)], ) assert len(turn_on_calls) == 0 diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index ff0f3f4d72b..be514ce2b6a 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -49,7 +49,7 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.core import HomeAssistantError, State -from homeassistant.helpers import device_registry, entity_registry as er +from homeassistant.helpers import device_registry, entity_registry as er, instance_id from homeassistant.helpers.entityfilter import ( CONF_EXCLUDE_DOMAINS, CONF_EXCLUDE_ENTITIES, @@ -201,7 +201,7 @@ async def test_homekit_setup(hass, hk_driver, mock_async_zeroconf): hass.states.async_set("light.demo", "on") hass.states.async_set("light.demo2", "on") zeroconf_mock = MagicMock() - uuid = await hass.helpers.instance_id.async_get() + uuid = await instance_id.async_get(hass) with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver: await hass.async_add_executor_job(homekit.setup, zeroconf_mock, uuid) @@ -245,7 +245,7 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_async_zeroconf): ) path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) - uuid = await hass.helpers.instance_id.async_get() + uuid = await instance_id.async_get(hass) with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver: await hass.async_add_executor_job(homekit.setup, mock_async_zeroconf, uuid) mock_driver.assert_called_with( @@ -287,7 +287,7 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_async_zeroconf): async_zeroconf_instance = MagicMock() path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) - uuid = await hass.helpers.instance_id.async_get() + uuid = await instance_id.async_get(hass) with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver: await hass.async_add_executor_job(homekit.setup, async_zeroconf_instance, uuid) mock_driver.assert_called_with( diff --git a/tests/components/plex/test_device_handling.py b/tests/components/plex/test_device_handling.py index 6d2b7524d34..38145b27deb 100644 --- a/tests/components/plex/test_device_handling.py +++ b/tests/components/plex/test_device_handling.py @@ -88,16 +88,12 @@ async def test_migrate_transient_devices( ) assert ( - len( - hass.helpers.entity_registry.async_entries_for_device( - entity_registry, device_id=plexweb_device.id - ) - ) + len(er.async_entries_for_device(entity_registry, device_id=plexweb_device.id)) == 1 ) assert ( len( - hass.helpers.entity_registry.async_entries_for_device( + er.async_entries_for_device( entity_registry, device_id=non_plexweb_device.id ) ) @@ -113,16 +109,12 @@ async def test_migrate_transient_devices( ) assert ( - len( - hass.helpers.entity_registry.async_entries_for_device( - entity_registry, device_id=plexweb_device.id - ) - ) + len(er.async_entries_for_device(entity_registry, device_id=plexweb_device.id)) == 0 ) assert ( len( - hass.helpers.entity_registry.async_entries_for_device( + er.async_entries_for_device( entity_registry, device_id=non_plexweb_device.id ) ) @@ -130,7 +122,7 @@ async def test_migrate_transient_devices( ) assert ( len( - hass.helpers.entity_registry.async_entries_for_device( + er.async_entries_for_device( entity_registry, device_id=plex_service_device.id ) ) diff --git a/tests/helpers/test_instance_id.py b/tests/helpers/test_instance_id.py index c8417c519a1..a8c040f48ba 100644 --- a/tests/helpers/test_instance_id.py +++ b/tests/helpers/test_instance_id.py @@ -1,10 +1,12 @@ """Tests for instance ID helper.""" from unittest.mock import patch +from homeassistant.helpers import instance_id + async def test_get_id_empty(hass, hass_storage): """Get unique ID.""" - uuid = await hass.helpers.instance_id.async_get() + uuid = await instance_id.async_get(hass) assert uuid is not None # Assert it's stored assert hass_storage["core.uuid"]["data"]["uuid"] == uuid @@ -15,7 +17,7 @@ async def test_get_id_migrate(hass, hass_storage): with patch( "homeassistant.util.json.load_json", return_value={"uuid": "1234"} ), patch("os.path.isfile", return_value=True), patch("os.remove") as mock_remove: - uuid = await hass.helpers.instance_id.async_get() + uuid = await instance_id.async_get(hass) assert uuid == "1234" diff --git a/tests/helpers/test_integration_platform.py b/tests/helpers/test_integration_platform.py index a9d0da0b849..584af602062 100644 --- a/tests/helpers/test_integration_platform.py +++ b/tests/helpers/test_integration_platform.py @@ -3,6 +3,7 @@ from unittest.mock import Mock from homeassistant.helpers.integration_platform import ( async_process_integration_platform_for_component, + async_process_integration_platforms, ) from homeassistant.setup import ATTR_COMPONENT, EVENT_COMPONENT_LOADED @@ -24,8 +25,8 @@ async def test_process_integration_platforms(hass): """Process platform.""" processed.append((domain, platform)) - await hass.helpers.integration_platform.async_process_integration_platforms( - "platform_to_check", _process_platform + await async_process_integration_platforms( + hass, "platform_to_check", _process_platform ) assert len(processed) == 1 @@ -40,9 +41,7 @@ async def test_process_integration_platforms(hass): assert processed[1][1] == event_platform # Verify we only process the platform once if we call it manually - await hass.helpers.integration_platform.async_process_integration_platform_for_component( - hass, "event" - ) + await async_process_integration_platform_for_component(hass, "event") assert len(processed) == 2 diff --git a/tests/helpers/test_system_info.py b/tests/helpers/test_system_info.py index e4aba5fbb24..a3b6dafe600 100644 --- a/tests/helpers/test_system_info.py +++ b/tests/helpers/test_system_info.py @@ -3,11 +3,12 @@ import json from unittest.mock import patch from homeassistant.const import __version__ as current_version +from homeassistant.helpers.system_info import async_get_system_info async def test_get_system_info(hass): """Test the get system info.""" - info = await hass.helpers.system_info.async_get_system_info() + info = await async_get_system_info(hass) assert isinstance(info, dict) assert info["version"] == current_version assert info["user"] is not None @@ -19,18 +20,18 @@ async def test_container_installationtype(hass): with patch("platform.system", return_value="Linux"), patch( "os.path.isfile", return_value=True ), patch("homeassistant.helpers.system_info.getuser", return_value="root"): - info = await hass.helpers.system_info.async_get_system_info() + info = await async_get_system_info(hass) assert info["installation_type"] == "Home Assistant Container" with patch("platform.system", return_value="Linux"), patch( "os.path.isfile", side_effect=lambda file: file == "/.dockerenv" ), patch("homeassistant.helpers.system_info.getuser", return_value="user"): - info = await hass.helpers.system_info.async_get_system_info() + info = await async_get_system_info(hass) assert info["installation_type"] == "Unsupported Third Party Container" async def test_getuser_keyerror(hass): """Test getuser keyerror.""" with patch("homeassistant.helpers.system_info.getuser", side_effect=KeyError): - info = await hass.helpers.system_info.async_get_system_info() + info = await async_get_system_info(hass) assert info["user"] is None From a4c1bcefb9d2a6f2aa0bc189fca496d46c78e3b0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 May 2022 18:12:15 -0500 Subject: [PATCH 0595/3516] Tune sqlite based on configured settings (#72016) --- homeassistant/components/recorder/util.py | 14 ++++- tests/components/recorder/test_util.py | 64 ++++++++++++++++++++--- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index eb99c304808..e464ac4126b 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -426,8 +426,18 @@ def setup_connection_for_dialect( version or version_string, "SQLite", MIN_VERSION_SQLITE ) - # approximately 8MiB of memory - execute_on_connection(dbapi_connection, "PRAGMA cache_size = -8192") + # The upper bound on the cache size is approximately 16MiB of memory + execute_on_connection(dbapi_connection, "PRAGMA cache_size = -16384") + + # + # Enable FULL synchronous if they have a commit interval of 0 + # or NORMAL if they do not. + # + # https://sqlite.org/pragma.html#pragma_synchronous + # The synchronous=NORMAL setting is a good choice for most applications running in WAL mode. + # + synchronous = "NORMAL" if instance.commit_interval else "FULL" + execute_on_connection(dbapi_connection, f"PRAGMA synchronous={synchronous}") # enable support for foreign keys execute_on_connection(dbapi_connection, "PRAGMA foreign_keys=ON") diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index c650ec20fb0..fea255647ec 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -234,18 +234,70 @@ def test_setup_connection_for_dialect_sqlite(sqlite_version, db_supports_row_num util.setup_connection_for_dialect(instance_mock, "sqlite", dbapi_connection, True) - assert len(execute_args) == 4 + assert len(execute_args) == 5 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" + assert execute_args[2] == "PRAGMA cache_size = -16384" + assert execute_args[3] == "PRAGMA synchronous=NORMAL" + assert execute_args[4] == "PRAGMA foreign_keys=ON" execute_args = [] util.setup_connection_for_dialect(instance_mock, "sqlite", dbapi_connection, False) - assert len(execute_args) == 2 - assert execute_args[0] == "PRAGMA cache_size = -8192" - assert execute_args[1] == "PRAGMA foreign_keys=ON" + assert len(execute_args) == 3 + assert execute_args[0] == "PRAGMA cache_size = -16384" + assert execute_args[1] == "PRAGMA synchronous=NORMAL" + assert execute_args[2] == "PRAGMA foreign_keys=ON" + + assert instance_mock._db_supports_row_number == db_supports_row_number + + +@pytest.mark.parametrize( + "sqlite_version, db_supports_row_number", + [ + ("3.25.0", True), + ("3.24.0", False), + ], +) +def test_setup_connection_for_dialect_sqlite_zero_commit_interval( + sqlite_version, db_supports_row_number +): + """Test setting up the connection for a sqlite dialect with a zero commit interval.""" + instance_mock = MagicMock(_db_supports_row_number=True, commit_interval=0) + 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 len(execute_args) == 5 + assert execute_args[0] == "PRAGMA journal_mode=WAL" + assert execute_args[1] == "SELECT sqlite_version()" + assert execute_args[2] == "PRAGMA cache_size = -16384" + assert execute_args[3] == "PRAGMA synchronous=FULL" + assert execute_args[4] == "PRAGMA foreign_keys=ON" + + execute_args = [] + util.setup_connection_for_dialect(instance_mock, "sqlite", dbapi_connection, False) + + assert len(execute_args) == 3 + assert execute_args[0] == "PRAGMA cache_size = -16384" + assert execute_args[1] == "PRAGMA synchronous=FULL" + assert execute_args[2] == "PRAGMA foreign_keys=ON" assert instance_mock._db_supports_row_number == db_supports_row_number From 1d6659224f49f6dcc7b8f1a7d06766620a8118a4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 18 May 2022 00:22:07 +0000 Subject: [PATCH 0596/3516] [ci skip] Translation update --- .../aladdin_connect/translations/id.json | 27 ++++++++++++ .../aladdin_connect/translations/ko.json | 27 ++++++++++++ .../components/baf/translations/id.json | 23 ++++++++++ .../components/baf/translations/ko.json | 10 +++++ .../components/fan/translations/ko.json | 1 + .../geocaching/translations/bg.json | 3 ++ .../geocaching/translations/ko.json | 25 +++++++++++ .../components/group/translations/de.json | 4 +- .../components/knx/translations/en.json | 2 +- .../components/knx/translations/et.json | 2 +- .../components/knx/translations/id.json | 2 +- .../components/knx/translations/it.json | 2 +- .../components/knx/translations/tr.json | 2 +- .../components/knx/translations/zh-Hant.json | 2 +- .../litterrobot/translations/sensor.id.json | 24 +++++++++++ .../litterrobot/translations/sensor.ko.json | 24 +++++++++++ .../components/meater/translations/ko.json | 11 +++++ .../components/onewire/translations/ko.json | 2 + .../components/recorder/translations/ko.json | 7 +++ .../components/siren/translations/hu.json | 3 ++ .../components/siren/translations/id.json | 3 ++ .../siren/translations/zh-Hant.json | 3 ++ .../components/slack/translations/id.json | 25 +++++++++++ .../components/slack/translations/ko.json | 20 +++++++++ .../components/sms/translations/ko.json | 1 + .../ukraine_alarm/translations/ko.json | 43 +++++++++++++++++++ .../components/vulcan/translations/ko.json | 12 ++++++ .../components/ws66i/translations/ko.json | 34 +++++++++++++++ .../components/yolink/translations/bg.json | 20 +++++++++ .../components/yolink/translations/et.json | 25 +++++++++++ .../components/yolink/translations/fr.json | 25 +++++++++++ .../components/yolink/translations/hu.json | 25 +++++++++++ .../components/yolink/translations/id.json | 24 +++++++++++ .../components/yolink/translations/it.json | 25 +++++++++++ .../components/yolink/translations/ja.json | 25 +++++++++++ .../components/yolink/translations/ko.json | 23 ++++++++++ .../components/yolink/translations/nl.json | 25 +++++++++++ .../components/yolink/translations/pt-BR.json | 25 +++++++++++ .../components/yolink/translations/tr.json | 25 +++++++++++ .../yolink/translations/zh-Hant.json | 25 +++++++++++ 40 files changed, 628 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/aladdin_connect/translations/id.json create mode 100644 homeassistant/components/aladdin_connect/translations/ko.json create mode 100644 homeassistant/components/baf/translations/id.json create mode 100644 homeassistant/components/geocaching/translations/ko.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.id.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.ko.json create mode 100644 homeassistant/components/meater/translations/ko.json create mode 100644 homeassistant/components/recorder/translations/ko.json create mode 100644 homeassistant/components/siren/translations/hu.json create mode 100644 homeassistant/components/siren/translations/id.json create mode 100644 homeassistant/components/siren/translations/zh-Hant.json create mode 100644 homeassistant/components/slack/translations/id.json create mode 100644 homeassistant/components/ukraine_alarm/translations/ko.json create mode 100644 homeassistant/components/vulcan/translations/ko.json create mode 100644 homeassistant/components/ws66i/translations/ko.json create mode 100644 homeassistant/components/yolink/translations/bg.json create mode 100644 homeassistant/components/yolink/translations/et.json create mode 100644 homeassistant/components/yolink/translations/fr.json create mode 100644 homeassistant/components/yolink/translations/hu.json create mode 100644 homeassistant/components/yolink/translations/id.json create mode 100644 homeassistant/components/yolink/translations/it.json create mode 100644 homeassistant/components/yolink/translations/ja.json create mode 100644 homeassistant/components/yolink/translations/ko.json create mode 100644 homeassistant/components/yolink/translations/nl.json create mode 100644 homeassistant/components/yolink/translations/pt-BR.json create mode 100644 homeassistant/components/yolink/translations/tr.json create mode 100644 homeassistant/components/yolink/translations/zh-Hant.json diff --git a/homeassistant/components/aladdin_connect/translations/id.json b/homeassistant/components/aladdin_connect/translations/id.json new file mode 100644 index 00000000000..37a42d096cf --- /dev/null +++ b/homeassistant/components/aladdin_connect/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" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Integrasi Aladdin Connect perlu mengautentikasi ulang akun Anda", + "title": "Autentikasi Ulang Integrasi" + }, + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/ko.json b/homeassistant/components/aladdin_connect/translations/ko.json new file mode 100644 index 00000000000..d2e9a9e1375 --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/ko.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638" + }, + "description": "Aladdin Connect \ud1b5\ud569\uad6c\uc131\uc694\uc18c\ub294 \uacc4\uc815\uc744 \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4.", + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" + }, + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/id.json b/homeassistant/components/baf/translations/id.json new file mode 100644 index 00000000000..116742e613c --- /dev/null +++ b/homeassistant/components/baf/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "ipv6_not_supported": "IPv6 tidak didukung." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "{name} - {model} ({ip_address})", + "step": { + "discovery_confirm": { + "description": "Ingin menyiapkan {name} - {model} ({ip_address})?" + }, + "user": { + "data": { + "ip_address": "Alamat IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/ko.json b/homeassistant/components/baf/translations/ko.json index a8884c0403e..9bf0efc5136 100644 --- a/homeassistant/components/baf/translations/ko.json +++ b/homeassistant/components/baf/translations/ko.json @@ -1,12 +1,22 @@ { "config": { "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "ipv6_not_supported": "IPv6\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, "flow_title": "{name} - {model} ( {ip_address} )", "step": { "discovery_confirm": { "description": "{name} - {model} ({ip_address}) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + }, + "user": { + "data": { + "ip_address": "IP \uc8fc\uc18c" + } } } } diff --git a/homeassistant/components/fan/translations/ko.json b/homeassistant/components/fan/translations/ko.json index c2157f29e72..6616d7a39f4 100644 --- a/homeassistant/components/fan/translations/ko.json +++ b/homeassistant/components/fan/translations/ko.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "{entity_name} \ud1a0\uae00", "turn_off": "{entity_name}\uc744(\ub97c) \ub044\uae30", "turn_on": "{entity_name}\uc744(\ub97c) \ucf1c\uae30" }, diff --git a/homeassistant/components/geocaching/translations/bg.json b/homeassistant/components/geocaching/translations/bg.json index 00c237dd077..d3ca579dff7 100644 --- a/homeassistant/components/geocaching/translations/bg.json +++ b/homeassistant/components/geocaching/translations/bg.json @@ -9,6 +9,9 @@ "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0435\u043d\u043e" }, "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": "\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/geocaching/translations/ko.json b/homeassistant/components/geocaching/translations/ko.json new file mode 100644 index 00000000000..0d46ef64d4b --- /dev/null +++ b/homeassistant/components/geocaching/translations/ko.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \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", + "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", + "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "oauth_error": "\uc798\ubabb\ub41c \ud1a0\ud070 \ub370\uc774\ud130\ub97c \ubc1b\uc558\uc2b5\ub2c8\ub2e4.", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "create_entry": { + "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "pick_implementation": { + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" + }, + "reauth_confirm": { + "description": "Geocaching \ud1b5\ud569\uad6c\uc131\uc694\uc18c\ub294 \uacc4\uc815\uc744 \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4", + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/de.json b/homeassistant/components/group/translations/de.json index aeaedd3a7cc..c76c92ccb19 100644 --- a/homeassistant/components/group/translations/de.json +++ b/homeassistant/components/group/translations/de.json @@ -108,9 +108,9 @@ "cover": "Abdeckung-Gruppe", "fan": "L\u00fcfter-Gruppe", "light": "Licht-Gruppe", - "lock": "Gruppe sperren", + "lock": "Schloss-Gruppe", "media_player": "Media-Player-Gruppe", - "switch": "Gruppe wechseln" + "switch": "Schalter-Gruppe" }, "title": "Gruppe hinzuf\u00fcgen" } diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index ba073eba888..fd8ac9499f1 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -104,7 +104,7 @@ "multicast_group": "Used for routing and discovery. Default: `224.0.23.12`", "multicast_port": "Used for routing and discovery. Default: `3671`", "rate_limit": "Maximum outgoing telegrams per second.\nRecommended: 20 to 40", - "state_updater": "Globally enable or disable reading states from the KNX Bus. When disabled, Home Assistant will not actively retrieve states from the KNX Bus, `sync_state` entity options will have no effect." + "state_updater": "Set default for reading states from the KNX Bus. When disabled, Home Assistant will not actively retrieve entity states from the KNX Bus. Can be overridden by `sync_state` entity options." } }, "tunnel": { diff --git a/homeassistant/components/knx/translations/et.json b/homeassistant/components/knx/translations/et.json index c4410d490e4..b7cd079ad3f 100644 --- a/homeassistant/components/knx/translations/et.json +++ b/homeassistant/components/knx/translations/et.json @@ -104,7 +104,7 @@ "multicast_group": "Kasutatakse marsruutimiseks ja avastamiseks. Vaikimisi: \"224.0.23.12\"", "multicast_port": "Kasutatakse marsruutimiseks ja avastamiseks. Vaikev\u00e4\u00e4rtus: \"3671\"", "rate_limit": "Maksimaalne v\u00e4ljaminevate telegrammide arv sekundis.\nSoovitatav: 20 kuni 40", - "state_updater": "KNX siini lugemisolekute globaalne lubamine v\u00f5i keelamine. Kui see on keelatud, ei too Home Assistant aktiivselt olekuid KNX siinilt, olemi s\u00fcnkroonimisoleku valikudi ei m\u00f5juta." + "state_updater": "M\u00e4\u00e4ra KNX siini olekute lugemise vaikev\u00e4\u00e4rtused. Kui see on keelatud, ei too Home Assistant aktiivselt olemi olekuid KNX siinilt. Saab alistada olemivalikute s\u00fcnkroonimise_olekuga." } }, "tunnel": { diff --git a/homeassistant/components/knx/translations/id.json b/homeassistant/components/knx/translations/id.json index 92d812c1b1b..e2ca98fff94 100644 --- a/homeassistant/components/knx/translations/id.json +++ b/homeassistant/components/knx/translations/id.json @@ -104,7 +104,7 @@ "multicast_group": "Digunakan untuk perutean dan penemuan. Bawaan: `224.0.23.12`", "multicast_port": "Digunakan untuk perutean dan penemuan. Bawaan: `3671`", "rate_limit": "Telegram keluar maksimum per detik.\nDirekomendasikan: 20 hingga 40", - "state_updater": "Secara global mengaktifkan atau menonaktifkan status pembacaan dari KNX Bus. Saat dinonaktifkan, Home Assistant tidak akan secara aktif mengambil status dari KNX Bus, opsi entitas 'sync_state' tidak akan berpengaruh." + "state_updater": "Menyetel default untuk status pembacaan KNX Bus. Saat dinonaktifkan, Home Assistant tidak akan secara aktif mengambil status entitas dari KNX Bus. Hal ini bisa ditimpa dengan opsi entitas 'sync_state'." } }, "tunnel": { diff --git a/homeassistant/components/knx/translations/it.json b/homeassistant/components/knx/translations/it.json index e47d93a27a5..ac780ea96ab 100644 --- a/homeassistant/components/knx/translations/it.json +++ b/homeassistant/components/knx/translations/it.json @@ -104,7 +104,7 @@ "multicast_group": "Utilizzato per l'instradamento e il rilevamento. Predefinito: `224.0.23.12`", "multicast_port": "Utilizzato per l'instradamento e il rilevamento. Predefinito: `3671`", "rate_limit": "Numero massimo di telegrammi in uscita al secondo.\n Consigliato: da 20 a 40", - "state_updater": "Abilita o disabilita globalmente gli stati di lettura dal bus KNX. Se disabilitato, Home Assistant non recuperer\u00e0 attivamente gli stati dal bus KNX, le opzioni dell'entit\u00e0 `sync_state` non avranno alcun effetto." + "state_updater": "Impostazione predefinita per la lettura degli stati dal bus KNX. Se disabilitata Home Assistant non recuperer\u00e0 attivamente gli stati delle entit\u00e0 dal bus KNX. Pu\u00f2 essere sovrascritta dalle opzioni dell'entit\u00e0 `sync_state`." } }, "tunnel": { diff --git a/homeassistant/components/knx/translations/tr.json b/homeassistant/components/knx/translations/tr.json index c10f35ca48f..5a2863baaf4 100644 --- a/homeassistant/components/knx/translations/tr.json +++ b/homeassistant/components/knx/translations/tr.json @@ -104,7 +104,7 @@ "multicast_group": "Y\u00f6nlendirme ve ke\u015fif i\u00e7in kullan\u0131l\u0131r. Varsay\u0131lan: \"224.0.23.12\"", "multicast_port": "Y\u00f6nlendirme ve ke\u015fif i\u00e7in kullan\u0131l\u0131r. Varsay\u0131lan: \"3671\"", "rate_limit": "Saniyede maksimum giden telegram say\u0131s\u0131.\n \u00d6nerilen: 20 ila 40", - "state_updater": "KNX Bus'tan okuma durumlar\u0131n\u0131 k\u00fcresel olarak etkinle\u015ftirin veya devre d\u0131\u015f\u0131 b\u0131rak\u0131n. Devre d\u0131\u015f\u0131 b\u0131rak\u0131ld\u0131\u011f\u0131nda, Home Assistant KNX Bus'tan durumlar\u0131 aktif olarak almaz, 'sync_state' varl\u0131k se\u00e7eneklerinin hi\u00e7bir etkisi olmaz." + "state_updater": "KNX Bus'tan okuma durumlar\u0131 i\u00e7in varsay\u0131lan\u0131 ayarlay\u0131n. Devre d\u0131\u015f\u0131 b\u0131rak\u0131ld\u0131\u011f\u0131nda, Home Assistant varl\u0131k durumlar\u0131n\u0131 KNX Bus'tan aktif olarak almaz. 'sync_state' varl\u0131k se\u00e7enekleri taraf\u0131ndan ge\u00e7ersiz k\u0131l\u0131nabilir." } }, "tunnel": { diff --git a/homeassistant/components/knx/translations/zh-Hant.json b/homeassistant/components/knx/translations/zh-Hant.json index 8f740b85f6d..613cf4f74e5 100644 --- a/homeassistant/components/knx/translations/zh-Hant.json +++ b/homeassistant/components/knx/translations/zh-Hant.json @@ -104,7 +104,7 @@ "multicast_group": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u81ea\u52d5\u641c\u7d22\u3002\u9810\u8a2d\u503c\uff1a`224.0.23.12`", "multicast_port": "\u4f7f\u7528\u65bc\u8def\u7531\u8207\u81ea\u52d5\u641c\u7d22\u3002\u9810\u8a2d\u503c\uff1a`3671`", "rate_limit": "\u6bcf\u79d2\u6700\u5927 Telegram \u767c\u9001\u91cf\u3002\u5efa\u8b70\uff1a20 - 40", - "state_updater": "\u5168\u5c40\u958b\u555f\u6216\u95dc\u9589\u81ea KNX Bus \u8b80\u53d6\u72c0\u614b\u3002\u7576\u95dc\u9589\u6642\u3001Home Assistant \u5c07\u4e0d\u6703\u4e3b\u52d5\u5f9e KNX Bus \u7372\u53d6\u72c0\u614b\uff0c`sync_state` \u5be6\u9ad4\u9078\u9805\u5c07\u4e0d\u5177\u6548\u679c\u3002" + "state_updater": "\u8a2d\u5b9a\u9810\u8a2d KNX Bus \u8b80\u53d6\u72c0\u614b\u3002\u7576\u95dc\u9589\u6642\u3001Home Assistant \u5c07\u4e0d\u6703\u4e3b\u52d5\u5f9e KNX Bus \u7372\u53d6\u5be6\u9ad4\u72c0\u614b\uff0c\u53ef\u88ab`sync_state` \u5be6\u9ad4\u9078\u9805\u8986\u84cb\u3002" } }, "tunnel": { diff --git a/homeassistant/components/litterrobot/translations/sensor.id.json b/homeassistant/components/litterrobot/translations/sensor.id.json new file mode 100644 index 00000000000..20f76ca4322 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.id.json @@ -0,0 +1,24 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "Bonnet Dihapus", + "ccc": "Siklus Bersih Selesai", + "ccp": "Siklus Bersih Sedang Berlangsung", + "csf": "Kesalahan Sensor Kucing", + "csi": "Sensor Kucing Terganggu", + "cst": "Waktu Sensor Kucing", + "df1": "Laci Hampir Penuh - Tersisa 2 Siklus", + "df2": "Laci Hampir Penuh - Tersisa 1 Siklus", + "dfs": "Laci Penuh", + "ec": "Siklus Kosong", + "hpf": "Kesalahan Posisi Rumah", + "off": "Mati", + "offline": "Luring", + "otf": "Kesalahan Torsi Berlebih", + "p": "Jeda", + "rdy": "Siap", + "scf": "Kesalahan Sensor Kucing Saat Mulai", + "sdf": "Laci Penuh Saat Memulai" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.ko.json b/homeassistant/components/litterrobot/translations/sensor.ko.json new file mode 100644 index 00000000000..c9c452b4275 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.ko.json @@ -0,0 +1,24 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "\ubcf4\ub137 \uc81c\uac70\ub428", + "ccc": "\uccad\uc18c \uc644\ub8cc", + "ccp": "\uccad\uc18c \uc911", + "csf": "\ucea3\uc13c\uc11c \uc624\ub958", + "csi": "\ucea3\uc13c\uc11c \uc911\uc9c0", + "df1": "\ubcf4\uad00\ud568 \uac70\uc758 \ucc38 - 2\uc0ac\uc774\ud074 \ub0a8\uc74c", + "df2": "\ubcf4\uad00\ud568 \uac70\uc758 \ucc38 - 1\uc0ac\uc774\ud074 \ub0a8\uc74c", + "dfs": "\ubcf4\uad00\ud568 \uac00\ub4dd \ucc38", + "ec": "\ube44\uc6c0 \uc8fc\uae30", + "off": "\uaebc\uc9d0", + "offline": "\uc624\ud504\ub77c\uc778", + "otf": "\ud1a0\ud06c \uc624\ub958 \uc774\uc0c1", + "p": "\uc77c\uc2dc\uc911\uc9c0", + "pd": "\ud540\uce58 \uac10\uc9c0", + "rdy": "\uc900\ube44", + "scf": "\ucea3\uc13c\uc11c \uc624\ub958", + "sdf": "\ubcf4\uad00\ud568 \uac00\ub4dd \ucc38", + "spf": "\ud540\uce58 \uac10\uc9c0" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/ko.json b/homeassistant/components/meater/translations/ko.json new file mode 100644 index 00000000000..b7fdeff3c2a --- /dev/null +++ b/homeassistant/components/meater/translations/ko.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/ko.json b/homeassistant/components/onewire/translations/ko.json index 038be16108a..e6665d83768 100644 --- a/homeassistant/components/onewire/translations/ko.json +++ b/homeassistant/components/onewire/translations/ko.json @@ -17,6 +17,8 @@ }, "user": { "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8", "type": "\uc5f0\uacb0 \uc720\ud615" }, "title": "1-Wire \uc124\uc815\ud558\uae30" diff --git a/homeassistant/components/recorder/translations/ko.json b/homeassistant/components/recorder/translations/ko.json new file mode 100644 index 00000000000..b8cd6320a55 --- /dev/null +++ b/homeassistant/components/recorder/translations/ko.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "estimated_db_size": "\uc608\uc0c1 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \ud06c\uae30(MiB)" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/siren/translations/hu.json b/homeassistant/components/siren/translations/hu.json new file mode 100644 index 00000000000..64642eb828d --- /dev/null +++ b/homeassistant/components/siren/translations/hu.json @@ -0,0 +1,3 @@ +{ + "title": "Szir\u00e9na" +} \ No newline at end of file diff --git a/homeassistant/components/siren/translations/id.json b/homeassistant/components/siren/translations/id.json new file mode 100644 index 00000000000..bcea16c56f1 --- /dev/null +++ b/homeassistant/components/siren/translations/id.json @@ -0,0 +1,3 @@ +{ + "title": "Sirene" +} \ No newline at end of file diff --git a/homeassistant/components/siren/translations/zh-Hant.json b/homeassistant/components/siren/translations/zh-Hant.json new file mode 100644 index 00000000000..549bad8914b --- /dev/null +++ b/homeassistant/components/siren/translations/zh-Hant.json @@ -0,0 +1,3 @@ +{ + "title": "Siren" +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/id.json b/homeassistant/components/slack/translations/id.json new file mode 100644 index 00000000000..3180294b97e --- /dev/null +++ b/homeassistant/components/slack/translations/id.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "icon": "Ikon", + "username": "Nama Pengguna" + }, + "data_description": { + "api_key": "Token API Slack yang digunakan untuk mengirim pesan Slack." + }, + "description": "Lihat dokumentasi tentang mendapatkan kunci API Slack Anda." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/ko.json b/homeassistant/components/slack/translations/ko.json index ffc01a9bbcb..3e0a8596029 100644 --- a/homeassistant/components/slack/translations/ko.json +++ b/homeassistant/components/slack/translations/ko.json @@ -1,7 +1,27 @@ { "config": { + "abort": { + "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, "step": { "user": { + "data": { + "api_key": "API \ud0a4", + "default_channel": "\uae30\ubcf8 \ucc44\ub110", + "icon": "\uc544\uc774\ucf58", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "data_description": { + "api_key": "Slack \uba54\uc2dc\uc9c0\ub97c \ubcf4\ub0b4\ub294 \ub370 \uc0ac\uc6a9\ud560 Slack API \ud1a0\ud070\uc785\ub2c8\ub2e4.", + "default_channel": "\uba54\uc2dc\uc9c0\ub97c \ubcf4\ub0bc \ub54c \ucc44\ub110\uc774 \uc9c0\uc815\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \uc0ac\uc6a9\ud560 \ucc44\ub110\uc785\ub2c8\ub2e4.", + "icon": "Slack \uc774\ubaa8\ud2f0\ucf58\uc744 \uc0ac\uc6a9\uc790 \uc774\ub984\uc758 \uc544\uc774\ucf58\uc73c\ub85c \uc0ac\uc6a9\ud569\ub2c8\ub2e4.", + "username": "Home Assistant\ub294 \uc0ac\uc6a9\uc790 \uc774\ub984\uc73c\ub85c Slack\uc5d0 \uac8c\uc2dc\ud569\ub2c8\ub2e4." + }, "description": "Slack API \ud0a4\ub97c \uac00\uc838\uc624\ub294 \ubc29\ubc95\uc5d0 \ub300\ud55c \uc124\uba85\uc11c\ub97c \ucc38\uc870\ud558\uc2ed\uc2dc\uc624." } } diff --git a/homeassistant/components/sms/translations/ko.json b/homeassistant/components/sms/translations/ko.json index 5ead95c1a27..aa954a52e19 100644 --- a/homeassistant/components/sms/translations/ko.json +++ b/homeassistant/components/sms/translations/ko.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "baud_speed": "\uc804\uc1a1 \uc18d\ub3c4", "device": "\uae30\uae30" }, "title": "\ubaa8\ub380\uc5d0 \uc5f0\uacb0\ud558\uae30" diff --git a/homeassistant/components/ukraine_alarm/translations/ko.json b/homeassistant/components/ukraine_alarm/translations/ko.json new file mode 100644 index 00000000000..ab66909f96a --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/ko.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "max_regions": "\ucd5c\ub300 5\uac1c \uc9c0\uc5ed\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "rate_limit": "\uc694\uccad\uc774 \ub108\ubb34 \ub9ce\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "community": { + "data": { + "region": "\uc9c0\uc5ed" + }, + "description": "\uc8fc \ubc0f \uc9c0\uc5ed\ubfd0\ub9cc \uc544\ub2c8\ub77c \ud574\ub2f9 \uc9c0\uc5ed\uc758 \ud2b9\uc815 \ucee4\ubba4\ub2c8\ud2f0\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud558\ub824\uba74 \ud574\ub2f9 \ucee4\ubba4\ub2c8\ud2f0\ub97c \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624" + }, + "district": { + "data": { + "region": "\uc9c0\uc5ed" + }, + "description": "\uc8fc\ubfd0\ub9cc \uc544\ub2c8\ub77c \ud2b9\uc815 \uc9c0\uc5ed\uc744 \ubaa8\ub2c8\ud130\ub9c1\ud558\ub824\uba74 \ud574\ub2f9 \uc9c0\uc5ed\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624" + }, + "state": { + "data": { + "region": "\uc9c0\uc5ed" + }, + "description": "\ubaa8\ub2c8\ud130\ub9c1\ud560 \uc8fc \uc120\ud0dd" + }, + "user": { + "data": { + "api_key": "API \ud0a4", + "region": "\uc9c0\uc5ed" + }, + "description": "\ubaa8\ub2c8\ud130\ub9c1\ud560 \uc8fc \uc120\ud0dd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/ko.json b/homeassistant/components/vulcan/translations/ko.json new file mode 100644 index 00000000000..c98f79dd84b --- /dev/null +++ b/homeassistant/components/vulcan/translations/ko.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "token": "\ud1a0\ud070" + }, + "description": "\ubaa8\ubc14\uc77c \uc571\uc73c\ub85c Vulcan \uacc4\uc815\uc5d0 \ub85c\uadf8\uc778\ud569\ub2c8\ub2e4." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/ko.json b/homeassistant/components/ws66i/translations/ko.json new file mode 100644 index 00000000000..ba31d74c21a --- /dev/null +++ b/homeassistant/components/ws66i/translations/ko.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \uc8fc\uc18c" + }, + "title": "\uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uae30" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "\uc18c\uc2a4 \uc774\ub984 #1", + "source_2": "\uc18c\uc2a4 \uc774\ub984 #2", + "source_3": "\uc18c\uc2a4 \uc774\ub984 #3", + "source_4": "\uc18c\uc2a4 \uc774\ub984 #4", + "source_5": "\uc18c\uc2a4 \uc774\ub984 #5", + "source_6": "\uc18c\uc2a4 \uc774\ub984 #6" + }, + "title": "\uc785\ub825 \uc18c\uc2a4 \uad6c\uc131\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/bg.json b/homeassistant/components/yolink/translations/bg.json new file mode 100644 index 00000000000..5f7e924f493 --- /dev/null +++ b/homeassistant/components/yolink/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", + "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": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "step": { + "pick_implementation": { + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, + "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/yolink/translations/et.json b/homeassistant/components/yolink/translations/et.json new file mode 100644 index 00000000000..0428b9229ad --- /dev/null +++ b/homeassistant/components/yolink/translations/et.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp", + "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.", + "no_url_available": "URL pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [spikrijaotis]({docs_url})", + "oauth_error": "Saadi sobimatud loaandmed.", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "create_entry": { + "default": "Tuvastamine \u00f5nnestus" + }, + "step": { + "pick_implementation": { + "title": "Vali tuvastusmeetod" + }, + "reauth_confirm": { + "description": "yolink sidumine peab konto uuesti tuvastama.", + "title": "Taastuvasta sidumine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/fr.json b/homeassistant/components/yolink/translations/fr.json new file mode 100644 index 00000000000..57fc1f3bb64 --- /dev/null +++ b/homeassistant/components/yolink/translations/fr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", + "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", + "oauth_error": "Des donn\u00e9es de jeton non valides ont \u00e9t\u00e9 re\u00e7ues.", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "create_entry": { + "default": "Authentification r\u00e9ussie" + }, + "step": { + "pick_implementation": { + "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + }, + "reauth_confirm": { + "description": "L'int\u00e9gration yolink doit r\u00e9-authentifier votre compte", + "title": "R\u00e9-authentifier l'int\u00e9gration" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/hu.json b/homeassistant/components/yolink/translations/hu.json new file mode 100644 index 00000000000..fc0d5809e79 --- /dev/null +++ b/homeassistant/components/yolink/translations/hu.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "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\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", + "oauth_error": "\u00c9rv\u00e9nytelen token adatok \u00e9rkeztek.", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + }, + "create_entry": { + "default": "Sikeres hiteles\u00edt\u00e9s" + }, + "step": { + "pick_implementation": { + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" + }, + "reauth_confirm": { + "description": "A yolink integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie a fi\u00f3kj\u00e1t", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/id.json b/homeassistant/components/yolink/translations/id.json new file mode 100644 index 00000000000..e2c5ce51b91 --- /dev/null +++ b/homeassistant/components/yolink/translations/id.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", + "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})", + "oauth_error": "Menerima respons token yang tidak valid.", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "create_entry": { + "default": "Berhasil diautentikasi" + }, + "step": { + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + }, + "reauth_confirm": { + "title": "Autentikasi Ulang Integrasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/it.json b/homeassistant/components/yolink/translations/it.json new file mode 100644 index 00000000000..343755c2171 --- /dev/null +++ b/homeassistant/components/yolink/translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", + "oauth_error": "Ricevuti dati token non validi.", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "create_entry": { + "default": "Autenticazione riuscita" + }, + "step": { + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + }, + "reauth_confirm": { + "description": "L'integrazione yolink deve autenticare nuovamente il tuo account", + "title": "Autentica nuovamente l'integrazione" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/ja.json b/homeassistant/components/yolink/translations/ja.json new file mode 100644 index 00000000000..7d2545803bf --- /dev/null +++ b/homeassistant/components/yolink/translations/ja.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", + "oauth_error": "\u7121\u52b9\u306a\u30c8\u30fc\u30af\u30f3\u30c7\u30fc\u30bf\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f\u3002", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "create_entry": { + "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" + }, + "step": { + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + }, + "reauth_confirm": { + "description": "Yolink\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/ko.json b/homeassistant/components/yolink/translations/ko.json new file mode 100644 index 00000000000..6ead9fca594 --- /dev/null +++ b/homeassistant/components/yolink/translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \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", + "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "create_entry": { + "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "pick_implementation": { + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" + }, + "reauth_confirm": { + "description": "yolink \ud1b5\ud569\uad6c\uc131\uc694\uc18c\ub294 \uacc4\uc815\uc744 \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4.", + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/nl.json b/homeassistant/components/yolink/translations/nl.json new file mode 100644 index 00000000000..ae9739b3bd6 --- /dev/null +++ b/homeassistant/components/yolink/translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "already_in_progress": "De configuratiestroom is al aan de gang", + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", + "oauth_error": "Ongeldige token data ontvangen.", + "reauth_successful": "Herauthenticatie was succesvol" + }, + "create_entry": { + "default": "Succesvol geauthenticeerd" + }, + "step": { + "pick_implementation": { + "title": "Kies een authenticatie methode" + }, + "reauth_confirm": { + "description": "De yolink-integratie moet opnieuw inloggen bij uw account", + "title": "Verifieer de integratie opnieuw" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/pt-BR.json b/homeassistant/components/yolink/translations/pt-BR.json new file mode 100644 index 00000000000..31a69f7ed3f --- /dev/null +++ b/homeassistant/components/yolink/translations/pt-BR.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 est\u00e1 configurada", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_url_available": "Nenhuma URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre este erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})", + "oauth_error": "Dados de token inv\u00e1lidos recebidos.", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "description": "A integra\u00e7\u00e3o do yolink precisa autenticar novamente sua conta", + "title": "Reautenticar Integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/tr.json b/homeassistant/components/yolink/translations/tr.json new file mode 100644 index 00000000000..f75dd975f1c --- /dev/null +++ b/homeassistant/components/yolink/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})", + "oauth_error": "Ge\u00e7ersiz anahtar verileri al\u0131nd\u0131.", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, + "step": { + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + }, + "reauth_confirm": { + "description": "Yollink entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor", + "title": "Entegrasyonu Yeniden Do\u011frula" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/zh-Hant.json b/homeassistant/components/yolink/translations/zh-Hant.json new file mode 100644 index 00000000000..48e9dc10c66 --- /dev/null +++ b/homeassistant/components/yolink/translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", + "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", + "oauth_error": "\u6536\u5230\u7121\u6548\u7684\u6b0a\u6756\u8cc7\u6599\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" + }, + "step": { + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, + "reauth_confirm": { + "description": "Yolink \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + } + } + } +} \ No newline at end of file From ec01e001848fbcaff405d1847b3e2c3767a37e85 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 17 May 2022 23:10:28 -0500 Subject: [PATCH 0597/3516] Small cleanup to logbook context augmenter (#72043) --- homeassistant/components/logbook/__init__.py | 22 ++++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 549518e875a..4b24e8e5ef5 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -393,7 +393,11 @@ def _humanify( context_lookup: dict[str | None, Row | None] = {None: None} event_cache = EventCache(event_data_cache) context_augmenter = ContextAugmenter( - context_lookup, entity_name_cache, external_events, event_cache + context_lookup, + entity_name_cache, + external_events, + event_cache, + include_entity_name, ) def _keep_row(row: Row, event_type: str) -> bool: @@ -447,7 +451,7 @@ def _humanify( if icon := row.icon or row.old_format_icon: data[LOGBOOK_ENTRY_ICON] = icon - context_augmenter.augment(data, row, context_id, include_entity_name) + context_augmenter.augment(data, row, context_id) yield data elif event_type in external_events: @@ -455,7 +459,7 @@ def _humanify( data = describe_event(event_cache.get(row)) data[LOGBOOK_ENTRY_WHEN] = format_time(row) data[LOGBOOK_ENTRY_DOMAIN] = domain - context_augmenter.augment(data, row, context_id, include_entity_name) + context_augmenter.augment(data, row, context_id) yield data elif event_type == EVENT_LOGBOOK_ENTRY: @@ -475,7 +479,7 @@ def _humanify( LOGBOOK_ENTRY_DOMAIN: entry_domain, LOGBOOK_ENTRY_ENTITY_ID: entry_entity_id, } - context_augmenter.augment(data, row, context_id, include_entity_name) + context_augmenter.augment(data, row, context_id) yield data @@ -558,16 +562,16 @@ class ContextAugmenter: str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] ], event_cache: EventCache, + include_entity_name: bool, ) -> None: """Init the augmenter.""" self.context_lookup = context_lookup self.entity_name_cache = entity_name_cache self.external_events = external_events self.event_cache = event_cache + self.include_entity_name = include_entity_name - def augment( - self, data: dict[str, Any], row: Row, context_id: str, include_entity_name: bool - ) -> None: + def augment(self, data: dict[str, Any], row: Row, context_id: str) -> None: """Augment data from the row and cache.""" if context_user_id := row.context_user_id: data[CONTEXT_USER_ID] = context_user_id @@ -594,7 +598,7 @@ class ContextAugmenter: # State change if context_entity_id := context_row.entity_id: data[CONTEXT_ENTITY_ID] = context_entity_id - if include_entity_name: + if self.include_entity_name: data[CONTEXT_ENTITY_ID_NAME] = self.entity_name_cache.get( context_entity_id, context_row ) @@ -625,7 +629,7 @@ class ContextAugmenter: if not (attr_entity_id := described.get(ATTR_ENTITY_ID)): return data[CONTEXT_ENTITY_ID] = attr_entity_id - if include_entity_name: + if self.include_entity_name: data[CONTEXT_ENTITY_ID_NAME] = self.entity_name_cache.get( attr_entity_id, context_row ) From bcabe5c6e42282f52c35b924823a8eb6f3e0ef4e Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Wed, 18 May 2022 07:27:00 +0200 Subject: [PATCH 0598/3516] Decouple up-down and position inversion for KNX covers (#72012) --- homeassistant/components/knx/cover.py | 1 + homeassistant/components/knx/schema.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index ca18ad4835a..29bd9b4f6a9 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -66,6 +66,7 @@ class KNXCover(KnxEntity, CoverEntity): group_address_position=config.get(CoverSchema.CONF_POSITION_ADDRESS), travel_time_down=config[CoverSchema.CONF_TRAVELLING_TIME_DOWN], travel_time_up=config[CoverSchema.CONF_TRAVELLING_TIME_UP], + invert_updown=config[CoverSchema.CONF_INVERT_UPDOWN], invert_position=config[CoverSchema.CONF_INVERT_POSITION], invert_angle=config[CoverSchema.CONF_INVERT_ANGLE], ) diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index abba1b0c027..c7c1e264975 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -489,6 +489,7 @@ class CoverSchema(KNXPlatformSchema): CONF_ANGLE_STATE_ADDRESS = "angle_state_address" CONF_TRAVELLING_TIME_DOWN = "travelling_time_down" CONF_TRAVELLING_TIME_UP = "travelling_time_up" + CONF_INVERT_UPDOWN = "invert_updown" CONF_INVERT_POSITION = "invert_position" CONF_INVERT_ANGLE = "invert_angle" @@ -521,6 +522,7 @@ class CoverSchema(KNXPlatformSchema): vol.Optional( CONF_TRAVELLING_TIME_UP, default=DEFAULT_TRAVEL_TIME ): cv.positive_float, + vol.Optional(CONF_INVERT_UPDOWN, default=False): cv.boolean, vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean, vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA, From 8eb4a16a0f6336042e5f7010bf5f69c7648849d1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 08:01:28 +0200 Subject: [PATCH 0599/3516] Drop unnecessary async definitions in samsungtv (#72019) * Drop unnecessary async definitions in samsungtv * keep prefix --- homeassistant/components/samsungtv/config_flow.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 02414d3f476..bfa2482f617 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -414,7 +414,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return entry - async def _async_start_discovery_with_mac_address(self) -> None: + @callback + def _async_start_discovery_with_mac_address(self) -> None: """Start discovery.""" assert self._host is not None if (entry := self._async_update_existing_matching_entry()) and entry.unique_id: @@ -491,7 +492,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.debug("Samsung device found via DHCP: %s", discovery_info) self._mac = discovery_info.macaddress self._host = discovery_info.ip - await self._async_start_discovery_with_mac_address() + self._async_start_discovery_with_mac_address() await self._async_set_device_unique_id() self.context["title_placeholders"] = {"device": self._title} return await self.async_step_confirm() @@ -503,7 +504,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.debug("Samsung device found via ZEROCONF: %s", discovery_info) self._mac = format_mac(discovery_info.properties["deviceid"]) self._host = discovery_info.host - await self._async_start_discovery_with_mac_address() + self._async_start_discovery_with_mac_address() await self._async_set_device_unique_id() self.context["title_placeholders"] = {"device": self._title} return await self.async_step_confirm() From 2bb6e4bb877e21f2e49bcf66eddba18b66ff13ed Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 18 May 2022 18:15:15 +1200 Subject: [PATCH 0600/3516] Bump aioesphomeapi to 10.9.0 (#72049) --- 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 72a36076bf4..50334808dbf 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==10.8.2"], + "requirements": ["aioesphomeapi==10.9.0"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index 3fa07679b1a..ad732ab2034 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -144,7 +144,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.8.2 +aioesphomeapi==10.9.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9195dc04ff4..02bf3ea39b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.8.2 +aioesphomeapi==10.9.0 # homeassistant.components.flo aioflo==2021.11.0 From 0dc12c70e38a2262f7c263eacdc56a91304c9bdb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 May 2022 01:47:02 -0500 Subject: [PATCH 0601/3516] Add logbook descriptions for elkm1 keypad press events (#72017) * Add logbook descriptions for elkm1 keypad press events * drop extra block --- homeassistant/components/elkm1/__init__.py | 8 +-- homeassistant/components/elkm1/const.py | 1 + homeassistant/components/elkm1/logbook.py | 39 ++++++++++++++ tests/components/elkm1/test_logbook.py | 63 ++++++++++++++++++++++ 4 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/elkm1/logbook.py create mode 100644 tests/components/elkm1/test_logbook.py diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 22dd50499d6..9a7c7dbc43a 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -42,6 +42,7 @@ from .const import ( ATTR_KEY, ATTR_KEY_NAME, ATTR_KEYPAD_ID, + ATTR_KEYPAD_NAME, CONF_AREA, CONF_AUTO_CONFIGURE, CONF_COUNTER, @@ -266,21 +267,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) elk.connect() - def _element_changed(element: Element, changeset: dict[str, Any]) -> None: + def _keypad_changed(keypad: Element, changeset: dict[str, Any]) -> None: if (keypress := changeset.get("last_keypress")) is None: return hass.bus.async_fire( EVENT_ELKM1_KEYPAD_KEY_PRESSED, { - ATTR_KEYPAD_ID: element.index + 1, + ATTR_KEYPAD_NAME: keypad.name, + ATTR_KEYPAD_ID: keypad.index + 1, ATTR_KEY_NAME: keypress[0], ATTR_KEY: keypress[1], }, ) for keypad in elk.keypads: - keypad.add_callback(_element_changed) + keypad.add_callback(_keypad_changed) try: if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, SYNC_TIMEOUT): diff --git a/homeassistant/components/elkm1/const.py b/homeassistant/components/elkm1/const.py index fd4856bd5d5..a2bb5744c11 100644 --- a/homeassistant/components/elkm1/const.py +++ b/homeassistant/components/elkm1/const.py @@ -43,6 +43,7 @@ EVENT_ELKM1_KEYPAD_KEY_PRESSED = "elkm1.keypad_key_pressed" ATTR_KEYPAD_ID = "keypad_id" ATTR_KEY = "key" ATTR_KEY_NAME = "key_name" +ATTR_KEYPAD_NAME = "keypad_name" ATTR_CHANGED_BY_KEYPAD = "changed_by_keypad" ATTR_CHANGED_BY_ID = "changed_by_id" ATTR_CHANGED_BY_TIME = "changed_by_time" diff --git a/homeassistant/components/elkm1/logbook.py b/homeassistant/components/elkm1/logbook.py new file mode 100644 index 00000000000..01019ce77e0 --- /dev/null +++ b/homeassistant/components/elkm1/logbook.py @@ -0,0 +1,39 @@ +"""Describe elkm1 logbook events.""" +from __future__ import annotations + +from collections.abc import Callable + +from homeassistant.core import Event, HomeAssistant, callback + +from .const import ( + ATTR_KEY, + ATTR_KEY_NAME, + ATTR_KEYPAD_ID, + ATTR_KEYPAD_NAME, + DOMAIN, + EVENT_ELKM1_KEYPAD_KEY_PRESSED, +) + + +@callback +def async_describe_events( + hass: HomeAssistant, + async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None], +) -> None: + """Describe logbook events.""" + + @callback + def async_describe_button_event(event: Event) -> dict[str, str]: + """Describe elkm1 logbook event.""" + data = event.data + keypad_name = data.get( + ATTR_KEYPAD_NAME, data[ATTR_KEYPAD_ID] + ) # added in 2022.6 + return { + "name": f"Elk Keypad {keypad_name}", + "message": f"pressed {data[ATTR_KEY_NAME]} ({data[ATTR_KEY]})", + } + + async_describe_event( + DOMAIN, EVENT_ELKM1_KEYPAD_KEY_PRESSED, async_describe_button_event + ) diff --git a/tests/components/elkm1/test_logbook.py b/tests/components/elkm1/test_logbook.py new file mode 100644 index 00000000000..d6ff9f75d5d --- /dev/null +++ b/tests/components/elkm1/test_logbook.py @@ -0,0 +1,63 @@ +"""The tests for elkm1 logbook.""" +from homeassistant.components.elkm1.const import ( + ATTR_KEY, + ATTR_KEY_NAME, + ATTR_KEYPAD_ID, + ATTR_KEYPAD_NAME, + DOMAIN, + EVENT_ELKM1_KEYPAD_KEY_PRESSED, +) +from homeassistant.const import CONF_HOST +from homeassistant.setup import async_setup_component + +from . import _patch_discovery, _patch_elk + +from tests.common import MockConfigEntry +from tests.components.logbook.common import MockRow, mock_humanify + + +async def test_humanify_elkm1_keypad_event(hass): + """Test humanifying elkm1 keypad presses.""" + hass.config.components.add("recorder") + assert await async_setup_component(hass, "logbook", {}) + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "elks://1.2.3.4"}, + unique_id="aa:bb:cc:dd:ee:ff", + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_elk(): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + (event1, event2) = mock_humanify( + hass, + [ + MockRow( + EVENT_ELKM1_KEYPAD_KEY_PRESSED, + { + ATTR_KEYPAD_ID: 1, + ATTR_KEY_NAME: "four", + ATTR_KEY: "4", + ATTR_KEYPAD_NAME: "Main Bedroom", + }, + ), + MockRow( + EVENT_ELKM1_KEYPAD_KEY_PRESSED, + { + ATTR_KEYPAD_ID: 1, + ATTR_KEY_NAME: "five", + ATTR_KEY: "5", + }, + ), + ], + ) + + assert event1["name"] == "Elk Keypad Main Bedroom" + assert event1["domain"] == DOMAIN + assert event1["message"] == "pressed four (4)" + + assert event2["name"] == "Elk Keypad 1" + assert event2["domain"] == DOMAIN + assert event2["message"] == "pressed five (5)" From c4fc84ec1e77a18ff392b34389baa86d52388246 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 May 2022 01:58:30 -0500 Subject: [PATCH 0602/3516] Add support for selecting device_ids from the logbook (#72039) Co-authored-by: Paulus Schoutsen --- homeassistant/components/logbook/__init__.py | 106 +++- homeassistant/components/logbook/queries.py | 451 ------------------ .../components/logbook/queries/__init__.py | 70 +++ .../components/logbook/queries/all.py | 64 +++ .../components/logbook/queries/common.py | 286 +++++++++++ .../components/logbook/queries/devices.py | 87 ++++ .../components/logbook/queries/entities.py | 124 +++++ .../logbook/queries/entities_and_devices.py | 111 +++++ homeassistant/components/recorder/models.py | 3 + tests/components/logbook/test_init.py | 280 ++++++++++- 10 files changed, 1112 insertions(+), 470 deletions(-) delete mode 100644 homeassistant/components/logbook/queries.py create mode 100644 homeassistant/components/logbook/queries/__init__.py create mode 100644 homeassistant/components/logbook/queries/all.py create mode 100644 homeassistant/components/logbook/queries/common.py create mode 100644 homeassistant/components/logbook/queries/devices.py create mode 100644 homeassistant/components/logbook/queries/entities.py create mode 100644 homeassistant/components/logbook/queries/entities_and_devices.py diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 4b24e8e5ef5..806ba00d2c8 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -50,7 +50,11 @@ from homeassistant.core import ( split_entity_id, ) from homeassistant.exceptions import InvalidEntityFormatError -from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry as er, +) from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, EntityFilter, @@ -64,7 +68,8 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util -from .queries import PSUEDO_EVENT_STATE_CHANGED, statement_for_request +from .queries import statement_for_request +from .queries.common import PSUEDO_EVENT_STATE_CHANGED _LOGGER = logging.getLogger(__name__) @@ -96,8 +101,11 @@ LOGBOOK_ENTRY_STATE = "state" LOGBOOK_ENTRY_WHEN = "when" ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED = {EVENT_LOGBOOK_ENTRY, EVENT_CALL_SERVICE} - -SCRIPT_AUTOMATION_EVENTS = {EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED} +ENTITY_EVENTS_WITHOUT_CONFIG_ENTRY = { + EVENT_LOGBOOK_ENTRY, + EVENT_AUTOMATION_TRIGGERED, + EVENT_SCRIPT_STARTED, +} LOG_MESSAGE_SCHEMA = vol.Schema( { @@ -209,12 +217,61 @@ async def _process_logbook_platform( platform.async_describe_events(hass, _async_describe_event) +def _async_determine_event_types( + hass: HomeAssistant, entity_ids: list[str] | None, device_ids: list[str] | None +) -> tuple[str, ...]: + """Reduce the event types based on the entity ids and device ids.""" + external_events: dict[ + str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] + ] = hass.data.get(DOMAIN, {}) + if not entity_ids and not device_ids: + return (*ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, *external_events) + config_entry_ids: set[str] = set() + intrested_event_types: set[str] = set() + + if entity_ids: + # + # Home Assistant doesn't allow firing events from + # entities so we have a limited list to check + # + # automations and scripts can refer to entities + # but they do not have a config entry so we need + # to add them. + # + # We also allow entity_ids to be recorded via + # manual logbook entries. + # + intrested_event_types |= ENTITY_EVENTS_WITHOUT_CONFIG_ENTRY + + if device_ids: + dev_reg = dr.async_get(hass) + for device_id in device_ids: + if (device := dev_reg.async_get(device_id)) and device.config_entries: + config_entry_ids |= device.config_entries + interested_domains: set[str] = set() + for entry_id in config_entry_ids: + if entry := hass.config_entries.async_get_entry(entry_id): + interested_domains.add(entry.domain) + for external_event, domain_call in external_events.items(): + if domain_call[0] in interested_domains: + intrested_event_types.add(external_event) + + return tuple( + event_type + for event_type in (EVENT_LOGBOOK_ENTRY, *external_events) + if event_type in intrested_event_types + ) + + def _ws_formatted_get_events( hass: HomeAssistant, msg_id: int, start_day: dt, end_day: dt, + event_types: tuple[str, ...], + ent_reg: er.EntityRegistry, entity_ids: list[str] | None = None, + device_ids: list[str] | None = None, filters: Filters | None = None, entities_filter: EntityFilter | Callable[[str], bool] | None = None, context_id: str | None = None, @@ -227,7 +284,10 @@ def _ws_formatted_get_events( hass, start_day, end_day, + event_types, + ent_reg, entity_ids, + device_ids, filters, entities_filter, context_id, @@ -244,6 +304,7 @@ def _ws_formatted_get_events( vol.Required("start_time"): str, vol.Optional("end_time"): str, vol.Optional("entity_ids"): [str], + vol.Optional("device_ids"): [str], vol.Optional("context_id"): str, } ) @@ -274,8 +335,11 @@ async def ws_get_events( connection.send_result(msg["id"], []) return + device_ids = msg.get("device_ids") entity_ids = msg.get("entity_ids") context_id = msg.get("context_id") + event_types = _async_determine_event_types(hass, entity_ids, device_ids) + ent_reg = er.async_get(hass) connection.send_message( await get_instance(hass).async_add_executor_job( @@ -284,7 +348,10 @@ async def ws_get_events( msg["id"], start_time, end_time, + event_types, + ent_reg, entity_ids, + device_ids, hass.data[LOGBOOK_FILTERS], hass.data[LOGBOOK_ENTITIES_FILTER], context_id, @@ -354,6 +421,9 @@ class LogbookView(HomeAssistantView): "Can't combine entity with context_id", HTTPStatus.BAD_REQUEST ) + event_types = _async_determine_event_types(hass, entity_ids, None) + ent_reg = er.async_get(hass) + def json_events() -> web.Response: """Fetch events and generate JSON.""" return self.json( @@ -361,7 +431,10 @@ class LogbookView(HomeAssistantView): hass, start_day, end_day, + event_types, + ent_reg, entity_ids, + None, self.filters, self.entities_filter, context_id, @@ -487,7 +560,10 @@ def _get_events( hass: HomeAssistant, start_day: dt, end_day: dt, + event_types: tuple[str, ...], + ent_reg: er.EntityRegistry, entity_ids: list[str] | None = None, + device_ids: list[str] | None = None, filters: Filters | None = None, entities_filter: EntityFilter | Callable[[str], bool] | None = None, context_id: str | None = None, @@ -496,17 +572,13 @@ def _get_events( ) -> list[dict[str, Any]]: """Get events for a period of time.""" assert not ( - entity_ids and context_id - ), "can't pass in both entity_ids and context_id" - + context_id and (entity_ids or device_ids) + ), "can't pass in both context_id and (entity_ids or device_ids)" external_events: dict[ str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] ] = hass.data.get(DOMAIN, {}) - event_types = (*ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, *external_events) format_time = _row_time_fired_timestamp if timestamp else _row_time_fired_isoformat entity_name_cache = EntityNameCache(hass) - ent_reg = er.async_get(hass) - if entity_ids is not None: entities_filter = generate_filter([], entity_ids, [], []) @@ -529,7 +601,13 @@ def _get_events( return query.yield_per(1024) # type: ignore[no-any-return] stmt = statement_for_request( - start_day, end_day, event_types, entity_ids, filters, context_id + start_day, + end_day, + event_types, + entity_ids, + device_ids, + filters, + context_id, ) if _LOGGER.isEnabledFor(logging.DEBUG): _LOGGER.debug( @@ -668,12 +746,6 @@ def _row_event_data_extract(row: Row, extractor: re.Pattern) -> str | None: return result.group(1) if result else None -def _row_attributes_extract(row: Row, extractor: re.Pattern) -> str | None: - """Extract from attributes row.""" - result = extractor.search(row.shared_attrs or row.attributes or "") - return result.group(1) if result else None - - def _row_time_fired_isoformat(row: Row) -> str: """Convert the row timed_fired to isoformat.""" return process_timestamp_to_utc_isoformat(row.time_fired or dt_util.utcnow()) diff --git a/homeassistant/components/logbook/queries.py b/homeassistant/components/logbook/queries.py deleted file mode 100644 index 6fe20bfc561..00000000000 --- a/homeassistant/components/logbook/queries.py +++ /dev/null @@ -1,451 +0,0 @@ -"""Queries for logbook.""" -from __future__ import annotations - -from collections.abc import Iterable -from datetime import datetime as dt - -import sqlalchemy -from sqlalchemy import JSON, lambda_stmt, select, type_coerce, union_all -from sqlalchemy.orm import Query, aliased -from sqlalchemy.sql.elements import ClauseList -from sqlalchemy.sql.expression import literal -from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import Select - -from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN -from homeassistant.components.recorder.filters import Filters -from homeassistant.components.recorder.models import ( - ENTITY_ID_LAST_UPDATED_INDEX, - JSON_VARIENT_CAST, - LAST_UPDATED_INDEX, - EventData, - Events, - StateAttributes, - States, -) -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN - -ENTITY_ID_JSON_TEMPLATE = '%"entity_id":"{}"%' - -CONTINUOUS_DOMAINS = {PROXIMITY_DOMAIN, SENSOR_DOMAIN} -CONTINUOUS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in CONTINUOUS_DOMAINS] - -UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' -UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%" - -OLD_STATE = aliased(States, name="old_state") - - -SHARED_ATTRS_JSON = type_coerce( - StateAttributes.shared_attrs.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) -) -OLD_FORMAT_ATTRS_JSON = type_coerce( - States.attributes.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) -) - - -PSUEDO_EVENT_STATE_CHANGED = None -# Since we don't store event_types and None -# and we don't store state_changed in events -# we use a NULL for state_changed events -# when we synthesize them from the states table -# since it avoids another column being sent -# in the payload - -EVENT_COLUMNS = ( - Events.event_id.label("event_id"), - Events.event_type.label("event_type"), - Events.event_data.label("event_data"), - Events.time_fired.label("time_fired"), - Events.context_id.label("context_id"), - Events.context_user_id.label("context_user_id"), - Events.context_parent_id.label("context_parent_id"), -) - -STATE_COLUMNS = ( - States.state_id.label("state_id"), - States.state.label("state"), - States.entity_id.label("entity_id"), - SHARED_ATTRS_JSON["icon"].as_string().label("icon"), - OLD_FORMAT_ATTRS_JSON["icon"].as_string().label("old_format_icon"), -) - - -EMPTY_STATE_COLUMNS = ( - literal(value=None, type_=sqlalchemy.String).label("state_id"), - literal(value=None, type_=sqlalchemy.String).label("state"), - literal(value=None, type_=sqlalchemy.String).label("entity_id"), - literal(value=None, type_=sqlalchemy.String).label("icon"), - literal(value=None, type_=sqlalchemy.String).label("old_format_icon"), -) - - -EVENT_ROWS_NO_STATES = ( - *EVENT_COLUMNS, - EventData.shared_data.label("shared_data"), - *EMPTY_STATE_COLUMNS, -) - -# Virtual column to tell logbook if it should avoid processing -# the event as its only used to link contexts -CONTEXT_ONLY = literal("1").label("context_only") -NOT_CONTEXT_ONLY = literal(None).label("context_only") - - -def statement_for_request( - start_day: dt, - end_day: dt, - event_types: tuple[str, ...], - entity_ids: list[str] | None = None, - filters: Filters | None = None, - context_id: str | None = None, -) -> StatementLambdaElement: - """Generate the logbook statement for a logbook request.""" - - # No entities: logbook sends everything for the timeframe - # limited by the context_id and the yaml configured filter - if not entity_ids: - entity_filter = filters.entity_filter() if filters else None - return _all_stmt(start_day, end_day, event_types, entity_filter, context_id) - - # Multiple entities: logbook sends everything for the timeframe for the entities - # - # This is the least efficient query because we use - # like matching which means part of the query has to be built each - # time when the entity_ids are not in the cache - if len(entity_ids) > 1: - return _entities_stmt(start_day, end_day, event_types, entity_ids) - - # Single entity: logbook sends everything for the timeframe for the entity - entity_id = entity_ids[0] - entity_like = ENTITY_ID_JSON_TEMPLATE.format(entity_id) - return _single_entity_stmt(start_day, end_day, event_types, entity_id, entity_like) - - -def _select_events_context_id_subquery( - start_day: dt, - end_day: dt, - event_types: tuple[str, ...], -) -> Select: - """Generate the select for a context_id subquery.""" - return ( - select(Events.context_id) - .where((Events.time_fired > start_day) & (Events.time_fired < end_day)) - .where(Events.event_type.in_(event_types)) - .outerjoin(EventData, (Events.data_id == EventData.data_id)) - ) - - -def _select_entities_context_ids_sub_query( - start_day: dt, - end_day: dt, - event_types: tuple[str, ...], - entity_ids: list[str], -) -> Select: - """Generate a subquery to find context ids for multiple entities.""" - return select( - union_all( - _select_events_context_id_subquery(start_day, end_day, event_types).where( - _apply_event_entity_id_matchers(entity_ids) - ), - _apply_entities_hints(select(States.context_id)) - .filter((States.last_updated > start_day) & (States.last_updated < end_day)) - .where(States.entity_id.in_(entity_ids)), - ).c.context_id - ) - - -def _select_events_context_only() -> Select: - """Generate an events query that mark them as for context_only. - - By marking them as context_only we know they are only for - linking context ids and we can avoid processing them. - """ - return select(*EVENT_ROWS_NO_STATES, CONTEXT_ONLY).outerjoin( - EventData, (Events.data_id == EventData.data_id) - ) - - -def _entities_stmt( - start_day: dt, - end_day: dt, - event_types: tuple[str, ...], - entity_ids: list[str], -) -> StatementLambdaElement: - """Generate a logbook query for multiple entities.""" - stmt = lambda_stmt( - lambda: _select_events_without_states(start_day, end_day, event_types) - ) - stmt = stmt.add_criteria( - lambda s: s.where(_apply_event_entity_id_matchers(entity_ids)).union_all( - _states_query_for_entity_ids(start_day, end_day, entity_ids), - _select_events_context_only().where( - Events.context_id.in_( - _select_entities_context_ids_sub_query( - start_day, - end_day, - event_types, - entity_ids, - ) - ) - ), - ), - # Since _apply_event_entity_id_matchers generates multiple - # like statements we need to use the entity_ids in the - # the cache key since the sql can change based on the - # likes. - track_on=(str(entity_ids),), - ) - stmt += lambda s: s.order_by(Events.time_fired) - return stmt - - -def _select_entity_context_ids_sub_query( - start_day: dt, - end_day: dt, - event_types: tuple[str, ...], - entity_id: str, - entity_id_like: str, -) -> Select: - """Generate a subquery to find context ids for a single entity.""" - return select( - union_all( - _select_events_context_id_subquery(start_day, end_day, event_types).where( - Events.event_data.like(entity_id_like) - | EventData.shared_data.like(entity_id_like) - ), - _apply_entities_hints(select(States.context_id)) - .filter((States.last_updated > start_day) & (States.last_updated < end_day)) - .where(States.entity_id == entity_id), - ).c.context_id - ) - - -def _single_entity_stmt( - start_day: dt, - end_day: dt, - event_types: tuple[str, ...], - entity_id: str, - entity_id_like: str, -) -> StatementLambdaElement: - """Generate a logbook query for a single entity.""" - stmt = lambda_stmt( - lambda: _select_events_without_states(start_day, end_day, event_types) - .where( - Events.event_data.like(entity_id_like) - | EventData.shared_data.like(entity_id_like) - ) - .union_all( - _states_query_for_entity_id(start_day, end_day, entity_id), - _select_events_context_only().where( - Events.context_id.in_( - _select_entity_context_ids_sub_query( - start_day, end_day, event_types, entity_id, entity_id_like - ) - ) - ), - ) - .order_by(Events.time_fired) - ) - return stmt - - -def _all_stmt( - start_day: dt, - end_day: dt, - event_types: tuple[str, ...], - entity_filter: ClauseList | None = None, - context_id: str | None = None, -) -> StatementLambdaElement: - """Generate a logbook query for all entities.""" - stmt = lambda_stmt( - lambda: _select_events_without_states(start_day, end_day, event_types) - ) - if context_id is not None: - # Once all the old `state_changed` events - # are gone from the database remove the - # _legacy_select_events_context_id() - stmt += lambda s: s.where(Events.context_id == context_id).union_all( - _states_query_for_context_id(start_day, end_day, context_id), - _legacy_select_events_context_id(start_day, end_day, context_id), - ) - elif entity_filter is not None: - stmt += lambda s: s.union_all( - _states_query_for_all(start_day, end_day).where(entity_filter) - ) - else: - stmt += lambda s: s.union_all(_states_query_for_all(start_day, end_day)) - stmt += lambda s: s.order_by(Events.time_fired) - return stmt - - -def _legacy_select_events_context_id( - start_day: dt, end_day: dt, context_id: str -) -> Select: - """Generate a legacy events context id select that also joins states.""" - # This can be removed once we no longer have event_ids in the states table - return ( - select( - *EVENT_COLUMNS, - literal(value=None, type_=sqlalchemy.String).label("shared_data"), - *STATE_COLUMNS, - NOT_CONTEXT_ONLY, - ) - .outerjoin(States, (Events.event_id == States.event_id)) - .where( - (States.last_updated == States.last_changed) | States.last_changed.is_(None) - ) - .where(_not_continuous_entity_matcher()) - .outerjoin( - StateAttributes, (States.attributes_id == StateAttributes.attributes_id) - ) - .where((Events.time_fired > start_day) & (Events.time_fired < end_day)) - .where(Events.context_id == context_id) - ) - - -def _select_events_without_states( - start_day: dt, end_day: dt, event_types: tuple[str, ...] -) -> Select: - """Generate an events select that does not join states.""" - return ( - select(*EVENT_ROWS_NO_STATES, NOT_CONTEXT_ONLY) - .where((Events.time_fired > start_day) & (Events.time_fired < end_day)) - .where(Events.event_type.in_(event_types)) - .outerjoin(EventData, (Events.data_id == EventData.data_id)) - ) - - -def _states_query_for_context_id(start_day: dt, end_day: dt, context_id: str) -> Query: - return _apply_states_filters(_select_states(), start_day, end_day).where( - States.context_id == context_id - ) - - -def _states_query_for_entity_id(start_day: dt, end_day: dt, entity_id: str) -> Query: - return _apply_states_filters( - _apply_entities_hints(_select_states()), start_day, end_day - ).where(States.entity_id == entity_id) - - -def _states_query_for_entity_ids( - start_day: dt, end_day: dt, entity_ids: list[str] -) -> Query: - return _apply_states_filters( - _apply_entities_hints(_select_states()), start_day, end_day - ).where(States.entity_id.in_(entity_ids)) - - -def _states_query_for_all(start_day: dt, end_day: dt) -> Query: - return _apply_states_filters(_apply_all_hints(_select_states()), start_day, end_day) - - -def _select_states() -> Select: - """Generate a states select that formats the states table as event rows.""" - return select( - literal(value=None, type_=sqlalchemy.Text).label("event_id"), - # We use PSUEDO_EVENT_STATE_CHANGED aka None for - # state_changed events since it takes up less - # space in the response and every row has to be - # marked with the event_type - literal(value=PSUEDO_EVENT_STATE_CHANGED, type_=sqlalchemy.String).label( - "event_type" - ), - literal(value=None, type_=sqlalchemy.Text).label("event_data"), - States.last_updated.label("time_fired"), - States.context_id.label("context_id"), - States.context_user_id.label("context_user_id"), - States.context_parent_id.label("context_parent_id"), - literal(value=None, type_=sqlalchemy.Text).label("shared_data"), - *STATE_COLUMNS, - NOT_CONTEXT_ONLY, - ) - - -def _apply_all_hints(query: Query) -> Query: - """Force mysql to use the right index on large selects.""" - return query.with_hint( - States, f"FORCE INDEX ({LAST_UPDATED_INDEX})", dialect_name="mysql" - ) - - -def _apply_entities_hints(query: Query) -> Query: - """Force mysql to use the right index on large selects.""" - return query.with_hint( - States, f"FORCE INDEX ({ENTITY_ID_LAST_UPDATED_INDEX})", dialect_name="mysql" - ) - - -def _apply_states_filters(query: Query, start_day: dt, end_day: dt) -> Query: - return ( - query.filter( - (States.last_updated > start_day) & (States.last_updated < end_day) - ) - .outerjoin(OLD_STATE, (States.old_state_id == OLD_STATE.state_id)) - .where(_missing_state_matcher()) - .where(_not_continuous_entity_matcher()) - .where( - (States.last_updated == States.last_changed) | States.last_changed.is_(None) - ) - .outerjoin( - StateAttributes, (States.attributes_id == StateAttributes.attributes_id) - ) - ) - - -def _missing_state_matcher() -> sqlalchemy.and_: - # The below removes state change events that do not have - # and old_state or the old_state is missing (newly added entities) - # or the new_state is missing (removed entities) - return sqlalchemy.and_( - OLD_STATE.state_id.isnot(None), - (States.state != OLD_STATE.state), - States.state.isnot(None), - ) - - -def _not_continuous_entity_matcher() -> sqlalchemy.or_: - """Match non continuous entities.""" - return sqlalchemy.or_( - _not_continuous_domain_matcher(), - sqlalchemy.and_( - _continuous_domain_matcher, _not_uom_attributes_matcher() - ).self_group(), - ) - - -def _not_continuous_domain_matcher() -> sqlalchemy.and_: - """Match not continuous domains.""" - return sqlalchemy.and_( - *[ - ~States.entity_id.like(entity_domain) - for entity_domain in CONTINUOUS_ENTITY_ID_LIKE - ], - ).self_group() - - -def _continuous_domain_matcher() -> sqlalchemy.or_: - """Match continuous domains.""" - return sqlalchemy.or_( - *[ - States.entity_id.like(entity_domain) - for entity_domain in CONTINUOUS_ENTITY_ID_LIKE - ], - ).self_group() - - -def _not_uom_attributes_matcher() -> ClauseList: - """Prefilter ATTR_UNIT_OF_MEASUREMENT as its much faster in sql.""" - return ~StateAttributes.shared_attrs.like( - UNIT_OF_MEASUREMENT_JSON_LIKE - ) | ~States.attributes.like(UNIT_OF_MEASUREMENT_JSON_LIKE) - - -def _apply_event_entity_id_matchers(entity_ids: Iterable[str]) -> sqlalchemy.or_: - """Create matchers for the entity_id in the event_data.""" - ors = [] - for entity_id in entity_ids: - like = ENTITY_ID_JSON_TEMPLATE.format(entity_id) - ors.append(Events.event_data.like(like)) - ors.append(EventData.shared_data.like(like)) - return sqlalchemy.or_(*ors) diff --git a/homeassistant/components/logbook/queries/__init__.py b/homeassistant/components/logbook/queries/__init__.py new file mode 100644 index 00000000000..3672f1e761c --- /dev/null +++ b/homeassistant/components/logbook/queries/__init__.py @@ -0,0 +1,70 @@ +"""Queries for logbook.""" +from __future__ import annotations + +from datetime import datetime as dt + +from sqlalchemy.sql.lambdas import StatementLambdaElement + +from homeassistant.components.recorder.filters import Filters + +from .all import all_stmt +from .devices import devices_stmt +from .entities import entities_stmt +from .entities_and_devices import entities_devices_stmt + + +def statement_for_request( + start_day: dt, + end_day: dt, + event_types: tuple[str, ...], + entity_ids: list[str] | None = None, + device_ids: list[str] | None = None, + filters: Filters | None = None, + context_id: str | None = None, +) -> StatementLambdaElement: + """Generate the logbook statement for a logbook request.""" + + # No entities: logbook sends everything for the timeframe + # limited by the context_id and the yaml configured filter + if not entity_ids and not device_ids: + entity_filter = filters.entity_filter() if filters else None + return all_stmt(start_day, end_day, event_types, entity_filter, context_id) + + # sqlalchemy caches object quoting, the + # json quotable ones must be a different + # object from the non-json ones to prevent + # sqlalchemy from quoting them incorrectly + + # entities and devices: logbook sends everything for the timeframe for the entities and devices + if entity_ids and device_ids: + json_quotable_entity_ids = list(entity_ids) + json_quotable_device_ids = list(device_ids) + return entities_devices_stmt( + start_day, + end_day, + event_types, + entity_ids, + json_quotable_entity_ids, + json_quotable_device_ids, + ) + + # entities: logbook sends everything for the timeframe for the entities + if entity_ids: + json_quotable_entity_ids = list(entity_ids) + return entities_stmt( + start_day, + end_day, + event_types, + entity_ids, + json_quotable_entity_ids, + ) + + # devices: logbook sends everything for the timeframe for the devices + assert device_ids is not None + json_quotable_device_ids = list(device_ids) + return devices_stmt( + start_day, + end_day, + event_types, + json_quotable_device_ids, + ) diff --git a/homeassistant/components/logbook/queries/all.py b/homeassistant/components/logbook/queries/all.py new file mode 100644 index 00000000000..da17c7bddeb --- /dev/null +++ b/homeassistant/components/logbook/queries/all.py @@ -0,0 +1,64 @@ +"""All queries for logbook.""" +from __future__ import annotations + +from datetime import datetime as dt + +from sqlalchemy import lambda_stmt +from sqlalchemy.orm import Query +from sqlalchemy.sql.elements import ClauseList +from sqlalchemy.sql.lambdas import StatementLambdaElement + +from homeassistant.components.recorder.models import LAST_UPDATED_INDEX, Events, States + +from .common import ( + apply_states_filters, + legacy_select_events_context_id, + select_events_without_states, + select_states, +) + + +def all_stmt( + start_day: dt, + end_day: dt, + event_types: tuple[str, ...], + entity_filter: ClauseList | None = None, + context_id: str | None = None, +) -> StatementLambdaElement: + """Generate a logbook query for all entities.""" + stmt = lambda_stmt( + lambda: select_events_without_states(start_day, end_day, event_types) + ) + if context_id is not None: + # Once all the old `state_changed` events + # are gone from the database remove the + # _legacy_select_events_context_id() + stmt += lambda s: s.where(Events.context_id == context_id).union_all( + _states_query_for_context_id(start_day, end_day, context_id), + legacy_select_events_context_id(start_day, end_day, context_id), + ) + elif entity_filter is not None: + stmt += lambda s: s.union_all( + _states_query_for_all(start_day, end_day).where(entity_filter) + ) + else: + stmt += lambda s: s.union_all(_states_query_for_all(start_day, end_day)) + stmt += lambda s: s.order_by(Events.time_fired) + return stmt + + +def _states_query_for_all(start_day: dt, end_day: dt) -> Query: + return apply_states_filters(_apply_all_hints(select_states()), start_day, end_day) + + +def _apply_all_hints(query: Query) -> Query: + """Force mysql to use the right index on large selects.""" + return query.with_hint( + States, f"FORCE INDEX ({LAST_UPDATED_INDEX})", dialect_name="mysql" + ) + + +def _states_query_for_context_id(start_day: dt, end_day: dt, context_id: str) -> Query: + return apply_states_filters(select_states(), start_day, end_day).where( + States.context_id == context_id + ) diff --git a/homeassistant/components/logbook/queries/common.py b/homeassistant/components/logbook/queries/common.py new file mode 100644 index 00000000000..237fde3f653 --- /dev/null +++ b/homeassistant/components/logbook/queries/common.py @@ -0,0 +1,286 @@ +"""Queries for logbook.""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime as dt +import json +from typing import Any + +import sqlalchemy +from sqlalchemy import JSON, select, type_coerce +from sqlalchemy.orm import Query, aliased +from sqlalchemy.sql.elements import ClauseList +from sqlalchemy.sql.expression import literal +from sqlalchemy.sql.selectable import Select + +from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN +from homeassistant.components.recorder.models import ( + JSON_VARIENT_CAST, + JSONB_VARIENT_CAST, + EventData, + Events, + StateAttributes, + States, +) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN + +CONTINUOUS_DOMAINS = {PROXIMITY_DOMAIN, SENSOR_DOMAIN} +CONTINUOUS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in CONTINUOUS_DOMAINS] + +UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' +UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%" + +OLD_STATE = aliased(States, name="old_state") + + +class JSONLiteral(JSON): # type: ignore[misc] + """Teach SA how to literalize json.""" + + def literal_processor(self, dialect: str) -> Callable[[Any], str]: + """Processor to convert a value to JSON.""" + + def process(value: Any) -> str: + """Dump json.""" + return json.dumps(value) + + return process + + +EVENT_DATA_JSON = type_coerce( + EventData.shared_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) +) +OLD_FORMAT_EVENT_DATA_JSON = type_coerce( + Events.event_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) +) + +SHARED_ATTRS_JSON = type_coerce( + StateAttributes.shared_attrs.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) +) +OLD_FORMAT_ATTRS_JSON = type_coerce( + States.attributes.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) +) + + +PSUEDO_EVENT_STATE_CHANGED = None +# Since we don't store event_types and None +# and we don't store state_changed in events +# we use a NULL for state_changed events +# when we synthesize them from the states table +# since it avoids another column being sent +# in the payload + +EVENT_COLUMNS = ( + Events.event_id.label("event_id"), + Events.event_type.label("event_type"), + Events.event_data.label("event_data"), + Events.time_fired.label("time_fired"), + Events.context_id.label("context_id"), + Events.context_user_id.label("context_user_id"), + Events.context_parent_id.label("context_parent_id"), +) + +STATE_COLUMNS = ( + States.state_id.label("state_id"), + States.state.label("state"), + States.entity_id.label("entity_id"), + SHARED_ATTRS_JSON["icon"].as_string().label("icon"), + OLD_FORMAT_ATTRS_JSON["icon"].as_string().label("old_format_icon"), +) + +STATE_CONTEXT_ONLY_COLUMNS = ( + States.state_id.label("state_id"), + States.state.label("state"), + States.entity_id.label("entity_id"), + literal(value=None, type_=sqlalchemy.String).label("icon"), + literal(value=None, type_=sqlalchemy.String).label("old_format_icon"), +) + +EVENT_COLUMNS_FOR_STATE_SELECT = [ + literal(value=None, type_=sqlalchemy.Text).label("event_id"), + # We use PSUEDO_EVENT_STATE_CHANGED aka None for + # state_changed events since it takes up less + # space in the response and every row has to be + # marked with the event_type + literal(value=PSUEDO_EVENT_STATE_CHANGED, type_=sqlalchemy.String).label( + "event_type" + ), + literal(value=None, type_=sqlalchemy.Text).label("event_data"), + States.last_updated.label("time_fired"), + States.context_id.label("context_id"), + States.context_user_id.label("context_user_id"), + States.context_parent_id.label("context_parent_id"), + literal(value=None, type_=sqlalchemy.Text).label("shared_data"), +] + +EMPTY_STATE_COLUMNS = ( + literal(value=None, type_=sqlalchemy.String).label("state_id"), + literal(value=None, type_=sqlalchemy.String).label("state"), + literal(value=None, type_=sqlalchemy.String).label("entity_id"), + literal(value=None, type_=sqlalchemy.String).label("icon"), + literal(value=None, type_=sqlalchemy.String).label("old_format_icon"), +) + + +EVENT_ROWS_NO_STATES = ( + *EVENT_COLUMNS, + EventData.shared_data.label("shared_data"), + *EMPTY_STATE_COLUMNS, +) + +# Virtual column to tell logbook if it should avoid processing +# the event as its only used to link contexts +CONTEXT_ONLY = literal("1").label("context_only") +NOT_CONTEXT_ONLY = literal(None).label("context_only") + + +def select_events_context_id_subquery( + start_day: dt, + end_day: dt, + event_types: tuple[str, ...], +) -> Select: + """Generate the select for a context_id subquery.""" + return ( + select(Events.context_id) + .where((Events.time_fired > start_day) & (Events.time_fired < end_day)) + .where(Events.event_type.in_(event_types)) + .outerjoin(EventData, (Events.data_id == EventData.data_id)) + ) + + +def select_events_context_only() -> Select: + """Generate an events query that mark them as for context_only. + + By marking them as context_only we know they are only for + linking context ids and we can avoid processing them. + """ + return select(*EVENT_ROWS_NO_STATES, CONTEXT_ONLY).outerjoin( + EventData, (Events.data_id == EventData.data_id) + ) + + +def select_states_context_only() -> Select: + """Generate an states query that mark them as for context_only. + + By marking them as context_only we know they are only for + linking context ids and we can avoid processing them. + """ + return select( + *EVENT_COLUMNS_FOR_STATE_SELECT, *STATE_CONTEXT_ONLY_COLUMNS, CONTEXT_ONLY + ) + + +def select_events_without_states( + start_day: dt, end_day: dt, event_types: tuple[str, ...] +) -> Select: + """Generate an events select that does not join states.""" + return ( + select(*EVENT_ROWS_NO_STATES, NOT_CONTEXT_ONLY) + .where((Events.time_fired > start_day) & (Events.time_fired < end_day)) + .where(Events.event_type.in_(event_types)) + .outerjoin(EventData, (Events.data_id == EventData.data_id)) + ) + + +def select_states() -> Select: + """Generate a states select that formats the states table as event rows.""" + return select( + *EVENT_COLUMNS_FOR_STATE_SELECT, + *STATE_COLUMNS, + NOT_CONTEXT_ONLY, + ) + + +def legacy_select_events_context_id( + start_day: dt, end_day: dt, context_id: str +) -> Select: + """Generate a legacy events context id select that also joins states.""" + # This can be removed once we no longer have event_ids in the states table + return ( + select( + *EVENT_COLUMNS, + literal(value=None, type_=sqlalchemy.String).label("shared_data"), + *STATE_COLUMNS, + NOT_CONTEXT_ONLY, + ) + .outerjoin(States, (Events.event_id == States.event_id)) + .where( + (States.last_updated == States.last_changed) | States.last_changed.is_(None) + ) + .where(_not_continuous_entity_matcher()) + .outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) + .where((Events.time_fired > start_day) & (Events.time_fired < end_day)) + .where(Events.context_id == context_id) + ) + + +def apply_states_filters(query: Query, start_day: dt, end_day: dt) -> Query: + """Filter states by time range. + + Filters states that do not have an old state or new state (added / removed) + Filters states that are in a continuous domain with a UOM. + Filters states that do not have matching last_updated and last_changed. + """ + return ( + query.filter( + (States.last_updated > start_day) & (States.last_updated < end_day) + ) + .outerjoin(OLD_STATE, (States.old_state_id == OLD_STATE.state_id)) + .where(_missing_state_matcher()) + .where(_not_continuous_entity_matcher()) + .where( + (States.last_updated == States.last_changed) | States.last_changed.is_(None) + ) + .outerjoin( + StateAttributes, (States.attributes_id == StateAttributes.attributes_id) + ) + ) + + +def _missing_state_matcher() -> sqlalchemy.and_: + # The below removes state change events that do not have + # and old_state or the old_state is missing (newly added entities) + # or the new_state is missing (removed entities) + return sqlalchemy.and_( + OLD_STATE.state_id.isnot(None), + (States.state != OLD_STATE.state), + States.state.isnot(None), + ) + + +def _not_continuous_entity_matcher() -> sqlalchemy.or_: + """Match non continuous entities.""" + return sqlalchemy.or_( + _not_continuous_domain_matcher(), + sqlalchemy.and_( + _continuous_domain_matcher, _not_uom_attributes_matcher() + ).self_group(), + ) + + +def _not_continuous_domain_matcher() -> sqlalchemy.and_: + """Match not continuous domains.""" + return sqlalchemy.and_( + *[ + ~States.entity_id.like(entity_domain) + for entity_domain in CONTINUOUS_ENTITY_ID_LIKE + ], + ).self_group() + + +def _continuous_domain_matcher() -> sqlalchemy.or_: + """Match continuous domains.""" + return sqlalchemy.or_( + *[ + States.entity_id.like(entity_domain) + for entity_domain in CONTINUOUS_ENTITY_ID_LIKE + ], + ).self_group() + + +def _not_uom_attributes_matcher() -> ClauseList: + """Prefilter ATTR_UNIT_OF_MEASUREMENT as its much faster in sql.""" + return ~StateAttributes.shared_attrs.like( + UNIT_OF_MEASUREMENT_JSON_LIKE + ) | ~States.attributes.like(UNIT_OF_MEASUREMENT_JSON_LIKE) diff --git a/homeassistant/components/logbook/queries/devices.py b/homeassistant/components/logbook/queries/devices.py new file mode 100644 index 00000000000..20b56ef8dd6 --- /dev/null +++ b/homeassistant/components/logbook/queries/devices.py @@ -0,0 +1,87 @@ +"""Devices queries for logbook.""" +from __future__ import annotations + +from collections.abc import Iterable +from datetime import datetime as dt + +from sqlalchemy import Column, lambda_stmt, select, union_all +from sqlalchemy.orm import Query +from sqlalchemy.sql.elements import ClauseList +from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Select + +from homeassistant.components.recorder.models import Events, States + +from .common import ( + EVENT_DATA_JSON, + select_events_context_id_subquery, + select_events_context_only, + select_events_without_states, + select_states_context_only, +) + +DEVICE_ID_IN_EVENT: Column = EVENT_DATA_JSON["device_id"] + + +def _select_device_id_context_ids_sub_query( + start_day: dt, + end_day: dt, + event_types: tuple[str, ...], + json_quotable_device_ids: list[str], +) -> Select: + """Generate a subquery to find context ids for multiple devices.""" + return select( + union_all( + select_events_context_id_subquery(start_day, end_day, event_types).where( + apply_event_device_id_matchers(json_quotable_device_ids) + ), + ).c.context_id + ) + + +def _apply_devices_context_union( + query: Query, + start_day: dt, + end_day: dt, + event_types: tuple[str, ...], + json_quotable_device_ids: list[str], +) -> StatementLambdaElement: + """Generate a CTE to find the device context ids and a query to find linked row.""" + devices_cte = _select_device_id_context_ids_sub_query( + start_day, + end_day, + event_types, + json_quotable_device_ids, + ).cte() + return query.union_all( + select_events_context_only().where(Events.context_id.in_(devices_cte)), + select_states_context_only().where(States.context_id.in_(devices_cte)), + ) + + +def devices_stmt( + start_day: dt, + end_day: dt, + event_types: tuple[str, ...], + json_quotable_device_ids: list[str], +) -> StatementLambdaElement: + """Generate a logbook query for multiple devices.""" + stmt = lambda_stmt( + lambda: _apply_devices_context_union( + select_events_without_states(start_day, end_day, event_types).where( + apply_event_device_id_matchers(json_quotable_device_ids) + ), + start_day, + end_day, + event_types, + json_quotable_device_ids, + ).order_by(Events.time_fired) + ) + return stmt + + +def apply_event_device_id_matchers( + json_quotable_device_ids: Iterable[str], +) -> ClauseList: + """Create matchers for the device_ids in the event_data.""" + return DEVICE_ID_IN_EVENT.in_(json_quotable_device_ids) diff --git a/homeassistant/components/logbook/queries/entities.py b/homeassistant/components/logbook/queries/entities.py new file mode 100644 index 00000000000..6db0931e9f3 --- /dev/null +++ b/homeassistant/components/logbook/queries/entities.py @@ -0,0 +1,124 @@ +"""Entities queries for logbook.""" +from __future__ import annotations + +from collections.abc import Iterable +from datetime import datetime as dt + +import sqlalchemy +from sqlalchemy import Column, lambda_stmt, select, union_all +from sqlalchemy.orm import Query +from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Select + +from homeassistant.components.recorder.models import ( + ENTITY_ID_LAST_UPDATED_INDEX, + Events, + States, +) + +from .common import ( + EVENT_DATA_JSON, + OLD_FORMAT_EVENT_DATA_JSON, + apply_states_filters, + select_events_context_id_subquery, + select_events_context_only, + select_events_without_states, + select_states, + select_states_context_only, +) + +ENTITY_ID_IN_EVENT: Column = EVENT_DATA_JSON["entity_id"] +OLD_ENTITY_ID_IN_EVENT: Column = OLD_FORMAT_EVENT_DATA_JSON["entity_id"] + + +def _select_entities_context_ids_sub_query( + start_day: dt, + end_day: dt, + event_types: tuple[str, ...], + entity_ids: list[str], + json_quotable_entity_ids: list[str], +) -> Select: + """Generate a subquery to find context ids for multiple entities.""" + return select( + union_all( + select_events_context_id_subquery(start_day, end_day, event_types).where( + apply_event_entity_id_matchers(json_quotable_entity_ids) + ), + apply_entities_hints(select(States.context_id)) + .filter((States.last_updated > start_day) & (States.last_updated < end_day)) + .where(States.entity_id.in_(entity_ids)), + ).c.context_id + ) + + +def _apply_entities_context_union( + query: Query, + start_day: dt, + end_day: dt, + event_types: tuple[str, ...], + entity_ids: list[str], + json_quotable_entity_ids: list[str], +) -> StatementLambdaElement: + """Generate a CTE to find the entity and device context ids and a query to find linked row.""" + entities_cte = _select_entities_context_ids_sub_query( + start_day, + end_day, + event_types, + entity_ids, + json_quotable_entity_ids, + ).cte() + return query.union_all( + states_query_for_entity_ids(start_day, end_day, entity_ids), + select_events_context_only().where(Events.context_id.in_(entities_cte)), + select_states_context_only() + .where(States.entity_id.not_in(entity_ids)) + .where(States.context_id.in_(entities_cte)), + ) + + +def entities_stmt( + start_day: dt, + end_day: dt, + event_types: tuple[str, ...], + entity_ids: list[str], + json_quotable_entity_ids: list[str], +) -> StatementLambdaElement: + """Generate a logbook query for multiple entities.""" + assert json_quotable_entity_ids is not None + return lambda_stmt( + lambda: _apply_entities_context_union( + select_events_without_states(start_day, end_day, event_types).where( + apply_event_entity_id_matchers(json_quotable_entity_ids) + ), + start_day, + end_day, + event_types, + entity_ids, + json_quotable_entity_ids, + ).order_by(Events.time_fired) + ) + + +def states_query_for_entity_ids( + start_day: dt, end_day: dt, entity_ids: list[str] +) -> Query: + """Generate a select for states from the States table for specific entities.""" + return apply_states_filters( + apply_entities_hints(select_states()), start_day, end_day + ).where(States.entity_id.in_(entity_ids)) + + +def apply_event_entity_id_matchers( + json_quotable_entity_ids: Iterable[str], +) -> sqlalchemy.or_: + """Create matchers for the entity_id in the event_data.""" + return ENTITY_ID_IN_EVENT.in_( + json_quotable_entity_ids + ) | OLD_ENTITY_ID_IN_EVENT.in_(json_quotable_entity_ids) + + +def apply_entities_hints(query: Query) -> Query: + """Force mysql to use the right index on large selects.""" + return query.with_hint( + States, f"FORCE INDEX ({ENTITY_ID_LAST_UPDATED_INDEX})", dialect_name="mysql" + ) diff --git a/homeassistant/components/logbook/queries/entities_and_devices.py b/homeassistant/components/logbook/queries/entities_and_devices.py new file mode 100644 index 00000000000..d8a23635ad7 --- /dev/null +++ b/homeassistant/components/logbook/queries/entities_and_devices.py @@ -0,0 +1,111 @@ +"""Entities and Devices queries for logbook.""" +from __future__ import annotations + +from collections.abc import Iterable +from datetime import datetime as dt + +import sqlalchemy +from sqlalchemy import lambda_stmt, select, union_all +from sqlalchemy.orm import Query +from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Select + +from homeassistant.components.recorder.models import Events, States + +from .common import ( + select_events_context_id_subquery, + select_events_context_only, + select_events_without_states, + select_states_context_only, +) +from .devices import apply_event_device_id_matchers +from .entities import ( + apply_entities_hints, + apply_event_entity_id_matchers, + states_query_for_entity_ids, +) + + +def _select_entities_device_id_context_ids_sub_query( + start_day: dt, + end_day: dt, + event_types: tuple[str, ...], + entity_ids: list[str], + json_quotable_entity_ids: list[str], + json_quotable_device_ids: list[str], +) -> Select: + """Generate a subquery to find context ids for multiple entities and multiple devices.""" + return select( + union_all( + select_events_context_id_subquery(start_day, end_day, event_types).where( + _apply_event_entity_id_device_id_matchers( + json_quotable_entity_ids, json_quotable_device_ids + ) + ), + apply_entities_hints(select(States.context_id)) + .filter((States.last_updated > start_day) & (States.last_updated < end_day)) + .where(States.entity_id.in_(entity_ids)), + ).c.context_id + ) + + +def _apply_entities_devices_context_union( + query: Query, + start_day: dt, + end_day: dt, + event_types: tuple[str, ...], + entity_ids: list[str], + json_quotable_entity_ids: list[str], + json_quotable_device_ids: list[str], +) -> StatementLambdaElement: + devices_entities_cte = _select_entities_device_id_context_ids_sub_query( + start_day, + end_day, + event_types, + entity_ids, + json_quotable_entity_ids, + json_quotable_device_ids, + ).cte() + return query.union_all( + states_query_for_entity_ids(start_day, end_day, entity_ids), + select_events_context_only().where(Events.context_id.in_(devices_entities_cte)), + select_states_context_only() + .where(States.entity_id.not_in(entity_ids)) + .where(States.context_id.in_(devices_entities_cte)), + ) + + +def entities_devices_stmt( + start_day: dt, + end_day: dt, + event_types: tuple[str, ...], + entity_ids: list[str], + json_quotable_entity_ids: list[str], + json_quotable_device_ids: list[str], +) -> StatementLambdaElement: + """Generate a logbook query for multiple entities.""" + stmt = lambda_stmt( + lambda: _apply_entities_devices_context_union( + select_events_without_states(start_day, end_day, event_types).where( + _apply_event_entity_id_device_id_matchers( + json_quotable_entity_ids, json_quotable_device_ids + ) + ), + start_day, + end_day, + event_types, + entity_ids, + json_quotable_entity_ids, + json_quotable_device_ids, + ).order_by(Events.time_fired) + ) + return stmt + + +def _apply_event_entity_id_device_id_matchers( + json_quotable_entity_ids: Iterable[str], json_quotable_device_ids: Iterable[str] +) -> sqlalchemy.or_: + """Create matchers for the device_id and entity_id in the event_data.""" + return apply_event_entity_id_matchers( + json_quotable_entity_ids + ) | apply_event_device_id_matchers(json_quotable_device_ids) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index d7bb59bdeb1..9d16541e398 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -105,6 +105,9 @@ class FAST_PYSQLITE_DATETIME(sqlite.DATETIME): # type: ignore[misc] JSON_VARIENT_CAST = Text().with_variant( postgresql.JSON(none_as_null=True), "postgresql" ) +JSONB_VARIENT_CAST = Text().with_variant( + postgresql.JSONB(none_as_null=True), "postgresql" +) DATETIME_TYPE = ( DateTime(timezone=True) .with_variant(mysql.DATETIME(timezone=True, fsp=6), "mysql") diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index dddbbc61134..7657ebf2b83 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -5,6 +5,7 @@ import collections from datetime import datetime, timedelta from http import HTTPStatus import json +from typing import Callable from unittest.mock import Mock, patch import pytest @@ -30,11 +31,13 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, + EVENT_LOGBOOK_ENTRY, STATE_OFF, STATE_ON, ) import homeassistant.core as ha -from homeassistant.helpers import entity_registry as er +from homeassistant.core import Event, HomeAssistant +from homeassistant.helpers import device_registry, entity_registry as er from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component @@ -42,7 +45,7 @@ import homeassistant.util.dt as dt_util from .common import MockRow, mock_humanify -from tests.common import async_capture_events, mock_platform +from tests.common import MockConfigEntry, async_capture_events, mock_platform from tests.components.recorder.common import ( async_recorder_block_till_done, async_wait_recording_done, @@ -92,12 +95,15 @@ async def test_service_call_create_logbook_entry(hass_): # Our service call will unblock when the event listeners have been # scheduled. This means that they may not have been processed yet. await async_wait_recording_done(hass_) + ent_reg = er.async_get(hass_) events = list( logbook._get_events( hass_, dt_util.utcnow() - timedelta(hours=1), dt_util.utcnow() + timedelta(hours=1), + (EVENT_LOGBOOK_ENTRY,), + ent_reg, ) ) assert len(events) == 2 @@ -131,12 +137,15 @@ async def test_service_call_create_logbook_entry_invalid_entity_id(hass, recorde }, ) await async_wait_recording_done(hass) + ent_reg = er.async_get(hass) events = list( logbook._get_events( hass, dt_util.utcnow() - timedelta(hours=1), dt_util.utcnow() + timedelta(hours=1), + (EVENT_LOGBOOK_ENTRY,), + ent_reg, ) ) assert len(events) == 1 @@ -2431,3 +2440,270 @@ async def test_get_events_bad_end_time(hass, hass_ws_client, recorder_mock): response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == "invalid_end_time" + + +async def test_get_events_with_device_ids(hass, hass_ws_client, recorder_mock): + """Test logbook get_events for device ids.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook") + ] + ) + + entry = MockConfigEntry(domain="test", data={"first": True}, options=None) + entry.add_to_hass(hass) + dev_reg = device_registry.async_get(hass) + device = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + sw_version="sw-version", + name="device name", + manufacturer="manufacturer", + model="model", + suggested_area="Game Room", + ) + + class MockLogbookPlatform: + """Mock a logbook platform.""" + + @ha.callback + def async_describe_events( + hass: HomeAssistant, + async_describe_event: Callable[ + [str, str, Callable[[Event], dict[str, str]]], None + ], + ) -> None: + """Describe logbook events.""" + + @ha.callback + def async_describe_test_event(event: Event) -> dict[str, str]: + """Describe mock logbook event.""" + return { + "name": "device name", + "message": "is on fire", + } + + async_describe_event("test", "mock_event", async_describe_test_event) + + await logbook._process_logbook_platform(hass, "test", MockLogbookPlatform) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire("mock_event", {"device_id": device.id}) + + hass.states.async_set("light.kitchen", STATE_OFF) + await hass.async_block_till_done() + hass.states.async_set("light.kitchen", STATE_ON, {"brightness": 100}) + await hass.async_block_till_done() + hass.states.async_set("light.kitchen", STATE_ON, {"brightness": 200}) + await hass.async_block_till_done() + hass.states.async_set("light.kitchen", STATE_ON, {"brightness": 300}) + await hass.async_block_till_done() + hass.states.async_set("light.kitchen", STATE_ON, {"brightness": 400}) + await hass.async_block_till_done() + context = ha.Context( + id="ac5bd62de45711eaaeb351041eec8dd9", + user_id="b400facee45711eaa9308bfd3d19e474", + ) + + hass.states.async_set("light.kitchen", STATE_OFF, context=context) + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + client = await hass_ws_client() + + await client.send_json( + { + "id": 1, + "type": "logbook/get_events", + "start_time": now.isoformat(), + "device_ids": [device.id], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + + results = response["result"] + assert len(results) == 1 + assert results[0]["name"] == "device name" + assert results[0]["message"] == "is on fire" + assert isinstance(results[0]["when"], float) + + await client.send_json( + { + "id": 2, + "type": "logbook/get_events", + "start_time": now.isoformat(), + "entity_ids": ["light.kitchen"], + "device_ids": [device.id], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 2 + + results = response["result"] + assert results[0]["entity_id"] == "light.kitchen" + assert results[0]["state"] == "on" + assert results[1]["entity_id"] == "light.kitchen" + assert results[1]["state"] == "off" + + await client.send_json( + { + "id": 3, + "type": "logbook/get_events", + "start_time": now.isoformat(), + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 3 + + results = response["result"] + assert len(results) == 4 + assert results[0]["message"] == "started" + assert results[1]["name"] == "device name" + assert results[1]["message"] == "is on fire" + assert isinstance(results[1]["when"], float) + assert results[2]["entity_id"] == "light.kitchen" + assert results[2]["state"] == "on" + assert isinstance(results[2]["when"], float) + assert results[3]["entity_id"] == "light.kitchen" + assert results[3]["state"] == "off" + assert isinstance(results[3]["when"], float) + + +async def test_logbook_select_entities_context_id(hass, recorder_mock, hass_client): + """Test the logbook view with end_time and entity with automations and scripts.""" + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + + await async_recorder_block_till_done(hass) + + context = ha.Context( + id="ac5bd62de45711eaaeb351041eec8dd9", + user_id="b400facee45711eaa9308bfd3d19e474", + ) + + # An Automation + automation_entity_id_test = "automation.alarm" + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation", ATTR_ENTITY_ID: automation_entity_id_test}, + context=context, + ) + hass.bus.async_fire( + EVENT_SCRIPT_STARTED, + {ATTR_NAME: "Mock script", ATTR_ENTITY_ID: "script.mock_script"}, + context=context, + ) + hass.states.async_set( + automation_entity_id_test, + STATE_ON, + {ATTR_FRIENDLY_NAME: "Alarm Automation"}, + context=context, + ) + + entity_id_test = "alarm_control_panel.area_001" + hass.states.async_set(entity_id_test, STATE_OFF, context=context) + await hass.async_block_till_done() + hass.states.async_set(entity_id_test, STATE_ON, context=context) + await hass.async_block_till_done() + entity_id_second = "alarm_control_panel.area_002" + hass.states.async_set(entity_id_second, STATE_OFF, context=context) + await hass.async_block_till_done() + hass.states.async_set(entity_id_second, STATE_ON, context=context) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + entity_id_third = "alarm_control_panel.area_003" + + logbook.async_log_entry( + hass, + "mock_name", + "mock_message", + "alarm_control_panel", + entity_id_third, + context, + ) + await hass.async_block_till_done() + + logbook.async_log_entry( + hass, + "mock_name", + "mock_message", + "homeassistant", + None, + context, + ) + await hass.async_block_till_done() + + # A service call + light_turn_off_service_context = ha.Context( + id="9c5bd62de45711eaaeb351041eec8dd9", + user_id="9400facee45711eaa9308bfd3d19e474", + ) + hass.states.async_set("light.switch", STATE_ON) + await hass.async_block_till_done() + + hass.bus.async_fire( + EVENT_CALL_SERVICE, + { + ATTR_DOMAIN: "light", + ATTR_SERVICE: "turn_off", + ATTR_ENTITY_ID: "light.switch", + }, + context=light_turn_off_service_context, + ) + await hass.async_block_till_done() + + hass.states.async_set( + "light.switch", STATE_OFF, context=light_turn_off_service_context + ) + await async_wait_recording_done(hass) + + client = await hass_client() + + # Today time 00:00:00 + start = dt_util.utcnow().date() + start_date = datetime(start.year, start.month, start.day) + + # Test today entries with filter by end_time + end_time = start + timedelta(hours=24) + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity={entity_id_test},{entity_id_second},{entity_id_third},light.switch" + ) + assert response.status == HTTPStatus.OK + json_dict = await response.json() + + assert json_dict[0]["entity_id"] == entity_id_test + assert json_dict[0]["context_event_type"] == "automation_triggered" + assert json_dict[0]["context_entity_id"] == "automation.alarm" + assert json_dict[0]["context_entity_id_name"] == "Alarm Automation" + assert json_dict[0]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[1]["entity_id"] == entity_id_second + assert json_dict[1]["context_event_type"] == "automation_triggered" + assert json_dict[1]["context_entity_id"] == "automation.alarm" + assert json_dict[1]["context_entity_id_name"] == "Alarm Automation" + assert json_dict[1]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[2]["entity_id"] == "alarm_control_panel.area_003" + assert json_dict[2]["context_event_type"] == "automation_triggered" + assert json_dict[2]["context_entity_id"] == "automation.alarm" + assert json_dict[2]["domain"] == "alarm_control_panel" + assert json_dict[2]["context_entity_id_name"] == "Alarm Automation" + assert json_dict[2]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[3]["entity_id"] == "light.switch" + assert json_dict[3]["context_event_type"] == "call_service" + assert json_dict[3]["context_domain"] == "light" + assert json_dict[3]["context_service"] == "turn_off" + assert json_dict[3]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" From c52f535eb3b0a48d5da203ed6d032f28c6ad2ebb Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 18 May 2022 10:00:46 +0300 Subject: [PATCH 0603/3516] Fix filesize doing IO in event loop (#72038) --- homeassistant/components/filesize/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 6c52fcbdd5a..5f66aefaab5 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -96,7 +96,7 @@ class FileSizeCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict[str, float | int | datetime]: """Fetch file information.""" try: - statinfo = os.stat(self._path) + statinfo = await self.hass.async_add_executor_job(os.stat, self._path) except OSError as error: raise UpdateFailed(f"Can not retrieve file statistics {error}") from error From 14361f95878d1e548430e0a792255c754156d11d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 May 2022 00:14:33 -0700 Subject: [PATCH 0604/3516] Add media browser support for GStreamer (#72051) * Add media browser support for GStreamer * Fix media type check --- .../components/gstreamer/media_player.py | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gstreamer/media_player.py b/homeassistant/components/gstreamer/media_player.py index d1b0fb056ff..545941f2924 100644 --- a/homeassistant/components/gstreamer/media_player.py +++ b/homeassistant/components/gstreamer/media_player.py @@ -6,11 +6,16 @@ import logging from gsp import GstreamerPlayer import voluptuous as vol +from homeassistant.components import media_source from homeassistant.components.media_player import ( PLATFORM_SCHEMA, + BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, ) +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP, STATE_IDLE from homeassistant.core import HomeAssistant @@ -58,6 +63,7 @@ class GstreamerDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.NEXT_TRACK + | MediaPlayerEntityFeature.BROWSE_MEDIA ) def __init__(self, player, name): @@ -86,12 +92,20 @@ class GstreamerDevice(MediaPlayerEntity): """Set the volume level.""" self._player.volume = volume - def play_media(self, media_type, media_id, **kwargs): + async def async_play_media(self, media_type, media_id, **kwargs): """Play media.""" - if media_type != MEDIA_TYPE_MUSIC: + # Handle media_source + if media_source.is_media_source_id(media_id): + sourced_media = await media_source.async_resolve_media(self.hass, media_id) + media_id = sourced_media.url + + elif media_type != MEDIA_TYPE_MUSIC: _LOGGER.error("Invalid media type") return - self._player.queue(media_id) + + media_id = async_process_play_media_url(self.hass, media_id) + + await self.hass.async_add_executor_job(self._player.queue, media_id) def media_play(self): """Play.""" @@ -149,3 +163,13 @@ class GstreamerDevice(MediaPlayerEntity): def media_album_name(self): """Media album.""" return self._album + + async def async_browse_media( + self, media_content_type=None, media_content_id=None + ) -> BrowseMedia: + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) From 12020ffac1a537e2f83df40ba911252ad478919d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 May 2022 00:15:17 -0700 Subject: [PATCH 0605/3516] Add Media Browser support to VLC (#72052) --- homeassistant/components/vlc/media_player.py | 35 +++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py index 10903295f95..7312eacd1c6 100644 --- a/homeassistant/components/vlc/media_player.py +++ b/homeassistant/components/vlc/media_player.py @@ -6,11 +6,16 @@ import logging import vlc import voluptuous as vol +from homeassistant.components import media_source from homeassistant.components.media_player import ( PLATFORM_SCHEMA, + BrowseMedia, MediaPlayerEntity, MediaPlayerEntityFeature, ) +from homeassistant.components.media_player.browse_media import ( + async_process_play_media_url, +) from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.const import CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant @@ -54,6 +59,7 @@ class VlcDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.PLAY_MEDIA | MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.BROWSE_MEDIA ) def __init__(self, name, arguments): @@ -158,15 +164,36 @@ class VlcDevice(MediaPlayerEntity): self._vlc.stop() self._state = STATE_IDLE - def play_media(self, media_type, media_id, **kwargs): + async def async_play_media(self, media_type, media_id, **kwargs): """Play media from a URL or file.""" - if media_type != MEDIA_TYPE_MUSIC: + # Handle media_source + if media_source.is_media_source_id(media_id): + sourced_media = await media_source.async_resolve_media(self.hass, media_id) + media_id = sourced_media.url + + elif media_type != MEDIA_TYPE_MUSIC: _LOGGER.error( "Invalid media type %s. Only %s is supported", media_type, MEDIA_TYPE_MUSIC, ) return - self._vlc.set_media(self._instance.media_new(media_id)) - self._vlc.play() + + media_id = async_process_play_media_url(self.hass, media_id) + + def play(): + self._vlc.set_media(self._instance.media_new(media_id)) + self._vlc.play() + + await self.hass.async_add_executor_job(play) self._state = STATE_PLAYING + + async def async_browse_media( + self, media_content_type=None, media_content_id=None + ) -> BrowseMedia: + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) From 8d57f704668c020f79e5590d046ee8868913e8cb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 09:36:20 +0200 Subject: [PATCH 0606/3516] Ignore UpnpXmlContentError in SamsungTV (#72056) --- homeassistant/components/samsungtv/media_player.py | 6 ++++-- tests/components/samsungtv/test_media_player.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index a0b70af2db5..0599115774e 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -15,6 +15,7 @@ from async_upnp_client.exceptions import ( UpnpConnectionError, UpnpError, UpnpResponseError, + UpnpXmlContentError, ) from async_upnp_client.profiles.dlna import DmrDevice from async_upnp_client.utils import async_get_local_ip @@ -270,11 +271,12 @@ class SamsungTVDevice(MediaPlayerEntity): # NETWORK,NONE upnp_factory = UpnpFactory(upnp_requester, non_strict=True) upnp_device: UpnpDevice | None = None - with contextlib.suppress(UpnpConnectionError, UpnpResponseError): + try: upnp_device = await upnp_factory.async_create_device( self._ssdp_rendering_control_location ) - if not upnp_device: + except (UpnpConnectionError, UpnpResponseError, UpnpXmlContentError) as err: + LOGGER.debug("Unable to create Upnp DMR device: %r", err, exc_info=True) return _, event_ip = await async_get_local_ip( self._ssdp_rendering_control_location, self.hass.loop diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index e8407a86a3e..56cce6ebfbf 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -1368,6 +1368,7 @@ async def test_upnp_not_available( ) -> None: """Test for volume control when Upnp is not available.""" await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + assert "Unable to create Upnp DMR device" in caplog.text # Upnp action fails assert await hass.services.async_call( @@ -1385,6 +1386,7 @@ async def test_upnp_missing_service( ) -> None: """Test for volume control when Upnp is not available.""" await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + assert "Unable to create Upnp DMR device" in caplog.text # Upnp action fails assert await hass.services.async_call( From 28515404533865fa8001000e393ba710d1feafb5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 10:34:20 +0200 Subject: [PATCH 0607/3516] Drop unnecessary async definitions in onewire (#72018) --- homeassistant/components/onewire/config_flow.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onewire/config_flow.py b/homeassistant/components/onewire/config_flow.py index 5e944c51e0a..5876bcfd206 100644 --- a/homeassistant/components/onewire/config_flow.py +++ b/homeassistant/components/onewire/config_flow.py @@ -139,7 +139,7 @@ class OnewireOptionsFlowHandler(OptionsFlow): if user_input.get(INPUT_ENTRY_CLEAR_OPTIONS): # Reset all options self.options = {} - return await self._update_options() + return self._async_update_options() selected_devices: list[str] = ( user_input.get(INPUT_ENTRY_DEVICE_SELECTION) or [] @@ -181,7 +181,7 @@ class OnewireOptionsFlowHandler(OptionsFlow): self._update_device_options(user_input) if self.devices_to_configure: return await self.async_step_configure_device(user_input=None) - return await self._update_options() + return self._async_update_options() self.current_device, description = self.devices_to_configure.popitem() data_schema = vol.Schema( @@ -201,7 +201,8 @@ class OnewireOptionsFlowHandler(OptionsFlow): description_placeholders={"sensor_id": self.current_device}, ) - async def _update_options(self) -> FlowResult: + @callback + def _async_update_options(self) -> FlowResult: """Update config entry options.""" return self.async_create_entry(title="", data=self.options) From 5724d87c4068fa76dab3c6787dc49393f818ebc7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 11:46:09 +0200 Subject: [PATCH 0608/3516] Cleanup deprecated async_get_registry in uptimerobot (#72076) --- homeassistant/components/uptimerobot/__init__.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/uptimerobot/__init__.py b/homeassistant/components/uptimerobot/__init__.py index 6d9be1b2364..a4c975ff58e 100644 --- a/homeassistant/components/uptimerobot/__init__.py +++ b/homeassistant/components/uptimerobot/__init__.py @@ -12,12 +12,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import ( - DeviceRegistry, - async_entries_for_config_entry, - async_get_registry, -) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import API_ATTR_OK, COORDINATOR_UPDATE_INTERVAL, DOMAIN, LOGGER, PLATFORMS @@ -32,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "Wrong API key type detected, use the 'main' API key" ) uptime_robot_api = UptimeRobot(key, async_get_clientsession(hass)) - dev_reg = await async_get_registry(hass) + dev_reg = dr.async_get(hass) hass.data[DOMAIN][entry.entry_id] = coordinator = UptimeRobotDataUpdateCoordinator( hass, @@ -67,7 +63,7 @@ class UptimeRobotDataUpdateCoordinator(DataUpdateCoordinator): self, hass: HomeAssistant, config_entry_id: str, - dev_reg: DeviceRegistry, + dev_reg: dr.DeviceRegistry, api: UptimeRobot, ) -> None: """Initialize coordinator.""" @@ -97,7 +93,7 @@ class UptimeRobotDataUpdateCoordinator(DataUpdateCoordinator): current_monitors = { list(device.identifiers)[0][1] - for device in async_entries_for_config_entry( + for device in dr.async_entries_for_config_entry( self._device_registry, self._config_entry_id ) } From 2060f428189eb15f9ef613c494ac32f4f794dcfc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 11:55:08 +0200 Subject: [PATCH 0609/3516] Cleanup deprecated async_get_registry in acmeda (#72060) --- homeassistant/components/acmeda/base.py | 8 +++----- homeassistant/components/acmeda/helpers.py | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/acmeda/base.py b/homeassistant/components/acmeda/base.py index 3338bf9667d..e9ffb94c6c6 100644 --- a/homeassistant/components/acmeda/base.py +++ b/homeassistant/components/acmeda/base.py @@ -2,10 +2,8 @@ import aiopulse from homeassistant.core import callback -from homeassistant.helpers import entity -from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg +from homeassistant.helpers import device_registry as dr, entity, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg from .const import ACMEDA_ENTITY_REMOVE, DOMAIN, LOGGER @@ -21,11 +19,11 @@ class AcmedaBase(entity.Entity): """Unregister from entity and device registry and call entity remove function.""" LOGGER.error("Removing %s %s", self.__class__.__name__, self.unique_id) - ent_registry = await get_ent_reg(self.hass) + ent_registry = er.async_get(self.hass) if self.entity_id in ent_registry.entities: ent_registry.async_remove(self.entity_id) - dev_registry = await get_dev_reg(self.hass) + dev_registry = dr.async_get(self.hass) device = dev_registry.async_get_device(identifiers={(DOMAIN, self.unique_id)}) if device is not None: dev_registry.async_update_device( diff --git a/homeassistant/components/acmeda/helpers.py b/homeassistant/components/acmeda/helpers.py index a1a262be77e..ff8f28ffbc3 100644 --- a/homeassistant/components/acmeda/helpers.py +++ b/homeassistant/components/acmeda/helpers.py @@ -3,7 +3,7 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, LOGGER @@ -36,7 +36,7 @@ def async_add_acmeda_entities( async def update_devices(hass: HomeAssistant, config_entry: ConfigEntry, api): """Tell hass that device info has been updated.""" - dev_registry = await get_dev_reg(hass) + dev_registry = dr.async_get(hass) for api_item in api.values(): # Update Device name From fc84e4061f8d723ef9df565cda355d1c6aff7e32 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 11:59:13 +0200 Subject: [PATCH 0610/3516] Cleanup deprecated async_get_registry in philips_js (#72071) --- homeassistant/components/philips_js/device_trigger.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/philips_js/device_trigger.py b/homeassistant/components/philips_js/device_trigger.py index 09784dae63f..a48f9e58331 100644 --- a/homeassistant/components/philips_js/device_trigger.py +++ b/homeassistant/components/philips_js/device_trigger.py @@ -12,7 +12,7 @@ from homeassistant.components.automation import ( from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant -from homeassistant.helpers.device_registry import DeviceRegistry, async_get_registry +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import ConfigType from . import PhilipsTVDataUpdateCoordinator @@ -53,7 +53,7 @@ async def async_attach_trigger( ) -> CALLBACK_TYPE | None: """Attach a trigger.""" trigger_data = automation_info["trigger_data"] - registry: DeviceRegistry = await async_get_registry(hass) + registry: dr.DeviceRegistry = dr.async_get(hass) if config[CONF_TYPE] == TRIGGER_TYPE_TURN_ON: variables = { "trigger": { From 8b5803735f1f0e3b049b8c3c89943af08fc7d956 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 12:00:01 +0200 Subject: [PATCH 0611/3516] Cleanup deprecated async_get_registry in rfxtrx (#72073) --- .../components/rfxtrx/config_flow.py | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 8fce56564a4..61d01b8d533 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -24,16 +24,10 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import ( - DeviceEntry, - DeviceRegistry, - async_entries_for_config_entry, - async_get_registry as async_get_device_registry, -) -from homeassistant.helpers.entity_registry import ( - async_entries_for_device, - async_get_registry as async_get_entity_registry, +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry as er, ) from . import ( @@ -80,8 +74,8 @@ def none_or_int(value, base): class OptionsFlow(config_entries.OptionsFlow): """Handle Rfxtrx options.""" - _device_registry: DeviceRegistry - _device_entries: list[DeviceEntry] + _device_registry: dr.DeviceRegistry + _device_entries: list[dr.DeviceEntry] def __init__(self, config_entry: ConfigEntry) -> None: """Initialize rfxtrx options flow.""" @@ -135,8 +129,8 @@ class OptionsFlow(config_entries.OptionsFlow): return self.async_create_entry(title="", data={}) - device_registry = await async_get_device_registry(self.hass) - device_entries = async_entries_for_config_entry( + device_registry = dr.async_get(self.hass) + device_entries = dr.async_entries_for_config_entry( device_registry, self._config_entry.entry_id ) self._device_registry = device_registry @@ -320,8 +314,8 @@ class OptionsFlow(config_entries.OptionsFlow): old_device_id = "_".join(x for x in old_device_data[CONF_DEVICE_ID]) new_device_id = "_".join(x for x in new_device_data[CONF_DEVICE_ID]) - entity_registry = await async_get_entity_registry(self.hass) - entity_entries = async_entries_for_device( + entity_registry = er.async_get(self.hass) + entity_entries = er.async_entries_for_device( entity_registry, old_device, include_disabled_entities=True ) entity_migration_map = {} From 8492f282cb0e105d6751449649183aa89a5b7e8b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 12:37:27 +0200 Subject: [PATCH 0612/3516] Cleanup deprecated async_get_registry in xbox (#72079) --- homeassistant/components/xbox/binary_sensor.py | 6 ++---- homeassistant/components/xbox/sensor.py | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/xbox/binary_sensor.py b/homeassistant/components/xbox/binary_sensor.py index 592909f3aac..7cf7ca6a6a5 100644 --- a/homeassistant/components/xbox/binary_sensor.py +++ b/homeassistant/components/xbox/binary_sensor.py @@ -6,10 +6,8 @@ from functools import partial from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity_registry import ( - async_get_registry as async_get_entity_registry, -) from . import XboxUpdateCoordinator from .base_sensor import XboxBaseSensorEntity @@ -80,7 +78,7 @@ async def async_remove_entities( current: dict[str, XboxBinarySensorEntity], ) -> None: """Remove friend sensors from Home Assistant.""" - registry = await async_get_entity_registry(coordinator.hass) + registry = er.async_get(coordinator.hass) entities = current[xuid] for entity in entities: if entity.entity_id in registry.entities: diff --git a/homeassistant/components/xbox/sensor.py b/homeassistant/components/xbox/sensor.py index edcc4a8c135..02b4f8b84a4 100644 --- a/homeassistant/components/xbox/sensor.py +++ b/homeassistant/components/xbox/sensor.py @@ -6,10 +6,8 @@ from functools import partial from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity_registry import ( - async_get_registry as async_get_entity_registry, -) from . import XboxUpdateCoordinator from .base_sensor import XboxBaseSensorEntity @@ -82,7 +80,7 @@ async def async_remove_entities( current: dict[str, XboxSensorEntity], ) -> None: """Remove friend sensors from Home Assistant.""" - registry = await async_get_entity_registry(coordinator.hass) + registry = er.async_get(coordinator.hass) entities = current[xuid] for entity in entities: if entity.entity_id in registry.entities: From 4eb46d45caa78712fcb4c0db764ade02bd53fb82 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 13:12:38 +0200 Subject: [PATCH 0613/3516] Cleanup deprecated async_get_registry in tests (#72059) --- tests/components/efergy/test_init.py | 2 +- tests/components/goalzero/test_init.py | 2 +- tests/components/hyperion/test_camera.py | 2 +- tests/components/hyperion/test_light.py | 2 +- tests/components/hyperion/test_switch.py | 2 +- tests/components/insteon/test_api_device.py | 6 +++--- tests/components/knx/test_binary_sensor.py | 6 ++---- tests/components/knx/test_scene.py | 6 ++---- tests/components/mikrotik/test_init.py | 2 +- tests/components/motioneye/test_camera.py | 7 +++---- tests/components/motioneye/test_media_source.py | 8 ++++---- tests/components/motioneye/test_sensor.py | 2 +- tests/components/motioneye/test_switch.py | 2 +- tests/components/motioneye/test_web_hooks.py | 12 ++++++------ tests/components/ps4/test_init.py | 2 +- tests/components/steam_online/test_init.py | 2 +- tests/components/unifiprotect/test_services.py | 4 ++-- tests/components/uptimerobot/test_init.py | 13 +++++-------- tests/helpers/test_service.py | 2 +- 19 files changed, 38 insertions(+), 46 deletions(-) diff --git a/tests/components/efergy/test_init.py b/tests/components/efergy/test_init.py index 07c80e7bb04..49a152516b7 100644 --- a/tests/components/efergy/test_init.py +++ b/tests/components/efergy/test_init.py @@ -49,7 +49,7 @@ async def test_async_setup_entry_auth_failed(hass: HomeAssistant): 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_registry = dr.async_get(hass) device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) diff --git a/tests/components/goalzero/test_init.py b/tests/components/goalzero/test_init.py index a436b491d48..5f84842ad27 100644 --- a/tests/components/goalzero/test_init.py +++ b/tests/components/goalzero/test_init.py @@ -68,7 +68,7 @@ async def test_update_failed( async def test_device_info(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): """Test device info.""" entry = await async_init_integration(hass, aioclient_mock) - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) diff --git a/tests/components/hyperion/test_camera.py b/tests/components/hyperion/test_camera.py index 71e1e42cb1a..f83ed9c7e78 100644 --- a/tests/components/hyperion/test_camera.py +++ b/tests/components/hyperion/test_camera.py @@ -200,7 +200,7 @@ async def test_device_info(hass: HomeAssistant) -> None: assert device.model == HYPERION_MODEL_NAME assert device.name == TEST_INSTANCE_1["friendly_name"] - entity_registry = await er.async_get_registry(hass) + entity_registry = er.async_get(hass) entities_from_device = [ entry.entity_id for entry in er.async_entries_for_device(entity_registry, device.id) diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index 2514f6b6cba..136a67f0dba 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -1331,7 +1331,7 @@ async def test_device_info(hass: HomeAssistant) -> None: assert device.model == HYPERION_MODEL_NAME assert device.name == TEST_INSTANCE_1["friendly_name"] - entity_registry = await er.async_get_registry(hass) + entity_registry = er.async_get(hass) entities_from_device = [ entry.entity_id for entry in er.async_entries_for_device(entity_registry, device.id) diff --git a/tests/components/hyperion/test_switch.py b/tests/components/hyperion/test_switch.py index 7ec441716ce..cd1dbdcda5b 100644 --- a/tests/components/hyperion/test_switch.py +++ b/tests/components/hyperion/test_switch.py @@ -172,7 +172,7 @@ async def test_device_info(hass: HomeAssistant) -> None: assert device.model == HYPERION_MODEL_NAME assert device.name == TEST_INSTANCE_1["friendly_name"] - entity_registry = await er.async_get_registry(hass) + entity_registry = er.async_get(hass) entities_from_device = [ entry.entity_id for entry in er.async_entries_for_device(entity_registry, device.id) diff --git a/tests/components/insteon/test_api_device.py b/tests/components/insteon/test_api_device.py index 49588c6ea8f..206565cb7c7 100644 --- a/tests/components/insteon/test_api_device.py +++ b/tests/components/insteon/test_api_device.py @@ -17,7 +17,7 @@ from homeassistant.components.insteon.api.device import ( async_device_name, ) from homeassistant.components.insteon.const import DOMAIN, MULTIPLE -from homeassistant.helpers.device_registry import async_get_registry +from homeassistant.helpers import device_registry as dr from .const import MOCK_USER_INPUT_PLM from .mock_devices import MockDevices @@ -40,7 +40,7 @@ async def _async_setup(hass, hass_ws_client): devices = MockDevices() await devices.async_load() - dev_reg = await async_get_registry(hass) + dev_reg = dr.async_get(hass) # Create device registry entry for mock node ha_device = dev_reg.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -94,7 +94,7 @@ async def test_no_insteon_device(hass, hass_ws_client): devices = MockDevices() await devices.async_load() - dev_reg = await async_get_registry(hass) + dev_reg = dr.async_get(hass) # Create device registry entry for a Insteon device not in the Insteon devices list ha_device_1 = dev_reg.async_get_or_create( config_entry_id=config_entry.entry_id, diff --git a/tests/components/knx/test_binary_sensor.py b/tests/components/knx/test_binary_sensor.py index 25a223e76c8..ff58a36391c 100644 --- a/tests/components/knx/test_binary_sensor.py +++ b/tests/components/knx/test_binary_sensor.py @@ -6,10 +6,8 @@ from homeassistant.components.knx.const import CONF_STATE_ADDRESS, CONF_SYNC_STA from homeassistant.components.knx.schema import BinarySensorSchema from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, State +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory -from homeassistant.helpers.entity_registry import ( - async_get_registry as async_get_entity_registry, -) from homeassistant.util import dt from .conftest import KNXTestKit @@ -35,7 +33,7 @@ async def test_binary_sensor_entity_category(hass: HomeAssistant, knx: KNXTestKi await knx.assert_read("1/1/1") await knx.receive_response("1/1/1", True) - registry = await async_get_entity_registry(hass) + registry = er.async_get(hass) entity = registry.async_get("binary_sensor.test_normal") assert entity.entity_category is EntityCategory.DIAGNOSTIC diff --git a/tests/components/knx/test_scene.py b/tests/components/knx/test_scene.py index 37e4ac12728..a7de98abb20 100644 --- a/tests/components/knx/test_scene.py +++ b/tests/components/knx/test_scene.py @@ -4,10 +4,8 @@ from homeassistant.components.knx.const import KNX_ADDRESS from homeassistant.components.knx.schema import SceneSchema from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory -from homeassistant.helpers.entity_registry import ( - async_get_registry as async_get_entity_registry, -) from .conftest import KNXTestKit @@ -28,7 +26,7 @@ async def test_activate_knx_scene(hass: HomeAssistant, knx: KNXTestKit): ) assert len(hass.states.async_all()) == 1 - registry = await async_get_entity_registry(hass) + registry = er.async_get(hass) entity = registry.async_get("scene.test") assert entity.entity_category is EntityCategory.DIAGNOSTIC assert entity.unique_id == "1/1/1_24" diff --git a/tests/components/mikrotik/test_init.py b/tests/components/mikrotik/test_init.py index 30fa1a0a89f..bc00602789c 100644 --- a/tests/components/mikrotik/test_init.py +++ b/tests/components/mikrotik/test_init.py @@ -76,7 +76,7 @@ async def test_unload_entry(hass): entry.add_to_hass(hass) with patch.object(mikrotik, "MikrotikHub") as mock_hub, patch( - "homeassistant.helpers.device_registry.async_get_registry", + "homeassistant.helpers.device_registry.async_get", return_value=Mock(), ): mock_hub.return_value.async_setup = AsyncMock(return_value=True) diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index 15462f6c592..8ba9fb07715 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -42,7 +42,6 @@ from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_URL from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.device_registry import async_get_registry import homeassistant.util.dt as dt_util from . import ( @@ -138,8 +137,8 @@ async def test_setup_camera_new_data_same(hass: HomeAssistant) -> None: async def test_setup_camera_new_data_camera_removed(hass: HomeAssistant) -> None: """Test a data refresh with a removed camera.""" - device_registry = await async_get_registry(hass) - entity_registry = await er.async_get_registry(hass) + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) client = create_mock_motioneye_client() config_entry = await setup_mock_motioneye_config_entry(hass, client=client) @@ -328,7 +327,7 @@ async def test_device_info(hass: HomeAssistant) -> None: assert device.model == MOTIONEYE_MANUFACTURER assert device.name == TEST_CAMERA_NAME - entity_registry = await er.async_get_registry(hass) + entity_registry = er.async_get(hass) entities_from_device = [ entry.entity_id for entry in er.async_entries_for_device(entity_registry, device.id) diff --git a/tests/components/motioneye/test_media_source.py b/tests/components/motioneye/test_media_source.py index 6979d5c645d..9b86b783d43 100644 --- a/tests/components/motioneye/test_media_source.py +++ b/tests/components/motioneye/test_media_source.py @@ -80,7 +80,7 @@ async def test_async_browse_media_success(hass: HomeAssistant) -> None: client = create_mock_motioneye_client() config = await setup_mock_motioneye_config_entry(hass, client=client) - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=config.entry_id, identifiers={TEST_CAMERA_DEVICE_IDENTIFIER}, @@ -301,7 +301,7 @@ async def test_async_browse_media_images_success(hass: HomeAssistant) -> None: client = create_mock_motioneye_client() config = await setup_mock_motioneye_config_entry(hass, client=client) - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=config.entry_id, identifiers={TEST_CAMERA_DEVICE_IDENTIFIER}, @@ -353,7 +353,7 @@ async def test_async_resolve_media_success(hass: HomeAssistant) -> None: config = await setup_mock_motioneye_config_entry(hass, client=client) - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=config.entry_id, identifiers={TEST_CAMERA_DEVICE_IDENTIFIER}, @@ -391,7 +391,7 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: config = await setup_mock_motioneye_config_entry(hass, client=client) - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=config.entry_id, identifiers={TEST_CAMERA_DEVICE_IDENTIFIER}, diff --git a/tests/components/motioneye/test_sensor.py b/tests/components/motioneye/test_sensor.py index 5ab6fc46f49..ea07834976b 100644 --- a/tests/components/motioneye/test_sensor.py +++ b/tests/components/motioneye/test_sensor.py @@ -91,7 +91,7 @@ async def test_sensor_device_info(hass: HomeAssistant) -> None: device = device_registry.async_get_device({device_identifer}) assert device - entity_registry = await er.async_get_registry(hass) + entity_registry = er.async_get(hass) entities_from_device = [ entry.entity_id for entry in er.async_entries_for_device(entity_registry, device.id) diff --git a/tests/components/motioneye/test_switch.py b/tests/components/motioneye/test_switch.py index 09db967e5e3..03c39a4b542 100644 --- a/tests/components/motioneye/test_switch.py +++ b/tests/components/motioneye/test_switch.py @@ -196,7 +196,7 @@ async def test_switch_device_info(hass: HomeAssistant) -> None: device = device_registry.async_get_device({device_identifer}) assert device - entity_registry = await er.async_get_registry(hass) + entity_registry = er.async_get(hass) entities_from_device = [ entry.entity_id for entry in er.async_entries_for_device(entity_registry, device.id) diff --git a/tests/components/motioneye/test_web_hooks.py b/tests/components/motioneye/test_web_hooks.py index c7aaa4a8638..442ea9eb782 100644 --- a/tests/components/motioneye/test_web_hooks.py +++ b/tests/components/motioneye/test_web_hooks.py @@ -67,7 +67,7 @@ async def test_setup_camera_without_webhook(hass: HomeAssistant) -> None: client = create_mock_motioneye_client() config_entry = await setup_mock_motioneye_config_entry(hass, client=client) - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device = device_registry.async_get_device( identifiers={TEST_CAMERA_DEVICE_IDENTIFIER} ) @@ -122,7 +122,7 @@ async def test_setup_camera_with_wrong_webhook( ) await hass.async_block_till_done() - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device = device_registry.async_get_device( identifiers={TEST_CAMERA_DEVICE_IDENTIFIER} ) @@ -175,7 +175,7 @@ async def test_setup_camera_with_old_webhook( ) assert client.async_set_camera.called - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device = device_registry.async_get_device( identifiers={TEST_CAMERA_DEVICE_IDENTIFIER} ) @@ -211,7 +211,7 @@ async def test_setup_camera_with_correct_webhook( hass, data={CONF_URL: TEST_URL, CONF_WEBHOOK_ID: "webhook_secret_id"} ) - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) device = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={TEST_CAMERA_DEVICE_IDENTIFIER}, @@ -281,7 +281,7 @@ async def test_good_query(hass: HomeAssistant, hass_client_no_auth: Any) -> None """Test good callbacks.""" await async_setup_component(hass, "http", {"http": {}}) - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) client = create_mock_motioneye_client() config_entry = await setup_mock_motioneye_config_entry(hass, client=client) @@ -378,7 +378,7 @@ async def test_event_media_data(hass: HomeAssistant, hass_client_no_auth: Any) - """Test an event with a file path generates media data.""" await async_setup_component(hass, "http", {"http": {}}) - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) client = create_mock_motioneye_client() config_entry = await setup_mock_motioneye_config_entry(hass, client=client) diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index 8c43bd5df90..a84adba2a70 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -155,7 +155,7 @@ async def test_config_flow_entry_migrate(hass): "homeassistant.util.location.async_detect_location_info", return_value=MOCK_LOCATION, ), patch( - "homeassistant.helpers.entity_registry.async_get_registry", + "homeassistant.helpers.entity_registry.async_get", return_value=mock_e_registry, ): await ps4.async_migrate_entry(hass, mock_entry) diff --git a/tests/components/steam_online/test_init.py b/tests/components/steam_online/test_init.py index 2a015a4ed36..435a5ac6f5a 100644 --- a/tests/components/steam_online/test_init.py +++ b/tests/components/steam_online/test_init.py @@ -41,7 +41,7 @@ async def test_device_info(hass: HomeAssistant) -> None: entry = create_entry(hass) with patch_interface(): await hass.config_entries.async_setup(entry.entry_id) - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) await hass.async_block_till_done() device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) diff --git a/tests/components/unifiprotect/test_services.py b/tests/components/unifiprotect/test_services.py index 82e4434ad08..0230bc3d36c 100644 --- a/tests/components/unifiprotect/test_services.py +++ b/tests/components/unifiprotect/test_services.py @@ -29,7 +29,7 @@ async def device_fixture(hass: HomeAssistant, mock_entry: MockEntityFixture): await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) return list(device_registry.devices.values())[0] @@ -48,7 +48,7 @@ async def subdevice_fixture( await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - device_registry = await dr.async_get_registry(hass) + device_registry = dr.async_get(hass) return [d for d in device_registry.devices.values() if d.name != "UnifiProtect"][0] diff --git a/tests/components/uptimerobot/test_init.py b/tests/components/uptimerobot/test_init.py index 8efa51e05a6..00e7f5c27e0 100644 --- a/tests/components/uptimerobot/test_init.py +++ b/tests/components/uptimerobot/test_init.py @@ -11,10 +11,7 @@ from homeassistant.components.uptimerobot.const import ( ) from homeassistant.const import STATE_ON, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import ( - async_entries_for_config_entry, - async_get_registry, -) +from homeassistant.helpers import device_registry as dr from homeassistant.util import dt from .common import ( @@ -187,9 +184,9 @@ async def test_update_errors(hass: HomeAssistant, caplog: LogCaptureFixture): async def test_device_management(hass: HomeAssistant): """Test that we are adding and removing devices for monitors returned from the API.""" mock_entry = await setup_uptimerobot_integration(hass) - dev_reg = await async_get_registry(hass) + dev_reg = dr.async_get(hass) - devices = async_entries_for_config_entry(dev_reg, mock_entry.entry_id) + devices = dr.async_entries_for_config_entry(dev_reg, mock_entry.entry_id) assert len(devices) == 1 assert devices[0].identifiers == {(DOMAIN, "1234")} @@ -207,7 +204,7 @@ async def test_device_management(hass: HomeAssistant): async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) await hass.async_block_till_done() - devices = async_entries_for_config_entry(dev_reg, mock_entry.entry_id) + devices = dr.async_entries_for_config_entry(dev_reg, mock_entry.entry_id) assert len(devices) == 2 assert devices[0].identifiers == {(DOMAIN, "1234")} assert devices[1].identifiers == {(DOMAIN, "12345")} @@ -224,7 +221,7 @@ async def test_device_management(hass: HomeAssistant): async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) await hass.async_block_till_done() - devices = async_entries_for_config_entry(dev_reg, mock_entry.entry_id) + devices = dr.async_entries_for_config_entry(dev_reg, mock_entry.entry_id) assert len(devices) == 1 assert devices[0].identifiers == {(DOMAIN, "1234")} diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 1e947a9353f..76cf83e31bf 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -900,7 +900,7 @@ async def test_domain_control_unknown(hass, mock_entities): calls.append(call) with patch( - "homeassistant.helpers.entity_registry.async_get_registry", + "homeassistant.helpers.entity_registry.async_get", return_value=Mock(entities=mock_entities), ): protected_mock_service = service.verify_domain_control(hass, "test_domain")( From 0cea2eba84dbe2044ce59f95e397a1e9b14bc33a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 13:15:28 +0200 Subject: [PATCH 0614/3516] Cleanup deprecated async_get_registry in airly (#72061) --- homeassistant/components/airly/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 83a9a50ec7f..31396ecf51b 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -15,9 +15,8 @@ from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import async_get_registry from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -82,7 +81,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # identifiers in device_info should use tuple[str, str] type, but latitude and # longitude are float, so we convert old device entries to use correct types # We used to use a str 3-tuple here sometime, convert that to a 2-tuple too. - device_registry = await async_get_registry(hass) + device_registry = dr.async_get(hass) old_ids = (DOMAIN, latitude, longitude) for old_ids in ( (DOMAIN, latitude, longitude), @@ -114,7 +113,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) # Remove air_quality entities from registry if they exist - ent_reg = entity_registry.async_get(hass) + ent_reg = er.async_get(hass) unique_id = f"{coordinator.latitude}-{coordinator.longitude}" if entity_id := ent_reg.async_get_entity_id( AIR_QUALITY_PLATFORM, DOMAIN, unique_id From 1c5541c875513104e162e4fa7e779afc963541a3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 13:16:04 +0200 Subject: [PATCH 0615/3516] Cleanup deprecated async_get_registry in august (#72062) --- homeassistant/components/august/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 29f17c69661..8a6d169fcb0 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -20,9 +20,9 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_PICTURE, PERCENTAGE, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.restore_state import RestoreEntity from . import AugustData @@ -154,7 +154,7 @@ async def async_setup_entry( async def _async_migrate_old_unique_ids(hass, devices): """Keypads now have their own serial number.""" - registry = await async_get_registry(hass) + registry = er.async_get(hass) for device in devices: old_entity_id = registry.async_get_entity_id( "sensor", DOMAIN, device.old_unique_id From 19fdc3e630514e717d1f89b4fc5ef905aabe2abb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 13:18:08 +0200 Subject: [PATCH 0616/3516] Cleanup deprecated async_get_registry in edl21 (#72063) --- homeassistant/components/edl21/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index 7614dc54ac4..f7a79d727a0 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -24,13 +24,12 @@ from homeassistant.const import ( POWER_WATT, ) from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.dt import utcnow @@ -340,7 +339,7 @@ class EDL21: async def add_entities(self, new_entities) -> None: """Migrate old unique IDs, then add entities to hass.""" - registry = await async_get_registry(self._hass) + registry = er.async_get(self._hass) for entity in new_entities: old_entity_id = registry.async_get_entity_id( From eae8a59b7ba2af5008755c8917d3c9095b0f4e03 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 13:18:43 +0200 Subject: [PATCH 0617/3516] Cleanup deprecated async_get_registry in gios (#72065) --- homeassistant/components/gios/__init__.py | 7 +++---- homeassistant/components/gios/sensor.py | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index 579b5ae0ab3..73d773561f5 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -13,9 +13,8 @@ from homeassistant.components.air_quality import DOMAIN as AIR_QUALITY_PLATFORM from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import async_get_registry from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import API_TIMEOUT, CONF_STATION_ID, DOMAIN, SCAN_INTERVAL @@ -35,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_update_entry(entry, unique_id=str(station_id)) # type: ignore[unreachable] # We used to use int in device_entry identifiers, convert this to str. - device_registry = await async_get_registry(hass) + device_registry = dr.async_get(hass) old_ids = (DOMAIN, station_id) device_entry = device_registry.async_get_device({old_ids}) # type: ignore[arg-type] if device_entry and entry.entry_id in device_entry.config_entries: @@ -53,7 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) # Remove air_quality entities from registry if they exist - ent_reg = entity_registry.async_get(hass) + ent_reg = er.async_get(hass) unique_id = str(coordinator.gios.station_id) if entity_id := ent_reg.async_get_entity_id( AIR_QUALITY_PLATFORM, DOMAIN, unique_id diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index c14a99051e4..391976ad793 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -8,10 +8,10 @@ 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 import entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryType 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 from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -43,7 +43,7 @@ async def async_setup_entry( # Due to the change of the attribute name of one sensor, it is necessary to migrate # the unique_id to the new name. - entity_registry = await async_get_registry(hass) + entity_registry = er.async_get(hass) old_unique_id = f"{coordinator.gios.station_id}-pm2.5" if entity_id := entity_registry.async_get_entity_id( PLATFORM, DOMAIN, old_unique_id From ca785266b453a70d75ba528f88c652754e5a1340 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 13:42:49 +0200 Subject: [PATCH 0618/3516] Cleanup deprecated async_get_registry in hue (#72068) --- homeassistant/components/hue/v1/helpers.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hue/v1/helpers.py b/homeassistant/components/hue/v1/helpers.py index d4582f0bb52..b0d774915df 100644 --- a/homeassistant/components/hue/v1/helpers.py +++ b/homeassistant/components/hue/v1/helpers.py @@ -1,7 +1,6 @@ """Helper functions for Philips Hue.""" -from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg -from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg +from homeassistant.helpers import device_registry as dr, entity_registry as er from ..const import DOMAIN @@ -18,10 +17,10 @@ async def remove_devices(bridge, api_ids, current): entity = current[item_id] removed_items.append(item_id) await entity.async_remove(force_remove=True) - ent_registry = await get_ent_reg(bridge.hass) + ent_registry = er.async_get(bridge.hass) if entity.entity_id in ent_registry.entities: ent_registry.async_remove(entity.entity_id) - dev_registry = await get_dev_reg(bridge.hass) + dev_registry = dr.async_get(bridge.hass) device = dev_registry.async_get_device(identifiers={(DOMAIN, entity.device_id)}) if device is not None: dev_registry.async_update_device( From 4e6887515bf8409d3f4ff621f244abbe6bf922e0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 13:45:49 +0200 Subject: [PATCH 0619/3516] Cleanup deprecated async_get_registry in zha (#72080) --- homeassistant/components/zha/core/gateway.py | 23 ++++++-------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 0ba375362f7..642a5b3ec55 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -25,18 +25,9 @@ from homeassistant.components.system_log import LogEntry, _figure_out_source from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.device_registry import ( - CONNECTION_ZIGBEE, - DeviceRegistry, - async_get_registry as get_dev_reg, -) +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.entity_registry import ( - EntityRegistry, - async_entries_for_device, - async_get_registry as get_ent_reg, -) from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType @@ -129,8 +120,8 @@ class ZHAGateway: # -- Set in async_initialize -- zha_storage: ZhaStorage - ha_device_registry: DeviceRegistry - ha_entity_registry: EntityRegistry + ha_device_registry: dr.DeviceRegistry + ha_entity_registry: er.EntityRegistry application_controller: ControllerApplication radio_description: str @@ -161,8 +152,8 @@ class ZHAGateway: discovery.GROUP_PROBE.initialize(self._hass) self.zha_storage = await async_get_registry(self._hass) - self.ha_device_registry = await get_dev_reg(self._hass) - self.ha_entity_registry = await get_ent_reg(self._hass) + self.ha_device_registry = dr.async_get(self._hass) + self.ha_entity_registry = er.async_get(self._hass) radio_type = self.config_entry.data[CONF_RADIO_TYPE] @@ -437,7 +428,7 @@ class ZHAGateway: ] # then we get all group entity entries tied to the coordinator - all_group_entity_entries = async_entries_for_device( + all_group_entity_entries = er.async_entries_for_device( self.ha_entity_registry, self.coordinator_zha_device.device_id, include_disabled_entities=True, @@ -528,7 +519,7 @@ class ZHAGateway: self._devices[zigpy_device.ieee] = zha_device device_registry_device = self.ha_device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, - connections={(CONNECTION_ZIGBEE, str(zha_device.ieee))}, + connections={(dr.CONNECTION_ZIGBEE, str(zha_device.ieee))}, identifiers={(DOMAIN, str(zha_device.ieee))}, name=zha_device.name, manufacturer=zha_device.manufacturer, From b9c84bd65576f8c0de7754aca037f733334f0aef Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 13:47:09 +0200 Subject: [PATCH 0620/3516] Cleanup deprecated async_get_registry in opentherm_gw (#72070) --- homeassistant/components/opentherm_gw/__init__.py | 7 ++----- homeassistant/components/opentherm_gw/binary_sensor.py | 4 ++-- homeassistant/components/opentherm_gw/sensor.py | 4 ++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 1ea24ae9709..5ec8c6420d5 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -23,10 +23,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import ( - async_get_registry as async_get_dev_reg, -) +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType @@ -436,7 +433,7 @@ class OpenThermGatewayDevice: _LOGGER.debug( "Connected to OpenTherm Gateway %s at %s", self.gw_version, self.device_path ) - dev_reg = await async_get_dev_reg(self.hass) + dev_reg = dr.async_get(self.hass) gw_dev = dev_reg.async_get_or_create( config_entry_id=self.config_entry_id, identifiers={(DOMAIN, self.gw_id)}, diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index f630cdf273b..e4880ed26e9 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -6,10 +6,10 @@ from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySenso from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity_registry import async_get_registry from . import DOMAIN from .const import ( @@ -32,7 +32,7 @@ async def async_setup_entry( sensors = [] deprecated_sensors = [] gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] - ent_reg = await async_get_registry(hass) + ent_reg = er.async_get(hass) for var, info in BINARY_SENSOR_INFO.items(): device_class = info[0] friendly_name_format = info[1] diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index 9b2e6c45899..7fb518b0e6d 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -6,10 +6,10 @@ from homeassistant.components.sensor import ENTITY_ID_FORMAT, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity_registry import async_get_registry from . import DOMAIN from .const import ( @@ -32,7 +32,7 @@ async def async_setup_entry( sensors = [] deprecated_sensors = [] gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] - ent_reg = await async_get_registry(hass) + ent_reg = er.async_get(hass) for var, info in SENSOR_INFO.items(): device_class = info[0] unit = info[1] From 9c4a046a2bccec3c3d9f42183c5e3b8c34b90a50 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 13:53:57 +0200 Subject: [PATCH 0621/3516] Cleanup deprecated async_get_registry in sense (#72074) --- homeassistant/components/sense/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sense/binary_sensor.py b/homeassistant/components/sense/binary_sensor.py index aa895da166f..10399fa8e2b 100644 --- a/homeassistant/components/sense/binary_sensor.py +++ b/homeassistant/components/sense/binary_sensor.py @@ -8,9 +8,9 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity_registry import async_get_registry from .const import ( ATTRIBUTION, @@ -50,7 +50,7 @@ async def async_setup_entry( async def _migrate_old_unique_ids(hass, devices): - registry = await async_get_registry(hass) + registry = er.async_get(hass) for device in devices: # Migration of old not so unique ids old_entity_id = registry.async_get_entity_id( From df13fcd1a53d35acae9ec412ab087dcf199d27d4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 14:09:04 +0200 Subject: [PATCH 0622/3516] Cleanup deprecated async_get_registry in synology_dsm (#72075) --- homeassistant/components/synology_dsm/__init__.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 1151bf128cc..8dbdecb9305 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -22,12 +22,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC, CONF_SCAN_INTERVAL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import device_registry -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import ( - DeviceEntry, - async_get_registry as get_dev_reg, -) +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .common import SynoApi @@ -57,8 +52,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Synology DSM sensors.""" # Migrate device indentifiers - dev_reg = await get_dev_reg(hass) - devices: list[DeviceEntry] = device_registry.async_entries_for_config_entry( + dev_reg = dr.async_get(hass) + devices: list[dr.DeviceEntry] = dr.async_entries_for_config_entry( dev_reg, entry.entry_id ) for device in devices: From 64b0dd1e7aed807b81869d85765689540e23ff3b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 14:09:25 +0200 Subject: [PATCH 0623/3516] Cleanup deprecated async_get_registry in plex (#72077) --- homeassistant/components/plex/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index a9f688d9b13..ce76c4be3ff 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -20,6 +20,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -27,7 +28,6 @@ from homeassistant.helpers.dispatcher import ( ) 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.network import is_internal_request from .const import ( @@ -76,7 +76,7 @@ async def async_setup_entry( ) -> None: """Set up Plex media_player from a config entry.""" server_id = config_entry.data[CONF_SERVER_IDENTIFIER] - registry = await async_get_registry(hass) + registry = er.async_get(hass) @callback def async_new_media_players(new_entities): From 70aa71eeed3140a2d9b32074b7997fb7c8fe478b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 14:09:43 +0200 Subject: [PATCH 0624/3516] Cleanup deprecated async_get_registry in isy994 (#72078) --- homeassistant/components/isy994/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index 7efbb3bade7..3b0de172a85 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -24,7 +24,7 @@ from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.helpers import entity_registry as er from .const import ( _LOGGER, @@ -383,7 +383,7 @@ async def migrate_old_unique_ids( hass: HomeAssistant, platform: str, entities: Sequence[ISYEntity] ) -> None: """Migrate to new controller-specific unique ids.""" - registry = await async_get_registry(hass) + registry = er.async_get(hass) for entity in entities: if entity.old_unique_id is None or entity.unique_id is None: From 4978d5f86e266764de4101c8a819374662c6cfb4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 14:21:56 +0200 Subject: [PATCH 0625/3516] Cleanup deprecated async_get_registry in homekit_controller (#72069) --- .../components/homekit_controller/config_flow.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index c3bdfcc42ae..b4bd66aa626 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -14,10 +14,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult -from homeassistant.helpers.device_registry import ( - CONNECTION_NETWORK_MAC, - async_get_registry as async_get_device_registry, -) +from homeassistant.helpers import device_registry as dr from .const import DOMAIN, KNOWN_DEVICES from .utils import async_get_controller @@ -163,9 +160,9 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _hkid_is_homekit(self, hkid): """Determine if the device is a homekit bridge or accessory.""" - dev_reg = await async_get_device_registry(self.hass) + dev_reg = dr.async_get(self.hass) device = dev_reg.async_get_device( - identifiers=set(), connections={(CONNECTION_NETWORK_MAC, hkid)} + identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, hkid)} ) if device is None: From 977c5b7693697ffe6d685f3370b2cb4f78c3d8ff Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 14:22:38 +0200 Subject: [PATCH 0626/3516] Cleanup deprecated async_get_registry in gdacs (#72066) --- homeassistant/components/gdacs/geo_location.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py index 2b6ffd556a9..5bfbd915b6c 100644 --- a/homeassistant/components/gdacs/geo_location.py +++ b/homeassistant/components/gdacs/geo_location.py @@ -12,9 +12,9 @@ from homeassistant.const import ( LENGTH_MILES, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.util.unit_system import IMPERIAL_SYSTEM from .const import DEFAULT_ICON, DOMAIN, FEED @@ -114,7 +114,7 @@ class GdacsEvent(GeolocationEvent): self._remove_signal_delete() self._remove_signal_update() # Remove from entity registry. - entity_registry = await async_get_registry(self.hass) + entity_registry = er.async_get(self.hass) if self.entity_id in entity_registry.entities: entity_registry.async_remove(self.entity_id) From 7c4cc389c9ea94bae7b477303defc6f5ea38fa32 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 14:22:56 +0200 Subject: [PATCH 0627/3516] Cleanup deprecated async_get_registry in geonetnz_quakes (#72064) --- homeassistant/components/geonetnz_quakes/geo_location.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py index 860a80f99c5..515fce56439 100644 --- a/homeassistant/components/geonetnz_quakes/geo_location.py +++ b/homeassistant/components/geonetnz_quakes/geo_location.py @@ -13,9 +13,9 @@ from homeassistant.const import ( LENGTH_MILES, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.util.unit_system import IMPERIAL_SYSTEM from .const import DOMAIN, FEED @@ -100,7 +100,7 @@ class GeonetnzQuakesEvent(GeolocationEvent): self._remove_signal_delete() self._remove_signal_update() # Remove from entity registry. - entity_registry = await async_get_registry(self.hass) + entity_registry = er.async_get(self.hass) if self.entity_id in entity_registry.entities: entity_registry.async_remove(self.entity_id) From 619a92eab9cc5564abe3f1e53c543ad7074fda13 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 14:23:49 +0200 Subject: [PATCH 0628/3516] Cleanup deprecated async_get_registry in fronius (#72067) --- homeassistant/components/fronius/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index 03340f19081..f6607aed11f 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -161,7 +161,7 @@ class FroniusSolarNet: "value" ] - device_registry = await dr.async_get_registry(self.hass) + device_registry = dr.async_get(self.hass) device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, **solar_net_device, From 23cb5cfd3f85cd1f480c3aa8e548fba6e38764a7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 May 2022 00:27:02 +1200 Subject: [PATCH 0629/3516] Bump aioesphomeapi to 10.10.0 (#72083) --- 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 50334808dbf..b89671c6f90 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==10.9.0"], + "requirements": ["aioesphomeapi==10.10.0"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], diff --git a/requirements_all.txt b/requirements_all.txt index ad732ab2034..94ab58682ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -144,7 +144,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.9.0 +aioesphomeapi==10.10.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02bf3ea39b0..3adafb068de 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.9.0 +aioesphomeapi==10.10.0 # homeassistant.components.flo aioflo==2021.11.0 From 26ee289be3824157cda155c53e06754b8446374c Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 18 May 2022 06:30:57 -0700 Subject: [PATCH 0630/3516] Add return code to integration application credentials in config flow (#71986) * Add return code to integration application credentials in config flow * Update google tests to use new return code * Update spotify test for no auth configured * Add translation for oauth2_missing_credentials * Add new return code to yolink * Update homeassistant/strings.json Co-authored-by: Martin Hjelmare Co-authored-by: Franck Nijhof Co-authored-by: Martin Hjelmare --- homeassistant/helpers/config_entry_oauth2_flow.py | 3 +++ homeassistant/strings.json | 1 + tests/components/google/test_config_flow.py | 4 ++-- tests/components/spotify/test_config_flow.py | 4 ++-- tests/components/yolink/test_config_flow.py | 2 +- tests/helpers/test_config_entry_oauth2_flow.py | 11 +++++++++++ 6 files changed, 20 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index d0aaca71304..2d45269a82c 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -25,6 +25,7 @@ from homeassistant import config_entries from homeassistant.components import http from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.loader import async_get_application_credentials from .aiohttp_client import async_get_clientsession from .network import NoURLAvailableError @@ -239,6 +240,8 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): return await self.async_step_auth() if not implementations: + if self.DOMAIN in await async_get_application_credentials(self.hass): + return self.async_abort(reason="missing_credentials") return self.async_abort(reason="missing_configuration") req = http.current_request.get() diff --git a/homeassistant/strings.json b/homeassistant/strings.json index 9dcf8c7fe49..e4d363c22be 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -71,6 +71,7 @@ "webhook_not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive webhook messages.", "oauth2_error": "Received invalid token data.", "oauth2_missing_configuration": "The component is not configured. Please follow the documentation.", + "oauth2_missing_credentials": "The integration requires application credentials.", "oauth2_authorize_url_timeout": "Timeout generating authorize URL.", "oauth2_no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", "reauth_successful": "Re-authentication was successful", diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index f061aeb3057..9339ca9988f 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -327,7 +327,7 @@ async def test_missing_configuration( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("type") == "abort" - assert result.get("reason") == "missing_configuration" + assert result.get("reason") == "missing_credentials" @pytest.mark.parametrize("google_config", [None]) @@ -342,7 +342,7 @@ async def test_missing_configuration_yaml_empty( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("type") == "abort" - assert result.get("reason") == "missing_configuration" + assert result.get("reason") == "missing_credentials" async def test_wrong_configuration( diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index e2f1878c04d..3b1e4851ff1 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -31,14 +31,14 @@ async def test_abort_if_no_configuration(hass): ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "missing_configuration" + assert result["reason"] == "missing_credentials" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "missing_configuration" + assert result["reason"] == "missing_credentials" async def test_zeroconf_abort_if_existing_entry(hass): diff --git a/tests/components/yolink/test_config_flow.py b/tests/components/yolink/test_config_flow.py index 4dd347f4076..5d6bb8fd727 100644 --- a/tests/components/yolink/test_config_flow.py +++ b/tests/components/yolink/test_config_flow.py @@ -25,7 +25,7 @@ async def test_abort_if_no_configuration(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "missing_configuration" + assert result["reason"] == "missing_credentials" async def test_abort_if_existing_entry(hass: HomeAssistant): diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 97e728d022d..248f3b8dbb0 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -114,6 +114,17 @@ async def test_abort_if_no_implementation(hass, flow_handler): assert result["reason"] == "missing_configuration" +async def test_missing_credentials_for_domain(hass, flow_handler): + """Check flow abort for integration supporting application credentials.""" + flow = flow_handler() + flow.hass = hass + + with patch("homeassistant.loader.APPLICATION_CREDENTIALS", [TEST_DOMAIN]): + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "missing_credentials" + + async def test_abort_if_authorization_timeout( hass, flow_handler, local_impl, current_request_with_host ): From f3c582815c21fd76ae1e85baa4d5d870d5cf2191 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 May 2022 09:22:21 -0500 Subject: [PATCH 0631/3516] Convert statistics to use lambda_stmt (#71903) * Convert stats to use lambda_stmt - Since baked queries are now [deprecated in 1.4](https://docs.sqlalchemy.org/en/14/orm/extensions/baked.html#module-sqlalchemy.ext.baked) the next step is to convert these to `lambda_stmt` https://docs.sqlalchemy.org/en/14/core/connections.html#quick-guidelines-for-lambdas * Update homeassistant/components/recorder/statistics.py Co-authored-by: Erik Montnemery --- .../components/recorder/statistics.py | 277 +++++++++--------- homeassistant/components/recorder/util.py | 5 +- 2 files changed, 143 insertions(+), 139 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 732c042d9c2..77f56bc59fa 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -14,11 +14,12 @@ import re from statistics import mean from typing import TYPE_CHECKING, Any, Literal, overload -from sqlalchemy import bindparam, func +from sqlalchemy import bindparam, func, lambda_stmt, select +from sqlalchemy.engine.row import Row from sqlalchemy.exc import SQLAlchemyError, StatementError -from sqlalchemy.ext import baked from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal_column, true +from sqlalchemy.sql.lambdas import StatementLambdaElement import voluptuous as vol from homeassistant.const import ( @@ -50,7 +51,12 @@ from .models import ( process_timestamp, process_timestamp_to_utc_isoformat, ) -from .util import execute, retryable_database_job, session_scope +from .util import ( + execute, + execute_stmt_lambda_element, + retryable_database_job, + session_scope, +) if TYPE_CHECKING: from . import Recorder @@ -120,8 +126,6 @@ QUERY_STATISTIC_META_ID = [ StatisticsMeta.statistic_id, ] -STATISTICS_BAKERY = "recorder_statistics_bakery" - # Convert pressure, temperature and volume statistics from the normalized unit used for # statistics to the unit configured by the user @@ -203,7 +207,6 @@ class ValidationIssue: def async_setup(hass: HomeAssistant) -> None: """Set up the history hooks.""" - hass.data[STATISTICS_BAKERY] = baked.bakery() def _entity_id_changed(event: Event) -> None: """Handle entity_id changed.""" @@ -420,6 +423,36 @@ def delete_duplicates(hass: HomeAssistant, session: Session) -> None: ) +def _compile_hourly_statistics_summary_mean_stmt( + start_time: datetime, end_time: datetime +) -> StatementLambdaElement: + """Generate the summary mean statement for hourly statistics.""" + stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SUMMARY_MEAN)) + stmt += ( + lambda q: q.filter(StatisticsShortTerm.start >= start_time) + .filter(StatisticsShortTerm.start < end_time) + .group_by(StatisticsShortTerm.metadata_id) + .order_by(StatisticsShortTerm.metadata_id) + ) + return stmt + + +def _compile_hourly_statistics_summary_sum_legacy_stmt( + start_time: datetime, end_time: datetime +) -> StatementLambdaElement: + """Generate the legacy sum statement for hourly statistics. + + This is used for databases not supporting row number. + """ + stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SUMMARY_SUM_LEGACY)) + stmt += ( + lambda q: q.filter(StatisticsShortTerm.start >= start_time) + .filter(StatisticsShortTerm.start < end_time) + .order_by(StatisticsShortTerm.metadata_id, StatisticsShortTerm.start.desc()) + ) + return stmt + + def compile_hourly_statistics( instance: Recorder, session: Session, start: datetime ) -> None: @@ -434,20 +467,8 @@ def compile_hourly_statistics( # Compute last hour's average, min, max summary: dict[str, StatisticData] = {} - baked_query = instance.hass.data[STATISTICS_BAKERY]( - lambda session: session.query(*QUERY_STATISTICS_SUMMARY_MEAN) - ) - - 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.group_by(StatisticsShortTerm.metadata_id) - baked_query += lambda q: q.order_by(StatisticsShortTerm.metadata_id) - - stats = execute( - baked_query(session).params(start_time=start_time, end_time=end_time) - ) + stmt = _compile_hourly_statistics_summary_mean_stmt(start_time, end_time) + stats = execute_stmt_lambda_element(session, stmt) if stats: for stat in stats: @@ -493,23 +514,8 @@ def compile_hourly_statistics( "sum": _sum, } else: - baked_query = instance.hass.data[STATISTICS_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) - ) + stmt = _compile_hourly_statistics_summary_sum_legacy_stmt(start_time, end_time) + stats = execute_stmt_lambda_element(session, stmt) if stats: for metadata_id, group in groupby(stats, lambda stat: stat["metadata_id"]): # type: ignore[no-any-return] @@ -669,6 +675,24 @@ def _update_statistics( ) +def _generate_get_metadata_stmt( + statistic_ids: list[str] | tuple[str] | None = None, + statistic_type: Literal["mean"] | Literal["sum"] | None = None, + statistic_source: str | None = None, +) -> StatementLambdaElement: + """Generate a statement to fetch metadata.""" + stmt = lambda_stmt(lambda: select(*QUERY_STATISTIC_META)) + if statistic_ids is not None: + stmt += lambda q: q.where(StatisticsMeta.statistic_id.in_(statistic_ids)) + if statistic_source is not None: + stmt += lambda q: q.where(StatisticsMeta.source == statistic_source) + if statistic_type == "mean": + stmt += lambda q: q.where(StatisticsMeta.has_mean == true()) + elif statistic_type == "sum": + stmt += lambda q: q.where(StatisticsMeta.has_sum == true()) + return stmt + + def get_metadata_with_session( hass: HomeAssistant, session: Session, @@ -686,26 +710,8 @@ def get_metadata_with_session( """ # Fetch metatadata from the database - baked_query = hass.data[STATISTICS_BAKERY]( - lambda session: session.query(*QUERY_STATISTIC_META) - ) - if statistic_ids is not None: - 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, statistic_source=statistic_source - ) - ) + stmt = _generate_get_metadata_stmt(statistic_ids, statistic_type, statistic_source) + result = execute_stmt_lambda_element(session, stmt) if not result: return {} @@ -852,31 +858,6 @@ def list_statistic_ids( ] -def _statistics_during_period_query( - hass: HomeAssistant, - end_time: datetime | None, - statistic_ids: list[str] | None, - baked_query: baked.BakedQuery, - table: type[Statistics | StatisticsShortTerm], -) -> Callable: - """Prepare a database query for statistics during a given period. - - This prepares a baked query, so we don't insert the parameters yet. - """ - baked_query += lambda q: q.filter(table.start >= bindparam("start_time")) - - if end_time is not None: - baked_query += lambda q: q.filter(table.start < bindparam("end_time")) - - if statistic_ids is not None: - baked_query += lambda q: q.filter( - table.metadata_id.in_(bindparam("metadata_ids")) - ) - - baked_query += lambda q: q.order_by(table.metadata_id, table.start) - return baked_query # type: ignore[no-any-return] - - def _reduce_statistics( stats: dict[str, list[dict[str, Any]]], same_period: Callable[[datetime, datetime], bool], @@ -975,6 +956,34 @@ def _reduce_statistics_per_month( return _reduce_statistics(stats, same_month, month_start_end, timedelta(days=31)) +def _statistics_during_period_stmt( + start_time: datetime, + end_time: datetime | None, + statistic_ids: list[str] | None, + metadata_ids: list[int] | None, + table: type[Statistics | StatisticsShortTerm], +) -> StatementLambdaElement: + """Prepare a database query for statistics during a given period. + + This prepares a lambda_stmt query, so we don't insert the parameters yet. + """ + if table == StatisticsShortTerm: + stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SHORT_TERM)) + else: + stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS)) + + stmt += lambda q: q.filter(table.start >= start_time) + + if end_time is not None: + stmt += lambda q: q.filter(table.start < end_time) + + if statistic_ids is not None: + stmt += lambda q: q.filter(table.metadata_id.in_(metadata_ids)) + + stmt += lambda q: q.order_by(table.metadata_id, table.start) + return stmt + + def statistics_during_period( hass: HomeAssistant, start_time: datetime, @@ -999,25 +1008,16 @@ def statistics_during_period( if statistic_ids is not None: metadata_ids = [metadata_id for metadata_id, _ in metadata.values()] - bakery = hass.data[STATISTICS_BAKERY] if period == "5minute": - baked_query = bakery( - lambda session: session.query(*QUERY_STATISTICS_SHORT_TERM) - ) table = StatisticsShortTerm else: - baked_query = bakery(lambda session: session.query(*QUERY_STATISTICS)) table = Statistics - baked_query = _statistics_during_period_query( - hass, end_time, statistic_ids, baked_query, table + stmt = _statistics_during_period_stmt( + start_time, end_time, statistic_ids, metadata_ids, table ) + stats = execute_stmt_lambda_element(session, stmt) - stats = execute( - baked_query(session).params( - start_time=start_time, end_time=end_time, metadata_ids=metadata_ids - ) - ) if not stats: return {} # Return statistics combined with metadata @@ -1044,6 +1044,24 @@ def statistics_during_period( return _reduce_statistics_per_month(result) +def _get_last_statistics_stmt( + metadata_id: int, + number_of_stats: int, + table: type[Statistics | StatisticsShortTerm], +) -> StatementLambdaElement: + """Generate a statement for number_of_stats statistics for a given statistic_id.""" + if table == StatisticsShortTerm: + stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SHORT_TERM)) + else: + stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS)) + stmt += ( + lambda q: q.filter_by(metadata_id=metadata_id) + .order_by(table.metadata_id, table.start.desc()) + .limit(number_of_stats) + ) + return stmt + + def _get_last_statistics( hass: HomeAssistant, number_of_stats: int, @@ -1058,27 +1076,10 @@ def _get_last_statistics( metadata = get_metadata_with_session(hass, session, statistic_ids=statistic_ids) if not metadata: return {} - - bakery = hass.data[STATISTICS_BAKERY] - if table == StatisticsShortTerm: - baked_query = bakery( - lambda session: session.query(*QUERY_STATISTICS_SHORT_TERM) - ) - else: - baked_query = bakery(lambda session: session.query(*QUERY_STATISTICS)) - - baked_query += lambda q: q.filter_by(metadata_id=bindparam("metadata_id")) metadata_id = metadata[statistic_id][0] + stmt = _get_last_statistics_stmt(metadata_id, number_of_stats, table) + stats = execute_stmt_lambda_element(session, stmt) - baked_query += lambda q: q.order_by(table.metadata_id, table.start.desc()) - - baked_query += lambda q: q.limit(bindparam("number_of_stats")) - - stats = execute( - baked_query(session).params( - number_of_stats=number_of_stats, metadata_id=metadata_id - ) - ) if not stats: return {} @@ -1113,14 +1114,36 @@ def get_last_short_term_statistics( ) +def _latest_short_term_statistics_stmt( + metadata_ids: list[int], +) -> StatementLambdaElement: + """Create the statement for finding the latest short term stat rows.""" + stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SHORT_TERM)) + most_recent_statistic_row = ( + select( + StatisticsShortTerm.metadata_id, + func.max(StatisticsShortTerm.start).label("start_max"), + ) + .where(StatisticsShortTerm.metadata_id.in_(metadata_ids)) + .group_by(StatisticsShortTerm.metadata_id) + ).subquery() + stmt += lambda s: s.join( + most_recent_statistic_row, + ( + StatisticsShortTerm.metadata_id # pylint: disable=comparison-with-callable + == most_recent_statistic_row.c.metadata_id + ) + & (StatisticsShortTerm.start == most_recent_statistic_row.c.start_max), + ) + return stmt + + def get_latest_short_term_statistics( hass: HomeAssistant, statistic_ids: list[str], metadata: dict[str, tuple[int, StatisticMetaData]] | None = None, ) -> dict[str, list[dict]]: """Return the latest short term statistics for a list of statistic_ids.""" - # This function doesn't use a baked query, we instead rely on the - # "Transparent SQL Compilation Caching" feature introduced in SQLAlchemy 1.4 with session_scope(hass=hass) as session: # Fetch metadata for the given statistic_ids if not metadata: @@ -1134,24 +1157,8 @@ def get_latest_short_term_statistics( for statistic_id in statistic_ids if statistic_id in metadata ] - most_recent_statistic_row = ( - session.query( - StatisticsShortTerm.metadata_id, - func.max(StatisticsShortTerm.start).label("start_max"), - ) - .filter(StatisticsShortTerm.metadata_id.in_(metadata_ids)) - .group_by(StatisticsShortTerm.metadata_id) - ).subquery() - stats = execute( - session.query(*QUERY_STATISTICS_SHORT_TERM).join( - most_recent_statistic_row, - ( - StatisticsShortTerm.metadata_id # pylint: disable=comparison-with-callable - == most_recent_statistic_row.c.metadata_id - ) - & (StatisticsShortTerm.start == most_recent_statistic_row.c.start_max), - ) - ) + stmt = _latest_short_term_statistics_stmt(metadata_ids) + stats = execute_stmt_lambda_element(session, stmt) if not stats: return {} @@ -1203,7 +1210,7 @@ def _statistics_at_time( def _sorted_statistics_to_dict( hass: HomeAssistant, session: Session, - stats: list, + stats: Iterable[Row], statistic_ids: list[str] | None, _metadata: dict[str, tuple[int, StatisticMetaData]], convert_units: bool, @@ -1215,7 +1222,7 @@ def _sorted_statistics_to_dict( result: dict = defaultdict(list) units = hass.config.units metadata = dict(_metadata.values()) - need_stat_at_start_time = set() + need_stat_at_start_time: set[int] = set() stats_at_start_time = {} def no_conversion(val: Any, _: Any) -> float | None: diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index e464ac4126b..f086119f7f9 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -20,7 +20,6 @@ from sqlalchemy import text from sqlalchemy.engine.cursor import CursorFetchStrategy from sqlalchemy.engine.row import Row from sqlalchemy.exc import OperationalError, SQLAlchemyError -from sqlalchemy.ext.baked import Result from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session from sqlalchemy.sql.lambdas import StatementLambdaElement @@ -123,9 +122,7 @@ def commit(session: Session, work: Any) -> bool: def execute( - qry: Query | Result, - to_native: bool = False, - validate_entity_ids: bool = True, + qry: Query, to_native: bool = False, validate_entity_ids: bool = True ) -> list[Row]: """Query the database and convert the objects to HA native form. From 037f6947d88f0754b15d156180cdffb053a25b1a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 May 2022 16:52:46 +0200 Subject: [PATCH 0632/3516] Fail recorder setup with unsupported dialect or version (#70888) --- homeassistant/components/recorder/core.py | 3 ++ homeassistant/components/recorder/models.py | 4 ++ homeassistant/components/recorder/util.py | 32 ++++++++-------- tests/components/recorder/test_util.py | 42 ++++++++++++--------- 4 files changed, 47 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 91dc0a27ead..5a3a41568a1 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -56,6 +56,7 @@ from .models import ( StatisticData, StatisticMetaData, StatisticsRuns, + UnsupportedDialect, process_timestamp, ) from .pool import POOL_SIZE, MutexPool, RecorderPool @@ -606,6 +607,8 @@ class Recorder(threading.Thread): try: self._setup_connection() return migration.get_schema_version(self.get_session) + except UnsupportedDialect: + break except Exception as err: # pylint: disable=broad-except _LOGGER.exception( "Error during connection setup: %s (retrying in %s seconds)", diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 9d16541e398..4dabd7899e0 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -123,6 +123,10 @@ EVENT_ORIGIN_ORDER = [EventOrigin.local, EventOrigin.remote] EVENT_ORIGIN_TO_IDX = {origin: idx for idx, origin in enumerate(EVENT_ORIGIN_ORDER)} +class UnsupportedDialect(Exception): + """The dialect or its version is not supported.""" + + class Events(Base): # type: ignore[misc,valid-type] """Event history data.""" diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index f086119f7f9..ce8812a653e 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -34,6 +34,7 @@ from .models import ( TABLE_SCHEMA_CHANGES, TABLES_TO_CHECK, RecorderRuns, + UnsupportedDialect, process_timestamp, ) @@ -328,29 +329,31 @@ def query_on_connection(dbapi_connection: Any, statement: str) -> Any: return result -def _warn_unsupported_dialect(dialect_name: str) -> None: +def _fail_unsupported_dialect(dialect_name: str) -> None: """Warn about unsupported database version.""" - _LOGGER.warning( + _LOGGER.error( "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", + "Starting with Home Assistant 2022.6 this prevents the recorder from " + "starting. Please migrate your database to a supported software", dialect_name, "MariaDB ≥ 10.3, MySQL ≥ 8.0, PostgreSQL ≥ 12, SQLite ≥ 3.31.0", ) + raise UnsupportedDialect -def _warn_unsupported_version( +def _fail_unsupported_version( server_version: str, dialect_name: str, minimum_version: str ) -> None: """Warn about unsupported database version.""" - _LOGGER.warning( + _LOGGER.error( "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", + "Starting with Home Assistant 2022.6 this prevents the recorder from " + "starting. Please upgrade your database software", server_version, dialect_name, minimum_version, ) + raise UnsupportedDialect def _extract_version_from_server_response( @@ -398,9 +401,6 @@ def setup_connection_for_dialect( first_connection: bool, ) -> None: """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 - # never needs to be setup again. if dialect_name == SupportedDialect.SQLITE: if first_connection: old_isolation = dbapi_connection.isolation_level @@ -419,7 +419,7 @@ def setup_connection_for_dialect( False ) if not version or version < MIN_VERSION_SQLITE: - _warn_unsupported_version( + _fail_unsupported_version( version or version_string, "SQLite", MIN_VERSION_SQLITE ) @@ -453,7 +453,7 @@ def setup_connection_for_dialect( False ) if not version or version < MIN_VERSION_MARIA_DB: - _warn_unsupported_version( + _fail_unsupported_version( version or version_string, "MariaDB", MIN_VERSION_MARIA_DB ) else: @@ -462,7 +462,7 @@ def setup_connection_for_dialect( False ) if not version or version < MIN_VERSION_MYSQL: - _warn_unsupported_version( + _fail_unsupported_version( version or version_string, "MySQL", MIN_VERSION_MYSQL ) @@ -473,12 +473,12 @@ def setup_connection_for_dialect( version_string = result[0][0] version = _extract_version_from_server_response(version_string) if not version or version < MIN_VERSION_PGSQL: - _warn_unsupported_version( + _fail_unsupported_version( version or version_string, "PostgreSQL", MIN_VERSION_PGSQL ) else: - _warn_unsupported_dialect(dialect_name) + _fail_unsupported_dialect(dialect_name) def end_incomplete_runs(session: Session, start_time: datetime) -> None: diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index fea255647ec..e7685a93ff2 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -14,7 +14,7 @@ from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components import recorder from homeassistant.components.recorder import history, util from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX -from homeassistant.components.recorder.models import RecorderRuns +from homeassistant.components.recorder.models import RecorderRuns, UnsupportedDialect from homeassistant.components.recorder.util import ( end_incomplete_runs, is_second_sunday, @@ -168,10 +168,8 @@ async def test_last_run_was_recently_clean( @pytest.mark.parametrize( "mysql_version, db_supports_row_number", [ - ("10.2.0-MariaDB", True), - ("10.1.0-MariaDB", False), - ("5.8.0", True), - ("5.7.0", False), + ("10.3.0-MariaDB", True), + ("8.0.0", True), ], ) def test_setup_connection_for_dialect_mysql(mysql_version, db_supports_row_number): @@ -207,8 +205,7 @@ def test_setup_connection_for_dialect_mysql(mysql_version, db_supports_row_numbe @pytest.mark.parametrize( "sqlite_version, db_supports_row_number", [ - ("3.25.0", True), - ("3.24.0", False), + ("3.31.0", True), ], ) def test_setup_connection_for_dialect_sqlite(sqlite_version, db_supports_row_number): @@ -255,8 +252,7 @@ def test_setup_connection_for_dialect_sqlite(sqlite_version, db_supports_row_num @pytest.mark.parametrize( "sqlite_version, db_supports_row_number", [ - ("3.25.0", True), - ("3.24.0", False), + ("3.31.0", True), ], ) def test_setup_connection_for_dialect_sqlite_zero_commit_interval( @@ -319,7 +315,7 @@ def test_setup_connection_for_dialect_sqlite_zero_commit_interval( ), ], ) -def test_warn_outdated_mysql(caplog, mysql_version, message): +def test_fail_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 = [] @@ -340,7 +336,10 @@ def test_warn_outdated_mysql(caplog, mysql_version, message): dbapi_connection = MagicMock(cursor=_make_cursor_mock) - util.setup_connection_for_dialect(instance_mock, "mysql", dbapi_connection, True) + with pytest.raises(UnsupportedDialect): + util.setup_connection_for_dialect( + instance_mock, "mysql", dbapi_connection, True + ) assert message in caplog.text @@ -395,7 +394,7 @@ def test_supported_mysql(caplog, mysql_version): ), ], ) -def test_warn_outdated_pgsql(caplog, pgsql_version, message): +def test_fail_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 = [] @@ -416,9 +415,10 @@ def test_warn_outdated_pgsql(caplog, pgsql_version, message): dbapi_connection = MagicMock(cursor=_make_cursor_mock) - util.setup_connection_for_dialect( - instance_mock, "postgresql", dbapi_connection, True - ) + with pytest.raises(UnsupportedDialect): + util.setup_connection_for_dialect( + instance_mock, "postgresql", dbapi_connection, True + ) assert message in caplog.text @@ -472,7 +472,7 @@ def test_supported_pgsql(caplog, pgsql_version): ), ], ) -def test_warn_outdated_sqlite(caplog, sqlite_version, message): +def test_fail_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 = [] @@ -493,7 +493,10 @@ def test_warn_outdated_sqlite(caplog, sqlite_version, message): dbapi_connection = MagicMock(cursor=_make_cursor_mock) - util.setup_connection_for_dialect(instance_mock, "sqlite", dbapi_connection, True) + with pytest.raises(UnsupportedDialect): + util.setup_connection_for_dialect( + instance_mock, "sqlite", dbapi_connection, True + ) assert message in caplog.text @@ -544,7 +547,10 @@ def test_warn_unsupported_dialect(caplog, dialect, message): instance_mock = MagicMock() dbapi_connection = MagicMock() - util.setup_connection_for_dialect(instance_mock, dialect, dbapi_connection, True) + with pytest.raises(UnsupportedDialect): + util.setup_connection_for_dialect( + instance_mock, dialect, dbapi_connection, True + ) assert message in caplog.text From bd78eec73257d85a291f9aa7660f88b030ea5053 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 May 2022 10:42:14 -0500 Subject: [PATCH 0633/3516] Fix reversed raise_on_progress in baf config_flow (#72094) --- homeassistant/components/baf/config_flow.py | 6 +- tests/components/baf/__init__.py | 36 ++++++++++ tests/components/baf/test_config_flow.py | 74 ++++++++++++++++----- 3 files changed, 98 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/baf/config_flow.py b/homeassistant/components/baf/config_flow.py index 1c2873eb759..2326d30937b 100644 --- a/homeassistant/components/baf/config_flow.py +++ b/homeassistant/components/baf/config_flow.py @@ -54,7 +54,7 @@ class BAFFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): uuid = properties["uuid"] model = properties["model"] name = properties["name"] - await self.async_set_unique_id(uuid, raise_on_progress=False) + await self.async_set_unique_id(uuid) self._abort_if_unique_id_configured(updates={CONF_IP_ADDRESS: ip_address}) self.discovery = BAFDiscovery(ip_address, name, uuid, model) return await self.async_step_discovery_confirm() @@ -98,7 +98,9 @@ class BAFFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) errors["base"] = "unknown" else: - await self.async_set_unique_id(device.dns_sd_uuid) + await self.async_set_unique_id( + device.dns_sd_uuid, raise_on_progress=False + ) self._abort_if_unique_id_configured( updates={CONF_IP_ADDRESS: ip_address} ) diff --git a/tests/components/baf/__init__.py b/tests/components/baf/__init__.py index e0432ed643a..4e435dc1a2e 100644 --- a/tests/components/baf/__init__.py +++ b/tests/components/baf/__init__.py @@ -1 +1,37 @@ """Tests for the Big Ass Fans integration.""" + + +import asyncio + +from aiobafi6 import Device + +MOCK_UUID = "1234" +MOCK_NAME = "Living Room Fan" + + +class MockBAFDevice(Device): + """A simple mock for a BAF Device.""" + + def __init__(self, async_wait_available_side_effect=None): + """Init simple mock.""" + self._async_wait_available_side_effect = async_wait_available_side_effect + + @property + def dns_sd_uuid(self): + """Mock the unique id.""" + return MOCK_UUID + + @property + def name(self): + """Mock the name of the device.""" + return MOCK_NAME + + async def async_wait_available(self): + """Mock async_wait_available.""" + if self._async_wait_available_side_effect: + raise self._async_wait_available_side_effect + return + + def async_run(self): + """Mock async_run.""" + return asyncio.Future() diff --git a/tests/components/baf/test_config_flow.py b/tests/components/baf/test_config_flow.py index 687a871ed4c..77a83a9673b 100644 --- a/tests/components/baf/test_config_flow.py +++ b/tests/components/baf/test_config_flow.py @@ -12,9 +12,20 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) +from . import MOCK_NAME, MOCK_UUID, MockBAFDevice + from tests.common import MockConfigEntry +def _patch_device_config_flow(side_effect=None): + """Mock out the BAF Device object.""" + + def _create_mock_baf(*args, **kwargs): + return MockBAFDevice(side_effect) + + return patch("homeassistant.components.baf.config_flow.Device", _create_mock_baf) + + async def test_form_user(hass): """Test we get the user form.""" @@ -24,9 +35,7 @@ async def test_form_user(hass): assert result["type"] == "form" assert result["errors"] == {} - with patch("homeassistant.components.baf.config_flow.Device.async_run",), patch( - "homeassistant.components.baf.config_flow.Device.async_wait_available", - ), patch( + with _patch_device_config_flow(), patch( "homeassistant.components.baf.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -37,7 +46,7 @@ async def test_form_user(hass): await hass.async_block_till_done() assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "127.0.0.1" + assert result2["title"] == MOCK_NAME assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.1"} assert len(mock_setup_entry.mock_calls) == 1 @@ -48,10 +57,7 @@ async def test_form_cannot_connect(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("homeassistant.components.baf.config_flow.Device.async_run",), patch( - "homeassistant.components.baf.config_flow.Device.async_wait_available", - side_effect=asyncio.TimeoutError, - ): + with _patch_device_config_flow(asyncio.TimeoutError): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_IP_ADDRESS: "127.0.0.1"}, @@ -67,10 +73,7 @@ async def test_form_unknown_exception(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("homeassistant.components.baf.config_flow.Device.async_run",), patch( - "homeassistant.components.baf.config_flow.Device.async_wait_available", - side_effect=Exception, - ): + with _patch_device_config_flow(Exception): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_IP_ADDRESS: "127.0.0.1"}, @@ -92,7 +95,7 @@ async def test_zeroconf_discovery(hass): hostname="mock_hostname", name="testfan", port=None, - properties={"name": "My Fan", "model": "Haiku", "uuid": "1234"}, + properties={"name": "My Fan", "model": "Haiku", "uuid": MOCK_UUID}, type="mock_type", ), ) @@ -118,7 +121,7 @@ async def test_zeroconf_discovery(hass): async def test_zeroconf_updates_existing_ip(hass): """Test we can setup from zeroconf discovery.""" entry = MockConfigEntry( - domain=DOMAIN, data={CONF_IP_ADDRESS: "127.0.0.2"}, unique_id="1234" + domain=DOMAIN, data={CONF_IP_ADDRESS: "127.0.0.2"}, unique_id=MOCK_UUID ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -130,7 +133,7 @@ async def test_zeroconf_updates_existing_ip(hass): hostname="mock_hostname", name="testfan", port=None, - properties={"name": "My Fan", "model": "Haiku", "uuid": "1234"}, + properties={"name": "My Fan", "model": "Haiku", "uuid": MOCK_UUID}, type="mock_type", ), ) @@ -150,9 +153,48 @@ async def test_zeroconf_rejects_ipv6(hass): hostname="mock_hostname", name="testfan", port=None, - properties={"name": "My Fan", "model": "Haiku", "uuid": "1234"}, + properties={"name": "My Fan", "model": "Haiku", "uuid": MOCK_UUID}, type="mock_type", ), ) assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "ipv6_not_supported" + + +async def test_user_flow_is_not_blocked_by_discovery(hass): + """Test we can setup from the user flow when there is also a discovery.""" + discovery_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="127.0.0.1", + addresses=["127.0.0.1"], + hostname="mock_hostname", + name="testfan", + port=None, + properties={"name": "My Fan", "model": "Haiku", "uuid": MOCK_UUID}, + type="mock_type", + ), + ) + assert discovery_result["type"] == RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with _patch_device_config_flow(), patch( + "homeassistant.components.baf.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_IP_ADDRESS: "127.0.0.1"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == MOCK_NAME + assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.1"} + assert len(mock_setup_entry.mock_calls) == 1 From 50ca14538a147b3c5be41c02ae05b389285998ae Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 17:58:28 +0200 Subject: [PATCH 0634/3516] Cleanup deprecated async_get_registry in core (#72087) --- .../binary_sensor/device_condition.py | 14 ++++---- .../components/device_tracker/legacy.py | 11 +++--- homeassistant/components/hassio/__init__.py | 35 ++++++++++--------- .../components/sensor/device_condition.py | 14 ++++---- homeassistant/helpers/entity_registry.py | 2 +- 5 files changed, 40 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index 1f16956ff41..4b3aa70d716 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -12,12 +12,12 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import condition, config_validation as cv -from homeassistant.helpers.entity import get_device_class -from homeassistant.helpers.entity_registry import ( - async_entries_for_device, - async_get_registry, +from homeassistant.helpers import ( + condition, + config_validation as cv, + entity_registry as er, ) +from homeassistant.helpers.entity import get_device_class from homeassistant.helpers.typing import ConfigType from . import DOMAIN, BinarySensorDeviceClass @@ -268,10 +268,10 @@ async def async_get_conditions( ) -> list[dict[str, str]]: """List device conditions.""" conditions: list[dict[str, str]] = [] - entity_registry = await async_get_registry(hass) + entity_registry = er.async_get(hass) entries = [ entry - for entry in async_entries_for_device(entity_registry, device_id) + for entry in er.async_entries_for_device(entity_registry, device_id) if entry.domain == DOMAIN ] diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 01fc8a444e1..87026fc32ff 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -30,9 +30,12 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_per_platform, discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.helpers import ( + config_per_platform, + config_validation as cv, + discovery, + entity_registry as er, +) from homeassistant.helpers.event import ( async_track_time_interval, async_track_utc_time_change, @@ -487,7 +490,7 @@ class DeviceTracker: This method is a coroutine. """ - registry = await async_get_registry(self.hass) + registry = er.async_get(self.hass) if mac is None and dev_id is None: raise HomeAssistantError("Neither mac or device id passed in") if mac is not None: diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 3ded34bd6f1..7e976536691 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -33,13 +33,12 @@ from homeassistant.core import ( callback, ) from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import config_validation as cv, recorder -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import ( - DeviceEntryType, - DeviceRegistry, - async_get_registry, +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + recorder, ) +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.storage import Store @@ -715,7 +714,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - dev_reg = await async_get_registry(hass) + dev_reg = dr.async_get(hass) coordinator = HassioDataUpdateCoordinator(hass, entry, dev_reg) hass.data[ADDONS_COORDINATOR] = coordinator await coordinator.async_config_entry_first_refresh() @@ -737,7 +736,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @callback def async_register_addons_in_dev_reg( - entry_id: str, dev_reg: DeviceRegistry, addons: list[dict[str, Any]] + entry_id: str, dev_reg: dr.DeviceRegistry, addons: list[dict[str, Any]] ) -> None: """Register addons in the device registry.""" for addon in addons: @@ -746,7 +745,7 @@ def async_register_addons_in_dev_reg( model=SupervisorEntityModel.ADDON, sw_version=addon[ATTR_VERSION], name=addon[ATTR_NAME], - entry_type=DeviceEntryType.SERVICE, + entry_type=dr.DeviceEntryType.SERVICE, configuration_url=f"homeassistant://hassio/addon/{addon[ATTR_SLUG]}", ) if manufacturer := addon.get(ATTR_REPOSITORY) or addon.get(ATTR_URL): @@ -756,7 +755,7 @@ def async_register_addons_in_dev_reg( @callback def async_register_os_in_dev_reg( - entry_id: str, dev_reg: DeviceRegistry, os_dict: dict[str, Any] + entry_id: str, dev_reg: dr.DeviceRegistry, os_dict: dict[str, Any] ) -> None: """Register OS in the device registry.""" params = DeviceInfo( @@ -765,7 +764,7 @@ def async_register_os_in_dev_reg( model=SupervisorEntityModel.OS, sw_version=os_dict[ATTR_VERSION], name="Home Assistant Operating System", - entry_type=DeviceEntryType.SERVICE, + entry_type=dr.DeviceEntryType.SERVICE, ) dev_reg.async_get_or_create(config_entry_id=entry_id, **params) @@ -773,7 +772,7 @@ def async_register_os_in_dev_reg( @callback def async_register_core_in_dev_reg( entry_id: str, - dev_reg: DeviceRegistry, + dev_reg: dr.DeviceRegistry, core_dict: dict[str, Any], ) -> None: """Register OS in the device registry.""" @@ -783,7 +782,7 @@ def async_register_core_in_dev_reg( model=SupervisorEntityModel.CORE, sw_version=core_dict[ATTR_VERSION], name="Home Assistant Core", - entry_type=DeviceEntryType.SERVICE, + entry_type=dr.DeviceEntryType.SERVICE, ) dev_reg.async_get_or_create(config_entry_id=entry_id, **params) @@ -791,7 +790,7 @@ def async_register_core_in_dev_reg( @callback def async_register_supervisor_in_dev_reg( entry_id: str, - dev_reg: DeviceRegistry, + dev_reg: dr.DeviceRegistry, supervisor_dict: dict[str, Any], ) -> None: """Register OS in the device registry.""" @@ -801,13 +800,15 @@ def async_register_supervisor_in_dev_reg( model=SupervisorEntityModel.SUPERVIOSR, sw_version=supervisor_dict[ATTR_VERSION], name="Home Assistant Supervisor", - entry_type=DeviceEntryType.SERVICE, + entry_type=dr.DeviceEntryType.SERVICE, ) dev_reg.async_get_or_create(config_entry_id=entry_id, **params) @callback -def async_remove_addons_from_dev_reg(dev_reg: DeviceRegistry, addons: set[str]) -> None: +def async_remove_addons_from_dev_reg( + dev_reg: dr.DeviceRegistry, addons: set[str] +) -> None: """Remove addons from the device registry.""" for addon_slug in addons: if dev := dev_reg.async_get_device({(DOMAIN, addon_slug)}): @@ -818,7 +819,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): """Class to retrieve Hass.io status.""" def __init__( - self, hass: HomeAssistant, config_entry: ConfigEntry, dev_reg: DeviceRegistry + self, hass: HomeAssistant, config_entry: ConfigEntry, dev_reg: dr.DeviceRegistry ) -> None: """Initialize coordinator.""" super().__init__( diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 70a265e3b25..a77105764d5 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -15,16 +15,16 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import condition, config_validation as cv +from homeassistant.helpers import ( + condition, + config_validation as cv, + entity_registry as er, +) from homeassistant.helpers.entity import ( get_capability, get_device_class, get_unit_of_measurement, ) -from homeassistant.helpers.entity_registry import ( - async_entries_for_device, - async_get_registry, -) from homeassistant.helpers.typing import ConfigType from . import ATTR_STATE_CLASS, DOMAIN, SensorDeviceClass @@ -141,10 +141,10 @@ async def async_get_conditions( ) -> list[dict[str, str]]: """List device conditions.""" conditions: list[dict[str, str]] = [] - entity_registry = await async_get_registry(hass) + entity_registry = er.async_get(hass) entries = [ entry - for entry in async_entries_for_device(entity_registry, device_id) + for entry in er.async_entries_for_device(entity_registry, device_id) if entry.domain == DOMAIN ] diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 64ab0323f6c..2c87c4cbdd2 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -995,7 +995,7 @@ async def async_migrate_entries( entry_callback: Callable[[RegistryEntry], dict[str, Any] | None], ) -> None: """Migrator of unique IDs.""" - ent_reg = await async_get_registry(hass) + ent_reg = async_get(hass) for entry in ent_reg.entities.values(): if entry.config_entry_id != config_entry_id: From f4b252a51d4c450821920e696339c8122180b531 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 18:43:43 +0200 Subject: [PATCH 0635/3516] Cleanup hue async methods which are not awaiting (#72097) --- homeassistant/components/hue/device_trigger.py | 4 ++-- homeassistant/components/hue/v1/device_trigger.py | 4 +++- homeassistant/components/hue/v2/device_trigger.py | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hue/device_trigger.py b/homeassistant/components/hue/device_trigger.py index a4b545aa141..29df1fd2476 100644 --- a/homeassistant/components/hue/device_trigger.py +++ b/homeassistant/components/hue/device_trigger.py @@ -99,5 +99,5 @@ async def async_get_triggers(hass: "HomeAssistant", device_id: str): bridge: HueBridge = hass.data[DOMAIN][conf_entry_id] if bridge.api_version == 1: - return await async_get_triggers_v1(bridge, device_entry) - return await async_get_triggers_v2(bridge, device_entry) + return async_get_triggers_v1(bridge, device_entry) + return async_get_triggers_v2(bridge, device_entry) diff --git a/homeassistant/components/hue/v1/device_trigger.py b/homeassistant/components/hue/v1/device_trigger.py index d6b471b7257..7b58bf42089 100644 --- a/homeassistant/components/hue/v1/device_trigger.py +++ b/homeassistant/components/hue/v1/device_trigger.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_TYPE, CONF_UNIQUE_ID, ) +from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceEntry from ..const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN @@ -160,7 +161,8 @@ async def async_attach_trigger(bridge, device_entry, config, action, automation_ ) -async def async_get_triggers(bridge: "HueBridge", device: DeviceEntry): +@callback +def async_get_triggers(bridge: "HueBridge", device: DeviceEntry): """Return device triggers for device on `v1` bridge. Make sure device is a supported remote model. diff --git a/homeassistant/components/hue/v2/device_trigger.py b/homeassistant/components/hue/v2/device_trigger.py index cab21b63d6d..8c5da8febb9 100644 --- a/homeassistant/components/hue/v2/device_trigger.py +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -119,7 +119,8 @@ async def async_attach_trigger( ) -async def async_get_triggers(bridge: "HueBridge", device_entry: DeviceEntry): +@callback +def async_get_triggers(bridge: HueBridge, device_entry: DeviceEntry): """Return device triggers for device on `v2` bridge.""" api: HueBridgeV2 = bridge.api From 8ff0ced846e505a0c33a848e21b19820861e6884 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 May 2022 04:46:13 +1200 Subject: [PATCH 0636/3516] Initial implementation of ESPHome media players (#72047) Co-authored-by: Paulus Schoutsen Co-authored-by: Franck Nijhof --- .coveragerc | 1 + .../components/esphome/entry_data.py | 2 + .../components/esphome/media_player.py | 151 ++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 homeassistant/components/esphome/media_player.py diff --git a/.coveragerc b/.coveragerc index da12b41d222..bd7aa405b94 100644 --- a/.coveragerc +++ b/.coveragerc @@ -314,6 +314,7 @@ omit = homeassistant/components/esphome/fan.py homeassistant/components/esphome/light.py homeassistant/components/esphome/lock.py + homeassistant/components/esphome/media_player.py homeassistant/components/esphome/number.py homeassistant/components/esphome/select.py homeassistant/components/esphome/sensor.py diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index c00073b4432..4c5a94afe0f 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -20,6 +20,7 @@ from aioesphomeapi import ( FanInfo, LightInfo, LockInfo, + MediaPlayerInfo, NumberInfo, SelectInfo, SensorInfo, @@ -46,6 +47,7 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { FanInfo: "fan", LightInfo: "light", LockInfo: "lock", + MediaPlayerInfo: "media_player", NumberInfo: "number", SelectInfo: "select", SensorInfo: "sensor", diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py new file mode 100644 index 00000000000..6e83d12a427 --- /dev/null +++ b/homeassistant/components/esphome/media_player.py @@ -0,0 +1,151 @@ +"""Support for ESPHome media players.""" +from __future__ import annotations + +from typing import Any + +from aioesphomeapi import ( + MediaPlayerCommand, + MediaPlayerEntityState, + MediaPlayerInfo, + MediaPlayerState, +) + +from homeassistant.components import media_source +from homeassistant.components.media_player import ( + MediaPlayerDeviceClass, + MediaPlayerEntity, +) +from homeassistant.components.media_player.browse_media import ( + BrowseMedia, + async_process_play_media_url, +) +from homeassistant.components.media_player.const import MediaPlayerEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import EsphomeEntity, EsphomeEnumMapper, platform_async_setup_entry + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up esphome media players based on a config entry.""" + await platform_async_setup_entry( + hass, + entry, + async_add_entities, + component_key="media_player", + info_type=MediaPlayerInfo, + entity_type=EsphomeMediaPlayer, + state_type=MediaPlayerEntityState, + ) + + +_STATES: EsphomeEnumMapper[MediaPlayerState, str] = EsphomeEnumMapper( + { + MediaPlayerState.IDLE: STATE_IDLE, + MediaPlayerState.PLAYING: STATE_PLAYING, + MediaPlayerState.PAUSED: STATE_PAUSED, + } +) + + +class EsphomeMediaPlayer( + EsphomeEntity[MediaPlayerInfo, MediaPlayerEntityState], MediaPlayerEntity +): + """A media player implementation for esphome.""" + + _attr_device_class = MediaPlayerDeviceClass.SPEAKER + + @property + def state(self) -> str | None: + """Return current state.""" + return _STATES.from_esphome(self._state.state) + + @property + def is_volume_muted(self) -> bool: + """Return true if volume is muted.""" + return self._state.muted + + @property + def volume_level(self) -> float | None: + """Volume level of the media player (0..1).""" + return self._state.volume + + @property + def supported_features(self) -> int: + """Flag supported features.""" + flags = ( + MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.BROWSE_MEDIA + | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.VOLUME_MUTE + ) + if self._static_info.supports_pause: + flags |= MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY + return flags + + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: + """Send the play command with media url to the media player.""" + if media_source.is_media_source_id(media_id): + sourced_media = await media_source.async_resolve_media(self.hass, media_id) + media_id = sourced_media.url + + media_id = async_process_play_media_url(self.hass, media_id) + + await self._client.media_player_command( + self._static_info.key, + media_url=media_id, + ) + + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: + """Implement the websocket media browsing helper.""" + return await media_source.async_browse_media( + self.hass, + media_content_id, + content_filter=lambda item: item.media_content_type.startswith("audio/"), + ) + + async def async_set_volume_level(self, volume: float) -> None: + """Set volume level, range 0..1.""" + await self._client.media_player_command( + self._static_info.key, + volume=volume, + ) + + async def async_media_pause(self) -> None: + """Send pause command.""" + await self._client.media_player_command( + self._static_info.key, + command=MediaPlayerCommand.PAUSE, + ) + + async def async_media_play(self) -> None: + """Send play command.""" + await self._client.media_player_command( + self._static_info.key, + command=MediaPlayerCommand.PLAY, + ) + + async def async_media_stop(self) -> None: + """Send stop command.""" + await self._client.media_player_command( + self._static_info.key, + command=MediaPlayerCommand.STOP, + ) + + async def async_mute_volume(self, mute: bool) -> None: + """Mute the volume.""" + await self._client.media_player_command( + self._static_info.key, + command=MediaPlayerCommand.MUTE if mute else MediaPlayerCommand.UNMUTE, + ) From 8f7f3f328e01c798c271dab8b744c7641f339c1c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 19:01:56 +0200 Subject: [PATCH 0637/3516] Suppress Upnp error in SamsungTV resubscribe (#71925) * Suppress Upnp error in SamsungTV resubscribe * Supress UpnpCommunicationError instead * Log resubscribe errors * Add tests * Add exc_info --- .../components/samsungtv/media_player.py | 5 +- .../components/samsungtv/test_media_player.py | 48 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 0599115774e..423a778011d 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -12,6 +12,7 @@ from async_upnp_client.client import UpnpDevice, UpnpService, UpnpStateVariable from async_upnp_client.client_factory import UpnpFactory from async_upnp_client.exceptions import ( UpnpActionResponseError, + UpnpCommunicationError, UpnpConnectionError, UpnpError, UpnpResponseError, @@ -309,8 +310,10 @@ class SamsungTVDevice(MediaPlayerEntity): async def _async_resubscribe_dmr(self) -> None: assert self._dmr_device - with contextlib.suppress(UpnpConnectionError): + try: await self._dmr_device.async_subscribe_services(auto_resubscribe=True) + except UpnpCommunicationError as err: + LOGGER.debug("Device rejected re-subscription: %r", err, exc_info=True) async def _async_shutdown_dmr(self) -> None: """Handle removal.""" diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 56cce6ebfbf..e548822f1d0 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -6,6 +6,8 @@ from unittest.mock import DEFAULT as DEFAULT_MOCK, AsyncMock, Mock, call, patch from async_upnp_client.exceptions import ( UpnpActionResponseError, + UpnpCommunicationError, + UpnpConnectionError, UpnpError, UpnpResponseError, ) @@ -1507,3 +1509,49 @@ async def test_upnp_re_subscribe_events( assert state.state == STATE_ON assert dmr_device.async_subscribe_services.call_count == 2 assert dmr_device.async_unsubscribe_services.call_count == 1 + + +@pytest.mark.usefixtures("rest_api", "upnp_notify_server") +@pytest.mark.parametrize( + "error", + {UpnpConnectionError(), UpnpCommunicationError(), UpnpResponseError(status=400)}, +) +async def test_upnp_failed_re_subscribe_events( + hass: HomeAssistant, + remotews: Mock, + dmr_device: Mock, + mock_now: datetime, + caplog: pytest.LogCaptureFixture, + error: Exception, +) -> None: + """Test for Upnp event feedback.""" + await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + assert dmr_device.async_subscribe_services.call_count == 1 + assert dmr_device.async_unsubscribe_services.call_count == 0 + + with patch.object( + remotews, "start_listening", side_effect=WebSocketException("Boom") + ), patch.object(remotews, "is_alive", return_value=False): + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + assert dmr_device.async_subscribe_services.call_count == 1 + assert dmr_device.async_unsubscribe_services.call_count == 1 + + next_update = mock_now + timedelta(minutes=10) + with patch("homeassistant.util.dt.utcnow", return_value=next_update), patch.object( + dmr_device, "async_subscribe_services", side_effect=error + ): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + assert "Device rejected re-subscription" in caplog.text From c74b2419495d1c29112dc745c05c6c11a9dd313a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 May 2022 12:28:04 -0500 Subject: [PATCH 0638/3516] Include initial state in history_stats count (#71952) --- .../components/history_stats/data.py | 14 +++++----- .../components/history_stats/sensor.py | 2 +- tests/components/history_stats/test_sensor.py | 26 +++++++++---------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 3d21cca6b6d..8153557422d 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -19,7 +19,7 @@ class HistoryStatsState: """The current stats of the history stats.""" hours_matched: float | None - changes_to_match_state: int | None + match_count: int | None period: tuple[datetime.datetime, datetime.datetime] @@ -121,14 +121,12 @@ class HistoryStats: self._state = HistoryStatsState(None, None, self._period) return self._state - hours_matched, changes_to_match_state = self._async_compute_hours_and_changes( + hours_matched, match_count = self._async_compute_hours_and_changes( now_timestamp, current_period_start_timestamp, current_period_end_timestamp, ) - self._state = HistoryStatsState( - hours_matched, changes_to_match_state, self._period - ) + self._state = HistoryStatsState(hours_matched, match_count, self._period) return self._state def _update_from_database( @@ -156,7 +154,7 @@ class HistoryStats: ) last_state_change_timestamp = start_timestamp elapsed = 0.0 - changes_to_match_state = 0 + match_count = 1 if previous_state_matches else 0 # Make calculations for item in self._history_current_period: @@ -166,7 +164,7 @@ class HistoryStats: if previous_state_matches: elapsed += state_change_timestamp - last_state_change_timestamp elif current_state_matches: - changes_to_match_state += 1 + match_count += 1 previous_state_matches = current_state_matches last_state_change_timestamp = state_change_timestamp @@ -178,4 +176,4 @@ class HistoryStats: # Save value in hours hours_matched = elapsed / 3600 - return hours_matched, changes_to_match_state + return hours_matched, match_count diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index b3e64106d9f..b0ce1a8fca5 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -166,4 +166,4 @@ class HistoryStatsSensor(HistoryStatsSensorBase): elif self._type == CONF_TYPE_RATIO: self._attr_native_value = pretty_ratio(state.hours_matched, state.period) elif self._type == CONF_TYPE_COUNT: - self._attr_native_value = state.changes_to_match_state + self._attr_native_value = state.match_count diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 4f56edaa291..b375a8f63c4 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -438,7 +438,7 @@ async def test_measure(hass, recorder_mock): assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.83" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -519,7 +519,7 @@ async def test_async_on_entire_period(hass, recorder_mock): assert hass.states.get("sensor.on_sensor1").state == "1.0" assert hass.states.get("sensor.on_sensor2").state == "1.0" - assert hass.states.get("sensor.on_sensor3").state == "0" + assert hass.states.get("sensor.on_sensor3").state == "1" assert hass.states.get("sensor.on_sensor4").state == "100.0" @@ -886,7 +886,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "0.0" assert hass.states.get("sensor.sensor2").state == "0.0" - assert hass.states.get("sensor.sensor3").state == "0" + assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor4").state == "0.0" one_hour_in = start_time + timedelta(minutes=60) @@ -896,7 +896,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.0" assert hass.states.get("sensor.sensor2").state == "1.0" - assert hass.states.get("sensor.sensor3").state == "0" + assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor4").state == "50.0" turn_off_time = start_time + timedelta(minutes=90) @@ -908,7 +908,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.5" assert hass.states.get("sensor.sensor2").state == "1.5" - assert hass.states.get("sensor.sensor3").state == "0" + assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor4").state == "75.0" turn_back_on_time = start_time + timedelta(minutes=105) @@ -918,7 +918,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.5" assert hass.states.get("sensor.sensor2").state == "1.5" - assert hass.states.get("sensor.sensor3").state == "0" + assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor4").state == "75.0" with freeze_time(turn_back_on_time): @@ -927,7 +927,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.5" assert hass.states.get("sensor.sensor2").state == "1.5" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "75.0" end_time = start_time + timedelta(minutes=120) @@ -937,7 +937,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.75" assert hass.states.get("sensor.sensor2").state == "1.75" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "87.5" @@ -1198,7 +1198,7 @@ async def test_measure_sliding_window(hass, recorder_mock): assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.83" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "41.7" past_next_update = start_time + timedelta(minutes=30) @@ -1211,7 +1211,7 @@ async def test_measure_sliding_window(hass, recorder_mock): assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.83" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "41.7" @@ -1291,7 +1291,7 @@ async def test_measure_from_end_going_backwards(hass, recorder_mock): assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.83" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" past_next_update = start_time + timedelta(minutes=30) @@ -1304,7 +1304,7 @@ async def test_measure_from_end_going_backwards(hass, recorder_mock): assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.83" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -1385,7 +1385,7 @@ async def test_measure_cet(hass, recorder_mock): assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.83" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" From 4a95539d9dd032eac5d7c7f9d67c303e56eff9c8 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 18 May 2022 19:29:02 +0200 Subject: [PATCH 0639/3516] Warn user if "model" key is missing from Shelly firmware (#71612) Co-authored-by: Paulus Schoutsen --- .../components/shelly/config_flow.py | 33 ++++++++++++------- homeassistant/components/shelly/strings.json | 3 +- .../components/shelly/translations/en.json | 1 + tests/components/shelly/test_config_flow.py | 23 +++++++++++++ 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 9311be1a49e..abcfe689e93 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -119,6 +119,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" + except KeyError: + errors["base"] = "firmware_not_fully_provisioned" except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -160,6 +162,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" except aioshelly.exceptions.JSONRPCError: errors["base"] = "cannot_connect" + except KeyError: + errors["base"] = "firmware_not_fully_provisioned" except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -219,6 +223,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: self.device_info = await validate_input(self.hass, self.host, self.info, {}) + except KeyError: + LOGGER.debug("Shelly host %s firmware not fully provisioned", self.host) except HTTP_CONNECT_ERRORS: return self.async_abort(reason="cannot_connect") @@ -229,18 +235,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle discovery confirm.""" errors: dict[str, str] = {} - if user_input is not None: - return self.async_create_entry( - title=self.device_info["title"], - data={ - "host": self.host, - CONF_SLEEP_PERIOD: self.device_info[CONF_SLEEP_PERIOD], - "model": self.device_info["model"], - "gen": self.device_info["gen"], - }, - ) - - self._set_confirm_only() + try: + if user_input is not None: + return self.async_create_entry( + title=self.device_info["title"], + data={ + "host": self.host, + CONF_SLEEP_PERIOD: self.device_info[CONF_SLEEP_PERIOD], + "model": self.device_info["model"], + "gen": self.device_info["gen"], + }, + ) + except KeyError: + errors["base"] = "firmware_not_fully_provisioned" + else: + self._set_confirm_only() return self.async_show_form( step_id="confirm_discovery", diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 209ae6682b8..db1c6043187 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -21,7 +21,8 @@ "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%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "firmware_not_fully_provisioned": "Device not fully provisioned. Please contact Shelly support" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json index c23eb13840c..f3e882c3016 100644 --- a/homeassistant/components/shelly/translations/en.json +++ b/homeassistant/components/shelly/translations/en.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Failed to connect", + "firmware_not_fully_provisioned": "Device not fully provisioned. Please contact Shelly support", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 1293fe92760..713999de36f 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -225,6 +225,29 @@ async def test_form_errors_get_info(hass, error): assert result2["errors"] == {"base": base_error} +@pytest.mark.parametrize("error", [(KeyError, "firmware_not_fully_provisioned")]) +async def test_form_missing_key_get_info(hass, error): + """Test we handle missing key.""" + exc, base_error = error + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch( + "aioshelly.common.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False, "gen": "2"}, + ), patch( + "homeassistant.components.shelly.config_flow.validate_input", + side_effect=KeyError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": base_error} + + @pytest.mark.parametrize( "error", [(asyncio.TimeoutError, "cannot_connect"), (ValueError, "unknown")] ) From 985bcb23f2bc8ad86a6b76c21262675dd1933fe2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 May 2022 12:33:58 -0500 Subject: [PATCH 0640/3516] Fix SAWarning in logbook queries (#72101) --- .../components/logbook/queries/devices.py | 12 ++++++------ .../components/logbook/queries/entities.py | 15 ++++++++------- .../logbook/queries/entities_and_devices.py | 14 ++++++++------ 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/logbook/queries/devices.py b/homeassistant/components/logbook/queries/devices.py index 20b56ef8dd6..5e7827b87a0 100644 --- a/homeassistant/components/logbook/queries/devices.py +++ b/homeassistant/components/logbook/queries/devices.py @@ -8,7 +8,7 @@ from sqlalchemy import Column, lambda_stmt, select, union_all from sqlalchemy.orm import Query from sqlalchemy.sql.elements import ClauseList from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import Select +from sqlalchemy.sql.selectable import CTE, CompoundSelect from homeassistant.components.recorder.models import Events, States @@ -28,7 +28,7 @@ def _select_device_id_context_ids_sub_query( end_day: dt, event_types: tuple[str, ...], json_quotable_device_ids: list[str], -) -> Select: +) -> CompoundSelect: """Generate a subquery to find context ids for multiple devices.""" return select( union_all( @@ -45,17 +45,17 @@ def _apply_devices_context_union( end_day: dt, event_types: tuple[str, ...], json_quotable_device_ids: list[str], -) -> StatementLambdaElement: +) -> CompoundSelect: """Generate a CTE to find the device context ids and a query to find linked row.""" - devices_cte = _select_device_id_context_ids_sub_query( + devices_cte: CTE = _select_device_id_context_ids_sub_query( start_day, end_day, event_types, json_quotable_device_ids, ).cte() return query.union_all( - select_events_context_only().where(Events.context_id.in_(devices_cte)), - select_states_context_only().where(States.context_id.in_(devices_cte)), + select_events_context_only().where(Events.context_id.in_(devices_cte.select())), + select_states_context_only().where(States.context_id.in_(devices_cte.select())), ) diff --git a/homeassistant/components/logbook/queries/entities.py b/homeassistant/components/logbook/queries/entities.py index 6db0931e9f3..844890c23a9 100644 --- a/homeassistant/components/logbook/queries/entities.py +++ b/homeassistant/components/logbook/queries/entities.py @@ -8,7 +8,7 @@ import sqlalchemy from sqlalchemy import Column, lambda_stmt, select, union_all from sqlalchemy.orm import Query from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import Select +from sqlalchemy.sql.selectable import CTE, CompoundSelect from homeassistant.components.recorder.models import ( ENTITY_ID_LAST_UPDATED_INDEX, @@ -37,7 +37,7 @@ def _select_entities_context_ids_sub_query( event_types: tuple[str, ...], entity_ids: list[str], json_quotable_entity_ids: list[str], -) -> Select: +) -> CompoundSelect: """Generate a subquery to find context ids for multiple entities.""" return select( union_all( @@ -58,9 +58,9 @@ def _apply_entities_context_union( event_types: tuple[str, ...], entity_ids: list[str], json_quotable_entity_ids: list[str], -) -> StatementLambdaElement: +) -> CompoundSelect: """Generate a CTE to find the entity and device context ids and a query to find linked row.""" - entities_cte = _select_entities_context_ids_sub_query( + entities_cte: CTE = _select_entities_context_ids_sub_query( start_day, end_day, event_types, @@ -69,10 +69,12 @@ def _apply_entities_context_union( ).cte() return query.union_all( states_query_for_entity_ids(start_day, end_day, entity_ids), - select_events_context_only().where(Events.context_id.in_(entities_cte)), + select_events_context_only().where( + Events.context_id.in_(entities_cte.select()) + ), select_states_context_only() .where(States.entity_id.not_in(entity_ids)) - .where(States.context_id.in_(entities_cte)), + .where(States.context_id.in_(entities_cte.select())), ) @@ -84,7 +86,6 @@ def entities_stmt( json_quotable_entity_ids: list[str], ) -> StatementLambdaElement: """Generate a logbook query for multiple entities.""" - assert json_quotable_entity_ids is not None return lambda_stmt( lambda: _apply_entities_context_union( select_events_without_states(start_day, end_day, event_types).where( diff --git a/homeassistant/components/logbook/queries/entities_and_devices.py b/homeassistant/components/logbook/queries/entities_and_devices.py index d8a23635ad7..d1c86ddbec5 100644 --- a/homeassistant/components/logbook/queries/entities_and_devices.py +++ b/homeassistant/components/logbook/queries/entities_and_devices.py @@ -8,7 +8,7 @@ import sqlalchemy from sqlalchemy import lambda_stmt, select, union_all from sqlalchemy.orm import Query from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import Select +from sqlalchemy.sql.selectable import CTE, CompoundSelect from homeassistant.components.recorder.models import Events, States @@ -33,7 +33,7 @@ def _select_entities_device_id_context_ids_sub_query( entity_ids: list[str], json_quotable_entity_ids: list[str], json_quotable_device_ids: list[str], -) -> Select: +) -> CompoundSelect: """Generate a subquery to find context ids for multiple entities and multiple devices.""" return select( union_all( @@ -57,8 +57,8 @@ def _apply_entities_devices_context_union( entity_ids: list[str], json_quotable_entity_ids: list[str], json_quotable_device_ids: list[str], -) -> StatementLambdaElement: - devices_entities_cte = _select_entities_device_id_context_ids_sub_query( +) -> CompoundSelect: + devices_entities_cte: CTE = _select_entities_device_id_context_ids_sub_query( start_day, end_day, event_types, @@ -68,10 +68,12 @@ def _apply_entities_devices_context_union( ).cte() return query.union_all( states_query_for_entity_ids(start_day, end_day, entity_ids), - select_events_context_only().where(Events.context_id.in_(devices_entities_cte)), + select_events_context_only().where( + Events.context_id.in_(devices_entities_cte.select()) + ), select_states_context_only() .where(States.entity_id.not_in(entity_ids)) - .where(States.context_id.in_(devices_entities_cte)), + .where(States.context_id.in_(devices_entities_cte.select())), ) From 784fbf3291160eea3d7d62bc2a2ef3c54beea3d4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 19:50:15 +0200 Subject: [PATCH 0641/3516] Cleanup nest async method which is not awaiting (#72096) --- homeassistant/components/nest/device_trigger.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index 67b361e76d8..1f7f2b642dc 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -15,7 +15,7 @@ from homeassistant.components.device_automation.exceptions import ( ) from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE -from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import ConfigType @@ -33,7 +33,8 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( ) -async def async_get_nest_device_id(hass: HomeAssistant, device_id: str) -> str | None: +@callback +def async_get_nest_device_id(hass: HomeAssistant, device_id: str) -> str | None: """Get the nest API device_id from the HomeAssistant device_id.""" device_registry = dr.async_get(hass) if device := device_registry.async_get(device_id): @@ -67,7 +68,7 @@ async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, Any]]: """List device triggers for a Nest device.""" - nest_device_id = await async_get_nest_device_id(hass, device_id) + nest_device_id = async_get_nest_device_id(hass, device_id) if not nest_device_id: raise InvalidDeviceAutomationConfig(f"Device not found {device_id}") trigger_types = await async_get_device_trigger_types(hass, nest_device_id) From e300908a8e732e4a9ec3f4a7245bfe4570e8c737 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 18 May 2022 19:59:35 +0200 Subject: [PATCH 0642/3516] Refresh camera stream source of Synology DSM connected cameras (#70938) Co-authored-by: Paulus Schoutsen --- .../components/synology_dsm/__init__.py | 24 +++++++++++++--- .../components/synology_dsm/camera.py | 28 ++++++++++++++++++- .../components/synology_dsm/const.py | 3 ++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 8dbdecb9305..ece38bf7326 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -23,6 +23,7 @@ from homeassistant.const import CONF_MAC, CONF_SCAN_INTERVAL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .common import SynoApi @@ -36,6 +37,7 @@ from .const import ( EXCEPTION_DETAILS, EXCEPTION_UNKNOWN, PLATFORMS, + SIGNAL_CAMERA_SOURCE_CHANGED, SYNO_API, SYSTEM_LOADED, UNDO_UPDATE_LISTENER, @@ -123,6 +125,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return None surveillance_station = api.surveillance_station + current_data: dict[str, SynoCamera] = { + camera.id: camera for camera in surveillance_station.get_all_cameras() + } try: async with async_timeout.timeout(30): @@ -130,12 +135,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SynologyDSMAPIErrorException as err: raise UpdateFailed(f"Error communicating with API: {err}") from err - return { - "cameras": { - camera.id: camera for camera in surveillance_station.get_all_cameras() - } + new_data: dict[str, SynoCamera] = { + camera.id: camera for camera in surveillance_station.get_all_cameras() } + for cam_id, cam_data_new in new_data.items(): + if ( + (cam_data_current := current_data.get(cam_id)) is not None + and cam_data_current.live_view.rtsp != cam_data_new.live_view.rtsp + ): + async_dispatcher_send( + hass, + f"{SIGNAL_CAMERA_SOURCE_CHANGED}_{entry.entry_id}_{cam_id}", + cam_data_new.live_view.rtsp, + ) + + return {"cameras": new_data} + async def async_coordinator_update_data_central() -> None: """Fetch all device and sensor data from api.""" try: diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index c6d44d8883d..cab2536187c 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -16,7 +16,8 @@ from homeassistant.components.camera import ( CameraEntityFeature, ) 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 DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -27,6 +28,7 @@ from .const import ( COORDINATOR_CAMERAS, DEFAULT_SNAPSHOT_QUALITY, DOMAIN, + SIGNAL_CAMERA_SOURCE_CHANGED, SYNO_API, ) from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription @@ -130,6 +132,29 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): """Return the camera motion detection status.""" return self.camera_data.is_motion_detection_enabled # type: ignore[no-any-return] + def _listen_source_updates(self) -> None: + """Listen for camera source changed events.""" + + @callback + def _handle_signal(url: str) -> None: + if self.stream: + _LOGGER.debug("Update stream URL for camera %s", self.camera_data.name) + self.stream.update_source(url) + + assert self.platform + assert self.platform.config_entry + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SIGNAL_CAMERA_SOURCE_CHANGED}_{self.platform.config_entry.entry_id}_{self.camera_data.id}", + _handle_signal, + ) + ) + + async def async_added_to_hass(self) -> None: + """Subscribe to signal.""" + self._listen_source_updates() + def camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: @@ -162,6 +187,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): ) if not self.available: return None + return self.camera_data.live_view.rtsp # type: ignore[no-any-return] def enable_motion_detection(self) -> None: diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 1b4e5f0bb36..f716130a5e4 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -43,6 +43,9 @@ DEFAULT_SNAPSHOT_QUALITY = SNAPSHOT_PROFILE_BALANCED ENTITY_UNIT_LOAD = "load" +# Signals +SIGNAL_CAMERA_SOURCE_CHANGED = "synology_dsm.camera_stream_source_changed" + # Services SERVICE_REBOOT = "reboot" SERVICE_SHUTDOWN = "shutdown" From 349347cade1c20b163233e05f08a82f07ad7321d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 20:04:42 +0200 Subject: [PATCH 0643/3516] Cleanup unused import in SamsungTV (#72102) --- homeassistant/components/samsungtv/media_player.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 423a778011d..6a884c59a87 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from collections.abc import Coroutine, Sequence -import contextlib from datetime import datetime, timedelta from typing import Any From 99941b1c32262f5bdeb5b9a7dce10d176c9e6f8f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 21:04:06 +0200 Subject: [PATCH 0644/3516] Warn on use of deprecated async_get_registry (#72088) Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/area_registry.py | 4 ++++ homeassistant/helpers/device_registry.py | 3 +++ homeassistant/helpers/entity_registry.py | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index ad4930825c8..e5d35ccbf44 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -12,6 +12,7 @@ from homeassistant.loader import bind_hass from homeassistant.util import slugify from . import device_registry as dr, entity_registry as er +from .frame import report from .storage import Store from .typing import UNDEFINED, UndefinedType @@ -226,6 +227,9 @@ async def async_get_registry(hass: HomeAssistant) -> AreaRegistry: This is deprecated and will be removed in the future. Use async_get instead. """ + report( + "uses deprecated `async_get_registry` to access area registry, use async_get instead" + ) return async_get(hass) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 901242607e3..ed3d5a7b06f 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -720,6 +720,9 @@ async def async_get_registry(hass: HomeAssistant) -> DeviceRegistry: This is deprecated and will be removed in the future. Use async_get instead. """ + report( + "uses deprecated `async_get_registry` to access device registry, use async_get instead" + ) return async_get(hass) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 2c87c4cbdd2..d03d272b1ac 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -45,6 +45,7 @@ from homeassistant.util.yaml import load_yaml from . import device_registry as dr, storage from .device_registry import EVENT_DEVICE_REGISTRY_UPDATED +from .frame import report from .typing import UNDEFINED, UndefinedType if TYPE_CHECKING: @@ -819,6 +820,9 @@ async def async_get_registry(hass: HomeAssistant) -> EntityRegistry: This is deprecated and will be removed in the future. Use async_get instead. """ + report( + "uses deprecated `async_get_registry` to access entity registry, use async_get instead" + ) return async_get(hass) From 5deb78a0dd26e663f54723c320c7452ba4d3ada0 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 18 May 2022 19:59:35 +0200 Subject: [PATCH 0645/3516] Refresh camera stream source of Synology DSM connected cameras (#70938) Co-authored-by: Paulus Schoutsen --- .../components/synology_dsm/__init__.py | 24 +++++++++++++--- .../components/synology_dsm/camera.py | 28 ++++++++++++++++++- .../components/synology_dsm/const.py | 3 ++ 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 1151bf128cc..0881d5a85e9 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -28,6 +28,7 @@ from homeassistant.helpers.device_registry import ( DeviceEntry, async_get_registry as get_dev_reg, ) +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .common import SynoApi @@ -41,6 +42,7 @@ from .const import ( EXCEPTION_DETAILS, EXCEPTION_UNKNOWN, PLATFORMS, + SIGNAL_CAMERA_SOURCE_CHANGED, SYNO_API, SYSTEM_LOADED, UNDO_UPDATE_LISTENER, @@ -128,6 +130,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return None surveillance_station = api.surveillance_station + current_data: dict[str, SynoCamera] = { + camera.id: camera for camera in surveillance_station.get_all_cameras() + } try: async with async_timeout.timeout(30): @@ -135,12 +140,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except SynologyDSMAPIErrorException as err: raise UpdateFailed(f"Error communicating with API: {err}") from err - return { - "cameras": { - camera.id: camera for camera in surveillance_station.get_all_cameras() - } + new_data: dict[str, SynoCamera] = { + camera.id: camera for camera in surveillance_station.get_all_cameras() } + for cam_id, cam_data_new in new_data.items(): + if ( + (cam_data_current := current_data.get(cam_id)) is not None + and cam_data_current.live_view.rtsp != cam_data_new.live_view.rtsp + ): + async_dispatcher_send( + hass, + f"{SIGNAL_CAMERA_SOURCE_CHANGED}_{entry.entry_id}_{cam_id}", + cam_data_new.live_view.rtsp, + ) + + return {"cameras": new_data} + async def async_coordinator_update_data_central() -> None: """Fetch all device and sensor data from api.""" try: diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index c6d44d8883d..cab2536187c 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -16,7 +16,8 @@ from homeassistant.components.camera import ( CameraEntityFeature, ) 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 DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -27,6 +28,7 @@ from .const import ( COORDINATOR_CAMERAS, DEFAULT_SNAPSHOT_QUALITY, DOMAIN, + SIGNAL_CAMERA_SOURCE_CHANGED, SYNO_API, ) from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription @@ -130,6 +132,29 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): """Return the camera motion detection status.""" return self.camera_data.is_motion_detection_enabled # type: ignore[no-any-return] + def _listen_source_updates(self) -> None: + """Listen for camera source changed events.""" + + @callback + def _handle_signal(url: str) -> None: + if self.stream: + _LOGGER.debug("Update stream URL for camera %s", self.camera_data.name) + self.stream.update_source(url) + + assert self.platform + assert self.platform.config_entry + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{SIGNAL_CAMERA_SOURCE_CHANGED}_{self.platform.config_entry.entry_id}_{self.camera_data.id}", + _handle_signal, + ) + ) + + async def async_added_to_hass(self) -> None: + """Subscribe to signal.""" + self._listen_source_updates() + def camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: @@ -162,6 +187,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): ) if not self.available: return None + return self.camera_data.live_view.rtsp # type: ignore[no-any-return] def enable_motion_detection(self) -> None: diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 1b4e5f0bb36..f716130a5e4 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -43,6 +43,9 @@ DEFAULT_SNAPSHOT_QUALITY = SNAPSHOT_PROFILE_BALANCED ENTITY_UNIT_LOAD = "load" +# Signals +SIGNAL_CAMERA_SOURCE_CHANGED = "synology_dsm.camera_stream_source_changed" + # Services SERVICE_REBOOT = "reboot" SERVICE_SHUTDOWN = "shutdown" From a3bd911ce396956e2cee038b2ea8d39011e12db0 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Wed, 18 May 2022 19:29:02 +0200 Subject: [PATCH 0646/3516] Warn user if "model" key is missing from Shelly firmware (#71612) Co-authored-by: Paulus Schoutsen --- .../components/shelly/config_flow.py | 33 ++++++++++++------- homeassistant/components/shelly/strings.json | 3 +- .../components/shelly/translations/en.json | 1 + tests/components/shelly/test_config_flow.py | 23 +++++++++++++ 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 9311be1a49e..abcfe689e93 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -119,6 +119,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" + except KeyError: + errors["base"] = "firmware_not_fully_provisioned" except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -160,6 +162,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" except aioshelly.exceptions.JSONRPCError: errors["base"] = "cannot_connect" + except KeyError: + errors["base"] = "firmware_not_fully_provisioned" except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -219,6 +223,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: self.device_info = await validate_input(self.hass, self.host, self.info, {}) + except KeyError: + LOGGER.debug("Shelly host %s firmware not fully provisioned", self.host) except HTTP_CONNECT_ERRORS: return self.async_abort(reason="cannot_connect") @@ -229,18 +235,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle discovery confirm.""" errors: dict[str, str] = {} - if user_input is not None: - return self.async_create_entry( - title=self.device_info["title"], - data={ - "host": self.host, - CONF_SLEEP_PERIOD: self.device_info[CONF_SLEEP_PERIOD], - "model": self.device_info["model"], - "gen": self.device_info["gen"], - }, - ) - - self._set_confirm_only() + try: + if user_input is not None: + return self.async_create_entry( + title=self.device_info["title"], + data={ + "host": self.host, + CONF_SLEEP_PERIOD: self.device_info[CONF_SLEEP_PERIOD], + "model": self.device_info["model"], + "gen": self.device_info["gen"], + }, + ) + except KeyError: + errors["base"] = "firmware_not_fully_provisioned" + else: + self._set_confirm_only() return self.async_show_form( step_id="confirm_discovery", diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 209ae6682b8..db1c6043187 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -21,7 +21,8 @@ "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%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "firmware_not_fully_provisioned": "Device not fully provisioned. Please contact Shelly support" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json index c23eb13840c..f3e882c3016 100644 --- a/homeassistant/components/shelly/translations/en.json +++ b/homeassistant/components/shelly/translations/en.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Failed to connect", + "firmware_not_fully_provisioned": "Device not fully provisioned. Please contact Shelly support", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 1293fe92760..713999de36f 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -225,6 +225,29 @@ async def test_form_errors_get_info(hass, error): assert result2["errors"] == {"base": base_error} +@pytest.mark.parametrize("error", [(KeyError, "firmware_not_fully_provisioned")]) +async def test_form_missing_key_get_info(hass, error): + """Test we handle missing key.""" + exc, base_error = error + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch( + "aioshelly.common.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False, "gen": "2"}, + ), patch( + "homeassistant.components.shelly.config_flow.validate_input", + side_effect=KeyError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": base_error} + + @pytest.mark.parametrize( "error", [(asyncio.TimeoutError, "cannot_connect"), (ValueError, "unknown")] ) From 5fbc4b8dbaab4f41e8fd030f8286e1912a0f4586 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 13 May 2022 21:30:44 +1000 Subject: [PATCH 0647/3516] Remove LIFX bulb discovery from the inflight list if it fails to connect (#71673) Remove the bulb discovery from the inflight list if it fails to connect Signed-off-by: Avi Miller --- homeassistant/components/lifx/light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 428ae8745e0..c4df24a0cb0 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -419,6 +419,8 @@ class LIFXManager: if color_resp is None or version_resp is None: _LOGGER.error("Failed to connect to %s", bulb.ip_addr) bulb.registered = False + if bulb.mac_addr in self.discoveries_inflight: + self.discoveries_inflight.pop(bulb.mac_addr) else: bulb.timeout = MESSAGE_TIMEOUT bulb.retry_count = MESSAGE_RETRIES From 107615ebefdb0b4db4985c560077276da7a1dd8c Mon Sep 17 00:00:00 2001 From: rappenze Date: Sun, 15 May 2022 15:02:05 +0200 Subject: [PATCH 0648/3516] Limit parallel requests in fibaro light (#71762) --- homeassistant/components/fibaro/light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index 08a9e651668..0115e0301c3 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -23,6 +23,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import FIBARO_DEVICES, FibaroDevice from .const import DOMAIN +PARALLEL_UPDATES = 2 + def scaleto255(value: int | None) -> int: """Scale the input value from 0-100 to 0-255.""" From 2448661371246bb0192fda88c6b11376686c91c2 Mon Sep 17 00:00:00 2001 From: Ethan Madden Date: Mon, 16 May 2022 01:30:49 -0700 Subject: [PATCH 0649/3516] Fix VeSync air_quality fan attribute (#71771) * Refactor attribute inclusion for VeSync fans. A recent change to pyvesync (introduced in 2.2) changed `air_quality` to refer to air quality as an integer representation of perceived air quality rather than a direct reading of the PM2.5 sensor. With 2.3 the PM2.5 sensor access was restored as `air_quality_value`. Unfortunately, `air_quality_value` was not added as an attribute on the fan object, and rather only exists in the `details` dictionary on the fan object. * Update homeassistant/components/vesync/fan.py Co-authored-by: Martin Hjelmare * Rename `air_quality_value` attribute to `pm25` This should make it more clear what the attribute actually represents * `air_quality` attribute reports `air_quality_value` This restores previous behavior for this integration to what it was before the `pyvesync==2.02` upgrade, using the `air_quality` attribute to report pm2.5 concentrations (formerly `air_quality`) rather the vague measurement now reported by `air_quality`. Co-authored-by: Martin Hjelmare --- homeassistant/components/vesync/fan.py | 4 ++-- homeassistant/components/vesync/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index e37a6c8893e..f16a785ee1e 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -171,8 +171,8 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): if hasattr(self.smartfan, "night_light"): attr["night_light"] = self.smartfan.night_light - if hasattr(self.smartfan, "air_quality"): - attr["air_quality"] = self.smartfan.air_quality + if self.smartfan.details.get("air_quality_value") is not None: + attr["air_quality"] = self.smartfan.details["air_quality_value"] if hasattr(self.smartfan, "mode"): attr["mode"] = self.smartfan.mode diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json index c93e070a484..49be473b748 100644 --- a/homeassistant/components/vesync/manifest.json +++ b/homeassistant/components/vesync/manifest.json @@ -3,7 +3,7 @@ "name": "VeSync", "documentation": "https://www.home-assistant.io/integrations/vesync", "codeowners": ["@markperdue", "@webdjoe", "@thegardenmonkey"], - "requirements": ["pyvesync==2.0.2"], + "requirements": ["pyvesync==2.0.3"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["pyvesync"] diff --git a/requirements_all.txt b/requirements_all.txt index ef48c8117f5..011e759b481 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1996,7 +1996,7 @@ pyvera==0.3.13 pyversasense==0.0.6 # homeassistant.components.vesync -pyvesync==2.0.2 +pyvesync==2.0.3 # homeassistant.components.vizio pyvizio==0.1.57 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb683ae0bab..49bbec71c35 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1313,7 +1313,7 @@ pyuptimerobot==22.2.0 pyvera==0.3.13 # homeassistant.components.vesync -pyvesync==2.0.2 +pyvesync==2.0.3 # homeassistant.components.vizio pyvizio==0.1.57 From 5f3c7f11d8bd572d7e1525c72353c59bd472b0f2 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Fri, 13 May 2022 18:42:33 -0400 Subject: [PATCH 0650/3516] Fix handling package detection for latest UniFi Protect beta (#71821) Co-authored-by: J. Nick Koston --- homeassistant/components/unifiprotect/config_flow.py | 8 ++++++-- homeassistant/components/unifiprotect/manifest.json | 2 +- homeassistant/components/unifiprotect/select.py | 2 +- homeassistant/components/unifiprotect/switch.py | 9 +++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifiprotect/test_switch.py | 10 +++++++--- 7 files changed, 26 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 0cfce44a6ca..27108d24eaa 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -143,7 +143,9 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_VERIFY_SSL] = False nvr_data, errors = await self._async_get_nvr_data(user_input) if nvr_data and not errors: - return self._async_create_entry(nvr_data.name, user_input) + return self._async_create_entry( + nvr_data.name or nvr_data.type, user_input + ) placeholders = { "name": discovery_info["hostname"] @@ -289,7 +291,9 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(nvr_data.mac) self._abort_if_unique_id_configured() - return self._async_create_entry(nvr_data.name, user_input) + return self._async_create_entry( + nvr_data.name or nvr_data.type, user_input + ) user_input = user_input or {} return self.async_show_form( diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 3c3b461ed4f..3efad51db31 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.4.1", "unifi-discovery==1.1.2"], + "requirements": ["pyunifiprotect==3.5.1", "unifi-discovery==1.1.2"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index b7b53ff81c8..f0500ea54e5 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -140,7 +140,7 @@ def _get_doorbell_options(api: ProtectApiClient) -> list[dict[str, Any]]: def _get_paired_camera_options(api: ProtectApiClient) -> list[dict[str, Any]]: options = [{"id": TYPE_EMPTY_VALUE, "name": "Not Paired"}] for camera in api.bootstrap.cameras.values(): - options.append({"id": camera.id, "name": camera.name}) + options.append({"id": camera.id, "name": camera.name or camera.type}) return options diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 2257f399f75..85a089994f8 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -159,6 +159,15 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_value="is_face_detection_on", ufp_set_method="set_face_detection", ), + ProtectSwitchEntityDescription( + key="smart_package", + name="Detections: Package", + icon="mdi:package-variant-closed", + entity_category=EntityCategory.CONFIG, + ufp_required_field="can_detect_package", + ufp_value="is_package_detection_on", + ufp_set_method="set_package_detection", + ), ) SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( diff --git a/requirements_all.txt b/requirements_all.txt index 011e759b481..a7cef9f1b43 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1981,7 +1981,7 @@ pytrafikverket==0.1.6.2 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.4.1 +pyunifiprotect==3.5.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49bbec71c35..eb2be965ee7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1304,7 +1304,7 @@ pytrafikverket==0.1.6.2 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.4.1 +pyunifiprotect==3.5.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 0a3ac92076e..c54d04a8cb7 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -26,9 +26,13 @@ from .conftest import ( ids_from_device_description, ) -CAMERA_SWITCHES_NO_FACE = [d for d in CAMERA_SWITCHES if d.name != "Detections: Face"] +CAMERA_SWITCHES_BASIC = [ + d + for d in CAMERA_SWITCHES + if d.name != "Detections: Face" and d.name != "Detections: Package" +] CAMERA_SWITCHES_NO_EXTRA = [ - d for d in CAMERA_SWITCHES_NO_FACE if d.name not in ("High FPS", "Privacy Mode") + d for d in CAMERA_SWITCHES_BASIC if d.name not in ("High FPS", "Privacy Mode") ] @@ -253,7 +257,7 @@ async def test_switch_setup_camera_all( entity_registry = er.async_get(hass) - for description in CAMERA_SWITCHES_NO_FACE: + for description in CAMERA_SWITCHES_BASIC: unique_id, entity_id = ids_from_device_description( Platform.SWITCH, camera, description ) From 81f9cc40dd8eaad557a58829fc488c1624b20083 Mon Sep 17 00:00:00 2001 From: RadekHvizdos <10856567+RadekHvizdos@users.noreply.github.com> Date: Sun, 15 May 2022 11:29:35 +0200 Subject: [PATCH 0651/3516] Add missing Shelly Cover sensors bugfix (#71831) Switching Shelly Plus 2PM from switch to cover mode results in missing sensors for Power, Voltage, Energy and Temperature. These parameters are still available in the API, but need to be accessed via "cover" key instead of "switch" key. This change adds the missing sensors. --- homeassistant/components/shelly/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index df5a75a7ed9..77c09283fbf 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -297,6 +297,9 @@ def get_rpc_key_instances(keys_dict: dict[str, Any], key: str) -> list[str]: if key in keys_dict: return [key] + if key == "switch" and "cover:0" in keys_dict: + key = "cover" + keys_list: list[str] = [] for i in range(MAX_RPC_KEY_INSTANCES): key_inst = f"{key}:{i}" From ce39461810194d8f2f4b2fb1e351c0fa24efc8ae Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 15 May 2022 20:36:57 +0200 Subject: [PATCH 0652/3516] Revert changing `pysnmp` to `pysnmplib` (#71901) --- homeassistant/components/brother/manifest.json | 2 +- homeassistant/components/snmp/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index c230373ea36..aaf1af72db9 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.2.0"], + "requirements": ["brother==1.1.0"], "zeroconf": [ { "type": "_printer._tcp.local.", diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index ef0213e82dc..76df9e18606 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -2,7 +2,7 @@ "domain": "snmp", "name": "SNMP", "documentation": "https://www.home-assistant.io/integrations/snmp", - "requirements": ["pysnmplib==5.0.10"], + "requirements": ["pysnmp==4.4.12"], "codeowners": [], "iot_class": "local_polling", "loggers": ["pyasn1", "pysmi", "pysnmp"] diff --git a/requirements_all.txt b/requirements_all.txt index a7cef9f1b43..b851ba6b9d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -436,7 +436,7 @@ bravia-tv==1.0.11 broadlink==0.18.1 # homeassistant.components.brother -brother==1.2.0 +brother==1.1.0 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 @@ -1829,7 +1829,7 @@ pysmarty==0.8 pysml==0.0.7 # homeassistant.components.snmp -pysnmplib==5.0.10 +pysnmp==4.4.12 # homeassistant.components.soma pysoma==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eb2be965ee7..192a5db2d40 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -327,7 +327,7 @@ bravia-tv==1.0.11 broadlink==0.18.1 # homeassistant.components.brother -brother==1.2.0 +brother==1.1.0 # homeassistant.components.brunt brunt==1.2.0 From a0d1c5d1e6eb8a2f27add6bcfac7483a976b7909 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 19:01:56 +0200 Subject: [PATCH 0653/3516] Suppress Upnp error in SamsungTV resubscribe (#71925) * Suppress Upnp error in SamsungTV resubscribe * Supress UpnpCommunicationError instead * Log resubscribe errors * Add tests * Add exc_info --- .../components/samsungtv/media_player.py | 5 +- .../components/samsungtv/test_media_player.py | 48 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index a0b70af2db5..e064b016dc6 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -12,6 +12,7 @@ from async_upnp_client.client import UpnpDevice, UpnpService, UpnpStateVariable from async_upnp_client.client_factory import UpnpFactory from async_upnp_client.exceptions import ( UpnpActionResponseError, + UpnpCommunicationError, UpnpConnectionError, UpnpError, UpnpResponseError, @@ -307,8 +308,10 @@ class SamsungTVDevice(MediaPlayerEntity): async def _async_resubscribe_dmr(self) -> None: assert self._dmr_device - with contextlib.suppress(UpnpConnectionError): + try: await self._dmr_device.async_subscribe_services(auto_resubscribe=True) + except UpnpCommunicationError as err: + LOGGER.debug("Device rejected re-subscription: %r", err, exc_info=True) async def _async_shutdown_dmr(self) -> None: """Handle removal.""" diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index e8407a86a3e..1686b8d6a95 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -6,6 +6,8 @@ from unittest.mock import DEFAULT as DEFAULT_MOCK, AsyncMock, Mock, call, patch from async_upnp_client.exceptions import ( UpnpActionResponseError, + UpnpCommunicationError, + UpnpConnectionError, UpnpError, UpnpResponseError, ) @@ -1505,3 +1507,49 @@ async def test_upnp_re_subscribe_events( assert state.state == STATE_ON assert dmr_device.async_subscribe_services.call_count == 2 assert dmr_device.async_unsubscribe_services.call_count == 1 + + +@pytest.mark.usefixtures("rest_api", "upnp_notify_server") +@pytest.mark.parametrize( + "error", + {UpnpConnectionError(), UpnpCommunicationError(), UpnpResponseError(status=400)}, +) +async def test_upnp_failed_re_subscribe_events( + hass: HomeAssistant, + remotews: Mock, + dmr_device: Mock, + mock_now: datetime, + caplog: pytest.LogCaptureFixture, + error: Exception, +) -> None: + """Test for Upnp event feedback.""" + await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + assert dmr_device.async_subscribe_services.call_count == 1 + assert dmr_device.async_unsubscribe_services.call_count == 0 + + with patch.object( + remotews, "start_listening", side_effect=WebSocketException("Boom") + ), patch.object(remotews, "is_alive", return_value=False): + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + assert dmr_device.async_subscribe_services.call_count == 1 + assert dmr_device.async_unsubscribe_services.call_count == 1 + + next_update = mock_now + timedelta(minutes=10) + with patch("homeassistant.util.dt.utcnow", return_value=next_update), patch.object( + dmr_device, "async_subscribe_services", side_effect=error + ): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + assert "Device rejected re-subscription" in caplog.text From 6155a642226d5e40f79b59d00de199678af1ca02 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 16 May 2022 12:50:03 +0200 Subject: [PATCH 0654/3516] Properly handle Shelly gen2 device disconnect (#71937) --- 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 d29584c4e83..41a9e68fbdd 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -812,7 +812,7 @@ class RpcPollingWrapper(update_coordinator.DataUpdateCoordinator): LOGGER.debug("Polling Shelly RPC Device - %s", self.name) async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): await self.device.update_status() - except OSError as err: + except (OSError, aioshelly.exceptions.RPCTimeout) as err: raise update_coordinator.UpdateFailed("Device disconnected") from err @property From d34d3baa07ab7a17a5644e049a3075e87f50872e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 May 2022 12:28:04 -0500 Subject: [PATCH 0655/3516] Include initial state in history_stats count (#71952) --- .../components/history_stats/data.py | 14 +++++----- .../components/history_stats/sensor.py | 2 +- tests/components/history_stats/test_sensor.py | 26 +++++++++---------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 3d21cca6b6d..8153557422d 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -19,7 +19,7 @@ class HistoryStatsState: """The current stats of the history stats.""" hours_matched: float | None - changes_to_match_state: int | None + match_count: int | None period: tuple[datetime.datetime, datetime.datetime] @@ -121,14 +121,12 @@ class HistoryStats: self._state = HistoryStatsState(None, None, self._period) return self._state - hours_matched, changes_to_match_state = self._async_compute_hours_and_changes( + hours_matched, match_count = self._async_compute_hours_and_changes( now_timestamp, current_period_start_timestamp, current_period_end_timestamp, ) - self._state = HistoryStatsState( - hours_matched, changes_to_match_state, self._period - ) + self._state = HistoryStatsState(hours_matched, match_count, self._period) return self._state def _update_from_database( @@ -156,7 +154,7 @@ class HistoryStats: ) last_state_change_timestamp = start_timestamp elapsed = 0.0 - changes_to_match_state = 0 + match_count = 1 if previous_state_matches else 0 # Make calculations for item in self._history_current_period: @@ -166,7 +164,7 @@ class HistoryStats: if previous_state_matches: elapsed += state_change_timestamp - last_state_change_timestamp elif current_state_matches: - changes_to_match_state += 1 + match_count += 1 previous_state_matches = current_state_matches last_state_change_timestamp = state_change_timestamp @@ -178,4 +176,4 @@ class HistoryStats: # Save value in hours hours_matched = elapsed / 3600 - return hours_matched, changes_to_match_state + return hours_matched, match_count diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index b3e64106d9f..b0ce1a8fca5 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -166,4 +166,4 @@ class HistoryStatsSensor(HistoryStatsSensorBase): elif self._type == CONF_TYPE_RATIO: self._attr_native_value = pretty_ratio(state.hours_matched, state.period) elif self._type == CONF_TYPE_COUNT: - self._attr_native_value = state.changes_to_match_state + self._attr_native_value = state.match_count diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 4f56edaa291..b375a8f63c4 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -438,7 +438,7 @@ async def test_measure(hass, recorder_mock): assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.83" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -519,7 +519,7 @@ async def test_async_on_entire_period(hass, recorder_mock): assert hass.states.get("sensor.on_sensor1").state == "1.0" assert hass.states.get("sensor.on_sensor2").state == "1.0" - assert hass.states.get("sensor.on_sensor3").state == "0" + assert hass.states.get("sensor.on_sensor3").state == "1" assert hass.states.get("sensor.on_sensor4").state == "100.0" @@ -886,7 +886,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "0.0" assert hass.states.get("sensor.sensor2").state == "0.0" - assert hass.states.get("sensor.sensor3").state == "0" + assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor4").state == "0.0" one_hour_in = start_time + timedelta(minutes=60) @@ -896,7 +896,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.0" assert hass.states.get("sensor.sensor2").state == "1.0" - assert hass.states.get("sensor.sensor3").state == "0" + assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor4").state == "50.0" turn_off_time = start_time + timedelta(minutes=90) @@ -908,7 +908,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.5" assert hass.states.get("sensor.sensor2").state == "1.5" - assert hass.states.get("sensor.sensor3").state == "0" + assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor4").state == "75.0" turn_back_on_time = start_time + timedelta(minutes=105) @@ -918,7 +918,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.5" assert hass.states.get("sensor.sensor2").state == "1.5" - assert hass.states.get("sensor.sensor3").state == "0" + assert hass.states.get("sensor.sensor3").state == "1" assert hass.states.get("sensor.sensor4").state == "75.0" with freeze_time(turn_back_on_time): @@ -927,7 +927,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.5" assert hass.states.get("sensor.sensor2").state == "1.5" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "75.0" end_time = start_time + timedelta(minutes=120) @@ -937,7 +937,7 @@ async def test_async_start_from_history_and_switch_to_watching_state_changes_mul assert hass.states.get("sensor.sensor1").state == "1.75" assert hass.states.get("sensor.sensor2").state == "1.75" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "87.5" @@ -1198,7 +1198,7 @@ async def test_measure_sliding_window(hass, recorder_mock): assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.83" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "41.7" past_next_update = start_time + timedelta(minutes=30) @@ -1211,7 +1211,7 @@ async def test_measure_sliding_window(hass, recorder_mock): assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.83" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "41.7" @@ -1291,7 +1291,7 @@ async def test_measure_from_end_going_backwards(hass, recorder_mock): assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.83" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" past_next_update = start_time + timedelta(minutes=30) @@ -1304,7 +1304,7 @@ async def test_measure_from_end_going_backwards(hass, recorder_mock): assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.83" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" @@ -1385,7 +1385,7 @@ async def test_measure_cet(hass, recorder_mock): assert hass.states.get("sensor.sensor1").state == "0.83" assert hass.states.get("sensor.sensor2").state == "0.83" - assert hass.states.get("sensor.sensor3").state == "1" + assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "83.3" From 6b0c7a2dd4cd52837cdf27f5b7aa80e673850589 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 18 May 2022 10:00:46 +0300 Subject: [PATCH 0656/3516] Fix filesize doing IO in event loop (#72038) --- homeassistant/components/filesize/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 22b8cd60d79..52bb869827a 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -132,7 +132,7 @@ class FileSizeCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict[str, float | int | datetime]: """Fetch file information.""" try: - statinfo = os.stat(self._path) + statinfo = await self.hass.async_add_executor_job(os.stat, self._path) except OSError as error: raise UpdateFailed(f"Can not retrieve file statistics {error}") from error From a1df9c33aabc37e280b8241baf0db2b1939035e0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 09:36:20 +0200 Subject: [PATCH 0657/3516] Ignore UpnpXmlContentError in SamsungTV (#72056) --- homeassistant/components/samsungtv/media_player.py | 6 ++++-- tests/components/samsungtv/test_media_player.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index e064b016dc6..423a778011d 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -16,6 +16,7 @@ from async_upnp_client.exceptions import ( UpnpConnectionError, UpnpError, UpnpResponseError, + UpnpXmlContentError, ) from async_upnp_client.profiles.dlna import DmrDevice from async_upnp_client.utils import async_get_local_ip @@ -271,11 +272,12 @@ class SamsungTVDevice(MediaPlayerEntity): # NETWORK,NONE upnp_factory = UpnpFactory(upnp_requester, non_strict=True) upnp_device: UpnpDevice | None = None - with contextlib.suppress(UpnpConnectionError, UpnpResponseError): + try: upnp_device = await upnp_factory.async_create_device( self._ssdp_rendering_control_location ) - if not upnp_device: + except (UpnpConnectionError, UpnpResponseError, UpnpXmlContentError) as err: + LOGGER.debug("Unable to create Upnp DMR device: %r", err, exc_info=True) return _, event_ip = await async_get_local_ip( self._ssdp_rendering_control_location, self.hass.loop diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 1686b8d6a95..e548822f1d0 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -1370,6 +1370,7 @@ async def test_upnp_not_available( ) -> None: """Test for volume control when Upnp is not available.""" await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + assert "Unable to create Upnp DMR device" in caplog.text # Upnp action fails assert await hass.services.async_call( @@ -1387,6 +1388,7 @@ async def test_upnp_missing_service( ) -> None: """Test for volume control when Upnp is not available.""" await setup_samsungtv_entry(hass, MOCK_ENTRY_WS) + assert "Unable to create Upnp DMR device" in caplog.text # Upnp action fails assert await hass.services.async_call( From 996633553be1b472c73dcb4de0c27d989718fbf7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 18 May 2022 20:04:42 +0200 Subject: [PATCH 0658/3516] Cleanup unused import in SamsungTV (#72102) --- homeassistant/components/samsungtv/media_player.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 423a778011d..6a884c59a87 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from collections.abc import Coroutine, Sequence -import contextlib from datetime import datetime, timedelta from typing import Any From 1b107f6845833592d9b1214ec27e8eb4997828ee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 May 2022 12:14:42 -0700 Subject: [PATCH 0659/3516] Bumped version to 2022.5.5 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5eb59e819da..e41189fda07 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "4" +PATCH_VERSION: Final = "5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index b492bd0a240..ab8af3d941b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = homeassistant -version = 2022.5.4 +version = 2022.5.5 author = The Home Assistant Authors author_email = hello@home-assistant.io license = Apache-2.0 From 18b40990a258ec81fceace827819b98cbd57cc80 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 May 2022 13:22:30 -0700 Subject: [PATCH 0660/3516] Bump frontend to 20220518.0 (#72106) --- 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 0040dece159..5cf48e76d4b 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220516.0"], + "requirements": ["home-assistant-frontend==20220518.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index db6433c65b7..4e6174db561 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220516.0 +home-assistant-frontend==20220518.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 94ab58682ff..bddac66cb90 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -822,7 +822,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220516.0 +home-assistant-frontend==20220518.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3adafb068de..522583f4ce4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -589,7 +589,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220516.0 +home-assistant-frontend==20220518.0 # homeassistant.components.home_connect homeconnect==0.7.0 From 274557361002a168fb14519f30f4590aeac42142 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 May 2022 15:35:35 -0500 Subject: [PATCH 0661/3516] Small cleanups lutron_caseta (#72099) --- .../components/lutron_caseta/__init__.py | 9 ++++++ .../components/lutron_caseta/binary_sensor.py | 18 ++++-------- .../components/lutron_caseta/cover.py | 29 +++++-------------- homeassistant/components/lutron_caseta/fan.py | 29 ++++--------------- .../components/lutron_caseta/light.py | 23 ++++----------- .../components/lutron_caseta/scene.py | 8 +---- .../components/lutron_caseta/switch.py | 24 ++++----------- 7 files changed, 41 insertions(+), 99 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index e9ada82bb54..3daa45d30ee 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -353,3 +353,12 @@ class LutronCasetaDevice(Entity): def extra_state_attributes(self): """Return the state attributes.""" return {"device_id": self.device_id, "zone_id": self._device["zone"]} + + +class LutronCasetaDeviceUpdatableEntity(LutronCasetaDevice): + """A lutron_caseta entity that can update by syncing data from the bridge.""" + + async def async_update(self): + """Update when forcing a refresh of the device.""" + self._device = self._smartbridge.get_device_by_id(self.device_id) + _LOGGER.debug(self._device) diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index f61e644a331..6a6e3853280 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -26,22 +26,21 @@ async def async_setup_entry( Adds occupancy groups from the Caseta bridge associated with the config_entry as binary_sensor entities. """ - entities = [] data = hass.data[CASETA_DOMAIN][config_entry.entry_id] bridge = data[BRIDGE_LEAP] bridge_device = data[BRIDGE_DEVICE] occupancy_groups = bridge.occupancy_groups - - for occupancy_group in occupancy_groups.values(): - entity = LutronOccupancySensor(occupancy_group, bridge, bridge_device) - entities.append(entity) - - async_add_entities(entities, True) + async_add_entities( + LutronOccupancySensor(occupancy_group, bridge, bridge_device) + for occupancy_group in occupancy_groups.values() + ) class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): """Representation of a Lutron occupancy group.""" + _attr_device_class = BinarySensorDeviceClass.OCCUPANCY + def __init__(self, device, bridge, bridge_device): """Init an occupancy sensor.""" super().__init__(device, bridge, bridge_device) @@ -59,11 +58,6 @@ class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): info[ATTR_SUGGESTED_AREA] = area self._attr_device_info = info - @property - def device_class(self): - """Flag supported features.""" - return BinarySensorDeviceClass.OCCUPANCY - @property def is_on(self): """Return the brightness of the light.""" diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py index 61c9c42a1b0..afddb2677a7 100644 --- a/homeassistant/components/lutron_caseta/cover.py +++ b/homeassistant/components/lutron_caseta/cover.py @@ -1,5 +1,4 @@ """Support for Lutron Caseta shades.""" -import logging from homeassistant.components.cover import ( ATTR_POSITION, @@ -12,11 +11,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import LutronCasetaDevice +from . import LutronCasetaDeviceUpdatableEntity from .const import BRIDGE_DEVICE, BRIDGE_LEAP, DOMAIN as CASETA_DOMAIN -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, @@ -28,20 +25,17 @@ async def async_setup_entry( Adds shades from the Caseta bridge associated with the config_entry as cover entities. """ - entities = [] data = hass.data[CASETA_DOMAIN][config_entry.entry_id] bridge = data[BRIDGE_LEAP] bridge_device = data[BRIDGE_DEVICE] cover_devices = bridge.get_devices_by_domain(DOMAIN) - - for cover_device in cover_devices: - entity = LutronCasetaCover(cover_device, bridge, bridge_device) - entities.append(entity) - - async_add_entities(entities, True) + async_add_entities( + LutronCasetaCover(cover_device, bridge, bridge_device) + for cover_device in cover_devices + ) -class LutronCasetaCover(LutronCasetaDevice, CoverEntity): +class LutronCasetaCover(LutronCasetaDeviceUpdatableEntity, CoverEntity): """Representation of a Lutron shade.""" _attr_supported_features = ( @@ -50,6 +44,7 @@ class LutronCasetaCover(LutronCasetaDevice, CoverEntity): | CoverEntityFeature.STOP | CoverEntityFeature.SET_POSITION ) + _attr_device_class = CoverDeviceClass.SHADE @property def is_closed(self): @@ -61,11 +56,6 @@ class LutronCasetaCover(LutronCasetaDevice, CoverEntity): """Return the current position of cover.""" return self._device["current_state"] - @property - def device_class(self): - """Return the device class.""" - return CoverDeviceClass.SHADE - async def async_stop_cover(self, **kwargs): """Top the cover.""" await self._smartbridge.stop_cover(self.device_id) @@ -87,8 +77,3 @@ class LutronCasetaCover(LutronCasetaDevice, CoverEntity): if ATTR_POSITION in kwargs: position = kwargs[ATTR_POSITION] await self._smartbridge.set_value(self.device_id, position) - - async def async_update(self): - """Call when forcing a refresh of the device.""" - self._device = self._smartbridge.get_device_by_id(self.device_id) - _LOGGER.debug(self._device) diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py index 00236405735..cdf00959ed1 100644 --- a/homeassistant/components/lutron_caseta/fan.py +++ b/homeassistant/components/lutron_caseta/fan.py @@ -1,8 +1,6 @@ """Support for Lutron Caseta fans.""" from __future__ import annotations -import logging - from pylutron_caseta import FAN_HIGH, FAN_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_OFF from homeassistant.components.fan import DOMAIN, FanEntity, FanEntityFeature @@ -14,11 +12,9 @@ from homeassistant.util.percentage import ( percentage_to_ordered_list_item, ) -from . import LutronCasetaDevice +from . import LutronCasetaDeviceUpdatableEntity from .const import BRIDGE_DEVICE, BRIDGE_LEAP, DOMAIN as CASETA_DOMAIN -_LOGGER = logging.getLogger(__name__) - DEFAULT_ON_PERCENTAGE = 50 ORDERED_NAMED_FAN_SPEEDS = [FAN_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH] @@ -33,23 +29,20 @@ async def async_setup_entry( Adds fan controllers from the Caseta bridge associated with the config_entry as fan entities. """ - entities = [] data = hass.data[CASETA_DOMAIN][config_entry.entry_id] bridge = data[BRIDGE_LEAP] bridge_device = data[BRIDGE_DEVICE] fan_devices = bridge.get_devices_by_domain(DOMAIN) - - for fan_device in fan_devices: - entity = LutronCasetaFan(fan_device, bridge, bridge_device) - entities.append(entity) - - async_add_entities(entities, True) + async_add_entities( + LutronCasetaFan(fan_device, bridge, bridge_device) for fan_device in fan_devices + ) -class LutronCasetaFan(LutronCasetaDevice, FanEntity): +class LutronCasetaFan(LutronCasetaDeviceUpdatableEntity, FanEntity): """Representation of a Lutron Caseta fan. Including Fan Speed.""" _attr_supported_features = FanEntityFeature.SET_SPEED + _attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS) @property def percentage(self) -> int | None: @@ -62,11 +55,6 @@ class LutronCasetaFan(LutronCasetaDevice, FanEntity): ORDERED_NAMED_FAN_SPEEDS, self._device["fan_speed"] ) - @property - def speed_count(self) -> int: - """Return the number of speeds the fan supports.""" - return len(ORDERED_NAMED_FAN_SPEEDS) - async def async_turn_on( self, percentage: int = None, @@ -98,8 +86,3 @@ class LutronCasetaFan(LutronCasetaDevice, FanEntity): def is_on(self): """Return true if device is on.""" return self.percentage and self.percentage > 0 - - async def async_update(self): - """Update when forcing a refresh of the device.""" - self._device = self._smartbridge.get_device_by_id(self.device_id) - _LOGGER.debug("State of this lutron fan device is %s", self._device) diff --git a/homeassistant/components/lutron_caseta/light.py b/homeassistant/components/lutron_caseta/light.py index cba4450bf27..a58fc21aadf 100644 --- a/homeassistant/components/lutron_caseta/light.py +++ b/homeassistant/components/lutron_caseta/light.py @@ -1,6 +1,5 @@ """Support for Lutron Caseta lights.""" from datetime import timedelta -import logging from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -14,11 +13,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import LutronCasetaDevice +from . import LutronCasetaDeviceUpdatableEntity from .const import BRIDGE_DEVICE, BRIDGE_LEAP, DOMAIN as CASETA_DOMAIN -_LOGGER = logging.getLogger(__name__) - def to_lutron_level(level): """Convert the given Home Assistant light level (0-255) to Lutron (0-100).""" @@ -40,20 +37,17 @@ async def async_setup_entry( Adds dimmers from the Caseta bridge associated with the config_entry as light entities. """ - entities = [] data = hass.data[CASETA_DOMAIN][config_entry.entry_id] bridge = data[BRIDGE_LEAP] bridge_device = data[BRIDGE_DEVICE] light_devices = bridge.get_devices_by_domain(DOMAIN) - - for light_device in light_devices: - entity = LutronCasetaLight(light_device, bridge, bridge_device) - entities.append(entity) - - async_add_entities(entities, True) + async_add_entities( + LutronCasetaLight(light_device, bridge, bridge_device) + for light_device in light_devices + ) -class LutronCasetaLight(LutronCasetaDevice, LightEntity): +class LutronCasetaLight(LutronCasetaDeviceUpdatableEntity, LightEntity): """Representation of a Lutron Light, including dimmable.""" _attr_color_mode = ColorMode.BRIGHTNESS @@ -88,8 +82,3 @@ class LutronCasetaLight(LutronCasetaDevice, LightEntity): def is_on(self): """Return true if device is on.""" return self._device["current_state"] > 0 - - async def async_update(self): - """Call when forcing a refresh of the device.""" - self._device = self._smartbridge.get_device_by_id(self.device_id) - _LOGGER.debug(self._device) diff --git a/homeassistant/components/lutron_caseta/scene.py b/homeassistant/components/lutron_caseta/scene.py index d81a741359c..bb932c61316 100644 --- a/homeassistant/components/lutron_caseta/scene.py +++ b/homeassistant/components/lutron_caseta/scene.py @@ -19,16 +19,10 @@ async def async_setup_entry( Adds scenes from the Caseta bridge associated with the config_entry as scene entities. """ - entities = [] data = hass.data[CASETA_DOMAIN][config_entry.entry_id] bridge = data[BRIDGE_LEAP] scenes = bridge.get_scenes() - - for scene in scenes: - entity = LutronCasetaScene(scenes[scene], bridge) - entities.append(entity) - - async_add_entities(entities, True) + async_add_entities(LutronCasetaScene(scenes[scene], bridge) for scene in scenes) class LutronCasetaScene(Scene): diff --git a/homeassistant/components/lutron_caseta/switch.py b/homeassistant/components/lutron_caseta/switch.py index 836968c3796..7e963352264 100644 --- a/homeassistant/components/lutron_caseta/switch.py +++ b/homeassistant/components/lutron_caseta/switch.py @@ -1,16 +1,13 @@ """Support for Lutron Caseta switches.""" -import logging from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import LutronCasetaDevice +from . import LutronCasetaDeviceUpdatableEntity from .const import BRIDGE_DEVICE, BRIDGE_LEAP, DOMAIN as CASETA_DOMAIN -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, @@ -22,21 +19,17 @@ async def async_setup_entry( Adds switches from the Caseta bridge associated with the config_entry as switch entities. """ - entities = [] data = hass.data[CASETA_DOMAIN][config_entry.entry_id] bridge = data[BRIDGE_LEAP] bridge_device = data[BRIDGE_DEVICE] switch_devices = bridge.get_devices_by_domain(DOMAIN) - - for switch_device in switch_devices: - entity = LutronCasetaLight(switch_device, bridge, bridge_device) - entities.append(entity) - - async_add_entities(entities, True) - return True + async_add_entities( + LutronCasetaLight(switch_device, bridge, bridge_device) + for switch_device in switch_devices + ) -class LutronCasetaLight(LutronCasetaDevice, SwitchEntity): +class LutronCasetaLight(LutronCasetaDeviceUpdatableEntity, SwitchEntity): """Representation of a Lutron Caseta switch.""" async def async_turn_on(self, **kwargs): @@ -51,8 +44,3 @@ class LutronCasetaLight(LutronCasetaDevice, SwitchEntity): def is_on(self): """Return true if device is on.""" return self._device["current_state"] > 0 - - async def async_update(self): - """Update when forcing a refresh of the device.""" - self._device = self._smartbridge.get_device_by_id(self.device_id) - _LOGGER.debug(self._device) From 5e59c3fd6d749c6885b7735019203ce118411ac9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 May 2022 15:36:47 -0500 Subject: [PATCH 0662/3516] Add switch platform to Big Ass Fans (#71954) --- .coveragerc | 1 + homeassistant/components/baf/__init__.py | 2 +- homeassistant/components/baf/switch.py | 148 +++++++++++++++++++++++ 3 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/baf/switch.py diff --git a/.coveragerc b/.coveragerc index bd7aa405b94..fbe98e4594f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -97,6 +97,7 @@ omit = homeassistant/components/baf/entity.py homeassistant/components/baf/fan.py homeassistant/components/baf/sensor.py + homeassistant/components/baf/switch.py homeassistant/components/baidu/tts.py homeassistant/components/balboa/__init__.py homeassistant/components/beewi_smartclim/sensor.py diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index f6401cbbc40..6e76014f0b7 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -14,7 +14,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN, QUERY_INTERVAL, RUN_TIMEOUT from .models import BAFData -PLATFORMS: list[Platform] = [Platform.FAN, Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.FAN, Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/baf/switch.py b/homeassistant/components/baf/switch.py new file mode 100644 index 00000000000..6cefa0db65d --- /dev/null +++ b/homeassistant/components/baf/switch.py @@ -0,0 +1,148 @@ +"""Support for Big Ass Fans switch.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any, Optional, cast + +from aiobafi6 import Device + +from homeassistant import config_entries +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import BAFEntity +from .models import BAFData + + +@dataclass +class BAFSwitchDescriptionMixin: + """Required values for BAF sensors.""" + + value_fn: Callable[[Device], bool | None] + + +@dataclass +class BAFSwitchDescription( + SwitchEntityDescription, + BAFSwitchDescriptionMixin, +): + """Class describing BAF switch entities.""" + + +BASE_SWITCHES = [ + BAFSwitchDescription( + key="legacy_ir_remote_enable", + name="Legacy IR Remote", + entity_category=EntityCategory.CONFIG, + value_fn=lambda device: cast(Optional[bool], device.legacy_ir_remote_enable), + ), + BAFSwitchDescription( + key="led_indicators_enable", + name="Led Indicators", + entity_category=EntityCategory.CONFIG, + value_fn=lambda device: cast(Optional[bool], device.led_indicators_enable), + ), +] + +FAN_SWITCHES = [ + BAFSwitchDescription( + key="comfort_heat_assist_enable", + name="Auto Comfort Heat Assist", + entity_category=EntityCategory.CONFIG, + value_fn=lambda device: cast(Optional[bool], device.comfort_heat_assist_enable), + ), + BAFSwitchDescription( + key="fan_beep_enable", + name="Beep", + entity_category=EntityCategory.CONFIG, + value_fn=lambda device: cast(Optional[bool], device.fan_beep_enable), + ), + BAFSwitchDescription( + key="eco_enable", + name="Eco Mode", + entity_category=EntityCategory.CONFIG, + value_fn=lambda device: cast(Optional[bool], device.eco_enable), + ), + BAFSwitchDescription( + key="motion_sense_enable", + name="Motion Sense", + entity_category=EntityCategory.CONFIG, + value_fn=lambda device: cast(Optional[bool], device.motion_sense_enable), + ), + BAFSwitchDescription( + key="return_to_auto_enable", + name="Return to Auto", + entity_category=EntityCategory.CONFIG, + value_fn=lambda device: cast(Optional[bool], device.return_to_auto_enable), + ), + BAFSwitchDescription( + key="whoosh_enable", + name="Whoosh", + # Not a configuration switch + value_fn=lambda device: cast(Optional[bool], device.whoosh_enable), + ), +] + + +LIGHT_SWITCHES = [ + BAFSwitchDescription( + key="light_dim_to_warm_enable", + name="Dim to Warm", + entity_category=EntityCategory.CONFIG, + value_fn=lambda device: cast(Optional[bool], device.light_dim_to_warm_enable), + ), + BAFSwitchDescription( + key="light_return_to_auto_enable", + name="Light Return to Auto", + entity_category=EntityCategory.CONFIG, + value_fn=lambda device: cast( + Optional[bool], device.light_return_to_auto_enable + ), + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up BAF fan switches.""" + data: BAFData = hass.data[DOMAIN][entry.entry_id] + device = data.device + descriptions: list[BAFSwitchDescription] = [] + descriptions.extend(BASE_SWITCHES) + if device.has_fan: + descriptions.extend(FAN_SWITCHES) + if device.has_light: + descriptions.extend(LIGHT_SWITCHES) + async_add_entities(BAFSwitch(device, description) for description in descriptions) + + +class BAFSwitch(BAFEntity, SwitchEntity): + """BAF switch component.""" + + entity_description: BAFSwitchDescription + + def __init__(self, device: Device, description: BAFSwitchDescription) -> None: + """Initialize the entity.""" + self.entity_description = description + super().__init__(device, f"{device.name} {description.name}") + self._attr_unique_id = f"{self._device.mac_address}-{description.key}" + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + self._attr_is_on = self.entity_description.value_fn(self._device) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the switch.""" + setattr(self._device, self.entity_description.key, True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the switch.""" + setattr(self._device, self.entity_description.key, False) From bf63d381b2d4daf6e923d2b738bc9b57c2f03b2a Mon Sep 17 00:00:00 2001 From: Jeef Date: Wed, 18 May 2022 14:54:52 -0600 Subject: [PATCH 0663/3516] IntelliFire On/Off Switches (#70377) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + .../components/intellifire/__init__.py | 2 +- .../components/intellifire/switch.py | 93 +++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/intellifire/switch.py diff --git a/.coveragerc b/.coveragerc index fbe98e4594f..5fabf2851fb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -550,6 +550,7 @@ omit = homeassistant/components/intellifire/coordinator.py homeassistant/components/intellifire/binary_sensor.py homeassistant/components/intellifire/sensor.py + homeassistant/components/intellifire/switch.py homeassistant/components/intellifire/entity.py homeassistant/components/incomfort/* homeassistant/components/intesishome/* diff --git a/homeassistant/components/intellifire/__init__.py b/homeassistant/components/intellifire/__init__.py index e139626b88c..83c6e05f572 100644 --- a/homeassistant/components/intellifire/__init__.py +++ b/homeassistant/components/intellifire/__init__.py @@ -13,7 +13,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import DOMAIN, LOGGER from .coordinator import IntellifireDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/intellifire/switch.py b/homeassistant/components/intellifire/switch.py new file mode 100644 index 00000000000..9c196a59fd4 --- /dev/null +++ b/homeassistant/components/intellifire/switch.py @@ -0,0 +1,93 @@ +"""Define switch func.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any + +from intellifire4py import IntellifireControlAsync, IntellifirePollData + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import IntellifireDataUpdateCoordinator +from .entity import IntellifireEntity + + +@dataclass() +class IntellifireSwitchRequiredKeysMixin: + """Mixin for required keys.""" + + on_fn: Callable[[IntellifireControlAsync], Awaitable] + off_fn: Callable[[IntellifireControlAsync], Awaitable] + value_fn: Callable[[IntellifirePollData], bool] + + +@dataclass +class IntellifireSwitchEntityDescription( + SwitchEntityDescription, IntellifireSwitchRequiredKeysMixin +): + """Describes a switch entity.""" + + +INTELLIFIRE_SWITCHES: tuple[IntellifireSwitchEntityDescription, ...] = ( + IntellifireSwitchEntityDescription( + key="on_off", + name="Flame", + on_fn=lambda control_api: control_api.flame_on( + fireplace=control_api.default_fireplace + ), + off_fn=lambda control_api: control_api.flame_off( + fireplace=control_api.default_fireplace + ), + value_fn=lambda data: data.is_on, + ), + IntellifireSwitchEntityDescription( + key="pilot", + name="Pilot Light", + icon="mdi:fire-alert", + on_fn=lambda control_api: control_api.pilot_on( + fireplace=control_api.default_fireplace + ), + off_fn=lambda control_api: control_api.pilot_off( + fireplace=control_api.default_fireplace + ), + value_fn=lambda data: data.pilot_on, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Configure switch entities.""" + coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + IntellifireSwitch(coordinator=coordinator, description=description) + for description in INTELLIFIRE_SWITCHES + ) + + +class IntellifireSwitch(IntellifireEntity, SwitchEntity): + """Define an Intellifire Switch.""" + + entity_description: IntellifireSwitchEntityDescription + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the switch.""" + await self.entity_description.on_fn(self.coordinator.control_api) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the switch.""" + await self.entity_description.off_fn(self.coordinator.control_api) + + @property + def is_on(self) -> bool | None: + """Return the on state.""" + return self.entity_description.value_fn(self.coordinator.read_api.data) From d8a580a90f8bf3206b31619493f4e653fceb3f4b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 May 2022 18:08:02 -0500 Subject: [PATCH 0664/3516] Update nexia to use asyncio (#72108) --- homeassistant/components/nexia/__init__.py | 40 ++++++----- homeassistant/components/nexia/climate.py | 72 ++++++++++--------- homeassistant/components/nexia/config_flow.py | 30 ++++---- homeassistant/components/nexia/coordinator.py | 2 +- homeassistant/components/nexia/entity.py | 13 ++-- homeassistant/components/nexia/manifest.json | 2 +- homeassistant/components/nexia/scene.py | 8 ++- homeassistant/components/nexia/sensor.py | 3 +- homeassistant/components/nexia/switch.py | 8 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nexia/test_config_flow.py | 22 +++--- tests/components/nexia/util.py | 15 ++-- 13 files changed, 121 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index 634f69b49d3..ab491c0a271 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -1,15 +1,16 @@ """Support for Nexia / Trane XL Thermostats.""" -from functools import partial +import asyncio import logging +import aiohttp from nexia.const import BRAND_NEXIA from nexia.home import NexiaHome -from requests.exceptions import ConnectTimeout, HTTPError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from .const import CONF_BRAND, DOMAIN, PLATFORMS @@ -30,31 +31,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: brand = conf.get(CONF_BRAND, BRAND_NEXIA) state_file = hass.config.path(f"nexia_config_{username}.conf") + session = async_get_clientsession(hass) + nexia_home = NexiaHome( + session, + username=username, + password=password, + device_name=hass.config.location_name, + state_file=state_file, + brand=brand, + ) try: - nexia_home = await hass.async_add_executor_job( - partial( - NexiaHome, - username=username, - password=password, - device_name=hass.config.location_name, - state_file=state_file, - brand=brand, - ) - ) - except ConnectTimeout as ex: - _LOGGER.error("Unable to connect to Nexia service: %s", ex) - raise ConfigEntryNotReady from ex - except HTTPError as http_ex: - if is_invalid_auth_code(http_ex.response.status_code): + await nexia_home.login() + except asyncio.TimeoutError as ex: + raise ConfigEntryNotReady( + f"Timed out trying to connect to Nexia service: {ex}" + ) from ex + except aiohttp.ClientResponseError as http_ex: + if is_invalid_auth_code(http_ex.status): _LOGGER.error( "Access error from Nexia service, please check credentials: %s", http_ex ) return False - _LOGGER.error("HTTP error from Nexia service: %s", http_ex) - raise ConfigEntryNotReady from http_ex + raise ConfigEntryNotReady(f"Error from Nexia service: {http_ex}") from http_ex coordinator = NexiaDataUpdateCoordinator(hass, nexia_home) + await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 7eeb04c5676..0c27b1497f5 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -123,13 +123,17 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_SET_HUMIDIFY_SETPOINT, SET_HUMIDITY_SCHEMA, - SERVICE_SET_HUMIDIFY_SETPOINT, + f"async_{SERVICE_SET_HUMIDIFY_SETPOINT}", ) platform.async_register_entity_service( - SERVICE_SET_AIRCLEANER_MODE, SET_AIRCLEANER_SCHEMA, SERVICE_SET_AIRCLEANER_MODE + SERVICE_SET_AIRCLEANER_MODE, + SET_AIRCLEANER_SCHEMA, + f"async_{SERVICE_SET_AIRCLEANER_MODE}", ) platform.async_register_entity_service( - SERVICE_SET_HVAC_RUN_MODE, SET_HVAC_RUN_MODE_SCHEMA, SERVICE_SET_HVAC_RUN_MODE + SERVICE_SET_HVAC_RUN_MODE, + SET_HVAC_RUN_MODE_SCHEMA, + f"async_{SERVICE_SET_HVAC_RUN_MODE}", ) entities: list[NexiaZone] = [] @@ -192,20 +196,20 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): """Return the fan setting.""" return self._thermostat.get_fan_mode() - def set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" - self._thermostat.set_fan_mode(fan_mode) + await self._thermostat.set_fan_mode(fan_mode) self._signal_thermostat_update() - def set_hvac_run_mode(self, run_mode, hvac_mode): + async def async_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() + await self._zone.call_permanent_hold() else: - self._zone.call_return_to_schedule() + await 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]) + await self._zone.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode]) self._signal_thermostat_update() @property @@ -213,12 +217,12 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): """Preset that is active.""" return self._zone.get_preset() - def set_humidity(self, humidity): + async def async_set_humidity(self, humidity): """Dehumidify target.""" if self._thermostat.has_dehumidify_support(): - self.set_dehumidify_setpoint(humidity) + await self.async_set_dehumidify_setpoint(humidity) else: - self.set_humidify_setpoint(humidity) + await self.async_set_humidify_setpoint(humidity) self._signal_thermostat_update() @property @@ -300,7 +304,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): return NEXIA_TO_HA_HVAC_MODE_MAP[mode] - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set target temperature.""" new_heat_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) new_cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) @@ -332,7 +336,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): ): new_heat_temp = new_cool_temp - deadband - self._zone.set_heat_cool_temp( + await self._zone.set_heat_cool_temp( heat_temperature=new_heat_temp, cool_temperature=new_cool_temp, set_temperature=set_temp, @@ -366,63 +370,63 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): return data - def set_preset_mode(self, preset_mode: str): + async def async_set_preset_mode(self, preset_mode: str): """Set the preset mode.""" - self._zone.set_preset(preset_mode) + await self._zone.set_preset(preset_mode) self._signal_zone_update() - def turn_aux_heat_off(self): + async def async_turn_aux_heat_off(self): """Turn. Aux Heat off.""" - self._thermostat.set_emergency_heat(False) + await self._thermostat.set_emergency_heat(False) self._signal_thermostat_update() - def turn_aux_heat_on(self): + async def async_turn_aux_heat_on(self): """Turn. Aux Heat on.""" self._thermostat.set_emergency_heat(True) self._signal_thermostat_update() - def turn_off(self): + async def async_turn_off(self): """Turn. off the zone.""" - self.set_hvac_mode(OPERATION_MODE_OFF) + await self.set_hvac_mode(OPERATION_MODE_OFF) self._signal_zone_update() - def turn_on(self): + async def async_turn_on(self): """Turn. on the zone.""" - self.set_hvac_mode(OPERATION_MODE_AUTO) + await self.set_hvac_mode(OPERATION_MODE_AUTO) self._signal_zone_update() - def set_hvac_mode(self, hvac_mode: HVACMode) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the system mode (Auto, Heat_Cool, Cool, Heat, etc).""" if hvac_mode == HVACMode.AUTO: - self._zone.call_return_to_schedule() - self._zone.set_mode(mode=OPERATION_MODE_AUTO) + await self._zone.call_return_to_schedule() + await self._zone.set_mode(mode=OPERATION_MODE_AUTO) else: - self._zone.call_permanent_hold() - self._zone.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode]) + await self._zone.call_permanent_hold() + await self._zone.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode]) self._signal_zone_update() - def set_aircleaner_mode(self, aircleaner_mode): + async def async_set_aircleaner_mode(self, aircleaner_mode): """Set the aircleaner mode.""" - self._thermostat.set_air_cleaner(aircleaner_mode) + await self._thermostat.set_air_cleaner(aircleaner_mode) self._signal_thermostat_update() - def set_humidify_setpoint(self, humidity): + async def async_set_humidify_setpoint(self, humidity): """Set the humidify setpoint.""" target_humidity = find_humidity_setpoint(humidity / 100.0) if self._thermostat.get_humidify_setpoint() == target_humidity: # Trying to set the humidify setpoint to the # same value will cause the api to timeout return - self._thermostat.set_humidify_setpoint(target_humidity) + await self._thermostat.set_humidify_setpoint(target_humidity) self._signal_thermostat_update() - def set_dehumidify_setpoint(self, humidity): + async def async_set_dehumidify_setpoint(self, humidity): """Set the dehumidify setpoint.""" target_humidity = find_humidity_setpoint(humidity / 100.0) if self._thermostat.get_dehumidify_setpoint() == target_humidity: # Trying to set the dehumidify setpoint to the # same value will cause the api to timeout return - self._thermostat.set_dehumidify_setpoint(target_humidity) + await self._thermostat.set_dehumidify_setpoint(target_humidity) self._signal_thermostat_update() diff --git a/homeassistant/components/nexia/config_flow.py b/homeassistant/components/nexia/config_flow.py index 4e48123a5de..de5640beef7 100644 --- a/homeassistant/components/nexia/config_flow.py +++ b/homeassistant/components/nexia/config_flow.py @@ -1,13 +1,15 @@ """Config flow for Nexia integration.""" +import asyncio import logging +import aiohttp from nexia.const import BRAND_ASAIR, BRAND_NEXIA, BRAND_TRANE from nexia.home import NexiaHome -from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( BRAND_ASAIR_NAME, @@ -44,23 +46,23 @@ async def validate_input(hass: core.HomeAssistant, data): state_file = hass.config.path( f"{data[CONF_BRAND]}_config_{data[CONF_USERNAME]}.conf" ) + session = async_get_clientsession(hass) + nexia_home = NexiaHome( + session, + username=data[CONF_USERNAME], + password=data[CONF_PASSWORD], + brand=data[CONF_BRAND], + device_name=hass.config.location_name, + state_file=state_file, + ) try: - nexia_home = NexiaHome( - username=data[CONF_USERNAME], - password=data[CONF_PASSWORD], - brand=data[CONF_BRAND], - auto_login=False, - auto_update=False, - device_name=hass.config.location_name, - state_file=state_file, - ) - await hass.async_add_executor_job(nexia_home.login) - except ConnectTimeout as ex: + await nexia_home.login() + except asyncio.TimeoutError as ex: _LOGGER.error("Unable to connect to Nexia service: %s", ex) raise CannotConnect from ex - except HTTPError as http_ex: + except aiohttp.ClientResponseError as http_ex: _LOGGER.error("HTTP error from Nexia service: %s", http_ex) - if is_invalid_auth_code(http_ex.response.status_code): + if is_invalid_auth_code(http_ex.status): raise InvalidAuth from http_ex raise CannotConnect from http_ex diff --git a/homeassistant/components/nexia/coordinator.py b/homeassistant/components/nexia/coordinator.py index 7f92ca9354b..ba61a3591f0 100644 --- a/homeassistant/components/nexia/coordinator.py +++ b/homeassistant/components/nexia/coordinator.py @@ -33,4 +33,4 @@ class NexiaDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> None: """Fetch data from API endpoint.""" - return await self.hass.async_add_executor_job(self.nexia_home.update) + return await self.nexia_home.update() diff --git a/homeassistant/components/nexia/entity.py b/homeassistant/components/nexia/entity.py index 0be5c05396d..dde0e5ae8f3 100644 --- a/homeassistant/components/nexia/entity.py +++ b/homeassistant/components/nexia/entity.py @@ -3,7 +3,10 @@ from nexia.thermostat import NexiaThermostat from nexia.zone import NexiaThermostatZone from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -20,7 +23,9 @@ from .coordinator import NexiaDataUpdateCoordinator class NexiaEntity(CoordinatorEntity): """Base class for nexia entities.""" - def __init__(self, coordinator, name, unique_id): + def __init__( + self, coordinator: NexiaDataUpdateCoordinator, name: str, unique_id: str + ) -> None: """Initialize the entity.""" super().__init__(coordinator) self._unique_id = unique_id @@ -85,7 +90,7 @@ class NexiaThermostatEntity(NexiaEntity): Update all the zones on the thermostat. """ - dispatcher_send( + async_dispatcher_send( self.hass, f"{SIGNAL_THERMOSTAT_UPDATE}-{self._thermostat.thermostat_id}" ) @@ -132,4 +137,4 @@ class NexiaThermostatZoneEntity(NexiaThermostatEntity): Update a single zone. """ - dispatcher_send(self.hass, f"{SIGNAL_ZONE_UPDATE}-{self._zone.zone_id}") + async_dispatcher_send(self.hass, f"{SIGNAL_ZONE_UPDATE}-{self._zone.zone_id}") diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 29b80fb00e9..bd1cd58c00b 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==0.9.13"], + "requirements": ["nexia==1.0.0"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/homeassistant/components/nexia/scene.py b/homeassistant/components/nexia/scene.py index 3dd90b07ca2..28c892fe8e5 100644 --- a/homeassistant/components/nexia/scene.py +++ b/homeassistant/components/nexia/scene.py @@ -1,6 +1,8 @@ """Support for Nexia Automations.""" from typing import Any +from nexia.automation import NexiaAutomation + from homeassistant.components.scene import Scene from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -37,7 +39,9 @@ async def async_setup_entry( class NexiaAutomationScene(NexiaEntity, Scene): """Provides Nexia automation support.""" - def __init__(self, coordinator, automation): + def __init__( + self, coordinator: NexiaDataUpdateCoordinator, automation: NexiaAutomation + ) -> None: """Initialize the automation scene.""" super().__init__( coordinator, @@ -60,7 +64,7 @@ class NexiaAutomationScene(NexiaEntity, Scene): async def async_activate(self, **kwargs: Any) -> None: """Activate an automation scene.""" - await self.hass.async_add_executor_job(self._automation.activate) + await self._automation.activate() async def refresh_callback(_): await self.coordinator.async_refresh() diff --git a/homeassistant/components/nexia/sensor.py b/homeassistant/components/nexia/sensor.py index 8137da8e5cc..3f280581ee7 100644 --- a/homeassistant/components/nexia/sensor.py +++ b/homeassistant/components/nexia/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from nexia.const import UNIT_CELSIUS +from nexia.thermostat import NexiaThermostat from homeassistant.components.sensor import ( SensorDeviceClass, @@ -32,7 +33,7 @@ async def async_setup_entry( # Thermostat / System Sensors for thermostat_id in nexia_home.get_thermostat_ids(): - thermostat = nexia_home.get_thermostat_by_id(thermostat_id) + thermostat: NexiaThermostat = nexia_home.get_thermostat_by_id(thermostat_id) entities.append( NexiaThermostatSensor( diff --git a/homeassistant/components/nexia/switch.py b/homeassistant/components/nexia/switch.py index 09bc8a3852e..380fea8c4a0 100644 --- a/homeassistant/components/nexia/switch.py +++ b/homeassistant/components/nexia/switch.py @@ -56,12 +56,12 @@ class NexiaHoldSwitch(NexiaThermostatZoneEntity, SwitchEntity): """Return the icon for the switch.""" return "mdi:timer-off" if self._zone.is_in_permanent_hold() else "mdi:timer" - def turn_on(self, **kwargs: Any) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Enable permanent hold.""" - self._zone.call_permanent_hold() + await self._zone.call_permanent_hold() self._signal_zone_update() - def turn_off(self, **kwargs: Any) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Disable permanent hold.""" - self._zone.call_return_to_schedule() + await self._zone.call_return_to_schedule() self._signal_zone_update() diff --git a/requirements_all.txt b/requirements_all.txt index bddac66cb90..05403b5d3aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ nettigo-air-monitor==1.2.4 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.9.13 +nexia==1.0.0 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 522583f4ce4..d11047f4c95 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -742,7 +742,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.2.4 # homeassistant.components.nexia -nexia==0.9.13 +nexia==1.0.0 # homeassistant.components.discord nextcord==2.0.0a8 diff --git a/tests/components/nexia/test_config_flow.py b/tests/components/nexia/test_config_flow.py index bd3eb9180e7..5bfd7fb582c 100644 --- a/tests/components/nexia/test_config_flow.py +++ b/tests/components/nexia/test_config_flow.py @@ -1,9 +1,10 @@ """Test the nexia config flow.""" +import asyncio from unittest.mock import MagicMock, patch +import aiohttp from nexia.const import BRAND_ASAIR, BRAND_NEXIA import pytest -from requests.exceptions import ConnectTimeout, HTTPError from homeassistant import config_entries from homeassistant.components.nexia.const import CONF_BRAND, DOMAIN @@ -52,7 +53,10 @@ async def test_form_invalid_auth(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("homeassistant.components.nexia.config_flow.NexiaHome.login"): + with patch("homeassistant.components.nexia.config_flow.NexiaHome.login",), patch( + "homeassistant.components.nexia.config_flow.NexiaHome.get_name", + return_value=None, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -74,7 +78,7 @@ async def test_form_cannot_connect(hass): with patch( "homeassistant.components.nexia.config_flow.NexiaHome.login", - side_effect=ConnectTimeout, + side_effect=asyncio.TimeoutError, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -95,11 +99,11 @@ async def test_form_invalid_auth_http_401(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - response_mock = MagicMock() - type(response_mock).status_code = 401 with patch( "homeassistant.components.nexia.config_flow.NexiaHome.login", - side_effect=HTTPError(response=response_mock), + side_effect=aiohttp.ClientResponseError( + status=401, request_info=MagicMock(), history=MagicMock() + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -120,11 +124,11 @@ async def test_form_cannot_connect_not_found(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - response_mock = MagicMock() - type(response_mock).status_code = 404 with patch( "homeassistant.components.nexia.config_flow.NexiaHome.login", - side_effect=HTTPError(response=response_mock), + side_effect=aiohttp.ClientResponseError( + status=404, request_info=MagicMock(), history=MagicMock() + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/nexia/util.py b/tests/components/nexia/util.py index b6d5c697a18..d564ccc351c 100644 --- a/tests/components/nexia/util.py +++ b/tests/components/nexia/util.py @@ -3,13 +3,13 @@ from unittest.mock import patch import uuid from nexia.home import NexiaHome -import requests_mock from homeassistant.components.nexia.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import mock_aiohttp_client async def async_init_integration( @@ -21,17 +21,18 @@ async def async_init_integration( house_fixture = "nexia/mobile_houses_123456.json" session_fixture = "nexia/session_123456.json" sign_in_fixture = "nexia/sign_in.json" - nexia = NexiaHome(auto_login=False) - - with requests_mock.mock() as m, patch( + with mock_aiohttp_client() as mock_session, patch( "nexia.home.load_or_create_uuid", return_value=uuid.uuid4() ): - m.post(nexia.API_MOBILE_SESSION_URL, text=load_fixture(session_fixture)) - m.get( + nexia = NexiaHome(mock_session) + mock_session.post( + nexia.API_MOBILE_SESSION_URL, text=load_fixture(session_fixture) + ) + mock_session.get( nexia.API_MOBILE_HOUSES_URL.format(house_id=123456), text=load_fixture(house_fixture), ) - m.post( + mock_session.post( nexia.API_MOBILE_ACCOUNTS_SIGN_IN_URL, text=load_fixture(sign_in_fixture), ) From 506d09d0580dc8550dda5b0a36390dcf768de411 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 18 May 2022 17:27:58 -0600 Subject: [PATCH 0665/3516] Increase timeout for SimpliSafe email-based 2FA to 10 minutes (#72115) Increase timeout for SimpliSafe email-based 2FAA to 10 minutes --- homeassistant/components/simplisafe/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 926f904a912..14afc743b23 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -20,7 +20,7 @@ from homeassistant.helpers import aiohttp_client, config_validation as cv from .const import DOMAIN, LOGGER DEFAULT_EMAIL_2FA_SLEEP = 3 -DEFAULT_EMAIL_2FA_TIMEOUT = 300 +DEFAULT_EMAIL_2FA_TIMEOUT = 600 STEP_REAUTH_SCHEMA = vol.Schema( { From 3a13ffcf1376bafff6a32df19b197083abf6e826 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 19 May 2022 00:26:11 +0000 Subject: [PATCH 0666/3516] [ci skip] Translation update --- .../accuweather/translations/ar.json | 9 +-- .../accuweather/translations/bg.json | 3 +- .../accuweather/translations/ca.json | 7 +- .../accuweather/translations/cs.json | 7 +- .../accuweather/translations/de.json | 7 +- .../accuweather/translations/el.json | 7 +- .../accuweather/translations/en.json | 7 +- .../accuweather/translations/es-419.json | 9 +-- .../accuweather/translations/es.json | 7 +- .../accuweather/translations/et.json | 7 +- .../accuweather/translations/fr.json | 7 +- .../accuweather/translations/he.json | 7 +- .../accuweather/translations/hu.json | 7 +- .../accuweather/translations/id.json | 7 +- .../accuweather/translations/it.json | 7 +- .../accuweather/translations/ja.json | 7 +- .../accuweather/translations/ko.json | 7 +- .../accuweather/translations/lb.json | 7 +- .../accuweather/translations/nl.json | 7 +- .../accuweather/translations/no.json | 7 +- .../accuweather/translations/pl.json | 7 +- .../accuweather/translations/pt-BR.json | 7 +- .../accuweather/translations/ru.json | 7 +- .../accuweather/translations/tr.json | 7 +- .../accuweather/translations/uk.json | 7 +- .../accuweather/translations/zh-Hant.json | 7 +- .../components/adax/translations/bg.json | 9 +-- .../components/adax/translations/ca.json | 8 +- .../components/adax/translations/cs.json | 8 +- .../components/adax/translations/de.json | 8 +- .../components/adax/translations/el.json | 8 +- .../components/adax/translations/en.json | 8 +- .../components/adax/translations/es-419.json | 1 - .../components/adax/translations/es.json | 8 +- .../components/adax/translations/et.json | 8 +- .../components/adax/translations/fr.json | 8 +- .../components/adax/translations/he.json | 10 +-- .../components/adax/translations/hu.json | 8 +- .../components/adax/translations/id.json | 8 +- .../components/adax/translations/it.json | 8 +- .../components/adax/translations/ja.json | 8 +- .../components/adax/translations/nl.json | 8 +- .../components/adax/translations/no.json | 8 +- .../components/adax/translations/pl.json | 8 +- .../components/adax/translations/pt-BR.json | 8 +- .../components/adax/translations/ru.json | 8 +- .../components/adax/translations/sk.json | 10 --- .../components/adax/translations/tr.json | 8 +- .../components/adax/translations/zh-Hans.json | 3 +- .../components/adax/translations/zh-Hant.json | 8 +- .../components/aemet/translations/ca.json | 3 +- .../components/aemet/translations/de.json | 3 +- .../components/aemet/translations/el.json | 3 +- .../components/aemet/translations/en.json | 3 +- .../components/aemet/translations/es-419.json | 3 +- .../components/aemet/translations/es.json | 3 +- .../components/aemet/translations/et.json | 3 +- .../components/aemet/translations/fr.json | 3 +- .../components/aemet/translations/hu.json | 3 +- .../components/aemet/translations/id.json | 3 +- .../components/aemet/translations/it.json | 3 +- .../components/aemet/translations/ja.json | 3 +- .../components/aemet/translations/ko.json | 3 +- .../components/aemet/translations/lb.json | 3 +- .../components/aemet/translations/nl.json | 3 +- .../components/aemet/translations/no.json | 3 +- .../components/aemet/translations/pl.json | 3 +- .../components/aemet/translations/pt-BR.json | 3 +- .../components/aemet/translations/ru.json | 3 +- .../components/aemet/translations/tr.json | 3 +- .../aemet/translations/zh-Hant.json | 3 +- .../components/airly/translations/bg.json | 3 +- .../components/airly/translations/ca.json | 3 +- .../components/airly/translations/cs.json | 3 +- .../components/airly/translations/da.json | 3 +- .../components/airly/translations/de.json | 3 +- .../components/airly/translations/el.json | 3 +- .../components/airly/translations/en.json | 3 +- .../components/airly/translations/es-419.json | 3 +- .../components/airly/translations/es.json | 3 +- .../components/airly/translations/et.json | 3 +- .../components/airly/translations/fr.json | 3 +- .../components/airly/translations/he.json | 3 +- .../components/airly/translations/hu.json | 3 +- .../components/airly/translations/id.json | 3 +- .../components/airly/translations/it.json | 3 +- .../components/airly/translations/ja.json | 3 +- .../components/airly/translations/ko.json | 3 +- .../components/airly/translations/lb.json | 3 +- .../components/airly/translations/nl.json | 3 +- .../components/airly/translations/nn.json | 9 --- .../components/airly/translations/no.json | 3 +- .../components/airly/translations/pl.json | 3 +- .../components/airly/translations/pt-BR.json | 3 +- .../components/airly/translations/pt.json | 3 +- .../components/airly/translations/ru.json | 3 +- .../components/airly/translations/sl.json | 3 +- .../components/airly/translations/sv.json | 3 +- .../components/airly/translations/tr.json | 3 +- .../components/airly/translations/uk.json | 3 +- .../airly/translations/zh-Hant.json | 3 +- .../components/airnow/translations/ca.json | 6 +- .../components/airnow/translations/cs.json | 6 +- .../components/airnow/translations/de.json | 6 +- .../components/airnow/translations/el.json | 6 +- .../components/airnow/translations/en.json | 6 +- .../airnow/translations/es-419.json | 6 +- .../components/airnow/translations/es.json | 6 +- .../components/airnow/translations/et.json | 6 +- .../components/airnow/translations/fr.json | 6 +- .../components/airnow/translations/hu.json | 6 +- .../components/airnow/translations/id.json | 6 +- .../components/airnow/translations/it.json | 6 +- .../components/airnow/translations/ja.json | 6 +- .../components/airnow/translations/ko.json | 6 +- .../components/airnow/translations/lb.json | 6 +- .../components/airnow/translations/nl.json | 6 +- .../components/airnow/translations/no.json | 6 +- .../components/airnow/translations/pl.json | 6 +- .../components/airnow/translations/pt-BR.json | 6 +- .../components/airnow/translations/pt.json | 6 +- .../components/airnow/translations/ru.json | 6 +- .../components/airnow/translations/tr.json | 6 +- .../components/airnow/translations/uk.json | 6 +- .../airnow/translations/zh-Hant.json | 6 +- .../aladdin_connect/translations/cs.json | 22 ++++++ .../aladdin_connect/translations/he.json | 26 +++++++ .../amberelectric/translations/bg.json | 6 +- .../amberelectric/translations/ca.json | 6 +- .../amberelectric/translations/de.json | 6 +- .../amberelectric/translations/el.json | 6 +- .../amberelectric/translations/en.json | 6 +- .../amberelectric/translations/es.json | 6 +- .../amberelectric/translations/et.json | 6 +- .../amberelectric/translations/fr.json | 6 +- .../amberelectric/translations/hu.json | 6 +- .../amberelectric/translations/id.json | 6 +- .../amberelectric/translations/it.json | 6 +- .../amberelectric/translations/ja.json | 6 +- .../amberelectric/translations/nl.json | 6 +- .../amberelectric/translations/no.json | 6 +- .../amberelectric/translations/pl.json | 6 +- .../amberelectric/translations/pt-BR.json | 6 +- .../amberelectric/translations/ru.json | 6 +- .../amberelectric/translations/tr.json | 6 +- .../amberelectric/translations/zh-Hant.json | 6 +- .../components/androidtv/translations/bg.json | 3 +- .../components/androidtv/translations/ca.json | 7 +- .../components/androidtv/translations/de.json | 7 +- .../components/androidtv/translations/el.json | 7 +- .../components/androidtv/translations/en.json | 7 +- .../androidtv/translations/es-419.json | 7 +- .../components/androidtv/translations/es.json | 7 +- .../components/androidtv/translations/et.json | 7 +- .../components/androidtv/translations/fr.json | 7 +- .../components/androidtv/translations/he.json | 7 +- .../components/androidtv/translations/hu.json | 7 +- .../components/androidtv/translations/id.json | 7 +- .../components/androidtv/translations/it.json | 7 +- .../components/androidtv/translations/ja.json | 7 +- .../components/androidtv/translations/nl.json | 7 +- .../components/androidtv/translations/no.json | 7 +- .../components/androidtv/translations/pl.json | 7 +- .../androidtv/translations/pt-BR.json | 7 +- .../components/androidtv/translations/ru.json | 7 +- .../components/androidtv/translations/tr.json | 7 +- .../androidtv/translations/zh-Hans.json | 7 +- .../androidtv/translations/zh-Hant.json | 7 +- .../components/apple_tv/translations/bg.json | 4 +- .../components/apple_tv/translations/ca.json | 6 +- .../components/apple_tv/translations/cs.json | 5 +- .../components/apple_tv/translations/de.json | 6 +- .../components/apple_tv/translations/el.json | 6 +- .../components/apple_tv/translations/en.json | 6 +- .../apple_tv/translations/es-419.json | 9 +-- .../components/apple_tv/translations/es.json | 6 +- .../components/apple_tv/translations/et.json | 6 +- .../components/apple_tv/translations/fr.json | 6 +- .../components/apple_tv/translations/he.json | 1 - .../components/apple_tv/translations/hu.json | 6 +- .../components/apple_tv/translations/id.json | 6 +- .../components/apple_tv/translations/it.json | 6 +- .../components/apple_tv/translations/ja.json | 6 +- .../components/apple_tv/translations/ko.json | 6 +- .../components/apple_tv/translations/lb.json | 5 +- .../components/apple_tv/translations/nl.json | 6 +- .../components/apple_tv/translations/no.json | 6 +- .../components/apple_tv/translations/pl.json | 6 +- .../apple_tv/translations/pt-BR.json | 6 +- .../components/apple_tv/translations/pt.json | 5 +- .../components/apple_tv/translations/ru.json | 6 +- .../components/apple_tv/translations/sl.json | 6 +- .../components/apple_tv/translations/tr.json | 6 +- .../components/apple_tv/translations/uk.json | 6 +- .../apple_tv/translations/zh-Hans.json | 6 +- .../apple_tv/translations/zh-Hant.json | 6 +- .../translations/ko.json | 3 + .../components/asuswrt/translations/ca.json | 4 +- .../components/asuswrt/translations/cs.json | 3 - .../components/asuswrt/translations/de.json | 4 +- .../components/asuswrt/translations/el.json | 4 +- .../components/asuswrt/translations/en.json | 4 +- .../components/asuswrt/translations/es.json | 3 +- .../components/asuswrt/translations/et.json | 4 +- .../components/asuswrt/translations/fr.json | 4 +- .../components/asuswrt/translations/he.json | 3 - .../components/asuswrt/translations/hu.json | 4 +- .../components/asuswrt/translations/id.json | 4 +- .../components/asuswrt/translations/it.json | 4 +- .../components/asuswrt/translations/ja.json | 4 +- .../components/asuswrt/translations/ko.json | 3 +- .../components/asuswrt/translations/nl.json | 4 +- .../components/asuswrt/translations/no.json | 4 +- .../components/asuswrt/translations/pl.json | 4 +- .../asuswrt/translations/pt-BR.json | 4 +- .../components/asuswrt/translations/ru.json | 4 +- .../components/asuswrt/translations/sv.json | 4 +- .../components/asuswrt/translations/tr.json | 4 +- .../asuswrt/translations/zh-Hans.json | 3 - .../asuswrt/translations/zh-Hant.json | 4 +- .../aurora_abb_powerone/translations/bg.json | 3 - .../aurora_abb_powerone/translations/ca.json | 3 +- .../aurora_abb_powerone/translations/cs.json | 7 -- .../aurora_abb_powerone/translations/de.json | 3 +- .../aurora_abb_powerone/translations/el.json | 3 +- .../aurora_abb_powerone/translations/en.json | 3 +- .../translations/en_GB.json | 3 - .../aurora_abb_powerone/translations/es.json | 3 +- .../aurora_abb_powerone/translations/et.json | 3 +- .../aurora_abb_powerone/translations/fr.json | 3 +- .../aurora_abb_powerone/translations/he.json | 3 - .../aurora_abb_powerone/translations/hu.json | 3 +- .../aurora_abb_powerone/translations/id.json | 3 +- .../aurora_abb_powerone/translations/it.json | 3 +- .../aurora_abb_powerone/translations/ja.json | 3 +- .../aurora_abb_powerone/translations/nl.json | 3 +- .../aurora_abb_powerone/translations/no.json | 3 +- .../aurora_abb_powerone/translations/pl.json | 3 +- .../translations/pt-BR.json | 3 +- .../aurora_abb_powerone/translations/ru.json | 3 +- .../aurora_abb_powerone/translations/sl.json | 3 - .../aurora_abb_powerone/translations/tr.json | 3 +- .../translations/zh-Hant.json | 3 +- .../aussie_broadband/translations/bg.json | 7 -- .../aussie_broadband/translations/ca.json | 7 -- .../aussie_broadband/translations/cs.json | 6 -- .../aussie_broadband/translations/de.json | 7 -- .../aussie_broadband/translations/el.json | 7 -- .../aussie_broadband/translations/en.json | 7 -- .../aussie_broadband/translations/es-419.json | 3 - .../aussie_broadband/translations/es.json | 4 - .../aussie_broadband/translations/et.json | 7 -- .../aussie_broadband/translations/fr.json | 7 -- .../aussie_broadband/translations/he.json | 6 -- .../aussie_broadband/translations/hu.json | 7 -- .../aussie_broadband/translations/id.json | 7 -- .../aussie_broadband/translations/it.json | 7 -- .../aussie_broadband/translations/ja.json | 7 -- .../aussie_broadband/translations/nl.json | 7 -- .../aussie_broadband/translations/no.json | 7 -- .../aussie_broadband/translations/pl.json | 7 -- .../aussie_broadband/translations/pt-BR.json | 7 -- .../aussie_broadband/translations/ru.json | 7 -- .../aussie_broadband/translations/sv.json | 7 -- .../aussie_broadband/translations/tr.json | 7 -- .../translations/zh-Hant.json | 7 -- .../azure_event_hub/translations/ko.json | 8 ++ .../components/baf/translations/cs.json | 18 +++++ .../components/baf/translations/he.json | 19 +++++ .../binary_sensor/translations/ca.json | 6 -- .../binary_sensor/translations/cs.json | 2 - .../binary_sensor/translations/de.json | 6 -- .../binary_sensor/translations/el.json | 6 -- .../binary_sensor/translations/en.json | 6 -- .../binary_sensor/translations/es-419.json | 4 - .../binary_sensor/translations/es.json | 6 -- .../binary_sensor/translations/et.json | 6 -- .../binary_sensor/translations/fr.json | 6 -- .../binary_sensor/translations/he.json | 6 -- .../binary_sensor/translations/hu.json | 6 -- .../binary_sensor/translations/id.json | 6 -- .../binary_sensor/translations/it.json | 6 -- .../binary_sensor/translations/ja.json | 6 -- .../binary_sensor/translations/nl.json | 6 -- .../binary_sensor/translations/no.json | 6 -- .../binary_sensor/translations/pl.json | 6 -- .../binary_sensor/translations/pt-BR.json | 6 -- .../binary_sensor/translations/ru.json | 6 -- .../binary_sensor/translations/sl.json | 2 - .../binary_sensor/translations/tr.json | 6 -- .../binary_sensor/translations/zh-Hans.json | 6 -- .../binary_sensor/translations/zh-Hant.json | 6 -- .../bmw_connected_drive/translations/ca.json | 3 +- .../bmw_connected_drive/translations/de.json | 3 +- .../bmw_connected_drive/translations/el.json | 3 +- .../bmw_connected_drive/translations/en.json | 3 +- .../translations/es-419.json | 3 +- .../bmw_connected_drive/translations/es.json | 3 +- .../bmw_connected_drive/translations/et.json | 3 +- .../bmw_connected_drive/translations/fr.json | 3 +- .../bmw_connected_drive/translations/hu.json | 3 +- .../bmw_connected_drive/translations/id.json | 3 +- .../bmw_connected_drive/translations/it.json | 3 +- .../bmw_connected_drive/translations/ja.json | 3 +- .../bmw_connected_drive/translations/ko.json | 3 +- .../bmw_connected_drive/translations/nl.json | 3 +- .../bmw_connected_drive/translations/no.json | 3 +- .../bmw_connected_drive/translations/pl.json | 3 +- .../translations/pt-BR.json | 3 +- .../bmw_connected_drive/translations/ru.json | 3 +- .../bmw_connected_drive/translations/sv.json | 3 +- .../bmw_connected_drive/translations/tr.json | 3 +- .../bmw_connected_drive/translations/uk.json | 3 +- .../translations/zh-Hant.json | 3 +- .../components/bosch_shc/translations/bg.json | 3 +- .../components/bosch_shc/translations/ca.json | 3 +- .../components/bosch_shc/translations/de.json | 3 +- .../components/bosch_shc/translations/el.json | 3 +- .../components/bosch_shc/translations/en.json | 3 +- .../bosch_shc/translations/es-419.json | 3 +- .../components/bosch_shc/translations/es.json | 3 +- .../components/bosch_shc/translations/et.json | 3 +- .../components/bosch_shc/translations/fr.json | 3 +- .../components/bosch_shc/translations/hu.json | 3 +- .../components/bosch_shc/translations/id.json | 3 +- .../components/bosch_shc/translations/it.json | 3 +- .../components/bosch_shc/translations/ja.json | 3 +- .../components/bosch_shc/translations/nl.json | 3 +- .../components/bosch_shc/translations/no.json | 3 +- .../components/bosch_shc/translations/pl.json | 3 +- .../bosch_shc/translations/pt-BR.json | 3 +- .../components/bosch_shc/translations/ru.json | 3 +- .../components/bosch_shc/translations/tr.json | 3 +- .../bosch_shc/translations/zh-Hant.json | 3 +- .../components/braviatv/translations/bg.json | 3 +- .../components/braviatv/translations/ca.json | 3 +- .../components/braviatv/translations/cs.json | 3 +- .../components/braviatv/translations/da.json | 3 - .../components/braviatv/translations/de.json | 3 +- .../components/braviatv/translations/el.json | 3 +- .../components/braviatv/translations/en.json | 3 +- .../braviatv/translations/es-419.json | 3 +- .../components/braviatv/translations/es.json | 3 +- .../components/braviatv/translations/et.json | 3 +- .../components/braviatv/translations/fr.json | 3 +- .../components/braviatv/translations/hu.json | 3 +- .../components/braviatv/translations/id.json | 3 +- .../components/braviatv/translations/it.json | 3 +- .../components/braviatv/translations/ja.json | 3 +- .../components/braviatv/translations/ko.json | 3 +- .../components/braviatv/translations/lb.json | 3 +- .../components/braviatv/translations/nl.json | 3 +- .../components/braviatv/translations/no.json | 3 +- .../components/braviatv/translations/pl.json | 3 +- .../braviatv/translations/pt-BR.json | 3 +- .../components/braviatv/translations/pt.json | 3 +- .../components/braviatv/translations/ru.json | 3 +- .../components/braviatv/translations/sl.json | 3 +- .../components/braviatv/translations/sv.json | 3 +- .../components/braviatv/translations/tr.json | 3 +- .../components/braviatv/translations/uk.json | 3 +- .../braviatv/translations/zh-Hans.json | 3 +- .../braviatv/translations/zh-Hant.json | 3 +- .../components/climacell/translations/af.json | 5 +- .../components/climacell/translations/bg.json | 18 ----- .../components/climacell/translations/ca.json | 23 +----- .../components/climacell/translations/cs.json | 21 ----- .../components/climacell/translations/de.json | 23 +----- .../components/climacell/translations/el.json | 23 +----- .../components/climacell/translations/en.json | 23 +----- .../climacell/translations/es-419.json | 15 +--- .../components/climacell/translations/es.json | 23 +----- .../components/climacell/translations/et.json | 25 +----- .../components/climacell/translations/fr.json | 23 +----- .../components/climacell/translations/hu.json | 25 +----- .../components/climacell/translations/id.json | 23 +----- .../components/climacell/translations/it.json | 23 +----- .../components/climacell/translations/ja.json | 25 +----- .../components/climacell/translations/ko.json | 25 +----- .../components/climacell/translations/lb.json | 11 --- .../components/climacell/translations/nl.json | 23 +----- .../components/climacell/translations/no.json | 23 +----- .../components/climacell/translations/pl.json | 23 +----- .../climacell/translations/pt-BR.json | 23 +----- .../components/climacell/translations/pt.json | 19 ----- .../components/climacell/translations/ru.json | 23 +----- .../components/climacell/translations/sk.json | 17 ---- .../components/climacell/translations/sv.json | 14 ---- .../components/climacell/translations/tr.json | 23 +----- .../climacell/translations/zh-Hans.json | 12 --- .../climacell/translations/zh-Hant.json | 23 +----- .../components/coinbase/translations/ar.json | 5 +- .../components/coinbase/translations/ca.json | 6 +- .../components/coinbase/translations/cs.json | 3 +- .../components/coinbase/translations/de.json | 6 +- .../components/coinbase/translations/el.json | 6 +- .../components/coinbase/translations/en.json | 6 +- .../coinbase/translations/es-419.json | 8 +- .../components/coinbase/translations/es.json | 6 +- .../components/coinbase/translations/et.json | 6 +- .../components/coinbase/translations/fr.json | 6 +- .../components/coinbase/translations/hu.json | 6 +- .../components/coinbase/translations/id.json | 6 +- .../components/coinbase/translations/it.json | 6 +- .../components/coinbase/translations/ja.json | 6 +- .../components/coinbase/translations/nl.json | 6 +- .../components/coinbase/translations/no.json | 6 +- .../components/coinbase/translations/pl.json | 6 +- .../coinbase/translations/pt-BR.json | 6 +- .../components/coinbase/translations/ru.json | 6 +- .../components/coinbase/translations/tr.json | 6 +- .../coinbase/translations/zh-Hans.json | 6 +- .../coinbase/translations/zh-Hant.json | 6 +- .../components/cpuspeed/translations/ko.json | 9 +++ .../components/deconz/translations/bg.json | 1 - .../components/deconz/translations/ca.json | 1 - .../components/deconz/translations/cs.json | 1 - .../components/deconz/translations/da.json | 1 - .../components/deconz/translations/de.json | 1 - .../components/deconz/translations/el.json | 1 - .../components/deconz/translations/en.json | 1 - .../deconz/translations/es-419.json | 1 - .../components/deconz/translations/es.json | 1 - .../components/deconz/translations/et.json | 1 - .../components/deconz/translations/fr.json | 1 - .../components/deconz/translations/hu.json | 1 - .../components/deconz/translations/id.json | 1 - .../components/deconz/translations/it.json | 1 - .../components/deconz/translations/ja.json | 1 - .../components/deconz/translations/ko.json | 2 +- .../components/deconz/translations/lb.json | 1 - .../components/deconz/translations/nl.json | 1 - .../components/deconz/translations/no.json | 1 - .../components/deconz/translations/pl.json | 1 - .../components/deconz/translations/pt-BR.json | 1 - .../components/deconz/translations/pt.json | 3 +- .../components/deconz/translations/ru.json | 1 - .../components/deconz/translations/sl.json | 1 - .../components/deconz/translations/sv.json | 1 - .../components/deconz/translations/tr.json | 1 - .../components/deconz/translations/uk.json | 1 - .../deconz/translations/zh-Hant.json | 1 - .../components/denonavr/translations/ca.json | 10 +-- .../components/denonavr/translations/cs.json | 3 +- .../components/denonavr/translations/de.json | 10 +-- .../components/denonavr/translations/el.json | 10 +-- .../components/denonavr/translations/en.json | 10 +-- .../denonavr/translations/es-419.json | 10 +-- .../components/denonavr/translations/es.json | 10 +-- .../components/denonavr/translations/et.json | 10 +-- .../components/denonavr/translations/fr.json | 10 +-- .../components/denonavr/translations/hu.json | 10 +-- .../components/denonavr/translations/id.json | 10 +-- .../components/denonavr/translations/it.json | 10 +-- .../components/denonavr/translations/ja.json | 10 +-- .../components/denonavr/translations/ko.json | 10 +-- .../components/denonavr/translations/lb.json | 10 +-- .../components/denonavr/translations/nl.json | 10 +-- .../components/denonavr/translations/no.json | 10 +-- .../components/denonavr/translations/pl.json | 10 +-- .../denonavr/translations/pt-BR.json | 10 +-- .../components/denonavr/translations/ru.json | 10 +-- .../components/denonavr/translations/tr.json | 10 +-- .../components/denonavr/translations/uk.json | 10 +-- .../denonavr/translations/zh-Hant.json | 10 +-- .../derivative/translations/bg.json | 5 -- .../derivative/translations/ca.json | 16 ---- .../derivative/translations/de.json | 16 ---- .../derivative/translations/el.json | 16 ---- .../derivative/translations/en.json | 16 ---- .../derivative/translations/es.json | 9 --- .../derivative/translations/et.json | 16 ---- .../derivative/translations/fr.json | 16 ---- .../derivative/translations/he.json | 11 --- .../derivative/translations/hu.json | 16 ---- .../derivative/translations/id.json | 16 ---- .../derivative/translations/it.json | 16 ---- .../derivative/translations/ja.json | 16 ---- .../derivative/translations/ko.json | 43 +++++++++++ .../derivative/translations/nl.json | 16 ---- .../derivative/translations/no.json | 16 ---- .../derivative/translations/pl.json | 16 ---- .../derivative/translations/pt-BR.json | 16 ---- .../derivative/translations/ru.json | 16 ---- .../derivative/translations/tr.json | 16 ---- .../derivative/translations/zh-Hans.json | 16 ---- .../derivative/translations/zh-Hant.json | 16 ---- .../devolo_home_network/translations/ko.json | 9 +++ .../components/dlna_dmr/translations/bg.json | 7 +- .../components/dlna_dmr/translations/ca.json | 5 +- .../components/dlna_dmr/translations/cs.json | 5 -- .../components/dlna_dmr/translations/de.json | 5 +- .../components/dlna_dmr/translations/el.json | 5 +- .../components/dlna_dmr/translations/en.json | 5 +- .../dlna_dmr/translations/es-419.json | 2 - .../components/dlna_dmr/translations/es.json | 5 +- .../components/dlna_dmr/translations/et.json | 5 +- .../components/dlna_dmr/translations/fr.json | 5 +- .../components/dlna_dmr/translations/he.json | 3 +- .../components/dlna_dmr/translations/hu.json | 5 +- .../components/dlna_dmr/translations/id.json | 5 +- .../components/dlna_dmr/translations/it.json | 5 +- .../components/dlna_dmr/translations/ja.json | 5 +- .../components/dlna_dmr/translations/ko.json | 24 ++++++ .../components/dlna_dmr/translations/nl.json | 5 +- .../components/dlna_dmr/translations/no.json | 5 +- .../components/dlna_dmr/translations/pl.json | 5 +- .../dlna_dmr/translations/pt-BR.json | 5 +- .../components/dlna_dmr/translations/ru.json | 5 +- .../components/dlna_dmr/translations/tr.json | 5 +- .../dlna_dmr/translations/zh-Hans.json | 5 +- .../dlna_dmr/translations/zh-Hant.json | 5 +- .../components/dlna_dms/translations/ko.json | 9 +++ .../components/doorbird/translations/ca.json | 6 +- .../components/doorbird/translations/cs.json | 6 +- .../components/doorbird/translations/de.json | 6 +- .../components/doorbird/translations/el.json | 6 +- .../components/doorbird/translations/en.json | 6 +- .../doorbird/translations/es-419.json | 6 +- .../components/doorbird/translations/es.json | 6 +- .../components/doorbird/translations/et.json | 6 +- .../components/doorbird/translations/fr.json | 6 +- .../components/doorbird/translations/hu.json | 6 +- .../components/doorbird/translations/id.json | 6 +- .../components/doorbird/translations/it.json | 6 +- .../components/doorbird/translations/ja.json | 6 +- .../components/doorbird/translations/ko.json | 6 +- .../components/doorbird/translations/lb.json | 6 +- .../components/doorbird/translations/nl.json | 6 +- .../components/doorbird/translations/no.json | 6 +- .../components/doorbird/translations/pl.json | 6 +- .../doorbird/translations/pt-BR.json | 6 +- .../components/doorbird/translations/ru.json | 6 +- .../components/doorbird/translations/sl.json | 6 +- .../components/doorbird/translations/sv.json | 3 +- .../components/doorbird/translations/tr.json | 6 +- .../components/doorbird/translations/uk.json | 6 +- .../doorbird/translations/zh-Hant.json | 6 +- .../components/dunehd/translations/ca.json | 3 +- .../components/dunehd/translations/cs.json | 3 +- .../components/dunehd/translations/de.json | 3 +- .../components/dunehd/translations/el.json | 3 +- .../components/dunehd/translations/en.json | 3 +- .../dunehd/translations/es-419.json | 3 +- .../components/dunehd/translations/es.json | 3 +- .../components/dunehd/translations/et.json | 3 +- .../components/dunehd/translations/fr.json | 3 +- .../components/dunehd/translations/hu.json | 3 +- .../components/dunehd/translations/id.json | 3 +- .../components/dunehd/translations/it.json | 3 +- .../components/dunehd/translations/ja.json | 3 +- .../components/dunehd/translations/ko.json | 3 +- .../components/dunehd/translations/lb.json | 3 +- .../components/dunehd/translations/nl.json | 3 +- .../components/dunehd/translations/no.json | 3 +- .../components/dunehd/translations/pl.json | 3 +- .../components/dunehd/translations/pt-BR.json | 3 +- .../components/dunehd/translations/ru.json | 3 +- .../components/dunehd/translations/tr.json | 3 +- .../components/dunehd/translations/uk.json | 3 +- .../dunehd/translations/zh-Hant.json | 3 +- .../components/efergy/translations/ca.json | 3 +- .../components/efergy/translations/cs.json | 3 +- .../components/efergy/translations/de.json | 3 +- .../components/efergy/translations/el.json | 3 +- .../components/efergy/translations/en.json | 3 +- .../components/efergy/translations/es.json | 3 +- .../components/efergy/translations/et.json | 3 +- .../components/efergy/translations/fr.json | 3 +- .../components/efergy/translations/hu.json | 3 +- .../components/efergy/translations/id.json | 3 +- .../components/efergy/translations/it.json | 3 +- .../components/efergy/translations/ja.json | 3 +- .../components/efergy/translations/nl.json | 3 +- .../components/efergy/translations/no.json | 3 +- .../components/efergy/translations/pl.json | 3 +- .../components/efergy/translations/pt-BR.json | 3 +- .../components/efergy/translations/ru.json | 3 +- .../components/efergy/translations/tr.json | 3 +- .../efergy/translations/zh-Hant.json | 3 +- .../components/elmax/translations/bg.json | 4 +- .../components/elmax/translations/ca.json | 13 +--- .../components/elmax/translations/cs.json | 13 +--- .../components/elmax/translations/de.json | 13 +--- .../components/elmax/translations/el.json | 13 +--- .../components/elmax/translations/en.json | 13 +--- .../components/elmax/translations/es.json | 13 +--- .../components/elmax/translations/et.json | 13 +--- .../components/elmax/translations/fr.json | 13 +--- .../components/elmax/translations/hu.json | 13 +--- .../components/elmax/translations/id.json | 13 +--- .../components/elmax/translations/it.json | 13 +--- .../components/elmax/translations/ja.json | 13 +--- .../components/elmax/translations/lt.json | 6 +- .../components/elmax/translations/nl.json | 13 +--- .../components/elmax/translations/no.json | 13 +--- .../components/elmax/translations/pl.json | 13 +--- .../components/elmax/translations/pt-BR.json | 13 +--- .../components/elmax/translations/ru.json | 13 +--- .../components/elmax/translations/sk.json | 3 +- .../components/elmax/translations/tr.json | 13 +--- .../elmax/translations/zh-Hans.json | 3 +- .../elmax/translations/zh-Hant.json | 13 +--- .../components/fan/translations/ca.json | 1 - .../components/fan/translations/cs.json | 2 +- .../components/fan/translations/de.json | 1 - .../components/fan/translations/el.json | 1 - .../components/fan/translations/en.json | 1 - .../components/fan/translations/es.json | 1 - .../components/fan/translations/et.json | 1 - .../components/fan/translations/fr.json | 1 - .../components/fan/translations/he.json | 1 - .../components/fan/translations/hu.json | 1 - .../components/fan/translations/id.json | 1 - .../components/fan/translations/it.json | 1 - .../components/fan/translations/ja.json | 1 - .../components/fan/translations/nl.json | 1 - .../components/fan/translations/no.json | 1 - .../components/fan/translations/pl.json | 1 - .../components/fan/translations/pt-BR.json | 1 - .../components/fan/translations/ru.json | 1 - .../components/fan/translations/sv.json | 1 - .../components/fan/translations/tr.json | 1 - .../components/fan/translations/zh-Hans.json | 1 - .../components/fan/translations/zh-Hant.json | 1 - .../components/fivem/translations/ca.json | 1 - .../components/fivem/translations/de.json | 1 - .../components/fivem/translations/el.json | 1 - .../components/fivem/translations/en.json | 1 - .../components/fivem/translations/es.json | 1 - .../components/fivem/translations/et.json | 1 - .../components/fivem/translations/fr.json | 1 - .../components/fivem/translations/hu.json | 1 - .../components/fivem/translations/id.json | 1 - .../components/fivem/translations/it.json | 1 - .../components/fivem/translations/ja.json | 1 - .../components/fivem/translations/nl.json | 1 - .../components/fivem/translations/no.json | 1 - .../components/fivem/translations/pl.json | 1 - .../components/fivem/translations/pt-BR.json | 1 - .../components/fivem/translations/ru.json | 1 - .../components/fivem/translations/tr.json | 1 - .../fivem/translations/zh-Hant.json | 1 - .../components/freebox/translations/ca.json | 3 +- .../components/freebox/translations/cs.json | 3 +- .../components/freebox/translations/de.json | 3 +- .../components/freebox/translations/el.json | 3 +- .../components/freebox/translations/en.json | 3 +- .../freebox/translations/es-419.json | 3 +- .../components/freebox/translations/es.json | 3 +- .../components/freebox/translations/et.json | 3 +- .../components/freebox/translations/fr.json | 3 +- .../components/freebox/translations/hu.json | 3 +- .../components/freebox/translations/id.json | 3 +- .../components/freebox/translations/it.json | 3 +- .../components/freebox/translations/ja.json | 3 +- .../components/freebox/translations/ko.json | 3 +- .../components/freebox/translations/lb.json | 3 +- .../components/freebox/translations/nl.json | 3 +- .../components/freebox/translations/no.json | 3 +- .../components/freebox/translations/pl.json | 3 +- .../freebox/translations/pt-BR.json | 3 +- .../components/freebox/translations/ru.json | 3 +- .../components/freebox/translations/sl.json | 3 +- .../components/freebox/translations/tr.json | 3 +- .../components/freebox/translations/uk.json | 3 +- .../freebox/translations/zh-Hant.json | 3 +- .../components/fritz/translations/bg.json | 5 -- .../components/fritz/translations/ca.json | 11 --- .../components/fritz/translations/cs.json | 9 --- .../components/fritz/translations/de.json | 11 --- .../components/fritz/translations/el.json | 11 --- .../components/fritz/translations/en.json | 11 --- .../components/fritz/translations/es.json | 11 --- .../components/fritz/translations/et.json | 11 --- .../components/fritz/translations/fr.json | 11 --- .../components/fritz/translations/he.json | 9 --- .../components/fritz/translations/hu.json | 11 --- .../components/fritz/translations/id.json | 11 --- .../components/fritz/translations/it.json | 11 --- .../components/fritz/translations/ja.json | 11 --- .../components/fritz/translations/ko.json | 10 +-- .../components/fritz/translations/nb.json | 5 -- .../components/fritz/translations/nl.json | 11 --- .../components/fritz/translations/no.json | 11 --- .../components/fritz/translations/pl.json | 11 --- .../components/fritz/translations/pt-BR.json | 11 --- .../components/fritz/translations/ru.json | 11 --- .../components/fritz/translations/sk.json | 5 -- .../components/fritz/translations/tr.json | 11 --- .../fritz/translations/zh-Hans.json | 5 -- .../fritz/translations/zh-Hant.json | 11 --- .../components/generic/translations/bg.json | 2 - .../components/generic/translations/ca.json | 2 - .../components/generic/translations/de.json | 2 - .../components/generic/translations/el.json | 2 - .../components/generic/translations/en.json | 2 - .../components/generic/translations/es.json | 2 - .../components/generic/translations/et.json | 2 - .../components/generic/translations/fr.json | 2 - .../components/generic/translations/he.json | 2 - .../components/generic/translations/hu.json | 2 - .../components/generic/translations/id.json | 2 - .../components/generic/translations/it.json | 2 - .../components/generic/translations/ja.json | 2 - .../components/generic/translations/ko.json | 9 +++ .../components/generic/translations/nl.json | 2 - .../components/generic/translations/no.json | 2 - .../components/generic/translations/pl.json | 2 - .../generic/translations/pt-BR.json | 2 - .../components/generic/translations/ru.json | 2 - .../components/generic/translations/tr.json | 2 - .../generic/translations/zh-Hant.json | 2 - .../geocaching/translations/he.json | 24 ++++++ .../components/gios/translations/ca.json | 1 - .../components/gios/translations/cs.json | 1 - .../components/gios/translations/da.json | 1 - .../components/gios/translations/de.json | 1 - .../components/gios/translations/el.json | 1 - .../components/gios/translations/en.json | 1 - .../components/gios/translations/es-419.json | 1 - .../components/gios/translations/es.json | 1 - .../components/gios/translations/et.json | 1 - .../components/gios/translations/fr.json | 1 - .../components/gios/translations/hu.json | 1 - .../components/gios/translations/id.json | 1 - .../components/gios/translations/it.json | 1 - .../components/gios/translations/ja.json | 1 - .../components/gios/translations/ko.json | 1 - .../components/gios/translations/lb.json | 1 - .../components/gios/translations/nl.json | 1 - .../components/gios/translations/no.json | 1 - .../components/gios/translations/pl.json | 1 - .../components/gios/translations/pt-BR.json | 1 - .../components/gios/translations/ru.json | 1 - .../components/gios/translations/sl.json | 1 - .../components/gios/translations/sv.json | 1 - .../components/gios/translations/tr.json | 1 - .../components/gios/translations/uk.json | 1 - .../components/gios/translations/zh-Hant.json | 1 - .../components/goalzero/translations/ca.json | 6 +- .../components/goalzero/translations/cs.json | 3 +- .../components/goalzero/translations/de.json | 6 +- .../components/goalzero/translations/el.json | 6 +- .../components/goalzero/translations/en.json | 6 +- .../components/goalzero/translations/es.json | 6 +- .../components/goalzero/translations/et.json | 6 +- .../components/goalzero/translations/fr.json | 6 +- .../components/goalzero/translations/hu.json | 6 +- .../components/goalzero/translations/id.json | 6 +- .../components/goalzero/translations/it.json | 6 +- .../components/goalzero/translations/ja.json | 6 +- .../components/goalzero/translations/ko.json | 3 +- .../components/goalzero/translations/lb.json | 3 +- .../components/goalzero/translations/nl.json | 6 +- .../components/goalzero/translations/no.json | 6 +- .../components/goalzero/translations/pl.json | 6 +- .../goalzero/translations/pt-BR.json | 6 +- .../components/goalzero/translations/ru.json | 6 +- .../components/goalzero/translations/tr.json | 6 +- .../components/goalzero/translations/uk.json | 3 +- .../goalzero/translations/zh-Hant.json | 6 +- .../components/google/translations/ko.json | 7 ++ .../components/group/translations/bg.json | 45 +---------- .../components/group/translations/ca.json | 77 +------------------ .../components/group/translations/cs.json | 77 +------------------ .../components/group/translations/de.json | 77 +------------------ .../components/group/translations/el.json | 77 +------------------ .../components/group/translations/en.json | 77 +------------------ .../components/group/translations/es.json | 63 +-------------- .../components/group/translations/et.json | 77 +------------------ .../components/group/translations/fr.json | 77 +------------------ .../components/group/translations/he.json | 77 +------------------ .../components/group/translations/hu.json | 77 +------------------ .../components/group/translations/id.json | 77 +------------------ .../components/group/translations/it.json | 77 +------------------ .../components/group/translations/ja.json | 77 +------------------ .../components/group/translations/ko.json | 7 ++ .../components/group/translations/nl.json | 77 +------------------ .../components/group/translations/no.json | 77 +------------------ .../components/group/translations/pl.json | 77 +------------------ .../components/group/translations/pt-BR.json | 77 +------------------ .../components/group/translations/pt.json | 19 ----- .../components/group/translations/ru.json | 77 +------------------ .../components/group/translations/sv.json | 53 ------------- .../components/group/translations/tr.json | 77 +------------------ .../group/translations/zh-Hant.json | 77 +------------------ .../components/guardian/translations/ca.json | 3 - .../components/guardian/translations/cs.json | 3 - .../components/guardian/translations/de.json | 3 - .../components/guardian/translations/el.json | 3 - .../components/guardian/translations/en.json | 3 - .../components/guardian/translations/es.json | 3 - .../components/guardian/translations/et.json | 3 - .../components/guardian/translations/fr.json | 3 - .../components/guardian/translations/hu.json | 3 - .../components/guardian/translations/id.json | 3 - .../components/guardian/translations/it.json | 3 - .../components/guardian/translations/ja.json | 3 - .../components/guardian/translations/ko.json | 3 - .../components/guardian/translations/lb.json | 3 - .../components/guardian/translations/nl.json | 3 - .../components/guardian/translations/no.json | 3 - .../components/guardian/translations/pl.json | 3 - .../guardian/translations/pt-BR.json | 3 - .../components/guardian/translations/ru.json | 3 - .../components/guardian/translations/tr.json | 3 - .../components/guardian/translations/uk.json | 3 - .../guardian/translations/zh-Hant.json | 3 - .../components/habitica/translations/ca.json | 3 +- .../components/habitica/translations/de.json | 3 +- .../components/habitica/translations/el.json | 3 +- .../components/habitica/translations/en.json | 3 +- .../components/habitica/translations/es.json | 3 +- .../components/habitica/translations/et.json | 3 +- .../components/habitica/translations/fr.json | 3 +- .../components/habitica/translations/hu.json | 3 +- .../components/habitica/translations/id.json | 3 +- .../components/habitica/translations/it.json | 3 +- .../components/habitica/translations/ja.json | 3 +- .../components/habitica/translations/ko.json | 3 +- .../components/habitica/translations/nl.json | 3 +- .../components/habitica/translations/no.json | 3 +- .../components/habitica/translations/pl.json | 3 +- .../habitica/translations/pt-BR.json | 3 +- .../components/habitica/translations/ru.json | 3 +- .../components/habitica/translations/tr.json | 3 +- .../habitica/translations/zh-Hant.json | 3 +- .../home_plus_control/translations/ca.json | 3 +- .../home_plus_control/translations/de.json | 3 +- .../home_plus_control/translations/el.json | 3 +- .../home_plus_control/translations/en.json | 3 +- .../translations/es-419.json | 3 +- .../home_plus_control/translations/es.json | 3 +- .../home_plus_control/translations/et.json | 3 +- .../home_plus_control/translations/fr.json | 3 +- .../home_plus_control/translations/hu.json | 3 +- .../home_plus_control/translations/id.json | 3 +- .../home_plus_control/translations/it.json | 3 +- .../home_plus_control/translations/ja.json | 3 +- .../home_plus_control/translations/ko.json | 3 +- .../home_plus_control/translations/nl.json | 3 +- .../home_plus_control/translations/no.json | 3 +- .../home_plus_control/translations/pl.json | 3 +- .../home_plus_control/translations/pt-BR.json | 3 +- .../home_plus_control/translations/ru.json | 3 +- .../home_plus_control/translations/tr.json | 3 +- .../translations/zh-Hant.json | 3 +- .../components/homekit/translations/ca.json | 1 - .../components/homekit/translations/de.json | 1 - .../components/homekit/translations/el.json | 1 - .../components/homekit/translations/en.json | 1 - .../homekit/translations/es-419.json | 3 - .../components/homekit/translations/es.json | 1 - .../components/homekit/translations/et.json | 1 - .../components/homekit/translations/fr.json | 1 - .../components/homekit/translations/hu.json | 1 - .../components/homekit/translations/id.json | 1 - .../components/homekit/translations/it.json | 1 - .../components/homekit/translations/ja.json | 1 - .../components/homekit/translations/ko.json | 3 - .../components/homekit/translations/lb.json | 3 - .../components/homekit/translations/nl.json | 1 - .../components/homekit/translations/no.json | 1 - .../components/homekit/translations/pl.json | 1 - .../homekit/translations/pt-BR.json | 1 - .../components/homekit/translations/pt.json | 3 - .../components/homekit/translations/ru.json | 1 - .../components/homekit/translations/sl.json | 3 - .../components/homekit/translations/tr.json | 1 - .../components/homekit/translations/uk.json | 3 - .../homekit/translations/zh-Hans.json | 1 - .../homekit/translations/zh-Hant.json | 1 - .../huawei_lte/translations/bg.json | 5 +- .../huawei_lte/translations/ca.json | 3 - .../huawei_lte/translations/cs.json | 5 +- .../huawei_lte/translations/da.json | 5 +- .../huawei_lte/translations/de.json | 3 - .../huawei_lte/translations/el.json | 3 - .../huawei_lte/translations/en.json | 3 - .../huawei_lte/translations/es-419.json | 5 +- .../huawei_lte/translations/es.json | 3 - .../huawei_lte/translations/et.json | 3 - .../huawei_lte/translations/fr.json | 3 - .../huawei_lte/translations/he.json | 4 - .../huawei_lte/translations/hu.json | 3 - .../huawei_lte/translations/id.json | 3 - .../huawei_lte/translations/it.json | 3 - .../huawei_lte/translations/ja.json | 3 - .../huawei_lte/translations/ko.json | 5 +- .../huawei_lte/translations/lb.json | 5 +- .../huawei_lte/translations/nl.json | 3 - .../huawei_lte/translations/no.json | 3 - .../huawei_lte/translations/pl.json | 3 - .../huawei_lte/translations/pt-BR.json | 3 - .../huawei_lte/translations/pt.json | 7 +- .../huawei_lte/translations/ru.json | 3 - .../huawei_lte/translations/sk.json | 3 - .../huawei_lte/translations/sl.json | 5 +- .../huawei_lte/translations/sv.json | 5 +- .../huawei_lte/translations/tr.json | 3 - .../huawei_lte/translations/uk.json | 5 +- .../huawei_lte/translations/zh-Hans.json | 3 - .../huawei_lte/translations/zh-Hant.json | 3 - .../humidifier/translations/ca.json | 1 - .../humidifier/translations/cs.json | 1 - .../humidifier/translations/de.json | 1 - .../humidifier/translations/el.json | 1 - .../humidifier/translations/en.json | 1 - .../humidifier/translations/es.json | 1 - .../humidifier/translations/et.json | 1 - .../humidifier/translations/fr.json | 1 - .../humidifier/translations/he.json | 1 - .../humidifier/translations/hu.json | 1 - .../humidifier/translations/id.json | 1 - .../humidifier/translations/it.json | 1 - .../humidifier/translations/ja.json | 1 - .../humidifier/translations/nl.json | 1 - .../humidifier/translations/no.json | 1 - .../humidifier/translations/pl.json | 1 - .../humidifier/translations/pt-BR.json | 1 - .../humidifier/translations/ru.json | 1 - .../humidifier/translations/sv.json | 1 - .../humidifier/translations/tr.json | 1 - .../humidifier/translations/zh-Hans.json | 1 - .../humidifier/translations/zh-Hant.json | 1 - .../components/insteon/translations/ca.json | 22 ++---- .../components/insteon/translations/cs.json | 22 ++---- .../components/insteon/translations/de.json | 22 ++---- .../components/insteon/translations/el.json | 22 ++---- .../components/insteon/translations/en.json | 22 ++---- .../components/insteon/translations/es.json | 22 ++---- .../components/insteon/translations/et.json | 22 ++---- .../components/insteon/translations/fr.json | 22 ++---- .../components/insteon/translations/hu.json | 22 ++---- .../components/insteon/translations/id.json | 22 ++---- .../components/insteon/translations/it.json | 22 ++---- .../components/insteon/translations/ja.json | 22 ++---- .../components/insteon/translations/ko.json | 26 +++---- .../components/insteon/translations/lb.json | 22 ++---- .../components/insteon/translations/nl.json | 22 ++---- .../components/insteon/translations/no.json | 22 ++---- .../components/insteon/translations/pl.json | 22 ++---- .../insteon/translations/pt-BR.json | 22 ++---- .../components/insteon/translations/pt.json | 6 +- .../components/insteon/translations/ru.json | 22 ++---- .../components/insteon/translations/tr.json | 22 ++---- .../components/insteon/translations/uk.json | 22 ++---- .../insteon/translations/zh-Hant.json | 22 ++---- .../integration/translations/ca.json | 6 -- .../integration/translations/cs.json | 6 -- .../integration/translations/de.json | 6 -- .../integration/translations/el.json | 6 -- .../integration/translations/en.json | 6 -- .../integration/translations/es.json | 3 - .../integration/translations/et.json | 6 -- .../integration/translations/fr.json | 6 -- .../integration/translations/he.json | 6 -- .../integration/translations/hu.json | 6 -- .../integration/translations/id.json | 6 -- .../integration/translations/it.json | 6 -- .../integration/translations/ja.json | 6 -- .../integration/translations/ko.json | 36 +++++++++ .../integration/translations/nl.json | 6 -- .../integration/translations/no.json | 6 -- .../integration/translations/pl.json | 6 -- .../integration/translations/pt-BR.json | 6 -- .../integration/translations/ru.json | 6 -- .../integration/translations/sk.json | 9 --- .../integration/translations/tr.json | 6 -- .../integration/translations/zh-Hans.json | 6 -- .../integration/translations/zh-Hant.json | 6 -- .../intellifire/translations/bg.json | 8 +- .../intellifire/translations/ca.json | 8 +- .../intellifire/translations/cs.json | 8 +- .../intellifire/translations/de.json | 8 +- .../intellifire/translations/el.json | 8 +- .../intellifire/translations/en.json | 8 +- .../intellifire/translations/es.json | 5 -- .../intellifire/translations/et.json | 8 +- .../intellifire/translations/fr.json | 8 +- .../intellifire/translations/he.json | 8 +- .../intellifire/translations/hu.json | 8 +- .../intellifire/translations/id.json | 8 +- .../intellifire/translations/it.json | 8 +- .../intellifire/translations/ja.json | 8 +- .../intellifire/translations/ko.json | 23 ++++++ .../intellifire/translations/nl.json | 8 +- .../intellifire/translations/no.json | 8 +- .../intellifire/translations/pl.json | 8 +- .../intellifire/translations/pt-BR.json | 8 +- .../intellifire/translations/ru.json | 8 +- .../intellifire/translations/sv.json | 10 +-- .../intellifire/translations/tr.json | 8 +- .../intellifire/translations/zh-Hant.json | 8 +- .../components/iqvia/translations/bg.json | 3 +- .../components/iqvia/translations/ca.json | 3 +- .../components/iqvia/translations/cs.json | 3 +- .../components/iqvia/translations/da.json | 3 +- .../components/iqvia/translations/de.json | 3 +- .../components/iqvia/translations/el.json | 3 +- .../components/iqvia/translations/en.json | 3 +- .../components/iqvia/translations/es-419.json | 3 +- .../components/iqvia/translations/es.json | 3 +- .../components/iqvia/translations/et.json | 3 +- .../components/iqvia/translations/fi.json | 3 +- .../components/iqvia/translations/fr.json | 3 +- .../components/iqvia/translations/hu.json | 3 +- .../components/iqvia/translations/id.json | 3 +- .../components/iqvia/translations/it.json | 3 +- .../components/iqvia/translations/ja.json | 3 +- .../components/iqvia/translations/ko.json | 3 +- .../components/iqvia/translations/lb.json | 3 +- .../components/iqvia/translations/nl.json | 3 +- .../components/iqvia/translations/nn.json | 9 --- .../components/iqvia/translations/no.json | 3 +- .../components/iqvia/translations/pl.json | 3 +- .../components/iqvia/translations/pt-BR.json | 3 +- .../components/iqvia/translations/ru.json | 3 +- .../components/iqvia/translations/sl.json | 3 +- .../components/iqvia/translations/sv.json | 3 +- .../components/iqvia/translations/tr.json | 3 +- .../components/iqvia/translations/uk.json | 3 +- .../iqvia/translations/zh-Hans.json | 3 +- .../iqvia/translations/zh-Hant.json | 3 +- .../components/iss/translations/bg.json | 3 - .../components/iss/translations/ca.json | 3 - .../components/iss/translations/de.json | 3 - .../components/iss/translations/el.json | 3 - .../components/iss/translations/en.json | 3 - .../components/iss/translations/es.json | 3 - .../components/iss/translations/et.json | 3 - .../components/iss/translations/fr.json | 3 - .../components/iss/translations/hu.json | 3 - .../components/iss/translations/id.json | 3 - .../components/iss/translations/it.json | 3 - .../components/iss/translations/ja.json | 3 - .../components/iss/translations/nl.json | 3 - .../components/iss/translations/no.json | 3 - .../components/iss/translations/pl.json | 3 - .../components/iss/translations/pt-BR.json | 5 +- .../components/iss/translations/ru.json | 3 - .../components/iss/translations/tr.json | 3 - .../components/iss/translations/uk.json | 3 - .../components/iss/translations/zh-Hans.json | 11 --- .../components/iss/translations/zh-Hant.json | 3 - .../components/isy994/translations/ko.json | 9 +++ .../kaleidescape/translations/bg.json | 6 +- .../kaleidescape/translations/ca.json | 6 +- .../kaleidescape/translations/de.json | 6 +- .../kaleidescape/translations/el.json | 6 +- .../kaleidescape/translations/en.json | 6 +- .../kaleidescape/translations/et.json | 6 +- .../kaleidescape/translations/fr.json | 6 +- .../kaleidescape/translations/hu.json | 6 +- .../kaleidescape/translations/id.json | 6 +- .../kaleidescape/translations/it.json | 6 +- .../kaleidescape/translations/ja.json | 6 +- .../kaleidescape/translations/nl.json | 6 +- .../kaleidescape/translations/no.json | 6 +- .../kaleidescape/translations/pl.json | 6 +- .../kaleidescape/translations/pt-BR.json | 6 +- .../kaleidescape/translations/ru.json | 6 +- .../kaleidescape/translations/tr.json | 6 +- .../kaleidescape/translations/zh-Hant.json | 6 +- .../components/knx/translations/bg.json | 1 - .../components/knx/translations/ca.json | 6 +- .../components/knx/translations/de.json | 6 +- .../components/knx/translations/el.json | 4 - .../components/knx/translations/en.json | 4 - .../components/knx/translations/es.json | 8 +- .../components/knx/translations/et.json | 4 - .../components/knx/translations/fr.json | 4 - .../components/knx/translations/hu.json | 4 - .../components/knx/translations/id.json | 4 - .../components/knx/translations/it.json | 4 - .../components/knx/translations/ja.json | 4 - .../components/knx/translations/ko.json | 6 +- .../components/knx/translations/nl.json | 4 - .../components/knx/translations/no.json | 6 +- .../components/knx/translations/pl.json | 4 - .../components/knx/translations/pt-BR.json | 6 +- .../components/knx/translations/ru.json | 4 - .../components/knx/translations/sl.json | 1 - .../components/knx/translations/tr.json | 4 - .../components/knx/translations/zh-Hant.json | 4 - .../components/kraken/translations/ko.json | 9 +++ .../components/light/translations/ca.json | 1 - .../components/light/translations/cs.json | 1 - .../components/light/translations/de.json | 1 - .../components/light/translations/el.json | 1 - .../components/light/translations/en.json | 1 - .../components/light/translations/es.json | 1 - .../components/light/translations/et.json | 1 - .../components/light/translations/fr.json | 1 - .../components/light/translations/he.json | 1 - .../components/light/translations/hu.json | 1 - .../components/light/translations/id.json | 1 - .../components/light/translations/it.json | 1 - .../components/light/translations/ja.json | 1 - .../components/light/translations/nl.json | 1 - .../components/light/translations/no.json | 1 - .../components/light/translations/pl.json | 1 - .../components/light/translations/pt-BR.json | 1 - .../components/light/translations/ru.json | 1 - .../components/light/translations/sv.json | 1 - .../components/light/translations/tr.json | 1 - .../light/translations/zh-Hans.json | 1 - .../light/translations/zh-Hant.json | 1 - .../litterrobot/translations/sensor.cs.json | 9 +++ .../litterrobot/translations/sensor.he.json | 8 ++ .../litterrobot/translations/sensor.ko.json | 3 + .../components/luftdaten/translations/bg.json | 3 +- .../components/luftdaten/translations/ca.json | 3 +- .../components/luftdaten/translations/cs.json | 3 +- .../components/luftdaten/translations/da.json | 3 +- .../components/luftdaten/translations/de.json | 3 +- .../components/luftdaten/translations/el.json | 3 +- .../components/luftdaten/translations/en.json | 3 +- .../luftdaten/translations/es-419.json | 3 +- .../components/luftdaten/translations/es.json | 3 +- .../components/luftdaten/translations/et.json | 3 +- .../components/luftdaten/translations/fi.json | 3 +- .../components/luftdaten/translations/fr.json | 3 +- .../components/luftdaten/translations/hu.json | 3 +- .../components/luftdaten/translations/id.json | 3 +- .../components/luftdaten/translations/it.json | 3 +- .../components/luftdaten/translations/ja.json | 3 +- .../components/luftdaten/translations/ko.json | 3 +- .../components/luftdaten/translations/lb.json | 3 +- .../components/luftdaten/translations/nl.json | 3 +- .../components/luftdaten/translations/no.json | 3 +- .../components/luftdaten/translations/pl.json | 3 +- .../luftdaten/translations/pt-BR.json | 5 +- .../components/luftdaten/translations/pt.json | 3 +- .../components/luftdaten/translations/ru.json | 3 +- .../components/luftdaten/translations/sl.json | 3 +- .../components/luftdaten/translations/sv.json | 3 +- .../components/luftdaten/translations/tr.json | 3 +- .../components/luftdaten/translations/uk.json | 3 +- .../luftdaten/translations/zh-Hans.json | 3 +- .../luftdaten/translations/zh-Hant.json | 3 +- .../components/meater/translations/ko.json | 18 ++++- .../components/mill/translations/bg.json | 6 -- .../components/mill/translations/ca.json | 4 +- .../components/mill/translations/cs.json | 8 -- .../components/mill/translations/de.json | 4 +- .../components/mill/translations/el.json | 4 +- .../components/mill/translations/en.json | 4 +- .../components/mill/translations/es.json | 4 +- .../components/mill/translations/et.json | 4 +- .../components/mill/translations/fi.json | 12 --- .../components/mill/translations/fr.json | 4 +- .../components/mill/translations/he.json | 6 -- .../components/mill/translations/hu.json | 4 +- .../components/mill/translations/id.json | 4 +- .../components/mill/translations/it.json | 4 +- .../components/mill/translations/ja.json | 4 +- .../components/mill/translations/ko.json | 6 -- .../components/mill/translations/lb.json | 8 -- .../components/mill/translations/nl.json | 4 +- .../components/mill/translations/no.json | 4 +- .../components/mill/translations/pl.json | 4 +- .../components/mill/translations/pt-BR.json | 4 +- .../components/mill/translations/pt.json | 8 -- .../components/mill/translations/ru.json | 4 +- .../components/mill/translations/sv.json | 11 --- .../components/mill/translations/tr.json | 4 +- .../components/mill/translations/uk.json | 8 -- .../components/mill/translations/zh-Hans.json | 7 -- .../components/mill/translations/zh-Hant.json | 4 +- .../components/min_max/translations/ca.json | 8 -- .../components/min_max/translations/cs.json | 8 -- .../components/min_max/translations/de.json | 8 -- .../components/min_max/translations/el.json | 8 -- .../components/min_max/translations/en.json | 8 -- .../components/min_max/translations/es.json | 7 -- .../components/min_max/translations/et.json | 8 -- .../components/min_max/translations/fr.json | 8 -- .../components/min_max/translations/he.json | 7 -- .../components/min_max/translations/hu.json | 8 -- .../components/min_max/translations/id.json | 8 -- .../components/min_max/translations/it.json | 8 -- .../components/min_max/translations/ja.json | 8 -- .../components/min_max/translations/ko.json | 34 ++++++++ .../components/min_max/translations/nl.json | 8 -- .../components/min_max/translations/no.json | 8 -- .../components/min_max/translations/pl.json | 8 -- .../min_max/translations/pt-BR.json | 8 -- .../components/min_max/translations/ru.json | 8 -- .../components/min_max/translations/tr.json | 8 -- .../min_max/translations/zh-Hans.json | 8 -- .../min_max/translations/zh-Hant.json | 8 -- .../modem_callerid/translations/bg.json | 6 +- .../modem_callerid/translations/ca.json | 6 +- .../modem_callerid/translations/de.json | 6 +- .../modem_callerid/translations/el.json | 6 +- .../modem_callerid/translations/en.json | 6 +- .../modem_callerid/translations/es.json | 6 +- .../modem_callerid/translations/et.json | 6 +- .../modem_callerid/translations/fr.json | 6 +- .../modem_callerid/translations/hu.json | 6 +- .../modem_callerid/translations/id.json | 6 +- .../modem_callerid/translations/it.json | 6 +- .../modem_callerid/translations/ja.json | 6 +- .../modem_callerid/translations/nl.json | 6 +- .../modem_callerid/translations/no.json | 6 +- .../modem_callerid/translations/pl.json | 6 +- .../modem_callerid/translations/pt-BR.json | 6 +- .../modem_callerid/translations/ru.json | 6 +- .../modem_callerid/translations/tr.json | 6 +- .../modem_callerid/translations/zh-Hant.json | 6 +- .../modern_forms/translations/bg.json | 6 +- .../modern_forms/translations/ca.json | 6 +- .../modern_forms/translations/de.json | 6 +- .../modern_forms/translations/el.json | 6 +- .../modern_forms/translations/en.json | 6 +- .../modern_forms/translations/es.json | 6 +- .../modern_forms/translations/et.json | 6 +- .../modern_forms/translations/fr.json | 6 +- .../modern_forms/translations/he.json | 3 - .../modern_forms/translations/hu.json | 6 +- .../modern_forms/translations/id.json | 6 +- .../modern_forms/translations/it.json | 6 +- .../modern_forms/translations/ja.json | 6 +- .../modern_forms/translations/nl.json | 6 +- .../modern_forms/translations/no.json | 6 +- .../modern_forms/translations/pl.json | 6 +- .../modern_forms/translations/pt-BR.json | 6 +- .../modern_forms/translations/ru.json | 6 +- .../modern_forms/translations/tr.json | 6 +- .../modern_forms/translations/zh-Hant.json | 6 +- .../components/moon/translations/ko.json | 9 +++ .../motion_blinds/translations/bg.json | 4 - .../motion_blinds/translations/ca.json | 17 ++-- .../motion_blinds/translations/cs.json | 4 +- .../motion_blinds/translations/de.json | 17 ++-- .../motion_blinds/translations/el.json | 17 ++-- .../motion_blinds/translations/en.json | 17 ++-- .../motion_blinds/translations/es.json | 17 ++-- .../motion_blinds/translations/et.json | 17 ++-- .../motion_blinds/translations/fr.json | 17 ++-- .../motion_blinds/translations/he.json | 1 - .../motion_blinds/translations/hu.json | 17 ++-- .../motion_blinds/translations/id.json | 17 ++-- .../motion_blinds/translations/it.json | 17 ++-- .../motion_blinds/translations/ja.json | 17 ++-- .../motion_blinds/translations/ka.json | 4 +- .../motion_blinds/translations/ko.json | 7 +- .../motion_blinds/translations/lb.json | 4 +- .../motion_blinds/translations/nl.json | 17 ++-- .../motion_blinds/translations/no.json | 17 ++-- .../motion_blinds/translations/pl.json | 17 ++-- .../motion_blinds/translations/pt-BR.json | 17 ++-- .../motion_blinds/translations/pt.json | 7 +- .../motion_blinds/translations/ru.json | 17 ++-- .../motion_blinds/translations/sk.json | 5 -- .../motion_blinds/translations/sl.json | 17 ---- .../motion_blinds/translations/tr.json | 17 ++-- .../motion_blinds/translations/uk.json | 7 +- .../motion_blinds/translations/zh-Hans.json | 1 - .../motion_blinds/translations/zh-Hant.json | 17 ++-- .../components/mysensors/translations/ca.json | 3 +- .../components/mysensors/translations/cs.json | 3 +- .../components/mysensors/translations/de.json | 3 +- .../components/mysensors/translations/el.json | 3 +- .../components/mysensors/translations/en.json | 3 +- .../components/mysensors/translations/es.json | 3 +- .../components/mysensors/translations/et.json | 3 +- .../components/mysensors/translations/fr.json | 3 +- .../components/mysensors/translations/hu.json | 3 +- .../components/mysensors/translations/id.json | 3 +- .../components/mysensors/translations/it.json | 3 +- .../components/mysensors/translations/ja.json | 3 +- .../components/mysensors/translations/ko.json | 3 +- .../components/mysensors/translations/nl.json | 3 +- .../components/mysensors/translations/no.json | 3 +- .../components/mysensors/translations/pl.json | 3 +- .../mysensors/translations/pt-BR.json | 3 +- .../components/mysensors/translations/ru.json | 3 +- .../components/mysensors/translations/tr.json | 3 +- .../mysensors/translations/zh-Hant.json | 3 +- .../components/neato/translations/ca.json | 3 +- .../components/neato/translations/cs.json | 3 +- .../components/neato/translations/de.json | 3 +- .../components/neato/translations/el.json | 3 +- .../components/neato/translations/en.json | 3 +- .../components/neato/translations/es.json | 3 +- .../components/neato/translations/et.json | 3 +- .../components/neato/translations/fr.json | 3 +- .../components/neato/translations/hu.json | 3 +- .../components/neato/translations/id.json | 3 +- .../components/neato/translations/it.json | 3 +- .../components/neato/translations/ja.json | 3 +- .../components/neato/translations/ko.json | 3 +- .../components/neato/translations/lb.json | 3 +- .../components/neato/translations/nl.json | 3 +- .../components/neato/translations/no.json | 3 +- .../components/neato/translations/pl.json | 3 +- .../components/neato/translations/pt-BR.json | 3 +- .../components/neato/translations/ru.json | 3 +- .../components/neato/translations/sl.json | 3 +- .../components/neato/translations/tr.json | 3 +- .../components/neato/translations/uk.json | 3 +- .../neato/translations/zh-Hant.json | 3 +- .../components/netatmo/translations/nl.json | 2 +- .../netatmo/translations/pt-BR.json | 2 +- .../components/netgear/translations/bg.json | 11 +-- .../components/netgear/translations/ca.json | 8 +- .../components/netgear/translations/cs.json | 2 - .../components/netgear/translations/de.json | 8 +- .../components/netgear/translations/el.json | 8 +- .../components/netgear/translations/en.json | 8 +- .../components/netgear/translations/es.json | 8 +- .../components/netgear/translations/et.json | 8 +- .../components/netgear/translations/fr.json | 8 +- .../components/netgear/translations/he.json | 12 +-- .../components/netgear/translations/hu.json | 8 +- .../components/netgear/translations/id.json | 8 +- .../components/netgear/translations/it.json | 8 +- .../components/netgear/translations/ja.json | 8 +- .../components/netgear/translations/nl.json | 8 +- .../components/netgear/translations/no.json | 8 +- .../components/netgear/translations/pl.json | 8 +- .../netgear/translations/pt-BR.json | 8 +- .../components/netgear/translations/ru.json | 8 +- .../components/netgear/translations/sk.json | 11 --- .../components/netgear/translations/tr.json | 8 +- .../netgear/translations/zh-Hans.json | 8 +- .../netgear/translations/zh-Hant.json | 8 +- .../components/nexia/translations/bg.json | 3 +- .../components/nexia/translations/ca.json | 3 +- .../components/nexia/translations/cs.json | 3 +- .../components/nexia/translations/de.json | 3 +- .../components/nexia/translations/el.json | 3 +- .../components/nexia/translations/en.json | 3 +- .../components/nexia/translations/es-419.json | 3 +- .../components/nexia/translations/es.json | 3 +- .../components/nexia/translations/et.json | 3 +- .../components/nexia/translations/fr.json | 3 +- .../components/nexia/translations/he.json | 3 +- .../components/nexia/translations/hu.json | 3 +- .../components/nexia/translations/id.json | 3 +- .../components/nexia/translations/it.json | 3 +- .../components/nexia/translations/ja.json | 3 +- .../components/nexia/translations/ko.json | 3 +- .../components/nexia/translations/lb.json | 3 +- .../components/nexia/translations/nl.json | 3 +- .../components/nexia/translations/no.json | 3 +- .../components/nexia/translations/pl.json | 3 +- .../components/nexia/translations/pt-BR.json | 3 +- .../components/nexia/translations/ru.json | 3 +- .../components/nexia/translations/sl.json | 3 +- .../components/nexia/translations/sv.json | 3 +- .../components/nexia/translations/tr.json | 3 +- .../components/nexia/translations/uk.json | 3 +- .../nexia/translations/zh-Hant.json | 3 +- .../nfandroidtv/translations/bg.json | 3 +- .../nfandroidtv/translations/ca.json | 3 +- .../nfandroidtv/translations/de.json | 3 +- .../nfandroidtv/translations/el.json | 3 +- .../nfandroidtv/translations/en.json | 3 +- .../nfandroidtv/translations/es.json | 3 +- .../nfandroidtv/translations/et.json | 3 +- .../nfandroidtv/translations/fr.json | 3 +- .../nfandroidtv/translations/he.json | 3 +- .../nfandroidtv/translations/hu.json | 3 +- .../nfandroidtv/translations/id.json | 3 +- .../nfandroidtv/translations/it.json | 3 +- .../nfandroidtv/translations/ja.json | 3 +- .../nfandroidtv/translations/nl.json | 3 +- .../nfandroidtv/translations/no.json | 3 +- .../nfandroidtv/translations/pl.json | 3 +- .../nfandroidtv/translations/pt-BR.json | 3 +- .../nfandroidtv/translations/ru.json | 3 +- .../nfandroidtv/translations/tr.json | 3 +- .../nfandroidtv/translations/zh-Hant.json | 3 +- .../nightscout/translations/ca.json | 1 - .../nightscout/translations/cs.json | 1 - .../nightscout/translations/de.json | 1 - .../nightscout/translations/el.json | 1 - .../nightscout/translations/en.json | 1 - .../nightscout/translations/es.json | 1 - .../nightscout/translations/et.json | 1 - .../nightscout/translations/fr.json | 1 - .../nightscout/translations/hu.json | 1 - .../nightscout/translations/id.json | 1 - .../nightscout/translations/it.json | 1 - .../nightscout/translations/ja.json | 1 - .../nightscout/translations/ko.json | 1 - .../nightscout/translations/lb.json | 1 - .../nightscout/translations/nl.json | 1 - .../nightscout/translations/no.json | 1 - .../nightscout/translations/pl.json | 1 - .../nightscout/translations/pt-BR.json | 1 - .../nightscout/translations/ru.json | 1 - .../nightscout/translations/tr.json | 1 - .../nightscout/translations/uk.json | 1 - .../nightscout/translations/zh-Hant.json | 1 - .../nmap_tracker/translations/ar.json | 3 +- .../nmap_tracker/translations/ca.json | 3 +- .../nmap_tracker/translations/cs.json | 3 +- .../nmap_tracker/translations/de.json | 3 +- .../nmap_tracker/translations/el.json | 3 +- .../nmap_tracker/translations/en.json | 3 +- .../nmap_tracker/translations/es.json | 3 +- .../nmap_tracker/translations/et.json | 3 +- .../nmap_tracker/translations/fr.json | 3 +- .../nmap_tracker/translations/he.json | 3 +- .../nmap_tracker/translations/hu.json | 3 +- .../nmap_tracker/translations/id.json | 3 +- .../nmap_tracker/translations/it.json | 3 +- .../nmap_tracker/translations/ja.json | 3 +- .../nmap_tracker/translations/nl.json | 3 +- .../nmap_tracker/translations/no.json | 3 +- .../nmap_tracker/translations/pl.json | 3 +- .../nmap_tracker/translations/pt-BR.json | 3 +- .../nmap_tracker/translations/ru.json | 3 +- .../nmap_tracker/translations/tr.json | 3 +- .../nmap_tracker/translations/zh-Hans.json | 3 +- .../nmap_tracker/translations/zh-Hant.json | 3 +- .../components/notion/translations/bg.json | 1 - .../components/notion/translations/ca.json | 1 - .../components/notion/translations/cs.json | 1 - .../components/notion/translations/da.json | 3 - .../components/notion/translations/de.json | 1 - .../components/notion/translations/el.json | 1 - .../components/notion/translations/en.json | 1 - .../notion/translations/es-419.json | 3 - .../components/notion/translations/es.json | 1 - .../components/notion/translations/et.json | 1 - .../components/notion/translations/fr.json | 1 - .../components/notion/translations/hr.json | 3 - .../components/notion/translations/hu.json | 1 - .../components/notion/translations/id.json | 1 - .../components/notion/translations/it.json | 1 - .../components/notion/translations/ja.json | 1 - .../components/notion/translations/ko.json | 3 +- .../components/notion/translations/lb.json | 3 +- .../components/notion/translations/nl.json | 1 - .../components/notion/translations/no.json | 1 - .../components/notion/translations/pl.json | 1 - .../components/notion/translations/pt-BR.json | 1 - .../components/notion/translations/ru.json | 1 - .../components/notion/translations/sl.json | 3 - .../components/notion/translations/sv.json | 3 - .../components/notion/translations/tr.json | 1 - .../components/notion/translations/uk.json | 3 +- .../notion/translations/zh-Hans.json | 3 - .../notion/translations/zh-Hant.json | 1 - .../components/nut/translations/bg.json | 10 --- .../components/nut/translations/ca.json | 17 +--- .../components/nut/translations/cs.json | 17 +--- .../components/nut/translations/de.json | 17 +--- .../components/nut/translations/el.json | 17 +--- .../components/nut/translations/en.json | 17 +--- .../components/nut/translations/es-419.json | 13 +--- .../components/nut/translations/es.json | 17 +--- .../components/nut/translations/et.json | 17 +--- .../components/nut/translations/fr.json | 17 +--- .../components/nut/translations/he.json | 6 -- .../components/nut/translations/hu.json | 17 +--- .../components/nut/translations/id.json | 17 +--- .../components/nut/translations/it.json | 17 +--- .../components/nut/translations/ja.json | 17 +--- .../components/nut/translations/ko.json | 17 +--- .../components/nut/translations/lb.json | 13 +--- .../components/nut/translations/nl.json | 17 +--- .../components/nut/translations/no.json | 17 +--- .../components/nut/translations/pl.json | 17 +--- .../components/nut/translations/pt-BR.json | 17 +--- .../components/nut/translations/pt.json | 11 --- .../components/nut/translations/ru.json | 17 +--- .../components/nut/translations/sk.json | 17 +--- .../components/nut/translations/sl.json | 13 +--- .../components/nut/translations/sv.json | 10 --- .../components/nut/translations/tr.json | 17 +--- .../components/nut/translations/uk.json | 13 +--- .../components/nut/translations/zh-Hans.json | 17 +--- .../components/nut/translations/zh-Hant.json | 17 +--- .../components/onewire/translations/af.json | 14 ---- .../components/onewire/translations/bg.json | 12 +-- .../components/onewire/translations/ca.json | 17 +--- .../components/onewire/translations/cs.json | 13 +--- .../components/onewire/translations/de.json | 17 +--- .../components/onewire/translations/el.json | 17 +--- .../components/onewire/translations/en.json | 17 +--- .../components/onewire/translations/es.json | 16 +--- .../components/onewire/translations/et.json | 17 +--- .../components/onewire/translations/fr.json | 17 +--- .../components/onewire/translations/he.json | 2 +- .../components/onewire/translations/hu.json | 17 +--- .../components/onewire/translations/id.json | 17 +--- .../components/onewire/translations/it.json | 17 +--- .../components/onewire/translations/ja.json | 17 +--- .../components/onewire/translations/ka.json | 13 +--- .../components/onewire/translations/ko.json | 13 +--- .../components/onewire/translations/lb.json | 16 +--- .../components/onewire/translations/nl.json | 17 +--- .../components/onewire/translations/no.json | 17 +--- .../components/onewire/translations/pl.json | 17 +--- .../onewire/translations/pt-BR.json | 17 +--- .../components/onewire/translations/pt.json | 8 -- .../components/onewire/translations/ru.json | 17 +--- .../components/onewire/translations/sk.json | 12 +-- .../components/onewire/translations/sl.json | 3 - .../components/onewire/translations/sv.json | 9 --- .../components/onewire/translations/tr.json | 17 +--- .../components/onewire/translations/uk.json | 13 +--- .../onewire/translations/zh-Hant.json | 17 +--- .../components/onvif/translations/bg.json | 13 ---- .../components/onvif/translations/ca.json | 15 ---- .../components/onvif/translations/cs.json | 15 ---- .../components/onvif/translations/de.json | 15 ---- .../components/onvif/translations/el.json | 15 ---- .../components/onvif/translations/en.json | 15 ---- .../components/onvif/translations/es-419.json | 14 ---- .../components/onvif/translations/es.json | 15 ---- .../components/onvif/translations/et.json | 15 ---- .../components/onvif/translations/fi.json | 13 ---- .../components/onvif/translations/fr.json | 15 ---- .../components/onvif/translations/he.json | 15 ---- .../components/onvif/translations/hu.json | 15 ---- .../components/onvif/translations/id.json | 15 ---- .../components/onvif/translations/it.json | 15 ---- .../components/onvif/translations/ja.json | 15 ---- .../components/onvif/translations/ko.json | 15 ---- .../components/onvif/translations/lb.json | 15 ---- .../components/onvif/translations/nl.json | 15 ---- .../components/onvif/translations/no.json | 15 ---- .../components/onvif/translations/pl.json | 15 ---- .../components/onvif/translations/pt-BR.json | 15 ---- .../components/onvif/translations/pt.json | 15 ---- .../components/onvif/translations/ru.json | 15 ---- .../components/onvif/translations/sk.json | 11 --- .../components/onvif/translations/sl.json | 14 ---- .../components/onvif/translations/sv.json | 12 --- .../components/onvif/translations/tr.json | 15 ---- .../components/onvif/translations/uk.json | 15 ---- .../onvif/translations/zh-Hans.json | 15 ---- .../onvif/translations/zh-Hant.json | 15 ---- .../opentherm_gw/translations/bg.json | 6 +- .../opentherm_gw/translations/ca.json | 6 +- .../opentherm_gw/translations/cs.json | 6 +- .../opentherm_gw/translations/da.json | 6 +- .../opentherm_gw/translations/de.json | 6 +- .../opentherm_gw/translations/el.json | 6 +- .../opentherm_gw/translations/en.json | 6 +- .../opentherm_gw/translations/es-419.json | 6 +- .../opentherm_gw/translations/es.json | 6 +- .../opentherm_gw/translations/et.json | 6 +- .../opentherm_gw/translations/fr.json | 6 +- .../opentherm_gw/translations/hu.json | 6 +- .../opentherm_gw/translations/id.json | 6 +- .../opentherm_gw/translations/it.json | 6 +- .../opentherm_gw/translations/ja.json | 6 +- .../opentherm_gw/translations/ko.json | 6 +- .../opentherm_gw/translations/lb.json | 6 +- .../opentherm_gw/translations/nl.json | 6 +- .../opentherm_gw/translations/no.json | 6 +- .../opentherm_gw/translations/pl.json | 6 +- .../opentherm_gw/translations/pt-BR.json | 6 +- .../opentherm_gw/translations/ru.json | 6 +- .../opentherm_gw/translations/sl.json | 6 +- .../opentherm_gw/translations/sv.json | 6 +- .../opentherm_gw/translations/tr.json | 6 +- .../opentherm_gw/translations/uk.json | 6 +- .../opentherm_gw/translations/zh-Hant.json | 6 +- .../openweathermap/translations/ar.json | 3 +- .../openweathermap/translations/bg.json | 3 +- .../openweathermap/translations/ca.json | 3 +- .../openweathermap/translations/cs.json | 3 +- .../openweathermap/translations/de.json | 3 +- .../openweathermap/translations/el.json | 3 +- .../openweathermap/translations/en.json | 3 +- .../openweathermap/translations/es.json | 3 +- .../openweathermap/translations/et.json | 3 +- .../openweathermap/translations/fr.json | 3 +- .../openweathermap/translations/he.json | 3 +- .../openweathermap/translations/hu.json | 3 +- .../openweathermap/translations/id.json | 3 +- .../openweathermap/translations/it.json | 3 +- .../openweathermap/translations/ja.json | 3 +- .../openweathermap/translations/ko.json | 3 +- .../openweathermap/translations/lb.json | 3 +- .../openweathermap/translations/nl.json | 3 +- .../openweathermap/translations/no.json | 3 +- .../openweathermap/translations/pl.json | 3 +- .../openweathermap/translations/pt-BR.json | 3 +- .../openweathermap/translations/ru.json | 3 +- .../openweathermap/translations/sv.json | 3 +- .../openweathermap/translations/tr.json | 3 +- .../openweathermap/translations/uk.json | 3 +- .../openweathermap/translations/zh-Hant.json | 3 +- .../p1_monitor/translations/bg.json | 3 +- .../p1_monitor/translations/ca.json | 3 +- .../p1_monitor/translations/cs.json | 3 +- .../p1_monitor/translations/de.json | 3 +- .../p1_monitor/translations/el.json | 3 +- .../p1_monitor/translations/en.json | 3 +- .../p1_monitor/translations/es.json | 3 +- .../p1_monitor/translations/et.json | 3 +- .../p1_monitor/translations/fr.json | 3 +- .../p1_monitor/translations/he.json | 3 +- .../p1_monitor/translations/hu.json | 3 +- .../p1_monitor/translations/id.json | 3 +- .../p1_monitor/translations/it.json | 3 +- .../p1_monitor/translations/ja.json | 3 +- .../p1_monitor/translations/ko.json | 3 +- .../p1_monitor/translations/nl.json | 3 +- .../p1_monitor/translations/no.json | 3 +- .../p1_monitor/translations/pl.json | 3 +- .../p1_monitor/translations/pt-BR.json | 3 +- .../p1_monitor/translations/ru.json | 3 +- .../p1_monitor/translations/tr.json | 3 +- .../p1_monitor/translations/zh-Hant.json | 3 +- .../components/peco/translations/ca.json | 4 +- .../components/peco/translations/de.json | 4 +- .../components/peco/translations/el.json | 4 +- .../components/peco/translations/en.json | 4 +- .../components/peco/translations/et.json | 4 +- .../components/peco/translations/fr.json | 4 +- .../components/peco/translations/hu.json | 4 +- .../components/peco/translations/id.json | 4 +- .../components/peco/translations/it.json | 4 +- .../components/peco/translations/ja.json | 4 +- .../components/peco/translations/nl.json | 4 +- .../components/peco/translations/no.json | 4 +- .../components/peco/translations/pl.json | 4 +- .../components/peco/translations/pt-BR.json | 4 +- .../components/peco/translations/ru.json | 4 +- .../components/peco/translations/tr.json | 4 +- .../components/peco/translations/zh-Hant.json | 4 +- .../components/picnic/translations/ca.json | 3 +- .../components/picnic/translations/de.json | 3 +- .../components/picnic/translations/el.json | 3 +- .../components/picnic/translations/en.json | 3 +- .../components/picnic/translations/es.json | 3 +- .../components/picnic/translations/et.json | 3 +- .../components/picnic/translations/fr.json | 3 +- .../components/picnic/translations/he.json | 3 +- .../components/picnic/translations/hu.json | 3 +- .../components/picnic/translations/id.json | 3 +- .../components/picnic/translations/it.json | 3 +- .../components/picnic/translations/ja.json | 3 +- .../components/picnic/translations/nl.json | 3 +- .../components/picnic/translations/no.json | 3 +- .../components/picnic/translations/pl.json | 3 +- .../components/picnic/translations/pt-BR.json | 3 +- .../components/picnic/translations/ru.json | 3 +- .../components/picnic/translations/tr.json | 3 +- .../picnic/translations/zh-Hant.json | 3 +- .../components/plex/translations/bg.json | 6 -- .../components/plex/translations/ca.json | 6 +- .../components/plex/translations/cs.json | 6 +- .../components/plex/translations/de.json | 6 +- .../components/plex/translations/el.json | 6 +- .../components/plex/translations/en.json | 6 +- .../components/plex/translations/es.json | 6 +- .../components/plex/translations/et.json | 6 +- .../components/plex/translations/fi.json | 6 +- .../components/plex/translations/fr.json | 6 +- .../components/plex/translations/hu.json | 6 +- .../components/plex/translations/id.json | 6 +- .../components/plex/translations/it.json | 6 +- .../components/plex/translations/ja.json | 6 +- .../components/plex/translations/ko.json | 6 +- .../components/plex/translations/lb.json | 6 +- .../components/plex/translations/nl.json | 6 +- .../components/plex/translations/no.json | 6 +- .../components/plex/translations/pl.json | 6 +- .../components/plex/translations/pt-BR.json | 6 +- .../components/plex/translations/ru.json | 6 +- .../components/plex/translations/sl.json | 6 +- .../components/plex/translations/sv.json | 6 +- .../components/plex/translations/tr.json | 6 +- .../components/plex/translations/uk.json | 6 +- .../components/plex/translations/zh-Hans.json | 6 +- .../components/plex/translations/zh-Hant.json | 6 +- .../components/poolsense/translations/bg.json | 4 +- .../components/poolsense/translations/ca.json | 4 +- .../components/poolsense/translations/cs.json | 4 +- .../components/poolsense/translations/de.json | 4 +- .../components/poolsense/translations/el.json | 4 +- .../components/poolsense/translations/en.json | 4 +- .../components/poolsense/translations/es.json | 4 +- .../components/poolsense/translations/et.json | 4 +- .../components/poolsense/translations/fr.json | 4 +- .../components/poolsense/translations/he.json | 3 +- .../components/poolsense/translations/hu.json | 4 +- .../components/poolsense/translations/id.json | 4 +- .../components/poolsense/translations/it.json | 4 +- .../components/poolsense/translations/ja.json | 4 +- .../components/poolsense/translations/ko.json | 4 +- .../components/poolsense/translations/lb.json | 4 +- .../components/poolsense/translations/nl.json | 4 +- .../components/poolsense/translations/no.json | 4 +- .../components/poolsense/translations/pl.json | 4 +- .../poolsense/translations/pt-BR.json | 4 +- .../components/poolsense/translations/pt.json | 4 +- .../components/poolsense/translations/ru.json | 4 +- .../components/poolsense/translations/sv.json | 9 --- .../components/poolsense/translations/tr.json | 4 +- .../components/poolsense/translations/uk.json | 4 +- .../poolsense/translations/zh-Hant.json | 4 +- .../components/ps4/translations/bg.json | 11 +-- .../components/ps4/translations/ca.json | 11 +-- .../components/ps4/translations/cs.json | 11 +-- .../components/ps4/translations/da.json | 11 +-- .../components/ps4/translations/de.json | 11 +-- .../components/ps4/translations/el.json | 11 +-- .../components/ps4/translations/en.json | 11 +-- .../components/ps4/translations/es-419.json | 11 +-- .../components/ps4/translations/es.json | 11 +-- .../components/ps4/translations/et.json | 11 +-- .../components/ps4/translations/fi.json | 9 +-- .../components/ps4/translations/fr.json | 11 +-- .../components/ps4/translations/he.json | 9 +-- .../components/ps4/translations/hu.json | 11 +-- .../components/ps4/translations/id.json | 11 +-- .../components/ps4/translations/it.json | 11 +-- .../components/ps4/translations/ja.json | 11 +-- .../components/ps4/translations/ko.json | 11 +-- .../components/ps4/translations/lb.json | 11 +-- .../components/ps4/translations/nl.json | 11 +-- .../components/ps4/translations/nn.json | 9 +-- .../components/ps4/translations/no.json | 11 +-- .../components/ps4/translations/pl.json | 11 +-- .../components/ps4/translations/pt-BR.json | 11 +-- .../components/ps4/translations/pt.json | 9 +-- .../components/ps4/translations/ru.json | 11 +-- .../components/ps4/translations/sl.json | 11 +-- .../components/ps4/translations/sv.json | 11 +-- .../components/ps4/translations/th.json | 9 +-- .../components/ps4/translations/tr.json | 11 +-- .../components/ps4/translations/uk.json | 11 +-- .../components/ps4/translations/zh-Hans.json | 10 +-- .../components/ps4/translations/zh-Hant.json | 11 +-- .../pvpc_hourly_pricing/translations/ca.json | 8 +- .../pvpc_hourly_pricing/translations/cs.json | 3 +- .../pvpc_hourly_pricing/translations/de.json | 8 +- .../pvpc_hourly_pricing/translations/el.json | 8 +- .../pvpc_hourly_pricing/translations/en.json | 8 +- .../translations/es-419.json | 4 +- .../pvpc_hourly_pricing/translations/es.json | 8 +- .../pvpc_hourly_pricing/translations/et.json | 8 +- .../pvpc_hourly_pricing/translations/fr.json | 8 +- .../pvpc_hourly_pricing/translations/he.json | 7 -- .../pvpc_hourly_pricing/translations/hu.json | 8 +- .../pvpc_hourly_pricing/translations/id.json | 8 +- .../pvpc_hourly_pricing/translations/it.json | 8 +- .../pvpc_hourly_pricing/translations/ja.json | 8 +- .../pvpc_hourly_pricing/translations/ko.json | 4 +- .../pvpc_hourly_pricing/translations/lb.json | 4 +- .../pvpc_hourly_pricing/translations/nl.json | 8 +- .../pvpc_hourly_pricing/translations/no.json | 8 +- .../pvpc_hourly_pricing/translations/pl.json | 8 +- .../translations/pt-BR.json | 8 +- .../pvpc_hourly_pricing/translations/ru.json | 8 +- .../pvpc_hourly_pricing/translations/sl.json | 4 +- .../pvpc_hourly_pricing/translations/tr.json | 8 +- .../pvpc_hourly_pricing/translations/uk.json | 4 +- .../translations/zh-Hant.json | 8 +- .../components/recorder/translations/ko.json | 4 +- .../components/remote/translations/ca.json | 1 - .../components/remote/translations/cs.json | 1 - .../components/remote/translations/de.json | 1 - .../components/remote/translations/el.json | 1 - .../components/remote/translations/en.json | 1 - .../components/remote/translations/es.json | 1 - .../components/remote/translations/et.json | 1 - .../components/remote/translations/fr.json | 1 - .../components/remote/translations/he.json | 1 - .../components/remote/translations/hu.json | 1 - .../components/remote/translations/id.json | 1 - .../components/remote/translations/it.json | 1 - .../components/remote/translations/ja.json | 1 - .../components/remote/translations/nl.json | 1 - .../components/remote/translations/no.json | 1 - .../components/remote/translations/pl.json | 1 - .../components/remote/translations/pt-BR.json | 1 - .../components/remote/translations/ru.json | 1 - .../components/remote/translations/sv.json | 1 - .../components/remote/translations/tr.json | 1 - .../remote/translations/zh-Hans.json | 1 - .../remote/translations/zh-Hant.json | 1 - .../components/rfxtrx/translations/ca.json | 5 +- .../components/rfxtrx/translations/cs.json | 7 +- .../components/rfxtrx/translations/de.json | 5 +- .../components/rfxtrx/translations/el.json | 5 +- .../components/rfxtrx/translations/en.json | 5 +- .../components/rfxtrx/translations/es.json | 5 +- .../components/rfxtrx/translations/et.json | 5 +- .../components/rfxtrx/translations/fr.json | 5 +- .../components/rfxtrx/translations/hu.json | 5 +- .../components/rfxtrx/translations/id.json | 5 +- .../components/rfxtrx/translations/it.json | 5 +- .../components/rfxtrx/translations/ja.json | 5 +- .../components/rfxtrx/translations/ko.json | 5 +- .../components/rfxtrx/translations/lb.json | 7 +- .../components/rfxtrx/translations/nl.json | 5 +- .../components/rfxtrx/translations/no.json | 5 +- .../components/rfxtrx/translations/pl.json | 5 +- .../components/rfxtrx/translations/pt-BR.json | 5 +- .../components/rfxtrx/translations/ru.json | 5 +- .../components/rfxtrx/translations/sv.json | 7 +- .../components/rfxtrx/translations/tr.json | 5 +- .../components/rfxtrx/translations/uk.json | 5 +- .../rfxtrx/translations/zh-Hant.json | 5 +- .../components/roku/translations/ca.json | 7 +- .../components/roku/translations/cs.json | 7 +- .../components/roku/translations/de.json | 7 +- .../components/roku/translations/el.json | 7 +- .../components/roku/translations/en.json | 7 +- .../components/roku/translations/es-419.json | 4 - .../components/roku/translations/es.json | 7 +- .../components/roku/translations/et.json | 7 +- .../components/roku/translations/fr.json | 7 +- .../components/roku/translations/hu.json | 7 +- .../components/roku/translations/id.json | 7 +- .../components/roku/translations/it.json | 7 +- .../components/roku/translations/ja.json | 7 +- .../components/roku/translations/ko.json | 7 +- .../components/roku/translations/lb.json | 7 +- .../components/roku/translations/nl.json | 7 +- .../components/roku/translations/no.json | 7 +- .../components/roku/translations/pl.json | 7 +- .../components/roku/translations/pt-BR.json | 7 +- .../components/roku/translations/pt.json | 3 - .../components/roku/translations/ru.json | 7 +- .../components/roku/translations/sl.json | 4 +- .../components/roku/translations/sv.json | 4 - .../components/roku/translations/tr.json | 7 +- .../components/roku/translations/uk.json | 7 +- .../components/roku/translations/zh-Hant.json | 7 +- .../components/roomba/translations/bg.json | 6 +- .../components/roomba/translations/ca.json | 14 +--- .../components/roomba/translations/cs.json | 9 +-- .../components/roomba/translations/da.json | 3 - .../components/roomba/translations/de.json | 14 +--- .../components/roomba/translations/el.json | 14 +--- .../components/roomba/translations/en.json | 14 +--- .../roomba/translations/es-419.json | 6 +- .../components/roomba/translations/es.json | 14 +--- .../components/roomba/translations/et.json | 14 +--- .../components/roomba/translations/fr.json | 14 +--- .../components/roomba/translations/he.json | 8 +- .../components/roomba/translations/hu.json | 14 +--- .../components/roomba/translations/id.json | 14 +--- .../components/roomba/translations/it.json | 14 +--- .../components/roomba/translations/ja.json | 14 +--- .../components/roomba/translations/ko.json | 14 +--- .../components/roomba/translations/lb.json | 14 +--- .../components/roomba/translations/nl.json | 14 +--- .../components/roomba/translations/no.json | 14 +--- .../components/roomba/translations/pl.json | 14 +--- .../components/roomba/translations/pt-BR.json | 14 +--- .../components/roomba/translations/pt.json | 5 +- .../components/roomba/translations/ru.json | 14 +--- .../components/roomba/translations/sl.json | 6 +- .../components/roomba/translations/sv.json | 6 +- .../components/roomba/translations/tr.json | 14 +--- .../components/roomba/translations/uk.json | 6 +- .../roomba/translations/zh-Hant.json | 14 +--- .../components/roon/translations/bg.json | 5 -- .../components/roon/translations/ca.json | 6 -- .../components/roon/translations/cs.json | 5 -- .../components/roon/translations/de.json | 6 -- .../components/roon/translations/el.json | 6 -- .../components/roon/translations/en.json | 6 -- .../components/roon/translations/es.json | 6 -- .../components/roon/translations/et.json | 6 -- .../components/roon/translations/fr.json | 6 -- .../components/roon/translations/he.json | 6 -- .../components/roon/translations/hu.json | 4 - .../components/roon/translations/id.json | 6 -- .../components/roon/translations/it.json | 4 - .../components/roon/translations/ja.json | 6 -- .../components/roon/translations/ko.json | 13 ++-- .../components/roon/translations/lb.json | 6 -- .../components/roon/translations/nl.json | 6 -- .../components/roon/translations/no.json | 6 -- .../components/roon/translations/pl.json | 6 -- .../components/roon/translations/pt-BR.json | 6 -- .../components/roon/translations/pt.json | 7 -- .../components/roon/translations/ru.json | 6 -- .../components/roon/translations/tr.json | 6 -- .../components/roon/translations/uk.json | 6 -- .../components/roon/translations/zh-Hant.json | 6 -- .../components/sabnzbd/translations/ko.json | 15 ++++ .../components/samsungtv/translations/bg.json | 3 - .../components/samsungtv/translations/ca.json | 4 +- .../components/samsungtv/translations/cs.json | 3 +- .../components/samsungtv/translations/da.json | 3 +- .../components/samsungtv/translations/de.json | 4 +- .../components/samsungtv/translations/el.json | 4 +- .../components/samsungtv/translations/en.json | 4 +- .../samsungtv/translations/es-419.json | 3 +- .../components/samsungtv/translations/es.json | 4 +- .../components/samsungtv/translations/et.json | 4 +- .../components/samsungtv/translations/fr.json | 4 +- .../components/samsungtv/translations/he.json | 3 - .../components/samsungtv/translations/hu.json | 4 +- .../components/samsungtv/translations/id.json | 4 +- .../components/samsungtv/translations/it.json | 4 +- .../components/samsungtv/translations/ja.json | 4 +- .../components/samsungtv/translations/ko.json | 6 +- .../components/samsungtv/translations/lb.json | 3 +- .../components/samsungtv/translations/nl.json | 4 +- .../components/samsungtv/translations/no.json | 4 +- .../components/samsungtv/translations/pl.json | 4 +- .../samsungtv/translations/pt-BR.json | 4 +- .../components/samsungtv/translations/pt.json | 3 - .../components/samsungtv/translations/ru.json | 4 +- .../components/samsungtv/translations/sl.json | 3 +- .../components/samsungtv/translations/sv.json | 3 +- .../components/samsungtv/translations/tr.json | 4 +- .../components/samsungtv/translations/uk.json | 3 +- .../samsungtv/translations/zh-Hans.json | 3 +- .../samsungtv/translations/zh-Hant.json | 4 +- .../components/sensibo/translations/bg.json | 3 +- .../components/sensibo/translations/ca.json | 3 +- .../components/sensibo/translations/cs.json | 3 +- .../components/sensibo/translations/de.json | 3 +- .../components/sensibo/translations/el.json | 3 +- .../components/sensibo/translations/en.json | 3 +- .../components/sensibo/translations/es.json | 3 +- .../components/sensibo/translations/et.json | 3 +- .../components/sensibo/translations/fr.json | 3 +- .../components/sensibo/translations/he.json | 3 +- .../components/sensibo/translations/hu.json | 3 +- .../components/sensibo/translations/id.json | 3 +- .../components/sensibo/translations/it.json | 3 +- .../components/sensibo/translations/ja.json | 3 +- .../components/sensibo/translations/nl.json | 3 +- .../components/sensibo/translations/no.json | 3 +- .../components/sensibo/translations/pl.json | 3 +- .../sensibo/translations/pt-BR.json | 3 +- .../components/sensibo/translations/ru.json | 3 +- .../components/sensibo/translations/sk.json | 3 +- .../components/sensibo/translations/tr.json | 3 +- .../sensibo/translations/zh-Hant.json | 3 +- .../components/sentry/translations/af.json | 9 --- .../components/sentry/translations/ca.json | 4 +- .../components/sentry/translations/cs.json | 4 +- .../components/sentry/translations/da.json | 6 -- .../components/sentry/translations/de.json | 4 +- .../components/sentry/translations/el.json | 4 +- .../components/sentry/translations/en.json | 4 +- .../sentry/translations/es-419.json | 6 -- .../components/sentry/translations/es.json | 4 +- .../components/sentry/translations/et.json | 4 +- .../components/sentry/translations/fr.json | 4 +- .../components/sentry/translations/hu.json | 4 +- .../components/sentry/translations/id.json | 4 +- .../components/sentry/translations/it.json | 4 +- .../components/sentry/translations/ja.json | 4 +- .../components/sentry/translations/ko.json | 4 +- .../components/sentry/translations/lb.json | 4 +- .../components/sentry/translations/nl.json | 4 +- .../components/sentry/translations/no.json | 4 +- .../components/sentry/translations/pl.json | 4 +- .../components/sentry/translations/pt-BR.json | 4 +- .../components/sentry/translations/ru.json | 4 +- .../components/sentry/translations/sl.json | 6 -- .../components/sentry/translations/sv.json | 6 -- .../components/sentry/translations/tr.json | 4 +- .../components/sentry/translations/uk.json | 4 +- .../sentry/translations/zh-Hant.json | 4 +- .../components/senz/translations/ko.json | 16 ++++ .../components/shelly/translations/ca.json | 1 + .../components/shelly/translations/fr.json | 1 + .../components/shelly/translations/hu.json | 1 + .../components/shelly/translations/it.json | 1 + .../components/shelly/translations/pt-BR.json | 1 + .../components/sia/translations/ar.json | 3 +- .../components/sia/translations/ca.json | 3 +- .../components/sia/translations/de.json | 3 +- .../components/sia/translations/el.json | 3 +- .../components/sia/translations/en.json | 3 +- .../components/sia/translations/es.json | 3 +- .../components/sia/translations/et.json | 3 +- .../components/sia/translations/fr.json | 3 +- .../components/sia/translations/hu.json | 3 +- .../components/sia/translations/id.json | 3 +- .../components/sia/translations/it.json | 3 +- .../components/sia/translations/ja.json | 3 +- .../components/sia/translations/nl.json | 3 +- .../components/sia/translations/no.json | 3 +- .../components/sia/translations/pl.json | 3 +- .../components/sia/translations/pt-BR.json | 3 +- .../components/sia/translations/ru.json | 3 +- .../components/sia/translations/tr.json | 3 +- .../components/sia/translations/zh-Hant.json | 3 +- .../simplisafe/translations/bg.json | 5 +- .../simplisafe/translations/ca.json | 14 +--- .../simplisafe/translations/cs.json | 9 +-- .../simplisafe/translations/da.json | 6 +- .../simplisafe/translations/de.json | 14 +--- .../simplisafe/translations/el.json | 14 +--- .../simplisafe/translations/en.json | 14 +--- .../simplisafe/translations/es-419.json | 7 +- .../simplisafe/translations/es.json | 14 +--- .../simplisafe/translations/et.json | 14 +--- .../simplisafe/translations/fi.json | 6 +- .../simplisafe/translations/fr.json | 14 +--- .../simplisafe/translations/hu.json | 14 +--- .../simplisafe/translations/id.json | 14 +--- .../simplisafe/translations/it.json | 14 +--- .../simplisafe/translations/ja.json | 14 +--- .../simplisafe/translations/ko.json | 12 +-- .../simplisafe/translations/lb.json | 9 +-- .../simplisafe/translations/nl.json | 14 +--- .../simplisafe/translations/no.json | 14 +--- .../simplisafe/translations/pl.json | 14 +--- .../simplisafe/translations/pt-BR.json | 14 +--- .../simplisafe/translations/pt.json | 4 +- .../simplisafe/translations/ro.json | 6 +- .../simplisafe/translations/ru.json | 14 +--- .../simplisafe/translations/sl.json | 10 +-- .../simplisafe/translations/sv.json | 7 +- .../simplisafe/translations/tr.json | 14 +--- .../simplisafe/translations/uk.json | 9 +-- .../simplisafe/translations/zh-Hans.json | 5 +- .../simplisafe/translations/zh-Hant.json | 14 +--- .../components/siren/translations/cs.json | 3 + .../components/siren/translations/no.json | 3 + .../components/slack/translations/cs.json | 19 +++++ .../{climacell => slack}/translations/he.json | 11 +-- .../components/slack/translations/id.json | 6 +- .../components/smhi/translations/bg.json | 4 +- .../components/smhi/translations/ca.json | 4 +- .../components/smhi/translations/cs.json | 4 +- .../components/smhi/translations/da.json | 4 +- .../components/smhi/translations/de.json | 4 +- .../components/smhi/translations/el.json | 4 +- .../components/smhi/translations/en.json | 4 +- .../components/smhi/translations/es-419.json | 4 +- .../components/smhi/translations/es.json | 4 +- .../components/smhi/translations/et.json | 4 +- .../components/smhi/translations/fi.json | 4 +- .../components/smhi/translations/fr.json | 4 +- .../components/smhi/translations/he.json | 3 +- .../components/smhi/translations/hu.json | 4 +- .../components/smhi/translations/id.json | 4 +- .../components/smhi/translations/it.json | 4 +- .../components/smhi/translations/ja.json | 4 +- .../components/smhi/translations/ko.json | 4 +- .../components/smhi/translations/lb.json | 4 +- .../components/smhi/translations/nl.json | 4 +- .../components/smhi/translations/no.json | 4 +- .../components/smhi/translations/pl.json | 4 +- .../components/smhi/translations/pt-BR.json | 4 +- .../components/smhi/translations/pt.json | 4 +- .../components/smhi/translations/ro.json | 4 +- .../components/smhi/translations/ru.json | 4 +- .../components/smhi/translations/sk.json | 3 +- .../components/smhi/translations/sl.json | 4 +- .../components/smhi/translations/sv.json | 4 +- .../components/smhi/translations/th.json | 3 +- .../components/smhi/translations/tr.json | 4 +- .../components/smhi/translations/uk.json | 4 +- .../components/smhi/translations/zh-Hans.json | 4 +- .../components/smhi/translations/zh-Hant.json | 4 +- .../somfy_mylink/translations/ca.json | 12 +-- .../somfy_mylink/translations/de.json | 12 +-- .../somfy_mylink/translations/el.json | 12 +-- .../somfy_mylink/translations/en.json | 12 +-- .../somfy_mylink/translations/es.json | 12 +-- .../somfy_mylink/translations/et.json | 12 +-- .../somfy_mylink/translations/fr.json | 12 +-- .../somfy_mylink/translations/hu.json | 12 +-- .../somfy_mylink/translations/id.json | 12 +-- .../somfy_mylink/translations/it.json | 12 +-- .../somfy_mylink/translations/ja.json | 12 +-- .../somfy_mylink/translations/ko.json | 12 +-- .../somfy_mylink/translations/lb.json | 3 +- .../somfy_mylink/translations/nl.json | 12 +-- .../somfy_mylink/translations/no.json | 12 +-- .../somfy_mylink/translations/pl.json | 12 +-- .../somfy_mylink/translations/pt-BR.json | 12 +-- .../somfy_mylink/translations/ru.json | 12 +-- .../somfy_mylink/translations/tr.json | 12 +-- .../somfy_mylink/translations/uk.json | 10 +-- .../somfy_mylink/translations/zh-Hant.json | 12 +-- .../components/sonarr/translations/bg.json | 3 - .../components/sonarr/translations/ca.json | 4 - .../components/sonarr/translations/cs.json | 3 - .../components/sonarr/translations/de.json | 4 - .../components/sonarr/translations/el.json | 4 - .../components/sonarr/translations/en.json | 4 - .../components/sonarr/translations/es.json | 4 - .../components/sonarr/translations/et.json | 4 - .../components/sonarr/translations/fr.json | 4 - .../components/sonarr/translations/he.json | 3 - .../components/sonarr/translations/hu.json | 4 - .../components/sonarr/translations/id.json | 4 - .../components/sonarr/translations/it.json | 4 - .../components/sonarr/translations/ja.json | 4 - .../components/sonarr/translations/ko.json | 4 - .../components/sonarr/translations/lb.json | 4 - .../components/sonarr/translations/nl.json | 4 - .../components/sonarr/translations/no.json | 4 - .../components/sonarr/translations/pl.json | 4 - .../components/sonarr/translations/pt-BR.json | 4 - .../components/sonarr/translations/pt.json | 3 - .../components/sonarr/translations/ru.json | 4 - .../components/sonarr/translations/sk.json | 3 +- .../components/sonarr/translations/sl.json | 1 - .../components/sonarr/translations/tr.json | 4 - .../components/sonarr/translations/uk.json | 4 - .../sonarr/translations/zh-Hans.json | 3 - .../sonarr/translations/zh-Hant.json | 4 - .../speedtestdotnet/translations/ar.json | 3 - .../speedtestdotnet/translations/ca.json | 3 +- .../speedtestdotnet/translations/cs.json | 3 +- .../speedtestdotnet/translations/de.json | 3 +- .../speedtestdotnet/translations/el.json | 3 +- .../speedtestdotnet/translations/en.json | 3 +- .../speedtestdotnet/translations/es.json | 3 +- .../speedtestdotnet/translations/et.json | 3 +- .../speedtestdotnet/translations/fr.json | 3 +- .../speedtestdotnet/translations/he.json | 3 +- .../speedtestdotnet/translations/hu.json | 3 +- .../speedtestdotnet/translations/id.json | 3 +- .../speedtestdotnet/translations/it.json | 3 +- .../speedtestdotnet/translations/ja.json | 3 +- .../speedtestdotnet/translations/ko.json | 3 +- .../speedtestdotnet/translations/lb.json | 3 +- .../speedtestdotnet/translations/nl.json | 3 +- .../speedtestdotnet/translations/no.json | 3 +- .../speedtestdotnet/translations/pl.json | 3 +- .../speedtestdotnet/translations/pt-BR.json | 3 +- .../speedtestdotnet/translations/ru.json | 3 +- .../speedtestdotnet/translations/tr.json | 3 +- .../speedtestdotnet/translations/uk.json | 3 +- .../speedtestdotnet/translations/zh-Hant.json | 3 +- .../components/sql/translations/ca.json | 6 +- .../components/sql/translations/de.json | 6 +- .../components/sql/translations/el.json | 6 +- .../components/sql/translations/en.json | 6 +- .../components/sql/translations/es.json | 3 +- .../components/sql/translations/et.json | 6 +- .../components/sql/translations/fr.json | 6 +- .../components/sql/translations/hu.json | 6 +- .../components/sql/translations/id.json | 6 +- .../components/sql/translations/it.json | 6 +- .../components/sql/translations/ja.json | 6 +- .../components/sql/translations/ko.json | 51 ++++++++++++ .../components/sql/translations/nl.json | 6 +- .../components/sql/translations/no.json | 6 +- .../components/sql/translations/pl.json | 6 +- .../components/sql/translations/pt-BR.json | 6 +- .../components/sql/translations/ru.json | 6 +- .../components/sql/translations/tr.json | 6 +- .../components/sql/translations/zh-Hant.json | 6 +- .../steam_online/translations/ko.json | 36 +++++++++ .../components/sun/translations/ko.json | 7 ++ .../components/switch/translations/ca.json | 11 --- .../components/switch/translations/cs.json | 11 --- .../components/switch/translations/de.json | 11 --- .../components/switch/translations/el.json | 11 --- .../components/switch/translations/en.json | 11 --- .../components/switch/translations/es.json | 8 -- .../components/switch/translations/et.json | 11 --- .../components/switch/translations/fr.json | 11 --- .../components/switch/translations/he.json | 11 --- .../components/switch/translations/hu.json | 11 --- .../components/switch/translations/id.json | 11 --- .../components/switch/translations/it.json | 11 --- .../components/switch/translations/ja.json | 11 --- .../components/switch/translations/nl.json | 11 --- .../components/switch/translations/no.json | 11 --- .../components/switch/translations/pl.json | 11 --- .../components/switch/translations/pt-BR.json | 11 --- .../components/switch/translations/ru.json | 11 --- .../components/switch/translations/sv.json | 1 - .../components/switch/translations/tr.json | 11 --- .../switch/translations/zh-Hans.json | 1 - .../switch/translations/zh-Hant.json | 11 --- .../switch_as_x/translations/ca.json | 8 +- .../switch_as_x/translations/cs.json | 8 +- .../switch_as_x/translations/de.json | 8 +- .../switch_as_x/translations/el.json | 8 +- .../switch_as_x/translations/en.json | 8 +- .../switch_as_x/translations/et.json | 8 +- .../switch_as_x/translations/fr.json | 8 +- .../switch_as_x/translations/he.json | 11 --- .../switch_as_x/translations/hu.json | 8 +- .../switch_as_x/translations/id.json | 8 +- .../switch_as_x/translations/it.json | 8 +- .../switch_as_x/translations/ja.json | 8 +- .../switch_as_x/translations/ko.json | 14 ++++ .../switch_as_x/translations/nl.json | 8 +- .../switch_as_x/translations/no.json | 8 +- .../switch_as_x/translations/pl.json | 8 +- .../switch_as_x/translations/pt-BR.json | 8 +- .../switch_as_x/translations/ru.json | 8 +- .../switch_as_x/translations/sv.json | 8 +- .../switch_as_x/translations/tr.json | 8 +- .../switch_as_x/translations/zh-Hans.json | 8 +- .../switch_as_x/translations/zh-Hant.json | 8 +- .../components/switchbot/translations/bg.json | 3 - .../components/switchbot/translations/ca.json | 3 - .../components/switchbot/translations/cs.json | 3 - .../components/switchbot/translations/de.json | 3 - .../components/switchbot/translations/el.json | 3 - .../components/switchbot/translations/en.json | 3 - .../components/switchbot/translations/es.json | 3 - .../components/switchbot/translations/et.json | 3 - .../components/switchbot/translations/fr.json | 3 - .../components/switchbot/translations/he.json | 3 - .../components/switchbot/translations/hu.json | 1 - .../components/switchbot/translations/id.json | 3 - .../components/switchbot/translations/it.json | 1 - .../components/switchbot/translations/ja.json | 3 - .../components/switchbot/translations/nl.json | 3 - .../components/switchbot/translations/no.json | 3 - .../components/switchbot/translations/pl.json | 1 - .../switchbot/translations/pt-BR.json | 3 - .../components/switchbot/translations/ru.json | 3 - .../components/switchbot/translations/tr.json | 1 - .../switchbot/translations/zh-Hant.json | 3 - .../switcher_kis/translations/ko.json | 9 +++ .../synology_dsm/translations/bg.json | 13 +--- .../synology_dsm/translations/ca.json | 14 +--- .../synology_dsm/translations/cs.json | 13 +--- .../synology_dsm/translations/de.json | 14 +--- .../synology_dsm/translations/el.json | 14 +--- .../synology_dsm/translations/en.json | 14 +--- .../synology_dsm/translations/es-419.json | 6 +- .../synology_dsm/translations/es.json | 14 +--- .../synology_dsm/translations/et.json | 14 +--- .../synology_dsm/translations/fi.json | 3 - .../synology_dsm/translations/fr.json | 14 +--- .../synology_dsm/translations/he.json | 6 -- .../synology_dsm/translations/hu.json | 14 +--- .../synology_dsm/translations/id.json | 14 +--- .../synology_dsm/translations/it.json | 14 +--- .../synology_dsm/translations/ja.json | 14 +--- .../synology_dsm/translations/ko.json | 6 +- .../synology_dsm/translations/lb.json | 6 +- .../synology_dsm/translations/nb.json | 5 -- .../synology_dsm/translations/nl.json | 14 +--- .../synology_dsm/translations/no.json | 14 +--- .../synology_dsm/translations/pl.json | 14 +--- .../synology_dsm/translations/pt-BR.json | 14 +--- .../synology_dsm/translations/ru.json | 14 +--- .../synology_dsm/translations/sk.json | 5 -- .../synology_dsm/translations/sl.json | 6 +- .../synology_dsm/translations/sv.json | 3 +- .../synology_dsm/translations/tr.json | 14 +--- .../synology_dsm/translations/uk.json | 6 +- .../synology_dsm/translations/zh-Hans.json | 12 +-- .../synology_dsm/translations/zh-Hant.json | 14 +--- .../components/tasmota/translations/bg.json | 3 - .../components/tasmota/translations/ca.json | 4 +- .../components/tasmota/translations/cs.json | 4 - .../components/tasmota/translations/de.json | 4 +- .../components/tasmota/translations/el.json | 4 +- .../components/tasmota/translations/en.json | 4 +- .../components/tasmota/translations/es.json | 4 +- .../components/tasmota/translations/et.json | 4 +- .../components/tasmota/translations/fr.json | 4 +- .../components/tasmota/translations/he.json | 4 +- .../components/tasmota/translations/hu.json | 4 +- .../components/tasmota/translations/id.json | 4 +- .../components/tasmota/translations/it.json | 4 +- .../components/tasmota/translations/ja.json | 4 +- .../components/tasmota/translations/ko.json | 4 +- .../components/tasmota/translations/lb.json | 4 +- .../components/tasmota/translations/nl.json | 4 +- .../components/tasmota/translations/no.json | 4 +- .../components/tasmota/translations/pl.json | 4 +- .../tasmota/translations/pt-BR.json | 4 +- .../components/tasmota/translations/pt.json | 3 +- .../components/tasmota/translations/ru.json | 4 +- .../components/tasmota/translations/tr.json | 4 +- .../components/tasmota/translations/uk.json | 4 +- .../tasmota/translations/zh-Hant.json | 4 +- .../components/tautulli/translations/ko.json | 12 +++ .../components/threshold/translations/ca.json | 2 - .../components/threshold/translations/de.json | 2 - .../components/threshold/translations/el.json | 2 - .../components/threshold/translations/en.json | 2 - .../components/threshold/translations/et.json | 2 - .../components/threshold/translations/fr.json | 2 - .../components/threshold/translations/hu.json | 2 - .../components/threshold/translations/id.json | 2 - .../components/threshold/translations/it.json | 2 - .../components/threshold/translations/ja.json | 2 - .../components/threshold/translations/ko.json | 38 +++++++++ .../components/threshold/translations/nl.json | 2 - .../components/threshold/translations/no.json | 2 - .../components/threshold/translations/pl.json | 2 - .../threshold/translations/pt-BR.json | 2 - .../components/threshold/translations/ru.json | 2 - .../components/threshold/translations/tr.json | 2 - .../threshold/translations/zh-Hans.json | 2 - .../threshold/translations/zh-Hant.json | 2 - .../components/tibber/translations/ca.json | 3 +- .../components/tibber/translations/cs.json | 3 +- .../components/tibber/translations/de.json | 3 +- .../components/tibber/translations/el.json | 3 +- .../components/tibber/translations/en.json | 3 +- .../components/tibber/translations/es.json | 3 +- .../components/tibber/translations/et.json | 3 +- .../components/tibber/translations/fi.json | 9 --- .../components/tibber/translations/fr.json | 3 +- .../components/tibber/translations/he.json | 3 +- .../components/tibber/translations/hu.json | 3 +- .../components/tibber/translations/id.json | 3 +- .../components/tibber/translations/it.json | 3 +- .../components/tibber/translations/ja.json | 3 +- .../components/tibber/translations/ko.json | 3 +- .../components/tibber/translations/lb.json | 3 +- .../components/tibber/translations/nl.json | 3 +- .../components/tibber/translations/no.json | 3 +- .../components/tibber/translations/pl.json | 3 +- .../components/tibber/translations/pt-BR.json | 3 +- .../components/tibber/translations/pt.json | 3 +- .../components/tibber/translations/ru.json | 3 +- .../components/tibber/translations/sl.json | 3 +- .../components/tibber/translations/tr.json | 3 +- .../components/tibber/translations/uk.json | 3 +- .../tibber/translations/zh-Hant.json | 3 +- .../components/tod/translations/ca.json | 7 +- .../components/tod/translations/de.json | 7 +- .../components/tod/translations/el.json | 7 +- .../components/tod/translations/en.json | 7 +- .../components/tod/translations/es.json | 3 +- .../components/tod/translations/et.json | 7 +- .../components/tod/translations/fr.json | 7 +- .../components/tod/translations/he.json | 7 +- .../components/tod/translations/hu.json | 7 +- .../components/tod/translations/id.json | 7 +- .../components/tod/translations/it.json | 7 +- .../components/tod/translations/ja.json | 7 +- .../components/tod/translations/ko.json | 3 + .../components/tod/translations/nl.json | 7 +- .../components/tod/translations/no.json | 7 +- .../components/tod/translations/pl.json | 7 +- .../components/tod/translations/pt-BR.json | 7 +- .../components/tod/translations/ru.json | 7 +- .../components/tod/translations/tr.json | 7 +- .../components/tod/translations/zh-Hans.json | 7 +- .../components/tod/translations/zh-Hant.json | 7 +- .../components/tolo/translations/bg.json | 3 +- .../components/tolo/translations/ca.json | 3 +- .../components/tolo/translations/de.json | 3 +- .../components/tolo/translations/el.json | 3 +- .../components/tolo/translations/en.json | 3 +- .../components/tolo/translations/es.json | 3 +- .../components/tolo/translations/et.json | 3 +- .../components/tolo/translations/fr.json | 3 +- .../components/tolo/translations/he.json | 3 +- .../components/tolo/translations/hu.json | 3 +- .../components/tolo/translations/id.json | 3 +- .../components/tolo/translations/it.json | 3 +- .../components/tolo/translations/ja.json | 3 +- .../components/tolo/translations/ko.json | 9 +++ .../components/tolo/translations/nl.json | 3 +- .../components/tolo/translations/no.json | 3 +- .../components/tolo/translations/pl.json | 3 +- .../components/tolo/translations/pt-BR.json | 3 +- .../components/tolo/translations/ru.json | 3 +- .../components/tolo/translations/sl.json | 3 +- .../components/tolo/translations/tr.json | 3 +- .../components/tolo/translations/zh-Hant.json | 3 +- .../tomorrowio/translations/bg.json | 2 - .../tomorrowio/translations/ca.json | 2 - .../tomorrowio/translations/de.json | 2 - .../tomorrowio/translations/el.json | 2 - .../tomorrowio/translations/en.json | 2 - .../tomorrowio/translations/es.json | 1 - .../tomorrowio/translations/et.json | 2 - .../tomorrowio/translations/fr.json | 2 - .../tomorrowio/translations/he.json | 2 - .../tomorrowio/translations/hu.json | 2 - .../tomorrowio/translations/id.json | 2 - .../tomorrowio/translations/it.json | 2 - .../tomorrowio/translations/ja.json | 2 - .../tomorrowio/translations/nl.json | 2 - .../tomorrowio/translations/no.json | 2 - .../tomorrowio/translations/pl.json | 2 - .../tomorrowio/translations/pt-BR.json | 2 - .../tomorrowio/translations/ru.json | 2 - .../tomorrowio/translations/tr.json | 2 - .../tomorrowio/translations/zh-Hant.json | 2 - .../totalconnect/translations/ca.json | 4 +- .../totalconnect/translations/cs.json | 8 +- .../totalconnect/translations/de.json | 4 +- .../totalconnect/translations/el.json | 4 +- .../totalconnect/translations/en.json | 4 +- .../totalconnect/translations/es-419.json | 3 +- .../totalconnect/translations/es.json | 4 +- .../totalconnect/translations/et.json | 4 +- .../totalconnect/translations/fr.json | 4 +- .../totalconnect/translations/he.json | 1 - .../totalconnect/translations/hu.json | 4 +- .../totalconnect/translations/id.json | 4 +- .../totalconnect/translations/it.json | 4 +- .../totalconnect/translations/ja.json | 4 +- .../totalconnect/translations/ko.json | 6 +- .../totalconnect/translations/lb.json | 3 +- .../totalconnect/translations/nl.json | 4 +- .../totalconnect/translations/no.json | 4 +- .../totalconnect/translations/pl.json | 4 +- .../totalconnect/translations/pt-BR.json | 4 +- .../totalconnect/translations/pt.json | 5 -- .../totalconnect/translations/ru.json | 4 +- .../totalconnect/translations/sk.json | 7 -- .../totalconnect/translations/sl.json | 3 +- .../totalconnect/translations/tr.json | 4 +- .../totalconnect/translations/uk.json | 3 +- .../totalconnect/translations/zh-Hant.json | 4 +- .../components/tplink/translations/bg.json | 6 +- .../components/tplink/translations/ca.json | 6 +- .../components/tplink/translations/cs.json | 6 +- .../components/tplink/translations/da.json | 8 +- .../components/tplink/translations/de.json | 6 +- .../components/tplink/translations/el.json | 6 +- .../components/tplink/translations/en.json | 6 +- .../tplink/translations/es-419.json | 8 +- .../components/tplink/translations/es.json | 6 +- .../components/tplink/translations/et.json | 6 +- .../components/tplink/translations/fr.json | 6 +- .../components/tplink/translations/he.json | 6 +- .../components/tplink/translations/hu.json | 6 +- .../components/tplink/translations/id.json | 6 +- .../components/tplink/translations/it.json | 6 +- .../components/tplink/translations/ja.json | 6 +- .../components/tplink/translations/ko.json | 6 +- .../components/tplink/translations/lb.json | 8 +- .../components/tplink/translations/nl.json | 6 +- .../components/tplink/translations/no.json | 6 +- .../components/tplink/translations/pl.json | 6 +- .../components/tplink/translations/pt-BR.json | 6 +- .../components/tplink/translations/pt.json | 8 +- .../components/tplink/translations/ru.json | 6 +- .../components/tplink/translations/sl.json | 8 +- .../components/tplink/translations/sv.json | 8 +- .../components/tplink/translations/tr.json | 6 +- .../components/tplink/translations/uk.json | 8 +- .../tplink/translations/zh-Hans.json | 8 +- .../tplink/translations/zh-Hant.json | 6 +- .../trafikverket_ferry/translations/ko.json | 28 +++++++ .../translations/bg.json | 1 - .../translations/ca.json | 2 - .../translations/de.json | 2 - .../translations/el.json | 2 - .../translations/en.json | 2 - .../translations/es.json | 2 - .../translations/et.json | 2 - .../translations/fr.json | 2 - .../translations/he.json | 1 - .../translations/hu.json | 2 - .../translations/id.json | 2 - .../translations/it.json | 2 - .../translations/ja.json | 2 - .../translations/lt.json | 11 --- .../translations/nb.json | 3 +- .../translations/nl.json | 2 - .../translations/no.json | 2 - .../translations/pl.json | 2 - .../translations/pt-BR.json | 2 - .../translations/ru.json | 2 - .../translations/tr.json | 2 - .../translations/zh-Hant.json | 2 - .../components/tuya/translations/af.json | 8 -- .../components/tuya/translations/bg.json | 36 +-------- .../components/tuya/translations/ca.json | 65 +--------------- .../components/tuya/translations/cs.json | 51 +----------- .../components/tuya/translations/de.json | 65 +--------------- .../components/tuya/translations/el.json | 65 +--------------- .../components/tuya/translations/en.json | 65 +--------------- .../components/tuya/translations/en_GB.json | 14 ---- .../components/tuya/translations/es.json | 65 +--------------- .../components/tuya/translations/et.json | 65 +--------------- .../components/tuya/translations/fi.json | 5 +- .../components/tuya/translations/fr.json | 65 +--------------- .../components/tuya/translations/he.json | 52 +------------ .../components/tuya/translations/hu.json | 65 +--------------- .../components/tuya/translations/id.json | 65 +--------------- .../components/tuya/translations/is.json | 15 +--- .../components/tuya/translations/it.json | 65 +--------------- .../components/tuya/translations/ja.json | 65 +--------------- .../components/tuya/translations/ka.json | 37 --------- .../components/tuya/translations/ko.json | 50 +----------- .../components/tuya/translations/lb.json | 44 +---------- .../components/tuya/translations/nl.json | 65 +--------------- .../components/tuya/translations/no.json | 65 +--------------- .../components/tuya/translations/pl.json | 65 +--------------- .../components/tuya/translations/pt-BR.json | 65 +--------------- .../components/tuya/translations/pt.json | 10 --- .../components/tuya/translations/ru.json | 65 +--------------- .../components/tuya/translations/sk.json | 11 +-- .../components/tuya/translations/sl.json | 20 ----- .../components/tuya/translations/sv.json | 5 +- .../components/tuya/translations/tr.json | 65 +--------------- .../components/tuya/translations/uk.json | 48 +----------- .../components/tuya/translations/zh-Hans.json | 64 +-------------- .../components/tuya/translations/zh-Hant.json | 65 +--------------- .../twentemilieu/translations/nl.json | 2 +- .../ukraine_alarm/translations/bg.json | 11 --- .../ukraine_alarm/translations/ca.json | 13 ---- .../ukraine_alarm/translations/de.json | 13 ---- .../ukraine_alarm/translations/el.json | 13 ---- .../ukraine_alarm/translations/en.json | 13 ---- .../ukraine_alarm/translations/es.json | 10 --- .../ukraine_alarm/translations/et.json | 13 ---- .../ukraine_alarm/translations/fr.json | 13 ---- .../ukraine_alarm/translations/he.json | 27 +++++++ .../ukraine_alarm/translations/hu.json | 17 +--- .../ukraine_alarm/translations/id.json | 13 ---- .../ukraine_alarm/translations/it.json | 17 +--- .../ukraine_alarm/translations/ja.json | 17 +--- .../ukraine_alarm/translations/ko.json | 12 --- .../ukraine_alarm/translations/nl.json | 17 +--- .../ukraine_alarm/translations/no.json | 13 ---- .../ukraine_alarm/translations/pl.json | 13 ---- .../ukraine_alarm/translations/pt-BR.json | 13 ---- .../ukraine_alarm/translations/ru.json | 13 ---- .../ukraine_alarm/translations/tr.json | 13 ---- .../ukraine_alarm/translations/uk.json | 13 ---- .../ukraine_alarm/translations/zh-Hant.json | 13 ---- .../components/unifi/translations/ko.json | 3 + .../unifiprotect/translations/bg.json | 3 +- .../unifiprotect/translations/ca.json | 6 +- .../unifiprotect/translations/cs.json | 6 +- .../unifiprotect/translations/de.json | 6 +- .../unifiprotect/translations/el.json | 6 +- .../unifiprotect/translations/en.json | 6 +- .../unifiprotect/translations/es.json | 6 +- .../unifiprotect/translations/et.json | 6 +- .../unifiprotect/translations/fr.json | 6 +- .../unifiprotect/translations/he.json | 6 +- .../unifiprotect/translations/hu.json | 6 +- .../unifiprotect/translations/id.json | 6 +- .../unifiprotect/translations/it.json | 6 +- .../unifiprotect/translations/ja.json | 6 +- .../unifiprotect/translations/ko.json | 9 +++ .../unifiprotect/translations/nl.json | 6 +- .../unifiprotect/translations/no.json | 6 +- .../unifiprotect/translations/pl.json | 6 +- .../unifiprotect/translations/pt-BR.json | 6 +- .../unifiprotect/translations/ru.json | 6 +- .../unifiprotect/translations/tr.json | 6 +- .../unifiprotect/translations/zh-Hant.json | 6 +- .../components/upnp/translations/bg.json | 3 +- .../components/upnp/translations/ca.json | 4 +- .../components/upnp/translations/cs.json | 6 -- .../components/upnp/translations/de.json | 4 +- .../components/upnp/translations/el.json | 4 +- .../components/upnp/translations/en.json | 4 +- .../components/upnp/translations/es.json | 4 +- .../components/upnp/translations/et.json | 4 +- .../components/upnp/translations/fi.json | 8 -- .../components/upnp/translations/fr.json | 4 +- .../components/upnp/translations/he.json | 3 +- .../components/upnp/translations/hu.json | 4 +- .../components/upnp/translations/id.json | 4 +- .../components/upnp/translations/it.json | 4 +- .../components/upnp/translations/ja.json | 4 +- .../components/upnp/translations/ko.json | 6 -- .../components/upnp/translations/lb.json | 6 -- .../components/upnp/translations/nl.json | 4 +- .../components/upnp/translations/no.json | 4 +- .../components/upnp/translations/pl.json | 4 +- .../components/upnp/translations/pt-BR.json | 4 +- .../components/upnp/translations/pt.json | 5 -- .../components/upnp/translations/ru.json | 4 +- .../components/upnp/translations/sl.json | 5 -- .../components/upnp/translations/sv.json | 7 -- .../components/upnp/translations/tr.json | 4 +- .../components/upnp/translations/uk.json | 6 -- .../components/upnp/translations/zh-Hans.json | 3 +- .../components/upnp/translations/zh-Hant.json | 4 +- .../components/uptime/translations/ko.json | 9 +++ .../utility_meter/translations/ko.json | 35 +++++++++ .../components/vicare/translations/af.json | 1 - .../components/vicare/translations/bg.json | 5 +- .../components/vicare/translations/ca.json | 5 +- .../components/vicare/translations/cs.json | 1 - .../components/vicare/translations/de.json | 5 +- .../components/vicare/translations/el.json | 5 +- .../components/vicare/translations/en.json | 5 +- .../components/vicare/translations/es.json | 5 +- .../components/vicare/translations/et.json | 5 +- .../components/vicare/translations/fr.json | 5 +- .../components/vicare/translations/he.json | 4 +- .../components/vicare/translations/hu.json | 5 +- .../components/vicare/translations/id.json | 5 +- .../components/vicare/translations/it.json | 5 +- .../components/vicare/translations/ja.json | 5 +- .../components/vicare/translations/nl.json | 5 +- .../components/vicare/translations/no.json | 5 +- .../components/vicare/translations/pl.json | 5 +- .../components/vicare/translations/pt-BR.json | 5 +- .../components/vicare/translations/ru.json | 5 +- .../components/vicare/translations/sk.json | 1 - .../components/vicare/translations/tr.json | 5 +- .../vicare/translations/zh-Hant.json | 5 +- .../components/vilfo/translations/ca.json | 4 +- .../components/vilfo/translations/cs.json | 4 +- .../components/vilfo/translations/da.json | 4 +- .../components/vilfo/translations/de.json | 4 +- .../components/vilfo/translations/el.json | 4 +- .../components/vilfo/translations/en.json | 4 +- .../components/vilfo/translations/es-419.json | 4 +- .../components/vilfo/translations/es.json | 4 +- .../components/vilfo/translations/et.json | 4 +- .../components/vilfo/translations/fr.json | 4 +- .../components/vilfo/translations/he.json | 3 +- .../components/vilfo/translations/hu.json | 4 +- .../components/vilfo/translations/id.json | 4 +- .../components/vilfo/translations/it.json | 4 +- .../components/vilfo/translations/ja.json | 4 +- .../components/vilfo/translations/ko.json | 4 +- .../components/vilfo/translations/lb.json | 4 +- .../components/vilfo/translations/nl.json | 4 +- .../components/vilfo/translations/no.json | 4 +- .../components/vilfo/translations/pl.json | 4 +- .../components/vilfo/translations/pt-BR.json | 4 +- .../components/vilfo/translations/ru.json | 4 +- .../components/vilfo/translations/sl.json | 4 +- .../components/vilfo/translations/sv.json | 4 +- .../components/vilfo/translations/tr.json | 4 +- .../components/vilfo/translations/uk.json | 4 +- .../vilfo/translations/zh-Hans.json | 4 +- .../vilfo/translations/zh-Hant.json | 4 +- .../components/vulcan/translations/bg.json | 13 ---- .../components/vulcan/translations/ca.json | 23 ------ .../components/vulcan/translations/de.json | 23 ------ .../components/vulcan/translations/el.json | 23 ------ .../components/vulcan/translations/en.json | 23 ------ .../components/vulcan/translations/es.json | 21 ----- .../components/vulcan/translations/et.json | 23 ------ .../components/vulcan/translations/fr.json | 23 ------ .../components/vulcan/translations/hu.json | 23 ------ .../components/vulcan/translations/id.json | 23 ------ .../components/vulcan/translations/it.json | 23 ------ .../components/vulcan/translations/ja.json | 23 ------ .../components/vulcan/translations/nl.json | 23 ------ .../components/vulcan/translations/no.json | 23 ------ .../components/vulcan/translations/pl.json | 23 ------ .../components/vulcan/translations/pt-BR.json | 23 ------ .../components/vulcan/translations/ru.json | 23 ------ .../components/vulcan/translations/tr.json | 23 ------ .../vulcan/translations/zh-Hant.json | 23 ------ .../components/webostv/translations/ko.json | 47 +++++++++++ .../components/wilight/translations/ar.json | 3 +- .../components/wilight/translations/ca.json | 3 +- .../components/wilight/translations/cs.json | 3 +- .../components/wilight/translations/de.json | 3 +- .../components/wilight/translations/el.json | 3 +- .../components/wilight/translations/en.json | 3 +- .../components/wilight/translations/es.json | 3 +- .../components/wilight/translations/et.json | 3 +- .../components/wilight/translations/fr.json | 3 +- .../components/wilight/translations/he.json | 7 +- .../components/wilight/translations/hu.json | 3 +- .../components/wilight/translations/id.json | 3 +- .../components/wilight/translations/it.json | 3 +- .../components/wilight/translations/ja.json | 3 +- .../components/wilight/translations/ko.json | 3 +- .../components/wilight/translations/lb.json | 3 +- .../components/wilight/translations/nl.json | 3 +- .../components/wilight/translations/no.json | 3 +- .../components/wilight/translations/pl.json | 3 +- .../wilight/translations/pt-BR.json | 3 +- .../components/wilight/translations/pt.json | 3 +- .../components/wilight/translations/ru.json | 3 +- .../components/wilight/translations/tr.json | 3 +- .../components/wilight/translations/uk.json | 3 +- .../wilight/translations/zh-Hant.json | 3 +- .../components/wiz/translations/bg.json | 3 +- .../components/wiz/translations/ca.json | 6 +- .../components/wiz/translations/cs.json | 6 +- .../components/wiz/translations/de.json | 6 +- .../components/wiz/translations/el.json | 6 +- .../components/wiz/translations/en.json | 6 +- .../components/wiz/translations/es.json | 6 +- .../components/wiz/translations/et.json | 6 +- .../components/wiz/translations/fr.json | 6 +- .../components/wiz/translations/he.json | 6 +- .../components/wiz/translations/hu.json | 6 +- .../components/wiz/translations/id.json | 6 +- .../components/wiz/translations/it.json | 6 +- .../components/wiz/translations/ja.json | 6 +- .../components/wiz/translations/nl.json | 6 +- .../components/wiz/translations/no.json | 6 +- .../components/wiz/translations/pl.json | 6 +- .../components/wiz/translations/pt-BR.json | 6 +- .../components/wiz/translations/ru.json | 6 +- .../components/wiz/translations/sk.json | 7 -- .../components/wiz/translations/tr.json | 6 +- .../components/wiz/translations/zh-Hant.json | 6 +- .../components/ws66i/translations/he.json | 18 +++++ .../xiaomi_aqara/translations/ca.json | 6 +- .../xiaomi_aqara/translations/cs.json | 6 +- .../xiaomi_aqara/translations/de.json | 6 +- .../xiaomi_aqara/translations/el.json | 6 +- .../xiaomi_aqara/translations/en.json | 6 +- .../xiaomi_aqara/translations/es.json | 6 +- .../xiaomi_aqara/translations/et.json | 6 +- .../xiaomi_aqara/translations/fr.json | 6 +- .../xiaomi_aqara/translations/he.json | 6 +- .../xiaomi_aqara/translations/hu.json | 6 +- .../xiaomi_aqara/translations/id.json | 6 +- .../xiaomi_aqara/translations/it.json | 6 +- .../xiaomi_aqara/translations/ja.json | 6 +- .../xiaomi_aqara/translations/ko.json | 6 +- .../xiaomi_aqara/translations/lb.json | 6 +- .../xiaomi_aqara/translations/nl.json | 6 +- .../xiaomi_aqara/translations/no.json | 6 +- .../xiaomi_aqara/translations/pl.json | 6 +- .../xiaomi_aqara/translations/pt-BR.json | 6 +- .../xiaomi_aqara/translations/ru.json | 6 +- .../xiaomi_aqara/translations/tr.json | 6 +- .../xiaomi_aqara/translations/uk.json | 6 +- .../xiaomi_aqara/translations/zh-Hans.json | 6 +- .../xiaomi_aqara/translations/zh-Hant.json | 6 +- .../xiaomi_miio/translations/bg.json | 14 +--- .../xiaomi_miio/translations/ca.json | 44 ++--------- .../xiaomi_miio/translations/cs.json | 44 ++--------- .../xiaomi_miio/translations/de.json | 44 ++--------- .../xiaomi_miio/translations/el.json | 44 ++--------- .../xiaomi_miio/translations/en.json | 44 ++--------- .../xiaomi_miio/translations/es-419.json | 25 ------ .../xiaomi_miio/translations/es.json | 44 ++--------- .../xiaomi_miio/translations/et.json | 44 ++--------- .../xiaomi_miio/translations/fi.json | 24 ------ .../xiaomi_miio/translations/fr.json | 44 ++--------- .../xiaomi_miio/translations/he.json | 44 ++--------- .../xiaomi_miio/translations/hu.json | 44 ++--------- .../xiaomi_miio/translations/id.json | 44 ++--------- .../xiaomi_miio/translations/it.json | 44 ++--------- .../xiaomi_miio/translations/ja.json | 44 ++--------- .../xiaomi_miio/translations/ko.json | 31 +------- .../xiaomi_miio/translations/lb.json | 23 +----- .../xiaomi_miio/translations/nl.json | 44 ++--------- .../xiaomi_miio/translations/no.json | 44 ++--------- .../xiaomi_miio/translations/pl.json | 44 ++--------- .../xiaomi_miio/translations/pt-BR.json | 44 ++--------- .../xiaomi_miio/translations/pt.json | 14 ---- .../xiaomi_miio/translations/ru.json | 44 ++--------- .../xiaomi_miio/translations/sk.json | 10 --- .../xiaomi_miio/translations/sl.json | 19 ----- .../xiaomi_miio/translations/sv.json | 24 ------ .../xiaomi_miio/translations/tr.json | 44 ++--------- .../xiaomi_miio/translations/uk.json | 23 +----- .../xiaomi_miio/translations/zh-Hans.json | 41 +--------- .../xiaomi_miio/translations/zh-Hant.json | 44 ++--------- .../yale_smart_alarm/translations/nl.json | 4 +- .../yamaha_musiccast/translations/ko.json | 9 +++ .../components/yeelight/translations/ar.json | 3 +- .../components/yeelight/translations/bg.json | 3 +- .../components/yeelight/translations/ca.json | 3 +- .../components/yeelight/translations/cs.json | 3 +- .../components/yeelight/translations/de.json | 3 +- .../components/yeelight/translations/el.json | 3 +- .../components/yeelight/translations/en.json | 3 +- .../components/yeelight/translations/es.json | 3 +- .../components/yeelight/translations/et.json | 3 +- .../components/yeelight/translations/fr.json | 3 +- .../components/yeelight/translations/he.json | 3 +- .../components/yeelight/translations/hu.json | 3 +- .../components/yeelight/translations/id.json | 3 +- .../components/yeelight/translations/it.json | 3 +- .../components/yeelight/translations/ja.json | 3 +- .../components/yeelight/translations/ko.json | 3 +- .../components/yeelight/translations/lb.json | 3 +- .../components/yeelight/translations/nl.json | 3 +- .../components/yeelight/translations/no.json | 3 +- .../components/yeelight/translations/pl.json | 3 +- .../yeelight/translations/pt-BR.json | 3 +- .../components/yeelight/translations/pt.json | 3 +- .../components/yeelight/translations/ru.json | 3 +- .../components/yeelight/translations/tr.json | 3 +- .../components/yeelight/translations/uk.json | 3 +- .../yeelight/translations/zh-Hans.json | 3 +- .../yeelight/translations/zh-Hant.json | 3 +- .../components/yolink/translations/ca.json | 25 ++++++ .../components/yolink/translations/de.json | 25 ++++++ .../components/yolink/translations/he.json | 24 ++++++ .../components/yolink/translations/id.json | 1 + .../components/yolink/translations/ko.json | 2 + .../components/yolink/translations/no.json | 25 ++++++ .../components/zwave_js/translations/bg.json | 1 - .../components/zwave_js/translations/ca.json | 5 +- .../components/zwave_js/translations/cs.json | 3 +- .../components/zwave_js/translations/de.json | 5 +- .../components/zwave_js/translations/el.json | 5 +- .../components/zwave_js/translations/en.json | 5 +- .../components/zwave_js/translations/es.json | 5 +- .../components/zwave_js/translations/et.json | 5 +- .../components/zwave_js/translations/fr.json | 5 +- .../components/zwave_js/translations/he.json | 4 +- .../components/zwave_js/translations/hu.json | 5 +- .../components/zwave_js/translations/id.json | 5 +- .../components/zwave_js/translations/it.json | 5 +- .../components/zwave_js/translations/ja.json | 5 +- .../components/zwave_js/translations/ko.json | 7 +- .../components/zwave_js/translations/lb.json | 3 +- .../components/zwave_js/translations/nl.json | 5 +- .../components/zwave_js/translations/no.json | 5 +- .../components/zwave_js/translations/pl.json | 5 +- .../zwave_js/translations/pt-BR.json | 5 +- .../components/zwave_js/translations/ru.json | 5 +- .../components/zwave_js/translations/tr.json | 5 +- .../components/zwave_js/translations/uk.json | 3 +- .../zwave_js/translations/zh-Hant.json | 5 +- 2813 files changed, 3881 insertions(+), 16900 deletions(-) delete mode 100644 homeassistant/components/airly/translations/nn.json create mode 100644 homeassistant/components/aladdin_connect/translations/cs.json create mode 100644 homeassistant/components/aladdin_connect/translations/he.json create mode 100644 homeassistant/components/application_credentials/translations/ko.json delete mode 100644 homeassistant/components/aurora_abb_powerone/translations/cs.json create mode 100644 homeassistant/components/azure_event_hub/translations/ko.json create mode 100644 homeassistant/components/baf/translations/cs.json create mode 100644 homeassistant/components/baf/translations/he.json delete mode 100644 homeassistant/components/climacell/translations/bg.json delete mode 100644 homeassistant/components/climacell/translations/cs.json delete mode 100644 homeassistant/components/climacell/translations/lb.json delete mode 100644 homeassistant/components/climacell/translations/pt.json delete mode 100644 homeassistant/components/climacell/translations/sk.json delete mode 100644 homeassistant/components/climacell/translations/sv.json delete mode 100644 homeassistant/components/climacell/translations/zh-Hans.json create mode 100644 homeassistant/components/cpuspeed/translations/ko.json delete mode 100644 homeassistant/components/derivative/translations/he.json create mode 100644 homeassistant/components/derivative/translations/ko.json create mode 100644 homeassistant/components/devolo_home_network/translations/ko.json create mode 100644 homeassistant/components/dlna_dmr/translations/ko.json create mode 100644 homeassistant/components/dlna_dms/translations/ko.json create mode 100644 homeassistant/components/generic/translations/ko.json create mode 100644 homeassistant/components/geocaching/translations/he.json create mode 100644 homeassistant/components/google/translations/ko.json create mode 100644 homeassistant/components/integration/translations/ko.json create mode 100644 homeassistant/components/intellifire/translations/ko.json delete mode 100644 homeassistant/components/iqvia/translations/nn.json delete mode 100644 homeassistant/components/iss/translations/zh-Hans.json create mode 100644 homeassistant/components/kraken/translations/ko.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.cs.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.he.json delete mode 100644 homeassistant/components/mill/translations/fi.json delete mode 100644 homeassistant/components/mill/translations/sv.json create mode 100644 homeassistant/components/min_max/translations/ko.json create mode 100644 homeassistant/components/moon/translations/ko.json delete mode 100644 homeassistant/components/motion_blinds/translations/sl.json delete mode 100644 homeassistant/components/netgear/translations/sk.json delete mode 100644 homeassistant/components/onewire/translations/af.json delete mode 100644 homeassistant/components/poolsense/translations/sv.json create mode 100644 homeassistant/components/sabnzbd/translations/ko.json delete mode 100644 homeassistant/components/sentry/translations/af.json create mode 100644 homeassistant/components/senz/translations/ko.json create mode 100644 homeassistant/components/siren/translations/cs.json create mode 100644 homeassistant/components/siren/translations/no.json create mode 100644 homeassistant/components/slack/translations/cs.json rename homeassistant/components/{climacell => slack}/translations/he.json (52%) create mode 100644 homeassistant/components/sql/translations/ko.json create mode 100644 homeassistant/components/steam_online/translations/ko.json delete mode 100644 homeassistant/components/switch_as_x/translations/he.json create mode 100644 homeassistant/components/switch_as_x/translations/ko.json create mode 100644 homeassistant/components/switcher_kis/translations/ko.json create mode 100644 homeassistant/components/tautulli/translations/ko.json create mode 100644 homeassistant/components/threshold/translations/ko.json delete mode 100644 homeassistant/components/tibber/translations/fi.json create mode 100644 homeassistant/components/tod/translations/ko.json create mode 100644 homeassistant/components/tolo/translations/ko.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/ko.json delete mode 100644 homeassistant/components/trafikverket_weatherstation/translations/lt.json delete mode 100644 homeassistant/components/tuya/translations/af.json delete mode 100644 homeassistant/components/tuya/translations/en_GB.json delete mode 100644 homeassistant/components/tuya/translations/ka.json delete mode 100644 homeassistant/components/tuya/translations/sl.json create mode 100644 homeassistant/components/ukraine_alarm/translations/he.json create mode 100644 homeassistant/components/unifiprotect/translations/ko.json create mode 100644 homeassistant/components/uptime/translations/ko.json create mode 100644 homeassistant/components/utility_meter/translations/ko.json create mode 100644 homeassistant/components/webostv/translations/ko.json create mode 100644 homeassistant/components/ws66i/translations/he.json delete mode 100644 homeassistant/components/xiaomi_miio/translations/es-419.json delete mode 100644 homeassistant/components/xiaomi_miio/translations/fi.json delete mode 100644 homeassistant/components/xiaomi_miio/translations/sv.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/ko.json create mode 100644 homeassistant/components/yolink/translations/ca.json create mode 100644 homeassistant/components/yolink/translations/de.json create mode 100644 homeassistant/components/yolink/translations/he.json create mode 100644 homeassistant/components/yolink/translations/no.json diff --git a/homeassistant/components/accuweather/translations/ar.json b/homeassistant/components/accuweather/translations/ar.json index 0694e096019..5aadd35df92 100644 --- a/homeassistant/components/accuweather/translations/ar.json +++ b/homeassistant/components/accuweather/translations/ar.json @@ -2,12 +2,6 @@ "config": { "error": { "requests_exceeded": "\u062a\u0645 \u062a\u062c\u0627\u0648\u0632 \u0627\u0644\u0639\u062f\u062f \u0627\u0644\u0645\u0633\u0645\u0648\u062d \u0628\u0647 \u0645\u0646 \u0627\u0644\u0637\u0644\u0628\u0627\u062a \u0625\u0644\u0649 Accuweather API. \u0639\u0644\u064a\u0643 \u0627\u0644\u0627\u0646\u062a\u0638\u0627\u0631 \u0623\u0648 \u062a\u063a\u064a\u064a\u0631 \u0645\u0641\u062a\u0627\u062d API." - }, - "step": { - "user": { - "description": "\u0625\u0630\u0627 \u0643\u0646\u062a \u0628\u062d\u0627\u062c\u0629 \u0625\u0644\u0649 \u0645\u0633\u0627\u0639\u062f\u0629 \u0641\u064a \u0627\u0644\u062a\u0643\u0648\u064a\u0646 \u060c \u0641\u0642\u0645 \u0628\u0625\u0644\u0642\u0627\u0621 \u0646\u0638\u0631\u0629 \u0647\u0646\u0627: https://www.home-assistant.io/integrations/accuweather/ \n\n \u0644\u0627 \u064a\u062a\u0645 \u062a\u0645\u0643\u064a\u0646 \u0628\u0639\u0636 \u0623\u062c\u0647\u0632\u0629 \u0627\u0644\u0627\u0633\u062a\u0634\u0639\u0627\u0631 \u0628\u0634\u0643\u0644 \u0627\u0641\u062a\u0631\u0627\u0636\u064a. \u064a\u0645\u0643\u0646\u0643 \u062a\u0645\u0643\u064a\u0646\u0647\u0645 \u0641\u064a \u0633\u062c\u0644 \u0627\u0644\u0643\u064a\u0627\u0646 \u0628\u0639\u062f \u062a\u0643\u0648\u064a\u0646 \u0627\u0644\u062a\u0643\u0627\u0645\u0644.\n \u0644\u0627 \u064a\u062a\u0645 \u062a\u0645\u0643\u064a\u0646 \u062a\u0648\u0642\u0639\u0627\u062a \u0627\u0644\u0637\u0642\u0633 \u0627\u0641\u062a\u0631\u0627\u0636\u064a\u064b\u0627. \u064a\u0645\u0643\u0646\u0643 \u062a\u0645\u0643\u064a\u0646\u0647 \u0641\u064a \u062e\u064a\u0627\u0631\u0627\u062a \u0627\u0644\u062a\u0643\u0627\u0645\u0644.", - "title": "AccuWeather" - } } }, "options": { @@ -16,8 +10,7 @@ "data": { "forecast": "\u0627\u0644\u0646\u0634\u0631\u0629 \u0627\u0644\u062c\u0648\u064a\u0629" }, - "description": "\u0646\u0638\u0631\u064b\u0627 \u0644\u0642\u064a\u0648\u062f \u0627\u0644\u0625\u0635\u062f\u0627\u0631 \u0627\u0644\u0645\u062c\u0627\u0646\u064a \u0645\u0646 \u0645\u0641\u062a\u0627\u062d AccuWeather API \u060c \u0639\u0646\u062f \u062a\u0645\u0643\u064a\u0646 \u0627\u0644\u062a\u0646\u0628\u0624 \u0628\u0627\u0644\u0637\u0642\u0633 \u060c \u0633\u064a\u062a\u0645 \u0625\u062c\u0631\u0627\u0621 \u062a\u062d\u062f\u064a\u062b\u0627\u062a \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a \u0643\u0644 80 \u062f\u0642\u064a\u0642\u0629 \u0628\u062f\u0644\u0627\u064b \u0645\u0646 \u0643\u0644 40 \u062f\u0642\u064a\u0642\u0629.", - "title": "\u062e\u064a\u0627\u0631\u0627\u062a AccuWeather" + "description": "\u0646\u0638\u0631\u064b\u0627 \u0644\u0642\u064a\u0648\u062f \u0627\u0644\u0625\u0635\u062f\u0627\u0631 \u0627\u0644\u0645\u062c\u0627\u0646\u064a \u0645\u0646 \u0645\u0641\u062a\u0627\u062d AccuWeather API \u060c \u0639\u0646\u062f \u062a\u0645\u0643\u064a\u0646 \u0627\u0644\u062a\u0646\u0628\u0624 \u0628\u0627\u0644\u0637\u0642\u0633 \u060c \u0633\u064a\u062a\u0645 \u0625\u062c\u0631\u0627\u0621 \u062a\u062d\u062f\u064a\u062b\u0627\u062a \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a \u0643\u0644 80 \u062f\u0642\u064a\u0642\u0629 \u0628\u062f\u0644\u0627\u064b \u0645\u0646 \u0643\u0644 40 \u062f\u0642\u064a\u0642\u0629." } } }, diff --git a/homeassistant/components/accuweather/translations/bg.json b/homeassistant/components/accuweather/translations/bg.json index b037c01144f..26fdf8e85d5 100644 --- a/homeassistant/components/accuweather/translations/bg.json +++ b/homeassistant/components/accuweather/translations/bg.json @@ -14,8 +14,7 @@ "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" - }, - "title": "AccuWeather" + } } } } diff --git a/homeassistant/components/accuweather/translations/ca.json b/homeassistant/components/accuweather/translations/ca.json index feda485d8fd..e80a006141a 100644 --- a/homeassistant/components/accuweather/translations/ca.json +++ b/homeassistant/components/accuweather/translations/ca.json @@ -18,9 +18,7 @@ "latitude": "Latitud", "longitude": "Longitud", "name": "Nom" - }, - "description": "Si necessites ajuda amb la configuraci\u00f3, consulta els seg\u00fcent enlla\u00e7: https://www.home-assistant.io/integrations/accuweather/ \n\n Alguns sensors no estan activats de manera predeterminada. Els pots activar des del registre d'entitats, despr\u00e9s de la configurraci\u00f3 de la integraci\u00f3.\n La previsi\u00f3 meteorol\u00f2gica no est\u00e0 activada de manera predeterminada. Pots activar-la en les opcions de la integraci\u00f3.", - "title": "AccuWeather" + } } } }, @@ -30,8 +28,7 @@ "data": { "forecast": "Previsi\u00f3 meteorol\u00f2gica" }, - "description": "Per culpa de les limitacions de la versi\u00f3 gratu\u00efta l'API d'AccuWeather, quan habilitis la previsi\u00f3 meteorol\u00f2gica, les actualitzacions de dades es faran cada 80 minuts en comptes de cada 40.", - "title": "Opcions d'AccuWeather" + "description": "Per culpa de les limitacions de la versi\u00f3 gratu\u00efta l'API d'AccuWeather, quan habilitis la previsi\u00f3 meteorol\u00f2gica, les actualitzacions de dades es faran cada 80 minuts en comptes de cada 40." } } }, diff --git a/homeassistant/components/accuweather/translations/cs.json b/homeassistant/components/accuweather/translations/cs.json index 1cf34a42695..e3ae982cddc 100644 --- a/homeassistant/components/accuweather/translations/cs.json +++ b/homeassistant/components/accuweather/translations/cs.json @@ -15,9 +15,7 @@ "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", "name": "Jm\u00e9no" - }, - "description": "Pokud pot\u0159ebujete pomoc s nastaven\u00ed, pod\u00edvejte se na: https://www.home-assistant.io/integrations/accuweather/\n\nN\u011bkter\u00e9 senzory nejsou ve v\u00fdchoz\u00edm nastaven\u00ed povoleny. M\u016f\u017eete je povolit po nastaven\u00ed integrace v registru entit.\nP\u0159edpov\u011b\u010f po\u010das\u00ed nen\u00ed ve v\u00fdchoz\u00edm nastaven\u00ed povolena. M\u016f\u017eete ji povolit v mo\u017enostech integrace.", - "title": "AccuWeather" + } } } }, @@ -27,8 +25,7 @@ "data": { "forecast": "P\u0159edpov\u011b\u010f po\u010das\u00ed" }, - "description": "Kdy\u017e povol\u00edte p\u0159edpov\u011b\u010f po\u010das\u00ed, budou aktualizace dat prov\u00e1d\u011bny ka\u017ed\u00fdch 80 minut nam\u00edsto 40 minut z d\u016fvodu omezen\u00ed bezplatn\u00e9 verze AccuWeather.", - "title": "Mo\u017enosti AccuWeather" + "description": "Kdy\u017e povol\u00edte p\u0159edpov\u011b\u010f po\u010das\u00ed, budou aktualizace dat prov\u00e1d\u011bny ka\u017ed\u00fdch 80 minut nam\u00edsto 40 minut z d\u016fvodu omezen\u00ed bezplatn\u00e9 verze AccuWeather." } } }, diff --git a/homeassistant/components/accuweather/translations/de.json b/homeassistant/components/accuweather/translations/de.json index ac0c430de04..f7b02feb091 100644 --- a/homeassistant/components/accuweather/translations/de.json +++ b/homeassistant/components/accuweather/translations/de.json @@ -18,9 +18,7 @@ "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad", "name": "Name" - }, - "description": "Wenn du Hilfe bei der Konfiguration ben\u00f6tigst, schaue hier nach: https://www.home-assistant.io/integrations/accuweather/\n\nEinige Sensoren sind standardm\u00e4\u00dfig nicht aktiviert. Du kannst sie in der Entit\u00e4tsregister nach der Integrationskonfiguration aktivieren.\nDie Wettervorhersage ist nicht standardm\u00e4\u00dfig aktiviert. Du kannst sie in den Integrationsoptionen aktivieren.", - "title": "AccuWeather" + } } } }, @@ -30,8 +28,7 @@ "data": { "forecast": "Wettervorhersage" }, - "description": "Aufgrund der Einschr\u00e4nkungen der kostenlosen Version des AccuWeather-API-Schl\u00fcssels werden bei aktivierter Wettervorhersage Datenaktualisierungen alle 80 Minuten statt alle 40 Minuten durchgef\u00fchrt.", - "title": "AccuWeather Optionen" + "description": "Aufgrund der Einschr\u00e4nkungen der kostenlosen Version des AccuWeather-API-Schl\u00fcssels werden bei aktivierter Wettervorhersage Datenaktualisierungen alle 80 Minuten statt alle 40 Minuten durchgef\u00fchrt." } } }, diff --git a/homeassistant/components/accuweather/translations/el.json b/homeassistant/components/accuweather/translations/el.json index 43a65158455..b8d7d22df70 100644 --- a/homeassistant/components/accuweather/translations/el.json +++ b/homeassistant/components/accuweather/translations/el.json @@ -18,9 +18,7 @@ "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", "name": "\u038c\u03bd\u03bf\u03bc\u03b1" - }, - "description": "\u0391\u03bd \u03c7\u03c1\u03b5\u03b9\u03ac\u03b6\u03b5\u03c3\u03c4\u03b5 \u03b2\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1 \u03bc\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7, \u03c1\u03af\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03bc\u03b1\u03c4\u03b9\u03ac \u03b5\u03b4\u03ce: https://www.home-assistant.io/integrations/accuweather/\n\n\u039f\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03b9 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03b9 \u03b1\u03c0\u03cc \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf\u03c5\u03c2 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03bf \u03bc\u03b7\u03c4\u03c1\u03ce\u03bf \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03bc\u03b5\u03c4\u03ac \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2.\n\u0397 \u03c0\u03c1\u03cc\u03b3\u03bd\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b1\u03c0\u03cc \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03b7\u03bd \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2.", - "title": "AccuWeather" + } } } }, @@ -30,8 +28,7 @@ "data": { "forecast": "\u03a0\u03c1\u03cc\u03b3\u03bd\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd" }, - "description": "\u039b\u03cc\u03b3\u03c9 \u03c4\u03c9\u03bd \u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ce\u03bd \u03c4\u03b7\u03c2 \u03b4\u03c9\u03c1\u03b5\u03ac\u03bd \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd API \u03c4\u03bf\u03c5 AccuWeather, \u03cc\u03c4\u03b1\u03bd \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03c1\u03cc\u03b3\u03bd\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd, \u03bf\u03b9 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03b8\u03b1 \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03ba\u03ac\u03b8\u03b5 80 \u03bb\u03b5\u03c0\u03c4\u03ac \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 40 \u03bb\u03b5\u03c0\u03c4\u03ac.", - "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 AccuWeather" + "description": "\u039b\u03cc\u03b3\u03c9 \u03c4\u03c9\u03bd \u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ce\u03bd \u03c4\u03b7\u03c2 \u03b4\u03c9\u03c1\u03b5\u03ac\u03bd \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd API \u03c4\u03bf\u03c5 AccuWeather, \u03cc\u03c4\u03b1\u03bd \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03c1\u03cc\u03b3\u03bd\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9\u03c1\u03bf\u03cd, \u03bf\u03b9 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03b8\u03b1 \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03ba\u03ac\u03b8\u03b5 80 \u03bb\u03b5\u03c0\u03c4\u03ac \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 40 \u03bb\u03b5\u03c0\u03c4\u03ac." } } }, diff --git a/homeassistant/components/accuweather/translations/en.json b/homeassistant/components/accuweather/translations/en.json index a984080fd86..d391ce83e1e 100644 --- a/homeassistant/components/accuweather/translations/en.json +++ b/homeassistant/components/accuweather/translations/en.json @@ -18,9 +18,7 @@ "latitude": "Latitude", "longitude": "Longitude", "name": "Name" - }, - "description": "If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/accuweather/\n\nSome sensors are not enabled by default. You can enable them in the entity registry after the integration configuration.\nWeather forecast is not enabled by default. You can enable it in the integration options.", - "title": "AccuWeather" + } } } }, @@ -30,8 +28,7 @@ "data": { "forecast": "Weather forecast" }, - "description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 80 minutes instead of every 40 minutes.", - "title": "AccuWeather Options" + "description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 80 minutes instead of every 40 minutes." } } }, diff --git a/homeassistant/components/accuweather/translations/es-419.json b/homeassistant/components/accuweather/translations/es-419.json index 72d295da073..2b2b8dc98ce 100644 --- a/homeassistant/components/accuweather/translations/es-419.json +++ b/homeassistant/components/accuweather/translations/es-419.json @@ -7,12 +7,6 @@ "cannot_connect": "No se pudo conectar", "invalid_api_key": "Clave de API no v\u00e1lida", "requests_exceeded": "Se super\u00f3 el n\u00famero permitido de solicitudes a la API de Accuweather. Tiene que esperar o cambiar la clave de API." - }, - "step": { - "user": { - "description": "Si necesita ayuda con la configuraci\u00f3n, eche un vistazo aqu\u00ed: https://www.home-assistant.io/integrations/accuweather/ \n\nAlgunos sensores no est\u00e1n habilitados de forma predeterminada. Puede habilitarlos en el registro de entidades despu\u00e9s de la configuraci\u00f3n de integraci\u00f3n. La previsi\u00f3n meteorol\u00f3gica no est\u00e1 habilitada de forma predeterminada. Puede habilitarlo en las opciones de integraci\u00f3n.", - "title": "AccuWeather" - } } }, "options": { @@ -21,8 +15,7 @@ "data": { "forecast": "Pron\u00f3stico del tiempo" }, - "description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilita el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 64 minutos en lugar de cada 32 minutos.", - "title": "Opciones de AccuWeather" + "description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilita el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 64 minutos en lugar de cada 32 minutos." } } }, diff --git a/homeassistant/components/accuweather/translations/es.json b/homeassistant/components/accuweather/translations/es.json index 9d0396fddb3..0c3d5560d67 100644 --- a/homeassistant/components/accuweather/translations/es.json +++ b/homeassistant/components/accuweather/translations/es.json @@ -15,9 +15,7 @@ "latitude": "Latitud", "longitude": "Longitud", "name": "Nombre" - }, - "description": "Si necesitas ayuda con la configuraci\u00f3n, echa un vistazo aqu\u00ed: https://www.home-assistant.io/integrations/accuweather/ \n\nAlgunos sensores no est\u00e1n habilitados por defecto. Los puedes habilitar en el registro de entidades despu\u00e9s de configurar la integraci\u00f3n.\nEl pron\u00f3stico del tiempo no est\u00e1 habilitado por defecto. Puedes habilitarlo en las opciones de la integraci\u00f3n.", - "title": "AccuWeather" + } } } }, @@ -27,8 +25,7 @@ "data": { "forecast": "Pron\u00f3stico del tiempo" }, - "description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilitas el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 64 minutos en lugar de cada 32 minutos.", - "title": "Opciones de AccuWeather" + "description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilitas el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 64 minutos en lugar de cada 32 minutos." } } }, diff --git a/homeassistant/components/accuweather/translations/et.json b/homeassistant/components/accuweather/translations/et.json index 429e2dec61e..2f85bd8663e 100644 --- a/homeassistant/components/accuweather/translations/et.json +++ b/homeassistant/components/accuweather/translations/et.json @@ -18,9 +18,7 @@ "latitude": "Laiuskraad", "longitude": "Pikkuskraad", "name": "Sidumise nimi" - }, - "description": "Kui vajate seadistamisel abi vaadake siit: https://www.home-assistant.io/integrations/accuweather/ \n\n M\u00f5ni andur pole vaikimisi lubatud. P\u00e4rast sidumise seadistamist saate need \u00fcksused lubada. \n Ilmapennustus pole vaikimisi lubatud. Saate selle lubada sidumise s\u00e4tetes.", - "title": "AccuWeather" + } } } }, @@ -30,8 +28,7 @@ "data": { "forecast": "Ilmateade" }, - "description": "AccuWeather API tasuta versioonis toimub ilmaennustuse lubamisel andmete v\u00e4rskendamine iga 80 minuti j\u00e4rel (muidu 40 minutit).", - "title": "AccuWeatheri valikud" + "description": "AccuWeather API tasuta versioonis toimub ilmaennustuse lubamisel andmete v\u00e4rskendamine iga 80 minuti j\u00e4rel (muidu 40 minutit)." } } }, diff --git a/homeassistant/components/accuweather/translations/fr.json b/homeassistant/components/accuweather/translations/fr.json index cf407ef3962..75ef209a1cf 100644 --- a/homeassistant/components/accuweather/translations/fr.json +++ b/homeassistant/components/accuweather/translations/fr.json @@ -18,9 +18,7 @@ "latitude": "Latitude", "longitude": "Longitude", "name": "Nom" - }, - "description": "Si vous avez besoin d'aide pour la configuration, consultez\u00a0: https://www.home-assistant.io/integrations/accuweather/\n\nCertains capteurs ne sont pas activ\u00e9s par d\u00e9faut. Vous pouvez les activer dans le registre des entit\u00e9s une fois la configuration de l'int\u00e9gration termin\u00e9e.\nLes pr\u00e9visions m\u00e9t\u00e9orologiques ne sont pas activ\u00e9es par d\u00e9faut. Vous pouvez les activer dans les options de l'int\u00e9gration.", - "title": "AccuWeather" + } } } }, @@ -30,8 +28,7 @@ "data": { "forecast": "Pr\u00e9visions m\u00e9t\u00e9orologiques" }, - "description": "En raison des limitations de la version gratuite de la cl\u00e9 API AccuWeather, lorsque vous activez les pr\u00e9visions m\u00e9t\u00e9orologiques, les mises \u00e0 jour des donn\u00e9es seront effectu\u00e9es toutes les 64 minutes au lieu de toutes les 32 minutes.", - "title": "Options AccuWeather" + "description": "En raison des limitations de la version gratuite de la cl\u00e9 API AccuWeather, lorsque vous activez les pr\u00e9visions m\u00e9t\u00e9orologiques, les mises \u00e0 jour des donn\u00e9es seront effectu\u00e9es toutes les 64 minutes au lieu de toutes les 32 minutes." } } }, diff --git a/homeassistant/components/accuweather/translations/he.json b/homeassistant/components/accuweather/translations/he.json index 77c1e54f3e5..0f054ff11fe 100644 --- a/homeassistant/components/accuweather/translations/he.json +++ b/homeassistant/components/accuweather/translations/he.json @@ -15,9 +15,7 @@ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", "name": "\u05e9\u05dd" - }, - "description": "\u05d0\u05dd \u05d4\u05d9\u05e0\u05da \u05d6\u05e7\u05d5\u05e7 \u05dc\u05e2\u05d6\u05e8\u05d4 \u05e2\u05dd \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4, \u05d9\u05e9 \u05dc\u05e2\u05d9\u05d9\u05df \u05db\u05d0\u05df: https://www.home-assistant.io/integrations/accuweather/\n\n\u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05de\u05e1\u05d5\u05d9\u05de\u05d9\u05dd \u05d0\u05d9\u05e0\u05dd \u05d6\u05de\u05d9\u05e0\u05d9\u05dd \u05db\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc. \u05d1\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea\u05da \u05dc\u05d4\u05e4\u05d5\u05da \u05d0\u05d5\u05ea\u05dd \u05dc\u05d6\u05de\u05d9\u05e0\u05d9\u05dd \u05d1\u05e8\u05d9\u05e9\u05d5\u05dd \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05dc\u05d0\u05d7\u05e8 \u05e7\u05d1\u05d9\u05e2\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1.\n\u05ea\u05d7\u05d6\u05d9\u05ea \u05de\u05d6\u05d2 \u05d4\u05d0\u05d5\u05d5\u05d9\u05e8 \u05d0\u05d9\u05e0\u05d4 \u05d6\u05de\u05d9\u05e0\u05d4 \u05db\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc. \u05d1\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea\u05da \u05dc\u05d4\u05e4\u05d5\u05da \u05d0\u05d5\u05ea\u05d5 \u05dc\u05d6\u05de\u05d9\u05df \u05d1\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1.", - "title": "AccuWeather" + } } } }, @@ -27,8 +25,7 @@ "data": { "forecast": "\u05ea\u05d7\u05d6\u05d9\u05ea \u05de\u05d6\u05d2 \u05d4\u05d0\u05d5\u05d5\u05d9\u05e8" }, - "description": "\u05d1\u05e9\u05dc \u05de\u05d2\u05d1\u05dc\u05d5\u05ea \u05d4\u05d2\u05d9\u05e8\u05e1\u05d4 \u05d4\u05d7\u05d9\u05e0\u05de\u05d9\u05ea \u05e9\u05dc \u05de\u05e4\u05ea\u05d7 \u05d4-API \u05e9\u05dc AccuWeather, \u05db\u05d0\u05e9\u05e8 \u05ea\u05e4\u05e2\u05d9\u05dc \u05ea\u05d7\u05d6\u05d9\u05ea \u05de\u05d6\u05d2 \u05d0\u05d5\u05d5\u05d9\u05e8, \u05e2\u05d3\u05db\u05d5\u05e0\u05d9 \u05e0\u05ea\u05d5\u05e0\u05d9\u05dd \u05d9\u05d1\u05d5\u05e6\u05e2\u05d5 \u05db\u05dc 80 \u05d3\u05e7\u05d5\u05ea \u05d1\u05de\u05e7\u05d5\u05dd \u05db\u05dc 40 \u05d3\u05e7\u05d5\u05ea.", - "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea AccuWeather" + "description": "\u05d1\u05e9\u05dc \u05de\u05d2\u05d1\u05dc\u05d5\u05ea \u05d4\u05d2\u05d9\u05e8\u05e1\u05d4 \u05d4\u05d7\u05d9\u05e0\u05de\u05d9\u05ea \u05e9\u05dc \u05de\u05e4\u05ea\u05d7 \u05d4-API \u05e9\u05dc AccuWeather, \u05db\u05d0\u05e9\u05e8 \u05ea\u05e4\u05e2\u05d9\u05dc \u05ea\u05d7\u05d6\u05d9\u05ea \u05de\u05d6\u05d2 \u05d0\u05d5\u05d5\u05d9\u05e8, \u05e2\u05d3\u05db\u05d5\u05e0\u05d9 \u05e0\u05ea\u05d5\u05e0\u05d9\u05dd \u05d9\u05d1\u05d5\u05e6\u05e2\u05d5 \u05db\u05dc 80 \u05d3\u05e7\u05d5\u05ea \u05d1\u05de\u05e7\u05d5\u05dd \u05db\u05dc 40 \u05d3\u05e7\u05d5\u05ea." } } }, diff --git a/homeassistant/components/accuweather/translations/hu.json b/homeassistant/components/accuweather/translations/hu.json index 1b30dd09daf..0e207c5fde8 100644 --- a/homeassistant/components/accuweather/translations/hu.json +++ b/homeassistant/components/accuweather/translations/hu.json @@ -18,9 +18,7 @@ "latitude": "Sz\u00e9less\u00e9g", "longitude": "Hossz\u00fas\u00e1g", "name": "Elnevez\u00e9s" - }, - "description": "Ha seg\u00edts\u00e9gre van sz\u00fcks\u00e9ge a konfigur\u00e1l\u00e1shoz, n\u00e9zze meg itt: https://www.home-assistant.io/integrations/accuweather/ \n\nEgyes \u00e9rz\u00e9kel\u0151k alap\u00e9rtelmez\u00e9s szerint nincsenek enged\u00e9lyezve. Az integr\u00e1ci\u00f3s konfigur\u00e1ci\u00f3 ut\u00e1n enged\u00e9lyezheti \u0151ket az entit\u00e1s-nyilv\u00e1ntart\u00e1sban.\nAz id\u0151j\u00e1r\u00e1s-el\u0151rejelz\u00e9s alap\u00e9rtelmez\u00e9s szerint nincs enged\u00e9lyezve. Ezt az integr\u00e1ci\u00f3s be\u00e1ll\u00edt\u00e1sokban enged\u00e9lyezheti.", - "title": "AccuWeather" + } } } }, @@ -30,8 +28,7 @@ "data": { "forecast": "Id\u0151j\u00e1r\u00e1s el\u0151rejelz\u00e9s" }, - "description": "Az AccuWeather API kulcs ingyenes verzi\u00f3j\u00e1nak korl\u00e1tai miatt, amikor enged\u00e9lyezi az id\u0151j\u00e1r\u00e1s -el\u0151rejelz\u00e9st, az adatfriss\u00edt\u00e9seket 40 percenk\u00e9nt 80 percenk\u00e9nt hajtj\u00e1k v\u00e9gre.", - "title": "AccuWeather be\u00e1ll\u00edt\u00e1sok" + "description": "Az AccuWeather API kulcs ingyenes verzi\u00f3j\u00e1nak korl\u00e1tai miatt, amikor enged\u00e9lyezi az id\u0151j\u00e1r\u00e1s -el\u0151rejelz\u00e9st, az adatfriss\u00edt\u00e9seket 40 percenk\u00e9nt 80 percenk\u00e9nt hajtj\u00e1k v\u00e9gre." } } }, diff --git a/homeassistant/components/accuweather/translations/id.json b/homeassistant/components/accuweather/translations/id.json index 7bf9f27e8b2..1cd49752342 100644 --- a/homeassistant/components/accuweather/translations/id.json +++ b/homeassistant/components/accuweather/translations/id.json @@ -18,9 +18,7 @@ "latitude": "Lintang", "longitude": "Bujur", "name": "Nama" - }, - "description": "Jika Anda memerlukan bantuan tentang konfigurasi, baca di sini: https://www.home-assistant.io/integrations/accuweather/\n\nBeberapa sensor tidak diaktifkan secara default. Anda dapat mengaktifkannya di registri entitas setelah konfigurasi integrasi.\nPrakiraan cuaca tidak diaktifkan secara default. Anda dapat mengaktifkannya di opsi integrasi.", - "title": "AccuWeather" + } } } }, @@ -30,8 +28,7 @@ "data": { "forecast": "Prakiraan cuaca" }, - "description": "Karena keterbatasan versi gratis kunci API AccuWeather, ketika Anda mengaktifkan prakiraan cuaca, pembaruan data akan dilakukan setiap 80 menit, bukan setiap 40 menit.", - "title": "Opsi AccuWeather" + "description": "Karena keterbatasan versi gratis kunci API AccuWeather, ketika Anda mengaktifkan prakiraan cuaca, pembaruan data akan dilakukan setiap 80 menit, bukan setiap 40 menit." } } }, diff --git a/homeassistant/components/accuweather/translations/it.json b/homeassistant/components/accuweather/translations/it.json index 9daaf378fb4..d07a900adc9 100644 --- a/homeassistant/components/accuweather/translations/it.json +++ b/homeassistant/components/accuweather/translations/it.json @@ -18,9 +18,7 @@ "latitude": "Latitudine", "longitude": "Logitudine", "name": "Nome" - }, - "description": "Se hai bisogno di aiuto con la configurazione dai un'occhiata qui: https://www.home-assistant.io/integrations/accuweather/ \n\nAlcuni sensori non sono abilitati per impostazione predefinita. \u00c8 possibile abilitarli nel registro entit\u00e0 dopo la configurazione di integrazione. \nLe previsioni meteo non sono abilitate per impostazione predefinita. Puoi abilitarle nelle opzioni di integrazione.", - "title": "AccuWeather" + } } } }, @@ -30,8 +28,7 @@ "data": { "forecast": "Previsioni meteo" }, - "description": "A causa delle limitazioni della versione gratuita della chiave API AccuWeather, quando si abilitano le previsioni del tempo, gli aggiornamenti dei dati verranno eseguiti ogni 80 minuti invece che ogni 40.", - "title": "Opzioni AccuWeather" + "description": "A causa delle limitazioni della versione gratuita della chiave API AccuWeather, quando si abilitano le previsioni del tempo, gli aggiornamenti dei dati verranno eseguiti ogni 80 minuti invece che ogni 40." } } }, diff --git a/homeassistant/components/accuweather/translations/ja.json b/homeassistant/components/accuweather/translations/ja.json index d05e75e74b5..21ec4e6068d 100644 --- a/homeassistant/components/accuweather/translations/ja.json +++ b/homeassistant/components/accuweather/translations/ja.json @@ -18,9 +18,7 @@ "latitude": "\u7def\u5ea6", "longitude": "\u7d4c\u5ea6", "name": "\u540d\u524d" - }, - "description": "\u8a2d\u5b9a\u306b\u3064\u3044\u3066\u30d8\u30eb\u30d7\u304c\u5fc5\u8981\u306a\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/accuweather/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u4e00\u90e8\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306e\u8a2d\u5b9a\u5f8c\u306b\u3001\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30ec\u30b8\u30b9\u30c8\u30ea\u3067\u6709\u52b9\u306b\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\n\u5929\u6c17\u4e88\u5831\u306f\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u6709\u52b9\u306b\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002", - "title": "AccuWeather" + } } } }, @@ -30,8 +28,7 @@ "data": { "forecast": "\u5929\u6c17\u4e88\u5831" }, - "description": "\u5236\u9650\u306b\u3088\u308a\u7121\u6599\u30d0\u30fc\u30b8\u30e7\u30f3\u306eAccuWeather API\u30ad\u30fc\u3067\u306f\u3001\u5929\u6c17\u4e88\u5831\u3092\u6709\u52b9\u306b\u3057\u3066\u3082\u30c7\u30fc\u30bf\u306e\u66f4\u65b0\u306f40\u5206\u3067\u306f\u306a\u304f80\u5206\u3054\u3068\u3067\u3059\u3002", - "title": "AccuWeather\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + "description": "\u5236\u9650\u306b\u3088\u308a\u7121\u6599\u30d0\u30fc\u30b8\u30e7\u30f3\u306eAccuWeather API\u30ad\u30fc\u3067\u306f\u3001\u5929\u6c17\u4e88\u5831\u3092\u6709\u52b9\u306b\u3057\u3066\u3082\u30c7\u30fc\u30bf\u306e\u66f4\u65b0\u306f40\u5206\u3067\u306f\u306a\u304f80\u5206\u3054\u3068\u3067\u3059\u3002" } } }, diff --git a/homeassistant/components/accuweather/translations/ko.json b/homeassistant/components/accuweather/translations/ko.json index d992d0bfdd4..ad33fae591c 100644 --- a/homeassistant/components/accuweather/translations/ko.json +++ b/homeassistant/components/accuweather/translations/ko.json @@ -15,9 +15,7 @@ "latitude": "\uc704\ub3c4", "longitude": "\uacbd\ub3c4", "name": "\uc774\ub984" - }, - "description": "\uad6c\uc131\uc5d0 \ub300\ud55c \ub3c4\uc6c0\uc774 \ud544\uc694\ud55c \uacbd\uc6b0 \ub2e4\uc74c\uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694: https://www.home-assistant.io/integrations/accuweather/\n\n\uc77c\ubd80 \uc13c\uc11c\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uad6c\uc131 \ud6c4 \uad6c\uc131\uc694\uc18c \ub808\uc9c0\uc2a4\ud2b8\ub9ac\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.\n\uc77c\uae30\uc608\ubcf4\ub294 \uae30\ubcf8\uc801\uc73c\ub85c \ud65c\uc131\ud654\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc635\uc158\uc5d0\uc11c \ud65c\uc131\ud654\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "title": "AccuWeather" + } } } }, @@ -27,8 +25,7 @@ "data": { "forecast": "\ub0a0\uc528 \uc608\ubcf4" }, - "description": "\ubb34\ub8cc \ubc84\uc804\uc758 AccuWeather API \ud0a4\ub85c \uc77c\uae30\uc608\ubcf4\ub97c \ud65c\uc131\ud654\ud55c \uacbd\uc6b0 \uc81c\ud55c\uc0ac\ud56d\uc73c\ub85c \uc778\ud574 \uc5c5\ub370\uc774\ud2b8\ub294 40 \ubd84\uc774 \uc544\ub2cc 80 \ubd84\ub9c8\ub2e4 \uc218\ud589\ub429\ub2c8\ub2e4.", - "title": "AccuWeather \uc635\uc158" + "description": "\ubb34\ub8cc \ubc84\uc804\uc758 AccuWeather API \ud0a4\ub85c \uc77c\uae30\uc608\ubcf4\ub97c \ud65c\uc131\ud654\ud55c \uacbd\uc6b0 \uc81c\ud55c\uc0ac\ud56d\uc73c\ub85c \uc778\ud574 \uc5c5\ub370\uc774\ud2b8\ub294 40 \ubd84\uc774 \uc544\ub2cc 80 \ubd84\ub9c8\ub2e4 \uc218\ud589\ub429\ub2c8\ub2e4." } } }, diff --git a/homeassistant/components/accuweather/translations/lb.json b/homeassistant/components/accuweather/translations/lb.json index 7f3855a7b9c..618b9ef984b 100644 --- a/homeassistant/components/accuweather/translations/lb.json +++ b/homeassistant/components/accuweather/translations/lb.json @@ -15,9 +15,7 @@ "latitude": "Breedegrad", "longitude": "L\u00e4ngegrad", "name": "Numm" - }, - "description": "Falls du H\u00ebllef mat der Konfiguratioun brauch kuck h\u00e9i:\nhttps://www.home-assistant.io/integrations/accuweather/\n\nVerschidde Sensoren si standardm\u00e9isseg net aktiv. Du kanns d\u00e9i an der Entit\u00e9ie Registry no der Konfiguratioun vun der Integratioun aschalten.\n\nWieder Pr\u00e9visounen si standardm\u00e9isseg net aktiv. Du kanns d\u00e9i an den Optioune vun der Integratioun aschalten.", - "title": "AccuWeather" + } } } }, @@ -27,8 +25,7 @@ "data": { "forecast": "Wieder Pr\u00e9visioun" }, - "description": "Duerch d'Limite vun der Gratis Versioun vun der AccuWeather API, wann d'Wieder Pr\u00e9visoune aktiv\u00e9iert sinn, ginn d'Aktualis\u00e9ierungen all 64 Minutten gemaach, am plaatz vun all 32 Minutten.", - "title": "AccuWeather Optiounen" + "description": "Duerch d'Limite vun der Gratis Versioun vun der AccuWeather API, wann d'Wieder Pr\u00e9visoune aktiv\u00e9iert sinn, ginn d'Aktualis\u00e9ierungen all 64 Minutten gemaach, am plaatz vun all 32 Minutten." } } }, diff --git a/homeassistant/components/accuweather/translations/nl.json b/homeassistant/components/accuweather/translations/nl.json index 2a6dd1a812b..e83b3d8355a 100644 --- a/homeassistant/components/accuweather/translations/nl.json +++ b/homeassistant/components/accuweather/translations/nl.json @@ -18,9 +18,7 @@ "latitude": "Breedtegraad", "longitude": "Lengtegraad", "name": "Naam" - }, - "description": "Als je hulp nodig hebt bij de configuratie, kijk dan hier: https://www.home-assistant.io/integrations/accuweather/ \n\n Sommige sensoren zijn niet standaard ingeschakeld. U kunt ze inschakelen in het entiteitenregister na de integratieconfiguratie.\n Weersvoorspelling is niet standaard ingeschakeld. U kunt het inschakelen in de integratieopties.", - "title": "AccuWeather" + } } } }, @@ -30,8 +28,7 @@ "data": { "forecast": "Weervoorspelling" }, - "description": "Vanwege de beperkingen van de gratis versie van de AccuWeather API-sleutel, worden gegevensupdates elke 64 minuten in plaats van elke 32 minuten uitgevoerd wanneer u weersvoorspelling inschakelt.", - "title": "AccuWeather-opties" + "description": "Vanwege de beperkingen van de gratis versie van de AccuWeather API-sleutel, worden gegevensupdates elke 64 minuten in plaats van elke 32 minuten uitgevoerd wanneer u weersvoorspelling inschakelt." } } }, diff --git a/homeassistant/components/accuweather/translations/no.json b/homeassistant/components/accuweather/translations/no.json index af689b2fd1c..dc203abe714 100644 --- a/homeassistant/components/accuweather/translations/no.json +++ b/homeassistant/components/accuweather/translations/no.json @@ -18,9 +18,7 @@ "latitude": "Breddegrad", "longitude": "Lengdegrad", "name": "Navn" - }, - "description": "Hvis du trenger hjelp med konfigurasjonen, kan du se her: https://www.home-assistant.io/integrations/accuweather/ \n\nNoen sensorer er ikke aktivert som standard. Du kan aktivere dem i entitetsregisteret etter integrasjonskonfigurasjonen. \nV\u00e6rmelding er ikke aktivert som standard. Du kan aktivere det i integrasjonsalternativene.", - "title": "" + } } } }, @@ -30,8 +28,7 @@ "data": { "forecast": "V\u00e6rmelding" }, - "description": "P\u00e5 grunn av begrensningene i den gratis versjonen av AccuWeather API-n\u00f8kkelen, vil dataoppdateringer utf\u00f8res hvert 80. minutt i stedet for hvert 40. minutt n\u00e5r du aktiverer v\u00e6rmelding.", - "title": "AccuWeather-alternativer" + "description": "P\u00e5 grunn av begrensningene i den gratis versjonen av AccuWeather API-n\u00f8kkelen, vil dataoppdateringer utf\u00f8res hvert 80. minutt i stedet for hvert 40. minutt n\u00e5r du aktiverer v\u00e6rmelding." } } }, diff --git a/homeassistant/components/accuweather/translations/pl.json b/homeassistant/components/accuweather/translations/pl.json index a8ef8a3bfa0..764218f8e11 100644 --- a/homeassistant/components/accuweather/translations/pl.json +++ b/homeassistant/components/accuweather/translations/pl.json @@ -18,9 +18,7 @@ "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "name": "Nazwa" - }, - "description": "Je\u015bli potrzebujesz pomocy z konfiguracj\u0105, przejd\u017a na stron\u0119: https://www.home-assistant.io/integrations/accuweather/ \n\nCz\u0119\u015b\u0107 sensor\u00f3w nie jest w\u0142\u0105czona domy\u015blnie. Mo\u017cesz je w\u0142\u0105czy\u0107 w rejestrze encji po konfiguracji integracji.\nPrognoza pogody nie jest domy\u015blnie w\u0142\u0105czona. Mo\u017cesz j\u0105 w\u0142\u0105czy\u0107 w opcjach integracji.", - "title": "AccuWeather" + } } } }, @@ -30,8 +28,7 @@ "data": { "forecast": "Prognoza pogody" }, - "description": "Ze wzgl\u0119du na ograniczenia darmowej wersji klucza API AccuWeather po w\u0142\u0105czeniu prognozy pogody aktualizacje danych b\u0119d\u0105 wykonywane co 80 minut zamiast co 40 minut.", - "title": "Opcje AccuWeather" + "description": "Ze wzgl\u0119du na ograniczenia darmowej wersji klucza API AccuWeather po w\u0142\u0105czeniu prognozy pogody aktualizacje danych b\u0119d\u0105 wykonywane co 80 minut zamiast co 40 minut." } } }, diff --git a/homeassistant/components/accuweather/translations/pt-BR.json b/homeassistant/components/accuweather/translations/pt-BR.json index 4bb1bbbc97e..3fc0d95ad2a 100644 --- a/homeassistant/components/accuweather/translations/pt-BR.json +++ b/homeassistant/components/accuweather/translations/pt-BR.json @@ -18,9 +18,7 @@ "latitude": "Latitude", "longitude": "Longitude", "name": "Nome" - }, - "description": "Se precisar de ajuda com a configura\u00e7\u00e3o, d\u00ea uma olhada aqui: https://www.home-assistant.io/integrations/accuweather/ \n\nAlguns sensores n\u00e3o s\u00e3o ativados por padr\u00e3o. Voc\u00ea pode habilit\u00e1-los no registro da entidade ap\u00f3s a configura\u00e7\u00e3o da integra\u00e7\u00e3o.\nA previs\u00e3o do tempo n\u00e3o est\u00e1 habilitada por padr\u00e3o. Voc\u00ea pode habilit\u00e1-lo nas op\u00e7\u00f5es de integra\u00e7\u00e3o.", - "title": "AccuWeather" + } } } }, @@ -30,8 +28,7 @@ "data": { "forecast": "Previs\u00e3o do Tempo" }, - "description": "Devido \u00e0s limita\u00e7\u00f5es da vers\u00e3o gratuita da chave da API AccuWeather, quando voc\u00ea habilita a previs\u00e3o do tempo, as atualiza\u00e7\u00f5es de dados ser\u00e3o realizadas a cada 64 minutos em vez de a cada 32 minutos.", - "title": "Op\u00e7\u00f5es do AccuWeather" + "description": "Devido \u00e0s limita\u00e7\u00f5es da vers\u00e3o gratuita da chave da API AccuWeather, quando voc\u00ea habilita a previs\u00e3o do tempo, as atualiza\u00e7\u00f5es de dados ser\u00e3o realizadas a cada 64 minutos em vez de a cada 32 minutos." } } }, diff --git a/homeassistant/components/accuweather/translations/ru.json b/homeassistant/components/accuweather/translations/ru.json index dfd24b6f147..2f08263b4f5 100644 --- a/homeassistant/components/accuweather/translations/ru.json +++ b/homeassistant/components/accuweather/translations/ru.json @@ -18,9 +18,7 @@ "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" - }, - "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438, \u0435\u0441\u043b\u0438 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u0430 \u043f\u043e\u043c\u043e\u0449\u044c \u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439:\nhttps://www.home-assistant.io/integrations/accuweather/ \n\n\u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u044b \u0441\u043a\u0440\u044b\u0442\u044b \u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u043d\u0443\u0436\u043d\u044b\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 \u0432 \u0440\u0435\u0435\u0441\u0442\u0440\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0438 \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.", - "title": "AccuWeather" + } } } }, @@ -30,8 +28,7 @@ "data": { "forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b" }, - "description": "\u0412 \u0441\u0432\u044f\u0437\u0438 \u0441 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u043a\u043b\u044e\u0447\u0430 API AccuWeather, \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u043f\u043e\u0433\u043e\u0434\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442\u044c \u043a\u0430\u0436\u0434\u044b\u0435 80 \u043c\u0438\u043d\u0443\u0442, \u0430 \u043d\u0435 \u043a\u0430\u0436\u0434\u044b\u0435 40 \u043c\u0438\u043d\u0443\u0442.", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AccuWeather" + "description": "\u0412 \u0441\u0432\u044f\u0437\u0438 \u0441 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u043a\u043b\u044e\u0447\u0430 API AccuWeather, \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u043f\u043e\u0433\u043e\u0434\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442\u044c \u043a\u0430\u0436\u0434\u044b\u0435 80 \u043c\u0438\u043d\u0443\u0442, \u0430 \u043d\u0435 \u043a\u0430\u0436\u0434\u044b\u0435 40 \u043c\u0438\u043d\u0443\u0442." } } }, diff --git a/homeassistant/components/accuweather/translations/tr.json b/homeassistant/components/accuweather/translations/tr.json index b5efeb7080e..e0907fe2fa8 100644 --- a/homeassistant/components/accuweather/translations/tr.json +++ b/homeassistant/components/accuweather/translations/tr.json @@ -18,9 +18,7 @@ "latitude": "Enlem", "longitude": "Boylam", "name": "Ad" - }, - "description": "Yap\u0131land\u0131rmayla ilgili yard\u0131ma ihtiyac\u0131n\u0131z varsa buraya bak\u0131n: https://www.home-assistant.io/integrations/accuweather/ \n\n Baz\u0131 sens\u00f6rler varsay\u0131lan olarak etkin de\u011fildir. Bunlar\u0131, entegrasyon yap\u0131land\u0131rmas\u0131ndan sonra varl\u0131k kay\u0131t defterinde etkinle\u015ftirebilirsiniz.\n Hava tahmini varsay\u0131lan olarak etkin de\u011fildir. Entegrasyon se\u00e7eneklerinde etkinle\u015ftirebilirsiniz.", - "title": "AccuWeather" + } } } }, @@ -30,8 +28,7 @@ "data": { "forecast": "Hava Durumu tahmini" }, - "description": "AccuWeather API anahtar\u0131n\u0131n \u00fccretsiz s\u00fcr\u00fcm\u00fcn\u00fcn s\u0131n\u0131rlamalar\u0131 nedeniyle, hava tahminini etkinle\u015ftirdi\u011finizde, veri g\u00fcncellemeleri her 40 dakikada bir yerine 80 dakikada bir ger\u00e7ekle\u015ftirilir.", - "title": "AccuWeather Se\u00e7enekleri" + "description": "AccuWeather API anahtar\u0131n\u0131n \u00fccretsiz s\u00fcr\u00fcm\u00fcn\u00fcn s\u0131n\u0131rlamalar\u0131 nedeniyle, hava tahminini etkinle\u015ftirdi\u011finizde, veri g\u00fcncellemeleri her 40 dakikada bir yerine 80 dakikada bir ger\u00e7ekle\u015ftirilir." } } }, diff --git a/homeassistant/components/accuweather/translations/uk.json b/homeassistant/components/accuweather/translations/uk.json index cb02c7e0ad5..99818d354f9 100644 --- a/homeassistant/components/accuweather/translations/uk.json +++ b/homeassistant/components/accuweather/translations/uk.json @@ -15,9 +15,7 @@ "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430" - }, - "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438, \u044f\u043a\u0449\u043e \u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u0430 \u0437 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c:\n https://www.home-assistant.io/integrations/accuweather/ \n\n\u0417\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0434\u0435\u044f\u043a\u0456 \u0441\u0435\u043d\u0441\u043e\u0440\u0438 \u043f\u0440\u0438\u0445\u043e\u0432\u0430\u043d\u0456 \u0456 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u0438. \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0430\u043a\u0442\u0438\u0432\u0443\u0432\u0430\u0442\u0438 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0438\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u0456\u0432 \u0432 \u0440\u0435\u0454\u0441\u0442\u0440\u0456 \u043e\u0431'\u0454\u043a\u0442\u0456\u0432 \u0456 \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u0438 \u0432 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u0445 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457.", - "title": "AccuWeather" + } } } }, @@ -27,8 +25,7 @@ "data": { "forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u0438" }, - "description": "\u0423 \u0437\u0432'\u044f\u0437\u043a\u0443 \u0437 \u043e\u0431\u043c\u0435\u0436\u0435\u043d\u043d\u044f\u043c\u0438 \u0431\u0435\u0437\u043a\u043e\u0448\u0442\u043e\u0432\u043d\u043e\u0457 \u0432\u0435\u0440\u0441\u0456\u0457 \u043a\u043b\u044e\u0447\u0430 API AccuWeather, \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u0456 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0443 \u043f\u043e\u0433\u043e\u0434\u0438 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0434\u0430\u043d\u0438\u0445 \u0431\u0443\u0434\u0435 \u0432\u0456\u0434\u0431\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u043a\u043e\u0436\u043d\u0456 64 \u0445\u0432\u0438\u043b\u0438\u043d\u0438, \u0430 \u043d\u0435 \u043a\u043e\u0436\u043d\u0456 32 \u0445\u0432\u0438\u043b\u0438\u043d\u0438.", - "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AccuWeather" + "description": "\u0423 \u0437\u0432'\u044f\u0437\u043a\u0443 \u0437 \u043e\u0431\u043c\u0435\u0436\u0435\u043d\u043d\u044f\u043c\u0438 \u0431\u0435\u0437\u043a\u043e\u0448\u0442\u043e\u0432\u043d\u043e\u0457 \u0432\u0435\u0440\u0441\u0456\u0457 \u043a\u043b\u044e\u0447\u0430 API AccuWeather, \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u0456 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0443 \u043f\u043e\u0433\u043e\u0434\u0438 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0434\u0430\u043d\u0438\u0445 \u0431\u0443\u0434\u0435 \u0432\u0456\u0434\u0431\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u043a\u043e\u0436\u043d\u0456 64 \u0445\u0432\u0438\u043b\u0438\u043d\u0438, \u0430 \u043d\u0435 \u043a\u043e\u0436\u043d\u0456 32 \u0445\u0432\u0438\u043b\u0438\u043d\u0438." } } }, diff --git a/homeassistant/components/accuweather/translations/zh-Hant.json b/homeassistant/components/accuweather/translations/zh-Hant.json index 6d9e7e1c36f..c7476b33460 100644 --- a/homeassistant/components/accuweather/translations/zh-Hant.json +++ b/homeassistant/components/accuweather/translations/zh-Hant.json @@ -18,9 +18,7 @@ "latitude": "\u7def\u5ea6", "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\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" + } } } }, @@ -30,8 +28,7 @@ "data": { "forecast": "\u5929\u6c23\u9810\u5831" }, - "description": "\u7531\u65bc AccuWeather API \u91d1\u9470\u514d\u8cbb\u7248\u672c\u9650\u5236\uff0c\u7576\u958b\u555f\u5929\u6c23\u9810\u5831\u6642\u3001\u6578\u64da\u6703\u6bcf 80 \u5206\u9418\u66f4\u65b0\u4e00\u6b21\uff0c\u800c\u975e 40 \u5206\u9418\u3002", - "title": "AccuWeather \u9078\u9805" + "description": "\u7531\u65bc AccuWeather API \u91d1\u9470\u514d\u8cbb\u7248\u672c\u9650\u5236\uff0c\u7576\u958b\u555f\u5929\u6c23\u9810\u5831\u6642\u3001\u6578\u64da\u6703\u6bcf 80 \u5206\u9418\u66f4\u65b0\u4e00\u6b21\uff0c\u800c\u975e 40 \u5206\u9418\u3002" } } }, diff --git a/homeassistant/components/adax/translations/bg.json b/homeassistant/components/adax/translations/bg.json index d76109bfe01..e0204d6b43b 100644 --- a/homeassistant/components/adax/translations/bg.json +++ b/homeassistant/components/adax/translations/bg.json @@ -5,8 +5,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" }, "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" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { "cloud": { @@ -19,12 +18,6 @@ "wifi_pswd": "Wi-Fi \u043f\u0430\u0440\u043e\u043b\u0430", "wifi_ssid": "Wi-Fi SSID" } - }, - "user": { - "data": { - "host": "\u0425\u043e\u0441\u0442", - "password": "\u041f\u0430\u0440\u043e\u043b\u0430" - } } } } diff --git a/homeassistant/components/adax/translations/ca.json b/homeassistant/components/adax/translations/ca.json index 4e9f5eeb317..af3f2c3dc3f 100644 --- a/homeassistant/components/adax/translations/ca.json +++ b/homeassistant/components/adax/translations/ca.json @@ -7,8 +7,7 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "error": { - "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + "cannot_connect": "Ha fallat la connexi\u00f3" }, "step": { "cloud": { @@ -26,10 +25,7 @@ }, "user": { "data": { - "account_id": "ID del compte", - "connection_type": "Selecciona el tipus de connexi\u00f3", - "host": "Amfitri\u00f3", - "password": "Contrasenya" + "connection_type": "Selecciona el tipus de connexi\u00f3" }, "description": "Selecciona el tipus de connexi\u00f3. La local necessita escalfadors amb Bluetooth" } diff --git a/homeassistant/components/adax/translations/cs.json b/homeassistant/components/adax/translations/cs.json index 07eeaa34f63..cbe934d3495 100644 --- a/homeassistant/components/adax/translations/cs.json +++ b/homeassistant/components/adax/translations/cs.json @@ -7,8 +7,7 @@ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, "error": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { "cloud": { @@ -26,10 +25,7 @@ }, "user": { "data": { - "account_id": "ID \u00fa\u010dtu", - "connection_type": "Vyberte typ p\u0159ipojen\u00ed", - "host": "Hostitel", - "password": "Heslo" + "connection_type": "Vyberte typ p\u0159ipojen\u00ed" }, "description": "Vyberte typ p\u0159ipojen\u00ed. Lok\u00e1ln\u00ed vy\u017eaduje oh\u0159\u00edva\u010de s bluetooth" } diff --git a/homeassistant/components/adax/translations/de.json b/homeassistant/components/adax/translations/de.json index 711a8b44645..a3e4b1b78d4 100644 --- a/homeassistant/components/adax/translations/de.json +++ b/homeassistant/components/adax/translations/de.json @@ -7,8 +7,7 @@ "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_auth": "Ung\u00fcltige Authentifizierung" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "cloud": { @@ -26,10 +25,7 @@ }, "user": { "data": { - "account_id": "Konto-ID", - "connection_type": "Verbindungstyp ausw\u00e4hlen", - "host": "Host", - "password": "Passwort" + "connection_type": "Verbindungstyp ausw\u00e4hlen" }, "description": "Verbindungstyp ausw\u00e4hlen. Lokal erfordert Heizungen mit Bluetooth" } diff --git a/homeassistant/components/adax/translations/el.json b/homeassistant/components/adax/translations/el.json index ead0eb7e4f1..e1be44a197a 100644 --- a/homeassistant/components/adax/translations/el.json +++ b/homeassistant/components/adax/translations/el.json @@ -7,8 +7,7 @@ "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" }, "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "step": { "cloud": { @@ -26,10 +25,7 @@ }, "user": { "data": { - "account_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd", - "connection_type": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c4\u03cd\u03c0\u03bf\u03c5 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + "connection_type": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c4\u03cd\u03c0\u03bf\u03c5 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03cd\u03c0\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2. \u03a4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03b8\u03b5\u03c1\u03bc\u03ac\u03c3\u03c4\u03c1\u03b5\u03c2 \u03bc\u03b5 bluetooth" } diff --git a/homeassistant/components/adax/translations/en.json b/homeassistant/components/adax/translations/en.json index ae31fdbc041..89c73702106 100644 --- a/homeassistant/components/adax/translations/en.json +++ b/homeassistant/components/adax/translations/en.json @@ -7,8 +7,7 @@ "invalid_auth": "Invalid authentication" }, "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication" + "cannot_connect": "Failed to connect" }, "step": { "cloud": { @@ -26,10 +25,7 @@ }, "user": { "data": { - "account_id": "Account ID", - "connection_type": "Select connection type", - "host": "Host", - "password": "Password" + "connection_type": "Select connection type" }, "description": "Select connection type. Local requires heaters with bluetooth" } diff --git a/homeassistant/components/adax/translations/es-419.json b/homeassistant/components/adax/translations/es-419.json index 85df150d663..597d031c598 100644 --- a/homeassistant/components/adax/translations/es-419.json +++ b/homeassistant/components/adax/translations/es-419.json @@ -20,7 +20,6 @@ }, "user": { "data": { - "account_id": "ID de cuenta", "connection_type": "Seleccione el tipo de conexi\u00f3n" }, "description": "Seleccione el tipo de conexi\u00f3n. Local requiere calentadores con bluetooth" diff --git a/homeassistant/components/adax/translations/es.json b/homeassistant/components/adax/translations/es.json index 413f8be0fa0..ea350d97f4e 100644 --- a/homeassistant/components/adax/translations/es.json +++ b/homeassistant/components/adax/translations/es.json @@ -7,8 +7,7 @@ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "error": { - "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + "cannot_connect": "No se pudo conectar" }, "step": { "cloud": { @@ -26,10 +25,7 @@ }, "user": { "data": { - "account_id": "ID de la cuenta", - "connection_type": "Seleccione el tipo de conexi\u00f3n", - "host": "Host", - "password": "Contrase\u00f1a" + "connection_type": "Seleccione el tipo de conexi\u00f3n" }, "description": "Seleccione el tipo de conexi\u00f3n. Local requiere calentadores con bluetooth" } diff --git a/homeassistant/components/adax/translations/et.json b/homeassistant/components/adax/translations/et.json index 2d154614295..8f164bc167c 100644 --- a/homeassistant/components/adax/translations/et.json +++ b/homeassistant/components/adax/translations/et.json @@ -7,8 +7,7 @@ "invalid_auth": "Tuvastamine nurjus" }, "error": { - "cannot_connect": "\u00dchendamine nurjus", - "invalid_auth": "Tuvastamise viga" + "cannot_connect": "\u00dchendamine nurjus" }, "step": { "cloud": { @@ -26,10 +25,7 @@ }, "user": { "data": { - "account_id": "Konto ID", - "connection_type": "Vali \u00fchenduse t\u00fc\u00fcp", - "host": "Host", - "password": "Salas\u00f5na" + "connection_type": "Vali \u00fchenduse t\u00fc\u00fcp" }, "description": "Vali \u00fchenduse t\u00fc\u00fcp. Kohalik n\u00f5uab bluetoothiga k\u00fctteseadmeid" } diff --git a/homeassistant/components/adax/translations/fr.json b/homeassistant/components/adax/translations/fr.json index edcdcdd2738..1883db9c7f0 100644 --- a/homeassistant/components/adax/translations/fr.json +++ b/homeassistant/components/adax/translations/fr.json @@ -7,8 +7,7 @@ "invalid_auth": "Authentification non valide" }, "error": { - "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification non valide" + "cannot_connect": "\u00c9chec de connexion" }, "step": { "cloud": { @@ -26,10 +25,7 @@ }, "user": { "data": { - "account_id": "identifiant de compte", - "connection_type": "S\u00e9lectionner le type de connexion", - "host": "H\u00f4te", - "password": "Mot de passe" + "connection_type": "S\u00e9lectionner le type de connexion" }, "description": "S\u00e9lectionnez le type de connexion. Local n\u00e9cessite des radiateurs avec Bluetooth" } diff --git a/homeassistant/components/adax/translations/he.json b/homeassistant/components/adax/translations/he.json index 0ba47926a07..e07cb6338ef 100644 --- a/homeassistant/components/adax/translations/he.json +++ b/homeassistant/components/adax/translations/he.json @@ -5,21 +5,13 @@ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "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" + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, "step": { "cloud": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" } - }, - "user": { - "data": { - "account_id": "\u05de\u05d6\u05d4\u05d4 \u05d7\u05e9\u05d1\u05d5\u05df", - "host": "\u05de\u05d0\u05e8\u05d7", - "password": "\u05e1\u05d9\u05e1\u05de\u05d4" - } } } } diff --git a/homeassistant/components/adax/translations/hu.json b/homeassistant/components/adax/translations/hu.json index a2a6dcdacaf..d0209483231 100644 --- a/homeassistant/components/adax/translations/hu.json +++ b/homeassistant/components/adax/translations/hu.json @@ -7,8 +7,7 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "error": { - "cannot_connect": "Nem siker\u00fclt csatlakozni", - "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + "cannot_connect": "Nem siker\u00fclt csatlakozni" }, "step": { "cloud": { @@ -26,10 +25,7 @@ }, "user": { "data": { - "account_id": "Fi\u00f3k ID", - "connection_type": "V\u00e1lassza ki a kapcsolat t\u00edpus\u00e1t", - "host": "C\u00edm", - "password": "Jelsz\u00f3" + "connection_type": "V\u00e1lassza ki a kapcsolat t\u00edpus\u00e1t" }, "description": "V\u00e1lassza ki a kapcsolat t\u00edpus\u00e1t. A Helyi kapcsolathoz bluetooth-os f\u0171t\u0151berendez\u00e9sekre van sz\u00fcks\u00e9g" } diff --git a/homeassistant/components/adax/translations/id.json b/homeassistant/components/adax/translations/id.json index 864a8ed623d..5dd3f88d47d 100644 --- a/homeassistant/components/adax/translations/id.json +++ b/homeassistant/components/adax/translations/id.json @@ -7,8 +7,7 @@ "invalid_auth": "Autentikasi tidak valid" }, "error": { - "cannot_connect": "Gagal terhubung", - "invalid_auth": "Autentikasi tidak valid" + "cannot_connect": "Gagal terhubung" }, "step": { "cloud": { @@ -26,10 +25,7 @@ }, "user": { "data": { - "account_id": "ID Akun", - "connection_type": "Pilih jenis koneksi", - "host": "Host", - "password": "Kata Sandi" + "connection_type": "Pilih jenis koneksi" }, "description": "Pilih jenis koneksi. Lokal membutuhkan pemanas dengan bluetooth" } diff --git a/homeassistant/components/adax/translations/it.json b/homeassistant/components/adax/translations/it.json index 17095fab12b..0b516951483 100644 --- a/homeassistant/components/adax/translations/it.json +++ b/homeassistant/components/adax/translations/it.json @@ -7,8 +7,7 @@ "invalid_auth": "Autenticazione non valida" }, "error": { - "cannot_connect": "Impossibile connettersi", - "invalid_auth": "Autenticazione non valida" + "cannot_connect": "Impossibile connettersi" }, "step": { "cloud": { @@ -26,10 +25,7 @@ }, "user": { "data": { - "account_id": "ID account", - "connection_type": "Seleziona il tipo di connessione", - "host": "Host", - "password": "Password" + "connection_type": "Seleziona il tipo di connessione" }, "description": "Seleziona il tipo di connessione. Locale richiede riscaldatori con bluetooth" } diff --git a/homeassistant/components/adax/translations/ja.json b/homeassistant/components/adax/translations/ja.json index e432e23e081..604aa892f5b 100644 --- a/homeassistant/components/adax/translations/ja.json +++ b/homeassistant/components/adax/translations/ja.json @@ -7,8 +7,7 @@ "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "cloud": { @@ -26,10 +25,7 @@ }, "user": { "data": { - "account_id": "\u30a2\u30ab\u30a6\u30f3\u30c8ID", - "connection_type": "\u63a5\u7d9a\u30bf\u30a4\u30d7\u306e\u9078\u629e", - "host": "\u30db\u30b9\u30c8", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "connection_type": "\u63a5\u7d9a\u30bf\u30a4\u30d7\u306e\u9078\u629e" }, "description": "\u63a5\u7d9a\u30bf\u30a4\u30d7\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u30ed\u30fc\u30ab\u30eb\u306b\u306fBluetooth\u4ed8\u304d\u306e\u30d2\u30fc\u30bf\u30fc\u304c\u5fc5\u8981\u3067\u3059" } diff --git a/homeassistant/components/adax/translations/nl.json b/homeassistant/components/adax/translations/nl.json index ced3b8bdab0..cef5c118af5 100644 --- a/homeassistant/components/adax/translations/nl.json +++ b/homeassistant/components/adax/translations/nl.json @@ -7,8 +7,7 @@ "invalid_auth": "Ongeldige authenticatie" }, "error": { - "cannot_connect": "Kan geen verbinding maken", - "invalid_auth": "Ongeldige authenticatie" + "cannot_connect": "Kan geen verbinding maken" }, "step": { "cloud": { @@ -26,10 +25,7 @@ }, "user": { "data": { - "account_id": "Account ID", - "connection_type": "Selecteer verbindingstype", - "host": "Host", - "password": "Wachtwoord" + "connection_type": "Selecteer verbindingstype" }, "description": "Selecteer verbindingstype. Lokaal vereist verwarming met Bluetooth." } diff --git a/homeassistant/components/adax/translations/no.json b/homeassistant/components/adax/translations/no.json index 23e82250673..a8787392af3 100644 --- a/homeassistant/components/adax/translations/no.json +++ b/homeassistant/components/adax/translations/no.json @@ -7,8 +7,7 @@ "invalid_auth": "Ugyldig godkjenning" }, "error": { - "cannot_connect": "Tilkobling mislyktes", - "invalid_auth": "Ugyldig godkjenning" + "cannot_connect": "Tilkobling mislyktes" }, "step": { "cloud": { @@ -26,10 +25,7 @@ }, "user": { "data": { - "account_id": "Konto-ID", - "connection_type": "Velg tilkoblingstype", - "host": "Vert", - "password": "Passord" + "connection_type": "Velg tilkoblingstype" }, "description": "Velg tilkoblingstype. Lokalt krever varmeovner med bluetooth" } diff --git a/homeassistant/components/adax/translations/pl.json b/homeassistant/components/adax/translations/pl.json index 2a8e1016805..c92862cbcd4 100644 --- a/homeassistant/components/adax/translations/pl.json +++ b/homeassistant/components/adax/translations/pl.json @@ -7,8 +7,7 @@ "invalid_auth": "Niepoprawne uwierzytelnienie" }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_auth": "Niepoprawne uwierzytelnienie" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { "cloud": { @@ -26,10 +25,7 @@ }, "user": { "data": { - "account_id": "Identyfikator konta", - "connection_type": "Wybierz typ po\u0142\u0105czenia", - "host": "Nazwa hosta lub adres IP", - "password": "Has\u0142o" + "connection_type": "Wybierz typ po\u0142\u0105czenia" }, "description": "Wybierz typ po\u0142\u0105czenia. \"Lokalny\" wymaga grzejnik\u00f3w z bluetooth." } diff --git a/homeassistant/components/adax/translations/pt-BR.json b/homeassistant/components/adax/translations/pt-BR.json index 09498932c20..f7cb68b30a9 100644 --- a/homeassistant/components/adax/translations/pt-BR.json +++ b/homeassistant/components/adax/translations/pt-BR.json @@ -7,8 +7,7 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "error": { - "cannot_connect": "Falha ao conectar", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + "cannot_connect": "Falha ao conectar" }, "step": { "cloud": { @@ -26,10 +25,7 @@ }, "user": { "data": { - "account_id": "ID da conta", - "connection_type": "Selecione o tipo de conex\u00e3o", - "host": "Nome do host", - "password": "Senha" + "connection_type": "Selecione o tipo de conex\u00e3o" }, "description": "Selecione o tipo de conex\u00e3o. Local requer aquecedores com bluetooth" } diff --git a/homeassistant/components/adax/translations/ru.json b/homeassistant/components/adax/translations/ru.json index 27596e86485..bb2d7c856e3 100644 --- a/homeassistant/components/adax/translations/ru.json +++ b/homeassistant/components/adax/translations/ru.json @@ -7,8 +7,7 @@ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "step": { "cloud": { @@ -26,10 +25,7 @@ }, "user": { "data": { - "account_id": "ID \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438", - "connection_type": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", - "host": "\u0425\u043e\u0441\u0442", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + "connection_type": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f. \u0414\u043b\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043e\u0431\u043e\u0433\u0440\u0435\u0432\u0430\u0442\u0435\u043b\u0438 \u0441 Bluetooth." } diff --git a/homeassistant/components/adax/translations/sk.json b/homeassistant/components/adax/translations/sk.json index 8bacfc145e2..af9d83e3703 100644 --- a/homeassistant/components/adax/translations/sk.json +++ b/homeassistant/components/adax/translations/sk.json @@ -2,16 +2,6 @@ "config": { "abort": { "invalid_auth": "Neplatn\u00e9 overenie" - }, - "error": { - "invalid_auth": "Neplatn\u00e9 overenie" - }, - "step": { - "user": { - "data": { - "password": "Heslo" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/adax/translations/tr.json b/homeassistant/components/adax/translations/tr.json index bd7ef0fb6e9..e1fec21628f 100644 --- a/homeassistant/components/adax/translations/tr.json +++ b/homeassistant/components/adax/translations/tr.json @@ -7,8 +7,7 @@ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131", - "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + "cannot_connect": "Ba\u011flanma hatas\u0131" }, "step": { "cloud": { @@ -26,10 +25,7 @@ }, "user": { "data": { - "account_id": "Hesap Kimli\u011fi", - "connection_type": "Ba\u011flant\u0131 t\u00fcr\u00fcn\u00fc se\u00e7in", - "host": "Sunucu", - "password": "Parola" + "connection_type": "Ba\u011flant\u0131 t\u00fcr\u00fcn\u00fc se\u00e7in" }, "description": "Ba\u011flant\u0131 t\u00fcr\u00fcn\u00fc se\u00e7in. Yerel Bluetooth'lu \u0131s\u0131t\u0131c\u0131lar gerektirir" } diff --git a/homeassistant/components/adax/translations/zh-Hans.json b/homeassistant/components/adax/translations/zh-Hans.json index 86a404bcbf6..161112a3500 100644 --- a/homeassistant/components/adax/translations/zh-Hans.json +++ b/homeassistant/components/adax/translations/zh-Hans.json @@ -21,8 +21,7 @@ }, "user": { "data": { - "connection_type": "\u9009\u62e9\u8fde\u63a5\u7c7b\u578b", - "password": "\u5bc6\u7801" + "connection_type": "\u9009\u62e9\u8fde\u63a5\u7c7b\u578b" } } } diff --git a/homeassistant/components/adax/translations/zh-Hant.json b/homeassistant/components/adax/translations/zh-Hant.json index 92018be07da..cd99affe40e 100644 --- a/homeassistant/components/adax/translations/zh-Hant.json +++ b/homeassistant/components/adax/translations/zh-Hant.json @@ -7,8 +7,7 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "step": { "cloud": { @@ -26,10 +25,7 @@ }, "user": { "data": { - "account_id": "\u5e33\u865f ID", - "connection_type": "\u9078\u64c7\u9023\u7dda\u985e\u5225", - "host": "\u4e3b\u6a5f\u7aef", - "password": "\u5bc6\u78bc" + "connection_type": "\u9078\u64c7\u9023\u7dda\u985e\u5225" }, "description": "\u9078\u64c7\u9023\u7dda\u985e\u5225\u3002\u672c\u5730\u7aef\u5c07\u9700\u8981\u5177\u5099\u85cd\u82bd\u52a0\u71b1\u5668" } diff --git a/homeassistant/components/aemet/translations/ca.json b/homeassistant/components/aemet/translations/ca.json index 7dab53fc049..91641dd4329 100644 --- a/homeassistant/components/aemet/translations/ca.json +++ b/homeassistant/components/aemet/translations/ca.json @@ -14,8 +14,7 @@ "longitude": "Longitud", "name": "Nom de la integraci\u00f3" }, - "description": "Per generar la clau API, v\u00e9s a https://opendata.aemet.es/centrodedescargas/altaUsuario", - "title": "AEMET OpenData" + "description": "Per generar la clau API, v\u00e9s a https://opendata.aemet.es/centrodedescargas/altaUsuario" } } }, diff --git a/homeassistant/components/aemet/translations/de.json b/homeassistant/components/aemet/translations/de.json index 25f374f2e43..cc8b8ead259 100644 --- a/homeassistant/components/aemet/translations/de.json +++ b/homeassistant/components/aemet/translations/de.json @@ -14,8 +14,7 @@ "longitude": "L\u00e4ngengrad", "name": "Name der Integration" }, - "description": "Um den API-Schl\u00fcssel zu generieren, besuche https://opendata.aemet.es/centrodedescargas/altaUsuario", - "title": "AEMET OpenData" + "description": "Um den API-Schl\u00fcssel zu generieren, besuche https://opendata.aemet.es/centrodedescargas/altaUsuario" } } }, diff --git a/homeassistant/components/aemet/translations/el.json b/homeassistant/components/aemet/translations/el.json index 757fd41be70..a1aa6fa7ba2 100644 --- a/homeassistant/components/aemet/translations/el.json +++ b/homeassistant/components/aemet/translations/el.json @@ -14,8 +14,7 @@ "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" }, - "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 AEMET OpenData. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://opendata.aemet.es/centrodedescargas/altaUsuario", - "title": "AEMET OpenData" + "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 AEMET OpenData. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://opendata.aemet.es/centrodedescargas/altaUsuario" } } }, diff --git a/homeassistant/components/aemet/translations/en.json b/homeassistant/components/aemet/translations/en.json index 7c10d1dc676..f732768c219 100644 --- a/homeassistant/components/aemet/translations/en.json +++ b/homeassistant/components/aemet/translations/en.json @@ -14,8 +14,7 @@ "longitude": "Longitude", "name": "Name of the integration" }, - "description": "To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario", - "title": "AEMET OpenData" + "description": "To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario" } } }, diff --git a/homeassistant/components/aemet/translations/es-419.json b/homeassistant/components/aemet/translations/es-419.json index 3a02d682f34..cc047365ee4 100644 --- a/homeassistant/components/aemet/translations/es-419.json +++ b/homeassistant/components/aemet/translations/es-419.json @@ -5,8 +5,7 @@ "data": { "name": "Nombre de la integraci\u00f3n" }, - "description": "Configure la integraci\u00f3n de AEMET OpenData. Para generar la clave API vaya a https://opendata.aemet.es/centrodedescargas/altaUsuario", - "title": "AEMET OpenData" + "description": "Configure la integraci\u00f3n de AEMET OpenData. Para generar la clave API vaya a https://opendata.aemet.es/centrodedescargas/altaUsuario" } } }, diff --git a/homeassistant/components/aemet/translations/es.json b/homeassistant/components/aemet/translations/es.json index 558d87886d9..b46f7720789 100644 --- a/homeassistant/components/aemet/translations/es.json +++ b/homeassistant/components/aemet/translations/es.json @@ -14,8 +14,7 @@ "longitude": "Longitud", "name": "Nombre de la integraci\u00f3n" }, - "description": "Configurar la integraci\u00f3n de AEMET OpenData. Para generar la clave API, ve a https://opendata.aemet.es/centrodedescargas/altaUsuario", - "title": "AEMET OpenData" + "description": "Configurar la integraci\u00f3n de AEMET OpenData. Para generar la clave API, ve a https://opendata.aemet.es/centrodedescargas/altaUsuario" } } }, diff --git a/homeassistant/components/aemet/translations/et.json b/homeassistant/components/aemet/translations/et.json index 04292b3d912..e1781241227 100644 --- a/homeassistant/components/aemet/translations/et.json +++ b/homeassistant/components/aemet/translations/et.json @@ -14,8 +14,7 @@ "longitude": "Pikkuskraad", "name": "Sidumise nimi" }, - "description": "API-v\u00f5tme loomiseks mine aadressile https://opendata.aemet.es/centrodedescargas/altaUsuario", - "title": "AEMET OpenData" + "description": "API-v\u00f5tme loomiseks mine aadressile https://opendata.aemet.es/centrodedescargas/altaUsuario" } } }, diff --git a/homeassistant/components/aemet/translations/fr.json b/homeassistant/components/aemet/translations/fr.json index bddfd2507bf..6c9d1be2cf2 100644 --- a/homeassistant/components/aemet/translations/fr.json +++ b/homeassistant/components/aemet/translations/fr.json @@ -14,8 +14,7 @@ "longitude": "Longitude", "name": "Nom de l'int\u00e9gration" }, - "description": "Pour g\u00e9n\u00e9rer une cl\u00e9 d'API, rendez-vous sur https://opendata.aemet.es/centrodedescargas/altaUsuario", - "title": "AEMET OpenData" + "description": "Pour g\u00e9n\u00e9rer une cl\u00e9 d'API, rendez-vous sur https://opendata.aemet.es/centrodedescargas/altaUsuario" } } }, diff --git a/homeassistant/components/aemet/translations/hu.json b/homeassistant/components/aemet/translations/hu.json index 2f2984b1c90..2fa9636ccec 100644 --- a/homeassistant/components/aemet/translations/hu.json +++ b/homeassistant/components/aemet/translations/hu.json @@ -14,8 +14,7 @@ "longitude": "Hossz\u00fas\u00e1g", "name": "Az integr\u00e1ci\u00f3 neve" }, - "description": "Az API-kulcs l\u00e9trehoz\u00e1s\u00e1hoz keresse fel a https://opendata.aemet.es/centrodedescargas/altaUsuario webhelyet", - "title": "AEMET OpenData" + "description": "Az API-kulcs l\u00e9trehoz\u00e1s\u00e1hoz keresse fel a https://opendata.aemet.es/centrodedescargas/altaUsuario webhelyet" } } }, diff --git a/homeassistant/components/aemet/translations/id.json b/homeassistant/components/aemet/translations/id.json index 62239172aae..22e7213b4a3 100644 --- a/homeassistant/components/aemet/translations/id.json +++ b/homeassistant/components/aemet/translations/id.json @@ -14,8 +14,7 @@ "longitude": "Bujur", "name": "Nama integrasi" }, - "description": "Untuk membuat kunci API, buka https://opendata.aemet.es/centrodedescargas/altaUsuario", - "title": "AEMET OpenData" + "description": "Untuk membuat kunci API, buka https://opendata.aemet.es/centrodedescargas/altaUsuario" } } }, diff --git a/homeassistant/components/aemet/translations/it.json b/homeassistant/components/aemet/translations/it.json index 4b3f6f2251b..ac62822d3c9 100644 --- a/homeassistant/components/aemet/translations/it.json +++ b/homeassistant/components/aemet/translations/it.json @@ -14,8 +14,7 @@ "longitude": "Logitudine", "name": "Nome dell'integrazione" }, - "description": "Per generare la chiave API, vai su https://opendata.aemet.es/centrodescargas/altaUsuario", - "title": "AEMET OpenData" + "description": "Per generare la chiave API, vai su https://opendata.aemet.es/centrodescargas/altaUsuario" } } }, diff --git a/homeassistant/components/aemet/translations/ja.json b/homeassistant/components/aemet/translations/ja.json index a3a0904c223..fd79c699cee 100644 --- a/homeassistant/components/aemet/translations/ja.json +++ b/homeassistant/components/aemet/translations/ja.json @@ -14,8 +14,7 @@ "longitude": "\u7d4c\u5ea6", "name": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u540d\u524d" }, - "description": "AEMET OpenData\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://opendata.aemet.es/centrodedescargas/altaUsuario \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", - "title": "AEMET OpenData" + "description": "AEMET OpenData\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://opendata.aemet.es/centrodedescargas/altaUsuario \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } }, diff --git a/homeassistant/components/aemet/translations/ko.json b/homeassistant/components/aemet/translations/ko.json index 95c11b018fe..23ca054f5be 100644 --- a/homeassistant/components/aemet/translations/ko.json +++ b/homeassistant/components/aemet/translations/ko.json @@ -14,8 +14,7 @@ "longitude": "\uacbd\ub3c4", "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc774\ub984" }, - "description": "AEMET OpenData \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://opendata.aemet.es/centrodedescargas/altaUsuario \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694", - "title": "AEMET OpenData" + "description": "AEMET OpenData \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://opendata.aemet.es/centrodedescargas/altaUsuario \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694" } } } diff --git a/homeassistant/components/aemet/translations/lb.json b/homeassistant/components/aemet/translations/lb.json index 8e83c0e86d3..a2152f0b4d0 100644 --- a/homeassistant/components/aemet/translations/lb.json +++ b/homeassistant/components/aemet/translations/lb.json @@ -7,8 +7,7 @@ "latitude": "L\u00e4ngegrad", "longitude": "Breedegrad", "name": "Numm vun der Integratioun" - }, - "title": "AEMET OpenData" + } } } } diff --git a/homeassistant/components/aemet/translations/nl.json b/homeassistant/components/aemet/translations/nl.json index abf590e5a36..5479d206fa9 100644 --- a/homeassistant/components/aemet/translations/nl.json +++ b/homeassistant/components/aemet/translations/nl.json @@ -14,8 +14,7 @@ "longitude": "Lengtegraad", "name": "Naam van de integratie" }, - "description": "Om een API sleutel te genereren ga naar https://opendata.aemet.es/centrodedescargas/altaUsuario", - "title": "AEMET OpenData" + "description": "Om een API sleutel te genereren ga naar https://opendata.aemet.es/centrodedescargas/altaUsuario" } } }, diff --git a/homeassistant/components/aemet/translations/no.json b/homeassistant/components/aemet/translations/no.json index a6076844122..86eed98a153 100644 --- a/homeassistant/components/aemet/translations/no.json +++ b/homeassistant/components/aemet/translations/no.json @@ -14,8 +14,7 @@ "longitude": "Lengdegrad", "name": "Navnet p\u00e5 integrasjonen" }, - "description": "For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://opendata.aemet.es/centrodedescargas/altaUsuario", - "title": "AEMET OpenData" + "description": "For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://opendata.aemet.es/centrodedescargas/altaUsuario" } } }, diff --git a/homeassistant/components/aemet/translations/pl.json b/homeassistant/components/aemet/translations/pl.json index f1021585140..5c83e00b974 100644 --- a/homeassistant/components/aemet/translations/pl.json +++ b/homeassistant/components/aemet/translations/pl.json @@ -14,8 +14,7 @@ "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "name": "Nazwa integracji" }, - "description": "Aby wygenerowa\u0107 klucz API, przejd\u017a do https://opendata.aemet.es/centrodedescargas/altaUsuario", - "title": "AEMET OpenData" + "description": "Aby wygenerowa\u0107 klucz API, przejd\u017a do https://opendata.aemet.es/centrodedescargas/altaUsuario" } } }, diff --git a/homeassistant/components/aemet/translations/pt-BR.json b/homeassistant/components/aemet/translations/pt-BR.json index 7c5972dccd8..7cd1c704f99 100644 --- a/homeassistant/components/aemet/translations/pt-BR.json +++ b/homeassistant/components/aemet/translations/pt-BR.json @@ -14,8 +14,7 @@ "longitude": "Longitude", "name": "Nome da integra\u00e7\u00e3o" }, - "description": "Para gerar a chave API acesse https://opendata.aemet.es/centrodedescargas/altaUsuario", - "title": "AEMET OpenData" + "description": "Para gerar a chave API acesse https://opendata.aemet.es/centrodedescargas/altaUsuario" } } }, diff --git a/homeassistant/components/aemet/translations/ru.json b/homeassistant/components/aemet/translations/ru.json index 9d8496cbb2d..142b05d3a52 100644 --- a/homeassistant/components/aemet/translations/ru.json +++ b/homeassistant/components/aemet/translations/ru.json @@ -14,8 +14,7 @@ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u0427\u0442\u043e\u0431\u044b \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://opendata.aemet.es/centrodedescargas/altaUsuario.", - "title": "AEMET OpenData" + "description": "\u0427\u0442\u043e\u0431\u044b \u0441\u0433\u0435\u043d\u0435\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://opendata.aemet.es/centrodedescargas/altaUsuario." } } }, diff --git a/homeassistant/components/aemet/translations/tr.json b/homeassistant/components/aemet/translations/tr.json index e1e3cae01ff..b835bf6ddc3 100644 --- a/homeassistant/components/aemet/translations/tr.json +++ b/homeassistant/components/aemet/translations/tr.json @@ -14,8 +14,7 @@ "longitude": "Boylam", "name": "Entegrasyonun ad\u0131" }, - "description": "API anahtar\u0131 olu\u015fturmak i\u00e7in https://opendata.aemet.es/centrodedescargas/altaUsuario adresine gidin.", - "title": "AEMET OpenData" + "description": "API anahtar\u0131 olu\u015fturmak i\u00e7in https://opendata.aemet.es/centrodedescargas/altaUsuario adresine gidin." } } }, diff --git a/homeassistant/components/aemet/translations/zh-Hant.json b/homeassistant/components/aemet/translations/zh-Hant.json index 4d77ca17505..63e6d69d6cd 100644 --- a/homeassistant/components/aemet/translations/zh-Hant.json +++ b/homeassistant/components/aemet/translations/zh-Hant.json @@ -14,8 +14,7 @@ "longitude": "\u7d93\u5ea6", "name": "\u6574\u5408\u540d\u7a31" }, - "description": "\u8acb\u81f3 https://opendata.aemet.es/centrodedescargas/altaUsuario \u4ee5\u7522\u751f API \u91d1\u9470", - "title": "AEMET OpenData" + "description": "\u8acb\u81f3 https://opendata.aemet.es/centrodedescargas/altaUsuario \u4ee5\u7522\u751f API \u91d1\u9470" } } }, diff --git a/homeassistant/components/airly/translations/bg.json b/homeassistant/components/airly/translations/bg.json index 955cc8c1ac4..03decc0ac69 100644 --- a/homeassistant/components/airly/translations/bg.json +++ b/homeassistant/components/airly/translations/bg.json @@ -15,8 +15,7 @@ "longitude": "\u0414\u044a\u043b\u0436\u0438\u043d\u0430", "name": "\u0418\u043c\u0435 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0430 \u0432\u044a\u0437\u0434\u0443\u0445\u0430 Airly \u0417\u0430 \u0434\u0430 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u0442\u0435 \u043a\u043b\u044e\u0447 \u0437\u0430 API, \u043e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 https://developer.airly.eu/register", - "title": "Airly" + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0430 \u0432\u044a\u0437\u0434\u0443\u0445\u0430 Airly \u0417\u0430 \u0434\u0430 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u0442\u0435 \u043a\u043b\u044e\u0447 \u0437\u0430 API, \u043e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 https://developer.airly.eu/register" } } } diff --git a/homeassistant/components/airly/translations/ca.json b/homeassistant/components/airly/translations/ca.json index ac956185c14..36d7aaf8190 100644 --- a/homeassistant/components/airly/translations/ca.json +++ b/homeassistant/components/airly/translations/ca.json @@ -15,8 +15,7 @@ "longitude": "Longitud", "name": "Nom" }, - "description": "Per generar la clau API, v\u00e9s a https://developer.airly.eu/register", - "title": "Airly" + "description": "Per generar la clau API, v\u00e9s a https://developer.airly.eu/register" } } }, diff --git a/homeassistant/components/airly/translations/cs.json b/homeassistant/components/airly/translations/cs.json index 8b35399bcb0..3b4f98f53f8 100644 --- a/homeassistant/components/airly/translations/cs.json +++ b/homeassistant/components/airly/translations/cs.json @@ -15,8 +15,7 @@ "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", "name": "Jm\u00e9no" }, - "description": "Nastavte integraci kvality vzduchu Airly. Chcete-li vygenerovat kl\u00ed\u010d rozhran\u00ed API, p\u0159ejd\u011bte na https://developer.airly.eu/register", - "title": "Airly" + "description": "Nastavte integraci kvality vzduchu Airly. Chcete-li vygenerovat kl\u00ed\u010d rozhran\u00ed API, p\u0159ejd\u011bte na https://developer.airly.eu/register" } } }, diff --git a/homeassistant/components/airly/translations/da.json b/homeassistant/components/airly/translations/da.json index 53b05a38992..2b1cbe643b8 100644 --- a/homeassistant/components/airly/translations/da.json +++ b/homeassistant/components/airly/translations/da.json @@ -14,8 +14,7 @@ "longitude": "L\u00e6ngdegrad", "name": "Integrationens navn" }, - "description": "Konfigurer Airly luftkvalitetsintegration. For at generere API-n\u00f8gle, g\u00e5 til https://developer.airly.eu/register", - "title": "Airly" + "description": "Konfigurer Airly luftkvalitetsintegration. For at generere API-n\u00f8gle, g\u00e5 til https://developer.airly.eu/register" } } } diff --git a/homeassistant/components/airly/translations/de.json b/homeassistant/components/airly/translations/de.json index 216a8c56c6d..bc37bbc139c 100644 --- a/homeassistant/components/airly/translations/de.json +++ b/homeassistant/components/airly/translations/de.json @@ -15,8 +15,7 @@ "longitude": "L\u00e4ngengrad", "name": "Name" }, - "description": "Um einen API-Schl\u00fcssel zu generieren, besuche https://developer.airly.eu/register", - "title": "Airly" + "description": "Um einen API-Schl\u00fcssel zu generieren, besuche https://developer.airly.eu/register" } } }, diff --git a/homeassistant/components/airly/translations/el.json b/homeassistant/components/airly/translations/el.json index adb944a4259..e8d61743523 100644 --- a/homeassistant/components/airly/translations/el.json +++ b/homeassistant/components/airly/translations/el.json @@ -15,8 +15,7 @@ "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, - "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c0\u03bf\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b1\u03ad\u03c1\u03b1 Airly. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://developer.airly.eu/register", - "title": "Airly" + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c0\u03bf\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b1\u03ad\u03c1\u03b1 Airly. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://developer.airly.eu/register" } } }, diff --git a/homeassistant/components/airly/translations/en.json b/homeassistant/components/airly/translations/en.json index 249905eff2a..1dee608b1da 100644 --- a/homeassistant/components/airly/translations/en.json +++ b/homeassistant/components/airly/translations/en.json @@ -15,8 +15,7 @@ "longitude": "Longitude", "name": "Name" }, - "description": "To generate API key go to https://developer.airly.eu/register", - "title": "Airly" + "description": "To generate API key go to https://developer.airly.eu/register" } } }, diff --git a/homeassistant/components/airly/translations/es-419.json b/homeassistant/components/airly/translations/es-419.json index c7d1e388d67..31149641d11 100644 --- a/homeassistant/components/airly/translations/es-419.json +++ b/homeassistant/components/airly/translations/es-419.json @@ -14,8 +14,7 @@ "longitude": "Longitud", "name": "Nombre de la integraci\u00f3n" }, - "description": "Configure la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave API, vaya a https://developer.airly.eu/register", - "title": "Airly" + "description": "Configure la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave API, vaya a https://developer.airly.eu/register" } } }, diff --git a/homeassistant/components/airly/translations/es.json b/homeassistant/components/airly/translations/es.json index a96a2f62293..9d0b254713b 100644 --- a/homeassistant/components/airly/translations/es.json +++ b/homeassistant/components/airly/translations/es.json @@ -15,8 +15,7 @@ "longitude": "Longitud", "name": "Nombre" }, - "description": "Establecer la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave de la API vaya a https://developer.airly.eu/register", - "title": "Airly" + "description": "Establecer la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave de la API vaya a https://developer.airly.eu/register" } } }, diff --git a/homeassistant/components/airly/translations/et.json b/homeassistant/components/airly/translations/et.json index 6a55262ac6c..ea17ec01ea6 100644 --- a/homeassistant/components/airly/translations/et.json +++ b/homeassistant/components/airly/translations/et.json @@ -15,8 +15,7 @@ "longitude": "Pikkuskraad", "name": "Nimi" }, - "description": "API-v\u00f5tme loomiseks mine aadressile https://developer.airly.eu/register", - "title": "" + "description": "API-v\u00f5tme loomiseks mine aadressile https://developer.airly.eu/register" } } }, diff --git a/homeassistant/components/airly/translations/fr.json b/homeassistant/components/airly/translations/fr.json index 17a7248f7ad..e9134879c00 100644 --- a/homeassistant/components/airly/translations/fr.json +++ b/homeassistant/components/airly/translations/fr.json @@ -15,8 +15,7 @@ "longitude": "Longitude", "name": "Nom" }, - "description": "Pour g\u00e9n\u00e9rer une cl\u00e9 d'API, rendez-vous sur https://developer.airly.eu/register", - "title": "Airly" + "description": "Pour g\u00e9n\u00e9rer une cl\u00e9 d'API, rendez-vous sur https://developer.airly.eu/register" } } }, diff --git a/homeassistant/components/airly/translations/he.json b/homeassistant/components/airly/translations/he.json index 8faa6ac2092..edbf648397f 100644 --- a/homeassistant/components/airly/translations/he.json +++ b/homeassistant/components/airly/translations/he.json @@ -13,8 +13,7 @@ "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", "name": "\u05e9\u05dd" - }, - "title": "\u05d0\u05d5\u05d5\u05e8\u05d9\u05e8\u05d9" + } } } } diff --git a/homeassistant/components/airly/translations/hu.json b/homeassistant/components/airly/translations/hu.json index 04d667f4079..6955c2456cc 100644 --- a/homeassistant/components/airly/translations/hu.json +++ b/homeassistant/components/airly/translations/hu.json @@ -15,8 +15,7 @@ "longitude": "Hossz\u00fas\u00e1g", "name": "Elnevez\u00e9s" }, - "description": "Az API-kulcs l\u00e9trehoz\u00e1s\u00e1hoz keresse fel a https://developer.airly.eu/register webhelyet", - "title": "Airly" + "description": "Az API-kulcs l\u00e9trehoz\u00e1s\u00e1hoz keresse fel a https://developer.airly.eu/register webhelyet" } } }, diff --git a/homeassistant/components/airly/translations/id.json b/homeassistant/components/airly/translations/id.json index bc97db4135a..7e2d7e3930e 100644 --- a/homeassistant/components/airly/translations/id.json +++ b/homeassistant/components/airly/translations/id.json @@ -15,8 +15,7 @@ "longitude": "Bujur", "name": "Nama" }, - "description": "Untuk membuat kunci API, buka https://developer.airly.eu/register", - "title": "Airly" + "description": "Untuk membuat kunci API, buka https://developer.airly.eu/register" } } }, diff --git a/homeassistant/components/airly/translations/it.json b/homeassistant/components/airly/translations/it.json index b96d5c8a74c..d57023faeca 100644 --- a/homeassistant/components/airly/translations/it.json +++ b/homeassistant/components/airly/translations/it.json @@ -15,8 +15,7 @@ "longitude": "Logitudine", "name": "Nome" }, - "description": "Per generare la chiave API, vai su https://developer.airly.eu/register", - "title": "Airly" + "description": "Per generare la chiave API, vai su https://developer.airly.eu/register" } } }, diff --git a/homeassistant/components/airly/translations/ja.json b/homeassistant/components/airly/translations/ja.json index 6835412e6db..6ff5e2216a6 100644 --- a/homeassistant/components/airly/translations/ja.json +++ b/homeassistant/components/airly/translations/ja.json @@ -15,8 +15,7 @@ "longitude": "\u7d4c\u5ea6", "name": "\u540d\u524d" }, - "description": "Airly\u306e\u7a7a\u6c17\u54c1\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.airly.eu/register \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", - "title": "Airly" + "description": "Airly\u306e\u7a7a\u6c17\u54c1\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.airly.eu/register \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } }, diff --git a/homeassistant/components/airly/translations/ko.json b/homeassistant/components/airly/translations/ko.json index 1f9db4a592e..d66bb93dc39 100644 --- a/homeassistant/components/airly/translations/ko.json +++ b/homeassistant/components/airly/translations/ko.json @@ -15,8 +15,7 @@ "longitude": "\uacbd\ub3c4", "name": "\uc774\ub984" }, - "description": "Airly \uacf5\uae30 \ud488\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://developer.airly.eu/register \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694", - "title": "Airly" + "description": "Airly \uacf5\uae30 \ud488\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://developer.airly.eu/register \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694" } } }, diff --git a/homeassistant/components/airly/translations/lb.json b/homeassistant/components/airly/translations/lb.json index dd24ee3066f..32832f52266 100644 --- a/homeassistant/components/airly/translations/lb.json +++ b/homeassistant/components/airly/translations/lb.json @@ -15,8 +15,7 @@ "longitude": "L\u00e4ngegrad", "name": "Numm" }, - "description": "Airly Loft Qualit\u00e9it Integratioun ariichten. Fir een API Schl\u00ebssel z'erstelle gitt op https://developer.airly.eu/register", - "title": "Airly" + "description": "Airly Loft Qualit\u00e9it Integratioun ariichten. Fir een API Schl\u00ebssel z'erstelle gitt op https://developer.airly.eu/register" } } }, diff --git a/homeassistant/components/airly/translations/nl.json b/homeassistant/components/airly/translations/nl.json index 70fbca2074f..7cfa0c5776f 100644 --- a/homeassistant/components/airly/translations/nl.json +++ b/homeassistant/components/airly/translations/nl.json @@ -15,8 +15,7 @@ "longitude": "Lengtegraad", "name": "Naam" }, - "description": "Om een API sleutel te genereren ga naar https://developer.airly.eu/register", - "title": "Airly" + "description": "Om een API sleutel te genereren ga naar https://developer.airly.eu/register" } } }, diff --git a/homeassistant/components/airly/translations/nn.json b/homeassistant/components/airly/translations/nn.json deleted file mode 100644 index 9cf2b5d70fb..00000000000 --- a/homeassistant/components/airly/translations/nn.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "Airly" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/airly/translations/no.json b/homeassistant/components/airly/translations/no.json index 015e95af8f2..f4f87d6d887 100644 --- a/homeassistant/components/airly/translations/no.json +++ b/homeassistant/components/airly/translations/no.json @@ -15,8 +15,7 @@ "longitude": "Lengdegrad", "name": "Navn" }, - "description": "For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://developer.airly.eu/register", - "title": "" + "description": "For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://developer.airly.eu/register" } } }, diff --git a/homeassistant/components/airly/translations/pl.json b/homeassistant/components/airly/translations/pl.json index dd44f2bf635..72d35ec97a7 100644 --- a/homeassistant/components/airly/translations/pl.json +++ b/homeassistant/components/airly/translations/pl.json @@ -15,8 +15,7 @@ "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "name": "Nazwa" }, - "description": "By wygenerowa\u0107 klucz API, przejd\u017a na stron\u0119 https://developer.airly.eu/register", - "title": "Airly" + "description": "By wygenerowa\u0107 klucz API, przejd\u017a na stron\u0119 https://developer.airly.eu/register" } } }, diff --git a/homeassistant/components/airly/translations/pt-BR.json b/homeassistant/components/airly/translations/pt-BR.json index 0e9913b559c..fe19bc8b3a7 100644 --- a/homeassistant/components/airly/translations/pt-BR.json +++ b/homeassistant/components/airly/translations/pt-BR.json @@ -15,8 +15,7 @@ "longitude": "Longitude", "name": "Nome" }, - "description": "Para gerar a chave de API, acesse https://developer.airly.eu/register", - "title": "Airly" + "description": "Para gerar a chave de API, acesse https://developer.airly.eu/register" } } }, diff --git a/homeassistant/components/airly/translations/pt.json b/homeassistant/components/airly/translations/pt.json index 6ebb22b565a..aff2cb2399b 100644 --- a/homeassistant/components/airly/translations/pt.json +++ b/homeassistant/components/airly/translations/pt.json @@ -13,8 +13,7 @@ "latitude": "Latitude", "longitude": "Longitude", "name": "Nome" - }, - "title": "" + } } } } diff --git a/homeassistant/components/airly/translations/ru.json b/homeassistant/components/airly/translations/ru.json index 80c6a98813c..4b159f1e50d 100644 --- a/homeassistant/components/airly/translations/ru.json +++ b/homeassistant/components/airly/translations/ru.json @@ -15,8 +15,7 @@ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://developer.airly.eu/register.", - "title": "Airly" + "description": "\u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://developer.airly.eu/register." } } }, diff --git a/homeassistant/components/airly/translations/sl.json b/homeassistant/components/airly/translations/sl.json index e1c89501394..2debfc1c0ef 100644 --- a/homeassistant/components/airly/translations/sl.json +++ b/homeassistant/components/airly/translations/sl.json @@ -14,8 +14,7 @@ "longitude": "Zemljepisna dol\u017eina", "name": "Ime integracije" }, - "description": "Nastavite Airly integracijo za kakovost zraka. \u010ce \u017eelite ustvariti API klju\u010d pojdite na https://developer.airly.eu/register", - "title": "Airly" + "description": "Nastavite Airly integracijo za kakovost zraka. \u010ce \u017eelite ustvariti API klju\u010d pojdite na https://developer.airly.eu/register" } } }, diff --git a/homeassistant/components/airly/translations/sv.json b/homeassistant/components/airly/translations/sv.json index e47cb5cd328..d182115230b 100644 --- a/homeassistant/components/airly/translations/sv.json +++ b/homeassistant/components/airly/translations/sv.json @@ -14,8 +14,7 @@ "longitude": "Longitud", "name": "Integrationens namn" }, - "description": "Konfigurera integration av luftkvalitet. F\u00f6r att skapa API-nyckel, g\u00e5 till https://developer.airly.eu/register", - "title": "Airly" + "description": "Konfigurera integration av luftkvalitet. F\u00f6r att skapa API-nyckel, g\u00e5 till https://developer.airly.eu/register" } } } diff --git a/homeassistant/components/airly/translations/tr.json b/homeassistant/components/airly/translations/tr.json index 7f5643be2d0..989179b73d8 100644 --- a/homeassistant/components/airly/translations/tr.json +++ b/homeassistant/components/airly/translations/tr.json @@ -15,8 +15,7 @@ "longitude": "Boylam", "name": "Ad" }, - "description": "API anahtar\u0131 olu\u015fturmak i\u00e7in https://developer.airly.eu/register adresine gidin.", - "title": "Airly" + "description": "API anahtar\u0131 olu\u015fturmak i\u00e7in https://developer.airly.eu/register adresine gidin." } } }, diff --git a/homeassistant/components/airly/translations/uk.json b/homeassistant/components/airly/translations/uk.json index 51bcf5195df..08d92e2282d 100644 --- a/homeassistant/components/airly/translations/uk.json +++ b/homeassistant/components/airly/translations/uk.json @@ -15,8 +15,7 @@ "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430" }, - "description": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0441\u0435\u0440\u0432\u0456\u0441\u0443 \u0437 \u0430\u043d\u0430\u043b\u0456\u0437\u0443 \u044f\u043a\u043e\u0441\u0442\u0456 \u043f\u043e\u0432\u0456\u0442\u0440\u044f Airly. \u0429\u043e\u0431 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0437\u0430 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c https://developer.airly.eu/register.", - "title": "Airly" + "description": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0441\u0435\u0440\u0432\u0456\u0441\u0443 \u0437 \u0430\u043d\u0430\u043b\u0456\u0437\u0443 \u044f\u043a\u043e\u0441\u0442\u0456 \u043f\u043e\u0432\u0456\u0442\u0440\u044f Airly. \u0429\u043e\u0431 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0437\u0430 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c https://developer.airly.eu/register." } } }, diff --git a/homeassistant/components/airly/translations/zh-Hant.json b/homeassistant/components/airly/translations/zh-Hant.json index 65f0bf8cde5..9973269facd 100644 --- a/homeassistant/components/airly/translations/zh-Hant.json +++ b/homeassistant/components/airly/translations/zh-Hant.json @@ -15,8 +15,7 @@ "longitude": "\u7d93\u5ea6", "name": "\u540d\u7a31" }, - "description": "\u8acb\u81f3 https://developer.airly.eu/register \u4ee5\u7522\u751f API \u91d1\u9470", - "title": "Airly" + "description": "\u8acb\u81f3 https://developer.airly.eu/register \u4ee5\u7522\u751f API \u91d1\u9470" } } }, diff --git a/homeassistant/components/airnow/translations/ca.json b/homeassistant/components/airnow/translations/ca.json index 9a2b0aa9afc..a4f8de82284 100644 --- a/homeassistant/components/airnow/translations/ca.json +++ b/homeassistant/components/airnow/translations/ca.json @@ -17,10 +17,8 @@ "longitude": "Longitud", "radius": "Radi de l'estaci\u00f3 (milles; opcional)" }, - "description": "Per generar la clau API, v\u00e9s a https://docs.airnowapi.org/account/request/", - "title": "AirNow" + "description": "Per generar la clau API, v\u00e9s a https://docs.airnowapi.org/account/request/" } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/cs.json b/homeassistant/components/airnow/translations/cs.json index d978e44c70a..5268257905e 100644 --- a/homeassistant/components/airnow/translations/cs.json +++ b/homeassistant/components/airnow/translations/cs.json @@ -14,10 +14,8 @@ "api_key": "Kl\u00ed\u010d API", "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka" - }, - "title": "AirNow" + } } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/de.json b/homeassistant/components/airnow/translations/de.json index 3c207a39cce..c42da3491c1 100644 --- a/homeassistant/components/airnow/translations/de.json +++ b/homeassistant/components/airnow/translations/de.json @@ -17,10 +17,8 @@ "longitude": "L\u00e4ngengrad", "radius": "Stationsradius (Meilen; optional)" }, - "description": "Um den API-Schl\u00fcssel zu generieren, besuche https://docs.airnowapi.org/account/request/", - "title": "AirNow" + "description": "Um den API-Schl\u00fcssel zu generieren, besuche https://docs.airnowapi.org/account/request/" } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/el.json b/homeassistant/components/airnow/translations/el.json index caaaafc9e9a..c8e97c5a925 100644 --- a/homeassistant/components/airnow/translations/el.json +++ b/homeassistant/components/airnow/translations/el.json @@ -17,10 +17,8 @@ "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", "radius": "\u0391\u03ba\u03c4\u03af\u03bd\u03b1 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd (\u03bc\u03af\u03bb\u03b9\u03b1, \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)" }, - "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 AirNow \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c0\u03bf\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03ad\u03c1\u03b1. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://docs.airnowapi.org/account/request/", - "title": "AirNow" + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 AirNow \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c0\u03bf\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03ad\u03c1\u03b1. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://docs.airnowapi.org/account/request/" } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/en.json b/homeassistant/components/airnow/translations/en.json index cb3081284b4..07915652847 100644 --- a/homeassistant/components/airnow/translations/en.json +++ b/homeassistant/components/airnow/translations/en.json @@ -17,10 +17,8 @@ "longitude": "Longitude", "radius": "Station Radius (miles; optional)" }, - "description": "To generate API key go to https://docs.airnowapi.org/account/request/", - "title": "AirNow" + "description": "To generate API key go to https://docs.airnowapi.org/account/request/" } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/es-419.json b/homeassistant/components/airnow/translations/es-419.json index 015d7242ef1..5f49ebdb143 100644 --- a/homeassistant/components/airnow/translations/es-419.json +++ b/homeassistant/components/airnow/translations/es-419.json @@ -8,10 +8,8 @@ "data": { "radius": "Radio de la estaci\u00f3n (millas; opcional)" }, - "description": "Configure la integraci\u00f3n de la calidad del aire de AirNow. Para generar la clave de API, vaya a https://docs.airnowapi.org/account/request/", - "title": "AirNow" + "description": "Configure la integraci\u00f3n de la calidad del aire de AirNow. Para generar la clave de API, vaya a https://docs.airnowapi.org/account/request/" } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/es.json b/homeassistant/components/airnow/translations/es.json index d6a228a6e27..b5b915910c5 100644 --- a/homeassistant/components/airnow/translations/es.json +++ b/homeassistant/components/airnow/translations/es.json @@ -17,10 +17,8 @@ "longitude": "Longitud", "radius": "Radio de la estaci\u00f3n (millas; opcional)" }, - "description": "Configurar la integraci\u00f3n de calidad del aire de AirNow. Para generar una clave API, ve a https://docs.airnowapi.org/account/request/", - "title": "AirNow" + "description": "Configurar la integraci\u00f3n de calidad del aire de AirNow. Para generar una clave API, ve a https://docs.airnowapi.org/account/request/" } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/et.json b/homeassistant/components/airnow/translations/et.json index 3bdf8661d36..1d6744e69ae 100644 --- a/homeassistant/components/airnow/translations/et.json +++ b/homeassistant/components/airnow/translations/et.json @@ -17,10 +17,8 @@ "longitude": "Pikkuskraad", "radius": "Jaama raadius (miilid; valikuline)" }, - "description": "API-v\u00f5tme loomiseks mine aadressile https://docs.airnowapi.org/account/request/", - "title": "" + "description": "API-v\u00f5tme loomiseks mine aadressile https://docs.airnowapi.org/account/request/" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/fr.json b/homeassistant/components/airnow/translations/fr.json index 2da7427e1be..ce45573f9bf 100644 --- a/homeassistant/components/airnow/translations/fr.json +++ b/homeassistant/components/airnow/translations/fr.json @@ -17,10 +17,8 @@ "longitude": "Longitude", "radius": "Rayon d'action de la station (en miles, facultatif)" }, - "description": "Pour g\u00e9n\u00e9rer une cl\u00e9 d'API, rendez-vous sur https://docs.airnowapi.org/account/request/", - "title": "AirNow" + "description": "Pour g\u00e9n\u00e9rer une cl\u00e9 d'API, rendez-vous sur https://docs.airnowapi.org/account/request/" } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/hu.json b/homeassistant/components/airnow/translations/hu.json index 52bf8cd63a0..a0d86014768 100644 --- a/homeassistant/components/airnow/translations/hu.json +++ b/homeassistant/components/airnow/translations/hu.json @@ -17,10 +17,8 @@ "longitude": "Hossz\u00fas\u00e1g", "radius": "\u00c1llom\u00e1s sugara (m\u00e9rf\u00f6ld; opcion\u00e1lis)" }, - "description": "Az API-kulcs l\u00e9trehoz\u00e1s\u00e1hoz keresse fel a https://docs.airnowapi.org/account/request/ webhelyet", - "title": "AirNow" + "description": "Az API-kulcs l\u00e9trehoz\u00e1s\u00e1hoz keresse fel a https://docs.airnowapi.org/account/request/ webhelyet" } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/id.json b/homeassistant/components/airnow/translations/id.json index 4d4ac987320..b0beea88cb1 100644 --- a/homeassistant/components/airnow/translations/id.json +++ b/homeassistant/components/airnow/translations/id.json @@ -17,10 +17,8 @@ "longitude": "Bujur", "radius": "Radius Stasiun (mil; opsional)" }, - "description": "Untuk membuat kunci API buka https://docs.airnowapi.org/account/request/", - "title": "AirNow" + "description": "Untuk membuat kunci API buka https://docs.airnowapi.org/account/request/" } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/it.json b/homeassistant/components/airnow/translations/it.json index 77715910c75..d35e4659c90 100644 --- a/homeassistant/components/airnow/translations/it.json +++ b/homeassistant/components/airnow/translations/it.json @@ -17,10 +17,8 @@ "longitude": "Logitudine", "radius": "Raggio stazione (miglia; opzionale)" }, - "description": "Per generare la chiave API vai su https://docs.airnowapi.org/account/request/", - "title": "AirNow" + "description": "Per generare la chiave API vai su https://docs.airnowapi.org/account/request/" } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/ja.json b/homeassistant/components/airnow/translations/ja.json index d535f7fc044..17a1b14e164 100644 --- a/homeassistant/components/airnow/translations/ja.json +++ b/homeassistant/components/airnow/translations/ja.json @@ -17,10 +17,8 @@ "longitude": "\u7d4c\u5ea6", "radius": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u534a\u5f84(\u30de\u30a4\u30eb; \u30aa\u30d7\u30b7\u30e7\u30f3)" }, - "description": "AirNow\u306e\u7a7a\u6c17\u54c1\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://docs.airnowapi.org/account/request/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", - "title": "AirNow" + "description": "AirNow\u306e\u7a7a\u6c17\u54c1\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://docs.airnowapi.org/account/request/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/ko.json b/homeassistant/components/airnow/translations/ko.json index adfbf0be8ed..0364b7642a6 100644 --- a/homeassistant/components/airnow/translations/ko.json +++ b/homeassistant/components/airnow/translations/ko.json @@ -17,10 +17,8 @@ "longitude": "\uacbd\ub3c4", "radius": "\uce21\uc815 \uc2a4\ud14c\uc774\uc158 \ubc18\uacbd (\ub9c8\uc77c; \uc120\ud0dd \uc0ac\ud56d)" }, - "description": "AirNow \uacf5\uae30 \ud488\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://docs.airnowapi.org/account/request \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694", - "title": "AirNow" + "description": "AirNow \uacf5\uae30 \ud488\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://docs.airnowapi.org/account/request \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694" } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/lb.json b/homeassistant/components/airnow/translations/lb.json index a62bd0bf478..90ce6b3a4b2 100644 --- a/homeassistant/components/airnow/translations/lb.json +++ b/homeassistant/components/airnow/translations/lb.json @@ -15,10 +15,8 @@ "api_key": "API Schl\u00ebssel", "latitude": "L\u00e4ngegrad", "longitude": "Breedegrag" - }, - "title": "AirNow" + } } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/nl.json b/homeassistant/components/airnow/translations/nl.json index 5e5e33bf93d..0ba6ce2a965 100644 --- a/homeassistant/components/airnow/translations/nl.json +++ b/homeassistant/components/airnow/translations/nl.json @@ -17,10 +17,8 @@ "longitude": "Lengtegraad", "radius": "Stationsradius (mijl; optioneel)" }, - "description": "Om een API sleutel te genereren ga naar https://docs.airnowapi.org/account/request/", - "title": "AirNow" + "description": "Om een API sleutel te genereren ga naar https://docs.airnowapi.org/account/request/" } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/no.json b/homeassistant/components/airnow/translations/no.json index b56b7096e2b..ee2bf44672c 100644 --- a/homeassistant/components/airnow/translations/no.json +++ b/homeassistant/components/airnow/translations/no.json @@ -17,10 +17,8 @@ "longitude": "Lengdegrad", "radius": "Stasjonsradius (miles; valgfritt)" }, - "description": "For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://docs.airnowapi.org/account/request/", - "title": "" + "description": "For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://docs.airnowapi.org/account/request/" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/pl.json b/homeassistant/components/airnow/translations/pl.json index f29a48dc47b..bbf31af597a 100644 --- a/homeassistant/components/airnow/translations/pl.json +++ b/homeassistant/components/airnow/translations/pl.json @@ -17,10 +17,8 @@ "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "radius": "Promie\u0144 od stacji (w milach; opcjonalnie)" }, - "description": "Aby wygenerowa\u0107 klucz API, przejd\u017a do https://docs.airnowapi.org/account/request/", - "title": "AirNow" + "description": "Aby wygenerowa\u0107 klucz API, przejd\u017a do https://docs.airnowapi.org/account/request/" } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/pt-BR.json b/homeassistant/components/airnow/translations/pt-BR.json index c1bda0098cd..a3450355e0c 100644 --- a/homeassistant/components/airnow/translations/pt-BR.json +++ b/homeassistant/components/airnow/translations/pt-BR.json @@ -17,10 +17,8 @@ "longitude": "Longitude", "radius": "Raio da Esta\u00e7\u00e3o (milhas; opcional)" }, - "description": "Para gerar a chave de API, acesse https://docs.airnowapi.org/account/request/", - "title": "AirNow" + "description": "Para gerar a chave de API, acesse https://docs.airnowapi.org/account/request/" } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/pt.json b/homeassistant/components/airnow/translations/pt.json index 7b9af9e8a23..f846047c307 100644 --- a/homeassistant/components/airnow/translations/pt.json +++ b/homeassistant/components/airnow/translations/pt.json @@ -14,10 +14,8 @@ "api_key": "Chave da API", "latitude": "Latitude", "longitude": "Longitude" - }, - "title": "" + } } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/ru.json b/homeassistant/components/airnow/translations/ru.json index c5a9137f4c7..fb8516e6529 100644 --- a/homeassistant/components/airnow/translations/ru.json +++ b/homeassistant/components/airnow/translations/ru.json @@ -17,10 +17,8 @@ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "radius": "\u0420\u0430\u0434\u0438\u0443\u0441 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 (\u0432 \u043c\u0438\u043b\u044f\u0445; \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" }, - "description": "\u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://docs.airnowapi.org/account/request/.", - "title": "AirNow" + "description": "\u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://docs.airnowapi.org/account/request/." } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/tr.json b/homeassistant/components/airnow/translations/tr.json index 1f80d7805da..25c3cf15d41 100644 --- a/homeassistant/components/airnow/translations/tr.json +++ b/homeassistant/components/airnow/translations/tr.json @@ -17,10 +17,8 @@ "longitude": "Boylam", "radius": "\u0130stasyon Yar\u0131\u00e7ap\u0131 (mil; iste\u011fe ba\u011fl\u0131)" }, - "description": "API anahtar\u0131 olu\u015fturmak i\u00e7in https://docs.airnowapi.org/account/request/ adresine gidin.", - "title": "AirNow" + "description": "API anahtar\u0131 olu\u015fturmak i\u00e7in https://docs.airnowapi.org/account/request/ adresine gidin." } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/uk.json b/homeassistant/components/airnow/translations/uk.json index bb872123f54..db8c740234f 100644 --- a/homeassistant/components/airnow/translations/uk.json +++ b/homeassistant/components/airnow/translations/uk.json @@ -17,10 +17,8 @@ "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", "radius": "\u0420\u0430\u0434\u0456\u0443\u0441 \u0441\u0442\u0430\u043d\u0446\u0456\u0457 (\u043c\u0438\u043b\u0456; \u043d\u0435\u043e\u0431\u043e\u0432\u2019\u044f\u0437\u043a\u043e\u0432\u043e)" }, - "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u044f\u043a\u043e\u0441\u0442\u0456 \u043f\u043e\u0432\u0456\u0442\u0440\u044f AirNow. \u0429\u043e\u0431 \u0437\u0433\u0435\u043d\u0435\u0440\u0443\u0432\u0430\u0442\u0438 \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043d\u0430 \u0441\u0442\u043e\u0440\u0456\u043d\u043a\u0443 https://docs.airnowapi.org/account/request/", - "title": "AirNow" + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u044f\u043a\u043e\u0441\u0442\u0456 \u043f\u043e\u0432\u0456\u0442\u0440\u044f AirNow. \u0429\u043e\u0431 \u0437\u0433\u0435\u043d\u0435\u0440\u0443\u0432\u0430\u0442\u0438 \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043d\u0430 \u0441\u0442\u043e\u0440\u0456\u043d\u043a\u0443 https://docs.airnowapi.org/account/request/" } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/zh-Hant.json b/homeassistant/components/airnow/translations/zh-Hant.json index bb18a5d8975..e257e43a3e6 100644 --- a/homeassistant/components/airnow/translations/zh-Hant.json +++ b/homeassistant/components/airnow/translations/zh-Hant.json @@ -17,10 +17,8 @@ "longitude": "\u7d93\u5ea6", "radius": "\u89c0\u6e2c\u7ad9\u534a\u5f91\uff08\u82f1\u91cc\uff1b\u9078\u9805\uff09" }, - "description": "\u8acb\u81f3 https://docs.airnowapi.org/account/request/ \u4ee5\u7522\u751f API \u91d1\u9470", - "title": "AirNow" + "description": "\u8acb\u81f3 https://docs.airnowapi.org/account/request/ \u4ee5\u7522\u751f API \u91d1\u9470" } } - }, - "title": "AirNow" + } } \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/cs.json b/homeassistant/components/aladdin_connect/translations/cs.json new file mode 100644 index 00000000000..a3144ba2f55 --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "title": "Znovu ov\u011b\u0159it integraci" + }, + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/he.json b/homeassistant/components/aladdin_connect/translations/he.json new file mode 100644 index 00000000000..a26c20aa4c7 --- /dev/null +++ b/homeassistant/components/aladdin_connect/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", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, + "user": { + "data": { + "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/amberelectric/translations/bg.json b/homeassistant/components/amberelectric/translations/bg.json index 5cfcc05b133..6f035fae4e6 100644 --- a/homeassistant/components/amberelectric/translations/bg.json +++ b/homeassistant/components/amberelectric/translations/bg.json @@ -1,12 +1,8 @@ { "config": { "step": { - "site": { - "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" + "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" } } } diff --git a/homeassistant/components/amberelectric/translations/ca.json b/homeassistant/components/amberelectric/translations/ca.json index cf9bca64df6..208c7fb9f7c 100644 --- a/homeassistant/components/amberelectric/translations/ca.json +++ b/homeassistant/components/amberelectric/translations/ca.json @@ -6,16 +6,14 @@ "site_name": "Nom del lloc", "site_nmi": "NMI del lloc" }, - "description": "Selecciona l'NMI del lloc que vulguis afegir", - "title": "Amber Electric" + "description": "Selecciona l'NMI del lloc que vulguis afegir" }, "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" + "description": "Ves a {api_url} per generar una clau API" } } } diff --git a/homeassistant/components/amberelectric/translations/de.json b/homeassistant/components/amberelectric/translations/de.json index 2143795f479..34ce233fed8 100644 --- a/homeassistant/components/amberelectric/translations/de.json +++ b/homeassistant/components/amberelectric/translations/de.json @@ -6,16 +6,14 @@ "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" + "description": "W\u00e4hle die NMI des Standorts, den du hinzuf\u00fcgen m\u00f6chtest" }, "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" + "description": "Gehe zu {api_url}, um einen API-Schl\u00fcssel zu generieren" } } } diff --git a/homeassistant/components/amberelectric/translations/el.json b/homeassistant/components/amberelectric/translations/el.json index b6e939ea466..3dc8fc9dd78 100644 --- a/homeassistant/components/amberelectric/translations/el.json +++ b/homeassistant/components/amberelectric/translations/el.json @@ -6,16 +6,14 @@ "site_name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1\u03c2", "site_nmi": "\u03a4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 NMI" }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf NMI \u03c4\u03b7\u03c2 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5.", - "title": "Amber Electric" + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf NMI \u03c4\u03b7\u03c2 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5." }, "user": { "data": { "api_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API", "site_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1\u03c2" }, - "description": "\u039c\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf {api_url} \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API", - "title": "Amber Electric" + "description": "\u039c\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf {api_url} \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API" } } } diff --git a/homeassistant/components/amberelectric/translations/en.json b/homeassistant/components/amberelectric/translations/en.json index 60c7caae456..b4d30925aa7 100644 --- a/homeassistant/components/amberelectric/translations/en.json +++ b/homeassistant/components/amberelectric/translations/en.json @@ -6,16 +6,14 @@ "site_name": "Site Name", "site_nmi": "Site NMI" }, - "description": "Select the NMI of the site you would like to add", - "title": "Amber Electric" + "description": "Select the NMI of the site you would like to add" }, "user": { "data": { "api_token": "API Token", "site_id": "Site ID" }, - "description": "Go to {api_url} to generate an API key", - "title": "Amber Electric" + "description": "Go to {api_url} to generate an API key" } } } diff --git a/homeassistant/components/amberelectric/translations/es.json b/homeassistant/components/amberelectric/translations/es.json index d4b09e69930..18af819d5d1 100644 --- a/homeassistant/components/amberelectric/translations/es.json +++ b/homeassistant/components/amberelectric/translations/es.json @@ -6,16 +6,14 @@ "site_name": "Nombre del sitio", "site_nmi": "Sitio NMI" }, - "description": "Seleccione el NMI del sitio que le gustar\u00eda agregar", - "title": "Amber Electric" + "description": "Seleccione el NMI del sitio que le gustar\u00eda agregar" }, "user": { "data": { "api_token": "API Token", "site_id": "ID del sitio" }, - "description": "Ve a {api_url} para generar una clave API", - "title": "Amber Electric" + "description": "Ve a {api_url} para generar una clave API" } } } diff --git a/homeassistant/components/amberelectric/translations/et.json b/homeassistant/components/amberelectric/translations/et.json index 05a7e6c6dc2..e48f6f2a749 100644 --- a/homeassistant/components/amberelectric/translations/et.json +++ b/homeassistant/components/amberelectric/translations/et.json @@ -6,16 +6,14 @@ "site_name": "Saidi nimi", "site_nmi": "Saidi NMI" }, - "description": "Vali lisatava saidi NMI", - "title": "Amber Electric" + "description": "Vali lisatava saidi NMI" }, "user": { "data": { "api_token": "API v\u00f5ti", "site_id": "Saidi ID" }, - "description": "API-v\u00f5tme saamiseks ava {api_url}.", - "title": "Amber Electric" + "description": "API-v\u00f5tme saamiseks ava {api_url}." } } } diff --git a/homeassistant/components/amberelectric/translations/fr.json b/homeassistant/components/amberelectric/translations/fr.json index 487ceff33f3..a5b1164924c 100644 --- a/homeassistant/components/amberelectric/translations/fr.json +++ b/homeassistant/components/amberelectric/translations/fr.json @@ -6,16 +6,14 @@ "site_name": "Nom du site", "site_nmi": "Site NMI" }, - "description": "S\u00e9lectionnez le NMI du site que vous souhaitez ajouter", - "title": "Amber Electrique" + "description": "S\u00e9lectionnez le NMI du site que vous souhaitez ajouter" }, "user": { "data": { "api_token": "Jeton d'API", "site_id": "ID du site" }, - "description": "Acc\u00e9dez \u00e0 {api_url} pour g\u00e9n\u00e9rer une cl\u00e9 API", - "title": "Amber Electrique" + "description": "Acc\u00e9dez \u00e0 {api_url} pour g\u00e9n\u00e9rer une cl\u00e9 API" } } } diff --git a/homeassistant/components/amberelectric/translations/hu.json b/homeassistant/components/amberelectric/translations/hu.json index 9811f5a5f8f..2d361e1e76f 100644 --- a/homeassistant/components/amberelectric/translations/hu.json +++ b/homeassistant/components/amberelectric/translations/hu.json @@ -6,16 +6,14 @@ "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" + "description": "V\u00e1lassza ki a hozz\u00e1adni k\u00edv\u00e1nt hely NMI-j\u00e9t." }, "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" + "description": "API-kulcs gener\u00e1l\u00e1s\u00e1hoz l\u00e1togasson el ide: {api_url}" } } } diff --git a/homeassistant/components/amberelectric/translations/id.json b/homeassistant/components/amberelectric/translations/id.json index 4920ee7b177..a88fca7c520 100644 --- a/homeassistant/components/amberelectric/translations/id.json +++ b/homeassistant/components/amberelectric/translations/id.json @@ -6,16 +6,14 @@ "site_name": "Nama Situs", "site_nmi": "Situs NMI" }, - "description": "Pilih NMI dari situs yang ingin ditambahkan", - "title": "Amber Electric" + "description": "Pilih NMI dari situs yang ingin ditambahkan" }, "user": { "data": { "api_token": "Token API", "site_id": "ID Site" }, - "description": "Buka {api_url} untuk membuat kunci API", - "title": "Amber Electric" + "description": "Buka {api_url} untuk membuat kunci API" } } } diff --git a/homeassistant/components/amberelectric/translations/it.json b/homeassistant/components/amberelectric/translations/it.json index 5b061561954..f181f549fff 100644 --- a/homeassistant/components/amberelectric/translations/it.json +++ b/homeassistant/components/amberelectric/translations/it.json @@ -6,16 +6,14 @@ "site_name": "Nome del sito", "site_nmi": "Sito NMI" }, - "description": "Seleziona l'NMI del sito che desideri aggiungere", - "title": "Amber Electric" + "description": "Seleziona l'NMI del sito che desideri aggiungere" }, "user": { "data": { "api_token": "Token API", "site_id": "ID sito" }, - "description": "Vai su {api_url} per generare una chiave API", - "title": "Amber Electric" + "description": "Vai su {api_url} per generare una chiave API" } } } diff --git a/homeassistant/components/amberelectric/translations/ja.json b/homeassistant/components/amberelectric/translations/ja.json index 1fc5f6c58c1..0c061b26112 100644 --- a/homeassistant/components/amberelectric/translations/ja.json +++ b/homeassistant/components/amberelectric/translations/ja.json @@ -6,16 +6,14 @@ "site_name": "\u30b5\u30a4\u30c8\u540d", "site_nmi": "\u30b5\u30a4\u30c8NMI" }, - "description": "\u8ffd\u52a0\u3057\u305f\u3044\u30b5\u30a4\u30c8\u306eNMI\u3092\u9078\u629e", - "title": "Amber Electric" + "description": "\u8ffd\u52a0\u3057\u305f\u3044\u30b5\u30a4\u30c8\u306eNMI\u3092\u9078\u629e" }, "user": { "data": { "api_token": "API\u30c8\u30fc\u30af\u30f3", "site_id": "\u30b5\u30a4\u30c8ID" }, - "description": "API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u305f\u3081\u306b {api_url} \u306b\u30a2\u30af\u30bb\u30b9\u3057\u307e\u3059", - "title": "Amber Electric" + "description": "API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u305f\u3081\u306b {api_url} \u306b\u30a2\u30af\u30bb\u30b9\u3057\u307e\u3059" } } } diff --git a/homeassistant/components/amberelectric/translations/nl.json b/homeassistant/components/amberelectric/translations/nl.json index a874c12f283..263e94962a0 100644 --- a/homeassistant/components/amberelectric/translations/nl.json +++ b/homeassistant/components/amberelectric/translations/nl.json @@ -6,16 +6,14 @@ "site_name": "Sitenaam", "site_nmi": "Site NMI" }, - "description": "Selecteer de NMI van de site die u wilt toevoegen", - "title": "Amber Electric" + "description": "Selecteer de NMI van de site die u wilt toevoegen" }, "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" + "description": "Ga naar {api_url} om een API sleutel aan te maken" } } } diff --git a/homeassistant/components/amberelectric/translations/no.json b/homeassistant/components/amberelectric/translations/no.json index 90d4bd930b9..74df9323ce5 100644 --- a/homeassistant/components/amberelectric/translations/no.json +++ b/homeassistant/components/amberelectric/translations/no.json @@ -6,16 +6,14 @@ "site_name": "Side navn", "site_nmi": "Nettsted NMI" }, - "description": "Velg NMI for nettstedet du vil legge til", - "title": "Amber Electric" + "description": "Velg NMI for nettstedet du vil legge til" }, "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" + "description": "G\u00e5 til {api_url} \u00e5 generere en API -n\u00f8kkel" } } } diff --git a/homeassistant/components/amberelectric/translations/pl.json b/homeassistant/components/amberelectric/translations/pl.json index 1e9b66e3c3c..2810149273f 100644 --- a/homeassistant/components/amberelectric/translations/pl.json +++ b/homeassistant/components/amberelectric/translations/pl.json @@ -6,16 +6,14 @@ "site_name": "Nazwa obiektu", "site_nmi": "Numer identyfikacyjny (NMI) obiektu" }, - "description": "Wybierz NMI obiektu, kt\u00f3ry chcesz doda\u0107", - "title": "Amber Electric" + "description": "Wybierz NMI obiektu, kt\u00f3ry chcesz doda\u0107" }, "user": { "data": { "api_token": "Token API", "site_id": "Identyfikator obiektu" }, - "description": "Przejd\u017a do {api_url}, aby wygenerowa\u0107 klucz API", - "title": "Amber Electric" + "description": "Przejd\u017a do {api_url}, aby wygenerowa\u0107 klucz API" } } } diff --git a/homeassistant/components/amberelectric/translations/pt-BR.json b/homeassistant/components/amberelectric/translations/pt-BR.json index 6e8eb4d0ac8..35541dbf290 100644 --- a/homeassistant/components/amberelectric/translations/pt-BR.json +++ b/homeassistant/components/amberelectric/translations/pt-BR.json @@ -6,16 +6,14 @@ "site_name": "Nome do site", "site_nmi": "Site NMI" }, - "description": "Selecione o NMI do site que voc\u00ea gostaria de adicionar", - "title": "\u00c2mbar el\u00e9trico" + "description": "Selecione o NMI do site que voc\u00ea gostaria de adicionar" }, "user": { "data": { "api_token": "Token de API", "site_id": "ID do site" }, - "description": "V\u00e1 para {api_url} para gerar uma chave de API", - "title": "\u00c2mbar el\u00e9trico" + "description": "V\u00e1 para {api_url} para gerar uma chave de API" } } } diff --git a/homeassistant/components/amberelectric/translations/ru.json b/homeassistant/components/amberelectric/translations/ru.json index 4b8caee72ee..793f8bcae9d 100644 --- a/homeassistant/components/amberelectric/translations/ru.json +++ b/homeassistant/components/amberelectric/translations/ru.json @@ -6,16 +6,14 @@ "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" + "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" }, "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" + "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." } } } diff --git a/homeassistant/components/amberelectric/translations/tr.json b/homeassistant/components/amberelectric/translations/tr.json index 274a347e578..393b2cf08ee 100644 --- a/homeassistant/components/amberelectric/translations/tr.json +++ b/homeassistant/components/amberelectric/translations/tr.json @@ -6,16 +6,14 @@ "site_name": "Site Ad\u0131", "site_nmi": "Site NMI" }, - "description": "Eklemek istedi\u011finiz sitenin NMI'sini se\u00e7in", - "title": "Amber Electric" + "description": "Eklemek istedi\u011finiz sitenin NMI'sini se\u00e7in" }, "user": { "data": { "api_token": "API Anahtar\u0131", "site_id": "Site Kimli\u011fi" }, - "description": "API anahtar\u0131 olu\u015fturmak i\u00e7in {api_url} konumuna gidin", - "title": "Amber Electric" + "description": "API anahtar\u0131 olu\u015fturmak i\u00e7in {api_url} konumuna gidin" } } } diff --git a/homeassistant/components/amberelectric/translations/zh-Hant.json b/homeassistant/components/amberelectric/translations/zh-Hant.json index c2cbef2778b..42b671d3530 100644 --- a/homeassistant/components/amberelectric/translations/zh-Hant.json +++ b/homeassistant/components/amberelectric/translations/zh-Hant.json @@ -6,16 +6,14 @@ "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" + "description": "\u9078\u64c7\u6240\u8981\u65b0\u589e\u7684\u4f4d\u5740 NMI" }, "user": { "data": { "api_token": "API \u6b0a\u6756", "site_id": "\u4f4d\u5740 ID" }, - "description": "\u9023\u7dda\u81f3 {api_url} \u4ee5\u7522\u751f API \u91d1\u9470", - "title": "Amber Electric" + "description": "\u9023\u7dda\u81f3 {api_url} \u4ee5\u7522\u751f API \u91d1\u9470" } } } diff --git a/homeassistant/components/androidtv/translations/bg.json b/homeassistant/components/androidtv/translations/bg.json index 9dd3bde62ad..f912f17d257 100644 --- a/homeassistant/components/androidtv/translations/bg.json +++ b/homeassistant/components/androidtv/translations/bg.json @@ -13,8 +13,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" - }, - "title": "Android TV" + } } } } diff --git a/homeassistant/components/androidtv/translations/ca.json b/homeassistant/components/androidtv/translations/ca.json index 3fccdc45771..d640a250fb0 100644 --- a/homeassistant/components/androidtv/translations/ca.json +++ b/homeassistant/components/androidtv/translations/ca.json @@ -20,9 +20,7 @@ "device_class": "Tipus de dispositiu", "host": "Amfitri\u00f3", "port": "Port" - }, - "description": "Estableix els par\u00e0metres necessaris per connectar al teu dispositiu Android TV", - "title": "Android TV" + } } } }, @@ -49,8 +47,7 @@ "state_detection_rules": "Configura les regles de detecci\u00f3 d'estat", "turn_off_command": "Comanda d'apagada de shell ADB (deixa-ho buit per utilitzar la predeterminada)", "turn_on_command": "Comanda d'engegada de shell ADB (deixa-ho buit per utilitzar la predeterminada)" - }, - "title": "Opcions d'Android TV" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/de.json b/homeassistant/components/androidtv/translations/de.json index e69e9ac2183..55a3b75d76c 100644 --- a/homeassistant/components/androidtv/translations/de.json +++ b/homeassistant/components/androidtv/translations/de.json @@ -20,9 +20,7 @@ "device_class": "Der Typ des Ger\u00e4ts", "host": "Host", "port": "Port" - }, - "description": "Stelle die erforderlichen Parameter f\u00fcr die Verbindung mit deinem Android TV-Ger\u00e4t ein", - "title": "Android TV" + } } } }, @@ -49,8 +47,7 @@ "state_detection_rules": "Regeln zur Statuserkennung konfigurieren", "turn_off_command": "ADB-Shell-Abschaltbefehl (f\u00fcr Standard leer lassen)", "turn_on_command": "ADB-Shell-Einschaltbefehl (f\u00fcr Standard leer lassen)" - }, - "title": "Android TV-Optionen" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/el.json b/homeassistant/components/androidtv/translations/el.json index 67db15ff170..a831586fc41 100644 --- a/homeassistant/components/androidtv/translations/el.json +++ b/homeassistant/components/androidtv/translations/el.json @@ -20,9 +20,7 @@ "device_class": "\u039f \u03c4\u03cd\u03c0\u03bf\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", "port": "\u0398\u03cd\u03c1\u03b1" - }, - "description": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b5\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2 Android TV", - "title": "Android TV" + } } } }, @@ -49,8 +47,7 @@ "state_detection_rules": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ba\u03b1\u03bd\u03cc\u03bd\u03c9\u03bd \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2", "turn_off_command": "\u0395\u03bd\u03c4\u03bf\u03bb\u03ae \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03ba\u03b5\u03bb\u03cd\u03c6\u03bf\u03c5\u03c2 ADB (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae)", "turn_on_command": "\u0395\u03bd\u03c4\u03bf\u03bb\u03ae \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03b5\u03bb\u03cd\u03c6\u03bf\u03c5\u03c2 ADB (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae)" - }, - "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 Android TV" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/en.json b/homeassistant/components/androidtv/translations/en.json index f3232b4b229..638ba7fbcd5 100644 --- a/homeassistant/components/androidtv/translations/en.json +++ b/homeassistant/components/androidtv/translations/en.json @@ -20,9 +20,7 @@ "device_class": "The type of device", "host": "Host", "port": "Port" - }, - "description": "Set required parameters to connect to your Android TV device", - "title": "Android TV" + } } } }, @@ -49,8 +47,7 @@ "state_detection_rules": "Configure state detection rules", "turn_off_command": "ADB shell turn off command (leave empty for default)", "turn_on_command": "ADB shell turn on command (leave empty for default)" - }, - "title": "Android TV Options" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/es-419.json b/homeassistant/components/androidtv/translations/es-419.json index 0e97b8c1265..d8f04b2e7c4 100644 --- a/homeassistant/components/androidtv/translations/es-419.json +++ b/homeassistant/components/androidtv/translations/es-419.json @@ -7,9 +7,7 @@ "adb_server_port": "Puerto del servidor ADB", "adbkey": "Ruta a su archivo de clave ADB (d\u00e9jelo en blanco para generarlo autom\u00e1ticamente)", "device_class": "El tipo de dispositivo" - }, - "description": "Establezca los par\u00e1metros requeridos para conectarse a su dispositivo Android TV", - "title": "Android TV" + } } } }, @@ -36,8 +34,7 @@ "state_detection_rules": "Configurar reglas de detecci\u00f3n de estado", "turn_off_command": "Comando de apagado de shell ADB (d\u00e9jelo vac\u00edo por defecto)", "turn_on_command": "Comando de activaci\u00f3n de shell ADB (d\u00e9jelo vac\u00edo por defecto)" - }, - "title": "Opciones de Android TV" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/es.json b/homeassistant/components/androidtv/translations/es.json index 333910a2b64..74c7484257c 100644 --- a/homeassistant/components/androidtv/translations/es.json +++ b/homeassistant/components/androidtv/translations/es.json @@ -20,9 +20,7 @@ "device_class": "Tipo de dispositivo", "host": "Host", "port": "Puerto" - }, - "description": "Establezca los par\u00e1metros necesarios para conectarse a su Android TV", - "title": "Android TV" + } } } }, @@ -49,8 +47,7 @@ "state_detection_rules": "Configurar reglas de detecci\u00f3n de estado", "turn_off_command": "Comando de apagado del shell de ADB (dejar vac\u00edo por defecto)", "turn_on_command": "Comando de activaci\u00f3n del shell ADB (dejar vac\u00edo por defecto)" - }, - "title": "Opciones de Android TV" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/et.json b/homeassistant/components/androidtv/translations/et.json index 292b01fa849..1704f450482 100644 --- a/homeassistant/components/androidtv/translations/et.json +++ b/homeassistant/components/androidtv/translations/et.json @@ -20,9 +20,7 @@ "device_class": "Seadme t\u00fc\u00fcp", "host": "Host", "port": "Port" - }, - "description": "Seadista Android TV seadmega \u00fchenduse loomiseks vajalikud parameetrid", - "title": "Android TV" + } } } }, @@ -49,8 +47,7 @@ "state_detection_rules": "M\u00e4\u00e4ra oleku tuvastamise reeglid", "turn_off_command": "ADB shell turn off k\u00e4sk (vaikimisi j\u00e4ta t\u00fchjaks)", "turn_on_command": "ADB shell turn on k\u00e4sk (vaikimisi j\u00e4ta t\u00fchjaks)" - }, - "title": "Android TV suvandid" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/fr.json b/homeassistant/components/androidtv/translations/fr.json index ea79fddb98a..76124671f2c 100644 --- a/homeassistant/components/androidtv/translations/fr.json +++ b/homeassistant/components/androidtv/translations/fr.json @@ -20,9 +20,7 @@ "device_class": "Le type d'appareil", "host": "H\u00f4te", "port": "Port" - }, - "description": "D\u00e9finissez les param\u00e8tres requis pour vous connecter \u00e0 votre appareil Android TV", - "title": "T\u00e9l\u00e9vision Android" + } } } }, @@ -49,8 +47,7 @@ "state_detection_rules": "Configurer les r\u00e8gles de d\u00e9tection d'\u00e9tat", "turn_off_command": "Commande de d\u00e9sactivation du shell ADB (laisser vide par d\u00e9faut)", "turn_on_command": "Commande d'activation du shell ADB (laisser vide par d\u00e9faut)" - }, - "title": "Options de t\u00e9l\u00e9vision Android" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/he.json b/homeassistant/components/androidtv/translations/he.json index 81870f85c75..557e868298f 100644 --- a/homeassistant/components/androidtv/translations/he.json +++ b/homeassistant/components/androidtv/translations/he.json @@ -20,9 +20,7 @@ "device_class": "\u05e1\u05d5\u05d2 \u05d4\u05d4\u05ea\u05e7\u05df", "host": "\u05de\u05d0\u05e8\u05d7", "port": "\u05e4\u05ea\u05d7\u05d4" - }, - "description": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05d4\u05e4\u05e8\u05de\u05d8\u05e8\u05d9\u05dd \u05d4\u05e0\u05d3\u05e8\u05e9\u05d9\u05dd \u05db\u05d3\u05d9 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d9\u05ea \u05d4\u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3 \u05e9\u05dc\u05da", - "title": "\u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05ea \u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3" + } } } }, @@ -49,8 +47,7 @@ "state_detection_rules": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05db\u05dc\u05dc\u05d9 \u05d6\u05d9\u05d4\u05d5\u05d9 \u05de\u05e6\u05d1\u05d9\u05dd", "turn_off_command": "\u05d4\u05e4\u05e7\u05d5\u05d3\u05d4 \u05db\u05d9\u05d1\u05d5\u05d9 \u05de\u05e2\u05d8\u05e4\u05ea ADB (\u05d4\u05e9\u05d0\u05e8 \u05e8\u05d9\u05e7\u05d4 \u05db\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc)", "turn_on_command": "\u05d4\u05e4\u05e7\u05d5\u05d3\u05d4 \u05d4\u05e4\u05e2\u05dc \u05de\u05e2\u05d8\u05e4\u05ea ADB (\u05d4\u05e9\u05d0\u05e8 \u05e8\u05d9\u05e7\u05d4 \u05db\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc)" - }, - "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05ea \u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/hu.json b/homeassistant/components/androidtv/translations/hu.json index 3b75153a351..98c82a9779b 100644 --- a/homeassistant/components/androidtv/translations/hu.json +++ b/homeassistant/components/androidtv/translations/hu.json @@ -20,9 +20,7 @@ "device_class": "Az eszk\u00f6z t\u00edpusa", "host": "C\u00edm", "port": "Port" - }, - "description": "Az Android TV k\u00e9sz\u00fcl\u00e9khez val\u00f3 csatlakoz\u00e1shoz sz\u00fcks\u00e9ges param\u00e9terek be\u00e1ll\u00edt\u00e1sa", - "title": "Android TV" + } } } }, @@ -49,8 +47,7 @@ "state_detection_rules": "\u00c1llapotfelismer\u00e9si szab\u00e1lyok konfigur\u00e1l\u00e1sa", "turn_off_command": "ADB shell kikapcsol\u00e1si parancs (alap\u00e9rtelmez\u00e9s szerint hagyja \u00fcresen)", "turn_on_command": "ADB shell bekapcsol\u00e1si parancs (alap\u00e9rtelmez\u00e9s szerint hagyja \u00fcresen)" - }, - "title": "Android TV be\u00e1ll\u00edt\u00e1sok" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/id.json b/homeassistant/components/androidtv/translations/id.json index 2b754860c5d..2c81a6b2b96 100644 --- a/homeassistant/components/androidtv/translations/id.json +++ b/homeassistant/components/androidtv/translations/id.json @@ -20,9 +20,7 @@ "device_class": "Jenis perangkat", "host": "Host", "port": "Port" - }, - "description": "Setel parameter yang diperlukan untuk terhubung ke perangkat Android TV Anda", - "title": "Android TV" + } } } }, @@ -49,8 +47,7 @@ "state_detection_rules": "Konfigurasikan aturan deteksi status", "turn_off_command": "Perintah mematikan shell ADB (kosongkan untuk default)", "turn_on_command": "Perintah nyalakan shell ADB (kosongkan untuk default)" - }, - "title": "Opsi Android TV" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/it.json b/homeassistant/components/androidtv/translations/it.json index ce6d3c92bca..91f56c48853 100644 --- a/homeassistant/components/androidtv/translations/it.json +++ b/homeassistant/components/androidtv/translations/it.json @@ -20,9 +20,7 @@ "device_class": "Il tipo di dispositivo", "host": "Host", "port": "Porta" - }, - "description": "Imposta i parametri richiesti per connetterti al tuo dispositivo Android TV", - "title": "Android TV" + } } } }, @@ -49,8 +47,7 @@ "state_detection_rules": "Configura le regole di rilevamento dello stato", "turn_off_command": "Comando di disattivazione della shell ADB (lascia vuoto per impostazione predefinita)", "turn_on_command": "Comando di attivazione della shell ADB (lascia vuoto per impostazione predefinita)" - }, - "title": "Opzioni Android TV" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/ja.json b/homeassistant/components/androidtv/translations/ja.json index 365a6078366..26b83a2643a 100644 --- a/homeassistant/components/androidtv/translations/ja.json +++ b/homeassistant/components/androidtv/translations/ja.json @@ -20,9 +20,7 @@ "device_class": "\u30c7\u30d0\u30a4\u30b9\u306e\u7a2e\u985e", "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8" - }, - "description": "Android TV\u30c7\u30d0\u30a4\u30b9\u306b\u63a5\u7d9a\u3059\u308b\u305f\u3081\u306b\u5fc5\u8981\u306a\u30d1\u30e9\u30e1\u30fc\u30bf\u30fc\u3092\u8a2d\u5b9a\u3057\u307e\u3059", - "title": "Android TV" + } } } }, @@ -49,8 +47,7 @@ "state_detection_rules": "\u72b6\u614b\u691c\u51fa\u30eb\u30fc\u30eb\u3092\u8a2d\u5b9a", "turn_off_command": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306eturn_off\u30b3\u30de\u30f3\u30c9\u3092\u4e0a\u66f8\u304d\u3059\u308bADB\u30b7\u30a7\u30eb\u30b3\u30de\u30f3\u30c9", "turn_on_command": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306eturn_on\u30b3\u30de\u30f3\u30c9\u3092\u4e0a\u66f8\u304d\u3059\u308bADB\u30b7\u30a7\u30eb\u30b3\u30de\u30f3\u30c9" - }, - "title": "Android TV\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/nl.json b/homeassistant/components/androidtv/translations/nl.json index e8fdf44387c..40df125d384 100644 --- a/homeassistant/components/androidtv/translations/nl.json +++ b/homeassistant/components/androidtv/translations/nl.json @@ -20,9 +20,7 @@ "device_class": "Het type apparaat", "host": "Host", "port": "Poort" - }, - "description": "Stel de vereiste parameters in om verbinding te maken met uw Android TV-apparaat", - "title": "Android TV" + } } } }, @@ -49,8 +47,7 @@ "state_detection_rules": "Regels voor statusdetectie van Android TV configureren", "turn_off_command": "ADB shell-uitschakelcommando(laat standaard leeg)", "turn_on_command": "ADB shell-inschakelcommando (laat standaard leeg)" - }, - "title": "Android TV-opties" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/no.json b/homeassistant/components/androidtv/translations/no.json index 92d74ad5e0b..707352476a2 100644 --- a/homeassistant/components/androidtv/translations/no.json +++ b/homeassistant/components/androidtv/translations/no.json @@ -20,9 +20,7 @@ "device_class": "Type enhet", "host": "Vert", "port": "Port" - }, - "description": "Angi n\u00f8dvendige parametere for \u00e5 koble til Android TV-enheten din", - "title": "" + } } } }, @@ -49,8 +47,7 @@ "state_detection_rules": "Konfigurere regler for tilstandsgjenkjenning", "turn_off_command": "ADB shell sl\u00e5 av kommando (la st\u00e5 tomt som standard)", "turn_on_command": "ADB-skall sl\u00e5 p\u00e5 kommando (la st\u00e5 tomt som standard)" - }, - "title": "Android TV-alternativer" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/pl.json b/homeassistant/components/androidtv/translations/pl.json index 5a4f7e39130..2f8dd47ee23 100644 --- a/homeassistant/components/androidtv/translations/pl.json +++ b/homeassistant/components/androidtv/translations/pl.json @@ -20,9 +20,7 @@ "device_class": "Typ urz\u0105dzenia", "host": "Nazwa hosta lub adres IP", "port": "Port" - }, - "description": "Ustaw wymagane parametry, aby po\u0142\u0105czy\u0107 si\u0119 z Android TV", - "title": "Android TV" + } } } }, @@ -49,8 +47,7 @@ "state_detection_rules": "Skonfiguruj regu\u0142y wykrywania stanu", "turn_off_command": "Polecenie wy\u0142\u0105czania z pow\u0142oki ADB (pozostaw puste dla warto\u015bci domy\u015blnej)", "turn_on_command": "Polecenie w\u0142\u0105czenia z pow\u0142oki ADB (pozostaw puste dla warto\u015bci domy\u015blnej)" - }, - "title": "Opcje Android TV" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/pt-BR.json b/homeassistant/components/androidtv/translations/pt-BR.json index 86e9a29dd12..496f7ea0013 100644 --- a/homeassistant/components/androidtv/translations/pt-BR.json +++ b/homeassistant/components/androidtv/translations/pt-BR.json @@ -20,9 +20,7 @@ "device_class": "O tipo de dispositivo", "host": "Nome do host", "port": "Porta" - }, - "description": "Defina os par\u00e2metros necess\u00e1rios para se conectar ao seu dispositivo Android TV", - "title": "AndroidTV" + } } } }, @@ -49,8 +47,7 @@ "state_detection_rules": "Configurar regras de detec\u00e7\u00e3o de estado", "turn_off_command": "Comando de desligamento do shell ADB (deixe vazio por padr\u00e3o)", "turn_on_command": "Comando de ativa\u00e7\u00e3o do shell ADB (deixe vazio por padr\u00e3o)" - }, - "title": "Op\u00e7\u00f5es de TV Android" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/ru.json b/homeassistant/components/androidtv/translations/ru.json index 0803a16c986..61eb431fbf4 100644 --- a/homeassistant/components/androidtv/translations/ru.json +++ b/homeassistant/components/androidtv/translations/ru.json @@ -20,9 +20,7 @@ "device_class": "\u0422\u0438\u043f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" - }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Android TV.", - "title": "Android TV" + } } } }, @@ -49,8 +47,7 @@ "state_detection_rules": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439", "turn_off_command": "\u041a\u043e\u043c\u0430\u043d\u0434\u0430 \u043e\u0431\u043e\u043b\u043e\u0447\u043a\u0438 ADB \u0434\u043b\u044f \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c)", "turn_on_command": "\u041a\u043e\u043c\u0430\u043d\u0434\u0430 \u043e\u0431\u043e\u043b\u043e\u0447\u043a\u0438 ADB \u0434\u043b\u044f \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c)" - }, - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Android TV" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/tr.json b/homeassistant/components/androidtv/translations/tr.json index bb77c35ed1b..6e52e9b3200 100644 --- a/homeassistant/components/androidtv/translations/tr.json +++ b/homeassistant/components/androidtv/translations/tr.json @@ -20,9 +20,7 @@ "device_class": "Cihaz\u0131n t\u00fcr\u00fc", "host": "Sunucu", "port": "Port" - }, - "description": "Android TV cihaz\u0131n\u0131za ba\u011flanmak i\u00e7in gerekli parametreleri ayarlay\u0131n", - "title": "Android TV" + } } } }, @@ -49,8 +47,7 @@ "state_detection_rules": "Durum alg\u0131lama kurallar\u0131n\u0131 yap\u0131land\u0131r\u0131n", "turn_off_command": "ADB kabu\u011fu kapatma komutu (varsay\u0131lan olarak bo\u015f b\u0131rak\u0131n)", "turn_on_command": "ADB kabu\u011fu a\u00e7ma komutu (varsay\u0131lan olarak bo\u015f b\u0131rak\u0131n)" - }, - "title": "Android TV Se\u00e7enekleri" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/zh-Hans.json b/homeassistant/components/androidtv/translations/zh-Hans.json index b3aa722d8a5..03bdfd8851d 100644 --- a/homeassistant/components/androidtv/translations/zh-Hans.json +++ b/homeassistant/components/androidtv/translations/zh-Hans.json @@ -19,9 +19,7 @@ "device_class": "\u8bbe\u5907\u7c7b\u578b", "host": "\u4e3b\u673a", "port": "\u7aef\u53e3" - }, - "description": "\u8bf7\u586b\u5199\u4ee5\u4e0b\u53c2\u6570\uff0c\u8fde\u63a5\u5230 Android TV \u8bbe\u5907\u3002", - "title": "Android TV" + } } } }, @@ -48,8 +46,7 @@ "state_detection_rules": "\u914d\u7f6e\u72b6\u6001\u68c0\u6d4b\u89c4\u5219", "turn_off_command": "ADB shell \u5173\u673a\u547d\u4ee4\uff08\u7559\u7a7a\u4ee5\u4f7f\u7528\u9ed8\u8ba4\u503c\uff09", "turn_on_command": "ADB shell \u5f00\u673a\u547d\u4ee4\uff08\u7559\u7a7a\u4ee5\u4f7f\u7528\u9ed8\u8ba4\u503c\uff09" - }, - "title": "Android TV \u9009\u9879" + } }, "rules": { "data": { diff --git a/homeassistant/components/androidtv/translations/zh-Hant.json b/homeassistant/components/androidtv/translations/zh-Hant.json index 9e9d78f0629..3e572ea509d 100644 --- a/homeassistant/components/androidtv/translations/zh-Hant.json +++ b/homeassistant/components/androidtv/translations/zh-Hant.json @@ -20,9 +20,7 @@ "device_class": "\u88dd\u7f6e\u985e\u5225", "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" - }, - "description": "\u8a2d\u5b9a\u9023\u7dda\u81f3 Android TV \u88dd\u7f6e\u6240\u9700\u53c3\u6578", - "title": "Android TV" + } } } }, @@ -49,8 +47,7 @@ "state_detection_rules": "\u8a2d\u5b9a\u72c0\u614b\u5075\u6e2c\u898f\u5247", "turn_off_command": "ADB shell turn off \u6307\u4ee4\uff08\u9810\u8a2d\u7a7a\u767d\uff09", "turn_on_command": "ADB shell turn on \u6307\u4ee4\uff08\u9810\u8a2d\u7a7a\u767d\uff09" - }, - "title": "Android TV \u9078\u9805" + } }, "rules": { "data": { diff --git a/homeassistant/components/apple_tv/translations/bg.json b/homeassistant/components/apple_tv/translations/bg.json index b1923cab650..cd0488141c4 100644 --- a/homeassistant/components/apple_tv/translations/bg.json +++ b/homeassistant/components/apple_tv/translations/bg.json @@ -2,7 +2,6 @@ "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_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", "ipv6_not_supported": "IPv6 \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430.", "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", "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", @@ -36,6 +35,5 @@ } } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/ca.json b/homeassistant/components/apple_tv/translations/ca.json index c672733193a..ca0ca27bcfe 100644 --- a/homeassistant/components/apple_tv/translations/ca.json +++ b/homeassistant/components/apple_tv/translations/ca.json @@ -2,13 +2,11 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", - "already_configured_device": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "backoff": "En aquests moments el dispositiu no accepta sol\u00b7licituds de vinculaci\u00f3 (\u00e9s possible que hagis introdu\u00eft un codi PIN inv\u00e0lid massa vegades), torna-ho a provar m\u00e9s tard.", "device_did_not_pair": "No s'ha fet cap intent d'acabar el proc\u00e9s de vinculaci\u00f3 des del dispositiu.", "device_not_found": "No s'ha trobat el dispositiu durant el descobriment, prova de tornar-lo a afegir.", "inconsistent_device": "Els protocols esperats no s'han trobat durant el descobriment. Normalment aix\u00f2 indica un problema amb el DNS multicast (Zeroconf). Prova d'afegir el dispositiu de nou.", - "invalid_config": "La configuraci\u00f3 d'aquest dispositiu no est\u00e0 completa. Intenta'l tornar a afegir.", "ipv6_not_supported": "IPv6 no est\u00e0 suportat.", "no_devices_found": "No s'han trobat dispositius a la xarxa", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", @@ -19,7 +17,6 @@ "already_configured": "El dispositiu ja est\u00e0 configurat", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "no_devices_found": "No s'han trobat dispositius a la xarxa", - "no_usable_service": "S'ha trobat un dispositiu per\u00f2 no ha pogut identificar cap manera d'establir-hi una connexi\u00f3. Si continues veient aquest missatge, prova d'especificar-ne l'adre\u00e7a IP o reinicia l'Apple TV.", "unknown": "Error inesperat" }, "flow_title": "{name} ({type})", @@ -73,6 +70,5 @@ "description": "Configuraci\u00f3 dels par\u00e0metres generals del dispositiu" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/cs.json b/homeassistant/components/apple_tv/translations/cs.json index 27b9e1dfeee..b7c44b60f6b 100644 --- a/homeassistant/components/apple_tv/translations/cs.json +++ b/homeassistant/components/apple_tv/translations/cs.json @@ -2,9 +2,7 @@ "config": { "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", - "already_configured_device": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", - "invalid_config": "Nastaven\u00ed tohoto za\u0159\u00edzen\u00ed je ne\u00fapln\u00e9. Zkuste jej p\u0159idat znovu.", "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" @@ -57,6 +55,5 @@ "description": "Konfigurace obecn\u00fdch mo\u017enost\u00ed za\u0159\u00edzen\u00ed" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/de.json b/homeassistant/components/apple_tv/translations/de.json index 48149ce8394..27ecc3452f0 100644 --- a/homeassistant/components/apple_tv/translations/de.json +++ b/homeassistant/components/apple_tv/translations/de.json @@ -2,13 +2,11 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "backoff": "Das Ger\u00e4t akzeptiert zur Zeit keine Kopplungsanfragen (Du hast m\u00f6glicherweise zu oft einen ung\u00fcltigen PIN-Code eingegeben), versuche es sp\u00e4ter erneut.", "device_did_not_pair": "Es wurde kein Versuch unternommen, den Kopplungsvorgang vom Ger\u00e4t aus abzuschlie\u00dfen.", "device_not_found": "Das Ger\u00e4t wurde bei der Erkennung nicht gefunden. Bitte versuche es erneut hinzuzuf\u00fcgen.", "inconsistent_device": "Die erwarteten Protokolle wurden bei der Erkennung nicht gefunden. Dies deutet normalerweise auf ein Problem mit Multicast-DNS (Zeroconf) hin. Bitte versuche das Ger\u00e4t erneut hinzuzuf\u00fcgen.", - "invalid_config": "Die Konfiguration f\u00fcr dieses Ger\u00e4t ist unvollst\u00e4ndig. Bitte versuche, es erneut hinzuzuf\u00fcgen.", "ipv6_not_supported": "IPv6 wird nicht unterst\u00fctzt.", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "reauth_successful": "Die erneute Authentifizierung war erfolgreich", @@ -19,7 +17,6 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "invalid_auth": "Ung\u00fcltige Authentifizierung", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", - "no_usable_service": "Es wurde ein Ger\u00e4t gefunden, aber es konnte keine M\u00f6glichkeit gefunden werden, eine Verbindung zu diesem Ger\u00e4t herzustellen. Wenn diese Meldung weiterhin erscheint, versuche, die IP-Adresse anzugeben oder den Apple TV neu zu starten.", "unknown": "Unerwarteter Fehler" }, "flow_title": "{name} ({type})", @@ -73,6 +70,5 @@ "description": "Konfiguriere die allgemeinen Ger\u00e4teeinstellungen" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/el.json b/homeassistant/components/apple_tv/translations/el.json index cbe36bf56fb..a017d67b834 100644 --- a/homeassistant/components/apple_tv/translations/el.json +++ b/homeassistant/components/apple_tv/translations/el.json @@ -2,13 +2,11 @@ "config": { "abort": { "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", - "already_configured_device": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", "backoff": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b4\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03b1\u03b9\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03c3\u03c4\u03b9\u03b3\u03bc\u03ae (\u03af\u03c3\u03c9\u03c2 \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c0\u03bb\u03b7\u03ba\u03c4\u03c1\u03bf\u03bb\u03bf\u03b3\u03ae\u03c3\u03b5\u03b9 \u03ac\u03ba\u03c5\u03c1\u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc PIN \u03c0\u03ac\u03c1\u03b1 \u03c0\u03bf\u03bb\u03bb\u03ad\u03c2 \u03c6\u03bf\u03c1\u03ad\u03c2), \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1.", "device_did_not_pair": "\u0394\u03b5\u03bd \u03ad\u03b3\u03b9\u03bd\u03b5 \u03ba\u03b1\u03bc\u03af\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03bf\u03bb\u03bf\u03ba\u03bb\u03ae\u03c1\u03c9\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03b9\u03ba\u03b1\u03c3\u03af\u03b1\u03c2 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae.", "device_not_found": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", "inconsistent_device": "\u03a4\u03b1 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b1 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03b1 \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7. \u0391\u03c5\u03c4\u03cc \u03c3\u03c5\u03bd\u03ae\u03b8\u03c9\u03c2 \u03c5\u03c0\u03bf\u03b4\u03b7\u03bb\u03ce\u03bd\u03b5\u03b9 \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1 \u03bc\u03b5 \u03c4\u03bf multicast DNS (Zeroconf). \u03a0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae.", - "invalid_config": "\u0397 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bb\u03bb\u03b9\u03c0\u03ae\u03c2. \u03a0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", "ipv6_not_supported": "\u03a4\u03bf IPv6 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9.", "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", @@ -19,7 +17,6 @@ "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", - "no_usable_service": "\u0392\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae, \u03b1\u03bb\u03bb\u03ac \u03b4\u03b5\u03bd \u03bc\u03c0\u03cc\u03c1\u03b5\u03c3\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03c4\u03b5\u03af \u03ba\u03b1\u03bd\u03ad\u03bd\u03b1\u03c2 \u03c4\u03c1\u03cc\u03c0\u03bf\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03bc\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd. \u0391\u03bd \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b2\u03bb\u03ad\u03c0\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ae \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Apple TV.", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "flow_title": "{name} ({type})", @@ -73,6 +70,5 @@ "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b3\u03b5\u03bd\u03b9\u03ba\u03ad\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/en.json b/homeassistant/components/apple_tv/translations/en.json index 792eaf32c29..f455d590d79 100644 --- a/homeassistant/components/apple_tv/translations/en.json +++ b/homeassistant/components/apple_tv/translations/en.json @@ -2,13 +2,11 @@ "config": { "abort": { "already_configured": "Device is already configured", - "already_configured_device": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "backoff": "Device does not accept pairing requests at this time (you might have entered an invalid PIN code too many times), try again later.", "device_did_not_pair": "No attempt to finish pairing process was made from the device.", "device_not_found": "Device was not found during discovery, please try adding it again.", "inconsistent_device": "Expected protocols were not found during discovery. This normally indicates a problem with multicast DNS (Zeroconf). Please try adding the device again.", - "invalid_config": "The configuration for this device is incomplete. Please try adding it again.", "ipv6_not_supported": "IPv6 is not supported.", "no_devices_found": "No devices found on the network", "reauth_successful": "Re-authentication was successful", @@ -19,7 +17,6 @@ "already_configured": "Device is already configured", "invalid_auth": "Invalid authentication", "no_devices_found": "No devices found on the network", - "no_usable_service": "A device was found but could not identify any way to establish a connection to it. If you keep seeing this message, try specifying its IP address or restarting your Apple TV.", "unknown": "Unexpected error" }, "flow_title": "{name} ({type})", @@ -73,6 +70,5 @@ "description": "Configure general device settings" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/es-419.json b/homeassistant/components/apple_tv/translations/es-419.json index 75e6fb43ff2..f8035822cb6 100644 --- a/homeassistant/components/apple_tv/translations/es-419.json +++ b/homeassistant/components/apple_tv/translations/es-419.json @@ -2,11 +2,7 @@ "config": { "abort": { "backoff": "El dispositivo no acepta solicitudes de emparejamiento en este momento (es posible que haya ingresado un c\u00f3digo PIN no v\u00e1lido demasiadas veces), vuelva a intentarlo m\u00e1s tarde.", - "device_did_not_pair": "No se intent\u00f3 finalizar el proceso de emparejamiento desde el dispositivo.", - "invalid_config": "La configuraci\u00f3n de este dispositivo est\u00e1 incompleta. Intente agregarlo nuevamente." - }, - "error": { - "no_usable_service": "Se encontr\u00f3 un dispositivo, pero no se pudo identificar ninguna forma de establecer una conexi\u00f3n con \u00e9l. Si sigue viendo este mensaje, intente especificar su direcci\u00f3n IP o reinicie su Apple TV." + "device_did_not_pair": "No se intent\u00f3 finalizar el proceso de emparejamiento desde el dispositivo." }, "step": { "confirm": { @@ -47,6 +43,5 @@ "description": "Configurar los ajustes generales del dispositivo" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/es.json b/homeassistant/components/apple_tv/translations/es.json index 3f22edd8d77..1a0ed773169 100644 --- a/homeassistant/components/apple_tv/translations/es.json +++ b/homeassistant/components/apple_tv/translations/es.json @@ -2,13 +2,11 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_configured_device": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "backoff": "El dispositivo no acepta solicitudes de emparejamiento en este momento (es posible que hayas introducido un c\u00f3digo PIN no v\u00e1lido demasiadas veces), int\u00e9ntalo de nuevo m\u00e1s tarde.", "device_did_not_pair": "No se ha intentado finalizar el proceso de emparejamiento desde el dispositivo.", "device_not_found": "No se ha encontrado el dispositivo durante la detecci\u00f3n, por favor, intente a\u00f1adirlo de nuevo.", "inconsistent_device": "No se encontraron los protocolos esperados durante el descubrimiento. Esto normalmente indica un problema con el DNS de multidifusi\u00f3n (Zeroconf). Por favor, intente a\u00f1adir el dispositivo de nuevo.", - "invalid_config": "La configuraci\u00f3n para este dispositivo est\u00e1 incompleta. Intenta a\u00f1adirlo de nuevo.", "no_devices_found": "No se encontraron dispositivos en la red", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "setup_failed": "No se ha podido configurar el dispositivo.", @@ -18,7 +16,6 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "no_devices_found": "No se encontraron dispositivos en la red", - "no_usable_service": "Se encontr\u00f3 un dispositivo, pero no se pudo identificar ninguna manera de establecer una conexi\u00f3n con \u00e9l. Si sigues viendo este mensaje, intenta especificar su direcci\u00f3n IP o reiniciar el Apple TV.", "unknown": "Error inesperado" }, "flow_title": "{name} ({type})", @@ -72,6 +69,5 @@ "description": "Configurar los ajustes generales del dispositivo" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/et.json b/homeassistant/components/apple_tv/translations/et.json index 8180b95fe12..81c5183ece1 100644 --- a/homeassistant/components/apple_tv/translations/et.json +++ b/homeassistant/components/apple_tv/translations/et.json @@ -2,13 +2,11 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", - "already_configured_device": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Seadistamine on juba k\u00e4imas", "backoff": "Seade ei aktsepteeri praegu sidumisn\u00f5udeid (v\u00f5ib-olla oled liiga palju kordi vale PIN-koodi sisestanud), proovi hiljem uuesti.", "device_did_not_pair": "Seade ei \u00fcritatud sidumisprotsessi l\u00f5pule viia.", "device_not_found": "Seadet avastamise ajal ei leitud, proovi seda uuesti lisada.", "inconsistent_device": "Eeldatavaid protokolle avastamise ajal ei leitud. See n\u00e4itab tavaliselt probleemi multcast DNS-iga (Zeroconf). Proovi seade uuesti lisada.", - "invalid_config": "Selle seadme s\u00e4tted on puudulikud. Proovi see uuesti lisada.", "ipv6_not_supported": "IPv6 ei ole toetatud.", "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet", "reauth_successful": "Taastuvastamine \u00f5nnestus", @@ -19,7 +17,6 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "invalid_auth": "Vigane autentimine", "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet", - "no_usable_service": "Leiti seade kuid ei suudetud tuvastada moodust \u00fchenduse loomiseks. Kui n\u00e4ed seda teadet pidevalt, proovi m\u00e4\u00e4rata seadme IP-aadress v\u00f5i taask\u00e4ivita Apple TV.", "unknown": "Ootamatu t\u00f5rge" }, "flow_title": "{name} ({type})", @@ -73,6 +70,5 @@ "description": "Seadme \u00fclds\u00e4tete seadistamine" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/fr.json b/homeassistant/components/apple_tv/translations/fr.json index 6d7deea6e5a..92075463f4d 100644 --- a/homeassistant/components/apple_tv/translations/fr.json +++ b/homeassistant/components/apple_tv/translations/fr.json @@ -2,13 +2,11 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "backoff": "L'appareil n'accepte pas les demandes d'appariement pour le moment (vous avez peut-\u00eatre saisi un code PIN non valide trop de fois), r\u00e9essayez plus tard.", "device_did_not_pair": "Aucune tentative pour terminer l'appairage n'a \u00e9t\u00e9 effectu\u00e9e \u00e0 partir de l'appareil.", "device_not_found": "L'appareil n'a pas \u00e9t\u00e9 trouv\u00e9 lors de la d\u00e9couverte, veuillez r\u00e9essayer de l'ajouter.", "inconsistent_device": "Les protocoles attendus n'ont pas \u00e9t\u00e9 trouv\u00e9s lors de la d\u00e9couverte. Cela indique normalement un probl\u00e8me avec le DNS multicast (Zeroconf). Veuillez r\u00e9essayer d'ajouter l'appareil.", - "invalid_config": "La configuration de cet appareil est incompl\u00e8te. Veuillez r\u00e9essayer de l'ajouter.", "ipv6_not_supported": "IPv6 n'est pas pris en charge.", "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", @@ -19,7 +17,6 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "invalid_auth": "Authentification non valide", "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", - "no_usable_service": "Un dispositif a \u00e9t\u00e9 trouv\u00e9, mais aucun moyen d\u2019\u00e9tablir un lien avec lui. Si vous continuez \u00e0 voir ce message, essayez de sp\u00e9cifier son adresse IP ou de red\u00e9marrer votre Apple TV.", "unknown": "Erreur inattendue" }, "flow_title": "{name} ({type})", @@ -73,6 +70,5 @@ "description": "Configurer les param\u00e8tres g\u00e9n\u00e9raux de l'appareil" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/he.json b/homeassistant/components/apple_tv/translations/he.json index 61ca863bd66..26ec85e8dc7 100644 --- a/homeassistant/components/apple_tv/translations/he.json +++ b/homeassistant/components/apple_tv/translations/he.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", - "already_configured_device": "\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", "ipv6_not_supported": "IPv6 \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da.", "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", diff --git a/homeassistant/components/apple_tv/translations/hu.json b/homeassistant/components/apple_tv/translations/hu.json index 73a30fbdd9a..4c3a8cdee94 100644 --- a/homeassistant/components/apple_tv/translations/hu.json +++ b/homeassistant/components/apple_tv/translations/hu.json @@ -2,13 +2,11 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "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.", "device_not_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a felder\u00edt\u00e9s sor\u00e1n, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni.", "inconsistent_device": "Az elv\u00e1rt protokollok nem tal\u00e1lhat\u00f3k a felder\u00edt\u00e9s sor\u00e1n. Ez \u00e1ltal\u00e1ban a multicast DNS (Zeroconf) probl\u00e9m\u00e1j\u00e1t jelzi. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni az eszk\u00f6zt.", - "invalid_config": "Az eszk\u00f6z konfigur\u00e1l\u00e1sa nem teljes. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni.", "ipv6_not_supported": "Az IPv6 nem t\u00e1mogatott.", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", @@ -19,7 +17,6 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", - "no_usable_service": "Tal\u00e1ltunk egy eszk\u00f6zt, de nem tudtuk azonos\u00edtani, hogyan lehetne kapcsolatot l\u00e9tes\u00edteni vele. Ha tov\u00e1bbra is ezt az \u00fczenetet l\u00e1tja, pr\u00f3b\u00e1lja meg megadni az IP-c\u00edm\u00e9t, vagy ind\u00edtsa \u00fajra az Apple TV-t.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "flow_title": "{name} ({type})", @@ -73,6 +70,5 @@ "description": "Konfigur\u00e1lja az eszk\u00f6z \u00e1ltal\u00e1nos be\u00e1ll\u00edt\u00e1sait" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/id.json b/homeassistant/components/apple_tv/translations/id.json index 7120d0671b8..fcb77511abe 100644 --- a/homeassistant/components/apple_tv/translations/id.json +++ b/homeassistant/components/apple_tv/translations/id.json @@ -2,13 +2,11 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", - "already_configured_device": "Perangkat sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", "backoff": "Perangkat tidak bisa menerima permintaan pemasangan saat ini (Anda mungkin telah berulang kali memasukkan kode PIN yang salah). Coba lagi nanti.", "device_did_not_pair": "Tidak ada upaya untuk menyelesaikan proses pemasangan dari sisi perangkat.", "device_not_found": "Perangkat tidak ditemukan selama penemuan, coba tambahkan lagi.", "inconsistent_device": "Protokol yang diharapkan tidak ditemukan selama penemuan. Ini biasanya terjadi karena masalah dengan DNS multicast (Zeroconf). Coba tambahkan perangkat lagi.", - "invalid_config": "Konfigurasi untuk perangkat ini tidak lengkap. Coba tambahkan lagi.", "ipv6_not_supported": "IPv6 tidak didukung.", "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", "reauth_successful": "Autentikasi ulang berhasil", @@ -19,7 +17,6 @@ "already_configured": "Perangkat sudah dikonfigurasi", "invalid_auth": "Autentikasi tidak valid", "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", - "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": "{name} ({type})", @@ -73,6 +70,5 @@ "description": "Konfigurasikan pengaturan umum perangkat" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/it.json b/homeassistant/components/apple_tv/translations/it.json index 8b6e744ce53..25bcbeaf9fe 100644 --- a/homeassistant/components/apple_tv/translations/it.json +++ b/homeassistant/components/apple_tv/translations/it.json @@ -2,13 +2,11 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "backoff": "Il dispositivo non accetta richieste di abbinamento in questo momento (potresti aver inserito un codice PIN non valido troppe volte), riprova pi\u00f9 tardi.", "device_did_not_pair": "Nessun tentativo di completare il processo di abbinamento \u00e8 stato effettuato dal dispositivo.", "device_not_found": "Il dispositivo non \u00e8 stato trovato durante il rilevamento, prova ad aggiungerlo di nuovo.", "inconsistent_device": "I protocolli previsti non sono stati trovati durante il rilevamento. Questo normalmente indica un problema con DNS multicast (Zeroconf). Prova ad aggiungere di nuovo il dispositivo.", - "invalid_config": "La configurazione per questo dispositivo \u00e8 incompleta. Prova ad aggiungerlo di nuovo.", "ipv6_not_supported": "IPv6 non \u00e8 supportato.", "no_devices_found": "Nessun dispositivo trovato sulla rete", "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", @@ -19,7 +17,6 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "invalid_auth": "Autenticazione non valida", "no_devices_found": "Nessun dispositivo trovato sulla rete", - "no_usable_service": "\u00c8 stato trovato un dispositivo, ma non \u00e8 stato possibile identificare alcun modo per stabilire una connessione con esso. Se continui a vedere questo messaggio, prova a specificarne l'indirizzo IP o a riavviare l'Apple TV.", "unknown": "Errore imprevisto" }, "flow_title": "{name} ({type})", @@ -73,6 +70,5 @@ "description": "Configura le impostazioni generali del dispositivo" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/ja.json b/homeassistant/components/apple_tv/translations/ja.json index 9984006365e..3661cbeedb3 100644 --- a/homeassistant/components/apple_tv/translations/ja.json +++ b/homeassistant/components/apple_tv/translations/ja.json @@ -2,13 +2,11 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "already_configured_device": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "backoff": "\u73fe\u5728\u3001\u30c7\u30d0\u30a4\u30b9\u306f\u30da\u30a2\u30ea\u30f3\u30b0\u8981\u6c42\u3092\u53d7\u3051\u4ed8\u3051\u3066\u3044\u307e\u305b\u3093(\u7121\u52b9\u306aPIN\u30b3\u30fc\u30c9\u3092\u4f55\u5ea6\u3082\u5165\u529b\u3057\u305f\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059)\u3001\u5f8c\u3067\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "device_did_not_pair": "\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u30da\u30a2\u30ea\u30f3\u30b0\u30d7\u30ed\u30bb\u30b9\u3092\u7d42\u4e86\u3059\u308b\u8a66\u307f\u306f\u884c\u308f\u308c\u307e\u305b\u3093\u3067\u3057\u305f\u3002", "device_not_found": "\u691c\u51fa\u4e2d\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u3082\u3046\u4e00\u5ea6\u8ffd\u52a0\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", "inconsistent_device": "\u691c\u51fa\u4e2d\u306b\u671f\u5f85\u3057\u305f\u30d7\u30ed\u30c8\u30b3\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u3053\u308c\u306f\u901a\u5e38\u3001\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8DNS(Zeroconf)\u306b\u554f\u984c\u304c\u3042\u308b\u3053\u3068\u3092\u793a\u3057\u3066\u3044\u307e\u3059\u3002\u30c7\u30d0\u30a4\u30b9\u3092\u3082\u3046\u4e00\u5ea6\u8ffd\u52a0\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", - "invalid_config": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a\u306f\u4e0d\u5b8c\u5168\u3067\u3059\u3002\u3082\u3046\u4e00\u5ea6\u8ffd\u52a0\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", "ipv6_not_supported": "IPv6\u306b\u306f\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093\u3002", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", @@ -19,7 +17,6 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "no_usable_service": "\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u3057\u305f\u304c\u3001\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u63a5\u7d9a\u3092\u78ba\u7acb\u3059\u308b\u65b9\u6cd5\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u3053\u306e\u30e1\u30c3\u30bb\u30fc\u30b8\u304c\u5f15\u304d\u7d9a\u304d\u8868\u793a\u3055\u308c\u308b\u5834\u5408\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9\u3092\u6307\u5b9a\u3059\u308b\u304b\u3001Apple TV\u3092\u518d\u8d77\u52d5\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name}", @@ -73,6 +70,5 @@ "description": "\u30c7\u30d0\u30a4\u30b9\u306e\u4e00\u822c\u7684\u306a\u8a2d\u5b9a" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/ko.json b/homeassistant/components/apple_tv/translations/ko.json index 278dbec04e4..fb2373eac4f 100644 --- a/homeassistant/components/apple_tv/translations/ko.json +++ b/homeassistant/components/apple_tv/translations/ko.json @@ -1,11 +1,9 @@ { "config": { "abort": { - "already_configured_device": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "backoff": "\uae30\uae30\uac00 \ud604\uc7ac \ud398\uc5b4\ub9c1 \uc694\uccad\uc744 \uc218\ub77d\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4(\uc798\ubabb\ub41c PIN \ucf54\ub4dc\ub97c \ub108\ubb34 \ub9ce\uc774 \uc785\ub825\ud588\uc744 \uc218 \uc788\uc74c). \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "device_did_not_pair": "\uae30\uae30\uc5d0\uc11c \ud398\uc5b4\ub9c1 \ud504\ub85c\uc138\uc2a4\ub97c \uc644\ub8cc\ud558\ub824\uace0 \uc2dc\ub3c4\ud558\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", - "invalid_config": "\uc774 \uae30\uae30\uc5d0 \ub300\ud55c \uad6c\uc131\uc774 \ubd88\uc644\uc804\ud569\ub2c8\ub2e4. \ub2e4\uc2dc \ucd94\uac00\ud574\uc8fc\uc138\uc694.", "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, @@ -13,7 +11,6 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "no_usable_service": "\uae30\uae30\ub97c \ucc3e\uc558\uc9c0\ub9cc \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\ub294 \ubc29\ubc95\uc744 \uc2dd\ubcc4\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uba54\uc2dc\uc9c0\uac00 \uacc4\uc18d \ud45c\uc2dc\ub418\uba74 \ud574\ub2f9 IP \uc8fc\uc18c\ub97c \uc9c1\uc811 \uc9c0\uc815\ud574\uc8fc\uc2dc\uac70\ub098 Apple TV\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud574\uc8fc\uc138\uc694.", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "flow_title": "Apple TV: {name}", @@ -59,6 +56,5 @@ "description": "\uc77c\ubc18 \uae30\uae30 \uc124\uc815 \uad6c\uc131" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/lb.json b/homeassistant/components/apple_tv/translations/lb.json index 2354033b577..0950ff8235a 100644 --- a/homeassistant/components/apple_tv/translations/lb.json +++ b/homeassistant/components/apple_tv/translations/lb.json @@ -1,9 +1,7 @@ { "config": { "abort": { - "already_configured_device": "Apparat ass scho konfigur\u00e9iert", "already_in_progress": "Konfiguratioun's Oflaf ass schon am gaang", - "invalid_config": "Konfiguratioun fir d\u00ebsen Apparat ass net komplett. Prob\u00e9ier fir et nach emol dob\u00e4i ze setzen.", "no_devices_found": "Keng Apparater am Netzwierk fonnt", "unknown": "Onerwaarte Feeler" }, @@ -55,6 +53,5 @@ "description": "Allgemeng Apparat Astellungen konfigur\u00e9ieren" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/nl.json b/homeassistant/components/apple_tv/translations/nl.json index 8aaf120403c..36bebc02761 100644 --- a/homeassistant/components/apple_tv/translations/nl.json +++ b/homeassistant/components/apple_tv/translations/nl.json @@ -2,13 +2,11 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_configured_device": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratiestroom is al aan de gang", "backoff": "Het apparaat accepteert op dit moment geen koppelingsverzoeken (u heeft mogelijk te vaak een ongeldige pincode ingevoerd), probeer het later opnieuw.", "device_did_not_pair": "Er is geen poging gedaan om het koppelingsproces te voltooien vanaf het apparaat.", "device_not_found": "Apparaat werd niet gevonden tijdens het zoeken, probeer het opnieuw toe te voegen.", "inconsistent_device": "De verwachte protocollen zijn niet gevonden tijdens het zoeken. Dit wijst gewoonlijk op een probleem met multicast DNS (Zeroconf). Probeer het apparaat opnieuw toe te voegen.", - "invalid_config": "De configuratie voor dit apparaat is onvolledig. Probeer het opnieuw toe te voegen.", "ipv6_not_supported": "IPv6 wordt niet ondersteund.", "no_devices_found": "Geen apparaten gevonden op het netwerk", "reauth_successful": "Herauthenticatie was succesvol", @@ -19,7 +17,6 @@ "already_configured": "Apparaat is al geconfigureerd", "invalid_auth": "Ongeldige authenticatie", "no_devices_found": "Geen apparaten gevonden op het netwerk", - "no_usable_service": "Er is een apparaat gevonden, maar er kon geen manier worden gevonden om er verbinding mee te maken. Als u dit bericht blijft zien, probeert u het IP-adres in te voeren of uw Apple TV opnieuw op te starten.", "unknown": "Onverwachte fout" }, "flow_title": "{name} ( {type} )", @@ -73,6 +70,5 @@ "description": "Algemene apparaatinstellingen configureren" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/no.json b/homeassistant/components/apple_tv/translations/no.json index e364633597e..97f80c7dfe7 100644 --- a/homeassistant/components/apple_tv/translations/no.json +++ b/homeassistant/components/apple_tv/translations/no.json @@ -2,13 +2,11 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "already_configured_device": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "backoff": "Enheten godtar ikke sammenkoblingsforesp\u00f8rsler for \u00f8yeblikket (du kan ha skrevet inn en ugyldig PIN-kode for mange ganger), pr\u00f8v igjen senere.", "device_did_not_pair": "Ingen fors\u00f8k p\u00e5 \u00e5 fullf\u00f8re paringsprosessen ble gjort fra enheten", "device_not_found": "Enheten ble ikke funnet under oppdagelsen. Pr\u00f8v \u00e5 legge den til p\u00e5 nytt.", "inconsistent_device": "Forventede protokoller ble ikke funnet under oppdagelsen. Dette indikerer vanligvis et problem med multicast DNS (Zeroconf). Pr\u00f8v \u00e5 legge til enheten p\u00e5 nytt.", - "invalid_config": "Konfigurasjonen for denne enheten er ufullstendig. Pr\u00f8v \u00e5 legge den til p\u00e5 nytt.", "ipv6_not_supported": "IPv6 st\u00f8ttes ikke.", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", @@ -19,7 +17,6 @@ "already_configured": "Enheten er allerede konfigurert", "invalid_auth": "Ugyldig godkjenning", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", - "no_usable_service": "En enhet ble funnet, men kunne ikke identifisere noen m\u00e5te \u00e5 etablere en tilkobling til den. Hvis du fortsetter \u00e5 se denne meldingen, kan du pr\u00f8ve \u00e5 angi IP-adressen eller starte Apple TV p\u00e5 nytt.", "unknown": "Uventet feil" }, "flow_title": "{name} ( {type} )", @@ -73,6 +70,5 @@ "description": "Konfigurer generelle enhetsinnstillinger" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/pl.json b/homeassistant/components/apple_tv/translations/pl.json index 303de09c1dd..6c236c7b3ea 100644 --- a/homeassistant/components/apple_tv/translations/pl.json +++ b/homeassistant/components/apple_tv/translations/pl.json @@ -2,13 +2,11 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", "backoff": "Urz\u0105dzenie w tej chwili nie akceptuje \u017c\u0105da\u0144 parowania (by\u0107 mo\u017ce zbyt wiele razy wpisa\u0142e\u015b nieprawid\u0142owy kod PIN), spr\u00f3buj ponownie p\u00f3\u017aniej.", "device_did_not_pair": "Nie podj\u0119to pr\u00f3by zako\u0144czenia procesu parowania z urz\u0105dzenia.", "device_not_found": "Urz\u0105dzenie nie zosta\u0142o znalezione podczas wykrywania, spr\u00f3buj doda\u0107 je ponownie.", "inconsistent_device": "Oczekiwane protoko\u0142y nie zosta\u0142y znalezione podczas wykrywania. Zwykle wskazuje to na problem z multicastem DNS (Zeroconf). Spr\u00f3buj ponownie doda\u0107 urz\u0105dzenie.", - "invalid_config": "Konfiguracja tego urz\u0105dzenia jest niekompletna. Spr\u00f3buj doda\u0107 go ponownie.", "ipv6_not_supported": "IPv6 nie jest obs\u0142ugiwany.", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", @@ -19,7 +17,6 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "invalid_auth": "Niepoprawne uwierzytelnienie", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", - "no_usable_service": "Znaleziono urz\u0105dzenie, ale nie uda\u0142o si\u0119 zidentyfikowa\u0107 \u017cadnego sposobu na nawi\u0105zanie z nim po\u0142\u0105czenia. Je\u015bli nadal widzisz t\u0119 wiadomo\u015b\u0107, spr\u00f3buj poda\u0107 jego adres IP lub uruchom ponownie Apple TV.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "flow_title": "{name} ({type})", @@ -73,6 +70,5 @@ "description": "Skonfiguruj og\u00f3lne ustawienia urz\u0105dzenia" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/pt-BR.json b/homeassistant/components/apple_tv/translations/pt-BR.json index 539335c17d3..801f6df094b 100644 --- a/homeassistant/components/apple_tv/translations/pt-BR.json +++ b/homeassistant/components/apple_tv/translations/pt-BR.json @@ -2,13 +2,11 @@ "config": { "abort": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", - "already_configured_device": "Dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "backoff": "O dispositivo n\u00e3o aceita solicita\u00e7\u00f5es de emparelhamento neste momento (voc\u00ea pode ter digitado um c\u00f3digo PIN inv\u00e1lido muitas vezes), tente novamente mais tarde.", "device_did_not_pair": "Nenhuma tentativa de concluir o processo de emparelhamento foi feita a partir do dispositivo.", "device_not_found": "O dispositivo n\u00e3o foi encontrado durante a descoberta. Tente adicion\u00e1-lo novamente.", "inconsistent_device": "Os protocolos esperados n\u00e3o foram encontrados durante a descoberta. Isso normalmente indica um problema com o DNS multicast (Zeroconf). Tente adicionar o dispositivo novamente.", - "invalid_config": "A configura\u00e7\u00e3o deste dispositivo est\u00e1 incompleta. Tente adicion\u00e1-lo novamente.", "ipv6_not_supported": "IPv6 n\u00e3o \u00e9 suportado.", "no_devices_found": "Nenhum dispositivo encontrado na rede", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", @@ -19,7 +17,6 @@ "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "no_devices_found": "Nenhum dispositivo encontrado na rede", - "no_usable_service": "Um dispositivo foi encontrado, mas n\u00e3o foi poss\u00edvel identificar nenhuma maneira de estabelecer uma conex\u00e3o com ele. Se voc\u00ea continuar vendo esta mensagem, tente especificar o endere\u00e7o IP ou reiniciar a Apple TV.", "unknown": "Erro inesperado" }, "flow_title": "{name} ({type})", @@ -73,6 +70,5 @@ "description": "Definir as configura\u00e7\u00f5es gerais do dispositivo" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/pt.json b/homeassistant/components/apple_tv/translations/pt.json index 486ff0c51e4..deec60a19ea 100644 --- a/homeassistant/components/apple_tv/translations/pt.json +++ b/homeassistant/components/apple_tv/translations/pt.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "no_devices_found": "Nenhum dispositivo encontrado na rede", "unknown": "Erro inesperado" @@ -10,7 +9,6 @@ "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "no_devices_found": "Nenhum dispositivo encontrado na rede", - "no_usable_service": "Foi encontrado um dispositivo, mas n\u00e3o foi poss\u00edvel identificar nenhuma forma de estabelecer uma liga\u00e7\u00e3o com ele. Se continuar a ver esta mensagem, tente especificar o endere\u00e7o IP ou reiniciar a sua Apple TV.", "unknown": "Erro inesperado" }, "flow_title": "Apple TV: {name}", @@ -56,6 +54,5 @@ "description": "Definir as configura\u00e7\u00f5es gerais do dispositivo" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/ru.json b/homeassistant/components/apple_tv/translations/ru.json index 88516d45af3..024103258a8 100644 --- a/homeassistant/components/apple_tv/translations/ru.json +++ b/homeassistant/components/apple_tv/translations/ru.json @@ -2,13 +2,11 @@ "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_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "backoff": "\u0412 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043d\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 (\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u0412\u044b \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u0440\u0430\u0437 \u0432\u0432\u043e\u0434\u0438\u043b\u0438 \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434), \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", "device_did_not_pair": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u044b\u0442\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f.", "device_not_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0433\u043e \u0435\u0449\u0451 \u0440\u0430\u0437.", "inconsistent_device": "\u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0435 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u044b. \u041e\u0431\u044b\u0447\u043d\u043e \u044d\u0442\u043e \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u043d\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443 \u0441 \u043c\u043d\u043e\u0433\u043e\u0430\u0434\u0440\u0435\u0441\u043d\u044b\u043c DNS (Zeroconf). \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0435\u0449\u0451 \u0440\u0430\u0437.", - "invalid_config": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0433\u043e \u0435\u0449\u0451 \u0440\u0430\u0437.", "ipv6_not_supported": "IPv6 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", @@ -19,7 +17,6 @@ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", - "no_usable_service": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u0415\u0441\u043b\u0438 \u0412\u044b \u0443\u0436\u0435 \u0432\u0438\u0434\u0435\u043b\u0438 \u044d\u0442\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 \u0435\u0433\u043e.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "flow_title": "{name} ({type})", @@ -73,6 +70,5 @@ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/sl.json b/homeassistant/components/apple_tv/translations/sl.json index 997d60402ca..da9af2347c0 100644 --- a/homeassistant/components/apple_tv/translations/sl.json +++ b/homeassistant/components/apple_tv/translations/sl.json @@ -1,11 +1,9 @@ { "config": { "abort": { - "already_configured_device": "Naprava je \u017ee name\u0161\u010dena", "already_in_progress": "Name\u0161\u010danje se \u017ee izvaja", "backoff": "Naprav v tem trenutku ne sprejema zahtev za seznanitev (morda ste preve\u010dkrat vnesli napa\u010den PIN). Pokusitve znova kasneje.", "device_did_not_pair": "Iz te naprave ni bilo poskusov zaklju\u010diti seznanjanja.", - "invalid_config": "Namestitev te naprave ni bila zaklju\u010dena. Poskusite ponovno.", "no_devices_found": "Ni najdenih naprav v omre\u017eju", "unknown": "Nepri\u010dakovana napaka" }, @@ -13,7 +11,6 @@ "already_configured": "Naprava je \u017ee name\u0161\u010dena", "invalid_auth": "Napaka pri overjanju", "no_devices_found": "Ni najdenih naprav v omre\u017eju", - "no_usable_service": "Najdena je bila naprava, za katero ni znan na\u010din povezovanja. \u010ce boste \u0161e vedno videli to sporo\u010dilo, poskusite dolo\u010diti IP naslov ali pa ponovno za\u017eenite Apple TV.", "unknown": "Nepri\u010dakovana napaka" }, "flow_title": "Apple TV: {name}", @@ -59,6 +56,5 @@ "description": "Konfiguracija splo\u0161nih nastavitev naprave" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/tr.json b/homeassistant/components/apple_tv/translations/tr.json index cee9fcce81e..4919d48c15c 100644 --- a/homeassistant/components/apple_tv/translations/tr.json +++ b/homeassistant/components/apple_tv/translations/tr.json @@ -2,13 +2,11 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "backoff": "Cihaz \u015fu anda e\u015fle\u015ftirme isteklerini kabul etmiyor (\u00e7ok say\u0131da ge\u00e7ersiz PIN kodu girmi\u015f olabilirsiniz), daha sonra tekrar deneyin.", "device_did_not_pair": "Cihazdan e\u015fle\u015ftirme i\u015flemini bitirmek i\u00e7in herhangi bir giri\u015fimde bulunulmad\u0131.", "device_not_found": "Cihaz ke\u015fif s\u0131ras\u0131nda bulunamad\u0131, l\u00fctfen tekrar eklemeyi deneyin.", "inconsistent_device": "Ke\u015fif s\u0131ras\u0131nda beklenen protokoller bulunamad\u0131. Bu normalde \u00e7ok noktaya yay\u0131n DNS (Zeroconf) ile ilgili bir sorunu g\u00f6sterir. L\u00fctfen cihaz\u0131 tekrar eklemeyi deneyin.", - "invalid_config": "Bu ayg\u0131t\u0131n yap\u0131land\u0131rmas\u0131 tamamlanmad\u0131. L\u00fctfen tekrar eklemeyi deneyin.", "ipv6_not_supported": "IPv6 desteklenmiyor.", "no_devices_found": "A\u011fda cihaz bulunamad\u0131", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", @@ -19,7 +17,6 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "no_devices_found": "A\u011fda cihaz bulunamad\u0131", - "no_usable_service": "Bir ayg\u0131t bulundu, ancak ba\u011flant\u0131 kurman\u0131n herhangi bir yolunu tan\u0131mlayamad\u0131. Bu iletiyi g\u00f6rmeye devam ederseniz, IP adresini belirtmeye veya Apple TV'nizi yeniden ba\u015flatmaya \u00e7al\u0131\u015f\u0131n.", "unknown": "Beklenmeyen hata" }, "flow_title": "{name} ( {type} )", @@ -73,6 +70,5 @@ "description": "Genel cihaz ayarlar\u0131n\u0131 yap\u0131land\u0131r\u0131n" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/uk.json b/homeassistant/components/apple_tv/translations/uk.json index a1ae2259ada..984aeceaaeb 100644 --- a/homeassistant/components/apple_tv/translations/uk.json +++ b/homeassistant/components/apple_tv/translations/uk.json @@ -1,11 +1,9 @@ { "config": { "abort": { - "already_configured_device": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", "backoff": "\u0412 \u0434\u0430\u043d\u0438\u0439 \u0447\u0430\u0441 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0440\u0438\u0439\u043c\u0430\u0454 \u0437\u0430\u043f\u0438\u0442\u0438 \u043d\u0430 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438 (\u043c\u043e\u0436\u043b\u0438\u0432\u043e, \u0412\u0438 \u0437\u0430\u043d\u0430\u0434\u0442\u043e \u0431\u0430\u0433\u0430\u0442\u043e \u0440\u0430\u0437 \u0432\u0432\u043e\u0434\u0438\u043b\u0438 \u043d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 PIN-\u043a\u043e\u0434), \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437 \u043f\u0456\u0437\u043d\u0456\u0448\u0435.", "device_did_not_pair": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043d\u0430\u043c\u0430\u0433\u0430\u0432\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043f\u0440\u043e\u0446\u0435\u0441 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438.", - "invalid_config": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u0446\u044c\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u043d\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430. \u0421\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u0439\u043e\u0433\u043e \u0449\u0435 \u0440\u0430\u0437.", "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, @@ -13,7 +11,6 @@ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", - "no_usable_service": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 \u0441\u043f\u043e\u0441\u0456\u0431 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \u042f\u043a\u0449\u043e \u0412\u0438 \u0432\u0436\u0435 \u0431\u0430\u0447\u0438\u043b\u0438 \u0446\u0435 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u043d\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0432\u043a\u0430\u0437\u0430\u0442\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0430\u0431\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c \u0439\u043e\u0433\u043e.", "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "flow_title": "Apple TV: {name}", @@ -59,6 +56,5 @@ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/zh-Hans.json b/homeassistant/components/apple_tv/translations/zh-Hans.json index b08ccafc837..365bf6cf6c0 100644 --- a/homeassistant/components/apple_tv/translations/zh-Hans.json +++ b/homeassistant/components/apple_tv/translations/zh-Hans.json @@ -2,13 +2,11 @@ "config": { "abort": { "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86", - "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\uff0c\u53ef\u80fd\u662f\u56e0\u4e3a\u591a\u6b21 PIN \u7801\u8f93\u5165\u9519\u8bef\u3002\u8bf7\u7a0d\u540e\u91cd\u8bd5\u3002", "device_did_not_pair": "\u672a\u5c1d\u8bd5\u4ece\u8bbe\u5907\u5b8c\u6210\u914d\u5bf9\u8fc7\u7a0b\u3002", "device_not_found": "\u672a\u627e\u5230\u8bbe\u5907\uff0c\u8bf7\u5c1d\u8bd5\u91cd\u65b0\u6dfb\u52a0\u3002", "inconsistent_device": "\u641c\u7d22\u671f\u95f4\u672a\u53d1\u73b0\u914d\u7f6e\u8bbe\u5907\u6240\u5fc5\u9700\u7684\u534f\u8bae\u3002\u8fd9\u901a\u5e38\u662f\u56e0\u4e3a mDNS \u534f\u8bae\uff08zeroconf\uff09\u5b58\u5728\u95ee\u9898\u3002\u8bf7\u7a0d\u540e\u518d\u91cd\u65b0\u5c1d\u8bd5\u6dfb\u52a0\u8bbe\u5907\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": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230\u8bbe\u5907", "reauth_successful": "\u91cd\u65b0\u8ba4\u8bc1\u6210\u529f", "setup_failed": "\u8bbe\u7f6e\u8bbe\u5907\u5931\u8d25\u3002", @@ -18,7 +16,6 @@ "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86", "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548", "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230\u8bbe\u5907", - "no_usable_service": "\u5df2\u627e\u5230\u8bbe\u5907\uff0c\u4f46\u672a\u8bc6\u522b\u51fa\u4e0e\u5176\u5efa\u7acb\u8fde\u63a5\u7684\u65b9\u5f0f\u3002\u5982\u679c\u6b64\u95ee\u9898\u6301\u7eed\u5b58\u5728\uff0c\u8bf7\u5c1d\u8bd5\u6307\u5b9a\u5176 IP \u5730\u5740\uff0c\u6216\u91cd\u65b0\u542f\u52a8\u8bbe\u5907\u3002", "unknown": "\u975e\u9884\u671f\u7684\u9519\u8bef" }, "flow_title": "{name} ({type})", @@ -72,6 +69,5 @@ "description": "\u914d\u7f6e\u8bbe\u5907\u901a\u7528\u8bbe\u7f6e" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/zh-Hant.json b/homeassistant/components/apple_tv/translations/zh-Hant.json index b4e5108d474..be2bfe08fdd 100644 --- a/homeassistant/components/apple_tv/translations/zh-Hant.json +++ b/homeassistant/components/apple_tv/translations/zh-Hant.json @@ -2,13 +2,11 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "already_configured_device": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "backoff": "\u88dd\u7f6e\u4e0d\u63a5\u53d7\u6b64\u6b21\u914d\u5c0d\u8acb\u6c42\uff08\u53ef\u80fd\u8f38\u5165\u592a\u591a\u6b21\u7121\u6548\u7684 PIN \u78bc\uff09\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66\u3002", "device_did_not_pair": "\u88dd\u7f6e\u6c92\u6709\u5617\u8a66\u914d\u5c0d\u5b8c\u6210\u904e\u7a0b\u3002", "device_not_found": "\u641c\u5c0b\u4e0d\u5230\u88dd\u7f6e\u3001\u8acb\u8a66\u8457\u518d\u65b0\u589e\u4e00\u6b21\u3002", "inconsistent_device": "\u641c\u5c0b\u4e0d\u5230\u9810\u671f\u7684\u901a\u8a0a\u5354\u5b9a\u3002\u901a\u5e38\u539f\u56e0\u70ba Multicast DNS (Zeroconf) \u554f\u984c\u3001\u8acb\u8a66\u8457\u518d\u65b0\u589e\u4e00\u6b21\u3002", - "invalid_config": "\u6b64\u88dd\u7f6e\u8a2d\u5b9a\u4e0d\u5b8c\u6574\uff0c\u8acb\u7a0d\u5019\u518d\u8a66\u4e00\u6b21\u3002", "ipv6_not_supported": "\u4e0d\u652f\u63f4 IPv6\u3002", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", @@ -19,7 +17,6 @@ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", - "no_usable_service": "\u627e\u5230\u7684\u88dd\u7f6e\u7121\u6cd5\u8b58\u5225\u4ee5\u9032\u884c\u9023\u7dda\u3002\u5047\u5982\u6b64\u8a0a\u606f\u91cd\u8907\u767c\u751f\u3002\u8acb\u8a66\u8457\u6307\u5b9a\u7279\u5b9a IP \u4f4d\u5740\u6216\u91cd\u555f Apple TV\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "flow_title": "{name} ({type})", @@ -73,6 +70,5 @@ "description": "\u8a2d\u5b9a\u4e00\u822c\u88dd\u7f6e\u8a2d\u5b9a" } } - }, - "title": "Apple TV" + } } \ No newline at end of file diff --git a/homeassistant/components/application_credentials/translations/ko.json b/homeassistant/components/application_credentials/translations/ko.json new file mode 100644 index 00000000000..3beeea8e2e4 --- /dev/null +++ b/homeassistant/components/application_credentials/translations/ko.json @@ -0,0 +1,3 @@ +{ + "title": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \uc790\uaca9 \uc99d\uba85" +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/ca.json b/homeassistant/components/asuswrt/translations/ca.json index 93821419fc4..8cded456ee9 100644 --- a/homeassistant/components/asuswrt/translations/ca.json +++ b/homeassistant/components/asuswrt/translations/ca.json @@ -2,9 +2,7 @@ "config": { "abort": { "invalid_unique_id": "No s'ha pogut determinar cap identificador \u00fanic v\u00e0lid del dispositiu", - "no_unique_id": "Ja s'ha configurat un dispositiu sense un identificador \u00fanic v\u00e0lid. La configuraci\u00f3 de diverses inst\u00e0ncies no \u00e9s possible", - "not_unique_id_exist": "Ja s'ha configurat un dispositiu sense un identificador \u00fanic v\u00e0lid. La configuraci\u00f3 de diverses inst\u00e0ncies no \u00e9s possible", - "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + "no_unique_id": "Ja s'ha configurat un dispositiu sense un identificador \u00fanic v\u00e0lid. La configuraci\u00f3 de diverses inst\u00e0ncies no \u00e9s possible" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", diff --git a/homeassistant/components/asuswrt/translations/cs.json b/homeassistant/components/asuswrt/translations/cs.json index d9766e9a6d0..26358d8c4bd 100644 --- a/homeassistant/components/asuswrt/translations/cs.json +++ b/homeassistant/components/asuswrt/translations/cs.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." - }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_host": "Neplatn\u00fd hostitel nebo IP adresa", diff --git a/homeassistant/components/asuswrt/translations/de.json b/homeassistant/components/asuswrt/translations/de.json index 1a0dc26903d..be17c242d58 100644 --- a/homeassistant/components/asuswrt/translations/de.json +++ b/homeassistant/components/asuswrt/translations/de.json @@ -2,9 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Unm\u00f6glich, eine g\u00fcltige eindeutige Kennung f\u00fcr das Ger\u00e4t zu ermitteln", - "no_unique_id": "Ein Ger\u00e4t ohne g\u00fcltige, eindeutige ID ist bereits konfiguriert. Die Konfiguration mehrerer Instanzen ist nicht m\u00f6glich", - "not_unique_id_exist": "Ein Ger\u00e4t ohne g\u00fcltige, eindeutige ID ist bereits konfiguriert. Die Konfiguration mehrerer Instanzen ist nicht m\u00f6glich", - "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + "no_unique_id": "Ein Ger\u00e4t ohne g\u00fcltige, eindeutige ID ist bereits konfiguriert. Die Konfiguration mehrerer Instanzen ist nicht m\u00f6glich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/asuswrt/translations/el.json b/homeassistant/components/asuswrt/translations/el.json index bb18890091c..6a380b23152 100644 --- a/homeassistant/components/asuswrt/translations/el.json +++ b/homeassistant/components/asuswrt/translations/el.json @@ -2,9 +2,7 @@ "config": { "abort": { "invalid_unique_id": "\u0391\u03b4\u03cd\u03bd\u03b1\u03c4\u03bf\u03c2 \u03bf \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b5\u03bd\u03cc\u03c2 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c5 \u03bc\u03bf\u03bd\u03b1\u03b4\u03b9\u03ba\u03bf\u03cd \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", - "no_unique_id": "\u039c\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c9\u03c1\u03af\u03c2 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03bc\u03bf\u03bd\u03b1\u03b4\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af. \u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ce\u03bd", - "not_unique_id_exist": "\u039c\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c9\u03c1\u03af\u03c2 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03bc\u03bf\u03bd\u03b1\u03b4\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af. \u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ce\u03bd", - "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + "no_unique_id": "\u039c\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c9\u03c1\u03af\u03c2 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03bc\u03bf\u03bd\u03b1\u03b4\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af. \u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ce\u03bd" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/asuswrt/translations/en.json b/homeassistant/components/asuswrt/translations/en.json index 93e8f8869b0..f93eae80e6d 100644 --- a/homeassistant/components/asuswrt/translations/en.json +++ b/homeassistant/components/asuswrt/translations/en.json @@ -2,9 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Impossible to determine a valid unique id for the device", - "no_unique_id": "A device without a valid unique id is already configured. Configuration of multiple instance is not possible", - "not_unique_id_exist": "A device without a valid unique id is already configured. Configuration of multiple instance is not possible", - "single_instance_allowed": "Already configured. Only a single configuration possible." + "no_unique_id": "A device without a valid unique id is already configured. Configuration of multiple instance is not possible" }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/asuswrt/translations/es.json b/homeassistant/components/asuswrt/translations/es.json index 8a7876bb45b..9a2e0485aa7 100644 --- a/homeassistant/components/asuswrt/translations/es.json +++ b/homeassistant/components/asuswrt/translations/es.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "invalid_unique_id": "No se pudo determinar ning\u00fan identificador \u00fanico v\u00e1lido del dispositivo", - "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n." + "invalid_unique_id": "No se pudo determinar ning\u00fan identificador \u00fanico v\u00e1lido del dispositivo" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/asuswrt/translations/et.json b/homeassistant/components/asuswrt/translations/et.json index 03dc665fcf9..e76f5006de7 100644 --- a/homeassistant/components/asuswrt/translations/et.json +++ b/homeassistant/components/asuswrt/translations/et.json @@ -2,9 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Seadme kehtivat kordumatut ID-d on v\u00f5imatu m\u00e4\u00e4rata", - "no_unique_id": "Seade, millel puudub kehtiv kordumatu ID, on juba seadistatud. Mitme eksemplari seadistamine pole v\u00f5imalik", - "not_unique_id_exist": "Seade, millel puudub kehtiv kordumatu ID, on juba seadistatud. Mitme eksemplari seadistamine pole v\u00f5imalik", - "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + "no_unique_id": "Seade, millel puudub kehtiv kordumatu ID, on juba seadistatud. Mitme eksemplari seadistamine pole v\u00f5imalik" }, "error": { "cannot_connect": "\u00dchendamine nurjus", diff --git a/homeassistant/components/asuswrt/translations/fr.json b/homeassistant/components/asuswrt/translations/fr.json index 9cb849632b3..7ef4eafc6b3 100644 --- a/homeassistant/components/asuswrt/translations/fr.json +++ b/homeassistant/components/asuswrt/translations/fr.json @@ -2,9 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Impossible de d\u00e9terminer un identifiant unique valide pour l'appareil", - "no_unique_id": "Un appareil sans identifiant unique valide est d\u00e9j\u00e0 configur\u00e9. La configuration de plusieurs instances n'est pas possible", - "not_unique_id_exist": "Un appareil sans identifiant unique valide est d\u00e9j\u00e0 configur\u00e9. La configuration de plusieurs instances n'est pas possible", - "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + "no_unique_id": "Un appareil sans identifiant unique valide est d\u00e9j\u00e0 configur\u00e9. La configuration de plusieurs instances n'est pas possible" }, "error": { "cannot_connect": "\u00c9chec de connexion", diff --git a/homeassistant/components/asuswrt/translations/he.json b/homeassistant/components/asuswrt/translations/he.json index 867cc6e4f5c..fdc894892fa 100644 --- a/homeassistant/components/asuswrt/translations/he.json +++ b/homeassistant/components/asuswrt/translations/he.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." - }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd", diff --git a/homeassistant/components/asuswrt/translations/hu.json b/homeassistant/components/asuswrt/translations/hu.json index 86339d7274c..34581ec6b7a 100644 --- a/homeassistant/components/asuswrt/translations/hu.json +++ b/homeassistant/components/asuswrt/translations/hu.json @@ -2,9 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Lehetetlen \u00e9rv\u00e9nyes egyedi azonos\u00edt\u00f3t meghat\u00e1rozni az eszk\u00f6zh\u00f6z", - "no_unique_id": "Az \u00e9rv\u00e9nyes egyedi azonos\u00edt\u00f3 n\u00e9lk\u00fcli eszk\u00f6z m\u00e1r konfigur\u00e1lva van. T\u00f6bb p\u00e9ld\u00e1ny konfigur\u00e1l\u00e1sa nem lehets\u00e9ges", - "not_unique_id_exist": "Egy \u00e9rv\u00e9nyes UniqueID azonos\u00edt\u00f3val nem rendelkez\u0151 eszk\u00f6z m\u00e1r konfigur\u00e1lva van. T\u00f6bb p\u00e9ld\u00e1ny konfigur\u00e1l\u00e1sa nem lehets\u00e9ges", - "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + "no_unique_id": "Az \u00e9rv\u00e9nyes egyedi azonos\u00edt\u00f3 n\u00e9lk\u00fcli eszk\u00f6z m\u00e1r konfigur\u00e1lva van. T\u00f6bb p\u00e9ld\u00e1ny konfigur\u00e1l\u00e1sa nem lehets\u00e9ges" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/asuswrt/translations/id.json b/homeassistant/components/asuswrt/translations/id.json index 249744a1ebe..758d88b9f48 100644 --- a/homeassistant/components/asuswrt/translations/id.json +++ b/homeassistant/components/asuswrt/translations/id.json @@ -2,9 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Penentuan ID unik yang valid untuk perangkat tidak dimungkinkan", - "no_unique_id": "Perangkat tanpa ID unik yang valid sudah dikonfigurasi. Konfigurasi beberapa instans tidak dimungkinkan", - "not_unique_id_exist": "Perangkat tanpa ID unik yang valid sudah dikonfigurasi. Konfigurasi beberapa instans tidak dimungkinkan", - "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + "no_unique_id": "Perangkat tanpa ID unik yang valid sudah dikonfigurasi. Konfigurasi beberapa instans tidak dimungkinkan" }, "error": { "cannot_connect": "Gagal terhubung", diff --git a/homeassistant/components/asuswrt/translations/it.json b/homeassistant/components/asuswrt/translations/it.json index d2152c85357..38225900691 100644 --- a/homeassistant/components/asuswrt/translations/it.json +++ b/homeassistant/components/asuswrt/translations/it.json @@ -2,9 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Impossibile determinare un ID univoco valido per il dispositivo", - "no_unique_id": "Un dispositivo senza un ID univoco valido \u00e8 gi\u00e0 configurato. La configurazione di pi\u00f9 istanze non \u00e8 possibile", - "not_unique_id_exist": "Un dispositivo senza un ID univoco valido \u00e8 gi\u00e0 configurato. La configurazione di pi\u00f9 istanze non \u00e8 possibile", - "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + "no_unique_id": "Un dispositivo senza un ID univoco valido \u00e8 gi\u00e0 configurato. La configurazione di pi\u00f9 istanze non \u00e8 possibile" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/asuswrt/translations/ja.json b/homeassistant/components/asuswrt/translations/ja.json index 0ae15e8b86b..6bbe87fbf7e 100644 --- a/homeassistant/components/asuswrt/translations/ja.json +++ b/homeassistant/components/asuswrt/translations/ja.json @@ -2,9 +2,7 @@ "config": { "abort": { "invalid_unique_id": "\u30c7\u30d0\u30a4\u30b9\u306e\u6709\u52b9\u306a\u30e6\u30cb\u30fc\u30afID\u3092\u6c7a\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093", - "no_unique_id": "\u6709\u52b9\u306a\u30e6\u30cb\u30fc\u30afID\u3092\u6301\u305f\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8907\u6570\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u69cb\u6210\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093", - "not_unique_id_exist": "\u6709\u52b9\u306a\u30e6\u30cb\u30fc\u30afID\u3092\u6301\u305f\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8907\u6570\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u69cb\u6210\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "no_unique_id": "\u6709\u52b9\u306a\u30e6\u30cb\u30fc\u30afID\u3092\u6301\u305f\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8907\u6570\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u69cb\u6210\u3059\u308b\u3053\u3068\u306f\u3067\u304d\u307e\u305b\u3093" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/asuswrt/translations/ko.json b/homeassistant/components/asuswrt/translations/ko.json index 4e60a0d15b0..3825ed92b2e 100644 --- a/homeassistant/components/asuswrt/translations/ko.json +++ b/homeassistant/components/asuswrt/translations/ko.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "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." + "invalid_unique_id": "\uae30\uae30\uc758 \uc720\ud6a8\ud55c \uace0\uc720 ID\ub97c \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "no_unique_id": "\uc720\ud6a8\ud55c \uace0\uc720 ID\uac00 \uc5c6\ub294 \uc7a5\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.\n\uc5ec\ub7ec \uc778\uc2a4\ud134\uc2a4\ub97c \uad6c\uc131\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", diff --git a/homeassistant/components/asuswrt/translations/nl.json b/homeassistant/components/asuswrt/translations/nl.json index 4f3611289e5..17be650bf07 100644 --- a/homeassistant/components/asuswrt/translations/nl.json +++ b/homeassistant/components/asuswrt/translations/nl.json @@ -2,9 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Onmogelijk om een geldige unieke id voor het apparaat te bepalen", - "no_unique_id": "Een apparaat zonder geldige unieke id is al geconfigureerd. Configuratie van meerdere instanties is niet mogelijk", - "not_unique_id_exist": "Een apparaat zonder geldige unieke id is al geconfigureerd. Configuratie van meerdere instanties is niet mogelijk", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "no_unique_id": "Een apparaat zonder geldige unieke id is al geconfigureerd. Configuratie van meerdere instanties is niet mogelijk" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/asuswrt/translations/no.json b/homeassistant/components/asuswrt/translations/no.json index de540484cac..39539e74f1c 100644 --- a/homeassistant/components/asuswrt/translations/no.json +++ b/homeassistant/components/asuswrt/translations/no.json @@ -2,9 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Umulig \u00e5 bestemme en gyldig unik ID for enheten", - "no_unique_id": "En enhet uten en gyldig unik ID er allerede konfigurert. Konfigurasjon av flere forekomster er ikke mulig", - "not_unique_id_exist": "En enhet uten en gyldig unik ID er allerede konfigurert. Konfigurasjon av flere forekomster er ikke mulig", - "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + "no_unique_id": "En enhet uten en gyldig unik ID er allerede konfigurert. Konfigurasjon av flere forekomster er ikke mulig" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/asuswrt/translations/pl.json b/homeassistant/components/asuswrt/translations/pl.json index d30a1e9e778..7a03225633d 100644 --- a/homeassistant/components/asuswrt/translations/pl.json +++ b/homeassistant/components/asuswrt/translations/pl.json @@ -2,9 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Nie mo\u017cna okre\u015bli\u0107 prawid\u0142owego unikalnego identyfikatora urz\u0105dzenia", - "no_unique_id": "Urz\u0105dzenie bez prawid\u0142owego unikalnego identyfikatora jest ju\u017c skonfigurowane. Konfiguracja wielu instancji nie jest mo\u017cliwa.", - "not_unique_id_exist": "Urz\u0105dzenie bez prawid\u0142owego unikalnego identyfikatora jest ju\u017c skonfigurowane. Konfiguracja wielu instancji nie jest mo\u017cliwa.", - "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + "no_unique_id": "Urz\u0105dzenie bez prawid\u0142owego unikalnego identyfikatora jest ju\u017c skonfigurowane. Konfiguracja wielu instancji nie jest mo\u017cliwa." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", diff --git a/homeassistant/components/asuswrt/translations/pt-BR.json b/homeassistant/components/asuswrt/translations/pt-BR.json index 24f2ab51305..a42cab6fe0d 100644 --- a/homeassistant/components/asuswrt/translations/pt-BR.json +++ b/homeassistant/components/asuswrt/translations/pt-BR.json @@ -2,9 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Imposs\u00edvel determinar um ID exclusivo v\u00e1lido para o dispositivo", - "no_unique_id": "Um dispositivo sem um ID exclusivo v\u00e1lido j\u00e1 est\u00e1 configurado. A configura\u00e7\u00e3o de v\u00e1rias inst\u00e2ncias n\u00e3o \u00e9 poss\u00edvel", - "not_unique_id_exist": "Um dispositivo sem um ID exclusivo v\u00e1lido j\u00e1 est\u00e1 configurado. A configura\u00e7\u00e3o de v\u00e1rias inst\u00e2ncias n\u00e3o \u00e9 poss\u00edvel", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + "no_unique_id": "[%key:component::asuswrt::config::abort::not_unique_id_exist%]" }, "error": { "cannot_connect": "Falha ao conectar", diff --git a/homeassistant/components/asuswrt/translations/ru.json b/homeassistant/components/asuswrt/translations/ru.json index 923faf174a5..0253cd20d1b 100644 --- a/homeassistant/components/asuswrt/translations/ru.json +++ b/homeassistant/components/asuswrt/translations/ru.json @@ -2,9 +2,7 @@ "config": { "abort": { "invalid_unique_id": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0434\u043b\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", - "no_unique_id": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0431\u0435\u0437 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0433\u043e \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e. \u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u043e\u0432 \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430.", - "not_unique_id_exist": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0431\u0435\u0437 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0433\u043e \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e. \u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u043e\u0432 \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430.", - "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + "no_unique_id": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0431\u0435\u0437 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0433\u043e \u0443\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e. \u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u043e\u0432 \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", diff --git a/homeassistant/components/asuswrt/translations/sv.json b/homeassistant/components/asuswrt/translations/sv.json index ba461c681e3..057a107356a 100644 --- a/homeassistant/components/asuswrt/translations/sv.json +++ b/homeassistant/components/asuswrt/translations/sv.json @@ -2,9 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Om\u00f6jligt att fastst\u00e4lla ett giltigt unikt ID f\u00f6r enheten", - "no_unique_id": "En enhet utan ett giltigt unikt ID \u00e4r redan konfigurerad. Konfiguration av flera instanser \u00e4r inte m\u00f6jlig", - "not_unique_id_exist": "En enhet utan ett giltigt unikt ID \u00e4r redan konfigurerad. Konfiguration av flera instanser \u00e4r inte m\u00f6jlig", - "single_instance_allowed": "Redan konfigurerad. Bara en konfiguration \u00e4r m\u00f6jlig." + "no_unique_id": "En enhet utan ett giltigt unikt ID \u00e4r redan konfigurerad. Konfiguration av flera instanser \u00e4r inte m\u00f6jlig" }, "error": { "cannot_connect": "Kunde inte ansluta", diff --git a/homeassistant/components/asuswrt/translations/tr.json b/homeassistant/components/asuswrt/translations/tr.json index 10be601fd10..177c069298f 100644 --- a/homeassistant/components/asuswrt/translations/tr.json +++ b/homeassistant/components/asuswrt/translations/tr.json @@ -2,9 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Cihaz i\u00e7in ge\u00e7erli bir benzersiz kimlik belirlemek imkans\u0131z", - "no_unique_id": "Ge\u00e7erli bir benzersiz kimli\u011fi olmayan bir cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Birden \u00e7ok \u00f6rne\u011fin yap\u0131land\u0131r\u0131lmas\u0131 m\u00fcmk\u00fcn de\u011fil", - "not_unique_id_exist": "Ge\u00e7erli bir benzersiz kimli\u011fi olmayan bir cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Birden \u00e7ok \u00f6rne\u011fin yap\u0131land\u0131r\u0131lmas\u0131 m\u00fcmk\u00fcn de\u011fil", - "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + "no_unique_id": "Ge\u00e7erli bir benzersiz kimli\u011fi olmayan bir cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Birden \u00e7ok \u00f6rne\u011fin yap\u0131land\u0131r\u0131lmas\u0131 m\u00fcmk\u00fcn de\u011fil" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", diff --git a/homeassistant/components/asuswrt/translations/zh-Hans.json b/homeassistant/components/asuswrt/translations/zh-Hans.json index 69f7bf98df3..a8e6dbf8c10 100644 --- a/homeassistant/components/asuswrt/translations/zh-Hans.json +++ b/homeassistant/components/asuswrt/translations/zh-Hans.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "single_instance_allowed": "\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86\uff0c\u4e14\u53ea\u80fd\u914d\u7f6e\u4e00\u6b21\u3002" - }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", "invalid_host": "\u65e0\u6548\u7684\u4e3b\u673a\u5730\u5740\u6216 IP \u5730\u5740", diff --git a/homeassistant/components/asuswrt/translations/zh-Hant.json b/homeassistant/components/asuswrt/translations/zh-Hant.json index d5955313b93..a8c4d18023a 100644 --- a/homeassistant/components/asuswrt/translations/zh-Hant.json +++ b/homeassistant/components/asuswrt/translations/zh-Hant.json @@ -2,9 +2,7 @@ "config": { "abort": { "invalid_unique_id": "\u7121\u6cd5\u78ba\u8a8d\u88dd\u7f6e\u6709\u6548\u552f\u4e00 ID", - "no_unique_id": "\u5df2\u8a2d\u5b9a\u4e0d\u5177\u6709\u6548\u552f\u4e00 ID \u7684\u88dd\u7f6e\uff0c\u7121\u6cd5\u8a2d\u5b9a\u591a\u500b\u5be6\u4f8b", - "not_unique_id_exist": "\u5df2\u8a2d\u5b9a\u4e0d\u5177\u6709\u6548\u552f\u4e00 ID \u7684\u88dd\u7f6e\uff0c\u7121\u6cd5\u8a2d\u5b9a\u591a\u500b\u5be6\u4f8b", - "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "no_unique_id": "\u5df2\u8a2d\u5b9a\u4e0d\u5177\u6709\u6548\u552f\u4e00 ID \u7684\u88dd\u7f6e\uff0c\u7121\u6cd5\u8a2d\u5b9a\u591a\u500b\u5be6\u4f8b" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/aurora_abb_powerone/translations/bg.json b/homeassistant/components/aurora_abb_powerone/translations/bg.json index 88f52d84269..37b6f40c82e 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/bg.json +++ b/homeassistant/components/aurora_abb_powerone/translations/bg.json @@ -2,9 +2,6 @@ "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": "\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/aurora_abb_powerone/translations/ca.json b/homeassistant/components/aurora_abb_powerone/translations/ca.json index 98f77dad13b..43b4acd25ea 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/ca.json +++ b/homeassistant/components/aurora_abb_powerone/translations/ca.json @@ -7,8 +7,7 @@ "error": { "cannot_connect": "No s'ha pogut connectar, comprova el port s\u00e8rie, l'adre\u00e7a, la connexi\u00f3 el\u00e8ctrica i que l'inversor estigui enc\u00e8s", "cannot_open_serial_port": "No s'ha pogut obrir el port s\u00e8rie, comprova'l i torna-ho a provar", - "invalid_serial_port": "El port s\u00e8rie no t\u00e9 un dispositiu v\u00e0lid o no s'ha pogut obrir", - "unknown": "Error inesperat" + "invalid_serial_port": "El port s\u00e8rie no t\u00e9 un dispositiu v\u00e0lid o no s'ha pogut obrir" }, "step": { "user": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/cs.json b/homeassistant/components/aurora_abb_powerone/translations/cs.json deleted file mode 100644 index e1bf8e7f45f..00000000000 --- a/homeassistant/components/aurora_abb_powerone/translations/cs.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "error": { - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/de.json b/homeassistant/components/aurora_abb_powerone/translations/de.json index a60138da16d..fab4d1f6e31 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/de.json +++ b/homeassistant/components/aurora_abb_powerone/translations/de.json @@ -7,8 +7,7 @@ "error": { "cannot_connect": "Verbindung kann nicht hergestellt werden, bitte \u00fcberpr\u00fcfe den seriellen Anschluss, die Adresse, die elektrische Verbindung und ob der Wechselrichter eingeschaltet ist (bei Tageslicht)", "cannot_open_serial_port": "Serielle Schnittstelle kann nicht ge\u00f6ffnet werden, bitte pr\u00fcfen und erneut versuchen", - "invalid_serial_port": "Serielle Schnittstelle ist kein g\u00fcltiges Ger\u00e4t oder konnte nicht ge\u00f6ffnet werden", - "unknown": "Unerwarteter Fehler" + "invalid_serial_port": "Serielle Schnittstelle ist kein g\u00fcltiges Ger\u00e4t oder konnte nicht ge\u00f6ffnet werden" }, "step": { "user": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/el.json b/homeassistant/components/aurora_abb_powerone/translations/el.json index 834c5794861..52b43a668be 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/el.json +++ b/homeassistant/components/aurora_abb_powerone/translations/el.json @@ -7,8 +7,7 @@ "error": { "cannot_connect": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7, \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1, \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7, \u03c4\u03b7\u03bd \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03b9\u03ba\u03ae \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03cc\u03c4\u03b9 \u03bf \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ad\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 (\u03c3\u03c4\u03bf \u03c6\u03c9\u03c2 \u03c4\u03b7\u03c2 \u03b7\u03bc\u03ad\u03c1\u03b1\u03c2)", "cannot_open_serial_port": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc \u03c4\u03bf \u03ac\u03bd\u03bf\u03b9\u03b3\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03b8\u03cd\u03c1\u03b1\u03c2, \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac", - "invalid_serial_port": "\u0397 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ae \u03b4\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc \u03bd\u03b1 \u03b1\u03bd\u03bf\u03af\u03be\u03b5\u03b9", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + "invalid_serial_port": "\u0397 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ae \u03b4\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc \u03bd\u03b1 \u03b1\u03bd\u03bf\u03af\u03be\u03b5\u03b9" }, "step": { "user": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/en.json b/homeassistant/components/aurora_abb_powerone/translations/en.json index fe5d668f573..748a570f291 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/en.json +++ b/homeassistant/components/aurora_abb_powerone/translations/en.json @@ -7,8 +7,7 @@ "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" + "invalid_serial_port": "Serial port is not a valid device or could not be openned" }, "step": { "user": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/en_GB.json b/homeassistant/components/aurora_abb_powerone/translations/en_GB.json index 59c6263bd14..25f797239b9 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/en_GB.json +++ b/homeassistant/components/aurora_abb_powerone/translations/en_GB.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "unknown": "Unexpected error" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/es.json b/homeassistant/components/aurora_abb_powerone/translations/es.json index f71039bda56..4e1d95a126d 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/es.json +++ b/homeassistant/components/aurora_abb_powerone/translations/es.json @@ -7,8 +7,7 @@ "error": { "cannot_connect": "No se puede conectar, por favor, compruebe el puerto serie, la direcci\u00f3n, la conexi\u00f3n el\u00e9ctrica y que el inversor est\u00e1 encendido", "cannot_open_serial_port": "No se puede abrir el puerto serie, por favor, compruebe y vuelva a intentarlo", - "invalid_serial_port": "El puerto serie no es v\u00e1lido para este dispositivo o no se ha podido abrir", - "unknown": "Error inesperado" + "invalid_serial_port": "El puerto serie no es v\u00e1lido para este dispositivo o no se ha podido abrir" }, "step": { "user": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/et.json b/homeassistant/components/aurora_abb_powerone/translations/et.json index b7764fa0f33..115bab56eca 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/et.json +++ b/homeassistant/components/aurora_abb_powerone/translations/et.json @@ -7,8 +7,7 @@ "error": { "cannot_connect": "\u00dchendust ei saa luua, palun kontrolli jadaporti, aadressi, elektri\u00fchendust ja et inverter on sisse l\u00fclitatud (p\u00e4evavalguses)", "cannot_open_serial_port": "Jadaporti ei saa avada, kontrolli ja proovi uuesti", - "invalid_serial_port": "Jadaport pole sobiv seade v\u00f5i seda ei saa avada", - "unknown": "Ootamatu t\u00f5rge" + "invalid_serial_port": "Jadaport pole sobiv seade v\u00f5i seda ei saa avada" }, "step": { "user": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/fr.json b/homeassistant/components/aurora_abb_powerone/translations/fr.json index 206167a5669..c36cc62196f 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/fr.json +++ b/homeassistant/components/aurora_abb_powerone/translations/fr.json @@ -7,8 +7,7 @@ "error": { "cannot_connect": "Connexion impossible, veuillez v\u00e9rifier le port s\u00e9rie, l'adresse, la connexion \u00e9lectrique et que l'onduleur est allum\u00e9 (\u00e0 la lumi\u00e8re du jour)", "cannot_open_serial_port": "Impossible d'ouvrir le port s\u00e9rie, veuillez v\u00e9rifier et r\u00e9essayer", - "invalid_serial_port": "Le port s\u00e9rie n'est pas un p\u00e9riph\u00e9rique valide ou n'a pas pu \u00eatre ouvert", - "unknown": "Erreur inattendue" + "invalid_serial_port": "Le port s\u00e9rie n'est pas un p\u00e9riph\u00e9rique valide ou n'a pas pu \u00eatre ouvert" }, "step": { "user": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/he.json b/homeassistant/components/aurora_abb_powerone/translations/he.json index ea40181bd9a..cdb921611c4 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/he.json +++ b/homeassistant/components/aurora_abb_powerone/translations/he.json @@ -2,9 +2,6 @@ "config": { "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" - }, - "error": { - "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" } } } \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/hu.json b/homeassistant/components/aurora_abb_powerone/translations/hu.json index ffc812dfd4b..6342466bffe 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/hu.json +++ b/homeassistant/components/aurora_abb_powerone/translations/hu.json @@ -7,8 +7,7 @@ "error": { "cannot_connect": "A csatlakoz\u00e1s sikertelen. Ellen\u0151rizze a soros portot, a c\u00edmet, az elektromos csatlakoz\u00e1st \u00e9s azt, hogy az inverter be van-e kapcsolva (nappal).", "cannot_open_serial_port": "A soros port nem nyithat\u00f3 meg, k\u00e9rem ellen\u0151rizze \u00e9s pr\u00f3b\u00e1lkozzon \u00fajra", - "invalid_serial_port": "A soros port nem \u00e9rv\u00e9nyes eszk\u00f6z, vagy nem nyithat\u00f3 meg", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "invalid_serial_port": "A soros port nem \u00e9rv\u00e9nyes eszk\u00f6z, vagy nem nyithat\u00f3 meg" }, "step": { "user": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/id.json b/homeassistant/components/aurora_abb_powerone/translations/id.json index 4157502c2ad..70811e170a1 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/id.json +++ b/homeassistant/components/aurora_abb_powerone/translations/id.json @@ -7,8 +7,7 @@ "error": { "cannot_connect": "Tidak dapat terhubung, periksa port serial, alamat, koneksi listrik dan apakah inverter sedang nyala (di siang hari)", "cannot_open_serial_port": "Tidak dapat membuka port serial, periksa dan coba lagi", - "invalid_serial_port": "Port serial bukan perangkat yang valid atau tidak dapat dibuka", - "unknown": "Kesalahan yang tidak diharapkan" + "invalid_serial_port": "Port serial bukan perangkat yang valid atau tidak dapat dibuka" }, "step": { "user": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/it.json b/homeassistant/components/aurora_abb_powerone/translations/it.json index bb534d079cc..765aaf5e9ed 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/it.json +++ b/homeassistant/components/aurora_abb_powerone/translations/it.json @@ -7,8 +7,7 @@ "error": { "cannot_connect": "Impossibile connettersi, controllare la porta seriale, l'indirizzo, la connessione elettrica e che l'inverter sia acceso (alla luce del giorno)", "cannot_open_serial_port": "Impossibile aprire la porta seriale, controllare e riprovare", - "invalid_serial_port": "La porta seriale non \u00e8 un dispositivo valido o non pu\u00f2 essere aperta", - "unknown": "Errore imprevisto" + "invalid_serial_port": "La porta seriale non \u00e8 un dispositivo valido o non pu\u00f2 essere aperta" }, "step": { "user": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/ja.json b/homeassistant/components/aurora_abb_powerone/translations/ja.json index a558f088f07..5977b1513b0 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/ja.json +++ b/homeassistant/components/aurora_abb_powerone/translations/ja.json @@ -7,8 +7,7 @@ "error": { "cannot_connect": "\u63a5\u7d9a\u3067\u304d\u306a\u3044\u5834\u5408\u306f\u3001\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3001\u30a2\u30c9\u30ec\u30b9\u3001\u96fb\u6c17\u7684\u63a5\u7d9a\u3092\u78ba\u8a8d\u3057\u3001\u30a4\u30f3\u30d0\u30fc\u30bf\u30fc\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044(\u663c\u9593)", "cannot_open_serial_port": "\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3092\u958b\u3051\u307e\u305b\u3093\u3002\u78ba\u8a8d\u3057\u3066\u304b\u3089\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044", - "invalid_serial_port": "\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u304c\u6709\u52b9\u306a\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u306a\u3044\u3001\u3082\u3057\u304f\u306f\u958b\u304f\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + "invalid_serial_port": "\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u304c\u6709\u52b9\u306a\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u306a\u3044\u3001\u3082\u3057\u304f\u306f\u958b\u304f\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" }, "step": { "user": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/nl.json b/homeassistant/components/aurora_abb_powerone/translations/nl.json index d70113e9c19..cb5cd511f81 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/nl.json +++ b/homeassistant/components/aurora_abb_powerone/translations/nl.json @@ -7,8 +7,7 @@ "error": { "cannot_connect": "Kan geen verbinding maken, controleer de seri\u00eble poort, het adres, de elektrische aansluiting en of de omvormer aan staat (bij daglicht)", "cannot_open_serial_port": "Kan seri\u00eble poort niet openen, controleer en probeer het opnieuw", - "invalid_serial_port": "Seri\u00eble poort is geen geldig apparaat of kan niet worden geopend", - "unknown": "Onverwachte fout" + "invalid_serial_port": "Seri\u00eble poort is geen geldig apparaat of kan niet worden geopend" }, "step": { "user": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/no.json b/homeassistant/components/aurora_abb_powerone/translations/no.json index 9d4cd656f45..38cef3a2c96 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/no.json +++ b/homeassistant/components/aurora_abb_powerone/translations/no.json @@ -7,8 +7,7 @@ "error": { "cannot_connect": "Kan ikke koble til, sjekk seriell port, adresse, elektrisk tilkobling og at omformeren er p\u00e5 (i dagslys)", "cannot_open_serial_port": "Kan ikke \u00e5pne serieporten, sjekk og pr\u00f8v igjen", - "invalid_serial_port": "Seriell port er ikke en gyldig enhet eller kunne ikke \u00e5pnes", - "unknown": "Uventet feil" + "invalid_serial_port": "Seriell port er ikke en gyldig enhet eller kunne ikke \u00e5pnes" }, "step": { "user": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/pl.json b/homeassistant/components/aurora_abb_powerone/translations/pl.json index d6131cfa195..3b773295256 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/pl.json +++ b/homeassistant/components/aurora_abb_powerone/translations/pl.json @@ -7,8 +7,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, sprawd\u017a port szeregowy, adres, po\u0142\u0105czenie elektryczne i czy falownik jest w\u0142\u0105czony (w \u015bwietle dziennym)", "cannot_open_serial_port": "Nie mo\u017cna otworzy\u0107 portu szeregowego, sprawd\u017a i spr\u00f3buj ponownie", - "invalid_serial_port": "Port szeregowy nie jest prawid\u0142owym urz\u0105dzeniem lub nie mo\u017cna go otworzy\u0107", - "unknown": "Nieoczekiwany b\u0142\u0105d" + "invalid_serial_port": "Port szeregowy nie jest prawid\u0142owym urz\u0105dzeniem lub nie mo\u017cna go otworzy\u0107" }, "step": { "user": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/pt-BR.json b/homeassistant/components/aurora_abb_powerone/translations/pt-BR.json index 8fb79bcefa2..82dbcb466ec 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/pt-BR.json +++ b/homeassistant/components/aurora_abb_powerone/translations/pt-BR.json @@ -7,8 +7,7 @@ "error": { "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar, verifique a porta serial, endere\u00e7o, conex\u00e3o el\u00e9trica e se o inversor est\u00e1 ligado (\u00e0 luz do dia)", "cannot_open_serial_port": "N\u00e3o \u00e9 poss\u00edvel abrir a porta serial, verifique e tente novamente", - "invalid_serial_port": "A porta serial n\u00e3o \u00e9 um dispositivo v\u00e1lido ou n\u00e3o p\u00f4de ser aberta", - "unknown": "Erro inesperado" + "invalid_serial_port": "A porta serial n\u00e3o \u00e9 um dispositivo v\u00e1lido ou n\u00e3o p\u00f4de ser aberta" }, "step": { "user": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/ru.json b/homeassistant/components/aurora_abb_powerone/translations/ru.json index 6baec9324d2..22e73931232 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/ru.json +++ b/homeassistant/components/aurora_abb_powerone/translations/ru.json @@ -7,8 +7,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442, \u0430\u0434\u0440\u0435\u0441, \u044d\u043b\u0435\u043a\u0442\u0440\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0438 \u0447\u0442\u043e \u0438\u043d\u0432\u0435\u0440\u0442\u043e\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d (\u043f\u0440\u0438 \u0434\u043d\u0435\u0432\u043d\u043e\u043c \u0441\u0432\u0435\u0442\u0435).", "cannot_open_serial_port": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", - "invalid_serial_port": "\u041f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u043c \u0438\u043b\u0438 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043e\u0442\u043a\u0440\u044b\u0442.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "invalid_serial_port": "\u041f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442 \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u043c \u0438\u043b\u0438 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043e\u0442\u043a\u0440\u044b\u0442." }, "step": { "user": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/sl.json b/homeassistant/components/aurora_abb_powerone/translations/sl.json index 87fce100c77..16a4bd0b1c1 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/sl.json +++ b/homeassistant/components/aurora_abb_powerone/translations/sl.json @@ -2,9 +2,6 @@ "config": { "abort": { "already_configured": "Naprava je \u017ee konfigurirana" - }, - "error": { - "unknown": "Nepri\u010dakovana napaka" } } } \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/tr.json b/homeassistant/components/aurora_abb_powerone/translations/tr.json index ec8ee6da4a3..39a046adf43 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/tr.json +++ b/homeassistant/components/aurora_abb_powerone/translations/tr.json @@ -7,8 +7,7 @@ "error": { "cannot_connect": "Ba\u011flant\u0131 kurulam\u0131yor, l\u00fctfen seri portu, adresi, elektrik ba\u011flant\u0131s\u0131n\u0131 ve invert\u00f6r\u00fcn a\u00e7\u0131k oldu\u011funu (g\u00fcn \u0131\u015f\u0131\u011f\u0131nda) kontrol edin.", "cannot_open_serial_port": "Seri ba\u011flant\u0131 noktas\u0131 a\u00e7\u0131lam\u0131yor, l\u00fctfen kontrol edip tekrar deneyin", - "invalid_serial_port": "Seri ba\u011flant\u0131 noktas\u0131 ge\u00e7erli bir ayg\u0131t de\u011fil veya a\u00e7\u0131lamad\u0131", - "unknown": "Beklenmeyen hata" + "invalid_serial_port": "Seri ba\u011flant\u0131 noktas\u0131 ge\u00e7erli bir ayg\u0131t de\u011fil veya a\u00e7\u0131lamad\u0131" }, "step": { "user": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/zh-Hant.json b/homeassistant/components/aurora_abb_powerone/translations/zh-Hant.json index 0a05e640994..bec84f72b80 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/zh-Hant.json +++ b/homeassistant/components/aurora_abb_powerone/translations/zh-Hant.json @@ -7,8 +7,7 @@ "error": { "cannot_connect": "\u7121\u6cd5\u9023\u63a5\uff0c\u8acb\u6aa2\u67e5\u5e8f\u5217\u57e0\u3001\u4f4d\u5740\u3001\u96fb\u529b\u9023\u63a5\uff0c\u4e26\u78ba\u5b9a\u8a72\u8b8a\u6d41\u5668\u70ba\u958b\u555f\u72c0\u614b\uff08\u767d\u5929\uff09", "cannot_open_serial_port": "\u7121\u6cd5\u958b\u555f\u5e8f\u5217\u57e0\u3001\u8acb\u6aa2\u67e5\u5f8c\u518d\u8a66\u4e00\u6b21", - "invalid_serial_port": "\u5e8f\u5217\u57e0\u70ba\u7121\u6548\u88dd\u7f6e\u6216\u7121\u6cd5\u958b\u555f", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "invalid_serial_port": "\u5e8f\u5217\u57e0\u70ba\u7121\u6548\u88dd\u7f6e\u6216\u7121\u6cd5\u958b\u555f" }, "step": { "user": { diff --git a/homeassistant/components/aussie_broadband/translations/bg.json b/homeassistant/components/aussie_broadband/translations/bg.json index 8c36ef66120..8098935e86c 100644 --- a/homeassistant/components/aussie_broadband/translations/bg.json +++ b/homeassistant/components/aussie_broadband/translations/bg.json @@ -11,13 +11,6 @@ "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" - }, - "description": "\u0410\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \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" - }, "reauth_confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" diff --git a/homeassistant/components/aussie_broadband/translations/ca.json b/homeassistant/components/aussie_broadband/translations/ca.json index 83ec53ad5a4..4688357a3c8 100644 --- a/homeassistant/components/aussie_broadband/translations/ca.json +++ b/homeassistant/components/aussie_broadband/translations/ca.json @@ -11,13 +11,6 @@ "unknown": "Error inesperat" }, "step": { - "reauth": { - "data": { - "password": "Contrasenya" - }, - "description": "Actualitza la contrasenya de {username}", - "title": "Reautenticaci\u00f3 de la integraci\u00f3" - }, "reauth_confirm": { "data": { "password": "Contrasenya" diff --git a/homeassistant/components/aussie_broadband/translations/cs.json b/homeassistant/components/aussie_broadband/translations/cs.json index 8ea2970a132..131dddca93a 100644 --- a/homeassistant/components/aussie_broadband/translations/cs.json +++ b/homeassistant/components/aussie_broadband/translations/cs.json @@ -10,12 +10,6 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { - "reauth": { - "data": { - "password": "Heslo" - }, - "title": "Znovu ov\u011b\u0159it integraci" - }, "user": { "data": { "password": "Heslo", diff --git a/homeassistant/components/aussie_broadband/translations/de.json b/homeassistant/components/aussie_broadband/translations/de.json index ed5cd5fd02c..e4c5bd327ff 100644 --- a/homeassistant/components/aussie_broadband/translations/de.json +++ b/homeassistant/components/aussie_broadband/translations/de.json @@ -11,13 +11,6 @@ "unknown": "Unerwarteter Fehler" }, "step": { - "reauth": { - "data": { - "password": "Passwort" - }, - "description": "Passwort f\u00fcr {username} aktualisieren", - "title": "Integration erneut authentifizieren" - }, "reauth_confirm": { "data": { "password": "Passwort" diff --git a/homeassistant/components/aussie_broadband/translations/el.json b/homeassistant/components/aussie_broadband/translations/el.json index 0b78eacb826..c1e36e01654 100644 --- a/homeassistant/components/aussie_broadband/translations/el.json +++ b/homeassistant/components/aussie_broadband/translations/el.json @@ -11,13 +11,6 @@ "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { - "reauth": { - "data": { - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" - }, - "description": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 {username}", - "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" - }, "reauth_confirm": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" diff --git a/homeassistant/components/aussie_broadband/translations/en.json b/homeassistant/components/aussie_broadband/translations/en.json index 2843916df2e..4d18251f270 100644 --- a/homeassistant/components/aussie_broadband/translations/en.json +++ b/homeassistant/components/aussie_broadband/translations/en.json @@ -11,13 +11,6 @@ "unknown": "Unexpected error" }, "step": { - "reauth": { - "data": { - "password": "Password" - }, - "description": "Update password for {username}", - "title": "Reauthenticate Integration" - }, "reauth_confirm": { "data": { "password": "Password" diff --git a/homeassistant/components/aussie_broadband/translations/es-419.json b/homeassistant/components/aussie_broadband/translations/es-419.json index df9322bf01d..f90e4891935 100644 --- a/homeassistant/components/aussie_broadband/translations/es-419.json +++ b/homeassistant/components/aussie_broadband/translations/es-419.json @@ -4,9 +4,6 @@ "no_services_found": "No se encontraron servicios para esta cuenta" }, "step": { - "reauth": { - "description": "Actualizar contrase\u00f1a para {username}" - }, "service": { "data": { "services": "Servicios" diff --git a/homeassistant/components/aussie_broadband/translations/es.json b/homeassistant/components/aussie_broadband/translations/es.json index e0df929fe9e..1410af6ad76 100644 --- a/homeassistant/components/aussie_broadband/translations/es.json +++ b/homeassistant/components/aussie_broadband/translations/es.json @@ -9,10 +9,6 @@ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { - "reauth": { - "description": "Actualizar la contrase\u00f1a de {username}", - "title": "Reautenticaci\u00f3n de la integraci\u00f3n" - }, "reauth_confirm": { "data": { "password": "Contrase\u00f1a" diff --git a/homeassistant/components/aussie_broadband/translations/et.json b/homeassistant/components/aussie_broadband/translations/et.json index 7dbf2d26b59..0d0d97b6ff2 100644 --- a/homeassistant/components/aussie_broadband/translations/et.json +++ b/homeassistant/components/aussie_broadband/translations/et.json @@ -11,13 +11,6 @@ "unknown": "Ootamatu t\u00f5rge" }, "step": { - "reauth": { - "data": { - "password": "Salas\u00f5na" - }, - "description": "{username} salas\u00f5na v\u00e4rskendamine", - "title": "Taastuvasta sidumine" - }, "reauth_confirm": { "data": { "password": "Salas\u00f5na" diff --git a/homeassistant/components/aussie_broadband/translations/fr.json b/homeassistant/components/aussie_broadband/translations/fr.json index d540e5fd2f9..37d570e77d6 100644 --- a/homeassistant/components/aussie_broadband/translations/fr.json +++ b/homeassistant/components/aussie_broadband/translations/fr.json @@ -11,13 +11,6 @@ "unknown": "Erreur inattendue" }, "step": { - "reauth": { - "data": { - "password": "Mot de passe" - }, - "description": "Mettre \u00e0 jour le mot de passe pour {username}", - "title": "R\u00e9-authentifier l'int\u00e9gration" - }, "reauth_confirm": { "data": { "password": "Mot de passe" diff --git a/homeassistant/components/aussie_broadband/translations/he.json b/homeassistant/components/aussie_broadband/translations/he.json index 5861357a6d4..7a1ef5f9cc5 100644 --- a/homeassistant/components/aussie_broadband/translations/he.json +++ b/homeassistant/components/aussie_broadband/translations/he.json @@ -10,12 +10,6 @@ "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { - "reauth": { - "data": { - "password": "\u05e1\u05d9\u05e1\u05de\u05d4" - }, - "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" - }, "reauth_confirm": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" diff --git a/homeassistant/components/aussie_broadband/translations/hu.json b/homeassistant/components/aussie_broadband/translations/hu.json index a8c3543873d..2ad64c2155d 100644 --- a/homeassistant/components/aussie_broadband/translations/hu.json +++ b/homeassistant/components/aussie_broadband/translations/hu.json @@ -11,13 +11,6 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { - "reauth": { - "data": { - "password": "Jelsz\u00f3" - }, - "description": "Jelsz\u00f3 friss\u00edt\u00e9se {username} sz\u00e1m\u00e1ra", - "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" - }, "reauth_confirm": { "data": { "password": "Jelsz\u00f3" diff --git a/homeassistant/components/aussie_broadband/translations/id.json b/homeassistant/components/aussie_broadband/translations/id.json index 18020014268..d3353b46f3c 100644 --- a/homeassistant/components/aussie_broadband/translations/id.json +++ b/homeassistant/components/aussie_broadband/translations/id.json @@ -11,13 +11,6 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { - "reauth": { - "data": { - "password": "Kata Sandi" - }, - "description": "Perbarui kata sandi untuk {username}", - "title": "Autentikasi Ulang Integrasi" - }, "reauth_confirm": { "data": { "password": "Kata Sandi" diff --git a/homeassistant/components/aussie_broadband/translations/it.json b/homeassistant/components/aussie_broadband/translations/it.json index a29e10db60a..6c53ec28dff 100644 --- a/homeassistant/components/aussie_broadband/translations/it.json +++ b/homeassistant/components/aussie_broadband/translations/it.json @@ -11,13 +11,6 @@ "unknown": "Errore imprevisto" }, "step": { - "reauth": { - "data": { - "password": "Password" - }, - "description": "Aggiorna la password per {username}", - "title": "Autentica nuovamente l'integrazione" - }, "reauth_confirm": { "data": { "password": "Password" diff --git a/homeassistant/components/aussie_broadband/translations/ja.json b/homeassistant/components/aussie_broadband/translations/ja.json index f08e02f73c1..d8351bbc98b 100644 --- a/homeassistant/components/aussie_broadband/translations/ja.json +++ b/homeassistant/components/aussie_broadband/translations/ja.json @@ -11,13 +11,6 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { - "reauth": { - "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - }, - "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" - }, "reauth_confirm": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" diff --git a/homeassistant/components/aussie_broadband/translations/nl.json b/homeassistant/components/aussie_broadband/translations/nl.json index 21da52666bc..ec95145c67b 100644 --- a/homeassistant/components/aussie_broadband/translations/nl.json +++ b/homeassistant/components/aussie_broadband/translations/nl.json @@ -11,13 +11,6 @@ "unknown": "Onverwachte fout" }, "step": { - "reauth": { - "data": { - "password": "Wachtwoord" - }, - "description": "Update wachtwoord voor {username}", - "title": "Verifieer de integratie opnieuw" - }, "reauth_confirm": { "data": { "password": "Wachtwoord" diff --git a/homeassistant/components/aussie_broadband/translations/no.json b/homeassistant/components/aussie_broadband/translations/no.json index 8129a8c9cc2..00e911bbfba 100644 --- a/homeassistant/components/aussie_broadband/translations/no.json +++ b/homeassistant/components/aussie_broadband/translations/no.json @@ -11,13 +11,6 @@ "unknown": "Uventet feil" }, "step": { - "reauth": { - "data": { - "password": "Passord" - }, - "description": "Oppdater passordet for {username}", - "title": "Godkjenne integrering p\u00e5 nytt" - }, "reauth_confirm": { "data": { "password": "Passord" diff --git a/homeassistant/components/aussie_broadband/translations/pl.json b/homeassistant/components/aussie_broadband/translations/pl.json index c2f686c11dc..ed6860b31f8 100644 --- a/homeassistant/components/aussie_broadband/translations/pl.json +++ b/homeassistant/components/aussie_broadband/translations/pl.json @@ -11,13 +11,6 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { - "reauth": { - "data": { - "password": "Has\u0142o" - }, - "description": "Zaktualizuj has\u0142o dla {username}", - "title": "Ponownie uwierzytelnij integracj\u0119" - }, "reauth_confirm": { "data": { "password": "Has\u0142o" diff --git a/homeassistant/components/aussie_broadband/translations/pt-BR.json b/homeassistant/components/aussie_broadband/translations/pt-BR.json index efe1fca7c80..884d426a04b 100644 --- a/homeassistant/components/aussie_broadband/translations/pt-BR.json +++ b/homeassistant/components/aussie_broadband/translations/pt-BR.json @@ -11,13 +11,6 @@ "unknown": "Erro inesperado" }, "step": { - "reauth": { - "data": { - "password": "Senha" - }, - "description": "Atualizar senha para {username}", - "title": "Reautenticar Integra\u00e7\u00e3o" - }, "reauth_confirm": { "data": { "password": "Senha" diff --git a/homeassistant/components/aussie_broadband/translations/ru.json b/homeassistant/components/aussie_broadband/translations/ru.json index 15a9f44a98a..24a3c3d67fb 100644 --- a/homeassistant/components/aussie_broadband/translations/ru.json +++ b/homeassistant/components/aussie_broadband/translations/ru.json @@ -11,13 +11,6 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { - "reauth": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u044c" - }, - "description": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {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" - }, "reauth_confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" diff --git a/homeassistant/components/aussie_broadband/translations/sv.json b/homeassistant/components/aussie_broadband/translations/sv.json index a82ed912c19..b159fb71b87 100644 --- a/homeassistant/components/aussie_broadband/translations/sv.json +++ b/homeassistant/components/aussie_broadband/translations/sv.json @@ -11,13 +11,6 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { - "reauth": { - "data": { - "password": "L\u00f6senord" - }, - "description": "Uppdatera l\u00f6senord f\u00f6r {username}", - "title": "\u00c5terautentisera integration" - }, "service": { "data": { "services": "Tj\u00e4nster" diff --git a/homeassistant/components/aussie_broadband/translations/tr.json b/homeassistant/components/aussie_broadband/translations/tr.json index 28eae33719d..e9d656acb26 100644 --- a/homeassistant/components/aussie_broadband/translations/tr.json +++ b/homeassistant/components/aussie_broadband/translations/tr.json @@ -11,13 +11,6 @@ "unknown": "Beklenmeyen hata" }, "step": { - "reauth": { - "data": { - "password": "Parola" - }, - "description": "{username} i\u00e7in \u015fifreyi g\u00fcncelleyin", - "title": "Entegrasyonu Yeniden Do\u011frula" - }, "reauth_confirm": { "data": { "password": "Parola" diff --git a/homeassistant/components/aussie_broadband/translations/zh-Hant.json b/homeassistant/components/aussie_broadband/translations/zh-Hant.json index f7beefb2962..5f248360de1 100644 --- a/homeassistant/components/aussie_broadband/translations/zh-Hant.json +++ b/homeassistant/components/aussie_broadband/translations/zh-Hant.json @@ -11,13 +11,6 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { - "reauth": { - "data": { - "password": "\u5bc6\u78bc" - }, - "description": "\u66f4\u65b0 {username} \u5bc6\u78bc", - "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" - }, "reauth_confirm": { "data": { "password": "\u5bc6\u78bc" diff --git a/homeassistant/components/azure_event_hub/translations/ko.json b/homeassistant/components/azure_event_hub/translations/ko.json new file mode 100644 index 00000000000..814b5cba06f --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/ko.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "cannot_connect": "configuration.yaml\uc758 \uc815\ubcf4\ub85c \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. yaml\uc5d0\uc11c \uc81c\uac70\ud558\uace0 \uad6c\uc131 \ud750\ub984\uc744 \uc0ac\uc6a9\ud558\uc138\uc694.", + "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\ub85c \uc778\ud574 configuration.yaml\uc758 \uc815\ubcf4\ub85c \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. yaml\uc5d0\uc11c \uc81c\uac70\ud558\uace0 \uad6c\uc131 \ud750\ub984\uc744 \uc0ac\uc6a9\ud558\uc138\uc694." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/cs.json b/homeassistant/components/baf/translations/cs.json new file mode 100644 index 00000000000..04f18366eaf --- /dev/null +++ b/homeassistant/components/baf/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/he.json b/homeassistant/components/baf/translations/he.json new file mode 100644 index 00000000000..61151ab6737 --- /dev/null +++ b/homeassistant/components/baf/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{name} - {model} ({ip_address})", + "step": { + "user": { + "data": { + "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/ca.json b/homeassistant/components/binary_sensor/translations/ca.json index 4c27c1e2966..10d705e797b 100644 --- a/homeassistant/components/binary_sensor/translations/ca.json +++ b/homeassistant/components/binary_sensor/translations/ca.json @@ -59,8 +59,6 @@ "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", @@ -138,10 +136,6 @@ "off": "Lliure", "on": "Detectat" }, - "co": { - "off": "Lliure", - "on": "Detectat" - }, "cold": { "off": "Normal", "on": "Fred" diff --git a/homeassistant/components/binary_sensor/translations/cs.json b/homeassistant/components/binary_sensor/translations/cs.json index ced02ffc326..b8793a2c087 100644 --- a/homeassistant/components/binary_sensor/translations/cs.json +++ b/homeassistant/components/binary_sensor/translations/cs.json @@ -53,8 +53,6 @@ "connected": "{entity_name} p\u0159ipojeno", "gas": "{entity_name} za\u010dalo detekovat plyn", "hot": "{entity_name} se zah\u0159\u00e1l", - "is_not_tampered": "{entity_name} p\u0159estalo detekovat neopr\u00e1vn\u011bnou manipulaci", - "is_tampered": "{entity_name} za\u010dalo detekovat neopr\u00e1vn\u011bnou manipulaci", "light": "{entity_name} za\u010dalo detekovat sv\u011btlo", "locked": "{entity_name} zam\u010deno", "moist": "{entity_name} zvlhnul", diff --git a/homeassistant/components/binary_sensor/translations/de.json b/homeassistant/components/binary_sensor/translations/de.json index 1d6257c48c6..85163bce0a3 100644 --- a/homeassistant/components/binary_sensor/translations/de.json +++ b/homeassistant/components/binary_sensor/translations/de.json @@ -59,8 +59,6 @@ "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", @@ -138,10 +136,6 @@ "off": "Normal", "on": "Erkannt" }, - "co": { - "off": "Normal", - "on": "Erkannt" - }, "cold": { "off": "Normal", "on": "Kalt" diff --git a/homeassistant/components/binary_sensor/translations/el.json b/homeassistant/components/binary_sensor/translations/el.json index ec78f1cd575..ed291a70f0d 100644 --- a/homeassistant/components/binary_sensor/translations/el.json +++ b/homeassistant/components/binary_sensor/translations/el.json @@ -59,8 +59,6 @@ "connected": "{entity_name} \u03c3\u03c5\u03bd\u03b4\u03ad\u03b8\u03b7\u03ba\u03b5", "gas": "{entity_name} \u03ac\u03c1\u03c7\u03b9\u03c3\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03b1\u03ad\u03c1\u03b9\u03bf", "hot": "{entity_name} \u03b6\u03b5\u03c3\u03c4\u03ac\u03b8\u03b7\u03ba\u03b5", - "is_not_tampered": "{entity_name} \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03b6\u03b5\u03b9 \u03c0\u03b1\u03c1\u03b1\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", - "is_tampered": "{entity_name} \u03ac\u03c1\u03c7\u03b9\u03c3\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03b6\u03b5\u03b9 \u03c0\u03b1\u03c1\u03b1\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", "light": "{entity_name} \u03ac\u03c1\u03c7\u03b9\u03c3\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03b5\u03b9 \u03c6\u03c9\u03c2", "locked": "{entity_name} \u03ba\u03bb\u03b5\u03b9\u03b4\u03ce\u03b8\u03b7\u03ba\u03b5", "moist": "{entity_name} \u03ad\u03b3\u03b9\u03bd\u03b5 \u03c5\u03b3\u03c1\u03cc", @@ -138,10 +136,6 @@ "off": "\u0394\u03b5\u03bd \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5", "on": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5" }, - "co": { - "off": "\u0394\u03b5\u03bd \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5", - "on": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5" - }, "cold": { "off": "\u03a6\u03c5\u03c3\u03b9\u03bf\u03bb\u03bf\u03b3\u03b9\u03ba\u03cc", "on": "\u039a\u03c1\u03cd\u03bf" diff --git a/homeassistant/components/binary_sensor/translations/en.json b/homeassistant/components/binary_sensor/translations/en.json index 1d4f30fef52..1dc6cf2caa1 100644 --- a/homeassistant/components/binary_sensor/translations/en.json +++ b/homeassistant/components/binary_sensor/translations/en.json @@ -59,8 +59,6 @@ "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", @@ -138,10 +136,6 @@ "off": "Clear", "on": "Detected" }, - "co": { - "off": "Clear", - "on": "Detected" - }, "cold": { "off": "Normal", "on": "Cold" diff --git a/homeassistant/components/binary_sensor/translations/es-419.json b/homeassistant/components/binary_sensor/translations/es-419.json index 2951e71b114..bc77a635165 100644 --- a/homeassistant/components/binary_sensor/translations/es-419.json +++ b/homeassistant/components/binary_sensor/translations/es-419.json @@ -121,10 +121,6 @@ "off": "No esta cargando", "on": "Cargando" }, - "co": { - "off": "Despejado", - "on": "Detectado" - }, "cold": { "off": "Normal", "on": "Fr\u00edo" diff --git a/homeassistant/components/binary_sensor/translations/es.json b/homeassistant/components/binary_sensor/translations/es.json index 5875804175e..005f2669e50 100644 --- a/homeassistant/components/binary_sensor/translations/es.json +++ b/homeassistant/components/binary_sensor/translations/es.json @@ -59,8 +59,6 @@ "connected": "{entity_name} conectado", "gas": "{entity_name} empez\u00f3 a detectar gas", "hot": "{entity_name} se est\u00e1 calentando", - "is_not_tampered": "{entity_name} dej\u00f3 de detectar alteraciones", - "is_tampered": "{entity_name} comenz\u00f3 a detectar alteraciones", "light": "{entity_name} empez\u00f3 a detectar la luz", "locked": "{entity_name} bloqueado", "moist": "{entity_name} se humedece", @@ -138,10 +136,6 @@ "off": "Libre", "on": "Detectado" }, - "co": { - "off": "No detectado", - "on": "Detectado" - }, "cold": { "off": "Normal", "on": "Fr\u00edo" diff --git a/homeassistant/components/binary_sensor/translations/et.json b/homeassistant/components/binary_sensor/translations/et.json index 92b4e52952d..6e16d5da000 100644 --- a/homeassistant/components/binary_sensor/translations/et.json +++ b/homeassistant/components/binary_sensor/translations/et.json @@ -59,8 +59,6 @@ "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", @@ -138,10 +136,6 @@ "off": "Korras", "on": "Tuvastatud" }, - "co": { - "off": "Puudub", - "on": "Tuvastatud" - }, "cold": { "off": "Normaalne", "on": "Jahe" diff --git a/homeassistant/components/binary_sensor/translations/fr.json b/homeassistant/components/binary_sensor/translations/fr.json index a9bd4ba30b1..b5280a897cc 100644 --- a/homeassistant/components/binary_sensor/translations/fr.json +++ b/homeassistant/components/binary_sensor/translations/fr.json @@ -59,8 +59,6 @@ "connected": "{entity_name} s'est connect\u00e9", "gas": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du gaz", "hot": "{entity_name} est devenu chaud", - "is_not_tampered": "{entity_name} a cess\u00e9 de d\u00e9tecter une manipulation", - "is_tampered": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter une manipulation", "light": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter de la lumi\u00e8re", "locked": "{entity_name} s'est verrouill\u00e9", "moist": "{entity_name} est devenu humide", @@ -138,10 +136,6 @@ "off": "Non d\u00e9tect\u00e9", "on": "D\u00e9tect\u00e9" }, - "co": { - "off": "Non d\u00e9tect\u00e9", - "on": "D\u00e9tect\u00e9" - }, "cold": { "off": "Normal", "on": "Froid" diff --git a/homeassistant/components/binary_sensor/translations/he.json b/homeassistant/components/binary_sensor/translations/he.json index 5f0e14ccac1..cd3b24ed9a3 100644 --- a/homeassistant/components/binary_sensor/translations/he.json +++ b/homeassistant/components/binary_sensor/translations/he.json @@ -59,8 +59,6 @@ "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", - "is_not_tampered": "{entity_name} \u05d4\u05e4\u05e1\u05d9\u05e7 \u05dc\u05d6\u05d4\u05d5\u05ea \u05d7\u05d1\u05dc\u05d4", - "is_tampered": "{entity_name} \u05d4\u05d7\u05dc \u05dc\u05d6\u05d4\u05d5\u05ea \u05d7\u05d1\u05dc\u05d4", "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", @@ -138,10 +136,6 @@ "off": "\u05e0\u05e7\u05d9", "on": "\u05d6\u05d5\u05d4\u05d4" }, - "co": { - "off": "\u05e0\u05e7\u05d9", - "on": "\u05d6\u05d5\u05d4\u05d4" - }, "cold": { "off": "\u05e0\u05d5\u05e8\u05de\u05dc\u05d9", "on": "\u05e7\u05e8" diff --git a/homeassistant/components/binary_sensor/translations/hu.json b/homeassistant/components/binary_sensor/translations/hu.json index 3690c1def1e..56df2994766 100644 --- a/homeassistant/components/binary_sensor/translations/hu.json +++ b/homeassistant/components/binary_sensor/translations/hu.json @@ -59,8 +59,6 @@ "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", @@ -138,10 +136,6 @@ "off": "Norm\u00e1l", "on": "\u00c9szlelve" }, - "co": { - "off": "Tiszta", - "on": "\u00c9rz\u00e9kelve" - }, "cold": { "off": "Norm\u00e1l", "on": "Hideg" diff --git a/homeassistant/components/binary_sensor/translations/id.json b/homeassistant/components/binary_sensor/translations/id.json index 5215f57814a..e01ee10cc3c 100644 --- a/homeassistant/components/binary_sensor/translations/id.json +++ b/homeassistant/components/binary_sensor/translations/id.json @@ -59,8 +59,6 @@ "connected": "{entity_name} terhubung", "gas": "{entity_name} mulai mendeteksi gas", "hot": "{entity_name} menjadi panas", - "is_not_tampered": "{entity_name} berhenti mendeteksi gangguan", - "is_tampered": "{entity_name} mulai mendeteksi gangguan", "light": "{entity_name} mulai mendeteksi cahaya", "locked": "{entity_name} terkunci", "moist": "{entity_name} menjadi lembab", @@ -138,10 +136,6 @@ "off": "Tidak ada", "on": "Terdeteksi" }, - "co": { - "off": "Tidak ada", - "on": "Terdeteksi" - }, "cold": { "off": "Normal", "on": "Dingin" diff --git a/homeassistant/components/binary_sensor/translations/it.json b/homeassistant/components/binary_sensor/translations/it.json index 6054fd9d9fe..933e72a285a 100644 --- a/homeassistant/components/binary_sensor/translations/it.json +++ b/homeassistant/components/binary_sensor/translations/it.json @@ -59,8 +59,6 @@ "connected": "{entity_name} \u00e8 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", @@ -138,10 +136,6 @@ "off": "Assente", "on": "Rilevato" }, - "co": { - "off": "Non rilevato", - "on": "Rilevato" - }, "cold": { "off": "Normale", "on": "Freddo" diff --git a/homeassistant/components/binary_sensor/translations/ja.json b/homeassistant/components/binary_sensor/translations/ja.json index 541c5073961..a49c683b00b 100644 --- a/homeassistant/components/binary_sensor/translations/ja.json +++ b/homeassistant/components/binary_sensor/translations/ja.json @@ -59,8 +59,6 @@ "connected": "{entity_name} \u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u3059", "gas": "{entity_name} \u304c\u30ac\u30b9\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", "hot": "{entity_name} \u6e29\u307e\u3063\u3066\u3044\u307e\u3059", - "is_not_tampered": "{entity_name} \u304c\u6539\u7ac4(tampering)\u306e\u691c\u51fa\u3092\u505c\u6b62\u3057\u307e\u3057\u305f", - "is_tampered": "{entity_name} \u304c\u6539\u7ac4(tampering)\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", "light": "{entity_name} \u306f\u3001\u5149\u306e\u691c\u51fa\u3092\u958b\u59cb\u3057\u307e\u3057\u305f", "locked": "{entity_name} \u306f\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059", "moist": "{entity_name} \u304c\u6e7f\u3063\u305f", @@ -138,10 +136,6 @@ "off": "\u30af\u30ea\u30a2", "on": "\u691c\u51fa" }, - "co": { - "off": "\u30af\u30ea\u30a2", - "on": "\u691c\u51fa\u3055\u308c\u307e\u3057\u305f" - }, "cold": { "off": "\u901a\u5e38", "on": "\u4f4e\u6e29" diff --git a/homeassistant/components/binary_sensor/translations/nl.json b/homeassistant/components/binary_sensor/translations/nl.json index 7afbcaf616f..eb830fdd450 100644 --- a/homeassistant/components/binary_sensor/translations/nl.json +++ b/homeassistant/components/binary_sensor/translations/nl.json @@ -59,8 +59,6 @@ "connected": "{entity_name} verbonden", "gas": "{entity_name} begon gas te detecteren", "hot": "{entity_name} werd heet", - "is_not_tampered": "{entity_name} gestopt met het detecteren van sabotage", - "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", @@ -138,10 +136,6 @@ "off": "Niet gedetecteerd", "on": "Gedetecteerd" }, - "co": { - "off": "Niet gedetecteerd", - "on": "Gedetecteerd" - }, "cold": { "off": "Normaal", "on": "Koud" diff --git a/homeassistant/components/binary_sensor/translations/no.json b/homeassistant/components/binary_sensor/translations/no.json index 62cf2d7cc1b..e29fab0a709 100644 --- a/homeassistant/components/binary_sensor/translations/no.json +++ b/homeassistant/components/binary_sensor/translations/no.json @@ -59,8 +59,6 @@ "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", @@ -138,10 +136,6 @@ "off": "Klart", "on": "Oppdaget" }, - "co": { - "off": "Klart", - "on": "Oppdaget" - }, "cold": { "off": "Normal", "on": "Kald" diff --git a/homeassistant/components/binary_sensor/translations/pl.json b/homeassistant/components/binary_sensor/translations/pl.json index d318968da56..d27d6443b55 100644 --- a/homeassistant/components/binary_sensor/translations/pl.json +++ b/homeassistant/components/binary_sensor/translations/pl.json @@ -59,8 +59,6 @@ "connected": "nast\u0105pi pod\u0142\u0105czenie {entity_name}", "gas": "sensor {entity_name} wykryje gaz", "hot": "sensor {entity_name} wykryje gor\u0105co", - "is_not_tampered": "sensor {entity_name} przestanie wykrywa\u0107 naruszenie", - "is_tampered": "sensor {entity_name} wykryje naruszenie", "light": "sensor {entity_name} wykryje \u015bwiat\u0142o", "locked": "nast\u0105pi zamkni\u0119cie {entity_name}", "moist": "nast\u0105pi wykrycie wilgoci {entity_name}", @@ -138,10 +136,6 @@ "off": "brak", "on": "wykryto" }, - "co": { - "off": "brak", - "on": "wykryto" - }, "cold": { "off": "normalnie", "on": "zimno" diff --git a/homeassistant/components/binary_sensor/translations/pt-BR.json b/homeassistant/components/binary_sensor/translations/pt-BR.json index 5b1ba2a1abf..5bd166f9367 100644 --- a/homeassistant/components/binary_sensor/translations/pt-BR.json +++ b/homeassistant/components/binary_sensor/translations/pt-BR.json @@ -59,8 +59,6 @@ "connected": "{entity_name} conectado", "gas": "{entity_name} come\u00e7ou a detectar g\u00e1s", "hot": "{entity_name} tornou-se quente", - "is_not_tampered": "{entity_name} parar de detectar adultera\u00e7\u00e3o", - "is_tampered": "{entity_name} come\u00e7ar a detectar adultera\u00e7\u00e3o", "light": "{entity_name} come\u00e7ou a detectar luz", "locked": "{entity_name} bloqueado", "moist": "{entity_name} ficar \u00famido", @@ -138,10 +136,6 @@ "off": "Normal", "on": "Detectado" }, - "co": { - "off": "Limpo", - "on": "Detectado" - }, "cold": { "off": "Normal", "on": "Frio" diff --git a/homeassistant/components/binary_sensor/translations/ru.json b/homeassistant/components/binary_sensor/translations/ru.json index cc9573e2b14..9522720571b 100644 --- a/homeassistant/components/binary_sensor/translations/ru.json +++ b/homeassistant/components/binary_sensor/translations/ru.json @@ -59,8 +59,6 @@ "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", @@ -138,10 +136,6 @@ "off": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d", "on": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d" }, - "co": { - "off": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d", - "on": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d" - }, "cold": { "off": "\u041d\u043e\u0440\u043c\u0430", "on": "\u041e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u0435" diff --git a/homeassistant/components/binary_sensor/translations/sl.json b/homeassistant/components/binary_sensor/translations/sl.json index cc34982ad0a..6b47f4e9e7e 100644 --- a/homeassistant/components/binary_sensor/translations/sl.json +++ b/homeassistant/components/binary_sensor/translations/sl.json @@ -52,8 +52,6 @@ "connected": "{entity_name} povezan", "gas": "{entity_name} za\u010del zaznavati plin", "hot": "{entity_name} je postal vro\u010d", - "is_not_tampered": "{entity_name} je prenehal zaznavati nedovoljena dejanja", - "is_tampered": "{entity_name} je za\u010del zaznavati nedovoljeno poseganje", "light": "{entity_name} za\u010del zaznavati svetlobo", "locked": "{entity_name} zaklenjen", "moist": "{entity_name} postal vla\u017een", diff --git a/homeassistant/components/binary_sensor/translations/tr.json b/homeassistant/components/binary_sensor/translations/tr.json index 2b7eb33e6f0..c54454b2230 100644 --- a/homeassistant/components/binary_sensor/translations/tr.json +++ b/homeassistant/components/binary_sensor/translations/tr.json @@ -59,8 +59,6 @@ "connected": "{entity_name} ba\u011fland\u0131", "gas": "{entity_name} gaz alg\u0131lamaya ba\u015flad\u0131", "hot": "{entity_name} \u0131s\u0131nd\u0131", - "is_not_tampered": "{entity_name} kurcalamay\u0131 alg\u0131lamay\u0131 durdurdu", - "is_tampered": "{entity_name} , kurcalamay\u0131 alg\u0131lamaya ba\u015flad\u0131", "light": "{entity_name} \u0131\u015f\u0131\u011f\u0131 alg\u0131lamaya ba\u015flad\u0131", "locked": "{entity_name} kilitlendi", "moist": "{entity_name} nemli oldu", @@ -138,10 +136,6 @@ "off": "Temiz", "on": "Alg\u0131land\u0131" }, - "co": { - "off": "Temiz", - "on": "Alg\u0131land\u0131" - }, "cold": { "off": "Normal", "on": "So\u011fuk" diff --git a/homeassistant/components/binary_sensor/translations/zh-Hans.json b/homeassistant/components/binary_sensor/translations/zh-Hans.json index 202a51740de..70ff33a6e53 100644 --- a/homeassistant/components/binary_sensor/translations/zh-Hans.json +++ b/homeassistant/components/binary_sensor/translations/zh-Hans.json @@ -59,8 +59,6 @@ "connected": "{entity_name} \u5df2\u8fde\u63a5", "gas": "{entity_name} \u5f00\u59cb\u68c0\u6d4b\u5230\u71c3\u6c14\u6cc4\u6f0f", "hot": "{entity_name} \u53d8\u70ed", - "is_not_tampered": "{entity_name} \u4e0d\u518d\u68c0\u6d4b\u5230\u81ea\u8eab\u88ab\u62c6\u89e3", - "is_tampered": "{entity_name} \u5f00\u59cb\u68c0\u6d4b\u5230\u81ea\u8eab\u88ab\u62c6\u89e3", "light": "{entity_name} \u5f00\u59cb\u68c0\u6d4b\u5230\u5149\u7ebf", "locked": "{entity_name} \u88ab\u9501\u5b9a", "moist": "{entity_name} \u53d8\u6e7f", @@ -134,10 +132,6 @@ "off": "\u672a\u5728\u5145\u7535", "on": "\u6b63\u5728\u5145\u7535" }, - "co": { - "off": "\u672a\u89e6\u53d1", - "on": "\u89e6\u53d1" - }, "cold": { "off": "\u6b63\u5e38", "on": "\u8fc7\u51b7" diff --git a/homeassistant/components/binary_sensor/translations/zh-Hant.json b/homeassistant/components/binary_sensor/translations/zh-Hant.json index 417ca652cb5..32c1aab1cd1 100644 --- a/homeassistant/components/binary_sensor/translations/zh-Hant.json +++ b/homeassistant/components/binary_sensor/translations/zh-Hant.json @@ -59,8 +59,6 @@ "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", @@ -138,10 +136,6 @@ "off": "\u672a\u89f8\u767c", "on": "\u5df2\u89f8\u767c" }, - "co": { - "off": "\u672a\u5075\u6e2c", - "on": "\u5075\u6e2c" - }, "cold": { "off": "\u6b63\u5e38", "on": "\u51b7" diff --git a/homeassistant/components/bmw_connected_drive/translations/ca.json b/homeassistant/components/bmw_connected_drive/translations/ca.json index eb12ac6fc3b..b6129aa4de8 100644 --- a/homeassistant/components/bmw_connected_drive/translations/ca.json +++ b/homeassistant/components/bmw_connected_drive/translations/ca.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "Nom\u00e9s de lectura (nom\u00e9s sensors i notificacions, sense execuci\u00f3 de serveis, sense bloqueig)", - "use_location": "Utilitza la ubicaci\u00f3 de Home Assistant per a les crides de localitzaci\u00f3 del cotxe (obligatori per a vehicles que no siguin i3/i8 produ\u00efts abans del 7/2014)" + "read_only": "Nom\u00e9s de lectura (nom\u00e9s sensors i notificacions, sense execuci\u00f3 de serveis, sense bloqueig)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/de.json b/homeassistant/components/bmw_connected_drive/translations/de.json index 85e27b5b3e4..0ee49c105f4 100644 --- a/homeassistant/components/bmw_connected_drive/translations/de.json +++ b/homeassistant/components/bmw_connected_drive/translations/de.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "Schreibgesch\u00fctzt (nur Sensoren und Notify, keine Ausf\u00fchrung von Diensten, kein Abschlie\u00dfen)", - "use_location": "Standort des Home Assistant f\u00fcr die Abfrage des Fahrzeugstandorts verwenden (erforderlich f\u00fcr nicht i3/i8 Fahrzeuge, die vor 7/2014 produziert wurden)" + "read_only": "Schreibgesch\u00fctzt (nur Sensoren und Notify, keine Ausf\u00fchrung von Diensten, kein Abschlie\u00dfen)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/el.json b/homeassistant/components/bmw_connected_drive/translations/el.json index bb20b0a61a2..8ba712e7373 100644 --- a/homeassistant/components/bmw_connected_drive/translations/el.json +++ b/homeassistant/components/bmw_connected_drive/translations/el.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "\u039c\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 (\u03bc\u03cc\u03bd\u03bf \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03ba\u03b1\u03b9 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2, \u03cc\u03c7\u03b9 \u03b5\u03ba\u03c4\u03ad\u03bb\u03b5\u03c3\u03b7 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd, \u03cc\u03c7\u03b9 \u03ba\u03bb\u03b5\u03af\u03b4\u03c9\u03bc\u03b1)", - "use_location": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1\u03c2 \u03c4\u03bf\u03c5 Home Assistant \u03b3\u03b9\u03b1 \u03c4\u03b9\u03c2 \u03b4\u03b7\u03bc\u03bf\u03c3\u03ba\u03bf\u03c0\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b8\u03ad\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b1\u03c5\u03c4\u03bf\u03ba\u03b9\u03bd\u03ae\u03c4\u03bf\u03c5 (\u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03bf\u03c7\u03ae\u03bc\u03b1\u03c4\u03b1 \u03c0\u03bf\u03c5 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 i3/i8 \u03ba\u03b1\u03b9 \u03ad\u03c7\u03bf\u03c5\u03bd \u03c0\u03b1\u03c1\u03b1\u03c7\u03b8\u03b5\u03af \u03c0\u03c1\u03b9\u03bd \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 7/2014)" + "read_only": "\u039c\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 (\u03bc\u03cc\u03bd\u03bf \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03ba\u03b1\u03b9 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2, \u03cc\u03c7\u03b9 \u03b5\u03ba\u03c4\u03ad\u03bb\u03b5\u03c3\u03b7 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd, \u03cc\u03c7\u03b9 \u03ba\u03bb\u03b5\u03af\u03b4\u03c9\u03bc\u03b1)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/en.json b/homeassistant/components/bmw_connected_drive/translations/en.json index dedd84d070b..db98df61d28 100644 --- a/homeassistant/components/bmw_connected_drive/translations/en.json +++ b/homeassistant/components/bmw_connected_drive/translations/en.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "Read-only (only sensors and notify, no execution of services, no lock)", - "use_location": "Use Home Assistant location for car location polls (required for non i3/i8 vehicles produced before 7/2014)" + "read_only": "Read-only (only sensors and notify, no execution of services, no lock)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/es-419.json b/homeassistant/components/bmw_connected_drive/translations/es-419.json index bb19124b55c..0bce46abd97 100644 --- a/homeassistant/components/bmw_connected_drive/translations/es-419.json +++ b/homeassistant/components/bmw_connected_drive/translations/es-419.json @@ -12,8 +12,7 @@ "step": { "account_options": { "data": { - "read_only": "Solo lectura (solo sensores y notificaci\u00f3n, sin ejecuci\u00f3n de servicios, sin bloqueo)", - "use_location": "Use la ubicaci\u00f3n de Home Assistant para encuestas de ubicaci\u00f3n de autom\u00f3viles (obligatorio para veh\u00edculos que no sean i3/i8 fabricados antes de julio de 2014)" + "read_only": "Solo lectura (solo sensores y notificaci\u00f3n, sin ejecuci\u00f3n de servicios, sin bloqueo)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/es.json b/homeassistant/components/bmw_connected_drive/translations/es.json index 65ed9643f89..2a4fd84f708 100644 --- a/homeassistant/components/bmw_connected_drive/translations/es.json +++ b/homeassistant/components/bmw_connected_drive/translations/es.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "S\u00f3lo lectura (s\u00f3lo sensores y notificaci\u00f3n, sin ejecuci\u00f3n de servicios, sin bloqueo)", - "use_location": "Usar la ubicaci\u00f3n de Home Assistant para las encuestas de localizaci\u00f3n de autom\u00f3viles (necesario para los veh\u00edculos no i3/i8 producidos antes del 7/2014)" + "read_only": "S\u00f3lo lectura (s\u00f3lo sensores y notificaci\u00f3n, sin ejecuci\u00f3n de servicios, sin bloqueo)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/et.json b/homeassistant/components/bmw_connected_drive/translations/et.json index f28209a1e7a..adee392525e 100644 --- a/homeassistant/components/bmw_connected_drive/translations/et.json +++ b/homeassistant/components/bmw_connected_drive/translations/et.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "Kirjutuskaitstud (ainult andurid ja teavitused, ei k\u00e4ivita teenuseid, kood puudub)", - "use_location": "Kasuta HA asukohta auto asukoha k\u00fcsitluste jaoks (n\u00f5utav enne 7/2014 toodetud muude kui i3 / i8 s\u00f5idukite jaoks)" + "read_only": "Kirjutuskaitstud (ainult andurid ja teavitused, ei k\u00e4ivita teenuseid, kood puudub)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/fr.json b/homeassistant/components/bmw_connected_drive/translations/fr.json index 5181620d465..afed216c64c 100644 --- a/homeassistant/components/bmw_connected_drive/translations/fr.json +++ b/homeassistant/components/bmw_connected_drive/translations/fr.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "Lecture seule (uniquement capteurs et notification, pas d'ex\u00e9cution de services, pas de verrouillage)", - "use_location": "Utilisez la localisation de Home Assistant pour les sondages de localisation de voiture (obligatoire pour les v\u00e9hicules non i3 / i8 produits avant 7/2014)" + "read_only": "Lecture seule (uniquement capteurs et notification, pas d'ex\u00e9cution de services, pas de verrouillage)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/hu.json b/homeassistant/components/bmw_connected_drive/translations/hu.json index fa3fcf2df57..637083154bd 100644 --- a/homeassistant/components/bmw_connected_drive/translations/hu.json +++ b/homeassistant/components/bmw_connected_drive/translations/hu.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "Csak olvashat\u00f3 (csak \u00e9rz\u00e9kel\u0151k \u00e9s \u00e9rtes\u00edt\u00e9sek, szolg\u00e1ltat\u00e1sok v\u00e9grehajt\u00e1sa, z\u00e1rol\u00e1s n\u00e9lk\u00fcl)", - "use_location": "Haszn\u00e1lja a Home Assistant hely\u00e9t az aut\u00f3 helymeghat\u00e1roz\u00e1si lek\u00e9rdez\u00e9seihez (a 2014.07.07. el\u0151tt gy\u00e1rtott nem i3/i8 j\u00e1rm\u0171vekhez sz\u00fcks\u00e9ges)" + "read_only": "Csak olvashat\u00f3 (csak \u00e9rz\u00e9kel\u0151k \u00e9s \u00e9rtes\u00edt\u00e9sek, szolg\u00e1ltat\u00e1sok v\u00e9grehajt\u00e1sa, z\u00e1rol\u00e1s n\u00e9lk\u00fcl)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/id.json b/homeassistant/components/bmw_connected_drive/translations/id.json index 3701a49ccfb..23e57c8e301 100644 --- a/homeassistant/components/bmw_connected_drive/translations/id.json +++ b/homeassistant/components/bmw_connected_drive/translations/id.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "Hanya baca (hanya sensor dan notifikasi, tidak ada eksekusi layanan, tidak ada fitur penguncian)", - "use_location": "Gunakan lokasi Home Assistant untuk polling lokasi mobil (diperlukan untuk kendaraan non i3/i8 yang diproduksi sebelum Juli 2014)" + "read_only": "Hanya baca (hanya sensor dan notifikasi, tidak ada eksekusi layanan, tidak ada fitur penguncian)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/it.json b/homeassistant/components/bmw_connected_drive/translations/it.json index eeca1909ea7..3c0bb4ee830 100644 --- a/homeassistant/components/bmw_connected_drive/translations/it.json +++ b/homeassistant/components/bmw_connected_drive/translations/it.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "Sola lettura (solo sensori e notifica, nessuna esecuzione di servizi, nessun blocco)", - "use_location": "Usa la posizione di Home Assistant per le richieste di posizione dell'auto (richiesto per veicoli non i3/i8 prodotti prima del 7/2014)" + "read_only": "Sola lettura (solo sensori e notifica, nessuna esecuzione di servizi, nessun blocco)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/ja.json b/homeassistant/components/bmw_connected_drive/translations/ja.json index a3fa86348fe..643e3b70cc3 100644 --- a/homeassistant/components/bmw_connected_drive/translations/ja.json +++ b/homeassistant/components/bmw_connected_drive/translations/ja.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "\u30ea\u30fc\u30c9\u30aa\u30f3\u30ea\u30fc(\u30bb\u30f3\u30b5\u30fc\u3068\u901a\u77e5\u306e\u307f\u3001\u30b5\u30fc\u30d3\u30b9\u306e\u5b9f\u884c\u306f\u4e0d\u53ef\u3001\u30ed\u30c3\u30af\u4e0d\u53ef)", - "use_location": "Home Assistant\u306e\u5834\u6240\u3092\u3001\u8eca\u306e\u4f4d\u7f6e\u3068\u3057\u3066\u30dd\u30fc\u30ea\u30f3\u30b0\u306b\u4f7f\u7528\u3059\u308b(2014\u5e747\u67087\u65e5\u4ee5\u524d\u306b\u751f\u7523\u3055\u308c\u305f\u3001i3/i8\u4ee5\u5916\u306e\u8eca\u4e21\u3067\u306f\u5fc5\u9808)" + "read_only": "\u30ea\u30fc\u30c9\u30aa\u30f3\u30ea\u30fc(\u30bb\u30f3\u30b5\u30fc\u3068\u901a\u77e5\u306e\u307f\u3001\u30b5\u30fc\u30d3\u30b9\u306e\u5b9f\u884c\u306f\u4e0d\u53ef\u3001\u30ed\u30c3\u30af\u4e0d\u53ef)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/ko.json b/homeassistant/components/bmw_connected_drive/translations/ko.json index 4c9872573be..588c5111afc 100644 --- a/homeassistant/components/bmw_connected_drive/translations/ko.json +++ b/homeassistant/components/bmw_connected_drive/translations/ko.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "\uc77d\uae30 \uc804\uc6a9(\uc13c\uc11c \ubc0f \uc54c\ub9bc\ub9cc \uac00\ub2a5, \uc11c\ube44\uc2a4 \uc2e4\ud589 \ubc0f \uc7a0\uae08 \uc5c6\uc74c)", - "use_location": "\ucc28\ub7c9 \uc704\uce58 \ud3f4\ub9c1\uc5d0 Home Assistant \uc704\uce58\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4(2014\ub144 7\uc6d4 \uc774\uc804\uc5d0 \uc0dd\uc0b0\ub41c i3/i8\uc774 \uc544\ub2cc \ucc28\ub7c9\uc758 \uacbd\uc6b0 \ud544\uc694)" + "read_only": "\uc77d\uae30 \uc804\uc6a9(\uc13c\uc11c \ubc0f \uc54c\ub9bc\ub9cc \uac00\ub2a5, \uc11c\ube44\uc2a4 \uc2e4\ud589 \ubc0f \uc7a0\uae08 \uc5c6\uc74c)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/nl.json b/homeassistant/components/bmw_connected_drive/translations/nl.json index 8fa6c839112..a566afd7ad8 100644 --- a/homeassistant/components/bmw_connected_drive/translations/nl.json +++ b/homeassistant/components/bmw_connected_drive/translations/nl.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "Alleen-lezen (alleen sensoren en notificatie, geen uitvoering van diensten, geen vergrendeling)", - "use_location": "Gebruik Home Assistant locatie voor auto locatie peilingen (vereist voor niet i3/i8 voertuigen geproduceerd voor 7/2014)" + "read_only": "Alleen-lezen (alleen sensoren en notificatie, geen uitvoering van diensten, geen vergrendeling)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/no.json b/homeassistant/components/bmw_connected_drive/translations/no.json index f1715c550db..ed6acfeac38 100644 --- a/homeassistant/components/bmw_connected_drive/translations/no.json +++ b/homeassistant/components/bmw_connected_drive/translations/no.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "Skrivebeskyttet (bare sensorer og varsler, ingen utf\u00f8relse av tjenester, ingen l\u00e5s)", - "use_location": "Bruk Home Assistant plassering for avstemningssteder for biler (p\u00e5krevd for ikke i3 / i8-kj\u00f8ret\u00f8y produsert f\u00f8r 7/2014)" + "read_only": "Skrivebeskyttet (bare sensorer og varsler, ingen utf\u00f8relse av tjenester, ingen l\u00e5s)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/pl.json b/homeassistant/components/bmw_connected_drive/translations/pl.json index 146916edd5b..00602b2c14e 100644 --- a/homeassistant/components/bmw_connected_drive/translations/pl.json +++ b/homeassistant/components/bmw_connected_drive/translations/pl.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "Tylko odczyt (tylko sensory i powiadomienia, brak wykonywania us\u0142ug, brak blokady)", - "use_location": "U\u017cyj lokalizacji Home Assistant do sondowania lokalizacji samochodu (wymagane w przypadku pojazd\u00f3w innych ni\u017c i3/i8 wyprodukowanych przed 7/2014)" + "read_only": "Tylko odczyt (tylko sensory i powiadomienia, brak wykonywania us\u0142ug, brak blokady)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/pt-BR.json b/homeassistant/components/bmw_connected_drive/translations/pt-BR.json index b4b82c1fe18..a30ede043bd 100644 --- a/homeassistant/components/bmw_connected_drive/translations/pt-BR.json +++ b/homeassistant/components/bmw_connected_drive/translations/pt-BR.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "Somente leitura (somente sensores e notificar, sem execu\u00e7\u00e3o de servi\u00e7os, sem bloqueio)", - "use_location": "Use a localiza\u00e7\u00e3o do Home Assistant para pesquisas de localiza\u00e7\u00e3o de carros (necess\u00e1rio para ve\u00edculos n\u00e3o i3/i8 produzidos antes de 7/2014)" + "read_only": "Somente leitura (somente sensores e notificar, sem execu\u00e7\u00e3o de servi\u00e7os, sem bloqueio)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/ru.json b/homeassistant/components/bmw_connected_drive/translations/ru.json index 8ab4e4e1207..7a398c8ca55 100644 --- a/homeassistant/components/bmw_connected_drive/translations/ru.json +++ b/homeassistant/components/bmw_connected_drive/translations/ru.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "\u0422\u043e\u043b\u044c\u043a\u043e \u0447\u0442\u0435\u043d\u0438\u0435 (\u0442\u043e\u043b\u044c\u043a\u043e \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f, \u0431\u0435\u0437 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0441\u043b\u0443\u0436\u0431, \u0431\u0435\u0437 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438)", - "use_location": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Home Assistant \u0434\u043b\u044f \u043e\u043f\u0440\u043e\u0441\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0438\u043b\u0435\u0439 (\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0438\u043b\u0435\u0439 \u043d\u0435 i3/i8, \u0432\u044b\u043f\u0443\u0449\u0435\u043d\u043d\u044b\u0445 \u0434\u043e 7/2014)" + "read_only": "\u0422\u043e\u043b\u044c\u043a\u043e \u0447\u0442\u0435\u043d\u0438\u0435 (\u0442\u043e\u043b\u044c\u043a\u043e \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f, \u0431\u0435\u0437 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0441\u043b\u0443\u0436\u0431, \u0431\u0435\u0437 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/sv.json b/homeassistant/components/bmw_connected_drive/translations/sv.json index 93cd828041e..eb2322abb1f 100644 --- a/homeassistant/components/bmw_connected_drive/translations/sv.json +++ b/homeassistant/components/bmw_connected_drive/translations/sv.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "Endast l\u00e4sbar (endast sensorer och meddelanden, ingen k\u00f6rning av tj\u00e4nster, ingen l\u00e5sning)", - "use_location": "Anv\u00e4nd plats f\u00f6r Home Assistant som plats f\u00f6r bilen (kr\u00e4vs f\u00f6r icke i3/i8-fordon tillverkade f\u00f6re 7/2014)" + "read_only": "Endast l\u00e4sbar (endast sensorer och meddelanden, ingen k\u00f6rning av tj\u00e4nster, ingen l\u00e5sning)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/tr.json b/homeassistant/components/bmw_connected_drive/translations/tr.json index d38f950392b..52a0b02b044 100644 --- a/homeassistant/components/bmw_connected_drive/translations/tr.json +++ b/homeassistant/components/bmw_connected_drive/translations/tr.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "Salt okunur (yaln\u0131zca sens\u00f6rler ve bildirim, hizmetlerin y\u00fcr\u00fct\u00fclmesi yok, kilit yok)", - "use_location": "Araba konumu anketleri i\u00e7in Home Assistant konumunu kullan\u0131n (7/2014 tarihinden \u00f6nce \u00fcretilmi\u015f i3/i8 olmayan ara\u00e7lar i\u00e7in gereklidir)" + "read_only": "Salt okunur (yaln\u0131zca sens\u00f6rler ve bildirim, hizmetlerin y\u00fcr\u00fct\u00fclmesi yok, kilit yok)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/uk.json b/homeassistant/components/bmw_connected_drive/translations/uk.json index 68cdee2a66f..4286e0e6063 100644 --- a/homeassistant/components/bmw_connected_drive/translations/uk.json +++ b/homeassistant/components/bmw_connected_drive/translations/uk.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "\u041b\u0438\u0448\u0435 \u0434\u043b\u044f \u0447\u0438\u0442\u0430\u043d\u043d\u044f (\u043b\u0438\u0448\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0442\u0430 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u043d\u044f, \u0431\u0435\u0437 \u0437\u0430\u043f\u0443\u0441\u043a\u0443 \u0441\u0435\u0440\u0432\u0456\u0441\u0456\u0432, \u0431\u0435\u0437 \u0431\u043b\u043e\u043a\u0443\u0432\u0430\u043d\u043d\u044f)", - "use_location": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f Home Assistant \u0434\u043b\u044f \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u044c \u043c\u0456\u0441\u0446\u044f \u0440\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0456\u043b\u0456\u0432 (\u043e\u0431\u043e\u0432\u2019\u044f\u0437\u043a\u043e\u0432\u043e \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0456\u043b\u0456\u0432, \u0449\u043e \u043d\u0435 \u043d\u0430\u043b\u0435\u0436\u0430\u0442\u044c \u0434\u043e i3/i8, \u0432\u0438\u0433\u043e\u0442\u043e\u0432\u043b\u0435\u043d\u0438\u0445 \u0434\u043e 7/2014)" + "read_only": "\u041b\u0438\u0448\u0435 \u0434\u043b\u044f \u0447\u0438\u0442\u0430\u043d\u043d\u044f (\u043b\u0438\u0448\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0442\u0430 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u043d\u044f, \u0431\u0435\u0437 \u0437\u0430\u043f\u0443\u0441\u043a\u0443 \u0441\u0435\u0440\u0432\u0456\u0441\u0456\u0432, \u0431\u0435\u0437 \u0431\u043b\u043e\u043a\u0443\u0432\u0430\u043d\u043d\u044f)" } } } diff --git a/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json b/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json index 4f62df586f5..38bf3bbb7cb 100644 --- a/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json +++ b/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json @@ -21,8 +21,7 @@ "step": { "account_options": { "data": { - "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" + "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" } } } diff --git a/homeassistant/components/bosch_shc/translations/bg.json b/homeassistant/components/bosch_shc/translations/bg.json index 759dd6b21fb..a0b0548b51b 100644 --- a/homeassistant/components/bosch_shc/translations/bg.json +++ b/homeassistant/components/bosch_shc/translations/bg.json @@ -20,6 +20,5 @@ } } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/ca.json b/homeassistant/components/bosch_shc/translations/ca.json index 6db49cf7103..f6b06e1d884 100644 --- a/homeassistant/components/bosch_shc/translations/ca.json +++ b/homeassistant/components/bosch_shc/translations/ca.json @@ -33,6 +33,5 @@ "title": "Par\u00e0metres d'autenticaci\u00f3 SHC" } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/de.json b/homeassistant/components/bosch_shc/translations/de.json index 46b6469afe6..b99c00d6a6f 100644 --- a/homeassistant/components/bosch_shc/translations/de.json +++ b/homeassistant/components/bosch_shc/translations/de.json @@ -33,6 +33,5 @@ "title": "SHC Authentifizierungsparameter" } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/el.json b/homeassistant/components/bosch_shc/translations/el.json index f9b5f4ad3d0..3b92111ce1a 100644 --- a/homeassistant/components/bosch_shc/translations/el.json +++ b/homeassistant/components/bosch_shc/translations/el.json @@ -33,6 +33,5 @@ "title": "\u03a0\u03b1\u03c1\u03ac\u03bc\u03b5\u03c4\u03c1\u03bf\u03b9 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 SHC" } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/en.json b/homeassistant/components/bosch_shc/translations/en.json index 65f675e2f4d..ab5cde9ef27 100644 --- a/homeassistant/components/bosch_shc/translations/en.json +++ b/homeassistant/components/bosch_shc/translations/en.json @@ -33,6 +33,5 @@ "title": "SHC authentication parameters" } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/es-419.json b/homeassistant/components/bosch_shc/translations/es-419.json index fdb8903a318..e868ffb1add 100644 --- a/homeassistant/components/bosch_shc/translations/es-419.json +++ b/homeassistant/components/bosch_shc/translations/es-419.json @@ -17,6 +17,5 @@ "title": "Par\u00e1metros de autenticaci\u00f3n SHC" } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/es.json b/homeassistant/components/bosch_shc/translations/es.json index 6d002cfadc2..0e3c536995d 100644 --- a/homeassistant/components/bosch_shc/translations/es.json +++ b/homeassistant/components/bosch_shc/translations/es.json @@ -33,6 +33,5 @@ "title": "Par\u00e1metros de autenticaci\u00f3n SHC" } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/et.json b/homeassistant/components/bosch_shc/translations/et.json index cb095bb9de7..7311d1f34e7 100644 --- a/homeassistant/components/bosch_shc/translations/et.json +++ b/homeassistant/components/bosch_shc/translations/et.json @@ -33,6 +33,5 @@ "title": "SHC autentimisparameetrid" } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/fr.json b/homeassistant/components/bosch_shc/translations/fr.json index c34f5549663..bc9f62e1d11 100644 --- a/homeassistant/components/bosch_shc/translations/fr.json +++ b/homeassistant/components/bosch_shc/translations/fr.json @@ -33,6 +33,5 @@ "title": "Param\u00e8tres d'authentification SHC" } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/hu.json b/homeassistant/components/bosch_shc/translations/hu.json index b3e0ed77815..df5a484cabe 100644 --- a/homeassistant/components/bosch_shc/translations/hu.json +++ b/homeassistant/components/bosch_shc/translations/hu.json @@ -33,6 +33,5 @@ "title": "SHC hiteles\u00edt\u00e9si param\u00e9terek" } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/id.json b/homeassistant/components/bosch_shc/translations/id.json index 723c6bdabbe..191c0cd96e8 100644 --- a/homeassistant/components/bosch_shc/translations/id.json +++ b/homeassistant/components/bosch_shc/translations/id.json @@ -33,6 +33,5 @@ "title": "Parameter autentikasi SHC" } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/it.json b/homeassistant/components/bosch_shc/translations/it.json index 2c64beee59c..05336a6abea 100644 --- a/homeassistant/components/bosch_shc/translations/it.json +++ b/homeassistant/components/bosch_shc/translations/it.json @@ -33,6 +33,5 @@ "title": "Parametri di autenticazione SHC" } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/ja.json b/homeassistant/components/bosch_shc/translations/ja.json index 0a624bd2eb0..da4b471c621 100644 --- a/homeassistant/components/bosch_shc/translations/ja.json +++ b/homeassistant/components/bosch_shc/translations/ja.json @@ -33,6 +33,5 @@ "title": "SHC\u8a8d\u8a3c\u30d1\u30e9\u30e1\u30fc\u30bf\u30fc" } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/nl.json b/homeassistant/components/bosch_shc/translations/nl.json index 8a6cbd6cfdd..50aa82e2c7b 100644 --- a/homeassistant/components/bosch_shc/translations/nl.json +++ b/homeassistant/components/bosch_shc/translations/nl.json @@ -33,6 +33,5 @@ "title": "SHC-authenticatieparameters" } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/no.json b/homeassistant/components/bosch_shc/translations/no.json index 53d64519fd5..1b5b9fb0642 100644 --- a/homeassistant/components/bosch_shc/translations/no.json +++ b/homeassistant/components/bosch_shc/translations/no.json @@ -33,6 +33,5 @@ "title": "SHC-autentiseringsparametere" } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/pl.json b/homeassistant/components/bosch_shc/translations/pl.json index 6b21fd87b7c..11ae44dbe48 100644 --- a/homeassistant/components/bosch_shc/translations/pl.json +++ b/homeassistant/components/bosch_shc/translations/pl.json @@ -33,6 +33,5 @@ "title": "Parametry uwierzytelniania SHC" } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/pt-BR.json b/homeassistant/components/bosch_shc/translations/pt-BR.json index f8a74157ab3..22f5bbe22bb 100644 --- a/homeassistant/components/bosch_shc/translations/pt-BR.json +++ b/homeassistant/components/bosch_shc/translations/pt-BR.json @@ -33,6 +33,5 @@ "title": "Par\u00e2metros de autentica\u00e7\u00e3o SHC" } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/ru.json b/homeassistant/components/bosch_shc/translations/ru.json index 498003b2501..c5a6a53582f 100644 --- a/homeassistant/components/bosch_shc/translations/ru.json +++ b/homeassistant/components/bosch_shc/translations/ru.json @@ -33,6 +33,5 @@ "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 SHC" } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/tr.json b/homeassistant/components/bosch_shc/translations/tr.json index f48286cf9cd..173b8e124ca 100644 --- a/homeassistant/components/bosch_shc/translations/tr.json +++ b/homeassistant/components/bosch_shc/translations/tr.json @@ -33,6 +33,5 @@ "title": "SHC kimlik do\u011frulama parametreleri" } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/zh-Hant.json b/homeassistant/components/bosch_shc/translations/zh-Hant.json index fd667210d3e..2e4086051d6 100644 --- a/homeassistant/components/bosch_shc/translations/zh-Hant.json +++ b/homeassistant/components/bosch_shc/translations/zh-Hant.json @@ -33,6 +33,5 @@ "title": "SHC \u8a8d\u8b49\u53c3\u6578" } } - }, - "title": "Bosch SHC" + } } \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/bg.json b/homeassistant/components/braviatv/translations/bg.json index d05511b8d29..ef8edbdab20 100644 --- a/homeassistant/components/braviatv/translations/bg.json +++ b/homeassistant/components/braviatv/translations/bg.json @@ -17,8 +17,7 @@ "user": { "data": { "host": "\u0425\u043e\u0441\u0442" - }, - "title": "Sony Bravia TV" + } } } } diff --git a/homeassistant/components/braviatv/translations/ca.json b/homeassistant/components/braviatv/translations/ca.json index b8616f5e8ba..2f35c77caa1 100644 --- a/homeassistant/components/braviatv/translations/ca.json +++ b/homeassistant/components/braviatv/translations/ca.json @@ -21,8 +21,7 @@ "data": { "host": "Amfitri\u00f3" }, - "description": "Assegura't que el televisor est\u00e0 engegat abans de configurar-lo.", - "title": "Televisor Sony Bravia" + "description": "Assegura't que el televisor est\u00e0 engegat abans de configurar-lo." } } }, diff --git a/homeassistant/components/braviatv/translations/cs.json b/homeassistant/components/braviatv/translations/cs.json index ae6e3e5c851..f08c5d82861 100644 --- a/homeassistant/components/braviatv/translations/cs.json +++ b/homeassistant/components/braviatv/translations/cs.json @@ -20,8 +20,7 @@ "data": { "host": "Hostitel" }, - "description": "Nastavte integraci televize Sony Bravia. Pokud m\u00e1te s nastaven\u00edm probl\u00e9my, pod\u00edvejte se na: https://www.home-assistant.io/integrations/braviatv\n\n Ujist\u011bte se, \u017ee va\u0161e televize je zapnut\u00e1.", - "title": "Televize Sony Bravia" + "description": "Nastavte integraci televize Sony Bravia. Pokud m\u00e1te s nastaven\u00edm probl\u00e9my, pod\u00edvejte se na: https://www.home-assistant.io/integrations/braviatv\n\n Ujist\u011bte se, \u017ee va\u0161e televize je zapnut\u00e1." } } }, diff --git a/homeassistant/components/braviatv/translations/da.json b/homeassistant/components/braviatv/translations/da.json index 006d6b708e5..5b9778575a4 100644 --- a/homeassistant/components/braviatv/translations/da.json +++ b/homeassistant/components/braviatv/translations/da.json @@ -3,9 +3,6 @@ "step": { "authorize": { "title": "Godkend Sony Bravia TV" - }, - "user": { - "title": "Sony Bravia TV" } } } diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json index 55ddbec464e..ff00828c0f3 100644 --- a/homeassistant/components/braviatv/translations/de.json +++ b/homeassistant/components/braviatv/translations/de.json @@ -21,8 +21,7 @@ "data": { "host": "Host" }, - "description": "Stelle sicher, dass dein Fernseher eingeschaltet ist, bevor du versuchst, ihn einzurichten.", - "title": "Sony Bravia TV" + "description": "Stelle sicher, dass dein Fernseher eingeschaltet ist, bevor du versuchst, ihn einzurichten." } } }, diff --git a/homeassistant/components/braviatv/translations/el.json b/homeassistant/components/braviatv/translations/el.json index 489bf0a7c36..070f546d532 100644 --- a/homeassistant/components/braviatv/translations/el.json +++ b/homeassistant/components/braviatv/translations/el.json @@ -21,8 +21,7 @@ "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" }, - "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7\u03c2 Sony Bravia. \u0395\u03ac\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: https://www.home-assistant.io/integrations/braviatv \n\n\u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7.", - "title": "\u03a4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 Sony Bravia" + "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7\u03c2 Sony Bravia. \u0395\u03ac\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: https://www.home-assistant.io/integrations/braviatv \n\n\u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7." } } }, diff --git a/homeassistant/components/braviatv/translations/en.json b/homeassistant/components/braviatv/translations/en.json index 9cc2ac23d17..45722d13b9a 100644 --- a/homeassistant/components/braviatv/translations/en.json +++ b/homeassistant/components/braviatv/translations/en.json @@ -21,8 +21,7 @@ "data": { "host": "Host" }, - "description": "Ensure that your TV is turned on before trying to set it up.", - "title": "Sony Bravia TV" + "description": "Ensure that your TV is turned on before trying to set it up." } } }, diff --git a/homeassistant/components/braviatv/translations/es-419.json b/homeassistant/components/braviatv/translations/es-419.json index 319eff13b98..ef12c71d681 100644 --- a/homeassistant/components/braviatv/translations/es-419.json +++ b/homeassistant/components/braviatv/translations/es-419.json @@ -18,8 +18,7 @@ "data": { "host": "Nombre de host de TV o direcci\u00f3n IP" }, - "description": "Configure la integraci\u00f3n de Sony Bravia TV. Si tiene problemas con la configuraci\u00f3n, vaya a: https://www.home-assistant.io/integrations/braviatv \n\n Aseg\u00farese de que su televisi\u00f3n est\u00e9 encendida.", - "title": "Sony Bravia TV" + "description": "Configure la integraci\u00f3n de Sony Bravia TV. Si tiene problemas con la configuraci\u00f3n, vaya a: https://www.home-assistant.io/integrations/braviatv \n\n Aseg\u00farese de que su televisi\u00f3n est\u00e9 encendida." } } }, diff --git a/homeassistant/components/braviatv/translations/es.json b/homeassistant/components/braviatv/translations/es.json index d095deea33b..401a254eeaf 100644 --- a/homeassistant/components/braviatv/translations/es.json +++ b/homeassistant/components/braviatv/translations/es.json @@ -21,8 +21,7 @@ "data": { "host": "Host" }, - "description": "Configura la integraci\u00f3n del televisor Sony Bravia. Si tienes problemas con la configuraci\u00f3n, ve a: https://www.home-assistant.io/integrations/braviatv\n\nAseg\u00farate de que tu televisor est\u00e1 encendido.", - "title": "Televisor Sony Bravia" + "description": "Configura la integraci\u00f3n del televisor Sony Bravia. Si tienes problemas con la configuraci\u00f3n, ve a: https://www.home-assistant.io/integrations/braviatv\n\nAseg\u00farate de que tu televisor est\u00e1 encendido." } } }, diff --git a/homeassistant/components/braviatv/translations/et.json b/homeassistant/components/braviatv/translations/et.json index c63e3635185..ecfc89b968d 100644 --- a/homeassistant/components/braviatv/translations/et.json +++ b/homeassistant/components/braviatv/translations/et.json @@ -21,8 +21,7 @@ "data": { "host": "" }, - "description": "Enne teleri seadistamist veendu, et see oleks sisse l\u00fclitatud.", - "title": "" + "description": "Enne teleri seadistamist veendu, et see oleks sisse l\u00fclitatud." } } }, diff --git a/homeassistant/components/braviatv/translations/fr.json b/homeassistant/components/braviatv/translations/fr.json index f85aea705fb..0290dad6857 100644 --- a/homeassistant/components/braviatv/translations/fr.json +++ b/homeassistant/components/braviatv/translations/fr.json @@ -21,8 +21,7 @@ "data": { "host": "H\u00f4te" }, - "description": "Assurez-vous que votre t\u00e9l\u00e9viseur est allum\u00e9 avant d'essayer de le configurer.", - "title": "Sony Bravia TV" + "description": "Assurez-vous que votre t\u00e9l\u00e9viseur est allum\u00e9 avant d'essayer de le configurer." } } }, diff --git a/homeassistant/components/braviatv/translations/hu.json b/homeassistant/components/braviatv/translations/hu.json index 554e74c52b1..d0d372df898 100644 --- a/homeassistant/components/braviatv/translations/hu.json +++ b/homeassistant/components/braviatv/translations/hu.json @@ -21,8 +21,7 @@ "data": { "host": "C\u00edm" }, - "description": "Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a TV k\u00e9sz\u00fcl\u00e9k be van kapcsolva a be\u00e1ll\u00edt\u00e1s el\u0151tt.", - "title": "Sony Bravia TV" + "description": "Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a TV k\u00e9sz\u00fcl\u00e9k be van kapcsolva a be\u00e1ll\u00edt\u00e1s el\u0151tt." } } }, diff --git a/homeassistant/components/braviatv/translations/id.json b/homeassistant/components/braviatv/translations/id.json index cb7d5396f7a..e387a2113b0 100644 --- a/homeassistant/components/braviatv/translations/id.json +++ b/homeassistant/components/braviatv/translations/id.json @@ -21,8 +21,7 @@ "data": { "host": "Host" }, - "description": "Pastikan TV Anda dinyalakan sebelum menyiapkan.", - "title": "TV Sony Bravia" + "description": "Pastikan TV Anda dinyalakan sebelum menyiapkan." } } }, diff --git a/homeassistant/components/braviatv/translations/it.json b/homeassistant/components/braviatv/translations/it.json index 7ab149fa6c7..3dd57d1359a 100644 --- a/homeassistant/components/braviatv/translations/it.json +++ b/homeassistant/components/braviatv/translations/it.json @@ -21,8 +21,7 @@ "data": { "host": "Host" }, - "description": "Assicurati che la tua TV sia accesa prima di provare a configurarla.", - "title": "Sony Bravia TV" + "description": "Assicurati che la tua TV sia accesa prima di provare a configurarla." } } }, diff --git a/homeassistant/components/braviatv/translations/ja.json b/homeassistant/components/braviatv/translations/ja.json index 87f903f0640..860fd5c89ea 100644 --- a/homeassistant/components/braviatv/translations/ja.json +++ b/homeassistant/components/braviatv/translations/ja.json @@ -21,8 +21,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "\u30bd\u30cb\u30fc Bravia TV\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/braviatv \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u30bd\u30cb\u30fc Bravia TV" + "description": "\u30bd\u30cb\u30fc Bravia TV\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/braviatv \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } }, diff --git a/homeassistant/components/braviatv/translations/ko.json b/homeassistant/components/braviatv/translations/ko.json index 7bad0f0047c..78c02bceb44 100644 --- a/homeassistant/components/braviatv/translations/ko.json +++ b/homeassistant/components/braviatv/translations/ko.json @@ -21,8 +21,7 @@ "data": { "host": "\ud638\uc2a4\ud2b8" }, - "description": "Sony Bravia TV \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694. \uad6c\uc131\uc5d0 \ubb38\uc81c\uac00 \uc788\ub294 \uacbd\uc6b0 https://www.home-assistant.io/integrations/braviatv \ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.\n\nTV\uac00 \ucf1c\uc838 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", - "title": "Sony Bravia TV" + "description": "Sony Bravia TV \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694. \uad6c\uc131\uc5d0 \ubb38\uc81c\uac00 \uc788\ub294 \uacbd\uc6b0 https://www.home-assistant.io/integrations/braviatv \ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.\n\nTV\uac00 \ucf1c\uc838 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694." } } }, diff --git a/homeassistant/components/braviatv/translations/lb.json b/homeassistant/components/braviatv/translations/lb.json index 3bcf8702c4f..6510666ad55 100644 --- a/homeassistant/components/braviatv/translations/lb.json +++ b/homeassistant/components/braviatv/translations/lb.json @@ -21,8 +21,7 @@ "data": { "host": "Host" }, - "description": "Sony Bravia TV Integratioun ariichten. Falls et Problemer mat der Konfiguratioun g\u00ebtt g\u00e9i op:\nhttps://www.home-assistant.io/integrations/braviatv\nStell s\u00e9cher dass d\u00e4in Fernseh un ass.", - "title": "Sony Bravia TV" + "description": "Sony Bravia TV Integratioun ariichten. Falls et Problemer mat der Konfiguratioun g\u00ebtt g\u00e9i op:\nhttps://www.home-assistant.io/integrations/braviatv\nStell s\u00e9cher dass d\u00e4in Fernseh un ass." } } }, diff --git a/homeassistant/components/braviatv/translations/nl.json b/homeassistant/components/braviatv/translations/nl.json index 133c5277045..f839b83e883 100644 --- a/homeassistant/components/braviatv/translations/nl.json +++ b/homeassistant/components/braviatv/translations/nl.json @@ -21,8 +21,7 @@ "data": { "host": "Host" }, - "description": "Zorg ervoor dat uw TV aan staat voordat u hem probeert in te stellen.", - "title": "Sony Bravia TV" + "description": "Zorg ervoor dat uw TV aan staat voordat u hem probeert in te stellen." } } }, diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json index 6465db8d3c0..067a2fcda97 100644 --- a/homeassistant/components/braviatv/translations/no.json +++ b/homeassistant/components/braviatv/translations/no.json @@ -21,8 +21,7 @@ "data": { "host": "Vert" }, - "description": "S\u00f8rg for at TV-en er sl\u00e5tt p\u00e5 f\u00f8r du pr\u00f8ver \u00e5 sette den opp.", - "title": "" + "description": "S\u00f8rg for at TV-en er sl\u00e5tt p\u00e5 f\u00f8r du pr\u00f8ver \u00e5 sette den opp." } } }, diff --git a/homeassistant/components/braviatv/translations/pl.json b/homeassistant/components/braviatv/translations/pl.json index 354962a62d9..83521554e21 100644 --- a/homeassistant/components/braviatv/translations/pl.json +++ b/homeassistant/components/braviatv/translations/pl.json @@ -21,8 +21,7 @@ "data": { "host": "Nazwa hosta lub adres IP" }, - "description": "Upewnij si\u0119, \u017ce telewizor jest w\u0142\u0105czony, zanim spr\u00f3bujesz go skonfigurowa\u0107.", - "title": "Sony Bravia TV" + "description": "Upewnij si\u0119, \u017ce telewizor jest w\u0142\u0105czony, zanim spr\u00f3bujesz go skonfigurowa\u0107." } } }, diff --git a/homeassistant/components/braviatv/translations/pt-BR.json b/homeassistant/components/braviatv/translations/pt-BR.json index a784e30d84c..3931935ff38 100644 --- a/homeassistant/components/braviatv/translations/pt-BR.json +++ b/homeassistant/components/braviatv/translations/pt-BR.json @@ -21,8 +21,7 @@ "data": { "host": "Nome do host" }, - "description": "Certifique-se de que sua TV esteja ligada antes de tentar configur\u00e1-la.", - "title": "Sony Bravia TV" + "description": "Certifique-se de que sua TV esteja ligada antes de tentar configur\u00e1-la." } } }, diff --git a/homeassistant/components/braviatv/translations/pt.json b/homeassistant/components/braviatv/translations/pt.json index 5e5f1367f58..e113d74d6fc 100644 --- a/homeassistant/components/braviatv/translations/pt.json +++ b/homeassistant/components/braviatv/translations/pt.json @@ -19,8 +19,7 @@ "user": { "data": { "host": "Servidor" - }, - "title": "TV Sony Bravia" + } } } }, diff --git a/homeassistant/components/braviatv/translations/ru.json b/homeassistant/components/braviatv/translations/ru.json index 3aeb83f7686..046a46c5ae4 100644 --- a/homeassistant/components/braviatv/translations/ru.json +++ b/homeassistant/components/braviatv/translations/ru.json @@ -21,8 +21,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u041f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d.", - "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Sony Bravia" + "description": "\u041f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d." } } }, diff --git a/homeassistant/components/braviatv/translations/sl.json b/homeassistant/components/braviatv/translations/sl.json index ae98bf15dc3..d412b545579 100644 --- a/homeassistant/components/braviatv/translations/sl.json +++ b/homeassistant/components/braviatv/translations/sl.json @@ -17,8 +17,7 @@ "data": { "host": "TV ime gostitelja ali IP naslov" }, - "description": "Nastavite integracijo Sony Bravia TV. \u010ce imate te\u017eave s konfiguracijo, pojdite na: https://www.home-assistant.io/integrations/braviatv \n\n Prepri\u010dajte se, da je va\u0161 televizor vklopljen.", - "title": "Sony Bravia TV" + "description": "Nastavite integracijo Sony Bravia TV. \u010ce imate te\u017eave s konfiguracijo, pojdite na: https://www.home-assistant.io/integrations/braviatv \n\n Prepri\u010dajte se, da je va\u0161 televizor vklopljen." } } }, diff --git a/homeassistant/components/braviatv/translations/sv.json b/homeassistant/components/braviatv/translations/sv.json index b07494f15cb..e3b98b4e76e 100644 --- a/homeassistant/components/braviatv/translations/sv.json +++ b/homeassistant/components/braviatv/translations/sv.json @@ -14,8 +14,7 @@ "user": { "data": { "host": "V\u00e4rdnamn eller IP-adress f\u00f6r TV" - }, - "title": "Sony Bravia TV" + } } } } diff --git a/homeassistant/components/braviatv/translations/tr.json b/homeassistant/components/braviatv/translations/tr.json index b6df4a7999a..cf5cc45640e 100644 --- a/homeassistant/components/braviatv/translations/tr.json +++ b/homeassistant/components/braviatv/translations/tr.json @@ -21,8 +21,7 @@ "data": { "host": "Ana Bilgisayar" }, - "description": "Kurmaya \u00e7al\u0131\u015fmadan \u00f6nce TV'nizin a\u00e7\u0131k oldu\u011fundan emin olun.", - "title": "Sony Bravia TV" + "description": "Kurmaya \u00e7al\u0131\u015fmadan \u00f6nce TV'nizin a\u00e7\u0131k oldu\u011fundan emin olun." } } }, diff --git a/homeassistant/components/braviatv/translations/uk.json b/homeassistant/components/braviatv/translations/uk.json index 7f66329c57e..5d9e22e59de 100644 --- a/homeassistant/components/braviatv/translations/uk.json +++ b/homeassistant/components/braviatv/translations/uk.json @@ -21,8 +21,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0454\u044e \u043f\u043e \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457:\nhttps://www.home-assistant.io/integrations/braviatv", - "title": "\u0422\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 Sony Bravia" + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0454\u044e \u043f\u043e \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457:\nhttps://www.home-assistant.io/integrations/braviatv" } } }, diff --git a/homeassistant/components/braviatv/translations/zh-Hans.json b/homeassistant/components/braviatv/translations/zh-Hans.json index d02d562d55d..447c136dcf4 100644 --- a/homeassistant/components/braviatv/translations/zh-Hans.json +++ b/homeassistant/components/braviatv/translations/zh-Hans.json @@ -9,8 +9,7 @@ "title": "\u6388\u6743 Sony Bravia \u7535\u89c6" }, "user": { - "description": "\u8bbe\u7f6e Sony Bravia \u7535\u89c6\u96c6\u6210\u3002\u5982\u679c\u60a8\u5728\u914d\u7f6e\u65b9\u9762\u9047\u5230\u95ee\u9898\uff0c\u8bf7\u8bbf\u95ee\uff1ahttps://www.home-assistant.io/integrations/braviatv\n\u786e\u4fdd\u7535\u89c6\u5df2\u6253\u5f00\u3002", - "title": "Sony Bravia TV" + "description": "\u8bbe\u7f6e Sony Bravia \u7535\u89c6\u96c6\u6210\u3002\u5982\u679c\u60a8\u5728\u914d\u7f6e\u65b9\u9762\u9047\u5230\u95ee\u9898\uff0c\u8bf7\u8bbf\u95ee\uff1ahttps://www.home-assistant.io/integrations/braviatv\n\u786e\u4fdd\u7535\u89c6\u5df2\u6253\u5f00\u3002" } } }, diff --git a/homeassistant/components/braviatv/translations/zh-Hant.json b/homeassistant/components/braviatv/translations/zh-Hant.json index 35ef6ef2e4f..1fb0931d4b6 100644 --- a/homeassistant/components/braviatv/translations/zh-Hant.json +++ b/homeassistant/components/braviatv/translations/zh-Hant.json @@ -21,8 +21,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u65bc\u8a2d\u5b9a\u524d\u78ba\u5b9a\u96fb\u8996\u5df2\u7d93\u958b\u555f\u3002", - "title": "Sony Bravia \u96fb\u8996" + "description": "\u65bc\u8a2d\u5b9a\u524d\u78ba\u5b9a\u96fb\u8996\u5df2\u7d93\u958b\u555f\u3002" } } }, diff --git a/homeassistant/components/climacell/translations/af.json b/homeassistant/components/climacell/translations/af.json index b62fc7023a4..d05e07e4eff 100644 --- a/homeassistant/components/climacell/translations/af.json +++ b/homeassistant/components/climacell/translations/af.json @@ -2,9 +2,8 @@ "options": { "step": { "init": { - "title": "Update ClimaCell opties" + "title": "Update [%key:component::climacell::title%] opties" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/bg.json b/homeassistant/components/climacell/translations/bg.json deleted file mode 100644 index af84485310d..00000000000 --- a/homeassistant/components/climacell/translations/bg.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "error": { - "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": { - "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/climacell/translations/ca.json b/homeassistant/components/climacell/translations/ca.json index 78d43991e90..2b6abb46737 100644 --- a/homeassistant/components/climacell/translations/ca.json +++ b/homeassistant/components/climacell/translations/ca.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_api_key": "Clau API inv\u00e0lida", - "rate_limited": "Freq\u00fc\u00e8ncia limitada temporalment, torna-ho a provar m\u00e9s tard.", - "unknown": "Error inesperat" - }, - "step": { - "user": { - "data": { - "api_key": "Clau API", - "api_version": "Versi\u00f3 de l'API", - "latitude": "Latitud", - "longitude": "Longitud", - "name": "Nom" - }, - "description": "Si no es proporcionen la Latitud i Longitud, s'utilitzaran els valors per defecte de la configuraci\u00f3 de Home Assistant. Es crear\u00e0 una entitat per a cada tipus de previsi\u00f3, per\u00f2 nom\u00e9s s'habilitaran les que seleccionis." - } - } - }, "options": { "step": { "init": { @@ -29,6 +9,5 @@ "title": "Actualitzaci\u00f3 d'opcions de ClimaCell" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/cs.json b/homeassistant/components/climacell/translations/cs.json deleted file mode 100644 index e9a608680d5..00000000000 --- a/homeassistant/components/climacell/translations/cs.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - }, - "step": { - "user": { - "data": { - "api_key": "Kl\u00ed\u010d API", - "api_version": "Verze API", - "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", - "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", - "name": "Jm\u00e9no" - } - } - } - }, - "title": "ClimaCell" -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/de.json b/homeassistant/components/climacell/translations/de.json index 01e9a81647e..7c3e929dde2 100644 --- a/homeassistant/components/climacell/translations/de.json +++ b/homeassistant/components/climacell/translations/de.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", - "rate_limited": "Aktuelle Aktualisierungsrate gedrosselt, bitte versuche es sp\u00e4ter erneut.", - "unknown": "Unerwarteter Fehler" - }, - "step": { - "user": { - "data": { - "api_key": "API-Schl\u00fcssel", - "api_version": "API Version", - "latitude": "Breitengrad", - "longitude": "L\u00e4ngengrad", - "name": "Name" - }, - "description": "Wenn Breitengrad und L\u00e4ngengrad nicht angegeben werden, werden die Standardwerte in der Home Assistant-Konfiguration verwendet. F\u00fcr jeden Vorhersagetyp wird eine Entit\u00e4t erstellt, aber nur die von Ihnen ausgew\u00e4hlten werden standardm\u00e4\u00dfig aktiviert." - } - } - }, "options": { "step": { "init": { @@ -29,6 +9,5 @@ "title": "ClimaCell-Optionen aktualisieren" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/el.json b/homeassistant/components/climacell/translations/el.json index 26c6d5e8d40..392573f693c 100644 --- a/homeassistant/components/climacell/translations/el.json +++ b/homeassistant/components/climacell/translations/el.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API", - "rate_limited": "\u0391\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c4\u03b9\u03b3\u03bc\u03ae \u03b7 \u03c4\u03b9\u03bc\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1.", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" - }, - "step": { - "user": { - "data": { - "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", - "api_version": "\u0388\u03ba\u03b4\u03bf\u03c3\u03b7 API", - "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03a0\u03bb\u03ac\u03c4\u03bf\u03c2", - "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u039c\u03ae\u03ba\u03bf\u03c2", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1" - }, - "description": "\u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c4\u03bf \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2 \u03ba\u03b1\u03b9 \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2, \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd \u03bf\u03b9 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c4\u03b9\u03bc\u03ad\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c4\u03bf\u03c5 Home Assistant. \u0398\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03bc\u03b9\u03b1 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c4\u03cd\u03c0\u03bf \u03b4\u03b5\u03bb\u03c4\u03af\u03bf\u03c5, \u03b1\u03bb\u03bb\u03ac \u03bc\u03cc\u03bd\u03bf \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03b5\u03c0\u03b9\u03bb\u03ad\u03b3\u03b5\u03c4\u03b5 \u03b8\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd \u03b1\u03c0\u03cc \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae." - } - } - }, "options": { "step": { "init": { @@ -29,6 +9,5 @@ "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd ClimaCell" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/en.json b/homeassistant/components/climacell/translations/en.json index 3e5cd436ba8..a35be85d5b2 100644 --- a/homeassistant/components/climacell/translations/en.json +++ b/homeassistant/components/climacell/translations/en.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "Failed to connect", - "invalid_api_key": "Invalid API key", - "rate_limited": "Currently rate limited, please try again later.", - "unknown": "Unexpected error" - }, - "step": { - "user": { - "data": { - "api_key": "API Key", - "api_version": "API Version", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Name" - }, - "description": "If Latitude and Longitude are not provided, the default values in the Home Assistant configuration will be used. An entity will be created for each forecast type but only the ones you select will be enabled by default." - } - } - }, "options": { "step": { "init": { @@ -29,6 +9,5 @@ "title": "Update ClimaCell Options" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/es-419.json b/homeassistant/components/climacell/translations/es-419.json index 17c63089dbf..449ad1ba367 100644 --- a/homeassistant/components/climacell/translations/es-419.json +++ b/homeassistant/components/climacell/translations/es-419.json @@ -1,16 +1,4 @@ { - "config": { - "error": { - "rate_limited": "Actualmente la tarifa est\u00e1 limitada. Vuelve a intentarlo m\u00e1s tarde." - }, - "step": { - "user": { - "data": { - "api_version": "Versi\u00f3n de la API" - } - } - } - }, "options": { "step": { "init": { @@ -21,6 +9,5 @@ "title": "Actualizar opciones de ClimaCell" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/es.json b/homeassistant/components/climacell/translations/es.json index dba1957dd21..056d26b077d 100644 --- a/homeassistant/components/climacell/translations/es.json +++ b/homeassistant/components/climacell/translations/es.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "Fallo al conectar", - "invalid_api_key": "Clave API no v\u00e1lida", - "rate_limited": "Actualmente la tarifa est\u00e1 limitada, por favor int\u00e9ntelo m\u00e1s tarde.", - "unknown": "Error inesperado" - }, - "step": { - "user": { - "data": { - "api_key": "Clave API", - "api_version": "Versi\u00f3n del API", - "latitude": "Latitud", - "longitude": "Longitud", - "name": "Nombre" - }, - "description": "Si no se proporcionan Latitud y Longitud , se utilizar\u00e1n los valores predeterminados en la configuraci\u00f3n de Home Assistant. Se crear\u00e1 una entidad para cada tipo de pron\u00f3stico, pero solo las que seleccione estar\u00e1n habilitadas de forma predeterminada." - } - } - }, "options": { "step": { "init": { @@ -29,6 +9,5 @@ "title": "Actualizaci\u00f3n de opciones de ClimaCell" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/et.json b/homeassistant/components/climacell/translations/et.json index 46ac184fa3c..5d915a87d80 100644 --- a/homeassistant/components/climacell/translations/et.json +++ b/homeassistant/components/climacell/translations/et.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "\u00dchendamine nurjus", - "invalid_api_key": "Vale API v\u00f5ti", - "rate_limited": "Hetkel on p\u00e4ringud piiratud, proovi hiljem uuesti.", - "unknown": "Ootamatu t\u00f5rge" - }, - "step": { - "user": { - "data": { - "api_key": "API v\u00f5ti", - "api_version": "API versioon", - "latitude": "Laiuskraad", - "longitude": "Pikkuskraad", - "name": "Nimi" - }, - "description": "Kui [%key:component::climacell::config::step::user::d ata::latitude%] ja [%key:component::climacell::config::step::user::d ata::longitude%] andmed pole sisestatud kasutatakse Home Assistanti vaikev\u00e4\u00e4rtusi. Olem luuakse iga prognoosit\u00fc\u00fcbi jaoks kuid vaikimisi lubatakse ainult need, mille valid." - } - } - }, "options": { "step": { "init": { @@ -26,9 +6,8 @@ "timestep": "Minuteid NowCasti prognooside vahel" }, "description": "Kui otsustad lubada \"nowcast\" prognoosi\u00fcksuse, saad seadistada minutite arvu iga prognoosi vahel. Esitatavate prognooside arv s\u00f5ltub prognooside vahel valitud minutite arvust.", - "title": "V\u00e4rskenda ClimaCell suvandeid" + "title": "V\u00e4rskenda [%key:component::climacell::title%] suvandeid" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/fr.json b/homeassistant/components/climacell/translations/fr.json index 9194073bb5a..b2c1285ecc9 100644 --- a/homeassistant/components/climacell/translations/fr.json +++ b/homeassistant/components/climacell/translations/fr.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 d'API non valide", - "rate_limited": "Nombre maximal de tentatives de connexion d\u00e9pass\u00e9, veuillez r\u00e9essayer ult\u00e9rieurement", - "unknown": "Erreur inattendue" - }, - "step": { - "user": { - "data": { - "api_key": "Cl\u00e9 d'API", - "api_version": "Version de l'API", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Nom" - }, - "description": "Si Latitude et Longitude ne sont pas fournis, les valeurs par d\u00e9faut de la configuration de Home Assistant seront utilis\u00e9es. Une entit\u00e9 sera cr\u00e9\u00e9e pour chaque type de pr\u00e9vision, mais seules celles que vous s\u00e9lectionnez seront activ\u00e9es par d\u00e9faut." - } - } - }, "options": { "step": { "init": { @@ -29,6 +9,5 @@ "title": "Mettre \u00e0 jour les options ClimaCell" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/hu.json b/homeassistant/components/climacell/translations/hu.json index bdeb913275c..f65fa638ced 100644 --- a/homeassistant/components/climacell/translations/hu.json +++ b/homeassistant/components/climacell/translations/hu.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", - "rate_limited": "Jelenleg korl\u00e1tozott a hozz\u00e1f\u00e9r\u00e9s, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" - }, - "step": { - "user": { - "data": { - "api_key": "API kulcs", - "api_version": "API Verzi\u00f3", - "latitude": "Sz\u00e9less\u00e9g", - "longitude": "Hossz\u00fas\u00e1g", - "name": "Elnevez\u00e9s" - }, - "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." - } - } - }, "options": { "step": { "init": { @@ -26,9 +6,8 @@ "timestep": "Min. A NowCast el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt" }, "description": "Ha a `nowcast` el\u0151rejelz\u00e9si entit\u00e1s enged\u00e9lyez\u00e9s\u00e9t v\u00e1lasztja, be\u00e1ll\u00edthatja az egyes el\u0151rejelz\u00e9sek k\u00f6z\u00f6tti percek sz\u00e1m\u00e1t. A megadott el\u0151rejelz\u00e9sek sz\u00e1ma az el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt kiv\u00e1lasztott percek sz\u00e1m\u00e1t\u00f3l f\u00fcgg.", - "title": "ClimaCell be\u00e1ll\u00edt\u00e1sok friss\u00edt\u00e9se" + "title": "[%key:component::climacell::title%] be\u00e1ll\u00edt\u00e1sok friss\u00edt\u00e9se" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/id.json b/homeassistant/components/climacell/translations/id.json index 88b377261bb..57f88557a69 100644 --- a/homeassistant/components/climacell/translations/id.json +++ b/homeassistant/components/climacell/translations/id.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "Gagal terhubung", - "invalid_api_key": "Kunci API tidak valid", - "rate_limited": "Saat ini tingkatnya dibatasi, coba lagi nanti.", - "unknown": "Kesalahan yang tidak diharapkan" - }, - "step": { - "user": { - "data": { - "api_key": "Kunci API", - "api_version": "Versi API", - "latitude": "Lintang", - "longitude": "Bujur", - "name": "Nama" - }, - "description": "Jika Lintang dan Bujur tidak tersedia, nilai default dalam konfigurasi Home Assistant akan digunakan. Entitas akan dibuat untuk setiap jenis prakiraan tetapi hanya yang Anda pilih yang akan diaktifkan secara default." - } - } - }, "options": { "step": { "init": { @@ -29,6 +9,5 @@ "title": "Perbarui Opsi ClimaCell" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/it.json b/homeassistant/components/climacell/translations/it.json index bd1bdd88238..b9667d6bfb1 100644 --- a/homeassistant/components/climacell/translations/it.json +++ b/homeassistant/components/climacell/translations/it.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "Impossibile connettersi", - "invalid_api_key": "Chiave API non valida", - "rate_limited": "Al momento la tariffa \u00e8 limitata, riprova pi\u00f9 tardi.", - "unknown": "Errore imprevisto" - }, - "step": { - "user": { - "data": { - "api_key": "Chiave API", - "api_version": "Versione API", - "latitude": "Latitudine", - "longitude": "Logitudine", - "name": "Nome" - }, - "description": "Se Latitudine e Logitudine non vengono forniti, verranno utilizzati i valori predefiniti nella configurazione di Home Assistant. Verr\u00e0 creata un'entit\u00e0 per ogni tipo di previsione, ma solo quelli selezionati saranno abilitati per impostazione predefinita." - } - } - }, "options": { "step": { "init": { @@ -29,6 +9,5 @@ "title": "Aggiorna le opzioni di ClimaCell" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ja.json b/homeassistant/components/climacell/translations/ja.json index 5114f8e9881..5c78820c853 100644 --- a/homeassistant/components/climacell/translations/ja.json +++ b/homeassistant/components/climacell/translations/ja.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", - "rate_limited": "\u73fe\u5728\u30ec\u30fc\u30c8\u304c\u5236\u9650\u3055\u308c\u3066\u3044\u307e\u3059\u306e\u3067\u3001\u5f8c\u3067\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" - }, - "step": { - "user": { - "data": { - "api_key": "API\u30ad\u30fc", - "api_version": "API\u30d0\u30fc\u30b8\u30e7\u30f3", - "latitude": "\u7def\u5ea6", - "longitude": "\u7d4c\u5ea6", - "name": "\u540d\u524d" - }, - "description": "\u7def\u5ea6\u3068\u7d4c\u5ea6\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001Home Assistant\u8a2d\u5b9a\u306e\u65e2\u5b9a\u5024\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306f\u4e88\u6e2c\u30bf\u30a4\u30d7\u3054\u3068\u306b\u4f5c\u6210\u3055\u308c\u307e\u3059\u304c\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u9078\u629e\u3057\u305f\u3082\u306e\u3060\u3051\u304c\u6709\u52b9\u306b\u306a\u308a\u307e\u3059\u3002" - } - } - }, "options": { "step": { "init": { @@ -26,9 +6,8 @@ "timestep": "\u6700\u5c0f: NowCast Forecasts\u306e\u9593" }, "description": "`nowcast` forecast(\u4e88\u6e2c) \u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u6709\u52b9\u306b\u3059\u308b\u3053\u3068\u3092\u9078\u629e\u3057\u305f\u5834\u5408\u3001\u5404\u4e88\u6e2c\u9593\u306e\u5206\u6570\u3092\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002\u63d0\u4f9b\u3055\u308c\u308bforecast(\u4e88\u6e2c)\u306e\u6570\u306f\u3001forecast(\u4e88\u6e2c)\u306e\u9593\u306b\u9078\u629e\u3057\u305f\u5206\u6570\u306b\u4f9d\u5b58\u3057\u307e\u3059\u3002", - "title": "ClimaCell\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u66f4\u65b0\u3057\u307e\u3059" + "title": "[%key:component::climacell::title%]\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u66f4\u65b0\u3057\u307e\u3059" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ko.json b/homeassistant/components/climacell/translations/ko.json index b5936bbc7d7..8accc07410d 100644 --- a/homeassistant/components/climacell/translations/ko.json +++ b/homeassistant/components/climacell/translations/ko.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "rate_limited": "\ud604\uc7ac \uc0ac\uc6a9 \ud69f\uc218\ub97c \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" - }, - "step": { - "user": { - "data": { - "api_key": "API \ud0a4", - "api_version": "API \ubc84\uc804", - "latitude": "\uc704\ub3c4", - "longitude": "\uacbd\ub3c4", - "name": "\uc774\ub984" - }, - "description": "\uc704\ub3c4 \ubc0f \uacbd\ub3c4\uac00 \uc81c\uacf5\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 Home Assistant \uad6c\uc131\uc758 \uae30\ubcf8\uac12\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4. \uac01 \uc77c\uae30\uc608\ubcf4 \uc720\ud615\uc5d0 \ub300\ud574 \uad6c\uc131\uc694\uc18c\uac00 \uc0dd\uc131\ub418\uc9c0\ub9cc \uae30\ubcf8\uc801\uc73c\ub85c \uc120\ud0dd\ud55c \uad6c\uc131\uc694\uc18c\ub9cc \ud65c\uc131\ud654\ub429\ub2c8\ub2e4." - } - } - }, "options": { "step": { "init": { @@ -26,9 +6,8 @@ "timestep": "\ub2e8\uae30\uc608\uce21 \uc77c\uae30\uc608\ubcf4 \uac04 \ucd5c\uc18c \uc2dc\uac04" }, "description": "`nowcast` \uc77c\uae30\uc608\ubcf4 \uad6c\uc131\uc694\uc18c\ub97c \uc0ac\uc6a9\ud558\ub3c4\ub85d \uc120\ud0dd\ud55c \uacbd\uc6b0 \uac01 \uc77c\uae30\uc608\ubcf4 \uc0ac\uc774\uc758 \uc2dc\uac04(\ubd84)\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc81c\uacf5\ub41c \uc77c\uae30\uc608\ubcf4 \ud69f\uc218\ub294 \uc608\uce21 \uac04 \uc120\ud0dd\ud55c \uc2dc\uac04(\ubd84)\uc5d0 \ub530\ub77c \ub2ec\ub77c\uc9d1\ub2c8\ub2e4.", - "title": "ClimaCell \uc635\uc158 \uc5c5\ub370\uc774\ud2b8\ud558\uae30" + "title": "[%key:component::climacell::title%] \uc635\uc158 \uc5c5\ub370\uc774\ud2b8\ud558\uae30" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/lb.json b/homeassistant/components/climacell/translations/lb.json deleted file mode 100644 index e075d198b7f..00000000000 --- a/homeassistant/components/climacell/translations/lb.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "api_version": "API Versioun" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/nl.json b/homeassistant/components/climacell/translations/nl.json index a8754e81943..a895fa8234d 100644 --- a/homeassistant/components/climacell/translations/nl.json +++ b/homeassistant/components/climacell/translations/nl.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "Kan geen verbinding maken", - "invalid_api_key": "Ongeldige API-sleutel", - "rate_limited": "Momenteel is een beperkt aantal aanvragen mogelijk, probeer later opnieuw.", - "unknown": "Onverwachte fout" - }, - "step": { - "user": { - "data": { - "api_key": "API-sleutel", - "api_version": "API-versie", - "latitude": "Breedtegraad", - "longitude": "Lengtegraad", - "name": "Naam" - }, - "description": "Indien Breedtegraad en Lengtegraad niet worden opgegeven, worden de standaardwaarden in de Home Assistant-configuratie gebruikt. Er wordt een entiteit gemaakt voor elk voorspellingstype, maar alleen degenen die u selecteert worden standaard ingeschakeld." - } - } - }, "options": { "step": { "init": { @@ -29,6 +9,5 @@ "title": "Update ClimaCell Opties" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/no.json b/homeassistant/components/climacell/translations/no.json index c20c458c64e..9f050624967 100644 --- a/homeassistant/components/climacell/translations/no.json +++ b/homeassistant/components/climacell/translations/no.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "Tilkobling mislyktes", - "invalid_api_key": "Ugyldig API-n\u00f8kkel", - "rate_limited": "Prisen er for \u00f8yeblikket begrenset. Pr\u00f8v igjen senere.", - "unknown": "Uventet feil" - }, - "step": { - "user": { - "data": { - "api_key": "API-n\u00f8kkel", - "api_version": "API-versjon", - "latitude": "Breddegrad", - "longitude": "Lengdegrad", - "name": "Navn" - }, - "description": "Hvis Breddegrad og Lengdegrad ikke er oppgitt, vil standardverdiene i Home Assistant-konfigurasjonen bli brukt. Det blir opprettet en entitet for hver prognosetype, men bare de du velger blir aktivert som standard." - } - } - }, "options": { "step": { "init": { @@ -29,6 +9,5 @@ "title": "Oppdater ClimaCell-alternativer" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/pl.json b/homeassistant/components/climacell/translations/pl.json index e107be1e001..5f69764ffab 100644 --- a/homeassistant/components/climacell/translations/pl.json +++ b/homeassistant/components/climacell/translations/pl.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_api_key": "Nieprawid\u0142owy klucz API", - "rate_limited": "Przekroczono limit, spr\u00f3buj ponownie p\u00f3\u017aniej.", - "unknown": "Nieoczekiwany b\u0142\u0105d" - }, - "step": { - "user": { - "data": { - "api_key": "Klucz API", - "api_version": "Wersja API", - "latitude": "Szeroko\u015b\u0107 geograficzna", - "longitude": "D\u0142ugo\u015b\u0107 geograficzna", - "name": "Nazwa" - }, - "description": "Je\u015bli szeroko\u015b\u0107 i d\u0142ugo\u015b\u0107 geograficzna nie zostan\u0105 podane, zostan\u0105 u\u017cyte domy\u015blne warto\u015bci z konfiguracji Home Assistanta. Zostanie utworzona encja dla ka\u017cdego typu prognozy, ale domy\u015blnie w\u0142\u0105czone bed\u0105 tylko te, kt\u00f3re wybierzesz." - } - } - }, "options": { "step": { "init": { @@ -29,6 +9,5 @@ "title": "Opcje aktualizacji ClimaCell" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/pt-BR.json b/homeassistant/components/climacell/translations/pt-BR.json index 54de15d1f7f..b7e71d45971 100644 --- a/homeassistant/components/climacell/translations/pt-BR.json +++ b/homeassistant/components/climacell/translations/pt-BR.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "Falha ao conectar", - "invalid_api_key": "Chave de API inv\u00e1lida", - "rate_limited": "Taxa atualmente limitada, tente novamente mais tarde.", - "unknown": "Erro inesperado" - }, - "step": { - "user": { - "data": { - "api_key": "Chave da API", - "api_version": "Vers\u00e3o da API", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Nome" - }, - "description": "Se Latitude e Longitude n\u00e3o forem fornecidos, os valores padr\u00f5es na configura\u00e7\u00e3o do Home Assistant ser\u00e3o usados. Uma entidade ser\u00e1 criada para cada tipo de previs\u00e3o, mas apenas as selecionadas ser\u00e3o habilitadas por padr\u00e3o." - } - } - }, "options": { "step": { "init": { @@ -29,6 +9,5 @@ "title": "Atualizar as op\u00e7\u00f5es do ClimaCell" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/pt.json b/homeassistant/components/climacell/translations/pt.json deleted file mode 100644 index 5c790233b20..00000000000 --- a/homeassistant/components/climacell/translations/pt.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o", - "invalid_api_key": "Chave de API inv\u00e1lida", - "unknown": "Erro inesperado" - }, - "step": { - "user": { - "data": { - "api_key": "Chave da API", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Nome" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/ru.json b/homeassistant/components/climacell/translations/ru.json index 0f1a80b5e09..9f3219ce4d6 100644 --- a/homeassistant/components/climacell/translations/ru.json +++ b/homeassistant/components/climacell/translations/ru.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", - "rate_limited": "\u041f\u0440\u0435\u0432\u044b\u0448\u0435\u043d\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u043f\u044b\u0442\u043e\u043a, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." - }, - "step": { - "user": { - "data": { - "api_key": "\u041a\u043b\u044e\u0447 API", - "api_version": "\u0412\u0435\u0440\u0441\u0438\u044f API", - "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", - "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" - }, - "description": "\u0415\u0441\u043b\u0438 \u0428\u0438\u0440\u043e\u0442\u0430 \u0438 \u0414\u043e\u043b\u0433\u043e\u0442\u0430 \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u044b, \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0431\u0443\u0434\u0443\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 Home Assistant. \u041e\u0431\u044a\u0435\u043a\u0442\u044b \u0431\u0443\u0434\u0443\u0442 \u0441\u043e\u0437\u0434\u0430\u043d\u044b \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0442\u0438\u043f\u0430 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430, \u043d\u043e \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0431\u0443\u0434\u0443\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0435 \u0412\u0430\u043c\u0438." - } - } - }, "options": { "step": { "init": { @@ -29,6 +9,5 @@ "title": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 ClimaCell" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sk.json b/homeassistant/components/climacell/translations/sk.json deleted file mode 100644 index 8e0bc629a13..00000000000 --- a/homeassistant/components/climacell/translations/sk.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "error": { - "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d" - }, - "step": { - "user": { - "data": { - "api_key": "API k\u013e\u00fa\u010d", - "latitude": "Zemepisn\u00e1 \u0161\u00edrka", - "longitude": "Zemepisn\u00e1 d\u013a\u017eka", - "name": "N\u00e1zov" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sv.json b/homeassistant/components/climacell/translations/sv.json deleted file mode 100644 index e6e7a77926f..00000000000 --- a/homeassistant/components/climacell/translations/sv.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "api_version": "API-version", - "latitude": "Latitud", - "longitude": "Longitud", - "name": "Namn" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/tr.json b/homeassistant/components/climacell/translations/tr.json index 369d4c6ae13..54e24f813e4 100644 --- a/homeassistant/components/climacell/translations/tr.json +++ b/homeassistant/components/climacell/translations/tr.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131", - "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", - "rate_limited": "\u015eu anda oran s\u0131n\u0131rl\u0131, l\u00fctfen daha sonra tekrar deneyin.", - "unknown": "Beklenmeyen hata" - }, - "step": { - "user": { - "data": { - "api_key": "API Anahtar\u0131", - "api_version": "API S\u00fcr\u00fcm\u00fc", - "latitude": "Enlem", - "longitude": "Boylam", - "name": "Ad" - }, - "description": "Enlem ve Boylam sa\u011flanmazsa, Home Assistant yap\u0131land\u0131rmas\u0131ndaki varsay\u0131lan de\u011ferler kullan\u0131l\u0131r. Her tahmin t\u00fcr\u00fc i\u00e7in bir varl\u0131k olu\u015fturulacak, ancak varsay\u0131lan olarak yaln\u0131zca se\u00e7tikleriniz etkinle\u015ftirilecektir." - } - } - }, "options": { "step": { "init": { @@ -29,6 +9,5 @@ "title": "ClimaCell Se\u00e7eneklerini G\u00fcncelleyin" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/zh-Hans.json b/homeassistant/components/climacell/translations/zh-Hans.json deleted file mode 100644 index 315d060bc69..00000000000 --- a/homeassistant/components/climacell/translations/zh-Hans.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "api_key": "API Key", - "name": "\u540d\u5b57" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/zh-Hant.json b/homeassistant/components/climacell/translations/zh-Hant.json index 68e06219ae7..309b39ab242 100644 --- a/homeassistant/components/climacell/translations/zh-Hant.json +++ b/homeassistant/components/climacell/translations/zh-Hant.json @@ -1,24 +1,4 @@ { - "config": { - "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_api_key": "API \u91d1\u9470\u7121\u6548", - "rate_limited": "\u9054\u5230\u9650\u5236\u983b\u7387\u3001\u8acb\u7a0d\u5019\u518d\u8a66\u3002", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" - }, - "step": { - "user": { - "data": { - "api_key": "API \u91d1\u9470", - "api_version": "API \u7248\u672c", - "latitude": "\u7def\u5ea6", - "longitude": "\u7d93\u5ea6", - "name": "\u540d\u7a31" - }, - "description": "\u5047\u5982\u672a\u63d0\u4f9b\u7def\u5ea6\u8207\u7d93\u5ea6\uff0c\u5c07\u6703\u4f7f\u7528 Home Assistant \u8a2d\u5b9a\u4f5c\u70ba\u9810\u8a2d\u503c\u3002\u6bcf\u4e00\u500b\u9810\u5831\u985e\u5225\u90fd\u6703\u7522\u751f\u4e00\u7d44\u5be6\u9ad4\uff0c\u6216\u8005\u9810\u8a2d\u70ba\u6240\u9078\u64c7\u555f\u7528\u7684\u9810\u5831\u3002" - } - } - }, "options": { "step": { "init": { @@ -29,6 +9,5 @@ "title": "\u66f4\u65b0 ClimaCell \u9078\u9805" } } - }, - "title": "ClimaCell" + } } \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/ar.json b/homeassistant/components/coinbase/translations/ar.json index 30655126631..ec83ad6d22b 100644 --- a/homeassistant/components/coinbase/translations/ar.json +++ b/homeassistant/components/coinbase/translations/ar.json @@ -3,8 +3,7 @@ "step": { "user": { "data": { - "api_token": "\u0633\u0631 API", - "exchange_rates": "\u0623\u0633\u0639\u0627\u0631 \u0627\u0644\u0635\u0631\u0641" + "api_token": "\u0633\u0631 API" }, "description": "\u064a\u0631\u062c\u0649 \u0625\u062f\u062e\u0627\u0644 \u062a\u0641\u0627\u0635\u064a\u0644 \u0645\u0641\u062a\u0627\u062d API \u0627\u0644\u062e\u0627\u0635 \u0628\u0643 \u0639\u0644\u0649 \u0627\u0644\u0646\u062d\u0648 \u0627\u0644\u0645\u0646\u0635\u0648\u0635 \u0639\u0644\u064a\u0647 \u0645\u0646 \u0642\u0628\u0644 Coinbase.", "title": "\u062a\u0641\u0627\u0635\u064a\u0644 \u0645\u0641\u062a\u0627\u062d Coinbase API" @@ -13,8 +12,6 @@ }, "options": { "error": { - "currency_unavaliable": "\u0644\u0627 \u064a\u062a\u0645 \u062a\u0648\u0641\u064a\u0631 \u0648\u0627\u062d\u062f \u0623\u0648 \u0623\u0643\u062b\u0631 \u0645\u0646 \u0623\u0631\u0635\u062f\u0629 \u0627\u0644\u0639\u0645\u0644\u0627\u062a \u0627\u0644\u0645\u0637\u0644\u0648\u0628\u0629 \u0628\u0648\u0627\u0633\u0637\u0629 Coinbase API \u0627\u0644\u062e\u0627\u0635 \u0628\u0643.", - "exchange_rate_unavaliable": "\u0644\u0627 \u064a\u062a\u0645 \u062a\u0648\u0641\u064a\u0631 \u0648\u0627\u062d\u062f \u0623\u0648 \u0623\u0643\u062b\u0631 \u0645\u0646 \u0623\u0633\u0639\u0627\u0631 \u0627\u0644\u0635\u0631\u0641 \u0627\u0644\u0645\u0637\u0644\u0648\u0628\u0629 \u0645\u0646 Coinbase.", "unknown": "\u062d\u062f\u062b \u062e\u0637\u0623 \u063a\u064a\u0631 \u0645\u062a\u0648\u0642\u0639" }, "step": { diff --git a/homeassistant/components/coinbase/translations/ca.json b/homeassistant/components/coinbase/translations/ca.json index 0543f97003a..116b611f272 100644 --- a/homeassistant/components/coinbase/translations/ca.json +++ b/homeassistant/components/coinbase/translations/ca.json @@ -14,9 +14,7 @@ "user": { "data": { "api_key": "Clau API", - "api_token": "Secret API", - "currencies": "Monedes del saldo del compte", - "exchange_rates": "Tipus de canvi" + "api_token": "Secret API" }, "description": "Introdueix els detalls de la teva clau API tal com els proporciona Coinbase.", "title": "Detalls de la clau API de Coinbase" @@ -26,9 +24,7 @@ "options": { "error": { "currency_unavailable": "L'API de Coinbase no proporciona algun/s dels saldos de moneda que has sol\u00b7licitat.", - "currency_unavaliable": "L'API de Coinbase no proporciona algun/s dels saldos de moneda que has sol\u00b7licitat.", "exchange_rate_unavailable": "L'API de Coinbase no proporciona algun/s dels tipus de canvi que has sol\u00b7licitat.", - "exchange_rate_unavaliable": "L'API de Coinbase no proporciona algun/s dels tipus de canvi que has sol\u00b7licitat.", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/coinbase/translations/cs.json b/homeassistant/components/coinbase/translations/cs.json index d25e431651d..7ec64b2fa14 100644 --- a/homeassistant/components/coinbase/translations/cs.json +++ b/homeassistant/components/coinbase/translations/cs.json @@ -12,8 +12,7 @@ "user": { "data": { "api_key": "Kl\u00ed\u010d API", - "api_token": "API Secret", - "exchange_rates": "Sm\u011bnn\u00e9 kurzy" + "api_token": "API Secret" }, "title": "Podrobnosti o API kl\u00ed\u010di Coinbase" } diff --git a/homeassistant/components/coinbase/translations/de.json b/homeassistant/components/coinbase/translations/de.json index d4b58ae42bd..f6200633950 100644 --- a/homeassistant/components/coinbase/translations/de.json +++ b/homeassistant/components/coinbase/translations/de.json @@ -14,9 +14,7 @@ "user": { "data": { "api_key": "API-Schl\u00fcssel", - "api_token": "API-Geheimnis", - "currencies": "Kontostand W\u00e4hrungen", - "exchange_rates": "Wechselkurse" + "api_token": "API-Geheimnis" }, "description": "Bitte gib die Details deines API-Schl\u00fcssels ein, wie von Coinbase bereitgestellt.", "title": "Coinbase API Schl\u00fcssel Details" @@ -26,9 +24,7 @@ "options": { "error": { "currency_unavailable": "Eine oder mehrere der angeforderten W\u00e4hrungssalden werden von deiner Coinbase-API nicht bereitgestellt.", - "currency_unavaliable": "Eine oder mehrere der angeforderten W\u00e4hrungssalden werden von deiner Coinbase-API nicht bereitgestellt.", "exchange_rate_unavailable": "Einer oder mehrere der angeforderten Wechselkurse werden nicht von Coinbase bereitgestellt.", - "exchange_rate_unavaliable": "Einer oder mehrere der angeforderten Wechselkurse werden nicht von Coinbase bereitgestellt.", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/coinbase/translations/el.json b/homeassistant/components/coinbase/translations/el.json index 05d6a1f7415..6b2141f091f 100644 --- a/homeassistant/components/coinbase/translations/el.json +++ b/homeassistant/components/coinbase/translations/el.json @@ -14,9 +14,7 @@ "user": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", - "api_token": "\u039c\u03c5\u03c3\u03c4\u03b9\u03ba\u03cc API", - "currencies": "\u039d\u03bf\u03bc\u03af\u03c3\u03bc\u03b1\u03c4\u03b1 \u03c5\u03c0\u03bf\u03bb\u03bf\u03af\u03c0\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd", - "exchange_rates": "\u03a3\u03c5\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b9\u03c3\u03bf\u03c4\u03b9\u03bc\u03af\u03b5\u03c2" + "api_token": "\u039c\u03c5\u03c3\u03c4\u03b9\u03ba\u03cc API" }, "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd API \u03c3\u03b1\u03c2, \u03cc\u03c0\u03c9\u03c2 \u03b1\u03c5\u03c4\u03ac \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd Coinbase.", "title": "\u0392\u03b1\u03c3\u03b9\u03ba\u03ad\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2 API \u03c4\u03bf\u03c5 Coinbase" @@ -26,9 +24,7 @@ "options": { "error": { "currency_unavailable": "\u0388\u03bd\u03b1 \u03ae \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03b1\u03c0\u03cc \u03c4\u03b1 \u03b6\u03b7\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b1 \u03c5\u03c0\u03cc\u03bb\u03bf\u03b9\u03c0\u03b1 \u03bd\u03bf\u03bc\u03b9\u03c3\u03bc\u03ac\u03c4\u03c9\u03bd \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf API \u03c4\u03b7\u03c2 Coinbase.", - "currency_unavaliable": "\u0388\u03bd\u03b1 \u03ae \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b1 \u03b1\u03c0\u03cc \u03c4\u03b1 \u03b6\u03b7\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b1 \u03c5\u03c0\u03cc\u03bb\u03bf\u03b9\u03c0\u03b1 \u03bd\u03bf\u03bc\u03b9\u03c3\u03bc\u03ac\u03c4\u03c9\u03bd \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Coinbase API \u03c3\u03b1\u03c2.", "exchange_rate_unavailable": "\u039c\u03af\u03b1 \u03ae \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 \u03b6\u03b7\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b5\u03c2 \u03c3\u03c5\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b9\u03c3\u03bf\u03c4\u03b9\u03bc\u03af\u03b5\u03c2 \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd Coinbase.", - "exchange_rate_unavaliable": "\u039c\u03af\u03b1 \u03ae \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 \u03b6\u03b7\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b5\u03c2 \u03c3\u03c5\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b9\u03c3\u03bf\u03c4\u03b9\u03bc\u03af\u03b5\u03c2 \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd Coinbase.", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { diff --git a/homeassistant/components/coinbase/translations/en.json b/homeassistant/components/coinbase/translations/en.json index d5d7483e260..019159c8057 100644 --- a/homeassistant/components/coinbase/translations/en.json +++ b/homeassistant/components/coinbase/translations/en.json @@ -14,9 +14,7 @@ "user": { "data": { "api_key": "API Key", - "api_token": "API Secret", - "currencies": "Account Balance Currencies", - "exchange_rates": "Exchange Rates" + "api_token": "API Secret" }, "description": "Please enter the details of your API key as provided by Coinbase.", "title": "Coinbase API Key Details" @@ -26,9 +24,7 @@ "options": { "error": { "currency_unavailable": "One or more of the requested currency balances is not provided by your Coinbase API.", - "currency_unavaliable": "One or more of the requested currency balances is not provided by your Coinbase API.", "exchange_rate_unavailable": "One or more of the requested exchange rates is not provided by Coinbase.", - "exchange_rate_unavaliable": "One or more of the requested exchange rates is not provided by Coinbase.", "unknown": "Unexpected error" }, "step": { diff --git a/homeassistant/components/coinbase/translations/es-419.json b/homeassistant/components/coinbase/translations/es-419.json index c5bc63ee29a..1b72a0c0b2e 100644 --- a/homeassistant/components/coinbase/translations/es-419.json +++ b/homeassistant/components/coinbase/translations/es-419.json @@ -7,9 +7,7 @@ "step": { "user": { "data": { - "api_token": "Secreto de la API", - "currencies": "Monedas del saldo de la cuenta", - "exchange_rates": "Tipos de cambio" + "api_token": "Secreto de la API" }, "description": "Ingrese los detalles de su clave API proporcionada por Coinbase.", "title": "Detalles clave de la API de Coinbase" @@ -19,9 +17,7 @@ "options": { "error": { "currency_unavailable": "Su API de Coinbase no proporciona uno o m\u00e1s de los saldos de divisas solicitados.", - "currency_unavaliable": "Su API de Coinbase no proporciona uno o m\u00e1s de los saldos de divisas solicitados.", - "exchange_rate_unavailable": "Coinbase no proporciona uno o m\u00e1s de los tipos de cambio solicitados.", - "exchange_rate_unavaliable": "Coinbase no proporciona uno o m\u00e1s de los tipos de cambio solicitados." + "exchange_rate_unavailable": "Coinbase no proporciona uno o m\u00e1s de los tipos de cambio solicitados." } } } \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/es.json b/homeassistant/components/coinbase/translations/es.json index d8961ee6b3a..5454aca8ec6 100644 --- a/homeassistant/components/coinbase/translations/es.json +++ b/homeassistant/components/coinbase/translations/es.json @@ -14,9 +14,7 @@ "user": { "data": { "api_key": "Clave API", - "api_token": "Secreto de la API", - "currencies": "Saldo de la cuenta Monedas", - "exchange_rates": "Tipos de cambio" + "api_token": "Secreto de la API" }, "description": "Por favor, introduce los detalles de tu clave API tal y como te la ha proporcionado Coinbase.", "title": "Detalles de la clave API de Coinbase" @@ -25,9 +23,7 @@ }, "options": { "error": { - "currency_unavaliable": "La API de Coinbase no proporciona uno o m\u00e1s de los saldos de divisas solicitados.", "exchange_rate_unavailable": "El API de Coinbase no proporciona alguno/s de los tipos de cambio que has solicitado.", - "exchange_rate_unavaliable": "Coinbase no proporciona uno o m\u00e1s de los tipos de cambio solicitados.", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/coinbase/translations/et.json b/homeassistant/components/coinbase/translations/et.json index 821d17656d2..14bd1eea370 100644 --- a/homeassistant/components/coinbase/translations/et.json +++ b/homeassistant/components/coinbase/translations/et.json @@ -14,9 +14,7 @@ "user": { "data": { "api_key": "API v\u00f5ti", - "api_token": "API salas\u00f5na", - "currencies": "Konto saldo valuutad", - "exchange_rates": "Vahetuskursid" + "api_token": "API salas\u00f5na" }, "description": "Sisesta Coinbase'i pakutava API-v\u00f5tme \u00fcksikasjad.", "title": "Coinbase'i API v\u00f5tme \u00fcksikasjad" @@ -26,9 +24,7 @@ "options": { "error": { "currency_unavailable": "Coinbase API ei paku \u00fchte v\u00f5i mitut soovitud valuutasaldot.", - "currency_unavaliable": "Coinbase'i API ei paku \u00fchte v\u00f5i mitut taotletud valuutasaldot.", "exchange_rate_unavailable": "Coinbase ei paku \u00fchte v\u00f5i mitut soovitud vahetuskurssi.", - "exchange_rate_unavaliable": "\u00dchte v\u00f5i mitut taotletud vahetuskurssi Coinbase ei paku.", "unknown": "Ootamatu t\u00f5rge" }, "step": { diff --git a/homeassistant/components/coinbase/translations/fr.json b/homeassistant/components/coinbase/translations/fr.json index 77664cca73d..91f52941a89 100644 --- a/homeassistant/components/coinbase/translations/fr.json +++ b/homeassistant/components/coinbase/translations/fr.json @@ -14,9 +14,7 @@ "user": { "data": { "api_key": "Cl\u00e9 d'API", - "api_token": "API secr\u00e8te", - "currencies": "Devises du solde du compte", - "exchange_rates": "Taux d'\u00e9change" + "api_token": "API secr\u00e8te" }, "description": "Veuillez saisir les d\u00e9tails de votre cl\u00e9 API tels que fournis par Coinbase.", "title": "D\u00e9tails de la cl\u00e9 de l'API Coinbase" @@ -26,9 +24,7 @@ "options": { "error": { "currency_unavailable": "Un ou plusieurs des soldes de devises demand\u00e9s ne sont pas fournis par votre API Coinbase.", - "currency_unavaliable": "Un ou plusieurs des soldes de devises demand\u00e9s ne sont pas fournis par votre API Coinbase.", "exchange_rate_unavailable": "Un ou plusieurs des taux de change demand\u00e9s ne sont pas fournis par Coinbase.", - "exchange_rate_unavaliable": "Un ou plusieurs des taux de change demand\u00e9s ne sont pas fournis par Coinbase.", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/coinbase/translations/hu.json b/homeassistant/components/coinbase/translations/hu.json index 44287da0ee6..bcf409d2dda 100644 --- a/homeassistant/components/coinbase/translations/hu.json +++ b/homeassistant/components/coinbase/translations/hu.json @@ -14,9 +14,7 @@ "user": { "data": { "api_key": "API kulcs", - "api_token": "API jelsz\u00f3", - "currencies": "Sz\u00e1mlaegyenleg-p\u00e9nznemek", - "exchange_rates": "\u00c1rfolyamok" + "api_token": "API jelsz\u00f3" }, "description": "K\u00e9rj\u00fck, adja meg API kulcs\u00e1nak adatait a Coinbase \u00e1ltal megadott m\u00f3don.", "title": "Coinbase API kulcs r\u00e9szletei" @@ -26,9 +24,7 @@ "options": { "error": { "currency_unavailable": "A k\u00e9rt valutaegyenlegek k\u00f6z\u00fcl egyet vagy t\u00f6bbet nem biztos\u00edt a Coinbase API.", - "currency_unavaliable": "A k\u00e9rt valutaegyenlegek k\u00f6z\u00fcl egyet vagy t\u00f6bbet nem biztos\u00edt a Coinbase API.", "exchange_rate_unavailable": "A k\u00e9rt \u00e1rfolyamok k\u00f6z\u00fcl egyet vagy t\u00f6bbet a Coinbase nem biztos\u00edt.", - "exchange_rate_unavaliable": "A k\u00e9rt \u00e1rfolyamok k\u00f6z\u00fcl egyet vagy t\u00f6bbet a Coinbase nem biztos\u00edt.", "unknown": "Ismeretlen hiba" }, "step": { diff --git a/homeassistant/components/coinbase/translations/id.json b/homeassistant/components/coinbase/translations/id.json index 477aceafa45..ba2d2de0753 100644 --- a/homeassistant/components/coinbase/translations/id.json +++ b/homeassistant/components/coinbase/translations/id.json @@ -14,9 +14,7 @@ "user": { "data": { "api_key": "Kunci API", - "api_token": "Kode Rahasia API", - "currencies": "Mata Uang Saldo Akun", - "exchange_rates": "Nilai Tukar" + "api_token": "Kode Rahasia API" }, "description": "Silakan masukkan detail kunci API Anda sesuai yang disediakan oleh Coinbase.", "title": "Detail Kunci API Coinbase" @@ -26,9 +24,7 @@ "options": { "error": { "currency_unavailable": "Satu atau beberapa saldo mata uang yang diminta tidak disediakan oleh API Coinbase Anda.", - "currency_unavaliable": "Satu atau beberapa saldo mata uang yang diminta tidak disediakan oleh API Coinbase Anda.", "exchange_rate_unavailable": "Satu atau beberapa nilai tukar yang diminta tidak disediakan oleh Coinbase.", - "exchange_rate_unavaliable": "Satu atau beberapa nilai tukar yang diminta tidak disediakan oleh Coinbase.", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/coinbase/translations/it.json b/homeassistant/components/coinbase/translations/it.json index 64b5b0cdca7..f26e08a727c 100644 --- a/homeassistant/components/coinbase/translations/it.json +++ b/homeassistant/components/coinbase/translations/it.json @@ -14,9 +14,7 @@ "user": { "data": { "api_key": "Chiave API", - "api_token": "API segreta", - "currencies": "Valute del saldo del conto", - "exchange_rates": "Tassi di cambio" + "api_token": "API segreta" }, "description": "Inserisci i dettagli della tua chiave API come forniti da Coinbase.", "title": "Dettagli della chiave API di Coinbase" @@ -26,9 +24,7 @@ "options": { "error": { "currency_unavailable": "Uno o pi\u00f9 dei saldi in valuta richiesti non sono forniti dalla tua API Coinbase.", - "currency_unavaliable": "Uno o pi\u00f9 saldi in valuta richiesti non sono forniti dalla tua API Coinbase.", "exchange_rate_unavailable": "Uno o pi\u00f9 dei tassi di cambio richiesti non sono forniti da Coinbase.", - "exchange_rate_unavaliable": "Uno o pi\u00f9 dei tassi di cambio richiesti non sono forniti da Coinbase.", "unknown": "Errore imprevisto" }, "step": { diff --git a/homeassistant/components/coinbase/translations/ja.json b/homeassistant/components/coinbase/translations/ja.json index 321e0c05d9d..011ff093747 100644 --- a/homeassistant/components/coinbase/translations/ja.json +++ b/homeassistant/components/coinbase/translations/ja.json @@ -14,9 +14,7 @@ "user": { "data": { "api_key": "API\u30ad\u30fc", - "api_token": "API\u30b7\u30fc\u30af\u30ec\u30c3\u30c8", - "currencies": "\u53e3\u5ea7\u6b8b\u9ad8 \u901a\u8ca8", - "exchange_rates": "\u70ba\u66ff\u30ec\u30fc\u30c8" + "api_token": "API\u30b7\u30fc\u30af\u30ec\u30c3\u30c8" }, "description": "Coinbase\u304b\u3089\u63d0\u4f9b\u3055\u308c\u305fAPI\u30ad\u30fc\u306e\u8a73\u7d30\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Coinbase API\u30ad\u30fc\u306e\u8a73\u7d30" @@ -26,9 +24,7 @@ "options": { "error": { "currency_unavailable": "\u30ea\u30af\u30a8\u30b9\u30c8\u3057\u305f\u901a\u8ca8\u6b8b\u9ad8\u306e1\u3064\u4ee5\u4e0a\u304c\u3001Coinbase API\u306b\u3088\u3063\u3066\u63d0\u4f9b\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "currency_unavaliable": "\u8981\u6c42\u3055\u308c\u305f\u901a\u8ca8\u6b8b\u9ad8\u306e1\u3064\u4ee5\u4e0a\u304c\u3001Coinbase API\u306b\u3088\u3063\u3066\u63d0\u4f9b\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "exchange_rate_unavailable": "\u30ea\u30af\u30a8\u30b9\u30c8\u3057\u305f\u70ba\u66ff\u30ec\u30fc\u30c8\u306e1\u3064\u4ee5\u4e0a\u304c\u3001Coinbase\u306b\u3088\u3063\u3066\u63d0\u4f9b\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "exchange_rate_unavaliable": "\u8981\u6c42\u3055\u308c\u305f\u70ba\u66ff\u30ec\u30fc\u30c8\u306e1\u3064\u4ee5\u4e0a\u304cCoinbase\u306b\u3088\u3063\u3066\u63d0\u4f9b\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/coinbase/translations/nl.json b/homeassistant/components/coinbase/translations/nl.json index 98763deb9a7..472a15659c0 100644 --- a/homeassistant/components/coinbase/translations/nl.json +++ b/homeassistant/components/coinbase/translations/nl.json @@ -14,9 +14,7 @@ "user": { "data": { "api_key": "API-sleutel", - "api_token": "API-geheim", - "currencies": "Valuta's van rekeningsaldo", - "exchange_rates": "Wisselkoersen" + "api_token": "API-geheim" }, "description": "Voer de gegevens van uw API-sleutel in zoals verstrekt door Coinbase.", "title": "Coinbase API Sleutel Details" @@ -26,9 +24,7 @@ "options": { "error": { "currency_unavailable": "Een of meer van de gevraagde valutabalansen wordt niet geleverd door uw Coinbase API.", - "currency_unavaliable": "Een of meer van de gevraagde valutasaldi worden niet geleverd door uw Coinbase API.", "exchange_rate_unavailable": "Een of meer van de gevraagde wisselkoersen worden niet door Coinbase geleverd.", - "exchange_rate_unavaliable": "Een of meer van de gevraagde wisselkoersen worden niet door Coinbase verstrekt.", "unknown": "Onverwachte fout" }, "step": { diff --git a/homeassistant/components/coinbase/translations/no.json b/homeassistant/components/coinbase/translations/no.json index 5171814cf9d..c3f2b34cf92 100644 --- a/homeassistant/components/coinbase/translations/no.json +++ b/homeassistant/components/coinbase/translations/no.json @@ -14,9 +14,7 @@ "user": { "data": { "api_key": "API-n\u00f8kkel", - "api_token": "API-hemmelighet", - "currencies": "Valutaer for kontosaldo", - "exchange_rates": "Valutakurser" + "api_token": "API-hemmelighet" }, "description": "Vennligst skriv inn detaljene for API-n\u00f8kkelen din som gitt av Coinbase.", "title": "Detaljer for Coinbase API-n\u00f8kkel" @@ -26,9 +24,7 @@ "options": { "error": { "currency_unavailable": "En eller flere av de forespurte valutasaldoene leveres ikke av Coinbase API.", - "currency_unavaliable": "En eller flere av de forespurte valutasaldoene leveres ikke av Coinbase API.", "exchange_rate_unavailable": "En eller flere av de forespurte valutakursene er ikke levert av Coinbase.", - "exchange_rate_unavaliable": "En eller flere av de forespurte valutakursene leveres ikke av Coinbase.", "unknown": "Uventet feil" }, "step": { diff --git a/homeassistant/components/coinbase/translations/pl.json b/homeassistant/components/coinbase/translations/pl.json index 7465ae24486..70a1a021cdf 100644 --- a/homeassistant/components/coinbase/translations/pl.json +++ b/homeassistant/components/coinbase/translations/pl.json @@ -14,9 +14,7 @@ "user": { "data": { "api_key": "Klucz API", - "api_token": "Sekretne API", - "currencies": "Waluty salda konta", - "exchange_rates": "Kursy wymiany" + "api_token": "Sekretne API" }, "description": "Wprowad\u017a dane swojego klucza API podane przez Coinbase.", "title": "Szczeg\u00f3\u0142y klucza API Coinbase" @@ -26,9 +24,7 @@ "options": { "error": { "currency_unavailable": "Jeden lub wi\u0119cej \u017c\u0105danych sald walutowych nie jest dostarczanych przez interfejs API Coinbase.", - "currency_unavaliable": "Jeden lub wi\u0119cej \u017c\u0105danych sald walutowych nie jest dostarczanych przez interfejs API Coinbase.", "exchange_rate_unavailable": "Jeden lub wi\u0119cej z \u017c\u0105danych kurs\u00f3w wymiany nie jest dostarczany przez Coinbase.", - "exchange_rate_unavaliable": "Jeden lub wi\u0119cej z \u017c\u0105danych kurs\u00f3w wymiany nie jest dostarczany przez Coinbase.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { diff --git a/homeassistant/components/coinbase/translations/pt-BR.json b/homeassistant/components/coinbase/translations/pt-BR.json index 5ed52fa7afc..5f2bb7d96e3 100644 --- a/homeassistant/components/coinbase/translations/pt-BR.json +++ b/homeassistant/components/coinbase/translations/pt-BR.json @@ -14,9 +14,7 @@ "user": { "data": { "api_key": "Chave da API", - "api_token": "Segredo da API", - "currencies": "Moedas do saldo da conta", - "exchange_rates": "Taxas de c\u00e2mbio" + "api_token": "Segredo da API" }, "description": "Por favor, insira os detalhes da sua chave de API conforme fornecido pela Coinbase.", "title": "Detalhes da chave da API Coinbase" @@ -26,9 +24,7 @@ "options": { "error": { "currency_unavailable": "Um ou mais dos saldos de moeda solicitados n\u00e3o s\u00e3o fornecidos pela sua API Coinbase.", - "currency_unavaliable": "Um ou mais dos saldos de moeda solicitados n\u00e3o s\u00e3o fornecidos pela sua API Coinbase.", "exchange_rate_unavailable": "Uma ou mais taxas de c\u00e2mbio solicitadas n\u00e3o s\u00e3o fornecidas pela Coinbase.", - "exchange_rate_unavaliable": "Uma ou mais taxas de c\u00e2mbio solicitadas n\u00e3o s\u00e3o fornecidas pela Coinbase.", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/coinbase/translations/ru.json b/homeassistant/components/coinbase/translations/ru.json index 951c0182320..cbdf39e61a6 100644 --- a/homeassistant/components/coinbase/translations/ru.json +++ b/homeassistant/components/coinbase/translations/ru.json @@ -14,9 +14,7 @@ "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", - "api_token": "\u0421\u0435\u043a\u0440\u0435\u0442 API", - "currencies": "\u041e\u0441\u0442\u0430\u0442\u043e\u043a \u0432\u0430\u043b\u044e\u0442\u044b \u043d\u0430 \u0441\u0447\u0435\u0442\u0435", - "exchange_rates": "\u041e\u0431\u043c\u0435\u043d\u043d\u044b\u0435 \u043a\u0443\u0440\u0441\u044b" + "api_token": "\u0421\u0435\u043a\u0440\u0435\u0442 API" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0412\u0430\u0448\u0435\u0433\u043e \u043a\u043b\u044e\u0447\u0430 API Coinbase.", "title": "\u041a\u043b\u044e\u0447 API Coinbase" @@ -26,9 +24,7 @@ "options": { "error": { "currency_unavailable": "\u041e\u0434\u0438\u043d \u0438\u043b\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u044b\u0445 \u043e\u0441\u0442\u0430\u0442\u043a\u043e\u0432 \u0432\u0430\u043b\u044e\u0442\u044b \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0412\u0430\u0448\u0438\u043c API Coinbase.", - "currency_unavaliable": "\u041e\u0434\u0438\u043d \u0438\u043b\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u044b\u0445 \u043e\u0441\u0442\u0430\u0442\u043a\u043e\u0432 \u0432\u0430\u043b\u044e\u0442\u044b \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0412\u0430\u0448\u0438\u043c API Coinbase.", "exchange_rate_unavailable": "Coinbase \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043e\u0434\u0438\u043d \u0438\u043b\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u044b\u0445 \u043e\u0431\u043c\u0435\u043d\u043d\u044b\u0445 \u043a\u0443\u0440\u0441\u043e\u0432.", - "exchange_rate_unavaliable": "Coinbase \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043e\u0434\u0438\u043d \u0438\u043b\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u044b\u0445 \u043e\u0431\u043c\u0435\u043d\u043d\u044b\u0445 \u043a\u0443\u0440\u0441\u043e\u0432.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/coinbase/translations/tr.json b/homeassistant/components/coinbase/translations/tr.json index e21cab489e4..b84e2bf740e 100644 --- a/homeassistant/components/coinbase/translations/tr.json +++ b/homeassistant/components/coinbase/translations/tr.json @@ -14,9 +14,7 @@ "user": { "data": { "api_key": "API Anahtar\u0131", - "api_token": "API Gizli Anahtar\u0131", - "currencies": "Hesap Bakiyesi Para Birimleri", - "exchange_rates": "D\u00f6viz Kurlar\u0131" + "api_token": "API Gizli Anahtar\u0131" }, "description": "L\u00fctfen API anahtar\u0131n\u0131z\u0131n ayr\u0131nt\u0131lar\u0131n\u0131 Coinbase taraf\u0131ndan sa\u011flanan \u015fekilde girin.", "title": "Coinbase API Anahtar Ayr\u0131nt\u0131lar\u0131" @@ -26,9 +24,7 @@ "options": { "error": { "currency_unavailable": "\u0130stenen para birimi bakiyelerinden biri veya daha fazlas\u0131 Coinbase API'niz taraf\u0131ndan sa\u011flanm\u0131yor.", - "currency_unavaliable": "\u0130stenen para birimi bakiyelerinden biri veya daha fazlas\u0131 Coinbase API'niz taraf\u0131ndan sa\u011flanm\u0131yor.", "exchange_rate_unavailable": "\u0130stenen d\u00f6viz kurlar\u0131ndan biri veya daha fazlas\u0131 Coinbase taraf\u0131ndan sa\u011flanm\u0131yor.", - "exchange_rate_unavaliable": "\u0130stenen d\u00f6viz kurlar\u0131ndan biri veya daha fazlas\u0131 Coinbase taraf\u0131ndan sa\u011flanm\u0131yor.", "unknown": "Beklenmeyen hata" }, "step": { diff --git a/homeassistant/components/coinbase/translations/zh-Hans.json b/homeassistant/components/coinbase/translations/zh-Hans.json index 1a5eaa19dec..954a10c70a6 100644 --- a/homeassistant/components/coinbase/translations/zh-Hans.json +++ b/homeassistant/components/coinbase/translations/zh-Hans.json @@ -12,9 +12,7 @@ "user": { "data": { "api_key": "API \u5bc6\u94a5", - "api_token": "API Token", - "currencies": "\u8d26\u6237\u4f59\u989d", - "exchange_rates": "\u6c47\u7387" + "api_token": "API Token" }, "description": "\u8bf7\u8f93\u5165\u7531 Coinbase \u63d0\u4f9b\u7684 API \u5bc6\u94a5\u4fe1\u606f", "title": "Coinbase API \u5bc6\u94a5\u8be6\u60c5" @@ -23,8 +21,6 @@ }, "options": { "error": { - "currency_unavaliable": "Coinbase \u65e0\u6cd5\u63d0\u4f9b\u5176\u8bbe\u5b9a\u7684\u6c47\u7387\u4fe1\u606f", - "exchange_rate_unavaliable": "Coinbase \u65e0\u6cd5\u63d0\u4f9b\u5176\u8bbe\u5b9a\u7684\u6c47\u7387\u4fe1\u606f", "unknown": "\u672a\u77e5\u9519\u8bef" }, "step": { diff --git a/homeassistant/components/coinbase/translations/zh-Hant.json b/homeassistant/components/coinbase/translations/zh-Hant.json index 315fe90254f..ea48d90fc7e 100644 --- a/homeassistant/components/coinbase/translations/zh-Hant.json +++ b/homeassistant/components/coinbase/translations/zh-Hant.json @@ -14,9 +14,7 @@ "user": { "data": { "api_key": "API \u91d1\u9470", - "api_token": "API \u79c1\u9470", - "currencies": "\u5e33\u6236\u9918\u984d\u8ca8\u5e63", - "exchange_rates": "\u532f\u7387" + "api_token": "API \u79c1\u9470" }, "description": "\u8acb\u8f38\u5165\u7531 Coinbase \u63d0\u4f9b\u7684 API \u91d1\u9470\u8cc7\u8a0a\u3002", "title": "Coinbase API \u91d1\u9470\u8cc7\u6599" @@ -26,9 +24,7 @@ "options": { "error": { "currency_unavailable": "Coinbase API \u672a\u63d0\u4f9b\u4e00\u500b\u6216\u591a\u500b\u6240\u8981\u6c42\u7684\u8ca8\u5e63\u9918\u984d\u3002", - "currency_unavaliable": "Coinbase API \u672a\u63d0\u4f9b\u4e00\u500b\u6216\u591a\u500b\u6240\u8981\u6c42\u7684\u8ca8\u5e63\u9918\u984d\u3002", "exchange_rate_unavailable": "Coinbase \u672a\u63d0\u4f9b\u4e00\u500b\u6216\u591a\u500b\u6240\u8981\u6c42\u7684\u532f\u7387\u3002", - "exchange_rate_unavaliable": "Coinbase \u672a\u63d0\u4f9b\u4e00\u500b\u6216\u591a\u500b\u6240\u8981\u6c42\u7684\u532f\u7387\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { diff --git a/homeassistant/components/cpuspeed/translations/ko.json b/homeassistant/components/cpuspeed/translations/ko.json new file mode 100644 index 00000000000..758f3336cd4 --- /dev/null +++ b/homeassistant/components/cpuspeed/translations/ko.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/bg.json b/homeassistant/components/deconz/translations/bg.json index efba936a8d0..f8b1e351fb0 100644 --- a/homeassistant/components/deconz/translations/bg.json +++ b/homeassistant/components/deconz/translations/bg.json @@ -4,7 +4,6 @@ "already_configured": "\u041c\u043e\u0441\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "already_in_progress": "\u0412 \u043c\u043e\u043c\u0435\u043d\u0442\u0430 \u0442\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f.", "no_bridges": "\u041d\u0435 \u0441\u0430 \u043e\u0442\u043a\u0440\u0438\u0442\u0438 \u043c\u043e\u0441\u0442\u043e\u0432\u0435 deCONZ", - "not_deconz_bridge": "\u041d\u0435 \u0435 deCONZ \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f", "updated_instance": "\u041e\u0431\u043d\u043e\u0432\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 deCONZ \u0441 \u043d\u043e\u0432 \u0430\u0434\u0440\u0435\u0441" }, "error": { diff --git a/homeassistant/components/deconz/translations/ca.json b/homeassistant/components/deconz/translations/ca.json index 2d15801b590..f4659557503 100644 --- a/homeassistant/components/deconz/translations/ca.json +++ b/homeassistant/components/deconz/translations/ca.json @@ -5,7 +5,6 @@ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "no_bridges": "No s'han descobert enlla\u00e7os amb deCONZ", "no_hardware_available": "No hi ha cap maquinari r\u00e0dio connectat a deCONZ", - "not_deconz_bridge": "No \u00e9s un enlla\u00e7 deCONZ", "updated_instance": "S'ha actualitzat la inst\u00e0ncia de deCONZ amb una nova adre\u00e7a" }, "error": { diff --git a/homeassistant/components/deconz/translations/cs.json b/homeassistant/components/deconz/translations/cs.json index 323b29ddac8..8e6c6a71a44 100644 --- a/homeassistant/components/deconz/translations/cs.json +++ b/homeassistant/components/deconz/translations/cs.json @@ -5,7 +5,6 @@ "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", "no_bridges": "\u017d\u00e1dn\u00e9 deCONZ p\u0159emost\u011bn\u00ed nebyly nalezeny", "no_hardware_available": "K deCONZ nen\u00ed p\u0159ipojeno \u017e\u00e1dn\u00e9 r\u00e1diov\u00e9 za\u0159\u00edzen\u00ed", - "not_deconz_bridge": "Nejedn\u00e1 se o deCONZ p\u0159emost\u011bn\u00ed", "updated_instance": "Instance deCONZ aktualizov\u00e1na s nov\u00fdm hostitelem" }, "error": { diff --git a/homeassistant/components/deconz/translations/da.json b/homeassistant/components/deconz/translations/da.json index 6f63540c924..0091e9d0349 100644 --- a/homeassistant/components/deconz/translations/da.json +++ b/homeassistant/components/deconz/translations/da.json @@ -4,7 +4,6 @@ "already_configured": "Bridge er allerede konfigureret", "already_in_progress": "Konfigurationsflow for bro er allerede i gang.", "no_bridges": "Ingen deConz-bro fundet", - "not_deconz_bridge": "Ikke en deCONZ-bro", "updated_instance": "Opdaterede deCONZ-instans med ny v\u00e6rtadresse" }, "error": { diff --git a/homeassistant/components/deconz/translations/de.json b/homeassistant/components/deconz/translations/de.json index 05fa6ce8b3f..29b322466d5 100644 --- a/homeassistant/components/deconz/translations/de.json +++ b/homeassistant/components/deconz/translations/de.json @@ -5,7 +5,6 @@ "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "no_bridges": "Keine deCONZ-Bridges entdeckt", "no_hardware_available": "Keine Funkhardware an deCONZ angeschlossen", - "not_deconz_bridge": "Keine deCONZ Bridge entdeckt", "updated_instance": "deCONZ-Instanz mit neuer Host-Adresse aktualisiert" }, "error": { diff --git a/homeassistant/components/deconz/translations/el.json b/homeassistant/components/deconz/translations/el.json index 18eeaf700b6..3e7d3b166c1 100644 --- a/homeassistant/components/deconz/translations/el.json +++ b/homeassistant/components/deconz/translations/el.json @@ -5,7 +5,6 @@ "already_in_progress": "\u03a4\u03bf \u03b4\u03b9\u03ac\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03b3\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", "no_bridges": "\u0394\u03b5\u03bd \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03b3\u03ad\u03c6\u03c5\u03c1\u03b5\u03c2 deCONZ", "no_hardware_available": "\u0394\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03b1\u03c3\u03cd\u03c1\u03bc\u03b1\u03c4\u03bf \u03c5\u03bb\u03b9\u03ba\u03cc \u03c3\u03c4\u03bf deCONZ", - "not_deconz_bridge": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b3\u03ad\u03c6\u03c5\u03c1\u03b1 deCONZ", "updated_instance": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03bc\u03ad\u03bd\u03b7 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 deCONZ \u03bc\u03b5 \u03bd\u03ad\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae" }, "error": { diff --git a/homeassistant/components/deconz/translations/en.json b/homeassistant/components/deconz/translations/en.json index 16034e12414..af73a3b43be 100644 --- a/homeassistant/components/deconz/translations/en.json +++ b/homeassistant/components/deconz/translations/en.json @@ -5,7 +5,6 @@ "already_in_progress": "Configuration flow is already in progress", "no_bridges": "No deCONZ bridges discovered", "no_hardware_available": "No radio hardware connected to deCONZ", - "not_deconz_bridge": "Not a deCONZ bridge", "updated_instance": "Updated deCONZ instance with new host address" }, "error": { diff --git a/homeassistant/components/deconz/translations/es-419.json b/homeassistant/components/deconz/translations/es-419.json index ceb0ca39d2c..a8a8965daf5 100644 --- a/homeassistant/components/deconz/translations/es-419.json +++ b/homeassistant/components/deconz/translations/es-419.json @@ -5,7 +5,6 @@ "already_in_progress": "El flujo de configuraci\u00f3n para el puente ya est\u00e1 en progreso.", "no_bridges": "No se descubrieron puentes deCONZ", "no_hardware_available": "No hay hardware de radio conectado a deCONZ", - "not_deconz_bridge": "No es un puente deCONZ", "updated_instance": "Instancia deCONZ actualizada con nueva direcci\u00f3n de host" }, "error": { diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index 0174c4cf76d..2921dc2f2bf 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -5,7 +5,6 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "no_bridges": "No se han descubierto pasarelas deCONZ", "no_hardware_available": "No hay hardware de radio conectado a deCONZ", - "not_deconz_bridge": "No es una pasarela deCONZ", "updated_instance": "Instancia deCONZ actualizada con nueva direcci\u00f3n de host" }, "error": { diff --git a/homeassistant/components/deconz/translations/et.json b/homeassistant/components/deconz/translations/et.json index 8519182878c..0fd28a0889d 100644 --- a/homeassistant/components/deconz/translations/et.json +++ b/homeassistant/components/deconz/translations/et.json @@ -5,7 +5,6 @@ "already_in_progress": "Seadistamine on juba k\u00e4imas", "no_bridges": "DeCONZ l\u00fc\u00fcse ei leitud", "no_hardware_available": "DeCONZi raadio riistvara puudub", - "not_deconz_bridge": "See pole deCONZ-i sild", "updated_instance": "DeCONZ-i eksemplarile omistati uus hostiaadress" }, "error": { diff --git a/homeassistant/components/deconz/translations/fr.json b/homeassistant/components/deconz/translations/fr.json index 12f63765314..711322df9dd 100644 --- a/homeassistant/components/deconz/translations/fr.json +++ b/homeassistant/components/deconz/translations/fr.json @@ -5,7 +5,6 @@ "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "no_bridges": "Aucun pont deCONZ n'a \u00e9t\u00e9 d\u00e9couvert", "no_hardware_available": "Aucun mat\u00e9riel radio connect\u00e9 \u00e0 deCONZ", - "not_deconz_bridge": "Pas un pont deCONZ", "updated_instance": "Instance deCONZ mise \u00e0 jour avec la nouvelle adresse d'h\u00f4te" }, "error": { diff --git a/homeassistant/components/deconz/translations/hu.json b/homeassistant/components/deconz/translations/hu.json index c92a5dd36cd..7bf6d021634 100644 --- a/homeassistant/components/deconz/translations/hu.json +++ b/homeassistant/components/deconz/translations/hu.json @@ -5,7 +5,6 @@ "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "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" }, "error": { diff --git a/homeassistant/components/deconz/translations/id.json b/homeassistant/components/deconz/translations/id.json index e76e8b98d55..b6d7329758b 100644 --- a/homeassistant/components/deconz/translations/id.json +++ b/homeassistant/components/deconz/translations/id.json @@ -5,7 +5,6 @@ "already_in_progress": "Alur konfigurasi sedang berlangsung", "no_bridges": "deCONZ bridge tidak ditemukan", "no_hardware_available": "Tidak ada perangkat keras radio yang terhubung ke deCONZ", - "not_deconz_bridge": "Bukan bridge deCONZ", "updated_instance": "Instans deCONZ yang diperbarui dengan alamat host baru" }, "error": { diff --git a/homeassistant/components/deconz/translations/it.json b/homeassistant/components/deconz/translations/it.json index 44c204d7ed9..638b4db4222 100644 --- a/homeassistant/components/deconz/translations/it.json +++ b/homeassistant/components/deconz/translations/it.json @@ -5,7 +5,6 @@ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "no_bridges": "Nessun bridge deCONZ rilevato", "no_hardware_available": "Nessun hardware radio collegato a deCONZ", - "not_deconz_bridge": "Non \u00e8 un bridge deCONZ", "updated_instance": "Istanza deCONZ aggiornata con nuovo indirizzo host" }, "error": { diff --git a/homeassistant/components/deconz/translations/ja.json b/homeassistant/components/deconz/translations/ja.json index ce1cab4dea6..2962f3d4531 100644 --- a/homeassistant/components/deconz/translations/ja.json +++ b/homeassistant/components/deconz/translations/ja.json @@ -5,7 +5,6 @@ "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "no_bridges": "deCONZ\u30d6\u30ea\u30c3\u30b8\u306f\u691c\u51fa\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f", "no_hardware_available": "deCONZ\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308b\u7121\u7dda\u30cf\u30fc\u30c9\u30a6\u30a7\u30a2\u304c\u3042\u308a\u307e\u305b\u3093", - "not_deconz_bridge": "deCONZ bridge\u3067\u306f\u3042\u308a\u307e\u305b\u3093", "updated_instance": "\u65b0\u3057\u3044\u30db\u30b9\u30c8\u30a2\u30c9\u30ec\u30b9\u3067deCONZ\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f" }, "error": { diff --git a/homeassistant/components/deconz/translations/ko.json b/homeassistant/components/deconz/translations/ko.json index 5158d557106..e12bab3b64a 100644 --- a/homeassistant/components/deconz/translations/ko.json +++ b/homeassistant/components/deconz/translations/ko.json @@ -5,10 +5,10 @@ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "no_bridges": "\ubc1c\uacac\ub41c deCONZ \ube0c\ub9ac\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", "no_hardware_available": "deCONZ\uc5d0 \uc5f0\uacb0\ub41c \ubb34\uc120 \ud558\ub4dc\uc6e8\uc5b4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", - "not_deconz_bridge": "deCONZ \ube0c\ub9ac\uc9c0\uac00 \uc544\ub2d9\ub2c8\ub2e4", "updated_instance": "deCONZ \uc778\uc2a4\ud134\uc2a4\ub97c \uc0c8\ub85c\uc6b4 \ud638\uc2a4\ud2b8 \uc8fc\uc18c\ub85c \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4" }, "error": { + "linking_not_possible": "\uac8c\uc774\ud2b8\uc6e8\uc774\uc640 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "no_key": "API \ud0a4\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" }, "flow_title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774 ({host})", diff --git a/homeassistant/components/deconz/translations/lb.json b/homeassistant/components/deconz/translations/lb.json index 84a535c8ebc..b5bb383d29a 100644 --- a/homeassistant/components/deconz/translations/lb.json +++ b/homeassistant/components/deconz/translations/lb.json @@ -5,7 +5,6 @@ "already_in_progress": "Konfiguratioun's Oflaf ass schonn am gaang.", "no_bridges": "Keng dECONZ bridges fonnt", "no_hardware_available": "Keng Radio Hardware verbonne mat deCONZ", - "not_deconz_bridge": "Keng deCONZ Bridge", "updated_instance": "deCONZ Instanz gouf mat der neier Adress vum Apparat ge\u00e4nnert" }, "error": { diff --git a/homeassistant/components/deconz/translations/nl.json b/homeassistant/components/deconz/translations/nl.json index 6339ad0fd10..caa6ca461fc 100644 --- a/homeassistant/components/deconz/translations/nl.json +++ b/homeassistant/components/deconz/translations/nl.json @@ -5,7 +5,6 @@ "already_in_progress": "De configuratiestroom is al aan de gang", "no_bridges": "Geen deCONZ bridges ontdekt", "no_hardware_available": "Geen radiohardware aangesloten op deCONZ", - "not_deconz_bridge": "Geen deCONZ bridge", "updated_instance": "DeCONZ-instantie bijgewerkt met nieuw host-adres" }, "error": { diff --git a/homeassistant/components/deconz/translations/no.json b/homeassistant/components/deconz/translations/no.json index 7682cb15a5d..22a294d3242 100644 --- a/homeassistant/components/deconz/translations/no.json +++ b/homeassistant/components/deconz/translations/no.json @@ -5,7 +5,6 @@ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "no_bridges": "Ingen deCONZ broer oppdaget", "no_hardware_available": "Ingen radiomaskinvare koblet til deCONZ", - "not_deconz_bridge": "Ikke en deCONZ bro", "updated_instance": "Oppdatert deCONZ forekomst med ny vertsadresse" }, "error": { diff --git a/homeassistant/components/deconz/translations/pl.json b/homeassistant/components/deconz/translations/pl.json index 7f49a259304..7894494336e 100644 --- a/homeassistant/components/deconz/translations/pl.json +++ b/homeassistant/components/deconz/translations/pl.json @@ -5,7 +5,6 @@ "already_in_progress": "Konfiguracja jest ju\u017c w toku", "no_bridges": "Nie odkryto mostk\u00f3w deCONZ", "no_hardware_available": "Nie wykryto komponentu radiowego pod\u0142\u0105czonego do deCONZ", - "not_deconz_bridge": "To nie jest mostek deCONZ", "updated_instance": "Zaktualizowano instancj\u0119 deCONZ o nowy adres hosta" }, "error": { diff --git a/homeassistant/components/deconz/translations/pt-BR.json b/homeassistant/components/deconz/translations/pt-BR.json index fbbd776cbe8..785ada6b4c3 100644 --- a/homeassistant/components/deconz/translations/pt-BR.json +++ b/homeassistant/components/deconz/translations/pt-BR.json @@ -5,7 +5,6 @@ "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_bridges": "N\u00e3o h\u00e1 pontes de deCONZ descobertas", "no_hardware_available": "Nenhum hardware de r\u00e1dio conectado ao deCONZ", - "not_deconz_bridge": "N\u00e3o \u00e9 uma ponte deCONZ", "updated_instance": "Atualiza\u00e7\u00e3o da inst\u00e2ncia deCONZ com novo endere\u00e7o de host" }, "error": { diff --git a/homeassistant/components/deconz/translations/pt.json b/homeassistant/components/deconz/translations/pt.json index a64e37ed15b..94efb5c68ca 100644 --- a/homeassistant/components/deconz/translations/pt.json +++ b/homeassistant/components/deconz/translations/pt.json @@ -3,8 +3,7 @@ "abort": { "already_configured": "Bridge j\u00e1 est\u00e1 configurada", "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", - "no_bridges": "Nenhum hub deCONZ descoberto", - "not_deconz_bridge": "N\u00e3o \u00e9 uma bridge deCONZ" + "no_bridges": "Nenhum hub deCONZ descoberto" }, "error": { "no_key": "N\u00e3o foi poss\u00edvel obter uma Chave da API" diff --git a/homeassistant/components/deconz/translations/ru.json b/homeassistant/components/deconz/translations/ru.json index f92ebd0e1c7..91c44036331 100644 --- a/homeassistant/components/deconz/translations/ru.json +++ b/homeassistant/components/deconz/translations/ru.json @@ -5,7 +5,6 @@ "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_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", "no_hardware_available": "\u041a deCONZ \u043d\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0440\u0430\u0434\u0438\u043e\u043e\u0431\u043e\u0440\u0443\u0434\u043e\u0432\u0430\u043d\u0438\u0435.", - "not_deconz_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c deCONZ.", "updated_instance": "\u0410\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d." }, "error": { diff --git a/homeassistant/components/deconz/translations/sl.json b/homeassistant/components/deconz/translations/sl.json index 9e8ed42c07e..cfb52ca7af5 100644 --- a/homeassistant/components/deconz/translations/sl.json +++ b/homeassistant/components/deconz/translations/sl.json @@ -4,7 +4,6 @@ "already_configured": "Most je \u017ee nastavljen", "already_in_progress": "Konfiguracijski tok za most je \u017ee v teku.", "no_bridges": "Ni odkritih mostov deCONZ", - "not_deconz_bridge": "Ni deCONZ most", "updated_instance": "Posodobljen deCONZ z novim naslovom gostitelja" }, "error": { diff --git a/homeassistant/components/deconz/translations/sv.json b/homeassistant/components/deconz/translations/sv.json index 1a867d0c7fb..fd0968a941a 100644 --- a/homeassistant/components/deconz/translations/sv.json +++ b/homeassistant/components/deconz/translations/sv.json @@ -4,7 +4,6 @@ "already_configured": "Bryggan \u00e4r redan konfigurerad", "already_in_progress": "Konfigurations fl\u00f6det f\u00f6r bryggan p\u00e5g\u00e5r redan.", "no_bridges": "Inga deCONZ-bryggor uppt\u00e4cktes", - "not_deconz_bridge": "Inte en deCONZ-brygga", "updated_instance": "Uppdaterad deCONZ-instans med ny v\u00e4rdadress" }, "error": { diff --git a/homeassistant/components/deconz/translations/tr.json b/homeassistant/components/deconz/translations/tr.json index 77045776c98..8b57d284864 100644 --- a/homeassistant/components/deconz/translations/tr.json +++ b/homeassistant/components/deconz/translations/tr.json @@ -5,7 +5,6 @@ "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "no_bridges": "DeCONZ k\u00f6pr\u00fcs\u00fc bulunamad\u0131", "no_hardware_available": "deCONZ'a ba\u011fl\u0131 radyo donan\u0131m\u0131 yok", - "not_deconz_bridge": "deCONZ k\u00f6pr\u00fcs\u00fc de\u011fil", "updated_instance": "DeCONZ yeni ana bilgisayar adresiyle g\u00fcncelle\u015ftirildi" }, "error": { diff --git a/homeassistant/components/deconz/translations/uk.json b/homeassistant/components/deconz/translations/uk.json index 3b09a517385..5df5e6a6078 100644 --- a/homeassistant/components/deconz/translations/uk.json +++ b/homeassistant/components/deconz/translations/uk.json @@ -5,7 +5,6 @@ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", "no_bridges": "\u0428\u043b\u044e\u0437\u0438 deCONZ \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456.", "no_hardware_available": "\u0420\u0430\u0434\u0456\u043e\u043e\u0431\u043b\u0430\u0434\u043d\u0430\u043d\u043d\u044f \u043d\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e deCONZ.", - "not_deconz_bridge": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0454 \u0448\u043b\u044e\u0437\u043e\u043c deCONZ.", "updated_instance": "\u0410\u0434\u0440\u0435\u0441\u0443 \u0445\u043e\u0441\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043e." }, "error": { diff --git a/homeassistant/components/deconz/translations/zh-Hant.json b/homeassistant/components/deconz/translations/zh-Hant.json index d2e37ffa710..bd945ecc360 100644 --- a/homeassistant/components/deconz/translations/zh-Hant.json +++ b/homeassistant/components/deconz/translations/zh-Hant.json @@ -5,7 +5,6 @@ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "no_bridges": "\u672a\u767c\u73fe\u5230 deCONZ Bridfe", "no_hardware_available": "deCONZ \u6c92\u6709\u4efb\u4f55\u7121\u7dda\u96fb\u88dd\u7f6e\u9023\u7dda", - "not_deconz_bridge": "\u975e deCONZ Bridge \u88dd\u7f6e", "updated_instance": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0 deCONZ \u88dd\u7f6e" }, "error": { diff --git a/homeassistant/components/denonavr/translations/ca.json b/homeassistant/components/denonavr/translations/ca.json index 11065514ce4..ba857fceab1 100644 --- a/homeassistant/components/denonavr/translations/ca.json +++ b/homeassistant/components/denonavr/translations/ca.json @@ -13,8 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Confirma l'addici\u00f3 del receptor", - "title": "Receptors de xarxa AVR de Denon" + "description": "Confirma l'addici\u00f3 del receptor" }, "select": { "data": { @@ -29,9 +28,7 @@ }, "data_description": { "host": "Deixeu-ho en blanc per utilitzar descobriment autom\u00e0tic" - }, - "description": "Connecta el teu receptor, si no es configura l'adre\u00e7a IP, s'utilitza el descobriment autom\u00e0tic", - "title": "Receptors de xarxa AVR de Denon" + } } } }, @@ -44,8 +41,7 @@ "zone2": "Configura la Zona 2", "zone3": "Configura la Zona 3" }, - "description": "Especifica par\u00e0metres opcionals", - "title": "Receptors de xarxa AVR de Denon" + "description": "Especifica par\u00e0metres opcionals" } } } diff --git a/homeassistant/components/denonavr/translations/cs.json b/homeassistant/components/denonavr/translations/cs.json index 1c66dae10f0..0f385ce1675 100644 --- a/homeassistant/components/denonavr/translations/cs.json +++ b/homeassistant/components/denonavr/translations/cs.json @@ -13,8 +13,7 @@ "flow_title": "S\u00ed\u0165ov\u00fd p\u0159ij\u00edma\u010d Denon AVR: {name}", "step": { "confirm": { - "description": "Potvr\u010fte pros\u00edm p\u0159id\u00e1n\u00ed p\u0159ij\u00edma\u010de", - "title": "S\u00ed\u0165ov\u00e9 p\u0159ij\u00edma\u010de Denon AVR" + "description": "Potvr\u010fte pros\u00edm p\u0159id\u00e1n\u00ed p\u0159ij\u00edma\u010de" }, "user": { "data": { diff --git a/homeassistant/components/denonavr/translations/de.json b/homeassistant/components/denonavr/translations/de.json index 1c9a1a8ec95..e4df128612b 100644 --- a/homeassistant/components/denonavr/translations/de.json +++ b/homeassistant/components/denonavr/translations/de.json @@ -13,8 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Bitte best\u00e4tige das Hinzuf\u00fcgen des Receivers", - "title": "Denon AVR-Netzwerk-Receiver" + "description": "Bitte best\u00e4tige das Hinzuf\u00fcgen des Receivers" }, "select": { "data": { @@ -29,9 +28,7 @@ }, "data_description": { "host": "Leer lassen, um automatische Erkennung zu verwenden" - }, - "description": "Verbinde dich mit deinem Receiver, wenn die IP-Adresse nicht eingestellt ist, wird die automatische Erkennung verwendet", - "title": "Denon AVR-Netzwerk-Receiver" + } } } }, @@ -44,8 +41,7 @@ "zone2": "Zone 2 einrichten", "zone3": "Zone 3 einrichten" }, - "description": "Optionale Einstellungen festlegen", - "title": "Denon AVR-Netzwerk-Receiver" + "description": "Optionale Einstellungen festlegen" } } } diff --git a/homeassistant/components/denonavr/translations/el.json b/homeassistant/components/denonavr/translations/el.json index d176fd944d6..77cc89731f5 100644 --- a/homeassistant/components/denonavr/translations/el.json +++ b/homeassistant/components/denonavr/translations/el.json @@ -13,8 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "\u0395\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03b9\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03c4\u03bf\u03c5 \u03b4\u03ad\u03ba\u03c4\u03b7", - "title": "\u0394\u03ad\u03ba\u03c4\u03b5\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 Denon AVR" + "description": "\u0395\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03b9\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03c4\u03bf\u03c5 \u03b4\u03ad\u03ba\u03c4\u03b7" }, "select": { "data": { @@ -29,9 +28,7 @@ }, "data_description": { "host": "\u0391\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7" - }, - "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b4\u03ad\u03ba\u03c4\u03b7 \u03c3\u03b1\u03c2, \u03b5\u03ac\u03bd \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7.", - "title": "\u0394\u03ad\u03ba\u03c4\u03b5\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 Denon AVR" + } } } }, @@ -44,8 +41,7 @@ "zone2": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b6\u03ce\u03bd\u03b7\u03c2 2", "zone3": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b6\u03ce\u03bd\u03b7\u03c2 3" }, - "description": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ad\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2", - "title": "\u0394\u03ad\u03ba\u03c4\u03b5\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 Denon AVR" + "description": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ad\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2" } } } diff --git a/homeassistant/components/denonavr/translations/en.json b/homeassistant/components/denonavr/translations/en.json index 2d937856b1c..668e18aae6e 100644 --- a/homeassistant/components/denonavr/translations/en.json +++ b/homeassistant/components/denonavr/translations/en.json @@ -13,8 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Please confirm adding the receiver", - "title": "Denon AVR Network Receivers" + "description": "Please confirm adding the receiver" }, "select": { "data": { @@ -29,9 +28,7 @@ }, "data_description": { "host": "Leave blank to use auto-discovery" - }, - "description": "Connect to your receiver, if the IP address is not set, auto-discovery is used", - "title": "Denon AVR Network Receivers" + } } } }, @@ -44,8 +41,7 @@ "zone2": "Set up Zone 2", "zone3": "Set up Zone 3" }, - "description": "Specify optional settings", - "title": "Denon AVR Network Receivers" + "description": "Specify optional settings" } } } diff --git a/homeassistant/components/denonavr/translations/es-419.json b/homeassistant/components/denonavr/translations/es-419.json index e22b8feebd1..eb8e5698c09 100644 --- a/homeassistant/components/denonavr/translations/es-419.json +++ b/homeassistant/components/denonavr/translations/es-419.json @@ -11,8 +11,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Por favor, confirme la adici\u00f3n del receptor", - "title": "Receptores de red Denon AVR" + "description": "Por favor, confirme la adici\u00f3n del receptor" }, "select": { "data": { @@ -20,10 +19,6 @@ }, "description": "Vuelva a ejecutar la configuraci\u00f3n si desea conectar receptores adicionales", "title": "Seleccione el receptor que desea conectar" - }, - "user": { - "description": "Con\u00e9ctese a su receptor, si la direcci\u00f3n IP no est\u00e1 configurada, se usa el descubrimiento autom\u00e1tico", - "title": "Receptores de red Denon AVR" } } }, @@ -36,8 +31,7 @@ "zone2": "Configurar Zona 2", "zone3": "Configurar Zona 3" }, - "description": "Especificar configuraciones opcionales", - "title": "Receptores de red Denon AVR" + "description": "Especificar configuraciones opcionales" } } } diff --git a/homeassistant/components/denonavr/translations/es.json b/homeassistant/components/denonavr/translations/es.json index 33051047719..3cfe69aeac4 100644 --- a/homeassistant/components/denonavr/translations/es.json +++ b/homeassistant/components/denonavr/translations/es.json @@ -13,8 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Por favor confirma la adici\u00f3n del receptor", - "title": "Receptores AVR Denon en Red" + "description": "Por favor confirma la adici\u00f3n del receptor" }, "select": { "data": { @@ -26,9 +25,7 @@ "user": { "data": { "host": "Direcci\u00f3n IP" - }, - "description": "Con\u00e9ctar con tu receptor, si la direcci\u00f3n IP no est\u00e1 configurada, se utilizar\u00e1 la detecci\u00f3n autom\u00e1tica", - "title": "Receptores AVR Denon en Red" + } } } }, @@ -41,8 +38,7 @@ "zone2": "Configurar la Zona 2", "zone3": "Configurar la Zona 3" }, - "description": "Especificar configuraciones opcionales", - "title": "Receptores AVR Denon en Red" + "description": "Especificar configuraciones opcionales" } } } diff --git a/homeassistant/components/denonavr/translations/et.json b/homeassistant/components/denonavr/translations/et.json index f133aaf9dd7..490624212b5 100644 --- a/homeassistant/components/denonavr/translations/et.json +++ b/homeassistant/components/denonavr/translations/et.json @@ -13,8 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Palun kinnita vastuv\u00f5tja lisamine", - "title": "" + "description": "Palun kinnita vastuv\u00f5tja lisamine" }, "select": { "data": { @@ -29,9 +28,7 @@ }, "data_description": { "host": "Automaatse avastamise kasutamiseks j\u00e4ta v\u00e4li t\u00fchjaks." - }, - "description": "Kui IP-aadressi pole m\u00e4\u00e4ratud, kasutatakse automaatset avastamist", - "title": "" + } } } }, @@ -44,8 +41,7 @@ "zone2": "Seadista tsoon 2", "zone3": "Seadista tsoon 3" }, - "description": "Valikuliste s\u00e4tete m\u00e4\u00e4ramine", - "title": "" + "description": "Valikuliste s\u00e4tete m\u00e4\u00e4ramine" } } } diff --git a/homeassistant/components/denonavr/translations/fr.json b/homeassistant/components/denonavr/translations/fr.json index 27a72477164..3b3af580f1c 100644 --- a/homeassistant/components/denonavr/translations/fr.json +++ b/homeassistant/components/denonavr/translations/fr.json @@ -13,8 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Veuillez confirmer l'ajout du r\u00e9cepteur", - "title": "R\u00e9cepteurs r\u00e9seaux Denon AVR" + "description": "Veuillez confirmer l'ajout du r\u00e9cepteur" }, "select": { "data": { @@ -29,9 +28,7 @@ }, "data_description": { "host": "Laissez le champ vide pour utiliser la d\u00e9couverte automatique" - }, - "description": "Connectez-vous \u00e0 votre r\u00e9cepteur, si l'adresse IP n'est pas d\u00e9finie, la d\u00e9tection automatique est utilis\u00e9e", - "title": "R\u00e9cepteurs r\u00e9seaux Denon AVR" + } } } }, @@ -44,8 +41,7 @@ "zone2": "Configurer Zone 2", "zone3": "Configurer Zone 3" }, - "description": "Sp\u00e9cifiez les param\u00e8tres optionnels", - "title": "R\u00e9cepteurs r\u00e9seaux Denon AVR" + "description": "Sp\u00e9cifiez les param\u00e8tres optionnels" } } } diff --git a/homeassistant/components/denonavr/translations/hu.json b/homeassistant/components/denonavr/translations/hu.json index 6891d18a9c4..8c51c7e990f 100644 --- a/homeassistant/components/denonavr/translations/hu.json +++ b/homeassistant/components/denonavr/translations/hu.json @@ -13,8 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "K\u00e9rj\u00fck, er\u0151s\u00edtse meg a vev\u0151 hozz\u00e1ad\u00e1s\u00e1t", - "title": "Denon AVR h\u00e1l\u00f3zati vev\u0151k\u00e9sz\u00fcl\u00e9kek" + "description": "K\u00e9rj\u00fck, er\u0151s\u00edtse meg a vev\u0151 hozz\u00e1ad\u00e1s\u00e1t" }, "select": { "data": { @@ -29,9 +28,7 @@ }, "data_description": { "host": "Az automatikus felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz hagyja \u00fcresen" - }, - "description": "Csatlakozzon a vev\u0151h\u00f6z, ha az IP-c\u00edm nincs be\u00e1ll\u00edtva, az automatikus felder\u00edt\u00e9st haszn\u00e1lja", - "title": "Denon AVR h\u00e1l\u00f3zati vev\u0151k\u00e9sz\u00fcl\u00e9kek" + } } } }, @@ -44,8 +41,7 @@ "zone2": "\u00c1ll\u00edtsa be a 2. z\u00f3n\u00e1t", "zone3": "\u00c1ll\u00edtsa be a 3. z\u00f3n\u00e1t" }, - "description": "Adja meg az opcion\u00e1lis be\u00e1ll\u00edt\u00e1sokat", - "title": "Denon AVR h\u00e1l\u00f3zati vev\u0151k\u00e9sz\u00fcl\u00e9kek" + "description": "Adja meg az opcion\u00e1lis be\u00e1ll\u00edt\u00e1sokat" } } } diff --git a/homeassistant/components/denonavr/translations/id.json b/homeassistant/components/denonavr/translations/id.json index 50a28ef5447..5c20f9b377d 100644 --- a/homeassistant/components/denonavr/translations/id.json +++ b/homeassistant/components/denonavr/translations/id.json @@ -13,8 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Konfirmasikan penambahan Receiver", - "title": "Network Receiver Denon AVR" + "description": "Konfirmasikan penambahan Receiver" }, "select": { "data": { @@ -29,9 +28,7 @@ }, "data_description": { "host": "Kosongkan untuk menggunakan penemuan otomatis" - }, - "description": "Hubungkan ke Receiver Anda. Jika alamat IP tidak ditentukan, penemuan otomatis akan digunakan", - "title": "Network Receiver Denon AVR" + } } } }, @@ -44,8 +41,7 @@ "zone2": "Siapkan Zona 2", "zone3": "Siapkan Zona 3" }, - "description": "Tentukan pengaturan opsional", - "title": "Network Receiver Denon AVR" + "description": "Tentukan pengaturan opsional" } } } diff --git a/homeassistant/components/denonavr/translations/it.json b/homeassistant/components/denonavr/translations/it.json index 23fffd3ab44..142ff573880 100644 --- a/homeassistant/components/denonavr/translations/it.json +++ b/homeassistant/components/denonavr/translations/it.json @@ -13,8 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Conferma l'aggiunta del ricevitore", - "title": "Ricevitori di rete Denon AVR" + "description": "Conferma l'aggiunta del ricevitore" }, "select": { "data": { @@ -29,9 +28,7 @@ }, "data_description": { "host": "Lascia vuoto per usare il rilevamento automatico" - }, - "description": "Collega il ricevitore, se l'indirizzo IP non \u00e8 impostato, sar\u00e0 utilizzato il rilevamento automatico", - "title": "Ricevitori di rete Denon AVR" + } } } }, @@ -44,8 +41,7 @@ "zone2": "Configura la zona 2", "zone3": "Configura la zona 3" }, - "description": "Specificare le impostazioni opzionali", - "title": "Ricevitori di rete Denon AVR" + "description": "Specificare le impostazioni opzionali" } } } diff --git a/homeassistant/components/denonavr/translations/ja.json b/homeassistant/components/denonavr/translations/ja.json index cd3198b7e10..d81d5fab462 100644 --- a/homeassistant/components/denonavr/translations/ja.json +++ b/homeassistant/components/denonavr/translations/ja.json @@ -13,8 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "\u53d7\u4fe1\u6a5f\u306e\u8ffd\u52a0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", - "title": "\u30c7\u30ce\u30f3(Denon)AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc" + "description": "\u53d7\u4fe1\u6a5f\u306e\u8ffd\u52a0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044" }, "select": { "data": { @@ -29,9 +28,7 @@ }, "data_description": { "host": "\u81ea\u52d5\u691c\u51fa\u3092\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u307e\u3059" - }, - "description": "\u53d7\u4fe1\u6a5f\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002IP\u30a2\u30c9\u30ec\u30b9\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u81ea\u52d5\u691c\u51fa\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059", - "title": "\u30c7\u30ce\u30f3(Denon)AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc" + } } } }, @@ -44,8 +41,7 @@ "zone2": "\u30be\u30fc\u30f32\u306e\u8a2d\u5b9a", "zone3": "\u30be\u30fc\u30f33\u306e\u8a2d\u5b9a" }, - "description": "\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a\u306e\u6307\u5b9a", - "title": "\u30c7\u30ce\u30f3(Denon)AVR\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ec\u30b7\u30fc\u30d0\u30fc" + "description": "\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a\u306e\u6307\u5b9a" } } } diff --git a/homeassistant/components/denonavr/translations/ko.json b/homeassistant/components/denonavr/translations/ko.json index c0121a1e2ca..573011186aa 100644 --- a/homeassistant/components/denonavr/translations/ko.json +++ b/homeassistant/components/denonavr/translations/ko.json @@ -13,8 +13,7 @@ "flow_title": "Denon AVR \ub124\ud2b8\uc6cc\ud06c \ub9ac\uc2dc\ubc84: {name}", "step": { "confirm": { - "description": "\ub9ac\uc2dc\ubc84 \ucd94\uac00\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694", - "title": "Denon AVR \ub124\ud2b8\uc6cc\ud06c \ub9ac\uc2dc\ubc84" + "description": "\ub9ac\uc2dc\ubc84 \ucd94\uac00\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694" }, "select": { "data": { @@ -26,9 +25,7 @@ "user": { "data": { "host": "IP \uc8fc\uc18c" - }, - "description": "\ub9ac\uc2dc\ubc84\uc5d0 \uc5f0\uacb0\ud569\ub2c8\ub2e4. IP \uc8fc\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \uc790\ub3d9 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4", - "title": "Denon AVR \ub124\ud2b8\uc6cc\ud06c \ub9ac\uc2dc\ubc84" + } } } }, @@ -40,8 +37,7 @@ "zone2": "Zone 2 \uc124\uc815", "zone3": "Zone 3 \uc124\uc815" }, - "description": "\uc635\uc158 \uc124\uc815 \uc9c0\uc815", - "title": "Denon AVR \ub124\ud2b8\uc6cc\ud06c \ub9ac\uc2dc\ubc84" + "description": "\uc635\uc158 \uc124\uc815 \uc9c0\uc815" } } } diff --git a/homeassistant/components/denonavr/translations/lb.json b/homeassistant/components/denonavr/translations/lb.json index fd8ece3c0bc..021d1de1a8d 100644 --- a/homeassistant/components/denonavr/translations/lb.json +++ b/homeassistant/components/denonavr/translations/lb.json @@ -13,8 +13,7 @@ "flow_title": "Denon AVR Netzwierk Empf\u00e4nger: {name}", "step": { "confirm": { - "description": "Best\u00e4teg dob\u00e4isetzen vum Receiver", - "title": "Denon AVR Netzwierk Empf\u00e4nger" + "description": "Best\u00e4teg dob\u00e4isetzen vum Receiver" }, "select": { "data": { @@ -26,9 +25,7 @@ "user": { "data": { "host": "IP Adress" - }, - "description": "Mam Receiver verbannen, falls keng IP Adress uginn ass g\u00ebtt auto-discovery benotzt", - "title": "Denon AVR Netzwierk Empf\u00e4nger" + } } } }, @@ -40,8 +37,7 @@ "zone2": "Zone 2 ariichten", "zone3": "Zone 3 ariichten" }, - "description": "Optionell Astellungen uginn", - "title": "Denon AVR Netzwierk Empf\u00e4nger" + "description": "Optionell Astellungen uginn" } } } diff --git a/homeassistant/components/denonavr/translations/nl.json b/homeassistant/components/denonavr/translations/nl.json index f03895452df..f3683561686 100644 --- a/homeassistant/components/denonavr/translations/nl.json +++ b/homeassistant/components/denonavr/translations/nl.json @@ -13,8 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Bevestig het toevoegen van de ontvanger", - "title": "Denon AVR Network Receivers" + "description": "Bevestig het toevoegen van de ontvanger" }, "select": { "data": { @@ -29,9 +28,7 @@ }, "data_description": { "host": "Leeg laten om auto-discovery te gebruiken" - }, - "description": "Maak verbinding met uw ontvanger. Als het IP-adres niet is ingesteld, wordt automatische detectie gebruikt", - "title": "Denon AVR Netwerk Ontvangers" + } } } }, @@ -44,8 +41,7 @@ "zone2": "Stel Zone 2 in", "zone3": "Stel Zone 3 in" }, - "description": "Optionele instellingen opgeven", - "title": "Denon AVR Network Receivers" + "description": "Optionele instellingen opgeven" } } } diff --git a/homeassistant/components/denonavr/translations/no.json b/homeassistant/components/denonavr/translations/no.json index 333c55a44f7..46f23680812 100644 --- a/homeassistant/components/denonavr/translations/no.json +++ b/homeassistant/components/denonavr/translations/no.json @@ -13,8 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Bekreft at du legger til mottakeren", - "title": "Denon AVR nettverksmottakere" + "description": "Bekreft at du legger til mottakeren" }, "select": { "data": { @@ -29,9 +28,7 @@ }, "data_description": { "host": "La feltet st\u00e5 tomt hvis du vil bruke automatisk s\u00f8k" - }, - "description": "Koble til mottakeren, hvis IP-adressen ikke er angitt, brukes automatisk oppdagelse", - "title": "Denon AVR Network Receivers" + } } } }, @@ -44,8 +41,7 @@ "zone2": "Sett opp sone 2", "zone3": "Sett opp sone 3" }, - "description": "Spesifiser valgfrie innstillinger", - "title": "Denon AVR Network Receivers" + "description": "Spesifiser valgfrie innstillinger" } } } diff --git a/homeassistant/components/denonavr/translations/pl.json b/homeassistant/components/denonavr/translations/pl.json index 054371c6ba1..b82a6867dd8 100644 --- a/homeassistant/components/denonavr/translations/pl.json +++ b/homeassistant/components/denonavr/translations/pl.json @@ -13,8 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Prosz\u0119 potwierdzi\u0107 dodanie urz\u0105dzenia", - "title": "Denon AVR" + "description": "Prosz\u0119 potwierdzi\u0107 dodanie urz\u0105dzenia" }, "select": { "data": { @@ -29,9 +28,7 @@ }, "data_description": { "host": "Pozostaw puste, aby u\u017cy\u0107 automatycznego wykrywania" - }, - "description": "\u0141\u0105czenie z urz\u0105dzeniem, je\u015bli adres IP nie jest zdefiniowany, u\u017cywane jest automatyczne wykrywanie.", - "title": "Denon AVR" + } } } }, @@ -44,8 +41,7 @@ "zone2": "Konfiguracja Strefy 2", "zone3": "Konfiguracja Strefy 3" }, - "description": "Ustawienia opcjonalne", - "title": "Denon AVR" + "description": "Ustawienia opcjonalne" } } } diff --git a/homeassistant/components/denonavr/translations/pt-BR.json b/homeassistant/components/denonavr/translations/pt-BR.json index 2a716dacdca..558accaf777 100644 --- a/homeassistant/components/denonavr/translations/pt-BR.json +++ b/homeassistant/components/denonavr/translations/pt-BR.json @@ -13,8 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Confirme a adi\u00e7\u00e3o do receptor", - "title": "Receptores de rede Denon AVR" + "description": "Confirme a adi\u00e7\u00e3o do receptor" }, "select": { "data": { @@ -29,9 +28,7 @@ }, "data_description": { "host": "Deixe em branco para usar a descoberta autom\u00e1tica" - }, - "description": "Conecte-se ao seu receptor, se o endere\u00e7o IP n\u00e3o estiver definido, a descoberta autom\u00e1tica ser\u00e1 usada", - "title": "Receptores de rede Denon AVR" + } } } }, @@ -44,8 +41,7 @@ "zone2": "Configure a Zona 2", "zone3": "Configurar a Zona 3" }, - "description": "Especificar configura\u00e7\u00f5es opcionais", - "title": "Receptores de rede Denon AVR" + "description": "Especificar configura\u00e7\u00f5es opcionais" } } } diff --git a/homeassistant/components/denonavr/translations/ru.json b/homeassistant/components/denonavr/translations/ru.json index 1db49decaad..01f0d868897 100644 --- a/homeassistant/components/denonavr/translations/ru.json +++ b/homeassistant/components/denonavr/translations/ru.json @@ -13,8 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0440\u0435\u0441\u0438\u0432\u0435\u0440\u0430", - "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon" + "description": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0440\u0435\u0441\u0438\u0432\u0435\u0440\u0430" }, "select": { "data": { @@ -29,9 +28,7 @@ }, "data_description": { "host": "\u041e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435" - }, - "description": "\u0415\u0441\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d, \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435", - "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon" + } } } }, @@ -44,8 +41,7 @@ "zone2": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0437\u043e\u043d\u044b 2", "zone3": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0437\u043e\u043d\u044b 3" }, - "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": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon" + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" } } } diff --git a/homeassistant/components/denonavr/translations/tr.json b/homeassistant/components/denonavr/translations/tr.json index 046e535c621..fde01548e14 100644 --- a/homeassistant/components/denonavr/translations/tr.json +++ b/homeassistant/components/denonavr/translations/tr.json @@ -13,8 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "L\u00fctfen al\u0131c\u0131y\u0131 eklemeyi onaylay\u0131n", - "title": "Denon AVR A\u011f Al\u0131c\u0131lar\u0131" + "description": "L\u00fctfen al\u0131c\u0131y\u0131 eklemeyi onaylay\u0131n" }, "select": { "data": { @@ -29,9 +28,7 @@ }, "data_description": { "host": "Otomatik bulmay\u0131 kullanmak i\u00e7in bo\u015f b\u0131rak\u0131n" - }, - "description": "Al\u0131c\u0131n\u0131za ba\u011flan\u0131n, IP adresi ayarlanmazsa otomatik bulma kullan\u0131l\u0131r", - "title": "Denon AVR A\u011f Al\u0131c\u0131lar\u0131" + } } } }, @@ -44,8 +41,7 @@ "zone2": "B\u00f6lge 2'yi kurun", "zone3": "B\u00f6lge 3'\u00fc kurun" }, - "description": "\u0130ste\u011fe ba\u011fl\u0131 ayarlar\u0131 belirtin", - "title": "Denon AVR A\u011f Al\u0131c\u0131lar\u0131" + "description": "\u0130ste\u011fe ba\u011fl\u0131 ayarlar\u0131 belirtin" } } } diff --git a/homeassistant/components/denonavr/translations/uk.json b/homeassistant/components/denonavr/translations/uk.json index efb4cb41777..dcc68648fcc 100644 --- a/homeassistant/components/denonavr/translations/uk.json +++ b/homeassistant/components/denonavr/translations/uk.json @@ -13,8 +13,7 @@ "flow_title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon: {name}", "step": { "confirm": { - "description": "\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0456\u0442\u044c \u0434\u043e\u0434\u0430\u0432\u0430\u043d\u043d\u044f \u0440\u0435\u0441\u0438\u0432\u0435\u0440\u0430", - "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon" + "description": "\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0456\u0442\u044c \u0434\u043e\u0434\u0430\u0432\u0430\u043d\u043d\u044f \u0440\u0435\u0441\u0438\u0432\u0435\u0440\u0430" }, "select": { "data": { @@ -26,9 +25,7 @@ "user": { "data": { "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" - }, - "description": "\u042f\u043a\u0449\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u0432\u043a\u0430\u0437\u0430\u043d\u0430, \u0431\u0443\u0434\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f", - "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon" + } } } }, @@ -40,8 +37,7 @@ "zone2": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u043e\u043d\u0438 2", "zone3": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u043e\u043d\u0438 3" }, - "description": "\u0414\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f", - "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon" + "description": "\u0414\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f" } } } diff --git a/homeassistant/components/denonavr/translations/zh-Hant.json b/homeassistant/components/denonavr/translations/zh-Hant.json index 1217a6d7b87..7983a4f7972 100644 --- a/homeassistant/components/denonavr/translations/zh-Hant.json +++ b/homeassistant/components/denonavr/translations/zh-Hant.json @@ -13,8 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "\u8acb\u78ba\u8a8d\u65b0\u589e\u63a5\u6536\u5668", - "title": "Denon AVR \u7db2\u8def\u63a5\u6536\u5668" + "description": "\u8acb\u78ba\u8a8d\u65b0\u589e\u63a5\u6536\u5668" }, "select": { "data": { @@ -29,9 +28,7 @@ }, "data_description": { "host": "\u4fdd\u6301\u7a7a\u767d\u4ee5\u4f7f\u7528\u81ea\u52d5\u641c\u7d22" - }, - "description": "\u9023\u7dda\u81f3\u63a5\u6536\u5668\u3002\u5047\u5982\u672a\u8a2d\u5b9a IP \u4f4d\u5740\uff0c\u5c07\u4f7f\u7528\u81ea\u52d5\u63a2\u7d22\u3002", - "title": "Denon AVR \u7db2\u8def\u63a5\u6536\u5668" + } } } }, @@ -44,8 +41,7 @@ "zone2": "\u8a2d\u5b9a\u5340\u57df 2", "zone3": "\u8a2d\u5b9a\u5340\u57df 3" }, - "description": "\u6307\u5b9a\u9078\u9805\u8a2d\u5b9a", - "title": "Denon AVR \u7db2\u8def\u63a5\u6536\u5668" + "description": "\u6307\u5b9a\u9078\u9805\u8a2d\u5b9a" } } } diff --git a/homeassistant/components/derivative/translations/bg.json b/homeassistant/components/derivative/translations/bg.json index 14946d95fe0..b6c3577d1d0 100644 --- a/homeassistant/components/derivative/translations/bg.json +++ b/homeassistant/components/derivative/translations/bg.json @@ -14,11 +14,6 @@ "data": { "name": "\u0418\u043c\u0435" } - }, - "options": { - "data": { - "name": "\u0418\u043c\u0435" - } } } } diff --git a/homeassistant/components/derivative/translations/ca.json b/homeassistant/components/derivative/translations/ca.json index 3003b9349fd..9dff83bb746 100644 --- a/homeassistant/components/derivative/translations/ca.json +++ b/homeassistant/components/derivative/translations/ca.json @@ -36,22 +36,6 @@ "time_window": "Si s'estableix, el valor del sensor \u00e9s una mitjana m\u00f2bil ponderada en el temps de les derivades dins d'aquesta finestra.", "unit_prefix": "La sortida s'escalar\u00e0 segons el prefix m\u00e8tric i la unitat de temps de la derivada seleccionats." } - }, - "options": { - "data": { - "name": "Nom", - "round": "Precisi\u00f3", - "source": "Sensor d'entrada", - "time_window": "Finestra de temps", - "unit_prefix": "Prefix m\u00e8tric", - "unit_time": "Unitat de temps" - }, - "data_description": { - "round": "Controla el nombre de d\u00edgits decimals a la sortida.", - "time_window": "Si s'estableix, el valor del sensor \u00e9s una mitjana m\u00f2bil ponderada en el temps de les derivades dins d'aquesta finestra.", - "unit_prefix": "La sortida s'escalar\u00e0 segons el prefix m\u00e8tric i la unitat de temps de la derivada seleccionats." - }, - "description": "Crea un sensor que estima la derivada d'un altre sensor." } } }, diff --git a/homeassistant/components/derivative/translations/de.json b/homeassistant/components/derivative/translations/de.json index 4e1dbc1929c..1a23de37c25 100644 --- a/homeassistant/components/derivative/translations/de.json +++ b/homeassistant/components/derivative/translations/de.json @@ -36,22 +36,6 @@ "time_window": "Wenn gesetzt, ist der Sensorwert ein zeitgewichteter gleitender Durchschnitt von Ableitungen innerhalb dieses Fensters.", "unit_prefix": "Die Ausgabe wird gem\u00e4\u00df dem ausgew\u00e4hlten metrischen Pr\u00e4fix und der Zeiteinheit der Ableitung skaliert.." } - }, - "options": { - "data": { - "name": "Name", - "round": "Genauigkeit", - "source": "Eingangssensor", - "time_window": "Zeitfenster", - "unit_prefix": "Metrisches Pr\u00e4fix", - "unit_time": "Zeiteinheit" - }, - "data_description": { - "round": "Steuert die Anzahl der Dezimalstellen in der Ausgabe.", - "time_window": "Wenn gesetzt, ist der Sensorwert ein zeitgewichteter gleitender Durchschnitt von Ableitungen innerhalb dieses Fensters.", - "unit_prefix": "Die Ausgabe wird gem\u00e4\u00df dem ausgew\u00e4hlten metrischen Pr\u00e4fix und der Zeiteinheit der Ableitung skaliert.." - }, - "description": "Erstelle einen Sensor, der die Ableitung eines Sensors sch\u00e4tzt." } } }, diff --git a/homeassistant/components/derivative/translations/el.json b/homeassistant/components/derivative/translations/el.json index a5a19efc1e5..a0c55e93a5e 100644 --- a/homeassistant/components/derivative/translations/el.json +++ b/homeassistant/components/derivative/translations/el.json @@ -36,22 +36,6 @@ "time_window": "\u0395\u03ac\u03bd \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af, \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03ac \u03c3\u03c4\u03b1\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03b9\u03bd\u03b7\u03c4\u03cc\u03c2 \u03bc\u03ad\u03c3\u03bf\u03c2 \u03cc\u03c1\u03bf\u03c2 \u03c4\u03c9\u03bd \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03c9\u03bd \u03b5\u03bd\u03c4\u03cc\u03c2 \u03b1\u03c5\u03c4\u03bf\u03cd \u03c4\u03bf\u03c5 \u03c0\u03b1\u03c1\u03b1\u03b8\u03cd\u03c1\u03bf\u03c5.", "unit_prefix": "\u0397 \u03c0\u03b1\u03c1\u03ac\u03b3\u03c9\u03b3\u03bf\u03c2 \u03b8\u03b1 \u03ba\u03bb\u03b9\u03bc\u03b1\u03ba\u03c9\u03b8\u03b5\u03af \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5 \u03c4\u03b7\u03c2 \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03bf\u03c5." } - }, - "options": { - "data": { - "name": "\u038c\u03bd\u03bf\u03bc\u03b1", - "round": "\u0391\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1", - "source": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5", - "time_window": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03c0\u03b1\u03c1\u03ac\u03b8\u03c5\u03c1\u03bf", - "unit_prefix": "\u039c\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1", - "unit_time": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b1 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5" - }, - "data_description": { - "round": "\u0395\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf.", - "time_window": "\u0395\u03ac\u03bd \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af, \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03ac \u03c3\u03c4\u03b1\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03b9\u03bd\u03b7\u03c4\u03cc\u03c2 \u03bc\u03ad\u03c3\u03bf\u03c2 \u03cc\u03c1\u03bf\u03c2 \u03c4\u03c9\u03bd \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03c9\u03bd \u03b5\u03bd\u03c4\u03cc\u03c2 \u03b1\u03c5\u03c4\u03bf\u03cd \u03c4\u03bf\u03c5 \u03c0\u03b1\u03c1\u03b1\u03b8\u03cd\u03c1\u03bf\u03c5.", - "unit_prefix": "\u0397 \u03c0\u03b1\u03c1\u03ac\u03b3\u03c9\u03b3\u03bf\u03c2 \u03b8\u03b1 \u03ba\u03bb\u03b9\u03bc\u03b1\u03ba\u03c9\u03b8\u03b5\u03af \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5 \u03c4\u03b7\u03c2 \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03bf\u03c5." - }, - "description": "\u0397 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf.\n\u0395\u03ac\u03bd \u03c4\u03bf \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03c0\u03b1\u03c1\u03ac\u03b8\u03c5\u03c1\u03bf \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 0, \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03ac \u03c3\u03c4\u03b1\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03b9\u03bd\u03b7\u03c4\u03cc\u03c2 \u03bc\u03ad\u03c3\u03bf\u03c2 \u03cc\u03c1\u03bf\u03c2 \u03c4\u03c9\u03bd \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03c9\u03bd \u03b5\u03bd\u03c4\u03cc\u03c2 \u03c4\u03bf\u03c5 \u03c0\u03b1\u03c1\u03b1\u03b8\u03cd\u03c1\u03bf\u03c5.\n\u0397 \u03c0\u03b1\u03c1\u03ac\u03b3\u03c9\u03b3\u03bf\u03c2 \u03b8\u03b1 \u03ba\u03bb\u03b9\u03bc\u03b1\u03ba\u03ce\u03bd\u03b5\u03c4\u03b1\u03b9 \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5 \u03c4\u03b7\u03c2 \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03bf\u03c5." } } }, diff --git a/homeassistant/components/derivative/translations/en.json b/homeassistant/components/derivative/translations/en.json index 884f1fa5244..b91318b5237 100644 --- a/homeassistant/components/derivative/translations/en.json +++ b/homeassistant/components/derivative/translations/en.json @@ -36,22 +36,6 @@ "time_window": "If set, the sensor's value is a time weighted moving average of derivatives within this window.", "unit_prefix": "The output will be scaled according to the selected metric prefix and time unit of the derivative.." } - }, - "options": { - "data": { - "name": "Name", - "round": "Precision", - "source": "Input sensor", - "time_window": "Time window", - "unit_prefix": "Metric prefix", - "unit_time": "Time unit" - }, - "data_description": { - "round": "Controls the number of decimal digits in the output.", - "time_window": "If set, the sensor's value is a time weighted moving average of derivatives within this window.", - "unit_prefix": "The output will be scaled according to the selected metric prefix and time unit of the derivative.." - }, - "description": "Create a sensor that estimates the derivative of a sensor." } } }, diff --git a/homeassistant/components/derivative/translations/es.json b/homeassistant/components/derivative/translations/es.json index 114c2102678..e6df75ba4d6 100644 --- a/homeassistant/components/derivative/translations/es.json +++ b/homeassistant/components/derivative/translations/es.json @@ -27,15 +27,6 @@ "round": "Controla el n\u00famero de d\u00edgitos decimales en la salida.", "unit_prefix": "a salida se escalar\u00e1 seg\u00fan el prefijo m\u00e9trico y la unidad de tiempo de la derivada seleccionados." } - }, - "options": { - "data": { - "name": "Nombre", - "time_window": "Ventana de tiempo", - "unit_prefix": "Prefijo m\u00e9trico", - "unit_time": "Unidad de tiempo" - }, - "description": "Crea un sensor que ama la derivada de otro sensor." } } }, diff --git a/homeassistant/components/derivative/translations/et.json b/homeassistant/components/derivative/translations/et.json index 45c566fac9b..4c3af55a294 100644 --- a/homeassistant/components/derivative/translations/et.json +++ b/homeassistant/components/derivative/translations/et.json @@ -36,22 +36,6 @@ "time_window": "Kui see on m\u00e4\u00e4ratud on anduri v\u00e4\u00e4rtus selle akna tuletisinstrumentide ajaga kaalutud liikuv keskmine.", "unit_prefix": "Tuletis skaleeritakse vastavalt valitud meetrilisele eesliitele ja tuletise aja\u00fchikule." } - }, - "options": { - "data": { - "name": "Nimi", - "round": "T\u00e4psus", - "source": "Sisendandur", - "time_window": "Ajavahemik", - "unit_prefix": "M\u00f5\u00f5diku eesliide", - "unit_time": "Aja\u00fchik" - }, - "data_description": { - "round": "K\u00fcmnendkohtade arv v\u00e4ljundis.", - "time_window": "Kui see on m\u00e4\u00e4ratud, on anduri v\u00e4\u00e4rtus selle akna tuletisinstrumentide ajaga kaalutud liikuv keskmine.", - "unit_prefix": "Tuletis skaleeritakse vastavalt valitud meetrilisele eesliitele ja tuletise aja\u00fchikule." - }, - "description": "T\u00e4psus reguleerib k\u00fcmnendkohtade arvu v\u00e4ljundis.\n Kui ajaaken ei ole 0, on anduri v\u00e4\u00e4rtuseks aknas olevate tuletisinstrumentide ajaga kaalutud liikuv keskmine.\n Tuletist skaleeritakse vastavalt valitud meetrilise prefiksile ja tuletise aja\u00fchikule." } } }, diff --git a/homeassistant/components/derivative/translations/fr.json b/homeassistant/components/derivative/translations/fr.json index d967a3abc89..3716c6907b9 100644 --- a/homeassistant/components/derivative/translations/fr.json +++ b/homeassistant/components/derivative/translations/fr.json @@ -36,22 +36,6 @@ "time_window": "Si d\u00e9finie, la valeur du capteur est une moyenne mobile pond\u00e9r\u00e9e dans le temps des d\u00e9riv\u00e9es dans cette p\u00e9riode.", "unit_prefix": "La sortie sera mise \u00e0 l'\u00e9chelle en fonction du pr\u00e9fixe m\u00e9trique et de l'unit\u00e9 de temps de la d\u00e9riv\u00e9e s\u00e9lectionn\u00e9s.." } - }, - "options": { - "data": { - "name": "Nom", - "round": "Pr\u00e9cision", - "source": "Capteur d'entr\u00e9e", - "time_window": "P\u00e9riode", - "unit_prefix": "Pr\u00e9fixe m\u00e9trique", - "unit_time": "Unit\u00e9 de temps" - }, - "data_description": { - "round": "Contr\u00f4le le nombre de chiffres d\u00e9cimaux dans la sortie.", - "time_window": "Si d\u00e9finie, la valeur du capteur est une moyenne mobile pond\u00e9r\u00e9e dans le temps des d\u00e9riv\u00e9es dans cette p\u00e9riode.", - "unit_prefix": "La sortie sera mise \u00e0 l'\u00e9chelle en fonction du pr\u00e9fixe m\u00e9trique et de l'unit\u00e9 de temps de la d\u00e9riv\u00e9e s\u00e9lectionn\u00e9s." - }, - "description": "Cr\u00e9ez un capteur calculant la d\u00e9riv\u00e9e d'un autre capteur." } } }, diff --git a/homeassistant/components/derivative/translations/he.json b/homeassistant/components/derivative/translations/he.json deleted file mode 100644 index 317b836da6d..00000000000 --- a/homeassistant/components/derivative/translations/he.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "options": { - "step": { - "options": { - "data_description": { - "unit_prefix": "." - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/hu.json b/homeassistant/components/derivative/translations/hu.json index e71175d2a32..65c9a38a473 100644 --- a/homeassistant/components/derivative/translations/hu.json +++ b/homeassistant/components/derivative/translations/hu.json @@ -36,22 +36,6 @@ "time_window": "Ha be van \u00e1ll\u00edtva, az \u00e9rz\u00e9kel\u0151 \u00e9rt\u00e9ke az ablakon bel\u00fcli sz\u00e1rmaz\u00e9kok id\u0151vel s\u00falyozott mozg\u00f3\u00e1tlaga.", "unit_prefix": "A sz\u00e1rmaz\u00e9kos \u00e9rt\u00e9k a sz\u00e1rmaztatott term\u00e9k kiv\u00e1lasztott m\u00e9rt\u00e9kegys\u00e9g el\u0151tagja \u00e9s id\u0151egys\u00e9ge szerint lesz sk\u00e1l\u00e1zva.." } - }, - "options": { - "data": { - "name": "Elnevez\u00e9s", - "round": "Pontoss\u00e1g", - "source": "Forr\u00e1s \u00e9rz\u00e9kel\u0151", - "time_window": "Id\u0151ablak", - "unit_prefix": "M\u00e9rt\u00e9kegys\u00e9g el\u0151tag", - "unit_time": "Id\u0151egys\u00e9g" - }, - "data_description": { - "round": "Az eredm\u00e9ny tizedesjegyeinek sz\u00e1ma.", - "time_window": "Ha be van \u00e1ll\u00edtva, az \u00e9rz\u00e9kel\u0151 \u00e9rt\u00e9ke az ablakon bel\u00fcli sz\u00e1rmaz\u00e9kok id\u0151vel s\u00falyozott mozg\u00f3\u00e1tlaga.", - "unit_prefix": "A sz\u00e1rmaz\u00e9kos \u00e9rt\u00e9k a sz\u00e1rmaztatott term\u00e9k kiv\u00e1lasztott m\u00e9rt\u00e9kegys\u00e9g el\u0151tagja \u00e9s id\u0151egys\u00e9ge szerint lesz sk\u00e1l\u00e1zva.." - }, - "description": "Hozzon l\u00e9tre egy \u00e9rz\u00e9kel\u0151t, amely megbecs\u00fcli a forr\u00e1s \u00e9rz\u00e9kel\u0151 sz\u00e1rmaz\u00e9k\u00e1t." } } }, diff --git a/homeassistant/components/derivative/translations/id.json b/homeassistant/components/derivative/translations/id.json index 67d6752a182..0461824523a 100644 --- a/homeassistant/components/derivative/translations/id.json +++ b/homeassistant/components/derivative/translations/id.json @@ -36,22 +36,6 @@ "time_window": "Jika disetel, nilai sensor adalah rata-rata bergerak berbobot waktu dari turunan dalam jangka ini.", "unit_prefix": "Output akan diskalakan sesuai dengan prefiks metrik yang dipilih dan unit waktu turunan.." } - }, - "options": { - "data": { - "name": "Nama", - "round": "Presisi", - "source": "Sensor input", - "time_window": "Jangka waktu", - "unit_prefix": "Prefiks metrik", - "unit_time": "Unit waktu" - }, - "data_description": { - "round": "Mengontrol jumlah digit desimal dalam output.", - "time_window": "Jika disetel, nilai sensor adalah rata-rata bergerak berbobot waktu dari turunan dalam jangka ini.", - "unit_prefix": "Output akan diskalakan sesuai dengan prefiks metrik yang dipilih dan unit waktu turunan.." - }, - "description": "Buat sensor yang memperkirakan nilai turunan dari sebuah sensor." } } }, diff --git a/homeassistant/components/derivative/translations/it.json b/homeassistant/components/derivative/translations/it.json index ad888e13745..713f8334037 100644 --- a/homeassistant/components/derivative/translations/it.json +++ b/homeassistant/components/derivative/translations/it.json @@ -36,22 +36,6 @@ "time_window": "Se impostato, il valore del sensore \u00e8 una media mobile delle derivate ponderata nel tempo all'interno di questa finestra.", "unit_prefix": "L'output sar\u00e0 ridimensionato in base al prefisso metrico selezionato e all'unit\u00e0 di tempo della derivata.." } - }, - "options": { - "data": { - "name": "Nome", - "round": "Precisione", - "source": "Sensore di ingresso", - "time_window": "Finestra temporale", - "unit_prefix": "Prefisso metrico", - "unit_time": "Unit\u00e0 di tempo" - }, - "data_description": { - "round": "Controlla il numero di cifre decimali nell'uscita.", - "time_window": "Se impostato, il valore del sensore \u00e8 una media mobile delle derivate ponderata nel tempo all'interno di questa finestra.", - "unit_prefix": "L'output sar\u00e0 ridimensionato in base al prefisso metrico selezionato e all'unit\u00e0 di tempo della derivata.." - }, - "description": "Crea un sensore che stimi la derivata di un sensore." } } }, diff --git a/homeassistant/components/derivative/translations/ja.json b/homeassistant/components/derivative/translations/ja.json index 10232f996d6..c5939e95deb 100644 --- a/homeassistant/components/derivative/translations/ja.json +++ b/homeassistant/components/derivative/translations/ja.json @@ -36,22 +36,6 @@ "time_window": "\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u308b\u5834\u5408\u3001\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u306f\u3053\u306e\u30a6\u30a3\u30f3\u30c9\u30a6\u5185\u306e\u5fae\u5206\u306e\u6642\u9593\u52a0\u91cd\u79fb\u52d5\u5e73\u5747\u3068\u306a\u308a\u307e\u3059\u3002", "unit_prefix": "\u5fae\u5206\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u5fae\u5206\u306e\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002." } - }, - "options": { - "data": { - "name": "\u540d\u524d", - "round": "\u7cbe\u5ea6", - "source": "\u5165\u529b\u30bb\u30f3\u30b5\u30fc", - "time_window": "\u6642\u9593\u8ef8", - "unit_prefix": "\u30e1\u30c8\u30ea\u30c3\u30af\u63a5\u982d\u8f9e", - "unit_time": "\u6642\u9593\u5358\u4f4d" - }, - "data_description": { - "round": "\u51fa\u529b\u5024\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3002", - "time_window": "\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u308b\u5834\u5408\u3001\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u306f\u3053\u306e\u30a6\u30a3\u30f3\u30c9\u30a6\u5185\u306e\u5fae\u5206\u306e\u6642\u9593\u52a0\u91cd\u79fb\u52d5\u5e73\u5747\u3068\u306a\u308a\u307e\u3059\u3002", - "unit_prefix": "\u5fae\u5206\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u5fae\u5206\u306e\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002." - }, - "description": "\u7cbe\u5ea6\u306f\u3001\u51fa\u529b\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u6642\u9593\u7a93\u304c0\u3067\u306a\u3044\u5834\u5408\u3001\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u306f\u7a93\u5185\u306e\u5fae\u5206\u306e\u6642\u9593\u52a0\u91cd\u79fb\u52d5\u5e73\u5747\u306b\u306a\u308a\u307e\u3059\u3002\n\u5fae\u5206\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ea\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u5fae\u5206\u306e\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/derivative/translations/ko.json b/homeassistant/components/derivative/translations/ko.json new file mode 100644 index 00000000000..a45cb7d22e5 --- /dev/null +++ b/homeassistant/components/derivative/translations/ko.json @@ -0,0 +1,43 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\uc774\ub984", + "round": "\uc18c\uc218\uc810", + "source": "\uc785\ub825 \uc13c\uc11c", + "time_window": "\uc2dc\uac04\ub300", + "unit_prefix": "\ubbf8\ud130\ubc95", + "unit_time": "\uc2dc\uac04 \ub2e8\uc704" + }, + "data_description": { + "round": "\uc18c\uc218\uc810 \uc790\ub9bf\uc218\ub97c \ubcc0\uacbd\ud569\ub2c8\ub2e4.", + "time_window": "\uc124\uc815\ub41c \uacbd\uc6b0 \uc13c\uc11c\uc758 \uac12\uc740 \uc2dc\uac04\ub300 \ub0b4 \ub3c4\ud568\uc218\uc758 \uc2dc\uac04 \uac00\uc911 \uc774\ub3d9 \ud3c9\uade0\uc785\ub2c8\ub2e4.", + "unit_prefix": "\uc120\ud0dd\ud55c \ubbf8\ud130\ubc95 \ubc0f \ub3c4\ud568\uc218\uc758 \ub2e8\uc704\uc2dc\uac04\uc73c\ub85c \ud45c\uc2dc\ub429\ub2c8\ub2e4." + }, + "description": "\ub3c4\ud568\uc218\ub97c \uad6c\ud558\ub294 \uc13c\uc11c\ub97c \uc0dd\uc131\ud569\ub2c8\ub2e4.", + "title": "\ub3c4\ud568\uc218 \uc13c\uc11c \ucd94\uac00" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "\uc774\ub984", + "round": "\uc18c\uc218\uc810", + "source": "\uc785\ub825 \uc13c\uc11c", + "time_window": "\uc2dc\uac04\ub300", + "unit_prefix": "\ubbf8\ud130\ubc95", + "unit_time": "\uc2dc\uac04 \ub2e8\uc704" + }, + "data_description": { + "round": "\uc18c\uc218\uc810 \uc790\ub9bf\uc218\ub97c \ubcc0\uacbd\ud569\ub2c8\ub2e4.", + "time_window": "\uc124\uc815\ub41c \uacbd\uc6b0 \uc13c\uc11c\uc758 \uac12\uc740 \uc2dc\uac04\ub300 \ub0b4 \ub3c4\ud568\uc218\uc758 \uc2dc\uac04 \uac00\uc911 \uc774\ub3d9 \ud3c9\uade0\uc785\ub2c8\ub2e4.", + "unit_prefix": "\uc120\ud0dd\ud55c \ubbf8\ud130\ubc95 \ubc0f \ub3c4\ud568\uc218\uc758 \ub2e8\uc704\uc2dc\uac04\uc73c\ub85c \ud45c\uc2dc\ub429\ub2c8\ub2e4.." + } + } + } + }, + "title": "\ub3c4\ud568\uc218 \uc13c\uc11c" +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/nl.json b/homeassistant/components/derivative/translations/nl.json index aa8eadc214c..f3dc9c07de7 100644 --- a/homeassistant/components/derivative/translations/nl.json +++ b/homeassistant/components/derivative/translations/nl.json @@ -36,22 +36,6 @@ "time_window": "Indien ingesteld, is de waarde van de sensor een tijdgewogen voortschrijdend gemiddelde van de afgeleiden binnen dit venster.", "unit_prefix": "De uitvoer wordt geschaald volgens het geselecteerde metrische voorvoegsel en de tijdseenheid van de afgeleide." } - }, - "options": { - "data": { - "name": "Naam", - "round": "Nauwkeurigheid", - "source": "Invoersensor", - "time_window": "Tijdsvenster", - "unit_prefix": "Metrisch voorvoegsel", - "unit_time": "Tijdseenheid" - }, - "data_description": { - "round": "Regelt het aantal decimalen in de uitvoer.", - "time_window": "Indien ingesteld, is de waarde van de sensor een tijdgewogen voortschrijdend gemiddelde van de afgeleiden binnen dit venster.", - "unit_prefix": "De uitvoer wordt geschaald volgens het geselecteerde metrische voorvoegsel en de tijdseenheid van de afgeleide." - }, - "description": "Maak een sensor die de afgeleide van een sensor schat." } } }, diff --git a/homeassistant/components/derivative/translations/no.json b/homeassistant/components/derivative/translations/no.json index 735f1fae6ae..ca04f9d0d28 100644 --- a/homeassistant/components/derivative/translations/no.json +++ b/homeassistant/components/derivative/translations/no.json @@ -36,22 +36,6 @@ "time_window": "Hvis den er angitt, er sensorens verdi et tidsvektet glidende gjennomsnitt av derivater i dette vinduet.", "unit_prefix": "Utdataene skaleres i henhold til det valgte metriske prefikset og tidsenheten til den deriverte.." } - }, - "options": { - "data": { - "name": "Navn", - "round": "Presisjon", - "source": "Inngangssensor", - "time_window": "Tidsvindu", - "unit_prefix": "Metrisk prefiks", - "unit_time": "Tidsenhet" - }, - "data_description": { - "round": "Styrer antall desimaler i utdataene.", - "time_window": "Hvis den er angitt, er sensorens verdi et tidsvektet glidende gjennomsnitt av derivater i dette vinduet.", - "unit_prefix": "Utdataene skaleres i henhold til det valgte metriske prefikset og tidsenheten til den deriverte.." - }, - "description": "Lag en sensor som estimerer den deriverte av en sensor." } } }, diff --git a/homeassistant/components/derivative/translations/pl.json b/homeassistant/components/derivative/translations/pl.json index 041d52ffeff..3f97ff29c2c 100644 --- a/homeassistant/components/derivative/translations/pl.json +++ b/homeassistant/components/derivative/translations/pl.json @@ -36,22 +36,6 @@ "time_window": "Je\u015bli jest ustawiona, warto\u015b\u0107 sensora jest wa\u017con\u0105 w czasie \u015bredni\u0105 ruchom\u0105 pochodnych w tym oknie.", "unit_prefix": "Wynik b\u0119dzie skalowany zgodnie z wybranym prefiksem metrycznym i jednostk\u0105 czasu pochodnej." } - }, - "options": { - "data": { - "name": "Nazwa", - "round": "Precyzja", - "source": "Sensor wej\u015bciowy", - "time_window": "Okno czasowe", - "unit_prefix": "Prefiks metryczny", - "unit_time": "Jednostka czasu" - }, - "data_description": { - "round": "Kontroluje liczb\u0119 cyfr dziesi\u0119tnych w danych wyj\u015bciowych.", - "time_window": "Je\u015bli jest ustawiona, warto\u015b\u0107 sensora jest wa\u017con\u0105 w czasie \u015bredni\u0105 ruchom\u0105 pochodnych w tym oknie.", - "unit_prefix": "Wynik b\u0119dzie skalowany zgodnie z wybranym prefiksem metrycznym i jednostk\u0105 czasu pochodnej." - }, - "description": "Tworzy sensor, kt\u00f3ry szacuje pochodn\u0105 sensora." } } }, diff --git a/homeassistant/components/derivative/translations/pt-BR.json b/homeassistant/components/derivative/translations/pt-BR.json index 4d29a3970ee..54acbb3a5e5 100644 --- a/homeassistant/components/derivative/translations/pt-BR.json +++ b/homeassistant/components/derivative/translations/pt-BR.json @@ -36,22 +36,6 @@ "time_window": "Se definido, o valor do sensor \u00e9 uma m\u00e9dia m\u00f3vel ponderada no tempo das derivadas dentro desta janela.", "unit_prefix": "A sa\u00edda ser\u00e1 dimensionada de acordo com o prefixo m\u00e9trico selecionado e a unidade de tempo da derivada.." } - }, - "options": { - "data": { - "name": "Nome", - "round": "Precis\u00e3o", - "source": "Sensor de entrada", - "time_window": "Janela do tempo", - "unit_prefix": "Prefixo da m\u00e9trica", - "unit_time": "Unidade de tempo" - }, - "data_description": { - "round": "Controla o n\u00famero de d\u00edgitos decimais na sa\u00edda.", - "time_window": "Se definido, o valor do sensor \u00e9 uma m\u00e9dia m\u00f3vel ponderada no tempo das derivadas dentro desta janela.", - "unit_prefix": "A sa\u00edda ser\u00e1 dimensionada de acordo com o prefixo m\u00e9trico selecionado e a unidade de tempo da derivada.." - }, - "description": "Crie um sensor que estime a derivada de um sensor." } } }, diff --git a/homeassistant/components/derivative/translations/ru.json b/homeassistant/components/derivative/translations/ru.json index f3ff7cf0e83..6155d64301a 100644 --- a/homeassistant/components/derivative/translations/ru.json +++ b/homeassistant/components/derivative/translations/ru.json @@ -36,22 +36,6 @@ "time_window": "\u0415\u0441\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e, \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0432\u043d\u043e \u0432\u0437\u0432\u0435\u0448\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043a\u043e\u043b\u044c\u0437\u044f\u0449\u0435\u043c\u0443 \u0441\u0440\u0435\u0434\u043d\u0435\u043c\u0443 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u044b\u0445 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u044d\u0442\u043e\u0433\u043e \u043e\u043a\u043d\u0430.", "unit_prefix": "\u0414\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0438 \u0441 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u043c \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \u043c\u0435\u0442\u0440\u0438\u043a\u0438 \u0438 \u0435\u0434\u0438\u043d\u0438\u0446\u0435\u0439 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u043e\u0439.." } - }, - "options": { - "data": { - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "round": "\u041e\u043a\u0440\u0443\u0433\u043b\u0435\u043d\u0438\u0435", - "source": "\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440", - "time_window": "\u0412\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0435 \u043e\u043a\u043d\u043e", - "unit_prefix": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441 \u043c\u0435\u0442\u0440\u0438\u043a\u0438", - "unit_time": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438" - }, - "data_description": { - "round": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439.", - "time_window": "\u0415\u0441\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e, \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0432\u043d\u043e \u0432\u0437\u0432\u0435\u0448\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043a\u043e\u043b\u044c\u0437\u044f\u0449\u0435\u043c\u0443 \u0441\u0440\u0435\u0434\u043d\u0435\u043c\u0443 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u044b\u0445 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u044d\u0442\u043e\u0433\u043e \u043e\u043a\u043d\u0430.", - "unit_prefix": "\u0414\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0438 \u0441 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u043c \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \u043c\u0435\u0442\u0440\u0438\u043a\u0438 \u0438 \u0435\u0434\u0438\u043d\u0438\u0446\u0435\u0439 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u043e\u0439.." - }, - "description": "\u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u0441\u0435\u043d\u0441\u043e\u0440, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u0443\u044e \u0441\u0435\u043d\u0441\u043e\u0440\u0430." } } }, diff --git a/homeassistant/components/derivative/translations/tr.json b/homeassistant/components/derivative/translations/tr.json index 68016c74372..500a0007494 100644 --- a/homeassistant/components/derivative/translations/tr.json +++ b/homeassistant/components/derivative/translations/tr.json @@ -36,22 +36,6 @@ "time_window": "Ayarlan\u0131rsa, sens\u00f6r\u00fcn de\u011feri, bu penceredeki t\u00fcrevlerin zaman a\u011f\u0131rl\u0131kl\u0131 hareketli ortalamas\u0131d\u0131r.", "unit_prefix": "\u00c7\u0131kt\u0131, t\u00fcrevin se\u00e7ilen metrik \u00f6nekine ve zaman birimine g\u00f6re \u00f6l\u00e7eklenecektir.." } - }, - "options": { - "data": { - "name": "Ad", - "round": "Hassas", - "source": "Giri\u015f sens\u00f6r\u00fc", - "time_window": "Zaman penceresi", - "unit_prefix": "Metrik \u00f6neki", - "unit_time": "Zaman birimi" - }, - "data_description": { - "round": "\u00c7\u0131kt\u0131daki ondal\u0131k basamak say\u0131s\u0131n\u0131 kontrol eder.", - "time_window": "Ayarlan\u0131rsa, sens\u00f6r\u00fcn de\u011feri, bu penceredeki t\u00fcrevlerin zaman a\u011f\u0131rl\u0131kl\u0131 hareketli ortalamas\u0131d\u0131r.", - "unit_prefix": "\u00c7\u0131kt\u0131, t\u00fcrevin se\u00e7ilen metrik \u00f6nekine ve zaman birimine g\u00f6re \u00f6l\u00e7eklenecektir.." - }, - "description": "Bir sens\u00f6r\u00fcn t\u00fcrevini tahmin eden bir sens\u00f6r olu\u015fturun." } } }, diff --git a/homeassistant/components/derivative/translations/zh-Hans.json b/homeassistant/components/derivative/translations/zh-Hans.json index 689f057dec0..130e7292282 100644 --- a/homeassistant/components/derivative/translations/zh-Hans.json +++ b/homeassistant/components/derivative/translations/zh-Hans.json @@ -36,22 +36,6 @@ "time_window": "\u5982\u679c\u8bbe\u7f6e\uff0c\u4f20\u611f\u5668\u5c06\u8f93\u51fa\u6b64\u65f6\u95f4\u7a97\u53e3\u5185\u7684\u53d8\u5316\u7387\u6309\u7167\u201c\u65f6\u95f4\u52a0\u6743\u79fb\u52a8\u5e73\u5747\u6cd5\u201d\u5904\u7406\u540e\u7684\u503c\u3002", "unit_prefix": "\u8f93\u51fa\u503c\u5c06\u6839\u636e\u6240\u9009\u7684\u5355\u4f4d\u524d\u7f00\u548c\u65f6\u95f4\u5355\u4f4d\u8fdb\u884c\u7f29\u653e\u3002" } - }, - "options": { - "data": { - "name": "\u540d\u79f0", - "round": "\u7cbe\u5ea6", - "source": "\u8f93\u5165\u4f20\u611f\u5668", - "time_window": "\u65f6\u95f4\u7a97\u53e3", - "unit_prefix": "\u5355\u4f4d\u524d\u7f00", - "unit_time": "\u65f6\u95f4\u5355\u4f4d" - }, - "data_description": { - "round": "\u63a7\u5236\u8f93\u51fa\u7684\u5c0f\u6570\u4f4d\u6570\u3002", - "time_window": "\u5982\u679c\u8bbe\u7f6e\uff0c\u4f20\u611f\u5668\u5c06\u8f93\u51fa\u6b64\u65f6\u95f4\u7a97\u53e3\u5185\u7684\u53d8\u5316\u7387\u6309\u7167\u201c\u65f6\u95f4\u52a0\u6743\u79fb\u52a8\u5e73\u5747\u6cd5\u201d\u5904\u7406\u540e\u7684\u503c\u3002", - "unit_prefix": "\u8f93\u51fa\u503c\u5c06\u6839\u636e\u6240\u9009\u7684\u5355\u4f4d\u524d\u7f00\u548c\u65f6\u95f4\u5355\u4f4d\u8fdb\u884c\u7f29\u653e\u3002" - }, - "description": "\u521b\u5efa\u4f20\u611f\u5668\u6765\u4f30\u7b97\u53e6\u4e00\u4e2a\u4f20\u611f\u5668\u7684\u53d8\u5316\u7387\u3002" } } }, diff --git a/homeassistant/components/derivative/translations/zh-Hant.json b/homeassistant/components/derivative/translations/zh-Hant.json index 3c100df8034..11236b0ad63 100644 --- a/homeassistant/components/derivative/translations/zh-Hant.json +++ b/homeassistant/components/derivative/translations/zh-Hant.json @@ -36,22 +36,6 @@ "time_window": "\u8a2d\u5b9a\u5f8c\u3001\u611f\u6e2c\u5668\u6578\u503c\u5c07\u70ba\u8996\u7a97\u5167\u5c0e\u6578\u7684\u6642\u9593\u52a0\u6b0a\u52a0\u6b0a\u79fb\u52d5\u5e73\u5747\u503c\u3002", "unit_prefix": "\u8f38\u51fa\u5c07\u53d7\u6240\u9078\u64c7\u516c\u5236\u524d\u7db4\u53ca\u5c0e\u6578\u6642\u9593\u55ae\u4f4d\u800c\u8b8a\u5316\u3002." } - }, - "options": { - "data": { - "name": "\u540d\u7a31", - "round": "\u6e96\u78ba\u5ea6", - "source": "\u8f38\u5165\u611f\u6e2c\u5668", - "time_window": "\u6642\u9593\u8996\u7a97", - "unit_prefix": "\u516c\u5236\u524d\u7db4", - "unit_time": "\u6642\u9593\u55ae\u4f4d" - }, - "data_description": { - "round": "\u63a7\u5236\u8f38\u51fa\u4e2d\u7684\u5c0f\u6578\u4f4d\u6578\u3002", - "time_window": "\u8a2d\u5b9a\u5f8c\u3001\u611f\u6e2c\u5668\u6578\u503c\u5c07\u70ba\u8996\u7a97\u5167\u5c0e\u6578\u7684\u6642\u9593\u52a0\u6b0a\u52a0\u6b0a\u79fb\u52d5\u5e73\u5747\u503c\u3002", - "unit_prefix": "\u8f38\u51fa\u5c07\u53d7\u6240\u9078\u64c7\u516c\u5236\u524d\u7db4\u53ca\u5c0e\u6578\u6642\u9593\u55ae\u4f4d\u800c\u8b8a\u5316\u3002" - }, - "description": "\u65b0\u589e\u9810\u4f30\u611f\u6e2c\u5668\u5c0e\u6578\u4e4b\u611f\u6e2c\u5668\u3002" } } }, diff --git a/homeassistant/components/devolo_home_network/translations/ko.json b/homeassistant/components/devolo_home_network/translations/ko.json new file mode 100644 index 00000000000..758f3336cd4 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/ko.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/bg.json b/homeassistant/components/dlna_dmr/translations/bg.json index 00e64e1568d..4227d0b68f6 100644 --- a/homeassistant/components/dlna_dmr/translations/bg.json +++ b/homeassistant/components/dlna_dmr/translations/bg.json @@ -3,13 +3,11 @@ "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" + "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": { @@ -23,8 +21,7 @@ }, "user": { "data": { - "host": "\u0425\u043e\u0441\u0442", - "url": "URL" + "host": "\u0425\u043e\u0441\u0442" }, "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/ca.json b/homeassistant/components/dlna_dmr/translations/ca.json index 7c528d5ad29..0bbdc0b3de8 100644 --- a/homeassistant/components/dlna_dmr/translations/ca.json +++ b/homeassistant/components/dlna_dmr/translations/ca.json @@ -4,7 +4,6 @@ "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", "non_unique_id": "S'han trobat diversos dispositius amb el mateix identificador \u00fanic", @@ -12,7 +11,6 @@ }, "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 compatible" }, "flow_title": "{name}", @@ -32,8 +30,7 @@ }, "user": { "data": { - "host": "Amfitri\u00f3", - "url": "URL" + "host": "Amfitri\u00f3" }, "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/cs.json b/homeassistant/components/dlna_dmr/translations/cs.json index 85c9a831dda..c9087b82ab7 100644 --- a/homeassistant/components/dlna_dmr/translations/cs.json +++ b/homeassistant/components/dlna_dmr/translations/cs.json @@ -7,11 +7,6 @@ "step": { "confirm": { "description": "Chcete za\u010d\u00edt nastavovat?" - }, - "user": { - "data": { - "url": "URL" - } } } } diff --git a/homeassistant/components/dlna_dmr/translations/de.json b/homeassistant/components/dlna_dmr/translations/de.json index baf1a53efca..64cae60c13e 100644 --- a/homeassistant/components/dlna_dmr/translations/de.json +++ b/homeassistant/components/dlna_dmr/translations/de.json @@ -4,7 +4,6 @@ "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", "non_unique_id": "Mehrere Ger\u00e4te mit derselben eindeutigen ID gefunden", @@ -12,7 +11,6 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "could_not_connect": "Verbindung zum DLNA-Ger\u00e4t fehlgeschlagen", "not_dmr": "Ger\u00e4t ist kein unterst\u00fctzter Digital Media Renderer" }, "flow_title": "{name}", @@ -32,8 +30,7 @@ }, "user": { "data": { - "host": "Host", - "url": "URL" + "host": "Host" }, "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/el.json b/homeassistant/components/dlna_dmr/translations/el.json index 5dc56ef3e37..884e1c51cb2 100644 --- a/homeassistant/components/dlna_dmr/translations/el.json +++ b/homeassistant/components/dlna_dmr/translations/el.json @@ -4,7 +4,6 @@ "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "alternative_integration": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03ba\u03b1\u03bb\u03cd\u03c4\u03b5\u03c1\u03b1 \u03b1\u03c0\u03cc \u03ac\u03bb\u03bb\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "could_not_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03bc\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae DLNA", "discovery_error": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2 \u03bc\u03b9\u03b1\u03c2 \u03b1\u03bd\u03c4\u03af\u03c3\u03c4\u03bf\u03b9\u03c7\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 DLNA", "incomplete_config": "\u0391\u03c0\u03cc \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bb\u03b5\u03af\u03c0\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7 \u03bc\u03b5\u03c4\u03b1\u03b2\u03bb\u03b7\u03c4\u03ae", "non_unique_id": "\u0392\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ad\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03bc\u03b5 \u03c4\u03bf \u03af\u03b4\u03b9\u03bf \u03bc\u03bf\u03bd\u03b1\u03b4\u03b9\u03ba\u03cc \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc", @@ -12,7 +11,6 @@ }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "could_not_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03bc\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae DLNA", "not_dmr": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf\u03c2 \u03c8\u03b7\u03c6\u03b9\u03b1\u03ba\u03cc\u03c2 \u03b1\u03bd\u03b1\u03bc\u03b5\u03c4\u03b1\u03b4\u03cc\u03c4\u03b7\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd" }, "flow_title": "{name}", @@ -32,8 +30,7 @@ }, "user": { "data": { - "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", - "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ae \u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b5\u03bd\u03ae \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 DLNA DMR" diff --git a/homeassistant/components/dlna_dmr/translations/en.json b/homeassistant/components/dlna_dmr/translations/en.json index 51cbd875211..153f3f892d0 100644 --- a/homeassistant/components/dlna_dmr/translations/en.json +++ b/homeassistant/components/dlna_dmr/translations/en.json @@ -4,7 +4,6 @@ "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", @@ -12,7 +11,6 @@ }, "error": { "cannot_connect": "Failed to connect", - "could_not_connect": "Failed to connect to DLNA device", "not_dmr": "Device is not a supported Digital Media Renderer" }, "flow_title": "{name}", @@ -32,8 +30,7 @@ }, "user": { "data": { - "host": "Host", - "url": "URL" + "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/dlna_dmr/translations/es-419.json b/homeassistant/components/dlna_dmr/translations/es-419.json index 3dff7685122..14f0a98c68f 100644 --- a/homeassistant/components/dlna_dmr/translations/es-419.json +++ b/homeassistant/components/dlna_dmr/translations/es-419.json @@ -2,14 +2,12 @@ "config": { "abort": { "alternative_integration": "El dispositivo es mejor compatible con otra integraci\u00f3n", - "could_not_connect": "No se pudo conectar al dispositivo DLNA", "discovery_error": "Error al descubrir un dispositivo DLNA coincidente", "incomplete_config": "A la configuraci\u00f3n le falta una variable requerida", "non_unique_id": "Varios dispositivos encontrados con la misma ID \u00fanica", "not_dmr": "El dispositivo no es un renderizador de medios digitales compatible" }, "error": { - "could_not_connect": "No se pudo conectar al dispositivo DLNA", "not_dmr": "El dispositivo no es un renderizador de medios digitales compatible" }, "flow_title": "{name}", diff --git a/homeassistant/components/dlna_dmr/translations/es.json b/homeassistant/components/dlna_dmr/translations/es.json index 6878a05b766..6bb9dd83dbd 100644 --- a/homeassistant/components/dlna_dmr/translations/es.json +++ b/homeassistant/components/dlna_dmr/translations/es.json @@ -4,7 +4,6 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "alternative_integration": "Dispositivo compatible con otra integraci\u00f3n", "cannot_connect": "No se pudo conectar", - "could_not_connect": "No se pudo conectar al dispositivo DLNA", "discovery_error": "No se ha podido descubrir un dispositivo DLNA coincidente", "incomplete_config": "A la configuraci\u00f3n le falta una variable necesaria", "non_unique_id": "Se han encontrado varios dispositivos con el mismo ID \u00fanico", @@ -12,7 +11,6 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "could_not_connect": "No se pudo conectar al dispositivo DLNA", "not_dmr": "El dispositivo no es un procesador de medios digitales compatible" }, "flow_title": "{name}", @@ -32,8 +30,7 @@ }, "user": { "data": { - "host": "Host", - "url": "URL" + "host": "Host" }, "description": "Elija un dispositivo para configurar o d\u00e9jelo en blanco para introducir una URL", "title": "Dispositivos DLNA DMR descubiertos" diff --git a/homeassistant/components/dlna_dmr/translations/et.json b/homeassistant/components/dlna_dmr/translations/et.json index 1981b6eefde..adf665e4d2e 100644 --- a/homeassistant/components/dlna_dmr/translations/et.json +++ b/homeassistant/components/dlna_dmr/translations/et.json @@ -4,7 +4,6 @@ "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", "non_unique_id": "Leiti mitu sama unikaalse ID-ga seadet", @@ -12,7 +11,6 @@ }, "error": { "cannot_connect": "\u00dchendamine nurjus", - "could_not_connect": "DLNA seadmega \u00fchenduse loomine nurjus", "not_dmr": "Seade ei ole toetatud digitaalne meediumiedastusseade" }, "flow_title": "{name}", @@ -32,8 +30,7 @@ }, "user": { "data": { - "host": "Host", - "url": "URL" + "host": "Host" }, "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/fr.json b/homeassistant/components/dlna_dmr/translations/fr.json index 87686251c4d..9d3ed1e9f0a 100644 --- a/homeassistant/components/dlna_dmr/translations/fr.json +++ b/homeassistant/components/dlna_dmr/translations/fr.json @@ -4,7 +4,6 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "alternative_integration": "L'appareil est mieux pris en charge par une autre int\u00e9gration", "cannot_connect": "\u00c9chec de connexion", - "could_not_connect": "\u00c9chec de la connexion \u00e0 l'appareil DLNA", "discovery_error": "\u00c9chec de la d\u00e9couverte d'un appareil DLNA correspondant", "incomplete_config": "Il manque une variable requise dans la configuration", "non_unique_id": "Plusieurs appareils trouv\u00e9s avec le m\u00eame identifiant unique", @@ -12,7 +11,6 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "could_not_connect": "\u00c9chec de la connexion \u00e0 l'appareil DLNA", "not_dmr": "L'appareil n'est pas un moteur de rendu multim\u00e9dia num\u00e9rique pris en charge" }, "flow_title": "{name}", @@ -32,8 +30,7 @@ }, "user": { "data": { - "host": "H\u00f4te", - "url": "URL" + "host": "H\u00f4te" }, "description": "Choisissez un appareil \u00e0 configurer ou laissez vide pour saisir une URL", "title": "Appareils DLNA DMR d\u00e9couverts" diff --git a/homeassistant/components/dlna_dmr/translations/he.json b/homeassistant/components/dlna_dmr/translations/he.json index 7025721fa43..145b91f2646 100644 --- a/homeassistant/components/dlna_dmr/translations/he.json +++ b/homeassistant/components/dlna_dmr/translations/he.json @@ -19,8 +19,7 @@ }, "user": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7", - "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8" + "host": "\u05de\u05d0\u05e8\u05d7" } } } diff --git a/homeassistant/components/dlna_dmr/translations/hu.json b/homeassistant/components/dlna_dmr/translations/hu.json index 2773cf2bae4..e030d89530f 100644 --- a/homeassistant/components/dlna_dmr/translations/hu.json +++ b/homeassistant/components/dlna_dmr/translations/hu.json @@ -4,7 +4,6 @@ "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", "non_unique_id": "T\u00f6bb eszk\u00f6z tal\u00e1lhat\u00f3 ugyanazzal az egyedi azonos\u00edt\u00f3val", @@ -12,7 +11,6 @@ }, "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" }, "flow_title": "{name}", @@ -32,8 +30,7 @@ }, "user": { "data": { - "host": "C\u00edm", - "url": "URL" + "host": "C\u00edm" }, "description": "V\u00e1lassza ki a konfigur\u00e1lni k\u00edv\u00e1nt eszk\u00f6zt, vagy hagyja \u00fcresen az URL-c\u00edm k\u00e9zi megad\u00e1s\u00e1hoz", "title": "DLNA digit\u00e1lis m\u00e9dia renderel\u0151" diff --git a/homeassistant/components/dlna_dmr/translations/id.json b/homeassistant/components/dlna_dmr/translations/id.json index 415ab7dc653..ee35acf0d5a 100644 --- a/homeassistant/components/dlna_dmr/translations/id.json +++ b/homeassistant/components/dlna_dmr/translations/id.json @@ -4,7 +4,6 @@ "already_configured": "Perangkat sudah dikonfigurasi", "alternative_integration": "Perangkat dapat didukung lebih baik lewat integrasi lainnya", "cannot_connect": "Gagal terhubung", - "could_not_connect": "Gagal terhubung ke perangkat DLNA", "discovery_error": "Gagal menemukan perangkat DLNA yang cocok", "incomplete_config": "Konfigurasi tidak memiliki variabel yang diperlukan", "non_unique_id": "Beberapa perangkat ditemukan dengan ID unik yang sama", @@ -12,7 +11,6 @@ }, "error": { "cannot_connect": "Gagal terhubung", - "could_not_connect": "Gagal terhubung ke perangkat DLNA", "not_dmr": "Perangkat bukan Digital Media Renderer yang didukung" }, "flow_title": "{name}", @@ -32,8 +30,7 @@ }, "user": { "data": { - "host": "Host", - "url": "URL" + "host": "Host" }, "description": "Pilih perangkat untuk dikonfigurasi atau biarkan kosong untuk memasukkan URL", "title": "Perangkat DLNA DMR yang ditemukan" diff --git a/homeassistant/components/dlna_dmr/translations/it.json b/homeassistant/components/dlna_dmr/translations/it.json index f9e3e11d5f0..e9b725de2d3 100644 --- a/homeassistant/components/dlna_dmr/translations/it.json +++ b/homeassistant/components/dlna_dmr/translations/it.json @@ -4,7 +4,6 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "alternative_integration": "Il dispositivo \u00e8 meglio supportato da un'altra integrazione", "cannot_connect": "Impossibile connettersi", - "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", @@ -12,7 +11,6 @@ }, "error": { "cannot_connect": "Impossibile connettersi", - "could_not_connect": "Impossibile connettersi al dispositivo DLNA", "not_dmr": "Il dispositivo non \u00e8 un Digital Media Renderer supportato" }, "flow_title": "{name}", @@ -32,8 +30,7 @@ }, "user": { "data": { - "host": "Host", - "url": "URL" + "host": "Host" }, "description": "Scegli un dispositivo da configurare o lascia vuoto per inserire un URL", "title": "Rilevati dispositivi DLNA DMR" diff --git a/homeassistant/components/dlna_dmr/translations/ja.json b/homeassistant/components/dlna_dmr/translations/ja.json index 4c71d57b468..73e242dbb36 100644 --- a/homeassistant/components/dlna_dmr/translations/ja.json +++ b/homeassistant/components/dlna_dmr/translations/ja.json @@ -4,7 +4,6 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "alternative_integration": "\u30c7\u30d0\u30a4\u30b9\u306f\u5225\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3001\u3088\u308a\u9069\u5207\u306b\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "could_not_connect": "DLNA\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "discovery_error": "\u4e00\u81f4\u3059\u308bDLNA \u30c7\u30d0\u30a4\u30b9\u3092\u691c\u51fa\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", "incomplete_config": "\u8a2d\u5b9a\u306b\u5fc5\u8981\u306a\u5909\u6570\u304c\u3042\u308a\u307e\u305b\u3093", "non_unique_id": "\u540c\u4e00\u306eID\u3067\u8907\u6570\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u691c\u51fa\u3055\u308c\u307e\u3057\u305f", @@ -12,7 +11,6 @@ }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "could_not_connect": "DLNA\u30c7\u30d0\u30a4\u30b9\u3078\u306e\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "not_dmr": "\u30c7\u30d0\u30a4\u30b9\u304c\u3001\u672a\u30b5\u30dd\u30fc\u30c8\u306aDigital Media Renderer\u3067\u3059" }, "flow_title": "{name}", @@ -32,8 +30,7 @@ }, "user": { "data": { - "host": "\u30db\u30b9\u30c8", - "url": "URL" + "host": "\u30db\u30b9\u30c8" }, "description": "\u8a2d\u5b9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3059\u308b\u304b\u3001\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u3066URL\u3092\u5165\u529b\u3057\u307e\u3059\u3002", "title": "\u767a\u898b\u3055\u308c\u305fDLNA DMR\u6a5f\u5668" diff --git a/homeassistant/components/dlna_dmr/translations/ko.json b/homeassistant/components/dlna_dmr/translations/ko.json new file mode 100644 index 00000000000..c3c5f1a74b2 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/ko.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "alternative_integration": "\uae30\uae30\uac00 \ub2e4\ub978 \ud1b5\ud569\uad6c\uc131\uc694\uc18c\uc5d0\uc11c \ub354 \uc798 \uc9c0\uc6d0\ub429\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + }, + "manual": { + "description": "\uc7a5\uce58 \uc124\uba85 XML \ud30c\uc77c\uc758 URL" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "browse_unfiltered": "\ud638\ud658\ub418\uc9c0 \uc54a\ub294 \ubbf8\ub514\uc5b4 \ud45c\uc2dc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/nl.json b/homeassistant/components/dlna_dmr/translations/nl.json index 6147fac2007..360d24fd590 100644 --- a/homeassistant/components/dlna_dmr/translations/nl.json +++ b/homeassistant/components/dlna_dmr/translations/nl.json @@ -4,7 +4,6 @@ "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", "non_unique_id": "Meerdere apparaten gevonden met hetzelfde unieke ID", @@ -12,7 +11,6 @@ }, "error": { "cannot_connect": "Kan geen verbinding maken", - "could_not_connect": "Mislukt om te verbinden met DNLA apparaat", "not_dmr": "Apparaat is een niet-ondersteund Digital Media Renderer" }, "flow_title": "{name}", @@ -32,8 +30,7 @@ }, "user": { "data": { - "host": "Host", - "url": "URL" + "host": "Host" }, "description": "Kies een apparaat om te configureren of laat leeg om een URL in te voeren", "title": "Ontdekt DLNA Digital Media Renderer" diff --git a/homeassistant/components/dlna_dmr/translations/no.json b/homeassistant/components/dlna_dmr/translations/no.json index 04fed89ca78..a1f4fb9b7cc 100644 --- a/homeassistant/components/dlna_dmr/translations/no.json +++ b/homeassistant/components/dlna_dmr/translations/no.json @@ -4,7 +4,6 @@ "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", "non_unique_id": "Flere enheter ble funnet med samme unike ID", @@ -12,7 +11,6 @@ }, "error": { "cannot_connect": "Tilkobling mislyktes", - "could_not_connect": "Kunne ikke koble til DLNA -enhet", "not_dmr": "Enheten er ikke en st\u00f8ttet Digital Media Renderer" }, "flow_title": "{name}", @@ -32,8 +30,7 @@ }, "user": { "data": { - "host": "Vert", - "url": "URL" + "host": "Vert" }, "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 23f46c84c38..dae2b58e1c9 100644 --- a/homeassistant/components/dlna_dmr/translations/pl.json +++ b/homeassistant/components/dlna_dmr/translations/pl.json @@ -4,7 +4,6 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "alternative_integration": "Urz\u0105dzenie jest lepiej obs\u0142ugiwane przez inn\u0105 integracj\u0119", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "could_not_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem DLNA", "discovery_error": "Nie uda\u0142o si\u0119 wykry\u0107 pasuj\u0105cego urz\u0105dzenia DLNA", "incomplete_config": "W konfiguracji brakuje wymaganej zmiennej", "non_unique_id": "Znaleziono wiele urz\u0105dze\u0144 z tym samym unikalnym identyfikatorem", @@ -12,7 +11,6 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "could_not_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem DLNA", "not_dmr": "Urz\u0105dzenie nie jest obs\u0142ugiwanym rendererem multimedi\u00f3w cyfrowych (DMR)" }, "flow_title": "{name}", @@ -32,8 +30,7 @@ }, "user": { "data": { - "host": "Nazwa hosta lub adres IP", - "url": "URL" + "host": "Nazwa hosta lub adres IP" }, "description": "Wybierz urz\u0105dzenie do skonfigurowania lub pozostaw puste, aby wprowadzi\u0107 adres URL", "title": "Wykryto urz\u0105dzenia DLNA DMR" diff --git a/homeassistant/components/dlna_dmr/translations/pt-BR.json b/homeassistant/components/dlna_dmr/translations/pt-BR.json index 5caf64af4b1..f5f8370e52c 100644 --- a/homeassistant/components/dlna_dmr/translations/pt-BR.json +++ b/homeassistant/components/dlna_dmr/translations/pt-BR.json @@ -4,7 +4,6 @@ "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "alternative_integration": "O dispositivo \u00e9 melhor suportado por outra integra\u00e7\u00e3o", "cannot_connect": "Falha ao conectar", - "could_not_connect": "Falha ao conectar ao dispositivo DLNA", "discovery_error": "Falha ao descobrir um dispositivo DLNA correspondente", "incomplete_config": "A configura\u00e7\u00e3o n\u00e3o tem uma vari\u00e1vel obrigat\u00f3ria", "non_unique_id": "V\u00e1rios dispositivos encontrados com o mesmo ID exclusivo", @@ -12,7 +11,6 @@ }, "error": { "cannot_connect": "Falha ao conectar", - "could_not_connect": "Falha ao conectar-se ao dispositivo DLNA", "not_dmr": "O dispositivo n\u00e3o \u00e9 um renderizador de m\u00eddia digital compat\u00edvel" }, "flow_title": "{name}", @@ -32,8 +30,7 @@ }, "user": { "data": { - "host": "Nome do host", - "url": "URL" + "host": "Nome do host" }, "description": "Escolha um dispositivo para configurar ou deixe em branco para inserir um URL", "title": "Dispositivos DMR DLNA descobertos" diff --git a/homeassistant/components/dlna_dmr/translations/ru.json b/homeassistant/components/dlna_dmr/translations/ru.json index 09c98173b3b..7028b754d5d 100644 --- a/homeassistant/components/dlna_dmr/translations/ru.json +++ b/homeassistant/components/dlna_dmr/translations/ru.json @@ -4,7 +4,6 @@ "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.", "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.", @@ -12,7 +11,6 @@ }, "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 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 Digital Media Renderer." }, "flow_title": "{name}", @@ -32,8 +30,7 @@ }, "user": { "data": { - "host": "\u0425\u043e\u0441\u0442", - "url": "URL-\u0430\u0434\u0440\u0435\u0441" + "host": "\u0425\u043e\u0441\u0442" }, "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/tr.json b/homeassistant/components/dlna_dmr/translations/tr.json index bee4a922a75..26d911056b7 100644 --- a/homeassistant/components/dlna_dmr/translations/tr.json +++ b/homeassistant/components/dlna_dmr/translations/tr.json @@ -4,7 +4,6 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "alternative_integration": "Cihaz ba\u015fka bir entegrasyon taraf\u0131ndan daha iyi destekleniyor", "cannot_connect": "Ba\u011flanma hatas\u0131", - "could_not_connect": "DLNA cihaz\u0131na ba\u011flan\u0131lamad\u0131", "discovery_error": "E\u015fle\u015fen bir DLNA cihaz\u0131 bulunamad\u0131", "incomplete_config": "Yap\u0131land\u0131rmada gerekli bir de\u011fi\u015fken eksik", "non_unique_id": "Ayn\u0131 benzersiz kimli\u011fe sahip birden fazla cihaz bulundu", @@ -12,7 +11,6 @@ }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", - "could_not_connect": "DLNA cihaz\u0131na ba\u011flan\u0131lamad\u0131", "not_dmr": "Cihaz, desteklenen bir Dijital Medya Olu\u015fturucu de\u011fil" }, "flow_title": "{name}", @@ -32,8 +30,7 @@ }, "user": { "data": { - "host": "Sunucu", - "url": "URL" + "host": "Sunucu" }, "description": "Yap\u0131land\u0131rmak i\u00e7in bir cihaz se\u00e7in veya bir URL girmek i\u00e7in bo\u015f b\u0131rak\u0131n", "title": "Ke\u015ffedilen DLNA DMR cihazlar\u0131" diff --git a/homeassistant/components/dlna_dmr/translations/zh-Hans.json b/homeassistant/components/dlna_dmr/translations/zh-Hans.json index 2046f1c2a47..16bd75d28bf 100644 --- a/homeassistant/components/dlna_dmr/translations/zh-Hans.json +++ b/homeassistant/components/dlna_dmr/translations/zh-Hans.json @@ -4,7 +4,6 @@ "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86", "alternative_integration": "\u8be5\u8bbe\u5907\u5728\u53e6\u4e00\u96c6\u6210\u80fd\u63d0\u4f9b\u66f4\u597d\u7684\u652f\u6301", "cannot_connect": "\u8fde\u63a5\u5931\u8d25", - "could_not_connect": "\u8fde\u63a5 DLNA \u8bbe\u5907\u5931\u8d25", "discovery_error": "\u672a\u53d1\u73b0\u53ef\u7528\u7684 DLNA \u8bbe\u5907", "incomplete_config": "\u914d\u7f6e\u7f3a\u5c11\u5fc5\u8981\u7684\u53d8\u91cf\u4fe1\u606f", "non_unique_id": "\u53d1\u73b0\u591a\u53f0\u8bbe\u5907\u5177\u6709\u76f8\u540c\u7684 unique ID", @@ -12,7 +11,6 @@ }, "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25", - "could_not_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230 DLNA \u8bbe\u5907", "not_dmr": "\u8be5\u8bbe\u5907\u4e0d\u662f\u53d7\u652f\u6301\u7684\u6570\u5b57\u5a92\u4f53\u6e32\u67d3\u5668\uff08DMR\uff09" }, "flow_title": "{name}", @@ -32,8 +30,7 @@ }, "user": { "data": { - "host": "\u4e3b\u673a", - "url": "\u7f51\u5740" + "host": "\u4e3b\u673a" }, "title": "\u53d1\u73b0 DLNA DMR \u8bbe\u5907" } diff --git a/homeassistant/components/dlna_dmr/translations/zh-Hant.json b/homeassistant/components/dlna_dmr/translations/zh-Hant.json index c75334df1ae..07293607685 100644 --- a/homeassistant/components/dlna_dmr/translations/zh-Hant.json +++ b/homeassistant/components/dlna_dmr/translations/zh-Hant.json @@ -4,7 +4,6 @@ "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\u641c\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", @@ -12,7 +11,6 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "could_not_connect": "DLNA \u88dd\u7f6e\u9023\u7dda\u5931\u6557\u3002", "not_dmr": "\u88dd\u7f6e\u70ba\u975e\u652f\u63f4 Digital Media Renderer" }, "flow_title": "{name}", @@ -32,8 +30,7 @@ }, "user": { "data": { - "host": "\u4e3b\u6a5f\u7aef", - "url": "\u7db2\u5740" + "host": "\u4e3b\u6a5f\u7aef" }, "description": "\u9078\u64c7\u88dd\u7f6e\u9032\u884c\u8a2d\u5b9a\u6216\u4fdd\u7559\u7a7a\u767d\u4ee5\u8f38\u5165 URL", "title": "\u5df2\u767c\u73fe\u7684 DLNA DMR \u88dd\u7f6e" diff --git a/homeassistant/components/dlna_dms/translations/ko.json b/homeassistant/components/dlna_dms/translations/ko.json new file mode 100644 index 00000000000..20ad990e862 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/ko.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/ca.json b/homeassistant/components/doorbird/translations/ca.json index 0049709caa3..14c5f5f452d 100644 --- a/homeassistant/components/doorbird/translations/ca.json +++ b/homeassistant/components/doorbird/translations/ca.json @@ -18,8 +18,7 @@ "name": "Nom del dispositiu", "password": "[%key::common::config_flow::data::password%]", "username": "[%key::common::config_flow::data::username%]" - }, - "title": "Connexi\u00f3 amb DoorBird" + } } } }, @@ -31,8 +30,7 @@ }, "data_description": { "events": "Afegeix el/s noms del/s esdeveniment/s que vulguis seguir separats per comes. Despr\u00e9s d'introduir-los, utilitza l'aplicaci\u00f3 de DoorBird per assignar-los a un esdeveniment espec\u00edfic.\n\nExemple: algu_ha_premut_el_boto, moviment" - }, - "description": "Afegeix el/s noms del/s esdeveniment/s que vulguis seguir separats per comes. Despr\u00e9s d'introduir-los, utilitza l'aplicaci\u00f3 de DoorBird per assignar-los a un esdeveniment espec\u00edfic. Consulta la documentaci\u00f3 a https://www.home-assistant.io/integrations/doorbird/#events.\nExemple: algu_ha_premut_el_boto, moviment_detectat" + } } } } diff --git a/homeassistant/components/doorbird/translations/cs.json b/homeassistant/components/doorbird/translations/cs.json index fea0647ec85..2b09c0648d0 100644 --- a/homeassistant/components/doorbird/translations/cs.json +++ b/homeassistant/components/doorbird/translations/cs.json @@ -18,8 +18,7 @@ "name": "Jm\u00e9no za\u0159\u00edzen\u00ed", "password": "Heslo", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - }, - "title": "P\u0159ipojen\u00ed k DoorBird" + } } } }, @@ -28,8 +27,7 @@ "init": { "data": { "events": "Seznam ud\u00e1lost\u00ed odd\u011blen\u00fdch \u010d\u00e1rkami." - }, - "description": "Zadejte n\u00e1zvy ud\u00e1lost\u00ed odd\u011blen\u00e9 \u010d\u00e1rkou, kter\u00e9 chcete sledovat. Po jejich zad\u00e1n\u00ed je pomoc\u00ed aplikace DoorBird p\u0159i\u0159a\u010fte ke konkr\u00e9tn\u00ed ud\u00e1losti. Viz dokumentace na https://www.home-assistant.io/integrations/doorbird/#events. P\u0159\u00edklad: nekdo_stiskl_tlacitko, pohyb" + } } } } diff --git a/homeassistant/components/doorbird/translations/de.json b/homeassistant/components/doorbird/translations/de.json index 7a60a3bf4ae..9689b0bc728 100644 --- a/homeassistant/components/doorbird/translations/de.json +++ b/homeassistant/components/doorbird/translations/de.json @@ -18,8 +18,7 @@ "name": "Ger\u00e4tename", "password": "Passwort", "username": "Benutzername" - }, - "title": "Stelle eine Verbindung zu DoorBird her" + } } } }, @@ -31,8 +30,7 @@ }, "data_description": { "events": "F\u00fcge f\u00fcr jedes Ereignis, das du verfolgen m\u00f6chtest, einen durch Komma getrennten Ereignisnamen hinzu. Nachdem du sie hier eingegeben hast, verwende die DoorBird-App, um sie einem bestimmten Ereignis zuzuordnen.\n\nBeispiel: jemand_drueckte_den_Knopf, bewegung" - }, - "description": "F\u00fcge f\u00fcr jedes Ereignis, das du verfolgen m\u00f6chtest, einen durch Kommas getrennten Ereignisnamen hinzu. Nachdem du sie hier eingegeben hast, verwende die DoorBird-App, um sie einem bestimmten Ereignis zuzuweisen. Weitere Informationen findest du in der Dokumentation unter https://www.home-assistant.io/integrations/doorbird/#events. Beispiel: jemand_hat_den_knopf_gedr\u00fcckt, bewegung" + } } } } diff --git a/homeassistant/components/doorbird/translations/el.json b/homeassistant/components/doorbird/translations/el.json index da06dc3e0be..1805e57e70a 100644 --- a/homeassistant/components/doorbird/translations/el.json +++ b/homeassistant/components/doorbird/translations/el.json @@ -18,8 +18,7 @@ "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" - }, - "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf DoorBird" + } } } }, @@ -31,8 +30,7 @@ }, "data_description": { "events": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03bf\u03c2 \u03c0\u03bf\u03c5 \u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1 \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03b5\u03c4\u03b5. \u0391\u03c6\u03bf\u03cd \u03c4\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03b5\u03b4\u03ce, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae DoorBird \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c4\u03b1 \u03b1\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03b9\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03bf \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd.\n\n\u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: somebody_pressed_the_button, motion" - }, - "description": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03bf\u03c2 \u03c0\u03bf\u03c5 \u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1 \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03b5\u03c4\u03b5. \u0391\u03c6\u03bf\u03cd \u03c4\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03b5\u03b4\u03ce, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae DoorBird \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c4\u03b1 \u03b1\u03bd\u03c4\u03b9\u03c3\u03c4\u03bf\u03b9\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03bf \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://www.home-assistant.io/integrations/doorbird/#events. \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: somebody_pressed_the_button, motion" + } } } } diff --git a/homeassistant/components/doorbird/translations/en.json b/homeassistant/components/doorbird/translations/en.json index ee005dfbfed..b5df9f89257 100644 --- a/homeassistant/components/doorbird/translations/en.json +++ b/homeassistant/components/doorbird/translations/en.json @@ -18,8 +18,7 @@ "name": "Device Name", "password": "Password", "username": "Username" - }, - "title": "Connect to the DoorBird" + } } } }, @@ -31,8 +30,7 @@ }, "data_description": { "events": "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event.\n\nExample: somebody_pressed_the_button, motion" - }, - "description": "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event. See the documentation at https://www.home-assistant.io/integrations/doorbird/#events. Example: somebody_pressed_the_button, motion" + } } } } diff --git a/homeassistant/components/doorbird/translations/es-419.json b/homeassistant/components/doorbird/translations/es-419.json index 6b893b93014..9eed0664e00 100644 --- a/homeassistant/components/doorbird/translations/es-419.json +++ b/homeassistant/components/doorbird/translations/es-419.json @@ -14,8 +14,7 @@ "data": { "host": "Host (direcci\u00f3n IP)", "name": "Nombre del dispositivo" - }, - "title": "Conectar con DoorBird" + } } } }, @@ -24,8 +23,7 @@ "init": { "data": { "events": "Lista de eventos separados por comas." - }, - "description": "Agregue un nombre de evento separado por comas para cada evento que desee rastrear. Despu\u00e9s de ingresarlos aqu\u00ed, use la aplicaci\u00f3n DoorBird para asignarlos a un evento espec\u00edfico. Consulte la documentaci\u00f3n en https://www.home-assistant.io/integrations/doorbird/#events. Ejemplo: somebody_pressed_the_button, motion" + } } } } diff --git a/homeassistant/components/doorbird/translations/es.json b/homeassistant/components/doorbird/translations/es.json index f7e2ba8d92c..68de2419d2a 100644 --- a/homeassistant/components/doorbird/translations/es.json +++ b/homeassistant/components/doorbird/translations/es.json @@ -18,8 +18,7 @@ "name": "Nombre del dispositivo", "password": "Contrase\u00f1a", "username": "Usuario" - }, - "title": "Conectar con DoorBird" + } } } }, @@ -28,8 +27,7 @@ "init": { "data": { "events": "Lista de eventos separados por comas." - }, - "description": "A\u00f1ade un nombre de evento separado por comas para cada evento del que deseas realizar un seguimiento. Despu\u00e9s de introducirlos aqu\u00ed, utiliza la aplicaci\u00f3n DoorBird para asignarlos a un evento espec\u00edfico. Consulta la documentaci\u00f3n en https://www.home-assistant.io/integrations/doorbird/#events. Ejemplo: somebody_pressed_the_button, motion" + } } } } diff --git a/homeassistant/components/doorbird/translations/et.json b/homeassistant/components/doorbird/translations/et.json index f37d5d0244b..01601861ffc 100644 --- a/homeassistant/components/doorbird/translations/et.json +++ b/homeassistant/components/doorbird/translations/et.json @@ -18,8 +18,7 @@ "name": "Seadme nimi", "password": "Salas\u00f5na", "username": "Kasutajanimi" - }, - "title": "Loo \u00fchendus DoorBird-ga" + } } } }, @@ -31,8 +30,7 @@ }, "data_description": { "events": "Lisa iga s\u00fcndmuse jaoks, mida soovid j\u00e4lgida, komaga eraldatud s\u00fcndmuse nimi. P\u00e4rast nende sisestamist siia kasuta DoorBirdi rakendust, et m\u00e4\u00e4rata need konkreetsele s\u00fcndmusele.\n\nN\u00e4ide: somebody_pressed_the_button, liikumine" - }, - "description": "Lisa komaga eraldatud s\u00fcndmuse nimi igale j\u00e4lgitavale s\u00fcndmusele. P\u00e4rast nende sisestamist kasuta rakendust DoorBird, et siduda need konkreetse s\u00fcndmusega. Vaata dokumentatsiooni aadressil https://www.home-assistant.io/integrations/doorbird/#events. N\u00e4ide: somebody_pressed_the_button, motion" + } } } } diff --git a/homeassistant/components/doorbird/translations/fr.json b/homeassistant/components/doorbird/translations/fr.json index b4819897366..873204b0ef0 100644 --- a/homeassistant/components/doorbird/translations/fr.json +++ b/homeassistant/components/doorbird/translations/fr.json @@ -18,8 +18,7 @@ "name": "Nom de l'appareil", "password": "Mot de passe", "username": "Nom d'utilisateur" - }, - "title": "Connectez-vous au DoorBird" + } } } }, @@ -31,8 +30,7 @@ }, "data_description": { "events": "Ajoutez les noms des \u00e9v\u00e9nements que vous souhaitez suivre, s\u00e9par\u00e9s par des virgules. Apr\u00e8s les avoir saisis ici, utilisez l'application DoorBird pour les affecter \u00e0 un \u00e9v\u00e9nement sp\u00e9cifique.\n\nExemple\u00a0: somebody_pressed_the_button, motion" - }, - "description": "Ajoutez un nom d'\u00e9v\u00e9nement s\u00e9par\u00e9 par des virgules pour chaque \u00e9v\u00e9nement que vous souhaitez suivre. Apr\u00e8s les avoir saisis ici, utilisez l'application DoorBird pour les affecter \u00e0 un \u00e9v\u00e9nement sp\u00e9cifique. Consultez la documentation sur https://www.home-assistant.io/integrations/doorbird/#events. Exemple: somebody_pressed_the_button, motion" + } } } } diff --git a/homeassistant/components/doorbird/translations/hu.json b/homeassistant/components/doorbird/translations/hu.json index 51bbb0864dd..0062ffd46ef 100644 --- a/homeassistant/components/doorbird/translations/hu.json +++ b/homeassistant/components/doorbird/translations/hu.json @@ -18,8 +18,7 @@ "name": "Eszk\u00f6z neve", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - }, - "title": "Csatlakozzon a DoorBird-hez" + } } } }, @@ -31,8 +30,7 @@ }, "data_description": { "events": "Adjon hozz\u00e1 egy vessz\u0151vel elv\u00e1lasztott esem\u00e9nynevet minden egyes esem\u00e9nyhez, amelyet nyomon k\u00edv\u00e1n k\u00f6vetni. Miut\u00e1n be\u00edrta \u0151ket ide, a DoorBird alkalmaz\u00e1ssal rendelje \u0151ket egy adott esem\u00e9nyhez.\n\nP\u00e9lda: valaki_megnyomta_a_gombot, motion" - }, - "description": "Adjon hozz\u00e1 vessz\u0151vel elv\u00e1lasztott esem\u00e9nynevet minden k\u00f6vetni k\u00edv\u00e1nt esem\u00e9nyhez. Miut\u00e1n itt megadta \u0151ket, haszn\u00e1lja a DoorBird alkalmaz\u00e1st, hogy hozz\u00e1rendelje \u0151ket egy adott esem\u00e9nyhez. Tekintse meg a dokument\u00e1ci\u00f3t a https://www.home-assistant.io/integrations/doorbird/#events c\u00edmen. P\u00e9lda: valaki_pr\u00e9selt_gomb, mozg\u00e1s" + } } } } diff --git a/homeassistant/components/doorbird/translations/id.json b/homeassistant/components/doorbird/translations/id.json index c5d5733457d..e9155a8315f 100644 --- a/homeassistant/components/doorbird/translations/id.json +++ b/homeassistant/components/doorbird/translations/id.json @@ -18,8 +18,7 @@ "name": "Nama Perangkat", "password": "Kata Sandi", "username": "Nama Pengguna" - }, - "title": "Hubungkan ke DoorBird" + } } } }, @@ -31,8 +30,7 @@ }, "data_description": { "events": "Tambahkan nama event yang dipisahkan koma untuk setiap event yang ingin dilacak. Setelah memasukkannya di sini, gunakan aplikasi DoorBird untuk menetapkannya ke event tertentu.\n\nMisalnya: ada_yang_menekan_tombol, gerakan" - }, - "description": "Tambahkan nama event yang dipisahkan koma untuk setiap event yang ingin dilacak. Setelah memasukkannya di sini, gunakan aplikasi DoorBird untuk menetapkannya ke event tertentu. Baca dokumentasi di https://www.home-assistant.io/integrations/doorbird/#events. Contoh: somebody_pressed_the_button, motion" + } } } } diff --git a/homeassistant/components/doorbird/translations/it.json b/homeassistant/components/doorbird/translations/it.json index 9864f100954..4922ed6bc48 100644 --- a/homeassistant/components/doorbird/translations/it.json +++ b/homeassistant/components/doorbird/translations/it.json @@ -18,8 +18,7 @@ "name": "Nome del dispositivo", "password": "Password", "username": "Nome utente" - }, - "title": "Connettiti a DoorBird" + } } } }, @@ -31,8 +30,7 @@ }, "data_description": { "events": "Aggiungi un nome evento separato da virgole per ogni evento che desideri monitorare. Dopo averli inseriti qui, usa l'applicazione DoorBird per assegnarli a un evento specifico. \n\nEsempio: somebody_pressed_the_button, movimento" - }, - "description": "Aggiungere un nome di evento separato da virgola per ogni evento che desideri monitorare. Dopo averli inseriti qui, usa l'applicazione DoorBird per assegnarli a un evento specifico. Consulta la documentazione su https://www.home-assistant.io/integrations/doorbird/#events. Esempio: qualcuno_premuto_il_pulsante, movimento" + } } } } diff --git a/homeassistant/components/doorbird/translations/ja.json b/homeassistant/components/doorbird/translations/ja.json index 55363310ec5..e47c1b21e5e 100644 --- a/homeassistant/components/doorbird/translations/ja.json +++ b/homeassistant/components/doorbird/translations/ja.json @@ -18,8 +18,7 @@ "name": "\u30c7\u30d0\u30a4\u30b9\u540d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - }, - "title": "DoorBird\u306b\u63a5\u7d9a" + } } } }, @@ -31,8 +30,7 @@ }, "data_description": { "events": "\u8ffd\u8de1\u3059\u308b\u30a4\u30d9\u30f3\u30c8\u3054\u3068\u306b\u30b3\u30f3\u30de\u533a\u5207\u308a\u306e\u30a4\u30d9\u30f3\u30c8\u540d\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002\u3053\u3053\u306b\u5165\u529b\u5f8c\u3001DoorBird\u30a2\u30d7\u30ea\u3092\u4f7f\u7528\u3057\u3066\u7279\u5b9a\u306e\u30a4\u30d9\u30f3\u30c8\u306b\u5272\u308a\u5f53\u3066\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002 \n\n\u4f8b: somebody_pressed_the_button, motion" - }, - "description": "\u8ffd\u8de1\u3059\u308b\u30a4\u30d9\u30f3\u30c8\u3054\u3068\u306b\u3001\u30b3\u30f3\u30de\u533a\u5207\u308a\u3067\u30a4\u30d9\u30f3\u30c8\u540d\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002\u3053\u3053\u306b\u5165\u529b\u3057\u305f\u5f8c\u3001DoorBird\u30a2\u30d7\u30ea\u3092\u4f7f\u7528\u3057\u3066\u7279\u5b9a\u306e\u30a4\u30d9\u30f3\u30c8\u306b\u5272\u308a\u5f53\u3066\u307e\u3059\u3002https://www.home-assistant.io/integrations/doorbird/#events. \u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u4f8b: somebody_pressed_the_button, motion" + } } } } diff --git a/homeassistant/components/doorbird/translations/ko.json b/homeassistant/components/doorbird/translations/ko.json index 85d00317c2d..7406536982e 100644 --- a/homeassistant/components/doorbird/translations/ko.json +++ b/homeassistant/components/doorbird/translations/ko.json @@ -18,8 +18,7 @@ "name": "\uae30\uae30 \uc774\ub984", "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - }, - "title": "DoorBird\uc5d0 \uc5f0\uacb0\ud558\uae30" + } } } }, @@ -28,8 +27,7 @@ "init": { "data": { "events": "\uc27c\ud45c\ub85c \uad6c\ubd84\ub41c \uc774\ubca4\ud2b8 \ubaa9\ub85d." - }, - "description": "\ucd94\uc801\ud558\ub824\ub294 \uac01 \uc774\ubca4\ud2b8\uc5d0 \ub300\ud574 \uc27c\ud45c\ub85c \uad6c\ubd84\ub41c \uc774\ubca4\ud2b8 \uc774\ub984\uc744 \ucd94\uac00\ud574\uc8fc\uc138\uc694. \uc5ec\uae30\uc5d0 \uc785\ub825\ud55c \ud6c4 DoorBird \uc571\uc744 \uc0ac\uc6a9\ud558\uc5ec \ud2b9\uc815 \uc774\ubca4\ud2b8\uc5d0 \ud560\ub2f9\ud574\uc8fc\uc138\uc694. \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 https://www.home-assistant.io/integrations/doorbird/#event \uc124\uba85\uc11c\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694. \uc608: somebody_pressed_the_button, motion" + } } } } diff --git a/homeassistant/components/doorbird/translations/lb.json b/homeassistant/components/doorbird/translations/lb.json index c7cb7234fea..1e10d04ba19 100644 --- a/homeassistant/components/doorbird/translations/lb.json +++ b/homeassistant/components/doorbird/translations/lb.json @@ -18,8 +18,7 @@ "name": "Numm vum Apparat", "password": "Passwuert", "username": "Benotzernumm" - }, - "title": "Mat DoorBird verbannen" + } } } }, @@ -28,8 +27,7 @@ "init": { "data": { "events": "Komma getrennte L\u00ebscht vun Evenementer" - }, - "description": "Setzt ee mat Komma getrennten Evenement Numm fir all Evenement dob\u00e4i d\u00e9i sollt suiv\u00e9iert ginn. Wann's du se hei aginn hues, benotz d'DoorBird App fir se zu engem spezifeschen Evenement dob\u00e4i ze setzen. Kuckt d'Dokumentatioun op https://www.home-assistant.io/integrations/doorbird/#events. Beispill: somebody_pressed_the_button, motion" + } } } } diff --git a/homeassistant/components/doorbird/translations/nl.json b/homeassistant/components/doorbird/translations/nl.json index 83e47ca4c5e..cc59cd156b4 100644 --- a/homeassistant/components/doorbird/translations/nl.json +++ b/homeassistant/components/doorbird/translations/nl.json @@ -18,8 +18,7 @@ "name": "Apparaatnaam", "password": "Wachtwoord", "username": "Gebruikersnaam" - }, - "title": "Maak verbinding met de DoorBird" + } } } }, @@ -31,8 +30,7 @@ }, "data_description": { "events": "Voeg een door komma's gescheiden evenementnaam toe voor elk evenement dat u wilt volgen. Nadat u ze hier hebt ingevoerd, gebruikt u de DoorBird app om ze aan een specifieke gebeurtenis toe te wijzen.\n\nVoorbeeld: iemand_drukt_op_de_knop, beweging" - }, - "description": "Voeg een door komma's gescheiden evenementnaam toe voor elk evenement dat u wilt volgen. Nadat je ze hier hebt ingevoerd, gebruik je de DoorBird-app om ze toe te wijzen aan een specifiek evenement. Zie de documentatie op https://www.home-assistant.io/integrations/doorbird/#events. Voorbeeld: iemand_drukte_knop, beweging" + } } } } diff --git a/homeassistant/components/doorbird/translations/no.json b/homeassistant/components/doorbird/translations/no.json index a30fc93013a..0c1732b64c6 100644 --- a/homeassistant/components/doorbird/translations/no.json +++ b/homeassistant/components/doorbird/translations/no.json @@ -18,8 +18,7 @@ "name": "Enhetsnavn", "password": "Passord", "username": "Brukernavn" - }, - "title": "Koble til DoorBird" + } } } }, @@ -31,8 +30,7 @@ }, "data_description": { "events": "Legg til et navn p\u00e5 en kommadelt hendelse for hver hendelse du vil spore. N\u00e5r du har skrevet dem inn her, bruker du DoorBird-appen til \u00e5 tilordne dem til en bestemt hendelse.\n\nEksempel: somebody_pressed_the_button, bevegelse" - }, - "description": "Legg til et kommaseparert hendelsesnavn for hvert arrangement du \u00f8nsker \u00e5 spore. Etter \u00e5 ha skrevet dem inn her, bruker du DoorBird-appen til \u00e5 tilordne dem til en bestemt hendelse. Se dokumentasjonen p\u00e5 https://www.home-assistant.io/integrations/doorbird/#events. Eksempel: noen_trykket_knappen, bevegelse" + } } } } diff --git a/homeassistant/components/doorbird/translations/pl.json b/homeassistant/components/doorbird/translations/pl.json index 85a7a8765a7..f8731e1bb04 100644 --- a/homeassistant/components/doorbird/translations/pl.json +++ b/homeassistant/components/doorbird/translations/pl.json @@ -18,8 +18,7 @@ "name": "Nazwa urz\u0105dzenia", "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" - }, - "title": "Po\u0142\u0105czenie z DoorBird" + } } } }, @@ -31,8 +30,7 @@ }, "data_description": { "events": "Dodaj nazw\u0119 oddzielon\u0105 przecinkami dla ka\u017cdego wydarzenia, kt\u00f3re chcesz \u015bledzi\u0107. Po wprowadzeniu ich tutaj u\u017cyj aplikacji DoorBird, aby przypisa\u0107 je do konkretnego wydarzenia. \n\nPrzyk\u0142ad: kto\u015b_nacisn\u0105\u0142_przycisk, ruch" - }, - "description": "Dodaj nazwy wydarze\u0144 oddzielonych przecinkami, kt\u00f3re chcesz \u015bledzi\u0107. Po wprowadzeniu ich tutaj u\u017cyj aplikacji DoorBird, aby przypisa\u0107 je do okre\u015blonych zdarze\u0144. Zapoznaj si\u0119 z dokumentacj\u0105 na stronie https://www.home-assistant.io/integrations/doorbird/#events.\nPrzyk\u0142ad: nacisniecie_przycisku, ruch" + } } } } diff --git a/homeassistant/components/doorbird/translations/pt-BR.json b/homeassistant/components/doorbird/translations/pt-BR.json index 07170c086b7..7265638b43b 100644 --- a/homeassistant/components/doorbird/translations/pt-BR.json +++ b/homeassistant/components/doorbird/translations/pt-BR.json @@ -18,8 +18,7 @@ "name": "Nome do dispositivo", "password": "Senha", "username": "Usu\u00e1rio" - }, - "title": "Conecte-se ao DoorBird" + } } } }, @@ -31,8 +30,7 @@ }, "data_description": { "events": "Adicione um nome de evento separado por v\u00edrgula para cada evento que voc\u00ea deseja rastrear. Depois de inseri-los aqui, use o aplicativo DoorBird para atribu\u00ed-los a um evento espec\u00edfico. \n\n Exemplo: someone_pressed_the_button, movimento" - }, - "description": "Adicione um nome de evento separado por v\u00edrgula para cada evento que voc\u00ea deseja rastrear. Depois de inseri-los aqui, use o aplicativo DoorBird para atribu\u00ed-los a um evento espec\u00edfico. Consulte a documenta\u00e7\u00e3o em https://www.home-assistant.io/integrations/doorbird/#events. Exemplo: alguem_pressionou_o_botao movimento" + } } } } diff --git a/homeassistant/components/doorbird/translations/ru.json b/homeassistant/components/doorbird/translations/ru.json index 045092a2b97..be790392f50 100644 --- a/homeassistant/components/doorbird/translations/ru.json +++ b/homeassistant/components/doorbird/translations/ru.json @@ -18,8 +18,7 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" - }, - "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a DoorBird" + } } } }, @@ -31,8 +30,7 @@ }, "data_description": { "events": "\u0414\u043e\u0431\u0430\u0432\u044c\u0442\u0435 \u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0441\u043e\u0431\u044b\u0442\u0438\u0439, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c. \u041f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 DoorBird, \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0437\u043d\u0430\u0447\u0438\u0442\u044c \u0438\u0445 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u043e\u043c\u0443 \u0441\u043e\u0431\u044b\u0442\u0438\u044e.\n\n\u041f\u0440\u0438\u043c\u0435\u0440: somebody_pressed_the_button, motion." - }, - "description": "\u0414\u043e\u0431\u0430\u0432\u044c\u0442\u0435 \u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0441\u043e\u0431\u044b\u0442\u0438\u0439, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c. \u041f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 DoorBird, \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0437\u043d\u0430\u0447\u0438\u0442\u044c \u0438\u0445 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u043e\u043c\u0443 \u0441\u043e\u0431\u044b\u0442\u0438\u044e. \u041f\u0440\u0438\u043c\u0435\u0440: somebody_pressed_the_button, motion. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438: https://www.home-assistant.io/integrations/doorbird/#events." + } } } } diff --git a/homeassistant/components/doorbird/translations/sl.json b/homeassistant/components/doorbird/translations/sl.json index 336a40904d2..446445a2b31 100644 --- a/homeassistant/components/doorbird/translations/sl.json +++ b/homeassistant/components/doorbird/translations/sl.json @@ -18,8 +18,7 @@ "name": "Ime naprave", "password": "Geslo", "username": "Uporabni\u0161ko ime" - }, - "title": "Pove\u017eite se z DoorBird" + } } } }, @@ -28,8 +27,7 @@ "init": { "data": { "events": "Seznam dogodkov, lo\u010denih z vejico." - }, - "description": "Za vsak dogodek, ki ga \u017eelite spremljati, dodajte ime, lo\u010deno z vejico. Ko jih tukaj vnesete, uporabite aplikacijo DoorBird, da jih dodelite dolo\u010denemu dogodku. Glej dokumentacijo na strani https://www.home-assistant.io/integrations/doorbird/#events. Primer: nekdo_pritisnil_gumb, gibanje" + } } } } diff --git a/homeassistant/components/doorbird/translations/sv.json b/homeassistant/components/doorbird/translations/sv.json index 546535fb937..b2a809a576e 100644 --- a/homeassistant/components/doorbird/translations/sv.json +++ b/homeassistant/components/doorbird/translations/sv.json @@ -9,8 +9,7 @@ "host": "V\u00e4rd (IP-adress)", "name": "Enhetsnamn", "username": "Anv\u00e4ndarnamn" - }, - "title": "Anslut till DoorBird" + } } } } diff --git a/homeassistant/components/doorbird/translations/tr.json b/homeassistant/components/doorbird/translations/tr.json index 1e472a43535..bffb5aacf82 100644 --- a/homeassistant/components/doorbird/translations/tr.json +++ b/homeassistant/components/doorbird/translations/tr.json @@ -18,8 +18,7 @@ "name": "Cihaz ad\u0131", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - }, - "title": "DoorBird'e ba\u011flan\u0131n" + } } } }, @@ -31,8 +30,7 @@ }, "data_description": { "events": "\u0130zlemek istedi\u011finiz her etkinlik i\u00e7in virg\u00fclle ayr\u0131lm\u0131\u015f bir etkinlik ad\u0131 ekleyin. Bunlar\u0131 buraya girdikten sonra, onlar\u0131 belirli bir etkinli\u011fe atamak i\u00e7in DoorBird uygulamas\u0131n\u0131 kullan\u0131n. \n\n \u00d6rnek: birisi_butona_basti, hareket" - }, - "description": "\u0130zlemek istedi\u011finiz her etkinlik i\u00e7in virg\u00fclle ayr\u0131lm\u0131\u015f bir etkinlik ad\u0131 ekleyin. Bunlar\u0131 buraya girdikten sonra, onlar\u0131 belirli bir etkinli\u011fe atamak i\u00e7in DoorBird uygulamas\u0131n\u0131 kullan\u0131n. https://www.home-assistant.io/integrations/doorbird/#events adresindeki belgelere bak\u0131n. \u00d6rnek: birisi_pressed_the_button, hareket" + } } } } diff --git a/homeassistant/components/doorbird/translations/uk.json b/homeassistant/components/doorbird/translations/uk.json index 07bbdfacafe..85a7959abdf 100644 --- a/homeassistant/components/doorbird/translations/uk.json +++ b/homeassistant/components/doorbird/translations/uk.json @@ -18,8 +18,7 @@ "name": "\u041d\u0430\u0437\u0432\u0430", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" - }, - "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e DoorBird" + } } } }, @@ -28,8 +27,7 @@ "init": { "data": { "events": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u0434\u0456\u0439 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u0443." - }, - "description": "\u0414\u043e\u0434\u0430\u0439\u0442\u0435 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u0443 \u043d\u0430\u0437\u0432\u0438 \u043f\u043e\u0434\u0456\u0439, \u044f\u043a\u0435 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0456\u0434\u0441\u043b\u0456\u0434\u043a\u043e\u0432\u0443\u0432\u0430\u0442\u0438. \u041f\u0456\u0441\u043b\u044f \u0446\u044c\u043e\u0433\u043e, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a DoorBird, \u0449\u043e\u0431 \u043f\u0440\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 \u0457\u0445 \u0434\u043e \u043f\u0435\u0432\u043d\u043e\u0457 \u043f\u043e\u0434\u0456\u0457. \u041f\u0440\u0438\u043a\u043b\u0430\u0434: somebody_pressed_the_button, motion. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457: https://www.home-assistant.io/integrations/doorbird/#events." + } } } } diff --git a/homeassistant/components/doorbird/translations/zh-Hant.json b/homeassistant/components/doorbird/translations/zh-Hant.json index 616e346b0dc..22bdf8b0c91 100644 --- a/homeassistant/components/doorbird/translations/zh-Hant.json +++ b/homeassistant/components/doorbird/translations/zh-Hant.json @@ -18,8 +18,7 @@ "name": "\u88dd\u7f6e\u540d\u7a31", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" - }, - "title": "\u9023\u7dda\u81f3 DoorBird" + } } } }, @@ -31,8 +30,7 @@ }, "data_description": { "events": "\u4ee5\u9017\u865f\u5206\u5225\u6240\u8981\u8ffd\u8e64\u7684\u4e8b\u4ef6\u540d\u7a31\u3002\u65bc\u6b64\u8f38\u5165\u5f8c\uff0c\u4f7f\u7528 DoorBird App \u6307\u5b9a\u81f3\u7279\u5b9a\u4e8b\u4ef6\u3002\n\n\u4f8b\u5982\uff1asomebody_pressed_the_button, motion" - }, - "description": "\u4ee5\u9017\u865f\u5206\u5225\u6240\u8981\u8ffd\u8e64\u7684\u4e8b\u4ef6\u540d\u7a31\u3002\u65bc\u6b64\u8f38\u5165\u5f8c\uff0c\u4f7f\u7528 DoorBird App \u6307\u5b9a\u81f3\u7279\u5b9a\u4e8b\u4ef6\u3002\u8acb\u53c3\u95b1\u6587\u4ef6\uff1ahttps://www.home-assistant.io/integrations/doorbird/#events\u3002\u4f8b\u5982\uff1asomebody_pressed_the_button, motion" + } } } } diff --git a/homeassistant/components/dunehd/translations/ca.json b/homeassistant/components/dunehd/translations/ca.json index 08dd841bf4f..e1da2dc867c 100644 --- a/homeassistant/components/dunehd/translations/ca.json +++ b/homeassistant/components/dunehd/translations/ca.json @@ -13,8 +13,7 @@ "data": { "host": "Amfitri\u00f3" }, - "description": "Assegura't que el reproductor est\u00e0 engegat.", - "title": "Dune HD" + "description": "Assegura't que el reproductor est\u00e0 engegat." } } } diff --git a/homeassistant/components/dunehd/translations/cs.json b/homeassistant/components/dunehd/translations/cs.json index b52eb3d2cdf..e0e097b925d 100644 --- a/homeassistant/components/dunehd/translations/cs.json +++ b/homeassistant/components/dunehd/translations/cs.json @@ -13,8 +13,7 @@ "data": { "host": "Hostitel" }, - "description": "Nastaven\u00ed integrace Dune HD. Pokud m\u00e1te probl\u00e9my s nastaven\u00edm, p\u0159ejd\u011bte na: https://www.home-assistant.io/integrations/dunehd \n\nUjist\u011bte se, \u017ee je v\u00e1\u0161 p\u0159ehr\u00e1va\u010d zapnut\u00fd.", - "title": "Dune HD" + "description": "Nastaven\u00ed integrace Dune HD. Pokud m\u00e1te probl\u00e9my s nastaven\u00edm, p\u0159ejd\u011bte na: https://www.home-assistant.io/integrations/dunehd \n\nUjist\u011bte se, \u017ee je v\u00e1\u0161 p\u0159ehr\u00e1va\u010d zapnut\u00fd." } } } diff --git a/homeassistant/components/dunehd/translations/de.json b/homeassistant/components/dunehd/translations/de.json index bcab02b2a09..58171e2fa23 100644 --- a/homeassistant/components/dunehd/translations/de.json +++ b/homeassistant/components/dunehd/translations/de.json @@ -13,8 +13,7 @@ "data": { "host": "Host" }, - "description": "Stelle sicher, dass dein Player eingeschaltet ist.", - "title": "Dune HD" + "description": "Stelle sicher, dass dein Player eingeschaltet ist." } } } diff --git a/homeassistant/components/dunehd/translations/el.json b/homeassistant/components/dunehd/translations/el.json index 96ad16ac67f..e14fb5c7308 100644 --- a/homeassistant/components/dunehd/translations/el.json +++ b/homeassistant/components/dunehd/translations/el.json @@ -13,8 +13,7 @@ "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" }, - "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Dune HD. \u0391\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c0\u03b7\u03b3\u03b1\u03af\u03bd\u03b5\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: https://www.home-assistant.io/integrations/dunehd \n\n\u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7.", - "title": "Dune HD" + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Dune HD. \u0391\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c0\u03c1\u03bf\u03b2\u03bb\u03ae\u03bc\u03b1\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c0\u03b7\u03b3\u03b1\u03af\u03bd\u03b5\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: https://www.home-assistant.io/integrations/dunehd \n\n\u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7." } } } diff --git a/homeassistant/components/dunehd/translations/en.json b/homeassistant/components/dunehd/translations/en.json index d6392509fcf..6139d5b5251 100644 --- a/homeassistant/components/dunehd/translations/en.json +++ b/homeassistant/components/dunehd/translations/en.json @@ -13,8 +13,7 @@ "data": { "host": "Host" }, - "description": "Ensure that your player is turned on.", - "title": "Dune HD" + "description": "Ensure that your player is turned on." } } } diff --git a/homeassistant/components/dunehd/translations/es-419.json b/homeassistant/components/dunehd/translations/es-419.json index 5ad7a6640b4..d74cafdedde 100644 --- a/homeassistant/components/dunehd/translations/es-419.json +++ b/homeassistant/components/dunehd/translations/es-419.json @@ -2,8 +2,7 @@ "config": { "step": { "user": { - "description": "Configure la integraci\u00f3n de Dune HD. Si tiene problemas con la configuraci\u00f3n, vaya a: https://www.home-assistant.io/integrations/dunehd \n\n Aseg\u00farese de que su reproductor est\u00e9 encendido.", - "title": "Dune HD" + "description": "Configure la integraci\u00f3n de Dune HD. Si tiene problemas con la configuraci\u00f3n, vaya a: https://www.home-assistant.io/integrations/dunehd \n\n Aseg\u00farese de que su reproductor est\u00e9 encendido." } } } diff --git a/homeassistant/components/dunehd/translations/es.json b/homeassistant/components/dunehd/translations/es.json index 33f2e026359..7e2c1282ca6 100644 --- a/homeassistant/components/dunehd/translations/es.json +++ b/homeassistant/components/dunehd/translations/es.json @@ -13,8 +13,7 @@ "data": { "host": "Host" }, - "description": "Configura la integraci\u00f3n de Dune HD. Si tienes problemas con la configuraci\u00f3n, ve a: https://www.home-assistant.io/integrations/dunehd \n\n Aseg\u00farate de que tu reproductor est\u00e1 encendido.", - "title": "Dune HD" + "description": "Configura la integraci\u00f3n de Dune HD. Si tienes problemas con la configuraci\u00f3n, ve a: https://www.home-assistant.io/integrations/dunehd \n\n Aseg\u00farate de que tu reproductor est\u00e1 encendido." } } } diff --git a/homeassistant/components/dunehd/translations/et.json b/homeassistant/components/dunehd/translations/et.json index 3f4a9b4b085..9cd7d30bc2c 100644 --- a/homeassistant/components/dunehd/translations/et.json +++ b/homeassistant/components/dunehd/translations/et.json @@ -13,8 +13,7 @@ "data": { "host": "" }, - "description": "Veendu, et m\u00e4ngija on sisse l\u00fclitatud.", - "title": "" + "description": "Veendu, et m\u00e4ngija on sisse l\u00fclitatud." } } } diff --git a/homeassistant/components/dunehd/translations/fr.json b/homeassistant/components/dunehd/translations/fr.json index 8c3bc89a0e6..a8831b94f49 100644 --- a/homeassistant/components/dunehd/translations/fr.json +++ b/homeassistant/components/dunehd/translations/fr.json @@ -13,8 +13,7 @@ "data": { "host": "H\u00f4te" }, - "description": "Assurez-vous que votre lecteur est allum\u00e9.", - "title": "Dune HD" + "description": "Assurez-vous que votre lecteur est allum\u00e9." } } } diff --git a/homeassistant/components/dunehd/translations/hu.json b/homeassistant/components/dunehd/translations/hu.json index 1dc7c8ec6cc..66245b690fd 100644 --- a/homeassistant/components/dunehd/translations/hu.json +++ b/homeassistant/components/dunehd/translations/hu.json @@ -13,8 +13,7 @@ "data": { "host": "C\u00edm" }, - "description": "Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a lej\u00e1tsz\u00f3 be van kapcsolva.", - "title": "Dune HD" + "description": "Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a lej\u00e1tsz\u00f3 be van kapcsolva." } } } diff --git a/homeassistant/components/dunehd/translations/id.json b/homeassistant/components/dunehd/translations/id.json index 5a86947a07a..0a0f90a5a0f 100644 --- a/homeassistant/components/dunehd/translations/id.json +++ b/homeassistant/components/dunehd/translations/id.json @@ -13,8 +13,7 @@ "data": { "host": "Host" }, - "description": "Pastikan pemutar Anda dinyalakan.", - "title": "Dune HD" + "description": "Pastikan pemutar Anda dinyalakan." } } } diff --git a/homeassistant/components/dunehd/translations/it.json b/homeassistant/components/dunehd/translations/it.json index 0e33feb2640..9ef4be284b6 100644 --- a/homeassistant/components/dunehd/translations/it.json +++ b/homeassistant/components/dunehd/translations/it.json @@ -13,8 +13,7 @@ "data": { "host": "Host" }, - "description": "Assicurati che il tuo lettore sia acceso.", - "title": "Dune HD" + "description": "Assicurati che il tuo lettore sia acceso." } } } diff --git a/homeassistant/components/dunehd/translations/ja.json b/homeassistant/components/dunehd/translations/ja.json index 79bb6b0746d..c82d7ce8f94 100644 --- a/homeassistant/components/dunehd/translations/ja.json +++ b/homeassistant/components/dunehd/translations/ja.json @@ -13,8 +13,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "Dune HD\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/dunehd \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "Dune HD" + "description": "Dune HD\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/dunehd \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } } diff --git a/homeassistant/components/dunehd/translations/ko.json b/homeassistant/components/dunehd/translations/ko.json index 5c7feb27f7e..8bb3de70327 100644 --- a/homeassistant/components/dunehd/translations/ko.json +++ b/homeassistant/components/dunehd/translations/ko.json @@ -13,8 +13,7 @@ "data": { "host": "\ud638\uc2a4\ud2b8" }, - "description": "Dune HD \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694. \uad6c\uc131\uc5d0 \ubb38\uc81c\uac00 \uc788\ub294 \uacbd\uc6b0 https://www.home-assistant.io/integrations/dunehd \ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.\n\n\uae30\uae30\uac00 \ucf1c\uc838 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", - "title": "Dune HD" + "description": "Dune HD \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694. \uad6c\uc131\uc5d0 \ubb38\uc81c\uac00 \uc788\ub294 \uacbd\uc6b0 https://www.home-assistant.io/integrations/dunehd \ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.\n\n\uae30\uae30\uac00 \ucf1c\uc838 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694." } } } diff --git a/homeassistant/components/dunehd/translations/lb.json b/homeassistant/components/dunehd/translations/lb.json index 035bd24f6b3..9dc5f1f2c5d 100644 --- a/homeassistant/components/dunehd/translations/lb.json +++ b/homeassistant/components/dunehd/translations/lb.json @@ -13,8 +13,7 @@ "data": { "host": "Host" }, - "description": "Dune HD Integratioun ariichten. Falls et Problemer mat der Konfiguratioun g\u00ebtt g\u00e9i op:\nhttps://www.home-assistant.io/integrations/dunehd \nStell s\u00e9cher dass d\u00e4in Ofspiller un ass.", - "title": "Dune HD" + "description": "Dune HD Integratioun ariichten. Falls et Problemer mat der Konfiguratioun g\u00ebtt g\u00e9i op:\nhttps://www.home-assistant.io/integrations/dunehd \nStell s\u00e9cher dass d\u00e4in Ofspiller un ass." } } } diff --git a/homeassistant/components/dunehd/translations/nl.json b/homeassistant/components/dunehd/translations/nl.json index f4c54a5dba5..3ee04091a1c 100644 --- a/homeassistant/components/dunehd/translations/nl.json +++ b/homeassistant/components/dunehd/translations/nl.json @@ -13,8 +13,7 @@ "data": { "host": "Host" }, - "description": "Zorg ervoor dat uw speler is ingeschakeld.", - "title": "Dune HD" + "description": "Zorg ervoor dat uw speler is ingeschakeld." } } } diff --git a/homeassistant/components/dunehd/translations/no.json b/homeassistant/components/dunehd/translations/no.json index 1b35f5581b1..ac38bbbdbf9 100644 --- a/homeassistant/components/dunehd/translations/no.json +++ b/homeassistant/components/dunehd/translations/no.json @@ -13,8 +13,7 @@ "data": { "host": "Vert" }, - "description": "S\u00f8rg for at spilleren er sl\u00e5tt p\u00e5.", - "title": "" + "description": "S\u00f8rg for at spilleren er sl\u00e5tt p\u00e5." } } } diff --git a/homeassistant/components/dunehd/translations/pl.json b/homeassistant/components/dunehd/translations/pl.json index 376b84e05d9..4af098c58f9 100644 --- a/homeassistant/components/dunehd/translations/pl.json +++ b/homeassistant/components/dunehd/translations/pl.json @@ -13,8 +13,7 @@ "data": { "host": "Nazwa hosta lub adres IP" }, - "description": "Upewnij si\u0119, \u017ce odtwarzacz jest w\u0142\u0105czony.", - "title": "Dune HD" + "description": "Upewnij si\u0119, \u017ce odtwarzacz jest w\u0142\u0105czony." } } } diff --git a/homeassistant/components/dunehd/translations/pt-BR.json b/homeassistant/components/dunehd/translations/pt-BR.json index 4775f1af379..a9c2f20a225 100644 --- a/homeassistant/components/dunehd/translations/pt-BR.json +++ b/homeassistant/components/dunehd/translations/pt-BR.json @@ -13,8 +13,7 @@ "data": { "host": "Nome do host" }, - "description": "Certifique-se de que seu player est\u00e1 ligado.", - "title": "Dune HD" + "description": "Certifique-se de que seu player est\u00e1 ligado." } } } diff --git a/homeassistant/components/dunehd/translations/ru.json b/homeassistant/components/dunehd/translations/ru.json index 134511e66f2..8c32af72af7 100644 --- a/homeassistant/components/dunehd/translations/ru.json +++ b/homeassistant/components/dunehd/translations/ru.json @@ -13,8 +13,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043f\u0440\u043e\u0438\u0433\u0440\u044b\u0432\u0430\u0442\u0435\u043b\u044c \u0432\u043a\u043b\u044e\u0447\u0435\u043d.", - "title": "Dune HD" + "description": "\u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u043f\u0440\u043e\u0438\u0433\u0440\u044b\u0432\u0430\u0442\u0435\u043b\u044c \u0432\u043a\u043b\u044e\u0447\u0435\u043d." } } } diff --git a/homeassistant/components/dunehd/translations/tr.json b/homeassistant/components/dunehd/translations/tr.json index 79c2f033cfc..f4c7c0db43a 100644 --- a/homeassistant/components/dunehd/translations/tr.json +++ b/homeassistant/components/dunehd/translations/tr.json @@ -13,8 +13,7 @@ "data": { "host": "Sunucu" }, - "description": "Oynat\u0131c\u0131n\u0131z\u0131n a\u00e7\u0131k oldu\u011fundan emin olun.", - "title": "Dune HD" + "description": "Oynat\u0131c\u0131n\u0131z\u0131n a\u00e7\u0131k oldu\u011fundan emin olun." } } } diff --git a/homeassistant/components/dunehd/translations/uk.json b/homeassistant/components/dunehd/translations/uk.json index d2f4eadbdcb..bec5590ecb5 100644 --- a/homeassistant/components/dunehd/translations/uk.json +++ b/homeassistant/components/dunehd/translations/uk.json @@ -13,8 +13,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Dune HD. \u042f\u043a\u0449\u043e \u0443 \u0412\u0430\u0441 \u0432\u0438\u043d\u0438\u043a\u043b\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0437 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438 \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e: https://www.home-assistant.io/integrations/dunehd \n\n \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0412\u0430\u0448 \u043f\u043b\u0435\u0454\u0440 \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439.", - "title": "Dune HD" + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Dune HD. \u042f\u043a\u0449\u043e \u0443 \u0412\u0430\u0441 \u0432\u0438\u043d\u0438\u043a\u043b\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0437 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438 \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e: https://www.home-assistant.io/integrations/dunehd \n\n \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0412\u0430\u0448 \u043f\u043b\u0435\u0454\u0440 \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439." } } } diff --git a/homeassistant/components/dunehd/translations/zh-Hant.json b/homeassistant/components/dunehd/translations/zh-Hant.json index 993a6d9a210..138b3801adf 100644 --- a/homeassistant/components/dunehd/translations/zh-Hant.json +++ b/homeassistant/components/dunehd/translations/zh-Hant.json @@ -13,8 +13,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u78ba\u5b9a\u64ad\u653e\u5668\u5df2\u7d93\u958b\u555f\u3002", - "title": "Dune HD" + "description": "\u78ba\u5b9a\u64ad\u653e\u5668\u5df2\u7d93\u958b\u555f\u3002" } } } diff --git a/homeassistant/components/efergy/translations/ca.json b/homeassistant/components/efergy/translations/ca.json index 298826e75e5..5b8a723019a 100644 --- a/homeassistant/components/efergy/translations/ca.json +++ b/homeassistant/components/efergy/translations/ca.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "Clau API" - }, - "title": "Efergy" + } } } } diff --git a/homeassistant/components/efergy/translations/cs.json b/homeassistant/components/efergy/translations/cs.json index b2fe2ea015f..058abebbf28 100644 --- a/homeassistant/components/efergy/translations/cs.json +++ b/homeassistant/components/efergy/translations/cs.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "API kl\u00ed\u010d" - }, - "title": "Efergy" + } } } } diff --git a/homeassistant/components/efergy/translations/de.json b/homeassistant/components/efergy/translations/de.json index 3945d3da7d4..659657731f4 100644 --- a/homeassistant/components/efergy/translations/de.json +++ b/homeassistant/components/efergy/translations/de.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "API-Schl\u00fcssel" - }, - "title": "Efergy" + } } } } diff --git a/homeassistant/components/efergy/translations/el.json b/homeassistant/components/efergy/translations/el.json index f1206afd71b..8ce9b0ddd8c 100644 --- a/homeassistant/components/efergy/translations/el.json +++ b/homeassistant/components/efergy/translations/el.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" - }, - "title": "Efergy" + } } } } diff --git a/homeassistant/components/efergy/translations/en.json b/homeassistant/components/efergy/translations/en.json index aa76f9c0636..0909241c423 100644 --- a/homeassistant/components/efergy/translations/en.json +++ b/homeassistant/components/efergy/translations/en.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "API Key" - }, - "title": "Efergy" + } } } } diff --git a/homeassistant/components/efergy/translations/es.json b/homeassistant/components/efergy/translations/es.json index a72347abef6..ce25d3f3184 100644 --- a/homeassistant/components/efergy/translations/es.json +++ b/homeassistant/components/efergy/translations/es.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "Clave API" - }, - "title": "Efergy" + } } } } diff --git a/homeassistant/components/efergy/translations/et.json b/homeassistant/components/efergy/translations/et.json index 73c2f1a3547..c3f599934dc 100644 --- a/homeassistant/components/efergy/translations/et.json +++ b/homeassistant/components/efergy/translations/et.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "API v\u00f5ti" - }, - "title": "Efergy" + } } } } diff --git a/homeassistant/components/efergy/translations/fr.json b/homeassistant/components/efergy/translations/fr.json index a506454bc14..9bcf81e0cab 100644 --- a/homeassistant/components/efergy/translations/fr.json +++ b/homeassistant/components/efergy/translations/fr.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "Cl\u00e9 d'API" - }, - "title": "Efergy" + } } } } diff --git a/homeassistant/components/efergy/translations/hu.json b/homeassistant/components/efergy/translations/hu.json index 032ef05d527..9d1f47f803c 100644 --- a/homeassistant/components/efergy/translations/hu.json +++ b/homeassistant/components/efergy/translations/hu.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "API kulcs" - }, - "title": "Efergy" + } } } } diff --git a/homeassistant/components/efergy/translations/id.json b/homeassistant/components/efergy/translations/id.json index 234e5122db2..037d72a0a15 100644 --- a/homeassistant/components/efergy/translations/id.json +++ b/homeassistant/components/efergy/translations/id.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "Kunci API" - }, - "title": "Efergy" + } } } } diff --git a/homeassistant/components/efergy/translations/it.json b/homeassistant/components/efergy/translations/it.json index d5677424d42..df4b96223e5 100644 --- a/homeassistant/components/efergy/translations/it.json +++ b/homeassistant/components/efergy/translations/it.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "Chiave API" - }, - "title": "Efergy" + } } } } diff --git a/homeassistant/components/efergy/translations/ja.json b/homeassistant/components/efergy/translations/ja.json index 98dd06ab4f0..319fbd744cf 100644 --- a/homeassistant/components/efergy/translations/ja.json +++ b/homeassistant/components/efergy/translations/ja.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "API\u30ad\u30fc" - }, - "title": "Efergy" + } } } } diff --git a/homeassistant/components/efergy/translations/nl.json b/homeassistant/components/efergy/translations/nl.json index 4f97bad11a0..0a51ce58a9c 100644 --- a/homeassistant/components/efergy/translations/nl.json +++ b/homeassistant/components/efergy/translations/nl.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "API-sleutel" - }, - "title": "Efergy" + } } } } diff --git a/homeassistant/components/efergy/translations/no.json b/homeassistant/components/efergy/translations/no.json index 388cbb36f64..4a109ab8fa9 100644 --- a/homeassistant/components/efergy/translations/no.json +++ b/homeassistant/components/efergy/translations/no.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "API-n\u00f8kkel" - }, - "title": "Efergy" + } } } } diff --git a/homeassistant/components/efergy/translations/pl.json b/homeassistant/components/efergy/translations/pl.json index b96038d24b3..9ef0e4a5a43 100644 --- a/homeassistant/components/efergy/translations/pl.json +++ b/homeassistant/components/efergy/translations/pl.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "Klucz API" - }, - "title": "Efergy" + } } } } diff --git a/homeassistant/components/efergy/translations/pt-BR.json b/homeassistant/components/efergy/translations/pt-BR.json index 8197121b5d5..065c29ab9ab 100644 --- a/homeassistant/components/efergy/translations/pt-BR.json +++ b/homeassistant/components/efergy/translations/pt-BR.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "Chave da API" - }, - "title": "Efergy" + } } } } diff --git a/homeassistant/components/efergy/translations/ru.json b/homeassistant/components/efergy/translations/ru.json index 6a659c9b7c6..3c0e7797c7b 100644 --- a/homeassistant/components/efergy/translations/ru.json +++ b/homeassistant/components/efergy/translations/ru.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API" - }, - "title": "Efergy" + } } } } diff --git a/homeassistant/components/efergy/translations/tr.json b/homeassistant/components/efergy/translations/tr.json index e13f215b5fb..cbb81598338 100644 --- a/homeassistant/components/efergy/translations/tr.json +++ b/homeassistant/components/efergy/translations/tr.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "API Anahtar\u0131" - }, - "title": "Efergy" + } } } } diff --git a/homeassistant/components/efergy/translations/zh-Hant.json b/homeassistant/components/efergy/translations/zh-Hant.json index c8f2d660c2f..dda28849714 100644 --- a/homeassistant/components/efergy/translations/zh-Hant.json +++ b/homeassistant/components/efergy/translations/zh-Hant.json @@ -13,8 +13,7 @@ "user": { "data": { "api_key": "API \u91d1\u9470" - }, - "title": "Efergy" + } } } } diff --git a/homeassistant/components/elmax/translations/bg.json b/homeassistant/components/elmax/translations/bg.json index babce99193f..aa6a3dc3ef5 100644 --- a/homeassistant/components/elmax/translations/bg.json +++ b/homeassistant/components/elmax/translations/bg.json @@ -4,12 +4,10 @@ "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": { - "bad_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", "invalid_pin": "\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d\u0438\u044f\u0442 \u041f\u0418\u041d \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d", "network_error": "\u0412\u044a\u0437\u043d\u0438\u043a\u043d\u0430 \u043c\u0440\u0435\u0436\u043e\u0432\u0430 \u0433\u0440\u0435\u0448\u043a\u0430", - "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430", - "unknown_error": "\u0412\u044a\u0437\u043d\u0438\u043a\u043d\u0430 \u043d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { "panels": { diff --git a/homeassistant/components/elmax/translations/ca.json b/homeassistant/components/elmax/translations/ca.json index 74d9f72dce5..bac698a9656 100644 --- a/homeassistant/components/elmax/translations/ca.json +++ b/homeassistant/components/elmax/translations/ca.json @@ -4,13 +4,11 @@ "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { - "bad_auth": "Autenticaci\u00f3 inv\u00e0lida", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "invalid_pin": "El pin proporcionat no \u00e9s v\u00e0lid", "network_error": "S'ha produ\u00eft un error de xarxa", "no_panel_online": "No s'ha trobat cap panell de control d'Elmax en l\u00ednia.", - "unknown": "Error inesperat", - "unknown_error": "S'ha produ\u00eft un error desconegut" + "unknown": "Error inesperat" }, "step": { "panels": { @@ -19,18 +17,15 @@ "panel_name": "Nom del panell", "panel_pin": "Codi PIN" }, - "description": "Selecciona quin panell vols controlar amb aquesta integraci\u00f3. Tingues en compte que el panell ha d'estar ON per poder ser configurat.", - "title": "Selecci\u00f3 del panell" + "description": "Selecciona quin panell vols controlar amb aquesta integraci\u00f3. Tingues en compte que el panell ha d'estar ON per poder ser configurat." }, "user": { "data": { "password": "Contrasenya", "username": "Nom d'usuari" }, - "description": "Inicia sessi\u00f3 a Elmax Cloud amb les teves credencials", - "title": "Inici de sessi\u00f3" + "description": "Inicia sessi\u00f3 a Elmax Cloud amb les teves credencials" } } - }, - "title": "Configuraci\u00f3 d'Elmax Cloud" + } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/cs.json b/homeassistant/components/elmax/translations/cs.json index a3e919af352..6c534c1a8a6 100644 --- a/homeassistant/components/elmax/translations/cs.json +++ b/homeassistant/components/elmax/translations/cs.json @@ -1,13 +1,11 @@ { "config": { "error": { - "bad_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "invalid_pin": "Poskytnut\u00fd k\u00f3d PIN je neplatn\u00fd", "network_error": "Do\u0161lo k chyb\u011b s\u00edt\u011b", "no_panel_online": "Nebyl nalezen \u017e\u00e1dn\u00fd online ovl\u00e1dac\u00ed panel Elmax.", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba", - "unknown_error": "Vyskytla se neo\u010dek\u00e1van\u00e1 chyba" + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { "panels": { @@ -16,18 +14,15 @@ "panel_name": "N\u00e1zev panelu", "panel_pin": "PIN k\u00f3d" }, - "description": "Vyberte, kter\u00fd panel chcete touto integrac\u00ed ovl\u00e1dat. Vezm\u011bte pros\u00edm na v\u011bdom\u00ed, \u017ee panel mus\u00ed b\u00fdt zapnut\u00fd, aby mohl b\u00fdt nakonfigurov\u00e1n.", - "title": "V\u00fdb\u011br panelu" + "description": "Vyberte, kter\u00fd panel chcete touto integrac\u00ed ovl\u00e1dat. Vezm\u011bte pros\u00edm na v\u011bdom\u00ed, \u017ee panel mus\u00ed b\u00fdt zapnut\u00fd, aby mohl b\u00fdt nakonfigurov\u00e1n." }, "user": { "data": { "password": "Heslo", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" }, - "description": "P\u0159ihlaste se do cloudu Elmax pomoc\u00ed sv\u00fdch p\u0159ihla\u0161ovac\u00edch \u00fadaj\u016f", - "title": "P\u0159ihl\u00e1\u0161en\u00ed k \u00fa\u010dtu" + "description": "P\u0159ihlaste se do cloudu Elmax pomoc\u00ed sv\u00fdch p\u0159ihla\u0161ovac\u00edch \u00fadaj\u016f" } } - }, - "title": "Nastaven\u00ed Elmax Cloud" + } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/de.json b/homeassistant/components/elmax/translations/de.json index 241802e82d4..55e947d5bd0 100644 --- a/homeassistant/components/elmax/translations/de.json +++ b/homeassistant/components/elmax/translations/de.json @@ -4,13 +4,11 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "bad_auth": "Ung\u00fcltige Authentifizierung", "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_pin": "Die angegebene Pin ist ung\u00fcltig", "network_error": "Ein Netzwerkfehler ist aufgetreten", "no_panel_online": "Es wurde kein Elmax-Bedienfeld gefunden, das online ist.", - "unknown": "Unerwarteter Fehler", - "unknown_error": "Ein unerwarteter Fehler ist aufgetreten" + "unknown": "Unerwarteter Fehler" }, "step": { "panels": { @@ -19,18 +17,15 @@ "panel_name": "Panel-Name", "panel_pin": "PIN-Code" }, - "description": "W\u00e4hle die Zentrale aus, die du mit dieser Integration steuern m\u00f6chtest. Bitte beachte, dass die Zentrale eingeschaltet sein muss, damit sie konfiguriert werden kann.", - "title": "Panelauswahl" + "description": "W\u00e4hle die Zentrale aus, die du mit dieser Integration steuern m\u00f6chtest. Bitte beachte, dass die Zentrale eingeschaltet sein muss, damit sie konfiguriert werden kann." }, "user": { "data": { "password": "Passwort", "username": "Benutzername" }, - "description": "Bitte melde dich mit deinen Zugangsdaten bei der Elmax-Cloud an", - "title": "Konto-Anmeldung" + "description": "Bitte melde dich mit deinen Zugangsdaten bei der Elmax-Cloud an" } } - }, - "title": "Elmax Cloud-Einrichtung" + } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/el.json b/homeassistant/components/elmax/translations/el.json index 916ab585935..2b99f715ccc 100644 --- a/homeassistant/components/elmax/translations/el.json +++ b/homeassistant/components/elmax/translations/el.json @@ -4,13 +4,11 @@ "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" }, "error": { - "bad_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "invalid_pin": "\u03a4\u03bf \u03c0\u03b1\u03c1\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf pin \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf", "network_error": "\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5", "no_panel_online": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc\u03c2 \u03c0\u03af\u03bd\u03b1\u03ba\u03b1\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 Elmax.", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", - "unknown_error": "\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bc\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { "panels": { @@ -19,18 +17,15 @@ "panel_name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c0\u03af\u03bd\u03b1\u03ba\u03b1", "panel_pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN" }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b8\u03ad\u03bb\u03b1\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bb\u03ad\u03b3\u03be\u03b5\u03c4\u03b5 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7. \u03a3\u03b7\u03bc\u03b5\u03b9\u03ce\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03bf \u03c0\u03af\u03bd\u03b1\u03ba\u03b1\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af.", - "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c0\u03af\u03bd\u03b1\u03ba\u03b1" + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03b8\u03ad\u03bb\u03b1\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bb\u03ad\u03b3\u03be\u03b5\u03c4\u03b5 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7. \u03a3\u03b7\u03bc\u03b5\u03b9\u03ce\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03bf \u03c0\u03af\u03bd\u03b1\u03ba\u03b1\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af." }, "user": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, - "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf cloud \u03c4\u03b7\u03c2 Elmax \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03ac \u03c3\u03b1\u03c2", - "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd" + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf cloud \u03c4\u03b7\u03c2 Elmax \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03ac \u03c3\u03b1\u03c2" } } - }, - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 Elmax Cloud" + } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/en.json b/homeassistant/components/elmax/translations/en.json index 60da5d88dc3..dabd58fc941 100644 --- a/homeassistant/components/elmax/translations/en.json +++ b/homeassistant/components/elmax/translations/en.json @@ -4,13 +4,11 @@ "already_configured": "Device is already configured" }, "error": { - "bad_auth": "Invalid authentication", "invalid_auth": "Invalid authentication", "invalid_pin": "The provided pin is invalid", "network_error": "A network error occurred", "no_panel_online": "No online Elmax control panel was found.", - "unknown": "Unexpected error", - "unknown_error": "An unexpected error occurred" + "unknown": "Unexpected error" }, "step": { "panels": { @@ -19,18 +17,15 @@ "panel_name": "Panel Name", "panel_pin": "PIN Code" }, - "description": "Select which panel you would like to control with this integration. Please note that the panel must be ON in order to be configured.", - "title": "Panel selection" + "description": "Select which panel you would like to control with this integration. Please note that the panel must be ON in order to be configured." }, "user": { "data": { "password": "Password", "username": "Username" }, - "description": "Please login to the Elmax cloud using your credentials", - "title": "Account Login" + "description": "Please login to the Elmax cloud using your credentials" } } - }, - "title": "Elmax Cloud Setup" + } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/es.json b/homeassistant/components/elmax/translations/es.json index 76d3d14aeab..a8130c70d7e 100644 --- a/homeassistant/components/elmax/translations/es.json +++ b/homeassistant/components/elmax/translations/es.json @@ -4,13 +4,11 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "bad_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_pin": "El pin proporcionado no es v\u00e1lido", "network_error": "Se ha producido un error de red", "no_panel_online": "No se encontr\u00f3 ning\u00fan panel de control de Elmax en l\u00ednea.", - "unknown": "Error inesperado", - "unknown_error": "Error inesperado" + "unknown": "Error inesperado" }, "step": { "panels": { @@ -19,18 +17,15 @@ "panel_name": "Nombre del panel", "panel_pin": "C\u00f3digo PIN" }, - "description": "Seleccione el panel que desea controlar con esta integraci\u00f3n. Tenga en cuenta que el panel debe estar activado para poder configurarlo.", - "title": "Selecci\u00f3n de panel" + "description": "Seleccione el panel que desea controlar con esta integraci\u00f3n. Tenga en cuenta que el panel debe estar activado para poder configurarlo." }, "user": { "data": { "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Inicie sesi\u00f3n en Elmax Cloud con sus credenciales", - "title": "Inicio de sesi\u00f3n de cuenta" + "description": "Inicie sesi\u00f3n en Elmax Cloud con sus credenciales" } } - }, - "title": "Configuraci\u00f3n de Elmax Cloud" + } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/et.json b/homeassistant/components/elmax/translations/et.json index 593bd00713f..5db777a8666 100644 --- a/homeassistant/components/elmax/translations/et.json +++ b/homeassistant/components/elmax/translations/et.json @@ -4,13 +4,11 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud" }, "error": { - "bad_auth": "Vigane autentimine", "invalid_auth": "Tuvastamine nurjus", "invalid_pin": "Sisestatud pin on kehtetu", "network_error": "Ilmnes v\u00f5rgut\u00f5rge", "no_panel_online": "V\u00f5rgus olevat Elmaxi juhtpaneeli ei leitud.", - "unknown": "Ootamatu t\u00f5rge", - "unknown_error": "Ilmnes ootamatu t\u00f5rge" + "unknown": "Ootamatu t\u00f5rge" }, "step": { "panels": { @@ -19,18 +17,15 @@ "panel_name": "Paneeli nimi", "panel_pin": "PIN kood" }, - "description": "Vali millist paneeli soovid selle sidumisega juhtida. Pane t\u00e4hele, et paneel peab seadistamiseks olema SEES.", - "title": "Paneeli valik" + "description": "Vali millist paneeli soovid selle sidumisega juhtida. Pane t\u00e4hele, et paneel peab seadistamiseks olema SEES." }, "user": { "data": { "password": "Salas\u00f5na", "username": "Kasutajanimi" }, - "description": "Logi oma mandaate kasutades Elmaxi pilve sisse", - "title": "Kontole sisselogimine" + "description": "Logi oma mandaate kasutades Elmaxi pilve sisse" } } - }, - "title": "Elmaxi pilve seadistamine" + } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/fr.json b/homeassistant/components/elmax/translations/fr.json index 7fe57a8290f..8ca02f3232a 100644 --- a/homeassistant/components/elmax/translations/fr.json +++ b/homeassistant/components/elmax/translations/fr.json @@ -4,13 +4,11 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "bad_auth": "Authentification non valide", "invalid_auth": "Authentification non valide", "invalid_pin": "Le code PIN fourni n\u2019est pas valide", "network_error": "Une erreur r\u00e9seau s'est produite", "no_panel_online": "Aucun panneau de contr\u00f4le Elmax en ligne n'a \u00e9t\u00e9 trouv\u00e9.", - "unknown": "Erreur inattendue", - "unknown_error": "une erreur inattendue est apparue" + "unknown": "Erreur inattendue" }, "step": { "panels": { @@ -19,18 +17,15 @@ "panel_name": "Nom du panneau", "panel_pin": "Code PIN" }, - "description": "S\u00e9lectionnez le panneau que vous souhaitez contr\u00f4ler avec cette int\u00e9gration. Veuillez noter que le panneau doit \u00eatre allum\u00e9 pour \u00eatre configur\u00e9.", - "title": "S\u00e9lection du panneau" + "description": "S\u00e9lectionnez le panneau que vous souhaitez contr\u00f4ler avec cette int\u00e9gration. Veuillez noter que le panneau doit \u00eatre allum\u00e9 pour \u00eatre configur\u00e9." }, "user": { "data": { "password": "Mot de passe", "username": "Nom d'utilisateur" }, - "description": "Veuillez vous connecter au cloud Elmax en utilisant vos informations d'identification", - "title": "Connexion au compte" + "description": "Veuillez vous connecter au cloud Elmax en utilisant vos informations d'identification" } } - }, - "title": "Configuration d\u2019Elmax Cloud" + } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/hu.json b/homeassistant/components/elmax/translations/hu.json index eac087baf27..9a5c70beba8 100644 --- a/homeassistant/components/elmax/translations/hu.json +++ b/homeassistant/components/elmax/translations/hu.json @@ -4,13 +4,11 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "bad_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_pin": "A megadott PIN-k\u00f3d \u00e9rv\u00e9nytelen", "network_error": "H\u00e1l\u00f3zati hiba t\u00f6rt\u00e9nt", "no_panel_online": "Nem tal\u00e1lhat\u00f3 online Elmax vez\u00e9rl\u0151panel.", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", - "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { "panels": { @@ -19,18 +17,15 @@ "panel_name": "Panel neve", "panel_pin": "PIN-k\u00f3d" }, - "description": "V\u00e1lassza ki, hogy melyik panelt szeretn\u00e9 vez\u00e9relni ezzel az integr\u00e1ci\u00f3val. A panelnek bekapcsolt \u00e1llapotban kell lennie a konfigur\u00e1l\u00e1shoz.", - "title": "Panel kiv\u00e1laszt\u00e1sa" + "description": "V\u00e1lassza ki, hogy melyik panelt szeretn\u00e9 vez\u00e9relni ezzel az integr\u00e1ci\u00f3val. A panelnek bekapcsolt \u00e1llapotban kell lennie a konfigur\u00e1l\u00e1shoz." }, "user": { "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rem, jelentkezzen be az Elmax felh\u0151be a hiteles\u00edt\u0151 adataival", - "title": "Fi\u00f3k bejelentkez\u00e9s" + "description": "K\u00e9rem, jelentkezzen be az Elmax felh\u0151be a hiteles\u00edt\u0151 adataival" } } - }, - "title": "Elmax Cloud be\u00e1ll\u00edt\u00e1sa" + } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/id.json b/homeassistant/components/elmax/translations/id.json index 1df5e47679d..9b12a0d166e 100644 --- a/homeassistant/components/elmax/translations/id.json +++ b/homeassistant/components/elmax/translations/id.json @@ -4,13 +4,11 @@ "already_configured": "Perangkat sudah dikonfigurasi" }, "error": { - "bad_auth": "Autentikasi tidak valid", "invalid_auth": "Autentikasi tidak valid", "invalid_pin": "PIN yang diberikan tidak valid", "network_error": "Terjadi kesalahan jaringan", "no_panel_online": "Tidak ada panel kontrol Elmax online yang ditemukan.", - "unknown": "Kesalahan yang tidak diharapkan", - "unknown_error": "Terjadi kesalahan tak terduga" + "unknown": "Kesalahan yang tidak diharapkan" }, "step": { "panels": { @@ -19,18 +17,15 @@ "panel_name": "Nama Panel", "panel_pin": "Kode PIN" }, - "description": "Pilih panel mana yang ingin dikontrol dengan integrasi ini. Perhatikan bahwa panel harus AKTIF agar dapat dikonfigurasi.", - "title": "Pemilihan panel" + "description": "Pilih panel mana yang ingin dikontrol dengan integrasi ini. Perhatikan bahwa panel harus AKTIF agar dapat dikonfigurasi." }, "user": { "data": { "password": "Kata Sandi", "username": "Nama Pengguna" }, - "description": "Masuk ke cloud Elmax menggunakan kredensial Anda", - "title": "Akun Masuk" + "description": "Masuk ke cloud Elmax menggunakan kredensial Anda" } } - }, - "title": "Penyiapan Elmax Cloud" + } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/it.json b/homeassistant/components/elmax/translations/it.json index 07539c739bd..5db43bdb935 100644 --- a/homeassistant/components/elmax/translations/it.json +++ b/homeassistant/components/elmax/translations/it.json @@ -4,13 +4,11 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { - "bad_auth": "Autenticazione non valida", "invalid_auth": "Autenticazione non valida", "invalid_pin": "Il PIN fornito non \u00e8 valido", "network_error": "Si \u00e8 verificato un errore di rete", "no_panel_online": "Non \u00e8 stato trovato alcun pannello di controllo Elmax in linea.", - "unknown": "Errore imprevisto", - "unknown_error": "Si \u00e8 verificato un errore imprevisto" + "unknown": "Errore imprevisto" }, "step": { "panels": { @@ -19,18 +17,15 @@ "panel_name": "Nome del pannello", "panel_pin": "Codice PIN" }, - "description": "Seleziona quale pannello vuoi controllare con questa integrazione. Nota che il pannello deve essere acceso per essere configurato.", - "title": "Selezione del pannello" + "description": "Seleziona quale pannello vuoi controllare con questa integrazione. Nota che il pannello deve essere acceso per essere configurato." }, "user": { "data": { "password": "Password", "username": "Nome utente" }, - "description": "Esegui l'accesso al cloud di Elmax utilizzando le tue credenziali", - "title": "Accesso all'account" + "description": "Esegui l'accesso al cloud di Elmax utilizzando le tue credenziali" } } - }, - "title": "Configurazione di Elmax Cloud" + } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/ja.json b/homeassistant/components/elmax/translations/ja.json index ffed5307921..8730ae4791b 100644 --- a/homeassistant/components/elmax/translations/ja.json +++ b/homeassistant/components/elmax/translations/ja.json @@ -4,13 +4,11 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "bad_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_pin": "\u63d0\u4f9b\u3055\u308c\u305f\u30d4\u30f3\u304c\u7121\u52b9\u3067\u3059", "network_error": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f", "no_panel_online": "\u30aa\u30f3\u30e9\u30a4\u30f3\u306eElmax\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", - "unknown_error": "\u4e88\u671f\u305b\u306c\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f" + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { "panels": { @@ -19,18 +17,15 @@ "panel_name": "\u30d1\u30cd\u30eb\u540d", "panel_pin": "PIN\u30b3\u30fc\u30c9" }, - "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u5236\u5fa1\u3059\u308b\u30d1\u30cd\u30eb\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u3059\u308b\u306b\u306f\u3001\u30d1\u30cd\u30eb\u304c\u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u308b\u5fc5\u8981\u304c\u3042\u308b\u3053\u3068\u306b\u6ce8\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u30d1\u30cd\u30eb\u306e\u9078\u629e" + "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u5236\u5fa1\u3059\u308b\u30d1\u30cd\u30eb\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u3059\u308b\u306b\u306f\u3001\u30d1\u30cd\u30eb\u304c\u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u308b\u5fc5\u8981\u304c\u3042\u308b\u3053\u3068\u306b\u6ce8\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "description": "\u3042\u306a\u305f\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u4f7f\u7528\u3057\u3066\u3001Elmax\u30af\u30e9\u30a6\u30c9\u306b\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u30a2\u30ab\u30a6\u30f3\u30c8 \u30ed\u30b0\u30a4\u30f3" + "description": "\u3042\u306a\u305f\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u4f7f\u7528\u3057\u3066\u3001Elmax\u30af\u30e9\u30a6\u30c9\u306b\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } - }, - "title": "Elmax Cloud\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/lt.json b/homeassistant/components/elmax/translations/lt.json index d59749831b1..955dc21bf1c 100644 --- a/homeassistant/components/elmax/translations/lt.json +++ b/homeassistant/components/elmax/translations/lt.json @@ -14,10 +14,8 @@ "password": "Slapta\u017eodis", "username": "Prisijungimo vardas" }, - "description": "Prisijunkite prie \"Elmax\" debesies naudodami savo prisijungimo duomenis", - "title": "Paskyros prisijungimas" + "description": "Prisijunkite prie \"Elmax\" debesies naudodami savo prisijungimo duomenis" } } - }, - "title": "Elmax Cloud nustatymai" + } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/nl.json b/homeassistant/components/elmax/translations/nl.json index ed82cbf82c4..0a282680ce6 100644 --- a/homeassistant/components/elmax/translations/nl.json +++ b/homeassistant/components/elmax/translations/nl.json @@ -4,13 +4,11 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "bad_auth": "Ongeldige authenticatie", "invalid_auth": "Ongeldige authenticatie", "invalid_pin": "De opgegeven pincode is ongeldig", "network_error": "Er is een netwerkfout opgetreden", "no_panel_online": "Er is geen online Elmax-controlepaneel gevonden.", - "unknown": "Onverwachte fout", - "unknown_error": "Een onbekende fout is opgetreden." + "unknown": "Onverwachte fout" }, "step": { "panels": { @@ -19,18 +17,15 @@ "panel_name": "Paneelnaam:", "panel_pin": "PIN Code" }, - "description": "Selecteer welk paneel je wilt bedienen met deze integratie. Houd er rekening mee dat het paneel AAN moet staan om te kunnen worden geconfigureerd.", - "title": "Paneelselectie" + "description": "Selecteer welk paneel je wilt bedienen met deze integratie. Houd er rekening mee dat het paneel AAN moet staan om te kunnen worden geconfigureerd." }, "user": { "data": { "password": "Wachtwoord", "username": "Gebruikersnaam" }, - "description": "Log in op de Elmax-cloud met uw inloggegevens", - "title": "Account Login" + "description": "Log in op de Elmax-cloud met uw inloggegevens" } } - }, - "title": "Elmax Cloud Setup" + } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/no.json b/homeassistant/components/elmax/translations/no.json index 35d8bb409f5..ef806106848 100644 --- a/homeassistant/components/elmax/translations/no.json +++ b/homeassistant/components/elmax/translations/no.json @@ -4,13 +4,11 @@ "already_configured": "Enheten er allerede konfigurert" }, "error": { - "bad_auth": "Ugyldig godkjenning", "invalid_auth": "Ugyldig godkjenning", "invalid_pin": "Den angitte PIN-koden er ugyldig", "network_error": "Det oppstod en nettverksfeil", "no_panel_online": "Ingen online Elmax kontrollpanel ble funnet.", - "unknown": "Uventet feil", - "unknown_error": "Uventet feil" + "unknown": "Uventet feil" }, "step": { "panels": { @@ -19,18 +17,15 @@ "panel_name": "Navn p\u00e5 panel", "panel_pin": "PIN kode" }, - "description": "Velg hvilket panel du vil kontrollere med denne integrasjonen. V\u00e6r oppmerksom p\u00e5 at panelet m\u00e5 v\u00e6re P\u00c5 for \u00e5 kunne konfigureres.", - "title": "Valg av panel" + "description": "Velg hvilket panel du vil kontrollere med denne integrasjonen. V\u00e6r oppmerksom p\u00e5 at panelet m\u00e5 v\u00e6re P\u00c5 for \u00e5 kunne konfigureres." }, "user": { "data": { "password": "Passord", "username": "Brukernavn" }, - "description": "Logg p\u00e5 Elmax-skyen ved \u00e5 bruke legitimasjonen din", - "title": "P\u00e5logging til konto" + "description": "Logg p\u00e5 Elmax-skyen ved \u00e5 bruke legitimasjonen din" } } - }, - "title": "Elmax Cloud Setup" + } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/pl.json b/homeassistant/components/elmax/translations/pl.json index d5776432f5f..a66a1e810cd 100644 --- a/homeassistant/components/elmax/translations/pl.json +++ b/homeassistant/components/elmax/translations/pl.json @@ -4,13 +4,11 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { - "bad_auth": "Niepoprawne uwierzytelnienie", "invalid_auth": "Niepoprawne uwierzytelnienie", "invalid_pin": "Podany kod PIN jest nieprawid\u0142owy", "network_error": "Wyst\u0105pi\u0142 b\u0142\u0105d sieci.", "no_panel_online": "Nie znaleziono panelu sterowania online Elmax.", - "unknown": "Nieoczekiwany b\u0142\u0105d", - "unknown_error": "Nieoczekiwany b\u0142\u0105d" + "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "panels": { @@ -19,18 +17,15 @@ "panel_name": "Nazwa panelu", "panel_pin": "Kod PIN" }, - "description": "Wybierz panel, kt\u00f3rym chcesz sterowa\u0107 za pomoc\u0105 tej integracji. Nale\u017cy pami\u0119ta\u0107, \u017ce panel musi by\u0107 w\u0142\u0105czony, aby mo\u017cna by\u0142o go skonfigurowa\u0107.", - "title": "Wyb\u00f3r panelu" + "description": "Wybierz panel, kt\u00f3rym chcesz sterowa\u0107 za pomoc\u0105 tej integracji. Nale\u017cy pami\u0119ta\u0107, \u017ce panel musi by\u0107 w\u0142\u0105czony, aby mo\u017cna by\u0142o go skonfigurowa\u0107." }, "user": { "data": { "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" }, - "description": "Prosz\u0119 zalogowa\u0107 si\u0119 do chmury Elmax za pomoc\u0105 swoich danych uwierzytelniaj\u0105cych", - "title": "Logowanie do konta" + "description": "Prosz\u0119 zalogowa\u0107 si\u0119 do chmury Elmax za pomoc\u0105 swoich danych uwierzytelniaj\u0105cych" } } - }, - "title": "Konfiguracja chmury Elmax" + } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/pt-BR.json b/homeassistant/components/elmax/translations/pt-BR.json index 9db13c83fbc..c68583bd431 100644 --- a/homeassistant/components/elmax/translations/pt-BR.json +++ b/homeassistant/components/elmax/translations/pt-BR.json @@ -4,13 +4,11 @@ "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "bad_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_pin": "O C\u00f3digo PIN fornecido \u00e9 inv\u00e1lido", "network_error": "Ocorreu um erro de rede", "no_panel_online": "Nenhum painel de controle on-line Elmax foi encontrado.", - "unknown": "Erro inesperado", - "unknown_error": "Erro inesperado" + "unknown": "Erro inesperado" }, "step": { "panels": { @@ -19,18 +17,15 @@ "panel_name": "Nome do painel", "panel_pin": "C\u00f3digo PIN" }, - "description": "Selecione qual painel voc\u00ea gostaria de controlar com esta integra\u00e7\u00e3o. Observe que o painel deve estar LIGADO para ser configurado.", - "title": "Sele\u00e7\u00e3o do painel" + "description": "Selecione qual painel voc\u00ea gostaria de controlar com esta integra\u00e7\u00e3o. Observe que o painel deve estar LIGADO para ser configurado." }, "user": { "data": { "password": "Senha", "username": "Usu\u00e1rio" }, - "description": "Fa\u00e7a login na nuvem Elmax usando suas credenciais", - "title": "Login da conta" + "description": "Fa\u00e7a login na nuvem Elmax usando suas credenciais" } } - }, - "title": "Configura\u00e7\u00e3o de nuvem Elmax" + } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/ru.json b/homeassistant/components/elmax/translations/ru.json index d5b3483ad32..35dc047efbb 100644 --- a/homeassistant/components/elmax/translations/ru.json +++ b/homeassistant/components/elmax/translations/ru.json @@ -4,13 +4,11 @@ "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": { - "bad_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.", "invalid_pin": "\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 PIN-\u043a\u043e\u0434 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", "network_error": "\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u0432 \u0441\u0435\u0442\u0438.", "no_panel_online": "\u041f\u0430\u043d\u0435\u043b\u044c \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f Elmax \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", - "unknown_error": "\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { "panels": { @@ -19,18 +17,15 @@ "panel_name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u0430\u043d\u0435\u043b\u0438", "panel_pin": "PIN-\u043a\u043e\u0434" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u043d\u0435\u043b\u044c, \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438. \u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0430\u043d\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430.", - "title": "\u0412\u044b\u0431\u043e\u0440 \u043f\u0430\u043d\u0435\u043b\u0438" + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u043d\u0435\u043b\u044c, \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438. \u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0430\u043d\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u0432\u043a\u043b\u044e\u0447\u0435\u043d\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\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 Elmax Cloud, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0441\u0432\u043e\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", - "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f" + "description": "\u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 Elmax Cloud, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0441\u0432\u043e\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." } } - }, - "title": "Elmax Cloud" + } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/sk.json b/homeassistant/components/elmax/translations/sk.json index ae37c2ed275..5ada995aa6e 100644 --- a/homeassistant/components/elmax/translations/sk.json +++ b/homeassistant/components/elmax/translations/sk.json @@ -1,8 +1,7 @@ { "config": { "error": { - "invalid_auth": "Neplatn\u00e9 overenie", - "unknown_error": "Vyskytla sa neo\u010dak\u00e1van\u00e1 chyba" + "invalid_auth": "Neplatn\u00e9 overenie" } } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/tr.json b/homeassistant/components/elmax/translations/tr.json index 5bcea9da388..dc43e42ab14 100644 --- a/homeassistant/components/elmax/translations/tr.json +++ b/homeassistant/components/elmax/translations/tr.json @@ -4,13 +4,11 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { - "bad_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "invalid_pin": "Sa\u011flanan pin ge\u00e7ersiz", "network_error": "Bir a\u011f hatas\u0131 olu\u015ftu", "no_panel_online": "\u00c7evrimi\u00e7i Elmax kontrol paneli bulunamad\u0131.", - "unknown": "Beklenmeyen hata", - "unknown_error": "Beklenmeyen bir hata olu\u015ftu" + "unknown": "Beklenmeyen hata" }, "step": { "panels": { @@ -19,18 +17,15 @@ "panel_name": "Panel Ad\u0131", "panel_pin": "PIN Kodu" }, - "description": "Bu entegrasyon ile hangi paneli kontrol etmek istedi\u011finizi se\u00e7in. L\u00fctfen konfig\u00fcre edilebilmesi i\u00e7in panelin A\u00c7IK olmas\u0131 gerekti\u011fini unutmay\u0131n.", - "title": "Panel se\u00e7imi" + "description": "Bu entegrasyon ile hangi paneli kontrol etmek istedi\u011finizi se\u00e7in. L\u00fctfen konfig\u00fcre edilebilmesi i\u00e7in panelin A\u00c7IK olmas\u0131 gerekti\u011fini unutmay\u0131n." }, "user": { "data": { "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "L\u00fctfen kimlik bilgilerinizi kullanarak Elmax bulutuna giri\u015f yap\u0131n", - "title": "Hesap Giri\u015fi" + "description": "L\u00fctfen kimlik bilgilerinizi kullanarak Elmax bulutuna giri\u015f yap\u0131n" } } - }, - "title": "Elmax Bulut Kurulumu" + } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/zh-Hans.json b/homeassistant/components/elmax/translations/zh-Hans.json index 0d0c87c6a5b..b07f985a2c7 100644 --- a/homeassistant/components/elmax/translations/zh-Hans.json +++ b/homeassistant/components/elmax/translations/zh-Hans.json @@ -10,8 +10,7 @@ "data": { "password": "\u5bc6\u7801", "username": "\u7528\u6237\u540d" - }, - "title": "\u8d26\u6237\u767b\u5f55" + } } } } diff --git a/homeassistant/components/elmax/translations/zh-Hant.json b/homeassistant/components/elmax/translations/zh-Hant.json index b1fc8886d1c..c67fcddaa7a 100644 --- a/homeassistant/components/elmax/translations/zh-Hant.json +++ b/homeassistant/components/elmax/translations/zh-Hant.json @@ -4,13 +4,11 @@ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "bad_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "invalid_pin": "\u6240\u63d0\u4f9b\u7684 PIN \u7121\u6548\u3002", "network_error": "\u767c\u751f\u7db2\u8def\u932f\u8aa4", "no_panel_online": "\u627e\u4e0d\u5230 Elmax \u63a7\u5236\u9762\u677f\u3002", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4", - "unknown_error": "\u767c\u751f\u672a\u77e5\u932f\u8aa4" + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { "panels": { @@ -19,18 +17,15 @@ "panel_name": "\u9762\u677f\u540d\u7a31", "panel_pin": "PIN \u78bc" }, - "description": "\u9078\u64c7\u6574\u5408\u6240\u8981\u4f7f\u7528\u7684\u9762\u677f\u3002\u8acb\u6ce8\u610f\u3001\u9762\u677f\u5fc5\u9808\u70ba\u958b\u555f\u72c0\u614b\u65b9\u80fd\u9032\u884c\u8a2d\u5b9a\u3002", - "title": "\u9078\u64c7\u9762\u677f" + "description": "\u9078\u64c7\u6574\u5408\u6240\u8981\u4f7f\u7528\u7684\u9762\u677f\u3002\u8acb\u6ce8\u610f\u3001\u9762\u677f\u5fc5\u9808\u70ba\u958b\u555f\u72c0\u614b\u65b9\u80fd\u9032\u884c\u8a2d\u5b9a\u3002" }, "user": { "data": { "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8acb\u4f7f\u7528\u6191\u8b49\u4ee5\u767b\u5165 Elmax \u96f2\u670d\u52d9", - "title": "\u767b\u5165\u5e33\u865f" + "description": "\u8acb\u4f7f\u7528\u6191\u8b49\u4ee5\u767b\u5165 Elmax \u96f2\u670d\u52d9" } } - }, - "title": "Elmax \u96f2\u670d\u52d9\u8a2d\u5b9a" + } } \ No newline at end of file diff --git a/homeassistant/components/fan/translations/ca.json b/homeassistant/components/fan/translations/ca.json index 206933b1c9a..c253be819c9 100644 --- a/homeassistant/components/fan/translations/ca.json +++ b/homeassistant/components/fan/translations/ca.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} s'enc\u00e9n o s'apaga", - "toggled": "{entity_name} s'enc\u00e9n o s'apaga", "turned_off": "{entity_name} s'ha apagat", "turned_on": "{entity_name} s'ha enc\u00e8s" } diff --git a/homeassistant/components/fan/translations/cs.json b/homeassistant/components/fan/translations/cs.json index 98336d5cfbe..9a2f89e5caa 100644 --- a/homeassistant/components/fan/translations/cs.json +++ b/homeassistant/components/fan/translations/cs.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "P\u0159epnout {entity_name}", "turn_off": "Vypnout {entity_name}", "turn_on": "Zapnout {entity_name}" }, @@ -9,7 +10,6 @@ "is_on": "{entity_name} je zapnuto" }, "trigger_type": { - "toggled": "{entity_name} zapnuto nebo vypnuto", "turned_off": "{entity_name} bylo vypnuto", "turned_on": "{entity_name} bylo zapnuto" } diff --git a/homeassistant/components/fan/translations/de.json b/homeassistant/components/fan/translations/de.json index 5048a576bee..6f27d79ad81 100644 --- a/homeassistant/components/fan/translations/de.json +++ b/homeassistant/components/fan/translations/de.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} ein- oder ausgeschaltet", - "toggled": "{entity_name} ein- oder ausgeschaltet", "turned_off": "{entity_name} ausgeschaltet", "turned_on": "{entity_name} eingeschaltet" } diff --git a/homeassistant/components/fan/translations/el.json b/homeassistant/components/fan/translations/el.json index b0ff671f737..78e78722bbc 100644 --- a/homeassistant/components/fan/translations/el.json +++ b/homeassistant/components/fan/translations/el.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "\u03a4\u03bf {entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5 \u03ae \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", - "toggled": "{entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5 \u03ae \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", "turned_off": "{entity_name} \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", "turned_on": "{entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5" } diff --git a/homeassistant/components/fan/translations/en.json b/homeassistant/components/fan/translations/en.json index 55feb3dad9f..94335a91799 100644 --- a/homeassistant/components/fan/translations/en.json +++ b/homeassistant/components/fan/translations/en.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} turned on or off", - "toggled": "{entity_name} turned on or off", "turned_off": "{entity_name} turned off", "turned_on": "{entity_name} turned on" } diff --git a/homeassistant/components/fan/translations/es.json b/homeassistant/components/fan/translations/es.json index 85cc347f82d..b66ca0b0256 100644 --- a/homeassistant/components/fan/translations/es.json +++ b/homeassistant/components/fan/translations/es.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} activado o desactivado", - "toggled": "{entity_name} activado o desactivado", "turned_off": "{entity_name} desactivado", "turned_on": "{entity_name} activado" } diff --git a/homeassistant/components/fan/translations/et.json b/homeassistant/components/fan/translations/et.json index d168ca75158..23066054d37 100644 --- a/homeassistant/components/fan/translations/et.json +++ b/homeassistant/components/fan/translations/et.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} l\u00fclitus sisse v\u00f5i v\u00e4lja", - "toggled": "{entity_name} l\u00fclitus sisse v\u00f5i v\u00e4lja", "turned_off": "{entity_name} l\u00fclitus v\u00e4lja", "turned_on": "{entity_name} l\u00fclitus sisse" } diff --git a/homeassistant/components/fan/translations/fr.json b/homeassistant/components/fan/translations/fr.json index 1ff0ed5eea9..27615a525dd 100644 --- a/homeassistant/components/fan/translations/fr.json +++ b/homeassistant/components/fan/translations/fr.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} allum\u00e9 ou \u00e9teint", - "toggled": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", "turned_off": "{entity_name} est \u00e9teint", "turned_on": "{entity_name} allum\u00e9" } diff --git a/homeassistant/components/fan/translations/he.json b/homeassistant/components/fan/translations/he.json index a97b300060a..5d1a6aea1c3 100644 --- a/homeassistant/components/fan/translations/he.json +++ b/homeassistant/components/fan/translations/he.json @@ -10,7 +10,6 @@ }, "trigger_type": { "changed_states": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc \u05d0\u05d5 \u05db\u05d5\u05d1\u05d4", - "toggled": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc \u05d0\u05d5 \u05db\u05d5\u05d1\u05d4", "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4", "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc" } diff --git a/homeassistant/components/fan/translations/hu.json b/homeassistant/components/fan/translations/hu.json index e7ef69d8800..626004a9f69 100644 --- a/homeassistant/components/fan/translations/hu.json +++ b/homeassistant/components/fan/translations/hu.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} be- vagy kikapcsolt", - "toggled": "{entity_name} \u00e1tkapcsolt", "turned_off": "{entity_name} ki lett kapcsolva", "turned_on": "{entity_name} be lett kapcsolva" } diff --git a/homeassistant/components/fan/translations/id.json b/homeassistant/components/fan/translations/id.json index 85fa77539c3..b87c2b5a2bc 100644 --- a/homeassistant/components/fan/translations/id.json +++ b/homeassistant/components/fan/translations/id.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} diaktifkan atau dinonaktifkan", - "toggled": "{entity_name} diaktifkan atau dinonaktifkan", "turned_off": "{entity_name} dimatikan", "turned_on": "{entity_name} dinyalakan" } diff --git a/homeassistant/components/fan/translations/it.json b/homeassistant/components/fan/translations/it.json index c74f477ea73..af4aff2691d 100644 --- a/homeassistant/components/fan/translations/it.json +++ b/homeassistant/components/fan/translations/it.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} attivata o disattivata", - "toggled": "{entity_name} attiva o disattiva", "turned_off": "{entity_name} disattivato", "turned_on": "{entity_name} attivato" } diff --git a/homeassistant/components/fan/translations/ja.json b/homeassistant/components/fan/translations/ja.json index 75d528404a2..e3fe8e8ba1a 100644 --- a/homeassistant/components/fan/translations/ja.json +++ b/homeassistant/components/fan/translations/ja.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} \u304c\u30aa\u30f3\u307e\u305f\u306f\u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", - "toggled": "{entity_name} \u304c\u30aa\u30f3\u307e\u305f\u306f\u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059" } diff --git a/homeassistant/components/fan/translations/nl.json b/homeassistant/components/fan/translations/nl.json index bdc2f64195b..d24f439ff16 100644 --- a/homeassistant/components/fan/translations/nl.json +++ b/homeassistant/components/fan/translations/nl.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} in- of uitgeschakeld", - "toggled": "{entity_name} in- of uitgeschakeld", "turned_off": "{entity_name} uitgeschakeld", "turned_on": "{entity_name} ingeschakeld" } diff --git a/homeassistant/components/fan/translations/no.json b/homeassistant/components/fan/translations/no.json index d9fdf8c5ea5..0c3649ecbc5 100644 --- a/homeassistant/components/fan/translations/no.json +++ b/homeassistant/components/fan/translations/no.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} sl\u00e5tt p\u00e5 eller av", - "toggled": "{entity_name} sl\u00e5tt p\u00e5 eller av", "turned_off": "{entity_name} sl\u00e5tt av", "turned_on": "{entity_name} sl\u00e5tt p\u00e5" } diff --git a/homeassistant/components/fan/translations/pl.json b/homeassistant/components/fan/translations/pl.json index 9cfc6fa9b28..817dbdde339 100644 --- a/homeassistant/components/fan/translations/pl.json +++ b/homeassistant/components/fan/translations/pl.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", - "toggled": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}" } diff --git a/homeassistant/components/fan/translations/pt-BR.json b/homeassistant/components/fan/translations/pt-BR.json index 14eafd36f40..8ebc5b2c73f 100644 --- a/homeassistant/components/fan/translations/pt-BR.json +++ b/homeassistant/components/fan/translations/pt-BR.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} ligado ou desligado", - "toggled": "{entity_name} ligado ou desligado", "turned_off": "{entity_name} for desligado", "turned_on": "{entity_name} for ligado" } diff --git a/homeassistant/components/fan/translations/ru.json b/homeassistant/components/fan/translations/ru.json index 5a4571f2dad..fcf8b6ec4a5 100644 --- a/homeassistant/components/fan/translations/ru.json +++ b/homeassistant/components/fan/translations/ru.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0438\u043b\u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", - "toggled": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0438\u043b\u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" } diff --git a/homeassistant/components/fan/translations/sv.json b/homeassistant/components/fan/translations/sv.json index fd1fec6cdfc..dd1aaad4052 100644 --- a/homeassistant/components/fan/translations/sv.json +++ b/homeassistant/components/fan/translations/sv.json @@ -9,7 +9,6 @@ "is_on": "{entity_name} \u00e4r p\u00e5" }, "trigger_type": { - "toggled": "{entity_name} slogs p\u00e5 eller av", "turned_off": "{entity_name} st\u00e4ngdes av", "turned_on": "{entity_name} aktiverades" } diff --git a/homeassistant/components/fan/translations/tr.json b/homeassistant/components/fan/translations/tr.json index b4f7a9b5291..5c06cddf5ee 100644 --- a/homeassistant/components/fan/translations/tr.json +++ b/homeassistant/components/fan/translations/tr.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} a\u00e7\u0131ld\u0131 veya kapat\u0131ld\u0131", - "toggled": "{entity_name} a\u00e7\u0131ld\u0131 veya kapat\u0131ld\u0131", "turned_off": "{entity_name} kapat\u0131ld\u0131", "turned_on": "{entity_name} a\u00e7\u0131ld\u0131" } diff --git a/homeassistant/components/fan/translations/zh-Hans.json b/homeassistant/components/fan/translations/zh-Hans.json index 082133913bc..13b6917f4ad 100644 --- a/homeassistant/components/fan/translations/zh-Hans.json +++ b/homeassistant/components/fan/translations/zh-Hans.json @@ -9,7 +9,6 @@ "is_on": "{entity_name} \u5df2\u5f00\u542f" }, "trigger_type": { - "toggled": "{entity_name} \u88ab\u6253\u5f00\u6216\u5173\u95ed", "turned_off": "{entity_name} \u88ab\u5173\u95ed", "turned_on": "{entity_name} \u88ab\u6253\u5f00" } diff --git a/homeassistant/components/fan/translations/zh-Hant.json b/homeassistant/components/fan/translations/zh-Hant.json index ba68f7c5158..8151ee76a77 100644 --- a/homeassistant/components/fan/translations/zh-Hant.json +++ b/homeassistant/components/fan/translations/zh-Hant.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name}\u5df2\u958b\u555f\u6216\u95dc\u9589", - "toggled": "{entity_name}\u5df2\u958b\u555f\u6216\u95dc\u9589", "turned_off": "{entity_name}\u5df2\u95dc\u9589", "turned_on": "{entity_name}\u5df2\u958b\u555f" } diff --git a/homeassistant/components/fivem/translations/ca.json b/homeassistant/components/fivem/translations/ca.json index 8e2a192a18c..ae322b4748c 100644 --- a/homeassistant/components/fivem/translations/ca.json +++ b/homeassistant/components/fivem/translations/ca.json @@ -6,7 +6,6 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3. Comprova l'amfitri\u00f3 i el port i torna-ho a provar. Assegurat que est\u00e0s utilitzant la versi\u00f3 del servidor FiveM m\u00e9s recent.", "invalid_game_name": "L'API del joc al qual est\u00e0s intentant connectar-te no \u00e9s d'un joc FiveM.", - "invalid_gamename": "L'API del joc al qual est\u00e0s intentant connectar-te no \u00e9s d'un joc FiveM.", "unknown_error": "Error inesperat" }, "step": { diff --git a/homeassistant/components/fivem/translations/de.json b/homeassistant/components/fivem/translations/de.json index 5c6852a126e..bb3b3439cbf 100644 --- a/homeassistant/components/fivem/translations/de.json +++ b/homeassistant/components/fivem/translations/de.json @@ -6,7 +6,6 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen. Bitte \u00fcberpr\u00fcfe den Host und den Port und versuche es erneut. Vergewissere dich auch, dass du den neuesten FiveM-Server verwendest.", "invalid_game_name": "Die API des Spiels, mit dem du dich verbinden willst, ist kein FiveM-Spiel.", - "invalid_gamename": "Die API des Spiels, mit dem du dich verbinden willst, ist kein FiveM-Spiel.", "unknown_error": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/fivem/translations/el.json b/homeassistant/components/fivem/translations/el.json index 29e798361ff..2f038de5ba9 100644 --- a/homeassistant/components/fivem/translations/el.json +++ b/homeassistant/components/fivem/translations/el.json @@ -6,7 +6,6 @@ "error": { "cannot_connect": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03b5\u03c0\u03af\u03c3\u03b7\u03c2 \u03cc\u03c4\u03b9 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c0\u03b9\u03bf \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae FiveM.", "invalid_game_name": "\u03a4\u03bf api \u03c4\u03bf\u03c5 \u03c0\u03b1\u03b9\u03c7\u03bd\u03b9\u03b4\u03b9\u03bf\u03cd \u03c3\u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03b1\u03b9\u03c7\u03bd\u03af\u03b4\u03b9 FiveM.", - "invalid_gamename": "\u03a4\u03bf api \u03c4\u03bf\u03c5 \u03c0\u03b1\u03b9\u03c7\u03bd\u03b9\u03b4\u03b9\u03bf\u03cd \u03c3\u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03b1\u03b9\u03c7\u03bd\u03af\u03b4\u03b9 FiveM.", "unknown_error": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { diff --git a/homeassistant/components/fivem/translations/en.json b/homeassistant/components/fivem/translations/en.json index e07c0666e24..8c4f7a54156 100644 --- a/homeassistant/components/fivem/translations/en.json +++ b/homeassistant/components/fivem/translations/en.json @@ -6,7 +6,6 @@ "error": { "cannot_connect": "Failed to connect. Please check the host and port and try again. Also ensure that you are running the latest FiveM server.", "invalid_game_name": "The api of the game you are trying to connect to is not a FiveM game.", - "invalid_gamename": "The api of the game you are trying to connect to is not a FiveM game.", "unknown_error": "Unexpected error" }, "step": { diff --git a/homeassistant/components/fivem/translations/es.json b/homeassistant/components/fivem/translations/es.json index 873119cf4b9..273435a4e5e 100644 --- a/homeassistant/components/fivem/translations/es.json +++ b/homeassistant/components/fivem/translations/es.json @@ -6,7 +6,6 @@ "error": { "cannot_connect": "Error al conectarse. Compruebe el host y el puerto e int\u00e9ntelo de nuevo. Aseg\u00farese tambi\u00e9n de que est\u00e1 ejecutando el servidor FiveM m\u00e1s reciente.", "invalid_game_name": "La API del juego al que intentas conectarte no es un juego de FiveM.", - "invalid_gamename": "La API del juego al que intentas conectarte no es un juego de FiveM.", "unknown_error": "Error inesperado" }, "step": { diff --git a/homeassistant/components/fivem/translations/et.json b/homeassistant/components/fivem/translations/et.json index 0ca491bd232..49edde59e1f 100644 --- a/homeassistant/components/fivem/translations/et.json +++ b/homeassistant/components/fivem/translations/et.json @@ -6,7 +6,6 @@ "error": { "cannot_connect": "\u00dchendamine eba\u00f5nnestus. Kontrolli hosti ja porti ning proovi uuesti. Veendu, et kasutad uusimat FiveM-i serverit.", "invalid_game_name": "M\u00e4ngu API, millega proovid \u00fchendust luua, ei ole FiveM-m\u00e4ng.", - "invalid_gamename": "M\u00e4ngu API, millega proovid \u00fchendust luua, ei ole FiveM-m\u00e4ng.", "unknown_error": "Ootamatu t\u00f5rge" }, "step": { diff --git a/homeassistant/components/fivem/translations/fr.json b/homeassistant/components/fivem/translations/fr.json index 4cd16be65a3..be8254a74de 100644 --- a/homeassistant/components/fivem/translations/fr.json +++ b/homeassistant/components/fivem/translations/fr.json @@ -6,7 +6,6 @@ "error": { "cannot_connect": "\u00c9chec de connexion. Veuillez v\u00e9rifier l'h\u00f4te et le port et r\u00e9essayer. Assurez-vous \u00e9galement que vous utilisez le dernier serveur FiveM.", "invalid_game_name": "L'API du jeu auquel vous essayez de vous connecter n'est pas un jeu FiveM.", - "invalid_gamename": "L\u2019API du jeu auquel vous essayez de vous connecter n\u2019est pas un jeu FiveM.", "unknown_error": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/fivem/translations/hu.json b/homeassistant/components/fivem/translations/hu.json index 4e56dc3c17f..d29d0c9a802 100644 --- a/homeassistant/components/fivem/translations/hu.json +++ b/homeassistant/components/fivem/translations/hu.json @@ -6,7 +6,6 @@ "error": { "cannot_connect": "Nem siker\u00fclt csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a c\u00edmet \u00e9s a portot, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra. Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l is, hogy a leg\u00fajabb FiveM szervert futtatja.", "invalid_game_name": "A j\u00e1t\u00e9k API-ja, amelyhez csatlakozni pr\u00f3b\u00e1l, nem FiveM.", - "invalid_gamename": "A j\u00e1t\u00e9k API-ja, amelyhez csatlakozni pr\u00f3b\u00e1l, nem FiveM.", "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { diff --git a/homeassistant/components/fivem/translations/id.json b/homeassistant/components/fivem/translations/id.json index 3cf44f86f5d..95e9e4decde 100644 --- a/homeassistant/components/fivem/translations/id.json +++ b/homeassistant/components/fivem/translations/id.json @@ -6,7 +6,6 @@ "error": { "cannot_connect": "Gagal terhubung ke server. Periksa host dan port lalu coba lagi. Pastikan juga Anda menjalankan server FiveM terbaru.", "invalid_game_name": "API dari permainan yang Anda coba hubungkan bukanlah game FiveM.", - "invalid_gamename": "API dari permainan yang Anda coba hubungkan bukanlah game FiveM.", "unknown_error": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/fivem/translations/it.json b/homeassistant/components/fivem/translations/it.json index 0128d1fbeca..e14e8492835 100644 --- a/homeassistant/components/fivem/translations/it.json +++ b/homeassistant/components/fivem/translations/it.json @@ -6,7 +6,6 @@ "error": { "cannot_connect": "Connessione non riuscita. Controlla l'host e la porta e riprova. Assicurati inoltre di eseguire il server FiveM pi\u00f9 recente.", "invalid_game_name": "L'API del gioco a cui stai tentando di connetterti non \u00e8 un gioco FiveM.", - "invalid_gamename": "L'API del gioco a cui stai tentando di connetterti non \u00e8 un gioco FiveM.", "unknown_error": "Errore imprevisto" }, "step": { diff --git a/homeassistant/components/fivem/translations/ja.json b/homeassistant/components/fivem/translations/ja.json index eb398cccff4..6ac9af80f2c 100644 --- a/homeassistant/components/fivem/translations/ja.json +++ b/homeassistant/components/fivem/translations/ja.json @@ -6,7 +6,6 @@ "error": { "cannot_connect": "\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30db\u30b9\u30c8\u3068\u30dd\u30fc\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u307e\u305f\u3001\u6700\u65b0\u306eFiveM\u30b5\u30fc\u30d0\u30fc\u3092\u5b9f\u884c\u3057\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "invalid_game_name": "\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u30b2\u30fc\u30e0\u306eAPI\u306f\u3001FiveM\u306e\u30b2\u30fc\u30e0\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002", - "invalid_gamename": "\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u30b2\u30fc\u30e0\u306eAPI\u306f\u3001FiveM\u306e\u30b2\u30fc\u30e0\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002", "unknown_error": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/fivem/translations/nl.json b/homeassistant/components/fivem/translations/nl.json index 599bcbc771e..cac4b7ed12f 100644 --- a/homeassistant/components/fivem/translations/nl.json +++ b/homeassistant/components/fivem/translations/nl.json @@ -6,7 +6,6 @@ "error": { "cannot_connect": "Kan geen verbinding maken. Controleer de host en poort en probeer het opnieuw. Zorg er ook voor dat u de nieuwste FiveM-server gebruikt.", "invalid_game_name": "De api van het spel waarmee je probeert te verbinden is geen FiveM spel.", - "invalid_gamename": "De api van het spel waarmee je probeert te verbinden is geen FiveM spel.", "unknown_error": "Onverwachte fout" }, "step": { diff --git a/homeassistant/components/fivem/translations/no.json b/homeassistant/components/fivem/translations/no.json index ac292c10b64..b609f8ad64f 100644 --- a/homeassistant/components/fivem/translations/no.json +++ b/homeassistant/components/fivem/translations/no.json @@ -6,7 +6,6 @@ "error": { "cannot_connect": "Tilkobling mislyktes. Kontroller verten og porten og pr\u00f8v igjen. S\u00f8rg ogs\u00e5 for at du kj\u00f8rer den nyeste FiveM-serveren.", "invalid_game_name": "API-et til spillet du pr\u00f8ver \u00e5 koble til er ikke et FiveM-spill.", - "invalid_gamename": "API-et til spillet du pr\u00f8ver \u00e5 koble til er ikke et FiveM-spill.", "unknown_error": "Uventet feil" }, "step": { diff --git a/homeassistant/components/fivem/translations/pl.json b/homeassistant/components/fivem/translations/pl.json index 420ebca7463..f60bd6c23df 100644 --- a/homeassistant/components/fivem/translations/pl.json +++ b/homeassistant/components/fivem/translations/pl.json @@ -6,7 +6,6 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia. Sprawd\u017a adres hosta oraz port i spr\u00f3buj ponownie. Upewnij si\u0119, \u017ce posiadasz najnowsz\u0105 wersj\u0119 serwera FiveM.", "invalid_game_name": "API gry, do kt\u00f3rej pr\u00f3bujesz si\u0119 po\u0142\u0105czy\u0107, nie jest gr\u0105 FiveM.", - "invalid_gamename": "API gry, do kt\u00f3rej pr\u00f3bujesz si\u0119 po\u0142\u0105czy\u0107, nie jest gr\u0105 FiveM.", "unknown_error": "Nieoczekiwany b\u0142\u0105d" }, "step": { diff --git a/homeassistant/components/fivem/translations/pt-BR.json b/homeassistant/components/fivem/translations/pt-BR.json index b576192718f..af5980a90f1 100644 --- a/homeassistant/components/fivem/translations/pt-BR.json +++ b/homeassistant/components/fivem/translations/pt-BR.json @@ -6,7 +6,6 @@ "error": { "cannot_connect": "Falha ao se conectar. Verifique o host e a porta e tente novamente. Verifique tamb\u00e9m se voc\u00ea est\u00e1 executando o servidor FiveM mais recente.", "invalid_game_name": "A API do jogo ao qual voc\u00ea est\u00e1 tentando se conectar n\u00e3o \u00e9 um jogo FiveM.", - "invalid_gamename": "A API do jogo ao qual voc\u00ea est\u00e1 tentando se conectar n\u00e3o \u00e9 um jogo FiveM.", "unknown_error": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/fivem/translations/ru.json b/homeassistant/components/fivem/translations/ru.json index c6da81663ca..2f2d3d3be73 100644 --- a/homeassistant/components/fivem/translations/ru.json +++ b/homeassistant/components/fivem/translations/ru.json @@ -6,7 +6,6 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442 \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443. \u0422\u0430\u043a\u0436\u0435 \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u044e\u044e \u0432\u0435\u0440\u0441\u0438\u044e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 FiveM.", "invalid_game_name": "API \u0438\u0433\u0440\u044b, \u043a \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0412\u044b \u043f\u044b\u0442\u0430\u0435\u0442\u0435\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0438\u0433\u0440\u043e\u0439 FiveM.", - "invalid_gamename": "API \u0438\u0433\u0440\u044b, \u043a \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0412\u044b \u043f\u044b\u0442\u0430\u0435\u0442\u0435\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0438\u0433\u0440\u043e\u0439 FiveM.", "unknown_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/fivem/translations/tr.json b/homeassistant/components/fivem/translations/tr.json index 46921dd33c0..f6bfc9cf274 100644 --- a/homeassistant/components/fivem/translations/tr.json +++ b/homeassistant/components/fivem/translations/tr.json @@ -6,7 +6,6 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131. L\u00fctfen ana bilgisayar\u0131 ve ba\u011flant\u0131 noktas\u0131n\u0131 kontrol edin ve tekrar deneyin. Ayr\u0131ca en son FiveM sunucusunu \u00e7al\u0131\u015ft\u0131rd\u0131\u011f\u0131n\u0131zdan emin olun.", "invalid_game_name": "Ba\u011flanmaya \u00e7al\u0131\u015ft\u0131\u011f\u0131n\u0131z oyunun api'si bir FiveM oyunu de\u011fil.", - "invalid_gamename": "Ba\u011flanmaya \u00e7al\u0131\u015ft\u0131\u011f\u0131n\u0131z oyunun api'si bir FiveM oyunu de\u011fil.", "unknown_error": "Beklenmeyen hata" }, "step": { diff --git a/homeassistant/components/fivem/translations/zh-Hant.json b/homeassistant/components/fivem/translations/zh-Hant.json index 3b1527f7a35..a05ecfb0dd4 100644 --- a/homeassistant/components/fivem/translations/zh-Hant.json +++ b/homeassistant/components/fivem/translations/zh-Hant.json @@ -6,7 +6,6 @@ "error": { "cannot_connect": "\u4f3a\u670d\u5668\u9023\u7dda\u5931\u6557\u3002\u8acb\u6aa2\u67e5\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u5f8c\u518d\u8a66\u4e00\u6b21\u3002\u53e6\u8acb\u78ba\u8a8d\u57f7\u884c\u6700\u65b0\u7248 FiveM \u4f3a\u670d\u5668\u3002", "invalid_game_name": "\u5617\u8a66\u9023\u7dda\u7684\u904a\u6232 API \u4e26\u975e FiveM \u904a\u6232\u3002", - "invalid_gamename": "\u5617\u8a66\u9023\u7dda\u7684\u904a\u6232 API \u4e26\u975e FiveM \u904a\u6232\u3002", "unknown_error": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { diff --git a/homeassistant/components/freebox/translations/ca.json b/homeassistant/components/freebox/translations/ca.json index 2568c3db0b4..e02ca372a3e 100644 --- a/homeassistant/components/freebox/translations/ca.json +++ b/homeassistant/components/freebox/translations/ca.json @@ -17,8 +17,7 @@ "data": { "host": "Amfitri\u00f3", "port": "Port" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/cs.json b/homeassistant/components/freebox/translations/cs.json index 4c11d4b442b..0bb07fc6535 100644 --- a/homeassistant/components/freebox/translations/cs.json +++ b/homeassistant/components/freebox/translations/cs.json @@ -17,8 +17,7 @@ "data": { "host": "Hostitel", "port": "Port" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/de.json b/homeassistant/components/freebox/translations/de.json index 50644a87982..864ce5f0c99 100644 --- a/homeassistant/components/freebox/translations/de.json +++ b/homeassistant/components/freebox/translations/de.json @@ -17,8 +17,7 @@ "data": { "host": "Host", "port": "Port" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/el.json b/homeassistant/components/freebox/translations/el.json index e881230014b..60c9f807765 100644 --- a/homeassistant/components/freebox/translations/el.json +++ b/homeassistant/components/freebox/translations/el.json @@ -17,8 +17,7 @@ "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", "port": "\u0398\u03cd\u03c1\u03b1" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/en.json b/homeassistant/components/freebox/translations/en.json index 539cfbcfe1e..f47fd810117 100644 --- a/homeassistant/components/freebox/translations/en.json +++ b/homeassistant/components/freebox/translations/en.json @@ -17,8 +17,7 @@ "data": { "host": "Host", "port": "Port" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/es-419.json b/homeassistant/components/freebox/translations/es-419.json index 1c99bc8b472..c013a730ae8 100644 --- a/homeassistant/components/freebox/translations/es-419.json +++ b/homeassistant/components/freebox/translations/es-419.json @@ -17,8 +17,7 @@ "data": { "host": "Host", "port": "Puerto" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/es.json b/homeassistant/components/freebox/translations/es.json index bbd691c9764..de176304d1f 100644 --- a/homeassistant/components/freebox/translations/es.json +++ b/homeassistant/components/freebox/translations/es.json @@ -17,8 +17,7 @@ "data": { "host": "Host", "port": "Puerto" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/et.json b/homeassistant/components/freebox/translations/et.json index 4f0239350e3..88bd7260291 100644 --- a/homeassistant/components/freebox/translations/et.json +++ b/homeassistant/components/freebox/translations/et.json @@ -17,8 +17,7 @@ "data": { "host": "", "port": "" - }, - "title": "" + } } } } diff --git a/homeassistant/components/freebox/translations/fr.json b/homeassistant/components/freebox/translations/fr.json index 7b459ebc0ab..60289791408 100644 --- a/homeassistant/components/freebox/translations/fr.json +++ b/homeassistant/components/freebox/translations/fr.json @@ -17,8 +17,7 @@ "data": { "host": "H\u00f4te", "port": "Port" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/hu.json b/homeassistant/components/freebox/translations/hu.json index 39cfe189449..5f35297b538 100644 --- a/homeassistant/components/freebox/translations/hu.json +++ b/homeassistant/components/freebox/translations/hu.json @@ -17,8 +17,7 @@ "data": { "host": "C\u00edm", "port": "Port" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/id.json b/homeassistant/components/freebox/translations/id.json index b03ec248edb..85800e695dd 100644 --- a/homeassistant/components/freebox/translations/id.json +++ b/homeassistant/components/freebox/translations/id.json @@ -17,8 +17,7 @@ "data": { "host": "Host", "port": "Port" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/it.json b/homeassistant/components/freebox/translations/it.json index f1978217547..7bf36f69ff4 100644 --- a/homeassistant/components/freebox/translations/it.json +++ b/homeassistant/components/freebox/translations/it.json @@ -17,8 +17,7 @@ "data": { "host": "Host", "port": "Porta" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/ja.json b/homeassistant/components/freebox/translations/ja.json index fa11e1c4822..5034f2bcc72 100644 --- a/homeassistant/components/freebox/translations/ja.json +++ b/homeassistant/components/freebox/translations/ja.json @@ -17,8 +17,7 @@ "data": { "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/ko.json b/homeassistant/components/freebox/translations/ko.json index c3bb5a9bd40..ff8e6b95282 100644 --- a/homeassistant/components/freebox/translations/ko.json +++ b/homeassistant/components/freebox/translations/ko.json @@ -17,8 +17,7 @@ "data": { "host": "\ud638\uc2a4\ud2b8", "port": "\ud3ec\ud2b8" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/lb.json b/homeassistant/components/freebox/translations/lb.json index e2fdaf18352..6d6155d4f68 100644 --- a/homeassistant/components/freebox/translations/lb.json +++ b/homeassistant/components/freebox/translations/lb.json @@ -17,8 +17,7 @@ "data": { "host": "Apparat", "port": "Port" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/nl.json b/homeassistant/components/freebox/translations/nl.json index 7fbd57dd6ff..2d3972a59af 100644 --- a/homeassistant/components/freebox/translations/nl.json +++ b/homeassistant/components/freebox/translations/nl.json @@ -17,8 +17,7 @@ "data": { "host": "Host", "port": "Poort" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/no.json b/homeassistant/components/freebox/translations/no.json index 69ef2af9c7f..7f157a5dc31 100644 --- a/homeassistant/components/freebox/translations/no.json +++ b/homeassistant/components/freebox/translations/no.json @@ -17,8 +17,7 @@ "data": { "host": "Vert", "port": "Port" - }, - "title": "" + } } } } diff --git a/homeassistant/components/freebox/translations/pl.json b/homeassistant/components/freebox/translations/pl.json index fe6aa517c88..c195a7f2a1f 100644 --- a/homeassistant/components/freebox/translations/pl.json +++ b/homeassistant/components/freebox/translations/pl.json @@ -17,8 +17,7 @@ "data": { "host": "Nazwa hosta lub adres IP", "port": "Port" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/pt-BR.json b/homeassistant/components/freebox/translations/pt-BR.json index 021ab5c902a..1e8e4b60315 100644 --- a/homeassistant/components/freebox/translations/pt-BR.json +++ b/homeassistant/components/freebox/translations/pt-BR.json @@ -17,8 +17,7 @@ "data": { "host": "Nome do host", "port": "Porta" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/ru.json b/homeassistant/components/freebox/translations/ru.json index 4ec5b68516c..c1012dbc743 100644 --- a/homeassistant/components/freebox/translations/ru.json +++ b/homeassistant/components/freebox/translations/ru.json @@ -17,8 +17,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/sl.json b/homeassistant/components/freebox/translations/sl.json index a161bfa494e..4dc79b32256 100644 --- a/homeassistant/components/freebox/translations/sl.json +++ b/homeassistant/components/freebox/translations/sl.json @@ -17,8 +17,7 @@ "data": { "host": "Gostitelj", "port": "Vrata" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/tr.json b/homeassistant/components/freebox/translations/tr.json index 6690b2a5b23..cfd42fbdba5 100644 --- a/homeassistant/components/freebox/translations/tr.json +++ b/homeassistant/components/freebox/translations/tr.json @@ -17,8 +17,7 @@ "data": { "host": "Sunucu", "port": "Port" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/uk.json b/homeassistant/components/freebox/translations/uk.json index 8676c9164a1..8492d3b3eff 100644 --- a/homeassistant/components/freebox/translations/uk.json +++ b/homeassistant/components/freebox/translations/uk.json @@ -17,8 +17,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/freebox/translations/zh-Hant.json b/homeassistant/components/freebox/translations/zh-Hant.json index 6cf0a90f4c0..9f45cdb44d4 100644 --- a/homeassistant/components/freebox/translations/zh-Hant.json +++ b/homeassistant/components/freebox/translations/zh-Hant.json @@ -17,8 +17,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" - }, - "title": "Freebox" + } } } } diff --git a/homeassistant/components/fritz/translations/bg.json b/homeassistant/components/fritz/translations/bg.json index 3fca53d1013..43f9aaf5357 100644 --- a/homeassistant/components/fritz/translations/bg.json +++ b/homeassistant/components/fritz/translations/bg.json @@ -4,11 +4,6 @@ "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { - "start_config": { - "data": { - "port": "\u041f\u043e\u0440\u0442" - } - }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/fritz/translations/ca.json b/homeassistant/components/fritz/translations/ca.json index 04d5b14cac3..df78d9ec4b0 100644 --- a/homeassistant/components/fritz/translations/ca.json +++ b/homeassistant/components/fritz/translations/ca.json @@ -10,7 +10,6 @@ "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", - "connection_error": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "upnp_not_configured": "Falta la configuraci\u00f3 UPnP al dispositiu." }, @@ -32,16 +31,6 @@ "description": "Actualitza les credencials de FRITZ!Box Tools de: {host}.\n\nFRITZ!Box Tools no pot iniciar sessi\u00f3 a FRITZ!Box.", "title": "Actualitzant les credencials de FRITZ!Box Tools" }, - "start_config": { - "data": { - "host": "Amfitri\u00f3", - "password": "Contrasenya", - "port": "Port", - "username": "Nom d'usuari" - }, - "description": "Configura FRITZ!Box Tools per poder controlar FRITZ!Box.\nEl m\u00ednim necessari \u00e9s: nom d'usuari i contrasenya.", - "title": "Configuraci\u00f3 de FRITZ!Box Tools - obligatori" - }, "user": { "data": { "host": "Amfitri\u00f3", diff --git a/homeassistant/components/fritz/translations/cs.json b/homeassistant/components/fritz/translations/cs.json index 9afc3d85536..55326bb1803 100644 --- a/homeassistant/components/fritz/translations/cs.json +++ b/homeassistant/components/fritz/translations/cs.json @@ -8,7 +8,6 @@ "error": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", - "connection_error": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, "step": { @@ -24,14 +23,6 @@ "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } }, - "start_config": { - "data": { - "host": "Hostitel", - "password": "Heslo", - "port": "Port", - "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - } - }, "user": { "data": { "port": "Port" diff --git a/homeassistant/components/fritz/translations/de.json b/homeassistant/components/fritz/translations/de.json index d64845ba9b7..16d2be68adb 100644 --- a/homeassistant/components/fritz/translations/de.json +++ b/homeassistant/components/fritz/translations/de.json @@ -10,7 +10,6 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "cannot_connect": "Verbindung fehlgeschlagen", - "connection_error": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "upnp_not_configured": "Fehlende UPnP-Einstellungen auf dem Ger\u00e4t." }, @@ -32,16 +31,6 @@ "description": "Aktualisiere die Anmeldeinformationen von FRITZ!Box Tools f\u00fcr: {host}. \n\nFRITZ!Box Tools kann sich nicht an deiner FRITZ!Box anmelden.", "title": "Aktualisieren der FRITZ!Box Tools - Anmeldeinformationen" }, - "start_config": { - "data": { - "host": "Host", - "password": "Passwort", - "port": "Port", - "username": "Benutzername" - }, - "description": "Einrichten der FRITZ!Box Tools zur Steuerung deiner FRITZ!Box.\nBen\u00f6tigt: Benutzername, Passwort.", - "title": "Setup FRITZ!Box Tools - obligatorisch" - }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/fritz/translations/el.json b/homeassistant/components/fritz/translations/el.json index 6dfc67e08d6..0cbaa10ef20 100644 --- a/homeassistant/components/fritz/translations/el.json +++ b/homeassistant/components/fritz/translations/el.json @@ -10,7 +10,6 @@ "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "connection_error": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "upnp_not_configured": "\u039b\u03b5\u03af\u03c0\u03bf\u03c5\u03bd \u03bf\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 UPnP \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae." }, @@ -32,16 +31,6 @@ "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c4\u03bf\u03c5 FRITZ!Box Tools \u03b3\u03b9\u03b1: {host} . \n\n \u03a4\u03bf FRITZ!Box Tools \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03bf FRITZ!Box \u03c3\u03b1\u03c2.", "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 FRITZ!Box Tools - \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1" }, - "start_config": { - "data": { - "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "port": "\u0398\u03cd\u03c1\u03b1", - "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" - }, - "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf FRITZ!Box Tools \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03b5\u03c4\u03b5 \u03c4\u03bf FRITZ!Box \u03c3\u03b1\u03c2.\n \u0395\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03bf: \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7, \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2.", - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 FRITZ!Box Tools - \u03c5\u03c0\u03bf\u03c7\u03c1\u03b5\u03c9\u03c4\u03b9\u03ba\u03cc" - }, "user": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", diff --git a/homeassistant/components/fritz/translations/en.json b/homeassistant/components/fritz/translations/en.json index 3ce31866c2f..e7ee1568684 100644 --- a/homeassistant/components/fritz/translations/en.json +++ b/homeassistant/components/fritz/translations/en.json @@ -10,7 +10,6 @@ "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "cannot_connect": "Failed to connect", - "connection_error": "Failed to connect", "invalid_auth": "Invalid authentication", "upnp_not_configured": "Missing UPnP settings on device." }, @@ -32,16 +31,6 @@ "description": "Update FRITZ!Box Tools credentials for: {host}.\n\nFRITZ!Box Tools is unable to log in to your FRITZ!Box.", "title": "Updating FRITZ!Box Tools - credentials" }, - "start_config": { - "data": { - "host": "Host", - "password": "Password", - "port": "Port", - "username": "Username" - }, - "description": "Setup FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.", - "title": "Setup FRITZ!Box Tools - mandatory" - }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/fritz/translations/es.json b/homeassistant/components/fritz/translations/es.json index e8337dbef60..964db5b5325 100644 --- a/homeassistant/components/fritz/translations/es.json +++ b/homeassistant/components/fritz/translations/es.json @@ -10,7 +10,6 @@ "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" }, "flow_title": "FRITZ!Box Tools: {name}", @@ -31,16 +30,6 @@ "description": "Actualizar credenciales de FRITZ!Box Tools para: {host}.\n\n FRITZ!Box Tools no puede iniciar sesi\u00f3n en tu FRITZ!Box.", "title": "Actualizando FRITZ!Box Tools - credenciales" }, - "start_config": { - "data": { - "host": "Host", - "password": "Contrase\u00f1a", - "port": "Puerto", - "username": "Usuario" - }, - "description": "Configurar FRITZ!Box Tools para controlar tu FRITZ!Box.\nM\u00ednimo necesario: usuario, contrase\u00f1a.", - "title": "Configurar FRITZ!Box Tools - obligatorio" - }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/fritz/translations/et.json b/homeassistant/components/fritz/translations/et.json index 89bf4a3b7c9..ac2c9dbfe40 100644 --- a/homeassistant/components/fritz/translations/et.json +++ b/homeassistant/components/fritz/translations/et.json @@ -10,7 +10,6 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Seadistamine on juba k\u00e4ivitatud", "cannot_connect": "\u00dchendamine nurjus", - "connection_error": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus", "upnp_not_configured": "Puuduvad seadme UPnP-seaded." }, @@ -32,16 +31,6 @@ "description": "V\u00e4rskenda FRITZ!Box Tools'i volitusi: {host}.\n\nFRITZ!Box Tools ei saa FRITZ!Boxi sisse logida.", "title": "FRITZ!Boxi t\u00f6\u00f6riistade uuendamine - volitused" }, - "start_config": { - "data": { - "host": "Host", - "password": "Salas\u00f5na", - "port": "Port", - "username": "Kasutajanimi" - }, - "description": "Seadista FRITZ!Boxi t\u00f6\u00f6riistad oma FRITZ!Boxi juhtimiseks.\n Minimaalselt vaja: kasutajanimi ja salas\u00f5na.", - "title": "FRITZ! Boxi t\u00f6\u00f6riistade seadistamine - kohustuslik" - }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/fritz/translations/fr.json b/homeassistant/components/fritz/translations/fr.json index cfcd942b530..538e542c4b6 100644 --- a/homeassistant/components/fritz/translations/fr.json +++ b/homeassistant/components/fritz/translations/fr.json @@ -10,7 +10,6 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "cannot_connect": "\u00c9chec de connexion", - "connection_error": "\u00c9chec de connexion", "invalid_auth": "Authentification non valide", "upnp_not_configured": "Param\u00e8tres UPnP manquants sur l'appareil." }, @@ -32,16 +31,6 @@ "description": "Mettre \u00e0 jour les informations d'identification FRITZ!Box Tools pour : {host}.", "title": "Mise \u00e0 jour de FRITZ!Box Tools - informations d'identification" }, - "start_config": { - "data": { - "host": "H\u00f4te", - "password": "Mot de passe", - "port": "Port", - "username": "Nom d'utilisateur" - }, - "description": "Configuration de FRITZ!Box Tools pour contr\u00f4ler votre FRITZ!Box.\nMinimum requis: nom d'utilisateur, mot de passe.", - "title": "Configuration FRITZ!Box Tools - obligatoire" - }, "user": { "data": { "host": "H\u00f4te", diff --git a/homeassistant/components/fritz/translations/he.json b/homeassistant/components/fritz/translations/he.json index 783f215cc40..1b27fdb7581 100644 --- a/homeassistant/components/fritz/translations/he.json +++ b/homeassistant/components/fritz/translations/he.json @@ -9,7 +9,6 @@ "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", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "connection_error": "\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" }, "flow_title": "{name}", @@ -26,14 +25,6 @@ "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } }, - "start_config": { - "data": { - "host": "\u05de\u05d0\u05e8\u05d7", - "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "port": "\u05e4\u05ea\u05d7\u05d4", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } - }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", diff --git a/homeassistant/components/fritz/translations/hu.json b/homeassistant/components/fritz/translations/hu.json index d1ff2c0c6bf..71e6c0233ae 100644 --- a/homeassistant/components/fritz/translations/hu.json +++ b/homeassistant/components/fritz/translations/hu.json @@ -10,7 +10,6 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A konfigur\u00e1ci\u00f3 m\u00e1r folyamatban van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "connection_error": "Nem siker\u00fclt csatlakozni", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "upnp_not_configured": "Hi\u00e1nyz\u00f3 UPnP-be\u00e1ll\u00edt\u00e1sok az eszk\u00f6z\u00f6n." }, @@ -32,16 +31,6 @@ "description": "{host} FRITZ! Box Tools hiteles\u00edt\u0151 adatait. \n\n A FRITZ! Box Tools nem tud bejelentkezni a FRITZ! Box eszk\u00f6zbe.", "title": "A FRITZ! Box Tools friss\u00edt\u00e9se - hiteles\u00edt\u0151 adatok" }, - "start_config": { - "data": { - "host": "C\u00edm", - "password": "Jelsz\u00f3", - "port": "Port", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - }, - "description": "A FRITZ! Box eszk\u00f6z\u00f6k be\u00e1ll\u00edt\u00e1sa a FRITZ! Box vez\u00e9rl\u00e9s\u00e9hez.\n Minimum sz\u00fcks\u00e9ges: felhaszn\u00e1l\u00f3n\u00e9v, jelsz\u00f3.", - "title": "A FRITZ! Box Tools be\u00e1ll\u00edt\u00e1sa - k\u00f6telez\u0151" - }, "user": { "data": { "host": "C\u00edm", diff --git a/homeassistant/components/fritz/translations/id.json b/homeassistant/components/fritz/translations/id.json index c31bdf8b77c..71e1e3e8246 100644 --- a/homeassistant/components/fritz/translations/id.json +++ b/homeassistant/components/fritz/translations/id.json @@ -10,7 +10,6 @@ "already_configured": "Perangkat sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", "cannot_connect": "Gagal terhubung", - "connection_error": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", "upnp_not_configured": "Pengaturan UPnP pada perangkat tidak ada." }, @@ -32,16 +31,6 @@ "description": "Perbarui kredensial FRITZ!Box Tools untuk: {host} . \n\nFRITZ!Box Tools tidak dapat masuk ke FRITZ!Box Anda.", "title": "Memperbarui FRITZ!Box Tools - kredensial" }, - "start_config": { - "data": { - "host": "Host", - "password": "Kata Sandi", - "port": "Port", - "username": "Nama Pengguna" - }, - "description": "Siapkan FRITZ!Box Tools untuk mengontrol FRITZ!Box Anda.\nDiperlukan minimal: nama pengguna dan kata sandi.", - "title": "Siapkan FRITZ!Box Tools - wajib" - }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/fritz/translations/it.json b/homeassistant/components/fritz/translations/it.json index 0516449f5db..b1fa200731e 100644 --- a/homeassistant/components/fritz/translations/it.json +++ b/homeassistant/components/fritz/translations/it.json @@ -10,7 +10,6 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "cannot_connect": "Impossibile connettersi", - "connection_error": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", "upnp_not_configured": "Impostazioni UPnP mancanti sul dispositivo." }, @@ -32,16 +31,6 @@ "description": "Aggiorna le credenziali di FRITZ!Box Tools per: {host} . \n\n FRITZ!Box Tools non riesce ad accedere al tuo FRITZ! Box.", "title": "Aggiornamento degli strumenti del FRITZ!Box - credenziali" }, - "start_config": { - "data": { - "host": "Host", - "password": "Password", - "port": "Porta", - "username": "Nome utente" - }, - "description": "Configura gli strumenti FRITZ!Box per controllare il tuo FRITZ!Box.\n Minimo necessario: nome utente, password.", - "title": "Configurazione degli strumenti FRITZ!Box - obbligatorio" - }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/fritz/translations/ja.json b/homeassistant/components/fritz/translations/ja.json index 8bd9747c9bc..1b915b28577 100644 --- a/homeassistant/components/fritz/translations/ja.json +++ b/homeassistant/components/fritz/translations/ja.json @@ -10,7 +10,6 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "connection_error": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "upnp_not_configured": "\u30c7\u30d0\u30a4\u30b9\u306bUPnP\u306e\u8a2d\u5b9a\u304c\u3042\u308a\u307e\u305b\u3093\u3002" }, @@ -32,16 +31,6 @@ "description": "FRITZ!Box Tools\u306e\u8a8d\u8a3c\u3092\u66f4\u65b0\u3057\u307e\u3059: {host}\n\nFRITZ!Box Tools\u304c\u3001FRITZ!Box\u306b\u30ed\u30b0\u30a4\u30f3\u3067\u304d\u307e\u305b\u3093\u3002", "title": "FRITZ!Box Tools\u306e\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8 - \u8a8d\u8a3c\u60c5\u5831" }, - "start_config": { - "data": { - "host": "\u30db\u30b9\u30c8", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8", - "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - }, - "description": "FRITZ!Box Tools\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066FRITZ!Box\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u6700\u4f4e\u9650\u5fc5\u8981\u306a\u3082\u306e: \u30e6\u30fc\u30b6\u30fc\u540d\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3002", - "title": "FRITZ!Box Tools\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7 - \u5fc5\u9808" - }, "user": { "data": { "host": "\u30db\u30b9\u30c8", diff --git a/homeassistant/components/fritz/translations/ko.json b/homeassistant/components/fritz/translations/ko.json index 718b105df33..99c42938d6f 100644 --- a/homeassistant/components/fritz/translations/ko.json +++ b/homeassistant/components/fritz/translations/ko.json @@ -3,12 +3,12 @@ "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", + "ignore_ip6_link_local": "IPv6 \ub9c1\ud06c \ub85c\uceec \uc8fc\uc18c\ub294 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\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}", @@ -24,14 +24,6 @@ "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" - } } } } diff --git a/homeassistant/components/fritz/translations/nb.json b/homeassistant/components/fritz/translations/nb.json index 5e712fccbd9..a7e24e2ed11 100644 --- a/homeassistant/components/fritz/translations/nb.json +++ b/homeassistant/components/fritz/translations/nb.json @@ -11,11 +11,6 @@ "username": "Brukernavn" } }, - "start_config": { - "data": { - "username": "Brukernavn" - } - }, "user": { "data": { "username": "Brukernavn" diff --git a/homeassistant/components/fritz/translations/nl.json b/homeassistant/components/fritz/translations/nl.json index 11fabceaf8d..80aebf4fc8a 100644 --- a/homeassistant/components/fritz/translations/nl.json +++ b/homeassistant/components/fritz/translations/nl.json @@ -10,7 +10,6 @@ "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratiestroom is al aan de gang", "cannot_connect": "Kan geen verbinding maken", - "connection_error": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "upnp_not_configured": "Ontbrekende UPnP instellingen op apparaat." }, @@ -32,16 +31,6 @@ "description": "Update FRITZ! Box Tools-inloggegevens voor: {host}. \n\n FRITZ! Box Tools kan niet inloggen op uw FRITZ!Box.", "title": "Updating FRITZ!Box Tools - referenties" }, - "start_config": { - "data": { - "host": "Host", - "password": "Wachtwoord", - "port": "Poort", - "username": "Gebruikersnaam" - }, - "description": "Stel FRITZ!Box Tools in om uw FRITZ!Box te bedienen.\nMinimaal nodig: gebruikersnaam, wachtwoord.", - "title": "Configureer FRITZ! Box Tools - verplicht" - }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/fritz/translations/no.json b/homeassistant/components/fritz/translations/no.json index a18df1a7b14..5ed97636675 100644 --- a/homeassistant/components/fritz/translations/no.json +++ b/homeassistant/components/fritz/translations/no.json @@ -10,7 +10,6 @@ "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "cannot_connect": "Tilkobling mislyktes", - "connection_error": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", "upnp_not_configured": "Mangler UPnP-innstillinger p\u00e5 enheten." }, @@ -32,16 +31,6 @@ "description": "Oppdater legitimasjonen til FRITZ!Box Tools for: {host} . \n\n FRITZ!Box Tools kan ikke logge p\u00e5 FRITZ! Box.", "title": "Oppdaterer FRITZ!Box verkt\u00f8y - legitimasjon" }, - "start_config": { - "data": { - "host": "Vert", - "password": "Passord", - "port": "Port", - "username": "Brukernavn" - }, - "description": "Sett opp FRITZ!Box verkt\u00f8y for \u00e5 kontrollere fritz! Boksen.\nMinimum n\u00f8dvendig: brukernavn, passord.", - "title": "Sett opp FRITZ!Box verkt\u00f8y - obligatorisk" - }, "user": { "data": { "host": "Vert", diff --git a/homeassistant/components/fritz/translations/pl.json b/homeassistant/components/fritz/translations/pl.json index fed010f1987..f14d41ed399 100644 --- a/homeassistant/components/fritz/translations/pl.json +++ b/homeassistant/components/fritz/translations/pl.json @@ -10,7 +10,6 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", "upnp_not_configured": "Brak ustawie\u0144 UPnP w urz\u0105dzeniu." }, @@ -32,16 +31,6 @@ "description": "Zaktualizuj dane logowania narz\u0119dzi FRITZ!Box dla: {host} . \n\nNarz\u0119dzia FRITZ!Box nie mo\u017ce zalogowa\u0107 si\u0119 do urz\u0105dzenia FRITZ!Box.", "title": "Aktualizacja danych logowania narz\u0119dzi FRITZ!Box" }, - "start_config": { - "data": { - "host": "Nazwa hosta lub adres IP", - "password": "Has\u0142o", - "port": "Port", - "username": "Nazwa u\u017cytkownika" - }, - "description": "Skonfiguruj narz\u0119dzia FRITZ!Box, aby sterowa\u0107 urz\u0105dzeniem FRITZ! Box.\nMinimalne wymagania: nazwa u\u017cytkownika, has\u0142o.", - "title": "Konfiguracja narz\u0119dzi FRITZ!Box - obowi\u0105zkowe" - }, "user": { "data": { "host": "Nazwa hosta lub adres IP", diff --git a/homeassistant/components/fritz/translations/pt-BR.json b/homeassistant/components/fritz/translations/pt-BR.json index ceb295310c7..29bb04ebf3f 100644 --- a/homeassistant/components/fritz/translations/pt-BR.json +++ b/homeassistant/components/fritz/translations/pt-BR.json @@ -10,7 +10,6 @@ "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "cannot_connect": "Falha ao conectar", - "connection_error": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "upnp_not_configured": "Faltam configura\u00e7\u00f5es de UPnP no dispositivo." }, @@ -32,16 +31,6 @@ "description": "Atualize as credenciais do FRITZ!Box Tools para: {host} . \n\n O FRITZ!Box Tools n\u00e3o consegue iniciar sess\u00e3o no seu FRITZ!Box.", "title": "Atualizando as Ferramentas do FRITZ!Box - credenciais" }, - "start_config": { - "data": { - "host": "Nome do host", - "password": "Senha", - "port": "Porta", - "username": "Usu\u00e1rio" - }, - "description": "Configure as Ferramentas do FRITZ!Box para controlar o seu FRITZ!Box.\n M\u00ednimo necess\u00e1rio: nome de usu\u00e1rio, senha.", - "title": "Configurar as Ferramentas do FRITZ!Box - obrigat\u00f3rio" - }, "user": { "data": { "host": "Nome do host", diff --git a/homeassistant/components/fritz/translations/ru.json b/homeassistant/components/fritz/translations/ru.json index e45c36fe736..63d4463a055 100644 --- a/homeassistant/components/fritz/translations/ru.json +++ b/homeassistant/components/fritz/translations/ru.json @@ -10,7 +10,6 @@ "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.", - "connection_error": "\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.", "upnp_not_configured": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 UPnP \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435." }, @@ -32,16 +31,6 @@ "description": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 FRITZ!Box Tools \u0434\u043b\u044f {host}.\n\n\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e FRITZ!Box Tools \u043d\u0430 \u0412\u0430\u0448\u0435\u043c FRITZ!Box.", "title": "FRITZ!Box Tools" }, - "start_config": { - "data": { - "host": "\u0425\u043e\u0441\u0442", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "port": "\u041f\u043e\u0440\u0442", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" - }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 FRITZ!Box Tools \u0434\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0412\u0430\u0448\u0438\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c FRITZ!Box.\n\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043a\u0430\u043a \u043c\u0438\u043d\u0438\u043c\u0443\u043c \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", - "title": "FRITZ!Box Tools" - }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/fritz/translations/sk.json b/homeassistant/components/fritz/translations/sk.json index 9d83bc4b756..17cb7bfc78b 100644 --- a/homeassistant/components/fritz/translations/sk.json +++ b/homeassistant/components/fritz/translations/sk.json @@ -9,11 +9,6 @@ "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { - "start_config": { - "data": { - "port": "Port" - } - }, "user": { "data": { "port": "Port" diff --git a/homeassistant/components/fritz/translations/tr.json b/homeassistant/components/fritz/translations/tr.json index 86cabbb782b..af443cac976 100644 --- a/homeassistant/components/fritz/translations/tr.json +++ b/homeassistant/components/fritz/translations/tr.json @@ -10,7 +10,6 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "cannot_connect": "Ba\u011flanma hatas\u0131", - "connection_error": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "upnp_not_configured": "Cihazda UPnP ayarlar\u0131 eksik." }, @@ -32,16 +31,6 @@ "description": "{host} i\u00e7in FRITZ!Box Tools kimlik bilgilerini g\u00fcncelleyin. \n\n FRITZ!Box Tools, FRITZ!Box'\u0131n\u0131zda oturum a\u00e7am\u0131yor.", "title": "FRITZ!Box Tools - kimlik bilgilerinin g\u00fcncellenmesi" }, - "start_config": { - "data": { - "host": "Sunucu", - "password": "Parola", - "port": "Port", - "username": "Kullan\u0131c\u0131 Ad\u0131" - }, - "description": "FRITZ!Box'\u0131n\u0131z\u0131 kontrol etmek i\u00e7in FRITZ!Box Tools'u kurun.\n Minimum gerekli: kullan\u0131c\u0131 ad\u0131, \u015fifre.", - "title": "FRITZ!Box Tools Kurulumu - zorunlu" - }, "user": { "data": { "host": "Sunucu", diff --git a/homeassistant/components/fritz/translations/zh-Hans.json b/homeassistant/components/fritz/translations/zh-Hans.json index 91d68989675..3f5a3f2afa0 100644 --- a/homeassistant/components/fritz/translations/zh-Hans.json +++ b/homeassistant/components/fritz/translations/zh-Hans.json @@ -11,11 +11,6 @@ "password": "\u5bc6\u7801" } }, - "start_config": { - "data": { - "password": "\u5bc6\u7801" - } - }, "user": { "data": { "password": "\u5bc6\u7801" diff --git a/homeassistant/components/fritz/translations/zh-Hant.json b/homeassistant/components/fritz/translations/zh-Hant.json index 778c38e36f7..0565a1e292a 100644 --- a/homeassistant/components/fritz/translations/zh-Hant.json +++ b/homeassistant/components/fritz/translations/zh-Hant.json @@ -10,7 +10,6 @@ "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", - "connection_error": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "upnp_not_configured": "\u672a\u8a2d\u5b9a\u88dd\u7f6e UPnP \u8a2d\u5b9a\u3002" }, @@ -32,16 +31,6 @@ "description": "\u66f4\u65b0 FRITZ!Box Tools \u6191\u8b49\uff1a{host}\u3002\n\nFRITZ!Box Tools \u7121\u6cd5\u767b\u5165 FRITZ!Box\u3002", "title": "\u66f4\u65b0 FRITZ!Box Tools - \u6191\u8b49" }, - "start_config": { - "data": { - "host": "\u4e3b\u6a5f\u7aef", - "password": "\u5bc6\u78bc", - "port": "\u901a\u8a0a\u57e0", - "username": "\u4f7f\u7528\u8005\u540d\u7a31" - }, - "description": "\u8a2d\u5b9a FRITZ!Box Tools \u4ee5\u63a7\u5236 FRITZ!Box\u3002\n\u9700\u8981\u8f38\u5165\uff1a\u4f7f\u7528\u8005\u540d\u7a31\u3001\u5bc6\u78bc\u3002", - "title": "\u8a2d\u5b9a FRITZ!Box Tools - \u5f37\u5236" - }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", diff --git a/homeassistant/components/generic/translations/bg.json b/homeassistant/components/generic/translations/bg.json index dc9bd439f0b..ebb2d32c21d 100644 --- a/homeassistant/components/generic/translations/bg.json +++ b/homeassistant/components/generic/translations/bg.json @@ -20,7 +20,6 @@ "user": { "data": { "authentication": "\u0423\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", - "content_type": "\u0422\u0438\u043f \u0441\u044a\u0434\u044a\u0440\u0436\u0430\u043d\u0438\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "rtsp_transport": "RTSP \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u0435\u043d \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" @@ -42,7 +41,6 @@ "init": { "data": { "authentication": "\u0423\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", - "content_type": "\u0422\u0438\u043f \u0441\u044a\u0434\u044a\u0440\u0436\u0430\u043d\u0438\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "rtsp_transport": "RTSP \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u0435\u043d \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" diff --git a/homeassistant/components/generic/translations/ca.json b/homeassistant/components/generic/translations/ca.json index 7d69e837130..3f2cd6afa47 100644 --- a/homeassistant/components/generic/translations/ca.json +++ b/homeassistant/components/generic/translations/ca.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "Autenticaci\u00f3", - "content_type": "Tipus de contingut", "framerate": "Freq\u00fc\u00e8ncia de visualitzaci\u00f3 (Hz)", "limit_refetch_to_url_change": "Limita la lectura al canvi d'URL", "password": "Contrasenya", @@ -72,7 +71,6 @@ "init": { "data": { "authentication": "Autenticaci\u00f3", - "content_type": "Tipus de contingut", "framerate": "Freq\u00fc\u00e8ncia de visualitzaci\u00f3 (Hz)", "limit_refetch_to_url_change": "Limita la lectura al canvi d'URL", "password": "Contrasenya", diff --git a/homeassistant/components/generic/translations/de.json b/homeassistant/components/generic/translations/de.json index 8d40216001a..d4c5c268eed 100644 --- a/homeassistant/components/generic/translations/de.json +++ b/homeassistant/components/generic/translations/de.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "Authentifizierung", - "content_type": "Inhaltstyp", "framerate": "Bildfrequenz (Hz)", "limit_refetch_to_url_change": "Neuabruf auf URL-\u00c4nderung beschr\u00e4nken", "password": "Passwort", @@ -72,7 +71,6 @@ "init": { "data": { "authentication": "Authentifizierung", - "content_type": "Inhaltstyp", "framerate": "Bildfrequenz (Hz)", "limit_refetch_to_url_change": "Neuabruf auf URL-\u00c4nderung beschr\u00e4nken", "password": "Passwort", diff --git a/homeassistant/components/generic/translations/el.json b/homeassistant/components/generic/translations/el.json index 8b15c7de851..be1c963740a 100644 --- a/homeassistant/components/generic/translations/el.json +++ b/homeassistant/components/generic/translations/el.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "\u0395\u03bb\u03ad\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", - "content_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5", "framerate": "\u03a1\u03c5\u03b8\u03bc\u03cc\u03c2 \u03ba\u03b1\u03c1\u03ad (Hz)", "limit_refetch_to_url_change": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 \u03c3\u03c4\u03b7\u03bd \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae url", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", @@ -72,7 +71,6 @@ "init": { "data": { "authentication": "\u0395\u03bb\u03ad\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", - "content_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5", "framerate": "\u03a1\u03c5\u03b8\u03bc\u03cc\u03c2 \u03ba\u03b1\u03c1\u03ad (Hz)", "limit_refetch_to_url_change": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 \u03c3\u03c4\u03b7\u03bd \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae url", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json index 334f00fab6f..b552c780d29 100644 --- a/homeassistant/components/generic/translations/en.json +++ b/homeassistant/components/generic/translations/en.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "Authentication", - "content_type": "Content Type", "framerate": "Frame Rate (Hz)", "limit_refetch_to_url_change": "Limit refetch to url change", "password": "Password", @@ -72,7 +71,6 @@ "init": { "data": { "authentication": "Authentication", - "content_type": "Content Type", "framerate": "Frame Rate (Hz)", "limit_refetch_to_url_change": "Limit refetch to url change", "password": "Password", diff --git a/homeassistant/components/generic/translations/es.json b/homeassistant/components/generic/translations/es.json index 90bff434f6d..4d8019002bb 100644 --- a/homeassistant/components/generic/translations/es.json +++ b/homeassistant/components/generic/translations/es.json @@ -24,7 +24,6 @@ "user": { "data": { "authentication": "Autenticaci\u00f3n", - "content_type": "Tipo de contenido", "framerate": "Frecuencia de visualizaci\u00f3n (Hz)", "limit_refetch_to_url_change": "Limita la lectura al cambio de URL", "still_image_url": "URL de imagen fija (ej. http://...)", @@ -53,7 +52,6 @@ "init": { "data": { "authentication": "Autenticaci\u00f3n", - "content_type": "Tipo de contenido", "framerate": "Frecuencia de visualizaci\u00f3n (Hz)", "limit_refetch_to_url_change": "Limita la lectura al cambio de URL", "password": "Contrase\u00f1a", diff --git a/homeassistant/components/generic/translations/et.json b/homeassistant/components/generic/translations/et.json index 0a4bdc0731f..41e1881573e 100644 --- a/homeassistant/components/generic/translations/et.json +++ b/homeassistant/components/generic/translations/et.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "Autentimine", - "content_type": "Sisu t\u00fc\u00fcp", "framerate": "Kaadrisagedus (Hz)", "limit_refetch_to_url_change": "Piira laadimist URL-i muutmiseni", "password": "Salas\u00f5na", @@ -72,7 +71,6 @@ "init": { "data": { "authentication": "Autentimine", - "content_type": "Sisu t\u00fc\u00fcp", "framerate": "Kaadrisagedus (Hz)", "limit_refetch_to_url_change": "Piira laadimist URL-i muutmiseni", "password": "Salas\u00f5na", diff --git a/homeassistant/components/generic/translations/fr.json b/homeassistant/components/generic/translations/fr.json index 454f9b0de03..6215c579be7 100644 --- a/homeassistant/components/generic/translations/fr.json +++ b/homeassistant/components/generic/translations/fr.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "Authentification", - "content_type": "Type de contenu", "framerate": "Fr\u00e9quence d'images (en hertz)", "limit_refetch_to_url_change": "Limiter la r\u00e9cup\u00e9ration aux changements d'URL", "password": "Mot de passe", @@ -72,7 +71,6 @@ "init": { "data": { "authentication": "Authentification", - "content_type": "Type de contenu", "framerate": "Fr\u00e9quence d'images (en hertz)", "limit_refetch_to_url_change": "Limiter la r\u00e9cup\u00e9ration aux changements d'URL", "password": "Mot de passe", diff --git a/homeassistant/components/generic/translations/he.json b/homeassistant/components/generic/translations/he.json index b20b0ea88a7..f39f78074f5 100644 --- a/homeassistant/components/generic/translations/he.json +++ b/homeassistant/components/generic/translations/he.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "\u05d0\u05d9\u05de\u05d5\u05ea", - "content_type": "\u05e1\u05d5\u05d2 \u05ea\u05d5\u05db\u05df", "framerate": "\u05e7\u05e6\u05d1 \u05e4\u05e8\u05d9\u05d9\u05de\u05d9\u05dd (\u05d4\u05e8\u05e5)", "limit_refetch_to_url_change": "\u05d4\u05d2\u05d1\u05dc\u05d4 \u05e9\u05dc \u05d0\u05d7\u05e1\u05d5\u05df \u05d7\u05d5\u05d6\u05e8 \u05dc\u05e9\u05d9\u05e0\u05d5\u05d9 \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", @@ -72,7 +71,6 @@ "init": { "data": { "authentication": "\u05d0\u05d9\u05de\u05d5\u05ea", - "content_type": "\u05e1\u05d5\u05d2 \u05ea\u05d5\u05db\u05df", "framerate": "\u05e7\u05e6\u05d1 \u05e4\u05e8\u05d9\u05d9\u05de\u05d9\u05dd (\u05d4\u05e8\u05e5)", "limit_refetch_to_url_change": "\u05d4\u05d2\u05d1\u05dc\u05d4 \u05e9\u05dc \u05d0\u05d7\u05e1\u05d5\u05df \u05d7\u05d5\u05d6\u05e8 \u05dc\u05e9\u05d9\u05e0\u05d5\u05d9 \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/generic/translations/hu.json b/homeassistant/components/generic/translations/hu.json index bc8172caaf9..59840b3195b 100644 --- a/homeassistant/components/generic/translations/hu.json +++ b/homeassistant/components/generic/translations/hu.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "Hiteles\u00edt\u00e9s", - "content_type": "Tartalom t\u00edpusa", "framerate": "K\u00e9pkockasebess\u00e9g (Hz)", "limit_refetch_to_url_change": "Korl\u00e1tozza a visszah\u00edv\u00e1st az url v\u00e1ltoz\u00e1sra", "password": "Jelsz\u00f3", @@ -72,7 +71,6 @@ "init": { "data": { "authentication": "Hiteles\u00edt\u00e9s", - "content_type": "Tartalom t\u00edpusa", "framerate": "K\u00e9pkockasebess\u00e9g (Hz)", "limit_refetch_to_url_change": "Korl\u00e1tozza a visszah\u00edv\u00e1st az url v\u00e1ltoz\u00e1sra", "password": "Jelsz\u00f3", diff --git a/homeassistant/components/generic/translations/id.json b/homeassistant/components/generic/translations/id.json index 7c4be981e4a..95c6d6e2ae2 100644 --- a/homeassistant/components/generic/translations/id.json +++ b/homeassistant/components/generic/translations/id.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "Autentikasi", - "content_type": "Jenis Konten", "framerate": "Frame Rate (Hz)", "limit_refetch_to_url_change": "Batasi pengambilan ulang untuk perubahan URL", "password": "Kata Sandi", @@ -72,7 +71,6 @@ "init": { "data": { "authentication": "Autentikasi", - "content_type": "Jenis Konten", "framerate": "Frame Rate (Hz)", "limit_refetch_to_url_change": "Batasi pengambilan ulang untuk perubahan URL", "password": "Kata Sandi", diff --git a/homeassistant/components/generic/translations/it.json b/homeassistant/components/generic/translations/it.json index e5e41e50285..ff4b9822601 100644 --- a/homeassistant/components/generic/translations/it.json +++ b/homeassistant/components/generic/translations/it.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "Autenticazione", - "content_type": "Tipo di contenuto", "framerate": "Frequenza fotogrammi (Hz)", "limit_refetch_to_url_change": "Limita il recupero alla modifica dell'URL", "password": "Password", @@ -72,7 +71,6 @@ "init": { "data": { "authentication": "Autenticazione", - "content_type": "Tipo di contenuto", "framerate": "Frequenza fotogrammi (Hz)", "limit_refetch_to_url_change": "Limita il recupero alla modifica dell'URL", "password": "Password", diff --git a/homeassistant/components/generic/translations/ja.json b/homeassistant/components/generic/translations/ja.json index 97c1a76ab36..a106cbf4cdc 100644 --- a/homeassistant/components/generic/translations/ja.json +++ b/homeassistant/components/generic/translations/ja.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "\u8a8d\u8a3c", - "content_type": "\u30b3\u30f3\u30c6\u30f3\u30c4\u306e\u7a2e\u985e", "framerate": "\u30d5\u30ec\u30fc\u30e0\u30ec\u30fc\u30c8\uff08Hz\uff09", "limit_refetch_to_url_change": "URL\u5909\u66f4\u6642\u306e\u518d\u53d6\u5f97\u3092\u5236\u9650\u3059\u308b", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", @@ -72,7 +71,6 @@ "init": { "data": { "authentication": "\u8a8d\u8a3c", - "content_type": "\u30b3\u30f3\u30c6\u30f3\u30c4\u306e\u7a2e\u985e", "framerate": "\u30d5\u30ec\u30fc\u30e0\u30ec\u30fc\u30c8\uff08Hz\uff09", "limit_refetch_to_url_change": "URL\u5909\u66f4\u6642\u306e\u518d\u53d6\u5f97\u3092\u5236\u9650\u3059\u308b", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", diff --git a/homeassistant/components/generic/translations/ko.json b/homeassistant/components/generic/translations/ko.json new file mode 100644 index 00000000000..20ad990e862 --- /dev/null +++ b/homeassistant/components/generic/translations/ko.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/nl.json b/homeassistant/components/generic/translations/nl.json index 5c45b783c4f..2b5bc828727 100644 --- a/homeassistant/components/generic/translations/nl.json +++ b/homeassistant/components/generic/translations/nl.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "Authenticatie", - "content_type": "Inhoudstype", "framerate": "Framesnelheid (Hz)", "limit_refetch_to_url_change": "Beperk refetch tot url verandering", "password": "Wachtwoord", @@ -72,7 +71,6 @@ "init": { "data": { "authentication": "Authenticatie", - "content_type": "Inhoudstype", "framerate": "Framesnelheid (Hz)", "limit_refetch_to_url_change": "Beperk refetch tot url verandering", "password": "Wachtwoord", diff --git a/homeassistant/components/generic/translations/no.json b/homeassistant/components/generic/translations/no.json index 78b5618b073..0c228e314d2 100644 --- a/homeassistant/components/generic/translations/no.json +++ b/homeassistant/components/generic/translations/no.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "Godkjenning", - "content_type": "Innholdstype", "framerate": "Bildefrekvens (Hz)", "limit_refetch_to_url_change": "Begrens gjenhenting til endring av nettadresse", "password": "Passord", @@ -72,7 +71,6 @@ "init": { "data": { "authentication": "Godkjenning", - "content_type": "Innholdstype", "framerate": "Bildefrekvens (Hz)", "limit_refetch_to_url_change": "Begrens gjenhenting til endring av nettadresse", "password": "Passord", diff --git a/homeassistant/components/generic/translations/pl.json b/homeassistant/components/generic/translations/pl.json index 16a912587e5..3669fd78e3f 100644 --- a/homeassistant/components/generic/translations/pl.json +++ b/homeassistant/components/generic/translations/pl.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "Uwierzytelnianie", - "content_type": "Typ zawarto\u015bci", "framerate": "Od\u015bwie\u017canie (Hz)", "limit_refetch_to_url_change": "Ogranicz pobieranie do zmiany adresu URL", "password": "Has\u0142o", @@ -72,7 +71,6 @@ "init": { "data": { "authentication": "Uwierzytelnianie", - "content_type": "Typ zawarto\u015bci", "framerate": "Od\u015bwie\u017canie (Hz)", "limit_refetch_to_url_change": "Ogranicz pobieranie do zmiany adresu URL", "password": "Has\u0142o", diff --git a/homeassistant/components/generic/translations/pt-BR.json b/homeassistant/components/generic/translations/pt-BR.json index ec093f3ff1a..ea1ede92f22 100644 --- a/homeassistant/components/generic/translations/pt-BR.json +++ b/homeassistant/components/generic/translations/pt-BR.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "Autentica\u00e7\u00e3o", - "content_type": "Tipo de conte\u00fado", "framerate": "Taxa de quadros (Hz)", "limit_refetch_to_url_change": "Limitar a nova busca \u00e0 altera\u00e7\u00e3o de URL", "password": "Senha", @@ -72,7 +71,6 @@ "init": { "data": { "authentication": "Autentica\u00e7\u00e3o", - "content_type": "Tipo de conte\u00fado", "framerate": "Taxa de quadros (Hz)", "limit_refetch_to_url_change": "Limitar a nova busca \u00e0 altera\u00e7\u00e3o de URL", "password": "Senha", diff --git a/homeassistant/components/generic/translations/ru.json b/homeassistant/components/generic/translations/ru.json index e88f9f9cd1e..f9fed6b5831 100644 --- a/homeassistant/components/generic/translations/ru.json +++ b/homeassistant/components/generic/translations/ru.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", - "content_type": "\u0422\u0438\u043f \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e", "framerate": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043a\u0430\u0434\u0440\u043e\u0432 (\u0413\u0446)", "limit_refetch_to_url_change": "\u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0438\u0432\u0430\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0443\u044e \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0443 \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 URL-\u0430\u0434\u0440\u0435\u0441\u0430", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", @@ -72,7 +71,6 @@ "init": { "data": { "authentication": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", - "content_type": "\u0422\u0438\u043f \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e", "framerate": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043a\u0430\u0434\u0440\u043e\u0432 (\u0413\u0446)", "limit_refetch_to_url_change": "\u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0438\u0432\u0430\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0443\u044e \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0443 \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 URL-\u0430\u0434\u0440\u0435\u0441\u0430", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/generic/translations/tr.json b/homeassistant/components/generic/translations/tr.json index d6ba8601beb..7b6ab65f948 100644 --- a/homeassistant/components/generic/translations/tr.json +++ b/homeassistant/components/generic/translations/tr.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "Kimlik Do\u011frulama", - "content_type": "\u0130\u00e7erik T\u00fcr\u00fc", "framerate": "Kare H\u0131z\u0131 (Hz)", "limit_refetch_to_url_change": "Yeniden getirmeyi url de\u011fi\u015fikli\u011fiyle s\u0131n\u0131rla", "password": "Parola", @@ -72,7 +71,6 @@ "init": { "data": { "authentication": "Kimlik Do\u011frulama", - "content_type": "\u0130\u00e7erik T\u00fcr\u00fc", "framerate": "Kare H\u0131z\u0131 (Hz)", "limit_refetch_to_url_change": "Yeniden getirmeyi url de\u011fi\u015fikli\u011fiyle s\u0131n\u0131rla", "password": "Parola", diff --git a/homeassistant/components/generic/translations/zh-Hant.json b/homeassistant/components/generic/translations/zh-Hant.json index e37ee2d9235..d1079724252 100644 --- a/homeassistant/components/generic/translations/zh-Hant.json +++ b/homeassistant/components/generic/translations/zh-Hant.json @@ -32,7 +32,6 @@ "user": { "data": { "authentication": "\u9a57\u8b49", - "content_type": "\u5167\u5bb9\u985e\u578b", "framerate": "\u5f71\u683c\u7387 (Hz)", "limit_refetch_to_url_change": "\u9650\u5236 URL \u8b8a\u66f4\u66f4\u65b0", "password": "\u5bc6\u78bc", @@ -72,7 +71,6 @@ "init": { "data": { "authentication": "\u9a57\u8b49", - "content_type": "\u5167\u5bb9\u985e\u578b", "framerate": "\u5f71\u683c\u7387 (Hz)", "limit_refetch_to_url_change": "\u9650\u5236 URL \u8b8a\u66f4\u66f4\u65b0", "password": "\u5bc6\u78bc", diff --git a/homeassistant/components/geocaching/translations/he.json b/homeassistant/components/geocaching/translations/he.json new file mode 100644 index 00000000000..525624782ac --- /dev/null +++ b/homeassistant/components/geocaching/translations/he.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "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})", + "oauth_error": "\u05d4\u05ea\u05e7\u05d1\u05dc\u05d5 \u05e0\u05ea\u05d5\u05e0\u05d9 \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd.", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "create_entry": { + "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" + }, + "step": { + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + }, + "reauth_confirm": { + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/ca.json b/homeassistant/components/gios/translations/ca.json index 8150e309b20..abfa55fef29 100644 --- a/homeassistant/components/gios/translations/ca.json +++ b/homeassistant/components/gios/translations/ca.json @@ -14,7 +14,6 @@ "name": "Nom", "station_id": "ID de l'estaci\u00f3 de mesura" }, - "description": "Integraci\u00f3 de mesura de qualitat de l'aire GIO\u015a (Polish Chief Inspectorate Of Environmental Protection). Si necessites ajuda amb la configuraci\u00f3, fes un cop d'ull a: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" } } diff --git a/homeassistant/components/gios/translations/cs.json b/homeassistant/components/gios/translations/cs.json index 8dea1f5e013..a80a5ae03bd 100644 --- a/homeassistant/components/gios/translations/cs.json +++ b/homeassistant/components/gios/translations/cs.json @@ -14,7 +14,6 @@ "name": "Jm\u00e9no", "station_id": "ID m\u011b\u0159ic\u00ed stanice" }, - "description": "Nastaven\u00ed integrace kvality ovzdu\u0161\u00ed GIO\u015a (polsk\u00fd hlavn\u00ed inspektor\u00e1t ochrany \u017eivotn\u00edho prost\u0159ed\u00ed). Pokud pot\u0159ebujete pomoc s nastaven\u00edm, pod\u00edvejte se na: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (polsk\u00fd hlavn\u00ed inspektor\u00e1t ochrany \u017eivotn\u00edho prost\u0159ed\u00ed)" } } diff --git a/homeassistant/components/gios/translations/da.json b/homeassistant/components/gios/translations/da.json index d4442982e1e..2d3a2e3e5e9 100644 --- a/homeassistant/components/gios/translations/da.json +++ b/homeassistant/components/gios/translations/da.json @@ -14,7 +14,6 @@ "name": "Navn p\u00e5 integrationen", "station_id": "ID for m\u00e5lestationen" }, - "description": "Ops\u00e6t GIO\u015a (polsk inspektorat for milj\u00f8beskyttelse) luftkvalitet-integration. Hvis du har brug for hj\u00e6lp med konfigurationen, kig her: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" } } diff --git a/homeassistant/components/gios/translations/de.json b/homeassistant/components/gios/translations/de.json index 99548187601..c22b2a197dd 100644 --- a/homeassistant/components/gios/translations/de.json +++ b/homeassistant/components/gios/translations/de.json @@ -14,7 +14,6 @@ "name": "Name", "station_id": "ID der Messstation" }, - "description": "Einrichtung von GIO\u015a (Polnische Hauptinspektion f\u00fcr Umweltschutz) Integration der Luftqualit\u00e4t. Wenn du Hilfe bei der Konfiguration ben\u00f6tigst, schaue hier: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Polnische Hauptinspektion f\u00fcr Umweltschutz)" } } diff --git a/homeassistant/components/gios/translations/el.json b/homeassistant/components/gios/translations/el.json index ae916d70667..46bcc2d0b9c 100644 --- a/homeassistant/components/gios/translations/el.json +++ b/homeassistant/components/gios/translations/el.json @@ -14,7 +14,6 @@ "name": "\u038c\u03bd\u03bf\u03bc\u03b1", "station_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c4\u03bf\u03c5 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2" }, - "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c0\u03bf\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c4\u03bf\u03c5 \u03b1\u03ad\u03c1\u03b1 GIO\u015a (\u03a0\u03bf\u03bb\u03c9\u03bd\u03b9\u03ba\u03ae \u0395\u03c0\u03b9\u03ba\u03b5\u03c6\u03b1\u03bb\u03ae\u03c2 \u0395\u03c0\u03b9\u03b8\u03b5\u03ce\u03c1\u03b7\u03c3\u03b7\u03c2 \u03a0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c3\u03af\u03b1\u03c2 \u03c4\u03bf\u03c5 \u03a0\u03b5\u03c1\u03b9\u03b2\u03ac\u03bb\u03bb\u03bf\u03bd\u03c4\u03bf\u03c2). \u0395\u03ac\u03bd \u03c7\u03c1\u03b5\u03b9\u03ac\u03b6\u03b5\u03c3\u03c4\u03b5 \u03b2\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1 \u03bc\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7, \u03c1\u03af\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03bc\u03b1\u03c4\u03b9\u03ac \u03b5\u03b4\u03ce: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (\u03a0\u03bf\u03bb\u03c9\u03bd\u03b9\u03ba\u03ae \u0393\u03b5\u03bd\u03b9\u03ba\u03ae \u0395\u03c0\u03b9\u03b8\u03b5\u03ce\u03c1\u03b7\u03c3\u03b7 \u03a0\u03c1\u03bf\u03c3\u03c4\u03b1\u03c3\u03af\u03b1\u03c2 \u03a0\u03b5\u03c1\u03b9\u03b2\u03ac\u03bb\u03bb\u03bf\u03bd\u03c4\u03bf\u03c2)" } } diff --git a/homeassistant/components/gios/translations/en.json b/homeassistant/components/gios/translations/en.json index 86f05b8987e..232389291be 100644 --- a/homeassistant/components/gios/translations/en.json +++ b/homeassistant/components/gios/translations/en.json @@ -14,7 +14,6 @@ "name": "Name", "station_id": "ID of the measuring station" }, - "description": "Set up GIO\u015a (Polish Chief Inspectorate Of Environmental Protection) air quality integration. If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" } } diff --git a/homeassistant/components/gios/translations/es-419.json b/homeassistant/components/gios/translations/es-419.json index 848247bdf75..52640708ce8 100644 --- a/homeassistant/components/gios/translations/es-419.json +++ b/homeassistant/components/gios/translations/es-419.json @@ -14,7 +14,6 @@ "name": "Nombre de la integraci\u00f3n", "station_id": "Identificaci\u00f3n de la estaci\u00f3n de medici\u00f3n" }, - "description": "Establecer la integraci\u00f3n de la calidad del aire GIO\u015a (Inspecci\u00f3n Jefe de Protecci\u00f3n Ambiental de Polonia). Si necesita ayuda con la configuraci\u00f3n, eche un vistazo aqu\u00ed: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" } } diff --git a/homeassistant/components/gios/translations/es.json b/homeassistant/components/gios/translations/es.json index 011ddd0b6f7..3d6bfc57150 100644 --- a/homeassistant/components/gios/translations/es.json +++ b/homeassistant/components/gios/translations/es.json @@ -14,7 +14,6 @@ "name": "Nombre", "station_id": "ID de la estaci\u00f3n de medici\u00f3n" }, - "description": "Configurar la integraci\u00f3n de la calidad del aire GIO\u015a (Inspecci\u00f3n Jefe de Protecci\u00f3n Ambiental de Polonia). Si necesita ayuda con la configuraci\u00f3n, eche un vistazo aqu\u00ed: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Inspecci\u00f3n Jefe de Protecci\u00f3n del Medio Ambiente de Polonia)" } } diff --git a/homeassistant/components/gios/translations/et.json b/homeassistant/components/gios/translations/et.json index 2d0906f73ab..8565849250e 100644 --- a/homeassistant/components/gios/translations/et.json +++ b/homeassistant/components/gios/translations/et.json @@ -14,7 +14,6 @@ "name": "Nimi", "station_id": "M\u00f5\u00f5tejaama ID" }, - "description": "Loo GIO\u015a (Poola keskkonnakaitse peainspektsioon) \u00f5hukvaliteedi sidumnie. Kui vajad seadistamisel abi, vaats siit: https://www.home-assistant.io/integrations/gios", "title": "" } } diff --git a/homeassistant/components/gios/translations/fr.json b/homeassistant/components/gios/translations/fr.json index af107914c06..5533e9c48bf 100644 --- a/homeassistant/components/gios/translations/fr.json +++ b/homeassistant/components/gios/translations/fr.json @@ -14,7 +14,6 @@ "name": "Nom", "station_id": "Identifiant de la station de mesure" }, - "description": "Mettre en place l'int\u00e9gration de la qualit\u00e9 de l'air GIO\u015a (Inspection g\u00e9n\u00e9rale polonaise de la protection de l'environnement). Si vous avez besoin d'aide pour la configuration, regardez ici: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Inspection g\u00e9n\u00e9rale polonaise de la protection de l'environnement)" } } diff --git a/homeassistant/components/gios/translations/hu.json b/homeassistant/components/gios/translations/hu.json index 9ad0e244eaf..87f54802368 100644 --- a/homeassistant/components/gios/translations/hu.json +++ b/homeassistant/components/gios/translations/hu.json @@ -14,7 +14,6 @@ "name": "Elnevez\u00e9s", "station_id": "A m\u00e9r\u0151\u00e1llom\u00e1s azonos\u00edt\u00f3ja" }, - "description": "A GIO\u015a (lengyel k\u00f6rnyezetv\u00e9delmi f\u0151fel\u00fcgyel\u0151) leveg\u0151min\u0151s\u00e9gi integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa. Ha seg\u00edts\u00e9gre van sz\u00fcks\u00e9ge a konfigur\u00e1ci\u00f3val kapcsolatban, l\u00e1togasson ide: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Lengyel K\u00f6rnyezetv\u00e9delmi F\u0151fel\u00fcgyel\u0151s\u00e9g)" } } diff --git a/homeassistant/components/gios/translations/id.json b/homeassistant/components/gios/translations/id.json index b32210c30d5..844b1e794b5 100644 --- a/homeassistant/components/gios/translations/id.json +++ b/homeassistant/components/gios/translations/id.json @@ -14,7 +14,6 @@ "name": "Nama", "station_id": "ID stasiun pengukuran" }, - "description": "Siapkan integrasi kualitas udara GIO\u015a (Inspektorat Jenderal Perlindungan Lingkungan Polandia). Jika Anda memerlukan bantuan tentang konfigurasi, baca di sini: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Inspektorat Jenderal Perlindungan Lingkungan Polandia)" } } diff --git a/homeassistant/components/gios/translations/it.json b/homeassistant/components/gios/translations/it.json index cb1f1cea6b0..bd88ded42b6 100644 --- a/homeassistant/components/gios/translations/it.json +++ b/homeassistant/components/gios/translations/it.json @@ -14,7 +14,6 @@ "name": "Nome", "station_id": "ID della stazione di misura" }, - "description": "Imposta l'integrazione della qualit\u00e0 dell'aria GIO\u015a (Ispettorato capo polacco di protezione ambientale). Se hai bisogno di aiuto con la configurazione dai un'occhiata qui: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Ispettorato capo polacco di protezione ambientale)" } } diff --git a/homeassistant/components/gios/translations/ja.json b/homeassistant/components/gios/translations/ja.json index ac05abbf923..f3c9464a7a5 100644 --- a/homeassistant/components/gios/translations/ja.json +++ b/homeassistant/components/gios/translations/ja.json @@ -14,7 +14,6 @@ "name": "\u540d\u524d", "station_id": "\u6e2c\u5b9a\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306eID" }, - "description": "GIO\u015a(Polish Chief Inspectorate Of Environmental Protection)\u306e\u5927\u6c17\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u3064\u3044\u3066\u30d8\u30eb\u30d7\u304c\u5fc5\u8981\u306a\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/gios \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" } } diff --git a/homeassistant/components/gios/translations/ko.json b/homeassistant/components/gios/translations/ko.json index e462ef4e3b6..c49bd258c50 100644 --- a/homeassistant/components/gios/translations/ko.json +++ b/homeassistant/components/gios/translations/ko.json @@ -14,7 +14,6 @@ "name": "\uc774\ub984", "station_id": "\uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc758 ID" }, - "description": "\ud3f4\ub780\ub4dc \ud658\uacbd\uccad (GIO\u015a) \ub300\uae30\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. \uad6c\uc131\uc5d0 \ub3c4\uc6c0\uc774 \ud544\uc694\ud55c \uacbd\uc6b0 https://www.home-assistant.io/integrations/gios \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694", "title": "\ud3f4\ub780\ub4dc \ud658\uacbd\uccad (GIO\u015a)" } } diff --git a/homeassistant/components/gios/translations/lb.json b/homeassistant/components/gios/translations/lb.json index cafea72fb78..22db9df424f 100644 --- a/homeassistant/components/gios/translations/lb.json +++ b/homeassistant/components/gios/translations/lb.json @@ -14,7 +14,6 @@ "name": "Numm", "station_id": "ID vun der Miess Statioun" }, - "description": "GIO\u015a (Polnesch Chefinspektorat vum \u00cbmweltschutz) Loft Qualit\u00e9it Integratioun ariichten. Fir w\u00e9ider H\u00ebllef mat der Konfiuratioun kuckt hei: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Polnesch Chefinspektorat vum \u00cbmweltschutz)" } } diff --git a/homeassistant/components/gios/translations/nl.json b/homeassistant/components/gios/translations/nl.json index 87104523a31..26c83cfff7b 100644 --- a/homeassistant/components/gios/translations/nl.json +++ b/homeassistant/components/gios/translations/nl.json @@ -14,7 +14,6 @@ "name": "Naam", "station_id": "ID van het meetstation" }, - "description": "GIO\u015a (Poolse hoofdinspectie van milieubescherming) luchtkwaliteitintegratie instellen. Als u hulp nodig hebt bij de configuratie, kijk dan hier: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Poolse hoofdinspectie van milieubescherming)" } } diff --git a/homeassistant/components/gios/translations/no.json b/homeassistant/components/gios/translations/no.json index 038cbdc20a3..68d02c7deec 100644 --- a/homeassistant/components/gios/translations/no.json +++ b/homeassistant/components/gios/translations/no.json @@ -14,7 +14,6 @@ "name": "Navn", "station_id": "ID til m\u00e5lestasjon" }, - "description": "Sett opp GIO\u015a (Polish Chief Inspectorate Of Environmental Protection) luftkvalitet integrasjon. Hvis du trenger hjelp med konfigurasjonen ta en titt her: https://www.home-assistant.io/integrations/gios", "title": "" } } diff --git a/homeassistant/components/gios/translations/pl.json b/homeassistant/components/gios/translations/pl.json index 8bc909e2bab..6a7d2aa7064 100644 --- a/homeassistant/components/gios/translations/pl.json +++ b/homeassistant/components/gios/translations/pl.json @@ -14,7 +14,6 @@ "name": "Nazwa", "station_id": "Identyfikator stacji pomiarowej" }, - "description": "Konfiguracja integracji jako\u015bci powietrza GIO\u015a (G\u0142\u00f3wny Inspektorat Ochrony \u015arodowiska). Je\u015bli potrzebujesz pomocy z konfiguracj\u0105, przejd\u017a na stron\u0119: https://www.home-assistant.io/integrations/gios", "title": "G\u0142\u00f3wny Inspektorat Ochrony \u015arodowiska (GIO\u015a)" } } diff --git a/homeassistant/components/gios/translations/pt-BR.json b/homeassistant/components/gios/translations/pt-BR.json index 39472c3b420..e4aad1faf74 100644 --- a/homeassistant/components/gios/translations/pt-BR.json +++ b/homeassistant/components/gios/translations/pt-BR.json @@ -14,7 +14,6 @@ "name": "Nome", "station_id": "ID da esta\u00e7\u00e3o de medi\u00e7\u00e3o" }, - "description": "Configurar a integra\u00e7\u00e3o da qualidade do ar GIO\u015a (Polish Chief Inspectorate Of Environmental Protection). Se precisar de ajuda com a configura\u00e7\u00e3o, d\u00ea uma olhada em https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Inspetor-Chefe Polon\u00eas de Prote\u00e7\u00e3o Ambiental)" } } diff --git a/homeassistant/components/gios/translations/ru.json b/homeassistant/components/gios/translations/ru.json index 68d6ee44b0b..ede5f2d9a70 100644 --- a/homeassistant/components/gios/translations/ru.json +++ b/homeassistant/components/gios/translations/ru.json @@ -14,7 +14,6 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "station_id": "ID \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438" }, - "description": "\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 \u043e\u0442 \u041f\u043e\u043b\u044c\u0441\u043a\u043e\u0439 \u0438\u043d\u0441\u043f\u0435\u043a\u0446\u0438\u0438 \u043f\u043e \u043e\u0445\u0440\u0430\u043d\u0435 \u043e\u043a\u0440\u0443\u0436\u0430\u044e\u0449\u0435\u0439 \u0441\u0440\u0435\u0434\u044b (GIO\u015a). \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0435\u0439 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438: https://www.home-assistant.io/integrations/gios.", "title": "GIO\u015a (\u041f\u043e\u043b\u044c\u0441\u043a\u0430\u044f \u0438\u043d\u0441\u043f\u0435\u043a\u0446\u0438\u044f \u043f\u043e \u043e\u0445\u0440\u0430\u043d\u0435 \u043e\u043a\u0440\u0443\u0436\u0430\u044e\u0449\u0435\u0439 \u0441\u0440\u0435\u0434\u044b)" } } diff --git a/homeassistant/components/gios/translations/sl.json b/homeassistant/components/gios/translations/sl.json index 4bbc28bfedd..bc8cac9941c 100644 --- a/homeassistant/components/gios/translations/sl.json +++ b/homeassistant/components/gios/translations/sl.json @@ -14,7 +14,6 @@ "name": "Ime integracije", "station_id": "ID merilne postaje" }, - "description": "Nastavite GIO\u015a (poljski glavni in\u0161pektorat za varstvo okolja) integracijo kakovosti zraka. \u010ce potrebujete pomo\u010d pri konfiguraciji si oglejte tukaj: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (glavni poljski in\u0161pektorat za varstvo okolja)" } } diff --git a/homeassistant/components/gios/translations/sv.json b/homeassistant/components/gios/translations/sv.json index a8bafa50119..98e9333c821 100644 --- a/homeassistant/components/gios/translations/sv.json +++ b/homeassistant/components/gios/translations/sv.json @@ -14,7 +14,6 @@ "name": "Integrationens namn", "station_id": "M\u00e4tstationens ID" }, - "description": "St\u00e4ll in luftkvalitetintegration f\u00f6r GIO\u015a (polsk chefinspektorat f\u00f6r milj\u00f6skydd). Om du beh\u00f6ver hj\u00e4lp med konfigurationen titta h\u00e4r: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" } } diff --git a/homeassistant/components/gios/translations/tr.json b/homeassistant/components/gios/translations/tr.json index c0444ed99ed..e9419d53963 100644 --- a/homeassistant/components/gios/translations/tr.json +++ b/homeassistant/components/gios/translations/tr.json @@ -14,7 +14,6 @@ "name": "Ad", "station_id": "\u00d6l\u00e7\u00fcm istasyonunun kimli\u011fi" }, - "description": "GIO\u015a (Polonya \u00c7evre Koruma Ba\u015f M\u00fcfetti\u015fli\u011fi) hava kalitesi entegrasyonunu kurun. Yap\u0131land\u0131rmayla ilgili yard\u0131ma ihtiyac\u0131n\u0131z varsa buraya bak\u0131n: https://www.home-assistant.io/integrations/gios", "title": "GIO\u015a (Polonya \u00c7evre Koruma Ba\u015f M\u00fcfetti\u015fli\u011fi)" } } diff --git a/homeassistant/components/gios/translations/uk.json b/homeassistant/components/gios/translations/uk.json index f62408c5e8e..8151bf21809 100644 --- a/homeassistant/components/gios/translations/uk.json +++ b/homeassistant/components/gios/translations/uk.json @@ -14,7 +14,6 @@ "name": "\u041d\u0430\u0437\u0432\u0430", "station_id": "ID \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043b\u044c\u043d\u043e\u0457 \u0441\u0442\u0430\u043d\u0446\u0456\u0457" }, - "description": "\u0406\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044f \u043f\u0440\u043e \u044f\u043a\u0456\u0441\u0442\u044c \u043f\u043e\u0432\u0456\u0442\u0440\u044f \u0432\u0456\u0434 \u041f\u043e\u043b\u044c\u0441\u044c\u043a\u043e\u0457 \u0456\u043d\u0441\u043f\u0435\u043a\u0446\u0456\u0457 \u0437 \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \u043d\u0430\u0432\u043a\u043e\u043b\u0438\u0448\u043d\u044c\u043e\u0433\u043e \u0441\u0435\u0440\u0435\u0434\u043e\u0432\u0438\u0449\u0430 (GIO\u015a). \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0454\u044e \u043f\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044e \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457: https://www.home-assistant.io/integrations/gios.", "title": "GIO\u015a (\u041f\u043e\u043b\u044c\u0441\u044c\u043a\u0430 \u0456\u043d\u0441\u043f\u0435\u043a\u0446\u0456\u044f \u0437 \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \u043d\u0430\u0432\u043a\u043e\u043b\u0438\u0448\u043d\u044c\u043e\u0433\u043e \u0441\u0435\u0440\u0435\u0434\u043e\u0432\u0438\u0449\u0430)" } } diff --git a/homeassistant/components/gios/translations/zh-Hant.json b/homeassistant/components/gios/translations/zh-Hant.json index 98a62385ee1..209367511e7 100644 --- a/homeassistant/components/gios/translations/zh-Hant.json +++ b/homeassistant/components/gios/translations/zh-Hant.json @@ -14,7 +14,6 @@ "name": "\u540d\u7a31", "station_id": "\u76e3\u6e2c\u7ad9 ID" }, - "description": "\u8a2d\u5b9a GIO\u015a\uff08\u6ce2\u862d\u7e3d\u74b0\u5883\u4fdd\u8b77\u7763\u5bdf\u8655\uff09\u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u5047\u5982\u9700\u8981\u5354\u52a9\uff0c\u8acb\u53c3\u8003\uff1ahttps://www.home-assistant.io/integrations/gios", "title": "GIO\u015a\uff08\u6ce2\u862d\u7e3d\u74b0\u5883\u4fdd\u8b77\u7763\u5bdf\u8655\uff09" } } diff --git a/homeassistant/components/goalzero/translations/ca.json b/homeassistant/components/goalzero/translations/ca.json index 3e7d6e714d3..d5ef3e9eda3 100644 --- a/homeassistant/components/goalzero/translations/ca.json +++ b/homeassistant/components/goalzero/translations/ca.json @@ -12,16 +12,14 @@ }, "step": { "confirm_discovery": { - "description": "Es recomana que la reserva DHCP del router estigui configurada. Si no ho est\u00e0, pot ser que el dispositiu no estigui disponible mentre Home Assistant no detecti la nova IP. Consulta el manual del router.", - "title": "Goal Zero Yeti" + "description": "Es recomana que la reserva DHCP del router estigui configurada. Si no ho est\u00e0, pot ser que el dispositiu no estigui disponible mentre Home Assistant no detecti la nova IP. Consulta el manual del router." }, "user": { "data": { "host": "Amfitri\u00f3", "name": "Nom" }, - "description": "Consulta la documentaci\u00f3 per assegurar-te que compleixes tots els requisits.", - "title": "Goal Zero Yeti" + "description": "Consulta la documentaci\u00f3 per assegurar-te que compleixes tots els requisits." } } } diff --git a/homeassistant/components/goalzero/translations/cs.json b/homeassistant/components/goalzero/translations/cs.json index 2f2735f3c45..c8d0ab45fd6 100644 --- a/homeassistant/components/goalzero/translations/cs.json +++ b/homeassistant/components/goalzero/translations/cs.json @@ -15,8 +15,7 @@ "host": "Hostitel", "name": "Jm\u00e9no" }, - "description": "Nejprve si mus\u00edte st\u00e1hnout aplikaci Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\nPodle pokyn\u016f p\u0159ipojte za\u0159\u00edzen\u00ed Yeti k s\u00edti Wifi. Pot\u00e9 z\u00edskejte hostitelskou IP z routeru. Aby se IP hostitele nezm\u011bnila, mus\u00ed b\u00fdt v nastaven\u00ed routeru pro za\u0159\u00edzen\u00ed nastaven DHCP. Informace nalezenete v u\u017eivatelsk\u00e9 p\u0159\u00edru\u010dce k routeru.", - "title": "Goal Zero Yeti" + "description": "Nejprve si mus\u00edte st\u00e1hnout aplikaci Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\nPodle pokyn\u016f p\u0159ipojte za\u0159\u00edzen\u00ed Yeti k s\u00edti Wifi. Pot\u00e9 z\u00edskejte hostitelskou IP z routeru. Aby se IP hostitele nezm\u011bnila, mus\u00ed b\u00fdt v nastaven\u00ed routeru pro za\u0159\u00edzen\u00ed nastaven DHCP. Informace nalezenete v u\u017eivatelsk\u00e9 p\u0159\u00edru\u010dce k routeru." } } } diff --git a/homeassistant/components/goalzero/translations/de.json b/homeassistant/components/goalzero/translations/de.json index a9f7e3fa887..d41a2238854 100644 --- a/homeassistant/components/goalzero/translations/de.json +++ b/homeassistant/components/goalzero/translations/de.json @@ -12,16 +12,14 @@ }, "step": { "confirm_discovery": { - "description": "Eine DHCP-Reservierung auf deinem Router wird empfohlen. Wenn sie nicht eingerichtet ist, ist das Ger\u00e4t m\u00f6glicherweise nicht mehr verf\u00fcgbar, bis Home Assistant die neue IP-Adresse erkennt. Schlage im Benutzerhandbuch deines Routers nach.", - "title": "Goal Zero Yeti" + "description": "Eine DHCP-Reservierung auf deinem Router wird empfohlen. Wenn sie nicht eingerichtet ist, ist das Ger\u00e4t m\u00f6glicherweise nicht mehr verf\u00fcgbar, bis Home Assistant die neue IP-Adresse erkennt. Schlage im Benutzerhandbuch deines Routers nach." }, "user": { "data": { "host": "Host", "name": "Name" }, - "description": "Bitte lies die Dokumentation, um sicherzustellen, dass alle Anforderungen erf\u00fcllt sind.", - "title": "Goal Zero Yeti" + "description": "Bitte lies die Dokumentation, um sicherzustellen, dass alle Anforderungen erf\u00fcllt sind." } } } diff --git a/homeassistant/components/goalzero/translations/el.json b/homeassistant/components/goalzero/translations/el.json index 865106aa33c..6e793797046 100644 --- a/homeassistant/components/goalzero/translations/el.json +++ b/homeassistant/components/goalzero/translations/el.json @@ -12,16 +12,14 @@ }, "step": { "confirm_discovery": { - "description": "\u03a3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b7 \u03ba\u03c1\u03ac\u03c4\u03b7\u03c3\u03b7 DHCP \u03c3\u03c4\u03bf \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2. \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af, \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03bd\u03b4\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03bc\u03b7\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03ad\u03c9\u03c2 \u03cc\u03c4\u03bf\u03c5 \u03c4\u03bf Home Assistant \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03b5\u03b9 \u03c4\u03b7 \u03bd\u03ad\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 ip. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b5\u03b3\u03c7\u03b5\u03b9\u03c1\u03af\u03b4\u03b9\u03bf \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2.", - "title": "Goal Zero Yeti" + "description": "\u03a3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b7 \u03ba\u03c1\u03ac\u03c4\u03b7\u03c3\u03b7 DHCP \u03c3\u03c4\u03bf \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2. \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af, \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03bd\u03b4\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03bc\u03b7\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03ad\u03c9\u03c2 \u03cc\u03c4\u03bf\u03c5 \u03c4\u03bf Home Assistant \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03b5\u03b9 \u03c4\u03b7 \u03bd\u03ad\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 ip. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b5\u03b3\u03c7\u03b5\u03b9\u03c1\u03af\u03b4\u03b9\u03bf \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2." }, "user": { "data": { "host": "\u0394\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2", "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, - "description": "\u03a0\u03c1\u03ce\u03c4\u03b1, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b5\u03b2\u03ac\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\n \u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Yeti \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf Wi-Fi. \u03a3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b7 \u03ba\u03c1\u03ac\u03c4\u03b7\u03c3\u03b7 DHCP \u03c3\u03c4\u03bf \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2. \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af, \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03bd\u03b4\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03bc\u03b7\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03ad\u03c9\u03c2 \u03cc\u03c4\u03bf\u03c5 \u03bf \u0392\u03bf\u03b7\u03b8\u03cc\u03c2 \u039f\u03b9\u03ba\u03af\u03b1\u03c2 \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03b5\u03b9 \u03c4\u03b7 \u03bd\u03ad\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b5\u03b3\u03c7\u03b5\u03b9\u03c1\u03af\u03b4\u03b9\u03bf \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2.", - "title": "Goal Zero Yeti" + "description": "\u03a0\u03c1\u03ce\u03c4\u03b1, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b5\u03b2\u03ac\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\n \u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Yeti \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf Wi-Fi. \u03a3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b7 \u03ba\u03c1\u03ac\u03c4\u03b7\u03c3\u03b7 DHCP \u03c3\u03c4\u03bf \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2. \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af, \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03bd\u03b4\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03bc\u03b7\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03ad\u03c9\u03c2 \u03cc\u03c4\u03bf\u03c5 \u03bf \u0392\u03bf\u03b7\u03b8\u03cc\u03c2 \u039f\u03b9\u03ba\u03af\u03b1\u03c2 \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03b5\u03b9 \u03c4\u03b7 \u03bd\u03ad\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b5\u03b3\u03c7\u03b5\u03b9\u03c1\u03af\u03b4\u03b9\u03bf \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2." } } } diff --git a/homeassistant/components/goalzero/translations/en.json b/homeassistant/components/goalzero/translations/en.json index fd27892c794..7c7237ce0af 100644 --- a/homeassistant/components/goalzero/translations/en.json +++ b/homeassistant/components/goalzero/translations/en.json @@ -12,16 +12,14 @@ }, "step": { "confirm_discovery": { - "description": "DHCP reservation on your router is recommended. If not set up, the device may become unavailable until Home Assistant detects the new ip address. Refer to your router's user manual.", - "title": "Goal Zero Yeti" + "description": "DHCP reservation on your router is recommended. If not set up, the device may become unavailable until Home Assistant detects the new ip address. Refer to your router's user manual." }, "user": { "data": { "host": "Host", "name": "Name" }, - "description": "Please refer to the documentation to make sure all requirements are met.", - "title": "Goal Zero Yeti" + "description": "Please refer to the documentation to make sure all requirements are met." } } } diff --git a/homeassistant/components/goalzero/translations/es.json b/homeassistant/components/goalzero/translations/es.json index 921941d1c16..e02b06d32f5 100644 --- a/homeassistant/components/goalzero/translations/es.json +++ b/homeassistant/components/goalzero/translations/es.json @@ -12,16 +12,14 @@ }, "step": { "confirm_discovery": { - "description": "Se recomienda reservar el DHCP en el router. Si no se configura, el dispositivo puede dejar de estar disponible hasta que el Home Assistant detecte la nueva direcci\u00f3n ip. Consulte el manual de usuario de su router.", - "title": "Goal Zero Yeti" + "description": "Se recomienda reservar el DHCP en el router. Si no se configura, el dispositivo puede dejar de estar disponible hasta que el Home Assistant detecte la nueva direcci\u00f3n ip. Consulte el manual de usuario de su router." }, "user": { "data": { "host": "Host", "name": "Nombre" }, - "description": "Primero, tienes que descargar la aplicaci\u00f3n Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nSigue las instrucciones para conectar tu Yeti a tu red Wifi. Luego obt\u00e9n la IP de tu router. El DHCP debe estar configurado en los ajustes de tu router para asegurar que la IP de host del dispositivo no cambie. Consulta el manual de usuario de tu router.", - "title": "Goal Zero Yeti" + "description": "Primero, tienes que descargar la aplicaci\u00f3n Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nSigue las instrucciones para conectar tu Yeti a tu red Wifi. Luego obt\u00e9n la IP de tu router. El DHCP debe estar configurado en los ajustes de tu router para asegurar que la IP de host del dispositivo no cambie. Consulta el manual de usuario de tu router." } } } diff --git a/homeassistant/components/goalzero/translations/et.json b/homeassistant/components/goalzero/translations/et.json index e529f0a3a4e..2947a5b82b6 100644 --- a/homeassistant/components/goalzero/translations/et.json +++ b/homeassistant/components/goalzero/translations/et.json @@ -12,16 +12,14 @@ }, "step": { "confirm_discovery": { - "description": "Soovitatav on DHCP aadressi reserveerimine ruuteris. Kui seda pole seadistatud, v\u00f5ib seade osutuda k\u00e4ttesaamatuks kuni Home Assistant tuvastab uue IP-aadressi. Vaata ruuteri kasutusjuhendit.", - "title": "Goal Zero Yeti" + "description": "Soovitatav on DHCP aadressi reserveerimine ruuteris. Kui seda pole seadistatud, v\u00f5ib seade osutuda k\u00e4ttesaamatuks kuni Home Assistant tuvastab uue IP-aadressi. Vaata ruuteri kasutusjuhendit." }, "user": { "data": { "host": "", "name": "Nimi" }, - "description": "Vaata dokumentatsiooni, et veenduda, et k\u00f5ik n\u00f5uded on t\u00e4idetud.", - "title": "" + "description": "Vaata dokumentatsiooni, et veenduda, et k\u00f5ik n\u00f5uded on t\u00e4idetud." } } } diff --git a/homeassistant/components/goalzero/translations/fr.json b/homeassistant/components/goalzero/translations/fr.json index 430ad9927c4..4c2e987e096 100644 --- a/homeassistant/components/goalzero/translations/fr.json +++ b/homeassistant/components/goalzero/translations/fr.json @@ -12,16 +12,14 @@ }, "step": { "confirm_discovery": { - "description": "La r\u00e9servation DHCP sur votre routeur est recommand\u00e9e. S'il n'est pas configur\u00e9, l'appareil peut devenir indisponible jusqu'\u00e0 ce que Home Assistant d\u00e9tecte la nouvelle adresse IP. Reportez-vous au manuel d'utilisation de votre routeur.", - "title": "Objectif Z\u00e9ro Y\u00e9ti" + "description": "La r\u00e9servation DHCP sur votre routeur est recommand\u00e9e. S'il n'est pas configur\u00e9, l'appareil peut devenir indisponible jusqu'\u00e0 ce que Home Assistant d\u00e9tecte la nouvelle adresse IP. Reportez-vous au manuel d'utilisation de votre routeur." }, "user": { "data": { "host": "H\u00f4te", "name": "Nom" }, - "description": "Veuillez consulter la documentation afin de vous assurer que toutes les conditions sont respect\u00e9es.", - "title": "Goal Zero Yeti" + "description": "Veuillez consulter la documentation afin de vous assurer que toutes les conditions sont respect\u00e9es." } } } diff --git a/homeassistant/components/goalzero/translations/hu.json b/homeassistant/components/goalzero/translations/hu.json index 00196952e0c..c0376fc29b9 100644 --- a/homeassistant/components/goalzero/translations/hu.json +++ b/homeassistant/components/goalzero/translations/hu.json @@ -12,16 +12,14 @@ }, "step": { "confirm_discovery": { - "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" + "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." }, "user": { "data": { "host": "C\u00edm", "name": "Elnevez\u00e9s" }, - "description": "K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, hogy megbizonyosodjon arr\u00f3l, hogy minden k\u00f6vetelm\u00e9ny teljes\u00fcl.", - "title": "Goal Zero Yeti" + "description": "K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, hogy megbizonyosodjon arr\u00f3l, hogy minden k\u00f6vetelm\u00e9ny teljes\u00fcl." } } } diff --git a/homeassistant/components/goalzero/translations/id.json b/homeassistant/components/goalzero/translations/id.json index d524b13bb0c..c34fd9edfd9 100644 --- a/homeassistant/components/goalzero/translations/id.json +++ b/homeassistant/components/goalzero/translations/id.json @@ -12,16 +12,14 @@ }, "step": { "confirm_discovery": { - "description": "Dianjurkan untuk menggunakan reservasi DHCP pada router Anda. Jika tidak diatur, perangkat mungkin tidak tersedia hingga Home Assistant mendeteksi alamat IP baru. Lihat panduan pengguna router Anda.", - "title": "Goal Zero Yeti" + "description": "Dianjurkan untuk menggunakan reservasi DHCP pada router Anda. Jika tidak diatur, perangkat mungkin tidak tersedia hingga Home Assistant mendeteksi alamat IP baru. Lihat panduan pengguna router Anda." }, "user": { "data": { "host": "Host", "name": "Nama" }, - "description": "Rujuk ke dokumentasi untuk memastikan semua persyaratan terpenuhi.", - "title": "Goal Zero Yeti" + "description": "Rujuk ke dokumentasi untuk memastikan semua persyaratan terpenuhi." } } } diff --git a/homeassistant/components/goalzero/translations/it.json b/homeassistant/components/goalzero/translations/it.json index dbd71c82f4a..af88e8713e8 100644 --- a/homeassistant/components/goalzero/translations/it.json +++ b/homeassistant/components/goalzero/translations/it.json @@ -12,16 +12,14 @@ }, "step": { "confirm_discovery": { - "description": "Si consiglia la prenotazione DHCP sul router. Se non configurato, il dispositivo potrebbe non essere disponibile fino a quando Home Assistant non rileva il nuovo indirizzo IP. Fai riferimento al manuale utente del router.", - "title": "Goal Zero Yeti" + "description": "Si consiglia la prenotazione DHCP sul router. Se non configurato, il dispositivo potrebbe non essere disponibile fino a quando Home Assistant non rileva il nuovo indirizzo IP. Fai riferimento al manuale utente del router." }, "user": { "data": { "host": "Host", "name": "Nome" }, - "description": "Fai riferimento alla documentazione per assicurarti che tutti i requisiti siano soddisfatti.", - "title": "Goal Zero Yeti" + "description": "Fai riferimento alla documentazione per assicurarti che tutti i requisiti siano soddisfatti." } } } diff --git a/homeassistant/components/goalzero/translations/ja.json b/homeassistant/components/goalzero/translations/ja.json index 3e2e33bc302..72818b6627f 100644 --- a/homeassistant/components/goalzero/translations/ja.json +++ b/homeassistant/components/goalzero/translations/ja.json @@ -12,16 +12,14 @@ }, "step": { "confirm_discovery": { - "description": "\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04(DHCP reservation)\u3092\u304a\u52e7\u3081\u3057\u307e\u3059\u3002\u3053\u306e\u8a2d\u5b9a\u3092\u884c\u3063\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u306f\u3001Home Assistant\u304c\u65b0\u3057\u3044IP\u30a2\u30c9\u30ec\u30b9\u3092\u691c\u51fa\u3059\u308b\u307e\u3067\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "Goal Zero Yeti" + "description": "\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04(DHCP reservation)\u3092\u304a\u52e7\u3081\u3057\u307e\u3059\u3002\u3053\u306e\u8a2d\u5b9a\u3092\u884c\u3063\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u306f\u3001Home Assistant\u304c\u65b0\u3057\u3044IP\u30a2\u30c9\u30ec\u30b9\u3092\u691c\u51fa\u3059\u308b\u307e\u3067\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "user": { "data": { "host": "\u30db\u30b9\u30c8", "name": "\u540d\u524d" }, - "description": "\u307e\u305a\u3001Goal Zero app\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: https://www.goalzero.com/product-features/yeti-app/\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04(DHCP reservation)\u3092\u304a\u52e7\u3081\u3057\u307e\u3059\u3002\u3053\u306e\u8a2d\u5b9a\u3092\u884c\u3063\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u306f\u3001Home Assistant\u304c\u65b0\u3057\u3044IP\u30a2\u30c9\u30ec\u30b9\u3092\u691c\u51fa\u3059\u308b\u307e\u3067\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "Goal Zero Yeti" + "description": "\u307e\u305a\u3001Goal Zero app\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: https://www.goalzero.com/product-features/yeti-app/\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04(DHCP reservation)\u3092\u304a\u52e7\u3081\u3057\u307e\u3059\u3002\u3053\u306e\u8a2d\u5b9a\u3092\u884c\u3063\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u306f\u3001Home Assistant\u304c\u65b0\u3057\u3044IP\u30a2\u30c9\u30ec\u30b9\u3092\u691c\u51fa\u3059\u308b\u307e\u3067\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } } diff --git a/homeassistant/components/goalzero/translations/ko.json b/homeassistant/components/goalzero/translations/ko.json index d5119363002..dd947530c16 100644 --- a/homeassistant/components/goalzero/translations/ko.json +++ b/homeassistant/components/goalzero/translations/ko.json @@ -14,8 +14,7 @@ "host": "\ud638\uc2a4\ud2b8", "name": "\uc774\ub984" }, - "description": "\uba3c\uc800 Goal Zero \uc571\uc744 \ub2e4\uc6b4\ub85c\ub4dc\ud574\uc57c \ud569\ub2c8\ub2e4. https://www.goalzero.com/product-features/yeti-app/\n\n\uc9c0\uce68\uc5d0 \ub530\ub77c Yeti\ub97c Wifi \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ud55c \ub2e4\uc74c \ub77c\uc6b0\ud130\uc5d0\uc11c \ud638\uc2a4\ud2b8 IP\ub97c \uac00\uc838\uc640\uc8fc\uc138\uc694. \ud638\uc2a4\ud2b8 IP\uac00 \ubcc0\uacbd\ub418\uc9c0 \uc54a\ub3c4\ub85d \ud558\ub824\uba74 \uae30\uae30\uc5d0 \ub300\ud574 \ub77c\uc6b0\ud130\uc5d0\uc11c DHCP\ub97c \uc54c\ub9de\uac8c \uc124\uc815\ud574\uc8fc\uc5b4\uc57c \ud569\ub2c8\ub2e4. \ud574\ub2f9 \ub0b4\uc6a9\uc5d0 \ub300\ud574\uc11c\ub294 \ub77c\uc6b0\ud130\uc758 \uc0ac\uc6a9\uc790 \uc124\uba85\uc11c\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", - "title": "Goal Zero Yeti" + "description": "\uba3c\uc800 Goal Zero \uc571\uc744 \ub2e4\uc6b4\ub85c\ub4dc\ud574\uc57c \ud569\ub2c8\ub2e4. https://www.goalzero.com/product-features/yeti-app/\n\n\uc9c0\uce68\uc5d0 \ub530\ub77c Yeti\ub97c Wifi \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ud55c \ub2e4\uc74c \ub77c\uc6b0\ud130\uc5d0\uc11c \ud638\uc2a4\ud2b8 IP\ub97c \uac00\uc838\uc640\uc8fc\uc138\uc694. \ud638\uc2a4\ud2b8 IP\uac00 \ubcc0\uacbd\ub418\uc9c0 \uc54a\ub3c4\ub85d \ud558\ub824\uba74 \uae30\uae30\uc5d0 \ub300\ud574 \ub77c\uc6b0\ud130\uc5d0\uc11c DHCP\ub97c \uc54c\ub9de\uac8c \uc124\uc815\ud574\uc8fc\uc5b4\uc57c \ud569\ub2c8\ub2e4. \ud574\ub2f9 \ub0b4\uc6a9\uc5d0 \ub300\ud574\uc11c\ub294 \ub77c\uc6b0\ud130\uc758 \uc0ac\uc6a9\uc790 \uc124\uba85\uc11c\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." } } } diff --git a/homeassistant/components/goalzero/translations/lb.json b/homeassistant/components/goalzero/translations/lb.json index 87a5590bfe4..203b70a269c 100644 --- a/homeassistant/components/goalzero/translations/lb.json +++ b/homeassistant/components/goalzero/translations/lb.json @@ -14,8 +14,7 @@ "host": "Host", "name": "Numm" }, - "description": "Fir d'\u00e9ischt muss Goal Zero App erofgeluede ginn:\nhttps://www.goalzero.com/product-features/yeti-app/\n\nFolleg d'Instruktioune fir d\u00e4in Yeti mat dengem Wifi ze verbannen. Dann erm\u00ebttel d'IP vum Yeti an dengem Router. DHCP muss aktiv sinn an de Yeti Apparat sollt \u00ebmmer d\u00e9iselwecht IP zougewise kr\u00e9ien. Kuck dat am Guide vun dengen Router Astellungen no.", - "title": "Goal Zero Yeti" + "description": "Fir d'\u00e9ischt muss Goal Zero App erofgeluede ginn:\nhttps://www.goalzero.com/product-features/yeti-app/\n\nFolleg d'Instruktioune fir d\u00e4in Yeti mat dengem Wifi ze verbannen. Dann erm\u00ebttel d'IP vum Yeti an dengem Router. DHCP muss aktiv sinn an de Yeti Apparat sollt \u00ebmmer d\u00e9iselwecht IP zougewise kr\u00e9ien. Kuck dat am Guide vun dengen Router Astellungen no." } } } diff --git a/homeassistant/components/goalzero/translations/nl.json b/homeassistant/components/goalzero/translations/nl.json index 680ff1a10cc..9f719d74efe 100644 --- a/homeassistant/components/goalzero/translations/nl.json +++ b/homeassistant/components/goalzero/translations/nl.json @@ -12,16 +12,14 @@ }, "step": { "confirm_discovery": { - "description": "DHCP-reservering op uw router wordt aanbevolen. Als dit niet het geval is, is het apparaat mogelijk niet meer beschikbaar totdat Home Assistant het nieuwe IP-adres detecteert. Raadpleeg de gebruikershandleiding van uw router.", - "title": "Goal Zero Yeti" + "description": "DHCP-reservering op uw router wordt aanbevolen. Als dit niet het geval is, is het apparaat mogelijk niet meer beschikbaar totdat Home Assistant het nieuwe IP-adres detecteert. Raadpleeg de gebruikershandleiding van uw router." }, "user": { "data": { "host": "Host", "name": "Naam" }, - "description": "Raadpleeg de documentatie om er zeker van te zijn dat aan alle vereisten wordt voldaan.", - "title": "Goal Zero Yeti" + "description": "Raadpleeg de documentatie om er zeker van te zijn dat aan alle vereisten wordt voldaan." } } } diff --git a/homeassistant/components/goalzero/translations/no.json b/homeassistant/components/goalzero/translations/no.json index 6bb00952fab..9c2da19b3e1 100644 --- a/homeassistant/components/goalzero/translations/no.json +++ b/homeassistant/components/goalzero/translations/no.json @@ -12,16 +12,14 @@ }, "step": { "confirm_discovery": { - "description": "DHCP-reservasjon p\u00e5 ruteren din anbefales. Hvis den ikke er konfigurert, kan enheten bli utilgjengelig til Home Assistant oppdager den nye ip-adressen. Se i brukerh\u00e5ndboken til ruteren.", - "title": "Goal Zero Yeti" + "description": "DHCP-reservasjon p\u00e5 ruteren din anbefales. Hvis den ikke er konfigurert, kan enheten bli utilgjengelig til Home Assistant oppdager den nye ip-adressen. Se i brukerh\u00e5ndboken til ruteren." }, "user": { "data": { "host": "Vert", "name": "Navn" }, - "description": "Se dokumentasjonen for \u00e5 sikre at alle krav er oppfylt.", - "title": "" + "description": "Se dokumentasjonen for \u00e5 sikre at alle krav er oppfylt." } } } diff --git a/homeassistant/components/goalzero/translations/pl.json b/homeassistant/components/goalzero/translations/pl.json index ee8f2022a0d..91b73044f11 100644 --- a/homeassistant/components/goalzero/translations/pl.json +++ b/homeassistant/components/goalzero/translations/pl.json @@ -12,16 +12,14 @@ }, "step": { "confirm_discovery": { - "description": "Zaleca si\u0119 rezerwacj\u0119 DHCP w ustawieniach routera. Je\u015bli tego nie ustawisz, urz\u0105dzenie mo\u017ce sta\u0107 si\u0119 niedost\u0119pne, do czasu a\u017c Home Assistant wykryje nowy adres IP. Post\u0119puj wg instrukcji obs\u0142ugi routera.", - "title": "Goal Zero Yeti" + "description": "Zaleca si\u0119 rezerwacj\u0119 DHCP w ustawieniach routera. Je\u015bli tego nie ustawisz, urz\u0105dzenie mo\u017ce sta\u0107 si\u0119 niedost\u0119pne, do czasu a\u017c Home Assistant wykryje nowy adres IP. Post\u0119puj wg instrukcji obs\u0142ugi routera." }, "user": { "data": { "host": "Nazwa hosta lub adres IP", "name": "Nazwa" }, - "description": "Zapoznaj si\u0119 z dokumentacj\u0105, aby upewni\u0107 si\u0119, \u017ce wszystkie wymagania s\u0105 spe\u0142nione.", - "title": "Goal Zero Yeti" + "description": "Zapoznaj si\u0119 z dokumentacj\u0105, aby upewni\u0107 si\u0119, \u017ce wszystkie wymagania s\u0105 spe\u0142nione." } } } diff --git a/homeassistant/components/goalzero/translations/pt-BR.json b/homeassistant/components/goalzero/translations/pt-BR.json index fa67129b08a..7561acac487 100644 --- a/homeassistant/components/goalzero/translations/pt-BR.json +++ b/homeassistant/components/goalzero/translations/pt-BR.json @@ -12,16 +12,14 @@ }, "step": { "confirm_discovery": { - "description": "A reserva de DHCP em seu roteador \u00e9 recomendada. Se n\u00e3o estiver configurado, o dispositivo pode ficar indispon\u00edvel at\u00e9 que o Home Assistant detecte o novo endere\u00e7o IP. Consulte o manual do usu\u00e1rio do seu roteador.", - "title": "Gol Zero Yeti" + "description": "A reserva de DHCP em seu roteador \u00e9 recomendada. Se n\u00e3o estiver configurado, o dispositivo pode ficar indispon\u00edvel at\u00e9 que o Home Assistant detecte o novo endere\u00e7o IP. Consulte o manual do usu\u00e1rio do seu roteador." }, "user": { "data": { "host": "Nome do host", "name": "Nome" }, - "description": "Consulte a documenta\u00e7\u00e3o para garantir que todos os requisitos sejam atendidos.", - "title": "Gol Zero Yeti" + "description": "Consulte a documenta\u00e7\u00e3o para garantir que todos os requisitos sejam atendidos." } } } diff --git a/homeassistant/components/goalzero/translations/ru.json b/homeassistant/components/goalzero/translations/ru.json index ca114f6d92e..7bd8c3df311 100644 --- a/homeassistant/components/goalzero/translations/ru.json +++ b/homeassistant/components/goalzero/translations/ru.json @@ -12,16 +12,14 @@ }, "step": { "confirm_discovery": { - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0442\u0430\u043a\u0438\u043c\u0438, \u0447\u0442\u043e\u0431\u044b IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u044f\u043b\u0441\u044f \u0441\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0435\u043c. \u0412 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435, \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043c\u043e\u0436\u0435\u0442 \u0441\u0442\u0430\u0442\u044c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c \u0434\u043e \u0442\u0435\u0445 \u043f\u043e\u0440, \u043f\u043e\u043a\u0430 Home Assistant \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442 \u043d\u043e\u0432\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441. \u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u043a \u0440\u0443\u043a\u043e\u0432\u043e\u0434\u0441\u0442\u0432\u0443 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u0440\u043e\u0443\u0442\u0435\u0440\u0430.", - "title": "Goal Zero Yeti" + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u0442\u0430\u043a\u0438\u043c\u0438, \u0447\u0442\u043e\u0431\u044b IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u044f\u043b\u0441\u044f \u0441\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0435\u043c. \u0412 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435, \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043c\u043e\u0436\u0435\u0442 \u0441\u0442\u0430\u0442\u044c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c \u0434\u043e \u0442\u0435\u0445 \u043f\u043e\u0440, \u043f\u043e\u043a\u0430 Home Assistant \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442 \u043d\u043e\u0432\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441. \u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u043a \u0440\u0443\u043a\u043e\u0432\u043e\u0434\u0441\u0442\u0432\u0443 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u0440\u043e\u0443\u0442\u0435\u0440\u0430." }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439, \u0447\u0442\u043e\u0431\u044b \u0443\u0431\u0435\u0434\u0438\u0442\u044c\u0441\u044f, \u0447\u0442\u043e \u0432\u0441\u0435 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u043e\u0431\u043b\u044e\u0434\u0435\u043d\u044b.", - "title": "Goal Zero Yeti" + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439, \u0447\u0442\u043e\u0431\u044b \u0443\u0431\u0435\u0434\u0438\u0442\u044c\u0441\u044f, \u0447\u0442\u043e \u0432\u0441\u0435 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u043e\u0431\u043b\u044e\u0434\u0435\u043d\u044b." } } } diff --git a/homeassistant/components/goalzero/translations/tr.json b/homeassistant/components/goalzero/translations/tr.json index 9be07def514..db04511a8f5 100644 --- a/homeassistant/components/goalzero/translations/tr.json +++ b/homeassistant/components/goalzero/translations/tr.json @@ -12,16 +12,14 @@ }, "step": { "confirm_discovery": { - "description": "Y\u00f6nlendiricinizde DHCP rezervasyonu yap\u0131lmas\u0131 \u00f6nerilir. Kurulmazsa, Home Assistant yeni ip adresini alg\u0131layana kadar cihaz kullan\u0131lamayabilir. Y\u00f6nlendiricinizin kullan\u0131m k\u0131lavuzuna bak\u0131n.", - "title": "Goal Zero Yeti" + "description": "Y\u00f6nlendiricinizde DHCP rezervasyonu yap\u0131lmas\u0131 \u00f6nerilir. Kurulmazsa, Home Assistant yeni ip adresini alg\u0131layana kadar cihaz kullan\u0131lamayabilir. Y\u00f6nlendiricinizin kullan\u0131m k\u0131lavuzuna bak\u0131n." }, "user": { "data": { "host": "Sunucu", "name": "Ad" }, - "description": "T\u00fcm gereksinimlerin kar\u015f\u0131land\u0131\u011f\u0131ndan emin olmak i\u00e7in l\u00fctfen belgelere bak\u0131n.", - "title": "Goal Zero Yeti" + "description": "T\u00fcm gereksinimlerin kar\u015f\u0131land\u0131\u011f\u0131ndan emin olmak i\u00e7in l\u00fctfen belgelere bak\u0131n." } } } diff --git a/homeassistant/components/goalzero/translations/uk.json b/homeassistant/components/goalzero/translations/uk.json index 6d67d949c28..6232a0af027 100644 --- a/homeassistant/components/goalzero/translations/uk.json +++ b/homeassistant/components/goalzero/translations/uk.json @@ -14,8 +14,7 @@ "host": "\u0425\u043e\u0441\u0442", "name": "\u041d\u0430\u0437\u0432\u0430" }, - "description": "\u0421\u043f\u043e\u0447\u0430\u0442\u043a\u0443 \u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0437\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0438\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Goal Zero: https://www.goalzero.com/product-features/yeti-app/. \n\n \u0414\u043e\u0442\u0440\u0438\u043c\u0443\u0439\u0442\u0435\u0441\u044c \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0439 \u043f\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044e Yeti \u0434\u043e \u043c\u0435\u0440\u0435\u0436\u0456 WiFi. \u041f\u043e\u0442\u0456\u043c \u0434\u0456\u0437\u043d\u0430\u0439\u0442\u0435\u0441\u044f IP \u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e, \u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434, \u0437 \u0412\u0430\u0448\u043e\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430. \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u0442\u0430\u043a\u0438\u043c\u0438, \u0449\u043e\u0431 IP \u0430\u0434\u0440\u0435\u0441\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u043d\u0435 \u0437\u043c\u0456\u043d\u044e\u0432\u0430\u043b\u0430\u0441\u044c \u0437 \u0447\u0430\u0441\u043e\u043c. \u041f\u0440\u043e \u0442\u0435, \u044f\u043a \u0446\u0435 \u0437\u0440\u043e\u0431\u0438\u0442\u0438, \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0456\u0437\u043d\u0430\u0442\u0438\u0441\u044f \u0432 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0457 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0412\u0430\u0448\u043e\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430.", - "title": "Goal Zero Yeti" + "description": "\u0421\u043f\u043e\u0447\u0430\u0442\u043a\u0443 \u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0437\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0438\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Goal Zero: https://www.goalzero.com/product-features/yeti-app/. \n\n \u0414\u043e\u0442\u0440\u0438\u043c\u0443\u0439\u0442\u0435\u0441\u044c \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0439 \u043f\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044e Yeti \u0434\u043e \u043c\u0435\u0440\u0435\u0436\u0456 WiFi. \u041f\u043e\u0442\u0456\u043c \u0434\u0456\u0437\u043d\u0430\u0439\u0442\u0435\u0441\u044f IP \u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e, \u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434, \u0437 \u0412\u0430\u0448\u043e\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430. \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u0442\u0430\u043a\u0438\u043c\u0438, \u0449\u043e\u0431 IP \u0430\u0434\u0440\u0435\u0441\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u043d\u0435 \u0437\u043c\u0456\u043d\u044e\u0432\u0430\u043b\u0430\u0441\u044c \u0437 \u0447\u0430\u0441\u043e\u043c. \u041f\u0440\u043e \u0442\u0435, \u044f\u043a \u0446\u0435 \u0437\u0440\u043e\u0431\u0438\u0442\u0438, \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0456\u0437\u043d\u0430\u0442\u0438\u0441\u044f \u0432 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0457 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0412\u0430\u0448\u043e\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430." } } } diff --git a/homeassistant/components/goalzero/translations/zh-Hant.json b/homeassistant/components/goalzero/translations/zh-Hant.json index 4f5b01fb9d4..2e5efb73350 100644 --- a/homeassistant/components/goalzero/translations/zh-Hant.json +++ b/homeassistant/components/goalzero/translations/zh-Hant.json @@ -12,16 +12,14 @@ }, "step": { "confirm_discovery": { - "description": "\u5efa\u8b70\u65bc\u8def\u7531\u5668\u7684 DHCP \u8a2d\u5b9a\u4e2d\u4fdd\u7559\u56fa\u5b9a IP\uff0c\u5047\u5982\u672a\u8a2d\u5b9a\u3001\u88dd\u7f6e\u53ef\u80fd\u6703\u5728 Home Assistant \u5075\u6e2c\u5230\u65b0 IP \u4e4b\u524d\u8b8a\u6210\u7121\u6cd5\u4f7f\u7528\u3002\u8acb\u53c3\u95b1\u8def\u7531\u5668\u624b\u518a\u4e86\u89e3\u5982\u4f55\u8a2d\u5b9a\u3002", - "title": "Goal Zero Yeti" + "description": "\u5efa\u8b70\u65bc\u8def\u7531\u5668\u7684 DHCP \u8a2d\u5b9a\u4e2d\u4fdd\u7559\u56fa\u5b9a IP\uff0c\u5047\u5982\u672a\u8a2d\u5b9a\u3001\u88dd\u7f6e\u53ef\u80fd\u6703\u5728 Home Assistant \u5075\u6e2c\u5230\u65b0 IP \u4e4b\u524d\u8b8a\u6210\u7121\u6cd5\u4f7f\u7528\u3002\u8acb\u53c3\u95b1\u8def\u7531\u5668\u624b\u518a\u4e86\u89e3\u5982\u4f55\u8a2d\u5b9a\u3002" }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", "name": "\u540d\u7a31" }, - "description": "\u8acb\u53c3\u8003\u76f8\u95dc\u6587\u4ef6\u4ee5\u4e86\u89e3\u6240\u6709\u5fc5\u8981\u9700\u6c42\u3002", - "title": "Goal Zero Yeti" + "description": "\u8acb\u53c3\u8003\u76f8\u95dc\u6587\u4ef6\u4ee5\u4e86\u89e3\u6240\u6709\u5fc5\u8981\u9700\u6c42\u3002" } } } diff --git a/homeassistant/components/google/translations/ko.json b/homeassistant/components/google/translations/ko.json new file mode 100644 index 00000000000..2906ebc5114 --- /dev/null +++ b/homeassistant/components/google/translations/ko.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/bg.json b/homeassistant/components/group/translations/bg.json index 584d48e3f28..6be0657c774 100644 --- a/homeassistant/components/group/translations/bg.json +++ b/homeassistant/components/group/translations/bg.json @@ -8,22 +8,6 @@ }, "title": "\u041d\u043e\u0432\u0430 \u0433\u0440\u0443\u043f\u0430" }, - "cover": { - "data": { - "title": "\u041d\u043e\u0432\u0430 \u0433\u0440\u0443\u043f\u0430" - } - }, - "fan": { - "data": { - "title": "\u041d\u043e\u0432\u0430 \u0433\u0440\u0443\u043f\u0430" - } - }, - "light": { - "data": { - "all": "\u0412\u0441\u0438\u0447\u043a\u0438 \u043e\u0431\u0435\u043a\u0442\u0438", - "title": "\u041d\u043e\u0432\u0430 \u0433\u0440\u0443\u043f\u0430" - } - }, "lock": { "data": { "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435", @@ -32,8 +16,7 @@ }, "media_player": { "data": { - "name": "\u0418\u043c\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0430", - "title": "\u041d\u043e\u0432\u0430 \u0433\u0440\u0443\u043f\u0430" + "name": "\u0418\u043c\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0430" } }, "switch": { @@ -56,42 +39,21 @@ "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" } }, - "binary_sensor_options": { - "data": { - "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" - } - }, "cover": { "data": { "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" } }, - "cover_options": { - "data": { - "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" - } - }, "fan": { "data": { "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" } }, - "fan_options": { - "data": { - "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" - } - }, "light": { "data": { "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" } }, - "light_options": { - "data": { - "all": "\u0412\u0441\u0438\u0447\u043a\u0438 \u043e\u0431\u0435\u043a\u0442\u0438", - "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" - } - }, "lock": { "data": { "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" @@ -102,11 +64,6 @@ "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" } }, - "media_player_options": { - "data": { - "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435" - } - }, "switch": { "data": { "all": "\u0412\u0441\u0438\u0447\u043a\u0438 \u043e\u0431\u0435\u043a\u0442\u0438", diff --git a/homeassistant/components/group/translations/ca.json b/homeassistant/components/group/translations/ca.json index bd824fc7428..bb9fba7e184 100644 --- a/homeassistant/components/group/translations/ca.json +++ b/homeassistant/components/group/translations/ca.json @@ -15,57 +15,26 @@ "data": { "entities": "Membres", "hide_members": "Amaga membres", - "name": "Nom", - "title": "Afegeix grup" + "name": "Nom" }, - "description": "Selecciona les opcions del grup", "title": "Afegeix grup" }, - "cover_options": { - "data": { - "entities": "Membres del grup" - }, - "description": "Selecciona les opcions del grup" - }, "fan": { "data": { "entities": "Membres", "hide_members": "Amaga membres", - "name": "Nom", - "title": "Afegeix grup" + "name": "Nom" }, - "description": "Selecciona les opcions del grup", "title": "Afegeix grup" }, - "fan_options": { - "data": { - "entities": "Membres del grup" - }, - "description": "Selecciona les opcions del grup" - }, - "init": { - "data": { - "group_type": "Tipus de grup" - }, - "description": "Selecciona el tipus de grup" - }, "light": { "data": { - "all": "Totes les entitats", "entities": "Membres", "hide_members": "Amaga membres", - "name": "Nom", - "title": "Afegeix grup" + "name": "Nom" }, - "description": "Si \"totes les entitats\" est\u00e0 activat, l'estat del grup estar\u00e0 activat (ON) si tots els membres estan activats. Si \"totes les entitats\" est\u00e0 desactivat, l'estat del grup s'activar\u00e0 si hi ha activat qualsevol membre.", "title": "Afegeix grup" }, - "light_options": { - "data": { - "entities": "Membres del grup" - }, - "description": "Selecciona les opcions del grup" - }, "lock": { "data": { "entities": "Membres", @@ -78,18 +47,10 @@ "data": { "entities": "Membres", "hide_members": "Amaga membres", - "name": "Nom", - "title": "Afegeix grup" + "name": "Nom" }, - "description": "Selecciona les opcions del grup", "title": "Afegeix grup" }, - "media_player_options": { - "data": { - "entities": "Membres del grup" - }, - "description": "Selecciona les opcions del grup" - }, "switch": { "data": { "entities": "Membres", @@ -99,9 +60,6 @@ "title": "Afegeix grup" }, "user": { - "data": { - "group_type": "Tipus de grup" - }, "description": "Els grups et permeten crear una nova entitat que representa m\u00faltiples entitats del mateix tipus.", "menu_options": { "binary_sensor": "Grup de sensors binaris", @@ -126,34 +84,18 @@ }, "description": "Si \"totes les entitats\" est\u00e0 activat, l'estat del grup estar\u00e0 activat (ON) si tots els membres estan activats. Si \"totes les entitats\" est\u00e0 desactivat, l'estat del grup s'activar\u00e0 si hi ha activat qualsevol membre." }, - "binary_sensor_options": { - "data": { - "all": "Totes les entitats", - "entities": "Membres" - } - }, "cover": { "data": { "entities": "Membres", "hide_members": "Amaga membres" } }, - "cover_options": { - "data": { - "entities": "Membres" - } - }, "fan": { "data": { "entities": "Membres", "hide_members": "Amaga membres" } }, - "fan_options": { - "data": { - "entities": "Membres" - } - }, "light": { "data": { "all": "Totes les entitats", @@ -162,12 +104,6 @@ }, "description": "Si \"totes les entitats\" est\u00e0 activat, l'estat del grup estar\u00e0 activat (ON) si tots els membres estan activats. Si \"totes les entitats\" est\u00e0 desactivat, l'estat del grup s'activar\u00e0 si hi ha activat qualsevol membre." }, - "light_options": { - "data": { - "all": "Totes les entitats", - "entities": "Membres" - } - }, "lock": { "data": { "entities": "Membres", @@ -180,11 +116,6 @@ "hide_members": "Amaga membres" } }, - "media_player_options": { - "data": { - "entities": "Membres" - } - }, "switch": { "data": { "all": "Totes les entitats", diff --git a/homeassistant/components/group/translations/cs.json b/homeassistant/components/group/translations/cs.json index 232dbe9e629..9d24e3580a2 100644 --- a/homeassistant/components/group/translations/cs.json +++ b/homeassistant/components/group/translations/cs.json @@ -15,57 +15,26 @@ "data": { "entities": "\u010clenov\u00e9", "hide_members": "Skr\u00fdt \u010dleny", - "name": "Jm\u00e9no", - "title": "Nov\u00e1 skupina" + "name": "Jm\u00e9no" }, - "description": "Vyberte mo\u017enosti skupiny", "title": "Nov\u00e1 skupina" }, - "cover_options": { - "data": { - "entities": "\u010clenov\u00e9 skupiny" - }, - "description": "Vyberte mo\u017enosti skupiny" - }, "fan": { "data": { "entities": "\u010clenov\u00e9", "hide_members": "Skr\u00fdt \u010dleny", - "name": "Jm\u00e9no", - "title": "Nov\u00e1 skupina" + "name": "Jm\u00e9no" }, - "description": "Vyberte mo\u017enosti skupiny", "title": "Nov\u00e1 skupina" }, - "fan_options": { - "data": { - "entities": "\u010clenov\u00e9 skupiny" - }, - "description": "Vyberte mo\u017enosti skupiny" - }, - "init": { - "data": { - "group_type": "Typ skupiny" - }, - "description": "Vyberte typ skupiny" - }, "light": { "data": { - "all": "V\u0161echny entity", "entities": "\u010clenov\u00e9", "hide_members": "Skr\u00fdt \u010dleny", - "name": "Jm\u00e9no", - "title": "Nov\u00e1 skupina" + "name": "Jm\u00e9no" }, - "description": "Pokud je povoleno \"v\u0161echny entity\", je stav skupiny zapnut\u00fd pouze tehdy, pokud jsou zapnuti v\u0161ichni \u010dlenov\u00e9.\nPokud je mo\u017enost \"v\u0161echny entity\" zak\u00e1z\u00e1na, je stav skupiny zapnut\u00fd, pokud je zapnut\u00fd kter\u00fdkoli \u010dlen.", "title": "Nov\u00e1 skupina" }, - "light_options": { - "data": { - "entities": "\u010clenov\u00e9 skupiny" - }, - "description": "Vyberte mo\u017enosti skupiny" - }, "lock": { "data": { "entities": "\u010clenov\u00e9", @@ -78,18 +47,10 @@ "data": { "entities": "\u010clenov\u00e9", "hide_members": "Skr\u00fdt \u010dleny", - "name": "Jm\u00e9no", - "title": "Nov\u00e1 skupina" + "name": "Jm\u00e9no" }, - "description": "Vyberte mo\u017enosti skupiny", "title": "Nov\u00e1 skupina" }, - "media_player_options": { - "data": { - "entities": "\u010clenov\u00e9 skupiny" - }, - "description": "Vyberte mo\u017enosti skupiny" - }, "switch": { "data": { "entities": "\u010clenov\u00e9", @@ -99,9 +60,6 @@ "title": "Nov\u00e1 skupina" }, "user": { - "data": { - "group_type": "Typ skupiny" - }, "description": "Vyberte typ skupiny", "menu_options": { "binary_sensor": "Skupina bin\u00e1rn\u00edch senzor\u016f", @@ -126,34 +84,18 @@ }, "description": "Pokud je povoleno \"v\u0161echny entity\", je stav skupiny zapnut\u00fd pouze tehdy, pokud jsou zapnuti v\u0161ichni \u010dlenov\u00e9.\nPokud je mo\u017enost \"v\u0161echny entity\" zak\u00e1z\u00e1na, je stav skupiny zapnut\u00fd, pokud je zapnut\u00fd kter\u00fdkoli \u010dlen." }, - "binary_sensor_options": { - "data": { - "all": "V\u0161echny entity", - "entities": "\u010clenov\u00e9" - } - }, "cover": { "data": { "entities": "\u010clenov\u00e9", "hide_members": "Skr\u00fdt \u010dleny" } }, - "cover_options": { - "data": { - "entities": "\u010clenov\u00e9" - } - }, "fan": { "data": { "entities": "\u010clenov\u00e9", "hide_members": "Skr\u00fdt \u010dleny" } }, - "fan_options": { - "data": { - "entities": "\u010clenov\u00e9" - } - }, "light": { "data": { "all": "V\u0161echny entity", @@ -162,12 +104,6 @@ }, "description": "Pokud je povoleno \"v\u0161echny entity\", je stav skupiny zapnut\u00fd pouze tehdy, pokud jsou zapnuti v\u0161ichni \u010dlenov\u00e9.\nPokud je mo\u017enost \"v\u0161echny entity\" zak\u00e1z\u00e1na, je stav skupiny zapnut\u00fd, pokud je zapnut\u00fd kter\u00fdkoli \u010dlen." }, - "light_options": { - "data": { - "all": "V\u0161echny entity", - "entities": "\u010clenov\u00e9" - } - }, "lock": { "data": { "entities": "\u010clenov\u00e9", @@ -180,11 +116,6 @@ "hide_members": "Skr\u00fdt \u010dleny" } }, - "media_player_options": { - "data": { - "entities": "\u010clenov\u00e9" - } - }, "switch": { "data": { "all": "V\u0161echny entity", diff --git a/homeassistant/components/group/translations/de.json b/homeassistant/components/group/translations/de.json index c76c92ccb19..00c036d18ee 100644 --- a/homeassistant/components/group/translations/de.json +++ b/homeassistant/components/group/translations/de.json @@ -15,57 +15,26 @@ "data": { "entities": "Mitglieder", "hide_members": "Mitglieder ausblenden", - "name": "Name", - "title": "Gruppe hinzuf\u00fcgen" + "name": "Name" }, - "description": "Gruppenoptionen ausw\u00e4hlen", "title": "Gruppe hinzuf\u00fcgen" }, - "cover_options": { - "data": { - "entities": "Gruppenmitglieder" - }, - "description": "Gruppenoptionen ausw\u00e4hlen" - }, "fan": { "data": { "entities": "Mitglieder", "hide_members": "Mitglieder ausblenden", - "name": "Name", - "title": "Gruppe hinzuf\u00fcgen" + "name": "Name" }, - "description": "Gruppenoptionen ausw\u00e4hlen", "title": "Gruppe hinzuf\u00fcgen" }, - "fan_options": { - "data": { - "entities": "Gruppenmitglieder" - }, - "description": "Gruppenoptionen ausw\u00e4hlen" - }, - "init": { - "data": { - "group_type": "Gruppentyp" - }, - "description": "Gruppentyp ausw\u00e4hlen" - }, "light": { "data": { - "all": "Alle Entit\u00e4ten", "entities": "Mitglieder", "hide_members": "Mitglieder ausblenden", - "name": "Name", - "title": "Gruppe hinzuf\u00fcgen" + "name": "Name" }, - "description": "Wenn \"alle Entit\u00e4ten\" aktiviert ist, ist der Status der Gruppe nur dann eingeschaltet, wenn alle Mitglieder eingeschaltet sind. Wenn \"alle Entit\u00e4ten\" deaktiviert ist, ist der Status der Gruppe eingeschaltet, wenn irgendein Mitglied eingeschaltet ist.", "title": "Gruppe hinzuf\u00fcgen" }, - "light_options": { - "data": { - "entities": "Gruppenmitglieder" - }, - "description": "Gruppenoptionen ausw\u00e4hlen" - }, "lock": { "data": { "entities": "Mitglieder", @@ -78,18 +47,10 @@ "data": { "entities": "Mitglieder", "hide_members": "Mitglieder ausblenden", - "name": "Name", - "title": "Gruppe hinzuf\u00fcgen" + "name": "Name" }, - "description": "Gruppenoptionen ausw\u00e4hlen", "title": "Gruppe hinzuf\u00fcgen" }, - "media_player_options": { - "data": { - "entities": "Gruppenmitglieder" - }, - "description": "Gruppenoptionen ausw\u00e4hlen" - }, "switch": { "data": { "entities": "Mitglieder", @@ -99,9 +60,6 @@ "title": "Gruppe hinzuf\u00fcgen" }, "user": { - "data": { - "group_type": "Gruppentyp" - }, "description": "Mit Gruppen kannst du eine neue Entit\u00e4t erstellen, die mehrere Entit\u00e4ten desselben Typs darstellt.", "menu_options": { "binary_sensor": "Bin\u00e4rer Sensor-Gruppe", @@ -126,34 +84,18 @@ }, "description": "Wenn \"alle Entit\u00e4ten\" aktiviert ist, ist der Status der Gruppe nur dann eingeschaltet, wenn alle Mitglieder eingeschaltet sind. Wenn \"alle Entit\u00e4ten\" deaktiviert ist, ist der Status der Gruppe eingeschaltet, wenn irgendein Mitglied eingeschaltet ist." }, - "binary_sensor_options": { - "data": { - "all": "Alle Entit\u00e4ten", - "entities": "Mitglieder" - } - }, "cover": { "data": { "entities": "Mitglieder", "hide_members": "Mitglieder ausblenden" } }, - "cover_options": { - "data": { - "entities": "Mitglieder" - } - }, "fan": { "data": { "entities": "Mitglieder", "hide_members": "Mitglieder ausblenden" } }, - "fan_options": { - "data": { - "entities": "Mitglieder" - } - }, "light": { "data": { "all": "Alle Entit\u00e4ten", @@ -162,12 +104,6 @@ }, "description": "Wenn \"alle Entit\u00e4ten\" aktiviert ist, ist der Status der Gruppe nur dann eingeschaltet, wenn alle Mitglieder eingeschaltet sind. Wenn \"alle Entit\u00e4ten\" deaktiviert ist, ist der Status der Gruppe eingeschaltet, wenn irgendein Mitglied eingeschaltet ist." }, - "light_options": { - "data": { - "all": "Alle Entit\u00e4ten", - "entities": "Mitglieder" - } - }, "lock": { "data": { "entities": "Mitglieder", @@ -180,11 +116,6 @@ "hide_members": "Mitglieder ausblenden" } }, - "media_player_options": { - "data": { - "entities": "Mitglieder" - } - }, "switch": { "data": { "all": "Alle Entit\u00e4ten", diff --git a/homeassistant/components/group/translations/el.json b/homeassistant/components/group/translations/el.json index edd67034de8..df1a5982f2c 100644 --- a/homeassistant/components/group/translations/el.json +++ b/homeassistant/components/group/translations/el.json @@ -15,57 +15,26 @@ "data": { "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", - "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, - "cover_options": { - "data": { - "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" - }, "fan": { "data": { "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", - "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, - "fan_options": { - "data": { - "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" - }, - "init": { - "data": { - "group_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03cd\u03c0\u03bf \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" - }, "light": { "data": { - "all": "\u038c\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2", "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", - "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, - "light_options": { - "data": { - "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" - }, "lock": { "data": { "entities": "\u039c\u03ad\u03bb\u03b7", @@ -78,18 +47,10 @@ "data": { "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", - "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, - "media_player_options": { - "data": { - "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" - }, "switch": { "data": { "entities": "\u039c\u03ad\u03bb\u03b7", @@ -99,9 +60,6 @@ "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, "user": { - "data": { - "group_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" - }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03cd\u03c0\u03bf \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", "menu_options": { "binary_sensor": "\u039f\u03bc\u03ac\u03b4\u03b1 \u03b4\u03c5\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03c9\u03bd", @@ -126,34 +84,18 @@ }, "description": "\u0395\u03ac\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \"\u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\", \u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03bc\u03cc\u03bd\u03bf \u03b5\u03ac\u03bd \u03cc\u03bb\u03b1 \u03c4\u03b1 \u03bc\u03ad\u03bb\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b1. \u0395\u03ac\u03bd \u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \"\u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\" \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7, \u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b5\u03ac\u03bd \u03bf\u03c0\u03bf\u03b9\u03bf\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5 \u03bc\u03ad\u03bb\u03bf\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf." }, - "binary_sensor_options": { - "data": { - "all": "\u038c\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2", - "entities": "\u039c\u03ad\u03bb\u03b7" - } - }, "cover": { "data": { "entities": "\u039c\u03ad\u03bb\u03b7", "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd" } }, - "cover_options": { - "data": { - "entities": "\u039c\u03ad\u03bb\u03b7" - } - }, "fan": { "data": { "entities": "\u039c\u03ad\u03bb\u03b7", "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd" } }, - "fan_options": { - "data": { - "entities": "\u039c\u03ad\u03bb\u03b7" - } - }, "light": { "data": { "all": "\u038c\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2", @@ -162,12 +104,6 @@ }, "description": "\u0395\u03ac\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \"\u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\", \u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03bc\u03cc\u03bd\u03bf \u03b5\u03ac\u03bd \u03cc\u03bb\u03b1 \u03c4\u03b1 \u03bc\u03ad\u03bb\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b1. \u0395\u03ac\u03bd \u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \"\u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\" \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7, \u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b5\u03ac\u03bd \u03bf\u03c0\u03bf\u03b9\u03bf\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5 \u03bc\u03ad\u03bb\u03bf\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf." }, - "light_options": { - "data": { - "all": "\u038c\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2", - "entities": "\u039c\u03ad\u03bb\u03b7" - } - }, "lock": { "data": { "entities": "\u039c\u03ad\u03bb\u03b7", @@ -180,11 +116,6 @@ "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd" } }, - "media_player_options": { - "data": { - "entities": "\u039c\u03ad\u03bb\u03b7" - } - }, "switch": { "data": { "all": "\u038c\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2", diff --git a/homeassistant/components/group/translations/en.json b/homeassistant/components/group/translations/en.json index f7b3942c696..a67d23b812d 100644 --- a/homeassistant/components/group/translations/en.json +++ b/homeassistant/components/group/translations/en.json @@ -15,57 +15,26 @@ "data": { "entities": "Members", "hide_members": "Hide members", - "name": "Name", - "title": "Add Group" + "name": "Name" }, - "description": "Select group options", "title": "Add Group" }, - "cover_options": { - "data": { - "entities": "Group members" - }, - "description": "Select group options" - }, "fan": { "data": { "entities": "Members", "hide_members": "Hide members", - "name": "Name", - "title": "Add Group" + "name": "Name" }, - "description": "Select group options", "title": "Add Group" }, - "fan_options": { - "data": { - "entities": "Group members" - }, - "description": "Select group options" - }, - "init": { - "data": { - "group_type": "Group type" - }, - "description": "Select group type" - }, "light": { "data": { - "all": "All entities", "entities": "Members", "hide_members": "Hide members", - "name": "Name", - "title": "Add Group" + "name": "Name" }, - "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on.", "title": "Add Group" }, - "light_options": { - "data": { - "entities": "Group members" - }, - "description": "Select group options" - }, "lock": { "data": { "entities": "Members", @@ -78,18 +47,10 @@ "data": { "entities": "Members", "hide_members": "Hide members", - "name": "Name", - "title": "Add Group" + "name": "Name" }, - "description": "Select group options", "title": "Add Group" }, - "media_player_options": { - "data": { - "entities": "Group members" - }, - "description": "Select group options" - }, "switch": { "data": { "entities": "Members", @@ -99,9 +60,6 @@ "title": "Add Group" }, "user": { - "data": { - "group_type": "Group type" - }, "description": "Groups allow you to create a new entity that represents multiple entities of the same type.", "menu_options": { "binary_sensor": "Binary sensor group", @@ -126,34 +84,18 @@ }, "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on." }, - "binary_sensor_options": { - "data": { - "all": "All entities", - "entities": "Members" - } - }, "cover": { "data": { "entities": "Members", "hide_members": "Hide members" } }, - "cover_options": { - "data": { - "entities": "Members" - } - }, "fan": { "data": { "entities": "Members", "hide_members": "Hide members" } }, - "fan_options": { - "data": { - "entities": "Members" - } - }, "light": { "data": { "all": "All entities", @@ -162,12 +104,6 @@ }, "description": "If \"all entities\" is enabled, the group's state is on only if all members are on. If \"all entities\" is disabled, the group's state is on if any member is on." }, - "light_options": { - "data": { - "all": "All entities", - "entities": "Members" - } - }, "lock": { "data": { "entities": "Members", @@ -180,11 +116,6 @@ "hide_members": "Hide members" } }, - "media_player_options": { - "data": { - "entities": "Members" - } - }, "switch": { "data": { "all": "All entities", diff --git a/homeassistant/components/group/translations/es.json b/homeassistant/components/group/translations/es.json index 719d42c772a..67742954447 100644 --- a/homeassistant/components/group/translations/es.json +++ b/homeassistant/components/group/translations/es.json @@ -10,50 +10,23 @@ "cover": { "data": { "hide_members": "Esconde miembros", - "name": "Nombre del Grupo", - "title": "Agregar grupo" - }, - "description": "Seleccionar opciones de grupo" - }, - "cover_options": { - "data": { - "entities": "Miembros del grupo" + "name": "Nombre del Grupo" } }, "fan": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros", - "title": "Agregar grupo" + "hide_members": "Esconde miembros" }, - "description": "Selecciona las opciones del grupo", "title": "Agregar grupo" }, - "fan_options": { - "data": { - "entities": "Miembros del grupo" - } - }, - "init": { - "data": { - "group_type": "Tipo de grupo" - }, - "description": "Selecciona el tipo de grupo" - }, "light": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros", - "title": "Agregar grupo" + "hide_members": "Esconde miembros" }, "title": "Agregar grupo" }, - "light_options": { - "data": { - "entities": "Miembros del grupo" - }, - "description": "Selecciona las opciones del grupo" - }, "lock": { "data": { "entities": "Miembros", @@ -67,12 +40,8 @@ "hide_members": "Esconde miembros", "name": "Nombre" }, - "description": "Selecciona las opciones del grupo", "title": "Agregar grupo" }, - "media_player_options": { - "description": "Selecciona las opciones del grupo" - }, "switch": { "data": { "entities": "Miembros" @@ -96,43 +65,22 @@ "hide_members": "Esconde miembros" } }, - "binary_sensor_options": { - "data": { - "all": "Todas las entidades" - } - }, "cover": { "data": { "hide_members": "Esconde miembros" } }, - "cover_options": { - "data": { - "entities": "Miembros" - } - }, "fan": { "data": { "hide_members": "Esconde miembros" } }, - "fan_options": { - "data": { - "entities": "Miembros" - } - }, "light": { "data": { "entities": "Miembros", "hide_members": "Esconde miembros" } }, - "light_options": { - "data": { - "all": "Todas las entidades", - "entities": "Miembros" - } - }, "lock": { "data": { "entities": "Miembros" @@ -143,11 +91,6 @@ "entities": "Miembros", "hide_members": "Esconde miembros" } - }, - "media_player_options": { - "data": { - "entities": "Miembros" - } } } }, diff --git a/homeassistant/components/group/translations/et.json b/homeassistant/components/group/translations/et.json index d0d1466e26b..709d7b2c929 100644 --- a/homeassistant/components/group/translations/et.json +++ b/homeassistant/components/group/translations/et.json @@ -15,57 +15,26 @@ "data": { "entities": "Liikmed", "hide_members": "Peida grupi liikmed", - "name": "Nimi", - "title": "Uus r\u00fchm" + "name": "Nimi" }, - "description": "R\u00fchmasuvandite valimine", "title": "Uus grupp" }, - "cover_options": { - "data": { - "entities": "R\u00fchma liikmed" - }, - "description": "R\u00fchmasuvandite valimine" - }, "fan": { "data": { "entities": "Liikmed", "hide_members": "Peida grupi liikmed", - "name": "Nimi", - "title": "Uus r\u00fchm" + "name": "Nimi" }, - "description": "R\u00fchmasuvandite valimine", "title": "Uus grupp" }, - "fan_options": { - "data": { - "entities": "R\u00fchma liikmed" - }, - "description": "R\u00fchmasuvandite valimine" - }, - "init": { - "data": { - "group_type": "R\u00fchma t\u00fc\u00fcp" - }, - "description": "Vali r\u00fchma t\u00fc\u00fcp" - }, "light": { "data": { - "all": "K\u00f5ik olemid", "entities": "Liikmed", "hide_members": "Peida grupi liikmed", - "name": "Nimi", - "title": "Uus r\u00fchm" + "name": "Nimi" }, - "description": "Kui on vlitud \"K\u00f5ik olemid\" siis on grupi olek Sees kui k\u00f5ik olemid on sees. Kui \"K\u00f5ik olemid\" on keelatud siis on grupi olek Sees kui m\u00f5ni olem on sisse l\u00fclitatud.", "title": "Uus grupp" }, - "light_options": { - "data": { - "entities": "R\u00fchma liikmed" - }, - "description": "R\u00fchmasuvandite valimine" - }, "lock": { "data": { "entities": "Liikmed", @@ -78,18 +47,10 @@ "data": { "entities": "Liikmed", "hide_members": "Peida grupi liikmed", - "name": "Nimi", - "title": "Uus r\u00fchm" + "name": "Nimi" }, - "description": "R\u00fchmasuvandite valimine", "title": "Uus grupp" }, - "media_player_options": { - "data": { - "entities": "R\u00fchma liikmed" - }, - "description": "R\u00fchmasuvandite valimine" - }, "switch": { "data": { "entities": "Liikmed", @@ -99,9 +60,6 @@ "title": "Uus grupp" }, "user": { - "data": { - "group_type": "R\u00fchma t\u00fc\u00fcp" - }, "description": "R\u00fchmad v\u00f5imaldavad luua uue olemi,mis esindab mitut sama t\u00fc\u00fcpi olemit.", "menu_options": { "binary_sensor": "Olekuandurite r\u00fchm", @@ -126,34 +84,18 @@ }, "description": "Kui \"k\u00f5ik olemid\" on lubatud,on r\u00fchma olek sees ainult siis kui k\u00f5ik liikmed on sisse l\u00fclitatud. Kui \"k\u00f5ik olemid\" on keelatud, on r\u00fchma olek sees kui m\u00f5ni liige on sisse l\u00fclitatud." }, - "binary_sensor_options": { - "data": { - "all": "K\u00f5ik olemid", - "entities": "Liikmed" - } - }, "cover": { "data": { "entities": "Grupi liikmed", "hide_members": "Peida grupi liikmed" } }, - "cover_options": { - "data": { - "entities": "Liikmed" - } - }, "fan": { "data": { "entities": "Grupi liikmed", "hide_members": "Peida grupi liikmed" } }, - "fan_options": { - "data": { - "entities": "Liikmed" - } - }, "light": { "data": { "all": "K\u00f5ik olemid", @@ -162,12 +104,6 @@ }, "description": "Kui \"k\u00f5ik olemid\" on lubatud,on r\u00fchma olek sees ainult siis kui k\u00f5ik liikmed on sisse l\u00fclitatud. Kui \"k\u00f5ik olemid\" on keelatud, on r\u00fchma olek sees kui m\u00f5ni liige on sisse l\u00fclitatud." }, - "light_options": { - "data": { - "all": "K\u00f5ik olemid", - "entities": "Liikmed" - } - }, "lock": { "data": { "entities": "Liikmed", @@ -180,11 +116,6 @@ "hide_members": "Peida grupi liikmed" } }, - "media_player_options": { - "data": { - "entities": "Liikmed" - } - }, "switch": { "data": { "all": "K\u00f5ik olemid", diff --git a/homeassistant/components/group/translations/fr.json b/homeassistant/components/group/translations/fr.json index e8b74e2f374..811776f8f9d 100644 --- a/homeassistant/components/group/translations/fr.json +++ b/homeassistant/components/group/translations/fr.json @@ -15,57 +15,26 @@ "data": { "entities": "Membres", "hide_members": "Cacher les membres", - "name": "Nom", - "title": "Ajouter un groupe" + "name": "Nom" }, - "description": "S\u00e9lectionnez les options du groupe", "title": "Ajouter un groupe" }, - "cover_options": { - "data": { - "entities": "Membres du groupe" - }, - "description": "S\u00e9lectionnez les options du groupe" - }, "fan": { "data": { "entities": "Membres", "hide_members": "Cacher les membres", - "name": "Nom", - "title": "Ajouter un groupe" + "name": "Nom" }, - "description": "S\u00e9lectionnez les options du groupe", "title": "Ajouter un groupe" }, - "fan_options": { - "data": { - "entities": "Membres du groupe" - }, - "description": "S\u00e9lectionnez les options du groupe" - }, - "init": { - "data": { - "group_type": "Type de groupe" - }, - "description": "S\u00e9lectionnez le type de groupe" - }, "light": { "data": { - "all": "Toutes les entit\u00e9s", "entities": "Membres", "hide_members": "Cacher les membres", - "name": "Nom", - "title": "Ajouter un groupe" + "name": "Nom" }, - "description": "Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est activ\u00e9, l'\u00e9tat du groupe n'est activ\u00e9 que si tous les membres sont activ\u00e9s. Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est d\u00e9sactiv\u00e9, l'\u00e9tat du groupe est activ\u00e9 si au moins un membre est activ\u00e9.", "title": "Ajouter un groupe" }, - "light_options": { - "data": { - "entities": "Membres du groupe" - }, - "description": "S\u00e9lectionnez les options du groupe" - }, "lock": { "data": { "entities": "Membres", @@ -78,18 +47,10 @@ "data": { "entities": "Membres", "hide_members": "Cacher les membres", - "name": "Nom", - "title": "Ajouter un groupe" + "name": "Nom" }, - "description": "S\u00e9lectionnez les options du groupe", "title": "Ajouter un groupe" }, - "media_player_options": { - "data": { - "entities": "Membres du groupe" - }, - "description": "S\u00e9lectionnez les options du groupe" - }, "switch": { "data": { "entities": "Membres", @@ -99,9 +60,6 @@ "title": "Ajouter un groupe" }, "user": { - "data": { - "group_type": "Type de groupe" - }, "description": "Les groupes vous permettent de cr\u00e9er une nouvelle entit\u00e9 repr\u00e9sentant plusieurs entit\u00e9s d'un m\u00eame type.", "menu_options": { "binary_sensor": "Groupe de capteurs binaires", @@ -126,34 +84,18 @@ }, "description": "Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est activ\u00e9, l'\u00e9tat du groupe n'est activ\u00e9 que si tous les membres sont activ\u00e9s. Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est d\u00e9sactiv\u00e9, l'\u00e9tat du groupe est activ\u00e9 si au moins un membre est activ\u00e9." }, - "binary_sensor_options": { - "data": { - "all": "Toutes les entit\u00e9s", - "entities": "Membres" - } - }, "cover": { "data": { "entities": "Membres", "hide_members": "Cacher les membres" } }, - "cover_options": { - "data": { - "entities": "Membres" - } - }, "fan": { "data": { "entities": "Membres", "hide_members": "Cacher les membres" } }, - "fan_options": { - "data": { - "entities": "Membres" - } - }, "light": { "data": { "all": "Toutes les entit\u00e9s", @@ -162,12 +104,6 @@ }, "description": "Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est activ\u00e9, l'\u00e9tat du groupe n'est activ\u00e9 que si tous les membres sont activ\u00e9s. Si \u00ab\u00a0toutes les entit\u00e9s\u00a0\u00bb est d\u00e9sactiv\u00e9, l'\u00e9tat du groupe est activ\u00e9 si au moins un membre est activ\u00e9." }, - "light_options": { - "data": { - "all": "Toutes les entit\u00e9s", - "entities": "Membres" - } - }, "lock": { "data": { "entities": "Membres", @@ -180,11 +116,6 @@ "hide_members": "Cacher les membres" } }, - "media_player_options": { - "data": { - "entities": "Membres" - } - }, "switch": { "data": { "all": "Toutes les entit\u00e9s", diff --git a/homeassistant/components/group/translations/he.json b/homeassistant/components/group/translations/he.json index ab7a90cb699..354a435f491 100644 --- a/homeassistant/components/group/translations/he.json +++ b/homeassistant/components/group/translations/he.json @@ -15,57 +15,26 @@ "data": { "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd", - "name": "\u05e9\u05dd", - "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + "name": "\u05e9\u05dd" }, - "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4", "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" }, - "cover_options": { - "data": { - "entities": "\u05d7\u05d1\u05e8\u05d9 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4" - }, - "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" - }, "fan": { "data": { "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd", - "name": "\u05e9\u05dd", - "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + "name": "\u05e9\u05dd" }, - "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4", "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" }, - "fan_options": { - "data": { - "entities": "\u05d7\u05d1\u05e8\u05d9 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4" - }, - "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" - }, - "init": { - "data": { - "group_type": "\u05e1\u05d5\u05d2 \u05e7\u05d1\u05d5\u05e6\u05d4" - }, - "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05e1\u05d5\u05d2 \u05e7\u05d1\u05d5\u05e6\u05d4" - }, "light": { "data": { - "all": "\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea", "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd", - "name": "\u05e9\u05dd", - "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + "name": "\u05e9\u05dd" }, - "description": "\u05d0\u05dd \u05d4\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d6\u05de\u05d9\u05e0\u05d4, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05e8\u05e7 \u05d0\u05dd \u05db\u05dc \u05d4\u05d7\u05d1\u05e8\u05d9\u05dd \u05e4\u05d5\u05e2\u05dc\u05d9\u05dd. \u05d0\u05dd \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d0\u05d9\u05e0\u05d5 \u05d6\u05de\u05d9\u05df, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05d0\u05dd \u05d7\u05d1\u05e8 \u05db\u05dc\u05e9\u05d4\u05d5 \u05e4\u05d5\u05e2\u05dc.", "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" }, - "light_options": { - "data": { - "entities": "\u05d7\u05d1\u05e8\u05d9 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4" - }, - "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" - }, "lock": { "data": { "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", @@ -78,18 +47,10 @@ "data": { "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd", - "name": "\u05e9\u05dd", - "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + "name": "\u05e9\u05dd" }, - "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4", "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" }, - "media_player_options": { - "data": { - "entities": "\u05d7\u05d1\u05e8\u05d9 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4" - }, - "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" - }, "switch": { "data": { "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", @@ -99,9 +60,6 @@ "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" }, "user": { - "data": { - "group_type": "\u05e1\u05d5\u05d2 \u05e7\u05d1\u05d5\u05e6\u05d4" - }, "description": "\u05e7\u05d1\u05d5\u05e6\u05d5\u05ea \u05de\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05da \u05dc\u05d9\u05e6\u05d5\u05e8 \u05d9\u05e9\u05d5\u05ea \u05d7\u05d3\u05e9\u05d4 \u05d4\u05de\u05d9\u05d9\u05e6\u05d2\u05ea \u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05de\u05e8\u05d5\u05d1\u05d5\u05ea \u05de\u05d0\u05d5\u05ea\u05d5 \u05e1\u05d5\u05d2.", "menu_options": { "binary_sensor": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05d1\u05d9\u05e0\u05d0\u05e8\u05d9\u05d9\u05dd", @@ -124,34 +82,18 @@ }, "description": "\u05d0\u05dd \u05d4\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d6\u05de\u05d9\u05e0\u05d4, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05e8\u05e7 \u05d0\u05dd \u05db\u05dc \u05d4\u05d7\u05d1\u05e8\u05d9\u05dd \u05e4\u05d5\u05e2\u05dc\u05d9\u05dd. \u05d0\u05dd \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d0\u05d9\u05e0\u05d5 \u05d6\u05de\u05d9\u05df, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05d0\u05dd \u05d7\u05d1\u05e8 \u05db\u05dc\u05e9\u05d4\u05d5 \u05e4\u05d5\u05e2\u05dc." }, - "binary_sensor_options": { - "data": { - "all": "\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea", - "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" - } - }, "cover": { "data": { "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd" } }, - "cover_options": { - "data": { - "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" - } - }, "fan": { "data": { "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd" } }, - "fan_options": { - "data": { - "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" - } - }, "light": { "data": { "all": "\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea", @@ -160,12 +102,6 @@ }, "description": "\u05d0\u05dd \u05d4\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d6\u05de\u05d9\u05e0\u05d4, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05e8\u05e7 \u05d0\u05dd \u05db\u05dc \u05d4\u05d7\u05d1\u05e8\u05d9\u05dd \u05e4\u05d5\u05e2\u05dc\u05d9\u05dd. \u05d0\u05dd \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d0\u05d9\u05e0\u05d5 \u05d6\u05de\u05d9\u05df, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05d0\u05dd \u05d7\u05d1\u05e8 \u05db\u05dc\u05e9\u05d4\u05d5 \u05e4\u05d5\u05e2\u05dc." }, - "light_options": { - "data": { - "all": "\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea", - "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" - } - }, "lock": { "data": { "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd", @@ -178,11 +114,6 @@ "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd" } }, - "media_player_options": { - "data": { - "entities": "\u05d7\u05d1\u05e8\u05d9\u05dd" - } - }, "switch": { "data": { "all": "\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea", diff --git a/homeassistant/components/group/translations/hu.json b/homeassistant/components/group/translations/hu.json index ba368532cff..ae455bafebe 100644 --- a/homeassistant/components/group/translations/hu.json +++ b/homeassistant/components/group/translations/hu.json @@ -15,57 +15,26 @@ "data": { "entities": "A csoport tagjai", "hide_members": "Tagok elrejt\u00e9se", - "name": "Elnevez\u00e9s", - "title": "Csoport hozz\u00e1ad\u00e1sa" + "name": "Elnevez\u00e9s" }, - "description": "Csoport be\u00e1ll\u00edt\u00e1sai", "title": "Csoport hozz\u00e1ad\u00e1sa" }, - "cover_options": { - "data": { - "entities": "A csoport tagjai" - }, - "description": "Csoport be\u00e1ll\u00edt\u00e1sai" - }, "fan": { "data": { "entities": "A csoport tagjai", "hide_members": "Tagok elrejt\u00e9se", - "name": "Elnevez\u00e9s", - "title": "Csoport hozz\u00e1ad\u00e1sa" + "name": "Elnevez\u00e9s" }, - "description": "Csoport be\u00e1ll\u00edt\u00e1sai", "title": "Csoport hozz\u00e1ad\u00e1sa" }, - "fan_options": { - "data": { - "entities": "A csoport tagjai" - }, - "description": "Csoport be\u00e1ll\u00edt\u00e1sai" - }, - "init": { - "data": { - "group_type": "A csoport t\u00edpusa" - }, - "description": "A csoport t\u00edpusa" - }, "light": { "data": { - "all": "Minden entit\u00e1s", "entities": "A csoport tagjai", "hide_members": "Tagok elrejt\u00e9se", - "name": "Elnevez\u00e9s", - "title": "Csoport hozz\u00e1ad\u00e1sa" + "name": "Elnevez\u00e9s" }, - "description": "Ha \u201eMinden entit\u00e1s\u201d enged\u00e9lyezve van, a csoport \u00e1llapota csak akkor van bekapcsolva, ha minden tag \u00e1llapota bekapcsolt. Ha \u201eMinden entit\u00e1s\u201d le van tiltva, a csoport \u00e1llapota akkor van bekapcsolva, ha b\u00e1rmelyik tag bekapcsolt \u00e1llapotban van.", "title": "Csoport hozz\u00e1ad\u00e1sa" }, - "light_options": { - "data": { - "entities": "A csoport tagjai" - }, - "description": "Csoport be\u00e1ll\u00edt\u00e1sai" - }, "lock": { "data": { "entities": "A csoport tagjai", @@ -78,18 +47,10 @@ "data": { "entities": "A csoport tagjai", "hide_members": "Tagok elrejt\u00e9se", - "name": "Elnevez\u00e9s", - "title": "Csoport hozz\u00e1ad\u00e1sa" + "name": "Elnevez\u00e9s" }, - "description": "Csoport be\u00e1ll\u00edt\u00e1sai", "title": "Csoport hozz\u00e1ad\u00e1sa" }, - "media_player_options": { - "data": { - "entities": "A csoport tagjai" - }, - "description": "Csoport be\u00e1ll\u00edt\u00e1sai" - }, "switch": { "data": { "entities": "A csoport tagjai", @@ -99,9 +60,6 @@ "title": "Csoport hozz\u00e1ad\u00e1sa" }, "user": { - "data": { - "group_type": "Csoport t\u00edpusa" - }, "description": "A csoportok lehet\u0151v\u00e9 teszik egy \u00faj entit\u00e1s l\u00e9trehoz\u00e1s\u00e1t, amely t\u00f6bb azonos t\u00edpus\u00fa entit\u00e1st k\u00e9pvisel.", "menu_options": { "binary_sensor": "Bin\u00e1ris \u00e9rz\u00e9kel\u0151 csoport", @@ -126,34 +84,18 @@ }, "description": "Ha \u201eMinden entit\u00e1s\u201d enged\u00e9lyezve van, a csoport \u00e1llapota csak akkor van bekapcsolva, ha minden tag \u00e1llapota bekapcsolt. Ha \u201eMinden entit\u00e1s\u201d le van tiltva, a csoport \u00e1llapota akkor van bekapcsolva, ha b\u00e1rmelyik tag bekapcsolt \u00e1llapotban van." }, - "binary_sensor_options": { - "data": { - "all": "Minden entit\u00e1s", - "entities": "A csoport tagjai" - } - }, "cover": { "data": { "entities": "A csoport tagjai", "hide_members": "Tagok elrejt\u00e9se" } }, - "cover_options": { - "data": { - "entities": "A csoport tagjai" - } - }, "fan": { "data": { "entities": "A csoport tagjai", "hide_members": "Tagok elrejt\u00e9se" } }, - "fan_options": { - "data": { - "entities": "A csoport tagjai" - } - }, "light": { "data": { "all": "Minden entit\u00e1s", @@ -162,12 +104,6 @@ }, "description": "Ha \u201eMinden entit\u00e1s\u201d enged\u00e9lyezve van, a csoport \u00e1llapota csak akkor van bekapcsolva, ha minden tag \u00e1llapota bekapcsolt. Ha \u201eMinden entit\u00e1s\u201d le van tiltva, a csoport \u00e1llapota akkor van bekapcsolva, ha b\u00e1rmelyik tag bekapcsolt \u00e1llapotban van." }, - "light_options": { - "data": { - "all": "Minden entit\u00e1s", - "entities": "A csoport tagjai" - } - }, "lock": { "data": { "entities": "A csoport tagjai", @@ -180,11 +116,6 @@ "hide_members": "Tagok elrejt\u00e9se" } }, - "media_player_options": { - "data": { - "entities": "A csoport tagjai" - } - }, "switch": { "data": { "all": "Minden entit\u00e1s", diff --git a/homeassistant/components/group/translations/id.json b/homeassistant/components/group/translations/id.json index c0425c5ede6..9aa29f472c5 100644 --- a/homeassistant/components/group/translations/id.json +++ b/homeassistant/components/group/translations/id.json @@ -15,57 +15,26 @@ "data": { "entities": "Anggota", "hide_members": "Sembunyikan anggota", - "name": "Nama", - "title": "Tambahkan Grup" + "name": "Nama" }, - "description": "Pilih opsi grup", "title": "Tambahkan Grup" }, - "cover_options": { - "data": { - "entities": "Anggota grup" - }, - "description": "Pilih opsi grup" - }, "fan": { "data": { "entities": "Anggota", "hide_members": "Sembunyikan anggota", - "name": "Nama", - "title": "Tambahkan Grup" + "name": "Nama" }, - "description": "Pilih opsi grup", "title": "Tambahkan Grup" }, - "fan_options": { - "data": { - "entities": "Anggota grup" - }, - "description": "Pilih opsi grup" - }, - "init": { - "data": { - "group_type": "Jenis grup" - }, - "description": "Pilih jenis grup" - }, "light": { "data": { - "all": "Semua entitas", "entities": "Anggota", "hide_members": "Sembunyikan anggota", - "name": "Nama", - "title": "Tambahkan Grup" + "name": "Nama" }, - "description": "Jika \"semua entitas\" diaktifkan, status grup akan menyala jika semua anggota nyala. Jika \"semua entitas\" dinonaktifkan, status grup akan menyala jika ada salah satu atau lebih anggota yang menyala.", "title": "Tambahkan Grup" }, - "light_options": { - "data": { - "entities": "Anggota grup" - }, - "description": "Pilih opsi grup" - }, "lock": { "data": { "entities": "Anggota", @@ -78,18 +47,10 @@ "data": { "entities": "Anggota", "hide_members": "Sembunyikan anggota", - "name": "Nama", - "title": "Tambahkan Grup" + "name": "Nama" }, - "description": "Pilih opsi grup", "title": "Tambahkan Grup" }, - "media_player_options": { - "data": { - "entities": "Anggota grup" - }, - "description": "Pilih opsi grup" - }, "switch": { "data": { "entities": "Anggota", @@ -99,9 +60,6 @@ "title": "Tambahkan Grup" }, "user": { - "data": { - "group_type": "Jenis grup" - }, "description": "Grup memungkinkan Anda membuat entitas baru yang mewakili beberapa entitas dari jenis yang sama.", "menu_options": { "binary_sensor": "Grup sensor biner", @@ -126,34 +84,18 @@ }, "description": "Jika \"semua entitas\" diaktifkan, status grup akan menyala jika semua anggota nyala. Jika \"semua entitas\" dinonaktifkan, status grup akan menyala jika ada salah satu atau lebih anggota yang menyala." }, - "binary_sensor_options": { - "data": { - "all": "Semua entitas", - "entities": "Anggota" - } - }, "cover": { "data": { "entities": "Anggota", "hide_members": "Sembunyikan anggota" } }, - "cover_options": { - "data": { - "entities": "Anggota" - } - }, "fan": { "data": { "entities": "Anggota", "hide_members": "Sembunyikan anggota" } }, - "fan_options": { - "data": { - "entities": "Anggota" - } - }, "light": { "data": { "all": "Semua entitas", @@ -162,12 +104,6 @@ }, "description": "Jika \"semua entitas\" diaktifkan, status grup akan menyala jika semua anggota nyala. Jika \"semua entitas\" dinonaktifkan, status grup akan menyala jika ada salah satu atau lebih anggota yang menyala." }, - "light_options": { - "data": { - "all": "Semua entitas", - "entities": "Anggota" - } - }, "lock": { "data": { "entities": "Anggota", @@ -180,11 +116,6 @@ "hide_members": "Sembunyikan anggota" } }, - "media_player_options": { - "data": { - "entities": "Anggota" - } - }, "switch": { "data": { "all": "Semua entitas", diff --git a/homeassistant/components/group/translations/it.json b/homeassistant/components/group/translations/it.json index 4cbac9145d8..dac9fd264f2 100644 --- a/homeassistant/components/group/translations/it.json +++ b/homeassistant/components/group/translations/it.json @@ -15,57 +15,26 @@ "data": { "entities": "Membri", "hide_members": "Nascondi membri", - "name": "Nome", - "title": "Aggiungi gruppo" + "name": "Nome" }, - "description": "Seleziona le opzioni di gruppo", "title": "Aggiungi gruppo" }, - "cover_options": { - "data": { - "entities": "Membri del gruppo" - }, - "description": "Seleziona le opzioni di gruppo" - }, "fan": { "data": { "entities": "Membri", "hide_members": "Nascondi membri", - "name": "Nome", - "title": "Aggiungi gruppo" + "name": "Nome" }, - "description": "Seleziona le opzioni di gruppo", "title": "Aggiungi gruppo" }, - "fan_options": { - "data": { - "entities": "Membri del gruppo" - }, - "description": "Seleziona le opzioni di gruppo" - }, - "init": { - "data": { - "group_type": "Tipo di gruppo" - }, - "description": "Seleziona il tipo di gruppo" - }, "light": { "data": { - "all": "Tutte le entit\u00e0", "entities": "Membri", "hide_members": "Nascondi membri", - "name": "Nome", - "title": "Aggiungi gruppo" + "name": "Nome" }, - "description": "Se \"tutte le entit\u00e0\" \u00e8 abilitata, lo stato del gruppo \u00e8 attivo solo se tutti i membri sono attivi. Se \"tutte le entit\u00e0\" \u00e8 disabilitata, lo stato del gruppo \u00e8 attivo se un membro \u00e8 attivo.", "title": "Aggiungi gruppo" }, - "light_options": { - "data": { - "entities": "Membri del gruppo" - }, - "description": "Seleziona le opzioni di gruppo" - }, "lock": { "data": { "entities": "Membri", @@ -78,18 +47,10 @@ "data": { "entities": "Membri", "hide_members": "Nascondi membri", - "name": "Nome", - "title": "Aggiungi gruppo" + "name": "Nome" }, - "description": "Seleziona le opzioni di gruppo", "title": "Aggiungi gruppo" }, - "media_player_options": { - "data": { - "entities": "Membri del gruppo" - }, - "description": "Seleziona le opzioni di gruppo" - }, "switch": { "data": { "entities": "Membri", @@ -99,9 +60,6 @@ "title": "Aggiungi gruppo" }, "user": { - "data": { - "group_type": "Tipo di gruppo" - }, "description": "I gruppi consentono di creare una nuova entit\u00e0 che rappresenta pi\u00f9 entit\u00e0 dello stesso tipo.", "menu_options": { "binary_sensor": "Gruppo di sensori binari", @@ -126,34 +84,18 @@ }, "description": "Se \"tutte le entit\u00e0\" \u00e8 abilitata, lo stato del gruppo \u00e8 attivo solo se tutti i membri sono attivi. Se \"tutte le entit\u00e0\" \u00e8 disabilitata, lo stato del gruppo \u00e8 attivo se un membro \u00e8 attivo." }, - "binary_sensor_options": { - "data": { - "all": "Tutte le entit\u00e0", - "entities": "Membri" - } - }, "cover": { "data": { "entities": "Membri", "hide_members": "Nascondi membri" } }, - "cover_options": { - "data": { - "entities": "Membri" - } - }, "fan": { "data": { "entities": "Membri", "hide_members": "Nascondi membri" } }, - "fan_options": { - "data": { - "entities": "Membri" - } - }, "light": { "data": { "all": "Tutte le entit\u00e0", @@ -162,12 +104,6 @@ }, "description": "Se \"tutte le entit\u00e0\" \u00e8 abilitata, lo stato del gruppo \u00e8 attivo solo se tutti i membri sono attivi. Se \"tutte le entit\u00e0\" \u00e8 disabilitata, lo stato del gruppo \u00e8 attivo se un membro \u00e8 attivo." }, - "light_options": { - "data": { - "all": "Tutte le entit\u00e0", - "entities": "Membri" - } - }, "lock": { "data": { "entities": "Membri", @@ -180,11 +116,6 @@ "hide_members": "Nascondi membri" } }, - "media_player_options": { - "data": { - "entities": "Membri" - } - }, "switch": { "data": { "all": "Tutte le entit\u00e0", diff --git a/homeassistant/components/group/translations/ja.json b/homeassistant/components/group/translations/ja.json index 55e414927a4..46fbbc7b22f 100644 --- a/homeassistant/components/group/translations/ja.json +++ b/homeassistant/components/group/translations/ja.json @@ -15,57 +15,26 @@ "data": { "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b", - "name": "\u30b0\u30eb\u30fc\u30d7\u540d", - "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" + "name": "\u30b0\u30eb\u30fc\u30d7\u540d" }, - "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e", "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, - "cover_options": { - "data": { - "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc" - }, - "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" - }, "fan": { "data": { "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b", - "name": "\u30b0\u30eb\u30fc\u30d7\u540d", - "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" + "name": "\u30b0\u30eb\u30fc\u30d7\u540d" }, - "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e", "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, - "fan_options": { - "data": { - "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc" - }, - "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" - }, - "init": { - "data": { - "group_type": "\u30b0\u30eb\u30fc\u30d7\u30bf\u30a4\u30d7" - }, - "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u7a2e\u985e\u3092\u9078\u629e" - }, "light": { "data": { - "all": "\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b", - "name": "\u30b0\u30eb\u30fc\u30d7\u540d", - "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" + "name": "\u30b0\u30eb\u30fc\u30d7\u540d" }, - "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e", "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, - "light_options": { - "data": { - "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc" - }, - "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" - }, "lock": { "data": { "entities": "\u30e1\u30f3\u30d0\u30fc", @@ -78,18 +47,10 @@ "data": { "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc", "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b", - "name": "\u30b0\u30eb\u30fc\u30d7\u540d", - "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" + "name": "\u30b0\u30eb\u30fc\u30d7\u540d" }, - "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e", "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, - "media_player_options": { - "data": { - "entities": "\u30b0\u30eb\u30fc\u30d7\u306e\u30e1\u30f3\u30d0\u30fc" - }, - "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e" - }, "switch": { "data": { "entities": "\u30e1\u30f3\u30d0\u30fc", @@ -99,9 +60,6 @@ "title": "\u65b0\u3057\u3044\u30b0\u30eb\u30fc\u30d7" }, "user": { - "data": { - "group_type": "\u30b0\u30eb\u30fc\u30d7\u30bf\u30a4\u30d7" - }, "description": "\u30b0\u30eb\u30fc\u30d7\u306e\u7a2e\u985e\u3092\u9078\u629e", "menu_options": { "binary_sensor": "\u30d0\u30a4\u30ca\u30ea\u30fc\u30bb\u30f3\u30b5\u30fc\u30b0\u30eb\u30fc\u30d7", @@ -126,34 +84,18 @@ }, "description": "\"\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\" \u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408\u3001\u30b0\u30eb\u30fc\u30d7\u306e\u72b6\u614b\u306f\u3001\u3059\u3079\u3066\u306e\u30e1\u30f3\u30d0\u30fc\u304c\u30aa\u30f3\u306e\u5834\u5408\u306b\u306e\u307f\u30aa\u30f3\u306b\u306a\u308a\u307e\u3059\u3002 \"\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\" \u304c\u7121\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408\u3001\u3044\u305a\u308c\u304b\u306e\u30e1\u30f3\u30d0\u30fc\u304c\u30aa\u30f3\u3067\u3042\u308c\u3070\u3001\u30b0\u30eb\u30fc\u30d7\u306e\u72b6\u614b\u306f\u30aa\u30f3\u306b\u306a\u308a\u307e\u3059\u3002" }, - "binary_sensor_options": { - "data": { - "all": "\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", - "entities": "\u30e1\u30f3\u30d0\u30fc" - } - }, "cover": { "data": { "entities": "\u30e1\u30f3\u30d0\u30fc", "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b" } }, - "cover_options": { - "data": { - "entities": "\u30e1\u30f3\u30d0\u30fc" - } - }, "fan": { "data": { "entities": "\u30e1\u30f3\u30d0\u30fc", "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b" } }, - "fan_options": { - "data": { - "entities": "\u30e1\u30f3\u30d0\u30fc" - } - }, "light": { "data": { "all": "\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", @@ -162,12 +104,6 @@ }, "description": "\"\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\" \u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408\u3001\u30b0\u30eb\u30fc\u30d7\u306e\u72b6\u614b\u306f\u3001\u3059\u3079\u3066\u306e\u30e1\u30f3\u30d0\u30fc\u304c\u30aa\u30f3\u306e\u5834\u5408\u306b\u306e\u307f\u30aa\u30f3\u306b\u306a\u308a\u307e\u3059\u3002 \"\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\" \u304c\u7121\u52b9\u306b\u306a\u3063\u3066\u3044\u308b\u5834\u5408\u3001\u3044\u305a\u308c\u304b\u306e\u30e1\u30f3\u30d0\u30fc\u304c\u30aa\u30f3\u3067\u3042\u308c\u3070\u3001\u30b0\u30eb\u30fc\u30d7\u306e\u72b6\u614b\u306f\u30aa\u30f3\u306b\u306a\u308a\u307e\u3059\u3002" }, - "light_options": { - "data": { - "all": "\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", - "entities": "\u30e1\u30f3\u30d0\u30fc" - } - }, "lock": { "data": { "entities": "\u30e1\u30f3\u30d0\u30fc", @@ -180,11 +116,6 @@ "hide_members": "\u30e1\u30f3\u30d0\u30fc\u3092\u975e\u8868\u793a\u306b\u3059\u308b" } }, - "media_player_options": { - "data": { - "entities": "\u30e1\u30f3\u30d0\u30fc" - } - }, "switch": { "data": { "all": "\u3059\u3079\u3066\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", diff --git a/homeassistant/components/group/translations/ko.json b/homeassistant/components/group/translations/ko.json index c2adb88c7ca..3b41a27075b 100644 --- a/homeassistant/components/group/translations/ko.json +++ b/homeassistant/components/group/translations/ko.json @@ -1,4 +1,11 @@ { + "config": { + "step": { + "user": { + "description": "\uadf8\ub8f9\uc744 \uc0ac\uc6a9\ud558\uba74 \ub3d9\uc77c\ud55c \uc720\ud615\uc758 \uc5ec\ub7ec \uad6c\uc131\uc694\uc18c\ub97c \ub098\ud0c0\ub0b4\ub294 \uc0c8 \uad6c\uc131\uc694\uc18c\ub97c \ub9cc\ub4e4 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + } + } + }, "state": { "_": { "closed": "\ub2eb\ud798", diff --git a/homeassistant/components/group/translations/nl.json b/homeassistant/components/group/translations/nl.json index 1343279c365..e78e3462c60 100644 --- a/homeassistant/components/group/translations/nl.json +++ b/homeassistant/components/group/translations/nl.json @@ -15,57 +15,26 @@ "data": { "entities": "Leden", "hide_members": "Verberg leden", - "name": "Naam", - "title": "Groep toevoegen" + "name": "Naam" }, - "description": "Selecteer groepsopties", "title": "Groep toevoegen" }, - "cover_options": { - "data": { - "entities": "Groepsleden" - }, - "description": "Selecteer groepsopties" - }, "fan": { "data": { "entities": "Leden", "hide_members": "Verberg leden", - "name": "Naam", - "title": "Groep toevoegen" + "name": "Naam" }, - "description": "Selecteer groepsopties", "title": "Groep toevoegen" }, - "fan_options": { - "data": { - "entities": "Groepsleden" - }, - "description": "Selecteer groepsopties" - }, - "init": { - "data": { - "group_type": "Groepstype" - }, - "description": "Selecteer groepstype" - }, "light": { "data": { - "all": "Alle entiteiten", "entities": "Leden", "hide_members": "Verberg leden", - "name": "Naam", - "title": "Groep toevoegen" + "name": "Naam" }, - "description": "Als \"alle entiteiten\" is ingeschakeld, is de groep alleen ingeschakeld als alle leden zijn ingeschakeld. Als \"all entities\" is uitgeschakeld, is de groep ingeschakeld als een lid is ingeschakeld.", "title": "Groep toevoegen" }, - "light_options": { - "data": { - "entities": "Groepsleden" - }, - "description": "Selecteer groepsopties" - }, "lock": { "data": { "entities": "Leden", @@ -78,18 +47,10 @@ "data": { "entities": "Leden", "hide_members": "Verberg leden", - "name": "Naam", - "title": "Groep toevoegen" + "name": "Naam" }, - "description": "Selecteer groepsopties", "title": "Groep toevoegen" }, - "media_player_options": { - "data": { - "entities": "Groepsleden" - }, - "description": "Selecteer groepsopties" - }, "switch": { "data": { "entities": "Leden", @@ -99,9 +60,6 @@ "title": "Nieuwe groep" }, "user": { - "data": { - "group_type": "Groepstype" - }, "description": "Met groepen kunt u een nieuwe entiteit cre\u00ebren die meerdere entiteiten van hetzelfde type vertegenwoordigt.", "menu_options": { "binary_sensor": "Binaire-sensorgroep", @@ -126,34 +84,18 @@ }, "description": "Als \"alle entiteiten\" is ingeschakeld, is de groep alleen ingeschakeld als alle leden zijn ingeschakeld. Als \"all entities\" is uitgeschakeld, is de groep ingeschakeld als een lid is ingeschakeld." }, - "binary_sensor_options": { - "data": { - "all": "Alle entiteiten", - "entities": "Leden" - } - }, "cover": { "data": { "entities": "Leden", "hide_members": "Verberg leden" } }, - "cover_options": { - "data": { - "entities": "Leden" - } - }, "fan": { "data": { "entities": "Leden", "hide_members": "Verberg leden" } }, - "fan_options": { - "data": { - "entities": "Leden" - } - }, "light": { "data": { "all": "Alle entiteiten", @@ -162,12 +104,6 @@ }, "description": "Als \"alle entiteiten\" is ingeschakeld, is de groep alleen ingeschakeld als alle leden zijn ingeschakeld. Als \"all entities\" is uitgeschakeld, is de groep ingeschakeld als een lid is ingeschakeld." }, - "light_options": { - "data": { - "all": "Alle entiteiten", - "entities": "Leden" - } - }, "lock": { "data": { "entities": "Leden", @@ -180,11 +116,6 @@ "hide_members": "Verberg leden" } }, - "media_player_options": { - "data": { - "entities": "Leden" - } - }, "switch": { "data": { "all": "Alle entiteiten", diff --git a/homeassistant/components/group/translations/no.json b/homeassistant/components/group/translations/no.json index 016a9c8e934..a79e374f2a5 100644 --- a/homeassistant/components/group/translations/no.json +++ b/homeassistant/components/group/translations/no.json @@ -15,57 +15,26 @@ "data": { "entities": "Medlemmer", "hide_members": "Skjul medlemmer", - "name": "Navn", - "title": "Legg til gruppe" + "name": "Navn" }, - "description": "Velg gruppealternativer", "title": "Legg til gruppe" }, - "cover_options": { - "data": { - "entities": "Gruppemedlemmer" - }, - "description": "Velg gruppealternativer" - }, "fan": { "data": { "entities": "Medlemmer", "hide_members": "Skjul medlemmer", - "name": "Navn", - "title": "Legg til gruppe" + "name": "Navn" }, - "description": "Velg gruppealternativer", "title": "Legg til gruppe" }, - "fan_options": { - "data": { - "entities": "Gruppemedlemmer" - }, - "description": "Velg gruppealternativer" - }, - "init": { - "data": { - "group_type": "Gruppetype" - }, - "description": "Velg gruppetype" - }, "light": { "data": { - "all": "Alle enheter", "entities": "Medlemmer", "hide_members": "Skjul medlemmer", - "name": "Navn", - "title": "Legg til gruppe" + "name": "Navn" }, - "description": "Hvis \"alle enheter\" er aktivert, er gruppens tilstand p\u00e5 bare hvis alle medlemmene er p\u00e5. Hvis \"alle enheter\" er deaktivert, er gruppens tilstand p\u00e5 hvis et medlem er p\u00e5.", "title": "Legg til gruppe" }, - "light_options": { - "data": { - "entities": "Gruppemedlemmer" - }, - "description": "Velg gruppealternativer" - }, "lock": { "data": { "entities": "Medlemmer", @@ -78,18 +47,10 @@ "data": { "entities": "Medlemmer", "hide_members": "Skjul medlemmer", - "name": "Navn", - "title": "Legg til gruppe" + "name": "Navn" }, - "description": "Velg gruppealternativer", "title": "Legg til gruppe" }, - "media_player_options": { - "data": { - "entities": "Gruppemedlemmer" - }, - "description": "Velg gruppealternativer" - }, "switch": { "data": { "entities": "Medlemmer", @@ -99,9 +60,6 @@ "title": "Legg til gruppe" }, "user": { - "data": { - "group_type": "Gruppetype" - }, "description": "Grupper lar deg opprette en ny enhet som representerer flere enheter av samme type.", "menu_options": { "binary_sensor": "Bin\u00e6r sensorgruppe", @@ -126,34 +84,18 @@ }, "description": "Hvis \"alle enheter\" er aktivert, er gruppens tilstand p\u00e5 bare hvis alle medlemmene er p\u00e5. Hvis \"alle enheter\" er deaktivert, er gruppens tilstand p\u00e5 hvis et medlem er p\u00e5." }, - "binary_sensor_options": { - "data": { - "all": "Alle enheter", - "entities": "Medlemmer" - } - }, "cover": { "data": { "entities": "Medlemmer", "hide_members": "Skjul medlemmer" } }, - "cover_options": { - "data": { - "entities": "Medlemmer" - } - }, "fan": { "data": { "entities": "Medlemmer", "hide_members": "Skjul medlemmer" } }, - "fan_options": { - "data": { - "entities": "Medlemmer" - } - }, "light": { "data": { "all": "Alle enheter", @@ -162,12 +104,6 @@ }, "description": "Hvis \"alle enheter\" er aktivert, er gruppens tilstand p\u00e5 bare hvis alle medlemmene er p\u00e5. Hvis \"alle enheter\" er deaktivert, er gruppens tilstand p\u00e5 hvis et medlem er p\u00e5." }, - "light_options": { - "data": { - "all": "Alle enheter", - "entities": "Medlemmer" - } - }, "lock": { "data": { "entities": "Medlemmer", @@ -180,11 +116,6 @@ "hide_members": "Skjul medlemmer" } }, - "media_player_options": { - "data": { - "entities": "Medlemmer" - } - }, "switch": { "data": { "all": "Alle enheter", diff --git a/homeassistant/components/group/translations/pl.json b/homeassistant/components/group/translations/pl.json index 84cbd9f7b4d..3b6b6ee40ba 100644 --- a/homeassistant/components/group/translations/pl.json +++ b/homeassistant/components/group/translations/pl.json @@ -15,57 +15,26 @@ "data": { "entities": "Encje", "hide_members": "Ukryj encje", - "name": "Nazwa", - "title": "Dodaj grup\u0119" + "name": "Nazwa" }, - "description": "Wybierz opcje grupy", "title": "Dodaj grup\u0119" }, - "cover_options": { - "data": { - "entities": "Encje w grupie" - }, - "description": "Wybierz opcje grupy" - }, "fan": { "data": { "entities": "Encje", "hide_members": "Ukryj encje", - "name": "Nazwa", - "title": "Dodaj grup\u0119" + "name": "Nazwa" }, - "description": "Wybierz opcje grupy", "title": "Dodaj grup\u0119" }, - "fan_options": { - "data": { - "entities": "Encje w grupie" - }, - "description": "Wybierz opcje grupy" - }, - "init": { - "data": { - "group_type": "Rodzaj grupy" - }, - "description": "Wybierz rodzaj grupy" - }, "light": { "data": { - "all": "Wszystkie encje", "entities": "Encje", "hide_members": "Ukryj encje", - "name": "Nazwa", - "title": "Dodaj grup\u0119" + "name": "Nazwa" }, - "description": "Je\u015bli \u201ewszystkie encje\u201d jest w\u0142\u0105czone, stan grupy jest w\u0142\u0105czony tylko wtedy, gdy wszystkie encje s\u0105 w\u0142\u0105czone. Je\u015bli \u201ewszystkie encje\u201d jest wy\u0142\u0105czone, stan grupy jest w\u0142\u0105czony, je\u015bli kt\u00f3rakolwiek encja jest w\u0142\u0105czona.", "title": "Dodaj grup\u0119" }, - "light_options": { - "data": { - "entities": "Encje w grupie" - }, - "description": "Wybierz opcje grupy" - }, "lock": { "data": { "entities": "Encje", @@ -78,18 +47,10 @@ "data": { "entities": "Encje", "hide_members": "Ukryj encje", - "name": "Nazwa", - "title": "Dodaj grup\u0119" + "name": "Nazwa" }, - "description": "Wybierz opcje grupy", "title": "Dodaj grup\u0119" }, - "media_player_options": { - "data": { - "entities": "Encje w grupie" - }, - "description": "Wybierz opcje grupy" - }, "switch": { "data": { "entities": "Encje", @@ -99,9 +60,6 @@ "title": "Dodaj grup\u0119" }, "user": { - "data": { - "group_type": "Rodzaj grupy" - }, "description": "Grupy umo\u017cliwiaj\u0105 tworzenie nowej encji, kt\u00f3ra reprezentuje wiele encji tego samego typu.", "menu_options": { "binary_sensor": "Grupa sensor\u00f3w binarnych", @@ -126,34 +84,18 @@ }, "description": "Je\u015bli \u201ewszystkie encje\u201d jest w\u0142\u0105czone, stan grupy jest w\u0142\u0105czony tylko wtedy, gdy wszystkie encje s\u0105 w\u0142\u0105czone. Je\u015bli \u201ewszystkie encje\u201d jest wy\u0142\u0105czone, stan grupy jest w\u0142\u0105czony, je\u015bli kt\u00f3rakolwiek encja jest w\u0142\u0105czona." }, - "binary_sensor_options": { - "data": { - "all": "Wszystkie encje", - "entities": "Encje" - } - }, "cover": { "data": { "entities": "Encje", "hide_members": "Ukryj encje" } }, - "cover_options": { - "data": { - "entities": "Encje" - } - }, "fan": { "data": { "entities": "Encje", "hide_members": "Ukryj encje" } }, - "fan_options": { - "data": { - "entities": "Encje" - } - }, "light": { "data": { "all": "Wszystkie encje", @@ -162,12 +104,6 @@ }, "description": "Je\u015bli \u201ewszystkie encje\u201d jest w\u0142\u0105czone, stan grupy jest w\u0142\u0105czony tylko wtedy, gdy wszystkie encje s\u0105 w\u0142\u0105czone. Je\u015bli \u201ewszystkie encje\u201d jest wy\u0142\u0105czone, stan grupy jest w\u0142\u0105czony, je\u015bli kt\u00f3rakolwiek encja jest w\u0142\u0105czona." }, - "light_options": { - "data": { - "all": "Wszystkie encje", - "entities": "Encje" - } - }, "lock": { "data": { "entities": "Encje", @@ -180,11 +116,6 @@ "hide_members": "Ukryj encje" } }, - "media_player_options": { - "data": { - "entities": "Encje" - } - }, "switch": { "data": { "all": "Wszystkie encje", diff --git a/homeassistant/components/group/translations/pt-BR.json b/homeassistant/components/group/translations/pt-BR.json index 0987b41f4ae..1f1c7d09719 100644 --- a/homeassistant/components/group/translations/pt-BR.json +++ b/homeassistant/components/group/translations/pt-BR.json @@ -15,57 +15,26 @@ "data": { "entities": "Membros", "hide_members": "Ocultar membros", - "name": "Nome", - "title": "Adicionar grupo" + "name": "Nome" }, - "description": "Selecione as op\u00e7\u00f5es do grupo", "title": "Adicionar grupo" }, - "cover_options": { - "data": { - "entities": "Membros do grupo" - }, - "description": "Selecione as op\u00e7\u00f5es do grupo" - }, "fan": { "data": { "entities": "Membros", "hide_members": "Ocultar membros", - "name": "Nome", - "title": "Adicionar grupo" + "name": "Nome" }, - "description": "Selecione as op\u00e7\u00f5es do grupo", "title": "Adicionar grupo" }, - "fan_options": { - "data": { - "entities": "Membros do grupo" - }, - "description": "Selecione as op\u00e7\u00f5es do grupo" - }, - "init": { - "data": { - "group_type": "Tipo de grupo" - }, - "description": "Selecione o tipo de grupo" - }, "light": { "data": { - "all": "Todas as entidades", "entities": "Membros", "hide_members": "Ocultar membros", - "name": "Nome", - "title": "Adicionar grupo" + "name": "Nome" }, - "description": "Se \"todas as entidades\" estiver habilitada, o estado do grupo estar\u00e1 ativado somente se todos os membros estiverem ativados. Se \"todas as entidades\" estiver desabilitada, o estado do grupo estar\u00e1 ativado se algum membro estiver ativado.", "title": "Adicionar grupo" }, - "light_options": { - "data": { - "entities": "Membros do grupo" - }, - "description": "Selecione as op\u00e7\u00f5es do grupo" - }, "lock": { "data": { "entities": "Membros", @@ -78,18 +47,10 @@ "data": { "entities": "Membros", "hide_members": "Ocultar membros", - "name": "Nome", - "title": "Adicionar grupo" + "name": "Nome" }, - "description": "Selecione as op\u00e7\u00f5es do grupo", "title": "Adicionar grupo" }, - "media_player_options": { - "data": { - "entities": "Membros do grupo" - }, - "description": "Selecione as op\u00e7\u00f5es do grupo" - }, "switch": { "data": { "entities": "Membros", @@ -99,9 +60,6 @@ "title": "Adicionar grupo" }, "user": { - "data": { - "group_type": "Tipo de grupo" - }, "description": "Os grupos permitem que voc\u00ea crie uma nova entidade que representa v\u00e1rias entidades do mesmo tipo.", "menu_options": { "binary_sensor": "Grupo de sensores bin\u00e1rios", @@ -126,34 +84,18 @@ }, "description": "Se \"todas as entidades\" estiver habilitada, o estado do grupo estar\u00e1 ativado somente se todos os membros estiverem ativados. Se \"todas as entidades\" estiver desabilitada, o estado do grupo estar\u00e1 ativado se algum membro estiver ativado." }, - "binary_sensor_options": { - "data": { - "all": "Todas as entidades", - "entities": "Membros" - } - }, "cover": { "data": { "entities": "Membros", "hide_members": "Ocultar membros" } }, - "cover_options": { - "data": { - "entities": "Membros" - } - }, "fan": { "data": { "entities": "Membros", "hide_members": "Ocultar membros" } }, - "fan_options": { - "data": { - "entities": "Membros" - } - }, "light": { "data": { "all": "Todas as entidades", @@ -162,12 +104,6 @@ }, "description": "Se \"todas as entidades\" estiver habilitada, o estado do grupo estar\u00e1 ativado somente se todos os membros estiverem ativados. Se \"todas as entidades\" estiver desabilitada, o estado do grupo estar\u00e1 ativado se algum membro estiver ativado." }, - "light_options": { - "data": { - "all": "Todas as entidades", - "entities": "Membros" - } - }, "lock": { "data": { "entities": "Membros", @@ -180,11 +116,6 @@ "hide_members": "Ocultar membros" } }, - "media_player_options": { - "data": { - "entities": "Membros" - } - }, "switch": { "data": { "all": "Todas as entidades", diff --git a/homeassistant/components/group/translations/pt.json b/homeassistant/components/group/translations/pt.json index 9333d19b997..940aa088ced 100644 --- a/homeassistant/components/group/translations/pt.json +++ b/homeassistant/components/group/translations/pt.json @@ -9,25 +9,6 @@ } } }, - "options": { - "step": { - "fan_options": { - "data": { - "entities": "Membros" - } - }, - "light_options": { - "data": { - "entities": "Membros" - } - }, - "media_player_options": { - "data": { - "entities": "Membros" - } - } - } - }, "state": { "_": { "closed": "Fechada", diff --git a/homeassistant/components/group/translations/ru.json b/homeassistant/components/group/translations/ru.json index 0ba6d79900e..319121b69e8 100644 --- a/homeassistant/components/group/translations/ru.json +++ b/homeassistant/components/group/translations/ru.json @@ -15,57 +15,26 @@ "data": { "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "title": "\u0413\u0440\u0443\u043f\u043f\u0430" + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b.", "title": "\u0413\u0440\u0443\u043f\u043f\u0430" }, - "cover_options": { - "data": { - "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." - }, "fan": { "data": { "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "title": "\u0413\u0440\u0443\u043f\u043f\u0430" + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b.", "title": "\u0413\u0440\u0443\u043f\u043f\u0430" }, - "fan_options": { - "data": { - "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." - }, - "init": { - "data": { - "group_type": "\u0422\u0438\u043f \u0433\u0440\u0443\u043f\u043f\u044b" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u0433\u0440\u0443\u043f\u043f\u044b." - }, "light": { "data": { - "all": "\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "title": "\u0413\u0440\u0443\u043f\u043f\u0430" + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u0415\u0441\u043b\u0438 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \"\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b\", \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0443\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0432\u0441\u0435 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438. \u0412 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u043b\u044e\u0431\u043e\u0439 \u0438\u0437 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432.", "title": "\u0413\u0440\u0443\u043f\u043f\u0430" }, - "light_options": { - "data": { - "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." - }, "lock": { "data": { "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", @@ -78,18 +47,10 @@ "data": { "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "title": "\u0413\u0440\u0443\u043f\u043f\u0430" + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b.", "title": "\u0413\u0440\u0443\u043f\u043f\u0430" }, - "media_player_options": { - "data": { - "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438 \u0433\u0440\u0443\u043f\u043f\u044b" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0433\u0440\u0443\u043f\u043f\u044b." - }, "switch": { "data": { "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", @@ -99,9 +60,6 @@ "title": "\u0413\u0440\u0443\u043f\u043f\u0430" }, "user": { - "data": { - "group_type": "\u0422\u0438\u043f \u0433\u0440\u0443\u043f\u043f\u044b" - }, "description": "\u0413\u0440\u0443\u043f\u043f\u044b \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u044e\u0442 \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u044e\u0449\u0438\u0435 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u043e\u0434\u043d\u043e\u0433\u043e \u0442\u0438\u043f\u0430.", "menu_options": { "binary_sensor": "\u0413\u0440\u0443\u043f\u043f\u0430 \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432", @@ -126,34 +84,18 @@ }, "description": "\u0415\u0441\u043b\u0438 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \"\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b\", \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0443\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0432\u0441\u0435 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438. \u0412 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u043b\u044e\u0431\u043e\u0439 \u0438\u0437 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432." }, - "binary_sensor_options": { - "data": { - "all": "\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", - "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" - } - }, "cover": { "data": { "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432" } }, - "cover_options": { - "data": { - "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" - } - }, "fan": { "data": { "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432" } }, - "fan_options": { - "data": { - "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" - } - }, "light": { "data": { "all": "\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", @@ -162,12 +104,6 @@ }, "description": "\u0415\u0441\u043b\u0438 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \"\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b\", \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0443\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0432\u0441\u0435 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438. \u0412 \u043f\u0440\u043e\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \"\u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\" \u043a\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u043b\u044e\u0431\u043e\u0439 \u0438\u0437 \u0435\u0451 \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432." }, - "light_options": { - "data": { - "all": "\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", - "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" - } - }, "lock": { "data": { "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438", @@ -180,11 +116,6 @@ "hide_members": "\u0421\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0443\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u043e\u0432" } }, - "media_player_options": { - "data": { - "entities": "\u0423\u0447\u0430\u0441\u0442\u043d\u0438\u043a\u0438" - } - }, "switch": { "data": { "all": "\u0412\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", diff --git a/homeassistant/components/group/translations/sv.json b/homeassistant/components/group/translations/sv.json index 9ee5390db8a..77ae43cb1ee 100644 --- a/homeassistant/components/group/translations/sv.json +++ b/homeassistant/components/group/translations/sv.json @@ -9,64 +9,11 @@ }, "title": "Ny grupp" }, - "cover": { - "data": { - "title": "Ny grupp" - } - }, - "fan": { - "data": { - "title": "Ny grupp" - } - }, - "light": { - "data": { - "title": "Ny grupp" - } - }, - "media_player": { - "data": { - "title": "Ny grupp" - } - }, "user": { - "data": { - "group_type": "Grupptyp" - }, "title": "Ny grupp" } } }, - "options": { - "step": { - "binary_sensor_options": { - "data": { - "all": "Alla entiteter", - "entities": "Medlemmar" - } - }, - "cover_options": { - "data": { - "entities": "Medlemmar" - } - }, - "fan_options": { - "data": { - "entities": "Medlemmar" - } - }, - "light_options": { - "data": { - "entities": "Medlemmar" - } - }, - "media_player_options": { - "data": { - "entities": "Medlemmar" - } - } - } - }, "state": { "_": { "closed": "St\u00e4ngd", diff --git a/homeassistant/components/group/translations/tr.json b/homeassistant/components/group/translations/tr.json index 354ed29214c..49648f55ec4 100644 --- a/homeassistant/components/group/translations/tr.json +++ b/homeassistant/components/group/translations/tr.json @@ -15,57 +15,26 @@ "data": { "entities": "\u00dcyeler", "hide_members": "\u00dcyeleri gizle", - "name": "Ad", - "title": "Grup Ekle" + "name": "Ad" }, - "description": "Grup se\u00e7eneklerini se\u00e7in", "title": "Grup Ekle" }, - "cover_options": { - "data": { - "entities": "Grup \u00fcyeleri" - }, - "description": "Grup se\u00e7eneklerini se\u00e7in" - }, "fan": { "data": { "entities": "\u00dcyeler", "hide_members": "\u00dcyeleri gizle", - "name": "Ad", - "title": "Grup Ekle" + "name": "Ad" }, - "description": "Grup se\u00e7eneklerini se\u00e7in", "title": "Grup Ekle" }, - "fan_options": { - "data": { - "entities": "Grup \u00fcyeleri" - }, - "description": "Grup se\u00e7eneklerini se\u00e7in" - }, - "init": { - "data": { - "group_type": "Grup t\u00fcr\u00fc" - }, - "description": "Grup t\u00fcr\u00fcn\u00fc se\u00e7in" - }, "light": { "data": { - "all": "T\u00fcm varl\u0131klar", "entities": "\u00dcyeler", "hide_members": "\u00dcyeleri gizle", - "name": "Ad", - "title": "Grup Ekle" + "name": "Ad" }, - "description": "\"T\u00fcm varl\u0131klar\" etkinle\u015ftirilirse, grubun durumu yaln\u0131zca t\u00fcm \u00fcyeler a\u00e7\u0131ksa a\u00e7\u0131kt\u0131r. \"T\u00fcm varl\u0131klar\" devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131rsa, herhangi bir \u00fcye a\u00e7\u0131ksa grubun durumu a\u00e7\u0131kt\u0131r.", "title": "Grup Ekle" }, - "light_options": { - "data": { - "entities": "Grup \u00fcyeleri" - }, - "description": "Grup se\u00e7eneklerini se\u00e7in" - }, "lock": { "data": { "entities": "\u00dcyeler", @@ -78,18 +47,10 @@ "data": { "entities": "\u00dcyeler", "hide_members": "\u00dcyeleri gizle", - "name": "Ad", - "title": "Grup Ekle" + "name": "Ad" }, - "description": "Grup se\u00e7eneklerini se\u00e7in", "title": "Grup Ekle" }, - "media_player_options": { - "data": { - "entities": "Grup \u00fcyeleri" - }, - "description": "Grup se\u00e7eneklerini se\u00e7in" - }, "switch": { "data": { "entities": "\u00dcyeler", @@ -99,9 +60,6 @@ "title": "Grup Ekle" }, "user": { - "data": { - "group_type": "Grup t\u00fcr\u00fc" - }, "description": "Gruplar, ayn\u0131 t\u00fcrden birden \u00e7ok varl\u0131\u011f\u0131 temsil eden yeni bir varl\u0131k olu\u015fturman\u0131za olanak tan\u0131r.", "menu_options": { "binary_sensor": "\u0130kili sens\u00f6r grubu", @@ -126,34 +84,18 @@ }, "description": "\"T\u00fcm varl\u0131klar\" etkinle\u015ftirilirse, grubun durumu yaln\u0131zca t\u00fcm \u00fcyeler a\u00e7\u0131ksa a\u00e7\u0131kt\u0131r. \"T\u00fcm varl\u0131klar\" devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131rsa, herhangi bir \u00fcye a\u00e7\u0131ksa grubun durumu a\u00e7\u0131kt\u0131r." }, - "binary_sensor_options": { - "data": { - "all": "T\u00fcm varl\u0131klar", - "entities": "\u00dcyeler" - } - }, "cover": { "data": { "entities": "\u00dcyeler", "hide_members": "\u00dcyeleri gizle" } }, - "cover_options": { - "data": { - "entities": "\u00dcyeler" - } - }, "fan": { "data": { "entities": "\u00dcyeler", "hide_members": "\u00dcyeleri gizle" } }, - "fan_options": { - "data": { - "entities": "\u00dcyeler" - } - }, "light": { "data": { "all": "T\u00fcm varl\u0131klar", @@ -162,12 +104,6 @@ }, "description": "\"T\u00fcm varl\u0131klar\" etkinle\u015ftirilirse, grubun durumu yaln\u0131zca t\u00fcm \u00fcyeler a\u00e7\u0131ksa a\u00e7\u0131kt\u0131r. \"T\u00fcm varl\u0131klar\" devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131rsa, herhangi bir \u00fcye a\u00e7\u0131ksa grubun durumu a\u00e7\u0131kt\u0131r." }, - "light_options": { - "data": { - "all": "T\u00fcm varl\u0131klar", - "entities": "\u00dcyeler" - } - }, "lock": { "data": { "entities": "\u00dcyeler", @@ -180,11 +116,6 @@ "hide_members": "\u00dcyeleri gizle" } }, - "media_player_options": { - "data": { - "entities": "\u00dcyeler" - } - }, "switch": { "data": { "all": "T\u00fcm varl\u0131klar", diff --git a/homeassistant/components/group/translations/zh-Hant.json b/homeassistant/components/group/translations/zh-Hant.json index 380c2216976..023c76ebbba 100644 --- a/homeassistant/components/group/translations/zh-Hant.json +++ b/homeassistant/components/group/translations/zh-Hant.json @@ -15,57 +15,26 @@ "data": { "entities": "\u6210\u54e1", "hide_members": "\u96b1\u85cf\u6210\u54e1", - "name": "\u540d\u7a31", - "title": "\u65b0\u589e\u7fa4\u7d44" + "name": "\u540d\u7a31" }, - "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805", "title": "\u65b0\u589e\u7fa4\u7d44" }, - "cover_options": { - "data": { - "entities": "\u7fa4\u7d44\u6210\u54e1" - }, - "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" - }, "fan": { "data": { "entities": "\u6210\u54e1", "hide_members": "\u96b1\u85cf\u6210\u54e1", - "name": "\u540d\u7a31", - "title": "\u65b0\u589e\u7fa4\u7d44" + "name": "\u540d\u7a31" }, - "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805", "title": "\u65b0\u589e\u7fa4\u7d44" }, - "fan_options": { - "data": { - "entities": "\u7fa4\u7d44\u6210\u54e1" - }, - "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" - }, - "init": { - "data": { - "group_type": "\u7fa4\u7d44\u985e\u5225" - }, - "description": "\u9078\u64c7\u7fa4\u7d44\u985e\u5225" - }, "light": { "data": { - "all": "\u6240\u6709\u5be6\u9ad4", "entities": "\u6210\u54e1", "hide_members": "\u96b1\u85cf\u6210\u54e1", - "name": "\u540d\u7a31", - "title": "\u65b0\u589e\u7fa4\u7d44" + "name": "\u540d\u7a31" }, - "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u88dd\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002", "title": "\u65b0\u589e\u7fa4\u7d44" }, - "light_options": { - "data": { - "entities": "\u7fa4\u7d44\u6210\u54e1" - }, - "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" - }, "lock": { "data": { "entities": "\u6210\u54e1", @@ -78,18 +47,10 @@ "data": { "entities": "\u6210\u54e1", "hide_members": "\u96b1\u85cf\u6210\u54e1", - "name": "\u540d\u7a31", - "title": "\u65b0\u589e\u7fa4\u7d44" + "name": "\u540d\u7a31" }, - "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805", "title": "\u65b0\u589e\u7fa4\u7d44" }, - "media_player_options": { - "data": { - "entities": "\u7fa4\u7d44\u6210\u54e1" - }, - "description": "\u9078\u64c7\u7fa4\u7d44\u9078\u9805" - }, "switch": { "data": { "entities": "\u6210\u54e1", @@ -99,9 +60,6 @@ "title": "\u65b0\u589e\u7fa4\u7d44" }, "user": { - "data": { - "group_type": "\u7fa4\u7d44\u985e\u5225" - }, "description": "\u7fa4\u7d44\u5141\u8a31\u4f7f\u7528\u76f8\u540c\u985e\u5225\u7684\u591a\u500b\u5be6\u9ad4\u7d44\u6210\u65b0\u5be6\u9ad4\u3002", "menu_options": { "binary_sensor": "\u4e8c\u9032\u4f4d\u611f\u6e2c\u5668\u7fa4\u7d44", @@ -126,34 +84,18 @@ }, "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u88dd\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002" }, - "binary_sensor_options": { - "data": { - "all": "\u6240\u6709\u5be6\u9ad4", - "entities": "\u6210\u54e1" - } - }, "cover": { "data": { "entities": "\u6210\u54e1", "hide_members": "\u96b1\u85cf\u6210\u54e1" } }, - "cover_options": { - "data": { - "entities": "\u6210\u54e1" - } - }, "fan": { "data": { "entities": "\u6210\u54e1", "hide_members": "\u96b1\u85cf\u6210\u54e1" } }, - "fan_options": { - "data": { - "entities": "\u6210\u54e1" - } - }, "light": { "data": { "all": "\u6240\u6709\u5be6\u9ad4", @@ -162,12 +104,6 @@ }, "description": "\u5047\u5982\u958b\u555f \"\u6240\u6709\u5be6\u9ad4\"\uff0c\u50c5\u65bc\u7576\u6240\u6709\u6210\u54e1\u90fd\u70ba\u958b\u555f\u6642\u3001\u88dd\u614b\u624d\u6703\u986f\u793a\u70ba\u958b\u555f\u3002\u5047\u5982 \"\u6240\u6709\u5be6\u9ad4\" \u70ba\u95dc\u9589\u3001\u5247\u4efb\u4f55\u6210\u54e1\u958b\u59cb\u6642\uff0c\u7686\u6703\u986f\u793a\u70ba\u958b\u555f\u3002" }, - "light_options": { - "data": { - "all": "\u6240\u6709\u5be6\u9ad4", - "entities": "\u6210\u54e1" - } - }, "lock": { "data": { "entities": "\u6210\u54e1", @@ -180,11 +116,6 @@ "hide_members": "\u96b1\u85cf\u6210\u54e1" } }, - "media_player_options": { - "data": { - "entities": "\u6210\u54e1" - } - }, "switch": { "data": { "all": "\u6240\u6709\u5be6\u9ad4", diff --git a/homeassistant/components/guardian/translations/ca.json b/homeassistant/components/guardian/translations/ca.json index 0831975511e..59d9f6d03b8 100644 --- a/homeassistant/components/guardian/translations/ca.json +++ b/homeassistant/components/guardian/translations/ca.json @@ -15,9 +15,6 @@ "port": "Port" }, "description": "Configura un dispositiu Elexa Guardian local." - }, - "zeroconf_confirm": { - "description": "Vols configurar aquest dispositiu Guardian?" } } } diff --git a/homeassistant/components/guardian/translations/cs.json b/homeassistant/components/guardian/translations/cs.json index cb5ee7ef102..087c4b7ba02 100644 --- a/homeassistant/components/guardian/translations/cs.json +++ b/homeassistant/components/guardian/translations/cs.json @@ -12,9 +12,6 @@ "port": "Port" }, "description": "Nastate m\u00edstn\u00ed za\u0159\u00edzen\u00ed Elexa Guardian." - }, - "zeroconf_confirm": { - "description": "Chcete nastavit toto za\u0159\u00edzen\u00ed Guardian?" } } } diff --git a/homeassistant/components/guardian/translations/de.json b/homeassistant/components/guardian/translations/de.json index 63949b22de6..2078df1cae9 100644 --- a/homeassistant/components/guardian/translations/de.json +++ b/homeassistant/components/guardian/translations/de.json @@ -15,9 +15,6 @@ "port": "Port" }, "description": "Konfiguriere ein lokales Elexa Guardian Ger\u00e4t." - }, - "zeroconf_confirm": { - "description": "M\u00f6chtest du dieses Guardian-Ger\u00e4t einrichten?" } } } diff --git a/homeassistant/components/guardian/translations/el.json b/homeassistant/components/guardian/translations/el.json index 8173f4d7fa0..8f6a77ac2ec 100644 --- a/homeassistant/components/guardian/translations/el.json +++ b/homeassistant/components/guardian/translations/el.json @@ -15,9 +15,6 @@ "port": "\u0398\u03cd\u03c1\u03b1" }, "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Elexa Guardian." - }, - "zeroconf_confirm": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Guardian;" } } } diff --git a/homeassistant/components/guardian/translations/en.json b/homeassistant/components/guardian/translations/en.json index 52932cce02b..310f550bcc1 100644 --- a/homeassistant/components/guardian/translations/en.json +++ b/homeassistant/components/guardian/translations/en.json @@ -15,9 +15,6 @@ "port": "Port" }, "description": "Configure a local Elexa Guardian device." - }, - "zeroconf_confirm": { - "description": "Do you want to set up this Guardian device?" } } } diff --git a/homeassistant/components/guardian/translations/es.json b/homeassistant/components/guardian/translations/es.json index 531d4ac5774..fe2367232a4 100644 --- a/homeassistant/components/guardian/translations/es.json +++ b/homeassistant/components/guardian/translations/es.json @@ -15,9 +15,6 @@ "port": "Puerto" }, "description": "Configurar un dispositivo local Elexa Guardian." - }, - "zeroconf_confirm": { - "description": "\u00bfQuieres configurar este dispositivo Guardian?" } } } diff --git a/homeassistant/components/guardian/translations/et.json b/homeassistant/components/guardian/translations/et.json index 56aec0e00c7..22ee0bf1300 100644 --- a/homeassistant/components/guardian/translations/et.json +++ b/homeassistant/components/guardian/translations/et.json @@ -15,9 +15,6 @@ "port": "" }, "description": "Seadista kohalik Elexa Guardiani seade." - }, - "zeroconf_confirm": { - "description": "Kas soovid seadistada seda Guardian'i seadet?" } } } diff --git a/homeassistant/components/guardian/translations/fr.json b/homeassistant/components/guardian/translations/fr.json index e1e1bcb4fcf..d76ec4886e3 100644 --- a/homeassistant/components/guardian/translations/fr.json +++ b/homeassistant/components/guardian/translations/fr.json @@ -15,9 +15,6 @@ "port": "Port" }, "description": "Configurez un appareil Elexa Guardian local." - }, - "zeroconf_confirm": { - "description": "Voulez-vous configurer cet appareil Guardian?" } } } diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json index 82fd422abf7..1f8f9039707 100644 --- a/homeassistant/components/guardian/translations/hu.json +++ b/homeassistant/components/guardian/translations/hu.json @@ -15,9 +15,6 @@ "port": "Port" }, "description": "Konfigur\u00e1lja a helyi Elexa Guardian eszk\u00f6zt." - }, - "zeroconf_confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani ezt a Guardian eszk\u00f6zt?" } } } diff --git a/homeassistant/components/guardian/translations/id.json b/homeassistant/components/guardian/translations/id.json index 8193386fb62..77c46e95afc 100644 --- a/homeassistant/components/guardian/translations/id.json +++ b/homeassistant/components/guardian/translations/id.json @@ -15,9 +15,6 @@ "port": "Port" }, "description": "Konfigurasikan perangkat Elexa Guardian lokal." - }, - "zeroconf_confirm": { - "description": "Ingin menyiapkan perangkat Guardian ini?" } } } diff --git a/homeassistant/components/guardian/translations/it.json b/homeassistant/components/guardian/translations/it.json index 5db0956d80f..e6450dae21d 100644 --- a/homeassistant/components/guardian/translations/it.json +++ b/homeassistant/components/guardian/translations/it.json @@ -15,9 +15,6 @@ "port": "Porta" }, "description": "Configura un dispositivo Elexa Guardian locale." - }, - "zeroconf_confirm": { - "description": "Vuoi configurare questo dispositivo Guardian?" } } } diff --git a/homeassistant/components/guardian/translations/ja.json b/homeassistant/components/guardian/translations/ja.json index 0c282a324bd..337223c5736 100644 --- a/homeassistant/components/guardian/translations/ja.json +++ b/homeassistant/components/guardian/translations/ja.json @@ -15,9 +15,6 @@ "port": "\u30dd\u30fc\u30c8" }, "description": "\u30ed\u30fc\u30ab\u30eb\u306eElexa Guardian\u30c7\u30d0\u30a4\u30b9\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002" - }, - "zeroconf_confirm": { - "description": "\u3053\u306eGuardian\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" } } } diff --git a/homeassistant/components/guardian/translations/ko.json b/homeassistant/components/guardian/translations/ko.json index d9f70ad2d33..8c3c5103c3c 100644 --- a/homeassistant/components/guardian/translations/ko.json +++ b/homeassistant/components/guardian/translations/ko.json @@ -12,9 +12,6 @@ "port": "\ud3ec\ud2b8" }, "description": "\ub85c\uceec Elexa Guardian \uae30\uae30\ub97c \uad6c\uc131\ud574\uc8fc\uc138\uc694." - }, - "zeroconf_confirm": { - "description": "\uc774 Guardian \uae30\uae30\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } } diff --git a/homeassistant/components/guardian/translations/lb.json b/homeassistant/components/guardian/translations/lb.json index 8d062eb8cf7..bc3fdc1eed3 100644 --- a/homeassistant/components/guardian/translations/lb.json +++ b/homeassistant/components/guardian/translations/lb.json @@ -12,9 +12,6 @@ "port": "Port" }, "description": "Ee lokalen Elexa Guardian Apparat ariichten." - }, - "zeroconf_confirm": { - "description": "Soll d\u00ebsen Guardian Apparat konfigur\u00e9iert ginn?" } } } diff --git a/homeassistant/components/guardian/translations/nl.json b/homeassistant/components/guardian/translations/nl.json index 409c3db9bed..42d499315bd 100644 --- a/homeassistant/components/guardian/translations/nl.json +++ b/homeassistant/components/guardian/translations/nl.json @@ -15,9 +15,6 @@ "port": "Poort" }, "description": "Configureer een lokaal Elexa Guardian-apparaat." - }, - "zeroconf_confirm": { - "description": "Wilt u dit Guardian-apparaat instellen?" } } } diff --git a/homeassistant/components/guardian/translations/no.json b/homeassistant/components/guardian/translations/no.json index 28313fa8520..a61b43dcfea 100644 --- a/homeassistant/components/guardian/translations/no.json +++ b/homeassistant/components/guardian/translations/no.json @@ -15,9 +15,6 @@ "port": "Port" }, "description": "Konfigurer en lokal Elexa Guardian-enhet." - }, - "zeroconf_confirm": { - "description": "Vil du konfigurere denne Guardian-enheten?" } } } diff --git a/homeassistant/components/guardian/translations/pl.json b/homeassistant/components/guardian/translations/pl.json index 663265a7a11..5e5b2d143e2 100644 --- a/homeassistant/components/guardian/translations/pl.json +++ b/homeassistant/components/guardian/translations/pl.json @@ -15,9 +15,6 @@ "port": "Port" }, "description": "Skonfiguruj lokalne urz\u0105dzenie Elexa Guardian." - }, - "zeroconf_confirm": { - "description": "Czy chcesz skonfigurowa\u0107 to urz\u0105dzenie Guardian?" } } } diff --git a/homeassistant/components/guardian/translations/pt-BR.json b/homeassistant/components/guardian/translations/pt-BR.json index f0996e6b84e..f4d4273b4ab 100644 --- a/homeassistant/components/guardian/translations/pt-BR.json +++ b/homeassistant/components/guardian/translations/pt-BR.json @@ -15,9 +15,6 @@ "port": "Porta" }, "description": "Configure um dispositivo local Elexa Guardian." - }, - "zeroconf_confirm": { - "description": "Deseja configurar este dispositivo Guardian?" } } } diff --git a/homeassistant/components/guardian/translations/ru.json b/homeassistant/components/guardian/translations/ru.json index ca904d980af..fe93ccb6369 100644 --- a/homeassistant/components/guardian/translations/ru.json +++ b/homeassistant/components/guardian/translations/ru.json @@ -15,9 +15,6 @@ "port": "\u041f\u043e\u0440\u0442" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Elexa Guardian." - }, - "zeroconf_confirm": { - "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Elexa Guardian?" } } } diff --git a/homeassistant/components/guardian/translations/tr.json b/homeassistant/components/guardian/translations/tr.json index 9d3e903a2d6..fe4dffad3aa 100644 --- a/homeassistant/components/guardian/translations/tr.json +++ b/homeassistant/components/guardian/translations/tr.json @@ -15,9 +15,6 @@ "port": "Port" }, "description": "Yerel bir Elexa Guardian cihaz\u0131 yap\u0131land\u0131r\u0131n." - }, - "zeroconf_confirm": { - "description": "Bu Guardian cihaz\u0131n\u0131 kurmak istiyor musunuz?" } } } diff --git a/homeassistant/components/guardian/translations/uk.json b/homeassistant/components/guardian/translations/uk.json index 439a225895e..fd6ecc8d6d1 100644 --- a/homeassistant/components/guardian/translations/uk.json +++ b/homeassistant/components/guardian/translations/uk.json @@ -12,9 +12,6 @@ "port": "\u041f\u043e\u0440\u0442" }, "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Elexa Guardian." - }, - "zeroconf_confirm": { - "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0446\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Elexa Guardian?" } } } diff --git a/homeassistant/components/guardian/translations/zh-Hant.json b/homeassistant/components/guardian/translations/zh-Hant.json index dc3bde8ec1c..40a7de81170 100644 --- a/homeassistant/components/guardian/translations/zh-Hant.json +++ b/homeassistant/components/guardian/translations/zh-Hant.json @@ -15,9 +15,6 @@ "port": "\u901a\u8a0a\u57e0" }, "description": "\u8a2d\u5b9a\u5340\u57df Elexa Guardian \u88dd\u7f6e\u3002" - }, - "zeroconf_confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Guardian \u88dd\u7f6e\uff1f" } } } diff --git a/homeassistant/components/habitica/translations/ca.json b/homeassistant/components/habitica/translations/ca.json index 675fc33db8c..729d03834b5 100644 --- a/homeassistant/components/habitica/translations/ca.json +++ b/homeassistant/components/habitica/translations/ca.json @@ -15,6 +15,5 @@ "description": "Connecta el perfil d'Habitica per permetre el seguiment del teu perfil i tasques d'usuari. Tingues en compte que l'api_id i l'api_key els has d'obtenir des de https://habitica.com/user/settings/api" } } - }, - "title": "Habitica" + } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/de.json b/homeassistant/components/habitica/translations/de.json index 694bbdd65d4..6eac60a55d3 100644 --- a/homeassistant/components/habitica/translations/de.json +++ b/homeassistant/components/habitica/translations/de.json @@ -15,6 +15,5 @@ "description": "Verbinde dein Habitica-Profil, um die \u00dcberwachung des Profils und der Aufgaben deines Benutzers zu erm\u00f6glichen. Beachte, dass api_id und api_key von https://habitica.com/user/settings/api bezogen werden m\u00fcssen." } } - }, - "title": "Habitica" + } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/el.json b/homeassistant/components/habitica/translations/el.json index 43257c239a9..ddacd192d6e 100644 --- a/homeassistant/components/habitica/translations/el.json +++ b/homeassistant/components/habitica/translations/el.json @@ -15,6 +15,5 @@ "description": "\u03a3\u03c5\u03bd\u03b4\u03ad\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03bf\u03c6\u03af\u03bb \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Habitica \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c8\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03c1\u03bf\u03c6\u03af\u03bb \u03ba\u03b1\u03b9 \u03c4\u03c9\u03bd \u03b5\u03c1\u03b3\u03b1\u03c3\u03b9\u03ce\u03bd \u03c4\u03bf\u03c5 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c3\u03b1\u03c2. \u03a3\u03b7\u03bc\u03b5\u03b9\u03ce\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c4\u03bf api_id \u03ba\u03b1\u03b9 \u03c4\u03bf api_key \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03bb\u03b7\u03c6\u03b8\u03bf\u03cd\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf https://habitica.com/user/settings/api." } } - }, - "title": "Habitica" + } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/en.json b/homeassistant/components/habitica/translations/en.json index ffbbd2de840..377e3129ee8 100644 --- a/homeassistant/components/habitica/translations/en.json +++ b/homeassistant/components/habitica/translations/en.json @@ -15,6 +15,5 @@ "description": "Connect your Habitica profile to allow monitoring of your user's profile and tasks. Note that api_id and api_key must be gotten from https://habitica.com/user/settings/api" } } - }, - "title": "Habitica" + } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/es.json b/homeassistant/components/habitica/translations/es.json index 6850c903b99..55cf8eb7642 100644 --- a/homeassistant/components/habitica/translations/es.json +++ b/homeassistant/components/habitica/translations/es.json @@ -15,6 +15,5 @@ "description": "Conecta tu perfil de Habitica para permitir la supervisi\u00f3n del perfil y las tareas de tu usuario. Ten en cuenta que api_id y api_key deben obtenerse de https://habitica.com/user/settings/api" } } - }, - "title": "Habitica" + } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/et.json b/homeassistant/components/habitica/translations/et.json index cfc2bcf898c..f2ff048c1c4 100644 --- a/homeassistant/components/habitica/translations/et.json +++ b/homeassistant/components/habitica/translations/et.json @@ -15,6 +15,5 @@ "description": "\u00dchenda oma Habitica profiil, et saaksid j\u00e4lgida oma kasutaja profiili ja \u00fclesandeid. Pane t\u00e4hele, et api_id ja api_key tuleb hankida aadressilt https://habitica.com/user/settings/api" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/fr.json b/homeassistant/components/habitica/translations/fr.json index 9f04d9ed588..343f28c0a3e 100644 --- a/homeassistant/components/habitica/translations/fr.json +++ b/homeassistant/components/habitica/translations/fr.json @@ -15,6 +15,5 @@ "description": "Connectez votre profil Habitica pour permettre la surveillance du profil et des t\u00e2ches de votre utilisateur. Notez que api_id et api_key doivent \u00eatre obtenus de https://habitica.com/user/settings/api" } } - }, - "title": "Habitica" + } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/hu.json b/homeassistant/components/habitica/translations/hu.json index 589f53e852c..a26871e3cbf 100644 --- a/homeassistant/components/habitica/translations/hu.json +++ b/homeassistant/components/habitica/translations/hu.json @@ -15,6 +15,5 @@ "description": "Csatlakoztassa Habitica-profilj\u00e1t, hogy figyelemmel k\u00eds\u00e9rhesse felhaszn\u00e1l\u00f3i profilj\u00e1t \u00e9s feladatait. Ne feledje, hogy az api_id \u00e9s api_key c\u00edmeket a https://habitica.com/user/settings/api webhelyr\u0151l kell beszerezni" } } - }, - "title": "Habitica" + } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/id.json b/homeassistant/components/habitica/translations/id.json index c7e4c549206..9130a320b6d 100644 --- a/homeassistant/components/habitica/translations/id.json +++ b/homeassistant/components/habitica/translations/id.json @@ -15,6 +15,5 @@ "description": "Hubungkan profil Habitica Anda untuk memungkinkan pemantauan profil dan tugas pengguna Anda. Perhatikan bahwa api_id dan api_key harus diperoleh dari https://habitica.com/user/settings/api" } } - }, - "title": "Habitica" + } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/it.json b/homeassistant/components/habitica/translations/it.json index 2bef21519b6..9467c5af335 100644 --- a/homeassistant/components/habitica/translations/it.json +++ b/homeassistant/components/habitica/translations/it.json @@ -15,6 +15,5 @@ "description": "Collega il tuo profilo Habitica per consentire il monitoraggio del profilo e delle attivit\u00e0 dell'utente. Nota che api_id e api_key devono essere ottenuti da https://habitica.com/user/settings/api" } } - }, - "title": "Habitica" + } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/ja.json b/homeassistant/components/habitica/translations/ja.json index 28d877a4788..b478f5f083d 100644 --- a/homeassistant/components/habitica/translations/ja.json +++ b/homeassistant/components/habitica/translations/ja.json @@ -15,6 +15,5 @@ "description": "Habitica profile\u306b\u63a5\u7d9a\u3057\u3066\u3001\u3042\u306a\u305f\u306e\u30e6\u30fc\u30b6\u30fc\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3068\u30bf\u30b9\u30af\u3092\u76e3\u8996\u3067\u304d\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002 \u6ce8\u610f: api_id\u3068api_key\u306f\u3001https://habitica.com/user/settings/api \u304b\u3089\u53d6\u5f97\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" } } - }, - "title": "Habitica" + } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/ko.json b/homeassistant/components/habitica/translations/ko.json index 6b890a320df..7640f163cc0 100644 --- a/homeassistant/components/habitica/translations/ko.json +++ b/homeassistant/components/habitica/translations/ko.json @@ -15,6 +15,5 @@ "description": "\uc0ac\uc6a9\uc790\uc758 \ud504\ub85c\ud544 \ubc0f \uc791\uc5c5\uc744 \ubaa8\ub2c8\ud130\ub9c1\ud560 \uc218 \uc788\ub3c4\ub85d \ud558\ub824\uba74 Habitica \ud504\ub85c\ud544\uc744 \uc5f0\uacb0\ud574\uc8fc\uc138\uc694.\n\ucc38\uace0\ub85c api_id \ubc0f api_key\ub294 https://habitica.com/user/settings/api \uc5d0\uc11c \uac00\uc838\uc640\uc57c \ud569\ub2c8\ub2e4." } } - }, - "title": "Habitica" + } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/nl.json b/homeassistant/components/habitica/translations/nl.json index 817ffd8c616..91461372e59 100644 --- a/homeassistant/components/habitica/translations/nl.json +++ b/homeassistant/components/habitica/translations/nl.json @@ -15,6 +15,5 @@ "description": "Verbind uw Habitica-profiel om het profiel en de taken van uw gebruiker te bewaken. Houd er rekening mee dat api_id en api_key van https://habitica.com/user/settings/api moeten worden gehaald" } } - }, - "title": "Habitica" + } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/no.json b/homeassistant/components/habitica/translations/no.json index cdb72d3c3d6..218b861e513 100644 --- a/homeassistant/components/habitica/translations/no.json +++ b/homeassistant/components/habitica/translations/no.json @@ -15,6 +15,5 @@ "description": "Koble til Habitica-profilen din for \u00e5 tillate overv\u00e5king av brukerens profil og oppgaver. Merk at api_id og api_key m\u00e5 hentes fra https://habitica.com/user/settings/api" } } - }, - "title": "Habitica" + } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/pl.json b/homeassistant/components/habitica/translations/pl.json index f06f1a0e1aa..7a8c6a088bc 100644 --- a/homeassistant/components/habitica/translations/pl.json +++ b/homeassistant/components/habitica/translations/pl.json @@ -15,6 +15,5 @@ "description": "Po\u0142\u0105cz sw\u00f3j profil Habitica, aby umo\u017cliwi\u0107 monitorowanie profilu i zada\u0144 u\u017cytkownika. Pami\u0119taj, \u017ce api_id i api_key musz\u0105 zosta\u0107 pobrane z https://habitica.com/user/settings/api" } } - }, - "title": "Habitica" + } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/pt-BR.json b/homeassistant/components/habitica/translations/pt-BR.json index cbdb6e3453f..b5aafdea88b 100644 --- a/homeassistant/components/habitica/translations/pt-BR.json +++ b/homeassistant/components/habitica/translations/pt-BR.json @@ -15,6 +15,5 @@ "description": "Conecte seu perfil do Habitica para permitir o monitoramento do perfil e das tarefas do seu usu\u00e1rio. Observe que api_id e api_key devem ser obtidos em https://habitica.com/user/settings/api" } } - }, - "title": "Habitica" + } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/ru.json b/homeassistant/components/habitica/translations/ru.json index 4899cd1e43b..23576d2a6d6 100644 --- a/homeassistant/components/habitica/translations/ru.json +++ b/homeassistant/components/habitica/translations/ru.json @@ -15,6 +15,5 @@ "description": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 \u043f\u0440\u043e\u0444\u0438\u043b\u044c Habitica, \u0447\u0442\u043e\u0431\u044b \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0444\u0438\u043b\u044c \u0438 \u0437\u0430\u0434\u0430\u0447\u0438 \u0412\u0430\u0448\u0435\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f. \u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e api_id \u0438 api_key \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u044b \u0441 https://habitica.com/user/settings/api" } } - }, - "title": "Habitica" + } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/tr.json b/homeassistant/components/habitica/translations/tr.json index 32ad5aa4957..dfb29c8d17c 100644 --- a/homeassistant/components/habitica/translations/tr.json +++ b/homeassistant/components/habitica/translations/tr.json @@ -15,6 +15,5 @@ "description": "Kullan\u0131c\u0131n\u0131z\u0131n profilinin ve g\u00f6revlerinin izlenmesine izin vermek i\u00e7in Habitica profilinizi ba\u011flay\u0131n. api_id ve api_key'in https://habitica.com/user/settings/api adresinden al\u0131nmas\u0131 gerekti\u011fini unutmay\u0131n." } } - }, - "title": "Habitica" + } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/zh-Hant.json b/homeassistant/components/habitica/translations/zh-Hant.json index 65918685161..d5aeda2c359 100644 --- a/homeassistant/components/habitica/translations/zh-Hant.json +++ b/homeassistant/components/habitica/translations/zh-Hant.json @@ -15,6 +15,5 @@ "description": "\u9023\u7dda\u81f3 Habitica \u8a2d\u5b9a\u6a94\u4ee5\u4f9b\u76e3\u63a7\u500b\u4eba\u8a2d\u5b9a\u8207\u4efb\u52d9\u3002\u6ce8\u610f\uff1a\u5fc5\u9808\u7531 https://habitica.com/user/settings/api \u53d6\u5f97 api_id \u8207 api_key" } } - }, - "title": "Habitica" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/ca.json b/homeassistant/components/home_plus_control/translations/ca.json index 6e6dc1e0577..1fe3adb2d6c 100644 --- a/homeassistant/components/home_plus_control/translations/ca.json +++ b/homeassistant/components/home_plus_control/translations/ca.json @@ -16,6 +16,5 @@ "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" } } - }, - "title": "Legrand Home+ Control" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/de.json b/homeassistant/components/home_plus_control/translations/de.json index 8cb47ae3fec..5b927e03007 100644 --- a/homeassistant/components/home_plus_control/translations/de.json +++ b/homeassistant/components/home_plus_control/translations/de.json @@ -16,6 +16,5 @@ "title": "W\u00e4hle die Authentifizierungsmethode" } } - }, - "title": "Legrand Home+ Steuerung" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/el.json b/homeassistant/components/home_plus_control/translations/el.json index 724dafff28f..f2fbb7f4bb3 100644 --- a/homeassistant/components/home_plus_control/translations/el.json +++ b/homeassistant/components/home_plus_control/translations/el.json @@ -16,6 +16,5 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" } } - }, - "title": "\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 Legrand Home+" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/en.json b/homeassistant/components/home_plus_control/translations/en.json index f5f8afe73d1..250ca9d56c2 100644 --- a/homeassistant/components/home_plus_control/translations/en.json +++ b/homeassistant/components/home_plus_control/translations/en.json @@ -16,6 +16,5 @@ "title": "Pick Authentication Method" } } - }, - "title": "Legrand Home+ Control" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/es-419.json b/homeassistant/components/home_plus_control/translations/es-419.json index e35bb529a85..9596f3c9d2f 100644 --- a/homeassistant/components/home_plus_control/translations/es-419.json +++ b/homeassistant/components/home_plus_control/translations/es-419.json @@ -16,6 +16,5 @@ "title": "Escoja el m\u00e9todo de autenticaci\u00f3n" } } - }, - "title": "Legrand Home+ Control" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/es.json b/homeassistant/components/home_plus_control/translations/es.json index 82144e727ab..194eff4bb8c 100644 --- a/homeassistant/components/home_plus_control/translations/es.json +++ b/homeassistant/components/home_plus_control/translations/es.json @@ -16,6 +16,5 @@ "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" } } - }, - "title": "Legrand Home+ Control" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/et.json b/homeassistant/components/home_plus_control/translations/et.json index cfe40d86bcd..d03d44a9901 100644 --- a/homeassistant/components/home_plus_control/translations/et.json +++ b/homeassistant/components/home_plus_control/translations/et.json @@ -16,6 +16,5 @@ "title": "Vali tuvastusmeetod" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/fr.json b/homeassistant/components/home_plus_control/translations/fr.json index 4e250b743b7..0eaeafc19d4 100644 --- a/homeassistant/components/home_plus_control/translations/fr.json +++ b/homeassistant/components/home_plus_control/translations/fr.json @@ -16,6 +16,5 @@ "title": "S\u00e9lectionner une m\u00e9thode d'authentification" } } - }, - "title": "Legrand Home + Control" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/hu.json b/homeassistant/components/home_plus_control/translations/hu.json index 3dc4b451332..5e64b3e3880 100644 --- a/homeassistant/components/home_plus_control/translations/hu.json +++ b/homeassistant/components/home_plus_control/translations/hu.json @@ -16,6 +16,5 @@ "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" } } - }, - "title": "Legrand Home+ vez\u00e9rl\u00e9s" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/id.json b/homeassistant/components/home_plus_control/translations/id.json index 2ef7efe3d87..5d66b185d9a 100644 --- a/homeassistant/components/home_plus_control/translations/id.json +++ b/homeassistant/components/home_plus_control/translations/id.json @@ -16,6 +16,5 @@ "title": "Pilih Metode Autentikasi" } } - }, - "title": "Legrand Home+ Control" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/it.json b/homeassistant/components/home_plus_control/translations/it.json index e79d22275f7..67c3ea2b92a 100644 --- a/homeassistant/components/home_plus_control/translations/it.json +++ b/homeassistant/components/home_plus_control/translations/it.json @@ -16,6 +16,5 @@ "title": "Scegli il metodo di autenticazione" } } - }, - "title": "Legrand Home + Control" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/ja.json b/homeassistant/components/home_plus_control/translations/ja.json index df5165fc3fa..3cef1cd5fa1 100644 --- a/homeassistant/components/home_plus_control/translations/ja.json +++ b/homeassistant/components/home_plus_control/translations/ja.json @@ -16,6 +16,5 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" } } - }, - "title": "Legrand Home+ Control" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/ko.json b/homeassistant/components/home_plus_control/translations/ko.json index 94c8bb91648..44667e6a324 100644 --- a/homeassistant/components/home_plus_control/translations/ko.json +++ b/homeassistant/components/home_plus_control/translations/ko.json @@ -16,6 +16,5 @@ "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" } } - }, - "title": "Legrand Home+ Control" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/nl.json b/homeassistant/components/home_plus_control/translations/nl.json index 8f6df2fdad0..ffebcf58d60 100644 --- a/homeassistant/components/home_plus_control/translations/nl.json +++ b/homeassistant/components/home_plus_control/translations/nl.json @@ -16,6 +16,5 @@ "title": "Kies een authenticatie methode" } } - }, - "title": "Legrand Home+ Control" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/no.json b/homeassistant/components/home_plus_control/translations/no.json index 363f5cf54f7..314cfe84c28 100644 --- a/homeassistant/components/home_plus_control/translations/no.json +++ b/homeassistant/components/home_plus_control/translations/no.json @@ -16,6 +16,5 @@ "title": "Velg godkjenningsmetode" } } - }, - "title": "Legrand Home + Control" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/pl.json b/homeassistant/components/home_plus_control/translations/pl.json index b684c874a7d..27448ab2398 100644 --- a/homeassistant/components/home_plus_control/translations/pl.json +++ b/homeassistant/components/home_plus_control/translations/pl.json @@ -16,6 +16,5 @@ "title": "Wybierz metod\u0119 uwierzytelniania" } } - }, - "title": "Legrand Home+ Control" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/pt-BR.json b/homeassistant/components/home_plus_control/translations/pt-BR.json index 3b7340be7c7..12ca9127cff 100644 --- a/homeassistant/components/home_plus_control/translations/pt-BR.json +++ b/homeassistant/components/home_plus_control/translations/pt-BR.json @@ -16,6 +16,5 @@ "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" } } - }, - "title": "Legrand Home+ Control" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/ru.json b/homeassistant/components/home_plus_control/translations/ru.json index c968cc261e2..b67c2eb9ce7 100644 --- a/homeassistant/components/home_plus_control/translations/ru.json +++ b/homeassistant/components/home_plus_control/translations/ru.json @@ -16,6 +16,5 @@ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" } } - }, - "title": "Legrand Home+ Control" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/tr.json b/homeassistant/components/home_plus_control/translations/tr.json index 0138716d548..105fe1ebbed 100644 --- a/homeassistant/components/home_plus_control/translations/tr.json +++ b/homeassistant/components/home_plus_control/translations/tr.json @@ -16,6 +16,5 @@ "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" } } - }, - "title": "Legrand Home+ Kontrol" + } } \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/zh-Hant.json b/homeassistant/components/home_plus_control/translations/zh-Hant.json index da55be65f04..572edf837d1 100644 --- a/homeassistant/components/home_plus_control/translations/zh-Hant.json +++ b/homeassistant/components/home_plus_control/translations/zh-Hant.json @@ -16,6 +16,5 @@ "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" } } - }, - "title": "Legrand Home+ Control" + } } \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/ca.json b/homeassistant/components/homekit/translations/ca.json index f1036cceb25..16843fa1741 100644 --- a/homeassistant/components/homekit/translations/ca.json +++ b/homeassistant/components/homekit/translations/ca.json @@ -27,7 +27,6 @@ }, "advanced": { "data": { - "auto_start": "Inici autom\u00e0tic (desactiva-ho si crides el servei homekit.start manualment)", "devices": "Dispositius (disparadors)" }, "description": "Els interruptors programables es creen per cada dispositiu seleccionat. HomeKit pot ser programat per a que executi una automatitzaci\u00f3 o escena quan un dispositiu es dispari.", diff --git a/homeassistant/components/homekit/translations/de.json b/homeassistant/components/homekit/translations/de.json index d9c07e4cf21..d145d826337 100644 --- a/homeassistant/components/homekit/translations/de.json +++ b/homeassistant/components/homekit/translations/de.json @@ -27,7 +27,6 @@ }, "advanced": { "data": { - "auto_start": "Autostart (deaktivieren, wenn du den homekit.start-Dienst manuell aufrufst)", "devices": "Ger\u00e4te (Trigger)" }, "description": "F\u00fcr jedes ausgew\u00e4hlte Ger\u00e4t werden programmierbare Schalter erstellt. Wenn ein Ger\u00e4teausl\u00f6ser ausgel\u00f6st wird, kann HomeKit so konfiguriert werden, dass eine Automatisierung oder Szene ausgef\u00fchrt wird.", diff --git a/homeassistant/components/homekit/translations/el.json b/homeassistant/components/homekit/translations/el.json index 370617bd5fd..1031fcca85d 100644 --- a/homeassistant/components/homekit/translations/el.json +++ b/homeassistant/components/homekit/translations/el.json @@ -27,7 +27,6 @@ }, "advanced": { "data": { - "auto_start": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7 (\u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03bd \u03ba\u03b1\u03bb\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 homekit.start \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1)", "devices": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 (\u0395\u03bd\u03b1\u03cd\u03c3\u03bc\u03b1\u03c4\u03b1)" }, "description": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf\u03b9 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \u038c\u03c4\u03b1\u03bd \u03c0\u03c5\u03c1\u03bf\u03b4\u03bf\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2, \u03c4\u03bf HomeKit \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03ba\u03c4\u03ad\u03bb\u03b5\u03c3\u03b7 \u03b5\u03bd\u03cc\u03c2 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd \u03ae \u03bc\u03b9\u03b1\u03c2 \u03c3\u03ba\u03b7\u03bd\u03ae\u03c2.", diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json index 14585b60f1e..f0c26868e7d 100644 --- a/homeassistant/components/homekit/translations/en.json +++ b/homeassistant/components/homekit/translations/en.json @@ -27,7 +27,6 @@ }, "advanced": { "data": { - "auto_start": "Autostart (disable if you are calling the homekit.start service manually)", "devices": "Devices (Triggers)" }, "description": "Programmable switches are created for each selected device. When a device trigger fires, HomeKit can be configured to run an automation or scene.", diff --git a/homeassistant/components/homekit/translations/es-419.json b/homeassistant/components/homekit/translations/es-419.json index 459832cac80..55cbafcb647 100644 --- a/homeassistant/components/homekit/translations/es-419.json +++ b/homeassistant/components/homekit/translations/es-419.json @@ -20,9 +20,6 @@ "options": { "step": { "advanced": { - "data": { - "auto_start": "Inicio autom\u00e1tico (deshabilitar si se usa Z-Wave u otro sistema de inicio diferido)" - }, "description": "Esta configuraci\u00f3n solo necesita ser ajustada si el puente HomeKit no es funcional.", "title": "Configuraci\u00f3n avanzada" }, diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index c673a4e9749..9aa71faef12 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -26,7 +26,6 @@ }, "advanced": { "data": { - "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.", diff --git a/homeassistant/components/homekit/translations/et.json b/homeassistant/components/homekit/translations/et.json index 671dede46e0..350812600e7 100644 --- a/homeassistant/components/homekit/translations/et.json +++ b/homeassistant/components/homekit/translations/et.json @@ -27,7 +27,6 @@ }, "advanced": { "data": { - "auto_start": "Autostart (keela kui kasutad homekit.start teenust k\u00e4sitsi)", "devices": "Seadmed (p\u00e4\u00e4stikud)" }, "description": "Iga valitud seadme jaoks luuakse programmeeritavad l\u00fclitid. Seadme p\u00e4\u00e4stiku k\u00e4ivitamisel saab HomeKiti seadistada automaatiseeringu v\u00f5i stseeni k\u00e4ivitamiseks.", diff --git a/homeassistant/components/homekit/translations/fr.json b/homeassistant/components/homekit/translations/fr.json index e4850893bd3..57294ad4315 100644 --- a/homeassistant/components/homekit/translations/fr.json +++ b/homeassistant/components/homekit/translations/fr.json @@ -27,7 +27,6 @@ }, "advanced": { "data": { - "auto_start": "D\u00e9marrage automatique (d\u00e9sactiver si vous utilisez Z-Wave ou un autre syst\u00e8me de d\u00e9marrage diff\u00e9r\u00e9)", "devices": "Appareils (d\u00e9clencheurs)" }, "description": "Ces param\u00e8tres ne doivent \u00eatre ajust\u00e9s que si le pont HomeKit n'est pas fonctionnel.", diff --git a/homeassistant/components/homekit/translations/hu.json b/homeassistant/components/homekit/translations/hu.json index a1c64f6fac3..eefbf2ad381 100644 --- a/homeassistant/components/homekit/translations/hu.json +++ b/homeassistant/components/homekit/translations/hu.json @@ -27,7 +27,6 @@ }, "advanced": { "data": { - "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": "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.", diff --git a/homeassistant/components/homekit/translations/id.json b/homeassistant/components/homekit/translations/id.json index 8293ad9d72c..e3889ce031f 100644 --- a/homeassistant/components/homekit/translations/id.json +++ b/homeassistant/components/homekit/translations/id.json @@ -27,7 +27,6 @@ }, "advanced": { "data": { - "auto_start": "Mulai otomatis (nonaktifkan jika Anda memanggil layanan homekit.start secara manual)", "devices": "Perangkat (Pemicu)" }, "description": "Sakelar yang dapat diprogram dibuat untuk setiap perangkat yang dipilih. Saat pemicu perangkat aktif, HomeKit dapat dikonfigurasi untuk menjalankan otomatisasi atau skenario.", diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index 7acec1af5c3..4086db071c2 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -27,7 +27,6 @@ }, "advanced": { "data": { - "auto_start": "Avvio automatico (disabilita se stai chiamando manualmente il servizio homekit.start)", "devices": "Dispositivi (Attivatori)" }, "description": "Gli interruttori programmabili vengono creati per ogni dispositivo selezionato. Quando si attiva un trigger del dispositivo, HomeKit pu\u00f2 essere configurato per eseguire un'automazione o una scena.", diff --git a/homeassistant/components/homekit/translations/ja.json b/homeassistant/components/homekit/translations/ja.json index 6bcad37ad61..a2364e44c78 100644 --- a/homeassistant/components/homekit/translations/ja.json +++ b/homeassistant/components/homekit/translations/ja.json @@ -27,7 +27,6 @@ }, "advanced": { "data": { - "auto_start": "\u81ea\u52d5\u8d77\u52d5(homekit.start\u30b5\u30fc\u30d3\u30b9\u3092\u624b\u52d5\u3067\u547c\u3073\u51fa\u3059\u5834\u5408\u306f\u7121\u52b9\u306b\u3059\u308b)", "devices": "\u30c7\u30d0\u30a4\u30b9(\u30c8\u30ea\u30ac\u30fc)" }, "description": "\u9078\u629e\u3057\u305f\u30c7\u30d0\u30a4\u30b9\u3054\u3068\u306b\u3001\u30d7\u30ed\u30b0\u30e9\u30e0\u53ef\u80fd\u306a\u30b9\u30a4\u30c3\u30c1\u304c\u4f5c\u6210\u3055\u308c\u307e\u3059\u3002\u30c7\u30d0\u30a4\u30b9\u306e\u30c8\u30ea\u30ac\u30fc\u304c\u767a\u751f\u3059\u308b\u3068\u3001HomeKit\u306f\u30aa\u30fc\u30c8\u30e1\u30fc\u30b7\u30e7\u30f3\u3084\u30b7\u30fc\u30f3\u3092\u5b9f\u884c\u3059\u308b\u3088\u3046\u306b\u69cb\u6210\u3067\u304d\u307e\u3059\u3002", diff --git a/homeassistant/components/homekit/translations/ko.json b/homeassistant/components/homekit/translations/ko.json index cd8cbb81943..7db067c5803 100644 --- a/homeassistant/components/homekit/translations/ko.json +++ b/homeassistant/components/homekit/translations/ko.json @@ -20,9 +20,6 @@ "options": { "step": { "advanced": { - "data": { - "auto_start": "\uc790\ub3d9 \uc2dc\uc791 (homekit.start \uc11c\ube44\uc2a4\ub97c \uc218\ub3d9\uc73c\ub85c \ud638\ucd9c\ud558\ub824\uba74 \ube44\ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694)" - }, "description": "\uc774 \uc124\uc815\uc740 HomeKit\uac00 \uc791\ub3d9\ud558\uc9c0 \uc54a\ub294 \uacbd\uc6b0\uc5d0\ub9cc \uc124\uc815\ud574\uc8fc\uc138\uc694.", "title": "\uace0\uae09 \uad6c\uc131" }, diff --git a/homeassistant/components/homekit/translations/lb.json b/homeassistant/components/homekit/translations/lb.json index 409d860940d..367472630bc 100644 --- a/homeassistant/components/homekit/translations/lb.json +++ b/homeassistant/components/homekit/translations/lb.json @@ -20,9 +20,6 @@ "options": { "step": { "advanced": { - "data": { - "auto_start": "Autostart (d\u00e9aktiv\u00e9ier falls Z-Wave oder een aanere verz\u00f6gerte Start System benotzt g\u00ebtt)" - }, "description": "D\u00ebs Astellungen brauche n\u00ebmmen ajust\u00e9iert ze ginn falls HomeKit net funktion\u00e9iert.", "title": "Erweidert Konfiguratioun" }, diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json index 7f8ab1e2ff8..6ad97c5ecf0 100644 --- a/homeassistant/components/homekit/translations/nl.json +++ b/homeassistant/components/homekit/translations/nl.json @@ -27,7 +27,6 @@ }, "advanced": { "data": { - "auto_start": "Autostart (deactiveer als je de homekit.start service handmatig aanroept)", "devices": "Apparaten (triggers)" }, "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.", diff --git a/homeassistant/components/homekit/translations/no.json b/homeassistant/components/homekit/translations/no.json index 29f775853fd..463b52bc27b 100644 --- a/homeassistant/components/homekit/translations/no.json +++ b/homeassistant/components/homekit/translations/no.json @@ -27,7 +27,6 @@ }, "advanced": { "data": { - "auto_start": "Autostart (deaktiver hvis du ringer til homekit.start-tjenesten manuelt)", "devices": "Enheter (utl\u00f8sere)" }, "description": "Programmerbare brytere opprettes for hver valgt enhet. N\u00e5r en enhetstrigger utl\u00f8ses, kan HomeKit konfigureres til \u00e5 kj\u00f8re en automatisering eller scene.", diff --git a/homeassistant/components/homekit/translations/pl.json b/homeassistant/components/homekit/translations/pl.json index 73ed271d636..9080a03ecc0 100644 --- a/homeassistant/components/homekit/translations/pl.json +++ b/homeassistant/components/homekit/translations/pl.json @@ -27,7 +27,6 @@ }, "advanced": { "data": { - "auto_start": "Automatyczne uruchomienie (wy\u0142\u0105cz, je\u015bli r\u0119cznie uruchamiasz us\u0142ug\u0119 homekit.start)", "devices": "Urz\u0105dzenia (Wyzwalacze)" }, "description": "Dla ka\u017cdego wybranego urz\u0105dzenia stworzony zostanie programowalny prze\u0142\u0105cznik. Po uruchomieniu wyzwalacza urz\u0105dzenia, HomeKit mo\u017cna skonfigurowa\u0107 do uruchamiania automatyzacji lub sceny.", diff --git a/homeassistant/components/homekit/translations/pt-BR.json b/homeassistant/components/homekit/translations/pt-BR.json index 5fcc2748d0c..5296facd831 100644 --- a/homeassistant/components/homekit/translations/pt-BR.json +++ b/homeassistant/components/homekit/translations/pt-BR.json @@ -27,7 +27,6 @@ }, "advanced": { "data": { - "auto_start": "Autostart (desabilite se voc\u00ea estiver chamando o servi\u00e7o homekit.start manualmente)", "devices": "Dispositivos (gatilhos)" }, "description": "Os interruptores program\u00e1veis s\u00e3o criados para cada dispositivo selecionado. Quando um dispositivo dispara, o HomeKit pode ser configurado para executar uma automa\u00e7\u00e3o ou cena.", diff --git a/homeassistant/components/homekit/translations/pt.json b/homeassistant/components/homekit/translations/pt.json index 3df84b70866..f122a97b19c 100644 --- a/homeassistant/components/homekit/translations/pt.json +++ b/homeassistant/components/homekit/translations/pt.json @@ -15,9 +15,6 @@ "options": { "step": { "advanced": { - "data": { - "auto_start": "[%key:component::homekit::config::step::user::data::auto_start%]" - }, "title": "Configura\u00e7\u00e3o avan\u00e7ada" }, "cameras": { diff --git a/homeassistant/components/homekit/translations/ru.json b/homeassistant/components/homekit/translations/ru.json index 81dd9afa302..1f10b632fba 100644 --- a/homeassistant/components/homekit/translations/ru.json +++ b/homeassistant/components/homekit/translations/ru.json @@ -27,7 +27,6 @@ }, "advanced": { "data": { - "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u0435, \u0435\u0441\u043b\u0438 \u0412\u044b \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0435 \u0441\u043b\u0443\u0436\u0431\u0443 homekit.start)", "devices": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u0442\u0440\u0438\u0433\u0433\u0435\u0440\u044b)" }, "description": "\u041f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u044b\u0435 \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u0438 \u0441\u043e\u0437\u0434\u0430\u044e\u0442\u0441\u044f \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. HomeKit \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0434\u043b\u044f \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438\u043b\u0438 \u0441\u0446\u0435\u043d\u044b, \u043a\u043e\u0433\u0434\u0430 \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442 \u0442\u0440\u0438\u0433\u0433\u0435\u0440 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", diff --git a/homeassistant/components/homekit/translations/sl.json b/homeassistant/components/homekit/translations/sl.json index 333ec699253..0428bebc85f 100644 --- a/homeassistant/components/homekit/translations/sl.json +++ b/homeassistant/components/homekit/translations/sl.json @@ -20,9 +20,6 @@ "options": { "step": { "advanced": { - "data": { - "auto_start": "Samodejni zagon (onemogo\u010dite, \u010de uporabljate Z-wave ali kakteri drug sistem z zakasnjenim zagonom)" - }, "description": "Te nastavitve je treba prilagoditi le, \u010de most HomeKit ni funkcionalen.", "title": "Napredna konfiguracija" }, diff --git a/homeassistant/components/homekit/translations/tr.json b/homeassistant/components/homekit/translations/tr.json index 52683302a34..f6ac036ff84 100644 --- a/homeassistant/components/homekit/translations/tr.json +++ b/homeassistant/components/homekit/translations/tr.json @@ -27,7 +27,6 @@ }, "advanced": { "data": { - "auto_start": "Otomatik ba\u015flatma (homekit.start hizmetini manuel olarak ar\u0131yorsan\u0131z devre d\u0131\u015f\u0131 b\u0131rak\u0131n)", "devices": "Cihazlar (Tetikleyiciler)" }, "description": "Se\u00e7ilen her cihaz i\u00e7in programlanabilir anahtarlar olu\u015fturulur. Bir cihaz tetikleyicisi tetiklendi\u011finde, HomeKit bir otomasyon veya sahne \u00e7al\u0131\u015ft\u0131racak \u015fekilde yap\u0131land\u0131r\u0131labilir.", diff --git a/homeassistant/components/homekit/translations/uk.json b/homeassistant/components/homekit/translations/uk.json index 52da83cca73..43fe9b25a9d 100644 --- a/homeassistant/components/homekit/translations/uk.json +++ b/homeassistant/components/homekit/translations/uk.json @@ -20,9 +20,6 @@ "options": { "step": { "advanced": { - "data": { - "auto_start": "[%key:component::homekit::config::step::user::data::auto_start%]" - }, "description": "\u0426\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0456, \u043b\u0438\u0448\u0435 \u044f\u043a\u0449\u043e HomeKit \u043d\u0435 \u043f\u0440\u0430\u0446\u044e\u0454.", "title": "\u0420\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f" }, diff --git a/homeassistant/components/homekit/translations/zh-Hans.json b/homeassistant/components/homekit/translations/zh-Hans.json index 852979fb12a..4415fcdcd38 100644 --- a/homeassistant/components/homekit/translations/zh-Hans.json +++ b/homeassistant/components/homekit/translations/zh-Hans.json @@ -27,7 +27,6 @@ }, "advanced": { "data": { - "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": "\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", diff --git a/homeassistant/components/homekit/translations/zh-Hant.json b/homeassistant/components/homekit/translations/zh-Hant.json index 3ad28a22f45..54c0bb29450 100644 --- a/homeassistant/components/homekit/translations/zh-Hant.json +++ b/homeassistant/components/homekit/translations/zh-Hant.json @@ -27,7 +27,6 @@ }, "advanced": { "data": { - "auto_start": "\u81ea\u52d5\u555f\u52d5\uff08\u5047\u5982\u624b\u52d5\u4f7f\u7528 homekit.start \u670d\u52d9\u6642\u3001\u8acb\u95dc\u9589\uff09", "devices": "\u88dd\u7f6e\uff08\u89f8\u767c\u5668\uff09" }, "description": "\u70ba\u6240\u9078\u64c7\u7684\u88dd\u7f6e\u65b0\u589e\u53ef\u7a0b\u5f0f\u958b\u95dc\u3002\u7576\u88dd\u7f6e\u89f8\u767c\u5668\u89f8\u767c\u6642\u3001Homekit \u53ef\u8a2d\u5b9a\u70ba\u57f7\u884c\u81ea\u52d5\u5316\u6216\u5834\u666f\u3002", diff --git a/homeassistant/components/huawei_lte/translations/bg.json b/homeassistant/components/huawei_lte/translations/bg.json index 741b8ec7d47..7e67477f000 100644 --- a/homeassistant/components/huawei_lte/translations/bg.json +++ b/homeassistant/components/huawei_lte/translations/bg.json @@ -1,8 +1,6 @@ { "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_in_progress": "\u0422\u043e\u0432\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0435\u0447\u0435 \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430", "not_huawei_lte": "\u041d\u0435 \u0435 Huawei LTE \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, "error": { @@ -30,8 +28,7 @@ "step": { "init": { "data": { - "recipient": "\u041f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u0438 \u043d\u0430 SMS \u0438\u0437\u0432\u0435\u0441\u0442\u0438\u044f", - "track_new_devices": "\u041f\u0440\u043e\u0441\u043b\u0435\u0434\u044f\u0432\u0430\u043d\u0435 \u043d\u0430 \u043d\u043e\u0432\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + "recipient": "\u041f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u0438 \u043d\u0430 SMS \u0438\u0437\u0432\u0435\u0441\u0442\u0438\u044f" } } } diff --git a/homeassistant/components/huawei_lte/translations/ca.json b/homeassistant/components/huawei_lte/translations/ca.json index 0347398ce8b..903ba233407 100644 --- a/homeassistant/components/huawei_lte/translations/ca.json +++ b/homeassistant/components/huawei_lte/translations/ca.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat", - "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "not_huawei_lte": "No \u00e9s un dispositiu Huawei LTE" }, "error": { @@ -34,7 +32,6 @@ "data": { "name": "Nom del servei de notificacions (reinici necessari si canvia)", "recipient": "Destinataris de notificacions SMS", - "track_new_devices": "Segueix dispositius nous", "track_wired_clients": "Segueix els clients connectats a la xarxa per cable", "unauthenticated_mode": "Mode no autenticat (canviar requereix tornar a carregar)" } diff --git a/homeassistant/components/huawei_lte/translations/cs.json b/homeassistant/components/huawei_lte/translations/cs.json index 298cf182b08..7782b2fc622 100644 --- a/homeassistant/components/huawei_lte/translations/cs.json +++ b/homeassistant/components/huawei_lte/translations/cs.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", - "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", "not_huawei_lte": "Nejedn\u00e1 se o za\u0159\u00edzen\u00ed Huawei LTE" }, "error": { @@ -33,8 +31,7 @@ "init": { "data": { "name": "Jm\u00e9no slu\u017eby ozn\u00e1men\u00ed (zm\u011bna vy\u017eaduje restart)", - "recipient": "P\u0159\u00edjemci ozn\u00e1men\u00ed SMS", - "track_new_devices": "Sledovat nov\u00e1 za\u0159\u00edzen\u00ed" + "recipient": "P\u0159\u00edjemci ozn\u00e1men\u00ed SMS" } } } diff --git a/homeassistant/components/huawei_lte/translations/da.json b/homeassistant/components/huawei_lte/translations/da.json index 52765ad704a..2b1f937b6be 100644 --- a/homeassistant/components/huawei_lte/translations/da.json +++ b/homeassistant/components/huawei_lte/translations/da.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "Denne enhed er allerede konfigureret", - "already_in_progress": "Denne enhed er allerede ved at blive konfigureret", "not_huawei_lte": "Ikke en Huawei LTE-enhed" }, "error": { @@ -29,8 +27,7 @@ "init": { "data": { "name": "Navn p\u00e5 meddelelsestjeneste (\u00e6ndring kr\u00e6ver genstart)", - "recipient": "Modtagere af SMS-meddelelse", - "track_new_devices": "Spor nye enheder" + "recipient": "Modtagere af SMS-meddelelse" } } } diff --git a/homeassistant/components/huawei_lte/translations/de.json b/homeassistant/components/huawei_lte/translations/de.json index a3b40d0c0ae..50e7b7a2e53 100644 --- a/homeassistant/components/huawei_lte/translations/de.json +++ b/homeassistant/components/huawei_lte/translations/de.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "not_huawei_lte": "Kein Huawei LTE-Ger\u00e4t" }, "error": { @@ -34,7 +32,6 @@ "data": { "name": "Name des Benachrichtigungsdienstes (\u00c4nderung erfordert Neustart)", "recipient": "SMS-Benachrichtigungsempf\u00e4nger", - "track_new_devices": "Neue Ger\u00e4te verfolgen", "track_wired_clients": "Kabelgebundene Netzwerk-Clients verfolgen", "unauthenticated_mode": "Nicht authentifizierter Modus (\u00c4nderung erfordert erneutes Laden)" } diff --git a/homeassistant/components/huawei_lte/translations/el.json b/homeassistant/components/huawei_lte/translations/el.json index 01526400165..8b6def091c9 100644 --- a/homeassistant/components/huawei_lte/translations/el.json +++ b/homeassistant/components/huawei_lte/translations/el.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", - "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", "not_huawei_lte": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Huawei LTE" }, "error": { @@ -34,7 +32,6 @@ "data": { "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1\u03c2 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 (\u03b7 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03af\u03bd\u03b7\u03c3\u03b7)", "recipient": "\u03a0\u03b1\u03c1\u03b1\u03bb\u03ae\u03c0\u03c4\u03b5\u03c2 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c9\u03bd SMS", - "track_new_devices": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03bd\u03ad\u03c9\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd", "track_wired_clients": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03c0\u03b5\u03bb\u03b1\u03c4\u03ce\u03bd \u03b5\u03bd\u03c3\u03cd\u03c1\u03bc\u03b1\u03c4\u03bf\u03c5 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5", "unauthenticated_mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c7\u03c9\u03c1\u03af\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 (\u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7)" } diff --git a/homeassistant/components/huawei_lte/translations/en.json b/homeassistant/components/huawei_lte/translations/en.json index ade7beed75c..5636d952b19 100644 --- a/homeassistant/components/huawei_lte/translations/en.json +++ b/homeassistant/components/huawei_lte/translations/en.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "Device is already configured", - "already_in_progress": "Configuration flow is already in progress", "not_huawei_lte": "Not a Huawei LTE device" }, "error": { @@ -34,7 +32,6 @@ "data": { "name": "Notification service name (change requires restart)", "recipient": "SMS notification recipients", - "track_new_devices": "Track new devices", "track_wired_clients": "Track wired network clients", "unauthenticated_mode": "Unauthenticated mode (change requires reload)" } diff --git a/homeassistant/components/huawei_lte/translations/es-419.json b/homeassistant/components/huawei_lte/translations/es-419.json index 29e7658b8e0..056b8dba886 100644 --- a/homeassistant/components/huawei_lte/translations/es-419.json +++ b/homeassistant/components/huawei_lte/translations/es-419.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "Este dispositivo ya ha sido configurado", - "already_in_progress": "Este dispositivo ya est\u00e1 siendo configurado", "not_huawei_lte": "No es un dispositivo Huawei LTE" }, "error": { @@ -29,8 +27,7 @@ "init": { "data": { "name": "Nombre del servicio de notificaci\u00f3n (el cambio requiere reiniciar)", - "recipient": "Destinatarios de notificaciones por SMS", - "track_new_devices": "Rastrear nuevos dispositivos" + "recipient": "Destinatarios de notificaciones por SMS" } } } diff --git a/homeassistant/components/huawei_lte/translations/es.json b/homeassistant/components/huawei_lte/translations/es.json index 09a63bb1f62..485d2e16f69 100644 --- a/homeassistant/components/huawei_lte/translations/es.json +++ b/homeassistant/components/huawei_lte/translations/es.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "Este dispositivo ya ha sido configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "not_huawei_lte": "No es un dispositivo Huawei LTE" }, "error": { @@ -34,7 +32,6 @@ "data": { "name": "Nombre del servicio de notificaci\u00f3n (el cambio requiere reiniciar)", "recipient": "Destinatarios de notificaciones por SMS", - "track_new_devices": "Rastrea nuevos dispositivos", "track_wired_clients": "Seguir clientes de red cableados", "unauthenticated_mode": "Modo no autenticado (el cambio requiere recarga)" } diff --git a/homeassistant/components/huawei_lte/translations/et.json b/homeassistant/components/huawei_lte/translations/et.json index 955d5cc2c5a..08fcca84b23 100644 --- a/homeassistant/components/huawei_lte/translations/et.json +++ b/homeassistant/components/huawei_lte/translations/et.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "Seade on juba seadistatud", - "already_in_progress": "Seadistamine on juba k\u00e4imas", "not_huawei_lte": "Pole Huawei LTE seade" }, "error": { @@ -34,7 +32,6 @@ "data": { "name": "Teavitusteenuse nimi (muudatus n\u00f5uab taask\u00e4ivitamist)", "recipient": "SMS teavituse saajad", - "track_new_devices": "Uute seadmete j\u00e4lgimine", "track_wired_clients": "J\u00e4lgi juhtmega v\u00f5rgukliente", "unauthenticated_mode": "Tuvastuseta re\u017eiim (muutmine n\u00f5uab taaslaadimist)" } diff --git a/homeassistant/components/huawei_lte/translations/fr.json b/homeassistant/components/huawei_lte/translations/fr.json index 1f5042d3aa1..117514985c9 100644 --- a/homeassistant/components/huawei_lte/translations/fr.json +++ b/homeassistant/components/huawei_lte/translations/fr.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "not_huawei_lte": "Pas un appareil Huawei LTE" }, "error": { @@ -34,7 +32,6 @@ "data": { "name": "Nom du service de notification (red\u00e9marrage requis)", "recipient": "Destinataires des notifications SMS", - "track_new_devices": "Suivre les nouveaux appareils", "track_wired_clients": "Suivre les clients du r\u00e9seau filaire", "unauthenticated_mode": "Mode non authentifi\u00e9 (le changement n\u00e9cessite un rechargement)" } diff --git a/homeassistant/components/huawei_lte/translations/he.json b/homeassistant/components/huawei_lte/translations/he.json index 1e4333a989a..daad7429a83 100644 --- a/homeassistant/components/huawei_lte/translations/he.json +++ b/homeassistant/components/huawei_lte/translations/he.json @@ -1,9 +1,5 @@ { "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": { "incorrect_password": "\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05d2\u05d5\u05d9\u05d4", "incorrect_username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05e9\u05d2\u05d5\u05d9", diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json index b3cc71d5a9d..c01a5cb4bb8 100644 --- a/homeassistant/components/huawei_lte/translations/hu.json +++ b/homeassistant/components/huawei_lte/translations/hu.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "not_huawei_lte": "Nem Huawei LTE eszk\u00f6z" }, "error": { @@ -34,7 +32,6 @@ "data": { "name": "\u00c9rtes\u00edt\u00e9si szolg\u00e1ltat\u00e1s neve (a m\u00f3dos\u00edt\u00e1s \u00fajraind\u00edt\u00e1st ig\u00e9nyel)", "recipient": "SMS-\u00e9rtes\u00edt\u00e9s c\u00edmzettjei", - "track_new_devices": "\u00daj eszk\u00f6z\u00f6k nyomk\u00f6vet\u00e9se", "track_wired_clients": "Vezet\u00e9kes h\u00e1l\u00f3zati \u00fcgyfelek nyomon k\u00f6vet\u00e9se", "unauthenticated_mode": "Nem hiteles\u00edtett m\u00f3d (a v\u00e1ltoztat\u00e1shoz \u00fajrat\u00f6lt\u00e9sre van sz\u00fcks\u00e9g)" } diff --git a/homeassistant/components/huawei_lte/translations/id.json b/homeassistant/components/huawei_lte/translations/id.json index fa586718cac..ac925d0b460 100644 --- a/homeassistant/components/huawei_lte/translations/id.json +++ b/homeassistant/components/huawei_lte/translations/id.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi", - "already_in_progress": "Alur konfigurasi sedang berlangsung", "not_huawei_lte": "Bukan perangkat Huawei LTE" }, "error": { @@ -34,7 +32,6 @@ "data": { "name": "Nama layanan notifikasi (perubahan harus dimulai ulang)", "recipient": "Penerima notifikasi SMS", - "track_new_devices": "Lacak perangkat baru", "track_wired_clients": "Lacak klien jaringan kabel", "unauthenticated_mode": "Mode tidak diautentikasi (perubahan memerlukan pemuatan ulang)" } diff --git a/homeassistant/components/huawei_lte/translations/it.json b/homeassistant/components/huawei_lte/translations/it.json index ad8d4a82b08..6a90b67d307 100644 --- a/homeassistant/components/huawei_lte/translations/it.json +++ b/homeassistant/components/huawei_lte/translations/it.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "not_huawei_lte": "Non \u00e8 un dispositivo Huawei LTE" }, "error": { @@ -34,7 +32,6 @@ "data": { "name": "Nome del servizio di notifica (la modifica richiede il riavvio)", "recipient": "Destinatari della notifica SMS", - "track_new_devices": "Traccia nuovi dispositivi", "track_wired_clients": "Tieni traccia dei client di rete cablata", "unauthenticated_mode": "Modalit\u00e0 non autenticata (la modifica richiede il ricaricamento)" } diff --git a/homeassistant/components/huawei_lte/translations/ja.json b/homeassistant/components/huawei_lte/translations/ja.json index 6c74f5a7918..c3b41fbbcfc 100644 --- a/homeassistant/components/huawei_lte/translations/ja.json +++ b/homeassistant/components/huawei_lte/translations/ja.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "not_huawei_lte": "Huawei LTE\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093" }, "error": { @@ -34,7 +32,6 @@ "data": { "name": "\u901a\u77e5\u30b5\u30fc\u30d3\u30b9\u540d(\u5909\u66f4\u306b\u306f\u518d\u8d77\u52d5\u304c\u5fc5\u8981)", "recipient": "SMS\u901a\u77e5\u306e\u53d7\u4fe1\u8005", - "track_new_devices": "\u65b0\u3057\u3044\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u8de1", "track_wired_clients": "\u6709\u7dda\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u3092\u8ffd\u8de1\u3059\u308b", "unauthenticated_mode": "\u8a8d\u8a3c\u306a\u3057\u306e\u30e2\u30fc\u30c9(\u5909\u66f4\u306b\u306f\u30ea\u30ed\u30fc\u30c9\u304c\u5fc5\u8981)" } diff --git a/homeassistant/components/huawei_lte/translations/ko.json b/homeassistant/components/huawei_lte/translations/ko.json index ab108964ed8..930431d6c87 100644 --- a/homeassistant/components/huawei_lte/translations/ko.json +++ b/homeassistant/components/huawei_lte/translations/ko.json @@ -1,8 +1,6 @@ { "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", "not_huawei_lte": "\ud654\uc6e8\uc774 LTE \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4" }, "error": { @@ -33,8 +31,7 @@ "init": { "data": { "name": "\uc54c\ub9bc \uc11c\ube44\uc2a4 \uc774\ub984 (\ubcc0\uacbd \uc2dc \ub2e4\uc2dc \uc2dc\uc791\ud574\uc57c \ud568)", - "recipient": "SMS \uc54c\ub9bc \uc218\uc2e0\uc790", - "track_new_devices": "\uc0c8\ub85c\uc6b4 \uae30\uae30 \ucd94\uc801" + "recipient": "SMS \uc54c\ub9bc \uc218\uc2e0\uc790" } } } diff --git a/homeassistant/components/huawei_lte/translations/lb.json b/homeassistant/components/huawei_lte/translations/lb.json index d6983bed8fc..2be64393358 100644 --- a/homeassistant/components/huawei_lte/translations/lb.json +++ b/homeassistant/components/huawei_lte/translations/lb.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "Apparat ass scho konfigur\u00e9iert", - "already_in_progress": "Konfiguratioun's Oflas ass schon am gaang", "not_huawei_lte": "Keen Huawei LTE Apparat" }, "error": { @@ -33,8 +31,7 @@ "init": { "data": { "name": "Numm vum Notifikatioun's Service (Restart n\u00e9ideg bei \u00c4nnerung)", - "recipient": "Empf\u00e4nger vun SMS Notifikatioune", - "track_new_devices": "Nei Apparater verfollegen" + "recipient": "Empf\u00e4nger vun SMS Notifikatioune" } } } diff --git a/homeassistant/components/huawei_lte/translations/nl.json b/homeassistant/components/huawei_lte/translations/nl.json index e65c261d62b..3e5148170a7 100644 --- a/homeassistant/components/huawei_lte/translations/nl.json +++ b/homeassistant/components/huawei_lte/translations/nl.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", "not_huawei_lte": "Geen Huawei LTE-apparaat" }, "error": { @@ -34,7 +32,6 @@ "data": { "name": "Naam meldingsservice (wijziging vereist opnieuw opstarten)", "recipient": "Ontvangers van sms-berichten", - "track_new_devices": "Volg nieuwe apparaten", "track_wired_clients": "Volg bekabelde netwerkclients", "unauthenticated_mode": "Niet-geverifieerde modus (wijzigen vereist opnieuw laden)" } diff --git a/homeassistant/components/huawei_lte/translations/no.json b/homeassistant/components/huawei_lte/translations/no.json index 3c8b26ab0cd..d1cd33ce2b0 100644 --- a/homeassistant/components/huawei_lte/translations/no.json +++ b/homeassistant/components/huawei_lte/translations/no.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert", - "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "not_huawei_lte": "Ikke en Huawei LTE-enhet" }, "error": { @@ -34,7 +32,6 @@ "data": { "name": "Navn p\u00e5 varslingstjeneste (endring krever omstart)", "recipient": "Mottakere av SMS-varsling", - "track_new_devices": "Spor nye enheter", "track_wired_clients": "Spor kablede nettverksklienter", "unauthenticated_mode": "Uautentisert modus (endring krever omlasting)" } diff --git a/homeassistant/components/huawei_lte/translations/pl.json b/homeassistant/components/huawei_lte/translations/pl.json index 38ee2117b9d..76e7c92a010 100644 --- a/homeassistant/components/huawei_lte/translations/pl.json +++ b/homeassistant/components/huawei_lte/translations/pl.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "already_in_progress": "Konfiguracja jest ju\u017c w toku", "not_huawei_lte": "To nie jest urz\u0105dzenie Huawei LTE" }, "error": { @@ -34,7 +32,6 @@ "data": { "name": "Nazwa us\u0142ugi powiadomie\u0144 (zmiana wymaga ponownego uruchomienia)", "recipient": "Odbiorcy powiadomie\u0144 SMS", - "track_new_devices": "\u015aled\u017a nowe urz\u0105dzenia", "track_wired_clients": "\u015aled\u017a klient\u00f3w sieci przewodowej", "unauthenticated_mode": "Tryb nieuwierzytelniony (zmiana wymaga prze\u0142adowania)" } diff --git a/homeassistant/components/huawei_lte/translations/pt-BR.json b/homeassistant/components/huawei_lte/translations/pt-BR.json index 7b69fce212d..a92c2de3f10 100644 --- a/homeassistant/components/huawei_lte/translations/pt-BR.json +++ b/homeassistant/components/huawei_lte/translations/pt-BR.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "not_huawei_lte": "N\u00e3o \u00e9 um dispositivo Huawei LTE" }, "error": { @@ -34,7 +32,6 @@ "data": { "name": "Nome do servi\u00e7o de notifica\u00e7\u00e3o (a altera\u00e7\u00e3o requer rein\u00edcio)", "recipient": "Destinat\u00e1rios de notifica\u00e7\u00e3o por SMS", - "track_new_devices": "Rastrear novos dispositivos", "track_wired_clients": "Rastrear clientes da rede cabeada", "unauthenticated_mode": "Modo n\u00e3o autenticado (a altera\u00e7\u00e3o requer recarga)" } diff --git a/homeassistant/components/huawei_lte/translations/pt.json b/homeassistant/components/huawei_lte/translations/pt.json index 34d057a9ab0..be10fe829f7 100644 --- a/homeassistant/components/huawei_lte/translations/pt.json +++ b/homeassistant/components/huawei_lte/translations/pt.json @@ -1,9 +1,5 @@ { "config": { - "abort": { - "already_configured": "Este dispositivo j\u00e1 foi configurado", - "already_in_progress": "Este dispositivo j\u00e1 est\u00e1 a ser configurado" - }, "error": { "connection_timeout": "Liga\u00e7\u00e3o expirou", "incorrect_password": "Palavra-passe incorreta", @@ -27,8 +23,7 @@ "step": { "init": { "data": { - "recipient": "Destinat\u00e1rios de notifica\u00e7\u00e3o por SMS", - "track_new_devices": "Seguir novos dispositivos" + "recipient": "Destinat\u00e1rios de notifica\u00e7\u00e3o por SMS" } } } diff --git a/homeassistant/components/huawei_lte/translations/ru.json b/homeassistant/components/huawei_lte/translations/ru.json index e9ddd73191d..6c27673b556 100644 --- a/homeassistant/components/huawei_lte/translations/ru.json +++ b/homeassistant/components/huawei_lte/translations/ru.json @@ -1,8 +1,6 @@ { "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.", "not_huawei_lte": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Huawei LTE" }, "error": { @@ -34,7 +32,6 @@ "data": { "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0441\u043b\u0443\u0436\u0431\u044b \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439 (\u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a)", "recipient": "\u041f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u0438 SMS-\u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439", - "track_new_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", "track_wired_clients": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0432\u043e\u0434\u043d\u043e\u0439 \u0441\u0435\u0442\u0438", "unauthenticated_mode": "\u0420\u0435\u0436\u0438\u043c \u0431\u0435\u0437 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 (\u0434\u043b\u044f \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0430)" } diff --git a/homeassistant/components/huawei_lte/translations/sk.json b/homeassistant/components/huawei_lte/translations/sk.json index 0b7bf878ea9..5ada995aa6e 100644 --- a/homeassistant/components/huawei_lte/translations/sk.json +++ b/homeassistant/components/huawei_lte/translations/sk.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" - }, "error": { "invalid_auth": "Neplatn\u00e9 overenie" } diff --git a/homeassistant/components/huawei_lte/translations/sl.json b/homeassistant/components/huawei_lte/translations/sl.json index 07a9b269986..0297db82c7b 100644 --- a/homeassistant/components/huawei_lte/translations/sl.json +++ b/homeassistant/components/huawei_lte/translations/sl.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "Ta naprava je \u017ee konfigurirana", - "already_in_progress": "Ta naprava se \u017ee nastavlja", "not_huawei_lte": "Ni naprava Huawei LTE" }, "error": { @@ -29,8 +27,7 @@ "init": { "data": { "name": "Ime storitve obve\u0161\u010danja (sprememba zahteva ponovni zagon)", - "recipient": "Prejemniki obvestil SMS", - "track_new_devices": "Sledi novim napravam" + "recipient": "Prejemniki obvestil SMS" } } } diff --git a/homeassistant/components/huawei_lte/translations/sv.json b/homeassistant/components/huawei_lte/translations/sv.json index 3f478c3f826..83cbe335b2d 100644 --- a/homeassistant/components/huawei_lte/translations/sv.json +++ b/homeassistant/components/huawei_lte/translations/sv.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "Den h\u00e4r enheten har redan konfigurerats", - "already_in_progress": "Den h\u00e4r enheten har redan konfigurerats", "not_huawei_lte": "Inte en Huawei LTE-enhet" }, "error": { @@ -29,8 +27,7 @@ "init": { "data": { "name": "Namn p\u00e5 meddelandetj\u00e4nsten (\u00e4ndring kr\u00e4ver omstart)", - "recipient": "Mottagare av SMS-meddelanden", - "track_new_devices": "Sp\u00e5ra nya enheter" + "recipient": "Mottagare av SMS-meddelanden" } } } diff --git a/homeassistant/components/huawei_lte/translations/tr.json b/homeassistant/components/huawei_lte/translations/tr.json index 92d40ca810b..6d231efa8ed 100644 --- a/homeassistant/components/huawei_lte/translations/tr.json +++ b/homeassistant/components/huawei_lte/translations/tr.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "not_huawei_lte": "Huawei LTE cihaz\u0131 de\u011fil" }, "error": { @@ -34,7 +32,6 @@ "data": { "name": "Bildirim hizmeti ad\u0131 (de\u011fi\u015fiklik yeniden ba\u015flatmay\u0131 gerektirir)", "recipient": "SMS bildirimi al\u0131c\u0131lar\u0131", - "track_new_devices": "Yeni cihazlar\u0131 izle", "track_wired_clients": "Kablolu a\u011f istemcilerini izleyin", "unauthenticated_mode": "Kimli\u011fi do\u011frulanmam\u0131\u015f mod (de\u011fi\u015fiklik yeniden y\u00fckleme gerektirir)" } diff --git a/homeassistant/components/huawei_lte/translations/uk.json b/homeassistant/components/huawei_lte/translations/uk.json index 17f3d3b71c3..ae98ce59332 100644 --- a/homeassistant/components/huawei_lte/translations/uk.json +++ b/homeassistant/components/huawei_lte/translations/uk.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", - "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", "not_huawei_lte": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0454 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c Huawei LTE" }, "error": { @@ -33,8 +31,7 @@ "init": { "data": { "name": "\u041d\u0430\u0437\u0432\u0430 \u0441\u043b\u0443\u0436\u0431\u0438 \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u044c (\u043f\u043e\u0442\u0440\u0456\u0431\u0435\u043d \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a)", - "recipient": "\u041e\u0434\u0435\u0440\u0436\u0443\u0432\u0430\u0447\u0456 SMS-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c", - "track_new_devices": "\u0412\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0432\u0430\u0442\u0438 \u043d\u043e\u0432\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457" + "recipient": "\u041e\u0434\u0435\u0440\u0436\u0443\u0432\u0430\u0447\u0456 SMS-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c" } } } diff --git a/homeassistant/components/huawei_lte/translations/zh-Hans.json b/homeassistant/components/huawei_lte/translations/zh-Hans.json index a63ff964b62..c04409b2783 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hans.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hans.json @@ -1,8 +1,6 @@ { "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": { @@ -32,7 +30,6 @@ "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" } diff --git a/homeassistant/components/huawei_lte/translations/zh-Hant.json b/homeassistant/components/huawei_lte/translations/zh-Hant.json index 0c1d2baa7a9..85a2d84f6de 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hant.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hant.json @@ -1,8 +1,6 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "not_huawei_lte": "\u4e26\u975e\u83ef\u70ba LTE \u88dd\u7f6e" }, "error": { @@ -34,7 +32,6 @@ "data": { "name": "\u901a\u77e5\u670d\u52d9\u540d\u7a31\uff08\u8b8a\u66f4\u5f8c\u9700\u91cd\u555f\uff09", "recipient": "\u7c21\u8a0a\u901a\u77e5\u6536\u4ef6\u8005", - "track_new_devices": "\u8ffd\u8e64\u65b0\u88dd\u7f6e", "track_wired_clients": "\u8ffd\u8e64\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef", "unauthenticated_mode": "\u672a\u6388\u6b0a\u6a21\u5f0f\uff08\u8b8a\u66f4\u5f8c\u9700\u91cd\u555f\uff09" } diff --git a/homeassistant/components/humidifier/translations/ca.json b/homeassistant/components/humidifier/translations/ca.json index ebfda0d3722..cd980a39591 100644 --- a/homeassistant/components/humidifier/translations/ca.json +++ b/homeassistant/components/humidifier/translations/ca.json @@ -15,7 +15,6 @@ "trigger_type": { "changed_states": "{entity_name} s'enc\u00e9n o s'apaga", "target_humidity_changed": "Ha canviat la humitat desitjada de {entity_name}", - "toggled": "{entity_name} s'activa o es desactiva", "turned_off": "{entity_name} s'ha apagat", "turned_on": "{entity_name} s'ha engegat" } diff --git a/homeassistant/components/humidifier/translations/cs.json b/homeassistant/components/humidifier/translations/cs.json index d47de36b72f..2e373587afc 100644 --- a/homeassistant/components/humidifier/translations/cs.json +++ b/homeassistant/components/humidifier/translations/cs.json @@ -14,7 +14,6 @@ }, "trigger_type": { "target_humidity_changed": "C\u00edlov\u00e1 vlhkost {entity_name} zm\u011bn\u011bna", - "toggled": "{entity_name} zapnuto nebo vypnuto", "turned_off": "{entity_name} vypnuto", "turned_on": "{entity_name} zapnuto" } diff --git a/homeassistant/components/humidifier/translations/de.json b/homeassistant/components/humidifier/translations/de.json index f55117ab05f..16ad95f9980 100644 --- a/homeassistant/components/humidifier/translations/de.json +++ b/homeassistant/components/humidifier/translations/de.json @@ -15,7 +15,6 @@ "trigger_type": { "changed_states": "{entity_name} ein- oder ausgeschaltet", "target_humidity_changed": "{entity_name} Soll-Luftfeuchtigkeit ge\u00e4ndert", - "toggled": "{entity_name} ein- oder ausgeschaltet", "turned_off": "{entity_name} ausgeschaltet", "turned_on": "{entity_name} eingeschaltet" } diff --git a/homeassistant/components/humidifier/translations/el.json b/homeassistant/components/humidifier/translations/el.json index 85efa779f01..d52b5c0ba43 100644 --- a/homeassistant/components/humidifier/translations/el.json +++ b/homeassistant/components/humidifier/translations/el.json @@ -15,7 +15,6 @@ "trigger_type": { "changed_states": "\u03a4\u03bf {entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5 \u03ae \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", "target_humidity_changed": "\u0397 \u03c3\u03c4\u03bf\u03c7\u03b5\u03c5\u03bc\u03ad\u03bd\u03b7 \u03c5\u03b3\u03c1\u03b1\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 {entity_name} \u03ac\u03bb\u03bb\u03b1\u03be\u03b5", - "toggled": "{entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5 \u03ae \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", "turned_off": "{entity_name} \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", "turned_on": "{entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5" } diff --git a/homeassistant/components/humidifier/translations/en.json b/homeassistant/components/humidifier/translations/en.json index 6c62ec89842..a2fc80fe7da 100644 --- a/homeassistant/components/humidifier/translations/en.json +++ b/homeassistant/components/humidifier/translations/en.json @@ -15,7 +15,6 @@ "trigger_type": { "changed_states": "{entity_name} turned on or off", "target_humidity_changed": "{entity_name} target humidity changed", - "toggled": "{entity_name} turned on or off", "turned_off": "{entity_name} turned off", "turned_on": "{entity_name} turned on" } diff --git a/homeassistant/components/humidifier/translations/es.json b/homeassistant/components/humidifier/translations/es.json index e9c8bb02df9..8506445b25c 100644 --- a/homeassistant/components/humidifier/translations/es.json +++ b/homeassistant/components/humidifier/translations/es.json @@ -15,7 +15,6 @@ "trigger_type": { "changed_states": "{entity_name} activado o desactivado", "target_humidity_changed": "La humedad objetivo ha cambiado en {entity_name}", - "toggled": "{entity_name} activado o desactivado", "turned_off": "{entity_name} desactivado", "turned_on": "{entity_name} activado" } diff --git a/homeassistant/components/humidifier/translations/et.json b/homeassistant/components/humidifier/translations/et.json index dcf1f8488a1..7a4ad019e2a 100644 --- a/homeassistant/components/humidifier/translations/et.json +++ b/homeassistant/components/humidifier/translations/et.json @@ -15,7 +15,6 @@ "trigger_type": { "changed_states": "{entity_name} l\u00fclitus sisse v\u00f5i v\u00e4lja", "target_humidity_changed": "{entity_name} eelseatud niiskus muutus", - "toggled": "{entity_name} l\u00fclitus sisse v\u00f5i v\u00e4lja", "turned_off": "{entity_name} l\u00fclitus v\u00e4lja", "turned_on": "{entity_name} l\u00fclitus sisse" } diff --git a/homeassistant/components/humidifier/translations/fr.json b/homeassistant/components/humidifier/translations/fr.json index 4caf57d81b5..3229b595881 100644 --- a/homeassistant/components/humidifier/translations/fr.json +++ b/homeassistant/components/humidifier/translations/fr.json @@ -15,7 +15,6 @@ "trigger_type": { "changed_states": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", "target_humidity_changed": "{nom_de_l'entit\u00e9} changement de l'humidit\u00e9 cible", - "toggled": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", "turned_off": "{entity_name} s'est \u00e9teint", "turned_on": "{entity_name} s'est allum\u00e9" } diff --git a/homeassistant/components/humidifier/translations/he.json b/homeassistant/components/humidifier/translations/he.json index 152de4c49ca..5a4c58c7934 100644 --- a/homeassistant/components/humidifier/translations/he.json +++ b/homeassistant/components/humidifier/translations/he.json @@ -13,7 +13,6 @@ }, "trigger_type": { "changed_states": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc \u05d0\u05d5 \u05db\u05d5\u05d1\u05d4", - "toggled": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc \u05d0\u05d5 \u05db\u05d5\u05d1\u05d4", "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4", "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc" } diff --git a/homeassistant/components/humidifier/translations/hu.json b/homeassistant/components/humidifier/translations/hu.json index 7979334429d..6572f0e4955 100644 --- a/homeassistant/components/humidifier/translations/hu.json +++ b/homeassistant/components/humidifier/translations/hu.json @@ -15,7 +15,6 @@ "trigger_type": { "changed_states": "{entity_name} be- vagy kikapcsolt", "target_humidity_changed": "{name} k\u00edv\u00e1nt p\u00e1ratartalom megv\u00e1ltozott", - "toggled": "{entity_name} \u00e1tkapcsolt", "turned_off": "{entity_name} ki lett kapcsolva", "turned_on": "{entity_name} be lett kapcsolva" } diff --git a/homeassistant/components/humidifier/translations/id.json b/homeassistant/components/humidifier/translations/id.json index 1996fd23786..8fd2ccaa9b0 100644 --- a/homeassistant/components/humidifier/translations/id.json +++ b/homeassistant/components/humidifier/translations/id.json @@ -15,7 +15,6 @@ "trigger_type": { "changed_states": "{entity_name} diaktifkan atau dinonaktifkan", "target_humidity_changed": "Kelembapan target {entity_name} berubah", - "toggled": "{entity_name} diaktifkan atau dinonaktifkan", "turned_off": "{entity_name} dimatikan", "turned_on": "{entity_name} dinyalakan" } diff --git a/homeassistant/components/humidifier/translations/it.json b/homeassistant/components/humidifier/translations/it.json index cc0220eca17..7a887c7f0c7 100644 --- a/homeassistant/components/humidifier/translations/it.json +++ b/homeassistant/components/humidifier/translations/it.json @@ -15,7 +15,6 @@ "trigger_type": { "changed_states": "{entity_name} attivata o disattivata", "target_humidity_changed": "{entity_name} umidit\u00e0 target modificata", - "toggled": "{entity_name} attiva o disattiva", "turned_off": "{entity_name} disattivato", "turned_on": "{entity_name} attivato" } diff --git a/homeassistant/components/humidifier/translations/ja.json b/homeassistant/components/humidifier/translations/ja.json index 713049acb52..5c51b58cbfa 100644 --- a/homeassistant/components/humidifier/translations/ja.json +++ b/homeassistant/components/humidifier/translations/ja.json @@ -15,7 +15,6 @@ "trigger_type": { "changed_states": "{entity_name} \u304c\u30aa\u30f3\u307e\u305f\u306f\u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", "target_humidity_changed": "{entity_name} \u30bf\u30fc\u30b2\u30c3\u30c8\u6e7f\u5ea6\u304c\u5909\u5316\u3057\u307e\u3057\u305f", - "toggled": "{entity_name} \u304c\u30aa\u30f3\u307e\u305f\u306f\u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059" } diff --git a/homeassistant/components/humidifier/translations/nl.json b/homeassistant/components/humidifier/translations/nl.json index 8d96d698209..1cdf417e6ef 100644 --- a/homeassistant/components/humidifier/translations/nl.json +++ b/homeassistant/components/humidifier/translations/nl.json @@ -15,7 +15,6 @@ "trigger_type": { "changed_states": "{entity_name} in- of uitgeschakeld", "target_humidity_changed": "{entity_name} doel luchtvochtigheid gewijzigd", - "toggled": "{entity_name} in- of uitgeschakeld", "turned_off": "{entity_name} is uitgeschakeld", "turned_on": "{entity_name} is ingeschakeld" } diff --git a/homeassistant/components/humidifier/translations/no.json b/homeassistant/components/humidifier/translations/no.json index dbbe95493f0..276577b81c3 100644 --- a/homeassistant/components/humidifier/translations/no.json +++ b/homeassistant/components/humidifier/translations/no.json @@ -15,7 +15,6 @@ "trigger_type": { "changed_states": "{entity_name} sl\u00e5tt p\u00e5 eller av", "target_humidity_changed": "{entity_name} m\u00e5let fuktighet endret", - "toggled": "{entity_name} sl\u00e5tt p\u00e5 eller av", "turned_off": "{entity_name} sl\u00e5tt av", "turned_on": "{entity_name} sl\u00e5tt p\u00e5" } diff --git a/homeassistant/components/humidifier/translations/pl.json b/homeassistant/components/humidifier/translations/pl.json index 5c82a1e944b..d31db39da8c 100644 --- a/homeassistant/components/humidifier/translations/pl.json +++ b/homeassistant/components/humidifier/translations/pl.json @@ -15,7 +15,6 @@ "trigger_type": { "changed_states": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", "target_humidity_changed": "zmieni si\u0119 wilgotno\u015b\u0107 docelowa {entity_name}", - "toggled": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}" } diff --git a/homeassistant/components/humidifier/translations/pt-BR.json b/homeassistant/components/humidifier/translations/pt-BR.json index bb4b6c00177..20b4e663f72 100644 --- a/homeassistant/components/humidifier/translations/pt-BR.json +++ b/homeassistant/components/humidifier/translations/pt-BR.json @@ -15,7 +15,6 @@ "trigger_type": { "changed_states": "{entity_name} for ligado ou desligado", "target_humidity_changed": "{entity_name} tiver a umidade alvo alterada", - "toggled": "{entity_name} for ligado ou desligado", "turned_off": "{entity_name} for desligado", "turned_on": "{entity_name} for ligado" } diff --git a/homeassistant/components/humidifier/translations/ru.json b/homeassistant/components/humidifier/translations/ru.json index 5cbaf6e8e54..fe8026c68b6 100644 --- a/homeassistant/components/humidifier/translations/ru.json +++ b/homeassistant/components/humidifier/translations/ru.json @@ -15,7 +15,6 @@ "trigger_type": { "changed_states": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0438\u043b\u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "target_humidity_changed": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0446\u0435\u043b\u0435\u0432\u043e\u0439 \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442\u0438", - "toggled": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0438\u043b\u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" } diff --git a/homeassistant/components/humidifier/translations/sv.json b/homeassistant/components/humidifier/translations/sv.json index 489d8a095a3..325e9f2e6a0 100644 --- a/homeassistant/components/humidifier/translations/sv.json +++ b/homeassistant/components/humidifier/translations/sv.json @@ -1,7 +1,6 @@ { "device_automation": { "trigger_type": { - "toggled": "{entity_name} slogs p\u00e5 eller av", "turned_off": "{entity_name} st\u00e4ngdes av" } } diff --git a/homeassistant/components/humidifier/translations/tr.json b/homeassistant/components/humidifier/translations/tr.json index 41b870d446a..2f2680dce9b 100644 --- a/homeassistant/components/humidifier/translations/tr.json +++ b/homeassistant/components/humidifier/translations/tr.json @@ -15,7 +15,6 @@ "trigger_type": { "changed_states": "{entity_name} a\u00e7\u0131ld\u0131 veya kapat\u0131ld\u0131", "target_humidity_changed": "{entity_name} hedef nem de\u011fi\u015fti", - "toggled": "{entity_name} a\u00e7\u0131ld\u0131 veya kapat\u0131ld\u0131", "turned_off": "{entity_name} kapat\u0131ld\u0131", "turned_on": "{entity_name} a\u00e7\u0131ld\u0131" } diff --git a/homeassistant/components/humidifier/translations/zh-Hans.json b/homeassistant/components/humidifier/translations/zh-Hans.json index 7a3185728fc..d21c7bf61f7 100644 --- a/homeassistant/components/humidifier/translations/zh-Hans.json +++ b/homeassistant/components/humidifier/translations/zh-Hans.json @@ -14,7 +14,6 @@ }, "trigger_type": { "target_humidity_changed": "{entity_name} \u7684\u8bbe\u5b9a\u6e7f\u5ea6\u53d8\u5316", - "toggled": "{entity_name} \u88ab\u6253\u5f00\u6216\u5173\u95ed", "turned_off": "{entity_name} \u88ab\u5173\u95ed", "turned_on": "{entity_name} \u88ab\u6253\u5f00" } diff --git a/homeassistant/components/humidifier/translations/zh-Hant.json b/homeassistant/components/humidifier/translations/zh-Hant.json index ed851c65795..eaeba2ab8a4 100644 --- a/homeassistant/components/humidifier/translations/zh-Hant.json +++ b/homeassistant/components/humidifier/translations/zh-Hant.json @@ -15,7 +15,6 @@ "trigger_type": { "changed_states": "{entity_name}\u5df2\u958b\u555f\u6216\u95dc\u9589", "target_humidity_changed": "{entity_name}\u8a2d\u5b9a\u6fd5\u5ea6\u5df2\u8b8a\u66f4", - "toggled": "{entity_name}\u5df2\u958b\u555f\u6216\u95dc\u9589", "turned_off": "{entity_name}\u5df2\u95dc\u9589", "turned_on": "{entity_name}\u5df2\u958b\u555f" } diff --git a/homeassistant/components/insteon/translations/ca.json b/homeassistant/components/insteon/translations/ca.json index 9805d03c685..f8e0e1c853e 100644 --- a/homeassistant/components/insteon/translations/ca.json +++ b/homeassistant/components/insteon/translations/ca.json @@ -43,8 +43,7 @@ "data": { "modem_type": "Tipus de m\u00f2dem." }, - "description": "Selecciona el tipus de m\u00f2dem Insteon.", - "title": "Insteon" + "description": "Selecciona el tipus de m\u00f2dem Insteon." } } }, @@ -61,8 +60,7 @@ "cat": "Categoria del dispositiu (ex: 0x10)", "subcat": "Subcategoria del dispositiu (ex: 0x0a)" }, - "description": "Afegeix una substituci\u00f3 de dispositiu.", - "title": "Insteon" + "description": "Afegeix una substituci\u00f3 de dispositiu." }, "add_x10": { "data": { @@ -71,8 +69,7 @@ "steps": "Passos del regulador d'intensitat (nom\u00e9s per a llums, 22 per defecte)", "unitcode": "Unitcode (1-16)" }, - "description": "Canvia la contrasenya de l'Insteon Hub.", - "title": "Insteon" + "description": "Canvia la contrasenya de l'Insteon Hub." }, "change_hub_config": { "data": { @@ -81,8 +78,7 @@ "port": "Port", "username": "Nom d'usuari" }, - "description": "Canvia la informaci\u00f3 de connexi\u00f3 de l'Insteon Hub. Has de reiniciar Home Assistant si fas canvis. Aix\u00f2 no canvia la configuraci\u00f3 del Hub en si. Per canviar la configuraci\u00f3 del Hub, utilitza la seva aplicaci\u00f3.", - "title": "Insteon" + "description": "Canvia la informaci\u00f3 de connexi\u00f3 de l'Insteon Hub. Has de reiniciar Home Assistant si fas canvis. Aix\u00f2 no canvia la configuraci\u00f3 del Hub en si. Per canviar la configuraci\u00f3 del Hub, utilitza la seva aplicaci\u00f3." }, "init": { "data": { @@ -91,23 +87,19 @@ "change_hub_config": "Canvia la configuraci\u00f3 del Hub.", "remove_override": "Elimina una substituci\u00f3 de dispositiu.", "remove_x10": "Elimina un dispositiu X10." - }, - "description": "Selecciona una opci\u00f3 a configurar.", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "Selecciona una adre\u00e7a de dispositiu a eliminar" }, - "description": "Elimina una substituci\u00f3 de dispositiu", - "title": "Insteon" + "description": "Elimina una substituci\u00f3 de dispositiu" }, "remove_x10": { "data": { "address": "Selecciona una adre\u00e7a de dispositiu a eliminar" }, - "description": "Elimina un dispositiu X10", - "title": "Insteon" + "description": "Elimina un dispositiu X10" } } } diff --git a/homeassistant/components/insteon/translations/cs.json b/homeassistant/components/insteon/translations/cs.json index 18d7ff1c999..e10e25e6361 100644 --- a/homeassistant/components/insteon/translations/cs.json +++ b/homeassistant/components/insteon/translations/cs.json @@ -37,8 +37,7 @@ "data": { "modem_type": "Typ modemu." }, - "description": "Vyberte typ modemu Insteon.", - "title": "Insteon" + "description": "Vyberte typ modemu Insteon." } } }, @@ -53,8 +52,7 @@ "data": { "address": "Adresa za\u0159\u00edzen\u00ed (tj. 1a2b3c)", "cat": "Kategorie za\u0159\u00edzen\u00ed (tj. 0x10)" - }, - "title": "Insteon" + } }, "add_x10": { "data": { @@ -62,8 +60,7 @@ "platform": "Platforma", "steps": "Kroky stm\u00edva\u010de (pouze pro sv\u011btla, v\u00fdchoz\u00ed 22)", "unitcode": "K\u00f3d jednotky (1-16)" - }, - "title": "Insteon" + } }, "change_hub_config": { "data": { @@ -72,29 +69,24 @@ "port": "Port", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" }, - "description": "Zm\u011b\u0148te informace o p\u0159ipojen\u00ed rozbo\u010dova\u010de Insteon. Po proveden\u00ed t\u00e9to zm\u011bny mus\u00edte Home Assistant restartovat. T\u00edm se nezm\u011bn\u00ed nastaven\u00ed samotn\u00e9ho rozbo\u010dova\u010de. Chcete-li zm\u011bnit nastaven\u00ed rozbo\u010dova\u010de, pou\u017eijte jeho aplikaci.", - "title": "Insteon" + "description": "Zm\u011b\u0148te informace o p\u0159ipojen\u00ed rozbo\u010dova\u010de Insteon. Po proveden\u00ed t\u00e9to zm\u011bny mus\u00edte Home Assistant restartovat. T\u00edm se nezm\u011bn\u00ed nastaven\u00ed samotn\u00e9ho rozbo\u010dova\u010de. Chcete-li zm\u011bnit nastaven\u00ed rozbo\u010dova\u010de, pou\u017eijte jeho aplikaci." }, "init": { "data": { "add_x10": "P\u0159idejte za\u0159\u00edzen\u00ed X10.", "remove_x10": "Odeberte za\u0159\u00edzen\u00ed X10." - }, - "description": "Vyberte mo\u017enost k nastaven\u00ed.", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "Vyberte adresu za\u0159\u00edzen\u00ed, kter\u00e9 chcete odebrat" - }, - "title": "Insteon" + } }, "remove_x10": { "data": { "address": "Vyberte adresu za\u0159\u00edzen\u00ed, kter\u00e9 chcete odebrat" }, - "description": "Odeberte za\u0159\u00edzen\u00ed X10", - "title": "Insteon" + "description": "Odeberte za\u0159\u00edzen\u00ed X10" } } } diff --git a/homeassistant/components/insteon/translations/de.json b/homeassistant/components/insteon/translations/de.json index 164fbccceed..d7f853d9a2a 100644 --- a/homeassistant/components/insteon/translations/de.json +++ b/homeassistant/components/insteon/translations/de.json @@ -43,8 +43,7 @@ "data": { "modem_type": "Modemtyp." }, - "description": "W\u00e4hle den Insteon-Modemtyp aus.", - "title": "Insteon" + "description": "W\u00e4hle den Insteon-Modemtyp aus." } } }, @@ -61,8 +60,7 @@ "cat": "Ger\u00e4tekategorie (z. B. 0x10)", "subcat": "Ger\u00e4teunterkategorie (z. B. 0x0a)" }, - "description": "F\u00fcge eine Ger\u00e4te\u00fcberschreibung hinzu.", - "title": "Insteon" + "description": "F\u00fcge eine Ger\u00e4te\u00fcberschreibung hinzu." }, "add_x10": { "data": { @@ -71,8 +69,7 @@ "steps": "Dimmerstufen (nur f\u00fcr Lichtger\u00e4te, Voreinstellung 22)", "unitcode": "Unitcode (1 - 16)" }, - "description": "\u00c4ndere das Insteon Hub-Passwort.", - "title": "Insteon" + "description": "\u00c4ndere das Insteon Hub-Passwort." }, "change_hub_config": { "data": { @@ -81,8 +78,7 @@ "port": "Port", "username": "Benutzername" }, - "description": "\u00c4ndere die Verbindungsinformationen des Insteon-Hubs. Du musst Home Assistant neu starten, nachdem du diese \u00c4nderung vorgenommen hast. Dies \u00e4ndert nicht die Konfiguration des Hubs selbst. Um die Konfiguration im Hub zu \u00e4ndern, verwende die Hub-App.", - "title": "Insteon" + "description": "\u00c4ndere die Verbindungsinformationen des Insteon-Hubs. Du musst Home Assistant neu starten, nachdem du diese \u00c4nderung vorgenommen hast. Dies \u00e4ndert nicht die Konfiguration des Hubs selbst. Um die Konfiguration im Hub zu \u00e4ndern, verwende die Hub-App." }, "init": { "data": { @@ -91,23 +87,19 @@ "change_hub_config": "\u00c4ndere die Konfiguration des Hubs.", "remove_override": "Entferne eine Ger\u00e4te\u00fcbersteuerung.", "remove_x10": "Entferne ein X10-Ger\u00e4t." - }, - "description": "W\u00e4hle eine Option zum Konfigurieren aus.", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "W\u00e4hle eine Ger\u00e4teadresse zum Entfernen" }, - "description": "Entfernen einer Ger\u00e4te\u00fcbersteuerung", - "title": "Insteon" + "description": "Entfernen einer Ger\u00e4te\u00fcbersteuerung" }, "remove_x10": { "data": { "address": "W\u00e4hle eine Ger\u00e4teadresse zum Entfernen" }, - "description": "Ein X10-Ger\u00e4t entfernen", - "title": "Insteon" + "description": "Ein X10-Ger\u00e4t entfernen" } } } diff --git a/homeassistant/components/insteon/translations/el.json b/homeassistant/components/insteon/translations/el.json index 4a4c402ddc0..9ff9e025f8f 100644 --- a/homeassistant/components/insteon/translations/el.json +++ b/homeassistant/components/insteon/translations/el.json @@ -43,8 +43,7 @@ "data": { "modem_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03bc\u03cc\u03bd\u03c4\u03b5\u03bc." }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03bc\u03cc\u03bd\u03c4\u03b5\u03bc Insteon.", - "title": "Insteon" + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03bc\u03cc\u03bd\u03c4\u03b5\u03bc Insteon." } } }, @@ -61,8 +60,7 @@ "cat": "\u039a\u03b1\u03c4\u03b7\u03b3\u03bf\u03c1\u03af\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 (\u03c0.\u03c7. 0x10)", "subcat": "\u03a5\u03c0\u03bf\u03ba\u03b1\u03c4\u03b7\u03b3\u03bf\u03c1\u03af\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 (\u03c0.\u03c7. 0x0a)" }, - "description": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2.", - "title": "Insteon" + "description": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2." }, "add_x10": { "data": { @@ -71,8 +69,7 @@ "steps": "\u0392\u03ae\u03bc\u03b1\u03c4\u03b1 \u03c1\u03bf\u03bf\u03c3\u03c4\u03ac\u03c4\u03b7 (\u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c6\u03c9\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd, \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae 22)", "unitcode": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1\u03c2 (1 - 16)" }, - "description": "\u0391\u03bb\u03bb\u03ac\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Insteon Hub.", - "title": "Insteon" + "description": "\u0391\u03bb\u03bb\u03ac\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Insteon Hub." }, "change_hub_config": { "data": { @@ -81,8 +78,7 @@ "port": "\u0398\u03cd\u03c1\u03b1", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, - "description": "\u0391\u03bb\u03bb\u03ac\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Insteon Hub. \u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03bc\u03b5\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b1\u03c5\u03c4\u03ae\u03c2 \u03c4\u03b7\u03c2 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae\u03c2. \u0391\u03c5\u03c4\u03cc \u03b4\u03b5\u03bd \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03af\u03b4\u03b9\u03bf\u03c5 \u03c4\u03bf\u03c5 Hub. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c3\u03c4\u03bf Hub \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Hub.", - "title": "Insteon" + "description": "\u0391\u03bb\u03bb\u03ac\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Insteon Hub. \u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03bc\u03b5\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b1\u03c5\u03c4\u03ae\u03c2 \u03c4\u03b7\u03c2 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae\u03c2. \u0391\u03c5\u03c4\u03cc \u03b4\u03b5\u03bd \u03b1\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03af\u03b4\u03b9\u03bf\u03c5 \u03c4\u03bf\u03c5 Hub. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c3\u03c4\u03bf Hub \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Hub." }, "init": { "data": { @@ -91,23 +87,19 @@ "change_hub_config": "\u0391\u03bb\u03bb\u03ac\u03be\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Hub.", "remove_override": "\u039a\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03bc\u03b9\u03b1\u03c2 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2.", "remove_x10": "\u0391\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae X10." - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7.", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7" }, - "description": "\u039a\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", - "title": "Insteon" + "description": "\u039a\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" }, "remove_x10": { "data": { "address": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7" }, - "description": "\u039a\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 X10", - "title": "Insteon" + "description": "\u039a\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 X10" } } } diff --git a/homeassistant/components/insteon/translations/en.json b/homeassistant/components/insteon/translations/en.json index f9e64dcf3cf..441f5cf576a 100644 --- a/homeassistant/components/insteon/translations/en.json +++ b/homeassistant/components/insteon/translations/en.json @@ -43,8 +43,7 @@ "data": { "modem_type": "Modem type." }, - "description": "Select the Insteon modem type.", - "title": "Insteon" + "description": "Select the Insteon modem type." } } }, @@ -61,8 +60,7 @@ "cat": "Device category (i.e. 0x10)", "subcat": "Device subcategory (i.e. 0x0a)" }, - "description": "Add a device override.", - "title": "Insteon" + "description": "Add a device override." }, "add_x10": { "data": { @@ -71,8 +69,7 @@ "steps": "Dimmer steps (for light devices only, default 22)", "unitcode": "Unitcode (1 - 16)" }, - "description": "Change the Insteon Hub password.", - "title": "Insteon" + "description": "Change the Insteon Hub password." }, "change_hub_config": { "data": { @@ -81,8 +78,7 @@ "port": "Port", "username": "Username" }, - "description": "Change the Insteon Hub connection information. You must restart Home Assistant after making this change. This does not change the configuration of the Hub itself. To change the configuration in the Hub use the Hub app.", - "title": "Insteon" + "description": "Change the Insteon Hub connection information. You must restart Home Assistant after making this change. This does not change the configuration of the Hub itself. To change the configuration in the Hub use the Hub app." }, "init": { "data": { @@ -91,23 +87,19 @@ "change_hub_config": "Change the Hub configuration.", "remove_override": "Remove a device override.", "remove_x10": "Remove an X10 device." - }, - "description": "Select an option to configure.", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "Select a device address to remove" }, - "description": "Remove a device override", - "title": "Insteon" + "description": "Remove a device override" }, "remove_x10": { "data": { "address": "Select a device address to remove" }, - "description": "Remove an X10 device", - "title": "Insteon" + "description": "Remove an X10 device" } } } diff --git a/homeassistant/components/insteon/translations/es.json b/homeassistant/components/insteon/translations/es.json index b7b72cefa28..5434bc77a8a 100644 --- a/homeassistant/components/insteon/translations/es.json +++ b/homeassistant/components/insteon/translations/es.json @@ -43,8 +43,7 @@ "data": { "modem_type": "Tipo de m\u00f3dem." }, - "description": "Seleccione el tipo de m\u00f3dem Insteon.", - "title": "Insteon" + "description": "Seleccione el tipo de m\u00f3dem Insteon." } } }, @@ -61,8 +60,7 @@ "cat": "Categor\u00eda del dispositivo (es decir, 0x10)", "subcat": "Subcategor\u00eda del dispositivo (es decir, 0x0a)" }, - "description": "Agregue una anulaci\u00f3n del dispositivo.", - "title": "Insteon" + "description": "Agregue una anulaci\u00f3n del dispositivo." }, "add_x10": { "data": { @@ -71,8 +69,7 @@ "steps": "Pasos de atenuaci\u00f3n (s\u00f3lo para dispositivos de luz, por defecto 22)", "unitcode": "Unitcode (1 - 16)" }, - "description": "Cambie la contrase\u00f1a del Hub Insteon.", - "title": "Insteon" + "description": "Cambie la contrase\u00f1a del Hub Insteon." }, "change_hub_config": { "data": { @@ -81,8 +78,7 @@ "port": "Puerto", "username": "Usuario" }, - "description": "Cambiar la informaci\u00f3n de la conexi\u00f3n del Hub Insteon. Debes reiniciar el Home Assistant despu\u00e9s de hacer este cambio. Esto no cambia la configuraci\u00f3n del Hub en s\u00ed. Para cambiar la configuraci\u00f3n del Hub usa la aplicaci\u00f3n Hub.", - "title": "Insteon" + "description": "Cambiar la informaci\u00f3n de la conexi\u00f3n del Hub Insteon. Debes reiniciar el Home Assistant despu\u00e9s de hacer este cambio. Esto no cambia la configuraci\u00f3n del Hub en s\u00ed. Para cambiar la configuraci\u00f3n del Hub usa la aplicaci\u00f3n Hub." }, "init": { "data": { @@ -91,23 +87,19 @@ "change_hub_config": "Cambie la configuraci\u00f3n del Hub.", "remove_override": "Eliminar una anulaci\u00f3n del dispositivo.", "remove_x10": "Eliminar un dispositivo X10" - }, - "description": "Seleccione una opci\u00f3n para configurar.", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "Seleccione una direcci\u00f3n del dispositivo para eliminar" }, - "description": "Eliminar una anulaci\u00f3n del dispositivo", - "title": "Insteon" + "description": "Eliminar una anulaci\u00f3n del dispositivo" }, "remove_x10": { "data": { "address": "Seleccione la direcci\u00f3n del dispositivo para eliminar" }, - "description": "Eliminar un dispositivo X10", - "title": "Insteon" + "description": "Eliminar un dispositivo X10" } } } diff --git a/homeassistant/components/insteon/translations/et.json b/homeassistant/components/insteon/translations/et.json index eff2fd9b9b2..72f2a9ac5c4 100644 --- a/homeassistant/components/insteon/translations/et.json +++ b/homeassistant/components/insteon/translations/et.json @@ -43,8 +43,7 @@ "data": { "modem_type": "Modemi mudel." }, - "description": "Vali Insteoni modemi mudel.", - "title": "" + "description": "Vali Insteoni modemi mudel." } } }, @@ -61,8 +60,7 @@ "cat": "Seadme kategooria (n\u00e4iteks 0x10)", "subcat": "Seadme alamkategooria (n\u00e4iteks 0x0a)" }, - "description": "Lisa seadme alistamine.", - "title": "" + "description": "Lisa seadme alistamine." }, "add_x10": { "data": { @@ -71,8 +69,7 @@ "steps": "H\u00e4mardamise samm (ainult valgustite jaoks, vaikimisi 22)", "unitcode": "" }, - "description": "Muuda Insteon Hubi parooli.", - "title": "" + "description": "Muuda Insteon Hubi parooli." }, "change_hub_config": { "data": { @@ -81,8 +78,7 @@ "port": "", "username": "Kasutajanimi" }, - "description": "Muutda Insteon Hubi \u00fchenduse teavet. P\u00e4rast selle muudatuse tegemist pead Home Assistanti taask\u00e4ivitama. See ei muuda jaoturi enda konfiguratsiooni. Hubis muudatuste tegemiseks kasuta rakendust Hub.", - "title": "" + "description": "Muutda Insteon Hubi \u00fchenduse teavet. P\u00e4rast selle muudatuse tegemist pead Home Assistanti taask\u00e4ivitama. See ei muuda jaoturi enda konfiguratsiooni. Hubis muudatuste tegemiseks kasuta rakendust Hub." }, "init": { "data": { @@ -91,23 +87,19 @@ "change_hub_config": "Muuda jaoturi konfiguratsiooni.", "remove_override": "Seadme alistamise eemaldamine.", "remove_x10": "Eemalda X10 seade." - }, - "description": "Vali seadistussuvand.", - "title": "" + } }, "remove_override": { "data": { "address": "Vali eemaldatava seadme aadress" }, - "description": "Seadme alistamise eemaldamine", - "title": "" + "description": "Seadme alistamise eemaldamine" }, "remove_x10": { "data": { "address": "Vali eemaldatava seadme aadress" }, - "description": "Eemalda X10 seade", - "title": "" + "description": "Eemalda X10 seade" } } } diff --git a/homeassistant/components/insteon/translations/fr.json b/homeassistant/components/insteon/translations/fr.json index 2bb0e4d2e33..e9570aaf7e2 100644 --- a/homeassistant/components/insteon/translations/fr.json +++ b/homeassistant/components/insteon/translations/fr.json @@ -43,8 +43,7 @@ "data": { "modem_type": "Type de modem." }, - "description": "S\u00e9lectionnez le type de modem Insteon.", - "title": "Insteon" + "description": "S\u00e9lectionnez le type de modem Insteon." } } }, @@ -61,8 +60,7 @@ "cat": "Cat\u00e9gorie d'appareil (c.-\u00e0-d. 0x10)", "subcat": "Sous-cat\u00e9gorie d'appareil (par exemple 0x0a)" }, - "description": "Ajouter un remplacement d'appareil.", - "title": "Insteon" + "description": "Ajouter un remplacement d'appareil." }, "add_x10": { "data": { @@ -71,8 +69,7 @@ "steps": "Pas de gradateur (pour les appareils d'\u00e9clairage uniquement, par d\u00e9faut 22)", "unitcode": "Code de l'unit\u00e9 (1-16)" }, - "description": "Modifiez le mot de passe Insteon Hub.", - "title": "Insteon" + "description": "Modifiez le mot de passe Insteon Hub." }, "change_hub_config": { "data": { @@ -81,8 +78,7 @@ "port": "Port", "username": "Nom d'utilisateur" }, - "description": "Modifiez les informations de connexion Insteon Hub. Vous devez red\u00e9marrer Home Assistant apr\u00e8s avoir effectu\u00e9 cette modification. Cela ne change pas la configuration du Hub lui-m\u00eame. Pour modifier la configuration dans le Hub, utilisez l'application Hub.", - "title": "Insteon" + "description": "Modifiez les informations de connexion Insteon Hub. Vous devez red\u00e9marrer Home Assistant apr\u00e8s avoir effectu\u00e9 cette modification. Cela ne change pas la configuration du Hub lui-m\u00eame. Pour modifier la configuration dans le Hub, utilisez l'application Hub." }, "init": { "data": { @@ -91,23 +87,19 @@ "change_hub_config": "Modifier la configuration du Hub.", "remove_override": "Supprimer un remplacement d'appareil", "remove_x10": "Retirer un appareil X10." - }, - "description": "S\u00e9lectionnez une option \u00e0 configurer.", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "S\u00e9lectionnez l'adresse d'un appareil \u00e0 retirer" }, - "description": "Supprimer un remplacement d'appareil", - "title": "Insteon" + "description": "Supprimer un remplacement d'appareil" }, "remove_x10": { "data": { "address": "S\u00e9lectionnez l'adresse d'un appareil \u00e0 retirer" }, - "description": "Retirer un appareil X10", - "title": "Insteon" + "description": "Retirer un appareil X10" } } } diff --git a/homeassistant/components/insteon/translations/hu.json b/homeassistant/components/insteon/translations/hu.json index 560c1fc118e..8ba8e3223fa 100644 --- a/homeassistant/components/insteon/translations/hu.json +++ b/homeassistant/components/insteon/translations/hu.json @@ -43,8 +43,7 @@ "data": { "modem_type": "Modem t\u00edpusa." }, - "description": "V\u00e1lassza ki az Insteon modem t\u00edpus\u00e1t.", - "title": "Insteon" + "description": "V\u00e1lassza ki az Insteon modem t\u00edpus\u00e1t." } } }, @@ -61,8 +60,7 @@ "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" + "description": "Eszk\u00f6z-fel\u00fclb\u00edr\u00e1l\u00e1s hozz\u00e1ad\u00e1sa." }, "add_x10": { "data": { @@ -71,8 +69,7 @@ "steps": "F\u00e9nyer\u0151-szab\u00e1lyoz\u00e1si l\u00e9p\u00e9sek (csak k\u00f6nny\u0171 eszk\u00f6z\u00f6k eset\u00e9n, alap\u00e9rtelmezett 22)", "unitcode": "Egys\u00e9gk\u00f3d (1 - 16)" }, - "description": "M\u00f3dos\u00edtsa az Insteon Hub jelszav\u00e1t.", - "title": "Insteon" + "description": "M\u00f3dos\u00edtsa az Insteon Hub jelszav\u00e1t." }, "change_hub_config": { "data": { @@ -81,8 +78,7 @@ "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "M\u00f3dos\u00edtsa az Insteon Hub csatlakoz\u00e1si adatait. A m\u00f3dos\u00edt\u00e1s elv\u00e9gz\u00e9se ut\u00e1n \u00fajra kell ind\u00edtania a Home Assistant alkalmaz\u00e1st. Ez nem v\u00e1ltoztatja meg a Hub konfigur\u00e1ci\u00f3j\u00e1t. A Hub konfigur\u00e1ci\u00f3j\u00e1nak m\u00f3dos\u00edt\u00e1s\u00e1hoz haszn\u00e1lja a Hub alkalmaz\u00e1st.", - "title": "Insteon" + "description": "M\u00f3dos\u00edtsa az Insteon Hub csatlakoz\u00e1si adatait. A m\u00f3dos\u00edt\u00e1s elv\u00e9gz\u00e9se ut\u00e1n \u00fajra kell ind\u00edtania a Home Assistant alkalmaz\u00e1st. Ez nem v\u00e1ltoztatja meg a Hub konfigur\u00e1ci\u00f3j\u00e1t. A Hub konfigur\u00e1ci\u00f3j\u00e1nak m\u00f3dos\u00edt\u00e1s\u00e1hoz haszn\u00e1lja a Hub alkalmaz\u00e1st." }, "init": { "data": { @@ -91,23 +87,19 @@ "change_hub_config": "M\u00f3dos\u00edtsa a Hub konfigur\u00e1ci\u00f3j\u00e1t.", "remove_override": "Egy eszk\u00f6z fel\u00fclb\u00edr\u00e1lat\u00e1nak elt\u00e1vol\u00edt\u00e1sa.", "remove_x10": "T\u00e1vol\u00edtson el egy X10 eszk\u00f6zt." - }, - "description": "V\u00e1lasszon egy be\u00e1ll\u00edt\u00e1st.", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "V\u00e1lassza ki az elt\u00e1vol\u00edtani k\u00edv\u00e1nt eszk\u00f6z c\u00edm\u00e9t" }, - "description": "T\u00e1vol\u00edtsa el az eszk\u00f6z fel\u00fclb\u00edr\u00e1l\u00e1s\u00e1t", - "title": "Insteon" + "description": "T\u00e1vol\u00edtsa el az eszk\u00f6z fel\u00fclb\u00edr\u00e1l\u00e1s\u00e1t" }, "remove_x10": { "data": { "address": "V\u00e1lassza ki az elt\u00e1vol\u00edtani k\u00edv\u00e1nt eszk\u00f6z c\u00edm\u00e9t" }, - "description": "T\u00e1vol\u00edtson el egy X10 eszk\u00f6zt", - "title": "Insteon" + "description": "T\u00e1vol\u00edtson el egy X10 eszk\u00f6zt" } } } diff --git a/homeassistant/components/insteon/translations/id.json b/homeassistant/components/insteon/translations/id.json index aae7e9f71ac..7520c6cd03d 100644 --- a/homeassistant/components/insteon/translations/id.json +++ b/homeassistant/components/insteon/translations/id.json @@ -43,8 +43,7 @@ "data": { "modem_type": "Jenis modem." }, - "description": "Pilih jenis modem Insteon.", - "title": "Insteon" + "description": "Pilih jenis modem Insteon." } } }, @@ -61,8 +60,7 @@ "cat": "Kategori perangkat (mis. 0x10)", "subcat": "Subkategori perangkat (mis. 0x0a)" }, - "description": "Tambahkan penimpaan nilai perangkat.", - "title": "Insteon" + "description": "Tambahkan penimpaan nilai perangkat." }, "add_x10": { "data": { @@ -71,8 +69,7 @@ "steps": "Langkah peredup (hanya untuk perangkat ringan, nilai bawaan adalah 22)", "unitcode": "Unitcode (1 - 16)" }, - "description": "Ubah kata sandi Insteon Hub.", - "title": "Insteon" + "description": "Ubah kata sandi Insteon Hub." }, "change_hub_config": { "data": { @@ -81,8 +78,7 @@ "port": "Port", "username": "Nama Pengguna" }, - "description": "Ubah informasi koneksi Insteon Hub. Anda harus memulai ulang Home Assistant setelah melakukan perubahan ini. Ini tidak mengubah konfigurasi Hub itu sendiri. Untuk mengubah konfigurasi di Hub, gunakan aplikasi Hub.", - "title": "Insteon" + "description": "Ubah informasi koneksi Insteon Hub. Anda harus memulai ulang Home Assistant setelah melakukan perubahan ini. Ini tidak mengubah konfigurasi Hub itu sendiri. Untuk mengubah konfigurasi di Hub, gunakan aplikasi Hub." }, "init": { "data": { @@ -91,23 +87,19 @@ "change_hub_config": "Ubah konfigurasi Hub.", "remove_override": "Hapus penimpaan nilai perangkat.", "remove_x10": "Hapus perangkat X10." - }, - "description": "Pilih opsi untuk dikonfigurasi.", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "Pilih alamat perangkat untuk dihapus" }, - "description": "Hapus penimpaan nilai perangkat", - "title": "Insteon" + "description": "Hapus penimpaan nilai perangkat" }, "remove_x10": { "data": { "address": "Pilih alamat perangkat untuk dihapus" }, - "description": "Hapus perangkat X10", - "title": "Insteon" + "description": "Hapus perangkat X10" } } } diff --git a/homeassistant/components/insteon/translations/it.json b/homeassistant/components/insteon/translations/it.json index b47a06c9d7a..c5bd0f1af87 100644 --- a/homeassistant/components/insteon/translations/it.json +++ b/homeassistant/components/insteon/translations/it.json @@ -43,8 +43,7 @@ "data": { "modem_type": "Tipo di modem." }, - "description": "Seleziona il tipo di modem Insteon.", - "title": "Insteon" + "description": "Seleziona il tipo di modem Insteon." } } }, @@ -61,8 +60,7 @@ "cat": "Categoria del dispositivo (ad es. 0x10)", "subcat": "Sottocategoria del dispositivo (ad es. 0x0a)" }, - "description": "Aggiungi una sostituzione del dispositivo.", - "title": "Insteon" + "description": "Aggiungi una sostituzione del dispositivo." }, "add_x10": { "data": { @@ -71,8 +69,7 @@ "steps": "Livelli del dimmer (solo per dispositivi luminosi, predefiniti 22)", "unitcode": "Codice unit\u00e0 (1 - 16)" }, - "description": "Cambia la password di Insteon Hub.", - "title": "Insteon" + "description": "Cambia la password di Insteon Hub." }, "change_hub_config": { "data": { @@ -81,8 +78,7 @@ "port": "Porta", "username": "Nome utente" }, - "description": "Modifica le informazioni di connessione di Insteon Hub. \u00c8 necessario riavviare Home Assistant dopo aver apportato questa modifica. Ci\u00f2 non modifica la configurazione dell'Hub stesso. Per modificare la configurazione nell'Hub, utilizza l'app Hub.", - "title": "Insteon" + "description": "Modifica le informazioni di connessione di Insteon Hub. \u00c8 necessario riavviare Home Assistant dopo aver apportato questa modifica. Ci\u00f2 non modifica la configurazione dell'Hub stesso. Per modificare la configurazione nell'Hub, utilizza l'app Hub." }, "init": { "data": { @@ -91,23 +87,19 @@ "change_hub_config": "Modifica la configurazione dell'hub.", "remove_override": "Rimuovere una sostituzione del dispositivo.", "remove_x10": "Rimuovi un dispositivo X10." - }, - "description": "Seleziona un'opzione da configurare.", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "Seleziona un indirizzo del dispositivo da rimuovere" }, - "description": "Rimuovere una sostituzione del dispositivo", - "title": "Insteon" + "description": "Rimuovere una sostituzione del dispositivo" }, "remove_x10": { "data": { "address": "Seleziona un indirizzo del dispositivo da rimuovere" }, - "description": "Rimuovi un dispositivo X10", - "title": "Insteon" + "description": "Rimuovi un dispositivo X10" } } } diff --git a/homeassistant/components/insteon/translations/ja.json b/homeassistant/components/insteon/translations/ja.json index 81b7f246e41..812f9c0bfd6 100644 --- a/homeassistant/components/insteon/translations/ja.json +++ b/homeassistant/components/insteon/translations/ja.json @@ -43,8 +43,7 @@ "data": { "modem_type": "\u30e2\u30c7\u30e0\u306e\u7a2e\u985e\u3002" }, - "description": "Insteon modem type\u3092\u9078\u629e\u3057\u307e\u3059\u3002", - "title": "Insteon" + "description": "Insteon modem type\u3092\u9078\u629e\u3057\u307e\u3059\u3002" } } }, @@ -61,8 +60,7 @@ "cat": "\u30c7\u30d0\u30a4\u30b9\u30ab\u30c6\u30b4\u30ea\u30fc(\u4f8b: 0x10)", "subcat": "\u30c7\u30d0\u30a4\u30b9 \u30b5\u30d6\u30ab\u30c6\u30b4\u30ea\u30fc(\u4f8b: 0x0a)" }, - "description": "\u30c7\u30d0\u30a4\u30b9\u3092\u8ffd\u52a0\u3057\u3066\u4e0a\u66f8\u304d\u3057\u307e\u3059\u3002", - "title": "Insteon" + "description": "\u30c7\u30d0\u30a4\u30b9\u3092\u8ffd\u52a0\u3057\u3066\u4e0a\u66f8\u304d\u3057\u307e\u3059\u3002" }, "add_x10": { "data": { @@ -71,8 +69,7 @@ "steps": "\u8abf\u5149\u30b9\u30c6\u30c3\u30d7(\u30e9\u30a4\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u307f\u3001\u30c7\u30d5\u30a9\u30eb\u30c822)", "unitcode": "\u30e6\u30cb\u30c3\u30c8\u30b3\u30fc\u30c9(1\u301c16)" }, - "description": "Insteon Hub\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5909\u66f4\u3059\u308b\u3002", - "title": "Insteon" + "description": "Insteon Hub\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5909\u66f4\u3059\u308b\u3002" }, "change_hub_config": { "data": { @@ -81,8 +78,7 @@ "port": "\u30dd\u30fc\u30c8", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "description": "Insteon Hub\u306e\u63a5\u7d9a\u60c5\u5831\u3092\u5909\u66f4\u3057\u307e\u3059\u3002\u3053\u306e\u5909\u66f4\u3092\u884c\u3063\u305f\u5f8c\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u3053\u308c\u306f\u3001Hub\u81ea\u4f53\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3059\u308b\u3082\u306e\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002Hub\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3059\u308b\u306b\u306f\u3001Hub\u30a2\u30d7\u30ea\u3092\u4f7f\u7528\u3057\u307e\u3059\u3002", - "title": "Insteon" + "description": "Insteon Hub\u306e\u63a5\u7d9a\u60c5\u5831\u3092\u5909\u66f4\u3057\u307e\u3059\u3002\u3053\u306e\u5909\u66f4\u3092\u884c\u3063\u305f\u5f8c\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u3053\u308c\u306f\u3001Hub\u81ea\u4f53\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3059\u308b\u3082\u306e\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002Hub\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3059\u308b\u306b\u306f\u3001Hub\u30a2\u30d7\u30ea\u3092\u4f7f\u7528\u3057\u307e\u3059\u3002" }, "init": { "data": { @@ -91,23 +87,19 @@ "change_hub_config": "Hub\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059\u3002", "remove_override": "\u30c7\u30d0\u30a4\u30b9\u3092\u524a\u9664\u3057\u3066\u4e0a\u66f8\u304d\u3057\u307e\u3059\u3002", "remove_x10": "X10\u30c7\u30d0\u30a4\u30b9\u524a\u9664\u3092\u524a\u9664\u3057\u307e\u3059\u3002" - }, - "description": "\u8a2d\u5b9a\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u9078\u629e\u3057\u307e\u3059\u3002", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "\u524a\u9664\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u9078\u629e" }, - "description": "\u30c7\u30d0\u30a4\u30b9\u3092\u524a\u9664\u3057\u3066\u4e0a\u66f8\u304d\u3057\u307e\u3059", - "title": "Insteon" + "description": "\u30c7\u30d0\u30a4\u30b9\u3092\u524a\u9664\u3057\u3066\u4e0a\u66f8\u304d\u3057\u307e\u3059" }, "remove_x10": { "data": { "address": "\u524a\u9664\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u9078\u629e" }, - "description": "X10\u30c7\u30d0\u30a4\u30b9\u306e\u524a\u9664", - "title": "Insteon" + "description": "X10\u30c7\u30d0\u30a4\u30b9\u306e\u524a\u9664" } } } diff --git a/homeassistant/components/insteon/translations/ko.json b/homeassistant/components/insteon/translations/ko.json index 559e3351932..d1a4624f7bf 100644 --- a/homeassistant/components/insteon/translations/ko.json +++ b/homeassistant/components/insteon/translations/ko.json @@ -8,7 +8,11 @@ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "select_single": "\ud558\ub098\uc758 \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694." }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "{name} \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + }, "hubv1": { "data": { "host": "IP \uc8fc\uc18c", @@ -38,8 +42,7 @@ "data": { "modem_type": "\ubaa8\ub380 \uc720\ud615." }, - "description": "Insteon \ubaa8\ub380 \uc720\ud615\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", - "title": "Insteon" + "description": "Insteon \ubaa8\ub380 \uc720\ud615\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694." } } }, @@ -56,8 +59,7 @@ "cat": "\uae30\uae30 \ubc94\uc8fc (\uc608: 0x10)", "subcat": "\uae30\uae30 \ud558\uc704 \ubc94\uc8fc (\uc608: 0x0a)" }, - "description": "\uae30\uae30 \uc7ac\uc815\uc758\ub97c \ucd94\uac00\ud569\ub2c8\ub2e4.", - "title": "Insteon" + "description": "\uae30\uae30 \uc7ac\uc815\uc758\ub97c \ucd94\uac00\ud569\ub2c8\ub2e4." }, "add_x10": { "data": { @@ -66,8 +68,7 @@ "steps": "\ubc1d\uae30 \uc870\uc808 \ub2e8\uacc4 (\uc870\uba85 \uae30\uae30 \uc804\uc6a9, \uae30\ubcf8\uac12 22)", "unitcode": "\uc720\ub2db \ucf54\ub4dc (1-16)" }, - "description": "Insteon Hub \ube44\ubc00\ubc88\ud638\ub97c \ubcc0\uacbd\ud569\ub2c8\ub2e4.", - "title": "Insteon" + "description": "Insteon Hub \ube44\ubc00\ubc88\ud638\ub97c \ubcc0\uacbd\ud569\ub2c8\ub2e4." }, "change_hub_config": { "data": { @@ -76,8 +77,7 @@ "port": "\ud3ec\ud2b8", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "description": "Insteon \ud5c8\ube0c\uc758 \uc5f0\uacb0 \uc815\ubcf4\ub97c \ubcc0\uacbd\ud569\ub2c8\ub2e4. \ubcc0\uacbd\ud55c \ud6c4\uc5d0\ub294 Home Assistant\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud574\uc57c \ud569\ub2c8\ub2e4. \ud5c8\ube0c \uc790\uccb4\uc758 \uad6c\uc131\uc740 \ubcc0\uacbd\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ud5c8\ube0c\uc758 \uad6c\uc131\uc744 \ubcc0\uacbd\ud558\ub824\uba74 \ud5c8\ube0c \uc571\uc744 \uc0ac\uc6a9\ud574\uc8fc\uc138\uc694.", - "title": "Insteon" + "description": "Insteon \ud5c8\ube0c\uc758 \uc5f0\uacb0 \uc815\ubcf4\ub97c \ubcc0\uacbd\ud569\ub2c8\ub2e4. \ubcc0\uacbd\ud55c \ud6c4\uc5d0\ub294 Home Assistant\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud574\uc57c \ud569\ub2c8\ub2e4. \ud5c8\ube0c \uc790\uccb4\uc758 \uad6c\uc131\uc740 \ubcc0\uacbd\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ud5c8\ube0c\uc758 \uad6c\uc131\uc744 \ubcc0\uacbd\ud558\ub824\uba74 \ud5c8\ube0c \uc571\uc744 \uc0ac\uc6a9\ud574\uc8fc\uc138\uc694." }, "init": { "data": { @@ -86,23 +86,19 @@ "change_hub_config": "\ud5c8\ube0c \uad6c\uc131\uc744 \ubcc0\uacbd\ud569\ub2c8\ub2e4.", "remove_override": "\uae30\uae30 \uc7ac\uc815\uc758\ub97c \uc81c\uac70\ud569\ub2c8\ub2e4.", "remove_x10": "X10 \uae30\uae30\ub97c \uc81c\uac70\ud569\ub2c8\ub2e4." - }, - "description": "\uad6c\uc131\ud560 \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "\uc81c\uac70\ud560 \uae30\uae30\uc758 \uc8fc\uc18c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694" }, - "description": "\uae30\uae30 \uc7ac\uc815\uc758 \uc81c\uac70\ud558\uae30", - "title": "Insteon" + "description": "\uae30\uae30 \uc7ac\uc815\uc758 \uc81c\uac70\ud558\uae30" }, "remove_x10": { "data": { "address": "\uc81c\uac70\ud560 \uae30\uae30\uc758 \uc8fc\uc18c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694" }, - "description": "X10 \uae30\uae30 \uc81c\uac70\ud558\uae30", - "title": "Insteon" + "description": "X10 \uae30\uae30 \uc81c\uac70\ud558\uae30" } } } diff --git a/homeassistant/components/insteon/translations/lb.json b/homeassistant/components/insteon/translations/lb.json index ea8bd295900..f5ebf3ea19f 100644 --- a/homeassistant/components/insteon/translations/lb.json +++ b/homeassistant/components/insteon/translations/lb.json @@ -38,8 +38,7 @@ "data": { "modem_type": "Typ vu Modem." }, - "description": "Insteon Modem Typ auswielen.", - "title": "Insteon" + "description": "Insteon Modem Typ auswielen." } } }, @@ -56,8 +55,7 @@ "cat": "Apparat Kategorie (Beispill 0x10)", "subcat": "Apparat \u00cbnnerkategorie (Beispill 0x0a)" }, - "description": "Apparat iwwerschr\u00e9iwen dob\u00e4isetzen", - "title": "Insteon" + "description": "Apparat iwwerschr\u00e9iwen dob\u00e4isetzen" }, "add_x10": { "data": { @@ -66,8 +64,7 @@ "steps": "Dimmer Schr\u00ebtt (n\u00ebmme fir Luuchten, standard 22)", "unitcode": "Unitcode (1-16)" }, - "description": "Insteon Hub passwuert \u00e4nneren", - "title": "Insteon" + "description": "Insteon Hub passwuert \u00e4nneren" }, "change_hub_config": { "data": { @@ -76,8 +73,7 @@ "port": "Port", "username": "Benotzernumm" }, - "description": "Insteon Hub Verbindungs Informatiounen \u00e4nneren. Du muss Home Assistant no der \u00c4nnerung fr\u00ebsch starten. D\u00ebst \u00e4nnert net d'Konfiguratioun vum Hub selwer. Fir d'Konfiguratioun vum Hub ze \u00e4nnere benotz d'Hub App.", - "title": "Insteon" + "description": "Insteon Hub Verbindungs Informatiounen \u00e4nneren. Du muss Home Assistant no der \u00c4nnerung fr\u00ebsch starten. D\u00ebst \u00e4nnert net d'Konfiguratioun vum Hub selwer. Fir d'Konfiguratioun vum Hub ze \u00e4nnere benotz d'Hub App." }, "init": { "data": { @@ -86,23 +82,19 @@ "change_hub_config": "Hub Konfiguratioun \u00e4nneren.", "remove_override": "Apparat iwwerschr\u00e9iwen l\u00e4schen", "remove_x10": "Een X10 Apparat l\u00e4schen." - }, - "description": "Eng Optioun auswielen fir ze konfigur\u00e9ieren", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "Eng Apparat Adress auswielen fir ze l\u00e4schen" }, - "description": "Apparat iwwerschr\u00e9iwen l\u00e4schen", - "title": "Insteon" + "description": "Apparat iwwerschr\u00e9iwen l\u00e4schen" }, "remove_x10": { "data": { "address": "Wiel eng Adress vun egem Apparat aus fir ze l\u00e4schen" }, - "description": "Een X10 Apparat l\u00e4schen", - "title": "Insteon" + "description": "Een X10 Apparat l\u00e4schen" } } } diff --git a/homeassistant/components/insteon/translations/nl.json b/homeassistant/components/insteon/translations/nl.json index 207e370e245..59e799a084c 100644 --- a/homeassistant/components/insteon/translations/nl.json +++ b/homeassistant/components/insteon/translations/nl.json @@ -43,8 +43,7 @@ "data": { "modem_type": "Modemtype." }, - "description": "Selecteer het Insteon-modemtype.", - "title": "Insteon" + "description": "Selecteer het Insteon-modemtype." } } }, @@ -61,8 +60,7 @@ "cat": "Apparaatcategorie (bijv. 0x10)", "subcat": "Apparaatsubcategorie (bijv. 0x0a)" }, - "description": "Voeg een apparaat overschrijven toe.", - "title": "Insteon" + "description": "Voeg een apparaat overschrijven toe." }, "add_x10": { "data": { @@ -71,8 +69,7 @@ "steps": "Dimmerstappen (alleen voor verlichtingsapparaten, standaard 22)", "unitcode": "Unitcode (1 - 16)" }, - "description": "Wijzig het wachtwoord van de Insteon Hub.", - "title": "Insteon" + "description": "Wijzig het wachtwoord van de Insteon Hub." }, "change_hub_config": { "data": { @@ -81,8 +78,7 @@ "port": "Poort", "username": "Gebruikersnaam" }, - "description": "Wijzig de verbindingsgegevens van de Insteon Hub. Je moet Home Assistant opnieuw opstarten nadat je deze wijziging hebt aangebracht. Dit verandert niets aan de configuratie van de Hub zelf. Gebruik de Hub-app om de configuratie in de Hub te wijzigen.", - "title": "Insteon" + "description": "Wijzig de verbindingsgegevens van de Insteon Hub. Je moet Home Assistant opnieuw opstarten nadat je deze wijziging hebt aangebracht. Dit verandert niets aan de configuratie van de Hub zelf. Gebruik de Hub-app om de configuratie in de Hub te wijzigen." }, "init": { "data": { @@ -91,23 +87,19 @@ "change_hub_config": "Wijzig de Hub-configuratie.", "remove_override": "Verwijder een apparaatoverschrijving.", "remove_x10": "Verwijder een X10-apparaat." - }, - "description": "Selecteer een optie om te configureren.", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "Selecteer een apparaatadres om te verwijderen" }, - "description": "Verwijder een apparaatoverschrijving", - "title": "Insteon" + "description": "Verwijder een apparaatoverschrijving" }, "remove_x10": { "data": { "address": "Selecteer een apparaatadres om te verwijderen" }, - "description": "Verwijder een X10 apparaat", - "title": "Insteon" + "description": "Verwijder een X10 apparaat" } } } diff --git a/homeassistant/components/insteon/translations/no.json b/homeassistant/components/insteon/translations/no.json index 3716312b00f..70ecd17fb21 100644 --- a/homeassistant/components/insteon/translations/no.json +++ b/homeassistant/components/insteon/translations/no.json @@ -43,8 +43,7 @@ "data": { "modem_type": "Modemtype." }, - "description": "Velg Insteon modemtype.", - "title": "" + "description": "Velg Insteon modemtype." } } }, @@ -61,8 +60,7 @@ "cat": "Enhetskategori (dvs. 0x10)", "subcat": "Underkategori for enhet (dvs. 0x0a)" }, - "description": "Legg til en enhetsoverstyring.", - "title": "" + "description": "Legg til en enhetsoverstyring." }, "add_x10": { "data": { @@ -71,8 +69,7 @@ "steps": "Dimmer trinn (kun for lette enheter, standard 22)", "unitcode": "Enhetskode (1 - 16)" }, - "description": "Endre insteon hub-passordet.", - "title": "" + "description": "Endre insteon hub-passordet." }, "change_hub_config": { "data": { @@ -81,8 +78,7 @@ "port": "Port", "username": "Brukernavn" }, - "description": "Endre Insteon Hub-tilkoblingsinformasjonen. Du m\u00e5 starte Home Assistant p\u00e5 nytt n\u00e5r du har gjort denne endringen. Dette endrer ikke konfigurasjonen av selve huben. For \u00e5 endre konfigurasjonen i huben bruker du hub-appen.", - "title": "" + "description": "Endre Insteon Hub-tilkoblingsinformasjonen. Du m\u00e5 starte Home Assistant p\u00e5 nytt n\u00e5r du har gjort denne endringen. Dette endrer ikke konfigurasjonen av selve huben. For \u00e5 endre konfigurasjonen i huben bruker du hub-appen." }, "init": { "data": { @@ -91,23 +87,19 @@ "change_hub_config": "Endre hub-konfigurasjonen.", "remove_override": "Fjern en enhet overstyring.", "remove_x10": "Fjern en X10-enhet." - }, - "description": "Velg et alternativ for \u00e5 konfigurere.", - "title": "" + } }, "remove_override": { "data": { "address": "Velg en enhetsadresse du vil fjerne" }, - "description": "Fjerne en enhetsoverstyring", - "title": "" + "description": "Fjerne en enhetsoverstyring" }, "remove_x10": { "data": { "address": "Velg en enhetsadresse du vil fjerne" }, - "description": "Fjern en X10-enhet", - "title": "" + "description": "Fjern en X10-enhet" } } } diff --git a/homeassistant/components/insteon/translations/pl.json b/homeassistant/components/insteon/translations/pl.json index 6d3b6c4391d..9cadcfa955c 100644 --- a/homeassistant/components/insteon/translations/pl.json +++ b/homeassistant/components/insteon/translations/pl.json @@ -43,8 +43,7 @@ "data": { "modem_type": "Typ modemu." }, - "description": "Wybierz typ modemu Insteon.", - "title": "Insteon" + "description": "Wybierz typ modemu Insteon." } } }, @@ -61,8 +60,7 @@ "cat": "Kategoria urz\u0105dzenia (np. 0x10)", "subcat": "Podkategoria urz\u0105dzenia (np. 0x0a)" }, - "description": "Dodawanie nadpisanie urz\u0105dzenia.", - "title": "Insteon" + "description": "Dodawanie nadpisanie urz\u0105dzenia." }, "add_x10": { "data": { @@ -71,8 +69,7 @@ "steps": "Kroki \u015bciemniacza (tylko dla urz\u0105dze\u0144 o\u015bwietleniowych, domy\u015blnie 22)", "unitcode": "Unitcode (1\u201316)" }, - "description": "Zmie\u0144 has\u0142o Huba Insteon.", - "title": "Insteon" + "description": "Zmie\u0144 has\u0142o Huba Insteon." }, "change_hub_config": { "data": { @@ -81,8 +78,7 @@ "port": "Port", "username": "Nazwa u\u017cytkownika" }, - "description": "Zmie\u0144 informacje o po\u0142\u0105czeniu Huba Insteon. Po wprowadzeniu tej zmiany musisz ponownie uruchomi\u0107 Home Assistanta. Nie zmienia to konfiguracji samego Huba. Aby zmieni\u0107 jego konfiguracj\u0119, u\u017cyj aplikacji Hub.", - "title": "Insteon" + "description": "Zmie\u0144 informacje o po\u0142\u0105czeniu Huba Insteon. Po wprowadzeniu tej zmiany musisz ponownie uruchomi\u0107 Home Assistanta. Nie zmienia to konfiguracji samego Huba. Aby zmieni\u0107 jego konfiguracj\u0119, u\u017cyj aplikacji Hub." }, "init": { "data": { @@ -91,23 +87,19 @@ "change_hub_config": "Zmie\u0144 konfiguracj\u0119 Huba.", "remove_override": "Usu\u0144 nadpisanie urz\u0105dzenia.", "remove_x10": "Usu\u0144 urz\u0105dzenie X10." - }, - "description": "Wybierz opcj\u0119 do skonfigurowania.", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "Wybierz adres urz\u0105dzenia do usuni\u0119cia" }, - "description": "Usu\u0144 nadpisanie urz\u0105dzenia", - "title": "Insteon" + "description": "Usu\u0144 nadpisanie urz\u0105dzenia" }, "remove_x10": { "data": { "address": "Wybierz adres urz\u0105dzenia do usuni\u0119cia" }, - "description": "Usu\u0144 urz\u0105dzenie X10", - "title": "Insteon" + "description": "Usu\u0144 urz\u0105dzenie X10" } } } diff --git a/homeassistant/components/insteon/translations/pt-BR.json b/homeassistant/components/insteon/translations/pt-BR.json index b5487d9260a..e03bd8a55c0 100644 --- a/homeassistant/components/insteon/translations/pt-BR.json +++ b/homeassistant/components/insteon/translations/pt-BR.json @@ -43,8 +43,7 @@ "data": { "modem_type": "Tipo de modem." }, - "description": "Selecione o tipo de modem Insteon.", - "title": "Insteon" + "description": "Selecione o tipo de modem Insteon." } } }, @@ -61,8 +60,7 @@ "cat": "Subcategoria de dispositivo (ou seja, 0x10)", "subcat": "Subcategoria de dispositivo (ou seja, 0x0a)" }, - "description": "Escolha um dispositivo para sobrescrever", - "title": "Insteon" + "description": "Escolha um dispositivo para sobrescrever" }, "add_x10": { "data": { @@ -71,8 +69,7 @@ "steps": "Etapas de dimmer (apenas para dispositivos de lux, padr\u00e3o 22)", "unitcode": "C\u00f3digo de unidade (1 - 16)" }, - "description": "Altere a senha do Insteon Hub.", - "title": "Insteon" + "description": "Altere a senha do Insteon Hub." }, "change_hub_config": { "data": { @@ -81,8 +78,7 @@ "port": "Porta", "username": "Usu\u00e1rio" }, - "description": "Altere as informa\u00e7\u00f5es de conex\u00e3o do Hub Insteon. Voc\u00ea deve reiniciar o Home Assistant depois de fazer essa altera\u00e7\u00e3o. Isso n\u00e3o altera a configura\u00e7\u00e3o do pr\u00f3prio Hub. Para alterar a configura\u00e7\u00e3o no Hub, use o aplicativo Hub.", - "title": "Insteon" + "description": "Altere as informa\u00e7\u00f5es de conex\u00e3o do Hub Insteon. Voc\u00ea deve reiniciar o Home Assistant depois de fazer essa altera\u00e7\u00e3o. Isso n\u00e3o altera a configura\u00e7\u00e3o do pr\u00f3prio Hub. Para alterar a configura\u00e7\u00e3o no Hub, use o aplicativo Hub." }, "init": { "data": { @@ -91,23 +87,19 @@ "change_hub_config": "Altere a configura\u00e7\u00e3o do Hub.", "remove_override": "Remova uma substitui\u00e7\u00e3o de dispositivo.", "remove_x10": "Remova um dispositivo X10." - }, - "description": "Selecione uma op\u00e7\u00e3o para configurar.", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "Selecione um endere\u00e7o de dispositivo para remover" }, - "description": "Remover uma substitui\u00e7\u00e3o de dispositivo", - "title": "Insteon" + "description": "Remover uma substitui\u00e7\u00e3o de dispositivo" }, "remove_x10": { "data": { "address": "Selecione um endere\u00e7o de dispositivo para remover" }, - "description": "Remover um dispositivo X10", - "title": "Insteon" + "description": "Remover um dispositivo X10" } } } diff --git a/homeassistant/components/insteon/translations/pt.json b/homeassistant/components/insteon/translations/pt.json index e25fe7db5bd..1a281774ffe 100644 --- a/homeassistant/components/insteon/translations/pt.json +++ b/homeassistant/components/insteon/translations/pt.json @@ -35,8 +35,7 @@ "data": { "modem_type": "Tipo de modem." }, - "description": "Selecione o tipo de modem Insteon.", - "title": "Insteon" + "description": "Selecione o tipo de modem Insteon." } } }, @@ -62,8 +61,7 @@ "init": { "data": { "remove_x10": "Remova um dispositivo X10." - }, - "description": "Selecione uma op\u00e7\u00e3o para configurar." + } } } } diff --git a/homeassistant/components/insteon/translations/ru.json b/homeassistant/components/insteon/translations/ru.json index 3151fa35252..a95d3a51afc 100644 --- a/homeassistant/components/insteon/translations/ru.json +++ b/homeassistant/components/insteon/translations/ru.json @@ -43,8 +43,7 @@ "data": { "modem_type": "\u0422\u0438\u043f \u043c\u043e\u0434\u0435\u043c\u0430" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043c\u043e\u0434\u0435\u043c\u0430 Insteon.", - "title": "Insteon" + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043c\u043e\u0434\u0435\u043c\u0430 Insteon." } } }, @@ -61,8 +60,7 @@ "cat": "\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 0x10)", "subcat": "\u041f\u043e\u0434\u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: 0x0a)" }, - "description": "\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", - "title": "Insteon" + "description": "\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" }, "add_x10": { "data": { @@ -71,8 +69,7 @@ "steps": "\u0428\u0430\u0433 \u0434\u0438\u043c\u043c\u0435\u0440\u0430 (\u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u043e\u0441\u0432\u0435\u0442\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043f\u0440\u0438\u0431\u043e\u0440\u043e\u0432, \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e 22)", "unitcode": "\u042e\u043d\u0438\u0442\u043a\u043e\u0434 (1 - 16)" }, - "description": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u043a Insteon Hub", - "title": "Insteon" + "description": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u043a Insteon Hub" }, "change_hub_config": { "data": { @@ -81,8 +78,7 @@ "port": "\u041f\u043e\u0440\u0442", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "description": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 Insteon Hub. \u041f\u043e\u0441\u043b\u0435 \u0432\u043d\u0435\u0441\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c Home Assistant. \u042d\u0442\u043e \u043d\u0435 \u043c\u0435\u043d\u044f\u0435\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0441\u0430\u043c\u043e\u0433\u043e \u0445\u0430\u0431\u0430. \u0427\u0442\u043e\u0431\u044b \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0445\u0430\u0431\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Hub.", - "title": "Insteon" + "description": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 Insteon Hub. \u041f\u043e\u0441\u043b\u0435 \u0432\u043d\u0435\u0441\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c Home Assistant. \u042d\u0442\u043e \u043d\u0435 \u043c\u0435\u043d\u044f\u0435\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0441\u0430\u043c\u043e\u0433\u043e \u0445\u0430\u0431\u0430. \u0427\u0442\u043e\u0431\u044b \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0445\u0430\u0431\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Hub." }, "init": { "data": { @@ -91,23 +87,19 @@ "change_hub_config": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0445\u0430\u0431\u0430", "remove_override": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", "remove_x10": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e X10" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u043f\u0446\u0438\u044e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0434\u043b\u044f \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f" }, - "description": "\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", - "title": "Insteon" + "description": "\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" }, "remove_x10": { "data": { "address": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0434\u043b\u044f \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f" }, - "description": "\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 X10", - "title": "Insteon" + "description": "\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 X10" } } } diff --git a/homeassistant/components/insteon/translations/tr.json b/homeassistant/components/insteon/translations/tr.json index 2f74d6a98ae..2bf143699f4 100644 --- a/homeassistant/components/insteon/translations/tr.json +++ b/homeassistant/components/insteon/translations/tr.json @@ -43,8 +43,7 @@ "data": { "modem_type": "Modem t\u00fcr\u00fc." }, - "description": "Insteon modem tipini se\u00e7in.", - "title": "Insteon" + "description": "Insteon modem tipini se\u00e7in." } } }, @@ -61,8 +60,7 @@ "cat": "Cihaz alt kategorisi (\u00f6rnek: 0x10)", "subcat": "Cihaz alt kategorisi (yani 0x0a)" }, - "description": "Bir cihaz\u0131 ge\u00e7ersiz k\u0131lma ekleyin.", - "title": "Insteon" + "description": "Bir cihaz\u0131 ge\u00e7ersiz k\u0131lma ekleyin." }, "add_x10": { "data": { @@ -71,8 +69,7 @@ "steps": "Dimmer ad\u0131mlar\u0131 (yaln\u0131zca hafif cihazlar i\u00e7in varsay\u0131lan 22)", "unitcode": "Birim kodu (1 - 16)" }, - "description": "Insteon Hub parolas\u0131n\u0131 de\u011fi\u015ftirin.", - "title": "Insteon" + "description": "Insteon Hub parolas\u0131n\u0131 de\u011fi\u015ftirin." }, "change_hub_config": { "data": { @@ -81,8 +78,7 @@ "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "Insteon Hub ba\u011flant\u0131 bilgilerini de\u011fi\u015ftirin. Bu de\u011fi\u015fikli\u011fi yapt\u0131ktan sonra Home Assistant'\u0131 yeniden ba\u015flatman\u0131z gerekir. Bu, Hub'\u0131n yap\u0131land\u0131rmas\u0131n\u0131 de\u011fi\u015ftirmez. Hub'daki yap\u0131land\u0131rmay\u0131 de\u011fi\u015ftirmek i\u00e7in Hub uygulamas\u0131n\u0131 kullan\u0131n.", - "title": "Insteon" + "description": "Insteon Hub ba\u011flant\u0131 bilgilerini de\u011fi\u015ftirin. Bu de\u011fi\u015fikli\u011fi yapt\u0131ktan sonra Home Assistant'\u0131 yeniden ba\u015flatman\u0131z gerekir. Bu, Hub'\u0131n yap\u0131land\u0131rmas\u0131n\u0131 de\u011fi\u015ftirmez. Hub'daki yap\u0131land\u0131rmay\u0131 de\u011fi\u015ftirmek i\u00e7in Hub uygulamas\u0131n\u0131 kullan\u0131n." }, "init": { "data": { @@ -91,23 +87,19 @@ "change_hub_config": "Hub yap\u0131land\u0131rmas\u0131n\u0131 de\u011fi\u015ftirin.", "remove_override": "Bir cihaz\u0131 ge\u00e7ersiz k\u0131lma i\u015flemini kald\u0131r\u0131n.", "remove_x10": "Bir X10 cihaz\u0131n\u0131 \u00e7\u0131kar\u0131n." - }, - "description": "Yap\u0131land\u0131rmak i\u00e7in bir se\u00e7enek se\u00e7in.", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "Kald\u0131r\u0131lacak bir cihaz adresi se\u00e7in" }, - "description": "Bir cihaz\u0131 ge\u00e7ersiz k\u0131lmay\u0131 kald\u0131rma", - "title": "Insteon" + "description": "Bir cihaz\u0131 ge\u00e7ersiz k\u0131lmay\u0131 kald\u0131rma" }, "remove_x10": { "data": { "address": "Kald\u0131r\u0131lacak bir cihaz adresi se\u00e7in" }, - "description": "Bir X10 cihaz\u0131n\u0131 kald\u0131r\u0131n", - "title": "Insteon" + "description": "Bir X10 cihaz\u0131n\u0131 kald\u0131r\u0131n" } } } diff --git a/homeassistant/components/insteon/translations/uk.json b/homeassistant/components/insteon/translations/uk.json index 747e3a30176..3d450d8d973 100644 --- a/homeassistant/components/insteon/translations/uk.json +++ b/homeassistant/components/insteon/translations/uk.json @@ -38,8 +38,7 @@ "data": { "modem_type": "\u0422\u0438\u043f \u043c\u043e\u0434\u0435\u043c\u0443" }, - "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u043c\u043e\u0434\u0435\u043c\u0443 Insteon.", - "title": "Insteon" + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u043c\u043e\u0434\u0435\u043c\u0443 Insteon." } } }, @@ -56,8 +55,7 @@ "cat": "\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0456\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 0x10)", "subcat": "\u041f\u0456\u0434\u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0456\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434, 0x0a)" }, - "description": "\u041f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", - "title": "Insteon" + "description": "\u041f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" }, "add_x10": { "data": { @@ -66,8 +64,7 @@ "steps": "\u041a\u0440\u043e\u043a \u0434\u0456\u043c\u043c\u0435\u0440\u0430 (\u0442\u0456\u043b\u044c\u043a\u0438 \u0434\u043b\u044f \u043e\u0441\u0432\u0456\u0442\u043b\u044e\u0432\u0430\u043b\u044c\u043d\u0438\u0445 \u043f\u0440\u0438\u043b\u0430\u0434\u0456\u0432, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c 22)", "unitcode": "\u042e\u043d\u0456\u0442\u043a\u043e\u0434 (1 - 16)" }, - "description": "\u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043e Insteon Hub", - "title": "Insteon" + "description": "\u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043e Insteon Hub" }, "change_hub_config": { "data": { @@ -76,8 +73,7 @@ "port": "\u041f\u043e\u0440\u0442", "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" }, - "description": "\u0417\u043c\u0456\u043d\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f Insteon Hub. \u041f\u0456\u0441\u043b\u044f \u0432\u043d\u0435\u0441\u0435\u043d\u043d\u044f \u0446\u0438\u0445 \u0437\u043c\u0456\u043d \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 Home Assistant. \u0426\u0435 \u043d\u0435 \u0437\u043c\u0456\u043d\u044e\u0454 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0441\u0430\u043c\u043e\u0433\u043e \u0445\u0430\u0431\u0430. \u0429\u043e\u0431 \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0445\u0430\u0431\u0430, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Hub.", - "title": "Insteon" + "description": "\u0417\u043c\u0456\u043d\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f Insteon Hub. \u041f\u0456\u0441\u043b\u044f \u0432\u043d\u0435\u0441\u0435\u043d\u043d\u044f \u0446\u0438\u0445 \u0437\u043c\u0456\u043d \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 Home Assistant. \u0426\u0435 \u043d\u0435 \u0437\u043c\u0456\u043d\u044e\u0454 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0441\u0430\u043c\u043e\u0433\u043e \u0445\u0430\u0431\u0430. \u0429\u043e\u0431 \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0445\u0430\u0431\u0430, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Hub." }, "init": { "data": { @@ -86,23 +82,19 @@ "change_hub_config": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0445\u0430\u0431\u0430", "remove_override": "\u0412\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u043f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", "remove_x10": "\u0412\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 X10" - }, - "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u043f\u0446\u0456\u044e \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e, \u044f\u043a\u0438\u0439 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438" }, - "description": "\u0412\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u043f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", - "title": "Insteon" + "description": "\u0412\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u043f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" }, "remove_x10": { "data": { "address": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e, \u044f\u043a\u0438\u0439 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438" }, - "description": "\u0412\u0438\u0434\u0430\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e X10", - "title": "Insteon" + "description": "\u0412\u0438\u0434\u0430\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e X10" } } } diff --git a/homeassistant/components/insteon/translations/zh-Hant.json b/homeassistant/components/insteon/translations/zh-Hant.json index c55a1ea0a5c..608b6929e52 100644 --- a/homeassistant/components/insteon/translations/zh-Hant.json +++ b/homeassistant/components/insteon/translations/zh-Hant.json @@ -43,8 +43,7 @@ "data": { "modem_type": "\u6578\u64da\u6a5f\u985e\u5225\u3002" }, - "description": "\u9078\u64c7 Insteon \u6578\u64da\u6a5f\u985e\u5225\u3002", - "title": "Insteon" + "description": "\u9078\u64c7 Insteon \u6578\u64da\u6a5f\u985e\u5225\u3002" } } }, @@ -61,8 +60,7 @@ "cat": "\u88dd\u7f6e\u5b50\u985e\u5225\uff08\u4f8b\u5982 0x10\uff09", "subcat": "\u88dd\u7f6e\u5b50\u985e\u5225\uff08\u4f8b\u5982 0x0a\uff09" }, - "description": "\u65b0\u589e\u88dd\u7f6e\u8986\u5beb\u3002", - "title": "Insteon" + "description": "\u65b0\u589e\u88dd\u7f6e\u8986\u5beb\u3002" }, "add_x10": { "data": { @@ -71,8 +69,7 @@ "steps": "\u8abf\u5149\u968e\u6bb5\uff08\u50c5\u9069\u7528\u7167\u660e\u88dd\u7f6e\u3001\u9810\u8a2d\u503c\u70ba 22\uff09", "unitcode": "Unitcode (1 - 16)" }, - "description": "\u8b8a\u66f4 Insteon Hub \u5bc6\u78bc\u3002", - "title": "Insteon" + "description": "\u8b8a\u66f4 Insteon Hub \u5bc6\u78bc\u3002" }, "change_hub_config": { "data": { @@ -81,8 +78,7 @@ "port": "\u901a\u8a0a\u57e0", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8b8a\u66f4 Insteon Hub \u9023\u7dda\u8cc7\u8a0a\u3002\u65bc\u8b8a\u66f4\u4e4b\u5f8c\u3001\u5fc5\u9808\u91cd\u555f Home Assistant\u3002\u6b64\u4e9b\u8a2d\u5b9a\u4e0d\u6703\u8b8a\u66f4 Hub \u88dd\u7f6e\u672c\u8eab\u7684\u8a2d\u5b9a\uff0c\u5982\u6b32\u8b8a\u66f4 Hub \u8a2d\u5b9a\u3001\u5247\u8acb\u4f7f\u7528 Hub app\u3002", - "title": "Insteon" + "description": "\u8b8a\u66f4 Insteon Hub \u9023\u7dda\u8cc7\u8a0a\u3002\u65bc\u8b8a\u66f4\u4e4b\u5f8c\u3001\u5fc5\u9808\u91cd\u555f Home Assistant\u3002\u6b64\u4e9b\u8a2d\u5b9a\u4e0d\u6703\u8b8a\u66f4 Hub \u88dd\u7f6e\u672c\u8eab\u7684\u8a2d\u5b9a\uff0c\u5982\u6b32\u8b8a\u66f4 Hub \u8a2d\u5b9a\u3001\u5247\u8acb\u4f7f\u7528 Hub app\u3002" }, "init": { "data": { @@ -91,23 +87,19 @@ "change_hub_config": "\u8b8a\u66f4 Hub \u8a2d\u5b9a\u3002", "remove_override": "\u79fb\u9664\u88dd\u7f6e\u8986\u5beb", "remove_x10": "\u79fb\u9664 X10 \u88dd\u7f6e\u3002" - }, - "description": "\u9078\u64c7\u9078\u9805\u4ee5\u8a2d\u5b9a", - "title": "Insteon" + } }, "remove_override": { "data": { "address": "\u9078\u64c7\u88dd\u7f6e\u4f4d\u5740\u4ee5\u79fb\u9664" }, - "description": "\u79fb\u9664\u88dd\u7f6e\u8986\u5beb", - "title": "Insteon" + "description": "\u79fb\u9664\u88dd\u7f6e\u8986\u5beb" }, "remove_x10": { "data": { "address": "\u9078\u64c7\u88dd\u7f6e\u4f4d\u5740\u4ee5\u79fb\u9664" }, - "description": "\u79fb\u9664 X10 \u88dd\u7f6e", - "title": "Insteon" + "description": "\u79fb\u9664 X10 \u88dd\u7f6e" } } } diff --git a/homeassistant/components/integration/translations/ca.json b/homeassistant/components/integration/translations/ca.json index bbe5e6e31b4..ad9553ff346 100644 --- a/homeassistant/components/integration/translations/ca.json +++ b/homeassistant/components/integration/translations/ca.json @@ -29,12 +29,6 @@ "data_description": { "round": "Controla el nombre de d\u00edgits decimals a la sortida." } - }, - "options": { - "data": { - "round": "Precisi\u00f3" - }, - "description": "La precisi\u00f3 controla el nombre de d\u00edgits decimals a la sortida." } } }, diff --git a/homeassistant/components/integration/translations/cs.json b/homeassistant/components/integration/translations/cs.json index 3e7fdfca729..64e92edeb1c 100644 --- a/homeassistant/components/integration/translations/cs.json +++ b/homeassistant/components/integration/translations/cs.json @@ -28,12 +28,6 @@ "data_description": { "round": "Ur\u010duje po\u010det desetinn\u00fdch m\u00edst ve v\u00fdstupu." } - }, - "options": { - "data": { - "round": "P\u0159esnost" - }, - "description": "P\u0159esnost ur\u010duje po\u010det desetinn\u00fdch m\u00edst ve v\u00fdstupu." } } }, diff --git a/homeassistant/components/integration/translations/de.json b/homeassistant/components/integration/translations/de.json index 3013fa9d039..af26ec446a2 100644 --- a/homeassistant/components/integration/translations/de.json +++ b/homeassistant/components/integration/translations/de.json @@ -29,12 +29,6 @@ "data_description": { "round": "Steuert die Anzahl der Dezimalstellen in der Ausgabe." } - }, - "options": { - "data": { - "round": "Genauigkeit" - }, - "description": "Die Genauigkeit steuert die Anzahl der Dezimalstellen in der Ausgabe." } } }, diff --git a/homeassistant/components/integration/translations/el.json b/homeassistant/components/integration/translations/el.json index e366137fce1..6893190d9bf 100644 --- a/homeassistant/components/integration/translations/el.json +++ b/homeassistant/components/integration/translations/el.json @@ -29,12 +29,6 @@ "data_description": { "round": "\u0395\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf." } - }, - "options": { - "data": { - "round": "\u0391\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1" - }, - "description": "\u0397 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf." } } }, diff --git a/homeassistant/components/integration/translations/en.json b/homeassistant/components/integration/translations/en.json index 3174eab3f69..1ee047b447f 100644 --- a/homeassistant/components/integration/translations/en.json +++ b/homeassistant/components/integration/translations/en.json @@ -29,12 +29,6 @@ "data_description": { "round": "Controls the number of decimal digits in the output." } - }, - "options": { - "data": { - "round": "Precision" - }, - "description": "Precision controls the number of decimal digits in the output." } } }, diff --git a/homeassistant/components/integration/translations/es.json b/homeassistant/components/integration/translations/es.json index 8c903976d1d..8034e4746fe 100644 --- a/homeassistant/components/integration/translations/es.json +++ b/homeassistant/components/integration/translations/es.json @@ -26,9 +26,6 @@ "data_description": { "round": "Controla el n\u00famero de d\u00edgitos decimales en la salida." } - }, - "options": { - "description": "La precisi\u00f3n controla el n\u00famero de d\u00edgitos decimales en la salida." } } }, diff --git a/homeassistant/components/integration/translations/et.json b/homeassistant/components/integration/translations/et.json index 31901bbb6f6..4b0143d0662 100644 --- a/homeassistant/components/integration/translations/et.json +++ b/homeassistant/components/integration/translations/et.json @@ -29,12 +29,6 @@ "data_description": { "round": "M\u00e4\u00e4rab k\u00fcmnendkohtade arvu v\u00e4ljundis." } - }, - "options": { - "data": { - "round": "T\u00e4psus" - }, - "description": "T\u00e4psus reguleerib k\u00fcmnendkohtade arvu v\u00e4ljundis." } } }, diff --git a/homeassistant/components/integration/translations/fr.json b/homeassistant/components/integration/translations/fr.json index 33b5ab86e7f..cbc0e89175b 100644 --- a/homeassistant/components/integration/translations/fr.json +++ b/homeassistant/components/integration/translations/fr.json @@ -29,12 +29,6 @@ "data_description": { "round": "Contr\u00f4le le nombre de chiffres d\u00e9cimaux dans la sortie." } - }, - "options": { - "data": { - "round": "Pr\u00e9cision" - }, - "description": "La pr\u00e9cision contr\u00f4le le nombre de chiffres d\u00e9cimaux dans la sortie." } } }, diff --git a/homeassistant/components/integration/translations/he.json b/homeassistant/components/integration/translations/he.json index 4061da5f233..219c75605bb 100644 --- a/homeassistant/components/integration/translations/he.json +++ b/homeassistant/components/integration/translations/he.json @@ -27,12 +27,6 @@ "data_description": { "round": "\u05e9\u05dc\u05d9\u05d8\u05d4 \u05d1\u05de\u05e1\u05e4\u05e8 \u05d4\u05e1\u05e4\u05e8\u05d5\u05ea \u05d4\u05e2\u05e9\u05e8\u05d5\u05e0\u05d9\u05d5\u05ea \u05d1\u05e4\u05dc\u05d8." } - }, - "options": { - "data": { - "round": "\u05d3\u05d9\u05d5\u05e7" - }, - "description": "\u05d3\u05d9\u05d5\u05e7 \u05e9\u05d5\u05dc\u05d8 \u05d1\u05de\u05e1\u05e4\u05e8 \u05d4\u05e1\u05e4\u05e8\u05d5\u05ea \u05d4\u05e2\u05e9\u05e8\u05d5\u05e0\u05d9\u05d5\u05ea \u05d1\u05e4\u05dc\u05d8." } } }, diff --git a/homeassistant/components/integration/translations/hu.json b/homeassistant/components/integration/translations/hu.json index c892e858b3f..763054d4469 100644 --- a/homeassistant/components/integration/translations/hu.json +++ b/homeassistant/components/integration/translations/hu.json @@ -29,12 +29,6 @@ "data_description": { "round": "Az eredm\u00e9ny tizedesjegyeinek sz\u00e1ma." } - }, - "options": { - "data": { - "round": "Pontoss\u00e1g" - }, - "description": "A pontoss\u00e1g hat\u00e1rozza meg az eredm\u00e9ny tizedesjegyeinek sz\u00e1m\u00e1t." } } }, diff --git a/homeassistant/components/integration/translations/id.json b/homeassistant/components/integration/translations/id.json index d585a4409e9..8327d6dc555 100644 --- a/homeassistant/components/integration/translations/id.json +++ b/homeassistant/components/integration/translations/id.json @@ -29,12 +29,6 @@ "data_description": { "round": "Mengontrol jumlah digit desimal dalam output." } - }, - "options": { - "data": { - "round": "Presisi" - }, - "description": "Presisi mengontrol jumlah digit desimal pada output." } } }, diff --git a/homeassistant/components/integration/translations/it.json b/homeassistant/components/integration/translations/it.json index d07b702962c..92e4077aa90 100644 --- a/homeassistant/components/integration/translations/it.json +++ b/homeassistant/components/integration/translations/it.json @@ -29,12 +29,6 @@ "data_description": { "round": "Controlla il numero di cifre decimali nell'output." } - }, - "options": { - "data": { - "round": "Precisione" - }, - "description": "Precisione controlla il numero di cifre decimali nell'uscita." } } }, diff --git a/homeassistant/components/integration/translations/ja.json b/homeassistant/components/integration/translations/ja.json index 5fac6ba692b..4f35ba53a4a 100644 --- a/homeassistant/components/integration/translations/ja.json +++ b/homeassistant/components/integration/translations/ja.json @@ -29,12 +29,6 @@ "data_description": { "round": "\u51fa\u529b\u5024\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3002" } - }, - "options": { - "data": { - "round": "\u7cbe\u5ea6" - }, - "description": "\u7cbe\u5ea6\u306f\u3001\u51fa\u529b\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/integration/translations/ko.json b/homeassistant/components/integration/translations/ko.json new file mode 100644 index 00000000000..d217725b835 --- /dev/null +++ b/homeassistant/components/integration/translations/ko.json @@ -0,0 +1,36 @@ +{ + "config": { + "step": { + "user": { + "data": { + "method": "\uc801\ubd84 \ubc29\ubc95", + "name": "\uc774\ub984", + "round": "\uc18c\uc218\uc810", + "source": "\uc785\ub825 \uc13c\uc11c", + "unit_prefix": "\ubbf8\ud130\ubc95", + "unit_time": "\uc2dc\uac04 \ub2e8\uc704" + }, + "data_description": { + "round": "\uc18c\uc218\uc810 \uc790\ub9bf\uc218\ub97c \ubcc0\uacbd\ud569\ub2c8\ub2e4.", + "unit_prefix": "\uc120\ud0dd\ud55c \ubbf8\ud130\ubc95\uc73c\ub85c \ud45c\uc2dc\ub429\ub2c8\ub2e4.", + "unit_time": "\uc120\ud0dd\ud55c \uc2dc\uac04 \ub2e8\uc704\ub85c \ud45c\uc2dc\ub429\ub2c8\ub2e4." + }, + "description": "\ub9ac\ub9cc \ud569\uc744 \uc0ac\uc6a9\ud574\uc11c \uc801\ubd84\uac12\uc744 \uad6c\ud558\ub294 \uc13c\uc11c\ub97c \uc0dd\uc131\ud569\ub2c8\ub2e4.", + "title": "\ub9ac\ub9cc \ud569 \uc801\ubd84 \uc13c\uc11c \ucd94\uac00" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "round": "\uc18c\uc218\uc810" + }, + "data_description": { + "round": "\uc18c\uc218\uc810 \uc790\ub9bf\uc218\ub97c \ubcc0\uacbd\ud569\ub2c8\ub2e4." + } + } + } + }, + "title": "\uc801\ubd84 - \ub9ac\ub9cc \ud569 \uc801\ubd84 \uc13c\uc11c" +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/nl.json b/homeassistant/components/integration/translations/nl.json index 8753ee30ea7..e3e0259da71 100644 --- a/homeassistant/components/integration/translations/nl.json +++ b/homeassistant/components/integration/translations/nl.json @@ -29,12 +29,6 @@ "data_description": { "round": "Regelt het aantal decimale cijfers in de uitvoer." } - }, - "options": { - "data": { - "round": "Precisie" - }, - "description": "Precisie bepaalt het aantal decimale cijfers in de uitvoer." } } }, diff --git a/homeassistant/components/integration/translations/no.json b/homeassistant/components/integration/translations/no.json index aafa60b5811..9965d1c7521 100644 --- a/homeassistant/components/integration/translations/no.json +++ b/homeassistant/components/integration/translations/no.json @@ -29,12 +29,6 @@ "data_description": { "round": "Styrer antall desimaler i utdataene." } - }, - "options": { - "data": { - "round": "Presisjon" - }, - "description": "Presisjon styrer antall desimaler i utdataene." } } }, diff --git a/homeassistant/components/integration/translations/pl.json b/homeassistant/components/integration/translations/pl.json index 7971d97129c..5dfe00cd31b 100644 --- a/homeassistant/components/integration/translations/pl.json +++ b/homeassistant/components/integration/translations/pl.json @@ -29,12 +29,6 @@ "data_description": { "round": "Kontroluje liczb\u0119 cyfr dziesi\u0119tnych w danych wyj\u015bciowych." } - }, - "options": { - "data": { - "round": "Precyzja" - }, - "description": "Precyzja kontroluje liczb\u0119 cyfr dziesi\u0119tnych w danych wyj\u015bciowych." } } }, diff --git a/homeassistant/components/integration/translations/pt-BR.json b/homeassistant/components/integration/translations/pt-BR.json index ae512b93fd4..999873290b8 100644 --- a/homeassistant/components/integration/translations/pt-BR.json +++ b/homeassistant/components/integration/translations/pt-BR.json @@ -29,12 +29,6 @@ "data_description": { "round": "Controla o n\u00famero de d\u00edgitos decimais na sa\u00edda." } - }, - "options": { - "data": { - "round": "Precis\u00e3o" - }, - "description": "A precis\u00e3o controla o n\u00famero de d\u00edgitos decimais na sa\u00edda." } } }, diff --git a/homeassistant/components/integration/translations/ru.json b/homeassistant/components/integration/translations/ru.json index 67891293d7b..8e7ad96b803 100644 --- a/homeassistant/components/integration/translations/ru.json +++ b/homeassistant/components/integration/translations/ru.json @@ -29,12 +29,6 @@ "data_description": { "round": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439." } - }, - "options": { - "data": { - "round": "\u041e\u043a\u0440\u0443\u0433\u043b\u0435\u043d\u0438\u0435" - }, - "description": "\u041e\u043a\u0440\u0443\u0433\u043b\u0435\u043d\u0438\u0435 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439." } } }, diff --git a/homeassistant/components/integration/translations/sk.json b/homeassistant/components/integration/translations/sk.json index aec16fc71b8..c1372b00a8a 100644 --- a/homeassistant/components/integration/translations/sk.json +++ b/homeassistant/components/integration/translations/sk.json @@ -7,14 +7,5 @@ } } } - }, - "options": { - "step": { - "options": { - "data": { - "round": "Presnos\u0165" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/integration/translations/tr.json b/homeassistant/components/integration/translations/tr.json index de99ebd3633..2271c616150 100644 --- a/homeassistant/components/integration/translations/tr.json +++ b/homeassistant/components/integration/translations/tr.json @@ -29,12 +29,6 @@ "data_description": { "round": "\u00c7\u0131kt\u0131daki ondal\u0131k basamak say\u0131s\u0131n\u0131 kontrol eder." } - }, - "options": { - "data": { - "round": "Hassas" - }, - "description": "Kesinlik, \u00e7\u0131kt\u0131daki ondal\u0131k basamak say\u0131s\u0131n\u0131 kontrol eder." } } }, diff --git a/homeassistant/components/integration/translations/zh-Hans.json b/homeassistant/components/integration/translations/zh-Hans.json index f7ebea98f4a..15cc310b2f4 100644 --- a/homeassistant/components/integration/translations/zh-Hans.json +++ b/homeassistant/components/integration/translations/zh-Hans.json @@ -29,12 +29,6 @@ "data_description": { "round": "\u63a7\u5236\u8f93\u51fa\u7684\u5c0f\u6570\u4f4d\u6570\u3002" } - }, - "options": { - "data": { - "round": "\u7cbe\u5ea6" - }, - "description": "\u7cbe\u5ea6\u63a7\u5236\u8f93\u51fa\u7684\u5c0f\u6570\u4f4d\u6570\u3002" } } }, diff --git a/homeassistant/components/integration/translations/zh-Hant.json b/homeassistant/components/integration/translations/zh-Hant.json index 2adbb3edc28..d7142365b05 100644 --- a/homeassistant/components/integration/translations/zh-Hant.json +++ b/homeassistant/components/integration/translations/zh-Hant.json @@ -29,12 +29,6 @@ "data_description": { "round": "\u63a7\u5236\u8f38\u51fa\u4e2d\u7684\u5c0f\u6578\u4f4d\u6578\u3002" } - }, - "options": { - "data": { - "round": "\u6e96\u78ba\u5ea6" - }, - "description": "\u7cbe\u6e96\u5ea6\u63a7\u5236\u8f38\u51fa\u4e2d\u7684\u5c0f\u6578\u4f4d\u6578\u3002" } } }, diff --git a/homeassistant/components/intellifire/translations/bg.json b/homeassistant/components/intellifire/translations/bg.json index 9926e7bb6a6..9df377170f4 100644 --- a/homeassistant/components/intellifire/translations/bg.json +++ b/homeassistant/components/intellifire/translations/bg.json @@ -7,8 +7,7 @@ "error": { "api_error": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0432\u043b\u0438\u0437\u0430\u043d\u0435", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", - "iftapi_connect": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435\u0442\u043e \u0441 iftapi.net", - "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + "iftapi_connect": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435\u0442\u043e \u0441 iftapi.net" }, "flow_title": "{serial} ({host})", "step": { @@ -32,11 +31,6 @@ "host": "\u0425\u043e\u0441\u0442" }, "title": "\u0418\u0437\u0431\u043e\u0440 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" - }, - "user": { - "data": { - "host": "\u0425\u043e\u0441\u0442" - } } } } diff --git a/homeassistant/components/intellifire/translations/ca.json b/homeassistant/components/intellifire/translations/ca.json index 9894ec4920a..d5991750966 100644 --- a/homeassistant/components/intellifire/translations/ca.json +++ b/homeassistant/components/intellifire/translations/ca.json @@ -8,8 +8,7 @@ "error": { "api_error": "Ha fallat l'inici de sessi\u00f3", "cannot_connect": "Ha fallat la connexi\u00f3", - "iftapi_connect": "S'ha produ\u00eft un error en connectar a iftapi.net", - "unknown": "Error inesperat" + "iftapi_connect": "S'ha produ\u00eft un error en connectar a iftapi.net" }, "flow_title": "{serial} ({host})", "step": { @@ -34,11 +33,6 @@ }, "description": "S'han descobert els dispositius IntelliFire seg\u00fcents. Selecciona el que vulguis configurar.", "title": "Selecci\u00f3 de dispositiu" - }, - "user": { - "data": { - "host": "Amfitri\u00f3" - } } } } diff --git a/homeassistant/components/intellifire/translations/cs.json b/homeassistant/components/intellifire/translations/cs.json index 48dbc509ea8..8684c426280 100644 --- a/homeassistant/components/intellifire/translations/cs.json +++ b/homeassistant/components/intellifire/translations/cs.json @@ -4,8 +4,7 @@ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" }, "error": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "flow_title": "{serial} ({host})", "step": { @@ -18,11 +17,6 @@ "data": { "host": "Hostitel" } - }, - "user": { - "data": { - "host": "Hostitel" - } } } } diff --git a/homeassistant/components/intellifire/translations/de.json b/homeassistant/components/intellifire/translations/de.json index f1c37a8a475..d9427853cdd 100644 --- a/homeassistant/components/intellifire/translations/de.json +++ b/homeassistant/components/intellifire/translations/de.json @@ -8,8 +8,7 @@ "error": { "api_error": "Login fehlgeschlagen", "cannot_connect": "Verbindung fehlgeschlagen", - "iftapi_connect": "Fehler beim Verbinden mit iftapi.net", - "unknown": "Unerwarteter Fehler" + "iftapi_connect": "Fehler beim Verbinden mit iftapi.net" }, "flow_title": "{serial} ({host})", "step": { @@ -34,11 +33,6 @@ }, "description": "Die folgenden IntelliFire-Ger\u00e4te wurden gefunden. Bitte w\u00e4hle aus, welche du konfigurieren m\u00f6chtest.", "title": "Ger\u00e4teauswahl" - }, - "user": { - "data": { - "host": "Host" - } } } } diff --git a/homeassistant/components/intellifire/translations/el.json b/homeassistant/components/intellifire/translations/el.json index fa72581a4a6..ff286c04952 100644 --- a/homeassistant/components/intellifire/translations/el.json +++ b/homeassistant/components/intellifire/translations/el.json @@ -8,8 +8,7 @@ "error": { "api_error": "\u0397 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "iftapi_connect": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf iftapi.net", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + "iftapi_connect": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf iftapi.net" }, "flow_title": "{serial} ({host})", "step": { @@ -34,11 +33,6 @@ }, "description": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03bf\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 IntelliFire. \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03bf\u03b9\u03b1 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5.", "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" - }, - "user": { - "data": { - "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" - } } } } diff --git a/homeassistant/components/intellifire/translations/en.json b/homeassistant/components/intellifire/translations/en.json index f0c317efb93..83acafacd48 100644 --- a/homeassistant/components/intellifire/translations/en.json +++ b/homeassistant/components/intellifire/translations/en.json @@ -8,8 +8,7 @@ "error": { "api_error": "Login failed", "cannot_connect": "Failed to connect", - "iftapi_connect": "Error conecting to iftapi.net", - "unknown": "Unexpected error" + "iftapi_connect": "Error conecting to iftapi.net" }, "flow_title": "{serial} ({host})", "step": { @@ -34,11 +33,6 @@ }, "description": "The following IntelliFire devices were discovered. Please select which you wish to configure.", "title": "Device Selection" - }, - "user": { - "data": { - "host": "Host" - } } } } diff --git a/homeassistant/components/intellifire/translations/es.json b/homeassistant/components/intellifire/translations/es.json index e848376bdfb..8d19b2ba3bf 100644 --- a/homeassistant/components/intellifire/translations/es.json +++ b/homeassistant/components/intellifire/translations/es.json @@ -32,11 +32,6 @@ }, "description": "Se han descubierto los siguientes dispositivos IntelliFire. Selecciona lo que quieras configurar.", "title": "Selecci\u00f3n de dispositivo" - }, - "user": { - "data": { - "host": "Host" - } } } } diff --git a/homeassistant/components/intellifire/translations/et.json b/homeassistant/components/intellifire/translations/et.json index 53c87f112a7..b2caebb611a 100644 --- a/homeassistant/components/intellifire/translations/et.json +++ b/homeassistant/components/intellifire/translations/et.json @@ -8,8 +8,7 @@ "error": { "api_error": "Sisselogimine nurjus", "cannot_connect": "\u00dchendamine nurjus", - "iftapi_connect": "\u00dchendumine iftapi.net'iga nurjus", - "unknown": "Ootamatu t\u00f5rge" + "iftapi_connect": "\u00dchendumine iftapi.net'iga nurjus" }, "flow_title": "{serial} ({host})", "step": { @@ -34,11 +33,6 @@ }, "description": "Avastati j\u00e4rgmised IntelliFire seadmed. Palun vali millist soovid seadistada.", "title": "Seadme valik" - }, - "user": { - "data": { - "host": "Host" - } } } } diff --git a/homeassistant/components/intellifire/translations/fr.json b/homeassistant/components/intellifire/translations/fr.json index 5c478358345..650f0cad77e 100644 --- a/homeassistant/components/intellifire/translations/fr.json +++ b/homeassistant/components/intellifire/translations/fr.json @@ -8,8 +8,7 @@ "error": { "api_error": "La connexion a \u00e9chou\u00e9", "cannot_connect": "\u00c9chec de connexion", - "iftapi_connect": "Erreur lors de la connexion \u00e0 iftapi.net", - "unknown": "Erreur inattendue" + "iftapi_connect": "Erreur lors de la connexion \u00e0 iftapi.net" }, "flow_title": "{serial} ({host})", "step": { @@ -34,11 +33,6 @@ }, "description": "Les appareils IntelliFire suivants ont \u00e9t\u00e9 d\u00e9couverts. Veuillez s\u00e9lectionner celui que vous souhaitez configurer.", "title": "S\u00e9lection de l'appareil" - }, - "user": { - "data": { - "host": "H\u00f4te" - } } } } diff --git a/homeassistant/components/intellifire/translations/he.json b/homeassistant/components/intellifire/translations/he.json index e4b64392380..18cf71bf358 100644 --- a/homeassistant/components/intellifire/translations/he.json +++ b/homeassistant/components/intellifire/translations/he.json @@ -5,8 +5,7 @@ "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", - "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, "step": { "api_config": { @@ -24,11 +23,6 @@ "data": { "host": "\u05de\u05d0\u05e8\u05d7" } - }, - "user": { - "data": { - "host": "\u05de\u05d0\u05e8\u05d7" - } } } } diff --git a/homeassistant/components/intellifire/translations/hu.json b/homeassistant/components/intellifire/translations/hu.json index f5d11abe4c0..2f680f2e776 100644 --- a/homeassistant/components/intellifire/translations/hu.json +++ b/homeassistant/components/intellifire/translations/hu.json @@ -8,8 +8,7 @@ "error": { "api_error": "A bejelentkez\u00e9s sikertelen", "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "iftapi_connect": "Hiba az iftapi.net-hez val\u00f3 csatlakoz\u00e1sban", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "iftapi_connect": "Hiba az iftapi.net-hez val\u00f3 csatlakoz\u00e1sban" }, "flow_title": "{serial} ({host})", "step": { @@ -34,11 +33,6 @@ }, "description": "A k\u00f6vetkez\u0151 IntelliFire eszk\u00f6z\u00f6k \u00e9szlelve. K\u00e9rj\u00fck, v\u00e1lassza ki, melyiket szeretn\u00e9 konfigur\u00e1lni.", "title": "Eszk\u00f6z v\u00e1laszt\u00e1sa" - }, - "user": { - "data": { - "host": "C\u00edm" - } } } } diff --git a/homeassistant/components/intellifire/translations/id.json b/homeassistant/components/intellifire/translations/id.json index 07ec946b6ba..6a38f501811 100644 --- a/homeassistant/components/intellifire/translations/id.json +++ b/homeassistant/components/intellifire/translations/id.json @@ -8,8 +8,7 @@ "error": { "api_error": "Gagal masuk", "cannot_connect": "Gagal terhubung", - "iftapi_connect": "Kesalahan saat menyambung ke iftapi.net", - "unknown": "Kesalahan yang tidak diharapkan" + "iftapi_connect": "Kesalahan saat menyambung ke iftapi.net" }, "flow_title": "{serial} ({host})", "step": { @@ -34,11 +33,6 @@ }, "description": "Perangkat IntelliFire berikut ditemukan. Pilih yang ingin dikonfigurasikan.", "title": "Pemilihan Perangkat" - }, - "user": { - "data": { - "host": "Host" - } } } } diff --git a/homeassistant/components/intellifire/translations/it.json b/homeassistant/components/intellifire/translations/it.json index 8689840c82c..a1282698c88 100644 --- a/homeassistant/components/intellifire/translations/it.json +++ b/homeassistant/components/intellifire/translations/it.json @@ -8,8 +8,7 @@ "error": { "api_error": "Accesso non riuscito", "cannot_connect": "Impossibile connettersi", - "iftapi_connect": "Errore durante la connessione a iftapi.net", - "unknown": "Errore imprevisto" + "iftapi_connect": "Errore durante la connessione a iftapi.net" }, "flow_title": "{serial} ({host})", "step": { @@ -34,11 +33,6 @@ }, "description": "Sono stati rilevati i seguenti dispositivi IntelliFire. Seleziona quello che desideri configurare.", "title": "Selezione del dispositivo" - }, - "user": { - "data": { - "host": "Host" - } } } } diff --git a/homeassistant/components/intellifire/translations/ja.json b/homeassistant/components/intellifire/translations/ja.json index c623f0f59cf..6c74e4743ed 100644 --- a/homeassistant/components/intellifire/translations/ja.json +++ b/homeassistant/components/intellifire/translations/ja.json @@ -8,8 +8,7 @@ "error": { "api_error": "\u30ed\u30b0\u30a4\u30f3\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "iftapi_connect": "iftapi.net\u3078\u306e\u63a5\u7d9a\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + "iftapi_connect": "iftapi.net\u3078\u306e\u63a5\u7d9a\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f" }, "flow_title": "{serial} ({host})", "step": { @@ -34,11 +33,6 @@ }, "description": "\u4ee5\u4e0b\u306eIntelliFire\u30c7\u30d0\u30a4\u30b9\u304c\u691c\u51fa\u3055\u308c\u307e\u3057\u305f\u3002\u8a2d\u5b9a\u3057\u305f\u3044\u3082\u306e\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u30c7\u30d0\u30a4\u30b9\u306e\u9078\u629e" - }, - "user": { - "data": { - "host": "\u30db\u30b9\u30c8" - } } } } diff --git a/homeassistant/components/intellifire/translations/ko.json b/homeassistant/components/intellifire/translations/ko.json new file mode 100644 index 00000000000..fde189b9541 --- /dev/null +++ b/homeassistant/components/intellifire/translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "api_error": "\ub85c\uadf8\uc778 \uc2e4\ud328", + "iftapi_connect": "iftapi.net\uc5d0 \uc5f0\uacb0\ud558\ub294 \ub3d9\uc548 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." + }, + "step": { + "api_config": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c" + } + }, + "pick_device": { + "description": "IntelliFire \uc7a5\uce58\uac00 \uac80\uc0c9\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uad6c\uc131\ud560 \ud56d\ubaa9\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624.", + "title": "\uae30\uae30 \uc120\ud0dd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/intellifire/translations/nl.json b/homeassistant/components/intellifire/translations/nl.json index a4f2b1c304a..7356c4a0d50 100644 --- a/homeassistant/components/intellifire/translations/nl.json +++ b/homeassistant/components/intellifire/translations/nl.json @@ -8,8 +8,7 @@ "error": { "api_error": "Inloggen mislukt", "cannot_connect": "Kan geen verbinding maken", - "iftapi_connect": "Fout bij het verbinden met iftapi.net", - "unknown": "Onverwachte fout" + "iftapi_connect": "Fout bij het verbinden met iftapi.net" }, "flow_title": "{serial} ({host})", "step": { @@ -34,11 +33,6 @@ }, "description": "De volgende IntelliFire-apparaten zijn ontdekt. Selecteer welke u wilt configureren.", "title": "Apparaat selectie" - }, - "user": { - "data": { - "host": "Host" - } } } } diff --git a/homeassistant/components/intellifire/translations/no.json b/homeassistant/components/intellifire/translations/no.json index 8175a085f30..53cf7495b94 100644 --- a/homeassistant/components/intellifire/translations/no.json +++ b/homeassistant/components/intellifire/translations/no.json @@ -8,8 +8,7 @@ "error": { "api_error": "Innlogging feilet", "cannot_connect": "Tilkobling mislyktes", - "iftapi_connect": "Feil ved tilkobling til iftapi.net", - "unknown": "Uventet feil" + "iftapi_connect": "Feil ved tilkobling til iftapi.net" }, "flow_title": "{serial} ( {host} )", "step": { @@ -34,11 +33,6 @@ }, "description": "F\u00f8lgende IntelliFire-enheter ble oppdaget. Velg hvilken du \u00f8nsker \u00e5 konfigurere.", "title": "Enhetsvalg" - }, - "user": { - "data": { - "host": "Vert" - } } } } diff --git a/homeassistant/components/intellifire/translations/pl.json b/homeassistant/components/intellifire/translations/pl.json index f63be88814f..c470c77d03b 100644 --- a/homeassistant/components/intellifire/translations/pl.json +++ b/homeassistant/components/intellifire/translations/pl.json @@ -8,8 +8,7 @@ "error": { "api_error": "Logowanie nie powiod\u0142o si\u0119", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "iftapi_connect": "B\u0142\u0105d po\u0142\u0105czenia z iftapi.net", - "unknown": "Nieoczekiwany b\u0142\u0105d" + "iftapi_connect": "B\u0142\u0105d po\u0142\u0105czenia z iftapi.net" }, "flow_title": "{serial} ({host})", "step": { @@ -34,11 +33,6 @@ }, "description": "Wykryto nast\u0119puj\u0105ce urz\u0105dzenia IntelliFire. Wybierz, kt\u00f3re chcesz skonfigurowa\u0107.", "title": "Wyb\u00f3r urz\u0105dzenia" - }, - "user": { - "data": { - "host": "Nazwa hosta lub adres IP" - } } } } diff --git a/homeassistant/components/intellifire/translations/pt-BR.json b/homeassistant/components/intellifire/translations/pt-BR.json index babcc22bd97..31a98a0d6d4 100644 --- a/homeassistant/components/intellifire/translations/pt-BR.json +++ b/homeassistant/components/intellifire/translations/pt-BR.json @@ -8,8 +8,7 @@ "error": { "api_error": "Login falhou", "cannot_connect": "Falha ao conectar", - "iftapi_connect": "Erro ao conectar ao iftapi.net", - "unknown": "Erro inesperado" + "iftapi_connect": "Erro ao conectar ao iftapi.net" }, "flow_title": "{serial} ({host})", "step": { @@ -34,11 +33,6 @@ }, "description": "Os seguintes dispositivos IntelliFire foram descobertos. Por favor, selecione o que voc\u00ea deseja configura.", "title": "Sele\u00e7\u00e3o de dispositivo" - }, - "user": { - "data": { - "host": "Nome do host" - } } } } diff --git a/homeassistant/components/intellifire/translations/ru.json b/homeassistant/components/intellifire/translations/ru.json index 8e201924028..4471edf0811 100644 --- a/homeassistant/components/intellifire/translations/ru.json +++ b/homeassistant/components/intellifire/translations/ru.json @@ -8,8 +8,7 @@ "error": { "api_error": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u043d\u0435 \u0443\u0434\u0430\u043b\u0430\u0441\u044c.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "iftapi_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a iftapi.net", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "iftapi_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a iftapi.net" }, "flow_title": "{serial} ({host})", "step": { @@ -34,11 +33,6 @@ }, "description": "\u0411\u044b\u043b\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 IntelliFire. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435, \u043a\u0430\u043a\u043e\u0435 \u0438\u0437 \u043d\u0438\u0445 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c.", "title": "\u0412\u044b\u0431\u043e\u0440 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" - }, - "user": { - "data": { - "host": "\u0425\u043e\u0441\u0442" - } } } } diff --git a/homeassistant/components/intellifire/translations/sv.json b/homeassistant/components/intellifire/translations/sv.json index f341a6314ee..be36fec5fe3 100644 --- a/homeassistant/components/intellifire/translations/sv.json +++ b/homeassistant/components/intellifire/translations/sv.json @@ -4,15 +4,7 @@ "already_configured": "Enheten \u00e4r redan konfigurerad" }, "error": { - "cannot_connect": "Det gick inte att ansluta.", - "unknown": "Ov\u00e4ntat fel" - }, - "step": { - "user": { - "data": { - "host": "V\u00e4rd" - } - } + "cannot_connect": "Det gick inte att ansluta." } } } \ No newline at end of file diff --git a/homeassistant/components/intellifire/translations/tr.json b/homeassistant/components/intellifire/translations/tr.json index b5be24edd2f..1eba2a43e72 100644 --- a/homeassistant/components/intellifire/translations/tr.json +++ b/homeassistant/components/intellifire/translations/tr.json @@ -8,8 +8,7 @@ "error": { "api_error": "Oturum a\u00e7ma ba\u015far\u0131s\u0131z oldu", "cannot_connect": "Ba\u011flanma hatas\u0131", - "iftapi_connect": "iftapi.net'e ba\u011flan\u0131rken hata olu\u015ftu", - "unknown": "Beklenmeyen hata" + "iftapi_connect": "iftapi.net'e ba\u011flan\u0131rken hata olu\u015ftu" }, "flow_title": "{serial} ({host})", "step": { @@ -34,11 +33,6 @@ }, "description": "A\u015fa\u011f\u0131daki IntelliFire cihazlar\u0131 ke\u015ffedildi. L\u00fctfen yap\u0131land\u0131rmak istedi\u011finizi se\u00e7in.", "title": "Cihaz Se\u00e7imi" - }, - "user": { - "data": { - "host": "Sunucu" - } } } } diff --git a/homeassistant/components/intellifire/translations/zh-Hant.json b/homeassistant/components/intellifire/translations/zh-Hant.json index 107f9cca74c..b55b8fc55bd 100644 --- a/homeassistant/components/intellifire/translations/zh-Hant.json +++ b/homeassistant/components/intellifire/translations/zh-Hant.json @@ -8,8 +8,7 @@ "error": { "api_error": "\u767b\u5165\u5931\u6557", "cannot_connect": "\u9023\u7dda\u5931\u6557", - "iftapi_connect": "\u9023\u7dda\u81f3 iftapi.net \u932f\u8aa4", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "iftapi_connect": "\u9023\u7dda\u81f3 iftapi.net \u932f\u8aa4" }, "flow_title": "{serial} ({host})", "step": { @@ -34,11 +33,6 @@ }, "description": "\u641c\u5c0b\u5230\u4ee5\u4e0b IntelliFire \u88dd\u7f6e\uff0c\u8acb\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e\u3002", "title": "\u88dd\u7f6e\u9078\u64c7" - }, - "user": { - "data": { - "host": "\u4e3b\u6a5f\u7aef" - } } } } diff --git a/homeassistant/components/iqvia/translations/bg.json b/homeassistant/components/iqvia/translations/bg.json index e59b361d2e0..3826c82abf7 100644 --- a/homeassistant/components/iqvia/translations/bg.json +++ b/homeassistant/components/iqvia/translations/bg.json @@ -11,8 +11,7 @@ "data": { "zip_code": "\u041f\u043e\u0449\u0435\u043d\u0441\u043a\u0438 \u043a\u043e\u0434" }, - "description": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0432\u0430\u0448\u0438\u044f \u0430\u043c\u0435\u0440\u0438\u043a\u0430\u043d\u0441\u043a\u0438 \u0438\u043b\u0438 \u043a\u0430\u043d\u0430\u0434\u0441\u043a\u0438 \u043f\u043e\u0449\u0435\u043d\u0441\u043a\u0438 \u043a\u043e\u0434.", - "title": "IQVIA" + "description": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0432\u0430\u0448\u0438\u044f \u0430\u043c\u0435\u0440\u0438\u043a\u0430\u043d\u0441\u043a\u0438 \u0438\u043b\u0438 \u043a\u0430\u043d\u0430\u0434\u0441\u043a\u0438 \u043f\u043e\u0449\u0435\u043d\u0441\u043a\u0438 \u043a\u043e\u0434." } } } diff --git a/homeassistant/components/iqvia/translations/ca.json b/homeassistant/components/iqvia/translations/ca.json index 0e062e192cf..ddea24c4711 100644 --- a/homeassistant/components/iqvia/translations/ca.json +++ b/homeassistant/components/iqvia/translations/ca.json @@ -11,8 +11,7 @@ "data": { "zip_code": "Codi postal" }, - "description": "Introdueix el teu codi postal d'Estats Units o Canad\u00e0.", - "title": "IQVIA" + "description": "Introdueix el teu codi postal d'Estats Units o Canad\u00e0." } } } diff --git a/homeassistant/components/iqvia/translations/cs.json b/homeassistant/components/iqvia/translations/cs.json index 04829af0b8f..ac4be10dc8d 100644 --- a/homeassistant/components/iqvia/translations/cs.json +++ b/homeassistant/components/iqvia/translations/cs.json @@ -11,8 +11,7 @@ "data": { "zip_code": "PS\u010c" }, - "description": "Vypl\u0148te sv\u00e9 americk\u00e9 nebo kanadsk\u00e9 PS\u010c.", - "title": "IQVIA" + "description": "Vypl\u0148te sv\u00e9 americk\u00e9 nebo kanadsk\u00e9 PS\u010c." } } } diff --git a/homeassistant/components/iqvia/translations/da.json b/homeassistant/components/iqvia/translations/da.json index 8cc93be50ec..8e074aacea7 100644 --- a/homeassistant/components/iqvia/translations/da.json +++ b/homeassistant/components/iqvia/translations/da.json @@ -8,8 +8,7 @@ "data": { "zip_code": "Postnummer" }, - "description": "Udfyld dit amerikanske eller canadiske postnummer.", - "title": "IQVIA" + "description": "Udfyld dit amerikanske eller canadiske postnummer." } } } diff --git a/homeassistant/components/iqvia/translations/de.json b/homeassistant/components/iqvia/translations/de.json index 5d307eea829..90cf3a434c8 100644 --- a/homeassistant/components/iqvia/translations/de.json +++ b/homeassistant/components/iqvia/translations/de.json @@ -11,8 +11,7 @@ "data": { "zip_code": "Postleitzahl" }, - "description": "Trage eine US-amerikanische oder kanadische Postleitzahl ein.", - "title": "IQVIA" + "description": "Trage eine US-amerikanische oder kanadische Postleitzahl ein." } } } diff --git a/homeassistant/components/iqvia/translations/el.json b/homeassistant/components/iqvia/translations/el.json index 4a3045201e2..44f2aab43eb 100644 --- a/homeassistant/components/iqvia/translations/el.json +++ b/homeassistant/components/iqvia/translations/el.json @@ -11,8 +11,7 @@ "data": { "zip_code": "\u03a4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b9\u03ba\u03cc\u03c2 \u03ba\u03ce\u03b4\u03b9\u03ba\u03b1\u03c2" }, - "description": "\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b9\u03ba\u03cc \u03ba\u03ce\u03b4\u03b9\u03ba\u03b1 \u03c4\u03c9\u03bd \u0397\u03a0\u0391 \u03ae \u03c4\u03bf\u03c5 \u039a\u03b1\u03bd\u03b1\u03b4\u03ac.", - "title": "IQVIA" + "description": "\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03b1\u03c7\u03c5\u03b4\u03c1\u03bf\u03bc\u03b9\u03ba\u03cc \u03ba\u03ce\u03b4\u03b9\u03ba\u03b1 \u03c4\u03c9\u03bd \u0397\u03a0\u0391 \u03ae \u03c4\u03bf\u03c5 \u039a\u03b1\u03bd\u03b1\u03b4\u03ac." } } } diff --git a/homeassistant/components/iqvia/translations/en.json b/homeassistant/components/iqvia/translations/en.json index 46b9189660b..1732f9c9c2e 100644 --- a/homeassistant/components/iqvia/translations/en.json +++ b/homeassistant/components/iqvia/translations/en.json @@ -11,8 +11,7 @@ "data": { "zip_code": "ZIP Code" }, - "description": "Fill out your U.S. or Canadian ZIP code.", - "title": "IQVIA" + "description": "Fill out your U.S. or Canadian ZIP code." } } } diff --git a/homeassistant/components/iqvia/translations/es-419.json b/homeassistant/components/iqvia/translations/es-419.json index 61fa3f885a2..6c47623626b 100644 --- a/homeassistant/components/iqvia/translations/es-419.json +++ b/homeassistant/components/iqvia/translations/es-419.json @@ -8,8 +8,7 @@ "data": { "zip_code": "C\u00f3digo postal" }, - "description": "Complete su c\u00f3digo postal de EE. UU. o Canad\u00e1.", - "title": "IQVIA" + "description": "Complete su c\u00f3digo postal de EE. UU. o Canad\u00e1." } } } diff --git a/homeassistant/components/iqvia/translations/es.json b/homeassistant/components/iqvia/translations/es.json index 1d02f6e60c8..cecbb7af591 100644 --- a/homeassistant/components/iqvia/translations/es.json +++ b/homeassistant/components/iqvia/translations/es.json @@ -11,8 +11,7 @@ "data": { "zip_code": "C\u00f3digo postal" }, - "description": "Indica tu c\u00f3digo postal de Estados Unidos o Canad\u00e1.", - "title": "IQVIA" + "description": "Indica tu c\u00f3digo postal de Estados Unidos o Canad\u00e1." } } } diff --git a/homeassistant/components/iqvia/translations/et.json b/homeassistant/components/iqvia/translations/et.json index e1f6e51ef7c..5a9771f0d0f 100644 --- a/homeassistant/components/iqvia/translations/et.json +++ b/homeassistant/components/iqvia/translations/et.json @@ -11,8 +11,7 @@ "data": { "zip_code": "Sihtnumber" }, - "description": "Sisesta oma USA v\u00f5i Kanada sihtnumber.", - "title": "" + "description": "Sisesta oma USA v\u00f5i Kanada sihtnumber." } } } diff --git a/homeassistant/components/iqvia/translations/fi.json b/homeassistant/components/iqvia/translations/fi.json index 375176873f0..bc76d09c8e5 100644 --- a/homeassistant/components/iqvia/translations/fi.json +++ b/homeassistant/components/iqvia/translations/fi.json @@ -7,8 +7,7 @@ "user": { "data": { "zip_code": "Postinumero" - }, - "title": "IQVIA" + } } } } diff --git a/homeassistant/components/iqvia/translations/fr.json b/homeassistant/components/iqvia/translations/fr.json index 97adf4c35a6..8bc7bd18ada 100644 --- a/homeassistant/components/iqvia/translations/fr.json +++ b/homeassistant/components/iqvia/translations/fr.json @@ -11,8 +11,7 @@ "data": { "zip_code": "Code postal" }, - "description": "Entrez votre code postal am\u00e9ricain ou canadien.", - "title": "IQVIA" + "description": "Entrez votre code postal am\u00e9ricain ou canadien." } } } diff --git a/homeassistant/components/iqvia/translations/hu.json b/homeassistant/components/iqvia/translations/hu.json index 0ae420e47aa..c28fc2b23ab 100644 --- a/homeassistant/components/iqvia/translations/hu.json +++ b/homeassistant/components/iqvia/translations/hu.json @@ -11,8 +11,7 @@ "data": { "zip_code": "Ir\u00e1ny\u00edt\u00f3sz\u00e1m" }, - "description": "T\u00f6ltse ki amerikai vagy kanadai ir\u00e1ny\u00edt\u00f3sz\u00e1m\u00e1t.", - "title": "IQVIA" + "description": "T\u00f6ltse ki amerikai vagy kanadai ir\u00e1ny\u00edt\u00f3sz\u00e1m\u00e1t." } } } diff --git a/homeassistant/components/iqvia/translations/id.json b/homeassistant/components/iqvia/translations/id.json index 32f9b77135b..ed6999558da 100644 --- a/homeassistant/components/iqvia/translations/id.json +++ b/homeassistant/components/iqvia/translations/id.json @@ -11,8 +11,7 @@ "data": { "zip_code": "Kode Pos" }, - "description": "Isi kode pos AS atau Kanada.", - "title": "IQVIA" + "description": "Isi kode pos AS atau Kanada." } } } diff --git a/homeassistant/components/iqvia/translations/it.json b/homeassistant/components/iqvia/translations/it.json index 67d537bab5f..2e9b1e7fd9a 100644 --- a/homeassistant/components/iqvia/translations/it.json +++ b/homeassistant/components/iqvia/translations/it.json @@ -11,8 +11,7 @@ "data": { "zip_code": "CAP" }, - "description": "Compila il tuo CAP americano o canadese.", - "title": "IQVIA" + "description": "Compila il tuo CAP americano o canadese." } } } diff --git a/homeassistant/components/iqvia/translations/ja.json b/homeassistant/components/iqvia/translations/ja.json index 7a9e136ddc9..e4fde9a5eef 100644 --- a/homeassistant/components/iqvia/translations/ja.json +++ b/homeassistant/components/iqvia/translations/ja.json @@ -11,8 +11,7 @@ "data": { "zip_code": "\u90f5\u4fbf\u756a\u53f7" }, - "description": "\u7c73\u56fd\u307e\u305f\u306f\u30ab\u30ca\u30c0\u306e\u90f5\u4fbf\u756a\u53f7\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "IQVIA" + "description": "\u7c73\u56fd\u307e\u305f\u306f\u30ab\u30ca\u30c0\u306e\u90f5\u4fbf\u756a\u53f7\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } } diff --git a/homeassistant/components/iqvia/translations/ko.json b/homeassistant/components/iqvia/translations/ko.json index 1b0dfd980ec..f1b73c4a101 100644 --- a/homeassistant/components/iqvia/translations/ko.json +++ b/homeassistant/components/iqvia/translations/ko.json @@ -11,8 +11,7 @@ "data": { "zip_code": "\uc6b0\ud3b8\ubc88\ud638" }, - "description": "\ubbf8\uad6d \ub610\ub294 \uce90\ub098\ub2e4\uc758 \uc6b0\ud3b8\ubc88\ud638\ub97c \uae30\uc785\ud574\uc8fc\uc138\uc694.", - "title": "IQVIA" + "description": "\ubbf8\uad6d \ub610\ub294 \uce90\ub098\ub2e4\uc758 \uc6b0\ud3b8\ubc88\ud638\ub97c \uae30\uc785\ud574\uc8fc\uc138\uc694." } } } diff --git a/homeassistant/components/iqvia/translations/lb.json b/homeassistant/components/iqvia/translations/lb.json index 02fa61fe5cc..45a7984368c 100644 --- a/homeassistant/components/iqvia/translations/lb.json +++ b/homeassistant/components/iqvia/translations/lb.json @@ -11,8 +11,7 @@ "data": { "zip_code": "Postleitzuel" }, - "description": "Gitt \u00e4r U.S. oder Kanadesch Postleitzuel un.", - "title": "IQVIA" + "description": "Gitt \u00e4r U.S. oder Kanadesch Postleitzuel un." } } } diff --git a/homeassistant/components/iqvia/translations/nl.json b/homeassistant/components/iqvia/translations/nl.json index 753e35c74ea..13d7e0ce4d3 100644 --- a/homeassistant/components/iqvia/translations/nl.json +++ b/homeassistant/components/iqvia/translations/nl.json @@ -11,8 +11,7 @@ "data": { "zip_code": "Postcode" }, - "description": "Vul uw Amerikaanse of Canadese postcode in.", - "title": "IQVIA" + "description": "Vul uw Amerikaanse of Canadese postcode in." } } } diff --git a/homeassistant/components/iqvia/translations/nn.json b/homeassistant/components/iqvia/translations/nn.json deleted file mode 100644 index edbbd37f82a..00000000000 --- a/homeassistant/components/iqvia/translations/nn.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "IQVIA" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/iqvia/translations/no.json b/homeassistant/components/iqvia/translations/no.json index 5e073cefead..4042cb5346e 100644 --- a/homeassistant/components/iqvia/translations/no.json +++ b/homeassistant/components/iqvia/translations/no.json @@ -11,8 +11,7 @@ "data": { "zip_code": "Postnummer" }, - "description": "Fyll ut ditt amerikanske eller kanadiske postnummer.", - "title": "" + "description": "Fyll ut ditt amerikanske eller kanadiske postnummer." } } } diff --git a/homeassistant/components/iqvia/translations/pl.json b/homeassistant/components/iqvia/translations/pl.json index b5d10e9cc09..9d396521cdf 100644 --- a/homeassistant/components/iqvia/translations/pl.json +++ b/homeassistant/components/iqvia/translations/pl.json @@ -11,8 +11,7 @@ "data": { "zip_code": "Kod pocztowy" }, - "description": "Wprowad\u017a ameryka\u0144ski lub kanadyjski kod pocztowy.", - "title": "IQVIA" + "description": "Wprowad\u017a ameryka\u0144ski lub kanadyjski kod pocztowy." } } } diff --git a/homeassistant/components/iqvia/translations/pt-BR.json b/homeassistant/components/iqvia/translations/pt-BR.json index a366280ec35..0287fbb0d42 100644 --- a/homeassistant/components/iqvia/translations/pt-BR.json +++ b/homeassistant/components/iqvia/translations/pt-BR.json @@ -11,8 +11,7 @@ "data": { "zip_code": "C\u00f3digo postal" }, - "description": "Preencha o seu CEP dos EUA ou Canad\u00e1.", - "title": "IQVIA" + "description": "Preencha o seu CEP dos EUA ou Canad\u00e1." } } } diff --git a/homeassistant/components/iqvia/translations/ru.json b/homeassistant/components/iqvia/translations/ru.json index 6e2ddb93698..a785ef97a05 100644 --- a/homeassistant/components/iqvia/translations/ru.json +++ b/homeassistant/components/iqvia/translations/ru.json @@ -11,8 +11,7 @@ "data": { "zip_code": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 (\u0434\u043b\u044f \u0421\u0428\u0410 \u0438\u043b\u0438 \u041a\u0430\u043d\u0430\u0434\u044b).", - "title": "IQVIA" + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 (\u0434\u043b\u044f \u0421\u0428\u0410 \u0438\u043b\u0438 \u041a\u0430\u043d\u0430\u0434\u044b)." } } } diff --git a/homeassistant/components/iqvia/translations/sl.json b/homeassistant/components/iqvia/translations/sl.json index 351ad666c37..6afc35a489e 100644 --- a/homeassistant/components/iqvia/translations/sl.json +++ b/homeassistant/components/iqvia/translations/sl.json @@ -8,8 +8,7 @@ "data": { "zip_code": "Po\u0161tna \u0161tevilka" }, - "description": "Izpolnite svojo ameri\u0161ko ali kanadsko po\u0161tno \u0161tevilko.", - "title": "IQVIA" + "description": "Izpolnite svojo ameri\u0161ko ali kanadsko po\u0161tno \u0161tevilko." } } } diff --git a/homeassistant/components/iqvia/translations/sv.json b/homeassistant/components/iqvia/translations/sv.json index 88054725823..71eb118a858 100644 --- a/homeassistant/components/iqvia/translations/sv.json +++ b/homeassistant/components/iqvia/translations/sv.json @@ -8,8 +8,7 @@ "data": { "zip_code": "Postnummer" }, - "description": "Fyll i ditt Amerikanska eller Kanadensiska postnummer", - "title": "IQVIA" + "description": "Fyll i ditt Amerikanska eller Kanadensiska postnummer" } } } diff --git a/homeassistant/components/iqvia/translations/tr.json b/homeassistant/components/iqvia/translations/tr.json index 0b8a702d696..c5a6be17c9c 100644 --- a/homeassistant/components/iqvia/translations/tr.json +++ b/homeassistant/components/iqvia/translations/tr.json @@ -11,8 +11,7 @@ "data": { "zip_code": "Posta kodu" }, - "description": "ABD veya Kanada Posta kodunuzu doldurun.", - "title": "IQVIA" + "description": "ABD veya Kanada Posta kodunuzu doldurun." } } } diff --git a/homeassistant/components/iqvia/translations/uk.json b/homeassistant/components/iqvia/translations/uk.json index ab9813d6289..0d4d8940a86 100644 --- a/homeassistant/components/iqvia/translations/uk.json +++ b/homeassistant/components/iqvia/translations/uk.json @@ -11,8 +11,7 @@ "data": { "zip_code": "\u041f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441" }, - "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0441\u0432\u0456\u0439 \u043f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441 (\u0434\u043b\u044f \u0421\u0428\u0410 \u0430\u0431\u043e \u041a\u0430\u043d\u0430\u0434\u0438).", - "title": "IQVIA" + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0441\u0432\u0456\u0439 \u043f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441 (\u0434\u043b\u044f \u0421\u0428\u0410 \u0430\u0431\u043e \u041a\u0430\u043d\u0430\u0434\u0438)." } } } diff --git a/homeassistant/components/iqvia/translations/zh-Hans.json b/homeassistant/components/iqvia/translations/zh-Hans.json index 9b5f24c1d5f..b5150f514c4 100644 --- a/homeassistant/components/iqvia/translations/zh-Hans.json +++ b/homeassistant/components/iqvia/translations/zh-Hans.json @@ -8,8 +8,7 @@ "data": { "zip_code": "\u90ae\u653f\u7f16\u7801" }, - "description": "\u586b\u5199\u60a8\u7684\u7f8e\u56fd\u6216\u52a0\u62ff\u5927\u90ae\u653f\u7f16\u7801\u3002", - "title": "IQVIA" + "description": "\u586b\u5199\u60a8\u7684\u7f8e\u56fd\u6216\u52a0\u62ff\u5927\u90ae\u653f\u7f16\u7801\u3002" } } } diff --git a/homeassistant/components/iqvia/translations/zh-Hant.json b/homeassistant/components/iqvia/translations/zh-Hant.json index 12d1edce896..fcb239e82c7 100644 --- a/homeassistant/components/iqvia/translations/zh-Hant.json +++ b/homeassistant/components/iqvia/translations/zh-Hant.json @@ -11,8 +11,7 @@ "data": { "zip_code": "\u90f5\u905e\u5340\u865f" }, - "description": "\u586b\u5beb\u7f8e\u570b\u6216\u52a0\u62ff\u5927\u90f5\u905e\u5340\u865f\u3002", - "title": "IQVIA" + "description": "\u586b\u5beb\u7f8e\u570b\u6216\u52a0\u62ff\u5927\u90f5\u905e\u5340\u865f\u3002" } } } diff --git a/homeassistant/components/iss/translations/bg.json b/homeassistant/components/iss/translations/bg.json index 05945056fb4..d1b61daf0ec 100644 --- a/homeassistant/components/iss/translations/bg.json +++ b/homeassistant/components/iss/translations/bg.json @@ -6,9 +6,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "\u0414\u0430 \u0441\u0435 \u043f\u043e\u043a\u0430\u0436\u0435 \u043d\u0430 \u043a\u0430\u0440\u0442\u0430\u0442\u0430?" - }, "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u041c\u0435\u0436\u0434\u0443\u043d\u0430\u0440\u043e\u0434\u043d\u0430\u0442\u0430 \u043a\u043e\u0441\u043c\u0438\u0447\u0435\u0441\u043a\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f?" } } diff --git a/homeassistant/components/iss/translations/ca.json b/homeassistant/components/iss/translations/ca.json index 23e4ff4eabd..3ce642e7564 100644 --- a/homeassistant/components/iss/translations/ca.json +++ b/homeassistant/components/iss/translations/ca.json @@ -6,9 +6,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "Mostrar al mapa?" - }, "description": "Vols configurar Estaci\u00f3 Espacial Internacional (ISS)?" } } diff --git a/homeassistant/components/iss/translations/de.json b/homeassistant/components/iss/translations/de.json index 04ae0f6e9d5..c7eb9434db2 100644 --- a/homeassistant/components/iss/translations/de.json +++ b/homeassistant/components/iss/translations/de.json @@ -6,9 +6,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "Auf der Karte anzeigen?" - }, "description": "M\u00f6chtest du die Internationale Raumstation (ISS) konfigurieren?" } } diff --git a/homeassistant/components/iss/translations/el.json b/homeassistant/components/iss/translations/el.json index 0938fd72c09..8d9570225ba 100644 --- a/homeassistant/components/iss/translations/el.json +++ b/homeassistant/components/iss/translations/el.json @@ -6,9 +6,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c3\u03c4\u03bf \u03c7\u03ac\u03c1\u03c4\u03b7;" - }, "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5 \u0394\u03b9\u03b5\u03b8\u03bd\u03bf\u03cd\u03c2 \u0394\u03b9\u03b1\u03c3\u03c4\u03b7\u03bc\u03b9\u03ba\u03bf\u03cd \u03a3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd (ISS);" } } diff --git a/homeassistant/components/iss/translations/en.json b/homeassistant/components/iss/translations/en.json index 56f9bd79e88..b90ff56964a 100644 --- a/homeassistant/components/iss/translations/en.json +++ b/homeassistant/components/iss/translations/en.json @@ -6,9 +6,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "Show on map?" - }, "description": "Do you want to configure International Space Station (ISS)?" } } diff --git a/homeassistant/components/iss/translations/es.json b/homeassistant/components/iss/translations/es.json index a0456431ad8..6ca3e875615 100644 --- a/homeassistant/components/iss/translations/es.json +++ b/homeassistant/components/iss/translations/es.json @@ -5,9 +5,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "\u00bfMostrar en el mapa?" - }, "description": "\u00bfQuieres configurar Estaci\u00f3n Espacial Internacional (ISS)?" } } diff --git a/homeassistant/components/iss/translations/et.json b/homeassistant/components/iss/translations/et.json index 60385881b19..d298c57c64b 100644 --- a/homeassistant/components/iss/translations/et.json +++ b/homeassistant/components/iss/translations/et.json @@ -6,9 +6,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "Kas n\u00e4idata kaardil?" - }, "description": "Kas seadistada rahvusvahelise kosmosejaama (ISS) sidumist?" } } diff --git a/homeassistant/components/iss/translations/fr.json b/homeassistant/components/iss/translations/fr.json index 2f9ad5d427c..d1ef91aed9d 100644 --- a/homeassistant/components/iss/translations/fr.json +++ b/homeassistant/components/iss/translations/fr.json @@ -6,9 +6,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "Afficher sur la carte\u00a0?" - }, "description": "Voulez-vous configurer la Station spatiale internationale (ISS)\u00a0?" } } diff --git a/homeassistant/components/iss/translations/hu.json b/homeassistant/components/iss/translations/hu.json index bfe593e9592..5dd2d53d074 100644 --- a/homeassistant/components/iss/translations/hu.json +++ b/homeassistant/components/iss/translations/hu.json @@ -6,9 +6,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "Megjelenjen a t\u00e9rk\u00e9pen?" - }, "description": "Szeretn\u00e9 konfigur\u00e1lni a Nemzetk\u00f6zi \u0170r\u00e1llom\u00e1st (ISS)?" } } diff --git a/homeassistant/components/iss/translations/id.json b/homeassistant/components/iss/translations/id.json index c53287164ee..ddf2b1a4f71 100644 --- a/homeassistant/components/iss/translations/id.json +++ b/homeassistant/components/iss/translations/id.json @@ -6,9 +6,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "Tampilkan di peta?" - }, "description": "Ingin mengonfigurasi Stasiun Luar Angkasa Internasional (ISS)?" } } diff --git a/homeassistant/components/iss/translations/it.json b/homeassistant/components/iss/translations/it.json index b3ec1329eae..eaa43bc34aa 100644 --- a/homeassistant/components/iss/translations/it.json +++ b/homeassistant/components/iss/translations/it.json @@ -6,9 +6,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "Mostrare sulla mappa?" - }, "description": "Vuoi configurare la Stazione Spaziale Internazionale (ISS)?" } } diff --git a/homeassistant/components/iss/translations/ja.json b/homeassistant/components/iss/translations/ja.json index 40178b203f9..bf5deeaa716 100644 --- a/homeassistant/components/iss/translations/ja.json +++ b/homeassistant/components/iss/translations/ja.json @@ -6,9 +6,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "\u5730\u56f3\u306b\u8868\u793a\u3057\u307e\u3059\u304b\uff1f" - }, "description": "\u56fd\u969b\u5b87\u5b99\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u304b\uff1f" } } diff --git a/homeassistant/components/iss/translations/nl.json b/homeassistant/components/iss/translations/nl.json index 08a170b1ccc..cbe31ac6674 100644 --- a/homeassistant/components/iss/translations/nl.json +++ b/homeassistant/components/iss/translations/nl.json @@ -6,9 +6,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "Op kaart tonen?" - }, "description": "Wilt u International Space Station configureren?" } } diff --git a/homeassistant/components/iss/translations/no.json b/homeassistant/components/iss/translations/no.json index c204f5a9012..1999c50c8be 100644 --- a/homeassistant/components/iss/translations/no.json +++ b/homeassistant/components/iss/translations/no.json @@ -6,9 +6,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "Vis p\u00e5 kart?" - }, "description": "Vil du konfigurere den internasjonale romstasjonen (ISS)?" } } diff --git a/homeassistant/components/iss/translations/pl.json b/homeassistant/components/iss/translations/pl.json index 2cdf8ef4863..c7e39260365 100644 --- a/homeassistant/components/iss/translations/pl.json +++ b/homeassistant/components/iss/translations/pl.json @@ -6,9 +6,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "Pokaza\u0107 na mapie?" - }, "description": "Czy chcesz skonfigurowa\u0107 Mi\u0119dzynarodow\u0105 Stacj\u0119 Kosmiczn\u0105 (ISS)?" } } diff --git a/homeassistant/components/iss/translations/pt-BR.json b/homeassistant/components/iss/translations/pt-BR.json index c69fefed0c5..6618abe68f4 100644 --- a/homeassistant/components/iss/translations/pt-BR.json +++ b/homeassistant/components/iss/translations/pt-BR.json @@ -6,9 +6,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "Mostrar no mapa" - }, "description": "Deseja configurar a Esta\u00e7\u00e3o Espacial Internacional (ISS)?" } } @@ -17,7 +14,7 @@ "step": { "init": { "data": { - "show_on_map": "Mostrar no mapa" + "show_on_map": "[%key:component::iss::config::step::user::data::show_on_map%]" } } } diff --git a/homeassistant/components/iss/translations/ru.json b/homeassistant/components/iss/translations/ru.json index 64604c1f460..e0f965bd212 100644 --- a/homeassistant/components/iss/translations/ru.json +++ b/homeassistant/components/iss/translations/ru.json @@ -6,9 +6,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0435" - }, "description": "\u041d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 International Space Station (ISS)?" } } diff --git a/homeassistant/components/iss/translations/tr.json b/homeassistant/components/iss/translations/tr.json index 3cb92db229c..f647a6c02c2 100644 --- a/homeassistant/components/iss/translations/tr.json +++ b/homeassistant/components/iss/translations/tr.json @@ -6,9 +6,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "Haritada g\u00f6sterilsin mi?" - }, "description": "Uluslararas\u0131 Uzay \u0130stasyonunu (ISS) yap\u0131land\u0131rmak istiyor musunuz?" } } diff --git a/homeassistant/components/iss/translations/uk.json b/homeassistant/components/iss/translations/uk.json index cfcf3e0c458..ca7bf63af76 100644 --- a/homeassistant/components/iss/translations/uk.json +++ b/homeassistant/components/iss/translations/uk.json @@ -6,9 +6,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043d\u0430 \u043c\u0430\u043f\u0456?" - }, "description": "\u0427\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u041c\u0456\u0436\u043d\u0430\u0440\u043e\u0434\u043d\u0443 \u041a\u043e\u0441\u043c\u0456\u0447\u043d\u0443 \u0421\u0442\u0430\u043d\u0446\u0456\u044e?" } } diff --git a/homeassistant/components/iss/translations/zh-Hans.json b/homeassistant/components/iss/translations/zh-Hans.json deleted file mode 100644 index 47c25d7ddff..00000000000 --- a/homeassistant/components/iss/translations/zh-Hans.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "show_on_map": "\u5728\u5730\u56fe\u4e0a\u663e\u793a\uff1f" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/iss/translations/zh-Hant.json b/homeassistant/components/iss/translations/zh-Hant.json index f50730ff9a4..cdccbe14058 100644 --- a/homeassistant/components/iss/translations/zh-Hant.json +++ b/homeassistant/components/iss/translations/zh-Hant.json @@ -6,9 +6,6 @@ }, "step": { "user": { - "data": { - "show_on_map": "\u65bc\u5730\u5716\u986f\u793a\uff1f" - }, "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u570b\u969b\u592a\u7a7a\u7ad9\uff08ISS\uff09\uff1f" } } diff --git a/homeassistant/components/isy994/translations/ko.json b/homeassistant/components/isy994/translations/ko.json index 29829e066b9..8673aa7a148 100644 --- a/homeassistant/components/isy994/translations/ko.json +++ b/homeassistant/components/isy994/translations/ko.json @@ -7,10 +7,19 @@ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_host": "\ud638\uc2a4\ud2b8 \ud56d\ubaa9\uc774 \uc644\uc804\ud55c URL \ud615\uc2dd\uc774 \uc544\ub2d9\ub2c8\ub2e4. \uc608: http://192.168.10.100:80", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "flow_title": "ISY994 \ubc94\uc6a9 \uae30\uae30: {name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "{host}\uc5d0 \ub300\ud55c \uc790\uaca9 \uc99d\uba85\uc774 \ub354 \uc774\uc0c1 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "title": "ISY \uc7ac\uc778\uc99d" + }, "user": { "data": { "host": "URL \uc8fc\uc18c", diff --git a/homeassistant/components/kaleidescape/translations/bg.json b/homeassistant/components/kaleidescape/translations/bg.json index cb36ec53c4b..c31eea5d60e 100644 --- a/homeassistant/components/kaleidescape/translations/bg.json +++ b/homeassistant/components/kaleidescape/translations/bg.json @@ -11,14 +11,10 @@ }, "flow_title": "{model} ({name})", "step": { - "discovery_confirm": { - "title": "Kaleidescape" - }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" - }, - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u0430 Kaleidescape" + } } } } diff --git a/homeassistant/components/kaleidescape/translations/ca.json b/homeassistant/components/kaleidescape/translations/ca.json index ec7eac6ef4f..8a8df0f6503 100644 --- a/homeassistant/components/kaleidescape/translations/ca.json +++ b/homeassistant/components/kaleidescape/translations/ca.json @@ -13,14 +13,12 @@ "flow_title": "{model} ({name})", "step": { "discovery_confirm": { - "description": "Vols configurar el reproductor {name} model {model}?", - "title": "Kaleidescape" + "description": "Vols configurar el reproductor {name} model {model}?" }, "user": { "data": { "host": "Amfitri\u00f3" - }, - "title": "Configuraci\u00f3 de Kaleidescape" + } } } } diff --git a/homeassistant/components/kaleidescape/translations/de.json b/homeassistant/components/kaleidescape/translations/de.json index 3b353208a64..0d9a93f8596 100644 --- a/homeassistant/components/kaleidescape/translations/de.json +++ b/homeassistant/components/kaleidescape/translations/de.json @@ -13,14 +13,12 @@ "flow_title": "{model} ({name})", "step": { "discovery_confirm": { - "description": "M\u00f6chtest du den Player {model} mit dem Namen {name} einrichten?", - "title": "Kaleidescape" + "description": "M\u00f6chtest du den Player {model} mit dem Namen {name} einrichten?" }, "user": { "data": { "host": "Host" - }, - "title": "Kaleidescape-Setup" + } } } } diff --git a/homeassistant/components/kaleidescape/translations/el.json b/homeassistant/components/kaleidescape/translations/el.json index dc042655b02..e9315dd4cf7 100644 --- a/homeassistant/components/kaleidescape/translations/el.json +++ b/homeassistant/components/kaleidescape/translations/el.json @@ -13,14 +13,12 @@ "flow_title": "{model} ({name})", "step": { "discovery_confirm": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 {model} \u03bc\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1{name};", - "title": "Kaleidescape" + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 {model} \u03bc\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1{name};" }, "user": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" - }, - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 Kaleidescape" + } } } } diff --git a/homeassistant/components/kaleidescape/translations/en.json b/homeassistant/components/kaleidescape/translations/en.json index 43be9c030c0..cd6460e0fa9 100644 --- a/homeassistant/components/kaleidescape/translations/en.json +++ b/homeassistant/components/kaleidescape/translations/en.json @@ -13,14 +13,12 @@ "flow_title": "{model} ({name})", "step": { "discovery_confirm": { - "description": "Do you want to set up the {model} player named {name}?", - "title": "Kaleidescape" + "description": "Do you want to set up the {model} player named {name}?" }, "user": { "data": { "host": "Host" - }, - "title": "Kaleidescape Setup" + } } } } diff --git a/homeassistant/components/kaleidescape/translations/et.json b/homeassistant/components/kaleidescape/translations/et.json index 52ece37ad97..9da5e2a8a68 100644 --- a/homeassistant/components/kaleidescape/translations/et.json +++ b/homeassistant/components/kaleidescape/translations/et.json @@ -13,14 +13,12 @@ "flow_title": "{model} ({name})", "step": { "discovery_confirm": { - "description": "Kas seadistada m\u00e4ngijat {model} nimega {name} ?", - "title": "Kaleidescape" + "description": "Kas seadistada m\u00e4ngijat {model} nimega {name} ?" }, "user": { "data": { "host": "Host" - }, - "title": "Kaleidescape'i seadistamine" + } } } } diff --git a/homeassistant/components/kaleidescape/translations/fr.json b/homeassistant/components/kaleidescape/translations/fr.json index 48dfd027765..bf03d0ecacf 100644 --- a/homeassistant/components/kaleidescape/translations/fr.json +++ b/homeassistant/components/kaleidescape/translations/fr.json @@ -13,14 +13,12 @@ "flow_title": "{model} ({name})", "step": { "discovery_confirm": { - "description": "Voulez-vous configurer le lecteur {model} nomm\u00e9 {name}\u00a0?", - "title": "Kaleidescape" + "description": "Voulez-vous configurer le lecteur {model} nomm\u00e9 {name}\u00a0?" }, "user": { "data": { "host": "H\u00f4te" - }, - "title": "Configuration de Kaleidescape" + } } } } diff --git a/homeassistant/components/kaleidescape/translations/hu.json b/homeassistant/components/kaleidescape/translations/hu.json index cd8b00e18a3..ad26902cdf3 100644 --- a/homeassistant/components/kaleidescape/translations/hu.json +++ b/homeassistant/components/kaleidescape/translations/hu.json @@ -13,14 +13,12 @@ "flow_title": "{model} ({name})", "step": { "discovery_confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a {name} nev\u0171, {model} t\u00edpus\u00fa lej\u00e1tsz\u00f3t?", - "title": "Kaleidescape" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a {name} nev\u0171, {model} t\u00edpus\u00fa lej\u00e1tsz\u00f3t?" }, "user": { "data": { "host": "C\u00edm" - }, - "title": "Kaleidescape be\u00e1ll\u00edt\u00e1sa" + } } } } diff --git a/homeassistant/components/kaleidescape/translations/id.json b/homeassistant/components/kaleidescape/translations/id.json index 626f23354d4..7833bf5ad4c 100644 --- a/homeassistant/components/kaleidescape/translations/id.json +++ b/homeassistant/components/kaleidescape/translations/id.json @@ -13,14 +13,12 @@ "flow_title": "{model} ({name})", "step": { "discovery_confirm": { - "description": "Ingin menyiapkan pemutar {model} dengan nama {name}?", - "title": "Kaleidescape" + "description": "Ingin menyiapkan pemutar {model} dengan nama {name}?" }, "user": { "data": { "host": "Host" - }, - "title": "Penyiapan Kaleidescape" + } } } } diff --git a/homeassistant/components/kaleidescape/translations/it.json b/homeassistant/components/kaleidescape/translations/it.json index 022b73ae0d7..35235b2986b 100644 --- a/homeassistant/components/kaleidescape/translations/it.json +++ b/homeassistant/components/kaleidescape/translations/it.json @@ -13,14 +13,12 @@ "flow_title": "{model} ({name})", "step": { "discovery_confirm": { - "description": "Vuoi configurare il lettore {model} nome {name}?", - "title": "Kaleidescape" + "description": "Vuoi configurare il lettore {model} nome {name}?" }, "user": { "data": { "host": "Host" - }, - "title": "Configurazione di Kaleidescape" + } } } } diff --git a/homeassistant/components/kaleidescape/translations/ja.json b/homeassistant/components/kaleidescape/translations/ja.json index 368a1565648..b4d49c09f74 100644 --- a/homeassistant/components/kaleidescape/translations/ja.json +++ b/homeassistant/components/kaleidescape/translations/ja.json @@ -13,14 +13,12 @@ "flow_title": "{model} ({name})", "step": { "discovery_confirm": { - "description": "{model} \u30d7\u30ec\u30a4\u30e4\u30fc\u540d {name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f", - "title": "\u30ab\u30ec\u30a4\u30c9\u30b9\u30b1\u30fc\u30d7(Kaleidescape)" + "description": "{model} \u30d7\u30ec\u30a4\u30e4\u30fc\u540d {name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, "user": { "data": { "host": "\u30db\u30b9\u30c8" - }, - "title": "\u30ab\u30ec\u30a4\u30c9\u30b9\u30b1\u30fc\u30d7(Kaleidescape)\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } } } } diff --git a/homeassistant/components/kaleidescape/translations/nl.json b/homeassistant/components/kaleidescape/translations/nl.json index 2b2491deb85..b3498db12a3 100644 --- a/homeassistant/components/kaleidescape/translations/nl.json +++ b/homeassistant/components/kaleidescape/translations/nl.json @@ -13,14 +13,12 @@ "flow_title": "{model} ({name})", "step": { "discovery_confirm": { - "description": "Wil je de speler {model} met de naam {name} instellen?", - "title": "Kaleidescape" + "description": "Wil je de speler {model} met de naam {name} instellen?" }, "user": { "data": { "host": "Host" - }, - "title": "Kaleidescape configuratie" + } } } } diff --git a/homeassistant/components/kaleidescape/translations/no.json b/homeassistant/components/kaleidescape/translations/no.json index c07276eeeb1..6d3450c0b68 100644 --- a/homeassistant/components/kaleidescape/translations/no.json +++ b/homeassistant/components/kaleidescape/translations/no.json @@ -13,14 +13,12 @@ "flow_title": "{model} ( {name} )", "step": { "discovery_confirm": { - "description": "Vil du sette opp {model} -spilleren med navnet {name} ?", - "title": "Kaleidescape" + "description": "Vil du sette opp {model} -spilleren med navnet {name} ?" }, "user": { "data": { "host": "Vert" - }, - "title": "Kaleidescape-oppsett" + } } } } diff --git a/homeassistant/components/kaleidescape/translations/pl.json b/homeassistant/components/kaleidescape/translations/pl.json index 111dc4db7e6..499f813aa27 100644 --- a/homeassistant/components/kaleidescape/translations/pl.json +++ b/homeassistant/components/kaleidescape/translations/pl.json @@ -13,14 +13,12 @@ "flow_title": "{model} ({name})", "step": { "discovery_confirm": { - "description": "Czy chcesz skonfigurowa\u0107 odtwarzacz {model} o nazwie {name}?", - "title": "Kaleidescape" + "description": "Czy chcesz skonfigurowa\u0107 odtwarzacz {model} o nazwie {name}?" }, "user": { "data": { "host": "Nazwa hosta lub adres IP" - }, - "title": "Konfiguracja Kaleidescape" + } } } } diff --git a/homeassistant/components/kaleidescape/translations/pt-BR.json b/homeassistant/components/kaleidescape/translations/pt-BR.json index f87534cb88a..5b5c4a35d2b 100644 --- a/homeassistant/components/kaleidescape/translations/pt-BR.json +++ b/homeassistant/components/kaleidescape/translations/pt-BR.json @@ -13,14 +13,12 @@ "flow_title": "{model} ({name})", "step": { "discovery_confirm": { - "description": "Deseja configurar o player {model} chamado {name}?", - "title": "Kaleidescape" + "description": "Deseja configurar o player {model} chamado {name}?" }, "user": { "data": { "host": "Nome do host" - }, - "title": "Configura\u00e7\u00e3o do Kaleidescape" + } } } } diff --git a/homeassistant/components/kaleidescape/translations/ru.json b/homeassistant/components/kaleidescape/translations/ru.json index 3112fdd06e5..d94ffb58900 100644 --- a/homeassistant/components/kaleidescape/translations/ru.json +++ b/homeassistant/components/kaleidescape/translations/ru.json @@ -13,14 +13,12 @@ "flow_title": "{model} ({name})", "step": { "discovery_confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c {model} ({name})?", - "title": "Kaleidescape" + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c {model} ({name})?" }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" - }, - "title": "Kaleidescape" + } } } } diff --git a/homeassistant/components/kaleidescape/translations/tr.json b/homeassistant/components/kaleidescape/translations/tr.json index 1b891cc450c..40b004c2ea2 100644 --- a/homeassistant/components/kaleidescape/translations/tr.json +++ b/homeassistant/components/kaleidescape/translations/tr.json @@ -13,14 +13,12 @@ "flow_title": "{model} ({name})", "step": { "discovery_confirm": { - "description": "{name} adl\u0131 {model} oynat\u0131c\u0131s\u0131n\u0131 kurmak istiyor musunuz?", - "title": "Kaleidescape" + "description": "{name} adl\u0131 {model} oynat\u0131c\u0131s\u0131n\u0131 kurmak istiyor musunuz?" }, "user": { "data": { "host": "Sunucu" - }, - "title": "Kaleidescape Kurulumu" + } } } } diff --git a/homeassistant/components/kaleidescape/translations/zh-Hant.json b/homeassistant/components/kaleidescape/translations/zh-Hant.json index 2fedf1ede20..4e6c5564c61 100644 --- a/homeassistant/components/kaleidescape/translations/zh-Hant.json +++ b/homeassistant/components/kaleidescape/translations/zh-Hant.json @@ -13,14 +13,12 @@ "flow_title": "{model} ({name})", "step": { "discovery_confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u540d\u70ba {name} \u7684 {model} \u64ad\u653e\u5668\uff1f", - "title": "Kaleidescape" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u540d\u70ba {name} \u7684 {model} \u64ad\u653e\u5668\uff1f" }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef" - }, - "title": "Kaleidescape \u8a2d\u5b9a" + } } } } diff --git a/homeassistant/components/knx/translations/bg.json b/homeassistant/components/knx/translations/bg.json index 3cbc8b57b0b..331c85d4065 100644 --- a/homeassistant/components/knx/translations/bg.json +++ b/homeassistant/components/knx/translations/bg.json @@ -49,7 +49,6 @@ "tunnel": { "data": { "host": "\u0425\u043e\u0441\u0442", - "local_ip": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, \u0430\u043a\u043e \u043d\u0435 \u0441\u0442\u0435 \u0441\u0438\u0433\u0443\u0440\u043d\u0438)", "port": "\u041f\u043e\u0440\u0442", "tunneling_type": "KNX \u0442\u0443\u043d\u0435\u043b\u0435\u043d \u0442\u0438\u043f" } diff --git a/homeassistant/components/knx/translations/ca.json b/homeassistant/components/knx/translations/ca.json index b79887e1586..ff83ebec070 100644 --- a/homeassistant/components/knx/translations/ca.json +++ b/homeassistant/components/knx/translations/ca.json @@ -15,10 +15,8 @@ "manual_tunnel": { "data": { "host": "Amfitri\u00f3", - "individual_address": "Adre\u00e7a individual de la connexi\u00f3", "local_ip": "IP local de Home Assistant", "port": "Port", - "route_back": "Encaminament de retorn / Mode NAT", "tunneling_type": "Tipus de t\u00fanel KNX" }, "data_description": { @@ -104,15 +102,13 @@ "multicast_group": "Utilitzada per a l'encaminament i el descobriment. Per defecte: `224.0.23.12`", "multicast_port": "Utilitzat per a l'encaminament i el descobriment. Per defecte: `3671`", "rate_limit": "Telegrames de sortida m\u00e0xims per segon.\nRecomanat: de 20 a 40", - "state_updater": "Activa o desactiva globalment la lectura d'estats del bus KNX. Si est\u00e0 desactivat, Home Assistant no obtindr\u00e0 activament els estats del bus KNX, les opcions d'entitat `sync_state` no tindran cap efecte." + "state_updater": "Configuraci\u00f3 predeterminadament per llegir els estats del bus KNX. Si est\u00e0 desactivat, Home Assistant no obtindr\u00e0 activament els estats del bus KNX. Les opcions d'entitat `sync_state` poden substituir-ho." } }, "tunnel": { "data": { "host": "Amfitri\u00f3", - "local_ip": "IP local (deixa-ho en blanc si no n'est\u00e0s segur/a)", "port": "Port", - "route_back": "Encaminament de retorn / Mode NAT", "tunneling_type": "Tipus de t\u00fanel KNX" }, "data_description": { diff --git a/homeassistant/components/knx/translations/de.json b/homeassistant/components/knx/translations/de.json index 588a853a8b6..1daffa9c301 100644 --- a/homeassistant/components/knx/translations/de.json +++ b/homeassistant/components/knx/translations/de.json @@ -15,10 +15,8 @@ "manual_tunnel": { "data": { "host": "Host", - "individual_address": "Physikalische Adresse f\u00fcr die Verbindung", "local_ip": "Lokale IP von Home Assistant", "port": "Port", - "route_back": "Route Back / NAT-Modus", "tunneling_type": "KNX Tunneling Typ" }, "data_description": { @@ -104,15 +102,13 @@ "multicast_group": "Wird f\u00fcr Routing und Netzwerkerkennung verwendet. Standard: `224.0.23.12`", "multicast_port": "Wird f\u00fcr Routing und Netzwerkerkennung verwendet. Standard: \u201e3671\u201c.", "rate_limit": "Maximal gesendete Telegramme pro Sekunde.\nEmpfohlen: 20 bis 40", - "state_updater": "Aktiviere oder deaktiviere global das Lesen von Zust\u00e4nden vom KNX-Bus. Wenn deaktiviert, ruft Home Assistant nicht aktiv Zust\u00e4nde vom KNX-Bus ab, \u201esync_state\u201c-Entity-Optionen haben keine Auswirkung." + "state_updater": "Standardeinstellung f\u00fcr das Lesen von Zust\u00e4nden aus dem KNX-Bus. Wenn diese Option deaktiviert ist, wird der Home Assistant den Zustand der Entit\u00e4ten nicht aktiv vom KNX-Bus abrufen. Kann durch die Entity-Optionen `sync_state` au\u00dfer Kraft gesetzt werden." } }, "tunnel": { "data": { "host": "Host", - "local_ip": "Lokale IP (im Zweifel leer lassen)", "port": "Port", - "route_back": "Route Back / NAT-Modus", "tunneling_type": "KNX Tunneling Typ" }, "data_description": { diff --git a/homeassistant/components/knx/translations/el.json b/homeassistant/components/knx/translations/el.json index 81dd03f73fa..30047781c26 100644 --- a/homeassistant/components/knx/translations/el.json +++ b/homeassistant/components/knx/translations/el.json @@ -15,10 +15,8 @@ "manual_tunnel": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", - "individual_address": "\u0391\u03c4\u03bf\u03bc\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7", "local_ip": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae IP \u03c4\u03bf\u03c5 Home Assistant (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b5\u03bd\u03ae \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7)", "port": "\u0398\u03cd\u03c1\u03b1", - "route_back": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 Route Back / NAT", "tunneling_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX" }, "data_description": { @@ -110,9 +108,7 @@ "tunnel": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", - "local_ip": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae IP (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b5\u03bd\u03ae \u03b1\u03bd \u03b4\u03b5\u03bd \u03b5\u03af\u03c3\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03bf\u03b9)", "port": "\u0398\u03cd\u03c1\u03b1", - "route_back": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 Route Back / NAT", "tunneling_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX" }, "data_description": { diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index fd8ac9499f1..6dffe059b2a 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -15,10 +15,8 @@ "manual_tunnel": { "data": { "host": "Host", - "individual_address": "Individual address for the connection", "local_ip": "Local IP of Home Assistant", "port": "Port", - "route_back": "Route Back / NAT Mode", "tunneling_type": "KNX Tunneling Type" }, "data_description": { @@ -110,9 +108,7 @@ "tunnel": { "data": { "host": "Host", - "local_ip": "Local IP (leave empty if unsure)", "port": "Port", - "route_back": "Route Back / NAT Mode", "tunneling_type": "KNX Tunneling Type" }, "data_description": { diff --git a/homeassistant/components/knx/translations/es.json b/homeassistant/components/knx/translations/es.json index 4e1c3130d64..6c8ec3e2eb6 100644 --- a/homeassistant/components/knx/translations/es.json +++ b/homeassistant/components/knx/translations/es.json @@ -13,10 +13,8 @@ "manual_tunnel": { "data": { "host": "Host", - "individual_address": "Direcci\u00f3n individual para la conexi\u00f3n", "local_ip": "IP local de Home Assistant", - "port": "Puerto", - "route_back": "Modo Route Back / NAT" + "port": "Puerto" }, "data_description": { "local_ip": "D\u00e9jalo en blanco para utilizar el descubrimiento autom\u00e1tico.", @@ -91,9 +89,7 @@ "tunnel": { "data": { "host": "Host", - "local_ip": "IP local (d\u00e9jelo en blanco si no est\u00e1 seguro)", - "port": "Puerto", - "route_back": "Modo Route Back / NAT" + "port": "Puerto" }, "data_description": { "host": "Direcci\u00f3n IP del dispositivo de tunelizaci\u00f3n KNX/IP.", diff --git a/homeassistant/components/knx/translations/et.json b/homeassistant/components/knx/translations/et.json index b7cd079ad3f..fe60f5404de 100644 --- a/homeassistant/components/knx/translations/et.json +++ b/homeassistant/components/knx/translations/et.json @@ -15,10 +15,8 @@ "manual_tunnel": { "data": { "host": "Host", - "individual_address": "\u00dchenduse individuaalne aadress", "local_ip": "Home Assistanti kohalik IP aadress", "port": "Port", - "route_back": "Marsruudi tagasitee / NAT-re\u017eiim", "tunneling_type": "KNX tunneli t\u00fc\u00fcp" }, "data_description": { @@ -110,9 +108,7 @@ "tunnel": { "data": { "host": "Host", - "local_ip": "Kohalik IP (j\u00e4ta t\u00fchjaks, kui ei ole kindel)", "port": "Port", - "route_back": "Marsruudi tagasitee / NAT-re\u017eiim", "tunneling_type": "KNX tunneli t\u00fc\u00fcp" }, "data_description": { diff --git a/homeassistant/components/knx/translations/fr.json b/homeassistant/components/knx/translations/fr.json index c75fd76debb..803fd71b734 100644 --- a/homeassistant/components/knx/translations/fr.json +++ b/homeassistant/components/knx/translations/fr.json @@ -15,10 +15,8 @@ "manual_tunnel": { "data": { "host": "H\u00f4te", - "individual_address": "Adresse individuelle pour la connexion", "local_ip": "IP locale de Home Assistant", "port": "Port", - "route_back": "Retour/Mode NAT", "tunneling_type": "Type de tunnel KNX" }, "data_description": { @@ -110,9 +108,7 @@ "tunnel": { "data": { "host": "H\u00f4te", - "local_ip": "IP locale (laisser vide en cas de doute)", "port": "Port", - "route_back": "Retour/Mode NAT", "tunneling_type": "Type de tunnel KNX" }, "data_description": { diff --git a/homeassistant/components/knx/translations/hu.json b/homeassistant/components/knx/translations/hu.json index 02b8b0a0465..ccb9eb63c61 100644 --- a/homeassistant/components/knx/translations/hu.json +++ b/homeassistant/components/knx/translations/hu.json @@ -15,10 +15,8 @@ "manual_tunnel": { "data": { "host": "C\u00edm", - "individual_address": "A kapcsolat egy\u00e9ni c\u00edme", "local_ip": "Home Assistant lok\u00e1lis IP c\u00edme", "port": "Port", - "route_back": "\u00datvonal (route) vissza / NAT m\u00f3d", "tunneling_type": "KNX alag\u00fat t\u00edpusa" }, "data_description": { @@ -110,9 +108,7 @@ "tunnel": { "data": { "host": "C\u00edm", - "local_ip": "Helyi IP c\u00edm (hagyja \u00fcresen, ha nem biztos benne)", "port": "Port", - "route_back": "\u00datvonal (route) vissza / NAT m\u00f3d", "tunneling_type": "KNX alag\u00fat t\u00edpusa" }, "data_description": { diff --git a/homeassistant/components/knx/translations/id.json b/homeassistant/components/knx/translations/id.json index e2ca98fff94..4c55a3ef392 100644 --- a/homeassistant/components/knx/translations/id.json +++ b/homeassistant/components/knx/translations/id.json @@ -15,10 +15,8 @@ "manual_tunnel": { "data": { "host": "Host", - "individual_address": "Alamat individu untuk koneksi", "local_ip": "IP lokal Home Assistant", "port": "Port", - "route_back": "Dirutekan Kembali/Mode NAT", "tunneling_type": "Jenis Tunnel KNX" }, "data_description": { @@ -110,9 +108,7 @@ "tunnel": { "data": { "host": "Host", - "local_ip": "IP lokal (kosongkan jika tidak yakin)", "port": "Port", - "route_back": "Dirutekan Kembali/Mode NAT", "tunneling_type": "Jenis Tunnel KNX" }, "data_description": { diff --git a/homeassistant/components/knx/translations/it.json b/homeassistant/components/knx/translations/it.json index ac780ea96ab..8c2c58ad2d5 100644 --- a/homeassistant/components/knx/translations/it.json +++ b/homeassistant/components/knx/translations/it.json @@ -15,10 +15,8 @@ "manual_tunnel": { "data": { "host": "Host", - "individual_address": "Indirizzo individuale per la connessione", "local_ip": "IP locale di Home Assistant", "port": "Porta", - "route_back": "Torna indietro / Modalit\u00e0 NAT", "tunneling_type": "Tipo tunnel KNX" }, "data_description": { @@ -110,9 +108,7 @@ "tunnel": { "data": { "host": "Host", - "local_ip": "IP locale (lascia vuoto se non sei sicuro)", "port": "Porta", - "route_back": "Torna indietro / Modalit\u00e0 NAT", "tunneling_type": "Tipo tunnel KNX" }, "data_description": { diff --git a/homeassistant/components/knx/translations/ja.json b/homeassistant/components/knx/translations/ja.json index 475958af9a0..dcd44d5838f 100644 --- a/homeassistant/components/knx/translations/ja.json +++ b/homeassistant/components/knx/translations/ja.json @@ -15,10 +15,8 @@ "manual_tunnel": { "data": { "host": "\u30db\u30b9\u30c8", - "individual_address": "\u63a5\u7d9a\u7528\u306e\u500b\u5225\u30a2\u30c9\u30ec\u30b9", "local_ip": "\u30ed\u30fc\u30ab\u30ebIP(\u4e0d\u660e\u306a\u5834\u5408\u306f\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", "port": "\u30dd\u30fc\u30c8", - "route_back": "\u30eb\u30fc\u30c8\u30d0\u30c3\u30af / NAT\u30e2\u30fc\u30c9", "tunneling_type": "KNX\u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u30bf\u30a4\u30d7" }, "data_description": { @@ -110,9 +108,7 @@ "tunnel": { "data": { "host": "\u30db\u30b9\u30c8", - "local_ip": "\u30ed\u30fc\u30ab\u30ebIP(\u4e0d\u660e\u306a\u5834\u5408\u306f\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u3066\u304f\u3060\u3055\u3044)", "port": "\u30dd\u30fc\u30c8", - "route_back": "\u30eb\u30fc\u30c8\u30d0\u30c3\u30af / NAT\u30e2\u30fc\u30c9", "tunneling_type": "KNX\u30c8\u30f3\u30cd\u30ea\u30f3\u30b0\u30bf\u30a4\u30d7" }, "data_description": { diff --git a/homeassistant/components/knx/translations/ko.json b/homeassistant/components/knx/translations/ko.json index 676037b15e4..0762b130455 100644 --- a/homeassistant/components/knx/translations/ko.json +++ b/homeassistant/components/knx/translations/ko.json @@ -1,10 +1,8 @@ { "config": { "step": { - "manual_tunnel": { - "data": { - "route_back": "\ub77c\uc6b0\ud2b8 \ubc31 / NAT \ubaa8\ub4dc" - } + "secure_manual": { + "description": "IP \ubcf4\uc548 \uc815\ubcf4\ub97c \uc785\ub825\ud558\uc138\uc694." } } } diff --git a/homeassistant/components/knx/translations/nl.json b/homeassistant/components/knx/translations/nl.json index f40b9d7729f..4c342d2741b 100644 --- a/homeassistant/components/knx/translations/nl.json +++ b/homeassistant/components/knx/translations/nl.json @@ -15,10 +15,8 @@ "manual_tunnel": { "data": { "host": "Host", - "individual_address": "Individueel adres voor de verbinding", "local_ip": "Lokale IP van Home Assistant", "port": "Poort", - "route_back": "Route Back / NAT Mode", "tunneling_type": "KNX Tunneling Type" }, "data_description": { @@ -110,9 +108,7 @@ "tunnel": { "data": { "host": "Host", - "local_ip": "Lokaal IP (laat leeg indien niet zeker)", "port": "Poort", - "route_back": "Route Back / NAT Mode", "tunneling_type": "KNX Tunneling Type" }, "data_description": { diff --git a/homeassistant/components/knx/translations/no.json b/homeassistant/components/knx/translations/no.json index f5d03e3160c..596071695b4 100644 --- a/homeassistant/components/knx/translations/no.json +++ b/homeassistant/components/knx/translations/no.json @@ -15,10 +15,8 @@ "manual_tunnel": { "data": { "host": "Vert", - "individual_address": "Individuell adresse for tilkoblingen", "local_ip": "Lokal IP for hjemmeassistent", "port": "Port", - "route_back": "Rute tilbake / NAT-modus", "tunneling_type": "KNX tunneltype" }, "data_description": { @@ -104,15 +102,13 @@ "multicast_group": "Brukes til ruting og oppdagelse. Standard: `224.0.23.12`", "multicast_port": "Brukes til ruting og oppdagelse. Standard: `3671`", "rate_limit": "Maksimalt utg\u00e5ende telegrammer per sekund.\n Anbefalt: 20 til 40", - "state_updater": "Globalt aktiver eller deaktiver lesetilstander fra KNX-bussen. N\u00e5r den er deaktivert, vil ikke Home Assistant aktivt hente statuser fra KNX-bussen, \"sync_state\"-enhetsalternativer vil ikke ha noen effekt." + "state_updater": "Sett standard for lesing av tilstander fra KNX-bussen. N\u00e5r den er deaktivert, vil ikke Home Assistant aktivt hente enhetstilstander fra KNX-bussen. Kan overstyres av entitetsalternativer for \"sync_state\"." } }, "tunnel": { "data": { "host": "Vert", - "local_ip": "Lokal IP (la st\u00e5 tomt hvis du er usikker)", "port": "Port", - "route_back": "Rute tilbake / NAT-modus", "tunneling_type": "KNX tunneltype" }, "data_description": { diff --git a/homeassistant/components/knx/translations/pl.json b/homeassistant/components/knx/translations/pl.json index e0821090f29..d972d9b7061 100644 --- a/homeassistant/components/knx/translations/pl.json +++ b/homeassistant/components/knx/translations/pl.json @@ -15,10 +15,8 @@ "manual_tunnel": { "data": { "host": "Nazwa hosta lub adres IP", - "individual_address": "Indywidualny adres dla po\u0142\u0105czenia", "local_ip": "Lokalny adres IP Home Assistanta", "port": "Port", - "route_back": "Tryb Route Back / NAT", "tunneling_type": "Typ tunelowania KNX" }, "data_description": { @@ -110,9 +108,7 @@ "tunnel": { "data": { "host": "Nazwa hosta lub adres IP", - "local_ip": "Lokalny adres IP (pozostaw pusty, je\u015bli nie masz pewno\u015bci)", "port": "Port", - "route_back": "Tryb Route Back / NAT", "tunneling_type": "Typ tunelowania KNX" }, "data_description": { diff --git a/homeassistant/components/knx/translations/pt-BR.json b/homeassistant/components/knx/translations/pt-BR.json index 950cedc0721..dcdf057493b 100644 --- a/homeassistant/components/knx/translations/pt-BR.json +++ b/homeassistant/components/knx/translations/pt-BR.json @@ -15,10 +15,8 @@ "manual_tunnel": { "data": { "host": "Nome do host", - "individual_address": "Endere\u00e7o individual para a conex\u00e3o", "local_ip": "IP local do Home Assistant", "port": "Porta", - "route_back": "Modo Rota de Retorno / NAT", "tunneling_type": "Tipo de t\u00fanel KNX" }, "data_description": { @@ -104,15 +102,13 @@ "multicast_group": "Usado para roteamento e descoberta. Padr\u00e3o: `224.0.23.12`", "multicast_port": "Usado para roteamento e descoberta. Padr\u00e3o: `3671`", "rate_limit": "M\u00e1ximo de telegramas de sa\u00edda por segundo.\n Recomendado: 20 a 40", - "state_updater": "Habilite ou desabilite globalmente os estados de leitura do barramento KNX. Quando desativado, o Home Assistant n\u00e3o recuperar\u00e1 ativamente os estados do barramento KNX, as op\u00e7\u00f5es de entidade `sync_state` n\u00e3o ter\u00e3o efeito." + "state_updater": "Defina o padr\u00e3o para estados de leitura do barramento KNX. Quando desativado, o Home Assistant n\u00e3o recuperar\u00e1 ativamente os estados de entidade do barramento KNX. Pode ser substitu\u00eddo pelas op\u00e7\u00f5es de entidade `sync_state`." } }, "tunnel": { "data": { "host": "Nome do host", - "local_ip": "IP local (deixe em branco se n\u00e3o tiver certeza)", "port": "Porta", - "route_back": "Modo Rota de Retorno / NAT", "tunneling_type": "Tipo de t\u00fanel KNX" }, "data_description": { diff --git a/homeassistant/components/knx/translations/ru.json b/homeassistant/components/knx/translations/ru.json index 14b9f919aa2..1ca87d9ac80 100644 --- a/homeassistant/components/knx/translations/ru.json +++ b/homeassistant/components/knx/translations/ru.json @@ -15,10 +15,8 @@ "manual_tunnel": { "data": { "host": "\u0425\u043e\u0441\u0442", - "individual_address": "\u0418\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 Home Assistant", "port": "\u041f\u043e\u0440\u0442", - "route_back": "\u041e\u0431\u0440\u0430\u0442\u043d\u044b\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 / \u0440\u0435\u0436\u0438\u043c NAT", "tunneling_type": "\u0422\u0438\u043f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX" }, "data_description": { @@ -110,9 +108,7 @@ "tunnel": { "data": { "host": "\u0425\u043e\u0441\u0442", - "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 (\u0435\u0441\u043b\u0438 \u043d\u0435 \u0437\u043d\u0430\u0435\u0442\u0435, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c)", "port": "\u041f\u043e\u0440\u0442", - "route_back": "\u041e\u0431\u0440\u0430\u0442\u043d\u044b\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 / \u0440\u0435\u0436\u0438\u043c NAT", "tunneling_type": "\u0422\u0438\u043f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX" }, "data_description": { diff --git a/homeassistant/components/knx/translations/sl.json b/homeassistant/components/knx/translations/sl.json index 2e32080bfa0..d44b7dbd9cb 100644 --- a/homeassistant/components/knx/translations/sl.json +++ b/homeassistant/components/knx/translations/sl.json @@ -11,7 +11,6 @@ "manual_tunnel": { "data": { "host": "Gostitelj", - "individual_address": "Posamezni naslov za povezavo", "port": "Vrata" } }, diff --git a/homeassistant/components/knx/translations/tr.json b/homeassistant/components/knx/translations/tr.json index 5a2863baaf4..6ed12e70aee 100644 --- a/homeassistant/components/knx/translations/tr.json +++ b/homeassistant/components/knx/translations/tr.json @@ -15,10 +15,8 @@ "manual_tunnel": { "data": { "host": "Sunucu", - "individual_address": "Ba\u011flant\u0131 i\u00e7in bireysel adres", "local_ip": "Home Asistan\u0131n\u0131n Yerel IP'si", "port": "Port", - "route_back": "Geri Y\u00f6nlendirme / NAT Modu", "tunneling_type": "KNX T\u00fcnel Tipi" }, "data_description": { @@ -110,9 +108,7 @@ "tunnel": { "data": { "host": "Sunucu", - "local_ip": "Yerel IP (emin de\u011filseniz bo\u015f b\u0131rak\u0131n)", "port": "Port", - "route_back": "Geri Y\u00f6nlendirme / NAT Modu", "tunneling_type": "KNX T\u00fcnel Tipi" }, "data_description": { diff --git a/homeassistant/components/knx/translations/zh-Hant.json b/homeassistant/components/knx/translations/zh-Hant.json index 613cf4f74e5..b348f38701d 100644 --- a/homeassistant/components/knx/translations/zh-Hant.json +++ b/homeassistant/components/knx/translations/zh-Hant.json @@ -15,10 +15,8 @@ "manual_tunnel": { "data": { "host": "\u4e3b\u6a5f\u7aef", - "individual_address": "\u9023\u7dda\u500b\u5225\u4f4d\u5740", "local_ip": "Home Assistant \u672c\u5730\u7aef IP", "port": "\u901a\u8a0a\u57e0", - "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f", "tunneling_type": "KNX \u901a\u9053\u985e\u5225" }, "data_description": { @@ -110,9 +108,7 @@ "tunnel": { "data": { "host": "\u4e3b\u6a5f\u7aef", - "local_ip": "\u672c\u5730\u7aef IP\uff08\u5047\u5982\u4e0d\u78ba\u5b9a\uff0c\u4fdd\u7559\u7a7a\u767d\uff09", "port": "\u901a\u8a0a\u57e0", - "route_back": "\u8def\u7531\u8fd4\u56de / NAT \u6a21\u5f0f", "tunneling_type": "KNX \u901a\u9053\u985e\u5225" }, "data_description": { diff --git a/homeassistant/components/kraken/translations/ko.json b/homeassistant/components/kraken/translations/ko.json new file mode 100644 index 00000000000..758f3336cd4 --- /dev/null +++ b/homeassistant/components/kraken/translations/ko.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/translations/ca.json b/homeassistant/components/light/translations/ca.json index 6ceae0d5100..2c59305e4d9 100644 --- a/homeassistant/components/light/translations/ca.json +++ b/homeassistant/components/light/translations/ca.json @@ -14,7 +14,6 @@ }, "trigger_type": { "changed_states": "{entity_name} s'enc\u00e9n o s'apaga", - "toggled": "{entity_name} s'enc\u00e9n o s'apaga", "turned_off": "{entity_name} apagat", "turned_on": "{entity_name} enc\u00e8s" } diff --git a/homeassistant/components/light/translations/cs.json b/homeassistant/components/light/translations/cs.json index 0a4294a44f3..5f43c684b09 100644 --- a/homeassistant/components/light/translations/cs.json +++ b/homeassistant/components/light/translations/cs.json @@ -13,7 +13,6 @@ "is_on": "{entity_name} je zapnuto" }, "trigger_type": { - "toggled": "{entity_name} zapnuto nebo vypnuto", "turned_off": "{entity_name} vypnuto", "turned_on": "{entity_name} zapnuto" } diff --git a/homeassistant/components/light/translations/de.json b/homeassistant/components/light/translations/de.json index a214f1cb9cf..f1ac946b64a 100644 --- a/homeassistant/components/light/translations/de.json +++ b/homeassistant/components/light/translations/de.json @@ -14,7 +14,6 @@ }, "trigger_type": { "changed_states": "{entity_name} ein- oder ausgeschaltet", - "toggled": "{entity_name} ein- oder ausgeschaltet", "turned_off": "{entity_name} ausgeschaltet", "turned_on": "{entity_name} eingeschaltet" } diff --git a/homeassistant/components/light/translations/el.json b/homeassistant/components/light/translations/el.json index dbba423dd2a..16198683deb 100644 --- a/homeassistant/components/light/translations/el.json +++ b/homeassistant/components/light/translations/el.json @@ -14,7 +14,6 @@ }, "trigger_type": { "changed_states": "\u03a4\u03bf {entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5 \u03ae \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", - "toggled": "{entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5 \u03ae \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", "turned_off": "{entity_name} \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", "turned_on": "{entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5" } diff --git a/homeassistant/components/light/translations/en.json b/homeassistant/components/light/translations/en.json index bdaa8a87b84..526f902b158 100644 --- a/homeassistant/components/light/translations/en.json +++ b/homeassistant/components/light/translations/en.json @@ -14,7 +14,6 @@ }, "trigger_type": { "changed_states": "{entity_name} turned on or off", - "toggled": "{entity_name} turned on or off", "turned_off": "{entity_name} turned off", "turned_on": "{entity_name} turned on" } diff --git a/homeassistant/components/light/translations/es.json b/homeassistant/components/light/translations/es.json index 10e8dfa3d17..53cf50215aa 100644 --- a/homeassistant/components/light/translations/es.json +++ b/homeassistant/components/light/translations/es.json @@ -14,7 +14,6 @@ }, "trigger_type": { "changed_states": "{entity_name} activado o desactivado", - "toggled": "{entity_name} activado o desactivado", "turned_off": "{entity_name} apagada", "turned_on": "{entity_name} encendida" } diff --git a/homeassistant/components/light/translations/et.json b/homeassistant/components/light/translations/et.json index 05d1a7690fc..224ec559bb6 100644 --- a/homeassistant/components/light/translations/et.json +++ b/homeassistant/components/light/translations/et.json @@ -14,7 +14,6 @@ }, "trigger_type": { "changed_states": "{entity_name} l\u00fclitus sisse v\u00f5i v\u00e4lja", - "toggled": "{entity_name} l\u00fclitus sisse v\u00f5i v\u00e4lja", "turned_off": "{entity_name} l\u00fclitus v\u00e4lja", "turned_on": "{entity_name} l\u00fclitus sisse" } diff --git a/homeassistant/components/light/translations/fr.json b/homeassistant/components/light/translations/fr.json index bcb6cac594e..c74bd8507a0 100644 --- a/homeassistant/components/light/translations/fr.json +++ b/homeassistant/components/light/translations/fr.json @@ -14,7 +14,6 @@ }, "trigger_type": { "changed_states": "{entity_name} a \u00e9t\u00e9 allum\u00e9e ou \u00e9teinte", - "toggled": "{entity_name} a \u00e9t\u00e9 allum\u00e9e ou \u00e9teinte", "turned_off": "{entity_name} a \u00e9t\u00e9 \u00e9teinte", "turned_on": "{entity_name} a \u00e9t\u00e9 allum\u00e9e" } diff --git a/homeassistant/components/light/translations/he.json b/homeassistant/components/light/translations/he.json index b802e64ad40..2ebb31d089e 100644 --- a/homeassistant/components/light/translations/he.json +++ b/homeassistant/components/light/translations/he.json @@ -14,7 +14,6 @@ }, "trigger_type": { "changed_states": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc \u05d0\u05d5 \u05db\u05d5\u05d1\u05d4", - "toggled": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc \u05d0\u05d5 \u05db\u05d5\u05d1\u05d4", "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4", "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc" } diff --git a/homeassistant/components/light/translations/hu.json b/homeassistant/components/light/translations/hu.json index 986fd5d1787..73bf81858b0 100644 --- a/homeassistant/components/light/translations/hu.json +++ b/homeassistant/components/light/translations/hu.json @@ -14,7 +14,6 @@ }, "trigger_type": { "changed_states": "{entity_name} be- vagy kikapcsolt", - "toggled": "{entity_name} \u00e1tkapcsolt", "turned_off": "{entity_name} le lett kapcsolva", "turned_on": "{entity_name} fel lett kapcsolva" } diff --git a/homeassistant/components/light/translations/id.json b/homeassistant/components/light/translations/id.json index 334e938d41e..600644a36c4 100644 --- a/homeassistant/components/light/translations/id.json +++ b/homeassistant/components/light/translations/id.json @@ -14,7 +14,6 @@ }, "trigger_type": { "changed_states": "{entity_name} diaktifkan atau dinonaktifkan", - "toggled": "{entity_name} diaktifkan atau dinonaktifkan", "turned_off": "{entity_name} dimatikan", "turned_on": "{entity_name} dinyalakan" } diff --git a/homeassistant/components/light/translations/it.json b/homeassistant/components/light/translations/it.json index 9e751a9a4b8..7a71a2dbd91 100644 --- a/homeassistant/components/light/translations/it.json +++ b/homeassistant/components/light/translations/it.json @@ -14,7 +14,6 @@ }, "trigger_type": { "changed_states": "{entity_name} attivata o disattivata", - "toggled": "{entity_name} attiva o disattiva", "turned_off": "{entity_name} disattivato", "turned_on": "{entity_name} attivato" } diff --git a/homeassistant/components/light/translations/ja.json b/homeassistant/components/light/translations/ja.json index 7e9599af54a..cd1008cc5a0 100644 --- a/homeassistant/components/light/translations/ja.json +++ b/homeassistant/components/light/translations/ja.json @@ -14,7 +14,6 @@ }, "trigger_type": { "changed_states": "{entity_name} \u304c\u30aa\u30f3\u307e\u305f\u306f\u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", - "toggled": "{entity_name} \u304c\u30aa\u30f3\u307e\u305f\u306f\u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059" } diff --git a/homeassistant/components/light/translations/nl.json b/homeassistant/components/light/translations/nl.json index f70830601c7..3708a1e9a8d 100644 --- a/homeassistant/components/light/translations/nl.json +++ b/homeassistant/components/light/translations/nl.json @@ -14,7 +14,6 @@ }, "trigger_type": { "changed_states": "{entity_name} in- of uitgeschakeld", - "toggled": "{entity_name} in- of uitgeschakeld", "turned_off": "{entity_name} is uitgeschakeld", "turned_on": "{entity_name} is ingeschakeld" } diff --git a/homeassistant/components/light/translations/no.json b/homeassistant/components/light/translations/no.json index 92d7102d217..8ed11defddc 100644 --- a/homeassistant/components/light/translations/no.json +++ b/homeassistant/components/light/translations/no.json @@ -14,7 +14,6 @@ }, "trigger_type": { "changed_states": "{entity_name} sl\u00e5tt p\u00e5 eller av", - "toggled": "{entity_name} sl\u00e5tt p\u00e5 eller av", "turned_off": "{entity_name} sl\u00e5tt av", "turned_on": "{entity_name} sl\u00e5tt p\u00e5" } diff --git a/homeassistant/components/light/translations/pl.json b/homeassistant/components/light/translations/pl.json index 375cf8a8ce3..1a3b9c8b124 100644 --- a/homeassistant/components/light/translations/pl.json +++ b/homeassistant/components/light/translations/pl.json @@ -14,7 +14,6 @@ }, "trigger_type": { "changed_states": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", - "toggled": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}" } diff --git a/homeassistant/components/light/translations/pt-BR.json b/homeassistant/components/light/translations/pt-BR.json index f884baf9b3c..893def8c67b 100644 --- a/homeassistant/components/light/translations/pt-BR.json +++ b/homeassistant/components/light/translations/pt-BR.json @@ -14,7 +14,6 @@ }, "trigger_type": { "changed_states": "{entity_name} for ligada ou desligada", - "toggled": "{entity_name} for ligada ou desligada", "turned_off": "{entity_name} for desligada", "turned_on": "{entity_name} for ligada" } diff --git a/homeassistant/components/light/translations/ru.json b/homeassistant/components/light/translations/ru.json index 4010139e381..efe8ba188ec 100644 --- a/homeassistant/components/light/translations/ru.json +++ b/homeassistant/components/light/translations/ru.json @@ -14,7 +14,6 @@ }, "trigger_type": { "changed_states": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0438\u043b\u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", - "toggled": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0438\u043b\u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" } diff --git a/homeassistant/components/light/translations/sv.json b/homeassistant/components/light/translations/sv.json index 99f0b123b6a..d5f0bdaf767 100644 --- a/homeassistant/components/light/translations/sv.json +++ b/homeassistant/components/light/translations/sv.json @@ -12,7 +12,6 @@ "is_on": "{entity_name} \u00e4r p\u00e5" }, "trigger_type": { - "toggled": "{entity_name} slogs p\u00e5 eller av", "turned_off": "{entity_name} avst\u00e4ngd", "turned_on": "{entity_name} slogs p\u00e5" } diff --git a/homeassistant/components/light/translations/tr.json b/homeassistant/components/light/translations/tr.json index 4ddde6dabb1..a5efe591214 100644 --- a/homeassistant/components/light/translations/tr.json +++ b/homeassistant/components/light/translations/tr.json @@ -14,7 +14,6 @@ }, "trigger_type": { "changed_states": "{entity_name} a\u00e7\u0131ld\u0131 veya kapat\u0131ld\u0131", - "toggled": "{entity_name} a\u00e7\u0131ld\u0131 veya kapat\u0131ld\u0131", "turned_off": "{entity_name} kapat\u0131ld\u0131", "turned_on": "{entity_name} a\u00e7\u0131ld\u0131" } diff --git a/homeassistant/components/light/translations/zh-Hans.json b/homeassistant/components/light/translations/zh-Hans.json index 211fa1ed50b..1054820c6bb 100644 --- a/homeassistant/components/light/translations/zh-Hans.json +++ b/homeassistant/components/light/translations/zh-Hans.json @@ -13,7 +13,6 @@ "is_on": "{entity_name} \u5df2\u6253\u5f00" }, "trigger_type": { - "toggled": "{entity_name} \u88ab\u6253\u5f00\u6216\u5173\u95ed", "turned_off": "{entity_name} \u88ab\u5173\u95ed", "turned_on": "{entity_name} \u88ab\u6253\u5f00" } diff --git a/homeassistant/components/light/translations/zh-Hant.json b/homeassistant/components/light/translations/zh-Hant.json index e008376298e..054c325be82 100644 --- a/homeassistant/components/light/translations/zh-Hant.json +++ b/homeassistant/components/light/translations/zh-Hant.json @@ -14,7 +14,6 @@ }, "trigger_type": { "changed_states": "{entity_name}\u5df2\u958b\u555f\u6216\u95dc\u9589", - "toggled": "{entity_name}\u5df2\u958b\u555f\u6216\u95dc\u9589", "turned_off": "{entity_name}\u5df2\u95dc\u9589", "turned_on": "{entity_name}\u5df2\u958b\u555f" } diff --git a/homeassistant/components/litterrobot/translations/sensor.cs.json b/homeassistant/components/litterrobot/translations/sensor.cs.json new file mode 100644 index 00000000000..5b728084bed --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.cs.json @@ -0,0 +1,9 @@ +{ + "state": { + "litterrobot__status_code": { + "off": "Vypnuto", + "offline": "Offline", + "p": "Pozastaveno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.he.json b/homeassistant/components/litterrobot/translations/sensor.he.json new file mode 100644 index 00000000000..0c693d10157 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.he.json @@ -0,0 +1,8 @@ +{ + "state": { + "litterrobot__status_code": { + "off": "\u05db\u05d1\u05d5\u05d9", + "p": "\u05de\u05d5\u05e9\u05d4\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.ko.json b/homeassistant/components/litterrobot/translations/sensor.ko.json index c9c452b4275..49812dd242a 100644 --- a/homeassistant/components/litterrobot/translations/sensor.ko.json +++ b/homeassistant/components/litterrobot/translations/sensor.ko.json @@ -9,7 +9,10 @@ "df1": "\ubcf4\uad00\ud568 \uac70\uc758 \ucc38 - 2\uc0ac\uc774\ud074 \ub0a8\uc74c", "df2": "\ubcf4\uad00\ud568 \uac70\uc758 \ucc38 - 1\uc0ac\uc774\ud074 \ub0a8\uc74c", "dfs": "\ubcf4\uad00\ud568 \uac00\ub4dd \ucc38", + "dhf": "\ub364\ud504 + \ud648 \uc704\uce58 \uc624\ub958", + "dpf": "\ub364\ud504 \uc704\uce58 \uc624\ub958", "ec": "\ube44\uc6c0 \uc8fc\uae30", + "hpf": "\ud648 \uc704\uce58 \uc624\ub958", "off": "\uaebc\uc9d0", "offline": "\uc624\ud504\ub77c\uc778", "otf": "\ud1a0\ud06c \uc624\ub958 \uc774\uc0c1", diff --git a/homeassistant/components/luftdaten/translations/bg.json b/homeassistant/components/luftdaten/translations/bg.json index 784b010219d..5221cf5fc0e 100644 --- a/homeassistant/components/luftdaten/translations/bg.json +++ b/homeassistant/components/luftdaten/translations/bg.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u0430\u0440\u0442\u0430\u0442\u0430", "station_id": "ID \u043d\u0430 \u0441\u0435\u043d\u0437\u043e\u0440\u0430 \u043d\u0430 Luftdaten" - }, - "title": "\u0414\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/ca.json b/homeassistant/components/luftdaten/translations/ca.json index 84794f6bccb..97ca6a0eebc 100644 --- a/homeassistant/components/luftdaten/translations/ca.json +++ b/homeassistant/components/luftdaten/translations/ca.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "Mostrar al mapa", "station_id": "ID del sensor" - }, - "title": "Configuraci\u00f3 de Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/cs.json b/homeassistant/components/luftdaten/translations/cs.json index 2a9d786cf12..a3522f1fb7f 100644 --- a/homeassistant/components/luftdaten/translations/cs.json +++ b/homeassistant/components/luftdaten/translations/cs.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "Uka\u017e na map\u011b", "station_id": "ID senzoru Luftdaten" - }, - "title": "Definujte Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/da.json b/homeassistant/components/luftdaten/translations/da.json index fa32979112b..0612d406aa5 100644 --- a/homeassistant/components/luftdaten/translations/da.json +++ b/homeassistant/components/luftdaten/translations/da.json @@ -8,8 +8,7 @@ "data": { "show_on_map": "Vis p\u00e5 kort", "station_id": "Luftdaten sensor-id" - }, - "title": "Definer Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/de.json b/homeassistant/components/luftdaten/translations/de.json index 09fd47e4e20..56615ee355f 100644 --- a/homeassistant/components/luftdaten/translations/de.json +++ b/homeassistant/components/luftdaten/translations/de.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "Auf Karte anzeigen", "station_id": "Sensor-ID" - }, - "title": "Luftdaten einrichten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/el.json b/homeassistant/components/luftdaten/translations/el.json index 59519c251ef..5b2db34c47e 100644 --- a/homeassistant/components/luftdaten/translations/el.json +++ b/homeassistant/components/luftdaten/translations/el.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c3\u03c4\u03bf \u03c7\u03ac\u03c1\u03c4\u03b7", "station_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1" - }, - "title": "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/en.json b/homeassistant/components/luftdaten/translations/en.json index ed6983eaa92..fa6a65dcaa7 100644 --- a/homeassistant/components/luftdaten/translations/en.json +++ b/homeassistant/components/luftdaten/translations/en.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "Show on map", "station_id": "Sensor ID" - }, - "title": "Define Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/es-419.json b/homeassistant/components/luftdaten/translations/es-419.json index e9e97ff034d..cdd130f7266 100644 --- a/homeassistant/components/luftdaten/translations/es-419.json +++ b/homeassistant/components/luftdaten/translations/es-419.json @@ -8,8 +8,7 @@ "data": { "show_on_map": "Mostrar en el mapa", "station_id": "ID del sensor de Luftdaten" - }, - "title": "Definir Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/es.json b/homeassistant/components/luftdaten/translations/es.json index 99e5f7d5058..075d30dc751 100644 --- a/homeassistant/components/luftdaten/translations/es.json +++ b/homeassistant/components/luftdaten/translations/es.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "Mostrar en el mapa", "station_id": "ID del sensor" - }, - "title": "Definir Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/et.json b/homeassistant/components/luftdaten/translations/et.json index 148007a0251..96897e55784 100644 --- a/homeassistant/components/luftdaten/translations/et.json +++ b/homeassistant/components/luftdaten/translations/et.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "Kuva kaardil", "station_id": "Anduri ID" - }, - "title": "M\u00e4\u00e4ratle Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/fi.json b/homeassistant/components/luftdaten/translations/fi.json index 452cda748f7..64a83b3ed50 100644 --- a/homeassistant/components/luftdaten/translations/fi.json +++ b/homeassistant/components/luftdaten/translations/fi.json @@ -5,8 +5,7 @@ "data": { "show_on_map": "N\u00e4yt\u00e4 kartalla", "station_id": "Luftdaten-anturin ID" - }, - "title": "M\u00e4\u00e4rit\u00e4 Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/fr.json b/homeassistant/components/luftdaten/translations/fr.json index 4eacd984ad8..225ce28d13d 100644 --- a/homeassistant/components/luftdaten/translations/fr.json +++ b/homeassistant/components/luftdaten/translations/fr.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "Montrer sur la carte", "station_id": "ID du capteur" - }, - "title": "D\u00e9finir Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/hu.json b/homeassistant/components/luftdaten/translations/hu.json index 7439546f796..c867456e945 100644 --- a/homeassistant/components/luftdaten/translations/hu.json +++ b/homeassistant/components/luftdaten/translations/hu.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "Megjelen\u00edt\u00e9s a t\u00e9rk\u00e9pen", "station_id": "\u00c9rz\u00e9kel\u0151 azonos\u00edt\u00f3" - }, - "title": "Luftdaten be\u00e1ll\u00edt\u00e1sa" + } } } } diff --git a/homeassistant/components/luftdaten/translations/id.json b/homeassistant/components/luftdaten/translations/id.json index 11fc2917061..2ed6e65e2c3 100644 --- a/homeassistant/components/luftdaten/translations/id.json +++ b/homeassistant/components/luftdaten/translations/id.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "Tampilkan di peta", "station_id": "ID Sensor" - }, - "title": "Konfigurasikan Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/it.json b/homeassistant/components/luftdaten/translations/it.json index a39b3dd7e59..84a870fd8be 100644 --- a/homeassistant/components/luftdaten/translations/it.json +++ b/homeassistant/components/luftdaten/translations/it.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "Mostra sulla mappa", "station_id": "ID sensore" - }, - "title": "Definisci Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/ja.json b/homeassistant/components/luftdaten/translations/ja.json index 15dc417c156..ab01dc703d6 100644 --- a/homeassistant/components/luftdaten/translations/ja.json +++ b/homeassistant/components/luftdaten/translations/ja.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "\u5730\u56f3\u306b\u8868\u793a", "station_id": "Luftdaten\u30bb\u30f3\u30b5\u30fcID" - }, - "title": "Luftdaten\u306e\u5b9a\u7fa9" + } } } } diff --git a/homeassistant/components/luftdaten/translations/ko.json b/homeassistant/components/luftdaten/translations/ko.json index fbb5a26e7ee..820f47803ad 100644 --- a/homeassistant/components/luftdaten/translations/ko.json +++ b/homeassistant/components/luftdaten/translations/ko.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "\uc9c0\ub3c4\uc5d0 \ud45c\uc2dc\ud558\uae30", "station_id": "Luftdaten \uc13c\uc11c ID" - }, - "title": "Luftdaten \uc815\uc758\ud558\uae30" + } } } } diff --git a/homeassistant/components/luftdaten/translations/lb.json b/homeassistant/components/luftdaten/translations/lb.json index 83254e05c83..5990ccefd17 100644 --- a/homeassistant/components/luftdaten/translations/lb.json +++ b/homeassistant/components/luftdaten/translations/lb.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "Op der Kaart uweisen", "station_id": "Luftdaten Sensor ID" - }, - "title": "Luftdaten d\u00e9fin\u00e9ieren" + } } } } diff --git a/homeassistant/components/luftdaten/translations/nl.json b/homeassistant/components/luftdaten/translations/nl.json index 44ace40baa9..adfdb1101a0 100644 --- a/homeassistant/components/luftdaten/translations/nl.json +++ b/homeassistant/components/luftdaten/translations/nl.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "Toon op kaart", "station_id": "Sensor ID" - }, - "title": "Definieer Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/no.json b/homeassistant/components/luftdaten/translations/no.json index 9ae681bcf91..bd5b6c97cda 100644 --- a/homeassistant/components/luftdaten/translations/no.json +++ b/homeassistant/components/luftdaten/translations/no.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "Vis p\u00e5 kart", "station_id": "Sensor ID" - }, - "title": "" + } } } } diff --git a/homeassistant/components/luftdaten/translations/pl.json b/homeassistant/components/luftdaten/translations/pl.json index 7045a0a3dbe..03b004037c5 100644 --- a/homeassistant/components/luftdaten/translations/pl.json +++ b/homeassistant/components/luftdaten/translations/pl.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "Poka\u017c na mapie", "station_id": "ID sensora" - }, - "title": "Konfiguracja Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/pt-BR.json b/homeassistant/components/luftdaten/translations/pt-BR.json index 82b1f09735b..877cc5d133c 100644 --- a/homeassistant/components/luftdaten/translations/pt-BR.json +++ b/homeassistant/components/luftdaten/translations/pt-BR.json @@ -8,10 +8,9 @@ "step": { "user": { "data": { - "show_on_map": "Mostrar no mapa", + "show_on_map": "[%key:component::iss::config::step::user::data::show_on_map%]", "station_id": "ID do Sensor Luftdaten" - }, - "title": "Definir Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/pt.json b/homeassistant/components/luftdaten/translations/pt.json index 5811494fffb..709ed6af0a1 100644 --- a/homeassistant/components/luftdaten/translations/pt.json +++ b/homeassistant/components/luftdaten/translations/pt.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "Mostrar no mapa", "station_id": "Luftdaten Sensor ID" - }, - "title": "Definir Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/ru.json b/homeassistant/components/luftdaten/translations/ru.json index 82813ee3a53..cfb5ca68f93 100644 --- a/homeassistant/components/luftdaten/translations/ru.json +++ b/homeassistant/components/luftdaten/translations/ru.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0435", "station_id": "ID \u0434\u0430\u0442\u0447\u0438\u043a\u0430" - }, - "title": "Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/sl.json b/homeassistant/components/luftdaten/translations/sl.json index 249a0258536..7697abcf9e7 100644 --- a/homeassistant/components/luftdaten/translations/sl.json +++ b/homeassistant/components/luftdaten/translations/sl.json @@ -8,8 +8,7 @@ "data": { "show_on_map": "Prika\u017ei na zemljevidu", "station_id": "Luftdaten ID Senzorja" - }, - "title": "Dolo\u010dite Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/sv.json b/homeassistant/components/luftdaten/translations/sv.json index 52180093ba4..63de146c6d7 100644 --- a/homeassistant/components/luftdaten/translations/sv.json +++ b/homeassistant/components/luftdaten/translations/sv.json @@ -8,8 +8,7 @@ "data": { "show_on_map": "Visa p\u00e5 karta", "station_id": "Luftdaten Sensor-ID" - }, - "title": "Definiera Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/tr.json b/homeassistant/components/luftdaten/translations/tr.json index a74b35252c1..f8bbaf818d3 100644 --- a/homeassistant/components/luftdaten/translations/tr.json +++ b/homeassistant/components/luftdaten/translations/tr.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "Haritada g\u00f6ster", "station_id": "Sens\u00f6r Kimli\u011fi" - }, - "title": "Luftdaten'i tan\u0131mlay\u0131n" + } } } } diff --git a/homeassistant/components/luftdaten/translations/uk.json b/homeassistant/components/luftdaten/translations/uk.json index 9fd33dc3da2..d4cb21d89bb 100644 --- a/homeassistant/components/luftdaten/translations/uk.json +++ b/homeassistant/components/luftdaten/translations/uk.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043d\u0430 \u043c\u0430\u043f\u0456", "station_id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 Luftdaten" - }, - "title": "Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/zh-Hans.json b/homeassistant/components/luftdaten/translations/zh-Hans.json index d66c44499df..193c2078bc1 100644 --- a/homeassistant/components/luftdaten/translations/zh-Hans.json +++ b/homeassistant/components/luftdaten/translations/zh-Hans.json @@ -9,8 +9,7 @@ "data": { "show_on_map": "\u5728\u5730\u56fe\u4e0a\u663e\u793a", "station_id": "Luftdaten \u4f20\u611f\u5668 ID" - }, - "title": "\u5b9a\u4e49 Luftdaten" + } } } } diff --git a/homeassistant/components/luftdaten/translations/zh-Hant.json b/homeassistant/components/luftdaten/translations/zh-Hant.json index ae9aa11da65..7fc2a8725e8 100644 --- a/homeassistant/components/luftdaten/translations/zh-Hant.json +++ b/homeassistant/components/luftdaten/translations/zh-Hant.json @@ -10,8 +10,7 @@ "data": { "show_on_map": "\u65bc\u5730\u5716\u986f\u793a", "station_id": "\u611f\u61c9\u5668 ID" - }, - "title": "\u5b9a\u7fa9 Luftdaten" + } } } } diff --git a/homeassistant/components/meater/translations/ko.json b/homeassistant/components/meater/translations/ko.json index b7fdeff3c2a..b414109dedc 100644 --- a/homeassistant/components/meater/translations/ko.json +++ b/homeassistant/components/meater/translations/ko.json @@ -1,10 +1,26 @@ { "config": { + "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "service_unavailable_error": "API\ub294 \ud604\uc7ac \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574 \uc8fc\uc138\uc694.", + "unknown_auth_error": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, "step": { "reauth_confirm": { "data": { "password": "\ube44\ubc00\ubc88\ud638" - } + }, + "description": "Meater Cloud \uacc4\uc815 {\uc0ac\uc6a9\uc790 \uc774\ub984}\uc5d0 \ub300\ud55c \uc554\ud638\ub97c \ud655\uc778\ud569\ub2c8\ub2e4." + }, + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "data_description": { + "username": "Meater Cloud \uc0ac\uc6a9\uc790 \uc774\ub984, \uc77c\ubc18\uc801\uc73c\ub85c \uc774\uba54\uc77c \uc8fc\uc18c\uc785\ub2c8\ub2e4." + }, + "description": "Meater Cloud \uacc4\uc815\uc744 \uc124\uc815\ud569\ub2c8\ub2e4." } } } diff --git a/homeassistant/components/mill/translations/bg.json b/homeassistant/components/mill/translations/bg.json index 43611df6920..63c760b7bee 100644 --- a/homeassistant/components/mill/translations/bg.json +++ b/homeassistant/components/mill/translations/bg.json @@ -18,12 +18,6 @@ "ip_address": "IP \u0430\u0434\u0440\u0435\u0441" }, "description": "\u041b\u043e\u043a\u0430\u043b\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e." - }, - "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" - } } } } diff --git a/homeassistant/components/mill/translations/ca.json b/homeassistant/components/mill/translations/ca.json index ffb567fc218..ba350e217ac 100644 --- a/homeassistant/components/mill/translations/ca.json +++ b/homeassistant/components/mill/translations/ca.json @@ -21,9 +21,7 @@ }, "user": { "data": { - "connection_type": "Selecciona el tipus de connexi\u00f3", - "password": "Contrasenya", - "username": "Nom d'usuari" + "connection_type": "Selecciona el tipus de connexi\u00f3" }, "description": "Selecciona el tipus de connexi\u00f3. La local necessita escalfadors de generaci\u00f3 3" } diff --git a/homeassistant/components/mill/translations/cs.json b/homeassistant/components/mill/translations/cs.json index fcbdf3fb428..f6c47b4c840 100644 --- a/homeassistant/components/mill/translations/cs.json +++ b/homeassistant/components/mill/translations/cs.json @@ -5,14 +5,6 @@ }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" - }, - "step": { - "user": { - "data": { - "password": "Heslo", - "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/mill/translations/de.json b/homeassistant/components/mill/translations/de.json index 1688dddb321..6b8e912674a 100644 --- a/homeassistant/components/mill/translations/de.json +++ b/homeassistant/components/mill/translations/de.json @@ -21,9 +21,7 @@ }, "user": { "data": { - "connection_type": "Verbindungstyp ausw\u00e4hlen", - "password": "Passwort", - "username": "Benutzername" + "connection_type": "Verbindungstyp ausw\u00e4hlen" }, "description": "W\u00e4hle die Anschlussart. Lokal erfordert Heizger\u00e4te der Generation 3" } diff --git a/homeassistant/components/mill/translations/el.json b/homeassistant/components/mill/translations/el.json index 18743aada0a..dc5fb70915b 100644 --- a/homeassistant/components/mill/translations/el.json +++ b/homeassistant/components/mill/translations/el.json @@ -21,9 +21,7 @@ }, "user": { "data": { - "connection_type": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c4\u03cd\u03c0\u03bf\u03c5 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + "connection_type": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c4\u03cd\u03c0\u03bf\u03c5 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03cd\u03c0\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2. \u0397 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03b8\u03b5\u03c1\u03bc\u03b1\u03bd\u03c4\u03ae\u03c1\u03b5\u03c2 3\u03b7\u03c2 \u03b3\u03b5\u03bd\u03b9\u03ac\u03c2" } diff --git a/homeassistant/components/mill/translations/en.json b/homeassistant/components/mill/translations/en.json index 20291847893..ee66706832e 100644 --- a/homeassistant/components/mill/translations/en.json +++ b/homeassistant/components/mill/translations/en.json @@ -21,9 +21,7 @@ }, "user": { "data": { - "connection_type": "Select connection type", - "password": "Password", - "username": "Username" + "connection_type": "Select connection type" }, "description": "Select connection type. Local requires generation 3 heaters" } diff --git a/homeassistant/components/mill/translations/es.json b/homeassistant/components/mill/translations/es.json index 104c9d37d84..280d4ad4ba9 100644 --- a/homeassistant/components/mill/translations/es.json +++ b/homeassistant/components/mill/translations/es.json @@ -21,9 +21,7 @@ }, "user": { "data": { - "connection_type": "Seleccione el tipo de conexi\u00f3n", - "password": "Contrase\u00f1a", - "username": "Usuario" + "connection_type": "Seleccione el tipo de conexi\u00f3n" }, "description": "Seleccione el tipo de conexi\u00f3n. Local requiere calentadores de generaci\u00f3n 3" } diff --git a/homeassistant/components/mill/translations/et.json b/homeassistant/components/mill/translations/et.json index dad66420441..7b1be866b6a 100644 --- a/homeassistant/components/mill/translations/et.json +++ b/homeassistant/components/mill/translations/et.json @@ -21,9 +21,7 @@ }, "user": { "data": { - "connection_type": "Vali \u00fchenduse t\u00fc\u00fcp", - "password": "Salas\u00f5na", - "username": "Kasutajanimi" + "connection_type": "Vali \u00fchenduse t\u00fc\u00fcp" }, "description": "Vali \u00fchenduse t\u00fc\u00fcp. Kohalik vajab 3. p\u00f5lvkonna k\u00fctteseadmeid" } diff --git a/homeassistant/components/mill/translations/fi.json b/homeassistant/components/mill/translations/fi.json deleted file mode 100644 index 61febe9dd9c..00000000000 --- a/homeassistant/components/mill/translations/fi.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "password": "Salasana", - "username": "K\u00e4ytt\u00e4j\u00e4tunnus" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/fr.json b/homeassistant/components/mill/translations/fr.json index 440ef77ea0a..78b25cd079a 100644 --- a/homeassistant/components/mill/translations/fr.json +++ b/homeassistant/components/mill/translations/fr.json @@ -21,9 +21,7 @@ }, "user": { "data": { - "connection_type": "S\u00e9lectionner le type de connexion", - "password": "Mot de passe", - "username": "Nom d'utilisateur" + "connection_type": "S\u00e9lectionner le type de connexion" }, "description": "S\u00e9lectionnez le type de connexion. Local n\u00e9cessite des radiateurs de g\u00e9n\u00e9ration 3" } diff --git a/homeassistant/components/mill/translations/he.json b/homeassistant/components/mill/translations/he.json index b551a319b2b..35c0b30e0de 100644 --- a/homeassistant/components/mill/translations/he.json +++ b/homeassistant/components/mill/translations/he.json @@ -17,12 +17,6 @@ "data": { "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP" } - }, - "user": { - "data": { - "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } } } } diff --git a/homeassistant/components/mill/translations/hu.json b/homeassistant/components/mill/translations/hu.json index 8a77a822fc8..38c3c96dbdb 100644 --- a/homeassistant/components/mill/translations/hu.json +++ b/homeassistant/components/mill/translations/hu.json @@ -21,9 +21,7 @@ }, "user": { "data": { - "connection_type": "V\u00e1lassza ki a kapcsolat t\u00edpus\u00e1t", - "password": "Jelsz\u00f3", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + "connection_type": "V\u00e1lassza ki a kapcsolat t\u00edpus\u00e1t" }, "description": "V\u00e1lassza ki a kapcsolat t\u00edpus\u00e1t. A helyi kapcsolat 3. gener\u00e1ci\u00f3s f\u0171t\u0151berendez\u00e9seket ig\u00e9nyel" } diff --git a/homeassistant/components/mill/translations/id.json b/homeassistant/components/mill/translations/id.json index 92670be251a..ee5c548bdf6 100644 --- a/homeassistant/components/mill/translations/id.json +++ b/homeassistant/components/mill/translations/id.json @@ -21,9 +21,7 @@ }, "user": { "data": { - "connection_type": "Pilih jenis koneksi", - "password": "Kata Sandi", - "username": "Nama Pengguna" + "connection_type": "Pilih jenis koneksi" }, "description": "Pilih jenis koneksi. Lokal membutuhkan pemanas generasi 3" } diff --git a/homeassistant/components/mill/translations/it.json b/homeassistant/components/mill/translations/it.json index fa19ebc4cbd..5085817a6ca 100644 --- a/homeassistant/components/mill/translations/it.json +++ b/homeassistant/components/mill/translations/it.json @@ -21,9 +21,7 @@ }, "user": { "data": { - "connection_type": "Seleziona il tipo di connessione", - "password": "Password", - "username": "Nome utente" + "connection_type": "Seleziona il tipo di connessione" }, "description": "Seleziona il tipo di connessione. Locale richiede riscaldatori di terza generazione" } diff --git a/homeassistant/components/mill/translations/ja.json b/homeassistant/components/mill/translations/ja.json index 9250a503b58..8bb2ab5f0ea 100644 --- a/homeassistant/components/mill/translations/ja.json +++ b/homeassistant/components/mill/translations/ja.json @@ -21,9 +21,7 @@ }, "user": { "data": { - "connection_type": "\u63a5\u7d9a\u30bf\u30a4\u30d7\u306e\u9078\u629e", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + "connection_type": "\u63a5\u7d9a\u30bf\u30a4\u30d7\u306e\u9078\u629e" }, "description": "\u63a5\u7d9a\u30bf\u30a4\u30d7\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u30ed\u30fc\u30ab\u30eb\u306b\u306f\u7b2c3\u4e16\u4ee3\u306e\u30d2\u30fc\u30bf\u30fc\u304c\u5fc5\u8981\u3067\u3059" } diff --git a/homeassistant/components/mill/translations/ko.json b/homeassistant/components/mill/translations/ko.json index 1041727d84a..a38c2d98582 100644 --- a/homeassistant/components/mill/translations/ko.json +++ b/homeassistant/components/mill/translations/ko.json @@ -9,12 +9,6 @@ "step": { "local": { "description": "\ub85c\uceec IP \uc8fc\uc18c" - }, - "user": { - "data": { - "password": "\ube44\ubc00\ubc88\ud638", - "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - } } } } diff --git a/homeassistant/components/mill/translations/lb.json b/homeassistant/components/mill/translations/lb.json index 84aeac8bd97..36841f1020f 100644 --- a/homeassistant/components/mill/translations/lb.json +++ b/homeassistant/components/mill/translations/lb.json @@ -5,14 +5,6 @@ }, "error": { "cannot_connect": "Feeler beim verbannen" - }, - "step": { - "user": { - "data": { - "password": "Passwuert", - "username": "Benotzernumm" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/mill/translations/nl.json b/homeassistant/components/mill/translations/nl.json index f37a5cf0758..1d431395b48 100644 --- a/homeassistant/components/mill/translations/nl.json +++ b/homeassistant/components/mill/translations/nl.json @@ -21,9 +21,7 @@ }, "user": { "data": { - "connection_type": "Selecteer verbindingstype", - "password": "Wachtwoord", - "username": "Gebruikersnaam" + "connection_type": "Selecteer verbindingstype" }, "description": "Selecteer verbindingstype. Lokaal vereist generatie 3 kachels" } diff --git a/homeassistant/components/mill/translations/no.json b/homeassistant/components/mill/translations/no.json index d5306fd090d..df51f7d73d3 100644 --- a/homeassistant/components/mill/translations/no.json +++ b/homeassistant/components/mill/translations/no.json @@ -21,9 +21,7 @@ }, "user": { "data": { - "connection_type": "Velg tilkoblingstype", - "password": "Passord", - "username": "Brukernavn" + "connection_type": "Velg tilkoblingstype" }, "description": "Velg tilkoblingstype. Lokal krever generasjon 3 varmeovner" } diff --git a/homeassistant/components/mill/translations/pl.json b/homeassistant/components/mill/translations/pl.json index 73abdedbfbc..b75a5da2df3 100644 --- a/homeassistant/components/mill/translations/pl.json +++ b/homeassistant/components/mill/translations/pl.json @@ -21,9 +21,7 @@ }, "user": { "data": { - "connection_type": "Wybierz typ po\u0142\u0105czenia", - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika" + "connection_type": "Wybierz typ po\u0142\u0105czenia" }, "description": "Wybierz typ po\u0142\u0105czenia. Lokalnie wymaga grzejnik\u00f3w 3 generacji" } diff --git a/homeassistant/components/mill/translations/pt-BR.json b/homeassistant/components/mill/translations/pt-BR.json index 8d90531191a..73eb1808a1c 100644 --- a/homeassistant/components/mill/translations/pt-BR.json +++ b/homeassistant/components/mill/translations/pt-BR.json @@ -21,9 +21,7 @@ }, "user": { "data": { - "connection_type": "Selecione o tipo de conex\u00e3o", - "password": "Senha", - "username": "Usu\u00e1rio" + "connection_type": "Selecione o tipo de conex\u00e3o" }, "description": "Selecione o tipo de conex\u00e3o. Local requer aquecedores de 3\u00aa gera\u00e7\u00e3o" } diff --git a/homeassistant/components/mill/translations/pt.json b/homeassistant/components/mill/translations/pt.json index 4348cecf5c3..63c9b4e0011 100644 --- a/homeassistant/components/mill/translations/pt.json +++ b/homeassistant/components/mill/translations/pt.json @@ -5,14 +5,6 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" - }, - "step": { - "user": { - "data": { - "password": "Palavra-passe", - "username": "Nome de Utilizador" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/mill/translations/ru.json b/homeassistant/components/mill/translations/ru.json index ab09ec1fdaa..bac13e68163 100644 --- a/homeassistant/components/mill/translations/ru.json +++ b/homeassistant/components/mill/translations/ru.json @@ -21,9 +21,7 @@ }, "user": { "data": { - "connection_type": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + "connection_type": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f. \u0414\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u043f\u0446\u0438\u0438 Local \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043e\u0431\u043e\u0433\u0440\u0435\u0432\u0430\u0442\u0435\u043b\u044c \u0442\u0440\u0435\u0442\u044c\u0435\u0433\u043e \u043f\u043e\u043a\u043e\u043b\u0435\u043d\u0438\u044f." } diff --git a/homeassistant/components/mill/translations/sv.json b/homeassistant/components/mill/translations/sv.json deleted file mode 100644 index 23c825f256f..00000000000 --- a/homeassistant/components/mill/translations/sv.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "username": "Anv\u00e4ndarnamn" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/tr.json b/homeassistant/components/mill/translations/tr.json index 63ddc4ee7b1..1b75d70c51c 100644 --- a/homeassistant/components/mill/translations/tr.json +++ b/homeassistant/components/mill/translations/tr.json @@ -21,9 +21,7 @@ }, "user": { "data": { - "connection_type": "Ba\u011flant\u0131 t\u00fcr\u00fcn\u00fc se\u00e7in", - "password": "Parola", - "username": "Kullan\u0131c\u0131 Ad\u0131" + "connection_type": "Ba\u011flant\u0131 t\u00fcr\u00fcn\u00fc se\u00e7in" }, "description": "Ba\u011flant\u0131 t\u00fcr\u00fcn\u00fc se\u00e7in. Yerel, 3. nesil \u0131s\u0131t\u0131c\u0131lar gerektirir" } diff --git a/homeassistant/components/mill/translations/uk.json b/homeassistant/components/mill/translations/uk.json index b8a5aea578e..ceee910d966 100644 --- a/homeassistant/components/mill/translations/uk.json +++ b/homeassistant/components/mill/translations/uk.json @@ -5,14 +5,6 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" - }, - "step": { - "user": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/mill/translations/zh-Hans.json b/homeassistant/components/mill/translations/zh-Hans.json index 79079e6c408..2941dfd9383 100644 --- a/homeassistant/components/mill/translations/zh-Hans.json +++ b/homeassistant/components/mill/translations/zh-Hans.json @@ -2,13 +2,6 @@ "config": { "error": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25" - }, - "step": { - "user": { - "data": { - "username": "\u7528\u6237\u540d" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/mill/translations/zh-Hant.json b/homeassistant/components/mill/translations/zh-Hant.json index 5c4745b23ae..cf8960a2a71 100644 --- a/homeassistant/components/mill/translations/zh-Hant.json +++ b/homeassistant/components/mill/translations/zh-Hant.json @@ -21,9 +21,7 @@ }, "user": { "data": { - "connection_type": "\u9078\u64c7\u9023\u7dda\u985e\u5225", - "password": "\u5bc6\u78bc", - "username": "\u4f7f\u7528\u8005\u540d\u7a31" + "connection_type": "\u9078\u64c7\u9023\u7dda\u985e\u5225" }, "description": "\u9078\u64c7\u9023\u7dda\u985e\u5225\u3002\u672c\u5730\u7aef\u5c07\u9700\u8981\u7b2c\u4e09\u4ee3\u52a0\u71b1\u5668" } diff --git a/homeassistant/components/min_max/translations/ca.json b/homeassistant/components/min_max/translations/ca.json index b114e8ecf40..4bc9e69f182 100644 --- a/homeassistant/components/min_max/translations/ca.json +++ b/homeassistant/components/min_max/translations/ca.json @@ -27,14 +27,6 @@ "data_description": { "round_digits": "Controla el nombre de d\u00edgits decimals quan la caracter\u00edstica estad\u00edstica \u00e9s la mitjana o la mediana." } - }, - "options": { - "data": { - "entity_ids": "Entitats d'entrada", - "round_digits": "Precisi\u00f3", - "type": "Caracter\u00edstica estad\u00edstica" - }, - "description": "Crea un sensor que calcula el m\u00ednim, m\u00e0xim, la mitjana o la mediana d'una llista de sensors d'entrada." } } }, diff --git a/homeassistant/components/min_max/translations/cs.json b/homeassistant/components/min_max/translations/cs.json index a969a39234a..181a5c93f14 100644 --- a/homeassistant/components/min_max/translations/cs.json +++ b/homeassistant/components/min_max/translations/cs.json @@ -20,14 +20,6 @@ "round_digits": "P\u0159esnost", "type": "Statistika" } - }, - "options": { - "data": { - "entity_ids": "Vstupn\u00ed entity", - "round_digits": "P\u0159esnost", - "type": "Statistika" - }, - "description": "P\u0159esnost ur\u010duje po\u010det desetinn\u00fdch m\u00edst, kdy\u017e je statistikou pr\u016fm\u011br nebo medi\u00e1n." } } } diff --git a/homeassistant/components/min_max/translations/de.json b/homeassistant/components/min_max/translations/de.json index c8b9d5ddf63..cb4ad7b9996 100644 --- a/homeassistant/components/min_max/translations/de.json +++ b/homeassistant/components/min_max/translations/de.json @@ -27,14 +27,6 @@ "data_description": { "round_digits": "Steuert die Anzahl der Dezimalstellen in der Ausgabe, wenn das Statistikmerkmal Mittelwert oder Median ist." } - }, - "options": { - "data": { - "entity_ids": "Eingabe-Entit\u00e4ten", - "round_digits": "Genauigkeit", - "type": "Statistisches Merkmal" - }, - "description": "Erstelle einen Sensor, der einen Mindest-, H\u00f6chst-, Mittel- oder Medianwert aus einer Liste von Eingabesensoren berechnet." } } }, diff --git a/homeassistant/components/min_max/translations/el.json b/homeassistant/components/min_max/translations/el.json index 218c46fd683..53499682941 100644 --- a/homeassistant/components/min_max/translations/el.json +++ b/homeassistant/components/min_max/translations/el.json @@ -27,14 +27,6 @@ "data_description": { "round_digits": "\u0395\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf \u03cc\u03c4\u03b1\u03bd \u03c4\u03bf \u03c3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03ad\u03c3\u03bf\u03c2 \u03ae \u03b4\u03b9\u03ac\u03bc\u03b5\u03c3\u03bf\u03c2." } - }, - "options": { - "data": { - "entity_ids": "\u039f\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5", - "round_digits": "\u0391\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1", - "type": "\u03a3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc" - }, - "description": "\u0397 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03cc\u03c4\u03b1\u03bd \u03c4\u03bf \u03c3\u03c4\u03b1\u03c4\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03b5\u03af\u03bd\u03b1\u03b9 \u03bf \u03bc\u03ad\u03c3\u03bf\u03c2 \u03cc\u03c1\u03bf\u03c2 \u03ae \u03b7 \u03b4\u03b9\u03ac\u03bc\u03b5\u03c3\u03bf\u03c2." } } }, diff --git a/homeassistant/components/min_max/translations/en.json b/homeassistant/components/min_max/translations/en.json index b5b6eb5d731..8cc0d41c419 100644 --- a/homeassistant/components/min_max/translations/en.json +++ b/homeassistant/components/min_max/translations/en.json @@ -27,14 +27,6 @@ "data_description": { "round_digits": "Controls the number of decimal digits in the output when the statistics characteristic is mean or median." } - }, - "options": { - "data": { - "entity_ids": "Input entities", - "round_digits": "Precision", - "type": "Statistic characteristic" - }, - "description": "Create a sensor that calculates a min, max, mean or median value from a list of input sensors." } } }, diff --git a/homeassistant/components/min_max/translations/es.json b/homeassistant/components/min_max/translations/es.json index ffe5217b1f9..aceaa287a3b 100644 --- a/homeassistant/components/min_max/translations/es.json +++ b/homeassistant/components/min_max/translations/es.json @@ -25,13 +25,6 @@ "data_description": { "round_digits": "Controla el n\u00famero de d\u00edgitos decimales cuando la caracter\u00edstica estad\u00edstica es la media o mediana." } - }, - "options": { - "data": { - "entity_ids": "Entidades de entrada", - "round_digits": "Precisi\u00f3n", - "type": "Caracter\u00edstica estad\u00edstica" - } } } }, diff --git a/homeassistant/components/min_max/translations/et.json b/homeassistant/components/min_max/translations/et.json index 2f1689e8577..c57b4a055a8 100644 --- a/homeassistant/components/min_max/translations/et.json +++ b/homeassistant/components/min_max/translations/et.json @@ -27,14 +27,6 @@ "data_description": { "round_digits": "M\u00e4\u00e4rab k\u00fcmnendkohtade arvu v\u00e4ljundis kui statistika tunnus on keskmine v\u00f5i mediaan." } - }, - "options": { - "data": { - "entity_ids": "Sisendolemid", - "round_digits": "T\u00e4psus", - "type": "Statistiline tunnus" - }, - "description": "T\u00e4psus kontrollib k\u00fcmnendnumbrite arvu kui statistika tunnus on keskmine v\u00f5i mediaan." } } }, diff --git a/homeassistant/components/min_max/translations/fr.json b/homeassistant/components/min_max/translations/fr.json index b42ccf0bb2a..562e7c7f735 100644 --- a/homeassistant/components/min_max/translations/fr.json +++ b/homeassistant/components/min_max/translations/fr.json @@ -27,14 +27,6 @@ "data_description": { "round_digits": "Contr\u00f4le le nombre de chiffres d\u00e9cimaux de la sortie lorsque l'indicateur statistique est la moyenne ou la m\u00e9diane." } - }, - "options": { - "data": { - "entity_ids": "Entit\u00e9s d'entr\u00e9e", - "round_digits": "Pr\u00e9cision", - "type": "Indicateur statistique" - }, - "description": "Cr\u00e9ez un capteur qui calcule une valeur minimale, maximale, moyenne ou m\u00e9diane \u00e0 partir d'une liste de capteurs d'entr\u00e9e." } } }, diff --git a/homeassistant/components/min_max/translations/he.json b/homeassistant/components/min_max/translations/he.json index 267e38f0ce2..b9f2f18a4c7 100644 --- a/homeassistant/components/min_max/translations/he.json +++ b/homeassistant/components/min_max/translations/he.json @@ -24,13 +24,6 @@ "data_description": { "round_digits": "\u05e9\u05d5\u05dc\u05d8 \u05d1\u05de\u05e1\u05e4\u05e8 \u05d4\u05e1\u05e4\u05e8\u05d5\u05ea \u05d4\u05e2\u05e9\u05e8\u05d5\u05e0\u05d9\u05d5\u05ea \u05d1\u05e4\u05dc\u05d8 \u05db\u05d0\u05e9\u05e8 \u05de\u05d0\u05e4\u05d9\u05d9\u05df \u05d4\u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4 \u05d4\u05d5\u05d0 \u05de\u05de\u05d5\u05e6\u05e2 \u05d0\u05d5 \u05d7\u05e6\u05d9\u05d5\u05e0\u05d9." } - }, - "options": { - "data": { - "entity_ids": "\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05e7\u05dc\u05d8", - "round_digits": "\u05d3\u05d9\u05d5\u05e7", - "type": "\u05de\u05d0\u05e4\u05d9\u05d9\u05df \u05e1\u05d8\u05d8\u05d9\u05e1\u05d8\u05d9\u05e7\u05d4" - } } } } diff --git a/homeassistant/components/min_max/translations/hu.json b/homeassistant/components/min_max/translations/hu.json index 98d021dbb31..7330df0d3e0 100644 --- a/homeassistant/components/min_max/translations/hu.json +++ b/homeassistant/components/min_max/translations/hu.json @@ -27,14 +27,6 @@ "data_description": { "round_digits": "Szab\u00e1lyozza a tizedesjegyek sz\u00e1m\u00e1t, amikor a statisztikai jellemz\u0151 az \u00e1tlag vagy a medi\u00e1n." } - }, - "options": { - "data": { - "entity_ids": "Forr\u00e1s entit\u00e1sok", - "round_digits": "Pontoss\u00e1g", - "type": "Statisztikai jellemz\u0151" - }, - "description": "A pontoss\u00e1g a tizedesjegyek sz\u00e1m\u00e1t szab\u00e1lyozza, ha a statisztikai jellemz\u0151 az \u00e1tlag vagy a medi\u00e1n." } } }, diff --git a/homeassistant/components/min_max/translations/id.json b/homeassistant/components/min_max/translations/id.json index 32ccfd2596d..9e06b47de95 100644 --- a/homeassistant/components/min_max/translations/id.json +++ b/homeassistant/components/min_max/translations/id.json @@ -27,14 +27,6 @@ "data_description": { "round_digits": "Mengontrol jumlah digit desimal dalam output ketika karakteristik statistik adalah rata-rata atau median." } - }, - "options": { - "data": { - "entity_ids": "Entitas input", - "round_digits": "Presisi", - "type": "Karakteristik statistik" - }, - "description": "Buat sensor yang menghitung nilai min, maks, rata-rata, atau median dari sejumlah sensor input." } } }, diff --git a/homeassistant/components/min_max/translations/it.json b/homeassistant/components/min_max/translations/it.json index 49c715a7507..4574b80e863 100644 --- a/homeassistant/components/min_max/translations/it.json +++ b/homeassistant/components/min_max/translations/it.json @@ -27,14 +27,6 @@ "data_description": { "round_digits": "Controlla il numero di cifre decimali nell'output quando la caratteristica statistica \u00e8 media o mediana." } - }, - "options": { - "data": { - "entity_ids": "Entit\u00e0 di ingresso", - "round_digits": "Precisione", - "type": "Caratteristica statistica" - }, - "description": "Crea un sensore che calcoli un valore minimo, massimo, medio o mediano da un elenco di sensori di input." } } }, diff --git a/homeassistant/components/min_max/translations/ja.json b/homeassistant/components/min_max/translations/ja.json index c853a7c389e..f9e767082d6 100644 --- a/homeassistant/components/min_max/translations/ja.json +++ b/homeassistant/components/min_max/translations/ja.json @@ -27,14 +27,6 @@ "data_description": { "round_digits": "\u7d71\u8a08\u7684\u7279\u6027\u304c\u5e73\u5747\u5024\u307e\u305f\u306f\u4e2d\u592e\u5024\u306e\u5834\u5408\u306b\u3001\u51fa\u529b\u306b\u542b\u307e\u308c\u308b\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002" } - }, - "options": { - "data": { - "entity_ids": "\u5165\u529b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3", - "round_digits": "\u7cbe\u5ea6", - "type": "\u7d71\u8a08\u7684\u7279\u6027" - }, - "description": "\u7d71\u8a08\u7684\u7279\u6027\u304c\u5e73\u5747\u307e\u305f\u306f\u4e2d\u592e\u5024\u306a\u5834\u5408\u306e\u7cbe\u5ea6\u3067\u3001\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/min_max/translations/ko.json b/homeassistant/components/min_max/translations/ko.json new file mode 100644 index 00000000000..3b9bcebca94 --- /dev/null +++ b/homeassistant/components/min_max/translations/ko.json @@ -0,0 +1,34 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_ids": "\uad6c\uc131\uc694\uc18c \uc785\ub825", + "name": "\uc774\ub984", + "round_digits": "\uc18c\uc218\uc810", + "type": "\ud1b5\uacc4\uc801 \ud2b9\uc131" + }, + "data_description": { + "round_digits": "\ud1b5\uacc4\uc801 \ud2b9\uc131\uc774 \ud3c9\uade0 \ub610\ub294 \uc911\uc559\uac12\uc77c \ub54c\uc758 \uc18c\uc218\uc810 \uc790\ub9bf\uc218\ub97c \uacb0\uc815\ud569\ub2c8\ub2e4." + }, + "description": "\ucd5c\uc18c, \ucd5c\ub300, \ud3c9\uade0 \ub610\ub294 \uc911\uc559\uac12\uc744 \uacc4\uc0b0\ud558\ub294 \uc13c\uc11c\ub97c \uc0dd\uc131\ud569\ub2c8\ub2e4.", + "title": "\ucd5c\uc18c/\ucd5c\ub300/\ud3c9\uade0/\uc911\uc559\uac12 \uc13c\uc11c \ucd94\uac00" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "entity_ids": "\uad6c\uc131\uc694\uc18c \uc785\ub825", + "round_digits": "\uc18c\uc218\uc810", + "type": "\ud1b5\uacc4\uc801 \ud2b9\uc131" + }, + "data_description": { + "round_digits": "\ud1b5\uacc4\uc801 \ud2b9\uc131\uc774 \ud3c9\uade0 \ub610\ub294 \uc911\uc559\uac12\uc77c \ub54c\uc758 \uc18c\uc218\uc810 \uc790\ub9bf\uc218\ub97c \uacb0\uc815\ud569\ub2c8\ub2e4." + } + } + } + }, + "title": "\ucd5c\uc18c / \ucd5c\ub300 / \ud3c9\uade0 / \uc911\uc559\uac12 \uc13c\uc11c" +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/nl.json b/homeassistant/components/min_max/translations/nl.json index 36f09f97d54..75d8d69357b 100644 --- a/homeassistant/components/min_max/translations/nl.json +++ b/homeassistant/components/min_max/translations/nl.json @@ -27,14 +27,6 @@ "data_description": { "round_digits": "Regelt het aantal decimale cijfers in de uitvoer wanneer de statistische eigenschap gemiddelde of mediaan is." } - }, - "options": { - "data": { - "entity_ids": "Invoer entiteiten", - "round_digits": "Precisie", - "type": "Statistische karakteristieken" - }, - "description": "Precisie bepaalt het aantal decimale cijfers wanneer het statistische kenmerk gemiddelde of mediaan is." } } }, diff --git a/homeassistant/components/min_max/translations/no.json b/homeassistant/components/min_max/translations/no.json index 798430cec03..5c9391b374f 100644 --- a/homeassistant/components/min_max/translations/no.json +++ b/homeassistant/components/min_max/translations/no.json @@ -27,14 +27,6 @@ "data_description": { "round_digits": "Styrer antall desimaler i utdata n\u00e5r statistikkkarakteristikken er gjennomsnitt eller median." } - }, - "options": { - "data": { - "entity_ids": "Inndataenheter", - "round_digits": "Presisjon", - "type": "Statistisk karakteristikk" - }, - "description": "Lag en sensor som beregner en min, maks, middelverdi eller medianverdi fra en liste over inngangssensorer." } } }, diff --git a/homeassistant/components/min_max/translations/pl.json b/homeassistant/components/min_max/translations/pl.json index 0a29ad78339..cf4157eb612 100644 --- a/homeassistant/components/min_max/translations/pl.json +++ b/homeassistant/components/min_max/translations/pl.json @@ -27,14 +27,6 @@ "data_description": { "round_digits": "Kontroluje liczb\u0119 cyfr dziesi\u0119tnych w danych wyj\u015bciowych, gdy charakterystyka statystyki jest \u015bredni\u0105 lub median\u0105." } - }, - "options": { - "data": { - "entity_ids": "Encje wej\u015bciowe", - "round_digits": "Precyzja", - "type": "Charakterystyka statystyczna" - }, - "description": "Utw\u00f3rz sensor, kt\u00f3ry oblicza warto\u015b\u0107 minimaln\u0105, maksymaln\u0105, \u015bredni\u0105 lub median\u0119 z listy sensor\u00f3w wej\u015bciowych." } } }, diff --git a/homeassistant/components/min_max/translations/pt-BR.json b/homeassistant/components/min_max/translations/pt-BR.json index 9c1c77c304a..5a2c9a85d4f 100644 --- a/homeassistant/components/min_max/translations/pt-BR.json +++ b/homeassistant/components/min_max/translations/pt-BR.json @@ -27,14 +27,6 @@ "data_description": { "round_digits": "Controla o n\u00famero de d\u00edgitos decimais na sa\u00edda quando a caracter\u00edstica estat\u00edstica \u00e9 m\u00e9dia ou mediana." } - }, - "options": { - "data": { - "entity_ids": "Entidades de entrada", - "round_digits": "Precis\u00e3o", - "type": "Caracter\u00edstica estat\u00edstica" - }, - "description": "Crie um sensor que calcula um valor m\u00ednimo, m\u00e1ximo, m\u00e9dio ou mediano de uma lista de sensores de entrada." } } }, diff --git a/homeassistant/components/min_max/translations/ru.json b/homeassistant/components/min_max/translations/ru.json index 551660fcabb..0fdd9198a72 100644 --- a/homeassistant/components/min_max/translations/ru.json +++ b/homeassistant/components/min_max/translations/ru.json @@ -27,14 +27,6 @@ "data_description": { "round_digits": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439, \u043a\u043e\u0433\u0434\u0430 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0430 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0441\u0440\u0435\u0434\u043d\u0435\u0439 \u0438\u043b\u0438 \u043c\u0435\u0434\u0438\u0430\u043d\u043d\u043e\u0439." } - }, - "options": { - "data": { - "entity_ids": "\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b", - "round_digits": "\u041e\u043a\u0440\u0443\u0433\u043b\u0435\u043d\u0438\u0435", - "type": "\u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0430" - }, - "description": "\u0412\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442 \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435, \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435, \u0441\u0440\u0435\u0434\u043d\u0435\u0435 \u0438\u043b\u0438 \u043c\u0435\u0434\u0438\u0430\u043d\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430 \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432." } } }, diff --git a/homeassistant/components/min_max/translations/tr.json b/homeassistant/components/min_max/translations/tr.json index 7f192888615..ab826ed913f 100644 --- a/homeassistant/components/min_max/translations/tr.json +++ b/homeassistant/components/min_max/translations/tr.json @@ -27,14 +27,6 @@ "data_description": { "round_digits": "\u0130statistik \u00f6zelli\u011fi ortalama veya medyan oldu\u011funda \u00e7\u0131kt\u0131daki ondal\u0131k basamak say\u0131s\u0131n\u0131 kontrol eder." } - }, - "options": { - "data": { - "entity_ids": "Giri\u015f varl\u0131klar\u0131", - "round_digits": "Hassas", - "type": "\u0130statistik \u00f6zelli\u011fi" - }, - "description": "Giri\u015f sens\u00f6rleri listesinden minimum, maksimum, ortalama veya medyan de\u011feri hesaplayan bir sens\u00f6r olu\u015fturun." } } }, diff --git a/homeassistant/components/min_max/translations/zh-Hans.json b/homeassistant/components/min_max/translations/zh-Hans.json index 9fb15dafd2a..bd0562d2e48 100644 --- a/homeassistant/components/min_max/translations/zh-Hans.json +++ b/homeassistant/components/min_max/translations/zh-Hans.json @@ -27,14 +27,6 @@ "data_description": { "round_digits": "\u5f53\u7edf\u8ba1\u9879\u4e3a\u5e73\u5747\u503c\u6216\u4e2d\u4f4d\u6570\u65f6\uff0c\u63a7\u5236\u8f93\u51fa\u7684\u5c0f\u6570\u4f4d\u6570\u3002" } - }, - "options": { - "data": { - "entity_ids": "\u8f93\u5165\u5b9e\u4f53", - "round_digits": "\u7cbe\u5ea6", - "type": "\u7edf\u8ba1\u9879" - }, - "description": "\u521b\u5efa\u4f20\u611f\u5668\u6765\u8ba1\u7b97\u591a\u4e2a\u8f93\u5165\u4f20\u611f\u5668\u503c\u7684\u6700\u5927\u503c\u3001\u6700\u5c0f\u503c\u3001\u5e73\u5747\u503c\u6216\u4e2d\u4f4d\u6570\u3002" } } }, diff --git a/homeassistant/components/min_max/translations/zh-Hant.json b/homeassistant/components/min_max/translations/zh-Hant.json index b0166e26cac..abdf3fe737f 100644 --- a/homeassistant/components/min_max/translations/zh-Hant.json +++ b/homeassistant/components/min_max/translations/zh-Hant.json @@ -27,14 +27,6 @@ "data_description": { "round_digits": "\u7576\u7d71\u8a08\u7279\u5fb5\u70ba\u5e73\u5747\u503c\u6216\u4e2d\u503c\u6642\u3001\u63a7\u5236\u8f38\u51fa\u5c0f\u6578\u4f4d\u6578\u3002" } - }, - "options": { - "data": { - "entity_ids": "\u8f38\u5165\u5be6\u9ad4", - "round_digits": "\u6e96\u78ba\u5ea6", - "type": "\u7d71\u8a08\u7279\u5fb5" - }, - "description": "\u65b0\u589e\u81ea\u8f38\u5165\u611f\u6e2c\u5668\u4e86\u8868\u4e2d\uff0c\u8a08\u7b97\u6700\u4f4e\u3001\u6700\u9ad8\u3001\u5e73\u5747\u503c\u6216\u4e2d\u503c\u611f\u6e2c\u5668\u3002" } } }, diff --git a/homeassistant/components/modem_callerid/translations/bg.json b/homeassistant/components/modem_callerid/translations/bg.json index 7383941d1ca..15ece1452ad 100644 --- a/homeassistant/components/modem_callerid/translations/bg.json +++ b/homeassistant/components/modem_callerid/translations/bg.json @@ -7,15 +7,11 @@ "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" + } } } } diff --git a/homeassistant/components/modem_callerid/translations/ca.json b/homeassistant/components/modem_callerid/translations/ca.json index d94d4cf392d..996406acdbe 100644 --- a/homeassistant/components/modem_callerid/translations/ca.json +++ b/homeassistant/components/modem_callerid/translations/ca.json @@ -10,16 +10,14 @@ }, "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" + "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." }, "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" + "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." } } } diff --git a/homeassistant/components/modem_callerid/translations/de.json b/homeassistant/components/modem_callerid/translations/de.json index 0bc505be5c8..51056a601c3 100644 --- a/homeassistant/components/modem_callerid/translations/de.json +++ b/homeassistant/components/modem_callerid/translations/de.json @@ -10,16 +10,14 @@ }, "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" + "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." }, "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" + "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." } } } diff --git a/homeassistant/components/modem_callerid/translations/el.json b/homeassistant/components/modem_callerid/translations/el.json index 6d3961cc258..cf2ef08b92e 100644 --- a/homeassistant/components/modem_callerid/translations/el.json +++ b/homeassistant/components/modem_callerid/translations/el.json @@ -10,16 +10,14 @@ }, "step": { "usb_confirm": { - "description": "\u03a0\u03c1\u03cc\u03ba\u03b5\u03b9\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03ba\u03bb\u03ae\u03c3\u03b5\u03b9\u03c2 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae\u03c2 \u03c4\u03b7\u03bb\u03b5\u03c6\u03c9\u03bd\u03af\u03b1\u03c2 \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c6\u03c9\u03bd\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd \u03bc\u03cc\u03bd\u03c4\u03b5\u03bc CX93001. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03ae\u03c3\u03b5\u03b9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ba\u03b1\u03bb\u03bf\u03cd\u03bd\u03c4\u03bf\u03c2 \u03bc\u03b5 \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b1\u03c0\u03cc\u03c1\u03c1\u03b9\u03c8\u03b7\u03c2 \u03bc\u03b9\u03b1\u03c2 \u03b5\u03b9\u03c3\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03b7\u03c2 \u03ba\u03bb\u03ae\u03c3\u03b7\u03c2.", - "title": "\u039c\u03cc\u03bd\u03c4\u03b5\u03bc \u03c4\u03b7\u03bb\u03b5\u03c6\u03ce\u03bd\u03bf\u03c5" + "description": "\u03a0\u03c1\u03cc\u03ba\u03b5\u03b9\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03ba\u03bb\u03ae\u03c3\u03b5\u03b9\u03c2 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae\u03c2 \u03c4\u03b7\u03bb\u03b5\u03c6\u03c9\u03bd\u03af\u03b1\u03c2 \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c6\u03c9\u03bd\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd \u03bc\u03cc\u03bd\u03c4\u03b5\u03bc CX93001. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03ae\u03c3\u03b5\u03b9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ba\u03b1\u03bb\u03bf\u03cd\u03bd\u03c4\u03bf\u03c2 \u03bc\u03b5 \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b1\u03c0\u03cc\u03c1\u03c1\u03b9\u03c8\u03b7\u03c2 \u03bc\u03b9\u03b1\u03c2 \u03b5\u03b9\u03c3\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03b7\u03c2 \u03ba\u03bb\u03ae\u03c3\u03b7\u03c2." }, "user": { "data": { "name": "\u038c\u03bd\u03bf\u03bc\u03b1", "port": "\u0398\u03cd\u03c1\u03b1" }, - "description": "\u03a0\u03c1\u03cc\u03ba\u03b5\u03b9\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03ba\u03bb\u03ae\u03c3\u03b5\u03b9\u03c2 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae\u03c2 \u03c4\u03b7\u03bb\u03b5\u03c6\u03c9\u03bd\u03af\u03b1\u03c2 \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c6\u03c9\u03bd\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd \u03bc\u03cc\u03bd\u03c4\u03b5\u03bc CX93001. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03ae\u03c3\u03b5\u03b9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ba\u03b1\u03bb\u03bf\u03cd\u03bd\u03c4\u03bf\u03c2 \u03bc\u03b5 \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b1\u03c0\u03cc\u03c1\u03c1\u03b9\u03c8\u03b7\u03c2 \u03bc\u03b9\u03b1\u03c2 \u03b5\u03b9\u03c3\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03b7\u03c2 \u03ba\u03bb\u03ae\u03c3\u03b7\u03c2.", - "title": "\u039c\u03cc\u03bd\u03c4\u03b5\u03bc \u03c4\u03b7\u03bb\u03b5\u03c6\u03ce\u03bd\u03bf\u03c5" + "description": "\u03a0\u03c1\u03cc\u03ba\u03b5\u03b9\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03ba\u03bb\u03ae\u03c3\u03b5\u03b9\u03c2 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae\u03c2 \u03c4\u03b7\u03bb\u03b5\u03c6\u03c9\u03bd\u03af\u03b1\u03c2 \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c6\u03c9\u03bd\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd \u03bc\u03cc\u03bd\u03c4\u03b5\u03bc CX93001. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03ae\u03c3\u03b5\u03b9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ba\u03b1\u03bb\u03bf\u03cd\u03bd\u03c4\u03bf\u03c2 \u03bc\u03b5 \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b1\u03c0\u03cc\u03c1\u03c1\u03b9\u03c8\u03b7\u03c2 \u03bc\u03b9\u03b1\u03c2 \u03b5\u03b9\u03c3\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03b7\u03c2 \u03ba\u03bb\u03ae\u03c3\u03b7\u03c2." } } } diff --git a/homeassistant/components/modem_callerid/translations/en.json b/homeassistant/components/modem_callerid/translations/en.json index 5450a930ff3..1cad101f36d 100644 --- a/homeassistant/components/modem_callerid/translations/en.json +++ b/homeassistant/components/modem_callerid/translations/en.json @@ -10,16 +10,14 @@ }, "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" + "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." }, "user": { "data": { "name": "Name", "port": "Port" }, - "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" + "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." } } } diff --git a/homeassistant/components/modem_callerid/translations/es.json b/homeassistant/components/modem_callerid/translations/es.json index 3a9853dd5f7..6c62f64a1d6 100644 --- a/homeassistant/components/modem_callerid/translations/es.json +++ b/homeassistant/components/modem_callerid/translations/es.json @@ -10,16 +10,14 @@ }, "step": { "usb_confirm": { - "description": "Se trata de una integraci\u00f3n para llamadas a tel\u00e9fonos fijos que utilizan un m\u00f3dem de voz CX93001. Puede recuperar la informaci\u00f3n del identificador de llamadas con una opci\u00f3n para rechazar una llamada entrante.", - "title": "M\u00f3dem telef\u00f3nico" + "description": "Se trata de una integraci\u00f3n para llamadas a tel\u00e9fonos fijos que utilizan un m\u00f3dem de voz CX93001. Puede recuperar la informaci\u00f3n del identificador de llamadas con una opci\u00f3n para rechazar una llamada entrante." }, "user": { "data": { "name": "Nombre", "port": "Puerto" }, - "description": "Se trata de una integraci\u00f3n para llamadas a tel\u00e9fonos fijos que utilizan un m\u00f3dem de voz CX93001. Puede recuperar la informaci\u00f3n del identificador de llamadas con una opci\u00f3n para rechazar una llamada entrante.", - "title": "M\u00f3dem telef\u00f3nico" + "description": "Se trata de una integraci\u00f3n para llamadas a tel\u00e9fonos fijos que utilizan un m\u00f3dem de voz CX93001. Puede recuperar la informaci\u00f3n del identificador de llamadas con una opci\u00f3n para rechazar una llamada entrante." } } } diff --git a/homeassistant/components/modem_callerid/translations/et.json b/homeassistant/components/modem_callerid/translations/et.json index 463d24e8f9f..10f80b53068 100644 --- a/homeassistant/components/modem_callerid/translations/et.json +++ b/homeassistant/components/modem_callerid/translations/et.json @@ -10,16 +10,14 @@ }, "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" + "description": "See on sidumine fiksv\u00f5rgu telefonile kasutades CX93001 modemit. See v\u00f5ib hankida helistaja ID teabe koos sissetulevast k\u00f5nestloobumise v\u00f5imalusega." }, "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" + "description": "See on sidumine fiksv\u00f5rgu telefonile kasutades CX93001 modemit. See v\u00f5ib hankida helistaja ID teabe koos sissetulevast k\u00f5nestloobumise v\u00f5imalusega." } } } diff --git a/homeassistant/components/modem_callerid/translations/fr.json b/homeassistant/components/modem_callerid/translations/fr.json index 696927d66db..36a07399836 100644 --- a/homeassistant/components/modem_callerid/translations/fr.json +++ b/homeassistant/components/modem_callerid/translations/fr.json @@ -10,16 +10,14 @@ }, "step": { "usb_confirm": { - "description": "Il s'agit d'une int\u00e9gration pour les appels fixes utilisant un modem vocal CX93001. Cela peut r\u00e9cup\u00e9rer les informations d'identification de l'appelant avec une option pour rejeter un appel entrant.", - "title": "Modem t\u00e9l\u00e9phonique" + "description": "Il s'agit d'une int\u00e9gration pour les appels fixes utilisant un modem vocal CX93001. Cela peut r\u00e9cup\u00e9rer les informations d'identification de l'appelant avec une option pour rejeter un appel entrant." }, "user": { "data": { "name": "Nom", "port": "Port" }, - "description": "Il s'agit d'une int\u00e9gration pour les appels fixes utilisant un modem vocal CX93001. Cela peut r\u00e9cup\u00e9rer les informations d'identification de l'appelant avec une option pour rejeter un appel entrant.", - "title": "Modem t\u00e9l\u00e9phonique" + "description": "Il s'agit d'une int\u00e9gration pour les appels fixes utilisant un modem vocal CX93001. Cela peut r\u00e9cup\u00e9rer les informations d'identification de l'appelant avec une option pour rejeter un appel entrant." } } } diff --git a/homeassistant/components/modem_callerid/translations/hu.json b/homeassistant/components/modem_callerid/translations/hu.json index 2a53c24d902..5e3f7727314 100644 --- a/homeassistant/components/modem_callerid/translations/hu.json +++ b/homeassistant/components/modem_callerid/translations/hu.json @@ -10,16 +10,14 @@ }, "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" + "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." }, "user": { "data": { "name": "Elnevez\u00e9s", "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" + "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." } } } diff --git a/homeassistant/components/modem_callerid/translations/id.json b/homeassistant/components/modem_callerid/translations/id.json index 3aa455d5f4c..accce6cea78 100644 --- a/homeassistant/components/modem_callerid/translations/id.json +++ b/homeassistant/components/modem_callerid/translations/id.json @@ -10,16 +10,14 @@ }, "step": { "usb_confirm": { - "description": "Integrasi ini adalah integrasi untuk panggilan darat menggunakan modem suara CX93001. Integrasi dapat mengambil informasi ID pemanggil dengan opsi untuk menolak panggilan masuk.", - "title": "Modem Telepon" + "description": "Integrasi ini adalah integrasi untuk panggilan darat menggunakan modem suara CX93001. Integrasi dapat mengambil informasi ID pemanggil dengan opsi untuk menolak panggilan masuk." }, "user": { "data": { "name": "Nama", "port": "Port" }, - "description": "Integrasi ini adalah integrasi untuk panggilan darat menggunakan modem suara CX93001. Integrasi dapat mengambil informasi ID pemanggil dengan opsi untuk menolak panggilan masuk.", - "title": "Modem Telepon" + "description": "Integrasi ini adalah integrasi untuk panggilan darat menggunakan modem suara CX93001. Integrasi dapat mengambil informasi ID pemanggil dengan opsi untuk menolak panggilan masuk." } } } diff --git a/homeassistant/components/modem_callerid/translations/it.json b/homeassistant/components/modem_callerid/translations/it.json index 65d1c74f956..1282541da5c 100644 --- a/homeassistant/components/modem_callerid/translations/it.json +++ b/homeassistant/components/modem_callerid/translations/it.json @@ -10,16 +10,14 @@ }, "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" + "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." }, "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" + "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." } } } diff --git a/homeassistant/components/modem_callerid/translations/ja.json b/homeassistant/components/modem_callerid/translations/ja.json index ab5ab732931..a5b9df933ef 100644 --- a/homeassistant/components/modem_callerid/translations/ja.json +++ b/homeassistant/components/modem_callerid/translations/ja.json @@ -10,16 +10,14 @@ }, "step": { "usb_confirm": { - "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002", - "title": "Phone\u30e2\u30c7\u30e0" + "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002" }, "user": { "data": { "name": "\u540d\u524d", "port": "\u30dd\u30fc\u30c8" }, - "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002", - "title": "Phone\u30e2\u30c7\u30e0" + "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/modem_callerid/translations/nl.json b/homeassistant/components/modem_callerid/translations/nl.json index 4077a03105b..6e26dd3d1c0 100644 --- a/homeassistant/components/modem_callerid/translations/nl.json +++ b/homeassistant/components/modem_callerid/translations/nl.json @@ -10,16 +10,14 @@ }, "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" + "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." }, "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" + "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." } } } diff --git a/homeassistant/components/modem_callerid/translations/no.json b/homeassistant/components/modem_callerid/translations/no.json index 2e1103b5092..768d8a29ce3 100644 --- a/homeassistant/components/modem_callerid/translations/no.json +++ b/homeassistant/components/modem_callerid/translations/no.json @@ -10,16 +10,14 @@ }, "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" + "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." }, "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" + "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." } } } diff --git a/homeassistant/components/modem_callerid/translations/pl.json b/homeassistant/components/modem_callerid/translations/pl.json index 5101fc574a0..b608a467395 100644 --- a/homeassistant/components/modem_callerid/translations/pl.json +++ b/homeassistant/components/modem_callerid/translations/pl.json @@ -10,16 +10,14 @@ }, "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.", - "title": "Modem telefoniczny" + "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.", - "title": "Modem telefoniczny" + "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." } } } diff --git a/homeassistant/components/modem_callerid/translations/pt-BR.json b/homeassistant/components/modem_callerid/translations/pt-BR.json index 39394d0752e..d82fbf8e5b6 100644 --- a/homeassistant/components/modem_callerid/translations/pt-BR.json +++ b/homeassistant/components/modem_callerid/translations/pt-BR.json @@ -10,16 +10,14 @@ }, "step": { "usb_confirm": { - "description": "Esta \u00e9 uma integra\u00e7\u00e3o para chamadas fixas usando um modem de voz CX93001. Isso pode recuperar informa\u00e7\u00f5es de identifica\u00e7\u00e3o de chamadas com a op\u00e7\u00e3o de rejeitar uma chamada recebida.", - "title": "Modem do telefone" + "description": "Esta \u00e9 uma integra\u00e7\u00e3o para chamadas fixas usando um modem de voz CX93001. Isso pode recuperar informa\u00e7\u00f5es de identifica\u00e7\u00e3o de chamadas com a op\u00e7\u00e3o de rejeitar uma chamada recebida." }, "user": { "data": { "name": "Nome", "port": "Porta" }, - "description": "Esta \u00e9 uma integra\u00e7\u00e3o para chamadas fixas usando um modem de voz CX93001. Isso pode recuperar informa\u00e7\u00f5es de identifica\u00e7\u00e3o de chamadas com a op\u00e7\u00e3o de rejeitar uma chamada recebida.", - "title": "Modem do telefone" + "description": "Esta \u00e9 uma integra\u00e7\u00e3o para chamadas fixas usando um modem de voz CX93001. Isso pode recuperar informa\u00e7\u00f5es de identifica\u00e7\u00e3o de chamadas com a op\u00e7\u00e3o de rejeitar uma chamada recebida." } } } diff --git a/homeassistant/components/modem_callerid/translations/ru.json b/homeassistant/components/modem_callerid/translations/ru.json index f5fa5061a4a..7217131b78f 100644 --- a/homeassistant/components/modem_callerid/translations/ru.json +++ b/homeassistant/components/modem_callerid/translations/ru.json @@ -10,16 +10,14 @@ }, "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" + "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." }, "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" + "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." } } } diff --git a/homeassistant/components/modem_callerid/translations/tr.json b/homeassistant/components/modem_callerid/translations/tr.json index eec011d8c6f..373fe0d5e2f 100644 --- a/homeassistant/components/modem_callerid/translations/tr.json +++ b/homeassistant/components/modem_callerid/translations/tr.json @@ -10,16 +10,14 @@ }, "step": { "usb_confirm": { - "description": "Bu, CX93001 sesli modem kullanan sabit hat aramalar\u0131 i\u00e7in bir entegrasyondur. Bu, gelen bir aramay\u0131 reddetme se\u00e7ene\u011fi ile arayan kimli\u011fi bilgilerini alabilir.", - "title": "Telefon Modemi" + "description": "Bu, CX93001 sesli modem kullanan sabit hat aramalar\u0131 i\u00e7in bir entegrasyondur. Bu, gelen bir aramay\u0131 reddetme se\u00e7ene\u011fi ile arayan kimli\u011fi bilgilerini alabilir." }, "user": { "data": { "name": "Ad", "port": "Port" }, - "description": "Bu, CX93001 sesli modem kullanan sabit hat aramalar\u0131 i\u00e7in bir entegrasyondur. Bu, gelen bir aramay\u0131 reddetme se\u00e7ene\u011fi ile arayan kimli\u011fi bilgilerini alabilir.", - "title": "Telefon Modemi" + "description": "Bu, CX93001 sesli modem kullanan sabit hat aramalar\u0131 i\u00e7in bir entegrasyondur. Bu, gelen bir aramay\u0131 reddetme se\u00e7ene\u011fi ile arayan kimli\u011fi bilgilerini alabilir." } } } diff --git a/homeassistant/components/modem_callerid/translations/zh-Hant.json b/homeassistant/components/modem_callerid/translations/zh-Hant.json index 542a12e8c5d..2d07c73cb1e 100644 --- a/homeassistant/components/modem_callerid/translations/zh-Hant.json +++ b/homeassistant/components/modem_callerid/translations/zh-Hant.json @@ -10,16 +10,14 @@ }, "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" + "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" }, "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" + "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" } } } diff --git a/homeassistant/components/modern_forms/translations/bg.json b/homeassistant/components/modern_forms/translations/bg.json index 4200524546f..d2124752f04 100644 --- a/homeassistant/components/modern_forms/translations/bg.json +++ b/homeassistant/components/modern_forms/translations/bg.json @@ -9,15 +9,11 @@ }, "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\u0432\u0430\u043d\u0435\u0442\u043e?" - }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" } } } - }, - "title": "Modern Forms" + } } \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/ca.json b/homeassistant/components/modern_forms/translations/ca.json index e7a70e80b93..4296368ac89 100644 --- a/homeassistant/components/modern_forms/translations/ca.json +++ b/homeassistant/components/modern_forms/translations/ca.json @@ -9,9 +9,6 @@ }, "flow_title": "{name}", "step": { - "confirm": { - "description": "Vols comen\u00e7ar la configuraci\u00f3?" - }, "user": { "data": { "host": "Amfitri\u00f3" @@ -23,6 +20,5 @@ "title": "Dispositiu ventilador Modern Forms descobert" } } - }, - "title": "Modern Forms" + } } \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/de.json b/homeassistant/components/modern_forms/translations/de.json index bf16c9532fc..3c9280b22f3 100644 --- a/homeassistant/components/modern_forms/translations/de.json +++ b/homeassistant/components/modern_forms/translations/de.json @@ -9,9 +9,6 @@ }, "flow_title": "{name}", "step": { - "confirm": { - "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" - }, "user": { "data": { "host": "Host" @@ -23,6 +20,5 @@ "title": "Erkannter Modern Forms Ventilator" } } - }, - "title": "Modern Forms" + } } \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/el.json b/homeassistant/components/modern_forms/translations/el.json index b8cb77ffa99..10811327076 100644 --- a/homeassistant/components/modern_forms/translations/el.json +++ b/homeassistant/components/modern_forms/translations/el.json @@ -9,9 +9,6 @@ }, "flow_title": "{name}", "step": { - "confirm": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7;" - }, "user": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" @@ -23,6 +20,5 @@ "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b1\u03bd\u03b5\u03bc\u03b9\u03c3\u03c4\u03ae\u03c1\u03c9\u03bd Modern Forms" } } - }, - "title": "Modern Forms" + } } \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/en.json b/homeassistant/components/modern_forms/translations/en.json index 6c51e42a2d1..f61cd67cfde 100644 --- a/homeassistant/components/modern_forms/translations/en.json +++ b/homeassistant/components/modern_forms/translations/en.json @@ -9,9 +9,6 @@ }, "flow_title": "{name}", "step": { - "confirm": { - "description": "Do you want to start set up?" - }, "user": { "data": { "host": "Host" @@ -23,6 +20,5 @@ "title": "Discovered Modern Forms fan device" } } - }, - "title": "Modern Forms" + } } \ 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 4512c0bc595..29b51c03cf1 100644 --- a/homeassistant/components/modern_forms/translations/es.json +++ b/homeassistant/components/modern_forms/translations/es.json @@ -9,9 +9,6 @@ }, "flow_title": "{name}", "step": { - "confirm": { - "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" - }, "user": { "data": { "host": "Host" @@ -23,6 +20,5 @@ "title": "Dispositivo de ventilador de Modern Forms descubierto" } } - }, - "title": "Modern Forms" + } } \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/et.json b/homeassistant/components/modern_forms/translations/et.json index 7140888db84..155c081f67c 100644 --- a/homeassistant/components/modern_forms/translations/et.json +++ b/homeassistant/components/modern_forms/translations/et.json @@ -9,9 +9,6 @@ }, "flow_title": "{name}", "step": { - "confirm": { - "description": "Kas soovid alustada seadistamist?" - }, "user": { "data": { "host": "Host" @@ -23,6 +20,5 @@ "title": "Leitud Modern Forms ventilaator" } } - }, - "title": "Modern Forms" + } } \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/fr.json b/homeassistant/components/modern_forms/translations/fr.json index d68f4a7f680..85b7681057a 100644 --- a/homeassistant/components/modern_forms/translations/fr.json +++ b/homeassistant/components/modern_forms/translations/fr.json @@ -9,9 +9,6 @@ }, "flow_title": "{name}", "step": { - "confirm": { - "description": "Voulez-vous commencer la configuration\u00a0?" - }, "user": { "data": { "host": "H\u00f4te" @@ -23,6 +20,5 @@ "title": "D\u00e9couverte du dispositif de ventilateur Modern Forms" } } - }, - "title": "Formes modernes" + } } \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/he.json b/homeassistant/components/modern_forms/translations/he.json index 701a3689598..1cd249b4daa 100644 --- a/homeassistant/components/modern_forms/translations/he.json +++ b/homeassistant/components/modern_forms/translations/he.json @@ -9,9 +9,6 @@ }, "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": { "host": "\u05de\u05d0\u05e8\u05d7" diff --git a/homeassistant/components/modern_forms/translations/hu.json b/homeassistant/components/modern_forms/translations/hu.json index 49f5da5339f..a4ef399188c 100644 --- a/homeassistant/components/modern_forms/translations/hu.json +++ b/homeassistant/components/modern_forms/translations/hu.json @@ -9,9 +9,6 @@ }, "flow_title": "{name}", "step": { - "confirm": { - "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" - }, "user": { "data": { "host": "C\u00edm" @@ -23,6 +20,5 @@ "title": "Felfedezte a Modern Forms rajong\u00f3i eszk\u00f6zt" } } - }, - "title": "Modern Forms" + } } \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/id.json b/homeassistant/components/modern_forms/translations/id.json index c85667c4449..703ffce37be 100644 --- a/homeassistant/components/modern_forms/translations/id.json +++ b/homeassistant/components/modern_forms/translations/id.json @@ -9,9 +9,6 @@ }, "flow_title": "{name}", "step": { - "confirm": { - "description": "Ingin memulai penyiapan?" - }, "user": { "data": { "host": "Host" @@ -23,6 +20,5 @@ "title": "Perangkat kipas Modern Forms yang Ditemukan" } } - }, - "title": "Modern Forms" + } } \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/it.json b/homeassistant/components/modern_forms/translations/it.json index 18f1d5f503a..8feeab0a058 100644 --- a/homeassistant/components/modern_forms/translations/it.json +++ b/homeassistant/components/modern_forms/translations/it.json @@ -9,9 +9,6 @@ }, "flow_title": "{name}", "step": { - "confirm": { - "description": "Vuoi iniziare la configurazione?" - }, "user": { "data": { "host": "Host" @@ -23,6 +20,5 @@ "title": "Rilevato il dispositivo ventilatore di Modern Forms" } } - }, - "title": "Modern Forms" + } } \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/ja.json b/homeassistant/components/modern_forms/translations/ja.json index 7806675b5c6..064f4b8f4cd 100644 --- a/homeassistant/components/modern_forms/translations/ja.json +++ b/homeassistant/components/modern_forms/translations/ja.json @@ -9,9 +9,6 @@ }, "flow_title": "{name}", "step": { - "confirm": { - "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" - }, "user": { "data": { "host": "\u30db\u30b9\u30c8" @@ -23,6 +20,5 @@ "title": "Modern Forms fan device\u3092\u767a\u898b" } } - }, - "title": "Modern Forms" + } } \ 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 ccbdf7d5b44..ecb65db17d8 100644 --- a/homeassistant/components/modern_forms/translations/nl.json +++ b/homeassistant/components/modern_forms/translations/nl.json @@ -9,9 +9,6 @@ }, "flow_title": "{name}", "step": { - "confirm": { - "description": "Wilt u beginnen met instellen?" - }, "user": { "data": { "host": "Host" @@ -23,6 +20,5 @@ "title": "Ontdekt Modern Forms ventilator apparaat" } } - }, - "title": "Modern Forms" + } } \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/no.json b/homeassistant/components/modern_forms/translations/no.json index e7da915d39b..a96d1617117 100644 --- a/homeassistant/components/modern_forms/translations/no.json +++ b/homeassistant/components/modern_forms/translations/no.json @@ -9,9 +9,6 @@ }, "flow_title": "{name}", "step": { - "confirm": { - "description": "Vil du starte oppsettet?" - }, "user": { "data": { "host": "Vert" @@ -23,6 +20,5 @@ "title": "Oppdaget Modern Forms-vifteenhet" } } - }, - "title": "Moderne former" + } } \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/pl.json b/homeassistant/components/modern_forms/translations/pl.json index f68be10b7cd..8437f00f2f8 100644 --- a/homeassistant/components/modern_forms/translations/pl.json +++ b/homeassistant/components/modern_forms/translations/pl.json @@ -9,9 +9,6 @@ }, "flow_title": "{name}", "step": { - "confirm": { - "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" - }, "user": { "data": { "host": "Nazwa hosta lub adres IP" @@ -23,6 +20,5 @@ "title": "Wykryto wentylator Modern Forms" } } - }, - "title": "Modern Forms" + } } \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/pt-BR.json b/homeassistant/components/modern_forms/translations/pt-BR.json index 07a68e2fb84..63b5f7e12f5 100644 --- a/homeassistant/components/modern_forms/translations/pt-BR.json +++ b/homeassistant/components/modern_forms/translations/pt-BR.json @@ -9,9 +9,6 @@ }, "flow_title": "{name}", "step": { - "confirm": { - "description": "Deseja iniciar a configura\u00e7\u00e3o?" - }, "user": { "data": { "host": "Nome do host" @@ -23,6 +20,5 @@ "title": "Dispositivo de ventilador do Modern Forms descoberto" } } - }, - "title": "Formas modernas" + } } \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/ru.json b/homeassistant/components/modern_forms/translations/ru.json index 68b46c57f47..86f2080a90f 100644 --- a/homeassistant/components/modern_forms/translations/ru.json +++ b/homeassistant/components/modern_forms/translations/ru.json @@ -9,9 +9,6 @@ }, "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": { "host": "\u0425\u043e\u0441\u0442" @@ -23,6 +20,5 @@ "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Modern Forms" } } - }, - "title": "Modern Forms" + } } \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/tr.json b/homeassistant/components/modern_forms/translations/tr.json index e27881802fb..d80e905e17a 100644 --- a/homeassistant/components/modern_forms/translations/tr.json +++ b/homeassistant/components/modern_forms/translations/tr.json @@ -9,9 +9,6 @@ }, "flow_title": "{name}", "step": { - "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" - }, "user": { "data": { "host": "Sunucu" @@ -23,6 +20,5 @@ "title": "Ke\u015ffedilen Modern Formlar fan cihaz\u0131" } } - }, - "title": "Modern Formlar" + } } \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/zh-Hant.json b/homeassistant/components/modern_forms/translations/zh-Hant.json index df3ebf486c7..054ad19ee1a 100644 --- a/homeassistant/components/modern_forms/translations/zh-Hant.json +++ b/homeassistant/components/modern_forms/translations/zh-Hant.json @@ -9,9 +9,6 @@ }, "flow_title": "{name}", "step": { - "confirm": { - "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" - }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef" @@ -23,6 +20,5 @@ "title": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Modern Forms \u98a8\u6247\u88dd\u7f6e" } } - }, - "title": "Modern Forms" + } } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/ko.json b/homeassistant/components/moon/translations/ko.json new file mode 100644 index 00000000000..758f3336cd4 --- /dev/null +++ b/homeassistant/components/moon/translations/ko.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/bg.json b/homeassistant/components/motion_blinds/translations/bg.json index dad41cb1999..e78d7032040 100644 --- a/homeassistant/components/motion_blinds/translations/bg.json +++ b/homeassistant/components/motion_blinds/translations/bg.json @@ -4,9 +4,6 @@ "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": { @@ -20,7 +17,6 @@ }, "user": { "data": { - "api_key": "API \u043a\u043b\u044e\u0447", "host": "IP \u0430\u0434\u0440\u0435\u0441" } } diff --git a/homeassistant/components/motion_blinds/translations/ca.json b/homeassistant/components/motion_blinds/translations/ca.json index 4ec912aa25c..8de2711298b 100644 --- a/homeassistant/components/motion_blinds/translations/ca.json +++ b/homeassistant/components/motion_blinds/translations/ca.json @@ -6,18 +6,15 @@ "connection_error": "Ha fallat la connexi\u00f3" }, "error": { - "discovery_error": "No s'ha pogut descobrir cap Motion Gateway", - "invalid_interface": "Interf\u00edcie de xarxa no v\u00e0lida" + "discovery_error": "No s'ha pogut descobrir cap Motion Gateway" }, "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { - "api_key": "Clau API", - "interface": "Interf\u00edcie de xarxa a utilitzar" + "api_key": "Clau API" }, - "description": "Necessitar\u00e0s la clau API de 16 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.", - "title": "Motion Blinds" + "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." }, "select": { "data": { @@ -28,11 +25,9 @@ }, "user": { "data": { - "api_key": "Clau API", "host": "Adre\u00e7a IP" }, - "description": "Connecta el teu Motion Gateway, si no es configura l'adre\u00e7a IP, s'utilitza el descobriment autom\u00e0tic", - "title": "Motion Blinds" + "description": "Connecta el teu Motion Gateway, si no es configura l'adre\u00e7a IP, s'utilitza el descobriment autom\u00e0tic" } } }, @@ -41,9 +36,7 @@ "init": { "data": { "wait_for_push": "Espera l'entrada multidifusi\u00f3 en actualitzar" - }, - "description": "Especifica configuracions opcionals", - "title": "Motion Blinds" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/cs.json b/homeassistant/components/motion_blinds/translations/cs.json index 899f04d7cd4..be4dd0eb8bf 100644 --- a/homeassistant/components/motion_blinds/translations/cs.json +++ b/homeassistant/components/motion_blinds/translations/cs.json @@ -19,10 +19,8 @@ }, "user": { "data": { - "api_key": "Kl\u00ed\u010d API", "host": "IP adresa" - }, - "title": "Motion Blinds" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/de.json b/homeassistant/components/motion_blinds/translations/de.json index d3b6b219516..b483ea35dd5 100644 --- a/homeassistant/components/motion_blinds/translations/de.json +++ b/homeassistant/components/motion_blinds/translations/de.json @@ -6,18 +6,15 @@ "connection_error": "Verbindung fehlgeschlagen" }, "error": { - "discovery_error": "Motion-Gateway konnte nicht gefunden werden", - "invalid_interface": "Ung\u00fcltige Netzwerkschnittstelle" + "discovery_error": "Motion-Gateway konnte nicht gefunden werden" }, "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { - "api_key": "API-Schl\u00fcssel", - "interface": "Die zu verwendende Netzwerkschnittstelle" + "api_key": "API-Schl\u00fcssel" }, - "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" + "description": "Ein 16-Zeichen-API-Schl\u00fcssel wird ben\u00f6tigt, siehe https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key" }, "select": { "data": { @@ -28,11 +25,9 @@ }, "user": { "data": { - "api_key": "API-Schl\u00fcssel", "host": "IP-Adresse" }, - "description": "Stelle eine Verbindung zu deinem Motion Gateway her. Wenn die IP-Adresse leer bleibt, wird die automatische Erkennung verwendet", - "title": "Jalousien" + "description": "Stelle eine Verbindung zu deinem Motion Gateway her. Wenn die IP-Adresse leer bleibt, wird die automatische Erkennung verwendet" } } }, @@ -41,9 +36,7 @@ "init": { "data": { "wait_for_push": "Warten auf Multicast-Push bei Aktualisierung" - }, - "description": "Optionale Einstellungen angeben", - "title": "Motion Jalousien" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/el.json b/homeassistant/components/motion_blinds/translations/el.json index 2914382e820..b9d67703c57 100644 --- a/homeassistant/components/motion_blinds/translations/el.json +++ b/homeassistant/components/motion_blinds/translations/el.json @@ -6,18 +6,15 @@ "connection_error": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "error": { - "discovery_error": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 \u03ba\u03af\u03bd\u03b7\u03c3\u03b7\u03c2", - "invalid_interface": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5" + "discovery_error": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 \u03ba\u03af\u03bd\u03b7\u03c3\u03b7\u03c2" }, "flow_title": "Motion Blinds", "step": { "connect": { "data": { - "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", - "interface": "\u0397 \u03b4\u03b9\u03b1\u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af" + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" }, - "description": "\u0398\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API 16 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd, \u03b4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u03b3\u03b9\u03b1 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2.", - "title": "Motion Blinds" + "description": "\u0398\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API 16 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd, \u03b4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u03b3\u03b9\u03b1 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2." }, "select": { "data": { @@ -28,11 +25,9 @@ }, "user": { "data": { - "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" }, - "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf Motion Gateway, \u03b5\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7", - "title": "Motion Blinds" + "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf Motion Gateway, \u03b5\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7" } } }, @@ -41,9 +36,7 @@ "init": { "data": { "wait_for_push": "\u0391\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae \u03b3\u03b9\u03b1 multicast push \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7" - }, - "description": "\u039a\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ce\u03bd \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd", - "title": "Motion Blinds" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/en.json b/homeassistant/components/motion_blinds/translations/en.json index 7a6d6763fdc..33784e1d386 100644 --- a/homeassistant/components/motion_blinds/translations/en.json +++ b/homeassistant/components/motion_blinds/translations/en.json @@ -6,18 +6,15 @@ "connection_error": "Failed to connect" }, "error": { - "discovery_error": "Failed to discover a Motion Gateway", - "invalid_interface": "Invalid network interface" + "discovery_error": "Failed to discover a Motion Gateway" }, "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { - "api_key": "API Key", - "interface": "The network interface to use" + "api_key": "API Key" }, - "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions", - "title": "Motion Blinds" + "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions" }, "select": { "data": { @@ -28,11 +25,9 @@ }, "user": { "data": { - "api_key": "API Key", "host": "IP Address" }, - "description": "Connect to your Motion Gateway, if the IP address is not set, auto-discovery is used", - "title": "Motion Blinds" + "description": "Connect to your Motion Gateway, if the IP address is not set, auto-discovery is used" } } }, @@ -41,9 +36,7 @@ "init": { "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/es.json b/homeassistant/components/motion_blinds/translations/es.json index 0c364f394ef..1a4312d4fe0 100644 --- a/homeassistant/components/motion_blinds/translations/es.json +++ b/homeassistant/components/motion_blinds/translations/es.json @@ -6,18 +6,15 @@ "connection_error": "No se pudo conectar" }, "error": { - "discovery_error": "No se pudo descubrir un detector de movimiento", - "invalid_interface": "Interfaz de red inv\u00e1lida" + "discovery_error": "No se pudo descubrir un detector de movimiento" }, "flow_title": "Motion Blinds", "step": { "connect": { "data": { - "api_key": "Clave API", - "interface": "La interfaz de la red a usar" + "api_key": "Clave API" }, - "description": "Necesitar\u00e1 la clave de API de 16 caracteres, consulte https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key para obtener instrucciones", - "title": "Estores motorizados" + "description": "Necesitar\u00e1 la clave de API de 16 caracteres, consulte https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key para obtener instrucciones" }, "select": { "data": { @@ -28,11 +25,9 @@ }, "user": { "data": { - "api_key": "Clave API", "host": "Direcci\u00f3n IP" }, - "description": "Con\u00e9ctate a tu Motion Gateway, si la direcci\u00f3n IP no est\u00e1 establecida, se utilitzar\u00e1 la detecci\u00f3n autom\u00e1tica", - "title": "Motion Blinds" + "description": "Con\u00e9ctate a tu Motion Gateway, si la direcci\u00f3n IP no est\u00e1 establecida, se utilitzar\u00e1 la detecci\u00f3n autom\u00e1tica" } } }, @@ -41,9 +36,7 @@ "init": { "data": { "wait_for_push": "Espere a que se realice la actualizaci\u00f3n de multidifusi\u00f3n" - }, - "description": "Especifica los ajustes opcionales", - "title": "Motion Blinds" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/et.json b/homeassistant/components/motion_blinds/translations/et.json index 715208cd9f4..eb1fecf1118 100644 --- a/homeassistant/components/motion_blinds/translations/et.json +++ b/homeassistant/components/motion_blinds/translations/et.json @@ -6,18 +6,15 @@ "connection_error": "\u00dchendamine nurjus" }, "error": { - "discovery_error": "Motion Gateway avastamine nurjus", - "invalid_interface": "Sobimatu v\u00f5rguliides" + "discovery_error": "Motion Gateway avastamine nurjus" }, "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { - "api_key": "API v\u00f5ti", - "interface": "Kasutatav v\u00f5rguliides" + "api_key": "API v\u00f5ti" }, - "description": "On vaja 16-kohalist API-v\u00f5tit, juhiste saamiseks vaata https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", - "title": "" + "description": "On vaja 16-kohalist API-v\u00f5tit, juhiste saamiseks vaata https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key" }, "select": { "data": { @@ -28,11 +25,9 @@ }, "user": { "data": { - "api_key": "API v\u00f5ti", "host": "IP-aadress" }, - "description": "\u00dchenda oma Motion Gatewayga. Kui IP-aadress on m\u00e4\u00e4ramata kasutatakse automaatset avastamist", - "title": "" + "description": "\u00dchenda oma Motion Gatewayga. Kui IP-aadress on m\u00e4\u00e4ramata kasutatakse automaatset avastamist" } } }, @@ -41,9 +36,7 @@ "init": { "data": { "wait_for_push": "Oota multicast'i t\u00f5ukev\u00e4rskendust" - }, - "description": "Valikuliste s\u00e4tete m\u00e4\u00e4ramine", - "title": "Motion Blinds" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/fr.json b/homeassistant/components/motion_blinds/translations/fr.json index 17df2654026..c22ac830ebe 100644 --- a/homeassistant/components/motion_blinds/translations/fr.json +++ b/homeassistant/components/motion_blinds/translations/fr.json @@ -6,18 +6,15 @@ "connection_error": "\u00c9chec de connexion" }, "error": { - "discovery_error": "Impossible de d\u00e9couvrir une Motion Gateway", - "invalid_interface": "Interface r\u00e9seau non valide" + "discovery_error": "Impossible de d\u00e9couvrir une Motion Gateway" }, "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { - "api_key": "Cl\u00e9 d'API", - "interface": "Interface r\u00e9seau \u00e0 utiliser" + "api_key": "Cl\u00e9 d'API" }, - "description": "Vous aurez besoin de la cl\u00e9 API de 16 caract\u00e8res, voir https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key pour les instructions", - "title": "Stores de mouvement" + "description": "Vous aurez besoin de la cl\u00e9 API de 16 caract\u00e8res, voir https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key pour les instructions" }, "select": { "data": { @@ -28,11 +25,9 @@ }, "user": { "data": { - "api_key": "Cl\u00e9 d'API", "host": "Adresse IP" }, - "description": "Connectez-vous \u00e0 votre Motion Gateway, si l'adresse IP n'est pas d\u00e9finie, la d\u00e9tection automatique est utilis\u00e9e", - "title": "Stores de mouvement" + "description": "Connectez-vous \u00e0 votre Motion Gateway, si l'adresse IP n'est pas d\u00e9finie, la d\u00e9tection automatique est utilis\u00e9e" } } }, @@ -41,9 +36,7 @@ "init": { "data": { "wait_for_push": "Attendre la mise \u00e0 jour de la diffusion group\u00e9e" - }, - "description": "Sp\u00e9cifiez les param\u00e8tres optionnels", - "title": "Store motoris\u00e9" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/he.json b/homeassistant/components/motion_blinds/translations/he.json index ce6f3d2918e..3cf199985e5 100644 --- a/homeassistant/components/motion_blinds/translations/he.json +++ b/homeassistant/components/motion_blinds/translations/he.json @@ -18,7 +18,6 @@ }, "user": { "data": { - "api_key": "\u05de\u05e4\u05ea\u05d7 API", "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP" } } diff --git a/homeassistant/components/motion_blinds/translations/hu.json b/homeassistant/components/motion_blinds/translations/hu.json index 64334c54a28..8ab58169d05 100644 --- a/homeassistant/components/motion_blinds/translations/hu.json +++ b/homeassistant/components/motion_blinds/translations/hu.json @@ -6,18 +6,15 @@ "connection_error": "Sikertelen csatlakoz\u00e1s" }, "error": { - "discovery_error": "Nem siker\u00fclt felfedezni a Motion Gateway-t", - "invalid_interface": "\u00c9rv\u00e9nytelen h\u00e1l\u00f3zati interf\u00e9sz" + "discovery_error": "Nem siker\u00fclt felfedezni a Motion Gateway-t" }, "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { - "api_key": "API kulcs", - "interface": "A haszn\u00e1lni k\u00edv\u00e1nt h\u00e1l\u00f3zati interf\u00e9sz" + "api_key": "API kulcs" }, - "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": "Red\u0151ny/rol\u00f3" + "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" }, "select": { "data": { @@ -28,11 +25,9 @@ }, "user": { "data": { - "api_key": "API kulcs", "host": "IP c\u00edm" }, - "description": "Csatlakozzon a Motion Gateway-hez, ha az IP-c\u00edm nincs be\u00e1ll\u00edtva, akkor az automatikus felder\u00edt\u00e9st haszn\u00e1lja", - "title": "Red\u0151ny/rol\u00f3" + "description": "Csatlakozzon a Motion Gateway-hez, ha az IP-c\u00edm nincs be\u00e1ll\u00edtva, akkor az automatikus felder\u00edt\u00e9st haszn\u00e1lja" } } }, @@ -41,9 +36,7 @@ "init": { "data": { "wait_for_push": "Multicast adatokra v\u00e1rakoz\u00e1s friss\u00edt\u00e9skor" - }, - "description": "Opcion\u00e1lis be\u00e1ll\u00edt\u00e1sok megad\u00e1sa", - "title": "Red\u0151ny/rol\u00f3" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/id.json b/homeassistant/components/motion_blinds/translations/id.json index 42b9b8d127d..5892916aa0d 100644 --- a/homeassistant/components/motion_blinds/translations/id.json +++ b/homeassistant/components/motion_blinds/translations/id.json @@ -6,18 +6,15 @@ "connection_error": "Gagal terhubung" }, "error": { - "discovery_error": "Gagal menemukan Motion Gateway", - "invalid_interface": "Antarmuka jaringan tidak valid" + "discovery_error": "Gagal menemukan Motion Gateway" }, "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { - "api_key": "Kunci API", - "interface": "Antarmuka jaringan yang akan digunakan" + "api_key": "Kunci API" }, - "description": "Anda akan memerlukan Kunci API 16 karakter, baca petunjuknya di https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", - "title": "Motion Blinds" + "description": "Anda akan memerlukan Kunci API 16 karakter, baca petunjuknya di https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key" }, "select": { "data": { @@ -28,11 +25,9 @@ }, "user": { "data": { - "api_key": "Kunci API", "host": "Alamat IP" }, - "description": "Hubungkan ke Motion Gateway Anda, jika alamat IP tidak disetel, penemuan otomatis akan digunakan", - "title": "Motion Blinds" + "description": "Hubungkan ke Motion Gateway Anda, jika alamat IP tidak disetel, penemuan otomatis akan digunakan" } } }, @@ -41,9 +36,7 @@ "init": { "data": { "wait_for_push": "Tunggu push multicast pada pembaruan" - }, - "description": "Tentukan pengaturan opsional", - "title": "Motion Blinds" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/it.json b/homeassistant/components/motion_blinds/translations/it.json index ddb29c2c042..35168c391b7 100644 --- a/homeassistant/components/motion_blinds/translations/it.json +++ b/homeassistant/components/motion_blinds/translations/it.json @@ -6,18 +6,15 @@ "connection_error": "Impossibile connettersi" }, "error": { - "discovery_error": "Impossibile rilevare un Motion Gateway", - "invalid_interface": "Interfaccia di rete non valida" + "discovery_error": "Impossibile rilevare un Motion Gateway" }, "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { - "api_key": "Chiave API", - "interface": "L'interfaccia di rete da utilizzare" + "api_key": "Chiave API" }, - "description": "Avrai bisogno della chiave API di 16 caratteri, consulta https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key per le istruzioni", - "title": "Motion Blinds" + "description": "Avrai bisogno della chiave API di 16 caratteri, consulta https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key per le istruzioni" }, "select": { "data": { @@ -28,11 +25,9 @@ }, "user": { "data": { - "api_key": "Chiave API", "host": "Indirizzo IP" }, - "description": "Connetti il tuo Motion Gateway, se l'indirizzo IP non \u00e8 impostato, sar\u00e0 utilizzato il rilevamento automatico", - "title": "Tende Motion" + "description": "Connetti il tuo Motion Gateway, se l'indirizzo IP non \u00e8 impostato, sar\u00e0 utilizzato il rilevamento automatico" } } }, @@ -41,9 +36,7 @@ "init": { "data": { "wait_for_push": "Attendi il push multicast all'aggiornamento" - }, - "description": "Specifica le impostazioni opzionali", - "title": "Motion Blinds" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/ja.json b/homeassistant/components/motion_blinds/translations/ja.json index b81cfa365c3..df0ba1f8e47 100644 --- a/homeassistant/components/motion_blinds/translations/ja.json +++ b/homeassistant/components/motion_blinds/translations/ja.json @@ -6,18 +6,15 @@ "connection_error": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { - "discovery_error": "Motion Gateway\u306e\u691c\u51fa\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "invalid_interface": "\u7121\u52b9\u306a\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9" + "discovery_error": "Motion Gateway\u306e\u691c\u51fa\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "\u30e2\u30fc\u30b7\u30e7\u30f3\u30d6\u30e9\u30a4\u30f3\u30c9", "step": { "connect": { "data": { - "api_key": "API\u30ad\u30fc", - "interface": "\u4f7f\u7528\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9" + "api_key": "API\u30ad\u30fc" }, - "description": "16\u6587\u5b57\u306eAPI\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002\u624b\u9806\u306f\u3001https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u30e2\u30fc\u30b7\u30e7\u30f3\u30d6\u30e9\u30a4\u30f3\u30c9" + "description": "16\u6587\u5b57\u306eAPI\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002\u624b\u9806\u306f\u3001https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "select": { "data": { @@ -28,11 +25,9 @@ }, "user": { "data": { - "api_key": "API\u30ad\u30fc", "host": "IP\u30a2\u30c9\u30ec\u30b9" }, - "description": "Motion Gateway\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002IP\u30a2\u30c9\u30ec\u30b9\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u81ea\u52d5\u691c\u51fa\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059", - "title": "\u30e2\u30fc\u30b7\u30e7\u30f3\u30d6\u30e9\u30a4\u30f3\u30c9" + "description": "Motion Gateway\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002IP\u30a2\u30c9\u30ec\u30b9\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u81ea\u52d5\u691c\u51fa\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059" } } }, @@ -41,9 +36,7 @@ "init": { "data": { "wait_for_push": "\u66f4\u65b0\u6642\u306b\u30de\u30eb\u30c1\u30ad\u30e3\u30b9\u30c8 \u30d7\u30c3\u30b7\u30e5\u3092\u5f85\u6a5f\u3059\u308b" - }, - "description": "\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a\u306e\u6307\u5b9a", - "title": "\u30e2\u30fc\u30b7\u30e7\u30f3\u30d6\u30e9\u30a4\u30f3\u30c9" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/ka.json b/homeassistant/components/motion_blinds/translations/ka.json index e8ea5e0deba..71223e44db4 100644 --- a/homeassistant/components/motion_blinds/translations/ka.json +++ b/homeassistant/components/motion_blinds/translations/ka.json @@ -9,11 +9,9 @@ "step": { "user": { "data": { - "api_key": "API Key", "host": "IP \u10db\u10d8\u10e1\u10d0\u10db\u10d0\u10e0\u10d7\u10d8" }, - "description": "\u10d7\u10e5\u10d5\u10d4\u10dc \u10d3\u10d0\u10d2\u10ed\u10d8\u10e0\u10d3\u10d4\u10d1\u10d0\u10d7 16 \u10d0\u10e1\u10dd\u10d8\u10d0\u10dc\u10d8 API key, \u10d8\u10dc\u10e1\u10e2\u10e0\u10e3\u10e5\u10ea\u10d8\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1 \u10d8\u10ee\u10d8\u10da\u10d4\u10d7 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", - "title": "\u10db\u10dd\u10eb\u10e0\u10d0\u10d5\u10d8 \u10df\u10d0\u10da\u10e3\u10d6\u10d4\u10d1\u10d8" + "description": "\u10d7\u10e5\u10d5\u10d4\u10dc \u10d3\u10d0\u10d2\u10ed\u10d8\u10e0\u10d3\u10d4\u10d1\u10d0\u10d7 16 \u10d0\u10e1\u10dd\u10d8\u10d0\u10dc\u10d8 API key, \u10d8\u10dc\u10e1\u10e2\u10e0\u10e3\u10e5\u10ea\u10d8\u10d8\u10e1\u10d7\u10d5\u10d8\u10e1 \u10d8\u10ee\u10d8\u10da\u10d4\u10d7 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key" } } } diff --git a/homeassistant/components/motion_blinds/translations/ko.json b/homeassistant/components/motion_blinds/translations/ko.json index 69ed2cd7b35..19ad1c03da8 100644 --- a/homeassistant/components/motion_blinds/translations/ko.json +++ b/homeassistant/components/motion_blinds/translations/ko.json @@ -14,8 +14,7 @@ "data": { "api_key": "API \ud0a4" }, - "description": "16\uac1c\uc758 \ubb38\uc790\uc5f4\ub85c \uad6c\uc131\ub41c API Key\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", - "title": "Motion Blinds" + "description": "16\uac1c\uc758 \ubb38\uc790\uc5f4\ub85c \uad6c\uc131\ub41c API Key\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "select": { "data": { @@ -26,11 +25,9 @@ }, "user": { "data": { - "api_key": "API \ud0a4", "host": "IP \uc8fc\uc18c" }, - "description": "Motion \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud569\ub2c8\ub2e4. IP \uc8fc\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \uc790\ub3d9 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4", - "title": "Motion Blinds" + "description": "Motion \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud569\ub2c8\ub2e4. IP \uc8fc\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \uc790\ub3d9 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4" } } } diff --git a/homeassistant/components/motion_blinds/translations/lb.json b/homeassistant/components/motion_blinds/translations/lb.json index 85caeea79e5..b8f301535bf 100644 --- a/homeassistant/components/motion_blinds/translations/lb.json +++ b/homeassistant/components/motion_blinds/translations/lb.json @@ -22,10 +22,8 @@ }, "user": { "data": { - "api_key": "API Schl\u00ebssel", "host": "IP Adresse" - }, - "title": "Steierbar Jalousien" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/nl.json b/homeassistant/components/motion_blinds/translations/nl.json index c837e656c56..73522480df5 100644 --- a/homeassistant/components/motion_blinds/translations/nl.json +++ b/homeassistant/components/motion_blinds/translations/nl.json @@ -6,18 +6,15 @@ "connection_error": "Kan geen verbinding maken" }, "error": { - "discovery_error": "Kan geen Motion Gateway vinden", - "invalid_interface": "Ongeldige netwerkinterface" + "discovery_error": "Kan geen Motion Gateway vinden" }, "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { - "api_key": "API-sleutel", - "interface": "De te gebruiken netwerkinterface" + "api_key": "API-sleutel" }, - "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" + "description": "U hebt de API-sleutel van 16 tekens nodig, zie https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key voor instructies" }, "select": { "data": { @@ -28,11 +25,9 @@ }, "user": { "data": { - "api_key": "API-sleutel", "host": "IP-adres" }, - "description": "Maak verbinding met uw Motion Gateway, als het IP-adres niet is ingesteld, wordt auto-discovery gebruikt", - "title": "Motion Blinds" + "description": "Maak verbinding met uw Motion Gateway, als het IP-adres niet is ingesteld, wordt auto-discovery gebruikt" } } }, @@ -41,9 +36,7 @@ "init": { "data": { "wait_for_push": "Wacht op multicast push bij update" - }, - "description": "Optionele instellingen opgeven", - "title": "Motion Blinds" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/no.json b/homeassistant/components/motion_blinds/translations/no.json index 655e574cab9..3545a03a543 100644 --- a/homeassistant/components/motion_blinds/translations/no.json +++ b/homeassistant/components/motion_blinds/translations/no.json @@ -6,18 +6,15 @@ "connection_error": "Tilkobling mislyktes" }, "error": { - "discovery_error": "Kunne ikke oppdage en Motion Gateway", - "invalid_interface": "Ugyldig nettverksgrensesnitt" + "discovery_error": "Kunne ikke oppdage en Motion Gateway" }, "flow_title": "{short_mac} ( {ip_address} )", "step": { "connect": { "data": { - "api_key": "API-n\u00f8kkel", - "interface": "Nettverksgrensesnittet som skal brukes" + "api_key": "API-n\u00f8kkel" }, - "description": "Du trenger API-n\u00f8kkelen med 16 tegn, se https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instruksjoner", - "title": "" + "description": "Du trenger API-n\u00f8kkelen med 16 tegn, se https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instruksjoner" }, "select": { "data": { @@ -28,11 +25,9 @@ }, "user": { "data": { - "api_key": "API-n\u00f8kkel", "host": "IP adresse" }, - "description": "Koble til Motion Gateway. Hvis IP-adressen ikke er angitt, brukes automatisk oppdagelse", - "title": "Motion Blinds" + "description": "Koble til Motion Gateway. Hvis IP-adressen ikke er angitt, brukes automatisk oppdagelse" } } }, @@ -41,9 +36,7 @@ "init": { "data": { "wait_for_push": "Vent p\u00e5 multicast push p\u00e5 oppdateringen" - }, - "description": "Spesifiser valgfrie innstillinger", - "title": "Bevegelse Persienner" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/pl.json b/homeassistant/components/motion_blinds/translations/pl.json index 2a042859b88..60277ba9360 100644 --- a/homeassistant/components/motion_blinds/translations/pl.json +++ b/homeassistant/components/motion_blinds/translations/pl.json @@ -6,18 +6,15 @@ "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "error": { - "discovery_error": "Nie uda\u0142o si\u0119 wykry\u0107 bramki ruchu", - "invalid_interface": "Nieprawid\u0142owy interfejs sieciowy" + "discovery_error": "Nie uda\u0142o si\u0119 wykry\u0107 bramki ruchu" }, "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { - "api_key": "Klucz API", - "interface": "Interfejs sieciowy" + "api_key": "Klucz API" }, - "description": "B\u0119dziesz potrzebowa\u0142 16-znakowego klucza API, instrukcje znajdziesz na https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", - "title": "Motion Blinds" + "description": "B\u0119dziesz potrzebowa\u0142 16-znakowego klucza API, instrukcje znajdziesz na https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key" }, "select": { "data": { @@ -28,11 +25,9 @@ }, "user": { "data": { - "api_key": "Klucz API", "host": "Adres IP" }, - "description": "Po\u0142\u0105cz si\u0119 z bram\u0105 ruchu. Je\u015bli adres IP nie jest ustawiony, u\u017cywane jest automatyczne wykrywanie", - "title": "Rolety Motion" + "description": "Po\u0142\u0105cz si\u0119 z bram\u0105 ruchu. Je\u015bli adres IP nie jest ustawiony, u\u017cywane jest automatyczne wykrywanie" } } }, @@ -41,9 +36,7 @@ "init": { "data": { "wait_for_push": "Poczekaj na aktualizacj\u0119 multicast push" - }, - "description": "Okre\u015bl opcjonalne ustawienia", - "title": "Rolety Motion" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/pt-BR.json b/homeassistant/components/motion_blinds/translations/pt-BR.json index afbd117d260..0a3b68357ee 100644 --- a/homeassistant/components/motion_blinds/translations/pt-BR.json +++ b/homeassistant/components/motion_blinds/translations/pt-BR.json @@ -6,18 +6,15 @@ "connection_error": "Falha ao conectar" }, "error": { - "discovery_error": "Falha ao descobrir um Motion Gateway", - "invalid_interface": "Interface de rede inv\u00e1lida" + "discovery_error": "Falha ao descobrir um Motion Gateway" }, "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { - "api_key": "Chave da API", - "interface": "A interface de rede a ser utilizada" + "api_key": "Chave da API" }, - "description": "Voc\u00ea precisar\u00e1 da chave de API de 16 caracteres, consulte https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key para obter instru\u00e7\u00f5es", - "title": "Cortinas de movimento" + "description": "Voc\u00ea precisar\u00e1 da chave de API de 16 caracteres, consulte https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key para obter instru\u00e7\u00f5es" }, "select": { "data": { @@ -28,11 +25,9 @@ }, "user": { "data": { - "api_key": "Chave da API", "host": "Endere\u00e7o IP" }, - "description": "Conecte-se ao seu Motion Gateway, se o endere\u00e7o IP n\u00e3o estiver definido, a descoberta autom\u00e1tica ser\u00e1 usada", - "title": "Cortinas de movimento" + "description": "Conecte-se ao seu Motion Gateway, se o endere\u00e7o IP n\u00e3o estiver definido, a descoberta autom\u00e1tica ser\u00e1 usada" } } }, @@ -41,9 +36,7 @@ "init": { "data": { "wait_for_push": "Aguarde o push multicast na atualiza\u00e7\u00e3o" - }, - "description": "Especifique as configura\u00e7\u00f5es opcionais", - "title": "Cortinas de movimento" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/pt.json b/homeassistant/components/motion_blinds/translations/pt.json index e43b71438db..7538043f1ce 100644 --- a/homeassistant/components/motion_blinds/translations/pt.json +++ b/homeassistant/components/motion_blinds/translations/pt.json @@ -10,8 +10,7 @@ "connect": { "data": { "api_key": "Chave da API" - }, - "title": "Cortinas Motion" + } }, "select": { "data": { @@ -20,10 +19,8 @@ }, "user": { "data": { - "api_key": "Chave da API", "host": "Endere\u00e7o IP" - }, - "title": "Cortinas Motion" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/ru.json b/homeassistant/components/motion_blinds/translations/ru.json index 0ab743f8895..afcde99b123 100644 --- a/homeassistant/components/motion_blinds/translations/ru.json +++ b/homeassistant/components/motion_blinds/translations/ru.json @@ -6,18 +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.", - "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." + "discovery_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0448\u043b\u044e\u0437 Motion." }, "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { - "api_key": "\u041a\u043b\u044e\u0447 API", - "interface": "\u0421\u0435\u0442\u0435\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441" + "api_key": "\u041a\u043b\u044e\u0447 API" }, - "description": "\u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.", - "title": "Motion Blinds" + "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." }, "select": { "data": { @@ -28,11 +25,9 @@ }, "user": { "data": { - "api_key": "\u041a\u043b\u044e\u0447 API", "host": "IP-\u0430\u0434\u0440\u0435\u0441" }, - "description": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u043b\u044e\u0437\u0443 Motion. \u0414\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0448\u043b\u044e\u0437\u0430, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u043f\u0443\u0441\u0442\u044b\u043c.", - "title": "Motion Blinds" + "description": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u043b\u044e\u0437\u0443 Motion. \u0414\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0448\u043b\u044e\u0437\u0430, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u043f\u0443\u0441\u0442\u044b\u043c." } } }, @@ -41,9 +36,7 @@ "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" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/sk.json b/homeassistant/components/motion_blinds/translations/sk.json index e58538162d7..6ea9c064f16 100644 --- a/homeassistant/components/motion_blinds/translations/sk.json +++ b/homeassistant/components/motion_blinds/translations/sk.json @@ -8,11 +8,6 @@ "data": { "api_key": "API k\u013e\u00fa\u010d" } - }, - "user": { - "data": { - "api_key": "API k\u013e\u00fa\u010d" - } } } } diff --git a/homeassistant/components/motion_blinds/translations/sl.json b/homeassistant/components/motion_blinds/translations/sl.json deleted file mode 100644 index 80ea8b24fbb..00000000000 --- a/homeassistant/components/motion_blinds/translations/sl.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "error": { - "invalid_interface": "Neveljaven omre\u017eni vmesnik" - }, - "step": { - "connect": { - "data": { - "interface": "Omre\u017eni vmesnik za uporabo" - } - }, - "user": { - "title": "Motion Blinds" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/tr.json b/homeassistant/components/motion_blinds/translations/tr.json index f53b251a8ec..5af004ae0cc 100644 --- a/homeassistant/components/motion_blinds/translations/tr.json +++ b/homeassistant/components/motion_blinds/translations/tr.json @@ -6,18 +6,15 @@ "connection_error": "Ba\u011flanma hatas\u0131" }, "error": { - "discovery_error": "Motion Gateway bulunamad\u0131", - "invalid_interface": "Ge\u00e7ersiz a\u011f aray\u00fcz\u00fc" + "discovery_error": "Motion Gateway bulunamad\u0131" }, "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { - "api_key": "API Anahtar\u0131", - "interface": "Kullan\u0131lacak a\u011f aray\u00fcz\u00fc" + "api_key": "API Anahtar\u0131" }, - "description": "16 karakterlik API Anahtar\u0131na ihtiyac\u0131n\u0131z olacak, talimatlar i\u00e7in https://www.home-assistant.io/integrations/motion_blinds/#retriving-the-key adresine bak\u0131n.", - "title": "Hareketli Perdeler" + "description": "16 karakterlik API Anahtar\u0131na ihtiyac\u0131n\u0131z olacak, talimatlar i\u00e7in https://www.home-assistant.io/integrations/motion_blinds/#retriving-the-key adresine bak\u0131n." }, "select": { "data": { @@ -28,11 +25,9 @@ }, "user": { "data": { - "api_key": "API Anahtar\u0131", "host": "IP Adresi" }, - "description": "Motion Gateway'inize ba\u011flan\u0131n, IP adresi ayarlanmad\u0131ysa, otomatik ke\u015fif kullan\u0131l\u0131r", - "title": "Hareketli Perdeler" + "description": "Motion Gateway'inize ba\u011flan\u0131n, IP adresi ayarlanmad\u0131ysa, otomatik ke\u015fif kullan\u0131l\u0131r" } } }, @@ -41,9 +36,7 @@ "init": { "data": { "wait_for_push": "G\u00fcncellemede \u00e7ok noktaya yay\u0131n i\u00e7in bekleyin" - }, - "description": "\u0130ste\u011fe ba\u011fl\u0131 ayarlar\u0131 belirtin", - "title": "Hareketli Panjurlar" + } } } } diff --git a/homeassistant/components/motion_blinds/translations/uk.json b/homeassistant/components/motion_blinds/translations/uk.json index 99ccb60dc6c..a3d8ee57c1f 100644 --- a/homeassistant/components/motion_blinds/translations/uk.json +++ b/homeassistant/components/motion_blinds/translations/uk.json @@ -14,8 +14,7 @@ "data": { "api_key": "\u041a\u043b\u044e\u0447 API" }, - "description": "\u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u0435\u043d 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API, \u0434\u0438\u0432. https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0439", - "title": "Motion Blinds" + "description": "\u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u0435\u043d 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API, \u0434\u0438\u0432. https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0439" }, "select": { "data": { @@ -26,11 +25,9 @@ }, "user": { "data": { - "api_key": "\u041a\u043b\u044e\u0447 API", "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" }, - "description": "\u041f\u0440\u043e \u0442\u0435, \u044f\u043a \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API, \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0456\u0437\u043d\u0430\u0442\u0438\u0441\u044f \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0457 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.", - "title": "Motion Blinds" + "description": "\u041f\u0440\u043e \u0442\u0435, \u044f\u043a \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API, \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0456\u0437\u043d\u0430\u0442\u0438\u0441\u044f \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0457 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key." } } } diff --git a/homeassistant/components/motion_blinds/translations/zh-Hans.json b/homeassistant/components/motion_blinds/translations/zh-Hans.json index f8dac159488..d8f87406260 100644 --- a/homeassistant/components/motion_blinds/translations/zh-Hans.json +++ b/homeassistant/components/motion_blinds/translations/zh-Hans.json @@ -6,7 +6,6 @@ "step": { "user": { "data": { - "api_key": "API\u5bc6\u7801", "host": "IP\u5730\u5740" } } diff --git a/homeassistant/components/motion_blinds/translations/zh-Hant.json b/homeassistant/components/motion_blinds/translations/zh-Hant.json index ccfe6e782ed..189f1474b22 100644 --- a/homeassistant/components/motion_blinds/translations/zh-Hant.json +++ b/homeassistant/components/motion_blinds/translations/zh-Hant.json @@ -6,18 +6,15 @@ "connection_error": "\u9023\u7dda\u5931\u6557" }, "error": { - "discovery_error": "\u641c\u7d22 Motion \u9598\u9053\u5668\u5931\u6557", - "invalid_interface": "\u7db2\u8def\u4ecb\u9762\u7121\u6548" + "discovery_error": "\u641c\u7d22 Motion \u9598\u9053\u5668\u5931\u6557" }, "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { - "api_key": "API \u91d1\u9470", - "interface": "\u4f7f\u7528\u7684\u7db2\u8def\u4ecb\u9762" + "api_key": "API \u91d1\u9470" }, - "description": "\u5c07\u9700\u8981\u8f38\u5165 16 \u4f4d\u5b57\u5143 API \u91d1\u9470\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u4ee5\u7372\u5f97\u7372\u53d6\u91d1\u9470\u7684\u6559\u5b78\u3002", - "title": "Motion Blinds" + "description": "\u5c07\u9700\u8981\u8f38\u5165 16 \u4f4d\u5b57\u5143 API \u91d1\u9470\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u4ee5\u7372\u5f97\u7372\u53d6\u91d1\u9470\u7684\u6559\u5b78\u3002" }, "select": { "data": { @@ -28,11 +25,9 @@ }, "user": { "data": { - "api_key": "API \u91d1\u9470", "host": "IP \u4f4d\u5740" }, - "description": "\u9023\u7dda\u81f3 Motion \u9598\u9053\u5668\uff0c\u5047\u5982\u672a\u63d0\u4f9b IP \u4f4d\u5740\uff0c\u5c07\u4f7f\u7528\u81ea\u52d5\u641c\u7d22", - "title": "Motion Blinds" + "description": "\u9023\u7dda\u81f3 Motion \u9598\u9053\u5668\uff0c\u5047\u5982\u672a\u63d0\u4f9b IP \u4f4d\u5740\uff0c\u5c07\u4f7f\u7528\u81ea\u52d5\u641c\u7d22" } } }, @@ -41,9 +36,7 @@ "init": { "data": { "wait_for_push": "\u7b49\u5019 Multicast \u63a8\u9001\u901a\u77e5" - }, - "description": "\u6307\u5b9a\u9078\u9805\u8a2d\u5b9a", - "title": "Motion Blinds" + } } } } diff --git a/homeassistant/components/mysensors/translations/ca.json b/homeassistant/components/mysensors/translations/ca.json index 6e20f0bcbee..99cf16939ad 100644 --- a/homeassistant/components/mysensors/translations/ca.json +++ b/homeassistant/components/mysensors/translations/ca.json @@ -75,6 +75,5 @@ "description": "Tria el m\u00e8tode de connexi\u00f3 a la passarel\u00b7la" } } - }, - "title": "MySensors" + } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/cs.json b/homeassistant/components/mysensors/translations/cs.json index abe47f046ff..3434cfe82ec 100644 --- a/homeassistant/components/mysensors/translations/cs.json +++ b/homeassistant/components/mysensors/translations/cs.json @@ -12,6 +12,5 @@ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" } - }, - "title": "MySensors" + } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/de.json b/homeassistant/components/mysensors/translations/de.json index cfef6f7d363..0b90165fa5a 100644 --- a/homeassistant/components/mysensors/translations/de.json +++ b/homeassistant/components/mysensors/translations/de.json @@ -75,6 +75,5 @@ "description": "Verbindungsmethode zum Gateway w\u00e4hlen" } } - }, - "title": "MySensors" + } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/el.json b/homeassistant/components/mysensors/translations/el.json index 17bf83158b7..81ba72eea7e 100644 --- a/homeassistant/components/mysensors/translations/el.json +++ b/homeassistant/components/mysensors/translations/el.json @@ -75,6 +75,5 @@ "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03bc\u03b5 \u03c4\u03b7\u03bd \u03c0\u03cd\u03bb\u03b7" } } - }, - "title": "MySensors" + } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/en.json b/homeassistant/components/mysensors/translations/en.json index 7ca3516e50d..5ec81c22186 100644 --- a/homeassistant/components/mysensors/translations/en.json +++ b/homeassistant/components/mysensors/translations/en.json @@ -75,6 +75,5 @@ "description": "Choose connection method to the gateway" } } - }, - "title": "MySensors" + } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/es.json b/homeassistant/components/mysensors/translations/es.json index 411501db49d..91f2b7f0d1e 100644 --- a/homeassistant/components/mysensors/translations/es.json +++ b/homeassistant/components/mysensors/translations/es.json @@ -75,6 +75,5 @@ "description": "Elija el m\u00e9todo de conexi\u00f3n con la pasarela" } } - }, - "title": "MySensors" + } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/et.json b/homeassistant/components/mysensors/translations/et.json index 7aff6b1c3da..00614e57252 100644 --- a/homeassistant/components/mysensors/translations/et.json +++ b/homeassistant/components/mysensors/translations/et.json @@ -75,6 +75,5 @@ "description": "Vali l\u00fc\u00fcsi \u00fchendusviis" } } - }, - "title": "MySensors" + } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/fr.json b/homeassistant/components/mysensors/translations/fr.json index c9d64ee73e6..722bf639111 100644 --- a/homeassistant/components/mysensors/translations/fr.json +++ b/homeassistant/components/mysensors/translations/fr.json @@ -75,6 +75,5 @@ "description": "Choisissez la m\u00e9thode de connexion \u00e0 la passerelle" } } - }, - "title": "MySensors" + } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/hu.json b/homeassistant/components/mysensors/translations/hu.json index a52ac3cd289..433714de477 100644 --- a/homeassistant/components/mysensors/translations/hu.json +++ b/homeassistant/components/mysensors/translations/hu.json @@ -75,6 +75,5 @@ "description": "V\u00e1lassza ki az \u00e1tj\u00e1r\u00f3hoz val\u00f3 csatlakoz\u00e1si m\u00f3dot" } } - }, - "title": "MySensors" + } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/id.json b/homeassistant/components/mysensors/translations/id.json index 93b10bb8c2c..e6256bd8757 100644 --- a/homeassistant/components/mysensors/translations/id.json +++ b/homeassistant/components/mysensors/translations/id.json @@ -75,6 +75,5 @@ "description": "Pilih metode koneksi ke gateway" } } - }, - "title": "MySensors" + } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/it.json b/homeassistant/components/mysensors/translations/it.json index 56ce395aa4c..0a16a4f045c 100644 --- a/homeassistant/components/mysensors/translations/it.json +++ b/homeassistant/components/mysensors/translations/it.json @@ -75,6 +75,5 @@ "description": "Scegli il metodo di connessione al gateway" } } - }, - "title": "MySensors" + } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/ja.json b/homeassistant/components/mysensors/translations/ja.json index b178d136009..da0354fe3c7 100644 --- a/homeassistant/components/mysensors/translations/ja.json +++ b/homeassistant/components/mysensors/translations/ja.json @@ -75,6 +75,5 @@ "description": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3078\u306e\u63a5\u7d9a\u65b9\u6cd5\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" } } - }, - "title": "MySensors" + } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/ko.json b/homeassistant/components/mysensors/translations/ko.json index e57f60aafbf..7a6f3e856a7 100644 --- a/homeassistant/components/mysensors/translations/ko.json +++ b/homeassistant/components/mysensors/translations/ko.json @@ -74,6 +74,5 @@ "description": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uc5f0\uacb0 \ubc29\ubc95\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694" } } - }, - "title": "MySensors" + } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/nl.json b/homeassistant/components/mysensors/translations/nl.json index 14055639f60..8293e815d8c 100644 --- a/homeassistant/components/mysensors/translations/nl.json +++ b/homeassistant/components/mysensors/translations/nl.json @@ -75,6 +75,5 @@ "description": "Kies de verbindingsmethode met de gateway" } } - }, - "title": "MySensors" + } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/no.json b/homeassistant/components/mysensors/translations/no.json index f0e307a1ab2..14fb39910fe 100644 --- a/homeassistant/components/mysensors/translations/no.json +++ b/homeassistant/components/mysensors/translations/no.json @@ -75,6 +75,5 @@ "description": "Velg tilkoblingsmetode til gatewayen" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/pl.json b/homeassistant/components/mysensors/translations/pl.json index f3233a01d50..3c4bed1ee86 100644 --- a/homeassistant/components/mysensors/translations/pl.json +++ b/homeassistant/components/mysensors/translations/pl.json @@ -75,6 +75,5 @@ "description": "Wybierz metod\u0119 po\u0142\u0105czenia z bramk\u0105" } } - }, - "title": "MySensors" + } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/pt-BR.json b/homeassistant/components/mysensors/translations/pt-BR.json index 468acf70588..2c0f312da72 100644 --- a/homeassistant/components/mysensors/translations/pt-BR.json +++ b/homeassistant/components/mysensors/translations/pt-BR.json @@ -75,6 +75,5 @@ "description": "Escolha o m\u00e9todo de conex\u00e3o com o gateway" } } - }, - "title": "MySensors" + } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/ru.json b/homeassistant/components/mysensors/translations/ru.json index 16f23f6efff..2801b8e8c00 100644 --- a/homeassistant/components/mysensors/translations/ru.json +++ b/homeassistant/components/mysensors/translations/ru.json @@ -75,6 +75,5 @@ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0448\u043b\u044e\u0437\u0443" } } - }, - "title": "MySensors" + } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/tr.json b/homeassistant/components/mysensors/translations/tr.json index 59f7a319e07..9f99525c7b2 100644 --- a/homeassistant/components/mysensors/translations/tr.json +++ b/homeassistant/components/mysensors/translations/tr.json @@ -75,6 +75,5 @@ "description": "A\u011f ge\u00e7idine ba\u011flant\u0131 y\u00f6ntemini se\u00e7in" } } - }, - "title": "MySensors" + } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/zh-Hant.json b/homeassistant/components/mysensors/translations/zh-Hant.json index 5774df64b78..a3ec7ba2dd7 100644 --- a/homeassistant/components/mysensors/translations/zh-Hant.json +++ b/homeassistant/components/mysensors/translations/zh-Hant.json @@ -75,6 +75,5 @@ "description": "\u9078\u64c7\u9598\u9053\u5668\u9023\u7dda\u65b9\u5f0f" } } - }, - "title": "MySensors" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/ca.json b/homeassistant/components/neato/translations/ca.json index ab8210afb2f..e45c8a28e5f 100644 --- a/homeassistant/components/neato/translations/ca.json +++ b/homeassistant/components/neato/translations/ca.json @@ -18,6 +18,5 @@ "title": "Vols comen\u00e7ar la configuraci\u00f3?" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/cs.json b/homeassistant/components/neato/translations/cs.json index 6cf7e314c4b..4618be9d933 100644 --- a/homeassistant/components/neato/translations/cs.json +++ b/homeassistant/components/neato/translations/cs.json @@ -18,6 +18,5 @@ "title": "Chcete za\u010d\u00edt nastavovat?" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/de.json b/homeassistant/components/neato/translations/de.json index c7fd239c585..03ad20e3caf 100644 --- a/homeassistant/components/neato/translations/de.json +++ b/homeassistant/components/neato/translations/de.json @@ -18,6 +18,5 @@ "title": "M\u00f6chtest Du mit der Einrichtung beginnen?" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/el.json b/homeassistant/components/neato/translations/el.json index 5e57ddd8b8b..a1a6232cfdb 100644 --- a/homeassistant/components/neato/translations/el.json +++ b/homeassistant/components/neato/translations/el.json @@ -18,6 +18,5 @@ "title": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7;" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/en.json b/homeassistant/components/neato/translations/en.json index 684d667678b..7c435c3749b 100644 --- a/homeassistant/components/neato/translations/en.json +++ b/homeassistant/components/neato/translations/en.json @@ -18,6 +18,5 @@ "title": "Do you want to start set up?" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/es.json b/homeassistant/components/neato/translations/es.json index bcb9dc3619e..7eeecb4dde0 100644 --- a/homeassistant/components/neato/translations/es.json +++ b/homeassistant/components/neato/translations/es.json @@ -18,6 +18,5 @@ "title": "\u00bfQuieres iniciar la configuraci\u00f3n?" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/et.json b/homeassistant/components/neato/translations/et.json index 40e601dfe9d..eff32d5b877 100644 --- a/homeassistant/components/neato/translations/et.json +++ b/homeassistant/components/neato/translations/et.json @@ -18,6 +18,5 @@ "title": "Kas soovid alustada seadistamist?" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/fr.json b/homeassistant/components/neato/translations/fr.json index df805121f6b..4334a2180bb 100644 --- a/homeassistant/components/neato/translations/fr.json +++ b/homeassistant/components/neato/translations/fr.json @@ -18,6 +18,5 @@ "title": "Voulez-vous commencer la configuration\u00a0?" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/hu.json b/homeassistant/components/neato/translations/hu.json index 255378eda35..3415d966717 100644 --- a/homeassistant/components/neato/translations/hu.json +++ b/homeassistant/components/neato/translations/hu.json @@ -18,6 +18,5 @@ "title": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/id.json b/homeassistant/components/neato/translations/id.json index b1811fe298e..e38096f47aa 100644 --- a/homeassistant/components/neato/translations/id.json +++ b/homeassistant/components/neato/translations/id.json @@ -18,6 +18,5 @@ "title": "Ingin memulai penyiapan?" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/it.json b/homeassistant/components/neato/translations/it.json index 000625a0294..cda062ceff0 100644 --- a/homeassistant/components/neato/translations/it.json +++ b/homeassistant/components/neato/translations/it.json @@ -18,6 +18,5 @@ "title": "Vuoi iniziare la configurazione?" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/ja.json b/homeassistant/components/neato/translations/ja.json index c5f4605159c..39eeedddc91 100644 --- a/homeassistant/components/neato/translations/ja.json +++ b/homeassistant/components/neato/translations/ja.json @@ -18,6 +18,5 @@ "title": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/ko.json b/homeassistant/components/neato/translations/ko.json index dc651939665..faaea344741 100644 --- a/homeassistant/components/neato/translations/ko.json +++ b/homeassistant/components/neato/translations/ko.json @@ -18,6 +18,5 @@ "title": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/lb.json b/homeassistant/components/neato/translations/lb.json index d54443e6671..31a2b8d2f73 100644 --- a/homeassistant/components/neato/translations/lb.json +++ b/homeassistant/components/neato/translations/lb.json @@ -17,6 +17,5 @@ "title": "Soll den Ariichtungs Prozess gestart ginn?" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/nl.json b/homeassistant/components/neato/translations/nl.json index 2e9ab212fa9..0bf8009b88e 100644 --- a/homeassistant/components/neato/translations/nl.json +++ b/homeassistant/components/neato/translations/nl.json @@ -18,6 +18,5 @@ "title": "Wilt u beginnen met instellen?" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/no.json b/homeassistant/components/neato/translations/no.json index fc50308eda8..d62c8e5d1f1 100644 --- a/homeassistant/components/neato/translations/no.json +++ b/homeassistant/components/neato/translations/no.json @@ -18,6 +18,5 @@ "title": "Vil du starte oppsettet?" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/pl.json b/homeassistant/components/neato/translations/pl.json index 34f1d42bda5..a97184b8b35 100644 --- a/homeassistant/components/neato/translations/pl.json +++ b/homeassistant/components/neato/translations/pl.json @@ -18,6 +18,5 @@ "title": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/pt-BR.json b/homeassistant/components/neato/translations/pt-BR.json index 684f7cdff22..41bc8c5e4a6 100644 --- a/homeassistant/components/neato/translations/pt-BR.json +++ b/homeassistant/components/neato/translations/pt-BR.json @@ -18,6 +18,5 @@ "title": "Deseja iniciar a configura\u00e7\u00e3o?" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/ru.json b/homeassistant/components/neato/translations/ru.json index 29201df669d..9c12f8c962c 100644 --- a/homeassistant/components/neato/translations/ru.json +++ b/homeassistant/components/neato/translations/ru.json @@ -18,6 +18,5 @@ "title": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/sl.json b/homeassistant/components/neato/translations/sl.json index 1aaae76ada8..a82510d9b95 100644 --- a/homeassistant/components/neato/translations/sl.json +++ b/homeassistant/components/neato/translations/sl.json @@ -18,6 +18,5 @@ "title": "Bi radi zagnali namestitev?" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/tr.json b/homeassistant/components/neato/translations/tr.json index a461d5b0c2f..4c93fdc78b0 100644 --- a/homeassistant/components/neato/translations/tr.json +++ b/homeassistant/components/neato/translations/tr.json @@ -18,6 +18,5 @@ "title": "Kuruluma ba\u015flamak ister misiniz?" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/uk.json b/homeassistant/components/neato/translations/uk.json index 353005546cf..532808c1fb6 100644 --- a/homeassistant/components/neato/translations/uk.json +++ b/homeassistant/components/neato/translations/uk.json @@ -18,6 +18,5 @@ "title": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/zh-Hant.json b/homeassistant/components/neato/translations/zh-Hant.json index 781ee1f952b..1252e80f4b7 100644 --- a/homeassistant/components/neato/translations/zh-Hant.json +++ b/homeassistant/components/neato/translations/zh-Hant.json @@ -18,6 +18,5 @@ "title": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" } } - }, - "title": "Neato Botvac" + } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/nl.json b/homeassistant/components/netatmo/translations/nl.json index 1a740f9d5b1..a5ddba5772c 100644 --- a/homeassistant/components/netatmo/translations/nl.json +++ b/homeassistant/components/netatmo/translations/nl.json @@ -59,7 +59,7 @@ }, "public_weather_areas": { "data": { - "new_area": "Naam van het gebied", + "new_area": "Naam van ruimte", "weather_areas": "Weersgebieden" }, "description": "Configureer openbare weersensoren.", diff --git a/homeassistant/components/netatmo/translations/pt-BR.json b/homeassistant/components/netatmo/translations/pt-BR.json index b47c0ea3646..9f438868d43 100644 --- a/homeassistant/components/netatmo/translations/pt-BR.json +++ b/homeassistant/components/netatmo/translations/pt-BR.json @@ -52,7 +52,7 @@ "lon_ne": "Longitude nordeste", "lon_sw": "Longitude sudoeste", "mode": "C\u00e1lculo", - "show_on_map": "Mostrar no mapa" + "show_on_map": "[%key:component::iss::config::step::user::data::show_on_map%]" }, "description": "Configure um sensor meteorol\u00f3gico p\u00fablico para uma \u00e1rea.", "title": "Sensor meteorol\u00f3gico p\u00fablico Netatmo" diff --git a/homeassistant/components/netgear/translations/bg.json b/homeassistant/components/netgear/translations/bg.json index dd5e4fa2f38..17bc7cd1666 100644 --- a/homeassistant/components/netgear/translations/bg.json +++ b/homeassistant/components/netgear/translations/bg.json @@ -11,18 +11,9 @@ "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)", "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" - } - } - }, - "options": { - "step": { - "init": { - "title": "Netgear" + "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}" } } } diff --git a/homeassistant/components/netgear/translations/ca.json b/homeassistant/components/netgear/translations/ca.json index 6d5edbd8e1f..f89e68cdfce 100644 --- a/homeassistant/components/netgear/translations/ca.json +++ b/homeassistant/components/netgear/translations/ca.json @@ -11,12 +11,9 @@ "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}\nNom d'usuari predeterminat: {username}", - "title": "Netgear" + "description": "Amfitri\u00f3 predeterminat: {host}\nNom d'usuari predeterminat: {username}" } } }, @@ -26,8 +23,7 @@ "data": { "consider_home": "Temps per considerar 'a casa' (segons)" }, - "description": "Especifica les configuracions opcional", - "title": "Netgear" + "description": "Especifica les configuracions opcional" } } } diff --git a/homeassistant/components/netgear/translations/cs.json b/homeassistant/components/netgear/translations/cs.json index 6d942ff2ff4..67e2611aa87 100644 --- a/homeassistant/components/netgear/translations/cs.json +++ b/homeassistant/components/netgear/translations/cs.json @@ -8,8 +8,6 @@ "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)" } } diff --git a/homeassistant/components/netgear/translations/de.json b/homeassistant/components/netgear/translations/de.json index b742fb36ec5..bbfadbb8254 100644 --- a/homeassistant/components/netgear/translations/de.json +++ b/homeassistant/components/netgear/translations/de.json @@ -11,12 +11,9 @@ "data": { "host": "Host (Optional)", "password": "Passwort", - "port": "Port (Optional)", - "ssl": "Verwendet ein SSL-Zertifikat", "username": "Benutzername (Optional)" }, - "description": "Standardhost: {host}\nStandardbenutzername: {username}", - "title": "Netgear" + "description": "Standardhost: {host}\nStandardbenutzername: {username}" } } }, @@ -26,8 +23,7 @@ "data": { "consider_home": "Zu Hause Zeit (Sekunden)" }, - "description": "Optionale Einstellungen angeben", - "title": "Netgear" + "description": "Optionale Einstellungen angeben" } } } diff --git a/homeassistant/components/netgear/translations/el.json b/homeassistant/components/netgear/translations/el.json index 0ac5cb328bc..51f982da008 100644 --- a/homeassistant/components/netgear/translations/el.json +++ b/homeassistant/components/netgear/translations/el.json @@ -11,12 +11,9 @@ "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2 (\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "port": "\u0398\u03cd\u03c1\u03b1 (\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", - "ssl": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ad\u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 (\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)" }, - "description": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2: {host}\n \u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7: {username}", - "title": "Netgear" + "description": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2: {host}\n \u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7: {username}" } } }, @@ -26,8 +23,7 @@ "data": { "consider_home": "\u0395\u03be\u03b5\u03c4\u03ac\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ce\u03c1\u03b1 \u03c3\u03c4\u03bf \u03c3\u03c0\u03af\u03c4\u03b9 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)" }, - "description": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ad\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2", - "title": "Netgear" + "description": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ad\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2" } } } diff --git a/homeassistant/components/netgear/translations/en.json b/homeassistant/components/netgear/translations/en.json index 42b014d9ed3..82712b7c738 100644 --- a/homeassistant/components/netgear/translations/en.json +++ b/homeassistant/components/netgear/translations/en.json @@ -11,12 +11,9 @@ "data": { "host": "Host (Optional)", "password": "Password", - "port": "Port (Optional)", - "ssl": "Uses an SSL certificate", "username": "Username (Optional)" }, - "description": "Default host: {host}\nDefault username: {username}", - "title": "Netgear" + "description": "Default host: {host}\nDefault username: {username}" } } }, @@ -26,8 +23,7 @@ "data": { "consider_home": "Consider home time (seconds)" }, - "description": "Specify optional settings", - "title": "Netgear" + "description": "Specify optional settings" } } } diff --git a/homeassistant/components/netgear/translations/es.json b/homeassistant/components/netgear/translations/es.json index e1edb726d52..9edbd1101d0 100644 --- a/homeassistant/components/netgear/translations/es.json +++ b/homeassistant/components/netgear/translations/es.json @@ -11,12 +11,9 @@ "data": { "host": "Host (Opcional)", "password": "Contrase\u00f1a", - "port": "Puerto (Opcional)", - "ssl": "Utiliza un certificado SSL", "username": "Usuario (Opcional)" }, - "description": "Host predeterminado: {host} \nNombre de usuario predeterminado: {username}", - "title": "Netgear" + "description": "Host predeterminado: {host} \nNombre de usuario predeterminado: {username}" } } }, @@ -26,8 +23,7 @@ "data": { "consider_home": "Considere el tiempo en casa (segundos)" }, - "description": "Especifica los ajustes opcionales", - "title": "Netgear" + "description": "Especifica los ajustes opcionales" } } } diff --git a/homeassistant/components/netgear/translations/et.json b/homeassistant/components/netgear/translations/et.json index dcbaf4bcb2e..56b999fdbef 100644 --- a/homeassistant/components/netgear/translations/et.json +++ b/homeassistant/components/netgear/translations/et.json @@ -11,12 +11,9 @@ "data": { "host": "Host (valikuline)", "password": "Salas\u00f5na", - "port": "Port (valikuline)", - "ssl": "Kasutusel on SSL sert", "username": "Kasutajanimi (valikuline)" }, - "description": "Vaikimisi host: {host}\nVaikimisi kasutajanimi: {username}", - "title": "Netgear" + "description": "Vaikimisi host: {host}\nVaikimisi kasutajanimi: {username}" } } }, @@ -26,8 +23,7 @@ "data": { "consider_home": "Kohaloleku m\u00e4\u00e4ramise aeg (sekundites)" }, - "description": "Valikuliste s\u00e4tete m\u00e4\u00e4ramine", - "title": "Netgear" + "description": "Valikuliste s\u00e4tete m\u00e4\u00e4ramine" } } } diff --git a/homeassistant/components/netgear/translations/fr.json b/homeassistant/components/netgear/translations/fr.json index fe507462e8d..a86df352ff6 100644 --- a/homeassistant/components/netgear/translations/fr.json +++ b/homeassistant/components/netgear/translations/fr.json @@ -11,12 +11,9 @@ "data": { "host": "H\u00f4te (facultatif)", "password": "Mot de passe", - "port": "Port (facultatif)", - "ssl": "Utilise un certificat SSL", "username": "Nom d'utilisateur (facultatif)" }, - "description": "H\u00f4te par d\u00e9faut\u00a0: {host}\nNom d'utilisateur par d\u00e9faut\u00a0: {username}", - "title": "Netgear" + "description": "H\u00f4te par d\u00e9faut\u00a0: {host}\nNom d'utilisateur par d\u00e9faut\u00a0: {username}" } } }, @@ -26,8 +23,7 @@ "data": { "consider_home": "Consid\u00e9rez le temps pass\u00e9 \u00e0 la maison (secondes)" }, - "description": "Sp\u00e9cifiez les param\u00e8tres optionnels", - "title": "Netgear" + "description": "Sp\u00e9cifiez les param\u00e8tres optionnels" } } } diff --git a/homeassistant/components/netgear/translations/he.json b/homeassistant/components/netgear/translations/he.json index f1f42b6c771..e1aa083fcb8 100644 --- a/homeassistant/components/netgear/translations/he.json +++ b/homeassistant/components/netgear/translations/he.json @@ -8,18 +8,8 @@ "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" + } } } } diff --git a/homeassistant/components/netgear/translations/hu.json b/homeassistant/components/netgear/translations/hu.json index cf8d31ec389..4432e08a508 100644 --- a/homeassistant/components/netgear/translations/hu.json +++ b/homeassistant/components/netgear/translations/hu.json @@ -11,12 +11,9 @@ "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 felhaszn\u00e1l\u00f3n\u00e9v: {username}", - "title": "Netgear" + "description": "Alap\u00e9rtelmezett c\u00edm: {host}\nAlap\u00e9rtelmezett felhaszn\u00e1l\u00f3n\u00e9v: {username}" } } }, @@ -26,8 +23,7 @@ "data": { "consider_home": "Otthoni \u00e1llapotnak tekint\u00e9s (m\u00e1sodperc)" }, - "description": "Opcion\u00e1lis be\u00e1ll\u00edt\u00e1sok megad\u00e1sa", - "title": "Netgear" + "description": "Opcion\u00e1lis be\u00e1ll\u00edt\u00e1sok megad\u00e1sa" } } } diff --git a/homeassistant/components/netgear/translations/id.json b/homeassistant/components/netgear/translations/id.json index 7f3eb3a0796..589459e180b 100644 --- a/homeassistant/components/netgear/translations/id.json +++ b/homeassistant/components/netgear/translations/id.json @@ -11,12 +11,9 @@ "data": { "host": "Host (Opsional)", "password": "Kata Sandi", - "port": "Port (Opsional)", - "ssl": "Menggunakan sertifikat SSL", "username": "Nama Pengguna (Opsional)" }, - "description": "Host default: {host}\nNama pengguna default: {username}", - "title": "Netgear" + "description": "Host default: {host}\nNama pengguna default: {username}" } } }, @@ -26,8 +23,7 @@ "data": { "consider_home": "Pertimbangan waktu sebagai di rumah (detik)" }, - "description": "Tentukan pengaturan opsional", - "title": "Netgear" + "description": "Tentukan pengaturan opsional" } } } diff --git a/homeassistant/components/netgear/translations/it.json b/homeassistant/components/netgear/translations/it.json index 15237e3a16a..28dc49326d2 100644 --- a/homeassistant/components/netgear/translations/it.json +++ b/homeassistant/components/netgear/translations/it.json @@ -11,12 +11,9 @@ "data": { "host": "Host (Facoltativo)", "password": "Password", - "port": "Porta (Facoltativo)", - "ssl": "Utilizza un certificato SSL", "username": "Nome utente (Facoltativo)" }, - "description": "Host predefinito: {host}\nNome utente predefinito: {username}", - "title": "Netgear" + "description": "Host predefinito: {host}\nNome utente predefinito: {username}" } } }, @@ -26,8 +23,7 @@ "data": { "consider_home": "Considera il tempo in casa (secondi)" }, - "description": "Specifica le impostazioni opzionali", - "title": "Netgear" + "description": "Specifica le impostazioni opzionali" } } } diff --git a/homeassistant/components/netgear/translations/ja.json b/homeassistant/components/netgear/translations/ja.json index a1d795bca9f..4402afcb92d 100644 --- a/homeassistant/components/netgear/translations/ja.json +++ b/homeassistant/components/netgear/translations/ja.json @@ -11,12 +11,9 @@ "data": { "host": "\u30db\u30b9\u30c8(\u30aa\u30d7\u30b7\u30e7\u30f3)", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8(\u30aa\u30d7\u30b7\u30e7\u30f3)", - "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", "username": "\u30e6\u30fc\u30b6\u30fc\u540d(\u30aa\u30d7\u30b7\u30e7\u30f3)" }, - "description": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30db\u30b9\u30c8: {host}\n\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30dd\u30fc\u30c8: {port}\n\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30e6\u30fc\u30b6\u30fc\u540d: {username}", - "title": "Netgear" + "description": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30db\u30b9\u30c8: {host}\n\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30dd\u30fc\u30c8: {port}\n\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30e6\u30fc\u30b6\u30fc\u540d: {username}" } } }, @@ -26,8 +23,7 @@ "data": { "consider_home": "\u30db\u30fc\u30e0\u30bf\u30a4\u30e0\u3092\u8003\u616e\u3059\u308b(\u79d2)" }, - "description": "\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a\u306e\u6307\u5b9a", - "title": "Netgear" + "description": "\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a\u306e\u6307\u5b9a" } } } diff --git a/homeassistant/components/netgear/translations/nl.json b/homeassistant/components/netgear/translations/nl.json index 1d0ae357a8f..95e824ef591 100644 --- a/homeassistant/components/netgear/translations/nl.json +++ b/homeassistant/components/netgear/translations/nl.json @@ -11,12 +11,9 @@ "data": { "host": "Host (optioneel)", "password": "Wachtwoord", - "port": "Poort (optioneel)", - "ssl": "Gebruikt een SSL certificaat", "username": "Gebruikersnaam (optioneel)" }, - "description": "Standaardhost: {host}\n Standaard gebruikersnaam: {username}", - "title": "Netgear" + "description": "Standaardhost: {host}\n Standaard gebruikersnaam: {username}" } } }, @@ -26,8 +23,7 @@ "data": { "consider_home": "Aantal seconden dat wordt gewacht voordat een apparaat als afwezig wordt beschouwd" }, - "description": "Optionele instellingen opgeven", - "title": "Netgear" + "description": "Optionele instellingen opgeven" } } } diff --git a/homeassistant/components/netgear/translations/no.json b/homeassistant/components/netgear/translations/no.json index 73735f3e91a..65b90889a20 100644 --- a/homeassistant/components/netgear/translations/no.json +++ b/homeassistant/components/netgear/translations/no.json @@ -11,12 +11,9 @@ "data": { "host": "Vert (valgfritt)", "password": "Passord", - "port": "Port (valgfritt)", - "ssl": "Bruker et SSL-sertifikat", "username": "Brukernavn (Valgfritt)" }, - "description": "Standard vert: {host}\n Standard brukernavn: {username}", - "title": "Netgear" + "description": "Standard vert: {host}\n Standard brukernavn: {username}" } } }, @@ -26,8 +23,7 @@ "data": { "consider_home": "Vurder hjemmetid (sekunder)" }, - "description": "Spesifiser valgfrie innstillinger", - "title": "Netgear" + "description": "Spesifiser valgfrie innstillinger" } } } diff --git a/homeassistant/components/netgear/translations/pl.json b/homeassistant/components/netgear/translations/pl.json index 0f3a6ad3cde..a3ec916efcc 100644 --- a/homeassistant/components/netgear/translations/pl.json +++ b/homeassistant/components/netgear/translations/pl.json @@ -11,12 +11,9 @@ "data": { "host": "Nazwa hosta lub adres IP (opcjonalnie)", "password": "Has\u0142o", - "port": "Port (opcjonalnie)", - "ssl": "Certyfikat SSL", "username": "Nazwa u\u017cytkownika (opcjonalnie)" }, - "description": "Domy\u015blne IP lub nazwa hosta: {host}\nDomy\u015blna nazwa u\u017cytkownika: {username}", - "title": "Netgear" + "description": "Domy\u015blne IP lub nazwa hosta: {host}\nDomy\u015blna nazwa u\u017cytkownika: {username}" } } }, @@ -26,8 +23,7 @@ "data": { "consider_home": "Czas przed oznaczeniem \"w domu\" (w sekundach)" }, - "description": "Okre\u015bl opcjonalne ustawienia", - "title": "Netgear" + "description": "Okre\u015bl opcjonalne ustawienia" } } } diff --git a/homeassistant/components/netgear/translations/pt-BR.json b/homeassistant/components/netgear/translations/pt-BR.json index 66ffd692374..ac52b1d18c3 100644 --- a/homeassistant/components/netgear/translations/pt-BR.json +++ b/homeassistant/components/netgear/translations/pt-BR.json @@ -11,12 +11,9 @@ "data": { "host": "Nome do host (Opcional)", "password": "Senha", - "port": "Porta (Opcional)", - "ssl": "Usar um certificado SSL", "username": "Usu\u00e1rio (Opcional)" }, - "description": "Host padr\u00e3o: {host}\n Nome de usu\u00e1rio padr\u00e3o: {username}", - "title": "Netgear" + "description": "Host padr\u00e3o: {host}\n Nome de usu\u00e1rio padr\u00e3o: {username}" } } }, @@ -26,8 +23,7 @@ "data": { "consider_home": "Considere o tempo de casa (segundos)" }, - "description": "Especifique configura\u00e7\u00f5es opcionais", - "title": "Netgear" + "description": "Especifique configura\u00e7\u00f5es opcionais" } } } diff --git a/homeassistant/components/netgear/translations/ru.json b/homeassistant/components/netgear/translations/ru.json index e6e5f9a008a..d2fd1f500f5 100644 --- a/homeassistant/components/netgear/translations/ru.json +++ b/homeassistant/components/netgear/translations/ru.json @@ -11,12 +11,9 @@ "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\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" + "description": "\u0425\u043e\u0441\u0442 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: {host}\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}" } } }, @@ -26,8 +23,7 @@ "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" + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" } } } diff --git a/homeassistant/components/netgear/translations/sk.json b/homeassistant/components/netgear/translations/sk.json deleted file mode 100644 index ea85ad39b4a..00000000000 --- a/homeassistant/components/netgear/translations/sk.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "port": "Port (volite\u013en\u00fd)" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/tr.json b/homeassistant/components/netgear/translations/tr.json index b29a9f075aa..2f49affd564 100644 --- a/homeassistant/components/netgear/translations/tr.json +++ b/homeassistant/components/netgear/translations/tr.json @@ -11,12 +11,9 @@ "data": { "host": "Sunucu (\u0130ste\u011fe ba\u011fl\u0131)", "password": "Parola", - "port": "Port (\u0130ste\u011fe ba\u011fl\u0131)", - "ssl": "SSL sertifikas\u0131 kullan\u0131r", "username": "Kullan\u0131c\u0131 Ad\u0131 (\u0130ste\u011fe ba\u011fl\u0131)" }, - "description": "Varsay\u0131lan sunucu: {host}\nVarsay\u0131lan kullan\u0131c\u0131 ad\u0131: {username}", - "title": "Netgear" + "description": "Varsay\u0131lan sunucu: {host}\nVarsay\u0131lan kullan\u0131c\u0131 ad\u0131: {username}" } } }, @@ -26,8 +23,7 @@ "data": { "consider_home": "Ev s\u00fcresini g\u00f6z \u00f6n\u00fcnde bulundurun (saniye)" }, - "description": "\u0130ste\u011fe ba\u011fl\u0131 ayarlar\u0131 belirtin", - "title": "Netgear" + "description": "\u0130ste\u011fe ba\u011fl\u0131 ayarlar\u0131 belirtin" } } } diff --git a/homeassistant/components/netgear/translations/zh-Hans.json b/homeassistant/components/netgear/translations/zh-Hans.json index dd7b165d2d4..4048634787b 100644 --- a/homeassistant/components/netgear/translations/zh-Hans.json +++ b/homeassistant/components/netgear/translations/zh-Hans.json @@ -11,20 +11,16 @@ "data": { "host": "\u4e3b\u673a\u5730\u5740 (\u53ef\u9009)", "password": "\u5bc6\u7801", - "port": "\u7aef\u53e3 (\u53ef\u9009)", - "ssl": "\u4f7f\u7528 SSL \u51ed\u8bc1\u767b\u5f55", "username": "\u7528\u6237\u540d (\u53ef\u9009)" }, - "description": "\u9ed8\u8ba4\u4e3b\u673a\u5730\u5740: {host}\n\u9ed8\u8ba4\u7528\u6237\u540d: {username}", - "title": "\u7f51\u4ef6\u8def\u7531\u5668" + "description": "\u9ed8\u8ba4\u4e3b\u673a\u5730\u5740: {host}\n\u9ed8\u8ba4\u7528\u6237\u540d: {username}" } } }, "options": { "step": { "init": { - "description": "\u6307\u5b9a\u53ef\u9009\u8bbe\u7f6e", - "title": "\u7f51\u4ef6\u8def\u7531\u5668" + "description": "\u6307\u5b9a\u53ef\u9009\u8bbe\u7f6e" } } } diff --git a/homeassistant/components/netgear/translations/zh-Hant.json b/homeassistant/components/netgear/translations/zh-Hant.json index 39b481f85b9..6a2866bc22d 100644 --- a/homeassistant/components/netgear/translations/zh-Hant.json +++ b/homeassistant/components/netgear/translations/zh-Hant.json @@ -11,12 +11,9 @@ "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\u4f7f\u7528\u8005\u540d\u7a31\uff1a{username}", - "title": "Netgear" + "description": "\u9810\u8a2d\u4e3b\u6a5f\u7aef\uff1a{host}\n\u9810\u8a2d\u4f7f\u7528\u8005\u540d\u7a31\uff1a{username}" } } }, @@ -26,8 +23,7 @@ "data": { "consider_home": "\u5224\u5b9a\u5728\u5bb6\u6642\u9593\uff08\u79d2\uff09" }, - "description": "\u6307\u5b9a\u9078\u9805\u8a2d\u5b9a", - "title": "Netgear" + "description": "\u6307\u5b9a\u9078\u9805\u8a2d\u5b9a" } } } diff --git a/homeassistant/components/nexia/translations/bg.json b/homeassistant/components/nexia/translations/bg.json index 0c8cf4f44a6..78264e2adbd 100644 --- a/homeassistant/components/nexia/translations/bg.json +++ b/homeassistant/components/nexia/translations/bg.json @@ -4,8 +4,7 @@ "user": { "data": { "brand": "\u041c\u0430\u0440\u043a\u0430" - }, - "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/ca.json b/homeassistant/components/nexia/translations/ca.json index e0f0c87f60f..f369d64bde9 100644 --- a/homeassistant/components/nexia/translations/ca.json +++ b/homeassistant/components/nexia/translations/ca.json @@ -14,8 +14,7 @@ "brand": "Marca", "password": "Contrasenya", "username": "Nom d'usuari" - }, - "title": "Connexi\u00f3 amb mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/cs.json b/homeassistant/components/nexia/translations/cs.json index 0196d0d03ed..dc27752e935 100644 --- a/homeassistant/components/nexia/translations/cs.json +++ b/homeassistant/components/nexia/translations/cs.json @@ -13,8 +13,7 @@ "data": { "password": "Heslo", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - }, - "title": "P\u0159ipojen\u00ed k mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/de.json b/homeassistant/components/nexia/translations/de.json index 94094f08037..b2eeebb2f17 100644 --- a/homeassistant/components/nexia/translations/de.json +++ b/homeassistant/components/nexia/translations/de.json @@ -14,8 +14,7 @@ "brand": "Marke", "password": "Passwort", "username": "Benutzername" - }, - "title": "Stelle eine Verbindung zu mynexia.com her" + } } } } diff --git a/homeassistant/components/nexia/translations/el.json b/homeassistant/components/nexia/translations/el.json index 47d2a624cd3..4db4ad008ed 100644 --- a/homeassistant/components/nexia/translations/el.json +++ b/homeassistant/components/nexia/translations/el.json @@ -14,8 +14,7 @@ "brand": "\u039c\u03ac\u03c1\u03ba\u03b1", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" - }, - "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/en.json b/homeassistant/components/nexia/translations/en.json index 20b0a137970..050db24e0cf 100644 --- a/homeassistant/components/nexia/translations/en.json +++ b/homeassistant/components/nexia/translations/en.json @@ -14,8 +14,7 @@ "brand": "Brand", "password": "Password", "username": "Username" - }, - "title": "Connect to mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/es-419.json b/homeassistant/components/nexia/translations/es-419.json index e2f04c7d4b4..ea277d080cc 100644 --- a/homeassistant/components/nexia/translations/es-419.json +++ b/homeassistant/components/nexia/translations/es-419.json @@ -13,8 +13,7 @@ "data": { "password": "Contrase\u00f1a", "username": "Nombre de usuario" - }, - "title": "Con\u00e9ctese a mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/es.json b/homeassistant/components/nexia/translations/es.json index 4306a8795b8..ee89d9db6f5 100644 --- a/homeassistant/components/nexia/translations/es.json +++ b/homeassistant/components/nexia/translations/es.json @@ -14,8 +14,7 @@ "brand": "Marca", "password": "Contrase\u00f1a", "username": "Usuario" - }, - "title": "Conectar con mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/et.json b/homeassistant/components/nexia/translations/et.json index 2f9348c1eed..7e24abee556 100644 --- a/homeassistant/components/nexia/translations/et.json +++ b/homeassistant/components/nexia/translations/et.json @@ -14,8 +14,7 @@ "brand": "Tootja", "password": "Salas\u00f5na", "username": "Kasutajanimi" - }, - "title": "\u00dchendu mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/fr.json b/homeassistant/components/nexia/translations/fr.json index 3955d07ff7a..be31f08787b 100644 --- a/homeassistant/components/nexia/translations/fr.json +++ b/homeassistant/components/nexia/translations/fr.json @@ -14,8 +14,7 @@ "brand": "Marque", "password": "Mot de passe", "username": "Nom d'utilisateur" - }, - "title": "Se connecter \u00e0 mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/he.json b/homeassistant/components/nexia/translations/he.json index 7f9efa5b24e..d27edd983c2 100644 --- a/homeassistant/components/nexia/translations/he.json +++ b/homeassistant/components/nexia/translations/he.json @@ -14,8 +14,7 @@ "brand": "\u05de\u05d5\u05ea\u05d2", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - }, - "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc-mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/hu.json b/homeassistant/components/nexia/translations/hu.json index ac85fec6456..f9178aafa07 100644 --- a/homeassistant/components/nexia/translations/hu.json +++ b/homeassistant/components/nexia/translations/hu.json @@ -14,8 +14,7 @@ "brand": "M\u00e1rka", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - }, - "title": "Csatlakoz\u00e1s a mynexia.com-hoz" + } } } } diff --git a/homeassistant/components/nexia/translations/id.json b/homeassistant/components/nexia/translations/id.json index 0600315fc78..1dba99e3984 100644 --- a/homeassistant/components/nexia/translations/id.json +++ b/homeassistant/components/nexia/translations/id.json @@ -14,8 +14,7 @@ "brand": "Merek", "password": "Kata Sandi", "username": "Nama Pengguna" - }, - "title": "Hubungkan ke mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/it.json b/homeassistant/components/nexia/translations/it.json index 65d06c3c8f0..dad681085de 100644 --- a/homeassistant/components/nexia/translations/it.json +++ b/homeassistant/components/nexia/translations/it.json @@ -14,8 +14,7 @@ "brand": "Marca", "password": "Password", "username": "Nome utente" - }, - "title": "Connettersi a mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/ja.json b/homeassistant/components/nexia/translations/ja.json index f06358ef78c..b80fe6c7cd2 100644 --- a/homeassistant/components/nexia/translations/ja.json +++ b/homeassistant/components/nexia/translations/ja.json @@ -14,8 +14,7 @@ "brand": "\u30d6\u30e9\u30f3\u30c9", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - }, - "title": "mynexia.com\u306b\u63a5\u7d9a" + } } } } diff --git a/homeassistant/components/nexia/translations/ko.json b/homeassistant/components/nexia/translations/ko.json index 4bd1589e4e8..94261de9637 100644 --- a/homeassistant/components/nexia/translations/ko.json +++ b/homeassistant/components/nexia/translations/ko.json @@ -13,8 +13,7 @@ "data": { "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - }, - "title": "mynexia.com\uc5d0 \uc5f0\uacb0\ud558\uae30" + } } } } diff --git a/homeassistant/components/nexia/translations/lb.json b/homeassistant/components/nexia/translations/lb.json index fa9b186ccbf..60b1bb6f305 100644 --- a/homeassistant/components/nexia/translations/lb.json +++ b/homeassistant/components/nexia/translations/lb.json @@ -13,8 +13,7 @@ "data": { "password": "Passwuert", "username": "Benotzernumm" - }, - "title": "Mat mynexia.com verbannen" + } } } } diff --git a/homeassistant/components/nexia/translations/nl.json b/homeassistant/components/nexia/translations/nl.json index 14efdfcd221..106dcc85cc7 100644 --- a/homeassistant/components/nexia/translations/nl.json +++ b/homeassistant/components/nexia/translations/nl.json @@ -14,8 +14,7 @@ "brand": "Brand", "password": "Wachtwoord", "username": "Gebruikersnaam" - }, - "title": "Verbinding maken met mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/no.json b/homeassistant/components/nexia/translations/no.json index 4533b94e48e..6cb06d0c827 100644 --- a/homeassistant/components/nexia/translations/no.json +++ b/homeassistant/components/nexia/translations/no.json @@ -14,8 +14,7 @@ "brand": "Merke", "password": "Passord", "username": "Brukernavn" - }, - "title": "Koble til mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/pl.json b/homeassistant/components/nexia/translations/pl.json index 6e40e21a7f0..8f4021ec9d8 100644 --- a/homeassistant/components/nexia/translations/pl.json +++ b/homeassistant/components/nexia/translations/pl.json @@ -14,8 +14,7 @@ "brand": "Marka", "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" - }, - "title": "Po\u0142\u0105czenie z mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/pt-BR.json b/homeassistant/components/nexia/translations/pt-BR.json index e9cce64aef9..01a830027fa 100644 --- a/homeassistant/components/nexia/translations/pt-BR.json +++ b/homeassistant/components/nexia/translations/pt-BR.json @@ -14,8 +14,7 @@ "brand": "Marca", "password": "Senha", "username": "Usu\u00e1rio" - }, - "title": "Conecte-se a mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/ru.json b/homeassistant/components/nexia/translations/ru.json index b5572c9f2da..e72388f6c9f 100644 --- a/homeassistant/components/nexia/translations/ru.json +++ b/homeassistant/components/nexia/translations/ru.json @@ -14,8 +14,7 @@ "brand": "\u041c\u0430\u0440\u043a\u0430", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" - }, - "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/sl.json b/homeassistant/components/nexia/translations/sl.json index 55c234fbde3..a1e926075f7 100644 --- a/homeassistant/components/nexia/translations/sl.json +++ b/homeassistant/components/nexia/translations/sl.json @@ -13,8 +13,7 @@ "data": { "password": "Geslo", "username": "Uporabni\u0161ko ime" - }, - "title": "Pove\u017eite se z mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/sv.json b/homeassistant/components/nexia/translations/sv.json index 60044361f65..9cfd620ac73 100644 --- a/homeassistant/components/nexia/translations/sv.json +++ b/homeassistant/components/nexia/translations/sv.json @@ -10,8 +10,7 @@ "data": { "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - }, - "title": "Anslut till mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/tr.json b/homeassistant/components/nexia/translations/tr.json index 8918bcd8f5e..11bacdf69df 100644 --- a/homeassistant/components/nexia/translations/tr.json +++ b/homeassistant/components/nexia/translations/tr.json @@ -14,8 +14,7 @@ "brand": "Marka", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - }, - "title": "Mynexia.com'a ba\u011flan\u0131n" + } } } } diff --git a/homeassistant/components/nexia/translations/uk.json b/homeassistant/components/nexia/translations/uk.json index 8cb2aec836a..49bceaa3f6e 100644 --- a/homeassistant/components/nexia/translations/uk.json +++ b/homeassistant/components/nexia/translations/uk.json @@ -13,8 +13,7 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" - }, - "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e mynexia.com" + } } } } diff --git a/homeassistant/components/nexia/translations/zh-Hant.json b/homeassistant/components/nexia/translations/zh-Hant.json index c066a433d1b..0924ebf7259 100644 --- a/homeassistant/components/nexia/translations/zh-Hant.json +++ b/homeassistant/components/nexia/translations/zh-Hant.json @@ -14,8 +14,7 @@ "brand": "\u54c1\u724c", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" - }, - "title": "\u9023\u7dda\u81f3 mynexia.com" + } } } } diff --git a/homeassistant/components/nfandroidtv/translations/bg.json b/homeassistant/components/nfandroidtv/translations/bg.json index 484dd2b98e3..5b58777a0e2 100644 --- a/homeassistant/components/nfandroidtv/translations/bg.json +++ b/homeassistant/components/nfandroidtv/translations/bg.json @@ -12,8 +12,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442", "name": "\u0418\u043c\u0435" - }, - "title": "\u0418\u0437\u0432\u0435\u0441\u0442\u0438\u044f \u0437\u0430 Android TV / Fire TV" + } } } } diff --git a/homeassistant/components/nfandroidtv/translations/ca.json b/homeassistant/components/nfandroidtv/translations/ca.json index 0eda4938d9d..90b910617f9 100644 --- a/homeassistant/components/nfandroidtv/translations/ca.json +++ b/homeassistant/components/nfandroidtv/translations/ca.json @@ -13,8 +13,7 @@ "host": "Amfitri\u00f3", "name": "Nom" }, - "description": "Consulta la documentaci\u00f3 per assegurar-te que compleixes tots els requisits.", - "title": "Notificacions per a Android TV / Fire TV" + "description": "Consulta la documentaci\u00f3 per assegurar-te que compleixes tots els requisits." } } } diff --git a/homeassistant/components/nfandroidtv/translations/de.json b/homeassistant/components/nfandroidtv/translations/de.json index d9df9058d13..fef64459261 100644 --- a/homeassistant/components/nfandroidtv/translations/de.json +++ b/homeassistant/components/nfandroidtv/translations/de.json @@ -13,8 +13,7 @@ "host": "Host", "name": "Name" }, - "description": "Bitte lies die Dokumentation, um sicherzustellen, dass alle Anforderungen erf\u00fcllt sind.", - "title": "Benachrichtigungen f\u00fcr Android TV / Fire TV" + "description": "Bitte lies die Dokumentation, um sicherzustellen, dass alle Anforderungen erf\u00fcllt sind." } } } diff --git a/homeassistant/components/nfandroidtv/translations/el.json b/homeassistant/components/nfandroidtv/translations/el.json index b95ec27a3bf..8512834e2b8 100644 --- a/homeassistant/components/nfandroidtv/translations/el.json +++ b/homeassistant/components/nfandroidtv/translations/el.json @@ -13,8 +13,7 @@ "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, - "description": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u0395\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b3\u03b9\u03b1 Android TV. \n\n \u0393\u03b9\u03b1 Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\n \u0393\u03b9\u03b1 Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\n \u0398\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03c1\u03ac\u03c4\u03b7\u03c3\u03b7 DHCP \u03c3\u03c4\u03bf \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2 (\u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b5\u03b3\u03c7\u03b5\u03b9\u03c1\u03af\u03b4\u03b9\u03bf \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2) \u03b5\u03af\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c4\u03b1\u03c4\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \u0395\u03ac\u03bd \u03cc\u03c7\u03b9, \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b5\u03af \u03c4\u03b5\u03bb\u03b9\u03ba\u03ac \u03bc\u03b7 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7.", - "title": "\u0395\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b3\u03b9\u03b1 Android TV / Fire TV" + "description": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u0395\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b3\u03b9\u03b1 Android TV. \n\n \u0393\u03b9\u03b1 Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\n \u0393\u03b9\u03b1 Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK \n\n \u0398\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03c1\u03ac\u03c4\u03b7\u03c3\u03b7 DHCP \u03c3\u03c4\u03bf \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2 (\u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b5\u03b3\u03c7\u03b5\u03b9\u03c1\u03af\u03b4\u03b9\u03bf \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03b1\u03c2) \u03b5\u03af\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c4\u03b1\u03c4\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \u0395\u03ac\u03bd \u03cc\u03c7\u03b9, \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b5\u03af \u03c4\u03b5\u03bb\u03b9\u03ba\u03ac \u03bc\u03b7 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7." } } } diff --git a/homeassistant/components/nfandroidtv/translations/en.json b/homeassistant/components/nfandroidtv/translations/en.json index 3c1383b91b8..4164a968c81 100644 --- a/homeassistant/components/nfandroidtv/translations/en.json +++ b/homeassistant/components/nfandroidtv/translations/en.json @@ -13,8 +13,7 @@ "host": "Host", "name": "Name" }, - "description": "Please refer to the documentation to make sure all requirements are met.", - "title": "Notifications for Android TV / Fire TV" + "description": "Please refer to the documentation to make sure all requirements are met." } } } diff --git a/homeassistant/components/nfandroidtv/translations/es.json b/homeassistant/components/nfandroidtv/translations/es.json index 880835cfb1e..e382855479f 100644 --- a/homeassistant/components/nfandroidtv/translations/es.json +++ b/homeassistant/components/nfandroidtv/translations/es.json @@ -13,8 +13,7 @@ "host": "Host", "name": "Nombre" }, - "description": "Esta integraci\u00f3n requiere la aplicaci\u00f3n de Notificaciones para Android TV.\n\nPara Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nPara Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nDebes configurar una reserva DHCP en su router (consulta el manual de usuario de tu router) o una direcci\u00f3n IP est\u00e1tica en el dispositivo. Si no, el dispositivo acabar\u00e1 por no estar disponible.", - "title": "Notificaciones para Android TV / Fire TV" + "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." } } } diff --git a/homeassistant/components/nfandroidtv/translations/et.json b/homeassistant/components/nfandroidtv/translations/et.json index f567795b608..4e5ae85ca00 100644 --- a/homeassistant/components/nfandroidtv/translations/et.json +++ b/homeassistant/components/nfandroidtv/translations/et.json @@ -13,8 +13,7 @@ "host": "Host", "name": "Nimi" }, - "description": "Vaata dokumentatsiooni, et veenduda, et k\u00f5ik n\u00f5uded on t\u00e4idetud.", - "title": "Android TV / Fire TV teavitused" + "description": "Vaata dokumentatsiooni, et veenduda, et k\u00f5ik n\u00f5uded on t\u00e4idetud." } } } diff --git a/homeassistant/components/nfandroidtv/translations/fr.json b/homeassistant/components/nfandroidtv/translations/fr.json index 7c69bbc6ac0..30d592c6edf 100644 --- a/homeassistant/components/nfandroidtv/translations/fr.json +++ b/homeassistant/components/nfandroidtv/translations/fr.json @@ -13,8 +13,7 @@ "host": "H\u00f4te", "name": "Nom" }, - "description": "Veuillez consulter la documentation afin de vous assurer que toutes les conditions sont respect\u00e9es.", - "title": "Notifications pour Android TV / Fire TV" + "description": "Veuillez consulter la documentation afin de vous assurer que toutes les conditions sont respect\u00e9es." } } } diff --git a/homeassistant/components/nfandroidtv/translations/he.json b/homeassistant/components/nfandroidtv/translations/he.json index 65eefcd05b4..cad2cfc04a3 100644 --- a/homeassistant/components/nfandroidtv/translations/he.json +++ b/homeassistant/components/nfandroidtv/translations/he.json @@ -13,8 +13,7 @@ "host": "\u05de\u05d0\u05e8\u05d7", "name": "\u05e9\u05dd" }, - "description": "\u05e0\u05d0 \u05dc\u05e2\u05d9\u05d9\u05df \u05d1\u05ea\u05d9\u05e2\u05d5\u05d3 \u05db\u05d3\u05d9 \u05dc\u05d5\u05d5\u05d3\u05d0 \u05e9\u05db\u05dc \u05d4\u05d3\u05e8\u05d9\u05e9\u05d5\u05ea \u05de\u05ea\u05e7\u05d9\u05d9\u05de\u05d5\u05ea.", - "title": "\u05d4\u05ea\u05e8\u05d0\u05d5\u05ea \u05e2\u05d1\u05d5\u05e8 \u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d9\u05ea \u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3 / \u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d9\u05ea \u05d0\u05de\u05d6\u05d5\u05df" + "description": "\u05e0\u05d0 \u05dc\u05e2\u05d9\u05d9\u05df \u05d1\u05ea\u05d9\u05e2\u05d5\u05d3 \u05db\u05d3\u05d9 \u05dc\u05d5\u05d5\u05d3\u05d0 \u05e9\u05db\u05dc \u05d4\u05d3\u05e8\u05d9\u05e9\u05d5\u05ea \u05de\u05ea\u05e7\u05d9\u05d9\u05de\u05d5\u05ea." } } } diff --git a/homeassistant/components/nfandroidtv/translations/hu.json b/homeassistant/components/nfandroidtv/translations/hu.json index 0c8d0567483..65cd2bebc01 100644 --- a/homeassistant/components/nfandroidtv/translations/hu.json +++ b/homeassistant/components/nfandroidtv/translations/hu.json @@ -13,8 +13,7 @@ "host": "C\u00edm", "name": "Elnevez\u00e9s" }, - "description": "K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, hogy megbizonyosodjon arr\u00f3l, hogy minden k\u00f6vetelm\u00e9ny teljes\u00fcl.", - "title": "\u00c9rtes\u00edt\u00e9sek Android TV / Fire TV eset\u00e9n" + "description": "K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, hogy megbizonyosodjon arr\u00f3l, hogy minden k\u00f6vetelm\u00e9ny teljes\u00fcl." } } } diff --git a/homeassistant/components/nfandroidtv/translations/id.json b/homeassistant/components/nfandroidtv/translations/id.json index de4ded1e98a..4495908ef84 100644 --- a/homeassistant/components/nfandroidtv/translations/id.json +++ b/homeassistant/components/nfandroidtv/translations/id.json @@ -13,8 +13,7 @@ "host": "Host", "name": "Nama" }, - "description": "Rujuk ke dokumentasi untuk memastikan semua persyaratan terpenuhi.", - "title": "Notifikasi untuk Android TV/Fire TV" + "description": "Rujuk ke dokumentasi untuk memastikan semua persyaratan terpenuhi." } } } diff --git a/homeassistant/components/nfandroidtv/translations/it.json b/homeassistant/components/nfandroidtv/translations/it.json index 83eea49b419..4fb88f80726 100644 --- a/homeassistant/components/nfandroidtv/translations/it.json +++ b/homeassistant/components/nfandroidtv/translations/it.json @@ -13,8 +13,7 @@ "host": "Host", "name": "Nome" }, - "description": "Fai riferimento alla documentazione per assicurarti che tutti i requisiti siano soddisfatti.", - "title": "Notifiche per Android TV / Fire TV" + "description": "Fai riferimento alla documentazione per assicurarti che tutti i requisiti siano soddisfatti." } } } diff --git a/homeassistant/components/nfandroidtv/translations/ja.json b/homeassistant/components/nfandroidtv/translations/ja.json index c3cd4fee235..fff28117234 100644 --- a/homeassistant/components/nfandroidtv/translations/ja.json +++ b/homeassistant/components/nfandroidtv/translations/ja.json @@ -13,8 +13,7 @@ "host": "\u30db\u30b9\u30c8", "name": "\u540d\u524d" }, - "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u306f\u3001AndroidTV\u30a2\u30d7\u30ea\u306e\u901a\u77e5\u304c\u5fc5\u8981\u3067\u3059\u3002 \n\nAndroid TV\u306e\u5834\u5408: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV\u306e\u5834\u5408: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04((DHCP reservation)\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167))\u307e\u305f\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u306b\u9759\u7684IP\u30a2\u30c9\u30ec\u30b9\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u305d\u3046\u3067\u306a\u3044\u5834\u5408\u3001\u30c7\u30d0\u30a4\u30b9\u306f\u6700\u7d42\u7684\u306b\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002", - "title": "Android TV / Fire TV\u306e\u901a\u77e5" + "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u306f\u3001AndroidTV\u30a2\u30d7\u30ea\u306e\u901a\u77e5\u304c\u5fc5\u8981\u3067\u3059\u3002 \n\nAndroid TV\u306e\u5834\u5408: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV\u306e\u5834\u5408: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04((DHCP reservation)\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167))\u307e\u305f\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u306b\u9759\u7684IP\u30a2\u30c9\u30ec\u30b9\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u305d\u3046\u3067\u306a\u3044\u5834\u5408\u3001\u30c7\u30d0\u30a4\u30b9\u306f\u6700\u7d42\u7684\u306b\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/nfandroidtv/translations/nl.json b/homeassistant/components/nfandroidtv/translations/nl.json index 10c9f44a94a..5a2342b740d 100644 --- a/homeassistant/components/nfandroidtv/translations/nl.json +++ b/homeassistant/components/nfandroidtv/translations/nl.json @@ -13,8 +13,7 @@ "host": "Host", "name": "Naam" }, - "description": "Raadpleeg de documentatie om er zeker van te zijn dat aan alle vereisten is voldaan.", - "title": "Meldingen voor Android TV / Fire TV" + "description": "Raadpleeg de documentatie om er zeker van te zijn dat aan alle vereisten is voldaan." } } } diff --git a/homeassistant/components/nfandroidtv/translations/no.json b/homeassistant/components/nfandroidtv/translations/no.json index 8d59ca40b0d..73c1a98538f 100644 --- a/homeassistant/components/nfandroidtv/translations/no.json +++ b/homeassistant/components/nfandroidtv/translations/no.json @@ -13,8 +13,7 @@ "host": "Vert", "name": "Navn" }, - "description": "Se dokumentasjonen for \u00e5 sikre at alle krav er oppfylt.", - "title": "Varsler for Android TV / Fire TV" + "description": "Se dokumentasjonen for \u00e5 sikre at alle krav er oppfylt." } } } diff --git a/homeassistant/components/nfandroidtv/translations/pl.json b/homeassistant/components/nfandroidtv/translations/pl.json index a42335ceb10..a050ee07a90 100644 --- a/homeassistant/components/nfandroidtv/translations/pl.json +++ b/homeassistant/components/nfandroidtv/translations/pl.json @@ -13,8 +13,7 @@ "host": "Nazwa hosta lub adres IP", "name": "Nazwa" }, - "description": "Zapoznaj si\u0119 z dokumentacj\u0105, aby upewni\u0107 si\u0119, \u017ce wszystkie wymagania s\u0105 spe\u0142nione.", - "title": "Powiadomienia dla Android TV / Fire TV" + "description": "Zapoznaj si\u0119 z dokumentacj\u0105, aby upewni\u0107 si\u0119, \u017ce wszystkie wymagania s\u0105 spe\u0142nione." } } } diff --git a/homeassistant/components/nfandroidtv/translations/pt-BR.json b/homeassistant/components/nfandroidtv/translations/pt-BR.json index f4488408b42..255c67527e9 100644 --- a/homeassistant/components/nfandroidtv/translations/pt-BR.json +++ b/homeassistant/components/nfandroidtv/translations/pt-BR.json @@ -13,8 +13,7 @@ "host": "Nome do host", "name": "Nome" }, - "description": "Consulte a documenta\u00e7\u00e3o para garantir que todos os requisitos sejam atendidos.", - "title": "Notifica\u00e7\u00f5es para Android TV / Fire TV" + "description": "Consulte a documenta\u00e7\u00e3o para garantir que todos os requisitos sejam atendidos." } } } diff --git a/homeassistant/components/nfandroidtv/translations/ru.json b/homeassistant/components/nfandroidtv/translations/ru.json index 12451d4735b..3cd5b134813 100644 --- a/homeassistant/components/nfandroidtv/translations/ru.json +++ b/homeassistant/components/nfandroidtv/translations/ru.json @@ -13,8 +13,7 @@ "host": "\u0425\u043e\u0441\u0442", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439, \u0447\u0442\u043e\u0431\u044b \u0443\u0431\u0435\u0434\u0438\u0442\u044c\u0441\u044f, \u0447\u0442\u043e \u0432\u0441\u0435 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u043e\u0431\u043b\u044e\u0434\u0435\u043d\u044b.", - "title": "Notifications for Android TV / Fire TV" + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439, \u0447\u0442\u043e\u0431\u044b \u0443\u0431\u0435\u0434\u0438\u0442\u044c\u0441\u044f, \u0447\u0442\u043e \u0432\u0441\u0435 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u043e\u0431\u043b\u044e\u0434\u0435\u043d\u044b." } } } diff --git a/homeassistant/components/nfandroidtv/translations/tr.json b/homeassistant/components/nfandroidtv/translations/tr.json index 835e61eea66..efb33dafb9c 100644 --- a/homeassistant/components/nfandroidtv/translations/tr.json +++ b/homeassistant/components/nfandroidtv/translations/tr.json @@ -13,8 +13,7 @@ "host": "Sunucu", "name": "Ad" }, - "description": "T\u00fcm gereksinimlerin kar\u015f\u0131land\u0131\u011f\u0131ndan emin olmak i\u00e7in l\u00fctfen belgelere bak\u0131n.", - "title": "Android TV / Fire TV i\u00e7in Bildirimler" + "description": "T\u00fcm gereksinimlerin kar\u015f\u0131land\u0131\u011f\u0131ndan emin olmak i\u00e7in l\u00fctfen belgelere bak\u0131n." } } } diff --git a/homeassistant/components/nfandroidtv/translations/zh-Hant.json b/homeassistant/components/nfandroidtv/translations/zh-Hant.json index 755ccdfeec8..f84b329d74f 100644 --- a/homeassistant/components/nfandroidtv/translations/zh-Hant.json +++ b/homeassistant/components/nfandroidtv/translations/zh-Hant.json @@ -13,8 +13,7 @@ "host": "\u4e3b\u6a5f\u7aef", "name": "\u540d\u7a31" }, - "description": "\u8acb\u53c3\u8003\u76f8\u95dc\u6587\u4ef6\u4ee5\u4e86\u89e3\u6240\u6709\u5fc5\u8981\u9700\u6c42\u3002", - "title": "Android TV / Fire TV \u901a\u77e5" + "description": "\u8acb\u53c3\u8003\u76f8\u95dc\u6587\u4ef6\u4ee5\u4e86\u89e3\u6240\u6709\u5fc5\u8981\u9700\u6c42\u3002" } } } diff --git a/homeassistant/components/nightscout/translations/ca.json b/homeassistant/components/nightscout/translations/ca.json index 21a472680b4..911c75a19de 100644 --- a/homeassistant/components/nightscout/translations/ca.json +++ b/homeassistant/components/nightscout/translations/ca.json @@ -8,7 +8,6 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/cs.json b/homeassistant/components/nightscout/translations/cs.json index dac121c7fb1..3a4f8bf2b0e 100644 --- a/homeassistant/components/nightscout/translations/cs.json +++ b/homeassistant/components/nightscout/translations/cs.json @@ -8,7 +8,6 @@ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/de.json b/homeassistant/components/nightscout/translations/de.json index 32bc6653af1..93524c79689 100644 --- a/homeassistant/components/nightscout/translations/de.json +++ b/homeassistant/components/nightscout/translations/de.json @@ -8,7 +8,6 @@ "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/el.json b/homeassistant/components/nightscout/translations/el.json index c484d715e76..c2359686297 100644 --- a/homeassistant/components/nightscout/translations/el.json +++ b/homeassistant/components/nightscout/translations/el.json @@ -8,7 +8,6 @@ "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/en.json b/homeassistant/components/nightscout/translations/en.json index baec475fc2d..97ce458c8c0 100644 --- a/homeassistant/components/nightscout/translations/en.json +++ b/homeassistant/components/nightscout/translations/en.json @@ -8,7 +8,6 @@ "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/es.json b/homeassistant/components/nightscout/translations/es.json index 1731cd7f48c..d98b8e345b6 100644 --- a/homeassistant/components/nightscout/translations/es.json +++ b/homeassistant/components/nightscout/translations/es.json @@ -8,7 +8,6 @@ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/et.json b/homeassistant/components/nightscout/translations/et.json index 0d00cebb6a5..7521ee633c5 100644 --- a/homeassistant/components/nightscout/translations/et.json +++ b/homeassistant/components/nightscout/translations/et.json @@ -8,7 +8,6 @@ "invalid_auth": "Tuvastamise viga", "unknown": "Tundmatu viga" }, - "flow_title": "", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/fr.json b/homeassistant/components/nightscout/translations/fr.json index 0b99785652d..9c9cd110990 100644 --- a/homeassistant/components/nightscout/translations/fr.json +++ b/homeassistant/components/nightscout/translations/fr.json @@ -8,7 +8,6 @@ "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/hu.json b/homeassistant/components/nightscout/translations/hu.json index 569a4f3aca9..85f0976d017 100644 --- a/homeassistant/components/nightscout/translations/hu.json +++ b/homeassistant/components/nightscout/translations/hu.json @@ -8,7 +8,6 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/id.json b/homeassistant/components/nightscout/translations/id.json index 147c3131213..d9a113784da 100644 --- a/homeassistant/components/nightscout/translations/id.json +++ b/homeassistant/components/nightscout/translations/id.json @@ -8,7 +8,6 @@ "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/it.json b/homeassistant/components/nightscout/translations/it.json index 8a1bfc826e2..c0438787915 100644 --- a/homeassistant/components/nightscout/translations/it.json +++ b/homeassistant/components/nightscout/translations/it.json @@ -8,7 +8,6 @@ "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/ja.json b/homeassistant/components/nightscout/translations/ja.json index d0a88be1985..211113d943c 100644 --- a/homeassistant/components/nightscout/translations/ja.json +++ b/homeassistant/components/nightscout/translations/ja.json @@ -8,7 +8,6 @@ "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/ko.json b/homeassistant/components/nightscout/translations/ko.json index 1146e6926e3..7574e1f6d47 100644 --- a/homeassistant/components/nightscout/translations/ko.json +++ b/homeassistant/components/nightscout/translations/ko.json @@ -8,7 +8,6 @@ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/lb.json b/homeassistant/components/nightscout/translations/lb.json index cc1f048e84e..b592e209dd8 100644 --- a/homeassistant/components/nightscout/translations/lb.json +++ b/homeassistant/components/nightscout/translations/lb.json @@ -8,7 +8,6 @@ "invalid_auth": "Ong\u00eblteg Authentifikatioun", "unknown": "Onerwaarte Feeler" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/nl.json b/homeassistant/components/nightscout/translations/nl.json index a9b81e9403e..397150acbde 100644 --- a/homeassistant/components/nightscout/translations/nl.json +++ b/homeassistant/components/nightscout/translations/nl.json @@ -8,7 +8,6 @@ "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/no.json b/homeassistant/components/nightscout/translations/no.json index 6a21b13aca7..08f1ae09fee 100644 --- a/homeassistant/components/nightscout/translations/no.json +++ b/homeassistant/components/nightscout/translations/no.json @@ -8,7 +8,6 @@ "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, - "flow_title": "", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/pl.json b/homeassistant/components/nightscout/translations/pl.json index bc70aab3bf8..fe0da7ee321 100644 --- a/homeassistant/components/nightscout/translations/pl.json +++ b/homeassistant/components/nightscout/translations/pl.json @@ -8,7 +8,6 @@ "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/pt-BR.json b/homeassistant/components/nightscout/translations/pt-BR.json index 4c31da85534..b8ebefa1f08 100644 --- a/homeassistant/components/nightscout/translations/pt-BR.json +++ b/homeassistant/components/nightscout/translations/pt-BR.json @@ -8,7 +8,6 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/ru.json b/homeassistant/components/nightscout/translations/ru.json index c7688973c1b..7e24a9cf1f2 100644 --- a/homeassistant/components/nightscout/translations/ru.json +++ b/homeassistant/components/nightscout/translations/ru.json @@ -8,7 +8,6 @@ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/tr.json b/homeassistant/components/nightscout/translations/tr.json index 8a27b28aa6e..dc134ac4f40 100644 --- a/homeassistant/components/nightscout/translations/tr.json +++ b/homeassistant/components/nightscout/translations/tr.json @@ -8,7 +8,6 @@ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen hata" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/uk.json b/homeassistant/components/nightscout/translations/uk.json index 6504b00eb88..f23c457f553 100644 --- a/homeassistant/components/nightscout/translations/uk.json +++ b/homeassistant/components/nightscout/translations/uk.json @@ -8,7 +8,6 @@ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nightscout/translations/zh-Hant.json b/homeassistant/components/nightscout/translations/zh-Hant.json index 2ad1c4fde39..cbf1a33cde5 100644 --- a/homeassistant/components/nightscout/translations/zh-Hant.json +++ b/homeassistant/components/nightscout/translations/zh-Hant.json @@ -8,7 +8,6 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "Nightscout", "step": { "user": { "data": { diff --git a/homeassistant/components/nmap_tracker/translations/ar.json b/homeassistant/components/nmap_tracker/translations/ar.json index 9f223966416..620adf5b41c 100644 --- a/homeassistant/components/nmap_tracker/translations/ar.json +++ b/homeassistant/components/nmap_tracker/translations/ar.json @@ -16,8 +16,7 @@ "step": { "init": { "data": { - "interval_seconds": "\u0641\u062a\u0631\u0629 \u062a\u0643\u0631\u0627\u0631 \u0627\u0644\u0628\u062d\u062b", - "track_new_devices": "\u062a\u062a\u0628\u0639 \u0627\u0644\u0623\u062c\u0647\u0632\u0629 \u0627\u0644\u062c\u062f\u064a\u062f\u0629" + "interval_seconds": "\u0641\u062a\u0631\u0629 \u062a\u0643\u0631\u0627\u0631 \u0627\u0644\u0628\u062d\u062b" } } } diff --git a/homeassistant/components/nmap_tracker/translations/ca.json b/homeassistant/components/nmap_tracker/translations/ca.json index e72c03cc63a..fd0cbadf71a 100644 --- a/homeassistant/components/nmap_tracker/translations/ca.json +++ b/homeassistant/components/nmap_tracker/translations/ca.json @@ -30,8 +30,7 @@ "home_interval": "Nombre m\u00ednim de minuts entre escanejos de dispositius actius (conserva la bateria)", "hosts": "Adreces de xarxa a escanejar (separades per comes)", "interval_seconds": "Interval d'escaneig", - "scan_options": "Opcions de configuraci\u00f3 d'escaneig d'Nmap en brut", - "track_new_devices": "Segueix dispositius nous" + "scan_options": "Opcions de configuraci\u00f3 d'escaneig d'Nmap en brut" }, "description": "Configura els amfitrions a explorar per Nmap. L'adre\u00e7a de xarxa i les exclusions poden ser adreces IP (192.168.1.1), xarxes IP (192.168.0.0/24) o intervals IP (192.168.1.0-32)." } diff --git a/homeassistant/components/nmap_tracker/translations/cs.json b/homeassistant/components/nmap_tracker/translations/cs.json index ac5f913d8e6..95f0bfc3ae8 100644 --- a/homeassistant/components/nmap_tracker/translations/cs.json +++ b/homeassistant/components/nmap_tracker/translations/cs.json @@ -8,8 +8,7 @@ "step": { "init": { "data": { - "interval_seconds": "Interval skenov\u00e1n\u00ed", - "track_new_devices": "Sledovat nov\u00e1 za\u0159\u00edzen\u00ed" + "interval_seconds": "Interval skenov\u00e1n\u00ed" } } } diff --git a/homeassistant/components/nmap_tracker/translations/de.json b/homeassistant/components/nmap_tracker/translations/de.json index 48f06bf320b..cfab5b828fd 100644 --- a/homeassistant/components/nmap_tracker/translations/de.json +++ b/homeassistant/components/nmap_tracker/translations/de.json @@ -30,8 +30,7 @@ "home_interval": "Mindestanzahl von Minuten zwischen den Scans aktiver Ger\u00e4te (Batterie schonen)", "hosts": "Netzwerkadressen (kommagetrennt) zum Scannen", "interval_seconds": "Scanintervall", - "scan_options": "Raw konfigurierbare Scan-Optionen f\u00fcr Nmap", - "track_new_devices": "Neue Ger\u00e4te verfolgen" + "scan_options": "Raw konfigurierbare Scan-Optionen f\u00fcr Nmap" }, "description": "Konfiguriere die Hosts, die von Nmap gescannt werden sollen. Netzwerkadresse und Ausschl\u00fcsse k\u00f6nnen IP-Adressen (192.168.1.1), IP-Netzwerke (192.168.0.0/24) oder IP-Bereiche (192.168.1.0-32) sein." } diff --git a/homeassistant/components/nmap_tracker/translations/el.json b/homeassistant/components/nmap_tracker/translations/el.json index 202bc8e0722..fe1a448a8be 100644 --- a/homeassistant/components/nmap_tracker/translations/el.json +++ b/homeassistant/components/nmap_tracker/translations/el.json @@ -30,8 +30,7 @@ "home_interval": "\u0395\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03bb\u03b5\u03c0\u03c4\u03ce\u03bd \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c4\u03c9\u03bd \u03c3\u03b1\u03c1\u03ce\u03c3\u03b5\u03c9\u03bd \u03b5\u03bd\u03b5\u03c1\u03b3\u03ce\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd (\u03b4\u03b9\u03b1\u03c4\u03ae\u03c1\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1\u03c2)", "hosts": "\u0394\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 (\u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1) \u03b3\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7", "interval_seconds": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7\u03c2", - "scan_options": "\u0391\u03ba\u03b1\u03c4\u03ad\u03c1\u03b3\u03b1\u03c3\u03c4\u03b5\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b9\u03bc\u03b5\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf Nmap", - "track_new_devices": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03bd\u03ad\u03c9\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd" + "scan_options": "\u0391\u03ba\u03b1\u03c4\u03ad\u03c1\u03b3\u03b1\u03c3\u03c4\u03b5\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b9\u03bc\u03b5\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf Nmap" }, "description": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03ce\u03bd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Nmap. \u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03ba\u03b1\u03b9 \u03bf\u03b9 \u03b5\u03be\u03b1\u03b9\u03c1\u03ad\u03c3\u03b5\u03b9\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u0394\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 IP (192.168.1.1), \u0394\u03af\u03ba\u03c4\u03c5\u03b1 IP (192.168.0.0/24) \u03ae \u0395\u03cd\u03c1\u03bf\u03c2 IP (192.168.1.0-32)." } diff --git a/homeassistant/components/nmap_tracker/translations/en.json b/homeassistant/components/nmap_tracker/translations/en.json index ee615b87e91..ae4175e0f14 100644 --- a/homeassistant/components/nmap_tracker/translations/en.json +++ b/homeassistant/components/nmap_tracker/translations/en.json @@ -30,8 +30,7 @@ "home_interval": "Minimum number of minutes between scans of active devices (preserve battery)", "hosts": "Network addresses (comma separated) to scan", "interval_seconds": "Scan interval", - "scan_options": "Raw configurable scan options for Nmap", - "track_new_devices": "Track new devices" + "scan_options": "Raw configurable scan options for Nmap" }, "description": "Configure hosts to be scanned by Nmap. Network address and excludes can be IP Addresses (192.168.1.1), IP Networks (192.168.0.0/24) or IP Ranges (192.168.1.0-32)." } diff --git a/homeassistant/components/nmap_tracker/translations/es.json b/homeassistant/components/nmap_tracker/translations/es.json index e68d8ff59ab..9b943edb310 100644 --- a/homeassistant/components/nmap_tracker/translations/es.json +++ b/homeassistant/components/nmap_tracker/translations/es.json @@ -30,8 +30,7 @@ "home_interval": "N\u00famero m\u00ednimo de minutos entre los escaneos de los dispositivos activos (preservar la bater\u00eda)", "hosts": "Direcciones de red a escanear (separadas por comas)", "interval_seconds": "Intervalo de exploraci\u00f3n", - "scan_options": "Opciones de escaneo configurables sin procesar para Nmap", - "track_new_devices": "Seguimiento de nuevos dispositivos" + "scan_options": "Opciones de escaneo configurables sin procesar para Nmap" }, "description": "Configure los hosts que ser\u00e1n escaneados por Nmap. Las direcciones de red y los excluidos pueden ser direcciones IP (192.168.1.1), redes IP (192.168.0.0/24) o rangos IP (192.168.1.0-32)." } diff --git a/homeassistant/components/nmap_tracker/translations/et.json b/homeassistant/components/nmap_tracker/translations/et.json index ac23cb7004f..e0b922fd007 100644 --- a/homeassistant/components/nmap_tracker/translations/et.json +++ b/homeassistant/components/nmap_tracker/translations/et.json @@ -30,8 +30,7 @@ "home_interval": "Minimaalne sk\u00e4nnimiste intervall minutites (eeldus on aku s\u00e4\u00e4stmine)", "hosts": "V\u00f5rguaadresside vahemik (komaga eraldatud)", "interval_seconds": "P\u00e4ringute intervall", - "scan_options": "Vaikimisi Nmap sk\u00e4nnimise valikud", - "track_new_devices": "Uute seadmete j\u00e4lgimine" + "scan_options": "Vaikimisi Nmap sk\u00e4nnimise valikud" }, "description": "Vali Nmap poolt sk\u00e4nnitavad hostid. Valikuks on IP aadressid (192.168.1.1), v\u00f5rgud (192.168.0.0/24) v\u00f5i IP vahemikud (192.168.1.0-32)." } diff --git a/homeassistant/components/nmap_tracker/translations/fr.json b/homeassistant/components/nmap_tracker/translations/fr.json index 0a4e75a9813..b0362217d33 100644 --- a/homeassistant/components/nmap_tracker/translations/fr.json +++ b/homeassistant/components/nmap_tracker/translations/fr.json @@ -30,8 +30,7 @@ "home_interval": "Nombre minimum de minutes entre les analyses des appareils actifs (pr\u00e9server la batterie)", "hosts": "Adresses r\u00e9seau (s\u00e9par\u00e9es par des virgules) \u00e0 analyser", "interval_seconds": "Intervalle d\u2019analyse", - "scan_options": "Options d'analyse brutes configurables pour Nmap", - "track_new_devices": "Suivre les nouveaux appareils" + "scan_options": "Options d'analyse brutes configurables pour Nmap" }, "description": "Configurer les h\u00f4tes \u00e0 analyser par Nmap. L'adresse r\u00e9seau et les exclusions peuvent \u00eatre des adresses IP (192.168.1.1), des r\u00e9seaux IP (192.168.0.0/24) ou des plages IP (192.168.1.0-32)." } diff --git a/homeassistant/components/nmap_tracker/translations/he.json b/homeassistant/components/nmap_tracker/translations/he.json index d57ca363944..22bc8fa7080 100644 --- a/homeassistant/components/nmap_tracker/translations/he.json +++ b/homeassistant/components/nmap_tracker/translations/he.json @@ -29,8 +29,7 @@ "home_interval": "\u05de\u05e1\u05e4\u05e8 \u05de\u05d9\u05e0\u05d9\u05de\u05dc\u05d9 \u05e9\u05dc \u05d3\u05e7\u05d5\u05ea \u05d1\u05d9\u05df \u05e1\u05e8\u05d9\u05e7\u05d5\u05ea \u05e9\u05dc \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05e4\u05e2\u05d9\u05dc\u05d9\u05dd (\u05e9\u05d9\u05de\u05d5\u05e8 \u05e1\u05d5\u05dc\u05dc\u05d4)", "hosts": "\u05db\u05ea\u05d5\u05d1\u05d5\u05ea \u05e8\u05e9\u05ea (\u05de\u05d5\u05e4\u05e8\u05d3\u05d5\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7) \u05dc\u05e1\u05e8\u05d9\u05e7\u05d4", "interval_seconds": "\u05de\u05e8\u05d5\u05d5\u05d7 \u05d6\u05de\u05df \u05dc\u05e1\u05e8\u05d9\u05e7\u05d4", - "scan_options": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e1\u05e8\u05d9\u05e7\u05d4 \u05d2\u05d5\u05dc\u05de\u05d9\u05d5\u05ea \u05d4\u05e0\u05d9\u05ea\u05e0\u05d5\u05ea \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4 \u05e2\u05d1\u05d5\u05e8 Nmap", - "track_new_devices": "\u05de\u05e2\u05e7\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d7\u05d3\u05e9\u05d9\u05dd" + "scan_options": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05e1\u05e8\u05d9\u05e7\u05d4 \u05d2\u05d5\u05dc\u05de\u05d9\u05d5\u05ea \u05d4\u05e0\u05d9\u05ea\u05e0\u05d5\u05ea \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4 \u05e2\u05d1\u05d5\u05e8 Nmap" }, "description": "\u05e7\u05d1\u05e2 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05de\u05d0\u05e8\u05d7\u05d9\u05dd \u05e9\u05d9\u05e1\u05e8\u05e7\u05d5 \u05e2\u05dc \u05d9\u05d3\u05d9 Nmap. \u05db\u05ea\u05d5\u05d1\u05ea \u05e8\u05e9\u05ea \u05d5\u05d0\u05d9 \u05d4\u05db\u05dc\u05dc\u05d4 \u05d9\u05db\u05d5\u05dc \u05dc\u05d4\u05d9\u05d5\u05ea \u05db\u05ea\u05d5\u05d1\u05d5\u05ea IP (192.168.1.1), \u05e8\u05e9\u05ea\u05d5\u05ea IP (192.168.0.0/24) \u05d0\u05d5 \u05d8\u05d5\u05d5\u05d7\u05d9 IP (192.168.1.0-32)." } diff --git a/homeassistant/components/nmap_tracker/translations/hu.json b/homeassistant/components/nmap_tracker/translations/hu.json index f54cf208e92..6c802e6d25c 100644 --- a/homeassistant/components/nmap_tracker/translations/hu.json +++ b/homeassistant/components/nmap_tracker/translations/hu.json @@ -30,8 +30,7 @@ "home_interval": "Minim\u00e1lis percsz\u00e1m az akt\u00edv eszk\u00f6z\u00f6k vizsg\u00e1lata k\u00f6z\u00f6tt (akkumul\u00e1tor k\u00edm\u00e9l\u00e9se)", "hosts": "H\u00e1l\u00f3zati c\u00edmek (vessz\u0151vel elv\u00e1lasztva) a beolvas\u00e1shoz", "interval_seconds": "Szkennel\u00e9si intervallum", - "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" + "scan_options": "Nyersen konfigur\u00e1lhat\u00f3 beolvas\u00e1si lehet\u0151s\u00e9gek az Nmap sz\u00e1m\u00e1ra" }, "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 e1894aff49b..fae1fdfe1bd 100644 --- a/homeassistant/components/nmap_tracker/translations/id.json +++ b/homeassistant/components/nmap_tracker/translations/id.json @@ -30,8 +30,7 @@ "home_interval": "Jumlah menit minimum antara pemindaian perangkat aktif (menghemat baterai)", "hosts": "Alamat jaringan (dipisahkan koma) untuk pemindaian", "interval_seconds": "Interval pindai", - "scan_options": "Opsi pemindaian mentah yang dapat dikonfigurasi untuk Nmap", - "track_new_devices": "Lacak perangkat baru" + "scan_options": "Opsi pemindaian mentah yang dapat dikonfigurasi untuk Nmap" }, "description": "Konfigurasikan host untuk dipindai oleh Nmap. Alamat jaringan dan pengecualian dapat berupa alamat IP (192.168.1.1), jaringan IP (192.168.0.0/24), atau rentang IP (192.168.1.0-32)." } diff --git a/homeassistant/components/nmap_tracker/translations/it.json b/homeassistant/components/nmap_tracker/translations/it.json index 8a7de165778..e6d5a41656f 100644 --- a/homeassistant/components/nmap_tracker/translations/it.json +++ b/homeassistant/components/nmap_tracker/translations/it.json @@ -30,8 +30,7 @@ "home_interval": "Numero minimo di minuti tra le scansioni dei dispositivi attivi (preserva la batteria)", "hosts": "Indirizzi di rete (separati da virgole) da scansionare", "interval_seconds": "Intervallo di scansione", - "scan_options": "Opzioni di scansione configurabili non elaborate per Nmap", - "track_new_devices": "Traccia nuovi dispositivi" + "scan_options": "Opzioni di scansione configurabili non elaborate per Nmap" }, "description": "Configura gli host da scansionare con Nmap. L'indirizzo di rete e le esclusioni possono essere indirizzi IP (192.168.1.1), reti IP (192.168.0.0/24) o intervalli IP (192.168.1.0-32)." } diff --git a/homeassistant/components/nmap_tracker/translations/ja.json b/homeassistant/components/nmap_tracker/translations/ja.json index 2e60a814468..60de8477859 100644 --- a/homeassistant/components/nmap_tracker/translations/ja.json +++ b/homeassistant/components/nmap_tracker/translations/ja.json @@ -30,8 +30,7 @@ "home_interval": "\u30a2\u30af\u30c6\u30a3\u30d6\u306a\u30c7\u30d0\u30a4\u30b9\u306e\u30b9\u30ad\u30e3\u30f3\u9593\u9694(\u5206)\u306e\u6700\u5c0f\u6642\u9593(\u30d0\u30c3\u30c6\u30ea\u30fc\u3092\u7bc0\u7d04)", "hosts": "\u30b9\u30ad\u30e3\u30f3\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9(\u30b3\u30f3\u30de\u533a\u5207\u308a)", "interval_seconds": "\u30b9\u30ad\u30e3\u30f3\u9593\u9694", - "scan_options": "Nmap\u306b\u672a\u52a0\u5de5\u3067\u305d\u306e\u307e\u307e\u6e21\u3055\u308c\u308b\u30b9\u30ad\u30e3\u30f3\u8a2d\u5b9a\u306e\u30aa\u30d7\u30b7\u30e7\u30f3", - "track_new_devices": "\u65b0\u3057\u3044\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u8de1" + "scan_options": "Nmap\u306b\u672a\u52a0\u5de5\u3067\u305d\u306e\u307e\u307e\u6e21\u3055\u308c\u308b\u30b9\u30ad\u30e3\u30f3\u8a2d\u5b9a\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" }, "description": "Nmap\u3067\u30b9\u30ad\u30e3\u30f3\u3055\u308c\u308b\u30db\u30b9\u30c8\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30a2\u30c9\u30ec\u30b9\u304a\u3088\u3073\u9664\u5916\u5bfe\u8c61\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9(192.168.1.1)\u3001IP\u30cd\u30c3\u30c8\u30ef\u30fc\u30af(192.168.0.0/24)\u3001\u307e\u305f\u306f\u3001IP\u7bc4\u56f2(192.168.1.0-32)\u3067\u3059\u3002" } diff --git a/homeassistant/components/nmap_tracker/translations/nl.json b/homeassistant/components/nmap_tracker/translations/nl.json index 8385aca1ffe..2331d7fda9f 100644 --- a/homeassistant/components/nmap_tracker/translations/nl.json +++ b/homeassistant/components/nmap_tracker/translations/nl.json @@ -30,8 +30,7 @@ "home_interval": "Minimum aantal minuten tussen scans van actieve apparaten (batterij sparen)", "hosts": "Netwerkadressen (gescheiden door komma's) om te scannen", "interval_seconds": "Scaninterval", - "scan_options": "Ruwe configureerbare scanopties voor Nmap", - "track_new_devices": "Volg nieuwe apparaten" + "scan_options": "Ruwe configureerbare scanopties voor Nmap" }, "description": "Configureer hosts die moeten worden gescand door Nmap. Netwerkadres en uitsluitingen kunnen IP-adressen (192.168.1.1), IP-netwerken (192.168.0.0/24) of IP-bereiken (192.168.1.0-32) zijn." } diff --git a/homeassistant/components/nmap_tracker/translations/no.json b/homeassistant/components/nmap_tracker/translations/no.json index 1de32bed121..de8ca5898aa 100644 --- a/homeassistant/components/nmap_tracker/translations/no.json +++ b/homeassistant/components/nmap_tracker/translations/no.json @@ -30,8 +30,7 @@ "home_interval": "Minimum antall minutter mellom skanninger av aktive enheter (lagre batteri)", "hosts": "Nettverksadresser (atskilt med komma) som skal skannes", "interval_seconds": "Skanneintervall", - "scan_options": "R\u00e5 konfigurerbare skannealternativer for Nmap", - "track_new_devices": "Spor nye enheter" + "scan_options": "R\u00e5 konfigurerbare skannealternativer for Nmap" }, "description": "Konfigurer verter som skal skannes av Nmap. Nettverksadresse og ekskluderer kan v\u00e6re IP-adresser (192.168.1.1), IP-nettverk (192.168.0.0/24) eller IP-omr\u00e5der (192.168.1.0-32)." } diff --git a/homeassistant/components/nmap_tracker/translations/pl.json b/homeassistant/components/nmap_tracker/translations/pl.json index 98f27dcdd4b..98b16043d4d 100644 --- a/homeassistant/components/nmap_tracker/translations/pl.json +++ b/homeassistant/components/nmap_tracker/translations/pl.json @@ -30,8 +30,7 @@ "home_interval": "Minimalna liczba minut mi\u0119dzy skanami aktywnych urz\u0105dze\u0144 (oszcz\u0119dzanie baterii)", "hosts": "Adresy sieciowe (oddzielone przecinkami) do skanowania", "interval_seconds": "Cz\u0119stotliwo\u015b\u0107 skanowania", - "scan_options": "Surowe konfigurowalne opcje skanowania dla Nmap", - "track_new_devices": "\u015aled\u017a nowe urz\u0105dzenia" + "scan_options": "Surowe konfigurowalne opcje skanowania dla Nmap" }, "description": "Skonfiguruj hosta do skanowania przez Nmap. Adresy sieciowe i te wykluczone mog\u0105 by\u0107 adresami IP (192.168.1.1), sieciami IP (192.168.0.0/24) lub zakresami IP (192.168.1.0-32)." } diff --git a/homeassistant/components/nmap_tracker/translations/pt-BR.json b/homeassistant/components/nmap_tracker/translations/pt-BR.json index a80213ce5d6..786d5d4df0b 100644 --- a/homeassistant/components/nmap_tracker/translations/pt-BR.json +++ b/homeassistant/components/nmap_tracker/translations/pt-BR.json @@ -30,8 +30,7 @@ "home_interval": "N\u00famero m\u00ednimo de minutos entre escaneamento de dispositivos ativos (preservar bateria)", "hosts": "Endere\u00e7os de rede (separados por v\u00edrgula) para digitalizar", "interval_seconds": "Intervalo de varredura", - "scan_options": "Op\u00e7\u00f5es de escaneamento bruto configur\u00e1veis para Nmap", - "track_new_devices": "Rastrear novos dispositivos" + "scan_options": "Op\u00e7\u00f5es de escaneamento bruto configur\u00e1veis para Nmap" }, "description": "Configure os hosts a serem verificados pelo Nmap. O endere\u00e7o de rede e as exclus\u00f5es podem ser endere\u00e7os IP (192.168.1.1), redes IP (192.168.0.0/24) ou intervalos de IP (192.168.1.0-32)." } diff --git a/homeassistant/components/nmap_tracker/translations/ru.json b/homeassistant/components/nmap_tracker/translations/ru.json index ba143e20d01..125f394fc23 100644 --- a/homeassistant/components/nmap_tracker/translations/ru.json +++ b/homeassistant/components/nmap_tracker/translations/ru.json @@ -30,8 +30,7 @@ "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)", "interval_seconds": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f", - "scan_options": "\u041d\u0435\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0434\u043b\u044f Nmap", - "track_new_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + "scan_options": "\u041d\u0435\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0434\u043b\u044f Nmap" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0445\u043e\u0441\u0442\u043e\u0432 \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f Nmap. \u0421\u0435\u0442\u0435\u0432\u044b\u043c \u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441\u0430 (192.168.1.1), IP-\u0441\u0435\u0442\u0438 (192.168.0.0/24) \u0438\u043b\u0438 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u044b IP-\u0430\u0434\u0440\u0435\u0441\u043e\u0432 (192.168.1.0-32)." } diff --git a/homeassistant/components/nmap_tracker/translations/tr.json b/homeassistant/components/nmap_tracker/translations/tr.json index 8e5dadb8100..f4d984f2bdb 100644 --- a/homeassistant/components/nmap_tracker/translations/tr.json +++ b/homeassistant/components/nmap_tracker/translations/tr.json @@ -30,8 +30,7 @@ "home_interval": "Aktif cihazlar\u0131n taranmas\u0131 aras\u0131ndaki minimum dakika say\u0131s\u0131 (pilden tasarruf edin)", "hosts": "Taranacak a\u011f adresleri (virg\u00fclle ayr\u0131lm\u0131\u015f)", "interval_seconds": "Tarama aral\u0131\u011f\u0131", - "scan_options": "Nmap i\u00e7in ham yap\u0131land\u0131r\u0131labilir tarama se\u00e7enekleri", - "track_new_devices": "Yeni cihazlar\u0131 izle" + "scan_options": "Nmap i\u00e7in ham yap\u0131land\u0131r\u0131labilir tarama se\u00e7enekleri" }, "description": "Nmap taraf\u0131ndan taranacak ana bilgisayarlar\u0131 yap\u0131land\u0131r\u0131n. A\u011f adresi ve hari\u00e7 tutulanlar, IP Adresleri (192.168.1.1), IP A\u011flar\u0131 (192.168.0.0/24) veya IP Aral\u0131klar\u0131 (192.168.1.0-32) olabilir." } diff --git a/homeassistant/components/nmap_tracker/translations/zh-Hans.json b/homeassistant/components/nmap_tracker/translations/zh-Hans.json index 5b1be2f497d..31f4568a848 100644 --- a/homeassistant/components/nmap_tracker/translations/zh-Hans.json +++ b/homeassistant/components/nmap_tracker/translations/zh-Hans.json @@ -27,8 +27,7 @@ "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", "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" + "scan_options": "Nmap \u7684\u539f\u59cb\u53ef\u914d\u7f6e\u626b\u63cf\u9009\u9879" }, "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/nmap_tracker/translations/zh-Hant.json b/homeassistant/components/nmap_tracker/translations/zh-Hant.json index 65b0b7afed4..acee979e8b8 100644 --- a/homeassistant/components/nmap_tracker/translations/zh-Hant.json +++ b/homeassistant/components/nmap_tracker/translations/zh-Hant.json @@ -30,8 +30,7 @@ "home_interval": "\u6383\u63cf\u6d3b\u52d5\u88dd\u7f6e\u7684\u6700\u4f4e\u9593\u9694\u5206\u9418\uff08\u8003\u91cf\u7701\u96fb\uff09", "hosts": "\u6240\u8981\u6383\u63cf\u7684\u7db2\u8def\u4f4d\u5740\uff08\u4ee5\u9017\u865f\u5206\u9694\uff09", "interval_seconds": "\u6383\u63cf\u9593\u8ddd", - "scan_options": "Nmap \u539f\u59cb\u8a2d\u5b9a\u6383\u63cf\u9078\u9805", - "track_new_devices": "\u8ffd\u8e64\u65b0\u88dd\u7f6e" + "scan_options": "Nmap \u539f\u59cb\u8a2d\u5b9a\u6383\u63cf\u9078\u9805" }, "description": "\u8a2d\u5b9a Nmap \u6383\u63cf\u4e3b\u6a5f\u3002\u7db2\u8def\u4f4d\u5740\u8207\u6392\u9664\u4f4d\u5740\u53ef\u4ee5\u662f IP \u4f4d\u5740\uff08192.168.1.1\uff09\u3001IP \u7db2\u8def\uff08192.168.0.0/24\uff09\u6216 IP \u7bc4\u570d\uff08192.168.1.0-32\uff09\u3002" } diff --git a/homeassistant/components/notion/translations/bg.json b/homeassistant/components/notion/translations/bg.json index ef4a919401e..1df2ad13a33 100644 --- a/homeassistant/components/notion/translations/bg.json +++ b/homeassistant/components/notion/translations/bg.json @@ -4,7 +4,6 @@ "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", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/notion/translations/ca.json b/homeassistant/components/notion/translations/ca.json index 51ca461f854..4047a5dbd30 100644 --- a/homeassistant/components/notion/translations/ca.json +++ b/homeassistant/components/notion/translations/ca.json @@ -6,7 +6,6 @@ }, "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "no_devices": "No s'han trobat dispositius al compte", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/notion/translations/cs.json b/homeassistant/components/notion/translations/cs.json index 53cae8ab73b..57ce0a06b95 100644 --- a/homeassistant/components/notion/translations/cs.json +++ b/homeassistant/components/notion/translations/cs.json @@ -6,7 +6,6 @@ }, "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", - "no_devices": "V \u00fa\u010dtu nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { diff --git a/homeassistant/components/notion/translations/da.json b/homeassistant/components/notion/translations/da.json index e8a1d35417a..add31a51324 100644 --- a/homeassistant/components/notion/translations/da.json +++ b/homeassistant/components/notion/translations/da.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Dette brugernavn er allerede i brug." }, - "error": { - "no_devices": "Ingen enheder fundet i konto" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/notion/translations/de.json b/homeassistant/components/notion/translations/de.json index 59ab1fdc1be..6ee08a8a732 100644 --- a/homeassistant/components/notion/translations/de.json +++ b/homeassistant/components/notion/translations/de.json @@ -6,7 +6,6 @@ }, "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung", - "no_devices": "Keine Ger\u00e4te im Konto gefunden", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/notion/translations/el.json b/homeassistant/components/notion/translations/el.json index ee39e67ae73..89c175337ce 100644 --- a/homeassistant/components/notion/translations/el.json +++ b/homeassistant/components/notion/translations/el.json @@ -6,7 +6,6 @@ }, "error": { "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", - "no_devices": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { diff --git a/homeassistant/components/notion/translations/en.json b/homeassistant/components/notion/translations/en.json index afd58a4d404..0eb4689bce6 100644 --- a/homeassistant/components/notion/translations/en.json +++ b/homeassistant/components/notion/translations/en.json @@ -6,7 +6,6 @@ }, "error": { "invalid_auth": "Invalid authentication", - "no_devices": "No devices found in account", "unknown": "Unexpected error" }, "step": { diff --git a/homeassistant/components/notion/translations/es-419.json b/homeassistant/components/notion/translations/es-419.json index 489df80f736..ec520bd89df 100644 --- a/homeassistant/components/notion/translations/es-419.json +++ b/homeassistant/components/notion/translations/es-419.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Este nombre de usuario ya est\u00e1 en uso." }, - "error": { - "no_devices": "No se han encontrado dispositivos en la cuenta." - }, "step": { "user": { "data": { diff --git a/homeassistant/components/notion/translations/es.json b/homeassistant/components/notion/translations/es.json index b537bd5bd3b..62a21b06022 100644 --- a/homeassistant/components/notion/translations/es.json +++ b/homeassistant/components/notion/translations/es.json @@ -6,7 +6,6 @@ }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "no_devices": "No se han encontrado dispositivos en la cuenta", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/notion/translations/et.json b/homeassistant/components/notion/translations/et.json index 7639901201d..488e66c57a3 100644 --- a/homeassistant/components/notion/translations/et.json +++ b/homeassistant/components/notion/translations/et.json @@ -6,7 +6,6 @@ }, "error": { "invalid_auth": "Tuvastamise viga", - "no_devices": "Kontolt ei leitud \u00fchtegi seadet", "unknown": "Ootamatu t\u00f5rge" }, "step": { diff --git a/homeassistant/components/notion/translations/fr.json b/homeassistant/components/notion/translations/fr.json index c5c0145e972..2340ad7620e 100644 --- a/homeassistant/components/notion/translations/fr.json +++ b/homeassistant/components/notion/translations/fr.json @@ -6,7 +6,6 @@ }, "error": { "invalid_auth": "Authentification non valide", - "no_devices": "Aucun appareil trouv\u00e9 sur le compte", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/notion/translations/hr.json b/homeassistant/components/notion/translations/hr.json index 237845502dd..75f9880c4e0 100644 --- a/homeassistant/components/notion/translations/hr.json +++ b/homeassistant/components/notion/translations/hr.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "no_devices": "Nisu prona\u0111eni ure\u0111aji na ra\u010dunu" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/notion/translations/hu.json b/homeassistant/components/notion/translations/hu.json index ef10a451a84..82e7df49eed 100644 --- a/homeassistant/components/notion/translations/hu.json +++ b/homeassistant/components/notion/translations/hu.json @@ -6,7 +6,6 @@ }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "no_devices": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a fi\u00f3kban", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { diff --git a/homeassistant/components/notion/translations/id.json b/homeassistant/components/notion/translations/id.json index bf8cb8e1186..8191e9bc85a 100644 --- a/homeassistant/components/notion/translations/id.json +++ b/homeassistant/components/notion/translations/id.json @@ -6,7 +6,6 @@ }, "error": { "invalid_auth": "Autentikasi tidak valid", - "no_devices": "Tidak ada perangkat yang ditemukan di akun", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/notion/translations/it.json b/homeassistant/components/notion/translations/it.json index 811884dddb4..48cda247e03 100644 --- a/homeassistant/components/notion/translations/it.json +++ b/homeassistant/components/notion/translations/it.json @@ -6,7 +6,6 @@ }, "error": { "invalid_auth": "Autenticazione non valida", - "no_devices": "Nessun dispositivo trovato nell'account", "unknown": "Errore imprevisto" }, "step": { diff --git a/homeassistant/components/notion/translations/ja.json b/homeassistant/components/notion/translations/ja.json index fb764116ecf..8cd2a58e1e0 100644 --- a/homeassistant/components/notion/translations/ja.json +++ b/homeassistant/components/notion/translations/ja.json @@ -6,7 +6,6 @@ }, "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "no_devices": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/notion/translations/ko.json b/homeassistant/components/notion/translations/ko.json index c81805446f9..991553a1531 100644 --- a/homeassistant/components/notion/translations/ko.json +++ b/homeassistant/components/notion/translations/ko.json @@ -4,8 +4,7 @@ "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { - "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "no_devices": "\uacc4\uc815\uc5d0 \ub4f1\ub85d\ub41c \uae30\uae30\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/notion/translations/lb.json b/homeassistant/components/notion/translations/lb.json index a1bc1601e00..9a7ca2855de 100644 --- a/homeassistant/components/notion/translations/lb.json +++ b/homeassistant/components/notion/translations/lb.json @@ -4,8 +4,7 @@ "already_configured": "Kont ass scho konfigur\u00e9iert" }, "error": { - "invalid_auth": "Ong\u00eblteg Authentifikatioun", - "no_devices": "Keng Apparater am Kont fonnt" + "invalid_auth": "Ong\u00eblteg Authentifikatioun" }, "step": { "user": { diff --git a/homeassistant/components/notion/translations/nl.json b/homeassistant/components/notion/translations/nl.json index 81da85f6240..3a86ffd2264 100644 --- a/homeassistant/components/notion/translations/nl.json +++ b/homeassistant/components/notion/translations/nl.json @@ -6,7 +6,6 @@ }, "error": { "invalid_auth": "Ongeldige authenticatie", - "no_devices": "Geen apparaten gevonden in account", "unknown": "Onverwachte fout" }, "step": { diff --git a/homeassistant/components/notion/translations/no.json b/homeassistant/components/notion/translations/no.json index 0bbbeb9c0dd..b8ffb36e040 100644 --- a/homeassistant/components/notion/translations/no.json +++ b/homeassistant/components/notion/translations/no.json @@ -6,7 +6,6 @@ }, "error": { "invalid_auth": "Ugyldig godkjenning", - "no_devices": "Ingen enheter funnet i kontoen", "unknown": "Uventet feil" }, "step": { diff --git a/homeassistant/components/notion/translations/pl.json b/homeassistant/components/notion/translations/pl.json index edd7b608b3d..bda5f89b4e1 100644 --- a/homeassistant/components/notion/translations/pl.json +++ b/homeassistant/components/notion/translations/pl.json @@ -6,7 +6,6 @@ }, "error": { "invalid_auth": "Niepoprawne uwierzytelnienie", - "no_devices": "Nie znaleziono urz\u0105dze\u0144 na koncie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { diff --git a/homeassistant/components/notion/translations/pt-BR.json b/homeassistant/components/notion/translations/pt-BR.json index d778a301ee1..d7d15016852 100644 --- a/homeassistant/components/notion/translations/pt-BR.json +++ b/homeassistant/components/notion/translations/pt-BR.json @@ -6,7 +6,6 @@ }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "no_devices": "Nenhum dispositivo encontrado na conta", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/notion/translations/ru.json b/homeassistant/components/notion/translations/ru.json index bebd8a66e0e..9ad8a0a8626 100644 --- a/homeassistant/components/notion/translations/ru.json +++ b/homeassistant/components/notion/translations/ru.json @@ -6,7 +6,6 @@ }, "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.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/notion/translations/sl.json b/homeassistant/components/notion/translations/sl.json index 9e4a4c8d77e..8b9dc856382 100644 --- a/homeassistant/components/notion/translations/sl.json +++ b/homeassistant/components/notion/translations/sl.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "To uporabni\u0161ko ime je \u017ee v uporabi." }, - "error": { - "no_devices": "V ra\u010dunu ni najdene nobene naprave" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/notion/translations/sv.json b/homeassistant/components/notion/translations/sv.json index 0cbf4a937b9..71836f79877 100644 --- a/homeassistant/components/notion/translations/sv.json +++ b/homeassistant/components/notion/translations/sv.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Det h\u00e4r anv\u00e4ndarnamnet anv\u00e4nds redan." }, - "error": { - "no_devices": "Inga enheter hittades p\u00e5 kontot" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/notion/translations/tr.json b/homeassistant/components/notion/translations/tr.json index cd95e502c8d..51ba57e9fa6 100644 --- a/homeassistant/components/notion/translations/tr.json +++ b/homeassistant/components/notion/translations/tr.json @@ -6,7 +6,6 @@ }, "error": { "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "no_devices": "Hesapta cihaz bulunamad\u0131", "unknown": "Beklenmeyen hata" }, "step": { diff --git a/homeassistant/components/notion/translations/uk.json b/homeassistant/components/notion/translations/uk.json index 6dc969c3609..1dd26aaf2a7 100644 --- a/homeassistant/components/notion/translations/uk.json +++ b/homeassistant/components/notion/translations/uk.json @@ -4,8 +4,7 @@ "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." }, "error": { - "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", - "no_devices": "\u041d\u0435\u043c\u0430\u0454 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432, \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u0438\u0445 \u0437 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u043c \u0437\u0430\u043f\u0438\u0441\u043e\u043c." + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." }, "step": { "user": { diff --git a/homeassistant/components/notion/translations/zh-Hans.json b/homeassistant/components/notion/translations/zh-Hans.json index 2d670456235..0b54dcf7771 100644 --- a/homeassistant/components/notion/translations/zh-Hans.json +++ b/homeassistant/components/notion/translations/zh-Hans.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "no_devices": "\u5e10\u6237\u4e2d\u627e\u4e0d\u5230\u8bbe\u5907" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/notion/translations/zh-Hant.json b/homeassistant/components/notion/translations/zh-Hant.json index 951ec07c8ad..7feae143a22 100644 --- a/homeassistant/components/notion/translations/zh-Hant.json +++ b/homeassistant/components/notion/translations/zh-Hant.json @@ -6,7 +6,6 @@ }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u88dd\u7f6e", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { diff --git a/homeassistant/components/nut/translations/bg.json b/homeassistant/components/nut/translations/bg.json index 1413a7f101c..4983c9a14b2 100644 --- a/homeassistant/components/nut/translations/bg.json +++ b/homeassistant/components/nut/translations/bg.json @@ -1,16 +1,6 @@ { "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/nut/translations/ca.json b/homeassistant/components/nut/translations/ca.json index fc8b5b719a0..c90f6fa8a12 100644 --- a/homeassistant/components/nut/translations/ca.json +++ b/homeassistant/components/nut/translations/ca.json @@ -8,16 +8,9 @@ "unknown": "Error inesperat" }, "step": { - "resources": { - "data": { - "resources": "Recursos" - }, - "title": "Selecciona els recursos a monitoritzar" - }, "ups": { "data": { - "alias": "\u00c0lies", - "resources": "Recursos" + "alias": "\u00c0lies" }, "title": "Selecciona el SAI (UPS) a monitoritzar" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "Ha fallat la connexi\u00f3", - "unknown": "Error inesperat" - }, "step": { "init": { "data": { - "resources": "Recursos", "scan_interval": "Interval d'escaneig (segons)" - }, - "description": "Selecciona els recursos del sensor." + } } } } diff --git a/homeassistant/components/nut/translations/cs.json b/homeassistant/components/nut/translations/cs.json index 37d73391ecf..839b01d622d 100644 --- a/homeassistant/components/nut/translations/cs.json +++ b/homeassistant/components/nut/translations/cs.json @@ -8,16 +8,9 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { - "resources": { - "data": { - "resources": "Zdroje" - }, - "title": "Vyberte zdroje, kter\u00e9 chcete sledovat" - }, "ups": { "data": { - "alias": "Alias", - "resources": "Zdroje" + "alias": "Alias" }, "title": "Vyberte UPS, kterou chcete sledovat" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" - }, "step": { "init": { "data": { - "resources": "Zdroje", "scan_interval": "Interval sledov\u00e1n\u00ed (v sekund\u00e1ch)" - }, - "description": "Zvolte zdroje senzoru." + } } } } diff --git a/homeassistant/components/nut/translations/de.json b/homeassistant/components/nut/translations/de.json index e82dca2506b..2e2d16a94d7 100644 --- a/homeassistant/components/nut/translations/de.json +++ b/homeassistant/components/nut/translations/de.json @@ -8,16 +8,9 @@ "unknown": "Unerwarteter Fehler" }, "step": { - "resources": { - "data": { - "resources": "Ressourcen" - }, - "title": "W\u00e4hle die zu \u00fcberwachenden Ressourcen aus" - }, "ups": { "data": { - "alias": "Alias", - "resources": "Ressourcen" + "alias": "Alias" }, "title": "W\u00e4hle die zu \u00fcberwachende USV" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "Verbindung fehlgeschlagen", - "unknown": "Unerwarteter Fehler" - }, "step": { "init": { "data": { - "resources": "Ressourcen", "scan_interval": "Scan-Intervall (Sekunden)" - }, - "description": "W\u00e4hle Sensorressourcen." + } } } } diff --git a/homeassistant/components/nut/translations/el.json b/homeassistant/components/nut/translations/el.json index 971d6476a23..a42670f70d8 100644 --- a/homeassistant/components/nut/translations/el.json +++ b/homeassistant/components/nut/translations/el.json @@ -8,16 +8,9 @@ "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { - "resources": { - "data": { - "resources": "\u03a0\u03cc\u03c1\u03bf\u03b9" - }, - "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03c0\u03cc\u03c1\u03bf\u03c5\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7" - }, "ups": { "data": { - "alias": "\u03a8\u03b5\u03c5\u03b4\u03ce\u03bd\u03c5\u03bc\u03bf", - "resources": "\u03a0\u03cc\u03c1\u03bf\u03b9" + "alias": "\u03a8\u03b5\u03c5\u03b4\u03ce\u03bd\u03c5\u03bc\u03bf" }, "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf UPS \u03b3\u03b9\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" - }, "step": { "init": { "data": { - "resources": "\u03a0\u03cc\u03c1\u03bf\u03b9", "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)" - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03cc\u03c1\u03bf\u03c5\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03c9\u03bd." + } } } } diff --git a/homeassistant/components/nut/translations/en.json b/homeassistant/components/nut/translations/en.json index 3d57189f7a5..fb8e548902b 100644 --- a/homeassistant/components/nut/translations/en.json +++ b/homeassistant/components/nut/translations/en.json @@ -8,16 +8,9 @@ "unknown": "Unexpected error" }, "step": { - "resources": { - "data": { - "resources": "Resources" - }, - "title": "Choose the Resources to Monitor" - }, "ups": { "data": { - "alias": "Alias", - "resources": "Resources" + "alias": "Alias" }, "title": "Choose the UPS to Monitor" }, @@ -33,17 +26,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/homeassistant/components/nut/translations/es-419.json b/homeassistant/components/nut/translations/es-419.json index b33bc854b23..992abf2d7c3 100644 --- a/homeassistant/components/nut/translations/es-419.json +++ b/homeassistant/components/nut/translations/es-419.json @@ -8,16 +8,9 @@ "unknown": "Error inesperado" }, "step": { - "resources": { - "data": { - "resources": "Recursos" - }, - "title": "Seleccione los recursos para monitorear" - }, "ups": { "data": { - "alias": "Alias", - "resources": "Recursos" + "alias": "Alias" }, "title": "Seleccione el UPS para monitorear" }, @@ -36,10 +29,8 @@ "step": { "init": { "data": { - "resources": "Recursos", "scan_interval": "Intervalo de escaneo (segundos)" - }, - "description": "Seleccione los Recursos del sensor." + } } } } diff --git a/homeassistant/components/nut/translations/es.json b/homeassistant/components/nut/translations/es.json index 234f34082e1..898bf1f027d 100644 --- a/homeassistant/components/nut/translations/es.json +++ b/homeassistant/components/nut/translations/es.json @@ -8,16 +8,9 @@ "unknown": "Error inesperado" }, "step": { - "resources": { - "data": { - "resources": "Recursos" - }, - "title": "Selecciona los recursos a monitorizar" - }, "ups": { "data": { - "alias": "Alias", - "resources": "Recursos" + "alias": "Alias" }, "title": "Selecciona el UPS a monitorizar" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "No se pudo conectar", - "unknown": "Error inesperado" - }, "step": { "init": { "data": { - "resources": "Recursos", "scan_interval": "Intervalo de escaneo (segundos)" - }, - "description": "Elige los recursos del sensor." + } } } } diff --git a/homeassistant/components/nut/translations/et.json b/homeassistant/components/nut/translations/et.json index 83d793eb22c..3b52b9856c7 100644 --- a/homeassistant/components/nut/translations/et.json +++ b/homeassistant/components/nut/translations/et.json @@ -8,16 +8,9 @@ "unknown": "Tundmatu viga" }, "step": { - "resources": { - "data": { - "resources": "Ressursid" - }, - "title": "Vali j\u00e4lgitavad ressursid" - }, "ups": { "data": { - "alias": "", - "resources": "Ressursid" + "alias": "" }, "title": "Vali j\u00e4lgitava UPS" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "\u00dchendus nurjus", - "unknown": "Tundmatu viga" - }, "step": { "init": { "data": { - "resources": "Ressursid", "scan_interval": "P\u00e4ringute intervall (sekundites)" - }, - "description": "Vali anduri ressursid." + } } } } diff --git a/homeassistant/components/nut/translations/fr.json b/homeassistant/components/nut/translations/fr.json index 3e1b345a8f1..18bde05e550 100644 --- a/homeassistant/components/nut/translations/fr.json +++ b/homeassistant/components/nut/translations/fr.json @@ -8,16 +8,9 @@ "unknown": "Erreur inattendue" }, "step": { - "resources": { - "data": { - "resources": "Ressources" - }, - "title": "Choisissez les ressources \u00e0 surveiller" - }, "ups": { "data": { - "alias": "Alias", - "resources": "Ressources" + "alias": "Alias" }, "title": "Choisir l'UPS \u00e0 surveiller" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "\u00c9chec de connexion", - "unknown": "Erreur inattendue" - }, "step": { "init": { "data": { - "resources": "Ressources", "scan_interval": "Intervalle de balayage (secondes)" - }, - "description": "Choisissez les ressources des capteurs." + } } } } diff --git a/homeassistant/components/nut/translations/he.json b/homeassistant/components/nut/translations/he.json index 77c12630351..b3fb785d55e 100644 --- a/homeassistant/components/nut/translations/he.json +++ b/homeassistant/components/nut/translations/he.json @@ -17,11 +17,5 @@ } } } - }, - "options": { - "error": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" - } } } \ No newline at end of file diff --git a/homeassistant/components/nut/translations/hu.json b/homeassistant/components/nut/translations/hu.json index aa8f7c37105..498c12e2fcd 100644 --- a/homeassistant/components/nut/translations/hu.json +++ b/homeassistant/components/nut/translations/hu.json @@ -8,16 +8,9 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { - "resources": { - "data": { - "resources": "Forr\u00e1sok" - }, - "title": "V\u00e1lassza ki a nyomon k\u00f6vetend\u0151 er\u0151forr\u00e1sokat" - }, "ups": { "data": { - "alias": "\u00c1ln\u00e9v", - "resources": "Forr\u00e1sok" + "alias": "\u00c1ln\u00e9v" }, "title": "V\u00e1lassza ki a fel\u00fcgyelni k\u00edv\u00e1nt UPS-t" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" - }, "step": { "init": { "data": { - "resources": "Forr\u00e1sok", "scan_interval": "Szkennel\u00e9si intervallum (m\u00e1sodperc)" - }, - "description": "V\u00e1lassza az \u00c9rz\u00e9kel\u0151 er\u0151forr\u00e1sokat." + } } } } diff --git a/homeassistant/components/nut/translations/id.json b/homeassistant/components/nut/translations/id.json index fc23e34fe8e..709256b2bc2 100644 --- a/homeassistant/components/nut/translations/id.json +++ b/homeassistant/components/nut/translations/id.json @@ -8,16 +8,9 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { - "resources": { - "data": { - "resources": "Sumber Daya" - }, - "title": "Pilih Sumber Daya untuk Dipantau" - }, "ups": { "data": { - "alias": "Alias", - "resources": "Sumber Daya" + "alias": "Alias" }, "title": "Pilih UPS untuk Dipantau" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "Gagal terhubung", - "unknown": "Kesalahan yang tidak diharapkan" - }, "step": { "init": { "data": { - "resources": "Sumber Daya", "scan_interval": "Interval Pindai (detik)" - }, - "description": "Pilih Sumber Daya Sensor." + } } } } diff --git a/homeassistant/components/nut/translations/it.json b/homeassistant/components/nut/translations/it.json index 6b29911b28a..976c675a8b6 100644 --- a/homeassistant/components/nut/translations/it.json +++ b/homeassistant/components/nut/translations/it.json @@ -8,16 +8,9 @@ "unknown": "Errore imprevisto" }, "step": { - "resources": { - "data": { - "resources": "Risorse" - }, - "title": "Scegli le risorse da monitorare" - }, "ups": { "data": { - "alias": "Alias", - "resources": "Risorse" + "alias": "Alias" }, "title": "Scegli l'UPS da monitorare" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "Impossibile connettersi", - "unknown": "Errore imprevisto" - }, "step": { "init": { "data": { - "resources": "Risorse", "scan_interval": "Intervallo di scansione (secondi)" - }, - "description": "Scegli le risorse del sensore." + } } } } diff --git a/homeassistant/components/nut/translations/ja.json b/homeassistant/components/nut/translations/ja.json index 4c52cc74ed2..91f5aafb20d 100644 --- a/homeassistant/components/nut/translations/ja.json +++ b/homeassistant/components/nut/translations/ja.json @@ -8,16 +8,9 @@ "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { - "resources": { - "data": { - "resources": "\u30ea\u30bd\u30fc\u30b9" - }, - "title": "\u76e3\u8996\u3059\u308b\u30ea\u30bd\u30fc\u30b9\u3092\u9078\u629e" - }, "ups": { "data": { - "alias": "\u30a8\u30a4\u30ea\u30a2\u30b9", - "resources": "\u30ea\u30bd\u30fc\u30b9" + "alias": "\u30a8\u30a4\u30ea\u30a2\u30b9" }, "title": "\u76e3\u8996\u3059\u308bUPS\u3092\u9078\u629e" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" - }, "step": { "init": { "data": { - "resources": "\u30ea\u30bd\u30fc\u30b9", "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u79d2)" - }, - "description": "\u30bb\u30f3\u30b5\u30fc\u30ea\u30bd\u30fc\u30b9\u3092\u9078\u629e\u3057\u307e\u3059\u3002" + } } } } diff --git a/homeassistant/components/nut/translations/ko.json b/homeassistant/components/nut/translations/ko.json index a5680f4ed48..89bd39e35b5 100644 --- a/homeassistant/components/nut/translations/ko.json +++ b/homeassistant/components/nut/translations/ko.json @@ -8,16 +8,9 @@ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { - "resources": { - "data": { - "resources": "\ub9ac\uc18c\uc2a4" - }, - "title": "\ubaa8\ub2c8\ud130\ub9c1\ud560 \ub9ac\uc18c\uc2a4 \uc120\ud0dd\ud558\uae30" - }, "ups": { "data": { - "alias": "\ubcc4\uba85", - "resources": "\ub9ac\uc18c\uc2a4" + "alias": "\ubcc4\uba85" }, "title": "\ubaa8\ub2c8\ud130\ub9c1\ud560 UPS \uc120\ud0dd\ud558\uae30" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" - }, "step": { "init": { "data": { - "resources": "\ub9ac\uc18c\uc2a4", "scan_interval": "\uc2a4\uce94 \uac04\uaca9 (\ucd08)" - }, - "description": "\uc13c\uc11c \ub9ac\uc18c\uc2a4\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694." + } } } } diff --git a/homeassistant/components/nut/translations/lb.json b/homeassistant/components/nut/translations/lb.json index da4840653c1..40a44ef19eb 100644 --- a/homeassistant/components/nut/translations/lb.json +++ b/homeassistant/components/nut/translations/lb.json @@ -8,16 +8,9 @@ "unknown": "Onerwaarte Feeler" }, "step": { - "resources": { - "data": { - "resources": "Ressourcen" - }, - "title": "Ressourcen auswielen fir z'iwwerwaachen" - }, "ups": { "data": { - "alias": "Alias", - "resources": "Ressourcen" + "alias": "Alias" }, "title": "UPS fir z'iwwerwaachen auswielen" }, @@ -36,10 +29,8 @@ "step": { "init": { "data": { - "resources": "Ressourcen", "scan_interval": "Scan Intervall (sekonnen)" - }, - "description": "Sensor Ressourcen auswielen" + } } } } diff --git a/homeassistant/components/nut/translations/nl.json b/homeassistant/components/nut/translations/nl.json index d90b75b4bcc..ae8c91f75f9 100644 --- a/homeassistant/components/nut/translations/nl.json +++ b/homeassistant/components/nut/translations/nl.json @@ -8,16 +8,9 @@ "unknown": "Onverwachte fout" }, "step": { - "resources": { - "data": { - "resources": "Bronnen" - }, - "title": "Kies de te controleren bronnen" - }, "ups": { "data": { - "alias": "Alias", - "resources": "Bronnen" + "alias": "Alias" }, "title": "Kies een UPS om uit te lezen" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "Verbinden mislukt", - "unknown": "Onverwachte fout" - }, "step": { "init": { "data": { - "resources": "Bronnen", "scan_interval": "Scaninterval (seconden)" - }, - "description": "Kies Sensorbronnen." + } } } } diff --git a/homeassistant/components/nut/translations/no.json b/homeassistant/components/nut/translations/no.json index 887d3b6d30a..d657b67fd07 100644 --- a/homeassistant/components/nut/translations/no.json +++ b/homeassistant/components/nut/translations/no.json @@ -8,16 +8,9 @@ "unknown": "Uventet feil" }, "step": { - "resources": { - "data": { - "resources": "Ressurser" - }, - "title": "Velg ressurser som skal overv\u00e5kes" - }, "ups": { "data": { - "alias": "", - "resources": "Ressurser" + "alias": "" }, "title": "Velg UPS som skal overv\u00e5kes" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "Tilkobling mislyktes", - "unknown": "Uventet feil" - }, "step": { "init": { "data": { - "resources": "Ressurser", "scan_interval": "Skanneintervall (sekunder)" - }, - "description": "Velg Sensor Ressurser." + } } } } diff --git a/homeassistant/components/nut/translations/pl.json b/homeassistant/components/nut/translations/pl.json index 2686a98e697..3f9d3c89b41 100644 --- a/homeassistant/components/nut/translations/pl.json +++ b/homeassistant/components/nut/translations/pl.json @@ -8,16 +8,9 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { - "resources": { - "data": { - "resources": "Zasoby" - }, - "title": "Wybierz zasoby do monitorowania" - }, "ups": { "data": { - "alias": "Alias", - "resources": "Zasoby" + "alias": "Alias" }, "title": "Wybierz UPS do monitorowania" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "unknown": "Nieoczekiwany b\u0142\u0105d" - }, "step": { "init": { "data": { - "resources": "Zasoby", "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (sekundy)" - }, - "description": "Wybierz zasoby sensor\u00f3w." + } } } } diff --git a/homeassistant/components/nut/translations/pt-BR.json b/homeassistant/components/nut/translations/pt-BR.json index d6be7b41044..ee2c84a7777 100644 --- a/homeassistant/components/nut/translations/pt-BR.json +++ b/homeassistant/components/nut/translations/pt-BR.json @@ -8,16 +8,9 @@ "unknown": "Erro inesperado" }, "step": { - "resources": { - "data": { - "resources": "Recursos" - }, - "title": "Escolha os recursos para monitorar" - }, "ups": { "data": { - "alias": "Alias", - "resources": "Recursos" + "alias": "Alias" }, "title": "Escolha o no-break (UPS) para monitorar" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "Falha ao conectar", - "unknown": "Erro inesperado" - }, "step": { "init": { "data": { - "resources": "Recursos", "scan_interval": "Intervalo de escaneamento (segundos)" - }, - "description": "Escolha os recursos dos sensores." + } } } } diff --git a/homeassistant/components/nut/translations/pt.json b/homeassistant/components/nut/translations/pt.json index f5e8690e383..9f6c04e78a3 100644 --- a/homeassistant/components/nut/translations/pt.json +++ b/homeassistant/components/nut/translations/pt.json @@ -8,11 +8,6 @@ "unknown": "Erro inesperado" }, "step": { - "ups": { - "data": { - "resources": "Recursos" - } - }, "user": { "data": { "host": "Servidor", @@ -22,11 +17,5 @@ } } } - }, - "options": { - "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o", - "unknown": "Erro inesperado" - } } } \ No newline at end of file diff --git a/homeassistant/components/nut/translations/ru.json b/homeassistant/components/nut/translations/ru.json index 071a7d0f09c..230c4d3d869 100644 --- a/homeassistant/components/nut/translations/ru.json +++ b/homeassistant/components/nut/translations/ru.json @@ -8,16 +8,9 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { - "resources": { - "data": { - "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u044b" - }, - "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0435\u0441\u0443\u0440\u0441\u044b \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430" - }, "ups": { "data": { - "alias": "\u041f\u0441\u0435\u0432\u0434\u043e\u043d\u0438\u043c", - "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u044b" + "alias": "\u041f\u0441\u0435\u0432\u0434\u043e\u043d\u0438\u043c" }, "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0418\u0411\u041f \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." - }, "step": { "init": { "data": { - "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u044b", "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0435\u0441\u0443\u0440\u0441\u044b \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432." + } } } } diff --git a/homeassistant/components/nut/translations/sk.json b/homeassistant/components/nut/translations/sk.json index 434ad4a26b2..d00d818716f 100644 --- a/homeassistant/components/nut/translations/sk.json +++ b/homeassistant/components/nut/translations/sk.json @@ -8,16 +8,9 @@ "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { - "resources": { - "data": { - "resources": "Prostriedky" - }, - "title": "Vyberte prostriedky, ktor\u00e9 chcete monitorova\u0165" - }, "ups": { "data": { - "alias": "Alias", - "resources": "Prostriedky" + "alias": "Alias" }, "title": "Vyberte UPS, ktor\u00fa chcete monitorova\u0165" }, @@ -32,17 +25,11 @@ } }, "options": { - "error": { - "cannot_connect": "Nepodarilo sa pripoji\u0165", - "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" - }, "step": { "init": { "data": { - "resources": "Prostriedky", "scan_interval": "Skenovac\u00ed interval (v sekund\u00e1ch)" - }, - "description": "Vyberte senzory." + } } } } diff --git a/homeassistant/components/nut/translations/sl.json b/homeassistant/components/nut/translations/sl.json index 1f0b269562d..156d3dc7d92 100644 --- a/homeassistant/components/nut/translations/sl.json +++ b/homeassistant/components/nut/translations/sl.json @@ -8,16 +8,9 @@ "unknown": "Nepri\u010dakovana napaka" }, "step": { - "resources": { - "data": { - "resources": "Viri" - }, - "title": "Izberite vire za spremljanje" - }, "ups": { "data": { - "alias": "Vzdevek", - "resources": "Viri" + "alias": "Vzdevek" }, "title": "Izberite UPS za spremljanje" }, @@ -36,10 +29,8 @@ "step": { "init": { "data": { - "resources": "Viri", "scan_interval": "Interval skeniranja (sekunde)" - }, - "description": "Izberite vire senzorja." + } } } } diff --git a/homeassistant/components/nut/translations/sv.json b/homeassistant/components/nut/translations/sv.json index 45832197f68..9a3cb690744 100644 --- a/homeassistant/components/nut/translations/sv.json +++ b/homeassistant/components/nut/translations/sv.json @@ -8,12 +8,6 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { - "resources": { - "data": { - "resources": "Resurser" - }, - "title": "V\u00e4lj resurser som ska \u00f6vervakas" - }, "ups": { "data": { "alias": "Alias" @@ -31,10 +25,6 @@ } }, "options": { - "error": { - "cannot_connect": "Kunde inte ansluta", - "unknown": "Ov\u00e4ntat fel" - }, "step": { "init": { "data": { diff --git a/homeassistant/components/nut/translations/tr.json b/homeassistant/components/nut/translations/tr.json index 7802c451fd0..e24f982268c 100644 --- a/homeassistant/components/nut/translations/tr.json +++ b/homeassistant/components/nut/translations/tr.json @@ -8,16 +8,9 @@ "unknown": "Beklenmeyen hata" }, "step": { - "resources": { - "data": { - "resources": "Kaynaklar" - }, - "title": "\u0130zlenecek Kaynaklar\u0131 Se\u00e7in" - }, "ups": { "data": { - "alias": "Takma ad", - "resources": "Kaynaklar" + "alias": "Takma ad" }, "title": "\u0130zlenecek UPS'i Se\u00e7in" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131", - "unknown": "Beklenmeyen hata" - }, "step": { "init": { "data": { - "resources": "Kaynaklar", "scan_interval": "Tarama Aral\u0131\u011f\u0131 (saniye)" - }, - "description": "Sens\u00f6r Kaynaklar\u0131'n\u0131 se\u00e7in." + } } } } diff --git a/homeassistant/components/nut/translations/uk.json b/homeassistant/components/nut/translations/uk.json index b25fe854560..64c8a44d219 100644 --- a/homeassistant/components/nut/translations/uk.json +++ b/homeassistant/components/nut/translations/uk.json @@ -8,16 +8,9 @@ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { - "resources": { - "data": { - "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u0438" - }, - "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0440\u0435\u0441\u0443\u0440\u0441\u0438 \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443" - }, "ups": { "data": { - "alias": "\u041f\u0441\u0435\u0432\u0434\u043e\u043d\u0456\u043c", - "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u0438" + "alias": "\u041f\u0441\u0435\u0432\u0434\u043e\u043d\u0456\u043c" }, "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c UPS \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443" }, @@ -36,10 +29,8 @@ "step": { "init": { "data": { - "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u0438", "scan_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" - }, - "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0440\u0435\u0441\u0443\u0440\u0441\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0456\u0432." + } } } } diff --git a/homeassistant/components/nut/translations/zh-Hans.json b/homeassistant/components/nut/translations/zh-Hans.json index 4afd1ff0031..deb09194b47 100644 --- a/homeassistant/components/nut/translations/zh-Hans.json +++ b/homeassistant/components/nut/translations/zh-Hans.json @@ -8,16 +8,9 @@ "unknown": "\u672a\u77e5\u9519\u8bef" }, "step": { - "resources": { - "data": { - "resources": "\u8d44\u6e90" - }, - "title": "\u9009\u62e9\u8981\u76d1\u89c6\u7684\u8d44\u6e90" - }, "ups": { "data": { - "alias": "\u522b\u540d", - "resources": "\u8d44\u6e90" + "alias": "\u522b\u540d" }, "title": "\u9009\u62e9\u8981\u76d1\u63a7\u7684 UPS" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "\u8fde\u63a5\u5931\u8d25", - "unknown": "\u4e0d\u5728\u9884\u671f\u5185\u7684\u9519\u8bef" - }, "step": { "init": { "data": { - "resources": "\u8d44\u6e90", "scan_interval": "\u626b\u63cf\u95f4\u9694\uff08\u79d2\uff09" - }, - "description": "\u9009\u62e9\u8981\u76d1\u89c6\u7684\u8d44\u6e90" + } } } } diff --git a/homeassistant/components/nut/translations/zh-Hant.json b/homeassistant/components/nut/translations/zh-Hant.json index 3b1daf88bf1..71dc2f78402 100644 --- a/homeassistant/components/nut/translations/zh-Hant.json +++ b/homeassistant/components/nut/translations/zh-Hant.json @@ -8,16 +8,9 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { - "resources": { - "data": { - "resources": "\u8cc7\u6e90" - }, - "title": "\u9078\u64c7\u6240\u8981\u76e3\u8996\u7684\u8cc7\u6e90" - }, "ups": { "data": { - "alias": "\u5225\u540d", - "resources": "\u8cc7\u6e90" + "alias": "\u5225\u540d" }, "title": "\u9078\u64c7\u6240\u8981\u76e3\u8996\u7684 UPS" }, @@ -33,17 +26,11 @@ } }, "options": { - "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" - }, "step": { "init": { "data": { - "resources": "\u8cc7\u6e90", "scan_interval": "\u6383\u63cf\u9593\u8ddd\uff08\u79d2\uff09" - }, - "description": "\u9078\u64c7\u611f\u6e2c\u5668\u8cc7\u6e90\u3002" + } } } } diff --git a/homeassistant/components/onewire/translations/af.json b/homeassistant/components/onewire/translations/af.json deleted file mode 100644 index 779febe67cd..00000000000 --- a/homeassistant/components/onewire/translations/af.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "config": { - "error": { - "invalid_path": "Verzeichnis nicht gefunden." - }, - "step": { - "user": { - "data": { - "type": "Verbindungstyp" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/bg.json b/homeassistant/components/onewire/translations/bg.json index a14f7fa00a2..4893ecc7a5d 100644 --- a/homeassistant/components/onewire/translations/bg.json +++ b/homeassistant/components/onewire/translations/bg.json @@ -4,21 +4,13 @@ "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_path": "\u0414\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u044f\u0442\u0430 \u043d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0430." + "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": { "host": "\u0425\u043e\u0441\u0442", - "port": "\u041f\u043e\u0440\u0442", - "type": "\u0412\u0438\u0434 \u043d\u0430 \u0432\u0440\u044a\u0437\u043a\u0430\u0442\u0430" + "port": "\u041f\u043e\u0440\u0442" } } } diff --git a/homeassistant/components/onewire/translations/ca.json b/homeassistant/components/onewire/translations/ca.json index a8f4da41b6b..ae434bc5be4 100644 --- a/homeassistant/components/onewire/translations/ca.json +++ b/homeassistant/components/onewire/translations/ca.json @@ -4,22 +4,13 @@ "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { - "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_path": "No s'ha trobat el directori." + "cannot_connect": "Ha fallat la connexi\u00f3" }, "step": { - "owserver": { - "data": { - "host": "Amfitri\u00f3", - "port": "Port" - }, - "title": "Defineix els detalls d'owserver" - }, "user": { "data": { "host": "Amfitri\u00f3", - "port": "Port", - "type": "Tipus de connexi\u00f3" + "port": "Port" }, "title": "Detalls del servidor" } @@ -30,10 +21,6 @@ "device_not_selected": "Selecciona els dispositius a configurar" }, "step": { - "ack_no_options": { - "description": "No hi ha opcions per a la implementaci\u00f3 de SysBus", - "title": "Opcions SysBus de OneWire" - }, "configure_device": { "data": { "precision": "Precisi\u00f3 del sensor" diff --git a/homeassistant/components/onewire/translations/cs.json b/homeassistant/components/onewire/translations/cs.json index c5298d095d8..ae22bc1ce25 100644 --- a/homeassistant/components/onewire/translations/cs.json +++ b/homeassistant/components/onewire/translations/cs.json @@ -4,21 +4,10 @@ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" }, "error": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "invalid_path": "Adres\u00e1\u0159 nebyl nalezen." + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { - "owserver": { - "data": { - "host": "Hostitel", - "port": "Port" - }, - "title": "Nastaven\u00ed owserver" - }, "user": { - "data": { - "type": "Typ p\u0159ipojen\u00ed" - }, "title": "Nastaven\u00ed 1-Wire" } } diff --git a/homeassistant/components/onewire/translations/de.json b/homeassistant/components/onewire/translations/de.json index 14f321c8004..feab77f3ec7 100644 --- a/homeassistant/components/onewire/translations/de.json +++ b/homeassistant/components/onewire/translations/de.json @@ -4,22 +4,13 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_path": "Verzeichnis nicht gefunden." + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { - "owserver": { - "data": { - "host": "Host", - "port": "Port" - }, - "title": "owserver-Details einstellen" - }, "user": { "data": { "host": "Host", - "port": "Port", - "type": "Verbindungstyp" + "port": "Port" }, "title": "Serverdetails festlegen" } @@ -30,10 +21,6 @@ "device_not_selected": "Zu konfigurierende Ger\u00e4te ausw\u00e4hlen" }, "step": { - "ack_no_options": { - "description": "Es gibt keine Optionen f\u00fcr die SysBus-Implementierung", - "title": "OneWire SysBus-Optionen" - }, "configure_device": { "data": { "precision": "Sensorgenauigkeit" diff --git a/homeassistant/components/onewire/translations/el.json b/homeassistant/components/onewire/translations/el.json index b0d37d679bc..1f70ecff348 100644 --- a/homeassistant/components/onewire/translations/el.json +++ b/homeassistant/components/onewire/translations/el.json @@ -4,22 +4,13 @@ "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" }, "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "invalid_path": "\u039f \u03ba\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf\u03c2 \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5." + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "step": { - "owserver": { - "data": { - "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", - "port": "\u0398\u03cd\u03c1\u03b1" - }, - "title": "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03b5\u03c1\u03b5\u03b9\u03ce\u03bd owserver" - }, "user": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", - "port": "\u0398\u03cd\u03c1\u03b1", - "type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + "port": "\u0398\u03cd\u03c1\u03b1" }, "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 1-Wire" } @@ -30,10 +21,6 @@ "device_not_selected": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7" }, "step": { - "ack_no_options": { - "description": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c5\u03bb\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 SysBus", - "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 SysBus OneWire" - }, "configure_device": { "data": { "precision": "\u0391\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1" diff --git a/homeassistant/components/onewire/translations/en.json b/homeassistant/components/onewire/translations/en.json index d8e2c157e0f..62b4dddf659 100644 --- a/homeassistant/components/onewire/translations/en.json +++ b/homeassistant/components/onewire/translations/en.json @@ -4,22 +4,13 @@ "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "Failed to connect", - "invalid_path": "Directory not found." + "cannot_connect": "Failed to connect" }, "step": { - "owserver": { - "data": { - "host": "Host", - "port": "Port" - }, - "title": "Set owserver details" - }, "user": { "data": { "host": "Host", - "port": "Port", - "type": "Connection type" + "port": "Port" }, "title": "Set server details" } @@ -30,10 +21,6 @@ "device_not_selected": "Select devices to configure" }, "step": { - "ack_no_options": { - "description": "There are no options for the SysBus implementation", - "title": "OneWire SysBus Options" - }, "configure_device": { "data": { "precision": "Sensor Precision" diff --git a/homeassistant/components/onewire/translations/es.json b/homeassistant/components/onewire/translations/es.json index cb13e1992bd..9fa4912166f 100644 --- a/homeassistant/components/onewire/translations/es.json +++ b/homeassistant/components/onewire/translations/es.json @@ -4,22 +4,13 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "No se pudo conectar", - "invalid_path": "Directorio no encontrado." + "cannot_connect": "No se pudo conectar" }, "step": { - "owserver": { - "data": { - "host": "Host", - "port": "Puerto" - }, - "title": "Configurar los detalles del servidor" - }, "user": { "data": { "host": "Host", - "port": "Puerto", - "type": "Tipo de conexi\u00f3n" + "port": "Puerto" }, "title": "Configurar 1 cable" } @@ -30,9 +21,6 @@ "device_not_selected": "Seleccionar los dispositivos a configurar" }, "step": { - "ack_no_options": { - "title": "Opciones SysBus de OneWire" - }, "configure_device": { "data": { "precision": "Precisi\u00f3n del sensor" diff --git a/homeassistant/components/onewire/translations/et.json b/homeassistant/components/onewire/translations/et.json index 7e57911127e..cc3588be15a 100644 --- a/homeassistant/components/onewire/translations/et.json +++ b/homeassistant/components/onewire/translations/et.json @@ -4,22 +4,13 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud" }, "error": { - "cannot_connect": "\u00dchendus nurjus", - "invalid_path": "Kausta ei leitud." + "cannot_connect": "\u00dchendus nurjus" }, "step": { - "owserver": { - "data": { - "host": "", - "port": "" - }, - "title": "M\u00e4\u00e4ra owserver-i \u00fcksikasjad" - }, "user": { "data": { "host": "Host", - "port": "Port", - "type": "\u00dchenduse t\u00fc\u00fcp" + "port": "Port" }, "title": "M\u00e4\u00e4ra serveri \u00fcksikasjad" } @@ -30,10 +21,6 @@ "device_not_selected": "Vali seadistatav seade" }, "step": { - "ack_no_options": { - "description": "SysBusi rakendamiseks pole v\u00f5imalusi", - "title": "OneWire SysBusi valikud" - }, "configure_device": { "data": { "precision": "Anduri t\u00e4psus" diff --git a/homeassistant/components/onewire/translations/fr.json b/homeassistant/components/onewire/translations/fr.json index 3d9e01916c3..937aa719f3d 100644 --- a/homeassistant/components/onewire/translations/fr.json +++ b/homeassistant/components/onewire/translations/fr.json @@ -4,22 +4,13 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "cannot_connect": "\u00c9chec de connexion", - "invalid_path": "R\u00e9pertoire introuvable." + "cannot_connect": "\u00c9chec de connexion" }, "step": { - "owserver": { - "data": { - "host": "H\u00f4te", - "port": "Port" - }, - "title": "D\u00e9finir les d\u00e9tails d'owserver" - }, "user": { "data": { "host": "H\u00f4te", - "port": "Port", - "type": "Type de connexion" + "port": "Port" }, "title": "Renseigner les informations du serveur" } @@ -30,10 +21,6 @@ "device_not_selected": "S\u00e9lectionnez les appareils \u00e0 configurer" }, "step": { - "ack_no_options": { - "description": "Il n'y a pas d'option pour l'impl\u00e9mentation de SysBus", - "title": "Options de bus syst\u00e8me OneWire" - }, "configure_device": { "data": { "precision": "Pr\u00e9cision du capteur" diff --git a/homeassistant/components/onewire/translations/he.json b/homeassistant/components/onewire/translations/he.json index d83d1f76175..c3a67844fdd 100644 --- a/homeassistant/components/onewire/translations/he.json +++ b/homeassistant/components/onewire/translations/he.json @@ -7,7 +7,7 @@ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, "step": { - "owserver": { + "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", "port": "\u05e4\u05ea\u05d7\u05d4" diff --git a/homeassistant/components/onewire/translations/hu.json b/homeassistant/components/onewire/translations/hu.json index e26721758cd..faae98589e3 100644 --- a/homeassistant/components/onewire/translations/hu.json +++ b/homeassistant/components/onewire/translations/hu.json @@ -4,22 +4,13 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_path": "A k\u00f6nyvt\u00e1r nem tal\u00e1lhat\u00f3." + "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { - "owserver": { - "data": { - "host": "C\u00edm", - "port": "Port" - }, - "title": "Owserver adatok be\u00e1ll\u00edt\u00e1sa" - }, "user": { "data": { "host": "C\u00edm", - "port": "Port", - "type": "Kapcsolat t\u00edpusa" + "port": "Port" }, "title": "A 1-Wire be\u00e1ll\u00edt\u00e1sa" } @@ -34,9 +25,7 @@ "data": { "one": "\u00dcres", "other": "\u00dcres" - }, - "description": "A SysBus implement\u00e1ci\u00f3j\u00e1nak nincsenek opci\u00f3i.", - "title": "OneWire SysBus opci\u00f3k" + } }, "configure_device": { "data": { diff --git a/homeassistant/components/onewire/translations/id.json b/homeassistant/components/onewire/translations/id.json index 203a38d1a4c..46d04ad89f4 100644 --- a/homeassistant/components/onewire/translations/id.json +++ b/homeassistant/components/onewire/translations/id.json @@ -4,22 +4,13 @@ "already_configured": "Perangkat sudah dikonfigurasi" }, "error": { - "cannot_connect": "Gagal terhubung", - "invalid_path": "Direktori tidak ditemukan." + "cannot_connect": "Gagal terhubung" }, "step": { - "owserver": { - "data": { - "host": "Host", - "port": "Port" - }, - "title": "Tetapkan detail owserver" - }, "user": { "data": { "host": "Host", - "port": "Port", - "type": "Jenis koneksi" + "port": "Port" }, "title": "Setel detail server" } @@ -30,10 +21,6 @@ "device_not_selected": "Pilih perangkat untuk dikonfigurasi" }, "step": { - "ack_no_options": { - "description": "Tidak ada opsi untuk implementasi SysBus", - "title": "Opsi OneWire SysBus" - }, "configure_device": { "data": { "precision": "Presisi Sensor" diff --git a/homeassistant/components/onewire/translations/it.json b/homeassistant/components/onewire/translations/it.json index a6bffe1e5fc..9679f7f3e69 100644 --- a/homeassistant/components/onewire/translations/it.json +++ b/homeassistant/components/onewire/translations/it.json @@ -4,22 +4,13 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { - "cannot_connect": "Impossibile connettersi", - "invalid_path": "Cartella non trovata." + "cannot_connect": "Impossibile connettersi" }, "step": { - "owserver": { - "data": { - "host": "Host", - "port": "Porta" - }, - "title": "Imposta i dettagli dell'owserver" - }, "user": { "data": { "host": "Host", - "port": "Porta", - "type": "Tipo di connessione" + "port": "Porta" }, "title": "Imposta i dettagli del server" } @@ -34,9 +25,7 @@ "data": { "one": "Vuoto", "other": "Vuoti" - }, - "description": "Non ci sono opzioni per l'implementazione SysBus", - "title": "Opzioni SysBus OneWire" + } }, "configure_device": { "data": { diff --git a/homeassistant/components/onewire/translations/ja.json b/homeassistant/components/onewire/translations/ja.json index b4acd54cd41..75b01a0cbc9 100644 --- a/homeassistant/components/onewire/translations/ja.json +++ b/homeassistant/components/onewire/translations/ja.json @@ -4,22 +4,13 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "invalid_path": "\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { - "owserver": { - "data": { - "host": "\u30db\u30b9\u30c8", - "port": "\u30dd\u30fc\u30c8" - }, - "title": "owserver\u306e\u8a73\u7d30\u8a2d\u5b9a" - }, "user": { "data": { "host": "\u30db\u30b9\u30c8", - "port": "\u30dd\u30fc\u30c8", - "type": "\u63a5\u7d9a\u30bf\u30a4\u30d7" + "port": "\u30dd\u30fc\u30c8" }, "title": "1-Wire\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } @@ -30,10 +21,6 @@ "device_not_selected": "\u8a2d\u5b9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e" }, "step": { - "ack_no_options": { - "description": "SysBus\u306e\u5b9f\u88c5\u306b\u95a2\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u306f\u3042\u308a\u307e\u305b\u3093", - "title": "OneWire SysBus\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" - }, "configure_device": { "data": { "precision": "\u30bb\u30f3\u30b5\u30fc\u306e\u7cbe\u5ea6" diff --git a/homeassistant/components/onewire/translations/ka.json b/homeassistant/components/onewire/translations/ka.json index 1b3c7e8ef5c..be486ef2f62 100644 --- a/homeassistant/components/onewire/translations/ka.json +++ b/homeassistant/components/onewire/translations/ka.json @@ -4,21 +4,10 @@ "already_configured": "\u10db\u10dd\u10ec\u10e7\u10dd\u10d1\u10d8\u10da\u10dd\u10d1\u10d0 \u10e3\u10d9\u10d5\u10d4 \u10d3\u10d0\u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10e3\u10da\u10d8\u10d0" }, "error": { - "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0", - "invalid_path": "\u10d3\u10d8\u10e0\u10d4\u10e5\u10e2\u10dd\u10e0\u10d8\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10d8\u10eb\u10d4\u10d1\u10dc\u10d0." + "cannot_connect": "\u10d3\u10d0\u10d9\u10d0\u10d5\u10e8\u10e0\u10d4\u10d1\u10d0 \u10d5\u10d4\u10e0 \u10db\u10dd\u10ee\u10d4\u10e0\u10ee\u10d3\u10d0" }, "step": { - "owserver": { - "data": { - "host": "\u10f0\u10dd\u10e1\u10e2\u10d8", - "port": "\u10de\u10dd\u10e0\u10e2\u10d8" - }, - "title": "owserver \u10e1\u10d4\u10e0\u10d5\u10d4\u10e0\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0" - }, "user": { - "data": { - "type": "\u1c99\u10d0\u10d5\u10e8\u10d8\u10e0\u10d8\u10e1 \u10e2\u10d8\u10de\u10d8" - }, "title": "1-\u10db\u10d0\u10d5\u10d7\u10d8\u10da\u10d8\u10e1 \u10d9\u10dd\u10dc\u10e4\u10d8\u10d2\u10e3\u10e0\u10d8\u10e0\u10d4\u10d1\u10d0" } } diff --git a/homeassistant/components/onewire/translations/ko.json b/homeassistant/components/onewire/translations/ko.json index e6665d83768..eeaa8f4c09d 100644 --- a/homeassistant/components/onewire/translations/ko.json +++ b/homeassistant/components/onewire/translations/ko.json @@ -4,22 +4,13 @@ "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_path": "\ub514\ub809\ud130\ub9ac\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { - "owserver": { - "data": { - "host": "\ud638\uc2a4\ud2b8", - "port": "\ud3ec\ud2b8" - }, - "title": "owserver \uc138\ubd80 \uc815\ubcf4 \uc124\uc815\ud558\uae30" - }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8", - "port": "\ud3ec\ud2b8", - "type": "\uc5f0\uacb0 \uc720\ud615" + "port": "\ud3ec\ud2b8" }, "title": "1-Wire \uc124\uc815\ud558\uae30" } diff --git a/homeassistant/components/onewire/translations/lb.json b/homeassistant/components/onewire/translations/lb.json index c7c302ab683..c403b09531a 100644 --- a/homeassistant/components/onewire/translations/lb.json +++ b/homeassistant/components/onewire/translations/lb.json @@ -4,21 +4,7 @@ "already_configured": "Apparat ass scho konfigur\u00e9iert" }, "error": { - "cannot_connect": "Feeler beim verbannen", - "invalid_path": "Dossier net fonnt" - }, - "step": { - "owserver": { - "data": { - "host": "Host", - "port": "Port" - } - }, - "user": { - "data": { - "type": "Typ vun der Verbindung" - } - } + "cannot_connect": "Feeler beim verbannen" } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/nl.json b/homeassistant/components/onewire/translations/nl.json index e6af2cebef3..652e5b17fd5 100644 --- a/homeassistant/components/onewire/translations/nl.json +++ b/homeassistant/components/onewire/translations/nl.json @@ -4,22 +4,13 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kan geen verbinding maken", - "invalid_path": "Directory niet gevonden." + "cannot_connect": "Kan geen verbinding maken" }, "step": { - "owserver": { - "data": { - "host": "Host", - "port": "Poort" - }, - "title": "Owserver-details instellen" - }, "user": { "data": { "host": "Host", - "port": "Poort", - "type": "Verbindingstype" + "port": "Poort" }, "title": "Serverdetails instellen" } @@ -34,9 +25,7 @@ "data": { "one": "Leeg", "other": "Ander" - }, - "description": "Er zijn geen opties voor de SysBus implementatie", - "title": "OneWire SysBus opties" + } }, "configure_device": { "data": { diff --git a/homeassistant/components/onewire/translations/no.json b/homeassistant/components/onewire/translations/no.json index 89529aa7810..13b3df80fde 100644 --- a/homeassistant/components/onewire/translations/no.json +++ b/homeassistant/components/onewire/translations/no.json @@ -4,22 +4,13 @@ "already_configured": "Enheten er allerede konfigurert" }, "error": { - "cannot_connect": "Tilkobling mislyktes", - "invalid_path": "Finner ikke mappen" + "cannot_connect": "Tilkobling mislyktes" }, "step": { - "owserver": { - "data": { - "host": "Vert", - "port": "Port" - }, - "title": "Angi detaljer om owserver" - }, "user": { "data": { "host": "Vert", - "port": "Port", - "type": "Tilkoblingstype" + "port": "Port" }, "title": "Angi serverdetaljer" } @@ -30,10 +21,6 @@ "device_not_selected": "Velg enheter som skal konfigureres" }, "step": { - "ack_no_options": { - "description": "Det er ingen alternativer for SysBus-implementeringen", - "title": "OneWire SysBus-alternativer" - }, "configure_device": { "data": { "precision": "Sensorpresisjon" diff --git a/homeassistant/components/onewire/translations/pl.json b/homeassistant/components/onewire/translations/pl.json index 743a1b28522..523afbd98d9 100644 --- a/homeassistant/components/onewire/translations/pl.json +++ b/homeassistant/components/onewire/translations/pl.json @@ -4,22 +4,13 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_path": "Nie znaleziono katalogu" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { - "owserver": { - "data": { - "host": "Nazwa hosta lub adres IP", - "port": "Port" - }, - "title": "Ustawienia owserver" - }, "user": { "data": { "host": "Nazwa hosta lub adres IP", - "port": "Port", - "type": "Rodzaj po\u0142\u0105czenia" + "port": "Port" }, "title": "Konfiguracja serwera" } @@ -30,10 +21,6 @@ "device_not_selected": "Wybierz urz\u0105dzenia do skonfigurowania" }, "step": { - "ack_no_options": { - "description": "Nie ma opcji dla implementacji magistrali SysBus", - "title": "Opcje magistrali OneWire SysBus" - }, "configure_device": { "data": { "precision": "Dok\u0142adno\u015b\u0107 sensora" diff --git a/homeassistant/components/onewire/translations/pt-BR.json b/homeassistant/components/onewire/translations/pt-BR.json index 74fe2918222..303d36f3cb2 100644 --- a/homeassistant/components/onewire/translations/pt-BR.json +++ b/homeassistant/components/onewire/translations/pt-BR.json @@ -4,22 +4,13 @@ "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falha ao conectar", - "invalid_path": "Diret\u00f3rio n\u00e3o encontrado." + "cannot_connect": "Falha ao conectar" }, "step": { - "owserver": { - "data": { - "host": "Nome do host", - "port": "Porta" - }, - "title": "Definir detalhes do servidor" - }, "user": { "data": { "host": "Host", - "port": "Porta", - "type": "Tipo de conex\u00e3o" + "port": "Porta" }, "title": "Definir detalhes do servidor" } @@ -30,10 +21,6 @@ "device_not_selected": "Selecione os dispositivos para configurar" }, "step": { - "ack_no_options": { - "description": "N\u00e3o h\u00e1 op\u00e7\u00f5es para a implementa\u00e7\u00e3o do SysBus", - "title": "Op\u00e7\u00f5es de OneWire SysBus" - }, "configure_device": { "data": { "precision": "Precis\u00e3o do Sensor" diff --git a/homeassistant/components/onewire/translations/pt.json b/homeassistant/components/onewire/translations/pt.json index bd1e14729e0..db0e0c2a137 100644 --- a/homeassistant/components/onewire/translations/pt.json +++ b/homeassistant/components/onewire/translations/pt.json @@ -5,14 +5,6 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" - }, - "step": { - "owserver": { - "data": { - "host": "Servidor", - "port": "Porta" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/ru.json b/homeassistant/components/onewire/translations/ru.json index d0ab8b8a282..300deadb447 100644 --- a/homeassistant/components/onewire/translations/ru.json +++ b/homeassistant/components/onewire/translations/ru.json @@ -4,22 +4,13 @@ "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_path": "\u041a\u0430\u0442\u0430\u043b\u043e\u0433 \u043d\u0435 \u043d\u0430\u0439\u0434\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." }, "step": { - "owserver": { - "data": { - "host": "\u0425\u043e\u0441\u0442", - "port": "\u041f\u043e\u0440\u0442" - }, - "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a owserver" - }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", - "port": "\u041f\u043e\u0440\u0442", - "type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + "port": "\u041f\u043e\u0440\u0442" }, "title": "1-Wire" } @@ -30,10 +21,6 @@ "device_not_selected": "\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" }, "step": { - "ack_no_options": { - "description": "\u0414\u043b\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0447\u0435\u0440\u0435\u0437 SysBus \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u043a\u0430\u043a\u0438\u0435-\u043b\u0438\u0431\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 OneWire SysBus" - }, "configure_device": { "data": { "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0430" diff --git a/homeassistant/components/onewire/translations/sk.json b/homeassistant/components/onewire/translations/sk.json index bd43098e555..8b2a2f7343b 100644 --- a/homeassistant/components/onewire/translations/sk.json +++ b/homeassistant/components/onewire/translations/sk.json @@ -4,20 +4,10 @@ "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" }, "error": { - "cannot_connect": "Nepodarilo sa pripoji\u0165", - "invalid_path": "Adres\u00e1r sa nena\u0161iel." + "cannot_connect": "Nepodarilo sa pripoji\u0165" }, "step": { - "owserver": { - "data": { - "port": "Port" - }, - "title": "Nastavenie owserver" - }, "user": { - "data": { - "type": "Typ pripojenia" - }, "title": "Nastavenie 1-Wire" } } diff --git a/homeassistant/components/onewire/translations/sl.json b/homeassistant/components/onewire/translations/sl.json index 7011c57c099..8797272c16c 100644 --- a/homeassistant/components/onewire/translations/sl.json +++ b/homeassistant/components/onewire/translations/sl.json @@ -2,9 +2,6 @@ "config": { "step": { "user": { - "data": { - "type": "Vrsta povezave" - }, "title": "Nastavite 1-Wire" } } diff --git a/homeassistant/components/onewire/translations/sv.json b/homeassistant/components/onewire/translations/sv.json index 3265a54aeaf..1b100c60d97 100644 --- a/homeassistant/components/onewire/translations/sv.json +++ b/homeassistant/components/onewire/translations/sv.json @@ -1,13 +1,4 @@ { - "config": { - "step": { - "owserver": { - "data": { - "port": "Port" - } - } - } - }, "options": { "step": { "device_selection": { diff --git a/homeassistant/components/onewire/translations/tr.json b/homeassistant/components/onewire/translations/tr.json index 4859b5ec865..5c7257319c8 100644 --- a/homeassistant/components/onewire/translations/tr.json +++ b/homeassistant/components/onewire/translations/tr.json @@ -4,22 +4,13 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131", - "invalid_path": "Dizin bulunamad\u0131." + "cannot_connect": "Ba\u011flanma hatas\u0131" }, "step": { - "owserver": { - "data": { - "host": "Sunucu", - "port": "Port" - }, - "title": "Sunucu ayr\u0131nt\u0131lar\u0131n\u0131 ayarla" - }, "user": { "data": { "host": "Sunucu", - "port": "Port", - "type": "Ba\u011flant\u0131 t\u00fcr\u00fc" + "port": "Port" }, "title": "Sunucu ayr\u0131nt\u0131lar\u0131n\u0131 ayarla" } @@ -34,9 +25,7 @@ "data": { "one": "Bo\u015f", "other": "Bo\u015f" - }, - "description": "SysBus uygulamas\u0131 i\u00e7in se\u00e7enek yok", - "title": "OneWire SysBus Se\u00e7enekleri" + } }, "configure_device": { "data": { diff --git a/homeassistant/components/onewire/translations/uk.json b/homeassistant/components/onewire/translations/uk.json index 9c9705d2993..879ba4c4414 100644 --- a/homeassistant/components/onewire/translations/uk.json +++ b/homeassistant/components/onewire/translations/uk.json @@ -4,21 +4,10 @@ "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." }, "error": { - "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", - "invalid_path": "\u041a\u0430\u0442\u0430\u043b\u043e\u0433 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e." + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" }, "step": { - "owserver": { - "data": { - "host": "\u0425\u043e\u0441\u0442", - "port": "\u041f\u043e\u0440\u0442" - }, - "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e owserver" - }, "user": { - "data": { - "type": "\u0422\u0438\u043f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" - }, "title": "1-Wire" } } diff --git a/homeassistant/components/onewire/translations/zh-Hant.json b/homeassistant/components/onewire/translations/zh-Hant.json index a77b86ea416..aa98353ca5a 100644 --- a/homeassistant/components/onewire/translations/zh-Hant.json +++ b/homeassistant/components/onewire/translations/zh-Hant.json @@ -4,22 +4,13 @@ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_path": "\u672a\u627e\u5230\u88dd\u7f6e\u3002" + "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "step": { - "owserver": { - "data": { - "host": "\u4e3b\u6a5f\u7aef", - "port": "\u901a\u8a0a\u57e0" - }, - "title": "\u8a2d\u5b9a owserver \u8a73\u7d30\u8cc7\u6599" - }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", - "port": "\u901a\u8a0a\u57e0", - "type": "\u9023\u7dda\u985e\u5225" + "port": "\u901a\u8a0a\u57e0" }, "title": "\u8a2d\u5b9a\u4f3a\u670d\u5668\u8a73\u7d30\u8cc7\u8a0a" } @@ -30,10 +21,6 @@ "device_not_selected": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" }, "step": { - "ack_no_options": { - "description": "SysBus implementation \u6c92\u6709\u8a2d\u5b9a\u9078\u9805", - "title": "OneWire SysBus \u9078\u9805" - }, "configure_device": { "data": { "precision": "\u611f\u6e2c\u5668\u7cbe\u6e96\u5ea6" diff --git a/homeassistant/components/onvif/translations/bg.json b/homeassistant/components/onvif/translations/bg.json index e45e78b79ee..ba0e0dd6277 100644 --- a/homeassistant/components/onvif/translations/bg.json +++ b/homeassistant/components/onvif/translations/bg.json @@ -4,12 +4,6 @@ "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": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" - } - }, "configure": { "data": { "host": "\u0425\u043e\u0441\u0442", @@ -18,13 +12,6 @@ "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": { - "host": "\u0425\u043e\u0441\u0442", - "name": "\u0418\u043c\u0435", - "port": "\u041f\u043e\u0440\u0442" - } } } } diff --git a/homeassistant/components/onvif/translations/ca.json b/homeassistant/components/onvif/translations/ca.json index 7ede84d4845..222921a0599 100644 --- a/homeassistant/components/onvif/translations/ca.json +++ b/homeassistant/components/onvif/translations/ca.json @@ -11,13 +11,6 @@ "cannot_connect": "Ha fallat la connexi\u00f3" }, "step": { - "auth": { - "data": { - "password": "Contrasenya", - "username": "Nom d'usuari" - }, - "title": "Configuraci\u00f3 d'autenticaci\u00f3" - }, "configure": { "data": { "host": "Amfitri\u00f3", @@ -41,14 +34,6 @@ }, "title": "Selecci\u00f3 de dispositiu ONVIF" }, - "manual_input": { - "data": { - "host": "Amfitri\u00f3", - "name": "Nom", - "port": "Port" - }, - "title": "Configura el dispositiu ONVIF" - }, "user": { "data": { "auto": "Cerca autom\u00e0ticament" diff --git a/homeassistant/components/onvif/translations/cs.json b/homeassistant/components/onvif/translations/cs.json index 4ddb2091cc3..100c4eb3788 100644 --- a/homeassistant/components/onvif/translations/cs.json +++ b/homeassistant/components/onvif/translations/cs.json @@ -11,13 +11,6 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { - "auth": { - "data": { - "password": "Heslo", - "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - }, - "title": "Konfigurace ov\u011b\u0159ov\u00e1n\u00ed" - }, "configure": { "data": { "host": "Hostitel", @@ -40,14 +33,6 @@ }, "title": "Vyberte za\u0159\u00edzen\u00ed ONVIF" }, - "manual_input": { - "data": { - "host": "Hostitel", - "name": "Jm\u00e9no", - "port": "Port" - }, - "title": "Konfigurovat za\u0159\u00edzen\u00ed ONVIF" - }, "user": { "description": "Kliknut\u00edm na tla\u010d\u00edtko Odeslat vyhled\u00e1me ve va\u0161\u00ed s\u00edti za\u0159\u00edzen\u00ed ONVIF, kter\u00e1 podporuj\u00ed profil S. \n\nN\u011bkte\u0159\u00ed v\u00fdrobci vypli funkci ONVIF v z\u00e1kladn\u00edm nastaven\u00ed. Ujist\u011bte se, \u017ee je v konfiguraci kamery povolena funkce ONVIF.", "title": "Nastaven\u00ed za\u0159\u00edzen\u00ed ONVIF" diff --git a/homeassistant/components/onvif/translations/de.json b/homeassistant/components/onvif/translations/de.json index fd992c4db05..c4c745f1766 100644 --- a/homeassistant/components/onvif/translations/de.json +++ b/homeassistant/components/onvif/translations/de.json @@ -11,13 +11,6 @@ "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { - "auth": { - "data": { - "password": "Passwort", - "username": "Benutzername" - }, - "title": "Konfiguriere die Authentifizierung" - }, "configure": { "data": { "host": "Host", @@ -41,14 +34,6 @@ }, "title": "W\u00e4hle ein ONVIF-Ger\u00e4t" }, - "manual_input": { - "data": { - "host": "Host", - "name": "Name", - "port": "Port" - }, - "title": "Konfiguriere das ONVIF-Ger\u00e4t" - }, "user": { "data": { "auto": "Automatisch suchen" diff --git a/homeassistant/components/onvif/translations/el.json b/homeassistant/components/onvif/translations/el.json index 8bc93c4a3d3..911994c7344 100644 --- a/homeassistant/components/onvif/translations/el.json +++ b/homeassistant/components/onvif/translations/el.json @@ -11,13 +11,6 @@ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "step": { - "auth": { - "data": { - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" - }, - "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" - }, "configure": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", @@ -41,14 +34,6 @@ }, "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae ONVIF" }, - "manual_input": { - "data": { - "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1", - "port": "\u0398\u03cd\u03c1\u03b1" - }, - "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 ONVIF" - }, "user": { "data": { "auto": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7" diff --git a/homeassistant/components/onvif/translations/en.json b/homeassistant/components/onvif/translations/en.json index c922fc18482..c3b328646ee 100644 --- a/homeassistant/components/onvif/translations/en.json +++ b/homeassistant/components/onvif/translations/en.json @@ -11,13 +11,6 @@ "cannot_connect": "Failed to connect" }, "step": { - "auth": { - "data": { - "password": "Password", - "username": "Username" - }, - "title": "Configure authentication" - }, "configure": { "data": { "host": "Host", @@ -41,14 +34,6 @@ }, "title": "Select ONVIF device" }, - "manual_input": { - "data": { - "host": "Host", - "name": "Name", - "port": "Port" - }, - "title": "Configure ONVIF device" - }, "user": { "data": { "auto": "Search automatically" diff --git a/homeassistant/components/onvif/translations/es-419.json b/homeassistant/components/onvif/translations/es-419.json index d0db75beb96..55ff3166c66 100644 --- a/homeassistant/components/onvif/translations/es-419.json +++ b/homeassistant/components/onvif/translations/es-419.json @@ -8,13 +8,6 @@ "onvif_error": "Error al configurar el dispositivo ONVIF. Consulte los registros para obtener m\u00e1s informaci\u00f3n." }, "step": { - "auth": { - "data": { - "password": "Contrase\u00f1a", - "username": "Nombre de usuario" - }, - "title": "Configurar autenticaci\u00f3n" - }, "configure_profile": { "data": { "include": "Crear entidad de c\u00e1mara" @@ -28,13 +21,6 @@ }, "title": "Seleccionar dispositivo ONVIF" }, - "manual_input": { - "data": { - "host": "Host", - "port": "Puerto" - }, - "title": "Configurar dispositivo ONVIF" - }, "user": { "description": "Al hacer clic en enviar, buscaremos en su red dispositivos ONVIF que admitan Profile S. \n\nAlgunos fabricantes han comenzado a deshabilitar ONVIF por defecto. Aseg\u00farese de que ONVIF est\u00e9 habilitado en la configuraci\u00f3n de su c\u00e1mara.", "title": "Configuraci\u00f3n del dispositivo ONVIF" diff --git a/homeassistant/components/onvif/translations/es.json b/homeassistant/components/onvif/translations/es.json index d5c9688b875..6ba7dbe0ee8 100644 --- a/homeassistant/components/onvif/translations/es.json +++ b/homeassistant/components/onvif/translations/es.json @@ -11,13 +11,6 @@ "cannot_connect": "No se pudo conectar" }, "step": { - "auth": { - "data": { - "password": "Contrase\u00f1a", - "username": "Usuario" - }, - "title": "Configurar la autenticaci\u00f3n" - }, "configure": { "data": { "host": "Host", @@ -41,14 +34,6 @@ }, "title": "Seleccione el dispositivo ONVIF" }, - "manual_input": { - "data": { - "host": "Host", - "name": "Nombre", - "port": "Puerto" - }, - "title": "Configurar el dispositivo ONVIF" - }, "user": { "data": { "auto": "Buscar autom\u00e1ticamente" diff --git a/homeassistant/components/onvif/translations/et.json b/homeassistant/components/onvif/translations/et.json index 61eefa84d12..1d7bede6657 100644 --- a/homeassistant/components/onvif/translations/et.json +++ b/homeassistant/components/onvif/translations/et.json @@ -11,13 +11,6 @@ "cannot_connect": "\u00dchendamine nurjus" }, "step": { - "auth": { - "data": { - "password": "Salas\u00f5na", - "username": "Kasutajanimi" - }, - "title": "Autentimise seadistamine" - }, "configure": { "data": { "host": "Host", @@ -41,14 +34,6 @@ }, "title": "Vali ONVIF-seade" }, - "manual_input": { - "data": { - "host": "", - "name": "Nimi", - "port": "" - }, - "title": "H\u00e4\u00e4lesta ONVIF-seade" - }, "user": { "data": { "auto": "Otsi automaatselt" diff --git a/homeassistant/components/onvif/translations/fi.json b/homeassistant/components/onvif/translations/fi.json index 47673eeaaa5..d32021cd4b5 100644 --- a/homeassistant/components/onvif/translations/fi.json +++ b/homeassistant/components/onvif/translations/fi.json @@ -1,24 +1,11 @@ { "config": { "step": { - "auth": { - "data": { - "password": "Salasana", - "username": "K\u00e4ytt\u00e4j\u00e4tunnus" - }, - "title": "M\u00e4\u00e4rit\u00e4 todennus" - }, "configure_profile": { "data": { "include": "Luo kamerakohde" }, "title": "M\u00e4\u00e4rit\u00e4 profiilit" - }, - "manual_input": { - "data": { - "host": "Palvelin", - "port": "Portti" - } } } } diff --git a/homeassistant/components/onvif/translations/fr.json b/homeassistant/components/onvif/translations/fr.json index ab772fcec2d..6939205b4bb 100644 --- a/homeassistant/components/onvif/translations/fr.json +++ b/homeassistant/components/onvif/translations/fr.json @@ -11,13 +11,6 @@ "cannot_connect": "\u00c9chec de connexion" }, "step": { - "auth": { - "data": { - "password": "Mot de passe", - "username": "Nom d'utilisateur" - }, - "title": "Configurer l'authentification" - }, "configure": { "data": { "host": "H\u00f4te", @@ -41,14 +34,6 @@ }, "title": "S\u00e9lectionnez l'appareil ONVIF" }, - "manual_input": { - "data": { - "host": "H\u00f4te", - "name": "Nom", - "port": "Port" - }, - "title": "Configurer l\u2019appareil ONVIF" - }, "user": { "data": { "auto": "Rechercher automatiquement" diff --git a/homeassistant/components/onvif/translations/he.json b/homeassistant/components/onvif/translations/he.json index 5883d1afc53..727457fac34 100644 --- a/homeassistant/components/onvif/translations/he.json +++ b/homeassistant/components/onvif/translations/he.json @@ -11,13 +11,6 @@ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, "step": { - "auth": { - "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": { "host": "\u05de\u05d0\u05e8\u05d7", @@ -41,14 +34,6 @@ }, "title": "\u05d1\u05d7\u05e8 \u05d4\u05ea\u05e7\u05df ONVIF" }, - "manual_input": { - "data": { - "host": "\u05de\u05d0\u05e8\u05d7", - "name": "\u05e9\u05dd", - "port": "\u05e4\u05d5\u05e8\u05d8" - }, - "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" diff --git a/homeassistant/components/onvif/translations/hu.json b/homeassistant/components/onvif/translations/hu.json index 3754243a740..4fcb62c26fc 100644 --- a/homeassistant/components/onvif/translations/hu.json +++ b/homeassistant/components/onvif/translations/hu.json @@ -11,13 +11,6 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { - "auth": { - "data": { - "password": "Jelsz\u00f3", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - }, - "title": "Hiteles\u00edt\u00e9s konfigur\u00e1l\u00e1sa" - }, "configure": { "data": { "host": "C\u00edm", @@ -41,14 +34,6 @@ }, "title": "ONVIF eszk\u00f6z kiv\u00e1laszt\u00e1sa" }, - "manual_input": { - "data": { - "host": "C\u00edm", - "name": "Elnevez\u00e9s", - "port": "Port" - }, - "title": "ONVIF eszk\u00f6z konfigur\u00e1l\u00e1sa" - }, "user": { "data": { "auto": "Automatikus keres\u00e9s" diff --git a/homeassistant/components/onvif/translations/id.json b/homeassistant/components/onvif/translations/id.json index 77e1353270f..383287db875 100644 --- a/homeassistant/components/onvif/translations/id.json +++ b/homeassistant/components/onvif/translations/id.json @@ -11,13 +11,6 @@ "cannot_connect": "Gagal terhubung" }, "step": { - "auth": { - "data": { - "password": "Kata Sandi", - "username": "Nama Pengguna" - }, - "title": "Konfigurasikan autentikasi" - }, "configure": { "data": { "host": "Host", @@ -41,14 +34,6 @@ }, "title": "Pilih perangkat ONVIF" }, - "manual_input": { - "data": { - "host": "Host", - "name": "Nama", - "port": "Port" - }, - "title": "Konfigurasikan perangkat ONVIF" - }, "user": { "data": { "auto": "Cari secara otomatis" diff --git a/homeassistant/components/onvif/translations/it.json b/homeassistant/components/onvif/translations/it.json index 3590025a6ad..34d23e2f3fe 100644 --- a/homeassistant/components/onvif/translations/it.json +++ b/homeassistant/components/onvif/translations/it.json @@ -11,13 +11,6 @@ "cannot_connect": "Impossibile connettersi" }, "step": { - "auth": { - "data": { - "password": "Password", - "username": "Nome utente" - }, - "title": "Configura l'autenticazione" - }, "configure": { "data": { "host": "Host", @@ -41,14 +34,6 @@ }, "title": "Seleziona il dispositivo ONVIF" }, - "manual_input": { - "data": { - "host": "Host", - "name": "Nome", - "port": "Porta" - }, - "title": "Configura il dispositivo ONVIF" - }, "user": { "data": { "auto": "Cerca automaticamente" diff --git a/homeassistant/components/onvif/translations/ja.json b/homeassistant/components/onvif/translations/ja.json index a2d863b54fa..ed8e3aa6bd5 100644 --- a/homeassistant/components/onvif/translations/ja.json +++ b/homeassistant/components/onvif/translations/ja.json @@ -11,13 +11,6 @@ "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { - "auth": { - "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - }, - "title": "\u8a8d\u8a3c\u306e\u8a2d\u5b9a" - }, "configure": { "data": { "host": "\u30db\u30b9\u30c8", @@ -41,14 +34,6 @@ }, "title": "ONVIF\u30c7\u30d0\u30a4\u30b9\u306e\u9078\u629e" }, - "manual_input": { - "data": { - "host": "\u30db\u30b9\u30c8", - "name": "\u540d\u524d", - "port": "\u30dd\u30fc\u30c8" - }, - "title": "ONVIF\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a" - }, "user": { "data": { "auto": "\u81ea\u52d5\u7684\u306b\u691c\u7d22" diff --git a/homeassistant/components/onvif/translations/ko.json b/homeassistant/components/onvif/translations/ko.json index 173cdb88512..5dcbd1df983 100644 --- a/homeassistant/components/onvif/translations/ko.json +++ b/homeassistant/components/onvif/translations/ko.json @@ -11,13 +11,6 @@ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { - "auth": { - "data": { - "password": "\ube44\ubc00\ubc88\ud638", - "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - }, - "title": "\uc778\uc99d \uad6c\uc131\ud558\uae30" - }, "configure_profile": { "data": { "include": "\uce74\uba54\ub77c \uad6c\uc131\uc694\uc18c \ub9cc\ub4e4\uae30" @@ -31,14 +24,6 @@ }, "title": "ONVIF \uae30\uae30 \uc120\ud0dd\ud558\uae30" }, - "manual_input": { - "data": { - "host": "\ud638\uc2a4\ud2b8", - "name": "\uc774\ub984", - "port": "\ud3ec\ud2b8" - }, - "title": "ONVIF \uae30\uae30 \uad6c\uc131\ud558\uae30" - }, "user": { "description": "\ud655\uc778\uc744 \ud074\ub9ad\ud558\uba74 \ud504\ub85c\ud544 S \ub97c \uc9c0\uc6d0\ud558\ub294 ONVIF \uae30\uae30\ub97c \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uac80\uc0c9\ud569\ub2c8\ub2e4. \n\n\uc77c\ubd80 \uc81c\uc870\uc5c5\uccb4\ub294 \uae30\ubcf8\uac12\uc73c\ub85c ONVIF \ub97c \ube44\ud65c\uc131\ud654 \ud574 \ub193\uc740 \uacbd\uc6b0\uac00 \uc788\uc2b5\ub2c8\ub2e4. \uce74\uba54\ub77c \uad6c\uc131\uc5d0\uc11c ONVIF \uac00 \ud65c\uc131\ud654\ub418\uc5b4 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", "title": "ONVIF \uae30\uae30 \uc124\uc815\ud558\uae30" diff --git a/homeassistant/components/onvif/translations/lb.json b/homeassistant/components/onvif/translations/lb.json index f9b2843b5cb..008b52b5338 100644 --- a/homeassistant/components/onvif/translations/lb.json +++ b/homeassistant/components/onvif/translations/lb.json @@ -11,13 +11,6 @@ "cannot_connect": "Feeler beim verbannen" }, "step": { - "auth": { - "data": { - "password": "Passwuert", - "username": "Benotzernumm" - }, - "title": "Authentifikatioun konfigur\u00e9ieren" - }, "configure_profile": { "data": { "include": "Kamera Entit\u00e9it erstellen" @@ -31,14 +24,6 @@ }, "title": "ONVIF Apparat auswielen" }, - "manual_input": { - "data": { - "host": "Apparat", - "name": "Numm", - "port": "Port" - }, - "title": "ONVIF Apparat ariichten" - }, "user": { "description": "Andeems du op ofsch\u00e9cke klicks, g\u00ebtt dain Netzwierk fir ONVIF Apparater duerchsicht d\u00e9i den Profile S. \u00ebnnerst\u00ebtzen.\n\nVerschidde Hiersteller hunn ugefaang ONVIF standardm\u00e9isseg ze d\u00e9aktiv\u00e9ieren. Stell s\u00e9cher dass ONVIF an denger Kamera ugeschalt ass.", "title": "ONVIF Apparat ariichten" diff --git a/homeassistant/components/onvif/translations/nl.json b/homeassistant/components/onvif/translations/nl.json index c48d2764672..f1101f759e9 100644 --- a/homeassistant/components/onvif/translations/nl.json +++ b/homeassistant/components/onvif/translations/nl.json @@ -11,13 +11,6 @@ "cannot_connect": "Kan geen verbinding maken" }, "step": { - "auth": { - "data": { - "password": "Wachtwoord", - "username": "Gebruikersnaam" - }, - "title": "Configureer authenticatie" - }, "configure": { "data": { "host": "Host", @@ -41,14 +34,6 @@ }, "title": "Selecteer ONVIF-apparaat" }, - "manual_input": { - "data": { - "host": "Host", - "name": "Naam", - "port": "Poort" - }, - "title": "Configureer ONVIF-apparaat" - }, "user": { "data": { "auto": "Automatisch zoeken" diff --git a/homeassistant/components/onvif/translations/no.json b/homeassistant/components/onvif/translations/no.json index 323f9aba5fe..a9087ba6be4 100644 --- a/homeassistant/components/onvif/translations/no.json +++ b/homeassistant/components/onvif/translations/no.json @@ -11,13 +11,6 @@ "cannot_connect": "Tilkobling mislyktes" }, "step": { - "auth": { - "data": { - "password": "Passord", - "username": "Brukernavn" - }, - "title": "Konfigurere godkjenning" - }, "configure": { "data": { "host": "Vert", @@ -41,14 +34,6 @@ }, "title": "Velg ONVIF-enhet" }, - "manual_input": { - "data": { - "host": "Vert", - "name": "Navn", - "port": "Port" - }, - "title": "Konfigurere ONVIF-enhet" - }, "user": { "data": { "auto": "S\u00f8k automatisk" diff --git a/homeassistant/components/onvif/translations/pl.json b/homeassistant/components/onvif/translations/pl.json index f45ad4064b2..2300f6d6041 100644 --- a/homeassistant/components/onvif/translations/pl.json +++ b/homeassistant/components/onvif/translations/pl.json @@ -11,13 +11,6 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { - "auth": { - "data": { - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika" - }, - "title": "Konfiguracja uwierzytelniania" - }, "configure": { "data": { "host": "Nazwa hosta lub adres IP", @@ -41,14 +34,6 @@ }, "title": "Wybierz urz\u0105dzenie ONVIF" }, - "manual_input": { - "data": { - "host": "Nazwa hosta lub adres IP", - "name": "Nazwa", - "port": "Port" - }, - "title": "Konfiguracja urz\u0105dzenia ONVIF" - }, "user": { "data": { "auto": "Wyszukaj automatycznie" diff --git a/homeassistant/components/onvif/translations/pt-BR.json b/homeassistant/components/onvif/translations/pt-BR.json index d5586b2dd2b..f491f21d56b 100644 --- a/homeassistant/components/onvif/translations/pt-BR.json +++ b/homeassistant/components/onvif/translations/pt-BR.json @@ -11,13 +11,6 @@ "cannot_connect": "Falha ao conectar" }, "step": { - "auth": { - "data": { - "password": "Senha", - "username": "Usu\u00e1rio" - }, - "title": "Configurar autentica\u00e7\u00e3o" - }, "configure": { "data": { "host": "Nome do host", @@ -41,14 +34,6 @@ }, "title": "Selecionar dispositivo ONVIF" }, - "manual_input": { - "data": { - "host": "Nome do host", - "name": "Nome", - "port": "Porta" - }, - "title": "Configurar dispositivo ONVIF" - }, "user": { "data": { "auto": "Pesquisar automaticamente" diff --git a/homeassistant/components/onvif/translations/pt.json b/homeassistant/components/onvif/translations/pt.json index c3662a032a6..f79bbec3201 100644 --- a/homeassistant/components/onvif/translations/pt.json +++ b/homeassistant/components/onvif/translations/pt.json @@ -11,13 +11,6 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { - "auth": { - "data": { - "password": "Palavra-passe", - "username": "Utilizador" - }, - "title": "Configurar autentica\u00e7\u00e3o" - }, "configure_profile": { "data": { "include": "Criar entidade da c\u00e2mara" @@ -31,14 +24,6 @@ }, "title": "Selecione o dispositivo ONVIF" }, - "manual_input": { - "data": { - "host": "Servidor", - "name": "Nome", - "port": "Porta" - }, - "title": "Configurar dispositivo ONVIF" - }, "user": { "description": "Ao clickar submeter, iremos procurar na sua rede por dispositivos ONVIF que suportem o Perfil S.\n\nAlguns fabricantes come\u00e7aram a desabilitar o ONVIF por omiss\u00e3o. Por favor verifique que o ONVIF est\u00e1 activo na configura\u00e7\u00e3o da sua c\u00e2mara.", "title": "Configura\u00e7\u00e3o do dispositivo ONVIF" diff --git a/homeassistant/components/onvif/translations/ru.json b/homeassistant/components/onvif/translations/ru.json index c8643fa9ec3..d9b09be4d42 100644 --- a/homeassistant/components/onvif/translations/ru.json +++ b/homeassistant/components/onvif/translations/ru.json @@ -11,13 +11,6 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "step": { - "auth": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" - }, - "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f" - }, "configure": { "data": { "host": "\u0425\u043e\u0441\u0442", @@ -41,14 +34,6 @@ }, "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e ONVIF" }, - "manual_input": { - "data": { - "host": "\u0425\u043e\u0441\u0442", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "port": "\u041f\u043e\u0440\u0442" - }, - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 ONVIF" - }, "user": { "data": { "auto": "\u0418\u0441\u043a\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438" diff --git a/homeassistant/components/onvif/translations/sk.json b/homeassistant/components/onvif/translations/sk.json index f9a7d7d07bf..f51c7e32bc8 100644 --- a/homeassistant/components/onvif/translations/sk.json +++ b/homeassistant/components/onvif/translations/sk.json @@ -5,23 +5,12 @@ "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha" }, "step": { - "auth": { - "data": { - "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" - } - }, "configure": { "data": { "name": "N\u00e1zov", "port": "Port", "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" } - }, - "manual_input": { - "data": { - "name": "N\u00e1zov", - "port": "Port" - } } } } diff --git a/homeassistant/components/onvif/translations/sl.json b/homeassistant/components/onvif/translations/sl.json index 37994d88e14..cbc5104ac4f 100644 --- a/homeassistant/components/onvif/translations/sl.json +++ b/homeassistant/components/onvif/translations/sl.json @@ -8,13 +8,6 @@ "onvif_error": "Napaka pri nastavitvi naprave ONVIF. Za ve\u010d informacij preverite dnevnike." }, "step": { - "auth": { - "data": { - "password": "Geslo", - "username": "Uporabni\u0161ko ime" - }, - "title": "Nastavite preverjanje pristnosti" - }, "configure_profile": { "data": { "include": "Ustvari entiteto kamere" @@ -28,13 +21,6 @@ }, "title": "Izberite ONVIF napravo" }, - "manual_input": { - "data": { - "host": "Gostitelj", - "port": "Vrata" - }, - "title": "Konfigurirajte ONVIF napravo" - }, "user": { "description": "S klikom na oddajo bomo v omre\u017eju iskali naprave ONVIF, ki podpirajo profil S. \n\n Nekateri proizvajalci so ONVIF privzeto za\u010deli onemogo\u010dati. Prepri\u010dajte se, da je ONVIF omogo\u010den v konfiguraciji naprave.", "title": "Nastavitev naprave ONVIF" diff --git a/homeassistant/components/onvif/translations/sv.json b/homeassistant/components/onvif/translations/sv.json index 0eff403bf45..626daebc7b2 100644 --- a/homeassistant/components/onvif/translations/sv.json +++ b/homeassistant/components/onvif/translations/sv.json @@ -1,20 +1,8 @@ { "config": { "step": { - "auth": { - "data": { - "password": "L\u00f6senord", - "username": "Anv\u00e4ndarnamn" - } - }, "configure_profile": { "title": "Konfigurera Profiler" - }, - "manual_input": { - "data": { - "host": "V\u00e4rd", - "port": "Port" - } } } } diff --git a/homeassistant/components/onvif/translations/tr.json b/homeassistant/components/onvif/translations/tr.json index c471775e8fa..3659586fe44 100644 --- a/homeassistant/components/onvif/translations/tr.json +++ b/homeassistant/components/onvif/translations/tr.json @@ -11,13 +11,6 @@ "cannot_connect": "Ba\u011flanma hatas\u0131" }, "step": { - "auth": { - "data": { - "password": "Parola", - "username": "Kullan\u0131c\u0131 Ad\u0131" - }, - "title": "Kimlik do\u011frulamay\u0131 yap\u0131land\u0131r" - }, "configure": { "data": { "host": "Sunucu", @@ -41,14 +34,6 @@ }, "title": "ONVIF cihaz\u0131n\u0131 se\u00e7in" }, - "manual_input": { - "data": { - "host": "Sunucu", - "name": "Ad", - "port": "Port" - }, - "title": "ONVIF cihaz\u0131n\u0131 yap\u0131land\u0131r\u0131n" - }, "user": { "data": { "auto": "Otomatik olarak ara" diff --git a/homeassistant/components/onvif/translations/uk.json b/homeassistant/components/onvif/translations/uk.json index 82a816add04..30381d900e3 100644 --- a/homeassistant/components/onvif/translations/uk.json +++ b/homeassistant/components/onvif/translations/uk.json @@ -11,13 +11,6 @@ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" }, "step": { - "auth": { - "data": { - "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" - }, - "title": "\u0423\u0432\u0456\u0439\u0442\u0438" - }, "configure_profile": { "data": { "include": "\u0421\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043e\u0431'\u0454\u043a\u0442 \u043a\u0430\u043c\u0435\u0440\u0438" @@ -31,14 +24,6 @@ }, "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 ONVIF" }, - "manual_input": { - "data": { - "host": "\u0425\u043e\u0441\u0442", - "name": "\u041d\u0430\u0437\u0432\u0430", - "port": "\u041f\u043e\u0440\u0442" - }, - "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e ONVIF" - }, "user": { "description": "\u041a\u043e\u043b\u0438 \u0412\u0438 \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u041d\u0430\u0434\u0456\u0441\u043b\u0430\u0442\u0438, \u043f\u043e\u0447\u043d\u0435\u0442\u044c\u0441\u044f \u043f\u043e\u0448\u0443\u043a \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 ONVIF, \u044f\u043a\u0456 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u044e\u0442\u044c Profile S. \n\n\u0414\u0435\u044f\u043a\u0456 \u0432\u0438\u0440\u043e\u0431\u043d\u0438\u043a\u0438 \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0437\u0430 \u0443\u043c\u043e\u0432\u0447\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0430\u044e\u0442\u044c ONVIF. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e ONVIF \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e \u0432 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u0445 \u0412\u0430\u0448\u043e\u0457 \u043a\u0430\u043c\u0435\u0440\u0438.", "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e ONVIF" diff --git a/homeassistant/components/onvif/translations/zh-Hans.json b/homeassistant/components/onvif/translations/zh-Hans.json index 1f87cc0a8cb..7009e91b336 100644 --- a/homeassistant/components/onvif/translations/zh-Hans.json +++ b/homeassistant/components/onvif/translations/zh-Hans.json @@ -11,13 +11,6 @@ "cannot_connect": "\u8fde\u63a5\u5931\u8d25" }, "step": { - "auth": { - "data": { - "password": "\u5bc6\u7801", - "username": "\u7528\u6237\u540d" - }, - "title": "\u914d\u7f6e\u8ba4\u8bc1\u4fe1\u606f" - }, "configure": { "data": { "host": "\u4e3b\u673a\u5730\u5740", @@ -41,14 +34,6 @@ }, "title": "\u9009\u62e9 ONVIF \u8bbe\u5907" }, - "manual_input": { - "data": { - "host": "\u4e3b\u673a\u5730\u5740", - "name": "\u540d\u79f0", - "port": "\u7aef\u53e3" - }, - "title": "\u914d\u7f6e ONVIF \u8bbe\u5907" - }, "user": { "data": { "auto": "\u81ea\u52a8\u641c\u7d22" diff --git a/homeassistant/components/onvif/translations/zh-Hant.json b/homeassistant/components/onvif/translations/zh-Hant.json index 23d0f2d12f0..f4d2ddf036d 100644 --- a/homeassistant/components/onvif/translations/zh-Hant.json +++ b/homeassistant/components/onvif/translations/zh-Hant.json @@ -11,13 +11,6 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "step": { - "auth": { - "data": { - "password": "\u5bc6\u78bc", - "username": "\u4f7f\u7528\u8005\u540d\u7a31" - }, - "title": "\u8a2d\u5b9a\u9a57\u8b49" - }, "configure": { "data": { "host": "\u4e3b\u6a5f\u7aef", @@ -41,14 +34,6 @@ }, "title": "\u9078\u64c7 ONVIF \u88dd\u7f6e" }, - "manual_input": { - "data": { - "host": "\u4e3b\u6a5f\u7aef", - "name": "\u540d\u7a31", - "port": "\u901a\u8a0a\u57e0" - }, - "title": "\u8a2d\u5b9a ONVIF \u88dd\u7f6e" - }, "user": { "data": { "auto": "\u81ea\u52d5\u641c\u5c0b" diff --git a/homeassistant/components/opentherm_gw/translations/bg.json b/homeassistant/components/opentherm_gw/translations/bg.json index a7c30b4a45a..b55da1b4098 100644 --- a/homeassistant/components/opentherm_gw/translations/bg.json +++ b/homeassistant/components/opentherm_gw/translations/bg.json @@ -11,8 +11,7 @@ "device": "\u041f\u044a\u0442 \u0438\u043b\u0438 URL \u0430\u0434\u0440\u0435\u0441", "id": "ID", "name": "\u0418\u043c\u0435" - }, - "title": "OpenTherm Gateway" + } } } }, @@ -21,8 +20,7 @@ "init": { "data": { "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043d\u0430 \u043f\u043e\u0434\u0430" - }, - "description": "\u041e\u043f\u0446\u0438\u0438 \u0437\u0430 OpenTherm Gateway" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/ca.json b/homeassistant/components/opentherm_gw/translations/ca.json index c4caf76d233..6427c3c7633 100644 --- a/homeassistant/components/opentherm_gw/translations/ca.json +++ b/homeassistant/components/opentherm_gw/translations/ca.json @@ -11,8 +11,7 @@ "device": "Ruta o URL", "id": "ID", "name": "Nom" - }, - "title": "Passarel\u00b7la d'OpenTherm" + } } } }, @@ -24,8 +23,7 @@ "read_precision": "Llegeix precisi\u00f3", "set_precision": "Defineix precisi\u00f3", "temporary_override_mode": "Mode de sobreescriptura temporal" - }, - "description": "Opcions del la passarel\u00b7la d'enlla\u00e7 d'OpenTherm" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/cs.json b/homeassistant/components/opentherm_gw/translations/cs.json index 39d70d05cfc..5bf8d4fc385 100644 --- a/homeassistant/components/opentherm_gw/translations/cs.json +++ b/homeassistant/components/opentherm_gw/translations/cs.json @@ -11,8 +11,7 @@ "device": "Cesta nebo URL", "id": "ID", "name": "Jm\u00e9no" - }, - "title": "Br\u00e1na OpenTherm" + } } } }, @@ -21,8 +20,7 @@ "init": { "data": { "floor_temperature": "Teplota podlahy" - }, - "description": "Mo\u017enosti br\u00e1ny OpenTherm" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/da.json b/homeassistant/components/opentherm_gw/translations/da.json index cca7cb347db..a9d675c2c6c 100644 --- a/homeassistant/components/opentherm_gw/translations/da.json +++ b/homeassistant/components/opentherm_gw/translations/da.json @@ -10,8 +10,7 @@ "device": "Sti eller webadresse", "id": "Id", "name": "Navn" - }, - "title": "OpenTherm Gateway" + } } } }, @@ -20,8 +19,7 @@ "init": { "data": { "floor_temperature": "Gulvtemperatur" - }, - "description": "Indstillinger for OpenTherm Gateway" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/de.json b/homeassistant/components/opentherm_gw/translations/de.json index e7be7815366..1217839eaed 100644 --- a/homeassistant/components/opentherm_gw/translations/de.json +++ b/homeassistant/components/opentherm_gw/translations/de.json @@ -11,8 +11,7 @@ "device": "Pfad oder URL", "id": "ID", "name": "Name" - }, - "title": "OpenTherm Gateway" + } } } }, @@ -24,8 +23,7 @@ "read_precision": "Pr\u00e4zision abfragen", "set_precision": "Pr\u00e4zision einstellen", "temporary_override_mode": "Tempor\u00e4rer Sollwert\u00fcbersteuerungsmodus" - }, - "description": "Optionen f\u00fcr das OpenTherm Gateway" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/el.json b/homeassistant/components/opentherm_gw/translations/el.json index 92dac2e9b3d..82be1ff6ce9 100644 --- a/homeassistant/components/opentherm_gw/translations/el.json +++ b/homeassistant/components/opentherm_gw/translations/el.json @@ -11,8 +11,7 @@ "device": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", "id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc", "name": "\u038c\u03bd\u03bf\u03bc\u03b1" - }, - "title": "\u03a0\u03cd\u03bb\u03b7 OpenTherm" + } } } }, @@ -24,8 +23,7 @@ "read_precision": "\u0394\u03b9\u03ac\u03b2\u03b1\u03c3\u03b5 \u03c4\u03b7\u03bd \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1", "set_precision": "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1\u03c2", "temporary_override_mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c9\u03c1\u03b9\u03bd\u03ae\u03c2 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7\u03c2 \u03c3\u03b7\u03bc\u03b5\u03af\u03bf\u03c5 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2" - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c0\u03cd\u03bb\u03b7 OpenTherm" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/en.json b/homeassistant/components/opentherm_gw/translations/en.json index c1e897c631b..c84e9dcf0e2 100644 --- a/homeassistant/components/opentherm_gw/translations/en.json +++ b/homeassistant/components/opentherm_gw/translations/en.json @@ -11,8 +11,7 @@ "device": "Path or URL", "id": "ID", "name": "Name" - }, - "title": "OpenTherm Gateway" + } } } }, @@ -24,8 +23,7 @@ "read_precision": "Read Precision", "set_precision": "Set Precision", "temporary_override_mode": "Temporary Setpoint Override Mode" - }, - "description": "Options for the OpenTherm Gateway" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/es-419.json b/homeassistant/components/opentherm_gw/translations/es-419.json index f9a6f60b463..6e28121d852 100644 --- a/homeassistant/components/opentherm_gw/translations/es-419.json +++ b/homeassistant/components/opentherm_gw/translations/es-419.json @@ -10,8 +10,7 @@ "device": "Ruta o URL", "id": "Identificaci\u00f3n", "name": "Nombre" - }, - "title": "OpenTherm Gateway" + } } } }, @@ -22,8 +21,7 @@ "floor_temperature": "Temperatura del piso", "read_precision": "Leer precisi\u00f3n", "set_precision": "Establecer precisi\u00f3n" - }, - "description": "Opciones para OpenTherm Gateway" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/es.json b/homeassistant/components/opentherm_gw/translations/es.json index 8c773c8c1e2..2b6cc0afd7f 100644 --- a/homeassistant/components/opentherm_gw/translations/es.json +++ b/homeassistant/components/opentherm_gw/translations/es.json @@ -11,8 +11,7 @@ "device": "Ruta o URL", "id": "ID", "name": "Nombre" - }, - "title": "Gateway OpenTherm" + } } } }, @@ -24,8 +23,7 @@ "read_precision": "Leer precisi\u00f3n", "set_precision": "Establecer precisi\u00f3n", "temporary_override_mode": "Modo de anulaci\u00f3n temporal del punto de ajuste" - }, - "description": "Opciones para OpenTherm Gateway" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/et.json b/homeassistant/components/opentherm_gw/translations/et.json index 67528b034c5..2458c8f1d52 100644 --- a/homeassistant/components/opentherm_gw/translations/et.json +++ b/homeassistant/components/opentherm_gw/translations/et.json @@ -11,8 +11,7 @@ "device": "Rada v\u00f5i URL", "id": "", "name": "Nimi" - }, - "title": "" + } } } }, @@ -24,8 +23,7 @@ "read_precision": "Lugemi t\u00e4psus", "set_precision": "M\u00e4\u00e4ra lugemi t\u00e4psus", "temporary_override_mode": "Ajutine seadepunkti alistamine" - }, - "description": "OpenTherm Gateway suvandid" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/fr.json b/homeassistant/components/opentherm_gw/translations/fr.json index 32b642fa639..d0a058eacfa 100644 --- a/homeassistant/components/opentherm_gw/translations/fr.json +++ b/homeassistant/components/opentherm_gw/translations/fr.json @@ -11,8 +11,7 @@ "device": "Chemin ou URL", "id": "ID", "name": "Nom" - }, - "title": "Passerelle OpenTherm" + } } } }, @@ -24,8 +23,7 @@ "read_precision": "Pr\u00e9cision de lecture", "set_precision": "D\u00e9finir la pr\u00e9cision", "temporary_override_mode": "Mode de neutralisation du point de consigne temporaire" - }, - "description": "Options pour la passerelle OpenTherm" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/hu.json b/homeassistant/components/opentherm_gw/translations/hu.json index e2a284d7dd1..9e2b6478bec 100644 --- a/homeassistant/components/opentherm_gw/translations/hu.json +++ b/homeassistant/components/opentherm_gw/translations/hu.json @@ -11,8 +11,7 @@ "device": "El\u00e9r\u00e9si \u00fat vagy URL", "id": "ID", "name": "Elnevez\u00e9s" - }, - "title": "OpenTherm \u00e1tj\u00e1r\u00f3" + } } } }, @@ -24,8 +23,7 @@ "read_precision": "Pontoss\u00e1g olvas\u00e1sa", "set_precision": "Pontoss\u00e1g be\u00e1ll\u00edt\u00e1sa", "temporary_override_mode": "Ideiglenes be\u00e1ll\u00edt\u00e1s fel\u00fclb\u00edr\u00e1l\u00e1si m\u00f3dja" - }, - "description": "Opci\u00f3k az OpenTherm \u00e1tj\u00e1r\u00f3hoz" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/id.json b/homeassistant/components/opentherm_gw/translations/id.json index f3677b9de3b..5f87fd0ed4b 100644 --- a/homeassistant/components/opentherm_gw/translations/id.json +++ b/homeassistant/components/opentherm_gw/translations/id.json @@ -11,8 +11,7 @@ "device": "Jalur atau URL", "id": "ID", "name": "Nama" - }, - "title": "Gateway OpenTherm" + } } } }, @@ -24,8 +23,7 @@ "read_precision": "Tingkat Presisi Baca", "set_precision": "Atur Presisi", "temporary_override_mode": "Mode Penimpaan Setpoint Sementara" - }, - "description": "Pilihan untuk Gateway OpenTherm" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/it.json b/homeassistant/components/opentherm_gw/translations/it.json index 4b3407cd95f..4d1feac60dd 100644 --- a/homeassistant/components/opentherm_gw/translations/it.json +++ b/homeassistant/components/opentherm_gw/translations/it.json @@ -11,8 +11,7 @@ "device": "Percorso o URL", "id": "ID", "name": "Nome" - }, - "title": "OpenTherm Gateway" + } } } }, @@ -24,8 +23,7 @@ "read_precision": "Leggi la precisione", "set_precision": "Imposta la precisione", "temporary_override_mode": "Modalit\u00e0 di esclusione temporanea del setpoint" - }, - "description": "Opzioni per OpenTherm Gateway" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/ja.json b/homeassistant/components/opentherm_gw/translations/ja.json index fa31eced5ab..16f4d55eac0 100644 --- a/homeassistant/components/opentherm_gw/translations/ja.json +++ b/homeassistant/components/opentherm_gw/translations/ja.json @@ -11,8 +11,7 @@ "device": "\u30d1\u30b9\u307e\u305f\u306fURL", "id": "ID", "name": "\u540d\u524d" - }, - "title": "OpenTherm Gateway" + } } } }, @@ -24,8 +23,7 @@ "read_precision": "\u7cbe\u5ea6\u3092\u8aad\u307f\u8fbc\u3080", "set_precision": "\u7cbe\u5ea6\u3092\u8a2d\u5b9a\u3059\u308b", "temporary_override_mode": "\u4e00\u6642\u7684\u306a\u30bb\u30c3\u30c8\u30dd\u30a4\u30f3\u30c8\u306e\u30aa\u30fc\u30d0\u30fc\u30e9\u30a4\u30c9\u30e2\u30fc\u30c9" - }, - "description": "OpenTherm Gateway\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/ko.json b/homeassistant/components/opentherm_gw/translations/ko.json index 178bf915a6b..01d1971ca6f 100644 --- a/homeassistant/components/opentherm_gw/translations/ko.json +++ b/homeassistant/components/opentherm_gw/translations/ko.json @@ -11,8 +11,7 @@ "device": "\uacbd\ub85c \ub610\ub294 URL", "id": "ID", "name": "\uc774\ub984" - }, - "title": "OpenTherm Gateway" + } } } }, @@ -24,8 +23,7 @@ "read_precision": "\uc77d\uae30 \uc815\ubc00\ub3c4", "set_precision": "\uc815\ubc00\ub3c4 \uc124\uc815\ud558\uae30", "temporary_override_mode": "\uc784\uc2dc \uc124\uc815\uac12 \uc7ac\uc815\uc758 \ubaa8\ub4dc" - }, - "description": "OpenTherm Gateway \uc635\uc158" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/lb.json b/homeassistant/components/opentherm_gw/translations/lb.json index e90a6c2a310..b9e28bbab0b 100644 --- a/homeassistant/components/opentherm_gw/translations/lb.json +++ b/homeassistant/components/opentherm_gw/translations/lb.json @@ -11,8 +11,7 @@ "device": "Pfad oder URL", "id": "ID", "name": "Numm" - }, - "title": "OpenTherm Gateway" + } } } }, @@ -21,8 +20,7 @@ "init": { "data": { "floor_temperature": "Buedem Temperatur" - }, - "description": "Optioune fir OpenTherm Gateway" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/nl.json b/homeassistant/components/opentherm_gw/translations/nl.json index 025cdea128d..fd8c97c4371 100644 --- a/homeassistant/components/opentherm_gw/translations/nl.json +++ b/homeassistant/components/opentherm_gw/translations/nl.json @@ -11,8 +11,7 @@ "device": "Pad of URL", "id": "ID", "name": "Naam" - }, - "title": "OpenTherm Gateway" + } } } }, @@ -24,8 +23,7 @@ "read_precision": "Lees Precisie", "set_precision": "Precisie instellen", "temporary_override_mode": "Tijdelijke setpoint-overschrijvingsmodus" - }, - "description": "Opties voor de OpenTherm Gateway" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/no.json b/homeassistant/components/opentherm_gw/translations/no.json index 9ef1aa0a5fb..5cf2e9732e9 100644 --- a/homeassistant/components/opentherm_gw/translations/no.json +++ b/homeassistant/components/opentherm_gw/translations/no.json @@ -11,8 +11,7 @@ "device": "Bane eller URL-adresse", "id": "", "name": "Navn" - }, - "title": "" + } } } }, @@ -24,8 +23,7 @@ "read_precision": "Les presisjon", "set_precision": "Angi presisjon", "temporary_override_mode": "Midlertidig overstyringsmodus for settpunkt" - }, - "description": "Alternativer for OpenTherm Gateway" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/pl.json b/homeassistant/components/opentherm_gw/translations/pl.json index 69dd6040093..fafc4fdc0d7 100644 --- a/homeassistant/components/opentherm_gw/translations/pl.json +++ b/homeassistant/components/opentherm_gw/translations/pl.json @@ -11,8 +11,7 @@ "device": "\u015acie\u017cka lub adres URL", "id": "Identyfikator", "name": "Nazwa" - }, - "title": "Bramka OpenTherm" + } } } }, @@ -24,8 +23,7 @@ "read_precision": "Odczytaj precyzj\u0119", "set_precision": "Ustaw precyzj\u0119", "temporary_override_mode": "Tryb tymczasowej zmiany nastawy" - }, - "description": "Opcje dla bramki OpenTherm" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/pt-BR.json b/homeassistant/components/opentherm_gw/translations/pt-BR.json index cf4ed0846d2..018aed87dc3 100644 --- a/homeassistant/components/opentherm_gw/translations/pt-BR.json +++ b/homeassistant/components/opentherm_gw/translations/pt-BR.json @@ -11,8 +11,7 @@ "device": "Caminho ou URL", "id": "ID", "name": "Nome" - }, - "title": "OpenTherm Gateway" + } } } }, @@ -24,8 +23,7 @@ "read_precision": "Precis\u00e3o de leitura", "set_precision": "Definir precis\u00e3o", "temporary_override_mode": "Modo de substitui\u00e7\u00e3o tempor\u00e1ria do ponto de ajuste" - }, - "description": "Op\u00e7\u00f5es para o OpenTherm Gateway" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/ru.json b/homeassistant/components/opentherm_gw/translations/ru.json index 9f2bfff07cf..e743830624a 100644 --- a/homeassistant/components/opentherm_gw/translations/ru.json +++ b/homeassistant/components/opentherm_gw/translations/ru.json @@ -11,8 +11,7 @@ "device": "\u041f\u0443\u0442\u044c \u0438\u043b\u0438 URL-\u0430\u0434\u0440\u0435\u0441", "id": "ID", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" - }, - "title": "OpenTherm" + } } } }, @@ -24,8 +23,7 @@ "read_precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0447\u0442\u0435\u043d\u0438\u044f", "set_precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438", "temporary_override_mode": "\u0420\u0435\u0436\u0438\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0430\u0432\u043a\u0438" - }, - "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0430 Opentherm" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/sl.json b/homeassistant/components/opentherm_gw/translations/sl.json index a54ceb5031e..9e58b21c4e0 100644 --- a/homeassistant/components/opentherm_gw/translations/sl.json +++ b/homeassistant/components/opentherm_gw/translations/sl.json @@ -10,8 +10,7 @@ "device": "Pot ali URL", "id": "ID", "name": "Ime" - }, - "title": "OpenTherm Prehod" + } } } }, @@ -20,8 +19,7 @@ "init": { "data": { "floor_temperature": "Temperatura nadstropja" - }, - "description": "Mo\u017enosti za prehod OpenTherm" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/sv.json b/homeassistant/components/opentherm_gw/translations/sv.json index 01aa96564ac..8ba8716897f 100644 --- a/homeassistant/components/opentherm_gw/translations/sv.json +++ b/homeassistant/components/opentherm_gw/translations/sv.json @@ -10,8 +10,7 @@ "device": "S\u00f6kv\u00e4g eller URL", "id": "ID", "name": "Namn" - }, - "title": "OpenTherm Gateway" + } } } }, @@ -20,8 +19,7 @@ "init": { "data": { "floor_temperature": "Golvetemperatur" - }, - "description": "Alternativ f\u00f6r OpenTherm Gateway" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/tr.json b/homeassistant/components/opentherm_gw/translations/tr.json index a969927ebe2..72a603cc827 100644 --- a/homeassistant/components/opentherm_gw/translations/tr.json +++ b/homeassistant/components/opentherm_gw/translations/tr.json @@ -11,8 +11,7 @@ "device": "Yol veya URL", "id": "ID", "name": "Ad" - }, - "title": "OpenTherm A\u011f Ge\u00e7idi" + } } } }, @@ -24,8 +23,7 @@ "read_precision": "Hassas Okuma", "set_precision": "Hassasiyeti Ayarla", "temporary_override_mode": "Ge\u00e7ici Ayar Noktas\u0131 Ge\u00e7ersiz K\u0131lma Modu" - }, - "description": "OpenTherm A\u011f Ge\u00e7idi i\u00e7in Se\u00e7enekler" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/uk.json b/homeassistant/components/opentherm_gw/translations/uk.json index fdffbfec00e..89fa29a5bf2 100644 --- a/homeassistant/components/opentherm_gw/translations/uk.json +++ b/homeassistant/components/opentherm_gw/translations/uk.json @@ -11,8 +11,7 @@ "device": "\u0428\u043b\u044f\u0445 \u0430\u0431\u043e URL-\u0430\u0434\u0440\u0435\u0441\u0430", "id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440", "name": "\u041d\u0430\u0437\u0432\u0430" - }, - "title": "OpenTherm" + } } } }, @@ -21,8 +20,7 @@ "init": { "data": { "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u0456\u0434\u043b\u043e\u0433\u0438" - }, - "description": "\u0414\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0443 Opentherm" + } } } } diff --git a/homeassistant/components/opentherm_gw/translations/zh-Hant.json b/homeassistant/components/opentherm_gw/translations/zh-Hant.json index 7b8466aa424..d5b9d02de42 100644 --- a/homeassistant/components/opentherm_gw/translations/zh-Hant.json +++ b/homeassistant/components/opentherm_gw/translations/zh-Hant.json @@ -11,8 +11,7 @@ "device": "\u8def\u5f91\u6216 URL", "id": "ID", "name": "\u540d\u7a31" - }, - "title": "OpenTherm \u9598\u9053\u5668" + } } } }, @@ -24,8 +23,7 @@ "read_precision": "\u8b80\u53d6\u7cbe\u6e96\u5ea6", "set_precision": "\u8a2d\u5b9a\u7cbe\u6e96\u5ea6", "temporary_override_mode": "\u81e8\u6642 Setpoint \u8986\u84cb\u6a21\u5f0f" - }, - "description": "OpenTherm \u9598\u9053\u5668\u9078\u9805" + } } } } diff --git a/homeassistant/components/openweathermap/translations/ar.json b/homeassistant/components/openweathermap/translations/ar.json index 1f69a1cf739..5ff07ca51e8 100644 --- a/homeassistant/components/openweathermap/translations/ar.json +++ b/homeassistant/components/openweathermap/translations/ar.json @@ -6,8 +6,7 @@ "language": "\u0627\u0644\u0644\u063a\u0629", "name": "\u0627\u0633\u0645 \u0627\u0644\u062a\u0643\u0627\u0645\u0644" }, - "description": "\u0642\u0645 \u0628\u0625\u0639\u062f\u0627\u062f \u062a\u0643\u0627\u0645\u0644 OpenWeatherMap. \u0644\u0625\u0646\u0634\u0627\u0621 \u0645\u0641\u062a\u0627\u062d API \u060c \u0627\u0646\u062a\u0642\u0644 \u0625\u0644\u0649 https://openweathermap.org/appid", - "title": "OpenWeatherMap" + "description": "\u0642\u0645 \u0628\u0625\u0639\u062f\u0627\u062f \u062a\u0643\u0627\u0645\u0644 OpenWeatherMap. \u0644\u0625\u0646\u0634\u0627\u0621 \u0645\u0641\u062a\u0627\u062d API \u060c \u0627\u0646\u062a\u0642\u0644 \u0625\u0644\u0649 https://openweathermap.org/appid" } } }, diff --git a/homeassistant/components/openweathermap/translations/bg.json b/homeassistant/components/openweathermap/translations/bg.json index c1816ddae03..a46653b9fed 100644 --- a/homeassistant/components/openweathermap/translations/bg.json +++ b/homeassistant/components/openweathermap/translations/bg.json @@ -16,8 +16,7 @@ "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" + } } } }, diff --git a/homeassistant/components/openweathermap/translations/ca.json b/homeassistant/components/openweathermap/translations/ca.json index f304a8d4f9f..251bb11a386 100644 --- a/homeassistant/components/openweathermap/translations/ca.json +++ b/homeassistant/components/openweathermap/translations/ca.json @@ -17,8 +17,7 @@ "mode": "Mode", "name": "Nom" }, - "description": "Per generar la clau API, v\u00e9s a https://openweathermap.org/appid", - "title": "OpenWeatherMap" + "description": "Per generar la clau API, v\u00e9s a https://openweathermap.org/appid" } } }, diff --git a/homeassistant/components/openweathermap/translations/cs.json b/homeassistant/components/openweathermap/translations/cs.json index 42383643be1..c793e56916f 100644 --- a/homeassistant/components/openweathermap/translations/cs.json +++ b/homeassistant/components/openweathermap/translations/cs.json @@ -17,8 +17,7 @@ "mode": "Re\u017eim", "name": "N\u00e1zev integrace" }, - "description": "Nastaven\u00ed integrace OpenWeatherMap. Chcete-li vygenerovat API kl\u00ed\u010d, p\u0159ejd\u011bte na https://openweathermap.org/appid", - "title": "OpenWeatherMap" + "description": "Nastaven\u00ed integrace OpenWeatherMap. Chcete-li vygenerovat API kl\u00ed\u010d, p\u0159ejd\u011bte na https://openweathermap.org/appid" } } }, diff --git a/homeassistant/components/openweathermap/translations/de.json b/homeassistant/components/openweathermap/translations/de.json index f74065ca1a1..644956b1d49 100644 --- a/homeassistant/components/openweathermap/translations/de.json +++ b/homeassistant/components/openweathermap/translations/de.json @@ -17,8 +17,7 @@ "mode": "Modus", "name": "Name" }, - "description": "Um den API-Schl\u00fcssel zu generieren, besuche https://openweathermap.org/appid", - "title": "OpenWeatherMap" + "description": "Um den API-Schl\u00fcssel zu generieren, besuche https://openweathermap.org/appid" } } }, diff --git a/homeassistant/components/openweathermap/translations/el.json b/homeassistant/components/openweathermap/translations/el.json index 90b876033f6..dada058b430 100644 --- a/homeassistant/components/openweathermap/translations/el.json +++ b/homeassistant/components/openweathermap/translations/el.json @@ -17,8 +17,7 @@ "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1", "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" }, - "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 OpenWeatherMap. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://openweathermap.org/appid", - "title": "OpenWeatherMap" + "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 OpenWeatherMap. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://openweathermap.org/appid" } } }, diff --git a/homeassistant/components/openweathermap/translations/en.json b/homeassistant/components/openweathermap/translations/en.json index 55f8d0a2338..27bfb9393d1 100644 --- a/homeassistant/components/openweathermap/translations/en.json +++ b/homeassistant/components/openweathermap/translations/en.json @@ -17,8 +17,7 @@ "mode": "Mode", "name": "Name" }, - "description": "To generate API key go to https://openweathermap.org/appid", - "title": "OpenWeatherMap" + "description": "To generate API key go to https://openweathermap.org/appid" } } }, diff --git a/homeassistant/components/openweathermap/translations/es.json b/homeassistant/components/openweathermap/translations/es.json index 5d276f60cb6..295de30fe47 100644 --- a/homeassistant/components/openweathermap/translations/es.json +++ b/homeassistant/components/openweathermap/translations/es.json @@ -17,8 +17,7 @@ "mode": "Modo", "name": "Nombre" }, - "description": "Para generar la clave API, ve a https://openweathermap.org/appid", - "title": "OpenWeatherMap" + "description": "Para generar la clave API, ve a https://openweathermap.org/appid" } } }, diff --git a/homeassistant/components/openweathermap/translations/et.json b/homeassistant/components/openweathermap/translations/et.json index 0d991c99e3f..02a8731c73b 100644 --- a/homeassistant/components/openweathermap/translations/et.json +++ b/homeassistant/components/openweathermap/translations/et.json @@ -17,8 +17,7 @@ "mode": "Re\u017eiim", "name": "Nimi" }, - "description": "API-v\u00f5tme loomiseks mine aadressile https://openweathermap.org/appid", - "title": "OpenWeatherMap" + "description": "API-v\u00f5tme loomiseks mine aadressile https://openweathermap.org/appid" } } }, diff --git a/homeassistant/components/openweathermap/translations/fr.json b/homeassistant/components/openweathermap/translations/fr.json index 387c3eefb28..6beac076ba0 100644 --- a/homeassistant/components/openweathermap/translations/fr.json +++ b/homeassistant/components/openweathermap/translations/fr.json @@ -17,8 +17,7 @@ "mode": "Mode", "name": "Nom" }, - "description": "Pour g\u00e9n\u00e9rer une cl\u00e9 d'API, rendez-vous sur https://openweathermap.org/appid", - "title": "OpenWeatherMap" + "description": "Pour g\u00e9n\u00e9rer une cl\u00e9 d'API, rendez-vous sur https://openweathermap.org/appid" } } }, diff --git a/homeassistant/components/openweathermap/translations/he.json b/homeassistant/components/openweathermap/translations/he.json index cadadeb1865..4de4cdf9e40 100644 --- a/homeassistant/components/openweathermap/translations/he.json +++ b/homeassistant/components/openweathermap/translations/he.json @@ -16,8 +16,7 @@ "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", "mode": "\u05de\u05e6\u05d1", "name": "\u05e9\u05dd" - }, - "title": "\u05de\u05e4\u05ea OpenWeather" + } } } }, diff --git a/homeassistant/components/openweathermap/translations/hu.json b/homeassistant/components/openweathermap/translations/hu.json index f5a7dade833..8b28a50e771 100644 --- a/homeassistant/components/openweathermap/translations/hu.json +++ b/homeassistant/components/openweathermap/translations/hu.json @@ -17,8 +17,7 @@ "mode": "M\u00f3d", "name": "Elnevez\u00e9s" }, - "description": "Az API-kulcs l\u00e9trehoz\u00e1s\u00e1hoz keresse fel a https://openweathermap.org/appid webhelyet", - "title": "OpenWeatherMap" + "description": "Az API-kulcs l\u00e9trehoz\u00e1s\u00e1hoz keresse fel a https://openweathermap.org/appid webhelyet" } } }, diff --git a/homeassistant/components/openweathermap/translations/id.json b/homeassistant/components/openweathermap/translations/id.json index d8ea0344cea..efa891428c9 100644 --- a/homeassistant/components/openweathermap/translations/id.json +++ b/homeassistant/components/openweathermap/translations/id.json @@ -17,8 +17,7 @@ "mode": "Mode", "name": "Nama" }, - "description": "Untuk membuat kunci API, buka https://openweathermap.org/appid", - "title": "OpenWeatherMap" + "description": "Untuk membuat kunci API, buka https://openweathermap.org/appid" } } }, diff --git a/homeassistant/components/openweathermap/translations/it.json b/homeassistant/components/openweathermap/translations/it.json index 133fa704109..475ae84503c 100644 --- a/homeassistant/components/openweathermap/translations/it.json +++ b/homeassistant/components/openweathermap/translations/it.json @@ -17,8 +17,7 @@ "mode": "Modalit\u00e0", "name": "Nome" }, - "description": "Per generare la chiave API, vai su https://openweathermap.org/appid", - "title": "OpenWeatherMap" + "description": "Per generare la chiave API, vai su https://openweathermap.org/appid" } } }, diff --git a/homeassistant/components/openweathermap/translations/ja.json b/homeassistant/components/openweathermap/translations/ja.json index 1253ba5af21..511d59b2138 100644 --- a/homeassistant/components/openweathermap/translations/ja.json +++ b/homeassistant/components/openweathermap/translations/ja.json @@ -17,8 +17,7 @@ "mode": "\u30e2\u30fc\u30c9", "name": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u540d\u524d" }, - "description": "OpenWeatherMap\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://openweathermap.org/appid \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", - "title": "OpenWeatherMap" + "description": "OpenWeatherMap\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://openweathermap.org/appid \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } }, diff --git a/homeassistant/components/openweathermap/translations/ko.json b/homeassistant/components/openweathermap/translations/ko.json index d2f5d9aa123..131caf44eff 100644 --- a/homeassistant/components/openweathermap/translations/ko.json +++ b/homeassistant/components/openweathermap/translations/ko.json @@ -17,8 +17,7 @@ "mode": "\ubaa8\ub4dc", "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc774\ub984" }, - "description": "OpenWeatherMap \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://openweathermap.org/appid \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694", - "title": "OpenWeatherMap" + "description": "OpenWeatherMap \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://openweathermap.org/appid \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694" } } }, diff --git a/homeassistant/components/openweathermap/translations/lb.json b/homeassistant/components/openweathermap/translations/lb.json index bed28c80575..0cb8299fdbf 100644 --- a/homeassistant/components/openweathermap/translations/lb.json +++ b/homeassistant/components/openweathermap/translations/lb.json @@ -17,8 +17,7 @@ "mode": "Modus", "name": "Numm vun der Integratioun" }, - "description": "OpenWeatherMap Integratioun ariichten. Fir een API Schl\u00ebssel z'erstelle g\u00e9i op https://openweathermap.org/appid", - "title": "OpenWeatherMap API Schl\u00ebssel" + "description": "OpenWeatherMap Integratioun ariichten. Fir een API Schl\u00ebssel z'erstelle g\u00e9i op https://openweathermap.org/appid" } } }, diff --git a/homeassistant/components/openweathermap/translations/nl.json b/homeassistant/components/openweathermap/translations/nl.json index 1c72f10c6d9..74ea3532765 100644 --- a/homeassistant/components/openweathermap/translations/nl.json +++ b/homeassistant/components/openweathermap/translations/nl.json @@ -17,8 +17,7 @@ "mode": "Mode", "name": "Naam" }, - "description": "Om een API sleutel te genereren ga naar https://openweathermap.org/appid", - "title": "OpenWeatherMap" + "description": "Om een API sleutel te genereren ga naar https://openweathermap.org/appid" } } }, diff --git a/homeassistant/components/openweathermap/translations/no.json b/homeassistant/components/openweathermap/translations/no.json index d751553465a..de839e3987e 100644 --- a/homeassistant/components/openweathermap/translations/no.json +++ b/homeassistant/components/openweathermap/translations/no.json @@ -17,8 +17,7 @@ "mode": "Modus", "name": "Navn" }, - "description": "For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://openweathermap.org/appid", - "title": "OpenWeatherMap" + "description": "For \u00e5 generere API-n\u00f8kkel, g\u00e5 til https://openweathermap.org/appid" } } }, diff --git a/homeassistant/components/openweathermap/translations/pl.json b/homeassistant/components/openweathermap/translations/pl.json index de8d857f168..be8e052bb3e 100644 --- a/homeassistant/components/openweathermap/translations/pl.json +++ b/homeassistant/components/openweathermap/translations/pl.json @@ -17,8 +17,7 @@ "mode": "Tryb", "name": "Nazwa" }, - "description": "Aby wygenerowa\u0107 klucz API, przejd\u017a do https://openweathermap.org/appid", - "title": "OpenWeatherMap" + "description": "Aby wygenerowa\u0107 klucz API, przejd\u017a do https://openweathermap.org/appid" } } }, diff --git a/homeassistant/components/openweathermap/translations/pt-BR.json b/homeassistant/components/openweathermap/translations/pt-BR.json index 795db96f36f..9178fb8ac0b 100644 --- a/homeassistant/components/openweathermap/translations/pt-BR.json +++ b/homeassistant/components/openweathermap/translations/pt-BR.json @@ -17,8 +17,7 @@ "mode": "Modo", "name": "Nome" }, - "description": "Para gerar a chave de API, acesse https://openweathermap.org/appid", - "title": "OpenWeatherMap" + "description": "Para gerar a chave de API, acesse https://openweathermap.org/appid" } } }, diff --git a/homeassistant/components/openweathermap/translations/ru.json b/homeassistant/components/openweathermap/translations/ru.json index a0724e90f01..ccbe560a3df 100644 --- a/homeassistant/components/openweathermap/translations/ru.json +++ b/homeassistant/components/openweathermap/translations/ru.json @@ -17,8 +17,7 @@ "mode": "\u0420\u0435\u0436\u0438\u043c", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 https://openweathermap.org/appid.", - "title": "OpenWeatherMap" + "description": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 https://openweathermap.org/appid." } } }, diff --git a/homeassistant/components/openweathermap/translations/sv.json b/homeassistant/components/openweathermap/translations/sv.json index cafa9c0fdd0..c0fdf3bbfdb 100644 --- a/homeassistant/components/openweathermap/translations/sv.json +++ b/homeassistant/components/openweathermap/translations/sv.json @@ -10,8 +10,7 @@ "language": "Spr\u00e5k", "mode": "L\u00e4ge", "name": "Integrationens namn" - }, - "title": "OpenWeatherMap" + } } } }, diff --git a/homeassistant/components/openweathermap/translations/tr.json b/homeassistant/components/openweathermap/translations/tr.json index 6458852712e..ad2da8b8102 100644 --- a/homeassistant/components/openweathermap/translations/tr.json +++ b/homeassistant/components/openweathermap/translations/tr.json @@ -17,8 +17,7 @@ "mode": "Mod", "name": "Ad" }, - "description": "API anahtar\u0131 olu\u015fturmak i\u00e7in https://openweathermap.org/appid adresine gidin.", - "title": "OpenWeatherMap" + "description": "API anahtar\u0131 olu\u015fturmak i\u00e7in https://openweathermap.org/appid adresine gidin." } } }, diff --git a/homeassistant/components/openweathermap/translations/uk.json b/homeassistant/components/openweathermap/translations/uk.json index 7a39cfa078e..8b3db718680 100644 --- a/homeassistant/components/openweathermap/translations/uk.json +++ b/homeassistant/components/openweathermap/translations/uk.json @@ -17,8 +17,7 @@ "mode": "\u0420\u0435\u0436\u0438\u043c", "name": "\u041d\u0430\u0437\u0432\u0430" }, - "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 OpenWeatherMap. \u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043d\u0430 https://openweathermap.org/appid.", - "title": "OpenWeatherMap" + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 OpenWeatherMap. \u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043d\u0430 https://openweathermap.org/appid." } } }, diff --git a/homeassistant/components/openweathermap/translations/zh-Hant.json b/homeassistant/components/openweathermap/translations/zh-Hant.json index fcebc12fe31..407bd026291 100644 --- a/homeassistant/components/openweathermap/translations/zh-Hant.json +++ b/homeassistant/components/openweathermap/translations/zh-Hant.json @@ -17,8 +17,7 @@ "mode": "\u6a21\u5f0f", "name": "\u540d\u7a31" }, - "description": "\u8acb\u81f3 https://openweathermap.org/appid \u4ee5\u7522\u751f API \u91d1\u9470", - "title": "OpenWeatherMap" + "description": "\u8acb\u81f3 https://openweathermap.org/appid \u4ee5\u7522\u751f API \u91d1\u9470" } } }, diff --git a/homeassistant/components/p1_monitor/translations/bg.json b/homeassistant/components/p1_monitor/translations/bg.json index 5c4f6a7bdc3..acbebfb4d36 100644 --- a/homeassistant/components/p1_monitor/translations/bg.json +++ b/homeassistant/components/p1_monitor/translations/bg.json @@ -1,8 +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", - "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/ca.json b/homeassistant/components/p1_monitor/translations/ca.json index 8d83cfd5026..d82a93389fe 100644 --- a/homeassistant/components/p1_monitor/translations/ca.json +++ b/homeassistant/components/p1_monitor/translations/ca.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "Ha fallat la connexi\u00f3", - "unknown": "Error inesperat" + "cannot_connect": "Ha fallat la connexi\u00f3" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/cs.json b/homeassistant/components/p1_monitor/translations/cs.json index 7981cfc800e..7a27355056b 100644 --- a/homeassistant/components/p1_monitor/translations/cs.json +++ b/homeassistant/components/p1_monitor/translations/cs.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/de.json b/homeassistant/components/p1_monitor/translations/de.json index 31a19a7a195..8ac00192251 100644 --- a/homeassistant/components/p1_monitor/translations/de.json +++ b/homeassistant/components/p1_monitor/translations/de.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "Verbindung fehlgeschlagen", - "unknown": "Unerwarteter Fehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/el.json b/homeassistant/components/p1_monitor/translations/el.json index 82c79d53acc..fd520c381f8 100644 --- a/homeassistant/components/p1_monitor/translations/el.json +++ b/homeassistant/components/p1_monitor/translations/el.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "unknown": "\u0391\u03bd\u03b5\u03c0\u03ac\u03bd\u03c4\u03b5\u03c7\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/en.json b/homeassistant/components/p1_monitor/translations/en.json index 34b64082b43..4bd61c19bdc 100644 --- a/homeassistant/components/p1_monitor/translations/en.json +++ b/homeassistant/components/p1_monitor/translations/en.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "Failed to connect", - "unknown": "Unexpected error" + "cannot_connect": "Failed to connect" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/es.json b/homeassistant/components/p1_monitor/translations/es.json index 46bfbb59e4f..31976986ec4 100644 --- a/homeassistant/components/p1_monitor/translations/es.json +++ b/homeassistant/components/p1_monitor/translations/es.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "No se pudo conectar", - "unknown": "Error inesperado" + "cannot_connect": "No se pudo conectar" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/et.json b/homeassistant/components/p1_monitor/translations/et.json index 96ab4e46491..69090de7640 100644 --- a/homeassistant/components/p1_monitor/translations/et.json +++ b/homeassistant/components/p1_monitor/translations/et.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u00dchendamine nurjus", - "unknown": "Ootamatu t\u00f5rge" + "cannot_connect": "\u00dchendamine nurjus" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/fr.json b/homeassistant/components/p1_monitor/translations/fr.json index 34c0a37fb2e..34699234e0e 100644 --- a/homeassistant/components/p1_monitor/translations/fr.json +++ b/homeassistant/components/p1_monitor/translations/fr.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u00c9chec de connexion", - "unknown": "Erreur inattendue" + "cannot_connect": "\u00c9chec de connexion" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/he.json b/homeassistant/components/p1_monitor/translations/he.json index fbd52ea83ec..33660936e12 100644 --- a/homeassistant/components/p1_monitor/translations/he.json +++ b/homeassistant/components/p1_monitor/translations/he.json @@ -1,8 +1,7 @@ { "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" + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/hu.json b/homeassistant/components/p1_monitor/translations/hu.json index b0c30613234..af1f1e62fdb 100644 --- a/homeassistant/components/p1_monitor/translations/hu.json +++ b/homeassistant/components/p1_monitor/translations/hu.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "Nem siker\u00fclt csatlakozni", - "unknown": "V\u00e1ratlan hiba" + "cannot_connect": "Nem siker\u00fclt csatlakozni" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/id.json b/homeassistant/components/p1_monitor/translations/id.json index a1241576b2e..2deaf4c09e0 100644 --- a/homeassistant/components/p1_monitor/translations/id.json +++ b/homeassistant/components/p1_monitor/translations/id.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "Gagal terhubung", - "unknown": "Kesalahan yang tidak diharapkan" + "cannot_connect": "Gagal terhubung" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/it.json b/homeassistant/components/p1_monitor/translations/it.json index 25cac38aff6..61b6d9d75f0 100644 --- a/homeassistant/components/p1_monitor/translations/it.json +++ b/homeassistant/components/p1_monitor/translations/it.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "Impossibile connettersi", - "unknown": "Errore imprevisto" + "cannot_connect": "Impossibile connettersi" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/ja.json b/homeassistant/components/p1_monitor/translations/ja.json index b47610f27e3..29bfd50332f 100644 --- a/homeassistant/components/p1_monitor/translations/ja.json +++ b/homeassistant/components/p1_monitor/translations/ja.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/ko.json b/homeassistant/components/p1_monitor/translations/ko.json index 40f9f3b7152..e2d6ae5c199 100644 --- a/homeassistant/components/p1_monitor/translations/ko.json +++ b/homeassistant/components/p1_monitor/translations/ko.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc5d0\ub7ec" + "cannot_connect": "\uc5f0\uacb0 \uc2e4\ud328" } } } \ No newline at end of file diff --git a/homeassistant/components/p1_monitor/translations/nl.json b/homeassistant/components/p1_monitor/translations/nl.json index fbb81d70c5d..1f5e9e5a422 100644 --- a/homeassistant/components/p1_monitor/translations/nl.json +++ b/homeassistant/components/p1_monitor/translations/nl.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "Kan geen verbinding maken", - "unknown": "Onverwachte fout" + "cannot_connect": "Kan geen verbinding maken" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/no.json b/homeassistant/components/p1_monitor/translations/no.json index a6b967e4a46..d0827d0e357 100644 --- a/homeassistant/components/p1_monitor/translations/no.json +++ b/homeassistant/components/p1_monitor/translations/no.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "Tilkobling mislyktes", - "unknown": "Uventet feil" + "cannot_connect": "Tilkobling mislyktes" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/pl.json b/homeassistant/components/p1_monitor/translations/pl.json index 5aacfb63336..9ec0f1bcef0 100644 --- a/homeassistant/components/p1_monitor/translations/pl.json +++ b/homeassistant/components/p1_monitor/translations/pl.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "unknown": "Nieoczekiwany b\u0142\u0105d" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/pt-BR.json b/homeassistant/components/p1_monitor/translations/pt-BR.json index 777a1fc5633..dda88d419e6 100644 --- a/homeassistant/components/p1_monitor/translations/pt-BR.json +++ b/homeassistant/components/p1_monitor/translations/pt-BR.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "Falha ao conectar", - "unknown": "Erro inesperado" + "cannot_connect": "Falha ao conectar" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/ru.json b/homeassistant/components/p1_monitor/translations/ru.json index 78277de42c1..4f118fe952c 100644 --- a/homeassistant/components/p1_monitor/translations/ru.json +++ b/homeassistant/components/p1_monitor/translations/ru.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/tr.json b/homeassistant/components/p1_monitor/translations/tr.json index 88684e7bcb0..f00060462fd 100644 --- a/homeassistant/components/p1_monitor/translations/tr.json +++ b/homeassistant/components/p1_monitor/translations/tr.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131", - "unknown": "Beklenmeyen hata" + "cannot_connect": "Ba\u011flanma hatas\u0131" }, "step": { "user": { diff --git a/homeassistant/components/p1_monitor/translations/zh-Hant.json b/homeassistant/components/p1_monitor/translations/zh-Hant.json index fafb7f9b7c2..a62ff38bbda 100644 --- a/homeassistant/components/p1_monitor/translations/zh-Hant.json +++ b/homeassistant/components/p1_monitor/translations/zh-Hant.json @@ -1,8 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "step": { "user": { diff --git a/homeassistant/components/peco/translations/ca.json b/homeassistant/components/peco/translations/ca.json index 1cca8a08a7f..d66b3e67e3c 100644 --- a/homeassistant/components/peco/translations/ca.json +++ b/homeassistant/components/peco/translations/ca.json @@ -7,9 +7,7 @@ "user": { "data": { "county": "Comtat" - }, - "description": "Trieu el teu comtat a continuaci\u00f3.", - "title": "Comptador d'interrupcions PECO" + } } } } diff --git a/homeassistant/components/peco/translations/de.json b/homeassistant/components/peco/translations/de.json index 4eb10b3e5e9..2d2c9bca38c 100644 --- a/homeassistant/components/peco/translations/de.json +++ b/homeassistant/components/peco/translations/de.json @@ -7,9 +7,7 @@ "user": { "data": { "county": "Bundesland" - }, - "description": "Bitte w\u00e4hle unten dein Bundesland aus.", - "title": "PECO-St\u00f6rungsz\u00e4hler" + } } } } diff --git a/homeassistant/components/peco/translations/el.json b/homeassistant/components/peco/translations/el.json index 6b47cc3ccc1..9e912b952cc 100644 --- a/homeassistant/components/peco/translations/el.json +++ b/homeassistant/components/peco/translations/el.json @@ -7,9 +7,7 @@ "user": { "data": { "county": "\u039a\u03bf\u03bc\u03b7\u03c4\u03b5\u03af\u03b1" - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03bf\u03bc\u03b7\u03c4\u03b5\u03af\u03b1 \u03c3\u03b1\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9.", - "title": "\u039c\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae\u03c2 \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03ce\u03bd PECO" + } } } } diff --git a/homeassistant/components/peco/translations/en.json b/homeassistant/components/peco/translations/en.json index 60483a1d65c..6f7ff2b0b12 100644 --- a/homeassistant/components/peco/translations/en.json +++ b/homeassistant/components/peco/translations/en.json @@ -7,9 +7,7 @@ "user": { "data": { "county": "County" - }, - "description": "Please choose your county below.", - "title": "PECO Outage Counter" + } } } } diff --git a/homeassistant/components/peco/translations/et.json b/homeassistant/components/peco/translations/et.json index 117d0502ca3..0b9303808d8 100644 --- a/homeassistant/components/peco/translations/et.json +++ b/homeassistant/components/peco/translations/et.json @@ -7,9 +7,7 @@ "user": { "data": { "county": "Maakond" - }, - "description": "Vali allpool oma maakond.", - "title": "PECO katkestuste loendur" + } } } } diff --git a/homeassistant/components/peco/translations/fr.json b/homeassistant/components/peco/translations/fr.json index e78f828f1af..2063ae4e661 100644 --- a/homeassistant/components/peco/translations/fr.json +++ b/homeassistant/components/peco/translations/fr.json @@ -7,9 +7,7 @@ "user": { "data": { "county": "Comt\u00e9" - }, - "description": "Veuillez choisir votre comt\u00e9 ci-dessous.", - "title": "Compteur de pannes PECO" + } } } } diff --git a/homeassistant/components/peco/translations/hu.json b/homeassistant/components/peco/translations/hu.json index 2e1e3482eb5..4de9d9a122a 100644 --- a/homeassistant/components/peco/translations/hu.json +++ b/homeassistant/components/peco/translations/hu.json @@ -7,9 +7,7 @@ "user": { "data": { "county": "Megye" - }, - "description": "K\u00e9rj\u00fck, v\u00e1lassza ki az al\u00e1bbiakban a megy\u00e9t.", - "title": "PECO kimarad\u00e1s sz\u00e1ml\u00e1l\u00f3" + } } } } diff --git a/homeassistant/components/peco/translations/id.json b/homeassistant/components/peco/translations/id.json index 0879348f593..f222d4db975 100644 --- a/homeassistant/components/peco/translations/id.json +++ b/homeassistant/components/peco/translations/id.json @@ -7,9 +7,7 @@ "user": { "data": { "county": "Kota/kabupaten" - }, - "description": "Pilih kota/kabupaten Anda di bawah ini.", - "title": "Penghitung Pemadaman PECO" + } } } } diff --git a/homeassistant/components/peco/translations/it.json b/homeassistant/components/peco/translations/it.json index a7b76c27f43..fc18d732b56 100644 --- a/homeassistant/components/peco/translations/it.json +++ b/homeassistant/components/peco/translations/it.json @@ -7,9 +7,7 @@ "user": { "data": { "county": "Provincia" - }, - "description": "Scegli la tua provincia qui sotto.", - "title": "Contatore interruzioni PECO" + } } } } diff --git a/homeassistant/components/peco/translations/ja.json b/homeassistant/components/peco/translations/ja.json index 139e1f77bd9..b0da74e5c56 100644 --- a/homeassistant/components/peco/translations/ja.json +++ b/homeassistant/components/peco/translations/ja.json @@ -7,9 +7,7 @@ "user": { "data": { "county": "\u90e1" - }, - "description": "\u4ee5\u4e0b\u304b\u3089\u304a\u4f4f\u307e\u3044\u306e\u56fd\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "PECO Outage\u30ab\u30a6\u30f3\u30bf\u30fc" + } } } } diff --git a/homeassistant/components/peco/translations/nl.json b/homeassistant/components/peco/translations/nl.json index 8e98f5077e4..088ae8f0c32 100644 --- a/homeassistant/components/peco/translations/nl.json +++ b/homeassistant/components/peco/translations/nl.json @@ -7,9 +7,7 @@ "user": { "data": { "county": "County" - }, - "description": "Kies hieronder uw county", - "title": "PECO Uitval Teller" + } } } } diff --git a/homeassistant/components/peco/translations/no.json b/homeassistant/components/peco/translations/no.json index 00f9c025114..7b3906f4b67 100644 --- a/homeassistant/components/peco/translations/no.json +++ b/homeassistant/components/peco/translations/no.json @@ -7,9 +7,7 @@ "user": { "data": { "county": "fylke" - }, - "description": "Velg ditt fylke nedenfor.", - "title": "PECO avbruddsteller" + } } } } diff --git a/homeassistant/components/peco/translations/pl.json b/homeassistant/components/peco/translations/pl.json index bdd8a7ffb7c..e2a56bf1916 100644 --- a/homeassistant/components/peco/translations/pl.json +++ b/homeassistant/components/peco/translations/pl.json @@ -7,9 +7,7 @@ "user": { "data": { "county": "Hrabstwo" - }, - "description": "Wybierz swoje hrabstwo poni\u017cej.", - "title": "Licznik awarii PECO" + } } } } diff --git a/homeassistant/components/peco/translations/pt-BR.json b/homeassistant/components/peco/translations/pt-BR.json index 845baf5b479..5d4e35de90b 100644 --- a/homeassistant/components/peco/translations/pt-BR.json +++ b/homeassistant/components/peco/translations/pt-BR.json @@ -7,9 +7,7 @@ "user": { "data": { "county": "Munic\u00edpio" - }, - "description": "Por favor, escolha seu munic\u00edpio abaixo.", - "title": "Contador de Interrup\u00e7\u00e3o PECO" + } } } } diff --git a/homeassistant/components/peco/translations/ru.json b/homeassistant/components/peco/translations/ru.json index 6c2c8d85cc6..dc5b481fc7c 100644 --- a/homeassistant/components/peco/translations/ru.json +++ b/homeassistant/components/peco/translations/ru.json @@ -7,9 +7,7 @@ "user": { "data": { "county": "\u041e\u043a\u0440\u0443\u0433" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 \u043e\u043a\u0440\u0443\u0433.", - "title": "\u0421\u0447\u0435\u0442\u0447\u0438\u043a \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 PECO" + } } } } diff --git a/homeassistant/components/peco/translations/tr.json b/homeassistant/components/peco/translations/tr.json index 6a76e600789..97375e1f61f 100644 --- a/homeassistant/components/peco/translations/tr.json +++ b/homeassistant/components/peco/translations/tr.json @@ -7,9 +7,7 @@ "user": { "data": { "county": "\u0130l\u00e7e" - }, - "description": "L\u00fctfen a\u015fa\u011f\u0131dan il\u00e7enizi se\u00e7iniz.", - "title": "PECO Kesinti Sayac\u0131" + } } } } diff --git a/homeassistant/components/peco/translations/zh-Hant.json b/homeassistant/components/peco/translations/zh-Hant.json index 94318397f6f..69d9400d2f6 100644 --- a/homeassistant/components/peco/translations/zh-Hant.json +++ b/homeassistant/components/peco/translations/zh-Hant.json @@ -7,9 +7,7 @@ "user": { "data": { "county": "\u7e23\u5e02" - }, - "description": "\u8acb\u9078\u64c7\u7e23\u5e02\u3002", - "title": "PECO Outage \u8a08\u6578\u5668" + } } } } diff --git a/homeassistant/components/picnic/translations/ca.json b/homeassistant/components/picnic/translations/ca.json index 83c0b75f9d3..aefba6058c1 100644 --- a/homeassistant/components/picnic/translations/ca.json +++ b/homeassistant/components/picnic/translations/ca.json @@ -19,6 +19,5 @@ } } } - }, - "title": "Picnic" + } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/de.json b/homeassistant/components/picnic/translations/de.json index 65b10f61df3..6079f023d91 100644 --- a/homeassistant/components/picnic/translations/de.json +++ b/homeassistant/components/picnic/translations/de.json @@ -19,6 +19,5 @@ } } } - }, - "title": "Picnic" + } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/el.json b/homeassistant/components/picnic/translations/el.json index ba974da6225..42717397a6b 100644 --- a/homeassistant/components/picnic/translations/el.json +++ b/homeassistant/components/picnic/translations/el.json @@ -19,6 +19,5 @@ } } } - }, - "title": "Picnic" + } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/en.json b/homeassistant/components/picnic/translations/en.json index 06b3018f88e..13b62c78757 100644 --- a/homeassistant/components/picnic/translations/en.json +++ b/homeassistant/components/picnic/translations/en.json @@ -19,6 +19,5 @@ } } } - }, - "title": "Picnic" + } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/es.json b/homeassistant/components/picnic/translations/es.json index f7a170871ef..5054ae22f5b 100644 --- a/homeassistant/components/picnic/translations/es.json +++ b/homeassistant/components/picnic/translations/es.json @@ -19,6 +19,5 @@ } } } - }, - "title": "Picnic" + } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/et.json b/homeassistant/components/picnic/translations/et.json index 41f5018079c..e8d45971d37 100644 --- a/homeassistant/components/picnic/translations/et.json +++ b/homeassistant/components/picnic/translations/et.json @@ -19,6 +19,5 @@ } } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/fr.json b/homeassistant/components/picnic/translations/fr.json index 77d9756b5b8..fa9699417b5 100644 --- a/homeassistant/components/picnic/translations/fr.json +++ b/homeassistant/components/picnic/translations/fr.json @@ -19,6 +19,5 @@ } } } - }, - "title": "Pique-nique" + } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/he.json b/homeassistant/components/picnic/translations/he.json index 856ac220d64..f8c5c7a1b01 100644 --- a/homeassistant/components/picnic/translations/he.json +++ b/homeassistant/components/picnic/translations/he.json @@ -19,6 +19,5 @@ } } } - }, - "title": "\u05e4\u05d9\u05e7\u05e0\u05d9\u05e7" + } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/hu.json b/homeassistant/components/picnic/translations/hu.json index 3841b7ddbaf..99908bb47d2 100644 --- a/homeassistant/components/picnic/translations/hu.json +++ b/homeassistant/components/picnic/translations/hu.json @@ -19,6 +19,5 @@ } } } - }, - "title": "Piknik" + } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/id.json b/homeassistant/components/picnic/translations/id.json index db97b991f6f..576f3b8a0e8 100644 --- a/homeassistant/components/picnic/translations/id.json +++ b/homeassistant/components/picnic/translations/id.json @@ -19,6 +19,5 @@ } } } - }, - "title": "Picnic" + } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/it.json b/homeassistant/components/picnic/translations/it.json index 209b6d6fdb9..f0495ee8d17 100644 --- a/homeassistant/components/picnic/translations/it.json +++ b/homeassistant/components/picnic/translations/it.json @@ -19,6 +19,5 @@ } } } - }, - "title": "Picnic" + } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/ja.json b/homeassistant/components/picnic/translations/ja.json index 194cffd7e6a..fd9fe67db73 100644 --- a/homeassistant/components/picnic/translations/ja.json +++ b/homeassistant/components/picnic/translations/ja.json @@ -19,6 +19,5 @@ } } } - }, - "title": "Picnic" + } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/nl.json b/homeassistant/components/picnic/translations/nl.json index dc040fc03d0..1d007e5eb43 100644 --- a/homeassistant/components/picnic/translations/nl.json +++ b/homeassistant/components/picnic/translations/nl.json @@ -19,6 +19,5 @@ } } } - }, - "title": "Picnic" + } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/no.json b/homeassistant/components/picnic/translations/no.json index ffd38bce705..1ebb4b8dece 100644 --- a/homeassistant/components/picnic/translations/no.json +++ b/homeassistant/components/picnic/translations/no.json @@ -19,6 +19,5 @@ } } } - }, - "title": "Picnic" + } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/pl.json b/homeassistant/components/picnic/translations/pl.json index abf8a4f9469..f45a8520a14 100644 --- a/homeassistant/components/picnic/translations/pl.json +++ b/homeassistant/components/picnic/translations/pl.json @@ -19,6 +19,5 @@ } } } - }, - "title": "Picnic" + } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/pt-BR.json b/homeassistant/components/picnic/translations/pt-BR.json index b864d13923d..296ca588e28 100644 --- a/homeassistant/components/picnic/translations/pt-BR.json +++ b/homeassistant/components/picnic/translations/pt-BR.json @@ -19,6 +19,5 @@ } } } - }, - "title": "Picnic" + } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/ru.json b/homeassistant/components/picnic/translations/ru.json index 9d7a7fbdb23..2f912b07667 100644 --- a/homeassistant/components/picnic/translations/ru.json +++ b/homeassistant/components/picnic/translations/ru.json @@ -19,6 +19,5 @@ } } } - }, - "title": "Picnic" + } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/tr.json b/homeassistant/components/picnic/translations/tr.json index b689f65ff96..a48dc699dd0 100644 --- a/homeassistant/components/picnic/translations/tr.json +++ b/homeassistant/components/picnic/translations/tr.json @@ -19,6 +19,5 @@ } } } - }, - "title": "Picnic" + } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/zh-Hant.json b/homeassistant/components/picnic/translations/zh-Hant.json index a82f4ace04d..99184422cef 100644 --- a/homeassistant/components/picnic/translations/zh-Hant.json +++ b/homeassistant/components/picnic/translations/zh-Hant.json @@ -19,6 +19,5 @@ } } } - }, - "title": "Picnic" + } } \ No newline at end of file diff --git a/homeassistant/components/plex/translations/bg.json b/homeassistant/components/plex/translations/bg.json index d1b1867c246..0e39e4b8b04 100644 --- a/homeassistant/components/plex/translations/bg.json +++ b/homeassistant/components/plex/translations/bg.json @@ -27,12 +27,6 @@ }, "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" } } }, diff --git a/homeassistant/components/plex/translations/ca.json b/homeassistant/components/plex/translations/ca.json index 6cad3cb9d6e..b917a81a772 100644 --- a/homeassistant/components/plex/translations/ca.json +++ b/homeassistant/components/plex/translations/ca.json @@ -35,14 +35,12 @@ "title": "Selecciona servidor Plex" }, "user": { - "description": "V\u00e9s a [plex.tv](https://plex.tv) per enlla\u00e7ar un servidor Plex.", - "title": "Servidor Multim\u00e8dia Plex" + "description": "V\u00e9s a [plex.tv](https://plex.tv) per enlla\u00e7ar un servidor Plex." }, "user_advanced": { "data": { "setup_method": "M\u00e8tode de configuraci\u00f3" - }, - "title": "Servidor Multim\u00e8dia Plex" + } } } }, diff --git a/homeassistant/components/plex/translations/cs.json b/homeassistant/components/plex/translations/cs.json index f5e7e538f84..851a8782023 100644 --- a/homeassistant/components/plex/translations/cs.json +++ b/homeassistant/components/plex/translations/cs.json @@ -35,14 +35,12 @@ "title": "Vyberte server Plex" }, "user": { - "description": "Pro propojen\u00ed Plex serveru, pokra\u010dujte na [plex.tv](https://plex.tv).", - "title": "Plex Media Server" + "description": "Pro propojen\u00ed Plex serveru, pokra\u010dujte na [plex.tv](https://plex.tv)." }, "user_advanced": { "data": { "setup_method": "Metoda nastaven\u00ed" - }, - "title": "Plex Media Server" + } } } }, diff --git a/homeassistant/components/plex/translations/de.json b/homeassistant/components/plex/translations/de.json index 130d34505d2..3eb1c1b7707 100644 --- a/homeassistant/components/plex/translations/de.json +++ b/homeassistant/components/plex/translations/de.json @@ -35,14 +35,12 @@ "title": "Plex-Server ausw\u00e4hlen" }, "user": { - "description": "Gehe zu [plex.tv] (https://plex.tv), um einen Plex-Server zu verbinden", - "title": "Plex Media Server" + "description": "Gehe zu [plex.tv] (https://plex.tv), um einen Plex-Server zu verbinden" }, "user_advanced": { "data": { "setup_method": "Einrichtungsmethode" - }, - "title": "Plex Media Server" + } } } }, diff --git a/homeassistant/components/plex/translations/el.json b/homeassistant/components/plex/translations/el.json index 679ef3d3937..cd2147b51f4 100644 --- a/homeassistant/components/plex/translations/el.json +++ b/homeassistant/components/plex/translations/el.json @@ -35,14 +35,12 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Plex" }, "user": { - "description": "\u03a3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03c4\u03b5 \u03c3\u03c4\u03bf [plex.tv](https://plex.tv) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Plex.", - "title": "\u0394\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 Plex Media" + "description": "\u03a3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03c4\u03b5 \u03c3\u03c4\u03bf [plex.tv](https://plex.tv) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Plex." }, "user_advanced": { "data": { "setup_method": "\u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2" - }, - "title": "\u0394\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 Plex Media" + } } } }, diff --git a/homeassistant/components/plex/translations/en.json b/homeassistant/components/plex/translations/en.json index 834594e1d27..1bd738ea10e 100644 --- a/homeassistant/components/plex/translations/en.json +++ b/homeassistant/components/plex/translations/en.json @@ -35,14 +35,12 @@ "title": "Select Plex server" }, "user": { - "description": "Continue to [plex.tv](https://plex.tv) to link a Plex server.", - "title": "Plex Media Server" + "description": "Continue to [plex.tv](https://plex.tv) to link a Plex server." }, "user_advanced": { "data": { "setup_method": "Setup method" - }, - "title": "Plex Media Server" + } } } }, diff --git a/homeassistant/components/plex/translations/es.json b/homeassistant/components/plex/translations/es.json index 38f57b91c7b..7099f63a9e8 100644 --- a/homeassistant/components/plex/translations/es.json +++ b/homeassistant/components/plex/translations/es.json @@ -35,14 +35,12 @@ "title": "Seleccione el servidor Plex" }, "user": { - "description": "Continuar hacia [plex.tv](https://plex.tv) para vincular un servidor Plex.", - "title": "Plex Media Server" + "description": "Continuar hacia [plex.tv](https://plex.tv) para vincular un servidor Plex." }, "user_advanced": { "data": { "setup_method": "M\u00e9todo de configuraci\u00f3n" - }, - "title": "Plex Media Server" + } } } }, diff --git a/homeassistant/components/plex/translations/et.json b/homeassistant/components/plex/translations/et.json index d815a092bca..31e6084146d 100644 --- a/homeassistant/components/plex/translations/et.json +++ b/homeassistant/components/plex/translations/et.json @@ -35,14 +35,12 @@ "title": "Vali Plex'i server" }, "user": { - "description": "Plexi serveri linkimiseks mine lehele [plex.tv] (https://plex.tv).", - "title": "" + "description": "Plexi serveri linkimiseks mine lehele [plex.tv] (https://plex.tv)." }, "user_advanced": { "data": { "setup_method": "Seadistusmeetod" - }, - "title": "" + } } } }, diff --git a/homeassistant/components/plex/translations/fi.json b/homeassistant/components/plex/translations/fi.json index 2c5872fe85a..4900dbbbec9 100644 --- a/homeassistant/components/plex/translations/fi.json +++ b/homeassistant/components/plex/translations/fi.json @@ -10,14 +10,10 @@ }, "title": "Manuaalinen Plex-konfigurointi" }, - "user": { - "title": "Plex Media Server" - }, "user_advanced": { "data": { "setup_method": "Asennusmenetelm\u00e4" - }, - "title": "Plex-mediapalvelin" + } } } }, diff --git a/homeassistant/components/plex/translations/fr.json b/homeassistant/components/plex/translations/fr.json index 73704d31c7a..e4c711afd8a 100644 --- a/homeassistant/components/plex/translations/fr.json +++ b/homeassistant/components/plex/translations/fr.json @@ -35,14 +35,12 @@ "title": "S\u00e9lectionnez le serveur Plex" }, "user": { - "description": "Continuez sur [plex.tv] (https://plex.tv) pour lier un serveur Plex.", - "title": "Plex Media Server" + "description": "Continuez sur [plex.tv] (https://plex.tv) pour lier un serveur Plex." }, "user_advanced": { "data": { "setup_method": "M\u00e9thode de configuration" - }, - "title": "Plex Media Server" + } } } }, diff --git a/homeassistant/components/plex/translations/hu.json b/homeassistant/components/plex/translations/hu.json index 3be797cc04f..3015494af0a 100644 --- a/homeassistant/components/plex/translations/hu.json +++ b/homeassistant/components/plex/translations/hu.json @@ -35,14 +35,12 @@ "title": "Plex-kiszolg\u00e1l\u00f3 kiv\u00e1laszt\u00e1sa" }, "user": { - "description": "Folytassa a [plex.tv] (https://plex.tv) oldalt a Plex szerver \u00f6sszekapcsol\u00e1s\u00e1hoz.", - "title": "Plex Media Server" + "description": "Folytassa a [plex.tv] (https://plex.tv) oldalt a Plex szerver \u00f6sszekapcsol\u00e1s\u00e1hoz." }, "user_advanced": { "data": { "setup_method": "Be\u00e1ll\u00edt\u00e1si m\u00f3dszer" - }, - "title": "Plex Media Server" + } } } }, diff --git a/homeassistant/components/plex/translations/id.json b/homeassistant/components/plex/translations/id.json index 7c596835e03..ccc2786ae54 100644 --- a/homeassistant/components/plex/translations/id.json +++ b/homeassistant/components/plex/translations/id.json @@ -35,14 +35,12 @@ "title": "Pilih server Plex" }, "user": { - "description": "Lanjutkan [plex.tv](https://plex.tv) untuk menautkan server Plex.", - "title": "Server Media Plex" + "description": "Lanjutkan [plex.tv](https://plex.tv) untuk menautkan server Plex." }, "user_advanced": { "data": { "setup_method": "Metode penyiapan" - }, - "title": "Server Media Plex" + } } } }, diff --git a/homeassistant/components/plex/translations/it.json b/homeassistant/components/plex/translations/it.json index 8fb0603a8ee..876ab634523 100644 --- a/homeassistant/components/plex/translations/it.json +++ b/homeassistant/components/plex/translations/it.json @@ -35,14 +35,12 @@ "title": "Seleziona il server Plex" }, "user": { - "description": "Continua su [plex.tv](https://plex.tv) per collegare un server Plex.", - "title": "Plex Media Server" + "description": "Continua su [plex.tv](https://plex.tv) per collegare un server Plex." }, "user_advanced": { "data": { "setup_method": "Metodo di impostazione" - }, - "title": "Plex Media Server" + } } } }, diff --git a/homeassistant/components/plex/translations/ja.json b/homeassistant/components/plex/translations/ja.json index 4b48bdfe695..be42bab1d85 100644 --- a/homeassistant/components/plex/translations/ja.json +++ b/homeassistant/components/plex/translations/ja.json @@ -35,14 +35,12 @@ "title": "Plex\u30b5\u30fc\u30d0\u30fc\u3092\u9078\u629e" }, "user": { - "description": "[plex.tv](https://plex.tv) \u306b\u9032\u307f\u3001Plex server\u3092\u30ea\u30f3\u30af\u3057\u307e\u3059\u3002", - "title": "Plex Media Server" + "description": "[plex.tv](https://plex.tv) \u306b\u9032\u307f\u3001Plex server\u3092\u30ea\u30f3\u30af\u3057\u307e\u3059\u3002" }, "user_advanced": { "data": { "setup_method": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u65b9\u6cd5" - }, - "title": "Plex Media Server" + } } } }, diff --git a/homeassistant/components/plex/translations/ko.json b/homeassistant/components/plex/translations/ko.json index d6b0c6a2341..453f32ee15b 100644 --- a/homeassistant/components/plex/translations/ko.json +++ b/homeassistant/components/plex/translations/ko.json @@ -35,14 +35,12 @@ "title": "Plex \uc11c\ubc84 \uc120\ud0dd\ud558\uae30" }, "user": { - "description": "Plex \uc11c\ubc84\ub97c \uc5f0\uacb0\ud558\ub824\uba74 [plex.tv](https://plex.tv)\ub85c \uacc4\uc18d \uc9c4\ud589\ud574\uc8fc\uc138\uc694.", - "title": "Plex \ubbf8\ub514\uc5b4 \uc11c\ubc84" + "description": "Plex \uc11c\ubc84\ub97c \uc5f0\uacb0\ud558\ub824\uba74 [plex.tv](https://plex.tv)\ub85c \uacc4\uc18d \uc9c4\ud589\ud574\uc8fc\uc138\uc694." }, "user_advanced": { "data": { "setup_method": "\uc124\uc815 \ubc29\ubc95" - }, - "title": "Plex \ubbf8\ub514\uc5b4 \uc11c\ubc84" + } } } }, diff --git a/homeassistant/components/plex/translations/lb.json b/homeassistant/components/plex/translations/lb.json index 916bcbc9042..d623b7cc011 100644 --- a/homeassistant/components/plex/translations/lb.json +++ b/homeassistant/components/plex/translations/lb.json @@ -35,14 +35,12 @@ "title": "Plex Server auswielen" }, "user": { - "description": "Verbann dech mat [plex.tv](https://pley.tv) fir ee Plex Server ze verlinken.", - "title": "Plex Media Server" + "description": "Verbann dech mat [plex.tv](https://pley.tv) fir ee Plex Server ze verlinken." }, "user_advanced": { "data": { "setup_method": "Setup Method" - }, - "title": "Plex Media Server" + } } } }, diff --git a/homeassistant/components/plex/translations/nl.json b/homeassistant/components/plex/translations/nl.json index a196555e7ea..afdc67007bd 100644 --- a/homeassistant/components/plex/translations/nl.json +++ b/homeassistant/components/plex/translations/nl.json @@ -35,14 +35,12 @@ "title": "Selecteer Plex server" }, "user": { - "description": "Ga verder naar [plex.tv] (https://plex.tv) om een Plex-server te koppelen.", - "title": "Plex Media Server" + "description": "Ga verder naar [plex.tv] (https://plex.tv) om een Plex-server te koppelen." }, "user_advanced": { "data": { "setup_method": "Installatiemethode" - }, - "title": "Plex Media Server" + } } } }, diff --git a/homeassistant/components/plex/translations/no.json b/homeassistant/components/plex/translations/no.json index ddd6ef4cdb2..c34b4b1c257 100644 --- a/homeassistant/components/plex/translations/no.json +++ b/homeassistant/components/plex/translations/no.json @@ -35,14 +35,12 @@ "title": "Velg Plex-server" }, "user": { - "description": "Fortsett til [plex.tv] (https://plex.tv) for \u00e5 koble en Plex-server.", - "title": "" + "description": "Fortsett til [plex.tv] (https://plex.tv) for \u00e5 koble en Plex-server." }, "user_advanced": { "data": { "setup_method": "Oppsettmetode" - }, - "title": "" + } } } }, diff --git a/homeassistant/components/plex/translations/pl.json b/homeassistant/components/plex/translations/pl.json index bd3e45da688..d4648973a5f 100644 --- a/homeassistant/components/plex/translations/pl.json +++ b/homeassistant/components/plex/translations/pl.json @@ -35,14 +35,12 @@ "title": "Wybierz serwer Plex" }, "user": { - "description": "Przejd\u017a do [plex.tv](https://plex.tv), aby po\u0142\u0105czy\u0107 serwer Plex.", - "title": "Serwer medi\u00f3w Plex" + "description": "Przejd\u017a do [plex.tv](https://plex.tv), aby po\u0142\u0105czy\u0107 serwer Plex." }, "user_advanced": { "data": { "setup_method": "Metoda konfiguracji" - }, - "title": "Serwer medi\u00f3w Plex" + } } } }, diff --git a/homeassistant/components/plex/translations/pt-BR.json b/homeassistant/components/plex/translations/pt-BR.json index ea74d2b173b..f8e9aa7dea4 100644 --- a/homeassistant/components/plex/translations/pt-BR.json +++ b/homeassistant/components/plex/translations/pt-BR.json @@ -35,14 +35,12 @@ "title": "Selecione servidor Plex" }, "user": { - "description": "Continue para [plex.tv](https://plex.tv) para vincular um servidor Plex.", - "title": "Plex Media Server" + "description": "Continue para [plex.tv](https://plex.tv) para vincular um servidor Plex." }, "user_advanced": { "data": { "setup_method": "M\u00e9todo de configura\u00e7\u00e3o" - }, - "title": "Plex Media Server" + } } } }, diff --git a/homeassistant/components/plex/translations/ru.json b/homeassistant/components/plex/translations/ru.json index b1530f0808b..fa05661f29c 100644 --- a/homeassistant/components/plex/translations/ru.json +++ b/homeassistant/components/plex/translations/ru.json @@ -35,14 +35,12 @@ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 Plex" }, "user": { - "description": "\u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 [plex.tv](https://plex.tv), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u043a Home Assistant.", - "title": "Plex Media Server" + "description": "\u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 [plex.tv](https://plex.tv), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u043a Home Assistant." }, "user_advanced": { "data": { "setup_method": "\u0421\u043f\u043e\u0441\u043e\u0431 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" - }, - "title": "Plex Media Server" + } } } }, diff --git a/homeassistant/components/plex/translations/sl.json b/homeassistant/components/plex/translations/sl.json index b1622219402..7d47584aad6 100644 --- a/homeassistant/components/plex/translations/sl.json +++ b/homeassistant/components/plex/translations/sl.json @@ -33,14 +33,12 @@ "title": "Izberite stre\u017enik Plex" }, "user": { - "description": "Nadaljujte do [plex.tv] (https://plex.tv), da pove\u017eete stre\u017enik Plex.", - "title": "Plex medijski stre\u017enik" + "description": "Nadaljujte do [plex.tv] (https://plex.tv), da pove\u017eete stre\u017enik Plex." }, "user_advanced": { "data": { "setup_method": "Na\u010din nastavitve" - }, - "title": "Plex medijski stre\u017enik" + } } } }, diff --git a/homeassistant/components/plex/translations/sv.json b/homeassistant/components/plex/translations/sv.json index 6ca36d302af..63b12e70e40 100644 --- a/homeassistant/components/plex/translations/sv.json +++ b/homeassistant/components/plex/translations/sv.json @@ -25,14 +25,10 @@ "description": "V\u00e4lj flera servrar tillg\u00e4ngliga, v\u00e4lj en:", "title": "V\u00e4lj Plex-server" }, - "user": { - "title": "Plex Media Server" - }, "user_advanced": { "data": { "setup_method": "Inst\u00e4llningsmetod" - }, - "title": "Plex Media Server" + } } } }, diff --git a/homeassistant/components/plex/translations/tr.json b/homeassistant/components/plex/translations/tr.json index c052006687f..423fc1ddbcb 100644 --- a/homeassistant/components/plex/translations/tr.json +++ b/homeassistant/components/plex/translations/tr.json @@ -35,14 +35,12 @@ "title": "Plex sunucusunu se\u00e7in" }, "user": { - "description": "Bir Plex sunucusunu ba\u011flamak i\u00e7in [plex.tv](https://plex.tv) ile devam edin.", - "title": "Plex Medya Sunucusu" + "description": "Bir Plex sunucusunu ba\u011flamak i\u00e7in [plex.tv](https://plex.tv) ile devam edin." }, "user_advanced": { "data": { "setup_method": "Kurulum y\u00f6ntemi" - }, - "title": "Plex Medya Sunucusu" + } } } }, diff --git a/homeassistant/components/plex/translations/uk.json b/homeassistant/components/plex/translations/uk.json index 20351cf735a..16ac314c1ad 100644 --- a/homeassistant/components/plex/translations/uk.json +++ b/homeassistant/components/plex/translations/uk.json @@ -35,14 +35,12 @@ "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 Plex" }, "user": { - "description": "\u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043d\u0430 [plex.tv](https://plex.tv), \u0449\u043e\u0431 \u043f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0434\u043e Home Assistant.", - "title": "Plex Media Server" + "description": "\u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043d\u0430 [plex.tv](https://plex.tv), \u0449\u043e\u0431 \u043f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0434\u043e Home Assistant." }, "user_advanced": { "data": { "setup_method": "\u0421\u043f\u043e\u0441\u0456\u0431 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f" - }, - "title": "Plex Media Server" + } } } }, diff --git a/homeassistant/components/plex/translations/zh-Hans.json b/homeassistant/components/plex/translations/zh-Hans.json index 02f548f2286..877543e90b0 100644 --- a/homeassistant/components/plex/translations/zh-Hans.json +++ b/homeassistant/components/plex/translations/zh-Hans.json @@ -32,14 +32,10 @@ "description": "\u6709\u591a\u4e2a\u53ef\u7528\u670d\u52a1\u5668\uff0c\u8bf7\u9009\u62e9\uff1a", "title": "\u9009\u62e9 Plex \u670d\u52a1\u5668" }, - "user": { - "title": "Plex \u5a92\u4f53\u670d\u52a1\u5668" - }, "user_advanced": { "data": { "setup_method": "\u8bbe\u7f6e\u65b9\u6cd5" - }, - "title": "Plex \u5a92\u4f53\u670d\u52a1\u5668" + } } } }, diff --git a/homeassistant/components/plex/translations/zh-Hant.json b/homeassistant/components/plex/translations/zh-Hant.json index 7f19fa0d035..7f05dc4b3be 100644 --- a/homeassistant/components/plex/translations/zh-Hant.json +++ b/homeassistant/components/plex/translations/zh-Hant.json @@ -35,14 +35,12 @@ "title": "\u9078\u64c7 Plex \u4f3a\u670d\u5668" }, "user": { - "description": "\u7e7c\u7e8c\u81f3 [plex.tv](https://plex.tv) \u4ee5\u9023\u7d50\u4e00\u7d44 Plex \u4f3a\u670d\u5668\u3002", - "title": "Plex \u5a92\u9ad4\u4f3a\u670d\u5668" + "description": "\u7e7c\u7e8c\u81f3 [plex.tv](https://plex.tv) \u4ee5\u9023\u7d50\u4e00\u7d44 Plex \u4f3a\u670d\u5668\u3002" }, "user_advanced": { "data": { "setup_method": "\u8a2d\u5b9a\u6a21\u5f0f" - }, - "title": "Plex \u5a92\u9ad4\u4f3a\u670d\u5668" + } } } }, diff --git a/homeassistant/components/poolsense/translations/bg.json b/homeassistant/components/poolsense/translations/bg.json index a89cca15270..eb033e74f0f 100644 --- a/homeassistant/components/poolsense/translations/bg.json +++ b/homeassistant/components/poolsense/translations/bg.json @@ -11,9 +11,7 @@ "data": { "email": "Email", "password": "\u041f\u0430\u0440\u043e\u043b\u0430" - }, - "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\u0432\u0430\u043d\u0435\u0442\u043e?", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/ca.json b/homeassistant/components/poolsense/translations/ca.json index 923aedcee66..d5d996f444a 100644 --- a/homeassistant/components/poolsense/translations/ca.json +++ b/homeassistant/components/poolsense/translations/ca.json @@ -11,9 +11,7 @@ "data": { "email": "Correu electr\u00f2nic", "password": "Contrasenya" - }, - "description": "Vols comen\u00e7ar la configuraci\u00f3?", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/cs.json b/homeassistant/components/poolsense/translations/cs.json index d4d94bd7334..520720ed5a1 100644 --- a/homeassistant/components/poolsense/translations/cs.json +++ b/homeassistant/components/poolsense/translations/cs.json @@ -11,9 +11,7 @@ "data": { "email": "E-mail", "password": "Heslo" - }, - "description": "Chcete za\u010d\u00edt nastavovat?", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/de.json b/homeassistant/components/poolsense/translations/de.json index 5ce9313b442..d19b110ff96 100644 --- a/homeassistant/components/poolsense/translations/de.json +++ b/homeassistant/components/poolsense/translations/de.json @@ -11,9 +11,7 @@ "data": { "email": "E-Mail", "password": "Passwort" - }, - "description": "M\u00f6chtest Du mit der Einrichtung beginnen?", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/el.json b/homeassistant/components/poolsense/translations/el.json index bb70337158e..f3fe349d939 100644 --- a/homeassistant/components/poolsense/translations/el.json +++ b/homeassistant/components/poolsense/translations/el.json @@ -11,9 +11,7 @@ "data": { "email": "Email", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" - }, - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7;", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/en.json b/homeassistant/components/poolsense/translations/en.json index 5203d6228c4..9e78fd62910 100644 --- a/homeassistant/components/poolsense/translations/en.json +++ b/homeassistant/components/poolsense/translations/en.json @@ -11,9 +11,7 @@ "data": { "email": "Email", "password": "Password" - }, - "description": "Do you want to start set up?", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/es.json b/homeassistant/components/poolsense/translations/es.json index 2d38dd670d3..d4b68388db2 100644 --- a/homeassistant/components/poolsense/translations/es.json +++ b/homeassistant/components/poolsense/translations/es.json @@ -11,9 +11,7 @@ "data": { "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a" - }, - "description": "\u00bfQuieres empezar la configuraci\u00f3n?", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/et.json b/homeassistant/components/poolsense/translations/et.json index 4e1dc8db1ff..57c3b8aef6e 100644 --- a/homeassistant/components/poolsense/translations/et.json +++ b/homeassistant/components/poolsense/translations/et.json @@ -11,9 +11,7 @@ "data": { "email": "E-post", "password": "Salas\u00f5na" - }, - "description": "Kas alustan seadistamist?", - "title": "" + } } } } diff --git a/homeassistant/components/poolsense/translations/fr.json b/homeassistant/components/poolsense/translations/fr.json index 4a32776bccb..dda4b0da753 100644 --- a/homeassistant/components/poolsense/translations/fr.json +++ b/homeassistant/components/poolsense/translations/fr.json @@ -11,9 +11,7 @@ "data": { "email": "Courriel", "password": "Mot de passe" - }, - "description": "Voulez-vous commencer la configuration\u00a0?", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/he.json b/homeassistant/components/poolsense/translations/he.json index f285e1ec479..8c832faf350 100644 --- a/homeassistant/components/poolsense/translations/he.json +++ b/homeassistant/components/poolsense/translations/he.json @@ -11,8 +11,7 @@ "data": { "email": "\u05d3\u05d5\u05d0\"\u05dc", "password": "\u05e1\u05d9\u05e1\u05de\u05d4" - }, - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } } } } diff --git a/homeassistant/components/poolsense/translations/hu.json b/homeassistant/components/poolsense/translations/hu.json index 39274e14c21..3a80610269b 100644 --- a/homeassistant/components/poolsense/translations/hu.json +++ b/homeassistant/components/poolsense/translations/hu.json @@ -11,9 +11,7 @@ "data": { "email": "E-mail", "password": "Jelsz\u00f3" - }, - "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/id.json b/homeassistant/components/poolsense/translations/id.json index 6e40f5f0925..3cf0dcd65bb 100644 --- a/homeassistant/components/poolsense/translations/id.json +++ b/homeassistant/components/poolsense/translations/id.json @@ -11,9 +11,7 @@ "data": { "email": "Email", "password": "Kata Sandi" - }, - "description": "Ingin memulai penyiapan?", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/it.json b/homeassistant/components/poolsense/translations/it.json index e6ea298e31d..d4a55b4a768 100644 --- a/homeassistant/components/poolsense/translations/it.json +++ b/homeassistant/components/poolsense/translations/it.json @@ -11,9 +11,7 @@ "data": { "email": "Email", "password": "Password" - }, - "description": "Vuoi iniziare la configurazione?", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/ja.json b/homeassistant/components/poolsense/translations/ja.json index 28d7e1d3d18..63dee3f0739 100644 --- a/homeassistant/components/poolsense/translations/ja.json +++ b/homeassistant/components/poolsense/translations/ja.json @@ -11,9 +11,7 @@ "data": { "email": "E\u30e1\u30fc\u30eb", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - }, - "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/ko.json b/homeassistant/components/poolsense/translations/ko.json index ec8c7dfc90f..d649b93e3ae 100644 --- a/homeassistant/components/poolsense/translations/ko.json +++ b/homeassistant/components/poolsense/translations/ko.json @@ -11,9 +11,7 @@ "data": { "email": "\uc774\uba54\uc77c", "password": "\ube44\ubc00\ubc88\ud638" - }, - "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/lb.json b/homeassistant/components/poolsense/translations/lb.json index 4d7065e6380..2a671d8e1de 100644 --- a/homeassistant/components/poolsense/translations/lb.json +++ b/homeassistant/components/poolsense/translations/lb.json @@ -11,9 +11,7 @@ "data": { "email": "E-Mail", "password": "Passwuert" - }, - "description": "Soll den Ariichtungs Prozess gestart ginn?", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/nl.json b/homeassistant/components/poolsense/translations/nl.json index 1fd59ebf2ea..46fc915d7bd 100644 --- a/homeassistant/components/poolsense/translations/nl.json +++ b/homeassistant/components/poolsense/translations/nl.json @@ -11,9 +11,7 @@ "data": { "email": "E-mail", "password": "Wachtwoord" - }, - "description": "Wilt u beginnen met instellen?", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/no.json b/homeassistant/components/poolsense/translations/no.json index e2873a36080..3b1ab46d773 100644 --- a/homeassistant/components/poolsense/translations/no.json +++ b/homeassistant/components/poolsense/translations/no.json @@ -11,9 +11,7 @@ "data": { "email": "E-post", "password": "Passord" - }, - "description": "Vil du starte oppsettet?", - "title": "" + } } } } diff --git a/homeassistant/components/poolsense/translations/pl.json b/homeassistant/components/poolsense/translations/pl.json index 6f87cacce4f..e29ac245760 100644 --- a/homeassistant/components/poolsense/translations/pl.json +++ b/homeassistant/components/poolsense/translations/pl.json @@ -11,9 +11,7 @@ "data": { "email": "Adres e-mail", "password": "Has\u0142o" - }, - "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/pt-BR.json b/homeassistant/components/poolsense/translations/pt-BR.json index 8b2e2bddda0..3fa0e51875e 100644 --- a/homeassistant/components/poolsense/translations/pt-BR.json +++ b/homeassistant/components/poolsense/translations/pt-BR.json @@ -11,9 +11,7 @@ "data": { "email": "Email", "password": "Senha" - }, - "description": "Deseja iniciar a configura\u00e7\u00e3o?", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/pt.json b/homeassistant/components/poolsense/translations/pt.json index 0dc8303b485..cc2666db4c5 100644 --- a/homeassistant/components/poolsense/translations/pt.json +++ b/homeassistant/components/poolsense/translations/pt.json @@ -11,9 +11,7 @@ "data": { "email": "Email", "password": "Palavra-passe" - }, - "description": "[%key:common::config_flow::description%]", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/ru.json b/homeassistant/components/poolsense/translations/ru.json index 09c94368cda..90911c39d8d 100644 --- a/homeassistant/components/poolsense/translations/ru.json +++ b/homeassistant/components/poolsense/translations/ru.json @@ -11,9 +11,7 @@ "data": { "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", "password": "\u041f\u0430\u0440\u043e\u043b\u044c" - }, - "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/sv.json b/homeassistant/components/poolsense/translations/sv.json deleted file mode 100644 index ad96ce6310d..00000000000 --- a/homeassistant/components/poolsense/translations/sv.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "config": { - "step": { - "user": { - "description": "Vill du starta konfigurationen?" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/tr.json b/homeassistant/components/poolsense/translations/tr.json index e1b4f150e92..1e2e9d0c5b8 100644 --- a/homeassistant/components/poolsense/translations/tr.json +++ b/homeassistant/components/poolsense/translations/tr.json @@ -11,9 +11,7 @@ "data": { "email": "E-posta", "password": "Parola" - }, - "description": "Kuruluma ba\u015flamak ister misiniz?", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/uk.json b/homeassistant/components/poolsense/translations/uk.json index 6ac3b97f741..9723921f479 100644 --- a/homeassistant/components/poolsense/translations/uk.json +++ b/homeassistant/components/poolsense/translations/uk.json @@ -11,9 +11,7 @@ "data": { "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438", "password": "\u041f\u0430\u0440\u043e\u043b\u044c" - }, - "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/poolsense/translations/zh-Hant.json b/homeassistant/components/poolsense/translations/zh-Hant.json index 62ffca35e9f..957a11cfd61 100644 --- a/homeassistant/components/poolsense/translations/zh-Hant.json +++ b/homeassistant/components/poolsense/translations/zh-Hant.json @@ -11,9 +11,7 @@ "data": { "email": "\u96fb\u5b50\u90f5\u4ef6", "password": "\u5bc6\u78bc" - }, - "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f", - "title": "PoolSense" + } } } } diff --git a/homeassistant/components/ps4/translations/bg.json b/homeassistant/components/ps4/translations/bg.json index 8ed9242fc3e..ac2b20066f4 100644 --- a/homeassistant/components/ps4/translations/bg.json +++ b/homeassistant/components/ps4/translations/bg.json @@ -14,8 +14,7 @@ }, "step": { "creds": { - "description": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0438 \u0441\u0430 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \"\u0417\u0430\u043f\u0430\u0437\u0432\u0430\u043d\u0435\" \u0438 \u0441\u043b\u0435\u0434 \u0442\u043e\u0432\u0430 \u0432 PS4 2nd Screen App, \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \"Refresh devices\" \u0438 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \"Home-Assistant\" \u0437\u0430 \u0434\u0430 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435.", - "title": "PlayStation 4" + "description": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0438 \u0441\u0430 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0434\u0430\u043d\u043d\u0438. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \"\u0417\u0430\u043f\u0430\u0437\u0432\u0430\u043d\u0435\" \u0438 \u0441\u043b\u0435\u0434 \u0442\u043e\u0432\u0430 \u0432 PS4 2nd Screen App, \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \"Refresh devices\" \u0438 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \"Home-Assistant\" \u0437\u0430 \u0434\u0430 \u043f\u0440\u043e\u0434\u044a\u043b\u0436\u0438\u0442\u0435." }, "link": { "data": { @@ -23,17 +22,13 @@ "ip_address": "IP \u0430\u0434\u0440\u0435\u0441", "name": "\u0418\u043c\u0435", "region": "\u0420\u0435\u0433\u0438\u043e\u043d" - }, - "description": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0432\u0430\u0448\u0430\u0442\u0430 PlayStation 4 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f. \u0417\u0430 \u201ePIN\u201c \u043e\u0442\u0438\u0434\u0435\u0442\u0435 \u0432 \u201eSettings\u201c \u043d\u0430 \u0412\u0430\u0448\u0430\u0442\u0430 PlayStation 4 \u043a\u043e\u043d\u0437\u043e\u043b\u0430. \u0421\u043b\u0435\u0434 \u0442\u043e\u0432\u0430 \u043f\u0440\u0435\u043c\u0438\u043d\u0435\u0442\u0435 \u043a\u044a\u043c \u201eMobile App Connection Settings\u201c \u0438 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u201eAdd Device\u201c. \u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0438\u044f PIN \u043a\u043e\u0434. \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.", - "title": "PlayStation 4" + } }, "mode": { "data": { "ip_address": "IP \u0430\u0434\u0440\u0435\u0441 (\u041e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e, \u0430\u043a\u043e \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e\u0442\u043e \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435).", "mode": "\u0420\u0435\u0436\u0438\u043c \u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435" - }, - "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0440\u0435\u0436\u0438\u043c \u0437\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435. \u041f\u043e\u043b\u0435\u0442\u043e IP \u0430\u0434\u0440\u0435\u0441 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0441\u0435 \u043e\u0441\u0442\u0430\u0432\u0438 \u043f\u0440\u0430\u0437\u043d\u043e, \u0430\u043a\u043e \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435, \u0442\u044a\u0439 \u043a\u0430\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u0442\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0449\u0435 \u0431\u044a\u0434\u0430\u0442 \u043e\u0442\u043a\u0440\u0438\u0442\u0438.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/ca.json b/homeassistant/components/ps4/translations/ca.json index 6486c154360..37cfabecf05 100644 --- a/homeassistant/components/ps4/translations/ca.json +++ b/homeassistant/components/ps4/translations/ca.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "Credencials necess\u00e0ries. Prem 'Envia' i, a continuaci\u00f3, a la segona pantalla de l'aplicaci\u00f3 de la PS4, actualitza els dispositius i selecciona 'Home-Assistant' per continuar.", - "title": "PlayStation 4" + "description": "Credencials necess\u00e0ries. Prem 'Envia' i, a continuaci\u00f3, a la segona pantalla de l'aplicaci\u00f3 de la PS4, actualitza els dispositius i selecciona 'Home-Assistant' per continuar." }, "link": { "data": { @@ -27,9 +26,7 @@ }, "data_description": { "code": "V\u00e9s a 'Configuraci\u00f3' de la teva consola PlayStation 4. A continuaci\u00f3, v\u00e9s a 'Configuraci\u00f3 de connexi\u00f3 de l'aplicaci\u00f3 m\u00f2bil' i selecciona 'Afegeix un dispositiu' per obtenir el PIN." - }, - "description": "Introdueix la informaci\u00f3 de la teva PlayStation 4. Per al Codi PIN, ves a 'Configuraci\u00f3' a la consola de la PlayStation 4. Despr\u00e9s navega fins a 'Configuraci\u00f3 de la connexi\u00f3 de l'aplicaci\u00f3 m\u00f2bil' i selecciona 'Afegir dispositiu'. Introdueix el Codi PIN que es mostra. Consulta la [documentaci\u00f3](https://www.home-assistant.io/components/ps4/) per a m\u00e9s informaci\u00f3.", - "title": "PlayStation 4" + } }, "mode": { "data": { @@ -38,9 +35,7 @@ }, "data_description": { "ip_address": "Deixa-ho en blanc si selecciones el descobriment autom\u00e0tic." - }, - "description": "Selecciona el mode de configuraci\u00f3. El camp de l'Adre\u00e7a IP es pot deixar en blanc si selecciones descobriment autom\u00e0tic (els dispositius es descobriran autom\u00e0ticament).", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/cs.json b/homeassistant/components/ps4/translations/cs.json index 0ef5eb5afee..e4a256e7522 100644 --- a/homeassistant/components/ps4/translations/cs.json +++ b/homeassistant/components/ps4/translations/cs.json @@ -13,26 +13,19 @@ "no_ipaddress": "Zadejte IP adresu PlayStation 4, kter\u00fd chcete konfigurovat." }, "step": { - "creds": { - "title": "PlayStation 4" - }, "link": { "data": { "code": "PIN k\u00f3d", "ip_address": "IP adresa", "name": "Jm\u00e9no", "region": "Region" - }, - "description": "Zadejte sv\u00e9 informace o PlayStation 4. \"PIN\" naleznete v \"Nastaven\u00ed\" sv\u00e9 konzole PlayStation 4. Pot\u00e9 p\u0159ejd\u011bte do \"Nastaven\u00ed p\u0159ipojen\u00ed mobiln\u00ed aplikace\" a vyberte \"P\u0159idat za\u0159\u00edzen\u00ed\". Zadejte PIN. Dal\u0161\u00ed informace naleznete v [dokumentaci](https://www.home-assistant.io/components/ps4/).", - "title": "PlayStation 4" + } }, "mode": { "data": { "ip_address": "IP adresa (Pokud pou\u017e\u00edv\u00e1te automatick\u00e9 zji\u0161\u0165ov\u00e1n\u00ed, ponechte pr\u00e1zdn\u00e9.)", "mode": "Re\u017eim nastaven\u00ed" - }, - "description": "Vyberte re\u017eim pro konfiguraci. Pole IP adresa m\u016f\u017ee b\u00fdt ponech\u00e1no pr\u00e1zdn\u00e9, pokud vyberete \"automatick\u00e9 zji\u0161\u0165ov\u00e1n\u00ed\", proto\u017ee za\u0159\u00edzen\u00ed budou automaticky objevena.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/da.json b/homeassistant/components/ps4/translations/da.json index 747e2d07146..2f95f6fa485 100644 --- a/homeassistant/components/ps4/translations/da.json +++ b/homeassistant/components/ps4/translations/da.json @@ -13,8 +13,7 @@ }, "step": { "creds": { - "description": "Der kr\u00e6ves legitimationsoplysninger. Tryk p\u00e5 'Indsend' og derefter i PS4 2. sk\u00e6rm-app, opdater enheder, og v\u00e6lg 'Home Assistant'-enhed for at forts\u00e6tte.", - "title": "PlayStation 4" + "description": "Der kr\u00e6ves legitimationsoplysninger. Tryk p\u00e5 'Indsend' og derefter i PS4 2. sk\u00e6rm-app, opdater enheder, og v\u00e6lg 'Home Assistant'-enhed for at forts\u00e6tte." }, "link": { "data": { @@ -22,17 +21,13 @@ "ip_address": "IP-adresse", "name": "Navn", "region": "Omr\u00e5de" - }, - "description": "Indtast dine PlayStation 4-oplysninger. For 'PIN' skal du navigere til 'Indstillinger' p\u00e5 din PlayStation 4-konsol. Naviger derefter til 'Mobile App Connection Settings' og v\u00e6lg 'Add Device'. Indtast den pinkode, der vises. Se [dokumentation](https://www.home-assistant.io/components/ps4/) for yderligere oplysninger.", - "title": "PlayStation 4" + } }, "mode": { "data": { "ip_address": "IP-adresse (lad det v\u00e6re tomt, hvis du bruger automatisk registrering).", "mode": "Konfigurationstilstand" - }, - "description": "V\u00e6lg tilstand for konfiguration. IP-adressefeltet kan v\u00e6re tomt, hvis du v\u00e6lger automatisk registrering, da enheder automatisk bliver fundet.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/de.json b/homeassistant/components/ps4/translations/de.json index 94012575c06..8a5d700f2fc 100644 --- a/homeassistant/components/ps4/translations/de.json +++ b/homeassistant/components/ps4/translations/de.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "Anmeldeinformationen ben\u00f6tigt. Klicke auf \"Senden\" und dann in der PS4 2nd Screen App, aktualisiere die Ger\u00e4te und w\u00e4hle das \"Home-Assistant\"-Ger\u00e4t aus, um fortzufahren.", - "title": "PlayStation 4" + "description": "Anmeldeinformationen ben\u00f6tigt. Klicke auf \"Senden\" und dann in der PS4 2nd Screen App, aktualisiere die Ger\u00e4te und w\u00e4hle das \"Home-Assistant\"-Ger\u00e4t aus, um fortzufahren." }, "link": { "data": { @@ -27,9 +26,7 @@ }, "data_description": { "code": "Navigiere auf deiner PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\", um den Pin zu erhalten." - }, - "description": "Gib deine PlayStation 4-Informationen ein. Navigiere f\u00fcr den PIN-Code auf der PlayStation 4-Konsole zu \"Einstellungen\". Navigiere dann zu \"Mobile App-Verbindungseinstellungen\" und w\u00e4hle \"Ger\u00e4t hinzuf\u00fcgen\" aus. Gib die angezeigte PIN-Code ein. Weitere Informationen findest du in der [Dokumentation](https://www.home-assistant.io/components/ps4/).", - "title": "PlayStation 4" + } }, "mode": { "data": { @@ -38,9 +35,7 @@ }, "data_description": { "ip_address": "Lasse das Feld leer, wenn du die automatische Erkennung ausw\u00e4hlst." - }, - "description": "W\u00e4hle den Modus f\u00fcr die Konfiguration aus. Das Feld IP-Adresse kann leer bleiben, wenn die automatische Erkennung ausgew\u00e4hlt wird, da Ger\u00e4te automatisch erkannt werden.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/el.json b/homeassistant/components/ps4/translations/el.json index f3899b682f3..7b5350a0418 100644 --- a/homeassistant/components/ps4/translations/el.json +++ b/homeassistant/components/ps4/translations/el.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "\u0391\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1. \u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \"\u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae\" \u03ba\u03b1\u03b9, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae PS4 2nd Screen App, \u03b1\u03bd\u03b1\u03bd\u03b5\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \"Home-Assistant\" \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5.", - "title": "PlayStation 4" + "description": "\u0391\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1. \u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \"\u03a5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae\" \u03ba\u03b1\u03b9, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae PS4 2nd Screen App, \u03b1\u03bd\u03b1\u03bd\u03b5\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \"Home-Assistant\" \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5." }, "link": { "data": { @@ -27,9 +26,7 @@ }, "data_description": { "code": "\u03a0\u03bb\u03bf\u03b7\u03b3\u03b7\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b9\u03c2 \"\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2\" \u03c3\u03c4\u03b7\u03bd \u03ba\u03bf\u03bd\u03c3\u03cc\u03bb\u03b1 PlayStation 4. \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c0\u03bb\u03bf\u03b7\u03b3\u03b7\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf 'Mobile App Connection Settings' \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 'Add Device' \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03bf pin." - }, - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03bf\u03c5 PlayStation 4. \u0393\u03b9\u03b1 \u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b9\u03c2 \"\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2\" \u03c3\u03c4\u03b7\u03bd \u03ba\u03bf\u03bd\u03c3\u03cc\u03bb\u03b1 PlayStation 4. \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c0\u03bb\u03bf\u03b7\u03b3\u03b7\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf 'Mobile App Connection Settings' (\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ce\u03bd \u03b3\u03b9\u03b1 \u03ba\u03b9\u03bd\u03b7\u03c4\u03ac) \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 'Add Device' (\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2). \u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03c0\u03bf\u03c5 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7](https://www.home-assistant.io/components/ps4/) \u03b3\u03b9\u03b1 \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2.", - "title": "PlayStation 4" + } }, "mode": { "data": { @@ -38,9 +35,7 @@ }, "data_description": { "ip_address": "\u0391\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b5\u03ac\u03bd \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7." - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2. \u03a4\u03bf \u03c0\u03b5\u03b4\u03af\u03bf \u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03af\u03bd\u03b5\u03b9 \u03ba\u03b5\u03bd\u03cc \u03b5\u03ac\u03bd \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03b5\u03c4\u03b5 Auto Discovery, \u03ba\u03b1\u03b8\u03ce\u03c2 \u03bf\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03b8\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03c4\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/en.json b/homeassistant/components/ps4/translations/en.json index 1c8640efe2e..e8a0c58a566 100644 --- a/homeassistant/components/ps4/translations/en.json +++ b/homeassistant/components/ps4/translations/en.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "Credentials needed. Press 'Submit' and then in the PS4 2nd Screen App, refresh devices and select the 'Home-Assistant' device to continue.", - "title": "PlayStation 4" + "description": "Credentials needed. Press 'Submit' and then in the PS4 2nd Screen App, refresh devices and select the 'Home-Assistant' device to continue." }, "link": { "data": { @@ -27,9 +26,7 @@ }, "data_description": { "code": "Navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device' to get the pin." - }, - "description": "Enter your PlayStation 4 information. For PIN Code, navigate to 'Settings' on your PlayStation 4 console. Then navigate to 'Mobile App Connection Settings' and select 'Add Device'. Enter the PIN Code that is displayed. Refer to the [documentation](https://www.home-assistant.io/components/ps4/) for additional info.", - "title": "PlayStation 4" + } }, "mode": { "data": { @@ -38,9 +35,7 @@ }, "data_description": { "ip_address": "Leave blank if selecting auto-discovery." - }, - "description": "Select mode for configuration. The IP Address field can be left blank if selecting Auto Discovery, as devices will be automatically discovered.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/es-419.json b/homeassistant/components/ps4/translations/es-419.json index 220183775f4..c5d8fb80a54 100644 --- a/homeassistant/components/ps4/translations/es-419.json +++ b/homeassistant/components/ps4/translations/es-419.json @@ -13,8 +13,7 @@ }, "step": { "creds": { - "description": "Credenciales necesarias. Presione 'Enviar' y luego en la aplicaci\u00f3n de la segunda pantalla de PS4, actualice los dispositivos y seleccione el dispositivo 'Home-Assistant' para continuar.", - "title": "Playstation 4" + "description": "Credenciales necesarias. Presione 'Enviar' y luego en la aplicaci\u00f3n de la segunda pantalla de PS4, actualice los dispositivos y seleccione el dispositivo 'Home-Assistant' para continuar." }, "link": { "data": { @@ -22,17 +21,13 @@ "ip_address": "Direcci\u00f3n IP", "name": "Nombre", "region": "Regi\u00f3n" - }, - "description": "Ingresa tu informaci\u00f3n de PlayStation 4. Para 'PIN', navegue hasta 'Configuraci\u00f3n' en su consola PlayStation 4. Luego navegue a 'Configuraci\u00f3n de conexi\u00f3n de la aplicaci\u00f3n m\u00f3vil' y seleccione 'Agregar dispositivo'. Ingrese el PIN que se muestra.", - "title": "Playstation 4" + } }, "mode": { "data": { "ip_address": "Direcci\u00f3n IP (dejar en blanco si se utiliza el descubrimiento autom\u00e1tico).", "mode": "Modo de configuraci\u00f3n" - }, - "description": "Seleccione el modo para la configuraci\u00f3n. El campo Direcci\u00f3n IP puede dejarse en blanco si selecciona Descubrimiento autom\u00e1tico, ya que los dispositivos se descubrir\u00e1n autom\u00e1ticamente.", - "title": "Playstation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/es.json b/homeassistant/components/ps4/translations/es.json index 7a807953b5e..a2c6e6fd1f4 100644 --- a/homeassistant/components/ps4/translations/es.json +++ b/homeassistant/components/ps4/translations/es.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "Credenciales necesarias. Pulsa 'Enviar' y, a continuaci\u00f3n, en la app de segunda pantalla de PS4, actualiza la lista de dispositivos y selecciona el dispositivo 'Home-Assistant' para continuar.", - "title": "PlayStation 4" + "description": "Credenciales necesarias. Pulsa 'Enviar' y, a continuaci\u00f3n, en la app de segunda pantalla de PS4, actualiza la lista de dispositivos y selecciona el dispositivo 'Home-Assistant' para continuar." }, "link": { "data": { @@ -24,17 +23,13 @@ "ip_address": "Direcci\u00f3n IP", "name": "Nombre", "region": "Regi\u00f3n" - }, - "description": "Introduce la informaci\u00f3n de tu PlayStation 4. Para el 'PIN', ve a los 'Ajustes' en tu PlayStation 4. Despu\u00e9s dir\u00edgete hasta 'Ajustes de conexi\u00f3n de la aplicaci\u00f3n para m\u00f3viles' y selecciona 'A\u00f1adir dispositivo'. Introduce el PIN mostrado. Consulta la [documentaci\u00f3n](https://www.home-assistant.io/components/ps4/) para m\u00e1s informaci\u00f3n.", - "title": "PlayStation 4" + } }, "mode": { "data": { "ip_address": "Direcci\u00f3n IP (d\u00e9jalo en blanco si usas la detecci\u00f3n autom\u00e1tica).", "mode": "Modo configuraci\u00f3n" - }, - "description": "Selecciona el modo de configuraci\u00f3n. El campo de direcci\u00f3n IP puede dejarse en blanco si se selecciona la detecci\u00f3n autom\u00e1tica, ya que los dispositivos se detectar\u00e1n autom\u00e1ticamente.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/et.json b/homeassistant/components/ps4/translations/et.json index 83ef7a6f14c..071d7813976 100644 --- a/homeassistant/components/ps4/translations/et.json +++ b/homeassistant/components/ps4/translations/et.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "Vaja on volitusi. Vajuta nuppu Esita ja seej\u00e4rel v\u00e4rskenda PS4 2nd Screen rakenduses seadmeid ning vali j\u00e4tkamiseks seade \u201eHome-Assistant\u201d.", - "title": "" + "description": "Vaja on volitusi. Vajuta nuppu Esita ja seej\u00e4rel v\u00e4rskenda PS4 2nd Screen rakenduses seadmeid ning vali j\u00e4tkamiseks seade \u201eHome-Assistant\u201d." }, "link": { "data": { @@ -27,9 +26,7 @@ }, "data_description": { "code": "Navigeeri oma PlayStation 4 konsoolis jaotisse \u201eSeaded\u201d. Seej\u00e4rel navigeeri jaotisse \"Mobiilirakenduse \u00fchenduse s\u00e4tted\" ja vali PIN-koodi saamiseks \"Lisa seade\"." - }, - "description": "Sisesta oma PlayStation 4 teave. PIN koodi saamiseks mine PlayStation 4 konsooli \u201eSeaded\u201d. Seej\u00e4rel liigu jaotisse \u201eMobiilirakenduse \u00fchenduse seaded\u201d ja vali \u201eLisa seade\u201d. SisestaPIN kood . Lisateavet leiad [dokumentatsioonist] (https://www.home-assistant.io/components/ps4/).", - "title": "" + } }, "mode": { "data": { @@ -38,9 +35,7 @@ }, "data_description": { "ip_address": "Automaatse avastamise valimiseks j\u00e4ta v\u00e4li t\u00fchjaks." - }, - "description": "Valikonfigureerimise re\u017eiim. V\u00e4lja IP-aadress saab automaatse tuvastamise valimisel t\u00fchjaks j\u00e4tta, kuna seadmed avastatakse automaatselt.", - "title": "" + } } } } diff --git a/homeassistant/components/ps4/translations/fi.json b/homeassistant/components/ps4/translations/fi.json index f61b9f0c162..8b179c48e4b 100644 --- a/homeassistant/components/ps4/translations/fi.json +++ b/homeassistant/components/ps4/translations/fi.json @@ -1,24 +1,19 @@ { "config": { "step": { - "creds": { - "title": "Playstation 4" - }, "link": { "data": { "code": "PIN", "ip_address": "IP-osoite", "name": "Nimi", "region": "Alue" - }, - "title": "Playstation 4" + } }, "mode": { "data": { "ip_address": "IP-osoite (j\u00e4t\u00e4 tyhj\u00e4ksi, jos k\u00e4yt\u00e4t automaattista etsint\u00e4\u00e4).", "mode": "Konfigurointitila" - }, - "title": "Playstation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/fr.json b/homeassistant/components/ps4/translations/fr.json index 8bc2575d246..73e2e18e22a 100644 --- a/homeassistant/components/ps4/translations/fr.json +++ b/homeassistant/components/ps4/translations/fr.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "Informations d'identification requises. Appuyez sur \u00ab\u00a0Envoyer\u00a0\u00bb, puis dans l'application PS4 Second Screen, actualisez les appareils et s\u00e9lectionnez l'appareil \u00ab\u00a0Home Assistant\u00a0\u00bb pour continuer.", - "title": "PlayStation 4" + "description": "Informations d'identification requises. Appuyez sur \u00ab\u00a0Envoyer\u00a0\u00bb, puis dans l'application PS4 Second Screen, actualisez les appareils et s\u00e9lectionnez l'appareil \u00ab\u00a0Home Assistant\u00a0\u00bb pour continuer." }, "link": { "data": { @@ -27,9 +26,7 @@ }, "data_description": { "code": "Acc\u00e9dez aux \u00ab\u00a0Param\u00e8tres\u00a0\u00bb sur votre console PlayStation\u00a04, puis acc\u00e9dez \u00e0 \u00ab\u00a0Param\u00e8tres de connexion de l'application mobile\u00a0\u00bb et s\u00e9lectionnez \u00ab\u00a0Ajouter un appareil\u00a0\u00bb afin d'obtenir le code PIN." - }, - "description": "Saisissez les informations de votre PlayStation\u00a04. Pour le Code PIN, acc\u00e9dez aux \u00ab\u00a0Param\u00e8tres\u00a0\u00bb sur votre console PlayStation\u00a04, puis acc\u00e9dez \u00e0 \u00ab\u00a0Param\u00e8tres de connexion de l'application mobile\u00a0\u00bb et s\u00e9lectionnez \u00ab\u00a0Ajouter un appareil\u00a0\u00bb. Entrez le Code PIN affich\u00e9. Consultez la [documentation](https://www.home-assistant.io/components/ps4/) pour plus d'informations.", - "title": "PlayStation 4" + } }, "mode": { "data": { @@ -38,9 +35,7 @@ }, "data_description": { "ip_address": "Laissez le champ vide si vous s\u00e9lectionnez la d\u00e9couverte automatique." - }, - "description": "S\u00e9lectionnez le mode de configuration. Le champ Adresse IP peut \u00eatre laiss\u00e9 vide si vous s\u00e9lectionnez D\u00e9couverte automatique, car les appareils seront automatiquement d\u00e9couverts.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/he.json b/homeassistant/components/ps4/translations/he.json index 421f869d8c5..0aaba028b7e 100644 --- a/homeassistant/components/ps4/translations/he.json +++ b/homeassistant/components/ps4/translations/he.json @@ -8,20 +8,13 @@ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, "step": { - "creds": { - "title": "\u05e4\u05dc\u05d9\u05d9\u05e1\u05d8\u05d9\u05d9\u05e9\u05df 4" - }, "link": { "data": { "code": "\u05e7\u05d5\u05d3 PIN", "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP", "name": "\u05e9\u05dd", "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 24da7fc15f5..ab48f3177e4 100644 --- a/homeassistant/components/ps4/translations/hu.json +++ b/homeassistant/components/ps4/translations/hu.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "Hiteles\u00edt\u0151 adatok sz\u00fcks\u00e9gesek. Nyomja meg a \u201eMehet\u201d gombot, majd a PS4 2. k\u00e9perny\u0151 alkalmaz\u00e1sban friss\u00edtse az eszk\u00f6z\u00f6ket, \u00e9s a folytat\u00e1shoz v\u00e1lassza a \u201eHome-Assistant\u201d eszk\u00f6zt.", - "title": "PlayStation 4" + "description": "Hiteles\u00edt\u0151 adatok sz\u00fcks\u00e9gesek. Nyomja meg a \u201eMehet\u201d gombot, majd a PS4 2. k\u00e9perny\u0151 alkalmaz\u00e1sban friss\u00edtse az eszk\u00f6z\u00f6ket, \u00e9s a folytat\u00e1shoz v\u00e1lassza a \u201eHome-Assistant\u201d eszk\u00f6zt." }, "link": { "data": { @@ -27,9 +26,7 @@ }, "data_description": { "code": "Keresse meg a PlayStation 4 konzol \"Be\u00e1ll\u00edt\u00e1sok\" gombj\u00e1t. Ezut\u00e1n keresse meg a \"Mobilalkalmaz\u00e1s-kapcsolat be\u00e1ll\u00edt\u00e1sai\" lehet\u0151s\u00e9get, \u00e9s v\u00e1lassza az \"Eszk\u00f6z hozz\u00e1ad\u00e1sa\" lehet\u0151s\u00e9get a pin lek\u00e9r\u00e9s\u00e9hez." - }, - "description": "Adja meg a PlayStation 4 adatait. A PIN-k\u00f3d eset\u00e9ben keresse meg a PlayStation 4 konzol \u201eBe\u00e1ll\u00edt\u00e1sok\u201d elem\u00e9t. Ezut\u00e1n keresse meg a \u201eMobilalkalmaz\u00e1s-kapcsolat be\u00e1ll\u00edt\u00e1sai\u201d elemet, \u00e9s v\u00e1lassza az \u201eEszk\u00f6z hozz\u00e1ad\u00e1sa\u201d lehet\u0151s\u00e9get. \u00cdrja be a megjelen\u0151 PIN-k\u00f3d . Tov\u00e1bbi inform\u00e1ci\u00f3k a [dokument\u00e1ci\u00f3ban] tal\u00e1lhat\u00f3k (https://www.home-assistant.io/components/ps4/).", - "title": "PlayStation 4" + } }, "mode": { "data": { @@ -38,9 +35,7 @@ }, "data_description": { "ip_address": "Az automatikus felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz hagyja \u00fcresen" - }, - "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.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/id.json b/homeassistant/components/ps4/translations/id.json index beb15fb5c4c..67c25538464 100644 --- a/homeassistant/components/ps4/translations/id.json +++ b/homeassistant/components/ps4/translations/id.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "Kredensial diperlukan. Tekan 'Kirim' dan kemudian di Aplikasi Layar ke-2 PS4, segarkan perangkat dan pilih perangkat 'Home-Assistant' untuk melanjutkan.", - "title": "PlayStation 4" + "description": "Kredensial diperlukan. Tekan 'Kirim' dan kemudian di Aplikasi Layar ke-2 PS4, segarkan perangkat dan pilih perangkat 'Home-Assistant' untuk melanjutkan." }, "link": { "data": { @@ -27,9 +26,7 @@ }, "data_description": { "code": "Navigasikan ke 'Pengaturan' di konsol PlayStation 4 Anda. Kemudian navigasikan ke 'Pengaturan Koneksi Aplikasi Seluler' dan pilih 'Tambahkan Perangkat' untuk mendapatkan PIN." - }, - "description": "Masukkan informasi PlayStation 4 Anda. Untuk Kode PIN, buka 'Pengaturan' di konsol PlayStation 4 Anda. Kemudian buka 'Pengaturan Koneksi Aplikasi Seluler' dan pilih 'Tambah Perangkat'. Masukkan Kode PIN yang ditampilkan. Lihat [dokumentasi] (https://www.home-assistant.io/components/ps4/) untuk info lebih lanjut.", - "title": "PlayStation 4" + } }, "mode": { "data": { @@ -38,9 +35,7 @@ }, "data_description": { "ip_address": "Kosongkan jika memilih penemuan otomatis." - }, - "description": "Pilih mode untuk konfigurasi. Alamat IP dapat dikosongkan jika memilih Penemuan Otomatis karena perangkat akan ditemukan secara otomatis.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/it.json b/homeassistant/components/ps4/translations/it.json index a8a97ad1144..aec6d943d4c 100644 --- a/homeassistant/components/ps4/translations/it.json +++ b/homeassistant/components/ps4/translations/it.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "Credenziali necessarie. Premi 'Invia' e poi, nell'applicazione PS4 Second Screen, aggiorna i dispositivi e seleziona il dispositivo 'Home-Assistant' per continuare.", - "title": "PlayStation 4" + "description": "Credenziali necessarie. Premi 'Invia' e poi, nell'applicazione PS4 Second Screen, aggiorna i dispositivi e seleziona il dispositivo 'Home-Assistant' per continuare." }, "link": { "data": { @@ -27,9 +26,7 @@ }, "data_description": { "code": "Vai a 'Impostazioni' sulla tua console PlayStation 4. Quindi vai su 'Impostazioni connessione app mobili' e seleziona 'Aggiungi dispositivo' per ottenere il pin." - }, - "description": "Inserisci le informazioni per la tua PlayStation 4. Per il codice PIN, vai a \"Impostazioni\" sulla PlayStation 4. Quindi vai a 'Impostazioni di connessione Mobile App' e seleziona 'Aggiungi dispositivo'. Immettere il codice PIN visualizzato. Fai riferimento alla [documentazione](https://www.home-assistant.io/components/ps4/) per ulteriori informazioni.", - "title": "PlayStation 4" + } }, "mode": { "data": { @@ -38,9 +35,7 @@ }, "data_description": { "ip_address": "Lascia vuoto se selezioni il rilevamento automatico." - }, - "description": "Seleziona la modalit\u00e0 per la configurazione. Il campo per l'indirizzo IP pu\u00f2 essere lasciato vuoto se si seleziona il rilevamento automatico, poich\u00e9 i dispositivi saranno rilevati automaticamente.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/ja.json b/homeassistant/components/ps4/translations/ja.json index 40ac55264a9..47d32431b54 100644 --- a/homeassistant/components/ps4/translations/ja.json +++ b/homeassistant/components/ps4/translations/ja.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "\u8a8d\u8a3c\u60c5\u5831\u304c\u5fc5\u8981\u3067\u3059\u3002'\u9001\u4fe1(submit)' \u3092\u62bc\u3057\u3066\u3001PS4\u306e2nd Screen App\u3067\u30c7\u30d0\u30a4\u30b9\u3092\u66f4\u65b0\u3057\u3001'Home-Assistant' \u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u7d9a\u884c\u3057\u307e\u3059\u3002", - "title": "Play Station 4" + "description": "\u8a8d\u8a3c\u60c5\u5831\u304c\u5fc5\u8981\u3067\u3059\u3002'\u9001\u4fe1(submit)' \u3092\u62bc\u3057\u3066\u3001PS4\u306e2nd Screen App\u3067\u30c7\u30d0\u30a4\u30b9\u3092\u66f4\u65b0\u3057\u3001'Home-Assistant' \u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u7d9a\u884c\u3057\u307e\u3059\u3002" }, "link": { "data": { @@ -27,9 +26,7 @@ }, "data_description": { "code": "PlayStation 4\u672c\u4f53\u306e' \u8a2d\u5b9a' \u306b\u79fb\u52d5\u3057\u307e\u3059\u3002\u6b21\u306b\u3001' \u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u63a5\u7d9a\u8a2d\u5b9a' \u306b\u79fb\u52d5\u3057\u3001' \u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u52a0' \u3092\u9078\u629e\u3057\u3066\u3001\u30d4\u30f3\u3092\u53d6\u5f97\u3057\u307e\u3059\u3002" - }, - "description": "PlayStation4\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002 PIN\u30b3\u30fc\u30c9(PIN CodePIN Code)\u306e\u5834\u5408\u306f\u3001PlayStation4\u672c\u4f53\u306e '\u8a2d\u5b9a' \u306b\u79fb\u52d5\u3057\u307e\u3059\u3002\u6b21\u306b\u3001'\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u63a5\u7d9a\u8a2d\u5b9a' \u306b\u79fb\u52d5\u3057\u3066\u3001'\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u52a0' \u3092\u9078\u629e\u3057\u307e\u3059\u3002\u8868\u793a\u3055\u308c\u305f PIN\u30b3\u30fc\u30c9(PIN CodePIN Code) \u3092\u5165\u529b\u3057\u307e\u3059\u3002\u8a73\u7d30\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/components/ps4/) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "Play Station 4" + } }, "mode": { "data": { @@ -38,9 +35,7 @@ }, "data_description": { "ip_address": "\u81ea\u52d5\u691c\u51fa\u3092\u9078\u629e\u3059\u308b\u5834\u5408\u306f\u3001\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u307e\u3059\u3002" - }, - "description": "\u30b3\u30f3\u30d5\u30a3\u30ae\u30e5\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30e2\u30fc\u30c9\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u81ea\u52d5\u691c\u51fa\u3092\u9078\u629e\u3059\u308b\u3068\u3001IP\u30a2\u30c9\u30ec\u30b9\u306e\u6b04\u304c\u7a7a\u767d\u306e\u307e\u307e\u3067\u3082\u30c7\u30d0\u30a4\u30b9\u306f\u81ea\u52d5\u7684\u306b\u691c\u51fa\u3055\u308c\u307e\u3059\u3002", - "title": "Play Station 4" + } } } } diff --git a/homeassistant/components/ps4/translations/ko.json b/homeassistant/components/ps4/translations/ko.json index 129927a9bab..b720db8241b 100644 --- a/homeassistant/components/ps4/translations/ko.json +++ b/homeassistant/components/ps4/translations/ko.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "\uc790\uaca9 \uc99d\uba85\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. '\ud655\uc778'\uc744 \ud074\ub9ad\ud55c \ub2e4\uc74c PS4 \uc138\ucee8\ub4dc \uc2a4\ud06c\ub9b0 \uc571\uc5d0\uc11c \uae30\uae30\ub97c \uc0c8\ub85c\uace0\uce68 \ud558\uace0 'Home-Assistant' \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", - "title": "PlayStation 4" + "description": "\uc790\uaca9 \uc99d\uba85\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. '\ud655\uc778'\uc744 \ud074\ub9ad\ud55c \ub2e4\uc74c PS4 \uc138\ucee8\ub4dc \uc2a4\ud06c\ub9b0 \uc571\uc5d0\uc11c \uae30\uae30\ub97c \uc0c8\ub85c\uace0\uce68 \ud558\uace0 'Home-Assistant' \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694." }, "link": { "data": { @@ -24,17 +23,13 @@ "ip_address": "IP \uc8fc\uc18c", "name": "\uc774\ub984", "region": "\uc9c0\uc5ed" - }, - "description": "PlayStation 4 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. PIN \ucf54\ub4dc\ub97c \ud655\uc778\ud558\ub824\uba74, PlayStation 4 \ucf58\uc194\uc5d0\uc11c '\uc124\uc815'\uc73c\ub85c \uc774\ub3d9\ud55c \ub4a4 '\ubaa8\ubc14\uc77c \uc571 \uc811\uc18d \uc124\uc815'\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec '\uae30\uae30 \ub4f1\ub85d\ud558\uae30'\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694. \ud654\uba74\uc5d0 \ud45c\uc2dc\ub41c PIN \ucf54\ub4dc(\uc744)\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \ucd94\uac00 \uc815\ubcf4\ub294 [\uad00\ub828 \ubb38\uc11c](https://www.home-assistant.io/components/ps4/)\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", - "title": "PlayStation 4" + } }, "mode": { "data": { "ip_address": "IP \uc8fc\uc18c (\uc790\ub3d9 \uac80\uc0c9\uc744 \uc0ac\uc6a9\ud558\ub294 \uacbd\uc6b0 \ube44\uc6cc\ub450\uc138\uc694)", "mode": "\uad6c\uc131 \ubaa8\ub4dc" - }, - "description": "\uad6c\uc131\ud560 \ubaa8\ub4dc\ub97c \uc120\ud0dd\ud569\ub2c8\ub2e4. \uc790\ub3d9 \uac80\uc0c9\uc744 \uc120\ud0dd\ud558\uba74 \uae30\uae30\uac00 \uc790\ub3d9\uc73c\ub85c \uac80\uc0c9\ub418\ubbc0\ub85c IP \uc8fc\uc18c \ud544\ub4dc\ub294 \ube44\uc6cc\ub450\uc154\ub3c4 \ub429\ub2c8\ub2e4.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/lb.json b/homeassistant/components/ps4/translations/lb.json index 444de75469c..b093b2bdd8e 100644 --- a/homeassistant/components/ps4/translations/lb.json +++ b/homeassistant/components/ps4/translations/lb.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "Umeldungsinformatioun sinn n\u00e9ideg. Dr\u00e9ckt op 'Ofsch\u00e9cken' , dann an der PS4 App, 2ten Ecran, erneiert Apparater an wielt den Home-Assistant Apparat aus fir weider ze fueren.", - "title": "PlayStation 4" + "description": "Umeldungsinformatioun sinn n\u00e9ideg. Dr\u00e9ckt op 'Ofsch\u00e9cken' , dann an der PS4 App, 2ten Ecran, erneiert Apparater an wielt den Home-Assistant Apparat aus fir weider ze fueren." }, "link": { "data": { @@ -24,17 +23,13 @@ "ip_address": "IP Adresse", "name": "Numm", "region": "Regioun" - }, - "description": "G\u00ebff deng Playstation 4 Informatiounen an. Fir 'PIN Code', g\u00e9i an d'Astellunge vun der Playstation 4 Konsole. Dann op 'Mobile App Verbindungs Astellungen' a wiel \"Apparat dob\u00e4isetzen' aus. G\u00ebff de PIN Code an deen ugewise g\u00ebtt. Lies [Dokumentatioun](https://www.home-assistant.io/components/ps4/) fir w\u00e9ider Informatiounen.", - "title": "PlayStation 4" + } }, "mode": { "data": { "ip_address": "IP Address (Eidel loossen falls Auto Discovery benotzt g\u00ebtt)", "mode": "Konfiguratioun's Modus" - }, - "description": "Konfiguratioun's Modus auswielen. D'Feld IP Adress kann eidel bl\u00e9iwen wann Auto Discovery benotzt g\u00ebtt, well d'Apparaten automatesch entdeckt ginn.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/nl.json b/homeassistant/components/ps4/translations/nl.json index 1a6ddd26f7f..8bebf95d0f0 100644 --- a/homeassistant/components/ps4/translations/nl.json +++ b/homeassistant/components/ps4/translations/nl.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "Aanmeldingsgegevens zijn nodig. Druk op 'Verzenden' en vervolgens in de PS4-app voor het 2e scherm, vernieuw apparaten en selecteer Home Assistant om door te gaan.", - "title": "PlayStation 4" + "description": "Aanmeldingsgegevens zijn nodig. Druk op 'Verzenden' en vervolgens in de PS4-app voor het 2e scherm, vernieuw apparaten en selecteer Home Assistant om door te gaan." }, "link": { "data": { @@ -27,9 +26,7 @@ }, "data_description": { "code": "Navigeer naar 'Instellingen' op je PlayStation 4-systeem. Navigeer vervolgens naar 'Instellingen mobiele app-verbinding' en selecteer 'Apparaat toevoegen' om de pin te krijgen." - }, - "description": "Voer je PlayStation 4-informatie in. Voor PIN-code navigeer je naar 'Instellingen' op je PlayStation 4-console. Ga vervolgens naar 'Verbindingsinstellingen mobiele app' en selecteer 'Apparaat toevoegen'. Voer de PIN-code in die wordt weergegeven. Raadpleeg de [documentatie](https://www.home-assistant.io/components/ps4/) voor meer informatie.", - "title": "PlayStation 4" + } }, "mode": { "data": { @@ -38,9 +35,7 @@ }, "data_description": { "ip_address": "Leeg laten indien u auto-discovery selecteert." - }, - "description": "Selecteer modus voor configuratie. Het veld IP-adres kan leeg worden gelaten als Auto Discovery wordt geselecteerd, omdat apparaten dan automatisch worden ontdekt.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/nn.json b/homeassistant/components/ps4/translations/nn.json index a9b44db3731..31a3808910a 100644 --- a/homeassistant/components/ps4/translations/nn.json +++ b/homeassistant/components/ps4/translations/nn.json @@ -5,18 +5,11 @@ "port_997_bind_error": "Kunne ikkje binde til port 997. Sj\u00e5 [dokumentasjonen] (https://www.home-assistant.io/components/ps4/) for ytterlegare informasjon." }, "step": { - "creds": { - "title": "Playstation 4" - }, "link": { "data": { "code": "PIN", "name": "Namn" - }, - "title": "Playstation 4" - }, - "mode": { - "title": "Playstation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/no.json b/homeassistant/components/ps4/translations/no.json index 69332862a33..f2d61d0955f 100644 --- a/homeassistant/components/ps4/translations/no.json +++ b/homeassistant/components/ps4/translations/no.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "Legitimasjon n\u00f8dvendig. Trykk 'Send' og deretter i PS4-ens andre skjerm app, kan du oppdatere enheter, og velg 'Home-Assistant' enheten for \u00e5 fortsette.", - "title": "" + "description": "Legitimasjon n\u00f8dvendig. Trykk 'Send' og deretter i PS4-ens andre skjerm app, kan du oppdatere enheter, og velg 'Home-Assistant' enheten for \u00e5 fortsette." }, "link": { "data": { @@ -27,9 +26,7 @@ }, "data_description": { "code": "Naviger til \"Innstillinger\" p\u00e5 PlayStation 4-konsollen. Naviger deretter til \"Mobile App Connection Settings\" og velg \"Add Device\" for \u00e5 hente pinnen." - }, - "description": "Skriv inn PlayStation 4-informasjonen din. For PIN kode , naviger til 'Innstillinger' p\u00e5 PlayStation 4-konsollen. Naviger deretter til 'Innstillinger for tilkobling av mobilapp' og velg 'Legg til enhet'. Skriv inn PIN kode som vises. Se [dokumentasjon] (https://www.home-assistant.io/components/ps4/) for mer informasjon.", - "title": "" + } }, "mode": { "data": { @@ -38,9 +35,7 @@ }, "data_description": { "ip_address": "La st\u00e5 tomt hvis du velger automatisk oppdagelse." - }, - "description": "Velg modus for konfigurasjon. Feltet IP adresse kan st\u00e5 tomt hvis du velger automatisk oppdagelse, da enheter automatisk blir oppdaget.", - "title": "" + } } } } diff --git a/homeassistant/components/ps4/translations/pl.json b/homeassistant/components/ps4/translations/pl.json index af4d36b9bf2..0ebb2fa9267 100644 --- a/homeassistant/components/ps4/translations/pl.json +++ b/homeassistant/components/ps4/translations/pl.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "Wymagane s\u0105 po\u015bwiadczenia. Naci\u015bnij przycisk \"Zatwierd\u017a\", a nast\u0119pnie w aplikacji PS4 Second Screen, od\u015bwie\u017c urz\u0105dzenia i wybierz urz\u0105dzenie 'Home-Assistant', aby kontynuowa\u0107.", - "title": "PlayStation 4" + "description": "Wymagane s\u0105 po\u015bwiadczenia. Naci\u015bnij przycisk \"Zatwierd\u017a\", a nast\u0119pnie w aplikacji PS4 Second Screen, od\u015bwie\u017c urz\u0105dzenia i wybierz urz\u0105dzenie 'Home-Assistant', aby kontynuowa\u0107." }, "link": { "data": { @@ -27,9 +26,7 @@ }, "data_description": { "code": "Przejd\u017a do \u201eUstawienia\u201d na konsoli PlayStation 4. Nast\u0119pnie przejd\u017a do \u201eUstawienia po\u0142\u0105czenia aplikacji mobilnej\u201d i wybierz \u201eDodaj urz\u0105dzenie\u201d, aby uzyska\u0107 kod PIN." - }, - "description": "Wprowad\u017a informacje o PlayStation 4. Aby uzyska\u0107 'PIN', przejd\u017a do 'Ustawienia' na konsoli PlayStation 4. Nast\u0119pnie przejd\u017a do 'Ustawienia po\u0142\u0105czenia aplikacji mobilnej' i wybierz 'Dodaj urz\u0105dzenie'. Wprowad\u017a wy\u015bwietlony kod PIN. Zapoznaj si\u0119 z [dokumentacj\u0105](https://www.home-assistant.io/components/ps4/), by pozna\u0107 szczeg\u00f3\u0142y.", - "title": "PlayStation 4" + } }, "mode": { "data": { @@ -38,9 +35,7 @@ }, "data_description": { "ip_address": "Pozostaw puste, je\u015bli wybierasz automatyczne wykrywanie." - }, - "description": "Wybierz tryb konfiguracji. Pole adresu IP mo\u017cna pozostawi\u0107 puste, je\u015bli wybierzesz opcj\u0119 Auto Discovery, poniewa\u017c urz\u0105dzenia zostan\u0105 automatycznie wykryte.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/pt-BR.json b/homeassistant/components/ps4/translations/pt-BR.json index 12d54264f78..f7731cc7ee8 100644 --- a/homeassistant/components/ps4/translations/pt-BR.json +++ b/homeassistant/components/ps4/translations/pt-BR.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "Credenciais necess\u00e1rias. Pressione 'Enviar' e, em seguida, no PS4, na Tela do segundo aplicativo, atualize os dispositivos e selecione o dispositivo 'Home-Assistant' para continuar.", - "title": "Playstation 4" + "description": "Credenciais necess\u00e1rias. Pressione 'Enviar' e, em seguida, no PS4, na Tela do segundo aplicativo, atualize os dispositivos e selecione o dispositivo 'Home-Assistant' para continuar." }, "link": { "data": { @@ -27,9 +26,7 @@ }, "data_description": { "code": "Navegue at\u00e9 'Configura\u00e7\u00f5es' em seu console PlayStation 4. Em seguida, navegue at\u00e9 'Configura\u00e7\u00f5es de conex\u00e3o do aplicativo m\u00f3vel' e selecione 'Adicionar dispositivo' para obter o PIN." - }, - "description": "Digite suas informa\u00e7\u00f5es do PlayStation 4. Para C\u00f3digo PIN, navegue at\u00e9 'Configura\u00e7\u00f5es' no seu console PlayStation 4. Em seguida, navegue at\u00e9 \"Configura\u00e7\u00f5es de conex\u00e3o de aplicativos m\u00f3veis\" e selecione \"Adicionar dispositivo\". Digite o C\u00f3digo PIN exibido. Consulte a [documenta\u00e7\u00e3o] (https://www.home-assistant.io/components/ps4/) para informa\u00e7\u00f5es adicionais.", - "title": "Playstation 4" + } }, "mode": { "data": { @@ -38,9 +35,7 @@ }, "data_description": { "ip_address": "Deixe em branco se selecionar a descoberta autom\u00e1tica." - }, - "description": "Selecione o modo para configura\u00e7\u00e3o. O campo Endere\u00e7o IP pode ser deixado em branco se selecionar Detec\u00e7\u00e3o Autom\u00e1tica, pois os dispositivos ser\u00e3o descobertos automaticamente.", - "title": "Playstation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/pt.json b/homeassistant/components/ps4/translations/pt.json index 5956937ac2f..cf428838b0b 100644 --- a/homeassistant/components/ps4/translations/pt.json +++ b/homeassistant/components/ps4/translations/pt.json @@ -13,23 +13,18 @@ "login_failed": "Falha ao emparelhar com a PlayStation 4. Verifique se o PIN est\u00e1 correto." }, "step": { - "creds": { - "title": "PlayStation 4" - }, "link": { "data": { "code": "PIN", "ip_address": "Endere\u00e7o de IP", "name": "Nome", "region": "Regi\u00e3o" - }, - "title": "PlayStation 4" + } }, "mode": { "data": { "mode": "Modo de configura\u00e7\u00e3o" - }, - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/ru.json b/homeassistant/components/ps4/translations/ru.json index 646346095b0..2b50ad57490 100644 --- a/homeassistant/components/ps4/translations/ru.json +++ b/homeassistant/components/ps4/translations/ru.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**, \u0430 \u0437\u0430\u0442\u0435\u043c \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 'PS4 Second Screen' \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0441\u043f\u0438\u0441\u043e\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e 'Home-Assistant'.", - "title": "PlayStation 4" + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**, \u0430 \u0437\u0430\u0442\u0435\u043c \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 'PS4 Second Screen' \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0441\u043f\u0438\u0441\u043e\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e 'Home-Assistant'." }, "link": { "data": { @@ -27,9 +26,7 @@ }, "data_description": { "code": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f PIN-\u043a\u043e\u0434\u0430 \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043f\u0443\u043d\u043a\u0442\u0443 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438** \u043d\u0430 \u043a\u043e\u043d\u0441\u043e\u043b\u0438 PlayStation 4. \u0417\u0430\u0442\u0435\u043c \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f** \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e**." - }, - "description": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f PIN-\u043a\u043e\u0434\u0430 \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043f\u0443\u043d\u043a\u0442\u0443 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438** \u043d\u0430 \u043a\u043e\u043d\u0441\u043e\u043b\u0438 PlayStation 4. \u0417\u0430\u0442\u0435\u043c \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 **\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f** \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e**. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/components/ps4/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", - "title": "PlayStation 4" + } }, "mode": { "data": { @@ -38,9 +35,7 @@ }, "data_description": { "ip_address": "\u041e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u041f\u043e\u043b\u0435 'IP-\u0430\u0434\u0440\u0435\u0441' \u043c\u043e\u0436\u043d\u043e \u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043f\u0443\u0441\u0442\u044b\u043c, \u0435\u0441\u043b\u0438 \u0432\u044b\u0431\u0440\u0430\u043d\u043e 'Auto Discovery', \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0431\u0443\u0434\u0443\u0442 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u044b \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/sl.json b/homeassistant/components/ps4/translations/sl.json index 1d5752eb326..5e056d76687 100644 --- a/homeassistant/components/ps4/translations/sl.json +++ b/homeassistant/components/ps4/translations/sl.json @@ -13,8 +13,7 @@ }, "step": { "creds": { - "description": "Potrebne so poverilnice. Pritisnite 'Po\u0161lji' in nato v aplikaciji PS4 2nd Screen App, osve\u017eite naprave in izberite napravo 'Home-Assistant' za nadaljevanje.", - "title": "PlayStation 4" + "description": "Potrebne so poverilnice. Pritisnite 'Po\u0161lji' in nato v aplikaciji PS4 2nd Screen App, osve\u017eite naprave in izberite napravo 'Home-Assistant' za nadaljevanje." }, "link": { "data": { @@ -22,17 +21,13 @@ "ip_address": "IP naslov", "name": "Ime", "region": "Regija" - }, - "description": "Vnesite va\u0161e PlayStation 4 podatke. Za 'PIN' pojdite na 'Nastavitve' na konzoli PlayStation 4. Nato se pomaknite do mo\u017enosti \u00bbNastavitve povezave z mobilno aplikacijo\u00ab in izberite \u00bbDodaj napravo\u00ab. Vnesite prikazano kodo PIN. Dodatne informacije najdete v [dokumentaciji] (https://www.home-assistant.io/components/ps4/).", - "title": "PlayStation 4" + } }, "mode": { "data": { "ip_address": "Naslov IP (Pustite prazno, \u010de uporabljate samodejno odkrivanje).", "mode": "Na\u010din konfiguracije" - }, - "description": "Izberite na\u010din za konfiguracijo. IP-Naslov, polje lahko pustite prazno, \u010de izberete samodejno odkrivanje, saj bodo naprave samodejno odkrite.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/sv.json b/homeassistant/components/ps4/translations/sv.json index ec117fb9b98..7435865c5d0 100644 --- a/homeassistant/components/ps4/translations/sv.json +++ b/homeassistant/components/ps4/translations/sv.json @@ -13,8 +13,7 @@ }, "step": { "creds": { - "description": "Autentiseringsuppgifter beh\u00f6vs. Tryck p\u00e5 'Skicka' och sedan uppdatera enheter i appen \"PS4 Second Screen\" p\u00e5 din mobiltelefon eller surfplatta och v\u00e4lj 'Home Assistent' enheten att forts\u00e4tta.", - "title": "PlayStation 4" + "description": "Autentiseringsuppgifter beh\u00f6vs. Tryck p\u00e5 'Skicka' och sedan uppdatera enheter i appen \"PS4 Second Screen\" p\u00e5 din mobiltelefon eller surfplatta och v\u00e4lj 'Home Assistent' enheten att forts\u00e4tta." }, "link": { "data": { @@ -22,17 +21,13 @@ "ip_address": "IP-adress", "name": "Namn", "region": "Region" - }, - "description": "Ange din PlayStation 4 information. F\u00f6r 'PIN', navigera till 'Inst\u00e4llningar' p\u00e5 din PlayStation 4 konsol. Navigera sedan till \"Inst\u00e4llningar f\u00f6r mobilappanslutning\" och v\u00e4lj \"L\u00e4gg till enhet\". Ange PIN-koden som visas.", - "title": "PlayStation 4" + } }, "mode": { "data": { "ip_address": "IP-adress (l\u00e4mna tom om du anv\u00e4nder automatisk uppt\u00e4ckt).", "mode": "Konfigureringsl\u00e4ge" - }, - "description": "V\u00e4lj l\u00e4ge f\u00f6r konfigurering. F\u00e4ltet IP-adress kan l\u00e4mnas tomt om du v\u00e4ljer Automatisk uppt\u00e4ckt, eftersom enheter d\u00e5 kommer att identifieras automatiskt.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/th.json b/homeassistant/components/ps4/translations/th.json index 2ffe2fb805a..bf910763725 100644 --- a/homeassistant/components/ps4/translations/th.json +++ b/homeassistant/components/ps4/translations/th.json @@ -1,20 +1,13 @@ { "config": { "step": { - "creds": { - "title": "PlayStation 4" - }, "link": { "data": { "code": "PIN", "ip_address": "\u0e17\u0e35\u0e48\u0e2d\u0e22\u0e39\u0e48 IP", "name": "\u0e0a\u0e37\u0e48\u0e2d", "region": "\u0e20\u0e39\u0e21\u0e34\u0e20\u0e32\u0e04" - }, - "title": "PlayStation 4" - }, - "mode": { - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/tr.json b/homeassistant/components/ps4/translations/tr.json index a2e8149db7d..09cd66d6826 100644 --- a/homeassistant/components/ps4/translations/tr.json +++ b/homeassistant/components/ps4/translations/tr.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "Kimlik bilgileri gerekli. \"G\u00f6nder\"e bas\u0131n ve ard\u0131ndan PS4 2. Ekran Uygulamas\u0131nda cihazlar\u0131 yenileyin ve devam etmek i\u00e7in \"Home-Asistan\" cihaz\u0131n\u0131 se\u00e7in.", - "title": "PlayStation 4" + "description": "Kimlik bilgileri gerekli. \"G\u00f6nder\"e bas\u0131n ve ard\u0131ndan PS4 2. Ekran Uygulamas\u0131nda cihazlar\u0131 yenileyin ve devam etmek i\u00e7in \"Home-Asistan\" cihaz\u0131n\u0131 se\u00e7in." }, "link": { "data": { @@ -27,9 +26,7 @@ }, "data_description": { "code": "PlayStation 4 konsolunuzda 'Ayarlar'a gidin. Ard\u0131ndan, \"Mobil Uygulama Ba\u011flant\u0131 Ayarlar\u0131\"na gidin ve PIN'i almak i\u00e7in \"Cihaz Ekle\"yi se\u00e7in." - }, - "description": "PlayStation 4 bilgilerinizi girin. PIN Kodu i\u00e7in PlayStation 4 konsolunuzda 'Ayarlar'a gidin. Ard\u0131ndan \"Mobil Uygulama Ba\u011flant\u0131 Ayarlar\u0131\"na gidin ve \"Cihaz Ekle\"yi se\u00e7in. PIN Kodu kodunu girin. Ek bilgi i\u00e7in [belgelere](https://www.home-assistant.io/components/ps4/) bak\u0131n.", - "title": "PlayStation 4" + } }, "mode": { "data": { @@ -38,9 +35,7 @@ }, "data_description": { "ip_address": "Otomatik bulma se\u00e7iliyorsa bo\u015f b\u0131rak\u0131n." - }, - "description": "Yap\u0131land\u0131rma i\u00e7in modu se\u00e7in. IP Adresi alan\u0131, Otomatik Ke\u015fif se\u00e7ildi\u011finde cihazlar otomatik olarak ke\u015ffedilece\u011finden bo\u015f b\u0131rak\u0131labilir.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/uk.json b/homeassistant/components/ps4/translations/uk.json index 696a46bf8d7..bf6716649c0 100644 --- a/homeassistant/components/ps4/translations/uk.json +++ b/homeassistant/components/ps4/translations/uk.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **, \u0430 \u043f\u043e\u0442\u0456\u043c \u0432 \u0434\u043e\u0434\u0430\u0442\u043a\u0443 'PS4 Second Screen' \u043e\u043d\u043e\u0432\u0456\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u0456 \u0432\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 'Home-Assistant'.", - "title": "PlayStation 4" + "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **, \u0430 \u043f\u043e\u0442\u0456\u043c \u0432 \u0434\u043e\u0434\u0430\u0442\u043a\u0443 'PS4 Second Screen' \u043e\u043d\u043e\u0432\u0456\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u0456 \u0432\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 'Home-Assistant'." }, "link": { "data": { @@ -24,17 +23,13 @@ "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", "name": "\u041d\u0430\u0437\u0432\u0430", "region": "\u0420\u0435\u0433\u0456\u043e\u043d" - }, - "description": "\u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f PIN-\u043a\u043e\u0434\u0443 \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0434\u043e \u043f\u0443\u043d\u043a\u0442\u0443 ** \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f ** \u043d\u0430 \u043a\u043e\u043d\u0441\u043e\u043b\u0456 PlayStation 4. \u041f\u043e\u0442\u0456\u043c \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 ** \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043c\u043e\u0431\u0456\u043b\u044c\u043d\u043e\u0433\u043e \u0434\u043e\u0434\u0430\u0442\u043a\u0430 ** \u0456 \u0432\u0438\u0431\u0435\u0440\u0456\u0442\u044c ** \u0414\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 ** . \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438](https://www.home-assistant.io/components/ps4/) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457.", - "title": "PlayStation 4" + } }, "mode": { "data": { "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430 (\u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c \u043f\u0440\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u0456 \u0440\u0435\u0436\u0438\u043c\u0443 \u0430\u0432\u0442\u043e\u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f)", "mode": "\u0420\u0435\u0436\u0438\u043c" - }, - "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u041f\u043e\u043b\u0435 'IP-\u0430\u0434\u0440\u0435\u0441\u0430' \u043c\u043e\u0436\u043d\u0430 \u0437\u0430\u043b\u0438\u0448\u0438\u0442\u0438 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c, \u044f\u043a\u0449\u043e \u0432\u0438\u0431\u0440\u0430\u043d\u043e 'Auto Discovery', \u043e\u0441\u043a\u0456\u043b\u044c\u043a\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0431\u0443\u0434\u0443\u0442\u044c \u0434\u043e\u0434\u0430\u043d\u0456 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e.", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/zh-Hans.json b/homeassistant/components/ps4/translations/zh-Hans.json index 3c240d96131..69f0694f853 100644 --- a/homeassistant/components/ps4/translations/zh-Hans.json +++ b/homeassistant/components/ps4/translations/zh-Hans.json @@ -12,8 +12,7 @@ }, "step": { "creds": { - "description": "\u9700\u8981\u51ed\u636e\u3002\u8bf7\u70b9\u51fb\u201c\u63d0\u4ea4\u201d\u7136\u540e\u5728 PS4 \u7b2c\u4e8c\u5c4f\u5e55\u5e94\u7528\u7a0b\u5e8f\u4e2d\u5237\u65b0\u8bbe\u5907\u5e76\u9009\u62e9\u201cHome-Assistant\u201d\u8bbe\u5907\u4ee5\u7ee7\u7eed\u3002", - "title": "PlayStation 4" + "description": "\u9700\u8981\u51ed\u636e\u3002\u8bf7\u70b9\u51fb\u201c\u63d0\u4ea4\u201d\u7136\u540e\u5728 PS4 \u7b2c\u4e8c\u5c4f\u5e55\u5e94\u7528\u7a0b\u5e8f\u4e2d\u5237\u65b0\u8bbe\u5907\u5e76\u9009\u62e9\u201cHome-Assistant\u201d\u8bbe\u5907\u4ee5\u7ee7\u7eed\u3002" }, "link": { "data": { @@ -21,15 +20,12 @@ "ip_address": "IP \u5730\u5740", "name": "\u540d\u79f0", "region": "\u5730\u533a" - }, - "description": "\u8f93\u5165\u60a8\u7684 PlayStation 4 \u4fe1\u606f\u3002\u5bf9\u4e8e \"PIN\", \u8bf7\u5bfc\u822a\u5230 PlayStation 4 \u63a7\u5236\u53f0\u4e0a\u7684 \"\u8bbe\u7f6e\"\u3002\u7136\u540e\u5bfc\u822a\u5230 \"\u79fb\u52a8\u5e94\u7528\u8fde\u63a5\u8bbe\u7f6e\", \u7136\u540e\u9009\u62e9 \"\u6dfb\u52a0\u8bbe\u5907\"\u3002\u8f93\u5165\u663e\u793a\u7684 PIN\u3002", - "title": "PlayStation 4" + } }, "mode": { "data": { "mode": "\u8bbe\u7f6e\u6a21\u5f0f" - }, - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/ps4/translations/zh-Hant.json b/homeassistant/components/ps4/translations/zh-Hant.json index ae7e86377d8..231e3ecc5e0 100644 --- a/homeassistant/components/ps4/translations/zh-Hant.json +++ b/homeassistant/components/ps4/translations/zh-Hant.json @@ -15,8 +15,7 @@ }, "step": { "creds": { - "description": "\u9700\u8981\u6191\u8b49\u3002\u6309\u4e0b\u300c\u50b3\u9001\u300d\u5f8c\u3001\u65bc PS4 \u7b2c\u4e8c\u756b\u9762 App\uff0c\u66f4\u65b0\u88dd\u7f6e\u4e26\u9078\u64c7\u300cHome-Assistant\u300d\u4ee5\u7e7c\u7e8c\u3002", - "title": "PlayStation 4" + "description": "\u9700\u8981\u6191\u8b49\u3002\u6309\u4e0b\u300c\u50b3\u9001\u300d\u5f8c\u3001\u65bc PS4 \u7b2c\u4e8c\u756b\u9762 App\uff0c\u66f4\u65b0\u88dd\u7f6e\u4e26\u9078\u64c7\u300cHome-Assistant\u300d\u4ee5\u7e7c\u7e8c\u3002" }, "link": { "data": { @@ -27,9 +26,7 @@ }, "data_description": { "code": "\u5207\u63db\u81f3 PlayStation 4 \u4e3b\u6a5f\u7684\u300c\u8a2d\u5b9a\u300d\u5167\uff0c\u4e26\u65bc\u300c\u884c\u52d5\u7a0b\u5f0f\u9023\u7dda\u8a2d\u5b9a\uff08Mobile App Connection Settings\uff09\u300d\u4e2d\u9078\u64c7\u300c\u65b0\u589e\u88dd\u7f6e\u300d\u4ee5\u53d6\u5f97 PIN \u78bc\u3002" - }, - "description": "\u8f38\u5165\u60a8\u7684 PlayStation 4 \u8cc7\u8a0a\uff0c\u300cPIN \u78bc\u300d\u65bc PlayStation 4 \u4e3b\u6a5f\u7684\u300c\u8a2d\u5b9a\u300d\u5167\uff0c\u4e26\u65bc\u300c\u884c\u52d5\u7a0b\u5f0f\u9023\u7dda\u8a2d\u5b9a\uff08Mobile App Connection Settings\uff09\u300d\u4e2d\u9078\u64c7\u300c\u65b0\u589e\u88dd\u7f6e\u300d\u3002\u8f38\u5165\u6240\u986f\u793a\u7684 PIN \u78bc\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", - "title": "PlayStation 4" + } }, "mode": { "data": { @@ -38,9 +35,7 @@ }, "data_description": { "ip_address": "\u5047\u5982\u9078\u64c7\u4f7f\u7528\u81ea\u52d5\u641c\u7d22\u8acb\u4fdd\u6301\u7a7a\u767d" - }, - "description": "\u9078\u64c7\u6a21\u5f0f\u4ee5\u9032\u884c\u8a2d\u5b9a\u3002\u5047\u5982\u9078\u64c7\u81ea\u52d5\u641c\u7d22\u6a21\u5f0f\u7684\u8a71\uff0c\u7531\u65bc\u6703\u81ea\u52d5\u9032\u884c\u88dd\u7f6e\u641c\u5c0b\uff0cIP \u4f4d\u5740\u53ef\u4fdd\u7559\u70ba\u7a7a\u767d\u3002", - "title": "PlayStation 4" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ca.json b/homeassistant/components/pvpc_hourly_pricing/translations/ca.json index ef5e00e25c3..b435e12a90a 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ca.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ca.json @@ -10,9 +10,7 @@ "power": "Pot\u00e8ncia contractada (kW)", "power_p3": "Pot\u00e8ncia contractada del per\u00edode vall P3 (kW)", "tariff": "Tarifa aplicable per zona geogr\u00e0fica" - }, - "description": "Aquest sensor utilitza l'API oficial per obtenir els [preus per hora de l'electricitat (PVPC)](https://www.esios.ree.es/es/pvpc) a Espanya.\nPer a m\u00e9s informaci\u00f3, consulta la [documentaci\u00f3 de la integraci\u00f3](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "Configuraci\u00f3 del sensor" + } } } }, @@ -23,9 +21,7 @@ "power": "Pot\u00e8ncia contractada (kW)", "power_p3": "Pot\u00e8ncia contractada del per\u00edode vall P3 (kW)", "tariff": "Tarifa aplicable per zona geogr\u00e0fica" - }, - "description": "Aquest sensor utilitza l'API oficial per obtenir els [preus per hora de l'electricitat (PVPC)](https://www.esios.ree.es/es/pvpc) a Espanya.\nPer a m\u00e9s informaci\u00f3, consulta la [documentaci\u00f3 de la integraci\u00f3](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "Configuraci\u00f3 del sensor" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/cs.json b/homeassistant/components/pvpc_hourly_pricing/translations/cs.json index 2dc8bc03afa..69d5d867dfa 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/cs.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/cs.json @@ -7,8 +7,7 @@ "user": { "data": { "name": "Jm\u00e9no senzoru" - }, - "title": "V\u00fdb\u011br tarifu" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/de.json b/homeassistant/components/pvpc_hourly_pricing/translations/de.json index 545a3d2cd9f..49f54ee41d8 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/de.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/de.json @@ -10,9 +10,7 @@ "power": "Vertraglich vereinbarte Leistung (kW)", "power_p3": "Vertraglich vereinbarte Leistung f\u00fcr Talperiode P3 (kW)", "tariff": "Geltender Tarif nach geografischer Zone" - }, - "description": "Dieser Sensor verwendet die offizielle API, um [st\u00fcndliche Strompreise (PVPC)] (https://www.esios.ree.es/es/pvpc) in Spanien zu erhalten. Weitere Informationen findest du in den [Integrations-Dokumentation] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "Sensoreinrichtung" + } } } }, @@ -23,9 +21,7 @@ "power": "Vertraglich vereinbarte Leistung (kW)", "power_p3": "Vertraglich vereinbarte Leistung f\u00fcr Talperiode P3 (kW)", "tariff": "Geltender Tarif nach geografischer Zone" - }, - "description": "Dieser Sensor verwendet die offizielle API, um [st\u00fcndliche Strompreise (PVPC)](https://www.esios.ree.es/es/pvpc) in Spanien zu erhalten.\nEine genauere Erkl\u00e4rung findest du in den [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "Sensoreinrichtung" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/el.json b/homeassistant/components/pvpc_hourly_pricing/translations/el.json index af128ba3bb3..4c9d056daf7 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/el.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/el.json @@ -10,9 +10,7 @@ "power": "\u03a3\u03c5\u03bc\u03b2\u03b1\u03c4\u03b9\u03ba\u03ae \u03b9\u03c3\u03c7\u03cd\u03c2 (kW)", "power_p3": "\u03a3\u03c5\u03bc\u03b2\u03b1\u03c4\u03b9\u03ba\u03ae \u03b9\u03c3\u03c7\u03cd\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c0\u03b5\u03c1\u03af\u03bf\u03b4\u03bf \u03ba\u03bf\u03b9\u03bb\u03ac\u03b4\u03b1\u03c2 P3 (kW)", "tariff": "\u0399\u03c3\u03c7\u03cd\u03bf\u03bd \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03bf \u03b1\u03bd\u03ac \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03ae \u03b6\u03ce\u03bd\u03b7" - }, - "description": "\u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03bf \u03b5\u03c0\u03af\u03c3\u03b7\u03bc\u03bf API \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03b9 [\u03c9\u03c1\u03b9\u03b1\u03af\u03b1 \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03b9\u03ba\u03ae\u03c2 \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1\u03c2 (PVPC)](https://www.esios.ree.es/es/pvpc) \u03c3\u03c4\u03b7\u03bd \u0399\u03c3\u03c0\u03b1\u03bd\u03af\u03b1.\n\u0393\u03b9\u03b1 \u03c0\u03b9\u03bf \u03b1\u03ba\u03c1\u03b9\u03b2\u03b5\u03af\u03c2 \u03b5\u03be\u03b7\u03b3\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b5\u03c0\u03b9\u03c3\u03ba\u03b5\u03c6\u03b8\u03b5\u03af\u03c4\u03b5 \u03c4\u03b1 [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1" + } } } }, @@ -23,9 +21,7 @@ "power": "\u03a3\u03c5\u03bc\u03b2\u03b1\u03c4\u03b9\u03ba\u03ae \u03b9\u03c3\u03c7\u03cd\u03c2 (kW)", "power_p3": "\u03a3\u03c5\u03bc\u03b2\u03b1\u03c4\u03b9\u03ba\u03ae \u03b9\u03c3\u03c7\u03cd\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c0\u03b5\u03c1\u03af\u03bf\u03b4\u03bf \u03ba\u03bf\u03b9\u03bb\u03ac\u03b4\u03b1\u03c2 P3 (kW)", "tariff": "\u0399\u03c3\u03c7\u03cd\u03bf\u03bd \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03bf \u03b1\u03bd\u03ac \u03b3\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03ae \u03b6\u03ce\u03bd\u03b7" - }, - "description": "\u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03bf \u03b5\u03c0\u03af\u03c3\u03b7\u03bc\u03bf API \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03b9 [\u03c9\u03c1\u03b9\u03b1\u03af\u03b1 \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03b9\u03ba\u03ae\u03c2 \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1\u03c2 (PVPC)](https://www.esios.ree.es/es/pvpc) \u03c3\u03c4\u03b7\u03bd \u0399\u03c3\u03c0\u03b1\u03bd\u03af\u03b1.\n\u0393\u03b9\u03b1 \u03c0\u03b9\u03bf \u03b1\u03ba\u03c1\u03b9\u03b2\u03b5\u03af\u03c2 \u03b5\u03be\u03b7\u03b3\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b5\u03c0\u03b9\u03c3\u03ba\u03b5\u03c6\u03b8\u03b5\u03af\u03c4\u03b5 \u03c4\u03b1 [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/en.json b/homeassistant/components/pvpc_hourly_pricing/translations/en.json index 38f45d36ab6..9667d14fd05 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/en.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/en.json @@ -10,9 +10,7 @@ "power": "Contracted power (kW)", "power_p3": "Contracted power for valley period P3 (kW)", "tariff": "Applicable tariff by geographic zone" - }, - "description": "This sensor uses official API to get [hourly pricing of electricity (PVPC)](https://www.esios.ree.es/es/pvpc) in Spain.\nFor more precise explanation visit the [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "Sensor setup" + } } } }, @@ -23,9 +21,7 @@ "power": "Contracted power (kW)", "power_p3": "Contracted power for valley period P3 (kW)", "tariff": "Applicable tariff by geographic zone" - }, - "description": "This sensor uses official API to get [hourly pricing of electricity (PVPC)](https://www.esios.ree.es/es/pvpc) in Spain.\nFor more precise explanation visit the [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "Sensor setup" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/es-419.json b/homeassistant/components/pvpc_hourly_pricing/translations/es-419.json index ed6ab16c8b5..a07120f5147 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/es-419.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/es-419.json @@ -8,9 +8,7 @@ "data": { "name": "Nombre del sensor", "tariff": "Tarifa contratada (1, 2 o 3 per\u00edodos)" - }, - "description": "Este sensor utiliza la API oficial para obtener [precios por hora de la electricidad (PVPC)] (https://www.esios.ree.es/es/pvpc) en Espa\u00f1a. \n Para obtener una explicaci\u00f3n m\u00e1s precisa, visite los [documentos de integraci\u00f3n] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\nSeleccione la tarifa contratada en funci\u00f3n de la cantidad de per\u00edodos de facturaci\u00f3n por d\u00eda: \n - 1 per\u00edodo: normal \n - 2 per\u00edodos: discriminaci\u00f3n (tarifa nocturna) \n - 3 per\u00edodos: coche el\u00e9ctrico (tarifa nocturna de 3 per\u00edodos)", - "title": "Selecci\u00f3n de tarifa" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/es.json b/homeassistant/components/pvpc_hourly_pricing/translations/es.json index ec5634a160e..eb96606831c 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/es.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/es.json @@ -10,9 +10,7 @@ "power": "Potencia contratada (kW)", "power_p3": "Potencia contratada para el per\u00edodo valle P3 (kW)", "tariff": "Tarifa aplicable por zona geogr\u00e1fica" - }, - "description": "Este sensor utiliza la API oficial para obtener [el precio horario de la electricidad (PVPC)](https://www.esios.ree.es/es/pvpc) en Espa\u00f1a.\nPara obtener una explicaci\u00f3n m\u00e1s precisa, visita los [documentos de la integraci\u00f3n](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSelecciona la tarifa contratada en funci\u00f3n del n\u00famero de per\u00edodos de facturaci\u00f3n por d\u00eda:\n- 1 per\u00edodo: normal\n- 2 per\u00edodos: discriminaci\u00f3n (tarifa nocturna)\n- 3 per\u00edodos: coche el\u00e9ctrico (tarifa nocturna de 3 per\u00edodos)", - "title": "Configuraci\u00f3n del sensor" + } } } }, @@ -23,9 +21,7 @@ "power": "Potencia contratada (kW)", "power_p3": "Potencia contratada para el per\u00edodo valle P3 (kW)", "tariff": "Tarifa aplicable por zona geogr\u00e1fica" - }, - "description": "Este sensor utiliza la API oficial para obtener el [precio horario de la electricidad (PVPC)](https://www.esios.ree.es/es/pvpc) en Espa\u00f1a.\nPara una explicaci\u00f3n m\u00e1s precisa visita los [documentos de integraci\u00f3n](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "Configuraci\u00f3n del sensor" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/et.json b/homeassistant/components/pvpc_hourly_pricing/translations/et.json index 5db4f9ec04b..8554ca18196 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/et.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/et.json @@ -10,9 +10,7 @@ "power": "Lepinguj\u00e4rgne v\u00f5imsus (kW)", "power_p3": "Lepinguj\u00e4rgne v\u00f5imsus soodusperioodil P3 (kW)", "tariff": "Kohaldatav tariif geograafilise tsooni j\u00e4rgi" - }, - "description": "See andur kasutab ametlikku API-d, et saada [elektri tunnihinda (PVPC)](https://www.esios.ree.es/es/pvpc) Hispaanias.\nT\u00e4psema selgituse saamiseks k\u00fclasta [integratsioonidokumente](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "Anduri seadistamine" + } } } }, @@ -23,9 +21,7 @@ "power": "Lepinguj\u00e4rgne v\u00f5imsus (kW)", "power_p3": "Lepinguj\u00e4rgne v\u00f5imsus soodusperioodil P3 (kW)", "tariff": "Kohaldatav tariif geograafilise tsooni j\u00e4rgi" - }, - "description": "See andur kasutab ametlikku API-d, et saada [elektri tunnihinda (PVPC)](https://www.esios.ree.es/es/pvpc) Hispaanias.\nT\u00e4psema selgituse saamiseks k\u00fclasta [integratsioonidokumente](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "Anduri seadistamine" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/fr.json b/homeassistant/components/pvpc_hourly_pricing/translations/fr.json index afbea0852c4..1f0c447ff0f 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/fr.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/fr.json @@ -10,9 +10,7 @@ "power": "Puissance souscrite (kW)", "power_p3": "Puissance souscrite pour la p\u00e9riode de vall\u00e9e P3 (kW)", "tariff": "Tarif applicable par zone g\u00e9ographique" - }, - "description": "Ce capteur utilise l'API officielle pour obtenir [tarification horaire de l'\u00e9lectricit\u00e9 (PVPC)](https://www.esios.ree.es/es/pvpc) en Espagne.\n Pour des explications plus pr\u00e9cises, visitez les [docs d'int\u00e9gration](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "Configuration du capteur" + } } } }, @@ -23,9 +21,7 @@ "power": "Puissance souscrite (kW)", "power_p3": "Puissance souscrite pour la p\u00e9riode de vall\u00e9e P3 (kW)", "tariff": "Tarif applicable par zone g\u00e9ographique" - }, - "description": "Ce capteur utilise l'API officielle pour obtenir [tarification horaire de l'\u00e9lectricit\u00e9 (PVPC)](https://www.esios.ree.es/es/pvpc) en Espagne.\n Pour des explications plus pr\u00e9cises, visitez les [docs d'int\u00e9gration](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "Configuration du capteur" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/he.json b/homeassistant/components/pvpc_hourly_pricing/translations/he.json index 951e9b21b2f..48a6eeeea33 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/he.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/he.json @@ -3,12 +3,5 @@ "abort": { "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" } - }, - "options": { - "step": { - "init": { - "title": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05d7\u05d9\u05d9\u05e9\u05df" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/hu.json b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json index c654c92969a..121a87af124 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/hu.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json @@ -10,9 +10,7 @@ "power": "Szerz\u0151d\u00e9s szerinti teljes\u00edtm\u00e9ny (kW)", "power_p3": "Szerz\u0151d\u00f6tt teljes\u00edtm\u00e9ny P3 v\u00f6lgyid\u0151szakra (kW)", "tariff": "Alkalmazand\u00f3 tarifa f\u00f6ldrajzi z\u00f3n\u00e1nk\u00e9nt" - }, - "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/).", - "title": "\u00c9rz\u00e9kel\u0151 be\u00e1ll\u00edt\u00e1sa" + } } } }, @@ -23,9 +21,7 @@ "power": "Szerz\u0151d\u00e9s szerinti teljes\u00edtm\u00e9ny (kW)", "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.\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 5705a2a4fcb..2bc5c67e533 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/id.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/id.json @@ -10,9 +10,7 @@ "power": "Daya terkontrak (kW)", "power_p3": "Daya terkontrak untuk periode lembah P3 (kW)", "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/).", - "title": "Penyiapan sensor" + } } } }, @@ -23,9 +21,7 @@ "power": "Daya terkontrak (kW)", "power_p3": "Daya terkontrak untuk periode lembah P3 (kW)", "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/).", - "title": "Penyiapan sensor" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/it.json b/homeassistant/components/pvpc_hourly_pricing/translations/it.json index 79e6e627a7e..bd6f8494d49 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/it.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/it.json @@ -10,9 +10,7 @@ "power": "Potenza contrattuale (kW)", "power_p3": "Potenza contrattuale per il periodo di valle P3 (kW)", "tariff": "Tariffa applicabile per zona geografica" - }, - "description": "Questo sensore utilizza l'API ufficiale per ottenere il [prezzo orario dell'elettricit\u00e0 (PVPC)](https://www.esios.ree.es/es/pvpc) in Spagna.\nPer una spiegazione pi\u00f9 precisa, visita i [documenti di integrazione](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "Configurazione del sensore" + } } } }, @@ -23,9 +21,7 @@ "power": "Potenza contrattuale (kW)", "power_p3": "Potenza contrattuale per il periodo di valle P3 (kW)", "tariff": "Tariffa applicabile per zona geografica" - }, - "description": "Questo sensore utilizza l'API ufficiale per ottenere il [prezzo orario dell'elettricit\u00e0 (PVPC)](https://www.esios.ree.es/es/pvpc) in Spagna.\nPer una spiegazione pi\u00f9 precisa, visita i [documenti di integrazione](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "Configurazione del sensore" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ja.json b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json index d88ee379678..6b85de978d3 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ja.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json @@ -10,9 +10,7 @@ "power": "\u5951\u7d04\u96fb\u529b (kW)", "power_p3": "\u8c37\u9593(valley period) P3 (kW)\u306e\u5951\u7d04\u96fb\u529b", "tariff": "\u5730\u57df\u5225\u9069\u7528\u95a2\u7a0e(Applicable tariff)" - }, - "description": "\u3053\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u516c\u5f0fAPI\u3092\u4f7f\u7528\u3057\u3066\u3001\u30b9\u30da\u30a4\u30f3\u3067\u306e[\u96fb\u6c17\u306e\u6642\u9593\u4fa1\u683c((hourly pricing of electricity)PVPC)](https://www.esios.ree.es/es/pvpc) \u3092\u53d6\u5f97\u3057\u307e\u3059\u3002\n\u3088\u308a\u6b63\u78ba\u306a\u8aac\u660e\u306b\u3064\u3044\u3066\u306f\u3001[\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3 \u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/) \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u30bb\u30f3\u30b5\u30fc\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } } } }, @@ -23,9 +21,7 @@ "power": "\u5951\u7d04\u96fb\u529b (kW)", "power_p3": "\u8c37\u9593(valley period) P3 (kW)\u306e\u5951\u7d04\u96fb\u529b", "tariff": "\u5730\u57df\u5225\u9069\u7528\u95a2\u7a0e(Applicable tariff)" - }, - "description": "\u3053\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u516c\u5f0fAPI\u3092\u4f7f\u7528\u3057\u3066\u3001\u30b9\u30da\u30a4\u30f3\u3067\u306e[\u96fb\u6c17\u306e\u6642\u9593\u4fa1\u683c(hourly pricing of electricity)PVPC)](https://www.esios.ree.es/es/pvpc) \u3092\u53d6\u5f97\u3057\u307e\u3059\u3002\n\u3088\u308a\u6b63\u78ba\u306a\u8aac\u660e\u306b\u3064\u3044\u3066\u306f\u3001[\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3 \u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/) \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u30bb\u30f3\u30b5\u30fc\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ko.json b/homeassistant/components/pvpc_hourly_pricing/translations/ko.json index c44d1217961..fe834a94f86 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ko.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ko.json @@ -8,9 +8,7 @@ "data": { "name": "\uc13c\uc11c \uc774\ub984", "tariff": "\uacc4\uc57d \uc694\uae08\uc81c (1, 2 \ub610\ub294 3 \uad6c\uac04)" - }, - "description": "\uc774 \uc13c\uc11c\ub294 \uacf5\uc2dd API\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc2a4\ud398\uc778\uc758 [\uc2dc\uac04\ub2f9 \uc804\uae30 \uc694\uae08 (PVPC)](https://www.esios.ree.es/es/pvpc) \uc744 \uac00\uc838\uc635\ub2c8\ub2e4.\n\ubcf4\ub2e4 \uc790\uc138\ud55c \uc124\uba85\uc740 [\uad00\ub828 \ubb38\uc11c](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.\n\n1\uc77c\ub2f9 \uccad\uad6c \uad6c\uac04\uc5d0 \ub530\ub77c \uacc4\uc57d \uc694\uae08\uc81c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.\n - 1 \uad6c\uac04: \uc77c\ubc18 \uc694\uae08\uc81c\n - 2 \uad6c\uac04: \ucc28\ub4f1 \uc694\uae08\uc81c (\uc57c\uac04 \uc694\uae08) \n - 3 \uad6c\uac04: \uc804\uae30\uc790\ub3d9\ucc28 (3 \uad6c\uac04 \uc57c\uac04 \uc694\uae08)", - "title": "\uc694\uae08\uc81c \uc120\ud0dd" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/lb.json b/homeassistant/components/pvpc_hourly_pricing/translations/lb.json index 78894094b72..d9e6d87437b 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/lb.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/lb.json @@ -8,9 +8,7 @@ "data": { "name": "Numm vum Sensor", "tariff": "Kontraktuellen Tarif (1, 2 oder 3 Perioden)" - }, - "description": "D\u00ebse Sensor benotzt d\u00e9i offiziell API fir de [Stonne Pr\u00e4is fir Elektrizit\u00e9it a Spuenien (PVPC)](https://www.esios.ree.es/es/pvpc) ze kr\u00e9ien. Fir m\u00e9i pr\u00e4zise Erkl\u00e4runge kuck [Dokumentatioun vun der Integratioun](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nWiel den Taux bas\u00e9ierend op der Unzuel vun de Rechnungsz\u00e4ite pro Dag aus:\n- 1 Period: Normal\n- 2 perioden: Nuets Tarif\n- 3 Perioden: Elektreschen Auto (Nuets Tarif fir 3 Perioden)", - "title": "Auswiel vum Tarif" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json index b4bc784dedf..9979909e040 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json @@ -10,9 +10,7 @@ "power": "Gecontracteerd vermogen (kW)", "power_p3": "Gecontracteerd vermogen voor dalperiode P3 (kW)", "tariff": "Toepasselijk tarief per geografische zone" - }, - "description": "Deze sensor gebruikt de offici\u00eble API om [uurprijs van elektriciteit (PVPC)](https://www.esios.ree.es/es/pvpc) in Spanje te verkrijgen.\n Ga voor een nauwkeurigere uitleg naar de [integratiedocumenten](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "Sensor instellen" + } } } }, @@ -23,9 +21,7 @@ "power": "Gecontracteerd vermogen (kW)", "power_p3": "Gecontracteerd vermogen voor dalperiode P3 (kW)", "tariff": "Toepasselijk tarief per geografische zone" - }, - "description": "Deze sensor maakt gebruik van offici\u00eble API om [uurprijzen van elektriciteit (PVPC)](https://www.esios.ree.es/es/pvpc) in Spanje te krijgen.\nGa voor een preciezere uitleg naar de [integratiedocumenten](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "Sensor setup" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/no.json b/homeassistant/components/pvpc_hourly_pricing/translations/no.json index 7429626674e..77dadcbcffd 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/no.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/no.json @@ -10,9 +10,7 @@ "power": "Kontrahert effekt (kW)", "power_p3": "Kontraktstr\u00f8m for dalperiode P3 (kW)", "tariff": "Gjeldende tariff etter geografisk sone" - }, - "description": "Denne sensoren bruker offisiell API for \u00e5 f\u00e5 [timeprisering av elektrisitet (PVPC)] (https://www.esios.ree.es/es/pvpc) i Spania.\n For mer presis forklaring bes\u00f8k [integrasjonsdokumentene] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "Oppsett av sensor" + } } } }, @@ -23,9 +21,7 @@ "power": "Kontrahert effekt (kW)", "power_p3": "Kontrahert kraft for dalperioden P3 (kW)", "tariff": "Gjeldende tariff etter geografisk sone" - }, - "description": "Denne sensoren bruker offisiell API for \u00e5 f\u00e5 [timeprisering av elektrisitet (PVPC)] (https://www.esios.ree.es/es/pvpc) i Spania.\n For mer presis forklaring bes\u00f8k [integrasjonsdokumentene] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "Oppsett av sensor" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/pl.json b/homeassistant/components/pvpc_hourly_pricing/translations/pl.json index 052cf66c1a6..52fb03fa985 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/pl.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/pl.json @@ -10,9 +10,7 @@ "power": "Moc zakontraktowana (kW)", "power_p3": "Moc zakontraktowana dla okresu zni\u017ckowego P3 (kW)", "tariff": "Obowi\u0105zuj\u0105ca taryfa wed\u0142ug strefy geograficznej" - }, - "description": "Ten sensor u\u017cywa oficjalnego interfejsu API w celu uzyskania [godzinowej ceny energii elektrycznej (PVPC)] (https://www.esios.ree.es/es/pvpc) w Hiszpanii. \n Aby uzyska\u0107 bardziej szczeg\u00f3\u0142owe wyja\u015bnienia, odwied\u017a [dokumentacj\u0119 dotycz\u0105c\u0105 integracji] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n Wybierz stawk\u0119 umown\u0105 na podstawie liczby okres\u00f3w rozliczeniowych dziennie: \n - 1 okres: normalny \n - 2 okresy: dyskryminacja (nocna stawka) \n - 3 okresy: samoch\u00f3d elektryczny (stawka nocna za 3 okresy)", - "title": "Konfiguracja sensora" + } } } }, @@ -23,9 +21,7 @@ "power": "Moc zakontraktowana (kW)", "power_p3": "Moc zakontraktowana dla okresu zni\u017ckowego P3 (kW)", "tariff": "Obowi\u0105zuj\u0105ca taryfa wed\u0142ug strefy geograficznej" - }, - "description": "Ten sensor u\u017cywa oficjalnego interfejsu API w celu uzyskania [godzinowej ceny energii elektrycznej (PVPC)] (https://www.esios.ree.es/es/pvpc) w Hiszpanii. \n Aby uzyska\u0107 bardziej szczeg\u00f3\u0142owe wyja\u015bnienia, odwied\u017a [dokumentacj\u0119 dotycz\u0105c\u0105 integracji] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n Wybierz stawk\u0119 umown\u0105 na podstawie liczby okres\u00f3w rozliczeniowych dziennie: \n - 1 okres: normalny \n - 2 okresy: dyskryminacja (nocna stawka) \n - 3 okresy: samoch\u00f3d elektryczny (stawka nocna za 3 okresy)", - "title": "Konfiguracja sensora" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/pt-BR.json b/homeassistant/components/pvpc_hourly_pricing/translations/pt-BR.json index e5754180a7c..7d66f5af13a 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/pt-BR.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/pt-BR.json @@ -10,9 +10,7 @@ "power": "Pot\u00eancia contratada (kW)", "power_p3": "Pot\u00eancia contratada para o per\u00edodo de vale P3 (kW)", "tariff": "Tarifa aplic\u00e1vel por zona geogr\u00e1fica" - }, - "description": "Esse sensor usa a API oficial para obter [pre\u00e7os por hora de eletricidade (PVPC)](https://www.esios.ree.es/es/pvpc) na Espanha. \nPara uma explica\u00e7\u00e3o mais precisa, visite os [documentos de integra\u00e7\u00e3o](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\nSelecione a taxa contratada com base no n\u00famero de per\u00edodos de cobran\u00e7a por dia: \n- 1 per\u00edodo: normal \n- 2 per\u00edodos: discrimina\u00e7\u00e3o (taxa noturna) \n- 3 per\u00edodos: carro el\u00e9trico (taxa noturna de 3 per\u00edodos)", - "title": "Configura\u00e7\u00e3o do sensor" + } } } }, @@ -23,9 +21,7 @@ "power": "Pot\u00eancia contratada (kW)", "power_p3": "Pot\u00eancia contratada para o per\u00edodo de vale P3 (kW)", "tariff": "Tarifa aplic\u00e1vel por zona geogr\u00e1fica" - }, - "description": "Este sensor usa a API oficial para obter [pre\u00e7os por hora de eletricidade (PVPC)](https://www.esios.ree.es/es/pvpc) na Espanha.\n Para uma explica\u00e7\u00e3o mais precisa, visite os [documentos de integra\u00e7\u00e3o](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "Configura\u00e7\u00e3o do sensor" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ru.json b/homeassistant/components/pvpc_hourly_pricing/translations/ru.json index e68bc7e6289..7994c217216 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ru.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ru.json @@ -10,9 +10,7 @@ "power": "\u0414\u043e\u0433\u043e\u0432\u043e\u0440\u043d\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c (\u043a\u0412\u0442)", "power_p3": "\u0414\u043e\u0433\u043e\u0432\u043e\u0440\u043d\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c \u043d\u0430 \u043f\u0435\u0440\u0438\u043e\u0434 P3 (\u043a\u0412\u0442)", "tariff": "\u041f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u043c\u044b\u0439 \u0442\u0430\u0440\u0438\u0444 \u043f\u043e \u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u0437\u043e\u043d\u0435" - }, - "description": "\u042d\u0442\u043e\u0442 \u0441\u0435\u043d\u0441\u043e\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 API \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f [\u043f\u043e\u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0446\u0435\u043d\u044b \u0437\u0430 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u044d\u043d\u0435\u0440\u0433\u0438\u044e (PVPC)](https://www.esios.ree.es/es/pvpc) \u0432 \u0418\u0441\u043f\u0430\u043d\u0438\u0438.\n\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438, \u043e\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](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430" + } } } }, @@ -23,9 +21,7 @@ "power": "\u0414\u043e\u0433\u043e\u0432\u043e\u0440\u043d\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c (\u043a\u0412\u0442)", "power_p3": "\u0414\u043e\u0433\u043e\u0432\u043e\u0440\u043d\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c \u043d\u0430 \u043f\u0435\u0440\u0438\u043e\u0434 P3 (\u043a\u0412\u0442)", "tariff": "\u041f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u043c\u044b\u0439 \u0442\u0430\u0440\u0438\u0444 \u043f\u043e \u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u0437\u043e\u043d\u0435" - }, - "description": "\u042d\u0442\u043e\u0442 \u0441\u0435\u043d\u0441\u043e\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 API \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f [\u043f\u043e\u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0446\u0435\u043d\u044b \u0437\u0430 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u044d\u043d\u0435\u0440\u0433\u0438\u044e (PVPC)](https://www.esios.ree.es/es/pvpc) \u0432 \u0418\u0441\u043f\u0430\u043d\u0438\u0438.\n\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438, \u043e\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](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/sl.json b/homeassistant/components/pvpc_hourly_pricing/translations/sl.json index c6765575c07..cc41e12b4b2 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/sl.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/sl.json @@ -8,9 +8,7 @@ "data": { "name": "Ime tipala", "tariff": "Pogodbena tarifa (1, 2 ali 3 obdobja)" - }, - "description": "Ta senzor uporablja uradni API za [urno dolo\u010danje cen elektri\u010dne energije (PVPC)] (https://www.esios.ree.es/es/pvpc) v \u0160paniji. \n Za natan\u010dnej\u0161o razlago obi\u0161\u010dite [integracijski dokumenti] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n Izberite pogodbeno tarifo glede na \u0161tevilo obra\u010dunskih obdobij na dan: \n - 1 obdobje: normalno \n - 2 obdobji: diskriminacija (no\u010dna cena) \n - 3 obdobja: elektri\u010dni avtomobil (no\u010dna cena 3 obdobja)", - "title": "Izbira tarife" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/tr.json b/homeassistant/components/pvpc_hourly_pricing/translations/tr.json index 5d27e2bb033..908f04f6622 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/tr.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/tr.json @@ -10,9 +10,7 @@ "power": "S\u00f6zle\u015fmeli g\u00fc\u00e7 (kW)", "power_p3": "Vadi d\u00f6nemi i\u00e7in taahh\u00fct edilen g\u00fc\u00e7 P3 (kW)", "tariff": "Co\u011frafi b\u00f6lgeye g\u00f6re ge\u00e7erli tarife" - }, - "description": "Bu sens\u00f6r, \u0130spanya'da [saatlik elektrik fiyatland\u0131rmas\u0131 (PVPC)](https://www.esios.ree.es/es/pvpc) almak i\u00e7in resmi API'yi kullan\u0131r.\n Daha kesin a\u00e7\u0131klama i\u00e7in [entegrasyon belgelerini](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/) ziyaret edin.", - "title": "Sens\u00f6r kurulumu" + } } } }, @@ -23,9 +21,7 @@ "power": "S\u00f6zle\u015fmeli g\u00fc\u00e7 (kW)", "power_p3": "Vadi d\u00f6nemi i\u00e7in taahh\u00fct edilen g\u00fc\u00e7 P3 (kW)", "tariff": "Co\u011frafi b\u00f6lgeye g\u00f6re ge\u00e7erli tarife" - }, - "description": "Bu sens\u00f6r, \u0130spanya'da [saatlik elektrik fiyatland\u0131rmas\u0131 (PVPC)](https://www.esios.ree.es/es/pvpc) almak i\u00e7in resmi API'yi kullan\u0131r.\n Daha kesin a\u00e7\u0131klama i\u00e7in [entegrasyon belgelerini](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/) ziyaret edin.", - "title": "Sens\u00f6r kurulumu" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/uk.json b/homeassistant/components/pvpc_hourly_pricing/translations/uk.json index da2136d7765..bc6d06b1b04 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/uk.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/uk.json @@ -8,9 +8,7 @@ "data": { "name": "\u041d\u0430\u0437\u0432\u0430", "tariff": "\u041a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u043d\u0438\u0439 \u0442\u0430\u0440\u0438\u0444 (1, 2 \u0430\u0431\u043e 3 \u043f\u0435\u0440\u0456\u043e\u0434\u0438)" - }, - "description": "\u0426\u0435\u0439 \u0441\u0435\u043d\u0441\u043e\u0440 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454 \u043e\u0444\u0456\u0446\u0456\u0439\u043d\u0438\u0439 API \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f [\u043f\u043e\u0433\u043e\u0434\u0438\u043d\u043d\u043e\u0457 \u0446\u0456\u043d\u0438 \u0437\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u0435\u043d\u0435\u0440\u0433\u0456\u044e (PVPC)] (https://www.esios.ree.es/es/pvpc) \u0432 \u0406\u0441\u043f\u0430\u043d\u0456\u0457.\n\u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u0435\u0442\u0430\u043b\u044c\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044c \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0430\u0440\u0438\u0444, \u0437\u0430\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u0439 \u043d\u0430 \u043a\u0456\u043b\u044c\u043a\u043e\u0441\u0442\u0456 \u0440\u043e\u0437\u0440\u0430\u0445\u0443\u043d\u043a\u043e\u0432\u0438\u0445 \u043f\u0435\u0440\u0456\u043e\u0434\u0456\u0432 \u0432 \u0434\u0435\u043d\u044c:\n- 1 \u043f\u0435\u0440\u0456\u043e\u0434: normal\n- 2 \u043f\u0435\u0440\u0456\u043e\u0434\u0438: discrimination (nightly rate)\n- 3 \u043f\u0435\u0440\u0456\u043e\u0434\u0438: electric car (nightly rate of 3 periods)", - "title": "\u0412\u0438\u0431\u0456\u0440 \u0442\u0430\u0440\u0438\u0444\u0443" + } } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json b/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json index ace4cce089a..35ace573ead 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json @@ -10,9 +10,7 @@ "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\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" + } } } }, @@ -23,9 +21,7 @@ "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\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/recorder/translations/ko.json b/homeassistant/components/recorder/translations/ko.json index b8cd6320a55..18cc73f3789 100644 --- a/homeassistant/components/recorder/translations/ko.json +++ b/homeassistant/components/recorder/translations/ko.json @@ -1,7 +1,9 @@ { "system_health": { "info": { - "estimated_db_size": "\uc608\uc0c1 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \ud06c\uae30(MiB)" + "current_recorder_run": "\ud604\uc7ac \uc2e4\ud589 \uc2dc\uc791 \uc2dc\uac04", + "estimated_db_size": "\uc608\uc0c1 \ub370\uc774\ud130\ubca0\uc774\uc2a4 \ud06c\uae30(MiB)", + "oldest_recorder_run": "\uac00\uc7a5 \uc624\ub798\ub41c \uc2e4\ud589 \uc2dc\uc791 \uc2dc\uac04" } } } \ No newline at end of file diff --git a/homeassistant/components/remote/translations/ca.json b/homeassistant/components/remote/translations/ca.json index ae9184b8ea5..6bf9dc141e0 100644 --- a/homeassistant/components/remote/translations/ca.json +++ b/homeassistant/components/remote/translations/ca.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} s'enc\u00e9n o s'apaga", - "toggled": "{entity_name} s'activa o es desactiva", "turned_off": "{entity_name} s'ha apagat", "turned_on": "{entity_name} s'ha engegat" } diff --git a/homeassistant/components/remote/translations/cs.json b/homeassistant/components/remote/translations/cs.json index c2afd70cb95..6907021e393 100644 --- a/homeassistant/components/remote/translations/cs.json +++ b/homeassistant/components/remote/translations/cs.json @@ -10,7 +10,6 @@ "is_on": "{entity_name} je zapnuto" }, "trigger_type": { - "toggled": "{entity_name} zapnuto nebo vypnuto", "turned_off": "{entity_name} vypnuto", "turned_on": "{entity_name} zapnuto" } diff --git a/homeassistant/components/remote/translations/de.json b/homeassistant/components/remote/translations/de.json index d0ad3d23ba2..8c34008da15 100644 --- a/homeassistant/components/remote/translations/de.json +++ b/homeassistant/components/remote/translations/de.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} ein- oder ausgeschaltet", - "toggled": "{entity_name} ein- oder ausgeschaltet", "turned_off": "{entity_name} ausgeschaltet", "turned_on": "{entity_name} eingeschaltet" } diff --git a/homeassistant/components/remote/translations/el.json b/homeassistant/components/remote/translations/el.json index 431d2e8af79..828f8b7fcb9 100644 --- a/homeassistant/components/remote/translations/el.json +++ b/homeassistant/components/remote/translations/el.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "\u03a4\u03bf {entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5 \u03ae \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", - "toggled": "{entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5 \u03ae \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", "turned_off": "{entity_name} \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", "turned_on": "{entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5" } diff --git a/homeassistant/components/remote/translations/en.json b/homeassistant/components/remote/translations/en.json index a232919b38a..2574de7e74d 100644 --- a/homeassistant/components/remote/translations/en.json +++ b/homeassistant/components/remote/translations/en.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} turned on or off", - "toggled": "{entity_name} turned on or off", "turned_off": "{entity_name} turned off", "turned_on": "{entity_name} turned on" } diff --git a/homeassistant/components/remote/translations/es.json b/homeassistant/components/remote/translations/es.json index dfa90cb1cc8..31e68384b8b 100644 --- a/homeassistant/components/remote/translations/es.json +++ b/homeassistant/components/remote/translations/es.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} activado o desactivado", - "toggled": "{entity_name} activado o desactivado", "turned_off": "{entity_name} desactivado", "turned_on": "{entity_name} activado" } diff --git a/homeassistant/components/remote/translations/et.json b/homeassistant/components/remote/translations/et.json index 12918472f33..aea377b6cb0 100644 --- a/homeassistant/components/remote/translations/et.json +++ b/homeassistant/components/remote/translations/et.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} l\u00fclitus sisse v\u00f5i v\u00e4lja", - "toggled": "{entity_name} l\u00fclitus sisse v\u00f5i v\u00e4lja", "turned_off": "{entity_name} l\u00fclitus v\u00e4lja", "turned_on": "{entity_name} l\u00fclitus sisse" } diff --git a/homeassistant/components/remote/translations/fr.json b/homeassistant/components/remote/translations/fr.json index 1560c52cba0..6b81eea5a41 100644 --- a/homeassistant/components/remote/translations/fr.json +++ b/homeassistant/components/remote/translations/fr.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", - "toggled": "{entity_name} activ\u00e9 ou d\u00e9sactiv\u00e9", "turned_off": "{entity_name} s'est \u00e9teint", "turned_on": "{entity_name} s'est allum\u00e9" } diff --git a/homeassistant/components/remote/translations/he.json b/homeassistant/components/remote/translations/he.json index acdd036b2cf..6e4857c217c 100644 --- a/homeassistant/components/remote/translations/he.json +++ b/homeassistant/components/remote/translations/he.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc \u05d0\u05d5 \u05db\u05d5\u05d1\u05d4", - "toggled": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc \u05d0\u05d5 \u05db\u05d5\u05d1\u05d4", "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4", "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc" } diff --git a/homeassistant/components/remote/translations/hu.json b/homeassistant/components/remote/translations/hu.json index 52b24f79f08..039e67efb6c 100644 --- a/homeassistant/components/remote/translations/hu.json +++ b/homeassistant/components/remote/translations/hu.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} be- vagy kikapcsolt", - "toggled": "{entity_name} \u00e1tkapcsolt", "turned_off": "{entity_name} ki lett kapcsolva", "turned_on": "{entity_name} be lett kapcsolva" } diff --git a/homeassistant/components/remote/translations/id.json b/homeassistant/components/remote/translations/id.json index 34eaa019be2..1853288f1ef 100644 --- a/homeassistant/components/remote/translations/id.json +++ b/homeassistant/components/remote/translations/id.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} diaktifkan atau dinonaktifkan", - "toggled": "{entity_name} diaktifkan atau dinonaktifkan", "turned_off": "{entity_name} dimatikan", "turned_on": "{entity_name} dinyalakan" } diff --git a/homeassistant/components/remote/translations/it.json b/homeassistant/components/remote/translations/it.json index 381894615d3..51fbdc6b755 100644 --- a/homeassistant/components/remote/translations/it.json +++ b/homeassistant/components/remote/translations/it.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} attivata o disattivata", - "toggled": "{entity_name} attiva o disattiva", "turned_off": "{entity_name} disattivato", "turned_on": "{entity_name} attivato" } diff --git a/homeassistant/components/remote/translations/ja.json b/homeassistant/components/remote/translations/ja.json index 304a47ed6d5..5c541a16e90 100644 --- a/homeassistant/components/remote/translations/ja.json +++ b/homeassistant/components/remote/translations/ja.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} \u304c\u30aa\u30f3\u307e\u305f\u306f\u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", - "toggled": "{entity_name} \u304c\u30aa\u30f3\u307e\u305f\u306f\u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059" } diff --git a/homeassistant/components/remote/translations/nl.json b/homeassistant/components/remote/translations/nl.json index 47ba3d7eda7..b512f1b55aa 100644 --- a/homeassistant/components/remote/translations/nl.json +++ b/homeassistant/components/remote/translations/nl.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} in- of uitgeschakeld", - "toggled": "{entity_name} in- of uitgeschakeld", "turned_off": "{entity_name} uitgeschakeld", "turned_on": "{entity_name} ingeschakeld" } diff --git a/homeassistant/components/remote/translations/no.json b/homeassistant/components/remote/translations/no.json index e563a6aa7d8..b6eba880981 100644 --- a/homeassistant/components/remote/translations/no.json +++ b/homeassistant/components/remote/translations/no.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} sl\u00e5tt p\u00e5 eller av", - "toggled": "{entity_name} sl\u00e5tt p\u00e5 eller av", "turned_off": "{entity_name} sl\u00e5tt av", "turned_on": "{entity_name} sl\u00e5tt p\u00e5" } diff --git a/homeassistant/components/remote/translations/pl.json b/homeassistant/components/remote/translations/pl.json index 2aaaf6dbd01..3c538ee2d03 100644 --- a/homeassistant/components/remote/translations/pl.json +++ b/homeassistant/components/remote/translations/pl.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", - "toggled": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}" } diff --git a/homeassistant/components/remote/translations/pt-BR.json b/homeassistant/components/remote/translations/pt-BR.json index 15d9b0d7c76..c2cbe8991c9 100644 --- a/homeassistant/components/remote/translations/pt-BR.json +++ b/homeassistant/components/remote/translations/pt-BR.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} ligado ou desligado", - "toggled": "{entity_name} ligado ou desligado", "turned_off": "{entity_name} for desligado", "turned_on": "{entity_name} for ligado" } diff --git a/homeassistant/components/remote/translations/ru.json b/homeassistant/components/remote/translations/ru.json index fe1407af417..4224eb7f2b5 100644 --- a/homeassistant/components/remote/translations/ru.json +++ b/homeassistant/components/remote/translations/ru.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0438\u043b\u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", - "toggled": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0438\u043b\u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" } diff --git a/homeassistant/components/remote/translations/sv.json b/homeassistant/components/remote/translations/sv.json index eeb5d486d25..1b6584c5bf8 100644 --- a/homeassistant/components/remote/translations/sv.json +++ b/homeassistant/components/remote/translations/sv.json @@ -9,7 +9,6 @@ "is_on": "{entity_name} \u00e4r p\u00e5" }, "trigger_type": { - "toggled": "{entity_name} slogs p\u00e5 eller av", "turned_off": "{entity_name} st\u00e4ngdes av", "turned_on": "{entity_name} slogs p\u00e5" } diff --git a/homeassistant/components/remote/translations/tr.json b/homeassistant/components/remote/translations/tr.json index 0b4fde944de..dc8fbf1ecad 100644 --- a/homeassistant/components/remote/translations/tr.json +++ b/homeassistant/components/remote/translations/tr.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} a\u00e7\u0131ld\u0131 veya kapat\u0131ld\u0131", - "toggled": "{entity_name} a\u00e7\u0131ld\u0131 veya kapat\u0131ld\u0131", "turned_off": "{entity_name} kapat\u0131ld\u0131", "turned_on": "{entity_name} a\u00e7\u0131ld\u0131" } diff --git a/homeassistant/components/remote/translations/zh-Hans.json b/homeassistant/components/remote/translations/zh-Hans.json index b6c5d88102f..f6c509d4a08 100644 --- a/homeassistant/components/remote/translations/zh-Hans.json +++ b/homeassistant/components/remote/translations/zh-Hans.json @@ -10,7 +10,6 @@ "is_on": "{entity_name} \u5df2\u6253\u5f00" }, "trigger_type": { - "toggled": "{entity_name} \u88ab\u6253\u5f00\u6216\u5173\u95ed", "turned_off": "{entity_name} \u88ab\u5173\u95ed", "turned_on": "{entity_name} \u88ab\u6253\u5f00" } diff --git a/homeassistant/components/remote/translations/zh-Hant.json b/homeassistant/components/remote/translations/zh-Hant.json index e8a07049e29..259121d7a4f 100644 --- a/homeassistant/components/remote/translations/zh-Hant.json +++ b/homeassistant/components/remote/translations/zh-Hant.json @@ -11,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name}\u5df2\u958b\u555f\u6216\u95dc\u9589", - "toggled": "{entity_name}\u5df2\u958b\u555f\u6216\u95dc\u9589", "turned_off": "{entity_name}\u5df2\u95dc\u9589", "turned_on": "{entity_name}\u5df2\u958b\u555f" } diff --git a/homeassistant/components/rfxtrx/translations/ca.json b/homeassistant/components/rfxtrx/translations/ca.json index a8a4f958cb6..edcd3a7e558 100644 --- a/homeassistant/components/rfxtrx/translations/ca.json +++ b/homeassistant/components/rfxtrx/translations/ca.json @@ -61,8 +61,7 @@ "debug": "Activa la depuraci\u00f3", "device": "Selecciona el dispositiu a configurar", "event_code": "Introdueix el codi de l'esdeveniment a afegir", - "protocols": "Protocols", - "remove_device": "Selecciona el dispositiu a eliminar" + "protocols": "Protocols" }, "title": "Opcions de Rfxtrx" }, @@ -71,11 +70,9 @@ "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", "replace_device": "Selecciona el dispositiu a substituir", - "signal_repetitions": "Nombre de repeticions del senyal", "venetian_blind_mode": "Mode persiana veneciana" }, "title": "Configuraci\u00f3 de les opcions del dispositiu" diff --git a/homeassistant/components/rfxtrx/translations/cs.json b/homeassistant/components/rfxtrx/translations/cs.json index 706d4499fb2..17fea91836c 100644 --- a/homeassistant/components/rfxtrx/translations/cs.json +++ b/homeassistant/components/rfxtrx/translations/cs.json @@ -50,18 +50,15 @@ "automatic_add": "Povolit automatick\u00e9 p\u0159id\u00e1n\u00ed", "debug": "Povolit lad\u011bn\u00ed", "device": "Vyberte za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit", - "event_code": "Zadejte k\u00f3d ud\u00e1losti, kterou chcete p\u0159idat", - "remove_device": "Vyberte za\u0159\u00edzen\u00ed, kter\u00e9 chcete odstranit" + "event_code": "Zadejte k\u00f3d ud\u00e1losti, kterou chcete p\u0159idat" }, "title": "Mo\u017enosti Rfxtrx" }, "set_device_options": { "data": { - "fire_event": "Povolit ud\u00e1lost za\u0159\u00edzen\u00ed", "off_delay": "Zpo\u017ed\u011bn\u00ed vypnut\u00ed", "off_delay_enabled": "Povolit zpo\u017ed\u011bn\u00ed vypnut\u00ed", - "replace_device": "Vyberte za\u0159\u00edzen\u00ed, kter\u00e9 chcete vym\u011bnit", - "signal_repetitions": "Po\u010det opakov\u00e1n\u00ed sign\u00e1lu" + "replace_device": "Vyberte za\u0159\u00edzen\u00ed, kter\u00e9 chcete vym\u011bnit" }, "title": "Nastaven\u00ed mo\u017enost\u00ed za\u0159\u00edzen\u00ed" } diff --git a/homeassistant/components/rfxtrx/translations/de.json b/homeassistant/components/rfxtrx/translations/de.json index a80bde85b0b..445a93b7e76 100644 --- a/homeassistant/components/rfxtrx/translations/de.json +++ b/homeassistant/components/rfxtrx/translations/de.json @@ -61,8 +61,7 @@ "debug": "Debugging aktivieren", "device": "Zu konfigurierendes Ger\u00e4t ausw\u00e4hlen", "event_code": "Ereigniscode zum Hinzuf\u00fcgen eingeben", - "protocols": "Protokolle", - "remove_device": "Zu l\u00f6schendes Ger\u00e4t ausw\u00e4hlen" + "protocols": "Protokolle" }, "title": "Rfxtrx Optionen" }, @@ -71,11 +70,9 @@ "command_off": "Datenbitwert f\u00fcr den Befehl \"aus\"", "command_on": "Datenbitwert f\u00fcr den Befehl \"ein\"", "data_bit": "Anzahl der Datenbits", - "fire_event": "Ger\u00e4teereignis aktivieren", "off_delay": "Ausschaltverz\u00f6gerung", "off_delay_enabled": "Ausschaltverz\u00f6gerung aktivieren", "replace_device": "W\u00e4hle ein Ger\u00e4t aus, das ersetzt werden soll", - "signal_repetitions": "Anzahl der Signalwiederholungen", "venetian_blind_mode": "Jalousie-Modus" }, "title": "Ger\u00e4teoptionen konfigurieren" diff --git a/homeassistant/components/rfxtrx/translations/el.json b/homeassistant/components/rfxtrx/translations/el.json index 8d55c7252a0..950982df50a 100644 --- a/homeassistant/components/rfxtrx/translations/el.json +++ b/homeassistant/components/rfxtrx/translations/el.json @@ -61,8 +61,7 @@ "debug": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03bf\u03cd \u03c3\u03c6\u03b1\u03bb\u03bc\u03ac\u03c4\u03c9\u03bd", "device": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd", "event_code": "\u0395\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03bf\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7", - "protocols": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03b1", - "remove_device": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae" + "protocols": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03b1" }, "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 Rfxtrx" }, @@ -71,11 +70,9 @@ "command_off": "\u03a4\u03b9\u03bc\u03ae bit \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03b3\u03b9\u03b1 \u03b5\u03bd\u03c4\u03bf\u03bb\u03ae off", "command_on": "\u03a4\u03b9\u03bc\u03ae bit \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03b3\u03b9\u03b1 \u03b5\u03bd\u03c4\u03bf\u03bb\u03ae on", "data_bit": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 bit \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd", - "fire_event": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03bf\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", "off_delay": "\u039a\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2", "off_delay_enabled": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03ba\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7\u03c2 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2", "replace_device": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7", - "signal_repetitions": "\u0391\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b5\u03c0\u03b1\u03bd\u03b1\u03bb\u03ae\u03c8\u03b5\u03c9\u03bd \u03c3\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2", "venetian_blind_mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b2\u03b5\u03bd\u03b5\u03c4\u03c3\u03b9\u03ac\u03bd\u03b9\u03ba\u03b7\u03c2 \u03c0\u03b5\u03c1\u03c3\u03af\u03b4\u03b1\u03c2" }, "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" diff --git a/homeassistant/components/rfxtrx/translations/en.json b/homeassistant/components/rfxtrx/translations/en.json index af042719f77..9a509246f5a 100644 --- a/homeassistant/components/rfxtrx/translations/en.json +++ b/homeassistant/components/rfxtrx/translations/en.json @@ -61,8 +61,7 @@ "debug": "Enable debugging", "device": "Select device to configure", "event_code": "Enter event code to add", - "protocols": "Protocols", - "remove_device": "Select device to delete" + "protocols": "Protocols" }, "title": "Rfxtrx Options" }, @@ -71,11 +70,9 @@ "command_off": "Data bits value for command off", "command_on": "Data bits value for command on", "data_bit": "Number of data bits", - "fire_event": "Enable device event", "off_delay": "Off delay", "off_delay_enabled": "Enable off delay", "replace_device": "Select device to replace", - "signal_repetitions": "Number of signal repetitions", "venetian_blind_mode": "Venetian blind mode" }, "title": "Configure device options" diff --git a/homeassistant/components/rfxtrx/translations/es.json b/homeassistant/components/rfxtrx/translations/es.json index ec7777ee0d4..997a6b0a0bf 100644 --- a/homeassistant/components/rfxtrx/translations/es.json +++ b/homeassistant/components/rfxtrx/translations/es.json @@ -61,8 +61,7 @@ "debug": "Activar la depuraci\u00f3n", "device": "Seleccionar dispositivo para configurar", "event_code": "Introducir el c\u00f3digo de evento para a\u00f1adir", - "protocols": "Protocolos", - "remove_device": "Selecciona el dispositivo a eliminar" + "protocols": "Protocolos" }, "title": "Opciones de Rfxtrx" }, @@ -71,11 +70,9 @@ "command_off": "Valor de bits de datos para comando apagado", "command_on": "Valor de bits de datos para comando activado", "data_bit": "N\u00famero de bits de datos", - "fire_event": "Habilitar evento del dispositivo", "off_delay": "Retraso de apagado", "off_delay_enabled": "Activar retardo de apagado", "replace_device": "Seleccione el dispositivo que desea reemplazar", - "signal_repetitions": "N\u00famero de repeticiones de la se\u00f1al", "venetian_blind_mode": "Modo de persiana veneciana" }, "title": "Configurar las opciones del dispositivo" diff --git a/homeassistant/components/rfxtrx/translations/et.json b/homeassistant/components/rfxtrx/translations/et.json index 2c4a36b1664..eff49a8a6c4 100644 --- a/homeassistant/components/rfxtrx/translations/et.json +++ b/homeassistant/components/rfxtrx/translations/et.json @@ -61,8 +61,7 @@ "debug": "Luba silumine", "device": "Vali seadistatav seade", "event_code": "Sisesta lisatava s\u00fcndmuse kood", - "protocols": "Protokollid", - "remove_device": "Vali eemaldatav seade" + "protocols": "Protokollid" }, "title": "Rfxtrx valikud" }, @@ -71,11 +70,9 @@ "command_off": "V\u00e4ljal\u00fclitamise k\u00e4su andmebittide v\u00e4\u00e4rtus", "command_on": "Sissel\u00fclitamise k\u00e4su andmebittide v\u00e4\u00e4rtus", "data_bit": "Andmebittide arv", - "fire_event": "Luba seadme s\u00fcndmus", "off_delay": "V\u00e4ljal\u00fclitamise viivitus", "off_delay_enabled": "Luba v\u00e4ljal\u00fclitusviivitus", "replace_device": "Vali asendav seade", - "signal_repetitions": "Signaali korduste arv", "venetian_blind_mode": "Ribikardinate juhtimine" }, "title": "Seadista seadme valikud" diff --git a/homeassistant/components/rfxtrx/translations/fr.json b/homeassistant/components/rfxtrx/translations/fr.json index 5eaff1ce406..9e31b450ce7 100644 --- a/homeassistant/components/rfxtrx/translations/fr.json +++ b/homeassistant/components/rfxtrx/translations/fr.json @@ -66,8 +66,7 @@ "debug": "Activer le d\u00e9bogage", "device": "S\u00e9lectionnez l'appareil \u00e0 configurer", "event_code": "Entrez le code d'\u00e9v\u00e9nement \u00e0 ajouter", - "protocols": "Protocoles", - "remove_device": "S\u00e9lectionnez l'appareil \u00e0 supprimer" + "protocols": "Protocoles" }, "title": "Options Rfxtrx" }, @@ -76,11 +75,9 @@ "command_off": "Valeur des bits de donn\u00e9es pour la commande \u00e9teindre", "command_on": "Valeur des bits de donn\u00e9es pour la commande allumer", "data_bit": "Nombre de bits de donn\u00e9es", - "fire_event": "Activer les \u00e9v\u00e9nements d'appareil", "off_delay": "D\u00e9lai d'arr\u00eat", "off_delay_enabled": "Activer le d\u00e9lai d'arr\u00eat", "replace_device": "S\u00e9lectionnez l'appareil \u00e0 remplacer", - "signal_repetitions": "Nombre de r\u00e9p\u00e9titions du signal", "venetian_blind_mode": "Mode store v\u00e9nitien" }, "title": "Configurer les options de l'appareil" diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json index a6f1c925fbf..158411ef618 100644 --- a/homeassistant/components/rfxtrx/translations/hu.json +++ b/homeassistant/components/rfxtrx/translations/hu.json @@ -66,8 +66,7 @@ "debug": "Enged\u00e9lyezze a hibakeres\u00e9st", "device": "V\u00e1lassza ki a konfigur\u00e1lni k\u00edv\u00e1nt eszk\u00f6zt", "event_code": "\u00cdrja be a hozz\u00e1adni k\u00edv\u00e1nt esem\u00e9ny k\u00f3dj\u00e1t", - "protocols": "Protokollok", - "remove_device": "V\u00e1lassza ki a t\u00f6r\u00f6lni k\u00edv\u00e1nt eszk\u00f6zt" + "protocols": "Protokollok" }, "title": "Rfxtrx opci\u00f3k" }, @@ -76,11 +75,9 @@ "command_off": "Adatbitek \u00e9rt\u00e9ke a parancs kikapcsol\u00e1s\u00e1hoz", "command_on": "Adatbitek \u00e9rt\u00e9ke a parancshoz", "data_bit": "Adatbitek sz\u00e1ma", - "fire_event": "Eszk\u00f6zesem\u00e9ny enged\u00e9lyez\u00e9se", "off_delay": "Kikapcsol\u00e1si k\u00e9sleltet\u00e9s", "off_delay_enabled": "Kikapcsol\u00e1si k\u00e9sleltet\u00e9s enged\u00e9lyez\u00e9se", "replace_device": "V\u00e1lassza ki a cser\u00e9lni k\u00edv\u00e1nt eszk\u00f6zt", - "signal_repetitions": "A jelism\u00e9tl\u00e9sek sz\u00e1ma", "venetian_blind_mode": "Velencei red\u0151ny \u00fczemm\u00f3d" }, "title": "Konfigur\u00e1lja az eszk\u00f6z be\u00e1ll\u00edt\u00e1sait" diff --git a/homeassistant/components/rfxtrx/translations/id.json b/homeassistant/components/rfxtrx/translations/id.json index cea00e08f9c..fa39b4c4a2e 100644 --- a/homeassistant/components/rfxtrx/translations/id.json +++ b/homeassistant/components/rfxtrx/translations/id.json @@ -61,8 +61,7 @@ "debug": "Aktifkan debugging", "device": "Pilih perangkat untuk dikonfigurasi", "event_code": "Masukkan kode event untuk ditambahkan", - "protocols": "Protokol", - "remove_device": "Pilih perangkat yang akan dihapus" + "protocols": "Protokol" }, "title": "Opsi Rfxtrx" }, @@ -71,11 +70,9 @@ "command_off": "Nilai bit data untuk perintah mematikan", "command_on": "Nilai bit data untuk perintah menyalakan", "data_bit": "Jumlah bit data", - "fire_event": "Aktifkan event perangkat", "off_delay": "Penundaan mematikan", "off_delay_enabled": "Aktifkan penundaan mematikan", "replace_device": "Pilih perangkat yang akan diganti", - "signal_repetitions": "Jumlah pengulangan sinyal", "venetian_blind_mode": "Mode penutup kerai Venesia" }, "title": "Konfigurasi opsi perangkat" diff --git a/homeassistant/components/rfxtrx/translations/it.json b/homeassistant/components/rfxtrx/translations/it.json index 5e5f07a013f..bcf9cebd99d 100644 --- a/homeassistant/components/rfxtrx/translations/it.json +++ b/homeassistant/components/rfxtrx/translations/it.json @@ -66,8 +66,7 @@ "debug": "Attiva il debug", "device": "Seleziona il dispositivo da configurare", "event_code": "Inserire il codice dell'evento da aggiungere", - "protocols": "Protocolli", - "remove_device": "Seleziona il dispositivo da eliminare" + "protocols": "Protocolli" }, "title": "Opzioni Rfxtrx" }, @@ -76,11 +75,9 @@ "command_off": "Valore dei bit di dati per il comando disattivato", "command_on": "Valore dei bit di dati per il comando attivato", "data_bit": "Numero di bit di dati", - "fire_event": "Abilita evento dispositivo", "off_delay": "Ritardo di spegnimento", "off_delay_enabled": "Attiva il ritardo di spegnimento", "replace_device": "Seleziona il dispositivo da sostituire", - "signal_repetitions": "Numero di ripetizioni del segnale", "venetian_blind_mode": "Modalit\u00e0 veneziana" }, "title": "Configura le opzioni del dispositivo" diff --git a/homeassistant/components/rfxtrx/translations/ja.json b/homeassistant/components/rfxtrx/translations/ja.json index cb79cc60a8e..9b22d34af58 100644 --- a/homeassistant/components/rfxtrx/translations/ja.json +++ b/homeassistant/components/rfxtrx/translations/ja.json @@ -61,8 +61,7 @@ "debug": "\u30c7\u30d0\u30c3\u30b0\u306e\u6709\u52b9\u5316", "device": "\u8a2d\u5b9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e", "event_code": "\u30a4\u30d9\u30f3\u30c8\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u8ffd\u52a0", - "protocols": "\u30d7\u30ed\u30c8\u30b3\u30eb", - "remove_device": "\u524a\u9664\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u306e\u9078\u629e" + "protocols": "\u30d7\u30ed\u30c8\u30b3\u30eb" }, "title": "Rfxtrx\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" }, @@ -71,11 +70,9 @@ "command_off": "\u30b3\u30de\u30f3\u30c9\u30aa\u30d5\u306e\u30c7\u30fc\u30bf\u30d3\u30c3\u30c8\u5024", "command_on": "\u30b3\u30de\u30f3\u30c9\u30aa\u30f3\u306e\u30c7\u30fc\u30bf\u30d3\u30c3\u30c8\u5024", "data_bit": "\u30c7\u30fc\u30bf\u30d3\u30c3\u30c8\u6570", - "fire_event": "\u30c7\u30d0\u30a4\u30b9 \u30a4\u30d9\u30f3\u30c8\u3092\u6709\u52b9\u306b\u3059\u308b", "off_delay": "\u30aa\u30d5\u9045\u5ef6", "off_delay_enabled": "\u30aa\u30d5\u9045\u5ef6\u3092\u6709\u52b9\u306b\u3059\u308b", "replace_device": "\u4ea4\u63db\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e", - "signal_repetitions": "\u4fe1\u53f7\u306e\u30ea\u30d4\u30fc\u30c8\u6570", "venetian_blind_mode": "Venetian blind\u30e2\u30fc\u30c9" }, "title": "\u30c7\u30d0\u30a4\u30b9\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a" diff --git a/homeassistant/components/rfxtrx/translations/ko.json b/homeassistant/components/rfxtrx/translations/ko.json index 891926083dd..0434e5b75ab 100644 --- a/homeassistant/components/rfxtrx/translations/ko.json +++ b/homeassistant/components/rfxtrx/translations/ko.json @@ -50,8 +50,7 @@ "automatic_add": "\uc790\ub3d9 \ucd94\uac00 \ud65c\uc131\ud654\ud558\uae30", "debug": "\ub514\ubc84\uae45 \ud65c\uc131\ud654\ud558\uae30", "device": "\uad6c\uc131\ud560 \uae30\uae30 \uc120\ud0dd\ud558\uae30", - "event_code": "\ucd94\uac00\ud560 \uc774\ubca4\ud2b8 \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", - "remove_device": "\uc0ad\uc81c\ud560 \uae30\uae30 \uc120\ud0dd\ud558\uae30" + "event_code": "\ucd94\uac00\ud560 \uc774\ubca4\ud2b8 \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" }, "title": "Rfxtrx \uc635\uc158" }, @@ -60,11 +59,9 @@ "command_off": "\ub044\uae30 \uba85\ub839\uc5d0 \ub300\ud55c \ub370\uc774\ud130 \ube44\ud2b8 \uac12", "command_on": "\ucf1c\uae30 \uba85\ub839\uc5d0 \ub300\ud55c \ub370\uc774\ud130 \ube44\ud2b8 \uac12", "data_bit": "\ub370\uc774\ud130 \ube44\ud2b8 \uc218", - "fire_event": "\uae30\uae30 \uc774\ubca4\ud2b8 \ud65c\uc131\ud654\ud558\uae30", "off_delay": "\uc790\ub3d9 \uaebc\uc9c4 \uc0c1\ud0dc", "off_delay_enabled": "\uc790\ub3d9 \uaebc\uc9c4 \uc0c1\ud0dc(Off Delay) \ud65c\uc131\ud654\ud558\uae30", "replace_device": "\uad50\uccb4\ud560 \uae30\uae30 \uc120\ud0dd\ud558\uae30", - "signal_repetitions": "\uc2e0\ud638 \ubc18\ubcf5 \ud69f\uc218", "venetian_blind_mode": "\ubca0\ub124\uc2dc\uc548 \ube14\ub77c\uc778\ub4dc \ubaa8\ub4dc" }, "title": "\uae30\uae30 \uc635\uc158 \uad6c\uc131\ud558\uae30" diff --git a/homeassistant/components/rfxtrx/translations/lb.json b/homeassistant/components/rfxtrx/translations/lb.json index f5441eeb298..82535926bd4 100644 --- a/homeassistant/components/rfxtrx/translations/lb.json +++ b/homeassistant/components/rfxtrx/translations/lb.json @@ -47,8 +47,7 @@ "automatic_add": "Aktiv\u00e9ier automatesch dob\u00e4isetzen", "debug": "Aktiv\u00e9ier Debuggen", "device": "Wiel een Apparat fir ze konfigur\u00e9ieren", - "event_code": "Evenement Code aginn fir dob\u00e4izesetzen", - "remove_device": "Wiel een Apparat fir ze l\u00e4schen" + "event_code": "Evenement Code aginn fir dob\u00e4izesetzen" }, "title": "Rfxtrx Optioune" }, @@ -57,11 +56,9 @@ "command_off": "Data bits W\u00e4ert fir Kommando aus", "command_on": "Data bits W\u00e4ert fir Kommando un", "data_bit": "Unzuel vun Data Bits", - "fire_event": "Aktiv\u00e9ier Apparat Evenement", "off_delay": "Aus Verz\u00f6gerung", "off_delay_enabled": "Aktiv\u00e9iert Aus Verz\u00f6gerung", - "replace_device": "Wiel een Apparat fir ze ersetzen", - "signal_repetitions": "Zuel vu Signalwidderhuelungen" + "replace_device": "Wiel een Apparat fir ze ersetzen" }, "title": "Apparat Optioune konfigur\u00e9ieren" } diff --git a/homeassistant/components/rfxtrx/translations/nl.json b/homeassistant/components/rfxtrx/translations/nl.json index e9b4c1a6c04..5a0ced3f52d 100644 --- a/homeassistant/components/rfxtrx/translations/nl.json +++ b/homeassistant/components/rfxtrx/translations/nl.json @@ -61,8 +61,7 @@ "debug": "Foutopsporing inschakelen", "device": "Selecteer het apparaat om te configureren", "event_code": "Voer de gebeurteniscode in om toe te voegen", - "protocols": "Protocollen", - "remove_device": "Apparaat selecteren dat u wilt verwijderen" + "protocols": "Protocollen" }, "title": "Rfxtrx-opties" }, @@ -71,11 +70,9 @@ "command_off": "Waarde gegevensbits voor commando uit", "command_on": "Waarde gegevensbits voor commando aan", "data_bit": "Aantal databits", - "fire_event": "Schakel apparaatgebeurtenis in", "off_delay": "Uitschakelvertraging", "off_delay_enabled": "Schakel uitschakelvertraging in", "replace_device": "Selecteer apparaat dat u wilt vervangen", - "signal_repetitions": "Aantal signaalherhalingen", "venetian_blind_mode": "Venetiaanse jaloezie modus" }, "title": "Configureer apparaatopties" diff --git a/homeassistant/components/rfxtrx/translations/no.json b/homeassistant/components/rfxtrx/translations/no.json index 62a658d4067..acdca83016e 100644 --- a/homeassistant/components/rfxtrx/translations/no.json +++ b/homeassistant/components/rfxtrx/translations/no.json @@ -61,8 +61,7 @@ "debug": "Aktiver feils\u00f8king", "device": "Velg enhet du vil konfigurere", "event_code": "Angi hendelseskode for \u00e5 legge til", - "protocols": "Protokoller", - "remove_device": "Velg enhet du vil slette" + "protocols": "Protokoller" }, "title": "[%key:component::rfxtrx::title%] alternativer" }, @@ -71,11 +70,9 @@ "command_off": "Databiter-verdi for kommando av", "command_on": "Databiter-verdi for kommando p\u00e5", "data_bit": "Antall databiter", - "fire_event": "Aktiver enhetshendelse", "off_delay": "Av forsinkelse", "off_delay_enabled": "Aktiver av forsinkelse", "replace_device": "Velg enheten du vil erstatte", - "signal_repetitions": "Antall signalrepetisjoner", "venetian_blind_mode": "Persiennemodus" }, "title": "Konfigurer enhetsalternativer" diff --git a/homeassistant/components/rfxtrx/translations/pl.json b/homeassistant/components/rfxtrx/translations/pl.json index b9f83ded473..c081baeafff 100644 --- a/homeassistant/components/rfxtrx/translations/pl.json +++ b/homeassistant/components/rfxtrx/translations/pl.json @@ -72,8 +72,7 @@ "debug": "W\u0142\u0105cz debugowanie", "device": "Wybierz urz\u0105dzenie do skonfigurowania", "event_code": "Podaj kod zdarzenia do dodania", - "protocols": "Protoko\u0142y", - "remove_device": "Wybierz urz\u0105dzenie do usuni\u0119cia" + "protocols": "Protoko\u0142y" }, "title": "Opcje Rfxtrx" }, @@ -82,11 +81,9 @@ "command_off": "Warto\u015b\u0107 bit\u00f3w danych dla komendy \"wy\u0142\u0105cz\" (off)", "command_on": "Warto\u015b\u0107 bit\u00f3w danych dla komendy \"w\u0142\u0105cz\" (on)", "data_bit": "Liczba bit\u00f3w danych", - "fire_event": "W\u0142\u0105cz zdarzenia na urz\u0105dzeniu", "off_delay": "Op\u00f3\u017anienie stanu \"off\"", "off_delay_enabled": "W\u0142\u0105cz op\u00f3\u017anienie stanu \"off\"", "replace_device": "Wybierz urz\u0105dzenie do zast\u0105pienia", - "signal_repetitions": "Liczba powt\u00f3rze\u0144 sygna\u0142u", "venetian_blind_mode": "Tryb \u017caluzji weneckich" }, "title": "Konfiguracja opcji urz\u0105dzenia" diff --git a/homeassistant/components/rfxtrx/translations/pt-BR.json b/homeassistant/components/rfxtrx/translations/pt-BR.json index 83252586d6a..a43b7e15a2b 100644 --- a/homeassistant/components/rfxtrx/translations/pt-BR.json +++ b/homeassistant/components/rfxtrx/translations/pt-BR.json @@ -61,8 +61,7 @@ "debug": "Habilitar a depura\u00e7\u00e3o", "device": "Selecione o dispositivo para configurar", "event_code": "Insira o c\u00f3digo do evento para adicionar", - "protocols": "Protocolos", - "remove_device": "Selecione o dispositivo para excluir" + "protocols": "Protocolos" }, "title": "Op\u00e7\u00f5es de Rfxtrx" }, @@ -71,11 +70,9 @@ "command_off": "Valor de bits de dados para comando desligado", "command_on": "Valor de bits de dados para comando ligado", "data_bit": "N\u00famero de bits de dados", - "fire_event": "Ativar evento do dispositivo", "off_delay": "Atraso de desligamento", "off_delay_enabled": "Ativar atraso de desligamento", "replace_device": "Selecione o dispositivo para substituir", - "signal_repetitions": "N\u00famero de repeti\u00e7\u00f5es de sinal", "venetian_blind_mode": "Modo de persianas" }, "title": "Configurar op\u00e7\u00f5es do dispositivo" diff --git a/homeassistant/components/rfxtrx/translations/ru.json b/homeassistant/components/rfxtrx/translations/ru.json index cb0fa979197..74dc2e218c9 100644 --- a/homeassistant/components/rfxtrx/translations/ru.json +++ b/homeassistant/components/rfxtrx/translations/ru.json @@ -61,8 +61,7 @@ "debug": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u043e\u0442\u043b\u0430\u0434\u043a\u0438", "device": "\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", "event_code": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u0431\u044b\u0442\u0438\u044f", - "protocols": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u044b", - "remove_device": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f" + "protocols": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u044b" }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" }, @@ -71,11 +70,9 @@ "command_off": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0431\u0438\u0442\u043e\u0432 \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", "command_on": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0431\u0438\u0442\u043e\u0432 \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u044b \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", "data_bit": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0431\u0438\u0442 \u0434\u0430\u043d\u043d\u044b\u0445", - "fire_event": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0441\u043e\u0431\u044b\u0442\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", "off_delay": "\u0417\u0430\u0434\u0435\u0440\u0436\u043a\u0430 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", "off_delay_enabled": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0437\u0430\u0434\u0435\u0440\u0436\u043a\u0443 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", "replace_device": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u0437\u0430\u043c\u0435\u043d\u044b", - "signal_repetitions": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u043e\u0432 \u0441\u0438\u0433\u043d\u0430\u043b\u0430", "venetian_blind_mode": "\u0420\u0435\u0436\u0438\u043c \u0432\u0435\u043d\u0435\u0446\u0438\u0430\u043d\u0441\u043a\u0438\u0445 \u0436\u0430\u043b\u044e\u0437\u0438" }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" diff --git a/homeassistant/components/rfxtrx/translations/sv.json b/homeassistant/components/rfxtrx/translations/sv.json index bdafd86c9a0..6cab9bd0b37 100644 --- a/homeassistant/components/rfxtrx/translations/sv.json +++ b/homeassistant/components/rfxtrx/translations/sv.json @@ -43,8 +43,7 @@ "automatic_add": "Aktivera automatisk till\u00e4gg av enheter", "debug": "Aktivera fels\u00f6kning", "device": "V\u00e4lj enhet att konfigurera", - "event_code": "Ange h\u00e4ndelsekod att l\u00e4gga till", - "remove_device": "V\u00e4lj enhet som ska tas bort" + "event_code": "Ange h\u00e4ndelsekod att l\u00e4gga till" }, "title": "Rfxtrx-alternativ" }, @@ -53,11 +52,9 @@ "command_off": "Databitv\u00e4rde f\u00f6r av-kommando", "command_on": "Databitv\u00e4rde f\u00f6r p\u00e5-kommando", "data_bit": "Antal databitar", - "fire_event": "Aktivera enhetsh\u00e4ndelse", "off_delay": "Avst\u00e4ngningsf\u00f6rdr\u00f6jning", "off_delay_enabled": "Aktivera avst\u00e4ngningsf\u00f6rdr\u00f6jning", - "replace_device": "V\u00e4lj enhet att ers\u00e4tta", - "signal_repetitions": "Antal signalrepetitioner" + "replace_device": "V\u00e4lj enhet att ers\u00e4tta" }, "title": "Konfigurera enhetsalternativ" } diff --git a/homeassistant/components/rfxtrx/translations/tr.json b/homeassistant/components/rfxtrx/translations/tr.json index fd29e034e31..999536c6b31 100644 --- a/homeassistant/components/rfxtrx/translations/tr.json +++ b/homeassistant/components/rfxtrx/translations/tr.json @@ -66,8 +66,7 @@ "debug": "Hata ay\u0131klamay\u0131 etkinle\u015ftir", "device": "Yap\u0131land\u0131rmak i\u00e7in cihaz\u0131 se\u00e7in", "event_code": "Eklemek i\u00e7in etkinlik kodunu girin", - "protocols": "Protokoller", - "remove_device": "Silinecek cihaz\u0131 se\u00e7in" + "protocols": "Protokoller" }, "title": "Rfxtrx Se\u00e7enekleri" }, @@ -76,11 +75,9 @@ "command_off": "Komut kapatma i\u00e7in veri bitleri de\u011feri", "command_on": "Komut i\u00e7in veri bitleri de\u011feri", "data_bit": "Veri biti say\u0131s\u0131", - "fire_event": "Cihaz etkinli\u011fini etkinle\u015ftir", "off_delay": "Kapanma gecikmesi", "off_delay_enabled": "Kapatma gecikmesini etkinle\u015ftir", "replace_device": "De\u011fi\u015ftirilecek cihaz\u0131 se\u00e7in", - "signal_repetitions": "Sinyal tekrar\u0131 say\u0131s\u0131", "venetian_blind_mode": "Jaluzi modu" }, "title": "Cihaz se\u00e7eneklerini yap\u0131land\u0131r\u0131n" diff --git a/homeassistant/components/rfxtrx/translations/uk.json b/homeassistant/components/rfxtrx/translations/uk.json index 65cca65679c..42ab18699ee 100644 --- a/homeassistant/components/rfxtrx/translations/uk.json +++ b/homeassistant/components/rfxtrx/translations/uk.json @@ -50,8 +50,7 @@ "automatic_add": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u0434\u043e\u0434\u0430\u0432\u0430\u043d\u043d\u044f", "debug": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0440\u0435\u0436\u0438\u043c \u043d\u0430\u043b\u0430\u0433\u043e\u0434\u0436\u0435\u043d\u043d\u044f", "device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f", - "event_code": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0456\u0457", - "remove_device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0434\u043b\u044f \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043d\u044f" + "event_code": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0456\u0457" }, "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438" }, @@ -60,11 +59,9 @@ "command_off": "\u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0431\u0456\u0442\u0456\u0432 \u0434\u0430\u043d\u0438\u0445 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u0438 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f", "command_on": "\u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0431\u0456\u0442\u0456\u0432 \u0434\u0430\u043d\u0438\u0445 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u0438 \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u044f", "data_bit": "\u041a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u0431\u0456\u0442\u0456\u0432 \u0434\u0430\u043d\u0438\u0445", - "fire_event": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u043f\u043e\u0434\u0456\u0457 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", "off_delay": "\u0417\u0430\u0442\u0440\u0438\u043c\u043a\u0430 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f", "off_delay_enabled": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0437\u0430\u0442\u0440\u0438\u043c\u043a\u0443 \u0432\u0438\u043c\u0438\u043a\u0430\u043d\u043d\u044f", "replace_device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0434\u043b\u044f \u0437\u0430\u043c\u0456\u043d\u0438", - "signal_repetitions": "\u041a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0432 \u0441\u0438\u0433\u043d\u0430\u043b\u0443", "venetian_blind_mode": "\u0420\u0435\u0436\u0438\u043c \u0436\u0430\u043b\u044e\u0437\u0456" }, "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" diff --git a/homeassistant/components/rfxtrx/translations/zh-Hant.json b/homeassistant/components/rfxtrx/translations/zh-Hant.json index 3f84f0a5f98..e477e6628ae 100644 --- a/homeassistant/components/rfxtrx/translations/zh-Hant.json +++ b/homeassistant/components/rfxtrx/translations/zh-Hant.json @@ -61,8 +61,7 @@ "debug": "\u958b\u555f\u9664\u932f", "device": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e", "event_code": "\u8f38\u5165\u4e8b\u4ef6\u4ee3\u78bc\u4ee5\u65b0\u589e", - "protocols": "\u901a\u8a0a\u5354\u5b9a", - "remove_device": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u522a\u9664" + "protocols": "\u901a\u8a0a\u5354\u5b9a" }, "title": "Rfxtrx \u9078\u9805" }, @@ -71,11 +70,9 @@ "command_off": "\u547d\u4ee4\u95dc\u9589\u7684\u8cc7\u6599\u4f4d\u5143\u503c", "command_on": "\u547d\u4ee4\u958b\u555f\u7684\u8cc7\u6599\u4f4d\u5143\u503c", "data_bit": "\u8cc7\u6599\u4f4d\u5143\u6578", - "fire_event": "\u958b\u555f\u88dd\u7f6e\u4e8b\u4ef6", "off_delay": "\u5ef6\u9072", "off_delay_enabled": "\u958b\u555f\u5ef6\u9072", "replace_device": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u53d6\u4ee3", - "signal_repetitions": "\u8a0a\u865f\u91cd\u8907\u6b21\u6578", "venetian_blind_mode": "\u767e\u8449\u7a97\u6a21\u5f0f" }, "title": "\u8a2d\u5b9a\u88dd\u7f6e\u9078\u9805" diff --git a/homeassistant/components/roku/translations/ca.json b/homeassistant/components/roku/translations/ca.json index be84d78fbff..df9d6fe48c7 100644 --- a/homeassistant/components/roku/translations/ca.json +++ b/homeassistant/components/roku/translations/ca.json @@ -11,12 +11,7 @@ "flow_title": "{name}", "step": { "discovery_confirm": { - "description": "Vols configurar {name}?", - "title": "Roku" - }, - "ssdp_confirm": { - "description": "Vols configurar {name}?", - "title": "Roku" + "description": "Vols configurar {name}?" }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/cs.json b/homeassistant/components/roku/translations/cs.json index 6914a519285..ee5ad4d600a 100644 --- a/homeassistant/components/roku/translations/cs.json +++ b/homeassistant/components/roku/translations/cs.json @@ -11,12 +11,7 @@ "flow_title": "Roku: {name}", "step": { "discovery_confirm": { - "description": "Chcete nastavit {name}?", - "title": "Roku" - }, - "ssdp_confirm": { - "description": "Chcete nastavit {name}?", - "title": "Roku" + "description": "Chcete nastavit {name}?" }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/de.json b/homeassistant/components/roku/translations/de.json index 5ff560809c8..25972162593 100644 --- a/homeassistant/components/roku/translations/de.json +++ b/homeassistant/components/roku/translations/de.json @@ -11,12 +11,7 @@ "flow_title": "{name}", "step": { "discovery_confirm": { - "description": "M\u00f6chtest du {name} einrichten?", - "title": "Roku" - }, - "ssdp_confirm": { - "description": "M\u00f6chtest du {name} einrichten?", - "title": "Roku" + "description": "M\u00f6chtest du {name} einrichten?" }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/el.json b/homeassistant/components/roku/translations/el.json index 91087c73a58..bf498993096 100644 --- a/homeassistant/components/roku/translations/el.json +++ b/homeassistant/components/roku/translations/el.json @@ -11,12 +11,7 @@ "flow_title": "{name}", "step": { "discovery_confirm": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};", - "title": "Roku" - }, - "ssdp_confirm": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};", - "title": "Roku" + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/en.json b/homeassistant/components/roku/translations/en.json index 192b9b23085..a4d931a8bcf 100644 --- a/homeassistant/components/roku/translations/en.json +++ b/homeassistant/components/roku/translations/en.json @@ -11,12 +11,7 @@ "flow_title": "{name}", "step": { "discovery_confirm": { - "description": "Do you want to set up {name}?", - "title": "Roku" - }, - "ssdp_confirm": { - "description": "Do you want to set up {name}?", - "title": "Roku" + "description": "Do you want to set up {name}?" }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/es-419.json b/homeassistant/components/roku/translations/es-419.json index 00b69c53c72..bff168d0084 100644 --- a/homeassistant/components/roku/translations/es-419.json +++ b/homeassistant/components/roku/translations/es-419.json @@ -9,10 +9,6 @@ }, "flow_title": "Roku: {name}", "step": { - "ssdp_confirm": { - "description": "\u00bfDesea configurar {name}?", - "title": "Roku" - }, "user": { "data": { "host": "Host o direcci\u00f3n IP" diff --git a/homeassistant/components/roku/translations/es.json b/homeassistant/components/roku/translations/es.json index 202c7f6c8ff..817f1d970cc 100644 --- a/homeassistant/components/roku/translations/es.json +++ b/homeassistant/components/roku/translations/es.json @@ -11,12 +11,7 @@ "flow_title": "{name}", "step": { "discovery_confirm": { - "description": "\u00bfQuieres configurar {name} ?", - "title": "Roku" - }, - "ssdp_confirm": { - "description": "\u00bfQuieres configurar {name}?", - "title": "Roku" + "description": "\u00bfQuieres configurar {name} ?" }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/et.json b/homeassistant/components/roku/translations/et.json index bb496ab8716..78ce0995ebd 100644 --- a/homeassistant/components/roku/translations/et.json +++ b/homeassistant/components/roku/translations/et.json @@ -11,12 +11,7 @@ "flow_title": "{name}", "step": { "discovery_confirm": { - "description": "Kas soovid seadistada {name}?", - "title": "" - }, - "ssdp_confirm": { - "description": "Kas soovid seadistada {name}?", - "title": "" + "description": "Kas soovid seadistada {name}?" }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/fr.json b/homeassistant/components/roku/translations/fr.json index 64a928de567..2d5ab38f828 100644 --- a/homeassistant/components/roku/translations/fr.json +++ b/homeassistant/components/roku/translations/fr.json @@ -15,16 +15,13 @@ "one": "Vide", "other": "Vide" }, - "description": "Voulez-vous configurer {name} ?", - "title": "Roku" + "description": "Voulez-vous configurer {name} ?" }, "ssdp_confirm": { "data": { "one": "Vide", "other": "Vide" - }, - "description": "Voulez-vous configurer {name} ?", - "title": "Roku" + } }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/hu.json b/homeassistant/components/roku/translations/hu.json index c28b0a712ea..e38c9ada483 100644 --- a/homeassistant/components/roku/translations/hu.json +++ b/homeassistant/components/roku/translations/hu.json @@ -15,16 +15,13 @@ "one": "Egy", "other": "Egy\u00e9b" }, - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?", - "title": "Roku felfedezve" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" }, "ssdp_confirm": { "data": { "one": "\u00dcres", "other": "\u00dcres" - }, - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?", - "title": "Roku" + } }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/id.json b/homeassistant/components/roku/translations/id.json index 3a227e80eaf..69d40e4aabb 100644 --- a/homeassistant/components/roku/translations/id.json +++ b/homeassistant/components/roku/translations/id.json @@ -11,12 +11,7 @@ "flow_title": "{name}", "step": { "discovery_confirm": { - "description": "Ingin menyiapkan {name}?", - "title": "Roku" - }, - "ssdp_confirm": { - "description": "Ingin menyiapkan {name}?", - "title": "Roku" + "description": "Ingin menyiapkan {name}?" }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/it.json b/homeassistant/components/roku/translations/it.json index 9f42c49c74f..4ebf0784669 100644 --- a/homeassistant/components/roku/translations/it.json +++ b/homeassistant/components/roku/translations/it.json @@ -15,16 +15,13 @@ "one": "Vuoto", "other": "Vuoti" }, - "description": "Vuoi configurare {name}?", - "title": "Roku" + "description": "Vuoi configurare {name}?" }, "ssdp_confirm": { "data": { "one": "Vuoto", "other": "Vuoti" - }, - "description": "Vuoi impostare {name}?", - "title": "Roku" + } }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/ja.json b/homeassistant/components/roku/translations/ja.json index 65f2ac2272a..59026c4d204 100644 --- a/homeassistant/components/roku/translations/ja.json +++ b/homeassistant/components/roku/translations/ja.json @@ -11,12 +11,7 @@ "flow_title": "{name}", "step": { "discovery_confirm": { - "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f", - "title": "Roku" - }, - "ssdp_confirm": { - "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f", - "title": "Roku" + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/ko.json b/homeassistant/components/roku/translations/ko.json index cb127234601..93fb292c458 100644 --- a/homeassistant/components/roku/translations/ko.json +++ b/homeassistant/components/roku/translations/ko.json @@ -11,12 +11,7 @@ "flow_title": "Roku: {name}", "step": { "discovery_confirm": { - "description": "{name}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Roku" - }, - "ssdp_confirm": { - "description": "{name}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Roku" + "description": "{name}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/lb.json b/homeassistant/components/roku/translations/lb.json index 04ad814c6b4..0a293006b9b 100644 --- a/homeassistant/components/roku/translations/lb.json +++ b/homeassistant/components/roku/translations/lb.json @@ -10,12 +10,7 @@ "flow_title": "Roku: {name}", "step": { "discovery_confirm": { - "description": "Soll {name} konfigur\u00e9iert ginn?", - "title": "Roku" - }, - "ssdp_confirm": { - "description": "Soll {name} konfigur\u00e9iert ginn?", - "title": "Roku" + "description": "Soll {name} konfigur\u00e9iert ginn?" }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/nl.json b/homeassistant/components/roku/translations/nl.json index 6bf3435a19b..7f413121fa7 100644 --- a/homeassistant/components/roku/translations/nl.json +++ b/homeassistant/components/roku/translations/nl.json @@ -15,16 +15,13 @@ "one": "Een", "other": "Ander" }, - "description": "Wilt u {name} instellen?", - "title": "Roku" + "description": "Wilt u {name} instellen?" }, "ssdp_confirm": { "data": { "one": "Leeg", "other": "Leeg" - }, - "description": "Wilt u {name} instellen?", - "title": "Roku" + } }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/no.json b/homeassistant/components/roku/translations/no.json index 1bbd5bbea86..8449aaabfdd 100644 --- a/homeassistant/components/roku/translations/no.json +++ b/homeassistant/components/roku/translations/no.json @@ -11,12 +11,7 @@ "flow_title": "{name}", "step": { "discovery_confirm": { - "description": "Vil du konfigurere {name}?", - "title": "" - }, - "ssdp_confirm": { - "description": "Vil du sette opp {name} ?", - "title": "" + "description": "Vil du konfigurere {name}?" }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/pl.json b/homeassistant/components/roku/translations/pl.json index 41ea348543e..59fb1aaa673 100644 --- a/homeassistant/components/roku/translations/pl.json +++ b/homeassistant/components/roku/translations/pl.json @@ -17,8 +17,7 @@ "one": "jeden", "other": "inne" }, - "description": "Czy chcesz skonfigurowa\u0107 {name}?", - "title": "Roku" + "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, "ssdp_confirm": { "data": { @@ -26,9 +25,7 @@ "many": "wiele", "one": "jeden", "other": "inne" - }, - "description": "Czy chcesz skonfigurowa\u0107 {name}?", - "title": "Roku" + } }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/pt-BR.json b/homeassistant/components/roku/translations/pt-BR.json index 408bbc915c0..1970fbd0886 100644 --- a/homeassistant/components/roku/translations/pt-BR.json +++ b/homeassistant/components/roku/translations/pt-BR.json @@ -11,12 +11,7 @@ "flow_title": "{name}", "step": { "discovery_confirm": { - "description": "Deseja configurar {name}?", - "title": "Roku" - }, - "ssdp_confirm": { - "description": "Voc\u00ea quer configurar o {name}?", - "title": "Roku" + "description": "Deseja configurar {name}?" }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/pt.json b/homeassistant/components/roku/translations/pt.json index e67de509456..1d4aea3279b 100644 --- a/homeassistant/components/roku/translations/pt.json +++ b/homeassistant/components/roku/translations/pt.json @@ -9,9 +9,6 @@ }, "flow_title": "Roku: {name}", "step": { - "ssdp_confirm": { - "title": "Roku" - }, "user": { "data": { "host": "Servidor" diff --git a/homeassistant/components/roku/translations/ru.json b/homeassistant/components/roku/translations/ru.json index 4ba55bc8e1a..76ac3a7f188 100644 --- a/homeassistant/components/roku/translations/ru.json +++ b/homeassistant/components/roku/translations/ru.json @@ -11,12 +11,7 @@ "flow_title": "{name}", "step": { "discovery_confirm": { - "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?", - "title": "Roku" - }, - "ssdp_confirm": { - "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?", - "title": "Roku" + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/sl.json b/homeassistant/components/roku/translations/sl.json index 4a198b3b5c9..bb39edf9753 100644 --- a/homeassistant/components/roku/translations/sl.json +++ b/homeassistant/components/roku/translations/sl.json @@ -15,9 +15,7 @@ "one": "ena", "other": "drugo", "two": "dva" - }, - "description": "Ali \u017eelite nastaviti {name}?", - "title": "Roku" + } }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/sv.json b/homeassistant/components/roku/translations/sv.json index 524e7753548..4272f65b2ae 100644 --- a/homeassistant/components/roku/translations/sv.json +++ b/homeassistant/components/roku/translations/sv.json @@ -5,10 +5,6 @@ }, "flow_title": "Roku: {name}", "step": { - "ssdp_confirm": { - "description": "Vill du konfigurera {name}?", - "title": "Roku" - }, "user": { "data": { "host": "V\u00e4rd" diff --git a/homeassistant/components/roku/translations/tr.json b/homeassistant/components/roku/translations/tr.json index 3e930a6c1b2..23cc6e56bd5 100644 --- a/homeassistant/components/roku/translations/tr.json +++ b/homeassistant/components/roku/translations/tr.json @@ -15,16 +15,13 @@ "one": "Bo\u015f", "other": "Bo\u015f" }, - "description": "{name} kurmak istiyor musunuz?", - "title": "Roku" + "description": "{name} kurmak istiyor musunuz?" }, "ssdp_confirm": { "data": { "one": "Bo\u015f", "other": "Bo\u015f" - }, - "description": "{name} kurmak istiyor musunuz?", - "title": "Roku" + } }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/uk.json b/homeassistant/components/roku/translations/uk.json index b7db8875f8e..8fd1ae7f630 100644 --- a/homeassistant/components/roku/translations/uk.json +++ b/homeassistant/components/roku/translations/uk.json @@ -10,12 +10,7 @@ "flow_title": "Roku: {name}", "step": { "discovery_confirm": { - "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name}?", - "title": "Roku" - }, - "ssdp_confirm": { - "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name}?", - "title": "Roku" + "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name}?" }, "user": { "data": { diff --git a/homeassistant/components/roku/translations/zh-Hant.json b/homeassistant/components/roku/translations/zh-Hant.json index 5cfe9232301..13c5886b46d 100644 --- a/homeassistant/components/roku/translations/zh-Hant.json +++ b/homeassistant/components/roku/translations/zh-Hant.json @@ -11,12 +11,7 @@ "flow_title": "{name}", "step": { "discovery_confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f", - "title": "Roku" - }, - "ssdp_confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f", - "title": "Roku" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, "user": { "data": { diff --git a/homeassistant/components/roomba/translations/bg.json b/homeassistant/components/roomba/translations/bg.json index 74075c671a1..3da613d9394 100644 --- a/homeassistant/components/roomba/translations/bg.json +++ b/homeassistant/components/roomba/translations/bg.json @@ -4,9 +4,6 @@ "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { - "init": { - "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" - }, "link_manual": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" @@ -18,8 +15,7 @@ }, "user": { "data": { - "host": "\u0425\u043e\u0441\u0442", - "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + "host": "\u0425\u043e\u0441\u0442" }, "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/roomba/translations/ca.json b/homeassistant/components/roomba/translations/ca.json index ba23e30e3f0..de0f673e971 100644 --- a/homeassistant/components/roomba/translations/ca.json +++ b/homeassistant/components/roomba/translations/ca.json @@ -11,13 +11,6 @@ }, "flow_title": "{name} ({host})", "step": { - "init": { - "data": { - "host": "Amfitri\u00f3" - }, - "description": "Selecciona un/a Roomba o Braava.", - "title": "Connexi\u00f3 autom\u00e0tica amb el dispositiu" - }, "link": { "description": "Mant\u00e9 premut el bot\u00f3 d'inici a {name} fins que el dispositiu emeti un so (aproximadament dos segons) despr\u00e9s, envia en els seg\u00fcents 30 segons.", "title": "Recupera la contrasenya" @@ -31,7 +24,6 @@ }, "manual": { "data": { - "blid": "BLID", "host": "Amfitri\u00f3" }, "description": "No s'ha descobert cap Roomba o Braava a la teva xarxa.", @@ -39,11 +31,7 @@ }, "user": { "data": { - "blid": "BLID", - "continuous": "Cont\u00ednua", - "delay": "Retard", - "host": "Amfitri\u00f3", - "password": "Contrasenya" + "host": "Amfitri\u00f3" }, "description": "Selecciona un/a Roomba o Braava.", "title": "Connexi\u00f3 autom\u00e0tica amb el dispositiu" diff --git a/homeassistant/components/roomba/translations/cs.json b/homeassistant/components/roomba/translations/cs.json index d94d39f8136..0d42bf1b5e5 100644 --- a/homeassistant/components/roomba/translations/cs.json +++ b/homeassistant/components/roomba/translations/cs.json @@ -9,11 +9,6 @@ }, "flow_title": "iRobot {name} ({host})", "step": { - "init": { - "data": { - "host": "Hostitel" - } - }, "link_manual": { "data": { "password": "Heslo" @@ -26,9 +21,7 @@ }, "user": { "data": { - "delay": "Zpo\u017ed\u011bn\u00ed", - "host": "Hostitel", - "password": "Heslo" + "host": "Hostitel" }, "title": "P\u0159ipojen\u00ed k za\u0159\u00edzen\u00ed" } diff --git a/homeassistant/components/roomba/translations/da.json b/homeassistant/components/roomba/translations/da.json index 2a8057ffc95..97ea58b2c51 100644 --- a/homeassistant/components/roomba/translations/da.json +++ b/homeassistant/components/roomba/translations/da.json @@ -2,9 +2,6 @@ "config": { "step": { "user": { - "data": { - "delay": "Forsinkelse" - }, "title": "Tilslut automatisk til enheden" } } diff --git a/homeassistant/components/roomba/translations/de.json b/homeassistant/components/roomba/translations/de.json index ae4ef7d9dc7..1717c07a735 100644 --- a/homeassistant/components/roomba/translations/de.json +++ b/homeassistant/components/roomba/translations/de.json @@ -11,13 +11,6 @@ }, "flow_title": "{name} ({host})", "step": { - "init": { - "data": { - "host": "Host" - }, - "description": "W\u00e4hle einen Roomba oder Braava aus.", - "title": "Automatisch mit dem Ger\u00e4t verbinden" - }, "link": { "description": "Halte die Home-Taste von {name} gedr\u00fcckt, bis das Ger\u00e4t einen Ton erzeugt (ca. zwei Sekunden) und sende die Best\u00e4tigung innerhalb von 30 Sekunden ab.", "title": "Passwort abrufen" @@ -31,7 +24,6 @@ }, "manual": { "data": { - "blid": "BLID", "host": "Host" }, "description": "Es wurde kein Roomba oder Braava in deinem Netzwerk entdeckt.", @@ -39,11 +31,7 @@ }, "user": { "data": { - "blid": "BLID", - "continuous": "Kontinuierlich", - "delay": "Verz\u00f6gerung", - "host": "Host", - "password": "Passwort" + "host": "Host" }, "description": "W\u00e4hle einen Roomba oder Braava aus.", "title": "Automatisch mit dem Ger\u00e4t verbinden" diff --git a/homeassistant/components/roomba/translations/el.json b/homeassistant/components/roomba/translations/el.json index e59ce426deb..cb8683f2be2 100644 --- a/homeassistant/components/roomba/translations/el.json +++ b/homeassistant/components/roomba/translations/el.json @@ -11,13 +11,6 @@ }, "flow_title": "{name} ({host})", "step": { - "init": { - "data": { - "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03af\u03b1 Roomba \u03ae Braava.", - "title": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" - }, "link": { "description": "\u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b1\u03b9 \u03ba\u03c1\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c0\u03b1\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf \u03c4\u03bf \u03c0\u03bb\u03ae\u03ba\u03c4\u03c1\u03bf Home \u03c3\u03c4\u03bf {name} \u03bc\u03ad\u03c7\u03c1\u03b9 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ac\u03b3\u03b5\u03b9 \u03ad\u03bd\u03b1\u03bd \u03ae\u03c7\u03bf (\u03c0\u03b5\u03c1\u03af\u03c0\u03bf\u03c5 \u03b4\u03cd\u03bf \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1) \u03ba\u03b1\u03b9, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03c5\u03c0\u03bf\u03b2\u03ac\u03bb\u03b5\u03c4\u03b5 \u03b5\u03bd\u03c4\u03cc\u03c2 30 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03bf\u03bb\u03ad\u03c0\u03c4\u03c9\u03bd.", "title": "\u0391\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd" @@ -31,7 +24,6 @@ }, "manual": { "data": { - "blid": "BLID", "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" }, "description": "\u0394\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03c5\u03c6\u03b8\u03b5\u03af Roomba \u03ae Braava \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2.", @@ -39,11 +31,7 @@ }, "user": { "data": { - "blid": "BLID", - "continuous": "\u03a3\u03c5\u03bd\u03b5\u03c7\u03ae\u03c2", - "delay": "\u039a\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7", - "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03af\u03b1 Roomba \u03ae Braava.", "title": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" diff --git a/homeassistant/components/roomba/translations/en.json b/homeassistant/components/roomba/translations/en.json index facd127985a..703ccebbb11 100644 --- a/homeassistant/components/roomba/translations/en.json +++ b/homeassistant/components/roomba/translations/en.json @@ -11,13 +11,6 @@ }, "flow_title": "{name} ({host})", "step": { - "init": { - "data": { - "host": "Host" - }, - "description": "Select a Roomba or Braava.", - "title": "Automatically connect to the device" - }, "link": { "description": "Press and hold the Home button on {name} until the device generates a sound (about two seconds), then submit within 30 seconds.", "title": "Retrieve Password" @@ -31,7 +24,6 @@ }, "manual": { "data": { - "blid": "BLID", "host": "Host" }, "description": "No Roomba or Braava have been discovered on your network.", @@ -39,11 +31,7 @@ }, "user": { "data": { - "blid": "BLID", - "continuous": "Continuous", - "delay": "Delay", - "host": "Host", - "password": "Password" + "host": "Host" }, "description": "Select a Roomba or Braava.", "title": "Automatically connect to the device" diff --git a/homeassistant/components/roomba/translations/es-419.json b/homeassistant/components/roomba/translations/es-419.json index 6d0295f7ce0..4fe61c8950c 100644 --- a/homeassistant/components/roomba/translations/es-419.json +++ b/homeassistant/components/roomba/translations/es-419.json @@ -6,11 +6,7 @@ "step": { "user": { "data": { - "blid": "BLID", - "continuous": "Continuo", - "delay": "Retraso", - "host": "Nombre de host o direcci\u00f3n IP", - "password": "Contrase\u00f1a" + "host": "Nombre de host o direcci\u00f3n IP" }, "description": "Actualmente recuperar el BLID y la contrase\u00f1a es un proceso manual. Siga los pasos descritos en la documentaci\u00f3n en: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", "title": "Conectarse al dispositivo" diff --git a/homeassistant/components/roomba/translations/es.json b/homeassistant/components/roomba/translations/es.json index c8760f75fdd..f2664baee31 100644 --- a/homeassistant/components/roomba/translations/es.json +++ b/homeassistant/components/roomba/translations/es.json @@ -11,13 +11,6 @@ }, "flow_title": "iRobot {name} ({host})", "step": { - "init": { - "data": { - "host": "Host" - }, - "description": "Selecciona una Roomba o Braava.", - "title": "Conexi\u00f3n autom\u00e1tica con el dispositivo" - }, "link": { "description": "Mant\u00e9n pulsado el bot\u00f3n Inicio en {name} hasta que el dispositivo genere un sonido (aproximadamente dos segundos).", "title": "Recuperar la contrase\u00f1a" @@ -31,7 +24,6 @@ }, "manual": { "data": { - "blid": "BLID", "host": "Host" }, "description": "No se ha descubierto ning\u00fan dispositivo Roomba ni Braava en tu red.", @@ -39,11 +31,7 @@ }, "user": { "data": { - "blid": "BLID", - "continuous": "Continuo", - "delay": "Retardo", - "host": "Host", - "password": "Contrase\u00f1a" + "host": "Host" }, "description": "Actualmente recuperar el BLID y la contrase\u00f1a es un proceso manual. Sigue los pasos descritos en la documentaci\u00f3n en: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", "title": "Conexi\u00f3n autom\u00e1tica con el dispositivo" diff --git a/homeassistant/components/roomba/translations/et.json b/homeassistant/components/roomba/translations/et.json index 43715399ef1..2cc599ca8ee 100644 --- a/homeassistant/components/roomba/translations/et.json +++ b/homeassistant/components/roomba/translations/et.json @@ -11,13 +11,6 @@ }, "flow_title": "{name} ( {host} )", "step": { - "init": { - "data": { - "host": "Host" - }, - "description": "Vali Roomba v\u00f5i Braava seade.", - "title": "\u00dchenda seadmega automaatselt" - }, "link": { "description": "Vajuta ja hoia all seadme {name} nuppu Home kuni seade teeb piiksu (umbes kaks sekundit), edasta 30 sekundi jooksul.", "title": "Hangi salas\u00f5na" @@ -31,7 +24,6 @@ }, "manual": { "data": { - "blid": "", "host": "Host" }, "description": "V\u00f5rgus ei tuvastatud \u00fchtegi Roomba ega Braava seadet.", @@ -39,11 +31,7 @@ }, "user": { "data": { - "blid": "", - "continuous": "Pidev", - "delay": "Viivitus", - "host": "", - "password": "Salas\u00f5na" + "host": "" }, "description": "Vali Roomba v\u00f5i Braava seade", "title": "\u00dchenda seadmega automaatselt" diff --git a/homeassistant/components/roomba/translations/fr.json b/homeassistant/components/roomba/translations/fr.json index 5cbb1090cc0..0f00f6fae61 100644 --- a/homeassistant/components/roomba/translations/fr.json +++ b/homeassistant/components/roomba/translations/fr.json @@ -11,13 +11,6 @@ }, "flow_title": "{name} ({host})", "step": { - "init": { - "data": { - "host": "H\u00f4te" - }, - "description": "S\u00e9lectionnez un Roomba ou un Braava.", - "title": "Se connecter automatiquement \u00e0 l'appareil" - }, "link": { "description": "Appuyez sur le bouton Accueil et maintenez-le enfonc\u00e9 jusqu'\u00e0 ce que l'appareil \u00e9mette un son (environ deux secondes).", "title": "R\u00e9cup\u00e9rer le mot de passe" @@ -31,7 +24,6 @@ }, "manual": { "data": { - "blid": "BLID", "host": "H\u00f4te" }, "description": "Aucun Roomba ou Braava n'a \u00e9t\u00e9 d\u00e9couvert sur votre r\u00e9seau.", @@ -39,11 +31,7 @@ }, "user": { "data": { - "blid": "BLID", - "continuous": "En continu", - "delay": "D\u00e9lai", - "host": "H\u00f4te", - "password": "Mot de passe" + "host": "H\u00f4te" }, "description": "La r\u00e9cup\u00e9ration du BLID et du mot de passe est actuellement un processus manuel. Veuillez suivre les \u00e9tapes d\u00e9crites dans la documentation \u00e0 l'adresse: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", "title": "Se connecter automatiquement \u00e0 l'appareil" diff --git a/homeassistant/components/roomba/translations/he.json b/homeassistant/components/roomba/translations/he.json index 4520671eedb..d61ade0479a 100644 --- a/homeassistant/components/roomba/translations/he.json +++ b/homeassistant/components/roomba/translations/he.json @@ -9,11 +9,6 @@ }, "flow_title": "{name} ({host})", "step": { - "init": { - "data": { - "host": "\u05de\u05d0\u05e8\u05d7" - } - }, "link": { "title": "\u05d0\u05d7\u05d6\u05e8 \u05e1\u05d9\u05e1\u05de\u05d4" }, @@ -31,8 +26,7 @@ }, "user": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7", - "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + "host": "\u05de\u05d0\u05e8\u05d7" }, "description": "\u05d1\u05d7\u05d9\u05e8\u05ea Roomba \u05d0\u05d5 Braava." } diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json index dd1c74cc0b6..e109b6e7043 100644 --- a/homeassistant/components/roomba/translations/hu.json +++ b/homeassistant/components/roomba/translations/hu.json @@ -11,13 +11,6 @@ }, "flow_title": "{name} ({host})", "step": { - "init": { - "data": { - "host": "C\u00edm" - }, - "description": "V\u00e1lasszon egy Roomba vagy Braava k\u00e9sz\u00fcl\u00e9ket.", - "title": "Automatikus csatlakoz\u00e1s az eszk\u00f6zh\u00f6z" - }, "link": { "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" @@ -31,7 +24,6 @@ }, "manual": { "data": { - "blid": "BLID", "host": "C\u00edm" }, "description": "A h\u00e1l\u00f3zaton egyetlen Roomba vagy Braava sem ker\u00fclt el\u0151.", @@ -39,11 +31,7 @@ }, "user": { "data": { - "blid": "BLID", - "continuous": "Folyamatos", - "delay": "K\u00e9sleltet\u00e9s", - "host": "C\u00edm", - "password": "Jelsz\u00f3" + "host": "C\u00edm" }, "description": "V\u00e1lasszon Roomba-t vagy Braava-t.", "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 1ade232fd70..9b8e9ef8bbe 100644 --- a/homeassistant/components/roomba/translations/id.json +++ b/homeassistant/components/roomba/translations/id.json @@ -11,13 +11,6 @@ }, "flow_title": "{name} ({host})", "step": { - "init": { - "data": { - "host": "Host" - }, - "description": "Pilih Roomba atau Braava.", - "title": "Sambungkan secara otomatis ke perangkat" - }, "link": { "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" @@ -31,7 +24,6 @@ }, "manual": { "data": { - "blid": "BLID", "host": "Host" }, "description": "Tidak ada Roomba atau Braava yang ditemukan di jaringan Anda.", @@ -39,11 +31,7 @@ }, "user": { "data": { - "blid": "BLID", - "continuous": "Terus menerus", - "delay": "Tunda", - "host": "Host", - "password": "Kata Sandi" + "host": "Host" }, "description": "Pilih Roomba atau Braava.", "title": "Sambungkan secara otomatis ke perangkat" diff --git a/homeassistant/components/roomba/translations/it.json b/homeassistant/components/roomba/translations/it.json index 4be1be53540..b87a7cb8ef6 100644 --- a/homeassistant/components/roomba/translations/it.json +++ b/homeassistant/components/roomba/translations/it.json @@ -11,13 +11,6 @@ }, "flow_title": "{name} ({host})", "step": { - "init": { - "data": { - "host": "Host" - }, - "description": "Seleziona un Roomba o un Braava.", - "title": "Connettiti automaticamente al dispositivo" - }, "link": { "description": "Tieni premuto il pulsante Home su {name} fino a quando il dispositivo non genera un suono (circa due secondi), quindi invialo entro 30 secondi.", "title": "Recupera password" @@ -31,7 +24,6 @@ }, "manual": { "data": { - "blid": "BLID", "host": "Host" }, "description": "Nessun Roomba o Braava \u00e8 stato rilevato sulla rete.", @@ -39,11 +31,7 @@ }, "user": { "data": { - "blid": "BLID", - "continuous": "Continuo", - "delay": "Ritardo", - "host": "Host", - "password": "Password" + "host": "Host" }, "description": "Seleziona un Roomba o un Braava.", "title": "Connetti automaticamente al dispositivo" diff --git a/homeassistant/components/roomba/translations/ja.json b/homeassistant/components/roomba/translations/ja.json index d7018e544ce..4cbdfd2a8bb 100644 --- a/homeassistant/components/roomba/translations/ja.json +++ b/homeassistant/components/roomba/translations/ja.json @@ -11,13 +11,6 @@ }, "flow_title": "{name} ({host})", "step": { - "init": { - "data": { - "host": "\u30db\u30b9\u30c8" - }, - "description": "\u30eb\u30f3\u30d0\u307e\u305f\u306f\u30d6\u30e9\u30fc\u30d0\u3092\u9078\u629e\u3057\u307e\u3059\u3002", - "title": "\u81ea\u52d5\u7684\u306b\u30c7\u30d0\u30a4\u30b9\u306b\u63a5\u7d9a\u3059\u308b" - }, "link": { "description": "{name} \u306e\u30db\u30fc\u30e0\u30dc\u30bf\u30f3\u3092\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u97f3\u3092\u51fa\u3059\u307e\u3067(\u7d042\u79d2)\u62bc\u3057\u7d9a\u3051\u300130\u79d2\u4ee5\u5185\u306b\u9001\u4fe1(submit)\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u53d6\u5f97" @@ -31,7 +24,6 @@ }, "manual": { "data": { - "blid": "BLID", "host": "\u30db\u30b9\u30c8" }, "description": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u3001\u30eb\u30f3\u30d0\u3084\u30d6\u30e9\u30fc\u30d0\u304c\u767a\u898b\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f\u3002", @@ -39,11 +31,7 @@ }, "user": { "data": { - "blid": "BLID", - "continuous": "\u9023\u7d9a", - "delay": "\u9045\u5ef6", - "host": "\u30db\u30b9\u30c8", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + "host": "\u30db\u30b9\u30c8" }, "description": "\u30eb\u30f3\u30d0\u307e\u305f\u306f\u30d6\u30e9\u30fc\u30d0\u3092\u9078\u629e\u3057\u307e\u3059\u3002", "title": "\u81ea\u52d5\u7684\u306b\u30c7\u30d0\u30a4\u30b9\u306b\u63a5\u7d9a\u3059\u308b" diff --git a/homeassistant/components/roomba/translations/ko.json b/homeassistant/components/roomba/translations/ko.json index bb33287c9b8..70ebc3b6ef4 100644 --- a/homeassistant/components/roomba/translations/ko.json +++ b/homeassistant/components/roomba/translations/ko.json @@ -11,13 +11,6 @@ }, "flow_title": "\uc544\uc774\ub85c\ubd07: {name} ({host})", "step": { - "init": { - "data": { - "host": "\ud638\uc2a4\ud2b8" - }, - "description": "\ub8f8\ubc14 \ub610\ub294 \ube0c\ub77c\ubc14\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", - "title": "\uae30\uae30\uc5d0 \uc790\ub3d9\uc73c\ub85c \uc5f0\uacb0\ud558\uae30" - }, "link": { "description": "\uae30\uae30\uc5d0\uc11c \uc18c\ub9ac\uac00 \ub0a0 \ub54c\uae4c\uc9c0 {name}\uc758 \ud648 \ubc84\ud2bc\uc744 \uae38\uac8c \ub204\ub978 \ub2e4\uc74c(\uc57d 2\ucd08) 30\ucd08 \uc774\ub0b4\uc5d0 \ud655\uc778 \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.", "title": "\ube44\ubc00\ubc88\ud638 \uac00\uc838\uc624\uae30" @@ -31,7 +24,6 @@ }, "manual": { "data": { - "blid": "BLID", "host": "\ud638\uc2a4\ud2b8" }, "description": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ub8f8\ubc14 \ub610\ub294 \ube0c\ub77c\ubc14\uac00 \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. BLID\ub294 `iRobot-` \ub610\ub294 `Roomba-` \ub4a4\uc5d0 \uc788\ub294 \uae30\uae30 \ud638\uc2a4\ud2b8 \uc774\ub984\uc758 \uc77c\ubd80\uc785\ub2c8\ub2e4. \uad00\ub828 \ubb38\uc11c\uc5d0 \uc124\uba85\ub41c \ub2e8\uacc4\ub97c \ub530\ub77c\uc8fc\uc138\uc694: {auth_help_url}", @@ -39,11 +31,7 @@ }, "user": { "data": { - "blid": "BLID", - "continuous": "\uc5f0\uc18d", - "delay": "\uc9c0\uc5f0", - "host": "\ud638\uc2a4\ud2b8", - "password": "\ube44\ubc00\ubc88\ud638" + "host": "\ud638\uc2a4\ud2b8" }, "description": "\ud604\uc7ac BLID \ubc0f \ube44\ubc00\ubc88\ud638\ub294 \uc218\ub3d9\uc73c\ub85c \uac00\uc838\uc640\uc57c\ud569\ub2c8\ub2e4. \ub2e4\uc74c \uad00\ub828 \ubb38\uc11c\uc5d0 \ub098\uc640 \uc788\ub294 \ub2e8\uacc4\ub97c \ub530\ub77c\uc8fc\uc138\uc694: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", "title": "\uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uae30" diff --git a/homeassistant/components/roomba/translations/lb.json b/homeassistant/components/roomba/translations/lb.json index 5578a7710db..b2c0b85d16a 100644 --- a/homeassistant/components/roomba/translations/lb.json +++ b/homeassistant/components/roomba/translations/lb.json @@ -8,13 +8,6 @@ }, "flow_title": "iRobot {name} ({host})", "step": { - "init": { - "data": { - "host": "Host" - }, - "description": "Ee Roomba oder Bravaa auswielen.", - "title": "Automatesch mam Apparat verbannen" - }, "link": { "title": "Passwuert ausliesen" }, @@ -26,18 +19,13 @@ }, "manual": { "data": { - "blid": "BLID", "host": "Host" }, "title": "Manuell mam Apparat verbannen" }, "user": { "data": { - "blid": "BLID", - "continuous": "Kontinu\u00e9ierlech", - "delay": "Delai", - "host": "Host", - "password": "Passwuert" + "host": "Host" }, "description": "De Prozess fir BLID an Passwuert opzeruffen ass fir de Moment manuell. Folleg w.e.g. de Schr\u00ebtt d\u00e9i an der Dokumentatioun op https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials beschriwwe sinn.", "title": "Mam Apparat verbannen" diff --git a/homeassistant/components/roomba/translations/nl.json b/homeassistant/components/roomba/translations/nl.json index bbb4fdbe765..68e18d7db93 100644 --- a/homeassistant/components/roomba/translations/nl.json +++ b/homeassistant/components/roomba/translations/nl.json @@ -11,13 +11,6 @@ }, "flow_title": "{name} ({host})", "step": { - "init": { - "data": { - "host": "Host" - }, - "description": "Kies een Roomba of Braava.", - "title": "Automatisch verbinding maken met het apparaat" - }, "link": { "description": "Houd de Home-knop op {name} ingedrukt totdat het apparaat een geluid genereert (ongeveer twee seconden), bevestig vervolgens binnen 30 seconden.", "title": "Wachtwoord opvragen" @@ -31,7 +24,6 @@ }, "manual": { "data": { - "blid": "BLID", "host": "Host" }, "description": "Er is geen Roomba of Braava ontdekt op uw netwerk.", @@ -39,11 +31,7 @@ }, "user": { "data": { - "blid": "BLID", - "continuous": "Doorlopend", - "delay": "Vertraging", - "host": "Host", - "password": "Wachtwoord" + "host": "Host" }, "description": "Kies een Roomba of Braava.", "title": "Automatisch verbinding maken met het apparaat" diff --git a/homeassistant/components/roomba/translations/no.json b/homeassistant/components/roomba/translations/no.json index 3a5d95eb006..f378bfb127c 100644 --- a/homeassistant/components/roomba/translations/no.json +++ b/homeassistant/components/roomba/translations/no.json @@ -11,13 +11,6 @@ }, "flow_title": "{name} ({host})", "step": { - "init": { - "data": { - "host": "Vert" - }, - "description": "Velg en Roomba eller Braava", - "title": "Koble automatisk til enheten" - }, "link": { "description": "Trykk og hold nede Hjem-knappen p\u00e5 {name} til enheten genererer en lyd (omtrent to sekunder), og send deretter innen 30 sekunder.", "title": "Hent passord" @@ -31,7 +24,6 @@ }, "manual": { "data": { - "blid": "", "host": "Vert" }, "description": "Ingen Roomba eller Braava er oppdaget p\u00e5 nettverket ditt.", @@ -39,11 +31,7 @@ }, "user": { "data": { - "blid": "Blid", - "continuous": "Kontinuerlige", - "delay": "Forsinkelse", - "host": "Vert", - "password": "Passord" + "host": "Vert" }, "description": "Velg en Roomba eller Braava.", "title": "Koble automatisk til enheten" diff --git a/homeassistant/components/roomba/translations/pl.json b/homeassistant/components/roomba/translations/pl.json index d2d76bee5ff..a3440c97ed2 100644 --- a/homeassistant/components/roomba/translations/pl.json +++ b/homeassistant/components/roomba/translations/pl.json @@ -11,13 +11,6 @@ }, "flow_title": "{name} ({host})", "step": { - "init": { - "data": { - "host": "Nazwa hosta lub adres IP" - }, - "description": "Wybierz Roomb\u0119 lub Braava", - "title": "Automatyczne po\u0142\u0105czenie z urz\u0105dzeniem" - }, "link": { "description": "Naci\u015bnij i przytrzymaj przycisk Home na {name} a\u017c urz\u0105dzenie wygeneruje d\u017awi\u0119k (oko\u0142o dwie sekundy), a nast\u0119pnie zatwierd\u017a w ci\u0105gu 30 sekund.", "title": "Odzyskiwanie has\u0142a" @@ -31,7 +24,6 @@ }, "manual": { "data": { - "blid": "BLID", "host": "Nazwa hosta lub adres IP" }, "description": "W Twojej sieci nie wykryto urz\u0105dzenia Roomba ani Braava.", @@ -39,11 +31,7 @@ }, "user": { "data": { - "blid": "BLID", - "continuous": "Ci\u0105g\u0142y", - "delay": "Op\u00f3\u017anienie", - "host": "Nazwa hosta lub adres IP", - "password": "Has\u0142o" + "host": "Nazwa hosta lub adres IP" }, "description": "Wybierz Roomb\u0119 lub Braava", "title": "Automatyczne po\u0142\u0105czenie z urz\u0105dzeniem" diff --git a/homeassistant/components/roomba/translations/pt-BR.json b/homeassistant/components/roomba/translations/pt-BR.json index ee0a29eac89..4db4e885ac7 100644 --- a/homeassistant/components/roomba/translations/pt-BR.json +++ b/homeassistant/components/roomba/translations/pt-BR.json @@ -11,13 +11,6 @@ }, "flow_title": "{name} ( {host} )", "step": { - "init": { - "data": { - "host": "Nome do host" - }, - "description": "Selecione um Roomba ou Braava.", - "title": "Conecte-se automaticamente ao dispositivo" - }, "link": { "description": "Pressione e segure o bot\u00e3o Home em {name} at\u00e9 que o dispositivo gere um som (cerca de dois segundos) e envie em 30 segundos.", "title": "Recuperar Senha" @@ -31,7 +24,6 @@ }, "manual": { "data": { - "blid": "BLID", "host": "Nome do host" }, "description": "Nenhum Roomba ou Braava foi descoberto em sua rede.", @@ -39,11 +31,7 @@ }, "user": { "data": { - "blid": "BLID", - "continuous": "Cont\u00ednuo", - "delay": "Atraso", - "host": "Nome do host", - "password": "Senha" + "host": "Nome do host" }, "description": "Selecione um Roomba ou Braava.", "title": "Conecte-se ao dispositivo" diff --git a/homeassistant/components/roomba/translations/pt.json b/homeassistant/components/roomba/translations/pt.json index 6036e870e6c..84fe0402f15 100644 --- a/homeassistant/components/roomba/translations/pt.json +++ b/homeassistant/components/roomba/translations/pt.json @@ -13,10 +13,7 @@ }, "user": { "data": { - "continuous": "Cont\u00ednuo", - "delay": "Atraso", - "host": "Servidor", - "password": "Palavra-passe" + "host": "Servidor" }, "title": "Conectar ao dispositivo" } diff --git a/homeassistant/components/roomba/translations/ru.json b/homeassistant/components/roomba/translations/ru.json index a8b31297f04..643da09744c 100644 --- a/homeassistant/components/roomba/translations/ru.json +++ b/homeassistant/components/roomba/translations/ru.json @@ -11,13 +11,6 @@ }, "flow_title": "{name} ({host})", "step": { - "init": { - "data": { - "host": "\u0425\u043e\u0441\u0442" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u044b\u043b\u0435\u0441\u043e\u0441 \u0438\u0437 \u043c\u043e\u0434\u0435\u043b\u0435\u0439 Roomba \u0438\u043b\u0438 Braava.", - "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" - }, "link": { "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0438 \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0439\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 Home \u043d\u0430 {name}, \u043f\u043e\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0438\u0437\u0434\u0430\u0441\u0442 \u0437\u0432\u0443\u043a (\u043e\u043a\u043e\u043b\u043e \u0434\u0432\u0443\u0445 \u0441\u0435\u043a\u0443\u043d\u0434). \u0417\u0430\u0442\u0435\u043c \u0432 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 30 \u0441\u0435\u043a\u0443\u043d\u0434 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\".", "title": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u043f\u0430\u0440\u043e\u043b\u044f" @@ -31,7 +24,6 @@ }, "manual": { "data": { - "blid": "BLID", "host": "\u0425\u043e\u0441\u0442" }, "description": "\u0412 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Roomba \u0438\u043b\u0438 Braava.", @@ -39,11 +31,7 @@ }, "user": { "data": { - "blid": "BLID", - "continuous": "\u041d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c", - "delay": "\u0417\u0430\u0434\u0435\u0440\u0436\u043a\u0430 (\u0441\u0435\u043a.)", - "host": "\u0425\u043e\u0441\u0442", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + "host": "\u0425\u043e\u0441\u0442" }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u044b\u043b\u0435\u0441\u043e\u0441 \u0438\u0437 \u043c\u043e\u0434\u0435\u043b\u0435\u0439 Roomba \u0438\u043b\u0438 Braava.", "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" diff --git a/homeassistant/components/roomba/translations/sl.json b/homeassistant/components/roomba/translations/sl.json index 9ad90ab82aa..ac046ac6ca7 100644 --- a/homeassistant/components/roomba/translations/sl.json +++ b/homeassistant/components/roomba/translations/sl.json @@ -6,11 +6,7 @@ "step": { "user": { "data": { - "blid": "BLID", - "continuous": "Nenehno", - "delay": "Zamik", - "host": "Ime gostitelja ali naslov IP", - "password": "Geslo" + "host": "Ime gostitelja ali naslov IP" }, "description": "Trenutno je pridobivanje BLID-a in gesla ro\u010dni postopek. Upo\u0161tevajte korake opisane v dokumentaciji na: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", "title": "Pove\u017eite se z napravo" diff --git a/homeassistant/components/roomba/translations/sv.json b/homeassistant/components/roomba/translations/sv.json index ee1f8972ef9..e2c491df80b 100644 --- a/homeassistant/components/roomba/translations/sv.json +++ b/homeassistant/components/roomba/translations/sv.json @@ -6,11 +6,7 @@ "step": { "user": { "data": { - "blid": "BLID", - "continuous": "Kontinuerlig", - "delay": "F\u00f6rdr\u00f6jning", - "host": "V\u00e4rdnamn eller IP-adress", - "password": "L\u00f6senord" + "host": "V\u00e4rdnamn eller IP-adress" }, "title": "Anslut till enheten" } diff --git a/homeassistant/components/roomba/translations/tr.json b/homeassistant/components/roomba/translations/tr.json index 97b4b61ada2..ecdaa8b97be 100644 --- a/homeassistant/components/roomba/translations/tr.json +++ b/homeassistant/components/roomba/translations/tr.json @@ -11,13 +11,6 @@ }, "flow_title": "{name} ({host})", "step": { - "init": { - "data": { - "host": "Ana Bilgisayar" - }, - "description": "Roomba veya Braava'y\u0131 se\u00e7in.", - "title": "Cihaza otomatik olarak ba\u011flan" - }, "link": { "description": "Cihaz bir ses \u00e7\u0131karana kadar (yakla\u015f\u0131k iki saniye) {name} \u00fczerindeki Ana Sayfa d\u00fc\u011fmesini bas\u0131l\u0131 tutun, ard\u0131ndan 30 saniye i\u00e7inde g\u00f6nderin.", "title": "\u015eifre Al" @@ -31,7 +24,6 @@ }, "manual": { "data": { - "blid": "BLID", "host": "Sunucu" }, "description": "A\u011f\u0131n\u0131zda Roomba veya Braava bulunamad\u0131.", @@ -39,11 +31,7 @@ }, "user": { "data": { - "blid": "BLID", - "continuous": "S\u00fcrekli", - "delay": "Gecikme", - "host": "Sunucu", - "password": "Parola" + "host": "Sunucu" }, "description": "Roomba veya Braava'y\u0131 se\u00e7in.", "title": "Cihaza ba\u011flan\u0131n" diff --git a/homeassistant/components/roomba/translations/uk.json b/homeassistant/components/roomba/translations/uk.json index 833a35f62f3..722b1127051 100644 --- a/homeassistant/components/roomba/translations/uk.json +++ b/homeassistant/components/roomba/translations/uk.json @@ -6,11 +6,7 @@ "step": { "user": { "data": { - "blid": "BLID", - "continuous": "\u0411\u0435\u0437\u043f\u0435\u0440\u0435\u0440\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", - "delay": "\u0417\u0430\u0442\u0440\u0438\u043c\u043a\u0430 (\u0441\u0435\u043a.)", - "host": "\u0425\u043e\u0441\u0442", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + "host": "\u0425\u043e\u0441\u0442" }, "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438, \u0449\u043e\u0431 \u0434\u0456\u0437\u043d\u0430\u0442\u0438\u0441\u044f \u044f\u043a \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 BLID \u0456 \u043f\u0430\u0440\u043e\u043b\u044c:\nhttps://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" diff --git a/homeassistant/components/roomba/translations/zh-Hant.json b/homeassistant/components/roomba/translations/zh-Hant.json index c17607e8be4..d0c59422625 100644 --- a/homeassistant/components/roomba/translations/zh-Hant.json +++ b/homeassistant/components/roomba/translations/zh-Hant.json @@ -11,13 +11,6 @@ }, "flow_title": "{name} ({host})", "step": { - "init": { - "data": { - "host": "\u4e3b\u6a5f\u7aef" - }, - "description": "\u9078\u64c7 Roomba \u6216 Braava\u3002", - "title": "\u81ea\u52d5\u9023\u7dda\u81f3\u88dd\u7f6e" - }, "link": { "description": "\u8acb\u6309\u4f4f {name} \u4e0a\u7684 Home \u9375\u76f4\u5230\u88dd\u7f6e\u767c\u51fa\u8072\u97f3\uff08\u7d04\u5169\u79d2\uff09\uff0c\u7136\u5f8c\u65bc 30 \u79d2\u5167\u50b3\u9001\u3002", "title": "\u91cd\u7f6e\u5bc6\u78bc" @@ -31,7 +24,6 @@ }, "manual": { "data": { - "blid": "BLID", "host": "\u4e3b\u6a5f\u7aef" }, "description": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Roomba \u6216 Braava\u3002", @@ -39,11 +31,7 @@ }, "user": { "data": { - "blid": "BLID", - "continuous": "\u9023\u7e8c", - "delay": "\u5ef6\u9072", - "host": "\u4e3b\u6a5f\u7aef", - "password": "\u5bc6\u78bc" + "host": "\u4e3b\u6a5f\u7aef" }, "description": "\u9078\u64c7 Roomba \u6216 Braava\u3002", "title": "\u81ea\u52d5\u9023\u7dda\u81f3\u88dd\u7f6e" diff --git a/homeassistant/components/roon/translations/bg.json b/homeassistant/components/roon/translations/bg.json index 5738012f715..54c68cff37a 100644 --- a/homeassistant/components/roon/translations/bg.json +++ b/homeassistant/components/roon/translations/bg.json @@ -13,11 +13,6 @@ "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } - }, - "user": { - "data": { - "host": "\u0425\u043e\u0441\u0442" - } } } } diff --git a/homeassistant/components/roon/translations/ca.json b/homeassistant/components/roon/translations/ca.json index 3f601ded490..860ad4cff38 100644 --- a/homeassistant/components/roon/translations/ca.json +++ b/homeassistant/components/roon/translations/ca.json @@ -18,12 +18,6 @@ "link": { "description": "Has d'autoritzar Home Assistant a Roon. Despr\u00e9s de fer clic a envia, ves a l'aplicaci\u00f3 Roon Core, obre la Configuraci\u00f3 i activa Home Assistant a la pestanya d'Extensions.", "title": "Autoritza Home Assistant a Roon" - }, - "user": { - "data": { - "host": "Amfitri\u00f3" - }, - "description": "No s'ha pogut descobrir el servidor Roon, introdueix el nom d'amfitri\u00f3 o la IP." } } } diff --git a/homeassistant/components/roon/translations/cs.json b/homeassistant/components/roon/translations/cs.json index 20187f9d4d2..150b2f5f74a 100644 --- a/homeassistant/components/roon/translations/cs.json +++ b/homeassistant/components/roon/translations/cs.json @@ -11,11 +11,6 @@ "link": { "description": "Mus\u00edte povolit Home Assistant v Roon. Po kliknut\u00ed na Odeslat p\u0159ejd\u011bte do aplikace Roon Core, otev\u0159ete Nastaven\u00ed a na z\u00e1lo\u017ece Roz\u0161\u00ed\u0159en\u00ed povolte Home Assistant.", "title": "Autorizujte HomeAssistant v Roon" - }, - "user": { - "data": { - "host": "Hostitel" - } } } } diff --git a/homeassistant/components/roon/translations/de.json b/homeassistant/components/roon/translations/de.json index e41fe77c3cd..377cae8946f 100644 --- a/homeassistant/components/roon/translations/de.json +++ b/homeassistant/components/roon/translations/de.json @@ -18,12 +18,6 @@ "link": { "description": "Du musst den Home Assistant in Roon autorisieren. Nachdem du auf \"Submit\" geklickt hast, gehe zur Roon Core-Anwendung, \u00f6ffne die Einstellungen und aktiviere HomeAssistant auf der Registerkarte \"Extensions\".", "title": "HomeAssistant in Roon autorisieren" - }, - "user": { - "data": { - "host": "Host" - }, - "description": "Roon-Server konnte nicht gefunden werden, bitte gib den Hostnamen oder die IP ein." } } } diff --git a/homeassistant/components/roon/translations/el.json b/homeassistant/components/roon/translations/el.json index 63cda0821b1..1216ff032a0 100644 --- a/homeassistant/components/roon/translations/el.json +++ b/homeassistant/components/roon/translations/el.json @@ -18,12 +18,6 @@ "link": { "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03c3\u03c4\u03bf Roon. \u0391\u03c6\u03bf\u03cd \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Roon Core, \u03b1\u03bd\u03bf\u03af\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03ba\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf HomeAssistant \u03c3\u03c4\u03b7\u03bd \u03ba\u03b1\u03c1\u03c4\u03ad\u03bb\u03b1 \u0395\u03c0\u03b5\u03ba\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2.", "title": "\u0395\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf HomeAssistant \u03c3\u03c4\u03bf Roon" - }, - "user": { - "data": { - "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" - }, - "description": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc\u03c2 \u03bf \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Roon, \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ae \u03c4\u03b7\u03bd IP \u03c3\u03b1\u03c2." } } } diff --git a/homeassistant/components/roon/translations/en.json b/homeassistant/components/roon/translations/en.json index b0affedc026..859f8c7307b 100644 --- a/homeassistant/components/roon/translations/en.json +++ b/homeassistant/components/roon/translations/en.json @@ -18,12 +18,6 @@ "link": { "description": "You must authorize Home Assistant in Roon. After you click submit, go to the Roon Core application, open Settings and enable HomeAssistant on the Extensions tab.", "title": "Authorize HomeAssistant in Roon" - }, - "user": { - "data": { - "host": "Host" - }, - "description": "Could not discover Roon server, please enter your the Hostname or IP." } } } diff --git a/homeassistant/components/roon/translations/es.json b/homeassistant/components/roon/translations/es.json index 45038d8e7dd..098ba60234a 100644 --- a/homeassistant/components/roon/translations/es.json +++ b/homeassistant/components/roon/translations/es.json @@ -18,12 +18,6 @@ "link": { "description": "Debes autorizar Home Assistant en Roon. Despu\u00e9s de pulsar en Enviar, ve a la aplicaci\u00f3n Roon Core, abre Configuraci\u00f3n y activa HomeAssistant en la pesta\u00f1a Extensiones.", "title": "Autorizar HomeAssistant en Roon" - }, - "user": { - "data": { - "host": "Host" - }, - "description": "No se ha podido descubrir el servidor Roon, introduce el nombre de host o la IP." } } } diff --git a/homeassistant/components/roon/translations/et.json b/homeassistant/components/roon/translations/et.json index d091f8e4064..3a7484680ea 100644 --- a/homeassistant/components/roon/translations/et.json +++ b/homeassistant/components/roon/translations/et.json @@ -18,12 +18,6 @@ "link": { "description": "Pead Roonis koduabilise volitama. Kui oled kl\u00f5psanud nuppu Esita, mine rakendusse Roon Core, ava Seaded ja luba vahekaardil Laiendused Home Assistant.", "title": "Volita HomeAssistant Roonis" - }, - "user": { - "data": { - "host": "" - }, - "description": "Rooni serverit ei leitud. Sisesta oma Rooni serveri hostinimi v\u00f5i IP." } } } diff --git a/homeassistant/components/roon/translations/fr.json b/homeassistant/components/roon/translations/fr.json index 7af98de188a..279ef703c59 100644 --- a/homeassistant/components/roon/translations/fr.json +++ b/homeassistant/components/roon/translations/fr.json @@ -18,12 +18,6 @@ "link": { "description": "Vous devez autoriser Home Assistant dans Roon. Apr\u00e8s avoir cliqu\u00e9 sur soumettre, acc\u00e9dez \u00e0 l'application Roon Core, ouvrez Param\u00e8tres et activez Home Assistant dans l'onglet Extensions.", "title": "Autoriser Home Assistant dans Roon" - }, - "user": { - "data": { - "host": "H\u00f4te" - }, - "description": "Veuillez entrer votre nom d\u2019h\u00f4te ou votre adresse IP sur votre serveur Roon." } } } diff --git a/homeassistant/components/roon/translations/he.json b/homeassistant/components/roon/translations/he.json index 05781910772..0c0d772740b 100644 --- a/homeassistant/components/roon/translations/he.json +++ b/homeassistant/components/roon/translations/he.json @@ -16,12 +16,6 @@ }, "link": { "description": "\u05e2\u05dc\u05d9\u05da \u05dc\u05d0\u05e9\u05e8 \u05dc-Home Assistant \u05d1-Roon. \u05dc\u05d0\u05d7\u05e8 \u05e9\u05ea\u05dc\u05d7\u05e5 \u05e2\u05dc \u05e9\u05dc\u05d7, \u05e2\u05d1\u05d5\u05e8 \u05dc\u05d9\u05d9\u05e9\u05d5\u05dd Roon Core, \u05e4\u05ea\u05d7 \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d5\u05d4\u05e4\u05e2\u05dc \u05d0\u05ea HomeAssistant \u05d1\u05db\u05e8\u05d8\u05d9\u05e1\u05d9\u05d9\u05d4 \u05d4\u05e8\u05d7\u05d1\u05d5\u05ea." - }, - "user": { - "data": { - "host": "\u05de\u05d0\u05e8\u05d7" - }, - "description": "\u05dc\u05d0 \u05d4\u05d9\u05ea\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d2\u05dc\u05d5\u05ea \u05d0\u05ea \u05e9\u05e8\u05ea Roon, \u05d4\u05d6\u05df \u05d0\u05ea \u05e9\u05dd \u05d4\u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05d4-IP." } } } diff --git a/homeassistant/components/roon/translations/hu.json b/homeassistant/components/roon/translations/hu.json index 3a9bcf3f522..aa536aeeddd 100644 --- a/homeassistant/components/roon/translations/hu.json +++ b/homeassistant/components/roon/translations/hu.json @@ -20,10 +20,6 @@ "title": "Enged\u00e9lyezze a Home Assistant alkalmaz\u00e1st Roon-ban" }, "user": { - "data": { - "host": "C\u00edm" - }, - "description": "A Roon szerver nem tal\u00e1lhat\u00f3, adja meg a hosztnev\u00e9t vagy c\u00edm\u00e9t", "one": "\u00dcres", "other": "\u00dcres" } diff --git a/homeassistant/components/roon/translations/id.json b/homeassistant/components/roon/translations/id.json index 85c1519e7be..cb3aa92df8c 100644 --- a/homeassistant/components/roon/translations/id.json +++ b/homeassistant/components/roon/translations/id.json @@ -18,12 +18,6 @@ "link": { "description": "Anda harus mengotorisasi Home Assistant di Roon. Setelah Anda mengeklik kirim, buka aplikasi Roon Core, buka Pengaturan dan aktifkan HomeAssistant pada tab Ekstensi.", "title": "Otorisasi HomeAssistant di Roon" - }, - "user": { - "data": { - "host": "Host" - }, - "description": "Tidak dapat menemukan server Roon, masukkan Nama Host atau IP Anda." } } } diff --git a/homeassistant/components/roon/translations/it.json b/homeassistant/components/roon/translations/it.json index 3236ebd0bb2..9a232281803 100644 --- a/homeassistant/components/roon/translations/it.json +++ b/homeassistant/components/roon/translations/it.json @@ -20,10 +20,6 @@ "title": "Autorizza HomeAssistant in Roon" }, "user": { - "data": { - "host": "Host" - }, - "description": "Impossibile individuare il server Roon, inserire l'hostname o l'IP.", "one": "Vuoto", "other": "Vuoti" } diff --git a/homeassistant/components/roon/translations/ja.json b/homeassistant/components/roon/translations/ja.json index 400d982d27c..12a1b074916 100644 --- a/homeassistant/components/roon/translations/ja.json +++ b/homeassistant/components/roon/translations/ja.json @@ -18,12 +18,6 @@ "link": { "description": "Roon\u3067Home Assistant\u3092\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u9001\u4fe1(submit) \u3092\u30af\u30ea\u30c3\u30af\u3057\u305f\u5f8c\u3001Roon Core\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3067\u3001\u8a2d\u5b9a(Settings )\u3092\u958b\u304d\u3001\u6a5f\u80fd\u62e1\u5f35\u30bf\u30d6(extensions tab)\u3067Home Assistant\u3092\u6709\u52b9(enable )\u306b\u3057\u307e\u3059\u3002", "title": "Roon\u3067HomeAssistant\u3092\u8a8d\u8a3c\u3059\u308b" - }, - "user": { - "data": { - "host": "\u30db\u30b9\u30c8" - }, - "description": "Roon server\u3092\u691c\u51fa\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3001 \u30db\u30b9\u30c8\u540d\u307e\u305f\u306f\u3001IP\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } } diff --git a/homeassistant/components/roon/translations/ko.json b/homeassistant/components/roon/translations/ko.json index a55249417af..96b9bf88e95 100644 --- a/homeassistant/components/roon/translations/ko.json +++ b/homeassistant/components/roon/translations/ko.json @@ -8,15 +8,16 @@ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { + "fallback": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8" + }, + "description": "Roon \uc11c\ubc84\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8 \uc774\ub984\uacfc \ud3ec\ud2b8\ub97c \uc785\ub825\ud558\uc2ed\uc2dc\uc624." + }, "link": { "description": "Roon\uc5d0\uc11c Home Assistant\ub97c \uc778\uc99d\ud574\uc8fc\uc5b4\uc57c \ud569\ub2c8\ub2e4. \ud655\uc778\uc744 \ud074\ub9ad\ud55c \ud6c4 Roon Core \uc560\ud50c\ub9ac\ucf00\uc774\uc158\uc73c\ub85c \uc774\ub3d9\ud558\uc5ec \uc124\uc815\uc744 \uc5f4\uace0 \ud655\uc7a5 \ud0ed\uc5d0\uc11c Home Assistant\ub97c \ud65c\uc131\ud654\ud574\uc8fc\uc138\uc694.", "title": "Roon\uc5d0\uc11c HomeAssistant \uc778\uc99d\ud558\uae30" - }, - "user": { - "data": { - "host": "\ud638\uc2a4\ud2b8" - }, - "description": "Roon \uc11c\ubc84\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." } } } diff --git a/homeassistant/components/roon/translations/lb.json b/homeassistant/components/roon/translations/lb.json index f5722ee75bb..6121d31b3ab 100644 --- a/homeassistant/components/roon/translations/lb.json +++ b/homeassistant/components/roon/translations/lb.json @@ -11,12 +11,6 @@ "link": { "description": "Du muss Home Assistant am Roon autoris\u00e9ieren. Nodeems Du op ofsch\u00e9cke geklickt hues, g\u00e9i an d'Roon Applikatioun, an d'Astellungen an aktiv\u00e9ier HomeAssistant an den Extensiounen.", "title": "HomeAssistant am Roon erlaaben" - }, - "user": { - "data": { - "host": "Host" - }, - "description": "G\u00ebff den Numm oder IP-Adress vun dengem Roon Server un." } } } diff --git a/homeassistant/components/roon/translations/nl.json b/homeassistant/components/roon/translations/nl.json index 9aafadc918b..fb35f22ce27 100644 --- a/homeassistant/components/roon/translations/nl.json +++ b/homeassistant/components/roon/translations/nl.json @@ -18,12 +18,6 @@ "link": { "description": "U moet Home Assistant autoriseren in Roon. Nadat je op verzenden hebt geklikt, ga je naar de Roon Core-applicatie, open je Instellingen en schakel je Home Assistant in op het tabblad Extensies.", "title": "Autoriseer Home Assistant in Roon" - }, - "user": { - "data": { - "host": "Host" - }, - "description": "Kon de Roon-server niet vinden, voer de hostnaam of het IP-adres in." } } } diff --git a/homeassistant/components/roon/translations/no.json b/homeassistant/components/roon/translations/no.json index 11eb60a04ce..4d44ef15e87 100644 --- a/homeassistant/components/roon/translations/no.json +++ b/homeassistant/components/roon/translations/no.json @@ -18,12 +18,6 @@ "link": { "description": "Du m\u00e5 godkjenne Home Assistant i Roon. N\u00e5r du klikker send inn, g\u00e5r du til Roon Core-programmet, \u00e5pner innstillingene og aktiverer Home Assistant p\u00e5 utvidelser-fanen.", "title": "Autoriser Home Assistant i Roon" - }, - "user": { - "data": { - "host": "Vert" - }, - "description": "Kunne ikke oppdage Roon-serveren. Angi vertsnavnet eller IP-adressen." } } } diff --git a/homeassistant/components/roon/translations/pl.json b/homeassistant/components/roon/translations/pl.json index b45a7fcb562..068153e2572 100644 --- a/homeassistant/components/roon/translations/pl.json +++ b/homeassistant/components/roon/translations/pl.json @@ -18,12 +18,6 @@ "link": { "description": "Musisz autoryzowa\u0107 Home Assistant w Roon. Po klikni\u0119ciu przycisku \"Zatwierd\u017a\", przejd\u017a do aplikacji Roon Core, otw\u00f3rz \"Ustawienia\" i w\u0142\u0105cz Home Assistant w karcie \"Rozszerzenia\" (Extensions).", "title": "Autoryzuj Home Assistant w Roon" - }, - "user": { - "data": { - "host": "Nazwa hosta lub adres IP" - }, - "description": "Nie wykryto serwera Roon, wprowad\u017a nazw\u0119 hosta lub adres IP." } } } diff --git a/homeassistant/components/roon/translations/pt-BR.json b/homeassistant/components/roon/translations/pt-BR.json index 85628df4a7c..6875841568d 100644 --- a/homeassistant/components/roon/translations/pt-BR.json +++ b/homeassistant/components/roon/translations/pt-BR.json @@ -18,12 +18,6 @@ "link": { "description": "Voc\u00ea deve autorizar o Home Assistant no Roon. Depois de clicar em enviar, v\u00e1 para o aplicativo Roon principal, abra Configura\u00e7\u00f5es e habilite o HomeAssistant na aba Extens\u00f5es.", "title": "Autorizar HomeAssistant no Roon" - }, - "user": { - "data": { - "host": "Nome do host" - }, - "description": "Por favor, digite seu hostname ou IP do servidor Roon." } } } diff --git a/homeassistant/components/roon/translations/pt.json b/homeassistant/components/roon/translations/pt.json index 6e36ff769cd..1e12fdcfcba 100644 --- a/homeassistant/components/roon/translations/pt.json +++ b/homeassistant/components/roon/translations/pt.json @@ -6,13 +6,6 @@ "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" - }, - "step": { - "user": { - "data": { - "host": "Servidor" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/roon/translations/ru.json b/homeassistant/components/roon/translations/ru.json index cc89dc2d2dd..17a44c95cf9 100644 --- a/homeassistant/components/roon/translations/ru.json +++ b/homeassistant/components/roon/translations/ru.json @@ -18,12 +18,6 @@ "link": { "description": "\u041f\u043e\u0441\u043b\u0435 \u043d\u0430\u0436\u0430\u0442\u0438\u044f \u043a\u043d\u043e\u043f\u043a\u0438 \u00ab\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\u00bb \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Roon Core, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u00ab\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438\u00bb \u0438 \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435 HomeAssistant \u043d\u0430 \u0432\u043a\u043b\u0430\u0434\u043a\u0435 \u00ab\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u044f\u00bb.", "title": "Roon" - }, - "user": { - "data": { - "host": "\u0425\u043e\u0441\u0442" - }, - "description": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 Roon, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." } } } diff --git a/homeassistant/components/roon/translations/tr.json b/homeassistant/components/roon/translations/tr.json index c18df90b9b8..a05dfed70f1 100644 --- a/homeassistant/components/roon/translations/tr.json +++ b/homeassistant/components/roon/translations/tr.json @@ -18,12 +18,6 @@ "link": { "description": "Roon'da HomeAssistant\u0131 yetkilendirmelisiniz. G\u00f6nder'e t\u0131klad\u0131ktan sonra, Roon Core uygulamas\u0131na gidin, Ayarlar'\u0131 a\u00e7\u0131n ve Uzant\u0131lar sekmesinde HomeAssistant'\u0131 etkinle\u015ftirin.", "title": "Roon'da HomeAssistant'\u0131 Yetkilendirme" - }, - "user": { - "data": { - "host": "Ana Bilgisayar" - }, - "description": "Roon sunucusu bulunamad\u0131, l\u00fctfen Ana Bilgisayar Ad\u0131n\u0131z\u0131 veya IP'nizi girin." } } } diff --git a/homeassistant/components/roon/translations/uk.json b/homeassistant/components/roon/translations/uk.json index a892e24692d..cdc6f369d43 100644 --- a/homeassistant/components/roon/translations/uk.json +++ b/homeassistant/components/roon/translations/uk.json @@ -11,12 +11,6 @@ "link": { "description": "\u041f\u0456\u0441\u043b\u044f \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f \u043a\u043d\u043e\u043f\u043a\u0438 \u00ab\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438\u00bb \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0432 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Roon Core, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u00ab\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u00bb \u0456 \u0443\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c HomeAssistant \u043d\u0430 \u0432\u043a\u043b\u0430\u0434\u0446\u0456 \u00ab\u0420\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u043d\u044f\u00bb.", "title": "Roon" - }, - "user": { - "data": { - "host": "\u0425\u043e\u0441\u0442" - }, - "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043d\u0430\u0437\u0432\u0443 \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Roon" } } } diff --git a/homeassistant/components/roon/translations/zh-Hant.json b/homeassistant/components/roon/translations/zh-Hant.json index f887b2d9847..8ec7bdf4276 100644 --- a/homeassistant/components/roon/translations/zh-Hant.json +++ b/homeassistant/components/roon/translations/zh-Hant.json @@ -18,12 +18,6 @@ "link": { "description": "\u5fc5\u9808\u65bc Roon \u4e2d\u8a8d\u8b49 Home Assistant\u3002\u9ede\u9078\u50b3\u9001\u5f8c\u3001\u958b\u555f Roon Core \u61c9\u7528\u7a0b\u5f0f\u3001\u6253\u958b\u8a2d\u5b9a\u4e26\u65bc\u64f4\u5145\uff08Extensions\uff09\u4e2d\u555f\u7528 HomeAssistant\u3002", "title": "\u65bc Roon \u4e2d\u8a8d\u8b49 HomeAssistant" - }, - "user": { - "data": { - "host": "\u4e3b\u6a5f\u7aef" - }, - "description": "\u627e\u4e0d\u5230 Roon \u4f3a\u670d\u5668\uff0c\u8acb\u8f38\u5165\u4e3b\u6a5f\u540d\u7a31\u6216 IP\u3002" } } } diff --git a/homeassistant/components/sabnzbd/translations/ko.json b/homeassistant/components/sabnzbd/translations/ko.json new file mode 100644 index 00000000000..86887876ff5 --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "api_key": "API \ud0a4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/bg.json b/homeassistant/components/samsungtv/translations/bg.json index 2246b4ad954..56d85b8ff5d 100644 --- a/homeassistant/components/samsungtv/translations/bg.json +++ b/homeassistant/components/samsungtv/translations/bg.json @@ -11,9 +11,6 @@ }, "flow_title": "{device}", "step": { - "confirm": { - "title": "Samsung TV" - }, "encrypted_pairing": { "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u041f\u0418\u041d \u043a\u043e\u0434\u0430, \u043f\u043e\u043a\u0430\u0437\u0430\u043d \u043d\u0430 {device}." }, diff --git a/homeassistant/components/samsungtv/translations/ca.json b/homeassistant/components/samsungtv/translations/ca.json index 95738ae65d9..ae4ead3f8e8 100644 --- a/homeassistant/components/samsungtv/translations/ca.json +++ b/homeassistant/components/samsungtv/translations/ca.json @@ -6,7 +6,6 @@ "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" @@ -18,8 +17,7 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "Vols configurar {device}? Si mai abans has connectat Home Assistant hauries de veure una finestra emergent a la pantalla del televisor demanant autenticaci\u00f3.", - "title": "Televisor Samsung" + "description": "Vols configurar {device}? Si mai abans has connectat Home Assistant hauries de veure una finestra emergent a la pantalla del televisor demanant autenticaci\u00f3." }, "encrypted_pairing": { "description": "Introdueix el PIN que es mostra a {device}." diff --git a/homeassistant/components/samsungtv/translations/cs.json b/homeassistant/components/samsungtv/translations/cs.json index 4c2241d4caa..8de7b1f9e13 100644 --- a/homeassistant/components/samsungtv/translations/cs.json +++ b/homeassistant/components/samsungtv/translations/cs.json @@ -16,8 +16,7 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "Chcete nastavit {device}? Pokud jste Home Assistant doposud nikdy nep\u0159ipojili, m\u011bla by se v\u00e1m na televizi zobrazit \u017e\u00e1dost o povolen\u00ed.", - "title": "Samsung TV" + "description": "Chcete nastavit {device}? Pokud jste Home Assistant doposud nikdy nep\u0159ipojili, m\u011bla by se v\u00e1m na televizi zobrazit \u017e\u00e1dost o povolen\u00ed." }, "encrypted_pairing": { "description": "Zadejte pros\u00edm PIN zobrazen\u00fd na {device}." diff --git a/homeassistant/components/samsungtv/translations/da.json b/homeassistant/components/samsungtv/translations/da.json index d41a62c5e7b..f335dbb9b29 100644 --- a/homeassistant/components/samsungtv/translations/da.json +++ b/homeassistant/components/samsungtv/translations/da.json @@ -9,8 +9,7 @@ "flow_title": "Samsung-tv: {model}", "step": { "confirm": { - "description": "Vil du konfigurere Samsung-tv {device}? Hvis du aldrig har oprettet forbindelse til Home Assistant f\u00f8r, b\u00f8r du se en popup p\u00e5 dit tv, der beder om godkendelse. Manuelle konfigurationer for dette tv vil blive overskrevet.", - "title": "Samsung-tv" + "description": "Vil du konfigurere Samsung-tv {device}? Hvis du aldrig har oprettet forbindelse til Home Assistant f\u00f8r, b\u00f8r du se en popup p\u00e5 dit tv, der beder om godkendelse. Manuelle konfigurationer for dette tv vil blive overskrevet." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/de.json b/homeassistant/components/samsungtv/translations/de.json index bfacbcd4b3c..7668a940f99 100644 --- a/homeassistant/components/samsungtv/translations/de.json +++ b/homeassistant/components/samsungtv/translations/de.json @@ -6,7 +6,6 @@ "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" @@ -18,8 +17,7 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "M\u00f6chtest du Samsung TV {device} einrichten? Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du eine Meldung auf deinem Fernseher sehen, die nach einer Autorisierung fragt. Manuelle Konfigurationen f\u00fcr dieses Fernsehger\u00e4t werden \u00fcberschrieben.", - "title": "Samsung TV" + "description": "M\u00f6chtest du Samsung TV {device} einrichten? Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du eine Meldung auf deinem Fernseher sehen, die nach einer Autorisierung fragt. Manuelle Konfigurationen f\u00fcr dieses Fernsehger\u00e4t werden \u00fcberschrieben." }, "encrypted_pairing": { "description": "Bitte gib die PIN ein, die auf {device} angezeigt wird." diff --git a/homeassistant/components/samsungtv/translations/el.json b/homeassistant/components/samsungtv/translations/el.json index 0e6c4df03bc..4388073f70b 100644 --- a/homeassistant/components/samsungtv/translations/el.json +++ b/homeassistant/components/samsungtv/translations/el.json @@ -6,7 +6,6 @@ "auth_missing": "\u03a4\u03bf Home Assistant \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03c3\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 Samsung. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c4\u03b7\u03c2 \u0394\u03b9\u03b1\u03c7\u03b5\u03af\u03c1\u03b9\u03c3\u03b7\u03c2 \u03b5\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03ce\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd \u03c4\u03b7\u03c2 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Home Assistant.", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "id_missing": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Samsung \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 SerialNumber.", - "missing_config_entry": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Samsung \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2.", "not_supported": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Samsung \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c4\u03b9\u03b3\u03bc\u03ae.", "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" @@ -18,8 +17,7 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {device}; \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03b9 \u03c0\u03bf\u03c4\u03ad \u03c4\u03bf Home Assistant \u03c0\u03c1\u03b9\u03bd, \u03b8\u03b1 \u03b4\u03b5\u03af\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03bd\u03b1\u03b4\u03c5\u03cc\u03bc\u03b5\u03bd\u03bf \u03c0\u03b1\u03c1\u03ac\u03b8\u03c5\u03c1\u03bf \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03b6\u03b7\u03c4\u03ac \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7.", - "title": "Samsung TV" + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {device}; \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03b9 \u03c0\u03bf\u03c4\u03ad \u03c4\u03bf Home Assistant \u03c0\u03c1\u03b9\u03bd, \u03b8\u03b1 \u03b4\u03b5\u03af\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03bd\u03b1\u03b4\u03c5\u03cc\u03bc\u03b5\u03bd\u03bf \u03c0\u03b1\u03c1\u03ac\u03b8\u03c5\u03c1\u03bf \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03b6\u03b7\u03c4\u03ac \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7." }, "encrypted_pairing": { "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf PIN \u03c0\u03bf\u03c5 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7 {device}." diff --git a/homeassistant/components/samsungtv/translations/en.json b/homeassistant/components/samsungtv/translations/en.json index 827f9992e46..c4e0e181090 100644 --- a/homeassistant/components/samsungtv/translations/en.json +++ b/homeassistant/components/samsungtv/translations/en.json @@ -6,7 +6,6 @@ "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" @@ -18,8 +17,7 @@ "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.", - "title": "Samsung TV" + "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." }, "encrypted_pairing": { "description": "Please enter the PIN displayed on {device}." diff --git a/homeassistant/components/samsungtv/translations/es-419.json b/homeassistant/components/samsungtv/translations/es-419.json index 179965fda80..c1e5110c967 100644 --- a/homeassistant/components/samsungtv/translations/es-419.json +++ b/homeassistant/components/samsungtv/translations/es-419.json @@ -9,8 +9,7 @@ "flow_title": "Televisi\u00f3n Samsung: {model}", "step": { "confirm": { - "description": "\u00bfDesea configurar la televisi\u00f3n Samsung {device}? Si nunca conect\u00f3 Home Assistant antes, deber\u00eda ver una ventana emergente en su televisor pidiendo autorizaci\u00f3n. Las configuraciones manuales para este televisor se sobrescribir\u00e1n.", - "title": "Samsung TV" + "description": "\u00bfDesea configurar la televisi\u00f3n Samsung {device}? Si nunca conect\u00f3 Home Assistant antes, deber\u00eda ver una ventana emergente en su televisor pidiendo autorizaci\u00f3n. Las configuraciones manuales para este televisor se sobrescribir\u00e1n." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json index 922ee0da4fe..1c2a81cb7c0 100644 --- a/homeassistant/components/samsungtv/translations/es.json +++ b/homeassistant/components/samsungtv/translations/es.json @@ -6,7 +6,6 @@ "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.", - "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" @@ -18,8 +17,7 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "\u00bfQuieres configurar {device}? Si nunca la has conectado a Home Assistant antes deber\u00edas ver una ventana en tu TV pidiendo autorizaci\u00f3n.", - "title": "Samsung TV" + "description": "\u00bfQuieres configurar {device}? Si nunca la has conectado a Home Assistant antes deber\u00edas ver una ventana en tu TV pidiendo autorizaci\u00f3n." }, "encrypted_pairing": { "description": "Introduce el PIN que se muestra en {device}." diff --git a/homeassistant/components/samsungtv/translations/et.json b/homeassistant/components/samsungtv/translations/et.json index 94e9db78ec2..e2dcf966ae9 100644 --- a/homeassistant/components/samsungtv/translations/et.json +++ b/homeassistant/components/samsungtv/translations/et.json @@ -6,7 +6,6 @@ "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" @@ -18,8 +17,7 @@ "flow_title": "{devicel}", "step": { "confirm": { - "description": "Kas soovid seadistada {devicel} ? Kui seda pole kunagi enne Home Assistantiga \u00fchendatud, n\u00e4ed oma teleris h\u00fcpikakent, mis k\u00fcsib tuvastamist.", - "title": "" + "description": "Kas soovid seadistada {devicel} ? Kui seda pole kunagi enne Home Assistantiga \u00fchendatud, n\u00e4ed oma teleris h\u00fcpikakent, mis k\u00fcsib tuvastamist." }, "encrypted_pairing": { "description": "Sisesta seadmes {device} kuvatav PIN-kood." diff --git a/homeassistant/components/samsungtv/translations/fr.json b/homeassistant/components/samsungtv/translations/fr.json index 438336f2818..1d651f9ce79 100644 --- a/homeassistant/components/samsungtv/translations/fr.json +++ b/homeassistant/components/samsungtv/translations/fr.json @@ -6,7 +6,6 @@ "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" @@ -18,8 +17,7 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "Voulez-vous configurer {device}\u00a0? Si vous n'avez jamais connect\u00e9 Home Assistant auparavant, une fen\u00eatre contextuelle d'autorisation devrait appara\u00eetre sur votre t\u00e9l\u00e9viseur.", - "title": "TV Samsung" + "description": "Voulez-vous configurer {device}\u00a0? Si vous n'avez jamais connect\u00e9 Home Assistant auparavant, une fen\u00eatre contextuelle d'autorisation devrait appara\u00eetre sur votre t\u00e9l\u00e9viseur." }, "encrypted_pairing": { "description": "Veuillez saisir le code PIN affich\u00e9 sur {device}." diff --git a/homeassistant/components/samsungtv/translations/he.json b/homeassistant/components/samsungtv/translations/he.json index 9f62de51b8c..2904c4c3012 100644 --- a/homeassistant/components/samsungtv/translations/he.json +++ b/homeassistant/components/samsungtv/translations/he.json @@ -13,9 +13,6 @@ }, "flow_title": "{device}", "step": { - "confirm": { - "title": "\u05d8\u05dc\u05d5\u05d5\u05d9\u05d6\u05d9\u05d4 \u05e9\u05dc \u05e1\u05de\u05e1\u05d5\u05e0\u05d2" - }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", diff --git a/homeassistant/components/samsungtv/translations/hu.json b/homeassistant/components/samsungtv/translations/hu.json index dc42596e290..b53f50ff74a 100644 --- a/homeassistant/components/samsungtv/translations/hu.json +++ b/homeassistant/components/samsungtv/translations/hu.json @@ -6,7 +6,6 @@ "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" @@ -18,8 +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 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" + "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." }, "encrypted_pairing": { "description": "K\u00e9rj\u00fck, adja meg az eszk\u00f6z\u00f6n megjelen\u0151 PIN-k\u00f3dot: {device}" diff --git a/homeassistant/components/samsungtv/translations/id.json b/homeassistant/components/samsungtv/translations/id.json index 394cfc41ae1..2dd96798748 100644 --- a/homeassistant/components/samsungtv/translations/id.json +++ b/homeassistant/components/samsungtv/translations/id.json @@ -6,7 +6,6 @@ "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" @@ -18,8 +17,7 @@ "flow_title": "{device}", "step": { "confirm": { - "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" + "description": "Apakah Anda ingin menyiapkan {device}? Jika Anda belum pernah menyambungkan Home Assistant sebelumnya, Anda akan melihat dialog di TV yang meminta otorisasi." }, "encrypted_pairing": { "description": "Masukkan PIN yang ditampilkan di {device} ." diff --git a/homeassistant/components/samsungtv/translations/it.json b/homeassistant/components/samsungtv/translations/it.json index 6bfc26fb445..2f96d7566a6 100644 --- a/homeassistant/components/samsungtv/translations/it.json +++ b/homeassistant/components/samsungtv/translations/it.json @@ -6,7 +6,6 @@ "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" @@ -18,8 +17,7 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "Vuoi configurare {device}? Se non hai mai collegato Home Assistant, dovresti vedere un popup sulla tua TV che chiede l'autorizzazione.", - "title": "Samsung TV" + "description": "Vuoi configurare {device}? Se non hai mai collegato Home Assistant, dovresti vedere un popup sulla tua TV che chiede l'autorizzazione." }, "encrypted_pairing": { "description": "Digita il PIN visualizzato su {device}." diff --git a/homeassistant/components/samsungtv/translations/ja.json b/homeassistant/components/samsungtv/translations/ja.json index d1b5670e25c..6b0cae1ba4d 100644 --- a/homeassistant/components/samsungtv/translations/ja.json +++ b/homeassistant/components/samsungtv/translations/ja.json @@ -6,7 +6,6 @@ "auth_missing": "Home Assistant\u306f\u3001\u3053\u306eSamsungTV\u3078\u306e\u63a5\u7d9a\u3092\u8a31\u53ef\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c6\u30ec\u30d3\u306e\u5916\u90e8\u30c7\u30d0\u30a4\u30b9\u30de\u30cd\u30fc\u30b8\u30e3\u30fc\u306e\u8a2d\u5b9a\u3092\u78ba\u8a8d\u3057\u3066\u3001Home Assistant\u3092\u8a8d\u8a3c\u3057\u307e\u3059\u3002", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "id_missing": "\u3053\u306eSamsung\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u30b7\u30ea\u30a2\u30eb\u756a\u53f7\u304c\u3042\u308a\u307e\u305b\u3093\u3002", - "missing_config_entry": "\u3053\u306eSamsung\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u3042\u308a\u307e\u305b\u3093\u3002", "not_supported": "\u3053\u306eSamsung\u30c7\u30d0\u30a4\u30b9\u306f\u73fe\u5728\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" @@ -18,8 +17,7 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "{device} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f\u3053\u308c\u307e\u3067\u306bHome Assistant\u3092\u4e00\u5ea6\u3082\u63a5\u7d9a\u3057\u305f\u3053\u3068\u304c\u306a\u3044\u5834\u5408\u306f\u3001\u30c6\u30ec\u30d3\u306b\u8a8d\u8a3c\u3092\u6c42\u3081\u308b\u30dd\u30c3\u30d7\u30a2\u30c3\u30d7\u304c\u8868\u793a\u3055\u308c\u307e\u3059\u3002", - "title": "Samsung TV" + "description": "{device} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f\u3053\u308c\u307e\u3067\u306bHome Assistant\u3092\u4e00\u5ea6\u3082\u63a5\u7d9a\u3057\u305f\u3053\u3068\u304c\u306a\u3044\u5834\u5408\u306f\u3001\u30c6\u30ec\u30d3\u306b\u8a8d\u8a3c\u3092\u6c42\u3081\u308b\u30dd\u30c3\u30d7\u30a2\u30c3\u30d7\u304c\u8868\u793a\u3055\u308c\u307e\u3059\u3002" }, "encrypted_pairing": { "description": "{device} \u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" diff --git a/homeassistant/components/samsungtv/translations/ko.json b/homeassistant/components/samsungtv/translations/ko.json index 4f62bbf6237..8b129d8ab50 100644 --- a/homeassistant/components/samsungtv/translations/ko.json +++ b/homeassistant/components/samsungtv/translations/ko.json @@ -10,8 +10,10 @@ "flow_title": "\uc0bc\uc131 TV: {model}", "step": { "confirm": { - "description": "{device} \uc0bc\uc131 TV\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? Home Assistant\ub97c \uc5f0\uacb0\ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV\uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4. \uc774 TV\uc758 \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ub41c \ub0b4\uc6a9\uc740 \ub36e\uc5b4\uc50c\uc6cc\uc9d1\ub2c8\ub2e4.", - "title": "\uc0bc\uc131 TV" + "description": "{device} \uc0bc\uc131 TV\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? Home Assistant\ub97c \uc5f0\uacb0\ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV\uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4. \uc774 TV\uc758 \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ub41c \ub0b4\uc6a9\uc740 \ub36e\uc5b4\uc50c\uc6cc\uc9d1\ub2c8\ub2e4." + }, + "pairing": { + "description": "{device} \uc0bc\uc131 TV\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? Home Assistant\ub97c \uc5f0\uacb0\ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV\uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4. \uc774 TV\uc758 \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ub41c \ub0b4\uc6a9\uc740 \ub36e\uc5b4\uc50c\uc6cc\uc9d1\ub2c8\ub2e4." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/lb.json b/homeassistant/components/samsungtv/translations/lb.json index 0740ab119a4..732507d3bb1 100644 --- a/homeassistant/components/samsungtv/translations/lb.json +++ b/homeassistant/components/samsungtv/translations/lb.json @@ -10,8 +10,7 @@ "flow_title": "Samsnung TV:{model}", "step": { "confirm": { - "description": "W\u00ebllt dir de Samsung TV {device} ariichten?. Falls dir Home Assistant nach ni domat verbonnen hutt misst den TV eng Meldung mat enger Authentifiz\u00e9ierung uweisen. Manuell Konfiguratioun fir d\u00ebse TV g\u00ebtt iwwerschriwwen.", - "title": "Samsnung TV" + "description": "W\u00ebllt dir de Samsung TV {device} ariichten?. Falls dir Home Assistant nach ni domat verbonnen hutt misst den TV eng Meldung mat enger Authentifiz\u00e9ierung uweisen. Manuell Konfiguratioun fir d\u00ebse TV g\u00ebtt iwwerschriwwen." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/nl.json b/homeassistant/components/samsungtv/translations/nl.json index b75f9b5970e..9fe7e1dc3f9 100644 --- a/homeassistant/components/samsungtv/translations/nl.json +++ b/homeassistant/components/samsungtv/translations/nl.json @@ -6,7 +6,6 @@ "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" @@ -18,8 +17,7 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "Wilt u Samsung TV {device} instellen? Als u nooit eerder Home Assistant hebt verbonden dan zou u een popup op uw TV moeten zien waarin u om toestemming wordt vraagt. Handmatige configuraties voor deze TV worden overschreven", - "title": "Samsung TV" + "description": "Wilt u Samsung TV {device} instellen? Als u nooit eerder Home Assistant hebt verbonden dan zou u een popup op uw TV moeten zien waarin u om toestemming wordt vraagt. Handmatige configuraties voor deze TV worden overschreven" }, "encrypted_pairing": { "description": "Voer de pincode in die wordt weergegeven op {device} ." diff --git a/homeassistant/components/samsungtv/translations/no.json b/homeassistant/components/samsungtv/translations/no.json index 2125b407f29..3b515608128 100644 --- a/homeassistant/components/samsungtv/translations/no.json +++ b/homeassistant/components/samsungtv/translations/no.json @@ -6,7 +6,6 @@ "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" @@ -18,8 +17,7 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "Vil du konfigurere {device} ? Hvis du aldri har koblet til Home Assistant f\u00f8r, b\u00f8r du se en popup p\u00e5 TV-en din som ber om autorisasjon.", - "title": "" + "description": "Vil du konfigurere {device} ? Hvis du aldri har koblet til Home Assistant f\u00f8r, b\u00f8r du se en popup p\u00e5 TV-en din som ber om autorisasjon." }, "encrypted_pairing": { "description": "Vennligst skriv inn PIN-koden som vises p\u00e5 {device} ." diff --git a/homeassistant/components/samsungtv/translations/pl.json b/homeassistant/components/samsungtv/translations/pl.json index 015f048dc60..f61c0eb8ced 100644 --- a/homeassistant/components/samsungtv/translations/pl.json +++ b/homeassistant/components/samsungtv/translations/pl.json @@ -6,7 +6,6 @@ "auth_missing": "Home Assistant nie ma uprawnie\u0144 do po\u0142\u0105czenia si\u0119 z tym telewizorem Samsung. Sprawd\u017a ustawienia \"Mened\u017cera urz\u0105dze\u0144 zewn\u0119trznych\", aby autoryzowa\u0107 Home Assistant.", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "id_missing": "To urz\u0105dzenie Samsung nie ma numeru seryjnego.", - "missing_config_entry": "To urz\u0105dzenie Samsung nie ma wpisu konfiguracyjnego.", "not_supported": "To urz\u0105dzenie Samsung nie jest obecnie obs\u0142ugiwane", "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", "unknown": "Nieoczekiwany b\u0142\u0105d" @@ -18,8 +17,7 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "Czy chcesz skonfigurowa\u0107 {device}? Je\u015bli nigdy wcze\u015bniej nie \u0142\u0105czy\u0142e\u015b go z Home Assistantem, na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie.", - "title": "Samsung TV" + "description": "Czy chcesz skonfigurowa\u0107 {device}? Je\u015bli nigdy wcze\u015bniej nie \u0142\u0105czy\u0142e\u015b go z Home Assistantem, na jego ekranie powinna pojawi\u0107 si\u0119 pro\u015bba o uwierzytelnienie." }, "encrypted_pairing": { "description": "Wprowad\u017a kod PIN wy\u015bwietlony na {device}." diff --git a/homeassistant/components/samsungtv/translations/pt-BR.json b/homeassistant/components/samsungtv/translations/pt-BR.json index dbb4b50fe93..c1480bb93c9 100644 --- a/homeassistant/components/samsungtv/translations/pt-BR.json +++ b/homeassistant/components/samsungtv/translations/pt-BR.json @@ -6,7 +6,6 @@ "auth_missing": "O Home Assistant n\u00e3o est\u00e1 autorizado a se conectar a esta TV Samsung. Verifique as configura\u00e7\u00f5es do Gerenciador de dispositivos externos da sua TV para autorizar o Home Assistant.", "cannot_connect": "Falha ao conectar", "id_missing": "Este dispositivo Samsung n\u00e3o possui um SerialNumber.", - "missing_config_entry": "Este dispositivo Samsung n\u00e3o tem uma entrada de configura\u00e7\u00e3o.", "not_supported": "Este dispositivo Samsung n\u00e3o \u00e9 compat\u00edvel no momento.", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", "unknown": "Erro inesperado" @@ -18,8 +17,7 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "Deseja configurar {device}? Se voc\u00ea nunca conectou o Home Assistant antes, aparecer\u00e1 um pop-up na sua TV pedindo autoriza\u00e7\u00e3o.", - "title": "TV Samsung" + "description": "Deseja configurar {device}? Se voc\u00ea nunca conectou o Home Assistant antes, aparecer\u00e1 um pop-up na sua TV pedindo autoriza\u00e7\u00e3o." }, "encrypted_pairing": { "description": "Insira o PIN exibido em {device}." diff --git a/homeassistant/components/samsungtv/translations/pt.json b/homeassistant/components/samsungtv/translations/pt.json index 15e61d23627..b2cd242c7da 100644 --- a/homeassistant/components/samsungtv/translations/pt.json +++ b/homeassistant/components/samsungtv/translations/pt.json @@ -7,9 +7,6 @@ }, "flow_title": "TV Samsung: {model}", "step": { - "confirm": { - "title": "TV Samsung" - }, "user": { "data": { "host": "Servidor", diff --git a/homeassistant/components/samsungtv/translations/ru.json b/homeassistant/components/samsungtv/translations/ru.json index ee73070cf0e..5d6a3cb8e12 100644 --- a/homeassistant/components/samsungtv/translations/ru.json +++ b/homeassistant/components/samsungtv/translations/ru.json @@ -6,7 +6,6 @@ "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." @@ -18,8 +17,7 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {device}? \u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0438\u043a\u043e\u0433\u0434\u0430 \u0440\u0430\u043d\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u043b\u0438 \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung" + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {device}? \u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0438\u043a\u043e\u0433\u0434\u0430 \u0440\u0430\u043d\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u043b\u0438 \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438." }, "encrypted_pairing": { "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u044b\u0439 \u043d\u0430 {device}." diff --git a/homeassistant/components/samsungtv/translations/sl.json b/homeassistant/components/samsungtv/translations/sl.json index 41536f72b16..5ec6f1aca76 100644 --- a/homeassistant/components/samsungtv/translations/sl.json +++ b/homeassistant/components/samsungtv/translations/sl.json @@ -9,8 +9,7 @@ "flow_title": "Samsung TV: {model}", "step": { "confirm": { - "description": "Vnesite podatke o televizorju Samsung {device}. \u010ce \u0161e nikoli niste povezali Home Assistant, bi morali na televizorju videli pojavno okno, ki zahteva va\u0161e dovoljenje. Ro\u010dna konfiguracija za ta TV bo prepisana.", - "title": "Samsung TV" + "description": "Vnesite podatke o televizorju Samsung {device}. \u010ce \u0161e nikoli niste povezali Home Assistant, bi morali na televizorju videli pojavno okno, ki zahteva va\u0161e dovoljenje. Ro\u010dna konfiguracija za ta TV bo prepisana." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/sv.json b/homeassistant/components/samsungtv/translations/sv.json index 38b3bea8a22..e9c0803c865 100644 --- a/homeassistant/components/samsungtv/translations/sv.json +++ b/homeassistant/components/samsungtv/translations/sv.json @@ -11,8 +11,7 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "Vill du st\u00e4lla in Samsung TV {device}? Om du aldrig har anslutit Home Assistant innan du ska se ett popup-f\u00f6nster p\u00e5 tv:n och be om auktorisering. Manuella konfigurationer f\u00f6r den h\u00e4r TV:n skrivs \u00f6ver.", - "title": "Samsung TV" + "description": "Vill du st\u00e4lla in Samsung TV {device}? Om du aldrig har anslutit Home Assistant innan du ska se ett popup-f\u00f6nster p\u00e5 tv:n och be om auktorisering. Manuella konfigurationer f\u00f6r den h\u00e4r TV:n skrivs \u00f6ver." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/tr.json b/homeassistant/components/samsungtv/translations/tr.json index 668b321e26d..172bd0e093e 100644 --- a/homeassistant/components/samsungtv/translations/tr.json +++ b/homeassistant/components/samsungtv/translations/tr.json @@ -6,7 +6,6 @@ "auth_missing": "Home Assistant'\u0131n bu Samsung TV'ye ba\u011flanma izni yok. Home Assistant'\u0131 yetkilendirmek i\u00e7in l\u00fctfen TV'nin ayarlar\u0131n\u0131 kontrol et.", "cannot_connect": "Ba\u011flanma hatas\u0131", "id_missing": "Bu Samsung cihaz\u0131n\u0131n Seri Numaras\u0131 yok.", - "missing_config_entry": "Bu Samsung cihaz\u0131nda bir yap\u0131land\u0131rma giri\u015fi yok.", "not_supported": "Bu Samsung TV cihaz\u0131 \u015fu anda desteklenmiyor.", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", "unknown": "Beklenmeyen hata" @@ -18,8 +17,7 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "{device} kurulumunu yapmak istiyor musunuz? Home Assistant'\u0131 daha \u00f6nce hi\u00e7 ba\u011flamad\u0131ysan\u0131z, TV'nizde yetki isteyen bir a\u00e7\u0131l\u0131r pencere g\u00f6rmelisiniz.", - "title": "Samsung TV" + "description": "{device} kurulumunu yapmak istiyor musunuz? Home Assistant'\u0131 daha \u00f6nce hi\u00e7 ba\u011flamad\u0131ysan\u0131z, TV'nizde yetki isteyen bir a\u00e7\u0131l\u0131r pencere g\u00f6rmelisiniz." }, "encrypted_pairing": { "description": "L\u00fctfen {device} \u00fczerinde g\u00f6r\u00fcnt\u00fclenen PIN'i girin." diff --git a/homeassistant/components/samsungtv/translations/uk.json b/homeassistant/components/samsungtv/translations/uk.json index f6aa504ccc8..ae3f98e3aac 100644 --- a/homeassistant/components/samsungtv/translations/uk.json +++ b/homeassistant/components/samsungtv/translations/uk.json @@ -10,8 +10,7 @@ "flow_title": "Samsung TV: {model}", "step": { "confirm": { - "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 Samsung {device}? \u042f\u043a\u0449\u043e \u0446\u0435\u0439 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0440\u0430\u043d\u0456\u0448\u0435 \u043d\u0435 \u0431\u0443\u0432 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u0434\u043e Home Assistant, \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 \u043c\u0430\u0454 \u0437'\u044f\u0432\u0438\u0442\u0438\u0441\u044f \u0441\u043f\u043b\u0438\u0432\u0430\u044e\u0447\u0435 \u0432\u0456\u043a\u043d\u043e \u0456\u0437 \u0437\u0430\u043f\u0438\u0442\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457. \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430, \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0456 \u0432\u0440\u0443\u0447\u043d\u0443, \u0431\u0443\u0434\u0443\u0442\u044c \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0430\u043d\u0456.", - "title": "\u0422\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 Samsung" + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 Samsung {device}? \u042f\u043a\u0449\u043e \u0446\u0435\u0439 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0440\u0430\u043d\u0456\u0448\u0435 \u043d\u0435 \u0431\u0443\u0432 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u0434\u043e Home Assistant, \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 \u043c\u0430\u0454 \u0437'\u044f\u0432\u0438\u0442\u0438\u0441\u044f \u0441\u043f\u043b\u0438\u0432\u0430\u044e\u0447\u0435 \u0432\u0456\u043a\u043d\u043e \u0456\u0437 \u0437\u0430\u043f\u0438\u0442\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457. \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430, \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0456 \u0432\u0440\u0443\u0447\u043d\u0443, \u0431\u0443\u0434\u0443\u0442\u044c \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0430\u043d\u0456." }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/zh-Hans.json b/homeassistant/components/samsungtv/translations/zh-Hans.json index da6a5c3c9ba..3a18da7fa94 100644 --- a/homeassistant/components/samsungtv/translations/zh-Hans.json +++ b/homeassistant/components/samsungtv/translations/zh-Hans.json @@ -16,8 +16,7 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "\u662f\u5426\u8981\u914d\u7f6e {device} ?\n\u5982\u679c\u60a8\u4e4b\u524d\u4ece\u672a\u8fde\u63a5\u8fc7 Home Assistant \uff0c\u60a8\u5c06\u4f1a\u5728\u8be5\u7535\u89c6\u4e0a\u770b\u5230\u8bf7\u6c42\u6388\u6743\u7684\u5f39\u7a97\u3002", - "title": "\u4e09\u661f\u7535\u89c6" + "description": "\u662f\u5426\u8981\u914d\u7f6e {device} ?\n\u5982\u679c\u60a8\u4e4b\u524d\u4ece\u672a\u8fde\u63a5\u8fc7 Home Assistant \uff0c\u60a8\u5c06\u4f1a\u5728\u8be5\u7535\u89c6\u4e0a\u770b\u5230\u8bf7\u6c42\u6388\u6743\u7684\u5f39\u7a97\u3002" }, "reauth_confirm": { "description": "\u63d0\u4ea4\u4fe1\u606f\u540e\uff0c\u8bf7\u5728 30 \u79d2\u5185\u5728 {device} \u540c\u610f\u83b7\u53d6\u76f8\u5173\u6388\u6743\u3002" diff --git a/homeassistant/components/samsungtv/translations/zh-Hant.json b/homeassistant/components/samsungtv/translations/zh-Hant.json index 8f30090dd24..da3d0db877a 100644 --- a/homeassistant/components/samsungtv/translations/zh-Hant.json +++ b/homeassistant/components/samsungtv/translations/zh-Hant.json @@ -6,7 +6,6 @@ "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" @@ -18,8 +17,7 @@ "flow_title": "{device}", "step": { "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a {device}\uff1f\u5047\u5982\u60a8\u4e4b\u524d\u672a\u66fe\u9023\u7dda\u81f3 Home Assistant\uff0c\u61c9\u8a72\u6703\u65bc\u96fb\u8996\u4e0a\u6536\u5230\u9a57\u8b49\u8a0a\u606f\u3002", - "title": "\u4e09\u661f\u96fb\u8996" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {device}\uff1f\u5047\u5982\u60a8\u4e4b\u524d\u672a\u66fe\u9023\u7dda\u81f3 Home Assistant\uff0c\u61c9\u8a72\u6703\u65bc\u96fb\u8996\u4e0a\u6536\u5230\u9a57\u8b49\u8a0a\u606f\u3002" }, "encrypted_pairing": { "description": "\u8acb\u8f38\u5165\u986f\u793a\u65bc {device} \u4e0a\u7684 PIN \u78bc\u3002" diff --git a/homeassistant/components/sensibo/translations/bg.json b/homeassistant/components/sensibo/translations/bg.json index e600bafdb4a..1435a77a2fb 100644 --- a/homeassistant/components/sensibo/translations/bg.json +++ b/homeassistant/components/sensibo/translations/bg.json @@ -16,8 +16,7 @@ }, "user": { "data": { - "api_key": "API \u043a\u043b\u044e\u0447", - "name": "\u0418\u043c\u0435" + "api_key": "API \u043a\u043b\u044e\u0447" } } } diff --git a/homeassistant/components/sensibo/translations/ca.json b/homeassistant/components/sensibo/translations/ca.json index f062af9e519..1634e0b8e40 100644 --- a/homeassistant/components/sensibo/translations/ca.json +++ b/homeassistant/components/sensibo/translations/ca.json @@ -19,8 +19,7 @@ }, "user": { "data": { - "api_key": "Clau API", - "name": "Nom" + "api_key": "Clau API" } } } diff --git a/homeassistant/components/sensibo/translations/cs.json b/homeassistant/components/sensibo/translations/cs.json index 8e25544e301..fcfba839932 100644 --- a/homeassistant/components/sensibo/translations/cs.json +++ b/homeassistant/components/sensibo/translations/cs.json @@ -9,8 +9,7 @@ "step": { "user": { "data": { - "api_key": "Kl\u00ed\u010d API", - "name": "Jm\u00e9no" + "api_key": "Kl\u00ed\u010d API" } } } diff --git a/homeassistant/components/sensibo/translations/de.json b/homeassistant/components/sensibo/translations/de.json index 9e3b02c726c..d5900f0f25e 100644 --- a/homeassistant/components/sensibo/translations/de.json +++ b/homeassistant/components/sensibo/translations/de.json @@ -19,8 +19,7 @@ }, "user": { "data": { - "api_key": "API-Schl\u00fcssel", - "name": "Name" + "api_key": "API-Schl\u00fcssel" } } } diff --git a/homeassistant/components/sensibo/translations/el.json b/homeassistant/components/sensibo/translations/el.json index baa2545a7e0..7fd7ee26f97 100644 --- a/homeassistant/components/sensibo/translations/el.json +++ b/homeassistant/components/sensibo/translations/el.json @@ -19,8 +19,7 @@ }, "user": { "data": { - "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" } } } diff --git a/homeassistant/components/sensibo/translations/en.json b/homeassistant/components/sensibo/translations/en.json index 1b5d7cd9214..7b7c8aab7f1 100644 --- a/homeassistant/components/sensibo/translations/en.json +++ b/homeassistant/components/sensibo/translations/en.json @@ -19,8 +19,7 @@ }, "user": { "data": { - "api_key": "API Key", - "name": "Name" + "api_key": "API Key" } } } diff --git a/homeassistant/components/sensibo/translations/es.json b/homeassistant/components/sensibo/translations/es.json index c5033bce962..b91e1b63f9b 100644 --- a/homeassistant/components/sensibo/translations/es.json +++ b/homeassistant/components/sensibo/translations/es.json @@ -18,8 +18,7 @@ }, "user": { "data": { - "api_key": "Clave API", - "name": "Nombre" + "api_key": "Clave API" } } } diff --git a/homeassistant/components/sensibo/translations/et.json b/homeassistant/components/sensibo/translations/et.json index de5a158c1ad..b216ecc3260 100644 --- a/homeassistant/components/sensibo/translations/et.json +++ b/homeassistant/components/sensibo/translations/et.json @@ -19,8 +19,7 @@ }, "user": { "data": { - "api_key": "API v\u00f5ti", - "name": "Nimi" + "api_key": "API v\u00f5ti" } } } diff --git a/homeassistant/components/sensibo/translations/fr.json b/homeassistant/components/sensibo/translations/fr.json index 09e2e5e5045..f674ad2c9b2 100644 --- a/homeassistant/components/sensibo/translations/fr.json +++ b/homeassistant/components/sensibo/translations/fr.json @@ -19,8 +19,7 @@ }, "user": { "data": { - "api_key": "Cl\u00e9 d'API", - "name": "Nom" + "api_key": "Cl\u00e9 d'API" } } } diff --git a/homeassistant/components/sensibo/translations/he.json b/homeassistant/components/sensibo/translations/he.json index 3486bd3c646..29f4fc720da 100644 --- a/homeassistant/components/sensibo/translations/he.json +++ b/homeassistant/components/sensibo/translations/he.json @@ -16,8 +16,7 @@ }, "user": { "data": { - "api_key": "\u05de\u05e4\u05ea\u05d7 API", - "name": "\u05e9\u05dd" + "api_key": "\u05de\u05e4\u05ea\u05d7 API" } } } diff --git a/homeassistant/components/sensibo/translations/hu.json b/homeassistant/components/sensibo/translations/hu.json index 065a7b982ed..14c7cd756d0 100644 --- a/homeassistant/components/sensibo/translations/hu.json +++ b/homeassistant/components/sensibo/translations/hu.json @@ -19,8 +19,7 @@ }, "user": { "data": { - "api_key": "API kulcs", - "name": "Elnevez\u00e9s" + "api_key": "API kulcs" } } } diff --git a/homeassistant/components/sensibo/translations/id.json b/homeassistant/components/sensibo/translations/id.json index 479edabe69b..dfaf05cca33 100644 --- a/homeassistant/components/sensibo/translations/id.json +++ b/homeassistant/components/sensibo/translations/id.json @@ -19,8 +19,7 @@ }, "user": { "data": { - "api_key": "Kunci API", - "name": "Nama" + "api_key": "Kunci API" } } } diff --git a/homeassistant/components/sensibo/translations/it.json b/homeassistant/components/sensibo/translations/it.json index c10adf2cf38..912bfee5114 100644 --- a/homeassistant/components/sensibo/translations/it.json +++ b/homeassistant/components/sensibo/translations/it.json @@ -19,8 +19,7 @@ }, "user": { "data": { - "api_key": "Chiave API", - "name": "Nome" + "api_key": "Chiave API" } } } diff --git a/homeassistant/components/sensibo/translations/ja.json b/homeassistant/components/sensibo/translations/ja.json index 859722fbe73..4c74b6412ac 100644 --- a/homeassistant/components/sensibo/translations/ja.json +++ b/homeassistant/components/sensibo/translations/ja.json @@ -19,8 +19,7 @@ }, "user": { "data": { - "api_key": "API\u30ad\u30fc", - "name": "\u540d\u524d" + "api_key": "API\u30ad\u30fc" } } } diff --git a/homeassistant/components/sensibo/translations/nl.json b/homeassistant/components/sensibo/translations/nl.json index cc6529c3540..7235aa80a80 100644 --- a/homeassistant/components/sensibo/translations/nl.json +++ b/homeassistant/components/sensibo/translations/nl.json @@ -19,8 +19,7 @@ }, "user": { "data": { - "api_key": "API-sleutel", - "name": "Naam" + "api_key": "API-sleutel" } } } diff --git a/homeassistant/components/sensibo/translations/no.json b/homeassistant/components/sensibo/translations/no.json index 9d3098ccd9e..43f17f52f2e 100644 --- a/homeassistant/components/sensibo/translations/no.json +++ b/homeassistant/components/sensibo/translations/no.json @@ -19,8 +19,7 @@ }, "user": { "data": { - "api_key": "API-n\u00f8kkel", - "name": "Navn" + "api_key": "API-n\u00f8kkel" } } } diff --git a/homeassistant/components/sensibo/translations/pl.json b/homeassistant/components/sensibo/translations/pl.json index 022aaf52039..830ab3399b9 100644 --- a/homeassistant/components/sensibo/translations/pl.json +++ b/homeassistant/components/sensibo/translations/pl.json @@ -19,8 +19,7 @@ }, "user": { "data": { - "api_key": "Klucz API", - "name": "Nazwa" + "api_key": "Klucz API" } } } diff --git a/homeassistant/components/sensibo/translations/pt-BR.json b/homeassistant/components/sensibo/translations/pt-BR.json index ff0ac4883ba..89b8984ac49 100644 --- a/homeassistant/components/sensibo/translations/pt-BR.json +++ b/homeassistant/components/sensibo/translations/pt-BR.json @@ -19,8 +19,7 @@ }, "user": { "data": { - "api_key": "Chave da API", - "name": "Nome" + "api_key": "Chave da API" } } } diff --git a/homeassistant/components/sensibo/translations/ru.json b/homeassistant/components/sensibo/translations/ru.json index 96574822758..aafef088706 100644 --- a/homeassistant/components/sensibo/translations/ru.json +++ b/homeassistant/components/sensibo/translations/ru.json @@ -19,8 +19,7 @@ }, "user": { "data": { - "api_key": "\u041a\u043b\u044e\u0447 API", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + "api_key": "\u041a\u043b\u044e\u0447 API" } } } diff --git a/homeassistant/components/sensibo/translations/sk.json b/homeassistant/components/sensibo/translations/sk.json index c3bf532e7a3..d2401d8b7a3 100644 --- a/homeassistant/components/sensibo/translations/sk.json +++ b/homeassistant/components/sensibo/translations/sk.json @@ -6,8 +6,7 @@ "step": { "user": { "data": { - "api_key": "API k\u013e\u00fa\u010d", - "name": "N\u00e1zov" + "api_key": "API k\u013e\u00fa\u010d" } } } diff --git a/homeassistant/components/sensibo/translations/tr.json b/homeassistant/components/sensibo/translations/tr.json index b08f683b200..fbe466cfd8d 100644 --- a/homeassistant/components/sensibo/translations/tr.json +++ b/homeassistant/components/sensibo/translations/tr.json @@ -19,8 +19,7 @@ }, "user": { "data": { - "api_key": "API Anahtar\u0131", - "name": "Ad" + "api_key": "API Anahtar\u0131" } } } diff --git a/homeassistant/components/sensibo/translations/zh-Hant.json b/homeassistant/components/sensibo/translations/zh-Hant.json index 39cc7dc9662..c2639ea2ab9 100644 --- a/homeassistant/components/sensibo/translations/zh-Hant.json +++ b/homeassistant/components/sensibo/translations/zh-Hant.json @@ -19,8 +19,7 @@ }, "user": { "data": { - "api_key": "API \u91d1\u9470", - "name": "\u540d\u7a31" + "api_key": "API \u91d1\u9470" } } } diff --git a/homeassistant/components/sentry/translations/af.json b/homeassistant/components/sentry/translations/af.json deleted file mode 100644 index 7db651b6b96..00000000000 --- a/homeassistant/components/sentry/translations/af.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "Sentry" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/ca.json b/homeassistant/components/sentry/translations/ca.json index d83abb16f1e..b353a3a5a0e 100644 --- a/homeassistant/components/sentry/translations/ca.json +++ b/homeassistant/components/sentry/translations/ca.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "Sentry DSN" - }, - "description": "Introdueix el DSN de Sentry", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/sentry/translations/cs.json b/homeassistant/components/sentry/translations/cs.json index 28a2d603dd0..815d7a9bd09 100644 --- a/homeassistant/components/sentry/translations/cs.json +++ b/homeassistant/components/sentry/translations/cs.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "DSN" - }, - "description": "Zadejte sv\u00e9 Sentry DSN", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/sentry/translations/da.json b/homeassistant/components/sentry/translations/da.json index 689bdad67ac..8a6f6788302 100644 --- a/homeassistant/components/sentry/translations/da.json +++ b/homeassistant/components/sentry/translations/da.json @@ -3,12 +3,6 @@ "error": { "bad_dsn": "Ugyldigt DSN", "unknown": "Uventet fejl" - }, - "step": { - "user": { - "description": "Indtast dit Sentry-DSN", - "title": "Sentry" - } } } } \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/de.json b/homeassistant/components/sentry/translations/de.json index d408af0d885..9af3d633924 100644 --- a/homeassistant/components/sentry/translations/de.json +++ b/homeassistant/components/sentry/translations/de.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "Sentry-DSN" - }, - "description": "Gib deine Sentry-DSN ein", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/sentry/translations/el.json b/homeassistant/components/sentry/translations/el.json index fd64b2140e2..d416005a9c2 100644 --- a/homeassistant/components/sentry/translations/el.json +++ b/homeassistant/components/sentry/translations/el.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "DSN" - }, - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf DSN \u03c4\u03bf\u03c5 Sentry \u03c3\u03b1\u03c2", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/sentry/translations/en.json b/homeassistant/components/sentry/translations/en.json index 16bdac6b510..e989d4811fd 100644 --- a/homeassistant/components/sentry/translations/en.json +++ b/homeassistant/components/sentry/translations/en.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "Sentry DSN" - }, - "description": "Enter your Sentry DSN", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/sentry/translations/es-419.json b/homeassistant/components/sentry/translations/es-419.json index dd6866f7610..6e6269f693b 100644 --- a/homeassistant/components/sentry/translations/es-419.json +++ b/homeassistant/components/sentry/translations/es-419.json @@ -3,12 +3,6 @@ "error": { "bad_dsn": "DSN inv\u00e1lido", "unknown": "Error inesperado" - }, - "step": { - "user": { - "description": "Ingrese su DSN Sentry", - "title": "Centinela" - } } } } \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/es.json b/homeassistant/components/sentry/translations/es.json index a2b5cc31f9a..0bb2f70720c 100644 --- a/homeassistant/components/sentry/translations/es.json +++ b/homeassistant/components/sentry/translations/es.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "DSN" - }, - "description": "Introduzca su DSN Sentry", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/sentry/translations/et.json b/homeassistant/components/sentry/translations/et.json index 01386fa97e6..9e414a9d935 100644 --- a/homeassistant/components/sentry/translations/et.json +++ b/homeassistant/components/sentry/translations/et.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "Sentry DSN" - }, - "description": "Sisesta oma Sentry DSN", - "title": "" + } } } }, diff --git a/homeassistant/components/sentry/translations/fr.json b/homeassistant/components/sentry/translations/fr.json index 6850bfe8a71..6f041e4acdd 100644 --- a/homeassistant/components/sentry/translations/fr.json +++ b/homeassistant/components/sentry/translations/fr.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "DSN Sentry" - }, - "description": "Entrez votre DSN Sentry", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/sentry/translations/hu.json b/homeassistant/components/sentry/translations/hu.json index 0535657c3d0..5be91c081b7 100644 --- a/homeassistant/components/sentry/translations/hu.json +++ b/homeassistant/components/sentry/translations/hu.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "Sentry DSN" - }, - "description": "Adja meg a Sentry DSN-t", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/sentry/translations/id.json b/homeassistant/components/sentry/translations/id.json index 5f81eb1a445..1acc9574e82 100644 --- a/homeassistant/components/sentry/translations/id.json +++ b/homeassistant/components/sentry/translations/id.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "Sentry DSN" - }, - "description": "Masukkan DSN Sentry Anda", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/sentry/translations/it.json b/homeassistant/components/sentry/translations/it.json index 10a0d1bad2b..24738be06b8 100644 --- a/homeassistant/components/sentry/translations/it.json +++ b/homeassistant/components/sentry/translations/it.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "DSN Sentry" - }, - "description": "Inserisci il tuo DSN Sentry", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/sentry/translations/ja.json b/homeassistant/components/sentry/translations/ja.json index 13ce3b4a5ff..8ac8ebf58a1 100644 --- a/homeassistant/components/sentry/translations/ja.json +++ b/homeassistant/components/sentry/translations/ja.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "DSN" - }, - "description": "Sentry DSN\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/sentry/translations/ko.json b/homeassistant/components/sentry/translations/ko.json index 92e26b30c42..efd5e558edb 100644 --- a/homeassistant/components/sentry/translations/ko.json +++ b/homeassistant/components/sentry/translations/ko.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "DSN" - }, - "description": "Sentry DSN \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/sentry/translations/lb.json b/homeassistant/components/sentry/translations/lb.json index 3041a7eb156..8ab1be71190 100644 --- a/homeassistant/components/sentry/translations/lb.json +++ b/homeassistant/components/sentry/translations/lb.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "DSN" - }, - "description": "Gitt \u00e4r Sentry DSN un", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/sentry/translations/nl.json b/homeassistant/components/sentry/translations/nl.json index 67be245ffde..0397ac0f609 100644 --- a/homeassistant/components/sentry/translations/nl.json +++ b/homeassistant/components/sentry/translations/nl.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "Sentry DSN" - }, - "description": "Voer uw Sentry DSN in", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/sentry/translations/no.json b/homeassistant/components/sentry/translations/no.json index c3bb4acd26e..11e358f4908 100644 --- a/homeassistant/components/sentry/translations/no.json +++ b/homeassistant/components/sentry/translations/no.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "Sentry DSN" - }, - "description": "Fyll inn din Sentry DNS", - "title": "" + } } } }, diff --git a/homeassistant/components/sentry/translations/pl.json b/homeassistant/components/sentry/translations/pl.json index be5d008dcff..37489c9b619 100644 --- a/homeassistant/components/sentry/translations/pl.json +++ b/homeassistant/components/sentry/translations/pl.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "Sentry DSN" - }, - "description": "Wprowad\u017a DSN Sentry", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/sentry/translations/pt-BR.json b/homeassistant/components/sentry/translations/pt-BR.json index c489de8ee6d..39a7448716e 100644 --- a/homeassistant/components/sentry/translations/pt-BR.json +++ b/homeassistant/components/sentry/translations/pt-BR.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "Sentinela DSN" - }, - "description": "Digite seu DSN Sentry", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/sentry/translations/ru.json b/homeassistant/components/sentry/translations/ru.json index 6a7192a5385..903ca88bc6b 100644 --- a/homeassistant/components/sentry/translations/ru.json +++ b/homeassistant/components/sentry/translations/ru.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "Sentry DSN" - }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448 DSN Sentry", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/sentry/translations/sl.json b/homeassistant/components/sentry/translations/sl.json index 5a448c5b673..57990b75063 100644 --- a/homeassistant/components/sentry/translations/sl.json +++ b/homeassistant/components/sentry/translations/sl.json @@ -3,12 +3,6 @@ "error": { "bad_dsn": "Neveljaven DSN", "unknown": "Nepri\u010dakovana napaka" - }, - "step": { - "user": { - "description": "Vpi\u0161ite va\u0161 Sentry DSN", - "title": "Sentry" - } } } } \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/sv.json b/homeassistant/components/sentry/translations/sv.json index d04ea24b72a..45c4508ff9e 100644 --- a/homeassistant/components/sentry/translations/sv.json +++ b/homeassistant/components/sentry/translations/sv.json @@ -3,12 +3,6 @@ "error": { "bad_dsn": "Ogiltig DSN", "unknown": "Ov\u00e4ntat fel" - }, - "step": { - "user": { - "description": "Ange din Sentry DSN", - "title": "Sentry" - } } } } \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/tr.json b/homeassistant/components/sentry/translations/tr.json index 6369f727d17..a32bf17f843 100644 --- a/homeassistant/components/sentry/translations/tr.json +++ b/homeassistant/components/sentry/translations/tr.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "Sentry DSN" - }, - "description": "Sentry DSN'nizi girin", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/sentry/translations/uk.json b/homeassistant/components/sentry/translations/uk.json index 124ac4543ed..4246abba8dc 100644 --- a/homeassistant/components/sentry/translations/uk.json +++ b/homeassistant/components/sentry/translations/uk.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "DSN" - }, - "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0412\u0430\u0448 DSN Sentry", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/sentry/translations/zh-Hant.json b/homeassistant/components/sentry/translations/zh-Hant.json index 6d4615db892..99330fb4f12 100644 --- a/homeassistant/components/sentry/translations/zh-Hant.json +++ b/homeassistant/components/sentry/translations/zh-Hant.json @@ -11,9 +11,7 @@ "user": { "data": { "dsn": "Sentry DSN" - }, - "description": "\u8f38\u5165 Sentry DSN", - "title": "Sentry" + } } } }, diff --git a/homeassistant/components/senz/translations/ko.json b/homeassistant/components/senz/translations/ko.json new file mode 100644 index 00000000000..c3f709296a5 --- /dev/null +++ b/homeassistant/components/senz/translations/ko.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", + "oauth_error": "\uc798\ubabb\ub41c \ud1a0\ud070 \ub370\uc774\ud130\ub97c \ubc1b\uc558\uc2b5\ub2c8\ub2e4." + }, + "create_entry": { + "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "pick_implementation": { + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/ca.json b/homeassistant/components/shelly/translations/ca.json index 6430bdcf6a6..a9b1347e62e 100644 --- a/homeassistant/components/shelly/translations/ca.json +++ b/homeassistant/components/shelly/translations/ca.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", + "firmware_not_fully_provisioned": "Dispositiu no totalment aprovisionat. Posa't en contacte amb l'assist\u00e8ncia de Shelly", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, diff --git a/homeassistant/components/shelly/translations/fr.json b/homeassistant/components/shelly/translations/fr.json index cd19af62bad..3ed78b53b63 100644 --- a/homeassistant/components/shelly/translations/fr.json +++ b/homeassistant/components/shelly/translations/fr.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", + "firmware_not_fully_provisioned": "L'appareil n'est pas enti\u00e8rement provisionn\u00e9. Veuillez contacter le support Shelly", "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, diff --git a/homeassistant/components/shelly/translations/hu.json b/homeassistant/components/shelly/translations/hu.json index bfaf591d7c2..18f53ca232d 100644 --- a/homeassistant/components/shelly/translations/hu.json +++ b/homeassistant/components/shelly/translations/hu.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "firmware_not_fully_provisioned": "Az eszk\u00f6z nincs teljesen be\u00fczemelve. K\u00e9rj\u00fck, vegye fel a kapcsolatot a Shelly \u00fcgyf\u00e9lszolg\u00e1lat\u00e1val", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/shelly/translations/it.json b/homeassistant/components/shelly/translations/it.json index c004141cac4..7bd26761ad8 100644 --- a/homeassistant/components/shelly/translations/it.json +++ b/homeassistant/components/shelly/translations/it.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Impossibile connettersi", + "firmware_not_fully_provisioned": "Dispositivo non completamente supportato. Si prega di contattare il supporto Shelly", "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, diff --git a/homeassistant/components/shelly/translations/pt-BR.json b/homeassistant/components/shelly/translations/pt-BR.json index 8b8ec5ea020..125334b8d28 100644 --- a/homeassistant/components/shelly/translations/pt-BR.json +++ b/homeassistant/components/shelly/translations/pt-BR.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Falha ao conectar", + "firmware_not_fully_provisioned": "Dispositivo n\u00e3o totalmente provisionado. Entre em contato com o suporte da Shelly", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, diff --git a/homeassistant/components/sia/translations/ar.json b/homeassistant/components/sia/translations/ar.json index ebf7325b114..54e29ccfa5d 100644 --- a/homeassistant/components/sia/translations/ar.json +++ b/homeassistant/components/sia/translations/ar.json @@ -5,6 +5,5 @@ "title": "\u0625\u0646\u0634\u0627\u0621 \u0627\u062a\u0635\u0627\u0644 \u0644\u0623\u0646\u0638\u0645\u0629 \u0627\u0644\u0625\u0646\u0630\u0627\u0631 \u0627\u0644\u0642\u0627\u0626\u0645\u0629 \u0639\u0644\u0649 SIA." } } - }, - "title": "\u0623\u0646\u0638\u0645\u0629 \u0625\u0646\u0630\u0627\u0631 SIA" + } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/ca.json b/homeassistant/components/sia/translations/ca.json index 904d1542c8f..ed51fec34cd 100644 --- a/homeassistant/components/sia/translations/ca.json +++ b/homeassistant/components/sia/translations/ca.json @@ -45,6 +45,5 @@ "title": "Opcions de configuraci\u00f3 de SIA." } } - }, - "title": "SIA Alarm Systems" + } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/de.json b/homeassistant/components/sia/translations/de.json index d2c9fc05040..385e19b9c64 100644 --- a/homeassistant/components/sia/translations/de.json +++ b/homeassistant/components/sia/translations/de.json @@ -45,6 +45,5 @@ "title": "Optionen f\u00fcr das SIA-Setup." } } - }, - "title": "SIA Alarmsysteme" + } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/el.json b/homeassistant/components/sia/translations/el.json index fe565e503a1..e6d2253301a 100644 --- a/homeassistant/components/sia/translations/el.json +++ b/homeassistant/components/sia/translations/el.json @@ -45,6 +45,5 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 SIA." } } - }, - "title": "\u03a3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03b1 \u03a3\u03c5\u03bd\u03b1\u03b3\u03b5\u03c1\u03bc\u03bf\u03cd SIA" + } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/en.json b/homeassistant/components/sia/translations/en.json index 50a00a4bf23..9dea235b379 100644 --- a/homeassistant/components/sia/translations/en.json +++ b/homeassistant/components/sia/translations/en.json @@ -45,6 +45,5 @@ "title": "Options for the SIA Setup." } } - }, - "title": "SIA Alarm Systems" + } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/es.json b/homeassistant/components/sia/translations/es.json index 8e1bb05978d..8b8b7e97f1f 100644 --- a/homeassistant/components/sia/translations/es.json +++ b/homeassistant/components/sia/translations/es.json @@ -45,6 +45,5 @@ "title": "Opciones para la configuraci\u00f3n de SIA." } } - }, - "title": "Sistemas de alarma SIA" + } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/et.json b/homeassistant/components/sia/translations/et.json index 931718d78d8..ff1f73d217e 100644 --- a/homeassistant/components/sia/translations/et.json +++ b/homeassistant/components/sia/translations/et.json @@ -45,6 +45,5 @@ "title": "SIA seadistuse valikud." } } - }, - "title": "SIA Alarm Systems" + } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/fr.json b/homeassistant/components/sia/translations/fr.json index 843c707ce19..e3f36f8930c 100644 --- a/homeassistant/components/sia/translations/fr.json +++ b/homeassistant/components/sia/translations/fr.json @@ -45,6 +45,5 @@ "title": "Options pour la configuration SIA." } } - }, - "title": "Syst\u00e8mes d'alarme SIA" + } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/hu.json b/homeassistant/components/sia/translations/hu.json index 6a5c609e1f6..63a5208a44c 100644 --- a/homeassistant/components/sia/translations/hu.json +++ b/homeassistant/components/sia/translations/hu.json @@ -45,6 +45,5 @@ "title": "A SIA be\u00e1ll\u00edt\u00e1si lehet\u0151s\u00e9gek." } } - }, - "title": "SIA riaszt\u00f3rendszerek" + } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/id.json b/homeassistant/components/sia/translations/id.json index 4c80d089cbe..b9c927997f3 100644 --- a/homeassistant/components/sia/translations/id.json +++ b/homeassistant/components/sia/translations/id.json @@ -45,6 +45,5 @@ "title": "Opsi untuk Pengaturan SIA." } } - }, - "title": "Sistem Alarm SIA" + } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/it.json b/homeassistant/components/sia/translations/it.json index 977b316c9a5..f3e1b02c55a 100644 --- a/homeassistant/components/sia/translations/it.json +++ b/homeassistant/components/sia/translations/it.json @@ -45,6 +45,5 @@ "title": "Opzioni per l'impostazione SIA." } } - }, - "title": "Sistemi di allarme SIA" + } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/ja.json b/homeassistant/components/sia/translations/ja.json index 9286548ddb4..7e653b1e348 100644 --- a/homeassistant/components/sia/translations/ja.json +++ b/homeassistant/components/sia/translations/ja.json @@ -45,6 +45,5 @@ "title": "SIA\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3002" } } - }, - "title": "SIA\u30a2\u30e9\u30fc\u30e0\u30b7\u30b9\u30c6\u30e0" + } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/nl.json b/homeassistant/components/sia/translations/nl.json index 8afc0b88651..cd0d5183cb9 100644 --- a/homeassistant/components/sia/translations/nl.json +++ b/homeassistant/components/sia/translations/nl.json @@ -45,6 +45,5 @@ "title": "Opties voor de SIA Setup." } } - }, - "title": "SIA Alarm Systems" + } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/no.json b/homeassistant/components/sia/translations/no.json index 61ce61b7ee5..7f8cee998e5 100644 --- a/homeassistant/components/sia/translations/no.json +++ b/homeassistant/components/sia/translations/no.json @@ -45,6 +45,5 @@ "title": "Alternativer for SIA-oppsett." } } - }, - "title": "SIA Alarm Systems" + } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/pl.json b/homeassistant/components/sia/translations/pl.json index d41281519d4..8a7f5342e3f 100644 --- a/homeassistant/components/sia/translations/pl.json +++ b/homeassistant/components/sia/translations/pl.json @@ -45,6 +45,5 @@ "title": "Opcje dla konfiguracji SIA." } } - }, - "title": "Systemy alarmowe SIA" + } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/pt-BR.json b/homeassistant/components/sia/translations/pt-BR.json index a113717859f..55738b7728e 100644 --- a/homeassistant/components/sia/translations/pt-BR.json +++ b/homeassistant/components/sia/translations/pt-BR.json @@ -45,6 +45,5 @@ "title": "Op\u00e7\u00f5es para a configura\u00e7\u00e3o SIA." } } - }, - "title": "Sistemas de alarme SIA" + } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/ru.json b/homeassistant/components/sia/translations/ru.json index f7cfd4b5152..807a7a80877 100644 --- a/homeassistant/components/sia/translations/ru.json +++ b/homeassistant/components/sia/translations/ru.json @@ -45,6 +45,5 @@ "title": "SIA Alarm Systems" } } - }, - "title": "SIA Alarm Systems" + } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/tr.json b/homeassistant/components/sia/translations/tr.json index 8d5a1d9dbcb..2fa88f6fbc6 100644 --- a/homeassistant/components/sia/translations/tr.json +++ b/homeassistant/components/sia/translations/tr.json @@ -45,6 +45,5 @@ "title": "SIA Kurulumu i\u00e7in se\u00e7enekler." } } - }, - "title": "SIA Alarm Sistemleri" + } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/zh-Hant.json b/homeassistant/components/sia/translations/zh-Hant.json index 81a0ad0fab4..310099e4882 100644 --- a/homeassistant/components/sia/translations/zh-Hant.json +++ b/homeassistant/components/sia/translations/zh-Hant.json @@ -45,6 +45,5 @@ "title": "SIA \u8a2d\u5b9a\u9078\u9805" } } - }, - "title": "SIA \u8b66\u5831\u7cfb\u7d71" + } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/bg.json b/homeassistant/components/simplisafe/translations/bg.json index d193393c534..da70697442c 100644 --- a/homeassistant/components/simplisafe/translations/bg.json +++ b/homeassistant/components/simplisafe/translations/bg.json @@ -4,7 +4,6 @@ "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": { - "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", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, @@ -25,11 +24,9 @@ }, "user": { "data": { - "auth_code": "\u041a\u043e\u0434 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "username": "E-mail \u0430\u0434\u0440\u0435\u0441" - }, - "title": "\u041f\u043e\u043f\u044a\u043b\u043d\u0435\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f\u0442\u0430 \u0441\u0438" + } } } } diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json index 82ac0eabbff..a2fd356932c 100644 --- a/homeassistant/components/simplisafe/translations/ca.json +++ b/homeassistant/components/simplisafe/translations/ca.json @@ -3,23 +3,16 @@ "abort": { "already_configured": "Aquest compte SimpliSafe ja est\u00e0 en \u00fas.", "email_2fa_timed_out": "S'ha esgotat el temps d'espera de l'autenticaci\u00f3 de dos factors a trav\u00e9s de correu electr\u00f2nic.", - "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", - "wrong_account": "Les credencials d'usuari proporcionades no coincideixen amb les d'aquest compte SimpliSafe." + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { - "2fa_timed_out": "S'ha esgotat el temps d'espera m\u00e0xim durant l'autenticaci\u00f3 de dos factors", - "identifier_exists": "Aquest compte ja est\u00e0 registrat", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "still_awaiting_mfa": "Esperant clic de l'enlla\u00e7 del correu MFA", "unknown": "Error inesperat" }, "progress": { "email_2fa": "Mira el correu electr\u00f2nic on hauries de trobar l'enlla\u00e7 de verificaci\u00f3 de Simplisafe." }, "step": { - "mfa": { - "title": "Autenticaci\u00f3 multi-factor SimpliSafe" - }, "reauth_confirm": { "data": { "password": "Contrasenya" @@ -35,13 +28,10 @@ }, "user": { "data": { - "auth_code": "Codi d'autoritzaci\u00f3", - "code": "Codi (utilitzat a la UI de Home Assistant)", "password": "Contrasenya", "username": "Nom d'usuari" }, - "description": "Introdueix el teu nom d'usuari i contrasenya.", - "title": "Introdueix la teva informaci\u00f3" + "description": "Introdueix el teu nom d'usuari i contrasenya." } } }, diff --git a/homeassistant/components/simplisafe/translations/cs.json b/homeassistant/components/simplisafe/translations/cs.json index 152c0282216..520dcc2567e 100644 --- a/homeassistant/components/simplisafe/translations/cs.json +++ b/homeassistant/components/simplisafe/translations/cs.json @@ -5,14 +5,10 @@ "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { - "identifier_exists": "\u00da\u010det je ji\u017e zaregistrov\u00e1n", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { - "mfa": { - "title": "V\u00edcefaktorov\u00e9 ov\u011b\u0159ov\u00e1n\u00ed SimpliSafe" - }, "reauth_confirm": { "data": { "password": "Heslo" @@ -22,12 +18,9 @@ }, "user": { "data": { - "auth_code": "Autoriza\u010dn\u00ed k\u00f3d", - "code": "K\u00f3d (pou\u017eit\u00fd v u\u017eivatelsk\u00e9m rozhran\u00ed Home Assistant)", "password": "Heslo", "username": "E-mail" - }, - "title": "Vypl\u0148te sv\u00e9 \u00fadaje." + } } } }, diff --git a/homeassistant/components/simplisafe/translations/da.json b/homeassistant/components/simplisafe/translations/da.json index a5119762f90..8133eee3ec9 100644 --- a/homeassistant/components/simplisafe/translations/da.json +++ b/homeassistant/components/simplisafe/translations/da.json @@ -3,16 +3,12 @@ "abort": { "already_configured": "Denne SimpliSafe-konto er allerede i brug." }, - "error": { - "identifier_exists": "Konto er allerede registreret" - }, "step": { "user": { "data": { "password": "Adgangskode", "username": "Emailadresse" - }, - "title": "Udfyld dine oplysninger" + } } } } diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index 1f7cd74043b..d224f3c2441 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -3,23 +3,16 @@ "abort": { "already_configured": "Dieses SimpliSafe-Konto wird bereits verwendet.", "email_2fa_timed_out": "Zeit\u00fcberschreitung beim Warten auf E-Mail-basierte Zwei-Faktor-Authentifizierung.", - "reauth_successful": "Die erneute Authentifizierung war erfolgreich", - "wrong_account": "Die angegebenen Benutzeranmeldeinformationen stimmen nicht mit diesem SimpliSafe-Konto \u00fcberein." + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { - "2fa_timed_out": "Zeit\u00fcberschreitung beim Warten auf Zwei-Faktor-Authentifizierung", - "identifier_exists": "Konto bereits registriert", "invalid_auth": "Ung\u00fcltige Authentifizierung", - "still_awaiting_mfa": "Immernoch warten auf MFA-E-Mail-Klick", "unknown": "Unerwarteter Fehler" }, "progress": { "email_2fa": "\u00dcberpr\u00fcfe deine E-Mails auf einen Best\u00e4tigungslink von Simplisafe." }, "step": { - "mfa": { - "title": "SimpliSafe Multi-Faktor-Authentifizierung" - }, "reauth_confirm": { "data": { "password": "Passwort" @@ -35,13 +28,10 @@ }, "user": { "data": { - "auth_code": "Autorisierungscode", - "code": "Code (wird in der Benutzeroberfl\u00e4che von Home Assistant verwendet)", "password": "Passwort", "username": "Benutzername" }, - "description": "Gib deinen Benutzernamen und Passwort ein.", - "title": "Gib deine Informationen ein" + "description": "Gib deinen Benutzernamen und Passwort ein." } } }, diff --git a/homeassistant/components/simplisafe/translations/el.json b/homeassistant/components/simplisafe/translations/el.json index 5253a1bfc52..d852664ce38 100644 --- a/homeassistant/components/simplisafe/translations/el.json +++ b/homeassistant/components/simplisafe/translations/el.json @@ -3,23 +3,16 @@ "abort": { "already_configured": "\u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 SimpliSafe \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7.", "email_2fa_timed_out": "\u03a4\u03bf \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03ad\u03bb\u03b7\u03be\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae \u03b3\u03b9\u03b1 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd \u03c0\u03bf\u03c5 \u03b2\u03b1\u03c3\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 email.", - "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", - "wrong_account": "\u03a4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03bf\u03c5\u03bd \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc SimpliSafe." + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { - "2fa_timed_out": "\u0388\u03bb\u03b7\u03be\u03b5 \u03c4\u03bf \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae \u03b3\u03b9\u03b1 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd", - "identifier_exists": "\u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ae\u03b4\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", - "still_awaiting_mfa": "\u0391\u03bd\u03b1\u03bc\u03ad\u03bd\u03b5\u03c4\u03b1\u03b9 \u03b1\u03ba\u03cc\u03bc\u03b7 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf email \u03c4\u03bf\u03c5 \u03a5\u03c0\u03bf\u03c5\u03c1\u03b3\u03b5\u03af\u03bf\u03c5 \u039f\u03b9\u03ba\u03bf\u03bd\u03bf\u03bc\u03b9\u03ba\u03ce\u03bd", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "progress": { "email_2fa": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd\n\u03c0\u03bf\u03c5 \u03c3\u03b1\u03c2 \u03b5\u03c3\u03c4\u03ac\u03bb\u03b7 \u03bc\u03ad\u03c3\u03c9 email." }, "step": { - "mfa": { - "title": "\u03a0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd SimpliSafe" - }, "reauth_confirm": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" @@ -35,13 +28,10 @@ }, "user": { "data": { - "auth_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2", - "code": "\u039a\u03ce\u03b4\u03b9\u03ba\u03b1\u03c2 (\u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf UI \u03c4\u03bf\u03c5 Home Assistant)", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "username": "Email" }, - "description": "\u03a4\u03bf SimpliSafe \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03c4\u03bf Home Assistant \u03bc\u03ad\u03c3\u03c9 \u03c4\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 SimpliSafe web. \u039b\u03cc\u03b3\u03c9 \u03c4\u03b5\u03c7\u03bd\u03b9\u03ba\u03ce\u03bd \u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ce\u03bd, \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ad\u03bd\u03b1 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03bf \u03b2\u03ae\u03bc\u03b1 \u03c3\u03c4\u03bf \u03c4\u03ad\u03bb\u03bf\u03c2 \u03b1\u03c5\u03c4\u03ae\u03c2 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03b9\u03ba\u03b1\u03c3\u03af\u03b1\u03c2- \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03ad\u03c7\u03b5\u03c4\u03b5 \u03b4\u03b9\u03b1\u03b2\u03ac\u03c3\u03b5\u03b9 \u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_url}) \u03c0\u03c1\u03b9\u03bd \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5.\n\n1. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf [\u03b5\u03b4\u03ce]({url}) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03bd\u03bf\u03af\u03be\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae SimpliSafe web \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03ac \u03c3\u03b1\u03c2.\n\n2. \u038c\u03c4\u03b1\u03bd \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03c9\u03b8\u03b5\u03af \u03b7 \u03b4\u03b9\u03b1\u03b4\u03b9\u03ba\u03b1\u03c3\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2, \u03b5\u03c0\u03b9\u03c3\u03c4\u03c1\u03ad\u03c8\u03c4\u03b5 \u03b5\u03b4\u03ce \u03ba\u03b1\u03b9 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2.", - "title": "\u03a3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03b1\u03c2" + "description": "\u03a4\u03bf SimpliSafe \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03c4\u03bf Home Assistant \u03bc\u03ad\u03c3\u03c9 \u03c4\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 SimpliSafe web. \u039b\u03cc\u03b3\u03c9 \u03c4\u03b5\u03c7\u03bd\u03b9\u03ba\u03ce\u03bd \u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ce\u03bd, \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ad\u03bd\u03b1 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03bf \u03b2\u03ae\u03bc\u03b1 \u03c3\u03c4\u03bf \u03c4\u03ad\u03bb\u03bf\u03c2 \u03b1\u03c5\u03c4\u03ae\u03c2 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03b9\u03ba\u03b1\u03c3\u03af\u03b1\u03c2- \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03ad\u03c7\u03b5\u03c4\u03b5 \u03b4\u03b9\u03b1\u03b2\u03ac\u03c3\u03b5\u03b9 \u03c4\u03b7\u03bd [\u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7]({docs_url}) \u03c0\u03c1\u03b9\u03bd \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5.\n\n1. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf [\u03b5\u03b4\u03ce]({url}) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03bd\u03bf\u03af\u03be\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae SimpliSafe web \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03ac \u03c3\u03b1\u03c2.\n\n2. \u038c\u03c4\u03b1\u03bd \u03bf\u03bb\u03bf\u03ba\u03bb\u03b7\u03c1\u03c9\u03b8\u03b5\u03af \u03b7 \u03b4\u03b9\u03b1\u03b4\u03b9\u03ba\u03b1\u03c3\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2, \u03b5\u03c0\u03b9\u03c3\u03c4\u03c1\u03ad\u03c8\u03c4\u03b5 \u03b5\u03b4\u03ce \u03ba\u03b1\u03b9 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2." } } }, diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 0aa7cf4cec1..0da6f6442e4 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -3,23 +3,16 @@ "abort": { "already_configured": "This SimpliSafe account is already in use.", "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", - "reauth_successful": "Re-authentication was successful", - "wrong_account": "The user credentials provided do not match this SimpliSafe account." + "reauth_successful": "Re-authentication was successful" }, "error": { - "2fa_timed_out": "Timed out while waiting for two-factor authentication", - "identifier_exists": "Account already registered", "invalid_auth": "Invalid authentication", - "still_awaiting_mfa": "Still awaiting MFA email click", "unknown": "Unexpected error" }, "progress": { "email_2fa": "Check your email for a verification link from Simplisafe." }, "step": { - "mfa": { - "title": "SimpliSafe Multi-Factor Authentication" - }, "reauth_confirm": { "data": { "password": "Password" @@ -35,13 +28,10 @@ }, "user": { "data": { - "auth_code": "Authorization Code", - "code": "Code (used in Home Assistant UI)", "password": "Password", "username": "Username" }, - "description": "Input your username and password.", - "title": "Fill in your information." + "description": "Input your username and password." } } }, diff --git a/homeassistant/components/simplisafe/translations/es-419.json b/homeassistant/components/simplisafe/translations/es-419.json index 7cdd07029eb..868d4d4f53d 100644 --- a/homeassistant/components/simplisafe/translations/es-419.json +++ b/homeassistant/components/simplisafe/translations/es-419.json @@ -3,17 +3,12 @@ "abort": { "already_configured": "Esta cuenta SimpliSafe ya est\u00e1 en uso." }, - "error": { - "identifier_exists": "Cuenta ya registrada" - }, "step": { "user": { "data": { - "code": "C\u00f3digo (utilizado en la interfaz de usuario de Home Assistant)", "password": "Contrase\u00f1a", "username": "Direcci\u00f3n de correo electr\u00f3nico" - }, - "title": "Completa tu informaci\u00f3n" + } } } }, diff --git a/homeassistant/components/simplisafe/translations/es.json b/homeassistant/components/simplisafe/translations/es.json index 632fc50e4c0..44716b87cec 100644 --- a/homeassistant/components/simplisafe/translations/es.json +++ b/homeassistant/components/simplisafe/translations/es.json @@ -2,23 +2,16 @@ "config": { "abort": { "already_configured": "Esta cuenta SimpliSafe ya est\u00e1 en uso.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", - "wrong_account": "Las credenciales de usuario proporcionadas no coinciden con esta cuenta de SimpliSafe." + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "2fa_timed_out": "Se ha agotado el tiempo de espera m\u00e1ximo durante la autenticaci\u00f3n de dos factores", - "identifier_exists": "Cuenta ya registrada", "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", - "still_awaiting_mfa": "Esperando todav\u00eda el clic en el correo electr\u00f3nico de MFA", "unknown": "Error inesperado" }, "progress": { "email_2fa": "Mira el correo electr\u00f3nico donde deber\u00edas encontrar el enlace de verificaci\u00f3n de Simplisafe." }, "step": { - "mfa": { - "title": "Autenticaci\u00f3n Multi-Factor SimpliSafe" - }, "reauth_confirm": { "data": { "password": "Contrase\u00f1a" @@ -34,13 +27,10 @@ }, "user": { "data": { - "auth_code": "C\u00f3digo de Autorizaci\u00f3n", - "code": "C\u00f3digo (utilizado en el interfaz de usuario de Home Assistant)", "password": "Contrase\u00f1a", "username": "Correo electr\u00f3nico" }, - "description": "SimpliSafe se autentifica con Home Assistant a trav\u00e9s de la aplicaci\u00f3n web de SimpliSafe. Debido a limitaciones t\u00e9cnicas, hay un paso manual al final de este proceso; aseg\u00farese de leer la [documentaci\u00f3n]({docs_url}) antes de empezar.\n\n1. Haga clic en [aqu\u00ed]({url}) para abrir la aplicaci\u00f3n web de SimpliSafe e introduzca sus credenciales.\n\n2. Cuando el proceso de inicio de sesi\u00f3n haya finalizado, vuelva aqu\u00ed e introduzca el c\u00f3digo de autorizaci\u00f3n que aparece a continuaci\u00f3n.", - "title": "Introduce tu informaci\u00f3n" + "description": "SimpliSafe se autentifica con Home Assistant a trav\u00e9s de la aplicaci\u00f3n web de SimpliSafe. Debido a limitaciones t\u00e9cnicas, hay un paso manual al final de este proceso; aseg\u00farese de leer la [documentaci\u00f3n]({docs_url}) antes de empezar.\n\n1. Haga clic en [aqu\u00ed]({url}) para abrir la aplicaci\u00f3n web de SimpliSafe e introduzca sus credenciales.\n\n2. Cuando el proceso de inicio de sesi\u00f3n haya finalizado, vuelva aqu\u00ed e introduzca el c\u00f3digo de autorizaci\u00f3n que aparece a continuaci\u00f3n." } } }, diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json index 75c908e60c0..0f01f4d9b9c 100644 --- a/homeassistant/components/simplisafe/translations/et.json +++ b/homeassistant/components/simplisafe/translations/et.json @@ -3,23 +3,16 @@ "abort": { "already_configured": "See SimpliSafe'i konto on juba kasutusel.", "email_2fa_timed_out": "Meilip\u00f5hise kahefaktorilise autentimise ajal\u00f5pp.", - "reauth_successful": "Taastuvastamine \u00f5nnestus", - "wrong_account": "Esitatud kasutaja mandaadid ei \u00fchti selle SimpliSafe kontoga." + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { - "2fa_timed_out": "Kahefaktoriline autentimine aegus", - "identifier_exists": "Konto on juba registreeritud", "invalid_auth": "Tuvastamise viga", - "still_awaiting_mfa": "Ootan endiselt MFA e-posti klikki", "unknown": "Tundmatu viga" }, "progress": { "email_2fa": "Kontrolli oma meili Simplisafe'i kinnituslingi saamiseks." }, "step": { - "mfa": { - "title": "SimpliSafe mitmeastmeline autentimine" - }, "reauth_confirm": { "data": { "password": "Salas\u00f5na" @@ -35,13 +28,10 @@ }, "user": { "data": { - "auth_code": "Tuvastuskood", - "code": "Kood (kasutatakse Home Assistant'i kasutajaliideses)", "password": "Salas\u00f5na", "username": "Kasutajanimi" }, - "description": "Sisesta kasutajatunnus ja salas\u00f5na", - "title": "Sisesta oma teave." + "description": "Sisesta kasutajatunnus ja salas\u00f5na" } } }, diff --git a/homeassistant/components/simplisafe/translations/fi.json b/homeassistant/components/simplisafe/translations/fi.json index d3d18987b44..47a52e9a0c6 100644 --- a/homeassistant/components/simplisafe/translations/fi.json +++ b/homeassistant/components/simplisafe/translations/fi.json @@ -1,15 +1,11 @@ { "config": { - "error": { - "identifier_exists": "Tili on jo rekister\u00f6ity" - }, "step": { "user": { "data": { "password": "Salasana", "username": "S\u00e4hk\u00f6postiosoite" - }, - "title": "T\u00e4yt\u00e4 tietosi." + } } } } diff --git a/homeassistant/components/simplisafe/translations/fr.json b/homeassistant/components/simplisafe/translations/fr.json index 1abfb1c4638..4de2af95885 100644 --- a/homeassistant/components/simplisafe/translations/fr.json +++ b/homeassistant/components/simplisafe/translations/fr.json @@ -3,23 +3,16 @@ "abort": { "already_configured": "Ce compte SimpliSafe est d\u00e9j\u00e0 utilis\u00e9.", "email_2fa_timed_out": "D\u00e9lai d'attente de l'authentification \u00e0 deux facteurs par courriel expir\u00e9.", - "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", - "wrong_account": "Les informations d'identification d'utilisateur fournies ne correspondent pas \u00e0 ce compte SimpliSafe." + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "2fa_timed_out": "D\u00e9lai d'attente de l'authentification \u00e0 deux facteurs expir\u00e9", - "identifier_exists": "Compte d\u00e9j\u00e0 enregistr\u00e9", "invalid_auth": "Authentification non valide", - "still_awaiting_mfa": "En attente de clic sur le message \u00e9lectronique d'authentification multi facteur", "unknown": "Erreur inattendue" }, "progress": { "email_2fa": "Vous devriez recevoir un lien de v\u00e9rification par courriel envoy\u00e9 par Simplisafe." }, "step": { - "mfa": { - "title": "Authentification multi facteur SimpliSafe" - }, "reauth_confirm": { "data": { "password": "Mot de passe" @@ -35,13 +28,10 @@ }, "user": { "data": { - "auth_code": "Code d'autorisation", - "code": "Code (utilis\u00e9 dans l'interface Home Assistant)", "password": "Mot de passe", "username": "Nom d'utilisateur" }, - "description": "Saisissez votre nom d'utilisateur et votre mot de passe.", - "title": "Veuillez saisir vos informations" + "description": "Saisissez votre nom d'utilisateur et votre mot de passe." } } }, diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index 6a0e6d33642..df71ba7ca8b 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -3,23 +3,16 @@ "abort": { "already_configured": "Ez a SimpliSafe-fi\u00f3k m\u00e1r haszn\u00e1latban van.", "email_2fa_timed_out": "Az e-mail alap\u00fa k\u00e9tfaktoros hiteles\u00edt\u00e9sre val\u00f3 v\u00e1rakoz\u00e1s ideje lej\u00e1rt", - "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." + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { - "2fa_timed_out": "A k\u00e9tfaktoros hiteles\u00edt\u00e9sre val\u00f3 v\u00e1rakoz\u00e1s ideje lej\u00e1rt", - "identifier_exists": "Fi\u00f3k m\u00e1r regisztr\u00e1lva van", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "still_awaiting_mfa": "M\u00e9g v\u00e1r az MFA e-mail kattint\u00e1sra", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "progress": { "email_2fa": "Adja meg a k\u00e9tfaktoros hiteles\u00edt\u00e9si k\u00f3dot amelyet e-mailben kapott." }, "step": { - "mfa": { - "title": "SimpliSafe t\u00f6bbt\u00e9nyez\u0151s hiteles\u00edt\u00e9s" - }, "reauth_confirm": { "data": { "password": "Jelsz\u00f3" @@ -35,13 +28,10 @@ }, "user": { "data": { - "auth_code": "Enged\u00e9lyez\u00e9si k\u00f3d", - "code": "K\u00f3d (a Home Assistant felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9n haszn\u00e1latos)", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Adja meg felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t.", - "title": "T\u00f6ltse ki az adatait" + "description": "Adja meg felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t." } } }, diff --git a/homeassistant/components/simplisafe/translations/id.json b/homeassistant/components/simplisafe/translations/id.json index 737831195f1..68da3b7689b 100644 --- a/homeassistant/components/simplisafe/translations/id.json +++ b/homeassistant/components/simplisafe/translations/id.json @@ -3,23 +3,16 @@ "abort": { "already_configured": "Akun SimpliSafe ini sudah digunakan.", "email_2fa_timed_out": "Tenggang waktu habis ketika menunggu autentikasi dua faktor berbasis email.", - "reauth_successful": "Autentikasi ulang berhasil", - "wrong_account": "Kredensial pengguna yang diberikan tidak cocok dengan akun SimpliSafe ini." + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { - "2fa_timed_out": "Tenggang waktu habis saat menunggu autentikasi dua faktor", - "identifier_exists": "Akun sudah terdaftar", "invalid_auth": "Autentikasi tidak valid", - "still_awaiting_mfa": "Masih menunggu pengeklikan dari email MFA", "unknown": "Kesalahan yang tidak diharapkan" }, "progress": { "email_2fa": "Periksa email Anda untuk tautan verifikasi dari Simplisafe." }, "step": { - "mfa": { - "title": "Autentikasi Multi-Faktor SimpliSafe" - }, "reauth_confirm": { "data": { "password": "Kata Sandi" @@ -35,13 +28,10 @@ }, "user": { "data": { - "auth_code": "Kode Otorisasi", - "code": "Kode (digunakan di antarmuka Home Assistant)", "password": "Kata Sandi", "username": "Nama Pengguna" }, - "description": "Masukkan nama pengguna dan kata sandi Anda.", - "title": "Isi informasi Anda." + "description": "Masukkan nama pengguna dan kata sandi Anda." } } }, diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index 17a9a2ceb47..b9caae3f0a4 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -3,23 +3,16 @@ "abort": { "already_configured": "Questo account SimpliSafe \u00e8 gi\u00e0 in uso.", "email_2fa_timed_out": "Timeout durante l'attesa dell'autenticazione a due fattori basata su email.", - "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", - "wrong_account": "Le credenziali utente fornite non corrispondono a questo account SimpliSafe." + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { - "2fa_timed_out": "Timeout durante l'attesa dell'autenticazione a due fattori", - "identifier_exists": "Account gi\u00e0 registrato", "invalid_auth": "Autenticazione non valida", - "still_awaiting_mfa": "Ancora in attesa del clic sull'email MFA", "unknown": "Errore imprevisto" }, "progress": { "email_2fa": "Verifica la presenza nella tua email di un collegamento di verifica da Simplisafe." }, "step": { - "mfa": { - "title": "Autenticazione a pi\u00f9 fattori (MFA) SimpliSafe " - }, "reauth_confirm": { "data": { "password": "Password" @@ -35,13 +28,10 @@ }, "user": { "data": { - "auth_code": "Codice di autorizzazione", - "code": "Codice (utilizzato nell'Interfaccia Utente di Home Assistant)", "password": "Password", "username": "Nome utente" }, - "description": "Digita il tuo nome utente e password.", - "title": "Inserisci le tue informazioni." + "description": "Digita il tuo nome utente e password." } } }, diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index 67e1630469e..57909d30f69 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -3,23 +3,16 @@ "abort": { "already_configured": "\u3053\u306eSimpliSafe account\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "email_2fa_timed_out": "\u96fb\u5b50\u30e1\u30fc\u30eb\u306b\u3088\u308b2\u8981\u7d20\u8a8d\u8a3c\u306e\u5f85\u6a5f\u4e2d\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", - "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "wrong_account": "\u63d0\u4f9b\u3055\u308c\u305f\u30e6\u30fc\u30b6\u30fc\u8a8d\u8a3c\u60c5\u5831\u304c\u3001\u3053\u306eSimpliSafe\u30a2\u30ab\u30a6\u30f3\u30c8\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { - "2fa_timed_out": "\u4e8c\u8981\u7d20\u8a8d\u8a3c\u306e\u5f85\u6a5f\u4e2d\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f", - "identifier_exists": "\u30a2\u30ab\u30a6\u30f3\u30c8\u767b\u9332\u6e08\u307f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "still_awaiting_mfa": "MFA email click\u3092\u307e\u3060\u5f85\u3063\u3066\u3044\u307e\u3059", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "progress": { "email_2fa": "Simplisafe\u304b\u3089\u306e\u78ba\u8a8d\u30ea\u30f3\u30af\u304c\u306a\u3044\u304b\u30e1\u30fc\u30eb\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { - "mfa": { - "title": "SimpliSafe\u591a\u8981\u7d20\u8a8d\u8a3c" - }, "reauth_confirm": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" @@ -35,13 +28,10 @@ }, "user": { "data": { - "auth_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9", - "code": "\u30b3\u30fc\u30c9(Home Assistant UI\u3067\u4f7f\u7528)", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "E\u30e1\u30fc\u30eb" }, - "description": "2021\u5e74\u3088\u308a\u3001SimpliSafe\u306fWeb\u30a2\u30d7\u30ea\u306b\u3088\u308b\u65b0\u3057\u3044\u8a8d\u8a3c\u6a5f\u69cb\u306b\u79fb\u884c\u3057\u307e\u3057\u305f\u3002\u6280\u8853\u7684\u306a\u5236\u9650\u306e\u305f\u3081\u3001\u3053\u306e\u30d7\u30ed\u30bb\u30b9\u306e\u6700\u5f8c\u306b\u624b\u52d5\u3067\u306e\u624b\u9806\u304c\u3042\u308a\u307e\u3059\u3002\u958b\u59cb\u3059\u308b\u524d\u306b\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3092\u5fc5\u305a\u304a\u8aad\u307f\u304f\u3060\u3055\u3044\u3002\n\n\u6e96\u5099\u304c\u3067\u304d\u305f\u3089\u3001[\u3053\u3053]({url}) \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066SimpliSafe\u306eWeb\u30a2\u30d7\u30ea\u3092\u958b\u304d\u3001\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002\u51e6\u7406\u304c\u5b8c\u4e86\u3057\u305f\u3089\u3001\u3053\u3053\u306b\u623b\u3063\u3066\u304d\u3066\u9001\u4fe1(submit) \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002", - "title": "\u3042\u306a\u305f\u306e\u60c5\u5831\u3092\u8a18\u5165\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "2021\u5e74\u3088\u308a\u3001SimpliSafe\u306fWeb\u30a2\u30d7\u30ea\u306b\u3088\u308b\u65b0\u3057\u3044\u8a8d\u8a3c\u6a5f\u69cb\u306b\u79fb\u884c\u3057\u307e\u3057\u305f\u3002\u6280\u8853\u7684\u306a\u5236\u9650\u306e\u305f\u3081\u3001\u3053\u306e\u30d7\u30ed\u30bb\u30b9\u306e\u6700\u5f8c\u306b\u624b\u52d5\u3067\u306e\u624b\u9806\u304c\u3042\u308a\u307e\u3059\u3002\u958b\u59cb\u3059\u308b\u524d\u306b\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3092\u5fc5\u305a\u304a\u8aad\u307f\u304f\u3060\u3055\u3044\u3002\n\n\u6e96\u5099\u304c\u3067\u304d\u305f\u3089\u3001[\u3053\u3053]({url}) \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066SimpliSafe\u306eWeb\u30a2\u30d7\u30ea\u3092\u958b\u304d\u3001\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002\u51e6\u7406\u304c\u5b8c\u4e86\u3057\u305f\u3089\u3001\u3053\u3053\u306b\u623b\u3063\u3066\u304d\u3066\u9001\u4fe1(submit) \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/simplisafe/translations/ko.json b/homeassistant/components/simplisafe/translations/ko.json index 5c98e8abaa6..4cbc24533e0 100644 --- a/homeassistant/components/simplisafe/translations/ko.json +++ b/homeassistant/components/simplisafe/translations/ko.json @@ -5,15 +5,13 @@ "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "identifier_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "still_awaiting_mfa": "\uc544\uc9c1 \ub2e4\ub2e8\uacc4 \uc778\uc99d(MFA) \uc774\uba54\uc77c\uc758 \ub9c1\ud06c \ud074\ub9ad\uc744 \uae30\ub2e4\ub9ac\uace0\uc788\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, + "progress": { + "email_2fa": "\uc774\uba54\uc77c\uc5d0\uc11c Simplisafe\uc758 \uc778\uc99d \ub9c1\ud06c\ub97c \ud655\uc778\ud558\uc2ed\uc2dc\uc624." + }, "step": { - "mfa": { - "title": "SimpliSafe \ub2e4\ub2e8\uacc4 \uc778\uc99d" - }, "reauth_confirm": { "data": { "password": "\ube44\ubc00\ubc88\ud638" @@ -23,11 +21,9 @@ }, "user": { "data": { - "code": "\ucf54\ub4dc (Home Assistant UI \uc5d0\uc11c \uc0ac\uc6a9\ub428)", "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc774\uba54\uc77c" - }, - "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." + } } } }, diff --git a/homeassistant/components/simplisafe/translations/lb.json b/homeassistant/components/simplisafe/translations/lb.json index 080d8afd394..e9034565e93 100644 --- a/homeassistant/components/simplisafe/translations/lb.json +++ b/homeassistant/components/simplisafe/translations/lb.json @@ -5,15 +5,10 @@ "reauth_successful": "Erfollegr\u00e4ich re-authentifiz\u00e9iert." }, "error": { - "identifier_exists": "Konto ass scho registr\u00e9iert", "invalid_auth": "Ong\u00eblteg Authentifikatioun", - "still_awaiting_mfa": "Waart nach den MFA E-Mail Klick.", "unknown": "Onerwaarte Feeler" }, "step": { - "mfa": { - "title": "SimpliSafe Multi-Faktor Authentifikatioun" - }, "reauth_confirm": { "data": { "password": "Passwuert" @@ -23,11 +18,9 @@ }, "user": { "data": { - "code": "Code (benotzt am Home Assistant Benotzer Interface)", "password": "Passwuert", "username": "E-Mail" - }, - "title": "F\u00ebllt \u00e4r Informatiounen aus" + } } } }, diff --git a/homeassistant/components/simplisafe/translations/nl.json b/homeassistant/components/simplisafe/translations/nl.json index ff282832e4c..55e7c3c7832 100644 --- a/homeassistant/components/simplisafe/translations/nl.json +++ b/homeassistant/components/simplisafe/translations/nl.json @@ -3,23 +3,16 @@ "abort": { "already_configured": "Dit SimpliSafe-account is al in gebruik.", "email_2fa_timed_out": "Timed out tijdens het wachten op email-gebaseerde twee-factor authenticatie.", - "reauth_successful": "Herauthenticatie was succesvol", - "wrong_account": "De opgegeven gebruikersgegevens komen niet overeen met deze SimpliSafe-account." + "reauth_successful": "Herauthenticatie was succesvol" }, "error": { - "2fa_timed_out": "Timed out tijdens het wachten op twee-factor authenticatie", - "identifier_exists": "Account bestaat al", "invalid_auth": "Ongeldige authenticatie", - "still_awaiting_mfa": "Wacht nog steeds op MFA-e-mailklik", "unknown": "Onverwachte fout" }, "progress": { "email_2fa": "Controleer uw e-mail voor een verificatielink van Simplisafe." }, "step": { - "mfa": { - "title": "SimpliSafe Multi-Factor Authenticatie" - }, "reauth_confirm": { "data": { "password": "Wachtwoord" @@ -35,13 +28,10 @@ }, "user": { "data": { - "auth_code": "Autorisatie Code", - "code": "Code (gebruikt in Home Assistant)", "password": "Wachtwoord", "username": "Gebruikersnaam" }, - "description": "Voer uw gebruikersnaam en wachtwoord in.", - "title": "Vul uw gegevens in" + "description": "Voer uw gebruikersnaam en wachtwoord in." } } }, diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json index 06a7260fd0e..7e6875a2590 100644 --- a/homeassistant/components/simplisafe/translations/no.json +++ b/homeassistant/components/simplisafe/translations/no.json @@ -3,23 +3,16 @@ "abort": { "already_configured": "Denne SimpliSafe-kontoen er allerede i bruk.", "email_2fa_timed_out": "Tidsavbrudd mens du ventet p\u00e5 e-postbasert tofaktorautentisering.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", - "wrong_account": "Brukerlegitimasjonen som er oppgitt, samsvarer ikke med denne SimpliSafe -kontoen." + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { - "2fa_timed_out": "Tidsavbrutt mens du ventet p\u00e5 tofaktorautentisering", - "identifier_exists": "Konto er allerede registrert", "invalid_auth": "Ugyldig godkjenning", - "still_awaiting_mfa": "Forventer fortsatt MFA-e-postklikk", "unknown": "Uventet feil" }, "progress": { "email_2fa": "Sjekk e-posten din for en bekreftelseslenke fra Simplisafe." }, "step": { - "mfa": { - "title": "SimpliSafe flertrinnsbekreftelse" - }, "reauth_confirm": { "data": { "password": "Passord" @@ -35,13 +28,10 @@ }, "user": { "data": { - "auth_code": "Autorisasjonskode", - "code": "Kode (brukt i Home Assistant brukergrensesnittet)", "password": "Passord", "username": "Brukernavn" }, - "description": "Skriv inn brukernavn og passord.", - "title": "Fyll ut informasjonen din." + "description": "Skriv inn brukernavn og passord." } } }, diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json index ec574217c58..22c4739b7dd 100644 --- a/homeassistant/components/simplisafe/translations/pl.json +++ b/homeassistant/components/simplisafe/translations/pl.json @@ -3,23 +3,16 @@ "abort": { "already_configured": "To konto SimpliSafe jest ju\u017c w u\u017cyciu", "email_2fa_timed_out": "Przekroczono limit czasu oczekiwania na e-mailowe uwierzytelnianie dwusk\u0142adnikowe.", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", - "wrong_account": "Podane dane uwierzytelniaj\u0105ce u\u017cytkownika nie pasuj\u0105 do tego konta SimpliSafe." + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { - "2fa_timed_out": "Przekroczono limit czasu oczekiwania na uwierzytelnianie dwusk\u0142adnikowe", - "identifier_exists": "Konto jest ju\u017c zarejestrowane", "invalid_auth": "Niepoprawne uwierzytelnienie", - "still_awaiting_mfa": "Wci\u0105\u017c nie potwierdzono linka w e-mailu od SimpliSafe", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "progress": { "email_2fa": "Sprawd\u017a e-mail z linkiem weryfikacyjnym od Simplisafe." }, "step": { - "mfa": { - "title": "Uwierzytelnianie wielosk\u0142adnikowe SimpliSafe" - }, "reauth_confirm": { "data": { "password": "Has\u0142o" @@ -35,13 +28,10 @@ }, "user": { "data": { - "auth_code": "Kod autoryzacji", - "code": "Kod (u\u017cywany w interfejsie Home Assistant)", "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" }, - "description": "Wprowad\u017a swoj\u0105 nazw\u0119 u\u017cytkownika i has\u0142o.", - "title": "Wprowad\u017a dane" + "description": "Wprowad\u017a swoj\u0105 nazw\u0119 u\u017cytkownika i has\u0142o." } } }, diff --git a/homeassistant/components/simplisafe/translations/pt-BR.json b/homeassistant/components/simplisafe/translations/pt-BR.json index 3cc1703e11c..ccfb13b6cc1 100644 --- a/homeassistant/components/simplisafe/translations/pt-BR.json +++ b/homeassistant/components/simplisafe/translations/pt-BR.json @@ -3,23 +3,16 @@ "abort": { "already_configured": "A conta j\u00e1 foi configurada", "email_2fa_timed_out": "Expirou enquanto aguardava a autentica\u00e7\u00e3o de dois fatores enviada por e-mail.", - "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", - "wrong_account": "As credenciais de usu\u00e1rio fornecidas n\u00e3o correspondem a esta conta SimpliSafe." + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { - "2fa_timed_out": "Expirou enquanto aguardava a autentica\u00e7\u00e3o de dois fatores", - "identifier_exists": "Conta j\u00e1 cadastrada", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "still_awaiting_mfa": "Ainda aguardando clique no e-mail da MFA", "unknown": "Erro inesperado" }, "progress": { "email_2fa": "Verifique seu e-mail para obter um link de verifica\u00e7\u00e3o do Simplisafe." }, "step": { - "mfa": { - "title": "Autentica\u00e7\u00e3o SimpliSafe multifator" - }, "reauth_confirm": { "data": { "password": "Senha" @@ -35,13 +28,10 @@ }, "user": { "data": { - "auth_code": "C\u00f3digo de autoriza\u00e7\u00e3o", - "code": "C\u00f3digo (usado na IU do Home Assistant)", "password": "Senha", "username": "Usu\u00e1rio" }, - "description": "Insira seu nome de usu\u00e1rio e senha.", - "title": "Preencha suas informa\u00e7\u00f5es." + "description": "Insira seu nome de usu\u00e1rio e senha." } } }, diff --git a/homeassistant/components/simplisafe/translations/pt.json b/homeassistant/components/simplisafe/translations/pt.json index 9b5df6cf934..ba84255bc9d 100644 --- a/homeassistant/components/simplisafe/translations/pt.json +++ b/homeassistant/components/simplisafe/translations/pt.json @@ -4,7 +4,6 @@ "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { - "identifier_exists": "Conta j\u00e1 registada", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, @@ -19,8 +18,7 @@ "data": { "password": "Palavra-passe", "username": "Email" - }, - "title": "Preencha as suas informa\u00e7\u00f5es" + } } } } diff --git a/homeassistant/components/simplisafe/translations/ro.json b/homeassistant/components/simplisafe/translations/ro.json index 7531bef0a8e..efbbe49f38a 100644 --- a/homeassistant/components/simplisafe/translations/ro.json +++ b/homeassistant/components/simplisafe/translations/ro.json @@ -1,15 +1,11 @@ { "config": { - "error": { - "identifier_exists": "Contul este deja \u00eenregistrat" - }, "step": { "user": { "data": { "password": "Parola", "username": "Adresa de email" - }, - "title": "Completa\u021bi informa\u021biile dvs." + } } } } diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index 0d2a36085e4..198d5225983 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -3,23 +3,16 @@ "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.", "email_2fa_timed_out": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "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." + "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": { - "2fa_timed_out": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "still_awaiting_mfa": "\u041e\u0436\u0438\u0434\u0430\u043d\u0438\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u043f\u043e \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u0435.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "progress": { "email_2fa": "\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 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f \u043e\u0442 Simplisafe." }, "step": { - "mfa": { - "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" - }, "reauth_confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" @@ -35,13 +28,10 @@ }, "user": { "data": { - "auth_code": "\u041a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438", - "code": "\u041a\u043e\u0434 (\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 Home Assistant)", "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.", - "title": "SimpliSafe" + "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." } } }, diff --git a/homeassistant/components/simplisafe/translations/sl.json b/homeassistant/components/simplisafe/translations/sl.json index 6f326bc5b68..d98ad9c20c7 100644 --- a/homeassistant/components/simplisafe/translations/sl.json +++ b/homeassistant/components/simplisafe/translations/sl.json @@ -1,20 +1,14 @@ { "config": { "abort": { - "already_configured": "Ta ra\u010dun SimpliSafe je \u017ee v uporabi.", - "wrong_account": "Navedene uporabni\u0161ke poverilnice se ne ujemajo s tem ra\u010dunom SimpliSafe." - }, - "error": { - "identifier_exists": "Ra\u010dun je \u017ee registriran" + "already_configured": "Ta ra\u010dun SimpliSafe je \u017ee v uporabi." }, "step": { "user": { "data": { - "code": "Koda (uporablja se v uporabni\u0161kem vmesniku Home Assistant)", "password": "Geslo", "username": "E-po\u0161tni naslov" - }, - "title": "Izpolnite svoje podatke" + } } } }, diff --git a/homeassistant/components/simplisafe/translations/sv.json b/homeassistant/components/simplisafe/translations/sv.json index 760d092e113..2b2d675f2b5 100644 --- a/homeassistant/components/simplisafe/translations/sv.json +++ b/homeassistant/components/simplisafe/translations/sv.json @@ -3,10 +3,6 @@ "abort": { "already_configured": "Det h\u00e4r SimpliSafe-kontot har redan konfigurerats." }, - "error": { - "2fa_timed_out": "Tidsgr\u00e4nsen tog slut i v\u00e4ntan p\u00e5 tv\u00e5faktorsautentisering", - "identifier_exists": "Kontot \u00e4r redan registrerat" - }, "progress": { "email_2fa": "Kontrollera din e-post f\u00f6r en verifieringsl\u00e4nk fr\u00e5n Simplisafe." }, @@ -21,8 +17,7 @@ "data": { "password": "L\u00f6senord", "username": "E-postadress" - }, - "title": "Fyll i din information" + } } } } diff --git a/homeassistant/components/simplisafe/translations/tr.json b/homeassistant/components/simplisafe/translations/tr.json index 156b3575756..74c260e2d3d 100644 --- a/homeassistant/components/simplisafe/translations/tr.json +++ b/homeassistant/components/simplisafe/translations/tr.json @@ -3,23 +3,16 @@ "abort": { "already_configured": "Bu SimpliSafe hesab\u0131 zaten kullan\u0131mda.", "email_2fa_timed_out": "E-posta tabanl\u0131 iki fakt\u00f6rl\u00fc kimlik do\u011frulama i\u00e7in beklerken zaman a\u015f\u0131m\u0131na u\u011frad\u0131.", - "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", - "wrong_account": "Sa\u011flanan kullan\u0131c\u0131 kimlik bilgileri bu SimpliSafe hesab\u0131yla e\u015fle\u015fmiyor." + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { - "2fa_timed_out": "\u0130ki fakt\u00f6rl\u00fc kimlik do\u011frulama i\u00e7in beklerken zaman a\u015f\u0131m\u0131na u\u011frad\u0131", - "identifier_exists": "Hesap zaten kay\u0131tl\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "still_awaiting_mfa": "Hala MFA e-posta t\u0131klamas\u0131 bekleniyor", "unknown": "Beklenmeyen hata" }, "progress": { "email_2fa": "Simplisafe'den bir do\u011frulama ba\u011flant\u0131s\u0131 i\u00e7in e-postan\u0131z\u0131 kontrol edin." }, "step": { - "mfa": { - "title": "SimpliSafe \u00c7ok Fakt\u00f6rl\u00fc Kimlik Do\u011frulama" - }, "reauth_confirm": { "data": { "password": "Parola" @@ -35,13 +28,10 @@ }, "user": { "data": { - "auth_code": "Yetkilendirme Kodu", - "code": "Kod (Home Assistant kullan\u0131c\u0131 aray\u00fcz\u00fcnde kullan\u0131l\u0131r)", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "Kullan\u0131c\u0131 ad\u0131n\u0131z\u0131 ve \u015fifrenizi girin.", - "title": "Bilgilerinizi doldurun." + "description": "Kullan\u0131c\u0131 ad\u0131n\u0131z\u0131 ve \u015fifrenizi girin." } } }, diff --git a/homeassistant/components/simplisafe/translations/uk.json b/homeassistant/components/simplisafe/translations/uk.json index 8ca29743c5b..76e0fb397cb 100644 --- a/homeassistant/components/simplisafe/translations/uk.json +++ b/homeassistant/components/simplisafe/translations/uk.json @@ -5,15 +5,10 @@ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" }, "error": { - "identifier_exists": "\u041e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043e\u0432\u0430\u043d\u043e.", "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", - "still_awaiting_mfa": "\u041e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f, \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u043e \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0456\u0439 \u043f\u043e\u0448\u0442\u0456.", "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { - "mfa": { - "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f SimpliSafe" - }, "reauth_confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" @@ -23,11 +18,9 @@ }, "user": { "data": { - "code": "\u041a\u043e\u0434 (\u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 Home Assistant)", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" - }, - "title": "\u0417\u0430\u043f\u043e\u0432\u043d\u0456\u0442\u044c \u0432\u0430\u0448\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e" + } } } }, diff --git a/homeassistant/components/simplisafe/translations/zh-Hans.json b/homeassistant/components/simplisafe/translations/zh-Hans.json index 134eee33bfe..d28951425ec 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hans.json +++ b/homeassistant/components/simplisafe/translations/zh-Hans.json @@ -1,17 +1,14 @@ { "config": { "error": { - "identifier_exists": "\u8d26\u6237\u5df2\u6ce8\u518c", "invalid_auth": "\u65e0\u6548\u7684\u8eab\u4efd\u9a8c\u8bc1" }, "step": { "user": { "data": { - "auth_code": "\u6388\u6743\u7801", "password": "\u5bc6\u7801", "username": "\u7535\u5b50\u90ae\u4ef6\u5730\u5740" - }, - "title": "\u586b\u5199\u60a8\u7684\u4fe1\u606f" + } } } } diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json index e3cc230962c..ce763da538e 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/translations/zh-Hant.json @@ -3,23 +3,16 @@ "abort": { "already_configured": "\u6b64 SimpliSafe \u5e33\u865f\u5df2\u88ab\u4f7f\u7528\u3002", "email_2fa_timed_out": "\u7b49\u5f85\u5169\u6b65\u9a5f\u9a57\u8b49\u78bc\u90f5\u4ef6\u903e\u6642\u3002", - "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", - "wrong_account": "\u6240\u4ee5\u63d0\u4f9b\u7684\u6191\u8b49\u8207 Simplisafe \u5e33\u865f\u4e0d\u7b26\u3002" + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { - "2fa_timed_out": "\u7b49\u5f85\u5169\u6b65\u9a5f\u9a57\u8b49\u78bc\u903e\u6642", - "identifier_exists": "\u5e33\u865f\u5df2\u8a3b\u518a", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "still_awaiting_mfa": "\u4ecd\u5728\u7b49\u5019\u9ede\u64ca\u591a\u6b65\u9a5f\u8a8d\u8b49\u90f5\u4ef6", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "progress": { "email_2fa": "\u8f38\u5165\u90f5\u4ef6\u6240\u6536\u5230 \u7684Simplisafe \u9a57\u8b49\u9023\u7d50\u3002" }, "step": { - "mfa": { - "title": "SimpliSafe \u591a\u6b65\u9a5f\u9a57\u8b49" - }, "reauth_confirm": { "data": { "password": "\u5bc6\u78bc" @@ -35,13 +28,10 @@ }, "user": { "data": { - "auth_code": "\u8a8d\u8b49\u78bc", - "code": "\u9a57\u8b49\u78bc\uff08\u4f7f\u7528\u65bc Home Assistant UI\uff09", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8f38\u5165\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002", - "title": "\u586b\u5beb\u8cc7\u8a0a\u3002" + "description": "\u8f38\u5165\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002" } } }, diff --git a/homeassistant/components/siren/translations/cs.json b/homeassistant/components/siren/translations/cs.json new file mode 100644 index 00000000000..50303faf105 --- /dev/null +++ b/homeassistant/components/siren/translations/cs.json @@ -0,0 +1,3 @@ +{ + "title": "Sir\u00e9na" +} \ No newline at end of file diff --git a/homeassistant/components/siren/translations/no.json b/homeassistant/components/siren/translations/no.json new file mode 100644 index 00000000000..bcea16c56f1 --- /dev/null +++ b/homeassistant/components/siren/translations/no.json @@ -0,0 +1,3 @@ +{ + "title": "Sirene" +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/cs.json b/homeassistant/components/slack/translations/cs.json new file mode 100644 index 00000000000..a4b5fb71cbe --- /dev/null +++ b/homeassistant/components/slack/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena" + }, + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "icon": "Ikona", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/he.json b/homeassistant/components/slack/translations/he.json similarity index 52% rename from homeassistant/components/climacell/translations/he.json rename to homeassistant/components/slack/translations/he.json index b663a5e0a0f..b0201cf2445 100644 --- a/homeassistant/components/climacell/translations/he.json +++ b/homeassistant/components/slack/translations/he.json @@ -1,18 +1,19 @@ { "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8" + }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "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", - "api_version": "\u05d2\u05e8\u05e1\u05ea API", - "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", - "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", - "name": "\u05e9\u05dd" + "icon": "\u05e1\u05de\u05dc\u05d9\u05dc", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } } } diff --git a/homeassistant/components/slack/translations/id.json b/homeassistant/components/slack/translations/id.json index 3180294b97e..3897704b540 100644 --- a/homeassistant/components/slack/translations/id.json +++ b/homeassistant/components/slack/translations/id.json @@ -12,11 +12,15 @@ "user": { "data": { "api_key": "Kunci API", + "default_channel": "Saluran Baku", "icon": "Ikon", "username": "Nama Pengguna" }, "data_description": { - "api_key": "Token API Slack yang digunakan untuk mengirim pesan Slack." + "api_key": "Token API Slack yang digunakan untuk mengirim pesan Slack.", + "default_channel": "Saluran tujuan posting jika tidak ada saluran yang ditentukan saat mengirim pesan.", + "icon": "Gunakan salah satu emoji Slack sebagai Ikon untuk nama pengguna yang disediakan.", + "username": "Home Assistant akan memposting ke Slack menggunakan nama pengguna yang ditentukan." }, "description": "Lihat dokumentasi tentang mendapatkan kunci API Slack Anda." } diff --git a/homeassistant/components/smhi/translations/bg.json b/homeassistant/components/smhi/translations/bg.json index bcd30370ad4..d1dfdde7a32 100644 --- a/homeassistant/components/smhi/translations/bg.json +++ b/homeassistant/components/smhi/translations/bg.json @@ -4,15 +4,13 @@ "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": { - "name_exists": "\u0418\u043c\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430", "wrong_location": "\u041f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u0442 \u0441\u0435 \u0441\u0430\u043c\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0432 \u0428\u0432\u0435\u0446\u0438\u044f" }, "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" + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430" }, "title": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0432 \u0428\u0432\u0435\u0446\u0438\u044f" } diff --git a/homeassistant/components/smhi/translations/ca.json b/homeassistant/components/smhi/translations/ca.json index 4755e35f2ba..c3f53542274 100644 --- a/homeassistant/components/smhi/translations/ca.json +++ b/homeassistant/components/smhi/translations/ca.json @@ -4,15 +4,13 @@ "already_configured": "El compte ja est\u00e0 configurat" }, "error": { - "name_exists": "El nom ja existeix", "wrong_location": "La ubicaci\u00f3 ha d'estar a Su\u00e8cia" }, "step": { "user": { "data": { "latitude": "Latitud", - "longitude": "Longitud", - "name": "Nom" + "longitude": "Longitud" }, "title": "Ubicaci\u00f3 a Su\u00e8cia" } diff --git a/homeassistant/components/smhi/translations/cs.json b/homeassistant/components/smhi/translations/cs.json index 5721edc084b..6d65b6411c9 100644 --- a/homeassistant/components/smhi/translations/cs.json +++ b/homeassistant/components/smhi/translations/cs.json @@ -1,15 +1,13 @@ { "config": { "error": { - "name_exists": "N\u00e1zev ji\u017e existuje", "wrong_location": "Lokalita pouze pro \u0160v\u00e9dsko" }, "step": { "user": { "data": { "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", - "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", - "name": "Jm\u00e9no" + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka" }, "title": "Lokalita ve \u0160v\u00e9dsku" } diff --git a/homeassistant/components/smhi/translations/da.json b/homeassistant/components/smhi/translations/da.json index 979f4b0f7b6..9b07283456b 100644 --- a/homeassistant/components/smhi/translations/da.json +++ b/homeassistant/components/smhi/translations/da.json @@ -1,15 +1,13 @@ { "config": { "error": { - "name_exists": "Navnet findes allerede", "wrong_location": "Lokalitet kun i Sverige" }, "step": { "user": { "data": { "latitude": "Breddegrad", - "longitude": "L\u00e6ngdegrad", - "name": "Navn" + "longitude": "L\u00e6ngdegrad" }, "title": "Lokalitet i Sverige" } diff --git a/homeassistant/components/smhi/translations/de.json b/homeassistant/components/smhi/translations/de.json index 716f9693e96..7434d9d5800 100644 --- a/homeassistant/components/smhi/translations/de.json +++ b/homeassistant/components/smhi/translations/de.json @@ -4,15 +4,13 @@ "already_configured": "Konto wurde bereits konfiguriert" }, "error": { - "name_exists": "Name existiert bereits", "wrong_location": "Standort nur in Schweden" }, "step": { "user": { "data": { "latitude": "Breitengrad", - "longitude": "L\u00e4ngengrad", - "name": "Name" + "longitude": "L\u00e4ngengrad" }, "title": "Standort in Schweden" } diff --git a/homeassistant/components/smhi/translations/el.json b/homeassistant/components/smhi/translations/el.json index c852bd3a08e..736dc0c026a 100644 --- a/homeassistant/components/smhi/translations/el.json +++ b/homeassistant/components/smhi/translations/el.json @@ -4,15 +4,13 @@ "already_configured": "\u039f \u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2" }, "error": { - "name_exists": "\u03a4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7", "wrong_location": "\u03a4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u039c\u03cc\u03bd\u03bf \u03a3\u03bf\u03c5\u03b7\u03b4\u03af\u03b1" }, "step": { "user": { "data": { "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", - "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2" }, "title": "\u03a4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03c3\u03c4\u03b7 \u03a3\u03bf\u03c5\u03b7\u03b4\u03af\u03b1" } diff --git a/homeassistant/components/smhi/translations/en.json b/homeassistant/components/smhi/translations/en.json index 5400122be8a..5c8f4f6ae24 100644 --- a/homeassistant/components/smhi/translations/en.json +++ b/homeassistant/components/smhi/translations/en.json @@ -4,15 +4,13 @@ "already_configured": "Account is already configured" }, "error": { - "name_exists": "Name already exists", "wrong_location": "Location Sweden only" }, "step": { "user": { "data": { "latitude": "Latitude", - "longitude": "Longitude", - "name": "Name" + "longitude": "Longitude" }, "title": "Location in Sweden" } diff --git a/homeassistant/components/smhi/translations/es-419.json b/homeassistant/components/smhi/translations/es-419.json index 3db65a34ad9..c73d574c5be 100644 --- a/homeassistant/components/smhi/translations/es-419.json +++ b/homeassistant/components/smhi/translations/es-419.json @@ -1,15 +1,13 @@ { "config": { "error": { - "name_exists": "El nombre ya existe", "wrong_location": "Ubicaci\u00f3n Suecia solamente" }, "step": { "user": { "data": { "latitude": "Latitud", - "longitude": "Longitud", - "name": "Nombre" + "longitude": "Longitud" }, "title": "Ubicaci\u00f3n en Suecia" } diff --git a/homeassistant/components/smhi/translations/es.json b/homeassistant/components/smhi/translations/es.json index ab34438b12d..430f04d59fc 100644 --- a/homeassistant/components/smhi/translations/es.json +++ b/homeassistant/components/smhi/translations/es.json @@ -4,15 +4,13 @@ "already_configured": "La cuenta ya est\u00e1 configurada" }, "error": { - "name_exists": "Nombre ya existe", "wrong_location": "Ubicaci\u00f3n Suecia solamente" }, "step": { "user": { "data": { "latitude": "Latitud", - "longitude": "Longitud", - "name": "Nombre" + "longitude": "Longitud" }, "title": "Ubicaci\u00f3n en Suecia" } diff --git a/homeassistant/components/smhi/translations/et.json b/homeassistant/components/smhi/translations/et.json index a32f38e28ca..d07ec7d3391 100644 --- a/homeassistant/components/smhi/translations/et.json +++ b/homeassistant/components/smhi/translations/et.json @@ -4,15 +4,13 @@ "already_configured": "Konto on juba h\u00e4\u00e4lestatud" }, "error": { - "name_exists": "Nimi on juba olemas", "wrong_location": "Asukoht saab olla ainult Rootsis" }, "step": { "user": { "data": { "latitude": "Laiuskraad", - "longitude": "Pikkuskraad", - "name": "Nimi" + "longitude": "Pikkuskraad" }, "title": "Asukoht Rootsis" } diff --git a/homeassistant/components/smhi/translations/fi.json b/homeassistant/components/smhi/translations/fi.json index 2a05c47dcbc..cc5377e3654 100644 --- a/homeassistant/components/smhi/translations/fi.json +++ b/homeassistant/components/smhi/translations/fi.json @@ -1,15 +1,13 @@ { "config": { "error": { - "name_exists": "Nimi on jo olemassa", "wrong_location": "Sijainti vain Ruotsi" }, "step": { "user": { "data": { "latitude": "Leveysaste", - "longitude": "Pituusaste", - "name": "Nimi" + "longitude": "Pituusaste" }, "title": "Sijainti Ruotsissa" } diff --git a/homeassistant/components/smhi/translations/fr.json b/homeassistant/components/smhi/translations/fr.json index 6bf27915f97..84d5c6cfa7c 100644 --- a/homeassistant/components/smhi/translations/fr.json +++ b/homeassistant/components/smhi/translations/fr.json @@ -4,15 +4,13 @@ "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9", "wrong_location": "En Su\u00e8de uniquement" }, "step": { "user": { "data": { "latitude": "Latitude", - "longitude": "Longitude", - "name": "Nom" + "longitude": "Longitude" }, "title": "Localisation en Su\u00e8de" } diff --git a/homeassistant/components/smhi/translations/he.json b/homeassistant/components/smhi/translations/he.json index cb8ff44626c..622d75d6c6c 100644 --- a/homeassistant/components/smhi/translations/he.json +++ b/homeassistant/components/smhi/translations/he.json @@ -7,8 +7,7 @@ "user": { "data": { "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", - "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", - "name": "\u05e9\u05dd" + "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da" } } } diff --git a/homeassistant/components/smhi/translations/hu.json b/homeassistant/components/smhi/translations/hu.json index e83ea03a193..22307044da2 100644 --- a/homeassistant/components/smhi/translations/hu.json +++ b/homeassistant/components/smhi/translations/hu.json @@ -4,15 +4,13 @@ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" }, "error": { - "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik", "wrong_location": "Csak sv\u00e9dorsz\u00e1gi helysz\u00edn megengedett" }, "step": { "user": { "data": { "latitude": "Sz\u00e9less\u00e9g", - "longitude": "Hossz\u00fas\u00e1g", - "name": "Elnevez\u00e9s" + "longitude": "Hossz\u00fas\u00e1g" }, "title": "Helysz\u00edn Sv\u00e9dorsz\u00e1gban" } diff --git a/homeassistant/components/smhi/translations/id.json b/homeassistant/components/smhi/translations/id.json index 842580dac79..b1b9450dc05 100644 --- a/homeassistant/components/smhi/translations/id.json +++ b/homeassistant/components/smhi/translations/id.json @@ -4,15 +4,13 @@ "already_configured": "Akun sudah dikonfigurasi" }, "error": { - "name_exists": "Nama sudah ada", "wrong_location": "Hanya untuk lokasi di Swedia" }, "step": { "user": { "data": { "latitude": "Lintang", - "longitude": "Bujur", - "name": "Nama" + "longitude": "Bujur" }, "title": "Lokasi di Swedia" } diff --git a/homeassistant/components/smhi/translations/it.json b/homeassistant/components/smhi/translations/it.json index 3df53d13ec1..830107967dd 100644 --- a/homeassistant/components/smhi/translations/it.json +++ b/homeassistant/components/smhi/translations/it.json @@ -4,15 +4,13 @@ "already_configured": "L'account \u00e8 gi\u00e0 configurato" }, "error": { - "name_exists": "Il nome \u00e8 gi\u00e0 esistente", "wrong_location": "Localit\u00e0 solamente della Svezia" }, "step": { "user": { "data": { "latitude": "Latitudine", - "longitude": "Logitudine", - "name": "Nome" + "longitude": "Logitudine" }, "title": "Localit\u00e0 in Svezia" } diff --git a/homeassistant/components/smhi/translations/ja.json b/homeassistant/components/smhi/translations/ja.json index 38e4ccad389..390732d7db4 100644 --- a/homeassistant/components/smhi/translations/ja.json +++ b/homeassistant/components/smhi/translations/ja.json @@ -4,15 +4,13 @@ "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "name_exists": "\u540d\u524d\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059", "wrong_location": "\u6240\u5728\u5730 \u30b9\u30a6\u30a7\u30fc\u30c7\u30f3\u306e\u307f" }, "step": { "user": { "data": { "latitude": "\u7def\u5ea6", - "longitude": "\u7d4c\u5ea6", - "name": "\u540d\u524d" + "longitude": "\u7d4c\u5ea6" }, "title": "\u30b9\u30a6\u30a7\u30fc\u30c7\u30f3\u3067\u306e\u4f4d\u7f6e" } diff --git a/homeassistant/components/smhi/translations/ko.json b/homeassistant/components/smhi/translations/ko.json index dac6c9018b2..6f1cbeda859 100644 --- a/homeassistant/components/smhi/translations/ko.json +++ b/homeassistant/components/smhi/translations/ko.json @@ -1,15 +1,13 @@ { "config": { "error": { - "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4", "wrong_location": "\uc2a4\uc6e8\ub374 \uc9c0\uc5ed \uc804\uc6a9\uc785\ub2c8\ub2e4" }, "step": { "user": { "data": { "latitude": "\uc704\ub3c4", - "longitude": "\uacbd\ub3c4", - "name": "\uc774\ub984" + "longitude": "\uacbd\ub3c4" }, "title": "\uc2a4\uc6e8\ub374 \uc9c0\uc5ed \uc704\uce58" } diff --git a/homeassistant/components/smhi/translations/lb.json b/homeassistant/components/smhi/translations/lb.json index 9444569c4a1..9abb503923d 100644 --- a/homeassistant/components/smhi/translations/lb.json +++ b/homeassistant/components/smhi/translations/lb.json @@ -1,15 +1,13 @@ { "config": { "error": { - "name_exists": "Numm g\u00ebtt et schonn", "wrong_location": "N\u00ebmmen Uertschaften an Schweden" }, "step": { "user": { "data": { "latitude": "Breedegrad", - "longitude": "L\u00e4ngegrad", - "name": "Numm" + "longitude": "L\u00e4ngegrad" }, "title": "Uertschaft an Schweden" } diff --git a/homeassistant/components/smhi/translations/nl.json b/homeassistant/components/smhi/translations/nl.json index 597e106ab7e..abf19b47ef9 100644 --- a/homeassistant/components/smhi/translations/nl.json +++ b/homeassistant/components/smhi/translations/nl.json @@ -4,15 +4,13 @@ "already_configured": "Account is al geconfigureerd" }, "error": { - "name_exists": "Naam bestaat al", "wrong_location": "Locatie alleen Zweden" }, "step": { "user": { "data": { "latitude": "Breedtegraad", - "longitude": "Lengtegraad", - "name": "Naam" + "longitude": "Lengtegraad" }, "title": "Locatie in Zweden" } diff --git a/homeassistant/components/smhi/translations/no.json b/homeassistant/components/smhi/translations/no.json index 914baaca9ce..b2ec9e8732a 100644 --- a/homeassistant/components/smhi/translations/no.json +++ b/homeassistant/components/smhi/translations/no.json @@ -4,15 +4,13 @@ "already_configured": "Kontoen er allerede konfigurert" }, "error": { - "name_exists": "Navnet eksisterer allerede", "wrong_location": "Bare plassering i Sverige" }, "step": { "user": { "data": { "latitude": "Breddegrad", - "longitude": "Lengdegrad", - "name": "Navn" + "longitude": "Lengdegrad" }, "title": "Plassering i Sverige" } diff --git a/homeassistant/components/smhi/translations/pl.json b/homeassistant/components/smhi/translations/pl.json index 18e9156a936..65fc02e0c7c 100644 --- a/homeassistant/components/smhi/translations/pl.json +++ b/homeassistant/components/smhi/translations/pl.json @@ -4,15 +4,13 @@ "already_configured": "Konto jest ju\u017c skonfigurowane" }, "error": { - "name_exists": "Nazwa ju\u017c istnieje", "wrong_location": "Lokalizacja w Szwecji" }, "step": { "user": { "data": { "latitude": "Szeroko\u015b\u0107 geograficzna", - "longitude": "D\u0142ugo\u015b\u0107 geograficzna", - "name": "Nazwa" + "longitude": "D\u0142ugo\u015b\u0107 geograficzna" }, "title": "Lokalizacja w Szwecji" } diff --git a/homeassistant/components/smhi/translations/pt-BR.json b/homeassistant/components/smhi/translations/pt-BR.json index 235008c7c31..2f335a58aa6 100644 --- a/homeassistant/components/smhi/translations/pt-BR.json +++ b/homeassistant/components/smhi/translations/pt-BR.json @@ -4,15 +4,13 @@ "already_configured": "A conta j\u00e1 foi configurada" }, "error": { - "name_exists": "O nome j\u00e1 existe", "wrong_location": "Localiza\u00e7\u00e3o apenas na Su\u00e9cia" }, "step": { "user": { "data": { "latitude": "Latitude", - "longitude": "Longitude", - "name": "Nome" + "longitude": "Longitude" }, "title": "Localiza\u00e7\u00e3o na Su\u00e9cia" } diff --git a/homeassistant/components/smhi/translations/pt.json b/homeassistant/components/smhi/translations/pt.json index c23a33b6e19..d5cd5e83a13 100644 --- a/homeassistant/components/smhi/translations/pt.json +++ b/homeassistant/components/smhi/translations/pt.json @@ -1,15 +1,13 @@ { "config": { "error": { - "name_exists": "Nome j\u00e1 existe", "wrong_location": "Localiza\u00e7\u00e3o apenas na Su\u00e9cia" }, "step": { "user": { "data": { "latitude": "Latitude", - "longitude": "Longitude", - "name": "Nome" + "longitude": "Longitude" }, "title": "Localiza\u00e7\u00e3o na Su\u00e9cia" } diff --git a/homeassistant/components/smhi/translations/ro.json b/homeassistant/components/smhi/translations/ro.json index 0261289fd31..f1ad756f798 100644 --- a/homeassistant/components/smhi/translations/ro.json +++ b/homeassistant/components/smhi/translations/ro.json @@ -1,15 +1,13 @@ { "config": { "error": { - "name_exists": "Numele exist\u0103 deja", "wrong_location": "Loca\u021bia numai \u00een Suedia" }, "step": { "user": { "data": { "latitude": "Latitudine", - "longitude": "Longitudine", - "name": "Nume" + "longitude": "Longitudine" }, "title": "Loca\u021bie \u00een Suedia" } diff --git a/homeassistant/components/smhi/translations/ru.json b/homeassistant/components/smhi/translations/ru.json index b771dfcf961..41e009b85d3 100644 --- a/homeassistant/components/smhi/translations/ru.json +++ b/homeassistant/components/smhi/translations/ru.json @@ -4,15 +4,13 @@ "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": { - "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", "wrong_location": "\u0422\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0438\u0438." }, "step": { "user": { "data": { "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", - "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430" }, "title": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0432 \u0428\u0432\u0435\u0446\u0438\u0438" } diff --git a/homeassistant/components/smhi/translations/sk.json b/homeassistant/components/smhi/translations/sk.json index 81532ef4801..e6945904d90 100644 --- a/homeassistant/components/smhi/translations/sk.json +++ b/homeassistant/components/smhi/translations/sk.json @@ -4,8 +4,7 @@ "user": { "data": { "latitude": "Zemepisn\u00e1 \u0161\u00edrka", - "longitude": "Zemepisn\u00e1 d\u013a\u017eka", - "name": "N\u00e1zov" + "longitude": "Zemepisn\u00e1 d\u013a\u017eka" } } } diff --git a/homeassistant/components/smhi/translations/sl.json b/homeassistant/components/smhi/translations/sl.json index 91fe7d8375f..07e249a2245 100644 --- a/homeassistant/components/smhi/translations/sl.json +++ b/homeassistant/components/smhi/translations/sl.json @@ -1,15 +1,13 @@ { "config": { "error": { - "name_exists": "Ime \u017ee obstaja", "wrong_location": "Lokacija le na \u0160vedskem" }, "step": { "user": { "data": { "latitude": "Zemljepisna \u0161irina", - "longitude": "Zemljepisna dol\u017eina", - "name": "Ime" + "longitude": "Zemljepisna dol\u017eina" }, "title": "Lokacija na \u0160vedskem" } diff --git a/homeassistant/components/smhi/translations/sv.json b/homeassistant/components/smhi/translations/sv.json index b6afc479b70..0bb597e393b 100644 --- a/homeassistant/components/smhi/translations/sv.json +++ b/homeassistant/components/smhi/translations/sv.json @@ -1,15 +1,13 @@ { "config": { "error": { - "name_exists": "Namnet finns redan", "wrong_location": "Plats i Sverige endast" }, "step": { "user": { "data": { "latitude": "Latitud", - "longitude": "Longitud", - "name": "Namn" + "longitude": "Longitud" }, "title": "Plats i Sverige" } diff --git a/homeassistant/components/smhi/translations/th.json b/homeassistant/components/smhi/translations/th.json index 0c08363fca6..85494a9dc6b 100644 --- a/homeassistant/components/smhi/translations/th.json +++ b/homeassistant/components/smhi/translations/th.json @@ -4,8 +4,7 @@ "user": { "data": { "latitude": "\u0e25\u0e30\u0e15\u0e34\u0e08\u0e39\u0e14", - "longitude": "\u0e25\u0e2d\u0e07\u0e08\u0e34\u0e08\u0e39\u0e14", - "name": "\u0e0a\u0e37\u0e48\u0e2d" + "longitude": "\u0e25\u0e2d\u0e07\u0e08\u0e34\u0e08\u0e39\u0e14" } } } diff --git a/homeassistant/components/smhi/translations/tr.json b/homeassistant/components/smhi/translations/tr.json index 206573ae69b..613ac6e691e 100644 --- a/homeassistant/components/smhi/translations/tr.json +++ b/homeassistant/components/smhi/translations/tr.json @@ -4,15 +4,13 @@ "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { - "name_exists": "Bu ad zaten var", "wrong_location": "Konum sadece \u0130sve\u00e7" }, "step": { "user": { "data": { "latitude": "Enlem", - "longitude": "Boylam", - "name": "Ad" + "longitude": "Boylam" }, "title": "\u0130sve\u00e7'teki konum" } diff --git a/homeassistant/components/smhi/translations/uk.json b/homeassistant/components/smhi/translations/uk.json index 24af32172ba..e7de5db5d1b 100644 --- a/homeassistant/components/smhi/translations/uk.json +++ b/homeassistant/components/smhi/translations/uk.json @@ -1,15 +1,13 @@ { "config": { "error": { - "name_exists": "\u0426\u044f \u043d\u0430\u0437\u0432\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f.", "wrong_location": "\u0422\u0456\u043b\u044c\u043a\u0438 \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0456\u0457." }, "step": { "user": { "data": { "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", - "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", - "name": "\u041d\u0430\u0437\u0432\u0430" + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430" }, "title": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432 \u0428\u0432\u0435\u0446\u0456\u0457" } diff --git a/homeassistant/components/smhi/translations/zh-Hans.json b/homeassistant/components/smhi/translations/zh-Hans.json index b2ddc3aa1d7..64421e462d3 100644 --- a/homeassistant/components/smhi/translations/zh-Hans.json +++ b/homeassistant/components/smhi/translations/zh-Hans.json @@ -1,15 +1,13 @@ { "config": { "error": { - "name_exists": "\u540d\u79f0\u5df2\u5b58\u5728", "wrong_location": "\u4ec5\u9650\u745e\u5178\u7684\u4f4d\u7f6e" }, "step": { "user": { "data": { "latitude": "\u7eac\u5ea6", - "longitude": "\u7ecf\u5ea6", - "name": "\u540d\u79f0" + "longitude": "\u7ecf\u5ea6" }, "title": "\u5728\u745e\u5178\u7684\u4f4d\u7f6e" } diff --git a/homeassistant/components/smhi/translations/zh-Hant.json b/homeassistant/components/smhi/translations/zh-Hant.json index f5d993ab6c9..8b68a30a4b2 100644 --- a/homeassistant/components/smhi/translations/zh-Hant.json +++ b/homeassistant/components/smhi/translations/zh-Hant.json @@ -4,15 +4,13 @@ "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728", "wrong_location": "\u50c5\u9650\u745e\u5178\u5ea7\u6a19" }, "step": { "user": { "data": { "latitude": "\u7def\u5ea6", - "longitude": "\u7d93\u5ea6", - "name": "\u540d\u7a31" + "longitude": "\u7d93\u5ea6" }, "title": "\u745e\u5178\u5ea7\u6a19" } diff --git a/homeassistant/components/somfy_mylink/translations/ca.json b/homeassistant/components/somfy_mylink/translations/ca.json index a9181aa4605..d25ba195027 100644 --- a/homeassistant/components/somfy_mylink/translations/ca.json +++ b/homeassistant/components/somfy_mylink/translations/ca.json @@ -25,17 +25,8 @@ "cannot_connect": "Ha fallat la connexi\u00f3" }, "step": { - "entity_config": { - "data": { - "reverse": "La coberta est\u00e0 invertida" - }, - "description": "Opcions de configuraci\u00f3 de `{entity_id}`", - "title": "Configura l'entitat" - }, "init": { "data": { - "default_reverse": "Estat d'inversi\u00f3 predeterminat per a cobertes sense configurar", - "entity_id": "Configura una entitat espec\u00edfica.", "target_id": "Opcions de configuraci\u00f3 de la coberta." }, "title": "Configura opcions de MyLink" @@ -48,6 +39,5 @@ "title": "Configura coberta MyLink" } } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/de.json b/homeassistant/components/somfy_mylink/translations/de.json index 9fc1af6d92c..303337542f0 100644 --- a/homeassistant/components/somfy_mylink/translations/de.json +++ b/homeassistant/components/somfy_mylink/translations/de.json @@ -25,17 +25,8 @@ "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { - "entity_config": { - "data": { - "reverse": "Jalousie ist invertiert" - }, - "description": "Optionen f\u00fcr `{entity_id}` konfigurieren", - "title": "Entit\u00e4t konfigurieren" - }, "init": { "data": { - "default_reverse": "Standardinvertierungsstatus f\u00fcr nicht konfigurierte Abdeckungen", - "entity_id": "Konfiguriere eine bestimmte Entit\u00e4t.", "target_id": "Konfigurieren der Optionen f\u00fcr eine Jalousie." }, "title": "MyLink-Optionen konfigurieren" @@ -48,6 +39,5 @@ "title": "MyLink-Cover konfigurieren" } } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/el.json b/homeassistant/components/somfy_mylink/translations/el.json index 59ffb563687..565f8459a3f 100644 --- a/homeassistant/components/somfy_mylink/translations/el.json +++ b/homeassistant/components/somfy_mylink/translations/el.json @@ -25,17 +25,8 @@ "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "step": { - "entity_config": { - "data": { - "reverse": "\u03a4\u03bf \u03ba\u03ac\u03bb\u03c5\u03bc\u03bc\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03bd\u03c4\u03b5\u03c3\u03c4\u03c1\u03b1\u03bc\u03bc\u03ad\u03bd\u03bf" - }, - "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd \u03b3\u03b9\u03b1 `{entity_id}`", - "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" - }, "init": { "data": { - "default_reverse": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b1\u03bd\u03c4\u03b9\u03c3\u03c4\u03c1\u03bf\u03c6\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03bc\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03b1 \u03ba\u03b1\u03bb\u03cd\u03bc\u03bc\u03b1\u03c4\u03b1", - "entity_id": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bc\u03b9\u03b1\u03c2 \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03b7\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2.", "target_id": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd \u03b3\u03b9\u03b1 \u03ad\u03bd\u03b1 \u03ba\u03ac\u03bb\u03c5\u03bc\u03bc\u03b1." }, "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd MyLink" @@ -48,6 +39,5 @@ "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 MyLink Cover" } } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/en.json b/homeassistant/components/somfy_mylink/translations/en.json index cb373008778..e646bf9abcf 100644 --- a/homeassistant/components/somfy_mylink/translations/en.json +++ b/homeassistant/components/somfy_mylink/translations/en.json @@ -25,17 +25,8 @@ "cannot_connect": "Failed to connect" }, "step": { - "entity_config": { - "data": { - "reverse": "Cover is reversed" - }, - "description": "Configure options for `{entity_id}`", - "title": "Configure Entity" - }, "init": { "data": { - "default_reverse": "Default reversal status for unconfigured covers", - "entity_id": "Configure a specific entity.", "target_id": "Configure options for a cover." }, "title": "Configure MyLink Options" @@ -48,6 +39,5 @@ "title": "Configure MyLink Cover" } } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/es.json b/homeassistant/components/somfy_mylink/translations/es.json index 40d82a4522a..8647f7bb8fd 100644 --- a/homeassistant/components/somfy_mylink/translations/es.json +++ b/homeassistant/components/somfy_mylink/translations/es.json @@ -25,17 +25,8 @@ "cannot_connect": "No se pudo conectar" }, "step": { - "entity_config": { - "data": { - "reverse": "La cubierta est\u00e1 invertida" - }, - "description": "Configurar opciones para `{entity_id}`", - "title": "Configurar entidad" - }, "init": { "data": { - "default_reverse": "Estado de inversi\u00f3n predeterminado para cubiertas no configuradas", - "entity_id": "Configurar una entidad espec\u00edfica.", "target_id": "Configurar opciones para una cubierta." }, "title": "Configurar opciones de MyLink" @@ -48,6 +39,5 @@ "title": "Configurar la cubierta MyLink" } } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/et.json b/homeassistant/components/somfy_mylink/translations/et.json index 17122eb449e..f3d2e4c4a9a 100644 --- a/homeassistant/components/somfy_mylink/translations/et.json +++ b/homeassistant/components/somfy_mylink/translations/et.json @@ -25,17 +25,8 @@ "cannot_connect": "\u00dchendamine nurjus" }, "step": { - "entity_config": { - "data": { - "reverse": "(Akna)kate t\u00f6\u00f6tab vastupidi" - }, - "description": "Olemi {entity_id} suvandite seadmine", - "title": "Seadista olem" - }, "init": { "data": { - "default_reverse": "Seadistamata (akna)katete vaikep\u00f6\u00f6rduse olek", - "entity_id": "Seadista konkreetne olem.", "target_id": "Seadista (akna)katte suvandid" }, "title": "Seadista MyLinki suvandid" @@ -48,6 +39,5 @@ "title": "Seadista MyLink Cover" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/fr.json b/homeassistant/components/somfy_mylink/translations/fr.json index cc257e16451..8dc822976fe 100644 --- a/homeassistant/components/somfy_mylink/translations/fr.json +++ b/homeassistant/components/somfy_mylink/translations/fr.json @@ -25,17 +25,8 @@ "cannot_connect": "\u00c9chec de connexion" }, "step": { - "entity_config": { - "data": { - "reverse": "La couverture est invers\u00e9e" - }, - "description": "Configurer les options pour \u00ab {entity_id} \u00bb", - "title": "Configurez une entit\u00e9 sp\u00e9cifique" - }, "init": { "data": { - "default_reverse": "Statut d'inversion par d\u00e9faut pour les couvertures non configur\u00e9es", - "entity_id": "Configurez une entit\u00e9 sp\u00e9cifique.", "target_id": "Configurez les options pour la couverture." }, "title": "Configurer les options MyLink" @@ -48,6 +39,5 @@ "title": "Configurer la couverture MyLink" } } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/hu.json b/homeassistant/components/somfy_mylink/translations/hu.json index 2b91ccd8676..c3348b1f628 100644 --- a/homeassistant/components/somfy_mylink/translations/hu.json +++ b/homeassistant/components/somfy_mylink/translations/hu.json @@ -25,17 +25,8 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s" }, "step": { - "entity_config": { - "data": { - "reverse": "A bor\u00edt\u00f3 megfordult" - }, - "description": "Konfigur\u00e1lja az \u201e {entity_id} \u201d be\u00e1ll\u00edt\u00e1sait", - "title": "Entit\u00e1s konfigur\u00e1l\u00e1sa" - }, "init": { "data": { - "default_reverse": "A konfigur\u00e1latlan bor\u00edt\u00f3k alap\u00e9rtelmezett megford\u00edt\u00e1si \u00e1llapota", - "entity_id": "Konfigur\u00e1ljon egy adott entit\u00e1st.", "target_id": "Az \u00e1rny\u00e9kol\u00f3 be\u00e1ll\u00edt\u00e1sainak konfigur\u00e1l\u00e1sa." }, "title": "Mylink be\u00e1ll\u00edt\u00e1sok konfigur\u00e1l\u00e1sa" @@ -48,6 +39,5 @@ "title": "MyLink \u00e1rny\u00e9kol\u00f3 konfigur\u00e1l\u00e1sa" } } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/id.json b/homeassistant/components/somfy_mylink/translations/id.json index c4b2269ef2c..38d0d550bf4 100644 --- a/homeassistant/components/somfy_mylink/translations/id.json +++ b/homeassistant/components/somfy_mylink/translations/id.json @@ -25,17 +25,8 @@ "cannot_connect": "Gagal terhubung" }, "step": { - "entity_config": { - "data": { - "reverse": "Penutup dibalik" - }, - "description": "Konfigurasikan opsi untuk `{entity_id}`", - "title": "Konfigurasikan Entitas" - }, "init": { "data": { - "default_reverse": "Status pembalikan baku untuk penutup yang belum dikonfigurasi", - "entity_id": "Konfigurasikan entitas tertentu.", "target_id": "Konfigurasikan opsi untuk penutup." }, "title": "Konfigurasikan Opsi MyLink" @@ -48,6 +39,5 @@ "title": "Konfigurasikan Cover MyLink" } } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/it.json b/homeassistant/components/somfy_mylink/translations/it.json index cfd3041d248..d4fcb7513c4 100644 --- a/homeassistant/components/somfy_mylink/translations/it.json +++ b/homeassistant/components/somfy_mylink/translations/it.json @@ -25,17 +25,8 @@ "cannot_connect": "Impossibile connettersi" }, "step": { - "entity_config": { - "data": { - "reverse": "La serranda \u00e8 invertita" - }, - "description": "Configura le opzioni per `{entity_id}`", - "title": "Configura entit\u00e0" - }, "init": { "data": { - "default_reverse": "Stato d'inversione predefinito per le serrande non configurate", - "entity_id": "Configura un'entit\u00e0 specifica.", "target_id": "Configura opzioni per una tapparella" }, "title": "Configura le opzioni MyLink" @@ -48,6 +39,5 @@ "title": "Configura serranda MyLink" } } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/ja.json b/homeassistant/components/somfy_mylink/translations/ja.json index 4482dd37db2..49f819659b4 100644 --- a/homeassistant/components/somfy_mylink/translations/ja.json +++ b/homeassistant/components/somfy_mylink/translations/ja.json @@ -25,17 +25,8 @@ "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { - "entity_config": { - "data": { - "reverse": "\u30ab\u30d0\u30fc\u304c\u9006\u306b\u306a\u3063\u3066\u3044\u307e\u3059" - }, - "description": "{entity_id}} \u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a", - "title": "\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u306e\u8a2d\u5b9a" - }, "init": { "data": { - "default_reverse": "\u672a\u8a2d\u5b9a\u306e\u30ab\u30d0\u30fc\u306e\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u53cd\u8ee2\u72b6\u614b", - "entity_id": "\u7279\u5b9a\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002", "target_id": "\u30ab\u30d0\u30fc\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002" }, "title": "MyLink\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a" @@ -48,6 +39,5 @@ "title": "MyLink\u30ab\u30d0\u30fc\u306e\u8a2d\u5b9a" } } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/ko.json b/homeassistant/components/somfy_mylink/translations/ko.json index b099d3d6ed8..3ec70c148b3 100644 --- a/homeassistant/components/somfy_mylink/translations/ko.json +++ b/homeassistant/components/somfy_mylink/translations/ko.json @@ -25,17 +25,8 @@ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { - "entity_config": { - "data": { - "reverse": "\uc5ec\ub2eb\uc774\uac00 \ubc18\uc804\ub418\uc5c8\uc2b5\ub2c8\ub2e4" - }, - "description": "`{entity_id}`\uc5d0 \ub300\ud55c \uc635\uc158 \uad6c\uc131\ud558\uae30", - "title": "\uad6c\uc131\uc694\uc18c \uad6c\uc131\ud558\uae30" - }, "init": { "data": { - "default_reverse": "\uad6c\uc131\ub418\uc9c0 \uc54a\uc740 \uc5ec\ub2eb\uc774\uc5d0 \ub300\ud55c \uae30\ubcf8 \ubc18\uc804 \uc0c1\ud0dc", - "entity_id": "\ud2b9\uc815 \uad6c\uc131\uc694\uc18c\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.", "target_id": "\uc5ec\ub2eb\uc774\uc5d0 \ub300\ud55c \uc635\uc158 \uad6c\uc131\ud558\uae30" }, "title": "MyLink \uc635\uc158 \uad6c\uc131\ud558\uae30" @@ -48,6 +39,5 @@ "title": "MyLink \uc5ec\ub2eb\uc774 \uad6c\uc131\ud558\uae30" } } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/lb.json b/homeassistant/components/somfy_mylink/translations/lb.json index efaba3ab497..0fe190e43ee 100644 --- a/homeassistant/components/somfy_mylink/translations/lb.json +++ b/homeassistant/components/somfy_mylink/translations/lb.json @@ -22,6 +22,5 @@ "abort": { "cannot_connect": "Feeler beim verbannen" } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/nl.json b/homeassistant/components/somfy_mylink/translations/nl.json index 279010485b6..43c71140cb9 100644 --- a/homeassistant/components/somfy_mylink/translations/nl.json +++ b/homeassistant/components/somfy_mylink/translations/nl.json @@ -25,17 +25,8 @@ "cannot_connect": "Kan geen verbinding maken" }, "step": { - "entity_config": { - "data": { - "reverse": "Rolluik is omgekeerd" - }, - "description": "Configureer opties voor `{entity_id}`", - "title": "Entiteit configureren" - }, "init": { "data": { - "default_reverse": "Standaard omkeerstatus voor niet-geconfigureerde rolluiken", - "entity_id": "Configureer een specifieke entiteit.", "target_id": "Configureer opties voor een rolluik." }, "title": "Configureer MyLink-opties" @@ -48,6 +39,5 @@ "title": "Configureer MyLink Cover" } } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/no.json b/homeassistant/components/somfy_mylink/translations/no.json index 65c89866f65..b826eabf45d 100644 --- a/homeassistant/components/somfy_mylink/translations/no.json +++ b/homeassistant/components/somfy_mylink/translations/no.json @@ -25,17 +25,8 @@ "cannot_connect": "Tilkobling mislyktes" }, "step": { - "entity_config": { - "data": { - "reverse": "Rullegardinet reverseres" - }, - "description": "Konfigurer alternativer for \"{entity_id}\"", - "title": "Konfigurer entitet" - }, "init": { "data": { - "default_reverse": "Standard tilbakef\u00f8ringsstatus for ukonfigurerte rullegardiner", - "entity_id": "Konfigurer en bestemt entitet.", "target_id": "Konfigurer alternativer for et rullgardin" }, "title": "Konfigurere MyLink-alternativer" @@ -48,6 +39,5 @@ "title": "Konfigurer MyLink-deksel" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/pl.json b/homeassistant/components/somfy_mylink/translations/pl.json index c4c10de4f9d..8c713ffd31a 100644 --- a/homeassistant/components/somfy_mylink/translations/pl.json +++ b/homeassistant/components/somfy_mylink/translations/pl.json @@ -25,17 +25,8 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { - "entity_config": { - "data": { - "reverse": "Roleta/pokrywa jest odwr\u00f3cona" - }, - "description": "Konfiguracja opcji dla \"{entity_id}\"", - "title": "Konfiguracja encji" - }, "init": { "data": { - "default_reverse": "Domy\u015blny stan odwr\u00f3cenia nieskonfigurowanych rolet/pokryw", - "entity_id": "Skonfiguruj okre\u015blon\u0105 encj\u0119.", "target_id": "Konfiguracja opcji rolety" }, "title": "Konfiguracja opcji MyLink" @@ -48,6 +39,5 @@ "title": "Konfiguracja rolety MyLink" } } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/pt-BR.json b/homeassistant/components/somfy_mylink/translations/pt-BR.json index 12efd378984..7cbe3e77f6c 100644 --- a/homeassistant/components/somfy_mylink/translations/pt-BR.json +++ b/homeassistant/components/somfy_mylink/translations/pt-BR.json @@ -25,17 +25,8 @@ "cannot_connect": "Falha ao conectar" }, "step": { - "entity_config": { - "data": { - "reverse": "A cobertura est\u00e1 invertida" - }, - "description": "Configurar op\u00e7\u00f5es para ` {entity_id} `", - "title": "Configurar entidade" - }, "init": { "data": { - "default_reverse": "Status de revers\u00e3o padr\u00e3o para coberturas n\u00e3o configuradas", - "entity_id": "Configure uma entidade espec\u00edfica.", "target_id": "Configure as op\u00e7\u00f5es para uma cobertura." }, "title": "Configurar op\u00e7\u00f5es do MyLink" @@ -48,6 +39,5 @@ "title": "Configurar a MyLink Cover" } } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/ru.json b/homeassistant/components/somfy_mylink/translations/ru.json index d1ab5624ecf..0385609cb21 100644 --- a/homeassistant/components/somfy_mylink/translations/ru.json +++ b/homeassistant/components/somfy_mylink/translations/ru.json @@ -25,17 +25,8 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "step": { - "entity_config": { - "data": { - "reverse": "\u0418\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0434\u043b\u044f \u0448\u0442\u043e\u0440 \u0438 \u0436\u0430\u043b\u044e\u0437\u0438" - }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0434\u043b\u044f `{entity_id}`", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043e\u0431\u044a\u0435\u043a\u0442\u0430" - }, "init": { "data": { - "default_reverse": "\u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0438\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0434\u043b\u044f \u0448\u0442\u043e\u0440 \u0438 \u0436\u0430\u043b\u044e\u0437\u0438", - "entity_id": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430", "target_id": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0448\u0442\u043e\u0440 \u0438\u043b\u0438 \u0436\u0430\u043b\u044e\u0437\u0438." }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 MyLink" @@ -48,6 +39,5 @@ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 MyLink Cover" } } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/tr.json b/homeassistant/components/somfy_mylink/translations/tr.json index fb402493e3b..8893b7362d9 100644 --- a/homeassistant/components/somfy_mylink/translations/tr.json +++ b/homeassistant/components/somfy_mylink/translations/tr.json @@ -25,17 +25,8 @@ "cannot_connect": "Ba\u011flanma hatas\u0131" }, "step": { - "entity_config": { - "data": { - "reverse": "Kapak ters \u00e7evrildi" - }, - "description": "'{entity_id}' i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n", - "title": "Varl\u0131\u011f\u0131 Yap\u0131land\u0131r" - }, "init": { "data": { - "default_reverse": "Yap\u0131land\u0131r\u0131lmam\u0131\u015f kapaklar i\u00e7in varsay\u0131lan geri alma durumu", - "entity_id": "Belirli bir varl\u0131\u011f\u0131 yap\u0131land\u0131r\u0131n.", "target_id": "Kapak i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n." }, "title": "MyLink Se\u00e7eneklerini Yap\u0131land\u0131r\u0131n" @@ -48,6 +39,5 @@ "title": "MyLink Kapa\u011f\u0131n\u0131 Yap\u0131land\u0131r\u0131n" } } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/uk.json b/homeassistant/components/somfy_mylink/translations/uk.json index 2d251531340..f458c21bcf9 100644 --- a/homeassistant/components/somfy_mylink/translations/uk.json +++ b/homeassistant/components/somfy_mylink/translations/uk.json @@ -24,17 +24,9 @@ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" }, "step": { - "entity_config": { - "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u0434\u043b\u044f \"{entity_id}\"", - "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c" - }, "init": { - "data": { - "entity_id": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u043f\u0435\u0446\u0438\u0444\u0456\u0447\u043d\u043e\u0457 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456." - }, "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0435\u0439 MyLink" } } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/zh-Hant.json b/homeassistant/components/somfy_mylink/translations/zh-Hant.json index 0aa72fb0c61..15a3927877d 100644 --- a/homeassistant/components/somfy_mylink/translations/zh-Hant.json +++ b/homeassistant/components/somfy_mylink/translations/zh-Hant.json @@ -25,17 +25,8 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "step": { - "entity_config": { - "data": { - "reverse": "\u7a97\u7c3e\u53cd\u5411" - }, - "description": "`{entity_id}` \u8a2d\u5b9a\u9078\u9805", - "title": "\u8a2d\u5b9a\u5be6\u9ad4" - }, "init": { "data": { - "default_reverse": "\u672a\u8a2d\u5b9a\u7a97\u7c3e\u9810\u8a2d\u70ba\u53cd\u5411", - "entity_id": "\u8a2d\u5b9a\u7279\u5b9a\u5be6\u9ad4\u3002", "target_id": "\u7a97\u7c3e\u8a2d\u5b9a\u9078\u9805\u3002" }, "title": "MyLink \u8a2d\u5b9a\u9078\u9805" @@ -48,6 +39,5 @@ "title": "\u8a2d\u5b9a MyLink \u7a97\u7c3e" } } - }, - "title": "Somfy MyLink" + } } \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/bg.json b/homeassistant/components/sonarr/translations/bg.json index d866b1d1813..05d96cf621c 100644 --- a/homeassistant/components/sonarr/translations/bg.json +++ b/homeassistant/components/sonarr/translations/bg.json @@ -17,9 +17,6 @@ "user": { "data": { "api_key": "API \u043a\u043b\u044e\u0447", - "base_path": "\u041f\u044a\u0442 \u0434\u043e API", - "host": "\u0425\u043e\u0441\u0442", - "port": "\u041f\u043e\u0440\u0442", "url": "URL" } } diff --git a/homeassistant/components/sonarr/translations/ca.json b/homeassistant/components/sonarr/translations/ca.json index d3cb5720875..49170885242 100644 --- a/homeassistant/components/sonarr/translations/ca.json +++ b/homeassistant/components/sonarr/translations/ca.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "Clau API", - "base_path": "Ruta a l'API", - "host": "Amfitri\u00f3", - "port": "Port", - "ssl": "Utilitza un certificat SSL", "url": "URL", "verify_ssl": "Verifica el certificat SSL" } diff --git a/homeassistant/components/sonarr/translations/cs.json b/homeassistant/components/sonarr/translations/cs.json index 9be2106b4e2..27c3a92bc2e 100644 --- a/homeassistant/components/sonarr/translations/cs.json +++ b/homeassistant/components/sonarr/translations/cs.json @@ -18,9 +18,6 @@ "user": { "data": { "api_key": "Kl\u00ed\u010d API", - "host": "Hostitel", - "port": "Port", - "ssl": "Pou\u017e\u00edv\u00e1 SSL certifik\u00e1t", "url": "URL", "verify_ssl": "Ov\u011b\u0159it certifik\u00e1t SSL" } diff --git a/homeassistant/components/sonarr/translations/de.json b/homeassistant/components/sonarr/translations/de.json index eb521c1c237..4559b75c655 100644 --- a/homeassistant/components/sonarr/translations/de.json +++ b/homeassistant/components/sonarr/translations/de.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "API-Schl\u00fcssel", - "base_path": "Pfad zur API", - "host": "Host", - "port": "Port", - "ssl": "Verwendet ein SSL-Zertifikat", "url": "URL", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" } diff --git a/homeassistant/components/sonarr/translations/el.json b/homeassistant/components/sonarr/translations/el.json index 22895b3a38a..542225c0960 100644 --- a/homeassistant/components/sonarr/translations/el.json +++ b/homeassistant/components/sonarr/translations/el.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", - "base_path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf API", - "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", - "port": "\u0398\u03cd\u03c1\u03b1", - "ssl": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ad\u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL", "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL", "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" } diff --git a/homeassistant/components/sonarr/translations/en.json b/homeassistant/components/sonarr/translations/en.json index f676005dcfe..61b1158d9a3 100644 --- a/homeassistant/components/sonarr/translations/en.json +++ b/homeassistant/components/sonarr/translations/en.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "API Key", - "base_path": "Path to API", - "host": "Host", - "port": "Port", - "ssl": "Uses an SSL certificate", "url": "URL", "verify_ssl": "Verify SSL certificate" } diff --git a/homeassistant/components/sonarr/translations/es.json b/homeassistant/components/sonarr/translations/es.json index f7d55cc5353..3c1b7c0fa9a 100644 --- a/homeassistant/components/sonarr/translations/es.json +++ b/homeassistant/components/sonarr/translations/es.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "Clave API", - "base_path": "Ruta a la API", - "host": "Host", - "port": "Puerto", - "ssl": "Utiliza un certificado SSL", "url": "URL", "verify_ssl": "Verificar certificado SSL" } diff --git a/homeassistant/components/sonarr/translations/et.json b/homeassistant/components/sonarr/translations/et.json index 4629e59e68d..8fa1d128455 100644 --- a/homeassistant/components/sonarr/translations/et.json +++ b/homeassistant/components/sonarr/translations/et.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "API v\u00f5ti", - "base_path": "API asukoht", - "host": "", - "port": "", - "ssl": "Kasutab SSL serti", "url": "URL", "verify_ssl": "Kontrolli SSL sertifikaati" } diff --git a/homeassistant/components/sonarr/translations/fr.json b/homeassistant/components/sonarr/translations/fr.json index 0793adc1b9f..dc88f6770bc 100644 --- a/homeassistant/components/sonarr/translations/fr.json +++ b/homeassistant/components/sonarr/translations/fr.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "Cl\u00e9 d'API", - "base_path": "Chemin vers l'API", - "host": "H\u00f4te", - "port": "Port", - "ssl": "Utilise un certificat SSL", "url": "URL", "verify_ssl": "V\u00e9rifier le certificat SSL" } diff --git a/homeassistant/components/sonarr/translations/he.json b/homeassistant/components/sonarr/translations/he.json index 53b98ae4139..3fbaf2d2684 100644 --- a/homeassistant/components/sonarr/translations/he.json +++ b/homeassistant/components/sonarr/translations/he.json @@ -17,9 +17,6 @@ "user": { "data": { "api_key": "\u05de\u05e4\u05ea\u05d7 API", - "host": "\u05de\u05d0\u05e8\u05d7", - "port": "\u05e4\u05ea\u05d7\u05d4", - "ssl": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05d0\u05d9\u05e9\u05d5\u05e8 SSL", "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" } diff --git a/homeassistant/components/sonarr/translations/hu.json b/homeassistant/components/sonarr/translations/hu.json index 5aabd38a974..9e77eecfb6f 100644 --- a/homeassistant/components/sonarr/translations/hu.json +++ b/homeassistant/components/sonarr/translations/hu.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "API kulcs", - "base_path": "El\u00e9r\u00e9si \u00fat az API-hoz", - "host": "C\u00edm", - "port": "Port", - "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", "url": "URL", "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 ec76bf44491..a84276eab01 100644 --- a/homeassistant/components/sonarr/translations/id.json +++ b/homeassistant/components/sonarr/translations/id.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "Kunci API", - "base_path": "Jalur ke API", - "host": "Host", - "port": "Port", - "ssl": "Menggunakan sertifikat SSL", "url": "URL", "verify_ssl": "Verifikasi sertifikat SSL" } diff --git a/homeassistant/components/sonarr/translations/it.json b/homeassistant/components/sonarr/translations/it.json index ba78810d928..727e556bb3c 100644 --- a/homeassistant/components/sonarr/translations/it.json +++ b/homeassistant/components/sonarr/translations/it.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "Chiave API", - "base_path": "Percorso dell'API", - "host": "Host", - "port": "Porta", - "ssl": "Utilizza un certificato SSL", "url": "URL", "verify_ssl": "Verifica il certificato SSL" } diff --git a/homeassistant/components/sonarr/translations/ja.json b/homeassistant/components/sonarr/translations/ja.json index 53a659d9bde..cbe230bbc94 100644 --- a/homeassistant/components/sonarr/translations/ja.json +++ b/homeassistant/components/sonarr/translations/ja.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "API\u30ad\u30fc", - "base_path": "API\u3078\u306e\u30d1\u30b9", - "host": "\u30db\u30b9\u30c8", - "port": "\u30dd\u30fc\u30c8", - "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", "url": "URL", "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" } diff --git a/homeassistant/components/sonarr/translations/ko.json b/homeassistant/components/sonarr/translations/ko.json index fbff72e46f2..b7389a03043 100644 --- a/homeassistant/components/sonarr/translations/ko.json +++ b/homeassistant/components/sonarr/translations/ko.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "API \ud0a4", - "base_path": "API \uacbd\ub85c", - "host": "\ud638\uc2a4\ud2b8", - "port": "\ud3ec\ud2b8", - "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" } } diff --git a/homeassistant/components/sonarr/translations/lb.json b/homeassistant/components/sonarr/translations/lb.json index 392b3f7d4e2..dfde55e9924 100644 --- a/homeassistant/components/sonarr/translations/lb.json +++ b/homeassistant/components/sonarr/translations/lb.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "API Schl\u00ebssel", - "base_path": "Pad zur API", - "host": "Host", - "port": "Port", - "ssl": "Benotzt ee SSL Zertifikat", "verify_ssl": "SSL Zertifikat iwwerpr\u00e9iwen" } } diff --git a/homeassistant/components/sonarr/translations/nl.json b/homeassistant/components/sonarr/translations/nl.json index 58bf04e283d..d6782781090 100644 --- a/homeassistant/components/sonarr/translations/nl.json +++ b/homeassistant/components/sonarr/translations/nl.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "API-sleutel", - "base_path": "Pad naar API", - "host": "Host", - "port": "Poort", - "ssl": "Maakt gebruik van een SSL-certificaat", "url": "URL", "verify_ssl": "SSL-certificaat verifi\u00ebren" } diff --git a/homeassistant/components/sonarr/translations/no.json b/homeassistant/components/sonarr/translations/no.json index 5ee028b8f02..3d64a9199a4 100644 --- a/homeassistant/components/sonarr/translations/no.json +++ b/homeassistant/components/sonarr/translations/no.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "API-n\u00f8kkel", - "base_path": "Bane til API", - "host": "Vert", - "port": "Port", - "ssl": "Bruker et SSL-sertifikat", "url": "URL", "verify_ssl": "Verifisere SSL-sertifikat" } diff --git a/homeassistant/components/sonarr/translations/pl.json b/homeassistant/components/sonarr/translations/pl.json index e5be700a752..3f93623af76 100644 --- a/homeassistant/components/sonarr/translations/pl.json +++ b/homeassistant/components/sonarr/translations/pl.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "Klucz API", - "base_path": "\u015acie\u017cka do API", - "host": "Nazwa hosta lub adres IP", - "port": "Port", - "ssl": "Certyfikat SSL", "url": "URL", "verify_ssl": "Weryfikacja certyfikatu SSL" } diff --git a/homeassistant/components/sonarr/translations/pt-BR.json b/homeassistant/components/sonarr/translations/pt-BR.json index 4c474ef2349..1aa3de5b209 100644 --- a/homeassistant/components/sonarr/translations/pt-BR.json +++ b/homeassistant/components/sonarr/translations/pt-BR.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "Chave da API", - "base_path": "Caminho para a API", - "host": "Nome do host", - "port": "Porta", - "ssl": "Usar um certificado SSL", "url": "URL", "verify_ssl": "Verifique o certificado SSL" } diff --git a/homeassistant/components/sonarr/translations/pt.json b/homeassistant/components/sonarr/translations/pt.json index bb36cb63dfc..9001b9c2c3f 100644 --- a/homeassistant/components/sonarr/translations/pt.json +++ b/homeassistant/components/sonarr/translations/pt.json @@ -17,9 +17,6 @@ "user": { "data": { "api_key": "Chave da API", - "host": "Servidor", - "port": "Porta", - "ssl": "Utiliza um certificado SSL", "verify_ssl": "Verificar o certificado SSL" } } diff --git a/homeassistant/components/sonarr/translations/ru.json b/homeassistant/components/sonarr/translations/ru.json index 531058f3fff..47843b028db 100644 --- a/homeassistant/components/sonarr/translations/ru.json +++ b/homeassistant/components/sonarr/translations/ru.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", - "base_path": "\u041f\u0443\u0442\u044c \u043a API", - "host": "\u0425\u043e\u0441\u0442", - "port": "\u041f\u043e\u0440\u0442", - "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", "url": "URL-\u0430\u0434\u0440\u0435\u0441", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" } diff --git a/homeassistant/components/sonarr/translations/sk.json b/homeassistant/components/sonarr/translations/sk.json index 6e1cd075aa9..64731388e98 100644 --- a/homeassistant/components/sonarr/translations/sk.json +++ b/homeassistant/components/sonarr/translations/sk.json @@ -9,8 +9,7 @@ "step": { "user": { "data": { - "api_key": "API k\u013e\u00fa\u010d", - "port": "Port" + "api_key": "API k\u013e\u00fa\u010d" } } } diff --git a/homeassistant/components/sonarr/translations/sl.json b/homeassistant/components/sonarr/translations/sl.json index b8c5332be9c..fd6fbd9cd2a 100644 --- a/homeassistant/components/sonarr/translations/sl.json +++ b/homeassistant/components/sonarr/translations/sl.json @@ -3,7 +3,6 @@ "step": { "user": { "data": { - "ssl": "Uporablja SSL certifikat", "verify_ssl": "Preverite SSL certifikat" } } diff --git a/homeassistant/components/sonarr/translations/tr.json b/homeassistant/components/sonarr/translations/tr.json index c064e4947c3..c72d84767d1 100644 --- a/homeassistant/components/sonarr/translations/tr.json +++ b/homeassistant/components/sonarr/translations/tr.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "API Anahtar\u0131", - "base_path": "API yolu", - "host": "Ana Bilgisayar", - "port": "Port", - "ssl": "SSL sertifikas\u0131 kullan\u0131r", "url": "URL", "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" } diff --git a/homeassistant/components/sonarr/translations/uk.json b/homeassistant/components/sonarr/translations/uk.json index 0b6b7acf26d..85f1c42cb42 100644 --- a/homeassistant/components/sonarr/translations/uk.json +++ b/homeassistant/components/sonarr/translations/uk.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", - "base_path": "\u0428\u043b\u044f\u0445 \u0434\u043e API", - "host": "\u0425\u043e\u0441\u0442", - "port": "\u041f\u043e\u0440\u0442", - "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" } } diff --git a/homeassistant/components/sonarr/translations/zh-Hans.json b/homeassistant/components/sonarr/translations/zh-Hans.json index 265928213f5..045eb27df30 100644 --- a/homeassistant/components/sonarr/translations/zh-Hans.json +++ b/homeassistant/components/sonarr/translations/zh-Hans.json @@ -17,9 +17,6 @@ "user": { "data": { "api_key": "API \u5bc6\u94a5", - "host": "\u4e3b\u673a\u5730\u5740", - "port": "\u7aef\u53e3", - "ssl": "\u4f7f\u7528 SSL \u8bc1\u4e66", "verify_ssl": "\u9a8c\u8bc1 SSL \u8bc1\u4e66" } } diff --git a/homeassistant/components/sonarr/translations/zh-Hant.json b/homeassistant/components/sonarr/translations/zh-Hant.json index 4688ec2e438..6d9be9dd294 100644 --- a/homeassistant/components/sonarr/translations/zh-Hant.json +++ b/homeassistant/components/sonarr/translations/zh-Hant.json @@ -18,10 +18,6 @@ "user": { "data": { "api_key": "API \u91d1\u9470", - "base_path": "API \u8def\u5f91", - "host": "\u4e3b\u6a5f\u7aef", - "port": "\u901a\u8a0a\u57e0", - "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49", "url": "\u7db2\u5740", "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" } diff --git a/homeassistant/components/speedtestdotnet/translations/ar.json b/homeassistant/components/speedtestdotnet/translations/ar.json index 527ad2ecd96..943182c47a4 100644 --- a/homeassistant/components/speedtestdotnet/translations/ar.json +++ b/homeassistant/components/speedtestdotnet/translations/ar.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "wrong_server_id": "\u0645\u0639\u0631\u0641 \u0627\u0644\u062e\u0627\u062f\u0645 \u063a\u064a\u0631 \u0635\u0627\u0644\u062d" - }, "step": { "user": { "description": "\u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f \u0645\u0646 \u0623\u0646\u0643 \u062a\u0631\u064a\u062f \u0625\u0639\u062f\u0627\u062f SpeedTest\u061f" diff --git a/homeassistant/components/speedtestdotnet/translations/ca.json b/homeassistant/components/speedtestdotnet/translations/ca.json index 57caf0ed69b..c1c5dda71cf 100644 --- a/homeassistant/components/speedtestdotnet/translations/ca.json +++ b/homeassistant/components/speedtestdotnet/translations/ca.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", - "wrong_server_id": "L'identificador del servidor no \u00e9s v\u00e0lid" + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/cs.json b/homeassistant/components/speedtestdotnet/translations/cs.json index 7564e3ab4f7..22ad2f23322 100644 --- a/homeassistant/components/speedtestdotnet/translations/cs.json +++ b/homeassistant/components/speedtestdotnet/translations/cs.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace.", - "wrong_server_id": "ID serveru nen\u00ed platn\u00e9." + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/de.json b/homeassistant/components/speedtestdotnet/translations/de.json index eff51fab0b4..81910cb9c70 100644 --- a/homeassistant/components/speedtestdotnet/translations/de.json +++ b/homeassistant/components/speedtestdotnet/translations/de.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", - "wrong_server_id": "Server-ID ist ung\u00fcltig" + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/el.json b/homeassistant/components/speedtestdotnet/translations/el.json index 096e339732c..25b5e23ab69 100644 --- a/homeassistant/components/speedtestdotnet/translations/el.json +++ b/homeassistant/components/speedtestdotnet/translations/el.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae.", - "wrong_server_id": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf" + "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/en.json b/homeassistant/components/speedtestdotnet/translations/en.json index b56ff193e33..eab480073bc 100644 --- a/homeassistant/components/speedtestdotnet/translations/en.json +++ b/homeassistant/components/speedtestdotnet/translations/en.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Already configured. Only a single configuration possible.", - "wrong_server_id": "Server ID is not valid" + "single_instance_allowed": "Already configured. Only a single configuration possible." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/es.json b/homeassistant/components/speedtestdotnet/translations/es.json index 1727f4d2a6c..42f263971ba 100644 --- a/homeassistant/components/speedtestdotnet/translations/es.json +++ b/homeassistant/components/speedtestdotnet/translations/es.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", - "wrong_server_id": "El identificador del servidor no es v\u00e1lido" + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/et.json b/homeassistant/components/speedtestdotnet/translations/et.json index 834581a9941..ac1915e760a 100644 --- a/homeassistant/components/speedtestdotnet/translations/et.json +++ b/homeassistant/components/speedtestdotnet/translations/et.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.", - "wrong_server_id": "Serveri ID ei sobi" + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/fr.json b/homeassistant/components/speedtestdotnet/translations/fr.json index 4ad47bb6a3e..d2efebd0eb1 100644 --- a/homeassistant/components/speedtestdotnet/translations/fr.json +++ b/homeassistant/components/speedtestdotnet/translations/fr.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", - "wrong_server_id": "L'ID du serveur n'est pas valide" + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/he.json b/homeassistant/components/speedtestdotnet/translations/he.json index ea3dd80aa94..08506bf3437 100644 --- a/homeassistant/components/speedtestdotnet/translations/he.json +++ b/homeassistant/components/speedtestdotnet/translations/he.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea.", - "wrong_server_id": "\u05de\u05d6\u05d4\u05d4 \u05d4\u05e9\u05e8\u05ea \u05d0\u05d9\u05e0\u05d5 \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." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/hu.json b/homeassistant/components/speedtestdotnet/translations/hu.json index 9e602652e5b..c223e8b9376 100644 --- a/homeassistant/components/speedtestdotnet/translations/hu.json +++ b/homeassistant/components/speedtestdotnet/translations/hu.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", - "wrong_server_id": "A szerver azonos\u00edt\u00f3 \u00e9rv\u00e9nytelen" + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/id.json b/homeassistant/components/speedtestdotnet/translations/id.json index 24e78609380..f609c3d384a 100644 --- a/homeassistant/components/speedtestdotnet/translations/id.json +++ b/homeassistant/components/speedtestdotnet/translations/id.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan.", - "wrong_server_id": "ID server tidak valid" + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/it.json b/homeassistant/components/speedtestdotnet/translations/it.json index d0c44329313..07615fe093c 100644 --- a/homeassistant/components/speedtestdotnet/translations/it.json +++ b/homeassistant/components/speedtestdotnet/translations/it.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", - "wrong_server_id": "L'ID Server non \u00e8 valido" + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/ja.json b/homeassistant/components/speedtestdotnet/translations/ja.json index 4e254367593..5712139b6b9 100644 --- a/homeassistant/components/speedtestdotnet/translations/ja.json +++ b/homeassistant/components/speedtestdotnet/translations/ja.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", - "wrong_server_id": "\u30b5\u30fc\u30d0\u30fcID\u304c\u7121\u52b9\u3067\u3059" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/ko.json b/homeassistant/components/speedtestdotnet/translations/ko.json index e3b65208317..dbef7c3b7d3 100644 --- a/homeassistant/components/speedtestdotnet/translations/ko.json +++ b/homeassistant/components/speedtestdotnet/translations/ko.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "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.", - "wrong_server_id": "\uc11c\ubc84 ID\uac00 \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." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/lb.json b/homeassistant/components/speedtestdotnet/translations/lb.json index 3081080d1fb..4007faf71e7 100644 --- a/homeassistant/components/speedtestdotnet/translations/lb.json +++ b/homeassistant/components/speedtestdotnet/translations/lb.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.", - "wrong_server_id": "Server ID ass ong\u00eblteg" + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/nl.json b/homeassistant/components/speedtestdotnet/translations/nl.json index 3a112d48e9d..8cd6cb960c8 100644 --- a/homeassistant/components/speedtestdotnet/translations/nl.json +++ b/homeassistant/components/speedtestdotnet/translations/nl.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", - "wrong_server_id": "Server-ID is niet geldig" + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/no.json b/homeassistant/components/speedtestdotnet/translations/no.json index a079bc1fa7b..01909d39f06 100644 --- a/homeassistant/components/speedtestdotnet/translations/no.json +++ b/homeassistant/components/speedtestdotnet/translations/no.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", - "wrong_server_id": "Server ID er ikke gyldig" + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/pl.json b/homeassistant/components/speedtestdotnet/translations/pl.json index d64a7920bea..b06d7cdd285 100644 --- a/homeassistant/components/speedtestdotnet/translations/pl.json +++ b/homeassistant/components/speedtestdotnet/translations/pl.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", - "wrong_server_id": "Identyfikator serwera jest nieprawid\u0142owy" + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/pt-BR.json b/homeassistant/components/speedtestdotnet/translations/pt-BR.json index 0498a2fad6d..739b3b41875 100644 --- a/homeassistant/components/speedtestdotnet/translations/pt-BR.json +++ b/homeassistant/components/speedtestdotnet/translations/pt-BR.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", - "wrong_server_id": "O ID do servidor n\u00e3o \u00e9 v\u00e1lido" + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/ru.json b/homeassistant/components/speedtestdotnet/translations/ru.json index c26c63d8511..3ffcd10bf31 100644 --- a/homeassistant/components/speedtestdotnet/translations/ru.json +++ b/homeassistant/components/speedtestdotnet/translations/ru.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.", - "wrong_server_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d." + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/tr.json b/homeassistant/components/speedtestdotnet/translations/tr.json index b13be7c5e0c..5becafdf153 100644 --- a/homeassistant/components/speedtestdotnet/translations/tr.json +++ b/homeassistant/components/speedtestdotnet/translations/tr.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", - "wrong_server_id": "Sunucu kimli\u011fi ge\u00e7erli de\u011fil" + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/uk.json b/homeassistant/components/speedtestdotnet/translations/uk.json index e54ed1da92e..0456d600e59 100644 --- a/homeassistant/components/speedtestdotnet/translations/uk.json +++ b/homeassistant/components/speedtestdotnet/translations/uk.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u0412\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e. \u041c\u043e\u0436\u043b\u0438\u0432\u0430 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f.", - "wrong_server_id": "\u041d\u0435\u043f\u0440\u0438\u043f\u0443\u0441\u0442\u0438\u043c\u0438\u0439 \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0430." + "single_instance_allowed": "\u0412\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e. \u041c\u043e\u0436\u043b\u0438\u0432\u0430 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f." }, "step": { "user": { diff --git a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json index 49b8b0cfb20..43d30d4aeb8 100644 --- a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json +++ b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", - "wrong_server_id": "\u4f3a\u670d\u5668 ID \u7121\u6548" + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "user": { diff --git a/homeassistant/components/sql/translations/ca.json b/homeassistant/components/sql/translations/ca.json index a579253e8d7..48498362c89 100644 --- a/homeassistant/components/sql/translations/ca.json +++ b/homeassistant/components/sql/translations/ca.json @@ -5,8 +5,7 @@ }, "error": { "db_url_invalid": "URL de la base de dades inv\u00e0lid", - "query_invalid": "Consulta SQL inv\u00e0lida", - "value_template_invalid": "Plantilla de valor inv\u00e0lida" + "query_invalid": "Consulta SQL inv\u00e0lida" }, "step": { "user": { @@ -32,8 +31,7 @@ "options": { "error": { "db_url_invalid": "URL de la base de dades inv\u00e0lid", - "query_invalid": "Consulta SQL inv\u00e0lida", - "value_template_invalid": "Plantilla de valor inv\u00e0lida" + "query_invalid": "Consulta SQL inv\u00e0lida" }, "step": { "init": { diff --git a/homeassistant/components/sql/translations/de.json b/homeassistant/components/sql/translations/de.json index 53642f5bede..258bd06f876 100644 --- a/homeassistant/components/sql/translations/de.json +++ b/homeassistant/components/sql/translations/de.json @@ -5,8 +5,7 @@ }, "error": { "db_url_invalid": "Datenbank-URL ung\u00fcltig", - "query_invalid": "SQL-Abfrage ung\u00fcltig", - "value_template_invalid": "Wertvorlage ung\u00fcltig" + "query_invalid": "SQL-Abfrage ung\u00fcltig" }, "step": { "user": { @@ -32,8 +31,7 @@ "options": { "error": { "db_url_invalid": "Datenbank-URL ung\u00fcltig", - "query_invalid": "SQL-Abfrage ung\u00fcltig", - "value_template_invalid": "Wertvorlage ung\u00fcltig" + "query_invalid": "SQL-Abfrage ung\u00fcltig" }, "step": { "init": { diff --git a/homeassistant/components/sql/translations/el.json b/homeassistant/components/sql/translations/el.json index 85326cb886e..9f568eb0b03 100644 --- a/homeassistant/components/sql/translations/el.json +++ b/homeassistant/components/sql/translations/el.json @@ -5,8 +5,7 @@ }, "error": { "db_url_invalid": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b2\u03ac\u03c3\u03b7\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd", - "query_invalid": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b5\u03c1\u03ce\u03c4\u03b7\u03bc\u03b1 SQL", - "value_template_invalid": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03c0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf \u03c4\u03b9\u03bc\u03ae\u03c2" + "query_invalid": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b5\u03c1\u03ce\u03c4\u03b7\u03bc\u03b1 SQL" }, "step": { "user": { @@ -32,8 +31,7 @@ "options": { "error": { "db_url_invalid": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b2\u03ac\u03c3\u03b7\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd", - "query_invalid": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b5\u03c1\u03ce\u03c4\u03b7\u03bc\u03b1 SQL", - "value_template_invalid": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03c0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf \u03c4\u03b9\u03bc\u03ae\u03c2" + "query_invalid": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b5\u03c1\u03ce\u03c4\u03b7\u03bc\u03b1 SQL" }, "step": { "init": { diff --git a/homeassistant/components/sql/translations/en.json b/homeassistant/components/sql/translations/en.json index 4b72d024df4..9d05f771853 100644 --- a/homeassistant/components/sql/translations/en.json +++ b/homeassistant/components/sql/translations/en.json @@ -5,8 +5,7 @@ }, "error": { "db_url_invalid": "Database URL invalid", - "query_invalid": "SQL Query invalid", - "value_template_invalid": "Value Template invalid" + "query_invalid": "SQL Query invalid" }, "step": { "user": { @@ -32,8 +31,7 @@ "options": { "error": { "db_url_invalid": "Database URL invalid", - "query_invalid": "SQL Query invalid", - "value_template_invalid": "Value Template invalid" + "query_invalid": "SQL Query invalid" }, "step": { "init": { diff --git a/homeassistant/components/sql/translations/es.json b/homeassistant/components/sql/translations/es.json index 3d794607feb..7117115efc7 100644 --- a/homeassistant/components/sql/translations/es.json +++ b/homeassistant/components/sql/translations/es.json @@ -28,8 +28,7 @@ "options": { "error": { "db_url_invalid": "URL de la base de datos inv\u00e1lido", - "query_invalid": "Consulta SQL inv\u00e1lida", - "value_template_invalid": "Plantilla de valor inv\u00e1lida" + "query_invalid": "Consulta SQL inv\u00e1lida" }, "step": { "init": { diff --git a/homeassistant/components/sql/translations/et.json b/homeassistant/components/sql/translations/et.json index 62bbd96b63e..701c0e6d490 100644 --- a/homeassistant/components/sql/translations/et.json +++ b/homeassistant/components/sql/translations/et.json @@ -5,8 +5,7 @@ }, "error": { "db_url_invalid": "Andmebaasi URL on vigane", - "query_invalid": "SQL-p\u00e4ring on kehtetu", - "value_template_invalid": "V\u00e4\u00e4rtuse mall on kehtetu" + "query_invalid": "SQL-p\u00e4ring on kehtetu" }, "step": { "user": { @@ -32,8 +31,7 @@ "options": { "error": { "db_url_invalid": "Andmebaasi URL on vigane", - "query_invalid": "V\u00e4\u00e4rtuse mall on kehtetu", - "value_template_invalid": "V\u00e4\u00e4rtuse mall on kehtetu" + "query_invalid": "V\u00e4\u00e4rtuse mall on kehtetu" }, "step": { "init": { diff --git a/homeassistant/components/sql/translations/fr.json b/homeassistant/components/sql/translations/fr.json index f7607845119..9d791062798 100644 --- a/homeassistant/components/sql/translations/fr.json +++ b/homeassistant/components/sql/translations/fr.json @@ -5,8 +5,7 @@ }, "error": { "db_url_invalid": "URL de la base de donn\u00e9es non valide", - "query_invalid": "Requ\u00eate SQL non valide", - "value_template_invalid": "Mod\u00e8le de valeur non valide" + "query_invalid": "Requ\u00eate SQL non valide" }, "step": { "user": { @@ -32,8 +31,7 @@ "options": { "error": { "db_url_invalid": "URL de la base de donn\u00e9es non valide", - "query_invalid": "Requ\u00eate SQL non valide", - "value_template_invalid": "Mod\u00e8le de valeur non valide" + "query_invalid": "Requ\u00eate SQL non valide" }, "step": { "init": { diff --git a/homeassistant/components/sql/translations/hu.json b/homeassistant/components/sql/translations/hu.json index c7f8b02a2b6..2e7f74c8fae 100644 --- a/homeassistant/components/sql/translations/hu.json +++ b/homeassistant/components/sql/translations/hu.json @@ -5,8 +5,7 @@ }, "error": { "db_url_invalid": "\u00c9rv\u00e9nytelen adatb\u00e1zis URL-c\u00edm", - "query_invalid": "\u00c9rv\u00e9nytelen SQL-lek\u00e9rdez\u00e9s", - "value_template_invalid": "\u00c9rv\u00e9nytelen \u00e9rt\u00e9ksablon" + "query_invalid": "\u00c9rv\u00e9nytelen SQL-lek\u00e9rdez\u00e9s" }, "step": { "user": { @@ -32,8 +31,7 @@ "options": { "error": { "db_url_invalid": "\u00c9rv\u00e9nytelen adatb\u00e1zis URL-c\u00edm", - "query_invalid": "\u00c9rv\u00e9nytelen SQL-lek\u00e9rdez\u00e9s", - "value_template_invalid": "\u00c9rv\u00e9nytelen \u00e9rt\u00e9ksablon" + "query_invalid": "\u00c9rv\u00e9nytelen SQL-lek\u00e9rdez\u00e9s" }, "step": { "init": { diff --git a/homeassistant/components/sql/translations/id.json b/homeassistant/components/sql/translations/id.json index 8680824e467..08810e24b2c 100644 --- a/homeassistant/components/sql/translations/id.json +++ b/homeassistant/components/sql/translations/id.json @@ -5,8 +5,7 @@ }, "error": { "db_url_invalid": "URL database tidak valid", - "query_invalid": "Kueri SQL tidak valid", - "value_template_invalid": "Templat Nilai tidak valid" + "query_invalid": "Kueri SQL tidak valid" }, "step": { "user": { @@ -32,8 +31,7 @@ "options": { "error": { "db_url_invalid": "URL database tidak valid", - "query_invalid": "Kueri SQL tidak valid", - "value_template_invalid": "Templat Nilai tidak valid" + "query_invalid": "Kueri SQL tidak valid" }, "step": { "init": { diff --git a/homeassistant/components/sql/translations/it.json b/homeassistant/components/sql/translations/it.json index 3d7bee5454e..04cfb5744bf 100644 --- a/homeassistant/components/sql/translations/it.json +++ b/homeassistant/components/sql/translations/it.json @@ -5,8 +5,7 @@ }, "error": { "db_url_invalid": "URL database non valido", - "query_invalid": "Query SQL non valida", - "value_template_invalid": "Modello di valore non valido" + "query_invalid": "Query SQL non valida" }, "step": { "user": { @@ -32,8 +31,7 @@ "options": { "error": { "db_url_invalid": "URL database non valido", - "query_invalid": "Query SQL non valida", - "value_template_invalid": "Modello di valore non valido" + "query_invalid": "Query SQL non valida" }, "step": { "init": { diff --git a/homeassistant/components/sql/translations/ja.json b/homeassistant/components/sql/translations/ja.json index 3b9fdfc8eaa..4b6e6fec7bb 100644 --- a/homeassistant/components/sql/translations/ja.json +++ b/homeassistant/components/sql/translations/ja.json @@ -5,8 +5,7 @@ }, "error": { "db_url_invalid": "\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306eURL\u304c\u7121\u52b9\u3067\u3059", - "query_invalid": "SQL\u30af\u30a8\u30ea\u304c\u7121\u52b9\u3067\u3059", - "value_template_invalid": "\u5024\u306e\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8\u304c\u7121\u52b9\u3067\u3059" + "query_invalid": "SQL\u30af\u30a8\u30ea\u304c\u7121\u52b9\u3067\u3059" }, "step": { "user": { @@ -32,8 +31,7 @@ "options": { "error": { "db_url_invalid": "\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306eURL\u304c\u7121\u52b9\u3067\u3059", - "query_invalid": "SQL\u30af\u30a8\u30ea\u304c\u7121\u52b9\u3067\u3059", - "value_template_invalid": "\u5024\u306e\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8\u304c\u7121\u52b9\u3067\u3059" + "query_invalid": "SQL\u30af\u30a8\u30ea\u304c\u7121\u52b9\u3067\u3059" }, "step": { "init": { diff --git a/homeassistant/components/sql/translations/ko.json b/homeassistant/components/sql/translations/ko.json new file mode 100644 index 00000000000..3d4baeb6e35 --- /dev/null +++ b/homeassistant/components/sql/translations/ko.json @@ -0,0 +1,51 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "db_url_invalid": "\ub370\uc774\ud130\ubca0\uc774\uc2a4 URL\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "query_invalid": "SQL \ucffc\ub9ac\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "column": "\uc5f4", + "db_url": "\ub370\uc774\ud130\ubca0\uc774\uc2a4 URL", + "name": "\uc774\ub984", + "query": "\ucffc\ub9ac \uc120\ud0dd", + "unit_of_measurement": "\uce21\uc815 \ub2e8\uc704", + "value_template": "\uac12 \ud15c\ud50c\ub9bf" + }, + "data_description": { + "column": "\ubc18\ud658\ub41c \ucffc\ub9ac\uac00 \uc0c1\ud0dc\ub85c \ud45c\uc2dc\ub418\ub3c4\ub85d \ud558\ub294 \uc5f4", + "db_url": "\ub370\uc774\ud130\ubca0\uc774\uc2a4 URL, \uae30\ubcf8 HA \ub370\uc774\ud130\ubca0\uc774\uc2a4\ub97c \uc0ac\uc6a9\ud558\ub824\uba74 \ube44\uc6cc \ub461\ub2c8\ub2e4.", + "name": "\ud1b5\ud569\uad6c\uc131\uc694\uc18c \ubc0f \uc13c\uc11c\uc5d0 \uc0ac\uc6a9\ub420 \uc774\ub984", + "query": "\uc2e4\ud589\ud560 \ucffc\ub9ac\ub294 'SELECT'\ub85c \uc2dc\uc791\ud574\uc57c \ud569\ub2c8\ub2e4.", + "unit_of_measurement": "\uce21\uc815 \ub2e8\uc704(\uc120\ud0dd \uc0ac\ud56d)", + "value_template": "\uac12 \ud15c\ud50c\ub9bf(\uc120\ud0dd \uc0ac\ud56d)" + } + } + } + }, + "options": { + "error": { + "db_url_invalid": "\ub370\uc774\ud130\ubca0\uc774\uc2a4 URL\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "step": { + "init": { + "data": { + "name": "\uc774\ub984" + }, + "data_description": { + "column": "\ubc18\ud658\ub41c \ucffc\ub9ac\uac00 \uc0c1\ud0dc\ub85c \ud45c\uc2dc\ub418\ub3c4\ub85d \ud558\ub294 \uc5f4", + "db_url": "\ub370\uc774\ud130\ubca0\uc774\uc2a4 URL, \uae30\ubcf8 HA \ub370\uc774\ud130\ubca0\uc774\uc2a4\ub97c \uc0ac\uc6a9\ud558\ub824\uba74 \ube44\uc6cc \ub461\ub2c8\ub2e4.", + "name": "\ud1b5\ud569\uad6c\uc131\uc694\uc18c \ubc0f \uc13c\uc11c\uc5d0 \uc0ac\uc6a9\ub420 \uc774\ub984", + "query": "\uc2e4\ud589\ud560 \ucffc\ub9ac\ub294 'SELECT'\ub85c \uc2dc\uc791\ud574\uc57c \ud569\ub2c8\ub2e4.", + "unit_of_measurement": "\uce21\uc815 \ub2e8\uc704(\uc120\ud0dd \uc0ac\ud56d)", + "value_template": "\uac12 \ud15c\ud50c\ub9bf(\uc120\ud0dd \uc0ac\ud56d)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/nl.json b/homeassistant/components/sql/translations/nl.json index 68aa1230432..e93b63df296 100644 --- a/homeassistant/components/sql/translations/nl.json +++ b/homeassistant/components/sql/translations/nl.json @@ -5,8 +5,7 @@ }, "error": { "db_url_invalid": "Database URL ongeldig", - "query_invalid": "SQL Query ongeldig", - "value_template_invalid": "Waarde Template ongeldig" + "query_invalid": "SQL Query ongeldig" }, "step": { "user": { @@ -32,8 +31,7 @@ "options": { "error": { "db_url_invalid": "Database URL ongeldig", - "query_invalid": "SQL Query ongeldig", - "value_template_invalid": "Waarde Template ongeldig" + "query_invalid": "SQL Query ongeldig" }, "step": { "init": { diff --git a/homeassistant/components/sql/translations/no.json b/homeassistant/components/sql/translations/no.json index ab2554c4fb9..907a618da75 100644 --- a/homeassistant/components/sql/translations/no.json +++ b/homeassistant/components/sql/translations/no.json @@ -5,8 +5,7 @@ }, "error": { "db_url_invalid": "Database-URL er ugyldig", - "query_invalid": "SQL-sp\u00f8rringen er ugyldig", - "value_template_invalid": "Verdimalen er ugyldig" + "query_invalid": "SQL-sp\u00f8rringen er ugyldig" }, "step": { "user": { @@ -32,8 +31,7 @@ "options": { "error": { "db_url_invalid": "Database-URL er ugyldig", - "query_invalid": "SQL-sp\u00f8rringen er ugyldig", - "value_template_invalid": "Verdimalen er ugyldig" + "query_invalid": "SQL-sp\u00f8rringen er ugyldig" }, "step": { "init": { diff --git a/homeassistant/components/sql/translations/pl.json b/homeassistant/components/sql/translations/pl.json index 2527a8239b3..3f4211c72a5 100644 --- a/homeassistant/components/sql/translations/pl.json +++ b/homeassistant/components/sql/translations/pl.json @@ -5,8 +5,7 @@ }, "error": { "db_url_invalid": "Nieprawid\u0142owy adres URL bazy danych", - "query_invalid": "Nieprawid\u0142owe zapytanie SQL", - "value_template_invalid": "Nieprawid\u0142owy szablon warto\u015bci" + "query_invalid": "Nieprawid\u0142owe zapytanie SQL" }, "step": { "user": { @@ -32,8 +31,7 @@ "options": { "error": { "db_url_invalid": "Nieprawid\u0142owy adres URL bazy danych", - "query_invalid": "Nieprawid\u0142owe zapytanie SQL", - "value_template_invalid": "Nieprawid\u0142owy szablon warto\u015bci" + "query_invalid": "Nieprawid\u0142owe zapytanie SQL" }, "step": { "init": { diff --git a/homeassistant/components/sql/translations/pt-BR.json b/homeassistant/components/sql/translations/pt-BR.json index 1846b8d5488..89231f88f2f 100644 --- a/homeassistant/components/sql/translations/pt-BR.json +++ b/homeassistant/components/sql/translations/pt-BR.json @@ -5,8 +5,7 @@ }, "error": { "db_url_invalid": "URL do banco de dados inv\u00e1lida", - "query_invalid": "Consulta SQL inv\u00e1lida", - "value_template_invalid": "Modelo do valor inv\u00e1lido" + "query_invalid": "Consulta SQL inv\u00e1lida" }, "step": { "user": { @@ -32,8 +31,7 @@ "options": { "error": { "db_url_invalid": "URL do banco de dados inv\u00e1lida", - "query_invalid": "Consulta SQL inv\u00e1lida", - "value_template_invalid": "Modelo do valor inv\u00e1lido" + "query_invalid": "Consulta SQL inv\u00e1lida" }, "step": { "init": { diff --git a/homeassistant/components/sql/translations/ru.json b/homeassistant/components/sql/translations/ru.json index 2d993776fa6..fbc3c00874a 100644 --- a/homeassistant/components/sql/translations/ru.json +++ b/homeassistant/components/sql/translations/ru.json @@ -5,8 +5,7 @@ }, "error": { "db_url_invalid": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445.", - "query_invalid": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 SQL-\u0437\u0430\u043f\u0440\u043e\u0441.", - "value_template_invalid": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u0448\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f." + "query_invalid": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 SQL-\u0437\u0430\u043f\u0440\u043e\u0441." }, "step": { "user": { @@ -32,8 +31,7 @@ "options": { "error": { "db_url_invalid": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445.", - "query_invalid": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 SQL-\u0437\u0430\u043f\u0440\u043e\u0441.", - "value_template_invalid": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 \u0448\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f." + "query_invalid": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 SQL-\u0437\u0430\u043f\u0440\u043e\u0441." }, "step": { "init": { diff --git a/homeassistant/components/sql/translations/tr.json b/homeassistant/components/sql/translations/tr.json index 5757f50cdfb..e37b603162d 100644 --- a/homeassistant/components/sql/translations/tr.json +++ b/homeassistant/components/sql/translations/tr.json @@ -5,8 +5,7 @@ }, "error": { "db_url_invalid": "Veritaban\u0131 URL'si ge\u00e7ersiz", - "query_invalid": "SQL Sorgusu ge\u00e7ersiz", - "value_template_invalid": "De\u011fer \u015eablonu ge\u00e7ersiz" + "query_invalid": "SQL Sorgusu ge\u00e7ersiz" }, "step": { "user": { @@ -32,8 +31,7 @@ "options": { "error": { "db_url_invalid": "Veritaban\u0131 URL'si ge\u00e7ersiz", - "query_invalid": "SQL Sorgusu ge\u00e7ersiz", - "value_template_invalid": "De\u011fer \u015eablonu ge\u00e7ersiz" + "query_invalid": "SQL Sorgusu ge\u00e7ersiz" }, "step": { "init": { diff --git a/homeassistant/components/sql/translations/zh-Hant.json b/homeassistant/components/sql/translations/zh-Hant.json index 43349c39295..3e746b78fff 100644 --- a/homeassistant/components/sql/translations/zh-Hant.json +++ b/homeassistant/components/sql/translations/zh-Hant.json @@ -5,8 +5,7 @@ }, "error": { "db_url_invalid": "\u8cc7\u6599\u5eab URL \u7121\u6548", - "query_invalid": "SQL \u67e5\u8a62\u7121\u6548", - "value_template_invalid": "\u6578\u503c\u6a21\u677f\u7121\u6548" + "query_invalid": "SQL \u67e5\u8a62\u7121\u6548" }, "step": { "user": { @@ -32,8 +31,7 @@ "options": { "error": { "db_url_invalid": "\u8cc7\u6599\u5eab URL \u7121\u6548", - "query_invalid": "SQL \u67e5\u8a62\u7121\u6548", - "value_template_invalid": "\u6578\u503c\u6a21\u677f\u7121\u6548" + "query_invalid": "SQL \u67e5\u8a62\u7121\u6548" }, "step": { "init": { diff --git a/homeassistant/components/steam_online/translations/ko.json b/homeassistant/components/steam_online/translations/ko.json new file mode 100644 index 00000000000..d106b141eb2 --- /dev/null +++ b/homeassistant/components/steam_online/translations/ko.json @@ -0,0 +1,36 @@ +{ + "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_account": "\uc798\ubabb\ub41c \uacc4\uc815 ID", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "reauth_confirm": { + "description": "Steam \ud1b5\ud569\uad6c\uc131\uc694\uc18c\ub294 \uacc4\uc815\uc744 \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c\ud569\ub2c8\ub2e4.\n\n\uc5ec\uae30\uc5d0\uc11c \ud0a4\ub97c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4: {api_key_url}", + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" + }, + "user": { + "data": { + "account": "Steam \uacc4\uc815 ID", + "api_key": "API \ud0a4" + }, + "description": "{account_id_url} \uc5d0\uc11c Steam \uacc4\uc815 ID\ub97c \ucc3e\uc73c\uc138\uc694." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "accounts": "\ubaa8\ub2c8\ud130\ub9c1\ud560 \uacc4\uc815 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sun/translations/ko.json b/homeassistant/components/sun/translations/ko.json index d9d6f6ff081..f918b879acf 100644 --- a/homeassistant/components/sun/translations/ko.json +++ b/homeassistant/components/sun/translations/ko.json @@ -1,4 +1,11 @@ { + "config": { + "step": { + "user": { + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + }, "state": { "_": { "above_horizon": "\uc8fc\uac04", diff --git a/homeassistant/components/switch/translations/ca.json b/homeassistant/components/switch/translations/ca.json index 49667b9f60a..a029be79d38 100644 --- a/homeassistant/components/switch/translations/ca.json +++ b/homeassistant/components/switch/translations/ca.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "init": { - "data": { - "entity_id": "Entitat d'interruptor" - }, - "description": "Selecciona l'interruptor del llum." - } - } - }, "device_automation": { "action_type": { "toggle": "Commuta {entity_name}", @@ -21,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} s'enc\u00e9n o s'apaga", - "toggled": "{entity_name} s'activa o es desactiva", "turned_off": "{entity_name} desactivat", "turned_on": "{entity_name} activat" } diff --git a/homeassistant/components/switch/translations/cs.json b/homeassistant/components/switch/translations/cs.json index a7b7f35033e..b82eaab9e4f 100644 --- a/homeassistant/components/switch/translations/cs.json +++ b/homeassistant/components/switch/translations/cs.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "init": { - "data": { - "entity_id": "Entita vyp\u00edna\u010de" - }, - "description": "Vyberte vyp\u00edna\u010d pro sv\u011btlo." - } - } - }, "device_automation": { "action_type": { "toggle": "P\u0159epnout {entity_name}", @@ -20,7 +10,6 @@ "is_on": "{entity_name} je zapnuto" }, "trigger_type": { - "toggled": "{entity_name} zapnuto nebo vypnuto", "turned_off": "{entity_name} bylo vypnuto", "turned_on": "{entity_name} bylo zapnuto" } diff --git a/homeassistant/components/switch/translations/de.json b/homeassistant/components/switch/translations/de.json index 12784fab206..886426619dc 100644 --- a/homeassistant/components/switch/translations/de.json +++ b/homeassistant/components/switch/translations/de.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "init": { - "data": { - "entity_id": "Switch-Entit\u00e4t" - }, - "description": "W\u00e4hle den Schalter f\u00fcr den Lichtschalter aus." - } - } - }, "device_automation": { "action_type": { "toggle": "{entity_name} umschalten", @@ -21,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} ein- oder ausgeschaltet", - "toggled": "{entity_name} ein- oder ausgeschaltet", "turned_off": "{entity_name} ausgeschaltet", "turned_on": "{entity_name} eingeschaltet" } diff --git a/homeassistant/components/switch/translations/el.json b/homeassistant/components/switch/translations/el.json index 986ee7cff92..280ddddcfed 100644 --- a/homeassistant/components/switch/translations/el.json +++ b/homeassistant/components/switch/translations/el.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "init": { - "data": { - "entity_id": "\u039f\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7" - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7 \u03c6\u03ce\u03c4\u03c9\u03bd." - } - } - }, "device_automation": { "action_type": { "toggle": "\u0395\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae {entity_name}", @@ -21,7 +11,6 @@ }, "trigger_type": { "changed_states": "\u03a4\u03bf {entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5 \u03ae \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", - "toggled": "To {entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5 \u03ae \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", "turned_off": "{entity_name} \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5", "turned_on": "{entity_name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03b8\u03b7\u03ba\u03b5" } diff --git a/homeassistant/components/switch/translations/en.json b/homeassistant/components/switch/translations/en.json index ae04ed42520..1e4fe8e4ad8 100644 --- a/homeassistant/components/switch/translations/en.json +++ b/homeassistant/components/switch/translations/en.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "init": { - "data": { - "entity_id": "Switch entity" - }, - "description": "Select the switch for the light switch." - } - } - }, "device_automation": { "action_type": { "toggle": "Toggle {entity_name}", @@ -21,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} turned on or off", - "toggled": "{entity_name} turned on or off", "turned_off": "{entity_name} turned off", "turned_on": "{entity_name} turned on" } diff --git a/homeassistant/components/switch/translations/es.json b/homeassistant/components/switch/translations/es.json index 2c1f6050ce6..e6190f32c8b 100644 --- a/homeassistant/components/switch/translations/es.json +++ b/homeassistant/components/switch/translations/es.json @@ -1,11 +1,4 @@ { - "config": { - "step": { - "init": { - "description": "Seleccione el interruptor de la l\u00e1mpara." - } - } - }, "device_automation": { "action_type": { "toggle": "Alternar {entity_name}", @@ -18,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} activado o desactivado", - "toggled": "{entity_name} activado o desactivado", "turned_off": "{entity_name} apagado", "turned_on": "{entity_name} encendido" } diff --git a/homeassistant/components/switch/translations/et.json b/homeassistant/components/switch/translations/et.json index e9c9928c89d..aaf1da62b5c 100644 --- a/homeassistant/components/switch/translations/et.json +++ b/homeassistant/components/switch/translations/et.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "init": { - "data": { - "entity_id": "L\u00fcliti olem" - }, - "description": "Vali valgusti l\u00fcliti" - } - } - }, "device_automation": { "action_type": { "toggle": "Muuda {entity_name} olekut", @@ -21,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} l\u00fclitus sisse v\u00f5i v\u00e4lja", - "toggled": "{entity_name} l\u00fclitus sisse v\u00f5i v\u00e4lja", "turned_off": "{entity_name} l\u00fclitus v\u00e4lja", "turned_on": "{entity_name} l\u00fclitus sisse" } diff --git a/homeassistant/components/switch/translations/fr.json b/homeassistant/components/switch/translations/fr.json index ec357e10c72..bbe40ea7732 100644 --- a/homeassistant/components/switch/translations/fr.json +++ b/homeassistant/components/switch/translations/fr.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "init": { - "data": { - "entity_id": "Entit\u00e9 du commutateur" - }, - "description": "S\u00e9lectionnez le commutateur correspondant \u00e0 l'interrupteur d'\u00e9clairage." - } - } - }, "device_automation": { "action_type": { "toggle": "Basculer {entity_name}", @@ -21,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} a \u00e9t\u00e9 activ\u00e9 ou d\u00e9sactiv\u00e9", - "toggled": "{entity_name} a \u00e9t\u00e9 activ\u00e9 ou d\u00e9sactiv\u00e9", "turned_off": "{entity_name} a \u00e9t\u00e9 d\u00e9sactiv\u00e9", "turned_on": "{entity_name} a \u00e9t\u00e9 activ\u00e9" } diff --git a/homeassistant/components/switch/translations/he.json b/homeassistant/components/switch/translations/he.json index c3d76bb6f9f..991f45744ca 100644 --- a/homeassistant/components/switch/translations/he.json +++ b/homeassistant/components/switch/translations/he.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "init": { - "data": { - "entity_id": "\u05d9\u05e9\u05d5\u05ea \u05de\u05ea\u05d2" - }, - "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05de\u05ea\u05d2 \u05e2\u05d1\u05d5\u05e8 \u05de\u05ea\u05d2 \u05d4\u05d0\u05d5\u05e8." - } - } - }, "device_automation": { "action_type": { "toggle": "\u05d4\u05d7\u05dc\u05e4\u05ea \u05de\u05e6\u05d1 {entity_name}", @@ -21,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc \u05d0\u05d5 \u05db\u05d5\u05d1\u05d4", - "toggled": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc \u05d0\u05d5 \u05db\u05d5\u05d1\u05d4", "turned_off": "{entity_name} \u05db\u05d5\u05d1\u05d4", "turned_on": "{entity_name} \u05d4\u05d5\u05e4\u05e2\u05dc" } diff --git a/homeassistant/components/switch/translations/hu.json b/homeassistant/components/switch/translations/hu.json index 2dff00040ca..86d77afaa43 100644 --- a/homeassistant/components/switch/translations/hu.json +++ b/homeassistant/components/switch/translations/hu.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "init": { - "data": { - "entity_id": "Kapcsol\u00f3 entit\u00e1s" - }, - "description": "V\u00e1lassza ki a kapcsol\u00f3t a vil\u00e1g\u00edt\u00e1shoz." - } - } - }, "device_automation": { "action_type": { "toggle": "{entity_name} kapcsol\u00e1sa", @@ -21,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} be- vagy kikapcsolt", - "toggled": "{entity_name} \u00e1tkapcsolt", "turned_off": "{entity_name} ki lett kapcsolva", "turned_on": "{entity_name} be lett kapcsolva" } diff --git a/homeassistant/components/switch/translations/id.json b/homeassistant/components/switch/translations/id.json index c49cab13a9c..589985509d0 100644 --- a/homeassistant/components/switch/translations/id.json +++ b/homeassistant/components/switch/translations/id.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "init": { - "data": { - "entity_id": "Entitas saklar" - }, - "description": "Pilih saklar mana untuk saklar lampu." - } - } - }, "device_automation": { "action_type": { "toggle": "Nyala/matikan {entity_name}", @@ -21,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} diaktifkan atau dinonaktifkan", - "toggled": "{entity_name} diaktifkan atau dinonaktifkan", "turned_off": "{entity_name} dimatikan", "turned_on": "{entity_name} dinyalakan" } diff --git a/homeassistant/components/switch/translations/it.json b/homeassistant/components/switch/translations/it.json index b71a46e48be..6168b6bdf4e 100644 --- a/homeassistant/components/switch/translations/it.json +++ b/homeassistant/components/switch/translations/it.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "init": { - "data": { - "entity_id": "Entit\u00e0 dell'interruttore" - }, - "description": "Seleziona l'interruttore per l'interruttore della luce." - } - } - }, "device_automation": { "action_type": { "toggle": "Attiva/Disattiva {entity_name}", @@ -21,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} attivata o disattivata", - "toggled": "{entity_name} attiva o disattiva", "turned_off": "{entity_name} disattivato", "turned_on": "{entity_name} attivato" } diff --git a/homeassistant/components/switch/translations/ja.json b/homeassistant/components/switch/translations/ja.json index d4e335257ad..071f47dbd58 100644 --- a/homeassistant/components/switch/translations/ja.json +++ b/homeassistant/components/switch/translations/ja.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "init": { - "data": { - "entity_id": "\u30b9\u30a4\u30c3\u30c1\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3" - }, - "description": "\u7167\u660e\u30b9\u30a4\u30c3\u30c1\u306e\u30b9\u30a4\u30c3\u30c1\u3092\u9078\u629e\u3057\u307e\u3059\u3002" - } - } - }, "device_automation": { "action_type": { "toggle": "\u30c8\u30b0\u30eb {entity_name}", @@ -21,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} \u304c\u30aa\u30f3\u307e\u305f\u306f\u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", - "toggled": "{entity_name} \u304c\u30aa\u30f3\u307e\u305f\u306f\u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", "turned_off": "{entity_name} \u30aa\u30d5\u306b\u306a\u308a\u307e\u3057\u305f", "turned_on": "{entity_name} \u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u307e\u3059" } diff --git a/homeassistant/components/switch/translations/nl.json b/homeassistant/components/switch/translations/nl.json index 87636c8a1f1..28398d9d8a6 100644 --- a/homeassistant/components/switch/translations/nl.json +++ b/homeassistant/components/switch/translations/nl.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "init": { - "data": { - "entity_id": "Entiteit wijzigen" - }, - "description": "Kies de schakelaar voor de lichtschakelaar." - } - } - }, "device_automation": { "action_type": { "toggle": "Omschakelen {entity_name}", @@ -21,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} in- of uitgeschakeld", - "toggled": "{entity_name} in-of uitgeschakeld", "turned_off": "{entity_name} uitgeschakeld", "turned_on": "{entity_name} ingeschakeld" } diff --git a/homeassistant/components/switch/translations/no.json b/homeassistant/components/switch/translations/no.json index 802df4e6a42..964723c68b6 100644 --- a/homeassistant/components/switch/translations/no.json +++ b/homeassistant/components/switch/translations/no.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "init": { - "data": { - "entity_id": "Bytt enhet" - }, - "description": "Velg bryteren for lysbryteren." - } - } - }, "device_automation": { "action_type": { "toggle": "Veksle {entity_name}", @@ -21,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} sl\u00e5tt p\u00e5 eller av", - "toggled": "{entity_name} sl\u00e5tt p\u00e5 eller av", "turned_off": "{entity_name} sl\u00e5tt av", "turned_on": "{entity_name} sl\u00e5tt p\u00e5" } diff --git a/homeassistant/components/switch/translations/pl.json b/homeassistant/components/switch/translations/pl.json index 51ecfc44c5d..3d87d8572df 100644 --- a/homeassistant/components/switch/translations/pl.json +++ b/homeassistant/components/switch/translations/pl.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "init": { - "data": { - "entity_id": "Encja prze\u0142\u0105cznika" - }, - "description": "Wybierz prze\u0142\u0105cznik do w\u0142\u0105cznika \u015bwiat\u0142a." - } - } - }, "device_automation": { "action_type": { "toggle": "prze\u0142\u0105cz {entity_name}", @@ -21,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", - "toggled": "{entity_name} zostanie w\u0142\u0105czony lub wy\u0142\u0105czony", "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}" } diff --git a/homeassistant/components/switch/translations/pt-BR.json b/homeassistant/components/switch/translations/pt-BR.json index 0d24cd74bfe..d2c2c3be5a0 100644 --- a/homeassistant/components/switch/translations/pt-BR.json +++ b/homeassistant/components/switch/translations/pt-BR.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "init": { - "data": { - "entity_id": "Entidade de interruptor" - }, - "description": "Selecione o interruptor para a l\u00e2mpada." - } - } - }, "device_automation": { "action_type": { "toggle": "Alternar {entity_name}", @@ -21,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} ligado ou desligado", - "toggled": "{entity_name} ligado ou desligado", "turned_off": "{entity_name} desligado", "turned_on": "{entity_name} ligado" } diff --git a/homeassistant/components/switch/translations/ru.json b/homeassistant/components/switch/translations/ru.json index 2602a1ac20b..a1483e071e8 100644 --- a/homeassistant/components/switch/translations/ru.json +++ b/homeassistant/components/switch/translations/ru.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "init": { - "data": { - "entity_id": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0435\u0433\u043e \u043a\u0430\u043a \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f." - } - } - }, "device_automation": { "action_type": { "toggle": "{entity_name}: \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c", @@ -21,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0438\u043b\u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", - "toggled": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0438\u043b\u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" } diff --git a/homeassistant/components/switch/translations/sv.json b/homeassistant/components/switch/translations/sv.json index 2db2a2718e0..a2cd74434eb 100644 --- a/homeassistant/components/switch/translations/sv.json +++ b/homeassistant/components/switch/translations/sv.json @@ -10,7 +10,6 @@ "is_on": "{entity_name} \u00e4r p\u00e5" }, "trigger_type": { - "toggled": "{entity_name} slogs p\u00e5 eller av", "turned_off": "{entity_name} st\u00e4ngdes av", "turned_on": "{entity_name} slogs p\u00e5" } diff --git a/homeassistant/components/switch/translations/tr.json b/homeassistant/components/switch/translations/tr.json index 2a98d2e972f..8387983017f 100644 --- a/homeassistant/components/switch/translations/tr.json +++ b/homeassistant/components/switch/translations/tr.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "init": { - "data": { - "entity_id": "Varl\u0131\u011f\u0131 de\u011fi\u015ftir" - }, - "description": "I\u015f\u0131k anahtar\u0131 i\u00e7in anahtar\u0131 se\u00e7in." - } - } - }, "device_automation": { "action_type": { "toggle": "{entity_name} de\u011fi\u015ftir", @@ -21,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name} a\u00e7\u0131ld\u0131 veya kapat\u0131ld\u0131", - "toggled": "{entity_name} a\u00e7\u0131ld\u0131 veya kapat\u0131ld\u0131", "turned_off": "{entity_name} kapat\u0131ld\u0131", "turned_on": "{entity_name} a\u00e7\u0131ld\u0131" } diff --git a/homeassistant/components/switch/translations/zh-Hans.json b/homeassistant/components/switch/translations/zh-Hans.json index 822ca2795df..afe9f58db8f 100644 --- a/homeassistant/components/switch/translations/zh-Hans.json +++ b/homeassistant/components/switch/translations/zh-Hans.json @@ -10,7 +10,6 @@ "is_on": "{entity_name} \u5df2\u6253\u5f00" }, "trigger_type": { - "toggled": "{entity_name} \u88ab\u6253\u5f00\u6216\u5173\u95ed", "turned_off": "{entity_name} \u88ab\u5173\u95ed", "turned_on": "{entity_name} \u88ab\u6253\u5f00" } diff --git a/homeassistant/components/switch/translations/zh-Hant.json b/homeassistant/components/switch/translations/zh-Hant.json index 631db326ae0..a1f38544f67 100644 --- a/homeassistant/components/switch/translations/zh-Hant.json +++ b/homeassistant/components/switch/translations/zh-Hant.json @@ -1,14 +1,4 @@ { - "config": { - "step": { - "init": { - "data": { - "entity_id": "\u958b\u95dc\u5be6\u9ad4" - }, - "description": "\u9078\u64c7\u6307\u5b9a\u70ba\u71c8\u5149\u4e4b\u958b\u95dc\u3002" - } - } - }, "device_automation": { "action_type": { "toggle": "\u5207\u63db{entity_name}", @@ -21,7 +11,6 @@ }, "trigger_type": { "changed_states": "{entity_name}\u5df2\u958b\u555f\u6216\u95dc\u9589", - "toggled": "{entity_name}\u5df2\u958b\u555f\u6216\u95dc\u9589", "turned_off": "{entity_name}\u5df2\u95dc\u9589", "turned_on": "{entity_name}\u5df2\u958b\u555f" } diff --git a/homeassistant/components/switch_as_x/translations/ca.json b/homeassistant/components/switch_as_x/translations/ca.json index f48a744066c..ea568634dc1 100644 --- a/homeassistant/components/switch_as_x/translations/ca.json +++ b/homeassistant/components/switch_as_x/translations/ca.json @@ -1,18 +1,12 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "Entitat d'interruptor" - } - }, "user": { "data": { "entity_id": "Commutador", "target_domain": "Nou tipus" }, - "description": "Tria un commutador que vulguis que aparegui a Home Assistant com a llum, coberta o qualsevol altra cosa. El commutador original s'amagar\u00e0.", - "title": "Canvia el tipus de dispositiu commutador" + "description": "Tria un commutador que vulguis que aparegui a Home Assistant com a llum, coberta o qualsevol altra cosa. El commutador original s'amagar\u00e0." } } }, diff --git a/homeassistant/components/switch_as_x/translations/cs.json b/homeassistant/components/switch_as_x/translations/cs.json index 11f8471a62a..c521a79e1d9 100644 --- a/homeassistant/components/switch_as_x/translations/cs.json +++ b/homeassistant/components/switch_as_x/translations/cs.json @@ -1,16 +1,10 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "Entita vyp\u00edna\u010de" - } - }, "user": { "data": { "target_domain": "Typ" - }, - "title": "Ud\u011blejte vyp\u00edna\u010dem ..." + } } } }, diff --git a/homeassistant/components/switch_as_x/translations/de.json b/homeassistant/components/switch_as_x/translations/de.json index 1125f4f5a8f..6278ddc9988 100644 --- a/homeassistant/components/switch_as_x/translations/de.json +++ b/homeassistant/components/switch_as_x/translations/de.json @@ -1,18 +1,12 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "Switch-Entit\u00e4t" - } - }, "user": { "data": { "entity_id": "Schalter", "target_domain": "Neuer Typ" }, - "description": "W\u00e4hle einen Schalter, der im Home Assistant als Licht, Abdeckung oder sonstiges angezeigt werden soll. Der urspr\u00fcngliche Schalter wird ausgeblendet.", - "title": "Switch-Ger\u00e4tetyp \u00e4ndern" + "description": "W\u00e4hle einen Schalter, der im Home Assistant als Licht, Abdeckung oder sonstiges angezeigt werden soll. Der urspr\u00fcngliche Schalter wird ausgeblendet." } } }, diff --git a/homeassistant/components/switch_as_x/translations/el.json b/homeassistant/components/switch_as_x/translations/el.json index ed10f241927..cf5925abc35 100644 --- a/homeassistant/components/switch_as_x/translations/el.json +++ b/homeassistant/components/switch_as_x/translations/el.json @@ -1,18 +1,12 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "\u039f\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7" - } - }, "user": { "data": { "entity_id": "\u0394\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7\u03c2", "target_domain": "\u03a4\u03cd\u03c0\u03bf\u03c2" }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant \u03c9\u03c2 \u03c6\u03c9\u03c2, \u03ba\u03ac\u03bb\u03c5\u03bc\u03bc\u03b1 \u03ae \u03bf\u03c4\u03b9\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5 \u03ac\u03bb\u03bb\u03bf. \u039f \u03b1\u03c1\u03c7\u03b9\u03ba\u03cc\u03c2 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7\u03c2 \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03c1\u03c5\u03bc\u03bc\u03ad\u03bd\u03bf\u03c2.", - "title": "\u039a\u03ac\u03bd\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7 \u03ad\u03bd\u03b1 ..." + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant \u03c9\u03c2 \u03c6\u03c9\u03c2, \u03ba\u03ac\u03bb\u03c5\u03bc\u03bc\u03b1 \u03ae \u03bf\u03c4\u03b9\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5 \u03ac\u03bb\u03bb\u03bf. \u039f \u03b1\u03c1\u03c7\u03b9\u03ba\u03cc\u03c2 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7\u03c2 \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03c1\u03c5\u03bc\u03bc\u03ad\u03bd\u03bf\u03c2." } } }, diff --git a/homeassistant/components/switch_as_x/translations/en.json b/homeassistant/components/switch_as_x/translations/en.json index 4253f0506ef..7709a27cf35 100644 --- a/homeassistant/components/switch_as_x/translations/en.json +++ b/homeassistant/components/switch_as_x/translations/en.json @@ -1,18 +1,12 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "Switch entity" - } - }, "user": { "data": { "entity_id": "Switch", "target_domain": "New Type" }, - "description": "Pick a switch that you want to show up in Home Assistant as a light, cover or anything else. The original switch will be hidden.", - "title": "Change switch device type" + "description": "Pick a switch that you want to show up in Home Assistant as a light, cover or anything else. The original switch will be hidden." } } }, diff --git a/homeassistant/components/switch_as_x/translations/et.json b/homeassistant/components/switch_as_x/translations/et.json index 9d5a5839fbf..bb3ee78e75c 100644 --- a/homeassistant/components/switch_as_x/translations/et.json +++ b/homeassistant/components/switch_as_x/translations/et.json @@ -1,18 +1,12 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "L\u00fcliti olem" - } - }, "user": { "data": { "entity_id": "L\u00fcliti", "target_domain": "Uus t\u00fc\u00fcp" }, - "description": "Vali l\u00fcliti mida soovid Home Assistantis valgustina, avakattena v\u00f5i millegi muuna kuvada. Algne l\u00fcliti peidetakse.", - "title": "Muuda l\u00fclitusseadme t\u00fc\u00fcpi" + "description": "Vali l\u00fcliti mida soovid Home Assistantis valgustina, avakattena v\u00f5i millegi muuna kuvada. Algne l\u00fcliti peidetakse." } } }, diff --git a/homeassistant/components/switch_as_x/translations/fr.json b/homeassistant/components/switch_as_x/translations/fr.json index fa14bd80885..484881e222a 100644 --- a/homeassistant/components/switch_as_x/translations/fr.json +++ b/homeassistant/components/switch_as_x/translations/fr.json @@ -1,18 +1,12 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "Entit\u00e9 du commutateur" - } - }, "user": { "data": { "entity_id": "Interrupteur", "target_domain": "Nouveau type" }, - "description": "Choisissez un interrupteur que vous voulez faire appara\u00eetre dans Home Assistant comme une lumi\u00e8re, une fermeture ou autre. L'interrupteur original sera cach\u00e9.", - "title": "Modifier le type d'appareil de l'interrupteur" + "description": "Choisissez un interrupteur que vous voulez faire appara\u00eetre dans Home Assistant comme une lumi\u00e8re, une fermeture ou autre. L'interrupteur original sera cach\u00e9." } } }, diff --git a/homeassistant/components/switch_as_x/translations/he.json b/homeassistant/components/switch_as_x/translations/he.json deleted file mode 100644 index 39889dab709..00000000000 --- a/homeassistant/components/switch_as_x/translations/he.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "config": { - "user": { - "entity_id": "\u05d9\u05e9\u05d5\u05ea \u05de\u05ea\u05d2" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/hu.json b/homeassistant/components/switch_as_x/translations/hu.json index 0d919f5ecd3..9f3d49ec348 100644 --- a/homeassistant/components/switch_as_x/translations/hu.json +++ b/homeassistant/components/switch_as_x/translations/hu.json @@ -1,18 +1,12 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "Kapcsol\u00f3 entit\u00e1s" - } - }, "user": { "data": { "entity_id": "Kapcsol\u00f3", "target_domain": "\u00daj t\u00edpus" }, - "description": "V\u00e1lassza ki azt a kapcsol\u00f3t, amelyet meg szeretne jelen\u00edteni a Home Assistantban l\u00e1mpak\u00e9nt, red\u0151nyk\u00e9nt vagy b\u00e1rmi m\u00e1sk\u00e9nt. Az eredeti kapcsol\u00f3 el lesz rejtve.", - "title": "Kapcsol\u00f3 eszk\u00f6zt\u00edpus m\u00f3dos\u00edt\u00e1sa" + "description": "V\u00e1lassza ki azt a kapcsol\u00f3t, amelyet meg szeretne jelen\u00edteni a Home Assistantban l\u00e1mpak\u00e9nt, red\u0151nyk\u00e9nt vagy b\u00e1rmi m\u00e1sk\u00e9nt. Az eredeti kapcsol\u00f3 el lesz rejtve." } } }, diff --git a/homeassistant/components/switch_as_x/translations/id.json b/homeassistant/components/switch_as_x/translations/id.json index 47d31e26c03..e8e86db884f 100644 --- a/homeassistant/components/switch_as_x/translations/id.json +++ b/homeassistant/components/switch_as_x/translations/id.json @@ -1,18 +1,12 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "Entitas saklar" - } - }, "user": { "data": { "entity_id": "Sakelar", "target_domain": "Tipe Baru" }, - "description": "Pilih sakelar yang ingin Anda tampilkan di Home Assistant sebagai lampu, penutup, atau apa pun. Sakelar asli akan disembunyikan.", - "title": "Ubah jenis perangkat sakelar" + "description": "Pilih sakelar yang ingin Anda tampilkan di Home Assistant sebagai lampu, penutup, atau apa pun. Sakelar asli akan disembunyikan." } } }, diff --git a/homeassistant/components/switch_as_x/translations/it.json b/homeassistant/components/switch_as_x/translations/it.json index 4706b4d9ece..9d7f3c798bf 100644 --- a/homeassistant/components/switch_as_x/translations/it.json +++ b/homeassistant/components/switch_as_x/translations/it.json @@ -1,18 +1,12 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "Cambia entit\u00e0" - } - }, "user": { "data": { "entity_id": "Interruttore", "target_domain": "Nuovo tipo" }, - "description": "Scegli un interruttore che vuoi mostrare in Home Assistant come luce, copertura o qualsiasi altra cosa. L'interruttore originale sar\u00e0 nascosto.", - "title": "Cambia tipo di dispositivo dell'interruttore" + "description": "Scegli un interruttore che vuoi mostrare in Home Assistant come luce, copertura o qualsiasi altra cosa. L'interruttore originale sar\u00e0 nascosto." } } }, diff --git a/homeassistant/components/switch_as_x/translations/ja.json b/homeassistant/components/switch_as_x/translations/ja.json index 434e1b588d5..41af80fa2b8 100644 --- a/homeassistant/components/switch_as_x/translations/ja.json +++ b/homeassistant/components/switch_as_x/translations/ja.json @@ -1,18 +1,12 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "\u30b9\u30a4\u30c3\u30c1\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3" - } - }, "user": { "data": { "entity_id": "\u30b9\u30a4\u30c3\u30c1", "target_domain": "\u30bf\u30a4\u30d7" }, - "description": "Home Assistant\u306b\u3001\u30e9\u30a4\u30c8\u3084\u30ab\u30d0\u30fc\u306a\u3069\u306e\u8868\u793a\u3055\u305b\u305f\u3044\u30b9\u30a4\u30c3\u30c1\u3092\u9078\u3073\u307e\u3059\u3002\u5143\u306e\u30b9\u30a4\u30c3\u30c1\u306f\u975e\u8868\u793a\u306b\u306a\u308a\u307e\u3059\u3002", - "title": "\u30b9\u30a4\u30c3\u30c1\u3092..." + "description": "Home Assistant\u306b\u3001\u30e9\u30a4\u30c8\u3084\u30ab\u30d0\u30fc\u306a\u3069\u306e\u8868\u793a\u3055\u305b\u305f\u3044\u30b9\u30a4\u30c3\u30c1\u3092\u9078\u3073\u307e\u3059\u3002\u5143\u306e\u30b9\u30a4\u30c3\u30c1\u306f\u975e\u8868\u793a\u306b\u306a\u308a\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/switch_as_x/translations/ko.json b/homeassistant/components/switch_as_x/translations/ko.json new file mode 100644 index 00000000000..bd0b909895d --- /dev/null +++ b/homeassistant/components/switch_as_x/translations/ko.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "entity_id": "\uc2a4\uc704\uce58", + "target_domain": "\uc0c8\ub85c\uc6b4 \uc720\ud615" + }, + "description": "Home Assistant\uc5d0\uc11c \uc870\uba85, \ucee4\ubc84 \ub610\ub294 \ub2e4\ub978 \uac83\uc73c\ub85c \ud45c\uc2dc\ud558\ub824\ub294 \uc2a4\uc704\uce58\ub97c \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624. \uc6d0\ub798 \uc2a4\uc704\uce58\ub294 \uc228\uaca8\uc9d1\ub2c8\ub2e4." + } + } + }, + "title": "\uc2a4\uc704\uce58\uc758 \uc7a5\uce58 \uc720\ud615 \ubcc0\uacbd" +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/nl.json b/homeassistant/components/switch_as_x/translations/nl.json index 4fcea818b9a..2a6043fbe75 100644 --- a/homeassistant/components/switch_as_x/translations/nl.json +++ b/homeassistant/components/switch_as_x/translations/nl.json @@ -1,18 +1,12 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "Entiteit wijzigen" - } - }, "user": { "data": { "entity_id": "Schakelaar", "target_domain": "Nieuw type" }, - "description": "Kies een schakelaar die u in Home Assistant wilt laten verschijnen als licht, klep of iets anders. De oorspronkelijke schakelaar wordt verborgen.", - "title": "Type schakelapparaat wijzigen" + "description": "Kies een schakelaar die u in Home Assistant wilt laten verschijnen als licht, klep of iets anders. De oorspronkelijke schakelaar wordt verborgen." } } }, diff --git a/homeassistant/components/switch_as_x/translations/no.json b/homeassistant/components/switch_as_x/translations/no.json index 5958c87f201..6c72f96873e 100644 --- a/homeassistant/components/switch_as_x/translations/no.json +++ b/homeassistant/components/switch_as_x/translations/no.json @@ -1,18 +1,12 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "Bytt enhet" - } - }, "user": { "data": { "entity_id": "Bryter", "target_domain": "Ny type" }, - "description": "Velg en bryter du vil vise i Home Assistant som lys, deksel eller noe annet. Den opprinnelige bryteren vil v\u00e6re skjult.", - "title": "Endre bryterenhetstype" + "description": "Velg en bryter du vil vise i Home Assistant som lys, deksel eller noe annet. Den opprinnelige bryteren vil v\u00e6re skjult." } } }, diff --git a/homeassistant/components/switch_as_x/translations/pl.json b/homeassistant/components/switch_as_x/translations/pl.json index 53a81fec1a5..92e642e746e 100644 --- a/homeassistant/components/switch_as_x/translations/pl.json +++ b/homeassistant/components/switch_as_x/translations/pl.json @@ -1,18 +1,12 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "Encja prze\u0142\u0105cznika" - } - }, "user": { "data": { "entity_id": "Prze\u0142\u0105cznik", "target_domain": "Nowy rodzaj" }, - "description": "Wybierz prze\u0142\u0105cznik, kt\u00f3ry chcesz pokaza\u0107 w Home Assistant jako \u015bwiat\u0142o, rolet\u0119 lub cokolwiek innego. Oryginalny prze\u0142\u0105cznik zostanie ukryty.", - "title": "Zmiana typu urz\u0105dzenia prze\u0142\u0105cznika" + "description": "Wybierz prze\u0142\u0105cznik, kt\u00f3ry chcesz pokaza\u0107 w Home Assistant jako \u015bwiat\u0142o, rolet\u0119 lub cokolwiek innego. Oryginalny prze\u0142\u0105cznik zostanie ukryty." } } }, diff --git a/homeassistant/components/switch_as_x/translations/pt-BR.json b/homeassistant/components/switch_as_x/translations/pt-BR.json index bf8bc276781..436a99a17f9 100644 --- a/homeassistant/components/switch_as_x/translations/pt-BR.json +++ b/homeassistant/components/switch_as_x/translations/pt-BR.json @@ -1,18 +1,12 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "Entidade de interruptor" - } - }, "user": { "data": { "entity_id": "Interruptor", "target_domain": "Novo tipo" }, - "description": "Escolha um interruptor que voc\u00ea deseja que apare\u00e7a no Home Assistant como luz, cortina ou qualquer outra coisa. A op\u00e7\u00e3o original ficar\u00e1 oculta.", - "title": "Alterar o tipo de dispositivo do interruptor" + "description": "Escolha um interruptor que voc\u00ea deseja que apare\u00e7a no Home Assistant como luz, cortina ou qualquer outra coisa. A op\u00e7\u00e3o original ficar\u00e1 oculta." } } }, diff --git a/homeassistant/components/switch_as_x/translations/ru.json b/homeassistant/components/switch_as_x/translations/ru.json index 3074365dd76..731a5c31c3c 100644 --- a/homeassistant/components/switch_as_x/translations/ru.json +++ b/homeassistant/components/switch_as_x/translations/ru.json @@ -1,18 +1,12 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c" - } - }, "user": { "data": { "entity_id": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c", "target_domain": "\u041d\u043e\u0432\u044b\u0439 \u0442\u0438\u043f" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u043d\u043e \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0432 Home Assistant \u043a\u0430\u043a \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u0435, \u0448\u0442\u043e\u0440\u044b \u0438\u043b\u0438 \u0447\u0442\u043e-\u0442\u043e \u0435\u0449\u0451. \u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u0431\u0443\u0434\u0435\u0442 \u0441\u043a\u0440\u044b\u0442.", - "title": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0442\u0438\u043f \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044f" + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u043d\u043e \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0432 Home Assistant \u043a\u0430\u043a \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u0435, \u0448\u0442\u043e\u0440\u044b \u0438\u043b\u0438 \u0447\u0442\u043e-\u0442\u043e \u0435\u0449\u0451. \u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u0431\u0443\u0434\u0435\u0442 \u0441\u043a\u0440\u044b\u0442." } } }, diff --git a/homeassistant/components/switch_as_x/translations/sv.json b/homeassistant/components/switch_as_x/translations/sv.json index 96419c39bf1..d21eefa5a1a 100644 --- a/homeassistant/components/switch_as_x/translations/sv.json +++ b/homeassistant/components/switch_as_x/translations/sv.json @@ -1,16 +1,10 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "Kontakt-entitet" - } - }, "user": { "data": { "target_domain": "Typ" - }, - "title": "G\u00f6r en kontakt till ..." + } } } }, diff --git a/homeassistant/components/switch_as_x/translations/tr.json b/homeassistant/components/switch_as_x/translations/tr.json index b793be6baf0..39fd95784a4 100644 --- a/homeassistant/components/switch_as_x/translations/tr.json +++ b/homeassistant/components/switch_as_x/translations/tr.json @@ -1,18 +1,12 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "Varl\u0131\u011f\u0131 de\u011fi\u015ftir" - } - }, "user": { "data": { "entity_id": "Anahtar", "target_domain": "Yeni T\u00fcr" }, - "description": "Home Assistant'ta \u0131\u015f\u0131k, \u00f6rt\u00fc veya ba\u015fka bir \u015fey olarak g\u00f6r\u00fcnmesini istedi\u011finiz bir anahtar se\u00e7in. Orijinal anahtar gizlenecektir.", - "title": "Anahtar cihaz t\u00fcr\u00fcn\u00fc de\u011fi\u015ftir" + "description": "Home Assistant'ta \u0131\u015f\u0131k, \u00f6rt\u00fc veya ba\u015fka bir \u015fey olarak g\u00f6r\u00fcnmesini istedi\u011finiz bir anahtar se\u00e7in. Orijinal anahtar gizlenecektir." } } }, diff --git a/homeassistant/components/switch_as_x/translations/zh-Hans.json b/homeassistant/components/switch_as_x/translations/zh-Hans.json index e765a436849..244fab0c4e9 100644 --- a/homeassistant/components/switch_as_x/translations/zh-Hans.json +++ b/homeassistant/components/switch_as_x/translations/zh-Hans.json @@ -1,18 +1,12 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "\u5f00\u5173\u5b9e\u4f53" - } - }, "user": { "data": { "entity_id": "\u5f00\u5173", "target_domain": "\u65b0\u7c7b\u578b" }, - "description": "\u9009\u62e9\u4e00\u4e2a\u5f00\u5173\uff0c\u8ba9\u5b83\u5728 Home Assistant \u4e2d\u663e\u793a\u4e3a\u706f\u3001\u5377\u5e18\u7b49\u5404\u79cd\u7c7b\u578b\u3002\u539f\u6765\u7684\u5f00\u5173\u5c06\u88ab\u9690\u85cf\u3002", - "title": "\u66f4\u6539\u5f00\u5173\u7684\u8bbe\u5907\u7c7b\u578b" + "description": "\u9009\u62e9\u4e00\u4e2a\u5f00\u5173\uff0c\u8ba9\u5b83\u5728 Home Assistant \u4e2d\u663e\u793a\u4e3a\u706f\u3001\u5377\u5e18\u7b49\u5404\u79cd\u7c7b\u578b\u3002\u539f\u6765\u7684\u5f00\u5173\u5c06\u88ab\u9690\u85cf\u3002" } } }, diff --git a/homeassistant/components/switch_as_x/translations/zh-Hant.json b/homeassistant/components/switch_as_x/translations/zh-Hant.json index bd6a1e15ba0..17813905e2a 100644 --- a/homeassistant/components/switch_as_x/translations/zh-Hant.json +++ b/homeassistant/components/switch_as_x/translations/zh-Hant.json @@ -1,18 +1,12 @@ { "config": { "step": { - "config": { - "user": { - "entity_id": "\u958b\u95dc\u5be6\u9ad4" - } - }, "user": { "data": { "entity_id": "\u958b\u95dc", "target_domain": "\u65b0\u589e\u985e\u5225" }, - "description": "\u9078\u64c7\u6240\u8981\u65bc Home Assistant \u4e2d\u986f\u793a\u70ba\u71c8\u5149\u7684\u958b\u95dc\u3001\u7a97\u7c3e\u6216\u5176\u4ed6\u5be6\u9ad4\u3002\u539f\u59cb\u958b\u95dc\u5c07\u6703\u9032\u884c\u96b1\u85cf\u3002", - "title": "\u8b8a\u66f4\u958b\u95dc\u985e\u5225" + "description": "\u9078\u64c7\u6240\u8981\u65bc Home Assistant \u4e2d\u986f\u793a\u70ba\u71c8\u5149\u7684\u958b\u95dc\u3001\u7a97\u7c3e\u6216\u5176\u4ed6\u5be6\u9ad4\u3002\u539f\u59cb\u958b\u95dc\u5c07\u6703\u9032\u884c\u96b1\u85cf\u3002" } } }, diff --git a/homeassistant/components/switchbot/translations/bg.json b/homeassistant/components/switchbot/translations/bg.json index 5a5b74b2318..05b3a4459bb 100644 --- a/homeassistant/components/switchbot/translations/bg.json +++ b/homeassistant/components/switchbot/translations/bg.json @@ -6,9 +6,6 @@ "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": { diff --git a/homeassistant/components/switchbot/translations/ca.json b/homeassistant/components/switchbot/translations/ca.json index 6409efcbab7..576aeda3b16 100644 --- a/homeassistant/components/switchbot/translations/ca.json +++ b/homeassistant/components/switchbot/translations/ca.json @@ -7,9 +7,6 @@ "switchbot_unsupported_type": "Tipus de Switchbot no compatible.", "unknown": "Error inesperat" }, - "error": { - "cannot_connect": "Ha fallat la connexi\u00f3" - }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/switchbot/translations/cs.json b/homeassistant/components/switchbot/translations/cs.json index 6f16b6faac5..f92951a0e15 100644 --- a/homeassistant/components/switchbot/translations/cs.json +++ b/homeassistant/components/switchbot/translations/cs.json @@ -5,9 +5,6 @@ "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": { diff --git a/homeassistant/components/switchbot/translations/de.json b/homeassistant/components/switchbot/translations/de.json index f499712718e..439524c8aa6 100644 --- a/homeassistant/components/switchbot/translations/de.json +++ b/homeassistant/components/switchbot/translations/de.json @@ -7,9 +7,6 @@ "switchbot_unsupported_type": "Nicht unterst\u00fctzter Switchbot-Typ.", "unknown": "Unerwarteter Fehler" }, - "error": { - "cannot_connect": "Verbindung fehlgeschlagen" - }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/switchbot/translations/el.json b/homeassistant/components/switchbot/translations/el.json index fe3b7448ac8..b191c90f53c 100644 --- a/homeassistant/components/switchbot/translations/el.json +++ b/homeassistant/components/switchbot/translations/el.json @@ -7,9 +7,6 @@ "switchbot_unsupported_type": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf\u03c2 \u03c4\u03cd\u03c0\u03bf\u03c2 Switchbot.", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, - "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" - }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index 4ea3d21de65..1cfaee8750f 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -7,9 +7,6 @@ "switchbot_unsupported_type": "Unsupported Switchbot Type.", "unknown": "Unexpected error" }, - "error": { - "cannot_connect": "Failed to connect" - }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/switchbot/translations/es.json b/homeassistant/components/switchbot/translations/es.json index 324c7200244..6fc4d59f693 100644 --- a/homeassistant/components/switchbot/translations/es.json +++ b/homeassistant/components/switchbot/translations/es.json @@ -7,9 +7,6 @@ "switchbot_unsupported_type": "Tipo de Switchbot no compatible.", "unknown": "Error inesperado" }, - "error": { - "cannot_connect": "Fall\u00f3 al conectar" - }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/switchbot/translations/et.json b/homeassistant/components/switchbot/translations/et.json index cc746796195..358a4748724 100644 --- a/homeassistant/components/switchbot/translations/et.json +++ b/homeassistant/components/switchbot/translations/et.json @@ -7,9 +7,6 @@ "switchbot_unsupported_type": "Toetamata Switchboti t\u00fc\u00fcp.", "unknown": "Ootamatu t\u00f5rge" }, - "error": { - "cannot_connect": "\u00dchendamine nurjus" - }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/switchbot/translations/fr.json b/homeassistant/components/switchbot/translations/fr.json index aca51b4e4c4..75eff0a9b7c 100644 --- a/homeassistant/components/switchbot/translations/fr.json +++ b/homeassistant/components/switchbot/translations/fr.json @@ -7,9 +7,6 @@ "switchbot_unsupported_type": "Type Switchbot non pris en charge.", "unknown": "Erreur inattendue" }, - "error": { - "cannot_connect": "\u00c9chec de connexion" - }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/switchbot/translations/he.json b/homeassistant/components/switchbot/translations/he.json index 09f62069706..836cd8b06b4 100644 --- a/homeassistant/components/switchbot/translations/he.json +++ b/homeassistant/components/switchbot/translations/he.json @@ -5,9 +5,6 @@ "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": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" - }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/switchbot/translations/hu.json b/homeassistant/components/switchbot/translations/hu.json index 2b80fbedbd8..b870e577426 100644 --- a/homeassistant/components/switchbot/translations/hu.json +++ b/homeassistant/components/switchbot/translations/hu.json @@ -8,7 +8,6 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s", "one": "\u00dcres", "other": "\u00dcres" }, diff --git a/homeassistant/components/switchbot/translations/id.json b/homeassistant/components/switchbot/translations/id.json index 4619f421811..f3a9cd169ef 100644 --- a/homeassistant/components/switchbot/translations/id.json +++ b/homeassistant/components/switchbot/translations/id.json @@ -7,9 +7,6 @@ "switchbot_unsupported_type": "Jenis Switchbot yang tidak didukung.", "unknown": "Kesalahan yang tidak diharapkan" }, - "error": { - "cannot_connect": "Gagal terhubung" - }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/switchbot/translations/it.json b/homeassistant/components/switchbot/translations/it.json index f589046d4db..b8997f9247b 100644 --- a/homeassistant/components/switchbot/translations/it.json +++ b/homeassistant/components/switchbot/translations/it.json @@ -8,7 +8,6 @@ "unknown": "Errore imprevisto" }, "error": { - "cannot_connect": "Impossibile connettersi", "one": "Vuoto", "other": "Vuoti" }, diff --git a/homeassistant/components/switchbot/translations/ja.json b/homeassistant/components/switchbot/translations/ja.json index 41fb320428f..91d87431774 100644 --- a/homeassistant/components/switchbot/translations/ja.json +++ b/homeassistant/components/switchbot/translations/ja.json @@ -7,9 +7,6 @@ "switchbot_unsupported_type": "\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u306a\u3044\u7a2e\u985e\u306eSwitchbot", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, - "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" - }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/switchbot/translations/nl.json b/homeassistant/components/switchbot/translations/nl.json index fb1e55f6b9d..becb7173194 100644 --- a/homeassistant/components/switchbot/translations/nl.json +++ b/homeassistant/components/switchbot/translations/nl.json @@ -7,9 +7,6 @@ "switchbot_unsupported_type": "Niet-ondersteund Switchbot-type.", "unknown": "Onverwachte fout" }, - "error": { - "cannot_connect": "Kan geen verbinding maken" - }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/switchbot/translations/no.json b/homeassistant/components/switchbot/translations/no.json index 4d8cb95061a..1d7836f6776 100644 --- a/homeassistant/components/switchbot/translations/no.json +++ b/homeassistant/components/switchbot/translations/no.json @@ -7,9 +7,6 @@ "switchbot_unsupported_type": "Switchbot-type st\u00f8ttes ikke.", "unknown": "Uventet feil" }, - "error": { - "cannot_connect": "Tilkobling mislyktes" - }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/switchbot/translations/pl.json b/homeassistant/components/switchbot/translations/pl.json index 15e64b86e5a..156ddbb9924 100644 --- a/homeassistant/components/switchbot/translations/pl.json +++ b/homeassistant/components/switchbot/translations/pl.json @@ -8,7 +8,6 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "few": "Puste", "many": "Pustych", "one": "Pusty", diff --git a/homeassistant/components/switchbot/translations/pt-BR.json b/homeassistant/components/switchbot/translations/pt-BR.json index bf9cb746dcb..3959425cbd3 100644 --- a/homeassistant/components/switchbot/translations/pt-BR.json +++ b/homeassistant/components/switchbot/translations/pt-BR.json @@ -7,9 +7,6 @@ "switchbot_unsupported_type": "Tipo de Switchbot sem suporte.", "unknown": "Erro inesperado" }, - "error": { - "cannot_connect": "Falha ao conectar" - }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/switchbot/translations/ru.json b/homeassistant/components/switchbot/translations/ru.json index 5eaa1cdbc4f..9ca076ff499 100644 --- a/homeassistant/components/switchbot/translations/ru.json +++ b/homeassistant/components/switchbot/translations/ru.json @@ -7,9 +7,6 @@ "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": { diff --git a/homeassistant/components/switchbot/translations/tr.json b/homeassistant/components/switchbot/translations/tr.json index 77a2921fa38..40b80dc4a3c 100644 --- a/homeassistant/components/switchbot/translations/tr.json +++ b/homeassistant/components/switchbot/translations/tr.json @@ -8,7 +8,6 @@ "unknown": "Beklenmeyen hata" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131", "one": "Bo\u015f", "other": "Bo\u015f" }, diff --git a/homeassistant/components/switchbot/translations/zh-Hant.json b/homeassistant/components/switchbot/translations/zh-Hant.json index 8e7b4495328..617129167ed 100644 --- a/homeassistant/components/switchbot/translations/zh-Hant.json +++ b/homeassistant/components/switchbot/translations/zh-Hant.json @@ -7,9 +7,6 @@ "switchbot_unsupported_type": "\u4e0d\u652f\u6301\u7684 Switchbot \u985e\u5225\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557" - }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/switcher_kis/translations/ko.json b/homeassistant/components/switcher_kis/translations/ko.json new file mode 100644 index 00000000000..20ad990e862 --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/ko.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/bg.json b/homeassistant/components/synology_dsm/translations/bg.json index acf35c4c2a4..a3a107a36e2 100644 --- a/homeassistant/components/synology_dsm/translations/bg.json +++ b/homeassistant/components/synology_dsm/translations/bg.json @@ -22,15 +22,7 @@ "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": { - "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}" + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name} ({host})?" }, "reauth_confirm": { "data": { @@ -44,8 +36,7 @@ "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" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/ca.json b/homeassistant/components/synology_dsm/translations/ca.json index bad95d30836..110e1cabdae 100644 --- a/homeassistant/components/synology_dsm/translations/ca.json +++ b/homeassistant/components/synology_dsm/translations/ca.json @@ -28,16 +28,7 @@ "username": "Nom d'usuari", "verify_ssl": "Verifica el certificat SSL" }, - "description": "Vols configurar {name} ({host})?", - "title": "Synology DSM" - }, - "reauth": { - "data": { - "password": "Contrasenya", - "username": "Nom d'usuari" - }, - "description": "Motiu: {details}", - "title": "Reautenticaci\u00f3 de la integraci\u00f3 Synology DSM" + "description": "Vols configurar {name} ({host})?" }, "reauth_confirm": { "data": { @@ -54,8 +45,7 @@ "ssl": "Utilitza un certificat SSL", "username": "Nom d'usuari", "verify_ssl": "Verifica el certificat SSL" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/cs.json b/homeassistant/components/synology_dsm/translations/cs.json index ae77a93a790..dfa0a5fd347 100644 --- a/homeassistant/components/synology_dsm/translations/cs.json +++ b/homeassistant/components/synology_dsm/translations/cs.json @@ -26,15 +26,7 @@ "username": "U\u017eivatelsk\u00e9 jm\u00e9no", "verify_ssl": "Ov\u011b\u0159it certifik\u00e1t SSL" }, - "description": "Chcete nastavit {name} ({host})?", - "title": "Synology DSM" - }, - "reauth": { - "data": { - "password": "Heslo", - "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - }, - "title": "Synology DSM Znovu ov\u011b\u0159it integraci" + "description": "Chcete nastavit {name} ({host})?" }, "reauth_confirm": { "data": { @@ -50,8 +42,7 @@ "ssl": "Pou\u017e\u00edv\u00e1 SSL certifik\u00e1t", "username": "U\u017eivatelsk\u00e9 jm\u00e9no", "verify_ssl": "Ov\u011b\u0159it certifik\u00e1t SSL" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/de.json b/homeassistant/components/synology_dsm/translations/de.json index 6b945c7bda2..247eba408c9 100644 --- a/homeassistant/components/synology_dsm/translations/de.json +++ b/homeassistant/components/synology_dsm/translations/de.json @@ -28,16 +28,7 @@ "username": "Benutzername", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, - "description": "M\u00f6chtest du {name} ({host}) einrichten?", - "title": "Synology DSM" - }, - "reauth": { - "data": { - "password": "Passwort", - "username": "Benutzername" - }, - "description": "Grund: {details}", - "title": "Synology DSM Integration erneut authentifizieren" + "description": "M\u00f6chtest du {name} ({host}) einrichten?" }, "reauth_confirm": { "data": { @@ -54,8 +45,7 @@ "ssl": "Verwendet ein SSL-Zertifikat", "username": "Benutzername", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/el.json b/homeassistant/components/synology_dsm/translations/el.json index 1cf10eb6175..fc4e0124056 100644 --- a/homeassistant/components/synology_dsm/translations/el.json +++ b/homeassistant/components/synology_dsm/translations/el.json @@ -28,16 +28,7 @@ "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" }, - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ({host});", - "title": "Synology DSM" - }, - "reauth": { - "data": { - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" - }, - "description": "\u0391\u03b9\u03c4\u03af\u03b1: {details}", - "title": "Synology DSM \u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ({host});" }, "reauth_confirm": { "data": { @@ -54,8 +45,7 @@ "ssl": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ad\u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/en.json b/homeassistant/components/synology_dsm/translations/en.json index ce3b8083965..267eb772e13 100644 --- a/homeassistant/components/synology_dsm/translations/en.json +++ b/homeassistant/components/synology_dsm/translations/en.json @@ -28,16 +28,7 @@ "username": "Username", "verify_ssl": "Verify SSL certificate" }, - "description": "Do you want to setup {name} ({host})?", - "title": "Synology DSM" - }, - "reauth": { - "data": { - "password": "Password", - "username": "Username" - }, - "description": "Reason: {details}", - "title": "Synology DSM Reauthenticate Integration" + "description": "Do you want to setup {name} ({host})?" }, "reauth_confirm": { "data": { @@ -54,8 +45,7 @@ "ssl": "Uses an SSL certificate", "username": "Username", "verify_ssl": "Verify SSL certificate" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/es-419.json b/homeassistant/components/synology_dsm/translations/es-419.json index 2886a8ef624..8b319773410 100644 --- a/homeassistant/components/synology_dsm/translations/es-419.json +++ b/homeassistant/components/synology_dsm/translations/es-419.json @@ -23,8 +23,7 @@ "ssl": "Utilice SSL/TLS para conectarse a su NAS", "username": "Nombre de usuario" }, - "description": "\u00bfDesea configurar {name} ({host})?", - "title": "Synology DSM" + "description": "\u00bfDesea configurar {name} ({host})?" }, "user": { "data": { @@ -33,8 +32,7 @@ "port": "Puerto (opcional)", "ssl": "Utilice SSL/TLS para conectarse a su NAS", "username": "Nombre de usuario" - }, - "title": "Synology DSM" + } } } } diff --git a/homeassistant/components/synology_dsm/translations/es.json b/homeassistant/components/synology_dsm/translations/es.json index 382797dc81e..779996d7023 100644 --- a/homeassistant/components/synology_dsm/translations/es.json +++ b/homeassistant/components/synology_dsm/translations/es.json @@ -28,16 +28,7 @@ "username": "Usuario", "verify_ssl": "Verificar certificado SSL" }, - "description": "\u00bfQuieres configurar {name} ({host})?", - "title": "Synology DSM" - }, - "reauth": { - "data": { - "password": "Contrase\u00f1a", - "username": "Usuario" - }, - "description": "Raz\u00f3n: {details}", - "title": "Volver a autenticar la integraci\u00f3n Synology DSM" + "description": "\u00bfQuieres configurar {name} ({host})?" }, "reauth_confirm": { "data": { @@ -54,8 +45,7 @@ "ssl": "Usar SSL/TLS para conectar con tu NAS", "username": "Usuario", "verify_ssl": "Verificar certificado SSL" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/et.json b/homeassistant/components/synology_dsm/translations/et.json index 691059a56e6..69341cab488 100644 --- a/homeassistant/components/synology_dsm/translations/et.json +++ b/homeassistant/components/synology_dsm/translations/et.json @@ -28,16 +28,7 @@ "username": "Kasutajanimi", "verify_ssl": "Kontrolli SSL sertifikaati" }, - "description": "Kas soovid seadistada {name}({host})?", - "title": "" - }, - "reauth": { - "data": { - "password": "Salas\u00f5na", - "username": "Kasutajanimi" - }, - "description": "P\u00f5hjus: {details}", - "title": "Synology DSM: Taastuvasta sidumine" + "description": "Kas soovid seadistada {name}({host})?" }, "reauth_confirm": { "data": { @@ -54,8 +45,7 @@ "ssl": "Kasutab SSL sertifikaati", "username": "Kasutajanimi", "verify_ssl": "Kontrolli SSL sertifikaati" - }, - "title": "" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/fi.json b/homeassistant/components/synology_dsm/translations/fi.json index 8e1cb61abce..4f5f2cc19fa 100644 --- a/homeassistant/components/synology_dsm/translations/fi.json +++ b/homeassistant/components/synology_dsm/translations/fi.json @@ -8,9 +8,6 @@ "data": { "otp_code": "Koodi" } - }, - "reauth": { - "description": "Syy: {details}" } } }, diff --git a/homeassistant/components/synology_dsm/translations/fr.json b/homeassistant/components/synology_dsm/translations/fr.json index c6488ea7356..3917d9f800e 100644 --- a/homeassistant/components/synology_dsm/translations/fr.json +++ b/homeassistant/components/synology_dsm/translations/fr.json @@ -28,16 +28,7 @@ "username": "Nom d'utilisateur", "verify_ssl": "V\u00e9rifier le certificat SSL" }, - "description": "Voulez-vous configurer {name} ({host})?", - "title": "Synology DSM" - }, - "reauth": { - "data": { - "password": "Mot de passe", - "username": "Nom d'utilisateur" - }, - "description": "Raison: {details}", - "title": "Synology DSM R\u00e9-authentifier l'int\u00e9gration" + "description": "Voulez-vous configurer {name} ({host})?" }, "reauth_confirm": { "data": { @@ -54,8 +45,7 @@ "ssl": "Utilise un certificat SSL", "username": "Nom d'utilisateur", "verify_ssl": "V\u00e9rifier le certificat SSL" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/he.json b/homeassistant/components/synology_dsm/translations/he.json index 7adcac9af84..2e7772fc56f 100644 --- a/homeassistant/components/synology_dsm/translations/he.json +++ b/homeassistant/components/synology_dsm/translations/he.json @@ -23,12 +23,6 @@ }, "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name} ({host})?" }, - "reauth": { - "data": { - "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } - }, "reauth_confirm": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/synology_dsm/translations/hu.json b/homeassistant/components/synology_dsm/translations/hu.json index f23702ba33f..12f2bae011e 100644 --- a/homeassistant/components/synology_dsm/translations/hu.json +++ b/homeassistant/components/synology_dsm/translations/hu.json @@ -28,16 +28,7 @@ "username": "Felhaszn\u00e1l\u00f3n\u00e9v", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" }, - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({host})?", - "title": "Synology DSM" - }, - "reauth": { - "data": { - "password": "Jelsz\u00f3", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - }, - "description": "Indokl\u00e1s: {details}", - "title": "Synology DSM Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({host})?" }, "reauth_confirm": { "data": { @@ -54,8 +45,7 @@ "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", "username": "Felhaszn\u00e1l\u00f3n\u00e9v", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/id.json b/homeassistant/components/synology_dsm/translations/id.json index 7f0242fedd0..204b7b372fa 100644 --- a/homeassistant/components/synology_dsm/translations/id.json +++ b/homeassistant/components/synology_dsm/translations/id.json @@ -28,16 +28,7 @@ "username": "Nama Pengguna", "verify_ssl": "Verifikasi sertifikat SSL" }, - "description": "Ingin menyiapkan {name} ({host})?", - "title": "Synology DSM" - }, - "reauth": { - "data": { - "password": "Kata Sandi", - "username": "Nama Pengguna" - }, - "description": "Alasan: {details}", - "title": "Autentikasi Ulang Integrasi Synology DSM" + "description": "Ingin menyiapkan {name} ({host})?" }, "reauth_confirm": { "data": { @@ -54,8 +45,7 @@ "ssl": "Menggunakan sertifikat SSL", "username": "Nama Pengguna", "verify_ssl": "Verifikasi sertifikat SSL" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/it.json b/homeassistant/components/synology_dsm/translations/it.json index 54d119c55b0..e0337ffaea9 100644 --- a/homeassistant/components/synology_dsm/translations/it.json +++ b/homeassistant/components/synology_dsm/translations/it.json @@ -28,16 +28,7 @@ "username": "Nome utente", "verify_ssl": "Verifica il certificato SSL" }, - "description": "Vuoi impostare {name} ({host})?", - "title": "Synology DSM" - }, - "reauth": { - "data": { - "password": "Password", - "username": "Nome utente" - }, - "description": "Motivo: {details}", - "title": "Synology DSM Autentica nuovamente l'integrazione" + "description": "Vuoi impostare {name} ({host})?" }, "reauth_confirm": { "data": { @@ -54,8 +45,7 @@ "ssl": "Utilizza un certificato SSL", "username": "Nome utente", "verify_ssl": "Verifica il certificato SSL" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/ja.json b/homeassistant/components/synology_dsm/translations/ja.json index 2cf84781cc0..47245b2ceb8 100644 --- a/homeassistant/components/synology_dsm/translations/ja.json +++ b/homeassistant/components/synology_dsm/translations/ja.json @@ -28,16 +28,7 @@ "username": "\u30e6\u30fc\u30b6\u30fc\u540d", "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" }, - "description": "{name} ({host})\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f", - "title": "Synology DSM" - }, - "reauth": { - "data": { - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - }, - "description": "\u7406\u7531: {details}", - "title": "Synology DSM \u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "{name} ({host})\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, "reauth_confirm": { "data": { @@ -54,8 +45,7 @@ "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", "username": "\u30e6\u30fc\u30b6\u30fc\u540d", "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/ko.json b/homeassistant/components/synology_dsm/translations/ko.json index da61e46731e..5de99988192 100644 --- a/homeassistant/components/synology_dsm/translations/ko.json +++ b/homeassistant/components/synology_dsm/translations/ko.json @@ -26,8 +26,7 @@ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" }, - "description": "{name} ({host})\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Synology DSM" + "description": "{name} ({host})\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" }, "user": { "data": { @@ -37,8 +36,7 @@ "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/lb.json b/homeassistant/components/synology_dsm/translations/lb.json index 4360b8685b3..d1faf4dca2f 100644 --- a/homeassistant/components/synology_dsm/translations/lb.json +++ b/homeassistant/components/synology_dsm/translations/lb.json @@ -26,8 +26,7 @@ "username": "Benotzernumm", "verify_ssl": "SSL Zertifikat iwwerpr\u00e9iwen" }, - "description": "Soll {name} ({host}) konfigur\u00e9iert ginn?", - "title": "Synology DSM" + "description": "Soll {name} ({host}) konfigur\u00e9iert ginn?" }, "user": { "data": { @@ -37,8 +36,7 @@ "ssl": "Benotzt ee SSL Zertifikat", "username": "Benotzernumm", "verify_ssl": "SSL Zertifikat iwwerpr\u00e9iwen" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/nb.json b/homeassistant/components/synology_dsm/translations/nb.json index 3a397e1f7d3..2ba01a2ddc4 100644 --- a/homeassistant/components/synology_dsm/translations/nb.json +++ b/homeassistant/components/synology_dsm/translations/nb.json @@ -1,11 +1,6 @@ { "config": { "step": { - "reauth": { - "data": { - "username": "Brukernavn" - } - }, "reauth_confirm": { "data": { "username": "Brukernavn" diff --git a/homeassistant/components/synology_dsm/translations/nl.json b/homeassistant/components/synology_dsm/translations/nl.json index 801c1d7fe82..c685137738a 100644 --- a/homeassistant/components/synology_dsm/translations/nl.json +++ b/homeassistant/components/synology_dsm/translations/nl.json @@ -28,16 +28,7 @@ "username": "Gebruikersnaam", "verify_ssl": "Controleer het SSL-certificaat" }, - "description": "Wil je {name} ({host}) instellen?", - "title": "Synology DSM" - }, - "reauth": { - "data": { - "password": "Wachtwoord", - "username": "Gebruikersnaam" - }, - "description": "Reden: {details}", - "title": "Synology DSM Verifieer de integratie opnieuw" + "description": "Wil je {name} ({host}) instellen?" }, "reauth_confirm": { "data": { @@ -54,8 +45,7 @@ "ssl": "Gebruik een SSL-certificaat", "username": "Gebruikersnaam", "verify_ssl": "Controleer het SSL-certificaat" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/no.json b/homeassistant/components/synology_dsm/translations/no.json index 41f56c135aa..89ca80b168e 100644 --- a/homeassistant/components/synology_dsm/translations/no.json +++ b/homeassistant/components/synology_dsm/translations/no.json @@ -28,16 +28,7 @@ "username": "Brukernavn", "verify_ssl": "Verifisere SSL-sertifikat" }, - "description": "Vil du konfigurere {name} ({host})?", - "title": "" - }, - "reauth": { - "data": { - "password": "Passord", - "username": "Brukernavn" - }, - "description": "\u00c5rsak: {details}", - "title": "Synology DSM Godkjenne integrering p\u00e5 nytt" + "description": "Vil du konfigurere {name} ({host})?" }, "reauth_confirm": { "data": { @@ -54,8 +45,7 @@ "ssl": "Bruker et SSL-sertifikat", "username": "Brukernavn", "verify_ssl": "Verifisere SSL-sertifikat" - }, - "title": "" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/pl.json b/homeassistant/components/synology_dsm/translations/pl.json index 9e285f65f84..a5c78b4390f 100644 --- a/homeassistant/components/synology_dsm/translations/pl.json +++ b/homeassistant/components/synology_dsm/translations/pl.json @@ -28,16 +28,7 @@ "username": "Nazwa u\u017cytkownika", "verify_ssl": "Weryfikacja certyfikatu SSL" }, - "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?", - "title": "Synology DSM" - }, - "reauth": { - "data": { - "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika" - }, - "description": "Pow\u00f3d: {details}", - "title": "Ponownie uwierzytelnij integracj\u0119 Synology DSM" + "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?" }, "reauth_confirm": { "data": { @@ -54,8 +45,7 @@ "ssl": "Certyfikat SSL", "username": "Nazwa u\u017cytkownika", "verify_ssl": "Weryfikacja certyfikatu SSL" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/pt-BR.json b/homeassistant/components/synology_dsm/translations/pt-BR.json index ddde3e1e29d..3410658fafe 100644 --- a/homeassistant/components/synology_dsm/translations/pt-BR.json +++ b/homeassistant/components/synology_dsm/translations/pt-BR.json @@ -28,16 +28,7 @@ "username": "Usu\u00e1rio", "verify_ssl": "Verifique o certificado SSL" }, - "description": "Voc\u00ea quer configurar o {name} ({host})?", - "title": "Synology DSM" - }, - "reauth": { - "data": { - "password": "Senha", - "username": "Usu\u00e1rio" - }, - "description": "Motivo: {details}", - "title": "Synology DSM Reautenticar Integra\u00e7\u00e3o" + "description": "Voc\u00ea quer configurar o {name} ({host})?" }, "reauth_confirm": { "data": { @@ -54,8 +45,7 @@ "ssl": "Usar um certificado SSL", "username": "Usu\u00e1rio", "verify_ssl": "Verifique o certificado SSL" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/ru.json b/homeassistant/components/synology_dsm/translations/ru.json index 3f73d41d740..007504c4828 100644 --- a/homeassistant/components/synology_dsm/translations/ru.json +++ b/homeassistant/components/synology_dsm/translations/ru.json @@ -28,16 +28,7 @@ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" }, - "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?", - "title": "Synology DSM" - }, - "reauth": { - "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": "\u041f\u0440\u0438\u0447\u0438\u043d\u0430: {details}", - "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f Synology DSM" + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?" }, "reauth_confirm": { "data": { @@ -54,8 +45,7 @@ "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", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/sk.json b/homeassistant/components/synology_dsm/translations/sk.json index 59f0e333d63..83965998f0e 100644 --- a/homeassistant/components/synology_dsm/translations/sk.json +++ b/homeassistant/components/synology_dsm/translations/sk.json @@ -13,11 +13,6 @@ "port": "Port" } }, - "reauth": { - "data": { - "password": "Heslo" - } - }, "user": { "data": { "port": "Port" diff --git a/homeassistant/components/synology_dsm/translations/sl.json b/homeassistant/components/synology_dsm/translations/sl.json index 164abf7e2cc..91d32273e62 100644 --- a/homeassistant/components/synology_dsm/translations/sl.json +++ b/homeassistant/components/synology_dsm/translations/sl.json @@ -23,8 +23,7 @@ "ssl": "Uporabite SSL/TLS za povezavo z va\u0161im NAS-om", "username": "Uporabni\u0161ko ime" }, - "description": "Ali \u017eelite nastaviti {name} ({host})?", - "title": "Synology DSM" + "description": "Ali \u017eelite nastaviti {name} ({host})?" }, "user": { "data": { @@ -33,8 +32,7 @@ "port": "Vrata (Izbirno)", "ssl": "Uporabite SSL/TLS za povezavo z va\u0161im NAS-om", "username": "Uporabni\u0161ko ime" - }, - "title": "Synology DSM" + } } } } diff --git a/homeassistant/components/synology_dsm/translations/sv.json b/homeassistant/components/synology_dsm/translations/sv.json index a6f5c496f22..04814596518 100644 --- a/homeassistant/components/synology_dsm/translations/sv.json +++ b/homeassistant/components/synology_dsm/translations/sv.json @@ -10,8 +10,7 @@ "port": "Port (Valfri)", "username": "Anv\u00e4ndarnamn" }, - "description": "Do vill du konfigurera {name} ({host})?", - "title": "Synology DSM" + "description": "Do vill du konfigurera {name} ({host})?" }, "user": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/tr.json b/homeassistant/components/synology_dsm/translations/tr.json index 0d527a34504..bcd2085218f 100644 --- a/homeassistant/components/synology_dsm/translations/tr.json +++ b/homeassistant/components/synology_dsm/translations/tr.json @@ -28,16 +28,7 @@ "username": "Kullan\u0131c\u0131 Ad\u0131", "verify_ssl": "SSL sertifikalar\u0131n\u0131 do\u011frula" }, - "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?", - "title": "Synology DSM" - }, - "reauth": { - "data": { - "password": "Parola", - "username": "Kullan\u0131c\u0131 Ad\u0131" - }, - "description": "Sebep: {details}", - "title": "Synology DSM Entegrasyonu Yeniden Do\u011frula" + "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?" }, "reauth_confirm": { "data": { @@ -54,8 +45,7 @@ "ssl": "SSL sertifikas\u0131 kullan\u0131r", "username": "Kullan\u0131c\u0131 Ad\u0131", "verify_ssl": "SSL sertifikalar\u0131n\u0131 do\u011frula" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/uk.json b/homeassistant/components/synology_dsm/translations/uk.json index 4d80350989f..21b0f4a68f0 100644 --- a/homeassistant/components/synology_dsm/translations/uk.json +++ b/homeassistant/components/synology_dsm/translations/uk.json @@ -26,8 +26,7 @@ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" }, - "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name} ({host})?", - "title": "Synology DSM" + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name} ({host})?" }, "user": { "data": { @@ -37,8 +36,7 @@ "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/zh-Hans.json b/homeassistant/components/synology_dsm/translations/zh-Hans.json index 862f526c38d..a62a79ae868 100644 --- a/homeassistant/components/synology_dsm/translations/zh-Hans.json +++ b/homeassistant/components/synology_dsm/translations/zh-Hans.json @@ -26,14 +26,7 @@ "ssl": "\u4f7f\u7528 SSL \u8bc1\u4e66", "username": "\u7528\u6237\u540d" }, - "description": "\u60a8\u60f3\u8981\u914d\u7f6e {name} ({host}) \u5417\uff1f", - "title": "Synology DSM" - }, - "reauth": { - "data": { - "password": "\u5bc6\u7801", - "username": "\u7528\u6237\u540d" - } + "description": "\u60a8\u60f3\u8981\u914d\u7f6e {name} ({host}) \u5417\uff1f" }, "user": { "data": { @@ -43,8 +36,7 @@ "ssl": "\u4f7f\u7528 SSL \u8bc1\u4e66", "username": "\u7528\u6237\u540d", "verify_ssl": "\u9a8c\u8bc1 SSL \u8bc1\u4e66" - }, - "title": "Synology DSM" + } } } }, diff --git a/homeassistant/components/synology_dsm/translations/zh-Hant.json b/homeassistant/components/synology_dsm/translations/zh-Hant.json index 33211d9a5a3..504e1bec32d 100644 --- a/homeassistant/components/synology_dsm/translations/zh-Hant.json +++ b/homeassistant/components/synology_dsm/translations/zh-Hant.json @@ -28,16 +28,7 @@ "username": "\u4f7f\u7528\u8005\u540d\u7a31", "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" }, - "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f", - "title": "\u7fa4\u6689 DSM" - }, - "reauth": { - "data": { - "password": "\u5bc6\u78bc", - "username": "\u4f7f\u7528\u8005\u540d\u7a31" - }, - "description": "\u8a73\u7d30\u8cc7\u8a0a\uff1a{details}", - "title": "Synology DSM \u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f" }, "reauth_confirm": { "data": { @@ -54,8 +45,7 @@ "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49", "username": "\u4f7f\u7528\u8005\u540d\u7a31", "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" - }, - "title": "\u7fa4\u6689 DSM" + } } } }, diff --git a/homeassistant/components/tasmota/translations/bg.json b/homeassistant/components/tasmota/translations/bg.json index a2321080a6a..425bec1cad5 100644 --- a/homeassistant/components/tasmota/translations/bg.json +++ b/homeassistant/components/tasmota/translations/bg.json @@ -4,9 +4,6 @@ "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?" } diff --git a/homeassistant/components/tasmota/translations/ca.json b/homeassistant/components/tasmota/translations/ca.json index 0a414bc5cfe..f5adb5b0694 100644 --- a/homeassistant/components/tasmota/translations/ca.json +++ b/homeassistant/components/tasmota/translations/ca.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "Prefix del topic de descoberta (discovery)" - }, - "description": "Introdureix la configuraci\u00f3 de Tasmota.", - "title": "Tasmota" + } }, "confirm": { "description": "Vols configurar Tasmota?" diff --git a/homeassistant/components/tasmota/translations/cs.json b/homeassistant/components/tasmota/translations/cs.json index 4fb0d555c63..673126f1cf0 100644 --- a/homeassistant/components/tasmota/translations/cs.json +++ b/homeassistant/components/tasmota/translations/cs.json @@ -4,10 +4,6 @@ "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." }, "step": { - "config": { - "description": "Pros\u00edm zajdete nastaven\u00ed pro Tasmota.", - "title": "Tasmota" - }, "confirm": { "description": "Chcete nastavit Tasmota?" } diff --git a/homeassistant/components/tasmota/translations/de.json b/homeassistant/components/tasmota/translations/de.json index 7e654d982c5..128b99a75b9 100644 --- a/homeassistant/components/tasmota/translations/de.json +++ b/homeassistant/components/tasmota/translations/de.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "Themenpr\u00e4fix f\u00fcr die Erkennung" - }, - "description": "Bitte die Tasmota-Konfiguration einstellen.", - "title": "Tasmota" + } }, "confirm": { "description": "M\u00f6chtest du Tasmota einrichten?" diff --git a/homeassistant/components/tasmota/translations/el.json b/homeassistant/components/tasmota/translations/el.json index cb9bc10730c..732724ab2ab 100644 --- a/homeassistant/components/tasmota/translations/el.json +++ b/homeassistant/components/tasmota/translations/el.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "\u03a0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03b8\u03ad\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2" - }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Tasmota.", - "title": "Tasmota" + } }, "confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd Tasmota;" diff --git a/homeassistant/components/tasmota/translations/en.json b/homeassistant/components/tasmota/translations/en.json index ddd7e726079..3e8b0b43bce 100644 --- a/homeassistant/components/tasmota/translations/en.json +++ b/homeassistant/components/tasmota/translations/en.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "Discovery topic prefix" - }, - "description": "Please enter the Tasmota configuration.", - "title": "Tasmota" + } }, "confirm": { "description": "Do you want to set up Tasmota?" diff --git a/homeassistant/components/tasmota/translations/es.json b/homeassistant/components/tasmota/translations/es.json index f5a5532f180..0d3b7317330 100644 --- a/homeassistant/components/tasmota/translations/es.json +++ b/homeassistant/components/tasmota/translations/es.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "Prefijo del tema para descubrimiento" - }, - "description": "Introduce la configuraci\u00f3n de Tasmota.", - "title": "Tasmota" + } }, "confirm": { "description": "\u00bfQuieres configurar Tasmota?" diff --git a/homeassistant/components/tasmota/translations/et.json b/homeassistant/components/tasmota/translations/et.json index e689dcf7e43..09ba6e5c328 100644 --- a/homeassistant/components/tasmota/translations/et.json +++ b/homeassistant/components/tasmota/translations/et.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "Tuvastusteema eesliide" - }, - "description": "Sisesta Tasmota konfiguratsioon.", - "title": "Tasmota" + } }, "confirm": { "description": "Kas soovid seadistada Tasmota sidumist?" diff --git a/homeassistant/components/tasmota/translations/fr.json b/homeassistant/components/tasmota/translations/fr.json index 901de884bcd..7521004ba2f 100644 --- a/homeassistant/components/tasmota/translations/fr.json +++ b/homeassistant/components/tasmota/translations/fr.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "Pr\u00e9fixe du sujet de d\u00e9couverte" - }, - "description": "Veuillez entrer la configuration Tasmota.", - "title": "Tasmota" + } }, "confirm": { "description": "Voulez-vous configurer Tasmota ?" diff --git a/homeassistant/components/tasmota/translations/he.json b/homeassistant/components/tasmota/translations/he.json index a2a5db62b37..2bc04cca267 100644 --- a/homeassistant/components/tasmota/translations/he.json +++ b/homeassistant/components/tasmota/translations/he.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "\u05d2\u05d9\u05dc\u05d5\u05d9 \u05dc\u05e4\u05d9 \u05e7\u05d9\u05d3\u05d5\u05de\u05ea \u05e0\u05d5\u05e9\u05d0" - }, - "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05d0\u05ea \u05ea\u05e6\u05d5\u05e8\u05ea Tasmota.", - "title": "Tasmota" + } }, "confirm": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea Tasmota?" diff --git a/homeassistant/components/tasmota/translations/hu.json b/homeassistant/components/tasmota/translations/hu.json index 7c77caadc8e..990bde11d58 100644 --- a/homeassistant/components/tasmota/translations/hu.json +++ b/homeassistant/components/tasmota/translations/hu.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "Felder\u00edt\u00e9si (discovery) topik el\u0151tagja" - }, - "description": "Adja meg a Tasmota konfigur\u00e1ci\u00f3t.", - "title": "Tasmota" + } }, "confirm": { "description": "Szeretn\u00e9 b\u00e1ll\u00edtani a Tasmota-t?" diff --git a/homeassistant/components/tasmota/translations/id.json b/homeassistant/components/tasmota/translations/id.json index a11acf50390..23ca02192db 100644 --- a/homeassistant/components/tasmota/translations/id.json +++ b/homeassistant/components/tasmota/translations/id.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "Prefiks topik penemuan" - }, - "description": "Masukkan konfigurasi Tasmota.", - "title": "Tasmota" + } }, "confirm": { "description": "Ingin menyiapkan Tasmota?" diff --git a/homeassistant/components/tasmota/translations/it.json b/homeassistant/components/tasmota/translations/it.json index 6cb52a616b0..22269eafd9f 100644 --- a/homeassistant/components/tasmota/translations/it.json +++ b/homeassistant/components/tasmota/translations/it.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "Prefisso dell'argomento di individuazione" - }, - "description": "Inserire la configurazione Tasmota.", - "title": "Tasmota" + } }, "confirm": { "description": "Vuoi configurare Tasmota?" diff --git a/homeassistant/components/tasmota/translations/ja.json b/homeassistant/components/tasmota/translations/ja.json index 353b3020e8c..8aad41de32f 100644 --- a/homeassistant/components/tasmota/translations/ja.json +++ b/homeassistant/components/tasmota/translations/ja.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "(\u691c\u51fa)Discovery topic prefix" - }, - "description": "Tasmota\u306e\u8a2d\u5b9a\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "Tasmota" + } }, "confirm": { "description": "Tasmota\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" diff --git a/homeassistant/components/tasmota/translations/ko.json b/homeassistant/components/tasmota/translations/ko.json index 45cac13f622..d647088c55d 100644 --- a/homeassistant/components/tasmota/translations/ko.json +++ b/homeassistant/components/tasmota/translations/ko.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "\uac80\uc0c9 \ud1a0\ud53d \uc811\ub450\uc0ac" - }, - "description": "Tasmota \uad6c\uc131\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", - "title": "Tasmota" + } }, "confirm": { "description": "Tasmota\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" diff --git a/homeassistant/components/tasmota/translations/lb.json b/homeassistant/components/tasmota/translations/lb.json index a7b8d6d0ce6..6208f54b7c0 100644 --- a/homeassistant/components/tasmota/translations/lb.json +++ b/homeassistant/components/tasmota/translations/lb.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "Discovery topic prefix" - }, - "description": "F\u00ebll Tasmota Konfiguratioun aus.", - "title": "Tasmota" + } }, "confirm": { "description": "Soll Tasmota konfigur\u00e9iert ginn?" diff --git a/homeassistant/components/tasmota/translations/nl.json b/homeassistant/components/tasmota/translations/nl.json index da16eb72bc3..474ed82b9d6 100644 --- a/homeassistant/components/tasmota/translations/nl.json +++ b/homeassistant/components/tasmota/translations/nl.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "Discovery-onderwerpvoorvoegsel" - }, - "description": "Vul de Tasmota gegevens in", - "title": "Tasmota" + } }, "confirm": { "description": "Wil je Tasmota instellen?" diff --git a/homeassistant/components/tasmota/translations/no.json b/homeassistant/components/tasmota/translations/no.json index 3c68c280086..7f0a67e7c9f 100644 --- a/homeassistant/components/tasmota/translations/no.json +++ b/homeassistant/components/tasmota/translations/no.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "Prefiks for oppdagelsesemne" - }, - "description": "Vennligst skriv inn Tasmota-konfigurasjonen.", - "title": "" + } }, "confirm": { "description": "Vil du sette opp Tasmota?" diff --git a/homeassistant/components/tasmota/translations/pl.json b/homeassistant/components/tasmota/translations/pl.json index b6bbf3fe953..70ffeb5c7e2 100644 --- a/homeassistant/components/tasmota/translations/pl.json +++ b/homeassistant/components/tasmota/translations/pl.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "Prefiks tematu wykrywania" - }, - "description": "Wprowad\u017a konfiguracj\u0119 dla Tasmota.", - "title": "Tasmota" + } }, "confirm": { "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" diff --git a/homeassistant/components/tasmota/translations/pt-BR.json b/homeassistant/components/tasmota/translations/pt-BR.json index 6fa1f064e88..afd1c76c25f 100644 --- a/homeassistant/components/tasmota/translations/pt-BR.json +++ b/homeassistant/components/tasmota/translations/pt-BR.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "Prefixo do t\u00f3pico de descoberta" - }, - "description": "Por favor, insira a configura\u00e7\u00e3o do Tasmota.", - "title": "Tasmota" + } }, "confirm": { "description": "Deseja configurar o Tasmota?" diff --git a/homeassistant/components/tasmota/translations/pt.json b/homeassistant/components/tasmota/translations/pt.json index 3df19f11fa9..508ab27abb0 100644 --- a/homeassistant/components/tasmota/translations/pt.json +++ b/homeassistant/components/tasmota/translations/pt.json @@ -10,8 +10,7 @@ "config": { "data": { "discovery_prefix": "Prefixo do t\u00f3pico para descoberta" - }, - "title": "" + } } } } diff --git a/homeassistant/components/tasmota/translations/ru.json b/homeassistant/components/tasmota/translations/ru.json index 4f01d164030..14e336a9b58 100644 --- a/homeassistant/components/tasmota/translations/ru.json +++ b/homeassistant/components/tasmota/translations/ru.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441 \u0442\u043e\u043f\u0438\u043a\u0430 \u0430\u0432\u0442\u043e\u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f" - }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Tasmota.", - "title": "Tasmota" + } }, "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 Tasmota?" diff --git a/homeassistant/components/tasmota/translations/tr.json b/homeassistant/components/tasmota/translations/tr.json index 95e4e41b11e..71c38ef1bc0 100644 --- a/homeassistant/components/tasmota/translations/tr.json +++ b/homeassistant/components/tasmota/translations/tr.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "Ba\u015fl\u0131k Ke\u015ffet" - }, - "description": "L\u00fctfen Tasmota yap\u0131land\u0131rmas\u0131n\u0131 girin.", - "title": "Tasmota" + } }, "confirm": { "description": "Tasmota'y\u0131 kurmak istiyor musunuz?" diff --git a/homeassistant/components/tasmota/translations/uk.json b/homeassistant/components/tasmota/translations/uk.json index 5b57f950866..d1445e5c0e7 100644 --- a/homeassistant/components/tasmota/translations/uk.json +++ b/homeassistant/components/tasmota/translations/uk.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "\u041f\u0440\u0435\u0444\u0456\u043a\u0441 \u0442\u0435\u043c\u0438 \u0430\u0432\u0442\u043e\u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f" - }, - "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Tasmota.", - "title": "Tasmota" + } }, "confirm": { "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Tasmota?" diff --git a/homeassistant/components/tasmota/translations/zh-Hant.json b/homeassistant/components/tasmota/translations/zh-Hant.json index e823937f972..58833747f46 100644 --- a/homeassistant/components/tasmota/translations/zh-Hant.json +++ b/homeassistant/components/tasmota/translations/zh-Hant.json @@ -10,9 +10,7 @@ "config": { "data": { "discovery_prefix": "\u63a2\u7d22\u4e3b\u984c prefix" - }, - "description": "\u8acb\u8f38\u5165 Tasmota \u8a2d\u5b9a\u3002", - "title": "Tasmota" + } }, "confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a Tasmota\uff1f" diff --git a/homeassistant/components/tautulli/translations/ko.json b/homeassistant/components/tautulli/translations/ko.json new file mode 100644 index 00000000000..effc12fad5a --- /dev/null +++ b/homeassistant/components/tautulli/translations/ko.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" + }, + "description": "API \ud0a4\ub97c \ucc3e\uc73c\ub824\uba74 Tautulli \uc6f9 \ud398\uc774\uc9c0\ub97c \uc5f4\uace0 \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud55c \ub2e4\uc74c \uc6f9 \uc778\ud130\ud398\uc774\uc2a4\ub85c \uc774\ub3d9\ud569\ub2c8\ub2e4. API \ud0a4\ub294 \ud574\ub2f9 \ud398\uc774\uc9c0\uc758 \ub9e8 \uc544\ub798\uc5d0 \uc788\uc2b5\ub2c8\ub2e4.\n\nURL\uc758 \uc608: '''http://192.168.0.10:8181''''(\uae30\ubcf8\ud3ec\ud2b8: 8181)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/ca.json b/homeassistant/components/threshold/translations/ca.json index b33bd8a04ed..b933b85b080 100644 --- a/homeassistant/components/threshold/translations/ca.json +++ b/homeassistant/components/threshold/translations/ca.json @@ -9,7 +9,6 @@ "entity_id": "Sensor d'entrada", "hysteresis": "Hist\u00e8resi", "lower": "L\u00edmit inferior", - "mode": "Mode llindar", "name": "Nom", "upper": "L\u00edmit superior" }, @@ -28,7 +27,6 @@ "entity_id": "Sensor d'entrada", "hysteresis": "Hist\u00e8resi", "lower": "L\u00edmit inferior", - "mode": "Mode llindar", "name": "Nom", "upper": "L\u00edmit superior" }, diff --git a/homeassistant/components/threshold/translations/de.json b/homeassistant/components/threshold/translations/de.json index 579dd4f49b0..f8f805742ca 100644 --- a/homeassistant/components/threshold/translations/de.json +++ b/homeassistant/components/threshold/translations/de.json @@ -9,7 +9,6 @@ "entity_id": "Eingangssensor", "hysteresis": "Hysterese", "lower": "Untere Grenze", - "mode": "Schwellenwertmodus", "name": "Name", "upper": "Obergrenze" }, @@ -28,7 +27,6 @@ "entity_id": "Eingangssensor", "hysteresis": "Hysterese", "lower": "Untere Grenze", - "mode": "Schwellenwertmodus", "name": "Name", "upper": "Obergrenze" }, diff --git a/homeassistant/components/threshold/translations/el.json b/homeassistant/components/threshold/translations/el.json index 2ad5f25e5f8..11735c53908 100644 --- a/homeassistant/components/threshold/translations/el.json +++ b/homeassistant/components/threshold/translations/el.json @@ -9,7 +9,6 @@ "entity_id": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5", "hysteresis": "\u03a5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7", "lower": "\u039a\u03ac\u03c4\u03c9 \u03cc\u03c1\u03b9\u03bf", - "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03bf\u03c1\u03af\u03bf\u03c5", "name": "\u038c\u03bd\u03bf\u03bc\u03b1", "upper": "\u0386\u03bd\u03c9 \u03cc\u03c1\u03b9\u03bf" }, @@ -28,7 +27,6 @@ "entity_id": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5", "hysteresis": "\u03a5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7", "lower": "\u039a\u03ac\u03c4\u03c9 \u03cc\u03c1\u03b9\u03bf", - "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03bf\u03c1\u03af\u03bf\u03c5", "name": "\u038c\u03bd\u03bf\u03bc\u03b1", "upper": "\u0386\u03bd\u03c9 \u03cc\u03c1\u03b9\u03bf" }, diff --git a/homeassistant/components/threshold/translations/en.json b/homeassistant/components/threshold/translations/en.json index 66a2bb33ddb..461ca244353 100644 --- a/homeassistant/components/threshold/translations/en.json +++ b/homeassistant/components/threshold/translations/en.json @@ -9,7 +9,6 @@ "entity_id": "Input sensor", "hysteresis": "Hysteresis", "lower": "Lower limit", - "mode": "Threshold mode", "name": "Name", "upper": "Upper limit" }, @@ -28,7 +27,6 @@ "entity_id": "Input sensor", "hysteresis": "Hysteresis", "lower": "Lower limit", - "mode": "Threshold mode", "name": "Name", "upper": "Upper limit" }, diff --git a/homeassistant/components/threshold/translations/et.json b/homeassistant/components/threshold/translations/et.json index 7a41c0bfc3f..c98f370e47d 100644 --- a/homeassistant/components/threshold/translations/et.json +++ b/homeassistant/components/threshold/translations/et.json @@ -9,7 +9,6 @@ "entity_id": "Sisendandur", "hysteresis": "H\u00fcsterees", "lower": "Alampiir", - "mode": "L\u00e4vendi kasutamine", "name": "Nimi", "upper": "\u00dclempiir" }, @@ -28,7 +27,6 @@ "entity_id": "Sisendandur", "hysteresis": "H\u00fcsterees", "lower": "Alampiir", - "mode": "L\u00e4vendi kasutamine", "name": "Nimi", "upper": "\u00dclempiir" }, diff --git a/homeassistant/components/threshold/translations/fr.json b/homeassistant/components/threshold/translations/fr.json index 29bc21e9cd5..8c6015f29a7 100644 --- a/homeassistant/components/threshold/translations/fr.json +++ b/homeassistant/components/threshold/translations/fr.json @@ -9,7 +9,6 @@ "entity_id": "Capteur d'entr\u00e9e", "hysteresis": "Hyst\u00e9r\u00e9sis", "lower": "Limite inf\u00e9rieure", - "mode": "Mode de seuil", "name": "Nom", "upper": "Limite sup\u00e9rieure" }, @@ -28,7 +27,6 @@ "entity_id": "Capteur d'entr\u00e9e", "hysteresis": "Hyst\u00e9r\u00e9sis", "lower": "Limite inf\u00e9rieure", - "mode": "Mode de seuil", "name": "Nom", "upper": "Limite sup\u00e9rieure" }, diff --git a/homeassistant/components/threshold/translations/hu.json b/homeassistant/components/threshold/translations/hu.json index 944a13680d3..3c582792d69 100644 --- a/homeassistant/components/threshold/translations/hu.json +++ b/homeassistant/components/threshold/translations/hu.json @@ -9,7 +9,6 @@ "entity_id": "Forr\u00e1s \u00e9rz\u00e9kel\u0151", "hysteresis": "Hiszter\u00e9zis", "lower": "Als\u00f3 hat\u00e1r", - "mode": "K\u00fcsz\u00f6b\u00e9rt\u00e9k m\u00f3d", "name": "Elnevez\u00e9s", "upper": "Fels\u0151 hat\u00e1r" }, @@ -28,7 +27,6 @@ "entity_id": "Forr\u00e1s \u00e9rz\u00e9kel\u0151", "hysteresis": "Hiszter\u00e9zis", "lower": "Als\u00f3 hat\u00e1r", - "mode": "K\u00fcsz\u00f6b\u00e9rt\u00e9k m\u00f3d", "name": "Elnevez\u00e9s", "upper": "Fels\u0151 hat\u00e1r" }, diff --git a/homeassistant/components/threshold/translations/id.json b/homeassistant/components/threshold/translations/id.json index 7356dd772e1..474f742324b 100644 --- a/homeassistant/components/threshold/translations/id.json +++ b/homeassistant/components/threshold/translations/id.json @@ -9,7 +9,6 @@ "entity_id": "Sensor input", "hysteresis": "Histeresis", "lower": "Batas bawah", - "mode": "Mode ambang batas", "name": "Nama", "upper": "Batas atas" }, @@ -28,7 +27,6 @@ "entity_id": "Sensor input", "hysteresis": "Histeresis", "lower": "Batas bawah", - "mode": "Mode ambang batas", "name": "Nama", "upper": "Batas atas" }, diff --git a/homeassistant/components/threshold/translations/it.json b/homeassistant/components/threshold/translations/it.json index 6bc62dce765..79c0e24b2af 100644 --- a/homeassistant/components/threshold/translations/it.json +++ b/homeassistant/components/threshold/translations/it.json @@ -9,7 +9,6 @@ "entity_id": "Sensore di ingresso", "hysteresis": "Isteresi", "lower": "Limite inferiore", - "mode": "Modalit\u00e0 soglia", "name": "Nome", "upper": "Limite superiore" }, @@ -28,7 +27,6 @@ "entity_id": "Sensore di ingresso", "hysteresis": "Isteresi", "lower": "Limite inferiore", - "mode": "Modalit\u00e0 soglia", "name": "Nome", "upper": "Limite superiore" }, diff --git a/homeassistant/components/threshold/translations/ja.json b/homeassistant/components/threshold/translations/ja.json index 75330ac830c..1978fa4f3c5 100644 --- a/homeassistant/components/threshold/translations/ja.json +++ b/homeassistant/components/threshold/translations/ja.json @@ -9,7 +9,6 @@ "entity_id": "\u5165\u529b\u30bb\u30f3\u30b5\u30fc", "hysteresis": "\u30d2\u30b9\u30c6\u30ea\u30b7\u30b9", "lower": "\u4e0b\u9650\u5024", - "mode": "\u3057\u304d\u3044\u5024\u30e2\u30fc\u30c9", "name": "\u540d\u524d", "upper": "\u4e0a\u9650\u5024" }, @@ -28,7 +27,6 @@ "entity_id": "\u5165\u529b\u30bb\u30f3\u30b5\u30fc", "hysteresis": "\u30d2\u30b9\u30c6\u30ea\u30b7\u30b9", "lower": "\u4e0b\u9650\u5024", - "mode": "\u3057\u304d\u3044\u5024\u30e2\u30fc\u30c9", "name": "\u540d\u524d", "upper": "\u4e0a\u9650\u5024" }, diff --git a/homeassistant/components/threshold/translations/ko.json b/homeassistant/components/threshold/translations/ko.json new file mode 100644 index 00000000000..fb30f1e5086 --- /dev/null +++ b/homeassistant/components/threshold/translations/ko.json @@ -0,0 +1,38 @@ +{ + "config": { + "error": { + "need_lower_upper": "\ud558\ud55c\uacfc \uc0c1\ud55c\uc744 \ubaa8\ub450 \ube44\uc6cc\ub458 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "entity_id": "\uc785\ub825 \uc13c\uc11c", + "hysteresis": "\ud788\uc2a4\ud14c\ub9ac\uc2dc\uc2a4", + "lower": "\ud558\ud55c", + "name": "\uc774\ub984", + "upper": "\uc0c1\ud55c" + }, + "description": "\uc13c\uc11c \uac12\uc5d0 \ub530\ub77c \ucf1c\uc9c0\uace0 \uaebc\uc9c0\ub294 \uc774\uc9c4 \uc13c\uc11c\ub97c \uc0dd\uc131\ud569\ub2c8\ub2e4.\n\n\ud558\ud55c\ub9cc \uad6c\uc131\ub41c \uacbd\uc6b0 - \uc13c\uc11c \uac12\uc774 \ud558\ud55c \uc774\ud558\uc77c \ub54c \ucf1c\uc9d1\ub2c8\ub2e4.\n\uc0c1\ud55c\ub9cc \uad6c\uc131\ub41c \uacbd\uc6b0 - \uc13c\uc11c \uac12\uc774 \uc0c1\ud55c \uc774\uc0c1\uc77c \ub54c \ucf1c\uc9d1\ub2c8\ub2e4.\n\uc0c1\ud55c, \ud558\ud55c \ubaa8\ub450 \uad6c\uc131\ub41c \uacbd\uc6b0 - \uc13c\uc11c \uac12\uc774 [\ud558\ud55c .. \uc0c1\ud55c] \ubc94\uc704\uc77c \ub54c \ucf1c\uc9d1\ub2c8\ub2e4.", + "title": "\uc784\uacc4\uac12 \uc13c\uc11c \ucd94\uac00" + } + } + }, + "options": { + "error": { + "need_lower_upper": "\ud558\ud55c\uacfc \uc0c1\ud55c\uc744 \ubaa8\ub450 \ube44\uc6cc\ub458 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "init": { + "data": { + "entity_id": "\uc785\ub825 \uc13c\uc11c", + "hysteresis": "\ud788\uc2a4\ud14c\ub9ac\uc2dc\uc2a4", + "lower": "\ud558\ud55c", + "name": "\uc774\ub984", + "upper": "\uc0c1\ud55c" + }, + "description": "\ud558\ud55c\ub9cc \uad6c\uc131\ub41c \uacbd\uc6b0 - \uc13c\uc11c \uac12\uc774 \ud558\ud55c \uc774\ud558\uc77c \ub54c \ucf1c\uc9d1\ub2c8\ub2e4.\n\uc0c1\ud55c\ub9cc \uad6c\uc131\ub41c \uacbd\uc6b0 - \uc13c\uc11c \uac12\uc774 \uc0c1\ud55c \uc774\uc0c1\uc77c \ub54c \ucf1c\uc9d1\ub2c8\ub2e4.\n\uc0c1\ud55c, \ud558\ud55c \ubaa8\ub450 \uad6c\uc131\ub41c \uacbd\uc6b0 - \uc13c\uc11c \uac12\uc774 [\ud558\ud55c .. \uc0c1\ud55c] \ubc94\uc704\uc77c \ub54c \ucf1c\uc9d1\ub2c8\ub2e4." + } + } + }, + "title": "\uc784\uacc4\uac12 \uc13c\uc11c" +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/nl.json b/homeassistant/components/threshold/translations/nl.json index c49b00eee0d..3bcf8bf599e 100644 --- a/homeassistant/components/threshold/translations/nl.json +++ b/homeassistant/components/threshold/translations/nl.json @@ -9,7 +9,6 @@ "entity_id": "Invoer sensor", "hysteresis": "Hyseterise", "lower": "Ondergrens", - "mode": "Drempelmodus", "name": "Naam", "upper": "Bovengrens" }, @@ -28,7 +27,6 @@ "entity_id": "Invoer sensor", "hysteresis": "Hyseterise", "lower": "Ondergrens", - "mode": "Drempelmodus", "name": "Naam", "upper": "Bovengrens" }, diff --git a/homeassistant/components/threshold/translations/no.json b/homeassistant/components/threshold/translations/no.json index 66c8461d8cc..1800e628f31 100644 --- a/homeassistant/components/threshold/translations/no.json +++ b/homeassistant/components/threshold/translations/no.json @@ -9,7 +9,6 @@ "entity_id": "Inngangssensor", "hysteresis": "Hysterese", "lower": "Nedre grense", - "mode": "Terskelverdi-modus", "name": "Navn", "upper": "\u00d8vre grense" }, @@ -28,7 +27,6 @@ "entity_id": "Inngangssensor", "hysteresis": "Hysterese", "lower": "Nedre grense", - "mode": "Terskelverdi-modus", "name": "Navn", "upper": "\u00d8vre grense" }, diff --git a/homeassistant/components/threshold/translations/pl.json b/homeassistant/components/threshold/translations/pl.json index 5db231947c2..e8f04d4a00b 100644 --- a/homeassistant/components/threshold/translations/pl.json +++ b/homeassistant/components/threshold/translations/pl.json @@ -9,7 +9,6 @@ "entity_id": "Sensor wej\u015bciowy", "hysteresis": "Op\u00f3\u017anienie", "lower": "Dolny limit", - "mode": "Tryb progowy", "name": "Nazwa", "upper": "G\u00f3rny limit" }, @@ -28,7 +27,6 @@ "entity_id": "Sensor wej\u015bciowy", "hysteresis": "Op\u00f3\u017anienie", "lower": "Dolny limit", - "mode": "Tryb progowy", "name": "Nazwa", "upper": "G\u00f3rny limit" }, diff --git a/homeassistant/components/threshold/translations/pt-BR.json b/homeassistant/components/threshold/translations/pt-BR.json index 1aa7358086a..fb919043313 100644 --- a/homeassistant/components/threshold/translations/pt-BR.json +++ b/homeassistant/components/threshold/translations/pt-BR.json @@ -9,7 +9,6 @@ "entity_id": "Sensor de entrada", "hysteresis": "Histerese", "lower": "Limite inferior", - "mode": "Modo Threshold", "name": "Nome", "upper": "Limite superior" }, @@ -28,7 +27,6 @@ "entity_id": "Sensor de entrada", "hysteresis": "Histerese", "lower": "Limite inferior", - "mode": "Modo Threshold", "name": "Nome", "upper": "Limite superior" }, diff --git a/homeassistant/components/threshold/translations/ru.json b/homeassistant/components/threshold/translations/ru.json index 5b8c4546823..f1be2c4f26c 100644 --- a/homeassistant/components/threshold/translations/ru.json +++ b/homeassistant/components/threshold/translations/ru.json @@ -9,7 +9,6 @@ "entity_id": "\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440", "hysteresis": "\u0413\u0438\u0441\u0442\u0435\u0440\u0435\u0437\u0438\u0441", "lower": "\u041d\u0438\u0436\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b", - "mode": "\u041f\u043e\u0440\u043e\u0433\u043e\u0432\u044b\u0439 \u0440\u0435\u0436\u0438\u043c", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "upper": "\u0412\u0435\u0440\u0445\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b" }, @@ -28,7 +27,6 @@ "entity_id": "\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440", "hysteresis": "\u0413\u0438\u0441\u0442\u0435\u0440\u0435\u0437\u0438\u0441", "lower": "\u041d\u0438\u0436\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b", - "mode": "\u041f\u043e\u0440\u043e\u0433\u043e\u0432\u044b\u0439 \u0440\u0435\u0436\u0438\u043c", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "upper": "\u0412\u0435\u0440\u0445\u043d\u0438\u0439 \u043f\u0440\u0435\u0434\u0435\u043b" }, diff --git a/homeassistant/components/threshold/translations/tr.json b/homeassistant/components/threshold/translations/tr.json index 0cb338ef681..0954ea81d3f 100644 --- a/homeassistant/components/threshold/translations/tr.json +++ b/homeassistant/components/threshold/translations/tr.json @@ -9,7 +9,6 @@ "entity_id": "Giri\u015f sens\u00f6r\u00fc", "hysteresis": "Histeresis", "lower": "Alt s\u0131n\u0131r", - "mode": "E\u015fik modu", "name": "Ad", "upper": "\u00dcst s\u0131n\u0131r" }, @@ -28,7 +27,6 @@ "entity_id": "Giri\u015f sens\u00f6r\u00fc", "hysteresis": "Histeresis", "lower": "Alt s\u0131n\u0131r", - "mode": "E\u015fik modu", "name": "Ad", "upper": "\u00dcst s\u0131n\u0131r" }, diff --git a/homeassistant/components/threshold/translations/zh-Hans.json b/homeassistant/components/threshold/translations/zh-Hans.json index 35bc919f0d1..ae46afaa740 100644 --- a/homeassistant/components/threshold/translations/zh-Hans.json +++ b/homeassistant/components/threshold/translations/zh-Hans.json @@ -9,7 +9,6 @@ "entity_id": "\u8f93\u5165\u4f20\u611f\u5668", "hysteresis": "\u9632\u6296\u8303\u56f4", "lower": "\u4e0b\u9650", - "mode": "\u9608\u503c\u6a21\u5f0f", "name": "\u540d\u79f0", "upper": "\u4e0a\u9650" }, @@ -28,7 +27,6 @@ "entity_id": "\u8f93\u5165\u4f20\u611f\u5668", "hysteresis": "\u9632\u6296\u8303\u56f4", "lower": "\u4e0b\u9650", - "mode": "\u9608\u503c\u6a21\u5f0f", "name": "\u540d\u79f0", "upper": "\u4e0a\u9650" }, diff --git a/homeassistant/components/threshold/translations/zh-Hant.json b/homeassistant/components/threshold/translations/zh-Hant.json index 66bdfe6915a..45ff245bb02 100644 --- a/homeassistant/components/threshold/translations/zh-Hant.json +++ b/homeassistant/components/threshold/translations/zh-Hant.json @@ -9,7 +9,6 @@ "entity_id": "\u8f38\u5165\u611f\u6e2c\u5668", "hysteresis": "\u9072\u6eef", "lower": "\u4e0b\u9650", - "mode": "\u81e8\u754c\u9ede\u5f0f", "name": "\u540d\u7a31", "upper": "\u4e0a\u9650" }, @@ -28,7 +27,6 @@ "entity_id": "\u8f38\u5165\u611f\u6e2c\u5668", "hysteresis": "\u9072\u6eef", "lower": "\u4e0b\u9650", - "mode": "\u81e8\u754c\u9ede\u5f0f", "name": "\u540d\u7a31", "upper": "\u4e0a\u9650" }, diff --git a/homeassistant/components/tibber/translations/ca.json b/homeassistant/components/tibber/translations/ca.json index bfeb2416e57..c149bd67098 100644 --- a/homeassistant/components/tibber/translations/ca.json +++ b/homeassistant/components/tibber/translations/ca.json @@ -13,8 +13,7 @@ "data": { "access_token": "Token d'acc\u00e9s" }, - "description": "Introdueix el token d'acc\u00e9s de https://developer.tibber.com/settings/accesstoken", - "title": "Tibber" + "description": "Introdueix el token d'acc\u00e9s de https://developer.tibber.com/settings/accesstoken" } } } diff --git a/homeassistant/components/tibber/translations/cs.json b/homeassistant/components/tibber/translations/cs.json index 278300caa53..441c716c0a0 100644 --- a/homeassistant/components/tibber/translations/cs.json +++ b/homeassistant/components/tibber/translations/cs.json @@ -12,8 +12,7 @@ "user": { "data": { "access_token": "P\u0159\u00edstupov\u00fd token" - }, - "title": "Tibber" + } } } } diff --git a/homeassistant/components/tibber/translations/de.json b/homeassistant/components/tibber/translations/de.json index f3f722ae835..2d8c853fcfd 100644 --- a/homeassistant/components/tibber/translations/de.json +++ b/homeassistant/components/tibber/translations/de.json @@ -13,8 +13,7 @@ "data": { "access_token": "Zugangstoken" }, - "description": "Gib dein Zugangsk\u00fcrzel von https://developer.tibber.com/settings/accesstoken ein.", - "title": "Tibber" + "description": "Gib dein Zugangsk\u00fcrzel von https://developer.tibber.com/settings/accesstoken ein." } } } diff --git a/homeassistant/components/tibber/translations/el.json b/homeassistant/components/tibber/translations/el.json index 0ad70ba6ac7..ea4cda01a8c 100644 --- a/homeassistant/components/tibber/translations/el.json +++ b/homeassistant/components/tibber/translations/el.json @@ -13,8 +13,7 @@ "data": { "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" }, - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b1\u03c0\u03cc \u03c4\u03bf https://developer.tibber.com/settings/accesstoken", - "title": "Tibber" + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03b1\u03c0\u03cc \u03c4\u03bf https://developer.tibber.com/settings/accesstoken" } } } diff --git a/homeassistant/components/tibber/translations/en.json b/homeassistant/components/tibber/translations/en.json index 63df31dca07..64c8cf0ab1e 100644 --- a/homeassistant/components/tibber/translations/en.json +++ b/homeassistant/components/tibber/translations/en.json @@ -13,8 +13,7 @@ "data": { "access_token": "Access Token" }, - "description": "Enter your access token from https://developer.tibber.com/settings/accesstoken", - "title": "Tibber" + "description": "Enter your access token from https://developer.tibber.com/settings/accesstoken" } } } diff --git a/homeassistant/components/tibber/translations/es.json b/homeassistant/components/tibber/translations/es.json index cdf80422023..0840339ac24 100644 --- a/homeassistant/components/tibber/translations/es.json +++ b/homeassistant/components/tibber/translations/es.json @@ -13,8 +13,7 @@ "data": { "access_token": "Token de acceso" }, - "description": "Introduzca su token de acceso desde https://developer.tibber.com/settings/accesstoken", - "title": "Tibber" + "description": "Introduzca su token de acceso desde https://developer.tibber.com/settings/accesstoken" } } } diff --git a/homeassistant/components/tibber/translations/et.json b/homeassistant/components/tibber/translations/et.json index 5edaac058c6..c00ab6821ad 100644 --- a/homeassistant/components/tibber/translations/et.json +++ b/homeassistant/components/tibber/translations/et.json @@ -13,8 +13,7 @@ "data": { "access_token": "Juurdep\u00e4\u00e4sut\u00f5end" }, - "description": "Sisesta oma juurdep\u00e4\u00e4suluba saidilt https://developer.tibber.com/settings/accesstoken", - "title": "" + "description": "Sisesta oma juurdep\u00e4\u00e4suluba saidilt https://developer.tibber.com/settings/accesstoken" } } } diff --git a/homeassistant/components/tibber/translations/fi.json b/homeassistant/components/tibber/translations/fi.json deleted file mode 100644 index 87e0bef29b2..00000000000 --- a/homeassistant/components/tibber/translations/fi.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "Tibber" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/fr.json b/homeassistant/components/tibber/translations/fr.json index 256516c44a6..1834878d731 100644 --- a/homeassistant/components/tibber/translations/fr.json +++ b/homeassistant/components/tibber/translations/fr.json @@ -13,8 +13,7 @@ "data": { "access_token": "Jeton d'acc\u00e8s" }, - "description": "Entrez votre jeton d'acc\u00e8s depuis https://developer.tibber.com/settings/accesstoken", - "title": "Tibber" + "description": "Entrez votre jeton d'acc\u00e8s depuis https://developer.tibber.com/settings/accesstoken" } } } diff --git a/homeassistant/components/tibber/translations/he.json b/homeassistant/components/tibber/translations/he.json index 3deeeef2e9c..d599be8e8cd 100644 --- a/homeassistant/components/tibber/translations/he.json +++ b/homeassistant/components/tibber/translations/he.json @@ -13,8 +13,7 @@ "data": { "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4" }, - "description": "\u05d4\u05d6\u05df \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d4\u05d2\u05d9\u05e9\u05d4 \u05e9\u05dc\u05da \u05de\u05behttps://developer.tibber.com/settings/accesstoken", - "title": "Tibber" + "description": "\u05d4\u05d6\u05df \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d4\u05d2\u05d9\u05e9\u05d4 \u05e9\u05dc\u05da \u05de\u05behttps://developer.tibber.com/settings/accesstoken" } } } diff --git a/homeassistant/components/tibber/translations/hu.json b/homeassistant/components/tibber/translations/hu.json index 1ff558b7280..57731668a90 100644 --- a/homeassistant/components/tibber/translations/hu.json +++ b/homeassistant/components/tibber/translations/hu.json @@ -13,8 +13,7 @@ "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token" }, - "description": "Adja meg a hozz\u00e1f\u00e9r\u00e9si tokent a https://developer.tibber.com/settings/accesstoken c\u00edmr\u0151l", - "title": "Tibber" + "description": "Adja meg a hozz\u00e1f\u00e9r\u00e9si tokent a https://developer.tibber.com/settings/accesstoken c\u00edmr\u0151l" } } } diff --git a/homeassistant/components/tibber/translations/id.json b/homeassistant/components/tibber/translations/id.json index 479cf83f8c7..a80aec5ee3f 100644 --- a/homeassistant/components/tibber/translations/id.json +++ b/homeassistant/components/tibber/translations/id.json @@ -13,8 +13,7 @@ "data": { "access_token": "Token Akses" }, - "description": "Masukkan token akses Anda dari https://developer.tibber.com/settings/accesstoken", - "title": "Tibber" + "description": "Masukkan token akses Anda dari https://developer.tibber.com/settings/accesstoken" } } } diff --git a/homeassistant/components/tibber/translations/it.json b/homeassistant/components/tibber/translations/it.json index aca7d94083a..2058c2260ad 100644 --- a/homeassistant/components/tibber/translations/it.json +++ b/homeassistant/components/tibber/translations/it.json @@ -13,8 +13,7 @@ "data": { "access_token": "Token di accesso" }, - "description": "Immettere il token di accesso da https://developer.tibber.com/settings/accesstoken", - "title": "Tibber" + "description": "Immettere il token di accesso da https://developer.tibber.com/settings/accesstoken" } } } diff --git a/homeassistant/components/tibber/translations/ja.json b/homeassistant/components/tibber/translations/ja.json index ed3c2f71357..0d16d720159 100644 --- a/homeassistant/components/tibber/translations/ja.json +++ b/homeassistant/components/tibber/translations/ja.json @@ -13,8 +13,7 @@ "data": { "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" }, - "description": "https://developer.tibber.com/settings/accesstoken \u304b\u3089\u306e\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u3092\u5165\u529b\u3057\u307e\u3059", - "title": "Tibber" + "description": "https://developer.tibber.com/settings/accesstoken \u304b\u3089\u306e\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u3092\u5165\u529b\u3057\u307e\u3059" } } } diff --git a/homeassistant/components/tibber/translations/ko.json b/homeassistant/components/tibber/translations/ko.json index 5ba1f62e4ed..ff15748a948 100644 --- a/homeassistant/components/tibber/translations/ko.json +++ b/homeassistant/components/tibber/translations/ko.json @@ -13,8 +13,7 @@ "data": { "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070" }, - "description": "https://developer.tibber.com/settings/accesstoken \uc5d0\uc11c \uc0dd\uc131\ud55c \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", - "title": "Tibber" + "description": "https://developer.tibber.com/settings/accesstoken \uc5d0\uc11c \uc0dd\uc131\ud55c \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694" } } } diff --git a/homeassistant/components/tibber/translations/lb.json b/homeassistant/components/tibber/translations/lb.json index fc84bd1af6f..33ef47fe2de 100644 --- a/homeassistant/components/tibber/translations/lb.json +++ b/homeassistant/components/tibber/translations/lb.json @@ -13,8 +13,7 @@ "data": { "access_token": "Acc\u00e8ss Jeton" }, - "description": "F\u00ebll d\u00e4in Acc\u00e8s Jeton vun https://developer.tibber.com/settings/accesstoken aus", - "title": "Tibber" + "description": "F\u00ebll d\u00e4in Acc\u00e8s Jeton vun https://developer.tibber.com/settings/accesstoken aus" } } } diff --git a/homeassistant/components/tibber/translations/nl.json b/homeassistant/components/tibber/translations/nl.json index 4a5e518f306..622e35baea4 100644 --- a/homeassistant/components/tibber/translations/nl.json +++ b/homeassistant/components/tibber/translations/nl.json @@ -13,8 +13,7 @@ "data": { "access_token": "Toegangstoken" }, - "description": "Voer uw toegangstoken in van https://developer.tibber.com/settings/accesstoken", - "title": "Tibber" + "description": "Voer uw toegangstoken in van https://developer.tibber.com/settings/accesstoken" } } } diff --git a/homeassistant/components/tibber/translations/no.json b/homeassistant/components/tibber/translations/no.json index 14bf482e381..00d56edf5d4 100644 --- a/homeassistant/components/tibber/translations/no.json +++ b/homeassistant/components/tibber/translations/no.json @@ -13,8 +13,7 @@ "data": { "access_token": "Tilgangstoken" }, - "description": "Fyll inn din tilgangstoken fra [https://developer.tibber.com/settings/accesstoken](https://developer.tibber.com/settings/accesstoken)", - "title": "" + "description": "Fyll inn din tilgangstoken fra [https://developer.tibber.com/settings/accesstoken](https://developer.tibber.com/settings/accesstoken)" } } } diff --git a/homeassistant/components/tibber/translations/pl.json b/homeassistant/components/tibber/translations/pl.json index d62027da3d9..7019bb3d17b 100644 --- a/homeassistant/components/tibber/translations/pl.json +++ b/homeassistant/components/tibber/translations/pl.json @@ -13,8 +13,7 @@ "data": { "access_token": "Token dost\u0119pu" }, - "description": "Wprowad\u017a token dost\u0119pu z https://developer.tibber.com/settings/accesstoken", - "title": "Tibber" + "description": "Wprowad\u017a token dost\u0119pu z https://developer.tibber.com/settings/accesstoken" } } } diff --git a/homeassistant/components/tibber/translations/pt-BR.json b/homeassistant/components/tibber/translations/pt-BR.json index 7ff66347009..57eb82ff948 100644 --- a/homeassistant/components/tibber/translations/pt-BR.json +++ b/homeassistant/components/tibber/translations/pt-BR.json @@ -13,8 +13,7 @@ "data": { "access_token": "Token de acesso" }, - "description": "Insira seu token de acesso em https://developer.tibber.com/settings/accesstoken", - "title": "Tibber" + "description": "Insira seu token de acesso em https://developer.tibber.com/settings/accesstoken" } } } diff --git a/homeassistant/components/tibber/translations/pt.json b/homeassistant/components/tibber/translations/pt.json index 941089ee0cb..50b5806457b 100644 --- a/homeassistant/components/tibber/translations/pt.json +++ b/homeassistant/components/tibber/translations/pt.json @@ -11,8 +11,7 @@ "user": { "data": { "access_token": "" - }, - "title": "" + } } } } diff --git a/homeassistant/components/tibber/translations/ru.json b/homeassistant/components/tibber/translations/ru.json index 7519f581352..6453e65830e 100644 --- a/homeassistant/components/tibber/translations/ru.json +++ b/homeassistant/components/tibber/translations/ru.json @@ -13,8 +13,7 @@ "data": { "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 https://developer.tibber.com/settings/accesstoken", - "title": "Tibber" + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 https://developer.tibber.com/settings/accesstoken" } } } diff --git a/homeassistant/components/tibber/translations/sl.json b/homeassistant/components/tibber/translations/sl.json index 33ec5ab773b..285c07434ce 100644 --- a/homeassistant/components/tibber/translations/sl.json +++ b/homeassistant/components/tibber/translations/sl.json @@ -12,8 +12,7 @@ "data": { "access_token": "Dostopni \u017eeton" }, - "description": "Vnesite svoj dostopni \u017eeton s strani https://developer.tibber.com/settings/accesstoken", - "title": "Tibber" + "description": "Vnesite svoj dostopni \u017eeton s strani https://developer.tibber.com/settings/accesstoken" } } } diff --git a/homeassistant/components/tibber/translations/tr.json b/homeassistant/components/tibber/translations/tr.json index a34aab70d6c..d7a98f49760 100644 --- a/homeassistant/components/tibber/translations/tr.json +++ b/homeassistant/components/tibber/translations/tr.json @@ -13,8 +13,7 @@ "data": { "access_token": "Eri\u015fim Belirteci" }, - "description": "https://developer.tibber.com/settings/accesstoken adresinden eri\u015fim anahtar\u0131n\u0131z\u0131 girin", - "title": "Tibber" + "description": "https://developer.tibber.com/settings/accesstoken adresinden eri\u015fim anahtar\u0131n\u0131z\u0131 girin" } } } diff --git a/homeassistant/components/tibber/translations/uk.json b/homeassistant/components/tibber/translations/uk.json index b1240116856..ba38a372472 100644 --- a/homeassistant/components/tibber/translations/uk.json +++ b/homeassistant/components/tibber/translations/uk.json @@ -13,8 +13,7 @@ "data": { "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443" }, - "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443, \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u0438\u0439 \u043d\u0430 \u0441\u0430\u0439\u0442\u0456 https://developer.tibber.com/settings/accesstoken", - "title": "Tibber" + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443, \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u0438\u0439 \u043d\u0430 \u0441\u0430\u0439\u0442\u0456 https://developer.tibber.com/settings/accesstoken" } } } diff --git a/homeassistant/components/tibber/translations/zh-Hant.json b/homeassistant/components/tibber/translations/zh-Hant.json index e4d0ec10e23..3bc02f810d5 100644 --- a/homeassistant/components/tibber/translations/zh-Hant.json +++ b/homeassistant/components/tibber/translations/zh-Hant.json @@ -13,8 +13,7 @@ "data": { "access_token": "\u5b58\u53d6\u6b0a\u6756" }, - "description": "\u8f38\u5165\u7531 https://developer.tibber.com/settings/accesstoken \u6240\u7372\u5f97\u7684\u5b58\u53d6\u6b0a\u6756", - "title": "Tibber" + "description": "\u8f38\u5165\u7531 https://developer.tibber.com/settings/accesstoken \u6240\u7372\u5f97\u7684\u5b58\u53d6\u6b0a\u6756" } } } diff --git a/homeassistant/components/tod/translations/ca.json b/homeassistant/components/tod/translations/ca.json index 3908cb7761f..2f62e5c8fb4 100644 --- a/homeassistant/components/tod/translations/ca.json +++ b/homeassistant/components/tod/translations/ca.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "after": "Activa despr\u00e9s de", "after_time": "Temps d'activaci\u00f3", - "before": "Desactiva despr\u00e9s de", "before_time": "Temps de desactivaci\u00f3", "name": "Nom" }, @@ -18,12 +16,9 @@ "step": { "init": { "data": { - "after": "Activa despr\u00e9s de", "after_time": "Temps d'activaci\u00f3", - "before": "Desactiva despr\u00e9s de", "before_time": "Temps de desactivaci\u00f3" - }, - "description": "Crea un sensor binari que s'activa o es desactiva en funci\u00f3 de l'hora." + } } } }, diff --git a/homeassistant/components/tod/translations/de.json b/homeassistant/components/tod/translations/de.json index eeb3d4dd8f9..663dc21c993 100644 --- a/homeassistant/components/tod/translations/de.json +++ b/homeassistant/components/tod/translations/de.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "after": "Angeschaltet nach", "after_time": "Einschaltzeit", - "before": "Ausgeschaltet nach", "before_time": "Ausschaltzeit", "name": "Name" }, @@ -18,12 +16,9 @@ "step": { "init": { "data": { - "after": "Angeschaltet nach", "after_time": "Einschaltzeit", - "before": "Ausgeschaltet nach", "before_time": "Ausschaltzeit" - }, - "description": "Erstelle einen bin\u00e4ren Sensor, der sich je nach Uhrzeit ein- oder ausschaltet." + } } } }, diff --git a/homeassistant/components/tod/translations/el.json b/homeassistant/components/tod/translations/el.json index ffa426365a4..453e36c4020 100644 --- a/homeassistant/components/tod/translations/el.json +++ b/homeassistant/components/tod/translations/el.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "after": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03bc\u03b5\u03c4\u03ac", "after_time": "\u0395\u03bd\u03c4\u03cc\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5", - "before": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03bc\u03b5\u03c4\u03ac", "before_time": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5", "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, @@ -18,12 +16,9 @@ "step": { "init": { "data": { - "after": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03bc\u03b5\u03c4\u03ac", "after_time": "\u0395\u03bd\u03c4\u03cc\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5", - "before": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03bc\u03b5\u03c4\u03ac", "before_time": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5" - }, - "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c0\u03cc\u03c4\u03b5 \u03bf \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9." + } } } }, diff --git a/homeassistant/components/tod/translations/en.json b/homeassistant/components/tod/translations/en.json index ced14151519..2ecb2c695c8 100644 --- a/homeassistant/components/tod/translations/en.json +++ b/homeassistant/components/tod/translations/en.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "after": "On after", "after_time": "On time", - "before": "Off after", "before_time": "Off time", "name": "Name" }, @@ -18,12 +16,9 @@ "step": { "init": { "data": { - "after": "On after", "after_time": "On time", - "before": "Off after", "before_time": "Off time" - }, - "description": "Create a binary sensor that turns on or off depending on the time." + } } } }, diff --git a/homeassistant/components/tod/translations/es.json b/homeassistant/components/tod/translations/es.json index 899b4ce18f0..302dc6cfdd9 100644 --- a/homeassistant/components/tod/translations/es.json +++ b/homeassistant/components/tod/translations/es.json @@ -16,8 +16,7 @@ "init": { "data": { "after_time": "Tiempo de activaci\u00f3n" - }, - "description": "Crea un sensor binario que se activa o desactiva en funci\u00f3n de la hora." + } } } }, diff --git a/homeassistant/components/tod/translations/et.json b/homeassistant/components/tod/translations/et.json index 06b40e0ce72..9d3fb544309 100644 --- a/homeassistant/components/tod/translations/et.json +++ b/homeassistant/components/tod/translations/et.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "after": "Kestus sissel\u00fclitumisest", "after_time": "Seesoleku aeg", - "before": "Kestus v\u00e4ljal\u00fclitumisest", "before_time": "V\u00e4ljasoleku aeg", "name": "Nimi" }, @@ -18,12 +16,9 @@ "step": { "init": { "data": { - "after": "Kestus sissel\u00fclitumisest", "after_time": "Seesoleku aeg", - "before": "Kestus v\u00e4ljal\u00fclitumisest", "before_time": "V\u00e4ljasoleku aeg" - }, - "description": "Vali millal andur on sisse v\u00f5i v\u00e4lja l\u00fclitatud." + } } } }, diff --git a/homeassistant/components/tod/translations/fr.json b/homeassistant/components/tod/translations/fr.json index 799e286343b..d430ede1295 100644 --- a/homeassistant/components/tod/translations/fr.json +++ b/homeassistant/components/tod/translations/fr.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "after": "Activ\u00e9 apr\u00e8s", "after_time": "Heure d'activation", - "before": "D\u00e9sactiv\u00e9 apr\u00e8s", "before_time": "Heure de d\u00e9sactivation", "name": "Nom" }, @@ -18,12 +16,9 @@ "step": { "init": { "data": { - "after": "Activ\u00e9 apr\u00e8s", "after_time": "Heure d'activation", - "before": "D\u00e9sactiv\u00e9 apr\u00e8s", "before_time": "Heure de d\u00e9sactivation" - }, - "description": "Cr\u00e9ez un capteur binaire qui s'active et se d\u00e9sactive en fonction de l'heure." + } } } }, diff --git a/homeassistant/components/tod/translations/he.json b/homeassistant/components/tod/translations/he.json index b10f9e2b1ca..efa0a9dc244 100644 --- a/homeassistant/components/tod/translations/he.json +++ b/homeassistant/components/tod/translations/he.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "after": "\u05d4\u05e4\u05e2\u05dc\u05d4 \u05dc\u05d0\u05d7\u05e8", "after_time": "\u05d6\u05de\u05df \u05d4\u05e4\u05e2\u05dc\u05d4", - "before": "\u05db\u05d9\u05d1\u05d5\u05d9 \u05dc\u05d0\u05d7\u05e8", "before_time": "\u05d6\u05de\u05df \u05db\u05d9\u05d1\u05d5\u05d9", "name": "\u05e9\u05dd" }, @@ -18,12 +16,9 @@ "step": { "init": { "data": { - "after": "\u05d4\u05e4\u05e2\u05dc\u05d4 \u05dc\u05d0\u05d7\u05e8", "after_time": "\u05d6\u05de\u05df \u05d4\u05e4\u05e2\u05dc\u05d4", - "before": "\u05db\u05d9\u05d1\u05d5\u05d9 \u05dc\u05d0\u05d7\u05e8", "before_time": "\u05d6\u05de\u05df \u05db\u05d9\u05d1\u05d5\u05d9" - }, - "description": "\u05d9\u05e6\u05d9\u05e8\u05ea \u05d7\u05d9\u05d9\u05e9\u05df \u05d1\u05d9\u05e0\u05d0\u05e8\u05d9 \u05d4\u05de\u05d5\u05e4\u05e2\u05dc \u05d0\u05d5 \u05e0\u05db\u05d1\u05d4 \u05d1\u05d4\u05ea\u05d0\u05dd \u05dc\u05d6\u05de\u05df." + } } } } diff --git a/homeassistant/components/tod/translations/hu.json b/homeassistant/components/tod/translations/hu.json index 28af029f230..feb4d66e40e 100644 --- a/homeassistant/components/tod/translations/hu.json +++ b/homeassistant/components/tod/translations/hu.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "after": "BE ut\u00e1n", "after_time": "BE id\u0151pont", - "before": "KI ut\u00e1n", "before_time": "KI id\u0151pont", "name": "Elnevez\u00e9s" }, @@ -18,12 +16,9 @@ "step": { "init": { "data": { - "after": "BE ut\u00e1n", "after_time": "BE id\u0151pont", - "before": "KI ut\u00e1n", "before_time": "KI id\u0151pont" - }, - "description": "\u00c1ll\u00edtsa be, hogy az \u00e9rz\u00e9kel\u0151 mikor kapcsoljon be \u00e9s ki." + } } } }, diff --git a/homeassistant/components/tod/translations/id.json b/homeassistant/components/tod/translations/id.json index 2ceef6c3a40..2a3681e7d78 100644 --- a/homeassistant/components/tod/translations/id.json +++ b/homeassistant/components/tod/translations/id.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "after": "Nyala setelah", "after_time": "Nyala pada", - "before": "Mati setelah", "before_time": "Mati pada", "name": "Nama" }, @@ -18,12 +16,9 @@ "step": { "init": { "data": { - "after": "Nyala setelah", "after_time": "Nyala pada", - "before": "Mati setelah", "before_time": "Mati pada" - }, - "description": "Buat sensor biner yang nyala atau mati tergantung waktu." + } } } }, diff --git a/homeassistant/components/tod/translations/it.json b/homeassistant/components/tod/translations/it.json index 072cc80f5ff..69015bf02f7 100644 --- a/homeassistant/components/tod/translations/it.json +++ b/homeassistant/components/tod/translations/it.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "after": "Acceso dopo", "after_time": "Ora di accensione", - "before": "Spento dopo", "before_time": "Ora di spegnimento", "name": "Nome" }, @@ -18,12 +16,9 @@ "step": { "init": { "data": { - "after": "Acceso dopo", "after_time": "Ora di accensione", - "before": "Spento dopo", "before_time": "Ora di spegnimento" - }, - "description": "Crea un sensore binario che si accende o si spegne a seconda dell'ora." + } } } }, diff --git a/homeassistant/components/tod/translations/ja.json b/homeassistant/components/tod/translations/ja.json index a497f3e3c98..10bad2407d0 100644 --- a/homeassistant/components/tod/translations/ja.json +++ b/homeassistant/components/tod/translations/ja.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "after": "\u5f8c\u306b\u30aa\u30f3(On after)", "after_time": "\u30aa\u30f3\u30bf\u30a4\u30e0(On time)", - "before": "\u30aa\u30d5\u5f8c(Off after)", "before_time": "\u30aa\u30d5\u30bf\u30a4\u30e0(Off time)", "name": "\u540d\u524d" }, @@ -18,12 +16,9 @@ "step": { "init": { "data": { - "after": "\u5f8c\u306b\u30aa\u30f3(On after)", "after_time": "\u30aa\u30f3\u30bf\u30a4\u30e0(On time)", - "before": "\u30aa\u30d5\u5f8c(Off after)", "before_time": "\u30aa\u30d5\u30bf\u30a4\u30e0(Off time)" - }, - "description": "\u30bb\u30f3\u30b5\u30fc\u306e\u30aa\u30f3\u3068\u30aa\u30d5\u3092\u5207\u308a\u66ff\u3048\u308b\u30bf\u30a4\u30df\u30f3\u30b0\u3092\u69cb\u6210\u3057\u307e\u3059\u3002" + } } } }, diff --git a/homeassistant/components/tod/translations/ko.json b/homeassistant/components/tod/translations/ko.json new file mode 100644 index 00000000000..474497f6557 --- /dev/null +++ b/homeassistant/components/tod/translations/ko.json @@ -0,0 +1,3 @@ +{ + "title": "\ubc94\uc704\uc2dc\uac04 \uc13c\uc11c" +} \ No newline at end of file diff --git a/homeassistant/components/tod/translations/nl.json b/homeassistant/components/tod/translations/nl.json index 82f06ea3f4f..374e295f691 100644 --- a/homeassistant/components/tod/translations/nl.json +++ b/homeassistant/components/tod/translations/nl.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "after": "Aan na", "after_time": "Op tijd", - "before": "Uit na", "before_time": "Uit tijd", "name": "Naam\n" }, @@ -18,12 +16,9 @@ "step": { "init": { "data": { - "after": "Aan na", "after_time": "Op tijd", - "before": "Uit na", "before_time": "Uit tijd" - }, - "description": "Maak een binaire sensor die afhankelijk van de tijd in- of uitgeschakeld wordt." + } } } }, diff --git a/homeassistant/components/tod/translations/no.json b/homeassistant/components/tod/translations/no.json index a2cbf7e6427..196c00663fe 100644 --- a/homeassistant/components/tod/translations/no.json +++ b/homeassistant/components/tod/translations/no.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "after": "P\u00e5 etter", "after_time": "P\u00e5 tide", - "before": "Av etter", "before_time": "Utenfor arbeidstid", "name": "Navn" }, @@ -18,12 +16,9 @@ "step": { "init": { "data": { - "after": "P\u00e5 etter", "after_time": "P\u00e5 tide", - "before": "Av etter", "before_time": "Utenfor arbeidstid" - }, - "description": "Lag en bin\u00e6r sensor som sl\u00e5s av eller p\u00e5 avhengig av tiden." + } } } }, diff --git a/homeassistant/components/tod/translations/pl.json b/homeassistant/components/tod/translations/pl.json index 8b03a798f5c..14d74f12b27 100644 --- a/homeassistant/components/tod/translations/pl.json +++ b/homeassistant/components/tod/translations/pl.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "after": "W\u0142\u0105cz po", "after_time": "Czas w\u0142\u0105czenia", - "before": "Wy\u0142\u0105cz po", "before_time": "Czas wy\u0142\u0105czenia", "name": "Nazwa" }, @@ -18,12 +16,9 @@ "step": { "init": { "data": { - "after": "W\u0142\u0105cz po", "after_time": "Czas w\u0142\u0105czenia", - "before": "Wy\u0142\u0105cz po", "before_time": "Czas wy\u0142\u0105czenia" - }, - "description": "Utw\u00f3rz sensor binarny, kt\u00f3ry w\u0142\u0105cza si\u0119 lub wy\u0142\u0105cza w zale\u017cno\u015bci od czasu." + } } } }, diff --git a/homeassistant/components/tod/translations/pt-BR.json b/homeassistant/components/tod/translations/pt-BR.json index e9784076fd7..0275ad3e148 100644 --- a/homeassistant/components/tod/translations/pt-BR.json +++ b/homeassistant/components/tod/translations/pt-BR.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "after": "Ligado depois", "after_time": "Ligado na hora", - "before": "Desligado depois", "before_time": "Desligado na hora", "name": "Nome" }, @@ -18,12 +16,9 @@ "step": { "init": { "data": { - "after": "Ligado depois", "after_time": "Ligado na hora", - "before": "Desligado depois", "before_time": "Desligado na hora" - }, - "description": "Crie um sensor bin\u00e1rio que liga ou desliga dependendo do tempo." + } } } }, diff --git a/homeassistant/components/tod/translations/ru.json b/homeassistant/components/tod/translations/ru.json index eda3e4efd77..c470dea348d 100644 --- a/homeassistant/components/tod/translations/ru.json +++ b/homeassistant/components/tod/translations/ru.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "after": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u043e\u0441\u043b\u0435", "after_time": "\u0412\u0440\u0435\u043c\u044f \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", - "before": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u043e\u0441\u043b\u0435", "before_time": "\u0412\u0440\u0435\u043c\u044f \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, @@ -18,12 +16,9 @@ "step": { "init": { "data": { - "after": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u043e\u0441\u043b\u0435", "after_time": "\u0412\u0440\u0435\u043c\u044f \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", - "before": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u043e\u0441\u043b\u0435", "before_time": "\u0412\u0440\u0435\u043c\u044f \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" - }, - "description": "\u0411\u0438\u043d\u0430\u0440\u043d\u044b\u0439 \u0441\u0435\u043d\u0441\u043e\u0440, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0438\u043b\u0438 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0432\u0440\u0435\u043c\u0435\u043d\u0438." + } } } }, diff --git a/homeassistant/components/tod/translations/tr.json b/homeassistant/components/tod/translations/tr.json index e2f04757369..8013dff322b 100644 --- a/homeassistant/components/tod/translations/tr.json +++ b/homeassistant/components/tod/translations/tr.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "after": "Daha sonra", "after_time": "Vaktinde", - "before": "Sonra kapat", "before_time": "Kapatma zaman\u0131", "name": "Ad" }, @@ -18,12 +16,9 @@ "step": { "init": { "data": { - "after": "Daha sonra", "after_time": "Vaktinde", - "before": "Sonra kapat", "before_time": "Kapatma zaman\u0131" - }, - "description": "Zamana ba\u011fl\u0131 olarak a\u00e7\u0131l\u0131p kapanan bir ikili sens\u00f6r olu\u015fturun." + } } } }, diff --git a/homeassistant/components/tod/translations/zh-Hans.json b/homeassistant/components/tod/translations/zh-Hans.json index 1c0b6ba1597..32306b99659 100644 --- a/homeassistant/components/tod/translations/zh-Hans.json +++ b/homeassistant/components/tod/translations/zh-Hans.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "after": "\u5728\u6b64\u65f6\u95f4\u540e\u6253\u5f00", "after_time": "\u6253\u5f00\u65f6\u95f4", - "before": "\u5728\u6b64\u65f6\u95f4\u540e\u5173\u95ed", "before_time": "\u5173\u95ed\u65f6\u95f4", "name": "\u540d\u79f0" }, @@ -18,12 +16,9 @@ "step": { "init": { "data": { - "after": "\u5728\u6b64\u65f6\u95f4\u540e\u6253\u5f00", "after_time": "\u6253\u5f00\u65f6\u95f4", - "before": "\u5728\u6b64\u65f6\u95f4\u540e\u5173\u95ed", "before_time": "\u5173\u95ed\u65f6\u95f4" - }, - "description": "\u521b\u5efa\u6839\u636e\u65f6\u95f4\u6539\u53d8\u5f00\u5173\u72b6\u6001\u7684\u4e8c\u5143\u4f20\u611f\u5668\u3002" + } } } }, diff --git a/homeassistant/components/tod/translations/zh-Hant.json b/homeassistant/components/tod/translations/zh-Hant.json index 0a47c33a318..333d092a613 100644 --- a/homeassistant/components/tod/translations/zh-Hant.json +++ b/homeassistant/components/tod/translations/zh-Hant.json @@ -3,9 +3,7 @@ "step": { "user": { "data": { - "after": "\u958b\u555f\u6642\u9577\u5f8c", "after_time": "\u958b\u555f\u6642\u9593", - "before": "\u95dc\u9589\u6642\u9577\u5f8c", "before_time": "\u95dc\u9589\u6642\u9593", "name": "\u540d\u7a31" }, @@ -18,12 +16,9 @@ "step": { "init": { "data": { - "after": "\u958b\u555f\u6642\u9577\u5f8c", "after_time": "\u958b\u555f\u6642\u9593", - "before": "\u95dc\u9589\u6642\u9577\u5f8c", "before_time": "\u95dc\u9589\u6642\u9593" - }, - "description": "\u65b0\u589e\u6839\u64da\u6642\u9593\u6c7a\u5b9a\u958b\u95dc\u4e4b\u6642\u9593\u611f\u6e2c\u5668\u3002" + } } } }, diff --git a/homeassistant/components/tolo/translations/bg.json b/homeassistant/components/tolo/translations/bg.json index f1c33573305..5f89c6bd4bd 100644 --- a/homeassistant/components/tolo/translations/bg.json +++ b/homeassistant/components/tolo/translations/bg.json @@ -1,8 +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", - "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" + "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\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" diff --git a/homeassistant/components/tolo/translations/ca.json b/homeassistant/components/tolo/translations/ca.json index 06d201141a5..567f4467f0b 100644 --- a/homeassistant/components/tolo/translations/ca.json +++ b/homeassistant/components/tolo/translations/ca.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat", - "no_devices_found": "No s'han trobat dispositius a la xarxa" + "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3" diff --git a/homeassistant/components/tolo/translations/de.json b/homeassistant/components/tolo/translations/de.json index 6002d2ada8b..3fc78255507 100644 --- a/homeassistant/components/tolo/translations/de.json +++ b/homeassistant/components/tolo/translations/de.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen" diff --git a/homeassistant/components/tolo/translations/el.json b/homeassistant/components/tolo/translations/el.json index 6b745e37a8e..16604a30baa 100644 --- a/homeassistant/components/tolo/translations/el.json +++ b/homeassistant/components/tolo/translations/el.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", - "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" diff --git a/homeassistant/components/tolo/translations/en.json b/homeassistant/components/tolo/translations/en.json index 488c2f7ae69..dea5a3b30df 100644 --- a/homeassistant/components/tolo/translations/en.json +++ b/homeassistant/components/tolo/translations/en.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Device is already configured", - "no_devices_found": "No devices found on the network" + "already_configured": "Device is already configured" }, "error": { "cannot_connect": "Failed to connect" diff --git a/homeassistant/components/tolo/translations/es.json b/homeassistant/components/tolo/translations/es.json index 573d6b7c344..76cb6c73275 100644 --- a/homeassistant/components/tolo/translations/es.json +++ b/homeassistant/components/tolo/translations/es.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado", - "no_devices_found": "No se encontraron dispositivos en la red" + "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { "cannot_connect": "No se pudo conectar" diff --git a/homeassistant/components/tolo/translations/et.json b/homeassistant/components/tolo/translations/et.json index 57d59b85713..ed09907df82 100644 --- a/homeassistant/components/tolo/translations/et.json +++ b/homeassistant/components/tolo/translations/et.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud", - "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" }, "error": { "cannot_connect": "\u00dchendamine nurjus" diff --git a/homeassistant/components/tolo/translations/fr.json b/homeassistant/components/tolo/translations/fr.json index 40b61d012a2..c27da305516 100644 --- a/homeassistant/components/tolo/translations/fr.json +++ b/homeassistant/components/tolo/translations/fr.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { "cannot_connect": "\u00c9chec de connexion" diff --git a/homeassistant/components/tolo/translations/he.json b/homeassistant/components/tolo/translations/he.json index 9da8a69a4fe..39ea73680a3 100644 --- a/homeassistant/components/tolo/translations/he.json +++ b/homeassistant/components/tolo/translations/he.json @@ -1,8 +1,7 @@ { "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" + "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" diff --git a/homeassistant/components/tolo/translations/hu.json b/homeassistant/components/tolo/translations/hu.json index 55239599c16..246edba220d 100644 --- a/homeassistant/components/tolo/translations/hu.json +++ b/homeassistant/components/tolo/translations/hu.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" diff --git a/homeassistant/components/tolo/translations/id.json b/homeassistant/components/tolo/translations/id.json index 53ea0e46cb1..88e22f45b0b 100644 --- a/homeassistant/components/tolo/translations/id.json +++ b/homeassistant/components/tolo/translations/id.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi", - "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + "already_configured": "Perangkat sudah dikonfigurasi" }, "error": { "cannot_connect": "Gagal terhubung" diff --git a/homeassistant/components/tolo/translations/it.json b/homeassistant/components/tolo/translations/it.json index 83704e3e767..169aaa7ebce 100644 --- a/homeassistant/components/tolo/translations/it.json +++ b/homeassistant/components/tolo/translations/it.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "no_devices_found": "Nessun dispositivo trovato sulla rete" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { "cannot_connect": "Impossibile connettersi" diff --git a/homeassistant/components/tolo/translations/ja.json b/homeassistant/components/tolo/translations/ja.json index f8d4a1646ae..f901986b6d2 100644 --- a/homeassistant/components/tolo/translations/ja.json +++ b/homeassistant/components/tolo/translations/ja.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" diff --git a/homeassistant/components/tolo/translations/ko.json b/homeassistant/components/tolo/translations/ko.json new file mode 100644 index 00000000000..20ad990e862 --- /dev/null +++ b/homeassistant/components/tolo/translations/ko.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/nl.json b/homeassistant/components/tolo/translations/nl.json index f65f6bae7c1..dcc6f1936d0 100644 --- a/homeassistant/components/tolo/translations/nl.json +++ b/homeassistant/components/tolo/translations/nl.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd", - "no_devices_found": "Geen apparaten gevonden op het netwerk" + "already_configured": "Apparaat is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken" diff --git a/homeassistant/components/tolo/translations/no.json b/homeassistant/components/tolo/translations/no.json index 20311dd8f69..16b9c0f2ead 100644 --- a/homeassistant/components/tolo/translations/no.json +++ b/homeassistant/components/tolo/translations/no.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert", - "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" + "already_configured": "Enheten er allerede konfigurert" }, "error": { "cannot_connect": "Tilkobling mislyktes" diff --git a/homeassistant/components/tolo/translations/pl.json b/homeassistant/components/tolo/translations/pl.json index 4809e00e29f..0bf5b5f75f5 100644 --- a/homeassistant/components/tolo/translations/pl.json +++ b/homeassistant/components/tolo/translations/pl.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" diff --git a/homeassistant/components/tolo/translations/pt-BR.json b/homeassistant/components/tolo/translations/pt-BR.json index c86e10b4e8e..457fb339b2d 100644 --- a/homeassistant/components/tolo/translations/pt-BR.json +++ b/homeassistant/components/tolo/translations/pt-BR.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", - "no_devices_found": "Nenhum dispositivo encontrado na rede" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { "cannot_connect": "Falha ao conectar" diff --git a/homeassistant/components/tolo/translations/ru.json b/homeassistant/components/tolo/translations/ru.json index 82fdffdb8b1..0243a40cf7e 100644 --- a/homeassistant/components/tolo/translations/ru.json +++ b/homeassistant/components/tolo/translations/ru.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", - "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." + "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." diff --git a/homeassistant/components/tolo/translations/sl.json b/homeassistant/components/tolo/translations/sl.json index e32b3eb95ca..62f8746a028 100644 --- a/homeassistant/components/tolo/translations/sl.json +++ b/homeassistant/components/tolo/translations/sl.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Naprava je \u017ee konfigurirana", - "no_devices_found": "V omre\u017eju ni mogo\u010de najti nobene naprave" + "already_configured": "Naprava je \u017ee konfigurirana" }, "error": { "cannot_connect": "Povezava ni uspela" diff --git a/homeassistant/components/tolo/translations/tr.json b/homeassistant/components/tolo/translations/tr.json index f5dd98e93ba..8f2b5ee1939 100644 --- a/homeassistant/components/tolo/translations/tr.json +++ b/homeassistant/components/tolo/translations/tr.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" diff --git a/homeassistant/components/tolo/translations/zh-Hant.json b/homeassistant/components/tolo/translations/zh-Hant.json index d887eb212a1..96cef3b9605 100644 --- a/homeassistant/components/tolo/translations/zh-Hant.json +++ b/homeassistant/components/tolo/translations/zh-Hant.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/tomorrowio/translations/bg.json b/homeassistant/components/tomorrowio/translations/bg.json index 783408dab84..261841d75d1 100644 --- a/homeassistant/components/tomorrowio/translations/bg.json +++ b/homeassistant/components/tomorrowio/translations/bg.json @@ -9,9 +9,7 @@ "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", "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", - "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430", "name": "\u0418\u043c\u0435" } } diff --git a/homeassistant/components/tomorrowio/translations/ca.json b/homeassistant/components/tomorrowio/translations/ca.json index fc351430ffb..6a1289fd7a4 100644 --- a/homeassistant/components/tomorrowio/translations/ca.json +++ b/homeassistant/components/tomorrowio/translations/ca.json @@ -10,9 +10,7 @@ "user": { "data": { "api_key": "Clau API", - "latitude": "Latitud", "location": "Ubicaci\u00f3", - "longitude": "Longitud", "name": "Nom" }, "description": "Per obtenir una clau API, registra't a [Tomorrow.io](https://app.tomorrow.io/signup)." diff --git a/homeassistant/components/tomorrowio/translations/de.json b/homeassistant/components/tomorrowio/translations/de.json index 03739ce5c2f..d65b1115e4b 100644 --- a/homeassistant/components/tomorrowio/translations/de.json +++ b/homeassistant/components/tomorrowio/translations/de.json @@ -10,9 +10,7 @@ "user": { "data": { "api_key": "API-Schl\u00fcssel", - "latitude": "Breitengrad", "location": "Standort", - "longitude": "L\u00e4ngengrad", "name": "Name" }, "description": "Um einen API-Schl\u00fcssel zu erhalten, melde dich bei [Tomorrow.io] (https://app.tomorrow.io/signup) an." diff --git a/homeassistant/components/tomorrowio/translations/el.json b/homeassistant/components/tomorrowio/translations/el.json index 28e3f56c379..aecdca57c9f 100644 --- a/homeassistant/components/tomorrowio/translations/el.json +++ b/homeassistant/components/tomorrowio/translations/el.json @@ -10,9 +10,7 @@ "user": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", - "latitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03c0\u03bb\u03ac\u03c4\u03bf\u03c2", "location": "\u03a4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1", - "longitude": "\u0393\u03b5\u03c9\u03b3\u03c1\u03b1\u03c6\u03b9\u03ba\u03cc \u03bc\u03ae\u03ba\u03bf\u03c2", "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API, \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf [Tomorrow.io](https://app.tomorrow.io/signup)." diff --git a/homeassistant/components/tomorrowio/translations/en.json b/homeassistant/components/tomorrowio/translations/en.json index 103f1c81679..f3706dd6a73 100644 --- a/homeassistant/components/tomorrowio/translations/en.json +++ b/homeassistant/components/tomorrowio/translations/en.json @@ -10,9 +10,7 @@ "user": { "data": { "api_key": "API Key", - "latitude": "Latitude", "location": "Location", - "longitude": "Longitude", "name": "Name" }, "description": "To get an API key, sign up at [Tomorrow.io](https://app.tomorrow.io/signup)." diff --git a/homeassistant/components/tomorrowio/translations/es.json b/homeassistant/components/tomorrowio/translations/es.json index 9ad330dab15..e2f36a0949e 100644 --- a/homeassistant/components/tomorrowio/translations/es.json +++ b/homeassistant/components/tomorrowio/translations/es.json @@ -9,7 +9,6 @@ "user": { "data": { "api_key": "Clave API", - "latitude": "Latitud", "location": "Ubicaci\u00f3n", "name": "Nombre" }, diff --git a/homeassistant/components/tomorrowio/translations/et.json b/homeassistant/components/tomorrowio/translations/et.json index 7ac38239349..0ae1ea43448 100644 --- a/homeassistant/components/tomorrowio/translations/et.json +++ b/homeassistant/components/tomorrowio/translations/et.json @@ -10,9 +10,7 @@ "user": { "data": { "api_key": "API v\u00f5ti", - "latitude": "Laiuskraad", "location": "Asukoht", - "longitude": "Pikkuskraad", "name": "Nimi" }, "description": "API v\u00f5tme saamiseks registreeru aadressil [Tomorrow.io](https://app.tomorrow.io/signup)." diff --git a/homeassistant/components/tomorrowio/translations/fr.json b/homeassistant/components/tomorrowio/translations/fr.json index bcb97eb3fc5..559c734612f 100644 --- a/homeassistant/components/tomorrowio/translations/fr.json +++ b/homeassistant/components/tomorrowio/translations/fr.json @@ -10,9 +10,7 @@ "user": { "data": { "api_key": "Cl\u00e9 d'API", - "latitude": "Latitude", "location": "Emplacement", - "longitude": "Longitude", "name": "Nom" }, "description": "Pour obtenir une cl\u00e9 d'API, inscrivez-vous sur [Tomorrow.io](https://app.tomorrow.io/signup)." diff --git a/homeassistant/components/tomorrowio/translations/he.json b/homeassistant/components/tomorrowio/translations/he.json index 20b48520f18..153e6fcd5f5 100644 --- a/homeassistant/components/tomorrowio/translations/he.json +++ b/homeassistant/components/tomorrowio/translations/he.json @@ -9,9 +9,7 @@ "user": { "data": { "api_key": "\u05de\u05e4\u05ea\u05d7 API", - "latitude": "\u05e7\u05d5 \u05e8\u05d5\u05d7\u05d1", "location": "\u05de\u05d9\u05e7\u05d5\u05dd", - "longitude": "\u05e7\u05d5 \u05d0\u05d5\u05e8\u05da", "name": "\u05e9\u05dd" } } diff --git a/homeassistant/components/tomorrowio/translations/hu.json b/homeassistant/components/tomorrowio/translations/hu.json index 8f90392234e..d619b6346a4 100644 --- a/homeassistant/components/tomorrowio/translations/hu.json +++ b/homeassistant/components/tomorrowio/translations/hu.json @@ -10,9 +10,7 @@ "user": { "data": { "api_key": "API kulcs", - "latitude": "Sz\u00e9less\u00e9g", "location": "Elhelyezked\u00e9s", - "longitude": "Hossz\u00fas\u00e1g", "name": "Elnevez\u00e9s" }, "description": "API-kulcs beszerz\u00e9s\u00e9hez regisztr\u00e1ljon a [Tomorrow.io] (https://app.tomorrow.io/signup) oldalon." diff --git a/homeassistant/components/tomorrowio/translations/id.json b/homeassistant/components/tomorrowio/translations/id.json index b428648e799..a1a6f41888a 100644 --- a/homeassistant/components/tomorrowio/translations/id.json +++ b/homeassistant/components/tomorrowio/translations/id.json @@ -10,9 +10,7 @@ "user": { "data": { "api_key": "Kunci API", - "latitude": "Lintang", "location": "Lokasi", - "longitude": "Bujur", "name": "Nama" }, "description": "Untuk mendapatkan kunci API, daftar di [Tomorrow.io](https://app.tomorrow.io/signup)." diff --git a/homeassistant/components/tomorrowio/translations/it.json b/homeassistant/components/tomorrowio/translations/it.json index 9a79896a3d8..8446eba5336 100644 --- a/homeassistant/components/tomorrowio/translations/it.json +++ b/homeassistant/components/tomorrowio/translations/it.json @@ -10,9 +10,7 @@ "user": { "data": { "api_key": "Chiave API", - "latitude": "Latitudine", "location": "Posizione", - "longitude": "Logitudine", "name": "Nome" }, "description": "Per ottenere una chiave API, registrati su [Tomorrow.io](https://app.tomorrow.io/signup)." diff --git a/homeassistant/components/tomorrowio/translations/ja.json b/homeassistant/components/tomorrowio/translations/ja.json index 17d31f74214..c935416c89b 100644 --- a/homeassistant/components/tomorrowio/translations/ja.json +++ b/homeassistant/components/tomorrowio/translations/ja.json @@ -10,9 +10,7 @@ "user": { "data": { "api_key": "API\u30ad\u30fc", - "latitude": "\u7def\u5ea6", "location": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3", - "longitude": "\u7d4c\u5ea6", "name": "\u540d\u524d" }, "description": "API\u30ad\u30fc\u3092\u53d6\u5f97\u3059\u308b\u306b\u306f\u3001 [Tomorrow.io](https://app.tomorrow.io/signup) \u306b\u30b5\u30a4\u30f3\u30a2\u30c3\u30d7\u3057\u3066\u304f\u3060\u3055\u3044\u3002" diff --git a/homeassistant/components/tomorrowio/translations/nl.json b/homeassistant/components/tomorrowio/translations/nl.json index d1efb7d75c3..8b6b585ef11 100644 --- a/homeassistant/components/tomorrowio/translations/nl.json +++ b/homeassistant/components/tomorrowio/translations/nl.json @@ -10,9 +10,7 @@ "user": { "data": { "api_key": "API-sleutel", - "latitude": "Breedtegraad", "location": "Locatie", - "longitude": "Lengtegraad", "name": "Naam" }, "description": "Om een API sleutel te krijgen, meld je aan bij [Tomorrow.io](https://app.tomorrow.io/signup)." diff --git a/homeassistant/components/tomorrowio/translations/no.json b/homeassistant/components/tomorrowio/translations/no.json index bf366895a70..acab85a03a4 100644 --- a/homeassistant/components/tomorrowio/translations/no.json +++ b/homeassistant/components/tomorrowio/translations/no.json @@ -10,9 +10,7 @@ "user": { "data": { "api_key": "API-n\u00f8kkel", - "latitude": "Breddegrad", "location": "Plassering", - "longitude": "Lengdegrad", "name": "Navn" }, "description": "For \u00e5 f\u00e5 en API-n\u00f8kkel, registrer deg p\u00e5 [Tomorrow.io](https://app.tomorrow.io/signup)." diff --git a/homeassistant/components/tomorrowio/translations/pl.json b/homeassistant/components/tomorrowio/translations/pl.json index 4637a553aa4..b715d9e2cab 100644 --- a/homeassistant/components/tomorrowio/translations/pl.json +++ b/homeassistant/components/tomorrowio/translations/pl.json @@ -10,9 +10,7 @@ "user": { "data": { "api_key": "Klucz API", - "latitude": "Szeroko\u015b\u0107 geograficzna", "location": "Lokalizacja", - "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "name": "Nazwa" }, "description": "Aby uzyska\u0107 klucz API, zarejestruj si\u0119 na [Tomorrow.io](https://app.tomorrow.io/signup)." diff --git a/homeassistant/components/tomorrowio/translations/pt-BR.json b/homeassistant/components/tomorrowio/translations/pt-BR.json index 83f78e31b8e..d0b7a33abc9 100644 --- a/homeassistant/components/tomorrowio/translations/pt-BR.json +++ b/homeassistant/components/tomorrowio/translations/pt-BR.json @@ -10,9 +10,7 @@ "user": { "data": { "api_key": "Chave da API", - "latitude": "Latitude", "location": "Localiza\u00e7\u00e3o", - "longitude": "Longitude", "name": "Nome" }, "description": "Para obter uma chave de API, inscreva-se em [Tomorrow.io](https://app.tomorrow.io/signup)." diff --git a/homeassistant/components/tomorrowio/translations/ru.json b/homeassistant/components/tomorrowio/translations/ru.json index 08e24c5a47a..eeb7eb58488 100644 --- a/homeassistant/components/tomorrowio/translations/ru.json +++ b/homeassistant/components/tomorrowio/translations/ru.json @@ -10,9 +10,7 @@ "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", - "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", - "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, "description": "\u0427\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API, \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0439\u0442\u0435\u0441\u044c \u043d\u0430 [Tomorrow.io](https://app.tomorrow.io/signup)." diff --git a/homeassistant/components/tomorrowio/translations/tr.json b/homeassistant/components/tomorrowio/translations/tr.json index 4193428459c..61802d7f328 100644 --- a/homeassistant/components/tomorrowio/translations/tr.json +++ b/homeassistant/components/tomorrowio/translations/tr.json @@ -10,9 +10,7 @@ "user": { "data": { "api_key": "API Anahtar\u0131", - "latitude": "Enlem", "location": "Konum", - "longitude": "Boylam", "name": "Ad" }, "description": "API anahtar\u0131 almak i\u00e7in [Tomorrow.io](https://app.tomorrow.io/signup) adresinden kaydolun." diff --git a/homeassistant/components/tomorrowio/translations/zh-Hant.json b/homeassistant/components/tomorrowio/translations/zh-Hant.json index bf7043b0225..00f5d5e0fcc 100644 --- a/homeassistant/components/tomorrowio/translations/zh-Hant.json +++ b/homeassistant/components/tomorrowio/translations/zh-Hant.json @@ -10,9 +10,7 @@ "user": { "data": { "api_key": "API \u91d1\u9470", - "latitude": "\u7def\u5ea6", "location": "\u5ea7\u6a19", - "longitude": "\u7d93\u5ea6", "name": "\u540d\u7a31" }, "description": "\u8acb\u53c3\u95b1\u7db2\u5740\u4ee5\u4e86\u89e3\u5982\u4f55\u53d6\u5f97 API \u91d1\u9470\uff1a[Tomorrow.io](https://app.tomorrow.io/signup)\u3002" diff --git a/homeassistant/components/totalconnect/translations/ca.json b/homeassistant/components/totalconnect/translations/ca.json index 0edad920cdf..404e07d6b69 100644 --- a/homeassistant/components/totalconnect/translations/ca.json +++ b/homeassistant/components/totalconnect/translations/ca.json @@ -12,7 +12,6 @@ "step": { "locations": { "data": { - "location": "Ubicaci\u00f3", "usercode": "Codi d'usuari" }, "description": "Introdueix el codi de l'usuari en la ubicaci\u00f3 {location_id}", @@ -26,8 +25,7 @@ "data": { "password": "Contrasenya", "username": "Nom d'usuari" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/totalconnect/translations/cs.json b/homeassistant/components/totalconnect/translations/cs.json index ad3b8dd6618..ed81d8cafd1 100644 --- a/homeassistant/components/totalconnect/translations/cs.json +++ b/homeassistant/components/totalconnect/translations/cs.json @@ -8,11 +8,6 @@ "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, "step": { - "locations": { - "data": { - "location": "Um\u00edst\u011bn\u00ed" - } - }, "reauth_confirm": { "title": "Znovu ov\u011b\u0159it integraci" }, @@ -20,8 +15,7 @@ "data": { "password": "Heslo", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/totalconnect/translations/de.json b/homeassistant/components/totalconnect/translations/de.json index b89b99fc2d4..890bbb753d9 100644 --- a/homeassistant/components/totalconnect/translations/de.json +++ b/homeassistant/components/totalconnect/translations/de.json @@ -12,7 +12,6 @@ "step": { "locations": { "data": { - "location": "Standort", "usercode": "Benutzercode" }, "description": "Gib den Benutzercode f\u00fcr den Benutzer {location_id} an dieser Stelle ein", @@ -26,8 +25,7 @@ "data": { "password": "Passwort", "username": "Benutzername" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/totalconnect/translations/el.json b/homeassistant/components/totalconnect/translations/el.json index e2e19a56b0a..4f60b082484 100644 --- a/homeassistant/components/totalconnect/translations/el.json +++ b/homeassistant/components/totalconnect/translations/el.json @@ -12,7 +12,6 @@ "step": { "locations": { "data": { - "location": "\u03a4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1", "usercode": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c3\u03c4\u03b7\u03bd \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 {location_id}", @@ -26,8 +25,7 @@ "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/totalconnect/translations/en.json b/homeassistant/components/totalconnect/translations/en.json index f3a96550cba..8df2b00a936 100644 --- a/homeassistant/components/totalconnect/translations/en.json +++ b/homeassistant/components/totalconnect/translations/en.json @@ -12,7 +12,6 @@ "step": { "locations": { "data": { - "location": "Location", "usercode": "Usercode" }, "description": "Enter the usercode for this user at location {location_id}", @@ -26,8 +25,7 @@ "data": { "password": "Password", "username": "Username" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/totalconnect/translations/es-419.json b/homeassistant/components/totalconnect/translations/es-419.json index b8507f2b8a7..7560aaa35d2 100644 --- a/homeassistant/components/totalconnect/translations/es-419.json +++ b/homeassistant/components/totalconnect/translations/es-419.json @@ -8,8 +8,7 @@ "data": { "password": "Contrase\u00f1a", "username": "Nombre de usuario" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/totalconnect/translations/es.json b/homeassistant/components/totalconnect/translations/es.json index 97822a10300..7983aa3a11e 100644 --- a/homeassistant/components/totalconnect/translations/es.json +++ b/homeassistant/components/totalconnect/translations/es.json @@ -12,7 +12,6 @@ "step": { "locations": { "data": { - "location": "Localizaci\u00f3n", "usercode": "Codigo de usuario" }, "description": "Introduce el c\u00f3digo de usuario para este usuario en la ubicaci\u00f3n {location_id}", @@ -26,8 +25,7 @@ "data": { "password": "Contrase\u00f1a", "username": "Usuario" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/totalconnect/translations/et.json b/homeassistant/components/totalconnect/translations/et.json index e2df8dd51e8..c4bca75a558 100644 --- a/homeassistant/components/totalconnect/translations/et.json +++ b/homeassistant/components/totalconnect/translations/et.json @@ -12,7 +12,6 @@ "step": { "locations": { "data": { - "location": "Asukoht", "usercode": "Kasutajakood" }, "description": "Sisesta kasutaja kood asukohale {location_id}", @@ -26,8 +25,7 @@ "data": { "password": "Salas\u00f5na", "username": "Kasutajanimi" - }, - "title": "" + } } } } diff --git a/homeassistant/components/totalconnect/translations/fr.json b/homeassistant/components/totalconnect/translations/fr.json index f6dcefac2bb..fcda553a018 100644 --- a/homeassistant/components/totalconnect/translations/fr.json +++ b/homeassistant/components/totalconnect/translations/fr.json @@ -12,7 +12,6 @@ "step": { "locations": { "data": { - "location": "Emplacement", "usercode": "Code d'utilisateur" }, "description": "Entrez le code utilisateur pour cet utilisateur \u00e0 l'emplacement {location_id}", @@ -26,8 +25,7 @@ "data": { "password": "Mot de passe", "username": "Nom d'utilisateur" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/totalconnect/translations/he.json b/homeassistant/components/totalconnect/translations/he.json index 712c12a1062..ec280bc5f69 100644 --- a/homeassistant/components/totalconnect/translations/he.json +++ b/homeassistant/components/totalconnect/translations/he.json @@ -10,7 +10,6 @@ "step": { "locations": { "data": { - "location": "\u05de\u05d9\u05e7\u05d5\u05dd", "usercode": "\u05e7\u05d5\u05d3 \u05de\u05e9\u05ea\u05de\u05e9" } }, diff --git a/homeassistant/components/totalconnect/translations/hu.json b/homeassistant/components/totalconnect/translations/hu.json index 3d40f84d262..3bb2b4136c9 100644 --- a/homeassistant/components/totalconnect/translations/hu.json +++ b/homeassistant/components/totalconnect/translations/hu.json @@ -12,7 +12,6 @@ "step": { "locations": { "data": { - "location": "Elhelyezked\u00e9s", "usercode": "Felhaszn\u00e1l\u00f3i k\u00f3d" }, "description": "Adja meg ennek a felhaszn\u00e1l\u00f3i k\u00f3dj\u00e1t a k\u00f6vetkez\u0151 helyen: {location_id}", @@ -26,8 +25,7 @@ "data": { "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/totalconnect/translations/id.json b/homeassistant/components/totalconnect/translations/id.json index 0c2cbbfc6e2..1702ceb5688 100644 --- a/homeassistant/components/totalconnect/translations/id.json +++ b/homeassistant/components/totalconnect/translations/id.json @@ -12,7 +12,6 @@ "step": { "locations": { "data": { - "location": "Lokasi", "usercode": "Kode pengguna" }, "description": "Masukkan kode pengguna untuk pengguna ini di lokasi {location_id}", @@ -26,8 +25,7 @@ "data": { "password": "Kata Sandi", "username": "Nama Pengguna" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/totalconnect/translations/it.json b/homeassistant/components/totalconnect/translations/it.json index ee66d81d83d..32f0815d380 100644 --- a/homeassistant/components/totalconnect/translations/it.json +++ b/homeassistant/components/totalconnect/translations/it.json @@ -12,7 +12,6 @@ "step": { "locations": { "data": { - "location": "Posizione", "usercode": "Codice utente" }, "description": "Inserisci il codice utente per questo utente nella posizione {location_id}", @@ -26,8 +25,7 @@ "data": { "password": "Password", "username": "Nome utente" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/totalconnect/translations/ja.json b/homeassistant/components/totalconnect/translations/ja.json index 7eefbc3a873..c20b55d5583 100644 --- a/homeassistant/components/totalconnect/translations/ja.json +++ b/homeassistant/components/totalconnect/translations/ja.json @@ -12,7 +12,6 @@ "step": { "locations": { "data": { - "location": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3", "usercode": "\u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9" }, "description": "\u3053\u306e\u30e6\u30fc\u30b6\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30b3\u30fc\u30c9\u3092\u5834\u6240 {location_id} \u306b\u5165\u529b\u3057\u307e\u3059", @@ -26,8 +25,7 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - }, - "title": "\u30c8\u30fc\u30bf\u30eb\u30b3\u30cd\u30af\u30c8" + } } } } diff --git a/homeassistant/components/totalconnect/translations/ko.json b/homeassistant/components/totalconnect/translations/ko.json index 246b62d5500..e88b9aeb451 100644 --- a/homeassistant/components/totalconnect/translations/ko.json +++ b/homeassistant/components/totalconnect/translations/ko.json @@ -11,9 +11,6 @@ }, "step": { "locations": { - "data": { - "location": "\uc704\uce58" - }, "description": "\uc774 \uc704\uce58\uc758 \ud574\ub2f9 \uc0ac\uc6a9\uc790\uc5d0 \ub300\ud55c \uc0ac\uc6a9\uc790 \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", "title": "\uc704\uce58 \uc0ac\uc6a9\uc790 \ucf54\ub4dc" }, @@ -25,8 +22,7 @@ "data": { "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/totalconnect/translations/lb.json b/homeassistant/components/totalconnect/translations/lb.json index af76c38d918..025e5f657da 100644 --- a/homeassistant/components/totalconnect/translations/lb.json +++ b/homeassistant/components/totalconnect/translations/lb.json @@ -11,8 +11,7 @@ "data": { "password": "Passwuert", "username": "Benotzernumm" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/totalconnect/translations/nl.json b/homeassistant/components/totalconnect/translations/nl.json index 674818d8428..186e2f60d50 100644 --- a/homeassistant/components/totalconnect/translations/nl.json +++ b/homeassistant/components/totalconnect/translations/nl.json @@ -12,7 +12,6 @@ "step": { "locations": { "data": { - "location": "Locatie", "usercode": "Gebruikerscode" }, "description": "Voer de gebruikerscode voor deze gebruiker in op locatie {location_id}", @@ -26,8 +25,7 @@ "data": { "password": "Wachtwoord", "username": "Gebruikersnaam" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/totalconnect/translations/no.json b/homeassistant/components/totalconnect/translations/no.json index c1624f08259..86cfedf51d4 100644 --- a/homeassistant/components/totalconnect/translations/no.json +++ b/homeassistant/components/totalconnect/translations/no.json @@ -12,7 +12,6 @@ "step": { "locations": { "data": { - "location": "Plassering", "usercode": "Brukerkode" }, "description": "Angi brukerkoden for denne brukeren p\u00e5 plasseringen {location_id}", @@ -26,8 +25,7 @@ "data": { "password": "Passord", "username": "Brukernavn" - }, - "title": "" + } } } } diff --git a/homeassistant/components/totalconnect/translations/pl.json b/homeassistant/components/totalconnect/translations/pl.json index 5174d717c26..d3927212c82 100644 --- a/homeassistant/components/totalconnect/translations/pl.json +++ b/homeassistant/components/totalconnect/translations/pl.json @@ -12,7 +12,6 @@ "step": { "locations": { "data": { - "location": "Lokalizacja", "usercode": "Kod u\u017cytkownika" }, "description": "Wprowad\u017a kod u\u017cytkownika dla u\u017cytkownika w lokalizacji {location_id}", @@ -26,8 +25,7 @@ "data": { "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/totalconnect/translations/pt-BR.json b/homeassistant/components/totalconnect/translations/pt-BR.json index 47dfe5437e0..1ffeb1337ec 100644 --- a/homeassistant/components/totalconnect/translations/pt-BR.json +++ b/homeassistant/components/totalconnect/translations/pt-BR.json @@ -12,7 +12,6 @@ "step": { "locations": { "data": { - "location": "Localiza\u00e7\u00e3o", "usercode": "C\u00f3digo de usu\u00e1rio" }, "description": "Insira o c\u00f3digo de usu\u00e1rio para este usu\u00e1rio no local {location_id}", @@ -26,8 +25,7 @@ "data": { "password": "Senha", "username": "Usu\u00e1rio" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/totalconnect/translations/pt.json b/homeassistant/components/totalconnect/translations/pt.json index 11ac8262267..0957880ae6c 100644 --- a/homeassistant/components/totalconnect/translations/pt.json +++ b/homeassistant/components/totalconnect/translations/pt.json @@ -8,11 +8,6 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { - "locations": { - "data": { - "location": "Localiza\u00e7\u00e3o" - } - }, "reauth_confirm": { "title": "Reautenticar integra\u00e7\u00e3o" }, diff --git a/homeassistant/components/totalconnect/translations/ru.json b/homeassistant/components/totalconnect/translations/ru.json index a46c37032a1..ee82564033c 100644 --- a/homeassistant/components/totalconnect/translations/ru.json +++ b/homeassistant/components/totalconnect/translations/ru.json @@ -12,7 +12,6 @@ "step": { "locations": { "data": { - "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", "usercode": "\u041a\u043e\u0434 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0432 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438 {location_id}.", @@ -26,8 +25,7 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/totalconnect/translations/sk.json b/homeassistant/components/totalconnect/translations/sk.json index 59d045e7603..71a7aea5018 100644 --- a/homeassistant/components/totalconnect/translations/sk.json +++ b/homeassistant/components/totalconnect/translations/sk.json @@ -5,13 +5,6 @@ }, "error": { "invalid_auth": "Neplatn\u00e9 overenie" - }, - "step": { - "locations": { - "data": { - "location": "Umiestnenie" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/sl.json b/homeassistant/components/totalconnect/translations/sl.json index 7214ff6aeb5..bbddcdf2459 100644 --- a/homeassistant/components/totalconnect/translations/sl.json +++ b/homeassistant/components/totalconnect/translations/sl.json @@ -8,8 +8,7 @@ "data": { "password": "Geslo", "username": "Uporabni\u0161ko ime" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/totalconnect/translations/tr.json b/homeassistant/components/totalconnect/translations/tr.json index 925353f05a4..ef50457f846 100644 --- a/homeassistant/components/totalconnect/translations/tr.json +++ b/homeassistant/components/totalconnect/translations/tr.json @@ -12,7 +12,6 @@ "step": { "locations": { "data": { - "location": "Konum", "usercode": "Kullan\u0131c\u0131 kodu" }, "description": "Bu kullan\u0131c\u0131n\u0131n kullan\u0131c\u0131 kodunu {location_id} konumuna girin", @@ -26,8 +25,7 @@ "data": { "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" - }, - "title": "Toplam Ba\u011flant\u0131" + } } } } diff --git a/homeassistant/components/totalconnect/translations/uk.json b/homeassistant/components/totalconnect/translations/uk.json index f34a279d598..afe3dfab539 100644 --- a/homeassistant/components/totalconnect/translations/uk.json +++ b/homeassistant/components/totalconnect/translations/uk.json @@ -11,8 +11,7 @@ "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/totalconnect/translations/zh-Hant.json b/homeassistant/components/totalconnect/translations/zh-Hant.json index beaeaa5d9bf..3b960a8bc43 100644 --- a/homeassistant/components/totalconnect/translations/zh-Hant.json +++ b/homeassistant/components/totalconnect/translations/zh-Hant.json @@ -12,7 +12,6 @@ "step": { "locations": { "data": { - "location": "\u5ea7\u6a19", "usercode": "\u4f7f\u7528\u8005\u4ee3\u78bc" }, "description": "\u8f38\u5165\u4f7f\u7528\u8005\u65bc\u6b64\u5ea7\u6a19 {location_id} \u4e4b\u4f7f\u7528\u8005\u4ee3\u78bc", @@ -26,8 +25,7 @@ "data": { "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" - }, - "title": "Total Connect" + } } } } diff --git a/homeassistant/components/tplink/translations/bg.json b/homeassistant/components/tplink/translations/bg.json index 33ae523d8cf..b31dd804044 100644 --- a/homeassistant/components/tplink/translations/bg.json +++ b/homeassistant/components/tplink/translations/bg.json @@ -2,17 +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", - "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." + "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." }, "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})?" }, diff --git a/homeassistant/components/tplink/translations/ca.json b/homeassistant/components/tplink/translations/ca.json index 4dfb749a9d7..490dbf5f56b 100644 --- a/homeassistant/components/tplink/translations/ca.json +++ b/homeassistant/components/tplink/translations/ca.json @@ -2,17 +2,13 @@ "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." + "no_devices_found": "No s'han trobat dispositius a la xarxa" }, "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})?" }, diff --git a/homeassistant/components/tplink/translations/cs.json b/homeassistant/components/tplink/translations/cs.json index 5ee79c8e6f6..bb063ffbcc7 100644 --- a/homeassistant/components/tplink/translations/cs.json +++ b/homeassistant/components/tplink/translations/cs.json @@ -2,16 +2,12 @@ "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." + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" }, "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" diff --git a/homeassistant/components/tplink/translations/da.json b/homeassistant/components/tplink/translations/da.json index e6fc3c895a3..e9cd7b269e2 100644 --- a/homeassistant/components/tplink/translations/da.json +++ b/homeassistant/components/tplink/translations/da.json @@ -1,13 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Ingen TP-Link enheder kunne findes p\u00e5 netv\u00e6rket.", - "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." - }, - "step": { - "confirm": { - "description": "Vil du konfigurere TP-Link-smartenheder?" - } + "no_devices_found": "Ingen TP-Link enheder kunne findes p\u00e5 netv\u00e6rket." } } } \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/de.json b/homeassistant/components/tplink/translations/de.json index 4d6a07b881e..1fd05c1325a 100644 --- a/homeassistant/components/tplink/translations/de.json +++ b/homeassistant/components/tplink/translations/de.json @@ -2,17 +2,13 @@ "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." + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" }, "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?" }, diff --git a/homeassistant/components/tplink/translations/el.json b/homeassistant/components/tplink/translations/el.json index bea659e039a..d7c4825b514 100644 --- a/homeassistant/components/tplink/translations/el.json +++ b/homeassistant/components/tplink/translations/el.json @@ -2,17 +2,13 @@ "config": { "abort": { "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", - "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", - "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "flow_title": "{name} {model} ({host})", "step": { - "confirm": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03ad\u03be\u03c5\u03c0\u03bd\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 TP-Link;" - }, "discovery_confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} {model} ({host});" }, diff --git a/homeassistant/components/tplink/translations/en.json b/homeassistant/components/tplink/translations/en.json index da4681145d8..0697974e708 100644 --- a/homeassistant/components/tplink/translations/en.json +++ b/homeassistant/components/tplink/translations/en.json @@ -2,17 +2,13 @@ "config": { "abort": { "already_configured": "Device is already configured", - "no_devices_found": "No devices found on the network", - "single_instance_allowed": "Already configured. Only a single configuration possible." + "no_devices_found": "No devices found on the network" }, "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/es-419.json b/homeassistant/components/tplink/translations/es-419.json index 4113e802e1a..302d49fa29e 100644 --- a/homeassistant/components/tplink/translations/es-419.json +++ b/homeassistant/components/tplink/translations/es-419.json @@ -1,13 +1,7 @@ { "config": { "abort": { - "no_devices_found": "No se encontraron dispositivos TP-Link en la red.", - "single_instance_allowed": "Solo es necesaria una \u00fanica configuraci\u00f3n." - }, - "step": { - "confirm": { - "description": "\u00bfDesea configurar dispositivos inteligentes TP-Link?" - } + "no_devices_found": "No se encontraron dispositivos TP-Link en la red." } } } \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/es.json b/homeassistant/components/tplink/translations/es.json index 81bb3f47433..e083acf61ed 100644 --- a/homeassistant/components/tplink/translations/es.json +++ b/homeassistant/components/tplink/translations/es.json @@ -2,17 +2,13 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "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_devices_found": "No se encontraron dispositivos en la red" }, "error": { "cannot_connect": "No se pudo conectar" }, "flow_title": "{name} {model} ({host})", "step": { - "confirm": { - "description": "\u00bfQuieres configurar dispositivos inteligentes de TP-Link?" - }, "discovery_confirm": { "description": "\u00bfQuieres configurar {name} {model} ({host})?" }, diff --git a/homeassistant/components/tplink/translations/et.json b/homeassistant/components/tplink/translations/et.json index 12c4f3d6f84..01dabe57ea7 100644 --- a/homeassistant/components/tplink/translations/et.json +++ b/homeassistant/components/tplink/translations/et.json @@ -2,17 +2,13 @@ "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." + "no_devices_found": "V\u00f5rgust ei leitud seadmeid" }, "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})?" }, diff --git a/homeassistant/components/tplink/translations/fr.json b/homeassistant/components/tplink/translations/fr.json index d3a5fad5939..e1105ea00e0 100644 --- a/homeassistant/components/tplink/translations/fr.json +++ b/homeassistant/components/tplink/translations/fr.json @@ -2,17 +2,13 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", - "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" }, "error": { "cannot_connect": "\u00c9chec de connexion" }, "flow_title": "{name} {model} ( {host} )", "step": { - "confirm": { - "description": "Voulez-vous configurer TP-Link smart devices?" - }, "discovery_confirm": { "description": "Voulez-vous configurer {name} {model} ( {host} )\u00a0?" }, diff --git a/homeassistant/components/tplink/translations/he.json b/homeassistant/components/tplink/translations/he.json index 6ac16c36476..fc44b0d7ae7 100644 --- a/homeassistant/components/tplink/translations/he.json +++ b/homeassistant/components/tplink/translations/he.json @@ -2,17 +2,13 @@ "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." + "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": "{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 ?" - }, "pick_device": { "data": { "device": "\u05d4\u05ea\u05e7\u05df" diff --git a/homeassistant/components/tplink/translations/hu.json b/homeassistant/components/tplink/translations/hu.json index c00744d0dcf..ba9ba65a1c6 100644 --- a/homeassistant/components/tplink/translations/hu.json +++ b/homeassistant/components/tplink/translations/hu.json @@ -2,17 +2,13 @@ "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." + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" }, "error": { "cannot_connect": "A csatlakoz\u00e1s sikertelen" }, "flow_title": "{name} {model} ({host})", "step": { - "confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a TP-Link intelligens eszk\u00f6zeit?" - }, "discovery_confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} {model} ({host})?" }, diff --git a/homeassistant/components/tplink/translations/id.json b/homeassistant/components/tplink/translations/id.json index 2a435ac1ac1..b7c0be074e7 100644 --- a/homeassistant/components/tplink/translations/id.json +++ b/homeassistant/components/tplink/translations/id.json @@ -2,17 +2,13 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", - "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", - "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" }, "error": { "cannot_connect": "Gagal terhubung" }, "flow_title": "{name} {model} ({host})", "step": { - "confirm": { - "description": "Ingin menyiapkan perangkat cerdas TP-Link?" - }, "discovery_confirm": { "description": "Ingin menyiapkan {name} {model} ({host})?" }, diff --git a/homeassistant/components/tplink/translations/it.json b/homeassistant/components/tplink/translations/it.json index 6ab46615373..4a35356d604 100644 --- a/homeassistant/components/tplink/translations/it.json +++ b/homeassistant/components/tplink/translations/it.json @@ -2,17 +2,13 @@ "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." + "no_devices_found": "Nessun dispositivo trovato sulla rete" }, "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})?" }, diff --git a/homeassistant/components/tplink/translations/ja.json b/homeassistant/components/tplink/translations/ja.json index 45e9854b431..1df87f71518 100644 --- a/homeassistant/components/tplink/translations/ja.json +++ b/homeassistant/components/tplink/translations/ja.json @@ -2,17 +2,13 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "flow_title": "{name} {model} ({host})", "step": { - "confirm": { - "description": "TP-Link\u30b9\u30de\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" - }, "discovery_confirm": { "description": "{name} {model} ({host}) \u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u884c\u3044\u307e\u3059\u304b\uff1f" }, diff --git a/homeassistant/components/tplink/translations/ko.json b/homeassistant/components/tplink/translations/ko.json index 01ae48031e4..f063f54207c 100644 --- a/homeassistant/components/tplink/translations/ko.json +++ b/homeassistant/components/tplink/translations/ko.json @@ -1,13 +1,9 @@ { "config": { "abort": { - "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" }, "step": { - "confirm": { - "description": "TP-Link \uc2a4\ub9c8\ud2b8 \uae30\uae30\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" - }, "discovery_confirm": { "description": "{name} {model} ( {host} )\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" } diff --git a/homeassistant/components/tplink/translations/lb.json b/homeassistant/components/tplink/translations/lb.json index 1560b68d3e4..47a897ef157 100644 --- a/homeassistant/components/tplink/translations/lb.json +++ b/homeassistant/components/tplink/translations/lb.json @@ -1,13 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Keng Apparater am Netzwierk fonnt.", - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." - }, - "step": { - "confirm": { - "description": "Soll TP-Link Smart Home konfigur\u00e9iert ginn?" - } + "no_devices_found": "Keng Apparater am Netzwierk fonnt." } } } \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/nl.json b/homeassistant/components/tplink/translations/nl.json index f6cf6a21e72..bceae9f3fff 100644 --- a/homeassistant/components/tplink/translations/nl.json +++ b/homeassistant/components/tplink/translations/nl.json @@ -2,17 +2,13 @@ "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." + "no_devices_found": "Geen apparaten gevonden op het netwerk" }, "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?" }, diff --git a/homeassistant/components/tplink/translations/no.json b/homeassistant/components/tplink/translations/no.json index 6c7bd7dcbf4..8ae66b3dce1 100644 --- a/homeassistant/components/tplink/translations/no.json +++ b/homeassistant/components/tplink/translations/no.json @@ -2,17 +2,13 @@ "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." + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" }, "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} )?" }, diff --git a/homeassistant/components/tplink/translations/pl.json b/homeassistant/components/tplink/translations/pl.json index 35e1e7f5354..3e6283a6d5e 100644 --- a/homeassistant/components/tplink/translations/pl.json +++ b/homeassistant/components/tplink/translations/pl.json @@ -2,17 +2,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." + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "flow_title": "{name} {model} ({host})", "step": { - "confirm": { - "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" - }, "discovery_confirm": { "description": "Czy chcesz skonfigurowa\u0107 {name} {model} ({host})?" }, diff --git a/homeassistant/components/tplink/translations/pt-BR.json b/homeassistant/components/tplink/translations/pt-BR.json index a034b44b761..7ce4d557844 100644 --- a/homeassistant/components/tplink/translations/pt-BR.json +++ b/homeassistant/components/tplink/translations/pt-BR.json @@ -2,17 +2,13 @@ "config": { "abort": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", - "no_devices_found": "Nenhum dispositivo encontrado na rede", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." + "no_devices_found": "Nenhum dispositivo encontrado na rede" }, "error": { "cannot_connect": "Falha ao conectar" }, "flow_title": "{name} {model} ({host})", "step": { - "confirm": { - "description": "Deseja configurar dispositivos inteligentes TP-Link?" - }, "discovery_confirm": { "description": "Deseja configurar {name} {model} ({host})?" }, diff --git a/homeassistant/components/tplink/translations/pt.json b/homeassistant/components/tplink/translations/pt.json index 9df803c325e..90afdfcf10a 100644 --- a/homeassistant/components/tplink/translations/pt.json +++ b/homeassistant/components/tplink/translations/pt.json @@ -1,13 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Nenhum dispositivo TP-Link encontrado na rede.", - "single_instance_allowed": "S\u00f3 \u00e9 necess\u00e1ria uma \u00fanica configura\u00e7\u00e3o." - }, - "step": { - "confirm": { - "description": "Deseja configurar os dispositivos inteligentes TP-Link?" - } + "no_devices_found": "Nenhum dispositivo TP-Link encontrado na rede." } } } \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/ru.json b/homeassistant/components/tplink/translations/ru.json index 47f1459e572..b622a77c77d 100644 --- a/homeassistant/components/tplink/translations/ru.json +++ b/homeassistant/components/tplink/translations/ru.json @@ -2,17 +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.", - "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." + "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": "{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})?" }, diff --git a/homeassistant/components/tplink/translations/sl.json b/homeassistant/components/tplink/translations/sl.json index 6c49eaf8d0a..039bf9966a7 100644 --- a/homeassistant/components/tplink/translations/sl.json +++ b/homeassistant/components/tplink/translations/sl.json @@ -1,13 +1,7 @@ { "config": { "abort": { - "no_devices_found": "TP-Link naprav ni mogo\u010de najti v omre\u017eju.", - "single_instance_allowed": "Potrebna je samo ena konfiguracija." - }, - "step": { - "confirm": { - "description": "\u017delite namestiti pametne naprave TP-Link?" - } + "no_devices_found": "TP-Link naprav ni mogo\u010de najti v omre\u017eju." } } } \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/sv.json b/homeassistant/components/tplink/translations/sv.json index 70ab1740f0d..c11ec674c36 100644 --- a/homeassistant/components/tplink/translations/sv.json +++ b/homeassistant/components/tplink/translations/sv.json @@ -1,13 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Inga TP-Link enheter hittades p\u00e5 n\u00e4tverket.", - "single_instance_allowed": "Endast en enda konfiguration \u00e4r n\u00f6dv\u00e4ndig." - }, - "step": { - "confirm": { - "description": "Vill du konfigurera TP-Link smart enheter?" - } + "no_devices_found": "Inga TP-Link enheter hittades p\u00e5 n\u00e4tverket." } } } \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/tr.json b/homeassistant/components/tplink/translations/tr.json index 5e4dca23c37..616997a0976 100644 --- a/homeassistant/components/tplink/translations/tr.json +++ b/homeassistant/components/tplink/translations/tr.json @@ -2,17 +2,13 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "no_devices_found": "A\u011fda cihaz bulunamad\u0131", - "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, "flow_title": "{name} {model} ({host})", "step": { - "confirm": { - "description": "TP-Link ak\u0131ll\u0131 cihazlar\u0131 kurmak istiyor musunuz?" - }, "discovery_confirm": { "description": "{name} {model} ( {host} ) kurulumu yapmak istiyor musunuz?" }, diff --git a/homeassistant/components/tplink/translations/uk.json b/homeassistant/components/tplink/translations/uk.json index abbfc076f7e..1efd10692f9 100644 --- a/homeassistant/components/tplink/translations/uk.json +++ b/homeassistant/components/tplink/translations/uk.json @@ -1,13 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", - "single_instance_allowed": "\u0412\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e. \u041c\u043e\u0436\u043b\u0438\u0432\u0430 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f." - }, - "step": { - "confirm": { - "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 TP-Link Smart Home?" - } + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456." } } } \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/zh-Hans.json b/homeassistant/components/tplink/translations/zh-Hans.json index 710cc0fe166..9b46cb915c5 100644 --- a/homeassistant/components/tplink/translations/zh-Hans.json +++ b/homeassistant/components/tplink/translations/zh-Hans.json @@ -1,13 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 TP-Link \u8bbe\u5907\u3002", - "single_instance_allowed": "\u53ea\u80fd\u914d\u7f6e\u4e00\u6b21\u3002" - }, - "step": { - "confirm": { - "description": "\u60a8\u60f3\u8981\u914d\u7f6e TP-Link \u667a\u80fd\u8bbe\u5907\u5417\uff1f" - } + "no_devices_found": "\u6ca1\u6709\u5728\u7f51\u7edc\u4e0a\u627e\u5230 TP-Link \u8bbe\u5907\u3002" } } } \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/zh-Hant.json b/homeassistant/components/tplink/translations/zh-Hant.json index 31bfeea42a2..47e976a7e30 100644 --- a/homeassistant/components/tplink/translations/zh-Hant.json +++ b/homeassistant/components/tplink/translations/zh-Hant.json @@ -2,17 +2,13 @@ "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": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" }, "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" }, diff --git a/homeassistant/components/trafikverket_ferry/translations/ko.json b/homeassistant/components/trafikverket_ferry/translations/ko.json new file mode 100644 index 00000000000..c50ce7212d1 --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/ko.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "incorrect_api_key": "\uc120\ud0dd\ud55c \uacc4\uc815\uc5d0 \ub300\ud55c \uc798\ubabb\ub41c API \ud0a4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_route": "\uc81c\uacf5\ub41c \uc815\ubcf4\ub85c \uacbd\ub85c\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \ud0a4" + } + }, + "user": { + "data": { + "api_key": "API \ud0a4", + "time": "Time", + "weekday": "\ud3c9\uc77c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/bg.json b/homeassistant/components/trafikverket_weatherstation/translations/bg.json index 2e96cb420e6..07c54468c03 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/bg.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/bg.json @@ -11,7 +11,6 @@ "user": { "data": { "api_key": "API \u043a\u043b\u044e\u0447", - "name": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435", "station": "\u0421\u0442\u0430\u043d\u0446\u0438\u044f" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/ca.json b/homeassistant/components/trafikverket_weatherstation/translations/ca.json index 80fe04ba83f..2ca00e2b4df 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/ca.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/ca.json @@ -13,8 +13,6 @@ "user": { "data": { "api_key": "Clau API", - "conditions": "Condicions monitoritzades", - "name": "Nom d'usuari", "station": "Estaci\u00f3" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/de.json b/homeassistant/components/trafikverket_weatherstation/translations/de.json index 47d3dfc2e9b..c57b913e42a 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/de.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/de.json @@ -13,8 +13,6 @@ "user": { "data": { "api_key": "API-Schl\u00fcssel", - "conditions": "\u00dcberwachte Bedingungen", - "name": "Benutzername", "station": "Station" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/el.json b/homeassistant/components/trafikverket_weatherstation/translations/el.json index e790e3d3d97..83f046e2c41 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/el.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/el.json @@ -13,8 +13,6 @@ "user": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", - "conditions": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03bf\u03cd\u03bc\u03b5\u03bd\u03b5\u03c2 \u03c3\u03c5\u03bd\u03b8\u03ae\u03ba\u03b5\u03c2", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", "station": "\u03a3\u03c4\u03b1\u03b8\u03bc\u03cc\u03c2/\u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/en.json b/homeassistant/components/trafikverket_weatherstation/translations/en.json index 0c0c15f5bb9..c2b5ba0b598 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/en.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/en.json @@ -13,8 +13,6 @@ "user": { "data": { "api_key": "API Key", - "conditions": "Monitored conditions", - "name": "Username", "station": "Station" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/es.json b/homeassistant/components/trafikverket_weatherstation/translations/es.json index 009c57bb275..9513ae65221 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/es.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/es.json @@ -13,8 +13,6 @@ "user": { "data": { "api_key": "Clave API", - "conditions": "Condiciones monitoreadas", - "name": "Nombre de usuario", "station": "Estaci\u00f3n" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/et.json b/homeassistant/components/trafikverket_weatherstation/translations/et.json index 7350031bd36..8485525e233 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/et.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/et.json @@ -13,8 +13,6 @@ "user": { "data": { "api_key": "API v\u00f5ti", - "conditions": "J\u00e4lgitavad elemendid", - "name": "Kasutajanimi", "station": "Seirejaam" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/fr.json b/homeassistant/components/trafikverket_weatherstation/translations/fr.json index 55a88c6a746..6a92e2249d9 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/fr.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/fr.json @@ -13,8 +13,6 @@ "user": { "data": { "api_key": "Cl\u00e9 d'API", - "conditions": "Conditions surveill\u00e9es", - "name": "Nom d'utilisateur", "station": "Station" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/he.json b/homeassistant/components/trafikverket_weatherstation/translations/he.json index e6eceda994c..6801953a80c 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/he.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/he.json @@ -11,7 +11,6 @@ "user": { "data": { "api_key": "\u05de\u05e4\u05ea\u05d7 API", - "name": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", "station": "\u05ea\u05d7\u05e0\u05d4" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/hu.json b/homeassistant/components/trafikverket_weatherstation/translations/hu.json index c4f830b20fa..57c2bf2e526 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/hu.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/hu.json @@ -13,8 +13,6 @@ "user": { "data": { "api_key": "API kulcs", - "conditions": "Megfigyelt k\u00f6r\u00fclm\u00e9nyek", - "name": "Felhaszn\u00e1l\u00f3n\u00e9v", "station": "\u00c1llom\u00e1s" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/id.json b/homeassistant/components/trafikverket_weatherstation/translations/id.json index 0db961235d4..e3d04cc2a45 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/id.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/id.json @@ -13,8 +13,6 @@ "user": { "data": { "api_key": "Kunci API", - "conditions": "Kondisi yang dipantau", - "name": "Nama Pengguna", "station": "Stasiun" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/it.json b/homeassistant/components/trafikverket_weatherstation/translations/it.json index a073a528586..927b8bdd19a 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/it.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/it.json @@ -13,8 +13,6 @@ "user": { "data": { "api_key": "Chiave API", - "conditions": "Condizioni monitorate", - "name": "Nome utente", "station": "Stazione" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/ja.json b/homeassistant/components/trafikverket_weatherstation/translations/ja.json index 5a022f011e2..5710ea7b693 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/ja.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/ja.json @@ -13,8 +13,6 @@ "user": { "data": { "api_key": "API\u30ad\u30fc", - "conditions": "\u30e2\u30cb\u30bf\u30fc\u306e\u72b6\u614b", - "name": "\u30e6\u30fc\u30b6\u30fc\u540d", "station": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/lt.json b/homeassistant/components/trafikverket_weatherstation/translations/lt.json deleted file mode 100644 index 87076aabdf1..00000000000 --- a/homeassistant/components/trafikverket_weatherstation/translations/lt.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "name": "Prisijungimo vardas" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/nb.json b/homeassistant/components/trafikverket_weatherstation/translations/nb.json index 19fbf894f8d..0d3a32ccb1c 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/nb.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/nb.json @@ -10,8 +10,7 @@ "step": { "user": { "data": { - "api_key": "API-n\u00f8kkel", - "name": "Brukernavn" + "api_key": "API-n\u00f8kkel" } } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/nl.json b/homeassistant/components/trafikverket_weatherstation/translations/nl.json index 1e8d8ae9f3a..ce958cb3d86 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/nl.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/nl.json @@ -13,8 +13,6 @@ "user": { "data": { "api_key": "API-sleutel", - "conditions": "Gemonitorde condities", - "name": "Gebruikersnaam", "station": "Station" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/no.json b/homeassistant/components/trafikverket_weatherstation/translations/no.json index fea917f9c38..7b39c2f9b58 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/no.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/no.json @@ -13,8 +13,6 @@ "user": { "data": { "api_key": "API-n\u00f8kkel", - "conditions": "Overv\u00e5kede forhold", - "name": "Brukernavn", "station": "Stasjon" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/pl.json b/homeassistant/components/trafikverket_weatherstation/translations/pl.json index 2dfd13bf268..2500bcc2b51 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/pl.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/pl.json @@ -13,8 +13,6 @@ "user": { "data": { "api_key": "Klucz API", - "conditions": "Monitorowane warunki pogodowe", - "name": "Nazwa u\u017cytkownika", "station": "Stacja" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/pt-BR.json b/homeassistant/components/trafikverket_weatherstation/translations/pt-BR.json index f73ab8555da..70d870a6dba 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/pt-BR.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/pt-BR.json @@ -13,8 +13,6 @@ "user": { "data": { "api_key": "Chave da API", - "conditions": "Condi\u00e7\u00f5es monitoradas", - "name": "Usu\u00e1rio", "station": "Esta\u00e7\u00e3o" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/ru.json b/homeassistant/components/trafikverket_weatherstation/translations/ru.json index d7658cda81b..7a903d8f624 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/ru.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/ru.json @@ -13,8 +13,6 @@ "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", - "conditions": "\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u0443\u0435\u043c\u044b\u0435 \u0443\u0441\u043b\u043e\u0432\u0438\u044f", - "name": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", "station": "\u0421\u0442\u0430\u043d\u0446\u0438\u044f" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/tr.json b/homeassistant/components/trafikverket_weatherstation/translations/tr.json index e999f7b50d1..89c8288a9ee 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/tr.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/tr.json @@ -13,8 +13,6 @@ "user": { "data": { "api_key": "API Anahtar\u0131", - "conditions": "\u0130zlenen ko\u015fullar", - "name": "Kullan\u0131c\u0131 Ad\u0131", "station": "\u0130stasyon" } } diff --git a/homeassistant/components/trafikverket_weatherstation/translations/zh-Hant.json b/homeassistant/components/trafikverket_weatherstation/translations/zh-Hant.json index 47394b08af9..f0d928c3aba 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/zh-Hant.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/zh-Hant.json @@ -13,8 +13,6 @@ "user": { "data": { "api_key": "API \u91d1\u9470", - "conditions": "\u5df2\u76e3\u63a7\u72c0\u614b", - "name": "\u4f7f\u7528\u8005\u540d\u7a31", "station": "\u76e3\u63a7\u7ad9" } } diff --git a/homeassistant/components/tuya/translations/af.json b/homeassistant/components/tuya/translations/af.json deleted file mode 100644 index 71ac741b6b8..00000000000 --- a/homeassistant/components/tuya/translations/af.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "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/bg.json b/homeassistant/components/tuya/translations/bg.json index 7a393100351..5bd4cc0b745 100644 --- a/homeassistant/components/tuya/translations/bg.json +++ b/homeassistant/components/tuya/translations/bg.json @@ -1,51 +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", - "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": { - "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": { "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" - } - } - }, - "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" - }, - "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" - } + "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" } } } diff --git a/homeassistant/components/tuya/translations/ca.json b/homeassistant/components/tuya/translations/ca.json index 5a9dd4ac44b..52ef20e69b9 100644 --- a/homeassistant/components/tuya/translations/ca.json +++ b/homeassistant/components/tuya/translations/ca.json @@ -1,82 +1,19 @@ { "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", "login_error": "Error d'inici de sessi\u00f3 ({code}): {msg}" }, - "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": { "access_id": "ID d'acc\u00e9s de Tuya IoT", "access_secret": "Secret d'acc\u00e9s de Tuya IoT", "country_code": "Pa\u00eds", "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": "Compte" }, - "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" + "description": "Introdueix les teves credencial de Tuya" } } } diff --git a/homeassistant/components/tuya/translations/cs.json b/homeassistant/components/tuya/translations/cs.json index 9a406ffcb4b..0364baac560 100644 --- a/homeassistant/components/tuya/translations/cs.json +++ b/homeassistant/components/tuya/translations/cs.json @@ -1,65 +1,16 @@ { "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": { - "login": { - "data": { - "country_code": "K\u00f3d zem\u011b" - } - }, "user": { "data": { "country_code": "Zem\u011b", "password": "Heslo", - "platform": "Aplikace, ve kter\u00e9 m\u00e1te zaregistrovan\u00fd \u00fa\u010det", - "region": "Region", "username": "\u00da\u010det" }, - "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" + "description": "Zadejte sv\u00e9 p\u0159ihla\u0161ovac\u00ed \u00fadaje k Tuya." } } } diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json index 61bdf308246..492159d4d6c 100644 --- a/homeassistant/components/tuya/translations/de.json +++ b/homeassistant/components/tuya/translations/de.json @@ -1,82 +1,19 @@ { "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", "login_error": "Anmeldefehler ({code}): {msg}" }, - "flow_title": "Tuya Konfiguration", "step": { - "login": { - "data": { - "access_id": "Zugangs-ID", - "access_secret": "Zugangsgeheimnis", - "country_code": "L\u00e4ndercode", - "endpoint": "Verf\u00fcgbarkeitsbereich", - "password": "Passwort", - "tuya_app_type": "Mobile App", - "username": "Konto" - }, - "description": "Gib deine Tuya-Anmeldedaten ein", - "title": "Tuya" - }, "user": { "data": { "access_id": "Tuya IoT-Zugriffs-ID", "access_secret": "Tuya IoT-Zugriffsgeheimnis", "country_code": "Land", "password": "Passwort", - "platform": "Die App, in der dein Konto registriert ist", - "region": "Region", - "tuya_project_type": "Tuya Cloud Projekttyp", "username": "Konto" }, - "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" + "description": "Gib deine Tuya-Anmeldeinformationen ein." } } } diff --git a/homeassistant/components/tuya/translations/el.json b/homeassistant/components/tuya/translations/el.json index 63ba3b2696a..7bbf8790316 100644 --- a/homeassistant/components/tuya/translations/el.json +++ b/homeassistant/components/tuya/translations/el.json @@ -1,82 +1,19 @@ { "config": { - "abort": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", - "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." - }, "error": { "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "login_error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 ({code}): {msg}" }, - "flow_title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Tuya", "step": { - "login": { - "data": { - "access_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "access_secret": "\u039c\u03c5\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "country_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c7\u03ce\u03c1\u03b1\u03c2", - "endpoint": "\u0396\u03ce\u03bd\u03b7 \u03b4\u03b9\u03b1\u03b8\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "tuya_app_type": "Mobile App", - "username": "\u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2" - }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03ac \u03c3\u03b1\u03c2 Tuya", - "title": "Tuya" - }, "user": { "data": { "access_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 Tuya IoT", "access_secret": "\u039c\u03c5\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 Tuya IoT", "country_code": "\u03a7\u03ce\u03c1\u03b1", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "platform": "\u0397 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03c3\u03c4\u03b7\u03bd \u03bf\u03c0\u03bf\u03af\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03b3\u03b3\u03b5\u03b3\u03c1\u03b1\u03bc\u03bc\u03ad\u03bd\u03bf\u03c2 \u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03c3\u03b1\u03c2", - "region": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae", - "tuya_project_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03ad\u03c1\u03b3\u03bf\u03c5 Tuya cloud", "username": "\u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2" }, - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03ac \u03c3\u03b1\u03c2 Tuya", - "title": "\u0395\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" - }, - "error": { - "dev_multi_type": "\u03a0\u03bf\u03bb\u03bb\u03ad\u03c2 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ad\u03c7\u03bf\u03c5\u03bd \u03c4\u03bf\u03bd \u03af\u03b4\u03b9\u03bf \u03c4\u03cd\u03c0\u03bf", - "dev_not_config": "\u039f \u03c4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", - "dev_not_found": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "\u0395\u03cd\u03c1\u03bf\u03c2 \u03c6\u03c9\u03c4\u03b5\u03b9\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", - "curr_temp_divider": "\u0394\u03b9\u03b1\u03b9\u03c1\u03ad\u03c4\u03b7\u03c2 \u03c4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1\u03c2 \u03c4\u03b9\u03bc\u03ae\u03c2 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 (0 = \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae\u03c2)", - "max_kelvin": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03b7 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1 \u03c7\u03c1\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c0\u03bf\u03c5 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf kelvin", - "max_temp": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03b7 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1-\u03c3\u03c4\u03cc\u03c7\u03bf\u03c2 (\u03c7\u03c1\u03ae\u03c3\u03b7 min \u03ba\u03b1\u03b9 max = 0 \u03b3\u03b9\u03b1 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae)", - "min_kelvin": "\u0395\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03b7 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1 \u03c7\u03c1\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c0\u03bf\u03c5 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 kelvin", - "min_temp": "\u0395\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03b7 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1-\u03c3\u03c4\u03cc\u03c7\u03bf\u03c2 (\u03c7\u03c1\u03ae\u03c3\u03b7 min \u03ba\u03b1\u03b9 max = 0 \u03b3\u03b9\u03b1 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae)", - "set_temp_divided": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b4\u03b9\u03b1\u03b9\u03c1\u03b5\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c4\u03b9\u03bc\u03ae\u03c2 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c4\u03bf\u03bb\u03ae \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2", - "support_color": "\u0391\u03bd\u03b1\u03b3\u03ba\u03b1\u03c3\u03c4\u03b9\u03ba\u03ae \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03c7\u03c1\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2", - "temp_divider": "\u0394\u03b9\u03b1\u03b9\u03c1\u03ad\u03c4\u03b7\u03c2 \u03c4\u03b9\u03bc\u03ce\u03bd \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 (0 = \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae\u03c2)", - "temp_step_override": "\u0392\u03ae\u03bc\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03c3\u03c4\u03cc\u03c7\u03bf\u03c5", - "tuya_max_coltemp": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03b7 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1 \u03c7\u03c1\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c0\u03bf\u03c5 \u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", - "unit_of_measurement": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" - }, - "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03c4\u03c9\u03bd \u03b5\u03bc\u03c6\u03b1\u03bd\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03c9\u03bd \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03b9\u03ce\u03bd \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae {device_type} `{device_name}`", - "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 Tuya" - }, - "init": { - "data": { - "discovery_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b7\u03bc\u03bf\u03c3\u03ba\u03cc\u03c0\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03bf\u03cd \u03c3\u03b5 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1", - "list_devices": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03ae \u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03ba\u03b5\u03bd\u03ad\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c0\u03bf\u03b8\u03b7\u03ba\u03b5\u03cd\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7", - "query_device": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03b7 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf \u03b5\u03c1\u03c9\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b1\u03c7\u03cd\u03c4\u03b5\u03c1\u03b7 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2", - "query_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b7\u03bc\u03bf\u03c3\u03ba\u03cc\u03c0\u03b7\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03c3\u03b5 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1" - }, - "description": "\u039c\u03b7\u03bd \u03bf\u03c1\u03af\u03b6\u03b5\u03c4\u03b5 \u03c0\u03bf\u03bb\u03cd \u03c7\u03b1\u03bc\u03b7\u03bb\u03ad\u03c2 \u03c4\u03b9\u03bc\u03ad\u03c2 \u03b4\u03b9\u03b1\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b4\u03b7\u03bc\u03bf\u03c3\u03ba\u03bf\u03c0\u03ae\u03c3\u03b5\u03c9\u03bd, \u03b1\u03bb\u03bb\u03b9\u03ce\u03c2 \u03bf\u03b9 \u03ba\u03bb\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b8\u03b1 \u03b1\u03c0\u03bf\u03c4\u03cd\u03c7\u03bf\u03c5\u03bd \u03ba\u03b1\u03b9 \u03b8\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c3\u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2.", - "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd Tuya" + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03ac \u03c3\u03b1\u03c2 Tuya" } } } diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json index e928dc37d57..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": "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/homeassistant/components/tuya/translations/en_GB.json b/homeassistant/components/tuya/translations/en_GB.json deleted file mode 100644 index 90df003a190..00000000000 --- a/homeassistant/components/tuya/translations/en_GB.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "options": { - "step": { - "device": { - "data": { - "max_kelvin": "Max colour temperature supported in Kelvin", - "min_kelvin": "Min colour temperature supported in Kelvin", - "support_color": "Force colour support", - "tuya_max_coltemp": "Max colour temperature reported by device" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/es.json b/homeassistant/components/tuya/translations/es.json index 235f87d5374..a15408bf96d 100644 --- a/homeassistant/components/tuya/translations/es.json +++ b/homeassistant/components/tuya/translations/es.json @@ -1,82 +1,19 @@ { "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", "login_error": "Error de inicio de sesi\u00f3n ({code}): {msg}" }, - "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": { "access_id": "ID de acceso de Tuya IoT", "access_secret": "Tuya IoT Access Secret", "country_code": "C\u00f3digo de pais de tu cuenta (por ejemplo, 1 para USA o 86 para China)", "password": "Contrase\u00f1a", - "platform": "La aplicaci\u00f3n donde se registra tu cuenta", - "region": "Regi\u00f3n", - "tuya_project_type": "Tipo de proyecto en la nube de Tuya", "username": "Usuario" }, - "description": "Introduce tus credencial de Tuya", - "title": "Integraci\u00f3n 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" + "description": "Introduce tus credencial de Tuya" } } } diff --git a/homeassistant/components/tuya/translations/et.json b/homeassistant/components/tuya/translations/et.json index bd23136cb1b..de84ef4aa2f 100644 --- a/homeassistant/components/tuya/translations/et.json +++ b/homeassistant/components/tuya/translations/et.json @@ -1,82 +1,19 @@ { "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", "login_error": "Sisenemine nurjus ( {code} ): {msg}" }, - "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": { "access_id": "Tuya IoT kasutajatunnus", "access_secret": "Tuya IoT salas\u00f5na", "country_code": "Riik", "password": "Salas\u00f5na", - "platform": "\u00c4pp kus konto registreeriti", - "region": "Piirkond", - "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" + "description": "Sisesta oma Tuya konto andmed." } } } diff --git a/homeassistant/components/tuya/translations/fi.json b/homeassistant/components/tuya/translations/fi.json index 3c74a9b8eeb..aeceadba0fc 100644 --- a/homeassistant/components/tuya/translations/fi.json +++ b/homeassistant/components/tuya/translations/fi.json @@ -1,16 +1,13 @@ { "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" + "description": "Anna Tuya-tunnistetietosi." } } } diff --git a/homeassistant/components/tuya/translations/fr.json b/homeassistant/components/tuya/translations/fr.json index c28e62e7ab4..a8049917f41 100644 --- a/homeassistant/components/tuya/translations/fr.json +++ b/homeassistant/components/tuya/translations/fr.json @@ -1,82 +1,19 @@ { "config": { - "abort": { - "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification non valide", - "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." - }, "error": { "invalid_auth": "Authentification non valide", "login_error": "Erreur de connexion ( {code} ): {msg}" }, - "flow_title": "Configuration Tuya", "step": { - "login": { - "data": { - "access_id": "Identifiant d'acc\u00e8s", - "access_secret": "Access Secret", - "country_code": "Code pays", - "endpoint": "Zone de disponibilit\u00e9", - "password": "Mot de passe", - "tuya_app_type": "Application mobile", - "username": "Compte" - }, - "description": "Entrez votre identifiant Tuya", - "title": "Tuya" - }, "user": { "data": { "access_id": "Identifiant d'acc\u00e8s Tuya IoT", "access_secret": "Tuya IoT Access Secret", "country_code": "Pays", "password": "Mot de passe", - "platform": "L'application dans laquelle votre compte est enregistr\u00e9", - "region": "R\u00e9gion", - "tuya_project_type": "Type de projet cloud Tuya", "username": "Compte" }, - "description": "Saisissez vos informations d'identification Tuya.", - "title": "Int\u00e9gration de Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "\u00c9chec de connexion" - }, - "error": { - "dev_multi_type": "Si plusieurs appareils sont s\u00e9lectionn\u00e9s pour \u00eatre configur\u00e9s, ils doivent tous \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" + "description": "Saisissez vos informations d'identification Tuya." } } } diff --git a/homeassistant/components/tuya/translations/he.json b/homeassistant/components/tuya/translations/he.json index 02e2dc94776..737ca754202 100644 --- a/homeassistant/components/tuya/translations/he.json +++ b/homeassistant/components/tuya/translations/he.json @@ -1,67 +1,17 @@ { "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", "login_error": "\u05e9\u05d2\u05d9\u05d0\u05ea \u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea ({code}): {msg}" }, - "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": "\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": "\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" - } - } - }, - "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", - "set_temp_divided": "\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e2\u05e8\u05da \u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4 \u05de\u05d7\u05d5\u05dc\u05e7 \u05e2\u05d1\u05d5\u05e8 \u05d4\u05e4\u05e7\u05d5\u05d3\u05d4 '\u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4 \u05de\u05d5\u05d2\u05d3\u05e8\u05ea'", - "support_color": "\u05db\u05e4\u05d4 \u05ea\u05de\u05d9\u05db\u05d4 \u05d1\u05e6\u05d1\u05e2", - "temp_step_override": "\u05e9\u05dc\u05d1 \u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05ea \u05d4\u05d9\u05e2\u05d3", - "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" - }, - "title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d4\u05ea\u05e7\u05df \u05d8\u05d5\u05d9\u05d4" - }, - "init": { - "title": "\u05e7\u05d1\u05d9\u05e2\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc \u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05d8\u05d5\u05d9\u05d4" + "description": "\u05d4\u05d6\u05e0\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 \u05d4-Tuya \u05e9\u05dc\u05da." } } } diff --git a/homeassistant/components/tuya/translations/hu.json b/homeassistant/components/tuya/translations/hu.json index 2c2969e589c..37414cbebf9 100644 --- a/homeassistant/components/tuya/translations/hu.json +++ b/homeassistant/components/tuya/translations/hu.json @@ -1,82 +1,19 @@ { "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", "login_error": "Bejelentkez\u00e9si hiba ({code}): {msg}" }, - "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": { "access_id": "Tuya IoT azonos\u00edt\u00f3", "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", - "region": "R\u00e9gi\u00f3", - "tuya_project_type": "Tuya felh\u0151 projekt t\u00edpusa", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Adja meg Tuya hiteles\u00edt\u0151 adatait.", - "title": "Tuya integr\u00e1ci\u00f3" - } - } - }, - "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 {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\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" + "description": "Adja meg Tuya hiteles\u00edt\u0151 adatait." } } } diff --git a/homeassistant/components/tuya/translations/id.json b/homeassistant/components/tuya/translations/id.json index 91bf29f3ec8..bbbd74822eb 100644 --- a/homeassistant/components/tuya/translations/id.json +++ b/homeassistant/components/tuya/translations/id.json @@ -1,82 +1,19 @@ { "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", "login_error": "Kesalahan masuk ({code}): {msg}" }, - "flow_title": "Konfigurasi Tuya", "step": { - "login": { - "data": { - "access_id": "ID Akses", - "access_secret": "Kode Rahasia Akses", - "country_code": "Kode Negara", - "endpoint": "Zona Ketersediaan", - "password": "Kata Sandi", - "tuya_app_type": "Aplikasi Seluler", - "username": "Akun" - }, - "description": "Masukkan kredensial Tuya Anda", - "title": "Tuya" - }, "user": { "data": { "access_id": "ID Akses Tuya IoT", "access_secret": "Kode Rahasia Akses Tuya IoT", "country_code": "Negara", "password": "Kata Sandi", - "platform": "Aplikasi tempat akun Anda terdaftar", - "region": "Wilayah", - "tuya_project_type": "Jenis proyek awan Tuya", "username": "Akun" }, - "description": "Masukkan kredensial Tuya Anda.", - "title": "Integrasi 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" + "description": "Masukkan kredensial Tuya Anda." } } } diff --git a/homeassistant/components/tuya/translations/is.json b/homeassistant/components/tuya/translations/is.json index fc7a4878a25..ce1e5122e65 100644 --- a/homeassistant/components/tuya/translations/is.json +++ b/homeassistant/components/tuya/translations/is.json @@ -4,23 +4,10 @@ "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" + "access_secret": "Tuya IoT a\u00f0gangsleyndarm\u00e1l" } } } diff --git a/homeassistant/components/tuya/translations/it.json b/homeassistant/components/tuya/translations/it.json index 0ab73ab3b60..f8013882fb2 100644 --- a/homeassistant/components/tuya/translations/it.json +++ b/homeassistant/components/tuya/translations/it.json @@ -1,82 +1,19 @@ { "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", "login_error": "Errore di accesso ({code}): {msg}" }, - "flow_title": "Configurazione di Tuya", "step": { - "login": { - "data": { - "access_id": "Access ID", - "access_secret": "Access Secret", - "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": { "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": "Account" }, - "description": "Inserisci le tue credenziali Tuya", - "title": "Integrazione Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "Impossibile connettersi" - }, - "error": { - "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" - }, - "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": "Seleziona i dispositivi da configurare o lascia vuoto per salvare la configurazione", - "query_device": "Seleziona 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" + "description": "Inserisci le tue credenziali Tuya" } } } diff --git a/homeassistant/components/tuya/translations/ja.json b/homeassistant/components/tuya/translations/ja.json index 1b5d29584cc..d54a17f7eff 100644 --- a/homeassistant/components/tuya/translations/ja.json +++ b/homeassistant/components/tuya/translations/ja.json @@ -1,82 +1,19 @@ { "config": { - "abort": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" - }, "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "login_error": "\u30ed\u30b0\u30a4\u30f3\u30a8\u30e9\u30fc ({code}): {msg}" }, - "flow_title": "Tuya\u306e\u8a2d\u5b9a", "step": { - "login": { - "data": { - "access_id": "\u30a2\u30af\u30bb\u30b9ID", - "access_secret": "\u30a2\u30af\u30bb\u30b9\u30b7\u30fc\u30af\u30ec\u30c3\u30c8", - "country_code": "\u56fd\u5225\u30b3\u30fc\u30c9", - "endpoint": "\u5229\u7528\u53ef\u80fd\u30be\u30fc\u30f3", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "tuya_app_type": "\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea", - "username": "\u30a2\u30ab\u30a6\u30f3\u30c8" - }, - "description": "Tuya\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", - "title": "Tuya" - }, "user": { "data": { "access_id": "Tuya IoT Access ID", "access_secret": "Tuya IoT Access Secret", "country_code": "\u56fd\u5225\u30b3\u30fc\u30c9", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "platform": "\u30a2\u30ab\u30a6\u30f3\u30c8\u304c\u767b\u9332\u3055\u308c\u3066\u3044\u308b\u30a2\u30d7\u30ea", - "region": "\u30ea\u30fc\u30b8\u30e7\u30f3", - "tuya_project_type": "Tuya Cloud\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u30bf\u30a4\u30d7", "username": "\u30a2\u30ab\u30a6\u30f3\u30c8" }, - "description": "Tuya\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", - "title": "Tuya\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3" - } - } - }, - "options": { - "abort": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" - }, - "error": { - "dev_multi_type": "\u69cb\u6210\u3059\u308b\u8907\u6570\u306e\u9078\u629e\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306f\u3001\u540c\u3058\u30bf\u30a4\u30d7\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "dev_not_config": "\u30c7\u30d0\u30a4\u30b9\u30bf\u30a4\u30d7\u304c\u8a2d\u5b9a\u3067\u304d\u307e\u305b\u3093", - "dev_not_found": "\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3059\u308b\u8f1d\u5ea6\u7bc4\u56f2", - "curr_temp_divider": "\u73fe\u5728\u306e\u6e29\u5ea6\u5024\u306e\u533a\u5207\u308a(0 = \u30c7\u30d5\u30a9\u30eb\u30c8\u3092\u4f7f\u7528)", - "max_kelvin": "\u30b1\u30eb\u30d3\u30f3\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u6700\u5927\u8272\u6e29\u5ea6", - "max_temp": "\u6700\u5927\u76ee\u6a19\u6e29\u5ea6(\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6700\u5c0f\u304a\u3088\u3073\u6700\u5927 = 0\u3092\u4f7f\u7528)", - "min_kelvin": "\u30b1\u30eb\u30d3\u30f3\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u6700\u5c0f\u8272\u6e29\u5ea6", - "min_temp": "\u6700\u5c0f\u76ee\u6a19\u6e29\u5ea6(\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6700\u5c0f\u304a\u3088\u3073\u6700\u5927 = 0\u3092\u4f7f\u7528)", - "set_temp_divided": "\u8a2d\u5b9a\u6e29\u5ea6\u30b3\u30de\u30f3\u30c9\u306b\u533a\u5207\u3089\u308c\u305f\u6e29\u5ea6\u5024\u3092\u4f7f\u7528", - "support_color": "\u5f37\u5236\u7684\u306b\u30ab\u30e9\u30fc\u3092\u30b5\u30dd\u30fc\u30c8", - "temp_divider": "\u6e29\u5ea6\u5024\u306e\u533a\u5207\u308a(0 = \u30c7\u30d5\u30a9\u30eb\u30c8\u3092\u4f7f\u7528)", - "temp_step_override": "\u76ee\u6a19\u6e29\u5ea6\u30b9\u30c6\u30c3\u30d7", - "tuya_max_coltemp": "\u30c7\u30d0\u30a4\u30b9\u306b\u3088\u3063\u3066\u5831\u544a\u3055\u308c\u305f\u6700\u5927\u8272\u6e29\u5ea6", - "unit_of_measurement": "\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3059\u308b\u6e29\u5ea6\u5358\u4f4d" - }, - "description": "{device_type} \u30c7\u30d0\u30a4\u30b9 `{device_name}` \u306e\u8868\u793a\u60c5\u5831\u3092\u8abf\u6574\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u69cb\u6210\u3057\u307e\u3059", - "title": "Tuya\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a" - }, - "init": { - "data": { - "discovery_interval": "\u30c7\u30d0\u30a4\u30b9\u691c\u51fa\u306e\u30dd\u30fc\u30ea\u30f3\u30b0\u9593\u9694(\u79d2\u5358\u4f4d)", - "list_devices": "\u8a2d\u5b9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3059\u308b\u304b\u3001\u7a7a\u767d\u306e\u307e\u307e\u306b\u3057\u3066\u8a2d\u5b9a\u3092\u4fdd\u5b58\u3057\u307e\u3059", - "query_device": "\u30b9\u30c6\u30fc\u30bf\u30b9\u306e\u66f4\u65b0\u3092\u9ad8\u901f\u5316\u3059\u308b\u305f\u3081\u306b\u30af\u30a8\u30ea\u306e\u65b9\u6cd5(query method)\u3092\u4f7f\u7528\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u307e\u3059", - "query_interval": "\u30af\u30a8\u30ea\u30c7\u30d0\u30a4\u30b9\u306e\u30dd\u30fc\u30ea\u30f3\u30b0\u9593\u9694(\u79d2)" - }, - "description": "\u30dd\u30fc\u30ea\u30f3\u30b0\u9593\u9694\u306e\u5024\u3092\u4f4e\u304f\u8a2d\u5b9a\u3057\u3059\u304e\u306a\u3044\u3067\u304f\u3060\u3055\u3044\u3002\u30b3\u30fc\u30eb\u306b\u5931\u6557\u3057\u3066\u30ed\u30b0\u306b\u30a8\u30e9\u30fc\u30e1\u30c3\u30bb\u30fc\u30b8\u304c\u751f\u6210\u3055\u308c\u307e\u3059\u3002", - "title": "Tuya\u30aa\u30d7\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a" + "description": "Tuya\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" } } } diff --git a/homeassistant/components/tuya/translations/ka.json b/homeassistant/components/tuya/translations/ka.json deleted file mode 100644 index 7c80ef1ffba..00000000000 --- a/homeassistant/components/tuya/translations/ka.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "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 index afa2541e7b9..be389e13b06 100644 --- a/homeassistant/components/tuya/translations/ko.json +++ b/homeassistant/components/tuya/translations/ko.json @@ -1,64 +1,16 @@ { "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" + "description": "Tuya \uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694." } } } diff --git a/homeassistant/components/tuya/translations/lb.json b/homeassistant/components/tuya/translations/lb.json index 0000f9ef6e6..8d0b16a3ff8 100644 --- a/homeassistant/components/tuya/translations/lb.json +++ b/homeassistant/components/tuya/translations/lb.json @@ -1,58 +1,16 @@ { "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" + "description": "F\u00ebll deng Tuya Umeldungs Informatiounen aus." } } } diff --git a/homeassistant/components/tuya/translations/nl.json b/homeassistant/components/tuya/translations/nl.json index e12c3ba29f2..ed2ce07c226 100644 --- a/homeassistant/components/tuya/translations/nl.json +++ b/homeassistant/components/tuya/translations/nl.json @@ -1,82 +1,19 @@ { "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", "login_error": "Aanmeldingsfout ({code}): {msg}" }, - "flow_title": "Tuya-configuratie", "step": { - "login": { - "data": { - "access_id": "Toegangs-ID", - "access_secret": "Access Secret", - "country_code": "Landcode", - "endpoint": "Beschikbaarheidszone", - "password": "Wachtwoord", - "tuya_app_type": "Mobiele app", - "username": "Account" - }, - "description": "Voer uw Tuya-inloggegevens in", - "title": "Tuya" - }, "user": { "data": { "access_id": "Tuya IoT-toegangs-ID", "access_secret": "Tuya IoT Access Secret", "country_code": "Land", "password": "Wachtwoord", - "platform": "De app waar uw account is geregistreerd", - "region": "Regio", - "tuya_project_type": "Tuya cloud project type", "username": "Gebruikersnaam" }, - "description": "Voer uw Tuya-inloggegevens in.", - "title": "Tuya-integratie" - } - } - }, - "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": "Ververstijd 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" + "description": "Voer uw Tuya-inloggegevens in." } } } diff --git a/homeassistant/components/tuya/translations/no.json b/homeassistant/components/tuya/translations/no.json index 16657972322..7c0137b80a6 100644 --- a/homeassistant/components/tuya/translations/no.json +++ b/homeassistant/components/tuya/translations/no.json @@ -1,82 +1,19 @@ { "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", "login_error": "P\u00e5loggingsfeil ( {code} ): {msg}" }, - "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": { "access_id": "Tuya IoT Access ID", "access_secret": "Tuya IoT Access Secret", "country_code": "Land", "password": "Passord", - "platform": "Appen der kontoen din er registrert", - "region": "Region", - "tuya_project_type": "Tuya -skyprosjekttype", "username": "Account" }, - "description": "Skriv inn Tuya -legitimasjonen din", - "title": "Tuya Integrasjon" - } - } - }, - "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" + "description": "Skriv inn Tuya -legitimasjonen din" } } } diff --git a/homeassistant/components/tuya/translations/pl.json b/homeassistant/components/tuya/translations/pl.json index a191c40ca3f..e7fe9caf65a 100644 --- a/homeassistant/components/tuya/translations/pl.json +++ b/homeassistant/components/tuya/translations/pl.json @@ -1,82 +1,19 @@ { "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", "login_error": "B\u0142\u0105d logowania ({code}): {msg}" }, - "flow_title": "Konfiguracja integracji Tuya", "step": { - "login": { - "data": { - "access_id": "Identyfikator dost\u0119pu", - "access_secret": "Has\u0142o dost\u0119pu", - "country_code": "Kod kraju", - "endpoint": "Strefa dost\u0119pno\u015bci", - "password": "Has\u0142o", - "tuya_app_type": "Aplikacja mobilna", - "username": "Konto" - }, - "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce Tuya", - "title": "Tuya" - }, "user": { "data": { "access_id": "Identyfikator dost\u0119pu do Tuya IoT", "access_secret": "Has\u0142o dost\u0119pu do Tuya IoT", "country_code": "Kraj", "password": "Has\u0142o", - "platform": "Aplikacja, w kt\u00f3rej zarejestrowane jest Twoje konto", - "region": "Region", - "tuya_project_type": "Typ projektu chmury Tuya", "username": "Konto" }, - "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce Tuya", - "title": "Integracja 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" + "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce Tuya" } } } diff --git a/homeassistant/components/tuya/translations/pt-BR.json b/homeassistant/components/tuya/translations/pt-BR.json index 242d1e9ee08..5ae0382d0b8 100644 --- a/homeassistant/components/tuya/translations/pt-BR.json +++ b/homeassistant/components/tuya/translations/pt-BR.json @@ -1,82 +1,19 @@ { "config": { - "abort": { - "cannot_connect": "Falha ao conectar", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." - }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "login_error": "Erro de login ({code}): {msg}" }, - "flow_title": "Configura\u00e7\u00e3o Tuya", "step": { - "login": { - "data": { - "access_id": "Tuya IoT Access ID", - "access_secret": "Tuya IoT Access Secret", - "country_code": "Pa\u00eds", - "endpoint": "Regi\u00e3o", - "password": "Senha do Aplicativo", - "tuya_app_type": "O aplicativo onde sua conta \u00e9 registrada", - "username": "Usu\u00e1rio do Aplicativo" - }, - "description": "Digite sua credencial Tuya", - "title": "Integra\u00e7\u00e3o Tuya" - }, "user": { "data": { "access_id": "Tuya IoT Access ID", "access_secret": "Tuya IoT Access Secret", "country_code": "Pa\u00eds", "password": "Senha do Aplicativo", - "platform": "O aplicativo onde sua conta \u00e9 registrada", - "region": "Regi\u00e3o", - "tuya_project_type": "Tipo de projeto de Tuya Cloud", "username": "Usu\u00e1rio do Aplicativo" }, - "description": "Digite sua credencial Tuya", - "title": "Integra\u00e7\u00e3o Tuya" - } - } - }, - "options": { - "abort": { - "cannot_connect": "Falha ao conectar" - }, - "error": { - "dev_multi_type": "V\u00e1rios dispositivos selecionados para configurar devem ser do mesmo tipo", - "dev_not_config": "Tipo de dispositivo n\u00e3o configur\u00e1vel", - "dev_not_found": "Dispositivo n\u00e3o encontrado" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "Faixa de brilho usada pelo dispositivo", - "curr_temp_divider": "Divisor do valor da temperatura atual (0 = usar o padr\u00e3o)", - "max_kelvin": "Temperatura m\u00e1xima de cor suportada em Kelvin", - "max_temp": "Temperatura m\u00e1xima do alvo (use min e max = 0 para padr\u00e3o)", - "min_kelvin": "Temperatura m\u00ednima de cor suportada em kelvin", - "min_temp": "Temperatura m\u00ednima desejada (use m\u00edn e m\u00e1x = 0 para o padr\u00e3o)", - "set_temp_divided": "Use o valor de temperatura dividido para o comando de temperatura definido", - "support_color": "For\u00e7ar suporte de cores", - "temp_divider": "Divisor de valores de temperatura (0 = usar padr\u00e3o)", - "temp_step_override": "Etapa de temperatura alvo", - "tuya_max_coltemp": "Temperatura m\u00e1xima de cor relatada pelo dispositivo", - "unit_of_measurement": "Unidade de temperatura usada pelo dispositivo" - }, - "description": "Configure as op\u00e7\u00f5es para ajustar as informa\u00e7\u00f5es exibidas para o dispositivo {device_type} `{device_name}`", - "title": "Configurar dispositivo Tuya" - }, - "init": { - "data": { - "discovery_interval": "Intervalo de pesquisa do dispositivo de descoberta em segundos", - "list_devices": "Selecione os dispositivos para configurar ou deixe em branco para salvar a configura\u00e7\u00e3o", - "query_device": "Selecione o dispositivo que usar\u00e1 o m\u00e9todo de consulta para atualiza\u00e7\u00e3o de status mais r\u00e1pida", - "query_interval": "Intervalo de sondagem do dispositivo de consulta em segundos" - }, - "description": "N\u00e3o defina valores de intervalo de sondagens muito baixos ou as chamadas falhar\u00e3o gerando mensagem de erro no log", - "title": "Configurar op\u00e7\u00f5es do Tuya" + "description": "Digite sua credencial Tuya" } } } diff --git a/homeassistant/components/tuya/translations/pt.json b/homeassistant/components/tuya/translations/pt.json index 566746538c0..f9de53a43fe 100644 --- a/homeassistant/components/tuya/translations/pt.json +++ b/homeassistant/components/tuya/translations/pt.json @@ -1,10 +1,5 @@ { "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" }, @@ -16,10 +11,5 @@ } } } - }, - "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 index e13914683f1..f58a78d1bb5 100644 --- a/homeassistant/components/tuya/translations/ru.json +++ b/homeassistant/components/tuya/translations/ru.json @@ -1,82 +1,19 @@ { "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.", "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": { - "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\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Tuya.", - "title": "Tuya" - }, "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": "\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", - "tuya_project_type": "\u0422\u0438\u043f \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0430 Tuya", "username": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 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" + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Tuya." } } } diff --git a/homeassistant/components/tuya/translations/sk.json b/homeassistant/components/tuya/translations/sk.json index 23d8822116c..93fa886f796 100644 --- a/homeassistant/components/tuya/translations/sk.json +++ b/homeassistant/components/tuya/translations/sk.json @@ -1,21 +1,12 @@ { "config": { - "abort": { - "invalid_auth": "Neplatn\u00e9 overenie" - }, "error": { "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { - "login": { - "data": { - "country_code": "K\u00f3d krajiny" - } - }, "user": { "data": { - "country_code": "Krajina", - "region": "Oblas\u0165" + "country_code": "Krajina" } } } diff --git a/homeassistant/components/tuya/translations/sl.json b/homeassistant/components/tuya/translations/sl.json deleted file mode 100644 index 52d2fd3e973..00000000000 --- a/homeassistant/components/tuya/translations/sl.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "region": "Regija" - } - } - } - }, - "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 index 85cc9c57fd3..344aea60f6f 100644 --- a/homeassistant/components/tuya/translations/sv.json +++ b/homeassistant/components/tuya/translations/sv.json @@ -1,16 +1,13 @@ { "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" + "description": "Ange dina Tuya anv\u00e4ndaruppgifter." } } } diff --git a/homeassistant/components/tuya/translations/tr.json b/homeassistant/components/tuya/translations/tr.json index 1bcc2bc627e..fe1b81831a6 100644 --- a/homeassistant/components/tuya/translations/tr.json +++ b/homeassistant/components/tuya/translations/tr.json @@ -1,82 +1,19 @@ { "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", "login_error": "Giri\u015f hatas\u0131 ( {code} ): {msg}" }, - "flow_title": "Tuya yap\u0131land\u0131rmas\u0131", "step": { - "login": { - "data": { - "access_id": "Eri\u015fim Kimli\u011fi", - "access_secret": "Eri\u015fim Anahtar\u0131", - "country_code": "\u00dclke Kodu", - "endpoint": "Kullan\u0131labilirlik Alan\u0131", - "password": "\u015eifre", - "tuya_app_type": "Tuya uygulama tipi", - "username": "Kullan\u0131c\u0131 Ad\u0131" - }, - "description": "Tuya kimlik bilgilerinizi girin", - "title": "Tuya" - }, "user": { "data": { "access_id": "Tuya IoT Eri\u015fim Kimli\u011fi", "access_secret": "Tuya IoT Eri\u015fim Anahtar\u0131", "country_code": "\u00dclke", "password": "Parola", - "platform": "Hesab\u0131n\u0131z\u0131n kay\u0131tl\u0131 oldu\u011fu uygulama", - "region": "B\u00f6lge", - "tuya_project_type": "Tuya bulut proje t\u00fcr\u00fc", "username": "Hesap" }, - "description": "Tuya kimlik bilgilerinizi girin.", - "title": "Tuya Entegrasyonu" - } - } - }, - "options": { - "abort": { - "cannot_connect": "Ba\u011flanma hatas\u0131" - }, - "error": { - "dev_multi_type": "Yap\u0131land\u0131r\u0131lacak birden \u00e7ok se\u00e7ili cihaz ayn\u0131 t\u00fcrde olmal\u0131d\u0131r", - "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", - "curr_temp_divider": "Mevcut S\u0131cakl\u0131k de\u011feri b\u00f6l\u00fcc\u00fc (0 = varsay\u0131lan\u0131 kullan)", - "max_kelvin": "Kelvin'de desteklenen maksimum renk s\u0131cakl\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)", - "set_temp_divided": "Ayarlanan s\u0131cakl\u0131k komutu i\u00e7in b\u00f6l\u00fcnm\u00fc\u015f S\u0131cakl\u0131k de\u011ferini kullan\u0131n", - "support_color": "Vurgu rengi", - "temp_divider": "S\u0131cakl\u0131k de\u011ferleri ay\u0131r\u0131c\u0131 (0 = varsay\u0131lan\u0131 kullan)", - "temp_step_override": "Hedef S\u0131cakl\u0131k ad\u0131m\u0131", - "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} cihaz\u0131 ` {device_name} device_name} ` i\u00e7in g\u00f6r\u00fcnt\u00fclenen bilgileri ayarlamak \u00fczere 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" + "description": "Tuya kimlik bilgilerinizi girin." } } } diff --git a/homeassistant/components/tuya/translations/uk.json b/homeassistant/components/tuya/translations/uk.json index 97616e5f388..0e1535d9681 100644 --- a/homeassistant/components/tuya/translations/uk.json +++ b/homeassistant/components/tuya/translations/uk.json @@ -1,62 +1,16 @@ { "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": "\u0412\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e. \u041c\u043e\u0436\u043b\u0438\u0432\u0430 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f." - }, "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" + "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." } } } diff --git a/homeassistant/components/tuya/translations/zh-Hans.json b/homeassistant/components/tuya/translations/zh-Hans.json index b990d5bb8a1..b5d00c5586a 100644 --- a/homeassistant/components/tuya/translations/zh-Hans.json +++ b/homeassistant/components/tuya/translations/zh-Hans.json @@ -1,81 +1,19 @@ { "config": { - "abort": { - "cannot_connect": "\u8fde\u63a5\u5931\u8d25", - "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548", - "single_instance_allowed": "\u8be5\u96c6\u6210\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86\uff0c\u4e14\u53ea\u80fd\u914d\u7f6e\u4e00\u6b21\u3002\u82e5\u8981\u91cd\u65b0\u914d\u7f6e\uff0c\u8bf7\u5148\u5220\u9664\u65e7\u96c6\u6210\u3002" - }, "error": { "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", - "tuya_project_type": "\u6d82\u9e26\u4e91\u9879\u76ee\u7c7b\u578b", "username": "\u7528\u6237\u540d" }, - "description": "\u8bf7\u8f93\u5165\u6d82\u9e26\u8d26\u6237\u4fe1\u606f\u3002", - "title": "\u6d82\u9e26" - } - } - }, - "options": { - "abort": { - "cannot_connect": "\u8fde\u63a5\u5931\u8d25" - }, - "error": { - "dev_multi_type": "\u591a\u4e2a\u8981\u914d\u7f6e\u7684\u8bbe\u5907\u5fc5\u987b\u5177\u6709\u76f8\u540c\u7684\u7c7b\u578b", - "dev_not_config": "\u8bbe\u5907\u7c7b\u578b\u4e0d\u53ef\u914d\u7f6e", - "dev_not_found": "\u672a\u627e\u5230\u8bbe\u5907" - }, - "step": { - "device": { - "data": { - "brightness_range_mode": "\u8bbe\u5907\u4f7f\u7528\u7684\u4eae\u5ea6\u8303\u56f4", - "curr_temp_divider": "\u5f53\u524d\u6e29\u5ea6\u503c\u9664\u6570\uff080 = \u4f7f\u7528\u9ed8\u8ba4\u503c\uff09", - "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", - "set_temp_divided": "\u5bf9\u8bbe\u7f6e\u6e29\u5ea6\u547d\u4ee4\u4f7f\u7528\u7ecf\u8fc7\u9664\u6cd5\u7684\u6e29\u5ea6\u503c", - "support_color": "\u5f3a\u5236\u652f\u6301\u8c03\u8272", - "temp_divider": "\u6e29\u5ea6\u503c\u9664\u6570\uff080 = \u4f7f\u7528\u9ed8\u8ba4\u503c\uff09", - "temp_step_override": "\u76ee\u6807\u6e29\u5ea6\u6b65\u957f", - "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": "\u6d82\u9e26\u914d\u7f6e\u9009\u9879" + "description": "\u8bf7\u8f93\u5165\u6d82\u9e26\u8d26\u6237\u4fe1\u606f\u3002" } } } diff --git a/homeassistant/components/tuya/translations/zh-Hant.json b/homeassistant/components/tuya/translations/zh-Hant.json index e9898f782af..c255cb723a9 100644 --- a/homeassistant/components/tuya/translations/zh-Hant.json +++ b/homeassistant/components/tuya/translations/zh-Hant.json @@ -1,82 +1,19 @@ { "config": { - "abort": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" - }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "login_error": "\u767b\u5165\u932f\u8aa4\uff08{code}\uff09\uff1a{msg}" }, - "flow_title": "Tuya \u8a2d\u5b9a", "step": { - "login": { - "data": { - "access_id": "Access ID", - "access_secret": "\u5b58\u53d6\u79c1\u9470", - "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": { "access_id": "Tuya IoT Access ID", "access_secret": "Tuya IoT \u5b58\u53d6\u79c1\u9470", "country_code": "\u570b\u5bb6", "password": "\u5bc6\u78bc", - "platform": "\u5e33\u6236\u8a3b\u518a\u6240\u5728\u4f4d\u7f6e", - "region": "\u5340\u57df", - "tuya_project_type": "Tuya \u96f2\u5c08\u6848\u985e\u5225", "username": "\u5e33\u865f" }, - "description": "\u8f38\u5165 Tuya \u6191\u8b49", - "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\u5225", - "dev_not_config": "\u88dd\u7f6e\u985e\u5225\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": "\u641c\u7d22\u88dd\u7f6e\u66f4\u65b0\u79d2\u9593\u8ddd", - "list_devices": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e\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" + "description": "\u8f38\u5165 Tuya \u6191\u8b49" } } } diff --git a/homeassistant/components/twentemilieu/translations/nl.json b/homeassistant/components/twentemilieu/translations/nl.json index 575c642a777..740c38da286 100644 --- a/homeassistant/components/twentemilieu/translations/nl.json +++ b/homeassistant/components/twentemilieu/translations/nl.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Kan geen verbinding maken", - "invalid_address": "Adres niet gevonden in servicegebied Twente Milieu." + "invalid_address": "Adres niet gevonden in Twente Milieu servicegebied." }, "step": { "user": { diff --git a/homeassistant/components/ukraine_alarm/translations/bg.json b/homeassistant/components/ukraine_alarm/translations/bg.json index 181f28745b9..ed64f99c6b2 100644 --- a/homeassistant/components/ukraine_alarm/translations/bg.json +++ b/homeassistant/components/ukraine_alarm/translations/bg.json @@ -6,11 +6,6 @@ "max_regions": "\u041c\u043e\u0433\u0430\u0442 \u0434\u0430 \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442 \u043c\u0430\u043a\u0441\u0438\u043c\u0443\u043c 5 \u0440\u0435\u0433\u0438\u043e\u043d\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\u0448\u043d\u043e \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": { "community": { "data": { @@ -22,14 +17,8 @@ "region": "\u0420\u0435\u0433\u0438\u043e\u043d" } }, - "state": { - "data": { - "region": "\u0420\u0435\u0433\u0438\u043e\u043d" - } - }, "user": { "data": { - "api_key": "API \u043a\u043b\u044e\u0447", "region": "\u0420\u0435\u0433\u0438\u043e\u043d" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 Ukraine Alarm. \u0417\u0430 \u0434\u0430 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u0442\u0435 API \u043a\u043b\u044e\u0447, \u043e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 {api_url}" diff --git a/homeassistant/components/ukraine_alarm/translations/ca.json b/homeassistant/components/ukraine_alarm/translations/ca.json index c650423723d..ea03b64dd3b 100644 --- a/homeassistant/components/ukraine_alarm/translations/ca.json +++ b/homeassistant/components/ukraine_alarm/translations/ca.json @@ -8,12 +8,6 @@ "timeout": "Temps m\u00e0xim d'espera per establir la connexi\u00f3 esgotat", "unknown": "Error inesperat" }, - "error": { - "cannot_connect": "Ha fallat la connexi\u00f3", - "invalid_api_key": "Clau API inv\u00e0lida", - "timeout": "Temps m\u00e0xim d'espera per establir la connexi\u00f3 esgotat", - "unknown": "Error inesperat" - }, "step": { "community": { "data": { @@ -27,15 +21,8 @@ }, "description": "Si vols monitorar no nom\u00e9s l'estat, tria el districte espec\u00edfic" }, - "state": { - "data": { - "region": "Regi\u00f3" - }, - "description": "Escull l'estat a monitorar" - }, "user": { "data": { - "api_key": "Clau API", "region": "Regi\u00f3" }, "description": "Escull l'estat a monitorar" diff --git a/homeassistant/components/ukraine_alarm/translations/de.json b/homeassistant/components/ukraine_alarm/translations/de.json index 2eb99ee692f..7c7c093c7cd 100644 --- a/homeassistant/components/ukraine_alarm/translations/de.json +++ b/homeassistant/components/ukraine_alarm/translations/de.json @@ -8,12 +8,6 @@ "timeout": "Zeit\u00fcberschreitung beim Verbindungsaufbau", "unknown": "Unerwarteter Fehler" }, - "error": { - "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", - "timeout": "Zeit\u00fcberschreitung beim Verbindungsaufbau", - "unknown": "Unerwarteter Fehler" - }, "step": { "community": { "data": { @@ -27,15 +21,8 @@ }, "description": "Wenn du nicht nur das Bundesland \u00fcberwachen m\u00f6chtest, w\u00e4hle seinen bestimmten Bezirk" }, - "state": { - "data": { - "region": "Region" - }, - "description": "Zu \u00fcberwachendes Bundesland ausw\u00e4hlen" - }, "user": { "data": { - "api_key": "API-Schl\u00fcssel", "region": "Region" }, "description": "Zu \u00fcberwachendes Bundesland ausw\u00e4hlen" diff --git a/homeassistant/components/ukraine_alarm/translations/el.json b/homeassistant/components/ukraine_alarm/translations/el.json index 61b40412415..779c7dc1102 100644 --- a/homeassistant/components/ukraine_alarm/translations/el.json +++ b/homeassistant/components/ukraine_alarm/translations/el.json @@ -8,12 +8,6 @@ "timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, - "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API", - "timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" - }, "step": { "community": { "data": { @@ -27,15 +21,8 @@ }, "description": "\u0391\u03bd \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c7\u03b9 \u03bc\u03cc\u03bd\u03bf \u03c4\u03b7\u03bd \u03c0\u03bf\u03bb\u03b9\u03c4\u03b5\u03af\u03b1, \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03b7 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae \u03c4\u03b7\u03c2." }, - "state": { - "data": { - "region": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae" - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7" - }, "user": { "data": { - "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", "region": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae\u03c2" }, "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c3\u03c5\u03bd\u03b1\u03b3\u03b5\u03c1\u03bc\u03bf\u03cd \u03c4\u03b7\u03c2 \u039f\u03c5\u03ba\u03c1\u03b1\u03bd\u03af\u03b1\u03c2. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {api_url}" diff --git a/homeassistant/components/ukraine_alarm/translations/en.json b/homeassistant/components/ukraine_alarm/translations/en.json index 6397f652aee..857311ea3e7 100644 --- a/homeassistant/components/ukraine_alarm/translations/en.json +++ b/homeassistant/components/ukraine_alarm/translations/en.json @@ -8,12 +8,6 @@ "timeout": "Timeout establishing connection", "unknown": "Unexpected error" }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_api_key": "Invalid API key", - "timeout": "Timeout establishing connection", - "unknown": "Unexpected error" - }, "step": { "community": { "data": { @@ -27,15 +21,8 @@ }, "description": "If you want to monitor not only state, choose its specific district" }, - "state": { - "data": { - "region": "Region" - }, - "description": "Choose state to monitor" - }, "user": { "data": { - "api_key": "API Key", "region": "Region" }, "description": "Choose state to monitor" diff --git a/homeassistant/components/ukraine_alarm/translations/es.json b/homeassistant/components/ukraine_alarm/translations/es.json index 7f8db3877c0..a9efcd0d536 100644 --- a/homeassistant/components/ukraine_alarm/translations/es.json +++ b/homeassistant/components/ukraine_alarm/translations/es.json @@ -6,9 +6,6 @@ "timeout": "Tiempo m\u00e1ximo de espera para establecer la conexi\u00f3n agotado", "unknown": "Error inesperado" }, - "error": { - "unknown": "Error inesperado" - }, "step": { "district": { "data": { @@ -16,15 +13,8 @@ }, "description": "Si quieres monitorear no s\u00f3lo el estado, elige el distrito espec\u00edfico" }, - "state": { - "data": { - "region": "Regi\u00f3n" - }, - "description": "Escoja el estado a monitorear" - }, "user": { "data": { - "api_key": "Clave API", "region": "Regi\u00f3n" }, "description": "Escoja el estado a monitorear" diff --git a/homeassistant/components/ukraine_alarm/translations/et.json b/homeassistant/components/ukraine_alarm/translations/et.json index 452b9d8cd11..1c5d74db071 100644 --- a/homeassistant/components/ukraine_alarm/translations/et.json +++ b/homeassistant/components/ukraine_alarm/translations/et.json @@ -8,12 +8,6 @@ "timeout": "\u00dchenduse ajal\u00f5pp", "unknown": "Ootamatu t\u00f5rge" }, - "error": { - "cannot_connect": "\u00dchendamine nurjus", - "invalid_api_key": "Vigane API v\u00f5ti", - "timeout": "\u00dchendamise ajal\u00f5pp", - "unknown": "Ootamatu t\u00f5rge" - }, "step": { "community": { "data": { @@ -27,15 +21,8 @@ }, "description": "Kui soovid j\u00e4lgida mitte ainult oblastit vali konkreetne piirkond" }, - "state": { - "data": { - "region": "Piirkond" - }, - "description": "Vali j\u00e4lgitav oblast" - }, "user": { "data": { - "api_key": "API v\u00f5ti", "region": "Piirkond" }, "description": "Vali j\u00e4lgitav oblast" diff --git a/homeassistant/components/ukraine_alarm/translations/fr.json b/homeassistant/components/ukraine_alarm/translations/fr.json index 6cafde0e4d0..9a8bd95da63 100644 --- a/homeassistant/components/ukraine_alarm/translations/fr.json +++ b/homeassistant/components/ukraine_alarm/translations/fr.json @@ -8,12 +8,6 @@ "timeout": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9", "unknown": "Erreur inattendue" }, - "error": { - "cannot_connect": "\u00c9chec de connexion", - "invalid_api_key": "Cl\u00e9 d'API non valide", - "timeout": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9", - "unknown": "Erreur inattendue" - }, "step": { "community": { "data": { @@ -27,15 +21,8 @@ }, "description": "Si vous ne d\u00e9sirez pas uniquement surveiller l'\u00c9tat, s\u00e9lectionnez son district sp\u00e9cifique" }, - "state": { - "data": { - "region": "R\u00e9gion" - }, - "description": "Choisissez l'\u00c9tat \u00e0 surveiller" - }, "user": { "data": { - "api_key": "Cl\u00e9 d'API", "region": "R\u00e9gion" }, "description": "Choisissez l'\u00c9tat \u00e0 surveiller" diff --git a/homeassistant/components/ukraine_alarm/translations/he.json b/homeassistant/components/ukraine_alarm/translations/he.json new file mode 100644 index 00000000000..6f3d95e2bb4 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/he.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "community": { + "data": { + "region": "\u05d0\u05d9\u05d6\u05d5\u05e8" + } + }, + "district": { + "data": { + "region": "\u05d0\u05d9\u05d6\u05d5\u05e8" + } + }, + "user": { + "data": { + "region": "\u05d0\u05d9\u05d6\u05d5\u05e8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/hu.json b/homeassistant/components/ukraine_alarm/translations/hu.json index e72d6140c01..64273f1348b 100644 --- a/homeassistant/components/ukraine_alarm/translations/hu.json +++ b/homeassistant/components/ukraine_alarm/translations/hu.json @@ -8,34 +8,21 @@ "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, - "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", - "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" - }, "step": { "community": { "data": { - "region": "R\u00e9gi\u00f3" + "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" }, "description": "Ha nem csak a megy\u00e9t \u00e9s a ker\u00fcletet szeretn\u00e9 figyelni, v\u00e1lassza ki az adott k\u00f6z\u00f6ss\u00e9get" }, "district": { "data": { - "region": "R\u00e9gi\u00f3" + "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" }, "description": "Ha nem csak megy\u00e9t szeretne figyelni, v\u00e1lassza ki az adott ker\u00fcletet" }, - "state": { - "data": { - "region": "R\u00e9gi\u00f3" - }, - "description": "V\u00e1lassza ki a megfigyelni k\u00edv\u00e1nt megy\u00e9t" - }, "user": { "data": { - "api_key": "API kulcs", "region": "R\u00e9gi\u00f3" }, "description": "\u00c1ll\u00edtsa be az Ukraine Alarm integr\u00e1ci\u00f3t. API-kulcs l\u00e9trehoz\u00e1s\u00e1hoz l\u00e1togasson el ide: {api_url}" diff --git a/homeassistant/components/ukraine_alarm/translations/id.json b/homeassistant/components/ukraine_alarm/translations/id.json index bd453d78e06..1751c132e9d 100644 --- a/homeassistant/components/ukraine_alarm/translations/id.json +++ b/homeassistant/components/ukraine_alarm/translations/id.json @@ -8,12 +8,6 @@ "timeout": "Tenggang waktu membuat koneksi habis", "unknown": "Kesalahan yang tidak diharapkan" }, - "error": { - "cannot_connect": "Gagal terhubung", - "invalid_api_key": "Kunci API tidak valid", - "timeout": "Tenggang waktu membuat koneksi habis", - "unknown": "Kesalahan yang tidak diharapkan" - }, "step": { "community": { "data": { @@ -27,15 +21,8 @@ }, "description": "Jika ingin memantau tidak hanya negara bagian, pilih distrik tertentu" }, - "state": { - "data": { - "region": "Wilayah" - }, - "description": "Pilih negara bagian untuk dipantau" - }, "user": { "data": { - "api_key": "Kunci API", "region": "Wilayah" }, "description": "Pilih negara bagian untuk dipantau" diff --git a/homeassistant/components/ukraine_alarm/translations/it.json b/homeassistant/components/ukraine_alarm/translations/it.json index 4994cf45d33..7c02193a43b 100644 --- a/homeassistant/components/ukraine_alarm/translations/it.json +++ b/homeassistant/components/ukraine_alarm/translations/it.json @@ -8,34 +8,21 @@ "timeout": "Tempo scaduto per stabile la connessione.", "unknown": "Errore imprevisto" }, - "error": { - "cannot_connect": "Impossibile connettersi", - "invalid_api_key": "Chiave API non valida", - "timeout": "Tempo scaduto per stabile la connessione.", - "unknown": "Errore imprevisto" - }, "step": { "community": { "data": { - "region": "Regione" + "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" }, "description": "Se vuoi monitorare non solo stato e distretto, scegli la sua comunit\u00e0 specifica" }, "district": { "data": { - "region": "Regione" + "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" }, "description": "Se vuoi monitorare non solo lo stato, scegli il suo distretto specifico" }, - "state": { - "data": { - "region": "Regione" - }, - "description": "Scegli lo stato da monitorare" - }, "user": { "data": { - "api_key": "Chiave API", "region": "Regione" }, "description": "Scegli lo stato da monitorare" diff --git a/homeassistant/components/ukraine_alarm/translations/ja.json b/homeassistant/components/ukraine_alarm/translations/ja.json index b879cf4541e..81bdabdc8d3 100644 --- a/homeassistant/components/ukraine_alarm/translations/ja.json +++ b/homeassistant/components/ukraine_alarm/translations/ja.json @@ -8,34 +8,21 @@ "timeout": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, - "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", - "timeout": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" - }, "step": { "community": { "data": { - "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" + "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" }, "description": "\u5dde\u3084\u5730\u533a\u3060\u3051\u3067\u306a\u304f\u3001\u7279\u5b9a\u306e\u5730\u57df\u3092\u76e3\u8996\u3057\u305f\u3044\u5834\u5408\u306f\u3001\u305d\u306e\u5730\u57df\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" }, "district": { "data": { - "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" + "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" }, "description": "\u5dde\u3060\u3051\u3067\u306a\u304f\u3001\u7279\u5b9a\u306e\u5730\u533a\u3092\u76e3\u8996\u3057\u305f\u3044\u5834\u5408\u306f\u3001\u305d\u306e\u5730\u533a\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" }, - "state": { - "data": { - "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" - }, - "description": "\u30e2\u30cb\u30bf\u30fc\u3059\u308b\u72b6\u614b\u3092\u9078\u629e" - }, "user": { "data": { - "api_key": "API\u30ad\u30fc", "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" }, "description": "\u30a6\u30af\u30e9\u30a4\u30ca\u306e\u8b66\u5831\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001 {api_url} \u306b\u79fb\u52d5\u3057\u3066\u304f\u3060\u3055\u3044" diff --git a/homeassistant/components/ukraine_alarm/translations/ko.json b/homeassistant/components/ukraine_alarm/translations/ko.json index ab66909f96a..e9da77f866b 100644 --- a/homeassistant/components/ukraine_alarm/translations/ko.json +++ b/homeassistant/components/ukraine_alarm/translations/ko.json @@ -7,11 +7,6 @@ "rate_limit": "\uc694\uccad\uc774 \ub108\ubb34 \ub9ce\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, - "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" - }, "step": { "community": { "data": { @@ -25,15 +20,8 @@ }, "description": "\uc8fc\ubfd0\ub9cc \uc544\ub2c8\ub77c \ud2b9\uc815 \uc9c0\uc5ed\uc744 \ubaa8\ub2c8\ud130\ub9c1\ud558\ub824\uba74 \ud574\ub2f9 \uc9c0\uc5ed\uc744 \uc120\ud0dd\ud558\uc2ed\uc2dc\uc624" }, - "state": { - "data": { - "region": "\uc9c0\uc5ed" - }, - "description": "\ubaa8\ub2c8\ud130\ub9c1\ud560 \uc8fc \uc120\ud0dd" - }, "user": { "data": { - "api_key": "API \ud0a4", "region": "\uc9c0\uc5ed" }, "description": "\ubaa8\ub2c8\ud130\ub9c1\ud560 \uc8fc \uc120\ud0dd" diff --git a/homeassistant/components/ukraine_alarm/translations/nl.json b/homeassistant/components/ukraine_alarm/translations/nl.json index ab9973a0618..6f09f794ac4 100644 --- a/homeassistant/components/ukraine_alarm/translations/nl.json +++ b/homeassistant/components/ukraine_alarm/translations/nl.json @@ -8,34 +8,21 @@ "timeout": "Time-out bij het maken van verbinding", "unknown": "Onverwachte fout" }, - "error": { - "cannot_connect": "Kan geen verbinding maken", - "invalid_api_key": "Ongeldige API-sleutel", - "timeout": "Time-out bij het maken van verbinding", - "unknown": "Onverwachte fout" - }, "step": { "community": { "data": { - "region": "Regio" + "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" }, "description": "Als u niet alleen de staat en het district wilt volgen, kiest u de specifieke gemeenschap ervan" }, "district": { "data": { - "region": "Regio" + "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" }, "description": "Als u niet alleen de staat wilt controleren, kies dan het specifieke district" }, - "state": { - "data": { - "region": "Regio" - }, - "description": "Kies staat om te monitoren" - }, "user": { "data": { - "api_key": "API-sleutel", "region": "Regio" }, "description": "Kies staat om te monitoren" diff --git a/homeassistant/components/ukraine_alarm/translations/no.json b/homeassistant/components/ukraine_alarm/translations/no.json index 10a717c72ec..de5e4284cb0 100644 --- a/homeassistant/components/ukraine_alarm/translations/no.json +++ b/homeassistant/components/ukraine_alarm/translations/no.json @@ -8,12 +8,6 @@ "timeout": "Tidsavbrudd oppretter forbindelse", "unknown": "Uventet feil" }, - "error": { - "cannot_connect": "Tilkobling mislyktes", - "invalid_api_key": "Ugyldig API-n\u00f8kkel", - "timeout": "Tidsavbrudd oppretter forbindelse", - "unknown": "Uventet feil" - }, "step": { "community": { "data": { @@ -27,15 +21,8 @@ }, "description": "Hvis du ikke bare vil overv\u00e5ke staten, velg dens spesifikke distrikt" }, - "state": { - "data": { - "region": "Region" - }, - "description": "Velg tilstand \u00e5 overv\u00e5ke" - }, "user": { "data": { - "api_key": "API-n\u00f8kkel", "region": "Region" }, "description": "Velg tilstand \u00e5 overv\u00e5ke" diff --git a/homeassistant/components/ukraine_alarm/translations/pl.json b/homeassistant/components/ukraine_alarm/translations/pl.json index 6b970b759ed..7d432900372 100644 --- a/homeassistant/components/ukraine_alarm/translations/pl.json +++ b/homeassistant/components/ukraine_alarm/translations/pl.json @@ -8,12 +8,6 @@ "timeout": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia", "unknown": "Nieoczekiwany b\u0142\u0105d" }, - "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "invalid_api_key": "Nieprawid\u0142owy klucz API", - "timeout": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia", - "unknown": "Nieoczekiwany b\u0142\u0105d" - }, "step": { "community": { "data": { @@ -27,15 +21,8 @@ }, "description": "Je\u015bli nie chcesz monitorowa\u0107 tylko rejonu, wybierz jego konkretny dystrykt" }, - "state": { - "data": { - "region": "Region" - }, - "description": "Wybierz rejon do monitorowania" - }, "user": { "data": { - "api_key": "Klucz API", "region": "Region" }, "description": "Wybierz rejon do monitorowania" diff --git a/homeassistant/components/ukraine_alarm/translations/pt-BR.json b/homeassistant/components/ukraine_alarm/translations/pt-BR.json index 316a8e96563..64b371196e3 100644 --- a/homeassistant/components/ukraine_alarm/translations/pt-BR.json +++ b/homeassistant/components/ukraine_alarm/translations/pt-BR.json @@ -8,12 +8,6 @@ "timeout": "Tempo limite estabelecendo conex\u00e3o", "unknown": "Erro inesperado" }, - "error": { - "cannot_connect": "Falhou ao conectar", - "invalid_api_key": "Chave de API inv\u00e1lida", - "timeout": "Tempo limite estabelecendo conex\u00e3o", - "unknown": "Erro inesperado" - }, "step": { "community": { "data": { @@ -27,15 +21,8 @@ }, "description": "Se voc\u00ea deseja monitorar n\u00e3o apenas o estado, escolha seu distrito espec\u00edfico" }, - "state": { - "data": { - "region": "Regi\u00e3o" - }, - "description": "Escolha o estado para monitorar" - }, "user": { "data": { - "api_key": "Chave de API", "region": "Regi\u00e3o" }, "description": "Escolha o estado para monitorar" diff --git a/homeassistant/components/ukraine_alarm/translations/ru.json b/homeassistant/components/ukraine_alarm/translations/ru.json index fdb9d4b3f23..8944129497a 100644 --- a/homeassistant/components/ukraine_alarm/translations/ru.json +++ b/homeassistant/components/ukraine_alarm/translations/ru.json @@ -8,12 +8,6 @@ "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, - "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", - "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." - }, "step": { "community": { "data": { @@ -27,15 +21,8 @@ }, "description": "\u0415\u0441\u043b\u0438 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0438\u0442\u044c \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0437\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u044c\u044e, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0435\u0451 \u0433\u0440\u043e\u043c\u0430\u0434\u0443" }, - "state": { - "data": { - "region": "\u041e\u0431\u043b\u0430\u0441\u0442\u044c" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u0434\u043b\u044f \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f" - }, "user": { "data": { - "api_key": "\u041a\u043b\u044e\u0447 API", "region": "\u041e\u0431\u043b\u0430\u0441\u0442\u044c" }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u0434\u043b\u044f \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f" diff --git a/homeassistant/components/ukraine_alarm/translations/tr.json b/homeassistant/components/ukraine_alarm/translations/tr.json index 3662a32b1fa..2b36f5b73f0 100644 --- a/homeassistant/components/ukraine_alarm/translations/tr.json +++ b/homeassistant/components/ukraine_alarm/translations/tr.json @@ -8,12 +8,6 @@ "timeout": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131", "unknown": "Beklenmeyen hata" }, - "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131", - "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", - "timeout": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131", - "unknown": "Beklenmeyen hata" - }, "step": { "community": { "data": { @@ -27,15 +21,8 @@ }, "description": "Sadece eyaleti izlemek istemiyorsan\u0131z, belirli bir b\u00f6lgeyi se\u00e7in" }, - "state": { - "data": { - "region": "B\u00f6lge" - }, - "description": "\u0130zlenecek durumu se\u00e7in" - }, "user": { "data": { - "api_key": "API Anahtar\u0131", "region": "B\u00f6lge" }, "description": "\u0130zlenecek durumu se\u00e7in" diff --git a/homeassistant/components/ukraine_alarm/translations/uk.json b/homeassistant/components/ukraine_alarm/translations/uk.json index 2d5e881e41b..930f9ab31de 100644 --- a/homeassistant/components/ukraine_alarm/translations/uk.json +++ b/homeassistant/components/ukraine_alarm/translations/uk.json @@ -8,12 +8,6 @@ "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0437\u2019\u0454\u0434\u043d\u0430\u043d\u043d\u044f", "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, - "error": { - "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", - "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API", - "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0437\u2019\u0454\u0434\u043d\u0430\u043d\u043d\u044f", - "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" - }, "step": { "community": { "data": { @@ -27,15 +21,8 @@ }, "description": "\u042f\u043a\u0449\u043e \u0432\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0432\u0430\u0442\u0438 \u043d\u0435 \u0442\u0456\u043b\u044c\u043a\u0438 \u043e\u0431\u043b\u0430\u0441\u0442\u044c, \u043e\u0431\u0435\u0440\u0456\u0442\u044c \u0457\u0457 \u0433\u0440\u043e\u043c\u0430\u0434\u0443" }, - "state": { - "data": { - "region": "\u041e\u0431\u043b\u0430\u0441\u0442\u044c" - }, - "description": "\u041e\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u0434\u043b\u044f \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f" - }, "user": { "data": { - "api_key": "\u041a\u043b\u044e\u0447 API", "region": "\u041e\u0431\u043b\u0430\u0441\u0442\u044c" }, "description": "\u041e\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u0434\u043b\u044f \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f" diff --git a/homeassistant/components/ukraine_alarm/translations/zh-Hant.json b/homeassistant/components/ukraine_alarm/translations/zh-Hant.json index 33e75d23df0..73b5ff2cce3 100644 --- a/homeassistant/components/ukraine_alarm/translations/zh-Hant.json +++ b/homeassistant/components/ukraine_alarm/translations/zh-Hant.json @@ -8,12 +8,6 @@ "timeout": "\u5efa\u7acb\u9023\u7dda\u903e\u6642", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_api_key": "API \u91d1\u9470\u7121\u6548", - "timeout": "\u5efa\u7acb\u9023\u7dda\u903e\u6642", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" - }, "step": { "community": { "data": { @@ -27,15 +21,8 @@ }, "description": "\u5047\u5982\u4f60\u4e0d\u53ea\u8981\u76e3\u770b\u72c0\u614b\u3001\u8acb\u9078\u64c7\u7279\u5b9a\u5340\u57df" }, - "state": { - "data": { - "region": "\u5340\u57df" - }, - "description": "\u9078\u64c7\u6240\u8981\u76e3\u8996\u7684\u72c0\u614b" - }, "user": { "data": { - "api_key": "API \u91d1\u9470", "region": "\u5340\u57df" }, "description": "\u9078\u64c7\u6240\u8981\u76e3\u8996\u7684\u72c0\u614b" diff --git a/homeassistant/components/unifi/translations/ko.json b/homeassistant/components/unifi/translations/ko.json index 3a13b420097..30d7559e4f3 100644 --- a/homeassistant/components/unifi/translations/ko.json +++ b/homeassistant/components/unifi/translations/ko.json @@ -26,6 +26,9 @@ } }, "options": { + "abort": { + "integration_not_setup": "UniFi \ud1b5\ud569\uad6c\uc131\uc694\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4." + }, "step": { "client_control": { "data": { diff --git a/homeassistant/components/unifiprotect/translations/bg.json b/homeassistant/components/unifiprotect/translations/bg.json index adc8d7ef699..a3987858b93 100644 --- a/homeassistant/components/unifiprotect/translations/bg.json +++ b/homeassistant/components/unifiprotect/translations/bg.json @@ -5,8 +5,7 @@ }, "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" + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "flow_title": "{name} ({ip_address})", "step": { diff --git a/homeassistant/components/unifiprotect/translations/ca.json b/homeassistant/components/unifiprotect/translations/ca.json index 2227011d113..a3d873ead5b 100644 --- a/homeassistant/components/unifiprotect/translations/ca.json +++ b/homeassistant/components/unifiprotect/translations/ca.json @@ -7,16 +7,14 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "protect_version": "La versi\u00f3 m\u00ednima necess\u00e0ria \u00e9s la v1.20.0. Actualitza UniFi Protect i torna-ho a provar.", - "unknown": "Error inesperat" + "protect_version": "La versi\u00f3 m\u00ednima necess\u00e0ria \u00e9s la v1.20.0. Actualitza UniFi Protect i torna-ho a provar." }, "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { "password": "Contrasenya", - "username": "Nom d'usuari", - "verify_ssl": "Verifica el certificat SSL" + "username": "Nom d'usuari" }, "description": "Vols configurar {name} ({ip_address})? Necessites un usuari local per iniciar sessi\u00f3, creat mitjan\u00e7ant la consola d'UniFi OS. Els usuaris d'Ubiquiti Cloud no funcionen. Per a m\u00e9s informaci\u00f3: {local_user_documentation_url}", "title": "UniFi Protect descobert" diff --git a/homeassistant/components/unifiprotect/translations/cs.json b/homeassistant/components/unifiprotect/translations/cs.json index 3668652797e..855cc6da6da 100644 --- a/homeassistant/components/unifiprotect/translations/cs.json +++ b/homeassistant/components/unifiprotect/translations/cs.json @@ -5,16 +5,14 @@ }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", - "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { "password": "Heslo", - "username": "U\u017eivatelsk\u00e9 jm\u00e9no", - "verify_ssl": "Ov\u011b\u0159it certifik\u00e1t SSL" + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" } }, "reauth_confirm": { diff --git a/homeassistant/components/unifiprotect/translations/de.json b/homeassistant/components/unifiprotect/translations/de.json index 668a8c7b749..0f6ffd77793 100644 --- a/homeassistant/components/unifiprotect/translations/de.json +++ b/homeassistant/components/unifiprotect/translations/de.json @@ -7,16 +7,14 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", - "protect_version": "Die erforderliche Mindestversion ist v1.20.0. Bitte aktualisiere UniFi Protect und versuche es dann erneut.", - "unknown": "Unerwarteter Fehler" + "protect_version": "Die erforderliche Mindestversion ist v1.20.0. Bitte aktualisiere UniFi Protect und versuche es dann erneut." }, "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { "password": "Passwort", - "username": "Benutzername", - "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + "username": "Benutzername" }, "description": "M\u00f6chtest du {name} ({ip_address}) einrichten? Du ben\u00f6tigst einen lokalen Benutzer, den du in deiner UniFi OS-Konsole angelegt hast, um sich damit anzumelden. Ubiquiti Cloud-Benutzer funktionieren nicht. F\u00fcr weitere Informationen: {local_user_documentation_url}", "title": "UniFi Protect erkannt" diff --git a/homeassistant/components/unifiprotect/translations/el.json b/homeassistant/components/unifiprotect/translations/el.json index c2f5d57d36d..8cede72d32b 100644 --- a/homeassistant/components/unifiprotect/translations/el.json +++ b/homeassistant/components/unifiprotect/translations/el.json @@ -7,16 +7,14 @@ "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", - "protect_version": "\u0397 \u03b5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03b7 \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 v1.20.0. \u0391\u03bd\u03b1\u03b2\u03b1\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf UniFi Protect \u03ba\u03b1\u03b9 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + "protect_version": "\u0397 \u03b5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03b7 \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 v1.20.0. \u0391\u03bd\u03b1\u03b2\u03b1\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf UniFi Protect \u03ba\u03b1\u03b9 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac." }, "flow_title": "{name} ( {ip_address} )", "step": { "discovery_confirm": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", - "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ({ip_address}); \u0398\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03c4\u03bf\u03c0\u03b9\u03ba\u03cc \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c0\u03bf\u03c5 \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u039a\u03bf\u03bd\u03c3\u03cc\u03bb\u03b1 UniFi OS \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5. \u039f\u03b9 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2 \u03c4\u03bf\u03c5 Ubiquiti Cloud \u03b4\u03b5\u03bd \u03b8\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03bf\u03c5\u03bd. \u0393\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2: {local_user_documentation_url}", "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf UniFi Protect" diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json index 45e8fb8ea2e..b9d787b382e 100644 --- a/homeassistant/components/unifiprotect/translations/en.json +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -7,16 +7,14 @@ "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", - "protect_version": "Minimum required version is v1.20.0. Please upgrade UniFi Protect and then retry.", - "unknown": "Unexpected error" + "protect_version": "Minimum required version is v1.20.0. Please upgrade UniFi Protect and then retry." }, "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { "password": "Password", - "username": "Username", - "verify_ssl": "Verify SSL certificate" + "username": "Username" }, "description": "Do you want to setup {name} ({ip_address})? You will need a local user created in your UniFi OS Console to log in with. Ubiquiti Cloud Users will not work. For more information: {local_user_documentation_url}", "title": "UniFi Protect Discovered" diff --git a/homeassistant/components/unifiprotect/translations/es.json b/homeassistant/components/unifiprotect/translations/es.json index 901f1707de5..9ca1b56cf01 100644 --- a/homeassistant/components/unifiprotect/translations/es.json +++ b/homeassistant/components/unifiprotect/translations/es.json @@ -7,14 +7,12 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "protect_version": "La versi\u00f3n m\u00ednima requerida es v1.20.0. Actualice UniFi Protect y vuelva a intentarlo.", - "unknown": "Error inesperado" + "protect_version": "La versi\u00f3n m\u00ednima requerida es v1.20.0. Actualice UniFi Protect y vuelva a intentarlo." }, "step": { "discovery_confirm": { "data": { - "username": "Usuario", - "verify_ssl": "Verifica el certificado SSL" + "username": "Usuario" } }, "reauth_confirm": { diff --git a/homeassistant/components/unifiprotect/translations/et.json b/homeassistant/components/unifiprotect/translations/et.json index 607edbe52bc..9abd63559ee 100644 --- a/homeassistant/components/unifiprotect/translations/et.json +++ b/homeassistant/components/unifiprotect/translations/et.json @@ -7,16 +7,14 @@ "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus", - "protect_version": "Minimaalne n\u00f5utav versioon on v1.20.0. Uuenda UniFi Protecti ja proovi seej\u00e4rel uuesti.", - "unknown": "Ootamatu t\u00f5rge" + "protect_version": "Minimaalne n\u00f5utav versioon on v1.20.0. Uuenda UniFi Protecti ja proovi seej\u00e4rel uuesti." }, "flow_title": "{name} ( {ip_address} )", "step": { "discovery_confirm": { "data": { "password": "Salas\u00f5na", - "username": "Kasutajanimi", - "verify_ssl": "Kontrolli SSL \u00fchendust" + "username": "Kasutajanimi" }, "description": "Kas soovid seadistada kasutaja {name} ( {ip_address} )? Sisselogimiseks on vaja UniFi OS-i konsoolis loodud kohalikku kasutajat. Ubiquiti pilve kasutajad ei t\u00f6\u00f6ta. Lisateabe saamiseks: {local_user_documentation_url}", "title": "Avastati UniFi Protect" diff --git a/homeassistant/components/unifiprotect/translations/fr.json b/homeassistant/components/unifiprotect/translations/fr.json index 592a18427b7..e1f189a55d5 100644 --- a/homeassistant/components/unifiprotect/translations/fr.json +++ b/homeassistant/components/unifiprotect/translations/fr.json @@ -7,16 +7,14 @@ "error": { "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification non valide", - "protect_version": "La version minimale requise est la v1.20.0. Veuillez mettre \u00e0 jour UniFi Protect, puis r\u00e9essayer.", - "unknown": "Erreur inattendue" + "protect_version": "La version minimale requise est la v1.20.0. Veuillez mettre \u00e0 jour UniFi Protect, puis r\u00e9essayer." }, "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { "password": "Mot de passe", - "username": "Nom d'utilisateur", - "verify_ssl": "V\u00e9rifier le certificat SSL" + "username": "Nom d'utilisateur" }, "description": "Voulez-vous configurer {name} ({ip_address})\u00a0? Vous aurez besoin d'un utilisateur local cr\u00e9\u00e9 dans votre console UniFi OS pour vous connecter. Les utilisateurs Ubiquiti Cloud ne fonctionneront pas. Pour plus d'informations\u00a0: {local_user_documentation_url}", "title": "UniFi Protect d\u00e9couvert" diff --git a/homeassistant/components/unifiprotect/translations/he.json b/homeassistant/components/unifiprotect/translations/he.json index 7940beb3986..63974aff3ec 100644 --- a/homeassistant/components/unifiprotect/translations/he.json +++ b/homeassistant/components/unifiprotect/translations/he.json @@ -5,16 +5,14 @@ }, "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" + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", - "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } }, "reauth_confirm": { diff --git a/homeassistant/components/unifiprotect/translations/hu.json b/homeassistant/components/unifiprotect/translations/hu.json index c3cd3560a9d..11e0151f165 100644 --- a/homeassistant/components/unifiprotect/translations/hu.json +++ b/homeassistant/components/unifiprotect/translations/hu.json @@ -7,16 +7,14 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "protect_version": "A minim\u00e1lisan sz\u00fcks\u00e9ges verzi\u00f3 a v1.20.0. Friss\u00edtse az UniFi Protect-et, majd pr\u00f3b\u00e1lkozzon \u00fajra.", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "protect_version": "A minim\u00e1lisan sz\u00fcks\u00e9ges verzi\u00f3 a v1.20.0. Friss\u00edtse az UniFi Protect-et, majd pr\u00f3b\u00e1lkozzon \u00fajra." }, "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { "password": "Jelsz\u00f3", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v", - "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({ipaddress})? Egy helyi felhaszn\u00e1l\u00f3t kell l\u00e9trehoznia Unifi OS-ben.", "title": "UniFi Protect felfedezve" diff --git a/homeassistant/components/unifiprotect/translations/id.json b/homeassistant/components/unifiprotect/translations/id.json index b2f8e2541fe..a0a3b9751d3 100644 --- a/homeassistant/components/unifiprotect/translations/id.json +++ b/homeassistant/components/unifiprotect/translations/id.json @@ -7,16 +7,14 @@ "error": { "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", - "protect_version": "Versi minimum yang diperlukan adalah v1.20.0. Tingkatkan UniFi Protect lalu coba lagi.", - "unknown": "Kesalahan yang tidak diharapkan" + "protect_version": "Versi minimum yang diperlukan adalah v1.20.0. Tingkatkan UniFi Protect lalu coba lagi." }, "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { "password": "Kata Sandi", - "username": "Nama Pengguna", - "verify_ssl": "Verifikasi sertifikat SSL" + "username": "Nama Pengguna" }, "description": "Ingin menyiapkan {name} ({ip_address})? Anda akan memerlukan pengguna lokal yang dibuat di Konsol OS UniFi Anda untuk masuk. Pengguna Ubiquiti Cloud tidak akan berfungsi. Untuk informasi lebih lanjut: {local_user_documentation_url}", "title": "UniFi Protect Ditemukan" diff --git a/homeassistant/components/unifiprotect/translations/it.json b/homeassistant/components/unifiprotect/translations/it.json index 747647b3d26..1d0fff95c3a 100644 --- a/homeassistant/components/unifiprotect/translations/it.json +++ b/homeassistant/components/unifiprotect/translations/it.json @@ -7,16 +7,14 @@ "error": { "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", - "protect_version": "La versione minima richiesta \u00e8 v1.20.0. Aggiorna UniFi Protect e riprova.", - "unknown": "Errore imprevisto" + "protect_version": "La versione minima richiesta \u00e8 v1.20.0. Aggiorna UniFi Protect e riprova." }, "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { "password": "Password", - "username": "Nome utente", - "verify_ssl": "Verifica il certificato SSL" + "username": "Nome utente" }, "description": "Vuoi configurare {name} ({ip_address})? Avrai bisogno di un utente locale creato nella tua console UniFi OS con cui accedere. Gli utenti Ubiquiti Cloud non funzioneranno. Per ulteriori informazioni: {local_user_documentation_url}", "title": "Rilevato UniFi Protect" diff --git a/homeassistant/components/unifiprotect/translations/ja.json b/homeassistant/components/unifiprotect/translations/ja.json index e4ad1b3f231..12275699260 100644 --- a/homeassistant/components/unifiprotect/translations/ja.json +++ b/homeassistant/components/unifiprotect/translations/ja.json @@ -7,16 +7,14 @@ "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "protect_version": "\u6700\u4f4e\u9650\u5fc5\u8981\u306a\u30d0\u30fc\u30b8\u30e7\u30f3\u306fv1.20.0\u3067\u3059\u3002UniFi Protect\u3092\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3057\u3066\u304b\u3089\u518d\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + "protect_version": "\u6700\u4f4e\u9650\u5fc5\u8981\u306a\u30d0\u30fc\u30b8\u30e7\u30f3\u306fv1.20.0\u3067\u3059\u3002UniFi Protect\u3092\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3057\u3066\u304b\u3089\u518d\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "username": "\u30e6\u30fc\u30b6\u30fc\u540d", - "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "{name} ({ip_address}) \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f", "title": "UniFi Protect\u304c\u691c\u51fa\u3055\u308c\u307e\u3057\u305f" diff --git a/homeassistant/components/unifiprotect/translations/ko.json b/homeassistant/components/unifiprotect/translations/ko.json new file mode 100644 index 00000000000..36be85fd61a --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/ko.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "discovery_confirm": { + "description": "{name} ( {ip_address} )\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? " + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/nl.json b/homeassistant/components/unifiprotect/translations/nl.json index 090624f2009..a5b834fc4d4 100644 --- a/homeassistant/components/unifiprotect/translations/nl.json +++ b/homeassistant/components/unifiprotect/translations/nl.json @@ -7,16 +7,14 @@ "error": { "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", - "protect_version": "Minimaal vereiste versie is v1.20.0. Upgrade UniFi Protect en probeer het opnieuw.", - "unknown": "Onverwachte fout" + "protect_version": "Minimaal vereiste versie is v1.20.0. Upgrade UniFi Protect en probeer het opnieuw." }, "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { "password": "Wachtwoord", - "username": "Gebruikersnaam", - "verify_ssl": "SSL-certificaat verifi\u00ebren" + "username": "Gebruikersnaam" }, "description": "Wilt u {name} ({ip_address}) instellen? U heeft een lokale gebruiker nodig die is aangemaakt in uw UniFi OS Console om mee in te loggen. Ubiquiti Cloud gebruikers zullen niet werken. Voor meer informatie: {local_user_documentation_url}", "title": "UniFi Protect ontdekt" diff --git a/homeassistant/components/unifiprotect/translations/no.json b/homeassistant/components/unifiprotect/translations/no.json index 9b45080b8f1..e11ed432313 100644 --- a/homeassistant/components/unifiprotect/translations/no.json +++ b/homeassistant/components/unifiprotect/translations/no.json @@ -7,16 +7,14 @@ "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", - "protect_version": "Minimum p\u00e5krevd versjon er v1.20.0. Vennligst oppgrader UniFi Protect og pr\u00f8v deretter p\u00e5 nytt.", - "unknown": "Uventet feil" + "protect_version": "Minimum p\u00e5krevd versjon er v1.20.0. Vennligst oppgrader UniFi Protect og pr\u00f8v deretter p\u00e5 nytt." }, "flow_title": "{name} ( {ip_address} )", "step": { "discovery_confirm": { "data": { "password": "Passord", - "username": "Brukernavn", - "verify_ssl": "Verifisere SSL-sertifikat" + "username": "Brukernavn" }, "description": "Vil du konfigurere {name} ( {ip_address} )? Du trenger en lokal bruker opprettet i UniFi OS-konsollen for \u00e5 logge p\u00e5. Ubiquiti Cloud-brukere vil ikke fungere. For mer informasjon: {local_user_documentation_url}", "title": "UniFi Protect oppdaget" diff --git a/homeassistant/components/unifiprotect/translations/pl.json b/homeassistant/components/unifiprotect/translations/pl.json index ef879805546..82aa3c91ee3 100644 --- a/homeassistant/components/unifiprotect/translations/pl.json +++ b/homeassistant/components/unifiprotect/translations/pl.json @@ -7,16 +7,14 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", - "protect_version": "Minimalna wymagana wersja to v1.20.0. Zaktualizuj UniFi Protect, a nast\u0119pnie spr\u00f3buj ponownie.", - "unknown": "Nieoczekiwany b\u0142\u0105d" + "protect_version": "Minimalna wymagana wersja to v1.20.0. Zaktualizuj UniFi Protect, a nast\u0119pnie spr\u00f3buj ponownie." }, "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika", - "verify_ssl": "Weryfikacja certyfikatu SSL" + "username": "Nazwa u\u017cytkownika" }, "description": "Czy chcesz skonfigurowa\u0107 {name} ({ip_address})? Aby si\u0119 zalogowa\u0107, b\u0119dziesz potrzebowa\u0107 lokalnego u\u017cytkownika utworzonego w konsoli UniFi OS. U\u017cytkownicy Ubiquiti Cloud nie b\u0119d\u0105 dzia\u0142a\u0107. Wi\u0119cej informacji: {local_user_documentation_url}", "title": "Wykryto UniFi Protect" diff --git a/homeassistant/components/unifiprotect/translations/pt-BR.json b/homeassistant/components/unifiprotect/translations/pt-BR.json index 1f26b952998..2c0c1270add 100644 --- a/homeassistant/components/unifiprotect/translations/pt-BR.json +++ b/homeassistant/components/unifiprotect/translations/pt-BR.json @@ -7,16 +7,14 @@ "error": { "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "protect_version": "A vers\u00e3o m\u00ednima exigida \u00e9 v1.20.0. Atualize o UniFi Protect e tente novamente.", - "unknown": "Erro inesperado" + "protect_version": "A vers\u00e3o m\u00ednima exigida \u00e9 v1.20.0. Atualize o UniFi Protect e tente novamente." }, "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { "password": "Senha", - "username": "Usu\u00e1rio", - "verify_ssl": "Verifique o certificado SSL" + "username": "Usu\u00e1rio" }, "description": "Deseja configurar {name} ({ip_address})?\nVoc\u00ea precisar\u00e1 de um usu\u00e1rio local criado no console do sistema operacional UniFi para fazer login. Usu\u00e1rios da Ubiquiti Cloud n\u00e3o funcionar\u00e3o. Para mais informa\u00e7\u00f5es: {local_user_documentation_url}", "title": "Descoberta UniFi Protect" diff --git a/homeassistant/components/unifiprotect/translations/ru.json b/homeassistant/components/unifiprotect/translations/ru.json index ea9810962f8..2e06e887cf4 100644 --- a/homeassistant/components/unifiprotect/translations/ru.json +++ b/homeassistant/components/unifiprotect/translations/ru.json @@ -7,16 +7,14 @@ "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.", - "protect_version": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0432\u0435\u0440\u0441\u0438\u044f 1.20.0 \u0438\u043b\u0438 \u0432\u044b\u0448\u0435. \u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 UniFi Protect \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "protect_version": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0432\u0435\u0440\u0441\u0438\u044f 1.20.0 \u0438\u043b\u0438 \u0432\u044b\u0448\u0435. \u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 UniFi Protect \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443." }, "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", - "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \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" }, "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({ip_address})? \u0414\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c, \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0439 \u0432 \u043a\u043e\u043d\u0441\u043e\u043b\u0438 UniFi OS \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0430 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443. \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438 Ubiquiti Cloud \u043d\u0435 \u0431\u0443\u0434\u0443\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438: {local_user_documentation_url}", "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e UniFi Protect" diff --git a/homeassistant/components/unifiprotect/translations/tr.json b/homeassistant/components/unifiprotect/translations/tr.json index d7b87b13f31..869c13608ce 100644 --- a/homeassistant/components/unifiprotect/translations/tr.json +++ b/homeassistant/components/unifiprotect/translations/tr.json @@ -7,16 +7,14 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "protect_version": "Minimum gerekli s\u00fcr\u00fcm v1.20.0'd\u0131r. L\u00fctfen UniFi Protect'i y\u00fckseltin ve ard\u0131ndan yeniden deneyin.", - "unknown": "Beklenmeyen hata" + "protect_version": "Minimum gerekli s\u00fcr\u00fcm v1.20.0'd\u0131r. L\u00fctfen UniFi Protect'i y\u00fckseltin ve ard\u0131ndan yeniden deneyin." }, "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { "password": "Parola", - "username": "Kullan\u0131c\u0131 Ad\u0131", - "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" + "username": "Kullan\u0131c\u0131 Ad\u0131" }, "description": "{name} ( {ip_address} ) kurulumu yapmak istiyor musunuz? Oturum a\u00e7mak i\u00e7in UniFi OS Konsolunuzda olu\u015fturulmu\u015f yerel bir kullan\u0131c\u0131ya ihtiyac\u0131n\u0131z olacak. Ubiquiti Bulut Kullan\u0131c\u0131lar\u0131 \u00e7al\u0131\u015fmayacakt\u0131r. Daha fazla bilgi i\u00e7in: {local_user_documentation_url}", "title": "UniFi Protect Ke\u015ffedildi" diff --git a/homeassistant/components/unifiprotect/translations/zh-Hant.json b/homeassistant/components/unifiprotect/translations/zh-Hant.json index 33a447e9f4c..d0c23849e12 100644 --- a/homeassistant/components/unifiprotect/translations/zh-Hant.json +++ b/homeassistant/components/unifiprotect/translations/zh-Hant.json @@ -7,16 +7,14 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "protect_version": "\u6240\u9700\u6700\u4f4e\u7248\u672c\u70ba V1.20.0\u3002\u8acb\u66f4\u65b0 UniFi \u76e3\u63a7\u5f8c\u518d\u91cd\u8a66\u4e00\u6b21\u3002", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "protect_version": "\u6240\u9700\u6700\u4f4e\u7248\u672c\u70ba V1.20.0\u3002\u8acb\u66f4\u65b0 UniFi \u76e3\u63a7\u5f8c\u518d\u91cd\u8a66\u4e00\u6b21\u3002" }, "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { "password": "\u5bc6\u78bc", - "username": "\u4f7f\u7528\u8005\u540d\u7a31", - "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({ip_address})\uff1f\u9700\u8981\u65bc UniFi OS Console \u65b0\u589e\u672c\u5730\u4f7f\u7528\u8005\u4ee5\u9032\u884c\u767b\u5165\u3001Ubiquiti \u96f2\u7aef\u4f7f\u7528\u8005\u7121\u6cd5\u4f7f\u7528\u3002\u66f4\u591a\u8a73\u7d30\u8cc7\u8a0a\uff1a{local_user_documentation_url}", "title": "\u767c\u73fe UniFi \u76e3\u63a7" diff --git a/homeassistant/components/upnp/translations/bg.json b/homeassistant/components/upnp/translations/bg.json index 82632bc19b5..8bc4e9b740a 100644 --- a/homeassistant/components/upnp/translations/bg.json +++ b/homeassistant/components/upnp/translations/bg.json @@ -15,8 +15,7 @@ }, "user": { "data": { - "unique_id": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", - "usn": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + "unique_id": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" } } } diff --git a/homeassistant/components/upnp/translations/ca.json b/homeassistant/components/upnp/translations/ca.json index 0fc68f8a9b7..94ffd69cbf4 100644 --- a/homeassistant/components/upnp/translations/ca.json +++ b/homeassistant/components/upnp/translations/ca.json @@ -16,9 +16,7 @@ }, "user": { "data": { - "scan_interval": "Interval d'actualitzaci\u00f3 (en segons, m\u00ednim 30)", - "unique_id": "Dispositiu", - "usn": "Dispositiu" + "unique_id": "Dispositiu" } } } diff --git a/homeassistant/components/upnp/translations/cs.json b/homeassistant/components/upnp/translations/cs.json index 319dd28c11e..c684856b52b 100644 --- a/homeassistant/components/upnp/translations/cs.json +++ b/homeassistant/components/upnp/translations/cs.json @@ -9,12 +9,6 @@ "step": { "ssdp_confirm": { "description": "Chcete nastavit toto za\u0159\u00edzen\u00ed UPnP/IGD?" - }, - "user": { - "data": { - "scan_interval": "Interval aktualizace (v sekund\u00e1ch, minim\u00e1ln\u011b 30)", - "usn": "Za\u0159\u00edzen\u00ed" - } } } } diff --git a/homeassistant/components/upnp/translations/de.json b/homeassistant/components/upnp/translations/de.json index b63d17947ae..6c99c42fd8e 100644 --- a/homeassistant/components/upnp/translations/de.json +++ b/homeassistant/components/upnp/translations/de.json @@ -16,9 +16,7 @@ }, "user": { "data": { - "scan_interval": "Aktualisierungsintervall (Sekunden, mindestens 30)", - "unique_id": "Ger\u00e4t", - "usn": "Ger\u00e4t" + "unique_id": "Ger\u00e4t" } } } diff --git a/homeassistant/components/upnp/translations/el.json b/homeassistant/components/upnp/translations/el.json index 9a84ba81ced..8e98495d4b7 100644 --- a/homeassistant/components/upnp/translations/el.json +++ b/homeassistant/components/upnp/translations/el.json @@ -12,9 +12,7 @@ }, "user": { "data": { - "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1, \u03b5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf 30)", - "unique_id": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", - "usn": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + "unique_id": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" } } } diff --git a/homeassistant/components/upnp/translations/en.json b/homeassistant/components/upnp/translations/en.json index c62ebfd2a87..aa22348e308 100644 --- a/homeassistant/components/upnp/translations/en.json +++ b/homeassistant/components/upnp/translations/en.json @@ -12,9 +12,7 @@ }, "user": { "data": { - "scan_interval": "Update interval (seconds, minimal 30)", - "unique_id": "Device", - "usn": "Device" + "unique_id": "Device" } } } diff --git a/homeassistant/components/upnp/translations/es.json b/homeassistant/components/upnp/translations/es.json index 7a76ab44bf0..9a8a9e0160b 100644 --- a/homeassistant/components/upnp/translations/es.json +++ b/homeassistant/components/upnp/translations/es.json @@ -16,9 +16,7 @@ }, "user": { "data": { - "scan_interval": "Intervalo de actualizaci\u00f3n (segundos, m\u00ednimo 30)", - "unique_id": "Dispositivo", - "usn": "Dispositivo" + "unique_id": "Dispositivo" } } } diff --git a/homeassistant/components/upnp/translations/et.json b/homeassistant/components/upnp/translations/et.json index 2ac4884c9ea..f090cebb31a 100644 --- a/homeassistant/components/upnp/translations/et.json +++ b/homeassistant/components/upnp/translations/et.json @@ -20,9 +20,7 @@ }, "user": { "data": { - "scan_interval": "P\u00e4ringute intervall (sekundites, v\u00e4hemalt 30)", - "unique_id": "Seade", - "usn": "Seade" + "unique_id": "Seade" } } } diff --git a/homeassistant/components/upnp/translations/fi.json b/homeassistant/components/upnp/translations/fi.json index aaf44e6c730..479b3be83de 100644 --- a/homeassistant/components/upnp/translations/fi.json +++ b/homeassistant/components/upnp/translations/fi.json @@ -2,14 +2,6 @@ "config": { "abort": { "already_configured": "Laite on jo m\u00e4\u00e4ritetty" - }, - "step": { - "user": { - "data": { - "scan_interval": "P\u00e4ivitysv\u00e4li (sekuntia, v\u00e4hint\u00e4\u00e4n 30)", - "usn": "Laite" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/fr.json b/homeassistant/components/upnp/translations/fr.json index 1c5a1c61309..b1f7ffb03fd 100644 --- a/homeassistant/components/upnp/translations/fr.json +++ b/homeassistant/components/upnp/translations/fr.json @@ -20,9 +20,7 @@ }, "user": { "data": { - "scan_interval": "Intervalle de mise \u00e0 jour (secondes, minimum 30)", - "unique_id": "Appareil", - "usn": "Appareil" + "unique_id": "Appareil" } } } diff --git a/homeassistant/components/upnp/translations/he.json b/homeassistant/components/upnp/translations/he.json index 6395b5f029f..a8501a7de41 100644 --- a/homeassistant/components/upnp/translations/he.json +++ b/homeassistant/components/upnp/translations/he.json @@ -8,8 +8,7 @@ "step": { "user": { "data": { - "unique_id": "\u05d4\u05ea\u05e7\u05df", - "usn": "\u05de\u05db\u05e9\u05d9\u05e8" + "unique_id": "\u05d4\u05ea\u05e7\u05df" } } } diff --git a/homeassistant/components/upnp/translations/hu.json b/homeassistant/components/upnp/translations/hu.json index 46c6bd2de1f..141d5ae8d8d 100644 --- a/homeassistant/components/upnp/translations/hu.json +++ b/homeassistant/components/upnp/translations/hu.json @@ -20,9 +20,7 @@ }, "user": { "data": { - "scan_interval": "Friss\u00edt\u00e9si intervallum (m\u00e1sodperc, minimum 30)", - "unique_id": "Eszk\u00f6z", - "usn": "Eszk\u00f6z" + "unique_id": "Eszk\u00f6z" } } } diff --git a/homeassistant/components/upnp/translations/id.json b/homeassistant/components/upnp/translations/id.json index f70fca145e8..2f2c195759e 100644 --- a/homeassistant/components/upnp/translations/id.json +++ b/homeassistant/components/upnp/translations/id.json @@ -12,9 +12,7 @@ }, "user": { "data": { - "scan_interval": "Interval pembaruan (dalam detik, minimal 30)", - "unique_id": "Perangkat", - "usn": "Perangkat" + "unique_id": "Perangkat" } } } diff --git a/homeassistant/components/upnp/translations/it.json b/homeassistant/components/upnp/translations/it.json index a57429ac78a..7b6a22de5c8 100644 --- a/homeassistant/components/upnp/translations/it.json +++ b/homeassistant/components/upnp/translations/it.json @@ -20,9 +20,7 @@ }, "user": { "data": { - "scan_interval": "Intervallo di aggiornamento (secondi, minimo 30)", - "unique_id": "Dispositivo", - "usn": "Dispositivo" + "unique_id": "Dispositivo" } } } diff --git a/homeassistant/components/upnp/translations/ja.json b/homeassistant/components/upnp/translations/ja.json index 2148b0d8a16..8c4d8d4695e 100644 --- a/homeassistant/components/upnp/translations/ja.json +++ b/homeassistant/components/upnp/translations/ja.json @@ -12,9 +12,7 @@ }, "user": { "data": { - "scan_interval": "\u66f4\u65b0\u9593\u9694(\u79d2\u3001\u6700\u5c0f30)", - "unique_id": "\u30c7\u30d0\u30a4\u30b9", - "usn": "\u30c7\u30d0\u30a4\u30b9" + "unique_id": "\u30c7\u30d0\u30a4\u30b9" } } } diff --git a/homeassistant/components/upnp/translations/ko.json b/homeassistant/components/upnp/translations/ko.json index ab80ceb9caa..8782bc180dd 100644 --- a/homeassistant/components/upnp/translations/ko.json +++ b/homeassistant/components/upnp/translations/ko.json @@ -9,12 +9,6 @@ "step": { "ssdp_confirm": { "description": "\uc774 UPnP/IGD \uae30\uae30\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" - }, - "user": { - "data": { - "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \uac04\uaca9 (\ucd08, \ucd5c\uc19f\uac12 30)", - "usn": "\uae30\uae30" - } } } } diff --git a/homeassistant/components/upnp/translations/lb.json b/homeassistant/components/upnp/translations/lb.json index ac1eecebf99..a1f8cf85c3b 100644 --- a/homeassistant/components/upnp/translations/lb.json +++ b/homeassistant/components/upnp/translations/lb.json @@ -13,12 +13,6 @@ "step": { "ssdp_confirm": { "description": "Soll d\u00ebsen UPnP/IGD Apparat konfigur\u00e9iert ginn?" - }, - "user": { - "data": { - "scan_interval": "Update Intervall (Sekonnen, minimum 30)", - "usn": "Apparat" - } } } } diff --git a/homeassistant/components/upnp/translations/nl.json b/homeassistant/components/upnp/translations/nl.json index b4d690ca58c..5b176c4b22f 100644 --- a/homeassistant/components/upnp/translations/nl.json +++ b/homeassistant/components/upnp/translations/nl.json @@ -20,9 +20,7 @@ }, "user": { "data": { - "scan_interval": "Update-interval (seconden, minimaal 30)", - "unique_id": "Apparaat", - "usn": "Apparaat" + "unique_id": "Apparaat" } } } diff --git a/homeassistant/components/upnp/translations/no.json b/homeassistant/components/upnp/translations/no.json index c92144bf40d..8eb74395fa2 100644 --- a/homeassistant/components/upnp/translations/no.json +++ b/homeassistant/components/upnp/translations/no.json @@ -20,9 +20,7 @@ }, "user": { "data": { - "scan_interval": "Oppdateringsintervall (sekunder, minimum 30)", - "unique_id": "Enhet", - "usn": "Enhet" + "unique_id": "Enhet" } } } diff --git a/homeassistant/components/upnp/translations/pl.json b/homeassistant/components/upnp/translations/pl.json index 30213436d27..e8c8d72b74c 100644 --- a/homeassistant/components/upnp/translations/pl.json +++ b/homeassistant/components/upnp/translations/pl.json @@ -24,9 +24,7 @@ }, "user": { "data": { - "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (sekundy, minimum 30)", - "unique_id": "Urz\u0105dzenie", - "usn": "Urz\u0105dzenie" + "unique_id": "Urz\u0105dzenie" } } } diff --git a/homeassistant/components/upnp/translations/pt-BR.json b/homeassistant/components/upnp/translations/pt-BR.json index a1544981ea9..3070be8a1d0 100644 --- a/homeassistant/components/upnp/translations/pt-BR.json +++ b/homeassistant/components/upnp/translations/pt-BR.json @@ -12,9 +12,7 @@ }, "user": { "data": { - "scan_interval": "Intervalo de atualiza\u00e7\u00e3o (segundos, m\u00ednimo 30)", - "unique_id": "Dispositivo", - "usn": "Dispositivo" + "unique_id": "Dispositivo" } } } diff --git a/homeassistant/components/upnp/translations/pt.json b/homeassistant/components/upnp/translations/pt.json index 8985d608033..022d1c823c1 100644 --- a/homeassistant/components/upnp/translations/pt.json +++ b/homeassistant/components/upnp/translations/pt.json @@ -12,11 +12,6 @@ "step": { "ssdp_confirm": { "description": "Deseja configurar este dispositivo UPnP/IGD?" - }, - "user": { - "data": { - "usn": "Dispositivo" - } } } } diff --git a/homeassistant/components/upnp/translations/ru.json b/homeassistant/components/upnp/translations/ru.json index 20c652fa1e0..40a8585ac95 100644 --- a/homeassistant/components/upnp/translations/ru.json +++ b/homeassistant/components/upnp/translations/ru.json @@ -18,9 +18,7 @@ }, "user": { "data": { - "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445, \u043c\u0438\u043d\u0438\u043c\u0443\u043c 30)", - "unique_id": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", - "usn": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + "unique_id": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" } } } diff --git a/homeassistant/components/upnp/translations/sl.json b/homeassistant/components/upnp/translations/sl.json index ee25689fd51..c6258ec4774 100644 --- a/homeassistant/components/upnp/translations/sl.json +++ b/homeassistant/components/upnp/translations/sl.json @@ -14,11 +14,6 @@ "step": { "ssdp_confirm": { "description": "Ali \u017eelite nastaviti to UPnP/IGD napravo?" - }, - "user": { - "data": { - "usn": "Naprava" - } } } } diff --git a/homeassistant/components/upnp/translations/sv.json b/homeassistant/components/upnp/translations/sv.json index b6702d976d0..5ffe4a62f26 100644 --- a/homeassistant/components/upnp/translations/sv.json +++ b/homeassistant/components/upnp/translations/sv.json @@ -7,13 +7,6 @@ "error": { "one": "En", "other": "Andra" - }, - "step": { - "user": { - "data": { - "usn": "Enheten" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/tr.json b/homeassistant/components/upnp/translations/tr.json index 8176c0541c7..4742549eaff 100644 --- a/homeassistant/components/upnp/translations/tr.json +++ b/homeassistant/components/upnp/translations/tr.json @@ -20,9 +20,7 @@ }, "user": { "data": { - "scan_interval": "G\u00fcncelleme aral\u0131\u011f\u0131 (saniye, minimum 30)", - "unique_id": "Cihaz", - "usn": "Cihaz" + "unique_id": "Cihaz" } } } diff --git a/homeassistant/components/upnp/translations/uk.json b/homeassistant/components/upnp/translations/uk.json index 905958eeca9..870c3d38ffd 100644 --- a/homeassistant/components/upnp/translations/uk.json +++ b/homeassistant/components/upnp/translations/uk.json @@ -9,12 +9,6 @@ "step": { "ssdp_confirm": { "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0446\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 UPnP / IGD?" - }, - "user": { - "data": { - "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445, \u043c\u0456\u043d\u0456\u043c\u0443\u043c 30)", - "usn": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" - } } } } diff --git a/homeassistant/components/upnp/translations/zh-Hans.json b/homeassistant/components/upnp/translations/zh-Hans.json index 09cf281418b..73ee0d983c1 100644 --- a/homeassistant/components/upnp/translations/zh-Hans.json +++ b/homeassistant/components/upnp/translations/zh-Hans.json @@ -11,8 +11,7 @@ }, "user": { "data": { - "unique_id": "\u8bbe\u5907", - "usn": "\u8bbe\u5907" + "unique_id": "\u8bbe\u5907" } } } diff --git a/homeassistant/components/upnp/translations/zh-Hant.json b/homeassistant/components/upnp/translations/zh-Hant.json index 80c92662d22..df91540c0a0 100644 --- a/homeassistant/components/upnp/translations/zh-Hant.json +++ b/homeassistant/components/upnp/translations/zh-Hant.json @@ -12,9 +12,7 @@ }, "user": { "data": { - "scan_interval": "\u66f4\u65b0\u9593\u9694\uff08\u79d2\u3001\u6700\u5c11 30 \u79d2\uff09", - "unique_id": "\u88dd\u7f6e", - "usn": "\u88dd\u7f6e" + "unique_id": "\u88dd\u7f6e" } } } diff --git a/homeassistant/components/uptime/translations/ko.json b/homeassistant/components/uptime/translations/ko.json new file mode 100644 index 00000000000..758f3336cd4 --- /dev/null +++ b/homeassistant/components/uptime/translations/ko.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/ko.json b/homeassistant/components/utility_meter/translations/ko.json new file mode 100644 index 00000000000..00df29f1e0b --- /dev/null +++ b/homeassistant/components/utility_meter/translations/ko.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "\ubbf8\ud130\uae30 \ub9ac\uc14b \uc8fc\uae30", + "delta_values": "\ub378\ud0c0 \uac12", + "name": "\uc774\ub984", + "net_consumption": "\uc21c \uc18c\ube44\ub7c9", + "offset": "\ubbf8\ud130 \ub9ac\uc14b \uc624\ud504\uc14b", + "source": "\uc785\ub825 \uc13c\uc11c", + "tariffs": "\uc9c0\uc6d0\ub418\ub294 \uc694\uae08\uccb4\uacc4" + }, + "data_description": { + "delta_values": "\uc18c\uc2a4 \uac12\uc774 \uc808\ub300\uac12\uc774 \uc544\ub2cc \ub9c8\uc9c0\ub9c9 \uac12 \uc774\ud6c4\uc758 \ub378\ud0c0 \uac12\uc778 \uacbd\uc6b0 \ud65c\uc131\ud654\ud569\ub2c8\ub2e4.", + "net_consumption": "\uc18c\uc2a4\uac00 \ub124\ud2b8 \ubbf8\ud130\uc778 \uacbd\uc6b0 \ud65c\uc131\ud654\ud569\ub2c8\ub2e4. \uc989, \uc99d\uac00\ud558\uac70\ub098 \uac10\uc18c\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "offset": "\uc6d4\ubcc4 \ubbf8\ud130\uae30 \ub9ac\uc14b \ub0a0\uc9dc\ub97c \uc624\ud504\uc14b\ud569\ub2c8\ub2e4.", + "tariffs": "\uc9c0\uc6d0\ub418\ub294 \uc694\uae08\uccb4\uacc4, \ub2e8\uc77c \uc694\uae08\uccb4\uacc4\ub9cc \ud544\uc694\ud55c \uacbd\uc6b0 \ube44\uc6cc \ub461\ub2c8\ub2e4." + }, + "description": "\uae30\uac04 \ub3d9\uc548 \uc18c\ube44\ub41c \uc790\uc6d0\ub7c9(\uc608: \uc5d0\ub108\uc9c0, \uac00\uc2a4, \ubb3c, \ub09c\ubc29)\uc744 \ucd94\uc801\ud558\ub294 \uc13c\uc11c\ub97c \ub9cc\ub4ed\ub2c8\ub2e4. \uc720\ud2f8\ub9ac\ud2f0 \ubbf8\ud130 \uc13c\uc11c\ub294 \uc694\uae08\uccb4\uacc4\uc5d0 \ub530\ub978 \ubd84\ub958\ub97c \uc9c0\uc6d0\ud569\ub2c8\ub2e4. \uc774 \uacbd\uc6b0 \uac01 \uc694\uae08\uccb4\uacc4\uc5d0 \ub300\ud574 \uac01\uac01\uc758 \uc13c\uc11c\uac00 \uc0dd\uc131\ub418\uace0 \ud574\ub2f9 \uc694\uae08\uccb4\uacc4\ub97c \uc0ac\uc6a9\ud560 \uad6c\uc131\uc694\uc18c\ub97c \uc120\ud0dd\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "\uc720\ud2f8\ub9ac\ud2f0 \ubbf8\ud130 \ucd94\uac00" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "\uc785\ub825 \uc13c\uc11c" + } + } + } + }, + "title": "\uc720\ud2f8\ub9ac\ud2f0 \ubbf8\ud130" +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/af.json b/homeassistant/components/vicare/translations/af.json index 92321a6be86..74536dab793 100644 --- a/homeassistant/components/vicare/translations/af.json +++ b/homeassistant/components/vicare/translations/af.json @@ -4,7 +4,6 @@ "user": { "data": { "password": "\u5bc6\u7801", - "scan_interval": "\u626b\u63cf\u95f4\u9694\u65f6\u95f4(\u79d2)", "username": "\u7535\u5b50\u90ae\u7bb1" } } diff --git a/homeassistant/components/vicare/translations/bg.json b/homeassistant/components/vicare/translations/bg.json index c3178df6b87..242339c3815 100644 --- a/homeassistant/components/vicare/translations/bg.json +++ b/homeassistant/components/vicare/translations/bg.json @@ -12,12 +12,9 @@ "user": { "data": { "client_id": "API \u043a\u043b\u044e\u0447", - "name": "\u0418\u043c\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", - "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043d\u0430 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435 (\u0441\u0435\u043a\u0443\u043d\u0434\u0438)", "username": "Email" - }, - "title": "{name}" + } } } } diff --git a/homeassistant/components/vicare/translations/ca.json b/homeassistant/components/vicare/translations/ca.json index a850cdf27fa..0fef2889375 100644 --- a/homeassistant/components/vicare/translations/ca.json +++ b/homeassistant/components/vicare/translations/ca.json @@ -13,13 +13,10 @@ "data": { "client_id": "Clau API", "heating_type": "Tipus d'escalfador", - "name": "Nom", "password": "Contrasenya", - "scan_interval": "Interval d'escaneig (segons)", "username": "Correu electr\u00f2nic" }, - "description": "Configura la integraci\u00f3 ViCare. Per generar la clau API, v\u00e9s a https://developer.viessmann.com", - "title": "{name}" + "description": "Configura la integraci\u00f3 ViCare. Per generar la clau API, v\u00e9s a https://developer.viessmann.com" } } } diff --git a/homeassistant/components/vicare/translations/cs.json b/homeassistant/components/vicare/translations/cs.json index 5f29c738f07..332b6206db2 100644 --- a/homeassistant/components/vicare/translations/cs.json +++ b/homeassistant/components/vicare/translations/cs.json @@ -12,7 +12,6 @@ "user": { "data": { "client_id": "Kl\u00ed\u010d API", - "name": "Jm\u00e9no", "password": "Heslo", "username": "E-mail" } diff --git a/homeassistant/components/vicare/translations/de.json b/homeassistant/components/vicare/translations/de.json index 854c1cadead..1ef07bd2fc4 100644 --- a/homeassistant/components/vicare/translations/de.json +++ b/homeassistant/components/vicare/translations/de.json @@ -13,13 +13,10 @@ "data": { "client_id": "API-Schl\u00fcssel", "heating_type": "Art der Heizung", - "name": "Name", "password": "Passwort", - "scan_interval": "Scanintervall (Sekunden)", "username": "E-Mail" }, - "description": "Richte die ViCare-Integration ein. Um einen API-Schl\u00fcssel zu generieren, gehe auf https://developer.viessmann.com", - "title": "{name}" + "description": "Richte die ViCare-Integration ein. Um einen API-Schl\u00fcssel zu generieren, gehe auf https://developer.viessmann.com" } } } diff --git a/homeassistant/components/vicare/translations/el.json b/homeassistant/components/vicare/translations/el.json index 4c4385ec88b..61ac6bdbb8f 100644 --- a/homeassistant/components/vicare/translations/el.json +++ b/homeassistant/components/vicare/translations/el.json @@ -13,13 +13,10 @@ "data": { "client_id": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", "heating_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03b8\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7\u03c2", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)", "username": "Email" }, - "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 ViCare. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://developer.viessmann.com", - "title": "{name}" + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 ViCare. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://developer.viessmann.com" } } } diff --git a/homeassistant/components/vicare/translations/en.json b/homeassistant/components/vicare/translations/en.json index ef7235f86f8..89fc4caeeaf 100644 --- a/homeassistant/components/vicare/translations/en.json +++ b/homeassistant/components/vicare/translations/en.json @@ -13,13 +13,10 @@ "data": { "client_id": "API Key", "heating_type": "Heating type", - "name": "Name", "password": "Password", - "scan_interval": "Scan Interval (seconds)", "username": "Email" }, - "description": "Set up ViCare integration. To generate API key go to https://developer.viessmann.com", - "title": "{name}" + "description": "Set up ViCare integration. To generate API key go to https://developer.viessmann.com" } } } diff --git a/homeassistant/components/vicare/translations/es.json b/homeassistant/components/vicare/translations/es.json index a921723e893..6653fa3198e 100644 --- a/homeassistant/components/vicare/translations/es.json +++ b/homeassistant/components/vicare/translations/es.json @@ -13,13 +13,10 @@ "data": { "client_id": "Clave API", "heating_type": "Tipo de calefacci\u00f3n", - "name": "Nombre", "password": "Contrase\u00f1a", - "scan_interval": "Intervalo de exploraci\u00f3n (segundos)", "username": "Email" }, - "description": "Configure la integraci\u00f3n de ViCare. Para generar la clave API, vaya a https://developer.viessmann.com", - "title": "{name}" + "description": "Configure la integraci\u00f3n de ViCare. Para generar la clave API, vaya a https://developer.viessmann.com" } } } diff --git a/homeassistant/components/vicare/translations/et.json b/homeassistant/components/vicare/translations/et.json index 9214a527580..453b9b691bb 100644 --- a/homeassistant/components/vicare/translations/et.json +++ b/homeassistant/components/vicare/translations/et.json @@ -13,13 +13,10 @@ "data": { "client_id": "API v\u00f5ti", "heating_type": "K\u00fcttere\u017eiim", - "name": "Nimi", "password": "Salas\u00f5na", - "scan_interval": "P\u00e4ringute intervall (sekundites)", "username": "E-posti aadress" }, - "description": "Seadista ViCare sidumine. API-v\u00f5tme loomiseks mine aadressile https://developer.viessmann.com", - "title": "{name}" + "description": "Seadista ViCare sidumine. API-v\u00f5tme loomiseks mine aadressile https://developer.viessmann.com" } } } diff --git a/homeassistant/components/vicare/translations/fr.json b/homeassistant/components/vicare/translations/fr.json index b3dce5ec826..bdf85ff8007 100644 --- a/homeassistant/components/vicare/translations/fr.json +++ b/homeassistant/components/vicare/translations/fr.json @@ -13,13 +13,10 @@ "data": { "client_id": "Cl\u00e9 d'API", "heating_type": "Type de chauffage", - "name": "Nom", "password": "Mot de passe", - "scan_interval": "Intervalle de balayage (secondes)", "username": "Courriel" }, - "description": "Configurer l'int\u00e9gration ViCare. Pour g\u00e9n\u00e9rer une cl\u00e9 d'API, rendez-vous sur https://developer.viessmann.com", - "title": "{name}" + "description": "Configurer l'int\u00e9gration ViCare. Pour g\u00e9n\u00e9rer une cl\u00e9 d'API, rendez-vous sur https://developer.viessmann.com" } } } diff --git a/homeassistant/components/vicare/translations/he.json b/homeassistant/components/vicare/translations/he.json index db5a392f540..c64540395cd 100644 --- a/homeassistant/components/vicare/translations/he.json +++ b/homeassistant/components/vicare/translations/he.json @@ -12,11 +12,9 @@ "user": { "data": { "client_id": "\u05de\u05e4\u05ea\u05d7 API", - "name": "\u05e9\u05dd", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05d3\u05d5\u05d0\"\u05dc" - }, - "title": "{name}" + } } } } diff --git a/homeassistant/components/vicare/translations/hu.json b/homeassistant/components/vicare/translations/hu.json index 0e69a395a90..cc94705aad8 100644 --- a/homeassistant/components/vicare/translations/hu.json +++ b/homeassistant/components/vicare/translations/hu.json @@ -13,13 +13,10 @@ "data": { "client_id": "API kulcs", "heating_type": "F\u0171t\u00e9s t\u00edpusa", - "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3", - "scan_interval": "Beolvas\u00e1si id\u0151k\u00f6z (m\u00e1sodperc)", "username": "E-mail" }, - "description": "A ViCare integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sa. Az API-kulcs gener\u00e1l\u00e1s\u00e1hoz keresse fel a https://developer.viessmann.com webhelyet.", - "title": "{name}" + "description": "A ViCare integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sa. Az API-kulcs gener\u00e1l\u00e1s\u00e1hoz keresse fel a https://developer.viessmann.com webhelyet." } } } diff --git a/homeassistant/components/vicare/translations/id.json b/homeassistant/components/vicare/translations/id.json index 7c69ec28d85..32d0813b672 100644 --- a/homeassistant/components/vicare/translations/id.json +++ b/homeassistant/components/vicare/translations/id.json @@ -13,13 +13,10 @@ "data": { "client_id": "Kunci API", "heating_type": "Jenis pemanasan", - "name": "Nama", "password": "Kata Sandi", - "scan_interval": "Interval Pindai (detik)", "username": "Email" }, - "description": "Siapkan integrasi ViCare. Untuk menghasilkan kunci API, buka https://developer.viessmann.com", - "title": "{name}" + "description": "Siapkan integrasi ViCare. Untuk menghasilkan kunci API, buka https://developer.viessmann.com" } } } diff --git a/homeassistant/components/vicare/translations/it.json b/homeassistant/components/vicare/translations/it.json index 151e0c84509..c6917117a1f 100644 --- a/homeassistant/components/vicare/translations/it.json +++ b/homeassistant/components/vicare/translations/it.json @@ -13,13 +13,10 @@ "data": { "client_id": "Chiave API", "heating_type": "Modalit\u00e0 di riscaldamento", - "name": "Nome", "password": "Password", - "scan_interval": "Intervallo di scansione (secondi)", "username": "Email" }, - "description": "Configura l'integrazione ViCare. Per generare la chiave API vai su https://developer.viessmann.com", - "title": "{name}" + "description": "Configura l'integrazione ViCare. Per generare la chiave API vai su https://developer.viessmann.com" } } } diff --git a/homeassistant/components/vicare/translations/ja.json b/homeassistant/components/vicare/translations/ja.json index f88edf8f359..aa7e159778c 100644 --- a/homeassistant/components/vicare/translations/ja.json +++ b/homeassistant/components/vicare/translations/ja.json @@ -13,13 +13,10 @@ "data": { "client_id": "API\u30ad\u30fc", "heating_type": "\u6696\u623f(\u52a0\u71b1)\u30bf\u30a4\u30d7", - "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u79d2)", "username": "E\u30e1\u30fc\u30eb" }, - "description": "ViCare\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.viessmann.com \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", - "title": "{name}" + "description": "ViCare\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.viessmann.com \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } } diff --git a/homeassistant/components/vicare/translations/nl.json b/homeassistant/components/vicare/translations/nl.json index d16758be360..1e5fc68f9e3 100644 --- a/homeassistant/components/vicare/translations/nl.json +++ b/homeassistant/components/vicare/translations/nl.json @@ -13,13 +13,10 @@ "data": { "client_id": "API-sleutel", "heating_type": "Verwarmingstype", - "name": "Naam", "password": "Wachtwoord", - "scan_interval": "Scaninterval (seconden)", "username": "E-mail" }, - "description": "ViCare integratie instellen. Ga naar https://developer.viessmann.com om een API-sleutel te genereren.", - "title": "{name}" + "description": "ViCare integratie instellen. Ga naar https://developer.viessmann.com om een API-sleutel te genereren." } } } diff --git a/homeassistant/components/vicare/translations/no.json b/homeassistant/components/vicare/translations/no.json index 6f2cde73484..dda5a48434b 100644 --- a/homeassistant/components/vicare/translations/no.json +++ b/homeassistant/components/vicare/translations/no.json @@ -13,13 +13,10 @@ "data": { "client_id": "API-n\u00f8kkel", "heating_type": "Oppvarmingstype", - "name": "Navn", "password": "Passord", - "scan_interval": "Skanneintervall (sekunder)", "username": "E-post" }, - "description": "Sett opp ViCare-integrasjon. Hvis du vil generere API-n\u00f8kkel, g\u00e5r du til https://developer.viessmann.com", - "title": "" + "description": "Sett opp ViCare-integrasjon. Hvis du vil generere API-n\u00f8kkel, g\u00e5r du til https://developer.viessmann.com" } } } diff --git a/homeassistant/components/vicare/translations/pl.json b/homeassistant/components/vicare/translations/pl.json index e8374d7cc46..ae7bf6e5672 100644 --- a/homeassistant/components/vicare/translations/pl.json +++ b/homeassistant/components/vicare/translations/pl.json @@ -13,13 +13,10 @@ "data": { "client_id": "Klucz API", "heating_type": "Typ ogrzewania", - "name": "Nazwa", "password": "Has\u0142o", - "scan_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania (w sekundach)", "username": "Adres e-mail" }, - "description": "Skonfiguruj integracj\u0119 ViCare. Aby wygenerowa\u0107 klucz API wejd\u017a na https://developer.viessmann.com", - "title": "{name}" + "description": "Skonfiguruj integracj\u0119 ViCare. Aby wygenerowa\u0107 klucz API wejd\u017a na https://developer.viessmann.com" } } } diff --git a/homeassistant/components/vicare/translations/pt-BR.json b/homeassistant/components/vicare/translations/pt-BR.json index 01504272dac..1fe4ffe5848 100644 --- a/homeassistant/components/vicare/translations/pt-BR.json +++ b/homeassistant/components/vicare/translations/pt-BR.json @@ -13,13 +13,10 @@ "data": { "client_id": "Chave da API", "heating_type": "Tipo de aquecimento", - "name": "Nome", "password": "Senha", - "scan_interval": "Intervalo de varredura (segundos)", "username": "Email" }, - "description": "Configure a integra\u00e7\u00e3o do ViCare. Para gerar a chave de API, acesse https://developer.viessmann.com", - "title": "{name}" + "description": "Configure a integra\u00e7\u00e3o do ViCare. Para gerar a chave de API, acesse https://developer.viessmann.com" } } } diff --git a/homeassistant/components/vicare/translations/ru.json b/homeassistant/components/vicare/translations/ru.json index 9ce8c37f956..434daeb8e4b 100644 --- a/homeassistant/components/vicare/translations/ru.json +++ b/homeassistant/components/vicare/translations/ru.json @@ -13,13 +13,10 @@ "data": { "client_id": "\u041a\u043b\u044e\u0447 API", "heating_type": "\u0422\u0438\u043f \u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u044f", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "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\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 ViCare. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 https://developer.viessmann.com.", - "title": "{name}" + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 ViCare. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 https://developer.viessmann.com." } } } diff --git a/homeassistant/components/vicare/translations/sk.json b/homeassistant/components/vicare/translations/sk.json index d68d88fd2cd..1a2c2a47260 100644 --- a/homeassistant/components/vicare/translations/sk.json +++ b/homeassistant/components/vicare/translations/sk.json @@ -7,7 +7,6 @@ "user": { "data": { "client_id": "API k\u013e\u00fa\u010d", - "name": "N\u00e1zov", "username": "Email" } } diff --git a/homeassistant/components/vicare/translations/tr.json b/homeassistant/components/vicare/translations/tr.json index 0e3bd45bdf7..87cc440733d 100644 --- a/homeassistant/components/vicare/translations/tr.json +++ b/homeassistant/components/vicare/translations/tr.json @@ -13,13 +13,10 @@ "data": { "client_id": "API Anahtar\u0131", "heating_type": "Is\u0131tma tipi", - "name": "Ad", "password": "Parola", - "scan_interval": "Tarama Aral\u0131\u011f\u0131 (saniye)", "username": "E-posta" }, - "description": "ViCare entegrasyonunu ayarlay\u0131n. API anahtar\u0131 olu\u015fturmak i\u00e7in https://developer.viessmann.com adresine gidin.", - "title": "{name}" + "description": "ViCare entegrasyonunu ayarlay\u0131n. API anahtar\u0131 olu\u015fturmak i\u00e7in https://developer.viessmann.com adresine gidin." } } } diff --git a/homeassistant/components/vicare/translations/zh-Hant.json b/homeassistant/components/vicare/translations/zh-Hant.json index c3b8fc02c4f..4d18a4f11df 100644 --- a/homeassistant/components/vicare/translations/zh-Hant.json +++ b/homeassistant/components/vicare/translations/zh-Hant.json @@ -13,13 +13,10 @@ "data": { "client_id": "API \u91d1\u9470", "heating_type": "\u6696\u6c23\u985e\u5225", - "name": "\u540d\u7a31", "password": "\u5bc6\u78bc", - "scan_interval": "\u6383\u63cf\u9593\u8ddd\uff08\u79d2\uff09", "username": "\u96fb\u5b50\u90f5\u4ef6" }, - "description": "\u6b32\u8a2d\u5b9a ViCare \u6574\u5408\u3002\u8acb\u81f3 https://developer.viessmann.com \u7522\u751f API \u91d1\u9470", - "title": "{name}" + "description": "\u6b32\u8a2d\u5b9a ViCare \u6574\u5408\u3002\u8acb\u81f3 https://developer.viessmann.com \u7522\u751f API \u91d1\u9470" } } } diff --git a/homeassistant/components/vilfo/translations/ca.json b/homeassistant/components/vilfo/translations/ca.json index 827545f5b54..b068144df09 100644 --- a/homeassistant/components/vilfo/translations/ca.json +++ b/homeassistant/components/vilfo/translations/ca.json @@ -13,9 +13,7 @@ "data": { "access_token": "Token d'acc\u00e9s", "host": "Amfitri\u00f3" - }, - "description": "Configura la integraci\u00f3 de l'encaminador Vilfo. Necessites la seva IP o nom d'amfitri\u00f3 i el token d'acc\u00e9s de l'API. Per a m\u00e9s informaci\u00f3, visita: https://www.home-assistant.io/integrations/vilfo", - "title": "Connexi\u00f3 amb l'encaminador Vilfo" + } } } } diff --git a/homeassistant/components/vilfo/translations/cs.json b/homeassistant/components/vilfo/translations/cs.json index b6735bd64cb..77566bbc59e 100644 --- a/homeassistant/components/vilfo/translations/cs.json +++ b/homeassistant/components/vilfo/translations/cs.json @@ -13,9 +13,7 @@ "data": { "access_token": "P\u0159\u00edstupov\u00fd token", "host": "Hostitel" - }, - "description": "Nastaven\u00ed integrace routeru Vilfo. Pot\u0159ebujete n\u00e1zev hostitele/IP adresu routeru Vilfo a p\u0159\u00edstupov\u00fd API token. Dal\u0161\u00ed informace o t\u00e9to integraci a o tom, jak tyto podrobnosti z\u00edskat, najdete na adrese: https://www.home-assistant.io/integrations/vilfo", - "title": "P\u0159ipojen\u00ed k routeru Vilfo" + } } } } diff --git a/homeassistant/components/vilfo/translations/da.json b/homeassistant/components/vilfo/translations/da.json index 0ef9c619c91..014c8aee190 100644 --- a/homeassistant/components/vilfo/translations/da.json +++ b/homeassistant/components/vilfo/translations/da.json @@ -13,9 +13,7 @@ "data": { "access_token": "Adgangstoken til Vilfo-router-API", "host": "Router-v\u00e6rtsnavn eller IP" - }, - "description": "Indstil Vilfo-routerintegration. Du har brug for dit Vilfo-routerv\u00e6rtsnavn/IP og et API-adgangstoken. For yderligere information om denne integration og hvordan du f\u00e5r disse detaljer, kan du bes\u00f8ge: https://www.home-assistant.io/integrations/vilfo", - "title": "Opret forbindelse til Vilfo-router" + } } } } diff --git a/homeassistant/components/vilfo/translations/de.json b/homeassistant/components/vilfo/translations/de.json index 798410c56e3..6e20a86a64a 100644 --- a/homeassistant/components/vilfo/translations/de.json +++ b/homeassistant/components/vilfo/translations/de.json @@ -13,9 +13,7 @@ "data": { "access_token": "Zugangstoken", "host": "Host" - }, - "description": "Richte die Vilfo Router-Integration ein. Du ben\u00f6tigst deinen Vilfo Router-Hostnamen / deine IP-Adresse und ein API-Zugriffstoken. Weitere Informationen zu dieser Integration und wie du diese Details erh\u00e4ltst, findest du unter: https://www.home-assistant.io/integrations/vilfo", - "title": "Stelle eine Verbindung zum Vilfo Router her" + } } } } diff --git a/homeassistant/components/vilfo/translations/el.json b/homeassistant/components/vilfo/translations/el.json index 7af0b69e97e..dad6b11a492 100644 --- a/homeassistant/components/vilfo/translations/el.json +++ b/homeassistant/components/vilfo/translations/el.json @@ -13,9 +13,7 @@ "data": { "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" - }, - "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae Vilfo. \u03a7\u03c1\u03b5\u03b9\u03ac\u03b6\u03b5\u03c3\u03c4\u03b5 \u03c4\u03bf hostname/IP \u03c4\u03bf\u03c5 Vilfo Router \u03ba\u03b1\u03b9 \u03ad\u03bd\u03b1 \u03ba\u03bf\u03c5\u03c0\u03cc\u03bd\u03b9 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 API. \u0393\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2, \u03b5\u03c0\u03b9\u03c3\u03ba\u03b5\u03c6\u03b8\u03b5\u03af\u03c4\u03b5: https://www.home-assistant.io/integrations/vilfo", - "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae Vilfo" + } } } } diff --git a/homeassistant/components/vilfo/translations/en.json b/homeassistant/components/vilfo/translations/en.json index 03f9a9bd23a..f26c34aa917 100644 --- a/homeassistant/components/vilfo/translations/en.json +++ b/homeassistant/components/vilfo/translations/en.json @@ -13,9 +13,7 @@ "data": { "access_token": "Access Token", "host": "Host" - }, - "description": "Set up the Vilfo Router integration. You need your Vilfo Router hostname/IP and an API access token. For additional information on this integration and how to get those details, visit: https://www.home-assistant.io/integrations/vilfo", - "title": "Connect to the Vilfo Router" + } } } } diff --git a/homeassistant/components/vilfo/translations/es-419.json b/homeassistant/components/vilfo/translations/es-419.json index 9c62c327235..03bd5f05446 100644 --- a/homeassistant/components/vilfo/translations/es-419.json +++ b/homeassistant/components/vilfo/translations/es-419.json @@ -13,9 +13,7 @@ "data": { "access_token": "Token de acceso para la API del enrutador Vilfo", "host": "Nombre de host o IP del enrutador" - }, - "description": "Configure la integraci\u00f3n del enrutador Vilfo. Necesita su nombre de host/IP de Vilfo Router y un token de acceso API. Para obtener informaci\u00f3n adicional sobre esta integraci\u00f3n y c\u00f3mo obtener esos detalles, visite: https://www.home-assistant.io/integrations/vilfo", - "title": "Conectar con el Router Vilfo" + } } } } diff --git a/homeassistant/components/vilfo/translations/es.json b/homeassistant/components/vilfo/translations/es.json index 863e0cfb6ce..eb5219b01a1 100644 --- a/homeassistant/components/vilfo/translations/es.json +++ b/homeassistant/components/vilfo/translations/es.json @@ -13,9 +13,7 @@ "data": { "access_token": "Token de acceso", "host": "Host" - }, - "description": "Configure la integraci\u00f3n del Router Vilfo. Necesita su nombre de host/IP del Router Vilfo y un token de acceso a la API. Para obtener informaci\u00f3n adicional sobre esta integraci\u00f3n y c\u00f3mo obtener esos detalles, visite: https://www.home-assistant.io/integrations/vilfo", - "title": "Conectar con el Router Vilfo" + } } } } diff --git a/homeassistant/components/vilfo/translations/et.json b/homeassistant/components/vilfo/translations/et.json index 8ed6aa9e14c..2274221a6bb 100644 --- a/homeassistant/components/vilfo/translations/et.json +++ b/homeassistant/components/vilfo/translations/et.json @@ -13,9 +13,7 @@ "data": { "access_token": "Juurdep\u00e4\u00e4sut\u00f5end", "host": "" - }, - "description": "Seadista Vilfo ruuteri sidumine. Vaja on oma Vilfo ruuteri hosti nime / IP-d ja API juurdep\u00e4\u00e4suluba. Lisateavet selle integreerimise ja selle kohta, kuidas neid \u00fcksikasju saada, leiad aadressilt https://www.home-assistant.io/integrations/vilfo", - "title": "\u00dchenda Vilfo ruuteriga" + } } } } diff --git a/homeassistant/components/vilfo/translations/fr.json b/homeassistant/components/vilfo/translations/fr.json index 10289500383..55658b0f1ca 100644 --- a/homeassistant/components/vilfo/translations/fr.json +++ b/homeassistant/components/vilfo/translations/fr.json @@ -13,9 +13,7 @@ "data": { "access_token": "Jeton d'acc\u00e8s", "host": "H\u00f4te" - }, - "description": "Configurez l'int\u00e9gration du routeur Vilfo. Vous avez besoin du nom d'h\u00f4te / IP de votre routeur Vilfo et d'un jeton d'acc\u00e8s API. Pour plus d'informations sur cette int\u00e9gration et comment obtenir ces d\u00e9tails, visitez: https://www.home-assistant.io/integrations/vilfo", - "title": "Connectez-vous au routeur Vilfo" + } } } } diff --git a/homeassistant/components/vilfo/translations/he.json b/homeassistant/components/vilfo/translations/he.json index 642f482d14e..87a65e1af93 100644 --- a/homeassistant/components/vilfo/translations/he.json +++ b/homeassistant/components/vilfo/translations/he.json @@ -13,8 +13,7 @@ "data": { "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4", "host": "\u05de\u05d0\u05e8\u05d7" - }, - "description": "\u05d4\u05d2\u05d3\u05e8 \u05d0\u05ea \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d4\u05e0\u05ea\u05d1 \u05e9\u05dc Vilfo. \u05d0\u05ea\u05d4 \u05e6\u05e8\u05d9\u05da \u05d0\u05ea \u05e9\u05dd \u05d4\u05de\u05d0\u05e8\u05d7/IP \u05e9\u05dc \u05e0\u05ea\u05d1 Vilfo \u05e9\u05dc\u05da \u05d5\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05ea API. \u05dc\u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3 \u05e2\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d6\u05d4 \u05d5\u05db\u05d9\u05e6\u05d3 \u05dc\u05e7\u05d1\u05dc \u05e4\u05e8\u05d8\u05d9\u05dd \u05d0\u05dc\u05d4, \u05d1\u05e7\u05e8 \u05d1\u05db\u05ea\u05d5\u05d1\u05ea: https://www.home-assistant.io/integrations/vilfo" + } } } } diff --git a/homeassistant/components/vilfo/translations/hu.json b/homeassistant/components/vilfo/translations/hu.json index 66e0b4d7d77..f2b23836336 100644 --- a/homeassistant/components/vilfo/translations/hu.json +++ b/homeassistant/components/vilfo/translations/hu.json @@ -13,9 +13,7 @@ "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", "host": "C\u00edm" - }, - "description": "\u00c1ll\u00edtsa be a Vilfo Router integr\u00e1ci\u00f3t. Sz\u00fcks\u00e9ge van a Vilfo Router g\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/vilfo/translations/id.json b/homeassistant/components/vilfo/translations/id.json index 3871670a1cd..9f8adda9737 100644 --- a/homeassistant/components/vilfo/translations/id.json +++ b/homeassistant/components/vilfo/translations/id.json @@ -13,9 +13,7 @@ "data": { "access_token": "Token Akses", "host": "Host" - }, - "description": "Siapkan integrasi Router Vilfo. Anda memerlukan nama host/IP Router Vilfo dan token akses API. Untuk informasi lebih lanjut tentang integrasi ini dan cara mendapatkan data tersebut, kunjungi: https://www.home-assistant.io/integrations/vilfo", - "title": "Hubungkan ke Router Vilfo" + } } } } diff --git a/homeassistant/components/vilfo/translations/it.json b/homeassistant/components/vilfo/translations/it.json index 7d30b0017fb..8c813c64226 100644 --- a/homeassistant/components/vilfo/translations/it.json +++ b/homeassistant/components/vilfo/translations/it.json @@ -13,9 +13,7 @@ "data": { "access_token": "Token di accesso", "host": "Host" - }, - "description": "Configura l'integrazione del Vilfo Router. Hai bisogno del tuo nome host/IP del Vilfo Router e un token di accesso API. Per ulteriori informazioni su questa integrazione e su come ottenere tali dettagli, visita il sito: https://www.home-assistant.io/integrations/vilfo", - "title": "Collegamento al Vilfo Router" + } } } } diff --git a/homeassistant/components/vilfo/translations/ja.json b/homeassistant/components/vilfo/translations/ja.json index 6f98d264f24..6a41c1168cb 100644 --- a/homeassistant/components/vilfo/translations/ja.json +++ b/homeassistant/components/vilfo/translations/ja.json @@ -13,9 +13,7 @@ "data": { "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", "host": "\u30db\u30b9\u30c8" - }, - "description": "Vilfo Router\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002Vilfo Router\u306e\u30db\u30b9\u30c8\u540d/IP\u3068API\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u3067\u3059\u3002\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u95a2\u3059\u308b\u8a73\u7d30\u3068\u305d\u308c\u3089\u306e\u53d6\u5f97\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001https://www.home-assistant.io/integrations/vilfo \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044", - "title": "Vilfo\u30eb\u30fc\u30bf\u30fc\u306b\u63a5\u7d9a" + } } } } diff --git a/homeassistant/components/vilfo/translations/ko.json b/homeassistant/components/vilfo/translations/ko.json index 980ee36d7a5..d2db1a7fb7a 100644 --- a/homeassistant/components/vilfo/translations/ko.json +++ b/homeassistant/components/vilfo/translations/ko.json @@ -13,9 +13,7 @@ "data": { "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070", "host": "\ud638\uc2a4\ud2b8" - }, - "description": "Vilfo \ub77c\uc6b0\ud130 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. Vilfo \ub77c\uc6b0\ud130 \ud638\uc2a4\ud2b8 \uc774\ub984/IP \uc640 API \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud55c \ucd94\uac00 \uc815\ubcf4\uc640 \uc138\ubd80 \uc0ac\ud56d\uc740 https://www.home-assistant.io/integrations/vilfo \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694", - "title": "Vilfo \ub77c\uc6b0\ud130\uc5d0 \uc5f0\uacb0\ud558\uae30" + } } } } diff --git a/homeassistant/components/vilfo/translations/lb.json b/homeassistant/components/vilfo/translations/lb.json index cdf16438469..ebbe55e48eb 100644 --- a/homeassistant/components/vilfo/translations/lb.json +++ b/homeassistant/components/vilfo/translations/lb.json @@ -13,9 +13,7 @@ "data": { "access_token": "Acc\u00e8s Jeton", "host": "Host" - }, - "description": "Vilfo Router Integratioun ariichten. Dir braucht \u00e4re Vilfo Router Numm/IP an een API Acc\u00e8s Jeton. Fir weider Informatiounen zu d\u00ebser Integratioun a w\u00e9i een zu d\u00ebsen n\u00e9idegen Informatioune k\u00ebnnt, gitt op: https://www.home-assistant.io/integrations/vilfo", - "title": "Mam Vilfo Router verbannen" + } } } } diff --git a/homeassistant/components/vilfo/translations/nl.json b/homeassistant/components/vilfo/translations/nl.json index 304871cc145..fc49ddbf7b8 100644 --- a/homeassistant/components/vilfo/translations/nl.json +++ b/homeassistant/components/vilfo/translations/nl.json @@ -13,9 +13,7 @@ "data": { "access_token": "Toegangstoken", "host": "Host" - }, - "description": "Stel de Vilfo Router-integratie in. U heeft de hostnaam/IP van uw Vilfo Router en een API-toegangstoken nodig. Voor meer informatie over deze integratie en hoe u die details kunt verkrijgen, gaat u naar: https://www.home-assistant.io/integrations/vilfo", - "title": "Maak verbinding met de Vilfo Router" + } } } } diff --git a/homeassistant/components/vilfo/translations/no.json b/homeassistant/components/vilfo/translations/no.json index b1f1cd8950f..6550778a853 100644 --- a/homeassistant/components/vilfo/translations/no.json +++ b/homeassistant/components/vilfo/translations/no.json @@ -13,9 +13,7 @@ "data": { "access_token": "Tilgangstoken", "host": "Vert" - }, - "description": "Sett opp Vilfo Router-integrasjonen. Du trenger ditt Vilfo Router vertsnavn/IP og et API-tilgangstoken. For ytterligere informasjon om denne integrasjonen og hvordan du f\u00e5r disse detaljene, bes\u00f8k [https://www.home-assistant.io/integrations/vilfo](https://www.home-assistant.io/integrations/vilfo)", - "title": "Koble til Vilfo Ruteren" + } } } } diff --git a/homeassistant/components/vilfo/translations/pl.json b/homeassistant/components/vilfo/translations/pl.json index 43b94ffd103..a5016c989f5 100644 --- a/homeassistant/components/vilfo/translations/pl.json +++ b/homeassistant/components/vilfo/translations/pl.json @@ -13,9 +13,7 @@ "data": { "access_token": "Token dost\u0119pu", "host": "Nazwa hosta lub adres IP" - }, - "description": "Skonfiguruj integracj\u0119 routera Vilfo. Potrzebujesz nazwy hosta/adresu IP routera Vilfo i tokena dost\u0119pu do interfejsu API. Aby uzyska\u0107 dodatkowe informacje na temat tej integracji i sposobu uzyskania niezb\u0119dnych danych do konfiguracji, odwied\u017a: https://www.home-assistant.io/integrations/vilfo", - "title": "Po\u0142\u0105czenie z routerem Vilfo" + } } } } diff --git a/homeassistant/components/vilfo/translations/pt-BR.json b/homeassistant/components/vilfo/translations/pt-BR.json index 8766261955f..f772ec37472 100644 --- a/homeassistant/components/vilfo/translations/pt-BR.json +++ b/homeassistant/components/vilfo/translations/pt-BR.json @@ -13,9 +13,7 @@ "data": { "access_token": "Token de acesso", "host": "Nome do host" - }, - "description": "Configure a integra\u00e7\u00e3o do roteador Vilfo. Voc\u00ea precisa do seu nome de host/IP do roteador Vilfo e um token de acesso \u00e0 API. Para obter informa\u00e7\u00f5es adicionais sobre essa integra\u00e7\u00e3o e como obter esses detalhes, visite: https://www.home-assistant.io/integrations/vilfo", - "title": "Conecte-se ao roteador Vilfo" + } } } } diff --git a/homeassistant/components/vilfo/translations/ru.json b/homeassistant/components/vilfo/translations/ru.json index 62ec2fe5dae..6a2a2163313 100644 --- a/homeassistant/components/vilfo/translations/ru.json +++ b/homeassistant/components/vilfo/translations/ru.json @@ -13,9 +13,7 @@ "data": { "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", "host": "\u0425\u043e\u0441\u0442" - }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 Vilfo. \u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0440\u043e\u0443\u0442\u0435\u0440\u0430 \u0438 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 API. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438, \u043f\u043e\u0441\u0435\u0442\u0438\u0442\u0435 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442: https://www.home-assistant.io/integrations/vilfo.", - "title": "Vilfo Router" + } } } } diff --git a/homeassistant/components/vilfo/translations/sl.json b/homeassistant/components/vilfo/translations/sl.json index c815698099f..d31dc23a3f6 100644 --- a/homeassistant/components/vilfo/translations/sl.json +++ b/homeassistant/components/vilfo/translations/sl.json @@ -13,9 +13,7 @@ "data": { "access_token": "Dostopni \u017eeton za API Vilfo Router", "host": "Ime gostitelja usmerjevalnika ali IP" - }, - "description": "Nastavite integracijo Vilfo Router. Potrebujete ime gostitelja ali IP Vilfo usmerjevalnika in dostopni \u017eeton API. Za dodatne informacije o tej integraciji in kako do teh podrobnosti obi\u0161\u010dite: https://www.home-assistant.io/integrations/vilfo", - "title": "Pove\u017eite se z usmerjevalnikom Vilfo" + } } } } diff --git a/homeassistant/components/vilfo/translations/sv.json b/homeassistant/components/vilfo/translations/sv.json index 7192f91ee1a..2594b350d1c 100644 --- a/homeassistant/components/vilfo/translations/sv.json +++ b/homeassistant/components/vilfo/translations/sv.json @@ -13,9 +13,7 @@ "data": { "access_token": "\u00c5tkomstnyckel f\u00f6r Vilfo-routerns API", "host": "Routerns v\u00e4rdnamn eller IP-adress" - }, - "description": "St\u00e4ll in Vilfo Router-integrationen. Du beh\u00f6ver din Vilfo-routers v\u00e4rdnamn eller IP-adress och en \u00e5tkomstnyckel till dess API. F\u00f6r ytterligare information om den h\u00e4r integrationen och hur du f\u00e5r fram den n\u00f6dv\u00e4ndiga informationen, bes\u00f6k: https://www.home-assistant.io/integrations/vilfo", - "title": "Anslut till Vilfo-routern" + } } } } diff --git a/homeassistant/components/vilfo/translations/tr.json b/homeassistant/components/vilfo/translations/tr.json index 2e27132afb6..dc66041e35a 100644 --- a/homeassistant/components/vilfo/translations/tr.json +++ b/homeassistant/components/vilfo/translations/tr.json @@ -13,9 +13,7 @@ "data": { "access_token": "Eri\u015fim Belirteci", "host": "Ana Bilgisayar" - }, - "description": "Vilfo Router entegrasyonunu ayarlay\u0131n. Vilfo Router ana bilgisayar ad\u0131n\u0131za/IP'nize ve bir API eri\u015fim anahtar\u0131na ihtiyac\u0131n\u0131z var. Bu entegrasyon ve bu ayr\u0131nt\u0131lar\u0131n nas\u0131l al\u0131naca\u011f\u0131 hakk\u0131nda ek bilgi i\u00e7in \u015fu adresi ziyaret edin: https://www.home-assistant.io/integrations/vilfo", - "title": "Vilfo Router'a ba\u011flan\u0131n" + } } } } diff --git a/homeassistant/components/vilfo/translations/uk.json b/homeassistant/components/vilfo/translations/uk.json index 1a93176f290..4688b58e187 100644 --- a/homeassistant/components/vilfo/translations/uk.json +++ b/homeassistant/components/vilfo/translations/uk.json @@ -13,9 +13,7 @@ "data": { "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443", "host": "\u0425\u043e\u0441\u0442" - }, - "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 Vilfo. \u0412\u043a\u0430\u0436\u0456\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u0440\u043e\u0443\u0442\u0435\u0440\u0430 \u0456 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 API. \u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0446\u0456\u0454\u0457 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457, \u0432\u0456\u0434\u0432\u0456\u0434\u0430\u0439\u0442\u0435 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442: https://www.home-assistant.io/integrations/vilfo.", - "title": "Vilfo Router" + } } } } diff --git a/homeassistant/components/vilfo/translations/zh-Hans.json b/homeassistant/components/vilfo/translations/zh-Hans.json index f3fec441d4c..756c7e68c98 100644 --- a/homeassistant/components/vilfo/translations/zh-Hans.json +++ b/homeassistant/components/vilfo/translations/zh-Hans.json @@ -9,9 +9,7 @@ "data": { "access_token": "Vilfo \u8def\u7531\u5668 API \u5b58\u53d6\u5bc6\u94a5", "host": "\u8def\u7531\u5668\u4e3b\u673a\u540d\u6216 IP \u5730\u5740" - }, - "description": "\u8bbe\u7f6e Vilfo \u8def\u7531\u5668\u6574\u5408\u3002\u60a8\u9700\u8981\u8f93\u5165 Vilfo \u8def\u7531\u5668\u4e3b\u673a\u540d/IP \u5730\u5740\u3001API\u5b58\u53d6\u5bc6\u94a5\u3002\u5176\u4ed6\u6574\u5408\u7684\u76f8\u5173\u4fe1\u606f\uff0c\u8bf7\u8bbf\u95ee\uff1ahttps://www.home-assistant.io/integrations/vilfo", - "title": "\u8fde\u63a5\u5230 Vilfo \u8def\u7531\u5668" + } } } } diff --git a/homeassistant/components/vilfo/translations/zh-Hant.json b/homeassistant/components/vilfo/translations/zh-Hant.json index eb3dba826be..171fa5b16e8 100644 --- a/homeassistant/components/vilfo/translations/zh-Hant.json +++ b/homeassistant/components/vilfo/translations/zh-Hant.json @@ -13,9 +13,7 @@ "data": { "access_token": "\u5b58\u53d6\u6b0a\u6756", "host": "\u4e3b\u6a5f\u7aef" - }, - "description": "\u8a2d\u5b9a Vilfo \u8def\u7531\u5668\u6574\u5408\u3002\u9700\u8981\u8f38\u5165 Vilfo \u8def\u7531\u5668\u4e3b\u6a5f\u540d\u7a31/IP \u4f4d\u5740\u3001API \u5b58\u53d6\u6b0a\u6756\u3002\u5176\u4ed6\u6574\u5408\u76f8\u95dc\u8cc7\u8a0a\uff0c\u8acb\u53c3\u8003\uff1ahttps://www.home-assistant.io/integrations/vilfo", - "title": "\u9023\u7dda\u81f3 Vilfo \u8def\u7531\u5668" + } } } } diff --git a/homeassistant/components/vulcan/translations/bg.json b/homeassistant/components/vulcan/translations/bg.json index 27893871c14..187ad8cff4b 100644 --- a/homeassistant/components/vulcan/translations/bg.json +++ b/homeassistant/components/vulcan/translations/bg.json @@ -13,18 +13,5 @@ } } } - }, - "options": { - "error": { - "error": "\u0412\u044a\u0437\u043d\u0438\u043a\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" - }, - "step": { - "init": { - "data": { - "message_notify": "\u041f\u043e\u043a\u0430\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0438\u0437\u0432\u0435\u0441\u0442\u0438\u044f \u043f\u0440\u0438 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u043d\u043e\u0432\u043e \u0441\u044a\u043e\u0431\u0449\u0435\u043d\u0438\u0435", - "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043d\u0430 \u043e\u0431\u043d\u043e\u0432\u044f\u0432\u0430\u043d\u0435 (\u0432 \u043c\u0438\u043d\u0443\u0442\u0438)" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/ca.json b/homeassistant/components/vulcan/translations/ca.json index a9bfe95310b..2f7e454be6b 100644 --- a/homeassistant/components/vulcan/translations/ca.json +++ b/homeassistant/components/vulcan/translations/ca.json @@ -30,14 +30,6 @@ }, "description": "Inicia sessi\u00f3 al teu compte de Vulcan mitjan\u00e7ant la p\u00e0gina de registre de l'aplicaci\u00f3 m\u00f2bil." }, - "reauth": { - "data": { - "pin": "PIN", - "region": "S\u00edmbol", - "token": "Token" - }, - "description": "Inicia sessi\u00f3 al teu compte de Vulcan mitjan\u00e7ant la p\u00e0gina de registre de l'aplicaci\u00f3 m\u00f2bil." - }, "reauth_confirm": { "data": { "pin": "PIN", @@ -59,20 +51,5 @@ "description": "Selecciona l'alumne, pots afegir m\u00e9s alumnes tornant a afegir la integraci\u00f3 de nou." } } - }, - "options": { - "error": { - "error": "S'ha produ\u00eft un error" - }, - "step": { - "init": { - "data": { - "attendance_notify": "Mostra les notificacions sobre les darreres assist\u00e8ncies", - "grade_notify": "Mostra notificacions de les \u00faltimes qualificacions", - "message_notify": "Mostra notificacions quan es rebi un missatge nou", - "scan_interval": "Interval d'actualitzaci\u00f3 (minuts)" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/de.json b/homeassistant/components/vulcan/translations/de.json index 94f25858119..66de1c8ec75 100644 --- a/homeassistant/components/vulcan/translations/de.json +++ b/homeassistant/components/vulcan/translations/de.json @@ -30,14 +30,6 @@ }, "description": "Melde dich bei deinem Vulcan-Konto \u00fcber die Registrierungsseite der mobilen App an." }, - "reauth": { - "data": { - "pin": "PIN", - "region": "Symbol", - "token": "Token" - }, - "description": "Melde dich bei deinem Vulcan-Konto \u00fcber die Registrierungsseite der mobilen App an." - }, "reauth_confirm": { "data": { "pin": "PIN", @@ -59,20 +51,5 @@ "description": "W\u00e4hle Sch\u00fcler aus, Du kannst weitere Sch\u00fcler hinzuf\u00fcgen, indem du die Integration erneut hinzuf\u00fcgst." } } - }, - "options": { - "error": { - "error": "Ein Fehler ist aufgetreten" - }, - "step": { - "init": { - "data": { - "attendance_notify": "Benachrichtigungen \u00fcber die neuesten Anwesenheitseintr\u00e4ge anzeigen", - "grade_notify": "Benachrichtigungen \u00fcber die neuesten Noten anzeigen", - "message_notify": "Benachrichtigungen anzeigen, wenn eine neue Nachricht eingegangen ist", - "scan_interval": "Aktualisierungsintervall (in Minuten)" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/el.json b/homeassistant/components/vulcan/translations/el.json index abd3a264463..a041679919a 100644 --- a/homeassistant/components/vulcan/translations/el.json +++ b/homeassistant/components/vulcan/translations/el.json @@ -30,14 +30,6 @@ }, "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 Vulcan \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b7 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03b9\u03bd\u03b7\u03c4\u03ac." }, - "reauth": { - "data": { - "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", - "region": "\u03a3\u03cd\u03bc\u03b2\u03bf\u03bb\u03bf", - "token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc" - }, - "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 Vulcan \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b7 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03b9\u03bd\u03b7\u03c4\u03ac." - }, "reauth_confirm": { "data": { "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", @@ -59,20 +51,5 @@ "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b1\u03b8\u03b7\u03c4\u03ae, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03bf\u03c5\u03c2 \u03bc\u03b1\u03b8\u03b7\u03c4\u03ad\u03c2 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c4\u03bf\u03bd\u03c4\u03b1\u03c2 \u03be\u03b1\u03bd\u03ac \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7." } } - }, - "options": { - "error": { - "error": "\u03a0\u03c1\u03bf\u03ad\u03ba\u03c5\u03c8\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" - }, - "step": { - "init": { - "data": { - "attendance_notify": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c9\u03bd \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b9\u03bf \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03b5\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03ae\u03c3\u03b5\u03b9\u03c2 \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ce\u03bd", - "grade_notify": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c9\u03bd \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03c0\u03b9\u03bf \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03bf\u03c5\u03c2 \u03b2\u03b1\u03b8\u03bc\u03bf\u03cd\u03c2", - "message_notify": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c9\u03bd \u03cc\u03c4\u03b1\u03bd \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03ad\u03bf \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1", - "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2 (\u03c3\u03b5 \u03bb\u03b5\u03c0\u03c4\u03ac)" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/en.json b/homeassistant/components/vulcan/translations/en.json index 8ea9fc59964..65b9616e1e7 100644 --- a/homeassistant/components/vulcan/translations/en.json +++ b/homeassistant/components/vulcan/translations/en.json @@ -30,14 +30,6 @@ }, "description": "Login to your Vulcan Account using mobile app registration page." }, - "reauth": { - "data": { - "pin": "Pin", - "region": "Symbol", - "token": "Token" - }, - "description": "Login to your Vulcan Account using mobile app registration page." - }, "reauth_confirm": { "data": { "pin": "Pin", @@ -59,20 +51,5 @@ "description": "Select student, you can add more students by adding integration again." } } - }, - "options": { - "error": { - "error": "Error occurred" - }, - "step": { - "init": { - "data": { - "attendance_notify": "Show notifications about the latest attendance entries", - "grade_notify": "Show notifications about the latest grades", - "message_notify": "Show notifications when new message received", - "scan_interval": "Update interval (in minutes)" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/es.json b/homeassistant/components/vulcan/translations/es.json index fa303806a76..a92a8878730 100644 --- a/homeassistant/components/vulcan/translations/es.json +++ b/homeassistant/components/vulcan/translations/es.json @@ -15,14 +15,6 @@ "pin": "PIN" } }, - "reauth": { - "data": { - "pin": "PIN", - "region": "S\u00edmbolo", - "token": "Token" - }, - "description": "Inicia sesi\u00f3n en tu cuenta de Vulcan mediante la p\u00e1gina de registro de la aplicaci\u00f3n m\u00f3vil." - }, "reauth_confirm": { "data": { "region": "S\u00edmbolo" @@ -34,18 +26,5 @@ } } } - }, - "options": { - "error": { - "error": "Se ha producido un error" - }, - "step": { - "init": { - "data": { - "message_notify": "Muestra notificaciones cuando se reciba un mensaje nuevo", - "scan_interval": "Intervalo de actualizaci\u00f3n (minutos)" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/et.json b/homeassistant/components/vulcan/translations/et.json index 80c97d1c4ed..f50fddae8f4 100644 --- a/homeassistant/components/vulcan/translations/et.json +++ b/homeassistant/components/vulcan/translations/et.json @@ -30,14 +30,6 @@ }, "description": "Logi Vulcani kontole sisse mobiilirakenduse registreerimislehe kaudu." }, - "reauth": { - "data": { - "pin": "PIN kood", - "region": "S\u00fcmbol", - "token": "Token" - }, - "description": "Logi Vulcani kontole sisse mobiilirakenduse registreerimislehe kaudu." - }, "reauth_confirm": { "data": { "pin": "PIN kood", @@ -59,20 +51,5 @@ "description": "Vali \u00f5pilane, saad lisada rohkem \u00f5pilasi lisades sidumise veelkord." } } - }, - "options": { - "error": { - "error": "Ilmnes t\u00f5rge" - }, - "step": { - "init": { - "data": { - "attendance_notify": "Kuva m\u00e4rguanded viimaste osalemiskirjete kohta", - "grade_notify": "Kuva m\u00e4rguanded viimaste hinnete kohta", - "message_notify": "Kuva teated uue s\u00f5numi saabumisel", - "scan_interval": "V\u00e4rskendamise intervall (minutites)" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/fr.json b/homeassistant/components/vulcan/translations/fr.json index 9a850d85796..0a1f16b8406 100644 --- a/homeassistant/components/vulcan/translations/fr.json +++ b/homeassistant/components/vulcan/translations/fr.json @@ -30,14 +30,6 @@ }, "description": "Connectez-vous \u00e0 votre compte Vulcan en utilisant la page d'inscription de l'application mobile." }, - "reauth": { - "data": { - "pin": "PIN", - "region": "Symbole", - "token": "Jeton" - }, - "description": "Connectez-vous \u00e0 votre compte Vulcan en utilisant la page d'inscription de l'application mobile." - }, "reauth_confirm": { "data": { "pin": "PIN", @@ -59,20 +51,5 @@ "description": "S\u00e9lectionnez l'\u00e9tudiant\u00a0; vous pouvez ajouter d'autres \u00e9tudiants en ajoutant \u00e0 nouveau l'int\u00e9gration." } } - }, - "options": { - "error": { - "error": "Une erreur est survenue" - }, - "step": { - "init": { - "data": { - "attendance_notify": "Afficher les notifications au sujet des derni\u00e8res entr\u00e9es de pr\u00e9sence", - "grade_notify": "Afficher les notifications au sujet des derni\u00e8res notes", - "message_notify": "Afficher les notifications lors de la r\u00e9ception d'un nouveau message", - "scan_interval": "Intervalle de mise \u00e0 jour (en minutes)" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/hu.json b/homeassistant/components/vulcan/translations/hu.json index 90566b69fe8..65d90d6e24a 100644 --- a/homeassistant/components/vulcan/translations/hu.json +++ b/homeassistant/components/vulcan/translations/hu.json @@ -30,14 +30,6 @@ }, "description": "Jelentkezzen be Vulcan fi\u00f3kj\u00e1ba a mobilalkalmaz\u00e1s regisztr\u00e1ci\u00f3s oldal\u00e1n." }, - "reauth": { - "data": { - "pin": "PIN", - "region": "Szimb\u00f3lum", - "token": "Token" - }, - "description": "Jelentkezzen be Vulcan fi\u00f3kj\u00e1ba a mobilalkalmaz\u00e1s regisztr\u00e1ci\u00f3s oldal\u00e1n." - }, "reauth_confirm": { "data": { "pin": "PIN", @@ -59,20 +51,5 @@ "description": "V\u00e1lassza ki a tanul\u00f3t, tov\u00e1bbi tanul\u00f3kat vehet fel az integr\u00e1ci\u00f3 \u00fajb\u00f3li hozz\u00e1ad\u00e1s\u00e1val." } } - }, - "options": { - "error": { - "error": "Hiba t\u00f6rt\u00e9nt" - }, - "step": { - "init": { - "data": { - "attendance_notify": "\u00c9rtes\u00edt\u00e9sek megjelen\u00edt\u00e9se a legut\u00f3bbi r\u00e9szv\u00e9teli bejegyz\u00e9sekr\u0151l", - "grade_notify": "\u00c9rtes\u00edt\u00e9sek megjelen\u00edt\u00e9se a leg\u00fajabb oszt\u00e1lyzatokr\u00f3l", - "message_notify": "\u00c9rtes\u00edt\u00e9sek megjelen\u00edt\u00e9se \u00faj \u00fczenet \u00e9rkez\u00e9sekor", - "scan_interval": "Friss\u00edt\u00e9si id\u0151k\u00f6z (percben)" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/id.json b/homeassistant/components/vulcan/translations/id.json index c372ca0ebe5..6aa1e0d7fa6 100644 --- a/homeassistant/components/vulcan/translations/id.json +++ b/homeassistant/components/vulcan/translations/id.json @@ -30,14 +30,6 @@ }, "description": "Masuk ke Akun Vulcan Anda menggunakan halaman pendaftaran aplikasi seluler." }, - "reauth": { - "data": { - "pin": "PIN", - "region": "Simbol", - "token": "Token" - }, - "description": "Masuk ke Akun Vulcan Anda menggunakan halaman pendaftaran aplikasi seluler." - }, "reauth_confirm": { "data": { "pin": "PIN", @@ -59,20 +51,5 @@ "description": "Pilih siswa, Anda dapat menambahkan lebih banyak siswa dengan menambahkan integrasi lagi." } } - }, - "options": { - "error": { - "error": "Terjadi kesalahan" - }, - "step": { - "init": { - "data": { - "attendance_notify": "Tampilkan notifikasi tentang entri kehadiran terbaru", - "grade_notify": "Tampilkan notifikasi tentang nilai terbaru", - "message_notify": "Tampilkan saat pesan baru diterima", - "scan_interval": "Interval pembaruan (dalam menit)" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/it.json b/homeassistant/components/vulcan/translations/it.json index 8fe26ddfba6..affc791af72 100644 --- a/homeassistant/components/vulcan/translations/it.json +++ b/homeassistant/components/vulcan/translations/it.json @@ -30,14 +30,6 @@ }, "description": "Accedi al tuo account Vulcan utilizzando la pagina di registrazione dell'applicazione mobile." }, - "reauth": { - "data": { - "pin": "PIN", - "region": "Simbolo", - "token": "Token" - }, - "description": "Accedi al tuo account Vulcan utilizzando la pagina di registrazione dell'applicazione mobile." - }, "reauth_confirm": { "data": { "pin": "PIN", @@ -59,20 +51,5 @@ "description": "Seleziona studente, puoi aggiungere pi\u00f9 studenti aggiungendo nuovamente l'integrazione." } } - }, - "options": { - "error": { - "error": "Si \u00e8 verificato un errore" - }, - "step": { - "init": { - "data": { - "attendance_notify": "Mostra le notifiche sulle ultime voci di partecipazione", - "grade_notify": "Mostra le notifiche sugli ultimi voti", - "message_notify": "Mostra le notifiche quando viene ricevuto un nuovo messaggio", - "scan_interval": "Intervallo di aggiornamento (in minuti)" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/ja.json b/homeassistant/components/vulcan/translations/ja.json index 2f83f44ab53..4f18d47b7ea 100644 --- a/homeassistant/components/vulcan/translations/ja.json +++ b/homeassistant/components/vulcan/translations/ja.json @@ -30,14 +30,6 @@ }, "description": "\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u767b\u9332\u30da\u30fc\u30b8\u3092\u4f7f\u7528\u3057\u3066\u3001Vulcan\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3059\u3002" }, - "reauth": { - "data": { - "pin": "\u30d4\u30f3", - "region": "\u30b7\u30f3\u30dc\u30eb", - "token": "\u30c8\u30fc\u30af\u30f3" - }, - "description": "\u30e2\u30d0\u30a4\u30eb\u30a2\u30d7\u30ea\u767b\u9332\u30da\u30fc\u30b8\u3092\u4f7f\u7528\u3057\u3066\u3001Vulcan\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3059\u3002" - }, "reauth_confirm": { "data": { "pin": "\u30d4\u30f3", @@ -59,20 +51,5 @@ "description": "\u751f\u5f92\u3092\u9078\u629e\u3057\u307e\u3059(Select student)\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u518d\u5ea6\u8ffd\u52a0\u3059\u308b\u3053\u3068\u3067\u3001\u3088\u308a\u591a\u304f\u306e\u751f\u5f92\u3092\u8ffd\u52a0\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002" } } - }, - "options": { - "error": { - "error": "\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f" - }, - "step": { - "init": { - "data": { - "attendance_notify": "\u6700\u65b0\u306e\u51fa\u5e2d\u30a8\u30f3\u30c8\u30ea\u306b\u95a2\u3059\u308b\u901a\u77e5\u3092\u8868\u793a\u3059\u308b", - "grade_notify": "\u6700\u65b0\u306e\u6210\u7e3e\u306b\u95a2\u3059\u308b\u901a\u77e5\u3092\u8868\u793a\u3059\u308b", - "message_notify": "\u65b0\u3057\u3044\u30e1\u30c3\u30bb\u30fc\u30b8\u306e\u53d7\u4fe1\u6642\u306b\u901a\u77e5\u3092\u8868\u793a\u3059\u308b", - "scan_interval": "\u66f4\u65b0\u9593\u9694(\u5206\u5358\u4f4d)" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/nl.json b/homeassistant/components/vulcan/translations/nl.json index df1990ce1a0..4b95ff00f0d 100644 --- a/homeassistant/components/vulcan/translations/nl.json +++ b/homeassistant/components/vulcan/translations/nl.json @@ -30,14 +30,6 @@ }, "description": "Log in op uw Vulcan-account via de registratiepagina voor mobiele apps." }, - "reauth": { - "data": { - "pin": "Pin", - "region": "Symbool", - "token": "Token" - }, - "description": "Log in op uw Vulcan-account via de registratiepagina voor mobiele apps." - }, "reauth_confirm": { "data": { "pin": "Pincode", @@ -59,20 +51,5 @@ "description": "Selecteer student, u kunt meer studenten toevoegen door integratie opnieuw toe te voegen." } } - }, - "options": { - "error": { - "error": "Er is een fout opgetreden." - }, - "step": { - "init": { - "data": { - "attendance_notify": "Toon meldingen over de laatste aanwezigheidsgegevens", - "grade_notify": "Toon meldingen over de laatste cijfers", - "message_notify": "Meldingen weergeven wanneer een nieuw bericht is ontvangen", - "scan_interval": "Update-interval (in minuten)" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/no.json b/homeassistant/components/vulcan/translations/no.json index 78444957ff1..8dafd8b80ba 100644 --- a/homeassistant/components/vulcan/translations/no.json +++ b/homeassistant/components/vulcan/translations/no.json @@ -30,14 +30,6 @@ }, "description": "Logg p\u00e5 Vulcan-kontoen din ved \u00e5 bruke registreringssiden for mobilappen." }, - "reauth": { - "data": { - "pin": "Pin", - "region": "Symbol", - "token": "Token" - }, - "description": "Logg p\u00e5 Vulcan-kontoen din ved \u00e5 bruke registreringssiden for mobilappen." - }, "reauth_confirm": { "data": { "pin": "Pin", @@ -59,20 +51,5 @@ "description": "Velg student, du kan legge til flere studenter ved \u00e5 legge til integrering p\u00e5 nytt." } } - }, - "options": { - "error": { - "error": "Det oppstod en feil" - }, - "step": { - "init": { - "data": { - "attendance_notify": "Vis varsler om de siste fremm\u00f8teoppf\u00f8ringene", - "grade_notify": "Vis varsler om de nyeste vurderingene", - "message_notify": "Vis varsler n\u00e5r ny melding mottas", - "scan_interval": "Oppdateringsintervall (i minutter)" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/pl.json b/homeassistant/components/vulcan/translations/pl.json index 220b05b6972..c8b1f4cf13a 100644 --- a/homeassistant/components/vulcan/translations/pl.json +++ b/homeassistant/components/vulcan/translations/pl.json @@ -30,14 +30,6 @@ }, "description": "Zaloguj si\u0119 do swojego konta Vulcan za pomoc\u0105 strony rejestracji aplikacji mobilnej." }, - "reauth": { - "data": { - "pin": "Kod pin", - "region": "Symbol", - "token": "Token" - }, - "description": "Zaloguj si\u0119 do swojego konta Vulcan za pomoc\u0105 strony rejestracji aplikacji mobilnej." - }, "reauth_confirm": { "data": { "pin": "Kod PIN", @@ -59,20 +51,5 @@ "description": "Wybierz ucznia. Mo\u017cesz doda\u0107 wi\u0119cej uczni\u00f3w, ponownie dodaj\u0105c integracj\u0119." } } - }, - "options": { - "error": { - "error": "Wyst\u0105pi\u0142 b\u0142\u0105d" - }, - "step": { - "init": { - "data": { - "attendance_notify": "Poka\u017c powiadomienia o najnowszych wpisach o obecno\u015bci", - "grade_notify": "Poka\u017c powiadomienia o najnowszych ocenach", - "message_notify": "Pokazuj powiadomienia po otrzymaniu nowej wiadomo\u015bci", - "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (w minutach)" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/pt-BR.json b/homeassistant/components/vulcan/translations/pt-BR.json index 98aaec3c33f..1413cccffc9 100644 --- a/homeassistant/components/vulcan/translations/pt-BR.json +++ b/homeassistant/components/vulcan/translations/pt-BR.json @@ -30,14 +30,6 @@ }, "description": "Fa\u00e7a login na sua conta Vulcan usando a p\u00e1gina de registro do aplicativo m\u00f3vel." }, - "reauth": { - "data": { - "pin": "Pin", - "region": "S\u00edmbolo", - "token": "Token" - }, - "description": "Fa\u00e7a login na sua conta Vulcan usando a p\u00e1gina de registro do aplicativo m\u00f3vel." - }, "reauth_confirm": { "data": { "pin": "C\u00f3digo PIN", @@ -59,20 +51,5 @@ "description": "Selecione aluno, voc\u00ea pode adicionar mais alunos adicionando integra\u00e7\u00e3o novamente." } } - }, - "options": { - "error": { - "error": "Ocorreu um erro" - }, - "step": { - "init": { - "data": { - "attendance_notify": "Mostrar notifica\u00e7\u00f5es sobre as entradas de participa\u00e7\u00e3o mais recentes", - "grade_notify": "Mostrar notifica\u00e7\u00f5es sobre as notas mais recentes", - "message_notify": "Mostrar notifica\u00e7\u00f5es quando uma nova mensagem for recebida", - "scan_interval": "Intervalo de atualiza\u00e7\u00e3o (em minutos)" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/ru.json b/homeassistant/components/vulcan/translations/ru.json index 5ce94bd932f..7692a97fc63 100644 --- a/homeassistant/components/vulcan/translations/ru.json +++ b/homeassistant/components/vulcan/translations/ru.json @@ -30,14 +30,6 @@ }, "description": "\u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Vulcan, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f." }, - "reauth": { - "data": { - "pin": "PIN-\u043a\u043e\u0434", - "region": "\u0421\u0438\u043c\u0432\u043e\u043b", - "token": "\u0422\u043e\u043a\u0435\u043d" - }, - "description": "\u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 \u0441\u0432\u043e\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Vulcan, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f." - }, "reauth_confirm": { "data": { "pin": "PIN-\u043a\u043e\u0434", @@ -59,20 +51,5 @@ "description": "\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0431\u043e\u043b\u044c\u0448\u0435 \u0441\u0442\u0443\u0434\u0435\u043d\u0442\u043e\u0432, \u0441\u043d\u043e\u0432\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u0432 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e." } } - }, - "options": { - "error": { - "error": "\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430." - }, - "step": { - "init": { - "data": { - "attendance_notify": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f \u043e \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0445 \u0437\u0430\u043f\u0438\u0441\u044f\u0445 \u043e \u043f\u043e\u0441\u0435\u0449\u0430\u0435\u043c\u043e\u0441\u0442\u0438", - "grade_notify": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f \u043e \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0445 \u043e\u0446\u0435\u043d\u043a\u0430\u0445", - "message_notify": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f \u043e \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u043d\u043e\u0432\u043e\u0433\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f", - "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/tr.json b/homeassistant/components/vulcan/translations/tr.json index e103d8626ab..6d79e4d80a1 100644 --- a/homeassistant/components/vulcan/translations/tr.json +++ b/homeassistant/components/vulcan/translations/tr.json @@ -30,14 +30,6 @@ }, "description": "Mobil uygulama kay\u0131t sayfas\u0131n\u0131 kullanarak Vulcan Hesab\u0131n\u0131za giri\u015f yap\u0131n." }, - "reauth": { - "data": { - "pin": "Pin", - "region": "Sembol", - "token": "Anahtar" - }, - "description": "Mobil uygulama kay\u0131t sayfas\u0131n\u0131 kullanarak Vulcan Hesab\u0131n\u0131za giri\u015f yap\u0131n." - }, "reauth_confirm": { "data": { "pin": "Pin", @@ -59,20 +51,5 @@ "description": "\u00d6\u011frenciyi se\u00e7in, yeniden t\u00fcmle\u015ftirme ekleyerek daha fazla \u00f6\u011frenci ekleyebilirsiniz." } } - }, - "options": { - "error": { - "error": "Hata olu\u015ftu" - }, - "step": { - "init": { - "data": { - "attendance_notify": "En son kat\u0131l\u0131m giri\u015fleriyle ilgili bildirimleri g\u00f6ster", - "grade_notify": "En son notlarla ilgili bildirimleri g\u00f6ster", - "message_notify": "Yeni mesaj al\u0131nd\u0131\u011f\u0131nda bildirimleri g\u00f6ster", - "scan_interval": "G\u00fcncelleme aral\u0131\u011f\u0131 (dakika olarak)" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/zh-Hant.json b/homeassistant/components/vulcan/translations/zh-Hant.json index b26ef1f5cc5..24adfff6e02 100644 --- a/homeassistant/components/vulcan/translations/zh-Hant.json +++ b/homeassistant/components/vulcan/translations/zh-Hant.json @@ -30,14 +30,6 @@ }, "description": "\u4f7f\u7528\u884c\u52d5 App \u8a3b\u518a\u9801\u9762\u767b\u5165 Vulcan \u5e33\u865f\u3002" }, - "reauth": { - "data": { - "pin": "Pin", - "region": "\u7b26\u865f", - "token": "\u6b0a\u6756" - }, - "description": "\u4f7f\u7528\u884c\u52d5 App \u8a3b\u518a\u9801\u9762\u767b\u5165 Vulcan \u5e33\u865f\u3002" - }, "reauth_confirm": { "data": { "pin": "Pin", @@ -59,20 +51,5 @@ "description": "\u9078\u64c7\u5b78\u751f\u3001\u53ef\u4ee5\u518d\u900f\u904e\u65b0\u589e\u6574\u5408\u65b0\u589e\u5176\u4ed6\u5b78\u751f\u3002" } } - }, - "options": { - "error": { - "error": "\u767c\u751f\u932f\u8aa4" - }, - "step": { - "init": { - "data": { - "attendance_notify": "\u95dc\u65bc\u6700\u65b0\u51fa\u52e4\u5be6\u9ad4\u986f\u793a\u901a\u77e5", - "grade_notify": "\u95dc\u65bc\u6700\u65b0\u6210\u7e3e\u986f\u793a\u901a\u77e5", - "message_notify": "\u7576\u6536\u5230\u65b0\u8a0a\u606f\u6642\u986f\u793a\u901a\u77e5", - "scan_interval": "\u66f4\u65b0\u983b\u7387\uff08\u5206\uff09" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/webostv/translations/ko.json b/homeassistant/components/webostv/translations/ko.json new file mode 100644 index 00000000000..e79ddbd7b3f --- /dev/null +++ b/homeassistant/components/webostv/translations/ko.json @@ -0,0 +1,47 @@ +{ + "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", + "error_pairing": "LG webOS TV\uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc9c0\ub9cc \ud398\uc5b4\ub9c1\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. TV\ub97c \ucf1c\uac70\ub098 IP \uc8fc\uc18c\ub97c \ud655\uc778\ud558\uc138\uc694." + }, + "flow_title": "LG webOS \uc2a4\ub9c8\ud2b8 TV", + "step": { + "pairing": { + "description": "\ud655\uc778\uc744 \ub204\ub974\uace0 TV\uc5d0\uc11c \ud398\uc5b4\ub9c1 \uc694\uccad\uc744 \uc218\ub77d\ud558\uc138\uc694.\n\n![Image](/static/images/config_webos.png)", + "title": "webOS TV \ud398\uc5b4\ub9c1" + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "name": "\uc774\ub984" + }, + "description": "TV\ub97c \ucf1c\uace0 \ub2e4\uc74c \ud544\ub4dc\ub97c \ucc44\uc6b0\uace0 \uc81c\ucd9c\uc744 \ud074\ub9ad\ud558\uc138\uc694.", + "title": "webOS TV\uc5d0 \uc5f0\uacb0" + } + } + }, + "device_automation": { + "trigger_type": { + "webostv.turn_on": "\uae30\uae30\uac00 \ucf1c\uc9c0\ub3c4\ub85d \uc694\uccad\ub418\uc5c8\uc744 \ub54c" + } + }, + "options": { + "error": { + "cannot_retrieve": "\uc18c\uc2a4 \ubaa9\ub85d\uc744 \uac80\uc0c9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc7a5\uce58\uac00 \ucf1c\uc838 \uc788\ub294\uc9c0 \ud655\uc778\ud558\uc138\uc694", + "script_not_found": "\uc2a4\ud06c\ub9bd\ud2b8\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc74c" + }, + "step": { + "init": { + "data": { + "sources": "\uc18c\uc2a4 \ubaa9\ub85d" + }, + "description": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c \uc18c\uc2a4 \uc120\ud0dd", + "title": "webOS \uc2a4\ub9c8\ud2b8 TV \uc635\uc158" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/ar.json b/homeassistant/components/wilight/translations/ar.json index 033cc81d349..f6b22194fe1 100644 --- a/homeassistant/components/wilight/translations/ar.json +++ b/homeassistant/components/wilight/translations/ar.json @@ -7,8 +7,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "\u0647\u0644 \u062a\u0631\u064a\u062f \u0625\u0639\u062f\u0627\u062f WiLight {name} \u061f \n\n \u064a\u062f\u0639\u0645: {components}", - "title": "WiLight" + "description": "\u0647\u0644 \u062a\u0631\u064a\u062f \u0625\u0639\u062f\u0627\u062f WiLight {name} \u061f \n\n \u064a\u062f\u0639\u0645: {components}" } } } diff --git a/homeassistant/components/wilight/translations/ca.json b/homeassistant/components/wilight/translations/ca.json index 22044549b0c..e9acff0a91e 100644 --- a/homeassistant/components/wilight/translations/ca.json +++ b/homeassistant/components/wilight/translations/ca.json @@ -8,8 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "S'admeten els seg\u00fcents components: {components}", - "title": "WiLight" + "description": "S'admeten els seg\u00fcents components: {components}" } } } diff --git a/homeassistant/components/wilight/translations/cs.json b/homeassistant/components/wilight/translations/cs.json index 0903e0e3a65..bd61d73667a 100644 --- a/homeassistant/components/wilight/translations/cs.json +++ b/homeassistant/components/wilight/translations/cs.json @@ -8,8 +8,7 @@ "flow_title": "WiLight: {name}", "step": { "confirm": { - "description": "Chcete nastavit WiLight {name} ? \n\n Podporuje: {components}", - "title": "WiLight" + "description": "Chcete nastavit WiLight {name} ? \n\n Podporuje: {components}" } } } diff --git a/homeassistant/components/wilight/translations/de.json b/homeassistant/components/wilight/translations/de.json index 27d48a90e58..84fcf14c2a4 100644 --- a/homeassistant/components/wilight/translations/de.json +++ b/homeassistant/components/wilight/translations/de.json @@ -8,8 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Die folgenden Komponenten werden unterst\u00fctzt: {components}", - "title": "WiLight" + "description": "Die folgenden Komponenten werden unterst\u00fctzt: {components}" } } } diff --git a/homeassistant/components/wilight/translations/el.json b/homeassistant/components/wilight/translations/el.json index 5bb3130753a..79bb1d443c4 100644 --- a/homeassistant/components/wilight/translations/el.json +++ b/homeassistant/components/wilight/translations/el.json @@ -8,8 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf WiLight {name} ;\n\n \u03a5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9: {components}", - "title": "WiLight" + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf WiLight {name} ;\n\n \u03a5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9: {components}" } } } diff --git a/homeassistant/components/wilight/translations/en.json b/homeassistant/components/wilight/translations/en.json index 1f0733d28f3..01f80fc6e85 100644 --- a/homeassistant/components/wilight/translations/en.json +++ b/homeassistant/components/wilight/translations/en.json @@ -8,8 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "The following components are supported: {components}", - "title": "WiLight" + "description": "The following components are supported: {components}" } } } diff --git a/homeassistant/components/wilight/translations/es.json b/homeassistant/components/wilight/translations/es.json index 23dfdfcc728..5836867c0e7 100644 --- a/homeassistant/components/wilight/translations/es.json +++ b/homeassistant/components/wilight/translations/es.json @@ -8,8 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Se admiten los siguientes componentes: {componentes}", - "title": "WiLight" + "description": "Se admiten los siguientes componentes: {componentes}" } } } diff --git a/homeassistant/components/wilight/translations/et.json b/homeassistant/components/wilight/translations/et.json index dae085d1084..f21fb75e796 100644 --- a/homeassistant/components/wilight/translations/et.json +++ b/homeassistant/components/wilight/translations/et.json @@ -8,8 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Toetatud on j\u00e4rgmised komponendid: {components}", - "title": "" + "description": "Toetatud on j\u00e4rgmised komponendid: {components}" } } } diff --git a/homeassistant/components/wilight/translations/fr.json b/homeassistant/components/wilight/translations/fr.json index ddd85a5cc04..2fc169c5891 100644 --- a/homeassistant/components/wilight/translations/fr.json +++ b/homeassistant/components/wilight/translations/fr.json @@ -8,8 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Les composants suivants sont pris en charge\u00a0: {components}", - "title": "WiLight" + "description": "Les composants suivants sont pris en charge\u00a0: {components}" } } } diff --git a/homeassistant/components/wilight/translations/he.json b/homeassistant/components/wilight/translations/he.json index 977167ec765..48ae2e01df1 100644 --- a/homeassistant/components/wilight/translations/he.json +++ b/homeassistant/components/wilight/translations/he.json @@ -3,11 +3,6 @@ "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": { - "title": "WiLight" - } - } + "flow_title": "{name}" } } \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/hu.json b/homeassistant/components/wilight/translations/hu.json index 02db2f1b7df..bae04810d43 100644 --- a/homeassistant/components/wilight/translations/hu.json +++ b/homeassistant/components/wilight/translations/hu.json @@ -8,8 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "A k\u00f6vetkez\u0151 komponensek t\u00e1mogatottak: {components}", - "title": "WiLight" + "description": "A k\u00f6vetkez\u0151 komponensek t\u00e1mogatottak: {components}" } } } diff --git a/homeassistant/components/wilight/translations/id.json b/homeassistant/components/wilight/translations/id.json index 0489a0e96bb..1597c4a2c72 100644 --- a/homeassistant/components/wilight/translations/id.json +++ b/homeassistant/components/wilight/translations/id.json @@ -8,8 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Komponen berikut didukung: {components}", - "title": "WiLight" + "description": "Komponen berikut didukung: {components}" } } } diff --git a/homeassistant/components/wilight/translations/it.json b/homeassistant/components/wilight/translations/it.json index 7b560b7bfdb..3f425fafbb1 100644 --- a/homeassistant/components/wilight/translations/it.json +++ b/homeassistant/components/wilight/translations/it.json @@ -8,8 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Sono supportati i seguenti componenti: {components}", - "title": "WiLight" + "description": "Sono supportati i seguenti componenti: {components}" } } } diff --git a/homeassistant/components/wilight/translations/ja.json b/homeassistant/components/wilight/translations/ja.json index 0a01f69d43a..1f09be66bf1 100644 --- a/homeassistant/components/wilight/translations/ja.json +++ b/homeassistant/components/wilight/translations/ja.json @@ -8,8 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "WiLight {name} \u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f\n\n \u5bfe\u5fdc\u3057\u3066\u3044\u308b: {components}", - "title": "WiLight" + "description": "WiLight {name} \u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f\n\n \u5bfe\u5fdc\u3057\u3066\u3044\u308b: {components}" } } } diff --git a/homeassistant/components/wilight/translations/ko.json b/homeassistant/components/wilight/translations/ko.json index 1b53a1ba544..ef740d1ca2c 100644 --- a/homeassistant/components/wilight/translations/ko.json +++ b/homeassistant/components/wilight/translations/ko.json @@ -8,8 +8,7 @@ "flow_title": "WiLight: {name}", "step": { "confirm": { - "description": "WiLight {name}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?\n\n\uc9c0\uc6d0 \uae30\uae30: {components}", - "title": "WiLight" + "description": "WiLight {name}\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?\n\n\uc9c0\uc6d0 \uae30\uae30: {components}" } } } diff --git a/homeassistant/components/wilight/translations/lb.json b/homeassistant/components/wilight/translations/lb.json index 2c0b831098e..002fb238482 100644 --- a/homeassistant/components/wilight/translations/lb.json +++ b/homeassistant/components/wilight/translations/lb.json @@ -8,8 +8,7 @@ "flow_title": "Wilight: {name}", "step": { "confirm": { - "description": "Soll WiLight {name} konfigur\u00e9iert ginn?\n\nEt \u00ebnnerst\u00ebtzt: {components}", - "title": "WiLight" + "description": "Soll WiLight {name} konfigur\u00e9iert ginn?\n\nEt \u00ebnnerst\u00ebtzt: {components}" } } } diff --git a/homeassistant/components/wilight/translations/nl.json b/homeassistant/components/wilight/translations/nl.json index 51cefe8ce28..c1afd57e34d 100644 --- a/homeassistant/components/wilight/translations/nl.json +++ b/homeassistant/components/wilight/translations/nl.json @@ -8,8 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "De volgende componenten worden ondersteund: {components}", - "title": "WiLight" + "description": "De volgende componenten worden ondersteund: {components}" } } } diff --git a/homeassistant/components/wilight/translations/no.json b/homeassistant/components/wilight/translations/no.json index 582b2289a32..aacb91e5638 100644 --- a/homeassistant/components/wilight/translations/no.json +++ b/homeassistant/components/wilight/translations/no.json @@ -8,8 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "F\u00f8lgende komponenter st\u00f8ttes: {components}", - "title": "" + "description": "F\u00f8lgende komponenter st\u00f8ttes: {components}" } } } diff --git a/homeassistant/components/wilight/translations/pl.json b/homeassistant/components/wilight/translations/pl.json index c1f636b9d80..3880698b61c 100644 --- a/homeassistant/components/wilight/translations/pl.json +++ b/homeassistant/components/wilight/translations/pl.json @@ -8,8 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Obs\u0142ugiwane s\u0105 nast\u0119puj\u0105ce komponenty: {components}", - "title": "WiLight" + "description": "Obs\u0142ugiwane s\u0105 nast\u0119puj\u0105ce komponenty: {components}" } } } diff --git a/homeassistant/components/wilight/translations/pt-BR.json b/homeassistant/components/wilight/translations/pt-BR.json index e70a286e812..48678c2d850 100644 --- a/homeassistant/components/wilight/translations/pt-BR.json +++ b/homeassistant/components/wilight/translations/pt-BR.json @@ -8,8 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Os seguintes componentes s\u00e3o compat\u00edveis: {components}", - "title": "WiLight" + "description": "Os seguintes componentes s\u00e3o compat\u00edveis: {components}" } } } diff --git a/homeassistant/components/wilight/translations/pt.json b/homeassistant/components/wilight/translations/pt.json index 5b1005a95f5..7604ea73337 100644 --- a/homeassistant/components/wilight/translations/pt.json +++ b/homeassistant/components/wilight/translations/pt.json @@ -8,8 +8,7 @@ "flow_title": "WiLight: {name}", "step": { "confirm": { - "description": "Deseja configurar o WiLight {name} ? \n\n Suporta: {components}", - "title": "WiLight" + "description": "Deseja configurar o WiLight {name} ? \n\n Suporta: {components}" } } } diff --git a/homeassistant/components/wilight/translations/ru.json b/homeassistant/components/wilight/translations/ru.json index 7873b8ca326..afe612cd77e 100644 --- a/homeassistant/components/wilight/translations/ru.json +++ b/homeassistant/components/wilight/translations/ru.json @@ -8,8 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b: {components}.", - "title": "WiLight" + "description": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b: {components}." } } } diff --git a/homeassistant/components/wilight/translations/tr.json b/homeassistant/components/wilight/translations/tr.json index 4762a678254..012326dc9df 100644 --- a/homeassistant/components/wilight/translations/tr.json +++ b/homeassistant/components/wilight/translations/tr.json @@ -8,8 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "A\u015fa\u011f\u0131daki bile\u015fenler desteklenir: {components}", - "title": "WiLight" + "description": "A\u015fa\u011f\u0131daki bile\u015fenler desteklenir: {components}" } } } diff --git a/homeassistant/components/wilight/translations/uk.json b/homeassistant/components/wilight/translations/uk.json index 7517538499e..f06c8b51d51 100644 --- a/homeassistant/components/wilight/translations/uk.json +++ b/homeassistant/components/wilight/translations/uk.json @@ -8,8 +8,7 @@ "flow_title": "WiLight: {name}", "step": { "confirm": { - "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 WiLight {name}? \n\n \u0426\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454: {components}", - "title": "WiLight" + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 WiLight {name}? \n\n \u0426\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454: {components}" } } } diff --git a/homeassistant/components/wilight/translations/zh-Hant.json b/homeassistant/components/wilight/translations/zh-Hant.json index 70a9bff0550..f3f7d9439a9 100644 --- a/homeassistant/components/wilight/translations/zh-Hant.json +++ b/homeassistant/components/wilight/translations/zh-Hant.json @@ -8,8 +8,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "\u652f\u63f4\u4ee5\u4e0b\u5143\u4ef6\uff1a{components}", - "title": "WiLight" + "description": "\u652f\u63f4\u4ee5\u4e0b\u5143\u4ef6\uff1a{components}" } } } diff --git a/homeassistant/components/wiz/translations/bg.json b/homeassistant/components/wiz/translations/bg.json index eb0697ca13d..059a8fb9358 100644 --- a/homeassistant/components/wiz/translations/bg.json +++ b/homeassistant/components/wiz/translations/bg.json @@ -20,8 +20,7 @@ }, "user": { "data": { - "host": "\u0425\u043e\u0441\u0442", - "name": "\u0418\u043c\u0435" + "host": "\u0425\u043e\u0441\u0442" } } } diff --git a/homeassistant/components/wiz/translations/ca.json b/homeassistant/components/wiz/translations/ca.json index 60ca2a8a884..157f2e21dff 100644 --- a/homeassistant/components/wiz/translations/ca.json +++ b/homeassistant/components/wiz/translations/ca.json @@ -14,9 +14,6 @@ }, "flow_title": "{name} ({host})", "step": { - "confirm": { - "description": "Vols comen\u00e7ar la configuraci\u00f3?" - }, "discovery_confirm": { "description": "Vols configurar {name} ({host})?" }, @@ -27,8 +24,7 @@ }, "user": { "data": { - "host": "Adre\u00e7a IP", - "name": "Nom" + "host": "Adre\u00e7a IP" }, "description": "Si deixes l'adre\u00e7a IP buida, s'utilitzar\u00e0 el descobriment per cercar dispositius." } diff --git a/homeassistant/components/wiz/translations/cs.json b/homeassistant/components/wiz/translations/cs.json index 0656e47c8be..b6b916d5d3a 100644 --- a/homeassistant/components/wiz/translations/cs.json +++ b/homeassistant/components/wiz/translations/cs.json @@ -10,13 +10,9 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { - "confirm": { - "description": "Chcete za\u010d\u00edt nastavovat?" - }, "user": { "data": { - "host": "IP adresa", - "name": "Jm\u00e9no" + "host": "IP adresa" } } } diff --git a/homeassistant/components/wiz/translations/de.json b/homeassistant/components/wiz/translations/de.json index 73b933fefcf..af503506c59 100644 --- a/homeassistant/components/wiz/translations/de.json +++ b/homeassistant/components/wiz/translations/de.json @@ -14,9 +14,6 @@ }, "flow_title": "{name} ({host})", "step": { - "confirm": { - "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" - }, "discovery_confirm": { "description": "M\u00f6chtest du {name} ({host}) einrichten?" }, @@ -27,8 +24,7 @@ }, "user": { "data": { - "host": "IP-Adresse", - "name": "Name" + "host": "IP-Adresse" }, "description": "Wenn du die IP-Adresse leer l\u00e4sst, wird die Erkennung verwendet, um Ger\u00e4te zu finden." } diff --git a/homeassistant/components/wiz/translations/el.json b/homeassistant/components/wiz/translations/el.json index aa137e84fe0..c92d036d5f6 100644 --- a/homeassistant/components/wiz/translations/el.json +++ b/homeassistant/components/wiz/translations/el.json @@ -14,9 +14,6 @@ }, "flow_title": "{name} ({host})", "step": { - "confirm": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7;" - }, "discovery_confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ({host});" }, @@ -27,8 +24,7 @@ }, "user": { "data": { - "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" }, "description": "\u0391\u03bd \u03b1\u03c6\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ba\u03b5\u03bd\u03ae, \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd." } diff --git a/homeassistant/components/wiz/translations/en.json b/homeassistant/components/wiz/translations/en.json index a612ea165a4..97bb7fc25ba 100644 --- a/homeassistant/components/wiz/translations/en.json +++ b/homeassistant/components/wiz/translations/en.json @@ -14,9 +14,6 @@ }, "flow_title": "{name} ({host})", "step": { - "confirm": { - "description": "Do you want to start set up?" - }, "discovery_confirm": { "description": "Do you want to setup {name} ({host})?" }, @@ -27,8 +24,7 @@ }, "user": { "data": { - "host": "IP Address", - "name": "Name" + "host": "IP Address" }, "description": "If you leave the IP Address empty, discovery will be used to find devices." } diff --git a/homeassistant/components/wiz/translations/es.json b/homeassistant/components/wiz/translations/es.json index bc06bff8053..b86f60649a2 100644 --- a/homeassistant/components/wiz/translations/es.json +++ b/homeassistant/components/wiz/translations/es.json @@ -14,9 +14,6 @@ }, "flow_title": "{name} ({host})", "step": { - "confirm": { - "description": "\u00bfDesea iniciar la configuraci\u00f3n?" - }, "discovery_confirm": { "description": "\u00bfDesea configurar {name} ({host})?" }, @@ -27,8 +24,7 @@ }, "user": { "data": { - "host": "Direcci\u00f3n IP", - "name": "Nombre" + "host": "Direcci\u00f3n IP" }, "description": "Si deja la direcci\u00f3n IP vac\u00eda, la detecci\u00f3n se utilizar\u00e1 para buscar dispositivos." } diff --git a/homeassistant/components/wiz/translations/et.json b/homeassistant/components/wiz/translations/et.json index 9cb84a0e7bd..e220b5beb9f 100644 --- a/homeassistant/components/wiz/translations/et.json +++ b/homeassistant/components/wiz/translations/et.json @@ -14,9 +14,6 @@ }, "flow_title": "{name} ({host})", "step": { - "confirm": { - "description": "Kas alustan seadistamist?" - }, "discovery_confirm": { "description": "Kas soovid seadistada {name}({host})?" }, @@ -27,8 +24,7 @@ }, "user": { "data": { - "host": "IP aadress", - "name": "Nimi" + "host": "IP aadress" }, "description": "Kui j\u00e4tad IP aadressi t\u00fchjaks kasutatakse seadmete leidmiseks avastamist." } diff --git a/homeassistant/components/wiz/translations/fr.json b/homeassistant/components/wiz/translations/fr.json index 290bda2405c..058e751242d 100644 --- a/homeassistant/components/wiz/translations/fr.json +++ b/homeassistant/components/wiz/translations/fr.json @@ -14,9 +14,6 @@ }, "flow_title": "{name} ({host})", "step": { - "confirm": { - "description": "Voulez-vous commencer la configuration\u00a0?" - }, "discovery_confirm": { "description": "Voulez-vous configurer {name} ({host})\u00a0?" }, @@ -27,8 +24,7 @@ }, "user": { "data": { - "host": "Adresse IP", - "name": "Nom" + "host": "Adresse IP" }, "description": "Si vous laissez l'adresse IP vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des appareils." } diff --git a/homeassistant/components/wiz/translations/he.json b/homeassistant/components/wiz/translations/he.json index 8d4e41401c8..81b954067f7 100644 --- a/homeassistant/components/wiz/translations/he.json +++ b/homeassistant/components/wiz/translations/he.json @@ -11,9 +11,6 @@ }, "flow_title": "{name} ({host})", "step": { - "confirm": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" - }, "discovery_confirm": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name} ({host})?" }, @@ -24,8 +21,7 @@ }, "user": { "data": { - "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", - "name": "\u05e9\u05dd" + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP" } } } diff --git a/homeassistant/components/wiz/translations/hu.json b/homeassistant/components/wiz/translations/hu.json index 63ae8479bb9..0c27ee730b3 100644 --- a/homeassistant/components/wiz/translations/hu.json +++ b/homeassistant/components/wiz/translations/hu.json @@ -14,9 +14,6 @@ }, "flow_title": "{name} ({host})", "step": { - "confirm": { - "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" - }, "discovery_confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({host})?" }, @@ -27,8 +24,7 @@ }, "user": { "data": { - "host": "IP c\u00edm", - "name": "Elnevez\u00e9s" + "host": "IP c\u00edm" }, "description": "Ha az IP-c\u00edmet \u00fcresen hagyja, akkor az eszk\u00f6z\u00f6k keres\u00e9se a felder\u00edt\u00e9ssel t\u00f6rt\u00e9nik." } diff --git a/homeassistant/components/wiz/translations/id.json b/homeassistant/components/wiz/translations/id.json index 694973f8ffa..2455999d1fd 100644 --- a/homeassistant/components/wiz/translations/id.json +++ b/homeassistant/components/wiz/translations/id.json @@ -14,9 +14,6 @@ }, "flow_title": "{name} ({host})", "step": { - "confirm": { - "description": "Ingin memulai penyiapan?" - }, "discovery_confirm": { "description": "Ingin menyiapkan {name} ({host})?" }, @@ -27,8 +24,7 @@ }, "user": { "data": { - "host": "Alamat IP", - "name": "Nama" + "host": "Alamat IP" }, "description": "Jika Alamat IP dibiarkan kosong, proses penemuan akan digunakan untuk menemukan perangkat." } diff --git a/homeassistant/components/wiz/translations/it.json b/homeassistant/components/wiz/translations/it.json index ebd093c22f7..ae87f88a151 100644 --- a/homeassistant/components/wiz/translations/it.json +++ b/homeassistant/components/wiz/translations/it.json @@ -14,9 +14,6 @@ }, "flow_title": "{name} ({host})", "step": { - "confirm": { - "description": "Vuoi iniziare la configurazione?" - }, "discovery_confirm": { "description": "Vuoi configurare {name} ({host})?" }, @@ -27,8 +24,7 @@ }, "user": { "data": { - "host": "Indirizzo IP", - "name": "Nome" + "host": "Indirizzo IP" }, "description": "Se lasci vuoto l'indirizzo IP, il rilevamento sar\u00e0 utilizzato per trovare i dispositivi." } diff --git a/homeassistant/components/wiz/translations/ja.json b/homeassistant/components/wiz/translations/ja.json index 8d9cbd0c055..e062fbdb75b 100644 --- a/homeassistant/components/wiz/translations/ja.json +++ b/homeassistant/components/wiz/translations/ja.json @@ -14,9 +14,6 @@ }, "flow_title": "{name} ({host})", "step": { - "confirm": { - "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" - }, "discovery_confirm": { "description": "{name} ({host})\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, @@ -27,8 +24,7 @@ }, "user": { "data": { - "host": "\u30db\u30b9\u30c8", - "name": "\u540d\u524d" + "host": "\u30db\u30b9\u30c8" }, "description": "\u65b0\u3057\u3044\u96fb\u7403(bulb)\u3092\u8ffd\u52a0\u3059\u308b\u306b\u306f\u3001\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9\u3068\u540d\u524d\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:" } diff --git a/homeassistant/components/wiz/translations/nl.json b/homeassistant/components/wiz/translations/nl.json index 3b59dd6a672..3a8d7be6ec6 100644 --- a/homeassistant/components/wiz/translations/nl.json +++ b/homeassistant/components/wiz/translations/nl.json @@ -14,9 +14,6 @@ }, "flow_title": "{name} ({host})", "step": { - "confirm": { - "description": "Wilt u beginnen met instellen?" - }, "discovery_confirm": { "description": "Wilt u {name} ({host}) instellen?" }, @@ -27,8 +24,7 @@ }, "user": { "data": { - "host": "IP-adres", - "name": "Naam" + "host": "IP-adres" }, "description": "Als u het IP-adres leeg laat, zal discovery worden gebruikt om apparaten te vinden." } diff --git a/homeassistant/components/wiz/translations/no.json b/homeassistant/components/wiz/translations/no.json index 7f2090b5b71..031b184ae84 100644 --- a/homeassistant/components/wiz/translations/no.json +++ b/homeassistant/components/wiz/translations/no.json @@ -14,9 +14,6 @@ }, "flow_title": "{name} ({host})", "step": { - "confirm": { - "description": "Vil du starte oppsettet?" - }, "discovery_confirm": { "description": "Vil du konfigurere {name} ({host})?" }, @@ -27,8 +24,7 @@ }, "user": { "data": { - "host": "IP adresse", - "name": "Navn" + "host": "IP adresse" }, "description": "Hvis du lar IP-adressen st\u00e5 tom, vil oppdagelse bli brukt til \u00e5 finne enheter." } diff --git a/homeassistant/components/wiz/translations/pl.json b/homeassistant/components/wiz/translations/pl.json index 30d1455c470..ea3ea48868c 100644 --- a/homeassistant/components/wiz/translations/pl.json +++ b/homeassistant/components/wiz/translations/pl.json @@ -14,9 +14,6 @@ }, "flow_title": "{name} ({host})", "step": { - "confirm": { - "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" - }, "discovery_confirm": { "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?" }, @@ -27,8 +24,7 @@ }, "user": { "data": { - "host": "Adres IP", - "name": "Nazwa" + "host": "Adres IP" }, "description": "Je\u015bli nie podasz adresu IP, zostanie u\u017cyte wykrywanie do odnalezienia urz\u0105dze\u0144." } diff --git a/homeassistant/components/wiz/translations/pt-BR.json b/homeassistant/components/wiz/translations/pt-BR.json index ce27ea82abc..0c2ee8b7c04 100644 --- a/homeassistant/components/wiz/translations/pt-BR.json +++ b/homeassistant/components/wiz/translations/pt-BR.json @@ -14,9 +14,6 @@ }, "flow_title": "{name} ({host})", "step": { - "confirm": { - "description": "Deseja iniciar a configura\u00e7\u00e3o?" - }, "discovery_confirm": { "description": "Deseja configurar {name} ({host})?" }, @@ -27,8 +24,7 @@ }, "user": { "data": { - "host": "Endere\u00e7o IP", - "name": "Nome" + "host": "Endere\u00e7o IP" }, "description": "Se voc\u00ea deixar o endere\u00e7o IP vazio, a descoberta ser\u00e1 usada para localizar dispositivos." } diff --git a/homeassistant/components/wiz/translations/ru.json b/homeassistant/components/wiz/translations/ru.json index dd422f29fea..c9fe68b3ce9 100644 --- a/homeassistant/components/wiz/translations/ru.json +++ b/homeassistant/components/wiz/translations/ru.json @@ -14,9 +14,6 @@ }, "flow_title": "{name} ({host})", "step": { - "confirm": { - "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" - }, "discovery_confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?" }, @@ -27,8 +24,7 @@ }, "user": { "data": { - "host": "IP-\u0430\u0434\u0440\u0435\u0441", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + "host": "IP-\u0430\u0434\u0440\u0435\u0441" }, "description": "\u0415\u0441\u043b\u0438 \u043d\u0435 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441, \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/wiz/translations/sk.json b/homeassistant/components/wiz/translations/sk.json index f97ee8e1837..641bd9c13ee 100644 --- a/homeassistant/components/wiz/translations/sk.json +++ b/homeassistant/components/wiz/translations/sk.json @@ -2,13 +2,6 @@ "config": { "abort": { "cannot_connect": "Nepodarilo sa pripoji\u0165" - }, - "step": { - "user": { - "data": { - "name": "N\u00e1zov" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/tr.json b/homeassistant/components/wiz/translations/tr.json index 3f6b1f68dc5..15b2b683a50 100644 --- a/homeassistant/components/wiz/translations/tr.json +++ b/homeassistant/components/wiz/translations/tr.json @@ -14,9 +14,6 @@ }, "flow_title": "{name} ({host})", "step": { - "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" - }, "discovery_confirm": { "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?" }, @@ -27,8 +24,7 @@ }, "user": { "data": { - "host": "IP Adresi", - "name": "Ad" + "host": "IP Adresi" }, "description": "IP Adresini bo\u015f b\u0131rak\u0131rsan\u0131z, cihazlar\u0131 bulmak i\u00e7in ke\u015fif kullan\u0131lacakt\u0131r." } diff --git a/homeassistant/components/wiz/translations/zh-Hant.json b/homeassistant/components/wiz/translations/zh-Hant.json index 9e716b4928e..3bce0700569 100644 --- a/homeassistant/components/wiz/translations/zh-Hant.json +++ b/homeassistant/components/wiz/translations/zh-Hant.json @@ -14,9 +14,6 @@ }, "flow_title": "{name} ({host})", "step": { - "confirm": { - "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" - }, "discovery_confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f" }, @@ -27,8 +24,7 @@ }, "user": { "data": { - "host": "IP \u4f4d\u5740", - "name": "\u540d\u7a31" + "host": "IP \u4f4d\u5740" }, "description": "\u5047\u5982 IP \u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u641c\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" } diff --git a/homeassistant/components/ws66i/translations/he.json b/homeassistant/components/ws66i/translations/he.json new file mode 100644 index 00000000000..fa770b28bf4 --- /dev/null +++ b/homeassistant/components/ws66i/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "ip_address": "\u05db\u05ea\u05d5\u05d1\u05ea IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/ca.json b/homeassistant/components/xiaomi_aqara/translations/ca.json index 78f5affd556..fe0e0d7c096 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ca.json +++ b/homeassistant/components/xiaomi_aqara/translations/ca.json @@ -18,8 +18,7 @@ "data": { "select_ip": "Adre\u00e7a IP" }, - "description": "Selecciona la passarel\u00b7la Xiaomi Aqara a la qual connectar-te", - "title": "Selecciona la passarel\u00b7la Xiaomi Aqara a la qual connectar-te" + "description": "Selecciona la passarel\u00b7la Xiaomi Aqara a la qual connectar-te" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "Interf\u00edcie de xarxa a utilitzar", "mac": "Adre\u00e7a MAC (opcional)" }, - "description": "Si les adreces IP i MAC es deixen buides s'utilitzar\u00e0 el descobriment autom\u00e0tic", - "title": "Passarel\u00b7la Xiaomi Aqara" + "description": "Si les adreces IP i MAC es deixen buides s'utilitzar\u00e0 el descobriment autom\u00e0tic" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/cs.json b/homeassistant/components/xiaomi_aqara/translations/cs.json index b344be8c647..f2fd489987f 100644 --- a/homeassistant/components/xiaomi_aqara/translations/cs.json +++ b/homeassistant/components/xiaomi_aqara/translations/cs.json @@ -18,8 +18,7 @@ "data": { "select_ip": "IP adresa" }, - "description": "Chcete-li p\u0159ipojit dal\u0161\u00ed br\u00e1ny, spus\u0165te znovu instalaci", - "title": "Vyberte br\u00e1nu Xiaomi Aqara, kterou chcete p\u0159ipojit" + "description": "Chcete-li p\u0159ipojit dal\u0161\u00ed br\u00e1ny, spus\u0165te znovu instalaci" }, "settings": { "data": { @@ -34,8 +33,7 @@ "host": "IP adresa (voliteln\u011b)", "mac": "MAC adresa (voliteln\u00e1)" }, - "description": "P\u0159ipojte se k va\u0161\u00ed br\u00e1n\u011b Xiaomi Aqara - pokud z\u016fstanou pr\u00e1zdn\u00e9 adresy IP a MAC, pou\u017eije se automatick\u00e9 zji\u0161\u0165ov\u00e1n\u00ed", - "title": "Br\u00e1na Xiaomi Aqara" + "description": "P\u0159ipojte se k va\u0161\u00ed br\u00e1n\u011b Xiaomi Aqara - pokud z\u016fstanou pr\u00e1zdn\u00e9 adresy IP a MAC, pou\u017eije se automatick\u00e9 zji\u0161\u0165ov\u00e1n\u00ed" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/de.json b/homeassistant/components/xiaomi_aqara/translations/de.json index 170442fc856..f9e5ea39abe 100644 --- a/homeassistant/components/xiaomi_aqara/translations/de.json +++ b/homeassistant/components/xiaomi_aqara/translations/de.json @@ -18,8 +18,7 @@ "data": { "select_ip": "IP-Adresse" }, - "description": "W\u00e4hle das Xiaomi Aqara Gateway, das du verbinden m\u00f6chtest", - "title": "W\u00e4hle das Xiaomi Aqara Gateway, das du verbinden m\u00f6chtest" + "description": "W\u00e4hle das Xiaomi Aqara Gateway, das du verbinden m\u00f6chtest" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "Die zu verwendende Netzwerkschnittstelle", "mac": "MAC-Adresse (optional)" }, - "description": "Wenn die IP- und MAC-Adressen leer gelassen werden, wird die automatische Erkennung verwendet", - "title": "Xiaomi Aqara Gateway" + "description": "Wenn die IP- und MAC-Adressen leer gelassen werden, wird die automatische Erkennung verwendet" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/el.json b/homeassistant/components/xiaomi_aqara/translations/el.json index 83923b660c2..8a2f85be99d 100644 --- a/homeassistant/components/xiaomi_aqara/translations/el.json +++ b/homeassistant/components/xiaomi_aqara/translations/el.json @@ -18,8 +18,7 @@ "data": { "select_ip": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" }, - "description": "\u0395\u03ba\u03c4\u03b5\u03bb\u03ad\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b5\u03ac\u03bd \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03b5\u03c0\u03b9\u03c0\u03bb\u03ad\u03bf\u03bd \u03c0\u03cd\u03bb\u03b5\u03c2", - "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03cd\u03bb\u03b7 Xiaomi Aqara \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5" + "description": "\u0395\u03ba\u03c4\u03b5\u03bb\u03ad\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03b5\u03ac\u03bd \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03b5\u03c0\u03b9\u03c0\u03bb\u03ad\u03bf\u03bd \u03c0\u03cd\u03bb\u03b5\u03c2" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "\u0397 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 \u03c0\u03c1\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03b7", "mac": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 Mac (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)" }, - "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c0\u03cd\u03bb\u03b7 Xiaomi Aqara Gateway, \u03b5\u03ac\u03bd \u03bf\u03b9 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 IP \u03ba\u03b1\u03b9 MAC \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03bd\u03bf\u03c5\u03bd \u03ba\u03b5\u03bd\u03ad\u03c2, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7.", - "title": "\u03a0\u03cd\u03bb\u03b7 Xiaomi Aqara" + "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c0\u03cd\u03bb\u03b7 Xiaomi Aqara Gateway, \u03b5\u03ac\u03bd \u03bf\u03b9 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 IP \u03ba\u03b1\u03b9 MAC \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03bd\u03bf\u03c5\u03bd \u03ba\u03b5\u03bd\u03ad\u03c2, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7." } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/en.json b/homeassistant/components/xiaomi_aqara/translations/en.json index 72949820bc0..ab6da1c7bfa 100644 --- a/homeassistant/components/xiaomi_aqara/translations/en.json +++ b/homeassistant/components/xiaomi_aqara/translations/en.json @@ -18,8 +18,7 @@ "data": { "select_ip": "IP Address" }, - "description": "Select the Xiaomi Aqara Gateway that you wish to connect", - "title": "Select the Xiaomi Aqara Gateway that you wish to connect" + "description": "Select the Xiaomi Aqara Gateway that you wish to connect" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "The network interface to use", "mac": "Mac Address (optional)" }, - "description": "If the IP and MAC addresses are left empty, auto-discovery is used", - "title": "Xiaomi Aqara Gateway" + "description": "If the IP and MAC addresses are left empty, auto-discovery is used" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/es.json b/homeassistant/components/xiaomi_aqara/translations/es.json index 671797b914f..e06806bbd9a 100644 --- a/homeassistant/components/xiaomi_aqara/translations/es.json +++ b/homeassistant/components/xiaomi_aqara/translations/es.json @@ -18,8 +18,7 @@ "data": { "select_ip": "Direcci\u00f3n IP" }, - "description": "Ejecuta la configuraci\u00f3n de nuevo si deseas conectar gateways adicionales", - "title": "Selecciona el Xiaomi Aqara Gateway que quieres conectar" + "description": "Ejecuta la configuraci\u00f3n de nuevo si deseas conectar gateways adicionales" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "La interfaz de la red a usar", "mac": "Direcci\u00f3n Mac (opcional)" }, - "description": "Con\u00e9ctate a tu Xiaomi Aqara Gateway, si las direcciones IP y mac se dejan vac\u00edas, se utiliza el auto-descubrimiento.", - "title": "Xiaomi Aqara Gateway" + "description": "Con\u00e9ctate a tu Xiaomi Aqara Gateway, si las direcciones IP y mac se dejan vac\u00edas, se utiliza el auto-descubrimiento." } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/et.json b/homeassistant/components/xiaomi_aqara/translations/et.json index 087c59dd2e2..90702af0a7c 100644 --- a/homeassistant/components/xiaomi_aqara/translations/et.json +++ b/homeassistant/components/xiaomi_aqara/translations/et.json @@ -18,8 +18,7 @@ "data": { "select_ip": "L\u00fc\u00fcsi IP aadress" }, - "description": "Vali Xiaomi Aqara Gateway mida soovid \u00fchendada.", - "title": "Vali Xiaomi Aqara l\u00fc\u00fcs mida soovid \u00fchendada" + "description": "Vali Xiaomi Aqara Gateway mida soovid \u00fchendada." }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "Kasutatav v\u00f5rguliides", "mac": "MAC aadress (valikuline)" }, - "description": "Kui IP- ja MAC-aadressid j\u00e4etakse t\u00fchjaks, kasutatakse automaatset avastamist", - "title": "" + "description": "Kui IP- ja MAC-aadressid j\u00e4etakse t\u00fchjaks, kasutatakse automaatset avastamist" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/fr.json b/homeassistant/components/xiaomi_aqara/translations/fr.json index ab042cb5f0e..3c5544cfff3 100644 --- a/homeassistant/components/xiaomi_aqara/translations/fr.json +++ b/homeassistant/components/xiaomi_aqara/translations/fr.json @@ -18,8 +18,7 @@ "data": { "select_ip": "Adresse IP" }, - "description": "S\u00e9lectionnez la passerelle Xiaomi Aqara que vous souhaitez connecter", - "title": "S\u00e9lectionnez la passerelle Xiaomi Aqara que vous souhaitez connecter" + "description": "S\u00e9lectionnez la passerelle Xiaomi Aqara que vous souhaitez connecter" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "Interface r\u00e9seau \u00e0 utiliser", "mac": "Adresse MAC (facultatif)" }, - "description": "Si les adresses IP et MAC sont laiss\u00e9es vides, la d\u00e9couverte automatique sera utilis\u00e9e", - "title": "Passerelle Xiaomi Aqara" + "description": "Si les adresses IP et MAC sont laiss\u00e9es vides, la d\u00e9couverte automatique sera utilis\u00e9e" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/he.json b/homeassistant/components/xiaomi_aqara/translations/he.json index ea6cd8e278b..8d123517560 100644 --- a/homeassistant/components/xiaomi_aqara/translations/he.json +++ b/homeassistant/components/xiaomi_aqara/translations/he.json @@ -18,8 +18,7 @@ "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" + "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" }, "settings": { "data": { @@ -35,8 +34,7 @@ "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": "\u05d0\u05dd \u05db\u05ea\u05d5\u05d1\u05d5\u05ea \u05d4-IP \u05d5\u05d4-MAC \u05e0\u05d5\u05ea\u05e8\u05d5\u05ea \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" + "description": "\u05d0\u05dd \u05db\u05ea\u05d5\u05d1\u05d5\u05ea \u05d4-IP \u05d5\u05d4-MAC \u05e0\u05d5\u05ea\u05e8\u05d5\u05ea \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" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/hu.json b/homeassistant/components/xiaomi_aqara/translations/hu.json index f0a1b076750..e3319baa594 100644 --- a/homeassistant/components/xiaomi_aqara/translations/hu.json +++ b/homeassistant/components/xiaomi_aqara/translations/hu.json @@ -18,8 +18,7 @@ "data": { "select_ip": "IP c\u00edm" }, - "description": "V\u00e1lassza ki a csatlakoztatni k\u00edv\u00e1nt Xiaomi Aqara K\u00f6zponti egys\u00e9get", - "title": "V\u00e1lassza ki a csatlakoztatni k\u00edv\u00e1nt Xiaomi Aqara K\u00f6zponti egys\u00e9get" + "description": "V\u00e1lassza ki a csatlakoztatni k\u00edv\u00e1nt Xiaomi Aqara K\u00f6zponti egys\u00e9get" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "A haszn\u00e1lni k\u00edv\u00e1nt h\u00e1l\u00f3zati interf\u00e9sz", "mac": "Mac-c\u00edm (opcion\u00e1lis)" }, - "description": "Ha az IP- \u00e9s MAC-c\u00edm mez\u0151ket \u00fcresen hagyja, akkor az automatikus felder\u00edt\u00e9s ker\u00fcl alkalmaz\u00e1sra.", - "title": "Xiaomi Aqara k\u00f6zponti egys\u00e9g" + "description": "Ha az IP- \u00e9s MAC-c\u00edm mez\u0151ket \u00fcresen hagyja, akkor az automatikus felder\u00edt\u00e9s ker\u00fcl alkalmaz\u00e1sra." } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/id.json b/homeassistant/components/xiaomi_aqara/translations/id.json index 2b33af8237f..746815075b4 100644 --- a/homeassistant/components/xiaomi_aqara/translations/id.json +++ b/homeassistant/components/xiaomi_aqara/translations/id.json @@ -18,8 +18,7 @@ "data": { "select_ip": "Alamat IP" }, - "description": "Pilih Gateway Xiaomi Aqara yang ingin disambungkan", - "title": "Pilih Gateway Xiaomi Aqara yang ingin dihubungkan" + "description": "Pilih Gateway Xiaomi Aqara yang ingin disambungkan" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "Antarmuka jaringan yang akan digunakan", "mac": "Alamat MAC (opsional)" }, - "description": "Jika alamat IP dan MAC dikosongkan, penemuan otomatis digunakan", - "title": "Xiaomi Aqara Gateway" + "description": "Jika alamat IP dan MAC dikosongkan, penemuan otomatis digunakan" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/it.json b/homeassistant/components/xiaomi_aqara/translations/it.json index ac5eff78190..2ea75af7f4a 100644 --- a/homeassistant/components/xiaomi_aqara/translations/it.json +++ b/homeassistant/components/xiaomi_aqara/translations/it.json @@ -18,8 +18,7 @@ "data": { "select_ip": "Indirizzo IP" }, - "description": "Seleziona lo Xiaomi Aqara Gateway che desideri connettere", - "title": "Seleziona il Gateway Xiaomi Aqara che si desidera collegare" + "description": "Seleziona lo Xiaomi Aqara Gateway che desideri connettere" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "L'interfaccia di rete da utilizzare", "mac": "Indirizzo Mac (opzionale)" }, - "description": "Se gli indirizzi IP e MAC vengono lasciati vuoti, viene utilizzato il rilevamento automatico", - "title": "Xiaomi Aqara Gateway" + "description": "Se gli indirizzi IP e MAC vengono lasciati vuoti, viene utilizzato il rilevamento automatico" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/ja.json b/homeassistant/components/xiaomi_aqara/translations/ja.json index 65e1870efe2..becb37c00f0 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ja.json +++ b/homeassistant/components/xiaomi_aqara/translations/ja.json @@ -18,8 +18,7 @@ "data": { "select_ip": "IP\u30a2\u30c9\u30ec\u30b9" }, - "description": "\u8ffd\u52a0\u306e\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3092\u63a5\u7d9a\u3059\u308b\u5834\u5408\u306f\u3001\u518d\u5ea6\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u63a5\u7d9a\u3057\u305f\u3044Xiaomi Aqara Gateway\u3092\u9078\u629e\u3057\u307e\u3059\u3002" + "description": "\u8ffd\u52a0\u306e\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3092\u63a5\u7d9a\u3059\u308b\u5834\u5408\u306f\u3001\u518d\u5ea6\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u5b9f\u884c\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "\u4f7f\u7528\u3059\u308b\u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u30a4\u30f3\u30bf\u30fc\u30d5\u30a7\u30a4\u30b9", "mac": "Mac\u30a2\u30c9\u30ec\u30b9 (\u30aa\u30d7\u30b7\u30e7\u30f3)" }, - "description": "Xiaomi Aqara Gateway\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002IP\u30a2\u30c9\u30ec\u30b9\u3068MAC\u30a2\u30c9\u30ec\u30b9\u304c\u7a7a\u767d\u306e\u307e\u307e\u306e\u5834\u5408\u3001\u81ea\u52d5\u691c\u51fa\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059", - "title": "Xiaomi Aqara Gateway" + "description": "Xiaomi Aqara Gateway\u306b\u63a5\u7d9a\u3057\u307e\u3059\u3002IP\u30a2\u30c9\u30ec\u30b9\u3068MAC\u30a2\u30c9\u30ec\u30b9\u304c\u7a7a\u767d\u306e\u307e\u307e\u306e\u5834\u5408\u3001\u81ea\u52d5\u691c\u51fa\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/ko.json b/homeassistant/components/xiaomi_aqara/translations/ko.json index dd8a9ae5ede..1538f2adcb5 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ko.json +++ b/homeassistant/components/xiaomi_aqara/translations/ko.json @@ -18,8 +18,7 @@ "data": { "select_ip": "IP \uc8fc\uc18c" }, - "description": "\uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \ucd94\uac00 \uc5f0\uacb0\ud558\ub824\uba74 \uc124\uc815\uc744 \ub2e4\uc2dc \uc2e4\ud589\ud574\uc8fc\uc138\uc694", - "title": "\uc5f0\uacb0\ud560 Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774 \uc120\ud0dd\ud558\uae30" + "description": "\uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \ucd94\uac00 \uc5f0\uacb0\ud558\ub824\uba74 \uc124\uc815\uc744 \ub2e4\uc2dc \uc2e4\ud589\ud574\uc8fc\uc138\uc694" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "\uc0ac\uc6a9\ud560 \ub124\ud2b8\uc6cc\ud06c \uc778\ud130\ud398\uc774\uc2a4", "mac": "Mac \uc8fc\uc18c (\uc120\ud0dd \uc0ac\ud56d)" }, - "description": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud569\ub2c8\ub2e4. IP \uc8fc\uc18c \ubc0f MAC \uc8fc\uc18c\ub97c \ube44\uc6cc\ub450\uba74 \uc790\ub3d9 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4", - "title": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774" + "description": "Xiaomi Aqara \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud569\ub2c8\ub2e4. IP \uc8fc\uc18c \ubc0f MAC \uc8fc\uc18c\ub97c \ube44\uc6cc\ub450\uba74 \uc790\ub3d9 \uac80\uc0c9\uc774 \uc0ac\uc6a9\ub429\ub2c8\ub2e4" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/lb.json b/homeassistant/components/xiaomi_aqara/translations/lb.json index 1d3c5f6ed76..3bb703ffb34 100644 --- a/homeassistant/components/xiaomi_aqara/translations/lb.json +++ b/homeassistant/components/xiaomi_aqara/translations/lb.json @@ -18,8 +18,7 @@ "data": { "select_ip": "IP Adresse" }, - "description": "Setup nach emol ausf\u00e9ieren fir eng zous\u00e4tzlech Gateway dob\u00e4i ze setzen", - "title": "Xiaomi Aqara Gateway auswielen mat der sech soll verbonne ginn" + "description": "Setup nach emol ausf\u00e9ieren fir eng zous\u00e4tzlech Gateway dob\u00e4i ze setzen" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "Netzwierk Interface dee soll benotzt ginn", "mac": "Mac Adress (Optionell)" }, - "description": "Mat denger Xiaomi Aqara Gateway verbannen, falls keng IP oder MAC Adress uginn ass g\u00ebtt auto-discovery benotzt", - "title": "Xiaomi Aqara Gateway" + "description": "Mat denger Xiaomi Aqara Gateway verbannen, falls keng IP oder MAC Adress uginn ass g\u00ebtt auto-discovery benotzt" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/nl.json b/homeassistant/components/xiaomi_aqara/translations/nl.json index c2042850fdb..a549d01313b 100644 --- a/homeassistant/components/xiaomi_aqara/translations/nl.json +++ b/homeassistant/components/xiaomi_aqara/translations/nl.json @@ -18,8 +18,7 @@ "data": { "select_ip": "IP-adres" }, - "description": "Selecteer de Xiaomi Aqara Gateway die u wilt verbinden", - "title": "Selecteer de Xiaomi Aqara Gateway waarmee u verbinding wilt maken" + "description": "Selecteer de Xiaomi Aqara Gateway die u wilt verbinden" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "De netwerkinterface die moet worden gebruikt", "mac": "MAC-adres (optioneel)" }, - "description": "Als de IP- en MAC-adressen leeg worden gelaten, wordt auto-discovery gebruikt", - "title": "Xiaomi Aqara Gateway" + "description": "Als de IP- en MAC-adressen leeg worden gelaten, wordt auto-discovery gebruikt" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/no.json b/homeassistant/components/xiaomi_aqara/translations/no.json index c325886dd22..8cb351dc7e2 100644 --- a/homeassistant/components/xiaomi_aqara/translations/no.json +++ b/homeassistant/components/xiaomi_aqara/translations/no.json @@ -18,8 +18,7 @@ "data": { "select_ip": "IP adresse" }, - "description": "Velg Xiaomi Aqara Gateway som du \u00f8nsker \u00e5 koble til", - "title": "Velg Xiaomi Aqara Gateway som du \u00f8nsker \u00e5 koble til" + "description": "Velg Xiaomi Aqara Gateway som du \u00f8nsker \u00e5 koble til" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "Nettverksgrensesnittet som skal brukes", "mac": "MAC-adresse (valgfritt)" }, - "description": "Hvis IP- og MAC-adressene er tomme, brukes automatisk oppdagelse", - "title": "" + "description": "Hvis IP- og MAC-adressene er tomme, brukes automatisk oppdagelse" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/pl.json b/homeassistant/components/xiaomi_aqara/translations/pl.json index e680415320c..dc7391166f5 100644 --- a/homeassistant/components/xiaomi_aqara/translations/pl.json +++ b/homeassistant/components/xiaomi_aqara/translations/pl.json @@ -18,8 +18,7 @@ "data": { "select_ip": "Adres IP" }, - "description": "Wybierz bramk\u0119 Xiaomi Aqara, z kt\u00f3r\u0105 chcesz si\u0119 po\u0142\u0105czy\u0107", - "title": "Wybierz bramk\u0119 Xiaomi Aqara, z kt\u00f3r\u0105 chcesz si\u0119 po\u0142\u0105czy\u0107" + "description": "Wybierz bramk\u0119 Xiaomi Aqara, z kt\u00f3r\u0105 chcesz si\u0119 po\u0142\u0105czy\u0107" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "Interfejs sieciowy", "mac": "Adres MAC (opcjonalnie)" }, - "description": "Je\u015bli zostawisz Adres IP oraz MAC puste to bramka zostanie automatycznie wykryta.", - "title": "Bramka Xiaomi Aqara" + "description": "Je\u015bli zostawisz Adres IP oraz MAC puste to bramka zostanie automatycznie wykryta." } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/pt-BR.json b/homeassistant/components/xiaomi_aqara/translations/pt-BR.json index 62deb3ba97c..f83050acfc7 100644 --- a/homeassistant/components/xiaomi_aqara/translations/pt-BR.json +++ b/homeassistant/components/xiaomi_aqara/translations/pt-BR.json @@ -18,8 +18,7 @@ "data": { "select_ip": "Endere\u00e7o IP" }, - "description": "Selecione o Xiaomi Aqara Gateway que voc\u00ea deseja conectar", - "title": "Selecione o Xiaomi Aqara Gateway que voc\u00ea deseja conectar" + "description": "Selecione o Xiaomi Aqara Gateway que voc\u00ea deseja conectar" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "A interface de rede a ser usada", "mac": "Endere\u00e7o Mac (opcional)" }, - "description": "Se os endere\u00e7os IP e MAC forem deixados vazios, a descoberta autom\u00e1tica \u00e9 usada", - "title": "Gateway Xiaomi Aqara" + "description": "Se os endere\u00e7os IP e MAC forem deixados vazios, a descoberta autom\u00e1tica \u00e9 usada" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/ru.json b/homeassistant/components/xiaomi_aqara/translations/ru.json index 499837889df..3483bf2d3f8 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ru.json +++ b/homeassistant/components/xiaomi_aqara/translations/ru.json @@ -18,8 +18,7 @@ "data": { "select_ip": "IP-\u0430\u0434\u0440\u0435\u0441" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 Xiaomi Aqara.", - "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 Xiaomi Aqara" + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 Xiaomi Aqara." }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "\u0421\u0435\u0442\u0435\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441", "mac": "MAC-\u0430\u0434\u0440\u0435\u0441 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" }, - "description": "\u0414\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0448\u043b\u044e\u0437\u0430, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u044f IP \u0438 MAC \u0430\u0434\u0440\u0435\u0441\u043e\u0432 \u043f\u0443\u0441\u0442\u044b\u043c\u0438.", - "title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara" + "description": "\u0414\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0448\u043b\u044e\u0437\u0430, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u044f IP \u0438 MAC \u0430\u0434\u0440\u0435\u0441\u043e\u0432 \u043f\u0443\u0441\u0442\u044b\u043c\u0438." } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/tr.json b/homeassistant/components/xiaomi_aqara/translations/tr.json index 0c961a34a3c..0d8b78fc3a8 100644 --- a/homeassistant/components/xiaomi_aqara/translations/tr.json +++ b/homeassistant/components/xiaomi_aqara/translations/tr.json @@ -18,8 +18,7 @@ "data": { "select_ip": "IP Adresi" }, - "description": "Ba\u011flamak istedi\u011finiz Xiaomi Aqara A\u011f Ge\u00e7idini se\u00e7in", - "title": "Ba\u011flamak istedi\u011finiz Xiaomi Aqara A\u011f Ge\u00e7idini se\u00e7in" + "description": "Ba\u011flamak istedi\u011finiz Xiaomi Aqara A\u011f Ge\u00e7idini se\u00e7in" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "Kullan\u0131lacak a\u011f aray\u00fcz\u00fc", "mac": "Mac Adresi (iste\u011fe ba\u011fl\u0131)" }, - "description": "IP ve MAC adresleri bo\u015f b\u0131rak\u0131l\u0131rsa otomatik ke\u015fif kullan\u0131l\u0131r", - "title": "Xiaomi Aqara A\u011f Ge\u00e7idi" + "description": "IP ve MAC adresleri bo\u015f b\u0131rak\u0131l\u0131rsa otomatik ke\u015fif kullan\u0131l\u0131r" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/uk.json b/homeassistant/components/xiaomi_aqara/translations/uk.json index 1598e96b38e..7c598b29fc7 100644 --- a/homeassistant/components/xiaomi_aqara/translations/uk.json +++ b/homeassistant/components/xiaomi_aqara/translations/uk.json @@ -18,8 +18,7 @@ "data": { "select_ip": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" }, - "description": "\u041f\u043e\u0447\u043d\u0456\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u044e\u0432\u0430\u043d\u043d\u044f \u0437\u043d\u043e\u0432\u0443, \u044f\u043a\u0449\u043e \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0434\u043e\u0434\u0430\u0442\u0438 \u0456\u043d\u0448\u0438\u0439 \u0448\u043b\u044e\u0437", - "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0448\u043b\u044e\u0437 Xiaomi Aqara" + "description": "\u041f\u043e\u0447\u043d\u0456\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u044e\u0432\u0430\u043d\u043d\u044f \u0437\u043d\u043e\u0432\u0443, \u044f\u043a\u0449\u043e \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0434\u043e\u0434\u0430\u0442\u0438 \u0456\u043d\u0448\u0438\u0439 \u0448\u043b\u044e\u0437" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "\u041c\u0435\u0440\u0435\u0436\u0435\u0432\u0438\u0439 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441", "mac": "MAC-\u0430\u0434\u0440\u0435\u0441\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)" }, - "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437\u0456 \u0448\u043b\u044e\u0437\u043e\u043c Xiaomi Aqara. \u0414\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u0448\u043b\u044e\u0437\u0443, \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u043b\u044f IP \u0456 MAC-\u0430\u0434\u0440\u0435\u0441\u0438 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c\u0438.", - "title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara" + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437\u0456 \u0448\u043b\u044e\u0437\u043e\u043c Xiaomi Aqara. \u0414\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u0448\u043b\u044e\u0437\u0443, \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u043b\u044f IP \u0456 MAC-\u0430\u0434\u0440\u0435\u0441\u0438 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c\u0438." } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/zh-Hans.json b/homeassistant/components/xiaomi_aqara/translations/zh-Hans.json index a0d0159e71c..1532e2eb05f 100644 --- a/homeassistant/components/xiaomi_aqara/translations/zh-Hans.json +++ b/homeassistant/components/xiaomi_aqara/translations/zh-Hans.json @@ -18,8 +18,7 @@ "data": { "select_ip": "\u7f51\u5173 IP" }, - "description": "\u5982\u679c\u8981\u8fde\u63a5\u5176\u4ed6\u7f51\u5173\uff0c\u8bf7\u518d\u6b21\u8fd0\u884c\u914d\u7f6e\u7a0b\u5e8f", - "title": "\u9009\u62e9\u8981\u8fde\u63a5\u7684\u5c0f\u7c73 Aqara \u7f51\u5173" + "description": "\u5982\u679c\u8981\u8fde\u63a5\u5176\u4ed6\u7f51\u5173\uff0c\u8bf7\u518d\u6b21\u8fd0\u884c\u914d\u7f6e\u7a0b\u5e8f" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "\u8981\u4f7f\u7528\u7684\u7f51\u7edc\u63a5\u53e3", "mac": "MAC \u5730\u5740 (\u53ef\u9009)" }, - "description": "\u8fde\u63a5\u5230\u60a8\u7684\u5c0f\u7c73 Aqara \u7f51\u5173\uff0c\u5982\u679c IP \u548c MAC \u5730\u5740\u7559\u7a7a\uff0c\u5219\u4f7f\u7528\u81ea\u52a8\u53d1\u73b0", - "title": "\u5c0f\u7c73 Aqara \u7f51\u5173" + "description": "\u8fde\u63a5\u5230\u60a8\u7684\u5c0f\u7c73 Aqara \u7f51\u5173\uff0c\u5982\u679c IP \u548c MAC \u5730\u5740\u7559\u7a7a\uff0c\u5219\u4f7f\u7528\u81ea\u52a8\u53d1\u73b0" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json index 5ffe34d5d39..d70d060efa4 100644 --- a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json @@ -18,8 +18,7 @@ "data": { "select_ip": "IP \u4f4d\u5740" }, - "description": "\u5982\u679c\u6240\u8981\u9023\u7dda\u7684\u5c0f\u7c73 Aqara \u7db2\u95dc", - "title": "\u9078\u64c7\u6240\u8981\u9023\u7dda\u7684\u5c0f\u7c73 Aqara \u7db2\u95dc" + "description": "\u5982\u679c\u6240\u8981\u9023\u7dda\u7684\u5c0f\u7c73 Aqara \u7db2\u95dc" }, "settings": { "data": { @@ -35,8 +34,7 @@ "interface": "\u4f7f\u7528\u7684\u7db2\u8def\u4ecb\u9762", "mac": "Mac \u4f4d\u5740\uff08\u9078\u9805\uff09" }, - "description": "\u5047\u5982 IP \u6216 Mac \u4f4d\u5740\u70ba\u7a7a\u767d\u3001\u5c07\u9032\u884c\u81ea\u52d5\u641c\u7d22", - "title": "\u5c0f\u7c73 Aqara \u7db2\u95dc" + "description": "\u5047\u5982 IP \u6216 Mac \u4f4d\u5740\u70ba\u7a7a\u767d\u3001\u5c07\u9032\u884c\u81ea\u52d5\u641c\u7d22" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/bg.json b/homeassistant/components/xiaomi_miio/translations/bg.json index a73df3c5609..2339d5bc830 100644 --- a/homeassistant/components/xiaomi_miio/translations/bg.json +++ b/homeassistant/components/xiaomi_miio/translations/bg.json @@ -5,8 +5,7 @@ "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." + "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": { @@ -20,17 +19,6 @@ "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" - } - }, - "gateway": { - "data": { - "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" diff --git a/homeassistant/components/xiaomi_miio/translations/ca.json b/homeassistant/components/xiaomi_miio/translations/ca.json index cb36fbbb2e0..614238a54b4 100644 --- a/homeassistant/components/xiaomi_miio/translations/ca.json +++ b/homeassistant/components/xiaomi_miio/translations/ca.json @@ -12,7 +12,6 @@ "cloud_credentials_incomplete": "Credencials del n\u00favol incompletes, introdueix el nom d'usuari, la contrasenya i el pa\u00eds", "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.", "wrong_token": "Error de verificaci\u00f3, token erroni" }, @@ -25,42 +24,19 @@ "cloud_username": "Nom d'usuari del n\u00favol", "manual": "Configuraci\u00f3 manual (no recomanada)" }, - "description": "Inicia sessi\u00f3 al n\u00favol Xiaomi Miio, consulta https://www.openhab.org/addons/bindings/miio/#country-servers per obtenir el servidor al n\u00favol.", - "title": "Connexi\u00f3 amb un dispositiu Xiaomi Miio o una passarel\u00b7la Xiaomi" + "description": "Inicia sessi\u00f3 al n\u00favol Xiaomi Miio, consulta https://www.openhab.org/addons/bindings/miio/#country-servers per obtenir el servidor al n\u00favol." }, "connect": { "data": { "model": "Model de dispositiu" - }, - "description": "Selecciona manualment el model de dispositiu entre els models compatibles.", - "title": "Connexi\u00f3 amb un dispositiu Xiaomi Miio o una passarel\u00b7la Xiaomi" - }, - "device": { - "data": { - "host": "Adre\u00e7a IP", - "model": "Model del dispositiu (opcional)", - "name": "Nom del dispositiu", - "token": "Token d'API" - }, - "description": "Necessitar\u00e0s el Token d'API de 32 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token. Tingues en compte que aquest Token d'API \u00e9s diferent a la clau utilitzada per la integraci\u00f3 Xiaomi Aqara.", - "title": "Connexi\u00f3 amb un dispositiu Xiaomi Miio o una passarel\u00b7la de Xiaomi" - }, - "gateway": { - "data": { - "host": "Adre\u00e7a IP", - "name": "Nom de la passarel\u00b7la", - "token": "Token d'API" - }, - "description": "Necessitar\u00e0s el Token d'API de 32 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. Tingues en compte que aquest Token d'API \u00e9s diferent a la clau utilitzada per la integraci\u00f3 Xiaomi Aqara.", - "title": "Connexi\u00f3 amb la passarel\u00b7la de Xiaomi" + } }, "manual": { "data": { "host": "Adre\u00e7a IP", "token": "Token d'API" }, - "description": "Necessitar\u00e0s el Token d'API de 32 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/xiaomi_miio/#retrieving-the-access-token. Tingues en compte que aquest Token d'API \u00e9s diferent a la clau utilitzada per la integraci\u00f3 Xiaomi Aqara.", - "title": "Connexi\u00f3 amb un dispositiu Xiaomi Miio o una passarel\u00b7la Xiaomi" + "description": "Necessitar\u00e0s el Token d'API de 32 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/xiaomi_miio/#retrieving-the-access-token. Tingues en compte que aquest Token d'API \u00e9s diferent a la clau utilitzada per la integraci\u00f3 Xiaomi Aqara." }, "reauth_confirm": { "description": "La integraci\u00f3 Xiaomi Miio ha de tornar a autenticar-se amb el teu compte per poder actualitzar els tokens o afegir credencials pel n\u00favol.", @@ -70,15 +46,7 @@ "data": { "select_device": "Dispositiu Miio" }, - "description": "Selecciona el dispositiu Xiaomi Miio a configurar.", - "title": "Connexi\u00f3 amb un dispositiu Xiaomi Miio o una passarel\u00b7la Xiaomi" - }, - "user": { - "data": { - "gateway": "Connexi\u00f3 amb la passarel\u00b7la de Xiaomi" - }, - "description": "Selecciona a quin dispositiu vols connectar-te.", - "title": "Xiaomi Miio" + "description": "Selecciona el dispositiu Xiaomi Miio a configurar." } } }, @@ -90,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "Utilitza el n\u00favol per obtenir subdispositius connectats" - }, - "description": "Especifica par\u00e0metres opcionals", - "title": "Xiaomi Miio" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/cs.json b/homeassistant/components/xiaomi_miio/translations/cs.json index 69b6cd221ee..858546d915f 100644 --- a/homeassistant/components/xiaomi_miio/translations/cs.json +++ b/homeassistant/components/xiaomi_miio/translations/cs.json @@ -9,7 +9,6 @@ }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "no_device_selected": "Nebylo vybr\u00e1no \u017e\u00e1dn\u00e9 za\u0159\u00edzen\u00ed, vyberte jedno za\u0159\u00edzen\u00ed.", "unknown_device": "Model za\u0159\u00edzen\u00ed nen\u00ed zn\u00e1m, nastaven\u00ed za\u0159\u00edzen\u00ed nen\u00ed mo\u017en\u00e9 dokon\u010dit.", "wrong_token": "Chyba kontroln\u00edho sou\u010dtu, \u0161patn\u00fd token" }, @@ -18,42 +17,19 @@ "cloud": { "data": { "manual": "Nastavit ru\u010dn\u011b (nedoporu\u010deno)" - }, - "title": "P\u0159ipojen\u00ed k za\u0159\u00edzen\u00ed Xiaomi Miio nebo k br\u00e1n\u011b Xiaomi" + } }, "connect": { "data": { "model": "Model za\u0159\u00edzen\u00ed" - }, - "description": "Ru\u010dn\u011b vyberte model za\u0159\u00edzen\u00ed ze seznamu podporovan\u00fdch za\u0159\u00edzen\u00ed.", - "title": "P\u0159ipojen\u00ed k za\u0159\u00edzen\u00ed Xiaomi Miio nebo k br\u00e1n\u011b Xiaomi" - }, - "device": { - "data": { - "host": "IP adresa", - "model": "Model za\u0159\u00edzen\u00ed (voliteln\u00e9)", - "name": "Jm\u00e9no za\u0159\u00edzen\u00ed", - "token": "API token" - }, - "description": "Budete pot\u0159ebovat 32 znakov\u00fd API token, pokyny naleznete na https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token. Upozor\u0148ujeme, \u017ee tento token se li\u0161\u00ed od kl\u00ed\u010de pou\u017e\u00edvan\u00e9ho v integraci Xiaomi Aqara.", - "title": "P\u0159ipojen\u00ed k za\u0159\u00edzen\u00ed Xiaomi Miio nebo k br\u00e1n\u011b Xiaomi" - }, - "gateway": { - "data": { - "host": "IP adresa", - "name": "N\u00e1zev br\u00e1ny", - "token": "API token" - }, - "description": "Budete pot\u0159ebovat 32 znakov\u00fd API token, pokyny naleznete na https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. Upozor\u0148ujeme, \u017ee tento token se li\u0161\u00ed od kl\u00ed\u010de pou\u017e\u00edvan\u00e9ho v integraci Xiaomi Aqara.", - "title": "P\u0159ipojen\u00ed k br\u00e1n\u011b Xiaomi" + } }, "manual": { "data": { "host": "IP adresa", "token": "API token" }, - "description": "Budete pot\u0159ebovat 32 znakov\u00fd API token, pokyny naleznete na https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token. Upozor\u0148ujeme, \u017ee tento token se li\u0161\u00ed od kl\u00ed\u010de pou\u017e\u00edvan\u00e9ho v integraci Xiaomi Aqara.", - "title": "P\u0159ipojen\u00ed k za\u0159\u00edzen\u00ed Xiaomi Miio nebo k br\u00e1n\u011b Xiaomi" + "description": "Budete pot\u0159ebovat 32 znakov\u00fd API token, pokyny naleznete na https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token. Upozor\u0148ujeme, \u017ee tento token se li\u0161\u00ed od kl\u00ed\u010de pou\u017e\u00edvan\u00e9ho v integraci Xiaomi Aqara." }, "reauth_confirm": { "title": "Znovu ov\u011b\u0159it integraci" @@ -62,15 +38,7 @@ "data": { "select_device": "Za\u0159\u00edzen\u00ed Miio" }, - "description": "Vyberte Xiaomi Miio za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit.", - "title": "P\u0159ipojen\u00ed k za\u0159\u00edzen\u00ed Xiaomi Miio nebo k br\u00e1n\u011b Xiaomi" - }, - "user": { - "data": { - "gateway": "P\u0159ipojen\u00ed k br\u00e1n\u011b Xiaomi" - }, - "description": "Vyberte, ke kter\u00e9mu za\u0159\u00edzen\u00ed se chcete p\u0159ipojit.", - "title": "Xiaomi Miio" + "description": "Vyberte Xiaomi Miio za\u0159\u00edzen\u00ed, kter\u00e9 chcete nastavit." } } }, @@ -79,9 +47,7 @@ "init": { "data": { "cloud_subdevices": "Pou\u017e\u00edt cloud pro z\u00edsk\u00e1n\u00ed p\u0159ipojen\u00fdch podru\u017en\u00fdch za\u0159\u00edzen\u00ed" - }, - "description": "Zadejte voliteln\u00e9 nastaven\u00ed", - "title": "Xiaomi Miio" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json index 291323f31bd..70a630f90f3 100644 --- a/homeassistant/components/xiaomi_miio/translations/de.json +++ b/homeassistant/components/xiaomi_miio/translations/de.json @@ -12,7 +12,6 @@ "cloud_credentials_incomplete": "Cloud-Anmeldeinformationen unvollst\u00e4ndig, bitte Benutzernamen, Passwort und Land eingeben", "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.", "wrong_token": "Pr\u00fcfsummenfehler, falscher Token" }, @@ -25,42 +24,19 @@ "cloud_username": "Cloud-Benutzername", "manual": "Manuell konfigurieren (nicht empfohlen)" }, - "description": "Melde dich bei der Xiaomi Miio Cloud an, siehe https://www.openhab.org/addons/bindings/miio/#country-servers f\u00fcr den zu verwendenden Cloud-Server.", - "title": "Verbinden mit einem Xiaomi Miio Ger\u00e4t oder Xiaomi Gateway" + "description": "Melde dich bei der Xiaomi Miio Cloud an, siehe https://www.openhab.org/addons/bindings/miio/#country-servers f\u00fcr den zu verwendenden Cloud-Server." }, "connect": { "data": { "model": "Ger\u00e4temodell" - }, - "description": "W\u00e4hle das Ger\u00e4temodell manuell aus den unterst\u00fctzten Modellen aus.", - "title": "Verbinden mit einem Xiaomi Miio Ger\u00e4t oder Xiaomi Gateway" - }, - "device": { - "data": { - "host": "IP-Adresse", - "model": "Ger\u00e4temodell (optional)", - "name": "Name des Ger\u00e4ts", - "token": "API-Token" - }, - "description": "Du ben\u00f6tigst den 32 Zeichen langen API-Token, siehe https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token f\u00fcr eine Anleitung. Dieser unterscheidet sich vom API-Token, den die Xiaomi Aqara-Integration nutzt.", - "title": "Herstellen einer Verbindung mit einem Xiaomi Miio-Ger\u00e4t oder Xiaomi Gateway" - }, - "gateway": { - "data": { - "host": "IP-Adresse", - "name": "Name des Gateways", - "token": "API-Token" - }, - "description": "Du ben\u00f6tigst den 32 Zeichen langen API-Token. Anweisungen findest du unter https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. Bitte beachte, dass sich dieser API-Token von dem Schl\u00fcssel unterscheidet, der von der Xiaomi Aqara Integration verwendet wird.", - "title": "Stelle eine Verbindung zu einem Xiaomi Gateway her" + } }, "manual": { "data": { "host": "IP-Adresse", "token": "API-Token" }, - "description": "Du ben\u00f6tigst den 32 Zeichen langen API-Token, siehe https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token f\u00fcr Anweisungen. Bitte beachte, dass sich dieser API-Token von dem Schl\u00fcssel unterscheidet, der von der Xiaomi Aqara-Integration verwendet wird.", - "title": "Verbinden mit einem Xiaomi Miio Ger\u00e4t oder Xiaomi Gateway" + "description": "Du ben\u00f6tigst den 32 Zeichen langen API-Token, siehe https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token f\u00fcr Anweisungen. Bitte beachte, dass sich dieser API-Token von dem Schl\u00fcssel unterscheidet, der von der Xiaomi Aqara-Integration verwendet wird." }, "reauth_confirm": { "description": "Die Xiaomi Miio-Integration muss dein Konto neu authentifizieren, um die Token zu aktualisieren oder fehlende Cloud-Anmeldedaten hinzuzuf\u00fcgen.", @@ -70,15 +46,7 @@ "data": { "select_device": "Miio-Ger\u00e4t" }, - "description": "W\u00e4hle das einzurichtende Xiaomi Miio-Ger\u00e4t aus.", - "title": "Verbinden mit einem Xiaomi Miio Ger\u00e4t oder Xiaomi Gateway" - }, - "user": { - "data": { - "gateway": "Stelle eine Verbindung zu einem Xiaomi Gateway her" - }, - "description": "W\u00e4hle aus, mit welchem Ger\u00e4t du eine Verbindung herstellen m\u00f6chtest.", - "title": "Xiaomi Miio" + "description": "W\u00e4hle das einzurichtende Xiaomi Miio-Ger\u00e4t aus." } } }, @@ -90,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "Cloud verwenden, um verbundene Subdevices zu erhalten" - }, - "description": "Optionale Einstellungen angeben", - "title": "Xiaomi Miio" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/el.json b/homeassistant/components/xiaomi_miio/translations/el.json index abd099a404b..48f55af9246 100644 --- a/homeassistant/components/xiaomi_miio/translations/el.json +++ b/homeassistant/components/xiaomi_miio/translations/el.json @@ -12,7 +12,6 @@ "cloud_credentials_incomplete": "\u03a4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 Cloud \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bb\u03bb\u03b9\u03c0\u03ae, \u03c3\u03c5\u03bc\u03c0\u03bb\u03b7\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7, \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03c7\u03ce\u03c1\u03b1", "cloud_login_error": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf Xiaomi Miio Cloud, \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1.", "cloud_no_devices": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc Xiaomi Miio cloud.", - "no_device_selected": "\u0394\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03b5\u03af \u03ba\u03b1\u03bc\u03af\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae, \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03af\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae.", "unknown_device": "\u03a4\u03bf \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b3\u03bd\u03c9\u03c3\u03c4\u03cc, \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03bc\u03b5 \u03c4\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c1\u03bf\u03ae\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd.", "wrong_token": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b1\u03b8\u03c1\u03bf\u03af\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5, \u03bb\u03ac\u03b8\u03bf\u03c2 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc" }, @@ -25,42 +24,19 @@ "cloud_username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 Cloud", "manual": "\u039c\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 (\u03b4\u03b5\u03bd \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9)" }, - "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf cloud Xiaomi Miio, \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://www.openhab.org/addons/bindings/miio/#country-servers \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae cloud \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5.", - "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Xiaomi Miio \u03ae \u03c0\u03cd\u03bb\u03b7 Xiaomi" + "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf cloud Xiaomi Miio, \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://www.openhab.org/addons/bindings/miio/#country-servers \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae cloud \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5." }, "connect": { "data": { "model": "\u039c\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1 \u03c4\u03bf \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b1 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b1 \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03b1.", - "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Xiaomi Miio \u03ae \u03c0\u03cd\u03bb\u03b7 Xiaomi" - }, - "device": { - "data": { - "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", - "model": "\u039c\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 (\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", - "token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API" - }, - "description": "\u0398\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf\u03bd 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03b5\u03c2 \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API, \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u03b3\u03b9\u03b1 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2. \u039b\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c5\u03c0\u03cc\u03c8\u03b7 \u03cc\u03c4\u03b9 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc \u03b1\u03c0\u03cc \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Xiaomi Aqara.\u0398\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf\u03bd 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03b5\u03c2 \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API, \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u03b3\u03b9\u03b1 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2. \u039b\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c5\u03c0\u03cc\u03c8\u03b7 \u03cc\u03c4\u03b9 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc \u03b1\u03c0\u03cc \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Xiaomi Aqara.", - "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Xiaomi Miio \u03ae \u03c0\u03cd\u03bb\u03b7 Xiaomi" - }, - "gateway": { - "data": { - "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03c0\u03cd\u03bb\u03b7\u03c2", - "token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API" - }, - "description": "\u0398\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03b5\u03c2 32 \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API , \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u03b3\u03b9\u03b1 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2. \u039b\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c5\u03c0\u03cc\u03c8\u03b7 \u03cc\u03c4\u03b9 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc \u03b1\u03c0\u03cc \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Xiaomi Aqara.", - "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 Xiaomi" + } }, "manual": { "data": { "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", "token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API" }, - "description": "\u0398\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API, \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u03b3\u03b9\u03b1 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2. \u039b\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c5\u03c0\u03cc\u03c8\u03b7 \u03cc\u03c4\u03b9 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc \u03b1\u03c0\u03cc \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Xiaomi Aqara.", - "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Xiaomi Miio \u03ae \u03c0\u03cd\u03bb\u03b7 Xiaomi" + "description": "\u0398\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API, \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u03b3\u03b9\u03b1 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2. \u039b\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c5\u03c0\u03cc\u03c8\u03b7 \u03cc\u03c4\u03b9 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc \u03b1\u03c0\u03cc \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Xiaomi Aqara." }, "reauth_confirm": { "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 xiaomi Miio \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03b5\u03b9 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03ac \u03ae \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03b9 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 cloud \u03c0\u03bf\u03c5 \u03bb\u03b5\u03af\u03c0\u03bf\u03c5\u03bd.", @@ -70,15 +46,7 @@ "data": { "select_device": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Miio" }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Xiaomi Miio \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7.", - "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Xiaomi Miio \u03ae \u03c0\u03cd\u03bb\u03b7 Xiaomi" - }, - "user": { - "data": { - "gateway": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 Xiaomi" - }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03b5 \u03c0\u03bf\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5.", - "title": "Xiaomi Miio" + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Xiaomi Miio \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7." } } }, @@ -90,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf cloud \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c5\u03c0\u03bf\u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2" - }, - "description": "\u039a\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ce\u03bd \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd", - "title": "Xiaomi Miio" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json index 8bd35f3e7d9..c37be0a7f74 100644 --- a/homeassistant/components/xiaomi_miio/translations/en.json +++ b/homeassistant/components/xiaomi_miio/translations/en.json @@ -12,7 +12,6 @@ "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.", "wrong_token": "Checksum error, wrong token" }, @@ -25,42 +24,19 @@ "cloud_username": "Cloud username", "manual": "Configure manually (not recommended)" }, - "description": "Log in to the Xiaomi Miio cloud, see https://www.openhab.org/addons/bindings/miio/#country-servers for the cloud server to use.", - "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + "description": "Log in to the Xiaomi Miio cloud, see https://www.openhab.org/addons/bindings/miio/#country-servers for the cloud server to use." }, "connect": { "data": { "model": "Device model" - }, - "description": "Manually select the device model from the supported models.", - "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" - }, - "device": { - "data": { - "host": "IP Address", - "model": "Device model (Optional)", - "name": "Name of the device", - "token": "API Token" - }, - "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", - "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" - }, - "gateway": { - "data": { - "host": "IP Address", - "name": "Name of the Gateway", - "token": "API Token" - }, - "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", - "title": "Connect to a Xiaomi Gateway" + } }, "manual": { "data": { "host": "IP Address", "token": "API Token" }, - "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration.", - "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" + "description": "You will need the 32 character API Token, see https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instructions. Please note, that this API Token is different from the key used by the Xiaomi Aqara integration." }, "reauth_confirm": { "description": "The Xiaomi Miio integration needs to re-authenticate your account in order to update the tokens or add missing cloud credentials.", @@ -70,15 +46,7 @@ "data": { "select_device": "Miio device" }, - "description": "Select the Xiaomi Miio device to setup.", - "title": "Connect to a Xiaomi Miio Device or Xiaomi Gateway" - }, - "user": { - "data": { - "gateway": "Connect to a Xiaomi Gateway" - }, - "description": "Select to which device you want to connect.", - "title": "Xiaomi Miio" + "description": "Select the Xiaomi Miio device to setup." } } }, @@ -90,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "Use cloud to get connected subdevices" - }, - "description": "Specify optional settings", - "title": "Xiaomi Miio" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/es-419.json b/homeassistant/components/xiaomi_miio/translations/es-419.json deleted file mode 100644 index b3bbc25ebba..00000000000 --- a/homeassistant/components/xiaomi_miio/translations/es-419.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "config": { - "error": { - "no_device_selected": "Ning\u00fan dispositivo seleccionado, seleccione un dispositivo." - }, - "step": { - "gateway": { - "data": { - "host": "Direcci\u00f3n IP", - "name": "Nombre de la puerta de enlace", - "token": "Token API" - }, - "description": "Necesitar\u00e1 el token API, consulte https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token para obtener instrucciones.", - "title": "Conectarse a una puerta de enlace Xiaomi" - }, - "user": { - "data": { - "gateway": "Conectarse a una puerta de enlace Xiaomi" - }, - "description": "Seleccione a qu\u00e9 dispositivo desea conectarse.", - "title": "Xiaomi Miio" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/es.json b/homeassistant/components/xiaomi_miio/translations/es.json index 2c1dad04111..c1a3eae784b 100644 --- a/homeassistant/components/xiaomi_miio/translations/es.json +++ b/homeassistant/components/xiaomi_miio/translations/es.json @@ -12,7 +12,6 @@ "cloud_credentials_incomplete": "Las credenciales de la nube est\u00e1n incompletas, por favor, rellene el nombre de usuario, la contrase\u00f1a y el pa\u00eds", "cloud_login_error": "No se ha podido iniciar sesi\u00f3n en Xiaomi Miio Cloud, comprueba las credenciales.", "cloud_no_devices": "No se han encontrado dispositivos en esta cuenta de Xiaomi Miio.", - "no_device_selected": "No se ha seleccionado ning\u00fan dispositivo, por favor, seleccione un dispositivo.", "unknown_device": "No se conoce el modelo del dispositivo, no se puede configurar el dispositivo mediante el flujo de configuraci\u00f3n.", "wrong_token": "Error de suma de comprobaci\u00f3n, token err\u00f3neo" }, @@ -25,42 +24,19 @@ "cloud_username": "Nombre de usuario de la nube", "manual": "Configurar manualmente (no recomendado)" }, - "description": "Inicie sesi\u00f3n en la nube de Xiaomi Miio, consulte https://www.openhab.org/addons/bindings/miio/#country-servers para conocer el servidor de la nube que debe utilizar.", - "title": "Con\u00e9ctese a un dispositivo Xiaomi Miio o una puerta de enlace Xiaomi" + "description": "Inicie sesi\u00f3n en la nube de Xiaomi Miio, consulte https://www.openhab.org/addons/bindings/miio/#country-servers para conocer el servidor de la nube que debe utilizar." }, "connect": { "data": { "model": "Modelo del dispositivo" - }, - "description": "Seleccione manualmente el modelo de dispositivo entre los modelos admitidos.", - "title": "Con\u00e9ctese a un dispositivo Xiaomi Miio o una puerta de enlace Xiaomi" - }, - "device": { - "data": { - "host": "Direcci\u00f3n IP", - "model": "Modelo de dispositivo (opcional)", - "name": "Nombre del dispositivo", - "token": "Token API" - }, - "description": "Necesitar\u00e1 la clave de 32 caracteres Token API, consulte https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token para obtener instrucciones. Tenga en cuenta que esta Token API es diferente de la clave utilizada por la integraci\u00f3n de Xiaomi Aqara.", - "title": "Con\u00e9ctese a un dispositivo Xiaomi Miio o Xiaomi Gateway" - }, - "gateway": { - "data": { - "host": "Direcci\u00f3n IP", - "name": "Nombre del Gateway", - "token": "Token API" - }, - "description": "Necesitar\u00e1s el token de la API de 32 caracteres, revisa https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token para m\u00e1s instrucciones. Por favor, ten en cuenta que este token es diferente de la clave utilizada por la integraci\u00f3n de Xiaomi Aqara.", - "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" + "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." }, "reauth_confirm": { "description": "La integraci\u00f3n de Xiaomi Miio necesita volver a autenticar tu cuenta para actualizar los tokens o a\u00f1adir las credenciales de la nube que faltan.", @@ -70,15 +46,7 @@ "data": { "select_device": "Dispositivo Miio" }, - "description": "Selecciona el dispositivo Xiaomi Miio para configurarlo.", - "title": "Con\u00e9ctese a un dispositivo Xiaomi Miio o una puerta de enlace Xiaomi" - }, - "user": { - "data": { - "gateway": "Conectar con un Xiaomi Gateway" - }, - "description": "Selecciona a qu\u00e9 dispositivo quieres conectar.", - "title": "Xiaomi Miio" + "description": "Selecciona el dispositivo Xiaomi Miio para configurarlo." } } }, @@ -90,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "Utiliza la nube para conectar subdispositivos" - }, - "description": "Especifica los ajustes opcionales", - "title": "Xiaomi Miio" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/et.json b/homeassistant/components/xiaomi_miio/translations/et.json index 6fd3c4257ef..fef6a73622a 100644 --- a/homeassistant/components/xiaomi_miio/translations/et.json +++ b/homeassistant/components/xiaomi_miio/translations/et.json @@ -12,7 +12,6 @@ "cloud_credentials_incomplete": "Pilve mandaat on poolik, palun t\u00e4ida kasutajanimi, salas\u00f5na ja riik", "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.", "wrong_token": "Kontrollsumma t\u00f5rge, vigane r\u00e4si" }, @@ -25,42 +24,19 @@ "cloud_username": "Pilve kasutajatunnus", "manual": "Seadista k\u00e4sitsi (pole soovitatav)" }, - "description": "Logi sisse Xiaomi Miio pilve, vaata https://www.openhab.org/addons/bindings/miio/#country-servers pilveserveri kasutamiseks.", - "title": "\u00dchenda Xiaomi Miio seade v\u00f5i Xiaomi Gateway" + "description": "Logi sisse Xiaomi Miio pilve, vaata https://www.openhab.org/addons/bindings/miio/#country-servers pilveserveri kasutamiseks." }, "connect": { "data": { "model": "Seadme mudel" - }, - "description": "Vali seadme mudel k\u00e4sitsi toetatud mudelite hulgast.", - "title": "\u00dchenda Xiaomi Miio seade v\u00f5i Xiaomi Gateway" - }, - "device": { - "data": { - "host": "IP-aadress", - "model": "Seadme mudel (valikuline)", - "name": "Seadme nimi", - "token": "API v\u00f5ti" - }, - "description": "Vaja on 32 t\u00e4hem\u00e4rgilist v\u00f5tit API v\u00f5ti , juhiste saamiseks vaata https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. Pane t\u00e4hele, et see v\u00f5ti API v\u00f5ti erineb Xiaomi Aqara sidumises kasutatavast v\u00f5tmest.", - "title": "\u00dchenda Xiaomi Miio seade v\u00f5i Xiaomi Gateway" - }, - "gateway": { - "data": { - "host": "IP aadress", - "name": "L\u00fc\u00fcsi nimi", - "token": "API string" - }, - "description": "On vaja 32-kohalist API-tokenti, juhiste saamiseks vaata lehte https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. Pane t\u00e4hele, et see token erineb Xiaomi Aqara sidumisel kasutatavast v\u00f5tmest.", - "title": "Loo \u00fchendus Xiaomi l\u00fc\u00fcsiga" + } }, "manual": { "data": { "host": "IP aadress", "token": "API v\u00f5ti" }, - "description": "On vajalik 32 t\u00e4hem\u00e4rki API v\u00f5ti, vt. https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token juhiseid. Pane t\u00e4hele, et see API v\u00f5ti erineb Xiaomi Aqara sidumises kasutatavast v\u00f5tmest.", - "title": "\u00dchenda Xiaomi Miio seade v\u00f5i Xiaomi Gateway" + "description": "On vajalik 32 t\u00e4hem\u00e4rki API v\u00f5ti, vt. https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token juhiseid. Pane t\u00e4hele, et see API v\u00f5ti erineb Xiaomi Aqara sidumises kasutatavast v\u00f5tmest." }, "reauth_confirm": { "description": "Xiaomi Miio sidumine peab konto uuesti tuvastama, et v\u00e4rskendada p\u00e4\u00e4sulube v\u00f5i lisada puuduv pilvemandaat.", @@ -70,15 +46,7 @@ "data": { "select_device": "Miio seade" }, - "description": "Vali seadistamiseks Xiaomi Miio seade.", - "title": "\u00dchenda Xiaomi Miio seade v\u00f5i Xiaomi Gateway" - }, - "user": { - "data": { - "gateway": "Loo \u00fchendus Xiaomi l\u00fc\u00fcsiga" - }, - "description": "Vali seade millega soovid \u00fchenduse luua.", - "title": "Xiaomi Miio" + "description": "Vali seadistamiseks Xiaomi Miio seade." } } }, @@ -90,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "\u00dchendatud alamseadmete hankimiseks kasuta pilve" - }, - "description": "Valikuliste s\u00e4tete m\u00e4\u00e4ramine", - "title": "Xiaomi Miio" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/fi.json b/homeassistant/components/xiaomi_miio/translations/fi.json deleted file mode 100644 index ab360a82a8f..00000000000 --- a/homeassistant/components/xiaomi_miio/translations/fi.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "no_device_selected": "Ei valittuja laitteita. Ole hyv\u00e4 ja valitse yksi." - }, - "step": { - "gateway": { - "data": { - "host": "IP-osoite", - "name": "Yhdysk\u00e4yt\u00e4v\u00e4n nimi", - "token": "API-tunnus" - }, - "title": "Yhdist\u00e4 Xiaomi Gatewayhin" - }, - "user": { - "data": { - "gateway": "Yhdist\u00e4 Xiaomi Gatewayhin" - }, - "description": "Valitse laite, johon haluat muodostaa yhteyden.", - "title": "Xiaomi Miio" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/fr.json b/homeassistant/components/xiaomi_miio/translations/fr.json index 2a9d8d65c77..91c6b745816 100644 --- a/homeassistant/components/xiaomi_miio/translations/fr.json +++ b/homeassistant/components/xiaomi_miio/translations/fr.json @@ -12,7 +12,6 @@ "cloud_credentials_incomplete": "Identifiants cloud incomplets, veuillez renseigner le nom d'utilisateur, le mot de passe et le pays", "cloud_login_error": "Impossible de se connecter \u00e0 Xioami Miio Cloud, v\u00e9rifiez les informations d'identification.", "cloud_no_devices": "Aucun appareil trouv\u00e9 dans ce compte cloud Xiaomi Miio.", - "no_device_selected": "Aucun appareil s\u00e9lectionn\u00e9, veuillez s\u00e9lectionner un appareil.", "unknown_device": "Le mod\u00e8le d'appareil n'est pas connu, impossible de configurer l'appareil \u00e0 l'aide du flux de configuration.", "wrong_token": "Erreur de somme de contr\u00f4le, jeton incorrect" }, @@ -25,42 +24,19 @@ "cloud_username": "Nom d'utilisateur cloud", "manual": "Configurer manuellement (non recommand\u00e9)" }, - "description": "Connectez-vous au cloud Xiaomi Miio, voir https://www.openhab.org/addons/bindings/miio/#country-servers pour le serveur cloud \u00e0 utiliser.", - "title": "Se connecter \u00e0 un appareil Xiaomi Miio ou \u00e0 une passerelle Xiaomi" + "description": "Connectez-vous au cloud Xiaomi Miio, voir https://www.openhab.org/addons/bindings/miio/#country-servers pour le serveur cloud \u00e0 utiliser." }, "connect": { "data": { "model": "Mod\u00e8le d'appareil" - }, - "description": "S\u00e9lectionner manuellement le mod\u00e8le d'appareil parmi les mod\u00e8les pris en charge.", - "title": "Se connecter \u00e0 un appareil Xiaomi Miio ou \u00e0 une passerelle Xiaomi" - }, - "device": { - "data": { - "host": "Adresse IP", - "model": "Mod\u00e8le d'appareil (facultatif)", - "name": "Nom de l'appareil", - "token": "Jeton d'API" - }, - "description": "Vous aurez besoin des 32 caract\u00e8res Jeton d'API , voir https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token pour les instructions. Veuillez noter que cette Jeton d'API est diff\u00e9rente de la cl\u00e9 utilis\u00e9e par l'int\u00e9gration Xiaomi Aqara.", - "title": "Connectez-vous \u00e0 un appareil Xiaomi Miio ou \u00e0 une passerelle Xiaomi" - }, - "gateway": { - "data": { - "host": "Adresse IP", - "name": "Nom de la passerelle", - "token": "Jeton d'API" - }, - "description": "Vous aurez besoin du jeton API, voir https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token pour les instructions.", - "title": "Se connecter \u00e0 la passerelle Xiaomi" + } }, "manual": { "data": { "host": "Adresse IP", "token": "Jeton d'API" }, - "description": "Vous aurez besoin du Jeton d'API de 32 caract\u00e8res, voir https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token pour les instructions. Veuillez noter que ce Jeton d'API est diff\u00e9rent de la cl\u00e9 utilis\u00e9e par l'int\u00e9gration Xiaomi Aqara.", - "title": "Se connecter \u00e0 un appareil Xiaomi Miio ou \u00e0 une passerelle Xiaomi" + "description": "Vous aurez besoin du Jeton d'API de 32 caract\u00e8res, voir https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token pour les instructions. Veuillez noter que ce Jeton d'API est diff\u00e9rent de la cl\u00e9 utilis\u00e9e par l'int\u00e9gration Xiaomi Aqara." }, "reauth_confirm": { "description": "L'int\u00e9gration de Xiaomi Miio doit r\u00e9-authentifier votre compte afin de mettre \u00e0 jour les jetons ou d'ajouter les informations d'identification cloud manquantes.", @@ -70,15 +46,7 @@ "data": { "select_device": "Appareil Miio" }, - "description": "S\u00e9lectionner l'appareil Xiaomi Miio \u00e0 configurer.", - "title": "Se connecter \u00e0 un appareil Xiaomi Miio ou \u00e0 une passerelle Xiaomi" - }, - "user": { - "data": { - "gateway": "Se connecter \u00e0 la passerelle Xiaomi" - }, - "description": "S\u00e9lectionnez \u00e0 quel appareil vous souhaitez vous connecter.", - "title": "Xiaomi Miio" + "description": "S\u00e9lectionner l'appareil Xiaomi Miio \u00e0 configurer." } } }, @@ -90,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "Utiliser le cloud pour connecter des sous-appareils" - }, - "description": "Sp\u00e9cifiez les param\u00e8tres optionnels", - "title": "Xiaomi Miio" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/he.json b/homeassistant/components/xiaomi_miio/translations/he.json index db0cee16884..1a21bd9840a 100644 --- a/homeassistant/components/xiaomi_miio/translations/he.json +++ b/homeassistant/components/xiaomi_miio/translations/he.json @@ -12,7 +12,6 @@ "cloud_credentials_incomplete": "\u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 \u05e2\u05e0\u05df \u05d0\u05d9\u05e0\u05dd \u05de\u05dc\u05d0\u05d9\u05dd, \u05e0\u05d0 \u05dc\u05de\u05dc\u05d0 \u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9, \u05e1\u05d9\u05e1\u05de\u05d4 \u05d5\u05de\u05d3\u05d9\u05e0\u05d4", "cloud_login_error": "\u05dc\u05d0 \u05d4\u05d9\u05ea\u05d4 \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05e2\u05e0\u05df \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05de\u05d9\u05d5, \u05e0\u05d0 \u05dc\u05d1\u05d3\u05d5\u05e7 \u05d0\u05ea \u05d4\u05d0\u05d9\u05e9\u05d5\u05e8\u05d9\u05dd.", "cloud_no_devices": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05d7\u05e9\u05d1\u05d5\u05df \u05d4\u05e2\u05e0\u05df \u05d4\u05d6\u05d4 \u05e9\u05dc \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05de\u05d9\u05d5.", - "no_device_selected": "\u05dc\u05d0 \u05e0\u05d1\u05d7\u05e8 \u05d4\u05ea\u05e7\u05df, \u05e0\u05d0 \u05d1\u05d7\u05e8 \u05d4\u05ea\u05e7\u05df \u05d0\u05d7\u05d3.", "unknown_device": "\u05d3\u05d2\u05dd \u05d4\u05d4\u05ea\u05e7\u05df \u05d0\u05d9\u05e0\u05d5 \u05d9\u05d3\u05d5\u05e2, \u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05d1\u05d0\u05de\u05e6\u05e2\u05d5\u05ea \u05d6\u05e8\u05d9\u05de\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4.", "wrong_token": "\u05e9\u05d2\u05d9\u05d0\u05ea \u05d1\u05d3\u05d9\u05e7\u05ea \u05e1\u05d9\u05db\u05d5\u05dd, \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05e9\u05d2\u05d5\u05d9" }, @@ -25,42 +24,19 @@ "cloud_username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05e2\u05e0\u05df", "manual": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d9\u05d3\u05e0\u05d9\u05ea (\u05dc\u05d0 \u05de\u05d5\u05de\u05dc\u05e5)" }, - "description": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5 \u05dc\u05e2\u05e0\u05df \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05de\u05d9\u05d5, \u05e8\u05d0\u05d5 https://www.openhab.org/addons/bindings/miio/#country-servers \u05dc\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05e9\u05e8\u05ea \u05d4\u05e2\u05e0\u05df.", - "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05db\u05e9\u05d9\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5 \u05d0\u05d5 \u05dc\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9" + "description": "\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5 \u05dc\u05e2\u05e0\u05df \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05de\u05d9\u05d5, \u05e8\u05d0\u05d5 https://www.openhab.org/addons/bindings/miio/#country-servers \u05dc\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05e9\u05e8\u05ea \u05d4\u05e2\u05e0\u05df." }, "connect": { "data": { "model": "\u05d3\u05d2\u05dd \u05d4\u05ea\u05e7\u05df" - }, - "description": "\u05d1\u05d7\u05e8 \u05d9\u05d3\u05e0\u05d9\u05ea \u05d0\u05ea \u05d3\u05d2\u05dd \u05d4\u05d4\u05ea\u05e7\u05df \u05de\u05d4\u05d3\u05d2\u05de\u05d9\u05dd \u05d4\u05e0\u05ea\u05de\u05db\u05d9\u05dd.", - "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05db\u05e9\u05d9\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5 \u05d0\u05d5 \u05dc\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9" - }, - "device": { - "data": { - "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", - "model": "\u05d3\u05d2\u05dd \u05d4\u05ea\u05e7\u05df (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)", - "name": "\u05e9\u05dd \u05d4\u05d4\u05ea\u05e7\u05df", - "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" - }, - "description": "\u05d0\u05ea\u05d4 \u05d6\u05e7\u05d5\u05e7 \u05dc-32 \u05ea\u05d5\u05d5\u05d9 \u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df API , \u05e8\u05d0\u05d4 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u05dc\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea. \u05e9\u05d9\u05dd \u05dc\u05d1, \u05db\u05d9 \u05d0\u05e1\u05d9\u05de\u05d5\u05df API \u05e9\u05d5\u05e0\u05d4 \u05de\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05de\u05e9\u05de\u05e9 \u05d0\u05ea \u05e9\u05d9\u05dc\u05d5\u05d1 Xiaomi Aqara.", - "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05db\u05e9\u05d9\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5 \u05d0\u05d5 \u05dc\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9" - }, - "gateway": { - "data": { - "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", - "name": "\u05e9\u05dd \u05d4\u05e9\u05e2\u05e8", - "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" - }, - "description": "\u05e0\u05d3\u05e8\u05e9\u05d9\u05dd 32 \u05ea\u05d5\u05d5\u05d9 \u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df API, \u05e8\u05d0\u05d4 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u05dc\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea. \u05e9\u05d9\u05dd \u05dc\u05d1, \u05db\u05d9 \u05d0\u05e1\u05d9\u05de\u05d5\u05df API \u05e9\u05d5\u05e0\u05d4 \u05de\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05de\u05e9\u05de\u05e9 \u05d0\u05ea \u05e9\u05d9\u05dc\u05d5\u05d1 Xiaomi Aqara.", - "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9" + } }, "manual": { "data": { "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", "token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df API" }, - "description": "\u05ea\u05d6\u05d3\u05e7\u05e7 \u05dc-32 \u05ea\u05d5\u05d5\u05d9 \u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df API, \u05e8\u05d0\u05d4 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u05dc\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea. \u05e9\u05d9\u05dd \u05dc\u05d1, \u05d6\u05d4 \u05d0\u05e1\u05d9\u05de\u05d5\u05df API \u05e9\u05d5\u05e0\u05d4 \u05de\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05de\u05e9\u05de\u05e9 \u05dc\u05e9\u05d9\u05dc\u05d5\u05d1 Xiaomi Aqara.", - "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05db\u05e9\u05d9\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5 \u05d0\u05d5 \u05dc\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9" + "description": "\u05ea\u05d6\u05d3\u05e7\u05e7 \u05dc-32 \u05ea\u05d5\u05d5\u05d9 \u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df API, \u05e8\u05d0\u05d4 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u05dc\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea. \u05e9\u05d9\u05dd \u05dc\u05d1, \u05d6\u05d4 \u05d0\u05e1\u05d9\u05de\u05d5\u05df API \u05e9\u05d5\u05e0\u05d4 \u05de\u05d4\u05de\u05e4\u05ea\u05d7 \u05d4\u05de\u05e9\u05de\u05e9 \u05dc\u05e9\u05d9\u05dc\u05d5\u05d1 Xiaomi Aqara." }, "reauth_confirm": { "description": "\u05d4\u05e9\u05d9\u05dc\u05d5\u05d1 \u05e9\u05dc \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05de\u05d9\u05d5 \u05e6\u05e8\u05d9\u05db\u05d4 \u05dc\u05d0\u05de\u05ea \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05e2\u05d3\u05db\u05df \u05d0\u05ea \u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05e0\u05d9\u05dd \u05d0\u05d5 \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 \u05e2\u05e0\u05df \u05d7\u05e1\u05e8\u05d9\u05dd.", @@ -70,15 +46,7 @@ "data": { "select_device": "\u05d4\u05ea\u05e7\u05df \u05de\u05d9\u05d5" }, - "description": "\u05d1\u05d7\u05e8 \u05d0\u05ea \u05d4\u05ea\u05e7\u05df \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05de\u05d9\u05d5 \u05dc\u05d4\u05ea\u05e7\u05e0\u05d4.", - "title": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05de\u05db\u05e9\u05d9\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5 \u05d0\u05d5 \u05dc\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9" - }, - "user": { - "data": { - "gateway": "\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05e9\u05e2\u05e8 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9" - }, - "description": "\u05d1\u05d7\u05e8 \u05dc\u05d0\u05d9\u05d6\u05d4 \u05d4\u05ea\u05e7\u05df \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8.", - "title": "\u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5" + "description": "\u05d1\u05d7\u05e8 \u05d0\u05ea \u05d4\u05ea\u05e7\u05df \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05de\u05d9\u05d5 \u05dc\u05d4\u05ea\u05e7\u05e0\u05d4." } } }, @@ -90,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "\u05d4\u05e9\u05ea\u05de\u05e9 \u05d1\u05e2\u05e0\u05df \u05db\u05d3\u05d9 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05ea\u05ea-\u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05de\u05d7\u05d5\u05d1\u05e8\u05d9\u05dd" - }, - "description": "\u05e6\u05d9\u05d9\u05df \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9\u05d5\u05ea", - "title": "\u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json index 189e9906e24..51f093b871c 100644 --- a/homeassistant/components/xiaomi_miio/translations/hu.json +++ b/homeassistant/components/xiaomi_miio/translations/hu.json @@ -12,7 +12,6 @@ "cloud_credentials_incomplete": "A felh\u0151alap\u00fa hiteles\u00edt\u0151 adatok hi\u00e1nyosak, k\u00e9rj\u00fck, adja meg a felhaszn\u00e1l\u00f3nevet, a jelsz\u00f3t \u00e9s az orsz\u00e1got", "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.", "wrong_token": "Ellen\u0151rz\u0151\u00f6sszeg-hiba, hib\u00e1s token" }, @@ -25,42 +24,19 @@ "cloud_username": "Felh\u0151 felhaszn\u00e1l\u00f3neve", "manual": "Konfigur\u00e1l\u00e1s manu\u00e1lisan (nem aj\u00e1nlott)" }, - "description": "Jelentkezzen be a Xiaomi Miio felh\u0151be, a felh\u0151szerver haszn\u00e1lat\u00e1hoz l\u00e1sd: https://www.openhab.org/addons/bindings/miio/#country-servers.", - "title": "Csatlakozzon egy Xiaomi Miio eszk\u00f6zh\u00f6z vagy a Xiaomi Gateway-hez" + "description": "Jelentkezzen be a Xiaomi Miio felh\u0151be, a felh\u0151szerver haszn\u00e1lat\u00e1hoz l\u00e1sd: https://www.openhab.org/addons/bindings/miio/#country-servers." }, "connect": { "data": { "model": "Eszk\u00f6z modell" - }, - "description": "V\u00e1lassza ki manu\u00e1lisan a modellt a t\u00e1mogatott modellek k\u00f6z\u00fcl.", - "title": "Csatlakozzon egy Xiaomi Miio eszk\u00f6zh\u00f6z vagy a Xiaomi Gateway-hez" - }, - "device": { - "data": { - "host": "IP c\u00edm", - "model": "Eszk\u00f6z modell (opcion\u00e1lis)", - "name": "Eszk\u00f6z neve", - "token": "API Token" - }, - "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": { - "data": { - "host": "IP c\u00edm", - "name": "K\u00f6zponti egys\u00e9g neve", - "token": "API Token" - }, - "description": "Sz\u00fcks\u00e9ge lesz az API Tokenre, tov\u00e1bbi inforaciok: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token. K\u00e9rj\u00fck, vegye figyelembe, hogy ez az API Token k\u00fcl\u00f6nb\u00f6zik a Xiaomi Aqara integr\u00e1ci\u00f3 \u00e1ltal haszn\u00e1lt kulcst\u00f3l.", - "title": "Csatlakozzon egy Xiaomi K\u00f6zponti egys\u00e9ghez" + } }, "manual": { "data": { "host": "IP c\u00edm", "token": "API Token" }, - "description": "Sz\u00fcks\u00e9ge lesz a 32 karakteres API Token -re. Az utas\u00edt\u00e1sok\u00e9rt l\u00e1sd: https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token. Felh\u00edvjuk figyelm\u00e9t, hogy ez a API Token elt\u00e9r a Xiaomi Aqara integr\u00e1ci\u00f3 \u00e1ltal haszn\u00e1lt kulcst\u00f3l.", - "title": "Csatlakozzon egy Xiaomi Miio eszk\u00f6zh\u00f6z vagy a Xiaomi Gateway-hez" + "description": "Sz\u00fcks\u00e9ge lesz a 32 karakteres API Token -re. Az utas\u00edt\u00e1sok\u00e9rt l\u00e1sd: https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token. Felh\u00edvjuk figyelm\u00e9t, hogy ez a API Token elt\u00e9r a Xiaomi Aqara integr\u00e1ci\u00f3 \u00e1ltal haszn\u00e1lt kulcst\u00f3l." }, "reauth_confirm": { "description": "A tokenek friss\u00edt\u00e9s\u00e9hez vagy hi\u00e1nyz\u00f3 felh\u0151alap\u00fa hiteles\u00edt\u0151 adatok hozz\u00e1ad\u00e1s\u00e1hoz a Xiaomi Miio integr\u00e1ci\u00f3nak \u00fajra hiteles\u00edtenie kell a fi\u00f3kj\u00e1t.", @@ -70,15 +46,7 @@ "data": { "select_device": "Miio eszk\u00f6z" }, - "description": "V\u00e1lassza ki a be\u00e1ll\u00edtand\u00f3 Xiaomi Miio eszk\u00f6zt.", - "title": "Csatlakoz\u00e1s Xiaomi Miio eszk\u00f6zh\u00f6z vagy Xiaomi Gateway-hez" - }, - "user": { - "data": { - "gateway": "Csatlakozzon egy Xiaomi K\u00f6zponti egys\u00e9ghez" - }, - "description": "V\u00e1lassza ki, melyik k\u00e9sz\u00fcl\u00e9khez szeretne csatlakozni.", - "title": "Xiaomi Miio" + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtand\u00f3 Xiaomi Miio eszk\u00f6zt." } } }, @@ -90,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "Haszn\u00e1lja a felh\u0151t a csatlakoztatott alegys\u00e9gek megszerz\u00e9s\u00e9hez" - }, - "description": "Adja meg az opcion\u00e1lis be\u00e1ll\u00edt\u00e1sokat", - "title": "Xiaomi Miio" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/id.json b/homeassistant/components/xiaomi_miio/translations/id.json index 4aae8d6396c..8d1d70642aa 100644 --- a/homeassistant/components/xiaomi_miio/translations/id.json +++ b/homeassistant/components/xiaomi_miio/translations/id.json @@ -12,7 +12,6 @@ "cloud_credentials_incomplete": "Kredensial cloud tidak lengkap, isi nama pengguna, kata sandi, dan negara", "cloud_login_error": "Tidak dapat masuk ke Xiaomi Miio Cloud, periksa kredensialnya.", "cloud_no_devices": "Tidak ada perangkat yang ditemukan di akun cloud Xiaomi Miio ini.", - "no_device_selected": "Tidak ada perangkat yang dipilih, pilih satu perangkat.", "unknown_device": "Model perangkat tidak diketahui, tidak dapat menyiapkan perangkat menggunakan alur konfigurasi.", "wrong_token": "Kesalahan checksum, token salah" }, @@ -25,42 +24,19 @@ "cloud_username": "Nama pengguna cloud", "manual": "Konfigurasi secara manual (tidak disarankan)" }, - "description": "Masuk ke cloud Xiaomi Miio, lihat https://www.openhab.org/addons/bindings/miio/#country-servers untuk menemukan server cloud yang digunakan.", - "title": "Hubungkan ke Perangkat Xiaomi Miio atau Xiaomi Gateway" + "description": "Masuk ke cloud Xiaomi Miio, lihat https://www.openhab.org/addons/bindings/miio/#country-servers untuk menemukan server cloud yang digunakan." }, "connect": { "data": { "model": "Model perangkat" - }, - "description": "Pilih model perangkat secara manual dari model yang didukung.", - "title": "Hubungkan ke Perangkat Xiaomi Miio atau Xiaomi Gateway" - }, - "device": { - "data": { - "host": "Alamat IP", - "model": "Model perangkat (Opsional)", - "name": "Nama perangkat", - "token": "Token API" - }, - "description": "Anda akan membutuhkan Token API 32 karakter, lihat https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token untuk mendapatkan petunjuknya. Perhatikan bahwa Token API ini berbeda dari kunci yang digunakan oleh integrasi Xiaomi Aqara.", - "title": "Hubungkan ke Perangkat Xiaomi Miio atau Xiaomi Gateway" - }, - "gateway": { - "data": { - "host": "Alamat IP", - "name": "Nama Gateway", - "token": "Token API" - }, - "description": "Anda akan membutuhkan Token API 32 karakter, lihat https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token untuk mendapatkan petunjuknya. Perhatikan bahwa Token API ini berbeda dari kunci yang digunakan oleh integrasi Xiaomi Aqara.", - "title": "Hubungkan ke Xiaomi Gateway" + } }, "manual": { "data": { "host": "Alamat IP", "token": "Token API" }, - "description": "Anda akan membutuhkan Token API 32 karakter, baca petunjuknya di https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token. Perhatikan bahwa Token API ini berbeda dengan kunci yang digunakan untuk integrasi Xiaomi Aqara.", - "title": "Hubungkan ke Perangkat Xiaomi Miio atau Xiaomi Gateway" + "description": "Anda akan membutuhkan Token API 32 karakter, baca petunjuknya di https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token. Perhatikan bahwa Token API ini berbeda dengan kunci yang digunakan untuk integrasi Xiaomi Aqara." }, "reauth_confirm": { "description": "Integrasi Xiaomi Miio perlu mengautentikasi ulang akun Anda untuk memperbarui token atau menambahkan kredensial cloud yang hilang.", @@ -70,15 +46,7 @@ "data": { "select_device": "Perangkat Miio" }, - "description": "Pilih perangkat Xiaomi Miio untuk disiapkan.", - "title": "Hubungkan ke Perangkat Xiaomi Miio atau Xiaomi Gateway" - }, - "user": { - "data": { - "gateway": "Hubungkan ke Xiaomi Gateway" - }, - "description": "Pilih perangkat mana yang ingin disambungkan.", - "title": "Xiaomi Miio" + "description": "Pilih perangkat Xiaomi Miio untuk disiapkan." } } }, @@ -90,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "Gunakan cloud untuk mendapatkan subperangkat yang tersambung" - }, - "description": "Tentukan pengaturan opsional", - "title": "Xiaomi Miio" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/it.json b/homeassistant/components/xiaomi_miio/translations/it.json index b643f7df4de..51b5f108751 100644 --- a/homeassistant/components/xiaomi_miio/translations/it.json +++ b/homeassistant/components/xiaomi_miio/translations/it.json @@ -12,7 +12,6 @@ "cloud_credentials_incomplete": "Credenziali cloud incomplete, inserisci nome utente, password e paese", "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, seleziona un dispositivo.", "unknown_device": "Il modello del dispositivo non \u00e8 noto, non \u00e8 possibile configurare il dispositivo utilizzando il flusso di configurazione.", "wrong_token": "Errore del codice di controllo, token errato" }, @@ -25,42 +24,19 @@ "cloud_username": "Nome utente cloud", "manual": "Configura manualmente (non consigliato)" }, - "description": "Accedi al cloud Xiaomi Miio, vedi https://www.openhab.org/addons/bindings/miio/#country-servers per utilizzare il server cloud.", - "title": "Connettiti a un dispositivo Xiaomi Miio o Xiaomi Gateway" + "description": "Accedi al cloud Xiaomi Miio, vedi https://www.openhab.org/addons/bindings/miio/#country-servers per utilizzare il server cloud." }, "connect": { "data": { "model": "Modello del dispositivo" - }, - "description": "Seleziona manualmente il modello del dispositivo tra i modelli supportati.", - "title": "Connettiti a un dispositivo Xiaomi Miio o Xiaomi Gateway" - }, - "device": { - "data": { - "host": "Indirizzo IP", - "model": "Modello del dispositivo (opzionale)", - "name": "Nome del dispositivo", - "token": "Token API" - }, - "description": "Avrai bisogno dei 32 caratteri del Token API, vedi https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token per istruzioni. Tieni presente che questo Token API \u00e8 diverso dalla chiave utilizzata dall'integrazione Xiaomi Aqara.", - "title": "Connettiti a un dispositivo Xiaomi Miio o Xiaomi Gateway" - }, - "gateway": { - "data": { - "host": "Indirizzo IP", - "name": "Nome del Gateway", - "token": "Token API" - }, - "description": "\u00c8 necessario il Token API di 32 caratteri, vedi https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token per le istruzioni. Nota che questo Token API \u00e8 diverso dalla chiave usata dall'integrazione di Xiaomi Aqara.", - "title": "Connettiti a un Xiaomi Gateway " + } }, "manual": { "data": { "host": "Indirizzo IP", "token": "Token API" }, - "description": "Avrai bisogno dei 32 caratteri del Token API, vedi https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token per istruzioni. Tieni presente che questo Token API \u00e8 diverso dalla chiave utilizzata dall'integrazione Xiaomi Aqara.", - "title": "Connettiti a un dispositivo Xiaomi Miio o Xiaomi Gateway" + "description": "Avrai bisogno dei 32 caratteri del Token API, vedi https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token per istruzioni. Tieni presente che questo Token API \u00e8 diverso dalla chiave utilizzata dall'integrazione Xiaomi Aqara." }, "reauth_confirm": { "description": "L'integrazione di Xiaomi Miio deve autenticare nuovamente il tuo account per aggiornare i token o aggiungere credenziali cloud mancanti.", @@ -70,15 +46,7 @@ "data": { "select_device": "Dispositivo Miio" }, - "description": "Seleziona il dispositivo Xiaomi Miio da configurare.", - "title": "Connettiti a un dispositivo Xiaomi Miio o Xiaomi Gateway" - }, - "user": { - "data": { - "gateway": "Connettiti a un Xiaomi Gateway" - }, - "description": "Seleziona a quale dispositivo desideri collegarti.", - "title": "Xiaomi Miio" + "description": "Seleziona il dispositivo Xiaomi Miio da configurare." } } }, @@ -90,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "Usa il cloud per connettere i sottodispositivi" - }, - "description": "Specifica le impostazioni opzionali", - "title": "Xiaomi Miio" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/ja.json b/homeassistant/components/xiaomi_miio/translations/ja.json index 44c6f7f149e..0877850f754 100644 --- a/homeassistant/components/xiaomi_miio/translations/ja.json +++ b/homeassistant/components/xiaomi_miio/translations/ja.json @@ -12,7 +12,6 @@ "cloud_credentials_incomplete": "\u30af\u30e9\u30a6\u30c9\u8a8d\u8a3c\u304c\u4e0d\u5b8c\u5168\u3067\u3059\u3002\u30e6\u30fc\u30b6\u30fc\u540d\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3001\u56fd\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "cloud_login_error": "Xiaomi Miio Cloud\u306b\u30ed\u30b0\u30a4\u30f3\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u8a8d\u8a3c\u60c5\u5831\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "cloud_no_devices": "\u3053\u306eXiaomi Miio cloud\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002", - "no_device_selected": "\u30c7\u30d0\u30a4\u30b9\u304c\u9078\u629e\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c7\u30d0\u30a4\u30b9\u30921\u3064\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown_device": "\u30c7\u30d0\u30a4\u30b9\u30e2\u30c7\u30eb\u304c\u4e0d\u660e\u306a\u306e\u3067\u3001\u69cb\u6210\u30d5\u30ed\u30fc\u3092\u4f7f\u7528\u3057\u3066\u30c7\u30d0\u30a4\u30b9\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3067\u304d\u307e\u305b\u3093\u3002", "wrong_token": "\u30c1\u30a7\u30c3\u30af\u30b5\u30e0\u30a8\u30e9\u30fc\u3001\u9593\u9055\u3063\u305f\u30c8\u30fc\u30af\u30f3" }, @@ -25,42 +24,19 @@ "cloud_username": "\u30af\u30e9\u30a6\u30c9\u306e\u30e6\u30fc\u30b6\u30fc\u540d", "manual": "\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b(\u975e\u63a8\u5968)" }, - "description": "Xiaomi Miio cloud\u306b\u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3059\u3002\u4f7f\u7528\u3059\u308b\u30af\u30e9\u30a6\u30c9\u30b5\u30fc\u30d0\u30fc\u306b\u3064\u3044\u3066\u306f\u3001https://www.openhab.org/addons/bindings/miio/#country-servers \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "Xiaomi Miio\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u3001Xiaomi Gateway\u306b\u63a5\u7d9a" + "description": "Xiaomi Miio cloud\u306b\u30ed\u30b0\u30a4\u30f3\u3057\u307e\u3059\u3002\u4f7f\u7528\u3059\u308b\u30af\u30e9\u30a6\u30c9\u30b5\u30fc\u30d0\u30fc\u306b\u3064\u3044\u3066\u306f\u3001https://www.openhab.org/addons/bindings/miio/#country-servers \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "connect": { "data": { "model": "\u30c7\u30d0\u30a4\u30b9\u30e2\u30c7\u30eb" - }, - "description": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u30e2\u30c7\u30eb\u304b\u3089\u30c7\u30d0\u30a4\u30b9 \u30e2\u30c7\u30eb\u3092\u624b\u52d5\u3067\u9078\u629e\u3057\u307e\u3059\u3002", - "title": "Xiaomi Miio\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u3001Xiaomi Gateway\u306b\u63a5\u7d9a" - }, - "device": { - "data": { - "host": "IP\u30a2\u30c9\u30ec\u30b9", - "model": "\u30c7\u30d0\u30a4\u30b9\u30e2\u30c7\u30eb(\u30aa\u30d7\u30b7\u30e7\u30f3)", - "name": "\u30c7\u30d0\u30a4\u30b9\u306e\u540d\u524d", - "token": "API\u30c8\u30fc\u30af\u30f3" - }, - "description": "32\u6587\u5b57\u306eAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u306b\u306a\u308a\u307e\u3059\u3002\u624b\u9806\u306f\u3001https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6ce8\u610f\u4e8b\u9805: \u3053\u306eAPI\u30c8\u30fc\u30af\u30f3\u306f\u3001Xiaomi Aqara\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u30ad\u30fc\u3068\u306f\u7570\u306a\u308a\u307e\u3059\u3002", - "title": "Xiaomi Miio\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u3001Xiaomi Gateway\u306b\u63a5\u7d9a" - }, - "gateway": { - "data": { - "host": "IP\u30a2\u30c9\u30ec\u30b9", - "name": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u540d\u524d", - "token": "API\u30c8\u30fc\u30af\u30f3" - }, - "description": "32\u6587\u5b57\u306eAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u306b\u306a\u308a\u307e\u3059\u3002\u624b\u9806\u306f\u3001https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6ce8\u610f\u4e8b\u9805: \u3053\u306eAPI\u30c8\u30fc\u30af\u30f3\u306f\u3001Xiaomi Aqara\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u30ad\u30fc\u3068\u306f\u7570\u306a\u308a\u307e\u3059\u3002", - "title": "Xiaomi Gateway\u306b\u63a5\u7d9a" + } }, "manual": { "data": { "host": "IP\u30a2\u30c9\u30ec\u30b9", "token": "API\u30c8\u30fc\u30af\u30f3" }, - "description": "32\u6587\u5b57\u306eAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u306b\u306a\u308a\u307e\u3059\u3002\u624b\u9806\u306f\u3001https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6ce8\u610f\u4e8b\u9805: \u3053\u306eAPI\u30c8\u30fc\u30af\u30f3\u306f\u3001Xiaomi Aqara\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u30ad\u30fc\u3068\u306f\u7570\u306a\u308a\u307e\u3059\u3002", - "title": "Xiaomi Miio\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u3001Xiaomi Gateway\u306b\u63a5\u7d9a" + "description": "32\u6587\u5b57\u306eAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u306b\u306a\u308a\u307e\u3059\u3002\u624b\u9806\u306f\u3001https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6ce8\u610f\u4e8b\u9805: \u3053\u306eAPI\u30c8\u30fc\u30af\u30f3\u306f\u3001Xiaomi Aqara\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u30ad\u30fc\u3068\u306f\u7570\u306a\u308a\u307e\u3059\u3002" }, "reauth_confirm": { "description": "Xiaomi Miio\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u66f4\u65b0\u3057\u305f\u308a\u3001\u4e0d\u8db3\u3057\u3066\u3044\u308b\u30af\u30e9\u30a6\u30c9\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u8ffd\u52a0\u3059\u308b\u305f\u3081\u306b\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", @@ -70,15 +46,7 @@ "data": { "select_device": "Miio\u30c7\u30d0\u30a4\u30b9" }, - "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308bXiaomi Miio\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u307e\u3059\u3002", - "title": "Xiaomi Miio\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u3001Xiaomi Gateway\u306b\u63a5\u7d9a" - }, - "user": { - "data": { - "gateway": "Xiaomi Gateway\u306b\u63a5\u7d9a" - }, - "description": "\u63a5\u7d9a\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u307e\u3059\u3002", - "title": "Xiaomi Miio" + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308bXiaomi Miio\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u307e\u3059\u3002" } } }, @@ -90,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "\u30af\u30e9\u30a6\u30c9\u3092\u4f7f\u7528\u3057\u3066\u63a5\u7d9a\u3055\u308c\u305f\u30b5\u30d6\u30c7\u30d0\u30a4\u30b9\u3092\u53d6\u5f97\u3059\u308b" - }, - "description": "\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a\u306e\u6307\u5b9a", - "title": "Xiaomi Miio" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/ko.json b/homeassistant/components/xiaomi_miio/translations/ko.json index 03043f92957..2c188fa670b 100644 --- a/homeassistant/components/xiaomi_miio/translations/ko.json +++ b/homeassistant/components/xiaomi_miio/translations/ko.json @@ -6,37 +6,8 @@ }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "no_device_selected": "\uc120\ud0dd\ub41c \uae30\uae30\uac00 \uc5c6\uc2b5\ub2c8\ub2e4. \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", "unknown_device": "\uae30\uae30\uc758 \ubaa8\ub378\uc744 \uc54c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uad6c\uc131 \ud750\ub984\uc5d0\uc11c \uae30\uae30\ub97c \uc124\uc815\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." }, - "flow_title": "Xiaomi Miio: {name}", - "step": { - "device": { - "data": { - "host": "IP \uc8fc\uc18c", - "model": "\uae30\uae30 \ubaa8\ub378 (\uc120\ud0dd \uc0ac\ud56d)", - "name": "\uae30\uae30 \uc774\ub984", - "token": "API \ud1a0\ud070" - }, - "description": "32\uac1c\uc758 \ubb38\uc790\uc5f4\ub85c \uad6c\uc131\ub41c API \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694. \ucc38\uace0\ub85c \uc774 API \ud1a0\ud070\uc740 Xiaomi Aqara \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0\uc11c \uc0ac\uc6a9\ub418\ub294 \ud0a4\uc640 \ub2e4\ub985\ub2c8\ub2e4.", - "title": "Xiaomi Miio \uae30\uae30 \ub610\ub294 Xiaomi \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\uae30" - }, - "gateway": { - "data": { - "host": "IP \uc8fc\uc18c", - "name": "\uac8c\uc774\ud2b8\uc6e8\uc774 \uc774\ub984", - "token": "API \ud1a0\ud070" - }, - "description": "32\uac1c\uc758 \ubb38\uc790\uc5f4\ub85c \uad6c\uc131\ub41c API \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \uc790\uc138\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694. \ucc38\uace0\ub85c \uc774 API \ud1a0\ud070\uc740 Xiaomi Aqara \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0\uc11c \uc0ac\uc6a9\ub418\ub294 \ud0a4\uc640 \ub2e4\ub985\ub2c8\ub2e4.", - "title": "Xiaomi \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\uae30" - }, - "user": { - "data": { - "gateway": "Xiaomi \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0" - }, - "description": "\uc5f0\uacb0\ud560 \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", - "title": "Xiaomi Miio" - } - } + "flow_title": "Xiaomi Miio: {name}" } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/lb.json b/homeassistant/components/xiaomi_miio/translations/lb.json index 67e286d4fa1..9a747e8e315 100644 --- a/homeassistant/components/xiaomi_miio/translations/lb.json +++ b/homeassistant/components/xiaomi_miio/translations/lb.json @@ -5,27 +5,8 @@ "already_in_progress": "Konfiguratioun's Oflaf ass schonn am gaangen." }, "error": { - "cannot_connect": "Feeler beim verbannen", - "no_device_selected": "Keen Apparat ausgewielt, wiel een Apparat aus w.e.g." + "cannot_connect": "Feeler beim verbannen" }, - "flow_title": "Xiaomi Miio: {name}", - "step": { - "gateway": { - "data": { - "host": "IP Adresse", - "name": "Numm vum Gateway", - "token": "API Jeton" - }, - "description": "Du brauchs den 32 stellegen API Jeton, kuck https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token fir Instruktiounen. Remarque, d\u00ebst ass een aaneren API Jeton w\u00e9i en vun der Xiaomi Aqara Integratioun benotzt g\u00ebtt.", - "title": "Mat enger Xiaomi Gateway verbannen" - }, - "user": { - "data": { - "gateway": "Mat enger Xiaomi Gateway verbannen" - }, - "description": "Wielt den Apparat aus dee soll verbonne ginn", - "title": "Xiaomi Miio" - } - } + "flow_title": "Xiaomi Miio: {name}" } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/nl.json b/homeassistant/components/xiaomi_miio/translations/nl.json index 8ef2ef70fae..219f7da0b4f 100644 --- a/homeassistant/components/xiaomi_miio/translations/nl.json +++ b/homeassistant/components/xiaomi_miio/translations/nl.json @@ -12,7 +12,6 @@ "cloud_credentials_incomplete": "Cloud-inloggegevens onvolledig, vul gebruikersnaam, wachtwoord en land in", "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.", "wrong_token": "Checksum-fout, verkeerd token" }, @@ -25,42 +24,19 @@ "cloud_username": "Cloud gebruikersnaam", "manual": "Handmatig configureren (niet aanbevolen)" }, - "description": "Log in op de Xiaomi Miio-cloud, zie https://www.openhab.org/addons/bindings/miio/#country-servers voor de te gebruiken cloudserver.", - "title": "Maak verbinding met een Xiaomi Miio-apparaat of Xiaomi Gateway" + "description": "Log in op de Xiaomi Miio-cloud, zie https://www.openhab.org/addons/bindings/miio/#country-servers voor de te gebruiken cloudserver." }, "connect": { "data": { "model": "Apparaatmodel" - }, - "description": "Selecteer handmatig het apparaatmodel uit de ondersteunde modellen.", - "title": "Maak verbinding met een Xiaomi Miio-apparaat of Xiaomi Gateway" - }, - "device": { - "data": { - "host": "IP-adres", - "model": "Apparaatmodel (Optioneel)", - "name": "Naam van het apparaat", - "token": "API-token" - }, - "description": "U hebt de 32 karakter API-token nodig, zie https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token voor instructies. Let op, deze API-token is anders dan de sleutel die wordt gebruikt door de Xiaomi Aqara integratie.", - "title": "Verbinding maken met een Xiaomi Miio-apparaat of Xiaomi Gateway" - }, - "gateway": { - "data": { - "host": "IP-adres", - "name": "Naam van de gateway", - "token": "API-token" - }, - "description": "U heeft het API-token nodig, zie https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token voor instructies.", - "title": "Maak verbinding met een Xiaomi Gateway" + } }, "manual": { "data": { "host": "IP-adres", "token": "API-token" }, - "description": "U hebt het 32-teken API-token , zie https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token voor instructies. Houd er rekening mee dat deze API-token verschilt van de sleutel die wordt gebruikt door de Xiaomi Aqara-integratie.", - "title": "Maak verbinding met een Xiaomi Miio-apparaat of Xiaomi Gateway" + "description": "U hebt het 32-teken API-token , zie https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token voor instructies. Houd er rekening mee dat deze API-token verschilt van de sleutel die wordt gebruikt door de Xiaomi Aqara-integratie." }, "reauth_confirm": { "description": "De Xiaomi Miio-integratie moet uw account opnieuw verifi\u00ebren om de tokens bij te werken of ontbrekende cloudreferenties toe te voegen.", @@ -70,15 +46,7 @@ "data": { "select_device": "Miio-apparaat" }, - "description": "Selecteer het Xiaomi Miio apparaat dat u wilt instellen.", - "title": "Maak verbinding met een Xiaomi Miio-apparaat of Xiaomi-gateway" - }, - "user": { - "data": { - "gateway": "Maak verbinding met een Xiaomi Gateway" - }, - "description": "Selecteer het apparaat waarmee u verbinding wilt maken", - "title": "Xiaomi Miio" + "description": "Selecteer het Xiaomi Miio apparaat dat u wilt instellen." } } }, @@ -90,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "Gebruik de cloud om aangesloten subapparaten te krijgen" - }, - "description": "Optionele instellingen opgeven", - "title": "Xiaomi Miio" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/no.json b/homeassistant/components/xiaomi_miio/translations/no.json index 9094cce023a..3d831df207c 100644 --- a/homeassistant/components/xiaomi_miio/translations/no.json +++ b/homeassistant/components/xiaomi_miio/translations/no.json @@ -12,7 +12,6 @@ "cloud_credentials_incomplete": "Utskriftsinformasjon for skyen er fullstendig. Fyll ut brukernavn, passord og land", "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.", "wrong_token": "Kontrollsumfeil, feil token" }, @@ -25,42 +24,19 @@ "cloud_username": "Brukernavn i skyen", "manual": "Konfigurer manuelt (anbefales ikke)" }, - "description": "Logg deg p\u00e5 Xiaomi Miio-skyen, se https://www.openhab.org/addons/bindings/miio/#country-servers for skyserveren du kan bruke.", - "title": "Koble til en Xiaomi Miio-enhet eller Xiaomi Gateway" + "description": "Logg deg p\u00e5 Xiaomi Miio-skyen, se https://www.openhab.org/addons/bindings/miio/#country-servers for skyserveren du kan bruke." }, "connect": { "data": { "model": "Enhetsmodell" - }, - "description": "Velg enhetsmodellen manuelt fra de st\u00f8ttede modellene.", - "title": "Koble til en Xiaomi Miio-enhet eller Xiaomi Gateway" - }, - "device": { - "data": { - "host": "IP adresse", - "model": "Enhetsmodell (valgfritt)", - "name": "Navnet p\u00e5 enheten", - "token": "API-token" - }, - "description": "Du trenger 32 tegn API-token , se https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instruksjoner. V\u00e6r oppmerksom p\u00e5 at denne API-token er forskjellig fra n\u00f8kkelen som brukes av Xiaomi Aqara-integrasjonen.", - "title": "Koble til en Xiaomi Miio-enhet eller Xiaomi Gateway" - }, - "gateway": { - "data": { - "host": "IP adresse", - "name": "Navnet p\u00e5 gatewayen", - "token": "API-token" - }, - "description": "Du trenger 32 tegn API-token , se https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token for instruksjoner. V\u00e6r oppmerksom p\u00e5 at denne API-token er forskjellig fra n\u00f8kkelen som brukes av Xiaomi Aqara-integrasjonen.", - "title": "Koble til en Xiaomi Gateway" + } }, "manual": { "data": { "host": "IP adresse", "token": "API-token" }, - "description": "Du trenger 32 tegn API-token , se https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instruksjoner. V\u00e6r oppmerksom p\u00e5 at denne API-token er forskjellig fra n\u00f8kkelen som brukes av Xiaomi Aqara-integrasjonen.", - "title": "Koble til en Xiaomi Miio-enhet eller Xiaomi Gateway" + "description": "Du trenger 32 tegn API-token , se https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token for instruksjoner. V\u00e6r oppmerksom p\u00e5 at denne API-token er forskjellig fra n\u00f8kkelen som brukes av Xiaomi Aqara-integrasjonen." }, "reauth_confirm": { "description": "Xiaomi Miio-integrasjonen m\u00e5 autentisere kontoen din p\u00e5 nytt for \u00e5 oppdatere tokens eller legge til manglende skylegitimasjon.", @@ -70,15 +46,7 @@ "data": { "select_device": "Miio-enhet" }, - "description": "Velg Xiaomi Miio-enheten du vil installere.", - "title": "Koble til en Xiaomi Miio-enhet eller Xiaomi Gateway" - }, - "user": { - "data": { - "gateway": "Koble til en Xiaomi Gateway" - }, - "description": "Velg hvilken enhet du vil koble til.", - "title": "" + "description": "Velg Xiaomi Miio-enheten du vil installere." } } }, @@ -90,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "Bruk skyen for \u00e5 f\u00e5 tilkoblede underenheter" - }, - "description": "Spesifiser valgfrie innstillinger", - "title": "Xiaomi Miio" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json index 60c82020a77..d0f3cc9a4a8 100644 --- a/homeassistant/components/xiaomi_miio/translations/pl.json +++ b/homeassistant/components/xiaomi_miio/translations/pl.json @@ -12,7 +12,6 @@ "cloud_credentials_incomplete": "Dane logowania do chmury niekompletne, prosz\u0119 poda\u0107 nazw\u0119 u\u017cytkownika, has\u0142o i kraj", "cloud_login_error": "Nie mo\u017cna zalogowa\u0107 si\u0119 do chmury Xiaomi Miio, sprawd\u017a po\u015bwiadczenia.", "cloud_no_devices": "Na tym koncie Xiaomi Miio nie znaleziono \u017cadnych urz\u0105dze\u0144.", - "no_device_selected": "Nie wybrano \u017cadnego urz\u0105dzenia, wybierz jedno urz\u0105dzenie", "unknown_device": "Model urz\u0105dzenia nie jest znany, nie mo\u017cna skonfigurowa\u0107 urz\u0105dzenia przy u\u017cyciu interfejsu u\u017cytkownika.", "wrong_token": "B\u0142\u0105d sumy kontrolnej, niew\u0142a\u015bciwy token" }, @@ -25,42 +24,19 @@ "cloud_username": "Nazwa u\u017cytkownika do chmury", "manual": "Skonfiguruj r\u0119cznie (niezalecane)" }, - "description": "Zaloguj si\u0119 do chmury Xiaomi Miio, zobacz https://www.openhab.org/addons/bindings/miio/#country-servers, aby zobaczy\u0107, kt\u00f3rego serwera u\u017cy\u0107.", - "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi lub urz\u0105dzeniem Xiaomi Miio" + "description": "Zaloguj si\u0119 do chmury Xiaomi Miio, zobacz https://www.openhab.org/addons/bindings/miio/#country-servers, aby zobaczy\u0107, kt\u00f3rego serwera u\u017cy\u0107." }, "connect": { "data": { "model": "Model urz\u0105dzenia" - }, - "description": "Wybierz r\u0119cznie model urz\u0105dzenia z listy obs\u0142ugiwanych modeli.", - "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi lub urz\u0105dzeniem Xiaomi Miio" - }, - "device": { - "data": { - "host": "Adres IP", - "model": "Model urz\u0105dzenia (opcjonalnie)", - "name": "Nazwa urz\u0105dzenia", - "token": "Token API" - }, - "description": "B\u0119dziesz potrzebowa\u0107 tokenu API (32 znaki), odwied\u017a https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token, aby uzyska\u0107 instrukcje. Zauwa\u017c i\u017c jest to inny token ni\u017c w integracji Xiaomi Aqara.", - "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi lub urz\u0105dzeniem Xiaomi Miio" - }, - "gateway": { - "data": { - "host": "Adres IP", - "name": "Nazwa bramki", - "token": "Token API" - }, - "description": "B\u0119dziesz potrzebowa\u0107 tokenu API (32 znaki), odwied\u017a https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token, aby uzyska\u0107 instrukcje. Zauwa\u017c i\u017c jest to inny token ni\u017c w integracji Xiaomi Aqara .", - "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi" + } }, "manual": { "data": { "host": "Adres IP", "token": "Token API" }, - "description": "B\u0119dziesz potrzebowa\u0107 tokenu API (32-znaki), zobacz https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token, aby uzyska\u0107 instrukcje. Pami\u0119taj, \u017ce ten token API r\u00f3\u017cni si\u0119 od klucza u\u017cywanego przez integracj\u0119 Xiaomi Aqara.", - "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi lub urz\u0105dzeniem Xiaomi Miio" + "description": "B\u0119dziesz potrzebowa\u0107 tokenu API (32-znaki), zobacz https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token, aby uzyska\u0107 instrukcje. Pami\u0119taj, \u017ce ten token API r\u00f3\u017cni si\u0119 od klucza u\u017cywanego przez integracj\u0119 Xiaomi Aqara." }, "reauth_confirm": { "description": "Integracja Xiaomi Miio wymaga ponownego uwierzytelnienia Twoje konta, aby zaktualizowa\u0107 tokeny lub doda\u0107 brakuj\u0105ce dane uwierzytelniaj\u0105ce do chmury.", @@ -70,15 +46,7 @@ "data": { "select_device": "Urz\u0105dzenie Miio" }, - "description": "Wybierz urz\u0105dzenie Xiaomi Miio do skonfigurowania.", - "title": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi lub urz\u0105dzeniem Xiaomi Miio" - }, - "user": { - "data": { - "gateway": "Po\u0142\u0105czenie z bramk\u0105 Xiaomi" - }, - "description": "Wybierz urz\u0105dzenie, z kt\u00f3rym chcesz si\u0119 po\u0142\u0105czy\u0107.", - "title": "Xiaomi Miio" + "description": "Wybierz urz\u0105dzenie Xiaomi Miio do skonfigurowania." } } }, @@ -90,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "U\u017cyj chmury, aby uzyska\u0107 pod\u0142\u0105czone podurz\u0105dzenia" - }, - "description": "Ustawienia opcjonalne", - "title": "Xiaomi Miio" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/pt-BR.json b/homeassistant/components/xiaomi_miio/translations/pt-BR.json index 1116de258fa..ce67173f9b2 100644 --- a/homeassistant/components/xiaomi_miio/translations/pt-BR.json +++ b/homeassistant/components/xiaomi_miio/translations/pt-BR.json @@ -12,7 +12,6 @@ "cloud_credentials_incomplete": "Credenciais da nuvem incompletas, preencha o nome de usu\u00e1rio, a senha e o pa\u00eds", "cloud_login_error": "N\u00e3o foi poss\u00edvel fazer login no Xiaomi Miio Cloud, verifique as credenciais.", "cloud_no_devices": "Nenhum dispositivo encontrado nesta conta de nuvem Xiaomi Miio.", - "no_device_selected": "Nenhum dispositivo selecionado, selecione um dispositivo.", "unknown_device": "O modelo do dispositivo n\u00e3o \u00e9 conhecido, n\u00e3o \u00e9 poss\u00edvel configurar o dispositivo usando o fluxo de configura\u00e7\u00e3o.", "wrong_token": "Erro de checksum, token errado" }, @@ -25,42 +24,19 @@ "cloud_username": "Usu\u00e1rio da Cloud", "manual": "Configurar manualmente (n\u00e3o recomendado)" }, - "description": "Coloque o login da Cloud Xiaomi Miio e consulte https://www.openhab.org/addons/bindings/miio/#country-servers para saber qual servidor cloud usar.", - "title": "Conecte-se a um dispositivo Xiaomi Miio ou Xiaomi Gateway" + "description": "Coloque o login da Cloud Xiaomi Miio e consulte https://www.openhab.org/addons/bindings/miio/#country-servers para saber qual servidor cloud usar." }, "connect": { "data": { "model": "Modelo do dispositivo" - }, - "description": "Selecione manualmente o modelo do dispositivo entre os modelos suportados.", - "title": "Conecte-se a um dispositivo Xiaomi Miio ou Xiaomi Gateway" - }, - "device": { - "data": { - "host": "Endere\u00e7o IP", - "model": "Modelo do dispositivo (opcional)", - "name": "Nome do dispositivo", - "token": "Token da API" - }, - "description": "Voc\u00ea precisar\u00e1 do Token da API com 32 caracteres, consulte https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token para obter instru\u00e7\u00f5es. Observe que o Token da API \u00e9 diferente da chave usada pela integra\u00e7\u00e3o Xiaomi Aqara.", - "title": "Conecte-se a um dispositivo Xiaomi Miio ou Xiaomi Gateway" - }, - "gateway": { - "data": { - "host": "Endere\u00e7o IP", - "name": "Nome do Gateway", - "token": "Token da API" - }, - "description": "Voc\u00ea precisar\u00e1 do Token da API com 32 caracteres, consulte https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token para obter instru\u00e7\u00f5es. Observe que o Token da API \u00e9 diferente da chave usada pela integra\u00e7\u00e3o Xiaomi Aqara.", - "title": "Conecte-se a um Xiaomi Gateway" + } }, "manual": { "data": { "host": "Endere\u00e7o IP", "token": "Token da API" }, - "description": "Voc\u00ea precisar\u00e1 do Token da API com 32 caracteres, consulte https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token para obter instru\u00e7\u00f5es. Observe que o Token da API \u00e9 diferente da chave usada pela integra\u00e7\u00e3o Xiaomi Aqara.", - "title": "Conecte-se a um dispositivo Xiaomi Miio ou Xiaomi Gateway" + "description": "Voc\u00ea precisar\u00e1 do Token da API com 32 caracteres, consulte https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token para obter instru\u00e7\u00f5es. Observe que o Token da API \u00e9 diferente da chave usada pela integra\u00e7\u00e3o Xiaomi Aqara." }, "reauth_confirm": { "description": "A integra\u00e7\u00e3o do Xiaomi Miio precisa autenticar novamente sua conta para atualizar os tokens ou adicionar credenciais de nuvem ausentes.", @@ -70,15 +46,7 @@ "data": { "select_device": "Dispositivo Miio" }, - "description": "Selecione o dispositivo Xiaomi Miio para configurar.", - "title": "Conecte-se a um dispositivo Xiaomi Miio ou Xiaomi Gateway" - }, - "user": { - "data": { - "gateway": "Conecte-se a um Xiaomi Gateway" - }, - "description": "Selecione a qual dispositivo voc\u00ea deseja se conectar.", - "title": "Xiaomi Miio" + "description": "Selecione o dispositivo Xiaomi Miio para configurar." } } }, @@ -90,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "Use a cloud para obter subdispositivos conectados" - }, - "description": "Especificar configura\u00e7\u00f5es opcionais", - "title": "Xiaomi Miio" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/pt.json b/homeassistant/components/xiaomi_miio/translations/pt.json index 922c2441c3d..db0e0c2a137 100644 --- a/homeassistant/components/xiaomi_miio/translations/pt.json +++ b/homeassistant/components/xiaomi_miio/translations/pt.json @@ -5,20 +5,6 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" - }, - "step": { - "device": { - "data": { - "host": "Endere\u00e7o IP", - "token": "API Token" - } - }, - "gateway": { - "data": { - "host": "Endere\u00e7o IP", - "token": "API Token" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/ru.json b/homeassistant/components/xiaomi_miio/translations/ru.json index 18bbc4271ad..1432666cb44 100644 --- a/homeassistant/components/xiaomi_miio/translations/ru.json +++ b/homeassistant/components/xiaomi_miio/translations/ru.json @@ -12,7 +12,6 @@ "cloud_credentials_incomplete": "\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432 \u043e\u0431\u043b\u0430\u043a\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u044b\u0435. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f, \u043f\u0430\u0440\u043e\u043b\u044c \u0438 \u0441\u0442\u0440\u0430\u043d\u0443.", "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.", "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." }, @@ -25,42 +24,19 @@ "cloud_username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0434\u043b\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430", "manual": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432\u0440\u0443\u0447\u043d\u0443\u044e (\u043d\u0435 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f)" }, - "description": "\u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 \u043e\u0431\u043b\u0430\u043a\u043e Xiaomi Miio, \u0441\u043c. https://www.openhab.org/addons/bindings/miio/#country-servers \u0434\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430.", - "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Xiaomi Miio \u0438\u043b\u0438 \u0448\u043b\u044e\u0437\u0443 Xiaomi" + "description": "\u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 \u043e\u0431\u043b\u0430\u043a\u043e Xiaomi Miio, \u0441\u043c. https://www.openhab.org/addons/bindings/miio/#country-servers \u0434\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430." }, "connect": { "data": { "model": "\u041c\u043e\u0434\u0435\u043b\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u043e\u0434\u0435\u043b\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438\u0437 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0445.", - "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Xiaomi Miio \u0438\u043b\u0438 \u0448\u043b\u044e\u0437\u0443 Xiaomi" - }, - "device": { - "data": { - "host": "IP-\u0430\u0434\u0440\u0435\u0441", - "model": "\u041c\u043e\u0434\u0435\u043b\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", - "token": "\u0422\u043e\u043a\u0435\u043d API" - }, - "description": "\u0414\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f 32-\u0445 \u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u0422\u043e\u043a\u0435\u043d API. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0437\u0434\u0435\u0441\u044c: \nhttps://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token.\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u044d\u0442\u043e\u0442 \u0442\u043e\u043a\u0435\u043d \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043a\u043b\u044e\u0447\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u043f\u0440\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Xiaomi Aqara.", - "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Xiaomi Miio \u0438\u043b\u0438 \u0448\u043b\u044e\u0437\u0443 Xiaomi" - }, - "gateway": { - "data": { - "host": "IP-\u0430\u0434\u0440\u0435\u0441", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "token": "\u0422\u043e\u043a\u0435\u043d API" - }, - "description": "\u0414\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f 32-\u0445 \u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u0422\u043e\u043a\u0435\u043d API. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0437\u0434\u0435\u0441\u044c: \nhttps://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u044d\u0442\u043e\u0442 \u0442\u043e\u043a\u0435\u043d \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043a\u043b\u044e\u0447\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u043f\u0440\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Xiaomi Aqara.", - "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u043b\u044e\u0437\u0443 Xiaomi" + } }, "manual": { "data": { "host": "IP-\u0430\u0434\u0440\u0435\u0441", "token": "\u0422\u043e\u043a\u0435\u043d API" }, - "description": "\u0414\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f 32-\u0445 \u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u0422\u043e\u043a\u0435\u043d API. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0437\u0434\u0435\u0441\u044c: \nhttps://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u044d\u0442\u043e\u0442 \u0442\u043e\u043a\u0435\u043d \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043a\u043b\u044e\u0447\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u043f\u0440\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Xiaomi Aqara.", - "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Xiaomi Miio \u0438\u043b\u0438 \u0448\u043b\u044e\u0437\u0443 Xiaomi" + "description": "\u0414\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f 32-\u0445 \u0437\u043d\u0430\u0447\u043d\u044b\u0439 \u0422\u043e\u043a\u0435\u043d API. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0437\u0434\u0435\u0441\u044c: \nhttps://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u044d\u0442\u043e\u0442 \u0442\u043e\u043a\u0435\u043d \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043a\u043b\u044e\u0447\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u043f\u0440\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Xiaomi Aqara." }, "reauth_confirm": { "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Xiaomi Miio, \u0447\u0442\u043e\u0431\u044b \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d\u044b \u0438\u043b\u0438 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u043e\u0431\u043b\u0430\u043a\u0430", @@ -70,15 +46,7 @@ "data": { "select_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Miio" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Xiaomi Miio \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", - "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Xiaomi Miio \u0438\u043b\u0438 \u0448\u043b\u044e\u0437\u0443 Xiaomi" - }, - "user": { - "data": { - "gateway": "\u0428\u043b\u044e\u0437 Xiaomi" - }, - "description": "\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 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c.", - "title": "Xiaomi Miio" + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Xiaomi Miio \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." } } }, @@ -90,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u043b\u0430\u043a\u043e \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432" - }, - "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": "Xiaomi Miio" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/sk.json b/homeassistant/components/xiaomi_miio/translations/sk.json index 022e4103fb7..fca4223f4ac 100644 --- a/homeassistant/components/xiaomi_miio/translations/sk.json +++ b/homeassistant/components/xiaomi_miio/translations/sk.json @@ -6,16 +6,6 @@ "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "step": { - "device": { - "data": { - "token": "API token" - } - }, - "gateway": { - "data": { - "token": "API token" - } - }, "manual": { "data": { "token": "API token" diff --git a/homeassistant/components/xiaomi_miio/translations/sl.json b/homeassistant/components/xiaomi_miio/translations/sl.json index 472317182bf..8596992fe39 100644 --- a/homeassistant/components/xiaomi_miio/translations/sl.json +++ b/homeassistant/components/xiaomi_miio/translations/sl.json @@ -4,26 +4,7 @@ "already_configured": "Naprava je \u017ee konfigurirana" }, "error": { - "no_device_selected": "Izbrana ni nobena naprava, izberite eno napravo.", "wrong_token": "Napaka kontrolne vsote, napa\u010den \u017eeton" - }, - "step": { - "gateway": { - "data": { - "host": "IP naslov", - "name": "Ime prehoda", - "token": "API \u017eeton" - }, - "description": "Potrebujete API \u017deton, glej https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token za navodila.", - "title": "Pove\u017eite se s prehodom Xiaomi" - }, - "user": { - "data": { - "gateway": "Pove\u017eite se s prehodom Xiaomi" - }, - "description": "Izberite, s katero napravo se \u017eelite povezati.", - "title": "Xiaomi Miio" - } } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/sv.json b/homeassistant/components/xiaomi_miio/translations/sv.json deleted file mode 100644 index fcd6e641091..00000000000 --- a/homeassistant/components/xiaomi_miio/translations/sv.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "error": { - "no_device_selected": "Ingen enhet har valts, v\u00e4lj en enhet." - }, - "step": { - "gateway": { - "data": { - "host": "IP-adress", - "name": "Namnet p\u00e5 Gatewayen", - "token": "API Token" - }, - "description": "Du beh\u00f6ver en API token, se https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token f\u00f6r mer instruktioner.", - "title": "Anslut till en Xiaomi Gateway" - }, - "user": { - "data": { - "gateway": "Anslut till en Xiaomi Gateway" - }, - "description": "V\u00e4lj den enhet som du vill ansluta till." - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/tr.json b/homeassistant/components/xiaomi_miio/translations/tr.json index 097ccdab6ac..8c81f08ee9f 100644 --- a/homeassistant/components/xiaomi_miio/translations/tr.json +++ b/homeassistant/components/xiaomi_miio/translations/tr.json @@ -12,7 +12,6 @@ "cloud_credentials_incomplete": "Bulut kimlik bilgileri eksik, l\u00fctfen kullan\u0131c\u0131 ad\u0131n\u0131, \u015fifreyi ve \u00fclkeyi girin", "cloud_login_error": "Xiaomi Miio Cloud'da oturum a\u00e7\u0131lamad\u0131, kimlik bilgilerini kontrol edin.", "cloud_no_devices": "Bu Xiaomi Miio bulut hesab\u0131nda cihaz bulunamad\u0131.", - "no_device_selected": "Cihaz se\u00e7ilmedi, l\u00fctfen bir cihaz se\u00e7in.", "unknown_device": "Cihaz modeli bilinmiyor, cihaz yap\u0131land\u0131rma ak\u0131\u015f\u0131n\u0131 kullanarak kurulam\u0131yor.", "wrong_token": "Sa\u011flama toplam\u0131 hatas\u0131, yanl\u0131\u015f anahtar" }, @@ -25,42 +24,19 @@ "cloud_username": "Bulut kullan\u0131c\u0131 ad\u0131", "manual": "Manuel olarak yap\u0131land\u0131r\u0131n (\u00f6nerilmez)" }, - "description": "Xiaomi Miio bulutunda oturum a\u00e7\u0131n, bulut sunucusunun kullanmas\u0131 i\u00e7in https://www.openhab.org/addons/bindings/miio/#country-servers adresine bak\u0131n.", - "title": "Bir Xiaomi Miio Cihaz\u0131na veya Xiaomi A\u011f Ge\u00e7idine Ba\u011flan" + "description": "Xiaomi Miio bulutunda oturum a\u00e7\u0131n, bulut sunucusunun kullanmas\u0131 i\u00e7in https://www.openhab.org/addons/bindings/miio/#country-servers adresine bak\u0131n." }, "connect": { "data": { "model": "Cihaz modeli" - }, - "description": "Desteklenen modellerden cihaz modelini manuel olarak se\u00e7in.", - "title": "Bir Xiaomi Miio Cihaz\u0131na veya Xiaomi A\u011f Ge\u00e7idine Ba\u011flan" - }, - "device": { - "data": { - "host": "IP Adresi", - "model": "Cihaz modeli (Opsiyonel)", - "name": "Cihaz\u0131n ad\u0131", - "token": "API Anahtar\u0131" - }, - "description": "32 karaktere API Anahtar\u0131 , talimatlar i\u00e7in https://www.home-assistant.io/integrations/xiaomi_miio#retriiving-the-access-token adresine bak\u0131n. L\u00fctfen bu API Anahtar\u0131 \u00f6\u011fesinin Xiaomi Aqara entegrasyonu taraf\u0131ndan kullan\u0131lan anahtardan farkl\u0131 oldu\u011funu unutmay\u0131n.", - "title": "Bir Xiaomi Miio Cihaz\u0131na veya Xiaomi A\u011f Ge\u00e7idine Ba\u011flan" - }, - "gateway": { - "data": { - "host": "\u0130p Adresi", - "name": "A\u011f Ge\u00e7idinin Ad\u0131", - "token": "API Belirteci" - }, - "description": "32 karaktere API Anahtar\u0131 , bkz. talimatlar i\u00e7in. L\u00fctfen bu API Anahtar\u0131 \u00f6\u011fesinin Xiaomi Aqara entegrasyonu taraf\u0131ndan kullan\u0131lan anahtardan farkl\u0131 oldu\u011funu unutmay\u0131n.", - "title": "Bir Xiaomi A\u011f Ge\u00e7idine ba\u011flan\u0131n" + } }, "manual": { "data": { "host": "IP Adresi", "token": "API Anahtar\u0131" }, - "description": "32 karaktere API Anahtar\u0131 , talimatlar i\u00e7in https://www.home-assistant.io/integrations/xiaomi_miio#retriiving-the-access-token adresine bak\u0131n. L\u00fctfen bu API Anahtar\u0131 \u00f6\u011fesinin Xiaomi Aqara entegrasyonu taraf\u0131ndan kullan\u0131lan anahtardan farkl\u0131 oldu\u011funu unutmay\u0131n.", - "title": "Bir Xiaomi Miio Cihaz\u0131na veya Xiaomi A\u011f Ge\u00e7idine Ba\u011flan" + "description": "32 karaktere API Anahtar\u0131 , talimatlar i\u00e7in https://www.home-assistant.io/integrations/xiaomi_miio#retriiving-the-access-token adresine bak\u0131n. L\u00fctfen bu API Anahtar\u0131 \u00f6\u011fesinin Xiaomi Aqara entegrasyonu taraf\u0131ndan kullan\u0131lan anahtardan farkl\u0131 oldu\u011funu unutmay\u0131n." }, "reauth_confirm": { "description": "Anahtarlar\u0131 g\u00fcncellemek veya eksik bulut kimlik bilgilerini eklemek i\u00e7in Xiaomi Miio entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekir.", @@ -70,15 +46,7 @@ "data": { "select_device": "Miio cihaz\u0131" }, - "description": "Kurulumu i\u00e7in Xiaomi Miio cihaz\u0131n\u0131 se\u00e7in.", - "title": "Bir Xiaomi Miio Cihaz\u0131na veya Xiaomi A\u011f Ge\u00e7idine Ba\u011flan" - }, - "user": { - "data": { - "gateway": "Bir Xiaomi A\u011f Ge\u00e7idine ba\u011flan\u0131n" - }, - "description": "Hangi cihaza ba\u011flanmak istedi\u011finizi se\u00e7in.", - "title": "Xiaomi Miio" + "description": "Kurulumu i\u00e7in Xiaomi Miio cihaz\u0131n\u0131 se\u00e7in." } } }, @@ -90,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "Ba\u011fl\u0131 alt cihazlar almak i\u00e7in bulutu kullan\u0131n" - }, - "description": "\u0130ste\u011fe ba\u011fl\u0131 ayarlar\u0131 belirtin", - "title": "Xiaomi Miio" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/uk.json b/homeassistant/components/xiaomi_miio/translations/uk.json index f32105589f6..bf1b8126e38 100644 --- a/homeassistant/components/xiaomi_miio/translations/uk.json +++ b/homeassistant/components/xiaomi_miio/translations/uk.json @@ -5,27 +5,8 @@ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454." }, "error": { - "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", - "no_device_selected": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u0434\u0438\u043d \u0437 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432." + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" }, - "flow_title": "Xiaomi Miio: {name}", - "step": { - "gateway": { - "data": { - "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", - "name": "\u041d\u0430\u0437\u0432\u0430", - "token": "\u0422\u043e\u043a\u0435\u043d API" - }, - "description": "\u0414\u043b\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e 32-\u0445 \u0437\u043d\u0430\u0447\u043d\u0438\u0439 \u0422\u043e\u043a\u0435\u043d API . \u041f\u0440\u043e \u0442\u0435, \u044f\u043a \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0442\u043e\u043a\u0435\u043d, \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0456\u0437\u043d\u0430\u0442\u0438\u0441\u044f \u0442\u0443\u0442:\nhttps://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.\n\u0417\u0432\u0435\u0440\u043d\u0456\u0442\u044c \u0443\u0432\u0430\u0433\u0443, \u0449\u043e \u0446\u0435\u0439 \u0442\u043e\u043a\u0435\u043d \u0432\u0456\u0434\u0440\u0456\u0437\u043d\u044f\u0454\u0442\u044c\u0441\u044f \u0432\u0456\u0434 \u043a\u043b\u044e\u0447\u0430, \u044f\u043a\u0438\u0439 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 Xiaomi Aqara.", - "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0448\u043b\u044e\u0437\u0443 Xiaomi" - }, - "user": { - "data": { - "gateway": "\u0428\u043b\u044e\u0437 Xiaomi" - }, - "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u044f\u043a\u0438\u0439 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438.", - "title": "Xiaomi Miio" - } - } + "flow_title": "Xiaomi Miio: {name}" } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hans.json b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json index f488618b945..0dda09965fd 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hans.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json @@ -12,7 +12,6 @@ "cloud_credentials_incomplete": "\u4e91\u7aef\u51ed\u636e\u4e0d\u5b8c\u6574\uff0c\u8bf7\u8f93\u5165\u7528\u6237\u540d\u3001\u5bc6\u7801\u548c\u56fd\u5bb6/\u5730\u533a", "cloud_login_error": "\u65e0\u6cd5\u767b\u5f55\u5c0f\u7c73\u4e91\u670d\u52a1\uff0c\u8bf7\u68c0\u67e5\u51ed\u636e\u3002", "cloud_no_devices": "\u672a\u5728\u5c0f\u7c73\u5e10\u6237\u4e2d\u53d1\u73b0\u8bbe\u5907\u3002", - "no_device_selected": "\u672a\u9009\u62e9\u8bbe\u5907\uff0c\u8bf7\u9009\u62e9\u4e00\u4e2a\u8bbe\u5907\u3002", "unknown_device": "\u8be5\u8bbe\u5907\u578b\u53f7\u6682\u672a\u9002\u914d\uff0c\u56e0\u6b64\u65e0\u6cd5\u901a\u8fc7\u914d\u7f6e\u5411\u5bfc\u6dfb\u52a0\u8bbe\u5907\u3002", "wrong_token": "\u6821\u9a8c\u548c\u9519\u8bef\uff0ctoken \u9519\u8bef" }, @@ -25,34 +24,12 @@ "cloud_username": "\u7528\u6237\u540d", "manual": "\u624b\u52a8\u914d\u7f6e\uff08\u4e0d\u63a8\u8350\uff09" }, - "description": "\u767b\u5f55\u5c0f\u7c73\u4e91\u670d\u52a1\u3002\u6709\u5173\u56fd\u5bb6/\u5730\u533a\u4fe1\u606f\uff0c\u8bf7\u53c2\u9605 https://www.openhab.org/addons/bindings/miio/#country-servers \u3002", - "title": "\u8fde\u63a5\u5230\u5c0f\u7c73 Miio \u8bbe\u5907\u6216\u5c0f\u7c73\u7f51\u5173" + "description": "\u767b\u5f55\u5c0f\u7c73\u4e91\u670d\u52a1\u3002\u6709\u5173\u56fd\u5bb6/\u5730\u533a\u4fe1\u606f\uff0c\u8bf7\u53c2\u9605 https://www.openhab.org/addons/bindings/miio/#country-servers \u3002" }, "connect": { "data": { "model": "\u8bbe\u5907 model" - }, - "description": "\u4ece\u652f\u6301\u7684\u578b\u53f7\u4e2d\u624b\u52a8\u9009\u62e9\u3002", - "title": "\u8fde\u63a5\u5230\u5c0f\u7c73 Miio \u8bbe\u5907\u6216\u5c0f\u7c73\u7f51\u5173" - }, - "device": { - "data": { - "host": "IP \u5730\u5740", - "model": "\u8bbe\u5907 model\uff08\u53ef\u9009\uff09", - "name": "\u8bbe\u5907\u540d\u79f0", - "token": "API Token" - }, - "description": "\u60a8\u9700\u8981\u83b7\u53d6\u4e00\u4e2a 32 \u4f4d\u7684 API Token\u3002\u5982\u9700\u5e2e\u52a9\uff0c\u8bf7\u53c2\u9605\u4ee5\u4e0b\u94fe\u63a5: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u3002\u8bf7\u6ce8\u610f\u6b64 token \u4e0d\u540c\u4e8e\u201cXiaomi Aqara\u201d\u96c6\u6210\u6240\u9700\u7684 key\u3002", - "title": "\u8fde\u63a5\u5230\u5c0f\u7c73 Miio \u8bbe\u5907\u6216\u5c0f\u7c73\u7f51\u5173" - }, - "gateway": { - "data": { - "host": "IP \u5730\u5740", - "name": "\u7f51\u5173\u540d\u79f0", - "token": "API Token" - }, - "description": "\u60a8\u9700\u8981\u83b7\u53d6\u4e00\u4e2a 32 \u4f4d\u7684 API Token\u3002\u5982\u9700\u5e2e\u52a9\uff0c\u8bf7\u53c2\u9605\u4ee5\u4e0b\u94fe\u63a5: https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u3002\u8bf7\u6ce8\u610f\u6b64 token \u4e0d\u540c\u4e8e\u201cXiaomi Aqara\u201d\u96c6\u6210\u6240\u9700\u7684 key\u3002", - "title": "\u8fde\u63a5\u5230\u5c0f\u7c73\u7f51\u5173" + } }, "manual": { "data": { @@ -69,15 +46,7 @@ "data": { "select_device": "Miio \u8bbe\u5907" }, - "description": "\u9009\u62e9\u8981\u6dfb\u52a0\u7684\u5c0f\u7c73 Miio \u8bbe\u5907\u3002", - "title": "\u8fde\u63a5\u5230\u5c0f\u7c73 Miio \u8bbe\u5907\u6216\u5c0f\u7c73\u7f51\u5173" - }, - "user": { - "data": { - "gateway": "\u8fde\u63a5\u5230\u5c0f\u7c73\u7f51\u5173" - }, - "description": "\u8bf7\u9009\u62e9\u8981\u8fde\u63a5\u7684\u8bbe\u5907\u3002", - "title": "Xiaomi Miio" + "description": "\u9009\u62e9\u8981\u6dfb\u52a0\u7684\u5c0f\u7c73 Miio \u8bbe\u5907\u3002" } } }, @@ -89,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "\u901a\u8fc7\u4e91\u7aef\u83b7\u53d6\u8fde\u63a5\u7684\u5b50\u8bbe\u5907" - }, - "description": "\u6307\u5b9a\u53ef\u9009\u8bbe\u7f6e", - "title": "Xiaomi Miio" + } } } } diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json index 2812f91be7e..e38567ea37c 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json @@ -12,7 +12,6 @@ "cloud_credentials_incomplete": "\u96f2\u7aef\u6191\u8b49\u672a\u5b8c\u6210\uff0c\u8acb\u586b\u5beb\u4f7f\u7528\u8005\u540d\u7a31\u3001\u5bc6\u78bc\u8207\u570b\u5bb6", "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", "wrong_token": "\u6838\u5c0d\u548c\u932f\u8aa4\u3001\u6b0a\u6756\u932f\u8aa4" }, @@ -25,42 +24,19 @@ "cloud_username": "\u96f2\u7aef\u670d\u52d9\u4f7f\u7528\u8005\u540d\u7a31", "manual": "\u624b\u52d5\u8a2d\u5b9a (\u4e0d\u5efa\u8b70)" }, - "description": "\u767b\u5165\u81f3\u5c0f\u7c73 Miio \u96f2\u670d\u52d9\uff0c\u8acb\u53c3\u95b1 https://www.openhab.org/addons/bindings/miio/#country-servers \u4ee5\u4e86\u89e3\u9078\u64c7\u54ea\u4e00\u7d44\u96f2\u7aef\u4f3a\u670d\u5668\u3002", - "title": "\u9023\u7dda\u81f3\u5c0f\u7c73 MIIO \u88dd\u7f6e\u6216\u5c0f\u7c73\u7db2\u95dc" + "description": "\u767b\u5165\u81f3\u5c0f\u7c73 Miio \u96f2\u670d\u52d9\uff0c\u8acb\u53c3\u95b1 https://www.openhab.org/addons/bindings/miio/#country-servers \u4ee5\u4e86\u89e3\u9078\u64c7\u54ea\u4e00\u7d44\u96f2\u7aef\u4f3a\u670d\u5668\u3002" }, "connect": { "data": { "model": "\u88dd\u7f6e\u578b\u865f" - }, - "description": "\u5f9e\u652f\u63f4\u7684\u578b\u865f\u4e2d\u624b\u52d5\u9078\u64c7\u88dd\u7f6e\u578b\u865f\u3002", - "title": "\u9023\u7dda\u81f3\u5c0f\u7c73 MIIO \u88dd\u7f6e\u6216\u5c0f\u7c73\u7db2\u95dc" - }, - "device": { - "data": { - "host": "IP \u4f4d\u5740", - "model": "\u88dd\u7f6e\u578b\u865f\uff08\u9078\u9805\uff09", - "name": "\u88dd\u7f6e\u540d\u7a31", - "token": "API \u6b0a\u6756" - }, - "description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u6b0a\u6756\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u6b0a\u6756\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64 API \u6b0a\u6756\u8207 Xiaomi Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u6b0a\u6756\u4e0d\u540c\u3002", - "title": "\u9023\u7dda\u81f3\u5c0f\u7c73 MIIO \u88dd\u7f6e\u6216\u5c0f\u7c73\u7db2\u95dc" - }, - "gateway": { - "data": { - "host": "IP \u4f4d\u5740", - "name": "\u7db2\u95dc\u540d\u7a31", - "token": "API \u6b0a\u6756" - }, - "description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u6b0a\u6756\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u6b0a\u6756\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64API \u6b0a\u6756\u8207 Xiaomi Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u6b0a\u6756\u4e0d\u540c\u3002", - "title": "\u9023\u7dda\u81f3\u5c0f\u7c73\u7db2\u95dc" + } }, "manual": { "data": { "host": "IP \u4f4d\u5740", "token": "API \u6b0a\u6756" }, - "description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u6b0a\u6756\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u91d1\u9470\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64 API \u6b0a\u6756\u8207\u5c0f\u7c73 Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u91d1\u9470\u4e0d\u540c\u3002", - "title": "\u9023\u7dda\u81f3\u5c0f\u7c73 MIIO \u88dd\u7f6e\u6216\u5c0f\u7c73\u7db2\u95dc" + "description": "\u5c07\u9700\u8981\u8f38\u5165 32 \u4f4d\u5b57\u5143 API \u6b0a\u6756\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u4ee5\u7372\u5f97\u7372\u53d6\u91d1\u9470\u7684\u6559\u5b78\u3002\u8acb\u6ce8\u610f\uff1a\u6b64 API \u6b0a\u6756\u8207\u5c0f\u7c73 Aqara \u6574\u5408\u6240\u4f7f\u7528\u4e4b\u91d1\u9470\u4e0d\u540c\u3002" }, "reauth_confirm": { "description": "\u5c0f\u7c73 Miio \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f\u3001\u65b9\u80fd\u66f4\u65b0\u6b0a\u6756\u6216\u65b0\u589e\u907a\u5931\u7684\u96f2\u7aef\u6191\u8b49\u3002", @@ -70,15 +46,7 @@ "data": { "select_device": "Miio \u88dd\u7f6e" }, - "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684 \u5c0f\u7c73 Miio \u88dd\u7f6e\u3002", - "title": "\u9023\u7dda\u81f3\u5c0f\u7c73 MIIO \u88dd\u7f6e\u6216\u5c0f\u7c73\u7db2\u95dc" - }, - "user": { - "data": { - "gateway": "\u9023\u7dda\u81f3\u5c0f\u7c73\u7db2\u95dc" - }, - "description": "\u9078\u64c7\u6240\u8981\u9023\u7dda\u7684\u88dd\u7f6e\u3002", - "title": "\u5c0f\u7c73 Miio" + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684 \u5c0f\u7c73 Miio \u88dd\u7f6e\u3002" } } }, @@ -90,9 +58,7 @@ "init": { "data": { "cloud_subdevices": "\u4f7f\u7528\u96f2\u7aef\u53d6\u5f97\u9023\u7dda\u5b50\u88dd\u7f6e" - }, - "description": "\u6307\u5b9a\u9078\u9805\u8a2d\u5b9a", - "title": "\u5c0f\u7c73 Miio" + } } } } diff --git a/homeassistant/components/yale_smart_alarm/translations/nl.json b/homeassistant/components/yale_smart_alarm/translations/nl.json index 04dbe9245c1..8d8697bbc59 100644 --- a/homeassistant/components/yale_smart_alarm/translations/nl.json +++ b/homeassistant/components/yale_smart_alarm/translations/nl.json @@ -11,7 +11,7 @@ "step": { "reauth_confirm": { "data": { - "area_id": "Area ID", + "area_id": "Area-ID", "name": "Naam", "password": "Wachtwoord", "username": "Gebruikersnaam" @@ -19,7 +19,7 @@ }, "user": { "data": { - "area_id": "Area ID", + "area_id": "Area-ID", "name": "Naam", "password": "Wachtwoord", "username": "Gebruikersnaam" diff --git a/homeassistant/components/yamaha_musiccast/translations/ko.json b/homeassistant/components/yamaha_musiccast/translations/ko.json new file mode 100644 index 00000000000..20ad990e862 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/ko.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/ar.json b/homeassistant/components/yeelight/translations/ar.json index e4146138625..5838a65a334 100644 --- a/homeassistant/components/yeelight/translations/ar.json +++ b/homeassistant/components/yeelight/translations/ar.json @@ -12,8 +12,7 @@ "init": { "data": { "use_music_mode": "\u062a\u0645\u0643\u064a\u0646 \u0648\u0636\u0639 \u0627\u0644\u0645\u0648\u0633\u064a\u0642\u0649" - }, - "description": "\u0625\u0630\u0627 \u062a\u0631\u0643\u062a \u0627\u0644\u0646\u0645\u0648\u0630\u062c \u0641\u0627\u0631\u063a\u064b\u0627 \u060c \u0641\u0633\u064a\u062a\u0645 \u0627\u0643\u062a\u0634\u0627\u0641\u0647 \u062a\u0644\u0642\u0627\u0626\u064a\u064b\u0627." + } } } } diff --git a/homeassistant/components/yeelight/translations/bg.json b/homeassistant/components/yeelight/translations/bg.json index a53214e40e4..4a962bbf3d0 100644 --- a/homeassistant/components/yeelight/translations/bg.json +++ b/homeassistant/components/yeelight/translations/bg.json @@ -26,8 +26,7 @@ "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." + } } } } diff --git a/homeassistant/components/yeelight/translations/ca.json b/homeassistant/components/yeelight/translations/ca.json index 0b62bebf1be..882c118b824 100644 --- a/homeassistant/components/yeelight/translations/ca.json +++ b/homeassistant/components/yeelight/translations/ca.json @@ -34,8 +34,7 @@ "save_on_change": "Desa l'estat en canviar", "transition": "Temps de transici\u00f3 (ms)", "use_music_mode": "Activa el mode M\u00fasica" - }, - "description": "Si deixes el model buit, es detectar\u00e0 autom\u00e0ticament." + } } } } diff --git a/homeassistant/components/yeelight/translations/cs.json b/homeassistant/components/yeelight/translations/cs.json index 2a3084fd3eb..5f91548bd4f 100644 --- a/homeassistant/components/yeelight/translations/cs.json +++ b/homeassistant/components/yeelight/translations/cs.json @@ -31,8 +31,7 @@ "save_on_change": "Ulo\u017eit stav p\u0159i zm\u011bn\u011b", "transition": "\u010cas p\u0159echodu (v ms)", "use_music_mode": "Povolit hudebn\u00ed re\u017eim" - }, - "description": "Pokud ponech\u00e1te model pr\u00e1zdn\u00fd, bude automaticky rozpozn\u00e1n." + } } } } diff --git a/homeassistant/components/yeelight/translations/de.json b/homeassistant/components/yeelight/translations/de.json index 5108b075968..a2323edbbdb 100644 --- a/homeassistant/components/yeelight/translations/de.json +++ b/homeassistant/components/yeelight/translations/de.json @@ -34,8 +34,7 @@ "save_on_change": "Status bei \u00c4nderung speichern", "transition": "\u00dcbergangszeit (ms)", "use_music_mode": "Musik-Modus aktivieren" - }, - "description": "Wenn du das Modell leer l\u00e4sst, wird es automatisch erkannt." + } } } } diff --git a/homeassistant/components/yeelight/translations/el.json b/homeassistant/components/yeelight/translations/el.json index 8b6c0ee2b9c..e69edd2cf74 100644 --- a/homeassistant/components/yeelight/translations/el.json +++ b/homeassistant/components/yeelight/translations/el.json @@ -34,8 +34,7 @@ "save_on_change": "\u0391\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae", "transition": "\u03a7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7\u03c2 (ms)", "use_music_mode": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03bc\u03bf\u03c5\u03c3\u03b9\u03ba\u03ae\u03c2" - }, - "description": "\u0395\u03ac\u03bd \u03b1\u03c6\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03ba\u03b5\u03bd\u03cc, \u03b8\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03c4\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1." + } } } } diff --git a/homeassistant/components/yeelight/translations/en.json b/homeassistant/components/yeelight/translations/en.json index 5e452dcf098..b49e3d7658e 100644 --- a/homeassistant/components/yeelight/translations/en.json +++ b/homeassistant/components/yeelight/translations/en.json @@ -34,8 +34,7 @@ "save_on_change": "Save Status On Change", "transition": "Transition Time (ms)", "use_music_mode": "Enable Music Mode" - }, - "description": "If you leave model empty, it will be automatically detected." + } } } } diff --git a/homeassistant/components/yeelight/translations/es.json b/homeassistant/components/yeelight/translations/es.json index bc9f26f665a..ef93c17e72c 100644 --- a/homeassistant/components/yeelight/translations/es.json +++ b/homeassistant/components/yeelight/translations/es.json @@ -34,8 +34,7 @@ "save_on_change": "Guardar estado al cambiar", "transition": "Tiempo de transici\u00f3n (ms)", "use_music_mode": "Activar el Modo M\u00fasica" - }, - "description": "Si dejas el modelo vac\u00edo, se detectar\u00e1 autom\u00e1ticamente." + } } } } diff --git a/homeassistant/components/yeelight/translations/et.json b/homeassistant/components/yeelight/translations/et.json index 70fdc7c8dbf..b994119bb46 100644 --- a/homeassistant/components/yeelight/translations/et.json +++ b/homeassistant/components/yeelight/translations/et.json @@ -34,8 +34,7 @@ "save_on_change": "Salvesta olek peale muutmist", "transition": "\u00dclemineku aeg (ms)", "use_music_mode": "Luba muusikare\u017eiim" - }, - "description": "Kui j\u00e4tad mudeli m\u00e4\u00e4ramata tuvastatakse see automaatselt." + } } } } diff --git a/homeassistant/components/yeelight/translations/fr.json b/homeassistant/components/yeelight/translations/fr.json index a319f15e36a..5b443a45530 100644 --- a/homeassistant/components/yeelight/translations/fr.json +++ b/homeassistant/components/yeelight/translations/fr.json @@ -34,8 +34,7 @@ "save_on_change": "Enregistrer l'\u00e9tat lors d'un changement", "transition": "Dur\u00e9e de la transition (en millisecondes)", "use_music_mode": "Activer le mode musique" - }, - "description": "Si vous ne pr\u00e9cisez pas le mod\u00e8le, il sera automatiquement d\u00e9tect\u00e9." + } } } } diff --git a/homeassistant/components/yeelight/translations/he.json b/homeassistant/components/yeelight/translations/he.json index 0fe8dd0f06e..94e0e87d87a 100644 --- a/homeassistant/components/yeelight/translations/he.json +++ b/homeassistant/components/yeelight/translations/he.json @@ -34,8 +34,7 @@ "save_on_change": "\u05e9\u05de\u05d5\u05e8 \u05e1\u05d8\u05d8\u05d5\u05e1 \u05d1\u05e9\u05d9\u05e0\u05d5\u05d9", "transition": "\u05d6\u05de\u05df \u05de\u05e2\u05d1\u05e8 (\u05d0\u05dc\u05e4\u05d9\u05d5\u05ea \u05e9\u05e0\u05d9\u05d4)", "use_music_mode": "\u05d4\u05e4\u05e2\u05dc\u05ea \u05de\u05e6\u05d1 \u05de\u05d5\u05e1\u05d9\u05e7\u05d4" - }, - "description": "\u05d0\u05dd \u05ea\u05e9\u05d0\u05d9\u05e8 \u05d0\u05ea \u05d4\u05d3\u05d2\u05dd \u05e8\u05d9\u05e7, \u05d4\u05d5\u05d0 \u05d9\u05d6\u05d5\u05d4\u05d4 \u05d1\u05d0\u05d5\u05e4\u05df \u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9." + } } } } diff --git a/homeassistant/components/yeelight/translations/hu.json b/homeassistant/components/yeelight/translations/hu.json index 6cf10422c28..b3969a44f99 100644 --- a/homeassistant/components/yeelight/translations/hu.json +++ b/homeassistant/components/yeelight/translations/hu.json @@ -34,8 +34,7 @@ "save_on_change": "\u00c1llapot ment\u00e9se m\u00f3dos\u00edt\u00e1s ut\u00e1n", "transition": "\u00c1tmeneti id\u0151 (ms)", "use_music_mode": "Zene m\u00f3d enged\u00e9lyez\u00e9se" - }, - "description": "Ha modellt \u00fcresen hagyja, a rendszer automatikusan \u00e9rz\u00e9keli." + } } } } diff --git a/homeassistant/components/yeelight/translations/id.json b/homeassistant/components/yeelight/translations/id.json index 19537d658a1..9209e2d917b 100644 --- a/homeassistant/components/yeelight/translations/id.json +++ b/homeassistant/components/yeelight/translations/id.json @@ -34,8 +34,7 @@ "save_on_change": "Simpan Status Saat Berubah", "transition": "Waktu Transisi (milidetik)", "use_music_mode": "Aktifkan Mode Musik" - }, - "description": "Jika model dibiarkan kosong, model akan dideteksi secara otomatis." + } } } } diff --git a/homeassistant/components/yeelight/translations/it.json b/homeassistant/components/yeelight/translations/it.json index 7022a016ce8..25a56dfb6c2 100644 --- a/homeassistant/components/yeelight/translations/it.json +++ b/homeassistant/components/yeelight/translations/it.json @@ -34,8 +34,7 @@ "save_on_change": "Salva stato su modifica", "transition": "Tempo di transizione (ms)", "use_music_mode": "Abilita la modalit\u00e0 musica" - }, - "description": "Se lasci il modello vuoto, sar\u00e0 rilevato automaticamente." + } } } } diff --git a/homeassistant/components/yeelight/translations/ja.json b/homeassistant/components/yeelight/translations/ja.json index 001cde41977..5a290043963 100644 --- a/homeassistant/components/yeelight/translations/ja.json +++ b/homeassistant/components/yeelight/translations/ja.json @@ -34,8 +34,7 @@ "save_on_change": "\u5909\u66f4\u6642\u306b\u30b9\u30c6\u30fc\u30bf\u30b9\u3092\u4fdd\u5b58", "transition": "\u9077\u79fb\u6642\u9593(Transition Time)(ms)", "use_music_mode": "\u97f3\u697d\u30e2\u30fc\u30c9\u3092\u6709\u52b9\u306b\u3059\u308b" - }, - "description": "\u30e2\u30c7\u30eb\u3092\u7a7a\u767d\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u81ea\u52d5\u7684\u306b\u691c\u51fa\u3055\u308c\u307e\u3059\u3002" + } } } } diff --git a/homeassistant/components/yeelight/translations/ko.json b/homeassistant/components/yeelight/translations/ko.json index 4abb8fcbbff..b8a4385bfdf 100644 --- a/homeassistant/components/yeelight/translations/ko.json +++ b/homeassistant/components/yeelight/translations/ko.json @@ -30,8 +30,7 @@ "save_on_change": "\ubcc0\uacbd \uc2dc \uc0c1\ud0dc\ub97c \uc800\uc7a5\ud558\uae30", "transition": "\uc804\ud658 \uc2dc\uac04(ms)", "use_music_mode": "\uc74c\uc545 \ubaa8\ub4dc \ud65c\uc131\ud654\ud558\uae30" - }, - "description": "\ubaa8\ub378\uc744 \ube44\uc6cc \ub450\uba74 \uc790\ub3d9\uc73c\ub85c \uac80\uc0c9\ub429\ub2c8\ub2e4." + } } } } diff --git a/homeassistant/components/yeelight/translations/lb.json b/homeassistant/components/yeelight/translations/lb.json index 482cbf23e48..5ffa8e23a18 100644 --- a/homeassistant/components/yeelight/translations/lb.json +++ b/homeassistant/components/yeelight/translations/lb.json @@ -30,8 +30,7 @@ "save_on_change": "Status sp\u00e4icheren bei \u00c4nnerung", "transition": "Iwwergangsz\u00e4it (ms)", "use_music_mode": "Musek Modus aktiv\u00e9ieren" - }, - "description": "Falls Modell eidel gelass g\u00ebtt, g\u00ebtt et automatesch erkannt." + } } } } diff --git a/homeassistant/components/yeelight/translations/nl.json b/homeassistant/components/yeelight/translations/nl.json index a83ef72695c..7767c56e7fb 100644 --- a/homeassistant/components/yeelight/translations/nl.json +++ b/homeassistant/components/yeelight/translations/nl.json @@ -34,8 +34,7 @@ "save_on_change": "Bewaar status bij wijziging", "transition": "Overgangstijd (ms)", "use_music_mode": "Schakel de muziekmodus in" - }, - "description": "Als u model leeg laat, wordt het automatisch gedetecteerd." + } } } } diff --git a/homeassistant/components/yeelight/translations/no.json b/homeassistant/components/yeelight/translations/no.json index ea4436d7769..a7ef2e05a5d 100644 --- a/homeassistant/components/yeelight/translations/no.json +++ b/homeassistant/components/yeelight/translations/no.json @@ -34,8 +34,7 @@ "save_on_change": "Lagre status ved endring", "transition": "Overgangstid (ms)", "use_music_mode": "Aktiver musikkmodus" - }, - "description": "Hvis du lar modellen v\u00e6re tom, blir den automatisk oppdaget." + } } } } diff --git a/homeassistant/components/yeelight/translations/pl.json b/homeassistant/components/yeelight/translations/pl.json index 818ff0946c4..612fab44a62 100644 --- a/homeassistant/components/yeelight/translations/pl.json +++ b/homeassistant/components/yeelight/translations/pl.json @@ -34,8 +34,7 @@ "save_on_change": "Zachowaj status po zmianie", "transition": "Czas przej\u015bcia (ms)", "use_music_mode": "W\u0142\u0105cz tryb muzyczny" - }, - "description": "Je\u015bli nie podasz modelu urz\u0105dzenia, zostanie on automatycznie wykryty." + } } } } diff --git a/homeassistant/components/yeelight/translations/pt-BR.json b/homeassistant/components/yeelight/translations/pt-BR.json index 2c54af41b25..482733df343 100644 --- a/homeassistant/components/yeelight/translations/pt-BR.json +++ b/homeassistant/components/yeelight/translations/pt-BR.json @@ -34,8 +34,7 @@ "save_on_change": "Salvar status na altera\u00e7\u00e3o", "transition": "Tempo de transi\u00e7\u00e3o (ms)", "use_music_mode": "Ativar o modo de m\u00fasica" - }, - "description": "Se voc\u00ea deixar o modelo vazio, ele ser\u00e1 detectado automaticamente." + } } } } diff --git a/homeassistant/components/yeelight/translations/pt.json b/homeassistant/components/yeelight/translations/pt.json index 6d350188989..b03d5b8fc6b 100644 --- a/homeassistant/components/yeelight/translations/pt.json +++ b/homeassistant/components/yeelight/translations/pt.json @@ -30,8 +30,7 @@ "save_on_change": "Salvar status ao alterar", "transition": "Tempo de transi\u00e7\u00e3o (ms)", "use_music_mode": "Ativar modo de m\u00fasica" - }, - "description": "Se voc\u00ea deixar o modelo vazio, ele ser\u00e1 detectado automaticamente." + } } } } diff --git a/homeassistant/components/yeelight/translations/ru.json b/homeassistant/components/yeelight/translations/ru.json index 70694147baa..88ab0a73735 100644 --- a/homeassistant/components/yeelight/translations/ru.json +++ b/homeassistant/components/yeelight/translations/ru.json @@ -34,8 +34,7 @@ "save_on_change": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0441\u0442\u0430\u0442\u0443\u0441 \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438", "transition": "\u0412\u0440\u0435\u043c\u044f \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 (\u0432 \u043c\u0438\u043b\u043b\u0438\u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "use_music_mode": "\u041c\u0443\u0437\u044b\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c" - }, - "description": "\u0415\u0441\u043b\u0438 \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u0430, \u043e\u043d\u0430 \u0431\u0443\u0434\u0435\u0442 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438." + } } } } diff --git a/homeassistant/components/yeelight/translations/tr.json b/homeassistant/components/yeelight/translations/tr.json index 4eed4f477b4..21ec60cde68 100644 --- a/homeassistant/components/yeelight/translations/tr.json +++ b/homeassistant/components/yeelight/translations/tr.json @@ -34,8 +34,7 @@ "save_on_change": "De\u011fi\u015fiklikte Durumu Kaydet", "transition": "Ge\u00e7i\u015f S\u00fcresi (ms)", "use_music_mode": "M\u00fczik Modunu Etkinle\u015ftir" - }, - "description": "Modeli bo\u015f b\u0131rak\u0131rsan\u0131z, otomatik olarak alg\u0131lanacakt\u0131r." + } } } } diff --git a/homeassistant/components/yeelight/translations/uk.json b/homeassistant/components/yeelight/translations/uk.json index 0a173ccb6e4..2149302e940 100644 --- a/homeassistant/components/yeelight/translations/uk.json +++ b/homeassistant/components/yeelight/translations/uk.json @@ -30,8 +30,7 @@ "save_on_change": "\u0417\u0431\u0435\u0440\u0456\u0433\u0430\u0442\u0438 \u0441\u0442\u0430\u0442\u0443\u0441 \u043f\u0440\u0438 \u0437\u043c\u0456\u043d\u0456", "transition": "\u0427\u0430\u0441 \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0443 (\u0432 \u043c\u0456\u043b\u0456\u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "use_music_mode": "\u041c\u0443\u0437\u0438\u0447\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c" - }, - "description": "\u042f\u043a\u0449\u043e \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u0432\u0438\u0431\u0440\u0430\u043d\u043e, \u0432\u043e\u043d\u0430 \u0431\u0443\u0434\u0435 \u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e." + } } } } diff --git a/homeassistant/components/yeelight/translations/zh-Hans.json b/homeassistant/components/yeelight/translations/zh-Hans.json index 36add653d1d..846324d143c 100644 --- a/homeassistant/components/yeelight/translations/zh-Hans.json +++ b/homeassistant/components/yeelight/translations/zh-Hans.json @@ -34,8 +34,7 @@ "save_on_change": "\u4fdd\u5b58\u66f4\u6539\u72b6\u6001", "transition": "\u8fc7\u6e21\u65f6\u95f4\uff08\u6beb\u79d2\uff09", "use_music_mode": "\u542f\u7528\u97f3\u4e50\u6a21\u5f0f" - }, - "description": "\u5982\u679c\u5c06\u4fe1\u53f7\u680f\u7559\u7a7a\uff0c\u96c6\u6210\u5c06\u4f1a\u81ea\u52a8\u68c0\u6d4b\u76f8\u5173\u4fe1\u606f" + } } } } diff --git a/homeassistant/components/yeelight/translations/zh-Hant.json b/homeassistant/components/yeelight/translations/zh-Hant.json index 7601a0b9552..816e2b02937 100644 --- a/homeassistant/components/yeelight/translations/zh-Hant.json +++ b/homeassistant/components/yeelight/translations/zh-Hant.json @@ -34,8 +34,7 @@ "save_on_change": "\u65bc\u8b8a\u66f4\u6642\u5132\u5b58\u72c0\u614b", "transition": "\u8f49\u63db\u6642\u9593\uff08\u6beb\u79d2\uff09", "use_music_mode": "\u958b\u555f\u97f3\u6a02\u6a21\u5f0f" - }, - "description": "\u5047\u5982\u578b\u865f\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u81ea\u52d5\u5075\u6e2c\u578b\u865f\u3002" + } } } } diff --git a/homeassistant/components/yolink/translations/ca.json b/homeassistant/components/yolink/translations/ca.json new file mode 100644 index 00000000000..db2adafa3f9 --- /dev/null +++ b/homeassistant/components/yolink/translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "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.", + "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", + "oauth_error": "S'han rebut dades token inv\u00e0lides.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa" + }, + "step": { + "pick_implementation": { + "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + }, + "reauth_confirm": { + "description": "La integraci\u00f3 yolink ha de tornar a autenticar-se amb el teu compte.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/de.json b/homeassistant/components/yolink/translations/de.json new file mode 100644 index 00000000000..d9d5c9a6efa --- /dev/null +++ b/homeassistant/components/yolink/translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", + "oauth_error": "Ung\u00fcltige Token-Daten empfangen.", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "create_entry": { + "default": "Erfolgreich authentifiziert" + }, + "step": { + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + }, + "reauth_confirm": { + "description": "Die yolink-Integration muss dein Konto neu authentifizieren", + "title": "Integration erneut authentifizieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/he.json b/homeassistant/components/yolink/translations/he.json new file mode 100644 index 00000000000..525624782ac --- /dev/null +++ b/homeassistant/components/yolink/translations/he.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "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})", + "oauth_error": "\u05d4\u05ea\u05e7\u05d1\u05dc\u05d5 \u05e0\u05ea\u05d5\u05e0\u05d9 \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd.", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "create_entry": { + "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" + }, + "step": { + "pick_implementation": { + "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" + }, + "reauth_confirm": { + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/id.json b/homeassistant/components/yolink/translations/id.json index e2c5ce51b91..a8211681abf 100644 --- a/homeassistant/components/yolink/translations/id.json +++ b/homeassistant/components/yolink/translations/id.json @@ -17,6 +17,7 @@ "title": "Pilih Metode Autentikasi" }, "reauth_confirm": { + "description": "Integrasi yolink perlu mengautentikasi ulang akun Anda", "title": "Autentikasi Ulang Integrasi" } } diff --git a/homeassistant/components/yolink/translations/ko.json b/homeassistant/components/yolink/translations/ko.json index 6ead9fca594..5a613bf437e 100644 --- a/homeassistant/components/yolink/translations/ko.json +++ b/homeassistant/components/yolink/translations/ko.json @@ -4,7 +4,9 @@ "already_configured": "\uacc4\uc815\uc774 \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", "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "oauth_error": "\uc798\ubabb\ub41c \ud1a0\ud070 \ub370\uc774\ud130\ub97c \ubc1b\uc558\uc2b5\ub2c8\ub2e4.", "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "create_entry": { diff --git a/homeassistant/components/yolink/translations/no.json b/homeassistant/components/yolink/translations/no.json new file mode 100644 index 00000000000..b5e26ac910d --- /dev/null +++ b/homeassistant/components/yolink/translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", + "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", + "oauth_error": "Mottatt ugyldige token data.", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "create_entry": { + "default": "Vellykket godkjenning" + }, + "step": { + "pick_implementation": { + "title": "Velg godkjenningsmetode" + }, + "reauth_confirm": { + "description": "Yolink-integrasjonen m\u00e5 autentisere kontoen din p\u00e5 nytt", + "title": "Godkjenne integrering p\u00e5 nytt" + } + } + } +} \ 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 59fcf968740..5d2486acbc1 100644 --- a/homeassistant/components/zwave_js/translations/bg.json +++ b/homeassistant/components/zwave_js/translations/bg.json @@ -35,7 +35,6 @@ "step": { "configure_addon": { "data": { - "network_key": "\u041c\u0440\u0435\u0436\u043e\u0432 \u043a\u043b\u044e\u0447", "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", diff --git a/homeassistant/components/zwave_js/translations/ca.json b/homeassistant/components/zwave_js/translations/ca.json index 6691894b79d..de21cc3f232 100644 --- a/homeassistant/components/zwave_js/translations/ca.json +++ b/homeassistant/components/zwave_js/translations/ca.json @@ -26,7 +26,6 @@ "step": { "configure_addon": { "data": { - "network_key": "Clau de xarxa", "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", @@ -117,7 +116,6 @@ "data": { "emulate_hardware": "Emula maquinari", "log_level": "Nivell dels registres", - "network_key": "Clau de xarxa", "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", @@ -146,6 +144,5 @@ "title": "El complement Z-Wave JS s'est\u00e0 iniciant." } } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/cs.json b/homeassistant/components/zwave_js/translations/cs.json index 5c16938b23b..6417d5a587f 100644 --- a/homeassistant/components/zwave_js/translations/cs.json +++ b/homeassistant/components/zwave_js/translations/cs.json @@ -45,8 +45,7 @@ "step": { "configure_addon": { "data": { - "log_level": "\u00darove\u0148 protokolu", - "network_key": "S\u00ed\u0165ov\u00fd kl\u00ed\u010d" + "log_level": "\u00darove\u0148 protokolu" } }, "manual": { diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json index 02418f44306..ec0157f9a66 100644 --- a/homeassistant/components/zwave_js/translations/de.json +++ b/homeassistant/components/zwave_js/translations/de.json @@ -26,7 +26,6 @@ "step": { "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", @@ -117,7 +116,6 @@ "data": { "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", @@ -146,6 +144,5 @@ "title": "Das Z-Wave JS Add-on wird gestartet." } } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/el.json b/homeassistant/components/zwave_js/translations/el.json index 66330206b60..930c82aa891 100644 --- a/homeassistant/components/zwave_js/translations/el.json +++ b/homeassistant/components/zwave_js/translations/el.json @@ -26,7 +26,6 @@ "step": { "configure_addon": { "data": { - "network_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5", "s0_legacy_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af S0 (\u03c0\u03b1\u03bb\u03b1\u03b9\u03bf\u03cd \u03c4\u03cd\u03c0\u03bf\u03c5)", "s2_access_control_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 S2", "s2_authenticated_key": "\u03a0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af S2", @@ -117,7 +116,6 @@ "data": { "emulate_hardware": "\u0395\u03be\u03bf\u03bc\u03bf\u03af\u03c9\u03c3\u03b7 \u03c5\u03bb\u03b9\u03ba\u03bf\u03cd", "log_level": "\u0395\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2", - "network_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5", "s0_legacy_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af S0 (\u03c0\u03b1\u03bb\u03b1\u03b9\u03bf\u03cd \u03c4\u03cd\u03c0\u03bf\u03c5)", "s2_access_control_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 S2", "s2_authenticated_key": "\u03a0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af S2", @@ -146,6 +144,5 @@ "title": "\u03a4\u03bf \u03c0\u03c1\u03cc\u03c3\u03b8\u03b5\u03c4\u03bf Z-Wave JS \u03be\u03b5\u03ba\u03b9\u03bd\u03ac." } } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index fa9794ed847..843f2aaf284 100644 --- a/homeassistant/components/zwave_js/translations/en.json +++ b/homeassistant/components/zwave_js/translations/en.json @@ -26,7 +26,6 @@ "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", @@ -117,7 +116,6 @@ "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", @@ -146,6 +144,5 @@ "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 dfe8c1de296..5d3efb0e7f4 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -26,7 +26,6 @@ "step": { "configure_addon": { "data": { - "network_key": "Clave de red", "s0_legacy_key": "Clave S0 (heredada)", "s2_access_control_key": "Clave de control de acceso S2", "s2_authenticated_key": "Clave autenticada de S2", @@ -117,7 +116,6 @@ "data": { "emulate_hardware": "Emular el hardware", "log_level": "Nivel de registro", - "network_key": "Clave de red", "s0_legacy_key": "Tecla S0 (heredada)", "s2_access_control_key": "Clave de control de acceso S2", "s2_authenticated_key": "Clave autenticada de S2", @@ -146,6 +144,5 @@ "title": "Se est\u00e1 iniciando el complemento Z-Wave JS." } } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/et.json b/homeassistant/components/zwave_js/translations/et.json index 29f50ccb290..ea0686e424f 100644 --- a/homeassistant/components/zwave_js/translations/et.json +++ b/homeassistant/components/zwave_js/translations/et.json @@ -26,7 +26,6 @@ "step": { "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", @@ -117,7 +116,6 @@ "data": { "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", @@ -146,6 +144,5 @@ "title": "Z-Wave JS lisandmoodul k\u00e4ivitub." } } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index 22e8ed19e32..47c3086489c 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -26,7 +26,6 @@ "step": { "configure_addon": { "data": { - "network_key": "Cl\u00e9 r\u00e9seau", "s0_legacy_key": "Cl\u00e9 S0 (h\u00e9rit\u00e9e)", "s2_access_control_key": "Cl\u00e9 de contr\u00f4le d'acc\u00e8s S2", "s2_authenticated_key": "Cl\u00e9 d'authentification S2", @@ -117,7 +116,6 @@ "data": { "emulate_hardware": "\u00c9muler le mat\u00e9riel", "log_level": "Niveau du journal", - "network_key": "Cl\u00e9 r\u00e9seau", "s0_legacy_key": "Cl\u00e9 S0 (h\u00e9rit\u00e9e)", "s2_access_control_key": "Cl\u00e9 de contr\u00f4le d'acc\u00e8s S2", "s2_authenticated_key": "Cl\u00e9 d'authentification S2", @@ -146,6 +144,5 @@ "title": "Le module compl\u00e9mentaire Z-Wave JS d\u00e9marre." } } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/he.json b/homeassistant/components/zwave_js/translations/he.json index 041e1cafec6..0d6b2a9f619 100644 --- a/homeassistant/components/zwave_js/translations/he.json +++ b/homeassistant/components/zwave_js/translations/he.json @@ -48,7 +48,6 @@ "configure_addon": { "data": { "log_level": "\u05e8\u05de\u05ea \u05d9\u05d5\u05de\u05df \u05e8\u05d9\u05e9\u05d5\u05dd", - "network_key": "\u05de\u05e4\u05ea\u05d7 \u05e8\u05e9\u05ea", "usb_path": "\u05e0\u05ea\u05d9\u05d1 \u05d4\u05ea\u05e7\u05df USB" } }, @@ -64,6 +63,5 @@ "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 diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json index 07dbb93b703..37e19e5471f 100644 --- a/homeassistant/components/zwave_js/translations/hu.json +++ b/homeassistant/components/zwave_js/translations/hu.json @@ -26,7 +26,6 @@ "step": { "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", @@ -117,7 +116,6 @@ "data": { "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", @@ -146,6 +144,5 @@ "title": "Indul a Z-Wave JS b\u0151v\u00edtm\u00e9ny." } } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/id.json b/homeassistant/components/zwave_js/translations/id.json index ef14a550210..340266e9a9b 100644 --- a/homeassistant/components/zwave_js/translations/id.json +++ b/homeassistant/components/zwave_js/translations/id.json @@ -26,7 +26,6 @@ "step": { "configure_addon": { "data": { - "network_key": "Kunci Jaringan", "s0_legacy_key": "Kunci S0 (Warisan)", "s2_access_control_key": "Kunci Kontrol Akses S2", "s2_authenticated_key": "Kunci Autentikasi S2", @@ -117,7 +116,6 @@ "data": { "emulate_hardware": "Emulasikan Perangkat Keras", "log_level": "Tingkat log", - "network_key": "Kunci Jaringan", "s0_legacy_key": "Kunci S0 (Warisan)", "s2_access_control_key": "Kunci Kontrol Akses S2", "s2_authenticated_key": "Kunci Autentikasi S2", @@ -146,6 +144,5 @@ "title": "Add-on Z-Wave JS sedang dimulai." } } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json index e82ccfe8476..eb1e829a52c 100644 --- a/homeassistant/components/zwave_js/translations/it.json +++ b/homeassistant/components/zwave_js/translations/it.json @@ -26,7 +26,6 @@ "step": { "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", @@ -117,7 +116,6 @@ "data": { "emulate_hardware": "Emula 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", @@ -146,6 +144,5 @@ "title": "Il componente aggiuntivo Z-Wave JS si sta avviando." } } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index 9b00f062993..902955c1b9e 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -26,7 +26,6 @@ "step": { "configure_addon": { "data": { - "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30ad\u30fc", "s0_legacy_key": "S0\u30ad\u30fc (\u30ec\u30ac\u30b7\u30fc)", "s2_access_control_key": "S2\u30a2\u30af\u30bb\u30b9\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30ad\u30fc", "s2_authenticated_key": "S2\u8a8d\u8a3c\u6e08\u307f\u306a\u30ad\u30fc", @@ -117,7 +116,6 @@ "data": { "emulate_hardware": "\u30cf\u30fc\u30c9\u30a6\u30a7\u30a2\u306e\u30a8\u30df\u30e5\u30ec\u30fc\u30b7\u30e7\u30f3", "log_level": "\u30ed\u30b0\u30ec\u30d9\u30eb", - "network_key": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af", "s0_legacy_key": "S0\u30ad\u30fc (\u30ec\u30ac\u30b7\u30fc)", "s2_access_control_key": "S2\u30a2\u30af\u30bb\u30b9\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30ad\u30fc", "s2_authenticated_key": "S2\u8a8d\u8a3c\u6e08\u307f\u306a\u30ad\u30fc", @@ -146,6 +144,5 @@ "title": "Z-Wave JS \u30a2\u30c9\u30aa\u30f3\u304c\u8d77\u52d5\u3057\u3066\u3044\u307e\u3059\u3002" } } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/ko.json b/homeassistant/components/zwave_js/translations/ko.json index 2c14397177e..7bf9dc8f062 100644 --- a/homeassistant/components/zwave_js/translations/ko.json +++ b/homeassistant/components/zwave_js/translations/ko.json @@ -23,7 +23,6 @@ "step": { "configure_addon": { "data": { - "network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4", "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c" }, "title": "Z-Wave JS \uc560\ub4dc\uc628\uc758 \uad6c\uc131\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694" @@ -56,7 +55,9 @@ "set_config_parameter": "\uad6c\uc131 \ub9e4\uac1c\ubcc0\uc218 {subtype} \uc758 \uac12 \uc124\uc815", "set_lock_usercode": "{entity_name} \uc5d0 \uc0ac\uc6a9\uc790 \ucf54\ub4dc \uc124\uc815", "set_value": "Z-Wave Value\uc758 \uc124\uc815\uac12" + }, + "trigger_type": { + "event.value_notification.scene_activation": "{subtype} \uc5d0\uc11c \uc7a5\uba74 \ud65c\uc131\ud654" } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/lb.json b/homeassistant/components/zwave_js/translations/lb.json index d84c5323fb9..04259de2303 100644 --- a/homeassistant/components/zwave_js/translations/lb.json +++ b/homeassistant/components/zwave_js/translations/lb.json @@ -8,6 +8,5 @@ "invalid_ws_url": "Ong\u00eblteg Websocket URL", "unknown": "Onerwaarte Feeler" } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json index 2d3ce5421ed..53bf45e2f4d 100644 --- a/homeassistant/components/zwave_js/translations/nl.json +++ b/homeassistant/components/zwave_js/translations/nl.json @@ -26,7 +26,6 @@ "step": { "configure_addon": { "data": { - "network_key": "Netwerksleutel", "s0_legacy_key": "S0 Sleutel (Legacy)", "s2_access_control_key": "S2 Toegangscontrolesleutel", "s2_authenticated_key": "S2 geverifieerde sleutel", @@ -117,7 +116,6 @@ "data": { "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", @@ -146,6 +144,5 @@ "title": "The Z-Wave JS add-on is aan het starten." } } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/no.json b/homeassistant/components/zwave_js/translations/no.json index 854bd307abc..c89e5582677 100644 --- a/homeassistant/components/zwave_js/translations/no.json +++ b/homeassistant/components/zwave_js/translations/no.json @@ -26,7 +26,6 @@ "step": { "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", @@ -117,7 +116,6 @@ "data": { "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", @@ -146,6 +144,5 @@ "title": "Z-Wave JS-tillegget starter" } } - }, - "title": "" + } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/pl.json b/homeassistant/components/zwave_js/translations/pl.json index 2edb9fd8c1f..3929aa668ef 100644 --- a/homeassistant/components/zwave_js/translations/pl.json +++ b/homeassistant/components/zwave_js/translations/pl.json @@ -26,7 +26,6 @@ "step": { "configure_addon": { "data": { - "network_key": "Klucz sieci", "s0_legacy_key": "Klucz S0 (Legacy)", "s2_access_control_key": "Klucz kontroli dost\u0119pu S2", "s2_authenticated_key": "Klucz uwierzytelniony S2", @@ -117,7 +116,6 @@ "data": { "emulate_hardware": "Emulacja sprz\u0119tu", "log_level": "Poziom loga", - "network_key": "Klucz sieci", "s0_legacy_key": "Klucz S0 (Legacy)", "s2_access_control_key": "Klucz kontroli dost\u0119pu S2", "s2_authenticated_key": "Klucz uwierzytelniony S2", @@ -146,6 +144,5 @@ "title": "Dodatek Z-Wave JS uruchamia si\u0119..." } } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/pt-BR.json b/homeassistant/components/zwave_js/translations/pt-BR.json index 5e1e8610fc7..e8587aa6f94 100644 --- a/homeassistant/components/zwave_js/translations/pt-BR.json +++ b/homeassistant/components/zwave_js/translations/pt-BR.json @@ -26,7 +26,6 @@ "step": { "configure_addon": { "data": { - "network_key": "Chave de rede", "s0_legacy_key": "Chave S0 (Legado)", "s2_access_control_key": "Chave de controle de acesso S2", "s2_authenticated_key": "Chave autenticada S2", @@ -117,7 +116,6 @@ "data": { "emulate_hardware": "Emular hardware", "log_level": "N\u00edvel de registro", - "network_key": "Chave de rede", "s0_legacy_key": "Chave S0 (Legado)", "s2_access_control_key": "Chave de controle de acesso S2", "s2_authenticated_key": "Chave autenticada S2", @@ -146,6 +144,5 @@ "title": "O add-on Z-Wave JS est\u00e1 iniciando." } } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/ru.json b/homeassistant/components/zwave_js/translations/ru.json index 14011c78517..44979a6cfe9 100644 --- a/homeassistant/components/zwave_js/translations/ru.json +++ b/homeassistant/components/zwave_js/translations/ru.json @@ -26,7 +26,6 @@ "step": { "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", @@ -117,7 +116,6 @@ "data": { "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", @@ -146,6 +144,5 @@ "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f" } } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/tr.json b/homeassistant/components/zwave_js/translations/tr.json index a3b5f6fb9f4..21d8f03bec6 100644 --- a/homeassistant/components/zwave_js/translations/tr.json +++ b/homeassistant/components/zwave_js/translations/tr.json @@ -26,7 +26,6 @@ "step": { "configure_addon": { "data": { - "network_key": "A\u011f Anahtar\u0131", "s0_legacy_key": "S0 Anahtar\u0131 (Eski)", "s2_access_control_key": "S2 Eri\u015fim Kontrol Anahtar\u0131", "s2_authenticated_key": "S2 Kimli\u011fi Do\u011frulanm\u0131\u015f Anahtar", @@ -117,7 +116,6 @@ "data": { "emulate_hardware": "Donan\u0131m\u0131 Taklit Et", "log_level": "G\u00fcnl\u00fck d\u00fczeyi", - "network_key": "A\u011f Anahtar\u0131", "s0_legacy_key": "S0 Anahtar\u0131 (Eski)", "s2_access_control_key": "S2 Eri\u015fim Kontrol Anahtar\u0131", "s2_authenticated_key": "S2 Kimli\u011fi Do\u011frulanm\u0131\u015f Anahtar", @@ -146,6 +144,5 @@ "title": "Z-Wave JS eklentisi ba\u015fl\u0131yor." } } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/uk.json b/homeassistant/components/zwave_js/translations/uk.json index fe77655ac29..95c9b44fb86 100644 --- a/homeassistant/components/zwave_js/translations/uk.json +++ b/homeassistant/components/zwave_js/translations/uk.json @@ -8,6 +8,5 @@ "invalid_ws_url": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0432\u0435\u0431-\u0441\u043e\u043a\u0435\u0442\u0430", "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json index 2ea728a591e..246800d1048 100644 --- a/homeassistant/components/zwave_js/translations/zh-Hant.json +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -26,7 +26,6 @@ "step": { "configure_addon": { "data": { - "network_key": "\u7db2\u8def\u91d1\u9470", "s0_legacy_key": "S0 \u91d1\u9470\uff08\u820a\u7248\uff09", "s2_access_control_key": "S2 \u5b58\u53d6\u63a7\u5236\u91d1\u9470", "s2_authenticated_key": "S2 \u9a57\u8b49\u91d1\u9470", @@ -117,7 +116,6 @@ "data": { "emulate_hardware": "\u6a21\u64ec\u786c\u9ad4", "log_level": "\u65e5\u8a8c\u8a18\u9304\u7b49\u7d1a", - "network_key": "\u7db2\u8def\u91d1\u9470", "s0_legacy_key": "S0 \u91d1\u9470\uff08\u820a\u7248\uff09", "s2_access_control_key": "S2 \u5b58\u53d6\u63a7\u5236\u91d1\u9470", "s2_authenticated_key": "S2 \u9a57\u8b49\u91d1\u9470", @@ -146,6 +144,5 @@ "title": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u4e2d\u3002" } } - }, - "title": "Z-Wave JS" + } } \ No newline at end of file From edd7a3427ca0522b907df91c9bd9cae40bbb9f33 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 19 May 2022 04:52:38 +0200 Subject: [PATCH 0667/3516] Remove support for databases without ROW_NUMBER (#72092) --- homeassistant/components/recorder/core.py | 1 - .../components/recorder/statistics.py | 97 +++++-------------- homeassistant/components/recorder/util.py | 15 --- tests/components/recorder/test_util.py | 49 ++++------ tests/components/sensor/test_recorder.py | 13 +-- 5 files changed, 45 insertions(+), 130 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 5a3a41568a1..5e6fafdfa61 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -180,7 +180,6 @@ class Recorder(threading.Thread): self._completed_first_database_setup: bool | None = None self.async_migration_event = asyncio.Event() self.migration_in_progress = False - self._db_supports_row_number = True self._database_lock_task: DatabaseLockTask | None = None self._db_executor: DBInterruptibleThreadPoolExecutor | None = None self._exclude_attributes_by_domain = exclude_attributes_by_domain diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 77f56bc59fa..2c6c51a31f4 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -437,22 +437,6 @@ def _compile_hourly_statistics_summary_mean_stmt( return stmt -def _compile_hourly_statistics_summary_sum_legacy_stmt( - start_time: datetime, end_time: datetime -) -> StatementLambdaElement: - """Generate the legacy sum statement for hourly statistics. - - This is used for databases not supporting row number. - """ - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SUMMARY_SUM_LEGACY)) - stmt += ( - lambda q: q.filter(StatisticsShortTerm.start >= start_time) - .filter(StatisticsShortTerm.start < end_time) - .order_by(StatisticsShortTerm.metadata_id, StatisticsShortTerm.start.desc()) - ) - return stmt - - def compile_hourly_statistics( instance: Recorder, session: Session, start: datetime ) -> None: @@ -481,66 +465,37 @@ def compile_hourly_statistics( } # Get last hour's last sum - 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)) + 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( - { - "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: - stmt = _compile_hourly_statistics_summary_sum_legacy_stmt(start_time, end_time) - stats = execute_stmt_lambda_element(session, stmt) - - if stats: - for metadata_id, group in groupby(stats, lambda stat: stat["metadata_id"]): # type: ignore[no-any-return] - ( - 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, + 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, + } # 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 ce8812a653e..b010ddfa853 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -52,12 +52,9 @@ SQLITE3_POSTFIXES = ["", "-wal", "-shm"] DEFAULT_YIELD_STATES_ROWS = 32768 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("12.0", AwesomeVersionStrategy.SIMPLEVER) MIN_VERSION_SQLITE = AwesomeVersion("3.31.0", 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 @@ -414,10 +411,6 @@ def setup_connection_for_dialect( 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: _fail_unsupported_version( version or version_string, "SQLite", MIN_VERSION_SQLITE @@ -448,19 +441,11 @@ def setup_connection_for_dialect( is_maria_db = "mariadb" in version_string.lower() 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: _fail_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: _fail_unsupported_version( version or version_string, "MySQL", MIN_VERSION_MYSQL diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index e7685a93ff2..343c57045cf 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -166,15 +166,12 @@ async def test_last_run_was_recently_clean( @pytest.mark.parametrize( - "mysql_version, db_supports_row_number", - [ - ("10.3.0-MariaDB", True), - ("8.0.0", True), - ], + "mysql_version", + ["10.3.0-MariaDB", "8.0.0"], ) -def test_setup_connection_for_dialect_mysql(mysql_version, db_supports_row_number): +def test_setup_connection_for_dialect_mysql(mysql_version): """Test setting up the connection for a mysql dialect.""" - instance_mock = MagicMock(_db_supports_row_number=True) + instance_mock = MagicMock() execute_args = [] close_mock = MagicMock() @@ -199,18 +196,14 @@ def test_setup_connection_for_dialect_mysql(mysql_version, db_supports_row_numbe 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 - @pytest.mark.parametrize( - "sqlite_version, db_supports_row_number", - [ - ("3.31.0", True), - ], + "sqlite_version", + ["3.31.0"], ) -def test_setup_connection_for_dialect_sqlite(sqlite_version, db_supports_row_number): +def test_setup_connection_for_dialect_sqlite(sqlite_version): """Test setting up the connection for a sqlite dialect.""" - instance_mock = MagicMock(_db_supports_row_number=True) + instance_mock = MagicMock() execute_args = [] close_mock = MagicMock() @@ -246,20 +239,16 @@ def test_setup_connection_for_dialect_sqlite(sqlite_version, db_supports_row_num assert execute_args[1] == "PRAGMA synchronous=NORMAL" assert execute_args[2] == "PRAGMA foreign_keys=ON" - assert instance_mock._db_supports_row_number == db_supports_row_number - @pytest.mark.parametrize( - "sqlite_version, db_supports_row_number", - [ - ("3.31.0", True), - ], + "sqlite_version", + ["3.31.0"], ) def test_setup_connection_for_dialect_sqlite_zero_commit_interval( - sqlite_version, db_supports_row_number + sqlite_version, ): """Test setting up the connection for a sqlite dialect with a zero commit interval.""" - instance_mock = MagicMock(_db_supports_row_number=True, commit_interval=0) + instance_mock = MagicMock(commit_interval=0) execute_args = [] close_mock = MagicMock() @@ -295,8 +284,6 @@ def test_setup_connection_for_dialect_sqlite_zero_commit_interval( assert execute_args[1] == "PRAGMA synchronous=FULL" assert execute_args[2] == "PRAGMA foreign_keys=ON" - assert instance_mock._db_supports_row_number == db_supports_row_number - @pytest.mark.parametrize( "mysql_version,message", @@ -317,7 +304,7 @@ def test_setup_connection_for_dialect_sqlite_zero_commit_interval( ) def test_fail_outdated_mysql(caplog, mysql_version, message): """Test setting up the connection for an outdated mysql version.""" - instance_mock = MagicMock(_db_supports_row_number=True) + instance_mock = MagicMock() execute_args = [] close_mock = MagicMock() @@ -353,7 +340,7 @@ def test_fail_outdated_mysql(caplog, mysql_version, message): ) 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) + instance_mock = MagicMock() execute_args = [] close_mock = MagicMock() @@ -396,7 +383,7 @@ def test_supported_mysql(caplog, mysql_version): ) def test_fail_outdated_pgsql(caplog, pgsql_version, message): """Test setting up the connection for an outdated PostgreSQL version.""" - instance_mock = MagicMock(_db_supports_row_number=True) + instance_mock = MagicMock() execute_args = [] close_mock = MagicMock() @@ -429,7 +416,7 @@ def test_fail_outdated_pgsql(caplog, pgsql_version, message): ) 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) + instance_mock = MagicMock() execute_args = [] close_mock = MagicMock() @@ -474,7 +461,7 @@ def test_supported_pgsql(caplog, pgsql_version): ) def test_fail_outdated_sqlite(caplog, sqlite_version, message): """Test setting up the connection for an outdated sqlite version.""" - instance_mock = MagicMock(_db_supports_row_number=True) + instance_mock = MagicMock() execute_args = [] close_mock = MagicMock() @@ -510,7 +497,7 @@ def test_fail_outdated_sqlite(caplog, sqlite_version, message): ) 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) + instance_mock = MagicMock() execute_args = [] close_mock = MagicMock() diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 9165fbc4354..66ed0032201 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -2279,13 +2279,7 @@ def test_compile_hourly_statistics_changing_statistics( assert "Error while processing event StatisticsTask" not in caplog.text -@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_daily_monthly_summary( - hass_recorder, caplog, db_supports_row_number, in_log, not_in_log -): +def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): """Test compiling hourly statistics + monthly and daily summary.""" zero = dt_util.utcnow() # August 31st, 23:00 local time @@ -2299,7 +2293,6 @@ def test_compile_statistics_hourly_daily_monthly_summary( # Remove this after dropping the use of the hass_recorder fixture hass.config.set_time_zone("America/Regina") recorder = hass.data[DATA_INSTANCE] - recorder._db_supports_row_number = db_supports_row_number setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added attributes = { @@ -2693,10 +2686,6 @@ def test_compile_statistics_hourly_daily_monthly_summary( 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 a6402697bb888cc61724d7ff8aa4379f14cff5a4 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 18 May 2022 21:02:30 -0700 Subject: [PATCH 0668/3516] Add display name for application credentials (#72053) * Add display name for application credentials * Rename display name to name * Improve test coverage for importing a named credential * Add a default credential name on import --- .../application_credentials/__init__.py | 19 ++++- .../application_credentials/test_init.py | 84 ++++++++++++++++++- 2 files changed, 98 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/application_credentials/__init__.py b/homeassistant/components/application_credentials/__init__.py index 9117a91c33d..1a128c5c378 100644 --- a/homeassistant/components/application_credentials/__init__.py +++ b/homeassistant/components/application_credentials/__init__.py @@ -15,7 +15,13 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.components.websocket_api.connection import ActiveConnection -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DOMAIN, CONF_ID +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_DOMAIN, + CONF_ID, + CONF_NAME, +) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, config_entry_oauth2_flow @@ -39,12 +45,14 @@ STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 DATA_STORAGE = "storage" CONF_AUTH_DOMAIN = "auth_domain" +DEFAULT_IMPORT_NAME = "Import from configuration.yaml" CREATE_FIELDS = { vol.Required(CONF_DOMAIN): cv.string, vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_CLIENT_SECRET): cv.string, vol.Optional(CONF_AUTH_DOMAIN): cv.string, + vol.Optional(CONF_NAME): cv.string, } UPDATE_FIELDS: dict = {} # Not supported @@ -55,6 +63,7 @@ class ClientCredential: client_id: str client_secret: str + name: str | None = None @dataclass @@ -122,7 +131,9 @@ class ApplicationCredentialsStorageCollection(collection.StorageCollection): item[CONF_AUTH_DOMAIN] if CONF_AUTH_DOMAIN in item else item[CONF_ID] ) credentials[auth_domain] = ClientCredential( - item[CONF_CLIENT_ID], item[CONF_CLIENT_SECRET] + client_id=item[CONF_CLIENT_ID], + client_secret=item[CONF_CLIENT_SECRET], + name=item.get(CONF_NAME), ) return credentials @@ -169,6 +180,7 @@ async def async_import_client_credential( CONF_CLIENT_SECRET: credential.client_secret, CONF_AUTH_DOMAIN: auth_domain if auth_domain else domain, } + item[CONF_NAME] = credential.name if credential.name else DEFAULT_IMPORT_NAME await storage_collection.async_import_item(item) @@ -191,11 +203,12 @@ class AuthImplementation(config_entry_oauth2_flow.LocalOAuth2Implementation): authorization_server.authorize_url, authorization_server.token_url, ) + self._name = credential.name @property def name(self) -> str: """Name of the implementation.""" - return self.client_id + return self._name or self.client_id async def _async_provide_implementation( diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py index b62f8a0139c..b89a60f42e4 100644 --- a/tests/components/application_credentials/test_init.py +++ b/tests/components/application_credentials/test_init.py @@ -13,13 +13,19 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components.application_credentials import ( CONF_AUTH_DOMAIN, + DEFAULT_IMPORT_NAME, DOMAIN, AuthImplementation, AuthorizationServer, ClientCredential, async_import_client_credential, ) -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_DOMAIN +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_DOMAIN, + CONF_NAME, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.setup import async_setup_component @@ -29,11 +35,13 @@ from tests.common import mock_platform CLIENT_ID = "some-client-id" CLIENT_SECRET = "some-client-secret" DEVELOPER_CREDENTIAL = ClientCredential(CLIENT_ID, CLIENT_SECRET) +NAMED_CREDENTIAL = ClientCredential(CLIENT_ID, CLIENT_SECRET, "Name") ID = "fake_integration_some_client_id" AUTHORIZE_URL = "https://example.com/auth" TOKEN_URL = "https://example.com/oauth2/v4/token" REFRESH_TOKEN = "mock-refresh-token" ACCESS_TOKEN = "mock-access-token" +NAME = "Name" TEST_DOMAIN = "fake_integration" @@ -118,6 +126,7 @@ class OAuthFixture: self.hass_client = hass_client self.aioclient_mock = aioclient_mock self.client_id = CLIENT_ID + self.title = CLIENT_ID async def complete_external_step( self, result: data_entry_flow.FlowResult @@ -152,7 +161,7 @@ class OAuthFixture: result = await self.hass.config_entries.flow.async_configure(result["flow_id"]) assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result.get("title") == self.client_id + assert result.get("title") == self.title assert "data" in result assert "token" in result["data"] return result @@ -348,6 +357,7 @@ async def test_websocket_import_config( CONF_CLIENT_SECRET: CLIENT_SECRET, "id": ID, CONF_AUTH_DOMAIN: TEST_DOMAIN, + CONF_NAME: DEFAULT_IMPORT_NAME, } ] @@ -375,6 +385,29 @@ async def test_import_duplicate_credentials( CONF_CLIENT_SECRET: CLIENT_SECRET, "id": ID, CONF_AUTH_DOMAIN: TEST_DOMAIN, + CONF_NAME: DEFAULT_IMPORT_NAME, + } + ] + + +@pytest.mark.parametrize("config_credential", [NAMED_CREDENTIAL]) +async def test_import_named_credential( + ws_client: ClientFixture, + config_credential: ClientCredential, + import_config_credential: Any, +): + """Test websocket list command for an imported credential.""" + client = await ws_client() + + # Imported creds returned from websocket + assert await client.cmd_result("list") == [ + { + CONF_DOMAIN: TEST_DOMAIN, + CONF_CLIENT_ID: CLIENT_ID, + CONF_CLIENT_SECRET: CLIENT_SECRET, + "id": ID, + CONF_AUTH_DOMAIN: TEST_DOMAIN, + CONF_NAME: NAME, } ] @@ -487,6 +520,7 @@ async def test_config_flow_multiple_entries( ) assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP oauth_fixture.client_id = CLIENT_ID + "2" + oauth_fixture.title = CLIENT_ID + "2" result = await oauth_fixture.complete_external_step(result) assert ( result["data"].get("auth_implementation") == "fake_integration_some_client_id2" @@ -532,6 +566,7 @@ async def test_config_flow_with_config_credential( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + oauth_fixture.title = DEFAULT_IMPORT_NAME result = await oauth_fixture.complete_external_step(result) # Uses the imported auth domain for compatibility assert result["data"].get("auth_implementation") == TEST_DOMAIN @@ -653,6 +688,7 @@ async def test_platform_with_auth_implementation( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + oauth_fixture.title = DEFAULT_IMPORT_NAME result = await oauth_fixture.complete_external_step(result) # Uses the imported auth domain for compatibility assert result["data"].get("auth_implementation") == TEST_DOMAIN @@ -667,3 +703,47 @@ async def test_websocket_integration_list(ws_client: ClientFixture): assert await client.cmd_result("config") == { "domains": ["example1", "example2"] } + + +async def test_name( + hass: HomeAssistant, ws_client: ClientFixture, oauth_fixture: OAuthFixture +): + """Test a credential with a name set.""" + client = await ws_client() + result = await client.cmd_result( + "create", + { + CONF_DOMAIN: TEST_DOMAIN, + CONF_CLIENT_ID: CLIENT_ID, + CONF_CLIENT_SECRET: CLIENT_SECRET, + CONF_NAME: NAME, + }, + ) + assert result == { + CONF_DOMAIN: TEST_DOMAIN, + CONF_CLIENT_ID: CLIENT_ID, + CONF_CLIENT_SECRET: CLIENT_SECRET, + CONF_NAME: NAME, + "id": ID, + } + + result = await client.cmd_result("list") + assert result == [ + { + CONF_DOMAIN: TEST_DOMAIN, + CONF_CLIENT_ID: CLIENT_ID, + CONF_CLIENT_SECRET: CLIENT_SECRET, + CONF_NAME: NAME, + "id": ID, + } + ] + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + oauth_fixture.title = NAME + result = await oauth_fixture.complete_external_step(result) + assert ( + result["data"].get("auth_implementation") == "fake_integration_some_client_id" + ) From 272e65f56db7affe84958d679ff3288a5b148d1f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 May 2022 23:27:31 -0500 Subject: [PATCH 0669/3516] Fix device_ids being filtered when entities also specified in the logbook (#72122) --- homeassistant/components/logbook/__init__.py | 5 ++--- tests/components/logbook/test_init.py | 9 ++++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 806ba00d2c8..e2073800c25 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -59,7 +59,6 @@ from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, EntityFilter, convert_include_exclude_filter, - generate_filter, ) from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, @@ -579,8 +578,8 @@ def _get_events( ] = hass.data.get(DOMAIN, {}) format_time = _row_time_fired_timestamp if timestamp else _row_time_fired_isoformat entity_name_cache = EntityNameCache(hass) - if entity_ids is not None: - entities_filter = generate_filter([], entity_ids, [], []) + if entity_ids or device_ids: + entities_filter = None def yield_rows(query: Query) -> Generator[Row, None, None]: """Yield rows from the database.""" diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 7657ebf2b83..a7420df9c5d 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -2546,10 +2546,13 @@ async def test_get_events_with_device_ids(hass, hass_ws_client, recorder_mock): assert response["id"] == 2 results = response["result"] - assert results[0]["entity_id"] == "light.kitchen" - assert results[0]["state"] == "on" + assert results[0]["domain"] == "test" + assert results[0]["message"] == "is on fire" + assert results[0]["name"] == "device name" assert results[1]["entity_id"] == "light.kitchen" - assert results[1]["state"] == "off" + assert results[1]["state"] == "on" + assert results[2]["entity_id"] == "light.kitchen" + assert results[2]["state"] == "off" await client.send_json( { From c4f6fcc3d2e4872554d8c75ebfc28b315206f90a Mon Sep 17 00:00:00 2001 From: Adam James Date: Thu, 19 May 2022 07:28:55 +0100 Subject: [PATCH 0670/3516] Increase range of valid source IDs in nad (#72086) nad: Increase max source ID to 12 Tested on a NAD C658 with an MDC HDM-2 card installed. --- homeassistant/components/nad/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nad/media_player.py b/homeassistant/components/nad/media_player.py index 2bcd7958de9..f031175a321 100644 --- a/homeassistant/components/nad/media_player.py +++ b/homeassistant/components/nad/media_player.py @@ -45,7 +45,8 @@ CONF_MAX_VOLUME = "max_volume" CONF_VOLUME_STEP = "volume_step" # for NADReceiverTCP CONF_SOURCE_DICT = "sources" # for NADReceiver -SOURCE_DICT_SCHEMA = vol.Schema({vol.Range(min=1, max=10): cv.string}) +# Max value based on a C658 with an MDC HDM-2 card installed +SOURCE_DICT_SCHEMA = vol.Schema({vol.Range(min=1, max=12): cv.string}) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { From be809980a119bdd8c4a9bfeddb276e6b4f683a79 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 May 2022 08:57:02 +0200 Subject: [PATCH 0671/3516] Adjust device_automation type hints in deconz (#72129) --- homeassistant/components/deconz/device_trigger.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index c92ad7f46dc..43ed3b2cdc4 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -703,7 +703,7 @@ async def async_attach_trigger( async def async_get_triggers( hass: HomeAssistant, device_id: str, -) -> list | None: +) -> list[dict[str, str]]: """List device triggers. Make sure device is a supported remote model. @@ -714,7 +714,7 @@ async def async_get_triggers( device = device_registry.devices[device_id] if device.model not in REMOTES: - return None + return [] triggers = [] for trigger, subtype in REMOTES[device.model].keys(): From 7e2f5968ccc5095a0d1707680a0f32d4bf23d3f2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 May 2022 09:12:54 +0200 Subject: [PATCH 0672/3516] Adjust device_automation type hints in shelly (#72139) --- homeassistant/components/shelly/device_trigger.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py index 3e839507127..c7d0f92336c 100644 --- a/homeassistant/components/shelly/device_trigger.py +++ b/homeassistant/components/shelly/device_trigger.py @@ -108,9 +108,9 @@ async def async_validate_trigger_config( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for Shelly devices.""" - triggers: list[dict[str, Any]] = [] + triggers: list[dict[str, str]] = [] if rpc_wrapper := get_rpc_device_wrapper(hass, device_id): input_triggers = get_rpc_input_triggers(rpc_wrapper.device) From 694dc97ac555d51c1769e7a3361038c9f7742b5a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 May 2022 09:15:41 +0200 Subject: [PATCH 0673/3516] Adjust device_automation type hints in tasmota (#72140) --- homeassistant/components/tasmota/device_trigger.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py index aca5a2848e3..512fa5dd547 100644 --- a/homeassistant/components/tasmota/device_trigger.py +++ b/homeassistant/components/tasmota/device_trigger.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Callable import logging -from typing import Any import attr from hatasmota.models import DiscoveryHashType @@ -265,7 +264,7 @@ async def async_remove_triggers(hass: HomeAssistant, device_id: str) -> None: async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for a Tasmota device.""" triggers: list[dict[str, str]] = [] From 453c6af5956ee5c6e296cc69bc17613ee5f78804 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 May 2022 09:16:46 +0200 Subject: [PATCH 0674/3516] Adjust device_automation type hints in arcam_fmj (#72128) --- homeassistant/components/arcam_fmj/device_trigger.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/arcam_fmj/device_trigger.py b/homeassistant/components/arcam_fmj/device_trigger.py index d1fca811fcc..593250e4983 100644 --- a/homeassistant/components/arcam_fmj/device_trigger.py +++ b/homeassistant/components/arcam_fmj/device_trigger.py @@ -1,8 +1,6 @@ """Provides device automations for Arcam FMJ Receiver control.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -35,7 +33,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for Arcam FMJ Receiver control devices.""" registry = entity_registry.async_get(hass) triggers = [] From f144b518b21a86a64445d19b9b8b87bd64b56ed6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 May 2022 09:18:01 +0200 Subject: [PATCH 0675/3516] Adjust device_automation type hints in kodi (#72131) --- homeassistant/components/kodi/device_trigger.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/kodi/device_trigger.py b/homeassistant/components/kodi/device_trigger.py index e4fe6cfa103..11d0b1567f9 100644 --- a/homeassistant/components/kodi/device_trigger.py +++ b/homeassistant/components/kodi/device_trigger.py @@ -1,8 +1,6 @@ """Provides device automations for Kodi.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -36,7 +34,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for Kodi devices.""" registry = entity_registry.async_get(hass) triggers = [] From 1d6e40451274cbe8ccc6be256146193bcfe97df5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 May 2022 09:19:43 +0200 Subject: [PATCH 0676/3516] Adjust device_automation type hints in homekit_controller (#72130) --- homeassistant/components/homekit_controller/device_trigger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index aa2765d9be5..3ba2abe3339 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -240,7 +240,7 @@ def async_fire_triggers(conn: HKDevice, events: dict[tuple[int, int], Any]): async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for homekit devices.""" if device_id not in hass.data.get(TRIGGERS, {}): From 9be7b02613ed22e8adfd139e5493e471c24a70a1 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 19 May 2022 10:36:22 +0200 Subject: [PATCH 0677/3516] Fix Motion Blinds checking interface for multiple gateways (#71474) * fix checking interface for multiple gateways * fix black * setup lock for shared multicast listener * fix black * bump motionblinds to 0.6.7 * compensate for extra Lock_key * unregister gateway when unloading * unsubscribe stop listener * fix black * only unsubscribe listener on last gateway remove * catch OSError for invalid interfaces * test coverage * make stop listen on last config entry more robust * also check ConfigEntryState * fix black --- .../components/motion_blinds/__init__.py | 72 ++++++++++++------- .../components/motion_blinds/const.py | 2 + .../components/motion_blinds/gateway.py | 2 + .../motion_blinds/test_config_flow.py | 12 ++-- 4 files changed, 58 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index 63c80d33dba..95ac1c5fd44 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING from motionblinds import DEVICE_TYPES_WIFI, AsyncMotionMulticast, ParseException -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -25,6 +25,8 @@ from .const import ( KEY_COORDINATOR, KEY_GATEWAY, KEY_MULTICAST_LISTENER, + KEY_SETUP_LOCK, + KEY_UNSUB_STOP, KEY_VERSION, MANUFACTURER, PLATFORMS, @@ -106,6 +108,7 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the motion_blinds components from a config entry.""" hass.data.setdefault(DOMAIN, {}) + setup_lock = hass.data[DOMAIN].setdefault(KEY_SETUP_LOCK, asyncio.Lock()) host = entry.data[CONF_HOST] key = entry.data[CONF_API_KEY] multicast_interface = entry.data.get(CONF_INTERFACE, DEFAULT_INTERFACE) @@ -113,33 +116,41 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(update_listener)) - # check multicast interface - check_multicast_class = ConnectMotionGateway(hass, interface=multicast_interface) - working_interface = await check_multicast_class.async_check_interface(host, key) - if working_interface != multicast_interface: - data = {**entry.data, CONF_INTERFACE: working_interface} - hass.config_entries.async_update_entry(entry, data=data) - _LOGGER.debug( - "Motion Blinds interface updated from %s to %s, " - "this should only occur after a network change", - multicast_interface, - working_interface, - ) - # Create multicast Listener - if KEY_MULTICAST_LISTENER not in hass.data[DOMAIN]: - multicast = AsyncMotionMulticast(interface=working_interface) - hass.data[DOMAIN][KEY_MULTICAST_LISTENER] = multicast - # start listening for local pushes (only once) - await multicast.Start_listen() + async with setup_lock: + if KEY_MULTICAST_LISTENER not in hass.data[DOMAIN]: + # check multicast interface + check_multicast_class = ConnectMotionGateway( + hass, interface=multicast_interface + ) + working_interface = await check_multicast_class.async_check_interface( + host, key + ) + if working_interface != multicast_interface: + data = {**entry.data, CONF_INTERFACE: working_interface} + hass.config_entries.async_update_entry(entry, data=data) + _LOGGER.debug( + "Motion Blinds interface updated from %s to %s, " + "this should only occur after a network change", + multicast_interface, + working_interface, + ) - # register stop callback to shutdown listening for local pushes - def stop_motion_multicast(event): - """Stop multicast thread.""" - _LOGGER.debug("Shutting down Motion Listener") - multicast.Stop_listen() + multicast = AsyncMotionMulticast(interface=working_interface) + hass.data[DOMAIN][KEY_MULTICAST_LISTENER] = multicast + # start listening for local pushes (only once) + await multicast.Start_listen() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_motion_multicast) + # register stop callback to shutdown listening for local pushes + def stop_motion_multicast(event): + """Stop multicast thread.""" + _LOGGER.debug("Shutting down Motion Listener") + multicast.Stop_listen() + + unsub = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, stop_motion_multicast + ) + hass.data[DOMAIN][KEY_UNSUB_STOP] = unsub # Connect to motion gateway multicast = hass.data[DOMAIN][KEY_MULTICAST_LISTENER] @@ -205,10 +216,19 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> ) if unload_ok: + multicast = hass.data[DOMAIN][KEY_MULTICAST_LISTENER] + multicast.Unregister_motion_gateway(config_entry.data[CONF_HOST]) hass.data[DOMAIN].pop(config_entry.entry_id) - if len(hass.data[DOMAIN]) == 1: + loaded_entries = [ + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + if len(loaded_entries) == 1: # No motion gateways left, stop Motion multicast + unsub_stop = hass.data[DOMAIN].pop(KEY_UNSUB_STOP) + unsub_stop() _LOGGER.debug("Shutting down Motion Listener") multicast = hass.data[DOMAIN].pop(KEY_MULTICAST_LISTENER) multicast.Stop_listen() diff --git a/homeassistant/components/motion_blinds/const.py b/homeassistant/components/motion_blinds/const.py index a35aeb6cd89..332a30a5e5f 100644 --- a/homeassistant/components/motion_blinds/const.py +++ b/homeassistant/components/motion_blinds/const.py @@ -16,6 +16,8 @@ KEY_GATEWAY = "gateway" KEY_API_LOCK = "api_lock" KEY_COORDINATOR = "coordinator" KEY_MULTICAST_LISTENER = "multicast_listener" +KEY_SETUP_LOCK = "setup_lock" +KEY_UNSUB_STOP = "unsub_stop" KEY_VERSION = "version" ATTR_WIDTH = "width" diff --git a/homeassistant/components/motion_blinds/gateway.py b/homeassistant/components/motion_blinds/gateway.py index 1775f7deb7d..218da9f625c 100644 --- a/homeassistant/components/motion_blinds/gateway.py +++ b/homeassistant/components/motion_blinds/gateway.py @@ -101,6 +101,8 @@ class ConnectMotionGateway: await check_multicast.Start_listen() except socket.gaierror: continue + except OSError: + continue # trigger test multicast self._gateway_device = MotionGateway( diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index 57ad3c20779..57ab45d9dbb 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -336,10 +336,14 @@ async def test_dhcp_flow(hass): 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}, - ) + with patch( + "homeassistant.components.motion_blinds.gateway.AsyncMotionMulticast.Start_listen", + side_effect=OSError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: TEST_API_KEY}, + ) assert result["type"] == "create_entry" assert result["title"] == DEFAULT_GATEWAY_NAME From bad245a856088c09c0159f76b2fa3790c3793f0c Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 19 May 2022 12:06:57 +0300 Subject: [PATCH 0678/3516] Fix Shelly triggers type hints (#72146) --- homeassistant/components/shelly/device_trigger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py index c7d0f92336c..ae2eb7d6440 100644 --- a/homeassistant/components/shelly/device_trigger.py +++ b/homeassistant/components/shelly/device_trigger.py @@ -54,7 +54,7 @@ TRIGGER_SCHEMA: Final = DEVICE_TRIGGER_BASE_SCHEMA.extend( def append_input_triggers( - triggers: list[dict[str, Any]], + triggers: list[dict[str, str]], input_triggers: list[tuple[str, str]], device_id: str, ) -> None: From cfe9ea033ac9b75834cddf0e0f92177e05743532 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 19 May 2022 11:14:07 +0200 Subject: [PATCH 0679/3516] Split miio gateway coordinator (#69755) --- .../components/xiaomi_miio/__init__.py | 43 +++++++++---------- .../components/xiaomi_miio/gateway.py | 2 +- homeassistant/components/xiaomi_miio/light.py | 4 +- .../components/xiaomi_miio/sensor.py | 4 +- .../components/xiaomi_miio/switch.py | 4 +- 5 files changed, 31 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 68d3fe04653..e2dc36ff568 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -405,36 +405,35 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> hw_version=gateway_info.hardware_version, ) - def update_data(): - """Fetch data from the subdevice.""" - data = {} - for sub_device in gateway.gateway_device.devices.values(): + def update_data_factory(sub_device): + """Create update function for a subdevice.""" + + async def async_update_data(): + """Fetch data from the subdevice.""" try: - sub_device.update() + await hass.async_add_executor_job(sub_device.update) except GatewayException as ex: _LOGGER.error("Got exception while fetching the state: %s", ex) - data[sub_device.sid] = {ATTR_AVAILABLE: False} - else: - data[sub_device.sid] = {ATTR_AVAILABLE: True} - return data + return {ATTR_AVAILABLE: False} + return {ATTR_AVAILABLE: True} - async def async_update_data(): - """Fetch data from the subdevice using async_add_executor_job.""" - return await hass.async_add_executor_job(update_data) + return async_update_data - # Create update coordinator - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name=name, - update_method=async_update_data, - # Polling interval. Will only be polled if there are subscribers. - update_interval=UPDATE_INTERVAL, - ) + coordinator_dict = {} + for sub_device in gateway.gateway_device.devices.values(): + # Create update coordinator + coordinator_dict[sub_device.sid] = DataUpdateCoordinator( + hass, + _LOGGER, + name=name, + update_method=update_data_factory(sub_device), + # Polling interval. Will only be polled if there are subscribers. + update_interval=UPDATE_INTERVAL, + ) hass.data[DOMAIN][entry.entry_id] = { CONF_GATEWAY: gateway.gateway_device, - KEY_COORDINATOR: coordinator, + KEY_COORDINATOR: coordinator_dict, } for platform in GATEWAY_PLATFORMS: diff --git a/homeassistant/components/xiaomi_miio/gateway.py b/homeassistant/components/xiaomi_miio/gateway.py index ebfb37ab8fb..6d0984a9aeb 100644 --- a/homeassistant/components/xiaomi_miio/gateway.py +++ b/homeassistant/components/xiaomi_miio/gateway.py @@ -169,4 +169,4 @@ class XiaomiGatewayDevice(CoordinatorEntity, Entity): if self.coordinator.data is None: return False - return self.coordinator.data[self._sub_device.sid][ATTR_AVAILABLE] + return self.coordinator.data[ATTR_AVAILABLE] diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index acb726e7c05..e97c6e76503 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -132,9 +132,11 @@ async def async_setup_entry( ) # Gateway sub devices sub_devices = gateway.devices - coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] for sub_device in sub_devices.values(): if sub_device.device_type == "LightBulb": + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR][ + sub_device.sid + ] entities.append( XiaomiGatewayBulb(coordinator, sub_device, config_entry) ) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 0beac2c0041..df0c953d62a 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -681,8 +681,10 @@ async def async_setup_entry( ) # Gateway sub devices sub_devices = gateway.devices - coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] for sub_device in sub_devices.values(): + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR][ + sub_device.sid + ] for sensor, description in SENSOR_TYPES.items(): if sensor not in sub_device.status: continue diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 968abceb57c..05d6543e93d 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -369,10 +369,12 @@ async def async_setup_other_entry(hass, config_entry, async_add_entities): gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY] # Gateway sub devices sub_devices = gateway.devices - coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] for sub_device in sub_devices.values(): if sub_device.device_type != "Switch": continue + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR][ + sub_device.sid + ] switch_variables = set(sub_device.status) & set(GATEWAY_SWITCH_VARS) if switch_variables: entities.extend( From f7b96c87d02eb520bbdc9e30ff0c89b137d6b663 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 19 May 2022 13:06:56 +0200 Subject: [PATCH 0680/3516] Netgear test coverage (#72150) --- .../components/netgear/config_flow.py | 10 +++---- tests/components/netgear/test_config_flow.py | 27 +++++++++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/netgear/config_flow.py b/homeassistant/components/netgear/config_flow.py index 79053c712fc..9024d0510dd 100644 --- a/homeassistant/components/netgear/config_flow.py +++ b/homeassistant/components/netgear/config_flow.py @@ -191,11 +191,6 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if errors: return await self._show_setup_form(user_input, errors) - # Check if already configured - info = await self.hass.async_add_executor_job(api.get_info) - await self.async_set_unique_id(info["SerialNumber"], raise_on_progress=False) - self._abort_if_unique_id_configured() - config_data = { CONF_USERNAME: username, CONF_PASSWORD: password, @@ -204,6 +199,11 @@ class NetgearFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_SSL: api.ssl, } + # Check if already configured + info = await self.hass.async_add_executor_job(api.get_info) + await self.async_set_unique_id(info["SerialNumber"], raise_on_progress=False) + self._abort_if_unique_id_configured(updates=config_data) + if info.get("ModelName") is not None and info.get("DeviceName") is not None: name = f"{info['ModelName']} - {info['DeviceName']}" else: diff --git a/tests/components/netgear/test_config_flow.py b/tests/components/netgear/test_config_flow.py index 33c634e250a..d46284f5049 100644 --- a/tests/components/netgear/test_config_flow.py +++ b/tests/components/netgear/test_config_flow.py @@ -59,6 +59,7 @@ SSL = False USERNAME = "Home_Assistant" PASSWORD = "password" SSDP_URL = f"http://{HOST}:{PORT}/rootDesc.xml" +SSDP_URLipv6 = f"http://[::ffff:a00:1]:{PORT}/rootDesc.xml" SSDP_URL_SLL = f"https://{HOST}:{PORT}/rootDesc.xml" @@ -234,6 +235,32 @@ async def test_ssdp_already_configured(hass): assert result["reason"] == "already_configured" +async def test_ssdp_ipv6(hass): + """Test ssdp abort when using a ipv6 address.""" + MockConfigEntry( + domain=DOMAIN, + data={CONF_PASSWORD: PASSWORD}, + unique_id=SERIAL, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_SSDP}, + data=ssdp.SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location=SSDP_URLipv6, + upnp={ + ssdp.ATTR_UPNP_MODEL_NUMBER: "RBR20", + ssdp.ATTR_UPNP_PRESENTATION_URL: URL, + ssdp.ATTR_UPNP_SERIAL: SERIAL, + }, + ), + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "not_ipv4_address" + + async def test_ssdp(hass, service): """Test ssdp step.""" result = await hass.config_entries.flow.async_init( From 9d377aabdbb0a5e3c84adffd5043c2717ac68011 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 19 May 2022 14:11:29 +0200 Subject: [PATCH 0681/3516] Fix Google tests (#72158) --- tests/components/google/test_config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index 9339ca9988f..88a090773bc 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -99,7 +99,7 @@ async def test_full_flow_yaml_creds( ) assert result.get("type") == "create_entry" - assert result.get("title") == "client-id" + assert result.get("title") == "Import from configuration.yaml" assert "data" in result data = result["data"] assert "token" in data @@ -161,7 +161,7 @@ async def test_full_flow_application_creds( ) assert result.get("type") == "create_entry" - assert result.get("title") == "client-id" + assert result.get("title") == "Import from configuration.yaml" assert "data" in result data = result["data"] assert "token" in data @@ -278,7 +278,7 @@ async def test_exchange_error( ) assert result.get("type") == "create_entry" - assert result.get("title") == "client-id" + assert result.get("title") == "Import from configuration.yaml" assert "data" in result data = result["data"] assert "token" in data From ed1c2ea2b8e4d2368c0b18a27c4e54c67f11a9e9 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 19 May 2022 15:04:53 +0200 Subject: [PATCH 0682/3516] Move manual configuration of MQTT fan and light to the integration key (#71676) * Processing yaml config through entry setup * Setup all platforms * Update homeassistant/components/mqtt/__init__.py Co-authored-by: Martin Hjelmare * adjust mock_mqtt - reference config from cache * Fix test config entry override * Add tests yaml setup * additional tests * Introduce PLATFORM_SCHEMA_MODERN * recover temporary MQTT_BASE_PLATFORM_SCHEMA * Allow extra key in light base schema, restore test * Fix test for exception on platform key * One deprecation message per platform * Remove deprecation checks from modern schema * Update homeassistant/components/mqtt/fan.py Co-authored-by: Erik Montnemery * Update homeassistant/components/mqtt/fan.py Co-authored-by: Erik Montnemery * Update homeassistant/components/mqtt/light/__init__.py Co-authored-by: Erik Montnemery * Update homeassistant/components/mqtt/light/__init__.py Co-authored-by: Erik Montnemery * Update homeassistant/components/mqtt/light/schema_json.py Co-authored-by: Erik Montnemery * Update homeassistant/components/mqtt/light/schema_template.py Co-authored-by: Erik Montnemery * Update homeassistant/components/mqtt/mixins.py Co-authored-by: Erik Montnemery * rename validate_modern_schema * Do not fail platform if a single config is broken * Update homeassistant/components/mqtt/__init__.py Co-authored-by: Erik Montnemery * Fix tests on asserting log * Update log. Make helper transparant, remove patch * Perform parallel processing * Update tests/components/mqtt/test_init.py Co-authored-by: Martin Hjelmare * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Update homeassistant/components/mqtt/mixins.py Co-authored-by: Martin Hjelmare * black * Fix tests and add #new_format anchor Co-authored-by: Martin Hjelmare Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 47 ++++++++++-- homeassistant/components/mqtt/const.py | 2 + homeassistant/components/mqtt/discovery.py | 6 +- homeassistant/components/mqtt/fan.py | 28 +++++++- .../components/mqtt/light/__init__.py | 57 +++++++++++++-- .../components/mqtt/light/schema_basic.py | 7 +- .../components/mqtt/light/schema_json.py | 10 ++- .../components/mqtt/light/schema_template.py | 7 +- homeassistant/components/mqtt/mixins.py | 52 ++++++++++++++ tests/components/mqtt/test_common.py | 21 ++++++ tests/components/mqtt/test_fan.py | 13 ++++ tests/components/mqtt/test_init.py | 71 ++++++++++++++++++- tests/components/mqtt/test_light.py | 13 ++++ tests/components/mqtt/test_light_json.py | 13 ++++ tests/components/mqtt/test_light_template.py | 13 ++++ tests/conftest.py | 7 +- 16 files changed, 339 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 162e344f852..e17a31480b1 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -81,6 +81,8 @@ from .const import ( CONF_TLS_VERSION, CONF_TOPIC, CONF_WILL_MESSAGE, + CONFIG_ENTRY_IS_SETUP, + DATA_CONFIG_ENTRY_LOCK, DATA_MQTT_CONFIG, DATA_MQTT_RELOAD_NEEDED, DEFAULT_BIRTH, @@ -171,7 +173,6 @@ PLATFORMS = [ Platform.VACUUM, ] - CLIENT_KEY_AUTH_MSG = ( "client_key and client_cert must both be present in " "the MQTT broker configuration" @@ -187,7 +188,14 @@ MQTT_WILL_BIRTH_SCHEMA = vol.Schema( required=True, ) -CONFIG_SCHEMA_BASE = vol.Schema( +PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( + { + vol.Optional(Platform.FAN.value): cv.ensure_list, + vol.Optional(Platform.LIGHT.value): cv.ensure_list, + } +) + +CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend( { vol.Optional(CONF_CLIENT_ID): cv.string, vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( @@ -253,10 +261,28 @@ SCHEMA_BASE = { vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, } +MQTT_BASE_SCHEMA = vol.Schema(SCHEMA_BASE) + +# Will be removed when all platforms support a modern platform schema MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE) +# Will be removed when all platforms support a modern platform schema +MQTT_RO_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + } +) +# Will be removed when all platforms support a modern platform schema +MQTT_RW_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, + } +) # Sensor type platforms subscribe to MQTT events -MQTT_RO_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend( +MQTT_RO_SCHEMA = MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, @@ -264,7 +290,7 @@ MQTT_RO_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend( ) # Switch type platforms publish to MQTT and may subscribe -MQTT_RW_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend( +MQTT_RW_SCHEMA = MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, @@ -774,6 +800,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ), ) + # setup platforms and discovery + hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() + hass.data[CONFIG_ENTRY_IS_SETUP] = set() + + async with hass.data[DATA_CONFIG_ENTRY_LOCK]: + for component in PLATFORMS: + config_entries_key = f"{component}.mqtt" + if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: + hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + if conf.get(CONF_DISCOVERY): await _async_setup_discovery(hass, conf, entry) diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 69865733763..106d0310158 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -28,6 +28,8 @@ CONF_CLIENT_CERT = "client_cert" CONF_TLS_INSECURE = "tls_insecure" CONF_TLS_VERSION = "tls_version" +CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" +DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" DATA_MQTT_CONFIG = "mqtt_config" DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index fae443dc411..8685c790fd2 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -27,6 +27,8 @@ from .const import ( ATTR_DISCOVERY_TOPIC, CONF_AVAILABILITY, CONF_TOPIC, + CONFIG_ENTRY_IS_SETUP, + DATA_CONFIG_ENTRY_LOCK, DOMAIN, ) @@ -62,8 +64,6 @@ SUPPORTED_COMPONENTS = [ ALREADY_DISCOVERED = "mqtt_discovered_components" PENDING_DISCOVERED = "mqtt_pending_components" -CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" -DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" DATA_CONFIG_FLOW_LOCK = "mqtt_discovery_config_flow_lock" DISCOVERY_UNSUBSCRIBE = "mqtt_discovery_unsubscribe" INTEGRATION_UNSUBSCRIBE = "mqtt_integration_discovery_unsubscribe" @@ -258,9 +258,7 @@ async def async_start( # noqa: C901 hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None ) - hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[DATA_CONFIG_FLOW_LOCK] = asyncio.Lock() - hass.data[CONFIG_ENTRY_IS_SETUP] = set() hass.data[ALREADY_DISCOVERED] = {} hass.data[PENDING_DISCOVERED] = {} diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index ff2eab7a68f..f2b738cd2bb 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -1,6 +1,7 @@ """Support for MQTT fans.""" from __future__ import annotations +import asyncio import functools import logging import math @@ -49,8 +50,10 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_get_platform_config_from_yaml, async_setup_entry_helper, async_setup_platform_helper, + warn_for_legacy_schema, ) CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic" @@ -122,7 +125,7 @@ def valid_preset_mode_configuration(config): return config -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, @@ -172,7 +175,15 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) +# Configuring MQTT Fans under the fan platform key is deprecated in HA Core 2022.6 PLATFORM_SCHEMA = vol.All( + cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), + valid_speed_range_configuration, + valid_preset_mode_configuration, + warn_for_legacy_schema(fan.DOMAIN), +) + +PLATFORM_SCHEMA_MODERN = vol.All( _PLATFORM_SCHEMA_BASE, valid_speed_range_configuration, valid_preset_mode_configuration, @@ -201,7 +212,8 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up MQTT fan through configuration.yaml.""" + """Set up MQTT fans configured under the fan platform key (deprecated).""" + # Deprecated in HA Core 2022.6 await async_setup_platform_helper( hass, fan.DOMAIN, config, async_add_entities, _async_setup_entity ) @@ -212,7 +224,17 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT fan dynamically through MQTT discovery.""" + """Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery.""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, fan.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index d78cd5e7baa..ab2a3462615 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -1,36 +1,47 @@ """Support for MQTT lights.""" from __future__ import annotations +import asyncio import functools import voluptuous as vol from homeassistant.components import light +from homeassistant.config_entries import ConfigEntry 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 ..mixins import async_setup_entry_helper, async_setup_platform_helper +from ..mixins import ( + async_get_platform_config_from_yaml, + async_setup_entry_helper, + async_setup_platform_helper, + warn_for_legacy_schema, +) from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import ( DISCOVERY_SCHEMA_BASIC, PLATFORM_SCHEMA_BASIC, + PLATFORM_SCHEMA_MODERN_BASIC, async_setup_entity_basic, ) from .schema_json import ( DISCOVERY_SCHEMA_JSON, PLATFORM_SCHEMA_JSON, + PLATFORM_SCHEMA_MODERN_JSON, async_setup_entity_json, ) from .schema_template import ( DISCOVERY_SCHEMA_TEMPLATE, + PLATFORM_SCHEMA_MODERN_TEMPLATE, PLATFORM_SCHEMA_TEMPLATE, async_setup_entity_template, ) def validate_mqtt_light_discovery(value): - """Validate MQTT light schema.""" + """Validate MQTT light schema for.""" schemas = { "basic": DISCOVERY_SCHEMA_BASIC, "json": DISCOVERY_SCHEMA_JSON, @@ -49,14 +60,31 @@ def validate_mqtt_light(value): return schemas[value[CONF_SCHEMA]](value) +def validate_mqtt_light_modern(value): + """Validate MQTT light schema.""" + schemas = { + "basic": PLATFORM_SCHEMA_MODERN_BASIC, + "json": PLATFORM_SCHEMA_MODERN_JSON, + "template": PLATFORM_SCHEMA_MODERN_TEMPLATE, + } + return schemas[value[CONF_SCHEMA]](value) + + DISCOVERY_SCHEMA = vol.All( MQTT_LIGHT_SCHEMA_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_light_discovery, ) - +# Configuring MQTT Lights under the light platform key is deprecated in HA Core 2022.6 PLATFORM_SCHEMA = vol.All( - MQTT_LIGHT_SCHEMA_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_light + cv.PLATFORM_SCHEMA.extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema, extra=vol.ALLOW_EXTRA), + validate_mqtt_light, + warn_for_legacy_schema(light.DOMAIN), +) + +PLATFORM_SCHEMA_MODERN = vol.All( + MQTT_LIGHT_SCHEMA_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), + validate_mqtt_light_modern, ) @@ -66,14 +94,29 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up MQTT light through configuration.yaml.""" + """Set up MQTT light through configuration.yaml (deprecated).""" + # Deprecated in HA Core 2022.6 await async_setup_platform_helper( hass, light.DOMAIN, config, async_add_entities, _async_setup_entity ) -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up MQTT light dynamically through MQTT discovery.""" +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up MQTT lights configured under the light platform key (deprecated).""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, light.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 497e8186fd0..eb4ec264981 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -156,7 +156,7 @@ VALUE_TEMPLATE_KEYS = [ ] _PLATFORM_SCHEMA_BASE = ( - mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + mqtt.MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_BRIGHTNESS_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic, @@ -220,13 +220,14 @@ _PLATFORM_SCHEMA_BASE = ( .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) +# The use of PLATFORM_SCHEMA is deprecated in HA Core 2022.6 PLATFORM_SCHEMA_BASIC = vol.All( # CONF_WHITE_VALUE_* is deprecated, support will be removed in release 2022.9 cv.deprecated(CONF_WHITE_VALUE_COMMAND_TOPIC), cv.deprecated(CONF_WHITE_VALUE_SCALE), cv.deprecated(CONF_WHITE_VALUE_STATE_TOPIC), cv.deprecated(CONF_WHITE_VALUE_TEMPLATE), - _PLATFORM_SCHEMA_BASE, + cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), ) DISCOVERY_SCHEMA_BASIC = vol.All( @@ -240,6 +241,8 @@ DISCOVERY_SCHEMA_BASIC = vol.All( _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), ) +PLATFORM_SCHEMA_MODERN_BASIC = _PLATFORM_SCHEMA_BASE + async def async_setup_entity_basic( hass, config, async_add_entities, config_entry, discovery_data=None diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index c1e0d7467e0..2049818ab31 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -103,7 +103,7 @@ def valid_color_configuration(config): _PLATFORM_SCHEMA_BASE = ( - mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + mqtt.MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_BRIGHTNESS, default=DEFAULT_BRIGHTNESS): cv.boolean, vol.Optional( @@ -146,10 +146,11 @@ _PLATFORM_SCHEMA_BASE = ( .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) +# Configuring MQTT Lights under the light platform key is deprecated in HA Core 2022.6 PLATFORM_SCHEMA_JSON = vol.All( # CONF_WHITE_VALUE is deprecated, support will be removed in release 2022.9 cv.deprecated(CONF_WHITE_VALUE), - _PLATFORM_SCHEMA_BASE, + cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), valid_color_configuration, ) @@ -160,6 +161,11 @@ DISCOVERY_SCHEMA_JSON = vol.All( valid_color_configuration, ) +PLATFORM_SCHEMA_MODERN_JSON = vol.All( + _PLATFORM_SCHEMA_BASE, + valid_color_configuration, +) + async def async_setup_entity_json( hass, config: ConfigType, async_add_entities, config_entry, discovery_data diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index a98f634642d..0165bfc8efa 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -67,7 +67,7 @@ CONF_RED_TEMPLATE = "red_template" CONF_WHITE_VALUE_TEMPLATE = "white_value_template" _PLATFORM_SCHEMA_BASE = ( - mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + mqtt.MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_BLUE_TEMPLATE): cv.template, vol.Optional(CONF_BRIGHTNESS_TEMPLATE): cv.template, @@ -90,10 +90,11 @@ _PLATFORM_SCHEMA_BASE = ( .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) +# Configuring MQTT Lights under the light platform key is deprecated in HA Core 2022.6 PLATFORM_SCHEMA_TEMPLATE = vol.All( # CONF_WHITE_VALUE_TEMPLATE is deprecated, support will be removed in release 2022.9 cv.deprecated(CONF_WHITE_VALUE_TEMPLATE), - _PLATFORM_SCHEMA_BASE, + cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), ) DISCOVERY_SCHEMA_TEMPLATE = vol.All( @@ -102,6 +103,8 @@ DISCOVERY_SCHEMA_TEMPLATE = vol.All( _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), ) +PLATFORM_SCHEMA_MODERN_TEMPLATE = _PLATFORM_SCHEMA_BASE + async def async_setup_entity_template( hass, config, async_add_entities, config_entry, discovery_data diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 43f75f08459..a46debeae54 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -9,6 +9,7 @@ from typing import Any, Protocol, cast, final import voluptuous as vol +from homeassistant.config import async_log_exception from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CONFIGURATION_URL, @@ -64,6 +65,7 @@ from .const import ( CONF_ENCODING, CONF_QOS, CONF_TOPIC, + DATA_MQTT_CONFIG, DATA_MQTT_RELOAD_NEEDED, DEFAULT_ENCODING, DEFAULT_PAYLOAD_AVAILABLE, @@ -223,6 +225,31 @@ MQTT_ENTITY_COMMON_SCHEMA = MQTT_AVAILABILITY_SCHEMA.extend( ) +def warn_for_legacy_schema(domain: str) -> Callable: + """Warn once when a legacy platform schema is used.""" + warned = set() + + def validator(config: ConfigType) -> ConfigType: + """Return a validator.""" + nonlocal warned + + if domain in warned: + return config + + _LOGGER.warning( + "Manually configured MQTT %s(s) found under platform key '%s', " + "please move to the mqtt integration key, see " + "https://www.home-assistant.io/integrations/%s.mqtt/#new_format", + domain, + domain, + domain, + ) + warned.add(domain) + return config + + return validator + + class SetupEntity(Protocol): """Protocol type for async_setup_entities.""" @@ -237,6 +264,31 @@ class SetupEntity(Protocol): """Define setup_entities type.""" +async def async_get_platform_config_from_yaml( + hass: HomeAssistant, domain: str, schema: vol.Schema +) -> list[ConfigType]: + """Return a list of validated configurations for the domain.""" + + def async_validate_config( + hass: HomeAssistant, + config: list[ConfigType], + ) -> list[ConfigType]: + """Validate config.""" + validated_config = [] + for config_item in config: + try: + validated_config.append(schema(config_item)) + except vol.MultipleInvalid as err: + async_log_exception(err, domain, config_item, hass) + + return validated_config + + config_yaml: ConfigType = hass.data.get(DATA_MQTT_CONFIG, {}) + if not (platform_configs := config_yaml.get(domain)): + return [] + return async_validate_config(hass, platform_configs) + + async def async_setup_entry_helper(hass, domain, async_setup, schema): """Set up entity, automation or tag creation dynamically through MQTT discovery.""" diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 8cf7353d196..b5bb5732617 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -1690,3 +1690,24 @@ async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): assert hass.states.get(f"{domain}.test_new_1") assert hass.states.get(f"{domain}.test_new_2") assert hass.states.get(f"{domain}.test_new_3") + + +async def help_test_setup_manual_entity_from_yaml( + hass, + caplog, + tmp_path, + platform, + config, +): + """Help to test setup from yaml through configuration entry.""" + config_structure = {mqtt.DOMAIN: {platform: config}} + + await async_setup_component(hass, mqtt.DOMAIN, config_structure) + # Mock config entry + entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry.add_to_hass(hass) + + with patch("paho.mqtt.client.Client") as mock_client: + mock_client().connect = lambda *args: 0 + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 64b5d272af8..1a533db63c0 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -55,6 +55,7 @@ from .test_common import ( help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -1805,3 +1806,15 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): domain = fan.DOMAIN config = DEFAULT_CONFIG[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = fan.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 543174653ec..a370bd67ec1 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1,5 +1,6 @@ """The tests for the MQTT component.""" import asyncio +import copy from datetime import datetime, timedelta from functools import partial import json @@ -30,6 +31,8 @@ from homeassistant.helpers.entity import Entity from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow +from .test_common import help_test_setup_manual_entity_from_yaml + from tests.common import ( MockConfigEntry, async_fire_mqtt_message, @@ -1279,6 +1282,51 @@ async def test_setup_override_configuration(hass, caplog, tmp_path): assert calls_username_password_set[0][1] == "somepassword" +async def test_setup_manual_mqtt_with_platform_key(hass, caplog, tmp_path): + """Test set up a manual MQTT item with a platform key.""" + config = {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} + await help_test_setup_manual_entity_from_yaml( + hass, + caplog, + tmp_path, + "light", + config, + ) + assert ( + "Invalid config for [light]: [platform] is an invalid option for [light]. " + "Check: light->platform. (See ?, line ?)" in caplog.text + ) + + +async def test_setup_manual_mqtt_with_invalid_config(hass, caplog, tmp_path): + """Test set up a manual MQTT item with an invalid config.""" + config = {"name": "test"} + await help_test_setup_manual_entity_from_yaml( + hass, + caplog, + tmp_path, + "light", + config, + ) + assert ( + "Invalid config for [light]: required key not provided @ data['command_topic']." + " Got None. (See ?, line ?)" in caplog.text + ) + + +async def test_setup_manual_mqtt_empty_platform(hass, caplog, tmp_path): + """Test set up a manual MQTT platform without items.""" + config = None + await help_test_setup_manual_entity_from_yaml( + hass, + caplog, + tmp_path, + "light", + config, + ) + assert "voluptuous.error.MultipleInvalid" not in caplog.text + + async def test_setup_mqtt_client_protocol(hass): """Test MQTT client protocol setup.""" entry = MockConfigEntry( @@ -1628,7 +1676,8 @@ async def test_setup_entry_with_config_override(hass, device_reg, mqtt_client_mo # User sets up a config entry entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) + with patch("homeassistant.components.mqtt.PLATFORMS", []): + assert await hass.config_entries.async_setup(entry.entry_id) # Discover a device to verify the entry was setup correctly async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) @@ -2413,3 +2462,23 @@ async def test_subscribe_connection_status(hass, mqtt_mock, mqtt_client_mock): assert len(mqtt_connected_calls) == 2 assert mqtt_connected_calls[0] is True assert mqtt_connected_calls[1] is False + + +async def test_one_deprecation_warning_per_platform(hass, mqtt_mock, caplog): + """Test a deprecation warning is is logged once per platform.""" + platform = "light" + config = {"platform": "mqtt", "command_topic": "test-topic"} + config1 = copy.deepcopy(config) + config1["name"] = "test1" + config2 = copy.deepcopy(config) + config2["name"] = "test2" + await async_setup_component(hass, platform, {platform: [config1, config2]}) + await hass.async_block_till_done() + count = 0 + for record in caplog.records: + if record.levelname == "WARNING" and ( + f"Manually configured MQTT {platform}(s) found under platform key '{platform}'" + in record.message + ): + count += 1 + assert count == 1 diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index ee59928c0c8..957178da14f 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -237,6 +237,7 @@ from .test_common import ( help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -3674,3 +3675,15 @@ async def test_sending_mqtt_effect_command_with_template(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("effect") == "colorloop" + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = light.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 93bb9b0f573..962bf534370 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -131,6 +131,7 @@ from .test_common import ( help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -2038,3 +2039,15 @@ async def test_encoding_subscribable_topics( init_payload, skip_raw_test=True, ) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = light.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 4461cf14ef4..a88fc094f6d 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -69,6 +69,7 @@ from .test_common import ( help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -1213,3 +1214,15 @@ async def test_encoding_subscribable_topics( attribute_value, init_payload, ) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = light.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/conftest.py b/tests/conftest.py index 37bdb05faf7..8ea6e114e9a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -561,9 +561,10 @@ async def mqtt_mock(hass, mqtt_client_mock, mqtt_config): ) entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + # Do not forward the entry setup to the components here + with patch("homeassistant.components.mqtt.PLATFORMS", []): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() mqtt_component_mock = MagicMock( return_value=hass.data["mqtt"], From f166a47df38a7dceb3ab677ecb33e4ea873a25c7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 May 2022 15:50:32 +0200 Subject: [PATCH 0683/3516] Adjust device_automation type hints in nest (#72135) --- homeassistant/components/nest/device_trigger.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index 1f7f2b642dc..7bdbab214b8 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -1,8 +1,6 @@ """Provides device automations for Nest.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -66,7 +64,7 @@ async def async_get_device_trigger_types( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for a Nest device.""" nest_device_id = async_get_nest_device_id(hass, device_id) if not nest_device_id: From c27d5631643bd09e0ca965674d6545f5cb31a1c2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 19 May 2022 16:19:39 +0200 Subject: [PATCH 0684/3516] Make changes to application_credentials trigger full CI run (#72157) --- .core_files.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.core_files.yaml b/.core_files.yaml index 654730e5613..2928d450ce2 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -48,6 +48,7 @@ base_platforms: &base_platforms components: &components - homeassistant/components/alert/** - homeassistant/components/alexa/** + - homeassistant/components/application_credentials/** - homeassistant/components/auth/** - homeassistant/components/automation/** - homeassistant/components/backup/** From 1e1016aa5fcc8ddb2bfd83530c1dc081333be0b6 Mon Sep 17 00:00:00 2001 From: Matrix Date: Thu, 19 May 2022 22:43:32 +0800 Subject: [PATCH 0685/3516] Add yolink binary sensor (#72000) * Add binary sensor platform * Add untest file to .coveragerc * change attr default --- .coveragerc | 1 + homeassistant/components/yolink/__init__.py | 2 +- .../components/yolink/binary_sensor.py | 90 +++++++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/yolink/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 5fabf2851fb..6cc9ffda714 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1472,6 +1472,7 @@ omit = homeassistant/components/yi/camera.py homeassistant/components/yolink/__init__.py homeassistant/components/yolink/api.py + homeassistant/components/yolink/binary_sensor.py homeassistant/components/yolink/const.py homeassistant/components/yolink/coordinator.py homeassistant/components/yolink/entity.py diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index d31a082f82f..8a8afb2f1b3 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -21,7 +21,7 @@ SCAN_INTERVAL = timedelta(minutes=5) _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/yolink/binary_sensor.py b/homeassistant/components/yolink/binary_sensor.py new file mode 100644 index 00000000000..c3f2d10cf4c --- /dev/null +++ b/homeassistant/components/yolink/binary_sensor.py @@ -0,0 +1,90 @@ +"""YoLink BinarySensor.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from yolink.device import YoLinkDevice + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ATTR_COORDINATOR, ATTR_DEVICE_DOOR_SENSOR, DOMAIN +from .coordinator import YoLinkCoordinator +from .entity import YoLinkEntity + + +@dataclass +class YoLinkBinarySensorEntityDescription(BinarySensorEntityDescription): + """YoLink BinarySensorEntityDescription.""" + + exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True + value: Callable[[str], bool | None] = lambda _: None + + +SENSOR_DEVICE_TYPE = [ATTR_DEVICE_DOOR_SENSOR] + +SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( + YoLinkBinarySensorEntityDescription( + key="state", + icon="mdi:door", + device_class=BinarySensorDeviceClass.DOOR, + name="state", + value=lambda value: value == "open", + exists_fn=lambda device: device.device_type in [ATTR_DEVICE_DOOR_SENSOR], + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up YoLink Sensor from a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATOR] + sensor_devices = [ + device + for device in coordinator.yl_devices + if device.device_type in SENSOR_DEVICE_TYPE + ] + entities = [] + for sensor_device in sensor_devices: + for description in SENSOR_TYPES: + if description.exists_fn(sensor_device): + entities.append( + YoLinkBinarySensorEntity(coordinator, description, sensor_device) + ) + async_add_entities(entities) + + +class YoLinkBinarySensorEntity(YoLinkEntity, BinarySensorEntity): + """YoLink Sensor Entity.""" + + entity_description: YoLinkBinarySensorEntityDescription + + def __init__( + self, + coordinator: YoLinkCoordinator, + description: YoLinkBinarySensorEntityDescription, + device: YoLinkDevice, + ) -> None: + """Init YoLink Sensor.""" + super().__init__(coordinator, device) + self.entity_description = description + self._attr_unique_id = f"{device.device_id} {self.entity_description.key}" + self._attr_name = f"{device.device_name} ({self.entity_description.name})" + + @callback + def update_entity_state(self, state: dict) -> None: + """Update HA Entity State.""" + self._attr_is_on = self.entity_description.value( + state[self.entity_description.key] + ) + self.async_write_ha_state() From b65b47f25ee9b44501f9865300628043eb004948 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 May 2022 17:05:25 +0200 Subject: [PATCH 0686/3516] Cleanup zha async method which is not awaiting (#72093) * Cleanup zha async method which is not awaiting * Missed a commit --- homeassistant/components/zha/core/helpers.py | 3 ++- homeassistant/components/zha/device_action.py | 4 ++-- homeassistant/components/zha/device_trigger.py | 6 +++--- homeassistant/components/zha/diagnostics.py | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 19a1d3fef9a..a101720e8df 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -160,7 +160,8 @@ def async_cluster_exists(hass, cluster_id): return False -async def async_get_zha_device(hass, device_id): +@callback +def async_get_zha_device(hass, device_id): """Get a ZHA device for the given device registry id.""" device_registry = dr.async_get(hass) registry_device = device_registry.async_get(device_id) diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 36696517eb6..f941b559cc7 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -61,7 +61,7 @@ async def async_get_actions( ) -> list[dict[str, str]]: """List device actions.""" try: - zha_device = await async_get_zha_device(hass, device_id) + zha_device = async_get_zha_device(hass, device_id) except (KeyError, AttributeError): return [] cluster_channels = [ @@ -89,7 +89,7 @@ async def _execute_service_based_action( action_type = config[CONF_TYPE] service_name = SERVICE_NAMES[action_type] try: - zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) + zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) except (KeyError, AttributeError): return diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 03bdc32e6a6..d81d8164bd4 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -28,7 +28,7 @@ async def async_validate_trigger_config(hass, config): if "zha" in hass.config.components: trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) try: - zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) + zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) except (KeyError, AttributeError) as err: raise InvalidDeviceAutomationConfig from err if ( @@ -44,7 +44,7 @@ async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) try: - zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) + zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) except (KeyError, AttributeError): return None @@ -71,7 +71,7 @@ async def async_get_triggers(hass, device_id): Make sure the device supports device automations and if it does return the trigger list. """ - zha_device = await async_get_zha_device(hass, device_id) + zha_device = async_get_zha_device(hass, device_id) if not zha_device.device_automation_triggers: return diff --git a/homeassistant/components/zha/diagnostics.py b/homeassistant/components/zha/diagnostics.py index ec8dab241de..5ae2ff23e96 100644 --- a/homeassistant/components/zha/diagnostics.py +++ b/homeassistant/components/zha/diagnostics.py @@ -76,5 +76,5 @@ async def async_get_device_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry ) -> dict: """Return diagnostics for a device.""" - zha_device: ZHADevice = await async_get_zha_device(hass, device.id) + zha_device: ZHADevice = async_get_zha_device(hass, device.id) return async_redact_data(zha_device.zha_device_info, KEYS_TO_REDACT) From 3526252cfce4b6ca2728c73271020b82d0858447 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 19 May 2022 08:16:49 -0700 Subject: [PATCH 0687/3516] Bump gcal-sync to 0.8.1 (#72164) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index fde04d96baf..03d15597c44 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", - "requirements": ["gcal-sync==0.8.0", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==0.8.1", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 05403b5d3aa..15bd902f9a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -689,7 +689,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==0.8.0 +gcal-sync==0.8.1 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d11047f4c95..a5ea5832e65 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -492,7 +492,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==0.8.0 +gcal-sync==0.8.1 # homeassistant.components.geocaching geocachingapi==0.2.1 From f06f94ea853701b210f1288cf2d04a11037d6728 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 May 2022 10:59:06 -0500 Subject: [PATCH 0688/3516] Add coverage to ensure we reject 0 length logbook filters (#72124) --- tests/components/logbook/test_init.py | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index a7420df9c5d..05df4f2ab3b 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -2442,6 +2442,34 @@ async def test_get_events_bad_end_time(hass, hass_ws_client, recorder_mock): assert response["error"]["code"] == "invalid_end_time" +async def test_get_events_invalid_filters(hass, hass_ws_client, recorder_mock): + """Test get_events invalid filters.""" + await async_setup_component(hass, "logbook", {}) + await async_recorder_block_till_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "logbook/get_events", + "entity_ids": [], + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_format" + await client.send_json( + { + "id": 2, + "type": "logbook/get_events", + "device_ids": [], + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_format" + + async def test_get_events_with_device_ids(hass, hass_ws_client, recorder_mock): """Test logbook get_events for device ids.""" now = dt_util.utcnow() From 3ece5965a8846d645e7c1d436b1d2db17ea42cdf Mon Sep 17 00:00:00 2001 From: Marcio Granzotto Rodrigues Date: Thu, 19 May 2022 13:04:48 -0300 Subject: [PATCH 0689/3516] Update bond-api to 0.1.18 (#72166) --- 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 e5f8b004502..b011a5f2dae 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.16"], + "requirements": ["bond-api==0.1.18"], "zeroconf": ["_bond._tcp.local."], "codeowners": ["@bdraco", "@prystupa", "@joshs85"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 15bd902f9a0..13b3b172803 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -420,7 +420,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bond -bond-api==0.1.16 +bond-api==0.1.18 # homeassistant.components.bosch_shc boschshcpy==0.2.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5ea5832e65..86f49132bc0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -321,7 +321,7 @@ blebox_uniapi==1.3.3 blinkpy==0.19.0 # homeassistant.components.bond -bond-api==0.1.16 +bond-api==0.1.18 # homeassistant.components.bosch_shc boschshcpy==0.2.30 From 17bb5034506d03f7a717d6ffd5b4eea8eca74376 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 May 2022 18:23:23 +0200 Subject: [PATCH 0690/3516] Add new methods to DeviceAutomationActionProtocol (#72163) * Add composite type * Extend action protocol * Drop return/raise --- .../components/device_automation/__init__.py | 2 ++ .../components/device_automation/action.py | 20 ++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index d7b75d4fcd4..e24ae15f9ff 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -44,6 +44,8 @@ if TYPE_CHECKING: ] # mypy: allow-untyped-calls, allow-untyped-defs +GetAutomationsResult = list[dict[str, Any]] +GetAutomationCapabilitiesResult = dict[str, vol.Schema] DOMAIN = "device_automation" diff --git a/homeassistant/components/device_automation/action.py b/homeassistant/components/device_automation/action.py index 5261757c645..b15ca12a927 100644 --- a/homeassistant/components/device_automation/action.py +++ b/homeassistant/components/device_automation/action.py @@ -1,6 +1,7 @@ """Device action validator.""" from __future__ import annotations +from collections.abc import Awaitable from typing import Any, Protocol, cast import voluptuous as vol @@ -9,7 +10,12 @@ from homeassistant.const import CONF_DOMAIN from homeassistant.core import Context, HomeAssistant from homeassistant.helpers.typing import ConfigType -from . import DeviceAutomationType, async_get_device_automation_platform +from . import ( + DeviceAutomationType, + GetAutomationCapabilitiesResult, + GetAutomationsResult, + async_get_device_automation_platform, +) from .exceptions import InvalidDeviceAutomationConfig @@ -25,7 +31,6 @@ class DeviceAutomationActionProtocol(Protocol): self, hass: HomeAssistant, config: ConfigType ) -> ConfigType: """Validate config.""" - raise NotImplementedError async def async_call_action_from_config( self, @@ -35,7 +40,16 @@ class DeviceAutomationActionProtocol(Protocol): context: Context | None, ) -> None: """Execute a device action.""" - raise NotImplementedError + + def async_get_action_capabilities( + self, hass: HomeAssistant, config: ConfigType + ) -> GetAutomationCapabilitiesResult | Awaitable[GetAutomationCapabilitiesResult]: + """List action capabilities.""" + + def async_get_actions( + self, hass: HomeAssistant, device_id: str + ) -> GetAutomationsResult | Awaitable[GetAutomationsResult]: + """List actions.""" async def async_validate_action_config( From 8a00281eaab9e4aeb9632427b8152dc4ebe881b6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 May 2022 18:23:40 +0200 Subject: [PATCH 0691/3516] Add new methods to DeviceAutomationTriggerProtocol (#72168) * Add composite type * Add new methods to DeviceAutomationTriggerProtocol --- .../components/device_automation/trigger.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py index 933c5c4c60a..e4a740d6599 100644 --- a/homeassistant/components/device_automation/trigger.py +++ b/homeassistant/components/device_automation/trigger.py @@ -1,4 +1,7 @@ """Offer device oriented automation.""" +from __future__ import annotations + +from collections.abc import Awaitable from typing import Protocol, cast import voluptuous as vol @@ -14,6 +17,8 @@ from homeassistant.helpers.typing import ConfigType from . import ( DEVICE_TRIGGER_BASE_SCHEMA, DeviceAutomationType, + GetAutomationCapabilitiesResult, + GetAutomationsResult, async_get_device_automation_platform, ) from .exceptions import InvalidDeviceAutomationConfig @@ -33,7 +38,6 @@ class DeviceAutomationTriggerProtocol(Protocol): self, hass: HomeAssistant, config: ConfigType ) -> ConfigType: """Validate config.""" - raise NotImplementedError async def async_attach_trigger( self, @@ -43,7 +47,16 @@ class DeviceAutomationTriggerProtocol(Protocol): automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" - raise NotImplementedError + + def async_get_trigger_capabilities( + self, hass: HomeAssistant, config: ConfigType + ) -> GetAutomationCapabilitiesResult | Awaitable[GetAutomationCapabilitiesResult]: + """List trigger capabilities.""" + + def async_get_triggers( + self, hass: HomeAssistant, device_id: str + ) -> GetAutomationsResult | Awaitable[GetAutomationsResult]: + """List triggers.""" async def async_validate_trigger_config( From 8ef39c3cfd99235caf951c9db8d875b95e4297f9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 May 2022 18:24:01 +0200 Subject: [PATCH 0692/3516] Add new methods to DeviceAutomationConditionProtocol (#72169) * Add composite type * Add new methods to DeviceAutomationConditionProtocol --- .../components/device_automation/condition.py | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/device_automation/condition.py b/homeassistant/components/device_automation/condition.py index 1c226ee8c29..7ff3216c702 100644 --- a/homeassistant/components/device_automation/condition.py +++ b/homeassistant/components/device_automation/condition.py @@ -1,6 +1,7 @@ """Validate device conditions.""" from __future__ import annotations +from collections.abc import Awaitable from typing import TYPE_CHECKING, Protocol, cast import voluptuous as vol @@ -10,7 +11,12 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType -from . import DeviceAutomationType, async_get_device_automation_platform +from . import ( + DeviceAutomationType, + GetAutomationCapabilitiesResult, + GetAutomationsResult, + async_get_device_automation_platform, +) from .exceptions import InvalidDeviceAutomationConfig if TYPE_CHECKING: @@ -29,13 +35,21 @@ class DeviceAutomationConditionProtocol(Protocol): self, hass: HomeAssistant, config: ConfigType ) -> ConfigType: """Validate config.""" - raise NotImplementedError def async_condition_from_config( self, hass: HomeAssistant, config: ConfigType ) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" - raise NotImplementedError + + def async_get_condition_capabilities( + self, hass: HomeAssistant, config: ConfigType + ) -> GetAutomationCapabilitiesResult | Awaitable[GetAutomationCapabilitiesResult]: + """List condition capabilities.""" + + def async_get_conditions( + self, hass: HomeAssistant, device_id: str + ) -> GetAutomationsResult | Awaitable[GetAutomationsResult]: + """List conditions.""" async def async_validate_condition_config( From 0422d7f2569c78c33be87dbb475764769b2f382c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 May 2022 18:34:51 +0200 Subject: [PATCH 0693/3516] Add type hints to homekit_controller (#72155) --- .../components/homekit_controller/device_trigger.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index 3ba2abe3339..dcac7238c8e 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -1,6 +1,7 @@ """Provides device automations for homekit devices.""" from __future__ import annotations +from collections.abc import Generator from typing import TYPE_CHECKING, Any from aiohomekit.model.characteristics import CharacteristicsTypes @@ -63,7 +64,7 @@ class TriggerSource: self._hass = connection.hass self._connection = connection self._aid = aid - self._triggers = {} + self._triggers: dict[tuple[str, str], dict[str, Any]] = {} for trigger in triggers: self._triggers[(trigger["type"], trigger["subtype"])] = trigger self._callbacks = {} @@ -73,7 +74,7 @@ class TriggerSource: for event_handler in self._callbacks.get(iid, []): event_handler(value) - def async_get_triggers(self): + def async_get_triggers(self) -> Generator[tuple[str, str], None, None]: """List device triggers for homekit devices.""" yield from self._triggers @@ -246,7 +247,7 @@ async def async_get_triggers( if device_id not in hass.data.get(TRIGGERS, {}): return [] - device = hass.data[TRIGGERS][device_id] + device: TriggerSource = hass.data[TRIGGERS][device_id] return [ { From 1c4c0f1eb33302b983cb39f6800cd0fe6943fc4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 May 2022 11:50:54 -0500 Subject: [PATCH 0694/3516] Small fixes and cleanups to legacy nexia code (#72176) --- homeassistant/components/nexia/climate.py | 32 ++++------ homeassistant/components/nexia/const.py | 9 +-- homeassistant/components/nexia/coordinator.py | 2 +- homeassistant/components/nexia/entity.py | 60 ++++++------------- homeassistant/components/nexia/scene.py | 33 ++++------ tests/components/nexia/test_binary_sensor.py | 4 +- tests/components/nexia/test_climate.py | 6 +- tests/components/nexia/test_scene.py | 6 +- tests/components/nexia/test_sensor.py | 18 +++--- 9 files changed, 60 insertions(+), 110 deletions(-) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 0c27b1497f5..20fcf5c6b85 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -40,7 +40,6 @@ from .const import ( ATTR_DEHUMIDIFY_SETPOINT, ATTR_HUMIDIFY_SETPOINT, ATTR_RUN_MODE, - ATTR_ZONE_STATUS, DOMAIN, ) from .coordinator import NexiaDataUpdateCoordinator @@ -344,31 +343,26 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): self._signal_zone_update() @property - def is_aux_heat(self): + def is_aux_heat(self) -> bool: """Emergency heat state.""" return self._thermostat.is_emergency_heat_active() @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, str] | None: """Return the device specific state attributes.""" - data = super().extra_state_attributes - - data[ATTR_ZONE_STATUS] = self._zone.get_status() - if not self._has_relative_humidity: - return data + return None + attrs = {} if self._has_dehumidify_support: dehumdify_setpoint = percent_conv( self._thermostat.get_dehumidify_setpoint() ) - data[ATTR_DEHUMIDIFY_SETPOINT] = dehumdify_setpoint - + attrs[ATTR_DEHUMIDIFY_SETPOINT] = dehumdify_setpoint if self._has_humidify_support: humdify_setpoint = percent_conv(self._thermostat.get_humidify_setpoint()) - data[ATTR_HUMIDIFY_SETPOINT] = humdify_setpoint - - return data + attrs[ATTR_HUMIDIFY_SETPOINT] = humdify_setpoint + return attrs async def async_set_preset_mode(self, preset_mode: str): """Set the preset mode.""" @@ -376,23 +370,23 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): self._signal_zone_update() async def async_turn_aux_heat_off(self): - """Turn. Aux Heat off.""" + """Turn Aux Heat off.""" await self._thermostat.set_emergency_heat(False) self._signal_thermostat_update() async def async_turn_aux_heat_on(self): - """Turn. Aux Heat on.""" + """Turn Aux Heat on.""" self._thermostat.set_emergency_heat(True) self._signal_thermostat_update() async def async_turn_off(self): - """Turn. off the zone.""" - await self.set_hvac_mode(OPERATION_MODE_OFF) + """Turn off the zone.""" + await self.async_set_hvac_mode(OPERATION_MODE_OFF) self._signal_zone_update() async def async_turn_on(self): - """Turn. on the zone.""" - await self.set_hvac_mode(OPERATION_MODE_AUTO) + """Turn on the zone.""" + await self.async_set_hvac_mode(OPERATION_MODE_AUTO) self._signal_zone_update() async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: diff --git a/homeassistant/components/nexia/const.py b/homeassistant/components/nexia/const.py index 5d84e8a3075..493fdd8a403 100644 --- a/homeassistant/components/nexia/const.py +++ b/homeassistant/components/nexia/const.py @@ -9,17 +9,11 @@ PLATFORMS = [ Platform.SWITCH, ] -ATTRIBUTION = "Data provided by mynexia.com" - -NOTIFICATION_ID = "nexia_notification" -NOTIFICATION_TITLE = "Nexia Setup" +ATTRIBUTION = "Data provided by Trane Technologies" CONF_BRAND = "brand" -NEXIA_SCAN_INTERVAL = "scan_interval" - DOMAIN = "nexia" -DEFAULT_ENTITY_NAMESPACE = "nexia" ATTR_DESCRIPTION = "description" @@ -27,7 +21,6 @@ ATTR_AIRCLEANER_MODE = "aircleaner_mode" ATTR_RUN_MODE = "run_mode" -ATTR_ZONE_STATUS = "zone_status" ATTR_HUMIDIFY_SETPOINT = "humidify_setpoint" ATTR_DEHUMIDIFY_SETPOINT = "dehumidify_setpoint" diff --git a/homeassistant/components/nexia/coordinator.py b/homeassistant/components/nexia/coordinator.py index ba61a3591f0..d44e1827f5a 100644 --- a/homeassistant/components/nexia/coordinator.py +++ b/homeassistant/components/nexia/coordinator.py @@ -1,4 +1,4 @@ -"""Component to embed TP-Link smart home devices.""" +"""Component to embed nexia devices.""" from __future__ import annotations from datetime import timedelta diff --git a/homeassistant/components/nexia/entity.py b/homeassistant/components/nexia/entity.py index dde0e5ae8f3..4f806d03eda 100644 --- a/homeassistant/components/nexia/entity.py +++ b/homeassistant/components/nexia/entity.py @@ -1,8 +1,14 @@ """The nexia integration base entity.""" + from nexia.thermostat import NexiaThermostat from nexia.zone import NexiaThermostatZone -from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.const import ( + ATTR_IDENTIFIERS, + ATTR_NAME, + ATTR_SUGGESTED_AREA, + ATTR_VIA_DEVICE, +) from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -20,33 +26,18 @@ from .const import ( from .coordinator import NexiaDataUpdateCoordinator -class NexiaEntity(CoordinatorEntity): +class NexiaEntity(CoordinatorEntity[NexiaDataUpdateCoordinator]): """Base class for nexia entities.""" + _attr_attribution = ATTRIBUTION + def __init__( self, coordinator: NexiaDataUpdateCoordinator, name: str, unique_id: str ) -> None: """Initialize the entity.""" super().__init__(coordinator) - self._unique_id = unique_id - self._name = name - - @property - def unique_id(self): - """Return the unique id.""" - return self._unique_id - - @property - def name(self): - """Return the name.""" - return self._name - - @property - def extra_state_attributes(self): - """Return the device specific state attributes.""" - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - } + self._attr_unique_id = unique_id + self._attr_name = name class NexiaThermostatEntity(NexiaEntity): @@ -56,12 +47,7 @@ class NexiaThermostatEntity(NexiaEntity): """Initialize the entity.""" super().__init__(coordinator, name, unique_id) self._thermostat: NexiaThermostat = thermostat - - @property - def device_info(self) -> DeviceInfo: - """Return the device_info of the device.""" - assert isinstance(self.coordinator, NexiaDataUpdateCoordinator) - return DeviceInfo( + self._attr_device_info = DeviceInfo( configuration_url=self.coordinator.nexia_home.root_url, identifiers={(DOMAIN, self._thermostat.thermostat_id)}, manufacturer=MANUFACTURER, @@ -102,21 +88,13 @@ class NexiaThermostatZoneEntity(NexiaThermostatEntity): """Initialize the entity.""" super().__init__(coordinator, zone.thermostat, name, unique_id) self._zone: NexiaThermostatZone = zone - - @property - def device_info(self): - """Return the device_info of the device.""" - data = super().device_info zone_name = self._zone.get_name() - data.update( - { - "identifiers": {(DOMAIN, self._zone.zone_id)}, - "name": zone_name, - "suggested_area": zone_name, - "via_device": (DOMAIN, self._zone.thermostat.thermostat_id), - } - ) - return data + self._attr_device_info |= { + ATTR_IDENTIFIERS: {(DOMAIN, self._zone.zone_id)}, + ATTR_NAME: zone_name, + ATTR_SUGGESTED_AREA: zone_name, + ATTR_VIA_DEVICE: (DOMAIN, self._zone.thermostat.thermostat_id), + } async def async_added_to_hass(self): """Listen for signals for services.""" diff --git a/homeassistant/components/nexia/scene.py b/homeassistant/components/nexia/scene.py index 28c892fe8e5..941785f8221 100644 --- a/homeassistant/components/nexia/scene.py +++ b/homeassistant/components/nexia/scene.py @@ -24,21 +24,19 @@ async def async_setup_entry( """Set up automations for a Nexia device.""" coordinator: NexiaDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] nexia_home = coordinator.nexia_home - - entities = [] - - # Automation switches - for automation_id in nexia_home.get_automation_ids(): - automation = nexia_home.get_automation_by_id(automation_id) - - entities.append(NexiaAutomationScene(coordinator, automation)) - - async_add_entities(entities) + async_add_entities( + NexiaAutomationScene( + coordinator, nexia_home.get_automation_by_id(automation_id) + ) + for automation_id in nexia_home.get_automation_ids() + ) class NexiaAutomationScene(NexiaEntity, Scene): """Provides Nexia automation support.""" + _attr_icon = "mdi:script-text-outline" + def __init__( self, coordinator: NexiaDataUpdateCoordinator, automation: NexiaAutomation ) -> None: @@ -48,19 +46,8 @@ class NexiaAutomationScene(NexiaEntity, Scene): name=automation.name, unique_id=automation.automation_id, ) - self._automation = automation - - @property - def extra_state_attributes(self): - """Return the scene specific state attributes.""" - data = super().extra_state_attributes - data[ATTR_DESCRIPTION] = self._automation.description - return data - - @property - def icon(self): - """Return the icon of the automation scene.""" - return "mdi:script-text-outline" + self._automation: NexiaAutomation = automation + self._attr_extra_state_attributes = {ATTR_DESCRIPTION: automation.description} async def async_activate(self, **kwargs: Any) -> None: """Activate an automation scene.""" diff --git a/tests/components/nexia/test_binary_sensor.py b/tests/components/nexia/test_binary_sensor.py index 64b2946ee2f..966f49bbb15 100644 --- a/tests/components/nexia/test_binary_sensor.py +++ b/tests/components/nexia/test_binary_sensor.py @@ -13,7 +13,7 @@ async def test_create_binary_sensors(hass): state = hass.states.get("binary_sensor.master_suite_blower_active") assert state.state == STATE_ON expected_attributes = { - "attribution": "Data provided by mynexia.com", + "attribution": "Data provided by Trane Technologies", "friendly_name": "Master Suite Blower Active", } # Only test for a subset of attributes in case @@ -25,7 +25,7 @@ async def test_create_binary_sensors(hass): state = hass.states.get("binary_sensor.downstairs_east_wing_blower_active") assert state.state == STATE_OFF expected_attributes = { - "attribution": "Data provided by mynexia.com", + "attribution": "Data provided by Trane Technologies", "friendly_name": "Downstairs East Wing Blower Active", } # Only test for a subset of attributes in case diff --git a/tests/components/nexia/test_climate.py b/tests/components/nexia/test_climate.py index 312d78ac256..797fbb4014c 100644 --- a/tests/components/nexia/test_climate.py +++ b/tests/components/nexia/test_climate.py @@ -12,7 +12,7 @@ async def test_climate_zones(hass): state = hass.states.get("climate.nick_office") assert state.state == HVACMode.HEAT_COOL expected_attributes = { - "attribution": "Data provided by mynexia.com", + "attribution": "Data provided by Trane Technologies", "current_humidity": 52.0, "current_temperature": 22.8, "dehumidify_setpoint": 45.0, @@ -33,7 +33,6 @@ async def test_climate_zones(hass): "target_temp_low": 17.2, "target_temp_step": 1.0, "temperature": None, - "zone_status": "Relieving Air", } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears @@ -45,7 +44,7 @@ async def test_climate_zones(hass): assert state.state == HVACMode.HEAT_COOL expected_attributes = { - "attribution": "Data provided by mynexia.com", + "attribution": "Data provided by Trane Technologies", "current_humidity": 36.0, "current_temperature": 25.0, "dehumidify_setpoint": 50.0, @@ -66,7 +65,6 @@ async def test_climate_zones(hass): "target_temp_low": 17.2, "target_temp_step": 1.0, "temperature": None, - "zone_status": "Idle", } # Only test for a subset of attributes in case diff --git a/tests/components/nexia/test_scene.py b/tests/components/nexia/test_scene.py index 4a325552e80..309af6b523d 100644 --- a/tests/components/nexia/test_scene.py +++ b/tests/components/nexia/test_scene.py @@ -10,7 +10,7 @@ async def test_automation_scenes(hass): state = hass.states.get("scene.away_short") expected_attributes = { - "attribution": "Data provided by mynexia.com", + "attribution": "Data provided by Trane Technologies", "description": "When IFTTT activates the automation Upstairs " "West Wing will permanently hold the heat to 63.0 " "and cool to 80.0 AND Downstairs East Wing will " @@ -37,7 +37,7 @@ async def test_automation_scenes(hass): state = hass.states.get("scene.power_outage") expected_attributes = { - "attribution": "Data provided by mynexia.com", + "attribution": "Data provided by Trane Technologies", "description": "When IFTTT activates the automation Upstairs " "West Wing will permanently hold the heat to 55.0 " "and cool to 90.0 AND Downstairs East Wing will " @@ -56,7 +56,7 @@ async def test_automation_scenes(hass): state = hass.states.get("scene.power_restored") expected_attributes = { - "attribution": "Data provided by mynexia.com", + "attribution": "Data provided by Trane Technologies", "description": "When IFTTT activates the automation Upstairs " "West Wing will Run Schedule AND Downstairs East " "Wing will Run Schedule AND Downstairs West Wing " diff --git a/tests/components/nexia/test_sensor.py b/tests/components/nexia/test_sensor.py index 289947fbdf6..ad15fc308f4 100644 --- a/tests/components/nexia/test_sensor.py +++ b/tests/components/nexia/test_sensor.py @@ -14,7 +14,7 @@ async def test_create_sensors(hass): assert state.state == "23" expected_attributes = { - "attribution": "Data provided by mynexia.com", + "attribution": "Data provided by Trane Technologies", "device_class": "temperature", "friendly_name": "Nick Office Temperature", "unit_of_measurement": TEMP_CELSIUS, @@ -28,7 +28,7 @@ async def test_create_sensors(hass): state = hass.states.get("sensor.nick_office_zone_setpoint_status") assert state.state == "Permanent Hold" expected_attributes = { - "attribution": "Data provided by mynexia.com", + "attribution": "Data provided by Trane Technologies", "friendly_name": "Nick Office Zone Setpoint Status", } # Only test for a subset of attributes in case @@ -41,7 +41,7 @@ async def test_create_sensors(hass): assert state.state == "Relieving Air" expected_attributes = { - "attribution": "Data provided by mynexia.com", + "attribution": "Data provided by Trane Technologies", "friendly_name": "Nick Office Zone Status", } # Only test for a subset of attributes in case @@ -54,7 +54,7 @@ async def test_create_sensors(hass): assert state.state == "auto" expected_attributes = { - "attribution": "Data provided by mynexia.com", + "attribution": "Data provided by Trane Technologies", "friendly_name": "Master Suite Air Cleaner Mode", } # Only test for a subset of attributes in case @@ -67,7 +67,7 @@ async def test_create_sensors(hass): assert state.state == "69.0" expected_attributes = { - "attribution": "Data provided by mynexia.com", + "attribution": "Data provided by Trane Technologies", "friendly_name": "Master Suite Current Compressor Speed", "unit_of_measurement": PERCENTAGE, } @@ -81,7 +81,7 @@ async def test_create_sensors(hass): assert state.state == "30.6" expected_attributes = { - "attribution": "Data provided by mynexia.com", + "attribution": "Data provided by Trane Technologies", "device_class": "temperature", "friendly_name": "Master Suite Outdoor Temperature", "unit_of_measurement": TEMP_CELSIUS, @@ -96,7 +96,7 @@ async def test_create_sensors(hass): assert state.state == "52.0" expected_attributes = { - "attribution": "Data provided by mynexia.com", + "attribution": "Data provided by Trane Technologies", "device_class": "humidity", "friendly_name": "Master Suite Relative Humidity", "unit_of_measurement": PERCENTAGE, @@ -111,7 +111,7 @@ async def test_create_sensors(hass): assert state.state == "69.0" expected_attributes = { - "attribution": "Data provided by mynexia.com", + "attribution": "Data provided by Trane Technologies", "friendly_name": "Master Suite Requested Compressor Speed", "unit_of_measurement": PERCENTAGE, } @@ -125,7 +125,7 @@ async def test_create_sensors(hass): assert state.state == "Cooling" expected_attributes = { - "attribution": "Data provided by mynexia.com", + "attribution": "Data provided by Trane Technologies", "friendly_name": "Master Suite System Status", } # Only test for a subset of attributes in case From 487819bbe5cf00547c1c2157c64b8b438ef0224a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 19 May 2022 13:31:24 -0400 Subject: [PATCH 0695/3516] Use device ID as input for zwave_js WS device cmds (#71667) * Use device ID as input for zwave_js WS device cmds * Additionally missed commands, update network_status command to include node status data * revert change to removed function * Revert register device change --- homeassistant/components/zwave_js/api.py | 148 +++----- homeassistant/components/zwave_js/helpers.py | 33 +- tests/components/zwave_js/test_api.py | 370 ++++++------------- 3 files changed, 179 insertions(+), 372 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 7d1c4622d50..d83b7261b5f 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -68,7 +68,11 @@ from .const import ( EVENT_DEVICE_ADDED_TO_REGISTRY, LOGGER, ) -from .helpers import async_enable_statistics, update_data_collection_preference +from .helpers import ( + async_enable_statistics, + async_get_node_from_device_id, + update_data_collection_preference, +) from .migrate import ( ZWaveMigrationData, async_get_migration_data, @@ -83,6 +87,7 @@ ID = "id" ENTRY_ID = "entry_id" ERR_NOT_LOADED = "not_loaded" NODE_ID = "node_id" +DEVICE_ID = "device_id" COMMAND_CLASS_ID = "command_class_id" TYPE = "type" PROPERTY = "property" @@ -254,21 +259,20 @@ def async_get_entry(orig_func: Callable) -> Callable: def async_get_node(orig_func: Callable) -> Callable: """Decorate async function to get node.""" - @async_get_entry @wraps(orig_func) async def async_get_node_func( - hass: HomeAssistant, - connection: ActiveConnection, - msg: dict, - entry: ConfigEntry, - client: Client, + hass: HomeAssistant, connection: ActiveConnection, msg: dict ) -> None: """Provide user specific data and store to function.""" - node_id = msg[NODE_ID] - node = client.driver.controller.nodes.get(node_id) + device_id = msg[DEVICE_ID] - if node is None: - connection.send_error(msg[ID], ERR_NOT_FOUND, f"Node {node_id} not found") + try: + node = async_get_node_from_device_id(hass, device_id) + except ValueError as err: + error_code = ERR_NOT_FOUND + if "loaded" in err.args[0]: + error_code = ERR_NOT_LOADED + connection.send_error(msg[ID], error_code, err.args[0]) return await orig_func(hass, connection, msg, node) @@ -299,13 +303,26 @@ def async_handle_failed_command(orig_func: Callable) -> Callable: return async_handle_failed_command_func +def node_status(node: Node) -> dict[str, Any]: + """Get node status.""" + return { + "node_id": node.node_id, + "is_routing": node.is_routing, + "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, + "is_controller_node": node.is_controller_node, + } + + @callback def async_register_api(hass: HomeAssistant) -> None: """Register all of our api endpoints.""" websocket_api.async_register_command(hass, websocket_network_status) websocket_api.async_register_command(hass, websocket_node_status) websocket_api.async_register_command(hass, websocket_node_metadata) - websocket_api.async_register_command(hass, websocket_ping_node) websocket_api.async_register_command(hass, websocket_add_node) websocket_api.async_register_command(hass, websocket_grant_security_classes) websocket_api.async_register_command(hass, websocket_validate_dsk_and_enter_pin) @@ -395,7 +412,9 @@ async def websocket_network_status( "supports_timers": controller.supports_timers, "is_heal_network_active": controller.is_heal_network_active, "inclusion_state": controller.inclusion_state, - "nodes": list(client.driver.controller.nodes), + "nodes": [ + node_status(node) for node in client.driver.controller.nodes.values() + ], }, } connection.send_result( @@ -407,8 +426,7 @@ async def websocket_network_status( @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/node_ready", - vol.Required(ENTRY_ID): str, - vol.Required(NODE_ID): int, + vol.Required(DEVICE_ID): str, } ) @websocket_api.async_response @@ -443,8 +461,7 @@ async def websocket_node_ready( @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/node_status", - vol.Required(ENTRY_ID): str, - vol.Required(NODE_ID): int, + vol.Required(DEVICE_ID): str, } ) @websocket_api.async_response @@ -456,27 +473,13 @@ async def websocket_node_status( node: Node, ) -> None: """Get the status of a Z-Wave JS node.""" - data = { - "node_id": node.node_id, - "is_routing": node.is_routing, - "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, - "is_controller_node": node.is_controller_node, - } - connection.send_result( - msg[ID], - data, - ) + connection.send_result(msg[ID], node_status(node)) @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/node_metadata", - vol.Required(ENTRY_ID): str, - vol.Required(NODE_ID): int, + vol.Required(DEVICE_ID): str, } ) @websocket_api.async_response @@ -504,30 +507,6 @@ async def websocket_node_metadata( ) -@websocket_api.websocket_command( - { - vol.Required(TYPE): "zwave_js/ping_node", - vol.Required(ENTRY_ID): str, - vol.Required(NODE_ID): int, - } -) -@websocket_api.async_response -@async_handle_failed_command -@async_get_node -async def websocket_ping_node( - hass: HomeAssistant, - connection: ActiveConnection, - msg: dict, - node: Node, -) -> None: - """Ping a Z-Wave JS node.""" - result = await node.async_ping() - connection.send_result( - msg[ID], - result, - ) - - @websocket_api.require_admin @websocket_api.websocket_command( { @@ -1189,23 +1168,20 @@ async def websocket_replace_failed_node( @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/remove_failed_node", - vol.Required(ENTRY_ID): str, - vol.Required(NODE_ID): int, + vol.Required(DEVICE_ID): str, } ) @websocket_api.async_response @async_handle_failed_command -@async_get_entry +@async_get_node async def websocket_remove_failed_node( hass: HomeAssistant, connection: ActiveConnection, msg: dict, - entry: ConfigEntry, - client: Client, + node: Node, ) -> None: """Remove a failed node from the Z-Wave network.""" - controller = client.driver.controller - node_id = msg[NODE_ID] + controller = node.client.driver.controller @callback def async_cleanup() -> None: @@ -1215,10 +1191,7 @@ async def websocket_remove_failed_node( @callback def node_removed(event: dict) -> None: - node = event["node"] - node_details = { - "node_id": node.node_id, - } + node_details = {"node_id": event["node"].node_id} connection.send_message( websocket_api.event_message( @@ -1229,7 +1202,7 @@ async def websocket_remove_failed_node( connection.subscriptions[msg["id"]] = async_cleanup msg[DATA_UNSUBSCRIBE] = unsubs = [controller.on("node removed", node_removed)] - result = await controller.async_remove_failed_node(node_id) + result = await controller.async_remove_failed_node(node.node_id) connection.send_result( msg[ID], result, @@ -1335,24 +1308,21 @@ async def websocket_stop_healing_network( @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/heal_node", - vol.Required(ENTRY_ID): str, - vol.Required(NODE_ID): int, + vol.Required(DEVICE_ID): str, } ) @websocket_api.async_response @async_handle_failed_command -@async_get_entry +@async_get_node async def websocket_heal_node( hass: HomeAssistant, connection: ActiveConnection, msg: dict, - entry: ConfigEntry, - client: Client, + node: Node, ) -> None: """Heal a node on the Z-Wave network.""" - controller = client.driver.controller - node_id = msg[NODE_ID] - result = await controller.async_heal_node(node_id) + controller = node.client.driver.controller + result = await controller.async_heal_node(node.node_id) connection.send_result( msg[ID], result, @@ -1363,8 +1333,7 @@ async def websocket_heal_node( @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/refresh_node_info", - vol.Required(ENTRY_ID): str, - vol.Required(NODE_ID): int, + vol.Required(DEVICE_ID): str, }, ) @websocket_api.async_response @@ -1414,8 +1383,7 @@ async def websocket_refresh_node_info( @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/refresh_node_values", - vol.Required(ENTRY_ID): str, - vol.Required(NODE_ID): int, + vol.Required(DEVICE_ID): str, }, ) @websocket_api.async_response @@ -1436,8 +1404,7 @@ async def websocket_refresh_node_values( @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/refresh_node_cc_values", - vol.Required(ENTRY_ID): str, - vol.Required(NODE_ID): int, + vol.Required(DEVICE_ID): str, vol.Required(COMMAND_CLASS_ID): int, }, ) @@ -1469,8 +1436,7 @@ async def websocket_refresh_node_cc_values( @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/set_config_parameter", - vol.Required(ENTRY_ID): str, - vol.Required(NODE_ID): int, + vol.Required(DEVICE_ID): str, vol.Required(PROPERTY): int, vol.Optional(PROPERTY_KEY): int, vol.Required(VALUE): vol.Any(int, BITMASK_SCHEMA), @@ -1521,8 +1487,7 @@ async def websocket_set_config_parameter( @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/get_config_parameters", - vol.Required(ENTRY_ID): str, - vol.Required(NODE_ID): int, + vol.Required(DEVICE_ID): str, } ) @websocket_api.async_response @@ -1762,8 +1727,7 @@ async def websocket_data_collection_status( @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/abort_firmware_update", - vol.Required(ENTRY_ID): str, - vol.Required(NODE_ID): int, + vol.Required(DEVICE_ID): str, } ) @websocket_api.async_response @@ -1794,8 +1758,7 @@ def _get_firmware_update_progress_dict( @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/subscribe_firmware_update_status", - vol.Required(ENTRY_ID): str, - vol.Required(NODE_ID): int, + vol.Required(DEVICE_ID): str, } ) @websocket_api.async_response @@ -2047,8 +2010,7 @@ def _get_node_statistics_dict(statistics: NodeStatistics) -> dict[str, int]: @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/subscribe_node_statistics", - vol.Required(ENTRY_ID): str, - vol.Required(NODE_ID): int, + vol.Required(DEVICE_ID): str, } ) @websocket_api.async_response diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index c4eb0396287..68ff0c89b15 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -179,24 +179,22 @@ def async_get_node_from_device_id( # Use device config entry ID's to validate that this is a valid zwave_js device # and to get the client config_entry_ids = device_entry.config_entries - config_entry_id = next( + entry = next( ( - config_entry_id - for config_entry_id in config_entry_ids - if cast( - ConfigEntry, - hass.config_entries.async_get_entry(config_entry_id), - ).domain - == DOMAIN + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.entry_id in config_entry_ids ), None, ) - if config_entry_id is None or config_entry_id not in hass.data[DOMAIN]: + if entry and entry.state != ConfigEntryState.LOADED: + raise ValueError(f"Device {device_id} config entry is not loaded") + if entry is None or entry.entry_id not in hass.data[DOMAIN]: raise ValueError( f"Device {device_id} is not from an existing zwave_js config entry" ) - client = hass.data[DOMAIN][config_entry_id][DATA_CLIENT] + client = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] # Get node ID from device identifier, perform some validation, and then get the # node @@ -390,21 +388,6 @@ def copy_available_params( ) -@callback -def async_is_device_config_entry_not_loaded( - hass: HomeAssistant, device_id: str -) -> bool: - """Return whether device's config entries are not loaded.""" - dev_reg = dr.async_get(hass) - if (device := dev_reg.async_get(device_id)) is None: - raise ValueError(f"Device {device_id} not found") - return any( - (entry := hass.config_entries.async_get_entry(entry_id)) - and entry.state != ConfigEntryState.LOADED - for entry_id in device.config_entries - ) - - def get_value_state_schema( value: ZwaveValue, ) -> vol.Schema | None: diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index d8a36fc6509..60b83630add 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -35,6 +35,7 @@ from homeassistant.components.zwave_js.api import ( CLIENT_SIDE_AUTH, COMMAND_CLASS_ID, CONFIG, + DEVICE_ID, DSK, ENABLED, ENTRY_ID, @@ -70,9 +71,17 @@ from homeassistant.components.zwave_js.const import ( CONF_DATA_COLLECTION_OPTED_IN, DOMAIN, ) +from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.helpers import device_registry as dr +def get_device(hass, node): + """Get device ID for a node.""" + dev_reg = dr.async_get(hass) + device_id = get_device_id(node.client, node) + return dev_reg.async_get_device({device_id}) + + async def test_network_status(hass, integration, hass_ws_client): """Test the network status websocket command.""" entry = integration @@ -117,12 +126,16 @@ async def test_node_ready( node.data["ready"] = False client.driver.controller.nodes[node.node_id] = node + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, identifiers={get_device_id(client, node)} + ) + await ws_client.send_json( { ID: 3, TYPE: "zwave_js/node_ready", - ENTRY_ID: entry.entry_id, - "node_id": node.node_id, + DEVICE_ID: device.id, } ) @@ -153,12 +166,12 @@ async def test_node_status(hass, multisensor_6, integration, hass_ws_client): ws_client = await hass_ws_client(hass) node = multisensor_6 + device = get_device(hass, node) await ws_client.send_json( { ID: 3, TYPE: "zwave_js/node_status", - ENTRY_ID: entry.entry_id, - NODE_ID: node.node_id, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -178,8 +191,7 @@ async def test_node_status(hass, multisensor_6, integration, hass_ws_client): { ID: 4, TYPE: "zwave_js/node_status", - ENTRY_ID: entry.entry_id, - NODE_ID: 99999, + DEVICE_ID: "fake_device", } ) msg = await ws_client.receive_json() @@ -194,8 +206,7 @@ async def test_node_status(hass, multisensor_6, integration, hass_ws_client): { ID: 5, TYPE: "zwave_js/node_status", - ENTRY_ID: entry.entry_id, - NODE_ID: node.node_id, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -210,12 +221,12 @@ async def test_node_metadata(hass, wallmote_central_scene, integration, hass_ws_ ws_client = await hass_ws_client(hass) node = wallmote_central_scene + device = get_device(hass, node) await ws_client.send_json( { ID: 3, TYPE: "zwave_js/node_metadata", - ENTRY_ID: entry.entry_id, - NODE_ID: node.node_id, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -256,8 +267,7 @@ async def test_node_metadata(hass, wallmote_central_scene, integration, hass_ws_ { ID: 4, TYPE: "zwave_js/node_metadata", - ENTRY_ID: entry.entry_id, - NODE_ID: 99999, + DEVICE_ID: "fake_device", } ) msg = await ws_client.receive_json() @@ -272,81 +282,7 @@ async def test_node_metadata(hass, wallmote_central_scene, integration, hass_ws_ { ID: 5, TYPE: "zwave_js/node_metadata", - ENTRY_ID: entry.entry_id, - NODE_ID: node.node_id, - } - ) - msg = await ws_client.receive_json() - - assert not msg["success"] - assert msg["error"]["code"] == ERR_NOT_LOADED - - -async def test_ping_node( - hass, wallmote_central_scene, integration, client, hass_ws_client -): - """Test the ping_node websocket command.""" - entry = integration - ws_client = await hass_ws_client(hass) - node = wallmote_central_scene - - client.async_send_command.return_value = {"responded": True} - - await ws_client.send_json( - { - ID: 3, - TYPE: "zwave_js/ping_node", - ENTRY_ID: entry.entry_id, - NODE_ID: node.node_id, - } - ) - - msg = await ws_client.receive_json() - assert msg["success"] - assert msg["result"] - - # Test getting non-existent node fails - await ws_client.send_json( - { - ID: 4, - TYPE: "zwave_js/ping_node", - ENTRY_ID: entry.entry_id, - NODE_ID: 99999, - } - ) - msg = await ws_client.receive_json() - assert not msg["success"] - assert msg["error"]["code"] == ERR_NOT_FOUND - - # Test FailedZWaveCommand is caught - with patch( - "zwave_js_server.model.node.Node.async_ping", - side_effect=FailedZWaveCommand("failed_command", 1, "error message"), - ): - await ws_client.send_json( - { - ID: 5, - TYPE: "zwave_js/ping_node", - ENTRY_ID: entry.entry_id, - NODE_ID: node.node_id, - } - ) - msg = await ws_client.receive_json() - - assert not msg["success"] - assert msg["error"]["code"] == "zwave_error" - assert msg["error"]["message"] == "Z-Wave error 1: error message" - - # Test sending command with not loaded entry fails - await hass.config_entries.async_unload(entry.entry_id) - await hass.async_block_till_done() - - await ws_client.send_json( - { - ID: 6, - TYPE: "zwave_js/ping_node", - ENTRY_ID: entry.entry_id, - NODE_ID: node.node_id, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -1812,19 +1748,38 @@ async def test_remove_failed_node( client, hass_ws_client, nortek_thermostat_removed_event, + nortek_thermostat_added_event, ): """Test the remove_failed_node websocket command.""" entry = integration ws_client = await hass_ws_client(hass) + device = get_device(hass, nortek_thermostat) client.async_send_command.return_value = {"success": True} + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_remove_failed_node", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/remove_failed_node", + DEVICE_ID: device.id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + await ws_client.send_json( { - ID: 3, + ID: 2, TYPE: "zwave_js/remove_failed_node", - ENTRY_ID: entry.entry_id, - NODE_ID: 67, + DEVICE_ID: device.id, } ) @@ -1846,29 +1801,15 @@ async def test_remove_failed_node( assert msg["event"]["event"] == "node removed" # Verify device was removed from device registry - device = dev_reg.async_get_device( - identifiers={(DOMAIN, "3245146787-67")}, - ) - assert device is None - - # Test FailedZWaveCommand is caught - with patch( - "zwave_js_server.model.controller.Controller.async_remove_failed_node", - side_effect=FailedZWaveCommand("failed_command", 1, "error message"), - ): - await ws_client.send_json( - { - ID: 4, - TYPE: "zwave_js/remove_failed_node", - ENTRY_ID: entry.entry_id, - NODE_ID: 67, - } + assert ( + dev_reg.async_get_device( + identifiers={(DOMAIN, "3245146787-67")}, ) - msg = await ws_client.receive_json() + is None + ) - assert not msg["success"] - assert msg["error"]["code"] == "zwave_error" - assert msg["error"]["message"] == "Z-Wave error 1: error message" + # Re-add node so we can test config entry not loaded + client.driver.receive_event(nortek_thermostat_added_event) # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) @@ -1876,10 +1817,9 @@ async def test_remove_failed_node( await ws_client.send_json( { - ID: 5, + ID: 3, TYPE: "zwave_js/remove_failed_node", - ENTRY_ID: entry.entry_id, - NODE_ID: 67, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -2091,6 +2031,7 @@ async def test_stop_healing_network( async def test_heal_node( hass, + multisensor_6, integration, client, hass_ws_client, @@ -2098,6 +2039,7 @@ async def test_heal_node( """Test the heal_node websocket command.""" entry = integration ws_client = await hass_ws_client(hass) + device = get_device(hass, multisensor_6) client.async_send_command.return_value = {"success": True} @@ -2105,8 +2047,7 @@ async def test_heal_node( { ID: 3, TYPE: "zwave_js/heal_node", - ENTRY_ID: entry.entry_id, - NODE_ID: 67, + DEVICE_ID: device.id, } ) @@ -2123,8 +2064,7 @@ async def test_heal_node( { ID: 4, TYPE: "zwave_js/heal_node", - ENTRY_ID: entry.entry_id, - NODE_ID: 67, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -2141,8 +2081,7 @@ async def test_heal_node( { ID: 5, TYPE: "zwave_js/heal_node", - ENTRY_ID: entry.entry_id, - NODE_ID: 67, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -2158,13 +2097,14 @@ async def test_refresh_node_info( entry = integration ws_client = await hass_ws_client(hass) + device = get_device(hass, multisensor_6) + client.async_send_command_no_wait.return_value = None await ws_client.send_json( { ID: 1, TYPE: "zwave_js/refresh_node_info", - ENTRY_ID: entry.entry_id, - NODE_ID: 52, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -2232,8 +2172,7 @@ async def test_refresh_node_info( { ID: 2, TYPE: "zwave_js/refresh_node_info", - ENTRY_ID: entry.entry_id, - NODE_ID: 9999, + DEVICE_ID: "fake_device", } ) msg = await ws_client.receive_json() @@ -2249,8 +2188,7 @@ async def test_refresh_node_info( { ID: 3, TYPE: "zwave_js/refresh_node_info", - ENTRY_ID: entry.entry_id, - NODE_ID: 52, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -2267,8 +2205,7 @@ async def test_refresh_node_info( { ID: 4, TYPE: "zwave_js/refresh_node_info", - ENTRY_ID: entry.entry_id, - NODE_ID: 52, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -2283,14 +2220,14 @@ async def test_refresh_node_values( """Test that the refresh_node_values WS API call works.""" entry = integration ws_client = await hass_ws_client(hass) + device = get_device(hass, multisensor_6) client.async_send_command_no_wait.return_value = None await ws_client.send_json( { ID: 1, TYPE: "zwave_js/refresh_node_values", - ENTRY_ID: entry.entry_id, - NODE_ID: 52, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -2303,26 +2240,12 @@ async def test_refresh_node_values( client.async_send_command_no_wait.reset_mock() - # Test getting non-existent node fails + # Test getting non-existent device fails await ws_client.send_json( { ID: 2, TYPE: "zwave_js/refresh_node_values", - ENTRY_ID: entry.entry_id, - NODE_ID: 99999, - } - ) - msg = await ws_client.receive_json() - assert not msg["success"] - assert msg["error"]["code"] == ERR_NOT_FOUND - - # Test getting non-existent entry fails - await ws_client.send_json( - { - ID: 3, - TYPE: "zwave_js/refresh_node_values", - ENTRY_ID: "fake_entry_id", - NODE_ID: 52, + DEVICE_ID: "fake_device", } ) msg = await ws_client.receive_json() @@ -2338,8 +2261,7 @@ async def test_refresh_node_values( { ID: 4, TYPE: "zwave_js/refresh_node_values", - ENTRY_ID: entry.entry_id, - NODE_ID: 52, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -2356,8 +2278,7 @@ async def test_refresh_node_values( { ID: 5, TYPE: "zwave_js/refresh_node_values", - ENTRY_ID: entry.entry_id, - NODE_ID: 52, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -2367,19 +2288,19 @@ async def test_refresh_node_values( async def test_refresh_node_cc_values( - hass, client, multisensor_6, integration, hass_ws_client + hass, multisensor_6, client, integration, hass_ws_client ): """Test that the refresh_node_cc_values WS API call works.""" entry = integration ws_client = await hass_ws_client(hass) + device = get_device(hass, multisensor_6) client.async_send_command_no_wait.return_value = None await ws_client.send_json( { ID: 1, TYPE: "zwave_js/refresh_node_cc_values", - ENTRY_ID: entry.entry_id, - NODE_ID: 52, + DEVICE_ID: device.id, COMMAND_CLASS_ID: 112, } ) @@ -2399,8 +2320,7 @@ async def test_refresh_node_cc_values( { ID: 2, TYPE: "zwave_js/refresh_node_cc_values", - ENTRY_ID: entry.entry_id, - NODE_ID: 52, + DEVICE_ID: device.id, COMMAND_CLASS_ID: 9999, } ) @@ -2408,13 +2328,12 @@ async def test_refresh_node_cc_values( assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_FOUND - # Test getting non-existent node fails + # Test getting non-existent device fails await ws_client.send_json( { ID: 3, TYPE: "zwave_js/refresh_node_cc_values", - ENTRY_ID: entry.entry_id, - NODE_ID: 9999, + DEVICE_ID: "fake_device", COMMAND_CLASS_ID: 112, } ) @@ -2431,8 +2350,7 @@ async def test_refresh_node_cc_values( { ID: 4, TYPE: "zwave_js/refresh_node_cc_values", - ENTRY_ID: entry.entry_id, - NODE_ID: 52, + DEVICE_ID: device.id, COMMAND_CLASS_ID: 112, } ) @@ -2450,8 +2368,7 @@ async def test_refresh_node_cc_values( { ID: 5, TYPE: "zwave_js/refresh_node_cc_values", - ENTRY_ID: entry.entry_id, - NODE_ID: 52, + DEVICE_ID: device.id, COMMAND_CLASS_ID: 112, } ) @@ -2462,11 +2379,12 @@ async def test_refresh_node_cc_values( async def test_set_config_parameter( - hass, client, hass_ws_client, multisensor_6, integration + hass, multisensor_6, client, hass_ws_client, integration ): """Test the set_config_parameter service.""" entry = integration ws_client = await hass_ws_client(hass) + device = get_device(hass, multisensor_6) client.async_send_command_no_wait.return_value = None @@ -2474,8 +2392,7 @@ async def test_set_config_parameter( { ID: 1, TYPE: "zwave_js/set_config_parameter", - ENTRY_ID: entry.entry_id, - NODE_ID: 52, + DEVICE_ID: device.id, PROPERTY: 102, PROPERTY_KEY: 1, VALUE: 1, @@ -2523,8 +2440,7 @@ async def test_set_config_parameter( { ID: 2, TYPE: "zwave_js/set_config_parameter", - ENTRY_ID: entry.entry_id, - NODE_ID: 52, + DEVICE_ID: device.id, PROPERTY: 102, PROPERTY_KEY: 1, VALUE: "0x1", @@ -2573,8 +2489,7 @@ async def test_set_config_parameter( { ID: 3, TYPE: "zwave_js/set_config_parameter", - ENTRY_ID: entry.entry_id, - NODE_ID: 52, + DEVICE_ID: device.id, PROPERTY: 102, PROPERTY_KEY: 1, VALUE: 1, @@ -2593,8 +2508,7 @@ async def test_set_config_parameter( { ID: 4, TYPE: "zwave_js/set_config_parameter", - ENTRY_ID: entry.entry_id, - NODE_ID: 52, + DEVICE_ID: device.id, PROPERTY: 102, PROPERTY_KEY: 1, VALUE: 1, @@ -2613,8 +2527,7 @@ async def test_set_config_parameter( { ID: 5, TYPE: "zwave_js/set_config_parameter", - ENTRY_ID: entry.entry_id, - NODE_ID: 52, + DEVICE_ID: device.id, PROPERTY: 102, PROPERTY_KEY: 1, VALUE: 1, @@ -2633,8 +2546,7 @@ async def test_set_config_parameter( { ID: 6, TYPE: "zwave_js/set_config_parameter", - ENTRY_ID: entry.entry_id, - NODE_ID: 9999, + DEVICE_ID: "fake_device", PROPERTY: 102, PROPERTY_KEY: 1, VALUE: 1, @@ -2653,8 +2565,7 @@ async def test_set_config_parameter( { ID: 7, TYPE: "zwave_js/set_config_parameter", - ENTRY_ID: entry.entry_id, - NODE_ID: 52, + DEVICE_ID: device.id, PROPERTY: 102, PROPERTY_KEY: 1, VALUE: 1, @@ -2674,8 +2585,7 @@ async def test_set_config_parameter( { ID: 8, TYPE: "zwave_js/set_config_parameter", - ENTRY_ID: entry.entry_id, - NODE_ID: 52, + DEVICE_ID: device.id, PROPERTY: 102, PROPERTY_KEY: 1, VALUE: 1, @@ -2693,14 +2603,14 @@ async def test_get_config_parameters(hass, multisensor_6, integration, hass_ws_c entry = integration ws_client = await hass_ws_client(hass) node = multisensor_6 + device = get_device(hass, node) # Test getting configuration parameter values await ws_client.send_json( { ID: 4, TYPE: "zwave_js/get_config_parameters", - ENTRY_ID: entry.entry_id, - NODE_ID: node.node_id, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -2722,8 +2632,7 @@ async def test_get_config_parameters(hass, multisensor_6, integration, hass_ws_c { ID: 5, TYPE: "zwave_js/get_config_parameters", - ENTRY_ID: entry.entry_id, - NODE_ID: 99999, + DEVICE_ID: "fake_device", } ) msg = await ws_client.receive_json() @@ -2738,8 +2647,7 @@ async def test_get_config_parameters(hass, multisensor_6, integration, hass_ws_c { ID: 6, TYPE: "zwave_js/get_config_parameters", - ENTRY_ID: entry.entry_id, - NODE_ID: node.node_id, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -3276,14 +3184,14 @@ async def test_abort_firmware_update( """Test that the abort_firmware_update WS API call works.""" entry = integration ws_client = await hass_ws_client(hass) + device = get_device(hass, multisensor_6) client.async_send_command_no_wait.return_value = {} await ws_client.send_json( { ID: 1, TYPE: "zwave_js/abort_firmware_update", - ENTRY_ID: entry.entry_id, - NODE_ID: multisensor_6.node_id, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -3303,8 +3211,7 @@ async def test_abort_firmware_update( { ID: 2, TYPE: "zwave_js/abort_firmware_update", - ENTRY_ID: entry.entry_id, - NODE_ID: multisensor_6.node_id, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -3321,8 +3228,7 @@ async def test_abort_firmware_update( { ID: 3, TYPE: "zwave_js/abort_firmware_update", - ENTRY_ID: entry.entry_id, - NODE_ID: multisensor_6.node_id, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -3332,32 +3238,19 @@ async def test_abort_firmware_update( async def test_abort_firmware_update_failures( - hass, integration, multisensor_6, client, hass_ws_client + hass, multisensor_6, client, integration, hass_ws_client ): """Test failures for the abort_firmware_update websocket command.""" entry = integration ws_client = await hass_ws_client(hass) - # Test sending command with improper entry ID fails - await ws_client.send_json( - { - ID: 1, - TYPE: "zwave_js/abort_firmware_update", - ENTRY_ID: "fake_entry_id", - NODE_ID: multisensor_6.node_id, - } - ) - msg = await ws_client.receive_json() + device = get_device(hass, multisensor_6) - assert not msg["success"] - assert msg["error"]["code"] == ERR_NOT_FOUND - - # Test sending command with improper node ID fails + # Test sending command with improper device ID fails await ws_client.send_json( { ID: 2, TYPE: "zwave_js/abort_firmware_update", - ENTRY_ID: entry.entry_id, - NODE_ID: multisensor_6.node_id + 100, + DEVICE_ID: "fake_device", } ) msg = await ws_client.receive_json() @@ -3373,8 +3266,7 @@ async def test_abort_firmware_update_failures( { ID: 3, TYPE: "zwave_js/abort_firmware_update", - ENTRY_ID: entry.entry_id, - NODE_ID: multisensor_6.node_id, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -3384,11 +3276,11 @@ async def test_abort_firmware_update_failures( async def test_subscribe_firmware_update_status( - hass, integration, multisensor_6, client, hass_ws_client + hass, multisensor_6, integration, client, hass_ws_client ): """Test the subscribe_firmware_update_status websocket command.""" - entry = integration ws_client = await hass_ws_client(hass) + device = get_device(hass, multisensor_6) client.async_send_command_no_wait.return_value = {} @@ -3396,8 +3288,7 @@ async def test_subscribe_firmware_update_status( { ID: 1, TYPE: "zwave_js/subscribe_firmware_update_status", - ENTRY_ID: entry.entry_id, - NODE_ID: multisensor_6.node_id, + DEVICE_ID: device.id, } ) @@ -3445,11 +3336,11 @@ async def test_subscribe_firmware_update_status( async def test_subscribe_firmware_update_status_initial_value( - hass, integration, multisensor_6, client, hass_ws_client + hass, multisensor_6, client, integration, hass_ws_client ): """Test subscribe_firmware_update_status websocket command with in progress update.""" - entry = integration ws_client = await hass_ws_client(hass) + device = get_device(hass, multisensor_6) assert multisensor_6.firmware_update_progress is None @@ -3472,8 +3363,7 @@ async def test_subscribe_firmware_update_status_initial_value( { ID: 1, TYPE: "zwave_js/subscribe_firmware_update_status", - ENTRY_ID: entry.entry_id, - NODE_ID: multisensor_6.node_id, + DEVICE_ID: device.id, } ) @@ -3490,32 +3380,18 @@ async def test_subscribe_firmware_update_status_initial_value( async def test_subscribe_firmware_update_status_failures( - hass, integration, multisensor_6, client, hass_ws_client + hass, multisensor_6, client, integration, hass_ws_client ): """Test failures for the subscribe_firmware_update_status websocket command.""" entry = integration ws_client = await hass_ws_client(hass) + device = get_device(hass, multisensor_6) # Test sending command with improper entry ID fails await ws_client.send_json( { ID: 1, TYPE: "zwave_js/subscribe_firmware_update_status", - ENTRY_ID: "fake_entry_id", - NODE_ID: multisensor_6.node_id, - } - ) - msg = await ws_client.receive_json() - - assert not msg["success"] - assert msg["error"]["code"] == ERR_NOT_FOUND - - # Test sending command with improper node ID fails - await ws_client.send_json( - { - ID: 2, - TYPE: "zwave_js/subscribe_firmware_update_status", - ENTRY_ID: entry.entry_id, - NODE_ID: multisensor_6.node_id + 100, + DEVICE_ID: "fake_device", } ) msg = await ws_client.receive_json() @@ -3531,8 +3407,7 @@ async def test_subscribe_firmware_update_status_failures( { ID: 3, TYPE: "zwave_js/subscribe_firmware_update_status", - ENTRY_ID: entry.entry_id, - NODE_ID: multisensor_6.node_id, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -3783,13 +3658,13 @@ async def test_subscribe_node_statistics( """Test the subscribe_node_statistics command.""" entry = integration ws_client = await hass_ws_client(hass) + device = get_device(hass, multisensor_6) await ws_client.send_json( { ID: 1, TYPE: "zwave_js/subscribe_node_statistics", - ENTRY_ID: entry.entry_id, - NODE_ID: multisensor_6.node_id, + DEVICE_ID: device.id, } ) @@ -3843,8 +3718,7 @@ async def test_subscribe_node_statistics( { ID: 2, TYPE: "zwave_js/subscribe_node_statistics", - ENTRY_ID: "fake_entry_id", - NODE_ID: multisensor_6.node_id, + DEVICE_ID: "fake_device", } ) msg = await ws_client.receive_json() @@ -3852,17 +3726,6 @@ async def test_subscribe_node_statistics( assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_FOUND - # Test sending command with improper node ID fails - await ws_client.send_json( - { - ID: 3, - TYPE: "zwave_js/subscribe_node_statistics", - ENTRY_ID: entry.entry_id, - NODE_ID: multisensor_6.node_id + 100, - } - ) - msg = await ws_client.receive_json() - # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() @@ -3871,8 +3734,7 @@ async def test_subscribe_node_statistics( { ID: 4, TYPE: "zwave_js/subscribe_node_statistics", - ENTRY_ID: entry.entry_id, - NODE_ID: multisensor_6.node_id, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() From a57697d6e98a5a267f8eb60cb333b6a8dcc360c3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 May 2022 21:16:06 +0200 Subject: [PATCH 0696/3516] Adjust device_automation type hints in deconz (#72194) --- homeassistant/components/deconz/device_trigger.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 43ed3b2cdc4..2ab1b2e6544 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -1,16 +1,16 @@ """Provides device automations for deconz events.""" - from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, ) -from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import ( + DEVICE_TRIGGER_BASE_SCHEMA, + GetAutomationsResult, +) from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -642,9 +642,8 @@ def _get_deconz_event_from_device( async def async_validate_trigger_config( - hass: HomeAssistant, - config: dict[str, Any], -) -> vol.Schema: + hass: HomeAssistant, config: ConfigType +) -> ConfigType: """Validate config.""" config = TRIGGER_SCHEMA(config) @@ -703,7 +702,7 @@ async def async_attach_trigger( async def async_get_triggers( hass: HomeAssistant, device_id: str, -) -> list[dict[str, str]]: +) -> GetAutomationsResult: """List device triggers. Make sure device is a supported remote model. From 700552689270020eb56bf43de937554ba5755929 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 May 2022 21:33:01 +0200 Subject: [PATCH 0697/3516] Adjust device_automation type hints in webostv (#72200) --- homeassistant/components/webostv/device_trigger.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/webostv/device_trigger.py b/homeassistant/components/webostv/device_trigger.py index feb5bae98fe..9836f561b9d 100644 --- a/homeassistant/components/webostv/device_trigger.py +++ b/homeassistant/components/webostv/device_trigger.py @@ -7,7 +7,10 @@ from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, ) -from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import ( + DEVICE_TRIGGER_BASE_SCHEMA, + GetAutomationsResult, +) from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -58,7 +61,7 @@ async def async_validate_trigger_config( async def async_get_triggers( _hass: HomeAssistant, device_id: str -) -> list[dict[str, str]]: +) -> GetAutomationsResult: """List device triggers for device.""" triggers = [] base_trigger = { @@ -77,7 +80,7 @@ async def async_attach_trigger( config: ConfigType, action: AutomationActionType, automation_info: AutomationTriggerInfo, -) -> CALLBACK_TYPE | None: +) -> CALLBACK_TYPE: """Attach a trigger.""" if (trigger_type := config[CONF_TYPE]) == TURN_ON_PLATFORM_TYPE: trigger_config = { From 7ce3e9ad7b3834f8c054058d9c42f1e51b687b1f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 May 2022 21:33:50 +0200 Subject: [PATCH 0698/3516] Adjust device_automation type hints in shelly (#72196) --- homeassistant/components/shelly/device_trigger.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py index ae2eb7d6440..1231f670d49 100644 --- a/homeassistant/components/shelly/device_trigger.py +++ b/homeassistant/components/shelly/device_trigger.py @@ -1,7 +1,7 @@ """Provides device triggers for Shelly.""" from __future__ import annotations -from typing import Any, Final +from typing import Final import voluptuous as vol @@ -9,7 +9,10 @@ from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, ) -from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import ( + DEVICE_TRIGGER_BASE_SCHEMA, + GetAutomationsResult, +) from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -72,8 +75,8 @@ def append_input_triggers( async def async_validate_trigger_config( - hass: HomeAssistant, config: dict[str, Any] -) -> dict[str, Any]: + hass: HomeAssistant, config: ConfigType +) -> ConfigType: """Validate config.""" config = TRIGGER_SCHEMA(config) @@ -108,9 +111,9 @@ async def async_validate_trigger_config( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, str]]: +) -> GetAutomationsResult: """List device triggers for Shelly devices.""" - triggers: list[dict[str, str]] = [] + triggers: GetAutomationsResult = [] if rpc_wrapper := get_rpc_device_wrapper(hass, device_id): input_triggers = get_rpc_input_triggers(rpc_wrapper.device) From 3604bb4c667e547b962ad50b70d3a4063c63dc47 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 May 2022 22:21:43 +0200 Subject: [PATCH 0699/3516] Adjust device_automation type hints in lutron_caseta (#72133) --- .../components/lutron_caseta/device_trigger.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index 4974a10d9ba..b6f01a5e49e 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -1,15 +1,16 @@ """Provides device triggers for lutron caseta.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, ) -from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import ( + DEVICE_TRIGGER_BASE_SCHEMA, + GetAutomationsResult, +) from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -329,7 +330,9 @@ TRIGGER_SCHEMA = vol.Any( ) -async def async_validate_trigger_config(hass: HomeAssistant, config: ConfigType): +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: """Validate config.""" # if device is available verify parameters against device capabilities device = get_button_device_by_dr_id(hass, config[CONF_DEVICE_ID]) @@ -347,14 +350,14 @@ async def async_validate_trigger_config(hass: HomeAssistant, config: ConfigType) async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> GetAutomationsResult: """List device triggers for lutron caseta devices.""" triggers = [] if not (device := get_button_device_by_dr_id(hass, device_id)): raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}") - valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LIP.get(device["type"], []) + valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LIP.get(device["type"], {}) for trigger in SUPPORTED_INPUTS_EVENTS_TYPES: for subtype in valid_buttons: From ebc883b43ff9ca00d3426bbd0bdfdab707576252 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 19 May 2022 22:22:09 +0200 Subject: [PATCH 0700/3516] Adjust device_automation type hints in homekit_controller (#72199) --- .../components/homekit_controller/device_trigger.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index dcac7238c8e..5c46aee6ca2 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -14,7 +14,10 @@ from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, ) -from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import ( + DEVICE_TRIGGER_BASE_SCHEMA, + GetAutomationsResult, +) from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.typing import ConfigType @@ -241,7 +244,7 @@ def async_fire_triggers(conn: HKDevice, events: dict[tuple[int, int], Any]): async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, str]]: +) -> GetAutomationsResult: """List device triggers for homekit devices.""" if device_id not in hass.data.get(TRIGGERS, {}): From 68b278d17099b50f46c5f6803ffdbb2c628ef05b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 19 May 2022 18:29:28 -0400 Subject: [PATCH 0701/3516] Remove legacy zwave migration logic (#72206) * Remove legacy zwave migration logic * Update test_migrate.py --- homeassistant/components/zwave_js/api.py | 77 --- homeassistant/components/zwave_js/entity.py | 6 - homeassistant/components/zwave_js/migrate.py | 338 +------------- tests/components/zwave_js/test_migrate.py | 466 ------------------- 4 files changed, 4 insertions(+), 883 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index d83b7261b5f..d27541fc61c 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -66,19 +66,12 @@ from .const import ( DATA_CLIENT, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY, - LOGGER, ) from .helpers import ( async_enable_statistics, async_get_node_from_device_id, update_data_collection_preference, ) -from .migrate import ( - ZWaveMigrationData, - async_get_migration_data, - async_map_legacy_zwave_values, - async_migrate_legacy_zwave, -) DATA_UNSUBSCRIBE = "unsubs" @@ -365,7 +358,6 @@ def async_register_api(hass: HomeAssistant) -> None: ) websocket_api.async_register_command(hass, websocket_subscribe_node_statistics) websocket_api.async_register_command(hass, websocket_node_ready) - websocket_api.async_register_command(hass, websocket_migrate_zwave) hass.http.register_view(FirmwareUploadView()) @@ -2059,72 +2051,3 @@ async def websocket_subscribe_node_statistics( }, ) ) - - -@websocket_api.require_admin -@websocket_api.websocket_command( - { - vol.Required(TYPE): "zwave_js/migrate_zwave", - vol.Required(ENTRY_ID): str, - vol.Optional(DRY_RUN, default=True): bool, - } -) -@websocket_api.async_response -@async_get_entry -async def websocket_migrate_zwave( - hass: HomeAssistant, - connection: ActiveConnection, - msg: dict, - entry: ConfigEntry, - client: Client, -) -> None: - """Migrate Z-Wave device and entity data to Z-Wave JS integration.""" - if "zwave" not in hass.config.components: - connection.send_message( - websocket_api.error_message( - msg["id"], "zwave_not_loaded", "Integration zwave is not loaded" - ) - ) - return - - zwave = hass.components.zwave - zwave_config_entries = hass.config_entries.async_entries("zwave") - zwave_config_entry = zwave_config_entries[0] # zwave only has a single config entry - zwave_data: dict[str, ZWaveMigrationData] = await zwave.async_get_migration_data( - hass, zwave_config_entry - ) - LOGGER.debug("Migration zwave data: %s", zwave_data) - - zwave_js_config_entry = entry - zwave_js_data = await async_get_migration_data(hass, zwave_js_config_entry) - LOGGER.debug("Migration zwave_js data: %s", zwave_js_data) - - migration_map = async_map_legacy_zwave_values(zwave_data, zwave_js_data) - - zwave_entity_ids = [entry["entity_id"] for entry in zwave_data.values()] - zwave_js_entity_ids = [entry["entity_id"] for entry in zwave_js_data.values()] - migration_device_map = { - zwave_device_id: zwave_js_device_id - for zwave_js_device_id, zwave_device_id in migration_map.device_entries.items() - } - migration_entity_map = { - zwave_entry["entity_id"]: zwave_js_entity_id - for zwave_js_entity_id, zwave_entry in migration_map.entity_entries.items() - } - LOGGER.debug("Migration entity map: %s", migration_entity_map) - - if not msg[DRY_RUN]: - await async_migrate_legacy_zwave( - hass, zwave_config_entry, zwave_js_config_entry, migration_map - ) - - connection.send_result( - msg[ID], - { - "migration_device_map": migration_device_map, - "zwave_entity_ids": zwave_entity_ids, - "zwave_js_entity_ids": zwave_js_entity_ids, - "migration_entity_map": migration_entity_map, - "migrated": not msg[DRY_RUN], - }, - ) diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index c6ed1902568..b89b4c9c9de 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -15,7 +15,6 @@ from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN from .discovery import ZwaveDiscoveryInfo from .helpers import get_device_id, get_unique_id -from .migrate import async_add_migration_entity_value LOGGER = logging.getLogger(__name__) @@ -117,11 +116,6 @@ class ZWaveBaseEntity(Entity): ) ) - # Add legacy Z-Wave migration data. - await async_add_migration_entity_value( - self.hass, self.config_entry, self.entity_id, self.info - ) - def generate_name( self, include_value_name: bool = False, diff --git a/homeassistant/components/zwave_js/migrate.py b/homeassistant/components/zwave_js/migrate.py index 204b5d0aebd..1413bf8e5b4 100644 --- a/homeassistant/components/zwave_js/migrate.py +++ b/homeassistant/components/zwave_js/migrate.py @@ -1,357 +1,27 @@ """Functions used to migrate unique IDs for Z-Wave JS entities.""" from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass import logging -from typing import TypedDict, cast from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.value import Value as ZwaveValue -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import LIGHT_LUX, STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import ( - DeviceEntry, - async_get as async_get_device_registry, -) +from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.entity_registry import ( EntityRegistry, RegistryEntry, async_entries_for_device, - async_get as async_get_entity_registry, ) -from homeassistant.helpers.singleton import singleton -from homeassistant.helpers.storage import Store from .const import DOMAIN from .discovery import ZwaveDiscoveryInfo -from .helpers import get_device_id, get_unique_id +from .helpers import get_unique_id _LOGGER = logging.getLogger(__name__) -LEGACY_ZWAVE_MIGRATION = f"{DOMAIN}_legacy_zwave_migration" -MIGRATED = "migrated" -STORAGE_WRITE_DELAY = 30 -STORAGE_KEY = f"{DOMAIN}.legacy_zwave_migration" -STORAGE_VERSION = 1 - -NOTIFICATION_CC_LABEL_TO_PROPERTY_NAME = { - "Smoke": "Smoke Alarm", - "Carbon Monoxide": "CO Alarm", - "Carbon Dioxide": "CO2 Alarm", - "Heat": "Heat Alarm", - "Flood": "Water Alarm", - "Access Control": "Access Control", - "Burglar": "Home Security", - "Power Management": "Power Management", - "System": "System", - "Emergency": "Siren", - "Clock": "Clock", - "Appliance": "Appliance", - "HomeHealth": "Home Health", -} - -SENSOR_MULTILEVEL_CC_LABEL_TO_PROPERTY_NAME = { - "Temperature": "Air temperature", - "General": "General purpose", - "Luminance": "Illuminance", - "Power": "Power", - "Relative Humidity": "Humidity", - "Velocity": "Velocity", - "Direction": "Direction", - "Atmospheric Pressure": "Atmospheric pressure", - "Barometric Pressure": "Barometric pressure", - "Solar Radiation": "Solar radiation", - "Dew Point": "Dew point", - "Rain Rate": "Rain rate", - "Tide Level": "Tide level", - "Weight": "Weight", - "Voltage": "Voltage", - "Current": "Current", - "CO2 Level": "Carbon dioxide (CO₂) level", - "Air Flow": "Air flow", - "Tank Capacity": "Tank capacity", - "Distance": "Distance", - "Angle Position": "Angle position", - "Rotation": "Rotation", - "Water Temperature": "Water temperature", - "Soil Temperature": "Soil temperature", - "Seismic Intensity": "Seismic Intensity", - "Seismic Magnitude": "Seismic magnitude", - "Ultraviolet": "Ultraviolet", - "Electrical Resistivity": "Electrical resistivity", - "Electrical Conductivity": "Electrical conductivity", - "Loudness": "Loudness", - "Moisture": "Moisture", -} - -CC_ID_LABEL_TO_PROPERTY = { - 49: SENSOR_MULTILEVEL_CC_LABEL_TO_PROPERTY_NAME, - 113: NOTIFICATION_CC_LABEL_TO_PROPERTY_NAME, -} - -UNIT_LEGACY_MIGRATION_MAP = {LIGHT_LUX: "Lux"} - - -class ZWaveMigrationData(TypedDict): - """Represent the Z-Wave migration data dict.""" - - node_id: int - node_instance: int - command_class: int - command_class_label: str - value_index: int - device_id: str - domain: str - entity_id: str - unique_id: str - unit_of_measurement: str | None - - -class ZWaveJSMigrationData(TypedDict): - """Represent the Z-Wave JS migration data dict.""" - - node_id: int - endpoint_index: int - command_class: int - value_property_name: str - value_property_key_name: str | None - value_id: str - device_id: str - domain: str - entity_id: str - unique_id: str - unit_of_measurement: str | None - - -@dataclass -class LegacyZWaveMappedData: - """Represent the mapped data between Z-Wave and Z-Wave JS.""" - - entity_entries: dict[str, ZWaveMigrationData] = field(default_factory=dict) - device_entries: dict[str, str] = field(default_factory=dict) - - -async def async_add_migration_entity_value( - hass: HomeAssistant, - config_entry: ConfigEntry, - entity_id: str, - discovery_info: ZwaveDiscoveryInfo, -) -> None: - """Add Z-Wave JS entity value for legacy Z-Wave migration.""" - migration_handler: LegacyZWaveMigration = await get_legacy_zwave_migration(hass) - migration_handler.add_entity_value(config_entry, entity_id, discovery_info) - - -async def async_get_migration_data( - hass: HomeAssistant, config_entry: ConfigEntry -) -> dict[str, ZWaveJSMigrationData]: - """Return Z-Wave JS migration data.""" - migration_handler: LegacyZWaveMigration = await get_legacy_zwave_migration(hass) - return await migration_handler.get_data(config_entry) - - -@singleton(LEGACY_ZWAVE_MIGRATION) -async def get_legacy_zwave_migration(hass: HomeAssistant) -> LegacyZWaveMigration: - """Return legacy Z-Wave migration handler.""" - migration_handler = LegacyZWaveMigration(hass) - await migration_handler.load_data() - return migration_handler - - -class LegacyZWaveMigration: - """Handle the migration from zwave to zwave_js.""" - - def __init__(self, hass: HomeAssistant) -> None: - """Set up migration instance.""" - self._hass = hass - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) - self._data: dict[str, dict[str, ZWaveJSMigrationData]] = {} - - async def load_data(self) -> None: - """Load Z-Wave JS migration data.""" - stored = cast(dict, await self._store.async_load()) - if stored: - self._data = stored - - @callback - def save_data( - self, config_entry_id: str, entity_id: str, data: ZWaveJSMigrationData - ) -> None: - """Save Z-Wave JS migration data.""" - if config_entry_id not in self._data: - self._data[config_entry_id] = {} - self._data[config_entry_id][entity_id] = data - self._store.async_delay_save(self._data_to_save, STORAGE_WRITE_DELAY) - - @callback - def _data_to_save(self) -> dict[str, dict[str, ZWaveJSMigrationData]]: - """Return data to save.""" - return self._data - - @callback - def add_entity_value( - self, - config_entry: ConfigEntry, - entity_id: str, - discovery_info: ZwaveDiscoveryInfo, - ) -> None: - """Add info for one entity and Z-Wave JS value.""" - ent_reg = async_get_entity_registry(self._hass) - dev_reg = async_get_device_registry(self._hass) - - node = discovery_info.node - primary_value = discovery_info.primary_value - entity_entry = ent_reg.async_get(entity_id) - assert entity_entry - device_identifier = get_device_id(node.client, node) - device_entry = dev_reg.async_get_device({device_identifier}, set()) - assert device_entry - - # Normalize unit of measurement. - if unit := entity_entry.unit_of_measurement: - _unit = UNIT_LEGACY_MIGRATION_MAP.get(unit, unit) - unit = _unit.lower() - if unit == "": - unit = None - - data: ZWaveJSMigrationData = { - "node_id": node.node_id, - "endpoint_index": node.index, - "command_class": primary_value.command_class, - "value_property_name": primary_value.property_name, - "value_property_key_name": primary_value.property_key_name, - "value_id": primary_value.value_id, - "device_id": device_entry.id, - "domain": entity_entry.domain, - "entity_id": entity_id, - "unique_id": entity_entry.unique_id, - "unit_of_measurement": unit, - } - - self.save_data(config_entry.entry_id, entity_id, data) - - async def get_data( - self, config_entry: ConfigEntry - ) -> dict[str, ZWaveJSMigrationData]: - """Return Z-Wave JS migration data for a config entry.""" - await self.load_data() - data = self._data.get(config_entry.entry_id) - return data or {} - - -@callback -def async_map_legacy_zwave_values( - zwave_data: dict[str, ZWaveMigrationData], - zwave_js_data: dict[str, ZWaveJSMigrationData], -) -> LegacyZWaveMappedData: - """Map Z-Wave node values onto Z-Wave JS node values.""" - migration_map = LegacyZWaveMappedData() - zwave_proc_data: dict[ - tuple[int, int, int, str, str | None, str | None], - ZWaveMigrationData | None, - ] = {} - zwave_js_proc_data: dict[ - tuple[int, int, int, str, str | None, str | None], - ZWaveJSMigrationData | None, - ] = {} - - for zwave_item in zwave_data.values(): - zwave_js_property_name = CC_ID_LABEL_TO_PROPERTY.get( - zwave_item["command_class"], {} - ).get(zwave_item["command_class_label"]) - item_id = ( - zwave_item["node_id"], - zwave_item["command_class"], - zwave_item["node_instance"] - 1, - zwave_item["domain"], - zwave_item["unit_of_measurement"], - zwave_js_property_name, - ) - - # Filter out duplicates that are not resolvable. - if item_id in zwave_proc_data: - zwave_proc_data[item_id] = None - continue - - zwave_proc_data[item_id] = zwave_item - - for zwave_js_item in zwave_js_data.values(): - # Only identify with property name if there is a command class label map. - if zwave_js_item["command_class"] in CC_ID_LABEL_TO_PROPERTY: - zwave_js_property_name = zwave_js_item["value_property_name"] - else: - zwave_js_property_name = None - item_id = ( - zwave_js_item["node_id"], - zwave_js_item["command_class"], - zwave_js_item["endpoint_index"], - zwave_js_item["domain"], - zwave_js_item["unit_of_measurement"], - zwave_js_property_name, - ) - - # Filter out duplicates that are not resolvable. - if item_id in zwave_js_proc_data: - zwave_js_proc_data[item_id] = None - continue - - zwave_js_proc_data[item_id] = zwave_js_item - - for item_id, zwave_entry in zwave_proc_data.items(): - zwave_js_entry = zwave_js_proc_data.pop(item_id, None) - - if zwave_entry is None or zwave_js_entry is None: - continue - - migration_map.entity_entries[zwave_js_entry["entity_id"]] = zwave_entry - migration_map.device_entries[zwave_js_entry["device_id"]] = zwave_entry[ - "device_id" - ] - - return migration_map - - -async def async_migrate_legacy_zwave( - hass: HomeAssistant, - zwave_config_entry: ConfigEntry, - zwave_js_config_entry: ConfigEntry, - migration_map: LegacyZWaveMappedData, -) -> None: - """Perform Z-Wave to Z-Wave JS migration.""" - dev_reg = async_get_device_registry(hass) - for zwave_js_device_id, zwave_device_id in migration_map.device_entries.items(): - zwave_device_entry = dev_reg.async_get(zwave_device_id) - if not zwave_device_entry: - continue - dev_reg.async_update_device( - zwave_js_device_id, - area_id=zwave_device_entry.area_id, - name_by_user=zwave_device_entry.name_by_user, - ) - - 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"] - if not (entity_entry := ent_reg.async_get(zwave_entity_id)): - continue - ent_reg.async_remove(zwave_entity_id) - ent_reg.async_update_entity( - zwave_js_entity_id, - new_entity_id=entity_entry.entity_id, - name=entity_entry.name, - icon=entity_entry.icon, - ) - - await hass.config_entries.async_remove(zwave_config_entry.entry_id) - - updates = { - **zwave_js_config_entry.data, - MIGRATED: True, - } - hass.config_entries.async_update_entry(zwave_js_config_entry, data=updates) - @dataclass class ValueID: diff --git a/tests/components/zwave_js/test_migrate.py b/tests/components/zwave_js/test_migrate.py index 79201ebbede..37c53700d95 100644 --- a/tests/components/zwave_js/test_migrate.py +++ b/tests/components/zwave_js/test_migrate.py @@ -1,481 +1,15 @@ """Test the Z-Wave JS migration module.""" import copy -from unittest.mock import patch import pytest from zwave_js_server.model.node import Node -from homeassistant.components.zwave_js.api import ENTRY_ID, ID, TYPE from homeassistant.components.zwave_js.const import DOMAIN from homeassistant.components.zwave_js.helpers import get_device_id -from homeassistant.const import LIGHT_LUX from homeassistant.helpers import device_registry as dr, entity_registry as er from .common import AIR_TEMPERATURE_SENSOR, NOTIFICATION_MOTION_BINARY_SENSOR -from tests.common import MockConfigEntry, mock_device_registry, mock_registry - -# Switch device -ZWAVE_SWITCH_DEVICE_ID = "zwave_switch_device_id" -ZWAVE_SWITCH_DEVICE_NAME = "Z-Wave Switch Device" -ZWAVE_SWITCH_DEVICE_AREA = "Z-Wave Switch Area" -ZWAVE_SWITCH_ENTITY = "switch.zwave_switch_node" -ZWAVE_SWITCH_UNIQUE_ID = "102-6789" -ZWAVE_SWITCH_NAME = "Z-Wave Switch" -ZWAVE_SWITCH_ICON = "mdi:zwave-test-switch" -ZWAVE_POWER_ENTITY = "sensor.zwave_power" -ZWAVE_POWER_UNIQUE_ID = "102-5678" -ZWAVE_POWER_NAME = "Z-Wave Power" -ZWAVE_POWER_ICON = "mdi:zwave-test-power" - -# Multisensor device -ZWAVE_MULTISENSOR_DEVICE_ID = "zwave_multisensor_device_id" -ZWAVE_MULTISENSOR_DEVICE_NAME = "Z-Wave Multisensor Device" -ZWAVE_MULTISENSOR_DEVICE_AREA = "Z-Wave Multisensor Area" -ZWAVE_SOURCE_NODE_ENTITY = "sensor.zwave_source_node" -ZWAVE_SOURCE_NODE_UNIQUE_ID = "52-4321" -ZWAVE_LUMINANCE_ENTITY = "sensor.zwave_luminance" -ZWAVE_LUMINANCE_UNIQUE_ID = "52-6543" -ZWAVE_LUMINANCE_NAME = "Z-Wave Luminance" -ZWAVE_LUMINANCE_ICON = "mdi:zwave-test-luminance" -ZWAVE_BATTERY_ENTITY = "sensor.zwave_battery_level" -ZWAVE_BATTERY_UNIQUE_ID = "52-1234" -ZWAVE_BATTERY_NAME = "Z-Wave Battery Level" -ZWAVE_BATTERY_ICON = "mdi:zwave-test-battery" -ZWAVE_TAMPERING_ENTITY = "sensor.zwave_tampering" -ZWAVE_TAMPERING_UNIQUE_ID = "52-3456" -ZWAVE_TAMPERING_NAME = "Z-Wave Tampering" -ZWAVE_TAMPERING_ICON = "mdi:zwave-test-tampering" - - -@pytest.fixture(name="zwave_migration_data") -def zwave_migration_data_fixture(hass): - """Return mock zwave migration data.""" - zwave_switch_device = dr.DeviceEntry( - id=ZWAVE_SWITCH_DEVICE_ID, - name_by_user=ZWAVE_SWITCH_DEVICE_NAME, - area_id=ZWAVE_SWITCH_DEVICE_AREA, - ) - zwave_switch_entry = er.RegistryEntry( - entity_id=ZWAVE_SWITCH_ENTITY, - unique_id=ZWAVE_SWITCH_UNIQUE_ID, - platform="zwave", - name=ZWAVE_SWITCH_NAME, - icon=ZWAVE_SWITCH_ICON, - ) - zwave_multisensor_device = dr.DeviceEntry( - id=ZWAVE_MULTISENSOR_DEVICE_ID, - name_by_user=ZWAVE_MULTISENSOR_DEVICE_NAME, - area_id=ZWAVE_MULTISENSOR_DEVICE_AREA, - ) - zwave_source_node_entry = er.RegistryEntry( - entity_id=ZWAVE_SOURCE_NODE_ENTITY, - unique_id=ZWAVE_SOURCE_NODE_UNIQUE_ID, - platform="zwave", - name="Z-Wave Source Node", - ) - zwave_luminance_entry = er.RegistryEntry( - entity_id=ZWAVE_LUMINANCE_ENTITY, - unique_id=ZWAVE_LUMINANCE_UNIQUE_ID, - platform="zwave", - name=ZWAVE_LUMINANCE_NAME, - icon=ZWAVE_LUMINANCE_ICON, - unit_of_measurement="lux", - ) - zwave_battery_entry = er.RegistryEntry( - entity_id=ZWAVE_BATTERY_ENTITY, - unique_id=ZWAVE_BATTERY_UNIQUE_ID, - platform="zwave", - name=ZWAVE_BATTERY_NAME, - icon=ZWAVE_BATTERY_ICON, - unit_of_measurement="%", - ) - zwave_power_entry = er.RegistryEntry( - entity_id=ZWAVE_POWER_ENTITY, - unique_id=ZWAVE_POWER_UNIQUE_ID, - platform="zwave", - name=ZWAVE_POWER_NAME, - icon=ZWAVE_POWER_ICON, - unit_of_measurement="W", - ) - zwave_tampering_entry = er.RegistryEntry( - entity_id=ZWAVE_TAMPERING_ENTITY, - unique_id=ZWAVE_TAMPERING_UNIQUE_ID, - platform="zwave", - name=ZWAVE_TAMPERING_NAME, - icon=ZWAVE_TAMPERING_ICON, - unit_of_measurement="", # Test empty string unit normalization. - ) - - zwave_migration_data = { - ZWAVE_SWITCH_ENTITY: { - "node_id": 102, - "node_instance": 1, - "command_class": 37, - "command_class_label": "", - "value_index": 1, - "device_id": zwave_switch_device.id, - "domain": zwave_switch_entry.domain, - "entity_id": zwave_switch_entry.entity_id, - "unique_id": ZWAVE_SWITCH_UNIQUE_ID, - "unit_of_measurement": zwave_switch_entry.unit_of_measurement, - }, - ZWAVE_POWER_ENTITY: { - "node_id": 102, - "node_instance": 1, - "command_class": 50, - "command_class_label": "Power", - "value_index": 8, - "device_id": zwave_switch_device.id, - "domain": zwave_power_entry.domain, - "entity_id": zwave_power_entry.entity_id, - "unique_id": ZWAVE_POWER_UNIQUE_ID, - "unit_of_measurement": zwave_power_entry.unit_of_measurement, - }, - ZWAVE_SOURCE_NODE_ENTITY: { - "node_id": 52, - "node_instance": 1, - "command_class": 113, - "command_class_label": "SourceNodeId", - "value_index": 1, - "device_id": zwave_multisensor_device.id, - "domain": zwave_source_node_entry.domain, - "entity_id": zwave_source_node_entry.entity_id, - "unique_id": ZWAVE_SOURCE_NODE_UNIQUE_ID, - "unit_of_measurement": zwave_source_node_entry.unit_of_measurement, - }, - ZWAVE_LUMINANCE_ENTITY: { - "node_id": 52, - "node_instance": 1, - "command_class": 49, - "command_class_label": "Luminance", - "value_index": 3, - "device_id": zwave_multisensor_device.id, - "domain": zwave_luminance_entry.domain, - "entity_id": zwave_luminance_entry.entity_id, - "unique_id": ZWAVE_LUMINANCE_UNIQUE_ID, - "unit_of_measurement": zwave_luminance_entry.unit_of_measurement, - }, - ZWAVE_BATTERY_ENTITY: { - "node_id": 52, - "node_instance": 1, - "command_class": 128, - "command_class_label": "Battery Level", - "value_index": 0, - "device_id": zwave_multisensor_device.id, - "domain": zwave_battery_entry.domain, - "entity_id": zwave_battery_entry.entity_id, - "unique_id": ZWAVE_BATTERY_UNIQUE_ID, - "unit_of_measurement": zwave_battery_entry.unit_of_measurement, - }, - ZWAVE_TAMPERING_ENTITY: { - "node_id": 52, - "node_instance": 1, - "command_class": 113, - "command_class_label": "Burglar", - "value_index": 10, - "device_id": zwave_multisensor_device.id, - "domain": zwave_tampering_entry.domain, - "entity_id": zwave_tampering_entry.entity_id, - "unique_id": ZWAVE_TAMPERING_UNIQUE_ID, - "unit_of_measurement": zwave_tampering_entry.unit_of_measurement, - }, - } - - mock_device_registry( - hass, - { - zwave_switch_device.id: zwave_switch_device, - zwave_multisensor_device.id: zwave_multisensor_device, - }, - ) - mock_registry( - hass, - { - ZWAVE_SWITCH_ENTITY: zwave_switch_entry, - ZWAVE_SOURCE_NODE_ENTITY: zwave_source_node_entry, - ZWAVE_LUMINANCE_ENTITY: zwave_luminance_entry, - ZWAVE_BATTERY_ENTITY: zwave_battery_entry, - ZWAVE_POWER_ENTITY: zwave_power_entry, - ZWAVE_TAMPERING_ENTITY: zwave_tampering_entry, - }, - ) - - return zwave_migration_data - - -@pytest.fixture(name="zwave_integration") -def zwave_integration_fixture(hass, zwave_migration_data): - """Mock the zwave integration.""" - hass.config.components.add("zwave") - zwave_config_entry = MockConfigEntry(domain="zwave", data={"usb_path": "/dev/test"}) - zwave_config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.zwave.async_get_migration_data", - return_value=zwave_migration_data, - ): - yield zwave_config_entry - - -@pytest.mark.skip(reason="The old zwave integration has been removed.") -async def test_migrate_zwave( - hass, - zwave_integration, - aeon_smart_switch_6, - multisensor_6, - integration, - hass_ws_client, -): - """Test the Z-Wave to Z-Wave JS migration websocket api.""" - entry = integration - client = await hass_ws_client(hass) - - assert hass.config_entries.async_entries("zwave") - - await client.send_json( - { - ID: 5, - TYPE: "zwave_js/migrate_zwave", - ENTRY_ID: entry.entry_id, - "dry_run": False, - } - ) - msg = await client.receive_json() - result = msg["result"] - - migration_entity_map = { - ZWAVE_SWITCH_ENTITY: "switch.smart_switch_6", - ZWAVE_LUMINANCE_ENTITY: "sensor.multisensor_6_illuminance", - ZWAVE_BATTERY_ENTITY: "sensor.multisensor_6_battery_level", - } - - assert result["zwave_entity_ids"] == [ - ZWAVE_SWITCH_ENTITY, - ZWAVE_POWER_ENTITY, - ZWAVE_SOURCE_NODE_ENTITY, - ZWAVE_LUMINANCE_ENTITY, - ZWAVE_BATTERY_ENTITY, - ZWAVE_TAMPERING_ENTITY, - ] - expected_zwave_js_entities = [ - "switch.smart_switch_6", - "sensor.multisensor_6_air_temperature", - "sensor.multisensor_6_illuminance", - "sensor.multisensor_6_humidity", - "sensor.multisensor_6_ultraviolet", - "binary_sensor.multisensor_6_home_security_tampering_product_cover_removed", - "binary_sensor.multisensor_6_home_security_motion_detection", - "sensor.multisensor_6_battery_level", - "binary_sensor.multisensor_6_low_battery_level", - "light.smart_switch_6", - "sensor.smart_switch_6_electric_consumed_kwh", - "sensor.smart_switch_6_electric_consumed_w", - "sensor.smart_switch_6_electric_consumed_v", - "sensor.smart_switch_6_electric_consumed_a", - ] - # Assert that both lists have the same items without checking order - assert not set(result["zwave_js_entity_ids"]) ^ set(expected_zwave_js_entities) - assert result["migration_entity_map"] == migration_entity_map - assert result["migrated"] is True - - dev_reg = dr.async_get(hass) - ent_reg = er.async_get(hass) - - # check the device registry migration - - # check that the migrated entries have correct attributes - multisensor_device_entry = dev_reg.async_get_device( - identifiers={("zwave_js", "3245146787-52")}, connections=set() - ) - assert multisensor_device_entry - assert multisensor_device_entry.name_by_user == ZWAVE_MULTISENSOR_DEVICE_NAME - assert multisensor_device_entry.area_id == ZWAVE_MULTISENSOR_DEVICE_AREA - switch_device_entry = dev_reg.async_get_device( - identifiers={("zwave_js", "3245146787-102")}, connections=set() - ) - assert switch_device_entry - assert switch_device_entry.name_by_user == ZWAVE_SWITCH_DEVICE_NAME - assert switch_device_entry.area_id == ZWAVE_SWITCH_DEVICE_AREA - - migration_device_map = { - ZWAVE_SWITCH_DEVICE_ID: switch_device_entry.id, - ZWAVE_MULTISENSOR_DEVICE_ID: multisensor_device_entry.id, - } - - assert result["migration_device_map"] == migration_device_map - - # check the entity registry migration - - # this should have been migrated and no longer present under that id - assert not ent_reg.async_is_registered("sensor.multisensor_6_battery_level") - assert not ent_reg.async_is_registered("sensor.multisensor_6_illuminance") - - # these should not have been migrated and is still in the registry - assert ent_reg.async_is_registered(ZWAVE_SOURCE_NODE_ENTITY) - source_entry = ent_reg.async_get(ZWAVE_SOURCE_NODE_ENTITY) - assert source_entry.unique_id == ZWAVE_SOURCE_NODE_UNIQUE_ID - assert ent_reg.async_is_registered(ZWAVE_POWER_ENTITY) - source_entry = ent_reg.async_get(ZWAVE_POWER_ENTITY) - assert source_entry.unique_id == ZWAVE_POWER_UNIQUE_ID - assert ent_reg.async_is_registered(ZWAVE_TAMPERING_ENTITY) - tampering_entry = ent_reg.async_get(ZWAVE_TAMPERING_ENTITY) - assert tampering_entry.unique_id == ZWAVE_TAMPERING_UNIQUE_ID - assert ent_reg.async_is_registered("sensor.smart_switch_6_electric_consumed_w") - - # this is the new entity_ids of the zwave_js entities - assert ent_reg.async_is_registered(ZWAVE_SWITCH_ENTITY) - assert ent_reg.async_is_registered(ZWAVE_BATTERY_ENTITY) - assert ent_reg.async_is_registered(ZWAVE_LUMINANCE_ENTITY) - - # check that the migrated entries have correct attributes - switch_entry = ent_reg.async_get(ZWAVE_SWITCH_ENTITY) - assert switch_entry - assert switch_entry.unique_id == "3245146787.102-37-0-currentValue" - assert switch_entry.name == ZWAVE_SWITCH_NAME - assert switch_entry.icon == ZWAVE_SWITCH_ICON - battery_entry = ent_reg.async_get(ZWAVE_BATTERY_ENTITY) - assert battery_entry - assert battery_entry.unique_id == "3245146787.52-128-0-level" - assert battery_entry.name == ZWAVE_BATTERY_NAME - assert battery_entry.icon == ZWAVE_BATTERY_ICON - luminance_entry = ent_reg.async_get(ZWAVE_LUMINANCE_ENTITY) - assert luminance_entry - assert luminance_entry.unique_id == "3245146787.52-49-0-Illuminance" - assert luminance_entry.name == ZWAVE_LUMINANCE_NAME - assert luminance_entry.icon == ZWAVE_LUMINANCE_ICON - assert luminance_entry.unit_of_measurement == LIGHT_LUX - - # check that the zwave config entry has been removed - assert not hass.config_entries.async_entries("zwave") - - # Check that the zwave integration fails entry setup after migration - zwave_config_entry = MockConfigEntry(domain="zwave") - zwave_config_entry.add_to_hass(hass) - assert not await hass.config_entries.async_setup(zwave_config_entry.entry_id) - - -@pytest.mark.skip(reason="The old zwave integration has been removed.") -async def test_migrate_zwave_dry_run( - hass, - zwave_integration, - aeon_smart_switch_6, - multisensor_6, - integration, - hass_ws_client, -): - """Test the zwave to zwave_js migration websocket api dry run.""" - entry = integration - client = await hass_ws_client(hass) - - await client.send_json( - {ID: 5, TYPE: "zwave_js/migrate_zwave", ENTRY_ID: entry.entry_id} - ) - msg = await client.receive_json() - result = msg["result"] - - migration_entity_map = { - ZWAVE_SWITCH_ENTITY: "switch.smart_switch_6", - ZWAVE_BATTERY_ENTITY: "sensor.multisensor_6_battery_level", - } - - assert result["zwave_entity_ids"] == [ - ZWAVE_SWITCH_ENTITY, - ZWAVE_POWER_ENTITY, - ZWAVE_SOURCE_NODE_ENTITY, - ZWAVE_BATTERY_ENTITY, - ZWAVE_TAMPERING_ENTITY, - ] - expected_zwave_js_entities = [ - "switch.smart_switch_6", - "sensor.multisensor_6_air_temperature", - "sensor.multisensor_6_illuminance", - "sensor.multisensor_6_humidity", - "sensor.multisensor_6_ultraviolet", - "binary_sensor.multisensor_6_home_security_tampering_product_cover_removed", - "binary_sensor.multisensor_6_home_security_motion_detection", - "sensor.multisensor_6_battery_level", - "binary_sensor.multisensor_6_low_battery_level", - "light.smart_switch_6", - "sensor.smart_switch_6_electric_consumed_kwh", - "sensor.smart_switch_6_electric_consumed_w", - "sensor.smart_switch_6_electric_consumed_v", - "sensor.smart_switch_6_electric_consumed_a", - ] - # Assert that both lists have the same items without checking order - assert not set(result["zwave_js_entity_ids"]) ^ set(expected_zwave_js_entities) - assert result["migration_entity_map"] == migration_entity_map - - dev_reg = dr.async_get(hass) - - multisensor_device_entry = dev_reg.async_get_device( - identifiers={("zwave_js", "3245146787-52")}, connections=set() - ) - assert multisensor_device_entry - assert multisensor_device_entry.name_by_user is None - assert multisensor_device_entry.area_id is None - switch_device_entry = dev_reg.async_get_device( - identifiers={("zwave_js", "3245146787-102")}, connections=set() - ) - assert switch_device_entry - assert switch_device_entry.name_by_user is None - assert switch_device_entry.area_id is None - - migration_device_map = { - ZWAVE_SWITCH_DEVICE_ID: switch_device_entry.id, - ZWAVE_MULTISENSOR_DEVICE_ID: multisensor_device_entry.id, - } - - assert result["migration_device_map"] == migration_device_map - - assert result["migrated"] is False - - ent_reg = er.async_get(hass) - - # no real migration should have been done - assert ent_reg.async_is_registered("switch.smart_switch_6") - assert ent_reg.async_is_registered("sensor.multisensor_6_battery_level") - assert ent_reg.async_is_registered("sensor.smart_switch_6_electric_consumed_w") - - assert ent_reg.async_is_registered(ZWAVE_SOURCE_NODE_ENTITY) - source_entry = ent_reg.async_get(ZWAVE_SOURCE_NODE_ENTITY) - assert source_entry - assert source_entry.unique_id == ZWAVE_SOURCE_NODE_UNIQUE_ID - - assert ent_reg.async_is_registered(ZWAVE_BATTERY_ENTITY) - battery_entry = ent_reg.async_get(ZWAVE_BATTERY_ENTITY) - assert battery_entry - assert battery_entry.unique_id == ZWAVE_BATTERY_UNIQUE_ID - - assert ent_reg.async_is_registered(ZWAVE_POWER_ENTITY) - power_entry = ent_reg.async_get(ZWAVE_POWER_ENTITY) - assert power_entry - assert power_entry.unique_id == ZWAVE_POWER_UNIQUE_ID - - # check that the zwave config entry has not been removed - assert hass.config_entries.async_entries("zwave") - - # Check that the zwave integration can be setup after dry run - zwave_config_entry = zwave_integration - with patch("openzwave.option.ZWaveOption"), patch("openzwave.network.ZWaveNetwork"): - assert await hass.config_entries.async_setup(zwave_config_entry.entry_id) - - -async def test_migrate_zwave_not_setup( - hass, aeon_smart_switch_6, multisensor_6, integration, hass_ws_client -): - """Test the zwave to zwave_js migration websocket without zwave setup.""" - entry = integration - client = await hass_ws_client(hass) - - await client.send_json( - {ID: 5, TYPE: "zwave_js/migrate_zwave", ENTRY_ID: entry.entry_id} - ) - msg = await client.receive_json() - - assert not msg["success"] - assert msg["error"]["code"] == "zwave_not_loaded" - assert msg["error"]["message"] == "Integration zwave is not loaded" - async def test_unique_id_migration_dupes( hass, multisensor_6_state, client, integration From 3681596430cb0b065ce0c5117f6efd89d2460556 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 May 2022 18:33:49 -0500 Subject: [PATCH 0702/3516] Bump nexia to 1.0.1 (#72185) - Fixes blower state when system is turned off - Changelog: https://github.com/bdraco/nexia/compare/1.0.0...1.0.1 - Fixes #71949 --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index bd1cd58c00b..f9ca21d9e0b 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==1.0.0"], + "requirements": ["nexia==1.0.1"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 13b3b172803..55765f9d139 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ nettigo-air-monitor==1.2.4 neurio==0.3.1 # homeassistant.components.nexia -nexia==1.0.0 +nexia==1.0.1 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 86f49132bc0..fdbe51fbb49 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -742,7 +742,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.2.4 # homeassistant.components.nexia -nexia==1.0.0 +nexia==1.0.1 # homeassistant.components.discord nextcord==2.0.0a8 From 9a3ecacf6bb03d227c7c10d0db01705c23dd3660 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 20 May 2022 00:21:27 +0000 Subject: [PATCH 0703/3516] [ci skip] Translation update --- .../aladdin_connect/translations/pl.json | 1 + .../components/baf/translations/nl.json | 4 +-- .../components/cover/translations/nl.json | 2 +- .../derivative/translations/nl.json | 4 +-- .../components/esphome/translations/nl.json | 6 ++--- .../components/group/translations/nl.json | 2 +- .../litterrobot/translations/sensor.pl.json | 21 +++++++++++++++- .../motion_blinds/translations/pl.json | 2 +- .../components/shelly/translations/de.json | 1 + .../components/shelly/translations/el.json | 1 + .../components/shelly/translations/et.json | 1 + .../shelly/translations/zh-Hant.json | 1 + .../components/slack/translations/nl.json | 2 +- .../components/slack/translations/pl.json | 5 ++++ .../somfy_mylink/translations/nl.json | 4 +-- .../components/yolink/translations/el.json | 25 +++++++++++++++++++ .../components/zone/translations/nl.json | 2 +- .../components/zwave_js/translations/nl.json | 4 +-- 18 files changed, 71 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/yolink/translations/el.json diff --git a/homeassistant/components/aladdin_connect/translations/pl.json b/homeassistant/components/aladdin_connect/translations/pl.json index 3ebc9a4ff92..14528332e1e 100644 --- a/homeassistant/components/aladdin_connect/translations/pl.json +++ b/homeassistant/components/aladdin_connect/translations/pl.json @@ -13,6 +13,7 @@ "data": { "password": "Has\u0142o" }, + "description": "Integracja Aladdin Connect wymaga ponownego uwierzytelnienia Twojego konta", "title": "Ponownie uwierzytelnij integracj\u0119" }, "user": { diff --git a/homeassistant/components/baf/translations/nl.json b/homeassistant/components/baf/translations/nl.json index 9b619390165..a05aedd9381 100644 --- a/homeassistant/components/baf/translations/nl.json +++ b/homeassistant/components/baf/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "ipv6_not_supported": "IPv6 wordt niet ondersteund" + "ipv6_not_supported": "IPv6 wordt niet ondersteund." }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -11,7 +11,7 @@ "flow_title": "{name} - {model} ({ip_address})", "step": { "discovery_confirm": { - "description": "Wilt u {name} - {model} ({ip_address}) instellen?" + "description": "Wilt je {name} - {model} ({ip_address}) instellen?" }, "user": { "data": { diff --git a/homeassistant/components/cover/translations/nl.json b/homeassistant/components/cover/translations/nl.json index c3998187c4f..80acc874379 100644 --- a/homeassistant/components/cover/translations/nl.json +++ b/homeassistant/components/cover/translations/nl.json @@ -35,5 +35,5 @@ "stopped": "Gestopt" } }, - "title": "Rolluik" + "title": "Afdekkingen" } \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/nl.json b/homeassistant/components/derivative/translations/nl.json index f3dc9c07de7..c6d485e7548 100644 --- a/homeassistant/components/derivative/translations/nl.json +++ b/homeassistant/components/derivative/translations/nl.json @@ -5,7 +5,7 @@ "data": { "name": "Naam", "round": "Nauwkeurigheid", - "source": "Invoersensor", + "source": "Bronsensor", "time_window": "Tijdsvenster", "unit_prefix": "Metrisch voorvoegsel", "unit_time": "Tijdseenheid" @@ -26,7 +26,7 @@ "data": { "name": "Naam", "round": "Nauwkeurigheid", - "source": "Invoersensor", + "source": "Bronsensor", "time_window": "Tijdsvenster", "unit_prefix": "Metrisch voorvoegsel", "unit_time": "Tijdseenheid" diff --git a/homeassistant/components/esphome/translations/nl.json b/homeassistant/components/esphome/translations/nl.json index 7f6f821104c..9c757e917e4 100644 --- a/homeassistant/components/esphome/translations/nl.json +++ b/homeassistant/components/esphome/translations/nl.json @@ -20,8 +20,8 @@ "description": "Voer het wachtwoord in dat u in uw configuratie heeft ingesteld voor {name}." }, "discovery_confirm": { - "description": "Wil je de ESPHome-node `{name}` toevoegen aan de Home Assistant?", - "title": "ESPHome node ontdekt" + "description": "Wil je de ESPHome-apparaat `{name}` toevoegen aan de Home Assistant?", + "title": "ESPHome apparaat ontdekt" }, "encryption_key": { "data": { @@ -40,7 +40,7 @@ "host": "Host", "port": "Poort" }, - "description": "Voer de verbindingsinstellingen in van uw [ESPHome](https://esphomelib.com/) node." + "description": "Voer de verbindingsinstellingen van je [ESPHome](https://esphome.io/) apparaat in." } } } diff --git a/homeassistant/components/group/translations/nl.json b/homeassistant/components/group/translations/nl.json index e78e3462c60..2b78d41e201 100644 --- a/homeassistant/components/group/translations/nl.json +++ b/homeassistant/components/group/translations/nl.json @@ -63,7 +63,7 @@ "description": "Met groepen kunt u een nieuwe entiteit cre\u00ebren die meerdere entiteiten van hetzelfde type vertegenwoordigt.", "menu_options": { "binary_sensor": "Binaire-sensorgroep", - "cover": "Rolluikgroep", + "cover": "Afdekkingengroep", "fan": "Ventilatorgroep", "light": "Lichtgroep", "lock": "Slotgroep", diff --git a/homeassistant/components/litterrobot/translations/sensor.pl.json b/homeassistant/components/litterrobot/translations/sensor.pl.json index 4f12cd1f30c..5e75c2ff68a 100644 --- a/homeassistant/components/litterrobot/translations/sensor.pl.json +++ b/homeassistant/components/litterrobot/translations/sensor.pl.json @@ -1,9 +1,28 @@ { "state": { "litterrobot__status_code": { + "br": "pokrywa otwarta", + "ccc": "cykl czyszczenia zako\u0144czony", + "ccp": "cykl czyszczenia w toku", + "csf": "b\u0142\u0105d sensora obecno\u015bci", + "csi": "przerwa w pracy sensora", + "cst": "czas pracy sensora", + "df1": "szuflada prawie pe\u0142na - pozosta\u0142y 2 cykle", + "df2": "szuflada prawie pe\u0142na - pozosta\u0142 1 cykl", + "dfs": "szuflada pe\u0142na", + "dhf": "b\u0142\u0105d pozycji wyj\u015bciowej + odchod\u00f3w", + "dpf": "b\u0142\u0105d pozycji odchod\u00f3w", + "ec": "cykl opr\u00f3\u017cniania", + "hpf": "b\u0142\u0105d pozycji wyj\u015bciowej", "off": "wy\u0142.", "offline": "Offline", - "p": "wstrzymany" + "otf": "b\u0142\u0105d nadmiernego momentu obrotowego", + "p": "wstrzymany", + "pd": "detektor obecno\u015bci", + "rdy": "gotowy", + "scf": "b\u0142\u0105d sensora obecno\u015bci podczas uruchamiania", + "sdf": "szuflada pe\u0142na podczas uruchamiania", + "spf": "detekcja obecno\u015bci podczas uruchamiania" } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/pl.json b/homeassistant/components/motion_blinds/translations/pl.json index 60277ba9360..6dfef34f4ab 100644 --- a/homeassistant/components/motion_blinds/translations/pl.json +++ b/homeassistant/components/motion_blinds/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane, ustawienia po\u0142\u0105czenia zosta\u0142y zaktualizowane", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json index a8d8bdbdf99..6d4c0e92110 100644 --- a/homeassistant/components/shelly/translations/de.json +++ b/homeassistant/components/shelly/translations/de.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "firmware_not_fully_provisioned": "Das Ger\u00e4t ist nicht vollst\u00e4ndig eingerichtet. Bitte kontaktiere den Shelly Support", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/shelly/translations/el.json b/homeassistant/components/shelly/translations/el.json index e83971e34fe..a5680f9343a 100644 --- a/homeassistant/components/shelly/translations/el.json +++ b/homeassistant/components/shelly/translations/el.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "firmware_not_fully_provisioned": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ae\u03c1\u03c9\u03c2 \u03b5\u03c6\u03bf\u03b4\u03b9\u03b1\u03c3\u03bc\u03ad\u03bd\u03b7. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03bc\u03b5 \u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03c4\u03b7\u03c2 Shelly", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, diff --git a/homeassistant/components/shelly/translations/et.json b/homeassistant/components/shelly/translations/et.json index 7db0eaad4ac..e248ee9eba4 100644 --- a/homeassistant/components/shelly/translations/et.json +++ b/homeassistant/components/shelly/translations/et.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "\u00dchendamine nurjus", + "firmware_not_fully_provisioned": "Seade pole t\u00e4ielikult toetatud. V\u00f5ta \u00fchendust Shelly toega", "invalid_auth": "Tuvastamine nurjus", "unknown": "Tundmatu viga" }, diff --git a/homeassistant/components/shelly/translations/zh-Hant.json b/homeassistant/components/shelly/translations/zh-Hant.json index 62d1bd18850..a2727fcc933 100644 --- a/homeassistant/components/shelly/translations/zh-Hant.json +++ b/homeassistant/components/shelly/translations/zh-Hant.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", + "firmware_not_fully_provisioned": "\u88dd\u7f6e\u4e26\u672a\u5b8c\u5168\u652f\u63f4\uff0c\u8acb\u806f\u7e6b Shelly \u5c0b\u6c42\u652f\u63f4", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, diff --git a/homeassistant/components/slack/translations/nl.json b/homeassistant/components/slack/translations/nl.json index 8022048798e..f531a126e9b 100644 --- a/homeassistant/components/slack/translations/nl.json +++ b/homeassistant/components/slack/translations/nl.json @@ -19,7 +19,7 @@ "data_description": { "api_key": "Het Slack API-token dat moet worden gebruikt voor het verzenden van Slack-berichten.", "default_channel": "Het kanaal waarnaar moet worden gepost als er geen kanaal is opgegeven bij het verzenden van een bericht.", - "icon": "Gebruik een van de Slack emoji's als pictogram voor de opgegeven gebruikersnaam.", + "icon": "Gebruik een van de Slack emoji's als icoon voor de opgegeven gebruikersnaam.", "username": "Home Assistant plaatst berichten op Slack met de opgegeven gebruikersnaam." }, "description": "Raadpleeg de documentatie over het verkrijgen van uw Slack API-sleutel." diff --git a/homeassistant/components/slack/translations/pl.json b/homeassistant/components/slack/translations/pl.json index 3ce8f4bb311..fd1cb508e4a 100644 --- a/homeassistant/components/slack/translations/pl.json +++ b/homeassistant/components/slack/translations/pl.json @@ -12,7 +12,12 @@ "user": { "data": { "api_key": "Klucz API", + "default_channel": "Domy\u015blny kana\u0142", + "icon": "Ikona", "username": "Nazwa u\u017cytkownika" + }, + "data_description": { + "api_key": "Token API Slack u\u017cywany do wysy\u0142ania wiadomo\u015bci Slack." } } } diff --git a/homeassistant/components/somfy_mylink/translations/nl.json b/homeassistant/components/somfy_mylink/translations/nl.json index 43c71140cb9..6fae9b7404a 100644 --- a/homeassistant/components/somfy_mylink/translations/nl.json +++ b/homeassistant/components/somfy_mylink/translations/nl.json @@ -27,13 +27,13 @@ "step": { "init": { "data": { - "target_id": "Configureer opties voor een rolluik." + "target_id": "Configureer opties voor een afdekking." }, "title": "Configureer MyLink-opties" }, "target_config": { "data": { - "reverse": "Rolluik is omgekeerd" + "reverse": "Afdekking is omgekeerd" }, "description": "Configureer opties voor ' {target_name} '", "title": "Configureer MyLink Cover" diff --git a/homeassistant/components/yolink/translations/el.json b/homeassistant/components/yolink/translations/el.json new file mode 100644 index 00000000000..87f7c0d4d41 --- /dev/null +++ b/homeassistant/components/yolink/translations/el.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2.", + "missing_configuration": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", + "no_url_available": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL. \u0393\u03b9\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1, [\u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b2\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1\u03c2] ( {docs_url} )", + "oauth_error": "\u039b\u03ae\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd.", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + }, + "create_entry": { + "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "step": { + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "reauth_confirm": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 yolink \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bb\u03ad\u03b3\u03be\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c4\u03b7\u03bd \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c4\u03bf\u03c5 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03c3\u03b1\u03c2", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/translations/nl.json b/homeassistant/components/zone/translations/nl.json index 6dcf565ada6..021fbfb837c 100644 --- a/homeassistant/components/zone/translations/nl.json +++ b/homeassistant/components/zone/translations/nl.json @@ -6,7 +6,7 @@ "step": { "init": { "data": { - "icon": "Pictogram", + "icon": "Icoon", "latitude": "Breedtegraad", "longitude": "Lengtegraad", "name": "Naam", diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json index 53bf45e2f4d..69481c2a98c 100644 --- a/homeassistant/components/zwave_js/translations/nl.json +++ b/homeassistant/components/zwave_js/translations/nl.json @@ -77,7 +77,7 @@ }, "condition_type": { "config_parameter": "Config parameter {subtype} waarde", - "node_status": "Knooppuntstatus", + "node_status": "Apparaatstatus", "value": "Huidige waarde van een Z-Wave-waarde" }, "trigger_type": { @@ -86,7 +86,7 @@ "event.value_notification.basic": "Basis CC-evenement op {subtype}", "event.value_notification.central_scene": "Centrale Sc\u00e8ne actie op {subtype}", "event.value_notification.scene_activation": "Sc\u00e8ne-activering op {subtype}", - "state.node_status": "Knooppuntstatus gewijzigd", + "state.node_status": "Apparaatstatus gewijzigd", "zwave_js.value_updated.config_parameter": "Waardeverandering op configuratieparameter {subtype}", "zwave_js.value_updated.value": "Waardeverandering op een Z-Wave JS-waarde" } From 5c2c6026864b05750e0a320c6797060e97a2694f Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 19 May 2022 21:34:58 -0400 Subject: [PATCH 0704/3516] Bumps pyunifiprotect to 3.6.0 (#72188) --- homeassistant/components/unifiprotect/button.py | 2 +- homeassistant/components/unifiprotect/camera.py | 3 +-- homeassistant/components/unifiprotect/config_flow.py | 2 +- homeassistant/components/unifiprotect/const.py | 2 +- homeassistant/components/unifiprotect/data.py | 3 ++- homeassistant/components/unifiprotect/entity.py | 2 +- homeassistant/components/unifiprotect/lock.py | 3 +-- homeassistant/components/unifiprotect/manifest.json | 2 +- homeassistant/components/unifiprotect/number.py | 2 +- homeassistant/components/unifiprotect/select.py | 3 ++- homeassistant/components/unifiprotect/switch.py | 8 ++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifiprotect/conftest.py | 2 +- tests/components/unifiprotect/test_camera.py | 4 +--- tests/components/unifiprotect/test_config_flow.py | 2 +- tests/components/unifiprotect/test_lock.py | 3 +-- tests/components/unifiprotect/test_select.py | 11 +++++++---- tests/components/unifiprotect/test_sensor.py | 10 ++++++++-- tests/components/unifiprotect/test_switch.py | 9 +++++++-- 20 files changed, 46 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 731b1eaf86a..fe82c0afbe0 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import Final -from pyunifiprotect.data.base import ProtectAdoptableDeviceModel +from pyunifiprotect.data import ProtectAdoptableDeviceModel from homeassistant.components.button import ( ButtonDeviceClass, diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index 928d82f317b..ca076e490a2 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -5,8 +5,7 @@ from collections.abc import Generator import logging from pyunifiprotect.api import ProtectApiClient -from pyunifiprotect.data import Camera as UFPCamera, StateType -from pyunifiprotect.data.devices import CameraChannel +from pyunifiprotect.data import Camera as UFPCamera, CameraChannel, StateType from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 27108d24eaa..daaae214df9 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -6,7 +6,7 @@ from typing import Any from aiohttp import CookieJar from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient -from pyunifiprotect.data.nvr import NVR +from pyunifiprotect.data import NVR import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 7d2842977b6..0fe4ca98afa 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -1,6 +1,6 @@ """Constant definitions for UniFi Protect Integration.""" -from pyunifiprotect.data.types import ModelType, Version +from pyunifiprotect.data import ModelType, Version from homeassistant.const import Platform diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 371c1c7831b..e20f956acd2 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -12,9 +12,10 @@ from pyunifiprotect.data import ( Event, Liveview, ModelType, + ProtectAdoptableDeviceModel, + ProtectDeviceModel, WSSubscriptionMessage, ) -from pyunifiprotect.data.base import ProtectAdoptableDeviceModel, ProtectDeviceModel from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 045a9c4fd6d..45e52db5963 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -6,6 +6,7 @@ import logging from typing import Any from pyunifiprotect.data import ( + NVR, Camera, Doorlock, Event, @@ -16,7 +17,6 @@ from pyunifiprotect.data import ( StateType, Viewer, ) -from pyunifiprotect.data.nvr import NVR from homeassistant.core import callback import homeassistant.helpers.device_registry as dr diff --git a/homeassistant/components/unifiprotect/lock.py b/homeassistant/components/unifiprotect/lock.py index 6fb70a2523f..c4d56dd1e71 100644 --- a/homeassistant/components/unifiprotect/lock.py +++ b/homeassistant/components/unifiprotect/lock.py @@ -4,8 +4,7 @@ from __future__ import annotations import logging from typing import Any -from pyunifiprotect.data import Doorlock -from pyunifiprotect.data.types import LockStatusType +from pyunifiprotect.data import Doorlock, LockStatusType from homeassistant.components.lock import LockEntity, LockEntityDescription from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 36b6011e8fc..898abd73a6f 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.5.1", "unifi-discovery==1.1.3"], + "requirements": ["pyunifiprotect==3.6.0", "unifi-discovery==1.1.3"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index d6dfba0c38f..58ae0502caa 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta -from pyunifiprotect.data.devices import Camera, Doorlock, Light +from pyunifiprotect.data import Camera, Doorlock, Light from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index f0500ea54e5..2c6c5fa4cc6 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -11,17 +11,18 @@ from typing import Any, Final from pyunifiprotect.api import ProtectApiClient from pyunifiprotect.data import ( Camera, + ChimeType, DoorbellMessageType, Doorlock, IRLEDMode, Light, LightModeEnableType, LightModeType, + MountType, RecordingMode, Sensor, Viewer, ) -from pyunifiprotect.data.types import ChimeType, MountType import voluptuous as vol from homeassistant.components.select import SelectEntity, SelectEntityDescription diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 6051e4e596f..971c637a8c2 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -5,8 +5,12 @@ from dataclasses import dataclass import logging from typing import Any -from pyunifiprotect.data import Camera, RecordingMode, VideoMode -from pyunifiprotect.data.base import ProtectAdoptableDeviceModel +from pyunifiprotect.data import ( + Camera, + ProtectAdoptableDeviceModel, + RecordingMode, + VideoMode, +) from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry diff --git a/requirements_all.txt b/requirements_all.txt index 55765f9d139..4ac46752539 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1984,7 +1984,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.5.1 +pyunifiprotect==3.6.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fdbe51fbb49..0327dcfa142 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1313,7 +1313,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.5.1 +pyunifiprotect==3.6.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index f495b2bc8f7..c83c6eb72cc 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -17,11 +17,11 @@ from pyunifiprotect.data import ( Doorlock, Light, Liveview, + ProtectAdoptableDeviceModel, Sensor, Viewer, WSSubscriptionMessage, ) -from pyunifiprotect.data.base import ProtectAdoptableDeviceModel from homeassistant.components.unifiprotect.const import DOMAIN from homeassistant.const import Platform diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index 362481bfb03..d7fc2a62325 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -6,9 +6,7 @@ from copy import copy from unittest.mock import AsyncMock, Mock import pytest -from pyunifiprotect.data import Camera as ProtectCamera -from pyunifiprotect.data.devices import CameraChannel -from pyunifiprotect.data.types import StateType +from pyunifiprotect.data import Camera as ProtectCamera, CameraChannel, StateType from pyunifiprotect.exceptions import NvrError from homeassistant.components.camera import ( diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 59304ce915f..80e845591b1 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -7,7 +7,7 @@ from unittest.mock import patch import pytest from pyunifiprotect import NotAuthorized, NvrError -from pyunifiprotect.data.nvr import NVR +from pyunifiprotect.data import NVR from homeassistant import config_entries from homeassistant.components import dhcp, ssdp diff --git a/tests/components/unifiprotect/test_lock.py b/tests/components/unifiprotect/test_lock.py index 740cb61b3c4..0a02fcb22a4 100644 --- a/tests/components/unifiprotect/test_lock.py +++ b/tests/components/unifiprotect/test_lock.py @@ -6,8 +6,7 @@ from copy import copy from unittest.mock import AsyncMock, Mock import pytest -from pyunifiprotect.data import Doorlock -from pyunifiprotect.data.types import LockStatusType +from pyunifiprotect.data import Doorlock, LockStatusType from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.const import ( diff --git a/tests/components/unifiprotect/test_select.py b/tests/components/unifiprotect/test_select.py index d09fa421ec3..fc0abbe29ca 100644 --- a/tests/components/unifiprotect/test_select.py +++ b/tests/components/unifiprotect/test_select.py @@ -7,16 +7,19 @@ from datetime import timedelta from unittest.mock import AsyncMock, Mock, patch import pytest -from pyunifiprotect.data import Camera, Light -from pyunifiprotect.data.devices import LCDMessage, Viewer -from pyunifiprotect.data.nvr import DoorbellMessage, Liveview -from pyunifiprotect.data.types import ( +from pyunifiprotect.data import ( + Camera, DoorbellMessageType, IRLEDMode, + LCDMessage, + Light, LightModeEnableType, LightModeType, + Liveview, RecordingMode, + Viewer, ) +from pyunifiprotect.data.nvr import DoorbellMessage from homeassistant.components.select.const import ATTR_OPTIONS from homeassistant.components.unifiprotect.const import ( diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index 971ee47405b..dff746c167f 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -7,10 +7,16 @@ from datetime import datetime, timedelta from unittest.mock import AsyncMock, Mock import pytest -from pyunifiprotect.data import NVR, Camera, Event, Sensor +from pyunifiprotect.data import ( + NVR, + Camera, + Event, + EventType, + Sensor, + SmartDetectObjectType, +) from pyunifiprotect.data.base import WifiConnectionState, WiredConnectionState from pyunifiprotect.data.nvr import EventMetadata -from pyunifiprotect.data.types import EventType, SmartDetectObjectType from homeassistant.components.unifiprotect.const import ( ATTR_EVENT_SCORE, diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 498f4c6b3b7..7918ea0b6cf 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -5,8 +5,13 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock import pytest -from pyunifiprotect.data import Camera, Light -from pyunifiprotect.data.types import RecordingMode, SmartDetectObjectType, VideoMode +from pyunifiprotect.data import ( + Camera, + Light, + RecordingMode, + SmartDetectObjectType, + VideoMode, +) from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.switch import ( From 1001f9e39a3f2204f566337a7750825eabd5f65d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 May 2022 22:58:32 -0500 Subject: [PATCH 0705/3516] Fix last state in history minimal respones when all the states at the end are skipped (#72203) --- homeassistant/components/recorder/history.py | 8 ---- tests/components/history/test_init.py | 44 +++++++++++++------- tests/components/recorder/test_history.py | 33 +++++++++------ 3 files changed, 49 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 3df444faccc..845a2af62bf 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -695,8 +695,6 @@ def _sorted_states_to_dict( prev_state = first_state.state ent_results.append(state_class(first_state, attr_cache)) - initial_state_count = len(ent_results) - row = None for row in group: # With minimal response we do not care about attribute # changes so we can filter out duplicate states @@ -716,12 +714,6 @@ def _sorted_states_to_dict( ) prev_state = state - if row and len(ent_results) != initial_state_count: - # There was at least one state change - # replace the last minimal state with - # a full state - ent_results[-1] = state_class(row, attr_cache) - # If there are no states beyond the initial state, # the state a was never popped from initial_states for ent_id, row in initial_states.items(): diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 0425c9bc2e7..a2626ab2004 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -61,23 +61,30 @@ def test_get_significant_states_minimal_response(hass_history): hist = get_significant_states( hass, zero, four, filters=history.Filters(), minimal_response=True ) + entites_with_reducable_states = [ + "media_player.test", + "media_player.test3", + ] - # The second media_player.test state is reduced + # All states for media_player.test state are reduced # down to last_changed and state when minimal_response + # is set except for the first state. # is set. We use JSONEncoder to make sure that are # pre-encoded last_changed is always the same as what # will happen with encoding a native state - input_state = states["media_player.test"][1] - orig_last_changed = json.dumps( - process_timestamp(input_state.last_changed), - cls=JSONEncoder, - ).replace('"', "") - orig_state = input_state.state - states["media_player.test"][1] = { - "last_changed": orig_last_changed, - "state": orig_state, - } - + for entity_id in entites_with_reducable_states: + entity_states = states[entity_id] + for state_idx in range(1, len(entity_states)): + input_state = entity_states[state_idx] + orig_last_changed = orig_last_changed = json.dumps( + process_timestamp(input_state.last_changed), + cls=JSONEncoder, + ).replace('"', "") + orig_state = input_state.state + entity_states[state_idx] = { + "last_changed": orig_last_changed, + "state": orig_state, + } assert states == hist @@ -616,6 +623,9 @@ async def test_fetch_period_api_with_minimal_response(hass, recorder_mock, hass_ hass.states.async_set("sensor.power", 50, {"attr": "any"}) await async_wait_recording_done(hass) hass.states.async_set("sensor.power", 23, {"attr": "any"}) + last_changed = hass.states.get("sensor.power").last_changed + await async_wait_recording_done(hass) + hass.states.async_set("sensor.power", 23, {"attr": "any"}) await async_wait_recording_done(hass) client = await hass_client() response = await client.get( @@ -634,9 +644,13 @@ async def test_fetch_period_api_with_minimal_response(hass, recorder_mock, hass_ assert "entity_id" not in state_list[1] assert state_list[1]["state"] == "50" - assert state_list[2]["entity_id"] == "sensor.power" - assert state_list[2]["attributes"] == {} + assert "attributes" not in state_list[2] + assert "entity_id" not in state_list[2] assert state_list[2]["state"] == "23" + assert state_list[2]["last_changed"] == json.dumps( + process_timestamp(last_changed), + cls=JSONEncoder, + ).replace('"', "") async def test_fetch_period_api_with_no_timestamp(hass, hass_client, recorder_mock): @@ -1131,7 +1145,7 @@ async def test_history_during_period(hass, hass_ws_client, recorder_mock): assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) assert sensor_test_history[2]["s"] == "on" - assert sensor_test_history[2]["a"] == {} + assert "a" not in sensor_test_history[2] await client.send_json( { diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index 1d59893745b..da6c3a8af35 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -388,23 +388,30 @@ def test_get_significant_states_minimal_response(hass_recorder): hass = hass_recorder() zero, four, states = record_states(hass) hist = history.get_significant_states(hass, zero, four, minimal_response=True) + entites_with_reducable_states = [ + "media_player.test", + "media_player.test3", + ] - # The second media_player.test state is reduced + # All states for media_player.test state are reduced # down to last_changed and state when minimal_response + # is set except for the first state. # is set. We use JSONEncoder to make sure that are # pre-encoded last_changed is always the same as what # will happen with encoding a native state - input_state = states["media_player.test"][1] - orig_last_changed = json.dumps( - process_timestamp(input_state.last_changed), - cls=JSONEncoder, - ).replace('"', "") - orig_state = input_state.state - states["media_player.test"][1] = { - "last_changed": orig_last_changed, - "state": orig_state, - } - + for entity_id in entites_with_reducable_states: + entity_states = states[entity_id] + for state_idx in range(1, len(entity_states)): + input_state = entity_states[state_idx] + orig_last_changed = orig_last_changed = json.dumps( + process_timestamp(input_state.last_changed), + cls=JSONEncoder, + ).replace('"', "") + orig_state = input_state.state + entity_states[state_idx] = { + "last_changed": orig_last_changed, + "state": orig_state, + } assert states == hist @@ -565,7 +572,7 @@ def test_get_significant_states_only(hass_recorder): assert states == hist[entity_id] -def record_states(hass): +def record_states(hass) -> tuple[datetime, datetime, dict[str, list[State]]]: """Record some test states. We inject a bunch of state updates from media player, zone and From 36e9088e6b50686db13e4562f628ef15ebd98826 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 20 May 2022 06:22:15 +0200 Subject: [PATCH 0706/3516] Allow frontend to store metadata in homeassistant scenes (#72178) --- homeassistant/components/homeassistant/scene.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index bf8affd902e..21c364ba65b 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -101,6 +101,7 @@ PLATFORM_SCHEMA = vol.Schema( vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_ICON): cv.icon, vol.Required(CONF_ENTITIES): STATES_SCHEMA, + vol.Optional("metadata"): dict, } ) ], From c028db00de35ac4d758e9690562402bd5ca5c6c4 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 19 May 2022 21:22:37 -0700 Subject: [PATCH 0707/3516] Add Neato application credentials platform and deprecate configuration.yaml (#72175) --- homeassistant/components/neato/__init__.py | 44 ++++++++++++------- homeassistant/components/neato/api.py | 3 +- .../neato/application_credentials.py | 28 ++++++++++++ homeassistant/components/neato/manifest.json | 2 +- .../generated/application_credentials.py | 1 + tests/components/neato/test_config_flow.py | 2 - 6 files changed, 59 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/neato/application_credentials.py diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index c1e193b7406..cbfd860a0b1 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -2,10 +2,14 @@ import logging import aiohttp -from pybotvac import Account, Neato +from pybotvac import Account from pybotvac.exceptions import NeatoException import voluptuous as vol +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN, Platform from homeassistant.core import HomeAssistant @@ -13,7 +17,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.helpers.typing import ConfigType -from . import api, config_flow +from . import api from .const import NEATO_CONFIG, NEATO_DOMAIN, NEATO_LOGIN from .hub import NeatoHub @@ -21,14 +25,17 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( - { - NEATO_DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - } - ) - }, + vol.All( + cv.deprecated(NEATO_DOMAIN), + { + NEATO_DOMAIN: vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -43,18 +50,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True hass.data[NEATO_CONFIG] = config[NEATO_DOMAIN] - vendor = Neato() - config_flow.OAuth2FlowHandler.async_register_implementation( + await async_import_client_credential( hass, - api.NeatoImplementation( - hass, - NEATO_DOMAIN, + NEATO_DOMAIN, + ClientCredential( config[NEATO_DOMAIN][CONF_CLIENT_ID], config[NEATO_DOMAIN][CONF_CLIENT_SECRET], - vendor.auth_endpoint, - vendor.token_endpoint, ), ) + _LOGGER.warning( + "Configuration of Neato integration in YAML is deprecated and " + "will be removed in a future release; Your existing OAuth " + "Application Credentials have been imported into the UI " + "automatically and can be safely removed from your " + "configuration.yaml file" + ) return True diff --git a/homeassistant/components/neato/api.py b/homeassistant/components/neato/api.py index cd26b009040..f3a4324b7ed 100644 --- a/homeassistant/components/neato/api.py +++ b/homeassistant/components/neato/api.py @@ -7,6 +7,7 @@ from typing import Any import pybotvac from homeassistant import config_entries, core +from homeassistant.components.application_credentials import AuthImplementation from homeassistant.helpers import config_entry_oauth2_flow @@ -35,7 +36,7 @@ class ConfigEntryAuth(pybotvac.OAuthSession): # type: ignore[misc] return self.session.token["access_token"] # type: ignore[no-any-return] -class NeatoImplementation(config_entry_oauth2_flow.LocalOAuth2Implementation): +class NeatoImplementation(AuthImplementation): """Neato implementation of LocalOAuth2Implementation. We need this class because we have to add client_secret and scope to the authorization request. diff --git a/homeassistant/components/neato/application_credentials.py b/homeassistant/components/neato/application_credentials.py new file mode 100644 index 00000000000..2abdb6bcc81 --- /dev/null +++ b/homeassistant/components/neato/application_credentials.py @@ -0,0 +1,28 @@ +"""Application credentials platform for neato.""" + +from pybotvac import Neato + +from homeassistant.components.application_credentials import ( + AuthorizationServer, + ClientCredential, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow + +from . import api + + +async def async_get_auth_implementation( + hass: HomeAssistant, auth_domain: str, credential: ClientCredential +) -> config_entry_oauth2_flow.AbstractOAuth2Implementation: + """Return auth implementation for a custom auth implementation.""" + vendor = Neato() + return api.NeatoImplementation( + hass, + auth_domain, + credential, + AuthorizationServer( + authorize_url=vendor.auth_endpoint, + token_url=vendor.token_endpoint, + ), + ) diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index f12f79c77a8..64c10abec06 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/neato", "requirements": ["pybotvac==0.0.23"], "codeowners": ["@dshokouhi", "@Santobert"], - "dependencies": ["auth"], + "dependencies": ["application_credentials"], "iot_class": "cloud_polling", "loggers": ["pybotvac"] } diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index 7521c5c30b8..41541943086 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -9,6 +9,7 @@ APPLICATION_CREDENTIALS = [ "geocaching", "google", "home_connect", + "neato", "netatmo", "spotify", "xbox", diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 48bdf247f51..7e187f1e2fd 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -27,7 +27,6 @@ async def test_full_flow( "neato", { "neato": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, - "http": {"base_url": "https://example.com"}, }, ) @@ -99,7 +98,6 @@ async def test_reauth( "neato", { "neato": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, - "http": {"base_url": "https://example.com"}, }, ) From ba4031718d8ec3d983b7f4fc322e184d76d1ec8e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 May 2022 06:25:29 +0200 Subject: [PATCH 0708/3516] Enforce application_credentials type hints (#72214) --- pylint/plugins/hass_enforce_type_hints.py | 24 ++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index cb90499b6ca..6cf521addac 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -40,12 +40,16 @@ _MODULE_FILTERS: dict[str, re.Pattern] = { "any_platform": re.compile( f"^homeassistant\\.components\\.\\w+\\.({'|'.join([platform.value for platform in Platform])})$" ), + # application_credentials matches only in the package root (application_credentials.py) + "application_credentials": re.compile( + r"^homeassistant\.components\.\w+\.(application_credentials)$" + ), # device_tracker matches only in the package root (device_tracker.py) "device_tracker": re.compile(r"^homeassistant\.components\.\w+\.(device_tracker)$"), # diagnostics matches only in the package root (diagnostics.py) "diagnostics": re.compile(r"^homeassistant\.components\.\w+\.(diagnostics)$"), # config_flow matches only in the package root (config_flow.py) - "config_flow": re.compile(r"^homeassistant\.components\.\w+\.(config_flow)$") + "config_flow": re.compile(r"^homeassistant\.components\.\w+\.(config_flow)$"), } _METHOD_MATCH: list[TypeHintMatch] = [ @@ -135,6 +139,24 @@ _METHOD_MATCH: list[TypeHintMatch] = [ }, return_type=None, ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["application_credentials"], + function_name="async_get_auth_implementation", + arg_types={ + 0: "HomeAssistant", + 1: "str", + 2: "ClientCredential", + }, + return_type="AbstractOAuth2Implementation", + ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["application_credentials"], + function_name="async_get_authorization_server", + arg_types={ + 0: "HomeAssistant", + }, + return_type="AuthorizationServer", + ), TypeHintMatch( module_filter=_MODULE_FILTERS["device_tracker"], function_name="setup_scanner", From 7cad1571a2e046f1d1a991344503dd07a2b33385 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 May 2022 00:21:09 -0500 Subject: [PATCH 0709/3516] Describe hue events in the logbook (#72220) Co-authored-by: Paulus Schoutsen --- homeassistant/components/hue/logbook.py | 73 +++++++++++++++++ tests/components/hue/test_logbook.py | 104 ++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 homeassistant/components/hue/logbook.py create mode 100644 tests/components/hue/test_logbook.py diff --git a/homeassistant/components/hue/logbook.py b/homeassistant/components/hue/logbook.py new file mode 100644 index 00000000000..40abce7e2d3 --- /dev/null +++ b/homeassistant/components/hue/logbook.py @@ -0,0 +1,73 @@ +"""Describe hue logbook events.""" +from __future__ import annotations + +from collections.abc import Callable + +from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_TYPE +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import device_registry as dr + +from .const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN + +TRIGGER_SUBTYPE = { + "button_1": "first button", + "button_2": "second button", + "button_3": "third button", + "button_4": "fourth button", + "double_buttons_1_3": "first and third buttons", + "double_buttons_2_4": "second and fourth buttons", + "dim_down": "dim down", + "dim_up": "dim up", + "turn_off": "turn off", + "turn_on": "turn on", + "1": "first button", + "2": "second button", + "3": "third button", + "4": "fourth button", +} +TRIGGER_TYPE = { + "remote_button_long_release": "{subtype} released after long press", + "remote_button_short_press": "{subtype} pressed", + "remote_button_short_release": "{subtype} released", + "remote_double_button_long_press": "both {subtype} released after long press", + "remote_double_button_short_press": "both {subtype} released", + "initial_press": "{subtype} pressed initially", + "repeat": "{subtype} held down", + "short_release": "{subtype} released after short press", + "long_release": "{subtype} released after long press", + "double_short_release": "both {subtype} released", +} + +UNKNOWN_TYPE = "unknown type" +UNKNOWN_SUB_TYPE = "unknown sub type" + + +@callback +def async_describe_events( + hass: HomeAssistant, + async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None], +) -> None: + """Describe hue logbook events.""" + + @callback + def async_describe_hue_event(event: Event) -> dict[str, str]: + """Describe hue logbook event.""" + data = event.data + name: str | None = None + if dev_ent := dr.async_get(hass).async_get(data[CONF_DEVICE_ID]): + name = dev_ent.name + if name is None: + name = data[CONF_ID] + if CONF_TYPE in data: # v2 + subtype = TRIGGER_SUBTYPE.get(str(data[CONF_SUBTYPE]), UNKNOWN_SUB_TYPE) + message = TRIGGER_TYPE.get(data[CONF_TYPE], UNKNOWN_TYPE).format( + subtype=subtype + ) + else: + message = f"Event {data[CONF_EVENT]}" # v1 + return { + "name": name, + "message": str(message), + } + + async_describe_event(DOMAIN, ATTR_HUE_EVENT, async_describe_hue_event) diff --git a/tests/components/hue/test_logbook.py b/tests/components/hue/test_logbook.py new file mode 100644 index 00000000000..fb7934da126 --- /dev/null +++ b/tests/components/hue/test_logbook.py @@ -0,0 +1,104 @@ +"""The tests for hue logbook.""" + +from homeassistant.components.hue.const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN +from homeassistant.components.hue.v1.hue_event import CONF_LAST_UPDATED +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_EVENT, + CONF_ID, + CONF_TYPE, + CONF_UNIQUE_ID, +) +from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component + +from .conftest import setup_platform + +from tests.components.logbook.common import MockRow, mock_humanify + +# v1 event +SAMPLE_V1_EVENT = { + CONF_DEVICE_ID: "fe346f17a9f8c15be633f9cc3f3d6631", + CONF_EVENT: 18, + CONF_ID: "hue_tap", + CONF_LAST_UPDATED: "2019-12-28T22:58:03", + CONF_UNIQUE_ID: "00:00:00:00:00:44:23:08-f2", +} +# v2 event +SAMPLE_V2_EVENT = { + CONF_DEVICE_ID: "f974028e7933aea703a2199a855bc4a3", + CONF_ID: "wall_switch_with_2_controls_button", + CONF_SUBTYPE: 1, + CONF_TYPE: "initial_press", + CONF_UNIQUE_ID: "c658d3d8-a013-4b81-8ac6-78b248537e70", +} + + +async def test_humanify_hue_events(hass, mock_bridge_v2): + """Test hue events when the devices are present in the registry.""" + await setup_platform(hass, mock_bridge_v2, "sensor") + hass.config.components.add("recorder") + assert await async_setup_component(hass, "logbook", {}) + await hass.async_block_till_done() + entry: ConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] + + dev_reg = device_registry.async_get(hass) + v1_device = dev_reg.async_get_or_create( + identifiers={(DOMAIN, "v1")}, name="Remote 1", config_entry_id=entry.entry_id + ) + v2_device = dev_reg.async_get_or_create( + identifiers={(DOMAIN, "v2")}, name="Remote 2", config_entry_id=entry.entry_id + ) + + (v1_event, v2_event) = mock_humanify( + hass, + [ + MockRow( + ATTR_HUE_EVENT, + {**SAMPLE_V1_EVENT, CONF_DEVICE_ID: v1_device.id}, + ), + MockRow( + ATTR_HUE_EVENT, + {**SAMPLE_V2_EVENT, CONF_DEVICE_ID: v2_device.id}, + ), + ], + ) + + assert v1_event["name"] == "Remote 1" + assert v1_event["domain"] == DOMAIN + assert v1_event["message"] == "Event 18" + + assert v2_event["name"] == "Remote 2" + assert v2_event["domain"] == DOMAIN + assert v2_event["message"] == "first button pressed initially" + + +async def test_humanify_hue_events_devices_removed(hass, mock_bridge_v2): + """Test hue events when the devices have been removed from the registry.""" + await setup_platform(hass, mock_bridge_v2, "sensor") + hass.config.components.add("recorder") + assert await async_setup_component(hass, "logbook", {}) + await hass.async_block_till_done() + + (v1_event, v2_event) = mock_humanify( + hass, + [ + MockRow( + ATTR_HUE_EVENT, + SAMPLE_V1_EVENT, + ), + MockRow( + ATTR_HUE_EVENT, + SAMPLE_V2_EVENT, + ), + ], + ) + + assert v1_event["name"] == "hue_tap" + assert v1_event["domain"] == DOMAIN + assert v1_event["message"] == "Event 18" + + assert v2_event["name"] == "wall_switch_with_2_controls_button" + assert v2_event["domain"] == DOMAIN + assert v2_event["message"] == "first button pressed initially" From 5f7594268a3137b1fe140cd4ba7a2d9246cb3b6c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 20 May 2022 01:50:13 -0400 Subject: [PATCH 0710/3516] Switch zwave_js firmware upload view to use device ID (#72219) * Switch zwave_js firmware upload view to use device ID * Store device registry in view --- homeassistant/components/zwave_js/api.py | 38 ++++++++++++++++-------- tests/components/zwave_js/test_api.py | 36 ++++++++++++---------- 2 files changed, 46 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index d27541fc61c..66c497a791f 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -57,7 +57,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import Unauthorized from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceEntry +import homeassistant.helpers.device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect from .config_validation import BITMASK_SCHEMA @@ -607,7 +607,7 @@ async def websocket_add_node( ) @callback - def device_registered(device: DeviceEntry) -> None: + def device_registered(device: dr.DeviceEntry) -> None: device_details = { "name": device.name, "id": device.id, @@ -1108,7 +1108,7 @@ async def websocket_replace_failed_node( ) @callback - def device_registered(device: DeviceEntry) -> None: + def device_registered(device: dr.DeviceEntry) -> None: device_details = { "name": device.name, "id": device.id, @@ -1819,25 +1819,37 @@ async def websocket_subscribe_firmware_update_status( class FirmwareUploadView(HomeAssistantView): """View to upload firmware.""" - url = r"/api/zwave_js/firmware/upload/{config_entry_id}/{node_id:\d+}" + url = r"/api/zwave_js/firmware/upload/{device_id}" name = "api:zwave_js:firmware:upload" - async def post( - self, request: web.Request, config_entry_id: str, node_id: str - ) -> web.Response: + def __init__(self) -> None: + """Initialize view.""" + super().__init__() + self._dev_reg: dr.DeviceRegistry | None = None + + async def post(self, request: web.Request, device_id: str) -> web.Response: """Handle upload.""" if not request["hass_user"].is_admin: raise Unauthorized() hass = request.app["hass"] - if config_entry_id not in hass.data[DOMAIN]: - raise web_exceptions.HTTPBadRequest - entry = hass.config_entries.async_get_entry(config_entry_id) - client: Client = hass.data[DOMAIN][config_entry_id][DATA_CLIENT] - node = client.driver.controller.nodes.get(int(node_id)) - if not node: + try: + node = async_get_node_from_device_id(hass, device_id) + except ValueError as err: + if "not loaded" in err.args[0]: + raise web_exceptions.HTTPBadRequest raise web_exceptions.HTTPNotFound + if not self._dev_reg: + self._dev_reg = dr.async_get(hass) + device = self._dev_reg.async_get(device_id) + assert device + entry = next( + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.entry_id in device.config_entries + ) + # Increase max payload request._client_max_size = 1024 * 1024 * 10 # pylint: disable=protected-access diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 60b83630add..3d491b98f93 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -2661,11 +2661,12 @@ async def test_firmware_upload_view( ): """Test the HTTP firmware upload view.""" client = await hass_client() + device = get_device(hass, multisensor_6) with patch( "homeassistant.components.zwave_js.api.begin_firmware_update", ) as mock_cmd: resp = await client.post( - f"/api/zwave_js/firmware/upload/{integration.entry_id}/{multisensor_6.node_id}", + f"/api/zwave_js/firmware/upload/{device.id}", data={"file": firmware_file}, ) assert mock_cmd.call_args[0][1:4] == (multisensor_6, "file", bytes(10)) @@ -2677,12 +2678,13 @@ async def test_firmware_upload_view_failed_command( ): """Test failed command for the HTTP firmware upload view.""" client = await hass_client() + device = get_device(hass, multisensor_6) with patch( "homeassistant.components.zwave_js.api.begin_firmware_update", side_effect=FailedCommand("test", "test"), ): resp = await client.post( - f"/api/zwave_js/firmware/upload/{integration.entry_id}/{multisensor_6.node_id}", + f"/api/zwave_js/firmware/upload/{device.id}", data={"file": firmware_file}, ) assert resp.status == HTTPStatus.BAD_REQUEST @@ -2692,9 +2694,10 @@ async def test_firmware_upload_view_invalid_payload( hass, multisensor_6, integration, hass_client ): """Test an invalid payload for the HTTP firmware upload view.""" + device = get_device(hass, multisensor_6) client = await hass_client() resp = await client.post( - f"/api/zwave_js/firmware/upload/{integration.entry_id}/{multisensor_6.node_id}", + f"/api/zwave_js/firmware/upload/{device.id}", data={"wrong_key": bytes(10)}, ) assert resp.status == HTTPStatus.BAD_REQUEST @@ -2702,40 +2705,43 @@ async def test_firmware_upload_view_invalid_payload( @pytest.mark.parametrize( "method, url", - [("post", "/api/zwave_js/firmware/upload/{}/{}")], + [("post", "/api/zwave_js/firmware/upload/{}")], ) async def test_node_view_non_admin_user( - multisensor_6, integration, hass_client, hass_admin_user, method, url + hass, multisensor_6, integration, hass_client, hass_admin_user, method, url ): """Test node level views for non-admin users.""" client = await hass_client() + device = get_device(hass, multisensor_6) # Verify we require admin user hass_admin_user.groups = [] - resp = await client.request( - method, url.format(integration.entry_id, multisensor_6.node_id) - ) + resp = await client.request(method, url.format(device.id)) assert resp.status == HTTPStatus.UNAUTHORIZED @pytest.mark.parametrize( "method, url", [ - ("post", "/api/zwave_js/firmware/upload/INVALID/1"), + ("post", "/api/zwave_js/firmware/upload/{}"), ], ) -async def test_view_invalid_entry_id(integration, hass_client, method, url): - """Test an invalid config entry id parameter.""" +async def test_view_unloaded_config_entry( + hass, multisensor_6, integration, hass_client, method, url +): + """Test an unloaded config entry raises Bad Request.""" client = await hass_client() - resp = await client.request(method, url) + device = get_device(hass, multisensor_6) + await hass.config_entries.async_unload(integration.entry_id) + resp = await client.request(method, url.format(device.id)) assert resp.status == HTTPStatus.BAD_REQUEST @pytest.mark.parametrize( "method, url", - [("post", "/api/zwave_js/firmware/upload/{}/111")], + [("post", "/api/zwave_js/firmware/upload/INVALID")], ) -async def test_view_invalid_node_id(integration, hass_client, method, url): - """Test an invalid config entry id parameter.""" +async def test_view_invalid_device_id(integration, hass_client, method, url): + """Test an invalid device id parameter.""" client = await hass_client() resp = await client.request(method, url.format(integration.entry_id)) assert resp.status == HTTPStatus.NOT_FOUND From 85ef0e3bd6bf59124b4942ee9c559ecfb017781c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 May 2022 08:27:24 +0200 Subject: [PATCH 0711/3516] Adjust device_automation type hints in nest (#72197) --- homeassistant/components/nest/device_trigger.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index 7bdbab214b8..9078f97f135 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -7,7 +7,10 @@ from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, ) -from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import ( + DEVICE_TRIGGER_BASE_SCHEMA, + GetAutomationsResult, +) from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -64,7 +67,7 @@ async def async_get_device_trigger_types( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, str]]: +) -> GetAutomationsResult: """List device triggers for a Nest device.""" nest_device_id = async_get_nest_device_id(hass, device_id) if not nest_device_id: From 4fb3a01c36b7c90f0b56ec4b34d49cea1fbcea36 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 May 2022 08:27:49 +0200 Subject: [PATCH 0712/3516] Adjust device_automation type hints in netatmo (#72136) * Adjust device_automation type hints in netatmo * Improve type hints --- homeassistant/components/netatmo/device_trigger.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/netatmo/device_trigger.py b/homeassistant/components/netatmo/device_trigger.py index ae62956b691..ce03aa91905 100644 --- a/homeassistant/components/netatmo/device_trigger.py +++ b/homeassistant/components/netatmo/device_trigger.py @@ -1,15 +1,16 @@ """Provides device automations for Netatmo.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, ) -from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import ( + DEVICE_TRIGGER_BASE_SCHEMA, + GetAutomationsResult, +) from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -100,7 +101,7 @@ async def async_validate_trigger_config( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> GetAutomationsResult: """List device triggers for Netatmo devices.""" registry = entity_registry.async_get(hass) device_registry = dr.async_get(hass) From 654b095498052a080c81687392710cd3da6ca8a3 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 20 May 2022 08:37:53 +0200 Subject: [PATCH 0713/3516] Move manual configuration of MQTT alarm control panel to the integration key (#72165) Add alarm_control_panel --- homeassistant/components/mqtt/__init__.py | 1 + .../components/mqtt/alarm_control_panel.py | 28 ++++++++++++++++--- .../mqtt/test_alarm_control_panel.py | 13 +++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e17a31480b1..e1de984f4df 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -190,6 +190,7 @@ MQTT_WILL_BIRTH_SCHEMA = vol.Schema( PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( { + vol.Optional(Platform.ALARM_CONTROL_PANEL.value): cv.ensure_list, vol.Optional(Platform.FAN.value): cv.ensure_list, vol.Optional(Platform.LIGHT.value): cv.ensure_list, } diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index a58e71e78d6..06c013ec744 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -1,6 +1,7 @@ """This platform enables the possibility to control a MQTT alarm.""" from __future__ import annotations +import asyncio import functools import logging import re @@ -44,8 +45,10 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_get_platform_config_from_yaml, async_setup_entry_helper, async_setup_platform_helper, + warn_for_legacy_schema, ) _LOGGER = logging.getLogger(__name__) @@ -82,7 +85,7 @@ DEFAULT_NAME = "MQTT Alarm" REMOTE_CODE = "REMOTE_CODE" REMOTE_CODE_TEXT = "REMOTE_CODE_TEXT" -PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_CODE): cv.string, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, @@ -110,7 +113,13 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -DISCOVERY_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA) +# Configuring MQTT alarm control panels under the alarm_control_panel platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA = vol.All( + cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_MODERN.schema), + warn_for_legacy_schema(alarm.DOMAIN), +) + +DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) async def async_setup_platform( @@ -119,7 +128,8 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up MQTT alarm control panel through configuration.yaml.""" + """Set up MQTT alarm control panel configured under the alarm_control_panel key (deprecated).""" + # Deprecated in HA Core 2022.6 await async_setup_platform_helper( hass, alarm.DOMAIN, config, async_add_entities, _async_setup_entity ) @@ -130,7 +140,17 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT alarm control panel dynamically through MQTT discovery.""" + """Set up MQTT alarm control panel through configuration.yaml and dynamically through MQTT discovery.""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, alarm.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 5c73463924e..f4d76d5474c 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -57,6 +57,7 @@ from .test_common import ( help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -848,3 +849,15 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): domain = alarm_control_panel.DOMAIN config = DEFAULT_CONFIG[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = alarm_control_panel.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None From 71d6a17073f4b29a150b52c3e4c61a1358243f4a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 May 2022 08:42:22 +0200 Subject: [PATCH 0714/3516] Adjust device_automation type hints in arcam_fmj (#72193) --- homeassistant/components/arcam_fmj/device_trigger.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/arcam_fmj/device_trigger.py b/homeassistant/components/arcam_fmj/device_trigger.py index 593250e4983..46c51789dfb 100644 --- a/homeassistant/components/arcam_fmj/device_trigger.py +++ b/homeassistant/components/arcam_fmj/device_trigger.py @@ -7,7 +7,10 @@ from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, ) -from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation import ( + DEVICE_TRIGGER_BASE_SCHEMA, + GetAutomationsResult, +) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_ID, @@ -33,7 +36,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, str]]: +) -> GetAutomationsResult: """List device triggers for Arcam FMJ Receiver control devices.""" registry = entity_registry.async_get(hass) triggers = [] From 99ad785d0a0d174da0d3a68169ba543565e6e69a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 May 2022 09:19:01 +0200 Subject: [PATCH 0715/3516] Adjust setup type hints in mqtt (#72227) --- .../components/mqtt/vacuum/__init__.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index f64a67820d4..4898a7b3351 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -1,9 +1,15 @@ """Support for MQTT vacuums.""" +from __future__ import annotations + import functools import voluptuous as vol from homeassistant.components import vacuum +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ..mixins import async_setup_entry_helper, async_setup_platform_helper from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE @@ -40,14 +46,23 @@ PLATFORM_SCHEMA = vol.All( ) -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, +) -> None: """Set up MQTT vacuum through configuration.yaml.""" await async_setup_platform_helper( hass, vacuum.DOMAIN, config, async_add_entities, _async_setup_entity ) -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 MQTT vacuum dynamically through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry From 74e8b076e5b9cc45c4e4bb6ac087c8e8f140c8bd Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 20 May 2022 12:05:53 +0300 Subject: [PATCH 0716/3516] Fix Shelly missing key config flow (#72116) --- .../components/shelly/config_flow.py | 64 +++++++------ tests/components/shelly/test_config_flow.py | 94 +++++++++++++++++-- 2 files changed, 117 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index abcfe689e93..41e0bd3031a 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -58,10 +58,12 @@ async def validate_input( options, ) await rpc_device.shutdown() + assert rpc_device.shelly + return { "title": get_rpc_device_name(rpc_device), CONF_SLEEP_PERIOD: 0, - "model": rpc_device.model, + "model": rpc_device.shelly.get("model"), "gen": 2, } @@ -119,21 +121,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) except HTTP_CONNECT_ERRORS: errors["base"] = "cannot_connect" - except KeyError: - errors["base"] = "firmware_not_fully_provisioned" except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - return self.async_create_entry( - title=device_info["title"], - data={ - **user_input, - CONF_SLEEP_PERIOD: device_info[CONF_SLEEP_PERIOD], - "model": device_info["model"], - "gen": device_info["gen"], - }, - ) + if device_info["model"]: + return self.async_create_entry( + title=device_info["title"], + data={ + **user_input, + CONF_SLEEP_PERIOD: device_info[CONF_SLEEP_PERIOD], + "model": device_info["model"], + "gen": device_info["gen"], + }, + ) + errors["base"] = "firmware_not_fully_provisioned" return self.async_show_form( step_id="user", data_schema=HOST_SCHEMA, errors=errors @@ -162,22 +164,22 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" except aioshelly.exceptions.JSONRPCError: errors["base"] = "cannot_connect" - except KeyError: - errors["base"] = "firmware_not_fully_provisioned" except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - return self.async_create_entry( - title=device_info["title"], - data={ - **user_input, - CONF_HOST: self.host, - CONF_SLEEP_PERIOD: device_info[CONF_SLEEP_PERIOD], - "model": device_info["model"], - "gen": device_info["gen"], - }, - ) + if device_info["model"]: + return self.async_create_entry( + title=device_info["title"], + data={ + **user_input, + CONF_HOST: self.host, + CONF_SLEEP_PERIOD: device_info[CONF_SLEEP_PERIOD], + "model": device_info["model"], + "gen": device_info["gen"], + }, + ) + errors["base"] = "firmware_not_fully_provisioned" else: user_input = {} @@ -223,8 +225,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: self.device_info = await validate_input(self.hass, self.host, self.info, {}) - except KeyError: - LOGGER.debug("Shelly host %s firmware not fully provisioned", self.host) except HTTP_CONNECT_ERRORS: return self.async_abort(reason="cannot_connect") @@ -235,7 +235,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle discovery confirm.""" errors: dict[str, str] = {} - try: + + if not self.device_info["model"]: + errors["base"] = "firmware_not_fully_provisioned" + model = "Shelly" + else: + model = get_model_name(self.info) if user_input is not None: return self.async_create_entry( title=self.device_info["title"], @@ -246,15 +251,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): "gen": self.device_info["gen"], }, ) - except KeyError: - errors["base"] = "firmware_not_fully_provisioned" - else: self._set_confirm_only() return self.async_show_form( step_id="confirm_discovery", description_placeholders={ - "model": get_model_name(self.info), + "model": model, "host": self.host, }, errors=errors, diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 713999de36f..145bcbb3566 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -58,7 +58,7 @@ async def test_form(hass, gen): "aioshelly.rpc_device.RpcDevice.create", new=AsyncMock( return_value=Mock( - model="SHSW-1", + shelly={"model": "SHSW-1", "gen": gen}, config=MOCK_CONFIG, shutdown=AsyncMock(), ) @@ -175,7 +175,7 @@ async def test_form_auth(hass, test_data): "aioshelly.rpc_device.RpcDevice.create", new=AsyncMock( return_value=Mock( - model="SHSW-1", + shelly={"model": "SHSW-1", "gen": gen}, config=MOCK_CONFIG, shutdown=AsyncMock(), ) @@ -225,19 +225,23 @@ async def test_form_errors_get_info(hass, error): assert result2["errors"] == {"base": base_error} -@pytest.mark.parametrize("error", [(KeyError, "firmware_not_fully_provisioned")]) -async def test_form_missing_key_get_info(hass, error): - """Test we handle missing key.""" - exc, base_error = error +async def test_form_missing_model_key(hass): + """Test we handle missing Shelly model key.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( "aioshelly.common.get_info", - return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False, "gen": "2"}, + return_value={"mac": "test-mac", "auth": False, "gen": "2"}, ), patch( - "homeassistant.components.shelly.config_flow.validate_input", - side_effect=KeyError, + "aioshelly.rpc_device.RpcDevice.create", + new=AsyncMock( + return_value=Mock( + shelly={"gen": 2}, + config=MOCK_CONFIG, + shutdown=AsyncMock(), + ) + ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -245,7 +249,77 @@ async def test_form_missing_key_get_info(hass, error): ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["errors"] == {"base": base_error} + assert result2["errors"] == {"base": "firmware_not_fully_provisioned"} + + +async def test_form_missing_model_key_auth_enabled(hass): + """Test we handle missing Shelly model key when auth enabled.""" + 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"] == {} + + with patch( + "aioshelly.common.get_info", + return_value={"mac": "test-mac", "auth": True, "gen": 2}, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "aioshelly.rpc_device.RpcDevice.create", + new=AsyncMock( + return_value=Mock( + shelly={"gen": 2}, + config=MOCK_CONFIG, + shutdown=AsyncMock(), + ) + ), + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], {"password": "1234"} + ) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["errors"] == {"base": "firmware_not_fully_provisioned"} + + +async def test_form_missing_model_key_zeroconf(hass, caplog): + """Test we handle missing Shelly model key via zeroconf.""" + + with patch( + "aioshelly.common.get_info", + return_value={"mac": "test-mac", "auth": False, "gen": 2}, + ), patch( + "aioshelly.rpc_device.RpcDevice.create", + new=AsyncMock( + return_value=Mock( + shelly={"gen": 2}, + config=MOCK_CONFIG, + shutdown=AsyncMock(), + ) + ), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "firmware_not_fully_provisioned"} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "firmware_not_fully_provisioned"} @pytest.mark.parametrize( From a72ce2415b41c02f4dabf327867dcf16917ed06e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 20 May 2022 11:15:08 +0200 Subject: [PATCH 0717/3516] Adjust setup type hints in agent_dvr (#72224) --- .../components/agent_dvr/alarm_control_panel.py | 9 +++++++-- homeassistant/components/agent_dvr/camera.py | 15 +++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/agent_dvr/alarm_control_panel.py b/homeassistant/components/agent_dvr/alarm_control_panel.py index 3e264a1985d..8978be97c1d 100644 --- a/homeassistant/components/agent_dvr/alarm_control_panel.py +++ b/homeassistant/components/agent_dvr/alarm_control_panel.py @@ -3,13 +3,16 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntity, AlarmControlPanelEntityFeature, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONNECTION, DOMAIN as AGENT_DOMAIN @@ -23,8 +26,10 @@ CONST_ALARM_CONTROL_PANEL_NAME = "Alarm Panel" async def async_setup_entry( - hass, config_entry, async_add_entities, discovery_info=None -): + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the Agent DVR Alarm Control Panels.""" async_add_entities( [AgentBaseStation(hass.data[AGENT_DOMAIN][config_entry.entry_id][CONNECTION])] diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py index 49ca342ca09..9aab33efd5a 100644 --- a/homeassistant/components/agent_dvr/camera.py +++ b/homeassistant/components/agent_dvr/camera.py @@ -6,9 +6,14 @@ from agent import AgentError from homeassistant.components.camera import CameraEntityFeature from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.helpers import entity_platform +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import ( + AddEntitiesCallback, + async_get_current_platform, +) from .const import ( ATTRIBUTION, @@ -37,8 +42,10 @@ CAMERA_SERVICES = { async def async_setup_entry( - hass, config_entry, async_add_entities, discovery_info=None -): + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up the Agent cameras.""" filter_urllib3_logging() cameras = [] @@ -55,7 +62,7 @@ async def async_setup_entry( async_add_entities(cameras) - platform = entity_platform.async_get_current_platform() + platform = async_get_current_platform() for service, method in CAMERA_SERVICES.items(): platform.async_register_entity_service(service, {}, method) From cbcf832436bd194e2a59df828ad14c1621e0128e Mon Sep 17 00:00:00 2001 From: SNoof85 Date: Fri, 20 May 2022 15:22:38 +0200 Subject: [PATCH 0718/3516] Fix for Xiaomi miio fan speed (#72027) Update fan.py --- homeassistant/components/xiaomi_miio/fan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 1e525887c56..969093545f0 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -1041,7 +1041,7 @@ class XiaomiFanMiot(XiaomiGenericFan): self._preset_mode = self.coordinator.data.mode.name self._oscillating = self.coordinator.data.oscillate if self.coordinator.data.is_on: - self._percentage = self.coordinator.data.fan_speed + self._percentage = self.coordinator.data.speed else: self._percentage = 0 From d459a5c66ecd1ad3fd769f42079220a05aa417d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 May 2022 09:46:01 -0500 Subject: [PATCH 0719/3516] Include context state in logbook responses to improve localization (#72222) * Include context state in logbook responses to improve localization * reduce payload, dont send context_event_type if sending context_state --- homeassistant/components/logbook/__init__.py | 3 +- tests/components/logbook/test_init.py | 63 ++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index e2073800c25..635868310f6 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -87,6 +87,7 @@ CONTEXT_ENTITY_ID = "context_entity_id" CONTEXT_ENTITY_ID_NAME = "context_entity_id_name" CONTEXT_EVENT_TYPE = "context_event_type" CONTEXT_DOMAIN = "context_domain" +CONTEXT_STATE = "context_state" CONTEXT_SERVICE = "context_service" CONTEXT_NAME = "context_name" CONTEXT_MESSAGE = "context_message" @@ -674,12 +675,12 @@ class ContextAugmenter: # State change if context_entity_id := context_row.entity_id: + data[CONTEXT_STATE] = context_row.state data[CONTEXT_ENTITY_ID] = context_entity_id if self.include_entity_name: data[CONTEXT_ENTITY_ID_NAME] = self.entity_name_cache.get( context_entity_id, context_row ) - data[CONTEXT_EVENT_TYPE] = event_type return # Call service diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 05df4f2ab3b..3b18c594e6e 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -2738,3 +2738,66 @@ async def test_logbook_select_entities_context_id(hass, recorder_mock, hass_clie assert json_dict[3]["context_domain"] == "light" assert json_dict[3]["context_service"] == "turn_off" assert json_dict[3]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" + + +async def test_get_events_with_context_state(hass, hass_ws_client, recorder_mock): + """Test logbook get_events with a context state.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook") + ] + ) + await async_recorder_block_till_done(hass) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.states.async_set("binary_sensor.is_light", STATE_ON) + hass.states.async_set("light.kitchen1", STATE_OFF) + hass.states.async_set("light.kitchen2", STATE_OFF) + + context = ha.Context( + id="ac5bd62de45711eaaeb351041eec8dd9", + user_id="b400facee45711eaa9308bfd3d19e474", + ) + hass.states.async_set("binary_sensor.is_light", STATE_OFF, context=context) + await hass.async_block_till_done() + hass.states.async_set( + "light.kitchen1", STATE_ON, {"brightness": 100}, context=context + ) + await hass.async_block_till_done() + hass.states.async_set( + "light.kitchen2", STATE_ON, {"brightness": 200}, context=context + ) + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + + await client.send_json( + { + "id": 1, + "type": "logbook/get_events", + "start_time": now.isoformat(), + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + results = response["result"] + assert results[1]["entity_id"] == "binary_sensor.is_light" + assert results[1]["state"] == "off" + assert "context_state" not in results[1] + assert results[2]["entity_id"] == "light.kitchen1" + assert results[2]["state"] == "on" + assert results[2]["context_entity_id"] == "binary_sensor.is_light" + assert results[2]["context_state"] == "off" + assert results[2]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + assert "context_event_type" not in results[2] + assert results[3]["entity_id"] == "light.kitchen2" + assert results[3]["state"] == "on" + assert results[3]["context_entity_id"] == "binary_sensor.is_light" + assert results[3]["context_state"] == "off" + assert results[3]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + assert "context_event_type" not in results[3] From 775be354a6935394013094835c999f015a85b8c7 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 20 May 2022 07:47:18 -0700 Subject: [PATCH 0720/3516] Cleanup nest async methods that do not need to actually await (#72170) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/nest/__init__.py | 9 ++++-- homeassistant/components/nest/camera_sdm.py | 14 +++------ homeassistant/components/nest/climate_sdm.py | 12 ++------ homeassistant/components/nest/const.py | 1 + .../components/nest/device_trigger.py | 13 ++++---- homeassistant/components/nest/diagnostics.py | 27 +++++++---------- homeassistant/components/nest/media_source.py | 21 ++++++------- homeassistant/components/nest/sensor_sdm.py | 13 ++------ tests/components/nest/test_diagnostics.py | 30 +------------------ 9 files changed, 43 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 7d8353738ff..0920b37e6ef 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -54,6 +54,7 @@ from . import api, config_flow from .const import ( CONF_PROJECT_ID, CONF_SUBSCRIBER_ID, + DATA_DEVICE_MANAGER, DATA_NEST_CONFIG, DATA_SDM, DATA_SUBSCRIBER, @@ -63,8 +64,8 @@ from .events import EVENT_NAME_MAP, NEST_EVENT from .legacy import async_setup_legacy, async_setup_legacy_entry from .media_source import ( async_get_media_event_store, + async_get_media_source_devices, async_get_transcoder, - get_media_source_devices, ) _LOGGER = logging.getLogger(__name__) @@ -205,7 +206,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from err try: - await subscriber.async_get_device_manager() + device_manager = await subscriber.async_get_device_manager() except ApiException as err: if DATA_NEST_UNAVAILABLE not in hass.data[DOMAIN]: _LOGGER.error("Device manager error: %s", err) @@ -215,6 +216,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None) hass.data[DOMAIN][DATA_SUBSCRIBER] = subscriber + hass.data[DOMAIN][DATA_DEVICE_MANAGER] = device_manager hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -232,6 +234,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].pop(DATA_SUBSCRIBER) + hass.data[DOMAIN].pop(DATA_DEVICE_MANAGER) hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None) return unload_ok @@ -275,7 +278,7 @@ class NestEventViewBase(HomeAssistantView, ABC): if not user.permissions.check_entity(entry.entity_id, POLICY_READ): raise Unauthorized(entity_id=entry.entity_id) - devices = await get_media_source_devices(self.hass) + devices = async_get_media_source_devices(self.hass) if not (nest_device := devices.get(device_id)): return self._json_error( f"No Nest Device found for '{device_id}'", HTTPStatus.NOT_FOUND diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index a46af2979f4..6e14100e881 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -15,19 +15,20 @@ from google_nest_sdm.camera_traits import ( StreamingProtocol, ) from google_nest_sdm.device import Device +from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.exceptions import ApiException from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.components.camera.const import StreamType from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError, PlatformNotReady +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -from .const import DATA_SUBSCRIBER, DOMAIN +from .const import DATA_DEVICE_MANAGER, DOMAIN from .device_info import NestDeviceInfo _LOGGER = logging.getLogger(__name__) @@ -43,14 +44,7 @@ async def async_setup_sdm_entry( ) -> None: """Set up the cameras.""" - subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] - try: - device_manager = await subscriber.async_get_device_manager() - except ApiException as err: - raise PlatformNotReady from err - - # Fetch initial data so we have data when entities subscribe. - + device_manager: DeviceManager = hass.data[DOMAIN][DATA_DEVICE_MANAGER] entities = [] for device in device_manager.devices.values(): if ( diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 89048b9f624..bbbf83501f7 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -4,8 +4,8 @@ from __future__ import annotations from typing import Any, cast from google_nest_sdm.device import Device +from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.device_traits import FanTrait, TemperatureTrait -from google_nest_sdm.exceptions import ApiException from google_nest_sdm.thermostat_traits import ( ThermostatEcoTrait, ThermostatHeatCoolTrait, @@ -30,11 +30,10 @@ from homeassistant.components.climate.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_SUBSCRIBER, DOMAIN +from .const import DATA_DEVICE_MANAGER, DOMAIN from .device_info import NestDeviceInfo # Mapping for sdm.devices.traits.ThermostatMode mode field @@ -80,12 +79,7 @@ async def async_setup_sdm_entry( ) -> None: """Set up the client entities.""" - subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] - try: - device_manager = await subscriber.async_get_device_manager() - except ApiException as err: - raise PlatformNotReady from err - + device_manager: DeviceManager = hass.data[DOMAIN][DATA_DEVICE_MANAGER] entities = [] for device in device_manager.devices.values(): if ThermostatHvacTrait.NAME in device.traits: diff --git a/homeassistant/components/nest/const.py b/homeassistant/components/nest/const.py index 7aabcfe8d77..bd951756eae 100644 --- a/homeassistant/components/nest/const.py +++ b/homeassistant/components/nest/const.py @@ -3,6 +3,7 @@ DOMAIN = "nest" DATA_SDM = "sdm" DATA_SUBSCRIBER = "subscriber" +DATA_DEVICE_MANAGER = "device_manager" DATA_NEST_CONFIG = "nest_config" WEB_AUTH_DOMAIN = DOMAIN diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index 9078f97f135..e2f4288d122 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -1,6 +1,7 @@ """Provides device automations for Nest.""" from __future__ import annotations +from google_nest_sdm.device_manager import DeviceManager import voluptuous as vol from homeassistant.components.automation import ( @@ -20,7 +21,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import ConfigType -from .const import DATA_SUBSCRIBER, DOMAIN +from .const import DATA_DEVICE_MANAGER, DOMAIN from .events import DEVICE_TRAIT_TRIGGER_MAP, NEST_EVENT DEVICE = "device" @@ -45,14 +46,12 @@ def async_get_nest_device_id(hass: HomeAssistant, device_id: str) -> str | None: return None -async def async_get_device_trigger_types( +@callback +def async_get_device_trigger_types( hass: HomeAssistant, nest_device_id: str ) -> list[str]: """List event triggers supported for a Nest device.""" - # All devices should have already been loaded so any failures here are - # "shouldn't happen" cases - subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] - device_manager = await subscriber.async_get_device_manager() + device_manager: DeviceManager = hass.data[DOMAIN][DATA_DEVICE_MANAGER] if not (nest_device := device_manager.devices.get(nest_device_id)): raise InvalidDeviceAutomationConfig(f"Nest device not found {nest_device_id}") @@ -72,7 +71,7 @@ async def async_get_triggers( nest_device_id = async_get_nest_device_id(hass, device_id) if not nest_device_id: raise InvalidDeviceAutomationConfig(f"Device not found {device_id}") - trigger_types = await async_get_device_trigger_types(hass, nest_device_id) + trigger_types = async_get_device_trigger_types(hass, nest_device_id) return [ { CONF_PLATFORM: DEVICE, diff --git a/homeassistant/components/nest/diagnostics.py b/homeassistant/components/nest/diagnostics.py index d178d52393e..c21842d5939 100644 --- a/homeassistant/components/nest/diagnostics.py +++ b/homeassistant/components/nest/diagnostics.py @@ -6,43 +6,39 @@ from typing import Any from google_nest_sdm import diagnostics from google_nest_sdm.device import Device +from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.device_traits import InfoTrait -from google_nest_sdm.exceptions import ApiException from homeassistant.components.camera import diagnostics as camera_diagnostics from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceEntry -from .const import DATA_SDM, DATA_SUBSCRIBER, DOMAIN +from .const import DATA_DEVICE_MANAGER, DATA_SDM, DOMAIN REDACT_DEVICE_TRAITS = {InfoTrait.NAME} -async def _get_nest_devices( +@callback +def _async_get_nest_devices( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Device]: """Return dict of available devices.""" if DATA_SDM not in config_entry.data: return {} - if DATA_SUBSCRIBER not in hass.data[DOMAIN]: + if DATA_DEVICE_MANAGER not in hass.data[DOMAIN]: return {} - subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] - device_manager = await subscriber.async_get_device_manager() - devices: dict[str, Device] = device_manager.devices - return devices + device_manager: DeviceManager = hass.data[DOMAIN][DATA_DEVICE_MANAGER] + return device_manager.devices async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict: """Return diagnostics for a config entry.""" - try: - nest_devices = await _get_nest_devices(hass, config_entry) - except ApiException as err: - return {"error": str(err)} + nest_devices = _async_get_nest_devices(hass, config_entry) if not nest_devices: return {} data: dict[str, Any] = { @@ -65,10 +61,7 @@ async def async_get_device_diagnostics( device: DeviceEntry, ) -> dict: """Return diagnostics for a device.""" - try: - nest_devices = await _get_nest_devices(hass, config_entry) - except ApiException as err: - return {"error": str(err)} + nest_devices = _async_get_nest_devices(hass, config_entry) nest_device_id = next(iter(device.identifiers))[1] nest_device = nest_devices.get(nest_device_id) return nest_device.get_diagnostics() if nest_device else {} diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 7a8ae49bdbc..e4e26153b3a 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -25,6 +25,7 @@ import os from google_nest_sdm.camera_traits import CameraClipPreviewTrait, CameraEventImageTrait from google_nest_sdm.device import Device +from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.event import EventImageType, ImageEventBase from google_nest_sdm.event_media import ( ClipPreviewSession, @@ -50,13 +51,13 @@ from homeassistant.components.media_source.models import ( MediaSourceItem, PlayMedia, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.storage import Store from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.util import dt as dt_util -from .const import DATA_SUBSCRIBER, DOMAIN +from .const import DATA_DEVICE_MANAGER, DOMAIN from .device_info import NestDeviceInfo from .events import EVENT_NAME_MAP, MEDIA_SOURCE_EVENT_TITLE_MAP @@ -267,13 +268,13 @@ async def async_get_media_source(hass: HomeAssistant) -> MediaSource: return NestMediaSource(hass) -async def get_media_source_devices(hass: HomeAssistant) -> Mapping[str, Device]: +@callback +def async_get_media_source_devices(hass: HomeAssistant) -> Mapping[str, Device]: """Return a mapping of device id to eligible Nest event media devices.""" - if DATA_SUBSCRIBER not in hass.data[DOMAIN]: + if DATA_DEVICE_MANAGER not in hass.data[DOMAIN]: # Integration unloaded, or is legacy nest integration return {} - subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] - device_manager = await subscriber.async_get_device_manager() + device_manager: DeviceManager = hass.data[DOMAIN][DATA_DEVICE_MANAGER] device_registry = dr.async_get(hass) devices = {} for device in device_manager.devices.values(): @@ -339,7 +340,7 @@ class NestMediaSource(MediaSource): media_id: MediaId | None = parse_media_id(item.identifier) if not media_id: raise Unresolvable("No identifier specified for MediaSourceItem") - devices = await self.devices() + devices = async_get_media_source_devices(self.hass) if not (device := devices.get(media_id.device_id)): raise Unresolvable( "Unable to find device with identifier: %s" % item.identifier @@ -376,7 +377,7 @@ class NestMediaSource(MediaSource): _LOGGER.debug( "Browsing media for identifier=%s, media_id=%s", item.identifier, media_id ) - devices = await self.devices() + devices = async_get_media_source_devices(self.hass) if media_id is None: # Browse the root and return child devices browse_root = _browse_root() @@ -443,10 +444,6 @@ class NestMediaSource(MediaSource): ) return _browse_image_event(media_id, device, single_image) - async def devices(self) -> Mapping[str, Device]: - """Return all event media related devices.""" - return await get_media_source_devices(self.hass) - async def _async_get_clip_preview_sessions( device: Device, diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index b9c13f6b9c7..d33aa3eff8b 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -4,8 +4,8 @@ from __future__ import annotations import logging from google_nest_sdm.device import Device +from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.device_traits import HumidityTrait, TemperatureTrait -from google_nest_sdm.exceptions import ApiException from homeassistant.components.sensor import ( SensorDeviceClass, @@ -15,10 +15,9 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_SUBSCRIBER, DOMAIN +from .const import DATA_DEVICE_MANAGER, DOMAIN from .device_info import NestDeviceInfo _LOGGER = logging.getLogger(__name__) @@ -37,13 +36,7 @@ async def async_setup_sdm_entry( ) -> None: """Set up the sensors.""" - subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] - try: - device_manager = await subscriber.async_get_device_manager() - except ApiException as err: - _LOGGER.warning("Failed to get devices: %s", err) - raise PlatformNotReady from err - + device_manager: DeviceManager = hass.data[DOMAIN][DATA_DEVICE_MANAGER] entities: list[SensorEntity] = [] for device in device_manager.devices.values(): if TemperatureTrait.NAME in device.traits: diff --git a/tests/components/nest/test_diagnostics.py b/tests/components/nest/test_diagnostics.py index b69f5970c2d..8e28222e356 100644 --- a/tests/components/nest/test_diagnostics.py +++ b/tests/components/nest/test_diagnostics.py @@ -2,7 +2,7 @@ from unittest.mock import patch -from google_nest_sdm.exceptions import ApiException, SubscriberException +from google_nest_sdm.exceptions import SubscriberException import pytest from homeassistant.components.nest.const import DOMAIN @@ -139,34 +139,6 @@ async def test_setup_susbcriber_failure( assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == {} -async def test_device_manager_failure( - hass, - hass_client, - config_entry, - setup_platform, - create_device, -): - """Test configuration error.""" - create_device.create(raw_data=DEVICE_API_DATA) - await setup_platform() - assert config_entry.state is ConfigEntryState.LOADED - - device_registry = dr.async_get(hass) - device = device_registry.async_get_device({(DOMAIN, NEST_DEVICE_ID)}) - assert device is not None - - with patch( - "homeassistant.components.nest.diagnostics._get_nest_devices", - side_effect=ApiException("Device manager failure"), - ): - assert await get_diagnostics_for_config_entry( - hass, hass_client, config_entry - ) == {"error": "Device manager failure"} - assert await get_diagnostics_for_device( - hass, hass_client, config_entry, device - ) == {"error": "Device manager failure"} - - @pytest.mark.parametrize("nest_test_config", [TEST_CONFIG_LEGACY]) async def test_legacy_config_entry_diagnostics( hass, hass_client, config_entry, setup_base_platform From 614c44b9f0a4c1e1daa1b50f5bee181a1e1325ee Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 20 May 2022 09:48:22 -0500 Subject: [PATCH 0721/3516] Bump plexapi to 4.11.1 (#72121) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 084356abf7b..2e2db01de77 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.10.1", + "plexapi==4.11.1", "plexauth==0.0.6", "plexwebsocket==0.0.13" ], diff --git a/requirements_all.txt b/requirements_all.txt index 4ac46752539..a5aae3b6880 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1233,7 +1233,7 @@ pillow==9.1.0 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.10.1 +plexapi==4.11.1 # homeassistant.components.plex plexauth==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0327dcfa142..9fefab5b2d5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -835,7 +835,7 @@ pilight==0.1.1 pillow==9.1.0 # homeassistant.components.plex -plexapi==4.10.1 +plexapi==4.11.1 # homeassistant.components.plex plexauth==0.0.6 From e9c861f2b2536690c567a2f7d76a5d1c0a37dbe2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 May 2022 09:49:26 -0500 Subject: [PATCH 0722/3516] Add support for cover positions in bond (#72180) --- homeassistant/components/bond/button.py | 14 +++++ homeassistant/components/bond/cover.py | 24 +++++++- homeassistant/components/bond/utils.py | 4 ++ tests/components/bond/test_cover.py | 79 ++++++++++++++++++++++++- 4 files changed, 118 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bond/button.py b/homeassistant/components/bond/button.py index 700c6b5f407..0152bedde23 100644 --- a/homeassistant/components/bond/button.py +++ b/homeassistant/components/bond/button.py @@ -223,6 +223,20 @@ BUTTONS: tuple[BondButtonEntityDescription, ...] = ( mutually_exclusive=Action.OPEN, argument=None, ), + BondButtonEntityDescription( + key=Action.INCREASE_POSITION, + name="Increase Position", + icon="mdi:plus-box", + mutually_exclusive=Action.SET_POSITION, + argument=STEP_SIZE, + ), + BondButtonEntityDescription( + key=Action.DECREASE_POSITION, + name="Decrease Position", + icon="mdi:minus-box", + mutually_exclusive=Action.SET_POSITION, + argument=STEP_SIZE, + ), ) diff --git a/homeassistant/components/bond/cover.py b/homeassistant/components/bond/cover.py index 664431a3145..a50f7b93bbb 100644 --- a/homeassistant/components/bond/cover.py +++ b/homeassistant/components/bond/cover.py @@ -6,6 +6,7 @@ from typing import Any from bond_api import Action, BPUPSubscriptions, DeviceType from homeassistant.components.cover import ( + ATTR_POSITION, CoverDeviceClass, CoverEntity, CoverEntityFeature, @@ -20,6 +21,16 @@ from .entity import BondEntity from .utils import BondDevice, BondHub +def _bond_to_hass_position(bond_position: int) -> int: + """Convert bond 0-open 100-closed to hass 0-closed 100-open.""" + return abs(bond_position - 100) + + +def _hass_to_bond_position(hass_position: int) -> int: + """Convert hass 0-closed 100-open to bond 0-open 100-closed.""" + return 100 - hass_position + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -50,6 +61,8 @@ class BondCover(BondEntity, CoverEntity): """Create HA entity representing Bond cover.""" super().__init__(hub, device, bpup_subs) supported_features = 0 + if self._device.supports_set_position(): + supported_features |= CoverEntityFeature.SET_POSITION if self._device.supports_open(): supported_features |= CoverEntityFeature.OPEN if self._device.supports_close(): @@ -67,8 +80,15 @@ class BondCover(BondEntity, CoverEntity): def _apply_state(self, state: dict) -> None: cover_open = state.get("open") - self._attr_is_closed = ( - True if cover_open == 0 else False if cover_open == 1 else None + self._attr_is_closed = None if cover_open is None else cover_open == 0 + if (bond_position := state.get("position")) is not None: + self._attr_current_cover_position = _bond_to_hass_position(bond_position) + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Set the cover position.""" + await self._hub.bond.action( + self._device.device_id, + Action.set_position(_hass_to_bond_position(kwargs[ATTR_POSITION])), ) async def async_open_cover(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index 785d7dfbd00..fc78c5758c1 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -82,6 +82,10 @@ class BondDevice: """Return True if this device supports any of the direction related commands.""" return self._has_any_action({Action.SET_DIRECTION}) + def supports_set_position(self) -> bool: + """Return True if this device supports setting the position.""" + return self._has_any_action({Action.SET_POSITION}) + def supports_open(self) -> bool: """Return True if this device supports opening.""" return self._has_any_action({Action.OPEN}) diff --git a/tests/components/bond/test_cover.py b/tests/components/bond/test_cover.py index 7a27617d607..ca467d4a38d 100644 --- a/tests/components/bond/test_cover.py +++ b/tests/components/bond/test_cover.py @@ -4,15 +4,22 @@ from datetime import timedelta from bond_api import Action, DeviceType from homeassistant import core -from homeassistant.components.cover import DOMAIN as COVER_DOMAIN, STATE_CLOSED +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_POSITION, + DOMAIN as COVER_DOMAIN, + STATE_CLOSED, +) from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, SERVICE_OPEN_COVER, SERVICE_OPEN_COVER_TILT, + SERVICE_SET_COVER_POSITION, SERVICE_STOP_COVER, SERVICE_STOP_COVER_TILT, + STATE_OPEN, STATE_UNKNOWN, ) from homeassistant.helpers import entity_registry as er @@ -38,6 +45,15 @@ def shades(name: str): } +def shades_with_position(name: str): + """Create motorized shades that supports set position.""" + return { + "name": name, + "type": DeviceType.MOTORIZED_SHADES, + "actions": [Action.OPEN, Action.CLOSE, Action.HOLD, Action.SET_POSITION], + } + + def tilt_only_shades(name: str): """Create motorized shades that only tilt.""" return { @@ -236,3 +252,64 @@ async def test_cover_available(hass: core.HomeAssistant): await help_test_entity_available( hass, COVER_DOMAIN, shades("name-1"), "cover.name_1" ) + + +async def test_set_position_cover(hass: core.HomeAssistant): + """Tests that set position cover command delegates to API.""" + await setup_platform( + hass, + COVER_DOMAIN, + shades_with_position("name-1"), + bond_device_id="test-device-id", + ) + + with patch_bond_action() as mock_hold, patch_bond_device_state( + return_value={"position": 0, "open": 1} + ): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.name_1", ATTR_POSITION: 100}, + blocking=True, + ) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + mock_hold.assert_called_once_with("test-device-id", Action.set_position(0)) + entity_state = hass.states.get("cover.name_1") + assert entity_state.state == STATE_OPEN + assert entity_state.attributes[ATTR_CURRENT_POSITION] == 100 + + with patch_bond_action() as mock_hold, patch_bond_device_state( + return_value={"position": 100, "open": 0} + ): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.name_1", ATTR_POSITION: 0}, + blocking=True, + ) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + mock_hold.assert_called_once_with("test-device-id", Action.set_position(100)) + entity_state = hass.states.get("cover.name_1") + assert entity_state.state == STATE_CLOSED + assert entity_state.attributes[ATTR_CURRENT_POSITION] == 0 + + with patch_bond_action() as mock_hold, patch_bond_device_state( + return_value={"position": 40, "open": 1} + ): + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.name_1", ATTR_POSITION: 60}, + blocking=True, + ) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + + mock_hold.assert_called_once_with("test-device-id", Action.set_position(40)) + entity_state = hass.states.get("cover.name_1") + assert entity_state.state == STATE_OPEN + assert entity_state.attributes[ATTR_CURRENT_POSITION] == 60 From 5866bf48da27a0d769b2d340ce69b64659fadf77 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 20 May 2022 09:07:02 -0700 Subject: [PATCH 0723/3516] Bump gcal_sync to 0.9.0 (#72237) --- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 03d15597c44..081eae34a95 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", - "requirements": ["gcal-sync==0.8.1", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==0.9.0", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index a5aae3b6880..d74741129d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -689,7 +689,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==0.8.1 +gcal-sync==0.9.0 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9fefab5b2d5..9ca019992c7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -492,7 +492,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==0.8.1 +gcal-sync==0.9.0 # homeassistant.components.geocaching geocachingapi==0.2.1 From 1e7b187fc6d9d7fd20f174d7307d3fa6a31dc05a Mon Sep 17 00:00:00 2001 From: Matrix Date: Sat, 21 May 2022 01:06:47 +0800 Subject: [PATCH 0724/3516] Add yolink sensors (#72186) * Add more sensor support * change codeowner to active account * fix suggest --- CODEOWNERS | 4 +-- .../components/yolink/binary_sensor.py | 31 +++++++++++++--- homeassistant/components/yolink/const.py | 3 ++ homeassistant/components/yolink/manifest.json | 2 +- homeassistant/components/yolink/sensor.py | 35 ++++++++++++++++--- 5 files changed, 64 insertions(+), 11 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index a1d329cbee0..c3e36d1fef7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1201,8 +1201,8 @@ build.json @home-assistant/supervisor /tests/components/yeelight/ @zewelor @shenxn @starkillerOG @alexyao2015 /homeassistant/components/yeelightsunflower/ @lindsaymarkward /homeassistant/components/yi/ @bachya -/homeassistant/components/yolink/ @YoSmart-Inc -/tests/components/yolink/ @YoSmart-Inc +/homeassistant/components/yolink/ @matrixd2 +/tests/components/yolink/ @matrixd2 /homeassistant/components/youless/ @gjong /tests/components/youless/ @gjong /homeassistant/components/zengge/ @emontnemery diff --git a/homeassistant/components/yolink/binary_sensor.py b/homeassistant/components/yolink/binary_sensor.py index c3f2d10cf4c..494a7ff10eb 100644 --- a/homeassistant/components/yolink/binary_sensor.py +++ b/homeassistant/components/yolink/binary_sensor.py @@ -15,7 +15,13 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_COORDINATOR, ATTR_DEVICE_DOOR_SENSOR, DOMAIN +from .const import ( + ATTR_COORDINATOR, + ATTR_DEVICE_DOOR_SENSOR, + ATTR_DEVICE_LEAK_SENSOR, + ATTR_DEVICE_MOTION_SENSOR, + DOMAIN, +) from .coordinator import YoLinkCoordinator from .entity import YoLinkEntity @@ -25,20 +31,37 @@ class YoLinkBinarySensorEntityDescription(BinarySensorEntityDescription): """YoLink BinarySensorEntityDescription.""" exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True + key: str = "state" value: Callable[[str], bool | None] = lambda _: None -SENSOR_DEVICE_TYPE = [ATTR_DEVICE_DOOR_SENSOR] +SENSOR_DEVICE_TYPE = [ + ATTR_DEVICE_DOOR_SENSOR, + ATTR_DEVICE_MOTION_SENSOR, + ATTR_DEVICE_LEAK_SENSOR, +] SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( YoLinkBinarySensorEntityDescription( - key="state", icon="mdi:door", device_class=BinarySensorDeviceClass.DOOR, - name="state", + name="State", value=lambda value: value == "open", exists_fn=lambda device: device.device_type in [ATTR_DEVICE_DOOR_SENSOR], ), + YoLinkBinarySensorEntityDescription( + device_class=BinarySensorDeviceClass.MOTION, + name="Motion", + value=lambda value: value == "alert", + exists_fn=lambda device: device.device_type in [ATTR_DEVICE_MOTION_SENSOR], + ), + YoLinkBinarySensorEntityDescription( + name="Leak", + icon="mdi:water", + device_class=BinarySensorDeviceClass.MOISTURE, + value=lambda value: value == "alert", + exists_fn=lambda device: device.device_type in [ATTR_DEVICE_LEAK_SENSOR], + ), ) diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index 1804838e8b3..303fd684c02 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -14,3 +14,6 @@ ATTR_CLIENT = "client" ATTR_MQTT_CLIENT = "mqtt_client" ATTR_DEVICE_ID = "deviceId" ATTR_DEVICE_DOOR_SENSOR = "DoorSensor" +ATTR_DEVICE_TH_SENSOR = "THSensor" +ATTR_DEVICE_MOTION_SENSOR = "MotionSensor" +ATTR_DEVICE_LEAK_SENSOR = "LeakSensor" diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index d0072145c19..a89934154e9 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -5,6 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/yolink", "requirements": ["yolink-api==0.0.5"], "dependencies": ["auth", "application_credentials"], - "codeowners": ["@YoSmart-Inc"], + "codeowners": ["@matrixd2"], "iot_class": "cloud_push" } diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 075cfc24179..e33772c24be 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -13,12 +13,18 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import percentage -from .const import ATTR_COORDINATOR, ATTR_DEVICE_DOOR_SENSOR, DOMAIN +from .const import ( + ATTR_COORDINATOR, + ATTR_DEVICE_DOOR_SENSOR, + ATTR_DEVICE_MOTION_SENSOR, + ATTR_DEVICE_TH_SENSOR, + DOMAIN, +) from .coordinator import YoLinkCoordinator from .entity import YoLinkEntity @@ -49,11 +55,32 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( value=lambda value: percentage.ordered_list_item_to_percentage( [1, 2, 3, 4], value ), - exists_fn=lambda device: device.device_type in [ATTR_DEVICE_DOOR_SENSOR], + exists_fn=lambda device: device.device_type + in [ATTR_DEVICE_DOOR_SENSOR, ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_MOTION_SENSOR], + ), + YoLinkSensorEntityDescription( + key="humidity", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + name="Humidity", + state_class=SensorStateClass.MEASUREMENT, + exists_fn=lambda device: device.device_type in [ATTR_DEVICE_TH_SENSOR], + ), + YoLinkSensorEntityDescription( + key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + name="Temperature", + state_class=SensorStateClass.MEASUREMENT, + exists_fn=lambda device: device.device_type in [ATTR_DEVICE_TH_SENSOR], ), ) -SENSOR_DEVICE_TYPE = [ATTR_DEVICE_DOOR_SENSOR] +SENSOR_DEVICE_TYPE = [ + ATTR_DEVICE_DOOR_SENSOR, + ATTR_DEVICE_MOTION_SENSOR, + ATTR_DEVICE_TH_SENSOR, +] async def async_setup_entry( From ad5dbae42557809c6f46dcafdc3d5f561c25fbfc Mon Sep 17 00:00:00 2001 From: jrester <31157644+jrester@users.noreply.github.com> Date: Fri, 20 May 2022 21:53:43 +0200 Subject: [PATCH 0725/3516] Fix reauthentication for powerwall integration (#72174) --- .../components/powerwall/__init__.py | 30 ++------- homeassistant/components/powerwall/const.py | 1 - homeassistant/components/powerwall/models.py | 1 - tests/components/powerwall/test_init.py | 66 +++++++++++++++++++ 4 files changed, 71 insertions(+), 27 deletions(-) create mode 100644 tests/components/powerwall/test_init.py diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index af75ace8bd4..31e249ec806 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -29,7 +29,6 @@ from .const import ( POWERWALL_API_CHANGED, POWERWALL_COORDINATOR, POWERWALL_HTTP_SESSION, - POWERWALL_LOGIN_FAILED_COUNT, UPDATE_INTERVAL, ) from .models import PowerwallBaseInfo, PowerwallData, PowerwallRuntimeData @@ -40,8 +39,6 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) -MAX_LOGIN_FAILURES = 5 - API_CHANGED_ERROR_BODY = ( "It seems like your powerwall uses an unsupported version. " "Please update the software of your powerwall or if it is " @@ -68,29 +65,15 @@ class PowerwallDataManager: self.runtime_data = runtime_data self.power_wall = power_wall - @property - def login_failed_count(self) -> int: - """Return the current number of failed logins.""" - return self.runtime_data[POWERWALL_LOGIN_FAILED_COUNT] - @property def api_changed(self) -> int: """Return true if the api has changed out from under us.""" return self.runtime_data[POWERWALL_API_CHANGED] - def _increment_failed_logins(self) -> None: - self.runtime_data[POWERWALL_LOGIN_FAILED_COUNT] += 1 - - def _clear_failed_logins(self) -> None: - self.runtime_data[POWERWALL_LOGIN_FAILED_COUNT] = 0 - def _recreate_powerwall_login(self) -> None: """Recreate the login on auth failure.""" - http_session = self.runtime_data[POWERWALL_HTTP_SESSION] - http_session.close() - http_session = requests.Session() - self.runtime_data[POWERWALL_HTTP_SESSION] = http_session - self.power_wall = Powerwall(self.ip_address, http_session=http_session) + if self.power_wall.is_authenticated(): + self.power_wall.logout() self.power_wall.login(self.password or "") async def async_update_data(self) -> PowerwallData: @@ -121,17 +104,15 @@ class PowerwallDataManager: raise UpdateFailed("The powerwall api has changed") from err except AccessDeniedError as err: if attempt == 1: - self._increment_failed_logins() + # failed to authenticate => the credentials must be wrong raise ConfigEntryAuthFailed from err if self.password is None: raise ConfigEntryAuthFailed from err - raise UpdateFailed( - f"Login attempt {self.login_failed_count}/{MAX_LOGIN_FAILURES} failed, will retry: {err}" - ) from err + _LOGGER.debug("Access denied, trying to reauthenticate") + # there is still an attempt left to authenticate, so we continue in the loop except APIError as err: raise UpdateFailed(f"Updated failed due to {err}, will retry") from err else: - self._clear_failed_logins() return data raise RuntimeError("unreachable") @@ -174,7 +155,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api_changed=False, base_info=base_info, http_session=http_session, - login_failed_count=0, coordinator=None, ) diff --git a/homeassistant/components/powerwall/const.py b/homeassistant/components/powerwall/const.py index b1791c4300e..9df710e2df4 100644 --- a/homeassistant/components/powerwall/const.py +++ b/homeassistant/components/powerwall/const.py @@ -7,7 +7,6 @@ POWERWALL_BASE_INFO: Final = "base_info" POWERWALL_COORDINATOR: Final = "coordinator" POWERWALL_API_CHANGED: Final = "api_changed" POWERWALL_HTTP_SESSION: Final = "http_session" -POWERWALL_LOGIN_FAILED_COUNT: Final = "login_failed_count" UPDATE_INTERVAL = 30 diff --git a/homeassistant/components/powerwall/models.py b/homeassistant/components/powerwall/models.py index 6f8ccb98459..522dc2806bf 100644 --- a/homeassistant/components/powerwall/models.py +++ b/homeassistant/components/powerwall/models.py @@ -45,7 +45,6 @@ class PowerwallRuntimeData(TypedDict): """Run time data for the powerwall.""" coordinator: DataUpdateCoordinator | None - login_failed_count: int base_info: PowerwallBaseInfo api_changed: bool http_session: Session diff --git a/tests/components/powerwall/test_init.py b/tests/components/powerwall/test_init.py new file mode 100644 index 00000000000..d5fa92fc23f --- /dev/null +++ b/tests/components/powerwall/test_init.py @@ -0,0 +1,66 @@ +"""Tests for the PowerwallDataManager.""" + +import datetime +from unittest.mock import MagicMock, patch + +from tesla_powerwall import AccessDeniedError, LoginResponse + +from homeassistant.components.powerwall.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow + +from .mocks import _mock_powerwall_with_fixtures + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_update_data_reauthenticate_on_access_denied(hass: HomeAssistant): + """Test if _update_data of PowerwallDataManager reauthenticates on AccessDeniedError.""" + + mock_powerwall = await _mock_powerwall_with_fixtures(hass) + # login responses for the different tests: + # 1. login success on entry setup + # 2. login success after reauthentication + # 3. login failure after reauthentication + mock_powerwall.login = MagicMock(name="login", return_value=LoginResponse({})) + mock_powerwall.get_charge = MagicMock(name="get_charge", return_value=90.0) + mock_powerwall.is_authenticated = MagicMock( + name="is_authenticated", return_value=True + ) + mock_powerwall.logout = MagicMock(name="logout") + + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4", CONF_PASSWORD: "password"} + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + mock_powerwall.login.reset_mock(return_value=True) + mock_powerwall.get_charge.side_effect = [AccessDeniedError("test"), 90.0] + + async_fire_time_changed(hass, utcnow() + datetime.timedelta(minutes=1)) + await hass.async_block_till_done() + flows = hass.config_entries.flow.async_progress(DOMAIN) + assert len(flows) == 0 + + mock_powerwall.login.reset_mock() + mock_powerwall.login.side_effect = AccessDeniedError("test") + mock_powerwall.get_charge.side_effect = [AccessDeniedError("test"), 90.0] + + async_fire_time_changed(hass, utcnow() + datetime.timedelta(minutes=1)) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress(DOMAIN) + assert len(flows) == 1 + reauth_flow = flows[0] + assert reauth_flow["context"]["source"] == "reauth" From 267266c7c353d4005cc03da59f6d9cd3bad14f3e Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Fri, 20 May 2022 16:16:01 -0400 Subject: [PATCH 0726/3516] Adds UP Chime support for UniFi Protect (#71874) Co-authored-by: J. Nick Koston --- .../components/unifiprotect/button.py | 18 ++- .../components/unifiprotect/const.py | 1 + homeassistant/components/unifiprotect/data.py | 3 +- .../components/unifiprotect/entity.py | 6 +- .../components/unifiprotect/number.py | 15 ++ .../components/unifiprotect/sensor.py | 11 ++ .../components/unifiprotect/services.py | 147 ++++++++++++------ .../components/unifiprotect/services.yaml | 25 +++ tests/components/unifiprotect/conftest.py | 12 ++ .../unifiprotect/fixtures/sample_chime.json | 48 ++++++ tests/components/unifiprotect/test_button.py | 61 +++++--- tests/components/unifiprotect/test_init.py | 57 ++++++- .../components/unifiprotect/test_services.py | 75 ++++++++- 13 files changed, 398 insertions(+), 81 deletions(-) create mode 100644 tests/components/unifiprotect/fixtures/sample_chime.json diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index fe82c0afbe0..3728b6b4224 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -43,6 +43,22 @@ ALL_DEVICE_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( ), ) +CHIME_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( + ProtectButtonEntityDescription( + key="play", + name="Play Chime", + device_class=DEVICE_CLASS_CHIME_BUTTON, + icon="mdi:play", + ufp_press="play", + ), + ProtectButtonEntityDescription( + key="play_buzzer", + name="Play Buzzer", + icon="mdi:play", + ufp_press="play_buzzer", + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -53,7 +69,7 @@ async def async_setup_entry( data: ProtectData = hass.data[DOMAIN][entry.entry_id] entities: list[ProtectDeviceEntity] = async_all_device_entities( - data, ProtectButton, all_descs=ALL_DEVICE_BUTTONS + data, ProtectButton, all_descs=ALL_DEVICE_BUTTONS, chime_descs=CHIME_BUTTONS ) async_add_entities(entities) diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 0fe4ca98afa..3ba22e6b85b 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -38,6 +38,7 @@ DEVICES_THAT_ADOPT = { ModelType.VIEWPORT, ModelType.SENSOR, ModelType.DOORLOCK, + ModelType.CHIME, } DEVICES_WITH_ENTITIES = DEVICES_THAT_ADOPT | {ModelType.NVR} DEVICES_FOR_SUBSCRIBE = DEVICES_WITH_ENTITIES | {ModelType.EVENT} diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index e20f956acd2..371c1c7831b 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -12,10 +12,9 @@ from pyunifiprotect.data import ( Event, Liveview, ModelType, - ProtectAdoptableDeviceModel, - ProtectDeviceModel, WSSubscriptionMessage, ) +from pyunifiprotect.data.base import ProtectAdoptableDeviceModel, ProtectDeviceModel from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 45e52db5963..f8ceaeec9e6 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -8,6 +8,7 @@ from typing import Any from pyunifiprotect.data import ( NVR, Camera, + Chime, Doorlock, Event, Light, @@ -42,7 +43,7 @@ def _async_device_entities( entities: list[ProtectDeviceEntity] = [] for device in data.get_by_types({model_type}): - assert isinstance(device, (Camera, Light, Sensor, Viewer, Doorlock)) + assert isinstance(device, (Camera, Light, Sensor, Viewer, Doorlock, Chime)) for description in descs: if description.ufp_required_field: required_field = get_nested_attr(device, description.ufp_required_field) @@ -75,6 +76,7 @@ def async_all_device_entities( sense_descs: Sequence[ProtectRequiredKeysMixin] | None = None, viewer_descs: Sequence[ProtectRequiredKeysMixin] | None = None, lock_descs: Sequence[ProtectRequiredKeysMixin] | None = None, + chime_descs: Sequence[ProtectRequiredKeysMixin] | None = None, all_descs: Sequence[ProtectRequiredKeysMixin] | None = None, ) -> list[ProtectDeviceEntity]: """Generate a list of all the device entities.""" @@ -84,6 +86,7 @@ def async_all_device_entities( sense_descs = list(sense_descs or []) + all_descs viewer_descs = list(viewer_descs or []) + all_descs lock_descs = list(lock_descs or []) + all_descs + chime_descs = list(chime_descs or []) + all_descs return ( _async_device_entities(data, klass, ModelType.CAMERA, camera_descs) @@ -91,6 +94,7 @@ def async_all_device_entities( + _async_device_entities(data, klass, ModelType.SENSOR, sense_descs) + _async_device_entities(data, klass, ModelType.VIEWPORT, viewer_descs) + _async_device_entities(data, klass, ModelType.DOORLOCK, lock_descs) + + _async_device_entities(data, klass, ModelType.CHIME, chime_descs) ) diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index 58ae0502caa..4ebdd17f5c9 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -149,6 +149,20 @@ DOORLOCK_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ), ) +CHIME_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( + ProtectNumberEntityDescription( + key="volume", + name="Volume", + icon="mdi:speaker", + entity_category=EntityCategory.CONFIG, + ufp_min=0, + ufp_max=100, + ufp_step=1, + ufp_value="volume", + ufp_set_method="set_volume", + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -164,6 +178,7 @@ async def async_setup_entry( light_descs=LIGHT_NUMBERS, sense_descs=SENSE_NUMBERS, lock_descs=DOORLOCK_NUMBERS, + chime_descs=CHIME_NUMBERS, ) async_add_entities(entities) diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index c3b49fbdf9d..c30cc7fb80f 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -450,6 +450,16 @@ MOTION_TRIP_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ), ) +CHIME_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( + ProtectSensorEntityDescription( + key="last_ring", + name="Last Ring", + device_class=SensorDeviceClass.TIMESTAMP, + icon="mdi:bell", + ufp_value="last_ring", + ), +) + async def async_setup_entry( hass: HomeAssistant, @@ -466,6 +476,7 @@ async def async_setup_entry( sense_descs=SENSE_SENSORS, light_descs=LIGHT_SENSORS, lock_descs=DOORLOCK_SENSORS, + chime_descs=CHIME_SENSORS, ) entities += _async_motion_entities(data) entities += _async_nvr_entities(data) diff --git a/homeassistant/components/unifiprotect/services.py b/homeassistant/components/unifiprotect/services.py index a149f516d2f..f8aa446f857 100644 --- a/homeassistant/components/unifiprotect/services.py +++ b/homeassistant/components/unifiprotect/services.py @@ -10,25 +10,32 @@ from pyunifiprotect.api import ProtectApiClient from pyunifiprotect.exceptions import BadRequest import voluptuous as vol +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.const import ATTR_DEVICE_ID, Platform from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError -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.service import async_extract_referenced_entity_ids +from homeassistant.util.read_only_dict import ReadOnlyDict from .const import ATTR_MESSAGE, DOMAIN from .data import ProtectData -from .utils import _async_unifi_mac_from_hass SERVICE_ADD_DOORBELL_TEXT = "add_doorbell_text" SERVICE_REMOVE_DOORBELL_TEXT = "remove_doorbell_text" SERVICE_SET_DEFAULT_DOORBELL_TEXT = "set_default_doorbell_text" +SERVICE_SET_CHIME_PAIRED = "set_chime_paired_doorbells" ALL_GLOBAL_SERIVCES = [ SERVICE_ADD_DOORBELL_TEXT, SERVICE_REMOVE_DOORBELL_TEXT, SERVICE_SET_DEFAULT_DOORBELL_TEXT, + SERVICE_SET_CHIME_PAIRED, ] DOORBELL_TEXT_SCHEMA = vol.All( @@ -41,70 +48,68 @@ DOORBELL_TEXT_SCHEMA = vol.All( cv.has_at_least_one_key(ATTR_DEVICE_ID), ) +CHIME_PAIRED_SCHEMA = vol.All( + vol.Schema( + { + **cv.ENTITY_SERVICE_FIELDS, + "doorbells": cv.TARGET_SERVICE_FIELDS, + }, + ), + cv.has_at_least_one_key(ATTR_DEVICE_ID), +) -def _async_all_ufp_instances(hass: HomeAssistant) -> list[ProtectApiClient]: - """All active UFP instances.""" - return [ - data.api for data in hass.data[DOMAIN].values() if isinstance(data, ProtectData) - ] + +def _async_ufp_instance_for_config_entry_ids( + hass: HomeAssistant, config_entry_ids: set[str] +) -> ProtectApiClient | None: + """Find the UFP instance for the config entry ids.""" + domain_data = hass.data[DOMAIN] + for config_entry_id in config_entry_ids: + if config_entry_id in domain_data: + protect_data: ProtectData = domain_data[config_entry_id] + return protect_data.api + return None @callback -def _async_get_macs_for_device(device_entry: dr.DeviceEntry) -> list[str]: - return [ - _async_unifi_mac_from_hass(cval) - for ctype, cval in device_entry.connections - if ctype == dr.CONNECTION_NETWORK_MAC - ] - - -@callback -def _async_get_ufp_instances( - hass: HomeAssistant, device_id: str -) -> tuple[dr.DeviceEntry, ProtectApiClient]: +def _async_get_ufp_instance(hass: HomeAssistant, device_id: str) -> ProtectApiClient: device_registry = dr.async_get(hass) if not (device_entry := device_registry.async_get(device_id)): raise HomeAssistantError(f"No device found for device id: {device_id}") if device_entry.via_device_id is not None: - return _async_get_ufp_instances(hass, device_entry.via_device_id) + return _async_get_ufp_instance(hass, device_entry.via_device_id) - macs = _async_get_macs_for_device(device_entry) - ufp_instances = [ - i for i in _async_all_ufp_instances(hass) if i.bootstrap.nvr.mac in macs - ] + config_entry_ids = device_entry.config_entries + if ufp_instance := _async_ufp_instance_for_config_entry_ids(hass, config_entry_ids): + return ufp_instance - if not ufp_instances: - # should not be possible unless user manually enters a bad device ID - raise HomeAssistantError( # pragma: no cover - f"No UniFi Protect NVR found for device ID: {device_id}" - ) - - return device_entry, ufp_instances[0] + raise HomeAssistantError(f"No device found for device id: {device_id}") @callback def _async_get_protect_from_call( hass: HomeAssistant, call: ServiceCall -) -> list[tuple[dr.DeviceEntry, ProtectApiClient]]: - referenced = async_extract_referenced_entity_ids(hass, call) - - instances: list[tuple[dr.DeviceEntry, ProtectApiClient]] = [] - for device_id in referenced.referenced_devices: - instances.append(_async_get_ufp_instances(hass, device_id)) - - return instances +) -> set[ProtectApiClient]: + return { + _async_get_ufp_instance(hass, device_id) + for device_id in async_extract_referenced_entity_ids( + hass, call + ).referenced_devices + } -async def _async_call_nvr( - instances: list[tuple[dr.DeviceEntry, ProtectApiClient]], +async def _async_service_call_nvr( + hass: HomeAssistant, + call: ServiceCall, method: str, *args: Any, **kwargs: Any, ) -> None: + instances = _async_get_protect_from_call(hass, call) try: await asyncio.gather( - *(getattr(i.bootstrap.nvr, method)(*args, **kwargs) for _, i in instances) + *(getattr(i.bootstrap.nvr, method)(*args, **kwargs) for i in instances) ) except (BadRequest, ValidationError) as err: raise HomeAssistantError(str(err)) from err @@ -113,22 +118,61 @@ async def _async_call_nvr( async def add_doorbell_text(hass: HomeAssistant, call: ServiceCall) -> None: """Add a custom doorbell text message.""" message: str = call.data[ATTR_MESSAGE] - instances = _async_get_protect_from_call(hass, call) - await _async_call_nvr(instances, "add_custom_doorbell_message", message) + await _async_service_call_nvr(hass, call, "add_custom_doorbell_message", message) async def remove_doorbell_text(hass: HomeAssistant, call: ServiceCall) -> None: """Remove a custom doorbell text message.""" message: str = call.data[ATTR_MESSAGE] - instances = _async_get_protect_from_call(hass, call) - await _async_call_nvr(instances, "remove_custom_doorbell_message", message) + await _async_service_call_nvr(hass, call, "remove_custom_doorbell_message", message) async def set_default_doorbell_text(hass: HomeAssistant, call: ServiceCall) -> None: """Set the default doorbell text message.""" message: str = call.data[ATTR_MESSAGE] - instances = _async_get_protect_from_call(hass, call) - await _async_call_nvr(instances, "set_default_doorbell_message", message) + await _async_service_call_nvr(hass, call, "set_default_doorbell_message", message) + + +@callback +def _async_unique_id_to_ufp_device_id(unique_id: str) -> str: + """Extract the UFP device id from the registry entry unique id.""" + return unique_id.split("_")[0] + + +async def set_chime_paired_doorbells(hass: HomeAssistant, call: ServiceCall) -> None: + """Set paired doorbells on chime.""" + ref = async_extract_referenced_entity_ids(hass, call) + entity_registry = er.async_get(hass) + + entity_id = ref.indirectly_referenced.pop() + chime_button = entity_registry.async_get(entity_id) + assert chime_button is not None + assert chime_button.device_id is not None + chime_ufp_device_id = _async_unique_id_to_ufp_device_id(chime_button.unique_id) + + instance = _async_get_ufp_instance(hass, chime_button.device_id) + chime = instance.bootstrap.chimes[chime_ufp_device_id] + + call.data = ReadOnlyDict(call.data.get("doorbells") or {}) + doorbell_refs = async_extract_referenced_entity_ids(hass, call) + doorbell_ids: set[str] = set() + for camera_id in doorbell_refs.referenced | doorbell_refs.indirectly_referenced: + doorbell_sensor = entity_registry.async_get(camera_id) + assert doorbell_sensor is not None + if ( + doorbell_sensor.platform != DOMAIN + or doorbell_sensor.domain != Platform.BINARY_SENSOR + or doorbell_sensor.original_device_class + != BinarySensorDeviceClass.OCCUPANCY + ): + continue + doorbell_ufp_device_id = _async_unique_id_to_ufp_device_id( + doorbell_sensor.unique_id + ) + camera = instance.bootstrap.cameras[doorbell_ufp_device_id] + doorbell_ids.add(camera.id) + chime.camera_ids = sorted(doorbell_ids) + await chime.save_device() def async_setup_services(hass: HomeAssistant) -> None: @@ -149,6 +193,11 @@ def async_setup_services(hass: HomeAssistant) -> None: functools.partial(set_default_doorbell_text, hass), DOORBELL_TEXT_SCHEMA, ), + ( + SERVICE_SET_CHIME_PAIRED, + functools.partial(set_chime_paired_doorbells, hass), + CHIME_PAIRED_SCHEMA, + ), ] for name, method, schema in services: if hass.services.has_service(DOMAIN, name): diff --git a/homeassistant/components/unifiprotect/services.yaml b/homeassistant/components/unifiprotect/services.yaml index 410dcae4699..037c10627ad 100644 --- a/homeassistant/components/unifiprotect/services.yaml +++ b/homeassistant/components/unifiprotect/services.yaml @@ -84,3 +84,28 @@ set_doorbell_message: step: 1 mode: slider unit_of_measurement: minutes +set_chime_paired_doorbells: + name: Set Chime Paired Doorbells + description: > + Use to set the paired doorbell(s) with a smart chime. + fields: + device_id: + name: Chime + description: The Chimes to link to the doorbells to + required: true + selector: + device: + integration: unifiprotect + entity: + device_class: unifiprotect__chime_button + doorbells: + name: Doorbells + description: The Doorbells to link to the chime + example: "binary_sensor.front_doorbell_doorbell" + required: false + selector: + target: + entity: + integration: unifiprotect + domain: binary_sensor + device_class: occupancy diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index c83c6eb72cc..6eeef02e817 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -14,6 +14,7 @@ import pytest from pyunifiprotect.data import ( NVR, Camera, + Chime, Doorlock, Light, Liveview, @@ -49,6 +50,7 @@ class MockBootstrap: liveviews: dict[str, Any] events: dict[str, Any] doorlocks: dict[str, Any] + chimes: dict[str, Any] def reset_objects(self) -> None: """Reset all devices on bootstrap for tests.""" @@ -59,6 +61,7 @@ class MockBootstrap: self.liveviews = {} self.events = {} self.doorlocks = {} + self.chimes = {} def process_ws_packet(self, msg: WSSubscriptionMessage) -> None: """Fake process method for tests.""" @@ -127,6 +130,7 @@ def mock_bootstrap_fixture(mock_nvr: NVR): liveviews={}, events={}, doorlocks={}, + chimes={}, ) @@ -220,6 +224,14 @@ def mock_doorlock(): return Doorlock.from_unifi_dict(**data) +@pytest.fixture +def mock_chime(): + """Mock UniFi Protect Chime device.""" + + data = json.loads(load_fixture("sample_chime.json", integration=DOMAIN)) + return Chime.from_unifi_dict(**data) + + @pytest.fixture def now(): """Return datetime object that will be consistent throughout test.""" diff --git a/tests/components/unifiprotect/fixtures/sample_chime.json b/tests/components/unifiprotect/fixtures/sample_chime.json new file mode 100644 index 00000000000..975cfcebaea --- /dev/null +++ b/tests/components/unifiprotect/fixtures/sample_chime.json @@ -0,0 +1,48 @@ +{ + "mac": "BEEEE2FBE413", + "host": "192.168.144.146", + "connectionHost": "192.168.234.27", + "type": "UP Chime", + "name": "Xaorvu Tvsv", + "upSince": 1651882870009, + "uptime": 567870, + "lastSeen": 1652450740009, + "connectedSince": 1652448904587, + "state": "CONNECTED", + "hardwareRevision": null, + "firmwareVersion": "1.3.4", + "latestFirmwareVersion": "1.3.4", + "firmwareBuild": "58bd350.220401.1859", + "isUpdating": false, + "isAdopting": false, + "isAdopted": true, + "isAdoptedByOther": false, + "isProvisioned": false, + "isRebooting": false, + "isSshEnabled": true, + "canAdopt": false, + "isAttemptingToConnect": false, + "volume": 100, + "isProbingForWifi": false, + "apMac": null, + "apRssi": null, + "elementInfo": null, + "lastRing": 1652116059940, + "isWirelessUplinkEnabled": true, + "wiredConnectionState": { + "phyRate": null + }, + "wifiConnectionState": { + "channel": null, + "frequency": null, + "phyRate": null, + "signalQuality": 100, + "signalStrength": -44, + "ssid": null + }, + "cameraIds": [], + "id": "cf1a330397c08f919d02bd7c", + "isConnected": true, + "marketName": "UP Chime", + "modelKey": "chime" +} diff --git a/tests/components/unifiprotect/test_button.py b/tests/components/unifiprotect/test_button.py index 64677dd1d77..f3b76cb7abb 100644 --- a/tests/components/unifiprotect/test_button.py +++ b/tests/components/unifiprotect/test_button.py @@ -5,7 +5,7 @@ from __future__ import annotations from unittest.mock import AsyncMock import pytest -from pyunifiprotect.data import Camera +from pyunifiprotect.data.devices import Chime from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, Platform @@ -15,42 +15,39 @@ from homeassistant.helpers import entity_registry as er from .conftest import MockEntityFixture, assert_entity_counts, enable_entity -@pytest.fixture(name="camera") -async def camera_fixture( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera +@pytest.fixture(name="chime") +async def chime_fixture( + hass: HomeAssistant, mock_entry: MockEntityFixture, mock_chime: Chime ): """Fixture for a single camera for testing the button platform.""" - camera_obj = mock_camera.copy(deep=True) - camera_obj._api = mock_entry.api - camera_obj.channels[0]._api = mock_entry.api - camera_obj.channels[1]._api = mock_entry.api - camera_obj.channels[2]._api = mock_entry.api - camera_obj.name = "Test Camera" + chime_obj = mock_chime.copy(deep=True) + chime_obj._api = mock_entry.api + chime_obj.name = "Test Chime" - mock_entry.api.bootstrap.cameras = { - camera_obj.id: camera_obj, + mock_entry.api.bootstrap.chimes = { + chime_obj.id: chime_obj, } await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - assert_entity_counts(hass, Platform.BUTTON, 1, 0) + assert_entity_counts(hass, Platform.BUTTON, 3, 2) - return (camera_obj, "button.test_camera_reboot_device") + return chime_obj -async def test_button( +async def test_reboot_button( hass: HomeAssistant, mock_entry: MockEntityFixture, - camera: tuple[Camera, str], + chime: Chime, ): """Test button entity.""" mock_entry.api.reboot_device = AsyncMock() - unique_id = f"{camera[0].id}_reboot" - entity_id = camera[1] + unique_id = f"{chime.id}_reboot" + entity_id = "button.test_chime_reboot_device" entity_registry = er.async_get(hass) entity = entity_registry.async_get(entity_id) @@ -67,3 +64,31 @@ async def test_button( "button", "press", {ATTR_ENTITY_ID: entity_id}, blocking=True ) mock_entry.api.reboot_device.assert_called_once() + + +async def test_chime_button( + hass: HomeAssistant, + mock_entry: MockEntityFixture, + chime: Chime, +): + """Test button entity.""" + + mock_entry.api.play_speaker = AsyncMock() + + unique_id = f"{chime.id}_play" + entity_id = "button.test_chime_play_chime" + + entity_registry = er.async_get(hass) + entity = entity_registry.async_get(entity_id) + assert entity + assert not entity.disabled + assert entity.unique_id == unique_id + + state = hass.states.get(entity_id) + assert state + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + + await hass.services.async_call( + "button", "press", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + mock_entry.api.play_speaker.assert_called_once() diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 53588984e25..95c2ee0b511 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -202,11 +202,11 @@ async def test_migrate_reboot_button( registry = er.async_get(hass) registry.async_get_or_create( - Platform.BUTTON, Platform.BUTTON, light1.id, config_entry=mock_entry.entry + Platform.BUTTON, DOMAIN, light1.id, config_entry=mock_entry.entry ) registry.async_get_or_create( Platform.BUTTON, - Platform.BUTTON, + DOMAIN, f"{light2.id}_reboot", config_entry=mock_entry.entry, ) @@ -218,24 +218,67 @@ async def test_migrate_reboot_button( assert mock_entry.api.update.called assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac + buttons = [] + for entity in er.async_entries_for_config_entry( + registry, mock_entry.entry.entry_id + ): + if entity.domain == Platform.BUTTON.value: + buttons.append(entity) + print(entity.entity_id) + assert len(buttons) == 2 + + assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device") is None assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device_2") is None - light = registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device") + light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_lightid1") assert light is not None assert light.unique_id == f"{light1.id}_reboot" + assert registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device") is None assert registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device_2") is None - light = registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device") + light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_lightid2_reboot") assert light is not None assert light.unique_id == f"{light2.id}_reboot" + +async def test_migrate_reboot_button_no_device( + hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light +): + """Test migrating unique ID of reboot button if UniFi Protect device ID changed.""" + + light1 = mock_light.copy() + light1._api = mock_entry.api + light1.name = "Test Light 1" + light1.id = "lightid1" + + mock_entry.api.bootstrap.lights = { + light1.id: light1, + } + mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) + + registry = er.async_get(hass) + registry.async_get_or_create( + Platform.BUTTON, DOMAIN, "lightid2", config_entry=mock_entry.entry + ) + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + + assert mock_entry.entry.state == ConfigEntryState.LOADED + assert mock_entry.api.update.called + assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac + buttons = [] for entity in er.async_entries_for_config_entry( registry, mock_entry.entry.entry_id ): - if entity.platform == Platform.BUTTON.value: + if entity.domain == Platform.BUTTON.value: buttons.append(entity) assert len(buttons) == 2 + light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_lightid2") + assert light is not None + assert light.unique_id == "lightid2" + async def test_migrate_reboot_button_fail( hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light @@ -255,14 +298,14 @@ async def test_migrate_reboot_button_fail( registry = er.async_get(hass) registry.async_get_or_create( Platform.BUTTON, - Platform.BUTTON, + DOMAIN, light1.id, config_entry=mock_entry.entry, suggested_object_id=light1.name, ) registry.async_get_or_create( Platform.BUTTON, - Platform.BUTTON, + DOMAIN, f"{light1.id}_reboot", config_entry=mock_entry.entry, suggested_object_id=light1.name, diff --git a/tests/components/unifiprotect/test_services.py b/tests/components/unifiprotect/test_services.py index 0230bc3d36c..22f7fdd1a6c 100644 --- a/tests/components/unifiprotect/test_services.py +++ b/tests/components/unifiprotect/test_services.py @@ -5,19 +5,21 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock import pytest -from pyunifiprotect.data import Light +from pyunifiprotect.data import Camera, Light, ModelType +from pyunifiprotect.data.devices import Chime from pyunifiprotect.exceptions import BadRequest from homeassistant.components.unifiprotect.const import ATTR_MESSAGE, DOMAIN from homeassistant.components.unifiprotect.services import ( SERVICE_ADD_DOORBELL_TEXT, SERVICE_REMOVE_DOORBELL_TEXT, + SERVICE_SET_CHIME_PAIRED, SERVICE_SET_DEFAULT_DOORBELL_TEXT, ) -from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from .conftest import MockEntityFixture @@ -143,3 +145,70 @@ async def test_set_default_doorbell_text( blocking=True, ) nvr.set_default_doorbell_message.assert_called_once_with("Test Message") + + +async def test_set_chime_paired_doorbells( + hass: HomeAssistant, + mock_entry: MockEntityFixture, + mock_chime: Chime, + mock_camera: Camera, +): + """Test set_chime_paired_doorbells.""" + + mock_entry.api.update_device = AsyncMock() + + mock_chime._api = mock_entry.api + mock_chime.name = "Test Chime" + mock_chime._initial_data = mock_chime.dict() + mock_entry.api.bootstrap.chimes = { + mock_chime.id: mock_chime, + } + + camera1 = mock_camera.copy() + camera1.id = "cameraid1" + camera1.name = "Test Camera 1" + camera1._api = mock_entry.api + camera1.channels[0]._api = mock_entry.api + camera1.channels[1]._api = mock_entry.api + camera1.channels[2]._api = mock_entry.api + camera1.feature_flags.has_chime = True + + camera2 = mock_camera.copy() + camera2.id = "cameraid2" + camera2.name = "Test Camera 2" + camera2._api = mock_entry.api + camera2.channels[0]._api = mock_entry.api + camera2.channels[1]._api = mock_entry.api + camera2.channels[2]._api = mock_entry.api + camera2.feature_flags.has_chime = True + + mock_entry.api.bootstrap.cameras = { + camera1.id: camera1, + camera2.id: camera2, + } + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + + registry = er.async_get(hass) + chime_entry = registry.async_get("button.test_chime_play_chime") + camera_entry = registry.async_get("binary_sensor.test_camera_2_doorbell") + assert chime_entry is not None + assert camera_entry is not None + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_CHIME_PAIRED, + { + ATTR_DEVICE_ID: chime_entry.device_id, + "doorbells": { + ATTR_ENTITY_ID: ["binary_sensor.test_camera_1_doorbell"], + ATTR_DEVICE_ID: [camera_entry.device_id], + }, + }, + blocking=True, + ) + + mock_entry.api.update_device.assert_called_once_with( + ModelType.CHIME, mock_chime.id, {"cameraIds": [camera1.id, camera2.id]} + ) From ceb8bb47455764aeccaa0a7a6e0fc2c296d48ea6 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 21 May 2022 00:22:49 +0000 Subject: [PATCH 0727/3516] [ci skip] Translation update --- .../components/abode/translations/nl.json | 6 +-- .../accuweather/translations/nl.json | 4 +- .../components/adguard/translations/nl.json | 4 +- .../components/aemet/translations/ko.json | 9 +++++ .../components/aemet/translations/nl.json | 2 +- .../components/agent_dvr/translations/nl.json | 2 +- .../components/airly/translations/nl.json | 2 +- .../components/airvisual/translations/nl.json | 12 +++--- .../components/airzone/translations/nl.json | 2 +- .../aladdin_connect/translations/nl.json | 4 +- .../alarm_control_panel/translations/nl.json | 8 ++-- .../components/almond/translations/nl.json | 4 +- .../components/ambee/translations/ko.json | 18 +++++++++ .../components/ambee/translations/nl.json | 2 +- .../ambiclimate/translations/nl.json | 4 +- .../components/apple_tv/translations/ko.json | 1 + .../components/apple_tv/translations/nl.json | 6 +-- .../components/arcam_fmj/translations/nl.json | 2 +- .../components/asuswrt/translations/nl.json | 2 +- .../components/atag/translations/nl.json | 4 +- .../components/august/translations/nl.json | 2 +- .../aussie_broadband/translations/nl.json | 4 +- .../components/awair/translations/nl.json | 4 +- .../components/axis/translations/nl.json | 2 +- .../azure_devops/translations/nl.json | 2 +- .../azure_event_hub/translations/nl.json | 2 +- .../components/baf/translations/pl.json | 7 +++- .../components/blebox/translations/nl.json | 2 +- .../components/bond/translations/nl.json | 2 +- .../components/bosch_shc/translations/ko.json | 37 +++++++++++++++++++ .../components/bosch_shc/translations/nl.json | 4 +- .../components/braviatv/translations/nl.json | 2 +- .../components/broadlink/translations/nl.json | 6 +-- .../components/brother/translations/nl.json | 2 +- .../components/brunt/translations/nl.json | 4 +- .../buienradar/translations/ko.json | 29 +++++++++++++++ .../buienradar/translations/nl.json | 4 +- .../components/camera/translations/nl.json | 2 +- .../components/canary/translations/nl.json | 4 +- .../components/cast/translations/ko.json | 5 +++ .../components/cast/translations/nl.json | 4 +- .../climacell/translations/sensor.ko.json | 7 ++++ .../climacell/translations/sensor.nl.json | 2 +- .../components/climate/translations/nl.json | 4 +- .../cloudflare/translations/nl.json | 4 +- .../configurator/translations/nl.json | 2 +- .../components/cover/translations/nl.json | 2 +- .../components/cpuspeed/translations/nl.json | 4 +- .../components/daikin/translations/nl.json | 2 +- .../components/deconz/translations/nl.json | 2 +- .../demo/translations/select.nl.json | 2 +- .../components/denonavr/translations/ko.json | 1 + .../components/denonavr/translations/nl.json | 2 +- .../devolo_home_control/translations/nl.json | 2 +- .../devolo_home_network/translations/ko.json | 3 ++ .../devolo_home_network/translations/nl.json | 2 +- .../dialogflow/translations/nl.json | 2 +- .../components/discord/translations/nl.json | 2 +- .../components/dlna_dmr/translations/ko.json | 3 ++ .../components/dlna_dmr/translations/nl.json | 2 +- .../components/dlna_dms/translations/ko.json | 3 ++ .../components/dlna_dms/translations/nl.json | 4 +- .../components/dunehd/translations/nl.json | 4 +- .../components/ecobee/translations/nl.json | 2 +- .../components/efergy/translations/nl.json | 2 +- .../components/elkm1/translations/ko.json | 8 ++++ .../components/elkm1/translations/nl.json | 2 +- .../components/elmax/translations/ko.json | 11 ++++++ .../components/enocean/translations/nl.json | 2 +- .../enphase_envoy/translations/nl.json | 2 +- .../components/esphome/translations/nl.json | 4 +- .../components/ezviz/translations/ko.json | 13 +++++-- .../components/fibaro/translations/nl.json | 2 +- .../fireservicerota/translations/nl.json | 4 +- .../fjaraskupan/translations/nl.json | 2 +- .../flick_electric/translations/nl.json | 2 +- .../components/flo/translations/nl.json | 2 +- .../components/flume/translations/ko.json | 10 ++++- .../components/flume/translations/nl.json | 2 +- .../flunearyou/translations/nl.json | 2 +- .../components/flux_led/translations/nl.json | 2 +- .../components/fritz/translations/ko.json | 24 +++++++++++- .../components/fritz/translations/nl.json | 6 +-- .../components/fritzbox/translations/nl.json | 4 +- .../garages_amsterdam/translations/ko.json | 8 ++++ .../components/generic/translations/nl.json | 4 +- .../geocaching/translations/nl.json | 16 ++++---- .../components/geofency/translations/nl.json | 2 +- .../components/gios/translations/nl.json | 2 +- .../components/glances/translations/nl.json | 2 +- .../components/goalzero/translations/nl.json | 2 +- .../components/gogogate2/translations/ko.json | 1 + .../components/gogogate2/translations/nl.json | 4 +- .../components/goodwe/translations/nl.json | 2 +- .../components/google/translations/ko.json | 5 +++ .../components/google/translations/nl.json | 14 +++---- .../google_travel_time/translations/nl.json | 2 +- .../components/gpslogger/translations/nl.json | 2 +- .../components/gree/translations/nl.json | 4 +- .../growatt_server/translations/ko.json | 3 ++ .../components/guardian/translations/ko.json | 3 ++ .../components/guardian/translations/nl.json | 2 +- .../hisense_aehw4a1/translations/nl.json | 2 +- .../components/hive/translations/nl.json | 2 +- .../home_connect/translations/nl.json | 8 ++-- .../home_plus_control/translations/nl.json | 12 +++--- .../homekit_controller/translations/ko.json | 2 + .../homekit_controller/translations/nl.json | 2 +- .../homematicip_cloud/translations/nl.json | 4 +- .../huawei_lte/translations/ko.json | 3 +- .../components/hue/translations/nl.json | 4 +- .../translations/nl.json | 2 +- .../hvv_departures/translations/nl.json | 2 +- .../components/hyperion/translations/nl.json | 4 +- .../components/icloud/translations/nl.json | 4 +- .../components/ifttt/translations/nl.json | 2 +- .../components/insteon/translations/ko.json | 1 + .../components/insteon/translations/nl.json | 6 +-- .../intellifire/translations/nl.json | 2 +- .../components/ios/translations/nl.json | 4 +- .../components/iotawatt/translations/nl.json | 2 +- .../components/ipma/translations/nl.json | 2 +- .../components/ipp/translations/nl.json | 2 +- .../components/iss/translations/nl.json | 2 +- .../components/isy994/translations/nl.json | 4 +- .../components/izone/translations/nl.json | 2 +- .../components/jellyfin/translations/nl.json | 2 +- .../components/juicenet/translations/nl.json | 2 +- .../kaleidescape/translations/nl.json | 4 +- .../keenetic_ndms2/translations/ko.json | 3 +- .../components/knx/translations/ko.json | 5 +++ .../components/knx/translations/nl.json | 2 +- .../components/knx/translations/pl.json | 2 +- .../components/kodi/translations/nl.json | 6 +-- .../components/konnected/translations/nl.json | 2 +- .../components/kraken/translations/nl.json | 4 +- .../components/kulersky/translations/nl.json | 4 +- .../launch_library/translations/nl.json | 2 +- .../components/lifx/translations/nl.json | 2 +- .../components/litejet/translations/nl.json | 2 +- .../litterrobot/translations/sensor.pl.json | 2 +- .../components/local_ip/translations/nl.json | 4 +- .../components/locative/translations/nl.json | 4 +- .../logi_circle/translations/nl.json | 4 +- .../components/lookin/translations/nl.json | 2 +- .../lutron_caseta/translations/nl.json | 4 +- .../components/lyric/translations/ko.json | 1 + .../components/lyric/translations/nl.json | 10 ++--- .../components/mailgun/translations/nl.json | 2 +- .../components/mazda/translations/nl.json | 2 +- .../media_player/translations/nl.json | 2 +- .../components/met/translations/ko.json | 3 ++ .../meteo_france/translations/nl.json | 2 +- .../components/metoffice/translations/nl.json | 2 +- .../components/min_max/translations/nl.json | 4 +- .../modem_callerid/translations/nl.json | 2 +- .../modern_forms/translations/ko.json | 20 ++++++++++ .../components/moon/translations/nl.json | 4 +- .../motion_blinds/translations/nl.json | 2 +- .../components/motioneye/translations/nl.json | 2 +- .../components/mqtt/translations/ko.json | 6 ++- .../components/mqtt/translations/nl.json | 6 +-- .../components/myq/translations/ko.json | 4 ++ .../components/myq/translations/nl.json | 2 +- .../components/mysensors/translations/ko.json | 1 + .../components/nam/translations/ko.json | 1 + .../components/nam/translations/nl.json | 2 +- .../components/nanoleaf/translations/nl.json | 2 +- .../components/neato/translations/nl.json | 12 +++--- .../components/nest/translations/nl.json | 18 ++++----- .../components/netatmo/translations/nl.json | 16 ++++---- .../components/netgear/translations/nl.json | 2 +- .../components/nina/translations/nl.json | 2 +- .../nmap_tracker/translations/nl.json | 2 +- .../components/notion/translations/nl.json | 4 +- .../components/nuki/translations/ko.json | 1 + .../components/nuki/translations/nl.json | 4 +- .../components/nzbget/translations/nl.json | 2 +- .../components/omnilogic/translations/nl.json | 4 +- .../ondilo_ico/translations/nl.json | 6 +-- .../components/onvif/translations/nl.json | 2 +- .../opentherm_gw/translations/nl.json | 2 +- .../components/openuv/translations/nl.json | 2 +- .../openweathermap/translations/nl.json | 8 ++-- .../components/overkiz/translations/nl.json | 2 +- .../overkiz/translations/select.ko.json | 7 ++++ .../panasonic_viera/translations/nl.json | 6 +-- .../philips_js/translations/nl.json | 2 +- .../components/pi_hole/translations/nl.json | 4 +- .../components/picnic/translations/nl.json | 2 +- .../components/plaato/translations/nl.json | 4 +- .../components/plex/translations/nl.json | 6 +-- .../components/plugwise/translations/nl.json | 4 +- .../plum_lightpad/translations/nl.json | 2 +- .../components/point/translations/nl.json | 10 ++--- .../components/powerwall/translations/nl.json | 2 +- .../components/profiler/translations/nl.json | 2 +- .../components/prosegur/translations/nl.json | 2 +- .../components/ps4/translations/nl.json | 4 +- .../pure_energie/translations/ko.json | 9 +++++ .../components/pvoutput/translations/nl.json | 4 +- .../radio_browser/translations/nl.json | 2 +- .../rainmachine/translations/ko.json | 1 + .../components/renault/translations/nl.json | 4 +- .../components/rfxtrx/translations/nl.json | 2 +- .../components/ridwell/translations/nl.json | 4 +- .../components/risco/translations/nl.json | 8 ++-- .../components/roku/translations/nl.json | 2 +- .../components/roon/translations/nl.json | 4 +- .../components/rpi_power/translations/nl.json | 2 +- .../rtsp_to_webrtc/translations/nl.json | 2 +- .../components/samsungtv/translations/ko.json | 11 +++++- .../components/samsungtv/translations/nl.json | 4 +- .../components/season/translations/nl.json | 2 +- .../components/sense/translations/ko.json | 3 ++ .../components/sense/translations/nl.json | 4 +- .../components/sensibo/translations/ko.json | 7 ++++ .../components/sensibo/translations/nl.json | 2 +- .../components/sensor/translations/ko.json | 1 + .../components/senz/translations/nl.json | 12 +++--- .../components/sharkiq/translations/nl.json | 4 +- .../components/shelly/translations/ja.json | 1 + .../components/shelly/translations/no.json | 1 + .../components/shelly/translations/pl.json | 1 + .../components/sia/translations/ko.json | 12 ++++++ .../components/sia/translations/nl.json | 2 +- .../simplisafe/translations/nl.json | 4 +- .../components/siren/translations/pl.json | 3 ++ .../components/slack/translations/pl.json | 8 +++- .../components/sleepiq/translations/nl.json | 4 +- .../components/slimproto/translations/nl.json | 2 +- .../components/sma/translations/ko.json | 6 ++- .../components/sma/translations/nl.json | 4 +- .../components/smappee/translations/nl.json | 6 +-- .../smart_meter_texas/translations/nl.json | 4 +- .../components/smarttub/translations/ko.json | 1 + .../components/smarttub/translations/nl.json | 4 +- .../components/sms/translations/nl.json | 4 +- .../components/soma/translations/nl.json | 6 +-- .../components/somfy/translations/nl.json | 8 ++-- .../components/sonarr/translations/nl.json | 6 +-- .../components/songpal/translations/nl.json | 2 +- .../components/sonos/translations/ko.json | 1 + .../components/sonos/translations/nl.json | 2 +- .../speedtestdotnet/translations/nl.json | 2 +- .../components/spider/translations/nl.json | 2 +- .../components/spotify/translations/nl.json | 4 +- .../squeezebox/translations/nl.json | 2 +- .../srp_energy/translations/nl.json | 2 +- .../steam_online/translations/nl.json | 4 +- .../components/steamist/translations/nl.json | 2 +- .../components/sun/translations/nl.json | 4 +- .../switcher_kis/translations/nl.json | 4 +- .../synology_dsm/translations/nl.json | 12 +++--- .../system_bridge/translations/ko.json | 31 ++++++++++++++++ .../system_bridge/translations/nl.json | 2 +- .../components/tailscale/translations/nl.json | 2 +- .../tankerkoenig/translations/nl.json | 2 +- .../components/tasmota/translations/nl.json | 2 +- .../components/tautulli/translations/nl.json | 4 +- .../tellduslive/translations/nl.json | 2 +- .../components/tile/translations/nl.json | 4 +- .../components/tolo/translations/nl.json | 2 +- .../tomorrowio/translations/sensor.ko.json | 7 ++++ .../components/toon/translations/nl.json | 6 +-- .../totalconnect/translations/nl.json | 4 +- .../components/tplink/translations/nl.json | 2 +- .../components/traccar/translations/nl.json | 2 +- .../components/tractive/translations/nl.json | 2 +- .../components/tradfri/translations/nl.json | 2 +- .../trafikverket_ferry/translations/nl.json | 2 +- .../trafikverket_train/translations/ko.json | 11 ++++++ .../trafikverket_train/translations/nl.json | 2 +- .../tuya/translations/select.nl.json | 8 ++-- .../components/twilio/translations/nl.json | 4 +- .../ukraine_alarm/translations/nl.json | 6 +-- .../components/unifi/translations/nl.json | 2 +- .../unifiprotect/translations/ko.json | 10 ++++- .../components/upb/translations/nl.json | 2 +- .../components/uptime/translations/nl.json | 4 +- .../uptimerobot/translations/nl.json | 4 +- .../components/vallox/translations/nl.json | 2 +- .../components/venstar/translations/nl.json | 4 +- .../components/verisure/translations/nl.json | 2 +- .../components/vesync/translations/nl.json | 2 +- .../components/vicare/translations/nl.json | 2 +- .../components/vizio/translations/nl.json | 6 +-- .../vlc_telnet/translations/nl.json | 6 +-- .../components/vulcan/translations/ko.json | 3 ++ .../components/wallbox/translations/nl.json | 2 +- .../water_heater/translations/nl.json | 4 +- .../components/watttime/translations/nl.json | 4 +- .../waze_travel_time/translations/ko.json | 20 +++++++++- .../waze_travel_time/translations/nl.json | 2 +- .../components/weather/translations/nl.json | 2 +- .../components/webostv/translations/nl.json | 2 +- .../components/wemo/translations/nl.json | 2 +- .../components/withings/translations/nl.json | 8 ++-- .../components/wiz/translations/ko.json | 9 +++++ .../components/xbox/translations/nl.json | 8 ++-- .../xiaomi_aqara/translations/nl.json | 2 +- .../xiaomi_miio/translations/nl.json | 6 +-- .../yale_smart_alarm/translations/nl.json | 2 +- .../yamaha_musiccast/translations/nl.json | 2 +- .../components/yeelight/translations/ko.json | 4 ++ .../components/yeelight/translations/nl.json | 2 +- .../components/yolink/translations/nl.json | 16 ++++---- .../components/yolink/translations/pl.json | 25 +++++++++++++ .../components/zerproc/translations/nl.json | 4 +- .../components/zha/translations/nl.json | 2 +- .../zodiac/translations/sensor.nl.json | 2 +- .../zoneminder/translations/nl.json | 6 +-- .../components/zwave_js/translations/ko.json | 7 +++- .../components/zwave_js/translations/nl.json | 2 +- 314 files changed, 899 insertions(+), 473 deletions(-) create mode 100644 homeassistant/components/ambee/translations/ko.json create mode 100644 homeassistant/components/bosch_shc/translations/ko.json create mode 100644 homeassistant/components/buienradar/translations/ko.json create mode 100644 homeassistant/components/climacell/translations/sensor.ko.json create mode 100644 homeassistant/components/elmax/translations/ko.json create mode 100644 homeassistant/components/garages_amsterdam/translations/ko.json create mode 100644 homeassistant/components/growatt_server/translations/ko.json create mode 100644 homeassistant/components/modern_forms/translations/ko.json create mode 100644 homeassistant/components/overkiz/translations/select.ko.json create mode 100644 homeassistant/components/pure_energie/translations/ko.json create mode 100644 homeassistant/components/sensibo/translations/ko.json create mode 100644 homeassistant/components/sia/translations/ko.json create mode 100644 homeassistant/components/siren/translations/pl.json create mode 100644 homeassistant/components/system_bridge/translations/ko.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.ko.json create mode 100644 homeassistant/components/trafikverket_train/translations/ko.json create mode 100644 homeassistant/components/wiz/translations/ko.json create mode 100644 homeassistant/components/yolink/translations/pl.json diff --git a/homeassistant/components/abode/translations/nl.json b/homeassistant/components/abode/translations/nl.json index 7b6a8b5aace..0b6b67c363c 100644 --- a/homeassistant/components/abode/translations/nl.json +++ b/homeassistant/components/abode/translations/nl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "reauth_successful": "Herauthenticatie was succesvol", - "single_instance_allowed": "Slechts een enkele configuratie van Abode is toegestaan." + "reauth_successful": "Herauthenticatie geslaagd", + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -26,7 +26,7 @@ "user": { "data": { "password": "Wachtwoord", - "username": "E-mailadres" + "username": "E-mail" }, "title": "Vul uw Abode-inloggegevens in" } diff --git a/homeassistant/components/accuweather/translations/nl.json b/homeassistant/components/accuweather/translations/nl.json index e83b3d8355a..df5c71dcf45 100644 --- a/homeassistant/components/accuweather/translations/nl.json +++ b/homeassistant/components/accuweather/translations/nl.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "create_entry": { "default": "Sommige sensoren zijn standaard niet ingeschakeld. U kunt ze inschakelen in het entiteitenregister na de integratieconfiguratie.\nWeersvoorspelling is niet standaard ingeschakeld. U kunt deze inschakelen in de integratieopties." }, "error": { "cannot_connect": "Kan geen verbinding maken", - "invalid_api_key": "API-sleutel", + "invalid_api_key": "Ongeldige API-sleutel", "requests_exceeded": "Het toegestane aantal verzoeken aan de Accuweather API is overschreden. U moet wachten of de API-sleutel wijzigen." }, "step": { diff --git a/homeassistant/components/adguard/translations/nl.json b/homeassistant/components/adguard/translations/nl.json index 9f991cbd407..38a64997e73 100644 --- a/homeassistant/components/adguard/translations/nl.json +++ b/homeassistant/components/adguard/translations/nl.json @@ -17,9 +17,9 @@ "host": "Host", "password": "Wachtwoord", "port": "Poort", - "ssl": "AdGuard Home maakt gebruik van een SSL certificaat", + "ssl": "Maakt gebruik van een SSL-certificaat", "username": "Gebruikersnaam", - "verify_ssl": "AdGuard Home maakt gebruik van een goed certificaat" + "verify_ssl": "SSL-certificaat verifi\u00ebren" }, "description": "Stel uw AdGuard Home-instantie in om toezicht en controle mogelijk te maken." } diff --git a/homeassistant/components/aemet/translations/ko.json b/homeassistant/components/aemet/translations/ko.json index 23ca054f5be..06d713755b6 100644 --- a/homeassistant/components/aemet/translations/ko.json +++ b/homeassistant/components/aemet/translations/ko.json @@ -17,5 +17,14 @@ "description": "AEMET OpenData \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://opendata.aemet.es/centrodedescargas/altaUsuario \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694" } } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "AEMET \uae30\uc0c1 \uad00\uce21\uc18c\uc5d0\uc11c \ub370\uc774\ud130 \uc218\uc9d1" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/nl.json b/homeassistant/components/aemet/translations/nl.json index 5479d206fa9..95062239ac2 100644 --- a/homeassistant/components/aemet/translations/nl.json +++ b/homeassistant/components/aemet/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd." + "already_configured": "Locatie is al geconfigureerd" }, "error": { "invalid_api_key": "Ongeldige API-sleutel" diff --git a/homeassistant/components/agent_dvr/translations/nl.json b/homeassistant/components/agent_dvr/translations/nl.json index 7c679f66c11..3999d18f7a7 100644 --- a/homeassistant/components/agent_dvr/translations/nl.json +++ b/homeassistant/components/agent_dvr/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "cannot_connect": "Kan geen verbinding maken" }, "step": { diff --git a/homeassistant/components/airly/translations/nl.json b/homeassistant/components/airly/translations/nl.json index 7cfa0c5776f..9bfce7e7708 100644 --- a/homeassistant/components/airly/translations/nl.json +++ b/homeassistant/components/airly/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd." + "already_configured": "Locatie is al geconfigureerd" }, "error": { "invalid_api_key": "Ongeldige API-sleutel", diff --git a/homeassistant/components/airvisual/translations/nl.json b/homeassistant/components/airvisual/translations/nl.json index ddbcc6e6009..c857a51ba61 100644 --- a/homeassistant/components/airvisual/translations/nl.json +++ b/homeassistant/components/airvisual/translations/nl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd. of Node/Pro IDis al geregistreerd.", - "reauth_successful": "Herauthenticatie was succesvol" + "already_configured": "Locatie is al geconfigureerd of Node/Pro IDis al geregistreerd.", + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", - "general_error": "Er is een onbekende fout opgetreden.", + "general_error": "Onverwachte fout", "invalid_api_key": "Ongeldige API-sleutel", "location_not_found": "Locatie niet gevonden" }, @@ -25,15 +25,15 @@ "api_key": "API-sleutel", "city": "Stad", "country": "Land", - "state": "staat" + "state": "status" }, "description": "Gebruik de AirVisual-cloud-API om een stad/staat/land te bewaken.", "title": "Configureer een geografie" }, "node_pro": { "data": { - "ip_address": "IP adres/hostname van component", - "password": "Wachtwoord van component" + "ip_address": "Host", + "password": "Wachtwoord" }, "description": "Monitor een persoonlijke AirVisual-eenheid. Het wachtwoord kan worden opgehaald uit de gebruikersinterface van het apparaat.", "title": "Configureer een AirVisual Node / Pro" diff --git a/homeassistant/components/airzone/translations/nl.json b/homeassistant/components/airzone/translations/nl.json index 0e84f756de1..e182d71963d 100644 --- a/homeassistant/components/airzone/translations/nl.json +++ b/homeassistant/components/airzone/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "invalid_system_id": "Ongeldige Airzone systeem ID" }, "step": { diff --git a/homeassistant/components/aladdin_connect/translations/nl.json b/homeassistant/components/aladdin_connect/translations/nl.json index 314df5712f8..90d1fb67029 100644 --- a/homeassistant/components/aladdin_connect/translations/nl.json +++ b/homeassistant/components/aladdin_connect/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -14,7 +14,7 @@ "password": "Wachtwoord" }, "description": "De Aladdin Connect-integratie moet uw account opnieuw verifi\u00ebren", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" }, "user": { "data": { diff --git a/homeassistant/components/alarm_control_panel/translations/nl.json b/homeassistant/components/alarm_control_panel/translations/nl.json index 5527101589b..a2a0eefb149 100644 --- a/homeassistant/components/alarm_control_panel/translations/nl.json +++ b/homeassistant/components/alarm_control_panel/translations/nl.json @@ -28,15 +28,15 @@ "state": { "_": { "armed": "Ingeschakeld", - "armed_away": "Ingeschakeld voor vertrek", + "armed_away": "Ingeschakeld afwezig", "armed_custom_bypass": "Ingeschakeld met overbrugging", - "armed_home": "Ingeschakeld voor thuis", - "armed_night": "Ingeschakeld voor 's nachts", + "armed_home": "Ingeschakeld thuis", + "armed_night": "Ingeschakeld nacht", "armed_vacation": "Vakantie ingeschakeld", "arming": "Schakelt in", "disarmed": "Uitgeschakeld", "disarming": "Schakelt uit", - "pending": "In wacht", + "pending": "In afwachting", "triggered": "Gaat af" } }, diff --git a/homeassistant/components/almond/translations/nl.json b/homeassistant/components/almond/translations/nl.json index 4c507cfab69..e548206e23e 100644 --- a/homeassistant/components/almond/translations/nl.json +++ b/homeassistant/components/almond/translations/nl.json @@ -2,8 +2,8 @@ "config": { "abort": { "cannot_connect": "Kan geen verbinding maken", - "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { diff --git a/homeassistant/components/ambee/translations/ko.json b/homeassistant/components/ambee/translations/ko.json new file mode 100644 index 00000000000..574b7cd0976 --- /dev/null +++ b/homeassistant/components/ambee/translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_api_key": "API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "api_key": "API \ud0a4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/nl.json b/homeassistant/components/ambee/translations/nl.json index 837e39a72d7..5d356037652 100644 --- a/homeassistant/components/ambee/translations/nl.json +++ b/homeassistant/components/ambee/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/ambiclimate/translations/nl.json b/homeassistant/components/ambiclimate/translations/nl.json index 6d3b3822224..e0679131de4 100644 --- a/homeassistant/components/ambiclimate/translations/nl.json +++ b/homeassistant/components/ambiclimate/translations/nl.json @@ -3,10 +3,10 @@ "abort": { "access_token": "Onbekende fout bij het genereren van een toegangstoken.", "already_configured": "Account is al geconfigureerd", - "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen." + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie." }, "create_entry": { - "default": "Succesvol geauthenticeerd" + "default": "Authenticatie geslaagd" }, "error": { "follow_link": "Gelieve de link te volgen en te verifi\u00ebren voordat u op Verzenden drukt.", diff --git a/homeassistant/components/apple_tv/translations/ko.json b/homeassistant/components/apple_tv/translations/ko.json index fb2373eac4f..9bddc31c56f 100644 --- a/homeassistant/components/apple_tv/translations/ko.json +++ b/homeassistant/components/apple_tv/translations/ko.json @@ -4,6 +4,7 @@ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "backoff": "\uae30\uae30\uac00 \ud604\uc7ac \ud398\uc5b4\ub9c1 \uc694\uccad\uc744 \uc218\ub77d\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4(\uc798\ubabb\ub41c PIN \ucf54\ub4dc\ub97c \ub108\ubb34 \ub9ce\uc774 \uc785\ub825\ud588\uc744 \uc218 \uc788\uc74c). \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "device_did_not_pair": "\uae30\uae30\uc5d0\uc11c \ud398\uc5b4\ub9c1 \ud504\ub85c\uc138\uc2a4\ub97c \uc644\ub8cc\ud558\ub824\uace0 \uc2dc\ub3c4\ud558\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "inconsistent_device": "\uc7a5\uce58\uac80\uc0c9 \uc911\uc5d0 \ud574\ub2f9 \ud504\ub85c\ud1a0\ucf5c\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774\ub294 \uc77c\ubc18\uc801\uc73c\ub85c \uba40\ud2f0\uce90\uc2a4\ud2b8 DNS(Zeroconf)\uc5d0 \ubb38\uc81c\uac00 \uc788\uc74c\uc744 \ub098\ud0c0\ub0c5\ub2c8\ub2e4. \uc7a5\uce58\ub97c \ub2e4\uc2dc \ucd94\uac00\ud574 \ubcf4\uc2ed\uc2dc\uc624.", "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/apple_tv/translations/nl.json b/homeassistant/components/apple_tv/translations/nl.json index 36bebc02761..c7adfa6d756 100644 --- a/homeassistant/components/apple_tv/translations/nl.json +++ b/homeassistant/components/apple_tv/translations/nl.json @@ -2,14 +2,14 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "backoff": "Het apparaat accepteert op dit moment geen koppelingsverzoeken (u heeft mogelijk te vaak een ongeldige pincode ingevoerd), probeer het later opnieuw.", "device_did_not_pair": "Er is geen poging gedaan om het koppelingsproces te voltooien vanaf het apparaat.", "device_not_found": "Apparaat werd niet gevonden tijdens het zoeken, probeer het opnieuw toe te voegen.", "inconsistent_device": "De verwachte protocollen zijn niet gevonden tijdens het zoeken. Dit wijst gewoonlijk op een probleem met multicast DNS (Zeroconf). Probeer het apparaat opnieuw toe te voegen.", "ipv6_not_supported": "IPv6 wordt niet ondersteund.", "no_devices_found": "Geen apparaten gevonden op het netwerk", - "reauth_successful": "Herauthenticatie was succesvol", + "reauth_successful": "Herauthenticatie geslaagd", "setup_failed": "Kan het apparaat niet instellen.", "unknown": "Onverwachte fout" }, @@ -31,7 +31,7 @@ }, "pair_with_pin": { "data": { - "pin": "PIN-code" + "pin": "Pincode" }, "description": "Koppelen is vereist voor het `{protocol}` protocol. Voer de PIN-code in die op het scherm wordt getoond. Beginnende nullen moeten worden weggelaten, d.w.z. voer 123 in als de getoonde code 0123 is.", "title": "Koppelen" diff --git a/homeassistant/components/arcam_fmj/translations/nl.json b/homeassistant/components/arcam_fmj/translations/nl.json index 45a7be867b9..76d3c93739a 100644 --- a/homeassistant/components/arcam_fmj/translations/nl.json +++ b/homeassistant/components/arcam_fmj/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "cannot_connect": "Kan geen verbinding maken" }, "error": { diff --git a/homeassistant/components/asuswrt/translations/nl.json b/homeassistant/components/asuswrt/translations/nl.json index 17be650bf07..5e85888843a 100644 --- a/homeassistant/components/asuswrt/translations/nl.json +++ b/homeassistant/components/asuswrt/translations/nl.json @@ -16,7 +16,7 @@ "user": { "data": { "host": "Host", - "mode": "Mode", + "mode": "Modus", "name": "Naam", "password": "Wachtwoord", "port": "Poort (leeg laten voor protocol standaard)", diff --git a/homeassistant/components/atag/translations/nl.json b/homeassistant/components/atag/translations/nl.json index 98200dd3f6e..da53d96e88f 100644 --- a/homeassistant/components/atag/translations/nl.json +++ b/homeassistant/components/atag/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Er kan slechts \u00e9\u00e9n Atag-apparaat worden toegevoegd aan Home Assistant " + "already_configured": "Apparaat is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -11,7 +11,7 @@ "user": { "data": { "host": "Host", - "port": "Poort " + "port": "Poort" }, "title": "Verbinding maken met het apparaat" } diff --git a/homeassistant/components/august/translations/nl.json b/homeassistant/components/august/translations/nl.json index 2d9a4202f74..e2bbc4d636d 100644 --- a/homeassistant/components/august/translations/nl.json +++ b/homeassistant/components/august/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/aussie_broadband/translations/nl.json b/homeassistant/components/aussie_broadband/translations/nl.json index ec95145c67b..efb553cca25 100644 --- a/homeassistant/components/aussie_broadband/translations/nl.json +++ b/homeassistant/components/aussie_broadband/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Account is al geconfigureerd", "no_services_found": "Er zijn geen services gevonden voor dit account", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -16,7 +16,7 @@ "password": "Wachtwoord" }, "description": "Update wachtwoord voor {username}", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" }, "service": { "data": { diff --git a/homeassistant/components/awair/translations/nl.json b/homeassistant/components/awair/translations/nl.json index d41b85cc09b..1b58405af32 100644 --- a/homeassistant/components/awair/translations/nl.json +++ b/homeassistant/components/awair/translations/nl.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "no_devices_found": "Geen apparaten op het netwerk gevonden", - "reauth_successful": "Herauthenticatie was succesvol" + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "invalid_access_token": "Ongeldig toegangstoken", diff --git a/homeassistant/components/axis/translations/nl.json b/homeassistant/components/axis/translations/nl.json index 3b41c1184ba..f51f59f775f 100644 --- a/homeassistant/components/axis/translations/nl.json +++ b/homeassistant/components/axis/translations/nl.json @@ -7,7 +7,7 @@ }, "error": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie" }, diff --git a/homeassistant/components/azure_devops/translations/nl.json b/homeassistant/components/azure_devops/translations/nl.json index a57dd85c495..3267ea89c19 100644 --- a/homeassistant/components/azure_devops/translations/nl.json +++ b/homeassistant/components/azure_devops/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/azure_event_hub/translations/nl.json b/homeassistant/components/azure_event_hub/translations/nl.json index 646d820c2ad..e0aabd1c971 100644 --- a/homeassistant/components/azure_event_hub/translations/nl.json +++ b/homeassistant/components/azure_event_hub/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Service is al geconfigureerd", "cannot_connect": "Verbinding maken met de credentials uit de configuration.yaml is mislukt, verwijder deze uit yaml en gebruik de config flow.", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", "unknown": "Verbinding maken met de credentials uit de configuration.yaml is mislukt met een onbekende fout, verwijder deze uit yaml en gebruik de config flow." }, "error": { diff --git a/homeassistant/components/baf/translations/pl.json b/homeassistant/components/baf/translations/pl.json index 5c64afd30a8..b0347c8318c 100644 --- a/homeassistant/components/baf/translations/pl.json +++ b/homeassistant/components/baf/translations/pl.json @@ -1,13 +1,18 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "ipv6_not_supported": "IPv6 nie jest obs\u0142ugiwany." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown": "Nieoczekiwany b\u0142\u0105d" }, + "flow_title": "{name} - {model} ({ip_address})", "step": { + "discovery_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name} - {model} ({ip_address})?" + }, "user": { "data": { "ip_address": "Adres IP" diff --git a/homeassistant/components/blebox/translations/nl.json b/homeassistant/components/blebox/translations/nl.json index 65a775e6f72..a9acfc5f71e 100644 --- a/homeassistant/components/blebox/translations/nl.json +++ b/homeassistant/components/blebox/translations/nl.json @@ -5,7 +5,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "unknown": "Onverwachte fout", "unsupported_version": "BleBox-apparaat heeft verouderde firmware. Upgrade het eerst." }, diff --git a/homeassistant/components/bond/translations/nl.json b/homeassistant/components/bond/translations/nl.json index fcf519d681d..b2ba43c8275 100644 --- a/homeassistant/components/bond/translations/nl.json +++ b/homeassistant/components/bond/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "old_firmware": "Niet-ondersteunde oude firmware op het Bond-apparaat - voer een upgrade uit voordat u doorgaat", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/bosch_shc/translations/ko.json b/homeassistant/components/bosch_shc/translations/ko.json new file mode 100644 index 00000000000..3289cb23ce1 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/ko.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "pairing_failed": "\ud398\uc5b4\ub9c1\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. Bosch Smart Home Controller\uac00 \ud398\uc5b4\ub9c1 \ubaa8\ub4dc(LED \uae5c\ubc15\uc784)\uc774\uace0 \ube44\ubc00\ubc88\ud638\uac00 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud558\uc2ed\uc2dc\uc624.", + "session_error": "\uc138\uc158 \uc624\ub958: API\uac00 \ube44\uc815\uc0c1 \uacb0\uacfc\ub97c \ubc18\ud658\ud569\ub2c8\ub2e4.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "Bosch SHC: {name}", + "step": { + "confirm_discovery": { + "description": "LED\uac00 \uae5c\ubc15\uc774\uae30 \uc2dc\uc791\ud560 \ub54c\uae4c\uc9c0 Bosch Smart Home Controller\uc758 \uc804\uba74 \ubc84\ud2bc\uc744 \ub204\ub974\uc2ed\uc2dc\uc624.\nHome Assistant\ub85c {model} @ {host} \ub97c \uacc4\uc18d \uc124\uc815\ud560 \uc900\ube44\uac00 \ub418\uc168\uc2b5\ub2c8\uae4c?" + }, + "credentials": { + "data": { + "password": "Smart Home Controller\uc758 \ube44\ubc00\ubc88\ud638" + } + }, + "reauth_confirm": { + "description": "bosch_shc \ud1b5\ud569\uad6c\uc131\uc694\uc18c\ub294 \uacc4\uc815\uc744 \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4.", + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8" + }, + "description": "Home Assistant\ub85c \ubaa8\ub2c8\ud130\ub9c1\ud558\uace0 \uc81c\uc5b4\ud560 \uc218 \uc788\ub3c4\ub85d Bosch Smart Home Controller\ub97c \uc124\uc815\ud558\uc2ed\uc2dc\uc624.", + "title": "SHC \uc778\uc99d \ub9e4\uac1c\ubcc0\uc218" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/nl.json b/homeassistant/components/bosch_shc/translations/nl.json index 50aa82e2c7b..dd134e54c06 100644 --- a/homeassistant/components/bosch_shc/translations/nl.json +++ b/homeassistant/components/bosch_shc/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -23,7 +23,7 @@ }, "reauth_confirm": { "description": "De bosch_shc integratie moet uw account herauthenticeren", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/nl.json b/homeassistant/components/braviatv/translations/nl.json index f839b83e883..d3696024643 100644 --- a/homeassistant/components/braviatv/translations/nl.json +++ b/homeassistant/components/braviatv/translations/nl.json @@ -12,7 +12,7 @@ "step": { "authorize": { "data": { - "pin": "PIN-code" + "pin": "Pincode" }, "description": "Voer de pincode in die wordt weergegeven op de Sony Bravia tv. \n\nAls de pincode niet wordt weergegeven, moet u de Home Assistant op uw tv afmelden, ga naar: Instellingen -> Netwerk -> Instellingen extern apparaat -> Afmelden extern apparaat.", "title": "Autoriseer Sony Bravia tv" diff --git a/homeassistant/components/broadlink/translations/nl.json b/homeassistant/components/broadlink/translations/nl.json index da75118d5b1..6be9db286e2 100644 --- a/homeassistant/components/broadlink/translations/nl.json +++ b/homeassistant/components/broadlink/translations/nl.json @@ -2,14 +2,14 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", - "cannot_connect": "Kon niet verbinden", + "already_in_progress": "De configuratie is momenteel al bezig", + "cannot_connect": "Kan geen verbinding maken", "invalid_host": "Ongeldige hostnaam of IP-adres", "not_supported": "Apparaat wordt niet ondersteund", "unknown": "Onverwachte fout" }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "invalid_host": "Ongeldige hostnaam of IP-adres", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/brother/translations/nl.json b/homeassistant/components/brother/translations/nl.json index 97c506299a7..2021acef608 100644 --- a/homeassistant/components/brother/translations/nl.json +++ b/homeassistant/components/brother/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Deze printer is al geconfigureerd.", + "already_configured": "Apparaat is al geconfigureerd", "unsupported_model": "Dit printermodel wordt niet ondersteund." }, "error": { diff --git a/homeassistant/components/brunt/translations/nl.json b/homeassistant/components/brunt/translations/nl.json index 86a7df6a585..9ea207d1eae 100644 --- a/homeassistant/components/brunt/translations/nl.json +++ b/homeassistant/components/brunt/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -15,7 +15,7 @@ "password": "Wachtwoord" }, "description": "Voer het wachtwoord opnieuw in voor: {username}", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" }, "user": { "data": { diff --git a/homeassistant/components/buienradar/translations/ko.json b/homeassistant/components/buienradar/translations/ko.json new file mode 100644 index 00000000000..8fe5422ea3e --- /dev/null +++ b/homeassistant/components/buienradar/translations/ko.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "\uce74\uba54\ub77c \uc774\ubbf8\uc9c0\ub97c \ud45c\uc2dc\ud560 \uad6d\uac00\uc758 \uad6d\uac00 \ucf54\ub4dc\uc785\ub2c8\ub2e4.", + "delta": "\uce74\uba54\ub77c \uc774\ubbf8\uc9c0 \uc5c5\ub370\uc774\ud2b8 \uc0ac\uc774\uc758 \uc2dc\uac04 \uac04\uaca9(\ucd08)", + "timeframe": "\uac15\uc218 \uc608\ubcf4\ub97c \uc704\ud574 \ubbf8\ub9ac \ubcfc \uc2dc\uac04(\ubd84)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/buienradar/translations/nl.json b/homeassistant/components/buienradar/translations/nl.json index 0d022f1c61e..c39ee638e08 100644 --- a/homeassistant/components/buienradar/translations/nl.json +++ b/homeassistant/components/buienradar/translations/nl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd." + "already_configured": "Locatie is al geconfigureerd" }, "error": { - "already_configured": "Locatie is al geconfigureerd." + "already_configured": "Locatie is al geconfigureerd" }, "step": { "user": { diff --git a/homeassistant/components/camera/translations/nl.json b/homeassistant/components/camera/translations/nl.json index 976d8e651fb..29dd5038b8f 100644 --- a/homeassistant/components/camera/translations/nl.json +++ b/homeassistant/components/camera/translations/nl.json @@ -3,7 +3,7 @@ "_": { "idle": "Inactief", "recording": "Opnemen", - "streaming": "Streamen" + "streaming": "Streaming" } }, "title": "Camera" diff --git a/homeassistant/components/canary/translations/nl.json b/homeassistant/components/canary/translations/nl.json index ed64af346ea..18cdee2c7a7 100644 --- a/homeassistant/components/canary/translations/nl.json +++ b/homeassistant/components/canary/translations/nl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n enkele configuratie mogelijk.", + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", "unknown": "Onverwachte fout" }, "error": { - "cannot_connect": "Kon niet verbinden" + "cannot_connect": "Kan geen verbinding maken" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/cast/translations/ko.json b/homeassistant/components/cast/translations/ko.json index 2f1ee52675f..806a05a01f4 100644 --- a/homeassistant/components/cast/translations/ko.json +++ b/homeassistant/components/cast/translations/ko.json @@ -22,6 +22,11 @@ "options": { "error": { "invalid_known_hosts": "\uc54c\ub824\uc9c4 \ud638\uc2a4\ud2b8\ub294 \uc27c\ud45c\ub85c \uad6c\ubd84\ub41c \ud638\uc2a4\ud2b8 \ubaa9\ub85d\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4." + }, + "step": { + "basic_options": { + "description": "\uc54c\ub824\uc9c4 \ud638\uc2a4\ud2b8 - \uce90\uc2a4\ud2b8 \uc7a5\uce58\uc758 \ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uc758 \uc27c\ud45c\ub85c \uad6c\ubd84\ub41c \ubaa9\ub85d\uc73c\ub85c, mDNS \uac80\uc0c9\uc774 \uc791\ub3d9\ud558\uc9c0 \uc54a\ub294 \uacbd\uc6b0\uc5d0 \uc0ac\uc6a9\ud569\ub2c8\ub2e4" + } } } } \ No newline at end of file diff --git a/homeassistant/components/cast/translations/nl.json b/homeassistant/components/cast/translations/nl.json index 26dc954ef13..b9dba7ef3e4 100644 --- a/homeassistant/components/cast/translations/nl.json +++ b/homeassistant/components/cast/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { "invalid_known_hosts": "Bekende hosts moet een door komma's gescheiden lijst van hosts zijn." @@ -15,7 +15,7 @@ "title": "Google Cast configuratie" }, "confirm": { - "description": "Wilt u beginnen met instellen?" + "description": "Wil je beginnen met instellen?" } } }, diff --git a/homeassistant/components/climacell/translations/sensor.ko.json b/homeassistant/components/climacell/translations/sensor.ko.json new file mode 100644 index 00000000000..e5ec616959e --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.ko.json @@ -0,0 +1,7 @@ +{ + "state": { + "climacell__precipitation_type": { + "snow": "\ub208" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climacell/translations/sensor.nl.json b/homeassistant/components/climacell/translations/sensor.nl.json index c457988681b..710198156d1 100644 --- a/homeassistant/components/climacell/translations/sensor.nl.json +++ b/homeassistant/components/climacell/translations/sensor.nl.json @@ -18,7 +18,7 @@ }, "climacell__precipitation_type": { "freezing_rain": "IJzel", - "ice_pellets": "Hagel", + "ice_pellets": "IJskorrels", "none": "Geen", "rain": "Regen", "snow": "Sneeuw" diff --git a/homeassistant/components/climate/translations/nl.json b/homeassistant/components/climate/translations/nl.json index 0237d2bbd9a..016131858a4 100644 --- a/homeassistant/components/climate/translations/nl.json +++ b/homeassistant/components/climate/translations/nl.json @@ -18,8 +18,8 @@ "_": { "auto": "Auto", "cool": "Koelen", - "dry": "Droog", - "fan_only": "Alleen ventilatie", + "dry": "Drogen", + "fan_only": "Alleen ventilator", "heat": "Verwarmen", "heat_cool": "Verwarmen/Koelen", "off": "Uit" diff --git a/homeassistant/components/cloudflare/translations/nl.json b/homeassistant/components/cloudflare/translations/nl.json index 5a1bf188a29..6be4bf1e0a8 100644 --- a/homeassistant/components/cloudflare/translations/nl.json +++ b/homeassistant/components/cloudflare/translations/nl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "reauth_successful": "Herauthenticatie was succesvol", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", + "reauth_successful": "Herauthenticatie geslaagd", + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", "unknown": "Onverwachte fout" }, "error": { diff --git a/homeassistant/components/configurator/translations/nl.json b/homeassistant/components/configurator/translations/nl.json index d8ad5061e0f..ea1bd002c5d 100644 --- a/homeassistant/components/configurator/translations/nl.json +++ b/homeassistant/components/configurator/translations/nl.json @@ -1,7 +1,7 @@ { "state": { "_": { - "configure": "Configureer", + "configure": "Configureren", "configured": "Geconfigureerd" } }, diff --git a/homeassistant/components/cover/translations/nl.json b/homeassistant/components/cover/translations/nl.json index 80acc874379..31c8c90d2b7 100644 --- a/homeassistant/components/cover/translations/nl.json +++ b/homeassistant/components/cover/translations/nl.json @@ -29,7 +29,7 @@ "state": { "_": { "closed": "Gesloten", - "closing": "Sluiten", + "closing": "Sluitend", "open": "Open", "opening": "Opent", "stopped": "Gestopt" diff --git a/homeassistant/components/cpuspeed/translations/nl.json b/homeassistant/components/cpuspeed/translations/nl.json index c3d09b0b8a6..fab4a86c645 100644 --- a/homeassistant/components/cpuspeed/translations/nl.json +++ b/homeassistant/components/cpuspeed/translations/nl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", + "already_configured": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", "not_compatible": "Kan geen CPU-informatie ophalen, deze integratie is niet compatibel met uw systeem" }, "step": { "user": { - "description": "Wilt u beginnen met instellen?", + "description": "Wil je beginnen met instellen?", "title": "CPU-snelheid" } } diff --git a/homeassistant/components/daikin/translations/nl.json b/homeassistant/components/daikin/translations/nl.json index 33659797e7a..47b65415437 100644 --- a/homeassistant/components/daikin/translations/nl.json +++ b/homeassistant/components/daikin/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "cannot_connect": "Kon niet verbinden" + "cannot_connect": "Kan geen verbinding maken" }, "error": { "api_password": "Ongeldige authenticatie, gebruik API-sleutel of wachtwoord.", diff --git a/homeassistant/components/deconz/translations/nl.json b/homeassistant/components/deconz/translations/nl.json index caa6ca461fc..474973b9594 100644 --- a/homeassistant/components/deconz/translations/nl.json +++ b/homeassistant/components/deconz/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Bridge is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "no_bridges": "Geen deCONZ bridges ontdekt", "no_hardware_available": "Geen radiohardware aangesloten op deCONZ", "updated_instance": "DeCONZ-instantie bijgewerkt met nieuw host-adres" diff --git a/homeassistant/components/demo/translations/select.nl.json b/homeassistant/components/demo/translations/select.nl.json index 4312d8c4d34..1d7e7f9be13 100644 --- a/homeassistant/components/demo/translations/select.nl.json +++ b/homeassistant/components/demo/translations/select.nl.json @@ -2,7 +2,7 @@ "state": { "demo__speed": { "light_speed": "Lichtsnelheid", - "ludicrous_speed": "Lachwekkende snelheid", + "ludicrous_speed": "Belachelijke snelheid", "ridiculous_speed": "Belachelijke snelheid" } } diff --git a/homeassistant/components/denonavr/translations/ko.json b/homeassistant/components/denonavr/translations/ko.json index 573011186aa..4a880f6757f 100644 --- a/homeassistant/components/denonavr/translations/ko.json +++ b/homeassistant/components/denonavr/translations/ko.json @@ -34,6 +34,7 @@ "init": { "data": { "show_all_sources": "\ubaa8\ub4e0 \uc785\ub825\uc18c\uc2a4 \ud45c\uc2dc", + "update_audyssey": "Audyssey \uc124\uc815 \uc5c5\ub370\uc774\ud2b8", "zone2": "Zone 2 \uc124\uc815", "zone3": "Zone 3 \uc124\uc815" }, diff --git a/homeassistant/components/denonavr/translations/nl.json b/homeassistant/components/denonavr/translations/nl.json index f3683561686..6ebb0d845d7 100644 --- a/homeassistant/components/denonavr/translations/nl.json +++ b/homeassistant/components/denonavr/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "cannot_connect": "Verbinding mislukt, probeer het opnieuw, de stroom- en ethernetkabels loskoppelen en opnieuw aansluiten kan helpen", "not_denonavr_manufacturer": "Geen Denon AVR Netwerk Receiver, ontdekte fabrikant komt niet overeen", "not_denonavr_missing": "Geen Denon AVR netwerkontvanger, zoekinformatie niet compleet" diff --git a/homeassistant/components/devolo_home_control/translations/nl.json b/homeassistant/components/devolo_home_control/translations/nl.json index c85c685597d..18ad384342b 100644 --- a/homeassistant/components/devolo_home_control/translations/nl.json +++ b/homeassistant/components/devolo_home_control/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "invalid_auth": "Ongeldige authenticatie", diff --git a/homeassistant/components/devolo_home_network/translations/ko.json b/homeassistant/components/devolo_home_network/translations/ko.json index 758f3336cd4..0e7d5b287f2 100644 --- a/homeassistant/components/devolo_home_network/translations/ko.json +++ b/homeassistant/components/devolo_home_network/translations/ko.json @@ -3,6 +3,9 @@ "step": { "user": { "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + }, + "zeroconf_confirm": { + "title": "devolo \ud648 \ub124\ud2b8\uc6cc\ud06c \uc7a5\uce58 \ubc1c\uacac" } } } diff --git a/homeassistant/components/devolo_home_network/translations/nl.json b/homeassistant/components/devolo_home_network/translations/nl.json index e8730f44b5e..af3d6e55365 100644 --- a/homeassistant/components/devolo_home_network/translations/nl.json +++ b/homeassistant/components/devolo_home_network/translations/nl.json @@ -14,7 +14,7 @@ "data": { "ip_address": "IP-adres" }, - "description": "Wilt u beginnen met instellen?" + "description": "Wil je beginnen met instellen?" }, "zeroconf_confirm": { "description": "Wilt u het devolo-thuisnetwerkapparaat met de hostnaam ` {host_name} ` aan Home Assistant toevoegen?", diff --git a/homeassistant/components/dialogflow/translations/nl.json b/homeassistant/components/dialogflow/translations/nl.json index 3d2617d25e5..7403e2c36dd 100644 --- a/homeassistant/components/dialogflow/translations/nl.json +++ b/homeassistant/components/dialogflow/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "cloud_not_connected": "Niet verbonden met Home Assistant Cloud.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", - "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." + "webhook_not_internet_accessible": "Je Home Assistant-instantie moet toegankelijk zijn vanaf internet om webhook-berichten te ontvangen." }, "create_entry": { "default": "Om evenementen naar de Home Assistant te verzenden, moet u [webhookintegratie van Dialogflow]({dialogflow_url}) instellen. \n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nZie [de documentatie]({docs_url}) voor verdere informatie." diff --git a/homeassistant/components/discord/translations/nl.json b/homeassistant/components/discord/translations/nl.json index 55fb894031d..303b22fdfad 100644 --- a/homeassistant/components/discord/translations/nl.json +++ b/homeassistant/components/discord/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Service is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/dlna_dmr/translations/ko.json b/homeassistant/components/dlna_dmr/translations/ko.json index c3c5f1a74b2..c25d0fcabfc 100644 --- a/homeassistant/components/dlna_dmr/translations/ko.json +++ b/homeassistant/components/dlna_dmr/translations/ko.json @@ -9,6 +9,9 @@ }, "manual": { "description": "\uc7a5\uce58 \uc124\uba85 XML \ud30c\uc77c\uc758 URL" + }, + "user": { + "title": "DLNA DMR \uc7a5\uce58 \ubc1c\uacac" } } }, diff --git a/homeassistant/components/dlna_dmr/translations/nl.json b/homeassistant/components/dlna_dmr/translations/nl.json index 360d24fd590..adc554b0fbc 100644 --- a/homeassistant/components/dlna_dmr/translations/nl.json +++ b/homeassistant/components/dlna_dmr/translations/nl.json @@ -16,7 +16,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Wilt u beginnen met instellen?" + "description": "Wil je beginnen met instellen?" }, "import_turn_on": { "description": "Zet het apparaat aan en klik op verzenden om door te gaan met de migratie" diff --git a/homeassistant/components/dlna_dms/translations/ko.json b/homeassistant/components/dlna_dms/translations/ko.json index 20ad990e862..853a3eb2b1a 100644 --- a/homeassistant/components/dlna_dms/translations/ko.json +++ b/homeassistant/components/dlna_dms/translations/ko.json @@ -3,6 +3,9 @@ "step": { "confirm": { "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + }, + "user": { + "title": "DLNA DMA \uc7a5\uce58 \ubc1c\uacac" } } } diff --git a/homeassistant/components/dlna_dms/translations/nl.json b/homeassistant/components/dlna_dms/translations/nl.json index d480118b76a..2edd4c5d2a6 100644 --- a/homeassistant/components/dlna_dms/translations/nl.json +++ b/homeassistant/components/dlna_dms/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "bad_ssdp": "SSDP-gegevens missen een vereiste waarde", "no_devices_found": "Geen apparaten gevonden op het netwerk", "not_dms": "Apparaat is geen ondersteunde mediaserver" @@ -10,7 +10,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Wilt u beginnen met instellen?" + "description": "Wil je beginnen met instellen?" }, "user": { "data": { diff --git a/homeassistant/components/dunehd/translations/nl.json b/homeassistant/components/dunehd/translations/nl.json index 3ee04091a1c..9f75cd5a91a 100644 --- a/homeassistant/components/dunehd/translations/nl.json +++ b/homeassistant/components/dunehd/translations/nl.json @@ -5,8 +5,8 @@ }, "error": { "already_configured": "Apparaat is al geconfigureerd", - "cannot_connect": "Kon niet verbinden", - "invalid_host": "ongeldige host of IP adres" + "cannot_connect": "Kan geen verbinding maken", + "invalid_host": "Ongeldige hostnaam of IP-adres" }, "step": { "user": { diff --git a/homeassistant/components/ecobee/translations/nl.json b/homeassistant/components/ecobee/translations/nl.json index 957d2f8244d..03ba56253bf 100644 --- a/homeassistant/components/ecobee/translations/nl.json +++ b/homeassistant/components/ecobee/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { "pin_request_failed": "Fout bij het aanvragen van pincode bij ecobee; Controleer of de API-sleutel correct is.", diff --git a/homeassistant/components/efergy/translations/nl.json b/homeassistant/components/efergy/translations/nl.json index 0a51ce58a9c..ea0ec0c62a9 100644 --- a/homeassistant/components/efergy/translations/nl.json +++ b/homeassistant/components/efergy/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/elkm1/translations/ko.json b/homeassistant/components/elkm1/translations/ko.json index 6ffa3679946..5b16671c05b 100644 --- a/homeassistant/components/elkm1/translations/ko.json +++ b/homeassistant/components/elkm1/translations/ko.json @@ -10,6 +10,14 @@ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { + "discovered_connection": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "\uc790\ub3d9\uac80\uc0c9\ub41c \uc2dc\uc2a4\ud15c\uc5d0 \uc5f0\uacb0: {mac_address} ({host})", + "title": "Elk-M1 \uc81c\uc5b4\uc5d0 \uc5f0\uacb0\ud558\uae30" + }, "user": { "description": "\uc8fc\uc18c \ubb38\uc790\uc5f4\uc740 '\ubcf4\uc548' \ubc0f '\ube44\ubcf4\uc548'\uc5d0 \ub300\ud574 'address[:port]' \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608: '192.168.1.1'. \ud3ec\ud2b8\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 '\ube44\ubcf4\uc548' \uc758 \uacbd\uc6b0 2101 \uc774\uace0 '\ubcf4\uc548' \uc758 \uacbd\uc6b0 2601 \uc785\ub2c8\ub2e4. \uc2dc\ub9ac\uc5bc \ud504\ub85c\ud1a0\ucf5c\uc758 \uacbd\uc6b0 \uc8fc\uc18c\ub294 'tty[:baud]' \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608: '/dev/ttyS1'. \uc804\uc1a1 \uc18d\ub3c4\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 115200 \uc785\ub2c8\ub2e4.", "title": "Elk-M1 \uc81c\uc5b4\uc5d0 \uc5f0\uacb0\ud558\uae30" diff --git a/homeassistant/components/elkm1/translations/nl.json b/homeassistant/components/elkm1/translations/nl.json index c27293ce050..c33aa601433 100644 --- a/homeassistant/components/elkm1/translations/nl.json +++ b/homeassistant/components/elkm1/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "address_already_configured": "Een ElkM1 met dit adres is al geconfigureerd", "already_configured": "Een ElkM1 met dit voorvoegsel is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/elmax/translations/ko.json b/homeassistant/components/elmax/translations/ko.json new file mode 100644 index 00000000000..b9f74068f56 --- /dev/null +++ b/homeassistant/components/elmax/translations/ko.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "panels": { + "data": { + "panel_pin": "PIN \ucf54\ub4dc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/nl.json b/homeassistant/components/enocean/translations/nl.json index 79e0ab6dfec..6641dfeaaa8 100644 --- a/homeassistant/components/enocean/translations/nl.json +++ b/homeassistant/components/enocean/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_dongle_path": "Ongeldig dongle-pad", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { "invalid_dongle_path": "Geen geldige dongle gevonden voor dit pad" diff --git a/homeassistant/components/enphase_envoy/translations/nl.json b/homeassistant/components/enphase_envoy/translations/nl.json index 26a96d5bde2..32975a82f7a 100644 --- a/homeassistant/components/enphase_envoy/translations/nl.json +++ b/homeassistant/components/enphase_envoy/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/esphome/translations/nl.json b/homeassistant/components/esphome/translations/nl.json index 9c757e917e4..aae5b639eb8 100644 --- a/homeassistant/components/esphome/translations/nl.json +++ b/homeassistant/components/esphome/translations/nl.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al begonnen", - "reauth_successful": "Herauthenticatie was succesvol" + "already_in_progress": "De configuratie is momenteel al bezig", + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "connection_error": "Kan geen verbinding maken met ESP. Zorg ervoor dat uw YAML-bestand een regel 'api:' bevat.", diff --git a/homeassistant/components/ezviz/translations/ko.json b/homeassistant/components/ezviz/translations/ko.json index 8ed11a874b0..8b44c79ce40 100644 --- a/homeassistant/components/ezviz/translations/ko.json +++ b/homeassistant/components/ezviz/translations/ko.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured_account": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "ezviz_cloud_account_missing": "Ezviz \ud074\ub77c\uc6b0\ub4dc \uacc4\uc815\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. Ezviz \ud074\ub77c\uc6b0\ub4dc \uacc4\uc815\uc744 \ub2e4\uc2dc \uad6c\uc131\ud558\uc2ed\uc2dc\uc624.", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { @@ -15,21 +16,26 @@ "data": { "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - } + }, + "description": "IP\uac00 {ip_address} \uc778 Ezviz \uce74\uba54\ub77c {serial} \uc5d0 \ub300\ud55c RTSP \uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud558\uc138\uc694.", + "title": "Ezviz \uce74\uba54\ub77c \ubc1c\uacac" }, "user": { "data": { "password": "\ube44\ubc00\ubc88\ud638", "url": "URL \uc8fc\uc18c", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - } + }, + "title": "Ezviz \ud074\ub77c\uc6b0\ub4dc\uc5d0 \uc5f0\uacb0" }, "user_custom_url": { "data": { "password": "\ube44\ubc00\ubc88\ud638", "url": "URL \uc8fc\uc18c", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - } + }, + "description": "URL\uc744 \uc218\ub3d9\uc73c\ub85c \uc785\ub825\ud558\uc138\uc694", + "title": "\uc0ac\uc6a9\uc790 \uc9c0\uc815 Ezviz URL\uc5d0 \uc5f0\uacb0" } } }, @@ -37,6 +43,7 @@ "step": { "init": { "data": { + "ffmpeg_arguments": "\uce74\uba54\ub77c\uc5d0 \ub300\ud55c ffmpeg \uc804\ub2ec \uc778\uc218", "timeout": "\uc694\uccad \uc81c\ud55c \uc2dc\uac04 (\ucd08)" } } diff --git a/homeassistant/components/fibaro/translations/nl.json b/homeassistant/components/fibaro/translations/nl.json index 72b8588d1e4..049168c73d8 100644 --- a/homeassistant/components/fibaro/translations/nl.json +++ b/homeassistant/components/fibaro/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/fireservicerota/translations/nl.json b/homeassistant/components/fireservicerota/translations/nl.json index 3a6ba936dee..62085c9a333 100644 --- a/homeassistant/components/fireservicerota/translations/nl.json +++ b/homeassistant/components/fireservicerota/translations/nl.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "create_entry": { - "default": "Succesvol geauthenticeerd" + "default": "Authenticatie geslaagd" }, "error": { "invalid_auth": "Ongeldige authenticatie" diff --git a/homeassistant/components/fjaraskupan/translations/nl.json b/homeassistant/components/fjaraskupan/translations/nl.json index 498ef7af1be..1c973e28d69 100644 --- a/homeassistant/components/fjaraskupan/translations/nl.json +++ b/homeassistant/components/fjaraskupan/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Geen apparaten gevonden op het netwerk", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "confirm": { diff --git a/homeassistant/components/flick_electric/translations/nl.json b/homeassistant/components/flick_electric/translations/nl.json index f90ee71301b..c7c6e2bec81 100644 --- a/homeassistant/components/flick_electric/translations/nl.json +++ b/homeassistant/components/flick_electric/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Account is al geconfigureerd" }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/flo/translations/nl.json b/homeassistant/components/flo/translations/nl.json index d4c409802c1..8ad15260b0d 100644 --- a/homeassistant/components/flo/translations/nl.json +++ b/homeassistant/components/flo/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/flume/translations/ko.json b/homeassistant/components/flume/translations/ko.json index c82f0a990d8..fc3970e69ad 100644 --- a/homeassistant/components/flume/translations/ko.json +++ b/homeassistant/components/flume/translations/ko.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", @@ -9,6 +10,13 @@ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638" + }, + "description": "{username} \uc758 \ube44\ubc00\ubc88\ud638\uac00 \ub354 \uc774\uc0c1 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "title": "Flume \uacc4\uc815 \uc7ac\uc778\uc99d" + }, "user": { "data": { "client_id": "\ud074\ub77c\uc774\uc5b8\ud2b8 ID", diff --git a/homeassistant/components/flume/translations/nl.json b/homeassistant/components/flume/translations/nl.json index de0e225be03..b08b37ca21c 100644 --- a/homeassistant/components/flume/translations/nl.json +++ b/homeassistant/components/flume/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/flunearyou/translations/nl.json b/homeassistant/components/flunearyou/translations/nl.json index d78abfcc187..0938bd45206 100644 --- a/homeassistant/components/flunearyou/translations/nl.json +++ b/homeassistant/components/flunearyou/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd." + "already_configured": "Locatie is al geconfigureerd" }, "error": { "unknown": "Onverwachte fout" diff --git a/homeassistant/components/flux_led/translations/nl.json b/homeassistant/components/flux_led/translations/nl.json index fd9e04bd475..2b64353e94c 100644 --- a/homeassistant/components/flux_led/translations/nl.json +++ b/homeassistant/components/flux_led/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "no_devices_found": "Geen apparaten gevonden op het netwerk" }, "error": { diff --git a/homeassistant/components/fritz/translations/ko.json b/homeassistant/components/fritz/translations/ko.json index 99c42938d6f..314e8cf7729 100644 --- a/homeassistant/components/fritz/translations/ko.json +++ b/homeassistant/components/fritz/translations/ko.json @@ -9,6 +9,7 @@ "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", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "flow_title": "{name}", @@ -17,13 +18,34 @@ "data": { "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" - } + }, + "description": "\ubc1c\uacac\ub41c FRITZ!Box: {name} \n\n FRITZ!Box Tool\uc744 \uc124\uc815\ud558\uc5ec \ub2e4\uc74c\uc744 \uc81c\uc5b4\ud574\ubcf4\uc138\uc694: {name}", + "title": "FRITZ!Box Tool \uc124\uc815" }, "reauth_confirm": { "data": { "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" } + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "FRITZ!Box Tool\uc744 \uc124\uc815\ud558\uc5ec FRITZ!Box\ub97c \uc81c\uc5b4\ud569\ub2c8\ub2e4.\n \ud544\uc218 \uc785\ub825\uc0ac\ud56d: \uc0ac\uc6a9\uc790 \uc774\ub984, \ube44\ubc00\ubc88\ud638.", + "title": "FRITZ!Box Tools \uc124\uc815" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "old_discovery": "\uc774\uc804 \uc7a5\uce58\uac80\uc0c9 \ubc29\ubc95 \uc0ac\uc6a9" + } } } } diff --git a/homeassistant/components/fritz/translations/nl.json b/homeassistant/components/fritz/translations/nl.json index 80aebf4fc8a..6bceac5dcc5 100644 --- a/homeassistant/components/fritz/translations/nl.json +++ b/homeassistant/components/fritz/translations/nl.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "ignore_ip6_link_local": "Lokaal IPv6-linkadres wordt niet ondersteund.", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "upnp_not_configured": "Ontbrekende UPnP instellingen op apparaat." diff --git a/homeassistant/components/fritzbox/translations/nl.json b/homeassistant/components/fritzbox/translations/nl.json index 43d51df760d..524cf40f018 100644 --- a/homeassistant/components/fritzbox/translations/nl.json +++ b/homeassistant/components/fritzbox/translations/nl.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "ignore_ip6_link_local": "IPv6 link lokaal adres wordt niet ondersteund.", "no_devices_found": "Geen apparaten gevonden op het netwerk", "not_supported": "Verbonden met AVM FRITZ! Box, maar het kan geen Smart Home-apparaten bedienen.", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "invalid_auth": "Ongeldige authenticatie" diff --git a/homeassistant/components/garages_amsterdam/translations/ko.json b/homeassistant/components/garages_amsterdam/translations/ko.json new file mode 100644 index 00000000000..f5088fca3e1 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/ko.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/nl.json b/homeassistant/components/generic/translations/nl.json index 2b5bc828727..354f944148f 100644 --- a/homeassistant/components/generic/translations/nl.json +++ b/homeassistant/components/generic/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Geen apparaten gevonden op het netwerk", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { "already_exists": "Een camera met deze URL instellingen bestaat al.", @@ -21,7 +21,7 @@ }, "step": { "confirm": { - "description": "Wilt u beginnen met instellen?" + "description": "Wil je beginnen met instellen?" }, "content_type": { "data": { diff --git a/homeassistant/components/geocaching/translations/nl.json b/homeassistant/components/geocaching/translations/nl.json index 82d40849146..e4cfc747c62 100644 --- a/homeassistant/components/geocaching/translations/nl.json +++ b/homeassistant/components/geocaching/translations/nl.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", - "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", - "oauth_error": "Ongeldige token data ontvangen.", - "reauth_successful": "Herauthenticatie was succesvol" + "already_in_progress": "De configuratie is momenteel al bezig", + "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})", + "oauth_error": "Ongeldige tokengegevens ontvangen.", + "reauth_successful": "Herauthenticatie geslaagd" }, "create_entry": { - "default": "Succesvol geauthenticeerd" + "default": "Authenticatie geslaagd" }, "step": { "pick_implementation": { @@ -18,7 +18,7 @@ }, "reauth_confirm": { "description": "De Geocaching integratie moet uw account herauthenticeren", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" } } } diff --git a/homeassistant/components/geofency/translations/nl.json b/homeassistant/components/geofency/translations/nl.json index 30a2acfdadd..096817de9a6 100644 --- a/homeassistant/components/geofency/translations/nl.json +++ b/homeassistant/components/geofency/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "cloud_not_connected": "Niet verbonden met Home Assistant Cloud.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", - "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." + "webhook_not_internet_accessible": "Je Home Assistant-instantie moet toegankelijk zijn vanaf internet om webhook-berichten te ontvangen." }, "create_entry": { "default": "Om locaties naar Home Assistant te sturen, moet u de Webhook-functie instellen in Geofency.\n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Methode: POST \n\n Zie [de documentatie]({docs_url}) voor meer informatie." diff --git a/homeassistant/components/gios/translations/nl.json b/homeassistant/components/gios/translations/nl.json index 26c83cfff7b..3f714a0fd1e 100644 --- a/homeassistant/components/gios/translations/nl.json +++ b/homeassistant/components/gios/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd." + "already_configured": "Locatie is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/glances/translations/nl.json b/homeassistant/components/glances/translations/nl.json index 6cb7fb445bb..ca414a92ab9 100644 --- a/homeassistant/components/glances/translations/nl.json +++ b/homeassistant/components/glances/translations/nl.json @@ -14,7 +14,7 @@ "name": "Naam", "password": "Wachtwoord", "port": "Poort", - "ssl": "Gebruik een SSL-certificaat", + "ssl": "Maakt gebruik van een SSL-certificaat", "username": "Gebruikersnaam", "verify_ssl": "SSL-certificaat verifi\u00ebren", "version": "Glances API-versie (2 of 3)" diff --git a/homeassistant/components/goalzero/translations/nl.json b/homeassistant/components/goalzero/translations/nl.json index 9f719d74efe..ab70229ace1 100644 --- a/homeassistant/components/goalzero/translations/nl.json +++ b/homeassistant/components/goalzero/translations/nl.json @@ -7,7 +7,7 @@ }, "error": { "cannot_connect": "Kan geen verbinding maken", - "invalid_host": "Onjuiste hostnaam of IP-adres", + "invalid_host": "Ongeldige hostnaam of IP-adres", "unknown": "Onverwachte fout" }, "step": { diff --git a/homeassistant/components/gogogate2/translations/ko.json b/homeassistant/components/gogogate2/translations/ko.json index dc37928db76..e8df5fa95dc 100644 --- a/homeassistant/components/gogogate2/translations/ko.json +++ b/homeassistant/components/gogogate2/translations/ko.json @@ -7,6 +7,7 @@ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/gogogate2/translations/nl.json b/homeassistant/components/gogogate2/translations/nl.json index a32bb1af69b..c194a1c21da 100644 --- a/homeassistant/components/gogogate2/translations/nl.json +++ b/homeassistant/components/gogogate2/translations/nl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "cannot_connect": "Kon niet verbinden" + "cannot_connect": "Kan geen verbinding maken" }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie" }, "flow_title": "{device} ({ip_address})", diff --git a/homeassistant/components/goodwe/translations/nl.json b/homeassistant/components/goodwe/translations/nl.json index 8986006fdeb..cb4d0e7b0b7 100644 --- a/homeassistant/components/goodwe/translations/nl.json +++ b/homeassistant/components/goodwe/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang" + "already_in_progress": "De configuratie is momenteel al bezig" }, "error": { "connection_error": "Kan geen verbinding maken" diff --git a/homeassistant/components/google/translations/ko.json b/homeassistant/components/google/translations/ko.json index 2906ebc5114..e4fc1875308 100644 --- a/homeassistant/components/google/translations/ko.json +++ b/homeassistant/components/google/translations/ko.json @@ -2,6 +2,11 @@ "config": { "abort": { "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + }, + "step": { + "reauth_confirm": { + "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" + } } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/nl.json b/homeassistant/components/google/translations/nl.json index e732fd7cd19..dca192c59d4 100644 --- a/homeassistant/components/google/translations/nl.json +++ b/homeassistant/components/google/translations/nl.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "already_in_progress": "Configuratie flow is al in bewerking", + "already_in_progress": "De configuratie is momenteel al bezig", "code_expired": "De authenticatiecode is verlopen of de instelling van de inloggegevens is ongeldig, probeer het opnieuw.", - "invalid_access_token": "Ongeldige toegang token", - "missing_configuration": "Het component is niet geconfigureerd. Volg a.u.b. de documentatie", - "oauth_error": "Ongeldige token data ontvangen", - "reauth_successful": "Herauthentiecatie was succesvol" + "invalid_access_token": "Ongeldig toegangstoken", + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", + "oauth_error": "Ongeldige tokengegevens ontvangen.", + "reauth_successful": "Herauthenticatie geslaagd" }, "create_entry": { - "default": "Authenticatie succesvol" + "default": "Authenticatie geslaagd" }, "progress": { "exchange": "Om uw Google-account te koppelen, gaat u naar de [ {url} ]( {url} ) en voert u de code in: \n\n {user_code}" @@ -24,7 +24,7 @@ }, "reauth_confirm": { "description": "De Google Agenda-integratie moet uw account opnieuw verifi\u00ebren", - "title": "Herauthentiseer integratie" + "title": "Integratie herauthentiseren" } } } diff --git a/homeassistant/components/google_travel_time/translations/nl.json b/homeassistant/components/google_travel_time/translations/nl.json index cb43d8afeae..bb088c97ca5 100644 --- a/homeassistant/components/google_travel_time/translations/nl.json +++ b/homeassistant/components/google_travel_time/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd." + "already_configured": "Locatie is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken" diff --git a/homeassistant/components/gpslogger/translations/nl.json b/homeassistant/components/gpslogger/translations/nl.json index 26bfba4eaea..f2b5d3158ad 100644 --- a/homeassistant/components/gpslogger/translations/nl.json +++ b/homeassistant/components/gpslogger/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "cloud_not_connected": "Niet verbonden met Home Assistant Cloud.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", - "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." + "webhook_not_internet_accessible": "Je Home Assistant-instantie moet toegankelijk zijn vanaf internet om webhook-berichten te ontvangen." }, "create_entry": { "default": "Om evenementen naar Home Assistant te verzenden, moet u de webhook-functie instellen in GPSLogger. \n\n Vul de volgende info in: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n\n Zie [de documentatie] ( {docs_url} ) voor meer informatie." diff --git a/homeassistant/components/gree/translations/nl.json b/homeassistant/components/gree/translations/nl.json index 0671f0b3674..6fc4a03e824 100644 --- a/homeassistant/components/gree/translations/nl.json +++ b/homeassistant/components/gree/translations/nl.json @@ -2,11 +2,11 @@ "config": { "abort": { "no_devices_found": "Geen apparaten gevonden op het netwerk", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "confirm": { - "description": "Wilt u beginnen met instellen?" + "description": "Wil je beginnen met instellen?" } } } diff --git a/homeassistant/components/growatt_server/translations/ko.json b/homeassistant/components/growatt_server/translations/ko.json new file mode 100644 index 00000000000..676542812e7 --- /dev/null +++ b/homeassistant/components/growatt_server/translations/ko.json @@ -0,0 +1,3 @@ +{ + "title": "Growatt \uc11c\ubc84" +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/ko.json b/homeassistant/components/guardian/translations/ko.json index 8c3c5103c3c..3faa244b246 100644 --- a/homeassistant/components/guardian/translations/ko.json +++ b/homeassistant/components/guardian/translations/ko.json @@ -6,6 +6,9 @@ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { + "discovery_confirm": { + "description": "Guardian \uae30\uae30\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + }, "user": { "data": { "ip_address": "IP \uc8fc\uc18c", diff --git a/homeassistant/components/guardian/translations/nl.json b/homeassistant/components/guardian/translations/nl.json index 42d499315bd..d8ed1242e92 100644 --- a/homeassistant/components/guardian/translations/nl.json +++ b/homeassistant/components/guardian/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "cannot_connect": "Kan geen verbinding maken" }, "step": { diff --git a/homeassistant/components/hisense_aehw4a1/translations/nl.json b/homeassistant/components/hisense_aehw4a1/translations/nl.json index c1f353558b6..0d7661a07b6 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/nl.json +++ b/homeassistant/components/hisense_aehw4a1/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Geen apparaten gevonden op het netwerk", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "confirm": { diff --git a/homeassistant/components/hive/translations/nl.json b/homeassistant/components/hive/translations/nl.json index 206aa661735..2cbc7c94fc6 100644 --- a/homeassistant/components/hive/translations/nl.json +++ b/homeassistant/components/hive/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol", + "reauth_successful": "Herauthenticatie geslaagd", "unknown_entry": "Kan bestaand item niet vinden." }, "error": { diff --git a/homeassistant/components/home_connect/translations/nl.json b/homeassistant/components/home_connect/translations/nl.json index 25a81209607..72bbb5a0e91 100644 --- a/homeassistant/components/home_connect/translations/nl.json +++ b/homeassistant/components/home_connect/translations/nl.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "missing_configuration": "Het component is niet geconfigureerd. Volg de documentatie.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})" + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})" }, "create_entry": { - "default": "Succesvol geverifieerd" + "default": "Authenticatie geslaagd" }, "step": { "pick_implementation": { - "title": "Kies de verificatiemethode" + "title": "Kies een authenticatie methode" } } } diff --git a/homeassistant/components/home_plus_control/translations/nl.json b/homeassistant/components/home_plus_control/translations/nl.json index ffebcf58d60..f1471f36273 100644 --- a/homeassistant/components/home_plus_control/translations/nl.json +++ b/homeassistant/components/home_plus_control/translations/nl.json @@ -2,14 +2,14 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", - "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "already_in_progress": "De configuratie is momenteel al bezig", + "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})", + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "create_entry": { - "default": "Succesvol geauthenticeerd" + "default": "Authenticatie geslaagd" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/homekit_controller/translations/ko.json b/homeassistant/components/homekit_controller/translations/ko.json index c28573ee6ac..fd40288a7e3 100644 --- a/homeassistant/components/homekit_controller/translations/ko.json +++ b/homeassistant/components/homekit_controller/translations/ko.json @@ -12,6 +12,7 @@ }, "error": { "authentication_error": "HomeKit \ucf54\ub4dc\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud655\uc778 \ud6c4 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "insecure_setup_code": "\uc694\uccad\ud55c \uc124\uc815 \ucf54\ub4dc\ub294 \uc548\uc804\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc774 \uc561\uc138\uc11c\ub9ac\ub294 \uae30\ubcf8 \ubcf4\uc548 \uc694\uad6c \uc0ac\ud56d\uc744 \ucda9\uc871\ud558\uc9c0 \ubabb\ud569\ub2c8\ub2e4.", "max_peers_error": "\uae30\uae30\uc5d0 \ube44\uc5b4\uc788\ub294 \ud398\uc5b4\ub9c1 \uc7a5\uc18c\uac00 \uc5c6\uc5b4 \ud398\uc5b4\ub9c1\uc744 \ucd94\uac00\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "pairing_failed": "\uc774 \uae30\uae30\uc640 \ud398\uc5b4\ub9c1\uc744 \uc2dc\ub3c4\ud558\ub294 \uc911 \ucc98\ub9ac\ub418\uc9c0 \uc54a\uc740 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc77c\uc2dc\uc801\uc778 \uc624\ub958\uc774\uac70\ub098 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \uae30\uae30 \uc77c \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "unable_to_pair": "\ud398\uc5b4\ub9c1 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", @@ -29,6 +30,7 @@ }, "pair": { "data": { + "allow_insecure_setup_codes": "\uc548\uc804\ud558\uc9c0 \uc54a\uc740 \uc124\uc815 \ucf54\ub4dc\uc640\uc758 \ud398\uc5b4\ub9c1\uc744 \ud5c8\uc6a9\ud569\ub2c8\ub2e4.", "pairing_code": "\ud398\uc5b4\ub9c1 \ucf54\ub4dc" }, "description": "HomeKit \ucee8\ud2b8\ub864\ub7ec\ub294 \ubcc4\ub3c4\uc758 HomeKit \ucee8\ud2b8\ub864\ub7ec \ub610\ub294 iCloud \uc5c6\uc774 \uc554\ud638\ud654\ub41c \ubcf4\uc548 \uc5f0\uacb0\uc744 \uc0ac\uc6a9\ud558\uc5ec \ub85c\uceec \uc601\uc5ed \ub124\ud2b8\uc6cc\ud06c \uc0c1\uc5d0\uc11c {name}\uacfc(\uc640) \ud1b5\uc2e0\ud569\ub2c8\ub2e4. \uc774 \uc561\uc138\uc11c\ub9ac\ub97c \uc0ac\uc6a9\ud558\ub824\uba74 HomeKit \ud398\uc5b4\ub9c1 \ucf54\ub4dc(XX-XX-XXX \ud615\uc2dd)\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. \uc774 \ucf54\ub4dc\ub294 \uc77c\ubc18\uc801\uc73c\ub85c \uae30\uae30\ub098 \ud3ec\uc7a5 \ubc15\uc2a4\uc5d0 \ud45c\uc2dc\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.", diff --git a/homeassistant/components/homekit_controller/translations/nl.json b/homeassistant/components/homekit_controller/translations/nl.json index 4312fdc033c..fd966ad43fa 100644 --- a/homeassistant/components/homekit_controller/translations/nl.json +++ b/homeassistant/components/homekit_controller/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "accessory_not_found_error": "Kan geen koppeling toevoegen omdat het apparaat niet langer kan worden gevonden.", "already_configured": "Accessoire is al geconfigureerd met deze controller.", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "already_paired": "Dit accessoire is al gekoppeld aan een ander apparaat. Reset het accessoire en probeer het opnieuw.", "ignored_model": "HomeKit-ondersteuning voor dit model is geblokkeerd omdat er een meer functie volledige native integratie beschikbaar is.", "invalid_config_entry": "Dit apparaat geeft aan dat het gereed is om te koppelen, maar er is al een conflicterend configuratie-item voor in de Home Assistant dat eerst moet worden verwijderd.", diff --git a/homeassistant/components/homematicip_cloud/translations/nl.json b/homeassistant/components/homematicip_cloud/translations/nl.json index cb65dee7bd1..d5945293089 100644 --- a/homeassistant/components/homematicip_cloud/translations/nl.json +++ b/homeassistant/components/homematicip_cloud/translations/nl.json @@ -6,7 +6,7 @@ "unknown": "Onverwachte fout" }, "error": { - "invalid_sgtin_or_pin": "Ongeldige SGTIN of PIN-code, probeer het opnieuw.", + "invalid_sgtin_or_pin": "Ongeldige SGTIN of Pincode, probeer het opnieuw.", "press_the_button": "Druk op de blauwe knop.", "register_failed": "Kan niet registreren, gelieve opnieuw te proberen.", "timeout_button": "Blauwe knop druk op timeout, probeer het opnieuw." @@ -16,7 +16,7 @@ "data": { "hapid": "Accesspoint ID (SGTIN)", "name": "Naam (optioneel, gebruikt als naamvoorvoegsel voor alle apparaten)", - "pin": "PIN-code" + "pin": "Pincode" }, "title": "Kies HomematicIP accesspoint" }, diff --git a/homeassistant/components/huawei_lte/translations/ko.json b/homeassistant/components/huawei_lte/translations/ko.json index 930431d6c87..2a29418e67d 100644 --- a/homeassistant/components/huawei_lte/translations/ko.json +++ b/homeassistant/components/huawei_lte/translations/ko.json @@ -31,7 +31,8 @@ "init": { "data": { "name": "\uc54c\ub9bc \uc11c\ube44\uc2a4 \uc774\ub984 (\ubcc0\uacbd \uc2dc \ub2e4\uc2dc \uc2dc\uc791\ud574\uc57c \ud568)", - "recipient": "SMS \uc54c\ub9bc \uc218\uc2e0\uc790" + "recipient": "SMS \uc54c\ub9bc \uc218\uc2e0\uc790", + "track_wired_clients": "\uc720\uc120 \ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ucd94\uc801" } } } diff --git a/homeassistant/components/hue/translations/nl.json b/homeassistant/components/hue/translations/nl.json index 56bac6b89d8..57c52c4156f 100644 --- a/homeassistant/components/hue/translations/nl.json +++ b/homeassistant/components/hue/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "all_configured": "Alle Philips Hue bridges zijn al geconfigureerd", "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "cannot_connect": "Kan geen verbinding maken", "discover_timeout": "Hue bridges kunnen niet worden gevonden", "invalid_host": "Ongeldige host", @@ -70,7 +70,7 @@ "data": { "allow_hue_groups": "Sta Hue-groepen toe", "allow_hue_scenes": "Sta Hue sc\u00e8nes toe", - "allow_unreachable": "Onbereikbare lampen toestaan hun status correct te melden", + "allow_unreachable": "Onbereikbare lampen toestaan hun status te melden", "ignore_availability": "Verbindingsstatus negeren voor de opgegeven apparaten" } } diff --git a/homeassistant/components/hunterdouglas_powerview/translations/nl.json b/homeassistant/components/hunterdouglas_powerview/translations/nl.json index 588fa21c813..d8d305383f8 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/nl.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Verbinding mislukt, probeer het opnieuw", + "cannot_connect": "Kan geen verbinding maken", "unknown": "Onverwachte fout" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/hvv_departures/translations/nl.json b/homeassistant/components/hvv_departures/translations/nl.json index 8782499ee05..aab03a0e576 100644 --- a/homeassistant/components/hvv_departures/translations/nl.json +++ b/homeassistant/components/hvv_departures/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "no_results": "Geen resultaten. Probeer het met een ander station/adres" }, diff --git a/homeassistant/components/hyperion/translations/nl.json b/homeassistant/components/hyperion/translations/nl.json index 992d705a533..ff2cd5f4f6b 100644 --- a/homeassistant/components/hyperion/translations/nl.json +++ b/homeassistant/components/hyperion/translations/nl.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "Service is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "auth_new_token_not_granted_error": "Nieuw aangemaakte token is niet goedgekeurd in Hyperion UI", "auth_new_token_not_work_error": "Verificatie met nieuw aangemaakt token mislukt", "auth_required_error": "Kan niet bepalen of autorisatie vereist is", "cannot_connect": "Kan geen verbinding maken", "no_id": "De Hyperion Ambilight instantie heeft zijn id niet gerapporteerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/icloud/translations/nl.json b/homeassistant/components/icloud/translations/nl.json index 7260f954c38..b93ebab4665 100644 --- a/homeassistant/components/icloud/translations/nl.json +++ b/homeassistant/components/icloud/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Account is al geconfigureerd", "no_device": "Op geen van uw apparaten is \"Find my iPhone\" geactiveerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "invalid_auth": "Ongeldige authenticatie", @@ -16,7 +16,7 @@ "password": "Wachtwoord" }, "description": "Uw eerder ingevoerde wachtwoord voor {username} werkt niet meer. Update uw wachtwoord om deze integratie te blijven gebruiken.", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" }, "trusted_device": { "data": { diff --git a/homeassistant/components/ifttt/translations/nl.json b/homeassistant/components/ifttt/translations/nl.json index 727b21f43be..93cdd225b98 100644 --- a/homeassistant/components/ifttt/translations/nl.json +++ b/homeassistant/components/ifttt/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "cloud_not_connected": "Niet verbonden met Home Assistant Cloud.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", - "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." + "webhook_not_internet_accessible": "Je Home Assistant-instantie moet toegankelijk zijn vanaf internet om webhook-berichten te ontvangen." }, "create_entry": { "default": "Om evenementen naar de Home Assistant te verzenden, moet u de actie \"Een webverzoek doen\" gebruiken vanuit de [IFTTT Webhook-applet]({applet_url}). \n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Method: POST \n - Content Type: application/json \n\nZie [the documentation]({docs_url}) voor informatie over het configureren van automatiseringen om inkomende gegevens te verwerken." diff --git a/homeassistant/components/insteon/translations/ko.json b/homeassistant/components/insteon/translations/ko.json index d1a4624f7bf..927839a6080 100644 --- a/homeassistant/components/insteon/translations/ko.json +++ b/homeassistant/components/insteon/translations/ko.json @@ -2,6 +2,7 @@ "config": { "abort": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "not_insteon_device": "\ubc1c\uacac\ub41c \uc7a5\uce58\uac00 Insteon \uc7a5\uce58\uac00 \uc544\ub2d9\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": { diff --git a/homeassistant/components/insteon/translations/nl.json b/homeassistant/components/insteon/translations/nl.json index 59e799a084c..2683342ad02 100644 --- a/homeassistant/components/insteon/translations/nl.json +++ b/homeassistant/components/insteon/translations/nl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "not_insteon_device": "Ontdekt apparaat is geen Insteon apparaat", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "select_single": "Selecteer een optie." }, "flow_title": "{name}", @@ -49,7 +49,7 @@ }, "options": { "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "input_error": "Ongeldige invoer, controleer uw waarden.", "select_single": "Selecteer \u00e9\u00e9n optie." }, diff --git a/homeassistant/components/intellifire/translations/nl.json b/homeassistant/components/intellifire/translations/nl.json index 7356c4a0d50..2af11edede8 100644 --- a/homeassistant/components/intellifire/translations/nl.json +++ b/homeassistant/components/intellifire/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "not_intellifire_device": "Niet een IntelliFire apparaat.", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "api_error": "Inloggen mislukt", diff --git a/homeassistant/components/ios/translations/nl.json b/homeassistant/components/ios/translations/nl.json index 1e660ec2f5d..2e50721297a 100644 --- a/homeassistant/components/ios/translations/nl.json +++ b/homeassistant/components/ios/translations/nl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "confirm": { - "description": "Wilt u beginnen met instellen?" + "description": "Wil je beginnen met instellen?" } } } diff --git a/homeassistant/components/iotawatt/translations/nl.json b/homeassistant/components/iotawatt/translations/nl.json index 617073e91c0..04c1bb9a262 100644 --- a/homeassistant/components/iotawatt/translations/nl.json +++ b/homeassistant/components/iotawatt/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/ipma/translations/nl.json b/homeassistant/components/ipma/translations/nl.json index 154aafe1033..ddd00b40e51 100644 --- a/homeassistant/components/ipma/translations/nl.json +++ b/homeassistant/components/ipma/translations/nl.json @@ -8,7 +8,7 @@ "data": { "latitude": "Breedtegraad", "longitude": "Lengtegraad", - "mode": "Mode", + "mode": "Modus", "name": "Naam" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", diff --git a/homeassistant/components/ipp/translations/nl.json b/homeassistant/components/ipp/translations/nl.json index 5296fcac36f..2a97724a596 100644 --- a/homeassistant/components/ipp/translations/nl.json +++ b/homeassistant/components/ipp/translations/nl.json @@ -20,7 +20,7 @@ "base_path": "Relatief pad naar de printer", "host": "Host", "port": "Poort", - "ssl": "Printer ondersteunt communicatie via SSL / TLS", + "ssl": "Maakt gebruik van een SSL-certificaat", "verify_ssl": "SSL-certificaat verifi\u00ebren" }, "description": "Stel uw printer in via Internet Printing Protocol (IPP) om te integreren met Home Assistant.", diff --git a/homeassistant/components/iss/translations/nl.json b/homeassistant/components/iss/translations/nl.json index cbe31ac6674..9e8d1f6bf95 100644 --- a/homeassistant/components/iss/translations/nl.json +++ b/homeassistant/components/iss/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "latitude_longitude_not_defined": "Breedte- en lengtegraad zijn niet gedefinieerd in Home Assistant.", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "user": { diff --git a/homeassistant/components/isy994/translations/nl.json b/homeassistant/components/isy994/translations/nl.json index 580bd307a5c..261be2929fc 100644 --- a/homeassistant/components/isy994/translations/nl.json +++ b/homeassistant/components/isy994/translations/nl.json @@ -4,10 +4,10 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "invalid_host": "De hostvermelding had de niet volledig URL-indeling, bijvoorbeeld http://192.168.10.100:80", - "reauth_successful": "Herauthenticatie was succesvol", + "reauth_successful": "Herauthenticatie geslaagd", "unknown": "Onverwachte fout" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/izone/translations/nl.json b/homeassistant/components/izone/translations/nl.json index b70bb738df0..707f6fb279d 100644 --- a/homeassistant/components/izone/translations/nl.json +++ b/homeassistant/components/izone/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Geen apparaten gevonden op het netwerk", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "confirm": { diff --git a/homeassistant/components/jellyfin/translations/nl.json b/homeassistant/components/jellyfin/translations/nl.json index 1072cfff418..947f89dcb81 100644 --- a/homeassistant/components/jellyfin/translations/nl.json +++ b/homeassistant/components/jellyfin/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/juicenet/translations/nl.json b/homeassistant/components/juicenet/translations/nl.json index 5f1b0d37e25..907ac3a68da 100644 --- a/homeassistant/components/juicenet/translations/nl.json +++ b/homeassistant/components/juicenet/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Account is al geconfigureerd" }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/kaleidescape/translations/nl.json b/homeassistant/components/kaleidescape/translations/nl.json index b3498db12a3..50a81b361cf 100644 --- a/homeassistant/components/kaleidescape/translations/nl.json +++ b/homeassistant/components/kaleidescape/translations/nl.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "Configuratiestroom is al bezig", + "already_in_progress": "De configuratie is momenteel al bezig", "unknown": "Onverwachte fout", "unsupported": "Niet-ondersteund apparaat" }, "error": { - "cannot_connect": "Verbinding mislukt", + "cannot_connect": "Kan geen verbinding maken", "unsupported": "Niet-ondersteund apparaat" }, "flow_title": "{model} ({name})", diff --git a/homeassistant/components/keenetic_ndms2/translations/ko.json b/homeassistant/components/keenetic_ndms2/translations/ko.json index 9444b447d37..e20111fd722 100644 --- a/homeassistant/components/keenetic_ndms2/translations/ko.json +++ b/homeassistant/components/keenetic_ndms2/translations/ko.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "not_keenetic_ndms2": "\ubc1c\uacac\ub41c \uc7a5\uce58\uac00 Keenetic \ub77c\uc6b0\ud130\uac00 \uc544\ub2d9\ub2c8\ub2e4." }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" diff --git a/homeassistant/components/knx/translations/ko.json b/homeassistant/components/knx/translations/ko.json index 0762b130455..0aac9be9341 100644 --- a/homeassistant/components/knx/translations/ko.json +++ b/homeassistant/components/knx/translations/ko.json @@ -1,6 +1,11 @@ { "config": { "step": { + "secure_knxkeys": { + "data": { + "knxkeys_filename": "`.knxkeys` \ud30c\uc77c\uc758 \ud30c\uc77c \uc774\ub984(\ud655\uc7a5\uc790 \ud3ec\ud568)" + } + }, "secure_manual": { "description": "IP \ubcf4\uc548 \uc815\ubcf4\ub97c \uc785\ub825\ud558\uc138\uc694." } diff --git a/homeassistant/components/knx/translations/nl.json b/homeassistant/components/knx/translations/nl.json index 4c342d2741b..1c597507c67 100644 --- a/homeassistant/components/knx/translations/nl.json +++ b/homeassistant/components/knx/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Service is al geconfigureerd", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/knx/translations/pl.json b/homeassistant/components/knx/translations/pl.json index d972d9b7061..23185a0ae49 100644 --- a/homeassistant/components/knx/translations/pl.json +++ b/homeassistant/components/knx/translations/pl.json @@ -102,7 +102,7 @@ "multicast_group": "U\u017cywany do routingu i wykrywania. Domy\u015blnie: `224.0.23.12`", "multicast_port": "U\u017cywany do routingu i wykrywania. Domy\u015blnie: `3671`", "rate_limit": "Maksymalna liczba wychodz\u0105cych wiadomo\u015bci na sekund\u0119.\nZalecane: od 20 do 40", - "state_updater": "Globalnie w\u0142\u0105czaj lub wy\u0142\u0105czaj odczytywanie stan\u00f3w z magistrali KNX. Po wy\u0142\u0105czeniu, Home Assistant nie b\u0119dzie aktywnie pobiera\u0107 stan\u00f3w z magistrali KNX, opcje encji `sync_state` nie b\u0119d\u0105 mia\u0142y \u017cadnego efektu." + "state_updater": "Ustaw domy\u015blne odczytywanie stan\u00f3w z magistrali KNX. Po wy\u0142\u0105czeniu, Home Assistant nie b\u0119dzie aktywnie pobiera\u0107 stan\u00f3w encji z magistrali KNX. Mo\u017cna to zast\u0105pi\u0107 przez opcj\u0119 encji `sync_state`." } }, "tunnel": { diff --git a/homeassistant/components/kodi/translations/nl.json b/homeassistant/components/kodi/translations/nl.json index 879ca83051f..6f3bac1e2e0 100644 --- a/homeassistant/components/kodi/translations/nl.json +++ b/homeassistant/components/kodi/translations/nl.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "no_uuid": "Kodi-instantie heeft geen unieke ID. Dit komt waarschijnlijk door een oude Kodi-versie (17.x of lager). U kunt de integratie handmatig configureren of upgraden naar een recentere Kodi-versie.", "unknown": "Onverwachte fout" }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, @@ -29,7 +29,7 @@ "data": { "host": "Host", "port": "Poort", - "ssl": "Gebruik een SSL-certificaat" + "ssl": "Maakt gebruik van een SSL-certificaat" }, "description": "Kodi-verbindingsinformatie. Zorg ervoor dat u \"Controle van Kodi via HTTP toestaan\" in Systeem / Instellingen / Netwerk / Services inschakelt." }, diff --git a/homeassistant/components/konnected/translations/nl.json b/homeassistant/components/konnected/translations/nl.json index 1680431a35b..6e46ef3e2bc 100644 --- a/homeassistant/components/konnected/translations/nl.json +++ b/homeassistant/components/konnected/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "cannot_connect": "Kan geen verbinding maken", "not_konn_panel": "Geen herkend Konnected.io apparaat", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/kraken/translations/nl.json b/homeassistant/components/kraken/translations/nl.json index 09b93b205e3..0ecf8885785 100644 --- a/homeassistant/components/kraken/translations/nl.json +++ b/homeassistant/components/kraken/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "already_configured": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { "one": "Leeg", @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Wilt u beginnen met instellen?" + "description": "Wil je beginnen met instellen?" } } }, diff --git a/homeassistant/components/kulersky/translations/nl.json b/homeassistant/components/kulersky/translations/nl.json index 0671f0b3674..6fc4a03e824 100644 --- a/homeassistant/components/kulersky/translations/nl.json +++ b/homeassistant/components/kulersky/translations/nl.json @@ -2,11 +2,11 @@ "config": { "abort": { "no_devices_found": "Geen apparaten gevonden op het netwerk", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "confirm": { - "description": "Wilt u beginnen met instellen?" + "description": "Wil je beginnen met instellen?" } } } diff --git a/homeassistant/components/launch_library/translations/nl.json b/homeassistant/components/launch_library/translations/nl.json index 4dd9a218aa5..cc2333416d8 100644 --- a/homeassistant/components/launch_library/translations/nl.json +++ b/homeassistant/components/launch_library/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "user": { diff --git a/homeassistant/components/lifx/translations/nl.json b/homeassistant/components/lifx/translations/nl.json index 0e0a6190f0d..c8d0ca83dd8 100644 --- a/homeassistant/components/lifx/translations/nl.json +++ b/homeassistant/components/lifx/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Geen apparaten gevonden op het netwerk", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "confirm": { diff --git a/homeassistant/components/litejet/translations/nl.json b/homeassistant/components/litejet/translations/nl.json index e2743b4165f..6831d710bad 100644 --- a/homeassistant/components/litejet/translations/nl.json +++ b/homeassistant/components/litejet/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { "open_failed": "Kan de opgegeven seri\u00eble poort niet openen." diff --git a/homeassistant/components/litterrobot/translations/sensor.pl.json b/homeassistant/components/litterrobot/translations/sensor.pl.json index 5e75c2ff68a..b3063c7ca62 100644 --- a/homeassistant/components/litterrobot/translations/sensor.pl.json +++ b/homeassistant/components/litterrobot/translations/sensor.pl.json @@ -15,7 +15,7 @@ "ec": "cykl opr\u00f3\u017cniania", "hpf": "b\u0142\u0105d pozycji wyj\u015bciowej", "off": "wy\u0142.", - "offline": "Offline", + "offline": "offline", "otf": "b\u0142\u0105d nadmiernego momentu obrotowego", "p": "wstrzymany", "pd": "detektor obecno\u015bci", diff --git a/homeassistant/components/local_ip/translations/nl.json b/homeassistant/components/local_ip/translations/nl.json index 4b2672d2a3b..0d09c23ea97 100644 --- a/homeassistant/components/local_ip/translations/nl.json +++ b/homeassistant/components/local_ip/translations/nl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "user": { - "description": "Wilt u beginnen met instellen?", + "description": "Wil je beginnen met instellen?", "title": "Lokaal IP-adres" } } diff --git a/homeassistant/components/locative/translations/nl.json b/homeassistant/components/locative/translations/nl.json index 0a459e566c5..11ccfc059bc 100644 --- a/homeassistant/components/locative/translations/nl.json +++ b/homeassistant/components/locative/translations/nl.json @@ -3,14 +3,14 @@ "abort": { "cloud_not_connected": "Niet verbonden met Home Assistant Cloud.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", - "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." + "webhook_not_internet_accessible": "Je Home Assistant-instantie moet toegankelijk zijn vanaf internet om webhook-berichten te ontvangen." }, "create_entry": { "default": "Om locaties naar Home Assistant te sturen, moet u de Webhook-functie instellen in de Locative app. \n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Methode: POST \n\n Zie [de documentatie]({docs_url}) voor meer informatie." }, "step": { "user": { - "description": "Wilt u beginnen met instellen?", + "description": "Wil je beginnen met instellen?", "title": "Stel de Locative Webhook in" } } diff --git a/homeassistant/components/logi_circle/translations/nl.json b/homeassistant/components/logi_circle/translations/nl.json index 96231086830..fcf867036db 100644 --- a/homeassistant/components/logi_circle/translations/nl.json +++ b/homeassistant/components/logi_circle/translations/nl.json @@ -4,10 +4,10 @@ "already_configured": "Account is al geconfigureerd", "external_error": "Uitzondering opgetreden uit een andere stroom.", "external_setup": "Logi Circle is met succes geconfigureerd vanuit een andere stroom.", - "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen." + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie." }, "error": { - "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", "follow_link": "Volg de link en authenticeer voordat u op Verzenden drukt.", "invalid_auth": "Ongeldige authenticatie" }, diff --git a/homeassistant/components/lookin/translations/nl.json b/homeassistant/components/lookin/translations/nl.json index 89f94de97ca..67ccb5da847 100644 --- a/homeassistant/components/lookin/translations/nl.json +++ b/homeassistant/components/lookin/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "cannot_connect": "Kan geen verbinding maken", "no_devices_found": "Geen apparaten gevonden op het netwerk" }, diff --git a/homeassistant/components/lutron_caseta/translations/nl.json b/homeassistant/components/lutron_caseta/translations/nl.json index c2a342123d0..0d1063eee8c 100644 --- a/homeassistant/components/lutron_caseta/translations/nl.json +++ b/homeassistant/components/lutron_caseta/translations/nl.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "not_lutron_device": "Ontdekt apparaat is geen Lutron-apparaat" }, "error": { - "cannot_connect": "Kon niet verbinden" + "cannot_connect": "Kan geen verbinding maken" }, "flow_title": "{name} ({host})", "step": { diff --git a/homeassistant/components/lyric/translations/ko.json b/homeassistant/components/lyric/translations/ko.json index 37093d340df..9a4b1cce222 100644 --- a/homeassistant/components/lyric/translations/ko.json +++ b/homeassistant/components/lyric/translations/ko.json @@ -13,6 +13,7 @@ "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" }, "reauth_confirm": { + "description": "Lyric \ud1b5\ud569\uad6c\uc131\uc694\uc18c\ub294 \uacc4\uc815\uc744 \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4.", "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" } } diff --git a/homeassistant/components/lyric/translations/nl.json b/homeassistant/components/lyric/translations/nl.json index 0d1f9da12e8..a2569f18dc2 100644 --- a/homeassistant/components/lyric/translations/nl.json +++ b/homeassistant/components/lyric/translations/nl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", - "reauth_successful": "Herauthenticatie was succesvol" + "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", + "reauth_successful": "Herauthenticatie geslaagd" }, "create_entry": { - "default": "Succesvol geauthenticeerd" + "default": "Authenticatie geslaagd" }, "step": { "pick_implementation": { @@ -14,7 +14,7 @@ }, "reauth_confirm": { "description": "De Lyric-integratie moet uw account opnieuw verifi\u00ebren.", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" } } } diff --git a/homeassistant/components/mailgun/translations/nl.json b/homeassistant/components/mailgun/translations/nl.json index 5e84c62a314..309c5b502dc 100644 --- a/homeassistant/components/mailgun/translations/nl.json +++ b/homeassistant/components/mailgun/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "cloud_not_connected": "Niet verbonden met Home Assistant Cloud.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", - "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." + "webhook_not_internet_accessible": "Je Home Assistant-instantie moet toegankelijk zijn vanaf internet om webhook-berichten te ontvangen." }, "create_entry": { "default": "Om evenementen naar Home Assistant te verzenden, moet u [Webhooks with Mailgun]({mailgun_url}) instellen. \n\n Vul de volgende info in: \n\n - URL: `{webhook_url}` \n - Methode: POST \n - Inhoudstype: application/json \n\n Zie [de documentatie]({docs_url}) voor informatie over het configureren van automatiseringen om binnenkomende gegevens te verwerken." diff --git a/homeassistant/components/mazda/translations/nl.json b/homeassistant/components/mazda/translations/nl.json index f7532b9fc45..5b267b914cb 100644 --- a/homeassistant/components/mazda/translations/nl.json +++ b/homeassistant/components/mazda/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "account_locked": "Account vergrendeld. Probeer het later nog eens.", diff --git a/homeassistant/components/media_player/translations/nl.json b/homeassistant/components/media_player/translations/nl.json index 23fe64d452a..f3ad43fb5ec 100644 --- a/homeassistant/components/media_player/translations/nl.json +++ b/homeassistant/components/media_player/translations/nl.json @@ -25,7 +25,7 @@ "off": "Uit", "on": "Aan", "paused": "Gepauzeerd", - "playing": "Afspelen", + "playing": "Speelt", "standby": "Stand-by" } }, diff --git a/homeassistant/components/met/translations/ko.json b/homeassistant/components/met/translations/ko.json index 17175c196c0..b8e89380d52 100644 --- a/homeassistant/components/met/translations/ko.json +++ b/homeassistant/components/met/translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_home": "Home Assistant\uc5d0 \ud648 \uc88c\ud45c\uac00 \uc124\uc815\ub418\uc5b4 \uc788\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." + }, "error": { "already_configured": "\uc11c\ube44\uc2a4\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/meteo_france/translations/nl.json b/homeassistant/components/meteo_france/translations/nl.json index 5e36e0585c7..1272d2c11e8 100644 --- a/homeassistant/components/meteo_france/translations/nl.json +++ b/homeassistant/components/meteo_france/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd.", + "already_configured": "Locatie is al geconfigureerd", "unknown": "Onverwachte fout" }, "error": { diff --git a/homeassistant/components/metoffice/translations/nl.json b/homeassistant/components/metoffice/translations/nl.json index a6ba36f07af..b6fbbfdbba4 100644 --- a/homeassistant/components/metoffice/translations/nl.json +++ b/homeassistant/components/metoffice/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Service is al geconfigureerd" }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "unknown": "Onverwachte fout" }, "step": { diff --git a/homeassistant/components/min_max/translations/nl.json b/homeassistant/components/min_max/translations/nl.json index 75d8d69357b..eb2106cc366 100644 --- a/homeassistant/components/min_max/translations/nl.json +++ b/homeassistant/components/min_max/translations/nl.json @@ -3,7 +3,7 @@ "step": { "user": { "data": { - "entity_ids": "Entiteiten invoeren", + "entity_ids": "Invoerentiteiten", "name": "Naam", "round_digits": "Precisie", "type": "statistisch kenmerk" @@ -20,7 +20,7 @@ "step": { "init": { "data": { - "entity_ids": "Entiteiten invoeren", + "entity_ids": "Invoerentiteiten", "round_digits": "Precisie", "type": "statistisch kenmerk" }, diff --git a/homeassistant/components/modem_callerid/translations/nl.json b/homeassistant/components/modem_callerid/translations/nl.json index 6e26dd3d1c0..c5107ce6e53 100644 --- a/homeassistant/components/modem_callerid/translations/nl.json +++ b/homeassistant/components/modem_callerid/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "no_devices_found": "Geen resterende apparaten gevonden" }, "error": { diff --git a/homeassistant/components/modern_forms/translations/ko.json b/homeassistant/components/modern_forms/translations/ko.json new file mode 100644 index 00000000000..6079edcd0da --- /dev/null +++ b/homeassistant/components/modern_forms/translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8" + }, + "description": "Home Assistant\uc640 \uc5f0\uacb0\ub418\ub3c4\ub85d Modern Forms \ud32c\uc744 \uc124\uc815\ud558\uc2ed\uc2dc\uc624." + }, + "zeroconf_confirm": { + "description": "` {name} ` Modern Forms \ud32c\uc744 Home Assistant\uc5d0 \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Modern Forms \ud32c \uc7a5\uce58 \ubc1c\uacac" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/nl.json b/homeassistant/components/moon/translations/nl.json index ebcef695e04..0d626abfd7b 100644 --- a/homeassistant/components/moon/translations/nl.json +++ b/homeassistant/components/moon/translations/nl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "user": { - "description": "Wilt u beginnen met instellen?" + "description": "Wil je beginnen met instellen?" } } }, diff --git a/homeassistant/components/motion_blinds/translations/nl.json b/homeassistant/components/motion_blinds/translations/nl.json index 73522480df5..316e2077796 100644 --- a/homeassistant/components/motion_blinds/translations/nl.json +++ b/homeassistant/components/motion_blinds/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "connection_error": "Kan geen verbinding maken" }, "error": { diff --git a/homeassistant/components/motioneye/translations/nl.json b/homeassistant/components/motioneye/translations/nl.json index dce66fcb5d1..a2e053e2493 100644 --- a/homeassistant/components/motioneye/translations/nl.json +++ b/homeassistant/components/motioneye/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Service is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/mqtt/translations/ko.json b/homeassistant/components/mqtt/translations/ko.json index dccd49b2ef3..454b1e0368f 100644 --- a/homeassistant/components/mqtt/translations/ko.json +++ b/homeassistant/components/mqtt/translations/ko.json @@ -62,7 +62,8 @@ "port": "\ud3ec\ud2b8", "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, - "description": "MQTT \ube0c\ub85c\ucee4\uc640\uc758 \uc5f0\uacb0 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." + "description": "MQTT \ube0c\ub85c\ucee4\uc640\uc758 \uc5f0\uacb0 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "\ube0c\ub85c\ucee4 \uc635\uc158" }, "options": { "data": { @@ -78,7 +79,8 @@ "will_retain": "Will \uba54\uc2dc\uc9c0 \ub9ac\ud14c\uc778", "will_topic": "Will \uba54\uc2dc\uc9c0 \ud1a0\ud53d" }, - "description": "MQTT \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694." + "description": "MQTT \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694.", + "title": "MQTT \uc635\uc158" } } } diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index 40448942a9f..d948cc43df7 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Dienst is al geconfigureerd", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "already_configured": "Service is al geconfigureerd", + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { "cannot_connect": "Kan geen verbinding maken" @@ -53,7 +53,7 @@ "error": { "bad_birth": "Ongeldig birth topic", "bad_will": "Ongeldig will topic", - "cannot_connect": "Kon niet verbinden" + "cannot_connect": "Kan geen verbinding maken" }, "step": { "broker": { diff --git a/homeassistant/components/myq/translations/ko.json b/homeassistant/components/myq/translations/ko.json index 23ba2eecea7..1218ce0e4f4 100644 --- a/homeassistant/components/myq/translations/ko.json +++ b/homeassistant/components/myq/translations/ko.json @@ -9,6 +9,10 @@ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "step": { + "reauth_confirm": { + "description": "{username} \uc758 \ube44\ubc00\ubc88\ud638\uac00 \ub354 \uc774\uc0c1 \uc720\ud6a8\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "title": "MyQ \uacc4\uc815 \uc7ac\uc778\uc99d" + }, "user": { "data": { "password": "\ube44\ubc00\ubc88\ud638", diff --git a/homeassistant/components/myq/translations/nl.json b/homeassistant/components/myq/translations/nl.json index 09a36665414..8a3020679e9 100644 --- a/homeassistant/components/myq/translations/nl.json +++ b/homeassistant/components/myq/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Service is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/mysensors/translations/ko.json b/homeassistant/components/mysensors/translations/ko.json index 7a6f3e856a7..4d5be0f44e3 100644 --- a/homeassistant/components/mysensors/translations/ko.json +++ b/homeassistant/components/mysensors/translations/ko.json @@ -33,6 +33,7 @@ "invalid_serial": "\uc2dc\ub9ac\uc5bc \ud3ec\ud2b8\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_subscribe_topic": "\uad6c\ub3c5 \ud1a0\ud53d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_version": "MySensors \ubc84\uc804\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "mqtt_required": "MQTT \ud1b5\ud569\uad6c\uc131\uc694\uc18c\uac00 \uc124\uc815\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", "not_a_number": "\uc22b\uc790\ub85c \uc785\ub825\ud574\uc8fc\uc138\uc694", "port_out_of_range": "\ud3ec\ud2b8 \ubc88\ud638\ub294 1 \uc774\uc0c1 65535 \uc774\ud558\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4", "same_topic": "\uad6c\ub3c5 \ubc0f \ubc1c\ud589 \ud1a0\ud53d\uc740 \ub3d9\uc77c\ud569\ub2c8\ub2e4", diff --git a/homeassistant/components/nam/translations/ko.json b/homeassistant/components/nam/translations/ko.json index a4cc4c01810..8112e2968c5 100644 --- a/homeassistant/components/nam/translations/ko.json +++ b/homeassistant/components/nam/translations/ko.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "reauth_unsuccessful": "\uc7ac\uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4. \uad6c\uc131\uc744 \uc81c\uac70\ud558\uace0 \ub2e4\uc2dc \uc124\uc815\ud558\uc2ed\uc2dc\uc624." }, "step": { diff --git a/homeassistant/components/nam/translations/nl.json b/homeassistant/components/nam/translations/nl.json index f900dccc295..a3d7925479f 100644 --- a/homeassistant/components/nam/translations/nl.json +++ b/homeassistant/components/nam/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "device_unsupported": "Het apparaat wordt niet ondersteund.", - "reauth_successful": "Herauthenticatie was succesvol", + "reauth_successful": "Herauthenticatie geslaagd", "reauth_unsuccessful": "Herauthenticatie is mislukt, verwijder de integratie en stel het opnieuw in." }, "error": { diff --git a/homeassistant/components/nanoleaf/translations/nl.json b/homeassistant/components/nanoleaf/translations/nl.json index 76c471769e3..292eb066bf3 100644 --- a/homeassistant/components/nanoleaf/translations/nl.json +++ b/homeassistant/components/nanoleaf/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken", "invalid_token": "Ongeldig toegangstoken", - "reauth_successful": "Herauthenticatie was succesvol", + "reauth_successful": "Herauthenticatie geslaagd", "unknown": "Onverwachte fout" }, "error": { diff --git a/homeassistant/components/neato/translations/nl.json b/homeassistant/components/neato/translations/nl.json index 0bf8009b88e..53cf562a748 100644 --- a/homeassistant/components/neato/translations/nl.json +++ b/homeassistant/components/neato/translations/nl.json @@ -2,20 +2,20 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", - "reauth_successful": "Herauthenticatie was succesvol" + "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})", + "reauth_successful": "Herauthenticatie geslaagd" }, "create_entry": { - "default": "Succesvol geauthenticeerd" + "default": "Authenticatie geslaagd" }, "step": { "pick_implementation": { "title": "Kies een authenticatie methode" }, "reauth_confirm": { - "title": "Wilt u beginnen met instellen?" + "title": "Wil je beginnen met instellen?" } } } diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index 0e2984972af..2b486ce79e2 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -1,21 +1,21 @@ { "config": { "abort": { - "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", "invalid_access_token": "Ongeldig toegangstoken", - "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", - "reauth_successful": "Herauthenticatie was succesvol", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})", + "reauth_successful": "Herauthenticatie geslaagd", + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL." }, "create_entry": { - "default": "Succesvol geauthenticeerd" + "default": "Authenticatie geslaagd" }, "error": { "bad_project_id": "Voer een geldige Cloud Project ID in (controleer Cloud Console)", "internal_error": "Interne foutvalidatiecode", - "invalid_pin": "Ongeldige PIN-code", + "invalid_pin": "Ongeldige Pincode", "subscriber_error": "Onbekende abonneefout, zie logs", "timeout": "Time-out validatie van code", "unknown": "Onverwachte fout", @@ -38,7 +38,7 @@ }, "link": { "data": { - "code": "PIN-code" + "code": "Pincode" }, "description": "Als je je Nest-account wilt koppelen, [autoriseer je account] ( {url} ). \n\nNa autorisatie, kopieer en plak de voorziene pincode hieronder.", "title": "Koppel Nest-account" @@ -55,7 +55,7 @@ }, "reauth_confirm": { "description": "De Nest-integratie moet uw account opnieuw verifi\u00ebren", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" } } }, diff --git a/homeassistant/components/netatmo/translations/nl.json b/homeassistant/components/netatmo/translations/nl.json index a5ddba5772c..e0b8998d652 100644 --- a/homeassistant/components/netatmo/translations/nl.json +++ b/homeassistant/components/netatmo/translations/nl.json @@ -1,22 +1,22 @@ { "config": { "abort": { - "authorize_url_timeout": "Time-out genereren autorisatie-URL.", - "missing_configuration": "Het component is niet geconfigureerd. Volg de documentatie.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", - "reauth_successful": "Herauthenticatie was succesvol", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})", + "reauth_successful": "Herauthenticatie geslaagd", + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "create_entry": { - "default": "Succesvol geauthenticeerd" + "default": "Authenticatie geslaagd" }, "step": { "pick_implementation": { - "title": "Kies de verificatiemethode" + "title": "Kies een authenticatie methode" }, "reauth_confirm": { "description": "De Netatmo-integratie moet uw account opnieuw verifi\u00ebren", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" } } }, diff --git a/homeassistant/components/netgear/translations/nl.json b/homeassistant/components/netgear/translations/nl.json index 95e824ef591..8a613738ffd 100644 --- a/homeassistant/components/netgear/translations/nl.json +++ b/homeassistant/components/netgear/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Apparaat is al ingesteld" + "already_configured": "Apparaat is al geconfigureerd" }, "error": { "config": "Verbindings- of inlogfout; controleer uw configuratie" diff --git a/homeassistant/components/nina/translations/nl.json b/homeassistant/components/nina/translations/nl.json index 3531cd8bb72..4b0100f9f07 100644 --- a/homeassistant/components/nina/translations/nl.json +++ b/homeassistant/components/nina/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/nmap_tracker/translations/nl.json b/homeassistant/components/nmap_tracker/translations/nl.json index 2331d7fda9f..6221162035a 100644 --- a/homeassistant/components/nmap_tracker/translations/nl.json +++ b/homeassistant/components/nmap_tracker/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd." + "already_configured": "Locatie is al geconfigureerd" }, "error": { "invalid_hosts": "Ongeldige hosts" diff --git a/homeassistant/components/notion/translations/nl.json b/homeassistant/components/notion/translations/nl.json index 3a86ffd2264..130d45635b8 100644 --- a/homeassistant/components/notion/translations/nl.json +++ b/homeassistant/components/notion/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "invalid_auth": "Ongeldige authenticatie", @@ -14,7 +14,7 @@ "password": "Wachtwoord" }, "description": "Voer het wachtwoord voor {username} opnieuw in.", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" }, "user": { "data": { diff --git a/homeassistant/components/nuki/translations/ko.json b/homeassistant/components/nuki/translations/ko.json index 3015596e7d4..75e9ab53e84 100644 --- a/homeassistant/components/nuki/translations/ko.json +++ b/homeassistant/components/nuki/translations/ko.json @@ -13,6 +13,7 @@ "data": { "token": "\uc561\uc138\uc2a4 \ud1a0\ud070" }, + "description": "Nuki \ud1b5\ud569\uad6c\uc131\uc694\uc18c\ub294 \ube0c\ub9ac\uc9c0\ub85c \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4.", "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" }, "user": { diff --git a/homeassistant/components/nuki/translations/nl.json b/homeassistant/components/nuki/translations/nl.json index 4157d21ffd3..b5c41a05421 100644 --- a/homeassistant/components/nuki/translations/nl.json +++ b/homeassistant/components/nuki/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -14,7 +14,7 @@ "token": "Toegangstoken" }, "description": "De Nuki integratie moet opnieuw authenticeren met uw bridge.", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" }, "user": { "data": { diff --git a/homeassistant/components/nzbget/translations/nl.json b/homeassistant/components/nzbget/translations/nl.json index 5f98d7435ec..80c96a509f2 100644 --- a/homeassistant/components/nzbget/translations/nl.json +++ b/homeassistant/components/nzbget/translations/nl.json @@ -5,7 +5,7 @@ "unknown": "Onverwachte fout" }, "error": { - "cannot_connect": "Kon niet verbinden" + "cannot_connect": "Kan geen verbinding maken" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/omnilogic/translations/nl.json b/homeassistant/components/omnilogic/translations/nl.json index b94599f93f4..ca6bd42b2b3 100644 --- a/homeassistant/components/omnilogic/translations/nl.json +++ b/homeassistant/components/omnilogic/translations/nl.json @@ -5,14 +5,14 @@ }, "error": { "cannot_connect": "Kan geen verbinding maken", - "invalid_auth": "Onjuiste gebruikersgegevens", + "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, "step": { "user": { "data": { "password": "Wachtwoord", - "username": "Benutzername" + "username": "Gebruikersnaam" } } } diff --git a/homeassistant/components/ondilo_ico/translations/nl.json b/homeassistant/components/ondilo_ico/translations/nl.json index 0613d559fce..bf5ad718617 100644 --- a/homeassistant/components/ondilo_ico/translations/nl.json +++ b/homeassistant/components/ondilo_ico/translations/nl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen." + "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie." }, "create_entry": { - "default": "Succesvol geauthenticeerd" + "default": "Authenticatie geslaagd" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/onvif/translations/nl.json b/homeassistant/components/onvif/translations/nl.json index f1101f759e9..f76aef12557 100644 --- a/homeassistant/components/onvif/translations/nl.json +++ b/homeassistant/components/onvif/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al begonnen", + "already_in_progress": "De configuratie is momenteel al bezig", "no_h264": "Er waren geen H264-streams beschikbaar. Controleer de profielconfiguratie op uw apparaat.", "no_mac": "Kan geen unieke ID configureren voor ONVIF-apparaat.", "onvif_error": "Fout bij het instellen van ONVIF-apparaat. Controleer de logboeken voor meer informatie." diff --git a/homeassistant/components/opentherm_gw/translations/nl.json b/homeassistant/components/opentherm_gw/translations/nl.json index fd8c97c4371..97f392075d8 100644 --- a/homeassistant/components/opentherm_gw/translations/nl.json +++ b/homeassistant/components/opentherm_gw/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "already_configured": "Gateway al geconfigureerd", + "already_configured": "Apparaat is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken", "id_exists": "Gateway id bestaat al" }, diff --git a/homeassistant/components/openuv/translations/nl.json b/homeassistant/components/openuv/translations/nl.json index bd56a1fa4e0..a85799658e3 100644 --- a/homeassistant/components/openuv/translations/nl.json +++ b/homeassistant/components/openuv/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd." + "already_configured": "Locatie is al geconfigureerd" }, "error": { "invalid_api_key": "Ongeldige API-sleutel" diff --git a/homeassistant/components/openweathermap/translations/nl.json b/homeassistant/components/openweathermap/translations/nl.json index 74ea3532765..85a685590e2 100644 --- a/homeassistant/components/openweathermap/translations/nl.json +++ b/homeassistant/components/openweathermap/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "OpenWeatherMap-integratie voor deze co\u00f6rdinaten is al geconfigureerd." + "already_configured": "Locatie is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -10,11 +10,11 @@ "step": { "user": { "data": { - "api_key": "OpenWeatherMap API-sleutel", + "api_key": "API-sleutel", "language": "Taal", "latitude": "Breedtegraad", "longitude": "Lengtegraad", - "mode": "Mode", + "mode": "Modus", "name": "Naam" }, "description": "Om een API sleutel te genereren ga naar https://openweathermap.org/appid" @@ -26,7 +26,7 @@ "init": { "data": { "language": "Taal", - "mode": "Mode" + "mode": "Modus" } } } diff --git a/homeassistant/components/overkiz/translations/nl.json b/homeassistant/components/overkiz/translations/nl.json index 5057f12afa6..957ca0862fc 100644 --- a/homeassistant/components/overkiz/translations/nl.json +++ b/homeassistant/components/overkiz/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol", + "reauth_successful": "Herauthenticatie geslaagd", "reauth_wrong_account": "U kunt deze invoer alleen opnieuw verifi\u00ebren met hetzelfde Overkiz account en hub" }, "error": { diff --git a/homeassistant/components/overkiz/translations/select.ko.json b/homeassistant/components/overkiz/translations/select.ko.json new file mode 100644 index 00000000000..c2af52fab9c --- /dev/null +++ b/homeassistant/components/overkiz/translations/select.ko.json @@ -0,0 +1,7 @@ +{ + "state": { + "overkiz__open_closed_pedestrian": { + "closed": "\ub2eb\ud798" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/nl.json b/homeassistant/components/panasonic_viera/translations/nl.json index fa63892730e..7e778c1e932 100644 --- a/homeassistant/components/panasonic_viera/translations/nl.json +++ b/homeassistant/components/panasonic_viera/translations/nl.json @@ -7,14 +7,14 @@ }, "error": { "cannot_connect": "Kan geen verbinding maken", - "invalid_pin_code": "De PIN-code die u hebt ingevoerd is ongeldig" + "invalid_pin_code": "De Pincode die u hebt ingevoerd is ongeldig" }, "step": { "pairing": { "data": { - "pin": "PIN-code" + "pin": "Pincode" }, - "description": "Voer de PIN-code in die op uw TV wordt weergegeven", + "description": "Voer de Pincode in die op uw TV wordt weergegeven", "title": "Koppelen" }, "user": { diff --git a/homeassistant/components/philips_js/translations/nl.json b/homeassistant/components/philips_js/translations/nl.json index 0b172e24f56..09f6b3b8f3d 100644 --- a/homeassistant/components/philips_js/translations/nl.json +++ b/homeassistant/components/philips_js/translations/nl.json @@ -12,7 +12,7 @@ "step": { "pair": { "data": { - "pin": "PIN-code" + "pin": "Pincode" }, "description": "Voer de pincode in die op uw tv wordt weergegeven", "title": "Koppel" diff --git a/homeassistant/components/pi_hole/translations/nl.json b/homeassistant/components/pi_hole/translations/nl.json index 156a248a80b..4c3919506ef 100644 --- a/homeassistant/components/pi_hole/translations/nl.json +++ b/homeassistant/components/pi_hole/translations/nl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Service al geconfigureerd" + "already_configured": "Service is al geconfigureerd" }, "error": { - "cannot_connect": "Kon niet verbinden" + "cannot_connect": "Kan geen verbinding maken" }, "step": { "api_key": { diff --git a/homeassistant/components/picnic/translations/nl.json b/homeassistant/components/picnic/translations/nl.json index 1d007e5eb43..7f7a2199d27 100644 --- a/homeassistant/components/picnic/translations/nl.json +++ b/homeassistant/components/picnic/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/plaato/translations/nl.json b/homeassistant/components/plaato/translations/nl.json index 83e2874ed73..938402d8ea9 100644 --- a/homeassistant/components/plaato/translations/nl.json +++ b/homeassistant/components/plaato/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Account is al geconfigureerd", "cloud_not_connected": "Niet verbonden met Home Assistant Cloud.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", - "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." + "webhook_not_internet_accessible": "Je Home Assistant-instantie moet toegankelijk zijn vanaf internet om webhook-berichten te ontvangen." }, "create_entry": { "default": "Uw Plaato {device_type} met naam **{device_name}** is succesvol ingesteld!" @@ -28,7 +28,7 @@ "device_name": "Geef uw apparaat een naam", "device_type": "Type Plaato-apparaat" }, - "description": "Wilt u beginnen met instellen?", + "description": "Wil je beginnen met instellen?", "title": "Stel de Plaato-apparaten in" }, "webhook": { diff --git a/homeassistant/components/plex/translations/nl.json b/homeassistant/components/plex/translations/nl.json index afdc67007bd..4d9f30056bd 100644 --- a/homeassistant/components/plex/translations/nl.json +++ b/homeassistant/components/plex/translations/nl.json @@ -3,8 +3,8 @@ "abort": { "all_configured": "Alle gekoppelde servers zijn al geconfigureerd", "already_configured": "Deze Plex-server is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", - "reauth_successful": "Herauthenticatie was succesvol", + "already_in_progress": "De configuratie is momenteel al bezig", + "reauth_successful": "Herauthenticatie geslaagd", "token_request_timeout": "Time-out verkrijgen van token", "unknown": "Onverwachte fout" }, @@ -23,7 +23,7 @@ "port": "Poort", "ssl": "Maakt gebruik van een SSL-certificaat", "token": "Token (optioneel)", - "verify_ssl": "Controleer het SSL-certificaat" + "verify_ssl": "SSL-certificaat verifi\u00ebren" }, "title": "Handmatige Plex-configuratie" }, diff --git a/homeassistant/components/plugwise/translations/nl.json b/homeassistant/components/plugwise/translations/nl.json index d295695016f..4dfd7d3b4e4 100644 --- a/homeassistant/components/plugwise/translations/nl.json +++ b/homeassistant/components/plugwise/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "De service is al geconfigureerd" + "already_configured": "Service is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -23,7 +23,7 @@ "host": "IP-adres", "password": "Smile-ID", "port": "Poort", - "username": "Smile Gebruikersnaam" + "username": "Smile gebruikersnaam" }, "description": "Voer in", "title": "Maak verbinding met de Smile" diff --git a/homeassistant/components/plum_lightpad/translations/nl.json b/homeassistant/components/plum_lightpad/translations/nl.json index 8410cabbbb9..6009e1fb384 100644 --- a/homeassistant/components/plum_lightpad/translations/nl.json +++ b/homeassistant/components/plum_lightpad/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Account is al geconfigureerd" }, "error": { - "cannot_connect": "Kon niet verbinden" + "cannot_connect": "Kan geen verbinding maken" }, "step": { "user": { diff --git a/homeassistant/components/point/translations/nl.json b/homeassistant/components/point/translations/nl.json index f0ab4d8696e..9a0f0c940a8 100644 --- a/homeassistant/components/point/translations/nl.json +++ b/homeassistant/components/point/translations/nl.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_setup": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", - "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "already_setup": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", + "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", "external_setup": "Punt succesvol geconfigureerd vanuit een andere stroom.", - "no_flows": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", + "no_flows": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL." }, "create_entry": { - "default": "Succesvol geauthenticeerd" + "default": "Authenticatie geslaagd" }, "error": { "follow_link": "Volg de link en verifieer voordat je op Verzenden klikt", @@ -23,7 +23,7 @@ "data": { "flow_impl": "Leverancier" }, - "description": "Wilt u beginnen met instellen?", + "description": "Wil je beginnen met instellen?", "title": "Kies een authenticatie methode" } } diff --git a/homeassistant/components/powerwall/translations/nl.json b/homeassistant/components/powerwall/translations/nl.json index 007d4e9e86b..45172d177b9 100644 --- a/homeassistant/components/powerwall/translations/nl.json +++ b/homeassistant/components/powerwall/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/profiler/translations/nl.json b/homeassistant/components/profiler/translations/nl.json index 8b99a128bd3..8690611b1c9 100644 --- a/homeassistant/components/profiler/translations/nl.json +++ b/homeassistant/components/profiler/translations/nl.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Wilt u beginnen met instellen?" + "description": "Wil je beginnen met instellen?" } } } diff --git a/homeassistant/components/prosegur/translations/nl.json b/homeassistant/components/prosegur/translations/nl.json index d87556b0742..db84f7df172 100644 --- a/homeassistant/components/prosegur/translations/nl.json +++ b/homeassistant/components/prosegur/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/ps4/translations/nl.json b/homeassistant/components/ps4/translations/nl.json index 8bebf95d0f0..c6e16281c19 100644 --- a/homeassistant/components/ps4/translations/nl.json +++ b/homeassistant/components/ps4/translations/nl.json @@ -10,7 +10,7 @@ "error": { "cannot_connect": "Kan geen verbinding maken", "credential_timeout": "Time-out van inlog service. Druk op Submit om opnieuw te starten.", - "login_failed": "Kan Playstation 4 niet koppelen. Controleer of de PIN-code correct is.", + "login_failed": "Kan Playstation 4 niet koppelen. Controleer of de Pincode correct is.", "no_ipaddress": "Voer het IP-adres in van de PlayStation 4 die u wilt configureren." }, "step": { @@ -19,7 +19,7 @@ }, "link": { "data": { - "code": "PIN-code", + "code": "Pincode", "ip_address": "IP-adres", "name": "Naam", "region": "Regio" diff --git a/homeassistant/components/pure_energie/translations/ko.json b/homeassistant/components/pure_energie/translations/ko.json new file mode 100644 index 00000000000..b4373b03a25 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/ko.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "zeroconf_confirm": { + "title": "Pure Energie Meter \uae30\uae30 \ubc1c\uacac" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvoutput/translations/nl.json b/homeassistant/components/pvoutput/translations/nl.json index 1619f899b73..e991fe84e32 100644 --- a/homeassistant/components/pvoutput/translations/nl.json +++ b/homeassistant/components/pvoutput/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -10,7 +10,7 @@ "step": { "reauth_confirm": { "data": { - "api_key": "API sleutel" + "api_key": "API-sleutel" }, "description": "Om opnieuw te verifi\u00ebren met PVOutput, moet u de API-sleutel op {account_url} ophalen." }, diff --git a/homeassistant/components/radio_browser/translations/nl.json b/homeassistant/components/radio_browser/translations/nl.json index d8f46a3130b..a4e4d7a6a33 100644 --- a/homeassistant/components/radio_browser/translations/nl.json +++ b/homeassistant/components/radio_browser/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "user": { diff --git a/homeassistant/components/rainmachine/translations/ko.json b/homeassistant/components/rainmachine/translations/ko.json index 0e38f4c4dfa..220cb38d8c5 100644 --- a/homeassistant/components/rainmachine/translations/ko.json +++ b/homeassistant/components/rainmachine/translations/ko.json @@ -6,6 +6,7 @@ "error": { "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/renault/translations/nl.json b/homeassistant/components/renault/translations/nl.json index c2e02b03166..212dd860874 100644 --- a/homeassistant/components/renault/translations/nl.json +++ b/homeassistant/components/renault/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Account is al geconfigureerd", "kamereon_no_account": "Kan Kamereon-account niet vinden.", - "reauth_successful": "Opnieuw verifi\u00ebren is gelukt" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "invalid_credentials": "Ongeldige authenticatie" @@ -20,7 +20,7 @@ "password": "Wachtwoord" }, "description": "Werk uw wachtwoord voor {gebruikersnaam} bij", - "title": "Integratie opnieuw verifi\u00ebren" + "title": "Integratie herauthentiseren" }, "user": { "data": { diff --git a/homeassistant/components/rfxtrx/translations/nl.json b/homeassistant/components/rfxtrx/translations/nl.json index 5a0ced3f52d..ee5e8b4a463 100644 --- a/homeassistant/components/rfxtrx/translations/nl.json +++ b/homeassistant/components/rfxtrx/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", - "cannot_connect": "Kon niet verbinden" + "cannot_connect": "Kan geen verbinding maken" }, "error": { "cannot_connect": "Kan geen verbinding maken" diff --git a/homeassistant/components/ridwell/translations/nl.json b/homeassistant/components/ridwell/translations/nl.json index afec8a578f5..e230ffec6f8 100644 --- a/homeassistant/components/ridwell/translations/nl.json +++ b/homeassistant/components/ridwell/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "invalid_auth": "Ongeldige authenticatie", @@ -14,7 +14,7 @@ "password": "Wachtwoord" }, "description": "Voer het wachtwoord voor {username} opnieuw in:", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" }, "user": { "data": { diff --git a/homeassistant/components/risco/translations/nl.json b/homeassistant/components/risco/translations/nl.json index fdf1ee5ad23..acab8c172c3 100644 --- a/homeassistant/components/risco/translations/nl.json +++ b/homeassistant/components/risco/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, @@ -12,7 +12,7 @@ "user": { "data": { "password": "Wachtwoord", - "pin": "PIN-code", + "pin": "Pincode", "username": "Gebruikersnaam" } } @@ -32,8 +32,8 @@ }, "init": { "data": { - "code_arm_required": "PIN-code vereist om in te schakelen", - "code_disarm_required": "PIN-code vereist om uit te schakelen", + "code_arm_required": "Pincode vereist om in te schakelen", + "code_disarm_required": "Pincode vereist om uit te schakelen", "scan_interval": "Hoe vaak moet Risco worden ververst (in seconden)" }, "title": "Configureer opties" diff --git a/homeassistant/components/roku/translations/nl.json b/homeassistant/components/roku/translations/nl.json index 7f413121fa7..e809c854060 100644 --- a/homeassistant/components/roku/translations/nl.json +++ b/homeassistant/components/roku/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "unknown": "Onverwachte fout" }, "error": { diff --git a/homeassistant/components/roon/translations/nl.json b/homeassistant/components/roon/translations/nl.json index fb35f22ce27..ad07fb6bc15 100644 --- a/homeassistant/components/roon/translations/nl.json +++ b/homeassistant/components/roon/translations/nl.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Apparaat is al toegevoegd" + "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "invalid_auth": "Ongeldige authencatie", + "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, "step": { diff --git a/homeassistant/components/rpi_power/translations/nl.json b/homeassistant/components/rpi_power/translations/nl.json index d9e42ef11f3..5529aa39f20 100644 --- a/homeassistant/components/rpi_power/translations/nl.json +++ b/homeassistant/components/rpi_power/translations/nl.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Wilt u beginnen met instellen?" + "description": "Wil je beginnen met instellen?" } } }, diff --git a/homeassistant/components/rtsp_to_webrtc/translations/nl.json b/homeassistant/components/rtsp_to_webrtc/translations/nl.json index 6b3344f2b6b..6454c48c22f 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/nl.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "server_failure": "RtsPtoWebRTC-server retourneerde een fout. Raadpleeg de logboeken voor meer informatie.", "server_unreachable": "Kan niet communiceren met de RTSPtoWebRTC-server. Controleer logboeken voor meer informatie.", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { "invalid_url": "Moet een geldige RTSPtoWebRTC server URL zijn, b.v. https://example.com", diff --git a/homeassistant/components/samsungtv/translations/ko.json b/homeassistant/components/samsungtv/translations/ko.json index 8b129d8ab50..5a3c64f120b 100644 --- a/homeassistant/components/samsungtv/translations/ko.json +++ b/homeassistant/components/samsungtv/translations/ko.json @@ -5,7 +5,13 @@ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", "auth_missing": "Home Assistant\uac00 \ud574\ub2f9 \uc0bc\uc131 TV\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc788\ub294 \uad8c\ud55c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. TV \uc124\uc815\uc744 \ud655\uc778\ud558\uc5ec Home Assistant\ub97c \uc2b9\uc778\ud574\uc8fc\uc138\uc694.", "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "not_supported": "\uc774 \uc0bc\uc131 TV \ubaa8\ub378\uc740 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." + "id_missing": "\ud574\ub2f9 \uc0bc\uc131 \uae30\uae30\uc5d0 \uc77c\ub828\ubc88\ud638\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.", + "not_supported": "\uc774 \uc0bc\uc131 TV \ubaa8\ub378\uc740 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "auth_missing": "Home Assistant\uac00 \ud574\ub2f9 \uc0bc\uc131 TV\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc788\ub294 \uad8c\ud55c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. TV \uc124\uc815\uc744 \ud655\uc778\ud558\uc5ec Home Assistant\ub97c \uc2b9\uc778\ud574\uc8fc\uc138\uc694." }, "flow_title": "\uc0bc\uc131 TV: {model}", "step": { @@ -15,6 +21,9 @@ "pairing": { "description": "{device} \uc0bc\uc131 TV\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? Home Assistant\ub97c \uc5f0\uacb0\ud55c \uc801\uc774 \uc5c6\ub2e4\uba74 TV\uc5d0\uc11c \uc778\uc99d\uc744 \uc694\uccad\ud558\ub294 \ud31d\uc5c5\uc774 \ud45c\uc2dc\ub429\ub2c8\ub2e4. \uc774 TV\uc758 \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ub41c \ub0b4\uc6a9\uc740 \ub36e\uc5b4\uc50c\uc6cc\uc9d1\ub2c8\ub2e4." }, + "reauth_confirm": { + "description": "\ud655\uc778 \ud6c4 30\ucd08 \uc774\ub0b4\uc5d0 \uc2b9\uc778\uc744 \uc694\uccad\ud558\ub294 {device} \ud31d\uc5c5\uc744 \uc218\ub77d\ud558\uac70\ub098 PIN\uc744 \uc785\ub825\ud558\uc138\uc694." + }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8", diff --git a/homeassistant/components/samsungtv/translations/nl.json b/homeassistant/components/samsungtv/translations/nl.json index 9fe7e1dc3f9..4b69c8e812c 100644 --- a/homeassistant/components/samsungtv/translations/nl.json +++ b/homeassistant/components/samsungtv/translations/nl.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "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.", "not_supported": "Deze Samsung TV wordt momenteel niet ondersteund.", - "reauth_successful": "Herauthenticatie was succesvol", + "reauth_successful": "Herauthenticatie geslaagd", "unknown": "Onverwachte fout" }, "error": { diff --git a/homeassistant/components/season/translations/nl.json b/homeassistant/components/season/translations/nl.json index 63067c8a814..1da54d9c360 100644 --- a/homeassistant/components/season/translations/nl.json +++ b/homeassistant/components/season/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dienst is al geconfigureerd" + "already_configured": "Service is al geconfigureerd" }, "step": { "user": { diff --git a/homeassistant/components/sense/translations/ko.json b/homeassistant/components/sense/translations/ko.json index 269d8a76fea..b28f41f0d64 100644 --- a/homeassistant/components/sense/translations/ko.json +++ b/homeassistant/components/sense/translations/ko.json @@ -15,6 +15,9 @@ "password": "\ube44\ubc00\ubc88\ud638" }, "title": "Sense Energy Monitor\uc5d0 \uc5f0\uacb0\ud558\uae30" + }, + "validation": { + "title": "Sense \ub2e4\ub2e8\uacc4 \uc778\uc99d" } } } diff --git a/homeassistant/components/sense/translations/nl.json b/homeassistant/components/sense/translations/nl.json index 11b1c07350c..1f2f9e3d26b 100644 --- a/homeassistant/components/sense/translations/nl.json +++ b/homeassistant/components/sense/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -15,7 +15,7 @@ "password": "Wachtwoord" }, "description": "De Sense-integratie moet uw account {email} opnieuw verifi\u00ebren.", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" }, "user": { "data": { diff --git a/homeassistant/components/sensibo/translations/ko.json b/homeassistant/components/sensibo/translations/ko.json new file mode 100644 index 00000000000..7f03247bb3d --- /dev/null +++ b/homeassistant/components/sensibo/translations/ko.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "no_devices": "\uac80\uc0c9\ub41c \uae30\uae30 \uc5c6\uc74c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/nl.json b/homeassistant/components/sensibo/translations/nl.json index 7235aa80a80..fe04b6b64a8 100644 --- a/homeassistant/components/sensibo/translations/nl.json +++ b/homeassistant/components/sensibo/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/sensor/translations/ko.json b/homeassistant/components/sensor/translations/ko.json index 69ff6adb5e7..b01e5d95b9e 100644 --- a/homeassistant/components/sensor/translations/ko.json +++ b/homeassistant/components/sensor/translations/ko.json @@ -27,6 +27,7 @@ "power": "{entity_name}\uc758 \uc18c\ube44 \uc804\ub825\uc774 \ubcc0\ud560 \ub54c", "power_factor": "{entity_name}\uc758 \uc5ed\ub960\uc774 \ubcc0\ud560 \ub54c", "pressure": "{entity_name}\uc758 \uc555\ub825\uc774 \ubcc0\ud560 \ub54c", + "reactive_power": "{entity_name} \ubb34\ud6a8 \uc804\ub825\uc774 \ubcc0\ud560 \ub54c", "signal_strength": "{entity_name}\uc758 \uc2e0\ud638 \uac15\ub3c4\uac00 \ubcc0\ud560 \ub54c", "temperature": "{entity_name}\uc758 \uc628\ub3c4\uac00 \ubcc0\ud560 \ub54c", "value": "{entity_name}\uc758 \uac12\uc774 \ubcc0\ud560 \ub54c", diff --git a/homeassistant/components/senz/translations/nl.json b/homeassistant/components/senz/translations/nl.json index 0f50cb0f918..7f1eaccf89c 100644 --- a/homeassistant/components/senz/translations/nl.json +++ b/homeassistant/components/senz/translations/nl.json @@ -2,14 +2,14 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", - "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", - "oauth_error": "Ongeldige token data ontvangen." + "already_in_progress": "De configuratie is momenteel al bezig", + "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})", + "oauth_error": "Ongeldige tokengegevens ontvangen." }, "create_entry": { - "default": "Succesvol geauthenticeerd" + "default": "Authenticatie geslaagd" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/sharkiq/translations/nl.json b/homeassistant/components/sharkiq/translations/nl.json index 3acfdbdf074..c76fc24ea92 100644 --- a/homeassistant/components/sharkiq/translations/nl.json +++ b/homeassistant/components/sharkiq/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Account is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken", - "reauth_successful": "Herauthenticatie was succesvol", + "reauth_successful": "Herauthenticatie geslaagd", "unknown": "Onverwachte fout" }, "error": { @@ -14,7 +14,7 @@ "step": { "reauth": { "data": { - "password": "Paswoord", + "password": "Wachtwoord", "username": "Gebruikersnaam" } }, diff --git a/homeassistant/components/shelly/translations/ja.json b/homeassistant/components/shelly/translations/ja.json index 12a97c8508a..748e70fd32a 100644 --- a/homeassistant/components/shelly/translations/ja.json +++ b/homeassistant/components/shelly/translations/ja.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "firmware_not_fully_provisioned": "\u30c7\u30d0\u30a4\u30b9\u304c\u5b8c\u5168\u306b\u30d7\u30ed\u30d3\u30b8\u30e7\u30cb\u30f3\u30b0(provisioned)\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002Shelly\u30b5\u30dd\u30fc\u30c8\u306b\u9023\u7d61\u3057\u3066\u304f\u3060\u3055\u3044", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/shelly/translations/no.json b/homeassistant/components/shelly/translations/no.json index fe7d30c098f..072d0bf4ee7 100644 --- a/homeassistant/components/shelly/translations/no.json +++ b/homeassistant/components/shelly/translations/no.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Tilkobling mislyktes", + "firmware_not_fully_provisioned": "Enheten er ikke fullstendig klargjort. Vennligst kontakt Shelly support", "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json index d1c658d7816..71d7f047e5f 100644 --- a/homeassistant/components/shelly/translations/pl.json +++ b/homeassistant/components/shelly/translations/pl.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "firmware_not_fully_provisioned": "Urz\u0105dzenie nie jest w pe\u0142ni udost\u0119pnione. Skontaktuj si\u0119 z pomoc\u0105 techniczn\u0105 Shelly.", "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, diff --git a/homeassistant/components/sia/translations/ko.json b/homeassistant/components/sia/translations/ko.json new file mode 100644 index 00000000000..cb0897c7873 --- /dev/null +++ b/homeassistant/components/sia/translations/ko.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "invalid_account_format": "\uacc4\uc815\uc774 16\uc9c4\uc218 \uac12\uc774 \uc544\ub2d9\ub2c8\ub2e4. 0-9 \ubc0f A-F\ub9cc \uc0ac\uc6a9\ud558\uc2ed\uc2dc\uc624.", + "invalid_account_length": "\uacc4\uc815\uc758 \uae38\uc774\uac00 \uc801\uc808\uce58 \uc54a\uc2b5\ub2c8\ub2e4. 3~16\uc790 \uc0ac\uc774\uc5ec\uc57c \ud569\ub2c8\ub2e4.", + "invalid_key_format": "\ud0a4\uac12\uc774 16\uc9c4\uc218 \uac12\uc774 \uc544\ub2d9\ub2c8\ub2e4. 0-9 \ubc0f A-F\ub9cc \uc0ac\uc6a9\ud558\uc2ed\uc2dc\uc624.", + "invalid_key_length": "\ud0a4\uac12\uc758 \uae38\uc774\uac00 \uc801\uc808\uce58 \uc54a\uc2b5\ub2c8\ub2e4. 16, 24 \ub610\ub294 32\uac1c\uc758 16\uc9c4\uc218\ubb38\uc790\uc5ec\uc57c \ub429\ub2c8\ub2e4.", + "invalid_ping": "\ud551 \uac04\uaca9\uc740 1\ubd84\uc5d0\uc11c 1440\ubd84 \uc0ac\uc774\uc5ec\uc57c \ud569\ub2c8\ub2e4.", + "invalid_zones": "\ucd5c\uc18c\ud55c 1\uac1c\uc758 \uc601\uc5ed\uc774 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sia/translations/nl.json b/homeassistant/components/sia/translations/nl.json index cd0d5183cb9..0b3790d53e2 100644 --- a/homeassistant/components/sia/translations/nl.json +++ b/homeassistant/components/sia/translations/nl.json @@ -26,7 +26,7 @@ "additional_account": "Extra accounts", "encryption_key": "Encryptiesleutel", "ping_interval": "Ping Interval (min)", - "port": "Port", + "port": "Poort", "protocol": "Protocol", "zones": "Aantal zones voor het account" }, diff --git a/homeassistant/components/simplisafe/translations/nl.json b/homeassistant/components/simplisafe/translations/nl.json index 55e7c3c7832..34db358c02b 100644 --- a/homeassistant/components/simplisafe/translations/nl.json +++ b/homeassistant/components/simplisafe/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Dit SimpliSafe-account is al in gebruik.", "email_2fa_timed_out": "Timed out tijdens het wachten op email-gebaseerde twee-factor authenticatie.", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "invalid_auth": "Ongeldige authenticatie", @@ -18,7 +18,7 @@ "password": "Wachtwoord" }, "description": "Voer het wachtwoord voor {username} opnieuw in.", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" }, "sms_2fa": { "data": { diff --git a/homeassistant/components/siren/translations/pl.json b/homeassistant/components/siren/translations/pl.json new file mode 100644 index 00000000000..965b368e83b --- /dev/null +++ b/homeassistant/components/siren/translations/pl.json @@ -0,0 +1,3 @@ +{ + "title": "Syrena" +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/pl.json b/homeassistant/components/slack/translations/pl.json index fd1cb508e4a..33e8155a7f2 100644 --- a/homeassistant/components/slack/translations/pl.json +++ b/homeassistant/components/slack/translations/pl.json @@ -17,8 +17,12 @@ "username": "Nazwa u\u017cytkownika" }, "data_description": { - "api_key": "Token API Slack u\u017cywany do wysy\u0142ania wiadomo\u015bci Slack." - } + "api_key": "Token API Slack u\u017cywany do wysy\u0142ania wiadomo\u015bci Slack.", + "default_channel": "Kana\u0142, do kt\u00f3rego zostanie wys\u0142ana wiadomo\u015b\u0107, je\u015bli podczas wysy\u0142ania wiadomo\u015bci nie zosta\u0142 okre\u015blony \u017caden kana\u0142.", + "icon": "U\u017cyj jednej z emotikonek Slack jako ikony dla podanej nazwy u\u017cytkownika.", + "username": "Home Assistant wy\u015ble wiadomo\u015bci, u\u017cywaj\u0105c podanej nazwy u\u017cytkownika." + }, + "description": "Zapoznaj si\u0119 z dokumentacj\u0105 dotycz\u0105c\u0105 uzyskiwania klucza API Slack." } } } diff --git a/homeassistant/components/sleepiq/translations/nl.json b/homeassistant/components/sleepiq/translations/nl.json index 09029be118e..9b829caa025 100644 --- a/homeassistant/components/sleepiq/translations/nl.json +++ b/homeassistant/components/sleepiq/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -14,7 +14,7 @@ "password": "Wachtwoord" }, "description": "De SleepIQ-integratie moet uw account {username} opnieuw verifi\u00ebren.", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" }, "user": { "data": { diff --git a/homeassistant/components/slimproto/translations/nl.json b/homeassistant/components/slimproto/translations/nl.json index 79aaec23123..703ac8614c4 100644 --- a/homeassistant/components/slimproto/translations/nl.json +++ b/homeassistant/components/slimproto/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." } } } \ No newline at end of file diff --git a/homeassistant/components/sma/translations/ko.json b/homeassistant/components/sma/translations/ko.json index 5e5aa899d96..bb627d5c622 100644 --- a/homeassistant/components/sma/translations/ko.json +++ b/homeassistant/components/sma/translations/ko.json @@ -6,17 +6,21 @@ }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "cannot_retrieve_device_info": "\uc131\uacf5\uc801\uc73c\ub85c \uc5f0\uacb0\ub418\uc5c8\uc9c0\ub9cc \uc7a5\uce58 \uc815\ubcf4\ub97c \uac80\uc0c9\ud560 \uc218 \uc5c6\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": { + "group": "\uadf8\ub8f9", "host": "\ud638\uc2a4\ud2b8", "password": "\ube44\ubc00\ubc88\ud638", "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" - } + }, + "description": "SMA \uc7a5\uce58 \uc815\ubcf4\ub97c \uc785\ub825\ud569\ub2c8\ub2e4.", + "title": "SMA Solar \uc124\uc815" } } } diff --git a/homeassistant/components/sma/translations/nl.json b/homeassistant/components/sma/translations/nl.json index d860518a18c..24298fe4bb3 100644 --- a/homeassistant/components/sma/translations/nl.json +++ b/homeassistant/components/sma/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang" + "already_in_progress": "De configuratie is momenteel al bezig" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -16,7 +16,7 @@ "group": "Groep", "host": "Host", "password": "Wachtwoord", - "ssl": "Gebruik een SSL-certificaat", + "ssl": "Maakt gebruik van een SSL-certificaat", "verify_ssl": "SSL-certificaat verifi\u00ebren" }, "description": "Voer uw SMA-apparaatgegevens in.", diff --git a/homeassistant/components/smappee/translations/nl.json b/homeassistant/components/smappee/translations/nl.json index ed9ef6e5e9f..8881aa1f047 100644 --- a/homeassistant/components/smappee/translations/nl.json +++ b/homeassistant/components/smappee/translations/nl.json @@ -3,11 +3,11 @@ "abort": { "already_configured_device": "Apparaat is al geconfigureerd", "already_configured_local_device": "Lokale apparaten zijn al geconfigureerd. Verwijder deze eerst voordat u een cloudapparaat configureert.", - "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", "cannot_connect": "Kan geen verbinding maken", "invalid_mdns": "Niet-ondersteund apparaat voor de Smappee-integratie.", - "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})" + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/smart_meter_texas/translations/nl.json b/homeassistant/components/smart_meter_texas/translations/nl.json index d25cf575b44..50b4c3f2fe6 100644 --- a/homeassistant/components/smart_meter_texas/translations/nl.json +++ b/homeassistant/components/smart_meter_texas/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, @@ -12,7 +12,7 @@ "user": { "data": { "password": "Wachtwoord", - "username": "Benutzername" + "username": "Gebruikersnaam" } } } diff --git a/homeassistant/components/smarttub/translations/ko.json b/homeassistant/components/smarttub/translations/ko.json index ff50dadc63e..6ba95c931bd 100644 --- a/homeassistant/components/smarttub/translations/ko.json +++ b/homeassistant/components/smarttub/translations/ko.json @@ -9,6 +9,7 @@ }, "step": { "reauth_confirm": { + "description": "SmartTub \ud1b5\ud569\uad6c\uc131\uc694\uc18c\ub294 \uacc4\uc815\uc744 \ub2e4\uc2dc \uc778\uc99d\ud574\uc57c \ud569\ub2c8\ub2e4.", "title": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc7ac\uc778\uc99d\ud558\uae30" }, "user": { diff --git a/homeassistant/components/smarttub/translations/nl.json b/homeassistant/components/smarttub/translations/nl.json index 6d1e605d315..cfe5b601491 100644 --- a/homeassistant/components/smarttub/translations/nl.json +++ b/homeassistant/components/smarttub/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "invalid_auth": "Ongeldige authenticatie" @@ -10,7 +10,7 @@ "step": { "reauth_confirm": { "description": "De SmartTub-integratie moet uw account opnieuw verifi\u00ebren", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" }, "user": { "data": { diff --git a/homeassistant/components/sms/translations/nl.json b/homeassistant/components/sms/translations/nl.json index 7c2bcb0568c..7e05de163cf 100644 --- a/homeassistant/components/sms/translations/nl.json +++ b/homeassistant/components/sms/translations/nl.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "unknown": "Onverwachte fout" }, "step": { diff --git a/homeassistant/components/soma/translations/nl.json b/homeassistant/components/soma/translations/nl.json index 32db40348ba..33d9452950b 100644 --- a/homeassistant/components/soma/translations/nl.json +++ b/homeassistant/components/soma/translations/nl.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_setup": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", - "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "already_setup": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", + "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", "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 geauthenticeerd" + "default": "Authenticatie geslaagd" }, "step": { "user": { diff --git a/homeassistant/components/somfy/translations/nl.json b/homeassistant/components/somfy/translations/nl.json index 11b3de442dd..efd07952467 100644 --- a/homeassistant/components/somfy/translations/nl.json +++ b/homeassistant/components/somfy/translations/nl.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", + "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "create_entry": { - "default": "Succesvol geauthenticeerd" + "default": "Authenticatie geslaagd" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/sonarr/translations/nl.json b/homeassistant/components/sonarr/translations/nl.json index d6782781090..a5f289a1428 100644 --- a/homeassistant/components/sonarr/translations/nl.json +++ b/homeassistant/components/sonarr/translations/nl.json @@ -2,18 +2,18 @@ "config": { "abort": { "already_configured": "Service is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol", + "reauth_successful": "Herauthenticatie geslaagd", "unknown": "Onverwachte fout" }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie" }, "flow_title": "{name}", "step": { "reauth_confirm": { "description": "De Sonarr-integratie moet handmatig opnieuw worden geverifieerd met de Sonarr-API die wordt gehost op: {url}", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" }, "user": { "data": { diff --git a/homeassistant/components/songpal/translations/nl.json b/homeassistant/components/songpal/translations/nl.json index 0550b154ca4..aa4be57dda2 100644 --- a/homeassistant/components/songpal/translations/nl.json +++ b/homeassistant/components/songpal/translations/nl.json @@ -5,7 +5,7 @@ "not_songpal_device": "Geen Songpal-apparaat" }, "error": { - "cannot_connect": "Kon niet verbinden" + "cannot_connect": "Kan geen verbinding maken" }, "flow_title": "{name} ({host})", "step": { diff --git a/homeassistant/components/sonos/translations/ko.json b/homeassistant/components/sonos/translations/ko.json index f85f3c5cab4..95a2c1ccb5a 100644 --- a/homeassistant/components/sonos/translations/ko.json +++ b/homeassistant/components/sonos/translations/ko.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "not_sonos_device": "\ubc1c\uacac\ub41c \uc7a5\uce58\uac00 Sonos \uc7a5\uce58\uac00 \uc544\ub2d9\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." }, "step": { diff --git a/homeassistant/components/sonos/translations/nl.json b/homeassistant/components/sonos/translations/nl.json index 0147c5c5382..615654c98ad 100644 --- a/homeassistant/components/sonos/translations/nl.json +++ b/homeassistant/components/sonos/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "no_devices_found": "Geen apparaten gevonden op het netwerk", "not_sonos_device": "Gevonden apparaat is geen Sonos-apparaat", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "confirm": { diff --git a/homeassistant/components/speedtestdotnet/translations/nl.json b/homeassistant/components/speedtestdotnet/translations/nl.json index 8cd6cb960c8..b5af5dbc78d 100644 --- a/homeassistant/components/speedtestdotnet/translations/nl.json +++ b/homeassistant/components/speedtestdotnet/translations/nl.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Wilt u beginnen met instellen?" + "description": "Wil je beginnen met instellen?" } } }, diff --git a/homeassistant/components/spider/translations/nl.json b/homeassistant/components/spider/translations/nl.json index 373d203aed7..7540b821163 100644 --- a/homeassistant/components/spider/translations/nl.json +++ b/homeassistant/components/spider/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { "invalid_auth": "Ongeldige authenticatie", diff --git a/homeassistant/components/spotify/translations/nl.json b/homeassistant/components/spotify/translations/nl.json index 0d1e63fde5d..577e70bb0d1 100644 --- a/homeassistant/components/spotify/translations/nl.json +++ b/homeassistant/components/spotify/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", "missing_configuration": "De Spotify integratie is niet geconfigureerd. Gelieve de documentatie te volgen.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout [check the help section] ({docs_url})", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})", "reauth_account_mismatch": "Het Spotify account waarmee er is geverifieerd, komt niet overeen met het account dat opnieuw moet worden geverifieerd." }, "create_entry": { @@ -15,7 +15,7 @@ }, "reauth_confirm": { "description": "De Spotify integratie moet opnieuw worden geverifieerd met Spotify voor account: {account}", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" } } }, diff --git a/homeassistant/components/squeezebox/translations/nl.json b/homeassistant/components/squeezebox/translations/nl.json index fc05f6d4807..ad18c117bb1 100644 --- a/homeassistant/components/squeezebox/translations/nl.json +++ b/homeassistant/components/squeezebox/translations/nl.json @@ -5,7 +5,7 @@ "no_server_found": "Geen LMS server gevonden." }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "no_server_found": "Kan server niet automatisch vinden.", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/srp_energy/translations/nl.json b/homeassistant/components/srp_energy/translations/nl.json index ce4ac90c223..b52bda50d98 100644 --- a/homeassistant/components/srp_energy/translations/nl.json +++ b/homeassistant/components/srp_energy/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/steam_online/translations/nl.json b/homeassistant/components/steam_online/translations/nl.json index d9281038e76..9255b0e06ff 100644 --- a/homeassistant/components/steam_online/translations/nl.json +++ b/homeassistant/components/steam_online/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Service is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -13,7 +13,7 @@ "step": { "reauth_confirm": { "description": "De Steam integratie moet handmatig opnieuw geauthenticeerd worden\n\nU kunt uw sleutel hier vinden: {api_key_url}", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" }, "user": { "data": { diff --git a/homeassistant/components/steamist/translations/nl.json b/homeassistant/components/steamist/translations/nl.json index 345ec01dce3..62c492f5e02 100644 --- a/homeassistant/components/steamist/translations/nl.json +++ b/homeassistant/components/steamist/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "cannot_connect": "Kan geen verbinding maken", "no_devices_found": "Geen apparaten gevonden op het netwerk", "not_steamist_device": "Geen steamist-apparaat" diff --git a/homeassistant/components/sun/translations/nl.json b/homeassistant/components/sun/translations/nl.json index 8c284e4e43d..57d17476331 100644 --- a/homeassistant/components/sun/translations/nl.json +++ b/homeassistant/components/sun/translations/nl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "user": { - "description": "Wilt u beginnen met instellen?" + "description": "Wil je beginnen met instellen?" } } }, diff --git a/homeassistant/components/switcher_kis/translations/nl.json b/homeassistant/components/switcher_kis/translations/nl.json index 0671f0b3674..6fc4a03e824 100644 --- a/homeassistant/components/switcher_kis/translations/nl.json +++ b/homeassistant/components/switcher_kis/translations/nl.json @@ -2,11 +2,11 @@ "config": { "abort": { "no_devices_found": "Geen apparaten gevonden op het netwerk", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "confirm": { - "description": "Wilt u beginnen met instellen?" + "description": "Wil je beginnen met instellen?" } } } diff --git a/homeassistant/components/synology_dsm/translations/nl.json b/homeassistant/components/synology_dsm/translations/nl.json index c685137738a..c43f41b4277 100644 --- a/homeassistant/components/synology_dsm/translations/nl.json +++ b/homeassistant/components/synology_dsm/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol", + "reauth_successful": "Herauthenticatie geslaagd", "reconfigure_successful": "Herconfiguratie was succesvol" }, "error": { @@ -10,7 +10,7 @@ "invalid_auth": "Ongeldige authenticatie", "missing_data": "Ontbrekende gegevens: probeer het later opnieuw of een andere configuratie", "otp_failed": "Tweestapsverificatie is mislukt, probeer het opnieuw met een nieuwe toegangscode", - "unknown": "Onbekende fout: controleer de logs voor meer informatie" + "unknown": "Onverwachte fout" }, "flow_title": "{name} ({host})", "step": { @@ -24,9 +24,9 @@ "data": { "password": "Wachtwoord", "port": "Poort", - "ssl": "Gebruik een SSL-certificaat", + "ssl": "Maakt gebruik van een SSL-certificaat", "username": "Gebruikersnaam", - "verify_ssl": "Controleer het SSL-certificaat" + "verify_ssl": "SSL-certificaat verifi\u00ebren" }, "description": "Wil je {name} ({host}) instellen?" }, @@ -42,9 +42,9 @@ "host": "Host", "password": "Wachtwoord", "port": "Poort", - "ssl": "Gebruik een SSL-certificaat", + "ssl": "Maakt gebruik van een SSL-certificaat", "username": "Gebruikersnaam", - "verify_ssl": "Controleer het SSL-certificaat" + "verify_ssl": "SSL-certificaat verifi\u00ebren" } } } diff --git a/homeassistant/components/system_bridge/translations/ko.json b/homeassistant/components/system_bridge/translations/ko.json new file mode 100644 index 00000000000..c40a7a55814 --- /dev/null +++ b/homeassistant/components/system_bridge/translations/ko.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "{name}", + "step": { + "authenticate": { + "data": { + "api_key": "API \ud0a4" + }, + "description": "{name} \uc5d0 \ub300\ud55c \uad6c\uc131\uc5d0\uc11c \uc124\uc815\ud55c API \ud0a4\ub97c \uc785\ub825\ud558\uc2ed\uc2dc\uc624." + }, + "user": { + "data": { + "api_key": "API \ud0a4", + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8" + }, + "description": "\uc5f0\uacb0 \uc138\ubd80 \uc815\ubcf4\ub97c \uc785\ub825\ud558\uc138\uc694." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/system_bridge/translations/nl.json b/homeassistant/components/system_bridge/translations/nl.json index c419123e50d..c9d1c45d6a3 100644 --- a/homeassistant/components/system_bridge/translations/nl.json +++ b/homeassistant/components/system_bridge/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol", + "reauth_successful": "Herauthenticatie geslaagd", "unknown": "Onverwachte fout" }, "error": { diff --git a/homeassistant/components/tailscale/translations/nl.json b/homeassistant/components/tailscale/translations/nl.json index 1a59a3ca029..5e05b0ebcb6 100644 --- a/homeassistant/components/tailscale/translations/nl.json +++ b/homeassistant/components/tailscale/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/tankerkoenig/translations/nl.json b/homeassistant/components/tankerkoenig/translations/nl.json index 96de058ad74..66d442a71f6 100644 --- a/homeassistant/components/tankerkoenig/translations/nl.json +++ b/homeassistant/components/tankerkoenig/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd." + "already_configured": "Locatie is al geconfigureerd" }, "error": { "invalid_auth": "Ongeldige authenticatie", diff --git a/homeassistant/components/tasmota/translations/nl.json b/homeassistant/components/tasmota/translations/nl.json index 474ed82b9d6..a116e840977 100644 --- a/homeassistant/components/tasmota/translations/nl.json +++ b/homeassistant/components/tasmota/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Is al geconfigureerd. Er is maar een configuratie mogelijk" + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { "invalid_discovery_topic": "Invalid discovery topic prefix." diff --git a/homeassistant/components/tautulli/translations/nl.json b/homeassistant/components/tautulli/translations/nl.json index 9f10f666030..5e791e33a8f 100644 --- a/homeassistant/components/tautulli/translations/nl.json +++ b/homeassistant/components/tautulli/translations/nl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "reauth_successful": "Herauthenticatie was succesvol", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "reauth_successful": "Herauthenticatie geslaagd", + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/tellduslive/translations/nl.json b/homeassistant/components/tellduslive/translations/nl.json index d0c03a341f0..0f718304059 100644 --- a/homeassistant/components/tellduslive/translations/nl.json +++ b/homeassistant/components/tellduslive/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Service is al geconfigureerd", - "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", "unknown": "Onverwachte fout", "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL." }, diff --git a/homeassistant/components/tile/translations/nl.json b/homeassistant/components/tile/translations/nl.json index d2c66b4fe65..8a6270009a8 100644 --- a/homeassistant/components/tile/translations/nl.json +++ b/homeassistant/components/tile/translations/nl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "invalid_auth": "Ongeldige authenticatie" diff --git a/homeassistant/components/tolo/translations/nl.json b/homeassistant/components/tolo/translations/nl.json index dcc6f1936d0..31d417dd9e7 100644 --- a/homeassistant/components/tolo/translations/nl.json +++ b/homeassistant/components/tolo/translations/nl.json @@ -9,7 +9,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Wilt u beginnen met instellen?" + "description": "Wil je beginnen met instellen?" }, "user": { "data": { diff --git a/homeassistant/components/tomorrowio/translations/sensor.ko.json b/homeassistant/components/tomorrowio/translations/sensor.ko.json new file mode 100644 index 00000000000..de8aaf71402 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.ko.json @@ -0,0 +1,7 @@ +{ + "state": { + "tomorrowio__precipitation_type": { + "freezing_rain": "\uc5b4\ub294 \ube44" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/nl.json b/homeassistant/components/toon/translations/nl.json index 021c6276b1a..176fa50e932 100644 --- a/homeassistant/components/toon/translations/nl.json +++ b/homeassistant/components/toon/translations/nl.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "De geselecteerde overeenkomst is al geconfigureerd.", - "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", + "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", "no_agreements": "Dit account heeft geen Toon schermen.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout [check the help section] ( {docs_url} )", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})", "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL." }, "step": { diff --git a/homeassistant/components/totalconnect/translations/nl.json b/homeassistant/components/totalconnect/translations/nl.json index 186e2f60d50..e80030ec06b 100644 --- a/homeassistant/components/totalconnect/translations/nl.json +++ b/homeassistant/components/totalconnect/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Account is al geconfigureerd", "no_locations": "Er zijn geen locaties beschikbaar voor deze gebruiker, controleer de instellingen van TotalConnect", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "invalid_auth": "Ongeldige authenticatie", @@ -19,7 +19,7 @@ }, "reauth_confirm": { "description": "Total Connect moet uw account opnieuw verifi\u00ebren", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" }, "user": { "data": { diff --git a/homeassistant/components/tplink/translations/nl.json b/homeassistant/components/tplink/translations/nl.json index bceae9f3fff..67f79e90223 100644 --- a/homeassistant/components/tplink/translations/nl.json +++ b/homeassistant/components/tplink/translations/nl.json @@ -5,7 +5,7 @@ "no_devices_found": "Geen apparaten gevonden op het netwerk" }, "error": { - "cannot_connect": "Kon geen verbinding maken" + "cannot_connect": "Kan geen verbinding maken" }, "flow_title": "{name} {model} ({host})", "step": { diff --git a/homeassistant/components/traccar/translations/nl.json b/homeassistant/components/traccar/translations/nl.json index 45a70a42b43..098b2c9376f 100644 --- a/homeassistant/components/traccar/translations/nl.json +++ b/homeassistant/components/traccar/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "cloud_not_connected": "Niet verbonden met Home Assistant Cloud.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", - "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." + "webhook_not_internet_accessible": "Je Home Assistant-instantie moet toegankelijk zijn vanaf internet om webhook-berichten te ontvangen." }, "create_entry": { "default": "Voor het verzenden van gebeurtenissen naar Home Assistant, moet u de webhook-functie in Traccar instellen.\n\nGebruik de volgende URL: ' {webhook_url} '\n\nZie [de documentatie] ({docs_url}) voor meer informatie." diff --git a/homeassistant/components/tractive/translations/nl.json b/homeassistant/components/tractive/translations/nl.json index b0e1f17cdc3..910131316da 100644 --- a/homeassistant/components/tractive/translations/nl.json +++ b/homeassistant/components/tractive/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "reauth_failed_existing": "Kon het configuratie-item niet bijwerken, verwijder de integratie en stel deze opnieuw in.", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "invalid_auth": "Ongeldige authenticatie", diff --git a/homeassistant/components/tradfri/translations/nl.json b/homeassistant/components/tradfri/translations/nl.json index 752270e78c6..02bffc6b60d 100644 --- a/homeassistant/components/tradfri/translations/nl.json +++ b/homeassistant/components/tradfri/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang" + "already_in_progress": "De configuratie is momenteel al bezig" }, "error": { "cannot_authenticate": "Kan niet authenticeren, is Gateway gekoppeld met een andere server zoals bijv. Homekit?", diff --git a/homeassistant/components/trafikverket_ferry/translations/nl.json b/homeassistant/components/trafikverket_ferry/translations/nl.json index f84232839c8..edaeb14fd48 100644 --- a/homeassistant/components/trafikverket_ferry/translations/nl.json +++ b/homeassistant/components/trafikverket_ferry/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/trafikverket_train/translations/ko.json b/homeassistant/components/trafikverket_train/translations/ko.json new file mode 100644 index 00000000000..82cb120d44d --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/ko.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \ud0a4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/nl.json b/homeassistant/components/trafikverket_train/translations/nl.json index d076f8b2961..3143a28cf16 100644 --- a/homeassistant/components/trafikverket_train/translations/nl.json +++ b/homeassistant/components/trafikverket_train/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/tuya/translations/select.nl.json b/homeassistant/components/tuya/translations/select.nl.json index e645694ae55..521a9c908ba 100644 --- a/homeassistant/components/tuya/translations/select.nl.json +++ b/homeassistant/components/tuya/translations/select.nl.json @@ -77,7 +77,7 @@ }, "tuya__light_mode": { "none": "Uit", - "pos": "Plaats van schakelaar aangeven", + "pos": "Schakelpositie aangeven", "relay": "Aan/uit-toestand aangeven" }, "tuya__motion_sensitivity": { @@ -90,8 +90,8 @@ "2": "Continu opnemen" }, "tuya__relay_status": { - "last": "Onthoud laatste staat", - "memory": "Onthoud laatste staat", + "last": "Laatste status onthouden", + "memory": "Laatste status onthouden", "off": "Uit", "on": "Aan", "power_off": "Uit", @@ -116,7 +116,7 @@ "mop": "Dweil", "part": "Deel", "partial_bow": "Boog gedeeltelijk", - "pick_zone": "Kies zone", + "pick_zone": "Zone kiezen", "point": "Punt", "pose": "Houding", "random": "Willekeurig", diff --git a/homeassistant/components/twilio/translations/nl.json b/homeassistant/components/twilio/translations/nl.json index cf180123c98..a63d0301dcd 100644 --- a/homeassistant/components/twilio/translations/nl.json +++ b/homeassistant/components/twilio/translations/nl.json @@ -3,14 +3,14 @@ "abort": { "cloud_not_connected": "Niet verbonden met Home Assistant Cloud.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", - "webhook_not_internet_accessible": "Uw Home Assistant-instantie moet toegankelijk zijn via internet om webhook-berichten te ontvangen." + "webhook_not_internet_accessible": "Je Home Assistant-instantie moet toegankelijk zijn vanaf internet om webhook-berichten te ontvangen." }, "create_entry": { "default": "Om evenementen naar de Home Assistant te verzenden, moet u [Webhooks with Twilio] ( {twilio_url} ) instellen. \n\n Vul de volgende info in: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhoudstype: application / x-www-form-urlencoded \n\n Zie [de documentatie] ( {docs_url} ) voor informatie over het configureren van automatiseringen om binnenkomende gegevens te verwerken." }, "step": { "user": { - "description": "Wilt u beginnen met instellen?", + "description": "Wil je beginnen met instellen?", "title": "Stel de Twilio Webhook in" } } diff --git a/homeassistant/components/ukraine_alarm/translations/nl.json b/homeassistant/components/ukraine_alarm/translations/nl.json index 6f09f794ac4..97fd17e23e0 100644 --- a/homeassistant/components/ukraine_alarm/translations/nl.json +++ b/homeassistant/components/ukraine_alarm/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd.", + "already_configured": "Locatie is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken", "max_regions": "Er kunnen maximaal 5 regio's worden geconfigureerd", "rate_limit": "Te veel verzoeken", @@ -11,13 +11,13 @@ "step": { "community": { "data": { - "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" + "region": "Regio" }, "description": "Als u niet alleen de staat en het district wilt volgen, kiest u de specifieke gemeenschap ervan" }, "district": { "data": { - "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" + "region": "Regio" }, "description": "Als u niet alleen de staat wilt controleren, kies dan het specifieke district" }, diff --git a/homeassistant/components/unifi/translations/nl.json b/homeassistant/components/unifi/translations/nl.json index 637dbd480f3..6e9b0ec7ede 100644 --- a/homeassistant/components/unifi/translations/nl.json +++ b/homeassistant/components/unifi/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Unifi Network site is al geconfigureerd", "configuration_updated": "Configuratie bijgewerkt", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "faulty_credentials": "Ongeldige authenticatie", diff --git a/homeassistant/components/unifiprotect/translations/ko.json b/homeassistant/components/unifiprotect/translations/ko.json index 36be85fd61a..916dcc8a7e9 100644 --- a/homeassistant/components/unifiprotect/translations/ko.json +++ b/homeassistant/components/unifiprotect/translations/ko.json @@ -1,8 +1,16 @@ { "config": { + "abort": { + "discovery_started": "\uc7a5\uce58\uac80\uc0c9 \uc2dc\uc791" + }, "step": { "discovery_confirm": { - "description": "{name} ( {ip_address} )\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? " + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "{name} ( {ip_address} )\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c? ", + "title": "UniFi Protect \ubc1c\uacac" } } } diff --git a/homeassistant/components/upb/translations/nl.json b/homeassistant/components/upb/translations/nl.json index 56ba7eae754..b3284eed134 100644 --- a/homeassistant/components/upb/translations/nl.json +++ b/homeassistant/components/upb/translations/nl.json @@ -4,7 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd" }, "error": { - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "invalid_upb_file": "Ontbrekend of ongeldig UPB UPStart-exportbestand, controleer de naam en het pad van het bestand.", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/uptime/translations/nl.json b/homeassistant/components/uptime/translations/nl.json index 786fec37f6b..b4ed0a1db36 100644 --- a/homeassistant/components/uptime/translations/nl.json +++ b/homeassistant/components/uptime/translations/nl.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "user": { - "description": "Wilt u beginnen met instellen?" + "description": "Wil je beginnen met instellen?" } } }, diff --git a/homeassistant/components/uptimerobot/translations/nl.json b/homeassistant/components/uptimerobot/translations/nl.json index d3ee1f7515a..61a2e8137e5 100644 --- a/homeassistant/components/uptimerobot/translations/nl.json +++ b/homeassistant/components/uptimerobot/translations/nl.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "reauth_failed_existing": "Kon de config entry niet updaten, gelieve de integratie te verwijderen en het opnieuw op te zetten.", - "reauth_successful": "Herauthenticatie was succesvol", + "reauth_successful": "Herauthenticatie geslaagd", "unknown": "Onverwachte fout" }, "error": { @@ -19,7 +19,7 @@ "api_key": "API-sleutel" }, "description": "U moet een nieuwe 'hoofd'-API-sleutel van UptimeRobot opgeven", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" }, "user": { "data": { diff --git a/homeassistant/components/vallox/translations/nl.json b/homeassistant/components/vallox/translations/nl.json index b8b5b6bdc57..9b064500704 100644 --- a/homeassistant/components/vallox/translations/nl.json +++ b/homeassistant/components/vallox/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Service is al geconfigureerd", - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "invalid_host": "Ongeldige hostnaam of IP-adres", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/venstar/translations/nl.json b/homeassistant/components/venstar/translations/nl.json index 3c8a61faf20..b39a0f356e1 100644 --- a/homeassistant/components/venstar/translations/nl.json +++ b/homeassistant/components/venstar/translations/nl.json @@ -12,8 +12,8 @@ "data": { "host": "Host", "password": "Wachtwoord", - "pin": "PIN-code", - "ssl": "Gebruik een SSL-certificaat", + "pin": "Pincode", + "ssl": "Maakt gebruik van een SSL-certificaat", "username": "Gebruikersnaam" }, "title": "Maak verbinding met de Venstar-thermostaat" diff --git a/homeassistant/components/verisure/translations/nl.json b/homeassistant/components/verisure/translations/nl.json index d0519e584fd..1a23da57319 100644 --- a/homeassistant/components/verisure/translations/nl.json +++ b/homeassistant/components/verisure/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "invalid_auth": "Ongeldige authenticatie", diff --git a/homeassistant/components/vesync/translations/nl.json b/homeassistant/components/vesync/translations/nl.json index ab330237afd..36e661eca21 100644 --- a/homeassistant/components/vesync/translations/nl.json +++ b/homeassistant/components/vesync/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slecht \u00e9\u00e9n configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { "invalid_auth": "Ongeldige authenticatie" diff --git a/homeassistant/components/vicare/translations/nl.json b/homeassistant/components/vicare/translations/nl.json index 1e5fc68f9e3..a3efaf0661e 100644 --- a/homeassistant/components/vicare/translations/nl.json +++ b/homeassistant/components/vicare/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", "unknown": "Onverwachte fout" }, "error": { diff --git a/homeassistant/components/vizio/translations/nl.json b/homeassistant/components/vizio/translations/nl.json index f17e0470799..871aa4e3e69 100644 --- a/homeassistant/components/vizio/translations/nl.json +++ b/homeassistant/components/vizio/translations/nl.json @@ -1,19 +1,19 @@ { "config": { "abort": { - "already_configured_device": "Dit apparaat is al geconfigureerd", + "already_configured_device": "Apparaat is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken", "updated_entry": "Dit item is al ingesteld, maar de naam en/of opties die zijn gedefinieerd in de configuratie komen niet overeen met de eerder ge\u00efmporteerde configuratie, dus het configuratie-item is dienovereenkomstig bijgewerkt." }, "error": { - "cannot_connect": "Verbinding mislukt", + "cannot_connect": "Kan geen verbinding maken", "complete_pairing_failed": "Kan het koppelen niet voltooien. Zorg ervoor dat de door u opgegeven pincode correct is en dat de tv nog steeds van stroom wordt voorzien en is verbonden met het netwerk voordat u opnieuw verzendt.", "existing_config_entry_found": "Een bestaande VIZIO SmartCast-apparaat config entry met hetzelfde serienummer is reeds geconfigureerd. U moet de bestaande entry verwijderen om deze te kunnen configureren." }, "step": { "pair_tv": { "data": { - "pin": "PIN-code" + "pin": "Pincode" }, "description": "Uw TV zou een code moeten weergeven. Voer die code in het formulier in en ga dan door naar de volgende stap om de koppeling te voltooien.", "title": "Voltooi het koppelingsproces" diff --git a/homeassistant/components/vlc_telnet/translations/nl.json b/homeassistant/components/vlc_telnet/translations/nl.json index 538421263c9..771ce98964d 100644 --- a/homeassistant/components/vlc_telnet/translations/nl.json +++ b/homeassistant/components/vlc_telnet/translations/nl.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_configured": "Deze service is al geconfigureerd", + "already_configured": "Service is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", - "reauth_successful": "Het opnieuw verifi\u00ebren is geslaagd", + "reauth_successful": "Herauthenticatie geslaagd", "unknown": "Onverwachte fout" }, "error": { - "cannot_connect": "Verbinding tot stand brengen is mislukt", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/vulcan/translations/ko.json b/homeassistant/components/vulcan/translations/ko.json index c98f79dd84b..f65f8ada294 100644 --- a/homeassistant/components/vulcan/translations/ko.json +++ b/homeassistant/components/vulcan/translations/ko.json @@ -1,6 +1,9 @@ { "config": { "step": { + "auth": { + "description": "\ubaa8\ubc14\uc77c \uc571 \ub4f1\ub85d \ud398\uc774\uc9c0\ub97c \uc0ac\uc6a9\ud558\uc5ec Vulcan \uacc4\uc815\uc5d0 \ub85c\uadf8\uc778\ud569\ub2c8\ub2e4." + }, "reauth_confirm": { "data": { "token": "\ud1a0\ud070" diff --git a/homeassistant/components/wallbox/translations/nl.json b/homeassistant/components/wallbox/translations/nl.json index 5cde7830b85..bedf5851a36 100644 --- a/homeassistant/components/wallbox/translations/nl.json +++ b/homeassistant/components/wallbox/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/water_heater/translations/nl.json b/homeassistant/components/water_heater/translations/nl.json index 4a2d1357188..05c8eaffc0e 100644 --- a/homeassistant/components/water_heater/translations/nl.json +++ b/homeassistant/components/water_heater/translations/nl.json @@ -11,9 +11,9 @@ "electric": "Elektriciteit", "gas": "Gas", "heat_pump": "Warmtepomp", - "high_demand": "Hoge vraag", + "high_demand": "Grote vraag", "off": "Uit", - "performance": "Prestaties" + "performance": "Prestatie" } } } \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/nl.json b/homeassistant/components/watttime/translations/nl.json index 72758f4a0d7..47e2fb9c600 100644 --- a/homeassistant/components/watttime/translations/nl.json +++ b/homeassistant/components/watttime/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "invalid_auth": "Ongeldige authenticatie", @@ -28,7 +28,7 @@ "password": "Wachtwoord" }, "description": "Voer het wachtwoord voor {username} opnieuw in:", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" }, "user": { "data": { diff --git a/homeassistant/components/waze_travel_time/translations/ko.json b/homeassistant/components/waze_travel_time/translations/ko.json index 3596754ca04..5b2dbd7ac46 100644 --- a/homeassistant/components/waze_travel_time/translations/ko.json +++ b/homeassistant/components/waze_travel_time/translations/ko.json @@ -16,5 +16,23 @@ "description": "\ucd9c\ubc1c\uc9c0 \ubc0f \ub3c4\ucc29\uc9c0\uc758 \uacbd\uc6b0 \uc704\uce58\uc758 \uc8fc\uc18c \ub610\ub294 GPS \uc88c\ud45c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. (GPS \uc88c\ud45c\ub294 \uc27c\ud45c\ub85c \uad6c\ubd84\ud574\uc57c \ud569\ub2c8\ub2e4) \ub610\ub294 \uc774\ub7ec\ud55c \uc815\ubcf4\ub97c \ud574\ub2f9 \uc0c1\ud0dc\ub85c \uc81c\uacf5\ud558\ub294 \uad6c\uc131\uc694\uc18c ID\ub098 \uc704\ub3c4 \ubc0f \uacbd\ub3c4 \uc18d\uc131\uc744 \uac00\uc9c4 \uad6c\uc131\uc694\uc18c ID \ub610\ub294 \uc9c0\uc5ed \uc774\ub984\uc744 \uc785\ub825\ud560 \uc218\ub3c4 \uc788\uc2b5\ub2c8\ub2e4." } } - } + }, + "options": { + "step": { + "init": { + "data": { + "avoid_ferries": "\ud398\ub9ac\ub97c \ud53c\ud558\uaca0\uc5b4\uc694?", + "avoid_subscription_roads": "\uace0\uc18d\ub3c4\ub85c/\uc720\ub8cc\ub3c4\ub85c\ub97c \ud53c\ud558\uaca0\uc5b4\uc694?", + "avoid_toll_roads": "\uc720\ub8cc\ub3c4\ub85c\ub97c \ud53c\ud558\uaca0\uc5b4\uc694?", + "excl_filter": "\uc120\ud0dd\ud55c \uacbd\ub85c\uc5d0\uc11c \ud558\uc704\ubb38\uc790\uc5f4\uc744 \uc81c\uc678\ud569\ub2c8\ub2e4.", + "incl_filter": "\uc120\ud0dd\ud55c \uacbd\ub85c\uc5d0\uc11c \ud558\uc704\ubb38\uc790\uc5f4\uc744 \ud3ec\ud568\ud569\ub2c8\ub2e4.", + "realtime": "\uc2e4\uc2dc\uac04 \uc774\ub3d9 \uc2dc\uac04?", + "units": "\ub2e8\uc704", + "vehicle_type": "\ucc28\ub7c9 \uc720\ud615" + }, + "description": "'\ud558\uc704 \ubb38\uc790\uc5f4'\uc785\ub825\uc744 \uc0ac\uc6a9\ud558\uba74 \ud1b5\ud569\uad6c\uc131\uc694\uc18c\uac00 \ud2b9\uc815 \uacbd\ub85c\ub97c \uc0ac\uc6a9\ud558\uac70\ub098 \ud2b9\uc815 \uacbd\ub85c\ub97c \ud53c\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + } + } + }, + "title": "Waze Travel Time" } \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/nl.json b/homeassistant/components/waze_travel_time/translations/nl.json index 223d69625d4..249b42f3063 100644 --- a/homeassistant/components/waze_travel_time/translations/nl.json +++ b/homeassistant/components/waze_travel_time/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd." + "already_configured": "Locatie is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken" diff --git a/homeassistant/components/weather/translations/nl.json b/homeassistant/components/weather/translations/nl.json index bab37782936..2fff10215ab 100644 --- a/homeassistant/components/weather/translations/nl.json +++ b/homeassistant/components/weather/translations/nl.json @@ -9,7 +9,7 @@ "lightning": "Bliksem", "lightning-rainy": "Bliksem, regenachtig", "partlycloudy": "Gedeeltelijk bewolkt", - "pouring": "Regen", + "pouring": "Gieten", "rainy": "Regenachtig", "snowy": "Sneeuwachtig", "snowy-rainy": "Sneeuw-, regenachtig", diff --git a/homeassistant/components/webostv/translations/nl.json b/homeassistant/components/webostv/translations/nl.json index 2a9fb59e1c1..f914287ce16 100644 --- a/homeassistant/components/webostv/translations/nl.json +++ b/homeassistant/components/webostv/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "error_pairing": "Verbonden met LG webOS TV maar niet gekoppeld" }, "error": { diff --git a/homeassistant/components/wemo/translations/nl.json b/homeassistant/components/wemo/translations/nl.json index 7fde5a7ef42..78b3b5f5a05 100644 --- a/homeassistant/components/wemo/translations/nl.json +++ b/homeassistant/components/wemo/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Geen apparaten gevonden op het netwerk", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "confirm": { diff --git a/homeassistant/components/withings/translations/nl.json b/homeassistant/components/withings/translations/nl.json index a7400e3a693..04da8b7eb0b 100644 --- a/homeassistant/components/withings/translations/nl.json +++ b/homeassistant/components/withings/translations/nl.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "Configuratie bijgewerkt voor profiel.", - "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})" + "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})" }, "create_entry": { "default": "Succesvol geverifieerd met Withings voor het geselecteerde profiel." @@ -26,7 +26,7 @@ }, "reauth": { "description": "Het {profile} \" moet opnieuw worden geverifieerd om Withings-gegevens te blijven ontvangen.", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" } } } diff --git a/homeassistant/components/wiz/translations/ko.json b/homeassistant/components/wiz/translations/ko.json new file mode 100644 index 00000000000..9c08b2d78b0 --- /dev/null +++ b/homeassistant/components/wiz/translations/ko.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "IP \uc8fc\uc18c\ub97c \ube44\uc6cc\ub450\uba74 \uc7a5\uce58\ub97c \uc790\ub3d9\uc73c\ub85c \ucc3e\uc2b5\ub2c8\ub2e4." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/nl.json b/homeassistant/components/xbox/translations/nl.json index 1e567e954d2..9d68863cc27 100644 --- a/homeassistant/components/xbox/translations/nl.json +++ b/homeassistant/components/xbox/translations/nl.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "create_entry": { - "default": "Succesvol geauthenticeerd" + "default": "Authenticatie geslaagd" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/xiaomi_aqara/translations/nl.json b/homeassistant/components/xiaomi_aqara/translations/nl.json index a549d01313b..bfdd88a30de 100644 --- a/homeassistant/components/xiaomi_aqara/translations/nl.json +++ b/homeassistant/components/xiaomi_aqara/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "not_xiaomi_aqara": "Geen Xiaomi Aqara Gateway, ontdekt apparaat kwam niet overeen met bekende gateways" }, "error": { diff --git a/homeassistant/components/xiaomi_miio/translations/nl.json b/homeassistant/components/xiaomi_miio/translations/nl.json index 219f7da0b4f..abb63ec15c5 100644 --- a/homeassistant/components/xiaomi_miio/translations/nl.json +++ b/homeassistant/components/xiaomi_miio/translations/nl.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "incomplete_info": "Onvolledige informatie voor het instellen van het apparaat, geen host of token opgegeven.", "not_xiaomi_miio": "Apparaat wordt (nog) niet ondersteund door Xiaomi Miio.", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -40,7 +40,7 @@ }, "reauth_confirm": { "description": "De Xiaomi Miio-integratie moet uw account opnieuw verifi\u00ebren om de tokens bij te werken of ontbrekende cloudreferenties toe te voegen.", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" }, "select": { "data": { diff --git a/homeassistant/components/yale_smart_alarm/translations/nl.json b/homeassistant/components/yale_smart_alarm/translations/nl.json index 8d8697bbc59..15956572847 100644 --- a/homeassistant/components/yale_smart_alarm/translations/nl.json +++ b/homeassistant/components/yale_smart_alarm/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "reauth_successful": "Herauthenticatie was succesvol" + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/yamaha_musiccast/translations/nl.json b/homeassistant/components/yamaha_musiccast/translations/nl.json index 8cb8265a1f0..e1e31149c06 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": "Wilt u beginnen met instellen?" + "description": "Wil je beginnen met instellen?" }, "user": { "data": { diff --git a/homeassistant/components/yeelight/translations/ko.json b/homeassistant/components/yeelight/translations/ko.json index b8a4385bfdf..d12c41b423e 100644 --- a/homeassistant/components/yeelight/translations/ko.json +++ b/homeassistant/components/yeelight/translations/ko.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, + "flow_title": "{model} {id} ({host})", "step": { + "discovery_confirm": { + "description": "{model} ( {host} )\uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + }, "pick_device": { "data": { "device": "\uae30\uae30" diff --git a/homeassistant/components/yeelight/translations/nl.json b/homeassistant/components/yeelight/translations/nl.json index 7767c56e7fb..540191e9222 100644 --- a/homeassistant/components/yeelight/translations/nl.json +++ b/homeassistant/components/yeelight/translations/nl.json @@ -5,7 +5,7 @@ "no_devices_found": "Geen apparaten gevonden op het netwerk" }, "error": { - "cannot_connect": "Kon niet verbinden" + "cannot_connect": "Kan geen verbinding maken" }, "flow_title": "{model} {id} ({host})", "step": { diff --git a/homeassistant/components/yolink/translations/nl.json b/homeassistant/components/yolink/translations/nl.json index ae9739b3bd6..401738dc381 100644 --- a/homeassistant/components/yolink/translations/nl.json +++ b/homeassistant/components/yolink/translations/nl.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "Account is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", - "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De component is niet geconfigureerd. Gelieve de documentatie volgen.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [check de helpsectie]({docs_url})", - "oauth_error": "Ongeldige token data ontvangen.", - "reauth_successful": "Herauthenticatie was succesvol" + "already_in_progress": "De configuratie is momenteel al bezig", + "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", + "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})", + "oauth_error": "Ongeldige tokengegevens ontvangen.", + "reauth_successful": "Herauthenticatie geslaagd" }, "create_entry": { - "default": "Succesvol geauthenticeerd" + "default": "Authenticatie geslaagd" }, "step": { "pick_implementation": { @@ -18,7 +18,7 @@ }, "reauth_confirm": { "description": "De yolink-integratie moet opnieuw inloggen bij uw account", - "title": "Verifieer de integratie opnieuw" + "title": "Integratie herauthentiseren" } } } diff --git a/homeassistant/components/yolink/translations/pl.json b/homeassistant/components/yolink/translations/pl.json new file mode 100644 index 00000000000..5cddd2a4944 --- /dev/null +++ b/homeassistant/components/yolink/translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", + "oauth_error": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono" + }, + "step": { + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + }, + "reauth_confirm": { + "description": "Integracja yolink wymaga ponownego uwierzytelnienia Twojego konta.", + "title": "Ponownie uwierzytelnij integracj\u0119" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/nl.json b/homeassistant/components/zerproc/translations/nl.json index 0671f0b3674..6fc4a03e824 100644 --- a/homeassistant/components/zerproc/translations/nl.json +++ b/homeassistant/components/zerproc/translations/nl.json @@ -2,11 +2,11 @@ "config": { "abort": { "no_devices_found": "Geen apparaten gevonden op het netwerk", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "step": { "confirm": { - "description": "Wilt u beginnen met instellen?" + "description": "Wil je beginnen met instellen?" } } } diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json index 54692b22598..1de0e24def3 100644 --- a/homeassistant/components/zha/translations/nl.json +++ b/homeassistant/components/zha/translations/nl.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_zha_device": "Dit apparaat is niet een zha-apparaat.", - "single_instance_allowed": "Al geconfigureerd. Slechts een enkele configuratie mogelijk.", + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", "usb_probe_failed": "Kon het USB apparaat niet onderzoeken" }, "error": { diff --git a/homeassistant/components/zodiac/translations/sensor.nl.json b/homeassistant/components/zodiac/translations/sensor.nl.json index 6dba645ed83..23088661aec 100644 --- a/homeassistant/components/zodiac/translations/sensor.nl.json +++ b/homeassistant/components/zodiac/translations/sensor.nl.json @@ -6,7 +6,7 @@ "cancer": "Kreeft", "capricorn": "Steenbok", "gemini": "Tweelingen", - "leo": "Leo", + "leo": "Leeuw", "libra": "Weegschaal", "pisces": "Vissen", "sagittarius": "Boogschutter", diff --git a/homeassistant/components/zoneminder/translations/nl.json b/homeassistant/components/zoneminder/translations/nl.json index 8aed5085391..5dbf8ca0d3d 100644 --- a/homeassistant/components/zoneminder/translations/nl.json +++ b/homeassistant/components/zoneminder/translations/nl.json @@ -11,7 +11,7 @@ }, "error": { "auth_fail": "Gebruikersnaam of wachtwoord is onjuist.", - "cannot_connect": "Kon niet verbinden", + "cannot_connect": "Kan geen verbinding maken", "connection_error": "Kan geen verbinding maken met een ZoneMinder-server.", "invalid_auth": "Ongeldige authenticatie" }, @@ -23,9 +23,9 @@ "password": "Wachtwoord", "path": "ZM-pad", "path_zms": "ZMS-pad", - "ssl": "Gebruik een SSL-certificaat", + "ssl": "Maakt gebruik van een SSL-certificaat", "username": "Gebruikersnaam", - "verify_ssl": "Verifieer SSL-certificaat" + "verify_ssl": "SSL-certificaat verifi\u00ebren" }, "title": "Voeg ZoneMinder server toe." } diff --git a/homeassistant/components/zwave_js/translations/ko.json b/homeassistant/components/zwave_js/translations/ko.json index 7bf9dc8f062..0d00d27fea2 100644 --- a/homeassistant/components/zwave_js/translations/ko.json +++ b/homeassistant/components/zwave_js/translations/ko.json @@ -8,7 +8,9 @@ "addon_start_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc744 \uc2dc\uc791\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4", - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "discovery_requires_supervisor": "\uc7a5\uce58\uac80\uc0c9\uc744 \uc704\ud574 supervisor\uac00 \ud544\uc694\ud569\ub2c8\ub2e4.", + "not_zwave_device": "\ubc1c\uacac\ub41c \uc7a5\uce58\uac00 Z-Wave \uc7a5\uce58\uac00 \uc544\ub2d9\ub2c8\ub2e4." }, "error": { "addon_start_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc744 \uc2dc\uc791\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uad6c\uc131 \ub0b4\uc6a9\uc744 \ud655\uc778\ud574\uc8fc\uc138\uc694.", @@ -47,6 +49,9 @@ }, "start_addon": { "title": "Z-Wave JS \uc560\ub4dc\uc628\uc774 \uc2dc\uc791\ud558\ub294 \uc911\uc785\ub2c8\ub2e4." + }, + "zeroconf_confirm": { + "title": "Z-Wave JS \uc11c\ubc84 \ubc1c\uacac" } } }, diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json index 69481c2a98c..f87b7a701e3 100644 --- a/homeassistant/components/zwave_js/translations/nl.json +++ b/homeassistant/components/zwave_js/translations/nl.json @@ -7,7 +7,7 @@ "addon_set_config_failed": "Instellen van de Z-Wave JS configuratie is mislukt.", "addon_start_failed": "Kan de Z-Wave JS add-on niet starten.", "already_configured": "Apparaat is al geconfigureerd", - "already_in_progress": "De configuratiestroom is al aan de gang", + "already_in_progress": "De configuratie is momenteel al bezig", "cannot_connect": "Kan geen verbinding maken", "discovery_requires_supervisor": "Ontdekking vereist de Supervisor.", "not_zwave_device": "Het ontdekte apparaat is niet een Z-Wave apparaat." From eac872331aa1b49a1ed77495d1125ed8b2f87cb1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 20 May 2022 18:06:31 -0700 Subject: [PATCH 0728/3516] Bump frontend to 20220521.0 (#72257) --- 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 5cf48e76d4b..215796d2df8 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220518.0"], + "requirements": ["home-assistant-frontend==20220521.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4e6174db561..09b3aca9d01 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220518.0 +home-assistant-frontend==20220521.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index d74741129d3..5747a6d0709 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -822,7 +822,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220518.0 +home-assistant-frontend==20220521.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ca019992c7..c22a565006e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -589,7 +589,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220518.0 +home-assistant-frontend==20220521.0 # homeassistant.components.home_connect homeconnect==0.7.0 From 3918059033e41b1fe44b8e38a733960ad56eed5b Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 21 May 2022 09:54:51 +0200 Subject: [PATCH 0729/3516] Move manual configuration of MQTT button to the integration key (#72167) Add button Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 1 + homeassistant/components/mqtt/button.py | 29 +++++++++++++++++++---- tests/components/mqtt/test_button.py | 13 ++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e1de984f4df..02a98f6ce90 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -191,6 +191,7 @@ MQTT_WILL_BIRTH_SCHEMA = vol.Schema( PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( { vol.Optional(Platform.ALARM_CONTROL_PANEL.value): cv.ensure_list, + vol.Optional(Platform.BUTTON.value): cv.ensure_list, vol.Optional(Platform.FAN.value): cv.ensure_list, vol.Optional(Platform.LIGHT.value): cv.ensure_list, } diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index 22ee7b6d5ae..47e96ff3e1a 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -1,6 +1,7 @@ """Support for MQTT buttons.""" from __future__ import annotations +import asyncio import functools import voluptuous as vol @@ -26,15 +27,17 @@ from .const import ( from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_get_platform_config_from_yaml, async_setup_entry_helper, async_setup_platform_helper, + warn_for_legacy_schema, ) CONF_PAYLOAD_PRESS = "payload_press" DEFAULT_NAME = "MQTT Button" DEFAULT_PAYLOAD_PRESS = "PRESS" -PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, @@ -45,7 +48,14 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -DISCOVERY_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA) +# Configuring MQTT Buttons under the button platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA = vol.All( + cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_MODERN.schema), + warn_for_legacy_schema(button.DOMAIN), +) + + +DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) async def async_setup_platform( @@ -54,7 +64,8 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up MQTT button through configuration.yaml.""" + """Set up MQTT button configured under the fan platform key (deprecated).""" + # Deprecated in HA Core 2022.6 await async_setup_platform_helper( hass, button.DOMAIN, config, async_add_entities, _async_setup_entity ) @@ -65,7 +76,17 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT button dynamically through MQTT discovery.""" + """Set up MQTT button through configuration.yaml and dynamically through MQTT discovery.""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, button.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 83ef7a42705..941f08e541c 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -30,6 +30,7 @@ from .test_common import ( help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -413,3 +414,15 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): domain = button.DOMAIN config = DEFAULT_CONFIG[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = button.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None From 4710ad07c4da52590d805334c30878a10cd4db7d Mon Sep 17 00:00:00 2001 From: Marcio Granzotto Rodrigues Date: Sat, 21 May 2022 05:27:59 -0300 Subject: [PATCH 0730/3516] Add marciogranzotto as a Bond codeowner (#72238) --- CODEOWNERS | 4 ++-- homeassistant/components/bond/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index c3e36d1fef7..4e1dcd5d93e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -137,8 +137,8 @@ build.json @home-assistant/supervisor /homeassistant/components/bluesound/ @thrawnarn /homeassistant/components/bmw_connected_drive/ @gerard33 @rikroe /tests/components/bmw_connected_drive/ @gerard33 @rikroe -/homeassistant/components/bond/ @bdraco @prystupa @joshs85 -/tests/components/bond/ @bdraco @prystupa @joshs85 +/homeassistant/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto +/tests/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto /homeassistant/components/bosch_shc/ @tschamm /tests/components/bosch_shc/ @tschamm /homeassistant/components/braviatv/ @bieniu @Drafteed diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index b011a5f2dae..187602057c0 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.18"], "zeroconf": ["_bond._tcp.local."], - "codeowners": ["@bdraco", "@prystupa", "@joshs85"], + "codeowners": ["@bdraco", "@prystupa", "@joshs85", "@marciogranzotto"], "quality_scale": "platinum", "iot_class": "local_push", "loggers": ["bond_api"] From 72dbca4f5bc3a40ac96b2c8fc355ce85bedd003c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 21 May 2022 05:20:37 -0400 Subject: [PATCH 0731/3516] Address late feedback on Deluge config flow (#71497) Address late feedback on Deluge --- homeassistant/components/deluge/const.py | 10 ++- .../components/deluge/coordinator.py | 12 +-- homeassistant/components/deluge/sensor.py | 76 +++++++++++-------- 3 files changed, 54 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/deluge/const.py b/homeassistant/components/deluge/const.py index 505c20e860f..704c024c41b 100644 --- a/homeassistant/components/deluge/const.py +++ b/homeassistant/components/deluge/const.py @@ -1,12 +1,16 @@ """Constants for the Deluge integration.""" import logging +from typing import Final CONF_WEB_PORT = "web_port" +CURRENT_STATUS = "current_status" +DATA_KEYS = ["upload_rate", "download_rate", "dht_upload_rate", "dht_download_rate"] DEFAULT_NAME = "Deluge" DEFAULT_RPC_PORT = 58846 DEFAULT_WEB_PORT = 8112 -DHT_UPLOAD = 1000 -DHT_DOWNLOAD = 1000 -DOMAIN = "deluge" +DOMAIN: Final = "deluge" +DOWNLOAD_SPEED = "download_speed" LOGGER = logging.getLogger(__package__) + +UPLOAD_SPEED = "upload_speed" diff --git a/homeassistant/components/deluge/coordinator.py b/homeassistant/components/deluge/coordinator.py index 0ac97e77674..89f9afc31ad 100644 --- a/homeassistant/components/deluge/coordinator.py +++ b/homeassistant/components/deluge/coordinator.py @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import LOGGER +from .const import DATA_KEYS, LOGGER class DelugeDataUpdateCoordinator(DataUpdateCoordinator): @@ -38,16 +38,12 @@ class DelugeDataUpdateCoordinator(DataUpdateCoordinator): """Get the latest data from Deluge and updates the state.""" data = {} try: - data[Platform.SENSOR] = await self.hass.async_add_executor_job( + _data = await self.hass.async_add_executor_job( self.api.call, "core.get_session_status", - [ - "upload_rate", - "download_rate", - "dht_upload_rate", - "dht_download_rate", - ], + DATA_KEYS, ) + data[Platform.SENSOR] = {k.decode(): v for k, v in _data.items()} data[Platform.SWITCH] = await self.hass.async_add_executor_job( self.api.call, "core.get_torrents_status", {}, ["paused"] ) diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index bad535def96..8a8e8f64657 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -1,6 +1,10 @@ """Support for monitoring the Deluge BitTorrent client API.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, @@ -13,25 +17,52 @@ from homeassistant.helpers import entity_platform from homeassistant.helpers.typing import StateType from . import DelugeEntity -from .const import DOMAIN +from .const import CURRENT_STATUS, DATA_KEYS, DOMAIN, DOWNLOAD_SPEED, UPLOAD_SPEED from .coordinator import DelugeDataUpdateCoordinator -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="current_status", + +def get_state(data: dict[str, float], key: str) -> str | float: + """Get current download/upload state.""" + upload = data[DATA_KEYS[0]] - data[DATA_KEYS[2]] + download = data[DATA_KEYS[1]] - data[DATA_KEYS[3]] + if key == CURRENT_STATUS: + if upload > 0 and download > 0: + return "Up/Down" + if upload > 0 and download == 0: + return "Seeding" + if upload == 0 and download > 0: + return "Downloading" + return STATE_IDLE + kb_spd = float(upload if key == UPLOAD_SPEED else download) / 1024 + return round(kb_spd, 2 if kb_spd < 0.1 else 1) + + +@dataclass +class DelugeSensorEntityDescription(SensorEntityDescription): + """Class to describe a Deluge sensor.""" + + value: Callable[[dict[str, float]], Any] = lambda val: val + + +SENSOR_TYPES: tuple[DelugeSensorEntityDescription, ...] = ( + DelugeSensorEntityDescription( + key=CURRENT_STATUS, name="Status", + value=lambda data: get_state(data, CURRENT_STATUS), ), - SensorEntityDescription( - key="download_speed", + DelugeSensorEntityDescription( + key=DOWNLOAD_SPEED, name="Down Speed", native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, + value=lambda data: get_state(data, DOWNLOAD_SPEED), ), - SensorEntityDescription( - key="upload_speed", + DelugeSensorEntityDescription( + key=UPLOAD_SPEED, name="Up Speed", native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, + value=lambda data: get_state(data, UPLOAD_SPEED), ), ) @@ -51,10 +82,12 @@ async def async_setup_entry( class DelugeSensor(DelugeEntity, SensorEntity): """Representation of a Deluge sensor.""" + entity_description: DelugeSensorEntityDescription + def __init__( self, coordinator: DelugeDataUpdateCoordinator, - description: SensorEntityDescription, + description: DelugeSensorEntityDescription, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) @@ -65,27 +98,4 @@ class DelugeSensor(DelugeEntity, SensorEntity): @property def native_value(self) -> StateType: """Return the state of the sensor.""" - if self.coordinator.data: - data = self.coordinator.data[Platform.SENSOR] - upload = data[b"upload_rate"] - data[b"dht_upload_rate"] - download = data[b"download_rate"] - data[b"dht_download_rate"] - if self.entity_description.key == "current_status": - if data: - if upload > 0 and download > 0: - return "Up/Down" - if upload > 0 and download == 0: - return "Seeding" - if upload == 0 and download > 0: - return "Downloading" - return STATE_IDLE - - if data: - if self.entity_description.key == "download_speed": - kb_spd = float(download) - kb_spd = kb_spd / 1024 - return round(kb_spd, 2 if kb_spd < 0.1 else 1) - if self.entity_description.key == "upload_speed": - kb_spd = float(upload) - kb_spd = kb_spd / 1024 - return round(kb_spd, 2 if kb_spd < 0.1 else 1) - return None + return self.entity_description.value(self.coordinator.data[Platform.SENSOR]) From 6c4ba07bd1fffd0be2b76da51f18bd092b2d54df Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 21 May 2022 12:37:47 +0200 Subject: [PATCH 0732/3516] Remove combined translations / optional markers from Konnected (#72252) --- homeassistant/components/konnected/strings.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/konnected/strings.json b/homeassistant/components/konnected/strings.json index 905597035d5..cd08638c775 100644 --- a/homeassistant/components/konnected/strings.json +++ b/homeassistant/components/konnected/strings.json @@ -63,7 +63,7 @@ "description": "{zone} options", "data": { "type": "Binary Sensor Type", - "name": "[%key:common::config_flow::data::name%] (optional)", + "name": "[%key:common::config_flow::data::name%]", "inverse": "Invert the open/close state" } }, @@ -72,19 +72,19 @@ "description": "{zone} options", "data": { "type": "Sensor Type", - "name": "[%key:common::config_flow::data::name%] (optional)", - "poll_interval": "Poll Interval (minutes) (optional)" + "name": "[%key:common::config_flow::data::name%]", + "poll_interval": "Poll Interval (minutes)" } }, "options_switch": { "title": "Configure Switchable Output", "description": "{zone} options: state {state}", "data": { - "name": "[%key:common::config_flow::data::name%] (optional)", + "name": "[%key:common::config_flow::data::name%]", "activation": "Output when on", - "momentary": "Pulse duration (ms) (optional)", - "pause": "Pause between pulses (ms) (optional)", - "repeat": "Times to repeat (-1=infinite) (optional)", + "momentary": "Pulse duration (ms)", + "pause": "Pause between pulses (ms)", + "repeat": "Times to repeat (-1=infinite)", "more_states": "Configure additional states for this zone" } }, @@ -95,7 +95,7 @@ "discovery": "Respond to discovery requests on your network", "blink": "Blink panel LED on when sending state change", "override_api_host": "Override default Home Assistant API host panel URL", - "api_host": "Override API host URL (optional)" + "api_host": "Override API host URL" } } }, From a152449b72ee2db3f06ae2f87ed9b3d5202d04eb Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 21 May 2022 07:31:24 -0400 Subject: [PATCH 0733/3516] Fix steam yaml import (#72245) * Fix steam yaml import * uno mas --- homeassistant/components/steam_online/config_flow.py | 12 ++++++++++-- homeassistant/components/steam_online/coordinator.py | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/steam_online/config_flow.py b/homeassistant/components/steam_online/config_flow.py index 246d54c0bff..91965685969 100644 --- a/homeassistant/components/steam_online/config_flow.py +++ b/homeassistant/components/steam_online/config_flow.py @@ -23,11 +23,16 @@ from .const import ( ) -def validate_input(user_input: dict[str, str | int]) -> list[dict[str, str | int]]: +def validate_input( + user_input: dict[str, str | int], multi: bool = False +) -> list[dict[str, str | int]]: """Handle common flow input validation.""" steam.api.key.set(user_input[CONF_API_KEY]) interface = steam.api.interface("ISteamUser") - names = interface.GetPlayerSummaries(steamids=user_input[CONF_ACCOUNT]) + if multi: + names = interface.GetPlayerSummaries(steamids=user_input[CONF_ACCOUNTS]) + else: + names = interface.GetPlayerSummaries(steamids=user_input[CONF_ACCOUNT]) return names["response"]["players"]["player"] @@ -75,6 +80,9 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="reauth_successful") self._abort_if_unique_id_configured() if self.source == config_entries.SOURCE_IMPORT: + res = await self.hass.async_add_executor_job( + validate_input, user_input, True + ) accounts_data = { CONF_ACCOUNTS: { acc["steamid"]: acc["personaname"] for acc in res diff --git a/homeassistant/components/steam_online/coordinator.py b/homeassistant/components/steam_online/coordinator.py index 78c850b0ac9..f210c449fa6 100644 --- a/homeassistant/components/steam_online/coordinator.py +++ b/homeassistant/components/steam_online/coordinator.py @@ -56,7 +56,7 @@ class SteamDataUpdateCoordinator(DataUpdateCoordinator): } for k in players: data = self.player_interface.GetSteamLevel(steamid=players[k]["steamid"]) - players[k]["level"] = data["response"]["player_level"] + players[k]["level"] = data["response"].get("player_level") return players async def _async_update_data(self) -> dict[str, dict[str, str | int]]: From 17588c39a44e510653268266d9a167643f2e874b Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 21 May 2022 16:14:24 +0200 Subject: [PATCH 0734/3516] Move manual configuration of MQTT binary_sensor to the integration key (#72183) Add binary_sensor Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 1 + .../components/mqtt/binary_sensor.py | 28 ++++++++++++++++--- tests/components/mqtt/test_binary_sensor.py | 13 +++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 02a98f6ce90..f723d8da3ca 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -191,6 +191,7 @@ MQTT_WILL_BIRTH_SCHEMA = vol.Schema( PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( { vol.Optional(Platform.ALARM_CONTROL_PANEL.value): cv.ensure_list, + vol.Optional(Platform.BINARY_SENSOR.value): cv.ensure_list, vol.Optional(Platform.BUTTON.value): cv.ensure_list, vol.Optional(Platform.FAN.value): cv.ensure_list, vol.Optional(Platform.LIGHT.value): cv.ensure_list, diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 5d0da99d786..b9ab190cc9b 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -1,6 +1,7 @@ """Support for MQTT binary sensors.""" from __future__ import annotations +import asyncio from datetime import timedelta import functools import logging @@ -41,8 +42,10 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, + async_get_platform_config_from_yaml, async_setup_entry_helper, async_setup_platform_helper, + warn_for_legacy_schema, ) _LOGGER = logging.getLogger(__name__) @@ -54,7 +57,7 @@ DEFAULT_PAYLOAD_ON = "ON" DEFAULT_FORCE_UPDATE = False CONF_EXPIRE_AFTER = "expire_after" -PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RO_SCHEMA.extend( { vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, @@ -66,7 +69,13 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -DISCOVERY_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA) +# Configuring MQTT Binary sensors under the binary_sensor platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA = vol.All( + cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_MODERN.schema), + warn_for_legacy_schema(binary_sensor.DOMAIN), +) + +DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) async def async_setup_platform( @@ -75,7 +84,8 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up MQTT binary sensor through configuration.yaml.""" + """Set up MQTT binary sensor configured under the fan platform key (deprecated).""" + # Deprecated in HA Core 2022.6 await async_setup_platform_helper( hass, binary_sensor.DOMAIN, config, async_add_entities, _async_setup_entity ) @@ -86,7 +96,17 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT binary sensor dynamically through MQTT discovery.""" + """Set up MQTT binary sensor through configuration.yaml and dynamically through MQTT discovery.""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, binary_sensor.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 5055550be7c..e4a48b07940 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -41,6 +41,7 @@ from .test_common import ( help_test_reloadable_late, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -973,3 +974,15 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( assert await async_setup_component(hass, domain, {domain: config3}) await hass.async_block_till_done() assert "Skip state recovery after reload for binary_sensor.test3" in caplog.text + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = binary_sensor.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None From dd0f9350ac52e31d598b36ddbda049b22c8096c9 Mon Sep 17 00:00:00 2001 From: Michael Chisholm Date: Sun, 22 May 2022 00:18:47 +1000 Subject: [PATCH 0735/3516] Update async-upnp-client to 0.30.0 (#72269) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/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 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index e8cdb282118..cae136cd8fd 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.29.0"], + "requirements": ["async-upnp-client==0.30.0"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index 20c38a5be3f..58c79a048b4 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dms", - "requirements": ["async-upnp-client==0.29.0"], + "requirements": ["async-upnp-client==0.30.0"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 9391fe5e311..bef293185a6 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -7,7 +7,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.5.0", "wakeonlan==2.0.1", - "async-upnp-client==0.29.0" + "async-upnp-client==0.30.0" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 993a0033b1b..cacce09e1f2 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.29.0"], + "requirements": ["async-upnp-client==0.30.0"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 94d9c8ab058..2bf1786ccbe 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.29.0", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.30.0", "getmac==0.8.2"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman", "@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 3df38a41995..853c95abcbb 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.10", "async-upnp-client==0.29.0"], + "requirements": ["yeelight==0.7.10", "async-upnp-client==0.30.0"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 09b3aca9d01..1a61d2ad2b0 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.11 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.29.0 +async-upnp-client==0.30.0 async_timeout==4.0.2 atomicwrites==1.4.0 attrs==21.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 5747a6d0709..4c88b57b9f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -339,7 +339,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.29.0 +async-upnp-client==0.30.0 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c22a565006e..ca6b5fd1628 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -281,7 +281,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.29.0 +async-upnp-client==0.30.0 # homeassistant.components.sleepiq asyncsleepiq==1.2.3 From f1ac9f8ccaf9ac052e3d542c4a89cf8ba7e9c75b Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Sat, 21 May 2022 16:36:02 +0200 Subject: [PATCH 0736/3516] Add ConfigFlow for here_travel_time (#69212) * Add ConfigFlow for here_travel_time * Use Selectors and Menu * Use separate config flow steps for menus * Move time options together * Update homeassistant/components/here_travel_time/config_flow.py Co-authored-by: Allen Porter * Blacken config_flow * Initialize _config * Only catch HERE errors * Fix unknown error test * Implement async_step_import * Only catch errors for validate_api_key * Split lat/lon * Add additional test coverage * Use TimeSelector in option flow * Assert config entry data/option Co-authored-by: Allen Porter --- .../components/here_travel_time/__init__.py | 146 ++++- .../here_travel_time/config_flow.py | 369 +++++++++++ .../components/here_travel_time/const.py | 15 +- .../components/here_travel_time/manifest.json | 1 + .../components/here_travel_time/model.py | 8 +- .../components/here_travel_time/sensor.py | 191 ++---- .../components/here_travel_time/strings.json | 82 +++ homeassistant/generated/config_flows.py | 1 + tests/components/here_travel_time/conftest.py | 15 + .../fixtures/empty_attribution_response.json | 131 ++++ .../here_travel_time/test_config_flow.py | 589 ++++++++++++++++++ .../components/here_travel_time/test_init.py | 48 ++ .../here_travel_time/test_sensor.py | 484 +++++++------- 13 files changed, 1686 insertions(+), 394 deletions(-) create mode 100644 homeassistant/components/here_travel_time/config_flow.py create mode 100644 homeassistant/components/here_travel_time/strings.json create mode 100644 tests/components/here_travel_time/fixtures/empty_attribution_response.json create mode 100644 tests/components/here_travel_time/test_config_flow.py create mode 100644 tests/components/here_travel_time/test_init.py diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index 0ad344e3fdf..310fe97fad8 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -8,7 +8,15 @@ import async_timeout from herepy import NoRouteFoundError, RouteMode, RoutingApi, RoutingResponse import voluptuous as vol -from homeassistant.const import ATTR_ATTRIBUTION, CONF_UNIT_SYSTEM_IMPERIAL, Platform +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_API_KEY, + CONF_MODE, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + Platform, +) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.location import find_coordinates @@ -24,9 +32,20 @@ from .const import ( ATTR_ORIGIN, ATTR_ORIGIN_NAME, ATTR_ROUTE, + CONF_ARRIVAL_TIME, + CONF_DEPARTURE_TIME, + CONF_DESTINATION_ENTITY_ID, + CONF_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE, + CONF_ORIGIN_ENTITY_ID, + CONF_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE, + CONF_ROUTE_MODE, + CONF_TRAFFIC_MODE, DEFAULT_SCAN_INTERVAL, DOMAIN, NO_ROUTE_ERROR_MESSAGE, + ROUTE_MODE_FASTEST, TRAFFIC_MODE_ENABLED, TRAVEL_MODES_VEHICLE, ) @@ -37,6 +56,74 @@ PLATFORMS = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up HERE Travel Time from a config entry.""" + api_key = config_entry.data[CONF_API_KEY] + here_client = RoutingApi(api_key) + setup_options(hass, config_entry) + + arrival = ( + dt.parse_time(config_entry.options[CONF_ARRIVAL_TIME]) + if config_entry.options[CONF_ARRIVAL_TIME] is not None + else None + ) + departure = ( + dt.parse_time(config_entry.options[CONF_DEPARTURE_TIME]) + if config_entry.options[CONF_DEPARTURE_TIME] is not None + else None + ) + + here_travel_time_config = HERETravelTimeConfig( + destination_latitude=config_entry.data.get(CONF_DESTINATION_LATITUDE), + destination_longitude=config_entry.data.get(CONF_DESTINATION_LONGITUDE), + destination_entity_id=config_entry.data.get(CONF_DESTINATION_ENTITY_ID), + origin_latitude=config_entry.data.get(CONF_ORIGIN_LATITUDE), + origin_longitude=config_entry.data.get(CONF_ORIGIN_LONGITUDE), + origin_entity_id=config_entry.data.get(CONF_ORIGIN_ENTITY_ID), + travel_mode=config_entry.data[CONF_MODE], + route_mode=config_entry.options[CONF_ROUTE_MODE], + units=config_entry.options[CONF_UNIT_SYSTEM], + arrival=arrival, + departure=departure, + ) + + coordinator = HereTravelTimeDataUpdateCoordinator( + hass, + here_client, + here_travel_time_config, + ) + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + + return True + + +def setup_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Set up options for a config entry if not set.""" + if not config_entry.options: + hass.config_entries.async_update_entry( + config_entry, + options={ + CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_ARRIVAL_TIME: None, + CONF_DEPARTURE_TIME: None, + CONF_UNIT_SYSTEM: hass.config.units.name, + }, + ) + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms( + config_entry, PLATFORMS + ) + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok + + class HereTravelTimeDataUpdateCoordinator(DataUpdateCoordinator): """HERETravelTime DataUpdateCoordinator.""" @@ -135,33 +222,40 @@ class HereTravelTimeDataUpdateCoordinator(DataUpdateCoordinator): ) -> tuple[list[str], list[str], str | None, str | None]: """Prepare parameters for the HERE api.""" - if self.config.origin_entity_id is not None: - origin = find_coordinates(self.hass, self.config.origin_entity_id) - else: - origin = self.config.origin + def _from_entity_id(entity_id: str) -> list[str]: + coordinates = find_coordinates(self.hass, entity_id) + if coordinates is None: + raise InvalidCoordinatesException( + f"No coordinatnes found for {entity_id}" + ) + try: + here_formatted_coordinates = coordinates.split(",") + vol.Schema(cv.gps(here_formatted_coordinates)) + except (AttributeError, vol.Invalid) as ex: + raise InvalidCoordinatesException( + f"{coordinates} are not valid coordinates" + ) from ex + return here_formatted_coordinates + # Destination if self.config.destination_entity_id is not None: - destination = find_coordinates(self.hass, self.config.destination_entity_id) + destination = _from_entity_id(self.config.destination_entity_id) else: - destination = self.config.destination - if destination is None: - raise InvalidCoordinatesException("Destination must be configured") - try: - here_formatted_destination = destination.split(",") - vol.Schema(cv.gps(here_formatted_destination)) - except (vol.Invalid) as ex: - raise InvalidCoordinatesException( - f"{destination} are not valid coordinates" - ) from ex - if origin is None: - raise InvalidCoordinatesException("Origin must be configured") - try: - here_formatted_origin = origin.split(",") - vol.Schema(cv.gps(here_formatted_origin)) - except (AttributeError, vol.Invalid) as ex: - raise InvalidCoordinatesException( - f"{origin} are not valid coordinates" - ) from ex + destination = [ + str(self.config.destination_latitude), + str(self.config.destination_longitude), + ] + + # Origin + if self.config.origin_entity_id is not None: + origin = _from_entity_id(self.config.origin_entity_id) + else: + origin = [ + str(self.config.origin_latitude), + str(self.config.origin_longitude), + ] + + # Arrival/Departure arrival: str | None = None departure: str | None = None if self.config.arrival is not None: @@ -172,7 +266,7 @@ class HereTravelTimeDataUpdateCoordinator(DataUpdateCoordinator): if arrival is None and departure is None: departure = "now" - return (here_formatted_origin, here_formatted_destination, arrival, departure) + return (origin, destination, arrival, departure) def build_hass_attribution(source_attribution: dict) -> str | None: diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py new file mode 100644 index 00000000000..bc6d57aa892 --- /dev/null +++ b/homeassistant/components/here_travel_time/config_flow.py @@ -0,0 +1,369 @@ +"""Config flow for HERE Travel Time integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from herepy import HEREError, InvalidCredentialsError, RouteMode, RoutingApi +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME, CONF_UNIT_SYSTEM +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.selector import ( + EntitySelector, + LocationSelector, + TimeSelector, + selector, +) + +from .const import ( + CONF_ARRIVAL, + CONF_ARRIVAL_TIME, + CONF_DEPARTURE, + CONF_DEPARTURE_TIME, + CONF_ROUTE_MODE, + CONF_TRAFFIC_MODE, + DEFAULT_NAME, + DOMAIN, + ROUTE_MODE_FASTEST, + ROUTE_MODES, + TRAFFIC_MODE_DISABLED, + TRAFFIC_MODE_ENABLED, + TRAFFIC_MODES, + TRAVEL_MODE_CAR, + TRAVEL_MODE_PUBLIC_TIME_TABLE, + TRAVEL_MODES, + UNITS, +) +from .sensor import ( + CONF_DESTINATION_ENTITY_ID, + CONF_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE, + CONF_ORIGIN_ENTITY_ID, + CONF_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE, +) + +_LOGGER = logging.getLogger(__name__) + + +def is_dupe_import( + entry: config_entries.ConfigEntry, + user_input: dict[str, Any], + options: dict[str, Any], +) -> bool: + """Return whether imported config already exists.""" + # Check the main data keys + if any( + user_input[key] != entry.data[key] + for key in (CONF_API_KEY, CONF_MODE, CONF_NAME) + ): + return False + + # Check origin/destination + for key in ( + CONF_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE, + CONF_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE, + CONF_DESTINATION_ENTITY_ID, + CONF_ORIGIN_ENTITY_ID, + ): + if user_input.get(key) != entry.data.get(key): + return False + + # We have to check for options that don't have defaults + for key in ( + CONF_TRAFFIC_MODE, + CONF_UNIT_SYSTEM, + CONF_ROUTE_MODE, + CONF_ARRIVAL_TIME, + CONF_DEPARTURE_TIME, + ): + if options.get(key) != entry.options.get(key): + return False + + return True + + +def validate_api_key(api_key: str) -> None: + """Validate the user input allows us to connect.""" + known_working_origin = [38.9, -77.04833] + known_working_destination = [39.0, -77.1] + RoutingApi(api_key).public_transport_timetable( + known_working_origin, + known_working_destination, + True, + [ + RouteMode[ROUTE_MODE_FASTEST], + RouteMode[TRAVEL_MODE_CAR], + RouteMode[TRAFFIC_MODE_ENABLED], + ], + arrival=None, + departure="now", + ) + + +def get_user_step_schema(data: dict[str, Any]) -> vol.Schema: + """Get a populated schema or default.""" + return vol.Schema( + { + vol.Optional( + CONF_NAME, default=data.get(CONF_NAME, DEFAULT_NAME) + ): cv.string, + vol.Required(CONF_API_KEY, default=data.get(CONF_API_KEY)): cv.string, + vol.Optional( + CONF_MODE, default=data.get(CONF_MODE, TRAVEL_MODE_CAR) + ): vol.In(TRAVEL_MODES), + } + ) + + +class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for HERE Travel Time.""" + + VERSION = 1 + + _config: dict[str, Any] = {} + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> HERETravelTimeOptionsFlow: + """Get the options flow.""" + return HERETravelTimeOptionsFlow(config_entry) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + user_input = user_input or {} + if user_input: + try: + await self.hass.async_add_executor_job( + validate_api_key, user_input[CONF_API_KEY] + ) + except InvalidCredentialsError: + errors["base"] = "invalid_auth" + except HEREError as error: + _LOGGER.exception("Unexpected exception: %s", error) + errors["base"] = "unknown" + if not errors: + self._config = user_input + return self.async_show_menu( + step_id="origin_menu", + menu_options=["origin_coordinates", "origin_entity"], + ) + return self.async_show_form( + step_id="user", data_schema=get_user_step_schema(user_input), errors=errors + ) + + async def async_step_origin_coordinates( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Configure origin by using gps coordinates.""" + if user_input is not None: + self._config[CONF_ORIGIN_LATITUDE] = user_input["origin"]["latitude"] + self._config[CONF_ORIGIN_LONGITUDE] = user_input["origin"]["longitude"] + return self.async_show_menu( + step_id="destination_menu", + menu_options=["destination_coordinates", "destination_entity"], + ) + schema = vol.Schema({"origin": selector({LocationSelector.selector_type: {}})}) + return self.async_show_form(step_id="origin_coordinates", data_schema=schema) + + async def async_step_origin_entity( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Configure origin by using an entity.""" + if user_input is not None: + self._config[CONF_ORIGIN_ENTITY_ID] = user_input[CONF_ORIGIN_ENTITY_ID] + return self.async_show_menu( + step_id="destination_menu", + menu_options=["destination_coordinates", "destination_entity"], + ) + schema = vol.Schema( + {CONF_ORIGIN_ENTITY_ID: selector({EntitySelector.selector_type: {}})} + ) + return self.async_show_form(step_id="origin_entity", data_schema=schema) + + async def async_step_destination_coordinates( + self, + user_input: dict[str, Any] | None = None, + ) -> FlowResult: + """Configure destination by using gps coordinates.""" + if user_input is not None: + self._config[CONF_DESTINATION_LATITUDE] = user_input["destination"][ + "latitude" + ] + self._config[CONF_DESTINATION_LONGITUDE] = user_input["destination"][ + "longitude" + ] + return self.async_create_entry( + title=self._config[CONF_NAME], data=self._config + ) + schema = vol.Schema( + {"destination": selector({LocationSelector.selector_type: {}})} + ) + return self.async_show_form( + step_id="destination_coordinates", data_schema=schema + ) + + async def async_step_destination_entity( + self, + user_input: dict[str, Any] | None = None, + ) -> FlowResult: + """Configure destination by using an entity.""" + if user_input is not None: + self._config[CONF_DESTINATION_ENTITY_ID] = user_input[ + CONF_DESTINATION_ENTITY_ID + ] + return self.async_create_entry( + title=self._config[CONF_NAME], data=self._config + ) + schema = vol.Schema( + {CONF_DESTINATION_ENTITY_ID: selector({EntitySelector.selector_type: {}})} + ) + return self.async_show_form(step_id="destination_entity", data_schema=schema) + + async def async_step_import( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Import from configuration.yaml.""" + options: dict[str, Any] = {} + user_input, options = self._transform_import_input(user_input) + # We need to prevent duplicate imports + if any( + is_dupe_import(entry, user_input, options) + for entry in self.hass.config_entries.async_entries(DOMAIN) + if entry.source == config_entries.SOURCE_IMPORT + ): + return self.async_abort(reason="already_configured") + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input, options=options + ) + + def _transform_import_input( + self, user_input + ) -> tuple[dict[str, Any], dict[str, Any]]: + """Transform platform schema input to new model.""" + options: dict[str, Any] = {} + if user_input.get(CONF_ORIGIN_LATITUDE) is not None: + user_input[CONF_ORIGIN_LATITUDE] = user_input.pop(CONF_ORIGIN_LATITUDE) + user_input[CONF_ORIGIN_LONGITUDE] = user_input.pop(CONF_ORIGIN_LONGITUDE) + else: + user_input[CONF_ORIGIN_ENTITY_ID] = user_input.pop(CONF_ORIGIN_ENTITY_ID) + + if user_input.get(CONF_DESTINATION_LATITUDE) is not None: + user_input[CONF_DESTINATION_LATITUDE] = user_input.pop( + CONF_DESTINATION_LATITUDE + ) + user_input[CONF_DESTINATION_LONGITUDE] = user_input.pop( + CONF_DESTINATION_LONGITUDE + ) + else: + user_input[CONF_DESTINATION_ENTITY_ID] = user_input.pop( + CONF_DESTINATION_ENTITY_ID + ) + + options[CONF_TRAFFIC_MODE] = ( + TRAFFIC_MODE_ENABLED + if user_input.pop(CONF_TRAFFIC_MODE, False) + else TRAFFIC_MODE_DISABLED + ) + options[CONF_ROUTE_MODE] = user_input.pop(CONF_ROUTE_MODE) + options[CONF_UNIT_SYSTEM] = user_input.pop( + CONF_UNIT_SYSTEM, self.hass.config.units.name + ) + options[CONF_ARRIVAL_TIME] = user_input.pop(CONF_ARRIVAL, None) + options[CONF_DEPARTURE_TIME] = user_input.pop(CONF_DEPARTURE, None) + + return user_input, options + + +class HERETravelTimeOptionsFlow(config_entries.OptionsFlow): + """Handle HERE Travel Time options.""" + + _config: dict[str, Any] = {} + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize HERE Travel Time options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the HERE Travel Time options.""" + if user_input is not None: + self._config = user_input + if self.config_entry.data[CONF_MODE] == TRAVEL_MODE_PUBLIC_TIME_TABLE: + return self.async_show_menu( + step_id="time_menu", + menu_options=["departure_time", "arrival_time", "no_time"], + ) + return self.async_show_menu( + step_id="time_menu", + menu_options=["departure_time", "no_time"], + ) + + options = { + vol.Optional( + CONF_TRAFFIC_MODE, + default=self.config_entry.options.get( + CONF_TRAFFIC_MODE, TRAFFIC_MODE_ENABLED + ), + ): vol.In(TRAFFIC_MODES), + vol.Optional( + CONF_ROUTE_MODE, + default=self.config_entry.options.get( + CONF_ROUTE_MODE, ROUTE_MODE_FASTEST + ), + ): vol.In(ROUTE_MODES), + vol.Optional( + CONF_UNIT_SYSTEM, + default=self.config_entry.options.get( + CONF_UNIT_SYSTEM, self.hass.config.units.name + ), + ): vol.In(UNITS), + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) + + async def async_step_no_time( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Create Options Entry.""" + return self.async_create_entry(title="", data=self._config) + + async def async_step_arrival_time( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Configure arrival time.""" + if user_input is not None: + self._config[CONF_ARRIVAL_TIME] = user_input[CONF_ARRIVAL_TIME] + return self.async_create_entry(title="", data=self._config) + + options = {"arrival_time": selector({TimeSelector.selector_type: {}})} + + return self.async_show_form( + step_id="arrival_time", data_schema=vol.Schema(options) + ) + + async def async_step_departure_time( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Configure departure time.""" + if user_input is not None: + self._config[CONF_DEPARTURE_TIME] = user_input[CONF_DEPARTURE_TIME] + return self.async_create_entry(title="", data=self._config) + + options = {"departure_time": selector({TimeSelector.selector_type: {}})} + + return self.async_show_form( + step_id="departure_time", data_schema=vol.Schema(options) + ) diff --git a/homeassistant/components/here_travel_time/const.py b/homeassistant/components/here_travel_time/const.py index a6b958ebf5e..bde17f5c306 100644 --- a/homeassistant/components/here_travel_time/const.py +++ b/homeassistant/components/here_travel_time/const.py @@ -8,20 +8,19 @@ from homeassistant.const import ( DOMAIN = "here_travel_time" DEFAULT_SCAN_INTERVAL = 300 -CONF_DESTINATION = "destination" -CONF_ORIGIN = "origin" + +CONF_DESTINATION_LATITUDE = "destination_latitude" +CONF_DESTINATION_LONGITUDE = "destination_longitude" +CONF_DESTINATION_ENTITY_ID = "destination_entity_id" +CONF_ORIGIN_LATITUDE = "origin_latitude" +CONF_ORIGIN_LONGITUDE = "origin_longitude" +CONF_ORIGIN_ENTITY_ID = "origin_entity_id" CONF_TRAFFIC_MODE = "traffic_mode" CONF_ROUTE_MODE = "route_mode" CONF_ARRIVAL = "arrival" CONF_DEPARTURE = "departure" CONF_ARRIVAL_TIME = "arrival_time" CONF_DEPARTURE_TIME = "departure_time" -CONF_TIME_TYPE = "time_type" -CONF_TIME = "time" - -ARRIVAL_TIME = "Arrival Time" -DEPARTURE_TIME = "Departure Time" -TIME_TYPES = [ARRIVAL_TIME, DEPARTURE_TIME] DEFAULT_NAME = "HERE Travel Time" diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json index b620153bba7..68370311254 100644 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -1,6 +1,7 @@ { "domain": "here_travel_time", "name": "HERE Travel Time", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/here_travel_time", "requirements": ["herepy==2.0.0"], "codeowners": ["@eifinger"], diff --git a/homeassistant/components/here_travel_time/model.py b/homeassistant/components/here_travel_time/model.py index eb85e966edf..65673a1e8b6 100644 --- a/homeassistant/components/here_travel_time/model.py +++ b/homeassistant/components/here_travel_time/model.py @@ -24,10 +24,12 @@ class HERERoutingData(TypedDict): class HERETravelTimeConfig: """Configuration for HereTravelTimeDataUpdateCoordinator.""" - origin: str | None - destination: str | None - origin_entity_id: str | None + destination_latitude: float | None + destination_longitude: float | None destination_entity_id: str | None + origin_latitude: float | None + origin_longitude: float | None + origin_entity_id: str | None travel_mode: str route_mode: str units: str diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 304c49b6bed..4a09252f068 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -4,11 +4,10 @@ from __future__ import annotations from datetime import timedelta import logging -import herepy -from herepy.here_enum import RouteMode import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_MODE, @@ -16,8 +15,6 @@ from homeassistant.const import ( CONF_MODE, CONF_NAME, CONF_UNIT_SYSTEM, - CONF_UNIT_SYSTEM_IMPERIAL, - CONF_UNIT_SYSTEM_METRIC, TIME_MINUTES, ) from homeassistant.core import HomeAssistant @@ -28,74 +25,47 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import HereTravelTimeDataUpdateCoordinator -from .model import HERETravelTimeConfig - -_LOGGER = logging.getLogger(__name__) - -CONF_DESTINATION_LATITUDE = "destination_latitude" -CONF_DESTINATION_LONGITUDE = "destination_longitude" -CONF_DESTINATION_ENTITY_ID = "destination_entity_id" -CONF_ORIGIN_LATITUDE = "origin_latitude" -CONF_ORIGIN_LONGITUDE = "origin_longitude" -CONF_ORIGIN_ENTITY_ID = "origin_entity_id" -CONF_TRAFFIC_MODE = "traffic_mode" -CONF_ROUTE_MODE = "route_mode" -CONF_ARRIVAL = "arrival" -CONF_DEPARTURE = "departure" - -DEFAULT_NAME = "HERE Travel Time" - -TRAVEL_MODE_BICYCLE = "bicycle" -TRAVEL_MODE_CAR = "car" -TRAVEL_MODE_PEDESTRIAN = "pedestrian" -TRAVEL_MODE_PUBLIC = "publicTransport" -TRAVEL_MODE_PUBLIC_TIME_TABLE = "publicTransportTimeTable" -TRAVEL_MODE_TRUCK = "truck" -TRAVEL_MODE = [ +from .const import ( + ATTR_DURATION, + ATTR_DURATION_IN_TRAFFIC, + ATTR_TRAFFIC_MODE, + ATTR_UNIT_SYSTEM, + CONF_ARRIVAL, + CONF_DEPARTURE, + CONF_DESTINATION_ENTITY_ID, + CONF_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE, + CONF_ORIGIN_ENTITY_ID, + CONF_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE, + CONF_ROUTE_MODE, + CONF_TRAFFIC_MODE, + DEFAULT_NAME, + DOMAIN, + ICON_BICYCLE, + ICON_CAR, + ICON_PEDESTRIAN, + ICON_PUBLIC, + ICON_TRUCK, + ROUTE_MODE_FASTEST, + ROUTE_MODES, + TRAFFIC_MODE_ENABLED, TRAVEL_MODE_BICYCLE, TRAVEL_MODE_CAR, TRAVEL_MODE_PEDESTRIAN, TRAVEL_MODE_PUBLIC, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAVEL_MODE_TRUCK, -] + TRAVEL_MODES, + TRAVEL_MODES_PUBLIC, + UNITS, +) -TRAVEL_MODES_PUBLIC = [TRAVEL_MODE_PUBLIC, TRAVEL_MODE_PUBLIC_TIME_TABLE] -TRAVEL_MODES_VEHICLE = [TRAVEL_MODE_CAR, TRAVEL_MODE_TRUCK] -TRAVEL_MODES_NON_VEHICLE = [TRAVEL_MODE_BICYCLE, TRAVEL_MODE_PEDESTRIAN] +_LOGGER = logging.getLogger(__name__) -TRAFFIC_MODE_ENABLED = "traffic_enabled" -TRAFFIC_MODE_DISABLED = "traffic_disabled" - -ROUTE_MODE_FASTEST = "fastest" -ROUTE_MODE_SHORTEST = "shortest" -ROUTE_MODE = [ROUTE_MODE_FASTEST, ROUTE_MODE_SHORTEST] - -ICON_BICYCLE = "mdi:bike" -ICON_CAR = "mdi:car" -ICON_PEDESTRIAN = "mdi:walk" -ICON_PUBLIC = "mdi:bus" -ICON_TRUCK = "mdi:truck" - -UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] - -ATTR_DURATION = "duration" -ATTR_DISTANCE = "distance" -ATTR_ROUTE = "route" -ATTR_ORIGIN = "origin" -ATTR_DESTINATION = "destination" - -ATTR_UNIT_SYSTEM = CONF_UNIT_SYSTEM -ATTR_TRAFFIC_MODE = CONF_TRAFFIC_MODE - -ATTR_DURATION_IN_TRAFFIC = "duration_in_traffic" -ATTR_ORIGIN_NAME = "origin_name" -ATTR_DESTINATION_NAME = "destination_name" SCAN_INTERVAL = timedelta(minutes=5) -NO_ROUTE_ERROR_MESSAGE = "HERE could not find a route based on the input" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, @@ -113,8 +83,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Exclusive(CONF_ORIGIN_ENTITY_ID, "origin"): cv.entity_id, vol.Optional(CONF_DEPARTURE): cv.time, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MODE, default=TRAVEL_MODE_CAR): vol.In(TRAVEL_MODE), - vol.Optional(CONF_ROUTE_MODE, default=ROUTE_MODE_FASTEST): vol.In(ROUTE_MODE), + vol.Optional(CONF_MODE, default=TRAVEL_MODE_CAR): vol.In(TRAVEL_MODES), + vol.Optional(CONF_ROUTE_MODE, default=ROUTE_MODE_FASTEST): vol.In(ROUTE_MODES), vol.Optional(CONF_TRAFFIC_MODE, default=False): cv.boolean, vol.Optional(CONF_UNIT_SYSTEM): vol.In(UNITS), } @@ -150,79 +120,36 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the HERE travel time platform.""" - api_key = config[CONF_API_KEY] - here_client = herepy.RoutingApi(api_key) - - if not await hass.async_add_executor_job( - _are_valid_client_credentials, here_client - ): - _LOGGER.error( - "Invalid credentials. This error is returned if the specified token was invalid or no contract could be found for this token" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, ) - return - - if config.get(CONF_ORIGIN_LATITUDE) is not None: - origin = f"{config[CONF_ORIGIN_LATITUDE]},{config[CONF_ORIGIN_LONGITUDE]}" - origin_entity_id = None - else: - origin = None - origin_entity_id = config[CONF_ORIGIN_ENTITY_ID] - - if config.get(CONF_DESTINATION_LATITUDE) is not None: - destination = ( - f"{config[CONF_DESTINATION_LATITUDE]},{config[CONF_DESTINATION_LONGITUDE]}" - ) - destination_entity_id = None - else: - destination = None - destination_entity_id = config[CONF_DESTINATION_ENTITY_ID] - - traffic_mode = config[CONF_TRAFFIC_MODE] - name = config[CONF_NAME] - - here_travel_time_config = HERETravelTimeConfig( - origin=origin, - destination=destination, - origin_entity_id=origin_entity_id, - destination_entity_id=destination_entity_id, - travel_mode=config[CONF_MODE], - route_mode=config[CONF_ROUTE_MODE], - units=config.get(CONF_UNIT_SYSTEM, hass.config.units.name), - arrival=config.get(CONF_ARRIVAL), - departure=config.get(CONF_DEPARTURE), ) - coordinator = HereTravelTimeDataUpdateCoordinator( - hass, - here_client, - here_travel_time_config, + _LOGGER.warning( + "Your HERE travel time configuration has been imported into the UI; " + "please remove it from configuration.yaml as support for it will be " + "removed in a future release" ) - sensor = HERETravelTimeSensor(name, traffic_mode, coordinator) - async_add_entities([sensor]) - - -def _are_valid_client_credentials(here_client: herepy.RoutingApi) -> bool: - """Check if the provided credentials are correct using defaults.""" - known_working_origin = [38.9, -77.04833] - known_working_destination = [39.0, -77.1] - try: - here_client.public_transport_timetable( - known_working_origin, - known_working_destination, - True, - [ - RouteMode[ROUTE_MODE_FASTEST], - RouteMode[TRAVEL_MODE_CAR], - RouteMode[TRAFFIC_MODE_ENABLED], - ], - arrival=None, - departure="now", - ) - except herepy.InvalidCredentialsError: - return False - return True +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add HERE travel time entities from a config_entry.""" + async_add_entities( + [ + HERETravelTimeSensor( + config_entry.data[CONF_NAME], + config_entry.options[CONF_TRAFFIC_MODE], + hass.data[DOMAIN][config_entry.entry_id], + ) + ], + ) class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): @@ -231,12 +158,12 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): def __init__( self, name: str, - traffic_mode: bool, + traffic_mode: str, coordinator: HereTravelTimeDataUpdateCoordinator, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._traffic_mode = traffic_mode + self._traffic_mode = traffic_mode == TRAFFIC_MODE_ENABLED self._attr_native_unit_of_measurement = TIME_MINUTES self._attr_name = name diff --git a/homeassistant/components/here_travel_time/strings.json b/homeassistant/components/here_travel_time/strings.json new file mode 100644 index 00000000000..e4a20a38d6b --- /dev/null +++ b/homeassistant/components/here_travel_time/strings.json @@ -0,0 +1,82 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "api_key": "[%key:common::config_flow::data::api_key%]", + "mode": "Travel Mode" + } + }, + "origin_coordinates": { + "title": "Choose Origin", + "data": { + "origin": "Origin as GPS coordinates" + } + }, + "origin_entity_id": { + "title": "Choose Origin", + "data": { + "origin_entity_id": "Origin using an entity" + } + }, + "destination_menu": { + "title": "Choose Destination", + "menu_options": { + "destination_coordinates": "Using a map location", + "destination_entity": "Using an entity" + } + }, + "destination_coordinates": { + "title": "Choose Destination", + "data": { + "destination": "Destination as GPS coordinates" + } + }, + "destination_entity_id": { + "title": "Choose Destination", + "data": { + "destination_entity_id": "Destination using an entity" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "traffic_mode": "Traffic Mode", + "route_mode": "Route Mode", + "unit_system": "Unit system" + } + }, + "time_menu": { + "title": "Choose Time Type", + "menu_options": { + "departure_time": "Configure a departure time", + "arrival_time": "Configure an arrival time", + "no_time": "Do not configure a time" + } + }, + "departure_time": { + "title": "Choose Departure Time", + "data": { + "departure_time": "Departure Time" + } + }, + "arrival_time": { + "title": "Choose Arrival Time", + "data": { + "arrival_time": "Arrival Time" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7f0059b2da9..67299f403c2 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -143,6 +143,7 @@ FLOWS = { "hangouts", "harmony", "heos", + "here_travel_time", "hisense_aehw4a1", "hive", "hlk_sw16", diff --git a/tests/components/here_travel_time/conftest.py b/tests/components/here_travel_time/conftest.py index 83f0659f516..368b070428e 100644 --- a/tests/components/here_travel_time/conftest.py +++ b/tests/components/here_travel_time/conftest.py @@ -12,6 +12,11 @@ RESPONSE = RoutingResponse.new_from_jsondict( ) RESPONSE.route_short = "US-29 - K St NW; US-29 - Whitehurst Fwy; I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd" +EMPTY_ATTRIBUTION_RESPONSE = RoutingResponse.new_from_jsondict( + json.loads(load_fixture("here_travel_time/empty_attribution_response.json")) +) +EMPTY_ATTRIBUTION_RESPONSE.route_short = "US-29 - K St NW; US-29 - Whitehurst Fwy; I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd" + @pytest.fixture(name="valid_response") def valid_response_fixture(): @@ -21,3 +26,13 @@ def valid_response_fixture(): return_value=RESPONSE, ) as mock: yield mock + + +@pytest.fixture(name="empty_attribution_response") +def empty_attribution_response_fixture(): + """Return valid api response with an empty attribution.""" + with patch( + "herepy.RoutingApi.public_transport_timetable", + return_value=EMPTY_ATTRIBUTION_RESPONSE, + ) as mock: + yield mock diff --git a/tests/components/here_travel_time/fixtures/empty_attribution_response.json b/tests/components/here_travel_time/fixtures/empty_attribution_response.json new file mode 100644 index 00000000000..cc1bb20a373 --- /dev/null +++ b/tests/components/here_travel_time/fixtures/empty_attribution_response.json @@ -0,0 +1,131 @@ +{ + "response": { + "metaInfo": { + "timestamp": "2019-07-19T07:38:39Z", + "mapVersion": "8.30.98.154", + "moduleVersion": "7.2.201928-4446", + "interfaceVersion": "2.6.64", + "availableMapVersion": ["8.30.98.154"] + }, + "route": [ + { + "waypoint": [ + { + "linkId": "+732182239", + "mappedPosition": { + "latitude": 38.9, + "longitude": -77.0488358 + }, + "originalPosition": { + "latitude": 38.9, + "longitude": -77.0483301 + }, + "type": "stopOver", + "spot": 0.4946237, + "sideOfStreet": "right", + "mappedRoadName": "22nd St NW", + "label": "22nd St NW", + "shapeIndex": 0, + "source": "user" + }, + { + "linkId": "+942865877", + "mappedPosition": { + "latitude": 38.9999735, + "longitude": -77.100141 + }, + "originalPosition": { + "latitude": 38.9999999, + "longitude": -77.1000001 + }, + "type": "stopOver", + "spot": 1, + "sideOfStreet": "left", + "mappedRoadName": "Service Rd S", + "label": "Service Rd S", + "shapeIndex": 279, + "source": "user" + } + ], + "mode": { + "type": "fastest", + "transportModes": ["car"], + "trafficMode": "enabled", + "feature": [] + }, + "leg": [ + { + "start": { + "linkId": "+732182239", + "mappedPosition": { + "latitude": 38.9, + "longitude": -77.0488358 + }, + "originalPosition": { + "latitude": 38.9, + "longitude": -77.0483301 + }, + "type": "stopOver", + "spot": 0.4946237, + "sideOfStreet": "right", + "mappedRoadName": "22nd St NW", + "label": "22nd St NW", + "shapeIndex": 0, + "source": "user" + }, + "end": { + "linkId": "+942865877", + "mappedPosition": { + "latitude": 38.9999735, + "longitude": -77.100141 + }, + "originalPosition": { + "latitude": 38.9999999, + "longitude": -77.1000001 + }, + "type": "stopOver", + "spot": 1, + "sideOfStreet": "left", + "mappedRoadName": "Service Rd S", + "label": "Service Rd S", + "shapeIndex": 279, + "source": "user" + }, + "length": 23903, + "travelTime": 1884, + "maneuver": [ + { + "position": { + "latitude": 38.9999735, + "longitude": -77.100141 + }, + "instruction": "Arrive at Service Rd S. Your destination is on the left.", + "travelTime": 0, + "length": 0, + "id": "M16", + "_type": "PrivateTransportManeuverType" + } + ] + } + ], + "summary": { + "distance": 23903, + "trafficTime": 1861, + "baseTime": 1803, + "flags": [ + "noThroughRoad", + "motorway", + "builtUpArea", + "park", + "privateRoad" + ], + "text": "The trip takes 23.9 km and 31 mins.", + "travelTime": 1861, + "_type": "RouteSummaryType" + } + } + ], + "language": "en-us", + "sourceAttribution": {} + } +} diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py new file mode 100644 index 00000000000..105128c7cfb --- /dev/null +++ b/tests/components/here_travel_time/test_config_flow.py @@ -0,0 +1,589 @@ +"""Test the HERE Travel Time config flow.""" +from unittest.mock import patch + +from herepy import HEREError +from herepy.routing_api import InvalidCredentialsError +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.here_travel_time.const import ( + CONF_ARRIVAL, + CONF_ARRIVAL_TIME, + CONF_DEPARTURE, + CONF_DEPARTURE_TIME, + CONF_ROUTE_MODE, + CONF_TRAFFIC_MODE, + DOMAIN, + ROUTE_MODE_FASTEST, + TRAFFIC_MODE_ENABLED, + TRAVEL_MODE_CAR, + TRAVEL_MODE_PUBLIC_TIME_TABLE, +) +from homeassistant.components.here_travel_time.sensor import ( + CONF_DESTINATION_ENTITY_ID, + CONF_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE, + CONF_ORIGIN_ENTITY_ID, + CONF_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE, +) +from homeassistant.const import ( + CONF_API_KEY, + CONF_MODE, + CONF_NAME, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, +) +from homeassistant.core import HomeAssistant + +from .const import ( + API_KEY, + CAR_DESTINATION_LATITUDE, + CAR_DESTINATION_LONGITUDE, + CAR_ORIGIN_LATITUDE, + CAR_ORIGIN_LONGITUDE, +) + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="user_step_result") +async def user_step_result_fixture(hass: HomeAssistant) -> data_entry_flow.FlowResult: + """Provide the result of a completed user step.""" + init_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + user_step_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + { + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_CAR, + CONF_NAME: "test", + }, + ) + await hass.async_block_till_done() + yield user_step_result + + +@pytest.fixture(name="option_init_result") +async def option_init_result_fixture(hass: HomeAssistant) -> data_entry_flow.FlowResult: + """Provide the result of a completed options init step.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={ + CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_PUBLIC_TIME_TABLE, + CONF_NAME: "test", + }, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + flow = await hass.config_entries.options.async_init(entry.entry_id) + result = await hass.config_entries.options.async_configure( + flow["flow_id"], + user_input={ + CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + }, + ) + yield result + + +@pytest.fixture(name="origin_step_result") +async def origin_step_result_fixture( + hass: HomeAssistant, user_step_result: data_entry_flow.FlowResult +) -> data_entry_flow.FlowResult: + """Provide the result of a completed origin by coordinates step.""" + origin_menu_result = await hass.config_entries.flow.async_configure( + user_step_result["flow_id"], {"next_step_id": "origin_coordinates"} + ) + + location_selector_result = await hass.config_entries.flow.async_configure( + origin_menu_result["flow_id"], + { + "origin": { + "latitude": float(CAR_ORIGIN_LATITUDE), + "longitude": float(CAR_ORIGIN_LONGITUDE), + "radius": 3.0, + } + }, + ) + yield location_selector_result + + +@pytest.mark.parametrize( + "menu_options", + (["origin_coordinates", "origin_entity"],), +) +@pytest.mark.usefixtures("valid_response") +async def test_step_user(hass: HomeAssistant, menu_options) -> None: + """Test the user step.""" + 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"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_CAR, + CONF_NAME: "test", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_MENU + assert result2["menu_options"] == menu_options + + +@pytest.mark.usefixtures("valid_response") +async def test_step_origin_coordinates( + hass: HomeAssistant, user_step_result: data_entry_flow.FlowResult +) -> None: + """Test the origin coordinates step.""" + menu_result = await hass.config_entries.flow.async_configure( + user_step_result["flow_id"], {"next_step_id": "origin_coordinates"} + ) + assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + + location_selector_result = await hass.config_entries.flow.async_configure( + menu_result["flow_id"], + { + "origin": { + "latitude": float(CAR_ORIGIN_LATITUDE), + "longitude": float(CAR_ORIGIN_LONGITUDE), + "radius": 3.0, + } + }, + ) + assert location_selector_result["type"] == data_entry_flow.RESULT_TYPE_MENU + + +@pytest.mark.usefixtures("valid_response") +async def test_step_origin_entity( + hass: HomeAssistant, user_step_result: data_entry_flow.FlowResult +) -> None: + """Test the origin coordinates step.""" + menu_result = await hass.config_entries.flow.async_configure( + user_step_result["flow_id"], {"next_step_id": "origin_entity"} + ) + assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + + entity_selector_result = await hass.config_entries.flow.async_configure( + menu_result["flow_id"], + {"origin_entity_id": "zone.home"}, + ) + assert entity_selector_result["type"] == data_entry_flow.RESULT_TYPE_MENU + + +@pytest.mark.usefixtures("valid_response") +async def test_step_destination_coordinates( + hass: HomeAssistant, origin_step_result: data_entry_flow.FlowResult +) -> None: + """Test the origin coordinates step.""" + menu_result = await hass.config_entries.flow.async_configure( + origin_step_result["flow_id"], {"next_step_id": "destination_coordinates"} + ) + assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + + location_selector_result = await hass.config_entries.flow.async_configure( + menu_result["flow_id"], + { + "destination": { + "latitude": float(CAR_DESTINATION_LATITUDE), + "longitude": float(CAR_DESTINATION_LONGITUDE), + "radius": 3.0, + } + }, + ) + assert location_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data == { + CONF_NAME: "test", + CONF_API_KEY: API_KEY, + CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), + CONF_MODE: TRAVEL_MODE_CAR, + } + + +@pytest.mark.usefixtures("valid_response") +async def test_step_destination_entity( + hass: HomeAssistant, origin_step_result: data_entry_flow.FlowResult +) -> None: + """Test the origin coordinates step.""" + menu_result = await hass.config_entries.flow.async_configure( + origin_step_result["flow_id"], {"next_step_id": "destination_entity"} + ) + assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + + entity_selector_result = await hass.config_entries.flow.async_configure( + menu_result["flow_id"], + {"destination_entity_id": "zone.home"}, + ) + assert entity_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data == { + CONF_NAME: "test", + CONF_API_KEY: API_KEY, + CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), + CONF_DESTINATION_ENTITY_ID: "zone.home", + CONF_MODE: TRAVEL_MODE_CAR, + } + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "herepy.RoutingApi.public_transport_timetable", + side_effect=InvalidCredentialsError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_CAR, + CONF_NAME: "test", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_unknown_error(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "herepy.RoutingApi.public_transport_timetable", + side_effect=HEREError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_CAR, + CONF_NAME: "test", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +@pytest.mark.usefixtures("valid_response") +async def test_options_flow(hass: HomeAssistant) -> None: + """Test the options flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={ + CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_CAR, + CONF_NAME: "test", + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == 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_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_MENU + + +@pytest.mark.usefixtures("valid_response") +async def test_options_flow_arrival_time_step( + hass: HomeAssistant, option_init_result: data_entry_flow.FlowResult +) -> None: + """Test the options flow arrival time type.""" + menu_result = await hass.config_entries.options.async_configure( + option_init_result["flow_id"], {"next_step_id": "arrival_time"} + ) + assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + time_selector_result = await hass.config_entries.options.async_configure( + option_init_result["flow_id"], + user_input={ + "arrival_time": "08:00:00", + }, + ) + + assert time_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.options == { + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, + CONF_ARRIVAL_TIME: "08:00:00", + } + + +@pytest.mark.usefixtures("valid_response") +async def test_options_flow_departure_time_step( + hass: HomeAssistant, option_init_result: data_entry_flow.FlowResult +) -> None: + """Test the options flow departure time type.""" + menu_result = await hass.config_entries.options.async_configure( + option_init_result["flow_id"], {"next_step_id": "departure_time"} + ) + assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + time_selector_result = await hass.config_entries.options.async_configure( + option_init_result["flow_id"], + user_input={ + "departure_time": "08:00:00", + }, + ) + + assert time_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.options == { + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, + CONF_DEPARTURE_TIME: "08:00:00", + } + + +@pytest.mark.usefixtures("valid_response") +async def test_options_flow_no_time_step( + hass: HomeAssistant, option_init_result: data_entry_flow.FlowResult +) -> None: + """Test the options flow arrival time type.""" + menu_result = await hass.config_entries.options.async_configure( + option_init_result["flow_id"], {"next_step_id": "no_time"} + ) + + assert menu_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.options == { + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, + } + + +@pytest.mark.usefixtures("valid_response") +async def test_import_flow_entity_id(hass: HomeAssistant) -> None: + """Test import_flow with entity ids.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: CONF_API_KEY, + CONF_ORIGIN_ENTITY_ID: "sensor.origin", + CONF_DESTINATION_ENTITY_ID: "sensor.destination", + CONF_NAME: "test_name", + CONF_MODE: TRAVEL_MODE_CAR, + CONF_DEPARTURE: "08:00:00", + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "test_name" + + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data == { + CONF_NAME: "test_name", + CONF_API_KEY: CONF_API_KEY, + CONF_ORIGIN_ENTITY_ID: "sensor.origin", + CONF_DESTINATION_ENTITY_ID: "sensor.destination", + CONF_MODE: TRAVEL_MODE_CAR, + } + assert entry.options == { + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, + CONF_DEPARTURE_TIME: "08:00:00", + CONF_ARRIVAL_TIME: None, + } + + +@pytest.mark.usefixtures("valid_response") +async def test_import_flow_coordinates(hass: HomeAssistant) -> None: + """Test import_flow with coordinates.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: CONF_API_KEY, + CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE, + CONF_DESTINATION_LATITUDE: CAR_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE, + CONF_NAME: "test_name", + CONF_MODE: TRAVEL_MODE_CAR, + CONF_ARRIVAL: "08:00:00", + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "test_name" + + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data == { + CONF_NAME: "test_name", + CONF_API_KEY: CONF_API_KEY, + CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE, + CONF_DESTINATION_LATITUDE: CAR_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE, + CONF_MODE: TRAVEL_MODE_CAR, + } + assert entry.options == { + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, + CONF_DEPARTURE_TIME: None, + CONF_ARRIVAL_TIME: "08:00:00", + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + } + + +@pytest.mark.usefixtures("valid_response") +async def test_dupe_import(hass: HomeAssistant) -> None: + """Test duplicate import.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: CONF_API_KEY, + CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE, + CONF_DESTINATION_LATITUDE: CAR_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE, + CONF_NAME: "test_name", + CONF_MODE: TRAVEL_MODE_CAR, + CONF_ARRIVAL: "08:00:00", + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: CONF_API_KEY, + CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE, + CONF_DESTINATION_LATITUDE: CAR_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE, + CONF_NAME: "test_name2", + CONF_MODE: TRAVEL_MODE_CAR, + CONF_ARRIVAL: "08:00:00", + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: CONF_API_KEY, + CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE, + CONF_DESTINATION_LATITUDE: CAR_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE, + CONF_NAME: "test_name", + CONF_MODE: TRAVEL_MODE_CAR, + CONF_ARRIVAL: "08:00:01", + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: CONF_API_KEY, + CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE, + CONF_DESTINATION_LATITUDE: "40.0", + CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE, + CONF_NAME: "test_name", + CONF_MODE: TRAVEL_MODE_CAR, + CONF_ARRIVAL: "08:00:01", + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: CONF_API_KEY, + CONF_ORIGIN_LATITUDE: CAR_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE: CAR_ORIGIN_LONGITUDE, + CONF_DESTINATION_LATITUDE: CAR_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE: CAR_DESTINATION_LONGITUDE, + CONF_NAME: "test_name", + CONF_MODE: TRAVEL_MODE_CAR, + CONF_ARRIVAL: "08:00:00", + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, + CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/here_travel_time/test_init.py b/tests/components/here_travel_time/test_init.py new file mode 100644 index 00000000000..02827acc4df --- /dev/null +++ b/tests/components/here_travel_time/test_init.py @@ -0,0 +1,48 @@ +"""The test for the HERE Travel Time integration.""" + +import pytest + +from homeassistant.components.here_travel_time.const import ( + CONF_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE, + CONF_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE, + DOMAIN, + TRAVEL_MODE_CAR, +) +from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME +from homeassistant.core import HomeAssistant + +from .const import ( + API_KEY, + CAR_DESTINATION_LATITUDE, + CAR_DESTINATION_LONGITUDE, + CAR_ORIGIN_LATITUDE, + CAR_ORIGIN_LONGITUDE, +) + +from tests.common import MockConfigEntry + + +@pytest.mark.usefixtures("valid_response") +async def test_unload_entry(hass: HomeAssistant) -> None: + """Test that unloading an entry works.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={ + CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_CAR, + CONF_NAME: "test", + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert await hass.config_entries.async_unload(entry.entry_id) + assert not hass.data[DOMAIN] diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 4ad2f757c77..585d5adc9d3 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -2,15 +2,10 @@ from unittest.mock import MagicMock, patch from herepy.here_enum import RouteMode -from herepy.routing_api import InvalidCredentialsError, NoRouteFoundError +from herepy.routing_api import NoRouteFoundError import pytest from homeassistant.components.here_travel_time.const import ( - ROUTE_MODE_FASTEST, - TRAFFIC_MODE_ENABLED, -) -from homeassistant.components.here_travel_time.sensor import ( - ATTR_ATTRIBUTION, ATTR_DESTINATION, ATTR_DESTINATION_NAME, ATTR_DISTANCE, @@ -19,16 +14,27 @@ from homeassistant.components.here_travel_time.sensor import ( ATTR_ORIGIN, ATTR_ORIGIN_NAME, ATTR_ROUTE, - CONF_MODE, + CONF_ARRIVAL_TIME, + CONF_DEPARTURE_TIME, + CONF_DESTINATION_ENTITY_ID, + CONF_DESTINATION_LATITUDE, + CONF_DESTINATION_LONGITUDE, + CONF_ORIGIN_ENTITY_ID, + CONF_ORIGIN_LATITUDE, + CONF_ORIGIN_LONGITUDE, + CONF_ROUTE_MODE, CONF_TRAFFIC_MODE, CONF_UNIT_SYSTEM, + DOMAIN, ICON_BICYCLE, ICON_CAR, ICON_PEDESTRIAN, ICON_PUBLIC, ICON_TRUCK, NO_ROUTE_ERROR_MESSAGE, - TIME_MINUTES, + ROUTE_MODE_FASTEST, + TRAFFIC_MODE_DISABLED, + TRAFFIC_MODE_ENABLED, TRAVEL_MODE_BICYCLE, TRAVEL_MODE_CAR, TRAVEL_MODE_PEDESTRIAN, @@ -36,11 +42,20 @@ from homeassistant.components.here_travel_time.sensor import ( TRAVEL_MODE_TRUCK, TRAVEL_MODES_VEHICLE, ) -from homeassistant.const import ATTR_ICON, EVENT_HOMEASSISTANT_START +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_ICON, + CONF_API_KEY, + CONF_MODE, + CONF_NAME, + EVENT_HOMEASSISTANT_START, + TIME_MINUTES, +) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.components.here_travel_time.const import ( +from .const import ( API_KEY, CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE, @@ -48,21 +63,41 @@ from tests.components.here_travel_time.const import ( CAR_ORIGIN_LONGITUDE, ) -DOMAIN = "sensor" - -PLATFORM = "here_travel_time" +from tests.common import MockConfigEntry @pytest.mark.parametrize( - "mode,icon,traffic_mode,unit_system,expected_state,expected_distance,expected_duration_in_traffic", + "mode,icon,traffic_mode,unit_system,arrival_time,departure_time,expected_state,expected_distance,expected_duration_in_traffic", [ - (TRAVEL_MODE_CAR, ICON_CAR, True, "metric", "31", 23.903, 31.016666666666666), - (TRAVEL_MODE_BICYCLE, ICON_BICYCLE, False, "metric", "30", 23.903, 30.05), + ( + TRAVEL_MODE_CAR, + ICON_CAR, + TRAFFIC_MODE_ENABLED, + "metric", + None, + None, + "31", + 23.903, + 31.016666666666666, + ), + ( + TRAVEL_MODE_BICYCLE, + ICON_BICYCLE, + TRAFFIC_MODE_DISABLED, + "metric", + None, + None, + "30", + 23.903, + 30.05, + ), ( TRAVEL_MODE_PEDESTRIAN, ICON_PEDESTRIAN, - False, + TRAFFIC_MODE_DISABLED, "imperial", + None, + None, "30", 14.852635608048994, 30.05, @@ -70,8 +105,10 @@ PLATFORM = "here_travel_time" ( TRAVEL_MODE_PUBLIC_TIME_TABLE, ICON_PUBLIC, - False, + TRAFFIC_MODE_DISABLED, "imperial", + "08:00:00", + None, "30", 14.852635608048994, 30.05, @@ -79,41 +116,52 @@ PLATFORM = "here_travel_time" ( TRAVEL_MODE_TRUCK, ICON_TRUCK, - True, + TRAFFIC_MODE_ENABLED, "metric", + None, + "08:00:00", "31", 23.903, 31.016666666666666, ), ], ) +@pytest.mark.usefixtures("valid_response") async def test_sensor( - hass, + hass: HomeAssistant, mode, icon, traffic_mode, unit_system, + arrival_time, + departure_time, expected_state, expected_distance, expected_duration_in_traffic, - valid_response, ): """Test that sensor works.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": CAR_ORIGIN_LATITUDE, - "origin_longitude": CAR_ORIGIN_LONGITUDE, - "destination_latitude": CAR_DESTINATION_LATITUDE, - "destination_longitude": CAR_DESTINATION_LONGITUDE, - "api_key": API_KEY, - "traffic_mode": traffic_mode, - "unit_system": unit_system, - "mode": mode, - } - } - assert await async_setup_component(hass, DOMAIN, config) + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={ + CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), + CONF_API_KEY: API_KEY, + CONF_MODE: mode, + CONF_NAME: "test", + }, + options={ + CONF_TRAFFIC_MODE: traffic_mode, + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_ARRIVAL_TIME: arrival_time, + CONF_DEPARTURE_TIME: departure_time, + CONF_UNIT_SYSTEM: unit_system, + }, + ) + 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() @@ -145,7 +193,9 @@ async def test_sensor( assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "22nd St NW" assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "Service Rd S" assert sensor.attributes.get(CONF_MODE) == mode - assert sensor.attributes.get(CONF_TRAFFIC_MODE) is traffic_mode + assert sensor.attributes.get(CONF_TRAFFIC_MODE) is ( + traffic_mode == TRAFFIC_MODE_ENABLED + ) assert sensor.attributes.get(ATTR_ICON) == icon @@ -156,7 +206,63 @@ async def test_sensor( ) -async def test_entity_ids(hass, valid_response: MagicMock): +@pytest.mark.usefixtures("valid_response") +async def test_circular_ref(hass: HomeAssistant, caplog): + """Test that a circular ref is handled.""" + hass.states.async_set( + "test.first", + "test.second", + ) + hass.states.async_set("test.second", "test.first") + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={ + CONF_ORIGIN_ENTITY_ID: "test.first", + CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_TRUCK, + CONF_NAME: "test", + }, + ) + 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 "No coordinatnes found for test.first" in caplog.text + + +@pytest.mark.usefixtures("empty_attribution_response") +async def test_no_attribution(hass: HomeAssistant): + """Test that an empty attribution is handled.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={ + CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_TRUCK, + CONF_NAME: "test", + }, + ) + 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.test").attributes.get(ATTR_ATTRIBUTION) is None + + +async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock): """Test that origin/destination supplied by entities works.""" utcnow = dt_util.utcnow() # Patching 'utcnow' to gain more control over the timed update. @@ -181,17 +287,19 @@ async def test_entity_ids(hass, valid_response: MagicMock): "longitude": float(CAR_DESTINATION_LONGITUDE), }, ) - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_entity_id": "zone.origin", - "destination_entity_id": "device_tracker.test", - "api_key": API_KEY, - "mode": TRAVEL_MODE_TRUCK, - } - } - assert await async_setup_component(hass, DOMAIN, config) + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={ + CONF_ORIGIN_ENTITY_ID: "zone.origin", + CONF_DESTINATION_ENTITY_ID: "device_tracker.test", + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_TRUCK, + CONF_NAME: "test", + }, + ) + 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) @@ -214,20 +322,23 @@ async def test_entity_ids(hass, valid_response: MagicMock): ) -async def test_destination_entity_not_found(hass, caplog, valid_response: MagicMock): +@pytest.mark.usefixtures("valid_response") +async def test_destination_entity_not_found(hass: HomeAssistant, caplog): """Test that a not existing destination_entity_id is caught.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": CAR_ORIGIN_LATITUDE, - "origin_longitude": CAR_ORIGIN_LONGITUDE, - "destination_entity_id": "device_tracker.test", - "api_key": API_KEY, - "mode": TRAVEL_MODE_TRUCK, - } - } - assert await async_setup_component(hass, DOMAIN, config) + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={ + CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), + CONF_DESTINATION_ENTITY_ID: "device_tracker.test", + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_TRUCK, + CONF_NAME: "test", + }, + ) + 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) @@ -236,20 +347,23 @@ async def test_destination_entity_not_found(hass, caplog, valid_response: MagicM assert "device_tracker.test are not valid coordinates" in caplog.text -async def test_origin_entity_not_found(hass, caplog, valid_response: MagicMock): +@pytest.mark.usefixtures("valid_response") +async def test_origin_entity_not_found(hass: HomeAssistant, caplog): """Test that a not existing origin_entity_id is caught.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_entity_id": "device_tracker.test", - "destination_latitude": CAR_ORIGIN_LATITUDE, - "destination_longitude": CAR_ORIGIN_LONGITUDE, - "api_key": API_KEY, - "mode": TRAVEL_MODE_TRUCK, - } - } - assert await async_setup_component(hass, DOMAIN, config) + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={ + CONF_ORIGIN_ENTITY_ID: "device_tracker.test", + CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_TRUCK, + CONF_NAME: "test", + }, + ) + 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) @@ -258,26 +372,27 @@ async def test_origin_entity_not_found(hass, caplog, valid_response: MagicMock): assert "device_tracker.test are not valid coordinates" in caplog.text -async def test_invalid_destination_entity_state( - hass, caplog, valid_response: MagicMock -): +@pytest.mark.usefixtures("valid_response") +async def test_invalid_destination_entity_state(hass: HomeAssistant, caplog): """Test that an invalid state of the destination_entity_id is caught.""" hass.states.async_set( "device_tracker.test", "test_state", ) - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": CAR_ORIGIN_LATITUDE, - "origin_longitude": CAR_ORIGIN_LONGITUDE, - "destination_entity_id": "device_tracker.test", - "api_key": API_KEY, - "mode": TRAVEL_MODE_TRUCK, - } - } - assert await async_setup_component(hass, DOMAIN, config) + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={ + CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), + CONF_DESTINATION_ENTITY_ID: "device_tracker.test", + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_TRUCK, + CONF_NAME: "test", + }, + ) + 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) @@ -286,24 +401,27 @@ async def test_invalid_destination_entity_state( assert "test_state are not valid coordinates" in caplog.text -async def test_invalid_origin_entity_state(hass, caplog, valid_response: MagicMock): +@pytest.mark.usefixtures("valid_response") +async def test_invalid_origin_entity_state(hass: HomeAssistant, caplog): """Test that an invalid state of the origin_entity_id is caught.""" hass.states.async_set( "device_tracker.test", "test_state", ) - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_entity_id": "device_tracker.test", - "destination_latitude": CAR_ORIGIN_LATITUDE, - "destination_longitude": CAR_ORIGIN_LONGITUDE, - "api_key": API_KEY, - "mode": TRAVEL_MODE_TRUCK, - } - } - assert await async_setup_component(hass, DOMAIN, config) + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={ + CONF_ORIGIN_ENTITY_ID: "device_tracker.test", + CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_TRUCK, + CONF_NAME: "test", + }, + ) + 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) @@ -312,27 +430,30 @@ async def test_invalid_origin_entity_state(hass, caplog, valid_response: MagicMo assert "test_state are not valid coordinates" in caplog.text -async def test_route_not_found(hass, caplog): +async def test_route_not_found(hass: HomeAssistant, caplog): """Test that route not found error is correctly handled.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": CAR_ORIGIN_LATITUDE, - "origin_longitude": CAR_ORIGIN_LONGITUDE, - "destination_latitude": CAR_DESTINATION_LATITUDE, - "destination_longitude": CAR_DESTINATION_LONGITUDE, - "api_key": API_KEY, - } - } with patch( - "homeassistant.components.here_travel_time.sensor._are_valid_client_credentials", - return_value=True, + "homeassistant.components.here_travel_time.config_flow.validate_api_key", + return_value=None, ), patch( "herepy.RoutingApi.public_transport_timetable", side_effect=NoRouteFoundError, ): - assert await async_setup_component(hass, DOMAIN, config) + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={ + CONF_ORIGIN_LATITUDE: float(CAR_ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(CAR_ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(CAR_DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(CAR_DESTINATION_LONGITUDE), + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_TRUCK, + CONF_NAME: "test", + }, + ) + 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() @@ -340,113 +461,26 @@ async def test_route_not_found(hass, caplog): assert NO_ROUTE_ERROR_MESSAGE in caplog.text -async def test_invalid_credentials(hass, caplog): - """Test that invalid credentials error is correctly handled.""" +@pytest.mark.usefixtures("valid_response") +async def test_setup_platform(hass: HomeAssistant, caplog): + """Test that setup platform migration works.""" + config = { + "sensor": { + "platform": DOMAIN, + "name": "test", + "origin_latitude": CAR_ORIGIN_LATITUDE, + "origin_longitude": CAR_ORIGIN_LONGITUDE, + "destination_latitude": CAR_DESTINATION_LATITUDE, + "destination_longitude": CAR_DESTINATION_LONGITUDE, + "api_key": API_KEY, + } + } with patch( - "herepy.RoutingApi.public_transport_timetable", - side_effect=InvalidCredentialsError, + "homeassistant.components.here_travel_time.async_setup_entry", return_value=True ): - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": CAR_ORIGIN_LATITUDE, - "origin_longitude": CAR_ORIGIN_LONGITUDE, - "destination_latitude": CAR_DESTINATION_LATITUDE, - "destination_longitude": CAR_DESTINATION_LONGITUDE, - "api_key": API_KEY, - } - } - assert await async_setup_component(hass, DOMAIN, config) + await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() - assert "Invalid credentials" in caplog.text - - -async def test_arrival(hass, valid_response): - """Test that arrival works.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": CAR_ORIGIN_LATITUDE, - "origin_longitude": CAR_ORIGIN_LONGITUDE, - "destination_latitude": CAR_DESTINATION_LATITUDE, - "destination_longitude": CAR_DESTINATION_LONGITUDE, - "api_key": API_KEY, - "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE, - "arrival": "01:00:00", - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - assert sensor.state == "30" - - -async def test_departure(hass, valid_response): - """Test that departure works.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": CAR_ORIGIN_LATITUDE, - "origin_longitude": CAR_ORIGIN_LONGITUDE, - "destination_latitude": CAR_DESTINATION_LATITUDE, - "destination_longitude": CAR_DESTINATION_LONGITUDE, - "api_key": API_KEY, - "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE, - "departure": "23:00:00", - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - - sensor = hass.states.get("sensor.test") - assert sensor.state == "30" - - -async def test_arrival_only_allowed_for_timetable(hass, caplog): - """Test that arrival is only allowed when mode is publicTransportTimeTable.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": CAR_ORIGIN_LATITUDE, - "origin_longitude": CAR_ORIGIN_LONGITUDE, - "destination_latitude": CAR_DESTINATION_LATITUDE, - "destination_longitude": CAR_DESTINATION_LONGITUDE, - "api_key": API_KEY, - "arrival": "01:00:00", - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - assert "[arrival] is an invalid option" in caplog.text - - -async def test_exclusive_arrival_and_departure(hass, caplog): - """Test that arrival and departure are exclusive.""" - config = { - DOMAIN: { - "platform": PLATFORM, - "name": "test", - "origin_latitude": CAR_ORIGIN_LATITUDE, - "origin_longitude": CAR_ORIGIN_LONGITUDE, - "destination_latitude": CAR_DESTINATION_LATITUDE, - "destination_longitude": CAR_DESTINATION_LONGITUDE, - "api_key": API_KEY, - "arrival": "01:00:00", - "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE, - "departure": "01:00:00", - } - } - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() - assert "two or more values in the same group of exclusion" in caplog.text + assert ( + "Your HERE travel time configuration has been imported into the UI" + in caplog.text + ) From b38289ee507d164e15dc3021b6aaae6134534eba Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 21 May 2022 08:39:41 -0700 Subject: [PATCH 0737/3516] Remove google found_calendar service (#72260) * Remove google found_calendar service * Partial revert of yaml changes * Store calendar id in a local variable --- homeassistant/components/google/__init__.py | 32 +++++++++---------- homeassistant/components/google/services.yaml | 3 -- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 61b25a9c027..73b7f8d8ae6 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -10,7 +10,7 @@ from typing import Any import aiohttp from gcal_sync.api import GoogleCalendarService from gcal_sync.exceptions import ApiException -from gcal_sync.model import DateOrDatetime, Event +from gcal_sync.model import Calendar, DateOrDatetime, Event from oauth2client.file import Storage import voluptuous as vol from voluptuous.error import Error as VoluptuousError @@ -87,7 +87,6 @@ NOTIFICATION_TITLE = "Google Calendar Setup" GROUP_NAME_ALL_CALENDARS = "Google Calendar Sensors" SERVICE_SCAN_CALENDARS = "scan_for_calendars" -SERVICE_FOUND_CALENDARS = "found_calendar" SERVICE_ADD_EVENT = "add_event" YAML_DEVICES = f"{DOMAIN}_calendars.yaml" @@ -250,19 +249,19 @@ async def async_setup_services( ) -> None: """Set up the service listeners.""" - created_calendars = set() calendars = await hass.async_add_executor_job( load_config, hass.config.path(YAML_DEVICES) ) - async def _found_calendar(call: ServiceCall) -> None: - calendar = get_calendar_info(hass, call.data) - calendar_id = calendar[CONF_CAL_ID] - - if calendar_id in created_calendars: - return - created_calendars.add(calendar_id) - + async def _found_calendar(calendar_item: Calendar) -> None: + calendar = get_calendar_info( + hass, + { + **calendar_item.dict(exclude_unset=True), + CONF_TRACK: track_new, + }, + ) + calendar_id = calendar_item.id # Populate the yaml file with all discovered calendars if calendar_id not in calendars: calendars[calendar_id] = calendar @@ -274,7 +273,7 @@ async def async_setup_services( calendar = calendars[calendar_id] async_dispatcher_send(hass, DISCOVER_CALENDAR, calendar) - hass.services.async_register(DOMAIN, SERVICE_FOUND_CALENDARS, _found_calendar) + created_calendars = set() async def _scan_for_calendars(call: ServiceCall) -> None: """Scan for new calendars.""" @@ -284,11 +283,10 @@ async def async_setup_services( raise HomeAssistantError(str(err)) from err tasks = [] for calendar_item in result.items: - calendar = calendar_item.dict(exclude_unset=True) - calendar[CONF_TRACK] = track_new - tasks.append( - hass.services.async_call(DOMAIN, SERVICE_FOUND_CALENDARS, calendar) - ) + if calendar_item.id in created_calendars: + continue + created_calendars.add(calendar_item.id) + tasks.append(_found_calendar(calendar_item)) await asyncio.gather(*tasks) hass.services.async_register(DOMAIN, SERVICE_SCAN_CALENDARS, _scan_for_calendars) diff --git a/homeassistant/components/google/services.yaml b/homeassistant/components/google/services.yaml index ff053f1be33..21df763374f 100644 --- a/homeassistant/components/google/services.yaml +++ b/homeassistant/components/google/services.yaml @@ -1,6 +1,3 @@ -found_calendar: - name: Found Calendar - description: Add calendar if it has not been already discovered. scan_for_calendars: name: Scan for calendars description: Scan for new calendars. From 809808dd80055c4cd4abc08556f7bead41049efa Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 21 May 2022 19:04:36 +0200 Subject: [PATCH 0738/3516] Move manual configuration of MQTT camera to the integration key (#72249) Add camera Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 1 + homeassistant/components/mqtt/camera.py | 28 +++++++++++++++++++---- tests/components/mqtt/test_camera.py | 14 ++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index f723d8da3ca..0cdba89f25e 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -193,6 +193,7 @@ PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( vol.Optional(Platform.ALARM_CONTROL_PANEL.value): cv.ensure_list, vol.Optional(Platform.BINARY_SENSOR.value): cv.ensure_list, vol.Optional(Platform.BUTTON.value): cv.ensure_list, + vol.Optional(Platform.CAMERA.value): cv.ensure_list, vol.Optional(Platform.FAN.value): cv.ensure_list, vol.Optional(Platform.LIGHT.value): cv.ensure_list, } diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 176e8e86b6b..2e5d95ebda4 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -1,6 +1,7 @@ """Camera that loads a picture from an MQTT topic.""" from __future__ import annotations +import asyncio from base64 import b64decode import functools @@ -22,8 +23,10 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_get_platform_config_from_yaml, async_setup_entry_helper, async_setup_platform_helper, + warn_for_legacy_schema, ) DEFAULT_NAME = "MQTT Camera" @@ -37,14 +40,20 @@ MQTT_CAMERA_ATTRIBUTES_BLOCKED = frozenset( } ) -PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -DISCOVERY_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA) +# Configuring MQTT Camera under the camera platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA = vol.All( + cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_MODERN.schema), + warn_for_legacy_schema(camera.DOMAIN), +) + +DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) async def async_setup_platform( @@ -53,7 +62,8 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up MQTT camera through configuration.yaml.""" + """Set up MQTT camera configured under the camera platform key (deprecated).""" + # Deprecated in HA Core 2022.6 await async_setup_platform_helper( hass, camera.DOMAIN, config, async_add_entities, _async_setup_entity ) @@ -64,7 +74,17 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT camera dynamically through MQTT discovery.""" + """Set up MQTT camera through configuration.yaml and dynamically through MQTT discovery.""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, camera.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 48f73a504bf..204103152a7 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -1,5 +1,6 @@ """The tests for mqtt camera component.""" from base64 import b64encode +import copy from http import HTTPStatus import json from unittest.mock import patch @@ -32,6 +33,7 @@ from .test_common import ( help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -289,3 +291,15 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): domain = camera.DOMAIN config = DEFAULT_CONFIG[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = camera.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None From 17669a728c5817392557436b586147773b445346 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sat, 21 May 2022 19:07:26 +0200 Subject: [PATCH 0739/3516] Move manual configuration of MQTT lock to the integration key (#72271) Add lock --- homeassistant/components/mqtt/__init__.py | 1 + homeassistant/components/mqtt/lock.py | 28 +++++++++++++++++++---- tests/components/mqtt/test_lock.py | 14 ++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 0cdba89f25e..dfef057a7f9 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -196,6 +196,7 @@ PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( vol.Optional(Platform.CAMERA.value): cv.ensure_list, vol.Optional(Platform.FAN.value): cv.ensure_list, vol.Optional(Platform.LIGHT.value): cv.ensure_list, + vol.Optional(Platform.LOCK.value): cv.ensure_list, } ) diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 66efe9fece7..5dc0a974d26 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -1,6 +1,7 @@ """Support for MQTT locks.""" from __future__ import annotations +import asyncio import functools import voluptuous as vol @@ -27,8 +28,10 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_get_platform_config_from_yaml, async_setup_entry_helper, async_setup_platform_helper, + warn_for_legacy_schema, ) CONF_PAYLOAD_LOCK = "payload_lock" @@ -53,7 +56,7 @@ MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset( } ) -PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, @@ -66,7 +69,13 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -DISCOVERY_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA) +# Configuring MQTT Locks under the lock platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA = vol.All( + cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_MODERN.schema), + warn_for_legacy_schema(lock.DOMAIN), +) + +DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) async def async_setup_platform( @@ -75,7 +84,8 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up MQTT lock panel through configuration.yaml.""" + """Set up MQTT locks configured under the lock platform key (deprecated).""" + # Deprecated in HA Core 2022.6 await async_setup_platform_helper( hass, lock.DOMAIN, config, async_add_entities, _async_setup_entity ) @@ -86,7 +96,17 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT lock dynamically through MQTT discovery.""" + """Set up MQTT lock through configuration.yaml and dynamically through MQTT discovery.""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, lock.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 86e21a261a3..ef752ef8749 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -1,4 +1,5 @@ """The tests for the MQTT lock platform.""" +import copy from unittest.mock import patch import pytest @@ -44,6 +45,7 @@ from .test_common import ( help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -675,3 +677,15 @@ async def test_encoding_subscribable_topics( attribute, attribute_value, ) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = LOCK_DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None From 3f8c896cb20526ef59e285b1f0eeb9b4e734efee Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 21 May 2022 11:22:27 -0700 Subject: [PATCH 0740/3516] Set user friendly name for Google Calendar config entry (#72243) * Set user friendly name for Google Calendar config entry * Add a new auth implementation for use during the config flow --- homeassistant/components/google/api.py | 22 +++++++ .../components/google/config_flow.py | 18 ++++- tests/components/google/conftest.py | 30 +++++++-- tests/components/google/test_config_flow.py | 66 ++++++++++++++++++- 4 files changed, 128 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index dceeb6ba6a4..3ce21fbb03d 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -173,3 +173,25 @@ class ApiAuthImpl(AbstractAuth): """Return a valid access token.""" await self._session.async_ensure_token_valid() return self._session.token["access_token"] + + +class AccessTokenAuthImpl(AbstractAuth): + """Authentication implementation used during config flow, without refresh. + + This exists to allow the config flow to use the API before it has fully + created a config entry required by OAuth2Session. This does not support + refreshing tokens, which is fine since it should have been just created. + """ + + def __init__( + self, + websession: aiohttp.ClientSession, + access_token: str, + ) -> None: + """Init the Google Calendar client library auth implementation.""" + super().__init__(websession) + self._access_token = access_token + + async def async_get_access_token(self) -> str: + """Return the access token.""" + return self._access_token diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index f5e567a5272..3b513f17197 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -4,13 +4,17 @@ from __future__ import annotations import logging from typing import Any +from gcal_sync.api import GoogleCalendarService +from gcal_sync.exceptions import ApiException from oauth2client.client import Credentials from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .api import ( DEVICE_AUTH_CREDS, + AccessTokenAuthImpl, DeviceAuth, DeviceFlow, OAuthError, @@ -130,7 +134,19 @@ class OAuth2FlowHandler( self.hass.config_entries.async_update_entry(entry, data=data) await self.hass.config_entries.async_reload(entry.entry_id) return self.async_abort(reason="reauth_successful") - return self.async_create_entry(title=self.flow_impl.name, data=data) + + calendar_service = GoogleCalendarService( + AccessTokenAuthImpl( + async_get_clientsession(self.hass), data["token"]["access_token"] + ) + ) + try: + primary_calendar = await calendar_service.async_get_calendar("primary") + except ApiException as err: + _LOGGER.debug("Error reading calendar primary calendar: %s", err) + primary_calendar = None + title = primary_calendar.id if primary_calendar else self.flow_impl.name + return self.async_create_entry(title=title, data=data) async def async_step_reauth( self, user_input: dict[str, Any] | None = None diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index f48996de72a..48badbc3ab5 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -6,6 +6,7 @@ import datetime from typing import Any, Generator, TypeVar from unittest.mock import mock_open, patch +from aiohttp.client_exceptions import ClientError from gcal_sync.auth import API_BASE_URL from oauth2client.client import Credentials, OAuth2Credentials import pytest @@ -207,7 +208,9 @@ def mock_events_list( """Fixture to construct a fake event list API response.""" def _put_result( - response: dict[str, Any], calendar_id: str = None, exc: Exception = None + response: dict[str, Any], + calendar_id: str = None, + exc: ClientError | None = None, ) -> None: if calendar_id is None: calendar_id = CALENDAR_ID @@ -240,7 +243,7 @@ def mock_calendars_list( ) -> ApiResult: """Fixture to construct a fake calendar list API response.""" - def _put_result(response: dict[str, Any], exc=None) -> None: + def _result(response: dict[str, Any], exc: ClientError | None = None) -> None: aioclient_mock.get( f"{API_BASE_URL}/users/me/calendarList", json=response, @@ -248,13 +251,32 @@ def mock_calendars_list( ) return - return _put_result + return _result + + +@pytest.fixture +def mock_calendar_get( + aioclient_mock: AiohttpClientMocker, +) -> Callable[[...], None]: + """Fixture for returning a calendar get response.""" + + def _result( + calendar_id: str, response: dict[str, Any], exc: ClientError | None = None + ) -> None: + aioclient_mock.get( + f"{API_BASE_URL}/calendars/{calendar_id}", + json=response, + exc=exc, + ) + return + + return _result @pytest.fixture def mock_insert_event( aioclient_mock: AiohttpClientMocker, -) -> Callable[[..., dict[str, Any]], None]: +) -> Callable[[...], None]: """Fixture for capturing event creation.""" def _expect_result(calendar_id: str = CALENDAR_ID) -> None: diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index 88a090773bc..625d1faa937 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -1,9 +1,13 @@ """Test the google config flow.""" +from __future__ import annotations + +from collections.abc import Callable import datetime from typing import Any from unittest.mock import Mock, patch +from aiohttp.client_exceptions import ClientError from oauth2client.client import ( FlowExchangeError, OAuth2Credentials, @@ -27,6 +31,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed CODE_CHECK_INTERVAL = 1 CODE_CHECK_ALARM_TIMEDELTA = datetime.timedelta(seconds=CODE_CHECK_INTERVAL * 2) +EMAIL_ADDRESS = "user@gmail.com" @pytest.fixture(autouse=True) @@ -63,6 +68,24 @@ async def mock_exchange(creds: OAuth2Credentials) -> YieldFixture[Mock]: yield mock +@pytest.fixture +async def primary_calendar_error() -> ClientError | None: + """Fixture for tests to inject an error during calendar lookup.""" + return None + + +@pytest.fixture(autouse=True) +async def primary_calendar( + mock_calendar_get: Callable[[...], None], primary_calendar_error: ClientError | None +) -> None: + """Fixture to return the primary calendar.""" + mock_calendar_get( + "primary", + {"id": EMAIL_ADDRESS, "summary": "Personal"}, + exc=primary_calendar_error, + ) + + async def fire_alarm(hass, point_in_time): """Fire an alarm and wait for callbacks to run.""" with patch("homeassistant.util.dt.utcnow", return_value=point_in_time): @@ -99,7 +122,7 @@ async def test_full_flow_yaml_creds( ) assert result.get("type") == "create_entry" - assert result.get("title") == "Import from configuration.yaml" + assert result.get("title") == EMAIL_ADDRESS assert "data" in result data = result["data"] assert "token" in data @@ -161,7 +184,7 @@ async def test_full_flow_application_creds( ) assert result.get("type") == "create_entry" - assert result.get("title") == "Import from configuration.yaml" + assert result.get("title") == EMAIL_ADDRESS assert "data" in result data = result["data"] assert "token" in data @@ -278,7 +301,7 @@ async def test_exchange_error( ) assert result.get("type") == "create_entry" - assert result.get("title") == "Import from configuration.yaml" + assert result.get("title") == EMAIL_ADDRESS assert "data" in result data = result["data"] assert "token" in data @@ -463,3 +486,40 @@ async def test_reauth_flow( } assert len(mock_setup.mock_calls) == 1 + + +@pytest.mark.parametrize("primary_calendar_error", [ClientError()]) +async def test_title_lookup_failure( + hass: HomeAssistant, + mock_code_flow: Mock, + mock_exchange: Mock, + component_setup: ComponentSetup, +) -> None: + """Test successful config flow and title fetch fails gracefully.""" + assert await component_setup() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "progress" + assert result.get("step_id") == "auth" + assert "description_placeholders" in result + assert "url" in result["description_placeholders"] + + with patch( + "homeassistant.components.google.async_setup_entry", return_value=True + ) as mock_setup: + # Run one tick to invoke the credential exchange check + now = utcnow() + await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"] + ) + + assert result.get("type") == "create_entry" + assert result.get("title") == "Import from configuration.yaml" + + assert len(mock_setup.mock_calls) == 1 + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 From abf9aab18f9a6953b49c4f8aee1ca7e560911e36 Mon Sep 17 00:00:00 2001 From: xLarry Date: Sat, 21 May 2022 21:18:01 +0200 Subject: [PATCH 0741/3516] Add laundrify integration (#65090) * First version of laundrify integration * Code cleanup * Code cleanup after review #2 * Move coordinator to its own file * Save devices as dict and implement available prop as fn * Validate token on init, abort if already configured * Some more cleanup after review * Add strict type hints * Minor changes after code review * Remove OptionsFlow (use default poll interval instead) * Fix CODEOWNERS to pass hassfest job * Fix formatting to pass prettier job * Fix mypy typing error * Update internal device property after fetching data * Call parental update handler and remove obsolete code * Add coordinator tests and fix some config flow tests * Refactor tests * Refactor fixtures * Device unavailable if polling fails --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/laundrify/__init__.py | 52 +++++++ .../components/laundrify/binary_sensor.py | 84 ++++++++++++ .../components/laundrify/config_flow.py | 94 +++++++++++++ homeassistant/components/laundrify/const.py | 10 ++ .../components/laundrify/coordinator.py | 46 +++++++ .../components/laundrify/manifest.json | 9 ++ homeassistant/components/laundrify/model.py | 13 ++ .../components/laundrify/strings.json | 25 ++++ .../components/laundrify/translations/en.json | 25 ++++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/laundrify/__init__.py | 22 +++ tests/components/laundrify/conftest.py | 51 +++++++ tests/components/laundrify/const.py | 11 ++ .../laundrify/fixtures/machines.json | 8 ++ .../components/laundrify/test_config_flow.py | 129 ++++++++++++++++++ .../components/laundrify/test_coordinator.py | 50 +++++++ tests/components/laundrify/test_init.py | 59 ++++++++ 22 files changed, 709 insertions(+) create mode 100644 homeassistant/components/laundrify/__init__.py create mode 100644 homeassistant/components/laundrify/binary_sensor.py create mode 100644 homeassistant/components/laundrify/config_flow.py create mode 100644 homeassistant/components/laundrify/const.py create mode 100644 homeassistant/components/laundrify/coordinator.py create mode 100644 homeassistant/components/laundrify/manifest.json create mode 100644 homeassistant/components/laundrify/model.py create mode 100644 homeassistant/components/laundrify/strings.json create mode 100644 homeassistant/components/laundrify/translations/en.json create mode 100644 tests/components/laundrify/__init__.py create mode 100644 tests/components/laundrify/conftest.py create mode 100644 tests/components/laundrify/const.py create mode 100644 tests/components/laundrify/fixtures/machines.json create mode 100644 tests/components/laundrify/test_config_flow.py create mode 100644 tests/components/laundrify/test_coordinator.py create mode 100644 tests/components/laundrify/test_init.py diff --git a/.strict-typing b/.strict-typing index 4ec4fbff5ee..563e6e9f91a 100644 --- a/.strict-typing +++ b/.strict-typing @@ -138,6 +138,7 @@ homeassistant.components.kaleidescape.* homeassistant.components.knx.* homeassistant.components.kraken.* homeassistant.components.lametric.* +homeassistant.components.laundrify.* homeassistant.components.lcn.* homeassistant.components.light.* homeassistant.components.local_ip.* diff --git a/CODEOWNERS b/CODEOWNERS index 4e1dcd5d93e..05e0d0b4377 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -556,6 +556,8 @@ build.json @home-assistant/supervisor /homeassistant/components/lametric/ @robbiet480 @frenck /homeassistant/components/launch_library/ @ludeeus @DurgNomis-drol /tests/components/launch_library/ @ludeeus @DurgNomis-drol +/homeassistant/components/laundrify/ @xLarry +/tests/components/laundrify/ @xLarry /homeassistant/components/lcn/ @alengwenus /tests/components/lcn/ @alengwenus /homeassistant/components/lg_netcast/ @Drafteed diff --git a/homeassistant/components/laundrify/__init__.py b/homeassistant/components/laundrify/__init__.py new file mode 100644 index 00000000000..27fc412abab --- /dev/null +++ b/homeassistant/components/laundrify/__init__.py @@ -0,0 +1,52 @@ +"""The laundrify integration.""" +from __future__ import annotations + +from laundrify_aio import LaundrifyAPI +from laundrify_aio.exceptions import ApiConnectionException, UnauthorizedException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DEFAULT_POLL_INTERVAL, DOMAIN +from .coordinator import LaundrifyUpdateCoordinator + +PLATFORMS = [Platform.BINARY_SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up laundrify from a config entry.""" + + session = async_get_clientsession(hass) + api_client = LaundrifyAPI(entry.data[CONF_ACCESS_TOKEN], session) + + try: + await api_client.validate_token() + except UnauthorizedException as err: + raise ConfigEntryAuthFailed("Invalid authentication") from err + except ApiConnectionException as err: + raise ConfigEntryNotReady("Cannot reach laundrify API") from err + + coordinator = LaundrifyUpdateCoordinator(hass, api_client, DEFAULT_POLL_INTERVAL) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + "api": api_client, + "coordinator": coordinator, + } + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/laundrify/binary_sensor.py b/homeassistant/components/laundrify/binary_sensor.py new file mode 100644 index 00000000000..2f64d8cee78 --- /dev/null +++ b/homeassistant/components/laundrify/binary_sensor.py @@ -0,0 +1,84 @@ +"""Platform for binary sensor integration.""" +from __future__ import annotations + +import logging + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MANUFACTURER, MODEL +from .coordinator import LaundrifyUpdateCoordinator +from .model import LaundrifyDevice + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up sensors from a config entry created in the integrations UI.""" + + coordinator = hass.data[DOMAIN][config.entry_id]["coordinator"] + + async_add_entities( + LaundrifyPowerPlug(coordinator, device) for device in coordinator.data.values() + ) + + +class LaundrifyPowerPlug( + CoordinatorEntity[LaundrifyUpdateCoordinator], BinarySensorEntity +): + """Representation of a laundrify Power Plug.""" + + _attr_device_class = BinarySensorDeviceClass.RUNNING + _attr_icon = "mdi:washing-machine" + + def __init__( + self, coordinator: LaundrifyUpdateCoordinator, device: LaundrifyDevice + ) -> None: + """Pass coordinator to CoordinatorEntity.""" + super().__init__(coordinator) + self._device = device + self._attr_unique_id = device["_id"] + + @property + def device_info(self) -> DeviceInfo: + """Configure the Device of this Entity.""" + return DeviceInfo( + identifiers={(DOMAIN, self._device["_id"])}, + name=self.name, + manufacturer=MANUFACTURER, + model=MODEL, + sw_version=self._device["firmwareVersion"], + ) + + @property + def available(self) -> bool: + """Check if the device is available.""" + return ( + self.unique_id in self.coordinator.data + and self.coordinator.last_update_success + ) + + @property + def name(self) -> str: + """Name of the entity.""" + return self._device["name"] + + @property + def is_on(self) -> bool: + """Return entity state.""" + return self._device["status"] == "ON" + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._device = self.coordinator.data[self.unique_id] + super()._handle_coordinator_update() diff --git a/homeassistant/components/laundrify/config_flow.py b/homeassistant/components/laundrify/config_flow.py new file mode 100644 index 00000000000..d8230863d7c --- /dev/null +++ b/homeassistant/components/laundrify/config_flow.py @@ -0,0 +1,94 @@ +"""Config flow for laundrify integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from laundrify_aio import LaundrifyAPI +from laundrify_aio.exceptions import ( + ApiConnectionException, + InvalidFormat, + UnknownAuthCode, +) +from voluptuous import Required, Schema + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CODE +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = Schema({Required(CONF_CODE): str}) + + +class LaundrifyConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for laundrify.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + return await self.async_step_init(user_input) + + async def async_step_init( + 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="init", data_schema=CONFIG_SCHEMA) + + errors = {} + + try: + access_token = await LaundrifyAPI.exchange_auth_code(user_input[CONF_CODE]) + + session = async_get_clientsession(self.hass) + api_client = LaundrifyAPI(access_token, session) + + account_id = await api_client.get_account_id() + except InvalidFormat: + errors[CONF_CODE] = "invalid_format" + except UnknownAuthCode: + errors[CONF_CODE] = "invalid_auth" + except ApiConnectionException: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + entry_data = {CONF_ACCESS_TOKEN: access_token} + + await self.async_set_unique_id(account_id) + self._abort_if_unique_id_configured() + + # Create a new entry if it doesn't exist + return self.async_create_entry( + title=DOMAIN, + data=entry_data, + ) + + return self.async_show_form( + step_id="init", data_schema=CONFIG_SCHEMA, errors=errors + ) + + async def async_step_reauth( + self, user_input: dict[str, Any] | 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[str, Any] | 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=Schema({}), + ) + return await self.async_step_init() diff --git a/homeassistant/components/laundrify/const.py b/homeassistant/components/laundrify/const.py new file mode 100644 index 00000000000..c312b895234 --- /dev/null +++ b/homeassistant/components/laundrify/const.py @@ -0,0 +1,10 @@ +"""Constants for the laundrify integration.""" + +DOMAIN = "laundrify" + +MANUFACTURER = "laundrify" +MODEL = "WLAN-Adapter (SU02)" + +DEFAULT_POLL_INTERVAL = 60 + +REQUEST_TIMEOUT = 10 diff --git a/homeassistant/components/laundrify/coordinator.py b/homeassistant/components/laundrify/coordinator.py new file mode 100644 index 00000000000..31447490506 --- /dev/null +++ b/homeassistant/components/laundrify/coordinator.py @@ -0,0 +1,46 @@ +"""Custom DataUpdateCoordinator for the laundrify integration.""" +from datetime import timedelta +import logging + +import async_timeout +from laundrify_aio import LaundrifyAPI +from laundrify_aio.exceptions import ApiConnectionException, UnauthorizedException + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, REQUEST_TIMEOUT +from .model import LaundrifyDevice + +_LOGGER = logging.getLogger(__name__) + + +class LaundrifyUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching laundrify API data.""" + + def __init__( + self, hass: HomeAssistant, laundrify_api: LaundrifyAPI, poll_interval: int + ) -> None: + """Initialize laundrify coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=poll_interval), + ) + self.laundrify_api = laundrify_api + + async def _async_update_data(self) -> dict[str, LaundrifyDevice]: + """Fetch data from laundrify API.""" + try: + # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # handled by the data update coordinator. + async with async_timeout.timeout(REQUEST_TIMEOUT): + return {m["_id"]: m for m in await self.laundrify_api.get_machines()} + except UnauthorizedException as err: + # Raising ConfigEntryAuthFailed will cancel future updates + # and start a config flow with SOURCE_REAUTH (async_step_reauth) + raise ConfigEntryAuthFailed from err + except ApiConnectionException as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err diff --git a/homeassistant/components/laundrify/manifest.json b/homeassistant/components/laundrify/manifest.json new file mode 100644 index 00000000000..6a61446d31c --- /dev/null +++ b/homeassistant/components/laundrify/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "laundrify", + "name": "laundrify", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/laundrify", + "requirements": ["laundrify_aio==1.1.1"], + "codeowners": ["@xLarry"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/laundrify/model.py b/homeassistant/components/laundrify/model.py new file mode 100644 index 00000000000..aa6bf77509f --- /dev/null +++ b/homeassistant/components/laundrify/model.py @@ -0,0 +1,13 @@ +"""Models for laundrify platform.""" +from __future__ import annotations + +from typing import TypedDict + + +class LaundrifyDevice(TypedDict): + """laundrify Power Plug.""" + + _id: str + name: str + status: str + firmwareVersion: str diff --git a/homeassistant/components/laundrify/strings.json b/homeassistant/components/laundrify/strings.json new file mode 100644 index 00000000000..b2fea9f307f --- /dev/null +++ b/homeassistant/components/laundrify/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "init": { + "description": "Please enter your personal Auth Code that is shown in the laundrify-App.", + "data": { + "code": "Auth Code (xxx-xxx)" + } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The laundrify integration needs to re-authenticate." + } + }, + "error": { + "invalid_format": "Invalid format. Please specify as xxx-xxx.", + "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": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + } +} diff --git a/homeassistant/components/laundrify/translations/en.json b/homeassistant/components/laundrify/translations/en.json new file mode 100644 index 00000000000..2bfb07d4041 --- /dev/null +++ b/homeassistant/components/laundrify/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_format": "Invalid format. Please specify as xxx-xxx.", + "unknown": "Unexpected error" + }, + "step": { + "init": { + "data": { + "code": "Auth Code (xxx-xxx)" + }, + "description": "Please enter your personal AuthCode that is shown in the laundrify-App." + }, + "reauth_confirm": { + "description": "The laundrify integration needs to re-authenticate.", + "title": "Reauthenticate Integration" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 67299f403c2..b0bbd3e1b36 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -187,6 +187,7 @@ FLOWS = { "kraken", "kulersky", "launch_library", + "laundrify", "life360", "lifx", "litejet", diff --git a/mypy.ini b/mypy.ini index 8f0b1868bce..ed073141fe1 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1281,6 +1281,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.laundrify.*] +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.lcn.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 4c88b57b9f4..ab4a232e864 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -926,6 +926,9 @@ krakenex==2.1.0 # homeassistant.components.eufy lakeside==0.12 +# homeassistant.components.laundrify +laundrify_aio==1.1.1 + # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ca6b5fd1628..5159abb4f43 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -654,6 +654,9 @@ kostal_plenticore==0.2.0 # homeassistant.components.kraken krakenex==2.1.0 +# homeassistant.components.laundrify +laundrify_aio==1.1.1 + # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/tests/components/laundrify/__init__.py b/tests/components/laundrify/__init__.py new file mode 100644 index 00000000000..c09c6290adf --- /dev/null +++ b/tests/components/laundrify/__init__.py @@ -0,0 +1,22 @@ +"""Tests for the laundrify integration.""" + +from homeassistant.components.laundrify import DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant + +from .const import VALID_ACCESS_TOKEN, VALID_ACCOUNT_ID + +from tests.common import MockConfigEntry + + +def create_entry( + hass: HomeAssistant, access_token: str = VALID_ACCESS_TOKEN +) -> MockConfigEntry: + """Create laundrify entry in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=VALID_ACCOUNT_ID, + data={CONF_ACCESS_TOKEN: access_token}, + ) + entry.add_to_hass(hass) + return entry diff --git a/tests/components/laundrify/conftest.py b/tests/components/laundrify/conftest.py new file mode 100644 index 00000000000..13e408b61a9 --- /dev/null +++ b/tests/components/laundrify/conftest.py @@ -0,0 +1,51 @@ +"""Configure py.test.""" +import json +from unittest.mock import patch + +import pytest + +from .const import VALID_ACCESS_TOKEN, VALID_ACCOUNT_ID + +from tests.common import load_fixture + + +@pytest.fixture(name="laundrify_setup_entry") +def laundrify_setup_entry_fixture(): + """Mock laundrify setup entry function.""" + with patch( + "homeassistant.components.laundrify.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture(name="laundrify_exchange_code") +def laundrify_exchange_code_fixture(): + """Mock laundrify exchange_auth_code function.""" + with patch( + "laundrify_aio.LaundrifyAPI.exchange_auth_code", + return_value=VALID_ACCESS_TOKEN, + ) as exchange_code_mock: + yield exchange_code_mock + + +@pytest.fixture(name="laundrify_validate_token") +def laundrify_validate_token_fixture(): + """Mock laundrify validate_token function.""" + with patch( + "laundrify_aio.LaundrifyAPI.validate_token", + return_value=True, + ) as validate_token_mock: + yield validate_token_mock + + +@pytest.fixture(name="laundrify_api_mock", autouse=True) +def laundrify_api_fixture(laundrify_exchange_code, laundrify_validate_token): + """Mock valid laundrify API responses.""" + with patch( + "laundrify_aio.LaundrifyAPI.get_account_id", + return_value=VALID_ACCOUNT_ID, + ), patch( + "laundrify_aio.LaundrifyAPI.get_machines", + return_value=json.loads(load_fixture("laundrify/machines.json")), + ) as get_machines_mock: + yield get_machines_mock diff --git a/tests/components/laundrify/const.py b/tests/components/laundrify/const.py new file mode 100644 index 00000000000..644631917c6 --- /dev/null +++ b/tests/components/laundrify/const.py @@ -0,0 +1,11 @@ +"""Constants for the laundrify tests.""" + +from homeassistant.const import CONF_CODE + +VALID_AUTH_CODE = "999-001" +VALID_ACCESS_TOKEN = "validAccessToken1234" +VALID_ACCOUNT_ID = "1234" + +VALID_USER_INPUT = { + CONF_CODE: VALID_AUTH_CODE, +} diff --git a/tests/components/laundrify/fixtures/machines.json b/tests/components/laundrify/fixtures/machines.json new file mode 100644 index 00000000000..ab1a737cb45 --- /dev/null +++ b/tests/components/laundrify/fixtures/machines.json @@ -0,0 +1,8 @@ +[ + { + "_id": "14", + "name": "Demo Waschmaschine", + "status": "OFF", + "firmwareVersion": "2.1.0" + } +] diff --git a/tests/components/laundrify/test_config_flow.py b/tests/components/laundrify/test_config_flow.py new file mode 100644 index 00000000000..5ee3efe1e45 --- /dev/null +++ b/tests/components/laundrify/test_config_flow.py @@ -0,0 +1,129 @@ +"""Test the laundrify config flow.""" + +from laundrify_aio import exceptions + +from homeassistant.components.laundrify.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CODE, 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 create_entry +from .const import VALID_ACCESS_TOKEN, VALID_AUTH_CODE, VALID_USER_INPUT + + +async def test_form(hass: HomeAssistant, laundrify_setup_entry) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=VALID_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == DOMAIN + assert result["data"] == { + CONF_ACCESS_TOKEN: VALID_ACCESS_TOKEN, + } + assert len(laundrify_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_format( + hass: HomeAssistant, laundrify_exchange_code +) -> None: + """Test we handle invalid format.""" + laundrify_exchange_code.side_effect = exceptions.InvalidFormat + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data={CONF_CODE: "invalidFormat"}, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {CONF_CODE: "invalid_format"} + + +async def test_form_invalid_auth(hass: HomeAssistant, laundrify_exchange_code) -> None: + """Test we handle invalid auth.""" + laundrify_exchange_code.side_effect = exceptions.UnknownAuthCode + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data=VALID_USER_INPUT, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {CONF_CODE: "invalid_auth"} + + +async def test_form_cannot_connect(hass: HomeAssistant, laundrify_exchange_code): + """Test we handle cannot connect error.""" + laundrify_exchange_code.side_effect = exceptions.ApiConnectionException + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data=VALID_USER_INPUT, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_form_unkown_exception(hass: HomeAssistant, laundrify_exchange_code): + """Test we handle all other errors.""" + laundrify_exchange_code.side_effect = Exception + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data=VALID_USER_INPUT, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} + + +async def test_step_reauth(hass: HomeAssistant) -> None: + """Test the reauth form is shown.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_REAUTH} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_FORM + + +async def test_integration_already_exists(hass: HomeAssistant): + """Test we only allow a single config flow.""" + create_entry(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_CODE: VALID_AUTH_CODE, + }, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/laundrify/test_coordinator.py b/tests/components/laundrify/test_coordinator.py new file mode 100644 index 00000000000..58bc297cc42 --- /dev/null +++ b/tests/components/laundrify/test_coordinator.py @@ -0,0 +1,50 @@ +"""Test the laundrify coordinator.""" + +from laundrify_aio import exceptions + +from homeassistant.components.laundrify.const import DOMAIN +from homeassistant.core import HomeAssistant + +from . import create_entry + + +async def test_coordinator_update_success(hass: HomeAssistant): + """Test the coordinator update is performed successfully.""" + config_entry = create_entry(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + await coordinator.async_refresh() + await hass.async_block_till_done() + + assert coordinator.last_update_success + + +async def test_coordinator_update_unauthorized(hass: HomeAssistant, laundrify_api_mock): + """Test the coordinator update fails if an UnauthorizedException is thrown.""" + config_entry = create_entry(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + laundrify_api_mock.side_effect = exceptions.UnauthorizedException + await coordinator.async_refresh() + await hass.async_block_till_done() + + assert not coordinator.last_update_success + + +async def test_coordinator_update_connection_failed( + hass: HomeAssistant, laundrify_api_mock +): + """Test the coordinator update fails if an ApiConnectionException is thrown.""" + config_entry = create_entry(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"] + laundrify_api_mock.side_effect = exceptions.ApiConnectionException + await coordinator.async_refresh() + await hass.async_block_till_done() + + assert not coordinator.last_update_success diff --git a/tests/components/laundrify/test_init.py b/tests/components/laundrify/test_init.py new file mode 100644 index 00000000000..129848e8808 --- /dev/null +++ b/tests/components/laundrify/test_init.py @@ -0,0 +1,59 @@ +"""Test the laundrify init file.""" + +from laundrify_aio import exceptions + +from homeassistant.components.laundrify.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import create_entry + + +async def test_setup_entry_api_unauthorized( + hass: HomeAssistant, laundrify_validate_token +): + """Test that ConfigEntryAuthFailed is thrown when authentication fails.""" + laundrify_validate_token.side_effect = exceptions.UnauthorizedException + config_entry = create_entry(hass) + + 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 == ConfigEntryState.SETUP_ERROR + assert not hass.data.get(DOMAIN) + + +async def test_setup_entry_api_cannot_connect( + hass: HomeAssistant, laundrify_validate_token +): + """Test that ApiConnectionException is thrown when connection fails.""" + laundrify_validate_token.side_effect = exceptions.ApiConnectionException + config_entry = create_entry(hass) + + 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 == ConfigEntryState.SETUP_RETRY + assert not hass.data.get(DOMAIN) + + +async def test_setup_entry_successful(hass: HomeAssistant): + """Test entry can be setup successfully.""" + config_entry = create_entry(hass) + 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 == ConfigEntryState.LOADED + + +async def test_setup_entry_unload(hass: HomeAssistant): + """Test unloading the laundrify entry.""" + config_entry = create_entry(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.config_entries.async_unload(config_entry.entry_id) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state == ConfigEntryState.NOT_LOADED From 654c59c19414c52f95f56603c460f63edeb60959 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sat, 21 May 2022 19:35:27 -0400 Subject: [PATCH 0742/3516] Add diagnostics for UniFi Protect (#72280) --- .../components/unifiprotect/diagnostics.py | 21 +++++++ tests/components/unifiprotect/conftest.py | 13 +++++ .../unifiprotect/test_diagnostics.py | 56 +++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 homeassistant/components/unifiprotect/diagnostics.py create mode 100644 tests/components/unifiprotect/test_diagnostics.py diff --git a/homeassistant/components/unifiprotect/diagnostics.py b/homeassistant/components/unifiprotect/diagnostics.py new file mode 100644 index 00000000000..b76c9eba1e7 --- /dev/null +++ b/homeassistant/components/unifiprotect/diagnostics.py @@ -0,0 +1,21 @@ +"""Diagnostics support for UniFi Network.""" +from __future__ import annotations + +from typing import Any, cast + +from pyunifiprotect.test_util.anonymize import anonymize_data + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .data import ProtectData + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + + data: ProtectData = hass.data[DOMAIN][config_entry.entry_id] + return cast(dict[str, Any], anonymize_data(data.api.bootstrap.unifi_dict())) diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 6eeef02e817..3986e4cd5a3 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -67,6 +67,19 @@ class MockBootstrap: """Fake process method for tests.""" pass + def unifi_dict(self) -> dict[str, Any]: + """Return UniFi formatted dict representation of the NVR.""" + return { + "nvr": self.nvr.unifi_dict(), + "cameras": [c.unifi_dict() for c in self.cameras.values()], + "lights": [c.unifi_dict() for c in self.lights.values()], + "sensors": [c.unifi_dict() for c in self.sensors.values()], + "viewers": [c.unifi_dict() for c in self.viewers.values()], + "liveviews": [c.unifi_dict() for c in self.liveviews.values()], + "doorlocks": [c.unifi_dict() for c in self.doorlocks.values()], + "chimes": [c.unifi_dict() for c in self.chimes.values()], + } + @dataclass class MockEntityFixture: diff --git a/tests/components/unifiprotect/test_diagnostics.py b/tests/components/unifiprotect/test_diagnostics.py new file mode 100644 index 00000000000..b58e164e913 --- /dev/null +++ b/tests/components/unifiprotect/test_diagnostics.py @@ -0,0 +1,56 @@ +"""Test UniFi Protect diagnostics.""" + +from pyunifiprotect.data import NVR, Light + +from homeassistant.core import HomeAssistant + +from .conftest import MockEntityFixture + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light, hass_client +): + """Test generating diagnostics for a config entry.""" + + light1 = mock_light.copy() + light1._api = mock_entry.api + light1.name = "Test Light 1" + light1.id = "lightid1" + + mock_entry.api.bootstrap.lights = { + light1.id: light1, + } + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + + diag = await get_diagnostics_for_config_entry(hass, hass_client, mock_entry.entry) + + nvr_obj: NVR = mock_entry.api.bootstrap.nvr + # validate some of the data + assert "nvr" in diag and isinstance(diag["nvr"], dict) + nvr = diag["nvr"] + # should have been anonymized + assert nvr["id"] != nvr_obj.id + assert nvr["mac"] != nvr_obj.mac + assert nvr["host"] != str(nvr_obj.host) + # should have been kept + assert nvr["firmwareVersion"] == nvr_obj.firmware_version + assert nvr["version"] == str(nvr_obj.version) + assert nvr["type"] == nvr_obj.type + + assert ( + "lights" in diag + and isinstance(diag["lights"], list) + and len(diag["lights"]) == 1 + ) + light = diag["lights"][0] + # should have been anonymized + assert light["id"] != light1.id + assert light["name"] != light1.mac + assert light["mac"] != light1.mac + assert light["host"] != str(light1.host) + # should have been kept + assert light["firmwareVersion"] == light1.firmware_version + assert light["type"] == light1.type From b6b72f50ec4a21e0c9cc5681bd09193e1a4ec256 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 22 May 2022 00:22:43 +0000 Subject: [PATCH 0743/3516] [ci skip] Translation update --- .../components/adguard/translations/nl.json | 2 +- .../ambient_station/translations/nl.json | 2 +- .../azure_event_hub/translations/nl.json | 2 +- .../cert_expiry/translations/nl.json | 2 +- .../coronavirus/translations/nl.json | 2 +- .../components/deluge/translations/nl.json | 2 +- .../components/discord/translations/nl.json | 2 +- .../components/filesize/translations/nl.json | 2 +- .../components/fivem/translations/nl.json | 2 +- .../components/gdacs/translations/nl.json | 2 +- .../geonetnz_quakes/translations/nl.json | 2 +- .../components/github/translations/nl.json | 2 +- .../google_travel_time/translations/fr.json | 2 +- .../components/hangouts/translations/nl.json | 2 +- .../here_travel_time/translations/ca.json | 82 +++++++++++++++++++ .../here_travel_time/translations/de.json | 82 +++++++++++++++++++ .../here_travel_time/translations/el.json | 82 +++++++++++++++++++ .../here_travel_time/translations/en.json | 82 +++++++++++++++++++ .../here_travel_time/translations/fr.json | 81 ++++++++++++++++++ .../here_travel_time/translations/pt-BR.json | 82 +++++++++++++++++++ .../humidifier/translations/nl.json | 2 +- .../components/hyperion/translations/nl.json | 2 +- .../components/insteon/translations/nl.json | 2 +- .../components/iqvia/translations/nl.json | 2 +- .../components/knx/translations/hu.json | 2 +- .../components/knx/translations/nl.json | 2 +- .../components/konnected/translations/ca.json | 16 ++-- .../components/konnected/translations/de.json | 16 ++-- .../components/konnected/translations/en.json | 16 ++-- .../components/konnected/translations/fr.json | 16 ++-- .../components/konnected/translations/pl.json | 16 ++-- .../konnected/translations/pt-BR.json | 16 ++-- .../konnected/translations/zh-Hant.json | 16 ++-- .../components/laundrify/translations/de.json | 25 ++++++ .../components/laundrify/translations/en.json | 42 +++++----- .../components/laundrify/translations/fr.json | 25 ++++++ .../laundrify/translations/pt-BR.json | 25 ++++++ .../components/luftdaten/translations/nl.json | 2 +- .../components/met/translations/nl.json | 2 +- .../met_eireann/translations/nl.json | 2 +- .../components/metoffice/translations/nl.json | 2 +- .../minecraft_server/translations/nl.json | 2 +- .../motion_blinds/translations/hu.json | 2 +- .../components/motioneye/translations/nl.json | 2 +- .../components/mqtt/translations/nl.json | 2 +- .../components/myq/translations/nl.json | 2 +- .../components/nws/translations/nl.json | 2 +- .../components/onewire/translations/hu.json | 2 +- .../components/peco/translations/nl.json | 2 +- .../components/pi_hole/translations/nl.json | 2 +- .../components/plugwise/translations/nl.json | 2 +- .../pvpc_hourly_pricing/translations/nl.json | 2 +- .../components/remote/translations/nl.json | 4 +- .../components/season/translations/nl.json | 2 +- .../components/shelly/translations/id.json | 1 + .../shopping_list/translations/nl.json | 2 +- .../simplisafe/translations/hu.json | 2 +- .../components/slack/translations/nl.json | 2 +- .../components/sonarr/translations/nl.json | 2 +- .../components/sql/translations/nl.json | 8 +- .../steam_online/translations/nl.json | 2 +- .../stookalert/translations/nl.json | 2 +- .../components/syncthing/translations/nl.json | 2 +- .../tellduslive/translations/nl.json | 2 +- .../components/tibber/translations/nl.json | 2 +- .../ukraine_alarm/translations/hu.json | 6 +- .../ukraine_alarm/translations/nl.json | 2 +- .../components/unifi/translations/hu.json | 2 +- .../components/vallox/translations/nl.json | 2 +- .../vlc_telnet/translations/nl.json | 2 +- .../components/whois/translations/nl.json | 2 +- .../wled/translations/select.nl.json | 2 +- 72 files changed, 704 insertions(+), 137 deletions(-) create mode 100644 homeassistant/components/here_travel_time/translations/ca.json create mode 100644 homeassistant/components/here_travel_time/translations/de.json create mode 100644 homeassistant/components/here_travel_time/translations/el.json create mode 100644 homeassistant/components/here_travel_time/translations/en.json create mode 100644 homeassistant/components/here_travel_time/translations/fr.json create mode 100644 homeassistant/components/here_travel_time/translations/pt-BR.json create mode 100644 homeassistant/components/laundrify/translations/de.json create mode 100644 homeassistant/components/laundrify/translations/fr.json create mode 100644 homeassistant/components/laundrify/translations/pt-BR.json diff --git a/homeassistant/components/adguard/translations/nl.json b/homeassistant/components/adguard/translations/nl.json index 38a64997e73..345bc914ec5 100644 --- a/homeassistant/components/adguard/translations/nl.json +++ b/homeassistant/components/adguard/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd", + "already_configured": "Dienst is al geconfigureerd", "existing_instance_updated": "Bestaande configuratie bijgewerkt." }, "error": { diff --git a/homeassistant/components/ambient_station/translations/nl.json b/homeassistant/components/ambient_station/translations/nl.json index 008bc10e084..332ff9f0a33 100644 --- a/homeassistant/components/ambient_station/translations/nl.json +++ b/homeassistant/components/ambient_station/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "error": { "invalid_key": "Ongeldige API-sleutel", diff --git a/homeassistant/components/azure_event_hub/translations/nl.json b/homeassistant/components/azure_event_hub/translations/nl.json index e0aabd1c971..e75443dbc88 100644 --- a/homeassistant/components/azure_event_hub/translations/nl.json +++ b/homeassistant/components/azure_event_hub/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd", + "already_configured": "Dienst is al geconfigureerd", "cannot_connect": "Verbinding maken met de credentials uit de configuration.yaml is mislukt, verwijder deze uit yaml en gebruik de config flow.", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", "unknown": "Verbinding maken met de credentials uit de configuration.yaml is mislukt met een onbekende fout, verwijder deze uit yaml en gebruik de config flow." diff --git a/homeassistant/components/cert_expiry/translations/nl.json b/homeassistant/components/cert_expiry/translations/nl.json index e3cd3d7983b..e330a8c01dd 100644 --- a/homeassistant/components/cert_expiry/translations/nl.json +++ b/homeassistant/components/cert_expiry/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd", + "already_configured": "Dienst is al geconfigureerd", "import_failed": "Importeren vanuit configuratie is mislukt" }, "error": { diff --git a/homeassistant/components/coronavirus/translations/nl.json b/homeassistant/components/coronavirus/translations/nl.json index fec0b6462eb..9b0872dafa2 100644 --- a/homeassistant/components/coronavirus/translations/nl.json +++ b/homeassistant/components/coronavirus/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd", + "already_configured": "Dienst is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken" }, "step": { diff --git a/homeassistant/components/deluge/translations/nl.json b/homeassistant/components/deluge/translations/nl.json index 6c824130708..d96824b10de 100644 --- a/homeassistant/components/deluge/translations/nl.json +++ b/homeassistant/components/deluge/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/discord/translations/nl.json b/homeassistant/components/discord/translations/nl.json index 303b22fdfad..b84af57af18 100644 --- a/homeassistant/components/discord/translations/nl.json +++ b/homeassistant/components/discord/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd", + "already_configured": "Dienst is al geconfigureerd", "reauth_successful": "Herauthenticatie geslaagd" }, "error": { diff --git a/homeassistant/components/filesize/translations/nl.json b/homeassistant/components/filesize/translations/nl.json index d8a35446044..4b72bc53292 100644 --- a/homeassistant/components/filesize/translations/nl.json +++ b/homeassistant/components/filesize/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "error": { "not_allowed": "Pad is niet toegestaan", diff --git a/homeassistant/components/fivem/translations/nl.json b/homeassistant/components/fivem/translations/nl.json index cac4b7ed12f..ef5952cb60a 100644 --- a/homeassistant/components/fivem/translations/nl.json +++ b/homeassistant/components/fivem/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken. Controleer de host en poort en probeer het opnieuw. Zorg er ook voor dat u de nieuwste FiveM-server gebruikt.", diff --git a/homeassistant/components/gdacs/translations/nl.json b/homeassistant/components/gdacs/translations/nl.json index 6dd3e5aa196..d46f8dc0a50 100644 --- a/homeassistant/components/gdacs/translations/nl.json +++ b/homeassistant/components/gdacs/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "step": { "user": { diff --git a/homeassistant/components/geonetnz_quakes/translations/nl.json b/homeassistant/components/geonetnz_quakes/translations/nl.json index 74766300e11..62f4649f429 100644 --- a/homeassistant/components/geonetnz_quakes/translations/nl.json +++ b/homeassistant/components/geonetnz_quakes/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "step": { "user": { diff --git a/homeassistant/components/github/translations/nl.json b/homeassistant/components/github/translations/nl.json index f7cb2ac22e4..8a53a0b38d4 100644 --- a/homeassistant/components/github/translations/nl.json +++ b/homeassistant/components/github/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd", + "already_configured": "Dienst is al geconfigureerd", "could_not_register": "Kan integratie niet met GitHub registreren" }, "progress": { diff --git a/homeassistant/components/google_travel_time/translations/fr.json b/homeassistant/components/google_travel_time/translations/fr.json index 790fe9117bd..86761315d58 100644 --- a/homeassistant/components/google_travel_time/translations/fr.json +++ b/homeassistant/components/google_travel_time/translations/fr.json @@ -24,7 +24,7 @@ "data": { "avoid": "\u00c9viter de", "language": "Langue", - "mode": "Mode voyage", + "mode": "Mode de d\u00e9placement", "time": "Temps", "time_type": "Type de temps", "transit_mode": "Mode de transit", diff --git a/homeassistant/components/hangouts/translations/nl.json b/homeassistant/components/hangouts/translations/nl.json index 276c310da5a..826bc560045 100644 --- a/homeassistant/components/hangouts/translations/nl.json +++ b/homeassistant/components/hangouts/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd", + "already_configured": "Dienst is al geconfigureerd", "unknown": "Onverwachte fout" }, "error": { diff --git a/homeassistant/components/here_travel_time/translations/ca.json b/homeassistant/components/here_travel_time/translations/ca.json new file mode 100644 index 00000000000..08dfb0d970d --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/ca.json @@ -0,0 +1,82 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "destination_coordinates": { + "data": { + "destination": "Destinaci\u00f3 com a coordenades GPS" + }, + "title": "Tria destinaci\u00f3" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "Destinaci\u00f3 utilitzant entitat" + }, + "title": "Tria destinaci\u00f3" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "Utilitzant una ubicaci\u00f3 de mapa", + "destination_entity": "Utilitzant una entitat" + }, + "title": "Tria destinaci\u00f3" + }, + "origin_coordinates": { + "data": { + "origin": "Origen com a coordenades GPS" + }, + "title": "Tria origen" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "Origen utilitzant entitat" + }, + "title": "Tria origen" + }, + "user": { + "data": { + "api_key": "Clau API", + "mode": "Mode de viatge", + "name": "Nom" + } + } + } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "Hora d'arribada" + }, + "title": "Tria l'hora d'arribada" + }, + "departure_time": { + "data": { + "departure_time": "Hora de sortida" + }, + "title": "Tria l'hora de sortida" + }, + "init": { + "data": { + "route_mode": "Mode ruta", + "traffic_mode": "Mode tr\u00e0nsit", + "unit_system": "Sistema d'unitats" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "Configura hora d'arribada", + "departure_time": "Configura hora de sortida", + "no_time": "No configurar hora" + }, + "title": "Tria tipus d'hora" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/de.json b/homeassistant/components/here_travel_time/translations/de.json new file mode 100644 index 00000000000..b50ef028089 --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/de.json @@ -0,0 +1,82 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "destination_coordinates": { + "data": { + "destination": "Ziel als GPS-Koordinaten" + }, + "title": "Ziel w\u00e4hlen" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "Ziel mit einer Entit\u00e4t" + }, + "title": "Ziel w\u00e4hlen" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "Verwendung einer Kartenposition", + "destination_entity": "Verwenden einer Entit\u00e4t" + }, + "title": "Ziel w\u00e4hlen" + }, + "origin_coordinates": { + "data": { + "origin": "Herkunft als GPS-Koordinaten" + }, + "title": "Herkunft w\u00e4hlen" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "Herkunft \u00fcber eine Entit\u00e4t" + }, + "title": "Herkunft w\u00e4hlen" + }, + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "mode": "Reisemodus", + "name": "Name" + } + } + } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "Ankunftszeit" + }, + "title": "Ankunftszeit w\u00e4hlen" + }, + "departure_time": { + "data": { + "departure_time": "Abfahrtszeit" + }, + "title": "Abfahrtszeit w\u00e4hlen" + }, + "init": { + "data": { + "route_mode": "Routenmodus", + "traffic_mode": "Verkehrsmodus", + "unit_system": "Einheitssystem" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "Ankunftszeit konfigurieren", + "departure_time": "Abfahrtszeit konfigurieren", + "no_time": "Keine Zeit konfigurieren" + }, + "title": "Zeitart w\u00e4hlen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/el.json b/homeassistant/components/here_travel_time/translations/el.json new file mode 100644 index 00000000000..cd4d986ed3f --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/el.json @@ -0,0 +1,82 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "destination_coordinates": { + "data": { + "destination": "\u03a0\u03c1\u03bf\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c9\u03c2 \u03c3\u03c5\u03bd\u03c4\u03b5\u03c4\u03b1\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 GPS" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c0\u03c1\u03bf\u03bf\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "\u03a0\u03c1\u03bf\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c0\u03c1\u03bf\u03bf\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b8\u03ad\u03c3\u03b7\u03c2 \u03c7\u03ac\u03c1\u03c4\u03b7", + "destination_entity": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c0\u03c1\u03bf\u03bf\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd" + }, + "origin_coordinates": { + "data": { + "origin": "\u03a0\u03c1\u03bf\u03ad\u03bb\u03b5\u03c5\u03c3\u03b7 \u03c9\u03c2 \u03c3\u03c5\u03bd\u03c4\u03b5\u03c4\u03b1\u03b3\u03bc\u03ad\u03bd\u03b5\u03c2 GPS" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c0\u03c1\u03bf\u03ad\u03bb\u03b5\u03c5\u03c3\u03b7\u03c2" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "\u03a0\u03c1\u03bf\u03ad\u03bb\u03b5\u03c5\u03c3\u03b7 \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c0\u03c1\u03bf\u03ad\u03bb\u03b5\u03c5\u03c3\u03b7\u03c2" + }, + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c4\u03b1\u03be\u03b9\u03b4\u03b9\u03bf\u03cd", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" + } + } + } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "\u038f\u03c1\u03b1 \u03ac\u03c6\u03b9\u03be\u03b7\u03c2" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03ce\u03c1\u03b1 \u03ac\u03c6\u03b9\u03be\u03b7\u03c2" + }, + "departure_time": { + "data": { + "departure_time": "\u038f\u03c1\u03b1 \u03b1\u03bd\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7\u03c2" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03ce\u03c1\u03b1 \u03b1\u03bd\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7\u03c2" + }, + "init": { + "data": { + "route_mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae\u03c2", + "traffic_mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03ba\u03c5\u03ba\u03bb\u03bf\u03c6\u03bf\u03c1\u03af\u03b1\u03c2", + "unit_system": "\u03a3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03bc\u03bf\u03bd\u03ac\u03b4\u03c9\u03bd" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03ce\u03c1\u03b1\u03c2 \u03ac\u03c6\u03b9\u03be\u03b7\u03c2", + "departure_time": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03ce\u03c1\u03b1\u03c2 \u03b1\u03bd\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7\u03c2", + "no_time": "\u039c\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c4\u03cd\u03c0\u03bf\u03c5 \u03ce\u03c1\u03b1\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/en.json b/homeassistant/components/here_travel_time/translations/en.json new file mode 100644 index 00000000000..d4f9984d945 --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/en.json @@ -0,0 +1,82 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "destination_coordinates": { + "data": { + "destination": "Destination as GPS coordinates" + }, + "title": "Choose Destination" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "Destination using an entity" + }, + "title": "Choose Destination" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "Using a map location", + "destination_entity": "Using an entity" + }, + "title": "Choose Destination" + }, + "origin_coordinates": { + "data": { + "origin": "Origin as GPS coordinates" + }, + "title": "Choose Origin" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "Origin using an entity" + }, + "title": "Choose Origin" + }, + "user": { + "data": { + "api_key": "API Key", + "mode": "Travel Mode", + "name": "Name" + } + } + } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "Arrival Time" + }, + "title": "Choose Arrival Time" + }, + "departure_time": { + "data": { + "departure_time": "Departure Time" + }, + "title": "Choose Departure Time" + }, + "init": { + "data": { + "route_mode": "Route Mode", + "traffic_mode": "Traffic Mode", + "unit_system": "Unit system" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "Configure an arrival time", + "departure_time": "Configure a departure time", + "no_time": "Do not configure a time" + }, + "title": "Choose Time Type" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/fr.json b/homeassistant/components/here_travel_time/translations/fr.json new file mode 100644 index 00000000000..063a4008779 --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/fr.json @@ -0,0 +1,81 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "destination_coordinates": { + "data": { + "destination": "Destination sous forme de coordonn\u00e9es GPS" + }, + "title": "Choix de la destination" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "Destination avec utilisation d'une entit\u00e9" + }, + "title": "Choix de la destination" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "Utilisation d'un emplacement sur la carte", + "destination_entity": "Utilisation d'une entit\u00e9" + }, + "title": "Choix de la destination" + }, + "origin_coordinates": { + "data": { + "origin": "Origine sous forme de coordonn\u00e9es GPS" + }, + "title": "Choix de l'origine" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "Origine avec utilisation d'une entit\u00e9" + }, + "title": "Choix de l'origine" + }, + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "mode": "Mode de d\u00e9placement", + "name": "Nom" + } + } + } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "Heure d'arriv\u00e9e" + }, + "title": "Choix de l'heure d'arriv\u00e9e" + }, + "departure_time": { + "data": { + "departure_time": "Heure de d\u00e9part" + }, + "title": "Choix de l'heure de d\u00e9part" + }, + "init": { + "data": { + "route_mode": "Mode itin\u00e9raire", + "traffic_mode": "Mode circulation", + "unit_system": "Syst\u00e8me d'unit\u00e9s" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "Configurer une heure d'arriv\u00e9e", + "departure_time": "Configurer une heure de d\u00e9part", + "no_time": "Ne configurez pas d'heure" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/pt-BR.json b/homeassistant/components/here_travel_time/translations/pt-BR.json new file mode 100644 index 00000000000..78996561564 --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/pt-BR.json @@ -0,0 +1,82 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "destination_coordinates": { + "data": { + "destination": "Destino como coordenadas GPS" + }, + "title": "Escolha o destino" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "Destino usando uma entidade" + }, + "title": "Escolha o destino" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "Usando uma localiza\u00e7\u00e3o no mapa", + "destination_entity": "Usando uma entidade" + }, + "title": "Escolha o destino" + }, + "origin_coordinates": { + "data": { + "origin": "Origem como coordenadas GPS" + }, + "title": "Escolha a Origem" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "Origem usando uma entidade" + }, + "title": "Escolha a Origem" + }, + "user": { + "data": { + "api_key": "Chave API", + "mode": "Modo de viagem", + "name": "Nome" + } + } + } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "Hora da chegada" + }, + "title": "Escolha a hora de chegada" + }, + "departure_time": { + "data": { + "departure_time": "Hora de partida" + }, + "title": "Escolha o hor\u00e1rio de partida" + }, + "init": { + "data": { + "route_mode": "Modo de rota", + "traffic_mode": "Modo de tr\u00e2nsito", + "unit_system": "Sistema de unidades" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "Configurar um hor\u00e1rio de chegada", + "departure_time": "Configurar um hor\u00e1rio de partida", + "no_time": "N\u00e3o configure um hor\u00e1rio" + }, + "title": "Escolha o tipo de hor\u00e1rio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/nl.json b/homeassistant/components/humidifier/translations/nl.json index 1cdf417e6ef..4ede3b9cb98 100644 --- a/homeassistant/components/humidifier/translations/nl.json +++ b/homeassistant/components/humidifier/translations/nl.json @@ -10,7 +10,7 @@ "condition_type": { "is_mode": "{entity_name} is ingesteld op een specifieke modus", "is_off": "{entity_name} is uitgeschakeld", - "is_on": "{entity_name} staat aan" + "is_on": "{entity_name} is ingeschakeld" }, "trigger_type": { "changed_states": "{entity_name} in- of uitgeschakeld", diff --git a/homeassistant/components/hyperion/translations/nl.json b/homeassistant/components/hyperion/translations/nl.json index ff2cd5f4f6b..c3d1684c250 100644 --- a/homeassistant/components/hyperion/translations/nl.json +++ b/homeassistant/components/hyperion/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd", + "already_configured": "Dienst is al geconfigureerd", "already_in_progress": "De configuratie is momenteel al bezig", "auth_new_token_not_granted_error": "Nieuw aangemaakte token is niet goedgekeurd in Hyperion UI", "auth_new_token_not_work_error": "Verificatie met nieuw aangemaakt token mislukt", diff --git a/homeassistant/components/insteon/translations/nl.json b/homeassistant/components/insteon/translations/nl.json index 2683342ad02..3d05a8ed011 100644 --- a/homeassistant/components/insteon/translations/nl.json +++ b/homeassistant/components/insteon/translations/nl.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "confirm_usb": { - "description": "Wilt u {name} instellen?" + "description": "Wil je {name} instellen?" }, "hubv1": { "data": { diff --git a/homeassistant/components/iqvia/translations/nl.json b/homeassistant/components/iqvia/translations/nl.json index 13d7e0ce4d3..cea9df26bac 100644 --- a/homeassistant/components/iqvia/translations/nl.json +++ b/homeassistant/components/iqvia/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "error": { "invalid_zip_code": "Postcode is ongeldig" diff --git a/homeassistant/components/knx/translations/hu.json b/homeassistant/components/knx/translations/hu.json index ccb9eb63c61..28cab2cea5f 100644 --- a/homeassistant/components/knx/translations/hu.json +++ b/homeassistant/components/knx/translations/hu.json @@ -102,7 +102,7 @@ "multicast_group": "\u00datv\u00e1laszt\u00e1shoz \u00e9s felder\u00edt\u00e9shez haszn\u00e1latos. Alap\u00e9rtelmezett: `224.0.23.12`.", "multicast_port": "\u00datv\u00e1laszt\u00e1shoz \u00e9s felder\u00edt\u00e9shez haszn\u00e1latos. Alap\u00e9rtelmezett: `3671`", "rate_limit": "Maxim\u00e1lis kimen\u0151 \u00fczenet m\u00e1sodpercenk\u00e9nt.\nAj\u00e1nlott: 20 \u00e9s 40 k\u00f6z\u00f6tt", - "state_updater": "Glob\u00e1lisan enged\u00e9lyezze vagy tiltsa le az olvas\u00e1si \u00e1llapotokat a KNX-buszr\u00f3l. Ha le van tiltva, a Home Assistant nem fogja akt\u00edvan lek\u00e9rni az \u00e1llapotokat a KNX-buszr\u00f3l, a \"sync_state\" entit\u00e1sopci\u00f3knak nincs hat\u00e1sa." + "state_updater": "Alap\u00e9rtelmezett be\u00e1ll\u00edt\u00e1s a KNX busz \u00e1llapotainak olvas\u00e1s\u00e1hoz. Ha le va tiltva, Home Assistant nem fog akt\u00edvan lek\u00e9rdezni egys\u00e9g\u00e1llapotokat a KNX buszr\u00f3l. Fel\u00fclb\u00edr\u00e1lhat\u00f3 a `sync_state` entit\u00e1s opci\u00f3kkal." } }, "tunnel": { diff --git a/homeassistant/components/knx/translations/nl.json b/homeassistant/components/knx/translations/nl.json index 1c597507c67..bc8f03eb06c 100644 --- a/homeassistant/components/knx/translations/nl.json +++ b/homeassistant/components/knx/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd", + "already_configured": "Dienst is al geconfigureerd", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { diff --git a/homeassistant/components/konnected/translations/ca.json b/homeassistant/components/konnected/translations/ca.json index c9bf5414d86..8d769749325 100644 --- a/homeassistant/components/konnected/translations/ca.json +++ b/homeassistant/components/konnected/translations/ca.json @@ -39,7 +39,7 @@ "options_binary": { "data": { "inverse": "Inverteix l'estat obert/tancat", - "name": "Nom (opcional)", + "name": "Nom", "type": "Tipus de sensor binari" }, "description": "Opcions de {zone}", @@ -47,8 +47,8 @@ }, "options_digital": { "data": { - "name": "Nom (opcional)", - "poll_interval": "Interval de sondeig (minuts) (opcional)", + "name": "Nom", + "poll_interval": "Interval de sondeig (minuts)", "type": "Tipus de sensor" }, "description": "Opcions de {zone}", @@ -84,7 +84,7 @@ }, "options_misc": { "data": { - "api_host": "Substitueix l'URL d'amfitri\u00f3 d'API (opcional)", + "api_host": "Substitueix l'URL d'amfitri\u00f3 d'API", "blink": "Parpelleja el LED del panell quan s'envien canvis d'estat", "discovery": "Respon a peticions de descobriment a la teva xarxa", "override_api_host": "Substitueix l'URL per defecte del panell d'amfitri\u00f3 de l'API de Home Assistant" @@ -95,11 +95,11 @@ "options_switch": { "data": { "activation": "Sortida quan estigui ON", - "momentary": "Durada del pols (ms) (opcional)", + "momentary": "Durada del pols (ms)", "more_states": "Configura estats addicionals per a aquesta zona", - "name": "Nom (opcional)", - "pause": "Pausa entre polsos (ms) (opcional)", - "repeat": "Repeticions (-1 = infinit) (opcional)" + "name": "Nom", + "pause": "Pausa entre polsos (ms)", + "repeat": "Repeticions (-1 = infinit)" }, "description": "Opcions de {zone}: estat {state}", "title": "Configuraci\u00f3 de sortida commutable" diff --git a/homeassistant/components/konnected/translations/de.json b/homeassistant/components/konnected/translations/de.json index 937425be1dc..82c99839f25 100644 --- a/homeassistant/components/konnected/translations/de.json +++ b/homeassistant/components/konnected/translations/de.json @@ -39,7 +39,7 @@ "options_binary": { "data": { "inverse": "Invertiere den \u00d6ffnungs- / Schlie\u00dfzustand", - "name": "Name (optional)", + "name": "Name", "type": "Bin\u00e4rer Sensortyp" }, "description": "Bitte w\u00e4hle die Optionen f\u00fcr den an {zone} angeschlossenen Bin\u00e4rsensor", @@ -47,8 +47,8 @@ }, "options_digital": { "data": { - "name": "Name (optional)", - "poll_interval": "Abfrageintervall (Minuten) (optional)", + "name": "Name", + "poll_interval": "Abfrageintervall (Minuten)", "type": "Sensortyp" }, "description": "Bitte w\u00e4hle die Optionen f\u00fcr den an {zone} angeschlossenen digitalen Sensor aus", @@ -84,7 +84,7 @@ }, "options_misc": { "data": { - "api_host": "API-Host-URL \u00fcberschreiben (optional)", + "api_host": "API-Host-URL \u00fcberschreiben", "blink": "LED Panel blinkt beim senden von Status\u00e4nderungen", "discovery": "Reagieren auf Suchanfragen in deinem Netzwerk", "override_api_host": "\u00dcberschreibe die Standard-Host-Panel-URL der Home Assistant-API" @@ -95,11 +95,11 @@ "options_switch": { "data": { "activation": "Ausgabe, wenn eingeschaltet", - "momentary": "Impulsdauer (ms) (optional)", + "momentary": "Impulsdauer (ms)", "more_states": "Konfiguriere zus\u00e4tzliche Zust\u00e4nde f\u00fcr diese Zone", - "name": "Name (optional)", - "pause": "Pause zwischen Impulsen (ms) (optional)", - "repeat": "Mal wiederholen (-1 = unendlich) (optional)" + "name": "Name", + "pause": "Pause zwischen Impulsen (ms)", + "repeat": "Mal wiederholen (-1 = unendlich)" }, "description": "Bitte w\u00e4hlen die Ausgabeoptionen f\u00fcr {zone} : Status {state}", "title": "Konfiguriere den schaltbaren Ausgang" diff --git a/homeassistant/components/konnected/translations/en.json b/homeassistant/components/konnected/translations/en.json index b5e6340b562..0015a81ef31 100644 --- a/homeassistant/components/konnected/translations/en.json +++ b/homeassistant/components/konnected/translations/en.json @@ -39,7 +39,7 @@ "options_binary": { "data": { "inverse": "Invert the open/close state", - "name": "Name (optional)", + "name": "Name", "type": "Binary Sensor Type" }, "description": "{zone} options", @@ -47,8 +47,8 @@ }, "options_digital": { "data": { - "name": "Name (optional)", - "poll_interval": "Poll Interval (minutes) (optional)", + "name": "Name", + "poll_interval": "Poll Interval (minutes)", "type": "Sensor Type" }, "description": "{zone} options", @@ -84,7 +84,7 @@ }, "options_misc": { "data": { - "api_host": "Override API host URL (optional)", + "api_host": "Override API host URL", "blink": "Blink panel LED on when sending state change", "discovery": "Respond to discovery requests on your network", "override_api_host": "Override default Home Assistant API host panel URL" @@ -95,11 +95,11 @@ "options_switch": { "data": { "activation": "Output when on", - "momentary": "Pulse duration (ms) (optional)", + "momentary": "Pulse duration (ms)", "more_states": "Configure additional states for this zone", - "name": "Name (optional)", - "pause": "Pause between pulses (ms) (optional)", - "repeat": "Times to repeat (-1=infinite) (optional)" + "name": "Name", + "pause": "Pause between pulses (ms)", + "repeat": "Times to repeat (-1=infinite)" }, "description": "{zone} options: state {state}", "title": "Configure Switchable Output" diff --git a/homeassistant/components/konnected/translations/fr.json b/homeassistant/components/konnected/translations/fr.json index 4b84efa351d..42b62ea490d 100644 --- a/homeassistant/components/konnected/translations/fr.json +++ b/homeassistant/components/konnected/translations/fr.json @@ -41,7 +41,7 @@ "options_binary": { "data": { "inverse": "Inverser l'\u00e9tat ouvert / ferm\u00e9", - "name": "Nom (facultatif)", + "name": "Nom", "type": "Type de capteur binaire" }, "description": "Veuillez s\u00e9lectionner les options du capteur binaire attach\u00e9 \u00e0 {zone}", @@ -49,8 +49,8 @@ }, "options_digital": { "data": { - "name": "Nom (facultatif)", - "poll_interval": "Intervalle d'interrogation (minutes) (facultatif)", + "name": "Nom", + "poll_interval": "Intervalle d'interrogation (en minutes)", "type": "Type de capteur" }, "description": "Veuillez s\u00e9lectionner les options du capteur digital attach\u00e9 \u00e0 {zone}", @@ -86,7 +86,7 @@ }, "options_misc": { "data": { - "api_host": "Remplacer l'URL de l'h\u00f4te de l'API (facultatif)", + "api_host": "Remplacer l'URL de l'h\u00f4te de l'API", "blink": "Voyant du panneau clignotant lors de l'envoi d'un changement d'\u00e9tat", "discovery": "R\u00e9pondre aux demandes de d\u00e9couverte sur votre r\u00e9seau", "override_api_host": "Remplacer l'URL par d\u00e9faut du panneau h\u00f4te de l'API Home Assistant" @@ -97,11 +97,11 @@ "options_switch": { "data": { "activation": "Sortie lorsque activ\u00e9", - "momentary": "Dur\u00e9e de l'impulsion (en millisecondes, facultatif)", + "momentary": "Dur\u00e9e de l'impulsion (en millisecondes)", "more_states": "Configurer des \u00e9tats suppl\u00e9mentaires pour cette zone", - "name": "Nom (facultatif)", - "pause": "Pause entre les impulsions (ms) (facultatif)", - "repeat": "Nombre de r\u00e9p\u00e9tition (-1=infini) (facultatif)" + "name": "Nom", + "pause": "Pause entre les impulsions (en millisecondes)", + "repeat": "Nombre de r\u00e9p\u00e9titions (-1\u00a0=\u00a0infini)" }, "description": "Veuillez s\u00e9lectionner les options de sortie pour {zone} : \u00e9tat {state}", "title": "Configurer la sortie commutable" diff --git a/homeassistant/components/konnected/translations/pl.json b/homeassistant/components/konnected/translations/pl.json index 7352bc82344..3eb24ac2cab 100644 --- a/homeassistant/components/konnected/translations/pl.json +++ b/homeassistant/components/konnected/translations/pl.json @@ -43,7 +43,7 @@ "options_binary": { "data": { "inverse": "Odwr\u00f3\u0107 stan otwarty/zamkni\u0119ty", - "name": "Nazwa (opcjonalnie)", + "name": "Nazwa", "type": "Typ sensora binarnego" }, "description": "Opcje {zone}", @@ -51,8 +51,8 @@ }, "options_digital": { "data": { - "name": "Nazwa (opcjonalnie)", - "poll_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (minuty) (opcjonalnie)", + "name": "Nazwa", + "poll_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (minuty)", "type": "Typ sensora" }, "description": "Opcje {zone}", @@ -88,7 +88,7 @@ }, "options_misc": { "data": { - "api_host": "Zast\u0119powanie adresu URL hosta API (opcjonalnie)", + "api_host": "Zast\u0119powanie adresu URL hosta API", "blink": "Miganie diody LED panelu podczas wysy\u0142ania zmiany stanu", "discovery": "Odpowiadaj na \u017c\u0105dania wykrywania w Twojej sieci", "override_api_host": "Zast\u0105p domy\u015blny adres URL API Home Assistanta" @@ -99,11 +99,11 @@ "options_switch": { "data": { "activation": "Wyj\u015bcie, gdy w\u0142\u0105czone", - "momentary": "Czas trwania impulsu (ms) (opcjonalnie)", + "momentary": "Czas trwania impulsu (ms)", "more_states": "Konfigurowanie dodatkowych stan\u00f3w dla tej strefy", - "name": "Nazwa (opcjonalnie)", - "pause": "Przerwa mi\u0119dzy impulsami (ms) (opcjonalnie)", - "repeat": "Ilo\u015b\u0107 powt\u00f3rze\u0144 (-1=niesko\u0144czenie) (opcjonalnie)" + "name": "Nazwa", + "pause": "Przerwa mi\u0119dzy impulsami (ms)", + "repeat": "Ilo\u015b\u0107 powt\u00f3rze\u0144 (-1=niesko\u0144czenie)" }, "description": "Opcje {zone}: stan {state}", "title": "Konfiguracja prze\u0142\u0105czalne wyj\u015bcie" diff --git a/homeassistant/components/konnected/translations/pt-BR.json b/homeassistant/components/konnected/translations/pt-BR.json index e3079fc50f7..92aaf3ce368 100644 --- a/homeassistant/components/konnected/translations/pt-BR.json +++ b/homeassistant/components/konnected/translations/pt-BR.json @@ -39,7 +39,7 @@ "options_binary": { "data": { "inverse": "Inverter o estado aberto/fechado", - "name": "Nome (opcional)", + "name": "Nome", "type": "Tipo de sensor bin\u00e1rio" }, "description": "Selecione as op\u00e7\u00f5es para o sensor bin\u00e1rio conectado a {zone}", @@ -47,8 +47,8 @@ }, "options_digital": { "data": { - "name": "Nome (opcional)", - "poll_interval": "Intervalo de vota\u00e7\u00e3o (minutos) (opcional)", + "name": "Nome", + "poll_interval": "Intervalo de vota\u00e7\u00e3o (minutos)", "type": "Tipo de sensor" }, "description": "Selecione as op\u00e7\u00f5es para o sensor digital conectado a {zone}", @@ -84,7 +84,7 @@ }, "options_misc": { "data": { - "api_host": "Substituir URL do host da API (opcional)", + "api_host": "Substituir URL do host da API", "blink": "LED do painel piscando ao enviar mudan\u00e7a de estado", "discovery": "Responder \u00e0s solicita\u00e7\u00f5es de descoberta em sua rede", "override_api_host": "Substituir o URL padr\u00e3o do painel do host da API do Home Assistant" @@ -95,11 +95,11 @@ "options_switch": { "data": { "activation": "Sa\u00edda quando ligado", - "momentary": "Dura\u00e7\u00e3o do pulso (ms) (opcional)", + "momentary": "Dura\u00e7\u00e3o do pulso (ms)", "more_states": "Configurar estados adicionais para esta zona", - "name": "Nome (opcional)", - "pause": "Pausa entre pulsos (ms) (opcional)", - "repeat": "Intervalo para repetir (-1=infinito) (opcional)" + "name": "Nome", + "pause": "Pausa entre pulsos (ms)", + "repeat": "Intervalo para repetir (-1=infinito)" }, "description": "Selecione as op\u00e7\u00f5es para o switch conectado a {zone}: estado {state}", "title": "Configure o interruptor de sa\u00edda" diff --git a/homeassistant/components/konnected/translations/zh-Hant.json b/homeassistant/components/konnected/translations/zh-Hant.json index 9d6d522b7e1..fbce1455270 100644 --- a/homeassistant/components/konnected/translations/zh-Hant.json +++ b/homeassistant/components/konnected/translations/zh-Hant.json @@ -39,7 +39,7 @@ "options_binary": { "data": { "inverse": "\u53cd\u8f49\u958b\u555f/\u95dc\u9589\u72c0\u614b", - "name": "\u540d\u7a31 \uff08\u9078\u9805\uff09", + "name": "\u540d\u7a31", "type": "\u4e8c\u9032\u4f4d\u611f\u61c9\u5668\u985e\u5225" }, "description": "{zone}\u9078\u9805", @@ -47,8 +47,8 @@ }, "options_digital": { "data": { - "name": "\u540d\u7a31 \uff08\u9078\u9805\uff09", - "poll_interval": "\u66f4\u65b0\u9593\u8ddd\uff08\u5206\u9418\uff09\uff08\u9078\u9805\uff09", + "name": "\u540d\u7a31", + "poll_interval": "\u66f4\u65b0\u9593\u8ddd\uff08\u5206\u9418\uff09", "type": "\u611f\u61c9\u5668\u985e\u5225" }, "description": "{zone}\u9078\u9805", @@ -84,7 +84,7 @@ }, "options_misc": { "data": { - "api_host": "\u8986\u5beb API \u4e3b\u6a5f\u7aef URL\uff08\u9078\u9805\uff09", + "api_host": "\u8986\u5beb API \u4e3b\u6a5f\u7aef URL", "blink": "\u7576\u50b3\u9001\u72c0\u614b\u8b8a\u66f4\u6642\u3001\u9583\u720d\u9762\u677f LED", "discovery": "\u56de\u61c9\u7db2\u8def\u4e0a\u7684\u63a2\u7d22\u8acb\u6c42", "override_api_host": "\u8986\u5beb\u9810\u8a2d Home Assistant API \u4e3b\u6a5f\u7aef\u9762\u677f URL" @@ -95,11 +95,11 @@ "options_switch": { "data": { "activation": "\u958b\u555f\u6642\u8f38\u51fa", - "momentary": "\u6301\u7e8c\u6642\u9593\uff08ms\uff09\uff08\u9078\u9805\uff09", + "momentary": "\u6301\u7e8c\u6642\u9593\uff08ms\uff09", "more_states": "\u8a2d\u5b9a\u6b64\u5340\u57df\u7684\u9644\u52a0\u72c0\u614b", - "name": "\u540d\u7a31 \uff08\u9078\u9805\uff09", - "pause": "\u66ab\u505c\u9593\u8ddd\uff08ms\uff09\uff08\u9078\u9805\uff09", - "repeat": "\u91cd\u8907\u6642\u9593\uff08-1=\u7121\u9650\uff09\uff08\u9078\u9805\uff09" + "name": "\u540d\u7a31", + "pause": "\u66ab\u505c\u9593\u8ddd\uff08ms\uff09", + "repeat": "\u91cd\u8907\u6642\u9593\uff08-1=\u7121\u9650\uff09" }, "description": "{zone}\u9078\u9805\uff1a\u72c0\u614b {state}", "title": "\u8a2d\u5b9a Switchable \u8f38\u51fa" diff --git a/homeassistant/components/laundrify/translations/de.json b/homeassistant/components/laundrify/translations/de.json new file mode 100644 index 00000000000..016f345e13d --- /dev/null +++ b/homeassistant/components/laundrify/translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_format": "Ung\u00fcltiges Format. Bitte als xxx-xxx angeben.", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "init": { + "data": { + "code": "Auth-Code (xxx-xxx)" + }, + "description": "Bitte gib deinen pers\u00f6nlichen Auth-Code ein, der in der laundrify-App angezeigt wird." + }, + "reauth_confirm": { + "description": "Die laundrify-Integration muss sich neu authentifizieren.", + "title": "Integration erneut authentifizieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/en.json b/homeassistant/components/laundrify/translations/en.json index 2bfb07d4041..0c8375746ed 100644 --- a/homeassistant/components/laundrify/translations/en.json +++ b/homeassistant/components/laundrify/translations/en.json @@ -1,25 +1,25 @@ { - "config": { - "abort": { - "single_instance_allowed": "Already configured. Only a single configuration possible." - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", - "invalid_format": "Invalid format. Please specify as xxx-xxx.", - "unknown": "Unexpected error" - }, - "step": { - "init": { - "data": { - "code": "Auth Code (xxx-xxx)" + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." }, - "description": "Please enter your personal AuthCode that is shown in the laundrify-App." - }, - "reauth_confirm": { - "description": "The laundrify integration needs to re-authenticate.", - "title": "Reauthenticate Integration" - } + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_format": "Invalid format. Please specify as xxx-xxx.", + "unknown": "Unexpected error" + }, + "step": { + "init": { + "data": { + "code": "Auth Code (xxx-xxx)" + }, + "description": "Please enter your personal Auth Code that is shown in the laundrify-App." + }, + "reauth_confirm": { + "description": "The laundrify integration needs to re-authenticate.", + "title": "Reauthenticate Integration" + } + } } - } } \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/fr.json b/homeassistant/components/laundrify/translations/fr.json new file mode 100644 index 00000000000..91674bdd50c --- /dev/null +++ b/homeassistant/components/laundrify/translations/fr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "invalid_format": "Format non valide. Veuillez sp\u00e9cifier au format xxx-xxx.", + "unknown": "Erreur inattendue" + }, + "step": { + "init": { + "data": { + "code": "Code d'authentification (xxx-xxx)" + }, + "description": "Veuillez saisir votre code d'authentification personnel tel qu'affich\u00e9 dans l'application laundrify." + }, + "reauth_confirm": { + "description": "L'int\u00e9gration laundrify doit \u00eatre r\u00e9-authentifi\u00e9e.", + "title": "R\u00e9-authentifier l'int\u00e9gration" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/pt-BR.json b/homeassistant/components/laundrify/translations/pt-BR.json new file mode 100644 index 00000000000..c053ae34fe0 --- /dev/null +++ b/homeassistant/components/laundrify/translations/pt-BR.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 est\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falhou ao se conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_format": "Formato Inv\u00e1lido. Especifique como xxx-xxx.", + "unknown": "Erro inesperado" + }, + "step": { + "init": { + "data": { + "code": "C\u00f3digo de autentica\u00e7\u00e3o (xxx-xxx)" + }, + "description": "Por favor, insira seu c\u00f3digo de autentica\u00e7\u00e3o pessoal que \u00e9 mostrado no aplicativo laundrify." + }, + "reauth_confirm": { + "description": "A integra\u00e7\u00e3o da laundrify precisa ser autenticada novamente.", + "title": "Reautenticar Integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/translations/nl.json b/homeassistant/components/luftdaten/translations/nl.json index adfdb1101a0..9da8bb945e7 100644 --- a/homeassistant/components/luftdaten/translations/nl.json +++ b/homeassistant/components/luftdaten/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "already_configured": "Service is al geconfigureerd", + "already_configured": "Dienst is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken", "invalid_sensor": "Sensor niet beschikbaar of ongeldig" }, diff --git a/homeassistant/components/met/translations/nl.json b/homeassistant/components/met/translations/nl.json index 7c3d03fdb1f..1d64c174e07 100644 --- a/homeassistant/components/met/translations/nl.json +++ b/homeassistant/components/met/translations/nl.json @@ -4,7 +4,7 @@ "no_home": "Er zijn geen thuisco\u00f6rdinaten ingesteld in de Home Assistant-configuratie" }, "error": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "step": { "user": { diff --git a/homeassistant/components/met_eireann/translations/nl.json b/homeassistant/components/met_eireann/translations/nl.json index b67c167ca8d..13b008b6a45 100644 --- a/homeassistant/components/met_eireann/translations/nl.json +++ b/homeassistant/components/met_eireann/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "step": { "user": { diff --git a/homeassistant/components/metoffice/translations/nl.json b/homeassistant/components/metoffice/translations/nl.json index b6fbbfdbba4..00f9400a78b 100644 --- a/homeassistant/components/metoffice/translations/nl.json +++ b/homeassistant/components/metoffice/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/minecraft_server/translations/nl.json b/homeassistant/components/minecraft_server/translations/nl.json index 0170964d5b0..b6dbe56016e 100644 --- a/homeassistant/components/minecraft_server/translations/nl.json +++ b/homeassistant/components/minecraft_server/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken met de server. Controleer de host en de poort en probeer het opnieuw. Zorg er ook voor dat u minimaal Minecraft versie 1.7 op uw server uitvoert.", diff --git a/homeassistant/components/motion_blinds/translations/hu.json b/homeassistant/components/motion_blinds/translations/hu.json index 8ab58169d05..cef4a0426da 100644 --- a/homeassistant/components/motion_blinds/translations/hu.json +++ b/homeassistant/components/motion_blinds/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van, a csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sai friss\u00edtve vannak", + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "connection_error": "Sikertelen csatlakoz\u00e1s" }, diff --git a/homeassistant/components/motioneye/translations/nl.json b/homeassistant/components/motioneye/translations/nl.json index a2e053e2493..bd62a7037ac 100644 --- a/homeassistant/components/motioneye/translations/nl.json +++ b/homeassistant/components/motioneye/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd", + "already_configured": "Dienst is al geconfigureerd", "reauth_successful": "Herauthenticatie geslaagd" }, "error": { diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index d948cc43df7..155cf0bf07a 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd", + "already_configured": "Dienst is al geconfigureerd", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { diff --git a/homeassistant/components/myq/translations/nl.json b/homeassistant/components/myq/translations/nl.json index 8a3020679e9..ea41554ff75 100644 --- a/homeassistant/components/myq/translations/nl.json +++ b/homeassistant/components/myq/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd", + "already_configured": "Dienst is al geconfigureerd", "reauth_successful": "Herauthenticatie geslaagd" }, "error": { diff --git a/homeassistant/components/nws/translations/nl.json b/homeassistant/components/nws/translations/nl.json index 5332f43f4c7..4aa958f385b 100644 --- a/homeassistant/components/nws/translations/nl.json +++ b/homeassistant/components/nws/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/onewire/translations/hu.json b/homeassistant/components/onewire/translations/hu.json index faae98589e3..94581202b99 100644 --- a/homeassistant/components/onewire/translations/hu.json +++ b/homeassistant/components/onewire/translations/hu.json @@ -12,7 +12,7 @@ "host": "C\u00edm", "port": "Port" }, - "title": "A 1-Wire be\u00e1ll\u00edt\u00e1sa" + "title": "A szerver be\u00e1ll\u00edt\u00e1sa" } } }, diff --git a/homeassistant/components/peco/translations/nl.json b/homeassistant/components/peco/translations/nl.json index 088ae8f0c32..5b8333a9c6d 100644 --- a/homeassistant/components/peco/translations/nl.json +++ b/homeassistant/components/peco/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "step": { "user": { diff --git a/homeassistant/components/pi_hole/translations/nl.json b/homeassistant/components/pi_hole/translations/nl.json index 4c3919506ef..8199969aae8 100644 --- a/homeassistant/components/pi_hole/translations/nl.json +++ b/homeassistant/components/pi_hole/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken" diff --git a/homeassistant/components/plugwise/translations/nl.json b/homeassistant/components/plugwise/translations/nl.json index 4dfd7d3b4e4..8109386013c 100644 --- a/homeassistant/components/plugwise/translations/nl.json +++ b/homeassistant/components/plugwise/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json index 9979909e040..a7d1ac0e743 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "step": { "user": { diff --git a/homeassistant/components/remote/translations/nl.json b/homeassistant/components/remote/translations/nl.json index b512f1b55aa..0c866181814 100644 --- a/homeassistant/components/remote/translations/nl.json +++ b/homeassistant/components/remote/translations/nl.json @@ -6,8 +6,8 @@ "turn_on": "{entity_name} inschakelen" }, "condition_type": { - "is_off": "{entity_name} staat uit", - "is_on": "{entity_name} staat aan" + "is_off": "{entity_name} is uitgeschakeld", + "is_on": "{entity_name} is ingeschakeld" }, "trigger_type": { "changed_states": "{entity_name} in- of uitgeschakeld", diff --git a/homeassistant/components/season/translations/nl.json b/homeassistant/components/season/translations/nl.json index 1da54d9c360..63067c8a814 100644 --- a/homeassistant/components/season/translations/nl.json +++ b/homeassistant/components/season/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "step": { "user": { diff --git a/homeassistant/components/shelly/translations/id.json b/homeassistant/components/shelly/translations/id.json index 9ed75694de0..59af237d5d3 100644 --- a/homeassistant/components/shelly/translations/id.json +++ b/homeassistant/components/shelly/translations/id.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Gagal terhubung", + "firmware_not_fully_provisioned": "Perangkat belum sepenuhnya disediakan. Hubungi dukungan Shelly.", "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, diff --git a/homeassistant/components/shopping_list/translations/nl.json b/homeassistant/components/shopping_list/translations/nl.json index e8de5fbae1d..6eb39857046 100644 --- a/homeassistant/components/shopping_list/translations/nl.json +++ b/homeassistant/components/shopping_list/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "step": { "user": { diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index df71ba7ca8b..12f745b3b69 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -10,7 +10,7 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "progress": { - "email_2fa": "Adja meg a k\u00e9tfaktoros hiteles\u00edt\u00e9si k\u00f3dot amelyet e-mailben kapott." + "email_2fa": "Ellen\u0151rizze az e-mailj\u00e9t a Simplisafe \u00e1ltal k\u00fcld\u00f6tt ellen\u0151rz\u0151 link\u00e9rt." }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/slack/translations/nl.json b/homeassistant/components/slack/translations/nl.json index f531a126e9b..bb04a7dbf18 100644 --- a/homeassistant/components/slack/translations/nl.json +++ b/homeassistant/components/slack/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/sonarr/translations/nl.json b/homeassistant/components/sonarr/translations/nl.json index a5f289a1428..0ef71c521b0 100644 --- a/homeassistant/components/sonarr/translations/nl.json +++ b/homeassistant/components/sonarr/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd", + "already_configured": "Dienst is al geconfigureerd", "reauth_successful": "Herauthenticatie geslaagd", "unknown": "Onverwachte fout" }, diff --git a/homeassistant/components/sql/translations/nl.json b/homeassistant/components/sql/translations/nl.json index e93b63df296..594f589018d 100644 --- a/homeassistant/components/sql/translations/nl.json +++ b/homeassistant/components/sql/translations/nl.json @@ -15,7 +15,7 @@ "name": "Naam", "query": "Selecteer Query", "unit_of_measurement": "Meeteenheid", - "value_template": "Waarde Template" + "value_template": "Waardesjabloon" }, "data_description": { "column": "Kolom voor teruggestuurde query om als status te presenteren", @@ -23,7 +23,7 @@ "name": "Naam die zal worden gebruikt voor Config Entry en ook de Sensor", "query": "Query die moet worden uitgevoerd, moet beginnen met 'SELECT'", "unit_of_measurement": "Meeteenheid (optioneel)", - "value_template": "Waarde Template (optioneel)" + "value_template": "Waardesjabloon (optioneel)" } } } @@ -41,7 +41,7 @@ "name": "Naam", "query": "Selecteer Query", "unit_of_measurement": "Meeteenheid", - "value_template": "Waarde Template" + "value_template": "Waardesjabloon" }, "data_description": { "column": "Kolom voor teruggestuurde query om als status te presenteren", @@ -49,7 +49,7 @@ "name": "Naam die zal worden gebruikt voor Config Entry en ook de Sensor", "query": "Query die moet worden uitgevoerd, moet beginnen met 'SELECT'", "unit_of_measurement": "Meeteenheid (optioneel)", - "value_template": "Waarde Template (optioneel)" + "value_template": "Waardesjabloon (optioneel)" } } } diff --git a/homeassistant/components/steam_online/translations/nl.json b/homeassistant/components/steam_online/translations/nl.json index 9255b0e06ff..f94d083b76a 100644 --- a/homeassistant/components/steam_online/translations/nl.json +++ b/homeassistant/components/steam_online/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd", + "already_configured": "Dienst is al geconfigureerd", "reauth_successful": "Herauthenticatie geslaagd" }, "error": { diff --git a/homeassistant/components/stookalert/translations/nl.json b/homeassistant/components/stookalert/translations/nl.json index b6bd443c31b..fe8f84469a5 100644 --- a/homeassistant/components/stookalert/translations/nl.json +++ b/homeassistant/components/stookalert/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "step": { "user": { diff --git a/homeassistant/components/syncthing/translations/nl.json b/homeassistant/components/syncthing/translations/nl.json index 4f66222ada8..dd941e4bc75 100644 --- a/homeassistant/components/syncthing/translations/nl.json +++ b/homeassistant/components/syncthing/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/tellduslive/translations/nl.json b/homeassistant/components/tellduslive/translations/nl.json index 0f718304059..07ba94774f1 100644 --- a/homeassistant/components/tellduslive/translations/nl.json +++ b/homeassistant/components/tellduslive/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd", + "already_configured": "Dienst is al geconfigureerd", "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", "unknown": "Onverwachte fout", "unknown_authorize_url_generation": "Onbekende fout bij het genereren van een autorisatie-URL." diff --git a/homeassistant/components/tibber/translations/nl.json b/homeassistant/components/tibber/translations/nl.json index 622e35baea4..efa896e08e6 100644 --- a/homeassistant/components/tibber/translations/nl.json +++ b/homeassistant/components/tibber/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "error": { "cannot_connect": "Kan geen verbinding maken", diff --git a/homeassistant/components/ukraine_alarm/translations/hu.json b/homeassistant/components/ukraine_alarm/translations/hu.json index 64273f1348b..6bbbae1eca2 100644 --- a/homeassistant/components/ukraine_alarm/translations/hu.json +++ b/homeassistant/components/ukraine_alarm/translations/hu.json @@ -11,13 +11,13 @@ "step": { "community": { "data": { - "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" + "region": "R\u00e9gi\u00f3" }, "description": "Ha nem csak a megy\u00e9t \u00e9s a ker\u00fcletet szeretn\u00e9 figyelni, v\u00e1lassza ki az adott k\u00f6z\u00f6ss\u00e9get" }, "district": { "data": { - "region": "[%key:component::ukraine_alarm::config::step::state::data::region%]" + "region": "R\u00e9gi\u00f3" }, "description": "Ha nem csak megy\u00e9t szeretne figyelni, v\u00e1lassza ki az adott ker\u00fcletet" }, @@ -25,7 +25,7 @@ "data": { "region": "R\u00e9gi\u00f3" }, - "description": "\u00c1ll\u00edtsa be az Ukraine Alarm integr\u00e1ci\u00f3t. API-kulcs l\u00e9trehoz\u00e1s\u00e1hoz l\u00e1togasson el ide: {api_url}" + "description": "V\u00e1lassza ki a megfigyelni k\u00edv\u00e1nt megy\u00e9t" } } } diff --git a/homeassistant/components/ukraine_alarm/translations/nl.json b/homeassistant/components/ukraine_alarm/translations/nl.json index 97fd17e23e0..d9caa461588 100644 --- a/homeassistant/components/ukraine_alarm/translations/nl.json +++ b/homeassistant/components/ukraine_alarm/translations/nl.json @@ -25,7 +25,7 @@ "data": { "region": "Regio" }, - "description": "Kies staat om te monitoren" + "description": "De status kiezen om te monitoren" } } } diff --git a/homeassistant/components/unifi/translations/hu.json b/homeassistant/components/unifi/translations/hu.json index f06c30d5343..be817737b5a 100644 --- a/homeassistant/components/unifi/translations/hu.json +++ b/homeassistant/components/unifi/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az UniFi Network webhely m\u00e1r konfigur\u00e1lva van", - "configuration_updated": "A konfigur\u00e1ci\u00f3 friss\u00edtve.", + "configuration_updated": "Konfigur\u00e1ci\u00f3 friss\u00edtve", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { diff --git a/homeassistant/components/vallox/translations/nl.json b/homeassistant/components/vallox/translations/nl.json index 9b064500704..867efc3ddc9 100644 --- a/homeassistant/components/vallox/translations/nl.json +++ b/homeassistant/components/vallox/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd", + "already_configured": "Dienst is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken", "invalid_host": "Ongeldige hostnaam of IP-adres", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/vlc_telnet/translations/nl.json b/homeassistant/components/vlc_telnet/translations/nl.json index 771ce98964d..87ddaffab2c 100644 --- a/homeassistant/components/vlc_telnet/translations/nl.json +++ b/homeassistant/components/vlc_telnet/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd", + "already_configured": "Dienst is al geconfigureerd", "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "reauth_successful": "Herauthenticatie geslaagd", diff --git a/homeassistant/components/whois/translations/nl.json b/homeassistant/components/whois/translations/nl.json index a6ad64881ef..714b9c9ddd9 100644 --- a/homeassistant/components/whois/translations/nl.json +++ b/homeassistant/components/whois/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd" }, "error": { "unexpected_response": "Onverwacht antwoord van whois-server", diff --git a/homeassistant/components/wled/translations/select.nl.json b/homeassistant/components/wled/translations/select.nl.json index f2ac9da54cb..e93c165520a 100644 --- a/homeassistant/components/wled/translations/select.nl.json +++ b/homeassistant/components/wled/translations/select.nl.json @@ -3,7 +3,7 @@ "wled__live_override": { "0": "Uit", "1": "Aan", - "2": "Totdat het apparaat opnieuw opstart" + "2": "Totdat het apparaat opnieuw wordt opgestart" } } } \ No newline at end of file From d1afbbfb095a610e095b9f35904a52375a9a7a85 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 22 May 2022 11:27:25 +0200 Subject: [PATCH 0744/3516] Move manual configuration of MQTT vacuum to the integration key (#72281) Add vacuum Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 1 + .../components/mqtt/vacuum/__init__.py | 39 +++++++++++++++++-- .../components/mqtt/vacuum/schema_legacy.py | 17 ++++++-- .../components/mqtt/vacuum/schema_state.py | 14 +++++-- tests/components/mqtt/test_legacy_vacuum.py | 13 +++++++ tests/components/mqtt/test_state_vacuum.py | 13 +++++++ 6 files changed, 86 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index dfef057a7f9..f2d826412ce 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -197,6 +197,7 @@ PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( vol.Optional(Platform.FAN.value): cv.ensure_list, vol.Optional(Platform.LIGHT.value): cv.ensure_list, vol.Optional(Platform.LOCK.value): cv.ensure_list, + vol.Optional(Platform.VACUUM.value): cv.ensure_list, } ) diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 4898a7b3351..34205ab7780 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -1,6 +1,7 @@ """Support for MQTT vacuums.""" from __future__ import annotations +import asyncio import functools import voluptuous as vol @@ -11,16 +12,22 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from ..mixins import async_setup_entry_helper, async_setup_platform_helper +from ..mixins import ( + async_get_platform_config_from_yaml, + async_setup_entry_helper, + async_setup_platform_helper, +) from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE from .schema_legacy import ( DISCOVERY_SCHEMA_LEGACY, PLATFORM_SCHEMA_LEGACY, + PLATFORM_SCHEMA_LEGACY_MODERN, async_setup_entity_legacy, ) from .schema_state import ( DISCOVERY_SCHEMA_STATE, PLATFORM_SCHEMA_STATE, + PLATFORM_SCHEMA_STATE_MODERN, async_setup_entity_state, ) @@ -31,20 +38,35 @@ def validate_mqtt_vacuum_discovery(value): return schemas[value[CONF_SCHEMA]](value) +# Configuring MQTT Vacuums under the vacuum platform key is deprecated in HA Core 2022.6 def validate_mqtt_vacuum(value): - """Validate MQTT vacuum schema.""" + """Validate MQTT vacuum schema (deprecated).""" schemas = {LEGACY: PLATFORM_SCHEMA_LEGACY, STATE: PLATFORM_SCHEMA_STATE} return schemas[value[CONF_SCHEMA]](value) +def validate_mqtt_vacuum_modern(value): + """Validate MQTT vacuum modern schema.""" + schemas = { + LEGACY: PLATFORM_SCHEMA_LEGACY_MODERN, + STATE: PLATFORM_SCHEMA_STATE_MODERN, + } + return schemas[value[CONF_SCHEMA]](value) + + DISCOVERY_SCHEMA = vol.All( MQTT_VACUUM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_vacuum_discovery ) +# Configuring MQTT Vacuums under the vacuum platform key is deprecated in HA Core 2022.6 PLATFORM_SCHEMA = vol.All( MQTT_VACUUM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_vacuum ) +PLATFORM_SCHEMA_MODERN = vol.All( + MQTT_VACUUM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_vacuum_modern +) + async def async_setup_platform( hass: HomeAssistant, @@ -53,6 +75,7 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up MQTT vacuum through configuration.yaml.""" + # Deprecated in HA Core 2022.6 await async_setup_platform_helper( hass, vacuum.DOMAIN, config, async_add_entities, _async_setup_entity ) @@ -63,7 +86,17 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT vacuum dynamically through MQTT discovery.""" + """Set up MQTT vacuum through configuration.yaml and dynamically through MQTT discovery.""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, vacuum.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 41d0c9faf17..eb5e01b6251 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant.components.vacuum import ( ATTR_STATUS, + DOMAIN as VACUUM_DOMAIN, ENTITY_ID_FORMAT, VacuumEntity, VacuumEntityFeature, @@ -18,7 +19,7 @@ from .. import MqttValueTemplate, subscription from ... import mqtt from ..const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN from ..debug_info import log_messages -from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity +from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -94,8 +95,8 @@ MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED = MQTT_VACUUM_ATTRIBUTES_BLOCKED | frozens {ATTR_STATUS} ) -PLATFORM_SCHEMA_LEGACY = ( - mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA_LEGACY_MODERN = ( + mqtt.MQTT_BASE_SCHEMA.extend( { vol.Inclusive(CONF_BATTERY_LEVEL_TEMPLATE, "battery"): cv.template, vol.Inclusive( @@ -147,7 +148,15 @@ PLATFORM_SCHEMA_LEGACY = ( .extend(MQTT_VACUUM_SCHEMA.schema) ) -DISCOVERY_SCHEMA_LEGACY = PLATFORM_SCHEMA_LEGACY.extend({}, extra=vol.REMOVE_EXTRA) +# Configuring MQTT Vacuums under the vacuum platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA_LEGACY = vol.All( + cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_LEGACY_MODERN.schema), + warn_for_legacy_schema(VACUUM_DOMAIN), +) + +DISCOVERY_SCHEMA_LEGACY = PLATFORM_SCHEMA_LEGACY_MODERN.extend( + {}, extra=vol.REMOVE_EXTRA +) async def async_setup_entity_legacy( diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 7a5424d7cbf..7aa7be07797 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -4,6 +4,7 @@ import json import voluptuous as vol from homeassistant.components.vacuum import ( + DOMAIN as VACUUM_DOMAIN, ENTITY_ID_FORMAT, STATE_CLEANING, STATE_DOCKED, @@ -31,7 +32,7 @@ from ..const import ( CONF_STATE_TOPIC, ) from ..debug_info import log_messages -from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity +from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -103,8 +104,8 @@ DEFAULT_PAYLOAD_LOCATE = "locate" DEFAULT_PAYLOAD_START = "start" DEFAULT_PAYLOAD_PAUSE = "pause" -PLATFORM_SCHEMA_STATE = ( - mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA_STATE_MODERN = ( + mqtt.MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All( cv.ensure_list, [cv.string] @@ -136,8 +137,13 @@ PLATFORM_SCHEMA_STATE = ( .extend(MQTT_VACUUM_SCHEMA.schema) ) +# Configuring MQTT Vacuums under the vacuum platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA_STATE = vol.All( + cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_STATE_MODERN.schema), + warn_for_legacy_schema(VACUUM_DOMAIN), +) -DISCOVERY_SCHEMA_STATE = PLATFORM_SCHEMA_STATE.extend({}, extra=vol.REMOVE_EXTRA) +DISCOVERY_SCHEMA_STATE = PLATFORM_SCHEMA_STATE_MODERN.extend({}, extra=vol.REMOVE_EXTRA) async def async_setup_entity_state( diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 992275105c5..f451079e0f0 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -56,6 +56,7 @@ from .test_common import ( help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -901,3 +902,15 @@ async def test_encoding_subscribable_topics( attribute_value, skip_raw_test=True, ) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = vacuum.DOMAIN + config = deepcopy(DEFAULT_CONFIG) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 8691aa73323..3f752f1b528 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -58,6 +58,7 @@ from .test_common import ( help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -648,3 +649,15 @@ async def test_encoding_subscribable_topics( attribute_value, skip_raw_test=True, ) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = vacuum.DOMAIN + config = deepcopy(DEFAULT_CONFIG) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None From e8664ce1aee95bb71f941be9de83f8f2bb6a74de Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 22 May 2022 11:28:10 +0200 Subject: [PATCH 0745/3516] Move manual configuration of MQTT climate to the integration key (#72251) Add climate Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 1 + homeassistant/components/mqtt/climate.py | 42 +++++++++++++++-------- tests/components/mqtt/test_climate.py | 13 +++++++ 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index f2d826412ce..8a27ff7d759 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -194,6 +194,7 @@ PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( vol.Optional(Platform.BINARY_SENSOR.value): cv.ensure_list, vol.Optional(Platform.BUTTON.value): cv.ensure_list, vol.Optional(Platform.CAMERA.value): cv.ensure_list, + vol.Optional(Platform.CLIMATE.value): cv.ensure_list, vol.Optional(Platform.FAN.value): cv.ensure_list, vol.Optional(Platform.LIGHT.value): cv.ensure_list, vol.Optional(Platform.LOCK.value): cv.ensure_list, diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index c095054ae9b..52465bbba24 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -1,16 +1,14 @@ """Support for MQTT climate devices.""" from __future__ import annotations +import asyncio import functools import logging import voluptuous as vol from homeassistant.components import climate -from homeassistant.components.climate import ( - PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, - ClimateEntity, -) +from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, @@ -46,20 +44,17 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import ( - MQTT_BASE_PLATFORM_SCHEMA, - MqttCommandTemplate, - MqttValueTemplate, - subscription, -) +from . import MqttCommandTemplate, MqttValueTemplate, subscription from .. import mqtt from .const import CONF_ENCODING, CONF_QOS, CONF_RETAIN, PAYLOAD_NONE from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_get_platform_config_from_yaml, async_setup_entry_helper, async_setup_platform_helper, + warn_for_legacy_schema, ) _LOGGER = logging.getLogger(__name__) @@ -237,8 +232,7 @@ def valid_preset_mode_configuration(config): return config -SCHEMA_BASE = CLIMATE_PLATFORM_SCHEMA.extend(MQTT_BASE_PLATFORM_SCHEMA.schema) -_PLATFORM_SCHEMA_BASE = SCHEMA_BASE.extend( +_PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_AUX_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template, @@ -330,8 +324,14 @@ _PLATFORM_SCHEMA_BASE = SCHEMA_BASE.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -PLATFORM_SCHEMA = vol.All( +PLATFORM_SCHEMA_MODERN = vol.All( _PLATFORM_SCHEMA_BASE, + valid_preset_mode_configuration, +) + +# Configuring MQTT Climate under the climate platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA = vol.All( + cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 cv.deprecated(CONF_SEND_IF_OFF), # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 @@ -344,6 +344,7 @@ PLATFORM_SCHEMA = vol.All( cv.deprecated(CONF_HOLD_STATE_TOPIC), cv.deprecated(CONF_HOLD_LIST), valid_preset_mode_configuration, + warn_for_legacy_schema(climate.DOMAIN), ) _DISCOVERY_SCHEMA_BASE = _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA) @@ -371,7 +372,8 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up MQTT climate device through configuration.yaml.""" + """Set up MQTT climate configured under the fan platform key (deprecated).""" + # The use of PLATFORM_SCHEMA is deprecated in HA Core 2022.6 await async_setup_platform_helper( hass, climate.DOMAIN, config, async_add_entities, _async_setup_entity ) @@ -382,7 +384,17 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT climate device dynamically through MQTT discovery.""" + """Set up MQTT climate device through configuration.yaml and dynamically through MQTT discovery.""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, climate.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index af6d65ac490..98af86248e4 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -53,6 +53,7 @@ from .test_common import ( help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -1761,3 +1762,15 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): domain = CLIMATE_DOMAIN config = DEFAULT_CONFIG[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = CLIMATE_DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None From 87d895929f4b586a6648ce86bd0a76b4ca51efd1 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 22 May 2022 13:06:49 +0200 Subject: [PATCH 0746/3516] Move manual configuration of MQTT switch to the integration key (#72279) Add switch Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 1 + homeassistant/components/mqtt/switch.py | 28 +++++++++++++++++++---- tests/components/mqtt/test_switch.py | 13 +++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 8a27ff7d759..54d7dc8feb9 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -198,6 +198,7 @@ PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( vol.Optional(Platform.FAN.value): cv.ensure_list, vol.Optional(Platform.LIGHT.value): cv.ensure_list, vol.Optional(Platform.LOCK.value): cv.ensure_list, + vol.Optional(Platform.SWITCH.value): cv.ensure_list, vol.Optional(Platform.VACUUM.value): cv.ensure_list, } ) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index db54ae08953..f5f8363eb33 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -1,6 +1,7 @@ """Support for MQTT switches.""" from __future__ import annotations +import asyncio import functools import voluptuous as vol @@ -37,8 +38,10 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_get_platform_config_from_yaml, async_setup_entry_helper, async_setup_platform_helper, + warn_for_legacy_schema, ) DEFAULT_NAME = "MQTT Switch" @@ -48,7 +51,7 @@ DEFAULT_OPTIMISTIC = False CONF_STATE_ON = "state_on" CONF_STATE_OFF = "state_off" -PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, @@ -61,7 +64,13 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -DISCOVERY_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA) +# Configuring MQTT Switches under the switch platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA = vol.All( + cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_MODERN.schema), + warn_for_legacy_schema(switch.DOMAIN), +) + +DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) async def async_setup_platform( @@ -70,7 +79,8 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up MQTT switch through configuration.yaml.""" + """Set up MQTT switch configured under the fan platform key (deprecated).""" + # Deprecated in HA Core 2022.6 await async_setup_platform_helper( hass, switch.DOMAIN, config, async_add_entities, _async_setup_entity ) @@ -81,7 +91,17 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT switch dynamically through MQTT discovery.""" + """Set up MQTT switch through configuration.yaml and dynamically through MQTT discovery.""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, switch.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index a7dd0d8c31e..699f0de87f0 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -39,6 +39,7 @@ from .test_common import ( help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -585,3 +586,15 @@ async def test_encoding_subscribable_topics( attribute, attribute_value, ) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = switch.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None From 3c3e394972af7dc5080ba439dee4d93865fa15d6 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 22 May 2022 13:07:14 +0200 Subject: [PATCH 0747/3516] Move manual configuration of MQTT cover to the integration key (#72268) Add cover Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 1 + homeassistant/components/mqtt/cover.py | 29 +++++++++++++++++++---- tests/components/mqtt/test_cover.py | 15 ++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 54d7dc8feb9..857580a1f88 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -195,6 +195,7 @@ PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( vol.Optional(Platform.BUTTON.value): cv.ensure_list, vol.Optional(Platform.CAMERA.value): cv.ensure_list, vol.Optional(Platform.CLIMATE.value): cv.ensure_list, + vol.Optional(Platform.COVER.value): cv.ensure_list, vol.Optional(Platform.FAN.value): cv.ensure_list, vol.Optional(Platform.LIGHT.value): cv.ensure_list, vol.Optional(Platform.LOCK.value): cv.ensure_list, diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 76f984f1bc9..8e36329946a 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -1,6 +1,7 @@ """Support for MQTT cover devices.""" from __future__ import annotations +import asyncio import functools from json import JSONDecodeError, loads as json_loads import logging @@ -45,8 +46,10 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_get_platform_config_from_yaml, async_setup_entry_helper, async_setup_platform_helper, + warn_for_legacy_schema, ) _LOGGER = logging.getLogger(__name__) @@ -149,7 +152,7 @@ def validate_options(value): return value -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, @@ -194,11 +197,18 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -PLATFORM_SCHEMA = vol.All( +PLATFORM_SCHEMA_MODERN = vol.All( _PLATFORM_SCHEMA_BASE, validate_options, ) +# Configuring MQTT Covers under the cover platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA = vol.All( + cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), + validate_options, + warn_for_legacy_schema(cover.DOMAIN), +) + DISCOVERY_SCHEMA = vol.All( cv.removed("tilt_invert_state"), _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), @@ -212,7 +222,8 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up MQTT cover through configuration.yaml.""" + """Set up MQTT covers configured under the fan platform key (deprecated).""" + # Deprecated in HA Core 2022.6 await async_setup_platform_helper( hass, cover.DOMAIN, config, async_add_entities, _async_setup_entity ) @@ -223,7 +234,17 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT cover dynamically through MQTT discovery.""" + """Set up MQTT cover through configuration.yaml and dynamically through MQTT discovery.""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, cover.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index aad6fa5d9ca..285af765ab4 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -1,4 +1,6 @@ """The tests for the MQTT cover platform.""" + +import copy from unittest.mock import patch import pytest @@ -68,6 +70,7 @@ from .test_common import ( help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -3206,3 +3209,15 @@ async def test_encoding_subscribable_topics( attribute_value, skip_raw_test=True, ) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = cover.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None From 4153edbcdd9660120e148f49f7b1be6bf1fac3e1 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 22 May 2022 13:07:49 +0200 Subject: [PATCH 0748/3516] Move manual configuration of MQTT humidifier to the integration key (#72270) Add humidifier --- homeassistant/components/mqtt/__init__.py | 1 + homeassistant/components/mqtt/humidifier.py | 27 ++++++++++++++++++--- tests/components/mqtt/test_humidifier.py | 13 ++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 857580a1f88..426fad45db1 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -197,6 +197,7 @@ PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( vol.Optional(Platform.CLIMATE.value): cv.ensure_list, vol.Optional(Platform.COVER.value): cv.ensure_list, vol.Optional(Platform.FAN.value): cv.ensure_list, + vol.Optional(Platform.HUMIDIFIER.value): cv.ensure_list, vol.Optional(Platform.LIGHT.value): cv.ensure_list, vol.Optional(Platform.LOCK.value): cv.ensure_list, vol.Optional(Platform.SWITCH.value): cv.ensure_list, diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index f5a8b372532..f6d4aa01dab 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -1,6 +1,7 @@ """Support for MQTT humidifiers.""" from __future__ import annotations +import asyncio import functools import logging @@ -45,8 +46,10 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_get_platform_config_from_yaml, async_setup_entry_helper, async_setup_platform_helper, + warn_for_legacy_schema, ) CONF_AVAILABLE_MODES_LIST = "modes" @@ -100,7 +103,7 @@ def valid_humidity_range_configuration(config): return config -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( { # CONF_AVAIALABLE_MODES_LIST and CONF_MODE_COMMAND_TOPIC must be used together vol.Inclusive( @@ -140,7 +143,15 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) +# Configuring MQTT Humidifiers under the humidifier platform key is deprecated in HA Core 2022.6 PLATFORM_SCHEMA = vol.All( + cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), + valid_humidity_range_configuration, + valid_mode_configuration, + warn_for_legacy_schema(humidifier.DOMAIN), +) + +PLATFORM_SCHEMA_MODERN = vol.All( _PLATFORM_SCHEMA_BASE, valid_humidity_range_configuration, valid_mode_configuration, @@ -159,7 +170,8 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up MQTT humidifier through configuration.yaml.""" + """Set up MQTT humidifier configured under the fan platform key (deprecated).""" + # Deprecated in HA Core 2022.6 await async_setup_platform_helper( hass, humidifier.DOMAIN, config, async_add_entities, _async_setup_entity ) @@ -170,7 +182,16 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT humidifier dynamically through MQTT discovery.""" + """Set up MQTT humidifier through configuration.yaml and dynamically through MQTT discovery.""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, humidifier.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 4aa5ff2350b..bc00d3afffb 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -56,6 +56,7 @@ from .test_common import ( help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -1182,3 +1183,15 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): domain = humidifier.DOMAIN config = DEFAULT_CONFIG[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = humidifier.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None From d9732ec78702fc0e5786b9cc7bb2512f58660aa5 Mon Sep 17 00:00:00 2001 From: Matrix Date: Sun, 22 May 2022 19:20:29 +0800 Subject: [PATCH 0749/3516] Add yolink outlet (#72247) * Add yolink outlet * add .coveragerc * buf fix * suggest fix * suggest fix --- .coveragerc | 1 + homeassistant/components/yolink/__init__.py | 2 +- homeassistant/components/yolink/const.py | 1 + homeassistant/components/yolink/switch.py | 114 ++++++++++++++++++++ 4 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/yolink/switch.py diff --git a/.coveragerc b/.coveragerc index 6cc9ffda714..2d530ab8f45 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1477,6 +1477,7 @@ omit = homeassistant/components/yolink/coordinator.py homeassistant/components/yolink/entity.py homeassistant/components/yolink/sensor.py + homeassistant/components/yolink/switch.py homeassistant/components/youless/__init__.py homeassistant/components/youless/const.py homeassistant/components/youless/sensor.py diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 8a8afb2f1b3..089f0cc79b9 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -21,7 +21,7 @@ SCAN_INTERVAL = timedelta(minutes=5) _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index 303fd684c02..b94727cb948 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -17,3 +17,4 @@ ATTR_DEVICE_DOOR_SENSOR = "DoorSensor" ATTR_DEVICE_TH_SENSOR = "THSensor" ATTR_DEVICE_MOTION_SENSOR = "MotionSensor" ATTR_DEVICE_LEAK_SENSOR = "LeakSensor" +ATTR_DEVICE_OUTLET = "Outlet" diff --git a/homeassistant/components/yolink/switch.py b/homeassistant/components/yolink/switch.py new file mode 100644 index 00000000000..b3756efb74c --- /dev/null +++ b/homeassistant/components/yolink/switch.py @@ -0,0 +1,114 @@ +"""YoLink Switch.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from yolink.device import YoLinkDevice +from yolink.exception import YoLinkAuthFailError, YoLinkClientError + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ATTR_COORDINATOR, ATTR_DEVICE_OUTLET, DOMAIN +from .coordinator import YoLinkCoordinator +from .entity import YoLinkEntity + + +@dataclass +class YoLinkSwitchEntityDescription(SwitchEntityDescription): + """YoLink SwitchEntityDescription.""" + + exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True + value: Callable[[str], bool | None] = lambda _: None + + +DEVICE_TYPES: tuple[YoLinkSwitchEntityDescription, ...] = ( + YoLinkSwitchEntityDescription( + key="state", + device_class=SwitchDeviceClass.OUTLET, + name="State", + value=lambda value: value == "open", + exists_fn=lambda device: device.device_type in [ATTR_DEVICE_OUTLET], + ), +) + +DEVICE_TYPE = [ATTR_DEVICE_OUTLET] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up YoLink Sensor from a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATOR] + devices = [ + device for device in coordinator.yl_devices if device.device_type in DEVICE_TYPE + ] + entities = [] + for device in devices: + for description in DEVICE_TYPES: + if description.exists_fn(device): + entities.append( + YoLinkSwitchEntity(config_entry, coordinator, description, device) + ) + async_add_entities(entities) + + +class YoLinkSwitchEntity(YoLinkEntity, SwitchEntity): + """YoLink Switch Entity.""" + + entity_description: YoLinkSwitchEntityDescription + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: YoLinkCoordinator, + description: YoLinkSwitchEntityDescription, + device: YoLinkDevice, + ) -> None: + """Init YoLink Outlet.""" + super().__init__(coordinator, device) + self.config_entry = config_entry + self.entity_description = description + self._attr_unique_id = f"{device.device_id} {self.entity_description.key}" + self._attr_name = f"{device.device_name} ({self.entity_description.name})" + + @callback + def update_entity_state(self, state: dict) -> None: + """Update HA Entity State.""" + self._attr_is_on = self.entity_description.value( + state[self.entity_description.key] + ) + self.async_write_ha_state() + + async def call_state_change(self, state: str) -> None: + """Call setState api to change outlet state.""" + try: + # call_device_http_api will check result, fail by raise YoLinkClientError + await self.device.call_device_http_api("setState", {"state": state}) + except YoLinkAuthFailError as yl_auth_err: + self.config_entry.async_start_reauth(self.hass) + raise HomeAssistantError(yl_auth_err) from yl_auth_err + except YoLinkClientError as yl_client_err: + self.coordinator.last_update_success = False + raise HomeAssistantError(yl_client_err) from yl_client_err + self._attr_is_on = self.entity_description.value(state) + self.async_write_ha_state() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.call_state_change("open") + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self.call_state_change("close") From 16088915ebe35e5d287955a20a2fee6db8c0904d Mon Sep 17 00:00:00 2001 From: Matrix Date: Mon, 23 May 2022 00:57:09 +0800 Subject: [PATCH 0750/3516] Fix yolink binary sensor (#72304) * Fix yolink binary sensor * suggest fix --- homeassistant/components/yolink/binary_sensor.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yolink/binary_sensor.py b/homeassistant/components/yolink/binary_sensor.py index 494a7ff10eb..42899e08a2c 100644 --- a/homeassistant/components/yolink/binary_sensor.py +++ b/homeassistant/components/yolink/binary_sensor.py @@ -31,7 +31,7 @@ class YoLinkBinarySensorEntityDescription(BinarySensorEntityDescription): """YoLink BinarySensorEntityDescription.""" exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True - key: str = "state" + state_key: str = "state" value: Callable[[str], bool | None] = lambda _: None @@ -43,6 +43,7 @@ SENSOR_DEVICE_TYPE = [ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( YoLinkBinarySensorEntityDescription( + key="door_state", icon="mdi:door", device_class=BinarySensorDeviceClass.DOOR, name="State", @@ -50,12 +51,14 @@ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( exists_fn=lambda device: device.device_type in [ATTR_DEVICE_DOOR_SENSOR], ), YoLinkBinarySensorEntityDescription( + key="motion_state", device_class=BinarySensorDeviceClass.MOTION, name="Motion", value=lambda value: value == "alert", exists_fn=lambda device: device.device_type in [ATTR_DEVICE_MOTION_SENSOR], ), YoLinkBinarySensorEntityDescription( + key="leak_state", name="Leak", icon="mdi:water", device_class=BinarySensorDeviceClass.MOISTURE, @@ -108,6 +111,6 @@ class YoLinkBinarySensorEntity(YoLinkEntity, BinarySensorEntity): def update_entity_state(self, state: dict) -> None: """Update HA Entity State.""" self._attr_is_on = self.entity_description.value( - state[self.entity_description.key] + state[self.entity_description.state_key] ) self.async_write_ha_state() From ead6e7da1f0855963f2b3b4fa6739ea1cbd39d16 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sun, 22 May 2022 18:59:44 +0200 Subject: [PATCH 0751/3516] Purge entity and device registries when importing lcn from configuration.yaml (#54266) * Identify LCN orphans in entity registry and device registry and remove them * Fix typing issues * Revert "Fix typing issues" This reverts commit eccd067b3b5f23135e6c8a79d25f7f2cbc2d0ae9. * Fix removal of devices which do not belong to given config_entry * Use helper for getting entities for config_entry * Rename variable --- homeassistant/components/lcn/config_flow.py | 17 ++--- homeassistant/components/lcn/helpers.py | 71 ++++++++++++++++++++- tests/components/lcn/test_config_flow.py | 4 ++ 3 files changed, 81 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/lcn/config_flow.py b/homeassistant/components/lcn/config_flow.py index 924ff5b278c..a8c5a311971 100644 --- a/homeassistant/components/lcn/config_flow.py +++ b/homeassistant/components/lcn/config_flow.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Any import pypck @@ -16,15 +15,16 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.typing import ConfigType from .const import CONF_DIM_MODE, CONF_SK_NUM_TRIES, DOMAIN +from .helpers import purge_device_registry, purge_entity_registry _LOGGER = logging.getLogger(__name__) def get_config_entry( - hass: HomeAssistant, data: dict[str, Any] + hass: HomeAssistant, data: ConfigType ) -> config_entries.ConfigEntry | None: """Check config entries for already configured entries based on the ip address/port.""" return next( @@ -38,7 +38,7 @@ def get_config_entry( ) -async def validate_connection(host_name: str, data: dict[str, Any]) -> dict[str, Any]: +async def validate_connection(host_name: str, data: ConfigType) -> ConfigType: """Validate if a connection to LCN can be established.""" host = data[CONF_IP_ADDRESS] port = data[CONF_PORT] @@ -70,7 +70,7 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_import(self, data: dict[str, Any]) -> FlowResult: + async def async_step_import(self, data: ConfigType) -> FlowResult: """Import existing configuration from LCN.""" host_name = data[CONF_HOST] # validate the imported connection parameters @@ -93,13 +93,10 @@ class LcnFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # check if we already have a host with the same address configured if entry := get_config_entry(self.hass, data): entry.source = config_entries.SOURCE_IMPORT - # Cleanup entity and device registry, if we imported from configuration.yaml to # remove orphans when entities were removed from configuration - entity_registry = er.async_get(self.hass) - entity_registry.async_clear_config_entry(entry.entry_id) - device_registry = dr.async_get(self.hass) - device_registry.async_clear_config_entry(entry.entry_id) + purge_entity_registry(self.hass, entry.entry_id, data) + purge_device_registry(self.hass, entry.entry_id, data) self.hass.config_entries.async_update_entry(entry, data=data) return self.async_abort(reason="existing_configuration_updated") diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 0c8edd9b852..e9fb2683f4f 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -30,7 +30,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.typing import ConfigType from .const import ( @@ -247,6 +247,75 @@ def import_lcn_config(lcn_config: ConfigType) -> list[ConfigType]: return list(data.values()) +def purge_entity_registry( + hass: HomeAssistant, entry_id: str, imported_entry_data: ConfigType +) -> None: + """Remove orphans from entity registry which are not in entry data.""" + entity_registry = er.async_get(hass) + + # Find all entities that are referenced in the config entry. + references_config_entry = { + entity_entry.entity_id + for entity_entry in er.async_entries_for_config_entry(entity_registry, entry_id) + } + + # Find all entities that are referenced by the entry_data. + references_entry_data = set() + for entity_data in imported_entry_data[CONF_ENTITIES]: + entity_unique_id = generate_unique_id( + entry_id, entity_data[CONF_ADDRESS], entity_data[CONF_RESOURCE] + ) + entity_id = entity_registry.async_get_entity_id( + entity_data[CONF_DOMAIN], DOMAIN, entity_unique_id + ) + if entity_id is not None: + references_entry_data.add(entity_id) + + orphaned_ids = references_config_entry - references_entry_data + for orphaned_id in orphaned_ids: + entity_registry.async_remove(orphaned_id) + + +def purge_device_registry( + hass: HomeAssistant, entry_id: str, imported_entry_data: ConfigType +) -> None: + """Remove orphans from device registry which are not in entry data.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + # Find all devices that are referenced in the entity registry. + references_entities = { + entry.device_id for entry in entity_registry.entities.values() + } + + # Find device that references the host. + references_host = set() + host_device = device_registry.async_get_device({(DOMAIN, entry_id)}) + if host_device is not None: + references_host.add(host_device.id) + + # Find all devices that are referenced by the entry_data. + references_entry_data = set() + for device_data in imported_entry_data[CONF_DEVICES]: + device_unique_id = generate_unique_id(entry_id, device_data[CONF_ADDRESS]) + device = device_registry.async_get_device({(DOMAIN, device_unique_id)}) + if device is not None: + references_entry_data.add(device.id) + + orphaned_ids = ( + { + entry.id + for entry in dr.async_entries_for_config_entry(device_registry, entry_id) + } + - references_entities + - references_host + - references_entry_data + ) + + for device_id in orphaned_ids: + device_registry.async_remove_device(device_id) + + def register_lcn_host_device(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Register LCN host for given config_entry in device registry.""" device_registry = dr.async_get(hass) diff --git a/tests/components/lcn/test_config_flow.py b/tests/components/lcn/test_config_flow.py index 49351b023b6..36b5d23739a 100644 --- a/tests/components/lcn/test_config_flow.py +++ b/tests/components/lcn/test_config_flow.py @@ -7,6 +7,8 @@ import pytest 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_DEVICES, + CONF_ENTITIES, CONF_HOST, CONF_IP_ADDRESS, CONF_PASSWORD, @@ -24,6 +26,8 @@ IMPORT_DATA = { CONF_PASSWORD: "lcn", CONF_SK_NUM_TRIES: 0, CONF_DIM_MODE: "STEPS200", + CONF_DEVICES: [], + CONF_ENTITIES: [], } From 3390d62b3dffb13fc729016bb470cc6cac6a5831 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 22 May 2022 20:53:00 +0200 Subject: [PATCH 0752/3516] Revert "Adjust device_automation type hints in deconz" (#72323) Revert "Adjust device_automation type hints in deconz (#72194)" This reverts commit a57697d6e98a5a267f8eb60cb333b6a8dcc360c3. --- homeassistant/components/deconz/device_trigger.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 2ab1b2e6544..43ed3b2cdc4 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -1,16 +1,16 @@ """Provides device automations for deconz events.""" + from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, ) -from homeassistant.components.device_automation import ( - DEVICE_TRIGGER_BASE_SCHEMA, - GetAutomationsResult, -) +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -642,8 +642,9 @@ def _get_deconz_event_from_device( async def async_validate_trigger_config( - hass: HomeAssistant, config: ConfigType -) -> ConfigType: + hass: HomeAssistant, + config: dict[str, Any], +) -> vol.Schema: """Validate config.""" config = TRIGGER_SCHEMA(config) @@ -702,7 +703,7 @@ async def async_attach_trigger( async def async_get_triggers( hass: HomeAssistant, device_id: str, -) -> GetAutomationsResult: +) -> list[dict[str, str]]: """List device triggers. Make sure device is a supported remote model. From 9c3f9491651f409e8b4d0d645115b55b14f06165 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 May 2022 14:57:54 -0500 Subject: [PATCH 0753/3516] Add live streaming logbook websocket endpoint (#72258) Co-authored-by: Paulus Schoutsen --- homeassistant/components/logbook/__init__.py | 732 +---------- homeassistant/components/logbook/const.py | 39 + homeassistant/components/logbook/helpers.py | 193 +++ homeassistant/components/logbook/models.py | 113 ++ homeassistant/components/logbook/processor.py | 488 +++++++ homeassistant/components/logbook/rest_api.py | 120 ++ .../components/logbook/websocket_api.py | 319 +++++ homeassistant/components/recorder/core.py | 7 + homeassistant/components/recorder/tasks.py | 14 + .../components/websocket_api/messages.py | 2 +- homeassistant/core.py | 5 +- tests/components/logbook/common.py | 21 +- tests/components/logbook/test_init.py | 22 +- .../components/logbook/test_websocket_api.py | 1166 +++++++++++++++++ tests/components/recorder/common.py | 31 +- tests/components/recorder/test_init.py | 23 + tests/test_core.py | 30 + 17 files changed, 2592 insertions(+), 733 deletions(-) create mode 100644 homeassistant/components/logbook/const.py create mode 100644 homeassistant/components/logbook/helpers.py create mode 100644 homeassistant/components/logbook/models.py create mode 100644 homeassistant/components/logbook/processor.py create mode 100644 homeassistant/components/logbook/rest_api.py create mode 100644 homeassistant/components/logbook/websocket_api.py create mode 100644 tests/components/logbook/test_websocket_api.py diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 635868310f6..d9893781748 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -1,63 +1,25 @@ """Event parser and human readable log generator.""" from __future__ import annotations -from collections.abc import Callable, Generator -from contextlib import suppress -from datetime import datetime as dt, timedelta -from http import HTTPStatus -import json -import logging -import re -from typing import Any, cast +from collections.abc import Callable +from typing import Any -from aiohttp import web -from sqlalchemy.engine.row import Row -from sqlalchemy.orm.query import Query import voluptuous as vol -from homeassistant.components import frontend, websocket_api -from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED -from homeassistant.components.http import HomeAssistantView -from homeassistant.components.recorder import get_instance +from homeassistant.components import frontend from homeassistant.components.recorder.filters import ( - Filters, sqlalchemy_filter_from_include_exclude_conf, ) -from homeassistant.components.recorder.models import ( - process_datetime_to_timestamp, - process_timestamp_to_utc_isoformat, -) -from homeassistant.components.recorder.util import session_scope -from homeassistant.components.script import EVENT_SCRIPT_STARTED -from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN -from homeassistant.components.websocket_api import messages -from homeassistant.components.websocket_api.const import JSON_DUMP from homeassistant.const import ( ATTR_DOMAIN, ATTR_ENTITY_ID, - ATTR_FRIENDLY_NAME, ATTR_NAME, - ATTR_SERVICE, - EVENT_CALL_SERVICE, EVENT_LOGBOOK_ENTRY, ) -from homeassistant.core import ( - Context, - Event, - HomeAssistant, - ServiceCall, - callback, - split_entity_id, -) -from homeassistant.exceptions import InvalidEntityFormatError -from homeassistant.helpers import ( - config_validation as cv, - device_registry as dr, - entity_registry as er, -) +from homeassistant.core import Context, Event, HomeAssistant, ServiceCall, callback +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entityfilter import ( INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, - EntityFilter, convert_include_exclude_filter, ) from homeassistant.helpers.integration_platform import ( @@ -65,47 +27,25 @@ from homeassistant.helpers.integration_platform import ( ) from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -import homeassistant.util.dt as dt_util -from .queries import statement_for_request -from .queries.common import PSUEDO_EVENT_STATE_CHANGED - -_LOGGER = logging.getLogger(__name__) - -ENTITY_ID_JSON_EXTRACT = re.compile('"entity_id": ?"([^"]+)"') -DOMAIN_JSON_EXTRACT = re.compile('"domain": ?"([^"]+)"') -ATTR_MESSAGE = "message" - -DOMAIN = "logbook" +from . import rest_api, websocket_api +from .const import ( + ATTR_MESSAGE, + DOMAIN, + LOGBOOK_ENTITIES_FILTER, + LOGBOOK_ENTRY_DOMAIN, + LOGBOOK_ENTRY_ENTITY_ID, + LOGBOOK_ENTRY_MESSAGE, + LOGBOOK_ENTRY_NAME, + LOGBOOK_FILTERS, +) +from .const import LOGBOOK_ENTRY_ICON # noqa: F401 +from .models import LazyEventPartialState # noqa: F401 CONFIG_SCHEMA = vol.Schema( {DOMAIN: INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA}, extra=vol.ALLOW_EXTRA ) -CONTEXT_USER_ID = "context_user_id" -CONTEXT_ENTITY_ID = "context_entity_id" -CONTEXT_ENTITY_ID_NAME = "context_entity_id_name" -CONTEXT_EVENT_TYPE = "context_event_type" -CONTEXT_DOMAIN = "context_domain" -CONTEXT_STATE = "context_state" -CONTEXT_SERVICE = "context_service" -CONTEXT_NAME = "context_name" -CONTEXT_MESSAGE = "context_message" - -LOGBOOK_ENTRY_DOMAIN = "domain" -LOGBOOK_ENTRY_ENTITY_ID = "entity_id" -LOGBOOK_ENTRY_ICON = "icon" -LOGBOOK_ENTRY_MESSAGE = "message" -LOGBOOK_ENTRY_NAME = "name" -LOGBOOK_ENTRY_STATE = "state" -LOGBOOK_ENTRY_WHEN = "when" - -ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED = {EVENT_LOGBOOK_ENTRY, EVENT_CALL_SERVICE} -ENTITY_EVENTS_WITHOUT_CONFIG_ENTRY = { - EVENT_LOGBOOK_ENTRY, - EVENT_AUTOMATION_TRIGGERED, - EVENT_SCRIPT_STARTED, -} LOG_MESSAGE_SCHEMA = vol.Schema( { @@ -117,10 +57,6 @@ LOG_MESSAGE_SCHEMA = vol.Schema( ) -LOGBOOK_FILTERS = "logbook_filters" -LOGBOOK_ENTITIES_FILTER = "entities_filter" - - @bind_hass def log_entry( hass: HomeAssistant, @@ -186,13 +122,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: else: filters = None entities_filter = None - hass.data[LOGBOOK_FILTERS] = filters hass.data[LOGBOOK_ENTITIES_FILTER] = entities_filter - - hass.http.register_view(LogbookView(conf, filters, entities_filter)) - websocket_api.async_register_command(hass, ws_get_events) - + websocket_api.async_setup(hass) + rest_api.async_setup(hass, config, filters, entities_filter) hass.services.async_register(DOMAIN, "log", log_message, schema=LOG_MESSAGE_SCHEMA) await async_process_integration_platforms(hass, DOMAIN, _process_logbook_platform) @@ -215,628 +148,3 @@ async def _process_logbook_platform( hass.data[DOMAIN][event_name] = (domain, describe_callback) platform.async_describe_events(hass, _async_describe_event) - - -def _async_determine_event_types( - hass: HomeAssistant, entity_ids: list[str] | None, device_ids: list[str] | None -) -> tuple[str, ...]: - """Reduce the event types based on the entity ids and device ids.""" - external_events: dict[ - str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] - ] = hass.data.get(DOMAIN, {}) - if not entity_ids and not device_ids: - return (*ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, *external_events) - config_entry_ids: set[str] = set() - intrested_event_types: set[str] = set() - - if entity_ids: - # - # Home Assistant doesn't allow firing events from - # entities so we have a limited list to check - # - # automations and scripts can refer to entities - # but they do not have a config entry so we need - # to add them. - # - # We also allow entity_ids to be recorded via - # manual logbook entries. - # - intrested_event_types |= ENTITY_EVENTS_WITHOUT_CONFIG_ENTRY - - if device_ids: - dev_reg = dr.async_get(hass) - for device_id in device_ids: - if (device := dev_reg.async_get(device_id)) and device.config_entries: - config_entry_ids |= device.config_entries - interested_domains: set[str] = set() - for entry_id in config_entry_ids: - if entry := hass.config_entries.async_get_entry(entry_id): - interested_domains.add(entry.domain) - for external_event, domain_call in external_events.items(): - if domain_call[0] in interested_domains: - intrested_event_types.add(external_event) - - return tuple( - event_type - for event_type in (EVENT_LOGBOOK_ENTRY, *external_events) - if event_type in intrested_event_types - ) - - -def _ws_formatted_get_events( - hass: HomeAssistant, - msg_id: int, - start_day: dt, - end_day: dt, - event_types: tuple[str, ...], - ent_reg: er.EntityRegistry, - entity_ids: list[str] | None = None, - device_ids: list[str] | None = None, - filters: Filters | None = None, - entities_filter: EntityFilter | Callable[[str], bool] | None = None, - context_id: str | None = None, -) -> str: - """Fetch events and convert them to json in the executor.""" - return JSON_DUMP( - messages.result_message( - msg_id, - _get_events( - hass, - start_day, - end_day, - event_types, - ent_reg, - entity_ids, - device_ids, - filters, - entities_filter, - context_id, - True, - False, - ), - ) - ) - - -@websocket_api.websocket_command( - { - vol.Required("type"): "logbook/get_events", - vol.Required("start_time"): str, - vol.Optional("end_time"): str, - vol.Optional("entity_ids"): [str], - vol.Optional("device_ids"): [str], - vol.Optional("context_id"): str, - } -) -@websocket_api.async_response -async def ws_get_events( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict -) -> None: - """Handle logbook get events websocket command.""" - start_time_str = msg["start_time"] - end_time_str = msg.get("end_time") - utc_now = dt_util.utcnow() - - if start_time := dt_util.parse_datetime(start_time_str): - start_time = dt_util.as_utc(start_time) - else: - connection.send_error(msg["id"], "invalid_start_time", "Invalid start_time") - return - - if not end_time_str: - end_time = utc_now - elif parsed_end_time := dt_util.parse_datetime(end_time_str): - end_time = dt_util.as_utc(parsed_end_time) - else: - connection.send_error(msg["id"], "invalid_end_time", "Invalid end_time") - return - - if start_time > utc_now: - connection.send_result(msg["id"], []) - return - - device_ids = msg.get("device_ids") - entity_ids = msg.get("entity_ids") - context_id = msg.get("context_id") - event_types = _async_determine_event_types(hass, entity_ids, device_ids) - ent_reg = er.async_get(hass) - - connection.send_message( - await get_instance(hass).async_add_executor_job( - _ws_formatted_get_events, - hass, - msg["id"], - start_time, - end_time, - event_types, - ent_reg, - entity_ids, - device_ids, - hass.data[LOGBOOK_FILTERS], - hass.data[LOGBOOK_ENTITIES_FILTER], - context_id, - ) - ) - - -class LogbookView(HomeAssistantView): - """Handle logbook view requests.""" - - url = "/api/logbook" - name = "api:logbook" - extra_urls = ["/api/logbook/{datetime}"] - - def __init__( - self, - config: dict[str, Any], - filters: Filters | None, - entities_filter: EntityFilter | None, - ) -> None: - """Initialize the logbook view.""" - self.config = config - self.filters = filters - self.entities_filter = entities_filter - - async def get( - self, request: web.Request, datetime: str | None = None - ) -> web.Response: - """Retrieve logbook entries.""" - if datetime: - if (datetime_dt := dt_util.parse_datetime(datetime)) is None: - return self.json_message("Invalid datetime", HTTPStatus.BAD_REQUEST) - else: - datetime_dt = dt_util.start_of_local_day() - - if (period_str := request.query.get("period")) is None: - period: int = 1 - else: - period = int(period_str) - - if entity_ids_str := request.query.get("entity"): - try: - entity_ids = cv.entity_ids(entity_ids_str) - except vol.Invalid: - raise InvalidEntityFormatError( - f"Invalid entity id(s) encountered: {entity_ids_str}. " - "Format should be ." - ) from vol.Invalid - else: - entity_ids = None - - if (end_time_str := request.query.get("end_time")) is None: - start_day = dt_util.as_utc(datetime_dt) - timedelta(days=period - 1) - end_day = start_day + timedelta(days=period) - else: - start_day = datetime_dt - if (end_day_dt := dt_util.parse_datetime(end_time_str)) is None: - return self.json_message("Invalid end_time", HTTPStatus.BAD_REQUEST) - end_day = end_day_dt - - hass = request.app["hass"] - - context_id = request.query.get("context_id") - - if entity_ids and context_id: - return self.json_message( - "Can't combine entity with context_id", HTTPStatus.BAD_REQUEST - ) - - event_types = _async_determine_event_types(hass, entity_ids, None) - ent_reg = er.async_get(hass) - - def json_events() -> web.Response: - """Fetch events and generate JSON.""" - return self.json( - _get_events( - hass, - start_day, - end_day, - event_types, - ent_reg, - entity_ids, - None, - self.filters, - self.entities_filter, - context_id, - False, - True, - ) - ) - - return cast( - web.Response, await get_instance(hass).async_add_executor_job(json_events) - ) - - -def _humanify( - rows: Generator[Row, None, None], - entities_filter: EntityFilter | Callable[[str], bool] | None, - ent_reg: er.EntityRegistry, - external_events: dict[ - str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] - ], - entity_name_cache: EntityNameCache, - format_time: Callable[[Row], Any], - include_entity_name: bool = True, -) -> Generator[dict[str, Any], None, None]: - """Generate a converted list of events into entries.""" - # Continuous sensors, will be excluded from the logbook - continuous_sensors: dict[str, bool] = {} - event_data_cache: dict[str, dict[str, Any]] = {} - context_lookup: dict[str | None, Row | None] = {None: None} - event_cache = EventCache(event_data_cache) - context_augmenter = ContextAugmenter( - context_lookup, - entity_name_cache, - external_events, - event_cache, - include_entity_name, - ) - - def _keep_row(row: Row, event_type: str) -> bool: - """Check if the entity_filter rejects a row.""" - assert entities_filter is not None - if entity_id := _row_event_data_extract(row, ENTITY_ID_JSON_EXTRACT): - return entities_filter(entity_id) - - if event_type in external_events: - # If the entity_id isn't described, use the domain that describes - # the event for filtering. - domain: str | None = external_events[event_type][0] - else: - domain = _row_event_data_extract(row, DOMAIN_JSON_EXTRACT) - - return domain is not None and entities_filter(f"{domain}._") - - # Process rows - for row in rows: - context_id = row.context_id - context_lookup.setdefault(context_id, row) - if row.context_only: - continue - event_type = row.event_type - if event_type == EVENT_CALL_SERVICE or ( - event_type is not PSUEDO_EVENT_STATE_CHANGED - and entities_filter is not None - and not _keep_row(row, event_type) - ): - continue - - if event_type is PSUEDO_EVENT_STATE_CHANGED: - entity_id = row.entity_id - assert entity_id is not None - # Skip continuous sensors - if ( - is_continuous := continuous_sensors.get(entity_id) - ) is None and split_entity_id(entity_id)[0] == SENSOR_DOMAIN: - is_continuous = _is_sensor_continuous(ent_reg, entity_id) - continuous_sensors[entity_id] = is_continuous - if is_continuous: - continue - - data = { - LOGBOOK_ENTRY_WHEN: format_time(row), - LOGBOOK_ENTRY_STATE: row.state, - LOGBOOK_ENTRY_ENTITY_ID: entity_id, - } - if include_entity_name: - data[LOGBOOK_ENTRY_NAME] = entity_name_cache.get(entity_id, row) - if icon := row.icon or row.old_format_icon: - data[LOGBOOK_ENTRY_ICON] = icon - - context_augmenter.augment(data, row, context_id) - yield data - - elif event_type in external_events: - domain, describe_event = external_events[event_type] - data = describe_event(event_cache.get(row)) - data[LOGBOOK_ENTRY_WHEN] = format_time(row) - data[LOGBOOK_ENTRY_DOMAIN] = domain - context_augmenter.augment(data, row, context_id) - yield data - - elif event_type == EVENT_LOGBOOK_ENTRY: - event = event_cache.get(row) - if not (event_data := event.data): - continue - entry_domain = event_data.get(ATTR_DOMAIN) - entry_entity_id = event_data.get(ATTR_ENTITY_ID) - if entry_domain is None and entry_entity_id is not None: - with suppress(IndexError): - entry_domain = split_entity_id(str(entry_entity_id))[0] - - data = { - LOGBOOK_ENTRY_WHEN: format_time(row), - LOGBOOK_ENTRY_NAME: event_data.get(ATTR_NAME), - LOGBOOK_ENTRY_MESSAGE: event_data.get(ATTR_MESSAGE), - LOGBOOK_ENTRY_DOMAIN: entry_domain, - LOGBOOK_ENTRY_ENTITY_ID: entry_entity_id, - } - context_augmenter.augment(data, row, context_id) - yield data - - -def _get_events( - hass: HomeAssistant, - start_day: dt, - end_day: dt, - event_types: tuple[str, ...], - ent_reg: er.EntityRegistry, - entity_ids: list[str] | None = None, - device_ids: list[str] | None = None, - filters: Filters | None = None, - entities_filter: EntityFilter | Callable[[str], bool] | None = None, - context_id: str | None = None, - timestamp: bool = False, - include_entity_name: bool = True, -) -> list[dict[str, Any]]: - """Get events for a period of time.""" - assert not ( - context_id and (entity_ids or device_ids) - ), "can't pass in both context_id and (entity_ids or device_ids)" - external_events: dict[ - str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] - ] = hass.data.get(DOMAIN, {}) - format_time = _row_time_fired_timestamp if timestamp else _row_time_fired_isoformat - entity_name_cache = EntityNameCache(hass) - if entity_ids or device_ids: - entities_filter = None - - def yield_rows(query: Query) -> Generator[Row, None, None]: - """Yield rows from the database.""" - # end_day - start_day intentionally checks .days and not .total_seconds() - # since we don't want to switch over to buffered if they go - # over one day by a few hours since the UI makes it so easy to do that. - if entity_ids or context_id or (end_day - start_day).days <= 1: - return query.all() # type: ignore[no-any-return] - # Only buffer rows to reduce memory pressure - # if we expect the result set is going to be very large. - # What is considered very large is going to differ - # based on the hardware Home Assistant is running on. - # - # sqlalchemy suggests that is at least 10k, but for - # even and RPi3 that number seems higher in testing - # so we don't switch over until we request > 1 day+ of data. - # - return query.yield_per(1024) # type: ignore[no-any-return] - - stmt = statement_for_request( - start_day, - end_day, - event_types, - entity_ids, - device_ids, - filters, - context_id, - ) - if _LOGGER.isEnabledFor(logging.DEBUG): - _LOGGER.debug( - "Literal statement: %s", - stmt.compile(compile_kwargs={"literal_binds": True}), - ) - - with session_scope(hass=hass) as session: - return list( - _humanify( - yield_rows(session.execute(stmt)), - entities_filter, - ent_reg, - external_events, - entity_name_cache, - format_time, - include_entity_name, - ) - ) - - -class ContextAugmenter: - """Augment data with context trace.""" - - def __init__( - self, - context_lookup: dict[str | None, Row | None], - entity_name_cache: EntityNameCache, - external_events: dict[ - str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] - ], - event_cache: EventCache, - include_entity_name: bool, - ) -> None: - """Init the augmenter.""" - self.context_lookup = context_lookup - self.entity_name_cache = entity_name_cache - self.external_events = external_events - self.event_cache = event_cache - self.include_entity_name = include_entity_name - - def augment(self, data: dict[str, Any], row: Row, context_id: str) -> None: - """Augment data from the row and cache.""" - if context_user_id := row.context_user_id: - data[CONTEXT_USER_ID] = context_user_id - - if not (context_row := self.context_lookup.get(context_id)): - return - - if _rows_match(row, context_row): - # This is the first event with the given ID. Was it directly caused by - # a parent event? - if ( - not row.context_parent_id - or (context_row := self.context_lookup.get(row.context_parent_id)) - is None - ): - return - # Ensure the (parent) context_event exists and is not the root cause of - # this log entry. - if _rows_match(row, context_row): - return - - event_type = context_row.event_type - - # State change - if context_entity_id := context_row.entity_id: - data[CONTEXT_STATE] = context_row.state - data[CONTEXT_ENTITY_ID] = context_entity_id - if self.include_entity_name: - data[CONTEXT_ENTITY_ID_NAME] = self.entity_name_cache.get( - context_entity_id, context_row - ) - return - - # Call service - if event_type == EVENT_CALL_SERVICE: - event = self.event_cache.get(context_row) - event_data = event.data - data[CONTEXT_DOMAIN] = event_data.get(ATTR_DOMAIN) - data[CONTEXT_SERVICE] = event_data.get(ATTR_SERVICE) - data[CONTEXT_EVENT_TYPE] = event_type - return - - if event_type not in self.external_events: - return - - domain, describe_event = self.external_events[event_type] - data[CONTEXT_EVENT_TYPE] = event_type - data[CONTEXT_DOMAIN] = domain - event = self.event_cache.get(context_row) - described = describe_event(event) - if name := described.get(ATTR_NAME): - data[CONTEXT_NAME] = name - if message := described.get(ATTR_MESSAGE): - data[CONTEXT_MESSAGE] = message - if not (attr_entity_id := described.get(ATTR_ENTITY_ID)): - return - data[CONTEXT_ENTITY_ID] = attr_entity_id - if self.include_entity_name: - data[CONTEXT_ENTITY_ID_NAME] = self.entity_name_cache.get( - attr_entity_id, context_row - ) - - -def _is_sensor_continuous(ent_reg: er.EntityRegistry, entity_id: str) -> bool: - """Determine if a sensor is continuous by checking its state class. - - Sensors with a unit_of_measurement are also considered continuous, but are filtered - already by the SQL query generated by _get_events - """ - if not (entry := ent_reg.async_get(entity_id)): - # Entity not registered, so can't have a state class - return False - return ( - entry.capabilities is not None - and entry.capabilities.get(ATTR_STATE_CLASS) is not None - ) - - -def _rows_match(row: Row, other_row: Row) -> bool: - """Check of rows match by using the same method as Events __hash__.""" - if ( - (state_id := row.state_id) is not None - and state_id == other_row.state_id - or (event_id := row.event_id) is not None - and event_id == other_row.event_id - ): - return True - return False - - -def _row_event_data_extract(row: Row, extractor: re.Pattern) -> str | None: - """Extract from event_data row.""" - result = extractor.search(row.shared_data or row.event_data or "") - return result.group(1) if result else None - - -def _row_time_fired_isoformat(row: Row) -> str: - """Convert the row timed_fired to isoformat.""" - return process_timestamp_to_utc_isoformat(row.time_fired or dt_util.utcnow()) - - -def _row_time_fired_timestamp(row: Row) -> float: - """Convert the row timed_fired to timestamp.""" - return process_datetime_to_timestamp(row.time_fired or dt_util.utcnow()) - - -class LazyEventPartialState: - """A lazy version of core Event with limited State joined in.""" - - __slots__ = [ - "row", - "_event_data", - "_event_data_cache", - "event_type", - "entity_id", - "state", - "context_id", - "context_user_id", - "context_parent_id", - "data", - ] - - def __init__( - self, - row: Row, - event_data_cache: dict[str, dict[str, Any]], - ) -> None: - """Init the lazy event.""" - self.row = row - self._event_data: dict[str, Any] | None = None - self._event_data_cache = event_data_cache - self.event_type: str = self.row.event_type - self.entity_id: str | None = self.row.entity_id - self.state = self.row.state - self.context_id: str | None = self.row.context_id - self.context_user_id: str | None = self.row.context_user_id - self.context_parent_id: str | None = self.row.context_parent_id - source: str = self.row.shared_data or self.row.event_data - if not source: - self.data = {} - elif event_data := self._event_data_cache.get(source): - self.data = event_data - else: - self.data = self._event_data_cache[source] = cast( - dict[str, Any], json.loads(source) - ) - - -class EntityNameCache: - """A cache to lookup the name for an entity. - - This class should not be used to lookup attributes - that are expected to change state. - """ - - def __init__(self, hass: HomeAssistant) -> None: - """Init the cache.""" - self._hass = hass - self._names: dict[str, str] = {} - - def get(self, entity_id: str, row: Row) -> str: - """Lookup an the friendly name.""" - if entity_id in self._names: - return self._names[entity_id] - if (current_state := self._hass.states.get(entity_id)) and ( - friendly_name := current_state.attributes.get(ATTR_FRIENDLY_NAME) - ): - self._names[entity_id] = friendly_name - else: - return split_entity_id(entity_id)[1].replace("_", " ") - - return self._names[entity_id] - - -class EventCache: - """Cache LazyEventPartialState by row.""" - - def __init__(self, event_data_cache: dict[str, dict[str, Any]]) -> None: - """Init the cache.""" - self._event_data_cache = event_data_cache - self.event_cache: dict[Row, LazyEventPartialState] = {} - - def get(self, row: Row) -> LazyEventPartialState: - """Get the event from the row.""" - if event := self.event_cache.get(row): - return event - event = self.event_cache[row] = LazyEventPartialState( - row, self._event_data_cache - ) - return event diff --git a/homeassistant/components/logbook/const.py b/homeassistant/components/logbook/const.py new file mode 100644 index 00000000000..406406e9dd8 --- /dev/null +++ b/homeassistant/components/logbook/const.py @@ -0,0 +1,39 @@ +"""Event parser and human readable log generator.""" +from __future__ import annotations + +from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED +from homeassistant.components.script import EVENT_SCRIPT_STARTED +from homeassistant.const import EVENT_CALL_SERVICE, EVENT_LOGBOOK_ENTRY + +ATTR_MESSAGE = "message" + +DOMAIN = "logbook" + +CONTEXT_USER_ID = "context_user_id" +CONTEXT_ENTITY_ID = "context_entity_id" +CONTEXT_ENTITY_ID_NAME = "context_entity_id_name" +CONTEXT_EVENT_TYPE = "context_event_type" +CONTEXT_DOMAIN = "context_domain" +CONTEXT_STATE = "context_state" +CONTEXT_SERVICE = "context_service" +CONTEXT_NAME = "context_name" +CONTEXT_MESSAGE = "context_message" + +LOGBOOK_ENTRY_DOMAIN = "domain" +LOGBOOK_ENTRY_ENTITY_ID = "entity_id" +LOGBOOK_ENTRY_ICON = "icon" +LOGBOOK_ENTRY_MESSAGE = "message" +LOGBOOK_ENTRY_NAME = "name" +LOGBOOK_ENTRY_STATE = "state" +LOGBOOK_ENTRY_WHEN = "when" + +ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED = {EVENT_LOGBOOK_ENTRY, EVENT_CALL_SERVICE} +ENTITY_EVENTS_WITHOUT_CONFIG_ENTRY = { + EVENT_LOGBOOK_ENTRY, + EVENT_AUTOMATION_TRIGGERED, + EVENT_SCRIPT_STARTED, +} + + +LOGBOOK_FILTERS = "logbook_filters" +LOGBOOK_ENTITIES_FILTER = "entities_filter" diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py new file mode 100644 index 00000000000..cc9ea238f8b --- /dev/null +++ b/homeassistant/components/logbook/helpers.py @@ -0,0 +1,193 @@ +"""Event parser and human readable log generator.""" +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ( + ATTR_DEVICE_ID, + ATTR_ENTITY_ID, + ATTR_UNIT_OF_MEASUREMENT, + EVENT_LOGBOOK_ENTRY, + EVENT_STATE_CHANGED, +) +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HomeAssistant, + State, + callback, + is_callback, +) +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.event import async_track_state_change_event + +from .const import ( + ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, + DOMAIN, + ENTITY_EVENTS_WITHOUT_CONFIG_ENTRY, +) +from .models import LazyEventPartialState + + +def async_filter_entities(hass: HomeAssistant, entity_ids: list[str]) -> list[str]: + """Filter out any entities that logbook will not produce results for.""" + ent_reg = er.async_get(hass) + return [ + entity_id + for entity_id in entity_ids + if not _is_entity_id_filtered(hass, ent_reg, entity_id) + ] + + +def async_determine_event_types( + hass: HomeAssistant, entity_ids: list[str] | None, device_ids: list[str] | None +) -> tuple[str, ...]: + """Reduce the event types based on the entity ids and device ids.""" + external_events: dict[ + str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] + ] = hass.data.get(DOMAIN, {}) + if not entity_ids and not device_ids: + return (*ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, *external_events) + config_entry_ids: set[str] = set() + intrested_event_types: set[str] = set() + + if entity_ids: + # + # Home Assistant doesn't allow firing events from + # entities so we have a limited list to check + # + # automations and scripts can refer to entities + # but they do not have a config entry so we need + # to add them. + # + # We also allow entity_ids to be recorded via + # manual logbook entries. + # + intrested_event_types |= ENTITY_EVENTS_WITHOUT_CONFIG_ENTRY + + if device_ids: + dev_reg = dr.async_get(hass) + for device_id in device_ids: + if (device := dev_reg.async_get(device_id)) and device.config_entries: + config_entry_ids |= device.config_entries + interested_domains: set[str] = set() + for entry_id in config_entry_ids: + if entry := hass.config_entries.async_get_entry(entry_id): + interested_domains.add(entry.domain) + for external_event, domain_call in external_events.items(): + if domain_call[0] in interested_domains: + intrested_event_types.add(external_event) + + return tuple( + event_type + for event_type in (EVENT_LOGBOOK_ENTRY, *external_events) + if event_type in intrested_event_types + ) + + +@callback +def async_subscribe_events( + hass: HomeAssistant, + subscriptions: list[CALLBACK_TYPE], + target: Callable[[Event], None], + event_types: tuple[str, ...], + entity_ids: list[str] | None, + device_ids: list[str] | None, +) -> None: + """Subscribe to events for the entities and devices or all. + + These are the events we need to listen for to do + the live logbook stream. + """ + ent_reg = er.async_get(hass) + assert is_callback(target), "target must be a callback" + event_forwarder = target + + if entity_ids or device_ids: + entity_ids_set = set(entity_ids) if entity_ids else set() + device_ids_set = set(device_ids) if device_ids else set() + + @callback + def _forward_events_filtered(event: Event) -> None: + event_data = event.data + if ( + entity_ids_set and event_data.get(ATTR_ENTITY_ID) in entity_ids_set + ) or (device_ids_set and event_data.get(ATTR_DEVICE_ID) in device_ids_set): + target(event) + + event_forwarder = _forward_events_filtered + + for event_type in event_types: + subscriptions.append( + hass.bus.async_listen(event_type, event_forwarder, run_immediately=True) + ) + + @callback + def _forward_state_events_filtered(event: Event) -> None: + if event.data.get("old_state") is None or event.data.get("new_state") is None: + return + state: State = event.data["new_state"] + if not _is_state_filtered(ent_reg, state): + target(event) + + if entity_ids: + subscriptions.append( + async_track_state_change_event( + hass, entity_ids, _forward_state_events_filtered + ) + ) + return + + # We want the firehose + subscriptions.append( + hass.bus.async_listen( + EVENT_STATE_CHANGED, + _forward_state_events_filtered, + run_immediately=True, + ) + ) + + +def is_sensor_continuous(ent_reg: er.EntityRegistry, entity_id: str) -> bool: + """Determine if a sensor is continuous by checking its state class. + + Sensors with a unit_of_measurement are also considered continuous, but are filtered + already by the SQL query generated by _get_events + """ + if not (entry := ent_reg.async_get(entity_id)): + # Entity not registered, so can't have a state class + return False + return ( + entry.capabilities is not None + and entry.capabilities.get(ATTR_STATE_CLASS) is not None + ) + + +def _is_state_filtered(ent_reg: er.EntityRegistry, state: State) -> bool: + """Check if the logbook should filter a state. + + Used when we are in live mode to ensure + we only get significant changes (state.last_changed != state.last_updated) + """ + return bool( + state.last_changed != state.last_updated + or ATTR_UNIT_OF_MEASUREMENT in state.attributes + or is_sensor_continuous(ent_reg, state.entity_id) + ) + + +def _is_entity_id_filtered( + hass: HomeAssistant, ent_reg: er.EntityRegistry, entity_id: str +) -> bool: + """Check if the logbook should filter an entity. + + Used to setup listeners and which entities to select + from the database when a list of entities is requested. + """ + return bool( + (state := hass.states.get(entity_id)) + and (ATTR_UNIT_OF_MEASUREMENT in state.attributes) + or is_sensor_continuous(ent_reg, entity_id) + ) diff --git a/homeassistant/components/logbook/models.py b/homeassistant/components/logbook/models.py new file mode 100644 index 00000000000..e4844751c6a --- /dev/null +++ b/homeassistant/components/logbook/models.py @@ -0,0 +1,113 @@ +"""Event parser and human readable log generator.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime as dt +import json +from typing import Any, cast + +from sqlalchemy.engine.row import Row + +from homeassistant.const import ATTR_ICON, EVENT_STATE_CHANGED +from homeassistant.core import Context, Event, State, callback + + +class LazyEventPartialState: + """A lazy version of core Event with limited State joined in.""" + + __slots__ = [ + "row", + "_event_data", + "_event_data_cache", + "event_type", + "entity_id", + "state", + "context_id", + "context_user_id", + "context_parent_id", + "data", + ] + + def __init__( + self, + row: Row | EventAsRow, + event_data_cache: dict[str, dict[str, Any]], + ) -> None: + """Init the lazy event.""" + self.row = row + self._event_data: dict[str, Any] | None = None + self._event_data_cache = event_data_cache + self.event_type: str | None = self.row.event_type + self.entity_id: str | None = self.row.entity_id + self.state = self.row.state + self.context_id: str | None = self.row.context_id + self.context_user_id: str | None = self.row.context_user_id + self.context_parent_id: str | None = self.row.context_parent_id + if data := getattr(row, "data", None): + # If its an EventAsRow we can avoid the whole + # json decode process as we already have the data + self.data = data + return + source = cast(str, self.row.shared_data or self.row.event_data) + if not source: + self.data = {} + elif event_data := self._event_data_cache.get(source): + self.data = event_data + else: + self.data = self._event_data_cache[source] = cast( + dict[str, Any], json.loads(source) + ) + + +@dataclass +class EventAsRow: + """Convert an event to a row.""" + + data: dict[str, Any] + context: Context + context_id: str + time_fired: dt + state_id: int + event_data: str | None = None + old_format_icon: None = None + event_id: None = None + entity_id: str | None = None + icon: str | None = None + context_user_id: str | None = None + context_parent_id: str | None = None + event_type: str | None = None + state: str | None = None + shared_data: str | None = None + context_only: None = None + + +@callback +def async_event_to_row(event: Event) -> EventAsRow | None: + """Convert an event to a row.""" + if event.event_type != EVENT_STATE_CHANGED: + return EventAsRow( + data=event.data, + context=event.context, + event_type=event.event_type, + context_id=event.context.id, + context_user_id=event.context.user_id, + context_parent_id=event.context.parent_id, + time_fired=event.time_fired, + state_id=hash(event), + ) + # States are prefiltered so we never get states + # that are missing new_state or old_state + # since the logbook does not show these + new_state: State = event.data["new_state"] + return EventAsRow( + data=event.data, + context=event.context, + entity_id=new_state.entity_id, + state=new_state.state, + context_id=new_state.context.id, + context_user_id=new_state.context.user_id, + context_parent_id=new_state.context.parent_id, + time_fired=new_state.last_updated, + state_id=hash(event), + icon=new_state.attributes.get(ATTR_ICON), + ) diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py new file mode 100644 index 00000000000..c9f20f27f7c --- /dev/null +++ b/homeassistant/components/logbook/processor.py @@ -0,0 +1,488 @@ +"""Event parser and human readable log generator.""" +from __future__ import annotations + +from collections.abc import Callable, Generator +from contextlib import suppress +from dataclasses import dataclass +from datetime import datetime as dt +import logging +import re +from typing import Any + +from sqlalchemy.engine.row import Row +from sqlalchemy.orm.query import Query + +from homeassistant.components.recorder.filters import Filters +from homeassistant.components.recorder.models import ( + process_datetime_to_timestamp, + process_timestamp_to_utc_isoformat, +) +from homeassistant.components.recorder.util import session_scope +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ( + ATTR_DOMAIN, + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_NAME, + ATTR_SERVICE, + EVENT_CALL_SERVICE, + EVENT_LOGBOOK_ENTRY, +) +from homeassistant.core import HomeAssistant, split_entity_id +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entityfilter import EntityFilter +import homeassistant.util.dt as dt_util + +from .const import ( + ATTR_MESSAGE, + CONTEXT_DOMAIN, + CONTEXT_ENTITY_ID, + CONTEXT_ENTITY_ID_NAME, + CONTEXT_EVENT_TYPE, + CONTEXT_MESSAGE, + CONTEXT_NAME, + CONTEXT_SERVICE, + CONTEXT_STATE, + CONTEXT_USER_ID, + DOMAIN, + LOGBOOK_ENTITIES_FILTER, + LOGBOOK_ENTRY_DOMAIN, + LOGBOOK_ENTRY_ENTITY_ID, + LOGBOOK_ENTRY_ICON, + LOGBOOK_ENTRY_MESSAGE, + LOGBOOK_ENTRY_NAME, + LOGBOOK_ENTRY_STATE, + LOGBOOK_ENTRY_WHEN, + LOGBOOK_FILTERS, +) +from .helpers import is_sensor_continuous +from .models import EventAsRow, LazyEventPartialState, async_event_to_row +from .queries import statement_for_request +from .queries.common import PSUEDO_EVENT_STATE_CHANGED + +_LOGGER = logging.getLogger(__name__) + +ENTITY_ID_JSON_EXTRACT = re.compile('"entity_id": ?"([^"]+)"') +DOMAIN_JSON_EXTRACT = re.compile('"domain": ?"([^"]+)"') + + +@dataclass +class LogbookRun: + """A logbook run which may be a long running event stream or single request.""" + + context_lookup: ContextLookup + external_events: dict[ + str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] + ] + event_cache: EventCache + entity_name_cache: EntityNameCache + include_entity_name: bool + format_time: Callable[[Row], Any] + + +class EventProcessor: + """Stream into logbook format.""" + + def __init__( + self, + hass: HomeAssistant, + event_types: tuple[str, ...], + entity_ids: list[str] | None = None, + device_ids: list[str] | None = None, + context_id: str | None = None, + timestamp: bool = False, + include_entity_name: bool = True, + ) -> None: + """Init the event stream.""" + assert not ( + context_id and (entity_ids or device_ids) + ), "can't pass in both context_id and (entity_ids or device_ids)" + self.hass = hass + self.ent_reg = er.async_get(hass) + self.event_types = event_types + self.entity_ids = entity_ids + self.device_ids = device_ids + self.context_id = context_id + self.filters: Filters | None = hass.data[LOGBOOK_FILTERS] + if self.limited_select: + self.entities_filter: EntityFilter | Callable[[str], bool] | None = None + else: + self.entities_filter = hass.data[LOGBOOK_ENTITIES_FILTER] + format_time = ( + _row_time_fired_timestamp if timestamp else _row_time_fired_isoformat + ) + external_events: dict[ + str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] + ] = hass.data.get(DOMAIN, {}) + self.logbook_run = LogbookRun( + context_lookup=ContextLookup(hass), + external_events=external_events, + event_cache=EventCache({}), + entity_name_cache=EntityNameCache(self.hass), + include_entity_name=include_entity_name, + format_time=format_time, + ) + self.context_augmenter = ContextAugmenter(self.logbook_run) + + @property + def limited_select(self) -> bool: + """Check if the stream is limited by entities context or device ids.""" + return bool(self.entity_ids or self.context_id or self.device_ids) + + def switch_to_live(self) -> None: + """Switch to live stream. + + Clear caches so we can reduce memory pressure. + """ + self.logbook_run.event_cache.clear() + self.logbook_run.context_lookup.clear() + + def get_events( + self, + start_day: dt, + end_day: dt, + ) -> list[dict[str, Any]]: + """Get events for a period of time.""" + + def yield_rows(query: Query) -> Generator[Row, None, None]: + """Yield rows from the database.""" + # end_day - start_day intentionally checks .days and not .total_seconds() + # since we don't want to switch over to buffered if they go + # over one day by a few hours since the UI makes it so easy to do that. + if self.limited_select or (end_day - start_day).days <= 1: + return query.all() # type: ignore[no-any-return] + # Only buffer rows to reduce memory pressure + # if we expect the result set is going to be very large. + # What is considered very large is going to differ + # based on the hardware Home Assistant is running on. + # + # sqlalchemy suggests that is at least 10k, but for + # even and RPi3 that number seems higher in testing + # so we don't switch over until we request > 1 day+ of data. + # + return query.yield_per(1024) # type: ignore[no-any-return] + + stmt = statement_for_request( + start_day, + end_day, + self.event_types, + self.entity_ids, + self.device_ids, + self.filters, + self.context_id, + ) + if _LOGGER.isEnabledFor(logging.DEBUG): + _LOGGER.debug( + "Literal statement: %s", + stmt.compile(compile_kwargs={"literal_binds": True}), + ) + + with session_scope(hass=self.hass) as session: + return self.humanify(yield_rows(session.execute(stmt))) + + def humanify( + self, row_generator: Generator[Row | EventAsRow, None, None] + ) -> list[dict[str, str]]: + """Humanify rows.""" + return list( + _humanify( + row_generator, + self.entities_filter, + self.ent_reg, + self.logbook_run, + self.context_augmenter, + ) + ) + + +def _humanify( + rows: Generator[Row | EventAsRow, None, None], + entities_filter: EntityFilter | Callable[[str], bool] | None, + ent_reg: er.EntityRegistry, + logbook_run: LogbookRun, + context_augmenter: ContextAugmenter, +) -> Generator[dict[str, Any], None, None]: + """Generate a converted list of events into entries.""" + # Continuous sensors, will be excluded from the logbook + continuous_sensors: dict[str, bool] = {} + context_lookup = logbook_run.context_lookup + external_events = logbook_run.external_events + event_cache = logbook_run.event_cache + entity_name_cache = logbook_run.entity_name_cache + include_entity_name = logbook_run.include_entity_name + format_time = logbook_run.format_time + + def _keep_row(row: Row | EventAsRow, event_type: str) -> bool: + """Check if the entity_filter rejects a row.""" + assert entities_filter is not None + if entity_id := _row_event_data_extract(row, ENTITY_ID_JSON_EXTRACT): + return entities_filter(entity_id) + + if event_type in external_events: + # If the entity_id isn't described, use the domain that describes + # the event for filtering. + domain: str | None = external_events[event_type][0] + else: + domain = _row_event_data_extract(row, DOMAIN_JSON_EXTRACT) + + return domain is not None and entities_filter(f"{domain}._") + + # Process rows + for row in rows: + context_id = context_lookup.memorize(row) + if row.context_only: + continue + event_type = row.event_type + if event_type == EVENT_CALL_SERVICE or ( + event_type is not PSUEDO_EVENT_STATE_CHANGED + and entities_filter is not None + and not _keep_row(row, event_type) + ): + continue + + if event_type is PSUEDO_EVENT_STATE_CHANGED: + entity_id = row.entity_id + assert entity_id is not None + # Skip continuous sensors + if ( + is_continuous := continuous_sensors.get(entity_id) + ) is None and split_entity_id(entity_id)[0] == SENSOR_DOMAIN: + is_continuous = is_sensor_continuous(ent_reg, entity_id) + continuous_sensors[entity_id] = is_continuous + if is_continuous: + continue + + data = { + LOGBOOK_ENTRY_WHEN: format_time(row), + LOGBOOK_ENTRY_STATE: row.state, + LOGBOOK_ENTRY_ENTITY_ID: entity_id, + } + if include_entity_name: + data[LOGBOOK_ENTRY_NAME] = entity_name_cache.get(entity_id) + if icon := row.icon or row.old_format_icon: + data[LOGBOOK_ENTRY_ICON] = icon + + context_augmenter.augment(data, row, context_id) + yield data + + elif event_type in external_events: + domain, describe_event = external_events[event_type] + data = describe_event(event_cache.get(row)) + data[LOGBOOK_ENTRY_WHEN] = format_time(row) + data[LOGBOOK_ENTRY_DOMAIN] = domain + context_augmenter.augment(data, row, context_id) + yield data + + elif event_type == EVENT_LOGBOOK_ENTRY: + event = event_cache.get(row) + if not (event_data := event.data): + continue + entry_domain = event_data.get(ATTR_DOMAIN) + entry_entity_id = event_data.get(ATTR_ENTITY_ID) + if entry_domain is None and entry_entity_id is not None: + with suppress(IndexError): + entry_domain = split_entity_id(str(entry_entity_id))[0] + data = { + LOGBOOK_ENTRY_WHEN: format_time(row), + LOGBOOK_ENTRY_NAME: event_data.get(ATTR_NAME), + LOGBOOK_ENTRY_MESSAGE: event_data.get(ATTR_MESSAGE), + LOGBOOK_ENTRY_DOMAIN: entry_domain, + LOGBOOK_ENTRY_ENTITY_ID: entry_entity_id, + } + context_augmenter.augment(data, row, context_id) + yield data + + +class ContextLookup: + """A lookup class for context origins.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Memorize context origin.""" + self.hass = hass + self._memorize_new = True + self._lookup: dict[str | None, Row | EventAsRow | None] = {None: None} + + def memorize(self, row: Row) -> str | None: + """Memorize a context from the database.""" + if self._memorize_new: + context_id: str = row.context_id + self._lookup.setdefault(context_id, row) + return context_id + return None + + def clear(self) -> None: + """Clear the context origins and stop recording new ones.""" + self._lookup.clear() + self._memorize_new = False + + def get(self, context_id: str) -> Row | None: + """Get the context origin.""" + return self._lookup.get(context_id) + + +class ContextAugmenter: + """Augment data with context trace.""" + + def __init__(self, logbook_run: LogbookRun) -> None: + """Init the augmenter.""" + self.context_lookup = logbook_run.context_lookup + self.entity_name_cache = logbook_run.entity_name_cache + self.external_events = logbook_run.external_events + self.event_cache = logbook_run.event_cache + self.include_entity_name = logbook_run.include_entity_name + + def _get_context_row( + self, context_id: str | None, row: Row | EventAsRow + ) -> Row | EventAsRow: + """Get the context row from the id or row context.""" + if context_id: + return self.context_lookup.get(context_id) + if (context := getattr(row, "context", None)) is not None and ( + origin_event := context.origin_event + ) is not None: + return async_event_to_row(origin_event) + return None + + def augment( + self, data: dict[str, Any], row: Row | EventAsRow, context_id: str | None + ) -> None: + """Augment data from the row and cache.""" + if context_user_id := row.context_user_id: + data[CONTEXT_USER_ID] = context_user_id + + if not (context_row := self._get_context_row(context_id, row)): + return + + if _rows_match(row, context_row): + # This is the first event with the given ID. Was it directly caused by + # a parent event? + if ( + not row.context_parent_id + or ( + context_row := self._get_context_row( + row.context_parent_id, context_row + ) + ) + is None + ): + return + # Ensure the (parent) context_event exists and is not the root cause of + # this log entry. + if _rows_match(row, context_row): + return + event_type = context_row.event_type + # State change + if context_entity_id := context_row.entity_id: + data[CONTEXT_STATE] = context_row.state + data[CONTEXT_ENTITY_ID] = context_entity_id + if self.include_entity_name: + data[CONTEXT_ENTITY_ID_NAME] = self.entity_name_cache.get( + context_entity_id + ) + return + + # Call service + if event_type == EVENT_CALL_SERVICE: + event = self.event_cache.get(context_row) + event_data = event.data + data[CONTEXT_DOMAIN] = event_data.get(ATTR_DOMAIN) + data[CONTEXT_SERVICE] = event_data.get(ATTR_SERVICE) + data[CONTEXT_EVENT_TYPE] = event_type + return + + if event_type not in self.external_events: + return + + domain, describe_event = self.external_events[event_type] + data[CONTEXT_EVENT_TYPE] = event_type + data[CONTEXT_DOMAIN] = domain + event = self.event_cache.get(context_row) + described = describe_event(event) + if name := described.get(ATTR_NAME): + data[CONTEXT_NAME] = name + if message := described.get(ATTR_MESSAGE): + data[CONTEXT_MESSAGE] = message + if not (attr_entity_id := described.get(ATTR_ENTITY_ID)): + return + data[CONTEXT_ENTITY_ID] = attr_entity_id + if self.include_entity_name: + data[CONTEXT_ENTITY_ID_NAME] = self.entity_name_cache.get(attr_entity_id) + + +def _rows_match(row: Row | EventAsRow, other_row: Row | EventAsRow) -> bool: + """Check of rows match by using the same method as Events __hash__.""" + if ( + (state_id := row.state_id) is not None + and state_id == other_row.state_id + or (event_id := row.event_id) is not None + and event_id == other_row.event_id + ): + return True + return False + + +def _row_event_data_extract(row: Row | EventAsRow, extractor: re.Pattern) -> str | None: + """Extract from event_data row.""" + result = extractor.search(row.shared_data or row.event_data or "") + return result.group(1) if result else None + + +def _row_time_fired_isoformat(row: Row | EventAsRow) -> str: + """Convert the row timed_fired to isoformat.""" + return process_timestamp_to_utc_isoformat(row.time_fired or dt_util.utcnow()) + + +def _row_time_fired_timestamp(row: Row | EventAsRow) -> float: + """Convert the row timed_fired to timestamp.""" + return process_datetime_to_timestamp(row.time_fired or dt_util.utcnow()) + + +class EntityNameCache: + """A cache to lookup the name for an entity. + + This class should not be used to lookup attributes + that are expected to change state. + """ + + def __init__(self, hass: HomeAssistant) -> None: + """Init the cache.""" + self._hass = hass + self._names: dict[str, str] = {} + + def get(self, entity_id: str) -> str: + """Lookup an the friendly name.""" + if entity_id in self._names: + return self._names[entity_id] + if (current_state := self._hass.states.get(entity_id)) and ( + friendly_name := current_state.attributes.get(ATTR_FRIENDLY_NAME) + ): + self._names[entity_id] = friendly_name + else: + return split_entity_id(entity_id)[1].replace("_", " ") + + return self._names[entity_id] + + +class EventCache: + """Cache LazyEventPartialState by row.""" + + def __init__(self, event_data_cache: dict[str, dict[str, Any]]) -> None: + """Init the cache.""" + self._event_data_cache = event_data_cache + self.event_cache: dict[Row | EventAsRow, LazyEventPartialState] = {} + + def get(self, row: EventAsRow | Row) -> LazyEventPartialState: + """Get the event from the row.""" + if isinstance(row, EventAsRow): + return LazyEventPartialState(row, self._event_data_cache) + if event := self.event_cache.get(row): + return event + self.event_cache[row] = lazy_event = LazyEventPartialState( + row, self._event_data_cache + ) + return lazy_event + + def clear(self) -> None: + """Clear the event cache.""" + self._event_data_cache = {} + self.event_cache = {} diff --git a/homeassistant/components/logbook/rest_api.py b/homeassistant/components/logbook/rest_api.py new file mode 100644 index 00000000000..a1a7db3ed2c --- /dev/null +++ b/homeassistant/components/logbook/rest_api.py @@ -0,0 +1,120 @@ +"""Event parser and human readable log generator.""" +from __future__ import annotations + +from datetime import timedelta +from http import HTTPStatus +from typing import Any, cast + +from aiohttp import web +import voluptuous as vol + +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.recorder import get_instance +from homeassistant.components.recorder.filters import Filters +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import InvalidEntityFormatError +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entityfilter import EntityFilter +from homeassistant.helpers.typing import ConfigType +import homeassistant.util.dt as dt_util + +from .helpers import async_determine_event_types +from .processor import EventProcessor + + +@callback +def async_setup( + hass: HomeAssistant, + conf: ConfigType, + filters: Filters | None, + entities_filter: EntityFilter | None, +) -> None: + """Set up the logbook rest API.""" + hass.http.register_view(LogbookView(conf, filters, entities_filter)) + + +class LogbookView(HomeAssistantView): + """Handle logbook view requests.""" + + url = "/api/logbook" + name = "api:logbook" + extra_urls = ["/api/logbook/{datetime}"] + + def __init__( + self, + config: dict[str, Any], + filters: Filters | None, + entities_filter: EntityFilter | None, + ) -> None: + """Initialize the logbook view.""" + self.config = config + self.filters = filters + self.entities_filter = entities_filter + + async def get( + self, request: web.Request, datetime: str | None = None + ) -> web.Response: + """Retrieve logbook entries.""" + if datetime: + if (datetime_dt := dt_util.parse_datetime(datetime)) is None: + return self.json_message("Invalid datetime", HTTPStatus.BAD_REQUEST) + else: + datetime_dt = dt_util.start_of_local_day() + + if (period_str := request.query.get("period")) is None: + period: int = 1 + else: + period = int(period_str) + + if entity_ids_str := request.query.get("entity"): + try: + entity_ids = cv.entity_ids(entity_ids_str) + except vol.Invalid: + raise InvalidEntityFormatError( + f"Invalid entity id(s) encountered: {entity_ids_str}. " + "Format should be ." + ) from vol.Invalid + else: + entity_ids = None + + if (end_time_str := request.query.get("end_time")) is None: + start_day = dt_util.as_utc(datetime_dt) - timedelta(days=period - 1) + end_day = start_day + timedelta(days=period) + else: + start_day = datetime_dt + if (end_day_dt := dt_util.parse_datetime(end_time_str)) is None: + return self.json_message("Invalid end_time", HTTPStatus.BAD_REQUEST) + end_day = end_day_dt + + hass = request.app["hass"] + + context_id = request.query.get("context_id") + + if entity_ids and context_id: + return self.json_message( + "Can't combine entity with context_id", HTTPStatus.BAD_REQUEST + ) + + event_types = async_determine_event_types(hass, entity_ids, None) + event_processor = EventProcessor( + hass, + event_types, + entity_ids, + None, + context_id, + timestamp=False, + include_entity_name=True, + ) + + def json_events() -> web.Response: + """Fetch events and generate JSON.""" + return self.json( + event_processor.get_events( + start_day, + end_day, + ) + ) + + return cast( + web.Response, await get_instance(hass).async_add_executor_job(json_events) + ) diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py new file mode 100644 index 00000000000..efb15efe97b --- /dev/null +++ b/homeassistant/components/logbook/websocket_api.py @@ -0,0 +1,319 @@ +"""Event parser and human readable log generator.""" +from __future__ import annotations + +import asyncio +from collections.abc import Callable +from datetime import datetime as dt, timedelta +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.components.recorder import get_instance +from homeassistant.components.websocket_api import messages +from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.components.websocket_api.const import JSON_DUMP +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +import homeassistant.util.dt as dt_util + +from .helpers import ( + async_determine_event_types, + async_filter_entities, + async_subscribe_events, +) +from .models import async_event_to_row +from .processor import EventProcessor + +MAX_PENDING_LOGBOOK_EVENTS = 2048 +EVENT_COALESCE_TIME = 0.5 +MAX_RECORDER_WAIT = 10 + +_LOGGER = logging.getLogger(__name__) + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Set up the logbook websocket API.""" + websocket_api.async_register_command(hass, ws_get_events) + websocket_api.async_register_command(hass, ws_event_stream) + + +async def _async_get_ws_formatted_events( + hass: HomeAssistant, + msg_id: int, + start_time: dt, + end_time: dt, + formatter: Callable[[int, Any], dict[str, Any]], + event_processor: EventProcessor, +) -> tuple[str, dt | None]: + """Async wrapper around _ws_formatted_get_events.""" + return await get_instance(hass).async_add_executor_job( + _ws_formatted_get_events, + msg_id, + start_time, + end_time, + formatter, + event_processor, + ) + + +def _ws_formatted_get_events( + msg_id: int, + start_day: dt, + end_day: dt, + formatter: Callable[[int, Any], dict[str, Any]], + event_processor: EventProcessor, +) -> tuple[str, dt | None]: + """Fetch events and convert them to json in the executor.""" + events = event_processor.get_events(start_day, end_day) + last_time = None + if events: + last_time = dt_util.utc_from_timestamp(events[-1]["when"]) + result = formatter(msg_id, events) + return JSON_DUMP(result), last_time + + +async def _async_events_consumer( + setup_complete_future: asyncio.Future[dt], + connection: ActiveConnection, + msg_id: int, + stream_queue: asyncio.Queue[Event], + event_processor: EventProcessor, +) -> None: + """Stream events from the queue.""" + subscriptions_setup_complete_time = await setup_complete_future + event_processor.switch_to_live() + + while True: + events: list[Event] = [await stream_queue.get()] + # If the event is older than the last db + # event we already sent it so we skip it. + if events[0].time_fired <= subscriptions_setup_complete_time: + continue + # We sleep for the EVENT_COALESCE_TIME so + # we can group events together to minimize + # the number of websocket messages when the + # system is overloaded with an event storm + await asyncio.sleep(EVENT_COALESCE_TIME) + while not stream_queue.empty(): + events.append(stream_queue.get_nowait()) + + if logbook_events := event_processor.humanify( + async_event_to_row(e) for e in events + ): + connection.send_message( + JSON_DUMP( + messages.event_message( + msg_id, + logbook_events, + ) + ) + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "logbook/event_stream", + vol.Required("start_time"): str, + vol.Optional("entity_ids"): [str], + vol.Optional("device_ids"): [str], + } +) +@websocket_api.async_response +async def ws_event_stream( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Handle logbook stream events websocket command.""" + start_time_str = msg["start_time"] + utc_now = dt_util.utcnow() + + if start_time := dt_util.parse_datetime(start_time_str): + start_time = dt_util.as_utc(start_time) + + if not start_time or start_time > utc_now: + connection.send_error(msg["id"], "invalid_start_time", "Invalid start_time") + return + + device_ids = msg.get("device_ids") + entity_ids = msg.get("entity_ids") + if entity_ids: + entity_ids = async_filter_entities(hass, entity_ids) + event_types = async_determine_event_types(hass, entity_ids, device_ids) + + event_processor = EventProcessor( + hass, + event_types, + entity_ids, + device_ids, + None, + timestamp=True, + include_entity_name=False, + ) + + stream_queue: asyncio.Queue[Event] = asyncio.Queue(MAX_PENDING_LOGBOOK_EVENTS) + subscriptions: list[CALLBACK_TYPE] = [] + setup_complete_future: asyncio.Future[dt] = asyncio.Future() + task = asyncio.create_task( + _async_events_consumer( + setup_complete_future, + connection, + msg["id"], + stream_queue, + event_processor, + ) + ) + + def _unsub() -> None: + """Unsubscribe from all events.""" + for subscription in subscriptions: + subscription() + subscriptions.clear() + if task: + task.cancel() + + @callback + def _queue_or_cancel(event: Event) -> None: + """Queue an event to be processed or cancel.""" + try: + stream_queue.put_nowait(event) + except asyncio.QueueFull: + _LOGGER.debug( + "Client exceeded max pending messages of %s", + MAX_PENDING_LOGBOOK_EVENTS, + ) + _unsub() + + async_subscribe_events( + hass, subscriptions, _queue_or_cancel, event_types, entity_ids, device_ids + ) + subscriptions_setup_complete_time = dt_util.utcnow() + connection.subscriptions[msg["id"]] = _unsub + connection.send_result(msg["id"]) + + # Fetch everything from history + message, last_event_time = await _async_get_ws_formatted_events( + hass, + msg["id"], + start_time, + subscriptions_setup_complete_time, + messages.event_message, + event_processor, + ) + # If there is no last_time there are no historical + # results, but we still send an empty message so + # consumers of the api know their request was + # answered but there were no results + connection.send_message(message) + try: + await asyncio.wait_for( + get_instance(hass).async_block_till_done(), MAX_RECORDER_WAIT + ) + except asyncio.TimeoutError: + _LOGGER.debug( + "Recorder is behind more than %s seconds, starting live stream; Some results may be missing" + ) + + if setup_complete_future.cancelled(): + # Unsubscribe happened while waiting for recorder + return + + # + # Fetch any events from the database that have + # not been committed since the original fetch + # so we can switch over to using the subscriptions + # + # We only want events that happened after the last event + # we had from the last database query or the maximum + # time we allow the recorder to be behind + # + max_recorder_behind = subscriptions_setup_complete_time - timedelta( + seconds=MAX_RECORDER_WAIT + ) + second_fetch_start_time = max( + last_event_time or max_recorder_behind, max_recorder_behind + ) + message, final_cutoff_time = await _async_get_ws_formatted_events( + hass, + msg["id"], + second_fetch_start_time, + subscriptions_setup_complete_time, + messages.event_message, + event_processor, + ) + if final_cutoff_time: # Only sends results if we have them + connection.send_message(message) + + if not setup_complete_future.cancelled(): + # Unsubscribe happened while waiting for formatted events + setup_complete_future.set_result(subscriptions_setup_complete_time) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "logbook/get_events", + vol.Required("start_time"): str, + vol.Optional("end_time"): str, + vol.Optional("entity_ids"): [str], + vol.Optional("device_ids"): [str], + vol.Optional("context_id"): str, + } +) +@websocket_api.async_response +async def ws_get_events( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Handle logbook get events websocket command.""" + start_time_str = msg["start_time"] + end_time_str = msg.get("end_time") + utc_now = dt_util.utcnow() + + if start_time := dt_util.parse_datetime(start_time_str): + start_time = dt_util.as_utc(start_time) + else: + connection.send_error(msg["id"], "invalid_start_time", "Invalid start_time") + return + + if not end_time_str: + end_time = utc_now + elif parsed_end_time := dt_util.parse_datetime(end_time_str): + end_time = dt_util.as_utc(parsed_end_time) + else: + connection.send_error(msg["id"], "invalid_end_time", "Invalid end_time") + return + + if start_time > utc_now: + connection.send_result(msg["id"], []) + return + + device_ids = msg.get("device_ids") + entity_ids = msg.get("entity_ids") + context_id = msg.get("context_id") + if entity_ids: + entity_ids = async_filter_entities(hass, entity_ids) + if not entity_ids and not device_ids: + # Everything has been filtered away + connection.send_result(msg["id"], []) + return + + event_types = async_determine_event_types(hass, entity_ids, device_ids) + + event_processor = EventProcessor( + hass, + event_types, + entity_ids, + device_ids, + context_id, + timestamp=True, + include_entity_name=False, + ) + + message, _ = await _async_get_ws_formatted_events( + hass, + msg["id"], + start_time, + end_time, + messages.result_message, + event_processor, + ) + connection.send_message(message) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 5e6fafdfa61..716e3a19d09 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -75,6 +75,7 @@ from .tasks import ( RecorderTask, StatisticsTask, StopTask, + SynchronizeTask, UpdateStatisticsMetadataTask, WaitTask, ) @@ -928,6 +929,12 @@ class Recorder(threading.Thread): if self._async_event_filter(event): self.queue_task(EventTask(event)) + async def async_block_till_done(self) -> None: + """Async version of block_till_done.""" + event = asyncio.Event() + self.queue_task(SynchronizeTask(event)) + await event.wait() + def block_till_done(self) -> None: """Block till all events processed. diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index e12526b316a..07855d27dff 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -248,3 +248,17 @@ class AddRecorderPlatformTask(RecorderTask): platforms[domain] = platform if hasattr(self.platform, "exclude_attributes"): hass.data[EXCLUDE_ATTRIBUTES][domain] = platform.exclude_attributes(hass) + + +@dataclass +class SynchronizeTask(RecorderTask): + """Ensure all pending data has been committed.""" + + # commit_before is the default + event: asyncio.Event + + def run(self, instance: Recorder) -> None: + """Handle the task.""" + # Does not use a tracked task to avoid + # blocking shutdown if the recorder is broken + instance.hass.loop.call_soon_threadsafe(self.event.set) diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index 1c09bc1d567..f546ba5eec6 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -61,7 +61,7 @@ def error_message(iden: int | None, code: str, message: str) -> dict[str, Any]: } -def event_message(iden: JSON_TYPE, event: Any) -> dict[str, Any]: +def event_message(iden: JSON_TYPE | int, event: Any) -> dict[str, Any]: """Return an event message.""" return {"id": iden, "type": "event", "event": event} diff --git a/homeassistant/core.py b/homeassistant/core.py index 7fcf07d9c66..2753b801347 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -716,13 +716,14 @@ class HomeAssistant: self._stopped.set() -@attr.s(slots=True, frozen=True) +@attr.s(slots=True, frozen=False) class Context: """The context that triggered something.""" user_id: str | None = attr.ib(default=None) parent_id: str | None = attr.ib(default=None) id: str = attr.ib(factory=ulid_util.ulid) + origin_event: Event | None = attr.ib(default=None, eq=False) def as_dict(self) -> dict[str, str | None]: """Return a dictionary representation of the context.""" @@ -866,6 +867,8 @@ class EventBus: listeners = match_all_listeners + listeners event = Event(event_type, event_data, origin, time_fired, context) + if not event.context.origin_event: + event.context.origin_event = event _LOGGER.debug("Bus:Handling %s", event) diff --git a/tests/components/logbook/common.py b/tests/components/logbook/common.py index 9d6f35eb702..b88c3854967 100644 --- a/tests/components/logbook/common.py +++ b/tests/components/logbook/common.py @@ -5,6 +5,7 @@ import json from typing import Any from homeassistant.components import logbook +from homeassistant.components.logbook import processor from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat from homeassistant.core import Context from homeassistant.helpers import entity_registry as er @@ -50,16 +51,26 @@ class MockRow: def mock_humanify(hass_, rows): """Wrap humanify with mocked logbook objects.""" - entity_name_cache = logbook.EntityNameCache(hass_) + entity_name_cache = processor.EntityNameCache(hass_) ent_reg = er.async_get(hass_) + event_cache = processor.EventCache({}) + context_lookup = processor.ContextLookup(hass_) external_events = hass_.data.get(logbook.DOMAIN, {}) + logbook_run = processor.LogbookRun( + context_lookup, + external_events, + event_cache, + entity_name_cache, + include_entity_name=True, + format_time=processor._row_time_fired_isoformat, + ) + context_augmenter = processor.ContextAugmenter(logbook_run) return list( - logbook._humanify( + processor._humanify( rows, None, ent_reg, - external_events, - entity_name_cache, - logbook._row_time_fired_isoformat, + logbook_run, + context_augmenter, ), ) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 3b18c594e6e..101fb74e690 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -14,6 +14,9 @@ import voluptuous as vol from homeassistant.components import logbook from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED +from homeassistant.components.logbook.models import LazyEventPartialState +from homeassistant.components.logbook.processor import EventProcessor +from homeassistant.components.logbook.queries.common import PSUEDO_EVENT_STATE_CHANGED from homeassistant.components.script import EVENT_SCRIPT_STARTED from homeassistant.components.sensor import SensorStateClass from homeassistant.const import ( @@ -95,15 +98,12 @@ async def test_service_call_create_logbook_entry(hass_): # Our service call will unblock when the event listeners have been # scheduled. This means that they may not have been processed yet. await async_wait_recording_done(hass_) - ent_reg = er.async_get(hass_) + event_processor = EventProcessor(hass_, (EVENT_LOGBOOK_ENTRY,)) events = list( - logbook._get_events( - hass_, + event_processor.get_events( dt_util.utcnow() - timedelta(hours=1), dt_util.utcnow() + timedelta(hours=1), - (EVENT_LOGBOOK_ENTRY,), - ent_reg, ) ) assert len(events) == 2 @@ -137,15 +137,11 @@ async def test_service_call_create_logbook_entry_invalid_entity_id(hass, recorde }, ) await async_wait_recording_done(hass) - ent_reg = er.async_get(hass) - + event_processor = EventProcessor(hass, (EVENT_LOGBOOK_ENTRY,)) events = list( - logbook._get_events( - hass, + event_processor.get_events( dt_util.utcnow() - timedelta(hours=1), dt_util.utcnow() + timedelta(hours=1), - (EVENT_LOGBOOK_ENTRY,), - ent_reg, ) ) assert len(events) == 1 @@ -335,7 +331,7 @@ def create_state_changed_event_from_old_new( ], ) - row.event_type = logbook.PSUEDO_EVENT_STATE_CHANGED + row.event_type = PSUEDO_EVENT_STATE_CHANGED row.event_data = "{}" row.shared_data = "{}" row.attributes = attributes_json @@ -353,7 +349,7 @@ def create_state_changed_event_from_old_new( row.context_parent_id = None row.old_state_id = old_state and 1 row.state_id = new_state and 1 - return logbook.LazyEventPartialState(row, {}) + return LazyEventPartialState(row, {}) async def test_logbook_view(hass, hass_client, recorder_mock): diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py new file mode 100644 index 00000000000..585732b66f1 --- /dev/null +++ b/tests/components/logbook/test_websocket_api.py @@ -0,0 +1,1166 @@ +"""The tests for the logbook component.""" +import asyncio +from collections.abc import Callable +from datetime import timedelta +from unittest.mock import ANY, patch + +import pytest + +from homeassistant import core +from homeassistant.components import logbook, recorder +from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED +from homeassistant.components.logbook import websocket_api +from homeassistant.components.script import EVENT_SCRIPT_STARTED +from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_NAME, + ATTR_UNIT_OF_MEASUREMENT, + EVENT_HOMEASSISTANT_START, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import Event, HomeAssistant, State +from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import MockConfigEntry, SetupRecorderInstanceT +from tests.components.recorder.common import ( + async_block_recorder, + async_recorder_block_till_done, + async_wait_recording_done, +) + + +@pytest.fixture() +def set_utc(hass): + """Set timezone to UTC.""" + hass.config.set_time_zone("UTC") + + +async def _async_mock_device_with_logbook_platform(hass): + """Mock an integration that provides a device that are described by the logbook.""" + entry = MockConfigEntry(domain="test", data={"first": True}, options=None) + entry.add_to_hass(hass) + dev_reg = device_registry.async_get(hass) + device = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + sw_version="sw-version", + name="device name", + manufacturer="manufacturer", + model="model", + suggested_area="Game Room", + ) + + class MockLogbookPlatform: + """Mock a logbook platform.""" + + @core.callback + def async_describe_events( + hass: HomeAssistant, + async_describe_event: Callable[ + [str, str, Callable[[Event], dict[str, str]]], None + ], + ) -> None: + """Describe logbook events.""" + + @core.callback + def async_describe_test_event(event: Event) -> dict[str, str]: + """Describe mock logbook event.""" + return { + "name": "device name", + "message": event.data.get("message", "is on fire"), + } + + async_describe_event("test", "mock_event", async_describe_test_event) + + await logbook._process_logbook_platform(hass, "test", MockLogbookPlatform) + return device + + +async def test_get_events(hass, hass_ws_client, recorder_mock): + """Test logbook get_events.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook") + ] + ) + await async_recorder_block_till_done(hass) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + + hass.states.async_set("light.kitchen", STATE_OFF) + await hass.async_block_till_done() + hass.states.async_set("light.kitchen", STATE_ON, {"brightness": 100}) + await hass.async_block_till_done() + hass.states.async_set("light.kitchen", STATE_ON, {"brightness": 200}) + await hass.async_block_till_done() + hass.states.async_set("light.kitchen", STATE_ON, {"brightness": 300}) + await hass.async_block_till_done() + hass.states.async_set("light.kitchen", STATE_ON, {"brightness": 400}) + await hass.async_block_till_done() + context = core.Context( + id="ac5bd62de45711eaaeb351041eec8dd9", + user_id="b400facee45711eaa9308bfd3d19e474", + ) + + hass.states.async_set("light.kitchen", STATE_OFF, context=context) + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "logbook/get_events", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "entity_ids": ["light.kitchen"], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [] + + await client.send_json( + { + "id": 2, + "type": "logbook/get_events", + "start_time": now.isoformat(), + "entity_ids": ["sensor.test"], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 2 + assert response["result"] == [] + + await client.send_json( + { + "id": 3, + "type": "logbook/get_events", + "start_time": now.isoformat(), + "entity_ids": ["light.kitchen"], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 3 + + results = response["result"] + assert results[0]["entity_id"] == "light.kitchen" + assert results[0]["state"] == "on" + assert results[1]["entity_id"] == "light.kitchen" + assert results[1]["state"] == "off" + + await client.send_json( + { + "id": 4, + "type": "logbook/get_events", + "start_time": now.isoformat(), + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 4 + + results = response["result"] + assert len(results) == 3 + assert results[0]["message"] == "started" + assert results[1]["entity_id"] == "light.kitchen" + assert results[1]["state"] == "on" + assert isinstance(results[1]["when"], float) + assert results[2]["entity_id"] == "light.kitchen" + assert results[2]["state"] == "off" + assert isinstance(results[2]["when"], float) + + await client.send_json( + { + "id": 5, + "type": "logbook/get_events", + "start_time": now.isoformat(), + "context_id": "ac5bd62de45711eaaeb351041eec8dd9", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 5 + + results = response["result"] + assert len(results) == 1 + assert results[0]["entity_id"] == "light.kitchen" + assert results[0]["state"] == "off" + assert isinstance(results[0]["when"], float) + + +async def test_get_events_entities_filtered_away(hass, hass_ws_client, recorder_mock): + """Test logbook get_events all entities filtered away.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook") + ] + ) + await async_recorder_block_till_done(hass) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + + hass.states.async_set("light.kitchen", STATE_ON) + await hass.async_block_till_done() + hass.states.async_set( + "light.filtered", STATE_ON, {"brightness": 100, ATTR_UNIT_OF_MEASUREMENT: "any"} + ) + await hass.async_block_till_done() + hass.states.async_set("light.kitchen", STATE_OFF, {"brightness": 200}) + await hass.async_block_till_done() + hass.states.async_set( + "light.filtered", + STATE_OFF, + {"brightness": 300, ATTR_UNIT_OF_MEASUREMENT: "any"}, + ) + + await async_wait_recording_done(hass) + client = await hass_ws_client() + + await client.send_json( + { + "id": 1, + "type": "logbook/get_events", + "start_time": now.isoformat(), + "entity_ids": ["light.kitchen"], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + + results = response["result"] + assert results[0]["entity_id"] == "light.kitchen" + assert results[0]["state"] == "off" + + await client.send_json( + { + "id": 2, + "type": "logbook/get_events", + "start_time": now.isoformat(), + "entity_ids": ["light.filtered"], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 2 + + results = response["result"] + assert len(results) == 0 + + +async def test_get_events_future_start_time(hass, hass_ws_client, recorder_mock): + """Test get_events with a future start time.""" + await async_setup_component(hass, "logbook", {}) + await async_recorder_block_till_done(hass) + future = dt_util.utcnow() + timedelta(hours=10) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "logbook/get_events", + "start_time": future.isoformat(), + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + + results = response["result"] + assert isinstance(results, list) + assert len(results) == 0 + + +async def test_get_events_bad_start_time(hass, hass_ws_client, recorder_mock): + """Test get_events bad start time.""" + await async_setup_component(hass, "logbook", {}) + await async_recorder_block_till_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "logbook/get_events", + "start_time": "cats", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_start_time" + + +async def test_get_events_bad_end_time(hass, hass_ws_client, recorder_mock): + """Test get_events bad end time.""" + now = dt_util.utcnow() + await async_setup_component(hass, "logbook", {}) + await async_recorder_block_till_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "logbook/get_events", + "start_time": now.isoformat(), + "end_time": "dogs", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_end_time" + + +async def test_get_events_invalid_filters(hass, hass_ws_client, recorder_mock): + """Test get_events invalid filters.""" + await async_setup_component(hass, "logbook", {}) + await async_recorder_block_till_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "logbook/get_events", + "entity_ids": [], + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_format" + await client.send_json( + { + "id": 2, + "type": "logbook/get_events", + "device_ids": [], + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_format" + + +async def test_get_events_with_device_ids(hass, hass_ws_client, recorder_mock): + """Test logbook get_events for device ids.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook") + ] + ) + + device = await _async_mock_device_with_logbook_platform(hass) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire("mock_event", {"device_id": device.id}) + + hass.states.async_set("light.kitchen", STATE_OFF) + await hass.async_block_till_done() + hass.states.async_set("light.kitchen", STATE_ON, {"brightness": 100}) + await hass.async_block_till_done() + hass.states.async_set("light.kitchen", STATE_ON, {"brightness": 200}) + await hass.async_block_till_done() + hass.states.async_set("light.kitchen", STATE_ON, {"brightness": 300}) + await hass.async_block_till_done() + hass.states.async_set("light.kitchen", STATE_ON, {"brightness": 400}) + await hass.async_block_till_done() + context = core.Context( + id="ac5bd62de45711eaaeb351041eec8dd9", + user_id="b400facee45711eaa9308bfd3d19e474", + ) + + hass.states.async_set("light.kitchen", STATE_OFF, context=context) + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + client = await hass_ws_client() + + await client.send_json( + { + "id": 1, + "type": "logbook/get_events", + "start_time": now.isoformat(), + "device_ids": [device.id], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + + results = response["result"] + assert len(results) == 1 + assert results[0]["name"] == "device name" + assert results[0]["message"] == "is on fire" + assert isinstance(results[0]["when"], float) + + await client.send_json( + { + "id": 2, + "type": "logbook/get_events", + "start_time": now.isoformat(), + "entity_ids": ["light.kitchen"], + "device_ids": [device.id], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 2 + + results = response["result"] + assert results[0]["domain"] == "test" + assert results[0]["message"] == "is on fire" + assert results[0]["name"] == "device name" + assert results[1]["entity_id"] == "light.kitchen" + assert results[1]["state"] == "on" + assert results[2]["entity_id"] == "light.kitchen" + assert results[2]["state"] == "off" + + await client.send_json( + { + "id": 3, + "type": "logbook/get_events", + "start_time": now.isoformat(), + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 3 + + results = response["result"] + assert len(results) == 4 + assert results[0]["message"] == "started" + assert results[1]["name"] == "device name" + assert results[1]["message"] == "is on fire" + assert isinstance(results[1]["when"], float) + assert results[2]["entity_id"] == "light.kitchen" + assert results[2]["state"] == "on" + assert isinstance(results[2]["when"], float) + assert results[3]["entity_id"] == "light.kitchen" + assert results[3]["state"] == "off" + assert isinstance(results[3]["when"], float) + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_unsubscribe_logbook_stream( + hass, recorder_mock, hass_ws_client +): + """Test subscribe/unsubscribe logbook stream.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + + await hass.async_block_till_done() + init_count = sum(hass.bus.async_listeners().values()) + + hass.states.async_set("binary_sensor.is_light", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + state: State = hass.states.get("binary_sensor.is_light") + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + {"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()} + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == [ + { + "entity_id": "binary_sensor.is_light", + "state": "off", + "when": state.last_updated.timestamp(), + } + ] + + hass.states.async_set("light.alpha", "on") + hass.states.async_set("light.alpha", "off") + alpha_off_state: State = hass.states.get("light.alpha") + hass.states.async_set("light.zulu", "on", {"color": "blue"}) + hass.states.async_set("light.zulu", "off", {"effect": "help"}) + zulu_off_state: State = hass.states.get("light.zulu") + hass.states.async_set( + "light.zulu", "on", {"effect": "help", "color": ["blue", "green"]} + ) + zulu_on_state: State = hass.states.get("light.zulu") + await hass.async_block_till_done() + + hass.states.async_remove("light.zulu") + await hass.async_block_till_done() + + hass.states.async_set("light.zulu", "on", {"effect": "help", "color": "blue"}) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == [ + { + "entity_id": "light.alpha", + "state": "off", + "when": alpha_off_state.last_updated.timestamp(), + }, + { + "entity_id": "light.zulu", + "state": "off", + "when": zulu_off_state.last_updated.timestamp(), + }, + { + "entity_id": "light.zulu", + "state": "on", + "when": zulu_on_state.last_updated.timestamp(), + }, + ] + + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation", ATTR_ENTITY_ID: "automation.mock_automation"}, + ) + hass.bus.async_fire( + EVENT_SCRIPT_STARTED, + {ATTR_NAME: "Mock script", ATTR_ENTITY_ID: "script.mock_script"}, + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == [ + { + "context_id": ANY, + "domain": "automation", + "entity_id": "automation.mock_automation", + "message": "triggered", + "name": "Mock automation", + "source": None, + "when": ANY, + }, + { + "context_id": ANY, + "domain": "script", + "entity_id": "script.mock_script", + "message": "started", + "name": "Mock script", + "when": ANY, + }, + { + "domain": "homeassistant", + "icon": "mdi:home-assistant", + "message": "started", + "name": "Home Assistant", + "when": ANY, + }, + ] + + context = core.Context( + id="ac5bd62de45711eaaeb351041eec8dd9", + user_id="b400facee45711eaa9308bfd3d19e474", + ) + automation_entity_id_test = "automation.alarm" + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation", ATTR_ENTITY_ID: automation_entity_id_test}, + context=context, + ) + hass.bus.async_fire( + EVENT_SCRIPT_STARTED, + {ATTR_NAME: "Mock script", ATTR_ENTITY_ID: "script.mock_script"}, + context=context, + ) + hass.states.async_set( + automation_entity_id_test, + STATE_ON, + {ATTR_FRIENDLY_NAME: "Alarm Automation"}, + context=context, + ) + entity_id_test = "alarm_control_panel.area_001" + hass.states.async_set(entity_id_test, STATE_OFF, context=context) + hass.states.async_set(entity_id_test, STATE_ON, context=context) + entity_id_second = "alarm_control_panel.area_002" + hass.states.async_set(entity_id_second, STATE_OFF, context=context) + hass.states.async_set(entity_id_second, STATE_ON, context=context) + + await hass.async_block_till_done() + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == [ + { + "context_id": "ac5bd62de45711eaaeb351041eec8dd9", + "context_user_id": "b400facee45711eaa9308bfd3d19e474", + "domain": "automation", + "entity_id": "automation.alarm", + "message": "triggered", + "name": "Mock automation", + "source": None, + "when": ANY, + }, + { + "context_domain": "automation", + "context_entity_id": "automation.alarm", + "context_event_type": "automation_triggered", + "context_id": "ac5bd62de45711eaaeb351041eec8dd9", + "context_message": "triggered", + "context_name": "Mock automation", + "context_user_id": "b400facee45711eaa9308bfd3d19e474", + "domain": "script", + "entity_id": "script.mock_script", + "message": "started", + "name": "Mock script", + "when": ANY, + }, + { + "context_domain": "automation", + "context_entity_id": "automation.alarm", + "context_event_type": "automation_triggered", + "context_message": "triggered", + "context_name": "Mock automation", + "context_user_id": "b400facee45711eaa9308bfd3d19e474", + "entity_id": "alarm_control_panel.area_001", + "state": "on", + "when": ANY, + }, + { + "context_domain": "automation", + "context_entity_id": "automation.alarm", + "context_event_type": "automation_triggered", + "context_message": "triggered", + "context_name": "Mock automation", + "context_user_id": "b400facee45711eaa9308bfd3d19e474", + "entity_id": "alarm_control_panel.area_002", + "state": "on", + "when": ANY, + }, + ] + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation 2", ATTR_ENTITY_ID: automation_entity_id_test}, + context=context, + ) + + await hass.async_block_till_done() + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == [ + { + "context_domain": "automation", + "context_entity_id": "automation.alarm", + "context_event_type": "automation_triggered", + "context_id": "ac5bd62de45711eaaeb351041eec8dd9", + "context_message": "triggered", + "context_name": "Mock automation", + "context_user_id": "b400facee45711eaa9308bfd3d19e474", + "domain": "automation", + "entity_id": "automation.alarm", + "message": "triggered", + "name": "Mock automation 2", + "source": None, + "when": ANY, + } + ] + + await async_wait_recording_done(hass) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation 3", ATTR_ENTITY_ID: automation_entity_id_test}, + context=context, + ) + + await hass.async_block_till_done() + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == [ + { + "context_domain": "automation", + "context_entity_id": "automation.alarm", + "context_event_type": "automation_triggered", + "context_id": "ac5bd62de45711eaaeb351041eec8dd9", + "context_message": "triggered", + "context_name": "Mock automation", + "context_user_id": "b400facee45711eaa9308bfd3d19e474", + "domain": "automation", + "entity_id": "automation.alarm", + "message": "triggered", + "name": "Mock automation 3", + "source": None, + "when": ANY, + } + ] + + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await websocket_client.receive_json() + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_unsubscribe_logbook_stream_entities( + hass, recorder_mock, hass_ws_client +): + """Test subscribe/unsubscribe logbook stream with specific entities.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + + await hass.async_block_till_done() + init_count = sum(hass.bus.async_listeners().values()) + hass.states.async_set("light.small", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + state: State = hass.states.get("binary_sensor.is_light") + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "entity_ids": ["light.small", "binary_sensor.is_light"], + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == [ + { + "entity_id": "binary_sensor.is_light", + "state": "off", + "when": state.last_updated.timestamp(), + } + ] + + hass.states.async_set("light.alpha", STATE_ON) + hass.states.async_set("light.alpha", STATE_OFF) + hass.states.async_set("light.small", STATE_OFF, {"effect": "help", "color": "blue"}) + + await hass.async_block_till_done() + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == [ + { + "entity_id": "light.small", + "state": "off", + "when": ANY, + }, + ] + + hass.states.async_remove("light.alpha") + hass.states.async_remove("light.small") + await hass.async_block_till_done() + + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await websocket_client.receive_json() + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_unsubscribe_logbook_stream_device( + hass, recorder_mock, hass_ws_client +): + """Test subscribe/unsubscribe logbook stream with a device.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + device = await _async_mock_device_with_logbook_platform(hass) + + await hass.async_block_till_done() + init_count = sum(hass.bus.async_listeners().values()) + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "device_ids": [device.id], + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # There are no answers to our initial query + # so we get an empty reply. This is to ensure + # consumers of the api know there are no results + # and its not a failure case. This is useful + # in the frontend so we can tell the user there + # are no results vs waiting for them to appear + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == [] + + hass.bus.async_fire("mock_event", {"device_id": device.id}) + await hass.async_block_till_done() + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == [ + {"domain": "test", "message": "is on fire", "name": "device name", "when": ANY} + ] + + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await websocket_client.receive_json() + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + +async def test_event_stream_bad_start_time(hass, hass_ws_client, recorder_mock): + """Test event_stream bad start time.""" + await async_setup_component(hass, "logbook", {}) + await async_recorder_block_till_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "logbook/event_stream", + "start_time": "cats", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_start_time" + + +async def test_live_stream_with_one_second_commit_interval( + hass: HomeAssistant, + async_setup_recorder_instance: SetupRecorderInstanceT, + hass_ws_client, +): + """Test the recorder with a 1s commit interval.""" + config = {recorder.CONF_COMMIT_INTERVAL: 0.5} + await async_setup_recorder_instance(hass, config) + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + device = await _async_mock_device_with_logbook_platform(hass) + + await hass.async_block_till_done() + init_count = sum(hass.bus.async_listeners().values()) + + hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "1"}) + + await async_wait_recording_done(hass) + + hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "2"}) + + await hass.async_block_till_done() + + hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "3"}) + + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "device_ids": [device.id], + } + ) + hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "4"}) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "5"}) + + recieved_rows = [] + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + recieved_rows.extend(msg["event"]) + + hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "6"}) + + await hass.async_block_till_done() + + hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "7"}) + + while not len(recieved_rows) == 7: + msg = await asyncio.wait_for(websocket_client.receive_json(), 2.5) + assert msg["id"] == 7 + assert msg["type"] == "event" + recieved_rows.extend(msg["event"]) + + # Make sure we get rows back in order + assert recieved_rows == [ + {"domain": "test", "message": "1", "name": "device name", "when": ANY}, + {"domain": "test", "message": "2", "name": "device name", "when": ANY}, + {"domain": "test", "message": "3", "name": "device name", "when": ANY}, + {"domain": "test", "message": "4", "name": "device name", "when": ANY}, + {"domain": "test", "message": "5", "name": "device name", "when": ANY}, + {"domain": "test", "message": "6", "name": "device name", "when": ANY}, + {"domain": "test", "message": "7", "name": "device name", "when": ANY}, + ] + + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await websocket_client.receive_json() + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_disconnected(hass, recorder_mock, hass_ws_client): + """Test subscribe/unsubscribe logbook stream gets disconnected.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + await async_wait_recording_done(hass) + + init_count = sum(hass.bus.async_listeners().values()) + hass.states.async_set("light.small", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + state: State = hass.states.get("binary_sensor.is_light") + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "entity_ids": ["light.small", "binary_sensor.is_light"], + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == [ + { + "entity_id": "binary_sensor.is_light", + "state": "off", + "when": state.last_updated.timestamp(), + } + ] + + await websocket_client.close() + await hass.async_block_till_done() + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_stream_consumer_stop_processing(hass, recorder_mock, hass_ws_client): + """Test we unsubscribe if the stream consumer fails or is canceled.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + await async_wait_recording_done(hass) + init_count = sum(hass.bus.async_listeners().values()) + hass.states.async_set("light.small", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + + after_ws_created_count = sum(hass.bus.async_listeners().values()) + + with patch.object(websocket_api, "MAX_PENDING_LOGBOOK_EVENTS", 5), patch.object( + websocket_api, "_async_events_consumer" + ): + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "entity_ids": ["light.small", "binary_sensor.is_light"], + } + ) + await async_wait_recording_done(hass) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + assert sum(hass.bus.async_listeners().values()) != init_count + for _ in range(5): + hass.states.async_set("binary_sensor.is_light", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + await async_wait_recording_done(hass) + + # Check our listener got unsubscribed because + # the queue got full and the overload safety tripped + assert sum(hass.bus.async_listeners().values()) == after_ws_created_count + await websocket_client.close() + assert sum(hass.bus.async_listeners().values()) == init_count + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +@patch("homeassistant.components.logbook.websocket_api.MAX_RECORDER_WAIT", 0.15) +async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplog): + """Test we still start live streaming if the recorder is far behind.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + await async_wait_recording_done(hass) + device = await _async_mock_device_with_logbook_platform(hass) + await async_wait_recording_done(hass) + + # Block the recorder queue + await async_block_recorder(hass, 0.3) + await hass.async_block_till_done() + + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "device_ids": [device.id], + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # There are no answers to our initial query + # so we get an empty reply. This is to ensure + # consumers of the api know there are no results + # and its not a failure case. This is useful + # in the frontend so we can tell the user there + # are no results vs waiting for them to appear + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == [] + + hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "1"}) + await hass.async_block_till_done() + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == [ + {"domain": "test", "message": "1", "name": "device name", "when": ANY} + ] + + hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "2"}) + await hass.async_block_till_done() + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == [ + {"domain": "test", "message": "2", "name": "device name", "when": ANY} + ] + + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await websocket_client.receive_json() + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + assert "Recorder is behind" in caplog.text diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index b36547309cd..39cde4c2e7c 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -1,7 +1,10 @@ """Common test utils for working with recorder.""" from __future__ import annotations +import asyncio +from dataclasses import dataclass from datetime import datetime, timedelta +import time from typing import Any, cast from sqlalchemy import create_engine @@ -10,8 +13,9 @@ from sqlalchemy.orm.session import Session from homeassistant import core as ha from homeassistant.components import recorder from homeassistant.components.recorder import get_instance, statistics +from homeassistant.components.recorder.core import Recorder from homeassistant.components.recorder.models import RecorderRuns -from homeassistant.components.recorder.tasks import StatisticsTask +from homeassistant.components.recorder.tasks import RecorderTask, StatisticsTask from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -21,6 +25,31 @@ from tests.components.recorder import models_schema_0 DEFAULT_PURGE_TASKS = 3 +@dataclass +class BlockRecorderTask(RecorderTask): + """A task to block the recorder for testing only.""" + + event: asyncio.Event + seconds: float + + def run(self, instance: Recorder) -> None: + """Block the recorders event loop.""" + instance.hass.loop.call_soon_threadsafe(self.event.set) + time.sleep(self.seconds) + + +async def async_block_recorder(hass: HomeAssistant, seconds: float) -> None: + """Block the recorders event loop for testing. + + Returns as soon as the recorder has started the block. + + Does not wait for the block to finish. + """ + event = asyncio.Event() + get_instance(hass).queue_task(BlockRecorderTask(event, seconds)) + await event.wait() + + def do_adhoc_statistics(hass: HomeAssistant, **kwargs: Any) -> None: """Trigger an adhoc statistics run.""" if not (start := kwargs.get("start")): diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 49133dd67bd..41c4428ee5e 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -55,6 +55,7 @@ from homeassistant.setup import async_setup_component, setup_component from homeassistant.util import dt as dt_util from .common import ( + async_block_recorder, async_wait_recording_done, corrupt_db_file, run_information_with_session, @@ -1537,3 +1538,25 @@ def test_deduplication_state_attributes_inside_commit_interval(hass_recorder, ca first_attributes_id = states[0].attributes_id last_attributes_id = states[-1].attributes_id assert first_attributes_id == last_attributes_id + + +async def test_async_block_till_done(hass, async_setup_recorder_instance): + """Test we can block until recordering is done.""" + instance = await async_setup_recorder_instance(hass) + await async_wait_recording_done(hass) + + entity_id = "test.recorder" + attributes = {"test_attr": 5, "test_attr_10": "nice"} + + hass.states.async_set(entity_id, "on", attributes) + hass.states.async_set(entity_id, "off", attributes) + + def _fetch_states(): + with session_scope(hass=hass) as session: + return list(session.query(States).filter(States.entity_id == entity_id)) + + await async_block_recorder(hass, 0.1) + await instance.async_block_till_done() + states = await instance.async_add_executor_job(_fetch_states) + assert len(states) == 2 + await hass.async_block_till_done() diff --git a/tests/test_core.py b/tests/test_core.py index 1ac7002cb7b..ee1005a60b0 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1799,3 +1799,33 @@ async def test_state_firing_event_matches_context_id_ulid_time(hass): assert _ulid_timestamp(event.context.id) == int( events[0].time_fired.timestamp() * 1000 ) + + +async def test_event_context(hass): + """Test we can lookup the origin of a context from an event.""" + events = [] + + @ha.callback + def capture_events(event): + nonlocal events + events.append(event) + + cancel = hass.bus.async_listen("dummy_event", capture_events) + cancel2 = hass.bus.async_listen("dummy_event_2", capture_events) + + hass.bus.async_fire("dummy_event") + await hass.async_block_till_done() + + dummy_event: ha.Event = events[0] + + hass.bus.async_fire("dummy_event_2", context=dummy_event.context) + await hass.async_block_till_done() + context_id = dummy_event.context.id + + dummy_event2: ha.Event = events[1] + assert dummy_event2.context == dummy_event.context + assert dummy_event2.context.id == context_id + cancel() + cancel2() + + assert dummy_event2.context.origin_event == dummy_event From e6ffae8bd3570c61ce3b8d9f15a3d3b678537650 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 22 May 2022 14:29:11 -0700 Subject: [PATCH 0754/3516] Deprecate google calendar configuration.yaml (#72288) * Deprecate google calendar configuration.yaml * Remove unused translations * Enable strict type checking and address pr feedback * Move default hass.data init to `async_setup` --- .strict-typing | 1 + homeassistant/components/google/__init__.py | 88 ++++++++++++++----- homeassistant/components/google/api.py | 23 ++--- homeassistant/components/google/calendar.py | 30 +++++-- .../components/google/config_flow.py | 68 ++++++++++++-- homeassistant/components/google/strings.json | 9 ++ .../components/google/translations/en.json | 9 ++ mypy.ini | 11 +++ tests/components/google/test_config_flow.py | 73 ++++++++++++++- tests/components/google/test_init.py | 2 +- 10 files changed, 267 insertions(+), 47 deletions(-) diff --git a/.strict-typing b/.strict-typing index 563e6e9f91a..2b6295b9157 100644 --- a/.strict-typing +++ b/.strict-typing @@ -101,6 +101,7 @@ homeassistant.components.geo_location.* homeassistant.components.geocaching.* homeassistant.components.gios.* homeassistant.components.goalzero.* +homeassistant.components.google.* homeassistant.components.greeneye_monitor.* homeassistant.components.group.* homeassistant.components.guardian.* diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 73b7f8d8ae6..70732af1fd8 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -97,23 +97,27 @@ PLATFORMS = ["calendar"] CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - vol.Optional(CONF_TRACK_NEW, default=True): cv.boolean, - vol.Optional(CONF_CALENDAR_ACCESS, default="read_write"): cv.enum( - FeatureAccess - ), - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + vol.Optional(CONF_TRACK_NEW, default=True): cv.boolean, + vol.Optional(CONF_CALENDAR_ACCESS, default="read_write"): cv.enum( + FeatureAccess + ), + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) _SINGLE_CALSEARCH_CONFIG = vol.All( cv.deprecated(CONF_MAX_RESULTS), + cv.deprecated(CONF_TRACK), vol.Schema( { vol.Required(CONF_NAME): cv.string, @@ -160,6 +164,9 @@ ADD_EVENT_SERVICE_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Google component.""" + if DOMAIN not in config: + return True + conf = config.get(DOMAIN, {}) hass.data[DOMAIN] = {DATA_CONFIG: conf} @@ -189,11 +196,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: }, ) ) + + _LOGGER.warning( + "Configuration of Google Calendar in YAML in configuration.yaml is " + "is deprecated and will be removed in a future release; Your existing " + "OAuth Application Credentials and other settings have been imported " + "into the UI automatically and can be safely removed from your " + "configuration.yaml file" + ) + return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Google from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + async_upgrade_entry(hass, entry) implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( hass, entry @@ -216,8 +234,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except aiohttp.ClientError as err: raise ConfigEntryNotReady from err - access = get_feature_access(hass) - if access.scope not in session.token.get("scope", []): + access = FeatureAccess[entry.options[CONF_CALENDAR_ACCESS]] + token_scopes = session.token.get("scope", []) + if access.scope not in token_scopes: + _LOGGER.debug("Scope '%s' not in scopes '%s'", access.scope, token_scopes) raise ConfigEntryAuthFailed( "Required scopes are not available, reauth required" ) @@ -226,25 +246,53 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data[DOMAIN][DATA_SERVICE] = calendar_service - track_new = hass.data[DOMAIN][DATA_CONFIG].get(CONF_TRACK_NEW, True) - await async_setup_services(hass, track_new, calendar_service) + await async_setup_services(hass, calendar_service) # Only expose the add event service if we have the correct permissions if access is FeatureAccess.read_write: await async_setup_add_event_service(hass, calendar_service) hass.config_entries.async_setup_platforms(entry, PLATFORMS) + # Reload entry when options are updated + entry.async_on_unload(entry.add_update_listener(async_reload_entry)) + return True +def async_upgrade_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Upgrade the config entry if needed.""" + if DATA_CONFIG not in hass.data[DOMAIN] and entry.options: + return + + options = ( + entry.options + if entry.options + else { + CONF_CALENDAR_ACCESS: get_feature_access(hass).name, + } + ) + disable_new_entities = ( + not hass.data[DOMAIN].get(DATA_CONFIG, {}).get(CONF_TRACK_NEW, True) + ) + hass.config_entries.async_update_entry( + entry, + options=options, + pref_disable_new_entities=disable_new_entities, + ) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Reload the config entry when it changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + async def async_setup_services( hass: HomeAssistant, - track_new: bool, calendar_service: GoogleCalendarService, ) -> None: """Set up the service listeners.""" @@ -256,10 +304,7 @@ async def async_setup_services( async def _found_calendar(calendar_item: Calendar) -> None: calendar = get_calendar_info( hass, - { - **calendar_item.dict(exclude_unset=True), - CONF_TRACK: track_new, - }, + calendar_item.dict(exclude_unset=True), ) calendar_id = calendar_item.id # Populate the yaml file with all discovered calendars @@ -363,7 +408,6 @@ def get_calendar_info( CONF_CAL_ID: calendar["id"], CONF_ENTITIES: [ { - CONF_TRACK: calendar["track"], CONF_NAME: calendar["summary"], CONF_DEVICE_ID: generate_entity_id( "{}", calendar["summary"], hass=hass diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index 3ce21fbb03d..eeac854a2ae 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable import datetime import logging import time -from typing import Any +from typing import Any, cast import aiohttp from gcal_sync.auth import AbstractAuth @@ -76,12 +76,12 @@ class DeviceFlow: @property def verification_url(self) -> str: """Return the verification url that the user should visit to enter the code.""" - return self._device_flow_info.verification_url + return self._device_flow_info.verification_url # type: ignore[no-any-return] @property def user_code(self) -> str: """Return the code that the user should enter at the verification url.""" - return self._device_flow_info.user_code + return self._device_flow_info.user_code # type: ignore[no-any-return] async def start_exchange_task( self, finished_cb: Callable[[Credentials | None], Awaitable[None]] @@ -131,10 +131,13 @@ def get_feature_access(hass: HomeAssistant) -> FeatureAccess: """Return the desired calendar feature access.""" # This may be called during config entry setup without integration setup running when there # is no google entry in configuration.yaml - return ( - hass.data.get(DOMAIN, {}) - .get(DATA_CONFIG, {}) - .get(CONF_CALENDAR_ACCESS, DEFAULT_FEATURE_ACCESS) + return cast( + FeatureAccess, + ( + hass.data.get(DOMAIN, {}) + .get(DATA_CONFIG, {}) + .get(CONF_CALENDAR_ACCESS, DEFAULT_FEATURE_ACCESS) + ), ) @@ -157,7 +160,7 @@ async def async_create_device_flow( return DeviceFlow(hass, oauth_flow, device_flow_info) -class ApiAuthImpl(AbstractAuth): +class ApiAuthImpl(AbstractAuth): # type: ignore[misc] """Authentication implementation for google calendar api library.""" def __init__( @@ -172,10 +175,10 @@ class ApiAuthImpl(AbstractAuth): async def async_get_access_token(self) -> str: """Return a valid access token.""" await self._session.async_ensure_token_valid() - return self._session.token["access_token"] + return cast(str, self._session.token["access_token"]) -class AccessTokenAuthImpl(AbstractAuth): +class AccessTokenAuthImpl(AbstractAuth): # type: ignore[misc] """Authentication implementation used during config flow, without refresh. This exists to allow the config flow to use the API before it has fully diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index b0d13c0c0c6..01780702b7f 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -1,4 +1,5 @@ """Support for Google Calendar Search binary sensors.""" + from __future__ import annotations import copy @@ -89,14 +90,25 @@ def _async_setup_entities( ) -> None: calendar_service = hass.data[DOMAIN][DATA_SERVICE] entities = [] + num_entities = len(disc_info[CONF_ENTITIES]) for data in disc_info[CONF_ENTITIES]: - if not data[CONF_TRACK]: - continue - entity_id = generate_entity_id( - ENTITY_ID_FORMAT, data[CONF_DEVICE_ID], hass=hass - ) + entity_enabled = data.get(CONF_TRACK, True) + entity_name = data[CONF_DEVICE_ID] + entity_id = generate_entity_id(ENTITY_ID_FORMAT, entity_name, hass=hass) + calendar_id = disc_info[CONF_CAL_ID] + if num_entities > 1: + # The google_calendars.yaml file lets users add multiple entities for + # the same calendar id and needs additional disambiguation + unique_id = f"{calendar_id}-{entity_name}" + else: + unique_id = calendar_id entity = GoogleCalendarEntity( - calendar_service, disc_info[CONF_CAL_ID], data, entity_id + calendar_service, + disc_info[CONF_CAL_ID], + data, + entity_id, + unique_id, + entity_enabled, ) entities.append(entity) @@ -112,6 +124,8 @@ class GoogleCalendarEntity(CalendarEntity): calendar_id: str, data: dict[str, Any], entity_id: str, + unique_id: str, + entity_enabled: bool, ) -> None: """Create the Calendar event device.""" self._calendar_service = calendar_service @@ -123,6 +137,8 @@ class GoogleCalendarEntity(CalendarEntity): self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET) self._offset_value: timedelta | None = None self.entity_id = entity_id + self._attr_unique_id = unique_id + self._attr_entity_registry_enabled_default = entity_enabled @property def extra_state_attributes(self) -> dict[str, bool]: @@ -152,7 +168,7 @@ class GoogleCalendarEntity(CalendarEntity): """Return True if the event is visible.""" if self._ignore_availability: return True - return event.transparency == OPAQUE + return event.transparency == OPAQUE # type: ignore[no-any-return] async def async_get_events( self, hass: HomeAssistant, start_date: datetime, end_date: datetime diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index 3b513f17197..be516230d2b 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -7,7 +7,10 @@ from typing import Any from gcal_sync.api import GoogleCalendarService from gcal_sync.exceptions import ApiException from oauth2client.client import Credentials +import voluptuous as vol +from homeassistant import config_entries +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -21,7 +24,7 @@ from .api import ( async_create_device_flow, get_feature_access, ) -from .const import DOMAIN +from .const import CONF_CALENDAR_ACCESS, DOMAIN, FeatureAccess _LOGGER = logging.getLogger(__name__) @@ -36,7 +39,7 @@ class OAuth2FlowHandler( def __init__(self) -> None: """Set up instance.""" super().__init__() - self._reauth = False + self._reauth_config_entry: config_entries.ConfigEntry | None = None self._device_flow: DeviceFlow | None = None @property @@ -60,7 +63,7 @@ class OAuth2FlowHandler( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle external yaml configuration.""" - if not self._reauth and self._async_current_entries(): + if not self._reauth_config_entry and self._async_current_entries(): return self.async_abort(reason="already_configured") return await super().async_step_user(user_input) @@ -84,12 +87,17 @@ class OAuth2FlowHandler( self.flow_impl, ) return self.async_abort(reason="oauth_error") + calendar_access = get_feature_access(self.hass) + if self._reauth_config_entry and self._reauth_config_entry.options: + calendar_access = FeatureAccess[ + self._reauth_config_entry.options[CONF_CALENDAR_ACCESS] + ] try: device_flow = await async_create_device_flow( self.hass, self.flow_impl.client_id, self.flow_impl.client_secret, - get_feature_access(self.hass), + calendar_access, ) except OAuthError as err: _LOGGER.error("Error initializing device flow: %s", str(err)) @@ -146,13 +154,21 @@ class OAuth2FlowHandler( _LOGGER.debug("Error reading calendar primary calendar: %s", err) primary_calendar = None title = primary_calendar.id if primary_calendar else self.flow_impl.name - return self.async_create_entry(title=title, data=data) + return self.async_create_entry( + title=title, + data=data, + options={ + CONF_CALENDAR_ACCESS: get_feature_access(self.hass).name, + }, + ) async def async_step_reauth( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Perform reauth upon an API authentication error.""" - self._reauth = True + self._reauth_config_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( @@ -162,3 +178,43 @@ class OAuth2FlowHandler( if user_input is None: return self.async_show_form(step_id="reauth_confirm") return await self.async_step_user() + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Create an options flow.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Google Calendar options flow.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_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_CALENDAR_ACCESS, + default=self.config_entry.options.get(CONF_CALENDAR_ACCESS), + ): vol.In( + { + "read_write": "Read/Write access (can create events)", + "read_only": "Read-only access", + } + ) + } + ), + ) diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json index e8ec7091030..e32223627be 100644 --- a/homeassistant/components/google/strings.json +++ b/homeassistant/components/google/strings.json @@ -27,5 +27,14 @@ "progress": { "exchange": "To link your Google account, visit the [{url}]({url}) and enter code:\n\n{user_code}" } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Home Assistant access to Google Calendar" + } + } + } } } diff --git a/homeassistant/components/google/translations/en.json b/homeassistant/components/google/translations/en.json index 02c8e6d7029..58c89834ca5 100644 --- a/homeassistant/components/google/translations/en.json +++ b/homeassistant/components/google/translations/en.json @@ -27,5 +27,14 @@ "title": "Reauthenticate Integration" } } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Home Assistant access to Google Calendar" + } + } + } } } \ No newline at end of file diff --git a/mypy.ini b/mypy.ini index ed073141fe1..3cc9653e27a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -874,6 +874,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.google.*] +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.greeneye_monitor.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index 625d1faa937..77ebe1e56cd 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -21,6 +21,7 @@ from homeassistant.components.application_credentials import ( async_import_client_credential, ) from homeassistant.components.google.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util.dt import utcnow @@ -143,6 +144,7 @@ async def test_full_flow_yaml_creds( "token_type": "Bearer", }, } + assert result.get("options") == {"calendar_access": "read_write"} assert len(mock_setup.mock_calls) == 1 entries = hass.config_entries.async_entries(DOMAIN) @@ -205,6 +207,7 @@ async def test_full_flow_application_creds( "token_type": "Bearer", }, } + assert result.get("options") == {"calendar_access": "read_write"} assert len(mock_setup.mock_calls) == 1 entries = hass.config_entries.async_entries(DOMAIN) @@ -441,7 +444,12 @@ async def test_reauth_flow( assert await component_setup() result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=config_entry.data + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + }, + data=config_entry.data, ) assert result["type"] == "form" assert result["step_id"] == "reauth_confirm" @@ -523,3 +531,66 @@ async def test_title_lookup_failure( assert len(mock_setup.mock_calls) == 1 entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 + + +async def test_options_flow_triggers_reauth( + hass: HomeAssistant, + component_setup: ComponentSetup, + config_entry: MockConfigEntry, +) -> None: + """Test load and unload of a ConfigEntry.""" + config_entry.add_to_hass(hass) + await component_setup() + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.options == {"calendar_access": "read_write"} + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == "form" + assert result["step_id"] == "init" + data_schema = result["data_schema"].schema + assert set(data_schema) == {"calendar_access"} + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "calendar_access": "read_only", + }, + ) + assert result["type"] == "create_entry" + + await hass.async_block_till_done() + assert config_entry.options == {"calendar_access": "read_only"} + # Re-auth flow was initiated because access level changed + assert config_entry.state is ConfigEntryState.SETUP_ERROR + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" + + +async def test_options_flow_no_changes( + hass: HomeAssistant, + component_setup: ComponentSetup, + config_entry: MockConfigEntry, +) -> None: + """Test load and unload of a ConfigEntry.""" + config_entry.add_to_hass(hass) + await component_setup() + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.options == {"calendar_access": "read_write"} + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == "form" + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "calendar_access": "read_write", + }, + ) + assert result["type"] == "create_entry" + + await hass.async_block_till_done() + assert config_entry.options == {"calendar_access": "read_write"} + # Re-auth flow was initiated because access level changed + assert config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 511e8545b40..4709379e840 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -46,7 +46,7 @@ HassApi = Callable[[], Awaitable[dict[str, Any]]] def assert_state(actual: State | None, expected: State | None) -> None: """Assert that the two states are equal.""" - if actual is None: + if actual is None or expected is None: assert actual == expected return assert actual.entity_id == expected.entity_id From 39ce25f76dd0ded7db2f1609fc457fe8a37fe8c1 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 22 May 2022 14:30:57 -0700 Subject: [PATCH 0755/3516] Add Withings application_credentials platform (#71990) * Add Withings application_credentials platform * Update withings auth implementation --- homeassistant/components/withings/__init__.py | 39 ++++++++++++------- .../withings/application_credentials.py | 28 +++++++++++++ homeassistant/components/withings/common.py | 4 +- .../components/withings/manifest.json | 2 +- .../generated/application_credentials.py | 1 + tests/components/withings/test_init.py | 2 - 6 files changed, 56 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/withings/application_credentials.py diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 9d1fe1a600f..47702090cc0 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -9,10 +9,13 @@ import asyncio from aiohttp.web import Request, Response import voluptuous as vol -from withings_api import AbstractWithingsApi, WithingsAuth from withings_api.common import NotifyAppli from homeassistant.components import webhook +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.components.webhook import ( async_unregister as async_unregister_webhook, ) @@ -28,10 +31,9 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType -from . import config_flow, const +from . import const from .common import ( _LOGGER, - WithingsLocalOAuth2Implementation, async_get_data_manager, async_remove_data_manager, get_data_manager_by_webhook_id, @@ -45,10 +47,12 @@ CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( cv.deprecated(const.CONF_PROFILES), + cv.deprecated(CONF_CLIENT_ID), + cv.deprecated(CONF_CLIENT_SECRET), vol.Schema( { - vol.Required(CONF_CLIENT_ID): vol.All(cv.string, vol.Length(min=1)), - vol.Required(CONF_CLIENT_SECRET): vol.All( + vol.Optional(CONF_CLIENT_ID): vol.All(cv.string, vol.Length(min=1)), + vol.Optional(CONF_CLIENT_SECRET): vol.All( cv.string, vol.Length(min=1) ), vol.Optional(const.CONF_USE_WEBHOOK, default=False): cv.boolean, @@ -76,17 +80,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DOMAIN] = {const.CONFIG: conf} # Setup the oauth2 config flow. - config_flow.WithingsFlowHandler.async_register_implementation( - hass, - WithingsLocalOAuth2Implementation( + if CONF_CLIENT_ID in conf: + await async_import_client_credential( hass, - const.DOMAIN, - conf[CONF_CLIENT_ID], - conf[CONF_CLIENT_SECRET], - f"{WithingsAuth.URL}/oauth2_user/authorize2", - f"{AbstractWithingsApi.URL}/v2/oauth2", - ), - ) + DOMAIN, + ClientCredential( + conf[CONF_CLIENT_ID], + conf[CONF_CLIENT_SECRET], + ), + ) + _LOGGER.warning( + "Configuration of Withings integration OAuth2 credentials in YAML " + "is deprecated and will be removed in a future release; Your " + "existing OAuth Application Credentials have been imported into " + "the UI automatically and can be safely removed from your " + "configuration.yaml file" + ) return True diff --git a/homeassistant/components/withings/application_credentials.py b/homeassistant/components/withings/application_credentials.py new file mode 100644 index 00000000000..e5c401d5e74 --- /dev/null +++ b/homeassistant/components/withings/application_credentials.py @@ -0,0 +1,28 @@ +"""application_credentials platform for Withings.""" + +from withings_api import AbstractWithingsApi, WithingsAuth + +from homeassistant.components.application_credentials import ( + AuthorizationServer, + ClientCredential, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow + +from .common import WithingsLocalOAuth2Implementation +from .const import DOMAIN + + +async def async_get_auth_implementation( + hass: HomeAssistant, auth_domain: str, credential: ClientCredential +) -> config_entry_oauth2_flow.AbstractOAuth2Implementation: + """Return auth implementation.""" + return WithingsLocalOAuth2Implementation( + hass, + DOMAIN, + credential, + authorization_server=AuthorizationServer( + authorize_url=f"{WithingsAuth.URL}/oauth2_user/authorize2", + token_url=f"{AbstractWithingsApi.URL}/v2/oauth2", + ), + ) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 78ae375b4bc..ad40378eafb 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -28,6 +28,7 @@ from withings_api.common import ( ) from homeassistant.components import webhook +from homeassistant.components.application_credentials import AuthImplementation from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.http import HomeAssistantView from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -45,7 +46,6 @@ from homeassistant.helpers import config_entry_oauth2_flow, entity_registry as e from homeassistant.helpers.config_entry_oauth2_flow import ( AUTH_CALLBACK_PATH, AbstractOAuth2Implementation, - LocalOAuth2Implementation, OAuth2Session, ) from homeassistant.helpers.entity import Entity @@ -1106,7 +1106,7 @@ def get_platform_attributes(platform: str) -> tuple[WithingsAttribute, ...]: ) -class WithingsLocalOAuth2Implementation(LocalOAuth2Implementation): +class WithingsLocalOAuth2Implementation(AuthImplementation): """Oauth2 implementation that only uses the external url.""" @property diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index 2e089bdbcc9..1437b7f2198 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/withings", "requirements": ["withings-api==2.4.0"], - "dependencies": ["auth", "http", "webhook"], + "dependencies": ["application_credentials", "http", "webhook"], "codeowners": ["@vangorra"], "iot_class": "cloud_polling", "loggers": ["withings_api"] diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index 41541943086..d7b3259a158 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -12,6 +12,7 @@ APPLICATION_CREDENTIALS = [ "neato", "netatmo", "spotify", + "withings", "xbox", "yolink" ] diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index 465c26deb32..db26f7328ce 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -59,7 +59,6 @@ def test_config_schema_basic_config() -> None: def test_config_schema_client_id() -> None: """Test schema.""" - config_schema_assert_fail({CONF_CLIENT_SECRET: "my_client_secret"}) config_schema_assert_fail( {CONF_CLIENT_SECRET: "my_client_secret", CONF_CLIENT_ID: ""} ) @@ -70,7 +69,6 @@ def test_config_schema_client_id() -> None: def test_config_schema_client_secret() -> None: """Test schema.""" - config_schema_assert_fail({CONF_CLIENT_ID: "my_client_id"}) config_schema_assert_fail({CONF_CLIENT_ID: "my_client_id", CONF_CLIENT_SECRET: ""}) config_schema_validate( {CONF_CLIENT_ID: "my_client_id", CONF_CLIENT_SECRET: "my_client_secret"} From 7df7e33d17bdcaef419e49428f42a5de489502c5 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 23 May 2022 00:25:42 +0000 Subject: [PATCH 0756/3516] [ci skip] Translation update --- .../aladdin_connect/translations/nl.json | 2 +- .../translations/nl.json | 2 +- .../aussie_broadband/translations/nl.json | 2 +- .../components/auth/translations/id.json | 2 +- .../components/bosch_shc/translations/nl.json | 2 +- .../components/brunt/translations/nl.json | 2 +- .../components/climacell/translations/id.json | 2 +- .../components/coinbase/translations/id.json | 2 +- .../geocaching/translations/nl.json | 2 +- .../components/google/translations/id.json | 2 +- .../components/google/translations/it.json | 9 ++ .../components/google/translations/nl.json | 2 +- .../components/google/translations/pt-BR.json | 9 ++ .../google_travel_time/translations/id.json | 4 +- .../here_travel_time/translations/id.json | 82 +++++++++++++++++++ .../here_travel_time/translations/it.json | 82 +++++++++++++++++++ .../here_travel_time/translations/nl.json | 82 +++++++++++++++++++ .../here_travel_time/translations/pl.json | 19 +++++ .../translations/zh-Hant.json | 82 +++++++++++++++++++ .../components/icloud/translations/nl.json | 2 +- .../components/knx/translations/id.json | 4 +- .../components/konnected/translations/id.json | 4 +- .../components/konnected/translations/it.json | 14 ++-- .../components/konnected/translations/nl.json | 10 +-- .../components/laundrify/translations/ca.json | 25 ++++++ .../components/laundrify/translations/el.json | 25 ++++++ .../components/laundrify/translations/id.json | 25 ++++++ .../components/laundrify/translations/it.json | 25 ++++++ .../components/laundrify/translations/nl.json | 25 ++++++ .../components/laundrify/translations/pl.json | 22 +++++ .../laundrify/translations/zh-Hant.json | 25 ++++++ .../components/lyric/translations/nl.json | 2 +- .../components/nest/translations/nl.json | 2 +- .../components/netatmo/translations/nl.json | 2 +- .../components/notion/translations/nl.json | 2 +- .../components/nuki/translations/nl.json | 2 +- .../components/nws/translations/id.json | 2 +- .../components/plaato/translations/id.json | 2 +- .../components/plex/translations/nl.json | 2 +- .../components/renault/translations/nl.json | 2 +- .../components/ridwell/translations/nl.json | 2 +- .../components/samsungtv/translations/id.json | 2 +- .../components/sense/translations/nl.json | 2 +- .../simplisafe/translations/nl.json | 2 +- .../components/sleepiq/translations/nl.json | 2 +- .../components/smarttub/translations/nl.json | 2 +- .../components/sonarr/translations/nl.json | 2 +- .../components/spotify/translations/nl.json | 2 +- .../steam_online/translations/nl.json | 2 +- .../components/subaru/translations/id.json | 2 +- .../tomorrowio/translations/id.json | 2 +- .../totalconnect/translations/nl.json | 2 +- .../uptimerobot/translations/nl.json | 2 +- .../components/watttime/translations/nl.json | 2 +- .../components/withings/translations/nl.json | 2 +- .../xiaomi_miio/translations/nl.json | 2 +- .../components/yolink/translations/nl.json | 2 +- 57 files changed, 593 insertions(+), 56 deletions(-) create mode 100644 homeassistant/components/here_travel_time/translations/id.json create mode 100644 homeassistant/components/here_travel_time/translations/it.json create mode 100644 homeassistant/components/here_travel_time/translations/nl.json create mode 100644 homeassistant/components/here_travel_time/translations/pl.json create mode 100644 homeassistant/components/here_travel_time/translations/zh-Hant.json create mode 100644 homeassistant/components/laundrify/translations/ca.json create mode 100644 homeassistant/components/laundrify/translations/el.json create mode 100644 homeassistant/components/laundrify/translations/id.json create mode 100644 homeassistant/components/laundrify/translations/it.json create mode 100644 homeassistant/components/laundrify/translations/nl.json create mode 100644 homeassistant/components/laundrify/translations/pl.json create mode 100644 homeassistant/components/laundrify/translations/zh-Hant.json diff --git a/homeassistant/components/aladdin_connect/translations/nl.json b/homeassistant/components/aladdin_connect/translations/nl.json index 90d1fb67029..bd3384bd0f1 100644 --- a/homeassistant/components/aladdin_connect/translations/nl.json +++ b/homeassistant/components/aladdin_connect/translations/nl.json @@ -14,7 +14,7 @@ "password": "Wachtwoord" }, "description": "De Aladdin Connect-integratie moet uw account opnieuw verifi\u00ebren", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" }, "user": { "data": { diff --git a/homeassistant/components/application_credentials/translations/nl.json b/homeassistant/components/application_credentials/translations/nl.json index c186d95ce27..3638594ac2e 100644 --- a/homeassistant/components/application_credentials/translations/nl.json +++ b/homeassistant/components/application_credentials/translations/nl.json @@ -1,3 +1,3 @@ { - "title": "Applicatiegegevens" + "title": "Applicatie inloggegevens" } \ No newline at end of file diff --git a/homeassistant/components/aussie_broadband/translations/nl.json b/homeassistant/components/aussie_broadband/translations/nl.json index efb553cca25..77e22db0a48 100644 --- a/homeassistant/components/aussie_broadband/translations/nl.json +++ b/homeassistant/components/aussie_broadband/translations/nl.json @@ -16,7 +16,7 @@ "password": "Wachtwoord" }, "description": "Update wachtwoord voor {username}", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" }, "service": { "data": { diff --git a/homeassistant/components/auth/translations/id.json b/homeassistant/components/auth/translations/id.json index ed7bede5fff..22b562e7c97 100644 --- a/homeassistant/components/auth/translations/id.json +++ b/homeassistant/components/auth/translations/id.json @@ -5,7 +5,7 @@ "no_available_service": "Tidak ada layanan notifikasi yang tersedia." }, "error": { - "invalid_code": "Kode tifak valid, coba lagi." + "invalid_code": "Kode tidak valid, coba lagi." }, "step": { "init": { diff --git a/homeassistant/components/bosch_shc/translations/nl.json b/homeassistant/components/bosch_shc/translations/nl.json index dd134e54c06..8e966ff9bbe 100644 --- a/homeassistant/components/bosch_shc/translations/nl.json +++ b/homeassistant/components/bosch_shc/translations/nl.json @@ -23,7 +23,7 @@ }, "reauth_confirm": { "description": "De bosch_shc integratie moet uw account herauthenticeren", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" }, "user": { "data": { diff --git a/homeassistant/components/brunt/translations/nl.json b/homeassistant/components/brunt/translations/nl.json index 9ea207d1eae..a5aa018a1d0 100644 --- a/homeassistant/components/brunt/translations/nl.json +++ b/homeassistant/components/brunt/translations/nl.json @@ -15,7 +15,7 @@ "password": "Wachtwoord" }, "description": "Voer het wachtwoord opnieuw in voor: {username}", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" }, "user": { "data": { diff --git a/homeassistant/components/climacell/translations/id.json b/homeassistant/components/climacell/translations/id.json index 57f88557a69..4d020351665 100644 --- a/homeassistant/components/climacell/translations/id.json +++ b/homeassistant/components/climacell/translations/id.json @@ -5,7 +5,7 @@ "data": { "timestep": "Jarak Interval Prakiraan NowCast dalam Menit" }, - "description": "Jika Anda memilih untuk mengaktifkan entitas prakiraan 'nowcast', Anda dapat mengonfigurasi jarak interval prakiraan dalam menit. Jumlah prakiraan yang diberikan tergantung pada nilai interval yang dipilih.", + "description": "Jika Anda memilih untuk mengaktifkan entitas prakiraan `nowcast`, Anda dapat mengonfigurasi jarak interval prakiraan dalam menit. Jumlah prakiraan yang diberikan tergantung pada nilai interval yang dipilih.", "title": "Perbarui Opsi ClimaCell" } } diff --git a/homeassistant/components/coinbase/translations/id.json b/homeassistant/components/coinbase/translations/id.json index ba2d2de0753..114c69acce2 100644 --- a/homeassistant/components/coinbase/translations/id.json +++ b/homeassistant/components/coinbase/translations/id.json @@ -16,7 +16,7 @@ "api_key": "Kunci API", "api_token": "Kode Rahasia API" }, - "description": "Silakan masukkan detail kunci API Anda sesuai yang disediakan oleh Coinbase.", + "description": "Masukkan detail kunci API Anda sesuai yang disediakan oleh Coinbase.", "title": "Detail Kunci API Coinbase" } } diff --git a/homeassistant/components/geocaching/translations/nl.json b/homeassistant/components/geocaching/translations/nl.json index e4cfc747c62..f221532a77e 100644 --- a/homeassistant/components/geocaching/translations/nl.json +++ b/homeassistant/components/geocaching/translations/nl.json @@ -18,7 +18,7 @@ }, "reauth_confirm": { "description": "De Geocaching integratie moet uw account herauthenticeren", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" } } } diff --git a/homeassistant/components/google/translations/id.json b/homeassistant/components/google/translations/id.json index 371fa7551b5..fd1002d41be 100644 --- a/homeassistant/components/google/translations/id.json +++ b/homeassistant/components/google/translations/id.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Akun sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", - "code_expired": "Kode autentikasi kedaluwarsa atau penyiapan kredensial tidak valid, silakan coba lagi.", + "code_expired": "Kode autentikasi kedaluwarsa atau penyiapan kredensial tidak valid, coba lagi.", "invalid_access_token": "Token akses tidak valid", "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", "oauth_error": "Menerima respons token yang tidak valid.", diff --git a/homeassistant/components/google/translations/it.json b/homeassistant/components/google/translations/it.json index 4530a690ed6..d57998894d9 100644 --- a/homeassistant/components/google/translations/it.json +++ b/homeassistant/components/google/translations/it.json @@ -27,5 +27,14 @@ "title": "Autentica nuovamente l'integrazione" } } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Accesso di Home Assistant a Google Calendar" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/nl.json b/homeassistant/components/google/translations/nl.json index dca192c59d4..baf0064ee63 100644 --- a/homeassistant/components/google/translations/nl.json +++ b/homeassistant/components/google/translations/nl.json @@ -24,7 +24,7 @@ }, "reauth_confirm": { "description": "De Google Agenda-integratie moet uw account opnieuw verifi\u00ebren", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" } } } diff --git a/homeassistant/components/google/translations/pt-BR.json b/homeassistant/components/google/translations/pt-BR.json index 8ab124f1b5c..85c7254b9a7 100644 --- a/homeassistant/components/google/translations/pt-BR.json +++ b/homeassistant/components/google/translations/pt-BR.json @@ -27,5 +27,14 @@ "title": "Reautenticar Integra\u00e7\u00e3o" } } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Acesso do Home Assistant ao Google Calendar" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/id.json b/homeassistant/components/google_travel_time/translations/id.json index 16b60148aa9..e960d03c341 100644 --- a/homeassistant/components/google_travel_time/translations/id.json +++ b/homeassistant/components/google_travel_time/translations/id.json @@ -14,7 +14,7 @@ "name": "Nama", "origin": "Asal" }, - "description": "Saat menentukan asal dan tujuan, Anda dapat menyediakan satu atau beberapa lokasi yang dipisahkan oleh karakter pipe, dalam bentuk alamat, koordinat lintang/bujur, atau ID tempat Google. Saat menentukan lokasi menggunakan ID tempat Google, ID harus diawali dengan \"place_id:'." + "description": "Saat menentukan asal dan tujuan, Anda dapat menyediakan satu atau beberapa lokasi yang dipisahkan oleh karakter pipe, dalam bentuk alamat, koordinat lintang/bujur, atau ID tempat Google. Saat menentukan lokasi menggunakan ID tempat Google, ID harus diawali dengan `place_id:`." } } }, @@ -31,7 +31,7 @@ "transit_routing_preference": "Preferensi Perutean Transit", "units": "Unit" }, - "description": "Anda dapat menentukan Waktu Keberangkatan atau Waktu Kedatangan secara opsional. Jika menentukan waktu keberangkatan, Anda dapat memasukkan 'sekarang', stempel waktu Unix, atau string waktu 24 jam seperti 08:00:00`. Jika menentukan waktu kedatangan, Anda dapat menggunakan stempel waktu Unix atau string waktu 24 jam seperti 08:00:00`" + "description": "Anda dapat menentukan Waktu Keberangkatan atau Waktu Kedatangan secara opsional. Jika menentukan waktu keberangkatan, Anda dapat memasukkan `now`, stempel waktu Unix, atau string waktu 24 jam seperti `08:00:00`. Jika menentukan waktu kedatangan, Anda dapat menggunakan stempel waktu Unix atau string waktu 24 jam seperti `08:00:00`." } } }, diff --git a/homeassistant/components/here_travel_time/translations/id.json b/homeassistant/components/here_travel_time/translations/id.json new file mode 100644 index 00000000000..f03910d9adf --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/id.json @@ -0,0 +1,82 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "destination_coordinates": { + "data": { + "destination": "Tujuan dalam koordinat GPS" + }, + "title": "Pilih Tujuan" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "Tujuan menggunakan entitas" + }, + "title": "Pilih Tujuan" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "Menggunakan lokasi pada peta", + "destination_entity": "Menggunakan entitas" + }, + "title": "Pilih Tujuan" + }, + "origin_coordinates": { + "data": { + "origin": "Asal dalam koordinat GPS" + }, + "title": "Pilih Asal" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "Asal menggunakan entitas" + }, + "title": "Pilih Asal" + }, + "user": { + "data": { + "api_key": "Kunci API", + "mode": "Mode Perjalanan", + "name": "Nama" + } + } + } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "Waktu Kedatangan" + }, + "title": "Pilih Waktu Kedatangan" + }, + "departure_time": { + "data": { + "departure_time": "Waktu Keberangkatan" + }, + "title": "Pilih Waktu Keberangkatan" + }, + "init": { + "data": { + "route_mode": "Mode Rute", + "traffic_mode": "Modus Lalu Lintas", + "unit_system": "Sistem unit" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "Konfigurasikan waktu kedatangan", + "departure_time": "Konfigurasikan waktu keberangkatan", + "no_time": "Jangan mengonfigurasi waktu" + }, + "title": "Pilih Jenis Waktu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/it.json b/homeassistant/components/here_travel_time/translations/it.json new file mode 100644 index 00000000000..e9716318adb --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/it.json @@ -0,0 +1,82 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "destination_coordinates": { + "data": { + "destination": "Destinazione come coordinate GPS" + }, + "title": "Scegli la destinazione" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "Destinazione utilizzando un'entit\u00e0" + }, + "title": "Scegli la destinazione" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "Utilizzando una posizione sulla mappa", + "destination_entity": "Utilizzando un'entit\u00e0" + }, + "title": "Scegli la destinazione" + }, + "origin_coordinates": { + "data": { + "origin": "Partenza come coordinate GPS" + }, + "title": "Scegli la partenza" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "Partenza utilizzando un'entit\u00e0" + }, + "title": "Scegli la partenza" + }, + "user": { + "data": { + "api_key": "Chiave API", + "mode": "Modalit\u00e0 di viaggio", + "name": "Nome" + } + } + } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "Orario di arrivo" + }, + "title": "Scegli l'orario di arrivo" + }, + "departure_time": { + "data": { + "departure_time": "Orario di partenza" + }, + "title": "Scegli l'orario di partenza" + }, + "init": { + "data": { + "route_mode": "Modalit\u00e0 percorso", + "traffic_mode": "Modalit\u00e0 traffico", + "unit_system": "Sistema di unit\u00e0" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "Configura un orario di arrivo", + "departure_time": "Configura un orario di partenza", + "no_time": "Non configurare un orario" + }, + "title": "Scegli il tipo di orario" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/nl.json b/homeassistant/components/here_travel_time/translations/nl.json new file mode 100644 index 00000000000..cbec21776e5 --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/nl.json @@ -0,0 +1,82 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "destination_coordinates": { + "data": { + "destination": "Bestemming als GPS-co\u00f6rdinaten" + }, + "title": "Bestemming kiezen" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "Bestemming van een entiteit" + }, + "title": "Bestemming kiezen" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "Een kaartlocatie gebruiken", + "destination_entity": "Een entiteit gebruiken" + }, + "title": "Bestemming kiezen" + }, + "origin_coordinates": { + "data": { + "origin": "Herkomst als GPS-co\u00f6rdinaten" + }, + "title": "Herkomst kiezen" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "Herkomst van een entiteit" + }, + "title": "Herkomst kiezen" + }, + "user": { + "data": { + "api_key": "API-sleutel", + "mode": "Reismodus", + "name": "Naam" + } + } + } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "Aankomsttijd" + }, + "title": "Aankomsttijd kiezen" + }, + "departure_time": { + "data": { + "departure_time": "Vertrektijd" + }, + "title": "Vertrektijd kiezen" + }, + "init": { + "data": { + "route_mode": "Routemodus", + "traffic_mode": "Verkeersmodus", + "unit_system": "Eenheidssysteem" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "Een aankomsttijd configureren", + "departure_time": "Een vertrektijd configureren", + "no_time": "Geen tijd configureren" + }, + "title": "Kies tijdtype" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/pl.json b/homeassistant/components/here_travel_time/translations/pl.json new file mode 100644 index 00000000000..f04290714e4 --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "name": "Nazwa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/zh-Hant.json b/homeassistant/components/here_travel_time/translations/zh-Hant.json new file mode 100644 index 00000000000..53d5eae18fd --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/zh-Hant.json @@ -0,0 +1,82 @@ +{ + "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" + }, + "step": { + "destination_coordinates": { + "data": { + "destination": "\u76ee\u7684\u5730\u70ba GPS \u5ea7\u6a19" + }, + "title": "\u9078\u64c7\u76ee\u7684\u5730" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "\u4f7f\u7528\u76ee\u7684\u5730\u70ba\u5be6\u9ad4" + }, + "title": "\u9078\u64c7\u76ee\u7684\u5730" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "\u4f7f\u7528\u5730\u5716\u5ea7\u6a19", + "destination_entity": "\u4f7f\u7528\u70ba\u5be6\u9ad4" + }, + "title": "\u9078\u64c7\u76ee\u7684\u5730" + }, + "origin_coordinates": { + "data": { + "origin": "\u51fa\u767c\u5730\u70ba GPS \u5ea7\u6a19" + }, + "title": "\u9078\u64c7\u51fa\u767c\u5730" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "\u4f7f\u7528\u51fa\u767c\u5730\u70ba\u5be6\u9ad4" + }, + "title": "\u9078\u64c7\u51fa\u767c\u5730" + }, + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "mode": "\u65c5\u884c\u6a21\u5f0f", + "name": "\u540d\u7a31" + } + } + } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "\u62b5\u9054\u6642\u9593" + }, + "title": "\u9078\u64c7\u62b5\u9054\u6642\u9593" + }, + "departure_time": { + "data": { + "departure_time": "\u51fa\u767c\u6642\u9593" + }, + "title": "\u9078\u64c7\u51fa\u767c\u6642\u9593" + }, + "init": { + "data": { + "route_mode": "\u8def\u5f91\u6a21\u5f0f", + "traffic_mode": "\u4ea4\u901a\u6a21\u5f0f", + "unit_system": "\u55ae\u4f4d\u7cfb\u7d71" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "\u8a2d\u5b9a\u62b5\u9054\u6642\u9593", + "departure_time": "\u8a2d\u5b9a\u51fa\u767c\u6642\u9593", + "no_time": "\u4e0d\u8981\u8a2d\u5b9a\u6642\u9593" + }, + "title": "\u9078\u64c7\u6642\u9593\u985e\u578b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/nl.json b/homeassistant/components/icloud/translations/nl.json index b93ebab4665..522df78a737 100644 --- a/homeassistant/components/icloud/translations/nl.json +++ b/homeassistant/components/icloud/translations/nl.json @@ -16,7 +16,7 @@ "password": "Wachtwoord" }, "description": "Uw eerder ingevoerde wachtwoord voor {username} werkt niet meer. Update uw wachtwoord om deze integratie te blijven gebruiken.", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" }, "trusted_device": { "data": { diff --git a/homeassistant/components/knx/translations/id.json b/homeassistant/components/knx/translations/id.json index 4c55a3ef392..bbf9a1b7862 100644 --- a/homeassistant/components/knx/translations/id.json +++ b/homeassistant/components/knx/translations/id.json @@ -41,7 +41,7 @@ }, "secure_knxkeys": { "data": { - "knxkeys_filename": "Nama file '.knxkeys' Anda (termasuk ekstensi)", + "knxkeys_filename": "Nama file `.knxkeys` Anda (termasuk ekstensi)", "knxkeys_password": "Kata sandi untuk mendekripsi file `.knxkeys`" }, "data_description": { @@ -102,7 +102,7 @@ "multicast_group": "Digunakan untuk perutean dan penemuan. Bawaan: `224.0.23.12`", "multicast_port": "Digunakan untuk perutean dan penemuan. Bawaan: `3671`", "rate_limit": "Telegram keluar maksimum per detik.\nDirekomendasikan: 20 hingga 40", - "state_updater": "Menyetel default untuk status pembacaan KNX Bus. Saat dinonaktifkan, Home Assistant tidak akan secara aktif mengambil status entitas dari KNX Bus. Hal ini bisa ditimpa dengan opsi entitas 'sync_state'." + "state_updater": "Menyetel default untuk status pembacaan KNX Bus. Saat dinonaktifkan, Home Assistant tidak akan secara aktif mengambil status entitas dari KNX Bus. Hal ini bisa ditimpa dengan opsi entitas `sync_state`." } }, "tunnel": { diff --git a/homeassistant/components/konnected/translations/id.json b/homeassistant/components/konnected/translations/id.json index f2e2035ca06..38443f483ca 100644 --- a/homeassistant/components/konnected/translations/id.json +++ b/homeassistant/components/konnected/translations/id.json @@ -39,7 +39,7 @@ "options_binary": { "data": { "inverse": "Balikkan status buka/tutup", - "name": "Nama (opsional)", + "name": "Nama", "type": "Jenis Sensor Biner" }, "description": "Opsi {zone}", @@ -47,7 +47,7 @@ }, "options_digital": { "data": { - "name": "Nama (opsional)", + "name": "Nama", "poll_interval": "Interval Polling (dalam menit) (opsional)", "type": "Jenis Sensor" }, diff --git a/homeassistant/components/konnected/translations/it.json b/homeassistant/components/konnected/translations/it.json index 682a27b50d0..4872dedcae4 100644 --- a/homeassistant/components/konnected/translations/it.json +++ b/homeassistant/components/konnected/translations/it.json @@ -41,7 +41,7 @@ "options_binary": { "data": { "inverse": "Invertire lo stato di apertura/chiusura", - "name": "Nome (opzionale)", + "name": "Nome", "type": "Tipo di sensore binario" }, "description": "Opzioni {zone}", @@ -49,8 +49,8 @@ }, "options_digital": { "data": { - "name": "Nome (opzionale)", - "poll_interval": "Intervallo di sondaggio (minuti) (opzionale)", + "name": "Nome", + "poll_interval": "Intervallo di sondaggio (minuti)", "type": "Tipo di sensore" }, "description": "Opzioni {zone}", @@ -97,11 +97,11 @@ "options_switch": { "data": { "activation": "Uscita quando acceso", - "momentary": "Durata impulso (ms) (opzionale)", + "momentary": "Durata impulso (ms)", "more_states": "Configura stati aggiuntivi per questa zona", - "name": "Nome (opzionale)", - "pause": "Pausa tra gli impulsi (ms) (opzionale)", - "repeat": "Numero di volte da ripetere (-1 = infinito) (opzionale)" + "name": "Nome", + "pause": "Pausa tra gli impulsi (ms)", + "repeat": "Numero di volte da ripetere (-1 = infinito)" }, "description": "Opzioni {zone}: stato {state}", "title": "Configurare l'uscita commutabile" diff --git a/homeassistant/components/konnected/translations/nl.json b/homeassistant/components/konnected/translations/nl.json index 6e46ef3e2bc..8f0548296b4 100644 --- a/homeassistant/components/konnected/translations/nl.json +++ b/homeassistant/components/konnected/translations/nl.json @@ -50,7 +50,7 @@ "options_digital": { "data": { "name": "Naam (optional)", - "poll_interval": "Poll interval (minuten) (optioneel)", + "poll_interval": "Poll interval (minuten)", "type": "Type sensor" }, "description": "{zone} opties", @@ -86,7 +86,7 @@ }, "options_misc": { "data": { - "api_host": "API host-URL overschrijven (optioneel)", + "api_host": "API host-URL overschrijven", "blink": "Led knipperen bij het verzenden van statuswijziging", "discovery": "Reageer op detectieverzoeken op uw netwerk", "override_api_host": "Overschrijf standaard Home Assistant API hostpaneel-URL" @@ -97,11 +97,11 @@ "options_switch": { "data": { "activation": "Uitvoer wanneer ingeschakeld", - "momentary": "Pulsduur (ms) (optioneel)", + "momentary": "Pulsduur (ms)", "more_states": "Aanvullende statussen voor deze zone configureren", "name": "Naam (optioneel)", - "pause": "Pauze tussen de pulsen (ms) (optioneel)", - "repeat": "Aantal herhalingen (-1=oneindig) (optioneel)" + "pause": "Pauze tussen de pulsen (ms)", + "repeat": "Aantal herhalingen (-1=oneindig)" }, "description": "{zone} opties: status {state}", "title": "Schakelbare uitgang configureren" diff --git a/homeassistant/components/laundrify/translations/ca.json b/homeassistant/components/laundrify/translations/ca.json new file mode 100644 index 00000000000..99b94ecfd77 --- /dev/null +++ b/homeassistant/components/laundrify/translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_format": "Format inv\u00e0lid. Escriu-lo com a xxx-xxx.", + "unknown": "Error inesperat" + }, + "step": { + "init": { + "data": { + "code": "Codi d'autenticaci\u00f3 (xxx-xxx)" + }, + "description": "Introdueix codi d'autenticaci\u00f3 personal que es mostra a l'aplicaci\u00f3 de Laundrify." + }, + "reauth_confirm": { + "description": "La integraci\u00f3 Laundrify ha de tornar a autenticar-se amb el teu compte.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/el.json b/homeassistant/components/laundrify/translations/el.json new file mode 100644 index 00000000000..fae492f678a --- /dev/null +++ b/homeassistant/components/laundrify/translations/el.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae.\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "invalid_format": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03ba\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c9\u03c2 xxx-xxx.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "init": { + "data": { + "code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 (xxx-xxx)" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b9\u03ba\u03cc \u03c3\u03b1\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae laundrify-App." + }, + "reauth_confirm": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 laundrify \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b3\u03af\u03bd\u03b5\u03b9 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2.", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/id.json b/homeassistant/components/laundrify/translations/id.json new file mode 100644 index 00000000000..ec80b01a84a --- /dev/null +++ b/homeassistant/components/laundrify/translations/id.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "invalid_format": "Format yang tidak valid. Tentukan dengan format xxx-xxx.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "init": { + "data": { + "code": "Kode Autentikasi (xxx-xxx)" + }, + "description": "Masukkan Kode Autentikasi pribadi Anda yang ditampilkan di aplikasi laundrify." + }, + "reauth_confirm": { + "description": "Integrasi laundrify perlu mengautentikasi ulang akun Anda", + "title": "Autentikasi Ulang Integrasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/it.json b/homeassistant/components/laundrify/translations/it.json new file mode 100644 index 00000000000..08fd2dcb87a --- /dev/null +++ b/homeassistant/components/laundrify/translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "invalid_format": "Formato non valido. Specificare come xxx-xxx.", + "unknown": "Errore imprevisto" + }, + "step": { + "init": { + "data": { + "code": "Codice di autorizzazione (xxx-xxx)" + }, + "description": "Inserisci il tuo codice di autorizzazione personale che viene mostrato nell'app di laundrify." + }, + "reauth_confirm": { + "description": "L'integrazione laundrify deve eseguire nuovamente l'autenticazione.", + "title": "Autentica nuovamente l'integrazione" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/nl.json b/homeassistant/components/laundrify/translations/nl.json new file mode 100644 index 00000000000..1fdb9f35870 --- /dev/null +++ b/homeassistant/components/laundrify/translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "invalid_format": "Ongeldig formaat. Specificeer als xxx-xxx.", + "unknown": "Onverwachte fout" + }, + "step": { + "init": { + "data": { + "code": "Verificatiecode (xxx-xxx)" + }, + "description": "Voer de verificatiecode in die wordt weergegeven in de laundrify-app." + }, + "reauth_confirm": { + "description": "De laundrify-integratie moet opnieuw worden geauthenticeerd.", + "title": "Integratie herauthenticeren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/pl.json b/homeassistant/components/laundrify/translations/pl.json new file mode 100644 index 00000000000..28145b20fce --- /dev/null +++ b/homeassistant/components/laundrify/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "init": { + "data": { + "code": "Kod autoryzacyjny (xxx-xxx)" + } + }, + "reauth_confirm": { + "title": "Ponownie uwierzytelnij integracj\u0119" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/zh-Hant.json b/homeassistant/components/laundrify/translations/zh-Hant.json new file mode 100644 index 00000000000..834ebb9125b --- /dev/null +++ b/homeassistant/components/laundrify/translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_format": "\u683c\u5f0f\u932f\u8aa4\u3001\u8acb\u4f7f\u7528 xxx-xxx\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "init": { + "data": { + "code": "\u8a8d\u8b49\u78bc\uff08xxx-xxx\uff09" + }, + "description": "\u8acb\u8f38\u5165\u65bc laundrify App \u4e0a\u6240\u986f\u793a\u4e4b\u8a8d\u8b49\u78bc\u3002" + }, + "reauth_confirm": { + "description": "Laundrify \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/nl.json b/homeassistant/components/lyric/translations/nl.json index a2569f18dc2..e820174aa5a 100644 --- a/homeassistant/components/lyric/translations/nl.json +++ b/homeassistant/components/lyric/translations/nl.json @@ -14,7 +14,7 @@ }, "reauth_confirm": { "description": "De Lyric-integratie moet uw account opnieuw verifi\u00ebren.", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" } } } diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index 2b486ce79e2..a2f8ba77d78 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -55,7 +55,7 @@ }, "reauth_confirm": { "description": "De Nest-integratie moet uw account opnieuw verifi\u00ebren", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" } } }, diff --git a/homeassistant/components/netatmo/translations/nl.json b/homeassistant/components/netatmo/translations/nl.json index e0b8998d652..80e3379b321 100644 --- a/homeassistant/components/netatmo/translations/nl.json +++ b/homeassistant/components/netatmo/translations/nl.json @@ -16,7 +16,7 @@ }, "reauth_confirm": { "description": "De Netatmo-integratie moet uw account opnieuw verifi\u00ebren", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" } } }, diff --git a/homeassistant/components/notion/translations/nl.json b/homeassistant/components/notion/translations/nl.json index 130d45635b8..fbc4b6ac7be 100644 --- a/homeassistant/components/notion/translations/nl.json +++ b/homeassistant/components/notion/translations/nl.json @@ -14,7 +14,7 @@ "password": "Wachtwoord" }, "description": "Voer het wachtwoord voor {username} opnieuw in.", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" }, "user": { "data": { diff --git a/homeassistant/components/nuki/translations/nl.json b/homeassistant/components/nuki/translations/nl.json index b5c41a05421..3577fad5a01 100644 --- a/homeassistant/components/nuki/translations/nl.json +++ b/homeassistant/components/nuki/translations/nl.json @@ -14,7 +14,7 @@ "token": "Toegangstoken" }, "description": "De Nuki integratie moet opnieuw authenticeren met uw bridge.", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" }, "user": { "data": { diff --git a/homeassistant/components/nws/translations/id.json b/homeassistant/components/nws/translations/id.json index 3c92ebaed64..e4e73efe685 100644 --- a/homeassistant/components/nws/translations/id.json +++ b/homeassistant/components/nws/translations/id.json @@ -15,7 +15,7 @@ "longitude": "Bujur", "station": "Kode stasiun METAR" }, - "description": "Jika kode stasiun METAR tidak ditentukan, informasi lintang dan bujur akan digunakan untuk menemukan stasiun terdekat. Untuk saat ini, Kunci API bisa berupa nilai sebarang. Disarankan untuk menggunakan alamat email yang valid.", + "description": "Jika kode stasiun METAR tidak ditentukan, informasi lintang dan bujur akan digunakan untuk menemukan stasiun terdekat. Untuk saat ini, Kunci API bisa berupa nilai sebarang. Disarankan untuk menggunakan alamat email yang valid.fsilak", "title": "Hubungkan ke National Weather Service" } } diff --git a/homeassistant/components/plaato/translations/id.json b/homeassistant/components/plaato/translations/id.json index 99783fd4a63..b159dec5a13 100644 --- a/homeassistant/components/plaato/translations/id.json +++ b/homeassistant/components/plaato/translations/id.json @@ -20,7 +20,7 @@ "token": "Tempel Token Auth di sini", "use_webhook": "Gunakan webhook" }, - "description": "Untuk dapat melakukan kueri API diperlukan 'auth_token'. Nilai token dapat diperoleh dengan mengikuti [petunjuk ini](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token)\n\nPerangkat yang dipilih: **{device_type}** \n\nJika Anda lebih memilih untuk menggunakan metode webhook bawaan (hanya Airlock), centang centang kotak di bawah ini dan kosongkan nilai Auth Token'", + "description": "Untuk dapat melakukan kueri API diperlukan `auth_token`. Nilai token dapat diperoleh dengan mengikuti [petunjuk ini](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token)\n\nPerangkat yang dipilih: **{device_type}** \n\nJika Anda lebih memilih untuk menggunakan metode webhook bawaan (hanya Airlock), centang centang kotak di bawah ini dan kosongkan nilai Auth Token'", "title": "Pilih metode API" }, "user": { diff --git a/homeassistant/components/plex/translations/nl.json b/homeassistant/components/plex/translations/nl.json index 4d9f30056bd..4f299e8b5c1 100644 --- a/homeassistant/components/plex/translations/nl.json +++ b/homeassistant/components/plex/translations/nl.json @@ -50,7 +50,7 @@ "data": { "ignore_new_shared_users": "Negeer nieuwe beheerde/gedeelde gebruikers", "ignore_plex_web_clients": "Negeer Plex-webclients", - "monitored_users": "Gecontroleerde gebruikers", + "monitored_users": "Gemonitorde gebruikers", "use_episode_art": "Gebruik aflevering kunst" }, "description": "Opties voor Plex-mediaspelers" diff --git a/homeassistant/components/renault/translations/nl.json b/homeassistant/components/renault/translations/nl.json index 212dd860874..1d2066dbdfc 100644 --- a/homeassistant/components/renault/translations/nl.json +++ b/homeassistant/components/renault/translations/nl.json @@ -20,7 +20,7 @@ "password": "Wachtwoord" }, "description": "Werk uw wachtwoord voor {gebruikersnaam} bij", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" }, "user": { "data": { diff --git a/homeassistant/components/ridwell/translations/nl.json b/homeassistant/components/ridwell/translations/nl.json index e230ffec6f8..0bd599ccf19 100644 --- a/homeassistant/components/ridwell/translations/nl.json +++ b/homeassistant/components/ridwell/translations/nl.json @@ -14,7 +14,7 @@ "password": "Wachtwoord" }, "description": "Voer het wachtwoord voor {username} opnieuw in:", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" }, "user": { "data": { diff --git a/homeassistant/components/samsungtv/translations/id.json b/homeassistant/components/samsungtv/translations/id.json index 2dd96798748..9ed190df2b7 100644 --- a/homeassistant/components/samsungtv/translations/id.json +++ b/homeassistant/components/samsungtv/translations/id.json @@ -12,7 +12,7 @@ }, "error": { "auth_missing": "Home Assistant tidak diizinkan untuk tersambung ke TV Samsung ini. Periksa Manajer Perangkat Eksternal TV Anda untuk mengotorisasi Home Assistant.", - "invalid_pin": "PIN tidak valid, silakan coba lagi." + "invalid_pin": "PIN tidak valid, coba lagi." }, "flow_title": "{device}", "step": { diff --git a/homeassistant/components/sense/translations/nl.json b/homeassistant/components/sense/translations/nl.json index 1f2f9e3d26b..3558f4a0c07 100644 --- a/homeassistant/components/sense/translations/nl.json +++ b/homeassistant/components/sense/translations/nl.json @@ -15,7 +15,7 @@ "password": "Wachtwoord" }, "description": "De Sense-integratie moet uw account {email} opnieuw verifi\u00ebren.", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" }, "user": { "data": { diff --git a/homeassistant/components/simplisafe/translations/nl.json b/homeassistant/components/simplisafe/translations/nl.json index 34db358c02b..8c356ed9e12 100644 --- a/homeassistant/components/simplisafe/translations/nl.json +++ b/homeassistant/components/simplisafe/translations/nl.json @@ -18,7 +18,7 @@ "password": "Wachtwoord" }, "description": "Voer het wachtwoord voor {username} opnieuw in.", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" }, "sms_2fa": { "data": { diff --git a/homeassistant/components/sleepiq/translations/nl.json b/homeassistant/components/sleepiq/translations/nl.json index 9b829caa025..4b32ac233b0 100644 --- a/homeassistant/components/sleepiq/translations/nl.json +++ b/homeassistant/components/sleepiq/translations/nl.json @@ -14,7 +14,7 @@ "password": "Wachtwoord" }, "description": "De SleepIQ-integratie moet uw account {username} opnieuw verifi\u00ebren.", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" }, "user": { "data": { diff --git a/homeassistant/components/smarttub/translations/nl.json b/homeassistant/components/smarttub/translations/nl.json index cfe5b601491..ad351ba42e3 100644 --- a/homeassistant/components/smarttub/translations/nl.json +++ b/homeassistant/components/smarttub/translations/nl.json @@ -10,7 +10,7 @@ "step": { "reauth_confirm": { "description": "De SmartTub-integratie moet uw account opnieuw verifi\u00ebren", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" }, "user": { "data": { diff --git a/homeassistant/components/sonarr/translations/nl.json b/homeassistant/components/sonarr/translations/nl.json index 0ef71c521b0..bb310896bed 100644 --- a/homeassistant/components/sonarr/translations/nl.json +++ b/homeassistant/components/sonarr/translations/nl.json @@ -13,7 +13,7 @@ "step": { "reauth_confirm": { "description": "De Sonarr-integratie moet handmatig opnieuw worden geverifieerd met de Sonarr-API die wordt gehost op: {url}", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" }, "user": { "data": { diff --git a/homeassistant/components/spotify/translations/nl.json b/homeassistant/components/spotify/translations/nl.json index 577e70bb0d1..2e478de73ab 100644 --- a/homeassistant/components/spotify/translations/nl.json +++ b/homeassistant/components/spotify/translations/nl.json @@ -15,7 +15,7 @@ }, "reauth_confirm": { "description": "De Spotify integratie moet opnieuw worden geverifieerd met Spotify voor account: {account}", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" } } }, diff --git a/homeassistant/components/steam_online/translations/nl.json b/homeassistant/components/steam_online/translations/nl.json index f94d083b76a..7eafa00e53c 100644 --- a/homeassistant/components/steam_online/translations/nl.json +++ b/homeassistant/components/steam_online/translations/nl.json @@ -13,7 +13,7 @@ "step": { "reauth_confirm": { "description": "De Steam integratie moet handmatig opnieuw geauthenticeerd worden\n\nU kunt uw sleutel hier vinden: {api_key_url}", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" }, "user": { "data": { diff --git a/homeassistant/components/subaru/translations/id.json b/homeassistant/components/subaru/translations/id.json index cf0d37a0c30..35395be56b5 100644 --- a/homeassistant/components/subaru/translations/id.json +++ b/homeassistant/components/subaru/translations/id.json @@ -11,7 +11,7 @@ "incorrect_pin": "PIN salah", "incorrect_validation_code": "Kode validasi salah", "invalid_auth": "Autentikasi tidak valid", - "two_factor_request_failed": "Permintaan kode 2FA gagal, silakan coba lagi" + "two_factor_request_failed": "Permintaan kode 2FA gagal, coba lagi" }, "step": { "pin": { diff --git a/homeassistant/components/tomorrowio/translations/id.json b/homeassistant/components/tomorrowio/translations/id.json index a1a6f41888a..364e697783f 100644 --- a/homeassistant/components/tomorrowio/translations/id.json +++ b/homeassistant/components/tomorrowio/translations/id.json @@ -23,7 +23,7 @@ "data": { "timestep": "Jarak Interval Prakiraan NowCast dalam Menit" }, - "description": "Jika Anda memilih untuk mengaktifkan entitas prakiraan 'nowcast', Anda dapat mengonfigurasi jumlah menit antar-prakiraan. Jumlah prakiraan yang diberikan tergantung pada jumlah menit yang dipilih antar-prakiraan.", + "description": "Jika Anda memilih untuk mengaktifkan entitas prakiraan `nowcast`, Anda dapat mengonfigurasi jumlah menit antar-prakiraan. Jumlah prakiraan yang diberikan tergantung pada jumlah menit yang dipilih antar-prakiraan.", "title": "Perbarui Opsi Tomorrow.io" } } diff --git a/homeassistant/components/totalconnect/translations/nl.json b/homeassistant/components/totalconnect/translations/nl.json index e80030ec06b..cf9a6bb10a1 100644 --- a/homeassistant/components/totalconnect/translations/nl.json +++ b/homeassistant/components/totalconnect/translations/nl.json @@ -19,7 +19,7 @@ }, "reauth_confirm": { "description": "Total Connect moet uw account opnieuw verifi\u00ebren", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" }, "user": { "data": { diff --git a/homeassistant/components/uptimerobot/translations/nl.json b/homeassistant/components/uptimerobot/translations/nl.json index 61a2e8137e5..96db2ba1f23 100644 --- a/homeassistant/components/uptimerobot/translations/nl.json +++ b/homeassistant/components/uptimerobot/translations/nl.json @@ -19,7 +19,7 @@ "api_key": "API-sleutel" }, "description": "U moet een nieuwe 'hoofd'-API-sleutel van UptimeRobot opgeven", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" }, "user": { "data": { diff --git a/homeassistant/components/watttime/translations/nl.json b/homeassistant/components/watttime/translations/nl.json index 47e2fb9c600..09fbfe6307a 100644 --- a/homeassistant/components/watttime/translations/nl.json +++ b/homeassistant/components/watttime/translations/nl.json @@ -28,7 +28,7 @@ "password": "Wachtwoord" }, "description": "Voer het wachtwoord voor {username} opnieuw in:", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" }, "user": { "data": { diff --git a/homeassistant/components/withings/translations/nl.json b/homeassistant/components/withings/translations/nl.json index 04da8b7eb0b..cca755effbb 100644 --- a/homeassistant/components/withings/translations/nl.json +++ b/homeassistant/components/withings/translations/nl.json @@ -26,7 +26,7 @@ }, "reauth": { "description": "Het {profile} \" moet opnieuw worden geverifieerd om Withings-gegevens te blijven ontvangen.", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/nl.json b/homeassistant/components/xiaomi_miio/translations/nl.json index abb63ec15c5..cc077839808 100644 --- a/homeassistant/components/xiaomi_miio/translations/nl.json +++ b/homeassistant/components/xiaomi_miio/translations/nl.json @@ -40,7 +40,7 @@ }, "reauth_confirm": { "description": "De Xiaomi Miio-integratie moet uw account opnieuw verifi\u00ebren om de tokens bij te werken of ontbrekende cloudreferenties toe te voegen.", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" }, "select": { "data": { diff --git a/homeassistant/components/yolink/translations/nl.json b/homeassistant/components/yolink/translations/nl.json index 401738dc381..55e8a016ff5 100644 --- a/homeassistant/components/yolink/translations/nl.json +++ b/homeassistant/components/yolink/translations/nl.json @@ -18,7 +18,7 @@ }, "reauth_confirm": { "description": "De yolink-integratie moet opnieuw inloggen bij uw account", - "title": "Integratie herauthentiseren" + "title": "Integratie herauthenticeren" } } } From 7649adde5f28bbc2d05d57a6f4cdd80d88cd6b76 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Mon, 23 May 2022 03:41:54 +0200 Subject: [PATCH 0757/3516] Fix here_travel_time config_flow import (#72313) * Handle import of entity_namespace * Update homeassistant/components/here_travel_time/config_flow.py Co-authored-by: Allen Porter * Fix mypy Co-authored-by: Allen Porter --- .../here_travel_time/config_flow.py | 60 +++++++++++-------- .../here_travel_time/test_config_flow.py | 8 ++- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py index bc6d57aa892..502eee68546 100644 --- a/homeassistant/components/here_travel_time/config_flow.py +++ b/homeassistant/components/here_travel_time/config_flow.py @@ -8,7 +8,13 @@ from herepy import HEREError, InvalidCredentialsError, RouteMode, RoutingApi import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_API_KEY, CONF_MODE, CONF_NAME, CONF_UNIT_SYSTEM +from homeassistant.const import ( + CONF_API_KEY, + CONF_ENTITY_NAMESPACE, + CONF_MODE, + CONF_NAME, + CONF_UNIT_SYSTEM, +) from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv @@ -231,9 +237,7 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_show_form(step_id="destination_entity", data_schema=schema) - async def async_step_import( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: """Import from configuration.yaml.""" options: dict[str, Any] = {} user_input, options = self._transform_import_input(user_input) @@ -249,39 +253,47 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) def _transform_import_input( - self, user_input + self, import_input: dict[str, Any] ) -> tuple[dict[str, Any], dict[str, Any]]: """Transform platform schema input to new model.""" options: dict[str, Any] = {} - if user_input.get(CONF_ORIGIN_LATITUDE) is not None: - user_input[CONF_ORIGIN_LATITUDE] = user_input.pop(CONF_ORIGIN_LATITUDE) - user_input[CONF_ORIGIN_LONGITUDE] = user_input.pop(CONF_ORIGIN_LONGITUDE) - else: - user_input[CONF_ORIGIN_ENTITY_ID] = user_input.pop(CONF_ORIGIN_ENTITY_ID) + user_input: dict[str, Any] = {} - if user_input.get(CONF_DESTINATION_LATITUDE) is not None: - user_input[CONF_DESTINATION_LATITUDE] = user_input.pop( - CONF_DESTINATION_LATITUDE - ) - user_input[CONF_DESTINATION_LONGITUDE] = user_input.pop( - CONF_DESTINATION_LONGITUDE - ) + if import_input.get(CONF_ORIGIN_LATITUDE) is not None: + user_input[CONF_ORIGIN_LATITUDE] = import_input[CONF_ORIGIN_LATITUDE] + user_input[CONF_ORIGIN_LONGITUDE] = import_input[CONF_ORIGIN_LONGITUDE] else: - user_input[CONF_DESTINATION_ENTITY_ID] = user_input.pop( + user_input[CONF_ORIGIN_ENTITY_ID] = import_input[CONF_ORIGIN_ENTITY_ID] + + if import_input.get(CONF_DESTINATION_LATITUDE) is not None: + user_input[CONF_DESTINATION_LATITUDE] = import_input[ + CONF_DESTINATION_LATITUDE + ] + user_input[CONF_DESTINATION_LONGITUDE] = import_input[ + CONF_DESTINATION_LONGITUDE + ] + else: + user_input[CONF_DESTINATION_ENTITY_ID] = import_input[ CONF_DESTINATION_ENTITY_ID - ) + ] + + user_input[CONF_API_KEY] = import_input[CONF_API_KEY] + user_input[CONF_MODE] = import_input[CONF_MODE] + user_input[CONF_NAME] = import_input[CONF_NAME] + if (namespace := import_input.get(CONF_ENTITY_NAMESPACE)) is not None: + user_input[CONF_NAME] = f"{namespace} {user_input[CONF_NAME]}" options[CONF_TRAFFIC_MODE] = ( TRAFFIC_MODE_ENABLED - if user_input.pop(CONF_TRAFFIC_MODE, False) + if import_input.get(CONF_TRAFFIC_MODE, False) else TRAFFIC_MODE_DISABLED ) - options[CONF_ROUTE_MODE] = user_input.pop(CONF_ROUTE_MODE) - options[CONF_UNIT_SYSTEM] = user_input.pop( + options[CONF_ROUTE_MODE] = import_input.get(CONF_ROUTE_MODE) + options[CONF_UNIT_SYSTEM] = import_input.get( CONF_UNIT_SYSTEM, self.hass.config.units.name ) - options[CONF_ARRIVAL_TIME] = user_input.pop(CONF_ARRIVAL, None) - options[CONF_DEPARTURE_TIME] = user_input.pop(CONF_DEPARTURE, None) + options[CONF_ARRIVAL_TIME] = import_input.get(CONF_ARRIVAL, None) + options[CONF_DEPARTURE_TIME] = import_input.get(CONF_DEPARTURE, None) return user_input, options diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py index 105128c7cfb..350e1ae36f6 100644 --- a/tests/components/here_travel_time/test_config_flow.py +++ b/tests/components/here_travel_time/test_config_flow.py @@ -29,8 +29,10 @@ from homeassistant.components.here_travel_time.sensor import ( ) from homeassistant.const import ( CONF_API_KEY, + CONF_ENTITY_NAMESPACE, CONF_MODE, CONF_NAME, + CONF_SCAN_INTERVAL, CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, @@ -416,16 +418,18 @@ async def test_import_flow_entity_id(hass: HomeAssistant) -> None: CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, + CONF_ENTITY_NAMESPACE: "namespace", + CONF_SCAN_INTERVAL: 2678400, }, ) await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "test_name" + assert result["title"] == "namespace test_name" entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.data == { - CONF_NAME: "test_name", + CONF_NAME: "namespace test_name", CONF_API_KEY: CONF_API_KEY, CONF_ORIGIN_ENTITY_ID: "sensor.origin", CONF_DESTINATION_ENTITY_ID: "sensor.destination", From 3ee5445b26a8c48b869a36c5a465734f419af56f Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 23 May 2022 04:43:21 +0200 Subject: [PATCH 0758/3516] Fix camera entity update coordinator subscription in Synology DSM (#72248) fix camera entity subscription to coordinator --- homeassistant/components/synology_dsm/camera.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index cab2536187c..0a6934b45a7 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -154,6 +154,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity, Camera): async def async_added_to_hass(self) -> None: """Subscribe to signal.""" self._listen_source_updates() + await super().async_added_to_hass() def camera_image( self, width: int | None = None, height: int | None = None From 4e4c745c4f0ccb130a23f8122eaa68a7a1c95b9e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 04:44:11 +0200 Subject: [PATCH 0759/3516] Adjust setup type hints in honeywell (#72226) --- homeassistant/components/honeywell/climate.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 5c238d73e0b..11b899dc0f5 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -21,7 +21,10 @@ from homeassistant.components.climate.const import ( HVACAction, HVACMode, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( _LOGGER, @@ -70,12 +73,14 @@ HW_FAN_MODE_TO_HA = { PARALLEL_UPDATES = 1 -async def async_setup_entry(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: """Set up the Honeywell thermostat.""" - cool_away_temp = config.options.get(CONF_COOL_AWAY_TEMPERATURE) - heat_away_temp = config.options.get(CONF_HEAT_AWAY_TEMPERATURE) + cool_away_temp = entry.options.get(CONF_COOL_AWAY_TEMPERATURE) + heat_away_temp = entry.options.get(CONF_HEAT_AWAY_TEMPERATURE) - data = hass.data[DOMAIN][config.entry_id] + data = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ From e9ffcad4d1c6fd30babad6d3b2554f4fe33b93ca Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 04:46:46 +0200 Subject: [PATCH 0760/3516] Adjust setup type hints in opnsense (#72225) --- homeassistant/components/opnsense/device_tracker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/opnsense/device_tracker.py b/homeassistant/components/opnsense/device_tracker.py index 9a024b2b260..e726a0484f9 100644 --- a/homeassistant/components/opnsense/device_tracker.py +++ b/homeassistant/components/opnsense/device_tracker.py @@ -1,10 +1,12 @@ """Device tracker support for OPNSense routers.""" from homeassistant.components.device_tracker import DeviceScanner +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType from . import CONF_TRACKER_INTERFACE, OPNSENSE_DATA -async def async_get_scanner(hass, config, discovery_info=None): +async def async_get_scanner(hass: HomeAssistant, config: ConfigType) -> DeviceScanner: """Configure the OPNSense device_tracker.""" interface_client = hass.data[OPNSENSE_DATA]["interfaces"] scanner = OPNSenseDeviceScanner( From 4dbc1ed7a79cf43f08ea948b03fa9c29cbf94d67 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 May 2022 23:29:30 -0500 Subject: [PATCH 0761/3516] Add dialect (database engine) and version to recorder system health data (#72339) --- homeassistant/components/recorder/core.py | 7 ++++-- .../components/recorder/strings.json | 4 +++- .../recorder/system_health/__init__.py | 23 +++++++++++++++---- .../components/recorder/translations/en.json | 2 ++ homeassistant/components/recorder/util.py | 5 +++- .../components/recorder/test_system_health.py | 6 +++++ 6 files changed, 39 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 716e3a19d09..2ba07e42f49 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -12,6 +12,7 @@ import threading import time from typing import Any, TypeVar, cast +from awesomeversion import AwesomeVersion from lru import LRU # pylint: disable=no-name-in-module from sqlalchemy import create_engine, event as sqlalchemy_event, exc, func, select from sqlalchemy.engine import Engine @@ -159,6 +160,7 @@ class Recorder(threading.Thread): self.db_url = uri self.db_max_retries = db_max_retries self.db_retry_wait = db_retry_wait + self.engine_version: AwesomeVersion | None = None self.async_db_ready: asyncio.Future[bool] = asyncio.Future() self.async_recorder_ready = asyncio.Event() self._queue_watch = threading.Event() @@ -1009,12 +1011,13 @@ class Recorder(threading.Thread): ) -> None: """Dbapi specific connection settings.""" assert self.engine is not None - setup_connection_for_dialect( + if version := setup_connection_for_dialect( self, self.engine.dialect.name, dbapi_connection, not self._completed_first_database_setup, - ) + ): + self.engine_version = version self._completed_first_database_setup = True if self.db_url == SQLITE_URL_PREFIX or ":memory:" in self.db_url: diff --git a/homeassistant/components/recorder/strings.json b/homeassistant/components/recorder/strings.json index b475b29a16a..9b616372adf 100644 --- a/homeassistant/components/recorder/strings.json +++ b/homeassistant/components/recorder/strings.json @@ -3,7 +3,9 @@ "info": { "oldest_recorder_run": "Oldest Run Start Time", "current_recorder_run": "Current Run Start Time", - "estimated_db_size": "Estimated Database Size (MiB)" + "estimated_db_size": "Estimated Database Size (MiB)", + "database_engine": "Database Engine", + "database_version": "Database Version" } } } diff --git a/homeassistant/components/recorder/system_health/__init__.py b/homeassistant/components/recorder/system_health/__init__.py index 3250aecf356..8ba68a1649b 100644 --- a/homeassistant/components/recorder/system_health/__init__.py +++ b/homeassistant/components/recorder/system_health/__init__.py @@ -44,17 +44,32 @@ def _get_db_stats(instance: Recorder, database_name: str) -> dict[str, Any]: return db_stats +@callback +def _async_get_db_engine_info(instance: Recorder) -> dict[str, Any]: + """Get database engine info.""" + db_engine_info: dict[str, Any] = {} + if dialect_name := instance.dialect_name: + db_engine_info["database_engine"] = dialect_name.value + if engine_version := instance.engine_version: + db_engine_info["database_version"] = str(engine_version) + return db_engine_info + + async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: """Get info for the info page.""" instance = get_instance(hass) + run_history = instance.run_history database_name = URL(instance.db_url).path.lstrip("/") + db_engine_info = _async_get_db_engine_info(instance) db_stats: dict[str, Any] = {} + if instance.async_db_ready.done(): db_stats = await instance.async_add_executor_job( _get_db_stats, instance, database_name ) - return { - "oldest_recorder_run": run_history.first.start, - "current_recorder_run": run_history.current.start, - } | db_stats + db_runs = { + "oldest_recorder_run": run_history.first.start, + "current_recorder_run": run_history.current.start, + } + return db_runs | db_stats | db_engine_info diff --git a/homeassistant/components/recorder/translations/en.json b/homeassistant/components/recorder/translations/en.json index fb802ff7ff9..c9ceffc7397 100644 --- a/homeassistant/components/recorder/translations/en.json +++ b/homeassistant/components/recorder/translations/en.json @@ -2,6 +2,8 @@ "system_health": { "info": { "current_recorder_run": "Current Run Start Time", + "database_engine": "Database Engine", + "database_version": "Database Version", "estimated_db_size": "Estimated Database Size (MiB)", "oldest_recorder_run": "Oldest Run Start Time" } diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index b010ddfa853..843f0e4b185 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -396,8 +396,9 @@ def setup_connection_for_dialect( dialect_name: str, dbapi_connection: Any, first_connection: bool, -) -> None: +) -> AwesomeVersion | None: """Execute statements needed for dialect connection.""" + version: AwesomeVersion | None = None if dialect_name == SupportedDialect.SQLITE: if first_connection: old_isolation = dbapi_connection.isolation_level @@ -465,6 +466,8 @@ def setup_connection_for_dialect( else: _fail_unsupported_dialect(dialect_name) + return version + def end_incomplete_runs(session: Session, start_time: datetime) -> None: """End any incomplete recorder runs.""" diff --git a/tests/components/recorder/test_system_health.py b/tests/components/recorder/test_system_health.py index 8a8d9ea4e80..80997b9df36 100644 --- a/tests/components/recorder/test_system_health.py +++ b/tests/components/recorder/test_system_health.py @@ -24,6 +24,8 @@ async def test_recorder_system_health(hass, recorder_mock): "current_recorder_run": instance.run_history.current.start, "oldest_recorder_run": instance.run_history.first.start, "estimated_db_size": ANY, + "database_engine": SupportedDialect.SQLITE.value, + "database_version": ANY, } @@ -46,6 +48,8 @@ async def test_recorder_system_health_alternate_dbms(hass, recorder_mock, dialec "current_recorder_run": instance.run_history.current.start, "oldest_recorder_run": instance.run_history.first.start, "estimated_db_size": "1.00 MiB", + "database_engine": dialect_name.value, + "database_version": ANY, } @@ -62,4 +66,6 @@ async def test_recorder_system_health_crashed_recorder_runs_table( "current_recorder_run": instance.run_history.current.start, "oldest_recorder_run": instance.run_history.current.start, "estimated_db_size": ANY, + "database_engine": SupportedDialect.SQLITE.value, + "database_version": ANY, } From 20960e182d213f6ae7e364f9ce0742cadbf83ba3 Mon Sep 17 00:00:00 2001 From: Zac West <74188+zacwest@users.noreply.github.com> Date: Sun, 22 May 2022 22:41:51 -0700 Subject: [PATCH 0762/3516] Log unknown websocket commands at info instead of error (#72336) --- homeassistant/components/websocket_api/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index cdcee408070..0280863f83e 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -93,7 +93,7 @@ class ActiveConnection: return if msg["type"] not in handlers: - self.logger.error("Received invalid command: {}".format(msg["type"])) + self.logger.info("Received unknown command: {}".format(msg["type"])) self.send_message( messages.error_message( cur_id, const.ERR_UNKNOWN_COMMAND, "Unknown command." From 79fb5e1bec90a9b1b313419702ea620172dd0849 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 23 May 2022 13:54:49 +0800 Subject: [PATCH 0763/3516] Add use wallclock as timestamps option to onvif (#71983) --- homeassistant/components/onvif/camera.py | 8 ++++++- homeassistant/components/onvif/config_flow.py | 21 ++++++++++++++++++- tests/components/onvif/test_config_flow.py | 6 +++++- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index e46e1a2ac59..6aa7ae42767 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -9,7 +9,10 @@ from yarl import URL from homeassistant.components import ffmpeg from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, get_ffmpeg_manager -from homeassistant.components.stream import CONF_RTSP_TRANSPORT +from homeassistant.components.stream import ( + CONF_RTSP_TRANSPORT, + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import HTTP_BASIC_AUTHENTICATION from homeassistant.core import HomeAssistant @@ -97,6 +100,9 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): self.stream_options[CONF_RTSP_TRANSPORT] = device.config_entry.options.get( CONF_RTSP_TRANSPORT ) + self.stream_options[ + CONF_USE_WALLCLOCK_AS_TIMESTAMPS + ] = device.config_entry.options.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS, False) self._basic_auth = ( device.config_entry.data.get(CONF_SNAPSHOT_AUTH) == HTTP_BASIC_AUTHENTICATION diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index 894f2fee3df..1ee0be18467 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -13,7 +13,11 @@ from zeep.exceptions import Fault from homeassistant import config_entries from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS -from homeassistant.components.stream import CONF_RTSP_TRANSPORT, RTSP_TRANSPORTS +from homeassistant.components.stream import ( + CONF_RTSP_TRANSPORT, + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, + RTSP_TRANSPORTS, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -272,8 +276,22 @@ class OnvifOptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: self.options[CONF_EXTRA_ARGUMENTS] = user_input[CONF_EXTRA_ARGUMENTS] self.options[CONF_RTSP_TRANSPORT] = user_input[CONF_RTSP_TRANSPORT] + self.options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = user_input.get( + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, + self.config_entry.options.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS, False), + ) return self.async_create_entry(title="", data=self.options) + advanced_options = {} + if self.show_advanced_options: + advanced_options[ + vol.Optional( + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, + default=self.config_entry.options.get( + CONF_USE_WALLCLOCK_AS_TIMESTAMPS, False + ), + ) + ] = bool return self.async_show_form( step_id="onvif_devices", data_schema=vol.Schema( @@ -290,6 +308,7 @@ class OnvifOptionsFlowHandler(config_entries.OptionsFlow): CONF_RTSP_TRANSPORT, next(iter(RTSP_TRANSPORTS)) ), ): vol.In(RTSP_TRANSPORTS), + **advanced_options, } ), ) diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index 1e4dabfe7cf..fec1e2b8132 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -314,7 +314,9 @@ async def test_option_flow(hass): """Test config flow options.""" entry, _, _ = await setup_onvif_integration(hass) - result = await hass.config_entries.options.async_init(entry.entry_id) + result = await hass.config_entries.options.async_init( + entry.entry_id, context={"show_advanced_options": True} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "onvif_devices" @@ -324,6 +326,7 @@ async def test_option_flow(hass): user_input={ config_flow.CONF_EXTRA_ARGUMENTS: "", config_flow.CONF_RTSP_TRANSPORT: list(config_flow.RTSP_TRANSPORTS)[1], + config_flow.CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, }, ) @@ -331,4 +334,5 @@ async def test_option_flow(hass): assert result["data"] == { config_flow.CONF_EXTRA_ARGUMENTS: "", config_flow.CONF_RTSP_TRANSPORT: list(config_flow.RTSP_TRANSPORTS)[1], + config_flow.CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, } From f965f542a316c9ed60e11d9fdb9a14d031ab21b0 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 23 May 2022 13:56:03 +0800 Subject: [PATCH 0764/3516] Always set use wallclock as timestamps in ezviz (#71984) --- homeassistant/components/ezviz/camera.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 2fa410e268c..91bab5f83af 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.components import ffmpeg from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.components.ffmpeg import get_ffmpeg_manager +from homeassistant.components.stream import CONF_USE_WALLCLOCK_AS_TIMESTAMPS from homeassistant.config_entries import ( SOURCE_IGNORE, SOURCE_INTEGRATION_DISCOVERY, @@ -186,6 +187,7 @@ class EzvizCamera(EzvizEntity, Camera): """Initialize a Ezviz security camera.""" super().__init__(coordinator, serial) Camera.__init__(self) + self.stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True self._username = camera_username self._password = camera_password self._rtsp_stream = camera_rtsp_stream From c2612d1adec64fc530819e864c503d6661c096b0 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 23 May 2022 13:56:26 +0800 Subject: [PATCH 0765/3516] Remove cache control headers from stream (#71996) --- homeassistant/components/stream/hls.py | 24 +----------------------- tests/components/stream/conftest.py | 2 +- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 44b19d2cc85..23584b59fb9 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -215,16 +215,6 @@ class HlsPlaylistView(StreamView): return web.Response( body=None, 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 - # be cached for four Target Durations. Successful responses to non-blocking - # Playlist requests should be cached for half the Target Duration. - # Unsuccessful responses to non-blocking Playlist requests should be - # cached for for one Target Duration. - headers={ - "Cache-Control": f"max-age={(4 if blocking else 1)*target_duration:.0f}" - }, ) @staticmethod @@ -233,9 +223,6 @@ class HlsPlaylistView(StreamView): return web.Response( body=None, status=HTTPStatus.NOT_FOUND, - headers={ - "Cache-Control": f"max-age={(4 if blocking else 1)*target_duration:.0f}" - }, ) async def handle( @@ -318,7 +305,6 @@ class HlsPlaylistView(StreamView): body=self.render(track).encode("utf-8"), headers={ "Content-Type": FORMAT_CONTENT_TYPE[HLS_PROVIDER], - "Cache-Control": f"max-age={(6 if blocking_request else 0.5)*track.target_duration:.0f}", }, ) response.enable_compression(web.ContentCoding.gzip) @@ -373,22 +359,16 @@ class HlsPartView(StreamView): return web.Response( body=None, status=HTTPStatus.NOT_FOUND, - headers={"Cache-Control": f"max-age={track.target_duration:.0f}"}, ) # If the part is ready or has been hinted, if int(part_num) == len(segment.parts): await track.part_recv(timeout=track.stream_settings.hls_part_timeout) if int(part_num) >= len(segment.parts): - return web.HTTPRequestRangeNotSatisfiable( - headers={ - "Cache-Control": f"max-age={track.target_duration:.0f}", - } - ) + return web.HTTPRequestRangeNotSatisfiable() return web.Response( body=segment.parts[int(part_num)].data, headers={ "Content-Type": "video/iso.segment", - "Cache-Control": f"max-age={6*track.target_duration:.0f}", }, ) @@ -421,12 +401,10 @@ class HlsSegmentView(StreamView): return web.Response( body=None, status=HTTPStatus.NOT_FOUND, - headers={"Cache-Control": f"max-age={track.target_duration:.0f}"}, ) return web.Response( body=segment.get_data(), headers={ "Content-Type": "video/iso.segment", - "Cache-Control": f"max-age={6*track.target_duration:.0f}", }, ) diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index 407b144267b..a3d2da8bd52 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -176,7 +176,7 @@ class HLSSync: self.check_requests_ready() return self._original_not_found() - def response(self, body, headers, status=HTTPStatus.OK): + def response(self, body, headers=None, status=HTTPStatus.OK): """Intercept the Response call so we know when the web handler is finished.""" self._num_finished += 1 self.check_requests_ready() From 623abb11474e8b69314bbc062a5b3391f11eef06 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 23 May 2022 08:03:09 +0200 Subject: [PATCH 0766/3516] Move manual configuration of MQTT number to the integration key (#72272) Add number --- homeassistant/components/mqtt/__init__.py | 1 + homeassistant/components/mqtt/number.py | 29 +++++++++++++++++++---- tests/components/mqtt/test_number.py | 14 +++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 426fad45db1..36c39e5b782 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -202,6 +202,7 @@ PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( vol.Optional(Platform.LOCK.value): cv.ensure_list, vol.Optional(Platform.SWITCH.value): cv.ensure_list, vol.Optional(Platform.VACUUM.value): cv.ensure_list, + vol.Optional(Platform.NUMBER.value): cv.ensure_list, } ) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index e13ae4ded84..001f9f4f668 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -1,6 +1,7 @@ """Configure number in a device through MQTT topic.""" from __future__ import annotations +import asyncio import functools import logging @@ -40,8 +41,10 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_get_platform_config_from_yaml, async_setup_entry_helper, async_setup_platform_helper, + warn_for_legacy_schema, ) _LOGGER = logging.getLogger(__name__) @@ -72,7 +75,7 @@ def validate_config(config): return config -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): vol.Coerce(float), @@ -88,11 +91,18 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( }, ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -PLATFORM_SCHEMA = vol.All( +PLATFORM_SCHEMA_MODERN = vol.All( _PLATFORM_SCHEMA_BASE, validate_config, ) +# Configuring MQTT Number under the number platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA = vol.All( + cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), + validate_config, + warn_for_legacy_schema(number.DOMAIN), +) + DISCOVERY_SCHEMA = vol.All( _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), validate_config, @@ -105,7 +115,8 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up MQTT number through configuration.yaml.""" + """Set up MQTT number configured under the number platform key (deprecated).""" + # Deprecated in HA Core 2022.6 await async_setup_platform_helper( hass, number.DOMAIN, config, async_add_entities, _async_setup_entity ) @@ -116,7 +127,17 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT number dynamically through MQTT discovery.""" + """Set up MQTT number through configuration.yaml and dynamically through MQTT discovery.""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, number.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 73c1da357fa..4eb8fdec351 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -1,4 +1,5 @@ """The tests for mqtt number component.""" +import copy import json from unittest.mock import patch @@ -50,6 +51,7 @@ from .test_common import ( help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -729,3 +731,15 @@ async def test_encoding_subscribable_topics( attribute, attribute_value, ) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = number.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None From c4893f96f60ab9f05215040c21526aa47ea0803f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 23 May 2022 08:08:08 +0200 Subject: [PATCH 0767/3516] Move manual configuration of MQTT siren to the integration key (#72278) Add siren --- homeassistant/components/mqtt/__init__.py | 1 + homeassistant/components/mqtt/siren.py | 28 +++++++++++++++++++---- tests/components/mqtt/test_siren.py | 13 +++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 36c39e5b782..dff5e75fa23 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -200,6 +200,7 @@ PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( vol.Optional(Platform.HUMIDIFIER.value): cv.ensure_list, vol.Optional(Platform.LIGHT.value): cv.ensure_list, vol.Optional(Platform.LOCK.value): cv.ensure_list, + vol.Optional(Platform.SIREN.value): cv.ensure_list, vol.Optional(Platform.SWITCH.value): cv.ensure_list, vol.Optional(Platform.VACUUM.value): cv.ensure_list, vol.Optional(Platform.NUMBER.value): cv.ensure_list, diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index beed5b51e20..c3a41c3618e 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -1,6 +1,7 @@ """Support for MQTT sirens.""" from __future__ import annotations +import asyncio import copy import functools import json @@ -51,8 +52,10 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_get_platform_config_from_yaml, async_setup_entry_helper, async_setup_platform_helper, + warn_for_legacy_schema, ) DEFAULT_NAME = "MQTT Siren" @@ -71,7 +74,7 @@ CONF_SUPPORT_VOLUME_SET = "support_volume_set" STATE = "state" -PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_AVAILABLE_TONES): cv.ensure_list, vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, @@ -88,7 +91,13 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( }, ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -DISCOVERY_SCHEMA = vol.All(PLATFORM_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA)) +# Configuring MQTT Sirens under the siren platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA = vol.All( + cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_MODERN.schema), + warn_for_legacy_schema(siren.DOMAIN), +) + +DISCOVERY_SCHEMA = vol.All(PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA)) MQTT_SIREN_ATTRIBUTES_BLOCKED = frozenset( { @@ -116,7 +125,8 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up MQTT siren through configuration.yaml.""" + """Set up MQTT sirens configured under the fan platform key (deprecated).""" + # Deprecated in HA Core 2022.6 await async_setup_platform_helper( hass, siren.DOMAIN, config, async_add_entities, _async_setup_entity ) @@ -127,7 +137,17 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT siren dynamically through MQTT discovery.""" + """Set up MQTT siren through configuration.yaml and dynamically through MQTT discovery.""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, siren.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index f39154badc9..197ed34b7e4 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -42,6 +42,7 @@ from .test_common import ( help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -895,3 +896,15 @@ async def test_encoding_subscribable_topics( attribute, attribute_value, ) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = siren.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None From cf5e21a996818d4273cb107f1de5c91ac69ab4e9 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Sun, 22 May 2022 23:43:42 -0700 Subject: [PATCH 0768/3516] Use properties of wemo Insight device (#72316) --- .../components/wemo/binary_sensor.py | 5 +- homeassistant/components/wemo/switch.py | 43 +++++-------- tests/components/wemo/conftest.py | 27 +++++--- tests/components/wemo/test_binary_sensor.py | 21 ++----- tests/components/wemo/test_sensor.py | 15 ----- tests/components/wemo/test_switch.py | 63 ++++++++++++++++++- 6 files changed, 103 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index cde13d632fe..3e7b159dc63 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -2,7 +2,7 @@ import asyncio from typing import cast -from pywemo import Insight, Maker +from pywemo import Insight, Maker, StandbyState from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.config_entries import ConfigEntry @@ -64,4 +64,5 @@ class InsightBinarySensor(WemoBinarySensor): @property def is_on(self) -> bool: """Return true device connected to the Insight Switch is on.""" - return super().is_on and self.wemo.insight_params["state"] == "1" + # Note: wemo.get_standby_state is a @property. + return super().is_on and self.wemo.get_standby_state == StandbyState.ON diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index d3c6264ab89..48cc1d92d67 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -5,7 +5,7 @@ import asyncio from datetime import datetime, timedelta from typing import Any, cast -from pywemo import CoffeeMaker, Insight, Maker +from pywemo import CoffeeMaker, Insight, Maker, StandbyState from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -13,7 +13,6 @@ from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOW from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import convert from .const import DOMAIN as WEMO_DOMAIN from .entity import WemoBinaryStateEntity @@ -22,19 +21,18 @@ from .wemo_device import DeviceCoordinator SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 0 -# The WEMO_ constants below come from pywemo itself +ATTR_COFFEMAKER_MODE = "coffeemaker_mode" +ATTR_CURRENT_STATE_DETAIL = "state_detail" +ATTR_ON_LATEST_TIME = "on_latest_time" +ATTR_ON_TODAY_TIME = "on_today_time" +ATTR_ON_TOTAL_TIME = "on_total_time" +ATTR_POWER_THRESHOLD = "power_threshold_w" ATTR_SENSOR_STATE = "sensor_state" ATTR_SWITCH_MODE = "switch_mode" -ATTR_CURRENT_STATE_DETAIL = "state_detail" -ATTR_COFFEMAKER_MODE = "coffeemaker_mode" MAKER_SWITCH_MOMENTARY = "momentary" MAKER_SWITCH_TOGGLE = "toggle" -WEMO_ON = 1 -WEMO_OFF = 0 -WEMO_STANDBY = 8 - async def async_setup_entry( hass: HomeAssistant, @@ -83,20 +81,10 @@ class WemoSwitch(WemoBinaryStateEntity, SwitchEntity): attr[ATTR_CURRENT_STATE_DETAIL] = self.detail_state if isinstance(self.wemo, Insight): - attr["on_latest_time"] = WemoSwitch.as_uptime( - self.wemo.insight_params.get("onfor", 0) - ) - attr["on_today_time"] = WemoSwitch.as_uptime( - self.wemo.insight_params.get("ontoday", 0) - ) - attr["on_total_time"] = WemoSwitch.as_uptime( - self.wemo.insight_params.get("ontotal", 0) - ) - threshold = convert( - self.wemo.insight_params.get("powerthreshold"), float, 0.0 - ) - assert isinstance(threshold, float) - attr["power_threshold_w"] = threshold / 1000.0 + attr[ATTR_ON_LATEST_TIME] = self.as_uptime(self.wemo.on_for) + attr[ATTR_ON_TODAY_TIME] = self.as_uptime(self.wemo.today_on_time) + attr[ATTR_ON_TOTAL_TIME] = self.as_uptime(self.wemo.total_on_time) + attr[ATTR_POWER_THRESHOLD] = self.wemo.threshold_power_watts if isinstance(self.wemo, CoffeeMaker): attr[ATTR_COFFEMAKER_MODE] = self.wemo.mode @@ -117,12 +105,13 @@ class WemoSwitch(WemoBinaryStateEntity, SwitchEntity): if isinstance(self.wemo, CoffeeMaker): return cast(str, self.wemo.mode_string) if isinstance(self.wemo, Insight): - standby_state = int(self.wemo.insight_params.get("state", 0)) - if standby_state == WEMO_ON: + # Note: wemo.get_standby_state is a @property. + standby_state = self.wemo.get_standby_state + if standby_state == StandbyState.ON: return STATE_ON - if standby_state == WEMO_OFF: + if standby_state == StandbyState.OFF: return STATE_OFF - if standby_state == WEMO_STANDBY: + if standby_state == StandbyState.STANDBY: return STATE_STANDBY return STATE_UNKNOWN assert False # Unreachable code statement. diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index af7002a2500..fbb2f186cf5 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -1,5 +1,6 @@ """Fixtures for pywemo.""" import asyncio +import contextlib from unittest.mock import create_autospec, patch import pytest @@ -52,9 +53,9 @@ def pywemo_discovery_responder_fixture(): yield -@pytest.fixture(name="pywemo_device") -def pywemo_device_fixture(pywemo_registry, pywemo_model): - """Fixture for WeMoDevice instances.""" +@contextlib.contextmanager +def create_pywemo_device(pywemo_registry, pywemo_model): + """Create a WeMoDevice instance.""" cls = getattr(pywemo, pywemo_model) device = create_autospec(cls, instance=True) device.host = MOCK_HOST @@ -83,15 +84,21 @@ def pywemo_device_fixture(pywemo_registry, pywemo_model): yield device +@pytest.fixture(name="pywemo_device") +def pywemo_device_fixture(pywemo_registry, pywemo_model): + """Fixture for WeMoDevice instances.""" + with create_pywemo_device(pywemo_registry, pywemo_model) as pywemo_device: + yield pywemo_device + + @pytest.fixture(name="wemo_entity_suffix") def wemo_entity_suffix_fixture(): """Fixture to select a specific entity for wemo_entity.""" return "" -@pytest.fixture(name="wemo_entity") -async def async_wemo_entity_fixture(hass, pywemo_device, wemo_entity_suffix): - """Fixture for a Wemo entity in hass.""" +async def async_create_wemo_entity(hass, pywemo_device, wemo_entity_suffix): + """Create a hass entity for a wemo device.""" assert await async_setup_component( hass, DOMAIN, @@ -106,7 +113,13 @@ async def async_wemo_entity_fixture(hass, pywemo_device, wemo_entity_suffix): entity_registry = er.async_get(hass) for entry in entity_registry.entities.values(): - if entry.entity_id.endswith(wemo_entity_suffix): + if entry.entity_id.endswith(wemo_entity_suffix or pywemo_device.name.lower()): return entry return None + + +@pytest.fixture(name="wemo_entity") +async def async_wemo_entity_fixture(hass, pywemo_device, wemo_entity_suffix): + """Fixture for a Wemo entity in hass.""" + return await async_create_wemo_entity(hass, pywemo_device, wemo_entity_suffix) diff --git a/tests/components/wemo/test_binary_sensor.py b/tests/components/wemo/test_binary_sensor.py index 481a6348688..eccc1b180a5 100644 --- a/tests/components/wemo/test_binary_sensor.py +++ b/tests/components/wemo/test_binary_sensor.py @@ -1,6 +1,7 @@ """Tests for the Wemo binary_sensor entity.""" import pytest +from pywemo import StandbyState from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, @@ -123,41 +124,27 @@ class TestInsight(EntityTestHelpers): """Select the InsightBinarySensor entity.""" return InsightBinarySensor._name_suffix.lower() - @pytest.fixture(name="pywemo_device") - def pywemo_device_fixture(self, pywemo_device): - """Fixture for WeMoDevice instances.""" - pywemo_device.insight_params = { - "currentpower": 1.0, - "todaymw": 200000000.0, - "state": "0", - "onfor": 0, - "ontoday": 0, - "ontotal": 0, - "powerthreshold": 0, - } - yield pywemo_device - async def test_registry_state_callback( self, hass, pywemo_registry, pywemo_device, wemo_entity ): """Verify that the binary_sensor receives state updates from the registry.""" # On state. pywemo_device.get_state.return_value = 1 - pywemo_device.insight_params["state"] = "1" + pywemo_device.get_standby_state = StandbyState.ON pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") await hass.async_block_till_done() assert hass.states.get(wemo_entity.entity_id).state == STATE_ON # Standby (Off) state. pywemo_device.get_state.return_value = 1 - pywemo_device.insight_params["state"] = "8" + pywemo_device.get_standby_state = StandbyState.STANDBY pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") await hass.async_block_till_done() assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF # Off state. pywemo_device.get_state.return_value = 0 - pywemo_device.insight_params["state"] = "1" + pywemo_device.get_standby_state = StandbyState.OFF pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") await hass.async_block_till_done() assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF diff --git a/tests/components/wemo/test_sensor.py b/tests/components/wemo/test_sensor.py index ab6753975f1..7e0c8fa72f0 100644 --- a/tests/components/wemo/test_sensor.py +++ b/tests/components/wemo/test_sensor.py @@ -12,21 +12,6 @@ def pywemo_model(): return "Insight" -@pytest.fixture(name="pywemo_device") -def pywemo_device_fixture(pywemo_device): - """Fixture for WeMoDevice instances.""" - pywemo_device.insight_params = { - "currentpower": 1.0, - "todaymw": 200000000.0, - "state": 0, - "onfor": 0, - "ontoday": 0, - "ontotal": 0, - "powerthreshold": 0, - } - yield pywemo_device - - class InsightTestTemplate(EntityTestHelpers): """Base class for testing WeMo Insight Sensors.""" diff --git a/tests/components/wemo/test_switch.py b/tests/components/wemo/test_switch.py index 9c1dc804645..d54099e5e4a 100644 --- a/tests/components/wemo/test_switch.py +++ b/tests/components/wemo/test_switch.py @@ -1,17 +1,35 @@ """Tests for the Wemo switch entity.""" import pytest -from pywemo.exceptions import ActionException +import pywemo from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, ) from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.components.wemo.switch import ( + ATTR_CURRENT_STATE_DETAIL, + ATTR_ON_LATEST_TIME, + ATTR_ON_TODAY_TIME, + ATTR_ON_TOTAL_TIME, + ATTR_POWER_THRESHOLD, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_STANDBY, + STATE_UNKNOWN, +) from homeassistant.setup import async_setup_component from . import entity_test_helpers +from .conftest import ( + MOCK_INSIGHT_STATE_THRESHOLD_POWER, + async_create_wemo_entity, + create_pywemo_device, +) @pytest.fixture @@ -80,7 +98,7 @@ async def test_available_after_update( hass, pywemo_registry, pywemo_device, wemo_entity ): """Test the avaliability when an On call fails and after an update.""" - pywemo_device.on.side_effect = ActionException + pywemo_device.on.side_effect = pywemo.exceptions.ActionException pywemo_device.get_state.return_value = 1 await entity_test_helpers.test_avaliable_after_update( hass, pywemo_registry, pywemo_device, wemo_entity, SWITCH_DOMAIN @@ -90,3 +108,42 @@ async def test_available_after_update( async def test_turn_off_state(hass, wemo_entity): """Test that the device state is updated after turning off.""" await entity_test_helpers.test_turn_off_state(hass, wemo_entity, SWITCH_DOMAIN) + + +async def test_insight_state_attributes(hass, pywemo_registry): + """Verify the switch attributes are set for the Insight device.""" + await async_setup_component(hass, HA_DOMAIN, {}) + with create_pywemo_device(pywemo_registry, "Insight") as insight: + wemo_entity = await async_create_wemo_entity(hass, insight, "") + attributes = hass.states.get(wemo_entity.entity_id).attributes + assert attributes[ATTR_ON_LATEST_TIME] == "00d 00h 20m 34s" + assert attributes[ATTR_ON_TODAY_TIME] == "00d 01h 34m 38s" + assert attributes[ATTR_ON_TOTAL_TIME] == "00d 02h 30m 12s" + assert attributes[ATTR_POWER_THRESHOLD] == MOCK_INSIGHT_STATE_THRESHOLD_POWER + assert attributes[ATTR_CURRENT_STATE_DETAIL] == STATE_OFF + + async def async_update(): + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + + # Test 'ON' state detail value. + insight.get_standby_state = pywemo.StandbyState.ON + await async_update() + attributes = hass.states.get(wemo_entity.entity_id).attributes + assert attributes[ATTR_CURRENT_STATE_DETAIL] == STATE_ON + + # Test 'STANDBY' state detail value. + insight.get_standby_state = pywemo.StandbyState.STANDBY + await async_update() + attributes = hass.states.get(wemo_entity.entity_id).attributes + assert attributes[ATTR_CURRENT_STATE_DETAIL] == STATE_STANDBY + + # Test 'UNKNOWN' state detail value. + insight.get_standby_state = None + await async_update() + attributes = hass.states.get(wemo_entity.entity_id).attributes + assert attributes[ATTR_CURRENT_STATE_DETAIL] == STATE_UNKNOWN From eb988f77921be9fbc9f6dab1c96f9569dcd27107 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 23 May 2022 09:03:30 +0200 Subject: [PATCH 0769/3516] Fix race in MQTT platform setup (#72344) Make sure MQTT platforms setup locks discovery --- homeassistant/components/mqtt/__init__.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index dff5e75fa23..9191b22064c 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -816,14 +816,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[CONFIG_ENTRY_IS_SETUP] = set() - async with hass.data[DATA_CONFIG_ENTRY_LOCK]: - for component in PLATFORMS: - config_entries_key = f"{component}.mqtt" - if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: - hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) - ) + async def async_forward_entry_setup(): + """Forward the config entry setup to the platforms.""" + async with hass.data[DATA_CONFIG_ENTRY_LOCK]: + for component in PLATFORMS: + config_entries_key = f"{component}.mqtt" + if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: + hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) + await hass.config_entries.async_forward_entry_setup( + entry, component + ) + + hass.async_create_task(async_forward_entry_setup()) if conf.get(CONF_DISCOVERY): await _async_setup_discovery(hass, conf, entry) From 673f43fbec52b2ef5c7ece311298af5f021ca803 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 23 May 2022 09:04:03 +0200 Subject: [PATCH 0770/3516] Move manual configuration of MQTT scene to the integration key (#72273) Add scene Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 1 + homeassistant/components/mqtt/scene.py | 28 +++++++++++++++++++---- tests/components/mqtt/test_scene.py | 13 +++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 9191b22064c..c22029f59f8 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -200,6 +200,7 @@ PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( vol.Optional(Platform.HUMIDIFIER.value): cv.ensure_list, vol.Optional(Platform.LIGHT.value): cv.ensure_list, vol.Optional(Platform.LOCK.value): cv.ensure_list, + vol.Optional(Platform.SCENE.value): cv.ensure_list, vol.Optional(Platform.SIREN.value): cv.ensure_list, vol.Optional(Platform.SWITCH.value): cv.ensure_list, vol.Optional(Platform.VACUUM.value): cv.ensure_list, diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index c9bcae2dac1..98c692ceaff 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -1,6 +1,7 @@ """Support for MQTT scenes.""" from __future__ import annotations +import asyncio import functools import voluptuous as vol @@ -21,14 +22,16 @@ from .mixins import ( CONF_OBJECT_ID, MQTT_AVAILABILITY_SCHEMA, MqttEntity, + async_get_platform_config_from_yaml, async_setup_entry_helper, async_setup_platform_helper, + warn_for_legacy_schema, ) DEFAULT_NAME = "MQTT Scene" DEFAULT_RETAIN = False -PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_ICON): cv.icon, @@ -42,7 +45,13 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( } ).extend(MQTT_AVAILABILITY_SCHEMA.schema) -DISCOVERY_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.REMOVE_EXTRA) +# Configuring MQTT Scenes under the scene platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA = vol.All( + cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_MODERN.schema), + warn_for_legacy_schema(scene.DOMAIN), +) + +DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) async def async_setup_platform( @@ -51,7 +60,8 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up MQTT scene through configuration.yaml.""" + """Set up MQTT scene configured under the scene platform key (deprecated).""" + # Deprecated in HA Core 2022.6 await async_setup_platform_helper( hass, scene.DOMAIN, config, async_add_entities, _async_setup_entity ) @@ -62,7 +72,17 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT scene dynamically through MQTT discovery.""" + """Set up MQTT scene through configuration.yaml and dynamically through MQTT discovery.""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, scene.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index 1ccacc1c5ee..15bbd3964e6 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -20,6 +20,7 @@ from .test_common import ( help_test_discovery_update_unchanged, help_test_reloadable, help_test_reloadable_late, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, ) @@ -191,3 +192,15 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): domain = scene.DOMAIN config = DEFAULT_CONFIG[domain] await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = scene.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None From 01c211a7f9bb055afb1226c46d1e1608cfc51560 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 23 May 2022 09:06:09 +0200 Subject: [PATCH 0771/3516] Bump async-upnp-client to 0.30.1 (#72332) Co-authored-by: J. Nick Koston --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/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 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index cae136cd8fd..15beea714da 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.30.0"], + "requirements": ["async-upnp-client==0.30.1"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index 58c79a048b4..21329440788 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dms", - "requirements": ["async-upnp-client==0.30.0"], + "requirements": ["async-upnp-client==0.30.1"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index bef293185a6..fd97eb12e54 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -7,7 +7,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.5.0", "wakeonlan==2.0.1", - "async-upnp-client==0.30.0" + "async-upnp-client==0.30.1" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index cacce09e1f2..5cbd3d0d10e 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.30.0"], + "requirements": ["async-upnp-client==0.30.1"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 2bf1786ccbe..2e76dac4adb 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.30.0", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.30.1", "getmac==0.8.2"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman", "@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 853c95abcbb..5ec224498be 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.10", "async-upnp-client==0.30.0"], + "requirements": ["yeelight==0.7.10", "async-upnp-client==0.30.1"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1a61d2ad2b0..f10cea82655 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.11 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.30.0 +async-upnp-client==0.30.1 async_timeout==4.0.2 atomicwrites==1.4.0 attrs==21.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index ab4a232e864..ddda9476be4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -339,7 +339,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.30.0 +async-upnp-client==0.30.1 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5159abb4f43..69535dc6f37 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -281,7 +281,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.30.0 +async-upnp-client==0.30.1 # homeassistant.components.sleepiq asyncsleepiq==1.2.3 From 911fc83606c31ab4c9593d7827fd4ffd33713236 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 May 2022 09:21:59 +0200 Subject: [PATCH 0772/3516] Bump actions/upload-artifact from 3.0.0 to 3.1.0 (#72343) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- .github/workflows/wheels.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0fb77fdc789..4995864b377 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -827,7 +827,7 @@ jobs: -p no:sugar \ tests/components/${{ matrix.group }} - name: Upload coverage artifact - uses: actions/upload-artifact@v3.0.0 + uses: actions/upload-artifact@v3.1.0 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 70b6534df5e..6019e533530 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -50,13 +50,13 @@ jobs: ) > .env_file - name: Upload env_file - uses: actions/upload-artifact@v3.0.0 + uses: actions/upload-artifact@v3.1.0 with: name: env_file path: ./.env_file - name: Upload requirements_diff - uses: actions/upload-artifact@v3.0.0 + uses: actions/upload-artifact@v3.1.0 with: name: requirements_diff path: ./requirements_diff.txt From c52e8f7cfe08731d2d7bfaa9c6a060d5070e6ccc Mon Sep 17 00:00:00 2001 From: Felipe Martins Diel <41558831+felipediel@users.noreply.github.com> Date: Mon, 23 May 2022 04:45:05 -0300 Subject: [PATCH 0773/3516] Bump broadlink to 0.18.2 (#72346) --- 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 f291ba83afa..949f8add20b 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.18.1"], + "requirements": ["broadlink==0.18.2"], "codeowners": ["@danielhiversen", "@felipediel", "@L-I-Am"], "config_flow": true, "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index ddda9476be4..02a895c703a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -433,7 +433,7 @@ boto3==1.20.24 bravia-tv==1.0.11 # homeassistant.components.broadlink -broadlink==0.18.1 +broadlink==0.18.2 # homeassistant.components.brother brother==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69535dc6f37..f7e948b333a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -330,7 +330,7 @@ boschshcpy==0.2.30 bravia-tv==1.0.11 # homeassistant.components.broadlink -broadlink==0.18.1 +broadlink==0.18.2 # homeassistant.components.brother brother==1.1.0 From bc6451bd64b8a552078f781d262e28b93961391f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 23 May 2022 10:08:44 +0200 Subject: [PATCH 0774/3516] Move manual configuration of MQTT select to the integration key (#72274) Add select Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 1 + homeassistant/components/mqtt/select.py | 28 +++++++++++++++++++---- tests/components/mqtt/test_select.py | 13 +++++++++++ 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index c22029f59f8..52204b31dcf 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -201,6 +201,7 @@ PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( vol.Optional(Platform.LIGHT.value): cv.ensure_list, vol.Optional(Platform.LOCK.value): cv.ensure_list, vol.Optional(Platform.SCENE.value): cv.ensure_list, + vol.Optional(Platform.SELECT.value): cv.ensure_list, vol.Optional(Platform.SIREN.value): cv.ensure_list, vol.Optional(Platform.SWITCH.value): cv.ensure_list, vol.Optional(Platform.VACUUM.value): cv.ensure_list, diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index f873a32a5de..0765eb7f176 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -1,6 +1,7 @@ """Configure select in a device through MQTT topic.""" from __future__ import annotations +import asyncio import functools import logging @@ -30,8 +31,10 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_get_platform_config_from_yaml, async_setup_entry_helper, async_setup_platform_helper, + warn_for_legacy_schema, ) _LOGGER = logging.getLogger(__name__) @@ -48,7 +51,7 @@ MQTT_SELECT_ATTRIBUTES_BLOCKED = frozenset( ) -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -58,9 +61,13 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( }, ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -PLATFORM_SCHEMA = vol.All(_PLATFORM_SCHEMA_BASE) +# Configuring MQTT Select under the select platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA = vol.All( + cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_MODERN.schema), + warn_for_legacy_schema(select.DOMAIN), +) -DISCOVERY_SCHEMA = vol.All(_PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA)) +DISCOVERY_SCHEMA = vol.All(PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA)) async def async_setup_platform( @@ -69,7 +76,8 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up MQTT select through configuration.yaml.""" + """Set up MQTT select configured under the select platform key (deprecated).""" + # Deprecated in HA Core 2022.6 await async_setup_platform_helper( hass, select.DOMAIN, config, async_add_entities, _async_setup_entity ) @@ -80,7 +88,17 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT select dynamically through MQTT discovery.""" + """Set up MQTT select through configuration.yaml and dynamically through MQTT discovery.""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, select.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index 069c0dfe4c9..cf5abf55854 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -41,6 +41,7 @@ from .test_common import ( help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -610,3 +611,15 @@ async def test_encoding_subscribable_topics( attribute, attribute_value, ) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = select.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None From 793ad568eb0006ba4b0fd10c3e0e95941980ea95 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Mon, 23 May 2022 10:47:40 +0200 Subject: [PATCH 0775/3516] here_travel_time: Add unique_id and DeviceInfo (#72352) Co-authored-by: Franck Nijhof --- homeassistant/components/here_travel_time/sensor.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 4a09252f068..75c9fd2ea3b 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -19,6 +19,8 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.start import async_at_start from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -144,6 +146,7 @@ async def async_setup_entry( async_add_entities( [ HERETravelTimeSensor( + config_entry.entry_id, config_entry.data[CONF_NAME], config_entry.options[CONF_TRAFFIC_MODE], hass.data[DOMAIN][config_entry.entry_id], @@ -157,6 +160,7 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): def __init__( self, + unique_id_prefix: str, name: str, traffic_mode: str, coordinator: HereTravelTimeDataUpdateCoordinator, @@ -166,6 +170,13 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): self._traffic_mode = traffic_mode == TRAFFIC_MODE_ENABLED self._attr_native_unit_of_measurement = TIME_MINUTES self._attr_name = name + self._attr_unique_id = unique_id_prefix + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id_prefix)}, + entry_type=DeviceEntryType.SERVICE, + name=name, + manufacturer="HERE Technologies", + ) async def async_added_to_hass(self) -> None: """Wait for start so origin and destination entities can be resolved.""" From 800410ddf09dad2bab842e7fa40c59568e40ff98 Mon Sep 17 00:00:00 2001 From: Matrix Date: Mon, 23 May 2022 16:52:05 +0800 Subject: [PATCH 0776/3516] Add yolink siren (#72341) * Add yolink siren * Add .coveragerc * fix wrong comments --- .coveragerc | 1 + homeassistant/components/yolink/__init__.py | 2 +- homeassistant/components/yolink/const.py | 1 + homeassistant/components/yolink/siren.py | 118 ++++++++++++++++++++ 4 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/yolink/siren.py diff --git a/.coveragerc b/.coveragerc index 2d530ab8f45..ed1e0b0bce4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1477,6 +1477,7 @@ omit = homeassistant/components/yolink/coordinator.py homeassistant/components/yolink/entity.py homeassistant/components/yolink/sensor.py + homeassistant/components/yolink/siren.py homeassistant/components/yolink/switch.py homeassistant/components/youless/__init__.py homeassistant/components/youless/const.py diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 089f0cc79b9..2c85344c54b 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -21,7 +21,7 @@ SCAN_INTERVAL = timedelta(minutes=5) _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SIREN, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index b94727cb948..00d6d6d028e 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -18,3 +18,4 @@ ATTR_DEVICE_TH_SENSOR = "THSensor" ATTR_DEVICE_MOTION_SENSOR = "MotionSensor" ATTR_DEVICE_LEAK_SENSOR = "LeakSensor" ATTR_DEVICE_OUTLET = "Outlet" +ATTR_DEVICE_SIREN = "Siren" diff --git a/homeassistant/components/yolink/siren.py b/homeassistant/components/yolink/siren.py new file mode 100644 index 00000000000..7a621db6eca --- /dev/null +++ b/homeassistant/components/yolink/siren.py @@ -0,0 +1,118 @@ +"""YoLink Siren.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from yolink.device import YoLinkDevice +from yolink.exception import YoLinkAuthFailError, YoLinkClientError + +from homeassistant.components.siren import ( + SirenEntity, + SirenEntityDescription, + SirenEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ATTR_COORDINATOR, ATTR_DEVICE_SIREN, DOMAIN +from .coordinator import YoLinkCoordinator +from .entity import YoLinkEntity + + +@dataclass +class YoLinkSirenEntityDescription(SirenEntityDescription): + """YoLink SirenEntityDescription.""" + + exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True + value: Callable[[str], bool | None] = lambda _: None + + +DEVICE_TYPES: tuple[YoLinkSirenEntityDescription, ...] = ( + YoLinkSirenEntityDescription( + key="state", + name="State", + value=lambda value: value == "alert", + exists_fn=lambda device: device.device_type in [ATTR_DEVICE_SIREN], + ), +) + +DEVICE_TYPE = [ATTR_DEVICE_SIREN] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up YoLink siren from a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATOR] + devices = [ + device for device in coordinator.yl_devices if device.device_type in DEVICE_TYPE + ] + entities = [] + for device in devices: + for description in DEVICE_TYPES: + if description.exists_fn(device): + entities.append( + YoLinkSirenEntity(config_entry, coordinator, description, device) + ) + async_add_entities(entities) + + +class YoLinkSirenEntity(YoLinkEntity, SirenEntity): + """YoLink Siren Entity.""" + + entity_description: YoLinkSirenEntityDescription + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: YoLinkCoordinator, + description: YoLinkSirenEntityDescription, + device: YoLinkDevice, + ) -> None: + """Init YoLink Siren.""" + super().__init__(coordinator, device) + self.config_entry = config_entry + self.entity_description = description + self._attr_unique_id = f"{device.device_id} {self.entity_description.key}" + self._attr_name = f"{device.device_name} ({self.entity_description.name})" + self._attr_supported_features = ( + SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF + ) + + @callback + def update_entity_state(self, state: dict) -> None: + """Update HA Entity State.""" + self._attr_is_on = self.entity_description.value( + state[self.entity_description.key] + ) + self.async_write_ha_state() + + async def call_state_change(self, state: bool) -> None: + """Call setState api to change siren state.""" + try: + # call_device_http_api will check result, fail by raise YoLinkClientError + await self.device.call_device_http_api( + "setState", {"state": {"alarm": state}} + ) + except YoLinkAuthFailError as yl_auth_err: + self.config_entry.async_start_reauth(self.hass) + raise HomeAssistantError(yl_auth_err) from yl_auth_err + except YoLinkClientError as yl_client_err: + self.coordinator.last_update_success = False + raise HomeAssistantError(yl_client_err) from yl_client_err + self._attr_is_on = self.entity_description.value("alert" if state else "normal") + self.async_write_ha_state() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.call_state_change(True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self.call_state_change(False) From e17a653cf0a84de374cc215737bfea11ded38fee Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Mon, 23 May 2022 10:59:10 +0200 Subject: [PATCH 0777/3516] Bump plugwise to v0.18.4 (#72263) --- homeassistant/components/plugwise/climate.py | 4 +--- .../components/plugwise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../all_data.json | 5 ----- .../fixtures/anna_heatpump/all_data.json | 9 ++++----- tests/components/plugwise/test_climate.py | 19 ++++--------------- tests/components/plugwise/test_diagnostics.py | 5 ----- 8 files changed, 12 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index 09f5181090c..c74a1baa9be 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -105,6 +105,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): return HVACAction.HEATING if heater_central_data["binary_sensors"].get("cooling_state"): return HVACAction.COOLING + return HVACAction.IDLE @property @@ -132,9 +133,6 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): @plugwise_command async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the hvac mode.""" - if hvac_mode == HVACMode.AUTO and not self.device.get("schedule_temperature"): - raise ValueError("Cannot set HVAC mode to Auto: No schedule available") - await self.coordinator.api.set_schedule_state( self.device["location"], self.device.get("last_used"), diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index abf6f27b3fa..2b7f21cd106 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.18.2"], + "requirements": ["plugwise==0.18.4"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 02a895c703a..28417beb21b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1245,7 +1245,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.18.2 +plugwise==0.18.4 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7e948b333a..da7577fa7d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -847,7 +847,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.18.2 +plugwise==0.18.4 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json index 85e5abb0b17..6ef0716e4b6 100644 --- a/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json +++ b/tests/components/plugwise/fixtures/adam_multiple_devices_per_zone/all_data.json @@ -34,7 +34,6 @@ ], "selected_schedule": "None", "last_used": "Badkamer Schema", - "schedule_temperature": 0.0, "mode": "heat", "sensors": { "temperature": 16.5, @@ -104,7 +103,6 @@ ], "selected_schedule": "GF7 Woonkamer", "last_used": "GF7 Woonkamer", - "schedule_temperature": 15.0, "mode": "auto", "sensors": { "temperature": 20.9, @@ -301,7 +299,6 @@ ], "selected_schedule": "CV Jessie", "last_used": "CV Jessie", - "schedule_temperature": 15.0, "mode": "auto", "sensors": { "temperature": 17.2, @@ -352,7 +349,6 @@ ], "selected_schedule": "Badkamer Schema", "last_used": "Badkamer Schema", - "schedule_temperature": 15.0, "mode": "auto", "sensors": { "temperature": 18.9, @@ -402,7 +398,6 @@ ], "selected_schedule": "None", "last_used": "Badkamer Schema", - "schedule_temperature": 0.0, "mode": "heat", "sensors": { "temperature": 15.6, diff --git a/tests/components/plugwise/fixtures/anna_heatpump/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump/all_data.json index cc3f8d8f385..60bc4c35668 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump/all_data.json @@ -63,11 +63,10 @@ "resolution": 0.1, "preset_modes": ["no_frost", "home", "away", "asleep", "vacation"], "active_preset": "home", - "available_schedules": ["None"], - "selected_schedule": "None", - "last_used": null, - "schedule_temperature": null, - "mode": "heat", + "available_schedules": ["standaard"], + "selected_schedule": "standaard", + "last_used": "standaard", + "mode": "auto", "sensors": { "temperature": 19.3, "setpoint": 21.0, diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index 34a7ea50d34..bf110cd8a91 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -150,10 +150,11 @@ async def test_anna_climate_entity_attributes( """Test creation of anna climate device environment.""" state = hass.states.get("climate.anna") assert state - assert state.state == HVACMode.HEAT + assert state.state == HVACMode.AUTO assert state.attributes["hvac_modes"] == [ HVACMode.HEAT, HVACMode.COOL, + HVACMode.AUTO, ] assert "no_frost" in state.attributes["preset_modes"] assert "home" in state.attributes["preset_modes"] @@ -199,24 +200,12 @@ async def test_anna_climate_entity_climate_changes( await hass.services.async_call( "climate", "set_hvac_mode", - {"entity_id": "climate.anna", "hvac_mode": "heat_cool"}, + {"entity_id": "climate.anna", "hvac_mode": "heat"}, blocking=True, ) assert mock_smile_anna.set_temperature.call_count == 1 assert mock_smile_anna.set_schedule_state.call_count == 1 mock_smile_anna.set_schedule_state.assert_called_with( - "c784ee9fdab44e1395b8dee7d7a497d5", None, "off" + "c784ee9fdab44e1395b8dee7d7a497d5", "standaard", "off" ) - - # Auto mode is not available, no schedules - with pytest.raises(ValueError): - await hass.services.async_call( - "climate", - "set_hvac_mode", - {"entity_id": "climate.anna", "hvac_mode": "auto"}, - blocking=True, - ) - - assert mock_smile_anna.set_temperature.call_count == 1 - assert mock_smile_anna.set_schedule_state.call_count == 1 diff --git a/tests/components/plugwise/test_diagnostics.py b/tests/components/plugwise/test_diagnostics.py index c2bb91746ae..372f410cd81 100644 --- a/tests/components/plugwise/test_diagnostics.py +++ b/tests/components/plugwise/test_diagnostics.py @@ -54,7 +54,6 @@ async def test_diagnostics( ], "selected_schedule": "None", "last_used": "Badkamer Schema", - "schedule_temperature": 0.0, "mode": "heat", "sensors": {"temperature": 16.5, "setpoint": 13.0, "battery": 67}, }, @@ -120,7 +119,6 @@ async def test_diagnostics( ], "selected_schedule": "GF7 Woonkamer", "last_used": "GF7 Woonkamer", - "schedule_temperature": 15.0, "mode": "auto", "sensors": {"temperature": 20.9, "setpoint": 21.5, "battery": 34}, }, @@ -290,7 +288,6 @@ async def test_diagnostics( ], "selected_schedule": "CV Jessie", "last_used": "CV Jessie", - "schedule_temperature": 15.0, "mode": "auto", "sensors": {"temperature": 17.2, "setpoint": 15.0, "battery": 37}, }, @@ -337,7 +334,6 @@ async def test_diagnostics( ], "selected_schedule": "Badkamer Schema", "last_used": "Badkamer Schema", - "schedule_temperature": 15.0, "mode": "auto", "sensors": {"temperature": 18.9, "setpoint": 14.0, "battery": 92}, }, @@ -380,7 +376,6 @@ async def test_diagnostics( ], "selected_schedule": "None", "last_used": "Badkamer Schema", - "schedule_temperature": 0.0, "mode": "heat", "sensors": { "temperature": 15.6, From 373ad21ce38eaba1c65e3f6458fae13d61e09c1d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 23 May 2022 02:00:17 -0700 Subject: [PATCH 0778/3516] Fix translations clean up script (#72114) --- script/translations/clean.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/script/translations/clean.py b/script/translations/clean.py index 63281b36229..0dcf40941ef 100644 --- a/script/translations/clean.py +++ b/script/translations/clean.py @@ -65,7 +65,7 @@ def find_frontend(): raise ExitApp(f"Unable to find frontend at {FRONTEND_DIR}") source = FRONTEND_DIR / "src/translations/en.json" - translated = FRONTEND_DIR / "translations/en.json" + translated = FRONTEND_DIR / "translations/frontend/en.json" missing_keys = [] find_extra( @@ -91,6 +91,8 @@ def run(): print("No missing translations!") return 0 + print(f"Found {len(missing_keys)} extra keys") + # We can't query too many keys at once, so limit the number to 50. for i in range(0, len(missing_keys), 50): chunk = missing_keys[i : i + 50] @@ -100,11 +102,13 @@ def run(): print( f"Lookin up key in Lokalise returns {len(key_data)} results, expected {len(chunk)}" ) - return 1 - print(f"Deleting {len(chunk)} keys:") - for key in chunk: - print(" -", key) + if not key_data: + continue + + print(f"Deleting {len(key_data)} keys:") + for key in key_data: + print(" -", key["key_name"]["web"]) print() while input("Type YES to delete these keys: ") != "YES": pass From ea05bd8c2ed0eb6acaea722e3a2432f5371d994d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 May 2022 11:58:19 +0200 Subject: [PATCH 0779/3516] Allow for using pip 22.1(.x) (#72348) --- .github/workflows/ci.yaml | 4 ++-- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.cfg | 2 +- tox.ini | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4995864b377..fb659cf21d2 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -203,7 +203,7 @@ jobs: python -m venv venv . venv/bin/activate python --version - pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.1" setuptools wheel + pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.2" setuptools wheel pip install --cache-dir=$PIP_CACHE -r requirements.txt -r requirements_test.txt --use-deprecated=legacy-resolver - name: Generate partial pre-commit restore key id: generate-pre-commit-key @@ -608,7 +608,7 @@ jobs: python -m venv venv . venv/bin/activate python --version - pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.1" setuptools wheel + pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.2" setuptools wheel pip install --cache-dir=$PIP_CACHE -r requirements_all.txt --use-deprecated=legacy-resolver pip install --cache-dir=$PIP_CACHE -r requirements_test.txt --use-deprecated=legacy-resolver pip install -e . diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f10cea82655..354b06d2c10 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ jinja2==3.1.2 lru-dict==1.1.7 paho-mqtt==1.6.1 pillow==9.1.0 -pip>=21.0,<22.1 +pip>=21.0,<22.2 pyserial==3.5 python-slugify==4.0.1 pyudev==0.22.0 diff --git a/requirements.txt b/requirements.txt index f2308908a30..0c13b9c319b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ ifaddr==0.1.7 jinja2==3.1.2 PyJWT==2.4.0 cryptography==36.0.2 -pip>=21.0,<22.1 +pip>=21.0,<22.2 python-slugify==4.0.1 pyyaml==6.0 requests==2.27.1 diff --git a/setup.cfg b/setup.cfg index 9d4fad881b6..e8688293683 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,7 +48,7 @@ install_requires = PyJWT==2.4.0 # PyJWT has loose dependency. We want the latest one. cryptography==36.0.2 - pip>=21.0,<22.1 + pip>=21.0,<22.2 python-slugify==4.0.1 pyyaml==6.0 requests==2.27.1 diff --git a/tox.ini b/tox.ini index 441960fbbf5..b39caacf471 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ isolated_build = True [testenv] basepython = {env:PYTHON3_PATH:python3} # pip version duplicated in homeassistant/package_constraints.txt -pip_version = pip>=21.0,<22.1 +pip_version = pip>=21.0,<22.2 install_command = python -m pip install --use-deprecated legacy-resolver {opts} {packages} commands = {envpython} -X dev -m pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar {posargs} From 19781d285c8f046b9cf9cb35cf2b5bb0acc901a0 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Mon, 23 May 2022 11:59:11 +0200 Subject: [PATCH 0780/3516] Add missing min and max values for some numbers in Overkiz (#72229) --- homeassistant/components/overkiz/number.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/overkiz/number.py b/homeassistant/components/overkiz/number.py index e6e64162dd8..741c666a42a 100644 --- a/homeassistant/components/overkiz/number.py +++ b/homeassistant/components/overkiz/number.py @@ -38,6 +38,8 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ name="My Position", icon="mdi:content-save-cog", command=OverkizCommand.SET_MEMORIZED_1_POSITION, + min_value=0, + max_value=100, entity_category=EntityCategory.CONFIG, ), # WaterHeater: Expected Number Of Shower (2 - 4) @@ -84,6 +86,8 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ key=OverkizState.CORE_LEVEL, icon="mdi:patio-heater", command=OverkizCommand.SET_LEVEL, + min_value=0, + max_value=100, inverted=True, ), ] From c770a81160c242f3e086fb224cf53094c7f05f64 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 23 May 2022 12:00:01 +0200 Subject: [PATCH 0781/3516] Use pydeconz interface controls for alarm control panel (#72317) --- .../components/deconz/alarm_control_panel.py | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py index 2e825dafd65..e635b11972e 100644 --- a/homeassistant/components/deconz/alarm_control_panel.py +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -1,7 +1,7 @@ """Support for deCONZ alarm control panel devices.""" from __future__ import annotations -from pydeconz.models.alarm_system import AlarmSystem +from pydeconz.interfaces.alarm_systems import ArmAction from pydeconz.models.event import EventType from pydeconz.models.sensor.ancillary_control import ( ANCILLARY_CONTROL_ARMED_AWAY, @@ -53,13 +53,13 @@ DECONZ_TO_ALARM_STATE = { } -def get_alarm_system_for_unique_id( +def get_alarm_system_id_for_unique_id( gateway: DeconzGateway, unique_id: str -) -> AlarmSystem | None: - """Retrieve alarm system unique ID is registered to.""" +) -> str | None: + """Retrieve alarm system ID the unique ID is registered to.""" for alarm_system in gateway.api.alarmsystems.values(): if unique_id in alarm_system.devices: - return alarm_system + return alarm_system.resource_id return None @@ -76,8 +76,12 @@ async def async_setup_entry( def async_add_sensor(_: EventType, sensor_id: str) -> None: """Add alarm control panel devices from deCONZ.""" sensor = gateway.api.sensors.ancillary_control[sensor_id] - if alarm_system := get_alarm_system_for_unique_id(gateway, sensor.unique_id): - async_add_entities([DeconzAlarmControlPanel(sensor, gateway, alarm_system)]) + if alarm_system_id := get_alarm_system_id_for_unique_id( + gateway, sensor.unique_id + ): + async_add_entities( + [DeconzAlarmControlPanel(sensor, gateway, alarm_system_id)] + ) config_entry.async_on_unload( gateway.api.sensors.ancillary_control.subscribe( @@ -107,11 +111,11 @@ class DeconzAlarmControlPanel(DeconzDevice, AlarmControlPanelEntity): self, device: AncillaryControl, gateway: DeconzGateway, - alarm_system: AlarmSystem, + alarm_system_id: str, ) -> None: """Set up alarm control panel device.""" super().__init__(device, gateway) - self.alarm_system = alarm_system + self.alarm_system_id = alarm_system_id @callback def async_update_callback(self) -> None: @@ -133,19 +137,27 @@ class DeconzAlarmControlPanel(DeconzDevice, AlarmControlPanelEntity): async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" if code: - await self.alarm_system.arm_away(code) + await self.gateway.api.alarmsystems.arm( + self.alarm_system_id, ArmAction.AWAY, code + ) async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" if code: - await self.alarm_system.arm_stay(code) + await self.gateway.api.alarmsystems.arm( + self.alarm_system_id, ArmAction.STAY, code + ) async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" if code: - await self.alarm_system.arm_night(code) + await self.gateway.api.alarmsystems.arm( + self.alarm_system_id, ArmAction.NIGHT, code + ) async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if code: - await self.alarm_system.disarm(code) + await self.gateway.api.alarmsystems.arm( + self.alarm_system_id, ArmAction.DISARM, code + ) From 31b53e7fc61cc0738d7432a483d01974ed6715a6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 May 2022 05:03:49 -0500 Subject: [PATCH 0782/3516] Remove superfluous underscore from lutron_caseta entity and device names (#72337) --- homeassistant/components/lutron_caseta/__init__.py | 14 +++++--------- homeassistant/components/lutron_caseta/scene.py | 7 +------ 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 3daa45d30ee..dd64ed4ec6f 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -197,15 +197,15 @@ def _async_register_button_devices( if "serial" not in device or device["serial"] in seen: continue seen.add(device["serial"]) + area, name = _area_and_name_from_name(device["name"]) device_args = { - "name": device["name"], + "name": f"{area} {name}", "manufacturer": MANUFACTURER, "config_entry_id": config_entry_id, "identifiers": {(DOMAIN, device["serial"])}, "model": f"{device['model']} ({device['type']})", "via_device": (DOMAIN, bridge_device["serial"]), } - area, _ = _area_and_name_from_name(device["name"]) if area != UNASSIGNED_AREA: device_args["suggested_area"] = area @@ -312,15 +312,16 @@ class LutronCasetaDevice(Entity): self._bridge_device = bridge_device if "serial" not in self._device: return + area, name = _area_and_name_from_name(device["name"]) + self._attr_name = full_name = f"{area} {name}" info = DeviceInfo( identifiers={(DOMAIN, self.serial)}, manufacturer=MANUFACTURER, model=f"{device['model']} ({device['type']})", - name=self.name, + name=full_name, via_device=(DOMAIN, self._bridge_device["serial"]), configuration_url=CONFIG_URL, ) - area, _ = _area_and_name_from_name(device["name"]) if area != UNASSIGNED_AREA: info[ATTR_SUGGESTED_AREA] = area self._attr_device_info = info @@ -334,11 +335,6 @@ class LutronCasetaDevice(Entity): """Return the device ID used for calling pylutron_caseta.""" return self._device["device_id"] - @property - def name(self): - """Return the name of the device.""" - return self._device["name"] - @property def serial(self): """Return the serial number of the device.""" diff --git a/homeassistant/components/lutron_caseta/scene.py b/homeassistant/components/lutron_caseta/scene.py index bb932c61316..d73d8011481 100644 --- a/homeassistant/components/lutron_caseta/scene.py +++ b/homeassistant/components/lutron_caseta/scene.py @@ -30,15 +30,10 @@ class LutronCasetaScene(Scene): def __init__(self, scene, bridge): """Initialize the Lutron Caseta scene.""" - self._scene_name = scene["name"] + self._attr_name = scene["name"] self._scene_id = scene["scene_id"] self._bridge = bridge - @property - def name(self): - """Return the name of the scene.""" - return self._scene_name - async def async_activate(self, **kwargs: Any) -> None: """Activate the scene.""" await self._bridge.activate_scene(self._scene_id) From 421167c548bad838b40e33ca78f473f6e675298c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 12:18:17 +0200 Subject: [PATCH 0783/3516] Drop GetAutomationsResult and GetAutomationCapabilitiesResult aliases (#72328) --- .../components/arcam_fmj/device_trigger.py | 7 ++----- .../components/device_automation/__init__.py | 3 --- .../components/device_automation/action.py | 11 +++-------- .../components/device_automation/condition.py | 13 ++++--------- .../components/device_automation/trigger.py | 8 +++----- .../components/homekit_controller/device_trigger.py | 7 ++----- .../components/lutron_caseta/device_trigger.py | 7 ++----- homeassistant/components/nest/device_trigger.py | 7 ++----- homeassistant/components/netatmo/device_trigger.py | 7 ++----- homeassistant/components/shelly/device_trigger.py | 9 +++------ homeassistant/components/webostv/device_trigger.py | 7 ++----- 11 files changed, 25 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/arcam_fmj/device_trigger.py b/homeassistant/components/arcam_fmj/device_trigger.py index 46c51789dfb..593250e4983 100644 --- a/homeassistant/components/arcam_fmj/device_trigger.py +++ b/homeassistant/components/arcam_fmj/device_trigger.py @@ -7,10 +7,7 @@ from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, ) -from homeassistant.components.device_automation import ( - DEVICE_TRIGGER_BASE_SCHEMA, - GetAutomationsResult, -) +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_ID, @@ -36,7 +33,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> GetAutomationsResult: +) -> list[dict[str, str]]: """List device triggers for Arcam FMJ Receiver control devices.""" registry = entity_registry.async_get(hass) triggers = [] diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index e24ae15f9ff..61fd93354fe 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -44,9 +44,6 @@ if TYPE_CHECKING: ] # mypy: allow-untyped-calls, allow-untyped-defs -GetAutomationsResult = list[dict[str, Any]] -GetAutomationCapabilitiesResult = dict[str, vol.Schema] - DOMAIN = "device_automation" DEVICE_TRIGGER_BASE_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( diff --git a/homeassistant/components/device_automation/action.py b/homeassistant/components/device_automation/action.py index b15ca12a927..5737fbc5bf3 100644 --- a/homeassistant/components/device_automation/action.py +++ b/homeassistant/components/device_automation/action.py @@ -10,12 +10,7 @@ from homeassistant.const import CONF_DOMAIN from homeassistant.core import Context, HomeAssistant from homeassistant.helpers.typing import ConfigType -from . import ( - DeviceAutomationType, - GetAutomationCapabilitiesResult, - GetAutomationsResult, - async_get_device_automation_platform, -) +from . import DeviceAutomationType, async_get_device_automation_platform from .exceptions import InvalidDeviceAutomationConfig @@ -43,12 +38,12 @@ class DeviceAutomationActionProtocol(Protocol): def async_get_action_capabilities( self, hass: HomeAssistant, config: ConfigType - ) -> GetAutomationCapabilitiesResult | Awaitable[GetAutomationCapabilitiesResult]: + ) -> dict[str, vol.Schema] | Awaitable[dict[str, vol.Schema]]: """List action capabilities.""" def async_get_actions( self, hass: HomeAssistant, device_id: str - ) -> GetAutomationsResult | Awaitable[GetAutomationsResult]: + ) -> list[dict[str, Any]] | Awaitable[list[dict[str, Any]]]: """List actions.""" diff --git a/homeassistant/components/device_automation/condition.py b/homeassistant/components/device_automation/condition.py index 7ff3216c702..1f1f8e94832 100644 --- a/homeassistant/components/device_automation/condition.py +++ b/homeassistant/components/device_automation/condition.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Awaitable -from typing import TYPE_CHECKING, Protocol, cast +from typing import TYPE_CHECKING, Any, Protocol, cast import voluptuous as vol @@ -11,12 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType -from . import ( - DeviceAutomationType, - GetAutomationCapabilitiesResult, - GetAutomationsResult, - async_get_device_automation_platform, -) +from . import DeviceAutomationType, async_get_device_automation_platform from .exceptions import InvalidDeviceAutomationConfig if TYPE_CHECKING: @@ -43,12 +38,12 @@ class DeviceAutomationConditionProtocol(Protocol): def async_get_condition_capabilities( self, hass: HomeAssistant, config: ConfigType - ) -> GetAutomationCapabilitiesResult | Awaitable[GetAutomationCapabilitiesResult]: + ) -> dict[str, vol.Schema] | Awaitable[dict[str, vol.Schema]]: """List condition capabilities.""" def async_get_conditions( self, hass: HomeAssistant, device_id: str - ) -> GetAutomationsResult | Awaitable[GetAutomationsResult]: + ) -> list[dict[str, Any]] | Awaitable[list[dict[str, Any]]]: """List conditions.""" diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py index e4a740d6599..c5f42b3e813 100644 --- a/homeassistant/components/device_automation/trigger.py +++ b/homeassistant/components/device_automation/trigger.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Awaitable -from typing import Protocol, cast +from typing import Any, Protocol, cast import voluptuous as vol @@ -17,8 +17,6 @@ from homeassistant.helpers.typing import ConfigType from . import ( DEVICE_TRIGGER_BASE_SCHEMA, DeviceAutomationType, - GetAutomationCapabilitiesResult, - GetAutomationsResult, async_get_device_automation_platform, ) from .exceptions import InvalidDeviceAutomationConfig @@ -50,12 +48,12 @@ class DeviceAutomationTriggerProtocol(Protocol): def async_get_trigger_capabilities( self, hass: HomeAssistant, config: ConfigType - ) -> GetAutomationCapabilitiesResult | Awaitable[GetAutomationCapabilitiesResult]: + ) -> dict[str, vol.Schema] | Awaitable[dict[str, vol.Schema]]: """List trigger capabilities.""" def async_get_triggers( self, hass: HomeAssistant, device_id: str - ) -> GetAutomationsResult | Awaitable[GetAutomationsResult]: + ) -> list[dict[str, Any]] | Awaitable[list[dict[str, Any]]]: """List triggers.""" diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index 5c46aee6ca2..dcac7238c8e 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -14,10 +14,7 @@ from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, ) -from homeassistant.components.device_automation import ( - DEVICE_TRIGGER_BASE_SCHEMA, - GetAutomationsResult, -) +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.typing import ConfigType @@ -244,7 +241,7 @@ def async_fire_triggers(conn: HKDevice, events: dict[tuple[int, int], Any]): async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> GetAutomationsResult: +) -> list[dict[str, str]]: """List device triggers for homekit devices.""" if device_id not in hass.data.get(TRIGGERS, {}): diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index b6f01a5e49e..68394667764 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -7,10 +7,7 @@ from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, ) -from homeassistant.components.device_automation import ( - DEVICE_TRIGGER_BASE_SCHEMA, - GetAutomationsResult, -) +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -350,7 +347,7 @@ async def async_validate_trigger_config( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> GetAutomationsResult: +) -> list[dict[str, str]]: """List device triggers for lutron caseta devices.""" triggers = [] diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index e2f4288d122..05769a407f2 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -8,10 +8,7 @@ from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, ) -from homeassistant.components.device_automation import ( - DEVICE_TRIGGER_BASE_SCHEMA, - GetAutomationsResult, -) +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -66,7 +63,7 @@ def async_get_device_trigger_types( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> GetAutomationsResult: +) -> list[dict[str, str]]: """List device triggers for a Nest device.""" nest_device_id = async_get_nest_device_id(hass, device_id) if not nest_device_id: diff --git a/homeassistant/components/netatmo/device_trigger.py b/homeassistant/components/netatmo/device_trigger.py index ce03aa91905..25f76307b5f 100644 --- a/homeassistant/components/netatmo/device_trigger.py +++ b/homeassistant/components/netatmo/device_trigger.py @@ -7,10 +7,7 @@ from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, ) -from homeassistant.components.device_automation import ( - DEVICE_TRIGGER_BASE_SCHEMA, - GetAutomationsResult, -) +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -101,7 +98,7 @@ async def async_validate_trigger_config( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> GetAutomationsResult: +) -> list[dict[str, str]]: """List device triggers for Netatmo devices.""" registry = entity_registry.async_get(hass) device_registry = dr.async_get(hass) diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py index 1231f670d49..0f9fc55ed71 100644 --- a/homeassistant/components/shelly/device_trigger.py +++ b/homeassistant/components/shelly/device_trigger.py @@ -9,10 +9,7 @@ from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, ) -from homeassistant.components.device_automation import ( - DEVICE_TRIGGER_BASE_SCHEMA, - GetAutomationsResult, -) +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -111,9 +108,9 @@ async def async_validate_trigger_config( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> GetAutomationsResult: +) -> list[dict[str, str]]: """List device triggers for Shelly devices.""" - triggers: GetAutomationsResult = [] + triggers: list[dict[str, str]] = [] if rpc_wrapper := get_rpc_device_wrapper(hass, device_id): input_triggers = get_rpc_input_triggers(rpc_wrapper.device) diff --git a/homeassistant/components/webostv/device_trigger.py b/homeassistant/components/webostv/device_trigger.py index 9836f561b9d..9ce49bbe79e 100644 --- a/homeassistant/components/webostv/device_trigger.py +++ b/homeassistant/components/webostv/device_trigger.py @@ -7,10 +7,7 @@ from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, ) -from homeassistant.components.device_automation import ( - DEVICE_TRIGGER_BASE_SCHEMA, - GetAutomationsResult, -) +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) @@ -61,7 +58,7 @@ async def async_validate_trigger_config( async def async_get_triggers( _hass: HomeAssistant, device_id: str -) -> GetAutomationsResult: +) -> list[dict[str, str]]: """List device triggers for device.""" triggers = [] base_trigger = { From 9ecc96e31c290f714817f4561279ec8e3cb40d4a Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 23 May 2022 12:29:21 +0200 Subject: [PATCH 0784/3516] Bump velbus-aio to 2022.5.1 (#72355) --- 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 ae50609baf1..f759eea0a34 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==2022.2.4"], + "requirements": ["velbus-aio==2022.5.1"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "dependencies": ["usb"], diff --git a/requirements_all.txt b/requirements_all.txt index 28417beb21b..ca2028b8eaa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2384,7 +2384,7 @@ vallox-websocket-api==2.11.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.2.4 +velbus-aio==2022.5.1 # homeassistant.components.venstar venstarcolortouch==0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index da7577fa7d3..25270998ab4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1566,7 +1566,7 @@ vallox-websocket-api==2.11.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.2.4 +velbus-aio==2022.5.1 # homeassistant.components.venstar venstarcolortouch==0.15 From f6fab65f60bfd09f01b838cb0588f789bbf56ea6 Mon Sep 17 00:00:00 2001 From: Steve HOLWEG Date: Mon, 23 May 2022 12:33:25 +0200 Subject: [PATCH 0785/3516] Add support for com.fibaro.binarySensor to fibaro (#65446) --- homeassistant/components/fibaro/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index c9b0cb345e1..fdb0f894a40 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -78,6 +78,7 @@ FIBARO_TYPEMAP = { "com.fibaro.FGT001": Platform.CLIMATE, "com.fibaro.thermostatDanfoss": Platform.CLIMATE, "com.fibaro.doorLock": Platform.LOCK, + "com.fibaro.binarySensor": Platform.BINARY_SENSOR, } DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema( From 0c58f813c55413f72deddbd8cbb7b5c70d8796b0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 12:55:26 +0200 Subject: [PATCH 0786/3516] Cleanup trigger type hint in deconz (#72358) --- homeassistant/components/deconz/device_trigger.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 43ed3b2cdc4..e15513bddbf 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -1,9 +1,6 @@ """Provides device automations for deconz events.""" - from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -643,8 +640,8 @@ def _get_deconz_event_from_device( async def async_validate_trigger_config( hass: HomeAssistant, - config: dict[str, Any], -) -> vol.Schema: + config: ConfigType, +) -> ConfigType: """Validate config.""" config = TRIGGER_SCHEMA(config) From fad766322cae4b76eb416842fa8a8a22b4776da6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 23 May 2022 13:48:44 +0200 Subject: [PATCH 0787/3516] Do not track Netgear AP or Bridge devices (#69102) --- homeassistant/components/netgear/__init__.py | 31 +++++++++++-- homeassistant/components/netgear/const.py | 3 ++ homeassistant/components/netgear/router.py | 48 +++++++++++--------- 3 files changed, 57 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index 72a56427e17..518a9051847 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -9,7 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( @@ -17,6 +17,7 @@ from .const import ( KEY_COORDINATOR, KEY_COORDINATOR_TRAFFIC, KEY_ROUTER, + MODE_ROUTER, PLATFORMS, ) from .errors import CannotLoginException @@ -69,7 +70,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update_devices() -> bool: """Fetch data from the router.""" - return await router.async_update_device_trackers() + if router.mode == MODE_ROUTER: + return await router.async_update_device_trackers() + return False async def async_update_traffic_meter() -> dict[str, Any] | None: """Fetch data from the router.""" @@ -91,7 +94,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval=SCAN_INTERVAL, ) - await coordinator.async_config_entry_first_refresh() + if router.mode == MODE_ROUTER: + await coordinator.async_config_entry_first_refresh() await coordinator_traffic_meter.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = { @@ -109,11 +113,32 @@ 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) + router = hass.data[DOMAIN][entry.entry_id][KEY_ROUTER] + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) + if router.mode != MODE_ROUTER: + router_id = None + # Remove devices that are no longer tracked + device_registry = dr.async_get(hass) + devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id) + for device_entry in devices: + if device_entry.via_device_id is None: + router_id = device_entry.id + continue # do not remove the router itself + device_registry.async_update_device( + device_entry.id, remove_config_entry_id=entry.entry_id + ) + # Remove entities that are no longer tracked + entity_registry = er.async_get(hass) + entries = er.async_entries_for_config_entry(entity_registry, entry.entry_id) + for entity_entry in entries: + if entity_entry.device_id is not router_id: + entity_registry.async_remove(entity_entry.entity_id) + return unload_ok diff --git a/homeassistant/components/netgear/const.py b/homeassistant/components/netgear/const.py index f2e0263a4e4..bc4f37114fd 100644 --- a/homeassistant/components/netgear/const.py +++ b/homeassistant/components/netgear/const.py @@ -16,6 +16,9 @@ KEY_COORDINATOR_TRAFFIC = "coordinator_traffic" DEFAULT_CONSIDER_HOME = timedelta(seconds=180) DEFAULT_NAME = "Netgear router" +MODE_ROUTER = "0" +MODE_AP = "1" + # models using port 80 instead of 5000 MODELS_PORT_80 = [ "Orbi", diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index f543ba8a5f3..6fb44e569d6 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -32,6 +32,7 @@ from .const import ( DEFAULT_CONSIDER_HOME, DEFAULT_NAME, DOMAIN, + MODE_ROUTER, MODELS_V2, ) from .errors import CannotLoginException @@ -73,6 +74,7 @@ class NetgearRouter: self._info = None self.model = "" + self.mode = MODE_ROUTER self.device_name = "" self.firmware_version = "" self.hardware_version = "" @@ -108,12 +110,13 @@ class NetgearRouter: self.firmware_version = self._info.get("Firmwareversion") self.hardware_version = self._info.get("Hardwareversion") self.serial_number = self._info["SerialNumber"] + self.mode = self._info.get("DeviceMode", MODE_ROUTER) for model in MODELS_V2: if self.model.startswith(model): self.method_version = 2 - if self.method_version == 2: + if self.method_version == 2 and self.mode == MODE_ROUTER: if not self._api.get_attached_devices_2(): _LOGGER.error( "Netgear Model '%s' in MODELS_V2 list, but failed to get attached devices using V2", @@ -130,28 +133,29 @@ class NetgearRouter: return False # set already known devices to away instead of unavailable - device_registry = dr.async_get(self.hass) - devices = dr.async_entries_for_config_entry(device_registry, self.entry_id) - for device_entry in devices: - if device_entry.via_device_id is None: - continue # do not add the router itself + if self.mode == MODE_ROUTER: + device_registry = dr.async_get(self.hass) + devices = dr.async_entries_for_config_entry(device_registry, self.entry_id) + for device_entry in devices: + if device_entry.via_device_id is None: + continue # do not add the router itself - device_mac = dict(device_entry.connections)[dr.CONNECTION_NETWORK_MAC] - self.devices[device_mac] = { - "mac": device_mac, - "name": device_entry.name, - "active": False, - "last_seen": dt_util.utcnow() - timedelta(days=365), - "device_model": None, - "device_type": None, - "type": None, - "link_rate": None, - "signal": None, - "ip": None, - "ssid": None, - "conn_ap_mac": None, - "allow_or_block": None, - } + device_mac = dict(device_entry.connections)[dr.CONNECTION_NETWORK_MAC] + self.devices[device_mac] = { + "mac": device_mac, + "name": device_entry.name, + "active": False, + "last_seen": dt_util.utcnow() - timedelta(days=365), + "device_model": None, + "device_type": None, + "type": None, + "link_rate": None, + "signal": None, + "ip": None, + "ssid": None, + "conn_ap_mac": None, + "allow_or_block": None, + } return True From 071f6d7099d1e755f0800b4145d042a47b64f7d2 Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 23 May 2022 08:13:21 -0400 Subject: [PATCH 0788/3516] Aladdin connect unload cleanup (#71948) --- .../components/aladdin_connect/__init__.py | 15 ++-- .../components/aladdin_connect/config_flow.py | 14 +-- .../components/aladdin_connect/cover.py | 11 +-- .../aladdin_connect/test_config_flow.py | 90 +------------------ .../components/aladdin_connect/test_cover.py | 82 +++++++---------- tests/components/aladdin_connect/test_init.py | 30 +------ 6 files changed, 51 insertions(+), 191 deletions(-) diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index cbd4a195a3a..048624641bd 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -7,7 +7,7 @@ from aladdin_connect import AladdinConnectClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed from .const import DOMAIN @@ -21,12 +21,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] acc = AladdinConnectClient(username, password) - try: - if not await hass.async_add_executor_job(acc.login): - raise ConfigEntryAuthFailed("Incorrect Password") - except (TypeError, KeyError, NameError, ValueError) as ex: - _LOGGER.error("%s", ex) - raise ConfigEntryNotReady from ex + if not await hass.async_add_executor_job(acc.login): + raise ConfigEntryAuthFailed("Incorrect Password") hass.data.setdefault(DOMAIN, {})[entry.entry_id] = acc hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -35,4 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + 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/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index f912d36a3f0..153a63ffb06 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -33,13 +33,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ acc = AladdinConnectClient(data[CONF_USERNAME], data[CONF_PASSWORD]) - try: - login = await hass.async_add_executor_job(acc.login) - except (TypeError, KeyError, NameError, ValueError) as ex: - raise ConnectionError from ex - else: - if not login: - raise InvalidAuth + login = await hass.async_add_executor_job(acc.login) + if not login: + raise InvalidAuth class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -72,8 +68,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: await validate_input(self.hass, data) - except ConnectionError: - errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" else: @@ -107,8 +101,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: await validate_input(self.hass, user_input) - except ConnectionError: - errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 9e18abe21f6..da3e6b81663 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -21,7 +21,7 @@ from homeassistant.const import ( STATE_OPENING, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -63,13 +63,10 @@ async def async_setup_entry( ) -> None: """Set up the Aladdin Connect platform.""" acc = hass.data[DOMAIN][config_entry.entry_id] - try: - doors = await hass.async_add_executor_job(acc.get_doors) - - except (TypeError, KeyError, NameError, ValueError) as ex: - _LOGGER.error("%s", ex) - raise ConfigEntryNotReady from ex + doors = await hass.async_add_executor_job(acc.get_doors) + if doors is None: + raise PlatformNotReady("Error from Aladdin Connect getting doors") async_add_entities( (AladdinDevice(acc, door) for door in doors), update_before_add=True, diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py index 37ec64ba6f0..899aa0a7e55 100644 --- a/tests/components/aladdin_connect/test_config_flow.py +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -2,7 +2,6 @@ from unittest.mock import patch from homeassistant import config_entries -from homeassistant.components.aladdin_connect.config_flow import InvalidAuth from homeassistant.components.aladdin_connect.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -48,15 +47,15 @@ async def test_form(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_invalid_auth(hass: HomeAssistant) -> None: - """Test we handle invalid auth.""" +async def test_form_failed_auth(hass: HomeAssistant) -> None: + """Test we handle failed authentication error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", - side_effect=InvalidAuth, + return_value=False, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -69,43 +68,6 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: assert result2["type"] == RESULT_TYPE_FORM assert result2["errors"] == {"base": "invalid_auth"} - -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.aladdin_connect.config_flow.AladdinConnectClient.login", - side_effect=ConnectionError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, - ) - - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "cannot_connect"} - - with patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", - side_effect=TypeError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, - ) - - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "cannot_connect"} - with patch( "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", return_value=False, @@ -236,7 +198,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None: - """Test a successful reauth flow.""" + """Test an authorization error reauth flow.""" mock_entry = MockConfigEntry( domain=DOMAIN, @@ -277,47 +239,3 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None: assert result2["type"] == RESULT_TYPE_FORM assert result2["errors"] == {"base": "invalid_auth"} - - -async def test_reauth_flow_other_error(hass: HomeAssistant) -> None: - """Test an unsuccessful reauth flow.""" - - 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_REAUTH, - "unique_id": mock_entry.unique_id, - "entry_id": mock_entry.entry_id, - }, - data={"username": "test-username", "password": "new-password"}, - ) - - assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {} - - with patch( - "homeassistant.components.aladdin_connect.cover.async_setup_platform", - return_value=True, - ), patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", - side_effect=ValueError, - ), patch( - "homeassistant.components.aladdin_connect.cover.async_setup_entry", - return_value=True, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_PASSWORD: "new-password"}, - ) - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/aladdin_connect/test_cover.py b/tests/components/aladdin_connect/test_cover.py index 4904d904ad4..c1571ed9fa2 100644 --- a/tests/components/aladdin_connect/test_cover.py +++ b/tests/components/aladdin_connect/test_cover.py @@ -4,7 +4,6 @@ from unittest.mock import patch import pytest from homeassistant.components.aladdin_connect.const import DOMAIN -import homeassistant.components.aladdin_connect.cover as cover from homeassistant.components.cover import DOMAIN as COVER_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( @@ -77,18 +76,7 @@ DEVICE_CONFIG_BAD_NO_DOOR = { } -@pytest.mark.parametrize( - "side_effect", - [ - (TypeError), - (KeyError), - (NameError), - (ValueError), - ], -) -async def test_setup_get_doors_errors( - hass: HomeAssistant, side_effect: Exception -) -> None: +async def test_setup_get_doors_errors(hass: HomeAssistant) -> None: """Test component setup Get Doors Errors.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -101,23 +89,14 @@ async def test_setup_get_doors_errors( return_value=True, ), patch( "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - side_effect=side_effect, + return_value=None, ): assert await hass.config_entries.async_setup(config_entry.entry_id) is True await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 -@pytest.mark.parametrize( - "side_effect", - [ - (TypeError), - (KeyError), - (NameError), - (ValueError), - ], -) -async def test_setup_login_error(hass: HomeAssistant, side_effect: Exception) -> None: +async def test_setup_login_error(hass: HomeAssistant) -> None: """Test component setup Login Errors.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -127,11 +106,9 @@ async def test_setup_login_error(hass: HomeAssistant, side_effect: Exception) -> config_entry.add_to_hass(hass) with patch( "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", - side_effect=side_effect, + return_value=False, ): assert await hass.config_entries.async_setup(config_entry.entry_id) is False - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 async def test_setup_component_noerror(hass: HomeAssistant) -> None: @@ -183,36 +160,27 @@ async def test_cover_operation(hass: HomeAssistant) -> None: with patch( "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.open_door", return_value=True, - ): - await hass.services.async_call( - "cover", "open_cover", {"entity_id": "cover.home"}, blocking=True - ) - - with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.close_door", - return_value=True, - ): - await hass.services.async_call( - "cover", "close_cover", {"entity_id": "cover.home"}, blocking=True - ) - with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=[DEVICE_CONFIG_CLOSED], - ): - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True - ) - assert hass.states.get("cover.home").state == STATE_CLOSED - - with patch( + ), patch( "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", return_value=[DEVICE_CONFIG_OPEN], ): await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True + "cover", "open_cover", {"entity_id": "cover.home"}, blocking=True ) assert hass.states.get("cover.home").state == STATE_OPEN + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.close_door", + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=[DEVICE_CONFIG_CLOSED], + ): + await hass.services.async_call( + "cover", "close_cover", {"entity_id": "cover.home"}, blocking=True + ) + assert hass.states.get("cover.home").state == STATE_CLOSED + with patch( "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", return_value=[DEVICE_CONFIG_OPENING], @@ -261,9 +229,19 @@ async def test_yaml_import(hass: HomeAssistant, caplog: pytest.LogCaptureFixture "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", return_value=[DEVICE_CONFIG_CLOSED], ): - await cover.async_setup_platform(hass, YAML_CONFIG, None) + await async_setup_component( + hass, + COVER_DOMAIN, + { + COVER_DOMAIN: { + "platform": DOMAIN, + "username": "test-user", + "password": "test-password", + } + }, + ) await hass.async_block_till_done() - + assert hass.config_entries.async_entries(DOMAIN) assert "Configuring Aladdin Connect through yaml is deprecated" in caplog.text assert hass.config_entries.async_entries(DOMAIN) diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py index c9814adb051..0ba9b317dfb 100644 --- a/tests/components/aladdin_connect/test_init.py +++ b/tests/components/aladdin_connect/test_init.py @@ -4,39 +4,14 @@ from unittest.mock import patch from homeassistant.components.aladdin_connect.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry YAML_CONFIG = {"username": "test-user", "password": "test-password"} -async def test_unload_entry(hass: HomeAssistant): - """Test successful unload of entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={"username": "test-user", "password": "test-password"}, - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", - return_value=True, - ): - - assert (await async_setup_component(hass, DOMAIN, entry)) is True - - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state is 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 - - async def test_entry_password_fail(hass: HomeAssistant): - """Test successful unload of entry.""" + """Test password fail during entry.""" entry = MockConfigEntry( domain=DOMAIN, data={"username": "test-user", "password": "test-password"}, @@ -48,7 +23,8 @@ async def test_entry_password_fail(hass: HomeAssistant): return_value=False, ): - assert (await async_setup_component(hass, DOMAIN, entry)) is True + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert entry.state is ConfigEntryState.SETUP_ERROR From 6ec755a79d4597116967cf7de1fdf41b6c91498e Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Mon, 23 May 2022 14:28:56 +0200 Subject: [PATCH 0789/3516] Update board file list to reflect currently available boards (#72085) * Update board file list to reflect currently available boards * Warn if board does not exist and migrate Intel NUC to Generic x86-64 * Remove integration in case a old board is referenced * Don't remove the config entry automatically/fix logging message * Address pylint issue --- homeassistant/components/version/__init__.py | 17 ++++++++++++++++- homeassistant/components/version/const.py | 6 +++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/version/__init__.py b/homeassistant/components/version/__init__.py index 75545adb8db..22dbad36d7b 100644 --- a/homeassistant/components/version/__init__.py +++ b/homeassistant/components/version/__init__.py @@ -1,6 +1,8 @@ """The Version integration.""" from __future__ import annotations +import logging + from pyhaversion import HaVersion from homeassistant.config_entries import ConfigEntry @@ -18,16 +20,29 @@ from .const import ( ) from .coordinator import VersionDataUpdateCoordinator +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the version integration from a config entry.""" + + board = entry.data[CONF_BOARD] + + if board not in BOARD_MAP: + _LOGGER.error( + 'Board "%s" is (no longer) valid. Please remove the integration "%s"', + board, + entry.title, + ) + return False + coordinator = VersionDataUpdateCoordinator( hass=hass, api=HaVersion( session=async_get_clientsession(hass), source=entry.data[CONF_SOURCE], image=entry.data[CONF_IMAGE], - board=BOARD_MAP[entry.data[CONF_BOARD]], + board=BOARD_MAP[board], channel=entry.data[CONF_CHANNEL].lower(), ), ) diff --git a/homeassistant/components/version/const.py b/homeassistant/components/version/const.py index 419e49d7240..1693f79ec64 100644 --- a/homeassistant/components/version/const.py +++ b/homeassistant/components/version/const.py @@ -61,8 +61,6 @@ HA_VERSION_SOURCES: Final[list[str]] = [source.value for source in HaVersionSour BOARD_MAP: Final[dict[str, str]] = { "OVA": "ova", - "RaspberryPi": "rpi", - "RaspberryPi Zero-W": "rpi0-w", "RaspberryPi 2": "rpi2", "RaspberryPi 3": "rpi3", "RaspberryPi 3 64bit": "rpi3-64", @@ -73,8 +71,10 @@ BOARD_MAP: Final[dict[str, str]] = { "ODROID C4": "odroid-c4", "ODROID N2": "odroid-n2", "ODROID XU4": "odroid-xu4", + "Generic AArch64": "generic-aarch64", "Generic x86-64": "generic-x86-64", - "Intel NUC": "intel-nuc", + "Home Assistant Yellow": "yellow", + "Khadas VIM3": "khadas-vim3", } VALID_BOARDS: Final[list[str]] = list(BOARD_MAP) From dfc8dee2d65aace19ec16932d7ac740ca354030c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 14:32:47 +0200 Subject: [PATCH 0790/3516] Adjust device_automation type hints in rfxtrx (#72138) --- .../components/rfxtrx/device_action.py | 18 +++++++++++++----- .../components/rfxtrx/device_trigger.py | 10 +++++++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/rfxtrx/device_action.py b/homeassistant/components/rfxtrx/device_action.py index 37fb39cb499..e8fc8c6707d 100644 --- a/homeassistant/components/rfxtrx/device_action.py +++ b/homeassistant/components/rfxtrx/device_action.py @@ -9,6 +9,7 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE from homeassistant.core import Context, HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DATA_RFXOBJECT, DOMAIN from .helpers import async_get_device_object @@ -37,7 +38,9 @@ ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( ) -async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: +async def async_get_actions( + hass: HomeAssistant, device_id: str +) -> list[dict[str, str]]: """List device actions for RFXCOM RFXtrx devices.""" try: @@ -48,8 +51,8 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: actions = [] for action_type in ACTION_TYPES: if hasattr(device, action_type): - values = getattr(device, ACTION_SELECTION[action_type], {}) - for value in values.values(): + data: dict[int, str] = getattr(device, ACTION_SELECTION[action_type], {}) + for value in data.values(): actions.append( { CONF_DEVICE_ID: device_id, @@ -69,7 +72,9 @@ def _get_commands(hass, device_id, action_type): return commands, send_fun -async def async_validate_action_config(hass, config): +async def async_validate_action_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: """Validate config.""" config = ACTION_SCHEMA(config) commands, _ = _get_commands(hass, config[CONF_DEVICE_ID], config[CONF_TYPE]) @@ -84,7 +89,10 @@ async def async_validate_action_config(hass, config): async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Context | None + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context | None, ) -> None: """Execute a device action.""" config = ACTION_SCHEMA(config) diff --git a/homeassistant/components/rfxtrx/device_trigger.py b/homeassistant/components/rfxtrx/device_trigger.py index 9ab10ca7f2b..196377dd60f 100644 --- a/homeassistant/components/rfxtrx/device_trigger.py +++ b/homeassistant/components/rfxtrx/device_trigger.py @@ -47,13 +47,15 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( ) -async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: +async def async_get_triggers( + hass: HomeAssistant, device_id: str +) -> list[dict[str, str]]: """List device triggers for RFXCOM RFXtrx devices.""" device = async_get_device_object(hass, device_id) triggers = [] for conf_type in TRIGGER_TYPES: - data = getattr(device, TRIGGER_SELECTION[conf_type], {}) + data: dict[int, str] = getattr(device, TRIGGER_SELECTION[conf_type], {}) for command in data.values(): triggers.append( { @@ -67,7 +69,9 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> list[dict]: return triggers -async def async_validate_trigger_config(hass, config): +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: """Validate config.""" config = TRIGGER_SCHEMA(config) From df3e3b52a0ca221f029a880fb5bfa0786c3b72b9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 14:34:55 +0200 Subject: [PATCH 0791/3516] Adjust device_automation type hints in lcn (#72132) --- homeassistant/components/lcn/device_trigger.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lcn/device_trigger.py b/homeassistant/components/lcn/device_trigger.py index 35575a442b2..a6fc17759b8 100644 --- a/homeassistant/components/lcn/device_trigger.py +++ b/homeassistant/components/lcn/device_trigger.py @@ -1,8 +1,6 @@ """Provides device triggers for LCN.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -53,7 +51,7 @@ TYPE_SCHEMAS = { async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for LCN devices.""" device_registry = dr.async_get(hass) if (device := device_registry.async_get(device_id)) is None: @@ -101,6 +99,8 @@ async def async_attach_trigger( ) -async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: +async def async_get_trigger_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: """List trigger capabilities.""" return TYPE_SCHEMAS.get(config[CONF_TYPE], {}) From 5bb39a1db576f99832764e94b99519dfd37faade Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 14:37:44 +0200 Subject: [PATCH 0792/3516] Adjust device_automation type hints in zwave_js (#72143) --- homeassistant/components/zwave_js/device_action.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index a67bd44e533..f001accd196 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -27,7 +27,7 @@ from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from .config_validation import VALUE_SCHEMA from .const import ( @@ -141,7 +141,9 @@ ACTION_SCHEMA = vol.Any( ) -async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: +async def async_get_actions( + hass: HomeAssistant, device_id: str +) -> list[dict[str, Any]]: """List device actions for Z-Wave JS devices.""" registry = entity_registry.async_get(hass) actions = [] @@ -238,7 +240,10 @@ async def async_get_actions(hass: HomeAssistant, device_id: str) -> list[dict]: async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Context | None + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context | None, ) -> None: """Execute a device action.""" action_type = service = config[CONF_TYPE] From 9763b44357380b10cf7c36001cd1b77fc6aeb1cf Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Mon, 23 May 2022 14:38:40 +0200 Subject: [PATCH 0793/3516] Remove uneeded patch statements in here_travel_time (#72361) --- .../here_travel_time/test_sensor.py | 103 ++++++++---------- 1 file changed, 48 insertions(+), 55 deletions(-) diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 585d5adc9d3..70470aee9d3 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -53,7 +53,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util from .const import ( API_KEY, @@ -264,62 +263,59 @@ async def test_no_attribution(hass: HomeAssistant): async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock): """Test that origin/destination supplied by entities works.""" - utcnow = dt_util.utcnow() - # Patching 'utcnow' to gain more control over the timed update. - with patch("homeassistant.util.dt.utcnow", return_value=utcnow): - zone_config = { - "zone": [ - { - "name": "Origin", - "latitude": CAR_ORIGIN_LATITUDE, - "longitude": CAR_ORIGIN_LONGITUDE, - "radius": 250, - "passive": False, - }, - ] - } - assert await async_setup_component(hass, "zone", zone_config) - hass.states.async_set( - "device_tracker.test", - "not_home", + zone_config = { + "zone": [ { - "latitude": float(CAR_DESTINATION_LATITUDE), - "longitude": float(CAR_DESTINATION_LONGITUDE), + "name": "Origin", + "latitude": CAR_ORIGIN_LATITUDE, + "longitude": CAR_ORIGIN_LONGITUDE, + "radius": 250, + "passive": False, }, - ) - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="0123456789", - data={ - CONF_ORIGIN_ENTITY_ID: "zone.origin", - CONF_DESTINATION_ENTITY_ID: "device_tracker.test", - CONF_API_KEY: API_KEY, - CONF_MODE: TRAVEL_MODE_TRUCK, - CONF_NAME: "test", - }, - ) - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + ] + } + assert await async_setup_component(hass, "zone", zone_config) + hass.states.async_set( + "device_tracker.test", + "not_home", + { + "latitude": float(CAR_DESTINATION_LATITUDE), + "longitude": float(CAR_DESTINATION_LONGITUDE), + }, + ) + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={ + CONF_ORIGIN_ENTITY_ID: "zone.origin", + CONF_DESTINATION_ENTITY_ID: "device_tracker.test", + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_TRUCK, + CONF_NAME: "test", + }, + ) + 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() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() - sensor = hass.states.get("sensor.test") - assert sensor.attributes.get(ATTR_DISTANCE) == 23.903 + sensor = hass.states.get("sensor.test") + assert sensor.attributes.get(ATTR_DISTANCE) == 23.903 - valid_response.assert_called_with( - [CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE], - [CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE], - True, - [ - RouteMode[ROUTE_MODE_FASTEST], - RouteMode[TRAVEL_MODE_TRUCK], - RouteMode[TRAFFIC_MODE_ENABLED], - ], - arrival=None, - departure="now", - ) + valid_response.assert_called_with( + [CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE], + [CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE], + True, + [ + RouteMode[ROUTE_MODE_FASTEST], + RouteMode[TRAVEL_MODE_TRUCK], + RouteMode[TRAFFIC_MODE_ENABLED], + ], + arrival=None, + departure="now", + ) @pytest.mark.usefixtures("valid_response") @@ -433,9 +429,6 @@ async def test_invalid_origin_entity_state(hass: HomeAssistant, caplog): async def test_route_not_found(hass: HomeAssistant, caplog): """Test that route not found error is correctly handled.""" with patch( - "homeassistant.components.here_travel_time.config_flow.validate_api_key", - return_value=None, - ), patch( "herepy.RoutingApi.public_transport_timetable", side_effect=NoRouteFoundError, ): From dc76cce96b8af4e8e02b3c9169e23d7da7bb4821 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 23 May 2022 05:44:45 -0700 Subject: [PATCH 0794/3516] Add SENZ application credentials platform (#72338) --- homeassistant/components/senz/__init__.py | 42 ++++++++++++------- .../senz/application_credentials.py | 14 +++++++ homeassistant/components/senz/manifest.json | 2 +- .../generated/application_credentials.py | 1 + tests/components/senz/test_config_flow.py | 1 - 5 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/senz/application_credentials.py diff --git a/homeassistant/components/senz/__init__.py b/homeassistant/components/senz/__init__.py index b6fc2422888..08aa26fa3c5 100644 --- a/homeassistant/components/senz/__init__.py +++ b/homeassistant/components/senz/__init__.py @@ -4,10 +4,14 @@ from __future__ import annotations from datetime import timedelta import logging -from aiosenz import AUTHORIZATION_ENDPOINT, SENZAPI, TOKEN_ENDPOINT, Thermostat +from aiosenz import SENZAPI, Thermostat from httpx import RequestError import voluptuous as vol +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform from homeassistant.core import HomeAssistant @@ -20,7 +24,6 @@ from homeassistant.helpers import ( from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from . import config_flow from .api import SENZConfigEntryAuth from .const import DOMAIN @@ -29,14 +32,17 @@ UPDATE_INTERVAL = timedelta(seconds=30) _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -52,17 +58,21 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if DOMAIN not in config: return True - config_flow.OAuth2FlowHandler.async_register_implementation( + await async_import_client_credential( hass, - config_entry_oauth2_flow.LocalOAuth2Implementation( - hass, - DOMAIN, + DOMAIN, + ClientCredential( config[DOMAIN][CONF_CLIENT_ID], config[DOMAIN][CONF_CLIENT_SECRET], - AUTHORIZATION_ENDPOINT, - TOKEN_ENDPOINT, ), ) + _LOGGER.warning( + "Configuration of SENZ integration in YAML is deprecated " + "and will be removed in a future release; Your existing OAuth " + "Application Credentials have been imported into the UI " + "automatically and can be safely removed from your " + "configuration.yaml file" + ) return True diff --git a/homeassistant/components/senz/application_credentials.py b/homeassistant/components/senz/application_credentials.py new file mode 100644 index 00000000000..205f00ff33f --- /dev/null +++ b/homeassistant/components/senz/application_credentials.py @@ -0,0 +1,14 @@ +"""Application credentials platform for senz.""" + +from aiosenz import AUTHORIZATION_ENDPOINT, TOKEN_ENDPOINT + +from homeassistant.components.application_credentials import AuthorizationServer +from homeassistant.core import HomeAssistant + + +async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: + """Return authorization server.""" + return AuthorizationServer( + authorize_url=AUTHORIZATION_ENDPOINT, + token_url=TOKEN_ENDPOINT, + ) diff --git a/homeassistant/components/senz/manifest.json b/homeassistant/components/senz/manifest.json index e9b6165cb26..937a20d8482 100644 --- a/homeassistant/components/senz/manifest.json +++ b/homeassistant/components/senz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/senz", "requirements": ["aiosenz==1.0.0"], - "dependencies": ["auth"], + "dependencies": ["application_credentials"], "codeowners": ["@milanmeu"], "iot_class": "cloud_polling" } diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index d7b3259a158..63c19fe10b8 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -11,6 +11,7 @@ APPLICATION_CREDENTIALS = [ "home_connect", "neato", "netatmo", + "senz", "spotify", "withings", "xbox", diff --git a/tests/components/senz/test_config_flow.py b/tests/components/senz/test_config_flow.py index 5143acea60a..3906f2c0320 100644 --- a/tests/components/senz/test_config_flow.py +++ b/tests/components/senz/test_config_flow.py @@ -24,7 +24,6 @@ async def test_full_flow( "senz", { "senz": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, - "http": {"base_url": "https://example.com"}, }, ) From 52686aae05bc2667863f592808767baaec300c90 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 14:53:12 +0200 Subject: [PATCH 0795/3516] Adjust device_automation type hints in nanoleaf (#72134) --- homeassistant/components/nanoleaf/device_trigger.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/nanoleaf/device_trigger.py b/homeassistant/components/nanoleaf/device_trigger.py index 311d12506ba..68dc6326719 100644 --- a/homeassistant/components/nanoleaf/device_trigger.py +++ b/homeassistant/components/nanoleaf/device_trigger.py @@ -1,8 +1,6 @@ """Provides device triggers for Nanoleaf.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -38,7 +36,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for Nanoleaf devices.""" device_registry = dr.async_get(hass) device_entry = device_registry.async_get(device_id) From bcc3c93b4ec96582fcd3c7d51ac51cd7207b64ef Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 14:56:17 +0200 Subject: [PATCH 0796/3516] Adjust device_automation type hints in wemo (#72141) --- homeassistant/components/wemo/device_trigger.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/wemo/device_trigger.py b/homeassistant/components/wemo/device_trigger.py index 1cdc29fa995..973a3358b1b 100644 --- a/homeassistant/components/wemo/device_trigger.py +++ b/homeassistant/components/wemo/device_trigger.py @@ -1,8 +1,6 @@ """Triggers for WeMo devices.""" from __future__ import annotations -from typing import Any - from pywemo.subscribe import EVENT_TYPE_LONG_PRESS import voluptuous as vol @@ -30,7 +28,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """Return a list of triggers.""" wemo_trigger = { From caa24121032f901896ff9da355f1b9e060368b50 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 14:57:09 +0200 Subject: [PATCH 0797/3516] Adjust device_automation type hints in philips_js (#72137) --- .../components/philips_js/device_trigger.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/philips_js/device_trigger.py b/homeassistant/components/philips_js/device_trigger.py index a48f9e58331..69a5932576f 100644 --- a/homeassistant/components/philips_js/device_trigger.py +++ b/homeassistant/components/philips_js/device_trigger.py @@ -1,8 +1,6 @@ """Provides device automations for control of device.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -12,6 +10,7 @@ from homeassistant.components.automation import ( from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import ConfigType @@ -30,7 +29,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for device.""" triggers = [] triggers.append( @@ -50,18 +49,18 @@ async def async_attach_trigger( config: ConfigType, action: AutomationActionType, automation_info: AutomationTriggerInfo, -) -> CALLBACK_TYPE | None: +) -> CALLBACK_TYPE: """Attach a trigger.""" trigger_data = automation_info["trigger_data"] registry: dr.DeviceRegistry = dr.async_get(hass) - if config[CONF_TYPE] == TRIGGER_TYPE_TURN_ON: + if (trigger_type := config[CONF_TYPE]) == TRIGGER_TYPE_TURN_ON: variables = { "trigger": { **trigger_data, "platform": "device", "domain": DOMAIN, "device_id": config[CONF_DEVICE_ID], - "description": f"philips_js '{config[CONF_TYPE]}' event", + "description": f"philips_js '{trigger_type}' event", } } @@ -73,4 +72,4 @@ async def async_attach_trigger( if coordinator: return coordinator.turn_on.async_attach(action, variables) - return None + raise HomeAssistantError(f"Unhandled trigger type {trigger_type}") From ee5f1b15776e7c3008de8139f477ded63aee72e9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 14:59:12 +0200 Subject: [PATCH 0798/3516] Adjust device_automation type hints in hue (#72144) --- .../components/hue/device_trigger.py | 18 +++++++---- .../components/hue/v1/device_trigger.py | 32 ++++++++++++++----- .../components/hue/v2/device_trigger.py | 12 ++++--- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/hue/device_trigger.py b/homeassistant/components/hue/device_trigger.py index 29df1fd2476..e8eb7695ed9 100644 --- a/homeassistant/components/hue/device_trigger.py +++ b/homeassistant/components/hue/device_trigger.py @@ -1,7 +1,7 @@ """Provides device automations for Philips Hue events.""" from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -33,7 +33,9 @@ if TYPE_CHECKING: from .bridge import HueBridge -async def async_validate_trigger_config(hass: "HomeAssistant", config: ConfigType): +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: """Validate config.""" if DOMAIN not in hass.data: # happens at startup @@ -51,13 +53,14 @@ async def async_validate_trigger_config(hass: "HomeAssistant", config: ConfigTyp if bridge.api_version == 1: return await async_validate_trigger_config_v1(bridge, device_entry, config) return await async_validate_trigger_config_v2(bridge, device_entry, config) + return config async def async_attach_trigger( - hass: "HomeAssistant", + hass: HomeAssistant, config: ConfigType, - action: "AutomationActionType", - automation_info: "AutomationTriggerInfo", + action: AutomationActionType, + automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" device_id = config[CONF_DEVICE_ID] @@ -82,7 +85,9 @@ async def async_attach_trigger( ) -async def async_get_triggers(hass: "HomeAssistant", device_id: str): +async def async_get_triggers( + hass: HomeAssistant, device_id: str +) -> list[dict[str, Any]]: """Get device triggers for given (hass) device id.""" if DOMAIN not in hass.data: return [] @@ -101,3 +106,4 @@ async def async_get_triggers(hass: "HomeAssistant", device_id: str): if bridge.api_version == 1: return async_get_triggers_v1(bridge, device_entry) return async_get_triggers_v2(bridge, device_entry) + return [] diff --git a/homeassistant/components/hue/v1/device_trigger.py b/homeassistant/components/hue/v1/device_trigger.py index 7b58bf42089..579f4b71efb 100644 --- a/homeassistant/components/hue/v1/device_trigger.py +++ b/homeassistant/components/hue/v1/device_trigger.py @@ -3,6 +3,10 @@ from typing import TYPE_CHECKING import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -16,8 +20,9 @@ from homeassistant.const import ( CONF_TYPE, CONF_UNIQUE_ID, ) -from homeassistant.core import callback +from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.typing import ConfigType from ..const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN @@ -95,7 +100,7 @@ HUE_FOHSWITCH_REMOTE = { } -REMOTES = { +REMOTES: dict[str, dict[tuple[str, str], dict[str, int]]] = { HUE_DIMMER_REMOTE_MODEL: HUE_DIMMER_REMOTE, HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE, HUE_BUTTON_REMOTE_MODEL: HUE_BUTTON_REMOTE, @@ -114,7 +119,9 @@ def _get_hue_event_from_device_id(hass, device_id): return None -async def async_validate_trigger_config(bridge, device_entry, config): +async def async_validate_trigger_config( + bridge: "HueBridge", device_entry: DeviceEntry, config: ConfigType +) -> ConfigType: """Validate config.""" config = TRIGGER_SCHEMA(config) trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) @@ -137,7 +144,13 @@ async def async_validate_trigger_config(bridge, device_entry, config): return config -async def async_attach_trigger(bridge, device_entry, config, action, automation_info): +async def async_attach_trigger( + bridge: "HueBridge", + device_entry: DeviceEntry, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" hass = bridge.hass @@ -145,9 +158,10 @@ async def async_attach_trigger(bridge, device_entry, config, action, automation_ if hue_event is None: raise InvalidDeviceAutomationConfig - trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + trigger_key: tuple[str, str] = (config[CONF_TYPE], config[CONF_SUBTYPE]) - trigger = REMOTES[device_entry.model][trigger] + assert device_entry.model + trigger = REMOTES[device_entry.model][trigger_key] event_config = { event_trigger.CONF_PLATFORM: "event", @@ -162,7 +176,9 @@ async def async_attach_trigger(bridge, device_entry, config, action, automation_ @callback -def async_get_triggers(bridge: "HueBridge", device: DeviceEntry): +def async_get_triggers( + bridge: "HueBridge", device: DeviceEntry +) -> list[dict[str, str]]: """Return device triggers for device on `v1` bridge. Make sure device is a supported remote model. @@ -170,7 +186,7 @@ def async_get_triggers(bridge: "HueBridge", device: DeviceEntry): Generate device trigger list. """ if device.model not in REMOTES: - return + return [] triggers = [] for trigger, subtype in REMOTES[device.model]: diff --git a/homeassistant/components/hue/v2/device_trigger.py b/homeassistant/components/hue/v2/device_trigger.py index 8c5da8febb9..2b868a4685c 100644 --- a/homeassistant/components/hue/v2/device_trigger.py +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -1,7 +1,7 @@ """Provides device automations for Philips Hue events.""" from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from aiohue.v2.models.button import ButtonEvent from aiohue.v2.models.resource import ResourceTypes @@ -86,7 +86,7 @@ async def async_validate_trigger_config( bridge: "HueBridge", device_entry: DeviceEntry, config: ConfigType, -): +) -> ConfigType: """Validate config.""" config = TRIGGER_SCHEMA(config) check_invalid_device_trigger(bridge, config, device_entry) @@ -97,8 +97,8 @@ async def async_attach_trigger( bridge: "HueBridge", device_entry: DeviceEntry, config: ConfigType, - action: "AutomationActionType", - automation_info: "AutomationTriggerInfo", + action: AutomationActionType, + automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" hass = bridge.hass @@ -120,7 +120,9 @@ async def async_attach_trigger( @callback -def async_get_triggers(bridge: HueBridge, device_entry: DeviceEntry): +def async_get_triggers( + bridge: HueBridge, device_entry: DeviceEntry +) -> list[dict[str, Any]]: """Return device triggers for device on `v2` bridge.""" api: HueBridgeV2 = bridge.api From 5137e6b18d9df386649fdcb635662217dcf03567 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 15:00:24 +0200 Subject: [PATCH 0799/3516] Adjust device_automation type hints in tasmota (#72201) --- homeassistant/components/tasmota/device_trigger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py index 512fa5dd547..26b1d9debf1 100644 --- a/homeassistant/components/tasmota/device_trigger.py +++ b/homeassistant/components/tasmota/device_trigger.py @@ -291,7 +291,7 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: Callable, + action: AutomationActionType, automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Attach a device trigger.""" From 82d4d96672c3c3a568bf0eed84b45a05eb2f9c6f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 15:38:55 +0200 Subject: [PATCH 0800/3516] Adjust config_flow type hints in amberelectric (#72236) --- .../components/amberelectric/config_flow.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/amberelectric/config_flow.py b/homeassistant/components/amberelectric/config_flow.py index efb5ddfb931..0258fdf4cb4 100644 --- a/homeassistant/components/amberelectric/config_flow.py +++ b/homeassistant/components/amberelectric/config_flow.py @@ -1,8 +1,6 @@ """Config flow for the Amber Electric integration.""" from __future__ import annotations -from typing import Any - import amberelectric from amberelectric.api import amber_api from amberelectric.model.site import Site @@ -10,6 +8,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_TOKEN +from homeassistant.data_entry_flow import FlowResult from .const import CONF_SITE_ID, CONF_SITE_NAME, CONF_SITE_NMI, DOMAIN @@ -44,7 +43,9 @@ class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._errors[CONF_API_TOKEN] = "unknown_error" return None - async def async_step_user(self, user_input: dict[str, Any] | None = None): + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Step when user initializes a integration.""" self._errors = {} self._sites = None @@ -76,11 +77,14 @@ class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=self._errors, ) - async def async_step_site(self, user_input: dict[str, Any] = None): + async def async_step_site( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Step to select site.""" self._errors = {} assert self._sites is not None + assert self._api_token is not None api_token = self._api_token if user_input is not None: From cc7a0e3c245d7296bf82233ae5a9d6828836390b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 23 May 2022 15:41:56 +0200 Subject: [PATCH 0801/3516] Streamline setup of deCONZ sensor platform (#71905) --- homeassistant/components/deconz/gateway.py | 47 +------ homeassistant/components/deconz/sensor.py | 133 +++++++------------- homeassistant/components/deconz/services.py | 3 - 3 files changed, 49 insertions(+), 134 deletions(-) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index d59c5e2d160..1f6a45f3ad6 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -9,12 +9,7 @@ from typing import TYPE_CHECKING, Any, cast import async_timeout from pydeconz import DeconzSession, errors -from pydeconz.models import ResourceGroup -from pydeconz.models.alarm_system import AlarmSystem as DeconzAlarmSystem from pydeconz.models.event import EventType -from pydeconz.models.group import Group as DeconzGroup -from pydeconz.models.light import LightBase as DeconzLight -from pydeconz.models.sensor import SensorBase as DeconzSensor from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT @@ -57,7 +52,6 @@ class DeconzGateway: self.config_entry = config_entry self.api = api - api.add_device_callback = self.async_add_device_callback api.connection_status_callback = self.async_connection_status_callback self.available = True @@ -67,14 +61,6 @@ class DeconzGateway: self.signal_reload_groups = f"deconz_reload_group_{config_entry.entry_id}" self.signal_reload_clip_sensors = f"deconz_reload_clip_{config_entry.entry_id}" - self.signal_new_light = f"deconz_new_light_{config_entry.entry_id}" - self.signal_new_sensor = f"deconz_new_sensor_{config_entry.entry_id}" - - self.deconz_resource_type_to_signal_new_device = { - ResourceGroup.LIGHT.value: self.signal_new_light, - ResourceGroup.SENSOR.value: self.signal_new_sensor, - } - self.deconz_ids: dict[str, str] = {} self.entities: dict[str, set[str]] = {} self.events: list[DeconzAlarmEvent | DeconzEvent] = [] @@ -153,37 +139,6 @@ class DeconzGateway: self.ignore_state_updates = False async_dispatcher_send(self.hass, self.signal_reachable) - @callback - def async_add_device_callback( - self, - resource_type: str, - device: DeconzAlarmSystem - | DeconzGroup - | DeconzLight - | DeconzSensor - | list[DeconzAlarmSystem | DeconzGroup | DeconzLight | DeconzSensor] - | None = None, - force: bool = False, - ) -> None: - """Handle event of new device creation in deCONZ.""" - 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 = [] - - if device is not None and not isinstance(device, list): - args.append([device]) - - async_dispatcher_send( - self.hass, - self.deconz_resource_type_to_signal_new_device[resource_type], - *args, # Don't send device if None, it would override default value in listeners - ) - async def async_update_device_registry(self) -> None: """Update device registry.""" if self.api.config.mac is None: @@ -237,7 +192,6 @@ class DeconzGateway: deconz_ids = [] if self.option_allow_clip_sensor: - self.async_add_device_callback(ResourceGroup.SENSOR.value) async_dispatcher_send(self.hass, self.signal_reload_clip_sensors) else: @@ -314,6 +268,7 @@ async def get_deconz_session( config[CONF_HOST], config[CONF_PORT], config[CONF_API_KEY], + legacy_add_device=False, ) try: async with async_timeout.timeout(10): diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index bf29e8db478..021dcf7168a 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from datetime import datetime from pydeconz.interfaces.sensors import SensorResources +from pydeconz.models.event import EventType from pydeconz.models.sensor.air_quality import AirQuality from pydeconz.models.sensor.consumption import Consumption from pydeconz.models.sensor.daylight import Daylight @@ -38,10 +39,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -244,61 +242,52 @@ async def async_setup_entry( gateway = get_gateway_from_config_entry(hass, config_entry) gateway.entities[DOMAIN] = set() - battery_handler = DeconzBatteryHandler(gateway) - @callback - def async_add_sensor(sensors: list[SensorResources] | None = None) -> None: - """Add sensors from deCONZ. + def async_add_sensor(_: EventType, sensor_id: str) -> None: + """Add sensor from deCONZ.""" + sensor = gateway.api.sensors[sensor_id] - Create DeconzBattery if sensor has a battery attribute. - Create DeconzSensor if not a battery, switch or thermostat and not a binary sensor. - """ - entities: list[DeconzSensor] = [] + if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): + return - if sensors is None: - sensors = gateway.api.sensors.values() + if sensor.battery is None: + DeconzBatteryTracker(sensor_id, gateway, async_add_entities) - for sensor in sensors: - - if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): + for description in ( + ENTITY_DESCRIPTIONS.get(type(sensor), []) + SENSOR_DESCRIPTIONS + ): + if ( + not hasattr(sensor, description.key) + or description.value_fn(sensor) is None + ): continue - if sensor.battery is None: - battery_handler.create_tracker(sensor) + async_add_entities([DeconzSensor(sensor, gateway, description)]) - known_entities = set(gateway.entities[DOMAIN]) - for description in ( - ENTITY_DESCRIPTIONS.get(type(sensor), []) + SENSOR_DESCRIPTIONS - ): + config_entry.async_on_unload( + gateway.api.sensors.subscribe( + gateway.evaluate_add_device(async_add_sensor), + EventType.ADDED, + ) + ) + for sensor_id in gateway.api.sensors: + async_add_sensor(EventType.ADDED, sensor_id) - if ( - not hasattr(sensor, description.key) - or description.value_fn(sensor) is None - ): - continue - - new_entity = DeconzSensor(sensor, gateway, description) - if new_entity.unique_id not in known_entities: - entities.append(new_entity) - - if description.key == "battery": - battery_handler.remove_tracker(sensor) - - if entities: - async_add_entities(entities) + @callback + def async_reload_clip_sensors() -> None: + """Load clip sensor sensors from deCONZ.""" + for sensor_id, sensor in gateway.api.sensors.items(): + if sensor.type.startswith("CLIP"): + async_add_sensor(EventType.ADDED, sensor_id) config_entry.async_on_unload( async_dispatcher_connect( hass, - gateway.signal_new_sensor, - async_add_sensor, + gateway.signal_reload_clip_sensors, + async_reload_clip_sensors, ) ) - async_add_sensor( - [gateway.api.sensors[key] for key in sorted(gateway.api.sensors, key=int)] - ) - class DeconzSensor(DeconzDevice, SensorEntity): """Representation of a deCONZ sensor.""" @@ -398,52 +387,26 @@ class DeconzSensor(DeconzDevice, SensorEntity): return attr -class DeconzSensorStateTracker: - """Track sensors without a battery state and signal when battery state exist.""" +class DeconzBatteryTracker: + """Track sensors without a battery state and add entity when battery state exist.""" - def __init__(self, sensor: SensorResources, gateway: DeconzGateway) -> None: + def __init__( + self, + sensor_id: str, + gateway: DeconzGateway, + async_add_entities: AddEntitiesCallback, + ) -> None: """Set up tracker.""" - self.sensor = sensor + self.sensor = gateway.api.sensors[sensor_id] self.gateway = gateway - sensor.register_callback(self.async_update_callback) - - @callback - def close(self) -> None: - """Clean up tracker.""" - self.sensor.remove_callback(self.async_update_callback) + self.async_add_entities = async_add_entities + self.unsub = self.sensor.subscribe(self.async_update_callback) @callback def async_update_callback(self) -> None: - """Sensor state updated.""" + """Update the device's state.""" if "battery" in self.sensor.changed_keys: - async_dispatcher_send( - self.gateway.hass, - self.gateway.signal_new_sensor, - [self.sensor], + self.unsub() + self.async_add_entities( + [DeconzSensor(self.sensor, self.gateway, SENSOR_DESCRIPTIONS[0])] ) - - -class DeconzBatteryHandler: - """Creates and stores trackers for sensors without a battery state.""" - - def __init__(self, gateway: DeconzGateway) -> None: - """Set up battery handler.""" - self.gateway = gateway - self._trackers: set[DeconzSensorStateTracker] = set() - - @callback - def create_tracker(self, sensor: SensorResources) -> None: - """Create new tracker for battery state.""" - for tracker in self._trackers: - if sensor == tracker.sensor: - return - self._trackers.add(DeconzSensorStateTracker(sensor, self.gateway)) - - @callback - def remove_tracker(self, sensor: SensorResources) -> None: - """Remove tracker of battery state.""" - for tracker in self._trackers: - if sensor == tracker.sensor: - tracker.close() - self._trackers.remove(tracker) - break diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index a9a5172d72e..e4399e53524 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -147,9 +147,6 @@ async def async_refresh_devices_service(gateway: DeconzGateway) -> None: gateway.load_ignored_devices() gateway.ignore_state_updates = False - 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: DeconzGateway) -> None: """Remove orphaned deCONZ entries from device and entity registries.""" From 30bf727dfe28c9b1e80ffc68231808678b800411 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 15:56:13 +0200 Subject: [PATCH 0802/3516] Adjust device_automation type hints in litejet (#72195) --- homeassistant/components/litejet/trigger.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/litejet/trigger.py b/homeassistant/components/litejet/trigger.py index 21b7927ebe2..5aff5dbc66c 100644 --- a/homeassistant/components/litejet/trigger.py +++ b/homeassistant/components/litejet/trigger.py @@ -1,12 +1,19 @@ """Trigger an automation when a LiteJet switch is released.""" +from __future__ import annotations + from collections.abc import Callable import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.const import CONF_PLATFORM -from homeassistant.core import HassJob, callback +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util from .const import DOMAIN @@ -29,14 +36,19 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( ) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: """Listen for events based on configuration.""" trigger_data = automation_info["trigger_data"] number = config.get(CONF_NUMBER) held_more_than = config.get(CONF_HELD_MORE_THAN) held_less_than = config.get(CONF_HELD_LESS_THAN) pressed_time = None - cancel_pressed_more_than: Callable = None + cancel_pressed_more_than: Callable | None = None job = HassJob(action) @callback From 571c90b8cf5218b19389777e076c3e946bb72be7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 15:59:15 +0200 Subject: [PATCH 0803/3516] Adjust pylint plugin for climate HVACMode (#71727) Co-authored-by: J. Nick Koston --- pylint/plugins/hass_imports.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index afbd930ec30..c6f6c25c7b6 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -62,6 +62,10 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { ), ], "homeassistant.components.climate": [ + ObsoleteImportMatch( + reason="replaced by HVACMode enum", + constant=re.compile(r"^HVAC_MODE_(\w*)$"), + ), ObsoleteImportMatch( reason="replaced by ClimateEntityFeature enum", constant=re.compile(r"^SUPPORT_(\w*)$"), @@ -72,6 +76,10 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { reason="replaced by HVACAction enum", constant=re.compile(r"^CURRENT_HVAC_(\w*)$"), ), + ObsoleteImportMatch( + reason="replaced by HVACMode enum", + constant=re.compile(r"^HVAC_MODE_(\w*)$"), + ), ObsoleteImportMatch( reason="replaced by ClimateEntityFeature enum", constant=re.compile(r"^SUPPORT_(\w*)$"), From b10ee779f935028ca45b580c0e2347d6c5b4887e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 16:01:40 +0200 Subject: [PATCH 0804/3516] Adjust device_automation type hints in core platforms 3/3 (#72211) --- .../components/remote/device_action.py | 2 +- .../components/remote/device_trigger.py | 4 +--- .../components/select/device_action.py | 7 ++++-- .../components/select/device_trigger.py | 4 +--- .../components/sensor/device_condition.py | 4 +++- .../components/sensor/device_trigger.py | 23 +++++++++++++++---- .../components/switch/device_action.py | 2 +- .../components/switch/device_trigger.py | 4 +--- .../components/update/device_trigger.py | 4 +--- .../components/vacuum/device_action.py | 6 ++++- .../components/vacuum/device_trigger.py | 4 +--- .../components/water_heater/device_action.py | 6 ++++- 12 files changed, 44 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/remote/device_action.py b/homeassistant/components/remote/device_action.py index a337f3275eb..09c540b5e01 100644 --- a/homeassistant/components/remote/device_action.py +++ b/homeassistant/components/remote/device_action.py @@ -19,7 +19,7 @@ async def async_call_action_from_config( hass: HomeAssistant, config: ConfigType, variables: TemplateVarsType, - context: Context, + context: Context | None, ) -> None: """Change state based on configuration.""" await toggle_entity.async_call_action_from_config( diff --git a/homeassistant/components/remote/device_trigger.py b/homeassistant/components/remote/device_trigger.py index c358d86b176..127f07827e2 100644 --- a/homeassistant/components/remote/device_trigger.py +++ b/homeassistant/components/remote/device_trigger.py @@ -1,8 +1,6 @@ """Provides device triggers for remotes.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -36,7 +34,7 @@ async def async_attach_trigger( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers.""" return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/select/device_action.py b/homeassistant/components/select/device_action.py index f55a12da62a..1212b9dea6b 100644 --- a/homeassistant/components/select/device_action.py +++ b/homeassistant/components/select/device_action.py @@ -15,7 +15,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import get_capability -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from .const import ATTR_OPTION, ATTR_OPTIONS, CONF_OPTION, DOMAIN, SERVICE_SELECT_OPTION @@ -48,7 +48,10 @@ async def async_get_actions( async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Context | None + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context | None, ) -> None: """Execute a device action.""" await hass.services.async_call( diff --git a/homeassistant/components/select/device_trigger.py b/homeassistant/components/select/device_trigger.py index 6d0378946e8..574acfc6893 100644 --- a/homeassistant/components/select/device_trigger.py +++ b/homeassistant/components/select/device_trigger.py @@ -1,8 +1,6 @@ """Provides device triggers for Select.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -47,7 +45,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for Select devices.""" registry = entity_registry.async_get(hass) return [ diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index a77105764d5..808d6367cdf 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -195,7 +195,9 @@ def async_condition_from_config( return condition.async_numeric_state_from_config(numeric_state_config) -async def async_get_condition_capabilities(hass, config): +async def async_get_condition_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: """List condition capabilities.""" try: unit_of_measurement = get_unit_of_measurement(hass, config[CONF_ENTITY_ID]) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index d760b92b31c..741c0281e0d 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -1,6 +1,10 @@ """Provides device triggers for sensors.""" import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -15,6 +19,7 @@ from homeassistant.const import ( CONF_FOR, CONF_TYPE, ) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity import ( @@ -22,6 +27,7 @@ from homeassistant.helpers.entity import ( get_device_class, get_unit_of_measurement, ) +from homeassistant.helpers.typing import ConfigType from . import ATTR_STATE_CLASS, DOMAIN, SensorDeviceClass @@ -134,7 +140,12 @@ TRIGGER_SCHEMA = vol.All( ) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" numeric_state_config = { numeric_state_trigger.CONF_PLATFORM: "numeric_state", @@ -155,9 +166,11 @@ async def async_attach_trigger(hass, config, action, automation_info): ) -async def async_get_triggers(hass, device_id): +async def async_get_triggers( + hass: HomeAssistant, device_id: str +) -> list[dict[str, str]]: """List device triggers.""" - triggers = [] + triggers: list[dict[str, str]] = [] entity_registry = er.async_get(hass) entries = [ @@ -192,7 +205,9 @@ async def async_get_triggers(hass, device_id): return triggers -async def async_get_trigger_capabilities(hass, config): +async def async_get_trigger_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: """List trigger capabilities.""" try: unit_of_measurement = get_unit_of_measurement(hass, config[CONF_ENTITY_ID]) diff --git a/homeassistant/components/switch/device_action.py b/homeassistant/components/switch/device_action.py index 6947656406b..1aed2fa2467 100644 --- a/homeassistant/components/switch/device_action.py +++ b/homeassistant/components/switch/device_action.py @@ -19,7 +19,7 @@ async def async_call_action_from_config( hass: HomeAssistant, config: ConfigType, variables: TemplateVarsType, - context: Context, + context: Context | None, ) -> None: """Change state based on configuration.""" await toggle_entity.async_call_action_from_config( diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py index 533ee8bd54d..9f56d7a09d2 100644 --- a/homeassistant/components/switch/device_trigger.py +++ b/homeassistant/components/switch/device_trigger.py @@ -1,8 +1,6 @@ """Provides device triggers for switches.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -36,7 +34,7 @@ async def async_attach_trigger( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers.""" return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/update/device_trigger.py b/homeassistant/components/update/device_trigger.py index 690e67cce56..ac8113d5708 100644 --- a/homeassistant/components/update/device_trigger.py +++ b/homeassistant/components/update/device_trigger.py @@ -1,8 +1,6 @@ """Provides device triggers for update entities.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -36,7 +34,7 @@ async def async_attach_trigger( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers.""" return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/vacuum/device_action.py b/homeassistant/components/vacuum/device_action.py index e8dac646153..e8fe53b08ae 100644 --- a/homeassistant/components/vacuum/device_action.py +++ b/homeassistant/components/vacuum/device_action.py @@ -13,6 +13,7 @@ from homeassistant.const import ( from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DOMAIN, SERVICE_RETURN_TO_BASE, SERVICE_START @@ -51,7 +52,10 @@ async def async_get_actions( async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Context | None + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context | None, ) -> None: """Execute a device action.""" config = ACTION_SCHEMA(config) diff --git a/homeassistant/components/vacuum/device_trigger.py b/homeassistant/components/vacuum/device_trigger.py index 502f9f01410..4b8ec2fc08d 100644 --- a/homeassistant/components/vacuum/device_trigger.py +++ b/homeassistant/components/vacuum/device_trigger.py @@ -1,8 +1,6 @@ """Provides device automations for Vacuum.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -38,7 +36,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for Vacuum devices.""" registry = entity_registry.async_get(hass) triggers = [] diff --git a/homeassistant/components/water_heater/device_action.py b/homeassistant/components/water_heater/device_action.py index f200498c350..6bc7e1ca635 100644 --- a/homeassistant/components/water_heater/device_action.py +++ b/homeassistant/components/water_heater/device_action.py @@ -15,6 +15,7 @@ from homeassistant.const import ( from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DOMAIN @@ -52,7 +53,10 @@ async def async_get_actions( async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Context | None + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context | None, ) -> None: """Execute a device action.""" service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} From 3a0e816f1b8173fafc5b5afdcc09fb430284bcd6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 16:02:36 +0200 Subject: [PATCH 0805/3516] Adjust device_automation type hints in core platforms 2/3 (#72210) --- homeassistant/components/fan/device_action.py | 6 +++++- homeassistant/components/fan/device_trigger.py | 4 +--- homeassistant/components/humidifier/device_action.py | 11 ++++++----- .../components/humidifier/device_condition.py | 4 +++- homeassistant/components/humidifier/device_trigger.py | 4 +--- homeassistant/components/light/device_action.py | 2 +- homeassistant/components/light/device_trigger.py | 4 +--- homeassistant/components/lock/device_action.py | 6 +++++- homeassistant/components/lock/device_trigger.py | 4 +--- .../components/media_player/device_trigger.py | 4 +--- homeassistant/components/mobile_app/device_action.py | 10 ++++++++-- homeassistant/components/number/device_action.py | 7 +++++-- 12 files changed, 38 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/fan/device_action.py b/homeassistant/components/fan/device_action.py index fd38c69c3da..55bd862349b 100644 --- a/homeassistant/components/fan/device_action.py +++ b/homeassistant/components/fan/device_action.py @@ -6,6 +6,7 @@ import voluptuous as vol from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DOMAIN @@ -20,7 +21,10 @@ async def async_get_actions( async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Context | None + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context | None, ) -> None: """Execute a device action.""" await toggle_entity.async_call_action_from_config( diff --git a/homeassistant/components/fan/device_trigger.py b/homeassistant/components/fan/device_trigger.py index 1d81ef18f4d..35eb5a9f50c 100644 --- a/homeassistant/components/fan/device_trigger.py +++ b/homeassistant/components/fan/device_trigger.py @@ -1,8 +1,6 @@ """Provides device automations for Fan.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -24,7 +22,7 @@ TRIGGER_SCHEMA = vol.All( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for Fan devices.""" return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/humidifier/device_action.py b/homeassistant/components/humidifier/device_action.py index 03f0bb1fea1..773caa72f95 100644 --- a/homeassistant/components/humidifier/device_action.py +++ b/homeassistant/components/humidifier/device_action.py @@ -1,8 +1,6 @@ """Provides device actions for Humidifier.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.device_automation import toggle_entity @@ -19,6 +17,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import get_capability, get_supported_features +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DOMAIN, const @@ -74,8 +73,8 @@ async def async_get_actions( async def async_call_action_from_config( hass: HomeAssistant, - config: dict[str, Any], - variables: dict[str, Any], + config: ConfigType, + variables: TemplateVarsType, context: Context | None, ) -> None: """Execute a device action.""" @@ -97,7 +96,9 @@ async def async_call_action_from_config( ) -async def async_get_action_capabilities(hass, config): +async def async_get_action_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: """List action capabilities.""" action_type = config[CONF_TYPE] diff --git a/homeassistant/components/humidifier/device_condition.py b/homeassistant/components/humidifier/device_condition.py index b8e8d2a13ae..949b25fdd15 100644 --- a/homeassistant/components/humidifier/device_condition.py +++ b/homeassistant/components/humidifier/device_condition.py @@ -85,7 +85,9 @@ def async_condition_from_config( return test_is_state -async def async_get_condition_capabilities(hass, config): +async def async_get_condition_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: """List condition capabilities.""" condition_type = config[CONF_TYPE] diff --git a/homeassistant/components/humidifier/device_trigger.py b/homeassistant/components/humidifier/device_trigger.py index a2d15097335..e8f3a0ac446 100644 --- a/homeassistant/components/humidifier/device_trigger.py +++ b/homeassistant/components/humidifier/device_trigger.py @@ -1,8 +1,6 @@ """Provides device automations for Climate.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -59,7 +57,7 @@ TRIGGER_SCHEMA = vol.All( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for Humidifier devices.""" registry = entity_registry.async_get(hass) triggers = await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index 285e34ebe08..2583bfa972f 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -55,7 +55,7 @@ async def async_call_action_from_config( hass: HomeAssistant, config: ConfigType, variables: TemplateVarsType, - context: Context, + context: Context | None, ) -> None: """Change state based on configuration.""" if ( diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py index 5b4083f41a6..6f5f5fd7f52 100644 --- a/homeassistant/components/light/device_trigger.py +++ b/homeassistant/components/light/device_trigger.py @@ -1,8 +1,6 @@ """Provides device trigger for lights.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -36,7 +34,7 @@ async def async_attach_trigger( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers.""" return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/lock/device_action.py b/homeassistant/components/lock/device_action.py index 038c2f1c0da..3ff8d10c7a2 100644 --- a/homeassistant/components/lock/device_action.py +++ b/homeassistant/components/lock/device_action.py @@ -17,6 +17,7 @@ from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import get_supported_features +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DOMAIN, LockEntityFeature @@ -61,7 +62,10 @@ async def async_get_actions( async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Context | None + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context | None, ) -> None: """Execute a device action.""" service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} diff --git a/homeassistant/components/lock/device_trigger.py b/homeassistant/components/lock/device_trigger.py index d875cadb446..b55a2ac254b 100644 --- a/homeassistant/components/lock/device_trigger.py +++ b/homeassistant/components/lock/device_trigger.py @@ -1,8 +1,6 @@ """Provides device automations for Lock.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -43,7 +41,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for Lock devices.""" registry = entity_registry.async_get(hass) triggers = [] diff --git a/homeassistant/components/media_player/device_trigger.py b/homeassistant/components/media_player/device_trigger.py index 9644e5e0010..e0c88489841 100644 --- a/homeassistant/components/media_player/device_trigger.py +++ b/homeassistant/components/media_player/device_trigger.py @@ -1,8 +1,6 @@ """Provides device automations for Media player.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -55,7 +53,7 @@ TRIGGER_SCHEMA = vol.All( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for Media player entities.""" registry = entity_registry.async_get(hass) triggers = await entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/mobile_app/device_action.py b/homeassistant/components/mobile_app/device_action.py index d17702ec24f..f6e870fc7d6 100644 --- a/homeassistant/components/mobile_app/device_action.py +++ b/homeassistant/components/mobile_app/device_action.py @@ -9,6 +9,7 @@ from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from .const import DOMAIN from .util import get_notify_service, supports_push, webhook_id_from_device_id @@ -36,7 +37,10 @@ async def async_get_actions( async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Context | None + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context | None, ) -> None: """Execute a device action.""" webhook_id = webhook_id_from_device_id(hass, config[CONF_DEVICE_ID]) @@ -73,7 +77,9 @@ async def async_call_action_from_config( ) -async def async_get_action_capabilities(hass, config): +async def async_get_action_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: """List action capabilities.""" if config[CONF_TYPE] != "notify": return {} diff --git a/homeassistant/components/number/device_action.py b/homeassistant/components/number/device_action.py index 3d449ffc213..e4311f50dd2 100644 --- a/homeassistant/components/number/device_action.py +++ b/homeassistant/components/number/device_action.py @@ -13,7 +13,7 @@ from homeassistant.const import ( from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from .const import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE @@ -53,7 +53,10 @@ async def async_get_actions( async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Context | None + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context | None, ) -> None: """Execute a device action.""" await hass.services.async_call( From fb53e39f05831ef796e133de59354b5a97021655 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 16:03:21 +0200 Subject: [PATCH 0806/3516] Adjust device_automation type hints in core platforms 1/3 (#72209) --- .../alarm_control_panel/device_action.py | 7 ++++-- .../alarm_control_panel/device_trigger.py | 4 ++-- .../binary_sensor/device_trigger.py | 23 +++++++++++++++---- .../components/button/device_action.py | 6 ++++- .../components/button/device_trigger.py | 4 +--- .../components/climate/device_action.py | 10 ++++++-- .../components/climate/device_condition.py | 4 +++- .../components/climate/device_trigger.py | 4 +--- .../components/cover/device_action.py | 7 ++++-- .../components/cover/device_trigger.py | 4 +--- .../device_tracker/device_trigger.py | 4 ++-- 11 files changed, 52 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py index ff14b851df8..dd0c3d03a43 100644 --- a/homeassistant/components/alarm_control_panel/device_action.py +++ b/homeassistant/components/alarm_control_panel/device_action.py @@ -24,7 +24,7 @@ from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import get_supported_features -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import ATTR_CODE_ARM_REQUIRED, DOMAIN from .const import ( @@ -90,7 +90,10 @@ async def async_get_actions( async def async_call_action_from_config( - hass: HomeAssistant, config: ConfigType, variables: dict, context: Context | None + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context | None, ) -> None: """Execute a device action.""" service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py index 18508661034..840b5eba6f3 100644 --- a/homeassistant/components/alarm_control_panel/device_trigger.py +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -1,7 +1,7 @@ """Provides device automations for Alarm control panel.""" from __future__ import annotations -from typing import Any, Final +from typing import Final import voluptuous as vol @@ -58,7 +58,7 @@ TRIGGER_SCHEMA: Final = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for Alarm control panel devices.""" registry = entity_registry.async_get(hass) triggers: list[dict[str, str]] = [] diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index 5b20b991403..12b620b8c4f 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -1,6 +1,10 @@ """Provides device triggers for binary sensors.""" import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.const import ( CONF_TURNED_OFF, @@ -8,8 +12,10 @@ from homeassistant.components.device_automation.const import ( ) from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import CONF_ENTITY_ID, CONF_FOR, CONF_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity import get_device_class +from homeassistant.helpers.typing import ConfigType from . import DOMAIN, BinarySensorDeviceClass @@ -254,7 +260,12 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( ) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_type = config[CONF_TYPE] if trigger_type in TURNED_ON: @@ -276,9 +287,11 @@ async def async_attach_trigger(hass, config, action, automation_info): ) -async def async_get_triggers(hass, device_id): +async def async_get_triggers( + hass: HomeAssistant, device_id: str +) -> list[dict[str, str]]: """List device triggers.""" - triggers = [] + triggers: list[dict[str, str]] = [] entity_registry = er.async_get(hass) entries = [ @@ -308,7 +321,9 @@ async def async_get_triggers(hass, device_id): return triggers -async def async_get_trigger_capabilities(hass, config): +async def async_get_trigger_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: """List trigger capabilities.""" return { "extra_fields": vol.Schema( diff --git a/homeassistant/components/button/device_action.py b/homeassistant/components/button/device_action.py index 2dffd9c600f..70033729692 100644 --- a/homeassistant/components/button/device_action.py +++ b/homeassistant/components/button/device_action.py @@ -13,6 +13,7 @@ from homeassistant.const import ( from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from .const import DOMAIN, SERVICE_PRESS @@ -44,7 +45,10 @@ async def async_get_actions( async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Context | None + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context | None, ) -> None: """Execute a device action.""" await hass.services.async_call( diff --git a/homeassistant/components/button/device_trigger.py b/homeassistant/components/button/device_trigger.py index ccad50dec05..1418039b2e8 100644 --- a/homeassistant/components/button/device_trigger.py +++ b/homeassistant/components/button/device_trigger.py @@ -1,8 +1,6 @@ """Provides device triggers for Button.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -39,7 +37,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for button devices.""" registry = entity_registry.async_get(hass) return [ diff --git a/homeassistant/components/climate/device_action.py b/homeassistant/components/climate/device_action.py index 45160eed8eb..3c9934d5cbf 100644 --- a/homeassistant/components/climate/device_action.py +++ b/homeassistant/components/climate/device_action.py @@ -15,6 +15,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import get_capability, get_supported_features +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DOMAIN, const @@ -67,7 +68,10 @@ async def async_get_actions( async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Context | None + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context | None, ) -> None: """Execute a device action.""" service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} @@ -84,7 +88,9 @@ async def async_call_action_from_config( ) -async def async_get_action_capabilities(hass, config): +async def async_get_action_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: """List action capabilities.""" action_type = config[CONF_TYPE] diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py index 56bac123d28..c6179d82215 100644 --- a/homeassistant/components/climate/device_condition.py +++ b/homeassistant/components/climate/device_condition.py @@ -92,7 +92,9 @@ def async_condition_from_config( return test_is_state -async def async_get_condition_capabilities(hass, config): +async def async_get_condition_capabilities( + hass: HomeAssistant, config: ConfigType +) -> dict[str, vol.Schema]: """List condition capabilities.""" condition_type = config[CONF_TYPE] diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index e40cf32f2a9..d8d46342603 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -1,8 +1,6 @@ """Provides device automations for Climate.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -65,7 +63,7 @@ TRIGGER_SCHEMA = vol.Any(HVAC_MODE_TRIGGER_SCHEMA, CURRENT_TRIGGER_SCHEMA) async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for Climate devices.""" registry = entity_registry.async_get(hass) triggers = [] diff --git a/homeassistant/components/cover/device_action.py b/homeassistant/components/cover/device_action.py index 9bdf21f168e..c3c0e928f0f 100644 --- a/homeassistant/components/cover/device_action.py +++ b/homeassistant/components/cover/device_action.py @@ -21,7 +21,7 @@ from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import get_supported_features -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import ( ATTR_POSITION, @@ -120,7 +120,10 @@ async def async_get_action_capabilities( async def async_call_action_from_config( - hass: HomeAssistant, config: dict, variables: dict, context: Context | None + hass: HomeAssistant, + config: ConfigType, + variables: TemplateVarsType, + context: Context | None, ) -> None: """Execute a device action.""" service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} diff --git a/homeassistant/components/cover/device_trigger.py b/homeassistant/components/cover/device_trigger.py index f7b2f54f4dd..a6ed7785486 100644 --- a/homeassistant/components/cover/device_trigger.py +++ b/homeassistant/components/cover/device_trigger.py @@ -1,8 +1,6 @@ """Provides device automations for Cover.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -74,7 +72,7 @@ TRIGGER_SCHEMA = vol.Any(POSITION_TRIGGER_SCHEMA, STATE_TRIGGER_SCHEMA) async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for Cover devices.""" registry = entity_registry.async_get(hass) triggers = [] diff --git a/homeassistant/components/device_tracker/device_trigger.py b/homeassistant/components/device_tracker/device_trigger.py index 134e9d8dca3..1d9dc4548b3 100644 --- a/homeassistant/components/device_tracker/device_trigger.py +++ b/homeassistant/components/device_tracker/device_trigger.py @@ -1,7 +1,7 @@ """Provides device automations for Device Tracker.""" from __future__ import annotations -from typing import Any, Final +from typing import Final import voluptuous as vol @@ -39,7 +39,7 @@ TRIGGER_SCHEMA: Final = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for Device Tracker devices.""" registry = entity_registry.async_get(hass) triggers = [] From 5cfb31d28a54357c047e46a74f273c17cf25745d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 16:07:34 +0200 Subject: [PATCH 0807/3516] Adjust device_automation type hints in core components (#72207) --- .../components/device_automation/entity.py | 4 +-- .../device_automation/toggle_entity.py | 4 +-- .../components/geo_location/trigger.py | 16 ++++++++-- .../components/homeassistant/trigger.py | 22 +++++++++++-- .../homeassistant/triggers/homeassistant.py | 14 +++++++-- .../homeassistant/triggers/numeric_state.py | 13 ++++++-- .../homeassistant/triggers/state.py | 10 ++++-- .../components/homeassistant/triggers/time.py | 16 ++++++++-- .../homeassistant/triggers/time_pattern.py | 14 +++++++-- .../components/mqtt/device_trigger.py | 6 ++-- homeassistant/components/mqtt/trigger.py | 14 +++++++-- homeassistant/components/sun/trigger.py | 14 +++++++-- homeassistant/components/template/trigger.py | 20 +++++++++--- homeassistant/components/webhook/trigger.py | 16 ++++++++-- homeassistant/components/zone/trigger.py | 13 ++++++-- homeassistant/helpers/trigger.py | 31 +++++++++++++------ 16 files changed, 176 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/device_automation/entity.py b/homeassistant/components/device_automation/entity.py index 9fa878ea038..4bc77370150 100644 --- a/homeassistant/components/device_automation/entity.py +++ b/homeassistant/components/device_automation/entity.py @@ -1,8 +1,6 @@ """Device automation helpers for entity.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -91,7 +89,7 @@ async def _async_get_automations( async def async_get_triggers( hass: HomeAssistant, device_id: str, domain: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers.""" return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 4ae32927bf4..af97de85f70 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -1,8 +1,6 @@ """Device automation helpers for toggle entity.""" from __future__ import annotations -from typing import Any - import voluptuous as vol from homeassistant.components.automation import ( @@ -228,7 +226,7 @@ async def async_get_conditions( async def async_get_triggers( hass: HomeAssistant, device_id: str, domain: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers.""" triggers = await entity.async_get_triggers(hass, device_id, domain) triggers.extend( diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index e57c7a9aec6..9f2b56a31c6 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -3,11 +3,16 @@ import logging import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.const import CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE -from homeassistant.core import HassJob, callback +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.config_validation import entity_domain from homeassistant.helpers.event import TrackStates, async_track_state_change_filtered +from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -36,10 +41,15 @@ def source_match(state, source): return state and state.attributes.get("source") == source -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_data = automation_info["trigger_data"] - source = config.get(CONF_SOURCE).lower() + source: str = config[CONF_SOURCE].lower() zone_entity_id = config.get(CONF_ZONE) trigger_event = config.get(CONF_EVENT) job = HassJob(action) diff --git a/homeassistant/components/homeassistant/trigger.py b/homeassistant/components/homeassistant/trigger.py index ca77747cd96..42b0e30af1d 100644 --- a/homeassistant/components/homeassistant/trigger.py +++ b/homeassistant/components/homeassistant/trigger.py @@ -1,14 +1,25 @@ """Home Assistant trigger dispatcher.""" import importlib +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) +from homeassistant.components.device_automation.trigger import ( + DeviceAutomationTriggerProtocol, +) from homeassistant.const import CONF_PLATFORM +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.typing import ConfigType -def _get_trigger_platform(config): +def _get_trigger_platform(config: ConfigType) -> DeviceAutomationTriggerProtocol: return importlib.import_module(f"..triggers.{config[CONF_PLATFORM]}", __name__) -async def async_validate_trigger_config(hass, config): +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: """Validate config.""" platform = _get_trigger_platform(config) if hasattr(platform, "async_validate_trigger_config"): @@ -17,7 +28,12 @@ async def async_validate_trigger_config(hass, config): return platform.TRIGGER_SCHEMA(config) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: """Attach trigger of specified platform.""" platform = _get_trigger_platform(config) return await platform.async_attach_trigger(hass, config, action, automation_info) diff --git a/homeassistant/components/homeassistant/triggers/homeassistant.py b/homeassistant/components/homeassistant/triggers/homeassistant.py index 6f2ec75e313..c9a5a780e88 100644 --- a/homeassistant/components/homeassistant/triggers/homeassistant.py +++ b/homeassistant/components/homeassistant/triggers/homeassistant.py @@ -1,9 +1,14 @@ """Offer Home Assistant core automation rules.""" import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.const import CONF_EVENT, CONF_PLATFORM, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HassJob, callback +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType # mypy: allow-untyped-defs @@ -18,7 +23,12 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( ) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: """Listen for events based on configuration.""" trigger_data = automation_info["trigger_data"] event = config.get(CONF_EVENT) diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 2d73f38d110..934cc99993a 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -4,6 +4,10 @@ import logging import voluptuous as vol from homeassistant import exceptions +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.const import ( CONF_ABOVE, CONF_ATTRIBUTE, @@ -81,10 +85,15 @@ async def async_validate_trigger_config( async def async_attach_trigger( - hass, config, action, automation_info, *, platform_type="numeric_state" + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, + *, + platform_type: str = "numeric_state", ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - entity_ids = config.get(CONF_ENTITY_ID) + entity_ids: list[str] = config[CONF_ENTITY_ID] below = config.get(CONF_BELOW) above = config.get(CONF_ABOVE) time_delta = config.get(CONF_FOR) diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index e6a4b90dbe8..4f1e823c90f 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -7,6 +7,10 @@ import logging import voluptuous as vol from homeassistant import exceptions +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.const import CONF_ATTRIBUTE, CONF_FOR, CONF_PLATFORM, MATCH_ALL from homeassistant.core import ( CALLBACK_TYPE, @@ -92,9 +96,9 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, - config, - action, - automation_info, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, *, platform_type: str = "state", ) -> CALLBACK_TYPE: diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index 49a42d3843d..619ef0e207c 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -5,6 +5,10 @@ from functools import partial import voluptuous as vol from homeassistant.components import sensor +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, CONF_AT, @@ -12,13 +16,14 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HassJob, callback +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import ( async_track_point_in_time, async_track_state_change_event, async_track_time_change, ) +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util # mypy: allow-untyped-defs, no-check-untyped-defs @@ -37,10 +42,15 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( ) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_data = automation_info["trigger_data"] - entities = {} + entities: dict[str, CALLBACK_TYPE] = {} removes = [] job = HassJob(action) diff --git a/homeassistant/components/homeassistant/triggers/time_pattern.py b/homeassistant/components/homeassistant/triggers/time_pattern.py index 000d73b6cd1..7ee1d218171 100644 --- a/homeassistant/components/homeassistant/triggers/time_pattern.py +++ b/homeassistant/components/homeassistant/triggers/time_pattern.py @@ -1,10 +1,15 @@ """Offer time listening automation rules.""" import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.const import CONF_PLATFORM -from homeassistant.core import HassJob, callback +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_time_change +from homeassistant.helpers.typing import ConfigType # mypy: allow-untyped-defs, no-check-untyped-defs @@ -55,7 +60,12 @@ TRIGGER_SCHEMA = vol.All( ) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_data = automation_info["trigger_data"] hours = config.get(CONF_HOURS) diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 56cfc3efc6b..42ffcee1644 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable import logging -from typing import Any, cast +from typing import cast import attr import voluptuous as vol @@ -290,9 +290,9 @@ async def async_removed_from_device(hass: HomeAssistant, device_id: str) -> None async def async_get_triggers( hass: HomeAssistant, device_id: str -) -> list[dict[str, Any]]: +) -> list[dict[str, str]]: """List device triggers for MQTT devices.""" - triggers: list[dict] = [] + triggers: list[dict[str, str]] = [] if DEVICE_TRIGGERS not in hass.data: return triggers diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index db366010bb2..7d1f93d30eb 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -5,9 +5,14 @@ import logging import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.const import CONF_PAYLOAD, CONF_PLATFORM, CONF_VALUE_TEMPLATE -from homeassistant.core import HassJob, callback +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers.typing import ConfigType from .. import mqtt from .const import CONF_ENCODING, CONF_QOS, CONF_TOPIC, DEFAULT_ENCODING, DEFAULT_QOS @@ -31,7 +36,12 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( _LOGGER = logging.getLogger(__name__) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_data = automation_info["trigger_data"] topic = config[CONF_TOPIC] diff --git a/homeassistant/components/sun/trigger.py b/homeassistant/components/sun/trigger.py index 266df1f6a3b..75f5b36f8f5 100644 --- a/homeassistant/components/sun/trigger.py +++ b/homeassistant/components/sun/trigger.py @@ -3,15 +3,20 @@ from datetime import timedelta import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.const import ( CONF_EVENT, CONF_OFFSET, CONF_PLATFORM, SUN_EVENT_SUNRISE, ) -from homeassistant.core import HassJob, callback +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_sunrise, async_track_sunset +from homeassistant.helpers.typing import ConfigType # mypy: allow-untyped-defs, no-check-untyped-defs @@ -24,7 +29,12 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( ) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: """Listen for events based on configuration.""" trigger_data = automation_info["trigger_data"] event = config.get(CONF_EVENT) diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index d2e50de53fd..33ac90079b7 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -4,15 +4,20 @@ import logging import voluptuous as vol from homeassistant import exceptions +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.const import CONF_FOR, CONF_PLATFORM, CONF_VALUE_TEMPLATE -from homeassistant.core import HassJob, callback +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.event import ( TrackTemplate, async_call_later, async_track_template_result, ) -from homeassistant.helpers.template import result_as_boolean +from homeassistant.helpers.template import Template, result_as_boolean +from homeassistant.helpers.typing import ConfigType # mypy: allow-untyped-defs, no-check-untyped-defs @@ -28,11 +33,16 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger( - hass, config, action, automation_info, *, platform_type="template" -): + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, + *, + platform_type: str = "template", +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_data = automation_info["trigger_data"] - value_template = config.get(CONF_VALUE_TEMPLATE) + value_template: Template = config[CONF_VALUE_TEMPLATE] value_template.hass = hass time_delta = config.get(CONF_FOR) template.attach(hass, time_delta) diff --git a/homeassistant/components/webhook/trigger.py b/homeassistant/components/webhook/trigger.py index 4eaf60595a5..3f790b1ec42 100644 --- a/homeassistant/components/webhook/trigger.py +++ b/homeassistant/components/webhook/trigger.py @@ -4,9 +4,14 @@ from functools import partial from aiohttp import hdrs import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.const import CONF_PLATFORM, CONF_WEBHOOK_ID -from homeassistant.core import HassJob, callback +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType from . import async_register, async_unregister @@ -37,10 +42,15 @@ async def _handle_webhook(job, trigger_data, hass, webhook_id, request): hass.async_run_hass_job(job, {"trigger": result}) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: """Trigger based on incoming webhooks.""" trigger_data = automation_info["trigger_data"] - webhook_id = config.get(CONF_WEBHOOK_ID) + webhook_id: str = config[CONF_WEBHOOK_ID] job = HassJob(action) async_register( hass, diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index 8c5af3a0ac2..0865182df80 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -3,6 +3,10 @@ import logging import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.const import ( ATTR_FRIENDLY_NAME, CONF_ENTITY_ID, @@ -56,11 +60,16 @@ async def async_validate_trigger_config( async def async_attach_trigger( - hass, config, action, automation_info, *, platform_type: str = "zone" + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, + *, + platform_type: str = "zone", ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_data = automation_info["trigger_data"] - entity_id = config.get(CONF_ENTITY_ID) + entity_id: list[str] = config[CONF_ENTITY_ID] zone_entity_id = config.get(CONF_ZONE) event = config.get(CONF_EVENT) job = HassJob(action) diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index a3cb2f9421d..e90c684365d 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -5,7 +5,7 @@ import asyncio from collections.abc import Callable import functools import logging -from typing import Any +from typing import TYPE_CHECKING, Any import voluptuous as vol @@ -16,13 +16,20 @@ from homeassistant.loader import IntegrationNotFound, async_get_integration from .typing import ConfigType, TemplateVarsType +if TYPE_CHECKING: + from homeassistant.components.device_automation.trigger import ( + DeviceAutomationTriggerProtocol, + ) + _PLATFORM_ALIASES = { "device_automation": ("device",), "homeassistant": ("event", "numeric_state", "state", "time_pattern", "time"), } -async def _async_get_trigger_platform(hass: HomeAssistant, config: ConfigType) -> Any: +async def _async_get_trigger_platform( + hass: HomeAssistant, config: ConfigType +) -> DeviceAutomationTriggerProtocol: platform_and_sub_type = config[CONF_PLATFORM].split(".") platform = platform_and_sub_type[0] for alias, triggers in _PLATFORM_ALIASES.items(): @@ -86,6 +93,10 @@ async def async_initialize_triggers( variables: TemplateVarsType = None, ) -> CALLBACK_TYPE | None: """Initialize triggers.""" + from homeassistant.components.automation import ( # pylint:disable=[import-outside-toplevel] + AutomationTriggerData, + AutomationTriggerInfo, + ) triggers = [] for idx, conf in enumerate(trigger_config): @@ -96,14 +107,14 @@ async def async_initialize_triggers( platform = await _async_get_trigger_platform(hass, conf) trigger_id = conf.get(CONF_ID, f"{idx}") trigger_idx = f"{idx}" - trigger_data = {"id": trigger_id, "idx": trigger_idx} - info = { - "domain": domain, - "name": name, - "home_assistant_start": home_assistant_start, - "variables": variables, - "trigger_data": trigger_data, - } + trigger_data = AutomationTriggerData(id=trigger_id, idx=trigger_idx) + info = AutomationTriggerInfo( + domain=domain, + name=name, + home_assistant_start=home_assistant_start, + variables=variables, + trigger_data=trigger_data, + ) triggers.append( platform.async_attach_trigger( From 204e26a1b534764ed31fa85447d0deed725fda00 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 23 May 2022 10:11:17 -0400 Subject: [PATCH 0808/3516] Warn user if Steam friends list is restricted (#72285) --- .../components/steam_online/config_flow.py | 14 ++++++++++--- .../components/steam_online/strings.json | 3 +++ .../steam_online/translations/en.json | 3 +++ tests/components/steam_online/__init__.py | 15 ++++++++++++++ .../steam_online/test_config_flow.py | 20 +++++++++++++++++++ 5 files changed, 52 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/steam_online/config_flow.py b/homeassistant/components/steam_online/config_flow.py index 91965685969..338b0a80fb6 100644 --- a/homeassistant/components/steam_online/config_flow.py +++ b/homeassistant/components/steam_online/config_flow.py @@ -174,11 +174,14 @@ class SteamOptionsFlowHandler(config_entries.OptionsFlow): } await self.hass.config_entries.async_reload(self.entry.entry_id) return self.async_create_entry(title="", data=channel_data) + error = None try: users = { name["steamid"]: name["personaname"] for name in await self.hass.async_add_executor_job(self.get_accounts) } + if not users: + error = {"base": "unauthorized"} except steam.api.HTTPTimeoutError: users = self.options[CONF_ACCOUNTS] @@ -191,12 +194,17 @@ class SteamOptionsFlowHandler(config_entries.OptionsFlow): } self.options[CONF_ACCOUNTS] = users | self.options[CONF_ACCOUNTS] - return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) + return self.async_show_form( + step_id="init", data_schema=vol.Schema(options), errors=error + ) def get_accounts(self) -> list[dict[str, str | int]]: """Get accounts.""" interface = steam.api.interface("ISteamUser") - friends = interface.GetFriendList(steamid=self.entry.data[CONF_ACCOUNT]) - _users_str = [user["steamid"] for user in friends["friendslist"]["friends"]] + try: + friends = interface.GetFriendList(steamid=self.entry.data[CONF_ACCOUNT]) + _users_str = [user["steamid"] for user in friends["friendslist"]["friends"]] + except steam.api.HTTPError: + return [] names = interface.GetPlayerSummaries(steamids=_users_str) return names["response"]["players"]["player"] diff --git a/homeassistant/components/steam_online/strings.json b/homeassistant/components/steam_online/strings.json index 6d80bb77f1b..1b431795ea4 100644 --- a/homeassistant/components/steam_online/strings.json +++ b/homeassistant/components/steam_online/strings.json @@ -31,6 +31,9 @@ "accounts": "Names of accounts to be monitored" } } + }, + "error": { + "unauthorized": "Friends list restricted: Please refer to the documentation on how to see all other friends" } } } diff --git a/homeassistant/components/steam_online/translations/en.json b/homeassistant/components/steam_online/translations/en.json index 990a33dbeff..f93a517b241 100644 --- a/homeassistant/components/steam_online/translations/en.json +++ b/homeassistant/components/steam_online/translations/en.json @@ -31,6 +31,9 @@ "accounts": "Names of accounts to be monitored" } } + }, + "error": { + "unauthorized": "Friends list restricted: Please refer to the documentation on how to see all other friends" } } } \ No newline at end of file diff --git a/tests/components/steam_online/__init__.py b/tests/components/steam_online/__init__.py index 27958b76576..4c8c398502f 100644 --- a/tests/components/steam_online/__init__.py +++ b/tests/components/steam_online/__init__.py @@ -1,6 +1,8 @@ """Tests for Steam integration.""" from unittest.mock import patch +import steam + from homeassistant.components.steam_online import DOMAIN from homeassistant.components.steam_online.const import CONF_ACCOUNT, CONF_ACCOUNTS from homeassistant.const import CONF_API_KEY @@ -96,11 +98,24 @@ class MockedInterface(dict): return {"response": {"player_level": 10}} +class MockedInterfacePrivate(MockedInterface): + """Mocked interface for private friends list.""" + + def GetFriendList(self, steamid: str) -> None: + """Get friend list.""" + raise steam.api.HTTPError + + def patch_interface() -> MockedInterface: """Patch interface.""" return patch("steam.api.interface", return_value=MockedInterface()) +def patch_interface_private() -> MockedInterfacePrivate: + """Patch interface for private friends list.""" + return patch("steam.api.interface", return_value=MockedInterfacePrivate()) + + def patch_user_interface_null() -> MockedUserInterfaceNull: """Patch player interface with no players.""" return patch("steam.api.interface", return_value=MockedUserInterfaceNull()) diff --git a/tests/components/steam_online/test_config_flow.py b/tests/components/steam_online/test_config_flow.py index 6d8a16f35f7..c4504bf1641 100644 --- a/tests/components/steam_online/test_config_flow.py +++ b/tests/components/steam_online/test_config_flow.py @@ -21,6 +21,7 @@ from . import ( CONF_OPTIONS_2, create_entry, patch_interface, + patch_interface_private, patch_user_interface_null, ) @@ -225,3 +226,22 @@ async def test_options_flow_timeout(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == CONF_OPTIONS + + +async def test_options_flow_unauthorized(hass: HomeAssistant) -> None: + """Test updating options when user's friends list is not public.""" + entry = create_entry(hass) + with patch_interface_private(): + 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_ACCOUNTS: [ACCOUNT_1]}, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == CONF_OPTIONS From d0556e6dd165e936e91e41f970f76cc236af46b7 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 23 May 2022 16:29:45 +0200 Subject: [PATCH 0809/3516] Move manual configuration of MQTT sensor to the integration key (#72276) Add sensor --- homeassistant/components/mqtt/__init__.py | 1 + homeassistant/components/mqtt/sensor.py | 31 +++++++++++++++++++---- tests/components/mqtt/test_sensor.py | 13 ++++++++++ 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 52204b31dcf..df5d52c443a 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -203,6 +203,7 @@ PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( vol.Optional(Platform.SCENE.value): cv.ensure_list, vol.Optional(Platform.SELECT.value): cv.ensure_list, vol.Optional(Platform.SIREN.value): cv.ensure_list, + vol.Optional(Platform.SENSOR.value): cv.ensure_list, vol.Optional(Platform.SWITCH.value): cv.ensure_list, vol.Optional(Platform.VACUUM.value): cv.ensure_list, vol.Optional(Platform.NUMBER.value): cv.ensure_list, diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index a13f58f95ea..d865d90c4ee 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -1,6 +1,7 @@ """Support for MQTT sensors.""" from __future__ import annotations +import asyncio from datetime import timedelta import functools import logging @@ -41,8 +42,10 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, + async_get_platform_config_from_yaml, async_setup_entry_helper, async_setup_platform_helper, + warn_for_legacy_schema, ) _LOGGER = logging.getLogger(__name__) @@ -86,7 +89,7 @@ def validate_options(conf): return conf -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RO_SCHEMA.extend( { vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, @@ -99,12 +102,19 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_LAST_RESET_TOPIC), +PLATFORM_SCHEMA_MODERN = vol.All( _PLATFORM_SCHEMA_BASE, validate_options, ) +# Configuring MQTT Sensors under the sensor platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA = vol.All( + cv.deprecated(CONF_LAST_RESET_TOPIC), + cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), + validate_options, + warn_for_legacy_schema(sensor.DOMAIN), +) + DISCOVERY_SCHEMA = vol.All( cv.deprecated(CONF_LAST_RESET_TOPIC), _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), @@ -118,7 +128,8 @@ async def async_setup_platform( async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up MQTT sensors through configuration.yaml.""" + """Set up MQTT sensors configured under the fan platform key (deprecated).""" + # Deprecated in HA Core 2022.6 await async_setup_platform_helper( hass, sensor.DOMAIN, config, async_add_entities, _async_setup_entity ) @@ -129,7 +140,17 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT sensors dynamically through MQTT discovery.""" + """Set up MQTT sensor through configuration.yaml and dynamically through MQTT discovery.""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, sensor.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index b653e04c82e..befb5785cdd 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -55,6 +55,7 @@ from .test_common import ( help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_setting_blocked_attribute_via_mqtt_json_message, + help_test_setup_manual_entity_from_yaml, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -1106,3 +1107,15 @@ async def test_encoding_subscribable_topics( attribute_value, skip_raw_test=True, ) + + +async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): + """Test setup manual configured MQTT entity.""" + platform = sensor.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, platform, config + ) + assert hass.states.get(f"{platform}.test") is not None From 967f4efc56c11f292105f47f7a2323f17170930b Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Mon, 23 May 2022 16:50:05 +0200 Subject: [PATCH 0810/3516] Cleanup config flow and tests for here_travel_time (#72364) --- .../components/here_travel_time/__init__.py | 18 ------------ .../here_travel_time/config_flow.py | 28 +++++++++++++++---- .../here_travel_time/test_config_flow.py | 16 +++++++++-- .../components/here_travel_time/test_init.py | 2 ++ .../here_travel_time/test_sensor.py | 9 ++++++ 5 files changed, 46 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index 310fe97fad8..da091e0bdbf 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -41,11 +41,9 @@ from .const import ( CONF_ORIGIN_LATITUDE, CONF_ORIGIN_LONGITUDE, CONF_ROUTE_MODE, - CONF_TRAFFIC_MODE, DEFAULT_SCAN_INTERVAL, DOMAIN, NO_ROUTE_ERROR_MESSAGE, - ROUTE_MODE_FASTEST, TRAFFIC_MODE_ENABLED, TRAVEL_MODES_VEHICLE, ) @@ -60,7 +58,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b """Set up HERE Travel Time from a config entry.""" api_key = config_entry.data[CONF_API_KEY] here_client = RoutingApi(api_key) - setup_options(hass, config_entry) arrival = ( dt.parse_time(config_entry.options[CONF_ARRIVAL_TIME]) @@ -98,21 +95,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return True -def setup_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None: - """Set up options for a config entry if not set.""" - if not config_entry.options: - hass.config_entries.async_update_entry( - config_entry, - options={ - CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, - CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, - CONF_ARRIVAL_TIME: None, - CONF_DEPARTURE_TIME: None, - CONF_UNIT_SYSTEM: hass.config.units.name, - }, - ) - - async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms( diff --git a/homeassistant/components/here_travel_time/config_flow.py b/homeassistant/components/here_travel_time/config_flow.py index 502eee68546..e8a05796b66 100644 --- a/homeassistant/components/here_travel_time/config_flow.py +++ b/homeassistant/components/here_travel_time/config_flow.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_NAME, CONF_UNIT_SYSTEM, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.selector import ( @@ -128,12 +128,25 @@ def get_user_step_schema(data: dict[str, Any]) -> vol.Schema: ) +def default_options(hass: HomeAssistant) -> dict[str, str | None]: + """Get the default options.""" + return { + CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, + CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, + CONF_ARRIVAL_TIME: None, + CONF_DEPARTURE_TIME: None, + CONF_UNIT_SYSTEM: hass.config.units.name, + } + + class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for HERE Travel Time.""" VERSION = 1 - _config: dict[str, Any] = {} + def __init__(self) -> None: + """Init Config Flow.""" + self._config: dict[str, Any] = {} @staticmethod @callback @@ -211,7 +224,9 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): "longitude" ] return self.async_create_entry( - title=self._config[CONF_NAME], data=self._config + title=self._config[CONF_NAME], + data=self._config, + options=default_options(self.hass), ) schema = vol.Schema( {"destination": selector({LocationSelector.selector_type: {}})} @@ -230,7 +245,9 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_DESTINATION_ENTITY_ID ] return self.async_create_entry( - title=self._config[CONF_NAME], data=self._config + title=self._config[CONF_NAME], + data=self._config, + options=default_options(self.hass), ) schema = vol.Schema( {CONF_DESTINATION_ENTITY_ID: selector({EntitySelector.selector_type: {}})} @@ -301,11 +318,10 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class HERETravelTimeOptionsFlow(config_entries.OptionsFlow): """Handle HERE Travel Time options.""" - _config: dict[str, Any] = {} - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize HERE Travel Time options flow.""" self.config_entry = config_entry + self._config: dict[str, Any] = {} async def async_step_init( self, user_input: dict[str, Any] | None = None diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py index 350e1ae36f6..c1ce6f823ae 100644 --- a/tests/components/here_travel_time/test_config_flow.py +++ b/tests/components/here_travel_time/test_config_flow.py @@ -50,6 +50,16 @@ from .const import ( from tests.common import MockConfigEntry +@pytest.fixture(autouse=True) +def bypass_setup_fixture(): + """Prevent setup.""" + with patch( + "homeassistant.components.here_travel_time.async_setup_entry", + return_value=True, + ): + yield + + @pytest.fixture(name="user_step_result") async def user_step_result_fixture(hass: HomeAssistant) -> data_entry_flow.FlowResult: """Provide the result of a completed user step.""" @@ -65,7 +75,7 @@ async def user_step_result_fixture(hass: HomeAssistant) -> data_entry_flow.FlowR }, ) await hass.async_block_till_done() - yield user_step_result + return user_step_result @pytest.fixture(name="option_init_result") @@ -96,7 +106,7 @@ async def option_init_result_fixture(hass: HomeAssistant) -> data_entry_flow.Flo CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, }, ) - yield result + return result @pytest.fixture(name="origin_step_result") @@ -118,7 +128,7 @@ async def origin_step_result_fixture( } }, ) - yield location_selector_result + return location_selector_result @pytest.mark.parametrize( diff --git a/tests/components/here_travel_time/test_init.py b/tests/components/here_travel_time/test_init.py index 02827acc4df..05b7f6983db 100644 --- a/tests/components/here_travel_time/test_init.py +++ b/tests/components/here_travel_time/test_init.py @@ -2,6 +2,7 @@ import pytest +from homeassistant.components.here_travel_time.config_flow import default_options from homeassistant.components.here_travel_time.const import ( CONF_DESTINATION_LATITUDE, CONF_DESTINATION_LONGITUDE, @@ -39,6 +40,7 @@ async def test_unload_entry(hass: HomeAssistant) -> None: CONF_MODE: TRAVEL_MODE_CAR, CONF_NAME: "test", }, + options=default_options(hass), ) entry.add_to_hass(hass) diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 70470aee9d3..9a15f14f53e 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -5,6 +5,7 @@ from herepy.here_enum import RouteMode from herepy.routing_api import NoRouteFoundError import pytest +from homeassistant.components.here_travel_time.config_flow import default_options from homeassistant.components.here_travel_time.const import ( ATTR_DESTINATION, ATTR_DESTINATION_NAME, @@ -224,6 +225,7 @@ async def test_circular_ref(hass: HomeAssistant, caplog): CONF_MODE: TRAVEL_MODE_TRUCK, CONF_NAME: "test", }, + options=default_options(hass), ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -250,6 +252,7 @@ async def test_no_attribution(hass: HomeAssistant): CONF_MODE: TRAVEL_MODE_TRUCK, CONF_NAME: "test", }, + options=default_options(hass), ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -293,6 +296,7 @@ async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock): CONF_MODE: TRAVEL_MODE_TRUCK, CONF_NAME: "test", }, + options=default_options(hass), ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -332,6 +336,7 @@ async def test_destination_entity_not_found(hass: HomeAssistant, caplog): CONF_MODE: TRAVEL_MODE_TRUCK, CONF_NAME: "test", }, + options=default_options(hass), ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -357,6 +362,7 @@ async def test_origin_entity_not_found(hass: HomeAssistant, caplog): CONF_MODE: TRAVEL_MODE_TRUCK, CONF_NAME: "test", }, + options=default_options(hass), ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -386,6 +392,7 @@ async def test_invalid_destination_entity_state(hass: HomeAssistant, caplog): CONF_MODE: TRAVEL_MODE_TRUCK, CONF_NAME: "test", }, + options=default_options(hass), ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -415,6 +422,7 @@ async def test_invalid_origin_entity_state(hass: HomeAssistant, caplog): CONF_MODE: TRAVEL_MODE_TRUCK, CONF_NAME: "test", }, + options=default_options(hass), ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -444,6 +452,7 @@ async def test_route_not_found(hass: HomeAssistant, caplog): CONF_MODE: TRAVEL_MODE_TRUCK, CONF_NAME: "test", }, + options=default_options(hass), ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) From 5b4fdb081ec765cd5a2b6da2550f88d9b1099853 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Mon, 23 May 2022 17:24:28 +0200 Subject: [PATCH 0811/3516] Add climate tests for devolo_home_control (#72230) --- .coveragerc | 3 - tests/components/devolo_home_control/mocks.py | 52 ++++++++++++ .../devolo_home_control/test_climate.py | 81 +++++++++++++++++++ 3 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 tests/components/devolo_home_control/test_climate.py diff --git a/.coveragerc b/.coveragerc index ed1e0b0bce4..1eb922b52e5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -207,12 +207,9 @@ omit = homeassistant/components/denonavr/media_player.py homeassistant/components/denonavr/receiver.py homeassistant/components/deutsche_bahn/sensor.py - homeassistant/components/devolo_home_control/climate.py - homeassistant/components/devolo_home_control/const.py homeassistant/components/devolo_home_control/cover.py homeassistant/components/devolo_home_control/light.py homeassistant/components/devolo_home_control/sensor.py - homeassistant/components/devolo_home_control/subscriber.py homeassistant/components/devolo_home_control/switch.py homeassistant/components/digital_ocean/* homeassistant/components/discogs/sensor.py diff --git a/tests/components/devolo_home_control/mocks.py b/tests/components/devolo_home_control/mocks.py index 79bf94b8fc3..b43cb77ad71 100644 --- a/tests/components/devolo_home_control/mocks.py +++ b/tests/components/devolo_home_control/mocks.py @@ -8,6 +8,9 @@ from devolo_home_control_api.homecontrol import HomeControl from devolo_home_control_api.properties.binary_sensor_property import ( BinarySensorProperty, ) +from devolo_home_control_api.properties.multi_level_sensor_property import ( + MultiLevelSensorProperty, +) from devolo_home_control_api.properties.multi_level_switch_property import ( MultiLevelSwitchProperty, ) @@ -28,6 +31,31 @@ class BinarySensorPropertyMock(BinarySensorProperty): self.state = False +class MultiLevelSensorPropertyMock(MultiLevelSensorProperty): + """devolo Home Control multi level sensor mock.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize the mock.""" + self.element_uid = "Test" + self.sensor_type = "temperature" + self._unit = "°C" + self._value = 20 + self._logger = MagicMock() + + +class MultiLevelSwitchPropertyMock(MultiLevelSwitchProperty): + """devolo Home Control multi level switch mock.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize the mock.""" + self.element_uid = "Test" + self.min = 4 + self.max = 24 + self.switch_type = "temperature" + self._value = 20 + self._logger = MagicMock() + + class SirenPropertyMock(MultiLevelSwitchProperty): """devolo Home Control siren mock.""" @@ -84,6 +112,17 @@ class BinarySensorMockOverload(DeviceMock): self.binary_sensor_property["Overload"].sensor_type = "overload" +class ClimateMock(DeviceMock): + """devolo Home Control climate device mock.""" + + def __init__(self) -> None: + """Initialize the mock.""" + super().__init__() + self.device_model_uid = "devolo.model.Room:Thermostat" + self.multi_level_switch_property = {"Test": MultiLevelSwitchPropertyMock()} + self.multi_level_sensor_property = {"Test": MultiLevelSensorPropertyMock()} + + class RemoteControlMock(DeviceMock): """devolo Home Control remote control device mock.""" @@ -143,6 +182,19 @@ class HomeControlMockBinarySensor(HomeControlMock): self.publisher.unregister = MagicMock() +class HomeControlMockClimate(HomeControlMock): + """devolo Home Control gateway mock with climate devices.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize the mock.""" + super().__init__() + self.devices = { + "Test": ClimateMock(), + } + self.publisher = Publisher(self.devices.keys()) + self.publisher.unregister = MagicMock() + + class HomeControlMockRemoteControl(HomeControlMock): """devolo Home Control gateway mock with remote control device.""" diff --git a/tests/components/devolo_home_control/test_climate.py b/tests/components/devolo_home_control/test_climate.py new file mode 100644 index 00000000000..4fca1825459 --- /dev/null +++ b/tests/components/devolo_home_control/test_climate.py @@ -0,0 +1,81 @@ +"""Tests for the devolo Home Control climate.""" +from unittest.mock import patch + +from homeassistant.components.climate import DOMAIN +from homeassistant.components.climate.const import ( + ATTR_HVAC_MODE, + SERVICE_SET_TEMPERATURE, + HVACMode, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant + +from . import configure_integration +from .mocks import HomeControlMock, HomeControlMockClimate + + +async def test_climate(hass: HomeAssistant): + """Test setup and state change of a climate device.""" + entry = configure_integration(hass) + test_gateway = HomeControlMockClimate() + test_gateway.devices["Test"].value = 20 + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test") + assert state is not None + assert state.state == HVACMode.HEAT + assert state.attributes[ATTR_TEMPERATURE] == test_gateway.devices["Test"].value + + # Emulate websocket message: temperature changed + test_gateway.publisher.dispatch("Test", ("Test", 21.0)) + await hass.async_block_till_done() + state = hass.states.get(f"{DOMAIN}.test") + assert state.state == HVACMode.HEAT + assert state.attributes[ATTR_TEMPERATURE] == 21.0 + + # Test setting temperature + with patch( + "devolo_home_control_api.properties.multi_level_switch_property.MultiLevelSwitchProperty.set" + ) as set_value: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: f"{DOMAIN}.test", + ATTR_HVAC_MODE: HVACMode.HEAT, + ATTR_TEMPERATURE: 20.0, + }, + blocking=True, + ) # In reality, this leads to a websocket message like already tested above + set_value.assert_called_once_with(20.0) + + # Emulate websocket message: device went offline + test_gateway.devices["Test"].status = 1 + test_gateway.publisher.dispatch("Test", ("Status", False, "status")) + await hass.async_block_till_done() + assert hass.states.get(f"{DOMAIN}.test").state == STATE_UNAVAILABLE + + +async def test_remove_from_hass(hass: HomeAssistant): + """Test removing entity.""" + entry = configure_integration(hass) + test_gateway = HomeControlMockClimate() + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test") + assert state is not None + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + assert test_gateway.publisher.unregister.call_count == 2 From 1b5a46a5ba751a455549c01aa7030e0ed6e5e82b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 17:35:35 +0200 Subject: [PATCH 0812/3516] Adjust device_automation type hints in zha (#72142) --- homeassistant/components/zha/core/helpers.py | 12 ++++-- homeassistant/components/zha/device_action.py | 2 +- .../components/zha/device_trigger.py | 38 ++++++++++++++----- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index a101720e8df..33d68822b9f 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -15,7 +15,7 @@ import itertools import logging from random import uniform import re -from typing import Any, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar import voluptuous as vol import zigpy.exceptions @@ -24,7 +24,7 @@ import zigpy.util import zigpy.zdo.types as zdo_types from homeassistant.config_entries import ConfigEntry -from homeassistant.core import State, callback +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import device_registry as dr from .const import ( @@ -37,6 +37,10 @@ from .const import ( from .registries import BINDABLE_CLUSTERS from .typing import ZhaDeviceType, ZigpyClusterType +if TYPE_CHECKING: + from .device import ZHADevice + from .gateway import ZHAGateway + _T = TypeVar("_T") @@ -161,11 +165,11 @@ def async_cluster_exists(hass, cluster_id): @callback -def async_get_zha_device(hass, device_id): +def async_get_zha_device(hass: HomeAssistant, device_id: str) -> ZHADevice: """Get a ZHA device for the given device registry id.""" device_registry = dr.async_get(hass) registry_device = device_registry.async_get(device_id) - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee_address = list(list(registry_device.identifiers)[0])[1] ieee = zigpy.types.EUI64.convert(ieee_address) return zha_gateway.devices[ieee] diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index f941b559cc7..049ffbd40f3 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -48,7 +48,7 @@ async def async_call_action_from_config( hass: HomeAssistant, config: ConfigType, variables: TemplateVarsType, - context: Context, + context: Context | None, ) -> None: """Perform an action based on configuration.""" await ZHA_ACTION_TYPES[DEVICE_ACTION_TYPES[config[CONF_TYPE]]]( diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index d81d8164bd4..670b1cc1477 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -1,12 +1,19 @@ """Provides device automations for ZHA devices that emit events.""" import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + AutomationTriggerInfo, +) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.typing import ConfigType from . import DOMAIN from .core.helpers import async_get_zha_device @@ -21,7 +28,9 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( ) -async def async_validate_trigger_config(hass, config): +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: """Validate config.""" config = TRIGGER_SCHEMA(config) @@ -40,18 +49,25 @@ async def async_validate_trigger_config(hass, config): return config -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: AutomationTriggerInfo, +) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + trigger_key: tuple[str, str] = (config[CONF_TYPE], config[CONF_SUBTYPE]) try: zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) - except (KeyError, AttributeError): - return None + except (KeyError, AttributeError) as err: + raise HomeAssistantError( + f"Unable to get zha device {config[CONF_DEVICE_ID]}" + ) from err - if trigger not in zha_device.device_automation_triggers: - return None + if trigger_key not in zha_device.device_automation_triggers: + raise HomeAssistantError(f"Unable to find trigger {trigger_key}") - trigger = zha_device.device_automation_triggers[trigger] + trigger = zha_device.device_automation_triggers[trigger_key] event_config = { event_trigger.CONF_PLATFORM: "event", @@ -65,7 +81,9 @@ async def async_attach_trigger(hass, config, action, automation_info): ) -async def async_get_triggers(hass, device_id): +async def async_get_triggers( + hass: HomeAssistant, device_id: str +) -> list[dict[str, str]]: """List device triggers. Make sure the device supports device automations and @@ -74,7 +92,7 @@ async def async_get_triggers(hass, device_id): zha_device = async_get_zha_device(hass, device_id) if not zha_device.device_automation_triggers: - return + return [] triggers = [] for trigger, subtype in zha_device.device_automation_triggers.keys(): From c10d456d116443a92b5c14be118a599b0e144274 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Mon, 23 May 2022 11:45:01 -0400 Subject: [PATCH 0813/3516] Handle Sense timeout exceptions from initial authentication (#72369) --- homeassistant/components/sense/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index aaf3630ae19..2e0ed4622e8 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -80,6 +80,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (SenseAuthenticationException, SenseMFARequiredException) as err: _LOGGER.warning("Sense authentication expired") raise ConfigEntryAuthFailed(err) from err + except SENSE_TIMEOUT_EXCEPTIONS as err: + raise ConfigEntryNotReady( + str(err) or "Timed out during authentication" + ) from err sense_devices_data = SenseDevicesData() try: From 7da9cac1f866a10c25d7ee5e33c7b807182da41c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 17:48:41 +0200 Subject: [PATCH 0814/3516] Log SamsungTV state changes (#71989) --- homeassistant/components/samsungtv/media_player.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 6a884c59a87..cf3bfcd64a1 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -197,12 +197,15 @@ class SamsungTVDevice(MediaPlayerEntity): """Update state of device.""" if self._auth_failed or self.hass.is_stopping: return + old_state = self._attr_state if self._power_off_in_progress(): self._attr_state = STATE_OFF else: self._attr_state = ( STATE_ON if await self._bridge.async_is_on() else STATE_OFF ) + if self._attr_state != old_state: + LOGGER.debug("TV %s state updated to %s", self._host, self._attr_state) if self._attr_state != STATE_ON: if self._dmr_device and self._dmr_device.is_subscribed: From deedbca46c163f0c4c5342888877b769d0e6f758 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 23 May 2022 17:52:21 +0200 Subject: [PATCH 0815/3516] Mark unused sync toggle method from ToggleEntity as final (#72370) --- homeassistant/components/hdmi_cec/switch.py | 9 --------- homeassistant/helpers/entity.py | 17 +++++++++++------ 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/hdmi_cec/switch.py b/homeassistant/components/hdmi_cec/switch.py index a5d64b2a7fa..076bedde0b2 100644 --- a/homeassistant/components/hdmi_cec/switch.py +++ b/homeassistant/components/hdmi_cec/switch.py @@ -52,15 +52,6 @@ class CecSwitchEntity(CecEntity, SwitchEntity): self._state = STATE_OFF self.schedule_update_ha_state(force_refresh=False) - def toggle(self, **kwargs): - """Toggle the entity.""" - self._device.toggle() - if self._state == STATE_ON: - self._state = STATE_OFF - else: - self._state = STATE_ON - self.schedule_update_ha_state(force_refresh=False) - @property def is_on(self) -> bool: """Return True if entity is on.""" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index c8faad53b0e..1c03e2334fe 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -1025,15 +1025,20 @@ class ToggleEntity(Entity): """Turn the entity off.""" await self.hass.async_add_executor_job(ft.partial(self.turn_off, **kwargs)) + @final def toggle(self, **kwargs: Any) -> None: - """Toggle the entity.""" - if self.is_on: - self.turn_off(**kwargs) - else: - self.turn_on(**kwargs) + """Toggle the entity. + + This method will never be called by Home Assistant and should not be implemented + by integrations. + """ async def async_toggle(self, **kwargs: Any) -> None: - """Toggle the entity.""" + """Toggle the entity. + + This method should typically not be implemented by integrations, it's enough to + implement async_turn_on + async_turn_off or turn_on + turn_off. + """ if self.is_on: await self.async_turn_off(**kwargs) else: From f90effc29988cece1b3f6acebc99f3563e5f313a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 23 May 2022 18:22:49 +0200 Subject: [PATCH 0816/3516] Add agent version to Supervisor system health (#72360) Co-authored-by: Stefan Agner --- homeassistant/components/hassio/strings.json | 1 + homeassistant/components/hassio/system_health.py | 1 + tests/components/hassio/test_system_health.py | 2 ++ 3 files changed, 4 insertions(+) diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 875a79a60d7..90142bd453f 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "Agent Version", "board": "Board", "disk_total": "Disk Total", "disk_used": "Disk Used", diff --git a/homeassistant/components/hassio/system_health.py b/homeassistant/components/hassio/system_health.py index 5ce38b0e121..1039a0237a8 100644 --- a/homeassistant/components/hassio/system_health.py +++ b/homeassistant/components/hassio/system_health.py @@ -44,6 +44,7 @@ async def system_health_info(hass: HomeAssistant): "host_os": host_info.get("operating_system"), "update_channel": info.get("channel"), "supervisor_version": f"supervisor-{info.get('supervisor')}", + "agent_version": host_info.get("agent_version"), "docker_version": info.get("docker"), "disk_total": f"{host_info.get('disk_total')} GB", "disk_used": f"{host_info.get('disk_used')} GB", diff --git a/tests/components/hassio/test_system_health.py b/tests/components/hassio/test_system_health.py index dcf18f7bc13..344ac9d63be 100644 --- a/tests/components/hassio/test_system_health.py +++ b/tests/components/hassio/test_system_health.py @@ -35,6 +35,7 @@ async def test_hassio_system_health(hass, aioclient_mock): } hass.data["hassio_host_info"] = { "operating_system": "Home Assistant OS 5.9", + "agent_version": "1337", "disk_total": "32.0", "disk_used": "30.0", } @@ -52,6 +53,7 @@ async def test_hassio_system_health(hass, aioclient_mock): info[key] = await val assert info == { + "agent_version": "1337", "board": "odroid-n2", "disk_total": "32.0 GB", "disk_used": "30.0 GB", From 3cdc5c8429338dfe0d0ffb88866052dc7ecd5ce1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 May 2022 11:38:59 -0500 Subject: [PATCH 0817/3516] Add climate platform to Big Ass Fans (#72117) --- .coveragerc | 1 + homeassistant/components/baf/__init__.py | 7 ++- homeassistant/components/baf/climate.py | 60 ++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/baf/climate.py diff --git a/.coveragerc b/.coveragerc index 1eb922b52e5..d884e6e8c13 100644 --- a/.coveragerc +++ b/.coveragerc @@ -94,6 +94,7 @@ omit = homeassistant/components/azure_devops/sensor.py homeassistant/components/azure_service_bus/* homeassistant/components/baf/__init__.py + homeassistant/components/baf/climate.py homeassistant/components/baf/entity.py homeassistant/components/baf/fan.py homeassistant/components/baf/sensor.py diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 6e76014f0b7..30de2582af1 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -14,7 +14,12 @@ from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN, QUERY_INTERVAL, RUN_TIMEOUT from .models import BAFData -PLATFORMS: list[Platform] = [Platform.FAN, Platform.SENSOR, Platform.SWITCH] +PLATFORMS: list[Platform] = [ + Platform.CLIMATE, + Platform.FAN, + Platform.SENSOR, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/baf/climate.py b/homeassistant/components/baf/climate.py new file mode 100644 index 00000000000..f785d18e06f --- /dev/null +++ b/homeassistant/components/baf/climate.py @@ -0,0 +1,60 @@ +"""Support for Big Ass Fans auto comfort.""" +from __future__ import annotations + +from typing import Any + +from homeassistant import config_entries +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import BAFEntity +from .models import BAFData + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up BAF fan auto comfort.""" + data: BAFData = hass.data[DOMAIN][entry.entry_id] + if data.device.has_fan: + async_add_entities( + [BAFAutoComfort(data.device, f"{data.device.name} Auto Comfort")] + ) + + +class BAFAutoComfort(BAFEntity, ClimateEntity): + """BAF climate auto comfort.""" + + _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_temperature_unit = TEMP_CELSIUS + _attr_hvac_modes = [HVACMode.OFF, HVACMode.FAN_ONLY] + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + device = self._device + auto_on = device.auto_comfort_enable + self._attr_hvac_mode = HVACMode.FAN_ONLY if auto_on else HVACMode.OFF + self._attr_hvac_action = HVACAction.FAN if device.speed else HVACAction.OFF + self._attr_target_temperature = device.comfort_ideal_temperature + self._attr_current_temperature = device.temperature + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set the HVAC mode.""" + self._device.auto_comfort_enable = hvac_mode == HVACMode.FAN_ONLY + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set the target temperature.""" + if not self._device.auto_comfort_enable: + self._device.auto_comfort_enable = True + self._device.comfort_ideal_temperature = kwargs[ATTR_TEMPERATURE] From f25663067cfbcc98b7307fd501ce67cc76a81937 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 18:51:40 +0200 Subject: [PATCH 0818/3516] Enforce type hints on device_automation platform (#72126) --- pylint/plugins/hass_enforce_type_hints.py | 158 ++++++++++++++++++++-- tests/pylint/test_enforce_type_hints.py | 75 ++++++++++ 2 files changed, 221 insertions(+), 12 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 6cf521addac..9d35d07fab2 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -31,6 +31,8 @@ _TYPE_HINT_MATCHERS: dict[str, re.Pattern] = { "x_of_y": re.compile(r"^(\w+)\[(.*?]*)\]$"), # x_of_y_comma_z matches items such as "Callable[..., Awaitable[None]]" "x_of_y_comma_z": re.compile(r"^(\w+)\[(.*?]*), (.*?]*)\]$"), + # x_of_y_of_z_comma_a matches items such as "list[dict[str, Any]]" + "x_of_y_of_z_comma_a": re.compile(r"^(\w+)\[(\w+)\[(.*?]*), (.*?]*)\]\]$"), } _MODULE_FILTERS: dict[str, re.Pattern] = { @@ -44,12 +46,20 @@ _MODULE_FILTERS: dict[str, re.Pattern] = { "application_credentials": re.compile( r"^homeassistant\.components\.\w+\.(application_credentials)$" ), - # device_tracker matches only in the package root (device_tracker.py) - "device_tracker": re.compile(r"^homeassistant\.components\.\w+\.(device_tracker)$"), - # diagnostics matches only in the package root (diagnostics.py) - "diagnostics": re.compile(r"^homeassistant\.components\.\w+\.(diagnostics)$"), # config_flow matches only in the package root (config_flow.py) "config_flow": re.compile(r"^homeassistant\.components\.\w+\.(config_flow)$"), + # device_action matches only in the package root (device_action.py) + "device_action": re.compile(r"^homeassistant\.components\.\w+\.(device_action)$"), + # device_condition matches only in the package root (device_condition.py) + "device_condition": re.compile( + r"^homeassistant\.components\.\w+\.(device_condition)$" + ), + # device_tracker matches only in the package root (device_tracker.py) + "device_tracker": re.compile(r"^homeassistant\.components\.\w+\.(device_tracker)$"), + # device_trigger matches only in the package root (device_trigger.py) + "device_trigger": re.compile(r"^homeassistant\.components\.\w+\.(device_trigger)$"), + # diagnostics matches only in the package root (diagnostics.py) + "diagnostics": re.compile(r"^homeassistant\.components\.\w+\.(diagnostics)$"), } _METHOD_MATCH: list[TypeHintMatch] = [ @@ -157,6 +167,88 @@ _METHOD_MATCH: list[TypeHintMatch] = [ }, return_type="AuthorizationServer", ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["config_flow"], + function_name="_async_has_devices", + arg_types={ + 0: "HomeAssistant", + }, + return_type="bool", + ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["device_action"], + function_name="async_validate_action_config", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + }, + return_type="ConfigType", + ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["device_action"], + function_name="async_call_action_from_config", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + 2: "TemplateVarsType", + 3: "Context | None", + }, + return_type=None, + ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["device_action"], + function_name="async_get_action_capabilities", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + }, + return_type="dict[str, Schema]", + ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["device_action"], + function_name="async_get_actions", + arg_types={ + 0: "HomeAssistant", + 1: "str", + }, + return_type=["list[dict[str, str]]", "list[dict[str, Any]]"], + ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["device_condition"], + function_name="async_validate_condition_config", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + }, + return_type="ConfigType", + ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["device_condition"], + function_name="async_condition_from_config", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + }, + return_type="ConditionCheckerType", + ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["device_condition"], + function_name="async_get_condition_capabilities", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + }, + return_type="dict[str, Schema]", + ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["device_condition"], + function_name="async_get_conditions", + arg_types={ + 0: "HomeAssistant", + 1: "str", + }, + return_type=["list[dict[str, str]]", "list[dict[str, Any]]"], + ), TypeHintMatch( module_filter=_MODULE_FILTERS["device_tracker"], function_name="setup_scanner", @@ -197,6 +289,44 @@ _METHOD_MATCH: list[TypeHintMatch] = [ }, return_type=["DeviceScanner", "DeviceScanner | None"], ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["device_trigger"], + function_name="async_validate_condition_config", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + }, + return_type="ConfigType", + ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["device_trigger"], + function_name="async_attach_trigger", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + 2: "AutomationActionType", + 3: "AutomationTriggerInfo", + }, + return_type="CALLBACK_TYPE", + ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["device_trigger"], + function_name="async_get_trigger_capabilities", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + }, + return_type="dict[str, Schema]", + ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["device_trigger"], + function_name="async_get_triggers", + arg_types={ + 0: "HomeAssistant", + 1: "str", + }, + return_type=["list[dict[str, str]]", "list[dict[str, Any]]"], + ), TypeHintMatch( module_filter=_MODULE_FILTERS["diagnostics"], function_name="async_get_config_entry_diagnostics", @@ -216,14 +346,6 @@ _METHOD_MATCH: list[TypeHintMatch] = [ }, return_type=UNDEFINED, ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["config_flow"], - function_name="_async_has_devices", - arg_types={ - 0: "HomeAssistant", - }, - return_type="bool", - ), ] @@ -254,6 +376,18 @@ def _is_valid_type(expected_type: list[str] | str | None, node: astroid.NodeNG) and _is_valid_type(match.group(2), node.right) ) + # Special case for xxx[yyy[zzz, aaa]]` + if match := _TYPE_HINT_MATCHERS["x_of_y_of_z_comma_a"].match(expected_type): + return ( + isinstance(node, astroid.Subscript) + and _is_valid_type(match.group(1), node.value) + and isinstance(subnode := node.slice, astroid.Subscript) + and _is_valid_type(match.group(2), subnode.value) + and isinstance(subnode.slice, astroid.Tuple) + and _is_valid_type(match.group(3), subnode.slice.elts[0]) + and _is_valid_type(match.group(4), subnode.slice.elts[1]) + ) + # Special case for xxx[yyy, zzz]` if match := _TYPE_HINT_MATCHERS["x_of_y_comma_z"].match(expected_type): return ( diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 64be85c9a44..feb3b6b341c 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -14,6 +14,32 @@ import pytest from . import assert_adds_messages, assert_no_messages +@pytest.mark.parametrize( + ("string", "expected_x", "expected_y", "expected_z", "expected_a"), + [ + ("list[dict[str, str]]", "list", "dict", "str", "str"), + ("list[dict[str, Any]]", "list", "dict", "str", "Any"), + ], +) +def test_regex_x_of_y_of_z_comma_a( + hass_enforce_type_hints: ModuleType, + string: str, + expected_x: str, + expected_y: str, + expected_z: str, + expected_a: str, +) -> None: + """Test x_of_y_of_z_comma_a regexes.""" + matchers: dict[str, re.Pattern] = hass_enforce_type_hints._TYPE_HINT_MATCHERS + + assert (match := matchers["x_of_y_of_z_comma_a"].match(string)) + assert match.group(0) == string + assert match.group(1) == expected_x + assert match.group(2) == expected_y + assert match.group(3) == expected_z + assert match.group(4) == expected_a + + @pytest.mark.parametrize( ("string", "expected_x", "expected_y", "expected_z"), [ @@ -165,3 +191,52 @@ def test_valid_discovery_info( with assert_no_messages(linter): type_hint_checker.visit_asyncfunctiondef(func_node) + + +def test_invalid_list_dict_str_any( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Ensure invalid hints are rejected for discovery_info.""" + type_hint_checker.module = "homeassistant.components.pylint_test.device_trigger" + func_node = astroid.extract_node( + """ + async def async_get_triggers( #@ + hass: HomeAssistant, + device_id: str + ) -> list: + pass + """ + ) + + with assert_adds_messages( + linter, + pylint.testutils.MessageTest( + msg_id="hass-return-type", + node=func_node, + args=["list[dict[str, str]]", "list[dict[str, Any]]"], + line=2, + col_offset=0, + end_line=2, + end_col_offset=28, + ), + ): + type_hint_checker.visit_asyncfunctiondef(func_node) + + +def test_valid_list_dict_str_any( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Ensure valid hints are accepted for discovery_info.""" + type_hint_checker.module = "homeassistant.components.pylint_test.device_trigger" + func_node = astroid.extract_node( + """ + async def async_get_triggers( #@ + hass: HomeAssistant, + device_id: str + ) -> list[dict[str, Any]]: + pass + """ + ) + + with assert_no_messages(linter): + type_hint_checker.visit_asyncfunctiondef(func_node) From e2a80e7a45d5d09c7eba00e4a620fc0320aa15ba Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Mon, 23 May 2022 10:02:42 -0700 Subject: [PATCH 0819/3516] Remove unnecessary class from wemo test_fan (#72377) --- tests/components/wemo/test_fan.py | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/tests/components/wemo/test_fan.py b/tests/components/wemo/test_fan.py index 56bf8939181..d58f8edbc3d 100644 --- a/tests/components/wemo/test_fan.py +++ b/tests/components/wemo/test_fan.py @@ -19,6 +19,7 @@ from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, STATE_OFF, STAT from homeassistant.setup import async_setup_component from . import entity_test_helpers +from .conftest import async_create_wemo_entity @pytest.fixture @@ -161,21 +162,14 @@ async def test_fan_set_percentage( pywemo_device.set_state.assert_called_with(expected_fan_mode) -class TestInitialFanMode: - """Test that the FanMode is set to High when turned on the first time.""" - - @pytest.fixture - def pywemo_device(self, pywemo_device): - """Set the FanMode to off initially.""" - pywemo_device.fan_mode = FanMode.Off - yield pywemo_device - - async def test_fan_mode_high_initially(self, hass, pywemo_device, wemo_entity): - """Verify the FanMode is set to High when turned on.""" - assert await hass.services.async_call( - FAN_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, - blocking=True, - ) - pywemo_device.set_state.assert_called_with(FanMode.High) +async def test_fan_mode_high_initially(hass, pywemo_device): + """Verify the FanMode is set to High when turned on.""" + pywemo_device.fan_mode = FanMode.Off + wemo_entity = await async_create_wemo_entity(hass, pywemo_device, "") + assert await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + pywemo_device.set_state.assert_called_with(FanMode.High) From 4baf59666ade3cb4a7d1efadf8fa298eda0f3706 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 May 2022 12:26:08 -0500 Subject: [PATCH 0820/3516] Remove sqlite 3.34.1 downgrade workaround by reverting "Downgrade sqlite-libs on docker image (#55591)" (#72342) --- Dockerfile | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1d6ce675e74..13552d55a3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,21 +25,6 @@ RUN \ -e ./homeassistant --use-deprecated=legacy-resolver \ && python3 -m compileall homeassistant/homeassistant -# Fix Bug with Alpine 3.14 and sqlite 3.35 -# https://gitlab.alpinelinux.org/alpine/aports/-/issues/12524 -ARG BUILD_ARCH -RUN \ - if [ "${BUILD_ARCH}" = "amd64" ]; then \ - export APK_ARCH=x86_64; \ - elif [ "${BUILD_ARCH}" = "i386" ]; then \ - export APK_ARCH=x86; \ - else \ - export APK_ARCH=${BUILD_ARCH}; \ - fi \ - && curl -O http://dl-cdn.alpinelinux.org/alpine/v3.13/main/${APK_ARCH}/sqlite-libs-3.34.1-r0.apk \ - && apk add --no-cache sqlite-libs-3.34.1-r0.apk \ - && rm -f sqlite-libs-3.34.1-r0.apk - # Home Assistant S6-Overlay COPY rootfs / From 92582beeff7a5d1e9fa6cfae3afa41f596b5e3c2 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Mon, 23 May 2022 10:28:16 -0700 Subject: [PATCH 0821/3516] Use properties of wemo Maker device (#72378) --- homeassistant/components/wemo/switch.py | 6 ++--- tests/components/wemo/conftest.py | 6 +++++ tests/components/wemo/test_binary_sensor.py | 13 ----------- tests/components/wemo/test_switch.py | 25 +++++++++++++++++++++ 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index 48cc1d92d67..c3d5bcb0462 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -64,15 +64,15 @@ class WemoSwitch(WemoBinaryStateEntity, SwitchEntity): attr: dict[str, Any] = {} if isinstance(self.wemo, Maker): # Is the maker sensor on or off. - if self.wemo.maker_params["hassensor"]: + if self.wemo.has_sensor: # Note a state of 1 matches the WeMo app 'not triggered'! - if self.wemo.maker_params["sensorstate"]: + if self.wemo.sensor_state: attr[ATTR_SENSOR_STATE] = STATE_OFF else: attr[ATTR_SENSOR_STATE] = STATE_ON # Is the maker switch configured as toggle(0) or momentary (1). - if self.wemo.maker_params["switchmode"]: + if self.wemo.switch_mode: attr[ATTR_SWITCH_MODE] = MAKER_SWITCH_MOMENTARY else: attr[ATTR_SWITCH_MODE] = MAKER_SWITCH_TOGGLE diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index fbb2f186cf5..ae900e2522f 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -77,6 +77,12 @@ def create_pywemo_device(pywemo_registry, pywemo_model): device.today_on_time = 5678 device.total_on_time = 9012 + if issubclass(cls, pywemo.Maker): + device.has_sensor = 1 + device.sensor_state = 1 + device.switch_mode = 1 + device.switch_state = 0 + url = f"http://{MOCK_HOST}:{MOCK_PORT}/setup.xml" with patch("pywemo.setup_url_for_address", return_value=url), patch( "pywemo.discovery.device_from_description", return_value=device diff --git a/tests/components/wemo/test_binary_sensor.py b/tests/components/wemo/test_binary_sensor.py index eccc1b180a5..f26428bf32b 100644 --- a/tests/components/wemo/test_binary_sensor.py +++ b/tests/components/wemo/test_binary_sensor.py @@ -81,19 +81,6 @@ class TestMaker(EntityTestHelpers): """Select the MakerBinarySensor entity.""" return MakerBinarySensor._name_suffix.lower() - @pytest.fixture(name="pywemo_device") - def pywemo_device_fixture(self, pywemo_device): - """Fixture for WeMoDevice instances.""" - pywemo_device.maker_params = { - "hassensor": 1, - "sensorstate": 1, - "switchmode": 1, - "switchstate": 0, - } - pywemo_device.has_sensor = pywemo_device.maker_params["hassensor"] - pywemo_device.sensor_state = pywemo_device.maker_params["sensorstate"] - yield pywemo_device - async def test_registry_state_callback( self, hass, pywemo_registry, pywemo_device, wemo_entity ): diff --git a/tests/components/wemo/test_switch.py b/tests/components/wemo/test_switch.py index d54099e5e4a..0c024295632 100644 --- a/tests/components/wemo/test_switch.py +++ b/tests/components/wemo/test_switch.py @@ -14,6 +14,9 @@ from homeassistant.components.wemo.switch import ( ATTR_ON_TODAY_TIME, ATTR_ON_TOTAL_TIME, ATTR_POWER_THRESHOLD, + ATTR_SENSOR_STATE, + ATTR_SWITCH_MODE, + MAKER_SWITCH_MOMENTARY, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -147,3 +150,25 @@ async def test_insight_state_attributes(hass, pywemo_registry): await async_update() attributes = hass.states.get(wemo_entity.entity_id).attributes assert attributes[ATTR_CURRENT_STATE_DETAIL] == STATE_UNKNOWN + + +async def test_maker_state_attributes(hass, pywemo_registry): + """Verify the switch attributes are set for the Insight device.""" + await async_setup_component(hass, HA_DOMAIN, {}) + with create_pywemo_device(pywemo_registry, "Maker") as maker: + wemo_entity = await async_create_wemo_entity(hass, maker, "") + attributes = hass.states.get(wemo_entity.entity_id).attributes + assert attributes[ATTR_SENSOR_STATE] == STATE_OFF + assert attributes[ATTR_SWITCH_MODE] == MAKER_SWITCH_MOMENTARY + + # Test 'ON' sensor state and 'TOGGLE' switch mode values. + maker.sensor_state = 0 + maker.switch_mode = 0 + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + attributes = hass.states.get(wemo_entity.entity_id).attributes + assert attributes[ATTR_SENSOR_STATE] == STATE_ON From 90e5d691848dc2eb327023504bebc8fc86fd44b9 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Mon, 23 May 2022 19:32:22 +0200 Subject: [PATCH 0822/3516] Add template as_timedelta (#71801) --- homeassistant/helpers/template.py | 7 +++ homeassistant/util/dt.py | 72 +++++++++++++++++++++++++++++++ tests/helpers/test_template.py | 12 ++++++ tests/util/test_dt.py | 28 ++++++++++++ 4 files changed, 119 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index d1ed9d06db9..1a8febf5ac2 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1535,6 +1535,11 @@ def as_datetime(value): return dt_util.parse_datetime(value) +def as_timedelta(value: str) -> timedelta | None: + """Parse a ISO8601 duration like 'PT10M' to a timedelta.""" + return dt_util.parse_duration(value) + + def strptime(string, fmt, default=_SENTINEL): """Parse a time string to datetime.""" try: @@ -1902,6 +1907,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["atan2"] = arc_tangent2 self.filters["sqrt"] = square_root self.filters["as_datetime"] = as_datetime + self.filters["as_timedelta"] = as_timedelta self.filters["as_timestamp"] = forgiving_as_timestamp self.filters["today_at"] = today_at self.filters["as_local"] = dt_util.as_local @@ -1947,6 +1953,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["float"] = forgiving_float self.globals["as_datetime"] = as_datetime self.globals["as_local"] = dt_util.as_local + self.globals["as_timedelta"] = as_timedelta self.globals["as_timestamp"] = forgiving_as_timestamp self.globals["today_at"] = today_at self.globals["relative_time"] = relative_time diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index c7073c0306f..80b322c1a14 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -28,6 +28,49 @@ DATETIME_RE = re.compile( r"(?PZ|[+-]\d{2}(?::?\d{2})?)?$" ) +# Copyright (c) Django Software Foundation and individual contributors. +# All rights reserved. +# https://github.com/django/django/blob/master/LICENSE +STANDARD_DURATION_RE = re.compile( + r"^" + r"(?:(?P-?\d+) (days?, )?)?" + r"(?P-?)" + r"((?:(?P\d+):)(?=\d+:\d+))?" + r"(?:(?P\d+):)?" + r"(?P\d+)" + r"(?:[\.,](?P\d{1,6})\d{0,6})?" + r"$" +) + +# Copyright (c) Django Software Foundation and individual contributors. +# All rights reserved. +# https://github.com/django/django/blob/master/LICENSE +ISO8601_DURATION_RE = re.compile( + r"^(?P[-+]?)" + r"P" + r"(?:(?P\d+([\.,]\d+)?)D)?" + r"(?:T" + r"(?:(?P\d+([\.,]\d+)?)H)?" + r"(?:(?P\d+([\.,]\d+)?)M)?" + r"(?:(?P\d+([\.,]\d+)?)S)?" + r")?" + r"$" +) + +# Copyright (c) Django Software Foundation and individual contributors. +# All rights reserved. +# https://github.com/django/django/blob/master/LICENSE +POSTGRES_INTERVAL_RE = re.compile( + r"^" + r"(?:(?P-?\d+) (days? ?))?" + r"(?:(?P[-+])?" + r"(?P\d+):" + r"(?P\d\d):" + r"(?P\d\d)" + r"(?:\.(?P\d{1,6}))?" + r")?$" +) + def set_default_time_zone(time_zone: dt.tzinfo) -> None: """Set a default time zone to be used when none is specified. @@ -171,6 +214,35 @@ def parse_date(dt_str: str) -> dt.date | None: return None +# Copyright (c) Django Software Foundation and individual contributors. +# All rights reserved. +# https://github.com/django/django/blob/master/LICENSE +def parse_duration(value: str) -> dt.timedelta | None: + """Parse a duration string and return a datetime.timedelta. + + Also supports ISO 8601 representation and PostgreSQL's day-time interval + format. + """ + match = ( + STANDARD_DURATION_RE.match(value) + or ISO8601_DURATION_RE.match(value) + or POSTGRES_INTERVAL_RE.match(value) + ) + if match: + kws = match.groupdict() + sign = -1 if kws.pop("sign", "+") == "-" else 1 + if kws.get("microseconds"): + kws["microseconds"] = kws["microseconds"].ljust(6, "0") + time_delta_args: dict[str, float] = { + k: float(v.replace(",", ".")) for k, v in kws.items() if v is not None + } + days = dt.timedelta(float(time_delta_args.pop("days", 0.0) or 0.0)) + if match.re == ISO8601_DURATION_RE: + days *= sign + return days + sign * dt.timedelta(**time_delta_args) + return None + + def parse_time(time_str: str) -> dt.time | None: """Parse a time string (00:20:00) into Time object. diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index b2448bc1b5c..ddda17c20ac 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -3388,6 +3388,18 @@ def test_urlencode(hass): assert tpl.async_render() == "the%20quick%20brown%20fox%20%3D%20true" +def test_as_timedelta(hass: HomeAssistant) -> None: + """Test the as_timedelta function/filter.""" + tpl = template.Template("{{ as_timedelta('PT10M') }}", hass) + assert tpl.async_render() == "0:10:00" + + tpl = template.Template("{{ 'PT10M' | as_timedelta }}", hass) + assert tpl.async_render() == "0:10:00" + + tpl = template.Template("{{ 'T10M' | as_timedelta }}", hass) + assert tpl.async_render() is None + + def test_iif(hass: HomeAssistant) -> None: """Test the immediate if function/filter.""" tpl = template.Template("{{ (1 == 1) | iif }}", hass) diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index c7992e05068..79cd4e5e0df 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -1,4 +1,6 @@ """Test Home Assistant date util methods.""" +from __future__ import annotations + from datetime import datetime, timedelta import pytest @@ -142,6 +144,32 @@ def test_parse_datetime_returns_none_for_incorrect_format(): assert dt_util.parse_datetime("not a datetime string") is None +@pytest.mark.parametrize( + "duration_string,expected_result", + [ + ("PT10M", timedelta(minutes=10)), + ("PT0S", timedelta(0)), + ("P10DT11H11M01S", timedelta(days=10, hours=11, minutes=11, seconds=1)), + ( + "4 1:20:30.111111", + timedelta(days=4, hours=1, minutes=20, seconds=30, microseconds=111111), + ), + ("4 1:2:30", timedelta(days=4, hours=1, minutes=2, seconds=30)), + ("3 days 04:05:06", timedelta(days=3, hours=4, minutes=5, seconds=6)), + ("P1YT10M", None), + ("P1MT10M", None), + ("1MT10M", None), + ("P1MT100M", None), + ("P1234", None), + ], +) +def test_parse_duration( + duration_string: str, expected_result: timedelta | None +) -> None: + """Test that parse_duration returns the expected result.""" + assert dt_util.parse_duration(duration_string) == expected_result + + def test_get_age(): """Test get_age.""" diff = dt_util.now() - timedelta(seconds=0) From f6370d052273facede196ff92678c58d653d0a65 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 23 May 2022 11:16:21 -0700 Subject: [PATCH 0823/3516] Add Honeywell Lyric application credentials platform and deprecate configuration in yaml (#72335) Add Honeywell Lyric application credentials platform and deprecate config yaml --- homeassistant/components/lyric/__init__.py | 49 ++++++++++++------- homeassistant/components/lyric/api.py | 3 +- .../lyric/application_credentials.py | 26 ++++++++++ homeassistant/components/lyric/manifest.json | 2 +- .../generated/application_credentials.py | 1 + tests/components/lyric/test_config_flow.py | 2 +- 6 files changed, 61 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/lyric/application_credentials.py diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index 3709fa5445f..2eaee440ae7 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -13,6 +13,10 @@ from aiolyric.objects.location import LyricLocation import async_timeout import voluptuous as vol +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform from homeassistant.core import HomeAssistant @@ -36,18 +40,20 @@ from .api import ( LyricLocalOAuth2Implementation, OAuth2SessionLyric, ) -from .config_flow import OAuth2FlowHandler -from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN +from .const import DOMAIN CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -63,20 +69,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if DOMAIN not in config: return True - hass.data[DOMAIN][CONF_CLIENT_ID] = config[DOMAIN][CONF_CLIENT_ID] - - OAuth2FlowHandler.async_register_implementation( + await async_import_client_credential( hass, - LyricLocalOAuth2Implementation( - hass, - DOMAIN, + DOMAIN, + ClientCredential( config[DOMAIN][CONF_CLIENT_ID], config[DOMAIN][CONF_CLIENT_SECRET], - OAUTH2_AUTHORIZE, - OAUTH2_TOKEN, ), ) + _LOGGER.warning( + "Configuration of Honeywell Lyric integration in YAML is deprecated " + "and will be removed in a future release; Your existing OAuth " + "Application Credentials have been imported into the UI " + "automatically and can be safely removed from your " + "configuration.yaml file" + ) + return True @@ -87,13 +96,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, entry ) ) + if not isinstance(implementation, LyricLocalOAuth2Implementation): + raise ValueError("Unexpected auth implementation; can't find oauth client id") session = aiohttp_client.async_get_clientsession(hass) oauth_session = OAuth2SessionLyric(hass, entry, implementation) client = ConfigEntryLyricClient(session, oauth_session) - client_id = hass.data[DOMAIN][CONF_CLIENT_ID] + client_id = implementation.client_id lyric = Lyric(client, client_id) async def async_update_data(force_refresh_token: bool = False) -> Lyric: diff --git a/homeassistant/components/lyric/api.py b/homeassistant/components/lyric/api.py index 4a8aa44417f..171e010137a 100644 --- a/homeassistant/components/lyric/api.py +++ b/homeassistant/components/lyric/api.py @@ -4,6 +4,7 @@ from typing import cast from aiohttp import BasicAuth, ClientSession from aiolyric.client import LyricClient +from homeassistant.components.application_credentials import AuthImplementation from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -41,7 +42,7 @@ class ConfigEntryLyricClient(LyricClient): class LyricLocalOAuth2Implementation( - config_entry_oauth2_flow.LocalOAuth2Implementation + AuthImplementation, ): """Lyric Local OAuth2 implementation.""" diff --git a/homeassistant/components/lyric/application_credentials.py b/homeassistant/components/lyric/application_credentials.py new file mode 100644 index 00000000000..2ccdca72bb6 --- /dev/null +++ b/homeassistant/components/lyric/application_credentials.py @@ -0,0 +1,26 @@ +"""Application credentials platform for the Honeywell Lyric integration.""" + +from homeassistant.components.application_credentials import ( + AuthorizationServer, + ClientCredential, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow + +from .api import LyricLocalOAuth2Implementation +from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN + + +async def async_get_auth_implementation( + hass: HomeAssistant, auth_domain: str, credential: ClientCredential +) -> config_entry_oauth2_flow.AbstractOAuth2Implementation: + """Return custom auth implementation.""" + return LyricLocalOAuth2Implementation( + hass, + auth_domain, + credential, + AuthorizationServer( + authorize_url=OAUTH2_AUTHORIZE, + token_url=OAUTH2_TOKEN, + ), + ) diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json index da60b046eb7..c0d9168f46f 100644 --- a/homeassistant/components/lyric/manifest.json +++ b/homeassistant/components/lyric/manifest.json @@ -3,7 +3,7 @@ "name": "Honeywell Lyric", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lyric", - "dependencies": ["auth"], + "dependencies": ["application_credentials"], "requirements": ["aiolyric==1.0.8"], "codeowners": ["@timmo001"], "quality_scale": "silver", diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index 63c19fe10b8..6d40b3fdef7 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -9,6 +9,7 @@ APPLICATION_CREDENTIALS = [ "geocaching", "google", "home_connect", + "lyric", "neato", "netatmo", "senz", diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index 25cc49c6c09..d9262e215fb 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -41,7 +41,7 @@ async def test_abort_if_no_configuration(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "missing_configuration" + assert result["reason"] == "missing_credentials" async def test_full_flow( From 0248a8710f347401dbafa47d350657781a66dba1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 May 2022 13:35:45 -0500 Subject: [PATCH 0824/3516] Always pass the source of the trigger for logbook context messages (#72333) --- homeassistant/components/alexa/logbook.py | 11 ++++- .../components/automation/logbook.py | 17 +++++--- homeassistant/components/deconz/logbook.py | 24 ++++++----- homeassistant/components/doorbird/logbook.py | 15 ++++--- homeassistant/components/elkm1/logbook.py | 8 +++- .../components/google_assistant/logbook.py | 6 ++- .../components/homeassistant/logbook.py | 2 +- homeassistant/components/homekit/logbook.py | 11 +++-- homeassistant/components/hue/logbook.py | 8 +++- homeassistant/components/logbook/__init__.py | 1 - homeassistant/components/logbook/const.py | 3 ++ homeassistant/components/logbook/processor.py | 11 +++-- .../components/lutron_caseta/logbook.py | 8 +++- .../components/mobile_app/logbook.py | 14 ++++-- homeassistant/components/script/logbook.py | 14 ++++-- homeassistant/components/shelly/logbook.py | 8 +++- .../components/logbook/test_websocket_api.py | 43 +++++++++++++------ 17 files changed, 145 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/alexa/logbook.py b/homeassistant/components/alexa/logbook.py index 65fb410c601..b72b884ec29 100644 --- a/homeassistant/components/alexa/logbook.py +++ b/homeassistant/components/alexa/logbook.py @@ -1,4 +1,9 @@ """Describe logbook events.""" +from homeassistant.components.logbook.const import ( + LOGBOOK_ENTRY_ENTITY_ID, + LOGBOOK_ENTRY_MESSAGE, + LOGBOOK_ENTRY_NAME, +) from homeassistant.core import callback from .const import DOMAIN, EVENT_ALEXA_SMART_HOME @@ -22,6 +27,10 @@ def async_describe_events(hass, async_describe_event): f"sent command {data['request']['namespace']}/{data['request']['name']}" ) - return {"name": "Amazon Alexa", "message": message, "entity_id": entity_id} + return { + LOGBOOK_ENTRY_NAME: "Amazon Alexa", + LOGBOOK_ENTRY_MESSAGE: message, + LOGBOOK_ENTRY_ENTITY_ID: entity_id, + } async_describe_event(DOMAIN, EVENT_ALEXA_SMART_HOME, async_describe_logbook_event) diff --git a/homeassistant/components/automation/logbook.py b/homeassistant/components/automation/logbook.py index 97a859d25b0..529fed80d26 100644 --- a/homeassistant/components/automation/logbook.py +++ b/homeassistant/components/automation/logbook.py @@ -1,5 +1,12 @@ """Describe logbook events.""" from homeassistant.components.logbook import LazyEventPartialState +from homeassistant.components.logbook.const import ( + LOGBOOK_ENTRY_CONTEXT_ID, + LOGBOOK_ENTRY_ENTITY_ID, + LOGBOOK_ENTRY_MESSAGE, + LOGBOOK_ENTRY_NAME, + LOGBOOK_ENTRY_SOURCE, +) from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME from homeassistant.core import HomeAssistant, callback @@ -20,11 +27,11 @@ def async_describe_events(hass: HomeAssistant, async_describe_event): # type: i message = f"{message} by {data[ATTR_SOURCE]}" return { - "name": data.get(ATTR_NAME), - "message": message, - "source": data.get(ATTR_SOURCE), - "entity_id": data.get(ATTR_ENTITY_ID), - "context_id": event.context_id, + LOGBOOK_ENTRY_NAME: data.get(ATTR_NAME), + LOGBOOK_ENTRY_MESSAGE: message, + LOGBOOK_ENTRY_SOURCE: data.get(ATTR_SOURCE), + LOGBOOK_ENTRY_ENTITY_ID: data.get(ATTR_ENTITY_ID), + LOGBOOK_ENTRY_CONTEXT_ID: event.context_id, } async_describe_event( diff --git a/homeassistant/components/deconz/logbook.py b/homeassistant/components/deconz/logbook.py index e67e07d2222..07dc7cb0124 100644 --- a/homeassistant/components/deconz/logbook.py +++ b/homeassistant/components/deconz/logbook.py @@ -3,6 +3,10 @@ from __future__ import annotations from collections.abc import Callable +from homeassistant.components.logbook.const import ( + LOGBOOK_ENTRY_MESSAGE, + LOGBOOK_ENTRY_NAME, +) from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.device_registry as dr @@ -135,8 +139,8 @@ def async_describe_events( data = event.data[CONF_EVENT] return { - "name": f"{deconz_alarm_event.device.name}", - "message": f"fired event '{data}'", + LOGBOOK_ENTRY_NAME: f"{deconz_alarm_event.device.name}", + LOGBOOK_ENTRY_MESSAGE: f"fired event '{data}'", } @callback @@ -157,27 +161,27 @@ def async_describe_events( # Unknown event if not data: return { - "name": f"{deconz_event.device.name}", - "message": "fired an unknown event", + LOGBOOK_ENTRY_NAME: f"{deconz_event.device.name}", + LOGBOOK_ENTRY_MESSAGE: "fired an unknown event", } # No device event match if not action: return { - "name": f"{deconz_event.device.name}", - "message": f"fired event '{data}'", + LOGBOOK_ENTRY_NAME: f"{deconz_event.device.name}", + LOGBOOK_ENTRY_MESSAGE: f"fired event '{data}'", } # Gesture event if not interface: return { - "name": f"{deconz_event.device.name}", - "message": f"fired event '{ACTIONS[action]}'", + LOGBOOK_ENTRY_NAME: f"{deconz_event.device.name}", + LOGBOOK_ENTRY_MESSAGE: f"fired event '{ACTIONS[action]}'", } return { - "name": f"{deconz_event.device.name}", - "message": f"'{ACTIONS[action]}' event for '{INTERFACES[interface]}' was fired", + LOGBOOK_ENTRY_NAME: f"{deconz_event.device.name}", + LOGBOOK_ENTRY_MESSAGE: f"'{ACTIONS[action]}' event for '{INTERFACES[interface]}' was fired", } async_describe_event( diff --git a/homeassistant/components/doorbird/logbook.py b/homeassistant/components/doorbird/logbook.py index fbd6c670a8d..110d3f22fbf 100644 --- a/homeassistant/components/doorbird/logbook.py +++ b/homeassistant/components/doorbird/logbook.py @@ -1,5 +1,10 @@ """Describe logbook events.""" +from homeassistant.components.logbook.const import ( + LOGBOOK_ENTRY_ENTITY_ID, + LOGBOOK_ENTRY_MESSAGE, + LOGBOOK_ENTRY_NAME, +) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import callback @@ -16,11 +21,11 @@ def async_describe_events(hass, async_describe_event): doorbird_event = event.event_type.split("_", 1)[1] return { - "name": "Doorbird", - "message": f"Event {event.event_type} was fired", - "entity_id": hass.data[DOMAIN][DOOR_STATION_EVENT_ENTITY_IDS].get( - doorbird_event, event.data.get(ATTR_ENTITY_ID) - ), + LOGBOOK_ENTRY_NAME: "Doorbird", + LOGBOOK_ENTRY_MESSAGE: f"Event {event.event_type} was fired", + LOGBOOK_ENTRY_ENTITY_ID: hass.data[DOMAIN][ + DOOR_STATION_EVENT_ENTITY_IDS + ].get(doorbird_event, event.data.get(ATTR_ENTITY_ID)), } domain_data = hass.data[DOMAIN] diff --git a/homeassistant/components/elkm1/logbook.py b/homeassistant/components/elkm1/logbook.py index 01019ce77e0..9aa85b599e0 100644 --- a/homeassistant/components/elkm1/logbook.py +++ b/homeassistant/components/elkm1/logbook.py @@ -3,6 +3,10 @@ from __future__ import annotations from collections.abc import Callable +from homeassistant.components.logbook.const import ( + LOGBOOK_ENTRY_MESSAGE, + LOGBOOK_ENTRY_NAME, +) from homeassistant.core import Event, HomeAssistant, callback from .const import ( @@ -30,8 +34,8 @@ def async_describe_events( ATTR_KEYPAD_NAME, data[ATTR_KEYPAD_ID] ) # added in 2022.6 return { - "name": f"Elk Keypad {keypad_name}", - "message": f"pressed {data[ATTR_KEY_NAME]} ({data[ATTR_KEY]})", + LOGBOOK_ENTRY_NAME: f"Elk Keypad {keypad_name}", + LOGBOOK_ENTRY_MESSAGE: f"pressed {data[ATTR_KEY_NAME]} ({data[ATTR_KEY]})", } async_describe_event( diff --git a/homeassistant/components/google_assistant/logbook.py b/homeassistant/components/google_assistant/logbook.py index 86caa8a9e6c..0ed5745004d 100644 --- a/homeassistant/components/google_assistant/logbook.py +++ b/homeassistant/components/google_assistant/logbook.py @@ -1,4 +1,8 @@ """Describe logbook events.""" +from homeassistant.components.logbook.const import ( + LOGBOOK_ENTRY_MESSAGE, + LOGBOOK_ENTRY_NAME, +) from homeassistant.core import callback from .const import DOMAIN, EVENT_COMMAND_RECEIVED, SOURCE_CLOUD @@ -25,6 +29,6 @@ def async_describe_events(hass, async_describe_event): if event.data["source"] != SOURCE_CLOUD: message += f" (via {event.data['source']})" - return {"name": "Google Assistant", "message": message} + return {LOGBOOK_ENTRY_NAME: "Google Assistant", LOGBOOK_ENTRY_MESSAGE: message} async_describe_event(DOMAIN, EVENT_COMMAND_RECEIVED, async_describe_logbook_event) diff --git a/homeassistant/components/homeassistant/logbook.py b/homeassistant/components/homeassistant/logbook.py index 229fb24cb27..548753982a8 100644 --- a/homeassistant/components/homeassistant/logbook.py +++ b/homeassistant/components/homeassistant/logbook.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable -from homeassistant.components.logbook import ( +from homeassistant.components.logbook.const import ( LOGBOOK_ENTRY_ICON, LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME, diff --git a/homeassistant/components/homekit/logbook.py b/homeassistant/components/homekit/logbook.py index b6805f8cf6c..a513b31b232 100644 --- a/homeassistant/components/homekit/logbook.py +++ b/homeassistant/components/homekit/logbook.py @@ -2,6 +2,11 @@ from collections.abc import Callable from typing import Any +from homeassistant.components.logbook.const import ( + LOGBOOK_ENTRY_ENTITY_ID, + LOGBOOK_ENTRY_MESSAGE, + LOGBOOK_ENTRY_NAME, +) from homeassistant.const import ATTR_ENTITY_ID, ATTR_SERVICE from homeassistant.core import Event, HomeAssistant, callback @@ -26,9 +31,9 @@ def async_describe_events( message = f"send command {data[ATTR_SERVICE]}{value_msg} for {data[ATTR_DISPLAY_NAME]}" return { - "name": "HomeKit", - "message": message, - "entity_id": entity_id, + LOGBOOK_ENTRY_NAME: "HomeKit", + LOGBOOK_ENTRY_MESSAGE: message, + LOGBOOK_ENTRY_ENTITY_ID: entity_id, } async_describe_event(DOMAIN, EVENT_HOMEKIT_CHANGED, async_describe_logbook_event) diff --git a/homeassistant/components/hue/logbook.py b/homeassistant/components/hue/logbook.py index 40abce7e2d3..e98a99f1861 100644 --- a/homeassistant/components/hue/logbook.py +++ b/homeassistant/components/hue/logbook.py @@ -3,6 +3,10 @@ from __future__ import annotations from collections.abc import Callable +from homeassistant.components.logbook.const import ( + LOGBOOK_ENTRY_MESSAGE, + LOGBOOK_ENTRY_NAME, +) from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_TYPE from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import device_registry as dr @@ -66,8 +70,8 @@ def async_describe_events( else: message = f"Event {data[CONF_EVENT]}" # v1 return { - "name": name, - "message": str(message), + LOGBOOK_ENTRY_NAME: name, + LOGBOOK_ENTRY_MESSAGE: message, } async_describe_event(DOMAIN, ATTR_HUE_EVENT, async_describe_hue_event) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index d9893781748..f66f1d5e920 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -39,7 +39,6 @@ from .const import ( LOGBOOK_ENTRY_NAME, LOGBOOK_FILTERS, ) -from .const import LOGBOOK_ENTRY_ICON # noqa: F401 from .models import LazyEventPartialState # noqa: F401 CONFIG_SCHEMA = vol.Schema( diff --git a/homeassistant/components/logbook/const.py b/homeassistant/components/logbook/const.py index 406406e9dd8..3f0c6599724 100644 --- a/homeassistant/components/logbook/const.py +++ b/homeassistant/components/logbook/const.py @@ -15,13 +15,16 @@ CONTEXT_ENTITY_ID_NAME = "context_entity_id_name" CONTEXT_EVENT_TYPE = "context_event_type" CONTEXT_DOMAIN = "context_domain" CONTEXT_STATE = "context_state" +CONTEXT_SOURCE = "context_source" CONTEXT_SERVICE = "context_service" CONTEXT_NAME = "context_name" CONTEXT_MESSAGE = "context_message" +LOGBOOK_ENTRY_CONTEXT_ID = "context_id" LOGBOOK_ENTRY_DOMAIN = "domain" LOGBOOK_ENTRY_ENTITY_ID = "entity_id" LOGBOOK_ENTRY_ICON = "icon" +LOGBOOK_ENTRY_SOURCE = "source" LOGBOOK_ENTRY_MESSAGE = "message" LOGBOOK_ENTRY_NAME = "name" LOGBOOK_ENTRY_STATE = "state" diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index c9f20f27f7c..03506695700 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -42,6 +42,7 @@ from .const import ( CONTEXT_MESSAGE, CONTEXT_NAME, CONTEXT_SERVICE, + CONTEXT_SOURCE, CONTEXT_STATE, CONTEXT_USER_ID, DOMAIN, @@ -51,6 +52,7 @@ from .const import ( LOGBOOK_ENTRY_ICON, LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME, + LOGBOOK_ENTRY_SOURCE, LOGBOOK_ENTRY_STATE, LOGBOOK_ENTRY_WHEN, LOGBOOK_FILTERS, @@ -398,11 +400,14 @@ class ContextAugmenter: data[CONTEXT_DOMAIN] = domain event = self.event_cache.get(context_row) described = describe_event(event) - if name := described.get(ATTR_NAME): + if name := described.get(LOGBOOK_ENTRY_NAME): data[CONTEXT_NAME] = name - if message := described.get(ATTR_MESSAGE): + if message := described.get(LOGBOOK_ENTRY_MESSAGE): data[CONTEXT_MESSAGE] = message - if not (attr_entity_id := described.get(ATTR_ENTITY_ID)): + # In 2022.12 and later drop `CONTEXT_MESSAGE` if `CONTEXT_SOURCE` is available + if source := described.get(LOGBOOK_ENTRY_SOURCE): + data[CONTEXT_SOURCE] = source + if not (attr_entity_id := described.get(LOGBOOK_ENTRY_ENTITY_ID)): return data[CONTEXT_ENTITY_ID] = attr_entity_id if self.include_entity_name: diff --git a/homeassistant/components/lutron_caseta/logbook.py b/homeassistant/components/lutron_caseta/logbook.py index 28342090a21..bcca548f64b 100644 --- a/homeassistant/components/lutron_caseta/logbook.py +++ b/homeassistant/components/lutron_caseta/logbook.py @@ -3,6 +3,10 @@ from __future__ import annotations from collections.abc import Callable +from homeassistant.components.logbook.const import ( + LOGBOOK_ENTRY_MESSAGE, + LOGBOOK_ENTRY_NAME, +) from homeassistant.core import Event, HomeAssistant, callback from .const import ( @@ -33,8 +37,8 @@ def async_describe_events( button_map = LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP[device_type] button_description = button_map[leap_button_number] return { - "name": f"{data[ATTR_AREA_NAME]} {data[ATTR_DEVICE_NAME]}", - "message": f"{data[ATTR_ACTION]} {button_description}", + LOGBOOK_ENTRY_NAME: f"{data[ATTR_AREA_NAME]} {data[ATTR_DEVICE_NAME]}", + LOGBOOK_ENTRY_MESSAGE: f"{data[ATTR_ACTION]} {button_description}", } async_describe_event( diff --git a/homeassistant/components/mobile_app/logbook.py b/homeassistant/components/mobile_app/logbook.py index 6dd4d007e3e..6f7c2e4e99c 100644 --- a/homeassistant/components/mobile_app/logbook.py +++ b/homeassistant/components/mobile_app/logbook.py @@ -3,6 +3,12 @@ from __future__ import annotations from collections.abc import Callable +from homeassistant.components.logbook.const import ( + LOGBOOK_ENTRY_ENTITY_ID, + LOGBOOK_ENTRY_ICON, + LOGBOOK_ENTRY_MESSAGE, + LOGBOOK_ENTRY_NAME, +) from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON from homeassistant.core import Event, HomeAssistant, callback @@ -42,12 +48,12 @@ def async_describe_events( zone_name = zone_state.attributes.get(ATTR_FRIENDLY_NAME) zone_icon = zone_state.attributes.get(ATTR_ICON) description = { - "name": source_device_name, - "message": f"{event_description} {zone_name or zone_entity_id}", - "icon": zone_icon or "mdi:crosshairs-gps", + LOGBOOK_ENTRY_NAME: source_device_name, + LOGBOOK_ENTRY_MESSAGE: f"{event_description} {zone_name or zone_entity_id}", + LOGBOOK_ENTRY_ICON: zone_icon or "mdi:crosshairs-gps", } if zone_entity_id: - description["entity_id"] = zone_entity_id + description[LOGBOOK_ENTRY_ENTITY_ID] = zone_entity_id return description async_describe_event(DOMAIN, IOS_EVENT_ZONE_ENTERED, async_describe_zone_event) diff --git a/homeassistant/components/script/logbook.py b/homeassistant/components/script/logbook.py index 250b7231b32..7fcbad07479 100644 --- a/homeassistant/components/script/logbook.py +++ b/homeassistant/components/script/logbook.py @@ -1,4 +1,10 @@ """Describe logbook events.""" +from homeassistant.components.logbook.const import ( + LOGBOOK_ENTRY_CONTEXT_ID, + LOGBOOK_ENTRY_ENTITY_ID, + LOGBOOK_ENTRY_MESSAGE, + LOGBOOK_ENTRY_NAME, +) from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME from homeassistant.core import callback @@ -14,10 +20,10 @@ def async_describe_events(hass, async_describe_event): """Describe the logbook event.""" data = event.data return { - "name": data.get(ATTR_NAME), - "message": "started", - "entity_id": data.get(ATTR_ENTITY_ID), - "context_id": event.context_id, + LOGBOOK_ENTRY_NAME: data.get(ATTR_NAME), + LOGBOOK_ENTRY_MESSAGE: "started", + LOGBOOK_ENTRY_ENTITY_ID: data.get(ATTR_ENTITY_ID), + LOGBOOK_ENTRY_CONTEXT_ID: event.context_id, } async_describe_event(DOMAIN, EVENT_SCRIPT_STARTED, async_describe_logbook_event) diff --git a/homeassistant/components/shelly/logbook.py b/homeassistant/components/shelly/logbook.py index 504dfe90791..a91f4e1cf56 100644 --- a/homeassistant/components/shelly/logbook.py +++ b/homeassistant/components/shelly/logbook.py @@ -3,6 +3,10 @@ from __future__ import annotations from collections.abc import Callable +from homeassistant.components.logbook.const import ( + LOGBOOK_ENTRY_MESSAGE, + LOGBOOK_ENTRY_NAME, +) from homeassistant.const import ATTR_DEVICE_ID from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.typing import EventType @@ -48,8 +52,8 @@ def async_describe_events( input_name = f"{device_name} channel {channel}" return { - "name": "Shelly", - "message": f"'{click_type}' click event for {input_name} Input was fired", + LOGBOOK_ENTRY_NAME: "Shelly", + LOGBOOK_ENTRY_MESSAGE: f"'{click_type}' click event for {input_name} Input was fired", } async_describe_event(DOMAIN, EVENT_SHELLY_CLICK, async_describe_shelly_click_event) diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 585732b66f1..e1fc1defe09 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -8,7 +8,7 @@ import pytest from homeassistant import core from homeassistant.components import logbook, recorder -from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED +from homeassistant.components.automation import ATTR_SOURCE, EVENT_AUTOMATION_TRIGGERED from homeassistant.components.logbook import websocket_api from homeassistant.components.script import EVENT_SCRIPT_STARTED from homeassistant.components.websocket_api.const import TYPE_RESULT @@ -535,11 +535,19 @@ async def test_subscribe_unsubscribe_logbook_stream( hass.bus.async_fire( EVENT_AUTOMATION_TRIGGERED, - {ATTR_NAME: "Mock automation", ATTR_ENTITY_ID: "automation.mock_automation"}, + { + ATTR_NAME: "Mock automation", + ATTR_ENTITY_ID: "automation.mock_automation", + ATTR_SOURCE: "numeric state of sensor.hungry_dogs", + }, ) hass.bus.async_fire( EVENT_SCRIPT_STARTED, - {ATTR_NAME: "Mock script", ATTR_ENTITY_ID: "script.mock_script"}, + { + ATTR_NAME: "Mock script", + ATTR_ENTITY_ID: "script.mock_script", + ATTR_SOURCE: "numeric state of sensor.hungry_dogs", + }, ) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -552,9 +560,9 @@ async def test_subscribe_unsubscribe_logbook_stream( "context_id": ANY, "domain": "automation", "entity_id": "automation.mock_automation", - "message": "triggered", + "message": "triggered by numeric state of sensor.hungry_dogs", "name": "Mock automation", - "source": None, + "source": "numeric state of sensor.hungry_dogs", "when": ANY, }, { @@ -581,7 +589,11 @@ async def test_subscribe_unsubscribe_logbook_stream( automation_entity_id_test = "automation.alarm" hass.bus.async_fire( EVENT_AUTOMATION_TRIGGERED, - {ATTR_NAME: "Mock automation", ATTR_ENTITY_ID: automation_entity_id_test}, + { + ATTR_NAME: "Mock automation", + ATTR_ENTITY_ID: automation_entity_id_test, + ATTR_SOURCE: "state of binary_sensor.dog_food_ready", + }, context=context, ) hass.bus.async_fire( @@ -613,9 +625,9 @@ async def test_subscribe_unsubscribe_logbook_stream( "context_user_id": "b400facee45711eaa9308bfd3d19e474", "domain": "automation", "entity_id": "automation.alarm", - "message": "triggered", + "message": "triggered by state of binary_sensor.dog_food_ready", "name": "Mock automation", - "source": None, + "source": "state of binary_sensor.dog_food_ready", "when": ANY, }, { @@ -623,8 +635,9 @@ async def test_subscribe_unsubscribe_logbook_stream( "context_entity_id": "automation.alarm", "context_event_type": "automation_triggered", "context_id": "ac5bd62de45711eaaeb351041eec8dd9", - "context_message": "triggered", + "context_message": "triggered by state of " "binary_sensor.dog_food_ready", "context_name": "Mock automation", + "context_source": "state of binary_sensor.dog_food_ready", "context_user_id": "b400facee45711eaa9308bfd3d19e474", "domain": "script", "entity_id": "script.mock_script", @@ -636,8 +649,9 @@ async def test_subscribe_unsubscribe_logbook_stream( "context_domain": "automation", "context_entity_id": "automation.alarm", "context_event_type": "automation_triggered", - "context_message": "triggered", + "context_message": "triggered by state of " "binary_sensor.dog_food_ready", "context_name": "Mock automation", + "context_source": "state of binary_sensor.dog_food_ready", "context_user_id": "b400facee45711eaa9308bfd3d19e474", "entity_id": "alarm_control_panel.area_001", "state": "on", @@ -647,8 +661,9 @@ async def test_subscribe_unsubscribe_logbook_stream( "context_domain": "automation", "context_entity_id": "automation.alarm", "context_event_type": "automation_triggered", - "context_message": "triggered", + "context_message": "triggered by state of " "binary_sensor.dog_food_ready", "context_name": "Mock automation", + "context_source": "state of binary_sensor.dog_food_ready", "context_user_id": "b400facee45711eaa9308bfd3d19e474", "entity_id": "alarm_control_panel.area_002", "state": "on", @@ -672,8 +687,9 @@ async def test_subscribe_unsubscribe_logbook_stream( "context_entity_id": "automation.alarm", "context_event_type": "automation_triggered", "context_id": "ac5bd62de45711eaaeb351041eec8dd9", - "context_message": "triggered", + "context_message": "triggered by state of binary_sensor.dog_food_ready", "context_name": "Mock automation", + "context_source": "state of binary_sensor.dog_food_ready", "context_user_id": "b400facee45711eaa9308bfd3d19e474", "domain": "automation", "entity_id": "automation.alarm", @@ -701,8 +717,9 @@ async def test_subscribe_unsubscribe_logbook_stream( "context_entity_id": "automation.alarm", "context_event_type": "automation_triggered", "context_id": "ac5bd62de45711eaaeb351041eec8dd9", - "context_message": "triggered", + "context_message": "triggered by state of binary_sensor.dog_food_ready", "context_name": "Mock automation", + "context_source": "state of binary_sensor.dog_food_ready", "context_user_id": "b400facee45711eaa9308bfd3d19e474", "domain": "automation", "entity_id": "automation.alarm", From 53d7eaa1a6f77a590886be7a413ec00e8b6c3dae Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 May 2022 20:50:39 +0200 Subject: [PATCH 0825/3516] Update Pillow to 9.1.1 (#72376) --- homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/generic/manifest.json | 2 +- homeassistant/components/image/manifest.json | 2 +- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- homeassistant/components/seven_segments/manifest.json | 2 +- homeassistant/components/sighthound/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 2ea550c3792..ec170733e44 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -2,7 +2,7 @@ "domain": "doods", "name": "DOODS - Dedicated Open Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": ["pydoods==1.0.2", "pillow==9.1.0"], + "requirements": ["pydoods==1.0.2", "pillow==9.1.1"], "codeowners": [], "iot_class": "local_polling", "loggers": ["pydoods"] diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index ae14c3c4d03..c590ddfffcd 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -2,7 +2,7 @@ "domain": "generic", "name": "Generic Camera", "config_flow": true, - "requirements": ["av==9.2.0", "pillow==9.1.0"], + "requirements": ["av==9.2.0", "pillow==9.1.1"], "documentation": "https://www.home-assistant.io/integrations/generic", "codeowners": ["@davet2001"], "iot_class": "local_push" diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json index 4ddba8f31e0..d2e08b77b23 100644 --- a/homeassistant/components/image/manifest.json +++ b/homeassistant/components/image/manifest.json @@ -3,7 +3,7 @@ "name": "Image", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/image", - "requirements": ["pillow==9.1.0"], + "requirements": ["pillow==9.1.1"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index ff3e610323f..d12f5ed92fd 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,6 +2,6 @@ "domain": "proxy", "name": "Camera Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["pillow==9.1.0"], + "requirements": ["pillow==9.1.1"], "codeowners": [] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 66a50bf98d9..6715d1ba6db 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -2,7 +2,7 @@ "domain": "qrcode", "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", - "requirements": ["pillow==9.1.0", "pyzbar==0.1.7"], + "requirements": ["pillow==9.1.1", "pyzbar==0.1.7"], "codeowners": [], "iot_class": "calculated", "loggers": ["pyzbar"] diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index b5a83dfd747..caee1d72c38 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,7 +2,7 @@ "domain": "seven_segments", "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": ["pillow==9.1.0"], + "requirements": ["pillow==9.1.1"], "codeowners": ["@fabaff"], "iot_class": "local_polling" } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index 3afe8052e03..e87e1d37304 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -2,7 +2,7 @@ "domain": "sighthound", "name": "Sighthound", "documentation": "https://www.home-assistant.io/integrations/sighthound", - "requirements": ["pillow==9.1.0", "simplehound==0.3"], + "requirements": ["pillow==9.1.1", "simplehound==0.3"], "codeowners": ["@robmarkcole"], "iot_class": "cloud_polling", "loggers": ["simplehound"] diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 0f53dd61cb4..efd4f3d76d0 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -7,7 +7,7 @@ "tf-models-official==2.5.0", "pycocotools==2.0.1", "numpy==1.21.6", - "pillow==9.1.0" + "pillow==9.1.1" ], "codeowners": [], "iot_class": "local_polling", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 354b06d2c10..e4bed71458f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.7 paho-mqtt==1.6.1 -pillow==9.1.0 +pillow==9.1.1 pip>=21.0,<22.2 pyserial==3.5 python-slugify==4.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index ca2028b8eaa..12bd9a705b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1230,7 +1230,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.1.0 +pillow==9.1.1 # homeassistant.components.dominos pizzapi==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 25270998ab4..1b392014a50 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -835,7 +835,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.1.0 +pillow==9.1.1 # homeassistant.components.plex plexapi==4.11.1 From 52808562ab236ada838b94e4f17cad420ffc55aa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 23 May 2022 21:32:03 +0200 Subject: [PATCH 0826/3516] Improve DEVICE_TRIGGERS typing in tasmota (#72149) * Improve DEVICE_TRIGGERS typing in tasmota * fix CI * Simplify * Simplify some more --- .../components/tasmota/device_trigger.py | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py index 26b1d9debf1..69cb3a0bd72 100644 --- a/homeassistant/components/tasmota/device_trigger.py +++ b/homeassistant/components/tasmota/device_trigger.py @@ -188,11 +188,13 @@ async def async_setup_trigger( _LOGGER.debug( "Got update for trigger with hash: %s '%s'", discovery_hash, trigger_config ) + device_triggers: dict[str, Trigger] = hass.data[DEVICE_TRIGGERS] if not trigger_config.is_active: # Empty trigger_config: Remove trigger _LOGGER.debug("Removing trigger: %s", discovery_hash) - if discovery_id in hass.data[DEVICE_TRIGGERS]: - device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id] + if discovery_id in device_triggers: + device_trigger = device_triggers[discovery_id] + assert device_trigger.tasmota_trigger await device_trigger.tasmota_trigger.unsubscribe_topics() device_trigger.detach_trigger() clear_discovery_hash(hass, discovery_hash) @@ -200,7 +202,8 @@ async def async_setup_trigger( remove_update_signal() return - device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id] + device_trigger = device_triggers[discovery_id] + assert device_trigger.tasmota_trigger if device_trigger.tasmota_trigger.config_same(trigger_config): # Unchanged payload: Ignore to avoid unnecessary unsubscribe / subscribe _LOGGER.debug("Ignoring unchanged update for: %s", discovery_hash) @@ -209,6 +212,7 @@ async def async_setup_trigger( # Non-empty, changed trigger_config: Update trigger _LOGGER.debug("Updating trigger: %s", discovery_hash) device_trigger.tasmota_trigger.config_update(trigger_config) + assert remove_update_signal await device_trigger.update_tasmota_trigger( trigger_config, remove_update_signal ) @@ -230,7 +234,8 @@ async def async_setup_trigger( if DEVICE_TRIGGERS not in hass.data: hass.data[DEVICE_TRIGGERS] = {} - if discovery_id not in hass.data[DEVICE_TRIGGERS]: + device_triggers: dict[str, Trigger] = hass.data[DEVICE_TRIGGERS] + if discovery_id not in device_triggers: device_trigger = Trigger( hass=hass, device_id=device.id, @@ -240,10 +245,10 @@ async def async_setup_trigger( type=tasmota_trigger.cfg.type, remove_update_signal=remove_update_signal, ) - hass.data[DEVICE_TRIGGERS][discovery_id] = device_trigger + device_triggers[discovery_id] = device_trigger else: # This Tasmota trigger is wanted by device trigger(s), set them up - device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id] + device_trigger = device_triggers[discovery_id] await device_trigger.set_tasmota_trigger(tasmota_trigger, remove_update_signal) await device_trigger.arm_tasmota_trigger() @@ -251,14 +256,20 @@ async def async_setup_trigger( async def async_remove_triggers(hass: HomeAssistant, device_id: str) -> None: """Cleanup any device triggers for a Tasmota device.""" triggers = await async_get_triggers(hass, device_id) + + if not triggers: + return + device_triggers: dict[str, Trigger] = hass.data[DEVICE_TRIGGERS] for trig in triggers: - device_trigger = hass.data[DEVICE_TRIGGERS].pop(trig[CONF_DISCOVERY_ID]) + device_trigger = device_triggers.pop(trig[CONF_DISCOVERY_ID]) if device_trigger: discovery_hash = device_trigger.discovery_hash + assert device_trigger.tasmota_trigger await device_trigger.tasmota_trigger.unsubscribe_topics() device_trigger.detach_trigger() clear_discovery_hash(hass, discovery_hash) + assert device_trigger.remove_update_signal device_trigger.remove_update_signal() @@ -271,7 +282,8 @@ async def async_get_triggers( if DEVICE_TRIGGERS not in hass.data: return triggers - for discovery_id, trig in hass.data[DEVICE_TRIGGERS].items(): + device_triggers: dict[str, Trigger] = hass.data[DEVICE_TRIGGERS] + for discovery_id, trig in device_triggers.items(): if trig.device_id != device_id or trig.tasmota_trigger is None: continue @@ -297,12 +309,13 @@ async def async_attach_trigger( """Attach a device trigger.""" if DEVICE_TRIGGERS not in hass.data: hass.data[DEVICE_TRIGGERS] = {} + device_triggers: dict[str, Trigger] = hass.data[DEVICE_TRIGGERS] device_id = config[CONF_DEVICE_ID] discovery_id = config[CONF_DISCOVERY_ID] - if discovery_id not in hass.data[DEVICE_TRIGGERS]: + if discovery_id not in device_triggers: # The trigger has not (yet) been discovered, prepare it for later - hass.data[DEVICE_TRIGGERS][discovery_id] = Trigger( + device_triggers[discovery_id] = Trigger( hass=hass, device_id=device_id, discovery_hash=None, @@ -311,5 +324,5 @@ async def async_attach_trigger( subtype=config[CONF_SUBTYPE], tasmota_trigger=None, ) - trigger: Trigger = hass.data[DEVICE_TRIGGERS][discovery_id] + trigger: Trigger = device_triggers[discovery_id] return await trigger.add_trigger(action, automation_info) From 9d95b9ab053b2106dc99f58805a2bfb862a2fb93 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 May 2022 17:40:00 -0500 Subject: [PATCH 0827/3516] Chunk large logbook queries and add an end_time to the api so we stop sending events (#72351) --- homeassistant/components/logbook/models.py | 2 +- .../components/logbook/websocket_api.py | 206 +++++++++-- .../components/logbook/test_websocket_api.py | 340 ++++++++++++++++-- 3 files changed, 483 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/logbook/models.py b/homeassistant/components/logbook/models.py index e4844751c6a..591781745f7 100644 --- a/homeassistant/components/logbook/models.py +++ b/homeassistant/components/logbook/models.py @@ -59,7 +59,7 @@ class LazyEventPartialState: ) -@dataclass +@dataclass(frozen=True) class EventAsRow: """Convert an event to a row.""" diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index efb15efe97b..edfe3177f26 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable +from dataclasses import dataclass from datetime import datetime as dt, timedelta import logging from typing import Any @@ -15,6 +16,7 @@ from homeassistant.components.websocket_api import messages from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.components.websocket_api.const import JSON_DUMP from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.dt as dt_util from .helpers import ( @@ -26,12 +28,26 @@ from .models import async_event_to_row from .processor import EventProcessor MAX_PENDING_LOGBOOK_EVENTS = 2048 -EVENT_COALESCE_TIME = 0.5 +EVENT_COALESCE_TIME = 0.35 MAX_RECORDER_WAIT = 10 +# minimum size that we will split the query +BIG_QUERY_HOURS = 25 +# how many hours to deliver in the first chunk when we split the query +BIG_QUERY_RECENT_HOURS = 24 _LOGGER = logging.getLogger(__name__) +@dataclass +class LogbookLiveStream: + """Track a logbook live stream.""" + + stream_queue: asyncio.Queue[Event] + subscriptions: list[CALLBACK_TYPE] + end_time_unsub: CALLBACK_TYPE | None = None + task: asyncio.Task | None = None + + @callback def async_setup(hass: HomeAssistant) -> None: """Set up the logbook websocket API.""" @@ -39,6 +55,94 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_event_stream) +async def _async_wait_for_recorder_sync(hass: HomeAssistant) -> None: + """Wait for the recorder to sync.""" + try: + await asyncio.wait_for( + get_instance(hass).async_block_till_done(), MAX_RECORDER_WAIT + ) + except asyncio.TimeoutError: + _LOGGER.debug( + "Recorder is behind more than %s seconds, starting live stream; Some results may be missing" + ) + + +async def _async_send_historical_events( + hass: HomeAssistant, + connection: ActiveConnection, + msg_id: int, + start_time: dt, + end_time: dt, + formatter: Callable[[int, Any], dict[str, Any]], + event_processor: EventProcessor, +) -> dt | None: + """Select historical data from the database and deliver it to the websocket. + + If the query is considered a big query we will split the request into + two chunks so that they get the recent events first and the select + that is expected to take a long time comes in after to ensure + they are not stuck at a loading screen and can start looking at + the data right away. + + This function returns the time of the most recent event we sent to the + websocket. + """ + is_big_query = ( + not event_processor.entity_ids + and not event_processor.device_ids + and ((end_time - start_time) > timedelta(hours=BIG_QUERY_HOURS)) + ) + + if not is_big_query: + message, last_event_time = await _async_get_ws_formatted_events( + hass, + msg_id, + start_time, + end_time, + formatter, + event_processor, + ) + # If there is no last_time, there are no historical + # results, but we still send an empty message so + # consumers of the api know their request was + # answered but there were no results + connection.send_message(message) + return last_event_time + + # This is a big query so we deliver + # the first three hours and then + # we fetch the old data + recent_query_start = end_time - timedelta(hours=BIG_QUERY_RECENT_HOURS) + recent_message, recent_query_last_event_time = await _async_get_ws_formatted_events( + hass, + msg_id, + recent_query_start, + end_time, + formatter, + event_processor, + ) + if recent_query_last_event_time: + connection.send_message(recent_message) + + older_message, older_query_last_event_time = await _async_get_ws_formatted_events( + hass, + msg_id, + start_time, + recent_query_start, + formatter, + event_processor, + ) + # If there is no last_time, there are no historical + # results, but we still send an empty message so + # consumers of the api know their request was + # answered but there were no results + if older_query_last_event_time or not recent_query_last_event_time: + connection.send_message(older_message) + + # Returns the time of the newest event + return recent_query_last_event_time or older_query_last_event_time + + async def _async_get_ws_formatted_events( hass: HomeAssistant, msg_id: int, @@ -75,14 +179,13 @@ def _ws_formatted_get_events( async def _async_events_consumer( - setup_complete_future: asyncio.Future[dt], + subscriptions_setup_complete_time: dt, connection: ActiveConnection, msg_id: int, stream_queue: asyncio.Queue[Event], event_processor: EventProcessor, ) -> None: """Stream events from the queue.""" - subscriptions_setup_complete_time = await setup_complete_future event_processor.switch_to_live() while True: @@ -116,6 +219,7 @@ async def _async_events_consumer( { vol.Required("type"): "logbook/event_stream", vol.Required("start_time"): str, + vol.Optional("end_time"): str, vol.Optional("entity_ids"): [str], vol.Optional("device_ids"): [str], } @@ -126,21 +230,32 @@ async def ws_event_stream( ) -> None: """Handle logbook stream events websocket command.""" start_time_str = msg["start_time"] + msg_id: int = msg["id"] utc_now = dt_util.utcnow() if start_time := dt_util.parse_datetime(start_time_str): start_time = dt_util.as_utc(start_time) if not start_time or start_time > utc_now: - connection.send_error(msg["id"], "invalid_start_time", "Invalid start_time") + connection.send_error(msg_id, "invalid_start_time", "Invalid start_time") return + end_time_str = msg.get("end_time") + end_time: dt | None = None + if end_time_str: + if not (end_time := dt_util.parse_datetime(end_time_str)): + connection.send_error(msg_id, "invalid_end_time", "Invalid end_time") + return + end_time = dt_util.as_utc(end_time) + if end_time < start_time: + connection.send_error(msg_id, "invalid_end_time", "Invalid end_time") + return + device_ids = msg.get("device_ids") entity_ids = msg.get("entity_ids") if entity_ids: entity_ids = async_filter_entities(hass, entity_ids) event_types = async_determine_event_types(hass, entity_ids, device_ids) - event_processor = EventProcessor( hass, event_types, @@ -151,26 +266,43 @@ async def ws_event_stream( include_entity_name=False, ) - stream_queue: asyncio.Queue[Event] = asyncio.Queue(MAX_PENDING_LOGBOOK_EVENTS) - subscriptions: list[CALLBACK_TYPE] = [] - setup_complete_future: asyncio.Future[dt] = asyncio.Future() - task = asyncio.create_task( - _async_events_consumer( - setup_complete_future, + if end_time and end_time <= utc_now: + # Not live stream but we it might be a big query + connection.subscriptions[msg_id] = callback(lambda: None) + connection.send_result(msg_id) + # Fetch everything from history + await _async_send_historical_events( + hass, connection, - msg["id"], - stream_queue, + msg_id, + start_time, + end_time, + messages.event_message, event_processor, ) + return + + subscriptions: list[CALLBACK_TYPE] = [] + stream_queue: asyncio.Queue[Event] = asyncio.Queue(MAX_PENDING_LOGBOOK_EVENTS) + live_stream = LogbookLiveStream( + subscriptions=subscriptions, stream_queue=stream_queue ) - def _unsub() -> None: + @callback + def _unsub(*time: Any) -> None: """Unsubscribe from all events.""" for subscription in subscriptions: subscription() subscriptions.clear() - if task: - task.cancel() + if live_stream.task: + live_stream.task.cancel() + if live_stream.end_time_unsub: + live_stream.end_time_unsub() + + if end_time: + live_stream.end_time_unsub = async_track_point_in_utc_time( + hass, _unsub, end_time + ) @callback def _queue_or_cancel(event: Event) -> None: @@ -188,33 +320,21 @@ async def ws_event_stream( hass, subscriptions, _queue_or_cancel, event_types, entity_ids, device_ids ) subscriptions_setup_complete_time = dt_util.utcnow() - connection.subscriptions[msg["id"]] = _unsub - connection.send_result(msg["id"]) - + connection.subscriptions[msg_id] = _unsub + connection.send_result(msg_id) # Fetch everything from history - message, last_event_time = await _async_get_ws_formatted_events( + last_event_time = await _async_send_historical_events( hass, - msg["id"], + connection, + msg_id, start_time, subscriptions_setup_complete_time, messages.event_message, event_processor, ) - # If there is no last_time there are no historical - # results, but we still send an empty message so - # consumers of the api know their request was - # answered but there were no results - connection.send_message(message) - try: - await asyncio.wait_for( - get_instance(hass).async_block_till_done(), MAX_RECORDER_WAIT - ) - except asyncio.TimeoutError: - _LOGGER.debug( - "Recorder is behind more than %s seconds, starting live stream; Some results may be missing" - ) - if setup_complete_future.cancelled(): + await _async_wait_for_recorder_sync(hass) + if not subscriptions: # Unsubscribe happened while waiting for recorder return @@ -235,7 +355,7 @@ async def ws_event_stream( ) message, final_cutoff_time = await _async_get_ws_formatted_events( hass, - msg["id"], + msg_id, second_fetch_start_time, subscriptions_setup_complete_time, messages.event_message, @@ -244,9 +364,19 @@ async def ws_event_stream( if final_cutoff_time: # Only sends results if we have them connection.send_message(message) - if not setup_complete_future.cancelled(): + if not subscriptions: # Unsubscribe happened while waiting for formatted events - setup_complete_future.set_result(subscriptions_setup_complete_time) + return + + live_stream.task = asyncio.create_task( + _async_events_consumer( + subscriptions_setup_complete_time, + connection, + msg_id, + stream_queue, + event_processor, + ) + ) @websocket_api.websocket_command( diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index e1fc1defe09..26ad7127296 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -4,6 +4,7 @@ from collections.abc import Callable from datetime import timedelta from unittest.mock import ANY, patch +from freezegun import freeze_time import pytest from homeassistant import core @@ -26,7 +27,11 @@ from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import MockConfigEntry, SetupRecorderInstanceT +from tests.common import ( + MockConfigEntry, + SetupRecorderInstanceT, + async_fire_time_changed, +) from tests.components.recorder.common import ( async_block_recorder, async_recorder_block_till_done, @@ -479,12 +484,12 @@ async def test_subscribe_unsubscribe_logbook_stream( {"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()} ) - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == TYPE_RESULT assert msg["success"] - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" assert msg["event"] == [ @@ -512,7 +517,7 @@ async def test_subscribe_unsubscribe_logbook_stream( hass.states.async_set("light.zulu", "on", {"effect": "help", "color": "blue"}) - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" assert msg["event"] == [ @@ -552,7 +557,7 @@ async def test_subscribe_unsubscribe_logbook_stream( hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" assert msg["event"] == [ @@ -616,7 +621,7 @@ async def test_subscribe_unsubscribe_logbook_stream( await hass.async_block_till_done() - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" assert msg["event"] == [ @@ -733,7 +738,7 @@ async def test_subscribe_unsubscribe_logbook_stream( await websocket_client.send_json( {"id": 8, "type": "unsubscribe_events", "subscription": 7} ) - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 8 assert msg["type"] == TYPE_RESULT @@ -775,12 +780,12 @@ async def test_subscribe_unsubscribe_logbook_stream_entities( } ) - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == TYPE_RESULT assert msg["success"] - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" assert msg["event"] == [ @@ -797,7 +802,7 @@ async def test_subscribe_unsubscribe_logbook_stream_entities( await hass.async_block_till_done() - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" assert msg["event"] == [ @@ -815,7 +820,258 @@ async def test_subscribe_unsubscribe_logbook_stream_entities( await websocket_client.send_json( {"id": 8, "type": "unsubscribe_events", "subscription": 7} ) - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_unsubscribe_logbook_stream_entities_with_end_time( + hass, recorder_mock, hass_ws_client +): + """Test subscribe/unsubscribe logbook stream with specific entities and an end_time.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + + await hass.async_block_till_done() + init_count = sum(hass.bus.async_listeners().values()) + hass.states.async_set("light.small", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + state: State = hass.states.get("binary_sensor.is_light") + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "end_time": (now + timedelta(minutes=10)).isoformat(), + "entity_ids": ["light.small", "binary_sensor.is_light"], + } + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == [ + { + "entity_id": "binary_sensor.is_light", + "state": "off", + "when": state.last_updated.timestamp(), + } + ] + + hass.states.async_set("light.alpha", STATE_ON) + hass.states.async_set("light.alpha", STATE_OFF) + hass.states.async_set("light.small", STATE_OFF, {"effect": "help", "color": "blue"}) + + await hass.async_block_till_done() + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == [ + { + "entity_id": "light.small", + "state": "off", + "when": ANY, + }, + ] + + hass.states.async_remove("light.alpha") + hass.states.async_remove("light.small") + await hass.async_block_till_done() + + async_fire_time_changed(hass, now + timedelta(minutes=11)) + await hass.async_block_till_done() + + # These states should not be sent since we should be unsubscribed + hass.states.async_set("light.small", STATE_ON) + hass.states.async_set("light.small", STATE_OFF) + await hass.async_block_till_done() + + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) <= init_count + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_unsubscribe_logbook_stream_entities_past_only( + hass, recorder_mock, hass_ws_client +): + """Test subscribe/unsubscribe logbook stream with specific entities in the past.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + + await hass.async_block_till_done() + init_count = sum(hass.bus.async_listeners().values()) + hass.states.async_set("light.small", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + state: State = hass.states.get("binary_sensor.is_light") + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "end_time": (dt_util.utcnow() - timedelta(microseconds=1)).isoformat(), + "entity_ids": ["light.small", "binary_sensor.is_light"], + } + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == [ + { + "entity_id": "binary_sensor.is_light", + "state": "off", + "when": state.last_updated.timestamp(), + } + ] + + # These states should not be sent since we should be unsubscribed + # since we only asked for the past + hass.states.async_set("light.small", STATE_ON) + hass.states.async_set("light.small", STATE_OFF) + await hass.async_block_till_done() + + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_unsubscribe_logbook_stream_big_query( + hass, recorder_mock, hass_ws_client +): + """Test subscribe/unsubscribe logbook stream and ask for a large time frame. + + We should get the data for the first 24 hours in the first message, and + anything older will come in a followup message. + """ + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + + await hass.async_block_till_done() + init_count = sum(hass.bus.async_listeners().values()) + four_days_ago = now - timedelta(days=4) + five_days_ago = now - timedelta(days=5) + + with freeze_time(four_days_ago): + hass.states.async_set("binary_sensor.four_days_ago", STATE_ON) + hass.states.async_set("binary_sensor.four_days_ago", STATE_OFF) + four_day_old_state: State = hass.states.get("binary_sensor.four_days_ago") + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + # Verify our state was recorded in the past + assert (now - four_day_old_state.last_updated).total_seconds() > 86400 * 3 + + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + hass.states.async_set("binary_sensor.is_light", STATE_ON) + current_state: State = hass.states.get("binary_sensor.is_light") + + # Verify our new state was recorded in the recent timeframe + assert (now - current_state.last_updated).total_seconds() < 2 + + await async_wait_recording_done(hass) + + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": five_days_ago.isoformat(), + } + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # With a big query we get the current state first + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == [ + { + "entity_id": "binary_sensor.is_light", + "state": "on", + "when": current_state.last_updated.timestamp(), + } + ] + + # With a big query we get the old states second + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"] == [ + { + "entity_id": "binary_sensor.four_days_ago", + "state": "off", + "when": four_day_old_state.last_updated.timestamp(), + } + ] + + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 8 assert msg["type"] == TYPE_RESULT @@ -853,7 +1109,7 @@ async def test_subscribe_unsubscribe_logbook_stream_device( } ) - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == TYPE_RESULT assert msg["success"] @@ -864,7 +1120,7 @@ async def test_subscribe_unsubscribe_logbook_stream_device( # and its not a failure case. This is useful # in the frontend so we can tell the user there # are no results vs waiting for them to appear - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" assert msg["event"] == [] @@ -872,7 +1128,7 @@ async def test_subscribe_unsubscribe_logbook_stream_device( hass.bus.async_fire("mock_event", {"device_id": device.id}) await hass.async_block_till_done() - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" assert msg["event"] == [ @@ -882,7 +1138,7 @@ async def test_subscribe_unsubscribe_logbook_stream_device( await websocket_client.send_json( {"id": 8, "type": "unsubscribe_events", "subscription": 7} ) - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 8 assert msg["type"] == TYPE_RESULT @@ -910,6 +1166,38 @@ async def test_event_stream_bad_start_time(hass, hass_ws_client, recorder_mock): assert response["error"]["code"] == "invalid_start_time" +async def test_event_stream_bad_end_time(hass, hass_ws_client, recorder_mock): + """Test event_stream bad end time.""" + await async_setup_component(hass, "logbook", {}) + await async_recorder_block_till_done(hass) + utc_now = dt_util.utcnow() + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "logbook/event_stream", + "start_time": utc_now.isoformat(), + "end_time": "cats", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_end_time" + + await client.send_json( + { + "id": 2, + "type": "logbook/event_stream", + "start_time": utc_now.isoformat(), + "end_time": (utc_now - timedelta(hours=5)).isoformat(), + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_end_time" + + async def test_live_stream_with_one_second_commit_interval( hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT, @@ -951,7 +1239,7 @@ async def test_live_stream_with_one_second_commit_interval( ) hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "4"}) - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == TYPE_RESULT assert msg["success"] @@ -959,7 +1247,7 @@ async def test_live_stream_with_one_second_commit_interval( hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "5"}) recieved_rows = [] - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" recieved_rows.extend(msg["event"]) @@ -990,7 +1278,7 @@ async def test_live_stream_with_one_second_commit_interval( await websocket_client.send_json( {"id": 8, "type": "unsubscribe_events", "subscription": 7} ) - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 8 assert msg["type"] == TYPE_RESULT @@ -1030,12 +1318,12 @@ async def test_subscribe_disconnected(hass, recorder_mock, hass_ws_client): } ) - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == TYPE_RESULT assert msg["success"] - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" assert msg["event"] == [ @@ -1088,7 +1376,7 @@ async def test_stream_consumer_stop_processing(hass, recorder_mock, hass_ws_clie ) await async_wait_recording_done(hass) - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == TYPE_RESULT assert msg["success"] @@ -1135,7 +1423,7 @@ async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplo } ) - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == TYPE_RESULT assert msg["success"] @@ -1146,7 +1434,7 @@ async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplo # and its not a failure case. This is useful # in the frontend so we can tell the user there # are no results vs waiting for them to appear - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" assert msg["event"] == [] @@ -1154,7 +1442,7 @@ async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplo hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "1"}) await hass.async_block_till_done() - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" assert msg["event"] == [ @@ -1164,7 +1452,7 @@ async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplo hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "2"}) await hass.async_block_till_done() - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" assert msg["event"] == [ @@ -1174,7 +1462,7 @@ async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplo await websocket_client.send_json( {"id": 8, "type": "unsubscribe_events", "subscription": 7} ) - msg = await websocket_client.receive_json() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 8 assert msg["type"] == TYPE_RESULT From 1eaf3bb817fb6bd0649084fe91fd72ef77b77b4e Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Mon, 23 May 2022 19:01:07 -0500 Subject: [PATCH 0828/3516] Bump Frontend to 20220523.0 (#72397) --- 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 215796d2df8..f6ede0676ca 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220521.0"], + "requirements": ["home-assistant-frontend==20220523.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e4bed71458f..a281d3cb30d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220521.0 +home-assistant-frontend==20220523.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 12bd9a705b2..bf4f2d556ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -822,7 +822,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220521.0 +home-assistant-frontend==20220523.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1b392014a50..f735b555859 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -589,7 +589,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220521.0 +home-assistant-frontend==20220523.0 # homeassistant.components.home_connect homeconnect==0.7.0 From abbfed24a4fb08de12d83cfe431b0f723938ae38 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 24 May 2022 00:25:27 +0000 Subject: [PATCH 0829/3516] [ci skip] Translation update --- .../components/baf/translations/ru.json | 23 ++++++ .../components/google/translations/de.json | 9 ++ .../components/google/translations/et.json | 9 ++ .../components/google/translations/fr.json | 9 ++ .../components/google/translations/id.json | 9 ++ .../components/google/translations/ja.json | 9 ++ .../components/google/translations/nl.json | 9 ++ .../components/google/translations/no.json | 9 ++ .../components/google/translations/ru.json | 9 ++ .../google/translations/zh-Hant.json | 9 ++ .../google_travel_time/translations/ja.json | 2 +- .../components/hassio/translations/de.json | 1 + .../components/hassio/translations/en.json | 1 + .../components/hassio/translations/fr.json | 1 + .../components/hassio/translations/id.json | 1 + .../components/hassio/translations/nl.json | 1 + .../components/hassio/translations/pt-BR.json | 1 + .../here_travel_time/translations/et.json | 82 +++++++++++++++++++ .../here_travel_time/translations/ja.json | 82 +++++++++++++++++++ .../here_travel_time/translations/no.json | 82 +++++++++++++++++++ .../here_travel_time/translations/ru.json | 82 +++++++++++++++++++ .../components/konnected/translations/et.json | 16 ++-- .../components/konnected/translations/id.json | 12 +-- .../components/konnected/translations/no.json | 16 ++-- .../components/konnected/translations/ru.json | 16 ++-- .../components/laundrify/translations/et.json | 25 ++++++ .../components/laundrify/translations/ja.json | 25 ++++++ .../components/laundrify/translations/no.json | 25 ++++++ .../components/laundrify/translations/ru.json | 25 ++++++ .../components/recorder/translations/de.json | 2 + .../components/recorder/translations/et.json | 2 + .../components/recorder/translations/fr.json | 2 + .../components/recorder/translations/id.json | 2 + .../components/recorder/translations/it.json | 2 + .../components/recorder/translations/ja.json | 2 + .../components/recorder/translations/nl.json | 2 + .../components/recorder/translations/no.json | 2 + .../recorder/translations/pt-BR.json | 2 + .../components/recorder/translations/ru.json | 2 + .../recorder/translations/zh-Hant.json | 2 + .../components/shelly/translations/ru.json | 1 + .../components/siren/translations/ru.json | 3 + .../steam_online/translations/de.json | 3 + .../steam_online/translations/en.json | 6 +- .../steam_online/translations/fr.json | 3 + .../steam_online/translations/id.json | 3 + .../steam_online/translations/nl.json | 3 + .../steam_online/translations/pt-BR.json | 3 + .../waze_travel_time/translations/ja.json | 2 +- .../components/yolink/translations/ru.json | 25 ++++++ 50 files changed, 639 insertions(+), 35 deletions(-) create mode 100644 homeassistant/components/baf/translations/ru.json create mode 100644 homeassistant/components/here_travel_time/translations/et.json create mode 100644 homeassistant/components/here_travel_time/translations/ja.json create mode 100644 homeassistant/components/here_travel_time/translations/no.json create mode 100644 homeassistant/components/here_travel_time/translations/ru.json create mode 100644 homeassistant/components/laundrify/translations/et.json create mode 100644 homeassistant/components/laundrify/translations/ja.json create mode 100644 homeassistant/components/laundrify/translations/no.json create mode 100644 homeassistant/components/laundrify/translations/ru.json create mode 100644 homeassistant/components/siren/translations/ru.json create mode 100644 homeassistant/components/yolink/translations/ru.json diff --git a/homeassistant/components/baf/translations/ru.json b/homeassistant/components/baf/translations/ru.json new file mode 100644 index 00000000000..e2e5d43123e --- /dev/null +++ b/homeassistant/components/baf/translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "ipv6_not_supported": "IPv6 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{name} - {model} ({ip_address})", + "step": { + "discovery_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} - {model} ({ip_address})?" + }, + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/de.json b/homeassistant/components/google/translations/de.json index 1b15c465497..c75cc90eb13 100644 --- a/homeassistant/components/google/translations/de.json +++ b/homeassistant/components/google/translations/de.json @@ -27,5 +27,14 @@ "title": "Integration erneut authentifizieren" } } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Home Assistant-Zugriff auf Google Kalender" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/et.json b/homeassistant/components/google/translations/et.json index 27dfa3f5290..a115378f3a2 100644 --- a/homeassistant/components/google/translations/et.json +++ b/homeassistant/components/google/translations/et.json @@ -27,5 +27,14 @@ "title": "Taastuvasta sidumine" } } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Home Assistanti juurdep\u00e4\u00e4s Google'i kalendrile" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/fr.json b/homeassistant/components/google/translations/fr.json index 1224ba76c9b..06903bdff07 100644 --- a/homeassistant/components/google/translations/fr.json +++ b/homeassistant/components/google/translations/fr.json @@ -27,5 +27,14 @@ "title": "R\u00e9-authentifier l'int\u00e9gration" } } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Acc\u00e8s de Home Assistant \u00e0 Google Agenda" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/id.json b/homeassistant/components/google/translations/id.json index fd1002d41be..25c3e9fa1e6 100644 --- a/homeassistant/components/google/translations/id.json +++ b/homeassistant/components/google/translations/id.json @@ -27,5 +27,14 @@ "title": "Autentikasi Ulang Integrasi" } } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Akses Home Assistant ke Google Kalender" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/ja.json b/homeassistant/components/google/translations/ja.json index 81f2d941a73..854e7ba1961 100644 --- a/homeassistant/components/google/translations/ja.json +++ b/homeassistant/components/google/translations/ja.json @@ -27,5 +27,14 @@ "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" } } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Home Assistant\u304b\u3089\u3001Google\u30ab\u30ec\u30f3\u30c0\u30fc\u3078\u306e\u30a2\u30af\u30bb\u30b9" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/nl.json b/homeassistant/components/google/translations/nl.json index baf0064ee63..419259d66f8 100644 --- a/homeassistant/components/google/translations/nl.json +++ b/homeassistant/components/google/translations/nl.json @@ -27,5 +27,14 @@ "title": "Integratie herauthenticeren" } } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Home Assistant-toegang tot Google Agenda" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/no.json b/homeassistant/components/google/translations/no.json index 4065583192c..ef65c7fe9a5 100644 --- a/homeassistant/components/google/translations/no.json +++ b/homeassistant/components/google/translations/no.json @@ -27,5 +27,14 @@ "title": "Godkjenne integrering p\u00e5 nytt" } } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Home Assistant tilgang til Google Kalender" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/ru.json b/homeassistant/components/google/translations/ru.json index a7db4b2f48b..51badc7226d 100644 --- a/homeassistant/components/google/translations/ru.json +++ b/homeassistant/components/google/translations/ru.json @@ -27,5 +27,14 @@ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" } } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "\u0414\u043e\u0441\u0442\u0443\u043f Home Assistant \u043a Google \u041a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u044e" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/zh-Hant.json b/homeassistant/components/google/translations/zh-Hant.json index 70e7d81c01e..988c6629af7 100644 --- a/homeassistant/components/google/translations/zh-Hant.json +++ b/homeassistant/components/google/translations/zh-Hant.json @@ -27,5 +27,14 @@ "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" } } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Home Assistant \u5b58\u53d6 Google Calendar" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/ja.json b/homeassistant/components/google_travel_time/translations/ja.json index 2fb8ae2883c..002a9cdd3b0 100644 --- a/homeassistant/components/google_travel_time/translations/ja.json +++ b/homeassistant/components/google_travel_time/translations/ja.json @@ -12,7 +12,7 @@ "api_key": "API\u30ad\u30fc", "destination": "\u76ee\u7684\u5730", "name": "\u540d\u524d", - "origin": "\u30aa\u30ea\u30b8\u30f3" + "origin": "\u539f\u70b9(Origin)" }, "description": "\u51fa\u767a\u5730\u3068\u76ee\u7684\u5730\u3092\u6307\u5b9a\u3059\u308b\u5834\u5408\u3001\u4f4f\u6240\u3001\u7def\u5ea6/\u7d4c\u5ea6\u306e\u5ea7\u6a19\u3001\u307e\u305f\u306fGoogle place ID\u306e\u5f62\u5f0f\u3067\u3001\u30d1\u30a4\u30d7\u6587\u5b57\u3067\u533a\u5207\u3089\u308c\u305f1\u3064\u4ee5\u4e0a\u306e\u5834\u6240\u3092\u6307\u5b9a\u3067\u304d\u307e\u3059\u3002Google place ID\u3092\u4f7f\u7528\u3057\u3066\u5834\u6240\u3092\u6307\u5b9a\u3059\u308b\u5834\u5408\u3001ID\u306e\u524d\u306b\u3001`place_id:` \u3092\u4ed8\u3051\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" } diff --git a/homeassistant/components/hassio/translations/de.json b/homeassistant/components/hassio/translations/de.json index 99747512e97..f25ae73b423 100644 --- a/homeassistant/components/hassio/translations/de.json +++ b/homeassistant/components/hassio/translations/de.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "Agent-Version", "board": "Board", "disk_total": "Speicherplatz gesamt", "disk_used": "Speicherplatz genutzt", diff --git a/homeassistant/components/hassio/translations/en.json b/homeassistant/components/hassio/translations/en.json index bb5f8e6f01a..14d79f0d8d6 100644 --- a/homeassistant/components/hassio/translations/en.json +++ b/homeassistant/components/hassio/translations/en.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "Agent Version", "board": "Board", "disk_total": "Disk Total", "disk_used": "Disk Used", diff --git a/homeassistant/components/hassio/translations/fr.json b/homeassistant/components/hassio/translations/fr.json index 6e20e37a2b9..9a042b97e57 100644 --- a/homeassistant/components/hassio/translations/fr.json +++ b/homeassistant/components/hassio/translations/fr.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "Version de l'agent", "board": "Tableau de bord", "disk_total": "Taille total du disque", "disk_used": "Taille du disque utilis\u00e9", diff --git a/homeassistant/components/hassio/translations/id.json b/homeassistant/components/hassio/translations/id.json index b87e1b47c44..250e6e7d4ad 100644 --- a/homeassistant/components/hassio/translations/id.json +++ b/homeassistant/components/hassio/translations/id.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "Versi Agen", "board": "Board", "disk_total": "Total Disk", "disk_used": "Disk Digunakan", diff --git a/homeassistant/components/hassio/translations/nl.json b/homeassistant/components/hassio/translations/nl.json index e5541ff1d00..3a31897fd67 100644 --- a/homeassistant/components/hassio/translations/nl.json +++ b/homeassistant/components/hassio/translations/nl.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "Agent-versie", "board": "Bord", "disk_total": "Totale schijfruimte", "disk_used": "Gebruikte schijfruimte", diff --git a/homeassistant/components/hassio/translations/pt-BR.json b/homeassistant/components/hassio/translations/pt-BR.json index b157725600d..4f3e5d84ec1 100644 --- a/homeassistant/components/hassio/translations/pt-BR.json +++ b/homeassistant/components/hassio/translations/pt-BR.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "Vers\u00e3o do Agent", "board": "Borda", "disk_total": "Total do disco", "disk_used": "Disco usado", diff --git a/homeassistant/components/here_travel_time/translations/et.json b/homeassistant/components/here_travel_time/translations/et.json new file mode 100644 index 00000000000..c9646af7544 --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/et.json @@ -0,0 +1,82 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "destination_coordinates": { + "data": { + "destination": "Sihtkoha GPS koordinaadid" + }, + "title": "Vali sihtkoht" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "Sihtkoha valik olemist" + }, + "title": "Vali sihtkoht" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "Kasuta asukohta kaardil", + "destination_entity": "Kasuta olemit" + }, + "title": "Vali sihtkoht" + }, + "origin_coordinates": { + "data": { + "origin": "L\u00e4htekoha GPS asukoht" + }, + "title": "Vali l\u00e4htekoht" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "L\u00e4htekoha valik olemist" + }, + "title": "Vali l\u00e4htekoht" + }, + "user": { + "data": { + "api_key": "API v\u00f5ti", + "mode": "Vali reisimise viis", + "name": "Nimi" + } + } + } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "Saabumisaeg" + }, + "title": "Vali saabumisaeg" + }, + "departure_time": { + "data": { + "departure_time": "V\u00e4ljumisaeg" + }, + "title": "Vali v\u00e4ljumisaeg" + }, + "init": { + "data": { + "route_mode": "Teekonna valik", + "traffic_mode": "S\u00f5iduvahend", + "unit_system": "\u00dchikute s\u00fcsteem" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "Vali saabumisaeg", + "departure_time": "Vali v\u00e4ljumisaeg", + "no_time": "\u00c4ra seadista aega" + }, + "title": "Vali aja vorming" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/ja.json b/homeassistant/components/here_travel_time/translations/ja.json new file mode 100644 index 00000000000..6db262d829a --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/ja.json @@ -0,0 +1,82 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "destination_coordinates": { + "data": { + "destination": "GPS\u5ea7\u6a19\u3068\u3057\u3066\u306e\u76ee\u7684\u5730" + }, + "title": "\u76ee\u7684\u5730\u3092\u9078\u629e" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u4f7f\u7528\u3057\u305f\u76ee\u7684\u5730" + }, + "title": "\u76ee\u7684\u5730\u3092\u9078\u629e" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "\u5730\u56f3\u4e0a\u306e\u5834\u6240\u3092\u4f7f\u7528\u3059\u308b", + "destination_entity": "\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u4f7f\u7528\u3059\u308b" + }, + "title": "\u76ee\u7684\u5730\u3092\u9078\u629e" + }, + "origin_coordinates": { + "data": { + "origin": "GPS\u5ea7\u6a19\u3068\u3057\u3066\u306e\u539f\u70b9(Origin)" + }, + "title": "\u539f\u70b9(Origin)\u3092\u9078\u629e" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u4f7f\u7528\u3057\u305f\u539f\u70b9(Origin)" + }, + "title": "\u539f\u70b9(Origin)\u3092\u9078\u629e" + }, + "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "mode": "\u30c8\u30e9\u30d9\u30eb\u30e2\u30fc\u30c9", + "name": "\u540d\u524d" + } + } + } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "\u5230\u7740\u6642\u523b" + }, + "title": "\u5230\u7740\u6642\u9593\u3092\u9078\u629e" + }, + "departure_time": { + "data": { + "departure_time": "\u51fa\u767a\u6642\u523b" + }, + "title": "\u51fa\u767a\u6642\u523b\u3092\u9078\u629e" + }, + "init": { + "data": { + "route_mode": "\u30eb\u30fc\u30c8\u30e2\u30fc\u30c9", + "traffic_mode": "\u30c8\u30e9\u30d5\u30a3\u30c3\u30af\u30e2\u30fc\u30c9", + "unit_system": "\u5358\u4f4d\u30b7\u30b9\u30c6\u30e0" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "\u5230\u7740\u6642\u523b\u3092\u8a2d\u5b9a\u3059\u308b", + "departure_time": "\u51fa\u767a\u6642\u523b\u3092\u8a2d\u5b9a\u3059\u308b", + "no_time": "\u6642\u523b\u3092\u8a2d\u5b9a\u3057\u306a\u3044" + }, + "title": "\u6642\u9593\u306e\u7a2e\u985e\u3092\u9078\u629e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/no.json b/homeassistant/components/here_travel_time/translations/no.json new file mode 100644 index 00000000000..52d4477f379 --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/no.json @@ -0,0 +1,82 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "destination_coordinates": { + "data": { + "destination": "Destinasjon som GPS-koordinater" + }, + "title": "Velg Destinasjon" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "Destinasjon ved hjelp av en enhet" + }, + "title": "Velg Destinasjon" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "Bruke en kartplassering", + "destination_entity": "Bruke en enhet" + }, + "title": "Velg Destinasjon" + }, + "origin_coordinates": { + "data": { + "origin": "Opprinnelse som GPS-koordinater" + }, + "title": "Velg Opprinnelse" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "Opprinnelse ved hjelp av en enhet" + }, + "title": "Velg Opprinnelse" + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "mode": "Reisemodus", + "name": "Navn" + } + } + } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "Ankomsttid" + }, + "title": "Velg Ankomsttid" + }, + "departure_time": { + "data": { + "departure_time": "Avgangstid" + }, + "title": "Velg Avreisetid" + }, + "init": { + "data": { + "route_mode": "Rutemodus", + "traffic_mode": "Trafikkmodus", + "unit_system": "Enhetssystem" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "Konfigurer en ankomsttid", + "departure_time": "Konfigurer en avgangstid", + "no_time": "Ikke konfigurer en tid" + }, + "title": "Velg Tidstype" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/ru.json b/homeassistant/components/here_travel_time/translations/ru.json new file mode 100644 index 00000000000..fc649d920df --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/ru.json @@ -0,0 +1,82 @@ +{ + "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." + }, + "step": { + "destination_coordinates": { + "data": { + "destination": "\u041f\u0443\u043d\u043a\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0432 \u0432\u0438\u0434\u0435 GPS-\u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0443\u043d\u043a\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "\u041e\u0431\u044a\u0435\u043a\u0442 \u043f\u0443\u043d\u043a\u0442\u0430 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0443\u043d\u043a\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "\u0423\u043a\u0430\u0437\u0430\u0442\u044c \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u043a\u0430\u0440\u0442\u0435", + "destination_entity": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0443\u043d\u043a\u0442 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f" + }, + "origin_coordinates": { + "data": { + "origin": "\u041f\u0443\u043d\u043a\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0432 \u0432\u0438\u0434\u0435 GPS-\u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0443\u043d\u043a\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "\u041e\u0431\u044a\u0435\u043a\u0442 \u043f\u0443\u043d\u043a\u0442\u0430 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0443\u043d\u043a\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f" + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "mode": "\u0421\u043f\u043e\u0441\u043e\u0431 \u043f\u0435\u0440\u0435\u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + } + } + } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "\u0412\u0440\u0435\u043c\u044f \u043f\u0440\u0438\u0431\u044b\u0442\u0438\u044f" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u0438\u0431\u044b\u0442\u0438\u044f" + }, + "departure_time": { + "data": { + "departure_time": "\u0412\u0440\u0435\u043c\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u0440\u0435\u043c\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f" + }, + "init": { + "data": { + "route_mode": "\u0420\u0435\u0436\u0438\u043c \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0430", + "traffic_mode": "\u0420\u0435\u0436\u0438\u043c \u0442\u0440\u0430\u0444\u0438\u043a\u0430", + "unit_system": "\u0415\u0434\u0438\u043d\u0438\u0446\u044b \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u0438\u0431\u044b\u0442\u0438\u044f", + "departure_time": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f", + "no_time": "\u041d\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0442\u044c \u0432\u0440\u0435\u043c\u044f" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u0432\u0440\u0435\u043c\u0435\u043d\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/et.json b/homeassistant/components/konnected/translations/et.json index ddddea5c800..bbbb417bb9f 100644 --- a/homeassistant/components/konnected/translations/et.json +++ b/homeassistant/components/konnected/translations/et.json @@ -39,7 +39,7 @@ "options_binary": { "data": { "inverse": "Vaheta avatud / suletud olek", - "name": "Nimi (valikuline)", + "name": "Nimi", "type": "Binaaranduri t\u00fc\u00fcp" }, "description": "{zone} suvandid", @@ -47,8 +47,8 @@ }, "options_digital": { "data": { - "name": "Nimi (valikuline)", - "poll_interval": "P\u00e4ringute intervall (sekundites) (valikuline)", + "name": "Nimi", + "poll_interval": "P\u00e4ringute intervall (minutites)", "type": "Anduri t\u00fc\u00fcp" }, "description": "{zone} valikud", @@ -84,7 +84,7 @@ }, "options_misc": { "data": { - "api_host": "Alista API hosti URL (valikuline)", + "api_host": "Alista API hosti URL", "blink": "Vilguts paneeli LEDi oleku muudatuse saatmisel", "discovery": "V\u00f5rgus esitatud tuvastusp\u00e4ringutele vastamine", "override_api_host": "Alista Home Assistanti API hostipaneeli vaikimisi URL" @@ -95,11 +95,11 @@ "options_switch": { "data": { "activation": "V\u00e4ljund sissel\u00fclitatuna", - "momentary": "Impulsi kestus (ms) (valikuline)", + "momentary": "Impulsi kestus (ms)", "more_states": "Selle tsooni t\u00e4iendavate olekute konfigureerimine", - "name": "Nimi (valikuline)", - "pause": "Paus impulsside vahel (ms) (valikuline)", - "repeat": "Korduste arv (-1 = l\u00f5pmatu) (valikuline)" + "name": "Nimi", + "pause": "Paus impulsside vahel (ms)", + "repeat": "Korduste arv (-1 = l\u00f5pmatu)" }, "description": "{tsooni} suvandid: olek {state}", "title": "Seadista l\u00fclitatav v\u00e4ljund" diff --git a/homeassistant/components/konnected/translations/id.json b/homeassistant/components/konnected/translations/id.json index 38443f483ca..0e64cb85032 100644 --- a/homeassistant/components/konnected/translations/id.json +++ b/homeassistant/components/konnected/translations/id.json @@ -48,7 +48,7 @@ "options_digital": { "data": { "name": "Nama", - "poll_interval": "Interval Polling (dalam menit) (opsional)", + "poll_interval": "Interval Polling (menit)", "type": "Jenis Sensor" }, "description": "Opsi {zone}", @@ -84,7 +84,7 @@ }, "options_misc": { "data": { - "api_host": "Ganti URL host API (opsional)", + "api_host": "Timpa URL host API", "blink": "Kedipkan panel LED saat mengirim perubahan status", "discovery": "Tanggapi permintaan penemuan di jaringan Anda", "override_api_host": "Timpa URL panel host API Home Assistant bawaan" @@ -95,11 +95,11 @@ "options_switch": { "data": { "activation": "Keluaran saat nyala", - "momentary": "Durasi pulsa (milidetik) (opsional)", + "momentary": "Durasi pulsa (milidetik)", "more_states": "Konfigurasikan status tambahan untuk zona ini", - "name": "Nama (opsional)", - "pause": "Jeda di antara pulsa (milidetik) (opsional)", - "repeat": "Waktu pengulangan (-1 = tak terbatas) (opsional)" + "name": "Nama", + "pause": "Jeda di antara pulsa (milidetik)", + "repeat": "Waktu pengulangan (-1 = tak terbatas)" }, "description": "Opsi {zone}: status {state}", "title": "Konfigurasikan Output yang Dapat Dialihkan" diff --git a/homeassistant/components/konnected/translations/no.json b/homeassistant/components/konnected/translations/no.json index 92d3efc4633..af9251f7c39 100644 --- a/homeassistant/components/konnected/translations/no.json +++ b/homeassistant/components/konnected/translations/no.json @@ -39,7 +39,7 @@ "options_binary": { "data": { "inverse": "Inverter \u00e5pen / lukk tilstand", - "name": "Navn (valgfritt)", + "name": "Navn", "type": "Bin\u00e6r sensortype" }, "description": "{zone}-alternativer", @@ -47,8 +47,8 @@ }, "options_digital": { "data": { - "name": "Navn (valgfritt)", - "poll_interval": "Avstemningsintervall (minutter) (valgfritt)", + "name": "Navn", + "poll_interval": "Avstemningsintervall (minutter)", "type": "Sensortype" }, "description": "{zone}-alternativer", @@ -84,7 +84,7 @@ }, "options_misc": { "data": { - "api_host": "Overstyre API-vert-URL (valgfritt)", + "api_host": "Overstyr API-verts-URL", "blink": "Blink p\u00e5 LED-lampen n\u00e5r du sender statusendring", "discovery": "Svar p\u00e5 oppdagelsesforesp\u00f8rsler i nettverket ditt", "override_api_host": "Overstyre standard Home Assistant API-vertspanel-URL" @@ -95,11 +95,11 @@ "options_switch": { "data": { "activation": "Utgang n\u00e5r den er p\u00e5", - "momentary": "Pulsvarighet (ms) (valgfritt)", + "momentary": "Pulsvarighet (ms)", "more_states": "Konfigurere flere tilstander for denne sonen", - "name": "Navn (valgfritt)", - "pause": "Pause mellom pulser (ms) (valgfritt)", - "repeat": "Tider \u00e5 gjenta (-1 = uendelig) (valgfritt)" + "name": "Navn", + "pause": "Pause mellom pulser (ms)", + "repeat": "Tider \u00e5 gjenta (-1=uendelig)" }, "description": "{zone} alternativer: tilstand {state}", "title": "Konfigurer utskiftbar utgang" diff --git a/homeassistant/components/konnected/translations/ru.json b/homeassistant/components/konnected/translations/ru.json index 79cc78e6a9a..eb93b825d04 100644 --- a/homeassistant/components/konnected/translations/ru.json +++ b/homeassistant/components/konnected/translations/ru.json @@ -39,7 +39,7 @@ "options_binary": { "data": { "inverse": "\u0418\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u0435/\u0437\u0430\u043a\u0440\u044b\u0442\u043e\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "type": "\u0422\u0438\u043f \u0431\u0438\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430" }, "description": "\u041e\u043f\u0446\u0438\u0438 {zone}", @@ -47,8 +47,8 @@ }, "options_digital": { "data": { - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", - "poll_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 \u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "poll_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0440\u043e\u0441\u0430 (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)", "type": "\u0422\u0438\u043f \u0441\u0435\u043d\u0441\u043e\u0440\u0430" }, "description": "\u041e\u043f\u0446\u0438\u0438 {zone}", @@ -84,7 +84,7 @@ }, "options_misc": { "data": { - "api_host": "\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c URL \u0445\u043e\u0441\u0442\u0430 API (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "api_host": "\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c URL \u0445\u043e\u0441\u0442\u0430 API", "blink": "LED-\u0438\u043d\u0434\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 \u043f\u0440\u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", "discovery": "\u041e\u0442\u0432\u0435\u0447\u0430\u0442\u044c \u043d\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0432 \u0412\u0430\u0448\u0435\u0439 \u0441\u0435\u0442\u0438", "override_api_host": "\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442-\u043f\u0430\u043d\u0435\u043b\u0438 Home Assistant API" @@ -95,11 +95,11 @@ "options_switch": { "data": { "activation": "\u0412\u044b\u0445\u043e\u0434 \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", - "momentary": "\u0414\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u0438\u043c\u043f\u0443\u043b\u044c\u0441\u0430 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "momentary": "\u0414\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u0438\u043c\u043f\u0443\u043b\u044c\u0441\u0430 (\u043c\u0441)", "more_states": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u0437\u043e\u043d\u044b", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", - "pause": "\u041f\u0430\u0443\u0437\u0430 \u043c\u0435\u0436\u0434\u0443 \u0438\u043c\u043f\u0443\u043b\u044c\u0441\u0430\u043c\u0438 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", - "repeat": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u0435\u043d\u0438\u0439 (-1 = \u0431\u0435\u0441\u043a\u043e\u043d\u0435\u0447\u043d\u043e) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "pause": "\u041f\u0430\u0443\u0437\u0430 \u043c\u0435\u0436\u0434\u0443 \u0438\u043c\u043f\u0443\u043b\u044c\u0441\u0430\u043c\u0438 (\u043c\u0441)", + "repeat": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u0435\u043d\u0438\u0439 (-1 = \u0431\u0435\u0441\u043a\u043e\u043d\u0435\u0447\u043d\u043e)" }, "description": "{zone}: \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 {state}", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0435\u043c\u043e\u0433\u043e \u0432\u044b\u0445\u043e\u0434\u0430" diff --git a/homeassistant/components/laundrify/translations/et.json b/homeassistant/components/laundrify/translations/et.json new file mode 100644 index 00000000000..cec7d1ec849 --- /dev/null +++ b/homeassistant/components/laundrify/translations/et.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "invalid_format": "Kehtetu vorming. Sisesta kui xxx-xxx.", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "init": { + "data": { + "code": "Auth kood (xxx-xxx)" + }, + "description": "Sisesta isiklik autentimiskood, mis kuvatakse pesumasina rakenduses." + }, + "reauth_confirm": { + "description": "laundrify sidumine tuleb uuesti autentida.", + "title": "Taastuvasta sidumine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/ja.json b/homeassistant/components/laundrify/translations/ja.json new file mode 100644 index 00000000000..153fcf99afb --- /dev/null +++ b/homeassistant/components/laundrify/translations/ja.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_format": "\u7121\u52b9\u306a\u5f62\u5f0f\u3067\u3059\u3002xxx-xxx\u3068\u3057\u3066\u6307\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "init": { + "data": { + "code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9 (xxx-xxx)" + }, + "description": "Laundrify-App\u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308b\u3001\u3042\u306a\u305f\u306e\u500b\u4eba\u8a8d\u8a3c\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, + "reauth_confirm": { + "description": "Laundrify\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/no.json b/homeassistant/components/laundrify/translations/no.json new file mode 100644 index 00000000000..fc2a24414d1 --- /dev/null +++ b/homeassistant/components/laundrify/translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "invalid_format": "Ugyldig format. Vennligst spesifiser som xxx-xxx.", + "unknown": "Uventet feil" + }, + "step": { + "init": { + "data": { + "code": "Auth-kode (xxx-xxx)" + }, + "description": "Vennligst skriv inn din personlige autentiseringskode som vises i laundrify-appen." + }, + "reauth_confirm": { + "description": "Integrasjonen av vaskeri m\u00e5 autentiseres p\u00e5 nytt.", + "title": "Godkjenne integrering p\u00e5 nytt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/ru.json b/homeassistant/components/laundrify/translations/ru.json new file mode 100644 index 00000000000..cceb04e1601 --- /dev/null +++ b/homeassistant/components/laundrify/translations/ru.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_format": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442. \u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 \u0445\u0445\u0445-\u0445\u0445\u0445.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "init": { + "data": { + "code": "\u041a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 (xxx-xxx)" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043b\u0438\u0447\u043d\u044b\u0439 \u043a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044f \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 laundrify." + }, + "reauth_confirm": { + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 laundrify.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/de.json b/homeassistant/components/recorder/translations/de.json index 27ba9f1da85..b05adc3c386 100644 --- a/homeassistant/components/recorder/translations/de.json +++ b/homeassistant/components/recorder/translations/de.json @@ -2,6 +2,8 @@ "system_health": { "info": { "current_recorder_run": "Aktuelle Startzeit der Ausf\u00fchrung", + "database_engine": "Datenbank-Engine", + "database_version": "Datenbankversion", "estimated_db_size": "Gesch\u00e4tzte Datenbankgr\u00f6\u00dfe (MiB)", "oldest_recorder_run": "\u00c4lteste Startzeit der Ausf\u00fchrung" } diff --git a/homeassistant/components/recorder/translations/et.json b/homeassistant/components/recorder/translations/et.json index 0dc6ae3ec62..b76e0f2e4da 100644 --- a/homeassistant/components/recorder/translations/et.json +++ b/homeassistant/components/recorder/translations/et.json @@ -2,6 +2,8 @@ "system_health": { "info": { "current_recorder_run": "Praegune k\u00e4ivitamise algusaeg", + "database_engine": "Andmebaasi mootor", + "database_version": "Andmebaasi versioon", "estimated_db_size": "Andmebaasi hinnanguline suurus (MB)", "oldest_recorder_run": "Vanim k\u00e4ivitamise algusaeg" } diff --git a/homeassistant/components/recorder/translations/fr.json b/homeassistant/components/recorder/translations/fr.json index 036fbdbb16c..24896a8e66f 100644 --- a/homeassistant/components/recorder/translations/fr.json +++ b/homeassistant/components/recorder/translations/fr.json @@ -2,6 +2,8 @@ "system_health": { "info": { "current_recorder_run": "Heure de d\u00e9marrage de l'ex\u00e9cution actuelle", + "database_engine": "Moteur de la base de donn\u00e9es", + "database_version": "Version de la base de donn\u00e9es", "estimated_db_size": "Taille estim\u00e9e de la base de donn\u00e9es (en Mio)", "oldest_recorder_run": "Heure de d\u00e9marrage de l'ex\u00e9cution la plus ancienne" } diff --git a/homeassistant/components/recorder/translations/id.json b/homeassistant/components/recorder/translations/id.json index ec9e87b39ab..5f391a24f04 100644 --- a/homeassistant/components/recorder/translations/id.json +++ b/homeassistant/components/recorder/translations/id.json @@ -2,6 +2,8 @@ "system_health": { "info": { "current_recorder_run": "Waktu Mulai Jalankan Saat Ini", + "database_engine": "Mesin Basis Data", + "database_version": "Versi Basis Data", "estimated_db_size": "Perkiraan Ukuran Basis Data (MiB)", "oldest_recorder_run": "Waktu Mulai Lari Terlama" } diff --git a/homeassistant/components/recorder/translations/it.json b/homeassistant/components/recorder/translations/it.json index ab0ac13772a..ed2bc900ec6 100644 --- a/homeassistant/components/recorder/translations/it.json +++ b/homeassistant/components/recorder/translations/it.json @@ -2,6 +2,8 @@ "system_health": { "info": { "current_recorder_run": "Ora di inizio esecuzione corrente", + "database_engine": "Motore del database", + "database_version": "Versione del database", "estimated_db_size": "Dimensione stimata del database (MiB)", "oldest_recorder_run": "Ora di inizio esecuzione meno recente" } diff --git a/homeassistant/components/recorder/translations/ja.json b/homeassistant/components/recorder/translations/ja.json index 8b54e4f544f..e8938f363bf 100644 --- a/homeassistant/components/recorder/translations/ja.json +++ b/homeassistant/components/recorder/translations/ja.json @@ -2,6 +2,8 @@ "system_health": { "info": { "current_recorder_run": "\u73fe\u5728\u306e\u5b9f\u884c\u958b\u59cb\u6642\u9593", + "database_engine": "\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u30a8\u30f3\u30b8\u30f3", + "database_version": "\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306e\u30d0\u30fc\u30b8\u30e7\u30f3", "estimated_db_size": "\u63a8\u5b9a\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u30b5\u30a4\u30ba(MiB)", "oldest_recorder_run": "\u6700\u3082\u53e4\u3044\u5b9f\u884c\u958b\u59cb\u6642\u9593" } diff --git a/homeassistant/components/recorder/translations/nl.json b/homeassistant/components/recorder/translations/nl.json index bb28428cc48..db1f5d6a2b4 100644 --- a/homeassistant/components/recorder/translations/nl.json +++ b/homeassistant/components/recorder/translations/nl.json @@ -2,6 +2,8 @@ "system_health": { "info": { "current_recorder_run": "Starttijd huidige run", + "database_engine": "Database-engine", + "database_version": "Databaseversie", "estimated_db_size": "Geschatte databasegrootte (MiB)", "oldest_recorder_run": "Starttijd oudste run" } diff --git a/homeassistant/components/recorder/translations/no.json b/homeassistant/components/recorder/translations/no.json index 982354a6e34..c2a9eae16b5 100644 --- a/homeassistant/components/recorder/translations/no.json +++ b/homeassistant/components/recorder/translations/no.json @@ -2,6 +2,8 @@ "system_health": { "info": { "current_recorder_run": "Gjeldende starttid for kj\u00f8ring", + "database_engine": "Databasemotor", + "database_version": "Database versjon", "estimated_db_size": "Estimert databasest\u00f8rrelse (MiB)", "oldest_recorder_run": "Eldste Run Start Time" } diff --git a/homeassistant/components/recorder/translations/pt-BR.json b/homeassistant/components/recorder/translations/pt-BR.json index 059dcc76779..4ebad0d43df 100644 --- a/homeassistant/components/recorder/translations/pt-BR.json +++ b/homeassistant/components/recorder/translations/pt-BR.json @@ -2,6 +2,8 @@ "system_health": { "info": { "current_recorder_run": "Hora de in\u00edcio de execu\u00e7\u00e3o atual", + "database_engine": "Motor do banco de dados", + "database_version": "Vers\u00e3o do banco de dados", "estimated_db_size": "Tamanho estimado do banco de dados (MiB)", "oldest_recorder_run": "Hora de in\u00edcio de execu\u00e7\u00e3o mais antiga" } diff --git a/homeassistant/components/recorder/translations/ru.json b/homeassistant/components/recorder/translations/ru.json index a637d37f720..052d442f15c 100644 --- a/homeassistant/components/recorder/translations/ru.json +++ b/homeassistant/components/recorder/translations/ru.json @@ -2,6 +2,8 @@ "system_health": { "info": { "current_recorder_run": "\u0412\u0440\u0435\u043c\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u0442\u0435\u043a\u0443\u0449\u0435\u0433\u043e \u0446\u0438\u043a\u043b\u0430", + "database_engine": "\u0414\u0432\u0438\u0436\u043e\u043a \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445", + "database_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445", "estimated_db_size": "\u041f\u0440\u0435\u0434\u043f\u043e\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u0440\u0430\u0437\u043c\u0435\u0440 \u0431\u0430\u0437\u044b \u0434\u0430\u043d\u043d\u044b\u0445 (MiB)", "oldest_recorder_run": "\u0412\u0440\u0435\u043c\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u0441\u0430\u043c\u043e\u0433\u043e \u0441\u0442\u0430\u0440\u043e\u0433\u043e \u0446\u0438\u043a\u043b\u0430" } diff --git a/homeassistant/components/recorder/translations/zh-Hant.json b/homeassistant/components/recorder/translations/zh-Hant.json index d9525c0826d..53715af2e92 100644 --- a/homeassistant/components/recorder/translations/zh-Hant.json +++ b/homeassistant/components/recorder/translations/zh-Hant.json @@ -2,6 +2,8 @@ "system_health": { "info": { "current_recorder_run": "\u76ee\u524d\u57f7\u884c\u958b\u59cb\u6642\u9593", + "database_engine": "\u8cc7\u6599\u5eab\u5f15\u64ce", + "database_version": "\u8cc7\u6599\u5eab\u7248\u672c", "estimated_db_size": "\u9810\u4f30\u8cc7\u6599\u5eab\u6a94\u6848\u5927\u5c0f(MiB)", "oldest_recorder_run": "\u6700\u65e9\u57f7\u884c\u958b\u59cb\u6642\u9593" } diff --git a/homeassistant/components/shelly/translations/ru.json b/homeassistant/components/shelly/translations/ru.json index d3f38aa9eeb..b80e58aa905 100644 --- a/homeassistant/components/shelly/translations/ru.json +++ b/homeassistant/components/shelly/translations/ru.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "firmware_not_fully_provisioned": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e \u043d\u0435 \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e. \u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u0432 \u0441\u043b\u0443\u0436\u0431\u0443 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0438 Shelly", "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/siren/translations/ru.json b/homeassistant/components/siren/translations/ru.json new file mode 100644 index 00000000000..bd7725464a7 --- /dev/null +++ b/homeassistant/components/siren/translations/ru.json @@ -0,0 +1,3 @@ +{ + "title": "\u0421\u0438\u0440\u0435\u043d\u0430" +} \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/de.json b/homeassistant/components/steam_online/translations/de.json index fe73cf71448..44fb7f8b08b 100644 --- a/homeassistant/components/steam_online/translations/de.json +++ b/homeassistant/components/steam_online/translations/de.json @@ -25,6 +25,9 @@ } }, "options": { + "error": { + "unauthorized": "Freundesliste eingeschr\u00e4nkt: Bitte lies in der Dokumentation nach, wie du alle anderen Freunde sehen kannst" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/steam_online/translations/en.json b/homeassistant/components/steam_online/translations/en.json index f93a517b241..7226c5ee177 100644 --- a/homeassistant/components/steam_online/translations/en.json +++ b/homeassistant/components/steam_online/translations/en.json @@ -25,15 +25,15 @@ } }, "options": { + "error": { + "unauthorized": "Friends list restricted: Please refer to the documentation on how to see all other friends" + }, "step": { "init": { "data": { "accounts": "Names of accounts to be monitored" } } - }, - "error": { - "unauthorized": "Friends list restricted: Please refer to the documentation on how to see all other friends" } } } \ No newline at end of file diff --git a/homeassistant/components/steam_online/translations/fr.json b/homeassistant/components/steam_online/translations/fr.json index ae8055638f7..416927493d1 100644 --- a/homeassistant/components/steam_online/translations/fr.json +++ b/homeassistant/components/steam_online/translations/fr.json @@ -25,6 +25,9 @@ } }, "options": { + "error": { + "unauthorized": "Liste d'amis restreinte\u00a0: Veuillez consulter la documentation afin d'afficher tous les autres amis" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/steam_online/translations/id.json b/homeassistant/components/steam_online/translations/id.json index abc61d42194..e944662fee1 100644 --- a/homeassistant/components/steam_online/translations/id.json +++ b/homeassistant/components/steam_online/translations/id.json @@ -25,6 +25,9 @@ } }, "options": { + "error": { + "unauthorized": "Daftar teman dibatasi: Rujuk ke dokumentasi tentang cara melihat semua teman lain" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/steam_online/translations/nl.json b/homeassistant/components/steam_online/translations/nl.json index 7eafa00e53c..5512c18ee2b 100644 --- a/homeassistant/components/steam_online/translations/nl.json +++ b/homeassistant/components/steam_online/translations/nl.json @@ -25,6 +25,9 @@ } }, "options": { + "error": { + "unauthorized": "Vriendenlijst beperkt: raadpleeg de documentatie over hoe je alle andere vrienden kunt zien" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/steam_online/translations/pt-BR.json b/homeassistant/components/steam_online/translations/pt-BR.json index cca660e891e..5aa9d0d2520 100644 --- a/homeassistant/components/steam_online/translations/pt-BR.json +++ b/homeassistant/components/steam_online/translations/pt-BR.json @@ -25,6 +25,9 @@ } }, "options": { + "error": { + "unauthorized": "Lista restrita de amigos: consulte a documenta\u00e7\u00e3o sobre como ver todos os outros amigos" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/waze_travel_time/translations/ja.json b/homeassistant/components/waze_travel_time/translations/ja.json index 5ed0741a67e..545c0cdb0d6 100644 --- a/homeassistant/components/waze_travel_time/translations/ja.json +++ b/homeassistant/components/waze_travel_time/translations/ja.json @@ -11,7 +11,7 @@ "data": { "destination": "\u76ee\u7684\u5730", "name": "\u540d\u524d", - "origin": "\u30aa\u30ea\u30b8\u30f3", + "origin": "\u539f\u70b9(Origin)", "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" }, "description": "\u51fa\u767a\u5730\u3068\u76ee\u7684\u5730\u306b\u3001\u5834\u6240\u306e\u4f4f\u6240\u307e\u305f\u306fGPS\u5ea7\u6a19\u3092\u5165\u529b\u3057\u307e\u3059(GPS\u306e\u5ea7\u6a19\u306f\u30b3\u30f3\u30de\u3067\u533a\u5207\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059)\u3002\u3053\u306e\u60c5\u5831\u3092\u72b6\u614b(state)\u3067\u63d0\u4f9b\u3059\u308b\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3ID\u3001\u7def\u5ea6\u3068\u7d4c\u5ea6\u306e\u5c5e\u6027\u3092\u6301\u3064\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3ID\u3001\u307e\u305f\u306f\u30be\u30fc\u30f3\u306e\u30d5\u30ec\u30f3\u30c9\u30ea\u30fc\u540d\u3092\u5165\u529b\u3059\u308b\u3053\u3068\u3082\u3067\u304d\u307e\u3059\u3002" diff --git a/homeassistant/components/yolink/translations/ru.json b/homeassistant/components/yolink/translations/ru.json new file mode 100644 index 00000000000..a1db7e744a8 --- /dev/null +++ b/homeassistant/components/yolink/translations/ru.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "already_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.", + "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", + "oauth_error": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u044b \u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0442\u043e\u043a\u0435\u043d\u0430.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "create_entry": { + "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "reauth_confirm": { + "description": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 yolink", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + } + } + } +} \ No newline at end of file From 1c25e1d7b1d8c1827f3ed4e204c1e1682904be7f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 May 2022 00:37:47 -0500 Subject: [PATCH 0830/3516] Add metadata to logbook live stream websocket endpoint (#72394) --- .../components/logbook/websocket_api.py | 88 +++++++++++++------ .../components/logbook/test_websocket_api.py | 80 ++++++++++++----- 2 files changed, 121 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index edfe3177f26..0bb7877b95b 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -75,6 +75,7 @@ async def _async_send_historical_events( end_time: dt, formatter: Callable[[int, Any], dict[str, Any]], event_processor: EventProcessor, + partial: bool, ) -> dt | None: """Select historical data from the database and deliver it to the websocket. @@ -94,88 +95,107 @@ async def _async_send_historical_events( ) if not is_big_query: - message, last_event_time = await _async_get_ws_formatted_events( + message, last_event_time = await _async_get_ws_stream_events( hass, msg_id, start_time, end_time, formatter, event_processor, + partial, ) - # If there is no last_time, there are no historical - # results, but we still send an empty message so + # If there is no last_event_time, there are no historical + # results, but we still send an empty message + # if its the last one (not partial) so # consumers of the api know their request was # answered but there were no results - connection.send_message(message) + if last_event_time or not partial: + connection.send_message(message) return last_event_time # This is a big query so we deliver # the first three hours and then # we fetch the old data recent_query_start = end_time - timedelta(hours=BIG_QUERY_RECENT_HOURS) - recent_message, recent_query_last_event_time = await _async_get_ws_formatted_events( + recent_message, recent_query_last_event_time = await _async_get_ws_stream_events( hass, msg_id, recent_query_start, end_time, formatter, event_processor, + partial=True, ) if recent_query_last_event_time: connection.send_message(recent_message) - older_message, older_query_last_event_time = await _async_get_ws_formatted_events( + older_message, older_query_last_event_time = await _async_get_ws_stream_events( hass, msg_id, start_time, recent_query_start, formatter, event_processor, + partial, ) - # If there is no last_time, there are no historical - # results, but we still send an empty message so + # If there is no last_event_time, there are no historical + # results, but we still send an empty message + # if its the last one (not partial) so # consumers of the api know their request was # answered but there were no results - if older_query_last_event_time or not recent_query_last_event_time: + if older_query_last_event_time or not partial: connection.send_message(older_message) # Returns the time of the newest event return recent_query_last_event_time or older_query_last_event_time -async def _async_get_ws_formatted_events( +async def _async_get_ws_stream_events( hass: HomeAssistant, msg_id: int, start_time: dt, end_time: dt, formatter: Callable[[int, Any], dict[str, Any]], event_processor: EventProcessor, + partial: bool, ) -> tuple[str, dt | None]: """Async wrapper around _ws_formatted_get_events.""" return await get_instance(hass).async_add_executor_job( - _ws_formatted_get_events, + _ws_stream_get_events, msg_id, start_time, end_time, formatter, event_processor, + partial, ) -def _ws_formatted_get_events( +def _ws_stream_get_events( msg_id: int, start_day: dt, end_day: dt, formatter: Callable[[int, Any], dict[str, Any]], event_processor: EventProcessor, + partial: bool, ) -> tuple[str, dt | None]: """Fetch events and convert them to json in the executor.""" events = event_processor.get_events(start_day, end_day) last_time = None if events: last_time = dt_util.utc_from_timestamp(events[-1]["when"]) - result = formatter(msg_id, events) - return JSON_DUMP(result), last_time + message = { + "events": events, + "start_time": dt_util.utc_to_timestamp(start_day), + "end_time": dt_util.utc_to_timestamp(end_day), + } + if partial: + # This is a hint to consumers of the api that + # we are about to send a another block of historical + # data in case the UI needs to show that historical + # data is still loading in the future + message["partial"] = True + return JSON_DUMP(formatter(msg_id, message)), last_time async def _async_events_consumer( @@ -209,7 +229,7 @@ async def _async_events_consumer( JSON_DUMP( messages.event_message( msg_id, - logbook_events, + {"events": logbook_events}, ) ) ) @@ -279,6 +299,7 @@ async def ws_event_stream( end_time, messages.event_message, event_processor, + partial=False, ) return @@ -331,6 +352,7 @@ async def ws_event_stream( subscriptions_setup_complete_time, messages.event_message, event_processor, + partial=True, ) await _async_wait_for_recorder_sync(hass) @@ -353,16 +375,16 @@ async def ws_event_stream( second_fetch_start_time = max( last_event_time or max_recorder_behind, max_recorder_behind ) - message, final_cutoff_time = await _async_get_ws_formatted_events( + await _async_send_historical_events( hass, + connection, msg_id, second_fetch_start_time, subscriptions_setup_complete_time, messages.event_message, event_processor, + partial=False, ) - if final_cutoff_time: # Only sends results if we have them - connection.send_message(message) if not subscriptions: # Unsubscribe happened while waiting for formatted events @@ -379,6 +401,20 @@ async def ws_event_stream( ) +def _ws_formatted_get_events( + msg_id: int, + start_time: dt, + end_time: dt, + event_processor: EventProcessor, +) -> str: + """Fetch events and convert them to json in the executor.""" + return JSON_DUMP( + messages.result_message( + msg_id, event_processor.get_events(start_time, end_time) + ) + ) + + @websocket_api.websocket_command( { vol.Required("type"): "logbook/get_events", @@ -438,12 +474,12 @@ async def ws_get_events( include_entity_name=False, ) - message, _ = await _async_get_ws_formatted_events( - hass, - msg["id"], - start_time, - end_time, - messages.result_message, - event_processor, + connection.send_message( + await hass.async_add_executor_job( + _ws_formatted_get_events, + msg["id"], + start_time, + end_time, + event_processor, + ) ) - connection.send_message(message) diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 26ad7127296..8706ccf7617 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -492,13 +492,16 @@ async def test_subscribe_unsubscribe_logbook_stream( msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"] == [ + assert msg["event"]["events"] == [ { "entity_id": "binary_sensor.is_light", "state": "off", "when": state.last_updated.timestamp(), } ] + assert msg["event"]["start_time"] == now.timestamp() + assert msg["event"]["end_time"] > msg["event"]["start_time"] + assert msg["event"]["partial"] is True hass.states.async_set("light.alpha", "on") hass.states.async_set("light.alpha", "off") @@ -520,7 +523,14 @@ async def test_subscribe_unsubscribe_logbook_stream( msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"] == [ + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [ { "entity_id": "light.alpha", "state": "off", @@ -560,7 +570,7 @@ async def test_subscribe_unsubscribe_logbook_stream( msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"] == [ + assert msg["event"]["events"] == [ { "context_id": ANY, "domain": "automation", @@ -624,7 +634,7 @@ async def test_subscribe_unsubscribe_logbook_stream( msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"] == [ + assert msg["event"]["events"] == [ { "context_id": "ac5bd62de45711eaaeb351041eec8dd9", "context_user_id": "b400facee45711eaa9308bfd3d19e474", @@ -686,7 +696,7 @@ async def test_subscribe_unsubscribe_logbook_stream( msg = await websocket_client.receive_json() assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"] == [ + assert msg["event"]["events"] == [ { "context_domain": "automation", "context_entity_id": "automation.alarm", @@ -716,7 +726,7 @@ async def test_subscribe_unsubscribe_logbook_stream( msg = await websocket_client.receive_json() assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"] == [ + assert msg["event"]["events"] == [ { "context_domain": "automation", "context_entity_id": "automation.alarm", @@ -788,7 +798,10 @@ async def test_subscribe_unsubscribe_logbook_stream_entities( msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"] == [ + assert "start_time" in msg["event"] + assert "end_time" in msg["event"] + assert msg["event"]["partial"] is True + assert msg["event"]["events"] == [ { "entity_id": "binary_sensor.is_light", "state": "off", @@ -805,7 +818,16 @@ async def test_subscribe_unsubscribe_logbook_stream_entities( msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"] == [ + assert "start_time" in msg["event"] + assert "end_time" in msg["event"] + assert "partial" not in msg["event"] + assert msg["event"]["events"] == [] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"] + assert msg["event"]["events"] == [ { "entity_id": "light.small", "state": "off", @@ -871,7 +893,8 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_with_end_time( msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"] == [ + assert msg["event"]["partial"] is True + assert msg["event"]["events"] == [ { "entity_id": "binary_sensor.is_light", "state": "off", @@ -888,7 +911,14 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_with_end_time( msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"] == [ + assert "partial" not in msg["event"] + assert msg["event"]["events"] == [] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"] + assert msg["event"]["events"] == [ { "entity_id": "light.small", "state": "off", @@ -962,7 +992,7 @@ async def test_subscribe_unsubscribe_logbook_stream_entities_past_only( msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"] == [ + assert msg["event"]["events"] == [ { "entity_id": "binary_sensor.is_light", "state": "off", @@ -1048,7 +1078,7 @@ async def test_subscribe_unsubscribe_logbook_stream_big_query( msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"] == [ + assert msg["event"]["events"] == [ { "entity_id": "binary_sensor.is_light", "state": "on", @@ -1060,7 +1090,8 @@ async def test_subscribe_unsubscribe_logbook_stream_big_query( msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"] == [ + assert msg["event"]["partial"] is True + assert msg["event"]["events"] == [ { "entity_id": "binary_sensor.four_days_ago", "state": "off", @@ -1068,6 +1099,13 @@ async def test_subscribe_unsubscribe_logbook_stream_big_query( } ] + # And finally a response without partial set to indicate no more + # historical data is coming + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [] + await websocket_client.send_json( {"id": 8, "type": "unsubscribe_events", "subscription": 7} ) @@ -1123,7 +1161,7 @@ async def test_subscribe_unsubscribe_logbook_stream_device( msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"] == [] + assert msg["event"]["events"] == [] hass.bus.async_fire("mock_event", {"device_id": device.id}) await hass.async_block_till_done() @@ -1131,7 +1169,7 @@ async def test_subscribe_unsubscribe_logbook_stream_device( msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"] == [ + assert msg["event"]["events"] == [ {"domain": "test", "message": "is on fire", "name": "device name", "when": ANY} ] @@ -1250,7 +1288,7 @@ async def test_live_stream_with_one_second_commit_interval( msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - recieved_rows.extend(msg["event"]) + recieved_rows.extend(msg["event"]["events"]) hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "6"}) @@ -1262,7 +1300,7 @@ async def test_live_stream_with_one_second_commit_interval( msg = await asyncio.wait_for(websocket_client.receive_json(), 2.5) assert msg["id"] == 7 assert msg["type"] == "event" - recieved_rows.extend(msg["event"]) + recieved_rows.extend(msg["event"]["events"]) # Make sure we get rows back in order assert recieved_rows == [ @@ -1326,7 +1364,7 @@ async def test_subscribe_disconnected(hass, recorder_mock, hass_ws_client): msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"] == [ + assert msg["event"]["events"] == [ { "entity_id": "binary_sensor.is_light", "state": "off", @@ -1437,7 +1475,7 @@ async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplo msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"] == [] + assert msg["event"]["events"] == [] hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "1"}) await hass.async_block_till_done() @@ -1445,7 +1483,7 @@ async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplo msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"] == [ + assert msg["event"]["events"] == [ {"domain": "test", "message": "1", "name": "device name", "when": ANY} ] @@ -1455,7 +1493,7 @@ async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplo msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" - assert msg["event"] == [ + assert msg["event"]["events"] == [ {"domain": "test", "message": "2", "name": "device name", "when": ANY} ] From 070cb616317f4661e8558706ae025d3d9b2b4868 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 24 May 2022 08:23:09 +0200 Subject: [PATCH 0831/3516] Adjust config-flow type hints in cloudflare (#72388) * Adjust config-flow type hints in cloudflare * Improve type hints --- .../components/cloudflare/config_flow.py | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/cloudflare/config_flow.py b/homeassistant/components/cloudflare/config_flow.py index 27a22dbc5bd..121e0fc9974 100644 --- a/homeassistant/components/cloudflare/config_flow.py +++ b/homeassistant/components/cloudflare/config_flow.py @@ -32,7 +32,7 @@ DATA_SCHEMA = vol.Schema( ) -def _zone_schema(zones: list | None = None): +def _zone_schema(zones: list[str] | None = None) -> vol.Schema: """Zone selection schema.""" zones_list = [] @@ -42,7 +42,7 @@ def _zone_schema(zones: list | None = None): return vol.Schema({vol.Required(CONF_ZONE): vol.In(zones_list)}) -def _records_schema(records: list | None = None): +def _records_schema(records: list[str] | None = None) -> vol.Schema: """Zone records selection schema.""" records_dict = {} @@ -52,13 +52,15 @@ def _records_schema(records: list | None = None): return vol.Schema({vol.Required(CONF_RECORDS): cv.multi_select(records_dict)}) -async def validate_input(hass: HomeAssistant, data: dict): +async def _validate_input( + hass: HomeAssistant, data: dict[str, Any] +) -> dict[str, list[str] | None]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. """ zone = data.get(CONF_ZONE) - records = None + records: list[str] | None = None cfupdate = CloudflareUpdater( async_get_clientsession(hass), @@ -68,7 +70,7 @@ async def validate_input(hass: HomeAssistant, data: dict): ) try: - zones = await cfupdate.get_zones() + zones: list[str] | None = await cfupdate.get_zones() if zone: zone_id = await cfupdate.get_zone_id() records = await cfupdate.get_zone_records(zone_id, "A") @@ -89,11 +91,11 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN): entry: ConfigEntry | None = None - def __init__(self): + def __init__(self) -> None: """Initialize the Cloudflare config flow.""" - self.cloudflare_config = {} - self.zones = None - self.records = None + self.cloudflare_config: dict[str, Any] = {} + self.zones: list[str] | None = None + self.records: list[str] | None = None async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: """Handle initiation of re-authentication with Cloudflare.""" @@ -104,7 +106,7 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle re-authentication with Cloudflare.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None and self.entry: _, errors = await self._async_validate_or_error(user_input) @@ -130,14 +132,16 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_user(self, user_input: dict | None = None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") persistent_notification.async_dismiss(self.hass, "cloudflare_setup") - errors = {} + errors: dict[str, str] = {} if user_input is not None: info, errors = await self._async_validate_or_error(user_input) @@ -151,9 +155,11 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_zone(self, user_input: dict | None = None): + async def async_step_zone( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the picking the zone.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: self.cloudflare_config.update(user_input) @@ -171,7 +177,9 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_records(self, user_input: dict | None = None): + async def async_step_records( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the picking the zone records.""" if user_input is not None: @@ -184,12 +192,14 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN): data_schema=_records_schema(self.records), ) - async def _async_validate_or_error(self, config): - errors = {} + async def _async_validate_or_error( + self, config: dict[str, Any] + ) -> tuple[dict[str, list[str] | None], dict[str, str]]: + errors: dict[str, str] = {} info = {} try: - info = await validate_input(self.hass, config) + info = await _validate_input(self.hass, config) except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: From f0b9aa7894e19da475c1e2debcd7f5189d7b1ebe Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 24 May 2022 00:53:01 -0700 Subject: [PATCH 0832/3516] Bump pywemo==0.8.1 (#72400) --- .../components/wemo/binary_sensor.py | 8 +++--- homeassistant/components/wemo/entity.py | 3 +-- homeassistant/components/wemo/fan.py | 2 +- homeassistant/components/wemo/light.py | 26 ++++++++++--------- homeassistant/components/wemo/manifest.json | 2 +- homeassistant/components/wemo/switch.py | 12 +++++---- homeassistant/components/wemo/wemo_device.py | 4 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wemo/conftest.py | 2 +- tests/components/wemo/test_binary_sensor.py | 6 ++--- tests/components/wemo/test_switch.py | 6 ++--- 12 files changed, 39 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index 3e7b159dc63..ce7dfc2fa11 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 -from typing import cast from pywemo import Insight, Maker, StandbyState @@ -49,20 +48,21 @@ class MakerBinarySensor(WemoEntity, BinarySensorEntity): """Maker device's sensor port.""" _name_suffix = "Sensor" + wemo: Maker @property def is_on(self) -> bool: """Return true if the Maker's sensor is pulled low.""" - return cast(int, self.wemo.has_sensor) != 0 and self.wemo.sensor_state == 0 + return self.wemo.has_sensor != 0 and self.wemo.sensor_state == 0 class InsightBinarySensor(WemoBinarySensor): """Sensor representing the device connected to the Insight Switch.""" _name_suffix = "Device" + wemo: Insight @property def is_on(self) -> bool: """Return true device connected to the Insight Switch is on.""" - # Note: wemo.get_standby_state is a @property. - return super().is_on and self.wemo.get_standby_state == StandbyState.ON + return super().is_on and self.wemo.standby_state == StandbyState.ON diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index 6d94e203932..c8ed9cdde08 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Generator import contextlib import logging -from typing import cast from pywemo.exceptions import ActionException @@ -89,4 +88,4 @@ class WemoBinaryStateEntity(WemoEntity): @property def is_on(self) -> bool: """Return true if the state is on.""" - return cast(int, self.wemo.get_state()) != 0 + return self.wemo.get_state() != 0 diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index d62a7a3b7e3..d24827bee96 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -6,7 +6,7 @@ from datetime import timedelta import math from typing import Any -from pywemo.ouimeaux_device.humidifier import DesiredHumidity, FanMode, Humidifier +from pywemo import DesiredHumidity, FanMode, Humidifier import voluptuous as vol from homeassistant.components.fan import FanEntity, FanEntityFeature diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 87ebbd9e2c3..3ff0f115a04 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -2,9 +2,9 @@ from __future__ import annotations import asyncio -from typing import Any, Optional, cast +from typing import Any, cast -from pywemo.ouimeaux_device import bridge +from pywemo import Bridge, BridgeLight, Dimmer from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -40,7 +40,7 @@ async def async_setup_entry( async def _discovered_wemo(coordinator: DeviceCoordinator) -> None: """Handle a discovered Wemo device.""" - if isinstance(coordinator.wemo, bridge.Bridge): + if isinstance(coordinator.wemo, Bridge): async_setup_bridge(hass, config_entry, async_add_entities, coordinator) else: async_add_entities([WemoDimmer(coordinator)]) @@ -70,7 +70,8 @@ def async_setup_bridge( """Check to see if the bridge has any new lights.""" new_lights = [] - for light_id, light in coordinator.wemo.Lights.items(): + bridge = cast(Bridge, coordinator.wemo) + for light_id, light in bridge.Lights.items(): if light_id not in known_light_ids: known_light_ids.add(light_id) new_lights.append(WemoLight(coordinator, light)) @@ -87,7 +88,7 @@ class WemoLight(WemoEntity, LightEntity): _attr_supported_features = LightEntityFeature.TRANSITION - def __init__(self, coordinator: DeviceCoordinator, light: bridge.Light) -> None: + def __init__(self, coordinator: DeviceCoordinator, light: BridgeLight) -> None: """Initialize the WeMo light.""" super().__init__(coordinator) self.light = light @@ -97,17 +98,17 @@ class WemoLight(WemoEntity, LightEntity): @property def name(self) -> str: """Return the name of the device if any.""" - return cast(str, self.light.name) + return self.light.name @property def available(self) -> bool: """Return true if the device is available.""" - return super().available and self.light.state.get("available") + return super().available and self.light.state.get("available", False) @property def unique_id(self) -> str: """Return the ID of this light.""" - return cast(str, self.light.uniqueID) + return self.light.uniqueID @property def device_info(self) -> DeviceInfo: @@ -123,17 +124,17 @@ class WemoLight(WemoEntity, LightEntity): @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - return cast(int, self.light.state.get("level", 255)) + return self.light.state.get("level", 255) @property def xy_color(self) -> tuple[float, float] | None: """Return the xy color value [float, float].""" - return self.light.state.get("color_xy") # type:ignore[no-any-return] + return self.light.state.get("color_xy") @property def color_temp(self) -> int | None: """Return the color temperature of this light in mireds.""" - return cast(Optional[int], self.light.state.get("temperature_mireds")) + return self.light.state.get("temperature_mireds") @property def color_mode(self) -> ColorMode: @@ -166,7 +167,7 @@ class WemoLight(WemoEntity, LightEntity): @property def is_on(self) -> bool: """Return true if device is on.""" - return cast(int, self.light.state.get("onoff")) != WEMO_OFF + return self.light.state.get("onoff", WEMO_OFF) != WEMO_OFF def turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" @@ -210,6 +211,7 @@ class WemoDimmer(WemoBinaryStateEntity, LightEntity): _attr_supported_color_modes = {ColorMode.BRIGHTNESS} _attr_color_mode = ColorMode.BRIGHTNESS + wemo: Dimmer @property def brightness(self) -> int: diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index d048a59d38c..40bb8161d90 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -3,7 +3,7 @@ "name": "Belkin WeMo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wemo", - "requirements": ["pywemo==0.7.0"], + "requirements": ["pywemo==0.8.1"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index c3d5bcb0462..1f9f8bce01f 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -3,9 +3,9 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta -from typing import Any, cast +from typing import Any -from pywemo import CoffeeMaker, Insight, Maker, StandbyState +from pywemo import CoffeeMaker, Insight, Maker, StandbyState, Switch from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -58,6 +58,9 @@ async def async_setup_entry( class WemoSwitch(WemoBinaryStateEntity, SwitchEntity): """Representation of a WeMo switch.""" + # All wemo devices used with WemoSwitch are subclasses of Switch. + wemo: Switch + @property def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the device.""" @@ -103,10 +106,9 @@ class WemoSwitch(WemoBinaryStateEntity, SwitchEntity): def detail_state(self) -> str: """Return the state of the device.""" if isinstance(self.wemo, CoffeeMaker): - return cast(str, self.wemo.mode_string) + return self.wemo.mode_string if isinstance(self.wemo, Insight): - # Note: wemo.get_standby_state is a @property. - standby_state = self.wemo.get_standby_state + standby_state = self.wemo.standby_state if standby_state == StandbyState.ON: return STATE_ON if standby_state == StandbyState.OFF: diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py index 3ca47544fd7..8f5e6864059 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -3,7 +3,7 @@ import asyncio from datetime import timedelta import logging -from pywemo import Insight, WeMoDevice +from pywemo import Insight, LongPressMixin, WeMoDevice from pywemo.exceptions import ActionException from pywemo.subscribe import EVENT_TYPE_LONG_PRESS @@ -159,7 +159,7 @@ async def async_register_device( registry.on(wemo, None, device.subscription_callback) await hass.async_add_executor_job(registry.register, wemo) - if device.supports_long_press: + if isinstance(wemo, LongPressMixin): try: await hass.async_add_executor_job(wemo.ensure_long_press_virtual_device) # Temporarily handling all exceptions for #52996 & pywemo/pywemo/issues/276 diff --git a/requirements_all.txt b/requirements_all.txt index bf4f2d556ca..b5c731367c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2017,7 +2017,7 @@ pyvolumio==0.1.5 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.7.0 +pywemo==0.8.1 # homeassistant.components.wilight pywilight==0.0.70 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f735b555859..f862c719f50 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1337,7 +1337,7 @@ pyvolumio==0.1.5 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.7.0 +pywemo==0.8.1 # homeassistant.components.wilight pywilight==0.0.70 diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index ae900e2522f..1a5998c1f94 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -69,7 +69,7 @@ def create_pywemo_device(pywemo_registry, pywemo_model): device.supports_long_press.return_value = cls.supports_long_press() if issubclass(cls, pywemo.Insight): - device.get_standby_state = pywemo.StandbyState.OFF + device.standby_state = pywemo.StandbyState.OFF device.current_power_watts = MOCK_INSIGHT_CURRENT_WATTS device.today_kwh = MOCK_INSIGHT_TODAY_KWH device.threshold_power_watts = MOCK_INSIGHT_STATE_THRESHOLD_POWER diff --git a/tests/components/wemo/test_binary_sensor.py b/tests/components/wemo/test_binary_sensor.py index f26428bf32b..99a5df47e25 100644 --- a/tests/components/wemo/test_binary_sensor.py +++ b/tests/components/wemo/test_binary_sensor.py @@ -117,21 +117,21 @@ class TestInsight(EntityTestHelpers): """Verify that the binary_sensor receives state updates from the registry.""" # On state. pywemo_device.get_state.return_value = 1 - pywemo_device.get_standby_state = StandbyState.ON + pywemo_device.standby_state = StandbyState.ON pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") await hass.async_block_till_done() assert hass.states.get(wemo_entity.entity_id).state == STATE_ON # Standby (Off) state. pywemo_device.get_state.return_value = 1 - pywemo_device.get_standby_state = StandbyState.STANDBY + pywemo_device.standby_state = StandbyState.STANDBY pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") await hass.async_block_till_done() assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF # Off state. pywemo_device.get_state.return_value = 0 - pywemo_device.get_standby_state = StandbyState.OFF + pywemo_device.standby_state = StandbyState.OFF pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") await hass.async_block_till_done() assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF diff --git a/tests/components/wemo/test_switch.py b/tests/components/wemo/test_switch.py index 0c024295632..0f23aa8b8b6 100644 --- a/tests/components/wemo/test_switch.py +++ b/tests/components/wemo/test_switch.py @@ -134,19 +134,19 @@ async def test_insight_state_attributes(hass, pywemo_registry): ) # Test 'ON' state detail value. - insight.get_standby_state = pywemo.StandbyState.ON + insight.standby_state = pywemo.StandbyState.ON await async_update() attributes = hass.states.get(wemo_entity.entity_id).attributes assert attributes[ATTR_CURRENT_STATE_DETAIL] == STATE_ON # Test 'STANDBY' state detail value. - insight.get_standby_state = pywemo.StandbyState.STANDBY + insight.standby_state = pywemo.StandbyState.STANDBY await async_update() attributes = hass.states.get(wemo_entity.entity_id).attributes assert attributes[ATTR_CURRENT_STATE_DETAIL] == STATE_STANDBY # Test 'UNKNOWN' state detail value. - insight.get_standby_state = None + insight.standby_state = None await async_update() attributes = hass.states.get(wemo_entity.entity_id).attributes assert attributes[ATTR_CURRENT_STATE_DETAIL] == STATE_UNKNOWN From 3cd398a5bdc7a6d2fc43752139a3c4bd9f8bbaaa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 24 May 2022 01:26:25 -0700 Subject: [PATCH 0833/3516] Warn for old Google SDK version (#72403) --- .../components/google_assistant/helpers.py | 19 ++++- .../google_assistant/test_helpers.py | 82 +++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 529778ce1b6..b425367f5c3 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -10,6 +10,7 @@ import logging import pprint from aiohttp.web import json_response +from awesomeversion import AwesomeVersion from homeassistant.components import webhook from homeassistant.const import ( @@ -43,6 +44,8 @@ from .error import SmartHomeError SYNC_DELAY = 15 _LOGGER = logging.getLogger(__name__) +LOCAL_SDK_VERSION_HEADER = "HA-Cloud-Version" +LOCAL_SDK_MIN_VERSION = AwesomeVersion("2.1.5") @callback @@ -86,6 +89,7 @@ class AbstractConfig(ABC): self._google_sync_unsub = {} self._local_sdk_active = False self._local_last_active: datetime | None = None + self._local_sdk_version_warn = False async def async_initialize(self): """Perform async initialization of config.""" @@ -327,6 +331,18 @@ class AbstractConfig(ABC): from . import smart_home self._local_last_active = utcnow() + + # Check version local SDK. + version = request.headers.get("HA-Cloud-Version") + if not self._local_sdk_version_warn and ( + not version or AwesomeVersion(version) < LOCAL_SDK_MIN_VERSION + ): + _LOGGER.warning( + "Local SDK version is too old (%s), check documentation on how to update to the latest version", + version, + ) + self._local_sdk_version_warn = True + payload = await request.json() if _LOGGER.isEnabledFor(logging.DEBUG): @@ -577,8 +593,9 @@ class GoogleEntity: device["customData"] = { "webhookId": self.config.get_local_webhook_id(agent_user_id), "httpPort": self.hass.http.server_port, - "httpSSL": self.hass.config.api.use_ssl, "uuid": instance_uuid, + # Below can be removed in HA 2022.9 + "httpSSL": self.hass.config.api.use_ssl, "baseUrl": get_url(self.hass, prefer_external=True), "proxyDeviceId": agent_user_id, } diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index 4490f0e3963..1ab573baf2a 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -345,3 +345,85 @@ def test_request_data(): config, "test_user", SOURCE_CLOUD, "test_request_id", None ) assert data.is_local_request is False + + +async def test_config_local_sdk_allow_min_version(hass, hass_client, caplog): + """Test the local SDK.""" + version = str(helpers.LOCAL_SDK_MIN_VERSION) + assert await async_setup_component(hass, "webhook", {}) + + config = MockConfig( + hass=hass, + agent_user_ids={ + "mock-user-id": { + STORE_GOOGLE_LOCAL_WEBHOOK_ID: "mock-webhook-id", + }, + }, + ) + + client = await hass_client() + + assert config._local_sdk_version_warn is False + config.async_enable_local_sdk() + + await client.post( + "/api/webhook/mock-webhook-id", + headers={helpers.LOCAL_SDK_VERSION_HEADER: version}, + json={ + "inputs": [ + { + "context": {"locale_country": "US", "locale_language": "en"}, + "intent": "action.devices.SYNC", + } + ], + "requestId": "mock-req-id", + }, + ) + assert config._local_sdk_version_warn is False + assert ( + f"Local SDK version is too old ({version}), check documentation on how " + "to update to the latest version" + ) not in caplog.text + + +@pytest.mark.parametrize("version", (None, "2.1.4")) +async def test_config_local_sdk_warn_version(hass, hass_client, caplog, version): + """Test the local SDK.""" + assert await async_setup_component(hass, "webhook", {}) + + config = MockConfig( + hass=hass, + agent_user_ids={ + "mock-user-id": { + STORE_GOOGLE_LOCAL_WEBHOOK_ID: "mock-webhook-id", + }, + }, + ) + + client = await hass_client() + + assert config._local_sdk_version_warn is False + config.async_enable_local_sdk() + + headers = {} + if version: + headers[helpers.LOCAL_SDK_VERSION_HEADER] = version + + await client.post( + "/api/webhook/mock-webhook-id", + headers=headers, + json={ + "inputs": [ + { + "context": {"locale_country": "US", "locale_language": "en"}, + "intent": "action.devices.SYNC", + } + ], + "requestId": "mock-req-id", + }, + ) + assert config._local_sdk_version_warn is True + assert ( + f"Local SDK version is too old ({version}), check documentation on how " + "to update to the latest version" + ) in caplog.text From cc162bf691e7b470e7c848b2ea82548173a79131 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 24 May 2022 10:49:05 +0200 Subject: [PATCH 0834/3516] Remove YAML configuration from Jandy iAqualink (#72404) --- .../components/iaqualink/__init__.py | 43 +++---------------- .../components/iaqualink/config_flow.py | 9 ++-- .../components/iaqualink/test_config_flow.py | 36 +++++----------- 3 files changed, 19 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 0d413b1ad7c..02e27d8e82f 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -19,9 +19,7 @@ from iaqualink.device import ( ) from iaqualink.exception import AqualinkServiceException from typing_extensions import Concatenate, ParamSpec -import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN @@ -32,14 +30,12 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, UPDATE_INTERVAL @@ -52,42 +48,13 @@ ATTR_CONFIG = "config" PARALLEL_UPDATES = 0 PLATFORMS = [ - BINARY_SENSOR_DOMAIN, - CLIMATE_DOMAIN, - LIGHT_DOMAIN, - SENSOR_DOMAIN, - SWITCH_DOMAIN, + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.LIGHT, + Platform.SENSOR, + Platform.SWITCH, ] -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Aqualink component.""" - if (conf := config.get(DOMAIN)) is not None: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=conf, - ) - ) - - return True - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Aqualink from a config entry.""" diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py index 921102b85dc..3b3a99cac6e 100644 --- a/homeassistant/components/iaqualink/config_flow.py +++ b/homeassistant/components/iaqualink/config_flow.py @@ -12,6 +12,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN @@ -21,7 +22,9 @@ class AqualinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input: dict[str, Any] | None = None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow start.""" # Supporting a single account. entries = self._async_current_entries() @@ -54,7 +57,3 @@ class AqualinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ), errors=errors, ) - - async def async_step_import(self, user_input: dict[str, Any] | None = None): - """Occurs when an entry is setup through config.""" - return await self.async_step_user(user_input) diff --git a/tests/components/iaqualink/test_config_flow.py b/tests/components/iaqualink/test_config_flow.py index 2d00284775d..df2add36b9c 100644 --- a/tests/components/iaqualink/test_config_flow.py +++ b/tests/components/iaqualink/test_config_flow.py @@ -5,13 +5,11 @@ from iaqualink.exception import ( AqualinkServiceException, AqualinkServiceUnauthorizedException, ) -import pytest from homeassistant.components.iaqualink import config_flow -@pytest.mark.parametrize("step", ["import", "user"]) -async def test_already_configured(hass, config_entry, config_data, step): +async def test_already_configured(hass, config_entry, config_data): """Test config flow when iaqualink component is already setup.""" config_entry.add_to_hass(hass) @@ -19,81 +17,67 @@ async def test_already_configured(hass, config_entry, config_data, step): flow.hass = hass flow.context = {} - fname = f"async_step_{step}" - func = getattr(flow, fname) - result = await func(config_data) + result = await flow.async_step_user(config_data) assert result["type"] == "abort" -@pytest.mark.parametrize("step", ["import", "user"]) -async def test_without_config(hass, step): +async def test_without_config(hass): """Test config flow with no configuration.""" flow = config_flow.AqualinkFlowHandler() flow.hass = hass flow.context = {} - fname = f"async_step_{step}" - func = getattr(flow, fname) - result = await func() + result = await flow.async_step_user() assert result["type"] == "form" assert result["step_id"] == "user" assert result["errors"] == {} -@pytest.mark.parametrize("step", ["import", "user"]) -async def test_with_invalid_credentials(hass, config_data, step): +async def test_with_invalid_credentials(hass, config_data): """Test config flow with invalid username and/or password.""" flow = config_flow.AqualinkFlowHandler() flow.hass = hass - fname = f"async_step_{step}" - func = getattr(flow, fname) with patch( "homeassistant.components.iaqualink.config_flow.AqualinkClient.login", side_effect=AqualinkServiceUnauthorizedException, ): - result = await func(config_data) + result = await flow.async_step_user(config_data) assert result["type"] == "form" assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} -@pytest.mark.parametrize("step", ["import", "user"]) -async def test_service_exception(hass, config_data, step): +async def test_service_exception(hass, config_data): """Test config flow encountering service exception.""" flow = config_flow.AqualinkFlowHandler() flow.hass = hass - fname = f"async_step_{step}" - func = getattr(flow, fname) with patch( "homeassistant.components.iaqualink.config_flow.AqualinkClient.login", side_effect=AqualinkServiceException, ): - result = await func(config_data) + result = await flow.async_step_user(config_data) assert result["type"] == "form" assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} -@pytest.mark.parametrize("step", ["import", "user"]) -async def test_with_existing_config(hass, config_data, step): +async def test_with_existing_config(hass, config_data): """Test config flow with existing configuration.""" flow = config_flow.AqualinkFlowHandler() flow.hass = hass flow.context = {} - fname = f"async_step_{step}" - func = getattr(flow, fname) with patch( "homeassistant.components.iaqualink.config_flow.AqualinkClient.login", return_value=None, ): - result = await func(config_data) + result = await flow.async_step_user(config_data) assert result["type"] == "create_entry" assert result["title"] == config_data["username"] From 54f5238ef660738c5fcdd6f1f904f568311613ac Mon Sep 17 00:00:00 2001 From: j-a-n Date: Tue, 24 May 2022 11:03:49 +0200 Subject: [PATCH 0835/3516] Moehlenhoff alpha2 sensors (#72161) Co-authored-by: Franck Nijhof --- .coveragerc | 2 + .../components/moehlenhoff_alpha2/__init__.py | 12 +++- .../moehlenhoff_alpha2/binary_sensor.py | 56 +++++++++++++++++++ .../components/moehlenhoff_alpha2/climate.py | 39 ++++++++----- .../moehlenhoff_alpha2/manifest.json | 2 +- .../components/moehlenhoff_alpha2/sensor.py | 56 +++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 152 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/moehlenhoff_alpha2/binary_sensor.py create mode 100644 homeassistant/components/moehlenhoff_alpha2/sensor.py diff --git a/.coveragerc b/.coveragerc index d884e6e8c13..dd03c3782c0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -724,8 +724,10 @@ omit = homeassistant/components/modem_callerid/button.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/moehlenhoff_alpha2/__init__.py + homeassistant/components/moehlenhoff_alpha2/binary_sensor.py homeassistant/components/moehlenhoff_alpha2/climate.py homeassistant/components/moehlenhoff_alpha2/const.py + homeassistant/components/moehlenhoff_alpha2/sensor.py homeassistant/components/motion_blinds/__init__.py homeassistant/components/motion_blinds/const.py homeassistant/components/motion_blinds/cover.py diff --git a/homeassistant/components/moehlenhoff_alpha2/__init__.py b/homeassistant/components/moehlenhoff_alpha2/__init__.py index 62e18917dc6..93ddaa781ab 100644 --- a/homeassistant/components/moehlenhoff_alpha2/__init__.py +++ b/homeassistant/components/moehlenhoff_alpha2/__init__.py @@ -17,7 +17,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.CLIMATE] +PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.BINARY_SENSOR] UPDATE_INTERVAL = timedelta(seconds=60) @@ -65,10 +65,16 @@ class Alpha2BaseCoordinator(DataUpdateCoordinator[dict[str, dict]]): update_interval=UPDATE_INTERVAL, ) - async def _async_update_data(self) -> dict[str, dict]: + async def _async_update_data(self) -> dict[str, dict[str, dict]]: """Fetch the latest data from the source.""" await self.base.update_data() - return {ha["ID"]: ha for ha in self.base.heat_areas if ha.get("ID")} + return { + "heat_areas": {ha["ID"]: ha for ha in self.base.heat_areas if ha.get("ID")}, + "heat_controls": { + hc["ID"]: hc for hc in self.base.heat_controls if hc.get("ID") + }, + "io_devices": {io["ID"]: io for io in self.base.io_devices if io.get("ID")}, + } def get_cooling(self) -> bool: """Return if cooling mode is enabled.""" diff --git a/homeassistant/components/moehlenhoff_alpha2/binary_sensor.py b/homeassistant/components/moehlenhoff_alpha2/binary_sensor.py new file mode 100644 index 00000000000..ddd92c3a70b --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/binary_sensor.py @@ -0,0 +1,56 @@ +"""Support for Alpha2 IO device battery sensors.""" + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import Alpha2BaseCoordinator +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add Alpha2 sensor entities from a config_entry.""" + + coordinator: Alpha2BaseCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + Alpha2IODeviceBatterySensor(coordinator, io_device_id) + for io_device_id, io_device in coordinator.data["io_devices"].items() + if io_device["_HEATAREA_ID"] + ) + + +class Alpha2IODeviceBatterySensor( + CoordinatorEntity[Alpha2BaseCoordinator], BinarySensorEntity +): + """Alpha2 IO device battery binary sensor.""" + + _attr_device_class = BinarySensorDeviceClass.BATTERY + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__(self, coordinator: Alpha2BaseCoordinator, io_device_id: str) -> None: + """Initialize Alpha2IODeviceBatterySensor.""" + super().__init__(coordinator) + self.io_device_id = io_device_id + self._attr_unique_id = f"{io_device_id}:battery" + io_device = self.coordinator.data["io_devices"][io_device_id] + heat_area = self.coordinator.data["heat_areas"][io_device["_HEATAREA_ID"]] + self._attr_name = ( + f"{heat_area['HEATAREA_NAME']} IO device {io_device['NR']} battery" + ) + + @property + def is_on(self): + """Return the state of the sensor.""" + # 0=empty, 1=weak, 2=good + return self.coordinator.data["io_devices"][self.io_device_id]["BATTERY"] < 2 diff --git a/homeassistant/components/moehlenhoff_alpha2/climate.py b/homeassistant/components/moehlenhoff_alpha2/climate.py index 225735293ca..e14f4801661 100644 --- a/homeassistant/components/moehlenhoff_alpha2/climate.py +++ b/homeassistant/components/moehlenhoff_alpha2/climate.py @@ -29,7 +29,8 @@ async def async_setup_entry( coordinator: Alpha2BaseCoordinator = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( - Alpha2Climate(coordinator, heat_area_id) for heat_area_id in coordinator.data + Alpha2Climate(coordinator, heat_area_id) + for heat_area_id in coordinator.data["heat_areas"] ) @@ -51,26 +52,34 @@ class Alpha2Climate(CoordinatorEntity[Alpha2BaseCoordinator], ClimateEntity): super().__init__(coordinator) self.heat_area_id = heat_area_id self._attr_unique_id = heat_area_id - - @property - def name(self) -> str: - """Return the name of the climate device.""" - return self.coordinator.data[self.heat_area_id]["HEATAREA_NAME"] + self._attr_name = self.coordinator.data["heat_areas"][heat_area_id][ + "HEATAREA_NAME" + ] @property def min_temp(self) -> float: """Return the minimum temperature.""" - return float(self.coordinator.data[self.heat_area_id].get("T_TARGET_MIN", 0.0)) + return float( + self.coordinator.data["heat_areas"][self.heat_area_id].get( + "T_TARGET_MIN", 0.0 + ) + ) @property def max_temp(self) -> float: """Return the maximum temperature.""" - return float(self.coordinator.data[self.heat_area_id].get("T_TARGET_MAX", 30.0)) + return float( + self.coordinator.data["heat_areas"][self.heat_area_id].get( + "T_TARGET_MAX", 30.0 + ) + ) @property def current_temperature(self) -> float: """Return the current temperature.""" - return float(self.coordinator.data[self.heat_area_id].get("T_ACTUAL", 0.0)) + return float( + self.coordinator.data["heat_areas"][self.heat_area_id].get("T_ACTUAL", 0.0) + ) @property def hvac_mode(self) -> HVACMode: @@ -86,7 +95,9 @@ class Alpha2Climate(CoordinatorEntity[Alpha2BaseCoordinator], ClimateEntity): @property def hvac_action(self) -> HVACAction: """Return the current running hvac operation.""" - if not self.coordinator.data[self.heat_area_id]["_HEATCTRL_STATE"]: + if not self.coordinator.data["heat_areas"][self.heat_area_id][ + "_HEATCTRL_STATE" + ]: return HVACAction.IDLE if self.coordinator.get_cooling(): return HVACAction.COOLING @@ -95,7 +106,9 @@ class Alpha2Climate(CoordinatorEntity[Alpha2BaseCoordinator], ClimateEntity): @property def target_temperature(self) -> float: """Return the temperature we try to reach.""" - return float(self.coordinator.data[self.heat_area_id].get("T_TARGET", 0.0)) + return float( + self.coordinator.data["heat_areas"][self.heat_area_id].get("T_TARGET", 0.0) + ) async def async_set_temperature(self, **kwargs) -> None: """Set new target temperatures.""" @@ -109,9 +122,9 @@ class Alpha2Climate(CoordinatorEntity[Alpha2BaseCoordinator], ClimateEntity): @property def preset_mode(self) -> str: """Return the current preset mode.""" - if self.coordinator.data[self.heat_area_id]["HEATAREA_MODE"] == 1: + if self.coordinator.data["heat_areas"][self.heat_area_id]["HEATAREA_MODE"] == 1: return PRESET_DAY - if self.coordinator.data[self.heat_area_id]["HEATAREA_MODE"] == 2: + if self.coordinator.data["heat_areas"][self.heat_area_id]["HEATAREA_MODE"] == 2: return PRESET_NIGHT return PRESET_AUTO diff --git a/homeassistant/components/moehlenhoff_alpha2/manifest.json b/homeassistant/components/moehlenhoff_alpha2/manifest.json index db362163e72..12e7a927906 100644 --- a/homeassistant/components/moehlenhoff_alpha2/manifest.json +++ b/homeassistant/components/moehlenhoff_alpha2/manifest.json @@ -3,7 +3,7 @@ "name": "Möhlenhoff Alpha 2", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/moehlenhoff_alpha2", - "requirements": ["moehlenhoff-alpha2==1.1.2"], + "requirements": ["moehlenhoff-alpha2==1.2.1"], "iot_class": "local_push", "codeowners": ["@j-a-n"] } diff --git a/homeassistant/components/moehlenhoff_alpha2/sensor.py b/homeassistant/components/moehlenhoff_alpha2/sensor.py new file mode 100644 index 00000000000..d26786e1923 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/sensor.py @@ -0,0 +1,56 @@ +"""Support for Alpha2 heat control valve opening sensors.""" + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import Alpha2BaseCoordinator +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add Alpha2 sensor entities from a config_entry.""" + + coordinator: Alpha2BaseCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + # HEATCTRL attribute ACTOR_PERCENT is not available in older firmware versions + async_add_entities( + Alpha2HeatControlValveOpeningSensor(coordinator, heat_control_id) + for heat_control_id, heat_control in coordinator.data["heat_controls"].items() + if heat_control["INUSE"] + and heat_control["_HEATAREA_ID"] + and heat_control.get("ACTOR_PERCENT") is not None + ) + + +class Alpha2HeatControlValveOpeningSensor( + CoordinatorEntity[Alpha2BaseCoordinator], SensorEntity +): + """Alpha2 heat control valve opening sensor.""" + + _attr_native_unit_of_measurement = PERCENTAGE + + def __init__( + self, coordinator: Alpha2BaseCoordinator, heat_control_id: str + ) -> None: + """Initialize Alpha2HeatControlValveOpeningSensor.""" + super().__init__(coordinator) + self.heat_control_id = heat_control_id + self._attr_unique_id = f"{heat_control_id}:valve_opening" + heat_control = self.coordinator.data["heat_controls"][heat_control_id] + heat_area = self.coordinator.data["heat_areas"][heat_control["_HEATAREA_ID"]] + self._attr_name = f"{heat_area['HEATAREA_NAME']} heat control {heat_control['NR']} valve opening" + + @property + def native_value(self) -> int: + """Return the current valve opening percentage.""" + return self.coordinator.data["heat_controls"][self.heat_control_id][ + "ACTOR_PERCENT" + ] diff --git a/requirements_all.txt b/requirements_all.txt index b5c731367c5..9bfc8b3671e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1035,7 +1035,7 @@ minio==5.0.10 mitemp_bt==0.0.5 # homeassistant.components.moehlenhoff_alpha2 -moehlenhoff-alpha2==1.1.2 +moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds motionblinds==0.6.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f862c719f50..1e6700d3add 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -712,7 +712,7 @@ millheater==0.9.0 minio==5.0.10 # homeassistant.components.moehlenhoff_alpha2 -moehlenhoff-alpha2==1.1.2 +moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds motionblinds==0.6.7 From d620072585053e87c63b3413f63d00c08ba6bb95 Mon Sep 17 00:00:00 2001 From: Greg Dowling Date: Tue, 24 May 2022 14:16:02 +0100 Subject: [PATCH 0836/3516] Remove pavoni as vera codeowner (#72421) --- CODEOWNERS | 2 -- homeassistant/components/vera/manifest.json | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 05e0d0b4377..887534e3461 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1116,8 +1116,6 @@ build.json @home-assistant/supervisor /homeassistant/components/velux/ @Julius2342 /homeassistant/components/venstar/ @garbled1 /tests/components/venstar/ @garbled1 -/homeassistant/components/vera/ @pavoni -/tests/components/vera/ @pavoni /homeassistant/components/verisure/ @frenck /tests/components/verisure/ @frenck /homeassistant/components/versasense/ @flamm3blemuff1n diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index 5a87ae29483..517d19a89d9 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/vera", "requirements": ["pyvera==0.3.13"], - "codeowners": ["@pavoni"], + "codeowners": [], "iot_class": "local_polling", "loggers": ["pyvera"] } From 23bd64b7a26459e3e19b2d7fc05228214dd65a23 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 24 May 2022 15:34:46 +0200 Subject: [PATCH 0837/3516] Prevent duplication of statistics metadata (#71637) * Prevent duplication of statistics metadata * Add models_schema_28.py * Handle entity renaming as a recorder job * Improve tests --- homeassistant/components/recorder/core.py | 13 +- .../components/recorder/migration.py | 23 +- homeassistant/components/recorder/models.py | 4 +- .../components/recorder/statistics.py | 106 ++- homeassistant/components/recorder/tasks.py | 9 +- .../components/recorder/websocket_api.py | 2 +- tests/components/recorder/models_schema_28.py | 753 ++++++++++++++++++ tests/components/recorder/test_statistics.py | 297 ++++++- 8 files changed, 1175 insertions(+), 32 deletions(-) create mode 100644 tests/components/recorder/models_schema_28.py diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 2ba07e42f49..7df4cf57e56 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -35,6 +35,7 @@ from homeassistant.helpers.event import ( async_track_time_interval, async_track_utc_time_change, ) +from homeassistant.helpers.typing import UNDEFINED, UndefinedType import homeassistant.util.dt as dt_util from . import migration, statistics @@ -461,10 +462,18 @@ class Recorder(threading.Thread): @callback def async_update_statistics_metadata( - self, statistic_id: str, unit_of_measurement: str | None + self, + statistic_id: str, + *, + new_statistic_id: str | UndefinedType = UNDEFINED, + new_unit_of_measurement: str | None | UndefinedType = UNDEFINED, ) -> None: """Update statistics metadata for a statistic_id.""" - self.queue_task(UpdateStatisticsMetadataTask(statistic_id, unit_of_measurement)) + self.queue_task( + UpdateStatisticsMetadataTask( + statistic_id, new_statistic_id, new_unit_of_measurement + ) + ) @callback def async_external_statistics( diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 5da31f18781..eadfc543b59 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -33,7 +33,11 @@ from .models import ( StatisticsShortTerm, process_timestamp, ) -from .statistics import delete_duplicates, get_start_time +from .statistics import ( + delete_statistics_duplicates, + delete_statistics_meta_duplicates, + get_start_time, +) from .util import session_scope _LOGGER = logging.getLogger(__name__) @@ -670,7 +674,7 @@ def _apply_update( # noqa: C901 # There may be duplicated statistics entries, delete duplicated statistics # and try again with session_scope(session=session_maker()) as session: - delete_duplicates(hass, session) + delete_statistics_duplicates(hass, session) _create_index( session_maker, "statistics", "ix_statistics_statistic_id_start" ) @@ -705,6 +709,21 @@ def _apply_update( # noqa: C901 _create_index(session_maker, "states", "ix_states_context_id") # Once there are no longer any state_changed events # in the events table we can drop the index on states.event_id + elif new_version == 29: + # Recreate statistics_meta index to block duplicated statistic_id + _drop_index(session_maker, "statistics_meta", "ix_statistics_meta_statistic_id") + try: + _create_index( + session_maker, "statistics_meta", "ix_statistics_meta_statistic_id" + ) + except DatabaseError: + # There may be duplicated statistics_meta entries, delete duplicates + # and try again + with session_scope(session=session_maker()) as session: + delete_statistics_meta_duplicates(session) + _create_index( + session_maker, "statistics_meta", "ix_statistics_meta_statistic_id" + ) 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 4dabd7899e0..90c2e5e5616 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -51,7 +51,7 @@ from .const import ALL_DOMAIN_EXCLUDE_ATTRS, JSON_DUMP # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 28 +SCHEMA_VERSION = 29 _LOGGER = logging.getLogger(__name__) @@ -515,7 +515,7 @@ class StatisticsMeta(Base): # type: ignore[misc,valid-type] ) __tablename__ = TABLE_STATISTICS_META id = Column(Integer, Identity(), primary_key=True) - statistic_id = Column(String(255), index=True) + statistic_id = Column(String(255), index=True, unique=True) source = Column(String(32)) unit_of_measurement = Column(String(255)) has_mean = Column(Boolean) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 2c6c51a31f4..4bed39fee4a 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -33,6 +33,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.storage import STORAGE_DIR +from homeassistant.helpers.typing import UNDEFINED, UndefinedType import homeassistant.util.dt as dt_util import homeassistant.util.pressure as pressure_util import homeassistant.util.temperature as temperature_util @@ -208,18 +209,11 @@ class ValidationIssue: def async_setup(hass: HomeAssistant) -> None: """Set up the history hooks.""" - def _entity_id_changed(event: Event) -> None: - """Handle entity_id changed.""" - old_entity_id = event.data["old_entity_id"] - entity_id = event.data["entity_id"] - with session_scope(hass=hass) as session: - session.query(StatisticsMeta).filter( - (StatisticsMeta.statistic_id == old_entity_id) - & (StatisticsMeta.source == DOMAIN) - ).update({StatisticsMeta.statistic_id: entity_id}) - - async def _async_entity_id_changed(event: Event) -> None: - await hass.data[DATA_INSTANCE].async_add_executor_job(_entity_id_changed, event) + @callback + def _async_entity_id_changed(event: Event) -> None: + hass.data[DATA_INSTANCE].async_update_statistics_metadata( + event.data["old_entity_id"], new_statistic_id=event.data["entity_id"] + ) @callback def entity_registry_changed_filter(event: Event) -> bool: @@ -380,7 +374,7 @@ def _delete_duplicates_from_table( return (total_deleted_rows, all_non_identical_duplicates) -def delete_duplicates(hass: HomeAssistant, session: Session) -> None: +def delete_statistics_duplicates(hass: HomeAssistant, session: Session) -> None: """Identify and delete duplicated statistics. A backup will be made of duplicated statistics before it is deleted. @@ -423,6 +417,69 @@ def delete_duplicates(hass: HomeAssistant, session: Session) -> None: ) +def _find_statistics_meta_duplicates(session: Session) -> list[int]: + """Find duplicated statistics_meta.""" + subquery = ( + session.query( + StatisticsMeta.statistic_id, + literal_column("1").label("is_duplicate"), + ) + .group_by(StatisticsMeta.statistic_id) + .having(func.count() > 1) + .subquery() + ) + query = ( + session.query(StatisticsMeta) + .outerjoin( + subquery, + (subquery.c.statistic_id == StatisticsMeta.statistic_id), + ) + .filter(subquery.c.is_duplicate == 1) + .order_by(StatisticsMeta.statistic_id, StatisticsMeta.id.desc()) + .limit(1000 * MAX_ROWS_TO_PURGE) + ) + duplicates = execute(query) + statistic_id = None + duplicate_ids: list[int] = [] + + if not duplicates: + return duplicate_ids + + for duplicate in duplicates: + if statistic_id != duplicate.statistic_id: + statistic_id = duplicate.statistic_id + continue + duplicate_ids.append(duplicate.id) + + return duplicate_ids + + +def _delete_statistics_meta_duplicates(session: Session) -> int: + """Identify and delete duplicated statistics from a specified table.""" + total_deleted_rows = 0 + while True: + duplicate_ids = _find_statistics_meta_duplicates(session) + if not duplicate_ids: + break + for i in range(0, len(duplicate_ids), MAX_ROWS_TO_PURGE): + deleted_rows = ( + session.query(StatisticsMeta) + .filter(StatisticsMeta.id.in_(duplicate_ids[i : i + MAX_ROWS_TO_PURGE])) + .delete(synchronize_session=False) + ) + total_deleted_rows += deleted_rows + return total_deleted_rows + + +def delete_statistics_meta_duplicates(session: Session) -> None: + """Identify and delete duplicated statistics_meta.""" + deleted_statistics_rows = _delete_statistics_meta_duplicates(session) + if deleted_statistics_rows: + _LOGGER.info( + "Deleted %s duplicated statistics_meta rows", deleted_statistics_rows + ) + + def _compile_hourly_statistics_summary_mean_stmt( start_time: datetime, end_time: datetime ) -> StatementLambdaElement: @@ -736,13 +793,26 @@ def clear_statistics(instance: Recorder, statistic_ids: list[str]) -> None: def update_statistics_metadata( - instance: Recorder, statistic_id: str, unit_of_measurement: str | None + instance: Recorder, + statistic_id: str, + new_statistic_id: str | None | UndefinedType, + new_unit_of_measurement: str | None | UndefinedType, ) -> None: """Update statistics metadata for a statistic_id.""" - with session_scope(session=instance.get_session()) as session: - session.query(StatisticsMeta).filter( - StatisticsMeta.statistic_id == statistic_id - ).update({StatisticsMeta.unit_of_measurement: unit_of_measurement}) + if new_unit_of_measurement is not UNDEFINED: + with session_scope(session=instance.get_session()) as session: + session.query(StatisticsMeta).filter( + StatisticsMeta.statistic_id == statistic_id + ).update({StatisticsMeta.unit_of_measurement: new_unit_of_measurement}) + if new_statistic_id is not UNDEFINED: + with session_scope( + session=instance.get_session(), + exception_filter=_filter_unique_constraint_integrity_error(instance), + ) as session: + session.query(StatisticsMeta).filter( + (StatisticsMeta.statistic_id == statistic_id) + & (StatisticsMeta.source == DOMAIN) + ).update({StatisticsMeta.statistic_id: new_statistic_id}) def list_statistic_ids( diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index 07855d27dff..5ec83a3cefc 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -10,6 +10,7 @@ import threading from typing import TYPE_CHECKING, Any from homeassistant.core import Event +from homeassistant.helpers.typing import UndefinedType from . import purge, statistics from .const import DOMAIN, EXCLUDE_ATTRIBUTES @@ -46,12 +47,16 @@ class UpdateStatisticsMetadataTask(RecorderTask): """Object to store statistics_id and unit for update of statistics metadata.""" statistic_id: str - unit_of_measurement: str | None + new_statistic_id: str | None | UndefinedType + new_unit_of_measurement: str | None | UndefinedType def run(self, instance: Recorder) -> None: """Handle the task.""" statistics.update_statistics_metadata( - instance, self.statistic_id, self.unit_of_measurement + instance, + self.statistic_id, + self.new_statistic_id, + self.new_unit_of_measurement, ) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index a851d2681f4..d0499fbf9cb 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -103,7 +103,7 @@ def ws_update_statistics_metadata( ) -> None: """Update statistics metadata for a statistic_id.""" hass.data[DATA_INSTANCE].async_update_statistics_metadata( - msg["statistic_id"], msg["unit_of_measurement"] + msg["statistic_id"], new_unit_of_measurement=msg["unit_of_measurement"] ) connection.send_result(msg["id"]) diff --git a/tests/components/recorder/models_schema_28.py b/tests/components/recorder/models_schema_28.py new file mode 100644 index 00000000000..8d2de0432ac --- /dev/null +++ b/tests/components/recorder/models_schema_28.py @@ -0,0 +1,753 @@ +"""Models for SQLAlchemy. + +This file contains the model definitions for schema version 28. +It is used to test the schema migration logic. +""" +from __future__ import annotations + +from datetime import datetime, timedelta +import json +import logging +from typing import Any, TypedDict, cast, overload + +from fnvhash import fnv1a_32 +from sqlalchemy import ( + BigInteger, + Boolean, + Column, + DateTime, + Float, + ForeignKey, + Identity, + Index, + Integer, + SmallInteger, + String, + Text, + distinct, +) +from sqlalchemy.dialects import mysql, oracle, postgresql +from sqlalchemy.engine.row import Row +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.orm import declarative_base, relationship +from sqlalchemy.orm.session import Session + +from homeassistant.components.recorder.const import ALL_DOMAIN_EXCLUDE_ATTRS, JSON_DUMP +from homeassistant.const import ( + MAX_LENGTH_EVENT_CONTEXT_ID, + MAX_LENGTH_EVENT_EVENT_TYPE, + MAX_LENGTH_EVENT_ORIGIN, + MAX_LENGTH_STATE_ENTITY_ID, + MAX_LENGTH_STATE_STATE, +) +from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id +import homeassistant.util.dt as dt_util + +# SQLAlchemy Schema +# pylint: disable=invalid-name +Base = declarative_base() + +SCHEMA_VERSION = 28 + +_LOGGER = logging.getLogger(__name__) + +DB_TIMEZONE = "+00:00" + +TABLE_EVENTS = "events" +TABLE_EVENT_DATA = "event_data" +TABLE_STATES = "states" +TABLE_STATE_ATTRIBUTES = "state_attributes" +TABLE_RECORDER_RUNS = "recorder_runs" +TABLE_SCHEMA_CHANGES = "schema_changes" +TABLE_STATISTICS = "statistics" +TABLE_STATISTICS_META = "statistics_meta" +TABLE_STATISTICS_RUNS = "statistics_runs" +TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" + +ALL_TABLES = [ + TABLE_STATES, + TABLE_STATE_ATTRIBUTES, + TABLE_EVENTS, + TABLE_EVENT_DATA, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + TABLE_STATISTICS, + TABLE_STATISTICS_META, + TABLE_STATISTICS_RUNS, + TABLE_STATISTICS_SHORT_TERM, +] + +TABLES_TO_CHECK = [ + TABLE_STATES, + TABLE_EVENTS, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, +] + + +EMPTY_JSON_OBJECT = "{}" + + +DATETIME_TYPE = DateTime(timezone=True).with_variant( + mysql.DATETIME(timezone=True, fsp=6), "mysql" +) +DOUBLE_TYPE = ( + Float() + .with_variant(mysql.DOUBLE(asdecimal=False), "mysql") + .with_variant(oracle.DOUBLE_PRECISION(), "oracle") + .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") +) +EVENT_ORIGIN_ORDER = [EventOrigin.local, EventOrigin.remote] +EVENT_ORIGIN_TO_IDX = {origin: idx for idx, origin in enumerate(EVENT_ORIGIN_ORDER)} + + +class Events(Base): # type: ignore[misc,valid-type] + """Event history data.""" + + __table_args__ = ( + # Used for fetching events at a specific time + # see logbook + Index("ix_events_event_type_time_fired", "event_type", "time_fired"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_EVENTS + event_id = Column(Integer, Identity(), primary_key=True) # no longer used + event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) + event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) # no longer used + origin_idx = Column(SmallInteger) + time_fired = Column(DATETIME_TYPE, index=True) + context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) + context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) + data_id = Column(Integer, ForeignKey("event_data.data_id"), index=True) + event_data_rel = relationship("EventData") + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> Events: + """Create an event database object from a native event.""" + return Events( + event_type=event.event_type, + event_data=None, + origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), + time_fired=event.time_fired, + context_id=event.context.id, + context_user_id=event.context.user_id, + context_parent_id=event.context.parent_id, + ) + + def to_native(self, validate_entity_id: bool = True) -> Event | None: + """Convert to a native HA Event.""" + context = Context( + id=self.context_id, + user_id=self.context_user_id, + parent_id=self.context_parent_id, + ) + try: + return Event( + self.event_type, + json.loads(self.event_data) if self.event_data else {}, + EventOrigin(self.origin) + if self.origin + else EVENT_ORIGIN_ORDER[self.origin_idx], + process_timestamp(self.time_fired), + context=context, + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting to event: %s", self) + return None + + +class EventData(Base): # type: ignore[misc,valid-type] + """Event data history.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_EVENT_DATA + data_id = Column(Integer, Identity(), primary_key=True) + hash = Column(BigInteger, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> EventData: + """Create object from an event.""" + shared_data = JSON_DUMP(event.data) + return EventData( + shared_data=shared_data, hash=EventData.hash_shared_data(shared_data) + ) + + @staticmethod + def shared_data_from_event(event: Event) -> str: + """Create shared_attrs from an event.""" + return JSON_DUMP(event.data) + + @staticmethod + def hash_shared_data(shared_data: str) -> int: + """Return the hash of json encoded shared data.""" + return cast(int, fnv1a_32(shared_data.encode("utf-8"))) + + def to_native(self) -> dict[str, Any]: + """Convert to an HA state object.""" + try: + return cast(dict[str, Any], json.loads(self.shared_data)) + except ValueError: + _LOGGER.exception("Error converting row to event data: %s", self) + return {} + + +class States(Base): # type: ignore[misc,valid-type] + """State change history.""" + + __table_args__ = ( + # Used for fetching the state of entities at a specific time + # (get_states in history.py) + Index("ix_states_entity_id_last_updated", "entity_id", "last_updated"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATES + state_id = Column(Integer, Identity(), primary_key=True) + entity_id = Column(String(MAX_LENGTH_STATE_ENTITY_ID)) + state = Column(String(MAX_LENGTH_STATE_STATE)) + attributes = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + event_id = Column( + Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True + ) + last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) + last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) + old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) + attributes_id = Column( + Integer, ForeignKey("state_attributes.attributes_id"), index=True + ) + context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) + context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) + origin_idx = Column(SmallInteger) # 0 is local, 1 is remote + old_state = relationship("States", remote_side=[state_id]) + state_attributes = relationship("StateAttributes") + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> States: + """Create object from a state_changed event.""" + entity_id = event.data["entity_id"] + state: State | None = event.data.get("new_state") + dbstate = States( + entity_id=entity_id, + attributes=None, + context_id=event.context.id, + context_user_id=event.context.user_id, + context_parent_id=event.context.parent_id, + origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), + ) + + # None state means the state was removed from the state machine + if state is None: + dbstate.state = "" + dbstate.last_changed = event.time_fired + dbstate.last_updated = event.time_fired + else: + dbstate.state = state.state + dbstate.last_changed = state.last_changed + dbstate.last_updated = state.last_updated + + return dbstate + + def to_native(self, validate_entity_id: bool = True) -> State | None: + """Convert to an HA state object.""" + context = Context( + id=self.context_id, + user_id=self.context_user_id, + parent_id=self.context_parent_id, + ) + try: + return State( + self.entity_id, + self.state, + # Join the state_attributes table on attributes_id to get the attributes + # for newer states + json.loads(self.attributes) if self.attributes else {}, + process_timestamp(self.last_changed), + process_timestamp(self.last_updated), + context=context, + validate_entity_id=validate_entity_id, + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state: %s", self) + return None + + +class StateAttributes(Base): # type: ignore[misc,valid-type] + """State attribute change history.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATE_ATTRIBUTES + attributes_id = Column(Integer, Identity(), primary_key=True) + hash = Column(BigInteger, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_attrs = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> StateAttributes: + """Create object from a state_changed event.""" + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + dbstate = StateAttributes( + shared_attrs="{}" if state is None else JSON_DUMP(state.attributes) + ) + dbstate.hash = StateAttributes.hash_shared_attrs(dbstate.shared_attrs) + return dbstate + + @staticmethod + def shared_attrs_from_event( + event: Event, exclude_attrs_by_domain: dict[str, set[str]] + ) -> str: + """Create shared_attrs from a state_changed event.""" + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + if state is None: + return "{}" + domain = split_entity_id(state.entity_id)[0] + exclude_attrs = ( + exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS + ) + return JSON_DUMP( + {k: v for k, v in state.attributes.items() if k not in exclude_attrs} + ) + + @staticmethod + def hash_shared_attrs(shared_attrs: str) -> int: + """Return the hash of json encoded shared attributes.""" + return cast(int, fnv1a_32(shared_attrs.encode("utf-8"))) + + def to_native(self) -> dict[str, Any]: + """Convert to an HA state object.""" + try: + return cast(dict[str, Any], json.loads(self.shared_attrs)) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state attributes: %s", self) + return {} + + +class StatisticResult(TypedDict): + """Statistic result data class. + + Allows multiple datapoints for the same statistic_id. + """ + + meta: StatisticMetaData + stat: StatisticData + + +class StatisticDataBase(TypedDict): + """Mandatory fields for statistic data class.""" + + start: datetime + + +class StatisticData(StatisticDataBase, total=False): + """Statistic data class.""" + + mean: float + min: float + max: float + last_reset: datetime | None + state: float + sum: float + + +class StatisticsBase: + """Statistics base class.""" + + id = Column(Integer, Identity(), primary_key=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + + @declared_attr # type: ignore[misc] + def metadata_id(self) -> Column: + """Define the metadata_id column for sub classes.""" + return Column( + Integer, + ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), + index=True, + ) + + start = Column(DATETIME_TYPE, index=True) + mean = Column(DOUBLE_TYPE) + min = Column(DOUBLE_TYPE) + max = Column(DOUBLE_TYPE) + last_reset = Column(DATETIME_TYPE) + state = Column(DOUBLE_TYPE) + sum = Column(DOUBLE_TYPE) + + @classmethod + def from_stats(cls, metadata_id: int, stats: StatisticData) -> StatisticsBase: + """Create object from a statistics.""" + return cls( # type: ignore[call-arg,misc] + metadata_id=metadata_id, + **stats, + ) + + +class Statistics(Base, StatisticsBase): # type: ignore[misc,valid-type] + """Long term statistics.""" + + duration = timedelta(hours=1) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index("ix_statistics_statistic_id_start", "metadata_id", "start", unique=True), + ) + __tablename__ = TABLE_STATISTICS + + +class StatisticsShortTerm(Base, StatisticsBase): # type: ignore[misc,valid-type] + """Short term statistics.""" + + duration = timedelta(minutes=5) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index( + "ix_statistics_short_term_statistic_id_start", + "metadata_id", + "start", + unique=True, + ), + ) + __tablename__ = TABLE_STATISTICS_SHORT_TERM + + +class StatisticMetaData(TypedDict): + """Statistic meta data class.""" + + has_mean: bool + has_sum: bool + name: str | None + source: str + statistic_id: str + unit_of_measurement: str | None + + +class StatisticsMeta(Base): # type: ignore[misc,valid-type] + """Statistics meta data.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATISTICS_META + id = Column(Integer, Identity(), primary_key=True) + statistic_id = Column(String(255), index=True) + source = Column(String(32)) + unit_of_measurement = Column(String(255)) + has_mean = Column(Boolean) + has_sum = Column(Boolean) + name = Column(String(255)) + + @staticmethod + def from_meta(meta: StatisticMetaData) -> StatisticsMeta: + """Create object from meta data.""" + return StatisticsMeta(**meta) + + +class RecorderRuns(Base): # type: ignore[misc,valid-type] + """Representation of recorder run.""" + + __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) + __tablename__ = TABLE_RECORDER_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True), default=dt_util.utcnow) + end = Column(DateTime(timezone=True)) + closed_incorrect = Column(Boolean, default=False) + created = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + end = ( + f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None + ) + return ( + f"" + ) + + def entity_ids(self, point_in_time: datetime | None = None) -> list[str]: + """Return the entity ids that existed in this run. + + Specify point_in_time if you want to know which existed at that point + in time inside the run. + """ + session = Session.object_session(self) + + assert session is not None, "RecorderRuns need to be persisted" + + query = session.query(distinct(States.entity_id)).filter( + States.last_updated >= self.start + ) + + if point_in_time is not None: + query = query.filter(States.last_updated < point_in_time) + elif self.end is not None: + query = query.filter(States.last_updated < self.end) + + return [row[0] for row in query] + + def to_native(self, validate_entity_id: bool = True) -> RecorderRuns: + """Return self, native format is this model.""" + return self + + +class SchemaChanges(Base): # type: ignore[misc,valid-type] + """Representation of schema version changes.""" + + __tablename__ = TABLE_SCHEMA_CHANGES + change_id = Column(Integer, Identity(), primary_key=True) + schema_version = Column(Integer) + changed = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +class StatisticsRuns(Base): # type: ignore[misc,valid-type] + """Representation of statistics run.""" + + __tablename__ = TABLE_STATISTICS_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True), index=True) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +@overload +def process_timestamp(ts: None) -> None: + ... + + +@overload +def process_timestamp(ts: datetime) -> datetime: + ... + + +def process_timestamp(ts: datetime | None) -> datetime | None: + """Process a timestamp into datetime object.""" + if ts is None: + return None + if ts.tzinfo is None: + return ts.replace(tzinfo=dt_util.UTC) + + return dt_util.as_utc(ts) + + +@overload +def process_timestamp_to_utc_isoformat(ts: None) -> None: + ... + + +@overload +def process_timestamp_to_utc_isoformat(ts: datetime) -> str: + ... + + +def process_timestamp_to_utc_isoformat(ts: datetime | None) -> str | None: + """Process a timestamp into UTC isotime.""" + if ts is None: + return None + if ts.tzinfo == dt_util.UTC: + return ts.isoformat() + if ts.tzinfo is None: + return f"{ts.isoformat()}{DB_TIMEZONE}" + return ts.astimezone(dt_util.UTC).isoformat() + + +class LazyState(State): + """A lazy version of core State.""" + + __slots__ = [ + "_row", + "_attributes", + "_last_changed", + "_last_updated", + "_context", + "_attr_cache", + ] + + def __init__( # pylint: disable=super-init-not-called + self, row: Row, attr_cache: dict[str, dict[str, Any]] | None = None + ) -> None: + """Init the lazy state.""" + self._row = row + self.entity_id: str = self._row.entity_id + self.state = self._row.state or "" + self._attributes: dict[str, Any] | None = None + self._last_changed: datetime | None = None + self._last_updated: datetime | None = None + self._context: Context | None = None + self._attr_cache = attr_cache + + @property # type: ignore[override] + def attributes(self) -> dict[str, Any]: # type: ignore[override] + """State attributes.""" + if self._attributes is None: + source = self._row.shared_attrs or self._row.attributes + if self._attr_cache is not None and ( + attributes := self._attr_cache.get(source) + ): + self._attributes = attributes + return attributes + if source == EMPTY_JSON_OBJECT or source is None: + self._attributes = {} + return self._attributes + try: + self._attributes = json.loads(source) + except ValueError: + # When json.loads fails + _LOGGER.exception( + "Error converting row to state attributes: %s", self._row + ) + self._attributes = {} + if self._attr_cache is not None: + self._attr_cache[source] = self._attributes + return self._attributes + + @attributes.setter + def attributes(self, value: dict[str, Any]) -> None: + """Set attributes.""" + self._attributes = value + + @property # type: ignore[override] + def context(self) -> Context: # type: ignore[override] + """State context.""" + if self._context is None: + self._context = Context(id=None) # type: ignore[arg-type] + return self._context + + @context.setter + def context(self, value: Context) -> None: + """Set context.""" + self._context = value + + @property # type: ignore[override] + def last_changed(self) -> datetime: # type: ignore[override] + """Last changed datetime.""" + if self._last_changed is None: + self._last_changed = process_timestamp(self._row.last_changed) + return self._last_changed + + @last_changed.setter + def last_changed(self, value: datetime) -> None: + """Set last changed datetime.""" + self._last_changed = value + + @property # type: ignore[override] + def last_updated(self) -> datetime: # type: ignore[override] + """Last updated datetime.""" + if self._last_updated is None: + if (last_updated := self._row.last_updated) is not None: + self._last_updated = process_timestamp(last_updated) + else: + self._last_updated = self.last_changed + return self._last_updated + + @last_updated.setter + def last_updated(self, value: datetime) -> None: + """Set last updated datetime.""" + self._last_updated = value + + def as_dict(self) -> dict[str, Any]: # type: ignore[override] + """Return a dict representation of the LazyState. + + Async friendly. + + To be used for JSON serialization. + """ + if self._last_changed is None and self._last_updated is None: + last_changed_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_changed + ) + if ( + self._row.last_updated is None + or self._row.last_changed == self._row.last_updated + ): + last_updated_isoformat = last_changed_isoformat + else: + last_updated_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_updated + ) + else: + last_changed_isoformat = self.last_changed.isoformat() + if self.last_changed == self.last_updated: + last_updated_isoformat = last_changed_isoformat + else: + last_updated_isoformat = self.last_updated.isoformat() + return { + "entity_id": self.entity_id, + "state": self.state, + "attributes": self._attributes or self.attributes, + "last_changed": last_changed_isoformat, + "last_updated": last_updated_isoformat, + } + + def __eq__(self, other: Any) -> bool: + """Return the comparison.""" + return ( + other.__class__ in [self.__class__, State] + and self.entity_id == other.entity_id + and self.state == other.state + and self.attributes == other.attributes + ) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 28eba51a4f3..882f00d2940 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -1,21 +1,26 @@ """The tests for sensor recorder platform.""" # pylint: disable=protected-access,invalid-name from datetime import timedelta +import importlib +import sys from unittest.mock import patch, sentinel import pytest from pytest import approx +from sqlalchemy import create_engine +from sqlalchemy.orm import Session from homeassistant.components import recorder from homeassistant.components.recorder import history, statistics -from homeassistant.components.recorder.const import DATA_INSTANCE +from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX from homeassistant.components.recorder.models import ( StatisticsShortTerm, process_timestamp_to_utc_isoformat, ) from homeassistant.components.recorder.statistics import ( async_add_external_statistics, - delete_duplicates, + delete_statistics_duplicates, + delete_statistics_meta_duplicates, get_last_short_term_statistics, get_last_statistics, get_latest_short_term_statistics, @@ -32,7 +37,7 @@ import homeassistant.util.dt as dt_util from .common import async_wait_recording_done, do_adhoc_statistics -from tests.common import mock_registry +from tests.common import get_test_home_assistant, mock_registry from tests.components.recorder.common import wait_recording_done ORIG_TZ = dt_util.DEFAULT_TIME_ZONE @@ -306,12 +311,92 @@ def test_rename_entity(hass_recorder): entity_reg.async_update_entity("sensor.test1", new_entity_id="sensor.test99") hass.add_job(rename_entry) - hass.block_till_done() + wait_recording_done(hass) stats = statistics_during_period(hass, zero, period="5minute") assert stats == {"sensor.test99": expected_stats99, "sensor.test2": expected_stats2} +def test_rename_entity_collision(hass_recorder, caplog): + """Test statistics is migrated when entity_id is changed.""" + hass = hass_recorder() + setup_component(hass, "sensor", {}) + + entity_reg = mock_registry(hass) + + @callback + def add_entry(): + reg_entry = entity_reg.async_get_or_create( + "sensor", + "test", + "unique_0000", + suggested_object_id="test1", + ) + assert reg_entry.entity_id == "sensor.test1" + + hass.add_job(add_entry) + hass.block_till_done() + + zero, four, states = record_states(hass) + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + + for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}): + stats = statistics_during_period(hass, zero, period="5minute", **kwargs) + assert stats == {} + stats = get_last_short_term_statistics(hass, 0, "sensor.test1", True) + assert stats == {} + + do_adhoc_statistics(hass, start=zero) + wait_recording_done(hass) + expected_1 = { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "end": process_timestamp_to_utc_isoformat(zero + timedelta(minutes=5)), + "mean": approx(14.915254237288135), + "min": approx(10.0), + "max": approx(20.0), + "last_reset": None, + "state": None, + "sum": None, + } + expected_stats1 = [ + {**expected_1, "statistic_id": "sensor.test1"}, + ] + expected_stats2 = [ + {**expected_1, "statistic_id": "sensor.test2"}, + ] + + stats = statistics_during_period(hass, zero, period="5minute") + assert stats == {"sensor.test1": expected_stats1, "sensor.test2": expected_stats2} + + # Insert metadata for sensor.test99 + metadata_1 = { + "has_mean": True, + "has_sum": False, + "name": "Total imported energy", + "source": "test", + "statistic_id": "sensor.test99", + "unit_of_measurement": "kWh", + } + + with session_scope(hass=hass) as session: + session.add(recorder.models.StatisticsMeta.from_meta(metadata_1)) + + # Rename entity sensor.test1 to sensor.test99 + @callback + def rename_entry(): + entity_reg.async_update_entity("sensor.test1", new_entity_id="sensor.test99") + + hass.add_job(rename_entry) + wait_recording_done(hass) + + # Statistics failed to migrate due to the collision + stats = statistics_during_period(hass, zero, period="5minute") + assert stats == {"sensor.test1": expected_stats1, "sensor.test2": expected_stats2} + assert "Blocked attempt to insert duplicated statistic rows" in caplog.text + + def test_statistics_duplicated(hass_recorder, caplog): """Test statistics with same start time is not compiled.""" hass = hass_recorder() @@ -737,7 +822,7 @@ def test_delete_duplicates_no_duplicates(hass_recorder, caplog): hass = hass_recorder() wait_recording_done(hass) with session_scope(hass=hass) as session: - delete_duplicates(hass, session) + delete_statistics_duplicates(hass, session) assert "duplicated statistics rows" not in caplog.text assert "Found non identical" not in caplog.text assert "Found duplicated" not in caplog.text @@ -800,6 +885,208 @@ def test_duplicate_statistics_handle_integrity_error(hass_recorder, caplog): assert "Blocked attempt to insert duplicated statistic rows" in caplog.text +def _create_engine_28(*args, **kwargs): + """Test version of create_engine that initializes with old schema. + + This simulates an existing db with the old schema. + """ + module = "tests.components.recorder.models_schema_28" + importlib.import_module(module) + old_models = sys.modules[module] + engine = create_engine(*args, **kwargs) + old_models.Base.metadata.create_all(engine) + with Session(engine) as session: + session.add(recorder.models.StatisticsRuns(start=statistics.get_start_time())) + session.add( + recorder.models.SchemaChanges(schema_version=old_models.SCHEMA_VERSION) + ) + session.commit() + return engine + + +def test_delete_metadata_duplicates(caplog, tmpdir): + """Test removal of duplicated statistics.""" + test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") + dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + + module = "tests.components.recorder.models_schema_28" + importlib.import_module(module) + old_models = sys.modules[module] + + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_metadata_2 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_co2_metadata = { + "has_mean": True, + "has_sum": False, + "name": "Fossil percentage", + "source": "test", + "statistic_id": "test:fossil_percentage", + "unit_of_measurement": "%", + } + + # Create some duplicated statistics_meta with schema version 28 + with patch.object(recorder, "models", old_models), patch.object( + recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION + ), patch( + "homeassistant.components.recorder.core.create_engine", new=_create_engine_28 + ): + hass = get_test_home_assistant() + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + wait_recording_done(hass) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) + ) + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) + ) + session.add(recorder.models.StatisticsMeta.from_meta(external_co2_metadata)) + + with session_scope(hass=hass) as session: + tmp = session.query(recorder.models.StatisticsMeta).all() + assert len(tmp) == 3 + assert tmp[0].id == 1 + assert tmp[0].statistic_id == "test:total_energy_import_tariff_1" + assert tmp[1].id == 2 + assert tmp[1].statistic_id == "test:total_energy_import_tariff_1" + assert tmp[2].id == 3 + assert tmp[2].statistic_id == "test:fossil_percentage" + + hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ + + # Test that the duplicates are removed during migration from schema 28 + hass = get_test_home_assistant() + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + hass.start() + wait_recording_done(hass) + wait_recording_done(hass) + + assert "Deleted 1 duplicated statistics_meta rows" in caplog.text + with session_scope(hass=hass) as session: + tmp = session.query(recorder.models.StatisticsMeta).all() + assert len(tmp) == 2 + assert tmp[0].id == 2 + assert tmp[0].statistic_id == "test:total_energy_import_tariff_1" + assert tmp[1].id == 3 + assert tmp[1].statistic_id == "test:fossil_percentage" + + hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ + + +def test_delete_metadata_duplicates_many(caplog, tmpdir): + """Test removal of duplicated statistics.""" + test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") + dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + + module = "tests.components.recorder.models_schema_28" + importlib.import_module(module) + old_models = sys.modules[module] + + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_metadata_2 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_2", + "unit_of_measurement": "kWh", + } + external_co2_metadata = { + "has_mean": True, + "has_sum": False, + "name": "Fossil percentage", + "source": "test", + "statistic_id": "test:fossil_percentage", + "unit_of_measurement": "%", + } + + # Create some duplicated statistics with schema version 28 + with patch.object(recorder, "models", old_models), patch.object( + recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION + ), patch( + "homeassistant.components.recorder.core.create_engine", new=_create_engine_28 + ): + hass = get_test_home_assistant() + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + wait_recording_done(hass) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) + ) + for _ in range(3000): + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) + ) + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) + ) + session.add( + recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) + ) + session.add(recorder.models.StatisticsMeta.from_meta(external_co2_metadata)) + session.add(recorder.models.StatisticsMeta.from_meta(external_co2_metadata)) + + hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ + + # Test that the duplicates are removed during migration from schema 28 + hass = get_test_home_assistant() + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + hass.start() + wait_recording_done(hass) + wait_recording_done(hass) + + assert "Deleted 3002 duplicated statistics_meta rows" in caplog.text + with session_scope(hass=hass) as session: + tmp = session.query(recorder.models.StatisticsMeta).all() + assert len(tmp) == 3 + assert tmp[0].id == 3001 + assert tmp[0].statistic_id == "test:total_energy_import_tariff_1" + assert tmp[1].id == 3003 + assert tmp[1].statistic_id == "test:total_energy_import_tariff_2" + assert tmp[2].id == 3005 + assert tmp[2].statistic_id == "test:fossil_percentage" + + hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ + + +def test_delete_metadata_duplicates_no_duplicates(hass_recorder, caplog): + """Test removal of duplicated statistics.""" + hass = hass_recorder() + wait_recording_done(hass) + with session_scope(hass=hass) as session: + delete_statistics_meta_duplicates(session) + assert "duplicated statistics_meta rows" not in caplog.text + + def record_states(hass): """Record some test states. From 6368df5e37cb391811ec2967bc53e77fe8d2f942 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 24 May 2022 15:51:06 +0200 Subject: [PATCH 0838/3516] Base Image: S6 overlay & jemalloc update (#72425) * Update base image with new s6-Overlay & jemalloc * add build version * Update finish --- build.yaml | 10 ++-- rootfs/etc/services.d/home-assistant/finish | 42 +++++++------ rootfs/etc/services.d/home-assistant/run | 0 rootfs/init | 66 ++++++++++++++------- 4 files changed, 75 insertions(+), 43 deletions(-) mode change 100644 => 100755 rootfs/etc/services.d/home-assistant/finish mode change 100644 => 100755 rootfs/etc/services.d/home-assistant/run diff --git a/build.yaml b/build.yaml index 3ced7b8d742..196277184a3 100644 --- a/build.yaml +++ b/build.yaml @@ -1,11 +1,11 @@ image: homeassistant/{arch}-homeassistant shadow_repository: ghcr.io/home-assistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.02.0 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.02.0 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.02.0 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.02.0 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.02.0 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.05.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.05.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.05.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.05.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.05.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io diff --git a/rootfs/etc/services.d/home-assistant/finish b/rootfs/etc/services.d/home-assistant/finish old mode 100644 new mode 100755 index 3691583ec81..115d8352618 --- a/rootfs/etc/services.d/home-assistant/finish +++ b/rootfs/etc/services.d/home-assistant/finish @@ -1,24 +1,30 @@ -#!/usr/bin/execlineb -S1 +#!/usr/bin/env bashio # ============================================================================== # Take down the S6 supervision tree when Home Assistant fails # ============================================================================== -define HA_RESTART_EXIT_CODE 100 -define SIGNAL_EXIT_CODE 256 -define SIGTERM 15 +declare RESTART_EXIT_CODE 100 +declare SIGNAL_EXIT_CODE 256 +declare SIGTERM 15 +declare APP_EXIT_CODE=${1} +declare SYS_EXIT_CODE=${2+x} +declare NEW_EXIT_CODE= -foreground { s6-echo "[finish] process exit code ${1}" } +bashio::log.info "Home Assistant Core finish process exit code ${1}" -if { s6-test ${1} -ne ${HA_RESTART_EXIT_CODE} } -ifelse { s6-test ${1} -eq ${SIGNAL_EXIT_CODE} } { - # Process terminated by a signal - define signal ${2} - foreground { s6-echo "[finish] process received signal ${signal}" } - backtick -n new_exit_code { s6-expr 128 + ${signal} } - importas -ui new_exit_code new_exit_code - foreground { redirfd -w 1 /var/run/s6/env-stage3/S6_STAGE2_EXITED s6-echo -n -- ${new_exit_code} } - if { s6-test ${signal} -ne ${SIGTERM} } - s6-svscanctl -t /var/run/s6/services -} +if [[ ${APP_EXIT_CODE} -eq ${RESTART_EXIT_CODE} ]]; then + exit 0 +elif [[ ${APP_EXIT_CODE} -eq ${SIGNAL_EXIT_CODE} ]]; then + bashio::log.info "Home Assistant Core finish process received signal ${APP_EXIT_CODE}" -foreground { redirfd -w 1 /var/run/s6/env-stage3/S6_STAGE2_EXITED s6-echo -n -- ${1} } -s6-svscanctl -t /var/run/s6/services + NEW_EXIT_CODE=$((128 + SYS_EXIT_CODE)) + echo ${NEW_EXIT_CODE} > /run/s6-linux-init-container-results/exitcode + + if [[ ${NEW_EXIT_CODE} -eq ${SIGTERM} ]]; then + /run/s6/basedir/bin/halt + fi +else + bashio::log.info "Home Assistant Core service shutdown" + + echo ${APP_EXIT_CODE} > /run/s6-linux-init-container-results/exitcode + /run/s6/basedir/bin/halt +fi diff --git a/rootfs/etc/services.d/home-assistant/run b/rootfs/etc/services.d/home-assistant/run old mode 100644 new mode 100755 diff --git a/rootfs/init b/rootfs/init index 7bea7ed88a9..bfc4802c858 100755 --- a/rootfs/init +++ b/rootfs/init @@ -1,23 +1,49 @@ -#!/bin/execlineb -S0 +#!/usr/bin/env bashio -## -## load default PATH (the same that Docker includes if not provided) if it doesn't exist, -## then go ahead with stage1. -## this was motivated due to this issue: -## - https://github.com/just-containers/s6-overlay/issues/108 -## +# This is the first program launched at container start. +# We don't know where our binaries are and we cannot guarantee +# that the default PATH can access them. +# So this script needs to be entirely self-contained until it has +# at least /command, /usr/bin and /bin in its PATH. -/bin/importas -D /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin PATH PATH -export PATH ${PATH} - -## -## Skip further init if the user has a given CMD. -## This is to prevent Home Assistant from starting twice if the user -## decided to override/start via the CMD. -## - -ifelse { s6-test $# -ne 0 } -{ - $@ +addpath () { + x="$1" + IFS=: + set -- $PATH + IFS= + while test "$#" -gt 0 ; do + if test "$1" = "$x" ; then + return + fi + shift + done + PATH="${x}:$PATH" } -/etc/s6/init/init-stage1 $@ \ No newline at end of file + +if test -z "$PATH" ; then + PATH=/bin +fi + +addpath /bin +addpath /usr/bin +addpath /command +export PATH + +# Now we're good: s6-overlay-suexec is accessible via PATH, as are +# all our binaries. + +# Skip further init if the user has a given CMD. +# This is to prevent Home Assistant from starting twice if the user +# decided to override/start via the CMD. +if test $# -ne 0 ; then + exec "$@" +fi + +# Run preinit as root, then run stage0 as the container's user (can be +# root, can be a normal user). + +exec s6-overlay-suexec \ + ' /package/admin/s6-overlay-@VERSION@/libexec/preinit' \ + '' \ + /package/admin/s6-overlay-@VERSION@/libexec/stage0 \ + "$@" From 1113d9bea958e83b17aa0ec106c1c41121f0c499 Mon Sep 17 00:00:00 2001 From: rappenze Date: Tue, 24 May 2022 16:00:15 +0200 Subject: [PATCH 0839/3516] Support fibaro garage door devices (#72299) * Add proper support for garage door controller in fibaro cover entity * Add proper support for garage door controller in fibaro cover entity --- homeassistant/components/fibaro/cover.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/homeassistant/components/fibaro/cover.py b/homeassistant/components/fibaro/cover.py index e347d0368d2..fc6d0a67d3c 100644 --- a/homeassistant/components/fibaro/cover.py +++ b/homeassistant/components/fibaro/cover.py @@ -6,6 +6,7 @@ from homeassistant.components.cover import ( ATTR_TILT_POSITION, ENTITY_ID_FORMAT, CoverEntity, + CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -41,6 +42,11 @@ class FibaroCover(FibaroDevice, CoverEntity): super().__init__(fibaro_device) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) + if self._is_open_close_only(): + self._attr_supported_features = ( + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE + ) + @staticmethod def bound(position): """Normalize the position.""" @@ -53,6 +59,14 @@ class FibaroCover(FibaroDevice, CoverEntity): return 100 return position + def _is_open_close_only(self) -> bool: + """Return if only open / close is supported.""" + # Normally positionable devices report the position over value, + # so if it is missing we have a device which supports open / close only + if "value" not in self.fibaro_device.properties: + return True + return False + @property def current_cover_position(self): """Return current position of cover. 0 is closed, 100 is open.""" @@ -74,6 +88,9 @@ class FibaroCover(FibaroDevice, CoverEntity): @property def is_closed(self): """Return if the cover is closed.""" + if self._is_open_close_only(): + return self.fibaro_device.properties.state.lower() == "closed" + if self.current_cover_position is None: return None return self.current_cover_position == 0 From dc0d065901096612a608c91acf2d0ea50df19a41 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 24 May 2022 16:06:30 +0200 Subject: [PATCH 0840/3516] Fix UniFi device tracker on controllers only reporting events (#72240) Add events to UniFi client tracker in case controller does not report proper data --- .../components/unifi/device_tracker.py | 21 ++- tests/components/unifi/test_device_tracker.py | 141 +++++++++++++++++- 2 files changed, 160 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 947e4523531..b2070362d02 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging -from aiounifi.api import SOURCE_DATA +from aiounifi.api import SOURCE_DATA, SOURCE_EVENT from aiounifi.events import ( ACCESS_POINT_UPGRADED, GATEWAY_UPGRADED, @@ -156,6 +156,8 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): self._controller_connection_state_changed = False + self._only_listen_to_data_source = False + last_seen = client.last_seen or 0 self.schedule_update = self._is_connected = ( self.is_wired == client.is_wired @@ -224,6 +226,23 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): ): self._is_connected = True self.schedule_update = True + self._only_listen_to_data_source = True + + elif ( + self.client.last_updated == SOURCE_EVENT + and not self._only_listen_to_data_source + ): + + if (self.is_wired and self.client.event.event in WIRED_CONNECTION) or ( + not self.is_wired and self.client.event.event in WIRELESS_CONNECTION + ): + self._is_connected = True + self.schedule_update = False + self.controller.async_heartbeat(self.unique_id) + super().async_update_callback() + + else: + self.schedule_update = True self._async_log_debug_data("update_callback") diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 736e85d1b8c..e5d53a6d882 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -3,7 +3,12 @@ from datetime import timedelta from unittest.mock import patch -from aiounifi.controller import MESSAGE_CLIENT, MESSAGE_CLIENT_REMOVED, MESSAGE_DEVICE +from aiounifi.controller import ( + MESSAGE_CLIENT, + MESSAGE_CLIENT_REMOVED, + MESSAGE_DEVICE, + MESSAGE_EVENT, +) from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING from homeassistant import config_entries @@ -169,6 +174,140 @@ async def test_tracked_clients( assert hass.states.get("device_tracker.client_1").state == STATE_HOME +async def test_tracked_wireless_clients_event_source( + hass, aioclient_mock, mock_unifi_websocket, mock_device_registry +): + """Verify tracking of wireless clients based on event source.""" + client = { + "ap_mac": "00:00:00:00:02:01", + "essid": "ssid", + "hostname": "client", + "ip": "10.0.0.1", + "is_wired": False, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:01", + } + config_entry = await setup_unifi_integration( + hass, aioclient_mock, clients_response=[client] + ) + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 1 + assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME + + # State change signalling works with events + + # Connected event + + event = { + "user": client["mac"], + "ssid": client["essid"], + "ap": client["ap_mac"], + "radio": "na", + "channel": "44", + "hostname": client["hostname"], + "key": "EVT_WU_Connected", + "subsystem": "wlan", + "site_id": "name", + "time": 1587753456179, + "datetime": "2020-04-24T18:37:36Z", + "msg": f'User{[client["mac"]]} has connected to AP[{client["ap_mac"]}] with SSID "{client["essid"]}" on "channel 44(na)"', + "_id": "5ea331fa30c49e00f90ddc1a", + } + mock_unifi_websocket( + data={ + "meta": {"message": MESSAGE_EVENT}, + "data": [event], + } + ) + await hass.async_block_till_done() + + assert hass.states.get("device_tracker.client").state == STATE_HOME + + # Disconnected event + + event = { + "user": client["mac"], + "ssid": client["essid"], + "hostname": client["hostname"], + "ap": client["ap_mac"], + "duration": 467, + "bytes": 459039, + "key": "EVT_WU_Disconnected", + "subsystem": "wlan", + "site_id": "name", + "time": 1587752927000, + "datetime": "2020-04-24T18:28:47Z", + "msg": f'User{[client["mac"]]} disconnected from "{client["essid"]}" (7m 47s connected, 448.28K bytes, last AP[{client["ap_mac"]}])', + "_id": "5ea32ff730c49e00f90dca1a", + } + mock_unifi_websocket( + data={ + "meta": {"message": MESSAGE_EVENT}, + "data": [event], + } + ) + await hass.async_block_till_done() + assert hass.states.get("device_tracker.client").state == STATE_HOME + + # Change time to mark client as away + new_time = dt_util.utcnow() + controller.option_detection_time + with patch("homeassistant.util.dt.utcnow", return_value=new_time): + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() + + assert hass.states.get("device_tracker.client").state == STATE_NOT_HOME + + # To limit false positives in client tracker + # data sources are prioritized when available + # once real data is received events will be ignored. + + # New data + + mock_unifi_websocket( + data={ + "meta": {"message": MESSAGE_CLIENT}, + "data": [client], + } + ) + await hass.async_block_till_done() + + assert hass.states.get("device_tracker.client").state == STATE_HOME + + # Disconnection event will be ignored + + event = { + "user": client["mac"], + "ssid": client["essid"], + "hostname": client["hostname"], + "ap": client["ap_mac"], + "duration": 467, + "bytes": 459039, + "key": "EVT_WU_Disconnected", + "subsystem": "wlan", + "site_id": "name", + "time": 1587752927000, + "datetime": "2020-04-24T18:28:47Z", + "msg": f'User{[client["mac"]]} disconnected from "{client["essid"]}" (7m 47s connected, 448.28K bytes, last AP[{client["ap_mac"]}])', + "_id": "5ea32ff730c49e00f90dca1a", + } + mock_unifi_websocket( + data={ + "meta": {"message": MESSAGE_EVENT}, + "data": [event], + } + ) + await hass.async_block_till_done() + assert hass.states.get("device_tracker.client").state == STATE_HOME + + # Change time to mark client as away + new_time = dt_util.utcnow() + controller.option_detection_time + with patch("homeassistant.util.dt.utcnow", return_value=new_time): + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() + + assert hass.states.get("device_tracker.client").state == STATE_HOME + + async def test_tracked_devices( hass, aioclient_mock, mock_unifi_websocket, mock_device_registry ): From d40cd20692db6ba84c02c1e3733237d6962ab3e7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 24 May 2022 16:07:18 +0200 Subject: [PATCH 0841/3516] Enforce type hints for backup and cast platforms (#72223) * Enforce backup type hints * Enforce cast type hints --- pylint/plugins/hass_enforce_type_hints.py | 52 +++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 9d35d07fab2..e230c27b4ee 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -46,6 +46,10 @@ _MODULE_FILTERS: dict[str, re.Pattern] = { "application_credentials": re.compile( r"^homeassistant\.components\.\w+\.(application_credentials)$" ), + # backup matches only in the package root (backup.py) + "backup": re.compile(r"^homeassistant\.components\.\w+\.(backup)$"), + # cast matches only in the package root (cast.py) + "cast": re.compile(r"^homeassistant\.components\.\w+\.(cast)$"), # config_flow matches only in the package root (config_flow.py) "config_flow": re.compile(r"^homeassistant\.components\.\w+\.(config_flow)$"), # device_action matches only in the package root (device_action.py) @@ -167,6 +171,54 @@ _METHOD_MATCH: list[TypeHintMatch] = [ }, return_type="AuthorizationServer", ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["backup"], + function_name="async_pre_backup", + arg_types={ + 0: "HomeAssistant", + }, + return_type=None, + ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["backup"], + function_name="async_post_backup", + arg_types={ + 0: "HomeAssistant", + }, + return_type=None, + ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["cast"], + function_name="async_get_media_browser_root_object", + arg_types={ + 0: "HomeAssistant", + 1: "str", + }, + return_type="list[BrowseMedia]", + ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["cast"], + function_name="async_browse_media", + arg_types={ + 0: "HomeAssistant", + 1: "str", + 2: "str", + 3: "str", + }, + return_type=["BrowseMedia", "BrowseMedia | None"], + ), + TypeHintMatch( + module_filter=_MODULE_FILTERS["cast"], + function_name="async_play_media", + arg_types={ + 0: "HomeAssistant", + 1: "str", + 2: "Chromecast", + 3: "str", + 4: "str", + }, + return_type="bool", + ), TypeHintMatch( module_filter=_MODULE_FILTERS["config_flow"], function_name="_async_has_devices", From a5402d725fe1e92f7043fbe48b577c11e5e0d939 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 May 2022 09:20:13 -0500 Subject: [PATCH 0842/3516] Add light platform to Big Ass Fans (#72382) Co-authored-by: Erik Montnemery --- .coveragerc | 1 + homeassistant/components/baf/__init__.py | 1 + homeassistant/components/baf/light.py | 102 +++++++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 homeassistant/components/baf/light.py diff --git a/.coveragerc b/.coveragerc index dd03c3782c0..ca6c57fb341 100644 --- a/.coveragerc +++ b/.coveragerc @@ -97,6 +97,7 @@ omit = homeassistant/components/baf/climate.py homeassistant/components/baf/entity.py homeassistant/components/baf/fan.py + homeassistant/components/baf/light.py homeassistant/components/baf/sensor.py homeassistant/components/baf/switch.py homeassistant/components/baidu/tts.py diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 30de2582af1..7127228383a 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -17,6 +17,7 @@ from .models import BAFData PLATFORMS: list[Platform] = [ Platform.CLIMATE, Platform.FAN, + Platform.LIGHT, Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/baf/light.py b/homeassistant/components/baf/light.py new file mode 100644 index 00000000000..b177d383cd5 --- /dev/null +++ b/homeassistant/components/baf/light.py @@ -0,0 +1,102 @@ +"""Support for Big Ass Fans lights.""" +from __future__ import annotations + +from typing import Any + +from aiobafi6 import Device, OffOnAuto + +from homeassistant import config_entries +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ColorMode, + LightEntity, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.color import ( + color_temperature_kelvin_to_mired, + color_temperature_mired_to_kelvin, +) + +from .const import DOMAIN +from .entity import BAFEntity +from .models import BAFData + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up BAF lights.""" + data: BAFData = hass.data[DOMAIN][entry.entry_id] + if data.device.has_light: + klass = BAFFanLight if data.device.has_fan else BAFStandaloneLight + async_add_entities([klass(data.device)]) + + +class BAFLight(BAFEntity, LightEntity): + """Representation of a Big Ass Fans light.""" + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + self._attr_is_on = self._device.light_mode == OffOnAuto.ON + if self._device.light_brightness_level is not None: + self._attr_brightness = round( + self._device.light_brightness_level / 16 * 255 + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: + self._device.light_brightness_level = max(int(brightness / 255 * 16), 1) + else: + self._device.light_mode = OffOnAuto.ON + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the light.""" + self._device.light_mode = OffOnAuto.OFF + + +class BAFFanLight(BAFLight): + """Representation of a Big Ass Fans light on a fan.""" + + def __init__(self, device: Device) -> None: + """Init a fan light.""" + super().__init__(device, device.name) + self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} + self._attr_color_mode = ColorMode.BRIGHTNESS + + +class BAFStandaloneLight(BAFLight): + """Representation of a Big Ass Fans light.""" + + def __init__(self, device: Device) -> None: + """Init a standalone light.""" + super().__init__(device, f"{device.name} Light") + self._attr_supported_color_modes = {ColorMode.COLOR_TEMP} + self._attr_color_mode = ColorMode.COLOR_TEMP + self._attr_min_mireds = color_temperature_kelvin_to_mired( + device.light_warmest_color_temperature + ) + self._attr_max_mireds = color_temperature_kelvin_to_mired( + device.light_coolest_color_temperature + ) + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + super()._async_update_attrs() + self._attr_color_temp = color_temperature_kelvin_to_mired( + self._device.light_color_temperature + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + if (color_temp := kwargs.get(ATTR_COLOR_TEMP)) is not None: + self._device.light_color_temperature = color_temperature_mired_to_kelvin( + color_temp + ) + await super().async_turn_on(**kwargs) From 652892e5357fd77b08ca60543d7d7a9e75f4a546 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 24 May 2022 16:30:41 +0200 Subject: [PATCH 0843/3516] Do not swallow WLED errors (#72407) --- homeassistant/components/wled/helpers.py | 6 +- tests/components/wled/test_button.py | 35 +++-- tests/components/wled/test_light.py | 35 +++-- tests/components/wled/test_number.py | 93 ++++++------ tests/components/wled/test_select.py | 185 +++++++++++------------ tests/components/wled/test_switch.py | 35 +++-- 6 files changed, 188 insertions(+), 201 deletions(-) diff --git a/homeassistant/components/wled/helpers.py b/homeassistant/components/wled/helpers.py index d5ca895390b..66cd8b13b42 100644 --- a/homeassistant/components/wled/helpers.py +++ b/homeassistant/components/wled/helpers.py @@ -2,7 +2,7 @@ from wled import WLEDConnectionError, WLEDError -from .const import LOGGER +from homeassistant.exceptions import HomeAssistantError def wled_exception_handler(func): @@ -18,11 +18,11 @@ def wled_exception_handler(func): self.coordinator.update_listeners() except WLEDConnectionError as error: - LOGGER.error("Error communicating with API: %s", error) self.coordinator.last_update_success = False self.coordinator.update_listeners() + raise HomeAssistantError("Error communicating with WLED API") from error except WLEDError as error: - LOGGER.error("Invalid response from API: %s", error) + raise HomeAssistantError("Invalid response from WLED API") from error return handler diff --git a/tests/components/wled/test_button.py b/tests/components/wled/test_button.py index 3cfa4f762ad..210d67a24a0 100644 --- a/tests/components/wled/test_button.py +++ b/tests/components/wled/test_button.py @@ -17,6 +17,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory @@ -55,43 +56,41 @@ async def test_button_error( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED buttons.""" mock_wled.reset.side_effect = WLEDError - await hass.services.async_call( - BUTTON_DOMAIN, - SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.wled_rgb_light_restart"}, - blocking=True, - ) - await hass.async_block_till_done() + with pytest.raises(HomeAssistantError, match="Invalid response from WLED API"): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.wled_rgb_light_restart"}, + blocking=True, + ) + await hass.async_block_till_done() state = hass.states.get("button.wled_rgb_light_restart") assert state assert state.state == "2021-11-04T16:37:00+00:00" - assert "Invalid response from API" in caplog.text async def test_button_connection_error( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED buttons.""" mock_wled.reset.side_effect = WLEDConnectionError - await hass.services.async_call( - BUTTON_DOMAIN, - SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.wled_rgb_light_restart"}, - blocking=True, - ) - await hass.async_block_till_done() + with pytest.raises(HomeAssistantError, match="Error communicating with WLED API"): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.wled_rgb_light_restart"}, + blocking=True, + ) + await hass.async_block_till_done() state = hass.states.get("button.wled_rgb_light_restart") assert state assert state.state == STATE_UNAVAILABLE - assert "Error communicating with API" in caplog.text diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index fb2efce404a..b52cf91a11e 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -25,6 +25,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util @@ -305,23 +306,22 @@ async def test_light_error( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED lights.""" mock_wled.segment.side_effect = WLEDError - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.wled_rgb_light"}, - blocking=True, - ) - await hass.async_block_till_done() + with pytest.raises(HomeAssistantError, match="Invalid response from WLED API"): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wled_rgb_light"}, + blocking=True, + ) + await hass.async_block_till_done() state = hass.states.get("light.wled_rgb_light") assert state assert state.state == STATE_ON - assert "Invalid response from API" in caplog.text assert mock_wled.segment.call_count == 1 mock_wled.segment.assert_called_with(on=False, segment_id=0, transition=None) @@ -330,23 +330,22 @@ async def test_light_connection_error( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED switches.""" mock_wled.segment.side_effect = WLEDConnectionError - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.wled_rgb_light"}, - blocking=True, - ) - await hass.async_block_till_done() + with pytest.raises(HomeAssistantError, match="Error communicating with WLED API"): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wled_rgb_light"}, + blocking=True, + ) + await hass.async_block_till_done() state = hass.states.get("light.wled_rgb_light") 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(on=False, segment_id=0, transition=None) diff --git a/tests/components/wled/test_number.py b/tests/components/wled/test_number.py index e4b8958b077..5c9d562e0c0 100644 --- a/tests/components/wled/test_number.py +++ b/tests/components/wled/test_number.py @@ -14,6 +14,7 @@ from homeassistant.components.number.const import ( 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.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util @@ -109,26 +110,25 @@ 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() + with pytest.raises(HomeAssistantError, match="Invalid response from WLED API"): + 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) @@ -137,26 +137,25 @@ 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() + with pytest.raises(HomeAssistantError, match="Error communicating with WLED API"): + 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) @@ -250,26 +249,25 @@ 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() + with pytest.raises(HomeAssistantError, match="Invalid response from WLED API"): + 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) @@ -278,25 +276,24 @@ 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() + with pytest.raises(HomeAssistantError, match="Error communicating with WLED API"): + 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) diff --git a/tests/components/wled/test_select.py b/tests/components/wled/test_select.py index d798a723246..ebe3c7ca018 100644 --- a/tests/components/wled/test_select.py +++ b/tests/components/wled/test_select.py @@ -16,6 +16,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory import homeassistant.util.dt as dt_util @@ -161,26 +162,25 @@ async def test_color_palette_select_error( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED selects.""" mock_wled.segment.side_effect = WLEDError - await hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - { - ATTR_ENTITY_ID: "select.wled_rgb_light_segment_1_color_palette", - ATTR_OPTION: "Icefire", - }, - blocking=True, - ) - await hass.async_block_till_done() + with pytest.raises(HomeAssistantError, match="Invalid response from WLED API"): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgb_light_segment_1_color_palette", + ATTR_OPTION: "Icefire", + }, + blocking=True, + ) + await hass.async_block_till_done() state = hass.states.get("select.wled_rgb_light_segment_1_color_palette") assert state assert state.state == "Random Cycle" - assert "Invalid response from API" in caplog.text assert mock_wled.segment.call_count == 1 mock_wled.segment.assert_called_with(segment_id=1, palette="Icefire") @@ -189,26 +189,25 @@ async def test_color_palette_select_connection_error( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED selects.""" mock_wled.segment.side_effect = WLEDConnectionError - await hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - { - ATTR_ENTITY_ID: "select.wled_rgb_light_segment_1_color_palette", - ATTR_OPTION: "Icefire", - }, - blocking=True, - ) - await hass.async_block_till_done() + with pytest.raises(HomeAssistantError, match="Error communicating with WLED API"): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgb_light_segment_1_color_palette", + ATTR_OPTION: "Icefire", + }, + blocking=True, + ) + await hass.async_block_till_done() state = hass.states.get("select.wled_rgb_light_segment_1_color_palette") 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, palette="Icefire") @@ -280,26 +279,25 @@ async def test_preset_select_error( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED selects.""" mock_wled.preset.side_effect = WLEDError - await hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - { - ATTR_ENTITY_ID: "select.wled_rgbw_light_preset", - ATTR_OPTION: "Preset 2", - }, - blocking=True, - ) - await hass.async_block_till_done() + with pytest.raises(HomeAssistantError, match="Invalid response from WLED API"): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgbw_light_preset", + ATTR_OPTION: "Preset 2", + }, + blocking=True, + ) + await hass.async_block_till_done() state = hass.states.get("select.wled_rgbw_light_preset") assert state assert state.state == "Preset 1" - assert "Invalid response from API" in caplog.text assert mock_wled.preset.call_count == 1 mock_wled.preset.assert_called_with(preset="Preset 2") @@ -309,26 +307,25 @@ async def test_preset_select_connection_error( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED selects.""" mock_wled.preset.side_effect = WLEDConnectionError - await hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - { - ATTR_ENTITY_ID: "select.wled_rgbw_light_preset", - ATTR_OPTION: "Preset 2", - }, - blocking=True, - ) - await hass.async_block_till_done() + with pytest.raises(HomeAssistantError, match="Error communicating with WLED API"): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgbw_light_preset", + ATTR_OPTION: "Preset 2", + }, + blocking=True, + ) + await hass.async_block_till_done() state = hass.states.get("select.wled_rgbw_light_preset") assert state assert state.state == STATE_UNAVAILABLE - assert "Error communicating with API" in caplog.text assert mock_wled.preset.call_count == 1 mock_wled.preset.assert_called_with(preset="Preset 2") @@ -400,26 +397,25 @@ async def test_playlist_select_error( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED selects.""" mock_wled.playlist.side_effect = WLEDError - await hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - { - ATTR_ENTITY_ID: "select.wled_rgbw_light_playlist", - ATTR_OPTION: "Playlist 2", - }, - blocking=True, - ) - await hass.async_block_till_done() + with pytest.raises(HomeAssistantError, match="Invalid response from WLED API"): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgbw_light_playlist", + ATTR_OPTION: "Playlist 2", + }, + blocking=True, + ) + await hass.async_block_till_done() state = hass.states.get("select.wled_rgbw_light_playlist") assert state assert state.state == "Playlist 1" - assert "Invalid response from API" in caplog.text assert mock_wled.playlist.call_count == 1 mock_wled.playlist.assert_called_with(playlist="Playlist 2") @@ -429,26 +425,25 @@ async def test_playlist_select_connection_error( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED selects.""" mock_wled.playlist.side_effect = WLEDConnectionError - await hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - { - ATTR_ENTITY_ID: "select.wled_rgbw_light_playlist", - ATTR_OPTION: "Playlist 2", - }, - blocking=True, - ) - await hass.async_block_till_done() + with pytest.raises(HomeAssistantError, match="Error communicating with WLED API"): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgbw_light_playlist", + ATTR_OPTION: "Playlist 2", + }, + blocking=True, + ) + await hass.async_block_till_done() state = hass.states.get("select.wled_rgbw_light_playlist") assert state assert state.state == STATE_UNAVAILABLE - assert "Error communicating with API" in caplog.text assert mock_wled.playlist.call_count == 1 mock_wled.playlist.assert_called_with(playlist="Playlist 2") @@ -489,26 +484,25 @@ async def test_live_select_error( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED selects.""" mock_wled.live.side_effect = WLEDError - await hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - { - ATTR_ENTITY_ID: "select.wled_rgb_light_live_override", - ATTR_OPTION: "1", - }, - blocking=True, - ) - await hass.async_block_till_done() + with pytest.raises(HomeAssistantError, match="Invalid response from WLED API"): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgb_light_live_override", + ATTR_OPTION: "1", + }, + blocking=True, + ) + await hass.async_block_till_done() state = hass.states.get("select.wled_rgb_light_live_override") assert state assert state.state == "0" - assert "Invalid response from API" in caplog.text assert mock_wled.live.call_count == 1 mock_wled.live.assert_called_with(live=1) @@ -517,25 +511,24 @@ async def test_live_select_connection_error( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED selects.""" mock_wled.live.side_effect = WLEDConnectionError - await hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - { - ATTR_ENTITY_ID: "select.wled_rgb_light_live_override", - ATTR_OPTION: "2", - }, - blocking=True, - ) - await hass.async_block_till_done() + with pytest.raises(HomeAssistantError, match="Error communicating with WLED API"): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgb_light_live_override", + ATTR_OPTION: "2", + }, + blocking=True, + ) + await hass.async_block_till_done() state = hass.states.get("select.wled_rgb_light_live_override") assert state assert state.state == STATE_UNAVAILABLE - assert "Error communicating with API" in caplog.text assert mock_wled.live.call_count == 1 mock_wled.live.assert_called_with(live=2) diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py index 2bf494284f5..1902a324764 100644 --- a/tests/components/wled/test_switch.py +++ b/tests/components/wled/test_switch.py @@ -23,6 +23,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory import homeassistant.util.dt as dt_util @@ -175,46 +176,44 @@ async def test_switch_error( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED switches.""" mock_wled.nightlight.side_effect = WLEDError - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, - blocking=True, - ) - await hass.async_block_till_done() + with pytest.raises(HomeAssistantError, match="Invalid response from WLED API"): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, + blocking=True, + ) + await hass.async_block_till_done() state = hass.states.get("switch.wled_rgb_light_nightlight") assert state assert state.state == STATE_OFF - assert "Invalid response from API" in caplog.text async def test_switch_connection_error( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock, - caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the WLED switches.""" mock_wled.nightlight.side_effect = WLEDConnectionError - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, - blocking=True, - ) - await hass.async_block_till_done() + with pytest.raises(HomeAssistantError, match="Error communicating with WLED API"): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, + blocking=True, + ) + await hass.async_block_till_done() state = hass.states.get("switch.wled_rgb_light_nightlight") assert state assert state.state == STATE_UNAVAILABLE - assert "Error communicating with API" in caplog.text @pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True) From b2e18682d2a7d64be0b4dd7885b5d8d1d00b0661 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 24 May 2022 16:31:03 +0200 Subject: [PATCH 0844/3516] Update coverage to 6.4 (#72347) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index f0cdac24e5f..29e7364059f 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.3.3 +coverage==6.4 freezegun==1.2.1 mock-open==1.4.0 mypy==0.950 From 1a43f107c492c6d8e531904188a532af1b251cd0 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 24 May 2022 07:40:14 -0700 Subject: [PATCH 0845/3516] All WeMo devices use the Sensor platform (#72396) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/wemo/__init__.py | 28 ++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 3b08a39f2ce..2727c913fee 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -32,7 +32,7 @@ WEMO_MODEL_DISPATCH = { "CoffeeMaker": [Platform.SWITCH], "Dimmer": [Platform.LIGHT], "Humidifier": [Platform.FAN], - "Insight": [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH], + "Insight": [Platform.BINARY_SENSOR, Platform.SWITCH], "LightSwitch": [Platform.SWITCH], "Maker": [Platform.BINARY_SENSOR, Platform.SWITCH], "Motion": [Platform.BINARY_SENSOR], @@ -141,7 +141,7 @@ class WemoDispatcher: """Initialize the WemoDispatcher.""" self._config_entry = config_entry self._added_serial_numbers: set[str] = set() - self._loaded_components: set[str] = set() + self._loaded_platforms: set[Platform] = set() async def async_add_unique_device( self, hass: HomeAssistant, wemo: pywemo.WeMoDevice @@ -151,28 +151,30 @@ class WemoDispatcher: return coordinator = await async_register_device(hass, self._config_entry, wemo) - for component in WEMO_MODEL_DISPATCH.get(wemo.model_name, [Platform.SWITCH]): + platforms = set(WEMO_MODEL_DISPATCH.get(wemo.model_name, [Platform.SWITCH])) + platforms.add(Platform.SENSOR) + for platform in platforms: # Three cases: - # - First time we see component, we need to load it and initialize the backlog - # - Component is being loaded, add to backlog - # - Component is loaded, backlog is gone, dispatch discovery + # - First time we see platform, we need to load it and initialize the backlog + # - Platform is being loaded, add to backlog + # - Platform is loaded, backlog is gone, dispatch discovery - if component not in self._loaded_components: - hass.data[DOMAIN]["pending"][component] = [coordinator] - self._loaded_components.add(component) + if platform not in self._loaded_platforms: + hass.data[DOMAIN]["pending"][platform] = [coordinator] + self._loaded_platforms.add(platform) hass.async_create_task( hass.config_entries.async_forward_entry_setup( - self._config_entry, component + self._config_entry, platform ) ) - elif component in hass.data[DOMAIN]["pending"]: - hass.data[DOMAIN]["pending"][component].append(coordinator) + elif platform in hass.data[DOMAIN]["pending"]: + hass.data[DOMAIN]["pending"][platform].append(coordinator) else: async_dispatcher_send( hass, - f"{DOMAIN}.{component}", + f"{DOMAIN}.{platform}", coordinator, ) From 8c9d7b392b9b97f955d4ddaf157525d261d0457d Mon Sep 17 00:00:00 2001 From: Ruben Date: Tue, 24 May 2022 16:48:22 +0200 Subject: [PATCH 0846/3516] Change default name of motion blind TDBU entities so they can be auto renamed (#72284) Change the default name of motion blind TDBU entities so they can be automatically renamed --- homeassistant/components/motion_blinds/cover.py | 4 ++-- homeassistant/components/motion_blinds/sensor.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index e09bb531208..a9f6df82ae0 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -182,7 +182,7 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): else: via_device = (DOMAIN, blind._gateway.mac) connections = {} - name = f"{blind.blind_type}-{blind.mac[12:]}" + name = f"{blind.blind_type} {blind.mac[12:]}" sw_version = None self._attr_device_class = device_class @@ -364,7 +364,7 @@ class MotionTDBUDevice(MotionPositionDevice): super().__init__(coordinator, blind, device_class, sw_version) self._motor = motor self._motor_key = motor[0] - self._attr_name = f"{blind.blind_type}-{motor}-{blind.mac[12:]}" + self._attr_name = f"{blind.blind_type} {blind.mac[12:]} {motor}" self._attr_unique_id = f"{blind.mac}-{motor}" if self._motor not in ["Bottom", "Top", "Combined"]: diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index 03e3a6e7618..ebaed95bcbf 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -55,9 +55,9 @@ class MotionBatterySensor(CoordinatorEntity, SensorEntity): super().__init__(coordinator) if blind.device_type in DEVICE_TYPES_WIFI: - name = f"{blind.blind_type}-battery" + name = f"{blind.blind_type} battery" else: - name = f"{blind.blind_type}-battery-{blind.mac[12:]}" + name = f"{blind.blind_type} {blind.mac[12:]} battery" self._blind = blind self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, blind.mac)}) @@ -104,9 +104,9 @@ class MotionTDBUBatterySensor(MotionBatterySensor): super().__init__(coordinator, blind) if blind.device_type in DEVICE_TYPES_WIFI: - name = f"{blind.blind_type}-{motor}-battery" + name = f"{blind.blind_type} {motor} battery" else: - name = f"{blind.blind_type}-{motor}-battery-{blind.mac[12:]}" + name = f"{blind.blind_type} {blind.mac[12:]} {motor} battery" self._motor = motor self._attr_unique_id = f"{blind.mac}-{motor}-battery" @@ -147,7 +147,7 @@ class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity): elif device.device_type in DEVICE_TYPES_WIFI: name = f"{device.blind_type} signal strength" else: - name = f"{device.blind_type} signal strength - {device.mac[12:]}" + name = f"{device.blind_type} {device.mac[12:]} signal strength" self._device = device self._device_type = device_type From 1482a8f73a62c2a822e696764cf5efaadabd9e65 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 24 May 2022 17:01:08 +0200 Subject: [PATCH 0847/3516] Adjust config-flow type hints in axis (#72387) --- homeassistant/components/axis/config_flow.py | 48 ++++++++++++-------- homeassistant/components/axis/device.py | 4 +- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 23b1ba4c753..2c5239a8fb3 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -1,13 +1,16 @@ """Config flow to configure Axis devices.""" +from __future__ import annotations +from collections.abc import Mapping from ipaddress import ip_address +from typing import Any from urllib.parse import urlsplit import voluptuous as vol from homeassistant import config_entries from homeassistant.components import dhcp, ssdp, zeroconf -from homeassistant.config_entries import SOURCE_IGNORE +from homeassistant.config_entries import SOURCE_IGNORE, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_MAC, @@ -29,7 +32,7 @@ from .const import ( DEFAULT_VIDEO_SOURCE, DOMAIN as AXIS_DOMAIN, ) -from .device import get_device +from .device import AxisNetworkDevice, get_device from .errors import AuthenticationRequired, CannotConnect AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f"} @@ -43,18 +46,18 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow(config_entry: ConfigEntry) -> AxisOptionsFlowHandler: """Get the options flow for this handler.""" return AxisOptionsFlowHandler(config_entry) - def __init__(self): + def __init__(self) -> None: """Initialize the Axis config flow.""" - self.device_config = {} - self.discovery_schema = {} - self.import_schema = {} - self.serial = None + self.device_config: dict[str, Any] = {} + self.discovery_schema: dict[vol.Required, type[str | int]] | None = None - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a Axis config flow start. Manage device specific parameters. @@ -71,8 +74,8 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): password=user_input[CONF_PASSWORD], ) - self.serial = device.vapix.serial_number - await self.async_set_unique_id(format_mac(self.serial)) + serial = device.vapix.serial_number + await self.async_set_unique_id(format_mac(serial)) self._abort_if_unique_id_configured( updates={ @@ -91,7 +94,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): CONF_MODEL: device.vapix.product_number, } - return await self._create_entry() + return await self._create_entry(serial) except AuthenticationRequired: errors["base"] = "invalid_auth" @@ -113,7 +116,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): errors=errors, ) - async def _create_entry(self): + async def _create_entry(self, serial: str) -> FlowResult: """Create entry for device. Generate a name to be used as a prefix for device entities. @@ -133,10 +136,10 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): self.device_config[CONF_NAME] = name - title = f"{model} - {self.serial}" + title = f"{model} - {serial}" return self.async_create_entry(title=title, data=self.device_config) - async def async_step_reauth(self, device_config: dict): + async def async_step_reauth(self, device_config: Mapping[str, Any]) -> FlowResult: """Trigger a reauthentication flow.""" self.context["title_placeholders"] = { CONF_NAME: device_config[CONF_NAME], @@ -188,7 +191,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): } ) - async def _process_discovered_device(self, device: dict): + async def _process_discovered_device(self, device: dict[str, Any]) -> FlowResult: """Prepare configuration for a discovered Axis device.""" if device[CONF_MAC][:8] not in AXIS_OUI: return self.async_abort(reason="not_axis_device") @@ -228,18 +231,22 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): class AxisOptionsFlowHandler(config_entries.OptionsFlow): """Handle Axis device options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize Axis device options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) - self.device = None + self.device: AxisNetworkDevice | None = None - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the Axis device options.""" self.device = self.hass.data[AXIS_DOMAIN][self.config_entry.unique_id] return await self.async_step_configure_stream() - async def async_step_configure_stream(self, user_input=None): + async def async_step_configure_stream( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the Axis device stream options.""" if user_input is not None: self.options.update(user_input) @@ -247,6 +254,7 @@ class AxisOptionsFlowHandler(config_entries.OptionsFlow): schema = {} + assert self.device vapix = self.device.api.vapix # Stream profiles diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 2338a90d562..d0d5e230d2f 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -274,7 +274,9 @@ class AxisNetworkDevice: ) -async def get_device(hass, host, port, username, password): +async def get_device( + hass: HomeAssistant, host: str, port: int, username: str, password: str +) -> axis.AxisDevice: """Create a Axis device.""" session = get_async_client(hass, verify_ssl=False) From 1d68934ae25821851ec7ed70e106a398337fb7a6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 24 May 2022 18:12:25 +0200 Subject: [PATCH 0848/3516] Deprecate vera YAML configuration (#72418) --- homeassistant/components/vera/__init__.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index d923861112b..2dda8fdb7c4 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -46,15 +46,18 @@ _LOGGER = logging.getLogger(__name__) VERA_ID_LIST_SCHEMA = vol.Schema([int]) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CONTROLLER): cv.url, - vol.Optional(CONF_EXCLUDE, default=[]): VERA_ID_LIST_SCHEMA, - vol.Optional(CONF_LIGHTS, default=[]): VERA_ID_LIST_SCHEMA, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CONTROLLER): cv.url, + vol.Optional(CONF_EXCLUDE, default=[]): VERA_ID_LIST_SCHEMA, + vol.Optional(CONF_LIGHTS, default=[]): VERA_ID_LIST_SCHEMA, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) From ffbec553d194f13bec8310e0ff47036d49688c00 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Tue, 24 May 2022 19:50:02 +0200 Subject: [PATCH 0849/3516] Bump plugwise to v0.18.5 (#72441) --- homeassistant/components/plugwise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 2b7f21cd106..a311151f645 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.18.4"], + "requirements": ["plugwise==0.18.5"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 9bfc8b3671e..e9438a330a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1245,7 +1245,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.18.4 +plugwise==0.18.5 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e6700d3add..e072289ac45 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -847,7 +847,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.18.4 +plugwise==0.18.5 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 From 6245d289078a499554a08671216b0b33d7613fe1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 24 May 2022 20:28:09 +0200 Subject: [PATCH 0850/3516] Remove YAML configuration from vicare (#72408) --- homeassistant/components/vicare/__init__.py | 51 +-------- .../components/vicare/config_flow.py | 19 +--- tests/components/vicare/__init__.py | 6 - tests/components/vicare/test_config_flow.py | 106 +----------------- 4 files changed, 6 insertions(+), 176 deletions(-) diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index 4af36835ed2..221f3a4374a 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -7,19 +7,14 @@ import logging from PyViCare.PyViCare import PyViCare from PyViCare.PyViCareDevice import Device -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import STORAGE_DIR -from homeassistant.helpers.typing import ConfigType from .const import ( - CONF_CIRCUIT, CONF_HEATING_TYPE, - DEFAULT_HEATING_TYPE, DEFAULT_SCAN_INTERVAL, DOMAIN, HEATING_TYPE_TO_CREATOR_METHOD, @@ -39,50 +34,6 @@ class ViCareRequiredKeysMixin: value_getter: Callable[[Device], bool] -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.All( - cv.deprecated(CONF_CIRCUIT), - cv.deprecated(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_CIRCUIT - ): int, # Ignored: All circuits are now supported. Will be removed when switching to Setup via UI. - vol.Optional( - CONF_HEATING_TYPE, default=DEFAULT_HEATING_TYPE.value - ): vol.In([e.value for e in HeatingType]), - } - ), - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the ViCare component from yaml.""" - if DOMAIN not in config: - # Setup via UI. No need to continue yaml-based setup - return True - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config[DOMAIN], - ) - ) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from config entry.""" _LOGGER.debug("Setting up ViCare component") diff --git a/homeassistant/components/vicare/config_flow.py b/homeassistant/components/vicare/config_flow.py index 0f0e2a85b3c..a0feb8f38ea 100644 --- a/homeassistant/components/vicare/config_flow.py +++ b/homeassistant/components/vicare/config_flow.py @@ -16,7 +16,6 @@ from homeassistant.helpers.device_registry import format_mac from . import vicare_login from .const import ( - CONF_CIRCUIT, CONF_HEATING_TYPE, DEFAULT_HEATING_TYPE, DOMAIN, @@ -32,7 +31,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input: dict[str, Any] | None = None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Invoke when a user initiates a flow via the user interface.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -75,17 +76,3 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="single_instance_allowed") return await self.async_step_user() - - async def async_step_import(self, import_info): - """Handle a flow initiated by a YAML config import.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - # Remove now unsupported config parameters - import_info.pop(CONF_CIRCUIT, None) - - # CONF_HEATING_TYPE is now required but was optional in yaml config. Add if missing. - if import_info.get(CONF_HEATING_TYPE) is None: - import_info[CONF_HEATING_TYPE] = DEFAULT_HEATING_TYPE.value - - return await self.async_step_user(import_info) diff --git a/tests/components/vicare/__init__.py b/tests/components/vicare/__init__.py index ae9df782886..66cbfdc1d26 100644 --- a/tests/components/vicare/__init__.py +++ b/tests/components/vicare/__init__.py @@ -13,10 +13,4 @@ ENTRY_CONFIG: Final[dict[str, str]] = { CONF_HEATING_TYPE: "auto", } -ENTRY_CONFIG_NO_HEATING_TYPE: Final[dict[str, str]] = { - CONF_USERNAME: "foo@bar.com", - CONF_PASSWORD: "1234", - CONF_CLIENT_ID: "5678", -} - MOCK_MAC = "B874241B7B9" diff --git a/tests/components/vicare/test_config_flow.py b/tests/components/vicare/test_config_flow.py index 0bedb0d73b8..3096f8a492c 100644 --- a/tests/components/vicare/test_config_flow.py +++ b/tests/components/vicare/test_config_flow.py @@ -5,10 +5,10 @@ from PyViCare.PyViCareUtils import PyViCareInvalidCredentialsError from homeassistant import config_entries, data_entry_flow from homeassistant.components import dhcp -from homeassistant.components.vicare.const import CONF_CIRCUIT, DOMAIN, VICARE_NAME +from homeassistant.components.vicare.const import DOMAIN from homeassistant.const import CONF_CLIENT_ID, CONF_PASSWORD, CONF_USERNAME -from . import ENTRY_CONFIG, ENTRY_CONFIG_NO_HEATING_TYPE, MOCK_MAC +from . import ENTRY_CONFIG, MOCK_MAC from tests.common import MockConfigEntry @@ -25,8 +25,6 @@ async def test_form(hass): "homeassistant.components.vicare.config_flow.vicare_login", return_value=None, ), patch( - "homeassistant.components.vicare.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.vicare.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -43,89 +41,9 @@ async def test_form(hass): assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["title"] == "ViCare" assert result2["data"] == ENTRY_CONFIG - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_import(hass): - """Test that the import works.""" - - with patch( - "homeassistant.components.vicare.config_flow.vicare_login", - return_value=True, - ), patch( - "homeassistant.components.vicare.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.vicare.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=ENTRY_CONFIG, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == VICARE_NAME - assert result["data"] == ENTRY_CONFIG - - await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_removes_circuit(hass): - """Test that the import works.""" - - with patch( - "homeassistant.components.vicare.config_flow.vicare_login", - return_value=True, - ), patch( - "homeassistant.components.vicare.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.vicare.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - ENTRY_CONFIG[CONF_CIRCUIT] = 1 - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=ENTRY_CONFIG, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == VICARE_NAME - assert result["data"] == ENTRY_CONFIG - - await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_adds_heating_type(hass): - """Test that the import works.""" - - with patch( - "homeassistant.components.vicare.config_flow.vicare_login", - return_value=True, - ), patch( - "homeassistant.components.vicare.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.vicare.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=ENTRY_CONFIG_NO_HEATING_TYPE, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == VICARE_NAME - assert result["data"] == ENTRY_CONFIG - - await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_invalid_login(hass) -> None: """Test a flow with an invalid Vicare login.""" result = await hass.config_entries.flow.async_init( @@ -171,8 +89,6 @@ async def test_form_dhcp(hass): "homeassistant.components.vicare.config_flow.vicare_login", return_value=None, ), patch( - "homeassistant.components.vicare.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.vicare.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -189,27 +105,9 @@ async def test_form_dhcp(hass): assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["title"] == "ViCare" assert result2["data"] == ENTRY_CONFIG - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_import_single_instance_allowed(hass): - """Test that configuring more than one instance is rejected.""" - mock_entry = MockConfigEntry( - domain=DOMAIN, - data=ENTRY_CONFIG, - ) - mock_entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=ENTRY_CONFIG, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "single_instance_allowed" - - async def test_dhcp_single_instance_allowed(hass): """Test that configuring more than one instance is rejected.""" mock_entry = MockConfigEntry( From a5e100176b76152ef40f4952eea6d13bdab4ad67 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 24 May 2022 20:46:06 +0200 Subject: [PATCH 0851/3516] Refactor zwave_js setup entry (#72414) * Refactor zwave_js setup entry * Improve message --- homeassistant/components/zwave_js/__init__.py | 244 +++++++++--------- 1 file changed, 121 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index d12c7df6a79..0c716a39c75 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -9,6 +9,7 @@ from typing import Any from async_timeout import timeout from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.exceptions import BaseZwaveJSServerError, InvalidServerVersion +from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.notification import ( EntryControlNotification, @@ -25,12 +26,6 @@ from homeassistant.const import ( 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, ) @@ -39,7 +34,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import UNDEFINED, ConfigType from .addon import AddonError, AddonManager, AddonState, get_addon_manager from .api import async_register_api @@ -154,39 +149,105 @@ def register_node_in_dev_reg( else: ids = {device_id} - params = { - ATTR_IDENTIFIERS: ids, - 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[ATTR_SUGGESTED_AREA] = node.location - device = dev_reg.async_get_or_create(config_entry_id=entry.entry_id, **params) + device = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers=ids, + sw_version=node.firmware_version, + name=node.name or node.device_config.description or f"Node {node.node_id}", + model=node.device_config.label, + manufacturer=node.device_config.manufacturer, + suggested_area=node.location if node.location else UNDEFINED, + ) async_dispatcher_send(hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device) return device -async def async_setup_entry( # noqa: C901 - hass: HomeAssistant, entry: ConfigEntry -) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Z-Wave JS from a config entry.""" if use_addon := entry.data.get(CONF_USE_ADDON): await async_ensure_addon_running(hass, entry) client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) + entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {}) + + # connect and throw error if connection failed + try: + async with timeout(CONNECT_TIMEOUT): + await client.connect() + except InvalidServerVersion as err: + if not entry_hass_data.get(DATA_INVALID_SERVER_VERSION_LOGGED): + LOGGER.error("Invalid server version: %s", err) + entry_hass_data[DATA_INVALID_SERVER_VERSION_LOGGED] = True + if use_addon: + async_ensure_addon_updated(hass) + raise ConfigEntryNotReady from err + except (asyncio.TimeoutError, BaseZwaveJSServerError) as err: + if not entry_hass_data.get(DATA_CONNECT_FAILED_LOGGED): + LOGGER.error("Failed to connect: %s", err) + entry_hass_data[DATA_CONNECT_FAILED_LOGGED] = True + raise ConfigEntryNotReady from err + else: + LOGGER.info("Connected to Zwave JS Server") + entry_hass_data[DATA_CONNECT_FAILED_LOGGED] = False + entry_hass_data[DATA_INVALID_SERVER_VERSION_LOGGED] = False + + dev_reg = device_registry.async_get(hass) + ent_reg = entity_registry.async_get(hass) + services = ZWaveServices(hass, ent_reg, dev_reg) + services.async_register() + + # Set up websocket API + async_register_api(hass) + + platform_task = hass.async_create_task(start_platforms(hass, entry, client)) + entry_hass_data[DATA_START_PLATFORM_TASK] = platform_task + + return True + + +async def start_platforms( + hass: HomeAssistant, entry: ConfigEntry, client: ZwaveClient +) -> None: + """Start platforms and perform discovery.""" + entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {}) + entry_hass_data[DATA_CLIENT] = client + entry_hass_data[DATA_PLATFORM_SETUP] = {} + driver_ready = asyncio.Event() + + async def handle_ha_shutdown(event: Event) -> None: + """Handle HA shutdown.""" + await disconnect_client(hass, entry) + + listen_task = asyncio.create_task(client_listen(hass, entry, client, driver_ready)) + entry_hass_data[DATA_CLIENT_LISTEN_TASK] = listen_task + entry.async_on_unload( + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown) + ) + + try: + await driver_ready.wait() + except asyncio.CancelledError: + LOGGER.debug("Cancelling start platforms") + return + + LOGGER.info("Connection to Zwave JS Server initialized") + + if client.driver is None: + raise RuntimeError("Driver not ready.") + + await setup_driver(hass, entry, client, client.driver) + + +async def setup_driver( # noqa: C901 + hass: HomeAssistant, entry: ConfigEntry, client: ZwaveClient, driver: Driver +) -> None: + """Set up devices using the ready driver.""" dev_reg = device_registry.async_get(hass) ent_reg = entity_registry.async_get(hass) entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {}) - - entry_hass_data[DATA_CLIENT] = client - platform_setup_tasks = entry_hass_data[DATA_PLATFORM_SETUP] = {} - + platform_setup_tasks = entry_hass_data[DATA_PLATFORM_SETUP] registered_unique_ids: dict[str, dict[str, set[str]]] = defaultdict(dict) discovered_value_ids: dict[str, set[str]] = defaultdict(set) @@ -384,7 +445,7 @@ async def async_setup_entry( # noqa: C901 { ATTR_DOMAIN: DOMAIN, ATTR_NODE_ID: notification.node.node_id, - ATTR_HOME_ID: client.driver.controller.home_id, + ATTR_HOME_ID: driver.controller.home_id, ATTR_ENDPOINT: notification.endpoint, ATTR_DEVICE_ID: device.id, ATTR_COMMAND_CLASS: notification.command_class, @@ -414,7 +475,7 @@ async def async_setup_entry( # noqa: C901 event_data = { ATTR_DOMAIN: DOMAIN, ATTR_NODE_ID: notification.node.node_id, - ATTR_HOME_ID: client.driver.controller.home_id, + ATTR_HOME_ID: driver.controller.home_id, ATTR_DEVICE_ID: device.id, ATTR_COMMAND_CLASS: notification.command_class, } @@ -487,7 +548,7 @@ async def async_setup_entry( # noqa: C901 ZWAVE_JS_VALUE_UPDATED_EVENT, { ATTR_NODE_ID: value.node.node_id, - ATTR_HOME_ID: client.driver.controller.home_id, + ATTR_HOME_ID: driver.controller.home_id, ATTR_DEVICE_ID: device.id, ATTR_ENTITY_ID: entity_id, ATTR_COMMAND_CLASS: value.command_class, @@ -502,105 +563,42 @@ async def async_setup_entry( # noqa: C901 }, ) - # connect and throw error if connection failed - try: - async with timeout(CONNECT_TIMEOUT): - await client.connect() - except InvalidServerVersion as err: - if not entry_hass_data.get(DATA_INVALID_SERVER_VERSION_LOGGED): - LOGGER.error("Invalid server version: %s", err) - entry_hass_data[DATA_INVALID_SERVER_VERSION_LOGGED] = True - if use_addon: - async_ensure_addon_updated(hass) - raise ConfigEntryNotReady from err - except (asyncio.TimeoutError, BaseZwaveJSServerError) as err: - if not entry_hass_data.get(DATA_CONNECT_FAILED_LOGGED): - LOGGER.error("Failed to connect: %s", err) - entry_hass_data[DATA_CONNECT_FAILED_LOGGED] = True - raise ConfigEntryNotReady from err - else: - LOGGER.info("Connected to Zwave JS Server") - entry_hass_data[DATA_CONNECT_FAILED_LOGGED] = False - entry_hass_data[DATA_INVALID_SERVER_VERSION_LOGGED] = False + # If opt in preference hasn't been specified yet, we do nothing, otherwise + # we apply the preference + if opted_in := entry.data.get(CONF_DATA_COLLECTION_OPTED_IN): + await async_enable_statistics(client) + elif opted_in is False: + await driver.async_disable_statistics() - services = ZWaveServices(hass, ent_reg, dev_reg) - services.async_register() + # Check for nodes that no longer exist and remove them + stored_devices = device_registry.async_entries_for_config_entry( + dev_reg, entry.entry_id + ) + known_devices = [ + dev_reg.async_get_device({get_device_id(client, node)}) + for node in driver.controller.nodes.values() + ] - # Set up websocket API - async_register_api(hass) + # Devices that are in the device registry that are not known by the controller can be removed + for device in stored_devices: + if device not in known_devices: + dev_reg.async_remove_device(device.id) - async def start_platforms() -> None: - """Start platforms and perform discovery.""" - driver_ready = asyncio.Event() + # run discovery on all ready nodes + await asyncio.gather( + *(async_on_node_added(node) for node in driver.controller.nodes.values()) + ) - async def handle_ha_shutdown(event: Event) -> None: - """Handle HA shutdown.""" - await disconnect_client(hass, entry) - - listen_task = asyncio.create_task( - client_listen(hass, entry, client, driver_ready) + # listen for new nodes being added to the mesh + entry.async_on_unload( + driver.controller.on( + "node added", + lambda event: hass.async_create_task(async_on_node_added(event["node"])), ) - entry_hass_data[DATA_CLIENT_LISTEN_TASK] = listen_task - entry.async_on_unload( - hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown) - ) - - try: - await driver_ready.wait() - except asyncio.CancelledError: - LOGGER.debug("Cancelling start platforms") - return - - LOGGER.info("Connection to Zwave JS Server initialized") - - # If opt in preference hasn't been specified yet, we do nothing, otherwise - # we apply the preference - if opted_in := entry.data.get(CONF_DATA_COLLECTION_OPTED_IN): - await async_enable_statistics(client) - elif opted_in is False: - await client.driver.async_disable_statistics() - - # Check for nodes that no longer exist and remove them - stored_devices = device_registry.async_entries_for_config_entry( - dev_reg, entry.entry_id - ) - known_devices = [ - dev_reg.async_get_device({get_device_id(client, node)}) - for node in client.driver.controller.nodes.values() - ] - - # Devices that are in the device registry that are not known by the controller can be removed - for device in stored_devices: - if device not in known_devices: - dev_reg.async_remove_device(device.id) - - # run discovery on all ready nodes - await asyncio.gather( - *( - async_on_node_added(node) - for node in client.driver.controller.nodes.values() - ) - ) - - # listen for new nodes being added to the mesh - entry.async_on_unload( - client.driver.controller.on( - "node added", - lambda event: hass.async_create_task( - async_on_node_added(event["node"]) - ), - ) - ) - # listen for nodes being removed from the mesh - # NOTE: This will not remove nodes that were removed when HA was not running - entry.async_on_unload( - client.driver.controller.on("node removed", async_on_node_removed) - ) - - platform_task = hass.async_create_task(start_platforms()) - entry_hass_data[DATA_START_PLATFORM_TASK] = platform_task - - return True + ) + # listen for nodes being removed from the mesh + # NOTE: This will not remove nodes that were removed when HA was not running + entry.async_on_unload(driver.controller.on("node removed", async_on_node_removed)) async def client_listen( From 2e36a79357e09c3cdede1085c8ce5308f89a42f2 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Tue, 24 May 2022 21:37:37 +0200 Subject: [PATCH 0852/3516] Changes after late upnp review (#72241) * Changes after review of #70008, part 1 * Changes after review from #70008 * Revert to UpnpDataUpdateCoordinator._async_update_data --- homeassistant/components/upnp/__init__.py | 27 ++- homeassistant/components/upnp/config_flow.py | 2 +- homeassistant/components/upnp/device.py | 51 ++--- tests/components/upnp/conftest.py | 196 +++---------------- tests/components/upnp/test_binary_sensor.py | 34 ++-- tests/components/upnp/test_sensor.py | 141 ++++++------- 6 files changed, 132 insertions(+), 319 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index b571a2b447f..07560f7413f 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -39,7 +39,7 @@ from .const import ( DOMAIN, LOGGER, ) -from .device import Device, async_get_mac_address_from_host +from .device import Device, async_create_device, async_get_mac_address_from_host NOTIFICATION_ID = "upnp_notification" NOTIFICATION_TITLE = "UPnP/IGD Setup" @@ -113,8 +113,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await asyncio.wait_for(device_discovered_event.wait(), timeout=10) except asyncio.TimeoutError as err: - LOGGER.debug("Device not discovered: %s", usn) - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady(f"Device not discovered: {usn}") from err finally: cancel_discovered_callback() @@ -123,12 +122,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: assert discovery_info.ssdp_location is not None location = discovery_info.ssdp_location try: - device = await Device.async_create_device(hass, location) + device = await async_create_device(hass, location) except UpnpConnectionError as err: - LOGGER.debug( - "Error connecting to device at location: %s, err: %s", location, err - ) - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady( + f"Error connecting to device at location: {location}, err: {err}" + ) from err # Track the original UDN such that existing sensors do not change their unique_id. if CONFIG_ENTRY_ORIGINAL_UDN not in entry.data: @@ -255,21 +253,15 @@ class UpnpDataUpdateCoordinator(DataUpdateCoordinator): LOGGER, name=device.name, update_interval=update_interval, - update_method=self._async_fetch_data, ) - async def _async_fetch_data(self) -> Mapping[str, Any]: + async def _async_update_data(self) -> Mapping[str, Any]: """Update data.""" try: update_values = await asyncio.gather( self.device.async_get_traffic_data(), self.device.async_get_status(), ) - - return { - **update_values[0], - **update_values[1], - } except UpnpCommunicationError as exception: LOGGER.debug( "Caught exception when updating device: %s, exception: %s", @@ -280,6 +272,11 @@ class UpnpDataUpdateCoordinator(DataUpdateCoordinator): f"Unable to communicate with IGD at: {self.device.device_url}" ) from exception + return { + **update_values[0], + **update_values[1], + } + class UpnpEntity(CoordinatorEntity[UpnpDataUpdateCoordinator]): """Base class for UPnP/IGD entities.""" diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index b7208e6e6e2..b54098b6566 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -54,7 +54,7 @@ async def _async_wait_for_discoveries(hass: HomeAssistant) -> bool: async def device_discovered(info: SsdpServiceInfo, change: SsdpChange) -> None: if change != SsdpChange.BYEBYE: - LOGGER.info( + LOGGER.debug( "Device discovered: %s, at: %s", info.ssdp_usn, info.ssdp_location, diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 0e7c7902bd9..334d870939f 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -9,7 +9,6 @@ from typing import Any from urllib.parse import urlparse from async_upnp_client.aiohttp import AiohttpSessionRequester -from async_upnp_client.client import UpnpDevice from async_upnp_client.client_factory import UpnpFactory from async_upnp_client.exceptions import UpnpError from async_upnp_client.profiles.igd import IgdDevice, StatusInfo @@ -47,15 +46,19 @@ async def async_get_mac_address_from_host(hass: HomeAssistant, host: str) -> str return mac_address -async def async_create_upnp_device( - hass: HomeAssistant, ssdp_location: str -) -> UpnpDevice: - """Create UPnP device.""" +async def async_create_device(hass: HomeAssistant, ssdp_location: str) -> Device: + """Create UPnP/IGD device.""" session = async_get_clientsession(hass) requester = AiohttpSessionRequester(session, with_sleep=True, timeout=20) factory = UpnpFactory(requester, disable_state_variable_validation=True) - return await factory.async_create_device(ssdp_location) + upnp_device = await factory.async_create_device(ssdp_location) + + # Create profile wrapper. + igd_device = IgdDevice(upnp_device, None) + device = Device(hass, igd_device) + + return device class Device: @@ -66,40 +69,8 @@ class Device: self.hass = hass self._igd_device = igd_device self.coordinator: DataUpdateCoordinator | None = None - self._mac_address: str | None = None - - @classmethod - async def async_create_device( - cls, hass: HomeAssistant, ssdp_location: str - ) -> Device: - """Create UPnP/IGD device.""" - upnp_device = await async_create_upnp_device(hass, ssdp_location) - - # Create profile wrapper. - igd_device = IgdDevice(upnp_device, None) - device = cls(hass, igd_device) - - return device - - @property - def mac_address(self) -> str | None: - """Get the mac address.""" - return self._mac_address - - @mac_address.setter - def mac_address(self, mac_address: str) -> None: - """Set the mac address.""" - self._mac_address = mac_address - - @property - def original_udn(self) -> str | None: - """Get the mac address.""" - return self._original_udn - - @original_udn.setter - def original_udn(self, original_udn: str) -> None: - """Set the original UDN.""" - self._original_udn = original_udn + self.mac_address: str | None = None + self.original_udn: str | None = None @property def udn(self) -> str: diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index dd22db878cf..687518bb46d 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -1,33 +1,23 @@ """Configuration for SSDP tests.""" from __future__ import annotations -from collections.abc import Sequence -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, create_autospec, patch from urllib.parse import urlparse from async_upnp_client.client import UpnpDevice -from async_upnp_client.event_handler import UpnpEventHandler -from async_upnp_client.profiles.igd import StatusInfo +from async_upnp_client.profiles.igd import IgdDevice, StatusInfo import pytest from homeassistant.components import ssdp from homeassistant.components.upnp.const import ( - BYTES_RECEIVED, - BYTES_SENT, CONFIG_ENTRY_LOCATION, CONFIG_ENTRY_MAC_ADDRESS, CONFIG_ENTRY_ORIGINAL_UDN, CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, DOMAIN, - PACKETS_RECEIVED, - PACKETS_SENT, - ROUTER_IP, - ROUTER_UPTIME, - WAN_STATUS, ) from homeassistant.core import HomeAssistant -from homeassistant.util import dt from tests.common import MockConfigEntry @@ -59,160 +49,37 @@ TEST_DISCOVERY = ssdp.SsdpServiceInfo( ) -class MockUpnpDevice: - """Mock async_upnp_client UpnpDevice.""" - - def __init__(self, location: str) -> None: - """Initialize.""" - self.device_url = location - - @property - def manufacturer(self) -> str: - """Get manufacturer.""" - return TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_MANUFACTURER] - - @property - def name(self) -> str: - """Get name.""" - return TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] - - @property - def model_name(self) -> str: - """Get the model name.""" - return TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_MODEL_NAME] - - @property - def device_type(self) -> str: - """Get the device type.""" - return TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_DEVICE_TYPE] - - @property - def udn(self) -> str: - """Get the UDN.""" - return TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_UDN] - - @property - def usn(self) -> str: - """Get the USN.""" - return f"{self.udn}::{self.device_type}" - - @property - def unique_id(self) -> str: - """Get the unique id.""" - return self.usn - - def reinit(self, new_upnp_device: UpnpDevice) -> None: - """Reinitialize.""" - self.device_url = new_upnp_device.device_url - - -class MockIgdDevice: - """Mock async_upnp_client IgdDevice.""" - - def __init__(self, device: MockUpnpDevice, event_handler: UpnpEventHandler) -> None: - """Initialize mock device.""" - self.device = device - self.profile_device = device - - self._timestamp = dt.utcnow() - self.traffic_times_polled = 0 - self.status_times_polled = 0 - - self.traffic_data = { - BYTES_RECEIVED: 0, - BYTES_SENT: 0, - PACKETS_RECEIVED: 0, - PACKETS_SENT: 0, - } - self.status_data = { - WAN_STATUS: "Connected", - ROUTER_UPTIME: 10, - ROUTER_IP: "8.9.10.11", - } - - @property - def name(self) -> str: - """Get the name of the device.""" - return self.profile_device.name - - @property - def manufacturer(self) -> str: - """Get the manufacturer of this device.""" - return self.profile_device.manufacturer - - @property - def model_name(self) -> str: - """Get the model name of this device.""" - return self.profile_device.model_name - - @property - def udn(self) -> str: - """Get the UDN of the device.""" - return self.profile_device.udn - - @property - def device_type(self) -> str: - """Get the device type of this device.""" - return self.profile_device.device_type - - async def async_get_total_bytes_received(self) -> int | None: - """Get total bytes received.""" - self.traffic_times_polled += 1 - return self.traffic_data[BYTES_RECEIVED] - - async def async_get_total_bytes_sent(self) -> int | None: - """Get total bytes sent.""" - return self.traffic_data[BYTES_SENT] - - async def async_get_total_packets_received(self) -> int | None: - """Get total packets received.""" - return self.traffic_data[PACKETS_RECEIVED] - - async def async_get_total_packets_sent(self) -> int | None: - """Get total packets sent.""" - return self.traffic_data[PACKETS_SENT] - - async def async_get_external_ip_address( - self, services: Sequence[str] | None = None - ) -> str | None: - """ - Get the external IP address. - - :param services List of service names to try to get action from, defaults to [WANIPC,WANPPP] - """ - return self.status_data[ROUTER_IP] - - async def async_get_status_info( - self, services: Sequence[str] | None = None - ) -> StatusInfo | None: - """ - Get status info. - - :param services List of service names to try to get action from, defaults to [WANIPC,WANPPP] - """ - self.status_times_polled += 1 - return StatusInfo( - self.status_data[WAN_STATUS], "", self.status_data[ROUTER_UPTIME] - ) - - @pytest.fixture(autouse=True) -def mock_upnp_device(): - """Mock homeassistant.components.upnp.Device.""" +def mock_igd_device() -> IgdDevice: + """Mock async_upnp_client device.""" + mock_upnp_device = create_autospec(UpnpDevice, instance=True) + mock_upnp_device.device_url = TEST_DISCOVERY.ssdp_location - async def mock_async_create_upnp_device( - hass: HomeAssistant, location: str - ) -> UpnpDevice: - """Create UPnP device.""" - return MockUpnpDevice(location) + mock_igd_device = create_autospec(IgdDevice) + mock_igd_device.name = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] + mock_igd_device.manufacturer = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_MANUFACTURER] + mock_igd_device.model_name = TEST_DISCOVERY.upnp[ssdp.ATTR_UPNP_MODEL_NAME] + mock_igd_device.udn = TEST_DISCOVERY.ssdp_udn + mock_igd_device.device = mock_upnp_device + + mock_igd_device.async_get_total_bytes_received.return_value = 0 + mock_igd_device.async_get_total_bytes_sent.return_value = 0 + mock_igd_device.async_get_total_packets_received.return_value = 0 + mock_igd_device.async_get_total_packets_sent.return_value = 0 + mock_igd_device.async_get_status_info.return_value = StatusInfo( + "Connected", + "", + 10, + ) + mock_igd_device.async_get_external_ip_address.return_value = "8.9.10.11" with patch( - "homeassistant.components.upnp.device.async_create_upnp_device", - side_effect=mock_async_create_upnp_device, - ) as mock_async_create_upnp_device, patch( - "homeassistant.components.upnp.device.IgdDevice", new=MockIgdDevice - ) as mock_igd_device: - yield mock_async_create_upnp_device, mock_igd_device + "homeassistant.components.upnp.device.UpnpFactory.async_create_device" + ), patch( + "homeassistant.components.upnp.device.IgdDevice.__new__", + return_value=mock_igd_device, + ): + yield mock_igd_device @pytest.fixture @@ -297,11 +164,11 @@ async def ssdp_no_discovery(): @pytest.fixture -async def config_entry( +async def mock_config_entry( hass: HomeAssistant, mock_get_source_ip, ssdp_instant_discovery, - mock_upnp_device, + mock_igd_device: IgdDevice, mock_mac_address_from_host, ): """Create an initialized integration.""" @@ -316,6 +183,7 @@ async def config_entry( CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, }, ) + entry.igd_device = mock_igd_device # Load config_entry. entry.add_to_hass(hass) diff --git a/tests/components/upnp/test_binary_sensor.py b/tests/components/upnp/test_binary_sensor.py index 22264792420..24e5cdce47c 100644 --- a/tests/components/upnp/test_binary_sensor.py +++ b/tests/components/upnp/test_binary_sensor.py @@ -2,36 +2,34 @@ from datetime import timedelta -from homeassistant.components.upnp.const import ( - DOMAIN, - ROUTER_IP, - ROUTER_UPTIME, - WAN_STATUS, -) +from async_upnp_client.profiles.igd import StatusInfo + +from homeassistant.components.upnp.const import DEFAULT_SCAN_INTERVAL from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from .conftest import MockIgdDevice - from tests.common import MockConfigEntry, async_fire_time_changed -async def test_upnp_binary_sensors(hass: HomeAssistant, config_entry: MockConfigEntry): +async def test_upnp_binary_sensors( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +): """Test normal sensors.""" # First poll. wan_status_state = hass.states.get("binary_sensor.mock_name_wan_status") assert wan_status_state.state == "on" # Second poll. - mock_device: MockIgdDevice = hass.data[DOMAIN][ - config_entry.entry_id - ].device._igd_device - mock_device.status_data = { - WAN_STATUS: "Disconnected", - ROUTER_UPTIME: 100, - ROUTER_IP: "", - } - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=31)) + mock_igd_device = mock_config_entry.igd_device + mock_igd_device.async_get_status_info.return_value = StatusInfo( + "Disconnected", + "", + 40, + ) + + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=DEFAULT_SCAN_INTERVAL) + ) await hass.async_block_till_done() wan_status_state = hass.states.get("binary_sensor.mock_name_wan_status") diff --git a/tests/components/upnp/test_sensor.py b/tests/components/upnp/test_sensor.py index 6c00f63a479..2abd357ac31 100644 --- a/tests/components/upnp/test_sensor.py +++ b/tests/components/upnp/test_sensor.py @@ -3,114 +3,93 @@ from datetime import timedelta from unittest.mock import patch +from async_upnp_client.profiles.igd import StatusInfo import pytest -from homeassistant.components.upnp import UpnpDataUpdateCoordinator -from homeassistant.components.upnp.const import ( - BYTES_RECEIVED, - BYTES_SENT, - DEFAULT_SCAN_INTERVAL, - DOMAIN, - PACKETS_RECEIVED, - PACKETS_SENT, - ROUTER_IP, - ROUTER_UPTIME, - WAN_STATUS, -) +from homeassistant.components.upnp.const import DEFAULT_SCAN_INTERVAL from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from .conftest import MockIgdDevice - from tests.common import MockConfigEntry, async_fire_time_changed -async def test_upnp_sensors(hass: HomeAssistant, config_entry: MockConfigEntry): +async def test_upnp_sensors(hass: HomeAssistant, mock_config_entry: MockConfigEntry): """Test normal sensors.""" # 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" + assert hass.states.get("sensor.mock_name_b_received").state == "0" + assert hass.states.get("sensor.mock_name_b_sent").state == "0" + assert hass.states.get("sensor.mock_name_packets_received").state == "0" + assert hass.states.get("sensor.mock_name_packets_sent").state == "0" + assert hass.states.get("sensor.mock_name_external_ip").state == "8.9.10.11" + assert hass.states.get("sensor.mock_name_wan_status").state == "Connected" # Second poll. - mock_device: MockIgdDevice = hass.data[DOMAIN][ - config_entry.entry_id - ].device._igd_device - mock_device.traffic_data = { - BYTES_RECEIVED: 10240, - BYTES_SENT: 20480, - PACKETS_RECEIVED: 30, - PACKETS_SENT: 40, - } - mock_device.status_data = { - WAN_STATUS: "Disconnected", - ROUTER_UPTIME: 100, - ROUTER_IP: "", - } + mock_igd_device = mock_config_entry.igd_device + mock_igd_device.async_get_total_bytes_received.return_value = 10240 + mock_igd_device.async_get_total_bytes_sent.return_value = 20480 + mock_igd_device.async_get_total_packets_received.return_value = 30 + mock_igd_device.async_get_total_packets_sent.return_value = 40 + mock_igd_device.async_get_status_info.return_value = StatusInfo( + "Disconnected", + "", + 40, + ) + mock_igd_device.async_get_external_ip_address.return_value = "" + now = dt_util.utcnow() async_fire_time_changed(hass, now + timedelta(seconds=DEFAULT_SCAN_INTERVAL)) 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" + assert hass.states.get("sensor.mock_name_b_received").state == "10240" + assert hass.states.get("sensor.mock_name_b_sent").state == "20480" + assert hass.states.get("sensor.mock_name_packets_received").state == "30" + assert hass.states.get("sensor.mock_name_packets_sent").state == "40" + assert hass.states.get("sensor.mock_name_external_ip").state == "" + assert hass.states.get("sensor.mock_name_wan_status").state == "Disconnected" -async def test_derived_upnp_sensors(hass: HomeAssistant, config_entry: MockConfigEntry): +async def test_derived_upnp_sensors( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +): """Test derived sensors.""" - coordinator: UpnpDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - # 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" + assert hass.states.get("sensor.mock_name_kib_s_received").state == "unknown" + assert hass.states.get("sensor.mock_name_kib_s_sent").state == "unknown" + assert hass.states.get("sensor.mock_name_packets_s_received").state == "unknown" + assert hass.states.get("sensor.mock_name_packets_s_sent").state == "unknown" # Second poll. + mock_igd_device = mock_config_entry.igd_device + mock_igd_device.async_get_total_bytes_received.return_value = int( + 10240 * DEFAULT_SCAN_INTERVAL + ) + mock_igd_device.async_get_total_bytes_sent.return_value = int( + 20480 * DEFAULT_SCAN_INTERVAL + ) + mock_igd_device.async_get_total_packets_received.return_value = int( + 30 * DEFAULT_SCAN_INTERVAL + ) + mock_igd_device.async_get_total_packets_sent.return_value = int( + 40 * DEFAULT_SCAN_INTERVAL + ) + now = dt_util.utcnow() with patch( "homeassistant.components.upnp.device.utcnow", return_value=now + timedelta(seconds=DEFAULT_SCAN_INTERVAL), ): - mock_device: MockIgdDevice = coordinator.device._igd_device - mock_device.traffic_data = { - BYTES_RECEIVED: int(10240 * DEFAULT_SCAN_INTERVAL), - BYTES_SENT: int(20480 * DEFAULT_SCAN_INTERVAL), - PACKETS_RECEIVED: int(30 * DEFAULT_SCAN_INTERVAL), - PACKETS_SENT: int(40 * DEFAULT_SCAN_INTERVAL), - } async_fire_time_changed(hass, now + timedelta(seconds=DEFAULT_SCAN_INTERVAL)) 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 float(kib_s_received_state.state) == pytest.approx(10.0, rel=0.1) - assert float(kib_s_sent_state.state) == pytest.approx(20.0, rel=0.1) - assert float(packets_s_received_state.state) == pytest.approx(30.0, rel=0.1) - assert float(packets_s_sent_state.state) == pytest.approx(40.0, rel=0.1) + assert float( + hass.states.get("sensor.mock_name_kib_s_received").state + ) == pytest.approx(10.0, rel=0.1) + assert float( + hass.states.get("sensor.mock_name_kib_s_sent").state + ) == pytest.approx(20.0, rel=0.1) + assert float( + hass.states.get("sensor.mock_name_packets_s_received").state + ) == pytest.approx(30.0, rel=0.1) + assert float( + hass.states.get("sensor.mock_name_packets_s_sent").state + ) == pytest.approx(40.0, rel=0.1) From f33151ff8b9457f19619d3c3a94ea7aad0b5f988 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 24 May 2022 21:42:11 +0200 Subject: [PATCH 0853/3516] Adjust config-flow type hints in unifi (#72411) * Adjust config-flow type hints in unifi * Use mapping * Use mapping * Fix tests * Fix tests * Revert "Use mapping" This reverts commit 126fedc84828dfa2badc1b6f673ab8a4e702d230. --- homeassistant/components/unifi/config_flow.py | 50 +++++++++++++------ tests/components/unifi/conftest.py | 2 +- tests/components/unifi/test_config_flow.py | 6 +-- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index d02f3f49a5e..50e578b1dae 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -5,7 +5,11 @@ Discovery of UniFi Network instances hosted on UDM and UDM Pro devices through SSDP. Reauthentication when issue with credentials are reported. Configuration of options through options flow. """ +from __future__ import annotations + +from collections.abc import Mapping import socket +from typing import Any from urllib.parse import urlparse import voluptuous as vol @@ -19,7 +23,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import format_mac @@ -63,11 +67,13 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> UnifiOptionsFlowHandler: """Get the options flow for this handler.""" return UnifiOptionsFlowHandler(config_entry) - def __init__(self): + def __init__(self) -> None: """Initialize the UniFi Network flow.""" self.config = {} self.site_ids = {} @@ -75,7 +81,9 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): self.reauth_config_entry = None self.reauth_schema = {} - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -123,7 +131,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): return await self.async_step_site() - if not (host := self.config.get(CONF_HOST, "")) and await async_discover_unifi( + if not (host := self.config.get(CONF_HOST, "")) and await _async_discover_unifi( self.hass ): host = "unifi" @@ -144,7 +152,9 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): errors=errors, ) - async def async_step_site(self, user_input=None): + async def async_step_site( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Select site to control.""" errors = {} @@ -192,7 +202,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): errors=errors, ) - async def async_step_reauth(self, data: dict): + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Trigger a reauthentication flow.""" config_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -248,13 +258,15 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): class UnifiOptionsFlowHandler(config_entries.OptionsFlow): """Handle Unifi Network options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize UniFi Network options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) self.controller = None - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the UniFi Network options.""" if self.config_entry.entry_id not in self.hass.data[UNIFI_DOMAIN]: return self.async_abort(reason="integration_not_setup") @@ -266,7 +278,9 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): return await self.async_step_simple_options() - async def async_step_simple_options(self, user_input=None): + async def async_step_simple_options( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """For users without advanced settings enabled.""" if user_input is not None: self.options.update(user_input) @@ -299,7 +313,9 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): last_step=True, ) - async def async_step_device_tracker(self, user_input=None): + async def async_step_device_tracker( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the device tracker options.""" if user_input is not None: self.options.update(user_input) @@ -359,7 +375,9 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): last_step=False, ) - async def async_step_client_control(self, user_input=None): + async def async_step_client_control( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage configuration of network access controlled clients.""" errors = {} @@ -403,7 +421,9 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): last_step=False, ) - async def async_step_statistics_sensors(self, user_input=None): + async def async_step_statistics_sensors( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the statistics sensors options.""" if user_input is not None: self.options.update(user_input) @@ -426,12 +446,12 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): last_step=True, ) - async def _update_options(self): + async def _update_options(self) -> FlowResult: """Update config entry options.""" return self.async_create_entry(title="", data=self.options) -async def async_discover_unifi(hass): +async def _async_discover_unifi(hass: HomeAssistant) -> str | None: """Discover UniFi Network address.""" try: return await hass.async_add_executor_job(socket.gethostbyname, "unifi") diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index e21c458386f..e2b77ac1ed1 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -34,7 +34,7 @@ def mock_unifi_websocket(): def mock_discovery(): """No real network traffic allowed.""" with patch( - "homeassistant.components.unifi.config_flow.async_discover_unifi", + "homeassistant.components.unifi.config_flow._async_discover_unifi", return_value=None, ) as mock: yield mock diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 321cbdfd9e8..e774c5a551d 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -7,7 +7,7 @@ import aiounifi from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp -from homeassistant.components.unifi.config_flow import async_discover_unifi +from homeassistant.components.unifi.config_flow import _async_discover_unifi from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, CONF_ALLOW_UPTIME_SENSORS, @@ -686,10 +686,10 @@ async def test_form_ssdp_gets_form_with_ignored_entry(hass): async def test_discover_unifi_positive(hass): """Verify positive run of UniFi discovery.""" with patch("socket.gethostbyname", return_value=True): - assert await async_discover_unifi(hass) + assert await _async_discover_unifi(hass) async def test_discover_unifi_negative(hass): """Verify negative run of UniFi discovery.""" with patch("socket.gethostbyname", side_effect=socket.gaierror): - assert await async_discover_unifi(hass) is None + assert await _async_discover_unifi(hass) is None From cd769a55c2a06707dd1bb64ceb8e60ce7c9f78b4 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Tue, 24 May 2022 21:44:18 +0200 Subject: [PATCH 0854/3516] Update BMW connected drive to async (#71827) * Change BMW connected drive to async * Fix coordinator exceptions, fix tests * Fix using deprecated property * Write HA state directly * Remove code that cannnot throw Exceptions from try/except * Use public async_write_ha_state Co-authored-by: Martin Hjelmare * Fix login using refresh_token if token expired * MyPy fixes * Fix pytest, bump dependency * Replace OptionFlow listener with explicit refresh * Remove uneeded async_get_entry * Update test to include change on OptionsFlow * Bump bimmer_connected to 0.9.0 * Migrate renamed entitity unique_ids * Don't replace async_migrate_entries, add tests * Rename existing_entry to existing_entry_id Co-authored-by: Martin Hjelmare * Update tests * Import full EntityRegistry * Fix comment * Increase timeout to 60s * Rely on library timeout Co-authored-by: rikroe Co-authored-by: Martin Hjelmare --- .../bmw_connected_drive/__init__.py | 81 +++++++---- .../bmw_connected_drive/binary_sensor.py | 97 ++++++------- .../components/bmw_connected_drive/button.py | 38 +++-- .../bmw_connected_drive/config_flow.py | 46 +++--- .../components/bmw_connected_drive/const.py | 5 +- .../bmw_connected_drive/coordinator.py | 82 ++++++----- .../bmw_connected_drive/device_tracker.py | 20 +-- .../components/bmw_connected_drive/lock.py | 32 ++--- .../bmw_connected_drive/manifest.json | 2 +- .../components/bmw_connected_drive/notify.py | 8 +- .../components/bmw_connected_drive/sensor.py | 58 +++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../bmw_connected_drive/__init__.py | 27 ++++ .../bmw_connected_drive/test_config_flow.py | 37 ++--- .../bmw_connected_drive/test_init.py | 136 ++++++++++++++++++ 16 files changed, 436 insertions(+), 237 deletions(-) create mode 100644 tests/components/bmw_connected_drive/test_init.py diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index dae211f91a2..7023dd7481a 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -1,31 +1,27 @@ -"""Reads vehicle status from BMW connected drive portal.""" +"""Reads vehicle status from MyBMW portal.""" from __future__ import annotations +import logging from typing import Any -from bimmer_connected.vehicle import ConnectedDriveVehicle +from bimmer_connected.vehicle import MyBMWVehicle import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_DEVICE_ID, - CONF_ENTITY_ID, - CONF_NAME, - CONF_PASSWORD, - CONF_REGION, - CONF_USERNAME, - Platform, -) +from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITY_ID, CONF_NAME, Platform from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import discovery +from homeassistant.helpers import discovery, entity_registry as er import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTR_VIN, ATTRIBUTION, CONF_READ_ONLY, DATA_HASS_CONFIG, DOMAIN from .coordinator import BMWDataUpdateCoordinator +_LOGGER = logging.getLogger(__name__) + + CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) SERVICE_SCHEMA = vol.Schema( @@ -74,18 +70,56 @@ def _async_migrate_options_from_data_if_missing( hass.config_entries.async_update_entry(entry, data=data, options=options) +async def _async_migrate_entries( + hass: HomeAssistant, config_entry: ConfigEntry +) -> bool: + """Migrate old entry.""" + entity_registry = er.async_get(hass) + + @callback + def update_unique_id(entry: er.RegistryEntry) -> dict[str, str] | None: + replacements = { + "charging_level_hv": "remaining_battery_percent", + "fuel_percent": "remaining_fuel_percent", + } + if (key := entry.unique_id.split("-")[-1]) in replacements: + new_unique_id = entry.unique_id.replace(key, replacements[key]) + _LOGGER.debug( + "Migrating entity '%s' unique_id from '%s' to '%s'", + entry.entity_id, + entry.unique_id, + new_unique_id, + ) + if existing_entity_id := entity_registry.async_get_entity_id( + entry.domain, entry.platform, new_unique_id + ): + _LOGGER.debug( + "Cannot migrate to unique_id '%s', already exists for '%s'", + new_unique_id, + existing_entity_id, + ) + return None + return { + "new_unique_id": new_unique_id, + } + return None + + await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up BMW Connected Drive from a config entry.""" _async_migrate_options_from_data_if_missing(hass, entry) + await _async_migrate_entries(hass, entry) + # Set up one data coordinator per account/config entry coordinator = BMWDataUpdateCoordinator( hass, - username=entry.data[CONF_USERNAME], - password=entry.data[CONF_PASSWORD], - region=entry.data[CONF_REGION], - read_only=entry.options[CONF_READ_ONLY], + entry=entry, ) await coordinator.async_config_entry_first_refresh() @@ -109,9 +143,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) - # Add event listener for option flow changes - entry.async_on_unload(entry.add_update_listener(async_update_options)) - return True @@ -127,20 +158,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None: - """Handle options update.""" - await hass.config_entries.async_reload(config_entry.entry_id) - - -class BMWConnectedDriveBaseEntity(CoordinatorEntity[BMWDataUpdateCoordinator], Entity): +class BMWBaseEntity(CoordinatorEntity[BMWDataUpdateCoordinator]): """Common base for BMW entities.""" + coordinator: BMWDataUpdateCoordinator _attr_attribution = ATTRIBUTION def __init__( self, coordinator: BMWDataUpdateCoordinator, - vehicle: ConnectedDriveVehicle, + vehicle: MyBMWVehicle, ) -> None: """Initialize entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index cae70f6de4b..a19ccc8f715 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -1,18 +1,15 @@ -"""Reads vehicle status from BMW connected drive portal.""" +"""Reads vehicle status from BMW MyBMW portal.""" from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass import logging -from typing import Any, cast +from typing import Any -from bimmer_connected.vehicle import ConnectedDriveVehicle -from bimmer_connected.vehicle_status import ( - ChargingState, - ConditionBasedServiceReport, - LockState, - VehicleStatus, -) +from bimmer_connected.vehicle import MyBMWVehicle +from bimmer_connected.vehicle.doors_windows import LockState +from bimmer_connected.vehicle.fuel_and_battery import ChargingState +from bimmer_connected.vehicle.reports import ConditionBasedService from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -24,7 +21,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.unit_system import UnitSystem -from . import BMWConnectedDriveBaseEntity +from . import BMWBaseEntity from .const import DOMAIN, UNIT_MAP from .coordinator import BMWDataUpdateCoordinator @@ -32,20 +29,20 @@ _LOGGER = logging.getLogger(__name__) def _condition_based_services( - vehicle_state: VehicleStatus, unit_system: UnitSystem + vehicle: MyBMWVehicle, unit_system: UnitSystem ) -> dict[str, Any]: extra_attributes = {} - for report in vehicle_state.condition_based_services: + for report in vehicle.condition_based_services.messages: extra_attributes.update(_format_cbs_report(report, unit_system)) return extra_attributes -def _check_control_messages(vehicle_state: VehicleStatus) -> dict[str, Any]: +def _check_control_messages(vehicle: MyBMWVehicle) -> dict[str, Any]: extra_attributes: dict[str, Any] = {} - if vehicle_state.has_check_control_messages: + if vehicle.check_control_messages.has_check_control_messages: cbs_list = [ message.description_short - for message in vehicle_state.check_control_messages + for message in vehicle.check_control_messages.messages ] extra_attributes["check_control_messages"] = cbs_list else: @@ -54,18 +51,18 @@ def _check_control_messages(vehicle_state: VehicleStatus) -> dict[str, Any]: def _format_cbs_report( - report: ConditionBasedServiceReport, unit_system: UnitSystem + report: ConditionBasedService, unit_system: UnitSystem ) -> dict[str, Any]: result: dict[str, Any] = {} service_type = report.service_type.lower().replace("_", " ") result[f"{service_type} status"] = report.state.value 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: + if report.due_distance.value and report.due_distance.unit: distance = round( unit_system.length( - report.due_distance[0], - UNIT_MAP.get(report.due_distance[1], report.due_distance[1]), + report.due_distance.value, + UNIT_MAP.get(report.due_distance.unit, report.due_distance.unit), ) ) result[f"{service_type} distance"] = f"{distance} {unit_system.length_unit}" @@ -76,7 +73,7 @@ def _format_cbs_report( class BMWRequiredKeysMixin: """Mixin for required keys.""" - value_fn: Callable[[VehicleStatus], bool] + value_fn: Callable[[MyBMWVehicle], bool] @dataclass @@ -85,7 +82,7 @@ class BMWBinarySensorEntityDescription( ): """Describes BMW binary_sensor entity.""" - attr_fn: Callable[[VehicleStatus, UnitSystem], dict[str, Any]] | None = None + attr_fn: Callable[[MyBMWVehicle, UnitSystem], dict[str, Any]] | None = None SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( @@ -95,8 +92,10 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.OPENING, icon="mdi:car-door-lock", # device class opening: On means open, Off means closed - value_fn=lambda s: not s.all_lids_closed, - attr_fn=lambda s, u: {lid.name: lid.state.value for lid in s.lids}, + value_fn=lambda v: not v.doors_and_windows.all_lids_closed, + attr_fn=lambda v, u: { + lid.name: lid.state.value for lid in v.doors_and_windows.lids + }, ), BMWBinarySensorEntityDescription( key="windows", @@ -104,8 +103,10 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.OPENING, icon="mdi:car-door", # device class opening: On means open, Off means closed - value_fn=lambda s: not s.all_windows_closed, - attr_fn=lambda s, u: {window.name: window.state.value for window in s.windows}, + value_fn=lambda v: not v.doors_and_windows.all_windows_closed, + attr_fn=lambda v, u: { + window.name: window.state.value for window in v.doors_and_windows.windows + }, ), BMWBinarySensorEntityDescription( key="door_lock_state", @@ -114,29 +115,19 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( icon="mdi:car-key", # device class lock: On means unlocked, Off means locked # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED - value_fn=lambda s: s.door_lock_state + value_fn=lambda v: v.doors_and_windows.door_lock_state not in {LockState.LOCKED, LockState.SECURED}, - attr_fn=lambda s, u: { - "door_lock_state": s.door_lock_state.value, - "last_update_reason": s.last_update_reason, + attr_fn=lambda v, u: { + "door_lock_state": v.doors_and_windows.door_lock_state.value }, ), - BMWBinarySensorEntityDescription( - key="lights_parking", - name="Parking lights", - device_class=BinarySensorDeviceClass.LIGHT, - icon="mdi:car-parking-lights", - # device class light: On means light detected, Off means no light - value_fn=lambda s: cast(bool, s.are_parking_lights_on), - attr_fn=lambda s, u: {"lights_parking": s.parking_lights.value}, - ), BMWBinarySensorEntityDescription( key="condition_based_services", name="Condition based services", device_class=BinarySensorDeviceClass.PROBLEM, icon="mdi:wrench", # device class problem: On means problem detected, Off means no problem - value_fn=lambda s: not s.are_all_cbs_ok, + value_fn=lambda v: v.condition_based_services.is_service_required, attr_fn=_condition_based_services, ), BMWBinarySensorEntityDescription( @@ -145,8 +136,8 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.PROBLEM, icon="mdi:car-tire-alert", # device class problem: On means problem detected, Off means no problem - value_fn=lambda s: cast(bool, s.has_check_control_messages), - attr_fn=lambda s, u: _check_control_messages(s), + value_fn=lambda v: v.check_control_messages.has_check_control_messages, + attr_fn=lambda v, u: _check_control_messages(v), ), # electric BMWBinarySensorEntityDescription( @@ -155,10 +146,9 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.BATTERY_CHARGING, icon="mdi:ev-station", # device class power: On means power detected, Off means no power - value_fn=lambda s: cast(bool, s.charging_status == ChargingState.CHARGING), - attr_fn=lambda s, u: { - "charging_status": s.charging_status.value, - "last_charging_end_result": s.last_charging_end_result, + value_fn=lambda v: v.fuel_and_battery.charging_status == ChargingState.CHARGING, + attr_fn=lambda v, u: { + "charging_status": str(v.fuel_and_battery.charging_status), }, ), BMWBinarySensorEntityDescription( @@ -166,8 +156,7 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( name="Connection status", device_class=BinarySensorDeviceClass.PLUG, icon="mdi:car-electric", - value_fn=lambda s: cast(str, s.connection_status) == "CONNECTED", - attr_fn=lambda s, u: {"connection_status": s.connection_status}, + value_fn=lambda v: v.fuel_and_battery.is_charger_connected, ), ) @@ -177,11 +166,11 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the BMW ConnectedDrive binary sensors from config entry.""" + """Set up the BMW binary sensors from config entry.""" coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] entities = [ - BMWConnectedDriveSensor(coordinator, vehicle, description, hass.config.units) + BMWBinarySensor(coordinator, vehicle, description, hass.config.units) for vehicle in coordinator.account.vehicles for description in SENSOR_TYPES if description.key in vehicle.available_attributes @@ -189,7 +178,7 @@ async def async_setup_entry( async_add_entities(entities) -class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): +class BMWBinarySensor(BMWBaseEntity, BinarySensorEntity): """Representation of a BMW vehicle binary sensor.""" entity_description: BMWBinarySensorEntityDescription @@ -197,7 +186,7 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): def __init__( self, coordinator: BMWDataUpdateCoordinator, - vehicle: ConnectedDriveVehicle, + vehicle: MyBMWVehicle, description: BMWBinarySensorEntityDescription, unit_system: UnitSystem, ) -> None: @@ -217,14 +206,12 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): self.entity_description.key, self.vehicle.name, ) - vehicle_state = self.vehicle.status - - self._attr_is_on = self.entity_description.value_fn(vehicle_state) + self._attr_is_on = self.entity_description.value_fn(self.vehicle) if self.entity_description.attr_fn: self._attr_extra_state_attributes = dict( self._attrs, - **self.entity_description.attr_fn(vehicle_state, self._unit_system), + **self.entity_description.attr_fn(self.vehicle, self._unit_system), ) super()._handle_coordinator_update() diff --git a/homeassistant/components/bmw_connected_drive/button.py b/homeassistant/components/bmw_connected_drive/button.py index 254fbebfdac..9cec9a73ce7 100644 --- a/homeassistant/components/bmw_connected_drive/button.py +++ b/homeassistant/components/bmw_connected_drive/button.py @@ -1,19 +1,20 @@ -"""Support for BMW connected drive button entities.""" +"""Support for MyBMW button entities.""" from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any -from bimmer_connected.vehicle import ConnectedDriveVehicle +from bimmer_connected.vehicle import MyBMWVehicle +from bimmer_connected.vehicle.remote_services import RemoteServiceStatus from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BMWConnectedDriveBaseEntity +from . import BMWBaseEntity from .const import DOMAIN if TYPE_CHECKING: @@ -27,7 +28,9 @@ class BMWButtonEntityDescription(ButtonEntityDescription): """Class describing BMW button entities.""" enabled_when_read_only: bool = False - remote_function: str | None = None + remote_function: Callable[ + [MyBMWVehicle], Coroutine[Any, Any, RemoteServiceStatus] + ] | None = None account_function: Callable[[BMWDataUpdateCoordinator], Coroutine] | None = None @@ -36,31 +39,31 @@ BUTTON_TYPES: tuple[BMWButtonEntityDescription, ...] = ( key="light_flash", icon="mdi:car-light-alert", name="Flash Lights", - remote_function="trigger_remote_light_flash", + remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_light_flash(), ), BMWButtonEntityDescription( key="sound_horn", icon="mdi:bullhorn", name="Sound Horn", - remote_function="trigger_remote_horn", + remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_horn(), ), BMWButtonEntityDescription( key="activate_air_conditioning", icon="mdi:hvac", name="Activate Air Conditioning", - remote_function="trigger_remote_air_conditioning", + remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_air_conditioning(), ), BMWButtonEntityDescription( key="deactivate_air_conditioning", icon="mdi:hvac-off", name="Deactivate Air Conditioning", - remote_function="trigger_remote_air_conditioning_stop", + remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_air_conditioning_stop(), ), BMWButtonEntityDescription( key="find_vehicle", icon="mdi:crosshairs-question", name="Find Vehicle", - remote_function="trigger_remote_vehicle_finder", + remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_vehicle_finder(), ), BMWButtonEntityDescription( key="refresh", @@ -77,7 +80,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the BMW ConnectedDrive buttons from config entry.""" + """Set up the BMW buttons from config entry.""" coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] entities: list[BMWButton] = [] @@ -95,15 +98,15 @@ async def async_setup_entry( async_add_entities(entities) -class BMWButton(BMWConnectedDriveBaseEntity, ButtonEntity): - """Representation of a BMW Connected Drive button.""" +class BMWButton(BMWBaseEntity, ButtonEntity): + """Representation of a MyBMW button.""" entity_description: BMWButtonEntityDescription def __init__( self, coordinator: BMWDataUpdateCoordinator, - vehicle: ConnectedDriveVehicle, + vehicle: MyBMWVehicle, description: BMWButtonEntityDescription, ) -> None: """Initialize BMW vehicle sensor.""" @@ -116,12 +119,7 @@ class BMWButton(BMWConnectedDriveBaseEntity, ButtonEntity): async def async_press(self) -> None: """Press the button.""" if self.entity_description.remote_function: - await self.hass.async_add_executor_job( - getattr( - self.vehicle.remote_services, - self.entity_description.remote_function, - ) - ) + await self.entity_description.remote_function(self.vehicle) elif self.entity_description.account_function: _LOGGER.warning( "The 'Refresh from cloud' button is deprecated. Use the 'homeassistant.update_entity' " diff --git a/homeassistant/components/bmw_connected_drive/config_flow.py b/homeassistant/components/bmw_connected_drive/config_flow.py index fec25390ff4..c07be4c8849 100644 --- a/homeassistant/components/bmw_connected_drive/config_flow.py +++ b/homeassistant/components/bmw_connected_drive/config_flow.py @@ -3,8 +3,9 @@ from __future__ import annotations from typing import Any -from bimmer_connected.account import ConnectedDriveAccount -from bimmer_connected.country_selector import get_region_from_name +from bimmer_connected.account import MyBMWAccount +from bimmer_connected.api.regions import get_region_from_name +from httpx import HTTPError import voluptuous as vol from homeassistant import config_entries, core, exceptions @@ -31,22 +32,23 @@ async def validate_input( Data has the keys from DATA_SCHEMA with values provided by the user. """ + account = MyBMWAccount( + data[CONF_USERNAME], + data[CONF_PASSWORD], + get_region_from_name(data[CONF_REGION]), + ) + try: - await hass.async_add_executor_job( - ConnectedDriveAccount, - data[CONF_USERNAME], - data[CONF_PASSWORD], - get_region_from_name(data[CONF_REGION]), - ) - except OSError as ex: + await account.get_vehicles() + except HTTPError as ex: raise CannotConnect from ex # Return info that you want to store in the config entry. return {"title": f"{data[CONF_USERNAME]}{data.get(CONF_SOURCE, '')}"} -class BMWConnectedDriveConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for BMW ConnectedDrive.""" +class BMWConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for MyBMW.""" VERSION = 1 @@ -78,16 +80,16 @@ class BMWConnectedDriveConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @callback def async_get_options_flow( config_entry: config_entries.ConfigEntry, - ) -> BMWConnectedDriveOptionsFlow: - """Return a BWM ConnectedDrive option flow.""" - return BMWConnectedDriveOptionsFlow(config_entry) + ) -> BMWOptionsFlow: + """Return a MyBMW option flow.""" + return BMWOptionsFlow(config_entry) -class BMWConnectedDriveOptionsFlow(config_entries.OptionsFlow): - """Handle a option flow for BMW ConnectedDrive.""" +class BMWOptionsFlow(config_entries.OptionsFlow): + """Handle a option flow for MyBMW.""" def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize BMW ConnectedDrive option flow.""" + """Initialize MyBMW option flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) @@ -102,6 +104,16 @@ class BMWConnectedDriveOptionsFlow(config_entries.OptionsFlow): ) -> FlowResult: """Handle the initial step.""" if user_input is not None: + # Manually update & reload the config entry after options change. + # Required as each successful login will store the latest refresh_token + # using async_update_entry, which would otherwise trigger a full reload + # if the options would be refreshed using a listener. + changed = self.hass.config_entries.async_update_entry( + self.config_entry, + options=user_input, + ) + if changed: + await self.hass.config_entries.async_reload(self.config_entry.entry_id) return self.async_create_entry(title="", data=user_input) return self.async_show_form( step_id="account_options", diff --git a/homeassistant/components/bmw_connected_drive/const.py b/homeassistant/components/bmw_connected_drive/const.py index a2082c0bede..6a8f82ae22d 100644 --- a/homeassistant/components/bmw_connected_drive/const.py +++ b/homeassistant/components/bmw_connected_drive/const.py @@ -1,4 +1,4 @@ -"""Const file for the BMW Connected Drive integration.""" +"""Const file for the MyBMW integration.""" from homeassistant.const import ( LENGTH_KILOMETERS, LENGTH_MILES, @@ -7,7 +7,7 @@ from homeassistant.const import ( ) DOMAIN = "bmw_connected_drive" -ATTRIBUTION = "Data provided by BMW Connected Drive" +ATTRIBUTION = "Data provided by MyBMW" ATTR_DIRECTION = "direction" ATTR_VIN = "vin" @@ -15,6 +15,7 @@ ATTR_VIN = "vin" CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"] CONF_READ_ONLY = "read_only" CONF_ACCOUNT = "account" +CONF_REFRESH_TOKEN = "refresh_token" DATA_HASS_CONFIG = "hass_config" diff --git a/homeassistant/components/bmw_connected_drive/coordinator.py b/homeassistant/components/bmw_connected_drive/coordinator.py index a02b4bdd27c..cff532ae3cb 100644 --- a/homeassistant/components/bmw_connected_drive/coordinator.py +++ b/homeassistant/components/bmw_connected_drive/coordinator.py @@ -4,14 +4,17 @@ from __future__ import annotations from datetime import timedelta import logging -import async_timeout -from bimmer_connected.account import ConnectedDriveAccount -from bimmer_connected.country_selector import get_region_from_name +from bimmer_connected.account import MyBMWAccount +from bimmer_connected.api.regions import get_region_from_name +from bimmer_connected.vehicle.models import GPSPosition +from httpx import HTTPError, TimeoutException +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN +from .const import CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN SCAN_INTERVAL = timedelta(seconds=300) _LOGGER = logging.getLogger(__name__) @@ -20,53 +23,56 @@ _LOGGER = logging.getLogger(__name__) class BMWDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching BMW data.""" - account: ConnectedDriveAccount + account: MyBMWAccount - def __init__( - self, - hass: HomeAssistant, - *, - username: str, - password: str, - region: str, - read_only: bool = False, - ) -> None: + def __init__(self, hass: HomeAssistant, *, entry: ConfigEntry) -> None: """Initialize account-wide BMW data updater.""" - # Storing username & password in coordinator is needed until a new library version - # that does not do blocking IO on init. - self._username = username - self._password = password - self._region = get_region_from_name(region) + self.account = MyBMWAccount( + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + get_region_from_name(entry.data[CONF_REGION]), + observer_position=GPSPosition(hass.config.latitude, hass.config.longitude), + ) + self.read_only = entry.options[CONF_READ_ONLY] + self._entry = entry - self.account = None - self.read_only = read_only + if CONF_REFRESH_TOKEN in entry.data: + self.account.set_refresh_token(entry.data[CONF_REFRESH_TOKEN]) super().__init__( hass, _LOGGER, - name=f"{DOMAIN}-{username}", + name=f"{DOMAIN}-{entry.data['username']}", update_interval=SCAN_INTERVAL, ) async def _async_update_data(self) -> None: """Fetch data from BMW.""" + old_refresh_token = self.account.refresh_token + try: - async with async_timeout.timeout(15): - if isinstance(self.account, ConnectedDriveAccount): - # pylint: disable=protected-access - await self.hass.async_add_executor_job(self.account._get_vehicles) - else: - self.account = await self.hass.async_add_executor_job( - ConnectedDriveAccount, - self._username, - self._password, - self._region, - ) - self.account.set_observer_position( - self.hass.config.latitude, self.hass.config.longitude - ) - except OSError as err: - raise UpdateFailed(f"Error communicating with API: {err}") from err + await self.account.get_vehicles() + except (HTTPError, TimeoutException) as err: + self._update_config_entry_refresh_token(None) + raise UpdateFailed(f"Error communicating with BMW API: {err}") from err + + if self.account.refresh_token != old_refresh_token: + self._update_config_entry_refresh_token(self.account.refresh_token) + _LOGGER.debug( + "bimmer_connected: refresh token %s > %s", + old_refresh_token, + self.account.refresh_token, + ) + + def _update_config_entry_refresh_token(self, refresh_token: str | None) -> None: + """Update or delete the refresh_token in the Config Entry.""" + data = { + **self._entry.data, + CONF_REFRESH_TOKEN: refresh_token, + } + if not refresh_token: + data.pop(CONF_REFRESH_TOKEN) + self.hass.config_entries.async_update_entry(self._entry, data=data) def notify_listeners(self) -> None: """Notify all listeners to refresh HA state machine.""" diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index b1fa429f5b9..dc71100455d 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -1,10 +1,10 @@ -"""Device tracker for BMW Connected Drive vehicles.""" +"""Device tracker for MyBMW vehicles.""" from __future__ import annotations import logging from typing import Literal -from bimmer_connected.vehicle import ConnectedDriveVehicle +from bimmer_connected.vehicle import MyBMWVehicle from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity @@ -12,7 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BMWConnectedDriveBaseEntity +from . import BMWBaseEntity from .const import ATTR_DIRECTION, DOMAIN from .coordinator import BMWDataUpdateCoordinator @@ -24,7 +24,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the BMW ConnectedDrive tracker from config entry.""" + """Set up the MyBMW tracker from config entry.""" coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] entities: list[BMWDeviceTracker] = [] @@ -39,8 +39,8 @@ async def async_setup_entry( async_add_entities(entities) -class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): - """BMW Connected Drive device tracker.""" +class BMWDeviceTracker(BMWBaseEntity, TrackerEntity): + """MyBMW device tracker.""" _attr_force_update = False _attr_icon = "mdi:car" @@ -48,7 +48,7 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): def __init__( self, coordinator: BMWDataUpdateCoordinator, - vehicle: ConnectedDriveVehicle, + vehicle: MyBMWVehicle, ) -> None: """Initialize the Tracker.""" super().__init__(coordinator, vehicle) @@ -59,13 +59,13 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): @property def extra_state_attributes(self) -> dict: """Return entity specific state attributes.""" - return dict(self._attrs, **{ATTR_DIRECTION: self.vehicle.status.gps_heading}) + return {**self._attrs, ATTR_DIRECTION: self.vehicle.vehicle_location.heading} @property def latitude(self) -> float | None: """Return latitude value of the device.""" return ( - self.vehicle.status.gps_position[0] + self.vehicle.vehicle_location.location[0] if self.vehicle.is_vehicle_tracking_enabled else None ) @@ -74,7 +74,7 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): def longitude(self) -> float | None: """Return longitude value of the device.""" return ( - self.vehicle.status.gps_position[1] + self.vehicle.vehicle_location.location[1] if self.vehicle.is_vehicle_tracking_enabled else None ) diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index a395c80ebcc..0c2c5a1e832 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -4,15 +4,15 @@ from __future__ import annotations import logging from typing import Any -from bimmer_connected.vehicle import ConnectedDriveVehicle -from bimmer_connected.vehicle_status import LockState +from bimmer_connected.vehicle import MyBMWVehicle +from bimmer_connected.vehicle.doors_windows import LockState from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BMWConnectedDriveBaseEntity +from . import BMWBaseEntity from .const import DOMAIN from .coordinator import BMWDataUpdateCoordinator @@ -25,7 +25,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the BMW ConnectedDrive binary sensors from config entry.""" + """Set up the MyBMW lock from config entry.""" coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] entities: list[BMWLock] = [] @@ -36,13 +36,13 @@ async def async_setup_entry( async_add_entities(entities) -class BMWLock(BMWConnectedDriveBaseEntity, LockEntity): - """Representation of a BMW vehicle lock.""" +class BMWLock(BMWBaseEntity, LockEntity): + """Representation of a MyBMW vehicle lock.""" def __init__( self, coordinator: BMWDataUpdateCoordinator, - vehicle: ConnectedDriveVehicle, + vehicle: MyBMWVehicle, attribute: str, sensor_name: str, ) -> None: @@ -55,7 +55,7 @@ class BMWLock(BMWConnectedDriveBaseEntity, LockEntity): self._sensor_name = sensor_name self.door_lock_state_available = DOOR_LOCK_STATE in vehicle.available_attributes - def lock(self, **kwargs: Any) -> None: + async def async_lock(self, **kwargs: Any) -> None: """Lock the car.""" _LOGGER.debug("%s: locking doors", self.vehicle.name) # Only update the HA state machine if the vehicle reliably reports its lock state @@ -63,10 +63,10 @@ class BMWLock(BMWConnectedDriveBaseEntity, LockEntity): # Optimistic state set here because it takes some time before the # update callback response self._attr_is_locked = True - self.schedule_update_ha_state() - self.vehicle.remote_services.trigger_remote_door_lock() + self.async_write_ha_state() + await self.vehicle.remote_services.trigger_remote_door_lock() - def unlock(self, **kwargs: Any) -> None: + async def async_unlock(self, **kwargs: Any) -> None: """Unlock the car.""" _LOGGER.debug("%s: unlocking doors", self.vehicle.name) # Only update the HA state machine if the vehicle reliably reports its lock state @@ -74,8 +74,8 @@ class BMWLock(BMWConnectedDriveBaseEntity, LockEntity): # Optimistic state set here because it takes some time before the # update callback response self._attr_is_locked = False - self.schedule_update_ha_state() - self.vehicle.remote_services.trigger_remote_door_unlock() + self.async_write_ha_state() + await self.vehicle.remote_services.trigger_remote_door_unlock() @callback def _handle_coordinator_update(self) -> None: @@ -83,16 +83,14 @@ class BMWLock(BMWConnectedDriveBaseEntity, LockEntity): _LOGGER.debug("Updating lock data of %s", self.vehicle.name) # Only update the HA state machine if the vehicle reliably reports its lock state if self.door_lock_state_available: - vehicle_state = self.vehicle.status - self._attr_is_locked = vehicle_state.door_lock_state in { + self._attr_is_locked = self.vehicle.doors_and_windows.door_lock_state in { LockState.LOCKED, LockState.SECURED, } self._attr_extra_state_attributes = dict( self._attrs, **{ - "door_lock_state": vehicle_state.door_lock_state.value, - "last_update_reason": vehicle_state.last_update_reason, + "door_lock_state": self.vehicle.doors_and_windows.door_lock_state.value, }, ) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 2d1b5e43984..d41a87ef2c1 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.8.12"], + "requirements": ["bimmer_connected==0.9.0"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/bmw_connected_drive/notify.py b/homeassistant/components/bmw_connected_drive/notify.py index 42e9c834459..14f6c94dff6 100644 --- a/homeassistant/components/bmw_connected_drive/notify.py +++ b/homeassistant/components/bmw_connected_drive/notify.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from typing import Any, cast -from bimmer_connected.vehicle import ConnectedDriveVehicle +from bimmer_connected.vehicle import MyBMWVehicle from homeassistant.components.notify import ( ATTR_DATA, @@ -52,14 +52,14 @@ def get_service( class BMWNotificationService(BaseNotificationService): """Send Notifications to BMW.""" - def __init__(self, targets: dict[str, ConnectedDriveVehicle]) -> None: + def __init__(self, targets: dict[str, MyBMWVehicle]) -> None: """Set up the notification service.""" - self.targets: dict[str, ConnectedDriveVehicle] = targets + self.targets: dict[str, MyBMWVehicle] = targets def send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message or POI to the car.""" for vehicle in kwargs[ATTR_TARGET]: - vehicle = cast(ConnectedDriveVehicle, vehicle) + vehicle = cast(MyBMWVehicle, vehicle) _LOGGER.debug("Sending message to %s", vehicle.name) # Extract params from data dict diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 4a928870eb2..3021e180158 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -1,4 +1,4 @@ -"""Support for reading vehicle status from BMW connected drive portal.""" +"""Support for reading vehicle status from MyBMW portal.""" from __future__ import annotations from collections.abc import Callable @@ -6,7 +6,8 @@ from dataclasses import dataclass import logging from typing import cast -from bimmer_connected.vehicle import ConnectedDriveVehicle +from bimmer_connected.vehicle import MyBMWVehicle +from bimmer_connected.vehicle.models import ValueWithUnit from homeassistant.components.sensor import ( SensorDeviceClass, @@ -27,7 +28,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.util.unit_system import UnitSystem -from . import BMWConnectedDriveBaseEntity +from . import BMWBaseEntity from .const import DOMAIN, UNIT_MAP from .coordinator import BMWDataUpdateCoordinator @@ -38,44 +39,54 @@ _LOGGER = logging.getLogger(__name__) class BMWSensorEntityDescription(SensorEntityDescription): """Describes BMW sensor entity.""" + key_class: str | None = None unit_metric: str | None = None unit_imperial: str | None = None value: Callable = lambda x, y: x def convert_and_round( - state: tuple, + state: ValueWithUnit, converter: Callable[[float | None, str], float], precision: int, ) -> float | None: - """Safely convert and round a value from a Tuple[value, unit].""" - if state[0] is None: - return None - return round(converter(state[0], UNIT_MAP.get(state[1], state[1])), precision) + """Safely convert and round a value from ValueWithUnit.""" + if state.value and state.unit: + return round( + converter(state.value, UNIT_MAP.get(state.unit, state.unit)), precision + ) + if state.value: + return state.value + return None SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { # --- Generic --- "charging_start_time": BMWSensorEntityDescription( key="charging_start_time", + key_class="fuel_and_battery", device_class=SensorDeviceClass.TIMESTAMP, entity_registry_enabled_default=False, ), "charging_end_time": BMWSensorEntityDescription( key="charging_end_time", + key_class="fuel_and_battery", device_class=SensorDeviceClass.TIMESTAMP, ), "charging_time_label": BMWSensorEntityDescription( key="charging_time_label", + key_class="fuel_and_battery", entity_registry_enabled_default=False, ), "charging_status": BMWSensorEntityDescription( key="charging_status", + key_class="fuel_and_battery", icon="mdi:ev-station", value=lambda x, y: x.value, ), - "charging_level_hv": BMWSensorEntityDescription( - key="charging_level_hv", + "remaining_battery_percent": BMWSensorEntityDescription( + key="remaining_battery_percent", + key_class="fuel_and_battery", unit_metric=PERCENTAGE, unit_imperial=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, @@ -90,6 +101,7 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { ), "remaining_range_total": BMWSensorEntityDescription( key="remaining_range_total", + key_class="fuel_and_battery", icon="mdi:map-marker-distance", unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, @@ -97,6 +109,7 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { ), "remaining_range_electric": BMWSensorEntityDescription( key="remaining_range_electric", + key_class="fuel_and_battery", icon="mdi:map-marker-distance", unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, @@ -104,6 +117,7 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { ), "remaining_range_fuel": BMWSensorEntityDescription( key="remaining_range_fuel", + key_class="fuel_and_battery", icon="mdi:map-marker-distance", unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, @@ -111,13 +125,15 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { ), "remaining_fuel": BMWSensorEntityDescription( key="remaining_fuel", + key_class="fuel_and_battery", icon="mdi:gas-station", unit_metric=VOLUME_LITERS, unit_imperial=VOLUME_GALLONS, value=lambda x, hass: convert_and_round(x, hass.config.units.volume, 2), ), - "fuel_percent": BMWSensorEntityDescription( - key="fuel_percent", + "remaining_fuel_percent": BMWSensorEntityDescription( + key="remaining_fuel_percent", + key_class="fuel_and_battery", icon="mdi:gas-station", unit_metric=PERCENTAGE, unit_imperial=PERCENTAGE, @@ -130,16 +146,16 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the BMW ConnectedDrive sensors from config entry.""" + """Set up the MyBMW sensors from config entry.""" unit_system = hass.config.units coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - entities: list[BMWConnectedDriveSensor] = [] + entities: list[BMWSensor] = [] for vehicle in coordinator.account.vehicles: entities.extend( [ - BMWConnectedDriveSensor(coordinator, vehicle, description, unit_system) + BMWSensor(coordinator, vehicle, description, unit_system) for attribute_name in vehicle.available_attributes if (description := SENSOR_TYPES.get(attribute_name)) ] @@ -148,7 +164,7 @@ async def async_setup_entry( async_add_entities(entities) -class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): +class BMWSensor(BMWBaseEntity, SensorEntity): """Representation of a BMW vehicle sensor.""" entity_description: BMWSensorEntityDescription @@ -156,7 +172,7 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): def __init__( self, coordinator: BMWDataUpdateCoordinator, - vehicle: ConnectedDriveVehicle, + vehicle: MyBMWVehicle, description: BMWSensorEntityDescription, unit_system: UnitSystem, ) -> None: @@ -178,7 +194,13 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): _LOGGER.debug( "Updating sensor '%s' of %s", self.entity_description.key, self.vehicle.name ) - state = getattr(self.vehicle.status, self.entity_description.key) + if self.entity_description.key_class is None: + state = getattr(self.vehicle, self.entity_description.key) + else: + state = getattr( + getattr(self.vehicle, self.entity_description.key_class), + self.entity_description.key, + ) self._attr_native_value = cast( StateType, self.entity_description.value(state, self.hass) ) diff --git a/requirements_all.txt b/requirements_all.txt index e9438a330a5..448748afb71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -397,7 +397,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.8.12 +bimmer_connected==0.9.0 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e072289ac45..33b778e80d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -312,7 +312,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.8.12 +bimmer_connected==0.9.0 # homeassistant.components.blebox blebox_uniapi==1.3.3 diff --git a/tests/components/bmw_connected_drive/__init__.py b/tests/components/bmw_connected_drive/__init__.py index e1243fe2c0a..4774032b409 100644 --- a/tests/components/bmw_connected_drive/__init__.py +++ b/tests/components/bmw_connected_drive/__init__.py @@ -1 +1,28 @@ """Tests for the for the BMW Connected Drive integration.""" + +from homeassistant import config_entries +from homeassistant.components.bmw_connected_drive.const import ( + CONF_READ_ONLY, + DOMAIN as BMW_DOMAIN, +) +from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME + +FIXTURE_USER_INPUT = { + CONF_USERNAME: "user@domain.com", + CONF_PASSWORD: "p4ssw0rd", + CONF_REGION: "rest_of_world", +} + +FIXTURE_CONFIG_ENTRY = { + "entry_id": "1", + "domain": BMW_DOMAIN, + "title": FIXTURE_USER_INPUT[CONF_USERNAME], + "data": { + CONF_USERNAME: FIXTURE_USER_INPUT[CONF_USERNAME], + CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD], + CONF_REGION: FIXTURE_USER_INPUT[CONF_REGION], + }, + "options": {CONF_READ_ONLY: False}, + "source": config_entries.SOURCE_USER, + "unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}", +} diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index 644da56a91d..d3c7c64dc99 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -1,35 +1,20 @@ """Test the for the BMW Connected Drive config flow.""" from unittest.mock import patch +from httpx import HTTPError + from homeassistant import config_entries, data_entry_flow from homeassistant.components.bmw_connected_drive.config_flow import DOMAIN from homeassistant.components.bmw_connected_drive.const import CONF_READ_ONLY -from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME +from homeassistant.const import CONF_USERNAME + +from . import FIXTURE_CONFIG_ENTRY, FIXTURE_USER_INPUT from tests.common import MockConfigEntry -FIXTURE_USER_INPUT = { - CONF_USERNAME: "user@domain.com", - CONF_PASSWORD: "p4ssw0rd", - CONF_REGION: "rest_of_world", -} FIXTURE_COMPLETE_ENTRY = FIXTURE_USER_INPUT.copy() FIXTURE_IMPORT_ENTRY = FIXTURE_USER_INPUT.copy() -FIXTURE_CONFIG_ENTRY = { - "entry_id": "1", - "domain": DOMAIN, - "title": FIXTURE_USER_INPUT[CONF_USERNAME], - "data": { - CONF_USERNAME: FIXTURE_USER_INPUT[CONF_USERNAME], - CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD], - CONF_REGION: FIXTURE_USER_INPUT[CONF_REGION], - }, - "options": {CONF_READ_ONLY: False}, - "source": config_entries.SOURCE_USER, - "unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}", -} - async def test_show_form(hass): """Test that the form is served with no input.""" @@ -48,8 +33,8 @@ async def test_connection_error(hass): pass with patch( - "bimmer_connected.account.ConnectedDriveAccount._get_oauth_token", - side_effect=OSError, + "bimmer_connected.api.authentication.MyBMWAuthentication.login", + side_effect=HTTPError("login failure"), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -65,7 +50,7 @@ async def test_connection_error(hass): async def test_full_user_flow_implementation(hass): """Test registering an integration and finishing flow works.""" with patch( - "bimmer_connected.account.ConnectedDriveAccount._get_vehicles", + "bimmer_connected.account.MyBMWAccount.get_vehicles", return_value=[], ), patch( "homeassistant.components.bmw_connected_drive.async_setup_entry", @@ -86,7 +71,7 @@ async def test_full_user_flow_implementation(hass): async def test_options_flow_implementation(hass): """Test config flow options.""" with patch( - "bimmer_connected.account.ConnectedDriveAccount._get_vehicles", + "bimmer_connected.account.MyBMWAccount.get_vehicles", return_value=[], ), patch( "homeassistant.components.bmw_connected_drive.async_setup_entry", @@ -104,13 +89,13 @@ async def test_options_flow_implementation(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={CONF_READ_ONLY: False}, + user_input={CONF_READ_ONLY: True}, ) await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == { - CONF_READ_ONLY: False, + CONF_READ_ONLY: True, } assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/bmw_connected_drive/test_init.py b/tests/components/bmw_connected_drive/test_init.py new file mode 100644 index 00000000000..70a64090203 --- /dev/null +++ b/tests/components/bmw_connected_drive/test_init.py @@ -0,0 +1,136 @@ +"""Test Axis component setup process.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.bmw_connected_drive.const import DOMAIN as BMW_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import FIXTURE_CONFIG_ENTRY + +from tests.common import MockConfigEntry + +VIN = "WBYYYYYYYYYYYYYYY" +VEHICLE_NAME = "i3 (+ REX)" +VEHICLE_NAME_SLUG = "i3_rex" + + +@pytest.mark.parametrize( + "entitydata,old_unique_id,new_unique_id", + [ + ( + { + "domain": SENSOR_DOMAIN, + "platform": BMW_DOMAIN, + "unique_id": f"{VIN}-charging_level_hv", + "suggested_object_id": f"{VEHICLE_NAME} charging_level_hv", + "disabled_by": None, + }, + f"{VIN}-charging_level_hv", + f"{VIN}-remaining_battery_percent", + ), + ( + { + "domain": SENSOR_DOMAIN, + "platform": BMW_DOMAIN, + "unique_id": f"{VIN}-remaining_range_total", + "suggested_object_id": f"{VEHICLE_NAME} remaining_range_total", + "disabled_by": None, + }, + f"{VIN}-remaining_range_total", + f"{VIN}-remaining_range_total", + ), + ], +) +async def test_migrate_unique_ids( + hass: HomeAssistant, + entitydata: dict, + old_unique_id: str, + new_unique_id: str, +) -> None: + """Test successful migration of entity unique_ids.""" + mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + mock_config_entry.add_to_hass(hass) + + entity_registry = er.async_get(hass) + entity: er.RegistryEntry = entity_registry.async_get_or_create( + **entitydata, + config_entry=mock_config_entry, + ) + + assert entity.unique_id == old_unique_id + + with patch( + "bimmer_connected.account.MyBMWAccount.get_vehicles", + return_value=[], + ): + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + entity_migrated = entity_registry.async_get(entity.entity_id) + assert entity_migrated + assert entity_migrated.unique_id == new_unique_id + + +@pytest.mark.parametrize( + "entitydata,old_unique_id,new_unique_id", + [ + ( + { + "domain": SENSOR_DOMAIN, + "platform": BMW_DOMAIN, + "unique_id": f"{VIN}-charging_level_hv", + "suggested_object_id": f"{VEHICLE_NAME} charging_level_hv", + "disabled_by": None, + }, + f"{VIN}-charging_level_hv", + f"{VIN}-remaining_battery_percent", + ), + ], +) +async def test_dont_migrate_unique_ids( + hass: HomeAssistant, + entitydata: dict, + old_unique_id: str, + new_unique_id: str, +) -> None: + """Test successful migration of entity unique_ids.""" + mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + mock_config_entry.add_to_hass(hass) + + entity_registry = er.async_get(hass) + + # create existing entry with new_unique_id + existing_entity = entity_registry.async_get_or_create( + SENSOR_DOMAIN, + BMW_DOMAIN, + unique_id=f"{VIN}-remaining_battery_percent", + suggested_object_id=f"{VEHICLE_NAME} remaining_battery_percent", + config_entry=mock_config_entry, + ) + + entity: er.RegistryEntry = entity_registry.async_get_or_create( + **entitydata, + config_entry=mock_config_entry, + ) + + assert entity.unique_id == old_unique_id + + with patch( + "bimmer_connected.account.MyBMWAccount.get_vehicles", + return_value=[], + ): + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + entity_migrated = entity_registry.async_get(entity.entity_id) + assert entity_migrated + assert entity_migrated.unique_id == old_unique_id + + entity_not_changed = entity_registry.async_get(existing_entity.entity_id) + assert entity_not_changed + assert entity_not_changed.unique_id == new_unique_id + + assert entity_migrated != entity_not_changed From c4ca10637990ec1123883d7889ba2a382829d5e2 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 24 May 2022 23:16:58 +0200 Subject: [PATCH 0855/3516] Clean zwave_js api driver access (#72419) --- homeassistant/components/zwave_js/__init__.py | 2 +- homeassistant/components/zwave_js/api.py | 107 ++++++++++++------ homeassistant/components/zwave_js/helpers.py | 17 ++- 3 files changed, 84 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 0c716a39c75..9f928eb7d3d 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -566,7 +566,7 @@ async def setup_driver( # noqa: C901 # If opt in preference hasn't been specified yet, we do nothing, otherwise # we apply the preference if opted_in := entry.data.get(CONF_DATA_COLLECTION_OPTED_IN): - await async_enable_statistics(client) + await async_enable_statistics(driver) elif opted_in is False: await driver.async_disable_statistics() diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 66c497a791f..15d3a68d4a4 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -32,6 +32,7 @@ from zwave_js_server.model.controller import ( ProvisioningEntry, QRProvisioningInformation, ) +from zwave_js_server.model.driver import Driver from zwave_js_server.model.firmware import ( FirmwareUpdateFinished, FirmwareUpdateProgress, @@ -243,8 +244,17 @@ def async_get_entry(orig_func: Callable) -> Callable: ) return - client = hass.data[DOMAIN][entry_id][DATA_CLIENT] - await orig_func(hass, connection, msg, entry, client) + client: Client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + + if client.driver is None: + connection.send_error( + msg[ID], + ERR_NOT_LOADED, + f"Config entry {entry_id} not loaded, driver not ready", + ) + return + + await orig_func(hass, connection, msg, entry, client, client.driver) return async_get_entry_func @@ -373,16 +383,20 @@ async def websocket_network_status( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Get the status of the Z-Wave JS network.""" - controller = client.driver.controller + controller = driver.controller + client_version_info = client.version + assert client_version_info # When client is connected version info is set. + await controller.async_get_state() data = { "client": { "ws_server_url": client.ws_server_url, "state": "connected" if client.connected else "disconnected", - "driver_version": client.version.driver_version, - "server_version": client.version.server_version, + "driver_version": client_version_info.driver_version, + "server_version": client_version_info.server_version, }, "controller": { "home_id": controller.home_id, @@ -404,9 +418,7 @@ async def websocket_network_status( "supports_timers": controller.supports_timers, "is_heal_network_active": controller.is_heal_network_active, "inclusion_state": controller.inclusion_state, - "nodes": [ - node_status(node) for node in client.driver.controller.nodes.values() - ], + "nodes": [node_status(node) for node in driver.controller.nodes.values()], }, } connection.send_result( @@ -533,9 +545,10 @@ async def websocket_add_node( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Add a node to the Z-Wave network.""" - controller = client.driver.controller + controller = driver.controller inclusion_strategy = InclusionStrategy(msg[INCLUSION_STRATEGY]) force_security = msg.get(FORCE_SECURITY) provisioning = ( @@ -672,13 +685,14 @@ async def websocket_grant_security_classes( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Choose SecurityClass grants as part of S2 inclusion process.""" inclusion_grant = InclusionGrant( [SecurityClass(sec_cls) for sec_cls in msg[SECURITY_CLASSES]], msg[CLIENT_SIDE_AUTH], ) - await client.driver.controller.async_grant_security_classes(inclusion_grant) + await driver.controller.async_grant_security_classes(inclusion_grant) connection.send_result(msg[ID]) @@ -699,9 +713,10 @@ async def websocket_validate_dsk_and_enter_pin( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Validate DSK and enter PIN as part of S2 inclusion process.""" - await client.driver.controller.async_validate_dsk_and_enter_pin(msg[PIN]) + await driver.controller.async_validate_dsk_and_enter_pin(msg[PIN]) connection.send_result(msg[ID]) @@ -728,6 +743,7 @@ async def websocket_provision_smart_start_node( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Pre-provision a smart start node.""" try: @@ -758,7 +774,7 @@ async def websocket_provision_smart_start_node( "QR code version S2 is not supported for this command", ) return - await client.driver.controller.async_provision_smart_start_node(provisioning_info) + await driver.controller.async_provision_smart_start_node(provisioning_info) connection.send_result(msg[ID]) @@ -780,6 +796,7 @@ async def websocket_unprovision_smart_start_node( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Unprovision a smart start node.""" try: @@ -792,7 +809,7 @@ async def websocket_unprovision_smart_start_node( ) return dsk_or_node_id = msg.get(DSK) or msg[NODE_ID] - await client.driver.controller.async_unprovision_smart_start_node(dsk_or_node_id) + await driver.controller.async_unprovision_smart_start_node(dsk_or_node_id) connection.send_result(msg[ID]) @@ -812,11 +829,10 @@ async def websocket_get_provisioning_entries( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Get provisioning entries (entries that have been pre-provisioned).""" - provisioning_entries = ( - await client.driver.controller.async_get_provisioning_entries() - ) + provisioning_entries = await driver.controller.async_get_provisioning_entries() connection.send_result( msg[ID], [dataclasses.asdict(entry) for entry in provisioning_entries] ) @@ -839,6 +855,7 @@ async def websocket_parse_qr_code_string( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Parse a QR Code String and return QRProvisioningInformation dict.""" qr_provisioning_information = await async_parse_qr_code_string( @@ -864,9 +881,10 @@ async def websocket_supports_feature( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Check if controller supports a particular feature.""" - supported = await client.driver.controller.async_supports_feature(msg[FEATURE]) + supported = await driver.controller.async_supports_feature(msg[FEATURE]) connection.send_result( msg[ID], {"supported": supported}, @@ -889,9 +907,10 @@ async def websocket_stop_inclusion( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Cancel adding a node to the Z-Wave network.""" - controller = client.driver.controller + controller = driver.controller result = await controller.async_stop_inclusion() connection.send_result( msg[ID], @@ -915,9 +934,10 @@ async def websocket_stop_exclusion( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Cancel removing a node from the Z-Wave network.""" - controller = client.driver.controller + controller = driver.controller result = await controller.async_stop_exclusion() connection.send_result( msg[ID], @@ -942,9 +962,10 @@ async def websocket_remove_node( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Remove a node from the Z-Wave network.""" - controller = client.driver.controller + controller = driver.controller @callback def async_cleanup() -> None: @@ -1021,9 +1042,10 @@ async def websocket_replace_failed_node( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Replace a failed node with a new node.""" - controller = client.driver.controller + controller = driver.controller node_id = msg[NODE_ID] inclusion_strategy = InclusionStrategy(msg[INCLUSION_STRATEGY]) force_security = msg.get(FORCE_SECURITY) @@ -1173,7 +1195,9 @@ async def websocket_remove_failed_node( node: Node, ) -> None: """Remove a failed node from the Z-Wave network.""" - controller = node.client.driver.controller + driver = node.client.driver + assert driver is not None # The node comes from the driver instance. + controller = driver.controller @callback def async_cleanup() -> None: @@ -1217,9 +1241,10 @@ async def websocket_begin_healing_network( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Begin healing the Z-Wave network.""" - controller = client.driver.controller + controller = driver.controller result = await controller.async_begin_healing_network() connection.send_result( @@ -1243,9 +1268,10 @@ async def websocket_subscribe_heal_network_progress( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Subscribe to heal Z-Wave network status updates.""" - controller = client.driver.controller + controller = driver.controller @callback def async_cleanup() -> None: @@ -1286,9 +1312,10 @@ async def websocket_stop_healing_network( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Stop healing the Z-Wave network.""" - controller = client.driver.controller + controller = driver.controller result = await controller.async_stop_healing_network() connection.send_result( msg[ID], @@ -1313,7 +1340,10 @@ async def websocket_heal_node( node: Node, ) -> None: """Heal a node on the Z-Wave network.""" - controller = node.client.driver.controller + driver = node.client.driver + assert driver is not None # The node comes from the driver instance. + controller = driver.controller + result = await controller.async_heal_node(node.node_id) connection.send_result( msg[ID], @@ -1540,9 +1570,9 @@ async def websocket_subscribe_log_updates( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Subscribe to log message events from the server.""" - driver = client.driver @callback def async_cleanup() -> None: @@ -1627,9 +1657,10 @@ async def websocket_update_log_config( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Update the driver log config.""" - await client.driver.async_update_log_config(LogConfig(**msg[CONFIG])) + await driver.async_update_log_config(LogConfig(**msg[CONFIG])) connection.send_result( msg[ID], ) @@ -1650,11 +1681,12 @@ async def websocket_get_log_config( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Get log configuration for the Z-Wave JS driver.""" connection.send_result( msg[ID], - dataclasses.asdict(client.driver.log_config), + dataclasses.asdict(driver.log_config), ) @@ -1675,15 +1707,16 @@ async def websocket_update_data_collection_preference( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Update preference for data collection and enable/disable collection.""" opted_in = msg[OPTED_IN] update_data_collection_preference(hass, entry, opted_in) if opted_in: - await async_enable_statistics(client) + await async_enable_statistics(driver) else: - await client.driver.async_disable_statistics() + await driver.async_disable_statistics() connection.send_result( msg[ID], @@ -1706,11 +1739,12 @@ async def websocket_data_collection_status( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Return data collection preference and status.""" result = { OPTED_IN: entry.data.get(CONF_DATA_COLLECTION_OPTED_IN), - ENABLED: await client.driver.async_is_statistics_enabled(), + ENABLED: await driver.async_is_statistics_enabled(), } connection.send_result(msg[ID], result) @@ -1890,9 +1924,10 @@ async def websocket_check_for_config_updates( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Check for config updates.""" - config_update = await client.driver.async_check_for_config_updates() + config_update = await driver.async_check_for_config_updates() connection.send_result( msg[ID], { @@ -1918,9 +1953,10 @@ async def websocket_install_config_update( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Check for config updates.""" - success = await client.driver.async_install_config_update() + success = await driver.async_install_config_update() connection.send_result(msg[ID], success) @@ -1956,6 +1992,7 @@ async def websocket_subscribe_controller_statistics( msg: dict, entry: ConfigEntry, client: Client, + driver: Driver, ) -> None: """Subsribe to the statistics updates for a controller.""" @@ -1979,7 +2016,7 @@ async def websocket_subscribe_controller_statistics( ) ) - controller = client.driver.controller + controller = driver.controller msg[DATA_UNSUBSCRIBE] = unsubs = [ controller.on("statistics updated", forward_stats) diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 68ff0c89b15..0657d8531f3 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -9,6 +9,7 @@ from typing import Any, cast import voluptuous as vol from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import ConfigurationValueType +from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import ( ConfigurationValue, @@ -92,10 +93,10 @@ def get_value_of_zwave_value(value: ZwaveValue | None) -> Any | None: return value.value if value else None -async def async_enable_statistics(client: ZwaveClient) -> None: +async def async_enable_statistics(driver: Driver) -> None: """Enable statistics on the driver.""" - await client.driver.async_enable_statistics("Home Assistant", HA_VERSION) - await client.driver.async_enable_error_reporting() + await driver.async_enable_statistics("Home Assistant", HA_VERSION) + await driver.async_enable_error_reporting() @callback @@ -194,7 +195,11 @@ def async_get_node_from_device_id( f"Device {device_id} is not from an existing zwave_js config entry" ) - client = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] + client: ZwaveClient = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] + driver = client.driver + + if driver is None: + raise ValueError("Driver is not ready.") # Get node ID from device identifier, perform some validation, and then get the # node @@ -202,10 +207,10 @@ def async_get_node_from_device_id( node_id = identifiers[1] if identifiers else None - if node_id is None or node_id not in client.driver.controller.nodes: + if node_id is None or node_id not in driver.controller.nodes: raise ValueError(f"Node for device {device_id} can't be found") - return client.driver.controller.nodes[node_id] + return driver.controller.nodes[node_id] @callback From f7475a5bdb9ad505a9edeccc3ed2928526450dee Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 24 May 2022 23:52:07 +0200 Subject: [PATCH 0856/3516] Clean zwave_js entity driver access (#72427) --- homeassistant/components/zwave_js/__init__.py | 28 ++++++------- .../components/zwave_js/binary_sensor.py | 21 ++++++---- homeassistant/components/zwave_js/button.py | 11 +++-- homeassistant/components/zwave_js/climate.py | 15 ++++--- homeassistant/components/zwave_js/cover.py | 21 ++++++---- homeassistant/components/zwave_js/entity.py | 19 +++++---- homeassistant/components/zwave_js/fan.py | 21 ++++++---- homeassistant/components/zwave_js/helpers.py | 16 +++---- .../components/zwave_js/humidifier.py | 11 +++-- homeassistant/components/zwave_js/light.py | 15 ++++--- homeassistant/components/zwave_js/lock.py | 4 +- homeassistant/components/zwave_js/migrate.py | 8 ++-- homeassistant/components/zwave_js/number.py | 15 ++++--- homeassistant/components/zwave_js/select.py | 21 ++++++---- homeassistant/components/zwave_js/sensor.py | 36 +++++++++------- homeassistant/components/zwave_js/siren.py | 9 ++-- homeassistant/components/zwave_js/switch.py | 15 ++++--- .../components/zwave_js/triggers/event.py | 4 +- .../zwave_js/triggers/value_updated.py | 4 +- tests/components/zwave_js/test_api.py | 7 ++-- tests/components/zwave_js/test_button.py | 5 ++- .../components/zwave_js/test_device_action.py | 32 ++++++++++---- tests/components/zwave_js/test_diagnostics.py | 2 +- tests/components/zwave_js/test_init.py | 8 ++-- tests/components/zwave_js/test_migrate.py | 42 ++++++++++++------- tests/components/zwave_js/test_sensor.py | 5 ++- tests/components/zwave_js/test_services.py | 20 ++++++--- 27 files changed, 250 insertions(+), 165 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 9f928eb7d3d..5c583d8321f 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -124,13 +124,13 @@ def register_node_in_dev_reg( hass: HomeAssistant, entry: ConfigEntry, dev_reg: device_registry.DeviceRegistry, - client: ZwaveClient, + driver: Driver, node: ZwaveNode, remove_device_func: Callable[[device_registry.DeviceEntry], None], ) -> device_registry.DeviceEntry: """Register node in dev reg.""" - device_id = get_device_id(client, node) - device_id_ext = get_device_id_ext(client, node) + device_id = get_device_id(driver, node) + device_id_ext = get_device_id_ext(driver, node) device = dev_reg.async_get_device({device_id}) # Replace the device if it can be determined that this node is not the @@ -281,7 +281,7 @@ async def setup_driver( # noqa: C901 ent_reg, registered_unique_ids[device.id][disc_info.platform], device, - client, + driver, disc_info, ) @@ -316,7 +316,7 @@ async def setup_driver( # noqa: C901 LOGGER.debug("Processing node %s", node) # register (or update) node in device registry device = register_node_in_dev_reg( - hass, entry, dev_reg, client, node, remove_device + hass, entry, dev_reg, driver, node, remove_device ) # We only want to create the defaultdict once, even on reinterviews if device.id not in registered_unique_ids: @@ -384,7 +384,7 @@ async def setup_driver( # noqa: C901 ) # we do submit the node to device registry so user has # some visual feedback that something is (in the process of) being added - register_node_in_dev_reg(hass, entry, dev_reg, client, node, remove_device) + register_node_in_dev_reg(hass, entry, dev_reg, driver, node, remove_device) async def async_on_value_added( value_updates_disc_info: dict[str, ZwaveDiscoveryInfo], value: Value @@ -393,7 +393,7 @@ async def setup_driver( # noqa: C901 # If node isn't ready or a device for this node doesn't already exist, we can # let the node ready event handler perform discovery. If a value has already # been processed, we don't need to do it again - device_id = get_device_id(client, value.node) + device_id = get_device_id(driver, value.node) if ( not value.node.ready or not (device := dev_reg.async_get_device({device_id})) @@ -417,7 +417,7 @@ async def setup_driver( # noqa: C901 node: ZwaveNode = event["node"] replaced: bool = event.get("replaced", False) # grab device in device registry attached to this node - dev_id = get_device_id(client, node) + dev_id = get_device_id(driver, node) device = dev_reg.async_get_device({dev_id}) # We assert because we know the device exists assert device @@ -426,7 +426,7 @@ async def setup_driver( # noqa: C901 async_dispatcher_send( hass, - f"{DOMAIN}_{get_valueless_base_unique_id(client, node)}_remove_entity", + f"{DOMAIN}_{get_valueless_base_unique_id(driver, node)}_remove_entity", ) else: remove_device(device) @@ -434,7 +434,7 @@ async def setup_driver( # noqa: C901 @callback def async_on_value_notification(notification: ValueNotification) -> None: """Relay stateless value notification events from Z-Wave nodes to hass.""" - device = dev_reg.async_get_device({get_device_id(client, notification.node)}) + device = dev_reg.async_get_device({get_device_id(driver, notification.node)}) # We assert because we know the device exists assert device raw_value = value = notification.value @@ -469,7 +469,7 @@ async def setup_driver( # noqa: C901 notification: EntryControlNotification | NotificationNotification | PowerLevelNotification | MultilevelSwitchNotification = event[ "notification" ] - device = dev_reg.async_get_device({get_device_id(client, notification.node)}) + device = dev_reg.async_get_device({get_device_id(driver, notification.node)}) # We assert because we know the device exists assert device event_data = { @@ -533,11 +533,11 @@ async def setup_driver( # noqa: C901 return disc_info = value_updates_disc_info[value.value_id] - device = dev_reg.async_get_device({get_device_id(client, value.node)}) + device = dev_reg.async_get_device({get_device_id(driver, value.node)}) # We assert because we know the device exists assert device - unique_id = get_unique_id(client, disc_info.primary_value.value_id) + unique_id = get_unique_id(driver, disc_info.primary_value.value_id) entity_id = ent_reg.async_get_entity_id(disc_info.platform, DOMAIN, unique_id) raw_value = value_ = value.value @@ -575,7 +575,7 @@ async def setup_driver( # noqa: C901 dev_reg, entry.entry_id ) known_devices = [ - dev_reg.async_get_device({get_device_id(client, node)}) + dev_reg.async_get_device({get_device_id(driver, node)}) for node in driver.controller.nodes.values() ] diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 8a86898239a..7e9882377bd 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -10,6 +10,7 @@ from zwave_js_server.const.command_class.lock import DOOR_STATUS_PROPERTY from zwave_js_server.const.command_class.notification import ( CC_SPECIFIC_NOTIFICATION_TYPE, ) +from zwave_js_server.model.driver import Driver from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR_DOMAIN, @@ -267,6 +268,8 @@ async def async_setup_entry( @callback def async_add_binary_sensor(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave Binary Sensor.""" + driver = client.driver + assert driver is not None # Driver is ready before platforms are loaded. entities: list[BinarySensorEntity] = [] if info.platform_hint == "notification": @@ -298,7 +301,7 @@ async def async_setup_entry( entities.append( ZWaveNotificationBinarySensor( - config_entry, client, info, state_key, notification_description + config_entry, driver, info, state_key, notification_description ) ) elif info.platform_hint == "property" and ( @@ -308,12 +311,12 @@ async def async_setup_entry( ): entities.append( ZWavePropertyBinarySensor( - config_entry, client, info, property_description + config_entry, driver, info, property_description ) ) else: # boolean sensor - entities.append(ZWaveBooleanBinarySensor(config_entry, client, info)) + entities.append(ZWaveBooleanBinarySensor(config_entry, driver, info)) async_add_entities(entities) @@ -332,11 +335,11 @@ class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity): def __init__( self, config_entry: ConfigEntry, - client: ZwaveClient, + driver: Driver, info: ZwaveDiscoveryInfo, ) -> None: """Initialize a ZWaveBooleanBinarySensor entity.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) # Entity class attributes self._attr_name = self.generate_name(include_value_name=True) @@ -359,13 +362,13 @@ class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): def __init__( self, config_entry: ConfigEntry, - client: ZwaveClient, + driver: Driver, info: ZwaveDiscoveryInfo, state_key: str, description: NotificationZWaveJSEntityDescription | None = None, ) -> None: """Initialize a ZWaveNotificationBinarySensor entity.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) self.state_key = state_key if description: self.entity_description = description @@ -394,12 +397,12 @@ class ZWavePropertyBinarySensor(ZWaveBaseEntity, BinarySensorEntity): def __init__( self, config_entry: ConfigEntry, - client: ZwaveClient, + driver: Driver, info: ZwaveDiscoveryInfo, description: PropertyZWaveJSEntityDescription, ) -> None: """Initialize a ZWavePropertyBinarySensor entity.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) self.entity_description = description self._attr_name = self.generate_name(include_value_name=True) diff --git a/homeassistant/components/zwave_js/button.py b/homeassistant/components/zwave_js/button.py index 49c2a76cccf..cef64f1724a 100644 --- a/homeassistant/components/zwave_js/button.py +++ b/homeassistant/components/zwave_js/button.py @@ -2,6 +2,7 @@ from __future__ import annotations from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node as ZwaveNode from homeassistant.components.button import ButtonEntity @@ -28,7 +29,9 @@ async def async_setup_entry( @callback def async_add_ping_button_entity(node: ZwaveNode) -> None: """Add ping button entity.""" - async_add_entities([ZWaveNodePingButton(client, node)]) + driver = client.driver + assert driver is not None # Driver is ready before platforms are loaded. + async_add_entities([ZWaveNodePingButton(driver, node)]) config_entry.async_on_unload( async_dispatcher_connect( @@ -45,7 +48,7 @@ class ZWaveNodePingButton(ButtonEntity): _attr_should_poll = False _attr_entity_category = EntityCategory.CONFIG - def __init__(self, client: ZwaveClient, node: ZwaveNode) -> None: + def __init__(self, driver: Driver, node: ZwaveNode) -> None: """Initialize a ping Z-Wave device button entity.""" self.node = node name: str = ( @@ -53,11 +56,11 @@ class ZWaveNodePingButton(ButtonEntity): ) # Entity class attributes self._attr_name = f"{name}: Ping" - self._base_unique_id = get_valueless_base_unique_id(client, node) + self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.ping" # device is precreated in main handler self._attr_device_info = DeviceInfo( - identifiers={get_device_id(client, node)}, + identifiers={get_device_id(driver, node)}, ) async def async_poll_value(self, _: bool) -> None: diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 67def698fc2..f721db41d9f 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -17,6 +17,7 @@ from zwave_js_server.const.command_class.thermostat import ( ThermostatOperatingState, ThermostatSetpointType, ) +from zwave_js_server.model.driver import Driver from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.components.climate import ( @@ -103,11 +104,13 @@ async def async_setup_entry( @callback def async_add_climate(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave Climate.""" + driver = client.driver + assert driver is not None # Driver is ready before platforms are loaded. entities: list[ZWaveBaseEntity] = [] if info.platform_hint == "dynamic_current_temp": - entities.append(DynamicCurrentTempClimate(config_entry, client, info)) + entities.append(DynamicCurrentTempClimate(config_entry, driver, info)) else: - entities.append(ZWaveClimate(config_entry, client, info)) + entities.append(ZWaveClimate(config_entry, driver, info)) async_add_entities(entities) @@ -124,10 +127,10 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): """Representation of a Z-Wave climate.""" def __init__( - self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize thermostat.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) self._hvac_modes: dict[HVACMode, int | None] = {} self._hvac_presets: dict[str, int | None] = {} self._unit_value: ZwaveValue | None = None @@ -479,10 +482,10 @@ class DynamicCurrentTempClimate(ZWaveClimate): """Representation of a thermostat that can dynamically use a different Zwave Value for current temp.""" def __init__( - self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize thermostat.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) self.data_template = cast( DynamicCurrentTempClimateDataTemplate, self.info.platform_data_template ) diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index c7ba50ee7e7..f9a9990ed77 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -15,6 +15,7 @@ from zwave_js_server.const.command_class.multilevel_switch import ( COVER_OPEN_PROPERTY, COVER_UP_PROPERTY, ) +from zwave_js_server.model.driver import Driver from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.components.cover import ( @@ -51,13 +52,15 @@ async def async_setup_entry( @callback def async_add_cover(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave cover.""" + driver = client.driver + assert driver is not None # Driver is ready before platforms are loaded. entities: list[ZWaveBaseEntity] = [] if info.platform_hint == "motorized_barrier": - entities.append(ZwaveMotorizedBarrier(config_entry, client, info)) + entities.append(ZwaveMotorizedBarrier(config_entry, driver, info)) elif info.platform_hint == "window_shutter_tilt": - entities.append(ZWaveTiltCover(config_entry, client, info)) + entities.append(ZWaveTiltCover(config_entry, driver, info)) else: - entities.append(ZWaveCover(config_entry, client, info)) + entities.append(ZWaveCover(config_entry, driver, info)) async_add_entities(entities) config_entry.async_on_unload( @@ -105,11 +108,11 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity): def __init__( self, config_entry: ConfigEntry, - client: ZwaveClient, + driver: Driver, info: ZwaveDiscoveryInfo, ) -> None: """Initialize a ZWaveCover entity.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) # Entity class attributes self._attr_device_class = CoverDeviceClass.WINDOW @@ -188,11 +191,11 @@ class ZWaveTiltCover(ZWaveCover): def __init__( self, config_entry: ConfigEntry, - client: ZwaveClient, + driver: Driver, info: ZwaveDiscoveryInfo, ) -> None: """Initialize a ZWaveCover entity.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) self.data_template = cast( CoverTiltDataTemplate, self.info.platform_data_template ) @@ -233,11 +236,11 @@ class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity): def __init__( self, config_entry: ConfigEntry, - client: ZwaveClient, + driver: Driver, info: ZwaveDiscoveryInfo, ) -> None: """Initialize a ZwaveMotorizedBarrier entity.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) self._target_state: ZwaveValue = self.get_zwave_value( TARGET_STATE_PROPERTY, add_to_watched_value_ids=False ) diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index b89b4c9c9de..a4271ac1c02 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -3,8 +3,8 @@ from __future__ import annotations import logging -from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import NodeStatus +from zwave_js_server.model.driver import Driver from zwave_js_server.model.value import Value as ZwaveValue, get_value_id from homeassistant.config_entries import ConfigEntry @@ -30,11 +30,11 @@ class ZWaveBaseEntity(Entity): _attr_should_poll = False def __init__( - self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a generic Z-Wave device entity.""" self.config_entry = config_entry - self.client = client + self.driver = driver self.info = info # entities requiring additional values, can add extra ids to this list self.watched_value_ids = {self.info.primary_value.value_id} @@ -46,16 +46,14 @@ class ZWaveBaseEntity(Entity): # Entity class attributes self._attr_name = self.generate_name() - self._attr_unique_id = get_unique_id( - self.client, self.info.primary_value.value_id - ) + self._attr_unique_id = get_unique_id(driver, self.info.primary_value.value_id) self._attr_entity_registry_enabled_default = ( self.info.entity_registry_enabled_default ) self._attr_assumed_state = self.info.assumed_state # device is precreated in main handler self._attr_device_info = DeviceInfo( - identifiers={get_device_id(self.client, self.info.node)}, + identifiers={get_device_id(driver, self.info.node)}, ) @callback @@ -145,7 +143,10 @@ class ZWaveBaseEntity(Entity): if item: name += f" - {item}" # append endpoint if > 1 - if self.info.primary_value.endpoint > 1: + if ( + self.info.primary_value.endpoint is not None + and self.info.primary_value.endpoint > 1 + ): name += f" ({self.info.primary_value.endpoint})" return name @@ -154,7 +155,7 @@ class ZWaveBaseEntity(Entity): def available(self) -> bool: """Return entity availability.""" return ( - self.client.connected + self.driver.client.connected and bool(self.info.node.ready) and self.info.node.status != NodeStatus.DEAD ) diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index f571884ac80..eb6d053f958 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -10,6 +10,7 @@ from zwave_js_server.const.command_class.thermostat import ( THERMOSTAT_FAN_OFF_PROPERTY, THERMOSTAT_FAN_STATE_PROPERTY, ) +from zwave_js_server.model.driver import Driver from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.components.fan import ( @@ -53,13 +54,15 @@ async def async_setup_entry( @callback def async_add_fan(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave fan.""" + driver = client.driver + assert driver is not None # Driver is ready before platforms are loaded. entities: list[ZWaveBaseEntity] = [] if info.platform_hint == "has_fan_value_mapping": - entities.append(ValueMappingZwaveFan(config_entry, client, info)) + entities.append(ValueMappingZwaveFan(config_entry, driver, info)) elif info.platform_hint == "thermostat_fan": - entities.append(ZwaveThermostatFan(config_entry, client, info)) + entities.append(ZwaveThermostatFan(config_entry, driver, info)) else: - entities.append(ZwaveFan(config_entry, client, info)) + entities.append(ZwaveFan(config_entry, driver, info)) async_add_entities(entities) @@ -78,10 +81,10 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): _attr_supported_features = FanEntityFeature.SET_SPEED def __init__( - self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize the fan.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) self._target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY) async def async_set_percentage(self, percentage: int) -> None: @@ -147,10 +150,10 @@ class ValueMappingZwaveFan(ZwaveFan): """A Zwave fan with a value mapping data (e.g., 1-24 is low).""" def __init__( - self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize the fan.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) self.data_template = cast( FanValueMappingDataTemplate, self.info.platform_data_template ) @@ -300,10 +303,10 @@ class ZwaveThermostatFan(ZWaveBaseEntity, FanEntity): _fan_state: ZwaveValue | None = None def __init__( - self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize the thermostat fan.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) self._fan_mode = self.info.primary_value diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 0657d8531f3..807ae0287eb 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -110,29 +110,29 @@ def update_data_collection_preference( @callback -def get_valueless_base_unique_id(client: ZwaveClient, node: ZwaveNode) -> str: +def get_valueless_base_unique_id(driver: Driver, node: ZwaveNode) -> str: """Return the base unique ID for an entity that is not based on a value.""" - return f"{client.driver.controller.home_id}.{node.node_id}" + return f"{driver.controller.home_id}.{node.node_id}" -def get_unique_id(client: ZwaveClient, value_id: str) -> str: +def get_unique_id(driver: Driver, value_id: str) -> str: """Get unique ID from client and value ID.""" - return f"{client.driver.controller.home_id}.{value_id}" + return f"{driver.controller.home_id}.{value_id}" @callback -def get_device_id(client: ZwaveClient, node: ZwaveNode) -> tuple[str, str]: +def get_device_id(driver: Driver, node: ZwaveNode) -> tuple[str, str]: """Get device registry identifier for Z-Wave node.""" - return (DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}") + return (DOMAIN, f"{driver.controller.home_id}-{node.node_id}") @callback -def get_device_id_ext(client: ZwaveClient, node: ZwaveNode) -> tuple[str, str] | None: +def get_device_id_ext(driver: Driver, node: ZwaveNode) -> tuple[str, str] | None: """Get extended device registry identifier for Z-Wave node.""" if None in (node.manufacturer_id, node.product_type, node.product_id): return None - domain, dev_id = get_device_id(client, node) + domain, dev_id = get_device_id(driver, node) return ( domain, f"{dev_id}-{node.manufacturer_id}:{node.product_type}:{node.product_id}", diff --git a/homeassistant/components/zwave_js/humidifier.py b/homeassistant/components/zwave_js/humidifier.py index 8cf0b6aec7c..5aeb6d0272f 100644 --- a/homeassistant/components/zwave_js/humidifier.py +++ b/homeassistant/components/zwave_js/humidifier.py @@ -11,6 +11,7 @@ from zwave_js_server.const.command_class.humidity_control import ( HumidityControlMode, HumidityControlSetpointType, ) +from zwave_js_server.model.driver import Driver from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.components.humidifier import ( @@ -85,6 +86,8 @@ async def async_setup_entry( @callback def async_add_humidifier(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave Humidifier.""" + driver = client.driver + assert driver is not None # Driver is ready before platforms are loaded. entities: list[ZWaveBaseEntity] = [] if ( @@ -93,7 +96,7 @@ async def async_setup_entry( ): entities.append( ZWaveHumidifier( - config_entry, client, info, HUMIDIFIER_ENTITY_DESCRIPTION + config_entry, driver, info, HUMIDIFIER_ENTITY_DESCRIPTION ) ) @@ -103,7 +106,7 @@ async def async_setup_entry( ): entities.append( ZWaveHumidifier( - config_entry, client, info, DEHUMIDIFIER_ENTITY_DESCRIPTION + config_entry, driver, info, DEHUMIDIFIER_ENTITY_DESCRIPTION ) ) @@ -128,12 +131,12 @@ class ZWaveHumidifier(ZWaveBaseEntity, HumidifierEntity): def __init__( self, config_entry: ConfigEntry, - client: ZwaveClient, + driver: Driver, info: ZwaveDiscoveryInfo, description: ZwaveHumidifierEntityDescription, ) -> None: """Initialize humidifier.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) self.entity_description = description diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index 534a86f1c86..d026868b418 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -23,6 +23,7 @@ from zwave_js_server.const.command_class.color_switch import ( TARGET_COLOR_PROPERTY, ColorComponent, ) +from zwave_js_server.model.driver import Driver from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -72,11 +73,13 @@ async def async_setup_entry( @callback def async_add_light(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave Light.""" + driver = client.driver + assert driver is not None # Driver is ready before platforms are loaded. if info.platform_hint == "black_is_off": - async_add_entities([ZwaveBlackIsOffLight(config_entry, client, info)]) + async_add_entities([ZwaveBlackIsOffLight(config_entry, driver, info)]) else: - async_add_entities([ZwaveLight(config_entry, client, info)]) + async_add_entities([ZwaveLight(config_entry, driver, info)]) config_entry.async_on_unload( async_dispatcher_connect( @@ -101,10 +104,10 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): """Representation of a Z-Wave light.""" def __init__( - self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize the light.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) self._supports_color = False self._supports_rgbw = False self._supports_color_temp = False @@ -445,10 +448,10 @@ class ZwaveBlackIsOffLight(ZwaveLight): """ def __init__( - self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize the light.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) self._last_color: dict[str, int] | None = None self._supported_color_modes.discard(ColorMode.BRIGHTNESS) diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py index 3781821e4c7..e7fbbeb3f99 100644 --- a/homeassistant/components/zwave_js/lock.py +++ b/homeassistant/components/zwave_js/lock.py @@ -61,8 +61,10 @@ async def async_setup_entry( @callback def async_add_lock(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave Lock.""" + driver = client.driver + assert driver is not None # Driver is ready before platforms are loaded. entities: list[ZWaveBaseEntity] = [] - entities.append(ZWaveLock(config_entry, client, info)) + entities.append(ZWaveLock(config_entry, driver, info)) async_add_entities(entities) diff --git a/homeassistant/components/zwave_js/migrate.py b/homeassistant/components/zwave_js/migrate.py index 1413bf8e5b4..400c2b3cffe 100644 --- a/homeassistant/components/zwave_js/migrate.py +++ b/homeassistant/components/zwave_js/migrate.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass import logging -from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.model.driver import Driver from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.const import STATE_UNAVAILABLE @@ -138,12 +138,12 @@ def async_migrate_discovered_value( ent_reg: EntityRegistry, registered_unique_ids: set[str], device: DeviceEntry, - client: ZwaveClient, + driver: Driver, disc_info: ZwaveDiscoveryInfo, ) -> None: """Migrate unique ID for entity/entities tied to discovered value.""" - new_unique_id = get_unique_id(client, disc_info.primary_value.value_id) + new_unique_id = get_unique_id(driver, disc_info.primary_value.value_id) # On reinterviews, there is no point in going through this logic again for already # discovered values @@ -155,7 +155,7 @@ def async_migrate_discovered_value( # 2021.2.*, 2021.3.0b0, and 2021.3.0 formats old_unique_ids = [ - get_unique_id(client, value_id) + get_unique_id(driver, value_id) for value_id in get_old_value_ids(disc_info.primary_value) ] diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py index fb9266ac071..56a7ce33be6 100644 --- a/homeassistant/components/zwave_js/number.py +++ b/homeassistant/components/zwave_js/number.py @@ -3,6 +3,7 @@ from __future__ import annotations from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import TARGET_VALUE_PROPERTY +from zwave_js_server.model.driver import Driver from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberEntity from homeassistant.config_entries import ConfigEntry @@ -28,11 +29,13 @@ async def async_setup_entry( @callback def async_add_number(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave number entity.""" + driver = client.driver + assert driver is not None # Driver is ready before platforms are loaded. entities: list[ZWaveBaseEntity] = [] if info.platform_hint == "volume": - entities.append(ZwaveVolumeNumberEntity(config_entry, client, info)) + entities.append(ZwaveVolumeNumberEntity(config_entry, driver, info)) else: - entities.append(ZwaveNumberEntity(config_entry, client, info)) + entities.append(ZwaveNumberEntity(config_entry, driver, info)) async_add_entities(entities) config_entry.async_on_unload( @@ -48,10 +51,10 @@ class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity): """Representation of a Z-Wave number entity.""" def __init__( - self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a ZwaveNumberEntity entity.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) if self.info.primary_value.metadata.writeable: self._target_value = self.info.primary_value else: @@ -99,10 +102,10 @@ class ZwaveVolumeNumberEntity(ZWaveBaseEntity, NumberEntity): """Representation of a volume number entity.""" def __init__( - self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a ZwaveVolumeNumberEntity entity.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) self.correction_factor = int( self.info.primary_value.metadata.max - self.info.primary_value.metadata.min ) diff --git a/homeassistant/components/zwave_js/select.py b/homeassistant/components/zwave_js/select.py index 085c694fc0e..f8cb294919e 100644 --- a/homeassistant/components/zwave_js/select.py +++ b/homeassistant/components/zwave_js/select.py @@ -6,6 +6,7 @@ from typing import cast from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import TARGET_VALUE_PROPERTY, CommandClass from zwave_js_server.const.command_class.sound_switch import ToneID +from zwave_js_server.model.driver import Driver from homeassistant.components.select import DOMAIN as SELECT_DOMAIN, SelectEntity from homeassistant.config_entries import ConfigEntry @@ -32,15 +33,17 @@ async def async_setup_entry( @callback def async_add_select(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave select entity.""" + driver = client.driver + assert driver is not None # Driver is ready before platforms are loaded. entities: list[ZWaveBaseEntity] = [] if info.platform_hint == "Default tone": - entities.append(ZwaveDefaultToneSelectEntity(config_entry, client, info)) + entities.append(ZwaveDefaultToneSelectEntity(config_entry, driver, info)) elif info.platform_hint == "multilevel_switch": entities.append( - ZwaveMultilevelSwitchSelectEntity(config_entry, client, info) + ZwaveMultilevelSwitchSelectEntity(config_entry, driver, info) ) else: - entities.append(ZwaveSelectEntity(config_entry, client, info)) + entities.append(ZwaveSelectEntity(config_entry, driver, info)) async_add_entities(entities) config_entry.async_on_unload( @@ -58,10 +61,10 @@ class ZwaveSelectEntity(ZWaveBaseEntity, SelectEntity): _attr_entity_category = EntityCategory.CONFIG def __init__( - self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a ZwaveSelectEntity entity.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) # Entity class attributes self._attr_name = self.generate_name(include_value_name=True) @@ -94,10 +97,10 @@ class ZwaveDefaultToneSelectEntity(ZWaveBaseEntity, SelectEntity): _attr_entity_category = EntityCategory.CONFIG def __init__( - self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a ZwaveDefaultToneSelectEntity entity.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) self._tones_value = self.get_zwave_value( "toneId", command_class=CommandClass.SOUND_SWITCH ) @@ -145,10 +148,10 @@ class ZwaveMultilevelSwitchSelectEntity(ZWaveBaseEntity, SelectEntity): """Representation of a Z-Wave Multilevel Switch CC select entity.""" def __init__( - self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a ZwaveSelectEntity entity.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) self._target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY) assert self.info.platform_data_template self._lookup_map = cast( diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 71e6bc48952..eb5bf778cc0 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -12,6 +12,7 @@ from zwave_js_server.const.command_class.meter import ( RESET_METER_OPTION_TARGET_VALUE, RESET_METER_OPTION_TYPE, ) +from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import ConfigurationValue from zwave_js_server.util.command_class.meter import get_meter_type @@ -179,6 +180,8 @@ async def async_setup_entry( @callback def async_add_sensor(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave Sensor.""" + driver = client.driver + assert driver is not None # Driver is ready before platforms are loaded. entities: list[ZWaveBaseEntity] = [] if info.platform_data: @@ -191,13 +194,13 @@ async def async_setup_entry( if info.platform_hint == "string_sensor": entities.append( - ZWaveStringSensor(config_entry, client, info, entity_description) + ZWaveStringSensor(config_entry, driver, info, entity_description) ) elif info.platform_hint == "numeric_sensor": entities.append( ZWaveNumericSensor( config_entry, - client, + driver, info, entity_description, data.unit_of_measurement, @@ -205,17 +208,17 @@ async def async_setup_entry( ) elif info.platform_hint == "list_sensor": entities.append( - ZWaveListSensor(config_entry, client, info, entity_description) + ZWaveListSensor(config_entry, driver, info, entity_description) ) elif info.platform_hint == "config_parameter": entities.append( ZWaveConfigParameterSensor( - config_entry, client, info, entity_description + config_entry, driver, info, entity_description ) ) elif info.platform_hint == "meter": entities.append( - ZWaveMeterSensor(config_entry, client, info, entity_description) + ZWaveMeterSensor(config_entry, driver, info, entity_description) ) else: LOGGER.warning( @@ -230,7 +233,9 @@ async def async_setup_entry( @callback def async_add_node_status_sensor(node: ZwaveNode) -> None: """Add node status sensor.""" - async_add_entities([ZWaveNodeStatusSensor(config_entry, client, node)]) + driver = client.driver + assert driver is not None # Driver is ready before platforms are loaded. + async_add_entities([ZWaveNodeStatusSensor(config_entry, driver, node)]) config_entry.async_on_unload( async_dispatcher_connect( @@ -265,13 +270,13 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): def __init__( self, config_entry: ConfigEntry, - client: ZwaveClient, + driver: Driver, info: ZwaveDiscoveryInfo, entity_description: SensorEntityDescription, unit_of_measurement: str | None = None, ) -> None: """Initialize a ZWaveSensorBase entity.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) self.entity_description = entity_description self._attr_native_unit_of_measurement = unit_of_measurement @@ -370,14 +375,14 @@ class ZWaveListSensor(ZwaveSensorBase): def __init__( self, config_entry: ConfigEntry, - client: ZwaveClient, + driver: Driver, info: ZwaveDiscoveryInfo, entity_description: SensorEntityDescription, unit_of_measurement: str | None = None, ) -> None: """Initialize a ZWaveListSensor entity.""" super().__init__( - config_entry, client, info, entity_description, unit_of_measurement + config_entry, driver, info, entity_description, unit_of_measurement ) # Entity class attributes @@ -414,14 +419,14 @@ class ZWaveConfigParameterSensor(ZwaveSensorBase): def __init__( self, config_entry: ConfigEntry, - client: ZwaveClient, + driver: Driver, info: ZwaveDiscoveryInfo, entity_description: SensorEntityDescription, unit_of_measurement: str | None = None, ) -> None: """Initialize a ZWaveConfigParameterSensor entity.""" super().__init__( - config_entry, client, info, entity_description, unit_of_measurement + config_entry, driver, info, entity_description, unit_of_measurement ) self._primary_value = cast(ConfigurationValue, self.info.primary_value) @@ -466,11 +471,10 @@ class ZWaveNodeStatusSensor(SensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__( - self, config_entry: ConfigEntry, client: ZwaveClient, node: ZwaveNode + self, config_entry: ConfigEntry, driver: Driver, node: ZwaveNode ) -> None: """Initialize a generic Z-Wave device entity.""" self.config_entry = config_entry - self.client = client self.node = node name: str = ( self.node.name @@ -479,11 +483,11 @@ class ZWaveNodeStatusSensor(SensorEntity): ) # Entity class attributes self._attr_name = f"{name}: Node Status" - self._base_unique_id = get_valueless_base_unique_id(client, node) + self._base_unique_id = get_valueless_base_unique_id(driver, node) self._attr_unique_id = f"{self._base_unique_id}.node_status" # device is precreated in main handler self._attr_device_info = DeviceInfo( - identifiers={get_device_id(self.client, self.node)}, + identifiers={get_device_id(driver, self.node)}, ) self._attr_native_value: str = node.status.name.lower() diff --git a/homeassistant/components/zwave_js/siren.py b/homeassistant/components/zwave_js/siren.py index e686c5446ca..67e6aa4afb4 100644 --- a/homeassistant/components/zwave_js/siren.py +++ b/homeassistant/components/zwave_js/siren.py @@ -5,6 +5,7 @@ from typing import Any from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const.command_class.sound_switch import ToneID +from zwave_js_server.model.driver import Driver from homeassistant.components.siren import ( DOMAIN as SIREN_DOMAIN, @@ -35,8 +36,10 @@ async def async_setup_entry( @callback def async_add_siren(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave siren entity.""" + driver = client.driver + assert driver is not None # Driver is ready before platforms are loaded. entities: list[ZWaveBaseEntity] = [] - entities.append(ZwaveSirenEntity(config_entry, client, info)) + entities.append(ZwaveSirenEntity(config_entry, driver, info)) async_add_entities(entities) config_entry.async_on_unload( @@ -52,10 +55,10 @@ class ZwaveSirenEntity(ZWaveBaseEntity, SirenEntity): """Representation of a Z-Wave siren entity.""" def __init__( - self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize a ZwaveSirenEntity entity.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) # Entity class attributes self._attr_available_tones = { int(id): val for id, val in self.info.primary_value.metadata.states.items() diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py index 115f90b8e11..52b8f813326 100644 --- a/homeassistant/components/zwave_js/switch.py +++ b/homeassistant/components/zwave_js/switch.py @@ -9,6 +9,7 @@ from zwave_js_server.const import TARGET_VALUE_PROPERTY from zwave_js_server.const.command_class.barrier_operator import ( BarrierEventSignalingSubsystemState, ) +from zwave_js_server.model.driver import Driver from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -36,13 +37,15 @@ async def async_setup_entry( @callback def async_add_switch(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave Switch.""" + driver = client.driver + assert driver is not None # Driver is ready before platforms are loaded. entities: list[ZWaveBaseEntity] = [] if info.platform_hint == "barrier_event_signaling_state": entities.append( - ZWaveBarrierEventSignalingSwitch(config_entry, client, info) + ZWaveBarrierEventSignalingSwitch(config_entry, driver, info) ) else: - entities.append(ZWaveSwitch(config_entry, client, info)) + entities.append(ZWaveSwitch(config_entry, driver, info)) async_add_entities(entities) @@ -59,10 +62,10 @@ class ZWaveSwitch(ZWaveBaseEntity, SwitchEntity): """Representation of a Z-Wave switch.""" def __init__( - self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo ) -> None: """Initialize the switch.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) self._target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY) @@ -91,11 +94,11 @@ class ZWaveBarrierEventSignalingSwitch(ZWaveBaseEntity, SwitchEntity): def __init__( self, config_entry: ConfigEntry, - client: ZwaveClient, + driver: Driver, info: ZwaveDiscoveryInfo, ) -> None: """Initialize a ZWaveBarrierEventSignalingSwitch entity.""" - super().__init__(config_entry, client, info) + super().__init__(config_entry, driver, info) self._state: bool | None = None self._update_state() diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py index fd46c89832b..83fd7570ab9 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -207,7 +207,9 @@ async def async_attach_trigger( unsubs.append(source.on(event_name, async_on_event)) for node in nodes: - device_identifier = get_device_id(node.client, node) + driver = node.client.driver + assert driver is not None # The node comes from the driver. + device_identifier = get_device_id(driver, node) device = dev_reg.async_get_device({device_identifier}) assert device # We need to store the device for the callback diff --git a/homeassistant/components/zwave_js/triggers/value_updated.py b/homeassistant/components/zwave_js/triggers/value_updated.py index 8a0b287c26b..38a19eaa377 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -162,7 +162,9 @@ async def async_attach_trigger( dev_reg = dr.async_get(hass) for node in nodes: - device_identifier = get_device_id(node.client, node) + driver = node.client.driver + assert driver is not None # The node comes from the driver. + device_identifier = get_device_id(driver, node) device = dev_reg.async_get_device({device_identifier}) assert device value_id = get_value_id(node, command_class, property_, endpoint, property_key) diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 3d491b98f93..58c26cd5797 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -78,7 +78,7 @@ from homeassistant.helpers import device_registry as dr def get_device(hass, node): """Get device ID for a node.""" dev_reg = dr.async_get(hass) - device_id = get_device_id(node.client, node) + device_id = get_device_id(node.client.driver, node) return dev_reg.async_get_device({device_id}) @@ -124,11 +124,12 @@ async def test_node_ready( node_data = deepcopy(multisensor_6_state) # Copy to allow modification in tests. node = Node(client, node_data) node.data["ready"] = False - client.driver.controller.nodes[node.node_id] = node + driver = client.driver + driver.controller.nodes[node.node_id] = node dev_reg = dr.async_get(hass) device = dev_reg.async_get_or_create( - config_entry_id=entry.entry_id, identifiers={get_device_id(client, node)} + config_entry_id=entry.entry_id, identifiers={get_device_id(driver, node)} ) await ws_client.send_json( diff --git a/tests/components/zwave_js/test_button.py b/tests/components/zwave_js/test_button.py index 29858e0eb97..5ae5f8e7254 100644 --- a/tests/components/zwave_js/test_button.py +++ b/tests/components/zwave_js/test_button.py @@ -49,11 +49,12 @@ async def test_ping_entity( assert "There is no value to refresh for this entity" in caplog.text # Assert a node ping button entity is not created for the controller - node = client.driver.controller.nodes[1] + driver = client.driver + node = driver.controller.nodes[1] assert node.is_controller_node assert ( async_get(hass).async_get_entity_id( - DOMAIN, "sensor", f"{get_valueless_base_unique_id(client, node)}.ping" + DOMAIN, "sensor", f"{get_valueless_base_unique_id(driver, node)}.ping" ) is None ) diff --git a/tests/components/zwave_js/test_device_action.py b/tests/components/zwave_js/test_device_action.py index b8fa43d9cec..ad9d61b3f33 100644 --- a/tests/components/zwave_js/test_device_action.py +++ b/tests/components/zwave_js/test_device_action.py @@ -30,7 +30,9 @@ async def test_get_actions( """Test we get the expected actions from a zwave_js node.""" node = lock_schlage_be469 dev_reg = device_registry.async_get(hass) - device = dev_reg.async_get_device({get_device_id(client, node)}) + driver = client.driver + assert driver + device = dev_reg.async_get_device({get_device_id(driver, node)}) assert device expected_actions = [ { @@ -99,7 +101,9 @@ async def test_get_actions_meter( """Test we get the expected meter actions from a zwave_js node.""" node = aeon_smart_switch_6 dev_reg = device_registry.async_get(hass) - device = dev_reg.async_get_device({get_device_id(client, node)}) + driver = client.driver + assert driver + device = dev_reg.async_get_device({get_device_id(driver, node)}) assert device actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device.id @@ -116,7 +120,9 @@ async def test_actions( ) -> None: """Test actions.""" node = climate_radio_thermostat_ct100_plus - device_id = get_device_id(client, node) + driver = client.driver + assert driver + device_id = get_device_id(driver, node) dev_reg = device_registry.async_get(hass) device = dev_reg.async_get_device({device_id}) assert device @@ -236,7 +242,9 @@ async def test_actions_multiple_calls( ) -> None: """Test actions can be called multiple times and still work.""" node = climate_radio_thermostat_ct100_plus - device_id = get_device_id(client, node) + driver = client.driver + assert driver + device_id = get_device_id(driver, node) dev_reg = device_registry.async_get(hass) device = dev_reg.async_get_device({device_id}) assert device @@ -281,7 +289,9 @@ async def test_lock_actions( ) -> None: """Test actions for locks.""" node = lock_schlage_be469 - device_id = get_device_id(client, node) + driver = client.driver + assert driver + device_id = get_device_id(driver, node) dev_reg = device_registry.async_get(hass) device = dev_reg.async_get_device({device_id}) assert device @@ -350,7 +360,9 @@ async def test_reset_meter_action( ) -> None: """Test reset_meter action.""" node = aeon_smart_switch_6 - device_id = get_device_id(client, node) + driver = client.driver + assert driver + device_id = get_device_id(driver, node) dev_reg = device_registry.async_get(hass) device = dev_reg.async_get_device({device_id}) assert device @@ -613,7 +625,9 @@ async def test_get_action_capabilities_meter_triggers( """Test we get the expected action capabilities for meter triggers.""" node = aeon_smart_switch_6 dev_reg = device_registry.async_get(hass) - device = dev_reg.async_get_device({get_device_id(client, node)}) + driver = client.driver + assert driver + device = dev_reg.async_get_device({get_device_id(driver, node)}) assert device capabilities = await device_action.async_get_action_capabilities( hass, @@ -669,7 +683,9 @@ async def test_unavailable_entity_actions( await hass.async_block_till_done() node = lock_schlage_be469 dev_reg = device_registry.async_get(hass) - device = dev_reg.async_get_device({get_device_id(client, node)}) + driver = client.driver + assert driver + device = dev_reg.async_get_device({get_device_id(driver, node)}) assert device actions = await async_get_device_automations( hass, DeviceAutomationType.ACTION, device.id diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index 99475cd63ec..8d00c9a2f64 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -51,7 +51,7 @@ async def test_device_diagnostics( ): """Test the device level diagnostics data dump.""" dev_reg = async_get(hass) - device = dev_reg.async_get_device({get_device_id(client, multisensor_6)}) + device = dev_reg.async_get_device({get_device_id(client.driver, multisensor_6)}) assert device # Update a value and ensure it is reflected in the node state diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index 7b3fd773839..a2962261ac3 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -800,8 +800,10 @@ async def test_removed_device( hass, client, climate_radio_thermostat_ct100_plus, lock_schlage_be469, integration ): """Test that the device registry gets updated when a device gets removed.""" + driver = client.driver + assert driver # Verify how many nodes are available - assert len(client.driver.controller.nodes) == 2 + assert len(driver.controller.nodes) == 2 # Make sure there are the same number of devices dev_reg = dr.async_get(hass) @@ -814,7 +816,7 @@ async def test_removed_device( assert len(entity_entries) == 29 # Remove a node and reload the entry - old_node = client.driver.controller.nodes.pop(13) + old_node = driver.controller.nodes.pop(13) await hass.config_entries.async_reload(integration.entry_id) await hass.async_block_till_done() @@ -824,7 +826,7 @@ async def test_removed_device( assert len(device_entries) == 1 entity_entries = er.async_entries_for_config_entry(ent_reg, integration.entry_id) assert len(entity_entries) == 17 - assert dev_reg.async_get_device({get_device_id(client, old_node)}) is None + assert dev_reg.async_get_device({get_device_id(driver, old_node)}) is None async def test_suggested_area(hass, client, eaton_rf9640_dimmer): diff --git a/tests/components/zwave_js/test_migrate.py b/tests/components/zwave_js/test_migrate.py index 37c53700d95..cf1e3ee7eaa 100644 --- a/tests/components/zwave_js/test_migrate.py +++ b/tests/components/zwave_js/test_migrate.py @@ -190,12 +190,14 @@ async def test_old_entity_migration( ): """Test old entity on a different endpoint is migrated to a new one.""" node = Node(client, copy.deepcopy(hank_binary_switch_state)) + driver = client.driver + assert driver ent_reg = er.async_get(hass) dev_reg = dr.async_get(hass) device = dev_reg.async_get_or_create( config_entry_id=integration.entry_id, - identifiers={get_device_id(client, node)}, + identifiers={get_device_id(driver, node)}, manufacturer=hank_binary_switch_state["deviceConfig"]["manufacturer"], model=hank_binary_switch_state["deviceConfig"]["label"], ) @@ -204,7 +206,7 @@ async def test_old_entity_migration( entity_name = SENSOR_NAME.split(".")[1] # Create entity RegistryEntry using fake endpoint - old_unique_id = f"{client.driver.controller.home_id}.32-50-1-value-66049" + old_unique_id = f"{driver.controller.home_id}.32-50-1-value-66049" entity_entry = ent_reg.async_get_or_create( "sensor", DOMAIN, @@ -221,7 +223,7 @@ async def test_old_entity_migration( for i in range(0, 2): # Add a ready node, unique ID should be migrated event = {"node": node} - client.driver.controller.emit("node added", event) + driver.controller.emit("node added", event) await hass.async_block_till_done() # Check that new RegistryEntry is using new unique ID format @@ -236,12 +238,14 @@ async def test_different_endpoint_migration_status_sensor( ): """Test that the different endpoint migration logic skips over the status sensor.""" node = Node(client, copy.deepcopy(hank_binary_switch_state)) + driver = client.driver + assert driver ent_reg = er.async_get(hass) dev_reg = dr.async_get(hass) device = dev_reg.async_get_or_create( config_entry_id=integration.entry_id, - identifiers={get_device_id(client, node)}, + identifiers={get_device_id(driver, node)}, manufacturer=hank_binary_switch_state["deviceConfig"]["manufacturer"], model=hank_binary_switch_state["deviceConfig"]["label"], ) @@ -250,7 +254,7 @@ async def test_different_endpoint_migration_status_sensor( entity_name = SENSOR_NAME.split(".")[1] # Create entity RegistryEntry using fake endpoint - old_unique_id = f"{client.driver.controller.home_id}.32.node_status" + old_unique_id = f"{driver.controller.home_id}.32.node_status" entity_entry = ent_reg.async_get_or_create( "sensor", DOMAIN, @@ -267,7 +271,7 @@ async def test_different_endpoint_migration_status_sensor( for i in range(0, 2): # Add a ready node, unique ID should be migrated event = {"node": node} - client.driver.controller.emit("node added", event) + driver.controller.emit("node added", event) await hass.async_block_till_done() # Check that the RegistryEntry is using the same unique ID @@ -280,12 +284,14 @@ async def test_skip_old_entity_migration_for_multiple( ): """Test that multiple entities of the same value but on a different endpoint get skipped.""" node = Node(client, copy.deepcopy(hank_binary_switch_state)) + driver = client.driver + assert driver ent_reg = er.async_get(hass) dev_reg = dr.async_get(hass) device = dev_reg.async_get_or_create( config_entry_id=integration.entry_id, - identifiers={get_device_id(client, node)}, + identifiers={get_device_id(driver, node)}, manufacturer=hank_binary_switch_state["deviceConfig"]["manufacturer"], model=hank_binary_switch_state["deviceConfig"]["label"], ) @@ -294,7 +300,7 @@ async def test_skip_old_entity_migration_for_multiple( entity_name = SENSOR_NAME.split(".")[1] # Create two entity entrrys using different endpoints - old_unique_id_1 = f"{client.driver.controller.home_id}.32-50-1-value-66049" + old_unique_id_1 = f"{driver.controller.home_id}.32-50-1-value-66049" entity_entry = ent_reg.async_get_or_create( "sensor", DOMAIN, @@ -308,7 +314,7 @@ async def test_skip_old_entity_migration_for_multiple( assert entity_entry.unique_id == old_unique_id_1 # Create two entity entrrys using different endpoints - old_unique_id_2 = f"{client.driver.controller.home_id}.32-50-2-value-66049" + old_unique_id_2 = f"{driver.controller.home_id}.32-50-2-value-66049" entity_entry = ent_reg.async_get_or_create( "sensor", DOMAIN, @@ -322,12 +328,12 @@ async def test_skip_old_entity_migration_for_multiple( assert entity_entry.unique_id == old_unique_id_2 # Add a ready node, unique ID should be migrated event = {"node": node} - client.driver.controller.emit("node added", event) + driver.controller.emit("node added", event) await hass.async_block_till_done() # Check that new RegistryEntry is created using new unique ID format entity_entry = ent_reg.async_get(SENSOR_NAME) - new_unique_id = f"{client.driver.controller.home_id}.32-50-0-value-66049" + new_unique_id = f"{driver.controller.home_id}.32-50-0-value-66049" assert entity_entry.unique_id == new_unique_id # Check that the old entities stuck around because we skipped the migration step @@ -340,12 +346,14 @@ async def test_old_entity_migration_notification_binary_sensor( ): """Test old entity on a different endpoint is migrated to a new one for a notification binary sensor.""" node = Node(client, copy.deepcopy(multisensor_6_state)) + driver = client.driver + assert driver ent_reg = er.async_get(hass) dev_reg = dr.async_get(hass) device = dev_reg.async_get_or_create( config_entry_id=integration.entry_id, - identifiers={get_device_id(client, node)}, + identifiers={get_device_id(driver, node)}, manufacturer=multisensor_6_state["deviceConfig"]["manufacturer"], model=multisensor_6_state["deviceConfig"]["label"], ) @@ -353,7 +361,9 @@ async def test_old_entity_migration_notification_binary_sensor( entity_name = NOTIFICATION_MOTION_BINARY_SENSOR.split(".")[1] # Create entity RegistryEntry using old unique ID format - old_unique_id = f"{client.driver.controller.home_id}.52-113-1-Home Security-Motion sensor status.8" + old_unique_id = ( + f"{driver.controller.home_id}.52-113-1-Home Security-Motion sensor status.8" + ) entity_entry = ent_reg.async_get_or_create( "binary_sensor", DOMAIN, @@ -370,12 +380,14 @@ async def test_old_entity_migration_notification_binary_sensor( for _ in range(0, 2): # Add a ready node, unique ID should be migrated event = {"node": node} - client.driver.controller.emit("node added", event) + driver.controller.emit("node added", event) await hass.async_block_till_done() # Check that new RegistryEntry is using new unique ID format entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_BINARY_SENSOR) - new_unique_id = f"{client.driver.controller.home_id}.52-113-0-Home Security-Motion sensor status.8" + new_unique_id = ( + f"{driver.controller.home_id}.52-113-0-Home Security-Motion sensor status.8" + ) assert entity_entry.unique_id == new_unique_id assert ( ent_reg.async_get_entity_id("binary_sensor", DOMAIN, old_unique_id) is None diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py index 1d41e145a95..848c4d7b0e5 100644 --- a/tests/components/zwave_js/test_sensor.py +++ b/tests/components/zwave_js/test_sensor.py @@ -205,13 +205,14 @@ async def test_node_status_sensor( assert hass.states.get(NODE_STATUS_ENTITY).state != STATE_UNAVAILABLE # Assert a node status sensor entity is not created for the controller - node = client.driver.controller.nodes[1] + driver = client.driver + node = driver.controller.nodes[1] assert node.is_controller_node assert ( ent_reg.async_get_entity_id( DOMAIN, "sensor", - f"{get_valueless_base_unique_id(client, node)}.node_status", + f"{get_valueless_base_unique_id(driver, node)}.node_status", ) is None ) diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index e04ec569c9f..dec4c3c0a9d 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -1367,11 +1367,11 @@ async def test_multicast_set_value( # Test using area ID dev_reg = async_get_dev_reg(hass) device_eurotronic = dev_reg.async_get_device( - {get_device_id(client, climate_eurotronic_spirit_z)} + {get_device_id(client.driver, climate_eurotronic_spirit_z)} ) assert device_eurotronic device_danfoss = dev_reg.async_get_device( - {get_device_id(client, climate_danfoss_lc_13)} + {get_device_id(client.driver, climate_danfoss_lc_13)} ) assert device_danfoss area_reg = async_get_area_reg(hass) @@ -1655,11 +1655,15 @@ async def test_ping( """Test ping service.""" dev_reg = async_get_dev_reg(hass) device_radio_thermostat = dev_reg.async_get_device( - {get_device_id(client, climate_radio_thermostat_ct100_plus_different_endpoints)} + { + get_device_id( + client.driver, climate_radio_thermostat_ct100_plus_different_endpoints + ) + } ) assert device_radio_thermostat device_danfoss = dev_reg.async_get_device( - {get_device_id(client, climate_danfoss_lc_13)} + {get_device_id(client.driver, climate_danfoss_lc_13)} ) assert device_danfoss @@ -1789,11 +1793,15 @@ async def test_invoke_cc_api( """Test invoke_cc_api service.""" dev_reg = async_get_dev_reg(hass) device_radio_thermostat = dev_reg.async_get_device( - {get_device_id(client, climate_radio_thermostat_ct100_plus_different_endpoints)} + { + get_device_id( + client.driver, climate_radio_thermostat_ct100_plus_different_endpoints + ) + } ) assert device_radio_thermostat device_danfoss = dev_reg.async_get_device( - {get_device_id(client, climate_danfoss_lc_13)} + {get_device_id(client.driver, climate_danfoss_lc_13)} ) assert device_danfoss From 777c9c08ffeedb2c3f1ac44807336bb11b131ac2 Mon Sep 17 00:00:00 2001 From: Jon Benson Date: Wed, 25 May 2022 08:48:54 +1000 Subject: [PATCH 0857/3516] Update Rainforest Eagle to use eagle100 instead of uEagle (#70177) --- CODEOWNERS | 4 ++-- homeassistant/components/rainforest_eagle/data.py | 2 +- homeassistant/components/rainforest_eagle/manifest.json | 6 +++--- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 887534e3461..9cc32503853 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -821,8 +821,8 @@ build.json @home-assistant/supervisor /homeassistant/components/radiotherm/ @vinnyfuria /homeassistant/components/rainbird/ @konikvranik /homeassistant/components/raincloud/ @vanstinator -/homeassistant/components/rainforest_eagle/ @gtdiehl @jcalbert -/tests/components/rainforest_eagle/ @gtdiehl @jcalbert +/homeassistant/components/rainforest_eagle/ @gtdiehl @jcalbert @hastarin +/tests/components/rainforest_eagle/ @gtdiehl @jcalbert @hastarin /homeassistant/components/rainmachine/ @bachya /tests/components/rainmachine/ @bachya /homeassistant/components/random/ @fabaff diff --git a/homeassistant/components/rainforest_eagle/data.py b/homeassistant/components/rainforest_eagle/data.py index 52f40e81d40..c7ef596bb61 100644 --- a/homeassistant/components/rainforest_eagle/data.py +++ b/homeassistant/components/rainforest_eagle/data.py @@ -7,8 +7,8 @@ import logging import aioeagle import aiohttp import async_timeout +from eagle100 import Eagle as Eagle100Reader from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout -from uEagle import Eagle as Eagle100Reader from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_TYPE diff --git a/homeassistant/components/rainforest_eagle/manifest.json b/homeassistant/components/rainforest_eagle/manifest.json index b4fbc78f241..b875a7f1ff4 100644 --- a/homeassistant/components/rainforest_eagle/manifest.json +++ b/homeassistant/components/rainforest_eagle/manifest.json @@ -2,8 +2,8 @@ "domain": "rainforest_eagle", "name": "Rainforest Eagle", "documentation": "https://www.home-assistant.io/integrations/rainforest_eagle", - "requirements": ["aioeagle==1.1.0", "uEagle==0.0.2"], - "codeowners": ["@gtdiehl", "@jcalbert"], + "requirements": ["aioeagle==1.1.0", "eagle100==0.1.1"], + "codeowners": ["@gtdiehl", "@jcalbert", "@hastarin"], "iot_class": "local_polling", "config_flow": true, "dhcp": [ @@ -11,5 +11,5 @@ "macaddress": "D8D5B9*" } ], - "loggers": ["aioeagle", "uEagle"] + "loggers": ["aioeagle", "eagle100"] } diff --git a/requirements_all.txt b/requirements_all.txt index 448748afb71..222067c8028 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -571,6 +571,9 @@ dweepy==0.3.0 # homeassistant.components.dynalite dynalite_devices==0.1.46 +# homeassistant.components.rainforest_eagle +eagle100==0.1.1 + # homeassistant.components.ebusd ebusdpy==0.0.17 @@ -2348,9 +2351,6 @@ twilio==6.32.0 # homeassistant.components.twitch twitchAPI==2.5.2 -# homeassistant.components.rainforest_eagle -uEagle==0.0.2 - # homeassistant.components.ukraine_alarm uasiren==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33b778e80d0..423122e079f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,6 +420,9 @@ dsmr_parser==0.33 # homeassistant.components.dynalite dynalite_devices==0.1.46 +# homeassistant.components.rainforest_eagle +eagle100==0.1.1 + # homeassistant.components.elgato elgato==3.0.0 @@ -1536,9 +1539,6 @@ twilio==6.32.0 # homeassistant.components.twitch twitchAPI==2.5.2 -# homeassistant.components.rainforest_eagle -uEagle==0.0.2 - # homeassistant.components.ukraine_alarm uasiren==0.0.1 From 6cac1dadeba6cb81285960db1ab6ec6239547cd9 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 25 May 2022 01:23:34 +0200 Subject: [PATCH 0858/3516] Clean zwave_js platform typing (#72439) * Fix binary sensor * Fix climate * Fix cover * Fix fan * Fix light * Fix lock * Fix number * Fix select * Fix sensor * Add back type ignore until library bump --- .../components/zwave_js/binary_sensor.py | 12 ++++++---- homeassistant/components/zwave_js/climate.py | 18 ++++++++------ homeassistant/components/zwave_js/cover.py | 17 ++++++++++--- homeassistant/components/zwave_js/fan.py | 24 ++++++++++++++----- homeassistant/components/zwave_js/light.py | 15 ++++++++---- homeassistant/components/zwave_js/lock.py | 7 +++--- homeassistant/components/zwave_js/number.py | 15 ++++++++---- homeassistant/components/zwave_js/select.py | 5 +++- homeassistant/components/zwave_js/sensor.py | 24 +++++++++++++------ 9 files changed, 97 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index 7e9882377bd..fb085cffe62 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -248,7 +248,7 @@ PROPERTY_SENSOR_MAPPINGS: dict[str, PropertyZWaveJSEntityDescription] = { # Mappings for boolean sensors -BOOLEAN_SENSOR_MAPPINGS: dict[str, BinarySensorEntityDescription] = { +BOOLEAN_SENSOR_MAPPINGS: dict[int, BinarySensorEntityDescription] = { CommandClass.BATTERY: BinarySensorEntityDescription( key=str(CommandClass.BATTERY), device_class=BinarySensorDeviceClass.BATTERY, @@ -304,9 +304,13 @@ async def async_setup_entry( config_entry, driver, info, state_key, notification_description ) ) - elif info.platform_hint == "property" and ( - property_description := PROPERTY_SENSOR_MAPPINGS.get( - info.primary_value.property_name + elif ( + info.platform_hint == "property" + and info.primary_value.property_name + and ( + property_description := PROPERTY_SENSOR_MAPPINGS.get( + info.primary_value.property_name + ) ) ): entities.append( diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index f721db41d9f..cad0b6a1e5c 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -138,7 +138,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): self._current_mode = self.get_zwave_value( THERMOSTAT_MODE_PROPERTY, command_class=CommandClass.THERMOSTAT_MODE ) - self._setpoint_values: dict[ThermostatSetpointType, ZwaveValue] = {} + self._setpoint_values: dict[ThermostatSetpointType, ZwaveValue | None] = {} for enum in ThermostatSetpointType: self._setpoint_values[enum] = self.get_zwave_value( THERMOSTAT_SETPOINT_PROPERTY, @@ -233,9 +233,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): self._hvac_presets = all_presets @property - def _current_mode_setpoint_enums(self) -> list[ThermostatSetpointType | None]: + def _current_mode_setpoint_enums(self) -> list[ThermostatSetpointType]: """Return the list of enums that are relevant to the current thermostat mode.""" - if self._current_mode is None: + if self._current_mode is None or self._current_mode.value is None: # Thermostat(valve) with no support for setting a mode is considered heating-only return [ThermostatSetpointType.HEATING] return THERMOSTAT_MODE_SETPOINT_MAP.get(int(self._current_mode.value), []) # type: ignore[no-any-return] @@ -329,12 +329,13 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): @property def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp.""" - if self._current_mode and self._current_mode.value is None: + if self._current_mode is None or self._current_mode.value is None: # guard missing value return None - if self._current_mode and int(self._current_mode.value) not in THERMOSTAT_MODES: - return_val: str = self._current_mode.metadata.states.get( - str(self._current_mode.value) + if int(self._current_mode.value) not in THERMOSTAT_MODES: + return_val: str = cast( + str, + self._current_mode.metadata.states.get(str(self._current_mode.value)), ) return return_val return PRESET_NONE @@ -468,6 +469,9 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new target preset mode.""" + if self._current_mode is None: + # Thermostat(valve) has no support for setting a mode, so we make it a no-op + return if preset_mode == PRESET_NONE: # try to restore to the (translated) main hvac mode await self.async_set_hvac_mode(self.hvac_mode) diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index f9a9990ed77..ee83db4578c 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -28,6 +28,7 @@ from homeassistant.components.cover import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -140,6 +141,8 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity): async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY) + if target_value is None: + raise HomeAssistantError("Missing target value on device.") await self.info.node.async_set_value( target_value, percent_to_zwave_position(kwargs[ATTR_POSITION]) ) @@ -147,11 +150,15 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY) + if target_value is None: + raise HomeAssistantError("Missing target value on device.") await self.info.node.async_set_value(target_value, 99) async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" target_value = self.get_zwave_value(TARGET_VALUE_PROPERTY) + if target_value is None: + raise HomeAssistantError("Missing target value on device.") await self.info.node.async_set_value(target_value, 0) async def async_stop_cover(self, **kwargs: Any) -> None: @@ -207,7 +214,9 @@ class ZWaveTiltCover(ZWaveCover): None is unknown, 0 is closed, 100 is fully open. """ value = self.data_template.current_tilt_value(self.info.platform_data) - return zwave_tilt_to_percent(value.value) if value else None + if value is None or value.value is None: + return None + return zwave_tilt_to_percent(int(value.value)) async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" @@ -241,8 +250,10 @@ class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity): ) -> None: """Initialize a ZwaveMotorizedBarrier entity.""" super().__init__(config_entry, driver, info) - self._target_state: ZwaveValue = self.get_zwave_value( - TARGET_STATE_PROPERTY, add_to_watched_value_ids=False + # TARGET_STATE_PROPERTY is required in the discovery schema. + self._target_state = cast( + ZwaveValue, + self.get_zwave_value(TARGET_STATE_PROPERTY, add_to_watched_value_ids=False), ) @property diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index eb6d053f958..ae9df47b420 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -96,7 +96,9 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): percentage_to_ranged_value(DEFAULT_SPEED_RANGE, percentage) ) - await self.info.node.async_set_value(self._target_value, zwave_speed) + if (target_value := self._target_value) is None: + raise HomeAssistantError("Missing target value on device.") + await self.info.node.async_set_value(target_value, zwave_speed) async def async_turn_on( self, @@ -110,12 +112,16 @@ class ZwaveFan(ZWaveBaseEntity, FanEntity): elif preset_mode is not None: await self.async_set_preset_mode(preset_mode) else: + if (target_value := self._target_value) is None: + raise HomeAssistantError("Missing target value on device.") # Value 255 tells device to return to previous value - await self.info.node.async_set_value(self._target_value, 255) + await self.info.node.async_set_value(target_value, 255) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - await self.info.node.async_set_value(self._target_value, 0) + if (target_value := self._target_value) is None: + raise HomeAssistantError("Missing target value on device.") + await self.info.node.async_set_value(target_value, 0) @property def is_on(self) -> bool | None: @@ -160,14 +166,18 @@ class ValueMappingZwaveFan(ZwaveFan): async def async_set_percentage(self, percentage: int) -> None: """Set the speed percentage of the fan.""" + if (target_value := self._target_value) is None: + raise HomeAssistantError("Missing target value on device.") zwave_speed = self.percentage_to_zwave_speed(percentage) - await self.info.node.async_set_value(self._target_value, zwave_speed) + await self.info.node.async_set_value(target_value, zwave_speed) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" + if (target_value := self._target_value) is None: + raise HomeAssistantError("Missing target value on device.") for zwave_value, mapped_preset_mode in self.fan_value_mapping.presets.items(): if preset_mode == mapped_preset_mode: - await self.info.node.async_set_value(self._target_value, zwave_value) + await self.info.node.async_set_value(target_value, zwave_value) return raise NotValidPresetModeError( @@ -210,7 +220,9 @@ class ValueMappingZwaveFan(ZwaveFan): @property def preset_mode(self) -> str | None: """Return the current preset mode.""" - return self.fan_value_mapping.presets.get(self.info.primary_value.value) + if (value := self.info.primary_value.value) is None: + return None + return self.fan_value_mapping.presets.get(value) @property def has_fan_value_mapping(self) -> bool: diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index d026868b418..17293e85a21 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, cast from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import ( @@ -24,6 +24,7 @@ from zwave_js_server.const.command_class.color_switch import ( ColorComponent, ) from zwave_js_server.model.driver import Driver +from zwave_js_server.model.value import Value from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -301,10 +302,14 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): """Set (multiple) defined colors to given value(s).""" # prefer the (new) combined color property # https://github.com/zwave-js/node-zwave-js/pull/1782 - combined_color_val = self.get_zwave_value( - "targetColor", - CommandClass.SWITCH_COLOR, - value_property_key=None, + # Setting colors is only done if there's a target color value. + combined_color_val = cast( + Value, + self.get_zwave_value( + "targetColor", + CommandClass.SWITCH_COLOR, + value_property_key=None, + ), ) zwave_transition = None diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py index e7fbbeb3f99..ffe99373991 100644 --- a/homeassistant/components/zwave_js/lock.py +++ b/homeassistant/components/zwave_js/lock.py @@ -14,7 +14,6 @@ from zwave_js_server.const.command_class.lock import ( LOCK_CMD_CLASS_TO_PROPERTY_MAP, DoorLockMode, ) -from zwave_js_server.model.value import Value as ZwaveValue from zwave_js_server.util.lock import clear_usercode, set_usercode from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity @@ -111,8 +110,10 @@ class ZWaveLock(ZWaveBaseEntity, LockEntity): async def _set_lock_state(self, target_state: str, **kwargs: Any) -> None: """Set the lock state.""" - target_value: ZwaveValue = self.get_zwave_value( - LOCK_CMD_CLASS_TO_PROPERTY_MAP[self.info.primary_value.command_class] + target_value = self.get_zwave_value( + LOCK_CMD_CLASS_TO_PROPERTY_MAP[ + CommandClass(self.info.primary_value.command_class) + ] ) if target_value is not None: await self.info.node.async_set_value( diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py index 56a7ce33be6..737b872b7bc 100644 --- a/homeassistant/components/zwave_js/number.py +++ b/homeassistant/components/zwave_js/number.py @@ -1,13 +1,17 @@ """Support for Z-Wave controls using the number platform.""" from __future__ import annotations +from typing import cast + from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import TARGET_VALUE_PROPERTY from zwave_js_server.model.driver import Driver +from zwave_js_server.model.value import Value from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -55,6 +59,7 @@ class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity): ) -> None: """Initialize a ZwaveNumberEntity entity.""" super().__init__(config_entry, driver, info) + self._target_value: Value | None if self.info.primary_value.metadata.writeable: self._target_value = self.info.primary_value else: @@ -95,7 +100,9 @@ class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity): async def async_set_value(self, value: float) -> None: """Set new value.""" - await self.info.node.async_set_value(self._target_value, value) + if (target_value := self._target_value) is None: + raise HomeAssistantError("Missing target value on device.") + await self.info.node.async_set_value(target_value, value) class ZwaveVolumeNumberEntity(ZWaveBaseEntity, NumberEntity): @@ -106,9 +113,9 @@ class ZwaveVolumeNumberEntity(ZWaveBaseEntity, NumberEntity): ) -> None: """Initialize a ZwaveVolumeNumberEntity entity.""" super().__init__(config_entry, driver, info) - self.correction_factor = int( - self.info.primary_value.metadata.max - self.info.primary_value.metadata.min - ) + max_value = cast(int, self.info.primary_value.metadata.max) + min_value = cast(int, self.info.primary_value.metadata.min) + self.correction_factor = max_value - min_value # Fallback in case we can't properly calculate correction factor if self.correction_factor == 0: self.correction_factor = 1 diff --git a/homeassistant/components/zwave_js/select.py b/homeassistant/components/zwave_js/select.py index f8cb294919e..f61149e5de7 100644 --- a/homeassistant/components/zwave_js/select.py +++ b/homeassistant/components/zwave_js/select.py @@ -11,6 +11,7 @@ from zwave_js_server.model.driver import Driver from homeassistant.components.select import DOMAIN as SELECT_DOMAIN, SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -174,5 +175,7 @@ class ZwaveMultilevelSwitchSelectEntity(ZWaveBaseEntity, SelectEntity): async def async_select_option(self, option: str) -> None: """Change the selected option.""" + if (target_value := self._target_value) is None: + raise HomeAssistantError("Missing target value on device.") key = next(key for key, val in self._lookup_map.items() if val == option) - await self.info.node.async_set_value(self._target_value, int(key)) + await self.info.node.async_set_value(target_value, int(key)) diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index eb5bf778cc0..2b2e2a0de2b 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -26,6 +26,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, EntityCategory @@ -224,7 +225,7 @@ async def async_setup_entry( LOGGER.warning( "Sensor not implemented for %s/%s", info.platform_hint, - info.primary_value.propertyname, + info.primary_value.property_name, ) return @@ -352,13 +353,15 @@ class ZWaveMeterSensor(ZWaveNumericSensor): """Reset meter(s) on device.""" node = self.info.node primary_value = self.info.primary_value + if (endpoint := primary_value.endpoint) is None: + raise HomeAssistantError("Missing endpoint on device.") options = {} if meter_type is not None: options[RESET_METER_OPTION_TYPE] = meter_type if value is not None: options[RESET_METER_OPTION_TARGET_VALUE] = value args = [options] if options else [] - await node.endpoints[primary_value.endpoint].async_invoke_cc_api( + await node.endpoints[endpoint].async_invoke_cc_api( CommandClass.METER, "reset", *args, wait_for_result=False ) LOGGER.debug( @@ -385,11 +388,12 @@ class ZWaveListSensor(ZwaveSensorBase): config_entry, driver, info, entity_description, unit_of_measurement ) + property_key_name = self.info.primary_value.property_key_name # Entity class attributes self._attr_name = self.generate_name( include_value_name=True, alternate_value_name=self.info.primary_value.property_name, - additional_info=[self.info.primary_value.property_key_name], + additional_info=[property_key_name] if property_key_name else None, ) @property @@ -409,8 +413,10 @@ class ZWaveListSensor(ZwaveSensorBase): @property def extra_state_attributes(self) -> dict[str, str] | None: """Return the device specific state attributes.""" + if (value := self.info.primary_value.value) is None: + return None # add the value's int value as property for multi-value (list) items - return {ATTR_VALUE: self.info.primary_value.value} + return {ATTR_VALUE: value} class ZWaveConfigParameterSensor(ZwaveSensorBase): @@ -430,11 +436,12 @@ class ZWaveConfigParameterSensor(ZwaveSensorBase): ) self._primary_value = cast(ConfigurationValue, self.info.primary_value) + property_key_name = self.info.primary_value.property_key_name # Entity class attributes self._attr_name = self.generate_name( include_value_name=True, alternate_value_name=self.info.primary_value.property_name, - additional_info=[self.info.primary_value.property_key_name], + additional_info=[property_key_name] if property_key_name else None, name_suffix="Config Parameter", ) @@ -458,10 +465,13 @@ class ZWaveConfigParameterSensor(ZwaveSensorBase): @property def extra_state_attributes(self) -> dict[str, str] | None: """Return the device specific state attributes.""" - if self._primary_value.configuration_value_type == ConfigurationValueType.RANGE: + if ( + self._primary_value.configuration_value_type == ConfigurationValueType.RANGE + or (value := self.info.primary_value.value) is None + ): return None # add the value's int value as property for multi-value (list) items - return {ATTR_VALUE: self.info.primary_value.value} + return {ATTR_VALUE: value} class ZWaveNodeStatusSensor(SensorEntity): From 0c2f22d4780612545c483627da729e44d46ee9fd Mon Sep 17 00:00:00 2001 From: rforro Date: Wed, 25 May 2022 01:43:35 +0200 Subject: [PATCH 0859/3516] Add configurable zha switch entity (#71784) * add configurable zha switch entity * final zha configurable switch * fix codecov * replaced errorneous cluster with local quirk * test fix * minor changes --- homeassistant/components/zha/switch.py | 129 ++++++++++++++++- tests/components/zha/test_switch.py | 186 +++++++++++++++++++++++++ 2 files changed, 314 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 76c41093ed6..800c42eb932 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -2,8 +2,10 @@ from __future__ import annotations import functools -from typing import Any +import logging +from typing import TYPE_CHECKING, Any +import zigpy.exceptions from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.foundation import Status @@ -12,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .core import discovery @@ -24,8 +27,17 @@ from .core.const import ( from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity, ZhaGroupEntity +if TYPE_CHECKING: + from .core.channels.base import ZigbeeChannel + from .core.device import ZHADevice + STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SWITCH) GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.SWITCH) +CONFIG_DIAGNOSTIC_MATCH = functools.partial( + ZHA_ENTITIES.config_diagnostic_match, Platform.SWITCH +) + +_LOGGER = logging.getLogger(__name__) async def async_setup_entry( @@ -138,3 +150,118 @@ class SwitchGroup(ZhaGroupEntity, SwitchEntity): self._state = len(on_states) > 0 self._available = any(state.state != STATE_UNAVAILABLE for state in states) + + +class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): + """Representation of a ZHA switch configuration entity.""" + + _zcl_attribute: str + _zcl_inverter_attribute: str = "" + + @classmethod + def create_entity( + cls, + unique_id: str, + zha_device: ZHADevice, + channels: list[ZigbeeChannel], + **kwargs, + ) -> ZhaEntity | None: + """Entity Factory. + + Return entity if it is a supported configuration, otherwise return None + """ + channel = channels[0] + if ( + cls._zcl_attribute in channel.cluster.unsupported_attributes + or channel.cluster.get(cls._zcl_attribute) is None + ): + _LOGGER.debug( + "%s is not supported - skipping %s entity creation", + cls._zcl_attribute, + cls.__name__, + ) + return None + + return cls(unique_id, zha_device, channels, **kwargs) + + def __init__( + self, + unique_id: str, + zha_device: ZHADevice, + channels: list[ZigbeeChannel], + **kwargs, + ) -> None: + """Init this number configuration entity.""" + self._channel: ZigbeeChannel = channels[0] + super().__init__(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() + self.async_accept_signal( + self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state + ) + + @callback + def async_set_state(self, attr_id: int, attr_name: str, value: Any): + """Handle state update from channel.""" + self.async_write_ha_state() + + @property + def is_on(self) -> bool: + """Return if the switch is on based on the statemachine.""" + val = bool(self._channel.cluster.get(self._zcl_attribute)) + invert = bool(self._channel.cluster.get(self._zcl_inverter_attribute)) + return (not val) if invert else val + + async def async_turn_on_off(self, state) -> None: + """Turn the entity on or off.""" + try: + invert = bool(self._channel.cluster.get(self._zcl_inverter_attribute)) + result = await self._channel.cluster.write_attributes( + {self._zcl_attribute: not state if invert else state} + ) + except zigpy.exceptions.ZigbeeException as ex: + self.error("Could not set value: %s", ex) + return + if not isinstance(result, Exception) and all( + record.status == Status.SUCCESS for record in result[0] + ): + self.async_write_ha_state() + + async def async_turn_on(self, **kwargs) -> None: + """Turn the entity on.""" + await self.async_turn_on_off(True) + + async def async_turn_off(self, **kwargs) -> None: + """Turn the entity off.""" + await self.async_turn_on_off(False) + + async def async_update(self) -> None: + """Attempt to retrieve the state of the entity.""" + await super().async_update() + _LOGGER.error("Polling current state") + if self._channel: + value = await self._channel.get_attribute_value( + self._zcl_attribute, from_cache=False + ) + invert = await self._channel.get_attribute_value( + self._zcl_inverter_attribute, from_cache=False + ) + _LOGGER.debug("read value=%s, inverter=%s", value, bool(invert)) + + +@CONFIG_DIAGNOSTIC_MATCH( + channel_names="tuya_manufacturer", + manufacturers={ + "_TZE200_b6wax7g0", + }, +) +class OnOffWindowDetectionFunctionConfigurationEntity( + ZHASwitchConfigurationEntity, id_suffix="on_off_window_opened_detection" +): + """Representation of a ZHA on off transition time configuration entity.""" + + _attr_entity_category = EntityCategory.CONFIG + _zcl_attribute = "window_detection_function" + _zcl_inverter_attribute = "window_detection_function_inverter" diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index a624e5f2c73..99e8a681348 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -2,13 +2,25 @@ from unittest.mock import call, patch import pytest +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from zigpy.exceptions import ZigbeeException import zigpy.profiles.zha as zha +from zigpy.quirks import CustomCluster, CustomDevice +import zigpy.types as t import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster import zigpy.zcl.foundation as zcl_f from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.zha.core.group import GroupMember from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform +from homeassistant.setup import async_setup_component from .common import ( async_enable_traffic, @@ -174,6 +186,61 @@ async def test_switch(hass, zha_device_joined_restored, zigpy_device): await async_test_rejoin(hass, zigpy_device, [cluster], (1,)) +class WindowDetectionFunctionQuirk(CustomDevice): + """Quirk with window detection function attribute.""" + + class TuyaManufCluster(CustomCluster, ManufacturerSpecificCluster): + """Tuya manufacturer specific cluster.""" + + cluster_id = 0xEF00 + ep_attribute = "tuya_manufacturer" + + attributes = { + 0xEF01: ("window_detection_function", t.Bool), + 0xEF02: ("window_detection_function_inverter", t.Bool), + } + + def __init__(self, *args, **kwargs): + """Initialize with task.""" + super().__init__(*args, **kwargs) + self._attr_cache.update( + {0xEF01: False} + ) # entity won't be created without this + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [general.Basic.cluster_id, TuyaManufCluster], + OUTPUT_CLUSTERS: [], + }, + } + } + + +@pytest.fixture +async def zigpy_device_tuya(hass, zigpy_device_mock, zha_device_joined): + """Device tracker zigpy tuya device.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [general.Basic.cluster_id], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, + } + }, + manufacturer="_TZE200_b6wax7g0", + quirk=WindowDetectionFunctionQuirk, + ) + + zha_device = await zha_device_joined(zigpy_device) + zha_device.available = True + await hass.async_block_till_done() + return zigpy_device + + @patch( "homeassistant.components.zha.entity.UPDATE_GROUP_FROM_CHILD_DELAY", new=0, @@ -292,3 +359,122 @@ async def test_zha_group_switch_entity( # test that group light is now back on assert hass.states.get(entity_id).state == STATE_ON + + +async def test_switch_configurable(hass, zha_device_joined_restored, zigpy_device_tuya): + """Test zha configurable switch platform.""" + + zha_device = await zha_device_joined_restored(zigpy_device_tuya) + cluster = zigpy_device_tuya.endpoints.get(1).tuya_manufacturer + entity_id = await find_entity_id(Platform.SWITCH, zha_device, hass) + assert entity_id is not None + + assert hass.states.get(entity_id).state == STATE_OFF + await async_enable_traffic(hass, [zha_device], enabled=False) + # test that the switch was created and that its state is unavailable + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, [zha_device]) + + # test that the state has changed from unavailable to off + assert hass.states.get(entity_id).state == STATE_OFF + + # turn on at switch + await send_attributes_report(hass, cluster, {"window_detection_function": True}) + assert hass.states.get(entity_id).state == STATE_ON + + # turn off at switch + await send_attributes_report(hass, cluster, {"window_detection_function": False}) + assert hass.states.get(entity_id).state == STATE_OFF + + # turn on from HA + with patch( + "zigpy.zcl.Cluster.write_attributes", + return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), + ): + # turn on via UI + await hass.services.async_call( + SWITCH_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(cluster.write_attributes.mock_calls) == 1 + assert cluster.write_attributes.call_args == call( + {"window_detection_function": True} + ) + + # turn off from HA + with patch( + "zigpy.zcl.Cluster.write_attributes", + return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), + ): + # turn off via UI + await hass.services.async_call( + SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(cluster.write_attributes.mock_calls) == 2 + assert cluster.write_attributes.call_args == call( + {"window_detection_function": False} + ) + + cluster.read_attributes.reset_mock() + await async_setup_component(hass, "homeassistant", {}) + await hass.async_block_till_done() + + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + # the mocking doesn't update the attr cache so this flips back to initial value + assert cluster.read_attributes.call_count == 2 + assert [ + call( + [ + "window_detection_function", + ], + allow_cache=False, + only_cache=False, + manufacturer=None, + ), + call( + [ + "window_detection_function_inverter", + ], + allow_cache=False, + only_cache=False, + manufacturer=None, + ), + ] == cluster.read_attributes.call_args_list + + cluster.write_attributes.reset_mock() + cluster.write_attributes.side_effect = ZigbeeException + + await hass.services.async_call( + SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True + ) + + assert len(cluster.write_attributes.mock_calls) == 1 + assert cluster.write_attributes.call_args == call( + {"window_detection_function": False} + ) + + # test inverter + cluster.write_attributes.reset_mock() + cluster._attr_cache.update({0xEF02: True}) + + await hass.services.async_call( + SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(cluster.write_attributes.mock_calls) == 1 + assert cluster.write_attributes.call_args == call( + {"window_detection_function": True} + ) + + await hass.services.async_call( + SWITCH_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(cluster.write_attributes.mock_calls) == 2 + assert cluster.write_attributes.call_args == call( + {"window_detection_function": False} + ) + + # test joining a new switch to the network and HA + await async_test_rejoin(hass, zigpy_device_tuya, [cluster], (0,)) From 7aca007a9a092075acaaf28b16d527bafb2d8115 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 24 May 2022 19:44:33 -0400 Subject: [PATCH 0860/3516] Don't discover entities or initialize cluster channels for the coordinator in ZHA (#72442) don't discover coord entities or init channels --- homeassistant/components/zha/core/channels/__init__.py | 3 ++- homeassistant/components/zha/core/device.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 00409794473..a0df976486f 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -277,7 +277,8 @@ class ChannelPool: pool = cls(channels, ep_id) pool.add_all_channels() pool.add_client_channels() - zha_disc.PROBE.discover_entities(pool) + if not channels.zha_device.is_coordinator: + zha_disc.PROBE.discover_entities(pool) return pool @callback diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 854d80ffb78..e5b3403ba54 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -273,7 +273,7 @@ class ZHADevice(LogMixin): @property def skip_configuration(self) -> bool: """Return true if the device should not issue configuration related commands.""" - return self._zigpy_device.skip_configuration + return self._zigpy_device.skip_configuration or self.is_coordinator @property def gateway(self): From db815a7504cae47cee7dc9906eae66cc0f0a9fd5 Mon Sep 17 00:00:00 2001 From: rforro Date: Wed, 25 May 2022 01:56:03 +0200 Subject: [PATCH 0861/3516] ZHA Add entities for Lidl water valve quirk (#72307) * init * added timer number entity * added write attribute button entity * fixed missed errors * minor changes & fixed failing test * removed icon * unit and icons --- homeassistant/components/zha/binary_sensor.py | 13 +++ homeassistant/components/zha/button.py | 53 +++++++++ homeassistant/components/zha/number.py | 17 +++ homeassistant/components/zha/sensor.py | 16 +++ tests/components/zha/test_button.py | 106 +++++++++++++++++- 5 files changed, 204 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 730748d74ae..954c60fa895 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -171,3 +171,16 @@ class IASZone(BinarySensor): value = await self._channel.get_attribute_value("zone_status") if value is not None: self._state = value & 3 + + +@MULTI_MATCH( + channel_names="tuya_manufacturer", + manufacturers={ + "_TZE200_htnnfasr", + }, +) +class FrostLock(BinarySensor, id_suffix="frost_lock"): + """ZHA BinarySensor.""" + + SENSOR_ATTR = "frost_lock" + _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.LOCK diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py index f130936df02..9f241795267 100644 --- a/homeassistant/components/zha/button.py +++ b/homeassistant/components/zha/button.py @@ -6,6 +6,9 @@ import functools import logging from typing import Any +import zigpy.exceptions +from zigpy.zcl.foundation import Status + from homeassistant.components.button import ButtonDeviceClass, ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -21,6 +24,9 @@ from .core.typing import ChannelType, ZhaDeviceType from .entity import ZhaEntity MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.BUTTON) +CONFIG_DIAGNOSTIC_MATCH = functools.partial( + ZHA_ENTITIES.config_diagnostic_match, Platform.BUTTON +) DEFAULT_DURATION = 5 # seconds _LOGGER = logging.getLogger(__name__) @@ -103,3 +109,50 @@ class ZHAIdentifyButton(ZHAButton): """Return the arguments to use in the command.""" return [DEFAULT_DURATION] + + +class ZHAAttributeButton(ZhaEntity, ButtonEntity): + """Defines a ZHA button, which stes value to an attribute.""" + + _attribute_name: str = None + _attribute_value: Any = None + + def __init__( + self, + unique_id: str, + zha_device: ZhaDeviceType, + channels: list[ChannelType], + **kwargs, + ) -> None: + """Init this button.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + self._channel: ChannelType = channels[0] + + async def async_press(self) -> None: + """Write attribute with defined value.""" + try: + result = await self._channel.cluster.write_attributes( + {self._attribute_name: self._attribute_value} + ) + except zigpy.exceptions.ZigbeeException as ex: + self.error("Could not set value: %s", ex) + return + if not isinstance(result, Exception) and all( + record.status == Status.SUCCESS for record in result[0] + ): + self.async_write_ha_state() + + +@CONFIG_DIAGNOSTIC_MATCH( + channel_names="tuya_manufacturer", + manufacturers={ + "_TZE200_htnnfasr", + }, +) +class FrostLockResetButton(ZHAAttributeButton, id_suffix="reset_frost_lock"): + """Defines a ZHA identify button.""" + + _attribute_name = "frost_lock_reset" + _attribute_value = 0 + _attr_device_class = ButtonDeviceClass.RESTART + _attr_entity_category = EntityCategory.CONFIG diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 22d086891ca..bece4bc894d 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -495,3 +495,20 @@ class StartUpCurrentLevelConfigurationEntity( _attr_min_value: float = 0x00 _attr_max_value: float = 0xFF _zcl_attribute: str = "start_up_current_level" + + +@CONFIG_DIAGNOSTIC_MATCH( + channel_names="tuya_manufacturer", + manufacturers={ + "_TZE200_htnnfasr", + }, +) +class TimerDurationMinutes(ZHANumberConfigurationEntity, id_suffix="timer_duration"): + """Representation of a ZHA timer duration configuration entity.""" + + _attr_entity_category = EntityCategory.CONFIG + _attr_icon: str = ICONS[14] + _attr_min_value: float = 0x00 + _attr_max_value: float = 0x257 + _attr_unit_of_measurement: str | None = UNITS[72] + _zcl_attribute: str = "timer_duration" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 249034ef068..3e3017f6fa9 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -27,6 +27,7 @@ from homeassistant.const import ( PRESSURE_HPA, TEMP_CELSIUS, TIME_HOURS, + TIME_MINUTES, TIME_SECONDS, VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, @@ -754,3 +755,18 @@ class RSSISensor(Sensor, id_suffix="rssi"): @MULTI_MATCH(channel_names=CHANNEL_BASIC) class LQISensor(RSSISensor, id_suffix="lqi"): """LQI sensor for a device.""" + + +@MULTI_MATCH( + channel_names="tuya_manufacturer", + manufacturers={ + "_TZE200_htnnfasr", + }, +) +class TimeLeft(Sensor, id_suffix="time_left"): + """Sensor that displays time left value.""" + + SENSOR_ATTR = "timer_time_left" + _attr_device_class: SensorDeviceClass = SensorDeviceClass.DURATION + _attr_icon = "mdi:timer" + _unit = TIME_MINUTES diff --git a/tests/components/zha/test_button.py b/tests/components/zha/test_button.py index 762d2d46e54..f692528203f 100644 --- a/tests/components/zha/test_button.py +++ b/tests/components/zha/test_button.py @@ -1,11 +1,22 @@ """Test ZHA button.""" -from unittest.mock import patch +from unittest.mock import call, patch from freezegun import freeze_time import pytest +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + OUTPUT_CLUSTERS, + PROFILE_ID, +) from zigpy.const import SIG_EP_PROFILE +from zigpy.exceptions import ZigbeeException import zigpy.profiles.zha as zha +from zigpy.quirks import CustomCluster, CustomDevice +import zigpy.types as t import zigpy.zcl.clusters.general as general +from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster import zigpy.zcl.clusters.security as security import zigpy.zcl.foundation as zcl_f @@ -14,6 +25,7 @@ from homeassistant.components.button.const import SERVICE_PRESS from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, + ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC, STATE_UNKNOWN, ) @@ -48,6 +60,49 @@ async def contact_sensor(hass, zigpy_device_mock, zha_device_joined_restored): return zha_device, zigpy_device.endpoints[1].identify +class FrostLockQuirk(CustomDevice): + """Quirk with frost lock attribute.""" + + class TuyaManufCluster(CustomCluster, ManufacturerSpecificCluster): + """Tuya manufacturer specific cluster.""" + + cluster_id = 0xEF00 + ep_attribute = "tuya_manufacturer" + + attributes = {0xEF01: ("frost_lock_reset", t.Bool)} + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, + INPUT_CLUSTERS: [general.Basic.cluster_id, TuyaManufCluster], + OUTPUT_CLUSTERS: [], + }, + } + } + + +@pytest.fixture +async def tuya_water_valve(hass, zigpy_device_mock, zha_device_joined_restored): + """Tuya Water Valve fixture.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [general.Basic.cluster_id], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, + } + }, + manufacturer="_TZE200_htnnfasr", + quirk=FrostLockQuirk, + ) + + zha_device = await zha_device_joined_restored(zigpy_device) + return zha_device, zigpy_device.endpoints[1].tuya_manufacturer + + @freeze_time("2021-11-04 17:37:00", tz_offset=-1) async def test_button(hass, contact_sensor): """Test zha button platform.""" @@ -87,3 +142,52 @@ async def test_button(hass, contact_sensor): assert state assert state.state == "2021-11-04T16:37:00+00:00" assert state.attributes[ATTR_DEVICE_CLASS] == ButtonDeviceClass.UPDATE + + +async def test_frost_unlock(hass, tuya_water_valve): + """Test custom frost unlock zha button.""" + + entity_registry = er.async_get(hass) + zha_device, cluster = tuya_water_valve + assert cluster is not None + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNKNOWN + assert state.attributes[ATTR_DEVICE_CLASS] == ButtonDeviceClass.RESTART + + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.entity_category == ENTITY_CATEGORY_CONFIG + + with patch( + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), + ): + await hass.services.async_call( + DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + assert len(cluster.write_attributes.mock_calls) == 1 + assert cluster.write_attributes.call_args == call({"frost_lock_reset": 0}) + + state = hass.states.get(entity_id) + assert state + assert state.attributes[ATTR_DEVICE_CLASS] == ButtonDeviceClass.RESTART + + cluster.write_attributes.reset_mock() + cluster.write_attributes.side_effect = ZigbeeException + + await hass.services.async_call( + DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + assert len(cluster.write_attributes.mock_calls) == 1 + assert cluster.write_attributes.call_args == call({"frost_lock_reset": 0}) From 301341a49feb4150ef78fad4713dc9eb159bda6e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 25 May 2022 00:26:18 +0000 Subject: [PATCH 0862/3516] [ci skip] Translation update --- .../components/google/translations/ca.json | 9 ++ .../components/google/translations/hu.json | 9 ++ .../components/google/translations/pl.json | 9 ++ .../components/google/translations/tr.json | 9 ++ .../components/hassio/translations/ca.json | 1 + .../components/hassio/translations/et.json | 1 + .../components/hassio/translations/hu.json | 1 + .../components/hassio/translations/it.json | 1 + .../components/hassio/translations/ja.json | 1 + .../components/hassio/translations/no.json | 1 + .../components/hassio/translations/pl.json | 1 + .../components/hassio/translations/tr.json | 1 + .../hassio/translations/zh-Hant.json | 1 + .../components/heos/translations/ja.json | 2 +- .../here_travel_time/translations/hu.json | 82 +++++++++++++++++++ .../here_travel_time/translations/pl.json | 63 ++++++++++++++ .../here_travel_time/translations/tr.json | 82 +++++++++++++++++++ .../components/konnected/translations/hu.json | 16 ++-- .../components/konnected/translations/tr.json | 16 ++-- .../components/laundrify/translations/hu.json | 25 ++++++ .../components/laundrify/translations/pl.json | 5 +- .../components/laundrify/translations/tr.json | 25 ++++++ .../components/recorder/translations/ca.json | 2 + .../components/recorder/translations/hu.json | 2 + .../components/recorder/translations/pl.json | 2 + .../components/recorder/translations/tr.json | 2 + .../components/shelly/translations/tr.json | 1 + .../steam_online/translations/ca.json | 3 + .../steam_online/translations/et.json | 3 + .../steam_online/translations/hu.json | 3 + .../steam_online/translations/it.json | 3 + .../steam_online/translations/ja.json | 3 + .../steam_online/translations/no.json | 3 + .../steam_online/translations/pl.json | 3 + .../steam_online/translations/tr.json | 3 + .../steam_online/translations/zh-Hant.json | 3 + 36 files changed, 379 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/here_travel_time/translations/hu.json create mode 100644 homeassistant/components/here_travel_time/translations/tr.json create mode 100644 homeassistant/components/laundrify/translations/hu.json create mode 100644 homeassistant/components/laundrify/translations/tr.json diff --git a/homeassistant/components/google/translations/ca.json b/homeassistant/components/google/translations/ca.json index 829ce1413d5..2c9190e4bfd 100644 --- a/homeassistant/components/google/translations/ca.json +++ b/homeassistant/components/google/translations/ca.json @@ -27,5 +27,14 @@ "title": "Reautenticaci\u00f3 de la integraci\u00f3" } } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Acc\u00e9s de Home Assistant a Google Calendar" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/hu.json b/homeassistant/components/google/translations/hu.json index 2c552eca30e..66ea71c72ec 100644 --- a/homeassistant/components/google/translations/hu.json +++ b/homeassistant/components/google/translations/hu.json @@ -27,5 +27,14 @@ "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" } } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Home Assistant hozz\u00e1f\u00e9r\u00e9s a Google Napt\u00e1rhoz" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/pl.json b/homeassistant/components/google/translations/pl.json index b4f45c0abe4..fff2a20ee39 100644 --- a/homeassistant/components/google/translations/pl.json +++ b/homeassistant/components/google/translations/pl.json @@ -27,5 +27,14 @@ "title": "Ponownie uwierzytelnij integracj\u0119" } } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Dost\u0119p Home Assistanta do Kalendarza Google" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/tr.json b/homeassistant/components/google/translations/tr.json index 9d5fc8d2416..f7b5b6d79ff 100644 --- a/homeassistant/components/google/translations/tr.json +++ b/homeassistant/components/google/translations/tr.json @@ -27,5 +27,14 @@ "title": "Entegrasyonu Yeniden Do\u011frula" } } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Google Takvim'e Home Assistant eri\u015fimi" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/ca.json b/homeassistant/components/hassio/translations/ca.json index 6a6874662b2..2c4285d4908 100644 --- a/homeassistant/components/hassio/translations/ca.json +++ b/homeassistant/components/hassio/translations/ca.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "Versi\u00f3 de l'agent", "board": "Placa", "disk_total": "Total del disc", "disk_used": "Emmagatzematge utilitzat", diff --git a/homeassistant/components/hassio/translations/et.json b/homeassistant/components/hassio/translations/et.json index 4449c058498..b86eef353b9 100644 --- a/homeassistant/components/hassio/translations/et.json +++ b/homeassistant/components/hassio/translations/et.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "Agendi versioon", "board": "Seade", "disk_total": "Kettaruum kokku", "disk_used": "Kasutatud kettaruum", diff --git a/homeassistant/components/hassio/translations/hu.json b/homeassistant/components/hassio/translations/hu.json index 8d2bdfd75ad..4c83b94935d 100644 --- a/homeassistant/components/hassio/translations/hu.json +++ b/homeassistant/components/hassio/translations/hu.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "\u00dcgyn\u00f6k verzi\u00f3", "board": "Alaplap", "disk_total": "\u00d6sszes hely", "disk_used": "Felhaszn\u00e1lt hely", diff --git a/homeassistant/components/hassio/translations/it.json b/homeassistant/components/hassio/translations/it.json index 44499d3f002..3dc55d0f525 100644 --- a/homeassistant/components/hassio/translations/it.json +++ b/homeassistant/components/hassio/translations/it.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "Versione agente", "board": "Scheda di base", "disk_total": "Disco totale", "disk_used": "Disco utilizzato", diff --git a/homeassistant/components/hassio/translations/ja.json b/homeassistant/components/hassio/translations/ja.json index b7489852bdb..2561cf75310 100644 --- a/homeassistant/components/hassio/translations/ja.json +++ b/homeassistant/components/hassio/translations/ja.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "\u30a8\u30fc\u30b8\u30a7\u30f3\u30c8\u306e\u30d0\u30fc\u30b8\u30e7\u30f3", "board": "\u30dc\u30fc\u30c9", "disk_total": "\u30c7\u30a3\u30b9\u30af\u5408\u8a08", "disk_used": "\u4f7f\u7528\u6e08\u307f\u30c7\u30a3\u30b9\u30af", diff --git a/homeassistant/components/hassio/translations/no.json b/homeassistant/components/hassio/translations/no.json index 30ff8b903a9..1fa10a98921 100644 --- a/homeassistant/components/hassio/translations/no.json +++ b/homeassistant/components/hassio/translations/no.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "Agentversjon", "board": "Styret", "disk_total": "Disk totalt", "disk_used": "Disk brukt", diff --git a/homeassistant/components/hassio/translations/pl.json b/homeassistant/components/hassio/translations/pl.json index 2f6b5cab1dc..8850b7066fd 100644 --- a/homeassistant/components/hassio/translations/pl.json +++ b/homeassistant/components/hassio/translations/pl.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "Wersja agenta", "board": "Uk\u0142ad", "disk_total": "Pojemno\u015b\u0107 dysku", "disk_used": "Pojemno\u015b\u0107 u\u017cyta", diff --git a/homeassistant/components/hassio/translations/tr.json b/homeassistant/components/hassio/translations/tr.json index 16504c32372..cf92d597e23 100644 --- a/homeassistant/components/hassio/translations/tr.json +++ b/homeassistant/components/hassio/translations/tr.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "Arac\u0131 S\u00fcr\u00fcm\u00fc", "board": "Panel", "disk_total": "Disk Toplam\u0131", "disk_used": "Kullan\u0131lan Disk", diff --git a/homeassistant/components/hassio/translations/zh-Hant.json b/homeassistant/components/hassio/translations/zh-Hant.json index 91c7f64e39c..5a503e54937 100644 --- a/homeassistant/components/hassio/translations/zh-Hant.json +++ b/homeassistant/components/hassio/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "Agent \u7248\u672c", "board": "\u677f", "disk_total": "\u7e3d\u78c1\u789f\u7a7a\u9593", "disk_used": "\u5df2\u4f7f\u7528\u7a7a\u9593", diff --git a/homeassistant/components/heos/translations/ja.json b/homeassistant/components/heos/translations/ja.json index 0464bf98979..55e075a548a 100644 --- a/homeassistant/components/heos/translations/ja.json +++ b/homeassistant/components/heos/translations/ja.json @@ -11,7 +11,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "Heos\u30c7\u30d0\u30a4\u30b9(\u3067\u304d\u308c\u3070\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u6709\u7dda\u3067\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9)\u306e\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "description": "Heos\u30c7\u30d0\u30a4\u30b9(\u53ef\u80fd\u306a\u306e\u3067\u3042\u308c\u3070\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u6709\u7dda\u3067\u63a5\u7d9a\u3055\u308c\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9)\u306e\u30db\u30b9\u30c8\u540d\u307e\u305f\u306fIP\u30a2\u30c9\u30ec\u30b9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Heos\u306b\u63a5\u7d9a" } } diff --git a/homeassistant/components/here_travel_time/translations/hu.json b/homeassistant/components/here_travel_time/translations/hu.json new file mode 100644 index 00000000000..cdd40f139d7 --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/hu.json @@ -0,0 +1,82 @@ +{ + "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" + }, + "step": { + "destination_coordinates": { + "data": { + "destination": "C\u00e9l GPS koordin\u00e1tak\u00e9nt" + }, + "title": "C\u00e9l kiv\u00e1laszt\u00e1sa" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "C\u00e9l egy entit\u00e1s haszn\u00e1lat\u00e1val" + }, + "title": "C\u00e9l kiv\u00e1laszt\u00e1sa" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "T\u00e9rk\u00e9pes hely haszn\u00e1lata", + "destination_entity": "Entit\u00e1s haszn\u00e1lata" + }, + "title": "C\u00e9l kiv\u00e1laszt\u00e1sa" + }, + "origin_coordinates": { + "data": { + "origin": "Eredet GPS koordin\u00e1t\u00e1kk\u00e9nt" + }, + "title": "Eredet kiv\u00e1laszt\u00e1sa" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "Eredet egy entit\u00e1s haszn\u00e1lat\u00e1val" + }, + "title": "Eredet kiv\u00e1laszt\u00e1sa" + }, + "user": { + "data": { + "api_key": "API kulcs", + "mode": "Utaz\u00e1si m\u00f3d", + "name": "Elnevez\u00e9s" + } + } + } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "\u00c9rkez\u00e9s ideje" + }, + "title": "\u00c9rkez\u00e9si id\u0151 kiv\u00e1laszt\u00e1sa" + }, + "departure_time": { + "data": { + "departure_time": "Indul\u00e1si id\u0151" + }, + "title": "Indul\u00e1si id\u0151 kiv\u00e1laszt\u00e1sa" + }, + "init": { + "data": { + "route_mode": "\u00datvonaltervez\u00e9si m\u00f3d", + "traffic_mode": "Forgalmi m\u00f3d", + "unit_system": "Egys\u00e9grendszer" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "\u00c9rkez\u00e9si id\u0151 konfigur\u00e1l\u00e1sa", + "departure_time": "Indul\u00e1si id\u0151 be\u00e1ll\u00edt\u00e1sa", + "no_time": "Ne konfigur\u00e1ljon id\u0151pontot" + }, + "title": "Id\u0151t\u00edpus kiv\u00e1laszt\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/pl.json b/homeassistant/components/here_travel_time/translations/pl.json index f04290714e4..3e4f41212a2 100644 --- a/homeassistant/components/here_travel_time/translations/pl.json +++ b/homeassistant/components/here_travel_time/translations/pl.json @@ -8,12 +8,75 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "destination_coordinates": { + "data": { + "destination": "Punkt docelowy jako wsp\u00f3\u0142rz\u0119dne GPS" + }, + "title": "Wybierz punkt docelowy" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "Punkt docelowy przy u\u017cyciu encji" + }, + "title": "Wybierz punkt docelowy" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "Lokalizacja na mapie", + "destination_entity": "Encja" + }, + "title": "Wybierz punkt docelowy" + }, + "origin_coordinates": { + "data": { + "origin": "Punkt pocz\u0105tkowy jako wsp\u00f3\u0142rz\u0119dne GPS" + }, + "title": "Wybierz punkt pocz\u0105tkowy" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "Punkt pocz\u0105tkowy przy u\u017cyciu encji" + }, + "title": "Wybierz punkt pocz\u0105tkowy" + }, "user": { "data": { "api_key": "Klucz API", + "mode": "Tryb podr\u00f3\u017cy", "name": "Nazwa" } } } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "Czas przyjazdu" + }, + "title": "Wybierz czas przyjazdu" + }, + "departure_time": { + "data": { + "departure_time": "Czas wyjazdu" + }, + "title": "Wybierz czas wyjazdu" + }, + "init": { + "data": { + "route_mode": "Tryb trasy", + "traffic_mode": "Tryb ruchu", + "unit_system": "System metryczny" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "Skonfiguruj czas przyjazdu", + "departure_time": "Skonfiguruj czas wyjazdu", + "no_time": "Nie konfiguruj czasu" + }, + "title": "Wybierz typ czasu" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/tr.json b/homeassistant/components/here_travel_time/translations/tr.json new file mode 100644 index 00000000000..181588ba54a --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/tr.json @@ -0,0 +1,82 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "destination_coordinates": { + "data": { + "destination": "GPS koordinatlar\u0131 olarak hedef" + }, + "title": "Hedef Se\u00e7" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "Bir varl\u0131k kullanarak hedef" + }, + "title": "Hedef Se\u00e7" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "Bir harita konumu kullan\u0131n", + "destination_entity": "Bir varl\u0131\u011f\u0131 kullan\u0131n" + }, + "title": "Hedef Se\u00e7" + }, + "origin_coordinates": { + "data": { + "origin": "GPS koordinatlar\u0131 olarak kalk\u0131\u015f" + }, + "title": "Kalk\u0131\u015f Se\u00e7in" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "Bir varl\u0131k kullanarak kaynak" + }, + "title": "Kalk\u0131\u015f Se\u00e7in" + }, + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "mode": "Seyahat Modu", + "name": "Ad" + } + } + } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "Var\u0131\u015f Zaman\u0131" + }, + "title": "Var\u0131\u015f Saatini Se\u00e7in" + }, + "departure_time": { + "data": { + "departure_time": "Hareket Saati" + }, + "title": "Kalk\u0131\u015f Saatini Se\u00e7in" + }, + "init": { + "data": { + "route_mode": "Rota Modu", + "traffic_mode": "Trafik Modu", + "unit_system": "\u00d6l\u00e7\u00fc sistemi" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "Var\u0131\u015f saatini yap\u0131land\u0131r\u0131n", + "departure_time": "Kalk\u0131\u015f saati yap\u0131land\u0131r\u0131n", + "no_time": "Bir zaman yap\u0131land\u0131rmay\u0131n" + }, + "title": "Zaman T\u00fcr\u00fcn\u00fc Se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/hu.json b/homeassistant/components/konnected/translations/hu.json index c40b1823424..57cbf0d9bb9 100644 --- a/homeassistant/components/konnected/translations/hu.json +++ b/homeassistant/components/konnected/translations/hu.json @@ -41,7 +41,7 @@ "options_binary": { "data": { "inverse": "Invert\u00e1lja a nyitott/z\u00e1rt \u00e1llapotot", - "name": "Elnevez\u00e9s (nem k\u00f6telez\u0151)", + "name": "Elnevez\u00e9s", "type": "Bin\u00e1ris \u00e9rz\u00e9kel\u0151 t\u00edpusa" }, "description": "{zone} opci\u00f3k", @@ -49,8 +49,8 @@ }, "options_digital": { "data": { - "name": "Elnevez\u00e9s (nem k\u00f6telez\u0151)", - "poll_interval": "Lek\u00e9rdez\u00e9si id\u0151k\u00f6z (perc) (opcion\u00e1lis)", + "name": "Elnevez\u00e9s", + "poll_interval": "Lek\u00e9rdez\u00e9si id\u0151k\u00f6z (perc)", "type": "\u00c9rz\u00e9kel\u0151 t\u00edpusa" }, "description": "{zone} opci\u00f3k", @@ -86,7 +86,7 @@ }, "options_misc": { "data": { - "api_host": "API host URL fel\u00fclb\u00edr\u00e1l\u00e1sa (opcion\u00e1lis)", + "api_host": "API host URL fel\u00fclb\u00edr\u00e1l\u00e1sa", "blink": "A panel LED villog\u00e1sa \u00e1llapotv\u00e1ltoz\u00e1skor", "discovery": "V\u00e1laszoljon a h\u00e1l\u00f3zaton \u00e9rkez\u0151 felder\u00edt\u00e9si k\u00e9r\u00e9sekre", "override_api_host": "Az alap\u00e9rtelmezett Home Assistant API host-URL fel\u00fcl\u00edr\u00e1sa" @@ -97,11 +97,11 @@ "options_switch": { "data": { "activation": "Kimenet bekapcsolt \u00e1llapotban", - "momentary": "Impulzus id\u0151tartama (ms) (opcion\u00e1lis)", + "momentary": "Impulzus id\u0151tartama (ms)", "more_states": "Tov\u00e1bbi \u00e1llapotok konfigur\u00e1l\u00e1sa ehhez a z\u00f3n\u00e1hoz", - "name": "Elnevez\u00e9s (nem k\u00f6telez\u0151)", - "pause": "Sz\u00fcnet impulzusok k\u00f6z\u00f6tt (ms) (opcion\u00e1lis)", - "repeat": "Ism\u00e9tl\u00e9si id\u0151k (-1 = v\u00e9gtelen) (opcion\u00e1lis)" + "name": "Elnevez\u00e9s", + "pause": "Sz\u00fcnet impulzusok k\u00f6z\u00f6tt (ms)", + "repeat": "Ism\u00e9tl\u00e9si id\u0151k (-1 = v\u00e9gtelen)" }, "description": "{zone} opci\u00f3k: \u00e1llapot {state}", "title": "Kapcsolhat\u00f3 kimenet konfigur\u00e1l\u00e1sa" diff --git a/homeassistant/components/konnected/translations/tr.json b/homeassistant/components/konnected/translations/tr.json index 3c23b26ab0d..f5cb48dc088 100644 --- a/homeassistant/components/konnected/translations/tr.json +++ b/homeassistant/components/konnected/translations/tr.json @@ -41,7 +41,7 @@ "options_binary": { "data": { "inverse": "A\u00e7\u0131k / kapal\u0131 durumunu tersine \u00e7evirin", - "name": "Ad (iste\u011fe ba\u011fl\u0131)", + "name": "Ad", "type": "\u0130kili Sens\u00f6r Tipi" }, "description": "{zone} se\u00e7enekleri", @@ -49,8 +49,8 @@ }, "options_digital": { "data": { - "name": "Ad (iste\u011fe ba\u011fl\u0131)", - "poll_interval": "Yoklama Aral\u0131\u011f\u0131 (dakika) (iste\u011fe ba\u011fl\u0131)", + "name": "Ad", + "poll_interval": "Yoklama Aral\u0131\u011f\u0131 (dakika)", "type": "Sens\u00f6r Tipi" }, "description": "{zone} se\u00e7enekleri", @@ -86,7 +86,7 @@ }, "options_misc": { "data": { - "api_host": "API ana makine URL'sini ge\u00e7ersiz k\u0131l (iste\u011fe ba\u011fl\u0131)", + "api_host": "API ana makine URL'sini ge\u00e7ersiz k\u0131l", "blink": "Durum de\u011fi\u015fikli\u011fi g\u00f6nderilirken panelin LED'ini yan\u0131p s\u00f6nd\u00fcr", "discovery": "A\u011f\u0131n\u0131zdaki ke\u015fif isteklerine yan\u0131t verin", "override_api_host": "Varsay\u0131lan Home Assistant API ana bilgisayar paneli URL'sini ge\u00e7ersiz k\u0131l" @@ -97,11 +97,11 @@ "options_switch": { "data": { "activation": "A\u00e7\u0131kken \u00e7\u0131kt\u0131", - "momentary": "Darbe s\u00fcresi (ms) (iste\u011fe ba\u011fl\u0131)", + "momentary": "Darbe s\u00fcresi (ms)", "more_states": "Bu b\u00f6lge i\u00e7in ek durumlar yap\u0131land\u0131r\u0131n", - "name": "Ad (iste\u011fe ba\u011fl\u0131)", - "pause": "Darbeler aras\u0131nda duraklama (ms) (iste\u011fe ba\u011fl\u0131)", - "repeat": "Tekrarlanacak zamanlar (-1=sonsuz) (iste\u011fe ba\u011fl\u0131)" + "name": "Ad", + "pause": "Darbeler aras\u0131nda duraklama (ms)", + "repeat": "Tekrarlanacak zamanlar (-1=sonsuz)" }, "description": "{zone} se\u00e7enekleri: durum {state}", "title": "De\u011fi\u015ftirilebilir \u00c7\u0131k\u0131\u015f\u0131 Yap\u0131land\u0131r" diff --git a/homeassistant/components/laundrify/translations/hu.json b/homeassistant/components/laundrify/translations/hu.json new file mode 100644 index 00000000000..aa22ddc3da3 --- /dev/null +++ b/homeassistant/components/laundrify/translations/hu.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_format": "\u00c9rv\u00e9nytelen form\u00e1tum. K\u00e9rj\u00fck, adja meg \u00edgy: xxx-xxx.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "init": { + "data": { + "code": "Hiteles\u00edt\u00e9si k\u00f3d (xxx-xxx)" + }, + "description": "K\u00e9rj\u00fck, adja meg a laundrify-alkalmaz\u00e1sban megjelen\u0151 szem\u00e9lyes enged\u00e9lyez\u00e9si k\u00f3dj\u00e1t." + }, + "reauth_confirm": { + "description": "A laundrify integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie.", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/pl.json b/homeassistant/components/laundrify/translations/pl.json index 28145b20fce..27b33ab5880 100644 --- a/homeassistant/components/laundrify/translations/pl.json +++ b/homeassistant/components/laundrify/translations/pl.json @@ -6,15 +6,18 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_format": "Niepoprawny format. Prosz\u0119 poda\u0107 jako xxx-xxx.", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { "init": { "data": { "code": "Kod autoryzacyjny (xxx-xxx)" - } + }, + "description": "Wprowad\u017a sw\u00f3j osobisty kod autoryzacyjny, kt\u00f3ry jest widoczny w aplikacji laundrify." }, "reauth_confirm": { + "description": "Integracja laundrify wymaga ponownego uwierzytelnienia Twojego konta.", "title": "Ponownie uwierzytelnij integracj\u0119" } } diff --git a/homeassistant/components/laundrify/translations/tr.json b/homeassistant/components/laundrify/translations/tr.json new file mode 100644 index 00000000000..97a1b13e192 --- /dev/null +++ b/homeassistant/components/laundrify/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_format": "Ge\u00e7ersiz format. L\u00fctfen xxx-xxx olarak belirtin.", + "unknown": "Beklenmeyen hata" + }, + "step": { + "init": { + "data": { + "code": "Yetkilendirme Kodu (xxx-xxx)" + }, + "description": "L\u00fctfen laundrify-App g\u00f6sterilen ki\u015fisel Yetkilendirme Kodunuzu girin." + }, + "reauth_confirm": { + "description": "\u00c7ama\u015f\u0131r y\u0131kama entegrasyonunun yeniden kimlik do\u011frulamas\u0131 yapmas\u0131 gerekiyor.", + "title": "Entegrasyonu Yeniden Do\u011frula" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/ca.json b/homeassistant/components/recorder/translations/ca.json index ee9efb50b21..4450be81574 100644 --- a/homeassistant/components/recorder/translations/ca.json +++ b/homeassistant/components/recorder/translations/ca.json @@ -2,6 +2,8 @@ "system_health": { "info": { "current_recorder_run": "Hora d'inici de l'execuci\u00f3 actual", + "database_engine": "Motor de bases de dades", + "database_version": "Versi\u00f3 de la base de dades", "estimated_db_size": "Mida estimada de la base de dades (MiB)", "oldest_recorder_run": "Hora d'inici de l'execuci\u00f3 m\u00e9s antiga" } diff --git a/homeassistant/components/recorder/translations/hu.json b/homeassistant/components/recorder/translations/hu.json index 96bb02c9c20..323c1d489f1 100644 --- a/homeassistant/components/recorder/translations/hu.json +++ b/homeassistant/components/recorder/translations/hu.json @@ -2,6 +2,8 @@ "system_health": { "info": { "current_recorder_run": "Aktu\u00e1lis futtat\u00e1s kezd\u00e9si id\u0151pontja", + "database_engine": "Adatb\u00e1zis motor", + "database_version": "Adatb\u00e1zis verzi\u00f3", "estimated_db_size": "Az adatb\u00e1zis becs\u00fclt m\u00e9rete (MiB)", "oldest_recorder_run": "Legr\u00e9gebbi futtat\u00e1s kezd\u00e9si id\u0151pontja" } diff --git a/homeassistant/components/recorder/translations/pl.json b/homeassistant/components/recorder/translations/pl.json index a91f92207f2..77c5f2750f7 100644 --- a/homeassistant/components/recorder/translations/pl.json +++ b/homeassistant/components/recorder/translations/pl.json @@ -2,6 +2,8 @@ "system_health": { "info": { "current_recorder_run": "Pocz\u0105tek aktualnej sesji rejestratora", + "database_engine": "Silnik bazy danych", + "database_version": "Wersja bazy danych", "estimated_db_size": "Szacowany rozmiar bazy danych (MiB)", "oldest_recorder_run": "Pocz\u0105tek najstarszej sesji rejestratora" } diff --git a/homeassistant/components/recorder/translations/tr.json b/homeassistant/components/recorder/translations/tr.json index 1fdc0e408b3..27606e9ab27 100644 --- a/homeassistant/components/recorder/translations/tr.json +++ b/homeassistant/components/recorder/translations/tr.json @@ -2,6 +2,8 @@ "system_health": { "info": { "current_recorder_run": "Mevcut \u00c7al\u0131\u015ft\u0131rma Ba\u015flang\u0131\u00e7 Zaman\u0131", + "database_engine": "Veritaban\u0131 Altyap\u0131s\u0131", + "database_version": "Veritaban\u0131 S\u00fcr\u00fcm\u00fc", "estimated_db_size": "Tahmini Veritaban\u0131 Boyutu (MB)", "oldest_recorder_run": "En Eski \u00c7al\u0131\u015ft\u0131rma Ba\u015flang\u0131\u00e7 Zaman\u0131" } diff --git a/homeassistant/components/shelly/translations/tr.json b/homeassistant/components/shelly/translations/tr.json index 0a70a067690..fac805e5134 100644 --- a/homeassistant/components/shelly/translations/tr.json +++ b/homeassistant/components/shelly/translations/tr.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", + "firmware_not_fully_provisioned": "Cihaz tam olarak sa\u011flanmad\u0131. L\u00fctfen Shelly deste\u011fiyle ileti\u015fime ge\u00e7in", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmeyen hata" }, diff --git a/homeassistant/components/steam_online/translations/ca.json b/homeassistant/components/steam_online/translations/ca.json index 699630c3732..a9491e2e502 100644 --- a/homeassistant/components/steam_online/translations/ca.json +++ b/homeassistant/components/steam_online/translations/ca.json @@ -25,6 +25,9 @@ } }, "options": { + "error": { + "unauthorized": "Llista d'amics restringida: consulta la documentaci\u00f3 sobre com veure tots els amics" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/steam_online/translations/et.json b/homeassistant/components/steam_online/translations/et.json index 7c09fa1de91..62e38eb4088 100644 --- a/homeassistant/components/steam_online/translations/et.json +++ b/homeassistant/components/steam_online/translations/et.json @@ -25,6 +25,9 @@ } }, "options": { + "error": { + "unauthorized": "S\u00f5prade nimekiri on piiratud: vaata dokumentatsiooni kuidas n\u00e4ha k\u00f5iki teisi s\u00f5pru" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/steam_online/translations/hu.json b/homeassistant/components/steam_online/translations/hu.json index edff854ab52..a7d6c6a7de4 100644 --- a/homeassistant/components/steam_online/translations/hu.json +++ b/homeassistant/components/steam_online/translations/hu.json @@ -25,6 +25,9 @@ } }, "options": { + "error": { + "unauthorized": "Bar\u00e1ti lista korl\u00e1tozott: K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t arr\u00f3l, hogyan l\u00e1thatja az \u00f6sszes t\u00f6bbi bar\u00e1tot." + }, "step": { "init": { "data": { diff --git a/homeassistant/components/steam_online/translations/it.json b/homeassistant/components/steam_online/translations/it.json index edb7878234d..3f2e9481301 100644 --- a/homeassistant/components/steam_online/translations/it.json +++ b/homeassistant/components/steam_online/translations/it.json @@ -25,6 +25,9 @@ } }, "options": { + "error": { + "unauthorized": "Elenco amici limitato: consultare la documentazione su come vedere tutti gli altri amici." + }, "step": { "init": { "data": { diff --git a/homeassistant/components/steam_online/translations/ja.json b/homeassistant/components/steam_online/translations/ja.json index 935c975c801..e138d46319b 100644 --- a/homeassistant/components/steam_online/translations/ja.json +++ b/homeassistant/components/steam_online/translations/ja.json @@ -25,6 +25,9 @@ } }, "options": { + "error": { + "unauthorized": "\u30d5\u30ec\u30f3\u30c9\u30ea\u30b9\u30c8\u306e\u5236\u9650: \u4ed6\u306e\u3059\u3079\u3066\u306e\u30d5\u30ec\u30f3\u30c9\u3092\u8868\u793a\u3059\u308b\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/steam_online/translations/no.json b/homeassistant/components/steam_online/translations/no.json index 021218823f5..1b30669fad4 100644 --- a/homeassistant/components/steam_online/translations/no.json +++ b/homeassistant/components/steam_online/translations/no.json @@ -25,6 +25,9 @@ } }, "options": { + "error": { + "unauthorized": "Begrenset venneliste: Se dokumentasjonen for hvordan du ser alle andre venner" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/steam_online/translations/pl.json b/homeassistant/components/steam_online/translations/pl.json index 370b997b3fc..3ceff9e442f 100644 --- a/homeassistant/components/steam_online/translations/pl.json +++ b/homeassistant/components/steam_online/translations/pl.json @@ -25,6 +25,9 @@ } }, "options": { + "error": { + "unauthorized": "Lista znajomych ograniczona: zapoznaj si\u0119 z dokumentacj\u0105, aby dowiedzie\u0107 si\u0119, jak zobaczy\u0107 wszystkich innych znajomych" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/steam_online/translations/tr.json b/homeassistant/components/steam_online/translations/tr.json index c378c90855e..a1717857080 100644 --- a/homeassistant/components/steam_online/translations/tr.json +++ b/homeassistant/components/steam_online/translations/tr.json @@ -25,6 +25,9 @@ } }, "options": { + "error": { + "unauthorized": "Arkada\u015f listesi k\u0131s\u0131tland\u0131: L\u00fctfen di\u011fer t\u00fcm arkada\u015flar\u0131 nas\u0131l g\u00f6rece\u011finizle ilgili belgelere bak\u0131n" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/steam_online/translations/zh-Hant.json b/homeassistant/components/steam_online/translations/zh-Hant.json index 17cff11185e..7de1d6f1a3c 100644 --- a/homeassistant/components/steam_online/translations/zh-Hant.json +++ b/homeassistant/components/steam_online/translations/zh-Hant.json @@ -25,6 +25,9 @@ } }, "options": { + "error": { + "unauthorized": "\u597d\u53cb\u5217\u8868\u53d7\u9650\uff1a\u8acb\u53c3\u8003\u6587\u4ef6\u8cc7\u6599\u4ee5\u4e86\u89e3\u5982\u4f55\u986f\u793a\u6240\u6709\u597d\u53cb\u3002" + }, "step": { "init": { "data": { From a2a691f232da063b93bada9941c7bf332769012a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 25 May 2022 02:40:26 +0200 Subject: [PATCH 0863/3516] Improve Sensibo terminology (#72451) --- homeassistant/components/sensibo/number.py | 4 ++-- homeassistant/components/sensibo/select.py | 4 ++-- homeassistant/components/sensibo/sensor.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py index 69d9237da7a..fc18e28f1a3 100644 --- a/homeassistant/components/sensibo/number.py +++ b/homeassistant/components/sensibo/number.py @@ -29,7 +29,7 @@ class SensiboNumberEntityDescription( """Class describing Sensibo Number entities.""" -NUMBER_TYPES = ( +DEVICE_NUMBER_TYPES = ( SensiboNumberEntityDescription( key="calibration_temp", remote_key="temperature", @@ -65,7 +65,7 @@ async def async_setup_entry( async_add_entities( SensiboNumber(coordinator, device_id, description) for device_id, device_data in coordinator.data.parsed.items() - for description in NUMBER_TYPES + for description in DEVICE_NUMBER_TYPES ) diff --git a/homeassistant/components/sensibo/select.py b/homeassistant/components/sensibo/select.py index c442fbc1374..56b8fbac4fd 100644 --- a/homeassistant/components/sensibo/select.py +++ b/homeassistant/components/sensibo/select.py @@ -29,7 +29,7 @@ class SensiboSelectEntityDescription( """Class describing Sensibo Number entities.""" -SELECT_TYPES = ( +DEVICE_SELECT_TYPES = ( SensiboSelectEntityDescription( key="horizontalSwing", remote_key="horizontal_swing_mode", @@ -57,7 +57,7 @@ async def async_setup_entry( async_add_entities( SensiboSelect(coordinator, device_id, description) for device_id, device_data in coordinator.data.parsed.items() - for description in SELECT_TYPES + for description in DEVICE_SELECT_TYPES if description.key in device_data.full_features ) diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 307cfac6003..7ac871a61eb 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -99,7 +99,7 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( value_fn=lambda data: data.temperature, ), ) -DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( +PURE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( SensiboDeviceSensorEntityDescription( key="pm25", device_class=SensorDeviceClass.PM25, @@ -137,8 +137,8 @@ async def async_setup_entry( entities.extend( SensiboDeviceSensor(coordinator, device_id, description) for device_id, device_data in coordinator.data.parsed.items() - for description in DEVICE_SENSOR_TYPES - if getattr(device_data, description.key) is not None + for description in PURE_SENSOR_TYPES + if device_data.model == "pure" ) async_add_entities(entities) From b9baaf573c551390667408b85b078dc166ca1b66 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 24 May 2022 20:54:11 -0500 Subject: [PATCH 0864/3516] Bump Frontend to 20220524.0 (#72467) --- 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 f6ede0676ca..b5fb12b92f6 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220523.0"], + "requirements": ["home-assistant-frontend==20220524.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a281d3cb30d..7dd302fea28 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220523.0 +home-assistant-frontend==20220524.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 222067c8028..22f104e1c36 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -825,7 +825,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220523.0 +home-assistant-frontend==20220524.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 423122e079f..e2c1d0ab918 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -592,7 +592,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220523.0 +home-assistant-frontend==20220524.0 # homeassistant.components.home_connect homeconnect==0.7.0 From 38ad1ef2337638edc1f3c111adcc4dfc7e5995aa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 25 May 2022 04:00:36 +0200 Subject: [PATCH 0865/3516] Use My Home Assistant for OAuth2 redirect callbacks (#72449) Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/config_entry_oauth2_flow.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 2d45269a82c..d369b872eb9 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -37,6 +37,7 @@ DATA_IMPLEMENTATIONS = "oauth2_impl" DATA_PROVIDERS = "oauth2_providers" AUTH_CALLBACK_PATH = "/auth/external/callback" HEADER_FRONTEND_BASE = "HA-Frontend-Base" +MY_AUTH_CALLBACK_PATH = "https://my.home-assistant.io/redirect/oauth" CLOCK_OUT_OF_SYNC_MAX_SEC = 20 @@ -129,6 +130,9 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation): @property def redirect_uri(self) -> str: """Return the redirect uri.""" + if "my" in self.hass.config.components: + return MY_AUTH_CALLBACK_PATH + if (req := http.current_request.get()) is None: raise RuntimeError("No current request in context") From d0008683beb2a21d0ae0bf2ed47c59f8fb2a5530 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 25 May 2022 04:01:27 +0200 Subject: [PATCH 0866/3516] Use new project metadata format [PEP 621] (#72422) --- pyproject.toml | 41 ++++++++++++++++++++++++++++++++++++++++- setup.cfg | 36 +----------------------------------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 203c3ef1686..e0db5324d02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,46 @@ [build-system] -requires = ["setuptools~=60.5", "wheel~=0.37.1"] +requires = ["setuptools~=62.3", "wheel~=0.37.1"] build-backend = "setuptools.build_meta" +[project] +name = "homeassistant" +license = {text = "Apache-2.0"} +description = "Open-source home automation platform running on Python 3." +readme = "README.rst" +authors = [ + {name = "The Home Assistant Authors", email = "hello@home-assistant.io"} +] +keywords = ["home", "automation"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Home Automation", +] +dynamic = ["version", "requires-python", "dependencies"] + +[project.urls] +"Source Code" = "https://github.com/home-assistant/core" +"Bug Reports" = "https://github.com/home-assistant/core/issues" +"Docs: Dev" = "https://developers.home-assistant.io/" +"Discord" = "https://www.home-assistant.io/join-chat/" +"Forum" = "https://community.home-assistant.io/" + +[project.scripts] +hass = "homeassistant.__main__:main" + +[tool.setuptools] +platforms = ["any"] +zip-safe = false +include-package-data = true + +[tool.setuptools.packages.find] +include = ["homeassistant*"] + [tool.black] target-version = ["py39", "py310"] exclude = 'generated' diff --git a/setup.cfg b/setup.cfg index e8688293683..825a4407012 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,34 +1,8 @@ [metadata] -name = homeassistant -version = 2022.6.0.dev0 -author = The Home Assistant Authors -author_email = hello@home-assistant.io -license = Apache-2.0 -platforms = any -description = Open-source home automation platform running on Python 3. -long_description = file: README.rst -long_description_content_type = text/x-rst -keywords = home, automation +version = 2022.6.0.dev0 url = https://www.home-assistant.io/ -project_urls = - Source Code = https://github.com/home-assistant/core - Bug Reports = https://github.com/home-assistant/core/issues - Docs: Dev = https://developers.home-assistant.io/ - Discord = https://discordapp.com/invite/c5DvZ4e - Forum = https://community.home-assistant.io/ -classifier = - Development Status :: 4 - Beta - Intended Audience :: End Users/Desktop - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Operating System :: OS Independent - Programming Language :: Python :: 3.9 - Topic :: Home Automation [options] -packages = find: -zip_safe = False -include_package_data = True python_requires = >=3.9.0 install_requires = aiohttp==3.8.1 @@ -57,14 +31,6 @@ install_requires = voluptuous-serialize==2.5.0 yarl==1.7.2 -[options.packages.find] -include = - homeassistant* - -[options.entry_points] -console_scripts = - hass = homeassistant.__main__:main - [flake8] exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build max-complexity = 25 From 352b7e86afb96bbee4eb8edf42560a3beca1c363 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 24 May 2022 22:11:43 -0400 Subject: [PATCH 0867/3516] Move zwave_js node metadata comments to separate WS API cmd (#71513) * Move zwave_js node metadata comments to separate WS API cmd * fix pr --- homeassistant/components/zwave_js/api.py | 23 ++++++++++++++++++++++- tests/components/zwave_js/test_api.py | 21 ++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 15d3a68d4a4..a4fca557242 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -326,6 +326,7 @@ def async_register_api(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, websocket_network_status) websocket_api.async_register_command(hass, websocket_node_status) websocket_api.async_register_command(hass, websocket_node_metadata) + websocket_api.async_register_command(hass, websocket_node_comments) websocket_api.async_register_command(hass, websocket_add_node) websocket_api.async_register_command(hass, websocket_grant_security_classes) websocket_api.async_register_command(hass, websocket_validate_dsk_and_enter_pin) @@ -503,7 +504,6 @@ async def websocket_node_metadata( "wakeup": node.device_config.metadata.wakeup, "reset": node.device_config.metadata.reset, "device_database_url": node.device_database_url, - "comments": node.device_config.metadata.comments, } connection.send_result( msg[ID], @@ -511,6 +511,27 @@ async def websocket_node_metadata( ) +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/node_comments", + vol.Required(DEVICE_ID): str, + } +) +@websocket_api.async_response +@async_get_node +async def websocket_node_comments( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + node: Node, +) -> None: + """Get the comments of a Z-Wave JS node.""" + connection.send_result( + msg[ID], + {"comments": node.device_config.metadata.comments}, + ) + + @websocket_api.require_admin @websocket_api.websocket_command( { diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 58c26cd5797..2309e7b02bf 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -261,7 +261,6 @@ async def test_node_metadata(hass, wallmote_central_scene, integration, hass_ws_ result["device_database_url"] == "https://devices.zwave-js.io/?jumpTo=0x0086:0x0002:0x0082:0.0" ) - assert result["comments"] == [{"level": "info", "text": "test"}] # Test getting non-existent node fails await ws_client.send_json( @@ -292,6 +291,26 @@ async def test_node_metadata(hass, wallmote_central_scene, integration, hass_ws_ assert msg["error"]["code"] == ERR_NOT_LOADED +async def test_node_comments(hass, wallmote_central_scene, integration, hass_ws_client): + """Test the node comments websocket command.""" + ws_client = await hass_ws_client(hass) + + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_device({(DOMAIN, "3245146787-35")}) + assert device + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/node_comments", + DEVICE_ID: device.id, + } + ) + msg = await ws_client.receive_json() + result = msg["result"] + assert result["comments"] == [{"level": "info", "text": "test"}] + + async def test_add_node( hass, nortek_thermostat_added_event, integration, client, hass_ws_client ): From e4cd04f936214d67c4feee00a92c1670ebb385cd Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Wed, 25 May 2022 05:42:37 +0200 Subject: [PATCH 0868/3516] Use length_util for here_travel_time (#72458) --- homeassistant/components/here_travel_time/__init__.py | 4 +++- tests/components/here_travel_time/test_sensor.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index da091e0bdbf..2b9853c3b10 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_MODE, CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_METERS, Platform, ) from homeassistant.core import HomeAssistant @@ -22,6 +23,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.location import find_coordinates from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt +from homeassistant.util.unit_system import IMPERIAL_SYSTEM from .const import ( ATTR_DESTINATION, @@ -178,7 +180,7 @@ class HereTravelTimeDataUpdateCoordinator(DataUpdateCoordinator): traffic_time = summary["trafficTime"] if self.config.units == CONF_UNIT_SYSTEM_IMPERIAL: # Convert to miles. - distance = distance / 1609.344 + distance = IMPERIAL_SYSTEM.length(distance, LENGTH_METERS) else: # Convert to kilometers distance = distance / 1000 diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 9a15f14f53e..dc9ba128c35 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -99,7 +99,7 @@ from tests.common import MockConfigEntry None, None, "30", - 14.852635608048994, + 14.852631013, 30.05, ), ( @@ -110,7 +110,7 @@ from tests.common import MockConfigEntry "08:00:00", None, "30", - 14.852635608048994, + 14.852631013, 30.05, ), ( From b8e9b8f540cad8a6d346eebba4ff7bb6230728ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 May 2022 17:45:27 -1000 Subject: [PATCH 0869/3516] Add number platform to Big Ass Fans (#72435) --- .coveragerc | 1 + homeassistant/components/baf/__init__.py | 1 + homeassistant/components/baf/number.py | 151 +++++++++++++++++++++++ 3 files changed, 153 insertions(+) create mode 100644 homeassistant/components/baf/number.py diff --git a/.coveragerc b/.coveragerc index ca6c57fb341..4e903d68aa5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -98,6 +98,7 @@ omit = homeassistant/components/baf/entity.py homeassistant/components/baf/fan.py homeassistant/components/baf/light.py + homeassistant/components/baf/number.py homeassistant/components/baf/sensor.py homeassistant/components/baf/switch.py homeassistant/components/baidu/tts.py diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 7127228383a..601215d4c61 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -18,6 +18,7 @@ PLATFORMS: list[Platform] = [ Platform.CLIMATE, Platform.FAN, Platform.LIGHT, + Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/baf/number.py b/homeassistant/components/baf/number.py new file mode 100644 index 00000000000..84358e79669 --- /dev/null +++ b/homeassistant/components/baf/number.py @@ -0,0 +1,151 @@ +"""Support for Big Ass Fans number.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Optional, cast + +from aiobafi6 import Device + +from homeassistant import config_entries +from homeassistant.components.number import ( + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.const import TIME_SECONDS +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, HALF_DAY_SECS, ONE_DAY_SECS, ONE_MIN_SECS, SPEED_RANGE +from .entity import BAFEntity +from .models import BAFData + + +@dataclass +class BAFNumberDescriptionMixin: + """Required values for BAF sensors.""" + + value_fn: Callable[[Device], int | None] + mode: NumberMode + + +@dataclass +class BAFNumberDescription(NumberEntityDescription, BAFNumberDescriptionMixin): + """Class describing BAF sensor entities.""" + + +FAN_NUMBER_DESCRIPTIONS = ( + BAFNumberDescription( + key="return_to_auto_timeout", + name="Return to Auto Timeout", + min_value=ONE_MIN_SECS, + max_value=HALF_DAY_SECS, + entity_category=EntityCategory.CONFIG, + unit_of_measurement=TIME_SECONDS, + value_fn=lambda device: cast(Optional[int], device.return_to_auto_timeout), + mode=NumberMode.SLIDER, + ), + BAFNumberDescription( + key="motion_sense_timeout", + name="Motion Sense Timeout", + min_value=ONE_MIN_SECS, + max_value=ONE_DAY_SECS, + entity_category=EntityCategory.CONFIG, + unit_of_measurement=TIME_SECONDS, + value_fn=lambda device: cast(Optional[int], device.motion_sense_timeout), + mode=NumberMode.SLIDER, + ), + BAFNumberDescription( + key="comfort_min_speed", + name="Auto Comfort Minimum Speed", + min_value=0, + max_value=SPEED_RANGE[1] - 1, + entity_category=EntityCategory.CONFIG, + value_fn=lambda device: cast(Optional[int], device.comfort_min_speed), + mode=NumberMode.BOX, + ), + BAFNumberDescription( + key="comfort_max_speed", + name="Auto Comfort Maximum Speed", + min_value=1, + max_value=SPEED_RANGE[1], + entity_category=EntityCategory.CONFIG, + value_fn=lambda device: cast(Optional[int], device.comfort_max_speed), + mode=NumberMode.BOX, + ), + BAFNumberDescription( + key="comfort_heat_assist_speed", + name="Auto Comfort Heat Assist Speed", + min_value=SPEED_RANGE[0], + max_value=SPEED_RANGE[1], + entity_category=EntityCategory.CONFIG, + value_fn=lambda device: cast(Optional[int], device.comfort_heat_assist_speed), + mode=NumberMode.BOX, + ), +) + +LIGHT_NUMBER_DESCRIPTIONS = ( + BAFNumberDescription( + key="light_return_to_auto_timeout", + name="Light Return to Auto Timeout", + min_value=ONE_MIN_SECS, + max_value=HALF_DAY_SECS, + entity_category=EntityCategory.CONFIG, + unit_of_measurement=TIME_SECONDS, + value_fn=lambda device: cast( + Optional[int], device.light_return_to_auto_timeout + ), + mode=NumberMode.SLIDER, + ), + BAFNumberDescription( + key="light_auto_motion_timeout", + name="Light Motion Sense Timeout", + min_value=ONE_MIN_SECS, + max_value=ONE_DAY_SECS, + entity_category=EntityCategory.CONFIG, + unit_of_measurement=TIME_SECONDS, + value_fn=lambda device: cast(Optional[int], device.light_auto_motion_timeout), + mode=NumberMode.SLIDER, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up BAF numbers.""" + data: BAFData = hass.data[DOMAIN][entry.entry_id] + device = data.device + descriptions: list[BAFNumberDescription] = [] + if device.has_fan: + descriptions.extend(FAN_NUMBER_DESCRIPTIONS) + if device.has_light: + descriptions.extend(LIGHT_NUMBER_DESCRIPTIONS) + async_add_entities(BAFNumber(device, description) for description in descriptions) + + +class BAFNumber(BAFEntity, NumberEntity): + """BAF number.""" + + entity_description: BAFNumberDescription + + def __init__(self, device: Device, description: BAFNumberDescription) -> None: + """Initialize the entity.""" + self.entity_description = description + super().__init__(device, f"{device.name} {description.name}") + self._attr_unique_id = f"{self._device.mac_address}-{description.key}" + self._attr_mode = description.mode + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + if (value := self.entity_description.value_fn(self._device)) is not None: + self._attr_value = float(value) + + async def async_set_value(self, value: float) -> None: + """Set the value.""" + setattr(self._device, self.entity_description.key, int(value)) From fbeaf200e4d18ed2816913d915beb14583acf18f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 24 May 2022 22:46:27 -0500 Subject: [PATCH 0870/3516] Handle Plex searches in URL media_content_id format (#72462) --- homeassistant/components/plex/services.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index b7eff8043f8..0847583635d 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -123,8 +123,10 @@ def process_plex_payload( plex_url = URL(content_id) if plex_url.name: if len(plex_url.parts) == 2: - # The path contains a single item, will always be a ratingKey - content = int(plex_url.name) + if plex_url.name == "search": + content = {} + else: + content = int(plex_url.name) else: # For "special" items like radio stations content = plex_url.path @@ -132,7 +134,10 @@ def process_plex_payload( plex_server = get_plex_server(hass, plex_server_id=server_id) else: # Handle legacy payloads without server_id in URL host position - content = int(plex_url.host) # type: ignore[arg-type] + if plex_url.host == "search": + content = {} + else: + content = int(plex_url.host) # type: ignore[arg-type] extra_params = dict(plex_url.query) else: content = json.loads(content_id) From e60b247b515f391d7f13768544b344cb0cfd9948 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 25 May 2022 05:48:09 +0200 Subject: [PATCH 0871/3516] Simplify setup of deCONZ platforms (#72453) --- .../components/deconz/alarm_control_panel.py | 11 ++-- .../components/deconz/binary_sensor.py | 10 ++-- homeassistant/components/deconz/button.py | 11 ++-- homeassistant/components/deconz/climate.py | 10 ++-- homeassistant/components/deconz/cover.py | 10 ++-- .../components/deconz/deconz_event.py | 23 +++----- homeassistant/components/deconz/fan.py | 10 ++-- homeassistant/components/deconz/gateway.py | 35 +++++++++--- homeassistant/components/deconz/light.py | 20 +++---- homeassistant/components/deconz/lock.py | 20 +++---- homeassistant/components/deconz/number.py | 10 ++-- homeassistant/components/deconz/scene.py | 11 ++-- homeassistant/components/deconz/sensor.py | 10 ++-- homeassistant/components/deconz/siren.py | 10 ++-- homeassistant/components/deconz/switch.py | 10 ++-- tests/components/deconz/test_binary_sensor.py | 53 ++++++++++++++++++- 16 files changed, 130 insertions(+), 134 deletions(-) diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py index e635b11972e..90c34da0f12 100644 --- a/homeassistant/components/deconz/alarm_control_panel.py +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -83,16 +83,11 @@ async def async_setup_entry( [DeconzAlarmControlPanel(sensor, gateway, alarm_system_id)] ) - config_entry.async_on_unload( - gateway.api.sensors.ancillary_control.subscribe( - gateway.evaluate_add_device(async_add_sensor), - EventType.ADDED, - ) + gateway.register_platform_add_device_callback( + async_add_sensor, + gateway.api.sensors.ancillary_control, ) - for sensor_id in gateway.api.sensors.ancillary_control: - async_add_sensor(EventType.ADDED, sensor_id) - class DeconzAlarmControlPanel(DeconzDevice, AlarmControlPanelEntity): """Representation of a deCONZ alarm control panel.""" diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index e57f0bde3a6..d109fb8b34d 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -207,14 +207,10 @@ async def async_setup_entry( async_add_entities([DeconzBinarySensor(sensor, gateway, description)]) - config_entry.async_on_unload( - gateway.api.sensors.subscribe( - gateway.evaluate_add_device(async_add_sensor), - EventType.ADDED, - ) + gateway.register_platform_add_device_callback( + async_add_sensor, + gateway.api.sensors, ) - for sensor_id in gateway.api.sensors: - async_add_sensor(EventType.ADDED, sensor_id) @callback def async_reload_clip_sensors() -> None: diff --git a/homeassistant/components/deconz/button.py b/homeassistant/components/deconz/button.py index ed61750af6f..498c88a2351 100644 --- a/homeassistant/components/deconz/button.py +++ b/homeassistant/components/deconz/button.py @@ -65,16 +65,11 @@ async def async_setup_entry( for description in ENTITY_DESCRIPTIONS.get(PydeconzScene, []) ) - config_entry.async_on_unload( - gateway.api.scenes.subscribe( - async_add_scene, - EventType.ADDED, - ) + gateway.register_platform_add_device_callback( + async_add_scene, + gateway.api.scenes, ) - for scene_id in gateway.api.scenes: - async_add_scene(EventType.ADDED, scene_id) - class DeconzButton(DeconzSceneMixin, ButtonEntity): """Representation of a deCONZ button entity.""" diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 1d57288d275..6887b4238d2 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -104,14 +104,10 @@ async def async_setup_entry( return async_add_entities([DeconzThermostat(climate, gateway)]) - config_entry.async_on_unload( - gateway.api.sensors.thermostat.subscribe( - gateway.evaluate_add_device(async_add_climate), - EventType.ADDED, - ) + gateway.register_platform_add_device_callback( + async_add_climate, + gateway.api.sensors.thermostat, ) - for climate_id in gateway.api.sensors.thermostat: - async_add_climate(EventType.ADDED, climate_id) @callback def async_reload_clip_sensors() -> None: diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 1931ac78389..9efbeac366f 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -43,14 +43,10 @@ async def async_setup_entry( cover = gateway.api.lights.covers[cover_id] async_add_entities([DeconzCover(cover, gateway)]) - config_entry.async_on_unload( - gateway.api.lights.covers.subscribe( - gateway.evaluate_add_device(async_add_cover), - EventType.ADDED, - ) + gateway.register_platform_add_device_callback( + async_add_cover, + gateway.api.lights.covers, ) - for cover_id in gateway.api.lights.covers: - async_add_cover(EventType.ADDED, cover_id) class DeconzCover(DeconzDevice, CoverEntity): diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index cfd60a63ec9..fa53ef1b5bc 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -58,29 +58,18 @@ async def async_setup_events(gateway: DeconzGateway) -> None: elif isinstance(sensor, AncillaryControl): new_event = DeconzAlarmEvent(sensor, gateway) - else: - return None - gateway.hass.async_create_task(new_event.async_update_device_registry()) gateway.events.append(new_event) - gateway.config_entry.async_on_unload( - gateway.api.sensors.ancillary_control.subscribe( - gateway.evaluate_add_device(async_add_sensor), - EventType.ADDED, - ) + gateway.register_platform_add_device_callback( + async_add_sensor, + gateway.api.sensors.switch, ) - - gateway.config_entry.async_on_unload( - gateway.api.sensors.switch.subscribe( - gateway.evaluate_add_device(async_add_sensor), - EventType.ADDED, - ) + gateway.register_platform_add_device_callback( + async_add_sensor, + gateway.api.sensors.ancillary_control, ) - for sensor_id in gateway.api.sensors: - async_add_sensor(EventType.ADDED, sensor_id) - @callback def async_unload_events(gateway: DeconzGateway) -> None: diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index b0d6d7755a2..ab9a1ba6f4a 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -48,14 +48,10 @@ async def async_setup_entry( fan = gateway.api.lights.fans[fan_id] async_add_entities([DeconzFan(fan, gateway)]) - config_entry.async_on_unload( - gateway.api.lights.fans.subscribe( - gateway.evaluate_add_device(async_add_fan), - EventType.ADDED, - ) + gateway.register_platform_add_device_callback( + async_add_fan, + gateway.api.lights.fans, ) - for fan_id in gateway.api.lights.fans: - async_add_fan(EventType.ADDED, fan_id) class DeconzFan(DeconzDevice, FanEntity): diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 1f6a45f3ad6..5890e372e66 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any, cast import async_timeout from pydeconz import DeconzSession, errors +from pydeconz.interfaces.api import APIItems, GroupedAPIItems from pydeconz.models.event import EventType from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry @@ -105,9 +106,11 @@ class DeconzGateway: ) @callback - def evaluate_add_device( - self, add_device_callback: Callable[[EventType, str], None] - ) -> Callable[[EventType, str], None]: + def register_platform_add_device_callback( + self, + add_device_callback: Callable[[EventType, str], None], + deconz_device_interface: APIItems | GroupedAPIItems, + ) -> None: """Wrap add_device_callback to check allow_new_devices option.""" def async_add_device(event: EventType, device_id: str) -> None: @@ -117,11 +120,19 @@ class DeconzGateway: Device_refresh is expected to load new devices. """ if not self.option_allow_new_devices and not self.ignore_state_updates: - self.ignored_devices.add((add_device_callback, device_id)) + self.ignored_devices.add((async_add_device, device_id)) return add_device_callback(event, device_id) - return async_add_device + self.config_entry.async_on_unload( + deconz_device_interface.subscribe( + async_add_device, + EventType.ADDED, + ) + ) + + for device_id in deconz_device_interface: + add_device_callback(EventType.ADDED, device_id) @callback def load_ignored_devices(self) -> None: @@ -191,6 +202,8 @@ class DeconzGateway: """Manage entities affected by config entry options.""" deconz_ids = [] + # Allow CLIP sensors + if self.option_allow_clip_sensor: async_dispatcher_send(self.hass, self.signal_reload_clip_sensors) @@ -201,6 +214,8 @@ class DeconzGateway: if sensor.type.startswith("CLIP") ] + # Allow Groups + if self.option_allow_deconz_groups: if not self._option_allow_deconz_groups: async_dispatcher_send(self.hass, self.signal_reload_groups) @@ -209,13 +224,17 @@ class DeconzGateway: self._option_allow_deconz_groups = self.option_allow_deconz_groups + # Allow adding new devices + option_allow_new_devices = self.config_entry.options.get( CONF_ALLOW_NEW_DEVICES, DEFAULT_ALLOW_NEW_DEVICES ) - if option_allow_new_devices and not self.option_allow_new_devices: - self.load_ignored_devices() + if option_allow_new_devices != self.option_allow_new_devices: + self.option_allow_new_devices = option_allow_new_devices + if option_allow_new_devices: + self.load_ignored_devices() - self.option_allow_new_devices = option_allow_new_devices + # Remove entities based on above categories entity_registry = er.async_get(self.hass) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 0f23ff02831..53773369176 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -93,23 +93,15 @@ async def async_setup_entry( async_add_entities([DeconzLight(light, gateway)]) - config_entry.async_on_unload( - gateway.api.lights.lights.subscribe( - gateway.evaluate_add_device(async_add_light), - EventType.ADDED, - ) + gateway.register_platform_add_device_callback( + async_add_light, + gateway.api.lights.lights, ) - for light_id in gateway.api.lights.lights: - async_add_light(EventType.ADDED, light_id) - config_entry.async_on_unload( - gateway.api.lights.fans.subscribe( - async_add_light, - EventType.ADDED, - ) + gateway.register_platform_add_device_callback( + async_add_light, + gateway.api.lights.fans, ) - for light_id in gateway.api.lights.fans: - async_add_light(EventType.ADDED, light_id) @callback def async_add_group(_: EventType, group_id: str) -> None: diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index dc3da3cbe8c..78ccae30441 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -32,14 +32,10 @@ async def async_setup_entry( lock = gateway.api.lights.locks[lock_id] async_add_entities([DeconzLock(lock, gateway)]) - config_entry.async_on_unload( - gateway.api.lights.locks.subscribe( - gateway.evaluate_add_device(async_add_lock_from_light), - EventType.ADDED, - ) + gateway.register_platform_add_device_callback( + async_add_lock_from_light, + gateway.api.lights.locks, ) - for lock_id in gateway.api.lights.locks: - async_add_lock_from_light(EventType.ADDED, lock_id) @callback def async_add_lock_from_sensor(_: EventType, lock_id: str) -> None: @@ -47,14 +43,10 @@ async def async_setup_entry( lock = gateway.api.sensors.door_lock[lock_id] async_add_entities([DeconzLock(lock, gateway)]) - config_entry.async_on_unload( - gateway.api.sensors.door_lock.subscribe( - gateway.evaluate_add_device(async_add_lock_from_sensor), - EventType.ADDED, - ) + gateway.register_platform_add_device_callback( + async_add_lock_from_sensor, + gateway.api.sensors.door_lock, ) - for lock_id in gateway.api.sensors.door_lock: - async_add_lock_from_sensor(EventType.ADDED, lock_id) class DeconzLock(DeconzDevice, LockEntity): diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index 5d555797372..a7bb014d76a 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -75,14 +75,10 @@ async def async_setup_entry( continue async_add_entities([DeconzNumber(sensor, gateway, description)]) - config_entry.async_on_unload( - gateway.api.sensors.presence.subscribe( - gateway.evaluate_add_device(async_add_sensor), - EventType.ADDED, - ) + gateway.register_platform_add_device_callback( + async_add_sensor, + gateway.api.sensors.presence, ) - for sensor_id in gateway.api.sensors.presence: - async_add_sensor(EventType.ADDED, sensor_id) class DeconzNumber(DeconzDevice, NumberEntity): diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index 28448da4f75..dfbb6ae828b 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -30,16 +30,11 @@ async def async_setup_entry( scene = gateway.api.scenes[scene_id] async_add_entities([DeconzScene(scene, gateway)]) - config_entry.async_on_unload( - gateway.api.scenes.subscribe( - async_add_scene, - EventType.ADDED, - ) + gateway.register_platform_add_device_callback( + async_add_scene, + gateway.api.scenes, ) - for scene_id in gateway.api.scenes: - async_add_scene(EventType.ADDED, scene_id) - class DeconzScene(DeconzSceneMixin, Scene): """Representation of a deCONZ scene.""" diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 021dcf7168a..663aadde73f 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -264,14 +264,10 @@ async def async_setup_entry( async_add_entities([DeconzSensor(sensor, gateway, description)]) - config_entry.async_on_unload( - gateway.api.sensors.subscribe( - gateway.evaluate_add_device(async_add_sensor), - EventType.ADDED, - ) + gateway.register_platform_add_device_callback( + async_add_sensor, + gateway.api.sensors, ) - for sensor_id in gateway.api.sensors: - async_add_sensor(EventType.ADDED, sensor_id) @callback def async_reload_clip_sensors() -> None: diff --git a/homeassistant/components/deconz/siren.py b/homeassistant/components/deconz/siren.py index 0c5b3010626..8427b6ce75d 100644 --- a/homeassistant/components/deconz/siren.py +++ b/homeassistant/components/deconz/siren.py @@ -35,14 +35,10 @@ async def async_setup_entry( siren = gateway.api.lights.sirens[siren_id] async_add_entities([DeconzSiren(siren, gateway)]) - config_entry.async_on_unload( - gateway.api.lights.sirens.subscribe( - gateway.evaluate_add_device(async_add_siren), - EventType.ADDED, - ) + gateway.register_platform_add_device_callback( + async_add_siren, + gateway.api.lights.sirens, ) - for siren_id in gateway.api.lights.sirens: - async_add_siren(EventType.ADDED, siren_id) class DeconzSiren(DeconzDevice, SirenEntity): diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index d6e12365407..d54ff1f36ba 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -37,14 +37,10 @@ async def async_setup_entry( return async_add_entities([DeconzPowerPlug(switch, gateway)]) - config_entry.async_on_unload( - gateway.api.lights.lights.subscribe( - gateway.evaluate_add_device(async_add_switch), - EventType.ADDED, - ) + gateway.register_platform_add_device_callback( + async_add_switch, + gateway.api.lights.lights, ) - for switch_id in gateway.api.lights.lights: - async_add_switch(EventType.ADDED, switch_id) class DeconzPowerPlug(DeconzDevice, SwitchEntity): diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 8d3eb105896..b6a21fb9fa6 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -636,7 +636,7 @@ async def test_add_new_binary_sensor(hass, aioclient_mock, mock_deconz_websocket assert hass.states.get("binary_sensor.presence_sensor").state == STATE_OFF -async def test_add_new_binary_sensor_ignored( +async def test_add_new_binary_sensor_ignored_load_entities_on_service_call( hass, aioclient_mock, mock_deconz_websocket ): """Test that adding a new binary sensor is not allowed.""" @@ -683,3 +683,54 @@ async def test_add_new_binary_sensor_ignored( assert len(hass.states.async_all()) == 1 assert hass.states.get("binary_sensor.presence_sensor") + + +async def test_add_new_binary_sensor_ignored_load_entities_on_options_change( + hass, aioclient_mock, mock_deconz_websocket +): + """Test that adding a new binary sensor is not allowed.""" + sensor = { + "name": "Presence sensor", + "type": "ZHAPresence", + "state": {"presence": False}, + "config": {"on": True, "reachable": True}, + "uniqueid": "00:00:00:00:00:00:00:00-00", + } + event_added_sensor = { + "t": "event", + "e": "added", + "r": "sensors", + "id": "1", + "sensor": sensor, + } + + config_entry = await setup_deconz_integration( + hass, + aioclient_mock, + options={CONF_MASTER_GATEWAY: True, CONF_ALLOW_NEW_DEVICES: False}, + ) + + assert len(hass.states.async_all()) == 0 + + await mock_deconz_websocket(data=event_added_sensor) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + assert not hass.states.get("binary_sensor.presence_sensor") + + entity_registry = er.async_get(hass) + assert ( + len(async_entries_for_config_entry(entity_registry, config_entry.entry_id)) == 0 + ) + + aioclient_mock.clear_requests() + data = {"config": {}, "groups": {}, "lights": {}, "sensors": {"1": sensor}} + mock_deconz_request(aioclient_mock, config_entry.data, data) + + hass.config_entries.async_update_entry( + config_entry, options={CONF_ALLOW_NEW_DEVICES: True} + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + assert hass.states.get("binary_sensor.presence_sensor") From 76146cf57cb4211cb25f28a61d300c780104d943 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 25 May 2022 05:51:02 +0200 Subject: [PATCH 0872/3516] Fix deCONZ does not generate unique IDs for battery sensors (#72455) --- homeassistant/components/deconz/sensor.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 663aadde73f..2aff2b12448 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -246,13 +246,16 @@ async def async_setup_entry( def async_add_sensor(_: EventType, sensor_id: str) -> None: """Add sensor from deCONZ.""" sensor = gateway.api.sensors[sensor_id] + entities: list[DeconzSensor] = [] if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): return - if sensor.battery is None: + if sensor.battery is None and not sensor.type.startswith("CLIP"): DeconzBatteryTracker(sensor_id, gateway, async_add_entities) + known_entities = set(gateway.entities[DOMAIN]) + for description in ( ENTITY_DESCRIPTIONS.get(type(sensor), []) + SENSOR_DESCRIPTIONS ): @@ -262,7 +265,11 @@ async def async_setup_entry( ): continue - async_add_entities([DeconzSensor(sensor, gateway, description)]) + entity = DeconzSensor(sensor, gateway, description) + if entity.unique_id not in known_entities: + entities.append(entity) + + async_add_entities(entities) gateway.register_platform_add_device_callback( async_add_sensor, @@ -403,6 +410,7 @@ class DeconzBatteryTracker: """Update the device's state.""" if "battery" in self.sensor.changed_keys: self.unsub() - self.async_add_entities( - [DeconzSensor(self.sensor, self.gateway, SENSOR_DESCRIPTIONS[0])] - ) + known_entities = set(self.gateway.entities[DOMAIN]) + entity = DeconzSensor(self.sensor, self.gateway, SENSOR_DESCRIPTIONS[0]) + if entity.unique_id not in known_entities: + self.async_add_entities([entity]) From 209f37196e779d96da8451a31d712313e3ae29e2 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Wed, 25 May 2022 05:51:19 +0200 Subject: [PATCH 0873/3516] Adjust path to version info in Github issue template (#72431) --- .github/ISSUE_TEMPLATE.md | 2 +- .github/ISSUE_TEMPLATE/bug_report.yml | 4 ++-- homeassistant/components/motioneye/__init__.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 5a64b0d5b73..2783972953b 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -16,7 +16,7 @@ - Home Assistant Core release with the issue: diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 9667775e8e9..6b13f0980b1 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -31,7 +31,7 @@ body: label: What version of Home Assistant Core has the issue? placeholder: core- description: > - Can be found in: [Configuration panel -> Info](https://my.home-assistant.io/redirect/info/). + Can be found in: [Settings -> About](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 @@ -46,7 +46,7 @@ body: attributes: label: What type of installation are you running? description: > - Can be found in: [Configuration panel -> Info](https://my.home-assistant.io/redirect/info/). + Can be found in: [Settings -> About](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: diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 37a15931920..279cc8bde70 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -177,7 +177,7 @@ def async_generate_motioneye_webhook( except NoURLAvailableError: _LOGGER.warning( "Unable to get Home Assistant URL. Have you set the internal and/or " - "external URLs in Configuration -> General?" + "external URLs in Settings -> System -> Network?" ) return None From eb7a521232791f6728079d5583dfad8b651ef1a1 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 25 May 2022 01:50:25 -0400 Subject: [PATCH 0874/3516] Fix more typing for zwave_js (#72472) * Fix more typing for zwave_js * Revert one change * reduce lines * Fix tests --- .../components/zwave_js/device_action.py | 9 ++- .../zwave_js/device_automation_helpers.py | 2 + .../components/zwave_js/device_trigger.py | 2 +- .../components/zwave_js/diagnostics.py | 55 +++++++++++++------ .../zwave_js/discovery_data_template.py | 6 +- homeassistant/components/zwave_js/helpers.py | 11 +--- homeassistant/components/zwave_js/services.py | 21 ++++--- .../components/zwave_js/triggers/event.py | 34 +++++++----- .../zwave_js/triggers/value_updated.py | 3 +- tests/components/zwave_js/test_diagnostics.py | 11 +++- tests/components/zwave_js/test_helpers.py | 10 ---- 11 files changed, 97 insertions(+), 67 deletions(-) delete mode 100644 tests/components/zwave_js/test_helpers.py diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index f001accd196..2494cb8216d 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -146,7 +146,7 @@ async def async_get_actions( ) -> list[dict[str, Any]]: """List device actions for Z-Wave JS devices.""" registry = entity_registry.async_get(hass) - actions = [] + actions: list[dict] = [] node = async_get_node_from_device_id(hass, device_id) @@ -207,10 +207,13 @@ async def async_get_actions( # If the value has the meterType CC specific value, we can add a reset_meter # action for it if CC_SPECIFIC_METER_TYPE in value.metadata.cc_specific: - meter_endpoints[value.endpoint].setdefault( + endpoint_idx = value.endpoint + if endpoint_idx is None: + endpoint_idx = 0 + meter_endpoints[endpoint_idx].setdefault( CONF_ENTITY_ID, entry.entity_id ) - meter_endpoints[value.endpoint].setdefault(ATTR_METER_TYPE, set()).add( + meter_endpoints[endpoint_idx].setdefault(ATTR_METER_TYPE, set()).add( get_meter_type(value) ) diff --git a/homeassistant/components/zwave_js/device_automation_helpers.py b/homeassistant/components/zwave_js/device_automation_helpers.py index f17ddccf03c..25cce978df1 100644 --- a/homeassistant/components/zwave_js/device_automation_helpers.py +++ b/homeassistant/components/zwave_js/device_automation_helpers.py @@ -44,6 +44,8 @@ def generate_config_parameter_subtype(config_value: ConfigurationValue) -> str: """Generate the config parameter name used in a device automation subtype.""" parameter = str(config_value.property_) if config_value.property_key: + # Property keys for config values are always an int + assert isinstance(config_value.property_key, int) parameter = f"{parameter}[{hex(config_value.property_key)}]" return f"{parameter} ({config_value.property_name})" diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py index 75a9647a5ca..ad860089d1d 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -254,7 +254,7 @@ async def async_get_triggers( dev_reg = device_registry.async_get(hass) node = async_get_node_from_device_id(hass, device_id, dev_reg) - triggers = [] + triggers: list[dict] = [] base_trigger = { CONF_PLATFORM: "device", CONF_DEVICE_ID: device_id, diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index dfb6661b5c0..4e1abe37b1b 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -1,7 +1,8 @@ """Provides diagnostics for Z-Wave JS.""" from __future__ import annotations -from dataclasses import astuple +from copy import deepcopy +from dataclasses import astuple, dataclass from typing import Any from zwave_js_server.client import Client @@ -20,25 +21,43 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DATA_CLIENT, DOMAIN from .helpers import ( - ZwaveValueID, get_home_and_node_id_from_device_entry, get_state_key_from_unique_id, get_value_id_from_unique_id, ) + +@dataclass +class ZwaveValueMatcher: + """Class to allow matching a Z-Wave Value.""" + + property_: str | int | None = None + command_class: int | None = None + endpoint: int | None = None + property_key: str | int | None = None + + def __post_init__(self) -> None: + """Post initialization check.""" + if all(val is None for val in astuple(self)): + raise ValueError("At least one of the fields must be set.") + + KEYS_TO_REDACT = {"homeId", "location"} VALUES_TO_REDACT = ( - ZwaveValueID(property_="userCode", command_class=CommandClass.USER_CODE), + ZwaveValueMatcher(property_="userCode", command_class=CommandClass.USER_CODE), ) def redact_value_of_zwave_value(zwave_value: ValueDataType) -> ValueDataType: """Redact value of a Z-Wave value.""" for value_to_redact in VALUES_TO_REDACT: - zwave_value_id = ZwaveValueID( - property_=zwave_value["property"], - command_class=CommandClass(zwave_value["commandClass"]), + command_class = None + if "commandClass" in zwave_value: + command_class = CommandClass(zwave_value["commandClass"]) + zwave_value_id = ZwaveValueMatcher( + property_=zwave_value.get("property"), + command_class=command_class, endpoint=zwave_value.get("endpoint"), property_key=zwave_value.get("propertyKey"), ) @@ -48,19 +67,19 @@ def redact_value_of_zwave_value(zwave_value: ValueDataType) -> ValueDataType: astuple(value_to_redact), astuple(zwave_value_id) ) ): - return {**zwave_value, "value": REDACTED} + redacted_value: ValueDataType = deepcopy(zwave_value) + redacted_value["value"] = REDACTED + return redacted_value return zwave_value def redact_node_state(node_state: NodeDataType) -> NodeDataType: """Redact node state.""" - return { - **node_state, - "values": [ - redact_value_of_zwave_value(zwave_value) - for zwave_value in node_state["values"] - ], - } + redacted_state: NodeDataType = deepcopy(node_state) + redacted_state["values"] = [ + redact_value_of_zwave_value(zwave_value) for zwave_value in node_state["values"] + ] + return redacted_state def get_device_entities( @@ -125,15 +144,17 @@ async def async_get_config_entry_diagnostics( async def async_get_device_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry -) -> NodeDataType: +) -> dict: """Return diagnostics for a device.""" client: Client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] identifiers = get_home_and_node_id_from_device_entry(device) node_id = identifiers[1] if identifiers else None - if node_id is None or node_id not in client.driver.controller.nodes: + assert (driver := client.driver) + if node_id is None or node_id not in driver.controller.nodes: raise ValueError(f"Node for device {device.id} can't be found") - node = client.driver.controller.nodes[node_id] + node = driver.controller.nodes[node_id] entities = get_device_entities(hass, node, device) + assert client.version return { "versionInfo": { "driverVersion": client.version.driver_version, diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 9dc90a43f3d..512cfafa63a 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -406,7 +406,7 @@ class TiltValueMix: class CoverTiltDataTemplate(BaseDiscoverySchemaDataTemplate, TiltValueMix): """Tilt data template class for Z-Wave Cover entities.""" - def resolve_data(self, value: ZwaveValue) -> dict[str, Any]: + def resolve_data(self, value: ZwaveValue) -> dict[str, ZwaveValue | None]: """Resolve helper class data for a discovered value.""" return {"tilt_value": self._get_value_from_id(value.node, self.tilt_value_id)} @@ -415,7 +415,9 @@ class CoverTiltDataTemplate(BaseDiscoverySchemaDataTemplate, TiltValueMix): return [resolved_data["tilt_value"]] @staticmethod - def current_tilt_value(resolved_data: dict[str, Any]) -> ZwaveValue | None: + def current_tilt_value( + resolved_data: dict[str, ZwaveValue | None] + ) -> ZwaveValue | None: """Get current tilt ZwaveValue from resolved data.""" return resolved_data["tilt_value"] diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 807ae0287eb..c047a3a9903 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Callable -from dataclasses import astuple, dataclass +from dataclasses import dataclass import logging from typing import Any, cast @@ -48,16 +48,11 @@ from .const import ( class ZwaveValueID: """Class to represent a value ID.""" - property_: str | int | None = None - command_class: int | None = None + property_: str | int + command_class: int endpoint: int | None = None property_key: str | int | None = None - def __post_init__(self) -> None: - """Post initialization check.""" - if all(val is None for val in astuple(self)): - raise ValueError("At least one of the fields must be set.") - @callback def get_value_id_from_unique_id(unique_id: str) -> str | None: diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index af8f3c4c4e7..3b56e0a073c 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -400,7 +400,7 @@ class ZWaveServices: async def async_set_config_parameter(self, service: ServiceCall) -> None: """Set a config value on a node.""" - nodes = service.data[const.ATTR_NODES] + nodes: set[ZwaveNode] = service.data[const.ATTR_NODES] property_or_property_name = service.data[const.ATTR_CONFIG_PARAMETER] property_key = service.data.get(const.ATTR_CONFIG_PARAMETER_BITMASK) new_value = service.data[const.ATTR_CONFIG_VALUE] @@ -434,7 +434,7 @@ class ZWaveServices: self, service: ServiceCall ) -> None: """Bulk set multiple partial config values on a node.""" - nodes = service.data[const.ATTR_NODES] + nodes: set[ZwaveNode] = service.data[const.ATTR_NODES] property_ = service.data[const.ATTR_CONFIG_PARAMETER] new_value = service.data[const.ATTR_CONFIG_VALUE] @@ -531,7 +531,7 @@ class ZWaveServices: async def async_multicast_set_value(self, service: ServiceCall) -> None: """Set a value via multicast to multiple nodes.""" - nodes = service.data[const.ATTR_NODES] + nodes: set[ZwaveNode] = service.data[const.ATTR_NODES] broadcast: bool = service.data[const.ATTR_BROADCAST] options = service.data.get(const.ATTR_OPTIONS) @@ -559,13 +559,15 @@ class ZWaveServices: # If there are no nodes, we can assume there is only one config entry due to # 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: ZwaveNode = next((node for node in nodes), None) - if first_node: + client: ZwaveClient + first_node: ZwaveNode + try: + first_node = next(node for node in nodes) client = first_node.client - else: + except StopIteration: entry_id = self._hass.config_entries.async_entries(const.DOMAIN)[0].entry_id client = self._hass.data[const.DOMAIN][entry_id][const.DATA_CLIENT] + assert client.driver first_node = next( node for node in client.driver.controller.nodes.values() @@ -688,6 +690,9 @@ class ZWaveServices: _LOGGER.warning("Skipping entity %s as it has no value ID", entity_id) continue - endpoints.add(node.endpoints[node.values[value_id].endpoint]) + endpoint_idx = node.values[value_id].endpoint + endpoints.add( + node.endpoints[endpoint_idx if endpoint_idx is not None else 0] + ) await _async_invoke_cc_api(endpoints) diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py index 83fd7570ab9..94afd1e9117 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -39,12 +39,6 @@ from homeassistant.helpers.typing import ConfigType # Platform type should be . PLATFORM_TYPE = f"{DOMAIN}.{__name__.rsplit('.', maxsplit=1)[-1]}" -EVENT_MODEL_MAP = { - "controller": CONTROLLER_EVENT_MODEL_MAP, - "driver": DRIVER_EVENT_MODEL_MAP, - "node": NODE_EVENT_MODEL_MAP, -} - def validate_non_node_event_source(obj: dict) -> dict: """Validate that a trigger for a non node event source has a config entry.""" @@ -58,7 +52,12 @@ def validate_event_name(obj: dict) -> dict: event_source = obj[ATTR_EVENT_SOURCE] event_name = obj[ATTR_EVENT] # the keys to the event source's model map are the event names - vol.In(EVENT_MODEL_MAP[event_source])(event_name) + if event_source == "controller": + vol.In(CONTROLLER_EVENT_MODEL_MAP)(event_name) + elif event_source == "driver": + vol.In(DRIVER_EVENT_MODEL_MAP)(event_name) + else: + vol.In(NODE_EVENT_MODEL_MAP)(event_name) return obj @@ -68,11 +67,16 @@ def validate_event_data(obj: dict) -> dict: if ATTR_EVENT_DATA not in obj: return obj - event_source = obj[ATTR_EVENT_SOURCE] - event_name = obj[ATTR_EVENT] - event_data = obj[ATTR_EVENT_DATA] + event_source: str = obj[ATTR_EVENT_SOURCE] + event_name: str = obj[ATTR_EVENT] + event_data: dict = obj[ATTR_EVENT_DATA] try: - EVENT_MODEL_MAP[event_source][event_name](**event_data) + if event_source == "controller": + CONTROLLER_EVENT_MODEL_MAP[event_name](**event_data) + elif event_source == "driver": + DRIVER_EVENT_MODEL_MAP[event_name](**event_data) + else: + NODE_EVENT_MODEL_MAP[event_name](**event_data) except ValidationError as exc: # Filter out required field errors if keys can be missing, and if there are # still errors, raise an exception @@ -90,7 +94,7 @@ TRIGGER_SCHEMA = vol.All( vol.Optional(ATTR_CONFIG_ENTRY_ID): str, vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_EVENT_SOURCE): vol.In(EVENT_MODEL_MAP), + vol.Required(ATTR_EVENT_SOURCE): vol.In(["controller", "driver", "node"]), vol.Required(ATTR_EVENT): cv.string, vol.Optional(ATTR_EVENT_DATA): dict, vol.Optional(ATTR_PARTIAL_DICT_MATCH, default=False): bool, @@ -200,11 +204,11 @@ async def async_attach_trigger( if not nodes: entry_id = config[ATTR_CONFIG_ENTRY_ID] client: Client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + assert client.driver if event_source == "controller": - source = client.driver.controller + unsubs.append(client.driver.controller.on(event_name, async_on_event)) else: - source = client.driver - unsubs.append(source.on(event_name, async_on_event)) + unsubs.append(client.driver.on(event_name, async_on_event)) for node in nodes: driver = node.client.driver diff --git a/homeassistant/components/zwave_js/triggers/value_updated.py b/homeassistant/components/zwave_js/triggers/value_updated.py index 38a19eaa377..ac3aae1efed 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -5,7 +5,6 @@ import functools import voluptuous as vol from zwave_js_server.const import CommandClass -from zwave_js_server.event import Event from zwave_js_server.model.node import Node from zwave_js_server.model.value import Value, get_value_id @@ -108,7 +107,7 @@ async def async_attach_trigger( @callback def async_on_value_updated( - value: Value, device: dr.DeviceEntry, event: Event + value: Value, device: dr.DeviceEntry, event: dict ) -> None: """Handle value update.""" event_value: Value = event["value"] diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index 8d00c9a2f64..3fe3bdfeb89 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -5,7 +5,10 @@ import pytest from zwave_js_server.event import Event from homeassistant.components.diagnostics.const import REDACTED -from homeassistant.components.zwave_js.diagnostics import async_get_device_diagnostics +from homeassistant.components.zwave_js.diagnostics import ( + ZwaveValueMatcher, + async_get_device_diagnostics, +) from homeassistant.components.zwave_js.discovery import async_discover_node_values from homeassistant.components.zwave_js.helpers import get_device_id from homeassistant.helpers.device_registry import async_get @@ -100,3 +103,9 @@ async def test_device_diagnostics_error(hass, integration): ) with pytest.raises(ValueError): await async_get_device_diagnostics(hass, integration, device) + + +async def test_empty_zwave_value_matcher(): + """Test empty ZwaveValueMatcher is invalid.""" + with pytest.raises(ValueError): + ZwaveValueMatcher() diff --git a/tests/components/zwave_js/test_helpers.py b/tests/components/zwave_js/test_helpers.py deleted file mode 100644 index 290c93fa084..00000000000 --- a/tests/components/zwave_js/test_helpers.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Test Z-Wave JS helpers module.""" -import pytest - -from homeassistant.components.zwave_js.helpers import ZwaveValueID - - -async def test_empty_zwave_value_id(): - """Test empty ZwaveValueID is invalid.""" - with pytest.raises(ValueError): - ZwaveValueID() From 4c8a77fbd4ba41688a2e0e71c1e814ff34647b7b Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Wed, 25 May 2022 08:30:21 +0200 Subject: [PATCH 0875/3516] Bump PyViCare==2.16.2 (#72448) --- homeassistant/components/vicare/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 3b3058058b6..575ca35729b 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -3,7 +3,7 @@ "name": "Viessmann ViCare", "documentation": "https://www.home-assistant.io/integrations/vicare", "codeowners": ["@oischinger"], - "requirements": ["PyViCare==2.16.1"], + "requirements": ["PyViCare==2.16.2"], "iot_class": "cloud_polling", "config_flow": true, "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 22f104e1c36..38cc91c2574 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -44,7 +44,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.6.6 # homeassistant.components.vicare -PyViCare==2.16.1 +PyViCare==2.16.2 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2c1d0ab918..cefa305a5b9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -40,7 +40,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.6.6 # homeassistant.components.vicare -PyViCare==2.16.1 +PyViCare==2.16.2 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 From 9514f491f0cc6bc6e813f8d5b8280c62784f7d33 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 25 May 2022 08:38:47 +0200 Subject: [PATCH 0876/3516] Add netgear speed test sensor (#72215) * implement speed_test * fix units * restore last speedtest result * fix import * fix restore state is None * fix styling * fix mypy * Use newer notation * correct unit * fix typing * fix pylint * fix issort * use RestoreSensor * fix import * fix sensor restore * do not extend SensorEntity * fix mypy * fix typing 2 --- homeassistant/components/netgear/__init__.py | 14 +++++ homeassistant/components/netgear/const.py | 1 + homeassistant/components/netgear/router.py | 7 +++ homeassistant/components/netgear/sensor.py | 63 ++++++++++++++++++-- 4 files changed, 81 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index 518a9051847..7a4d0e7a8cd 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -15,6 +15,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( DOMAIN, KEY_COORDINATOR, + KEY_COORDINATOR_SPEED, KEY_COORDINATOR_TRAFFIC, KEY_ROUTER, MODE_ROUTER, @@ -26,6 +27,7 @@ from .router import NetgearRouter _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=30) +SPEED_TEST_INTERVAL = timedelta(seconds=1800) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -78,6 +80,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Fetch data from the router.""" return await router.async_get_traffic_meter() + async def async_update_speed_test() -> dict[str, Any] | None: + """Fetch data from the router.""" + return await router.async_get_speed_test() + # Create update coordinators coordinator = DataUpdateCoordinator( hass, @@ -93,6 +99,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=async_update_traffic_meter, update_interval=SCAN_INTERVAL, ) + coordinator_speed_test = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"{router.device_name} Speed test", + update_method=async_update_speed_test, + update_interval=SPEED_TEST_INTERVAL, + ) if router.mode == MODE_ROUTER: await coordinator.async_config_entry_first_refresh() @@ -102,6 +115,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: KEY_ROUTER: router, KEY_COORDINATOR: coordinator, KEY_COORDINATOR_TRAFFIC: coordinator_traffic_meter, + KEY_COORDINATOR_SPEED: coordinator_speed_test, } hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/netgear/const.py b/homeassistant/components/netgear/const.py index bc4f37114fd..936777a7961 100644 --- a/homeassistant/components/netgear/const.py +++ b/homeassistant/components/netgear/const.py @@ -12,6 +12,7 @@ CONF_CONSIDER_HOME = "consider_home" KEY_ROUTER = "router" KEY_COORDINATOR = "coordinator" KEY_COORDINATOR_TRAFFIC = "coordinator_traffic" +KEY_COORDINATOR_SPEED = "coordinator_speed" DEFAULT_CONSIDER_HOME = timedelta(seconds=180) DEFAULT_NAME = "Netgear router" diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 6fb44e569d6..301906f22b6 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -208,6 +208,13 @@ class NetgearRouter: async with self._api_lock: return await self.hass.async_add_executor_job(self._api.get_traffic_meter) + async def async_get_speed_test(self) -> dict[str, Any] | None: + """Perform a speed test and get the results from the router.""" + async with self._api_lock: + return await self.hass.async_add_executor_job( + self._api.get_new_speed_test_result + ) + async def async_allow_block_device(self, mac: str, allow_block: str) -> None: """Allow or block a device connected to the router.""" async with self._api_lock: diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 16c63a8cdcb..9dec1ab3390 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -1,20 +1,37 @@ """Support for Netgear routers.""" +from __future__ import annotations + from collections.abc import Callable from dataclasses import dataclass +from datetime import date, datetime +from decimal import Decimal from homeassistant.components.sensor import ( + RestoreSensor, SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DATA_MEGABYTES, PERCENTAGE +from homeassistant.const import ( + DATA_MEGABYTES, + DATA_RATE_MEGABITS_PER_SECOND, + PERCENTAGE, + TIME_MILLISECONDS, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, KEY_COORDINATOR, KEY_COORDINATOR_TRAFFIC, KEY_ROUTER +from .const import ( + DOMAIN, + KEY_COORDINATOR, + KEY_COORDINATOR_SPEED, + KEY_COORDINATOR_TRAFFIC, + KEY_ROUTER, +) from .router import NetgearDeviceEntity, NetgearRouter, NetgearRouterEntity SENSOR_TYPES = { @@ -200,6 +217,30 @@ SENSOR_TRAFFIC_TYPES = [ ), ] +SENSOR_SPEED_TYPES = [ + NetgearSensorEntityDescription( + key="NewOOKLAUplinkBandwidth", + name="Uplink Bandwidth", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, + icon="mdi:upload", + ), + NetgearSensorEntityDescription( + key="NewOOKLADownlinkBandwidth", + name="Downlink Bandwidth", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND, + icon="mdi:download", + ), + NetgearSensorEntityDescription( + key="AveragePing", + name="Average Ping", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=TIME_MILLISECONDS, + icon="mdi:wan", + ), +] + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -208,6 +249,7 @@ async def async_setup_entry( router = hass.data[DOMAIN][entry.entry_id][KEY_ROUTER] coordinator = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR] coordinator_traffic = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR_TRAFFIC] + coordinator_speed = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR_SPEED] # Router entities router_entities = [] @@ -217,6 +259,11 @@ async def async_setup_entry( NetgearRouterSensorEntity(coordinator_traffic, router, description) ) + for description in SENSOR_SPEED_TYPES: + router_entities.append( + NetgearRouterSensorEntity(coordinator_speed, router, description) + ) + async_add_entities(router_entities) # Entities per network device @@ -288,7 +335,7 @@ class NetgearSensorEntity(NetgearDeviceEntity, SensorEntity): self._state = self._device[self._attribute] -class NetgearRouterSensorEntity(NetgearRouterEntity, SensorEntity): +class NetgearRouterSensorEntity(NetgearRouterEntity, RestoreSensor): """Representation of a device connected to a Netgear router.""" _attr_entity_registry_enabled_default = False @@ -306,7 +353,7 @@ class NetgearRouterSensorEntity(NetgearRouterEntity, SensorEntity): self._name = f"{router.device_name} {entity_description.name}" self._unique_id = f"{router.serial_number}-{entity_description.key}-{entity_description.index}" - self._value = None + self._value: StateType | date | datetime | Decimal = None self.async_update_device() @property @@ -314,6 +361,14 @@ class NetgearRouterSensorEntity(NetgearRouterEntity, SensorEntity): """Return the state of the sensor.""" return self._value + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + if self.coordinator.data is None: + sensor_data = await self.async_get_last_sensor_data() + if sensor_data is not None: + self._value = sensor_data.native_value + @callback def async_update_device(self) -> None: """Update the Netgear device.""" From 5dfeb1e02a5bb1153086aa1ca2af6f8a3507d7f2 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Wed, 25 May 2022 08:39:05 +0200 Subject: [PATCH 0877/3516] Allow removing devices in devolo_home_control (#72190) Allow removing devices --- .../devolo_home_control/__init__.py | 8 ++++ .../devolo_home_control/test_init.py | 43 ++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index 46b8f9dcaea..b79d0a5c8fe 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -15,6 +15,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.device_registry import DeviceEntry from .const import ( CONF_MYDEVOLO, @@ -92,6 +93,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + """Remove a config entry from a device.""" + return True + + def configure_mydevolo(conf: dict[str, Any] | MappingProxyType[str, Any]) -> Mydevolo: """Configure mydevolo.""" mydevolo = Mydevolo() diff --git a/tests/components/devolo_home_control/test_init.py b/tests/components/devolo_home_control/test_init.py index 311da47aac0..f12ea9486b4 100644 --- a/tests/components/devolo_home_control/test_init.py +++ b/tests/components/devolo_home_control/test_init.py @@ -1,13 +1,20 @@ """Tests for the devolo Home Control integration.""" +from collections.abc import Awaitable, Callable from unittest.mock import patch +from aiohttp import ClientWebSocketResponse from devolo_home_control_api.exceptions.gateway import GatewayOfflineError import pytest +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.devolo_home_control import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component -from tests.components.devolo_home_control import configure_integration +from . import configure_integration +from .mocks import HomeControlMock, HomeControlMockBinarySensor async def test_setup_entry(hass: HomeAssistant, mock_zeroconf): @@ -53,3 +60,37 @@ async def test_unload_entry(hass: HomeAssistant): await hass.async_block_till_done() await hass.config_entries.async_unload(entry.entry_id) assert entry.state is ConfigEntryState.NOT_LOADED + + +async def test_remove_device( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +): + """Test removing a device.""" + assert await async_setup_component(hass, "config", {}) + entry = configure_integration(hass) + test_gateway = HomeControlMockBinarySensor() + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "Test")}) + assert device_entry + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": entry.entry_id, + "device_id": device_entry.id, + } + ) + response = await client.receive_json() + assert response["success"] + assert device_registry.async_get_device(identifiers={(DOMAIN, "Test")}) is None + assert hass.states.get(f"{BINARY_SENSOR_DOMAIN}.test") is None From c1ddde376480b7353503a48c5765f4829a6e9d25 Mon Sep 17 00:00:00 2001 From: RoboMagus <68224306+RoboMagus@users.noreply.github.com> Date: Wed, 25 May 2022 08:44:08 +0200 Subject: [PATCH 0878/3516] Check if attributes are present in new_state before accessing them (#71967) * Check if attributes are present in new_state before accessing them. * Early return if new state is None|Unknown|Unavailable * Removed whitespace at line endings. +black run * Update test for coverage --- homeassistant/components/integration/sensor.py | 15 +++++++++------ tests/components/integration/test_sensor.py | 10 ++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 8cf5da5bc7c..5c982c6ec5e 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -191,11 +191,16 @@ class IntegrationSensor(RestoreEntity, SensorEntity): old_state = event.data.get("old_state") new_state = event.data.get("new_state") + if new_state is None or new_state.state in ( + STATE_UNKNOWN, + STATE_UNAVAILABLE, + ): + return + # We may want to update our state before an early return, # based on the source sensor's unit_of_measurement # or device_class. update_state = False - unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if unit is not None: new_unit_of_measurement = self._unit_template.format(unit) @@ -214,11 +219,9 @@ class IntegrationSensor(RestoreEntity, SensorEntity): if update_state: self.async_write_ha_state() - if ( - old_state is None - or new_state is None - or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) - or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) + if old_state is None or old_state.state in ( + STATE_UNKNOWN, + STATE_UNAVAILABLE, ): return diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 90c9e6f0fbc..e4173d62eb4 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -9,6 +9,7 @@ from homeassistant.const import ( ENERGY_WATT_HOUR, POWER_KILO_WATT, POWER_WATT, + STATE_UNAVAILABLE, STATE_UNKNOWN, TIME_SECONDS, ) @@ -350,6 +351,15 @@ async def test_units(hass): # they became valid assert state.attributes.get("unit_of_measurement") == ENERGY_WATT_HOUR + # When source state goes to None / Unknown, expect an early exit without + # changes to the state or unit_of_measurement + hass.states.async_set(entity_id, STATE_UNAVAILABLE, None) + await hass.async_block_till_done() + + new_state = hass.states.get("sensor.integration") + assert state == new_state + assert state.attributes.get("unit_of_measurement") == ENERGY_WATT_HOUR + async def test_device_class(hass): """Test integration sensor units using a power source.""" From a98af2ad58f53fc4bce68fe446044f275b82778a Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Wed, 25 May 2022 00:51:58 -0600 Subject: [PATCH 0879/3516] Better handling of balboa spa connection (#71909) * Better handling of balboa spa connection * Send a single message for keep alive task rather than multiple --- homeassistant/components/balboa/__init__.py | 39 ++++++++++++------- .../components/balboa/config_flow.py | 33 +++++++++------- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/balboa/__init__.py b/homeassistant/components/balboa/__init__.py index 731f7b2c2d1..60989ecc6d6 100644 --- a/homeassistant/components/balboa/__init__.py +++ b/homeassistant/components/balboa/__init__.py @@ -22,6 +22,7 @@ from .const import ( SIGNAL_UPDATE, ) +KEEP_ALIVE_INTERVAL = timedelta(minutes=1) SYNC_TIME_INTERVAL = timedelta(days=1) @@ -31,7 +32,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("Attempting to connect to %s", host) spa = BalboaSpaWifi(host) - connected = await spa.connect() if not connected: _LOGGER.error("Failed to connect to spa at %s", host) @@ -39,11 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = spa - # send config requests, and then listen until we are configured. - await spa.send_mod_ident_req() - await spa.send_panel_req(0, 1) - - async def _async_balboa_update_cb(): + async def _async_balboa_update_cb() -> None: """Primary update callback called from pybalboa.""" _LOGGER.debug("Primary update callback triggered") async_dispatcher_send(hass, SIGNAL_UPDATE.format(entry.entry_id)) @@ -52,13 +48,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: spa.new_data_cb = _async_balboa_update_cb _LOGGER.debug("Starting listener and monitor tasks") - asyncio.create_task(spa.listen()) + monitoring_tasks = [asyncio.create_task(spa.listen())] await spa.spa_configured() - asyncio.create_task(spa.check_connection_status()) + monitoring_tasks.append(asyncio.create_task(spa.check_connection_status())) + + def stop_monitoring() -> None: + """Stop monitoring the spa connection.""" + _LOGGER.debug("Canceling listener and monitor tasks") + for task in monitoring_tasks: + task.cancel() + + entry.async_on_unload(stop_monitoring) # At this point we have a configured spa. hass.config_entries.async_setup_platforms(entry, PLATFORMS) + async def keep_alive(now: datetime) -> None: + """Keep alive task.""" + _LOGGER.debug("Keep alive") + await spa.send_mod_ident_req() + + entry.async_on_unload( + async_track_time_interval(hass, keep_alive, KEEP_ALIVE_INTERVAL) + ) + # call update_listener on startup and for options change as well. await async_setup_time_sync(hass, entry) entry.async_on_unload(entry.add_update_listener(update_listener)) @@ -68,14 +81,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - _LOGGER.debug("Disconnecting from spa") - spa = hass.data[DOMAIN][entry.entry_id] - await spa.disconnect() + spa: BalboaSpaWifi = hass.data[DOMAIN][entry.entry_id] if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) + await spa.disconnect() + return unload_ok @@ -90,9 +103,9 @@ async def async_setup_time_sync(hass: HomeAssistant, entry: ConfigEntry) -> None return _LOGGER.debug("Setting up daily time sync") - spa = hass.data[DOMAIN][entry.entry_id] + spa: BalboaSpaWifi = hass.data[DOMAIN][entry.entry_id] - async def sync_time(now: datetime): + async def sync_time(now: datetime) -> None: _LOGGER.debug("Syncing time with Home Assistant") await spa.set_time(time.strptime(str(dt_util.now()), "%Y-%m-%d %H:%M:%S.%f%z")) diff --git a/homeassistant/components/balboa/config_flow.py b/homeassistant/components/balboa/config_flow.py index 42895e5ccd6..c0301bc9892 100644 --- a/homeassistant/components/balboa/config_flow.py +++ b/homeassistant/components/balboa/config_flow.py @@ -1,12 +1,16 @@ """Config flow for Balboa Spa Client integration.""" +from __future__ import annotations + import asyncio +from typing import Any from pybalboa import BalboaSpaWifi import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries, exceptions from homeassistant.const import CONF_HOST from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac from .const import _LOGGER, CONF_SYNC_TIME, DOMAIN @@ -14,9 +18,8 @@ from .const import _LOGGER, CONF_SYNC_TIME, DOMAIN DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(data: dict[str, Any]) -> dict[str, str]: """Validate the user input allows us to connect.""" - _LOGGER.debug("Attempting to connect to %s", data[CONF_HOST]) spa = BalboaSpaWifi(data[CONF_HOST]) connected = await spa.connect() @@ -24,16 +27,12 @@ async def validate_input(hass: core.HomeAssistant, data): if not connected: raise CannotConnect - # send config requests, and then listen until we are configured. - await spa.send_mod_ident_req() - await spa.send_panel_req(0, 1) - - asyncio.create_task(spa.listen()) - + task = asyncio.create_task(spa.listen()) await spa.spa_configured() mac_addr = format_mac(spa.get_macaddr()) model = spa.get_model_name() + task.cancel() await spa.disconnect() return {"title": model, "formatted_mac": mac_addr} @@ -46,17 +45,21 @@ class BalboaSpaClientFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: """Get the options flow for this handler.""" return BalboaSpaClientOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} 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) + info = await validate_input(user_input) except CannotConnect: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except @@ -79,11 +82,13 @@ class CannotConnect(exceptions.HomeAssistantError): class BalboaSpaClientOptionsFlowHandler(config_entries.OptionsFlow): """Handle Balboa Spa Client options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize Balboa Spa Client options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage Balboa Spa Client options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) From 9591d5366efc783c580f6e5ab66bfa8ae7c7fc4e Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 25 May 2022 02:57:26 -0400 Subject: [PATCH 0880/3516] Add config entities for the Aqara P1 motion sensor to ZHA (#72466) * initial work for configurable detection interval * update opple cluster channel * detection interval * motion sensitivity * only set the init attributes for the right device * add trigger indicator configuration entity --- .../zha/core/channels/manufacturerspecific.py | 26 ++++++++++++++++++- homeassistant/components/zha/number.py | 11 ++++++++ homeassistant/components/zha/select.py | 17 ++++++++++++ homeassistant/components/zha/switch.py | 13 ++++++++-- 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index f31c7f51371..3bcab38e026 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -1,7 +1,9 @@ """Manufacturer specific channels module for Zigbee Home Automation.""" +import logging + from homeassistant.core import callback -from .. import registries +from .. import registries, typing as zha_typing from ..const import ( ATTR_ATTRIBUTE_ID, ATTR_ATTRIBUTE_NAME, @@ -14,6 +16,8 @@ from ..const import ( ) from .base import ClientChannel, ZigbeeChannel +_LOGGER = logging.getLogger(__name__) + @registries.ZIGBEE_CHANNEL_REGISTRY.register(registries.SMARTTHINGS_HUMIDITY_CLUSTER) class SmartThingsHumidity(ZigbeeChannel): @@ -50,6 +54,26 @@ class OppleRemote(ZigbeeChannel): REPORT_CONFIG = [] + def __init__( + self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType + ) -> None: + """Initialize Opple channel.""" + super().__init__(cluster, ch_pool) + if self.cluster.endpoint.model == "lumi.motion.ac02": + self.ZCL_INIT_ATTRS = { # pylint: disable=C0103 + "detection_interval": True, + "motion_sensitivity": True, + "trigger_indicator": True, + } + + async def async_initialize_channel_specific(self, from_cache: bool) -> None: + """Initialize channel specific.""" + if self.cluster.endpoint.model == "lumi.motion.ac02": + interval = self.cluster.get("detection_interval", self.cluster.get(0x0102)) + if interval is not None: + self.debug("Loaded detection interval at startup: %s", interval) + self.cluster.endpoint.ias_zone.reset_s = int(interval) + @registries.ZIGBEE_CHANNEL_REGISTRY.register( registries.SMARTTHINGS_ACCELERATION_CLUSTER diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index bece4bc894d..216b9974df6 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -433,6 +433,17 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity): _LOGGER.debug("read value=%s", value) +@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.motion.ac02"}) +class AqaraMotionDetectionInterval( + ZHANumberConfigurationEntity, id_suffix="detection_interval" +): + """Representation of a ZHA on off transition time configuration entity.""" + + _attr_min_value: float = 2 + _attr_max_value: float = 65535 + _zcl_attribute: str = "detection_interval" + + @CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL) class OnOffTransitionTimeConfigurationEntity( ZHANumberConfigurationEntity, id_suffix="on_off_transition_time" diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index afa2ee18da9..8714d804790 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -5,6 +5,7 @@ from enum import Enum import functools import logging +from zigpy import types from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.clusters.security import IasWd @@ -208,3 +209,19 @@ class ZHAStartupOnOffSelectEntity( _select_attr = "start_up_on_off" _enum: Enum = OnOff.StartUpOnOff + + +class AqaraMotionSensitivities(types.enum8): + """Aqara motion sensitivities.""" + + Low = 0x01 + Medium = 0x02 + High = 0x03 + + +@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.motion.ac02"}) +class AqaraMotionSensitivity(ZCLEnumSelectEntity, id_suffix="motion_sensitivity"): + """Representation of a ZHA on off transition time configuration entity.""" + + _select_attr = "motion_sensitivity" + _enum: Enum = AqaraMotionSensitivities diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 800c42eb932..d9199ed77c8 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -155,6 +155,7 @@ class SwitchGroup(ZhaGroupEntity, SwitchEntity): class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): """Representation of a ZHA switch configuration entity.""" + _attr_entity_category = EntityCategory.CONFIG _zcl_attribute: str _zcl_inverter_attribute: str = "" @@ -260,8 +261,16 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): class OnOffWindowDetectionFunctionConfigurationEntity( ZHASwitchConfigurationEntity, id_suffix="on_off_window_opened_detection" ): - """Representation of a ZHA on off transition time configuration entity.""" + """Representation of a ZHA window detection configuration entity.""" - _attr_entity_category = EntityCategory.CONFIG _zcl_attribute = "window_detection_function" _zcl_inverter_attribute = "window_detection_function_inverter" + + +@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.motion.ac02"}) +class P1MotionTriggerIndicatorSwitch( + ZHASwitchConfigurationEntity, id_suffix="trigger_indicator" +): + """Representation of a ZHA motion triggering configuration entity.""" + + _zcl_attribute = "trigger_indicator" From 71bc650ac70a417cbe0d42be55abaf3a6da75bc6 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 24 May 2022 23:59:27 -0700 Subject: [PATCH 0881/3516] Stop updating google_calendars.yaml if it does not already exist (#72340) * Stop updating google_calendars.yaml if it does not already exist * Add additional test coverage to make CI pass * Add test for no updates to google_calendar.yaml * Add parameter to test for expecting write calls * Missing call argument * Remove conditional and replace with inline assert --- homeassistant/components/google/__init__.py | 23 +++++---- tests/components/google/conftest.py | 14 ++++-- tests/components/google/test_calendar.py | 4 +- tests/components/google/test_init.py | 53 ++++++++++++++++++++- 4 files changed, 79 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 70732af1fd8..f034d48b9c5 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -300,6 +300,7 @@ async def async_setup_services( calendars = await hass.async_add_executor_job( load_config, hass.config.path(YAML_DEVICES) ) + calendars_file_lock = asyncio.Lock() async def _found_calendar(calendar_item: Calendar) -> None: calendar = get_calendar_info( @@ -307,15 +308,19 @@ async def async_setup_services( calendar_item.dict(exclude_unset=True), ) calendar_id = calendar_item.id - # Populate the yaml file with all discovered calendars - if calendar_id not in calendars: - calendars[calendar_id] = calendar - await hass.async_add_executor_job( - update_config, hass.config.path(YAML_DEVICES), calendar - ) - else: - # Prefer entity/name information from yaml, overriding api - calendar = calendars[calendar_id] + # If the google_calendars.yaml file already exists, populate it for + # backwards compatibility, but otherwise do not create it if it does + # not exist. + if calendars: + if calendar_id not in calendars: + calendars[calendar_id] = calendar + async with calendars_file_lock: + await hass.async_add_executor_job( + update_config, hass.config.path(YAML_DEVICES), calendar + ) + else: + # Prefer entity/name information from yaml, overriding api + calendar = calendars[calendar_id] async_dispatcher_send(hass, DISCOVER_CALENDAR, calendar) created_calendars = set() diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 48badbc3ab5..b5566450913 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable import datetime from typing import Any, Generator, TypeVar -from unittest.mock import mock_open, patch +from unittest.mock import Mock, mock_open, patch from aiohttp.client_exceptions import ClientError from gcal_sync.auth import API_BASE_URL @@ -104,11 +104,11 @@ def calendars_config(calendars_config_entity: dict[str, Any]) -> list[dict[str, def mock_calendars_yaml( hass: HomeAssistant, calendars_config: list[dict[str, Any]], -) -> None: +) -> Generator[Mock, None, None]: """Fixture that prepares the google_calendars.yaml mocks.""" mocked_open_function = mock_open(read_data=yaml.dump(calendars_config)) with patch("homeassistant.components.google.open", mocked_open_function): - yield + yield mocked_open_function class FakeStorage: @@ -170,10 +170,17 @@ def config_entry_token_expiry(token_expiry: datetime.datetime) -> float: return token_expiry.timestamp() +@pytest.fixture +def config_entry_options() -> dict[str, Any] | None: + """Fixture to set initial config entry options.""" + return None + + @pytest.fixture def config_entry( token_scopes: list[str], config_entry_token_expiry: float, + config_entry_options: dict[str, Any] | None, ) -> MockConfigEntry: """Fixture to create a config entry for the integration.""" return MockConfigEntry( @@ -188,6 +195,7 @@ def config_entry( "expires_at": config_entry_token_expiry, }, }, + options=config_entry_options, ) diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 6ae62f50914..794065ca09a 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -572,14 +572,16 @@ async def test_opaque_event( assert (len(events) > 0) == expect_visible_event +@pytest.mark.parametrize("mock_test_setup", [None]) async def test_scan_calendar_error( hass, component_setup, test_api_calendar, mock_calendars_list, + config_entry, ): """Test that the calendar update handles a server error.""" - + config_entry.add_to_hass(hass) mock_calendars_list({}, exc=ClientError()) assert await component_setup() diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 4709379e840..93c0642514e 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -6,7 +6,7 @@ import datetime import http import time from typing import Any -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest @@ -19,6 +19,7 @@ from homeassistant.components.google import ( SERVICE_ADD_EVENT, SERVICE_SCAN_CALENDARS, ) +from homeassistant.components.google.const import CONF_CALENDAR_ACCESS from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant, State @@ -229,7 +230,10 @@ async def test_found_calendar_from_api( assert not hass.states.get(TEST_YAML_ENTITY) -@pytest.mark.parametrize("calendars_config,google_config", [([], {})]) +@pytest.mark.parametrize( + "calendars_config,google_config,config_entry_options", + [([], {}, {CONF_CALENDAR_ACCESS: "read_write"})], +) async def test_load_application_credentials( hass: HomeAssistant, component_setup: ComponentSetup, @@ -604,3 +608,48 @@ async def test_expired_token_requires_reauth( flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 assert flows[0]["step_id"] == "reauth_confirm" + + +@pytest.mark.parametrize( + "calendars_config,expect_write_calls", + [ + ( + [ + { + "cal_id": "ignored", + "entities": {"device_id": "existing", "name": "existing"}, + } + ], + True, + ), + ([], False), + ], + ids=["has_yaml", "no_yaml"], +) +async def test_calendar_yaml_update( + hass: HomeAssistant, + component_setup: ComponentSetup, + mock_calendars_yaml: Mock, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + mock_events_list: ApiResult, + setup_config_entry: MockConfigEntry, + calendars_config: dict[str, Any], + expect_write_calls: bool, +) -> None: + """Test updating the yaml file with a new calendar.""" + + mock_calendars_list({"items": [test_api_calendar]}) + mock_events_list({}) + assert await component_setup() + + mock_calendars_yaml().read.assert_called() + mock_calendars_yaml().write.called is expect_write_calls + + state = hass.states.get(TEST_API_ENTITY) + assert state + assert state.name == TEST_API_ENTITY_NAME + assert state.state == STATE_OFF + + # No yaml config loaded that overwrites the entity name + assert not hass.states.get(TEST_YAML_ENTITY) From 4f14d40072b37b409b87f08c0cfe85f5547c32f6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 25 May 2022 09:00:42 +0200 Subject: [PATCH 0882/3516] Adjust config-flow type hints in philips_js (#72443) --- .../components/philips_js/config_flow.py | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py index 29abbe5dd71..9785aaf54a3 100644 --- a/homeassistant/components/philips_js/config_flow.py +++ b/homeassistant/components/philips_js/config_flow.py @@ -15,12 +15,13 @@ from homeassistant.const import ( CONF_PIN, CONF_USERNAME, ) +from homeassistant.data_entry_flow import FlowResult from . import LOGGER from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, CONST_APP_ID, CONST_APP_NAME, DOMAIN -async def validate_input( +async def _validate_input( hass: core.HomeAssistant, host: str, api_version: int ) -> tuple[dict, PhilipsTV]: """Validate the user input allows us to connect.""" @@ -47,7 +48,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._hub: PhilipsTV | None = None self._pair_state: Any = None - async def _async_create_current(self): + async def _async_create_current(self) -> FlowResult: system = self._current[CONF_SYSTEM] return self.async_create_entry( @@ -55,7 +56,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data=self._current, ) - async def async_step_pair(self, user_input: dict | None = None) -> dict: + async def async_step_pair( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Attempt to pair with device.""" assert self._hub @@ -106,13 +109,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._current[CONF_PASSWORD] = password return await self._async_create_current() - async def async_step_user(self, user_input: dict | None = None) -> dict: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" errors = {} if user_input: self._current = user_input try: - hub = await validate_input( + hub = await _validate_input( self.hass, user_input[CONF_HOST], user_input[CONF_API_VERSION] ) except ConnectionFailure as exc: @@ -146,7 +151,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @core.callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) @@ -158,7 +165,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Initialize options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle options flow.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) From ce477e65ce5105fc0b42cfc196b70defac6d9bbf Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 25 May 2022 08:02:48 +0100 Subject: [PATCH 0883/3516] Render template during stream_url test for generic camera (#69716) --- homeassistant/components/generic/config_flow.py | 7 +++++++ homeassistant/components/generic/strings.json | 2 ++ homeassistant/components/generic/translations/en.json | 2 ++ tests/components/generic/test_config_flow.py | 10 ++++++++++ 4 files changed, 21 insertions(+) diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 651ed87da39..272c7a2d98e 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -198,6 +198,13 @@ async def async_test_stream(hass, info) -> dict[str, str]: """Verify that the stream is valid before we create an entity.""" if not (stream_source := info.get(CONF_STREAM_SOURCE)): return {} + if not isinstance(stream_source, template_helper.Template): + stream_source = template_helper.Template(stream_source, hass) + try: + stream_source = stream_source.async_render(parse_result=False) + except TemplateError as err: + _LOGGER.warning("Problem rendering template %s: %s", stream_source, err) + return {CONF_STREAM_SOURCE: "template_error"} try: # For RTSP streams, prefer TCP. This code is duplicated from # homeassistant.components.stream.__init__.py:create_stream() diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index 0954656f71d..6b73c70cf3d 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -8,6 +8,7 @@ "invalid_still_image": "URL did not return a valid still image", "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", + "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "stream_no_route_to_host": "Could not find host while trying to connect to stream", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", @@ -79,6 +80,7 @@ "invalid_still_image": "[%key:component::generic::config::error::invalid_still_image%]", "stream_file_not_found": "[%key:component::generic::config::error::stream_file_not_found%]", "stream_http_not_found": "[%key:component::generic::config::error::stream_http_not_found%]", + "template_error": "[%key:component::generic::config::error::template_error%]", "timeout": "[%key:component::generic::config::error::timeout%]", "stream_no_route_to_host": "[%key:component::generic::config::error::stream_no_route_to_host%]", "stream_io_error": "[%key:component::generic::config::error::stream_io_error%]", diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json index b552c780d29..d01e6e59a4b 100644 --- a/homeassistant/components/generic/translations/en.json +++ b/homeassistant/components/generic/translations/en.json @@ -15,6 +15,7 @@ "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", "stream_unauthorised": "Authorisation failed while trying to connect to stream", + "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", "unknown": "Unexpected error" @@ -57,6 +58,7 @@ "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", "stream_unauthorised": "Authorisation failed while trying to connect to stream", + "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", "unknown": "Unexpected error" diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index b7f3bf73527..a525619d962 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -534,6 +534,16 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_av_open): assert result4.get("type") == data_entry_flow.RESULT_TYPE_FORM assert result4["errors"] == {"still_image_url": "template_error"} + # verify that an invalid template reports the correct UI error. + data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/1" + data[CONF_STREAM_SOURCE] = "http://127.0.0.2/testurl/{{1/0}}" + result5 = await hass.config_entries.options.async_configure( + result4["flow_id"], + user_input=data, + ) + assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result5["errors"] == {"stream_source": "template_error"} + @respx.mock async def test_options_only_stream(hass, fakeimgbytes_png, mock_av_open): From 88c49f034a01f9d1dae540c74e4b1003aa073f85 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Wed, 25 May 2022 09:05:11 +0200 Subject: [PATCH 0884/3516] Use 'python-homewizard-energy' dependency for HomeWizard (#71781) * Update requirement * Remove aiohwenergy and use python-homewizard-energy * Update test to work with python-homewizard-energy * Bumb python-homewizard-energy to 1.0.3 --- .../components/homewizard/__init__.py | 17 +- .../components/homewizard/config_flow.py | 37 ++-- homeassistant/components/homewizard/const.py | 6 +- .../components/homewizard/coordinator.py | 59 ++----- .../components/homewizard/manifest.json | 4 +- homeassistant/components/homewizard/sensor.py | 15 +- homeassistant/components/homewizard/switch.py | 16 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/homewizard/generator.py | 21 ++- .../components/homewizard/test_config_flow.py | 104 +++++------- tests/components/homewizard/test_init.py | 30 ++-- tests/components/homewizard/test_sensor.py | 160 +++++++----------- tests/components/homewizard/test_switch.py | 73 ++++---- 14 files changed, 218 insertions(+), 336 deletions(-) diff --git a/homeassistant/components/homewizard/__init__.py b/homeassistant/components/homewizard/__init__.py index b50d87a940d..4a087988b61 100644 --- a/homeassistant/components/homewizard/__init__.py +++ b/homeassistant/components/homewizard/__init__.py @@ -1,14 +1,11 @@ """The Homewizard integration.""" import logging -from aiohwenergy import DisabledError - from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.update_coordinator import UpdateFailed from .const import DOMAIN, PLATFORMS from .coordinator import HWEnergyDeviceUpdateCoordinator as Coordinator @@ -69,16 +66,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Create coordinator coordinator = Coordinator(hass, entry.data[CONF_IP_ADDRESS]) try: - await coordinator.initialize_api() - - except DisabledError: - _LOGGER.error("API is disabled, enable API in HomeWizard Energy app") - return False - - except UpdateFailed as ex: - raise ConfigEntryNotReady from ex - - await coordinator.async_config_entry_first_refresh() + await coordinator.async_config_entry_first_refresh() + except ConfigEntryNotReady: + await coordinator.api.close() + raise # Finalize hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index 6ab485f534f..df883baf3b1 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -4,9 +4,8 @@ from __future__ import annotations import logging from typing import Any -import aiohwenergy -from aiohwenergy.hwenergy import SUPPORTED_DEVICES -import async_timeout +from homewizard_energy import HomeWizardEnergy +from homewizard_energy.errors import DisabledError, UnsupportedError from voluptuous import Required, Schema from homeassistant import config_entries @@ -175,16 +174,19 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # Make connection with device # This is to test the connection and to get info for unique_id - energy_api = aiohwenergy.HomeWizardEnergy(ip_address) + energy_api = HomeWizardEnergy(ip_address) try: - with async_timeout.timeout(10): - await energy_api.initialize() + device = await energy_api.device() - except aiohwenergy.DisabledError as ex: + except DisabledError as ex: _LOGGER.error("API disabled, API must be enabled in the app") raise AbortFlow("api_not_enabled") from ex + except UnsupportedError as ex: + _LOGGER.error("API version unsuppored") + raise AbortFlow("unsupported_api_version") from ex + except Exception as ex: _LOGGER.exception( "Error connecting with Energy Device at %s", @@ -195,25 +197,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): finally: await energy_api.close() - if energy_api.device is None: - _LOGGER.error("Initialization failed") - raise AbortFlow("unknown_error") - - # Validate metadata - if energy_api.device.api_version != "v1": - raise AbortFlow("unsupported_api_version") - - if energy_api.device.product_type not in SUPPORTED_DEVICES: - _LOGGER.error( - "Device (%s) not supported by integration", - energy_api.device.product_type, - ) - raise AbortFlow("device_not_supported") - return { - CONF_PRODUCT_NAME: energy_api.device.product_name, - CONF_PRODUCT_TYPE: energy_api.device.product_type, - CONF_SERIAL: energy_api.device.serial, + CONF_PRODUCT_NAME: device.product_name, + CONF_PRODUCT_TYPE: device.product_type, + CONF_SERIAL: device.serial, } async def _async_set_and_check_unique_id(self, entry_info: dict[str, Any]) -> None: diff --git a/homeassistant/components/homewizard/const.py b/homeassistant/components/homewizard/const.py index 75c522a211e..c1c788d371b 100644 --- a/homeassistant/components/homewizard/const.py +++ b/homeassistant/components/homewizard/const.py @@ -5,10 +5,9 @@ from datetime import timedelta from typing import TypedDict # Set up. -from aiohwenergy.device import Device +from homewizard_energy.models import Data, Device, State from homeassistant.const import Platform -from homeassistant.helpers.typing import StateType DOMAIN = "homewizard" PLATFORMS = [Platform.SENSOR, Platform.SWITCH] @@ -29,4 +28,5 @@ class DeviceResponseEntry(TypedDict): """Dict describing a single response entry.""" device: Device - data: dict[str, StateType] + data: Data + state: State diff --git a/homeassistant/components/homewizard/coordinator.py b/homeassistant/components/homewizard/coordinator.py index 2cce88cbe36..e12edda63ae 100644 --- a/homeassistant/components/homewizard/coordinator.py +++ b/homeassistant/components/homewizard/coordinator.py @@ -1,11 +1,10 @@ """Update coordinator for HomeWizard.""" from __future__ import annotations -import asyncio import logging -import aiohwenergy -import async_timeout +from homewizard_energy import HomeWizardEnergy +from homewizard_energy.errors import DisabledError from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -19,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry]): """Gather data for the energy device.""" - api: aiohwenergy.HomeWizardEnergy + api: HomeWizardEnergy def __init__( self, @@ -29,56 +28,20 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry] """Initialize Update Coordinator.""" super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) - - session = async_get_clientsession(hass) - self.api = aiohwenergy.HomeWizardEnergy(host, clientsession=session) + self.api = HomeWizardEnergy(host, clientsession=async_get_clientsession(hass)) async def _async_update_data(self) -> DeviceResponseEntry: """Fetch all device and sensor data from api.""" - async with async_timeout.timeout(10): - - if self.api.device is None: - await self.initialize_api() - - # Update all properties - try: - if not await self.api.update(): - raise UpdateFailed("Failed to communicate with device") - - except aiohwenergy.DisabledError as ex: - raise UpdateFailed( - "API disabled, API must be enabled in the app" - ) from ex - + # Update all properties + try: data: DeviceResponseEntry = { - "device": self.api.device, - "data": {}, + "device": await self.api.device(), + "data": await self.api.data(), + "state": await self.api.state(), } - for datapoint in self.api.data.available_datapoints: - data["data"][datapoint] = getattr(self.api.data, datapoint) + except DisabledError as ex: + raise UpdateFailed("API disabled, API must be enabled in the app") from ex return data - - async def initialize_api(self) -> aiohwenergy: - """Initialize API and validate connection.""" - - try: - await self.api.initialize() - - except (asyncio.TimeoutError, aiohwenergy.RequestError) as ex: - raise UpdateFailed( - f"Error connecting to the Energy device at {self.api.host}" - ) from ex - - except aiohwenergy.DisabledError as ex: - raise ex - - except aiohwenergy.AiohwenergyException as ex: - raise UpdateFailed("Unknown Energy API error occurred") from ex - - except Exception as ex: - raise UpdateFailed( - f"Unknown error connecting with Energy Device at {self.api.host}" - ) from ex diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index 1bd856334b7..e426234b449 100644 --- a/homeassistant/components/homewizard/manifest.json +++ b/homeassistant/components/homewizard/manifest.json @@ -4,9 +4,9 @@ "documentation": "https://www.home-assistant.io/integrations/homewizard", "codeowners": ["@DCSBL"], "dependencies": [], - "requirements": ["aiohwenergy==0.8.0"], + "requirements": ["python-homewizard-energy==1.0.3"], "zeroconf": ["_hwenergy._tcp.local."], "config_flow": true, "iot_class": "local_polling", - "loggers": ["aiohwenergy"] + "loggers": ["homewizard_energy"] } diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index 34479dd6b0a..b7f759f6a0e 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Final +from typing import Final, cast from homeassistant.components.sensor import ( SensorDeviceClass, @@ -129,12 +129,9 @@ async def async_setup_entry( coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] entities = [] - if coordinator.api.data is not None: + if coordinator.data["data"] is not None: for description in SENSORS: - if ( - description.key in coordinator.api.data.available_datapoints - and getattr(coordinator.api.data, description.key) is not None - ): + if getattr(coordinator.data["data"], description.key) is not None: entities.append(HWEnergySensor(coordinator, entry, description)) async_add_entities(entities) @@ -165,7 +162,7 @@ class HWEnergySensor(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SensorE "total_power_export_t1_kwh", "total_power_export_t2_kwh", ]: - if self.data["data"][self.data_type] == 0: + if self.native_value == 0: self._attr_entity_registry_enabled_default = False @property @@ -187,9 +184,9 @@ class HWEnergySensor(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SensorE @property def native_value(self) -> StateType: """Return state of meter.""" - return self.data["data"][self.data_type] + return cast(StateType, getattr(self.data["data"], self.data_type)) @property def available(self) -> bool: """Return availability of meter.""" - return super().available and self.data_type in self.data["data"] + return super().available and self.native_value is not None diff --git a/homeassistant/components/homewizard/switch.py b/homeassistant/components/homewizard/switch.py index 3c6b1a1c5dc..eb2e9c49afe 100644 --- a/homeassistant/components/homewizard/switch.py +++ b/homeassistant/components/homewizard/switch.py @@ -22,7 +22,7 @@ async def async_setup_entry( """Set up switches.""" coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - if coordinator.api.state: + if coordinator.data["state"]: async_add_entities( [ HWEnergyMainSwitchEntity(coordinator, entry), @@ -70,12 +70,12 @@ class HWEnergyMainSwitchEntity(HWEnergySwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" - await self.coordinator.api.state.set(power_on=True) + await self.coordinator.api.state_set(power_on=True) await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" - await self.coordinator.api.state.set(power_on=False) + await self.coordinator.api.state_set(power_on=False) await self.coordinator.async_refresh() @property @@ -85,12 +85,12 @@ class HWEnergyMainSwitchEntity(HWEnergySwitchEntity): This switch becomes unavailable when switch_lock is enabled. """ - return super().available and not self.coordinator.api.state.switch_lock + return super().available and not self.coordinator.data["state"].switch_lock @property def is_on(self) -> bool: """Return true if switch is on.""" - return bool(self.coordinator.api.state.power_on) + return bool(self.coordinator.data["state"].power_on) class HWEnergySwitchLockEntity(HWEnergySwitchEntity): @@ -115,15 +115,15 @@ class HWEnergySwitchLockEntity(HWEnergySwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn switch-lock on.""" - await self.coordinator.api.state.set(switch_lock=True) + await self.coordinator.api.state_set(switch_lock=True) await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: """Turn switch-lock off.""" - await self.coordinator.api.state.set(switch_lock=False) + await self.coordinator.api.state_set(switch_lock=False) await self.coordinator.async_refresh() @property def is_on(self) -> bool: """Return true if switch is on.""" - return bool(self.coordinator.api.state.switch_lock) + return bool(self.coordinator.data["state"].switch_lock) diff --git a/requirements_all.txt b/requirements_all.txt index 38cc91c2574..a52d465fac6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -171,9 +171,6 @@ aiohttp_cors==0.7.0 # homeassistant.components.hue aiohue==4.4.1 -# homeassistant.components.homewizard -aiohwenergy==0.8.0 - # homeassistant.components.imap aioimaplib==0.9.0 @@ -1903,6 +1900,9 @@ python-gc100==1.0.3a0 # homeassistant.components.gitlab_ci python-gitlab==1.6.0 +# homeassistant.components.homewizard +python-homewizard-energy==1.0.3 + # homeassistant.components.hp_ilo python-hpilo==4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cefa305a5b9..897ffe50074 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -155,9 +155,6 @@ aiohttp_cors==0.7.0 # homeassistant.components.hue aiohue==4.4.1 -# homeassistant.components.homewizard -aiohwenergy==0.8.0 - # homeassistant.components.apache_kafka aiokafka==0.6.0 @@ -1265,6 +1262,9 @@ python-ecobee-api==0.2.14 # homeassistant.components.darksky python-forecastio==1.4.0 +# homeassistant.components.homewizard +python-homewizard-energy==1.0.3 + # homeassistant.components.izone python-izone==1.2.3 diff --git a/tests/components/homewizard/generator.py b/tests/components/homewizard/generator.py index 74d33c9e609..0f94580ad84 100644 --- a/tests/components/homewizard/generator.py +++ b/tests/components/homewizard/generator.py @@ -2,6 +2,8 @@ from unittest.mock import AsyncMock +from homewizard_energy.models import Device + def get_mock_device( serial="aabbccddeeff", @@ -13,15 +15,18 @@ def get_mock_device( mock_device = AsyncMock() mock_device.host = host - mock_device.device.product_name = product_name - mock_device.device.product_type = product_type - mock_device.device.serial = serial - mock_device.device.api_version = "v1" - mock_device.device.firmware_version = "1.00" + mock_device.device = AsyncMock( + return_value=Device( + product_name=product_name, + product_type=product_type, + serial=serial, + api_version="V1", + firmware_version="1.00", + ) + ) + mock_device.data = AsyncMock(return_value=None) + mock_device.state = AsyncMock(return_value=None) - mock_device.state = None - - mock_device.initialize = AsyncMock() mock_device.close = AsyncMock() return mock_device diff --git a/tests/components/homewizard/test_config_flow.py b/tests/components/homewizard/test_config_flow.py index d0dc7d04509..d2e7d4c58ae 100644 --- a/tests/components/homewizard/test_config_flow.py +++ b/tests/components/homewizard/test_config_flow.py @@ -2,7 +2,7 @@ import logging from unittest.mock import patch -from aiohwenergy import DisabledError +from homewizard_energy.errors import DisabledError, UnsupportedError from homeassistant import config_entries from homeassistant.components import zeroconf @@ -33,7 +33,10 @@ async def test_manual_flow_works(hass, aioclient_mock): assert result["type"] == "form" assert result["step_id"] == "user" - with patch("aiohwenergy.HomeWizardEnergy", return_value=device,), patch( + with patch( + "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", + return_value=device, + ), patch( "homeassistant.components.homewizard.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -42,12 +45,12 @@ async def test_manual_flow_works(hass, aioclient_mock): ) assert result["type"] == "create_entry" - assert result["title"] == f"{device.device.product_name} (aabbccddeeff)" + assert result["title"] == "P1 meter (aabbccddeeff)" assert result["data"][CONF_IP_ADDRESS] == "2.2.2.2" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert len(device.initialize.mock_calls) == 1 + assert len(device.device.mock_calls) == 1 assert len(device.close.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -72,7 +75,10 @@ async def test_discovery_flow_works(hass, aioclient_mock): }, ) - with patch("aiohwenergy.HomeWizardEnergy", return_value=get_mock_device()): + with patch( + "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", + return_value=get_mock_device(), + ): flow = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, @@ -82,7 +88,10 @@ async def test_discovery_flow_works(hass, aioclient_mock): with patch( "homeassistant.components.homewizard.async_setup_entry", return_value=True, - ), patch("aiohwenergy.HomeWizardEnergy", return_value=get_mock_device()): + ), patch( + "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", + return_value=get_mock_device(), + ): result = await hass.config_entries.flow.async_configure( flow["flow_id"], user_input=None ) @@ -92,7 +101,10 @@ async def test_discovery_flow_works(hass, aioclient_mock): with patch( "homeassistant.components.homewizard.async_setup_entry", return_value=True, - ), patch("aiohwenergy.HomeWizardEnergy", return_value=get_mock_device()): + ), patch( + "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", + return_value=get_mock_device(), + ): result = await hass.config_entries.flow.async_configure( flow["flow_id"], user_input={"ip_address": "192.168.43.183"} ) @@ -113,7 +125,10 @@ async def test_config_flow_imports_entry(aioclient_mock, hass): mock_entry = MockConfigEntry(domain="homewizard_energy", data={"host": "1.2.3.4"}) mock_entry.add_to_hass(hass) - with patch("aiohwenergy.HomeWizardEnergy", return_value=device,), patch( + with patch( + "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", + return_value=device, + ), patch( "homeassistant.components.homewizard.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -127,11 +142,11 @@ async def test_config_flow_imports_entry(aioclient_mock, hass): ) assert result["type"] == "create_entry" - assert result["title"] == f"{device.device.product_name} (aabbccddeeff)" + assert result["title"] == "P1 meter (aabbccddeeff)" assert result["data"][CONF_IP_ADDRESS] == "1.2.3.4" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert len(device.initialize.mock_calls) == 1 + assert len(device.device.mock_calls) == 1 assert len(device.close.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -166,7 +181,10 @@ async def test_discovery_disabled_api(hass, aioclient_mock): with patch( "homeassistant.components.homewizard.async_setup_entry", return_value=True, - ), patch("aiohwenergy.HomeWizardEnergy", return_value=get_mock_device()): + ), patch( + "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", + return_value=get_mock_device(), + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"ip_address": "192.168.43.183"} ) @@ -240,7 +258,7 @@ async def test_check_disabled_api(hass, aioclient_mock): raise DisabledError device = get_mock_device() - device.initialize.side_effect = mock_initialize + device.device.side_effect = mock_initialize result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -250,7 +268,7 @@ async def test_check_disabled_api(hass, aioclient_mock): assert result["step_id"] == "user" with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", return_value=device, ): result = await hass.config_entries.flow.async_configure( @@ -268,7 +286,7 @@ async def test_check_error_handling_api(hass, aioclient_mock): raise Exception() device = get_mock_device() - device.initialize.side_effect = mock_initialize + device.device.side_effect = mock_initialize result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -278,32 +296,7 @@ async def test_check_error_handling_api(hass, aioclient_mock): assert result["step_id"] == "user" with patch( - "aiohwenergy.HomeWizardEnergy", - return_value=device, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "unknown_error" - - -async def test_check_detects_unexpected_api_response(hass, aioclient_mock): - """Test check detecting device endpoint failed fetching data.""" - - device = get_mock_device() - device.device = None - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] == "form" - assert result["step_id"] == "user" - - with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", return_value=device, ): result = await hass.config_entries.flow.async_configure( @@ -317,8 +310,11 @@ async def test_check_detects_unexpected_api_response(hass, aioclient_mock): async def test_check_detects_invalid_api(hass, aioclient_mock): """Test check detecting device endpoint failed fetching data.""" + def mock_initialize(): + raise UnsupportedError + device = get_mock_device() - device.device.api_version = "not_v1" + device.device.side_effect = mock_initialize result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -328,7 +324,7 @@ async def test_check_detects_invalid_api(hass, aioclient_mock): assert result["step_id"] == "user" with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", return_value=device, ): result = await hass.config_entries.flow.async_configure( @@ -337,27 +333,3 @@ async def test_check_detects_invalid_api(hass, aioclient_mock): assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "unsupported_api_version" - - -async def test_check_detects_unsuported_device(hass, aioclient_mock): - """Test check detecting device endpoint failed fetching data.""" - - device = get_mock_device(product_type="not_an_energy_device") - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] == "form" - assert result["step_id"] == "user" - - with patch( - "aiohwenergy.HomeWizardEnergy", - return_value=device, - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "device_not_supported" diff --git a/tests/components/homewizard/test_init.py b/tests/components/homewizard/test_init.py index 984a5431004..c7cc5bd7bdb 100644 --- a/tests/components/homewizard/test_init.py +++ b/tests/components/homewizard/test_init.py @@ -2,7 +2,7 @@ from asyncio import TimeoutError from unittest.mock import patch -from aiohwenergy import AiohwenergyException, DisabledError +from homewizard_energy.errors import DisabledError, HomeWizardEnergyException from homeassistant import config_entries from homeassistant.components.homewizard.const import DOMAIN @@ -28,7 +28,7 @@ async def test_load_unload(aioclient_mock, hass): entry.add_to_hass(hass) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=device, ): await hass.config_entries.async_setup(entry.entry_id) @@ -50,7 +50,7 @@ async def test_load_failed_host_unavailable(aioclient_mock, hass): raise TimeoutError() device = get_mock_device() - device.initialize.side_effect = MockInitialize + device.device.side_effect = MockInitialize entry = MockConfigEntry( domain=DOMAIN, @@ -60,7 +60,7 @@ async def test_load_failed_host_unavailable(aioclient_mock, hass): entry.add_to_hass(hass) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=device, ): await hass.config_entries.async_setup(entry.entry_id) @@ -127,7 +127,7 @@ async def test_init_accepts_and_migrates_old_entry(aioclient_mock, hass): # Add the entry_id to trigger migration with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=device, ): await hass.config_entries.async_setup(imported_entry.entry_id) @@ -168,7 +168,7 @@ async def test_load_detect_api_disabled(aioclient_mock, hass): raise DisabledError() device = get_mock_device() - device.initialize.side_effect = MockInitialize + device.device.side_effect = MockInitialize entry = MockConfigEntry( domain=DOMAIN, @@ -178,24 +178,24 @@ async def test_load_detect_api_disabled(aioclient_mock, hass): entry.add_to_hass(hass) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=device, ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state is ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_RETRY -async def test_load_handles_aiohwenergy_exception(aioclient_mock, hass): +async def test_load_handles_homewizardenergy_exception(aioclient_mock, hass): """Test setup handles exception from API.""" def MockInitialize(): - raise AiohwenergyException() + raise HomeWizardEnergyException() device = get_mock_device() - device.initialize.side_effect = MockInitialize + device.device.side_effect = MockInitialize entry = MockConfigEntry( domain=DOMAIN, @@ -205,7 +205,7 @@ async def test_load_handles_aiohwenergy_exception(aioclient_mock, hass): entry.add_to_hass(hass) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=device, ): await hass.config_entries.async_setup(entry.entry_id) @@ -222,7 +222,7 @@ async def test_load_handles_generic_exception(aioclient_mock, hass): raise Exception() device = get_mock_device() - device.initialize.side_effect = MockInitialize + device.device.side_effect = MockInitialize entry = MockConfigEntry( domain=DOMAIN, @@ -232,7 +232,7 @@ async def test_load_handles_generic_exception(aioclient_mock, hass): entry.add_to_hass(hass) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=device, ): await hass.config_entries.async_setup(entry.entry_id) @@ -256,7 +256,7 @@ async def test_load_handles_initialization_error(aioclient_mock, hass): entry.add_to_hass(hass) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=device, ): await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index c1a98c07108..8195aa11708 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -3,7 +3,8 @@ from datetime import timedelta from unittest.mock import AsyncMock, patch -from aiohwenergy.errors import DisabledError +from homewizard_energy.errors import DisabledError, RequestError +from homewizard_energy.models import Data from homeassistant.components.sensor import ( ATTR_STATE_CLASS, @@ -36,13 +37,10 @@ async def test_sensor_entity_smr_version( """Test entity loads smr version.""" api = get_mock_device() - api.data.available_datapoints = [ - "smr_version", - ] - api.data.smr_version = 50 + api.data = AsyncMock(return_value=Data.from_dict({"smr_version": 50})) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -77,13 +75,10 @@ async def test_sensor_entity_meter_model( """Test entity loads meter model.""" api = get_mock_device() - api.data.available_datapoints = [ - "meter_model", - ] - api.data.meter_model = "Model X" + api.data = AsyncMock(return_value=Data.from_dict({"meter_model": "Model X"})) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -118,13 +113,10 @@ async def test_sensor_entity_wifi_ssid(hass, mock_config_entry_data, mock_config """Test entity loads wifi ssid.""" api = get_mock_device() - api.data.available_datapoints = [ - "wifi_ssid", - ] - api.data.wifi_ssid = "My Wifi" + api.data = AsyncMock(return_value=Data.from_dict({"wifi_ssid": "My Wifi"})) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -159,13 +151,10 @@ async def test_sensor_entity_wifi_strength( """Test entity loads wifi strength.""" api = get_mock_device() - api.data.available_datapoints = [ - "wifi_strength", - ] - api.data.wifi_strength = 42 + api.data = AsyncMock(return_value=Data.from_dict({"wifi_strength": 42})) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -189,13 +178,12 @@ async def test_sensor_entity_total_power_import_t1_kwh( """Test entity loads total power import t1.""" api = get_mock_device() - api.data.available_datapoints = [ - "total_power_import_t1_kwh", - ] - api.data.total_power_import_t1_kwh = 1234.123 + api.data = AsyncMock( + return_value=Data.from_dict({"total_power_import_t1_kwh": 1234.123}) + ) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -232,13 +220,12 @@ async def test_sensor_entity_total_power_import_t2_kwh( """Test entity loads total power import t2.""" api = get_mock_device() - api.data.available_datapoints = [ - "total_power_import_t2_kwh", - ] - api.data.total_power_import_t2_kwh = 1234.123 + api.data = AsyncMock( + return_value=Data.from_dict({"total_power_import_t2_kwh": 1234.123}) + ) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -275,13 +262,12 @@ async def test_sensor_entity_total_power_export_t1_kwh( """Test entity loads total power export t1.""" api = get_mock_device() - api.data.available_datapoints = [ - "total_power_export_t1_kwh", - ] - api.data.total_power_export_t1_kwh = 1234.123 + api.data = AsyncMock( + return_value=Data.from_dict({"total_power_export_t1_kwh": 1234.123}) + ) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -318,13 +304,12 @@ async def test_sensor_entity_total_power_export_t2_kwh( """Test entity loads total power export t2.""" api = get_mock_device() - api.data.available_datapoints = [ - "total_power_export_t2_kwh", - ] - api.data.total_power_export_t2_kwh = 1234.123 + api.data = AsyncMock( + return_value=Data.from_dict({"total_power_export_t2_kwh": 1234.123}) + ) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -361,13 +346,10 @@ async def test_sensor_entity_active_power( """Test entity loads active power.""" api = get_mock_device() - api.data.available_datapoints = [ - "active_power_w", - ] - api.data.active_power_w = 123.123 + api.data = AsyncMock(return_value=Data.from_dict({"active_power_w": 123.123})) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -402,13 +384,10 @@ async def test_sensor_entity_active_power_l1( """Test entity loads active power l1.""" api = get_mock_device() - api.data.available_datapoints = [ - "active_power_l1_w", - ] - api.data.active_power_l1_w = 123.123 + api.data = AsyncMock(return_value=Data.from_dict({"active_power_l1_w": 123.123})) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -445,13 +424,10 @@ async def test_sensor_entity_active_power_l2( """Test entity loads active power l2.""" api = get_mock_device() - api.data.available_datapoints = [ - "active_power_l2_w", - ] - api.data.active_power_l2_w = 456.456 + api.data = AsyncMock(return_value=Data.from_dict({"active_power_l2_w": 456.456})) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -488,13 +464,10 @@ async def test_sensor_entity_active_power_l3( """Test entity loads active power l3.""" api = get_mock_device() - api.data.available_datapoints = [ - "active_power_l3_w", - ] - api.data.active_power_l3_w = 789.789 + api.data = AsyncMock(return_value=Data.from_dict({"active_power_l3_w": 789.789})) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -529,13 +502,10 @@ async def test_sensor_entity_total_gas(hass, mock_config_entry_data, mock_config """Test entity loads total gas.""" api = get_mock_device() - api.data.available_datapoints = [ - "total_gas_m3", - ] - api.data.total_gas_m3 = 50 + api.data = AsyncMock(return_value=Data.from_dict({"total_gas_m3": 50})) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -570,17 +540,14 @@ async def test_sensor_entity_disabled_when_null( """Test sensor disables data with null by default.""" api = get_mock_device() - api.data.available_datapoints = [ - "active_power_l2_w", - "active_power_l3_w", - "total_gas_m3", - ] - api.data.active_power_l2_w = None - api.data.active_power_l3_w = None - api.data.total_gas_m3 = None + api.data = AsyncMock( + return_value=Data.from_dict( + {"active_power_l2_w": None, "active_power_l3_w": None, "total_gas_m3": None} + ) + ) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -612,15 +579,14 @@ async def test_sensor_entity_export_disabled_when_unused( """Test sensor disables export if value is 0.""" api = get_mock_device() - api.data.available_datapoints = [ - "total_power_export_t1_kwh", - "total_power_export_t2_kwh", - ] - api.data.total_power_export_t1_kwh = 0 - api.data.total_power_export_t2_kwh = 0 + api.data = AsyncMock( + return_value=Data.from_dict( + {"total_power_export_t1_kwh": 0, "total_power_export_t2_kwh": 0} + ) + ) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -649,17 +615,14 @@ async def test_sensors_unreachable(hass, mock_config_entry_data, mock_config_ent """Test sensor handles api unreachable.""" api = get_mock_device() - api.data.available_datapoints = [ - "total_power_import_t1_kwh", - ] - api.data.total_power_import_t1_kwh = 1234.123 + api.data = AsyncMock( + return_value=Data.from_dict({"total_power_import_t1_kwh": 1234.123}) + ) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): - api.update = AsyncMock(return_value=True) - entry = mock_config_entry entry.data = mock_config_entry_data entry.add_to_hass(hass) @@ -675,7 +638,7 @@ async def test_sensors_unreachable(hass, mock_config_entry_data, mock_config_ent == "1234.123" ) - api.update = AsyncMock(return_value=False) + api.data.side_effect = RequestError async_fire_time_changed(hass, utcnow + timedelta(seconds=5)) await hass.async_block_till_done() assert ( @@ -685,7 +648,7 @@ async def test_sensors_unreachable(hass, mock_config_entry_data, mock_config_ent == "unavailable" ) - api.update = AsyncMock(return_value=True) + api.data.side_effect = None async_fire_time_changed(hass, utcnow + timedelta(seconds=10)) await hass.async_block_till_done() assert ( @@ -700,17 +663,14 @@ async def test_api_disabled(hass, mock_config_entry_data, mock_config_entry): """Test sensor handles api unreachable.""" api = get_mock_device() - api.data.available_datapoints = [ - "total_power_import_t1_kwh", - ] - api.data.total_power_import_t1_kwh = 1234.123 + api.data = AsyncMock( + return_value=Data.from_dict({"total_power_import_t1_kwh": 1234.123}) + ) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): - api.update = AsyncMock(return_value=True) - entry = mock_config_entry entry.data = mock_config_entry_data entry.add_to_hass(hass) @@ -726,7 +686,7 @@ async def test_api_disabled(hass, mock_config_entry_data, mock_config_entry): == "1234.123" ) - api.update = AsyncMock(side_effect=DisabledError) + api.data.side_effect = DisabledError async_fire_time_changed(hass, utcnow + timedelta(seconds=5)) await hass.async_block_till_done() assert ( @@ -736,7 +696,7 @@ async def test_api_disabled(hass, mock_config_entry_data, mock_config_entry): == "unavailable" ) - api.update = AsyncMock(return_value=True) + api.data.side_effect = None async_fire_time_changed(hass, utcnow + timedelta(seconds=10)) await hass.async_block_till_done() assert ( diff --git a/tests/components/homewizard/test_switch.py b/tests/components/homewizard/test_switch.py index f3792a9d75b..118f0774a47 100644 --- a/tests/components/homewizard/test_switch.py +++ b/tests/components/homewizard/test_switch.py @@ -2,6 +2,8 @@ from unittest.mock import AsyncMock, patch +from homewizard_energy.models import State + from homeassistant.components import switch from homeassistant.components.switch import DEVICE_CLASS_OUTLET, DEVICE_CLASS_SWITCH from homeassistant.const import ( @@ -27,7 +29,7 @@ async def test_switch_entity_not_loaded_when_not_available( api = get_mock_device() with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -48,13 +50,12 @@ async def test_switch_loads_entities(hass, mock_config_entry_data, mock_config_e """Test entity loads smr version.""" api = get_mock_device() - api.state = AsyncMock() - - api.state.power_on = False - api.state.switch_lock = False + api.state = AsyncMock( + return_value=State.from_dict({"power_on": False, "switch_lock": False}) + ) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -104,17 +105,19 @@ async def test_switch_power_on_off(hass, mock_config_entry_data, mock_config_ent """Test entity turns switch on and off.""" api = get_mock_device() - api.state = AsyncMock() - api.state.power_on = False - api.state.switch_lock = False + api.state = AsyncMock( + return_value=State.from_dict({"power_on": False, "switch_lock": False}) + ) - def set_power_on(power_on): - api.state.power_on = power_on + def state_set(power_on): + api.state = AsyncMock( + return_value=State.from_dict({"power_on": power_on, "switch_lock": False}) + ) - api.state.set = AsyncMock(side_effect=set_power_on) + api.state_set = AsyncMock(side_effect=state_set) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -138,7 +141,7 @@ async def test_switch_power_on_off(hass, mock_config_entry_data, mock_config_ent ) await hass.async_block_till_done() - assert len(api.state.set.mock_calls) == 1 + assert len(api.state_set.mock_calls) == 1 assert ( hass.states.get("switch.product_name_aabbccddeeff_switch").state == STATE_ON ) @@ -156,7 +159,7 @@ async def test_switch_power_on_off(hass, mock_config_entry_data, mock_config_ent hass.states.get("switch.product_name_aabbccddeeff_switch").state == STATE_OFF ) - assert len(api.state.set.mock_calls) == 2 + assert len(api.state_set.mock_calls) == 2 async def test_switch_lock_power_on_off( @@ -165,17 +168,19 @@ async def test_switch_lock_power_on_off( """Test entity turns switch on and off.""" api = get_mock_device() - api.state = AsyncMock() - api.state.power_on = False - api.state.switch_lock = False + api.state = AsyncMock( + return_value=State.from_dict({"power_on": False, "switch_lock": False}) + ) - def set_switch_lock(switch_lock): - api.state.switch_lock = switch_lock + def state_set(switch_lock): + api.state = AsyncMock( + return_value=State.from_dict({"power_on": True, "switch_lock": switch_lock}) + ) - api.state.set = AsyncMock(side_effect=set_switch_lock) + api.state_set = AsyncMock(side_effect=state_set) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -199,7 +204,7 @@ async def test_switch_lock_power_on_off( ) await hass.async_block_till_done() - assert len(api.state.set.mock_calls) == 1 + assert len(api.state_set.mock_calls) == 1 assert ( hass.states.get("switch.product_name_aabbccddeeff_switch_lock").state == STATE_ON @@ -218,7 +223,7 @@ async def test_switch_lock_power_on_off( hass.states.get("switch.product_name_aabbccddeeff_switch_lock").state == STATE_OFF ) - assert len(api.state.set.mock_calls) == 2 + assert len(api.state_set.mock_calls) == 2 async def test_switch_lock_sets_power_on_unavailable( @@ -227,17 +232,19 @@ async def test_switch_lock_sets_power_on_unavailable( """Test entity turns switch on and off.""" api = get_mock_device() - api.state = AsyncMock() - api.state.power_on = True - api.state.switch_lock = False + api.state = AsyncMock( + return_value=State.from_dict({"power_on": True, "switch_lock": False}) + ) - def set_switch_lock(switch_lock): - api.state.switch_lock = switch_lock + def state_set(switch_lock): + api.state = AsyncMock( + return_value=State.from_dict({"power_on": True, "switch_lock": switch_lock}) + ) - api.state.set = AsyncMock(side_effect=set_switch_lock) + api.state_set = AsyncMock(side_effect=state_set) with patch( - "aiohwenergy.HomeWizardEnergy", + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", return_value=api, ): entry = mock_config_entry @@ -264,7 +271,7 @@ async def test_switch_lock_sets_power_on_unavailable( ) await hass.async_block_till_done() - assert len(api.state.set.mock_calls) == 1 + assert len(api.state_set.mock_calls) == 1 assert ( hass.states.get("switch.product_name_aabbccddeeff_switch").state == STATE_UNAVAILABLE @@ -290,4 +297,4 @@ async def test_switch_lock_sets_power_on_unavailable( hass.states.get("switch.product_name_aabbccddeeff_switch_lock").state == STATE_OFF ) - assert len(api.state.set.mock_calls) == 2 + assert len(api.state_set.mock_calls) == 2 From 1e4690626ff7e0e978805d9fa7b4dc7270811711 Mon Sep 17 00:00:00 2001 From: rappenze Date: Wed, 25 May 2022 09:07:55 +0200 Subject: [PATCH 0885/3516] Better detection for brightness support in fibaro light (#71615) --- homeassistant/components/fibaro/light.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/fibaro/light.py b/homeassistant/components/fibaro/light.py index 0115e0301c3..300b7c4c5f8 100644 --- a/homeassistant/components/fibaro/light.py +++ b/homeassistant/components/fibaro/light.py @@ -86,7 +86,10 @@ class FibaroLight(FibaroDevice, LightEntity): or "RGBW" in fibaro_device.type or "rgbw" in fibaro_device.type ) - supports_dimming = "levelChange" in fibaro_device.interfaces + supports_dimming = ( + "levelChange" in fibaro_device.interfaces + and "setValue" in fibaro_device.actions + ) if supports_color and supports_white_v: self._attr_supported_color_modes = {ColorMode.RGBW} From 804c88809886823128d1cbcb385716a493e5ec1a Mon Sep 17 00:00:00 2001 From: Lars Date: Wed, 25 May 2022 09:28:36 +0200 Subject: [PATCH 0886/3516] Free color selection for Fritz!Smarthome lights (#66213) * Fritz light free color selection * Use setcolor as fallback * better debug log message Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> * change if-clause Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- homeassistant/components/fritzbox/light.py | 40 ++++++++++++++++------ tests/components/fritzbox/test_light.py | 28 ++++++++++++++- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/fritzbox/light.py b/homeassistant/components/fritzbox/light.py index 0edcb8ee260..e01fb76ec11 100644 --- a/homeassistant/components/fritzbox/light.py +++ b/homeassistant/components/fritzbox/light.py @@ -3,6 +3,8 @@ from __future__ import annotations from typing import Any +from requests.exceptions import HTTPError + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -21,6 +23,7 @@ from .const import ( COLOR_TEMP_MODE, CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN, + LOGGER, ) from .coordinator import FritzboxDataUpdateCoordinator @@ -135,16 +138,33 @@ class FritzboxLight(FritzBoxEntity, LightEntity): 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) - ) + # Try setunmappedcolor first. This allows free color selection, + # but we don't know if its supported by all devices. + try: + # HA gives 0..360 for hue, fritz light only supports 0..359 + unmapped_hue = int(kwargs[ATTR_HS_COLOR][0] % 360) + unmapped_saturation = round(kwargs[ATTR_HS_COLOR][1] * 255.0 / 100.0) + await self.hass.async_add_executor_job( + self.device.set_unmapped_color, (unmapped_hue, unmapped_saturation) + ) + # This will raise 400 BAD REQUEST if the setunmappedcolor is not available + except HTTPError as err: + if err.response.status_code != 400: + raise + LOGGER.debug( + "fritzbox does not support method 'setunmappedcolor', fallback to 'setcolor'" + ) + # find supported hs values closest to what user selected + hue = min( + self._supported_hs.keys(), key=lambda x: abs(x - unmapped_hue) + ) + saturation = min( + self._supported_hs[hue], + key=lambda x: abs(x - unmapped_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]) diff --git a/tests/components/fritzbox/test_light.py b/tests/components/fritzbox/test_light.py index b63d19da7e0..0759beb6849 100644 --- a/tests/components/fritzbox/test_light.py +++ b/tests/components/fritzbox/test_light.py @@ -113,7 +113,34 @@ async def test_turn_on_color(hass: HomeAssistant, fritz: Mock): 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_unmapped_color.call_count == 1 + +async def test_turn_on_color_unsupported_api_method(hass: HomeAssistant, fritz: Mock): + """Test turn device on in mapped color mode if unmapped is not supported.""" + 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")] + } + mockresponse = Mock() + mockresponse.status_code = 400 + + error = HTTPError("Bad Request") + error.response = mockresponse + device.set_unmapped_color.side_effect = error + + 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, @@ -135,7 +162,6 @@ async def test_turn_off(hass: HomeAssistant, fritz: Mock): 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 ) From 1d5b8746feeb331e1e77fef46efa1294eda88e4b Mon Sep 17 00:00:00 2001 From: Bryton Hall Date: Wed, 25 May 2022 03:51:19 -0400 Subject: [PATCH 0887/3516] Add co2 and iaq entities to venstar component (#71467) --- homeassistant/components/venstar/sensor.py | 38 ++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/venstar/sensor.py b/homeassistant/components/venstar/sensor.py index 824774f3e31..723b26d1956 100644 --- a/homeassistant/components/venstar/sensor.py +++ b/homeassistant/components/venstar/sensor.py @@ -12,7 +12,13 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT, TIME_MINUTES +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, + PERCENTAGE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TIME_MINUTES, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -58,7 +64,7 @@ class VenstarSensorTypeMixin: value_fn: Callable[[Any, Any], Any] name_fn: Callable[[Any, Any], str] - uom_fn: Callable[[Any], str] + uom_fn: Callable[[Any], str | None] @dataclass @@ -140,7 +146,7 @@ class VenstarSensor(VenstarEntity, SensorEntity): return self.entity_description.value_fn(self.coordinator, self.sensor_name) @property - def native_unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str | None: """Return unit of measurement the value is expressed in.""" return self.entity_description.uom_fn(self.coordinator) @@ -150,7 +156,7 @@ SENSOR_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( key="hum", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, - uom_fn=lambda coordinator: PERCENTAGE, + uom_fn=lambda _: PERCENTAGE, value_fn=lambda coordinator, sensor_name: coordinator.client.get_sensor( sensor_name, "hum" ), @@ -166,11 +172,31 @@ SENSOR_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( ), name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {sensor_name.replace(' Temp', '')} Temperature", ), + VenstarSensorEntityDescription( + key="co2", + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, + uom_fn=lambda _: CONCENTRATION_PARTS_PER_MILLION, + value_fn=lambda coordinator, sensor_name: coordinator.client.get_sensor( + sensor_name, "co2" + ), + name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {sensor_name} CO2", + ), + VenstarSensorEntityDescription( + key="iaq", + device_class=SensorDeviceClass.AQI, + state_class=SensorStateClass.MEASUREMENT, + uom_fn=lambda _: None, + value_fn=lambda coordinator, sensor_name: coordinator.client.get_sensor( + sensor_name, "iaq" + ), + name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {sensor_name} IAQ", + ), VenstarSensorEntityDescription( key="battery", device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, - uom_fn=lambda coordinator: PERCENTAGE, + uom_fn=lambda _: PERCENTAGE, value_fn=lambda coordinator, sensor_name: coordinator.client.get_sensor( sensor_name, "battery" ), @@ -181,7 +207,7 @@ SENSOR_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( RUNTIME_ENTITY = VenstarSensorEntityDescription( key="runtime", state_class=SensorStateClass.MEASUREMENT, - uom_fn=lambda coordinator: TIME_MINUTES, + uom_fn=lambda _: TIME_MINUTES, value_fn=lambda coordinator, sensor_name: coordinator.runtimes[-1][sensor_name], name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {RUNTIME_ATTRIBUTES[sensor_name]} Runtime", ) From cc7b6244181cb68931c324b960d81a2f22f1cc53 Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Wed, 25 May 2022 09:57:40 +0200 Subject: [PATCH 0888/3516] Improve AndroidTV typing (#71036) * Improve AndroidTV typing * Change input type in async_connect_androidtv --- .../components/androidtv/__init__.py | 30 ++++-- .../components/androidtv/config_flow.py | 96 ++++++++++++------- 2 files changed, 83 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py index c1875a883e3..8a34b8aa858 100644 --- a/homeassistant/components/androidtv/__init__.py +++ b/homeassistant/components/androidtv/__init__.py @@ -1,9 +1,17 @@ """Support for functionality to interact with Android TV/Fire TV devices.""" +from __future__ import annotations + +from collections.abc import Mapping import os +from typing import Any from adb_shell.auth.keygen import keygen -from androidtv.adb_manager.adb_manager_sync import ADBPythonSync -from androidtv.setup_async import setup as async_androidtv_setup +from androidtv.adb_manager.adb_manager_sync import ADBPythonSync, PythonRSASigner +from androidtv.setup_async import ( + AndroidTVAsync, + FireTVAsync, + setup as async_androidtv_setup, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -41,7 +49,7 @@ RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES] _INVALID_MACS = {"ff:ff:ff:ff:ff:ff"} -def get_androidtv_mac(dev_props): +def get_androidtv_mac(dev_props: dict[str, Any]) -> str | None: """Return formatted mac from device properties.""" for prop_mac in (PROP_ETHMAC, PROP_WIFIMAC): if if_mac := dev_props.get(prop_mac): @@ -51,9 +59,13 @@ def get_androidtv_mac(dev_props): return None -def _setup_androidtv(hass, config): +def _setup_androidtv( + hass: HomeAssistant, config: dict[str, Any] +) -> tuple[str, PythonRSASigner | None, str]: """Generate an ADB key (if needed) and load it.""" - adbkey = config.get(CONF_ADBKEY, hass.config.path(STORAGE_DIR, "androidtv_adbkey")) + adbkey: str = config.get( + CONF_ADBKEY, hass.config.path(STORAGE_DIR, "androidtv_adbkey") + ) if CONF_ADB_SERVER_IP not in config: # Use "adb_shell" (Python ADB implementation) if not os.path.isfile(adbkey): @@ -73,8 +85,12 @@ def _setup_androidtv(hass, config): async def async_connect_androidtv( - hass, config, *, state_detection_rules=None, timeout=30.0 -): + hass: HomeAssistant, + config: Mapping[str, Any], + *, + state_detection_rules: dict[str, Any] | None = None, + timeout: float = 30.0, +) -> tuple[AndroidTVAsync | FireTVAsync | None, str | None]: """Connect to Android device.""" address = f"{config[CONF_HOST]}:{config[CONF_PORT]}" diff --git a/homeassistant/components/androidtv/config_flow.py b/homeassistant/components/androidtv/config_flow.py index 9df87eaffe2..bdc067c4275 100644 --- a/homeassistant/components/androidtv/config_flow.py +++ b/homeassistant/components/androidtv/config_flow.py @@ -1,14 +1,18 @@ """Config flow to configure the Android TV integration.""" +from __future__ import annotations + import json import logging import os +from typing import Any from androidtv import state_detection_rules_validator import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_DEVICE_CLASS, CONF_HOST, CONF_PORT from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from . import async_connect_androidtv, get_androidtv_mac @@ -51,24 +55,28 @@ RESULT_UNKNOWN = "unknown" _LOGGER = logging.getLogger(__name__) -def _is_file(value): +def _is_file(value: str) -> bool: """Validate that the value is an existing file.""" - file_in = os.path.expanduser(str(value)) + file_in = os.path.expanduser(value) return os.path.isfile(file_in) and os.access(file_in, os.R_OK) -class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class AndroidTVFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 @callback - def _show_setup_form(self, user_input=None, error=None): + def _show_setup_form( + self, + user_input: dict[str, Any] | None = None, + error: str | None = None, + ) -> FlowResult: """Show the setup form to the user.""" - user_input = user_input or {} + host = user_input.get(CONF_HOST, "") if user_input else "" data_schema = vol.Schema( { - vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, + vol.Required(CONF_HOST, default=host): str, vol.Required(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): vol.In( DEVICE_CLASSES ), @@ -90,10 +98,12 @@ class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=data_schema, - errors={"base": error}, + errors={"base": error} if error else None, ) - async def _async_check_connection(self, user_input): + async def _async_check_connection( + self, user_input: dict[str, Any] + ) -> tuple[str | None, str | None]: """Attempt to connect the Android TV.""" try: @@ -121,7 +131,9 @@ class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await aftv.adb_close() return None, unique_id - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" error = None @@ -152,32 +164,31 @@ class AndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data=user_input, ) - user_input = user_input or {} return self._show_setup_form(user_input, error) @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle an option flow for Android TV.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry apps = config_entry.options.get(CONF_APPS, {}) det_rules = config_entry.options.get(CONF_STATE_DETECTION_RULES, {}) - self._apps = apps.copy() - self._state_det_rules = det_rules.copy() - self._conf_app_id = None - self._conf_rule_id = None + self._apps: dict[str, Any] = apps.copy() + self._state_det_rules: dict[str, Any] = det_rules.copy() + self._conf_app_id: str | None = None + self._conf_rule_id: str | None = None @callback - def _save_config(self, data): + def _save_config(self, data: dict[str, Any]) -> FlowResult: """Save the updated options.""" new_data = { k: v @@ -191,7 +202,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_create_entry(title="", data=new_data) - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle options flow.""" if user_input is not None: if sel_app := user_input.get(CONF_APPS): @@ -203,7 +216,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self._async_init_form() @callback - def _async_init_form(self): + def _async_init_form(self) -> FlowResult: """Return initial configuration form.""" apps_list = {k: f"{v} ({k})" if v else k for k, v in self._apps.items()} @@ -246,7 +259,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_show_form(step_id="init", data_schema=data_schema) - async def async_step_apps(self, user_input=None, app_id=None): + async def async_step_apps( + self, user_input: dict[str, Any] | None = None, app_id: str | None = None + ) -> FlowResult: """Handle options flow for apps list.""" if app_id is not None: self._conf_app_id = app_id if app_id != APPS_NEW_ID else None @@ -263,28 +278,32 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return await self.async_step_init() @callback - def _async_apps_form(self, app_id): + def _async_apps_form(self, app_id: str) -> FlowResult: """Return configuration form for apps.""" - data_schema = { + app_schema = { vol.Optional( CONF_APP_NAME, description={"suggested_value": self._apps.get(app_id, "")}, ): str, } if app_id == APPS_NEW_ID: - data_schema[vol.Optional(CONF_APP_ID)] = str + data_schema = vol.Schema({**app_schema, vol.Optional(CONF_APP_ID): str}) else: - data_schema[vol.Optional(CONF_APP_DELETE, default=False)] = bool + data_schema = vol.Schema( + {**app_schema, vol.Optional(CONF_APP_DELETE, default=False): bool} + ) return self.async_show_form( step_id="apps", - data_schema=vol.Schema(data_schema), + data_schema=data_schema, description_placeholders={ "app_id": f"`{app_id}`" if app_id != APPS_NEW_ID else "", }, ) - async def async_step_rules(self, user_input=None, rule_id=None): + async def async_step_rules( + self, user_input: dict[str, Any] | None = None, rule_id: str | None = None + ) -> FlowResult: """Handle options flow for detection rules.""" if rule_id is not None: self._conf_rule_id = rule_id if rule_id != RULES_NEW_ID else None @@ -308,21 +327,26 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return await self.async_step_init() @callback - def _async_rules_form(self, rule_id, default_id="", errors=None): + def _async_rules_form( + self, rule_id: str, default_id: str = "", errors: dict[str, str] | None = None + ) -> FlowResult: """Return configuration form for detection rules.""" state_det_rule = self._state_det_rules.get(rule_id) str_det_rule = json.dumps(state_det_rule) if state_det_rule else "" - data_schema = {} + rule_schema = {vol.Optional(CONF_RULE_VALUES, default=str_det_rule): str} if rule_id == RULES_NEW_ID: - data_schema[vol.Optional(CONF_RULE_ID, default=default_id)] = str - data_schema[vol.Optional(CONF_RULE_VALUES, default=str_det_rule)] = str - if rule_id != RULES_NEW_ID: - data_schema[vol.Optional(CONF_RULE_DELETE, default=False)] = bool + data_schema = vol.Schema( + {vol.Optional(CONF_RULE_ID, default=default_id): str, **rule_schema} + ) + else: + data_schema = vol.Schema( + {**rule_schema, vol.Optional(CONF_RULE_DELETE, default=False): bool} + ) return self.async_show_form( step_id="rules", - data_schema=vol.Schema(data_schema), + data_schema=data_schema, description_placeholders={ "rule_id": f"`{rule_id}`" if rule_id != RULES_NEW_ID else "", }, @@ -330,7 +354,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ) -def _validate_state_det_rules(state_det_rules): +def _validate_state_det_rules(state_det_rules: str) -> list[Any] | None: """Validate a string that contain state detection rules and return a dict.""" try: json_rules = json.loads(state_det_rules) From 72cb320ed769275424b739bf00890d4d33451f5c Mon Sep 17 00:00:00 2001 From: Abadede Date: Wed, 25 May 2022 10:33:05 +0200 Subject: [PATCH 0889/3516] Fix Hue SONOFF S31 Lite zb plug (#69589) * Update light.py Same issue as https://github.com/home-assistant/core/issues/46619 with SONOFF S13 Lite Zigbee plug. * Update light.py --- homeassistant/components/hue/v1/light.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hue/v1/light.py b/homeassistant/components/hue/v1/light.py index 420256f421b..74fe25dafcf 100644 --- a/homeassistant/components/hue/v1/light.py +++ b/homeassistant/components/hue/v1/light.py @@ -341,6 +341,7 @@ class HueLight(CoordinatorEntity, LightEntity): self.is_innr = False self.is_ewelink = False self.is_livarno = False + self.is_s31litezb = False self.gamut_typ = GAMUT_TYPE_UNAVAILABLE self.gamut = None else: @@ -349,6 +350,7 @@ class HueLight(CoordinatorEntity, LightEntity): self.is_innr = light.manufacturername == "innr" self.is_ewelink = light.manufacturername == "eWeLink" self.is_livarno = light.manufacturername.startswith("_TZ3000_") + self.is_s31litezb = light.modelid == "S31 Lite zb" self.gamut_typ = self.light.colorgamuttype self.gamut = self.light.colorgamut LOGGER.debug("Color gamut of %s: %s", self.name, str(self.gamut)) @@ -554,7 +556,12 @@ class HueLight(CoordinatorEntity, LightEntity): elif flash == FLASH_SHORT: command["alert"] = "select" del command["on"] - elif not self.is_innr and not self.is_ewelink and not self.is_livarno: + elif ( + not self.is_innr + and not self.is_ewelink + and not self.is_livarno + and not self.is_s31litezb + ): command["alert"] = "none" if ATTR_EFFECT in kwargs: From bf75cb3cc5620d9996bf9534c234d855c1c762b7 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Wed, 25 May 2022 10:36:09 +0200 Subject: [PATCH 0890/3516] Improve tests of devolo_home_network (#71873) * Improve tests * Use entity_registry_enabled_by_default fixture --- .../devolo_home_network/test_binary_sensor.py | 10 +- .../devolo_home_network/test_sensor.py | 104 ++++++++---------- 2 files changed, 48 insertions(+), 66 deletions(-) diff --git a/tests/components/devolo_home_network/test_binary_sensor.py b/tests/components/devolo_home_network/test_binary_sensor.py index 3d181da6106..564e6e5ade6 100644 --- a/tests/components/devolo_home_network/test_binary_sensor.py +++ b/tests/components/devolo_home_network/test_binary_sensor.py @@ -21,7 +21,7 @@ from .const import PLCNET_ATTACHED from tests.common import async_fire_time_changed -@pytest.mark.usefixtures("mock_device", "mock_zeroconf") +@pytest.mark.usefixtures("mock_device") async def test_binary_sensor_setup(hass: HomeAssistant): """Test default setup of the binary sensor component.""" entry = configure_integration(hass) @@ -33,7 +33,7 @@ async def test_binary_sensor_setup(hass: HomeAssistant): await hass.config_entries.async_unload(entry.entry_id) -@pytest.mark.usefixtures("mock_device", "mock_zeroconf") +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_device") async def test_update_attached_to_router(hass: HomeAssistant): """Test state change of a attached_to_router binary sensor device.""" state_key = f"{DOMAIN}.{CONNECTED_TO_ROUTER}" @@ -44,12 +44,6 @@ async def test_update_attached_to_router(hass: HomeAssistant): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - # Enable entity - er.async_update_entity(state_key, disabled_by=None) - await hass.async_block_till_done() - async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) - await hass.async_block_till_done() - state = hass.states.get(state_key) assert state is not None assert state.state == STATE_OFF diff --git a/tests/components/devolo_home_network/test_sensor.py b/tests/components/devolo_home_network/test_sensor.py index a7b1be5f07d..582d6533802 100644 --- a/tests/components/devolo_home_network/test_sensor.py +++ b/tests/components/devolo_home_network/test_sensor.py @@ -21,7 +21,6 @@ from tests.common import async_fire_time_changed @pytest.mark.usefixtures("mock_device") -@pytest.mark.usefixtures("mock_zeroconf") async def test_sensor_setup(hass: HomeAssistant): """Test default setup of the sensor component.""" entry = configure_integration(hass) @@ -36,7 +35,6 @@ async def test_sensor_setup(hass: HomeAssistant): @pytest.mark.usefixtures("mock_device") -@pytest.mark.usefixtures("mock_zeroconf") async def test_update_connected_wifi_clients(hass: HomeAssistant): """Test state change of a connected_wifi_clients sensor device.""" state_key = f"{DOMAIN}.connected_wifi_clients" @@ -73,85 +71,75 @@ async def test_update_connected_wifi_clients(hass: HomeAssistant): await hass.config_entries.async_unload(entry.entry_id) -@pytest.mark.usefixtures("mock_device") -@pytest.mark.usefixtures("mock_zeroconf") +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_device") async def test_update_neighboring_wifi_networks(hass: HomeAssistant): """Test state change of a neighboring_wifi_networks sensor device.""" state_key = f"{DOMAIN}.neighboring_wifi_networks" entry = configure_integration(hass) + er = entity_registry.async_get(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == "1" + assert er.async_get(state_key).entity_category is EntityCategory.DIAGNOSTIC + + # Emulate device failure with patch( - "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", - return_value=True, + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_neighbor_access_points", + side_effect=DeviceUnavailable, ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - state = hass.states.get(state_key) - assert state is not None - assert state.state == "1" - - er = entity_registry.async_get(hass) - assert er.async_get(state_key).entity_category is EntityCategory.DIAGNOSTIC - - # Emulate device failure - with patch( - "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_neighbor_access_points", - side_effect=DeviceUnavailable, - ): - async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) - await hass.async_block_till_done() - - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_UNAVAILABLE - - # Emulate state change async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) await hass.async_block_till_done() state = hass.states.get(state_key) assert state is not None - assert state.state == "1" + assert state.state == STATE_UNAVAILABLE - await hass.config_entries.async_unload(entry.entry_id) + # Emulate state change + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == "1" + + await hass.config_entries.async_unload(entry.entry_id) -@pytest.mark.usefixtures("mock_device") -@pytest.mark.usefixtures("mock_zeroconf") +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_device") async def test_update_connected_plc_devices(hass: HomeAssistant): """Test state change of a connected_plc_devices sensor device.""" state_key = f"{DOMAIN}.connected_plc_devices" entry = configure_integration(hass) + er = entity_registry.async_get(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == "1" + assert er.async_get(state_key).entity_category is EntityCategory.DIAGNOSTIC + + # Emulate device failure with patch( - "homeassistant.helpers.entity.Entity.entity_registry_enabled_default", - return_value=True, + "devolo_plc_api.plcnet_api.plcnetapi.PlcNetApi.async_get_network_overview", + side_effect=DeviceUnavailable, ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - state = hass.states.get(state_key) - assert state is not None - assert state.state == "1" - - er = entity_registry.async_get(hass) - assert er.async_get(state_key).entity_category is EntityCategory.DIAGNOSTIC - - # Emulate device failure - with patch( - "devolo_plc_api.plcnet_api.plcnetapi.PlcNetApi.async_get_network_overview", - side_effect=DeviceUnavailable, - ): - async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) - await hass.async_block_till_done() - - state = hass.states.get(state_key) - assert state is not None - assert state.state == STATE_UNAVAILABLE - - # Emulate state change async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) await hass.async_block_till_done() state = hass.states.get(state_key) assert state is not None - assert state.state == "1" + assert state.state == STATE_UNAVAILABLE - await hass.config_entries.async_unload(entry.entry_id) + # Emulate state change + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == "1" + + await hass.config_entries.async_unload(entry.entry_id) From 53fb581bff2481cbd1e53d2a47d583071277f6c2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 25 May 2022 10:37:09 +0200 Subject: [PATCH 0891/3516] Adjust config-flow type hints in dynalite (#72476) --- homeassistant/components/dynalite/config_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/dynalite/config_flow.py b/homeassistant/components/dynalite/config_flow.py index d148d09354f..d723825319a 100644 --- a/homeassistant/components/dynalite/config_flow.py +++ b/homeassistant/components/dynalite/config_flow.py @@ -5,6 +5,7 @@ from typing import Any from homeassistant import config_entries from homeassistant.const import CONF_HOST +from homeassistant.data_entry_flow import FlowResult from .bridge import DynaliteBridge from .const import DOMAIN, LOGGER @@ -20,7 +21,7 @@ class DynaliteFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize the Dynalite flow.""" self.host = None - async def async_step_import(self, import_info: dict[str, Any]) -> Any: + async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult: """Import a new bridge as a config entry.""" LOGGER.debug("Starting async_step_import - %s", import_info) host = import_info[CONF_HOST] From 5b896b315eb1eea2b89e385ed1566d1ab1f22ef0 Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Wed, 25 May 2022 01:49:53 -0700 Subject: [PATCH 0892/3516] Add TotalConnect options flow to auto-bypass low battery (#62458) * rebase * use bypass option in async_setup_entry * add test for options flow * default to False for AUTO_BYPASS * fix bypass defaults --- .../components/totalconnect/__init__.py | 16 ++++++- .../components/totalconnect/config_flow.py | 34 +++++++++++++- .../components/totalconnect/const.py | 2 + .../components/totalconnect/strings.json | 11 +++++ .../totalconnect/test_config_flow.py | 46 ++++++++++++++++++- 5 files changed, 105 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index 8a4aee0debb..87977e5c1db 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -17,7 +17,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_USERCODES, DOMAIN +from .const import AUTO_BYPASS, CONF_USERCODES, DOMAIN PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.BINARY_SENSOR] @@ -31,6 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: conf = entry.data username = conf[CONF_USERNAME] password = conf[CONF_PASSWORD] + bypass = entry.options.get(AUTO_BYPASS, False) if CONF_USERCODES not in conf: # should only happen for those who used UI before we added usercodes @@ -41,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: client = await hass.async_add_executor_job( - TotalConnectClient, username, password, usercodes + TotalConnectClient, username, password, usercodes, bypass ) except AuthenticationError as exception: raise ConfigEntryAuthFailed( @@ -54,6 +55,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + entry.async_on_unload(entry.add_update_listener(update_listener)) + return True @@ -66,6 +70,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener.""" + bypass = entry.options.get(AUTO_BYPASS, False) + client = hass.data[DOMAIN][entry.entry_id].client + for location_id in client.locations: + client.locations[location_id].auto_bypass_low_battery = bypass + + class TotalConnectDataUpdateCoordinator(DataUpdateCoordinator): """Class to fetch data from TotalConnect.""" diff --git a/homeassistant/components/totalconnect/config_flow.py b/homeassistant/components/totalconnect/config_flow.py index 013b08b50be..49e60b5b46e 100644 --- a/homeassistant/components/totalconnect/config_flow.py +++ b/homeassistant/components/totalconnect/config_flow.py @@ -5,8 +5,9 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_LOCATION, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback -from .const import CONF_USERCODES, DOMAIN +from .const import AUTO_BYPASS, CONF_USERCODES, DOMAIN PASSWORD_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) @@ -162,3 +163,34 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_abort(reason="reauth_successful") + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get options flow.""" + return TotalConnectOptionsFlowHandler(config_entry) + + +class TotalConnectOptionsFlowHandler(config_entries.OptionsFlow): + """TotalConnect options flow handler.""" + + def __init__(self, config_entry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + AUTO_BYPASS, + default=self.config_entry.options.get(AUTO_BYPASS, False), + ): bool + } + ), + ) diff --git a/homeassistant/components/totalconnect/const.py b/homeassistant/components/totalconnect/const.py index ba217bd4ca7..5012a303b69 100644 --- a/homeassistant/components/totalconnect/const.py +++ b/homeassistant/components/totalconnect/const.py @@ -2,6 +2,8 @@ DOMAIN = "totalconnect" CONF_USERCODES = "usercodes" +CONF_LOCATION = "location" +AUTO_BYPASS = "auto_bypass_low_battery" # Most TotalConnect alarms will work passing '-1' as usercode DEFAULT_USERCODE = "-1" diff --git a/homeassistant/components/totalconnect/strings.json b/homeassistant/components/totalconnect/strings.json index 64ca1beafd8..346ea7ef403 100644 --- a/homeassistant/components/totalconnect/strings.json +++ b/homeassistant/components/totalconnect/strings.json @@ -28,5 +28,16 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "no_locations": "No locations are available for this user, check TotalConnect settings" } + }, + "options": { + "step": { + "init": { + "title": "TotalConnect Options", + "description": "Automatically bypass zones the moment they report a low battery.", + "data": { + "auto_bypass_low_battery": "Auto bypass low battery" + } + } + } } } diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index 631553a4af4..78b121dda77 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -4,9 +4,14 @@ from unittest.mock import patch from total_connect_client.exceptions import AuthenticationError from homeassistant import data_entry_flow -from homeassistant.components.totalconnect.const import CONF_USERCODES, DOMAIN +from homeassistant.components.totalconnect.const import ( + AUTO_BYPASS, + CONF_USERCODES, + DOMAIN, +) from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD +from homeassistant.core import HomeAssistant from .common import ( CONFIG_DATA, @@ -190,3 +195,42 @@ async def test_no_locations(hass): await hass.async_block_till_done() assert mock_request.call_count == 1 + + +async def test_options_flow(hass: HomeAssistant): + """Test config flow options.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=CONFIG_DATA, + unique_id=USERNAME, + ) + config_entry.add_to_hass(hass) + + responses = [ + RESPONSE_AUTHENTICATE, + RESPONSE_PARTITION_DETAILS, + RESPONSE_GET_ZONE_DETAILS_SUCCESS, + RESPONSE_DISARMED, + RESPONSE_DISARMED, + RESPONSE_DISARMED, + ] + + with patch(TOTALCONNECT_REQUEST, side_effect=responses): + 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={AUTO_BYPASS: True} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == {AUTO_BYPASS: True} + await hass.async_block_till_done() + + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() From 42c80dda85f567192c182da2b4c603408a890381 Mon Sep 17 00:00:00 2001 From: BigMoby Date: Wed, 25 May 2022 10:52:06 +0200 Subject: [PATCH 0893/3516] Create iAlarmXR integration (#67817) * Creating iAlarmXR integration * fixing after review code * fixing remaining review hints * fixing remaining review hints * updating underlying pyialarm library * Creating iAlarmXR integration * fixing after review code * fixing remaining review hints * fixing remaining review hints * updating underlying pyialarm library * fixing after iMicknl review * Improving exception handling * Updating pyialarmxr library * fixing after merge dev * fixing after iMicknl review * Update CODEOWNERS Co-authored-by: Ludovico de Nittis * fixing iot_class * Update homeassistant/components/ialarmxr/config_flow.py Co-authored-by: J. Nick Koston * fixing after bdraco review * Update homeassistant/components/ialarmxr/config_flow.py Co-authored-by: J. Nick Koston * reverting catching exception in setup step * Update homeassistant/components/ialarmxr/__init__.py Co-authored-by: J. Nick Koston * Update homeassistant/components/ialarmxr/__init__.py Co-authored-by: J. Nick Koston * fixing after bdraco suggestions * Update homeassistant/components/ialarmxr/alarm_control_panel.py Co-authored-by: J. Nick Koston * Update homeassistant/components/ialarmxr/alarm_control_panel.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/ialarmxr/config_flow.py Co-authored-by: J. Nick Koston * Update homeassistant/components/ialarmxr/config_flow.py Co-authored-by: J. Nick Koston * Update homeassistant/components/ialarmxr/__init__.py Co-authored-by: J. Nick Koston * Update homeassistant/components/ialarmxr/__init__.py Co-authored-by: J. Nick Koston * Update homeassistant/components/ialarmxr/utils.py Co-authored-by: J. Nick Koston * regenerate translation and rename function to async_get_ialarmxr_mac * removing and collapsing unused error messages * fixing tests * improve code coverage in tests * improve code coverage in tests * improve code coverage in tests * fixing retry policy with new pyalarmxr library * snake case fix * renaming integration in ialarm_xr * renaming control panel name Co-authored-by: Ludovico de Nittis Co-authored-by: J. Nick Koston Co-authored-by: Mick Vleeshouwer --- .coveragerc | 1 + .strict-typing | 1 + CODEOWNERS | 2 + .../components/ialarm_xr/__init__.py | 101 ++++++++++ .../ialarm_xr/alarm_control_panel.py | 63 ++++++ .../components/ialarm_xr/config_flow.py | 94 +++++++++ homeassistant/components/ialarm_xr/const.py | 18 ++ .../components/ialarm_xr/manifest.json | 10 + .../components/ialarm_xr/strings.json | 21 ++ .../components/ialarm_xr/translations/en.json | 21 ++ homeassistant/components/ialarm_xr/utils.py | 18 ++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/ialarm_xr/__init__.py | 1 + .../components/ialarm_xr/test_config_flow.py | 185 ++++++++++++++++++ tests/components/ialarm_xr/test_init.py | 120 ++++++++++++ 18 files changed, 674 insertions(+) create mode 100644 homeassistant/components/ialarm_xr/__init__.py create mode 100644 homeassistant/components/ialarm_xr/alarm_control_panel.py create mode 100644 homeassistant/components/ialarm_xr/config_flow.py create mode 100644 homeassistant/components/ialarm_xr/const.py create mode 100644 homeassistant/components/ialarm_xr/manifest.json create mode 100644 homeassistant/components/ialarm_xr/strings.json create mode 100644 homeassistant/components/ialarm_xr/translations/en.json create mode 100644 homeassistant/components/ialarm_xr/utils.py create mode 100644 tests/components/ialarm_xr/__init__.py create mode 100644 tests/components/ialarm_xr/test_config_flow.py create mode 100644 tests/components/ialarm_xr/test_init.py diff --git a/.coveragerc b/.coveragerc index 4e903d68aa5..aced69a4714 100644 --- a/.coveragerc +++ b/.coveragerc @@ -514,6 +514,7 @@ omit = homeassistant/components/hvv_departures/__init__.py homeassistant/components/hydrawise/* homeassistant/components/ialarm/alarm_control_panel.py + homeassistant/components/ialarm_xr/alarm_control_panel.py homeassistant/components/iammeter/sensor.py homeassistant/components/iaqualink/binary_sensor.py homeassistant/components/iaqualink/climate.py diff --git a/.strict-typing b/.strict-typing index 2b6295b9157..e07d8b9cfc8 100644 --- a/.strict-typing +++ b/.strict-typing @@ -127,6 +127,7 @@ homeassistant.components.homewizard.* homeassistant.components.http.* homeassistant.components.huawei_lte.* homeassistant.components.hyperion.* +homeassistant.components.ialarm_xr.* homeassistant.components.image_processing.* homeassistant.components.input_button.* homeassistant.components.input_select.* diff --git a/CODEOWNERS b/CODEOWNERS index 9cc32503853..8a7a058e01f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -470,6 +470,8 @@ build.json @home-assistant/supervisor /tests/components/hyperion/ @dermotduffy /homeassistant/components/ialarm/ @RyuzakiKK /tests/components/ialarm/ @RyuzakiKK +/homeassistant/components/ialarm_xr/ @bigmoby +/tests/components/ialarm_xr/ @bigmoby /homeassistant/components/iammeter/ @lewei50 /homeassistant/components/iaqualink/ @flz /tests/components/iaqualink/ @flz diff --git a/homeassistant/components/ialarm_xr/__init__.py b/homeassistant/components/ialarm_xr/__init__.py new file mode 100644 index 00000000000..9a41b5ebab7 --- /dev/null +++ b/homeassistant/components/ialarm_xr/__init__.py @@ -0,0 +1,101 @@ +"""iAlarmXR integration.""" +from __future__ import annotations + +import asyncio +import logging + +from async_timeout import timeout +from pyialarmxr import ( + IAlarmXR, + IAlarmXRGenericException, + IAlarmXRSocketTimeoutException, +) + +from homeassistant.components.alarm_control_panel import SCAN_INTERVAL +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, IALARMXR_TO_HASS +from .utils import async_get_ialarmxr_mac + +PLATFORMS = [Platform.ALARM_CONTROL_PANEL] +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up iAlarmXR config.""" + host = entry.data[CONF_HOST] + port = entry.data[CONF_PORT] + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + + ialarmxr = IAlarmXR(username, password, host, port) + + try: + async with timeout(10): + ialarmxr_mac = await async_get_ialarmxr_mac(hass, ialarmxr) + except ( + asyncio.TimeoutError, + ConnectionError, + IAlarmXRGenericException, + IAlarmXRSocketTimeoutException, + ) as ex: + raise ConfigEntryNotReady from ex + + coordinator = IAlarmXRDataUpdateCoordinator(hass, ialarmxr, ialarmxr_mac) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload iAlarmXR config.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok + + +class IAlarmXRDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching iAlarmXR data.""" + + def __init__(self, hass: HomeAssistant, ialarmxr: IAlarmXR, mac: str) -> None: + """Initialize global iAlarm data updater.""" + self.ialarmxr: IAlarmXR = ialarmxr + self.state: str | None = None + self.host: str = ialarmxr.host + self.mac: str = mac + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + + def _update_data(self) -> None: + """Fetch data from iAlarmXR via sync functions.""" + status: int = self.ialarmxr.get_status() + _LOGGER.debug("iAlarmXR status: %s", status) + + self.state = IALARMXR_TO_HASS.get(status) + + async def _async_update_data(self) -> None: + """Fetch data from iAlarmXR.""" + try: + async with timeout(10): + await self.hass.async_add_executor_job(self._update_data) + except ConnectionError as error: + raise UpdateFailed(error) from error diff --git a/homeassistant/components/ialarm_xr/alarm_control_panel.py b/homeassistant/components/ialarm_xr/alarm_control_panel.py new file mode 100644 index 00000000000..7b47ce3d7fa --- /dev/null +++ b/homeassistant/components/ialarm_xr/alarm_control_panel.py @@ -0,0 +1,63 @@ +"""Interfaces with iAlarmXR control panels.""" +from __future__ import annotations + +from homeassistant.components.alarm_control_panel import ( + AlarmControlPanelEntity, + AlarmControlPanelEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import IAlarmXRDataUpdateCoordinator +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up a iAlarmXR alarm control panel based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([IAlarmXRPanel(coordinator)]) + + +class IAlarmXRPanel(CoordinatorEntity, AlarmControlPanelEntity): + """Representation of an iAlarmXR device.""" + + _attr_supported_features = ( + AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_AWAY + ) + _attr_name = "iAlarm_XR" + _attr_icon = "mdi:security" + + def __init__(self, coordinator: IAlarmXRDataUpdateCoordinator) -> None: + """Initialize the alarm panel.""" + super().__init__(coordinator) + self.coordinator: IAlarmXRDataUpdateCoordinator = coordinator + self._attr_unique_id = coordinator.mac + self._attr_device_info = DeviceInfo( + manufacturer="Antifurto365 - Meian", + name=self.name, + connections={(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)}, + ) + + @property + def state(self) -> str | None: + """Return the state of the device.""" + return self.coordinator.state + + def alarm_disarm(self, code: str | None = None) -> None: + """Send disarm command.""" + self.coordinator.ialarmxr.disarm() + + def alarm_arm_home(self, code: str | None = None) -> None: + """Send arm home command.""" + self.coordinator.ialarmxr.arm_stay() + + def alarm_arm_away(self, code: str | None = None) -> None: + """Send arm away command.""" + self.coordinator.ialarmxr.arm_away() diff --git a/homeassistant/components/ialarm_xr/config_flow.py b/homeassistant/components/ialarm_xr/config_flow.py new file mode 100644 index 00000000000..06509a82eb5 --- /dev/null +++ b/homeassistant/components/ialarm_xr/config_flow.py @@ -0,0 +1,94 @@ +"""Config flow for Antifurto365 iAlarmXR integration.""" +from __future__ import annotations + +import logging +from logging import Logger +from typing import Any + +from pyialarmxr import ( + IAlarmXR, + IAlarmXRGenericException, + IAlarmXRSocketTimeoutException, +) +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN +from .utils import async_get_ialarmxr_mac + +_LOGGER: Logger = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST, default=IAlarmXR.IALARM_P2P_DEFAULT_HOST): str, + vol.Required(CONF_PORT, default=IAlarmXR.IALARM_P2P_DEFAULT_PORT): int, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +async def _async_get_device_formatted_mac( + hass: core.HomeAssistant, username: str, password: str, host: str, port: int +) -> str: + """Return iAlarmXR mac address.""" + + ialarmxr = IAlarmXR(username, password, host, port) + return await async_get_ialarmxr_mac(hass, ialarmxr) + + +class IAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Antifurto365 iAlarmXR.""" + + VERSION = 1 + + 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: + mac = None + host = user_input[CONF_HOST] + port = user_input[CONF_PORT] + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + + try: + # If we are able to get the MAC address, we are able to establish + # a connection to the device. + mac = await _async_get_device_formatted_mac( + self.hass, username, password, host, port + ) + except ConnectionError: + errors["base"] = "cannot_connect" + except IAlarmXRGenericException as ialarmxr_exception: + _LOGGER.debug( + "IAlarmXRGenericException with message: [ %s ]", + ialarmxr_exception.message, + ) + errors["base"] = "unknown" + except IAlarmXRSocketTimeoutException as ialarmxr_socket_timeout_exception: + _LOGGER.debug( + "IAlarmXRSocketTimeoutException with message: [ %s ]", + ialarmxr_socket_timeout_exception.message, + ) + errors["base"] = "unknown" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if not errors: + await self.async_set_unique_id(mac) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input[CONF_HOST], data=user_input + ) + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/ialarm_xr/const.py b/homeassistant/components/ialarm_xr/const.py new file mode 100644 index 00000000000..a208f5290b6 --- /dev/null +++ b/homeassistant/components/ialarm_xr/const.py @@ -0,0 +1,18 @@ +"""Constants for the iAlarmXR integration.""" +from pyialarmxr import IAlarmXR + +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) + +DOMAIN = "ialarm_xr" + +IALARMXR_TO_HASS = { + IAlarmXR.ARMED_AWAY: STATE_ALARM_ARMED_AWAY, + IAlarmXR.ARMED_STAY: STATE_ALARM_ARMED_HOME, + IAlarmXR.DISARMED: STATE_ALARM_DISARMED, + IAlarmXR.TRIGGERED: STATE_ALARM_TRIGGERED, +} diff --git a/homeassistant/components/ialarm_xr/manifest.json b/homeassistant/components/ialarm_xr/manifest.json new file mode 100644 index 00000000000..4861e9c901f --- /dev/null +++ b/homeassistant/components/ialarm_xr/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "ialarm_xr", + "name": "Antifurto365 iAlarmXR", + "documentation": "https://www.home-assistant.io/integrations/ialarmxr", + "requirements": ["pyialarmxr==1.0.13"], + "codeowners": ["@bigmoby"], + "config_flow": true, + "iot_class": "cloud_polling", + "loggers": ["pyialarmxr"] +} diff --git a/homeassistant/components/ialarm_xr/strings.json b/homeassistant/components/ialarm_xr/strings.json new file mode 100644 index 00000000000..1650ae28c84 --- /dev/null +++ b/homeassistant/components/ialarm_xr/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/ialarm_xr/translations/en.json b/homeassistant/components/ialarm_xr/translations/en.json new file mode 100644 index 00000000000..bf2bf989dcd --- /dev/null +++ b/homeassistant/components/ialarm_xr/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/utils.py b/homeassistant/components/ialarm_xr/utils.py new file mode 100644 index 00000000000..db82a3fcd44 --- /dev/null +++ b/homeassistant/components/ialarm_xr/utils.py @@ -0,0 +1,18 @@ +"""iAlarmXR utils.""" +import logging + +from pyialarmxr import IAlarmXR + +from homeassistant import core +from homeassistant.helpers.device_registry import format_mac + +_LOGGER = logging.getLogger(__name__) + + +async def async_get_ialarmxr_mac(hass: core.HomeAssistant, ialarmxr: IAlarmXR) -> str: + """Retrieve iAlarmXR MAC address.""" + _LOGGER.debug("Retrieving ialarmxr mac address") + + mac = await hass.async_add_executor_job(ialarmxr.get_mac) + + return format_mac(mac) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b0bbd3e1b36..e9ba5971e07 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -161,6 +161,7 @@ FLOWS = { "hvv_departures", "hyperion", "ialarm", + "ialarm_xr", "iaqualink", "icloud", "ifttt", diff --git a/mypy.ini b/mypy.ini index 3cc9653e27a..e0c512782fb 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1160,6 +1160,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ialarm_xr.*] +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.image_processing.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index a52d465fac6..0f11d8576bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1549,6 +1549,9 @@ pyhomeworks==0.0.6 # homeassistant.components.ialarm pyialarm==1.9.0 +# homeassistant.components.ialarm_xr +pyialarmxr==1.0.13 + # homeassistant.components.icloud pyicloud==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 897ffe50074..f13a1a87dda 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1037,6 +1037,9 @@ pyhomematic==0.1.77 # homeassistant.components.ialarm pyialarm==1.9.0 +# homeassistant.components.ialarm_xr +pyialarmxr==1.0.13 + # homeassistant.components.icloud pyicloud==1.0.0 diff --git a/tests/components/ialarm_xr/__init__.py b/tests/components/ialarm_xr/__init__.py new file mode 100644 index 00000000000..4097867f70b --- /dev/null +++ b/tests/components/ialarm_xr/__init__.py @@ -0,0 +1 @@ +"""Tests for the Antifurto365 iAlarmXR integration.""" diff --git a/tests/components/ialarm_xr/test_config_flow.py b/tests/components/ialarm_xr/test_config_flow.py new file mode 100644 index 00000000000..22a70bda067 --- /dev/null +++ b/tests/components/ialarm_xr/test_config_flow.py @@ -0,0 +1,185 @@ +"""Test the Antifurto365 iAlarmXR config flow.""" + +from unittest.mock import patch + +from pyialarmxr import IAlarmXRGenericException, IAlarmXRSocketTimeoutException + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.ialarm_xr.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME + +from tests.common import MockConfigEntry + +TEST_DATA = { + CONF_HOST: "1.1.1.1", + CONF_PORT: 18034, + CONF_USERNAME: "000ZZZ0Z00", + CONF_PASSWORD: "00000000", +} + +TEST_MAC = "00:00:54:12:34:56" + + +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"] == data_entry_flow.RESULT_TYPE_FORM + assert result["handler"] == "ialarm_xr" + assert result["data_schema"].schema.get("host") == str + assert result["data_schema"].schema.get("port") == int + assert result["data_schema"].schema.get("password") == str + assert result["data_schema"].schema.get("username") == str + assert result["errors"] == {} + + with patch( + "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_status", + return_value=1, + ), patch( + "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", + return_value=TEST_MAC, + ), patch( + "homeassistant.components.ialarm_xr.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"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == TEST_DATA["host"] + assert result2["data"] == TEST_DATA + 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} + ) + + with patch( + "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", + side_effect=ConnectionError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_exception(hass): + """Test we handle unknown exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_cannot_connect_throwing_connection_error(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", + side_effect=ConnectionError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_cannot_connect_throwing_socket_timeout_exception(hass): + """Test we handle cannot connect error because of socket timeout.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", + side_effect=IAlarmXRSocketTimeoutException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_cannot_connect_throwing_generic_exception(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", + side_effect=IAlarmXRGenericException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_already_exists(hass): + """Test that a flow with an existing host aborts.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_MAC, + data=TEST_DATA, + ) + + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", + return_value=TEST_MAC, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], TEST_DATA + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + +async def test_flow_user_step_no_input(hass): + """Test appropriate error when no input is provided.""" + _result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + _result["flow_id"], user_input=None + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == config_entries.SOURCE_USER + assert result["errors"] == {} diff --git a/tests/components/ialarm_xr/test_init.py b/tests/components/ialarm_xr/test_init.py new file mode 100644 index 00000000000..8486b7049e6 --- /dev/null +++ b/tests/components/ialarm_xr/test_init.py @@ -0,0 +1,120 @@ +"""Test the Antifurto365 iAlarmXR init.""" +import asyncio +from datetime import timedelta +from unittest.mock import Mock, patch +from uuid import uuid4 + +import pytest + +from homeassistant.components.ialarm_xr.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.util.dt import utcnow + +from tests.common import MockConfigEntry, async_fire_time_changed + + +@pytest.fixture(name="ialarmxr_api") +def ialarmxr_api_fixture(): + """Set up IAlarmXR API fixture.""" + with patch("homeassistant.components.ialarm_xr.IAlarmXR") as mock_ialarm_api: + yield mock_ialarm_api + + +@pytest.fixture(name="mock_config_entry") +def mock_config_fixture(): + """Return a fake config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.168.10.20", + CONF_PORT: 18034, + CONF_USERNAME: "000ZZZ0Z00", + CONF_PASSWORD: "00000000", + }, + entry_id=str(uuid4()), + ) + + +async def test_setup_entry(hass, ialarmxr_api, mock_config_entry): + """Test setup entry.""" + ialarmxr_api.return_value.get_mac = Mock(return_value="00:00:54:12:34:56") + + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + ialarmxr_api.return_value.get_mac.assert_called_once() + assert mock_config_entry.state is ConfigEntryState.LOADED + + +async def test_setup_not_ready(hass, ialarmxr_api, mock_config_entry): + """Test setup failed because we can't connect to the alarm system.""" + ialarmxr_api.return_value.get_mac = Mock(side_effect=ConnectionError) + + mock_config_entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_unload_entry(hass, ialarmxr_api, mock_config_entry): + """Test being able to unload an entry.""" + ialarmxr_api.return_value.get_mac = Mock(return_value="00:00:54:12:34:56") + + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(mock_config_entry.entry_id) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_setup_not_ready_connection_error(hass, ialarmxr_api, mock_config_entry): + """Test setup failed because we can't connect to the alarm system.""" + ialarmxr_api.return_value.get_status = Mock(side_effect=ConnectionError) + + mock_config_entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + future = utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, future) + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_setup_not_ready_timeout(hass, ialarmxr_api, mock_config_entry): + """Test setup failed because we can't connect to the alarm system.""" + ialarmxr_api.return_value.get_status = Mock(side_effect=asyncio.TimeoutError) + + mock_config_entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + future = utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, future) + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_setup_entry_and_then_fail_on_update( + hass, ialarmxr_api, mock_config_entry +): + """Test setup entry.""" + ialarmxr_api.return_value.get_mac = Mock(return_value="00:00:54:12:34:56") + ialarmxr_api.return_value.get_status = Mock(value=ialarmxr_api.DISARMED) + + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + ialarmxr_api.return_value.get_mac.assert_called_once() + ialarmxr_api.return_value.get_status.assert_called_once() + assert mock_config_entry.state is ConfigEntryState.LOADED + + ialarmxr_api.return_value.get_status = Mock(side_effect=asyncio.TimeoutError) + future = utcnow() + timedelta(seconds=60) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + ialarmxr_api.return_value.get_status.assert_called_once() + assert hass.states.get("alarm_control_panel.ialarm_xr").state == "unavailable" From 36ff15b038abef56a3750b4ce2d1fc0951b5d019 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 25 May 2022 12:15:33 +0200 Subject: [PATCH 0894/3516] Fix container init (#72478) --- rootfs/init | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rootfs/init b/rootfs/init index bfc4802c858..0aa246c33ab 100755 --- a/rootfs/init +++ b/rootfs/init @@ -1,4 +1,4 @@ -#!/usr/bin/env bashio +#!/bin/sh -e # This is the first program launched at container start. # We don't know where our binaries are and we cannot guarantee @@ -43,7 +43,7 @@ fi # root, can be a normal user). exec s6-overlay-suexec \ - ' /package/admin/s6-overlay-@VERSION@/libexec/preinit' \ + ' /package/admin/s6-overlay/libexec/preinit' \ '' \ - /package/admin/s6-overlay-@VERSION@/libexec/stage0 \ + /package/admin/s6-overlay/libexec/stage0 \ "$@" From 84d1e109483e1202a82714b9c0bddae2632c1f83 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 25 May 2022 12:53:12 +0200 Subject: [PATCH 0895/3516] Bump pychromecast to 12.1.3 (#72475) --- 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 10edc81e0fc..644a517c666 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==12.1.2"], + "requirements": ["pychromecast==12.1.3"], "after_dependencies": [ "cloud", "http", diff --git a/requirements_all.txt b/requirements_all.txt index 0f11d8576bc..9093246c7ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1405,7 +1405,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==12.1.2 +pychromecast==12.1.3 # homeassistant.components.pocketcasts pycketcasts==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f13a1a87dda..096db3431a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -950,7 +950,7 @@ pybotvac==0.0.23 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==12.1.2 +pychromecast==12.1.3 # homeassistant.components.climacell pyclimacell==0.18.2 From 692a602aeabc2f11dfed9bdb7d6ac2acd20bde8d Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 25 May 2022 08:48:34 -0400 Subject: [PATCH 0896/3516] Bump ZHA quirks to 0.0.74 (#72482) --- 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 8ea9e9d8d4a..8d6e6162d76 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.30.0", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.73", + "zha-quirks==0.0.74", "zigpy-deconz==0.16.0", "zigpy==0.45.1", "zigpy-xbee==0.14.0", diff --git a/requirements_all.txt b/requirements_all.txt index 9093246c7ff..9e6729d6af1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2501,7 +2501,7 @@ zengge==0.2 zeroconf==0.38.6 # homeassistant.components.zha -zha-quirks==0.0.73 +zha-quirks==0.0.74 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 096db3431a1..1d26030bedb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1647,7 +1647,7 @@ youless-api==0.16 zeroconf==0.38.6 # homeassistant.components.zha -zha-quirks==0.0.73 +zha-quirks==0.0.74 # homeassistant.components.zha zigpy-deconz==0.16.0 From 101b1489c8ca2da53412ffe9c2fa5314bf68ee1f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 25 May 2022 18:09:53 +0200 Subject: [PATCH 0897/3516] Fix meater remaining time sensor (#72490) --- homeassistant/components/meater/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index f6d06dc2b25..84ef3a2e2a9 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -52,7 +52,7 @@ def _remaining_time_to_timestamp(probe: MeaterProbe) -> datetime | None: """Convert remaining time to timestamp.""" if not probe.cook or probe.cook.time_remaining < 0: return None - return dt_util.utcnow() + timedelta(probe.cook.time_remaining) + return dt_util.utcnow() + timedelta(seconds=probe.cook.time_remaining) SENSOR_TYPES = ( From f9f87c607e7135c05ac68f4233c25e796b458c66 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 25 May 2022 18:35:54 +0200 Subject: [PATCH 0898/3516] Clean zwave_js api typing (#72484) * Clean zwave_js api typing * Add temporary type ignore --- homeassistant/components/zwave_js/api.py | 38 +++++++++++++++++------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index a4fca557242..1ab8e831b74 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable import dataclasses from functools import partial, wraps -from typing import Any +from typing import Any, Literal from aiohttp import web, web_exceptions, web_request import voluptuous as vol @@ -113,6 +113,22 @@ DRY_RUN = "dry_run" # constants for inclusion INCLUSION_STRATEGY = "inclusion_strategy" + +# Remove type ignore when bumping library to 0.37.0 +INCLUSION_STRATEGY_NOT_SMART_START: dict[ # type: ignore[misc] + int, + Literal[ + InclusionStrategy.DEFAULT, + InclusionStrategy.SECURITY_S0, + InclusionStrategy.SECURITY_S2, + InclusionStrategy.INSECURE, + ], +] = { + InclusionStrategy.DEFAULT.value: InclusionStrategy.DEFAULT, + InclusionStrategy.SECURITY_S0.value: InclusionStrategy.SECURITY_S0, + InclusionStrategy.SECURITY_S2.value: InclusionStrategy.SECURITY_S2, + InclusionStrategy.INSECURE.value: InclusionStrategy.INSECURE, +} PIN = "pin" FORCE_SECURITY = "force_security" PLANNED_PROVISIONING_ENTRY = "planned_provisioning_entry" @@ -143,20 +159,19 @@ MINIMUM_QR_STRING_LENGTH = 52 def convert_planned_provisioning_entry(info: dict) -> ProvisioningEntry: """Handle provisioning entry dict to ProvisioningEntry.""" - info = ProvisioningEntry( + return ProvisioningEntry( dsk=info[DSK], security_classes=[SecurityClass(sec_cls) for sec_cls in info[SECURITY_CLASSES]], additional_properties={ k: v for k, v in info.items() if k not in (DSK, SECURITY_CLASSES) }, ) - return info def convert_qr_provisioning_information(info: dict) -> QRProvisioningInformation: """Convert QR provisioning information dict to QRProvisioningInformation.""" protocols = [Protocols(proto) for proto in info.get(SUPPORTED_PROTOCOLS, [])] - info = QRProvisioningInformation( + return QRProvisioningInformation( version=QRCodeVersion(info[VERSION]), security_classes=[SecurityClass(sec_cls) for sec_cls in info[SECURITY_CLASSES]], dsk=info[DSK], @@ -172,7 +187,6 @@ def convert_qr_provisioning_information(info: dict) -> QRProvisioningInformation supported_protocols=protocols if protocols else None, additional_properties=info.get(ADDITIONAL_PROPERTIES, {}), ) - return info # Helper schemas @@ -655,7 +669,7 @@ async def websocket_add_node( ) connection.subscriptions[msg["id"]] = async_cleanup - msg[DATA_UNSUBSCRIBE] = unsubs = [ + unsubs: list[Callable[[], None]] = [ controller.on("inclusion started", forward_event), controller.on("inclusion failed", forward_event), controller.on("inclusion stopped", forward_event), @@ -666,10 +680,13 @@ async def websocket_add_node( hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device_registered ), ] + msg[DATA_UNSUBSCRIBE] = unsubs try: result = await controller.async_begin_inclusion( - inclusion_strategy, force_security=force_security, provisioning=provisioning + INCLUSION_STRATEGY_NOT_SMART_START[inclusion_strategy.value], + force_security=force_security, + provisioning=provisioning, ) except ValueError as err: connection.send_error( @@ -1165,7 +1182,7 @@ async def websocket_replace_failed_node( ) connection.subscriptions[msg["id"]] = async_cleanup - msg[DATA_UNSUBSCRIBE] = unsubs = [ + unsubs: list[Callable[[], None]] = [ controller.on("inclusion started", forward_event), controller.on("inclusion failed", forward_event), controller.on("inclusion stopped", forward_event), @@ -1177,11 +1194,12 @@ async def websocket_replace_failed_node( hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device_registered ), ] + msg[DATA_UNSUBSCRIBE] = unsubs try: result = await controller.async_replace_failed_node( node_id, - inclusion_strategy, + INCLUSION_STRATEGY_NOT_SMART_START[inclusion_strategy.value], force_security=force_security, provisioning=provisioning, ) @@ -1540,7 +1558,7 @@ async def websocket_get_config_parameters( ) -> None: """Get a list of configuration parameters for a Z-Wave node.""" values = node.get_configuration_values() - result = {} + result: dict[str, Any] = {} for value_id, zwave_value in values.items(): metadata = zwave_value.metadata result[value_id] = { From 10f0509ca3a2b1fd186bf7a801883b427ed40fc4 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 25 May 2022 18:39:42 +0200 Subject: [PATCH 0899/3516] Clean zwave_js services typing (#72485) Fix services --- homeassistant/components/zwave_js/services.py | 80 +++++++++++-------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 3b56e0a073c..d60532fcf75 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Generator +from collections.abc import Generator, Sequence import logging from typing import Any @@ -12,7 +12,7 @@ from zwave_js_server.const import CommandClass, CommandStatus from zwave_js_server.exceptions import SetValueFailed from zwave_js_server.model.endpoint import Endpoint from zwave_js_server.model.node import Node as ZwaveNode -from zwave_js_server.model.value import get_value_id +from zwave_js_server.model.value import ValueDataType, get_value_id from zwave_js_server.util.multicast import async_multicast_set_value from zwave_js_server.util.node import ( async_bulk_set_partial_config_parameters, @@ -72,7 +72,7 @@ def broadcast_command(val: dict[str, Any]) -> dict[str, Any]: def get_valid_responses_from_results( - zwave_objects: set[ZwaveNode | Endpoint], results: tuple[Any, ...] + zwave_objects: Sequence[ZwaveNode | Endpoint], results: Sequence[Any] ) -> Generator[tuple[ZwaveNode | Endpoint, Any], None, None]: """Return valid responses from a list of results.""" for zwave_object, result in zip(zwave_objects, results): @@ -81,8 +81,8 @@ def get_valid_responses_from_results( def raise_exceptions_from_results( - zwave_objects: set[ZwaveNode | Endpoint] | tuple[ZwaveNode | str, ...], - results: tuple[Any, ...], + zwave_objects: Sequence[ZwaveNode | Endpoint], + results: Sequence[Any], ) -> None: """Raise list of exceptions from a list of results.""" if errors := [ @@ -153,12 +153,20 @@ class ZWaveServices: first_node = next((node for node in nodes), None) + if first_node and not all(node.client.driver is not None for node in nodes): + raise vol.Invalid(f"Driver not ready for all nodes: {nodes}") + # If any nodes don't have matching home IDs, we can't run the command because # we can't multicast across multiple networks - if first_node and any( - node.client.driver.controller.home_id - != first_node.client.driver.controller.home_id - for node in nodes + if ( + first_node + and first_node.client.driver # We checked the driver was ready above. + and any( + node.client.driver.controller.home_id + != first_node.client.driver.controller.home_id + for node in nodes + if node.client.driver is not None + ) ): raise vol.Invalid( "Multicast commands only work on devices in the same network" @@ -417,7 +425,8 @@ class ZWaveServices: ), return_exceptions=True, ) - for node, result in get_valid_responses_from_results(nodes, results): + nodes_list = list(nodes) + for node, result in get_valid_responses_from_results(nodes_list, results): zwave_value = result[0] cmd_status = result[1] if cmd_status == CommandStatus.ACCEPTED: @@ -428,7 +437,7 @@ class ZWaveServices: "%s with value %s. Parameter will be set when the device wakes up" ) _LOGGER.info(msg, zwave_value, node, new_value) - raise_exceptions_from_results(nodes, results) + raise_exceptions_from_results(nodes_list, results) async def async_bulk_set_partial_config_parameters( self, service: ServiceCall @@ -450,7 +459,8 @@ class ZWaveServices: return_exceptions=True, ) - for node, cmd_status in get_valid_responses_from_results(nodes, results): + nodes_list = list(nodes) + for node, cmd_status in get_valid_responses_from_results(nodes_list, results): if cmd_status == CommandStatus.ACCEPTED: msg = "Bulk set partials for configuration parameter %s on Node %s" else: @@ -461,7 +471,7 @@ class ZWaveServices: _LOGGER.info(msg, property_, node) - raise_exceptions_from_results(nodes, results) + raise_exceptions_from_results(nodes_list, results) async def async_poll_value(self, service: ServiceCall) -> None: """Poll value on a node.""" @@ -477,10 +487,10 @@ class ZWaveServices: async def async_set_value(self, service: ServiceCall) -> None: """Set a value on a node.""" 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) - endpoint = service.data.get(const.ATTR_ENDPOINT) + command_class: CommandClass = service.data[const.ATTR_COMMAND_CLASS] + property_: int | str = service.data[const.ATTR_PROPERTY] + property_key: int | str | None = service.data.get(const.ATTR_PROPERTY_KEY) + endpoint: int | None = service.data.get(const.ATTR_ENDPOINT) new_value = service.data[const.ATTR_VALUE] wait_for_result = service.data.get(const.ATTR_WAIT_FOR_RESULT) options = service.data.get(const.ATTR_OPTIONS) @@ -515,17 +525,18 @@ class ZWaveServices: ) results = await asyncio.gather(*coros, return_exceptions=True) + nodes_list = list(nodes) # multiple set_values my fail so we will track the entire list - set_value_failed_nodes_list = [] - for node, success in get_valid_responses_from_results(nodes, results): + set_value_failed_nodes_list: list[ZwaveNode | Endpoint] = [] + for node_, success in get_valid_responses_from_results(nodes_list, results): if success is False: # If we failed to set a value, add node to SetValueFailed exception list - set_value_failed_nodes_list.append(node) + set_value_failed_nodes_list.append(node_) # Add the SetValueFailed exception to the results and the nodes to the node # list. No-op if there are no SetValueFailed exceptions raise_exceptions_from_results( - (*nodes, *set_value_failed_nodes_list), + (*nodes_list, *set_value_failed_nodes_list), (*results, *([SET_VALUE_FAILED_EXC] * len(set_value_failed_nodes_list))), ) @@ -543,17 +554,17 @@ 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) + command_class: CommandClass = service.data[const.ATTR_COMMAND_CLASS] + property_: int | str = service.data[const.ATTR_PROPERTY] + property_key: int | str | None = service.data.get(const.ATTR_PROPERTY_KEY) + endpoint: int | None = service.data.get(const.ATTR_ENDPOINT) + + value = ValueDataType(commandClass=command_class, property=property_) + if property_key is not None: + value["propertyKey"] = property_key + if endpoint is not None: + value["endpoint"] = endpoint - value = { - "commandClass": command_class, - "property": property_, - "propertyKey": property_key, - "endpoint": endpoint, - } new_value = service.data[const.ATTR_VALUE] # If there are no nodes, we can assume there is only one config entry due to @@ -590,7 +601,7 @@ class ZWaveServices: success = await async_multicast_set_value( client=client, new_value=new_value, - value_data={k: v for k, v in value.items() if v is not None}, + value_data=value, nodes=None if broadcast else list(nodes), options=options, ) @@ -627,8 +638,9 @@ class ZWaveServices: ), return_exceptions=True, ) + endpoints_list = list(endpoints) for endpoint, result in get_valid_responses_from_results( - endpoints, results + endpoints_list, results ): _LOGGER.info( ( @@ -640,7 +652,7 @@ class ZWaveServices: endpoint, result, ) - raise_exceptions_from_results(endpoints, results) + raise_exceptions_from_results(endpoints_list, results) # If an endpoint is provided, we assume the user wants to call the CC API on # that endpoint for all target nodes From 4723119fad514fc51b756c55241ee77b10940607 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 25 May 2022 18:40:38 +0200 Subject: [PATCH 0900/3516] Clean zwave_js remaining typing issues (#72488) --- .../zwave_js/discovery_data_template.py | 56 ++++++++++++------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 512cfafa63a..5c853d9ea9f 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -1,10 +1,10 @@ """Data template classes for discovery used to generate additional data for setup.""" from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Iterable, Mapping from dataclasses import dataclass, field import logging -from typing import Any +from typing import Any, Union, cast from zwave_js_server.const import CommandClass from zwave_js_server.const.command_class.meter import ( @@ -242,7 +242,7 @@ class BaseDiscoverySchemaDataTemplate: """ return {} - def values_to_watch(self, resolved_data: Any) -> Iterable[ZwaveValue]: + def values_to_watch(self, resolved_data: Any) -> Iterable[ZwaveValue | None]: """ Return list of all ZwaveValues resolved by helper that should be watched. @@ -261,7 +261,7 @@ class BaseDiscoverySchemaDataTemplate: @staticmethod def _get_value_from_id( node: ZwaveNode, value_id_obj: ZwaveValueID - ) -> ZwaveValue | None: + ) -> ZwaveValue | ZwaveConfigurationValue | None: """Get a ZwaveValue from a node using a ZwaveValueDict.""" value_id = get_value_id( node, @@ -295,7 +295,9 @@ class DynamicCurrentTempClimateDataTemplate(BaseDiscoverySchemaDataTemplate): return data - def values_to_watch(self, resolved_data: dict[str, Any]) -> Iterable[ZwaveValue]: + def values_to_watch( + self, resolved_data: dict[str, Any] + ) -> Iterable[ZwaveValue | None]: """Return list of all ZwaveValues resolved by helper that should be watched.""" return [ *resolved_data["lookup_table"].values(), @@ -331,8 +333,11 @@ class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate): @staticmethod def find_key_from_matching_set( enum_value: MultilevelSensorType | MultilevelSensorScaleType | MeterScaleType, - set_map: dict[ - str, set[MultilevelSensorType | MultilevelSensorScaleType | MeterScaleType] + set_map: Mapping[ + str, + set[MultilevelSensorType] + | set[MultilevelSensorScaleType] + | set[MeterScaleType], ], ) -> str | None: """Find a key in a set map that matches a given enum value.""" @@ -354,11 +359,11 @@ class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate): return NumericSensorDataTemplateData(ENTITY_DESC_KEY_BATTERY, PERCENTAGE) if value.command_class == CommandClass.METER: - scale_type = get_meter_scale_type(value) - unit = self.find_key_from_matching_set(scale_type, METER_UNIT_MAP) + meter_scale_type = get_meter_scale_type(value) + unit = self.find_key_from_matching_set(meter_scale_type, METER_UNIT_MAP) # We do this because even though these are energy scales, they don't meet # the unit requirements for the energy device class. - if scale_type in ( + if meter_scale_type in ( ElectricScale.PULSE_COUNT, ElectricScale.KILOVOLT_AMPERE_HOUR, ElectricScale.KILOVOLT_AMPERE_REACTIVE_HOUR, @@ -368,19 +373,21 @@ class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate): ) # We do this because even though these are power scales, they don't meet # the unit requirements for the power device class. - if scale_type == ElectricScale.KILOVOLT_AMPERE_REACTIVE: + if meter_scale_type == ElectricScale.KILOVOLT_AMPERE_REACTIVE: return NumericSensorDataTemplateData(ENTITY_DESC_KEY_MEASUREMENT, unit) return NumericSensorDataTemplateData( - self.find_key_from_matching_set(scale_type, METER_DEVICE_CLASS_MAP), + self.find_key_from_matching_set( + meter_scale_type, METER_DEVICE_CLASS_MAP + ), unit, ) if value.command_class == CommandClass.SENSOR_MULTILEVEL: sensor_type = get_multilevel_sensor_type(value) - scale_type = get_multilevel_sensor_scale_type(value) + multilevel_sensor_scale_type = get_multilevel_sensor_scale_type(value) unit = self.find_key_from_matching_set( - scale_type, MULTILEVEL_SENSOR_UNIT_MAP + multilevel_sensor_scale_type, MULTILEVEL_SENSOR_UNIT_MAP ) if sensor_type == MultilevelSensorType.TARGET_TEMPERATURE: return NumericSensorDataTemplateData( @@ -410,7 +417,9 @@ class CoverTiltDataTemplate(BaseDiscoverySchemaDataTemplate, TiltValueMix): """Resolve helper class data for a discovered value.""" return {"tilt_value": self._get_value_from_id(value.node, self.tilt_value_id)} - def values_to_watch(self, resolved_data: dict[str, Any]) -> Iterable[ZwaveValue]: + def values_to_watch( + self, resolved_data: dict[str, Any] + ) -> Iterable[ZwaveValue | None]: """Return list of all ZwaveValues resolved by helper that should be watched.""" return [resolved_data["tilt_value"]] @@ -492,24 +501,29 @@ class ConfigurableFanValueMappingDataTemplate( `configuration_option` to the value mapping object. """ - def resolve_data(self, value: ZwaveValue) -> dict[str, ZwaveConfigurationValue]: + def resolve_data( + self, value: ZwaveValue + ) -> dict[str, ZwaveConfigurationValue | None]: """Resolve helper class data for a discovered value.""" - zwave_value: ZwaveValue = self._get_value_from_id( - value.node, self.configuration_option + zwave_value = cast( # type: ignore[redundant-cast] + Union[ZwaveConfigurationValue, None], + self._get_value_from_id(value.node, self.configuration_option), ) return {"configuration_value": zwave_value} - def values_to_watch(self, resolved_data: dict[str, Any]) -> Iterable[ZwaveValue]: + def values_to_watch( + self, resolved_data: dict[str, ZwaveConfigurationValue | None] + ) -> Iterable[ZwaveConfigurationValue | None]: """Return list of all ZwaveValues that should be watched.""" return [ resolved_data["configuration_value"], ] def get_fan_value_mapping( - self, resolved_data: dict[str, ZwaveConfigurationValue] + self, resolved_data: dict[str, ZwaveConfigurationValue | None] ) -> FanValueMapping | None: """Get current fan properties from resolved data.""" - zwave_value: ZwaveValue = resolved_data["configuration_value"] + zwave_value = resolved_data["configuration_value"] if zwave_value is None: _LOGGER.warning("Unable to read device configuration value") From 9b40de18cd603fb6dbcc55a778b4fa1a461ef1a9 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 25 May 2022 12:49:04 -0400 Subject: [PATCH 0901/3516] Allow zwave_js/network_status WS API to accept device or entry ID (#72205) * Allow zwave_js/network_status WS API to accept device or entry ID * Fix based on upstream feedback * Fixt ests * Fixes --- homeassistant/components/zwave_js/api.py | 120 +++++++++++++++-------- tests/components/zwave_js/test_api.py | 94 +++++++++++++++++- 2 files changed, 167 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 1ab8e831b74..05591e8ecfb 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -236,6 +236,36 @@ QR_PROVISIONING_INFORMATION_SCHEMA = vol.All( QR_CODE_STRING_SCHEMA = vol.All(str, vol.Length(min=MINIMUM_QR_STRING_LENGTH)) +async def _async_get_entry( + hass: HomeAssistant, connection: ActiveConnection, msg: dict, entry_id: str +) -> tuple[ConfigEntry | None, Client | None, Driver | None]: + """Get config entry and client from message data.""" + entry = hass.config_entries.async_get_entry(entry_id) + if entry is None: + connection.send_error( + msg[ID], ERR_NOT_FOUND, f"Config entry {entry_id} not found" + ) + return None, None, None + + if entry.state is not ConfigEntryState.LOADED: + connection.send_error( + msg[ID], ERR_NOT_LOADED, f"Config entry {entry_id} not loaded" + ) + return None, None, None + + client: Client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + + if client.driver is None: + connection.send_error( + msg[ID], + ERR_NOT_LOADED, + f"Config entry {msg[ENTRY_ID]} not loaded, driver not ready", + ) + return None, None, None + + return entry, client, client.driver + + def async_get_entry(orig_func: Callable) -> Callable: """Decorate async function to get entry.""" @@ -244,35 +274,33 @@ def async_get_entry(orig_func: Callable) -> Callable: hass: HomeAssistant, connection: ActiveConnection, msg: dict ) -> None: """Provide user specific data and store to function.""" - entry_id = msg[ENTRY_ID] - entry = hass.config_entries.async_get_entry(entry_id) - if entry is None: - connection.send_error( - msg[ID], ERR_NOT_FOUND, f"Config entry {entry_id} not found" - ) + entry, client, driver = await _async_get_entry( + hass, connection, msg, msg[ENTRY_ID] + ) + + if not entry and not client and not driver: return - if entry.state is not ConfigEntryState.LOADED: - connection.send_error( - msg[ID], ERR_NOT_LOADED, f"Config entry {entry_id} not loaded" - ) - return - - client: Client = hass.data[DOMAIN][entry_id][DATA_CLIENT] - - if client.driver is None: - connection.send_error( - msg[ID], - ERR_NOT_LOADED, - f"Config entry {entry_id} not loaded, driver not ready", - ) - return - - await orig_func(hass, connection, msg, entry, client, client.driver) + await orig_func(hass, connection, msg, entry, client, driver) return async_get_entry_func +async def _async_get_node( + hass: HomeAssistant, connection: ActiveConnection, msg: dict, device_id: str +) -> Node | None: + """Get node from message data.""" + try: + node = async_get_node_from_device_id(hass, device_id) + except ValueError as err: + error_code = ERR_NOT_FOUND + if "loaded" in err.args[0]: + error_code = ERR_NOT_LOADED + connection.send_error(msg[ID], error_code, err.args[0]) + return None + return node + + def async_get_node(orig_func: Callable) -> Callable: """Decorate async function to get node.""" @@ -281,15 +309,8 @@ def async_get_node(orig_func: Callable) -> Callable: hass: HomeAssistant, connection: ActiveConnection, msg: dict ) -> None: """Provide user specific data and store to function.""" - device_id = msg[DEVICE_ID] - - try: - node = async_get_node_from_device_id(hass, device_id) - except ValueError as err: - error_code = ERR_NOT_FOUND - if "loaded" in err.args[0]: - error_code = ERR_NOT_LOADED - connection.send_error(msg[ID], error_code, err.args[0]) + node = await _async_get_node(hass, connection, msg, msg[DEVICE_ID]) + if not node: return await orig_func(hass, connection, msg, node) @@ -388,24 +409,37 @@ def async_register_api(hass: HomeAssistant) -> None: @websocket_api.require_admin @websocket_api.websocket_command( - {vol.Required(TYPE): "zwave_js/network_status", vol.Required(ENTRY_ID): str} + { + vol.Required(TYPE): "zwave_js/network_status", + vol.Exclusive(DEVICE_ID, "id"): str, + vol.Exclusive(ENTRY_ID, "id"): str, + } ) @websocket_api.async_response -@async_get_entry async def websocket_network_status( - hass: HomeAssistant, - connection: ActiveConnection, - msg: dict, - entry: ConfigEntry, - client: Client, - driver: Driver, + hass: HomeAssistant, connection: ActiveConnection, msg: dict ) -> None: """Get the status of the Z-Wave JS network.""" + if ENTRY_ID in msg: + _, client, driver = await _async_get_entry(hass, connection, msg, msg[ENTRY_ID]) + if not client or not driver: + return + elif DEVICE_ID in msg: + node = await _async_get_node(hass, connection, msg, msg[DEVICE_ID]) + if not node: + return + client = node.client + assert client.driver + driver = client.driver + else: + connection.send_error( + msg[ID], ERR_INVALID_FORMAT, "Must specify either device_id or entry_id" + ) + return controller = driver.controller + await controller.async_get_state() client_version_info = client.version assert client_version_info # When client is connected version info is set. - - await controller.async_get_state() data = { "client": { "ws_server_url": client.ws_server_url, @@ -1723,6 +1757,7 @@ async def websocket_get_log_config( driver: Driver, ) -> None: """Get log configuration for the Z-Wave JS driver.""" + assert client and client.driver connection.send_result( msg[ID], dataclasses.asdict(driver.log_config), @@ -1781,6 +1816,7 @@ async def websocket_data_collection_status( driver: Driver, ) -> None: """Return data collection preference and status.""" + assert client and client.driver result = { OPTED_IN: entry.data.get(CONF_DATA_COLLECTION_OPTED_IN), ENABLED: await driver.async_is_statistics_enabled(), diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 2309e7b02bf..ee6a85db70f 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -28,7 +28,10 @@ from zwave_js_server.model.controller import ( ) from zwave_js_server.model.node import Node -from homeassistant.components.websocket_api.const import ERR_NOT_FOUND +from homeassistant.components.websocket_api.const import ( + ERR_INVALID_FORMAT, + ERR_NOT_FOUND, +) from homeassistant.components.zwave_js.api import ( ADDITIONAL_PROPERTIES, APPLICATION_VERSION, @@ -82,14 +85,19 @@ def get_device(hass, node): return dev_reg.async_get_device({device_id}) -async def test_network_status(hass, integration, hass_ws_client): +async def test_network_status(hass, multisensor_6, integration, hass_ws_client): """Test the network status websocket command.""" entry = integration ws_client = await hass_ws_client(hass) + # Try API call with entry ID with patch("zwave_js_server.model.controller.Controller.async_get_state"): await ws_client.send_json( - {ID: 2, TYPE: "zwave_js/network_status", ENTRY_ID: entry.entry_id} + { + ID: 1, + TYPE: "zwave_js/network_status", + ENTRY_ID: entry.entry_id, + } ) msg = await ws_client.receive_json() result = msg["result"] @@ -98,18 +106,94 @@ async def test_network_status(hass, integration, hass_ws_client): assert result["client"]["server_version"] == "1.0.0" assert result["controller"]["inclusion_state"] == InclusionState.IDLE - # Test sending command with not loaded entry fails + # Try API call with device ID + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_device( + identifiers={(DOMAIN, "3245146787-52")}, + ) + assert device + with patch("zwave_js_server.model.controller.Controller.async_get_state"): + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/network_status", + DEVICE_ID: device.id, + } + ) + msg = await ws_client.receive_json() + result = msg["result"] + + assert result["client"]["ws_server_url"] == "ws://test:3000/zjs" + assert result["client"]["server_version"] == "1.0.0" + assert result["controller"]["inclusion_state"] == InclusionState.IDLE + + # Test sending command with invalid config entry ID fails + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/network_status", + ENTRY_ID: "fake_id", + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with invalid device ID fails + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/network_status", + DEVICE_ID: "fake_id", + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails with config entry ID await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() await ws_client.send_json( - {ID: 3, TYPE: "zwave_js/network_status", ENTRY_ID: entry.entry_id} + { + ID: 5, + TYPE: "zwave_js/network_status", + ENTRY_ID: entry.entry_id, + } ) msg = await ws_client.receive_json() assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_LOADED + # Test sending command with not loaded entry fails with device ID + await ws_client.send_json( + { + ID: 6, + TYPE: "zwave_js/network_status", + DEVICE_ID: device.id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + # Test sending command with no device ID or entry ID fails + await ws_client.send_json( + { + ID: 7, + TYPE: "zwave_js/network_status", + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_INVALID_FORMAT + async def test_node_ready( hass, From c8c4bf6c376b23906d40811934c80b180806b95e Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 25 May 2022 13:20:40 -0400 Subject: [PATCH 0902/3516] Bypass dynamic validation for zwave_js custom triggers (#72471) --- .../components/zwave_js/triggers/event.py | 11 +- .../components/zwave_js/triggers/helpers.py | 35 +++++ .../zwave_js/triggers/value_updated.py | 6 +- tests/components/zwave_js/test_trigger.py | 127 ++++++++++++++---- 4 files changed, 144 insertions(+), 35 deletions(-) create mode 100644 homeassistant/components/zwave_js/triggers/helpers.py diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py index 94afd1e9117..17bb52fb392 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -30,12 +30,13 @@ from homeassistant.components.zwave_js.helpers import ( get_device_id, get_home_and_node_id_from_device_entry, ) -from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.typing import ConfigType +from .helpers import async_bypass_dynamic_config_validation + # Platform type should be . PLATFORM_TYPE = f"{DOMAIN}.{__name__.rsplit('.', maxsplit=1)[-1]}" @@ -115,6 +116,9 @@ async def async_validate_trigger_config( """Validate config.""" config = TRIGGER_SCHEMA(config) + if async_bypass_dynamic_config_validation(hass, config): + return config + if config[ATTR_EVENT_SOURCE] == "node": config[ATTR_NODES] = async_get_nodes_from_targets(hass, config) if not config[ATTR_NODES]: @@ -126,12 +130,9 @@ async def async_validate_trigger_config( return config entry_id = config[ATTR_CONFIG_ENTRY_ID] - if (entry := hass.config_entries.async_get_entry(entry_id)) is None: + if hass.config_entries.async_get_entry(entry_id) is None: raise vol.Invalid(f"Config entry '{entry_id}' not found") - if entry.state is not ConfigEntryState.LOADED: - raise vol.Invalid(f"Config entry '{entry_id}' not loaded") - return config diff --git a/homeassistant/components/zwave_js/triggers/helpers.py b/homeassistant/components/zwave_js/triggers/helpers.py new file mode 100644 index 00000000000..2fbc585c887 --- /dev/null +++ b/homeassistant/components/zwave_js/triggers/helpers.py @@ -0,0 +1,35 @@ +"""Helpers for Z-Wave JS custom triggers.""" +from homeassistant.components.zwave_js.const import ATTR_CONFIG_ENTRY_ID, DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.typing import ConfigType + + +@callback +def async_bypass_dynamic_config_validation( + hass: HomeAssistant, config: ConfigType +) -> bool: + """Return whether target zwave_js config entry is not loaded.""" + # If the config entry is not loaded for a zwave_js device, entity, or the + # config entry ID provided, we can't perform dynamic validation + dev_reg = dr.async_get(hass) + ent_reg = er.async_get(hass) + trigger_devices = config.get(ATTR_DEVICE_ID, []) + trigger_entities = config.get(ATTR_ENTITY_ID, []) + return any( + entry.state != ConfigEntryState.LOADED + and ( + entry.entry_id == config.get(ATTR_CONFIG_ENTRY_ID) + or any( + device.id in trigger_devices + for device in dr.async_entries_for_config_entry(dev_reg, entry.entry_id) + ) + or ( + entity.entity_id in trigger_entities + for entity in er.async_entries_for_config_entry(ent_reg, entry.entry_id) + ) + ) + for entry in hass.config_entries.async_entries(DOMAIN) + ) diff --git a/homeassistant/components/zwave_js/triggers/value_updated.py b/homeassistant/components/zwave_js/triggers/value_updated.py index ac3aae1efed..4f15b87a6db 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -12,6 +12,7 @@ from homeassistant.components.automation import ( AutomationActionType, AutomationTriggerInfo, ) +from homeassistant.components.zwave_js.config_validation import VALUE_SCHEMA from homeassistant.components.zwave_js.const import ( ATTR_COMMAND_CLASS, ATTR_COMMAND_CLASS_NAME, @@ -37,7 +38,7 @@ 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 -from ..config_validation import VALUE_SCHEMA +from .helpers import async_bypass_dynamic_config_validation # Platform type should be . PLATFORM_TYPE = f"{DOMAIN}.{__name__.rsplit('.', maxsplit=1)[-1]}" @@ -75,6 +76,9 @@ async def async_validate_trigger_config( """Validate config.""" config = TRIGGER_SCHEMA(config) + if async_bypass_dynamic_config_validation(hass, config): + return config + config[ATTR_NODES] = async_get_nodes_from_targets(hass, config) if not config[ATTR_NODES]: raise vol.Invalid( diff --git a/tests/components/zwave_js/test_trigger.py b/tests/components/zwave_js/test_trigger.py index 45de09e8b17..9758f566d81 100644 --- a/tests/components/zwave_js/test_trigger.py +++ b/tests/components/zwave_js/test_trigger.py @@ -10,6 +10,9 @@ from zwave_js_server.model.node import Node from homeassistant.components import automation from homeassistant.components.zwave_js import DOMAIN from homeassistant.components.zwave_js.trigger import async_validate_trigger_config +from homeassistant.components.zwave_js.triggers.helpers import ( + async_bypass_dynamic_config_validation, +) from homeassistant.const import SERVICE_RELOAD from homeassistant.helpers.device_registry import ( async_entries_for_config_entry, @@ -671,35 +674,6 @@ async def test_zwave_js_event_invalid_config_entry_id( caplog.clear() -async def test_zwave_js_event_unloaded_config_entry(hass, client, integration, caplog): - """Test zwave_js.event automation trigger fails when config entry is unloaded.""" - trigger_type = f"{DOMAIN}.event" - - await hass.config_entries.async_unload(integration.entry_id) - - assert await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": { - "platform": trigger_type, - "config_entry_id": integration.entry_id, - "event_source": "controller", - "event": "inclusion started", - }, - "action": { - "event": "node_no_event_data_filter", - }, - } - ] - }, - ) - - assert f"Config entry '{integration.entry_id}' not loaded" in caplog.text - - async def test_async_validate_trigger_config(hass): """Test async_validate_trigger_config.""" mock_platform = AsyncMock() @@ -735,3 +709,98 @@ async def test_invalid_trigger_configs(hass): "property": "latchStatus", }, ) + + +async def test_zwave_js_trigger_config_entry_unloaded( + hass, client, lock_schlage_be469, integration +): + """Test zwave_js triggers bypass dynamic validation when needed.""" + dev_reg = async_get_dev_reg(hass) + device = async_entries_for_config_entry(dev_reg, integration.entry_id)[0] + + # Test bypass check is False + assert not async_bypass_dynamic_config_validation( + hass, + { + "platform": f"{DOMAIN}.value_updated", + "entity_id": SCHLAGE_BE469_LOCK_ENTITY, + "command_class": CommandClass.DOOR_LOCK.value, + "property": "latchStatus", + }, + ) + + await hass.config_entries.async_unload(integration.entry_id) + + # Test full validation for both events + assert await async_validate_trigger_config( + hass, + { + "platform": f"{DOMAIN}.value_updated", + "entity_id": SCHLAGE_BE469_LOCK_ENTITY, + "command_class": CommandClass.DOOR_LOCK.value, + "property": "latchStatus", + }, + ) + + assert await async_validate_trigger_config( + hass, + { + "platform": f"{DOMAIN}.event", + "entity_id": SCHLAGE_BE469_LOCK_ENTITY, + "event_source": "node", + "event": "interview stage completed", + }, + ) + + # Test bypass check + assert async_bypass_dynamic_config_validation( + hass, + { + "platform": f"{DOMAIN}.value_updated", + "entity_id": SCHLAGE_BE469_LOCK_ENTITY, + "command_class": CommandClass.DOOR_LOCK.value, + "property": "latchStatus", + }, + ) + + assert async_bypass_dynamic_config_validation( + hass, + { + "platform": f"{DOMAIN}.value_updated", + "device_id": device.id, + "command_class": CommandClass.DOOR_LOCK.value, + "property": "latchStatus", + "from": "ajar", + }, + ) + + assert async_bypass_dynamic_config_validation( + hass, + { + "platform": f"{DOMAIN}.event", + "entity_id": SCHLAGE_BE469_LOCK_ENTITY, + "event_source": "node", + "event": "interview stage completed", + }, + ) + + assert async_bypass_dynamic_config_validation( + hass, + { + "platform": f"{DOMAIN}.event", + "device_id": device.id, + "event_source": "node", + "event": "interview stage completed", + "event_data": {"stageName": "ProtocolInfo"}, + }, + ) + + assert async_bypass_dynamic_config_validation( + hass, + { + "platform": f"{DOMAIN}.event", + "config_entry_id": integration.entry_id, + "event_source": "controller", + "event": "nvm convert progress", + }, + ) From fe3fa0ae17569898144a5710933b2069f9a6122c Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 25 May 2022 13:46:55 -0400 Subject: [PATCH 0903/3516] Bump zwave-js-server-python to 0.37.0 (#72395) --- homeassistant/components/zwave_js/api.py | 3 +- homeassistant/components/zwave_js/climate.py | 2 +- .../components/zwave_js/device_action.py | 4 +- .../components/zwave_js/device_condition.py | 4 +- .../components/zwave_js/device_trigger.py | 4 +- .../zwave_js/discovery_data_template.py | 2 +- .../components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../fixtures/aeon_smart_switch_6_state.json | 9 +- .../fixtures/climate_danfoss_lc_13_state.json | 58 ++- ..._ct100_plus_different_endpoints_state.json | 354 +++++++++++++++++- tests/components/zwave_js/test_api.py | 5 + tests/components/zwave_js/test_services.py | 18 +- 14 files changed, 445 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 05591e8ecfb..5d81dc46803 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -114,8 +114,7 @@ DRY_RUN = "dry_run" # constants for inclusion INCLUSION_STRATEGY = "inclusion_strategy" -# Remove type ignore when bumping library to 0.37.0 -INCLUSION_STRATEGY_NOT_SMART_START: dict[ # type: ignore[misc] +INCLUSION_STRATEGY_NOT_SMART_START: dict[ int, Literal[ InclusionStrategy.DEFAULT, diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index cad0b6a1e5c..7b07eb09619 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -238,7 +238,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): if self._current_mode is None or self._current_mode.value is None: # Thermostat(valve) with no support for setting a mode is considered heating-only return [ThermostatSetpointType.HEATING] - return THERMOSTAT_MODE_SETPOINT_MAP.get(int(self._current_mode.value), []) # type: ignore[no-any-return] + return THERMOSTAT_MODE_SETPOINT_MAP.get(int(self._current_mode.value), []) @property def temperature_unit(self) -> str: diff --git a/homeassistant/components/zwave_js/device_action.py b/homeassistant/components/zwave_js/device_action.py index 2494cb8216d..728b376228e 100644 --- a/homeassistant/components/zwave_js/device_action.py +++ b/homeassistant/components/zwave_js/device_action.py @@ -326,7 +326,9 @@ async def async_get_action_capabilities( vol.Required(ATTR_COMMAND_CLASS): vol.In( { CommandClass(cc.id).value: cc.name - for cc in sorted(node.command_classes, key=lambda cc: cc.name) # type: ignore[no-any-return] + for cc in sorted( + node.command_classes, key=lambda cc: cc.name + ) } ), vol.Required(ATTR_PROPERTY): cv.string, diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py index 549319d23f4..7775995437a 100644 --- a/homeassistant/components/zwave_js/device_condition.py +++ b/homeassistant/components/zwave_js/device_condition.py @@ -221,7 +221,9 @@ async def async_get_condition_capabilities( vol.Required(ATTR_COMMAND_CLASS): vol.In( { CommandClass(cc.id).value: cc.name - for cc in sorted(node.command_classes, key=lambda cc: cc.name) # type: ignore[no-any-return] + for cc in sorted( + node.command_classes, key=lambda cc: cc.name + ) if cc.id != CommandClass.CONFIGURATION } ), diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py index ad860089d1d..dfca2eb4b27 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -534,7 +534,9 @@ async def async_get_trigger_capabilities( vol.Required(ATTR_COMMAND_CLASS): vol.In( { CommandClass(cc.id).value: cc.name - for cc in sorted(node.command_classes, key=lambda cc: cc.name) # type: ignore[no-any-return] + for cc in sorted( + node.command_classes, key=lambda cc: cc.name + ) if cc.id != CommandClass.CONFIGURATION } ), diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 5c853d9ea9f..74847c3f4da 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -505,7 +505,7 @@ class ConfigurableFanValueMappingDataTemplate( self, value: ZwaveValue ) -> dict[str, ZwaveConfigurationValue | None]: """Resolve helper class data for a discovered value.""" - zwave_value = cast( # type: ignore[redundant-cast] + zwave_value = cast( Union[ZwaveConfigurationValue, None], self._get_value_from_id(value.node, self.configuration_option), ) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 934c3f9a3f5..555da5fe954 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.36.1"], + "requirements": ["zwave-js-server-python==0.37.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 9e6729d6af1..27c5a401bd4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2528,7 +2528,7 @@ zigpy==0.45.1 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.36.1 +zwave-js-server-python==0.37.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d26030bedb..58a345adbfc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1665,7 +1665,7 @@ zigpy-znp==0.7.0 zigpy==0.45.1 # homeassistant.components.zwave_js -zwave-js-server-python==0.36.1 +zwave-js-server-python==0.37.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/tests/components/zwave_js/fixtures/aeon_smart_switch_6_state.json b/tests/components/zwave_js/fixtures/aeon_smart_switch_6_state.json index cc26ce14e3e..62b6205926b 100644 --- a/tests/components/zwave_js/fixtures/aeon_smart_switch_6_state.json +++ b/tests/components/zwave_js/fixtures/aeon_smart_switch_6_state.json @@ -51,7 +51,14 @@ "index": 0, "installerIcon": 1792, "userIcon": 1792, - "commandClasses": [] + "commandClasses": [ + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + } + ] } ], "values": [ diff --git a/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json b/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json index a877f82b53f..cb8e78881df 100644 --- a/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json +++ b/tests/components/zwave_js/fixtures/climate_danfoss_lc_13_state.json @@ -126,7 +126,63 @@ "endpoints": [ { "nodeId": 5, - "index": 0 + "index": 0, + "commandClasses": [ + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 2, + "isSecure": false + }, + { + "id": 70, + "name": "Climate Control Schedule", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 1, + "isSecure": false + }, + { + "id": 117, + "name": "Protection", + "version": 2, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": false + }, + { + "id": 132, + "name": "Wake Up", + "version": 1, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 1, + "isSecure": false + }, + { + "id": 143, + "name": "Multi Command", + "version": 1, + "isSecure": false + } + ] } ], "values": [ diff --git a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json index 15823a8f6ca..ca0efb56711 100644 --- a/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json +++ b/tests/components/zwave_js/fixtures/climate_radio_thermostat_ct100_plus_different_endpoints_state.json @@ -62,7 +62,123 @@ }, "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 5, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 2, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 2, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 2, + "isSecure": false + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 1, + "isSecure": false + }, + { + "id": 69, + "name": "Thermostat Fan State", + "version": 1, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ] }, { "nodeId": 26, @@ -82,7 +198,123 @@ }, "mandatorySupportedCCs": [32, 114, 64, 67, 134], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 5, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 2, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 2, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 2, + "isSecure": false + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 1, + "isSecure": false + }, + { + "id": 69, + "name": "Thermostat Fan State", + "version": 1, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ] }, { "nodeId": 26, @@ -102,7 +334,123 @@ }, "mandatorySupportedCCs": [32, 49], "mandatoryControlledCCs": [] - } + }, + "commandClasses": [ + { + "id": 49, + "name": "Multilevel Sensor", + "version": 5, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 2, + "isSecure": false + }, + { + "id": 66, + "name": "Thermostat Operating State", + "version": 2, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 2, + "isSecure": false + }, + { + "id": 68, + "name": "Thermostat Fan Mode", + "version": 1, + "isSecure": false + }, + { + "id": 69, + "name": "Thermostat Fan State", + "version": 1, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 3, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 129, + "name": "Clock", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 135, + "name": "Indicator", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ] } ], "values": [ diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index ee6a85db70f..e59a923ff44 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -1147,6 +1147,8 @@ async def test_get_provisioning_entries(hass, integration, client, hass_ws_clien { "dsk": "test", "security_classes": [SecurityClass.S2_UNAUTHENTICATED], + "requested_security_classes": None, + "status": 0, "additional_properties": {"fake": "test"}, } ] @@ -1235,6 +1237,8 @@ async def test_parse_qr_code_string(hass, integration, client, hass_ws_client): "max_inclusion_request_interval": 1, "uuid": "test", "supported_protocols": [Protocols.ZWAVE], + "status": 0, + "requested_security_classes": None, "additional_properties": {}, } @@ -3535,6 +3539,7 @@ async def test_check_for_config_updates(hass, client, integration, hass_ws_clien client.async_send_command.return_value = { "updateAvailable": True, "newVersion": "test", + "installedVersion": "test", } await ws_client.send_json( { diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index dec4c3c0a9d..b5ef083bf64 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -1817,7 +1817,7 @@ async def test_invoke_cc_api( device_radio_thermostat.id, device_danfoss.id, ], - ATTR_COMMAND_CLASS: 132, + ATTR_COMMAND_CLASS: 67, ATTR_ENDPOINT: 0, ATTR_METHOD_NAME: "someMethod", ATTR_PARAMETERS: [1, 2], @@ -1827,7 +1827,7 @@ async def test_invoke_cc_api( assert len(client.async_send_command.call_args_list) == 1 args = client.async_send_command.call_args[0][0] assert args["command"] == "endpoint.invoke_cc_api" - assert args["commandClass"] == 132 + assert args["commandClass"] == 67 assert args["endpoint"] == 0 assert args["methodName"] == "someMethod" assert args["args"] == [1, 2] @@ -1839,7 +1839,7 @@ async def test_invoke_cc_api( assert len(client.async_send_command_no_wait.call_args_list) == 1 args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "endpoint.invoke_cc_api" - assert args["commandClass"] == 132 + assert args["commandClass"] == 67 assert args["endpoint"] == 0 assert args["methodName"] == "someMethod" assert args["args"] == [1, 2] @@ -1870,7 +1870,7 @@ async def test_invoke_cc_api( "select.living_connect_z_thermostat_local_protection_state", "sensor.living_connect_z_thermostat_node_status", ], - ATTR_COMMAND_CLASS: 132, + ATTR_COMMAND_CLASS: 67, ATTR_METHOD_NAME: "someMethod", ATTR_PARAMETERS: [1, 2], }, @@ -1879,7 +1879,7 @@ async def test_invoke_cc_api( assert len(client.async_send_command.call_args_list) == 1 args = client.async_send_command.call_args[0][0] assert args["command"] == "endpoint.invoke_cc_api" - assert args["commandClass"] == 132 + assert args["commandClass"] == 67 assert args["endpoint"] == 0 assert args["methodName"] == "someMethod" assert args["args"] == [1, 2] @@ -1891,7 +1891,7 @@ async def test_invoke_cc_api( assert len(client.async_send_command_no_wait.call_args_list) == 1 args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "endpoint.invoke_cc_api" - assert args["commandClass"] == 132 + assert args["commandClass"] == 67 assert args["endpoint"] == 0 assert args["methodName"] == "someMethod" assert args["args"] == [1, 2] @@ -1916,7 +1916,7 @@ async def test_invoke_cc_api( device_danfoss.id, device_radio_thermostat.id, ], - ATTR_COMMAND_CLASS: 132, + ATTR_COMMAND_CLASS: 67, ATTR_ENDPOINT: 0, ATTR_METHOD_NAME: "someMethod", ATTR_PARAMETERS: [1, 2], @@ -1926,7 +1926,7 @@ async def test_invoke_cc_api( assert len(client.async_send_command.call_args_list) == 1 args = client.async_send_command.call_args[0][0] assert args["command"] == "endpoint.invoke_cc_api" - assert args["commandClass"] == 132 + assert args["commandClass"] == 67 assert args["endpoint"] == 0 assert args["methodName"] == "someMethod" assert args["args"] == [1, 2] @@ -1938,7 +1938,7 @@ async def test_invoke_cc_api( assert len(client.async_send_command_no_wait.call_args_list) == 1 args = client.async_send_command_no_wait.call_args[0][0] assert args["command"] == "endpoint.invoke_cc_api" - assert args["commandClass"] == 132 + assert args["commandClass"] == 67 assert args["endpoint"] == 0 assert args["methodName"] == "someMethod" assert args["args"] == [1, 2] From f166fc009a8111be8e3b172a845009462b5c0d47 Mon Sep 17 00:00:00 2001 From: Philip Allgaier Date: Wed, 25 May 2022 20:29:42 +0200 Subject: [PATCH 0904/3516] Fix typo in ISY994 re-authentication dialog (#72497) --- homeassistant/components/isy994/strings.json | 2 +- homeassistant/components/isy994/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/isy994/strings.json b/homeassistant/components/isy994/strings.json index 74b54f406a0..821f8889978 100644 --- a/homeassistant/components/isy994/strings.json +++ b/homeassistant/components/isy994/strings.json @@ -13,7 +13,7 @@ "title": "Connect to your ISY" }, "reauth_confirm": { - "description": "The credentials for {host} is no longer valid.", + "description": "The credentials for {host} are no longer valid.", "title": "Reauthenticate your ISY", "data": { "username": "[%key:common::config_flow::data::username%]", diff --git a/homeassistant/components/isy994/translations/en.json b/homeassistant/components/isy994/translations/en.json index b221765cd99..3610b35c194 100644 --- a/homeassistant/components/isy994/translations/en.json +++ b/homeassistant/components/isy994/translations/en.json @@ -17,7 +17,7 @@ "password": "Password", "username": "Username" }, - "description": "The credentials for {host} is no longer valid.", + "description": "The credentials for {host} are no longer valid.", "title": "Reauthenticate your ISY" }, "user": { From 2bc093a04da7b8849357cd57d81cf22278c1a883 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 25 May 2022 20:39:15 +0200 Subject: [PATCH 0905/3516] Hardware integration MVP (#71677) --- CODEOWNERS | 4 ++ homeassistant/components/hardware/__init__.py | 17 +++++ homeassistant/components/hardware/const.py | 3 + homeassistant/components/hardware/hardware.py | 31 ++++++++ .../components/hardware/manifest.json | 7 ++ homeassistant/components/hardware/models.py | 34 +++++++++ .../components/hardware/websocket_api.py | 47 ++++++++++++ homeassistant/components/hassio/__init__.py | 27 +++++++ .../components/raspberry_pi/__init__.py | 26 +++++++ .../components/raspberry_pi/config_flow.py | 22 ++++++ .../components/raspberry_pi/const.py | 3 + .../components/raspberry_pi/hardware.py | 54 ++++++++++++++ .../components/raspberry_pi/manifest.json | 9 +++ .../components/rpi_power/config_flow.py | 2 + script/hassfest/manifest.py | 4 +- tests/components/hardware/__init__.py | 1 + .../components/hardware/test_websocket_api.py | 18 +++++ tests/components/hassio/test_init.py | 41 ++++++++++- tests/components/raspberry_pi/__init__.py | 1 + .../raspberry_pi/test_config_flow.py | 58 +++++++++++++++ .../components/raspberry_pi/test_hardware.py | 57 +++++++++++++++ tests/components/raspberry_pi/test_init.py | 72 +++++++++++++++++++ 22 files changed, 535 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/hardware/__init__.py create mode 100644 homeassistant/components/hardware/const.py create mode 100644 homeassistant/components/hardware/hardware.py create mode 100644 homeassistant/components/hardware/manifest.json create mode 100644 homeassistant/components/hardware/models.py create mode 100644 homeassistant/components/hardware/websocket_api.py create mode 100644 homeassistant/components/raspberry_pi/__init__.py create mode 100644 homeassistant/components/raspberry_pi/config_flow.py create mode 100644 homeassistant/components/raspberry_pi/const.py create mode 100644 homeassistant/components/raspberry_pi/hardware.py create mode 100644 homeassistant/components/raspberry_pi/manifest.json create mode 100644 tests/components/hardware/__init__.py create mode 100644 tests/components/hardware/test_websocket_api.py create mode 100644 tests/components/raspberry_pi/__init__.py create mode 100644 tests/components/raspberry_pi/test_config_flow.py create mode 100644 tests/components/raspberry_pi/test_hardware.py create mode 100644 tests/components/raspberry_pi/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 8a7a058e01f..98d60fbfcb7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -416,6 +416,8 @@ build.json @home-assistant/supervisor /tests/components/guardian/ @bachya /homeassistant/components/habitica/ @ASMfreaK @leikoilja /tests/components/habitica/ @ASMfreaK @leikoilja +/homeassistant/components/hardware/ @home-assistant/core +/tests/components/hardware/ @home-assistant/core /homeassistant/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan /tests/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan /homeassistant/components/hassio/ @home-assistant/supervisor @@ -829,6 +831,8 @@ build.json @home-assistant/supervisor /tests/components/rainmachine/ @bachya /homeassistant/components/random/ @fabaff /tests/components/random/ @fabaff +/homeassistant/components/raspberry_pi/ @home-assistant/core +/tests/components/raspberry_pi/ @home-assistant/core /homeassistant/components/rdw/ @frenck /tests/components/rdw/ @frenck /homeassistant/components/recollect_waste/ @bachya diff --git a/homeassistant/components/hardware/__init__.py b/homeassistant/components/hardware/__init__.py new file mode 100644 index 00000000000..b3f342d4e32 --- /dev/null +++ b/homeassistant/components/hardware/__init__.py @@ -0,0 +1,17 @@ +"""The Hardware integration.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + +from . import websocket_api +from .const import DOMAIN + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Hardware.""" + hass.data[DOMAIN] = {} + + websocket_api.async_setup(hass) + + return True diff --git a/homeassistant/components/hardware/const.py b/homeassistant/components/hardware/const.py new file mode 100644 index 00000000000..7fd64d5d968 --- /dev/null +++ b/homeassistant/components/hardware/const.py @@ -0,0 +1,3 @@ +"""Constants for the Hardware integration.""" + +DOMAIN = "hardware" diff --git a/homeassistant/components/hardware/hardware.py b/homeassistant/components/hardware/hardware.py new file mode 100644 index 00000000000..e07f70022f4 --- /dev/null +++ b/homeassistant/components/hardware/hardware.py @@ -0,0 +1,31 @@ +"""The Hardware integration.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.integration_platform import ( + async_process_integration_platforms, +) + +from .const import DOMAIN +from .models import HardwareProtocol + + +async def async_process_hardware_platforms(hass: HomeAssistant): + """Start processing hardware platforms.""" + hass.data[DOMAIN]["hardware_platform"] = {} + + await async_process_integration_platforms(hass, DOMAIN, _register_hardware_platform) + + return True + + +async def _register_hardware_platform( + hass: HomeAssistant, integration_domain: str, platform: HardwareProtocol +): + """Register a hardware platform.""" + if integration_domain == DOMAIN: + return + if not hasattr(platform, "async_info"): + raise HomeAssistantError(f"Invalid hardware platform {platform}") + hass.data[DOMAIN]["hardware_platform"][integration_domain] = platform diff --git a/homeassistant/components/hardware/manifest.json b/homeassistant/components/hardware/manifest.json new file mode 100644 index 00000000000..e7e156b6065 --- /dev/null +++ b/homeassistant/components/hardware/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "hardware", + "name": "Hardware", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/hardware", + "codeowners": ["@home-assistant/core"] +} diff --git a/homeassistant/components/hardware/models.py b/homeassistant/components/hardware/models.py new file mode 100644 index 00000000000..067c2d955df --- /dev/null +++ b/homeassistant/components/hardware/models.py @@ -0,0 +1,34 @@ +"""Models for Hardware.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Protocol + +from homeassistant.core import HomeAssistant, callback + + +@dataclass +class BoardInfo: + """Board info type.""" + + hassio_board_id: str | None + manufacturer: str + model: str | None + revision: str | None + + +@dataclass +class HardwareInfo: + """Hardware info type.""" + + name: str | None + board: BoardInfo | None + url: str | None + + +class HardwareProtocol(Protocol): + """Define the format of hardware platforms.""" + + @callback + def async_info(self, hass: HomeAssistant) -> HardwareInfo: + """Return info.""" diff --git a/homeassistant/components/hardware/websocket_api.py b/homeassistant/components/hardware/websocket_api.py new file mode 100644 index 00000000000..388b9597481 --- /dev/null +++ b/homeassistant/components/hardware/websocket_api.py @@ -0,0 +1,47 @@ +"""The Hardware websocket API.""" +from __future__ import annotations + +import contextlib +from dataclasses import asdict + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN +from .hardware import async_process_hardware_platforms +from .models import HardwareProtocol + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Set up the hardware websocket API.""" + websocket_api.async_register_command(hass, ws_info) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "hardware/info", + } +) +@websocket_api.async_response +async def ws_info( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Return hardware info.""" + hardware_info = [] + + if "hardware_platform" not in hass.data[DOMAIN]: + await async_process_hardware_platforms(hass) + + hardware_platform: dict[str, HardwareProtocol] = hass.data[DOMAIN][ + "hardware_platform" + ] + for platform in hardware_platform.values(): + if hasattr(platform, "async_info"): + with contextlib.suppress(HomeAssistantError): + hardware_info.append(asdict(platform.async_info(hass))) + + connection.send_result(msg["id"], {"hardware": hardware_info}) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 7e976536691..93d902e4bae 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -205,6 +205,10 @@ MAP_SERVICE_API = { ), } +HARDWARE_INTEGRATIONS = { + "rpi": "raspberry_pi", +} + @bind_hass async def async_get_addon_info(hass: HomeAssistant, slug: str) -> dict: @@ -705,6 +709,29 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: # Init add-on ingress panels await async_setup_addon_panel(hass, hassio) + # Setup hardware integration for the detected board type + async def _async_setup_hardware_integration(hass): + """Set up hardaware integration for the detected board type.""" + if (os_info := get_os_info(hass)) is None: + # os info not yet fetched from supervisor, retry later + async_track_point_in_utc_time( + hass, + _async_setup_hardware_integration, + utcnow() + HASSIO_UPDATE_INTERVAL, + ) + return + if (board := os_info.get("board")) is None: + return + if (hw_integration := HARDWARE_INTEGRATIONS.get(board)) is None: + return + hass.async_create_task( + hass.config_entries.flow.async_init( + hw_integration, context={"source": "system"} + ) + ) + + await _async_setup_hardware_integration(hass) + hass.async_create_task( hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"}) ) diff --git a/homeassistant/components/raspberry_pi/__init__.py b/homeassistant/components/raspberry_pi/__init__.py new file mode 100644 index 00000000000..ab1114722c6 --- /dev/null +++ b/homeassistant/components/raspberry_pi/__init__.py @@ -0,0 +1,26 @@ +"""The Raspberry Pi integration.""" +from __future__ import annotations + +from homeassistant.components.hassio import get_os_info +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a Raspberry Pi config entry.""" + if (os_info := get_os_info(hass)) is None: + # The hassio integration has not yet fetched data from the supervisor + raise ConfigEntryNotReady + + board: str + if (board := os_info.get("board")) is None or not board.startswith("rpi"): + # Not running on a Raspberry Pi, Home Assistant may have been migrated + hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) + return False + + await hass.config_entries.flow.async_init( + "rpi_power", context={"source": "onboarding"} + ) + + return True diff --git a/homeassistant/components/raspberry_pi/config_flow.py b/homeassistant/components/raspberry_pi/config_flow.py new file mode 100644 index 00000000000..db0f8643e5c --- /dev/null +++ b/homeassistant/components/raspberry_pi/config_flow.py @@ -0,0 +1,22 @@ +"""Config flow for the Raspberry Pi integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class RaspberryPiConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Raspberry Pi.""" + + VERSION = 1 + + async def async_step_system(self, data: dict[str, Any] | None = None) -> FlowResult: + """Handle the initial step.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + return self.async_create_entry(title="Raspberry Pi", data={}) diff --git a/homeassistant/components/raspberry_pi/const.py b/homeassistant/components/raspberry_pi/const.py new file mode 100644 index 00000000000..48c004a6447 --- /dev/null +++ b/homeassistant/components/raspberry_pi/const.py @@ -0,0 +1,3 @@ +"""Constants for the Raspberry Pi integration.""" + +DOMAIN = "raspberry_pi" diff --git a/homeassistant/components/raspberry_pi/hardware.py b/homeassistant/components/raspberry_pi/hardware.py new file mode 100644 index 00000000000..343ba69d76b --- /dev/null +++ b/homeassistant/components/raspberry_pi/hardware.py @@ -0,0 +1,54 @@ +"""The Raspberry Pi hardware platform.""" +from __future__ import annotations + +from homeassistant.components.hardware.models import BoardInfo, HardwareInfo +from homeassistant.components.hassio import get_os_info +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN + +BOARD_NAMES = { + "rpi": "Raspberry Pi", + "rpi0": "Raspberry Pi Zero", + "rpi0-w": "Raspberry Pi Zero W", + "rpi2": "Raspberry Pi 2", + "rpi3": "Raspberry Pi 3 (32-bit)", + "rpi3-64": "Raspberry Pi 3", + "rpi4": "Raspberry Pi 4 (32-bit)", + "rpi4-64": "Raspberry Pi 4", +} + +MODELS = { + "rpi": "1", + "rpi0": "zero", + "rpi0-w": "zero_w", + "rpi2": "2", + "rpi3": "3", + "rpi3-64": "3", + "rpi4": "4", + "rpi4-64": "4", +} + + +@callback +def async_info(hass: HomeAssistant) -> HardwareInfo: + """Return board info.""" + if (os_info := get_os_info(hass)) is None: + raise HomeAssistantError + board: str + if (board := os_info.get("board")) is None: + raise HomeAssistantError + if not board.startswith("rpi"): + raise HomeAssistantError + + return HardwareInfo( + board=BoardInfo( + hassio_board_id=board, + manufacturer=DOMAIN, + model=MODELS.get(board), + revision=None, + ), + name=BOARD_NAMES.get(board, f"Unknown Raspberry Pi model '{board}'"), + url=None, + ) diff --git a/homeassistant/components/raspberry_pi/manifest.json b/homeassistant/components/raspberry_pi/manifest.json new file mode 100644 index 00000000000..5ba4f87e783 --- /dev/null +++ b/homeassistant/components/raspberry_pi/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "raspberry_pi", + "name": "Raspberry Pi", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/raspberry_pi", + "dependencies": ["hardware", "hassio"], + "codeowners": ["@home-assistant/core"], + "integration_type": "hardware" +} diff --git a/homeassistant/components/rpi_power/config_flow.py b/homeassistant/components/rpi_power/config_flow.py index ed8a45822b0..97814c2a866 100644 --- a/homeassistant/components/rpi_power/config_flow.py +++ b/homeassistant/components/rpi_power/config_flow.py @@ -36,6 +36,8 @@ class RPiPowerFlow(DiscoveryFlowHandler[Awaitable[bool]], domain=DOMAIN): self, data: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initialized by onboarding.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") has_devices = await self._discovery_function(self.hass) if not has_devices: diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index b66b33486cb..c478d16cf0f 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -52,6 +52,7 @@ NO_IOT_CLASS = [ "downloader", "ffmpeg", "frontend", + "hardware", "history", "homeassistant", "image", @@ -76,6 +77,7 @@ NO_IOT_CLASS = [ "profiler", "proxy", "python_script", + "raspberry_pi", "safe_mode", "script", "search", @@ -153,7 +155,7 @@ MANIFEST_SCHEMA = vol.Schema( { vol.Required("domain"): str, vol.Required("name"): str, - vol.Optional("integration_type"): "helper", + vol.Optional("integration_type"): vol.In(["hardware", "helper"]), vol.Optional("config_flow"): bool, vol.Optional("mqtt"): [str], vol.Optional("zeroconf"): [ diff --git a/tests/components/hardware/__init__.py b/tests/components/hardware/__init__.py new file mode 100644 index 00000000000..9b3acf65c59 --- /dev/null +++ b/tests/components/hardware/__init__.py @@ -0,0 +1 @@ +"""Tests for the Hardware integration.""" diff --git a/tests/components/hardware/test_websocket_api.py b/tests/components/hardware/test_websocket_api.py new file mode 100644 index 00000000000..116879aa628 --- /dev/null +++ b/tests/components/hardware/test_websocket_api.py @@ -0,0 +1,18 @@ +"""Test the hardware websocket API.""" +from homeassistant.components.hardware.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def test_board_info(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can get the board info.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "hardware/info"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] == {"hardware": []} diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 6f4b9a39a9f..ff595aaa602 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -20,8 +20,19 @@ from tests.common import MockConfigEntry, async_fire_time_changed MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"} +@pytest.fixture() +def os_info(): + """Mock os/info.""" + return { + "json": { + "result": "ok", + "data": {"version_latest": "1.0.0", "version": "1.0.0"}, + } + } + + @pytest.fixture(autouse=True) -def mock_all(aioclient_mock, request): +def mock_all(aioclient_mock, request, os_info): """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"}) @@ -64,7 +75,7 @@ def mock_all(aioclient_mock, request): ) aioclient_mock.get( "http://127.0.0.1/os/info", - json={"result": "ok", "data": {"version_latest": "1.0.0", "version": "1.0.0"}}, + **os_info, ) aioclient_mock.get( "http://127.0.0.1/supervisor/info", @@ -701,3 +712,29 @@ async def test_coordinator_updates(hass, caplog): ) assert refresh_updates_mock.call_count == 1 assert "Error on Supervisor API: Unknown" in caplog.text + + +@pytest.mark.parametrize( + "os_info", + [ + { + "json": { + "result": "ok", + "data": {"version_latest": "1.0.0", "version": "1.0.0", "board": "rpi"}, + } + } + ], +) +async def test_setup_hardware_integration(hass, aioclient_mock): + """Test setup initiates hardware integration.""" + + with patch.dict(os.environ, MOCK_ENVIRON), patch( + "homeassistant.components.raspberry_pi.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await async_setup_component(hass, "hassio", {"hassio": {}}) + assert result + await hass.async_block_till_done() + + assert aioclient_mock.call_count == 15 + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/raspberry_pi/__init__.py b/tests/components/raspberry_pi/__init__.py new file mode 100644 index 00000000000..0b10b50c6f0 --- /dev/null +++ b/tests/components/raspberry_pi/__init__.py @@ -0,0 +1 @@ +"""Tests for the Raspberry Pi integration.""" diff --git a/tests/components/raspberry_pi/test_config_flow.py b/tests/components/raspberry_pi/test_config_flow.py new file mode 100644 index 00000000000..dfad1100cad --- /dev/null +++ b/tests/components/raspberry_pi/test_config_flow.py @@ -0,0 +1,58 @@ +"""Test the Raspberry Pi config flow.""" +from unittest.mock import patch + +from homeassistant.components.raspberry_pi.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY + +from tests.common import MockConfigEntry, MockModule, mock_integration + + +async def test_config_flow(hass: HomeAssistant) -> None: + """Test the config flow.""" + mock_integration(hass, MockModule("hassio")) + + with patch( + "homeassistant.components.raspberry_pi.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "system"} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Raspberry Pi" + assert result["data"] == {} + assert result["options"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == {} + assert config_entry.title == "Raspberry Pi" + + +async def test_config_flow_single_entry(hass: HomeAssistant) -> None: + """Test only a single entry is allowed.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Raspberry Pi", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.raspberry_pi.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "system"} + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + mock_setup_entry.assert_not_called() diff --git a/tests/components/raspberry_pi/test_hardware.py b/tests/components/raspberry_pi/test_hardware.py new file mode 100644 index 00000000000..748972c8d60 --- /dev/null +++ b/tests/components/raspberry_pi/test_hardware.py @@ -0,0 +1,57 @@ +"""Test the Raspberry Pi hardware platform.""" +import pytest + +from homeassistant.components.hassio import DATA_OS_INFO +from homeassistant.components.raspberry_pi.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockModule, mock_integration + + +async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can get the board info.""" + mock_integration(hass, MockModule("hassio")) + hass.data[DATA_OS_INFO] = {"board": "rpi"} + + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "hardware/info"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] == { + "hardware": [ + { + "board": { + "hassio_board_id": "rpi", + "manufacturer": "raspberry_pi", + "model": "1", + "revision": None, + }, + "name": "Raspberry Pi", + "url": None, + } + ] + } + + +@pytest.mark.parametrize("os_info", [None, {"board": None}, {"board": "other"}]) +async def test_hardware_info_fail(hass: HomeAssistant, hass_ws_client, os_info) -> None: + """Test async_info raises if os_info is not as expected.""" + mock_integration(hass, MockModule("hassio")) + hass.data[DATA_OS_INFO] = os_info + + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "hardware/info"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] == {"hardware": []} diff --git a/tests/components/raspberry_pi/test_init.py b/tests/components/raspberry_pi/test_init.py new file mode 100644 index 00000000000..dd86da7bce0 --- /dev/null +++ b/tests/components/raspberry_pi/test_init.py @@ -0,0 +1,72 @@ +"""Test the Raspberry Pi integration.""" +from unittest.mock import patch + +from homeassistant.components.raspberry_pi.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, MockModule, mock_integration + + +async def test_setup_entry(hass: HomeAssistant) -> None: + """Test setup of a config entry.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Raspberry Pi", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.raspberry_pi.get_os_info", + return_value={"board": "rpi"}, + ) as mock_get_os_info: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + + +async def test_setup_entry_wrong_board(hass: HomeAssistant) -> None: + """Test setup of a config entry with wrong board type.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Raspberry Pi", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.raspberry_pi.get_os_info", + return_value={"board": "generic-x86-64"}, + ) as mock_get_os_info: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + + +async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None: + """Test setup of a config entry when hassio has not fetched os_info.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Raspberry Pi", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.raspberry_pi.get_os_info", + return_value=None, + ) as mock_get_os_info: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + assert config_entry.state == ConfigEntryState.SETUP_RETRY From 3c246b78004917d494e6c81a389e73bee4f0bf70 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 25 May 2022 20:42:14 +0200 Subject: [PATCH 0906/3516] Update mypy to 0.960 (#72481) --- homeassistant/components/recorder/purge.py | 4 ++-- homeassistant/components/sonos/helpers.py | 2 +- homeassistant/util/color.py | 6 +++--- requirements_test.txt | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 432e3993add..10136dfb5a6 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -315,7 +315,7 @@ def _select_unused_attributes_ids( seen_ids |= { attrs_id[0] for attrs_id in session.execute( - attributes_ids_exist_in_states(*attr_ids) + attributes_ids_exist_in_states(*attr_ids) # type: ignore[arg-type] ).all() if attrs_id[0] is not None } @@ -362,7 +362,7 @@ def _select_unused_event_data_ids( seen_ids |= { data_id[0] for data_id in session.execute( - data_ids_exist_in_events(*data_ids_group) + data_ids_exist_in_events(*data_ids_group) # type: ignore[arg-type] ).all() if data_id[0] is not None } diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index 22d9f6c08a5..9e8fcbd6a12 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -62,7 +62,7 @@ def soco_error( def wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> _R | None: """Wrap for all soco UPnP exception.""" - args_soco = next((arg for arg in args if isinstance(arg, SoCo)), None) # type: ignore[attr-defined] + args_soco = next((arg for arg in args if isinstance(arg, SoCo)), None) try: result = funct(self, *args, **kwargs) except (OSError, SoCoException, SoCoUPnPException) as err: diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 21c877f9377..448b417cc97 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -3,7 +3,7 @@ from __future__ import annotations import colorsys import math -from typing import NamedTuple, cast +from typing import NamedTuple import attr @@ -295,9 +295,9 @@ def color_xy_brightness_to_RGB( # Apply reverse gamma correction. r, g, b = map( - lambda x: (12.92 * x) + lambda x: (12.92 * x) # type: ignore[no-any-return] if (x <= 0.0031308) - else ((1.0 + 0.055) * cast(float, pow(x, (1.0 / 2.4))) - 0.055), + else ((1.0 + 0.055) * pow(x, (1.0 / 2.4)) - 0.055), [r, g, b], ) diff --git a/requirements_test.txt b/requirements_test.txt index 29e7364059f..7e4e29de339 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,7 +11,7 @@ codecov==2.1.12 coverage==6.4 freezegun==1.2.1 mock-open==1.4.0 -mypy==0.950 +mypy==0.960 pre-commit==2.19.0 pylint==2.13.9 pipdeptree==2.2.1 From a947c80f3086a153ac938674fe320aa6d707b885 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Wed, 25 May 2022 13:42:27 -0500 Subject: [PATCH 0907/3516] Bump Frontend to 20220525.0 (#72496) --- 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 b5fb12b92f6..6c8f568c4d2 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220524.0"], + "requirements": ["home-assistant-frontend==20220525.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7dd302fea28..5a6dc9891a6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220524.0 +home-assistant-frontend==20220525.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 27c5a401bd4..fbcada246a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -822,7 +822,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220524.0 +home-assistant-frontend==20220525.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 58a345adbfc..f347a7ea082 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -589,7 +589,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220524.0 +home-assistant-frontend==20220525.0 # homeassistant.components.home_connect homeconnect==0.7.0 From b8d260f6eabe3fe20cd444846e621e3ea794e01a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 25 May 2022 20:49:10 +0200 Subject: [PATCH 0908/3516] Bumped version to 2022.6.0b0 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 74fe6adfdfd..e437c171c50 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 825a4407012..c9b915b229a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.0.dev0 +version = 2022.6.0b0 url = https://www.home-assistant.io/ [options] From 3e0e8dd1059ea56e599d985b3e4660f041a90d65 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 25 May 2022 21:57:55 +0200 Subject: [PATCH 0909/3516] Bump version to 2022.7.0dev0 (#72500) --- .github/workflows/ci.yaml | 2 +- homeassistant/const.py | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fb659cf21d2..6264c1379fd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ on: env: CACHE_VERSION: 9 PIP_CACHE_VERSION: 3 - HA_SHORT_VERSION: 2022.6 + HA_SHORT_VERSION: 2022.7 DEFAULT_PYTHON: 3.9 PRE_COMMIT_CACHE: ~/.cache/pre-commit PIP_CACHE: /tmp/pip-cache diff --git a/homeassistant/const.py b/homeassistant/const.py index 74fe6adfdfd..7b91973a930 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -6,7 +6,7 @@ from typing import Final from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 -MINOR_VERSION: Final = 6 +MINOR_VERSION: Final = 7 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" diff --git a/setup.cfg b/setup.cfg index 825a4407012..15db31bd306 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.0.dev0 +version = 2022.7.0.dev0 url = https://www.home-assistant.io/ [options] From c181af92a2a2711ecdf7a80704c9e0152d1aa254 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 25 May 2022 13:00:48 -0700 Subject: [PATCH 0910/3516] Throw nest climate API errors as HomeAssistantErrors (#72474) --- homeassistant/components/nest/climate_sdm.py | 34 +++++++---- tests/components/nest/test_climate_sdm.py | 60 ++++++++++++++++++++ 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index bbbf83501f7..8a56f78028b 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -6,6 +6,7 @@ from typing import Any, cast from google_nest_sdm.device import Device from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.device_traits import FanTrait, TemperatureTrait +from google_nest_sdm.exceptions import ApiException from google_nest_sdm.thermostat_traits import ( ThermostatEcoTrait, ThermostatHeatCoolTrait, @@ -30,6 +31,7 @@ from homeassistant.components.climate.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -294,7 +296,10 @@ class ThermostatEntity(ClimateEntity): hvac_mode = HVACMode.OFF api_mode = THERMOSTAT_INV_MODE_MAP[hvac_mode] trait = self._device.traits[ThermostatModeTrait.NAME] - await trait.set_mode(api_mode) + try: + await trait.set_mode(api_mode) + except ApiException as err: + raise HomeAssistantError(f"Error setting HVAC mode: {err}") from err async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" @@ -308,20 +313,26 @@ class ThermostatEntity(ClimateEntity): if ThermostatTemperatureSetpointTrait.NAME not in self._device.traits: return trait = self._device.traits[ThermostatTemperatureSetpointTrait.NAME] - if self.preset_mode == PRESET_ECO or hvac_mode == HVACMode.HEAT_COOL: - if low_temp and high_temp: - await trait.set_range(low_temp, high_temp) - elif hvac_mode == HVACMode.COOL and temp: - await trait.set_cool(temp) - elif hvac_mode == HVACMode.HEAT and temp: - await trait.set_heat(temp) + try: + if self.preset_mode == PRESET_ECO or hvac_mode == HVACMode.HEAT_COOL: + if low_temp and high_temp: + await trait.set_range(low_temp, high_temp) + elif hvac_mode == HVACMode.COOL and temp: + await trait.set_cool(temp) + elif hvac_mode == HVACMode.HEAT and temp: + await trait.set_heat(temp) + except ApiException as err: + raise HomeAssistantError(f"Error setting HVAC mode: {err}") from err async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new target preset mode.""" if preset_mode not in self.preset_modes: raise ValueError(f"Unsupported preset_mode '{preset_mode}'") trait = self._device.traits[ThermostatEcoTrait.NAME] - await trait.set_mode(PRESET_INV_MODE_MAP[preset_mode]) + try: + await trait.set_mode(PRESET_INV_MODE_MAP[preset_mode]) + except ApiException as err: + raise HomeAssistantError(f"Error setting HVAC mode: {err}") from err async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" @@ -331,4 +342,7 @@ class ThermostatEntity(ClimateEntity): duration = None if fan_mode != FAN_OFF: duration = MAX_FAN_DURATION - await trait.set_timer(FAN_INV_MODE_MAP[fan_mode], duration=duration) + try: + await trait.set_timer(FAN_INV_MODE_MAP[fan_mode], duration=duration) + except ApiException as err: + raise HomeAssistantError(f"Error setting HVAC mode: {err}") from err diff --git a/tests/components/nest/test_climate_sdm.py b/tests/components/nest/test_climate_sdm.py index 5f3efa362b3..123742607ad 100644 --- a/tests/components/nest/test_climate_sdm.py +++ b/tests/components/nest/test_climate_sdm.py @@ -6,8 +6,10 @@ pubsub subscriber. """ from collections.abc import Awaitable, Callable +from http import HTTPStatus from typing import Any +import aiohttp from google_nest_sdm.auth import AbstractAuth from google_nest_sdm.event import EventMessage import pytest @@ -41,6 +43,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from .common import ( DEVICE_COMMAND, @@ -1380,3 +1383,60 @@ async def test_thermostat_invalid_set_preset_mode( # Preset is unchanged assert thermostat.attributes[ATTR_PRESET_MODE] == PRESET_NONE assert thermostat.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_NONE] + + +async def test_thermostat_hvac_mode_failure( + hass: HomeAssistant, + setup_platform: PlatformSetup, + auth: FakeAuth, + create_device: CreateDevice, +) -> None: + """Test setting an hvac_mode that is not supported.""" + create_device.create( + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "OFF", + }, + "sdm.devices.traits.Fan": { + "timerMode": "OFF", + "timerTimeout": "2019-05-10T03:22:54Z", + }, + "sdm.devices.traits.ThermostatEco": { + "availableModes": ["MANUAL_ECO", "OFF"], + "mode": "OFF", + "heatCelsius": 15.0, + "coolCelsius": 28.0, + }, + } + ) + await setup_platform() + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + + auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)] + with pytest.raises(HomeAssistantError): + await common.async_set_hvac_mode(hass, HVAC_MODE_HEAT) + await hass.async_block_till_done() + + auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)] + with pytest.raises(HomeAssistantError): + await common.async_set_temperature( + hass, hvac_mode=HVAC_MODE_HEAT, temperature=25.0 + ) + await hass.async_block_till_done() + + auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)] + with pytest.raises(HomeAssistantError): + await common.async_set_fan_mode(hass, FAN_ON) + await hass.async_block_till_done() + + auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)] + with pytest.raises(HomeAssistantError): + await common.async_set_preset_mode(hass, PRESET_ECO) + await hass.async_block_till_done() From 30edc039aee259c6a4f451f0e574f02e2b74eacc Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 26 May 2022 00:23:39 +0000 Subject: [PATCH 0911/3516] [ci skip] Translation update --- .../components/generic/translations/de.json | 2 ++ .../components/generic/translations/et.json | 2 ++ .../components/generic/translations/fr.json | 2 ++ .../components/generic/translations/hu.json | 2 ++ .../components/generic/translations/it.json | 2 ++ .../components/generic/translations/pl.json | 2 ++ .../generic/translations/pt-BR.json | 2 ++ .../components/ialarm_xr/translations/de.json | 21 +++++++++++++++++++ .../components/ialarm_xr/translations/fr.json | 21 +++++++++++++++++++ .../components/ialarm_xr/translations/hu.json | 21 +++++++++++++++++++ .../components/ialarm_xr/translations/it.json | 21 +++++++++++++++++++ .../components/ialarm_xr/translations/pl.json | 21 +++++++++++++++++++ .../ialarm_xr/translations/pt-BR.json | 21 +++++++++++++++++++ .../totalconnect/translations/de.json | 11 ++++++++++ .../totalconnect/translations/en.json | 11 ++++++++++ .../totalconnect/translations/fr.json | 11 ++++++++++ .../totalconnect/translations/hu.json | 11 ++++++++++ .../totalconnect/translations/it.json | 11 ++++++++++ .../totalconnect/translations/pl.json | 11 ++++++++++ .../totalconnect/translations/pt-BR.json | 11 ++++++++++ 20 files changed, 217 insertions(+) create mode 100644 homeassistant/components/ialarm_xr/translations/de.json create mode 100644 homeassistant/components/ialarm_xr/translations/fr.json create mode 100644 homeassistant/components/ialarm_xr/translations/hu.json create mode 100644 homeassistant/components/ialarm_xr/translations/it.json create mode 100644 homeassistant/components/ialarm_xr/translations/pl.json create mode 100644 homeassistant/components/ialarm_xr/translations/pt-BR.json diff --git a/homeassistant/components/generic/translations/de.json b/homeassistant/components/generic/translations/de.json index d4c5c268eed..0c14e95a683 100644 --- a/homeassistant/components/generic/translations/de.json +++ b/homeassistant/components/generic/translations/de.json @@ -15,6 +15,7 @@ "stream_no_video": "Stream enth\u00e4lt kein Video", "stream_not_permitted": "Beim Versuch, eine Verbindung zum Stream herzustellen, ist ein Vorgang nicht zul\u00e4ssig. Falsches RTSP-Transportprotokoll?", "stream_unauthorised": "Autorisierung beim Versuch, eine Verbindung zum Stream herzustellen, fehlgeschlagen", + "template_error": "Fehler beim Rendern der Vorlage. \u00dcberpr\u00fcfe das Protokoll f\u00fcr weitere Informationen.", "timeout": "Zeit\u00fcberschreitung beim Laden der URL", "unable_still_load": "Es konnte kein g\u00fcltiges Bild von der Standbild-URL geladen werden (z. B. ung\u00fcltiger Host, URL oder Authentifizierungsfehler). \u00dcberpr\u00fcfe das Protokoll f\u00fcr weitere Informationen.", "unknown": "Unerwarteter Fehler" @@ -57,6 +58,7 @@ "stream_no_video": "Stream enth\u00e4lt kein Video", "stream_not_permitted": "Beim Versuch, eine Verbindung zum Stream herzustellen, ist ein Vorgang nicht zul\u00e4ssig. Falsches RTSP-Transportprotokoll?", "stream_unauthorised": "Autorisierung beim Versuch, eine Verbindung zum Stream herzustellen, fehlgeschlagen", + "template_error": "Fehler beim Rendern der Vorlage. \u00dcberpr\u00fcfe das Protokoll f\u00fcr weitere Informationen.", "timeout": "Zeit\u00fcberschreitung beim Laden der URL", "unable_still_load": "Es konnte kein g\u00fcltiges Bild von der Standbild-URL geladen werden (z. B. ung\u00fcltiger Host, URL oder Authentifizierungsfehler). \u00dcberpr\u00fcfe das Protokoll f\u00fcr weitere Informationen.", "unknown": "Unerwarteter Fehler" diff --git a/homeassistant/components/generic/translations/et.json b/homeassistant/components/generic/translations/et.json index 41e1881573e..a50e4d4aaa7 100644 --- a/homeassistant/components/generic/translations/et.json +++ b/homeassistant/components/generic/translations/et.json @@ -15,6 +15,7 @@ "stream_no_video": "Voos pole videot", "stream_not_permitted": "Vooga \u00fchenduse loomisel pole toiming lubatud. Vale RTSP transpordiprotokoll?", "stream_unauthorised": "Autoriseerimine eba\u00f5nnestus vooga \u00fchendamise ajal", + "template_error": "Viga malli renderdamisel. Lisateabe saamiseks vaata logi.", "timeout": "URL-i laadimise ajal\u00f5pp", "unable_still_load": "Pilti ei saa laadida URL-ist (nt kehtetu host, URL v\u00f5i autentimise t\u00f5rge). Lisateabe saamiseks vaata logi.", "unknown": "Ootamatu t\u00f5rge" @@ -57,6 +58,7 @@ "stream_no_video": "Voos pole videot", "stream_not_permitted": "Vooga \u00fchenduse loomisel pole toiming lubatud. Vale RTSP transpordiprotokoll?", "stream_unauthorised": "Autoriseerimine eba\u00f5nnestus vooga \u00fchendamise ajal", + "template_error": "Viga malli renderdamisel. Lisateabe saamiseks vaata logi.", "timeout": "URL-i laadimise ajal\u00f5pp", "unable_still_load": "Pilti ei saa laadida URL-ist (nt kehtetu host, URL v\u00f5i autentimise t\u00f5rge). Lisateabe saamiseks vaata logi.", "unknown": "Ootamatu t\u00f5rge" diff --git a/homeassistant/components/generic/translations/fr.json b/homeassistant/components/generic/translations/fr.json index 6215c579be7..0d517a846e7 100644 --- a/homeassistant/components/generic/translations/fr.json +++ b/homeassistant/components/generic/translations/fr.json @@ -15,6 +15,7 @@ "stream_no_video": "Le flux ne contient pas de vid\u00e9o", "stream_not_permitted": "Op\u00e9ration non autoris\u00e9e lors de la tentative de connexion au flux. Mauvais protocole de transport RTSP\u00a0?", "stream_unauthorised": "\u00c9chec de l'autorisation lors de la tentative de connexion au flux", + "template_error": "Erreur lors du rendu du mod\u00e8le. Consultez le journal pour plus d'informations.", "timeout": "D\u00e9lai d'attente expir\u00e9 lors du chargement de l'URL", "unable_still_load": "Impossible de charger une image valide depuis l'URL d'image fixe (cela pourrait \u00eatre d\u00fb \u00e0 un h\u00f4te ou \u00e0 une URL non valide, ou \u00e0 un \u00e9chec de l'authentification). Consultez le journal pour plus d'informations.", "unknown": "Erreur inattendue" @@ -57,6 +58,7 @@ "stream_no_video": "Le flux ne contient pas de vid\u00e9o", "stream_not_permitted": "Op\u00e9ration non autoris\u00e9e lors de la tentative de connexion au flux. Mauvais protocole de transport RTSP\u00a0?", "stream_unauthorised": "\u00c9chec de l'autorisation lors de la tentative de connexion au flux", + "template_error": "Erreur lors du rendu du mod\u00e8le. Consultez le journal pour plus d'informations.", "timeout": "D\u00e9lai d'attente expir\u00e9 lors du chargement de l'URL", "unable_still_load": "Impossible de charger une image valide depuis l'URL d'image fixe (cela pourrait \u00eatre d\u00fb \u00e0 un h\u00f4te ou \u00e0 une URL non valide, ou \u00e0 un \u00e9chec de l'authentification). Consultez le journal pour plus d'informations.", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/generic/translations/hu.json b/homeassistant/components/generic/translations/hu.json index 59840b3195b..76992a1fde7 100644 --- a/homeassistant/components/generic/translations/hu.json +++ b/homeassistant/components/generic/translations/hu.json @@ -15,6 +15,7 @@ "stream_no_video": "Az adatfolyamban nincs vide\u00f3", "stream_not_permitted": "A m\u0171velet nem enged\u00e9lyezett, mik\u00f6zben megpr\u00f3b\u00e1l csatlakozni a folyamhoz. Rossz fajta RTSP protokoll?", "stream_unauthorised": "A hiteles\u00edt\u00e9s meghi\u00fasult, mik\u00f6zben megpr\u00f3b\u00e1lt csatlakozni az adatfolyamhoz", + "template_error": "Hiba t\u00f6rt\u00e9nt a sablon renderel\u00e9se k\u00f6zben. Tov\u00e1bbi inform\u00e1ci\u00f3\u00e9rt tekintse \u00e1t a napl\u00f3t.", "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az URL bet\u00f6lt\u00e9se k\u00f6zben", "unable_still_load": "Nem siker\u00fclt \u00e9rv\u00e9nyes k\u00e9pet bet\u00f6lteni az \u00e1ll\u00f3k\u00e9p URL-c\u00edm\u00e9r\u0151l (pl. \u00e9rv\u00e9nytelen host, URL vagy hiteles\u00edt\u00e9si hiba). Tov\u00e1bbi inform\u00e1ci\u00f3\u00e9rt tekintse \u00e1t a napl\u00f3t.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" @@ -57,6 +58,7 @@ "stream_no_video": "Az adatfolyamban nincs vide\u00f3", "stream_not_permitted": "A m\u0171velet nem enged\u00e9lyezett, mik\u00f6zben megpr\u00f3b\u00e1l csatlakozni a folyamhoz. Rossz fajta RTSP protokoll?", "stream_unauthorised": "A hiteles\u00edt\u00e9s meghi\u00fasult, mik\u00f6zben megpr\u00f3b\u00e1lt csatlakozni az adatfolyamhoz", + "template_error": "Hiba t\u00f6rt\u00e9nt a sablon renderel\u00e9se k\u00f6zben. Tov\u00e1bbi inform\u00e1ci\u00f3\u00e9rt tekintse \u00e1t a napl\u00f3t.", "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az URL bet\u00f6lt\u00e9se k\u00f6zben", "unable_still_load": "Nem siker\u00fclt \u00e9rv\u00e9nyes k\u00e9pet bet\u00f6lteni az \u00e1ll\u00f3k\u00e9p URL-c\u00edm\u00e9r\u0151l (pl. \u00e9rv\u00e9nytelen host, URL vagy hiteles\u00edt\u00e9si hiba). Tov\u00e1bbi inform\u00e1ci\u00f3\u00e9rt tekintse \u00e1t a napl\u00f3t.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" diff --git a/homeassistant/components/generic/translations/it.json b/homeassistant/components/generic/translations/it.json index ff4b9822601..1cd63544700 100644 --- a/homeassistant/components/generic/translations/it.json +++ b/homeassistant/components/generic/translations/it.json @@ -15,6 +15,7 @@ "stream_no_video": "Il flusso non ha video", "stream_not_permitted": "Operazione non consentita durante il tentativo di connessione al . Protocollo di trasporto RTSP errato?", "stream_unauthorised": "Autorizzazione non riuscita durante il tentativo di connessione al flusso", + "template_error": "Errore durante l'esecuzione del modello. Esamina il registro per ulteriori informazioni.", "timeout": "Timeout durante il caricamento dell'URL", "unable_still_load": "Impossibile caricare un'immagine valida dall'URL dell'immagine fissa (ad es. host, URL non valido o errore di autenticazione). Esamina il registro per ulteriori informazioni.", "unknown": "Errore imprevisto" @@ -57,6 +58,7 @@ "stream_no_video": "Il flusso non ha video", "stream_not_permitted": "Operazione non consentita durante il tentativo di connessione al . Protocollo di trasporto RTSP errato?", "stream_unauthorised": "Autorizzazione non riuscita durante il tentativo di connessione al flusso", + "template_error": "Errore durante l'esecuzione del modello. Esamina il registro per ulteriori informazioni.", "timeout": "Timeout durante il caricamento dell'URL", "unable_still_load": "Impossibile caricare un'immagine valida dall'URL dell'immagine fissa (ad es. host, URL non valido o errore di autenticazione). Esamina il registro per ulteriori informazioni.", "unknown": "Errore imprevisto" diff --git a/homeassistant/components/generic/translations/pl.json b/homeassistant/components/generic/translations/pl.json index 3669fd78e3f..81817faf236 100644 --- a/homeassistant/components/generic/translations/pl.json +++ b/homeassistant/components/generic/translations/pl.json @@ -15,6 +15,7 @@ "stream_no_video": "Strumie\u0144 nie zawiera wideo", "stream_not_permitted": "Operacja nie jest dozwolona podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem. Z\u0142y protok\u00f3\u0142 transportowy RTSP?", "stream_unauthorised": "Autoryzacja nie powiod\u0142a si\u0119 podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", + "template_error": "B\u0142\u0105d renderowania szablonu. Przejrzyj log, aby uzyska\u0107 wi\u0119cej informacji.", "timeout": "Przekroczono limit czasu podczas \u0142adowania adresu URL", "unable_still_load": "Nie mo\u017cna za\u0142adowa\u0107 prawid\u0142owego obrazu z adresu URL nieruchomego obrazu (np. nieprawid\u0142owy host, adres URL lub b\u0142\u0105d uwierzytelniania). Przejrzyj logi, aby uzyska\u0107 wi\u0119cej informacji.", "unknown": "Nieoczekiwany b\u0142\u0105d" @@ -57,6 +58,7 @@ "stream_no_video": "Strumie\u0144 nie zawiera wideo", "stream_not_permitted": "Operacja nie jest dozwolona podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem. Z\u0142y protok\u00f3\u0142 transportowy RTSP?", "stream_unauthorised": "Autoryzacja nie powiod\u0142a si\u0119 podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", + "template_error": "B\u0142\u0105d renderowania szablonu. Przejrzyj log, aby uzyska\u0107 wi\u0119cej informacji.", "timeout": "Przekroczono limit czasu podczas \u0142adowania adresu URL", "unable_still_load": "Nie mo\u017cna za\u0142adowa\u0107 prawid\u0142owego obrazu z adresu URL nieruchomego obrazu (np. nieprawid\u0142owy host, adres URL lub b\u0142\u0105d uwierzytelniania). Przejrzyj logi, aby uzyska\u0107 wi\u0119cej informacji.", "unknown": "Nieoczekiwany b\u0142\u0105d" diff --git a/homeassistant/components/generic/translations/pt-BR.json b/homeassistant/components/generic/translations/pt-BR.json index ea1ede92f22..1a61cdeac97 100644 --- a/homeassistant/components/generic/translations/pt-BR.json +++ b/homeassistant/components/generic/translations/pt-BR.json @@ -15,6 +15,7 @@ "stream_no_video": "A stream n\u00e3o tem v\u00eddeo", "stream_not_permitted": "Opera\u00e7\u00e3o n\u00e3o permitida ao tentar se conectar a stream. Protocolo RTSP errado?", "stream_unauthorised": "Falha na autoriza\u00e7\u00e3o ao tentar se conectar a stream", + "template_error": "Erro ao renderizar o modelo. Revise o registro para obter mais informa\u00e7\u00f5es.", "timeout": "Tempo limite ao carregar a URL", "unable_still_load": "N\u00e3o foi poss\u00edvel carregar uma imagem v\u00e1lida do URL da imagem est\u00e1tica (por exemplo, host inv\u00e1lido, URL ou falha de autentica\u00e7\u00e3o). Revise o log para obter mais informa\u00e7\u00f5es.", "unknown": "Erro inesperado" @@ -57,6 +58,7 @@ "stream_no_video": "A stream n\u00e3o tem v\u00eddeo", "stream_not_permitted": "Opera\u00e7\u00e3o n\u00e3o permitida ao tentar se conectar a stream. Protocolo RTSP errado?", "stream_unauthorised": "Falha na autoriza\u00e7\u00e3o ao tentar se conectar a stream", + "template_error": "Erro ao renderizar o modelo. Revise o registro para obter mais informa\u00e7\u00f5es.", "timeout": "Tempo limite ao carregar a URL", "unable_still_load": "N\u00e3o foi poss\u00edvel carregar uma imagem v\u00e1lida do URL da imagem est\u00e1tica (por exemplo, host inv\u00e1lido, URL ou falha de autentica\u00e7\u00e3o). Revise o log para obter mais informa\u00e7\u00f5es.", "unknown": "Erro inesperado" diff --git a/homeassistant/components/ialarm_xr/translations/de.json b/homeassistant/components/ialarm_xr/translations/de.json new file mode 100644 index 00000000000..ccda778dcf6 --- /dev/null +++ b/homeassistant/components/ialarm_xr/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/fr.json b/homeassistant/components/ialarm_xr/translations/fr.json new file mode 100644 index 00000000000..dec09505d3e --- /dev/null +++ b/homeassistant/components/ialarm_xr/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/hu.json b/homeassistant/components/ialarm_xr/translations/hu.json new file mode 100644 index 00000000000..015de9b4bf6 --- /dev/null +++ b/homeassistant/components/ialarm_xr/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/it.json b/homeassistant/components/ialarm_xr/translations/it.json new file mode 100644 index 00000000000..52fb89a1d54 --- /dev/null +++ b/homeassistant/components/ialarm_xr/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/pl.json b/homeassistant/components/ialarm_xr/translations/pl.json new file mode 100644 index 00000000000..137973010ca --- /dev/null +++ b/homeassistant/components/ialarm_xr/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", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/pt-BR.json b/homeassistant/components/ialarm_xr/translations/pt-BR.json new file mode 100644 index 00000000000..f18e4820eac --- /dev/null +++ b/homeassistant/components/ialarm_xr/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao se conectar", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Senha", + "port": "Porta", + "username": "Nome de usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/de.json b/homeassistant/components/totalconnect/translations/de.json index 890bbb753d9..17b8ddca010 100644 --- a/homeassistant/components/totalconnect/translations/de.json +++ b/homeassistant/components/totalconnect/translations/de.json @@ -28,5 +28,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "Automatische Umgehung bei schwacher Batterie" + }, + "description": "Automatische Umgehung von Zonen, sobald sie einen niedrigen Batteriestand melden.", + "title": "TotalConnect-Optionen" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/en.json b/homeassistant/components/totalconnect/translations/en.json index 8df2b00a936..f19b6196552 100644 --- a/homeassistant/components/totalconnect/translations/en.json +++ b/homeassistant/components/totalconnect/translations/en.json @@ -28,5 +28,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "Auto bypass low battery" + }, + "description": "Automatically bypass zones the moment they report a low battery.", + "title": "TotalConnect Options" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/fr.json b/homeassistant/components/totalconnect/translations/fr.json index fcda553a018..d06fe595890 100644 --- a/homeassistant/components/totalconnect/translations/fr.json +++ b/homeassistant/components/totalconnect/translations/fr.json @@ -28,5 +28,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "Contournement automatique en cas de batterie faible" + }, + "description": "Contourner automatiquement les zones d\u00e8s qu'elles signalent une batterie faible.", + "title": "Options TotalConnect" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/hu.json b/homeassistant/components/totalconnect/translations/hu.json index 3bb2b4136c9..0b62895ddcd 100644 --- a/homeassistant/components/totalconnect/translations/hu.json +++ b/homeassistant/components/totalconnect/translations/hu.json @@ -28,5 +28,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "Automatikus kiiktat\u00e1s alacsony akkumul\u00e1torral" + }, + "description": "Automatikusan kiiktatja a z\u00f3n\u00e1kat abban a pillanatban, amikor lemer\u00fclt akkumul\u00e1tort jelentenek.", + "title": "TotalConnect be\u00e1ll\u00edt\u00e1sok" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/it.json b/homeassistant/components/totalconnect/translations/it.json index 32f0815d380..13a95fa9cff 100644 --- a/homeassistant/components/totalconnect/translations/it.json +++ b/homeassistant/components/totalconnect/translations/it.json @@ -28,5 +28,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "Bypass automatico della batteria scarica" + }, + "description": "Esclusione automatica delle zone nel momento in cui segnalano una batteria scarica.", + "title": "Opzioni TotalConnect" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/pl.json b/homeassistant/components/totalconnect/translations/pl.json index d3927212c82..07645e24ac5 100644 --- a/homeassistant/components/totalconnect/translations/pl.json +++ b/homeassistant/components/totalconnect/translations/pl.json @@ -28,5 +28,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "Automatyczne obej\u015bcie niskiego poziomu baterii" + }, + "description": "Automatycznie pomijaj strefy, kt\u00f3re zg\u0142osz\u0105 niski poziom na\u0142adowania baterii.", + "title": "Opcje TotalConnect" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/pt-BR.json b/homeassistant/components/totalconnect/translations/pt-BR.json index 1ffeb1337ec..0ead8fba802 100644 --- a/homeassistant/components/totalconnect/translations/pt-BR.json +++ b/homeassistant/components/totalconnect/translations/pt-BR.json @@ -28,5 +28,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "Ignoar autom\u00e1ticamente bateria fraca" + }, + "description": "Desative zonas automaticamente no momento em que relatam uma bateria fraca.", + "title": "Op\u00e7\u00f5es do TotalConnect" + } + } } } \ No newline at end of file From 1ac71455cbcaf05d8b9b13d69b9bd7372c92111c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 26 May 2022 02:54:49 +0200 Subject: [PATCH 0912/3516] Move remaining metadata to pyproject (#72469) --- .core_files.yaml | 2 +- .pre-commit-config.yaml | 4 ++-- CODEOWNERS | 1 + pyproject.toml | 32 ++++++++++++++++++++++++++++++-- requirements_test.txt | 1 + script/gen_requirements_all.py | 14 +++++++++----- script/hassfest/codeowners.py | 1 + script/hassfest/metadata.py | 21 +++++++++++++-------- script/version_bump.py | 8 ++++---- setup.cfg | 30 ------------------------------ 10 files changed, 62 insertions(+), 52 deletions(-) diff --git a/.core_files.yaml b/.core_files.yaml index 2928d450ce2..55b543a333e 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -132,7 +132,7 @@ requirements: &requirements - homeassistant/package_constraints.txt - script/pip_check - requirements*.txt - - setup.cfg + - pyproject.toml any: - *base_platforms diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 539308c08f1..ff00ce07e0c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -106,7 +106,7 @@ repos: pass_filenames: false language: script types: [text] - files: ^(homeassistant/.+/manifest\.json|setup\.cfg|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$ + files: ^(homeassistant/.+/manifest\.json|pyproject\.toml|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$ - id: hassfest name: hassfest entry: script/run-in-env.sh python3 -m script.hassfest @@ -120,7 +120,7 @@ repos: pass_filenames: false language: script types: [text] - files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|setup\.cfg)$ + files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml)$ - id: hassfest-mypy-config name: hassfest-mypy-config entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config diff --git a/CODEOWNERS b/CODEOWNERS index 98d60fbfcb7..3baeb6dda68 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -6,6 +6,7 @@ # Home Assistant Core setup.cfg @home-assistant/core +pyproject.toml @home-assistant/core /homeassistant/*.py @home-assistant/core /homeassistant/helpers/ @home-assistant/core /homeassistant/util/ @home-assistant/core diff --git a/pyproject.toml b/pyproject.toml index e0db5324d02..60551dae997 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,11 +4,12 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" +version = "2022.7.0.dev0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" authors = [ - {name = "The Home Assistant Authors", email = "hello@home-assistant.io"} + {name = "The Home Assistant Authors", email = "hello@home-assistant.io"} ] keywords = ["home", "automation"] classifiers = [ @@ -21,7 +22,34 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Topic :: Home Automation", ] -dynamic = ["version", "requires-python", "dependencies"] +requires-python = ">=3.9.0" +dependencies = [ + "aiohttp==3.8.1", + "astral==2.2", + "async_timeout==4.0.2", + "attrs==21.2.0", + "atomicwrites==1.4.0", + "awesomeversion==22.5.1", + "bcrypt==3.1.7", + "certifi>=2021.5.30", + "ciso8601==2.2.0", + # When bumping httpx, please check the version pins of + # httpcore, anyio, and h11 in gen_requirements_all + "httpx==0.22.0", + "ifaddr==0.1.7", + "jinja2==3.1.2", + "PyJWT==2.4.0", + # PyJWT has loose dependency. We want the latest one. + "cryptography==36.0.2", + "pip>=21.0,<22.2", + "python-slugify==4.0.1", + "pyyaml==6.0", + "requests==2.27.1", + "typing-extensions>=3.10.0.2,<5.0", + "voluptuous==0.13.1", + "voluptuous-serialize==2.5.0", + "yarl==1.7.2", +] [project.urls] "Source Code" = "https://github.com/home-assistant/core" diff --git a/requirements_test.txt b/requirements_test.txt index 7e4e29de339..e888d715cd4 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -28,6 +28,7 @@ pytest==7.1.1 requests_mock==1.9.2 respx==0.19.0 stdlib-list==0.7.0 +tomli==2.0.1;python_version<"3.11" tqdm==4.49.0 types-atomicwrites==1.4.1 types-croniter==1.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 369045e5124..a7b26d297c9 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 """Generate an updated requirements_all.txt.""" -import configparser import difflib import importlib import os @@ -12,6 +11,11 @@ import sys from homeassistant.util.yaml.loader import load_yaml from script.hassfest.model import Integration +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + COMMENT_REQUIREMENTS = ( "Adafruit_BBIO", "avea", # depends on bluepy @@ -166,10 +170,10 @@ def explore_module(package, explore_children): def core_requirements(): - """Gather core requirements out of setup.cfg.""" - parser = configparser.ConfigParser() - parser.read("setup.cfg") - return parser["options"]["install_requires"].strip().split("\n") + """Gather core requirements out of pyproject.toml.""" + with open("pyproject.toml", "rb") as fp: + data = tomllib.load(fp) + return data["project"]["dependencies"] def gather_recursive_requirements(domain, seen=None): diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index d2ee6182f22..5511bc8a518 100644 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -12,6 +12,7 @@ BASE = """ # Home Assistant Core setup.cfg @home-assistant/core +pyproject.toml @home-assistant/core /homeassistant/*.py @home-assistant/core /homeassistant/helpers/ @home-assistant/core /homeassistant/util/ @home-assistant/core diff --git a/script/hassfest/metadata.py b/script/hassfest/metadata.py index ab5ba3f036d..48459eacb72 100644 --- a/script/hassfest/metadata.py +++ b/script/hassfest/metadata.py @@ -1,31 +1,36 @@ """Package metadata validation.""" -import configparser +import sys from homeassistant.const import REQUIRED_PYTHON_VER, __version__ from .model import Config, Integration +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + def validate(integrations: dict[str, Integration], config: Config) -> None: """Validate project metadata keys.""" - metadata_path = config.root / "setup.cfg" - parser = configparser.ConfigParser() - parser.read(metadata_path) + metadata_path = config.root / "pyproject.toml" + with open(metadata_path, "rb") as fp: + data = tomllib.load(fp) try: - if parser["metadata"]["version"] != __version__: + if data["project"]["version"] != __version__: config.add_error( - "metadata", f"'metadata.version' value does not match '{__version__}'" + "metadata", f"'project.version' value does not match '{__version__}'" ) except KeyError: config.add_error("metadata", "No 'metadata.version' key found!") required_py_version = f">={'.'.join(map(str, REQUIRED_PYTHON_VER))}" try: - if parser["options"]["python_requires"] != required_py_version: + if data["project"]["requires-python"] != required_py_version: config.add_error( "metadata", - f"'options.python_requires' value doesn't match '{required_py_version}", + f"'project.requires-python' value doesn't match '{required_py_version}", ) except KeyError: config.add_error("metadata", "No 'options.python_requires' key found!") diff --git a/script/version_bump.py b/script/version_bump.py index d714c5183b7..f7dc37b5e22 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -121,13 +121,13 @@ def write_version(version): def write_version_metadata(version: Version) -> None: - """Update setup.cfg file with new version.""" - with open("setup.cfg") as fp: + """Update pyproject.toml file with new version.""" + with open("pyproject.toml", encoding="utf8") as fp: content = fp.read() - content = re.sub(r"(version\W+=\W).+\n", f"\\g<1>{version}\n", content, count=1) + content = re.sub(r"(version\W+=\W).+\n", f'\\g<1>"{version}"\n', content, count=1) - with open("setup.cfg", "w") as fp: + with open("pyproject.toml", "w", encoding="utf8") as fp: fp.write(content) diff --git a/setup.cfg b/setup.cfg index 15db31bd306..dbf815a56e9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,36 +1,6 @@ [metadata] -version = 2022.7.0.dev0 url = https://www.home-assistant.io/ -[options] -python_requires = >=3.9.0 -install_requires = - aiohttp==3.8.1 - astral==2.2 - async_timeout==4.0.2 - attrs==21.2.0 - atomicwrites==1.4.0 - awesomeversion==22.5.1 - bcrypt==3.1.7 - certifi>=2021.5.30 - ciso8601==2.2.0 - # When bumping httpx, please check the version pins of - # httpcore, anyio, and h11 in gen_requirements_all - httpx==0.22.0 - ifaddr==0.1.7 - jinja2==3.1.2 - PyJWT==2.4.0 - # PyJWT has loose dependency. We want the latest one. - cryptography==36.0.2 - pip>=21.0,<22.2 - python-slugify==4.0.1 - pyyaml==6.0 - requests==2.27.1 - typing-extensions>=3.10.0.2,<5.0 - voluptuous==0.13.1 - voluptuous-serialize==2.5.0 - yarl==1.7.2 - [flake8] exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build max-complexity = 25 From bfa7693d18bc9467ed9e16ca9971758ffef81b04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 May 2022 15:17:08 -1000 Subject: [PATCH 0913/3516] Fixes for logbook filtering and add it to the live stream (#72501) --- homeassistant/components/logbook/processor.py | 32 ++- .../components/logbook/queries/__init__.py | 12 +- .../components/logbook/queries/all.py | 18 +- .../components/logbook/queries/common.py | 42 +--- .../components/logbook/queries/devices.py | 7 +- .../components/logbook/queries/entities.py | 9 +- homeassistant/components/recorder/filters.py | 100 +++++++--- homeassistant/components/recorder/history.py | 4 +- homeassistant/components/recorder/models.py | 40 +++- tests/components/logbook/test_init.py | 5 +- .../components/logbook/test_websocket_api.py | 185 ++++++++++++++++++ 11 files changed, 340 insertions(+), 114 deletions(-) diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index 03506695700..ea6002cc62c 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -173,12 +173,6 @@ class EventProcessor: self.filters, self.context_id, ) - if _LOGGER.isEnabledFor(logging.DEBUG): - _LOGGER.debug( - "Literal statement: %s", - stmt.compile(compile_kwargs={"literal_binds": True}), - ) - with session_scope(hass=self.hass) as session: return self.humanify(yield_rows(session.execute(stmt))) @@ -214,20 +208,16 @@ def _humanify( include_entity_name = logbook_run.include_entity_name format_time = logbook_run.format_time - def _keep_row(row: Row | EventAsRow, event_type: str) -> bool: + def _keep_row(row: EventAsRow) -> bool: """Check if the entity_filter rejects a row.""" assert entities_filter is not None - if entity_id := _row_event_data_extract(row, ENTITY_ID_JSON_EXTRACT): + if entity_id := row.entity_id: return entities_filter(entity_id) - - if event_type in external_events: - # If the entity_id isn't described, use the domain that describes - # the event for filtering. - domain: str | None = external_events[event_type][0] - else: - domain = _row_event_data_extract(row, DOMAIN_JSON_EXTRACT) - - return domain is not None and entities_filter(f"{domain}._") + if entity_id := row.data.get(ATTR_ENTITY_ID): + return entities_filter(entity_id) + if domain := row.data.get(ATTR_DOMAIN): + return entities_filter(f"{domain}._") + return True # Process rows for row in rows: @@ -236,12 +226,12 @@ def _humanify( continue event_type = row.event_type if event_type == EVENT_CALL_SERVICE or ( - event_type is not PSUEDO_EVENT_STATE_CHANGED - and entities_filter is not None - and not _keep_row(row, event_type) + entities_filter + # We literally mean is EventAsRow not a subclass of EventAsRow + and type(row) is EventAsRow # pylint: disable=unidiomatic-typecheck + and not _keep_row(row) ): continue - if event_type is PSUEDO_EVENT_STATE_CHANGED: entity_id = row.entity_id assert entity_id is not None diff --git a/homeassistant/components/logbook/queries/__init__.py b/homeassistant/components/logbook/queries/__init__.py index 3672f1e761c..3c027823612 100644 --- a/homeassistant/components/logbook/queries/__init__.py +++ b/homeassistant/components/logbook/queries/__init__.py @@ -27,8 +27,16 @@ def statement_for_request( # No entities: logbook sends everything for the timeframe # limited by the context_id and the yaml configured filter if not entity_ids and not device_ids: - entity_filter = filters.entity_filter() if filters else None - return all_stmt(start_day, end_day, event_types, entity_filter, context_id) + states_entity_filter = filters.states_entity_filter() if filters else None + events_entity_filter = filters.events_entity_filter() if filters else None + return all_stmt( + start_day, + end_day, + event_types, + states_entity_filter, + events_entity_filter, + context_id, + ) # sqlalchemy caches object quoting, the # json quotable ones must be a different diff --git a/homeassistant/components/logbook/queries/all.py b/homeassistant/components/logbook/queries/all.py index da17c7bddeb..d321578f545 100644 --- a/homeassistant/components/logbook/queries/all.py +++ b/homeassistant/components/logbook/queries/all.py @@ -22,7 +22,8 @@ def all_stmt( start_day: dt, end_day: dt, event_types: tuple[str, ...], - entity_filter: ClauseList | None = None, + states_entity_filter: ClauseList | None = None, + events_entity_filter: ClauseList | None = None, context_id: str | None = None, ) -> StatementLambdaElement: """Generate a logbook query for all entities.""" @@ -37,12 +38,17 @@ def all_stmt( _states_query_for_context_id(start_day, end_day, context_id), legacy_select_events_context_id(start_day, end_day, context_id), ) - elif entity_filter is not None: - stmt += lambda s: s.union_all( - _states_query_for_all(start_day, end_day).where(entity_filter) - ) else: - stmt += lambda s: s.union_all(_states_query_for_all(start_day, end_day)) + if events_entity_filter is not None: + stmt += lambda s: s.where(events_entity_filter) + + if states_entity_filter is not None: + stmt += lambda s: s.union_all( + _states_query_for_all(start_day, end_day).where(states_entity_filter) + ) + else: + stmt += lambda s: s.union_all(_states_query_for_all(start_day, end_day)) + stmt += lambda s: s.order_by(Events.time_fired) return stmt diff --git a/homeassistant/components/logbook/queries/common.py b/homeassistant/components/logbook/queries/common.py index 237fde3f653..6049d6beb81 100644 --- a/homeassistant/components/logbook/queries/common.py +++ b/homeassistant/components/logbook/queries/common.py @@ -1,22 +1,20 @@ """Queries for logbook.""" from __future__ import annotations -from collections.abc import Callable from datetime import datetime as dt -import json -from typing import Any import sqlalchemy -from sqlalchemy import JSON, select, type_coerce -from sqlalchemy.orm import Query, aliased +from sqlalchemy import select +from sqlalchemy.orm import Query from sqlalchemy.sql.elements import ClauseList from sqlalchemy.sql.expression import literal from sqlalchemy.sql.selectable import Select from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN from homeassistant.components.recorder.models import ( - JSON_VARIENT_CAST, - JSONB_VARIENT_CAST, + OLD_FORMAT_ATTRS_JSON, + OLD_STATE, + SHARED_ATTRS_JSON, EventData, Events, StateAttributes, @@ -30,36 +28,6 @@ CONTINUOUS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in CONTINUOUS_DOMAINS] UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%" -OLD_STATE = aliased(States, name="old_state") - - -class JSONLiteral(JSON): # type: ignore[misc] - """Teach SA how to literalize json.""" - - def literal_processor(self, dialect: str) -> Callable[[Any], str]: - """Processor to convert a value to JSON.""" - - def process(value: Any) -> str: - """Dump json.""" - return json.dumps(value) - - return process - - -EVENT_DATA_JSON = type_coerce( - EventData.shared_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) -) -OLD_FORMAT_EVENT_DATA_JSON = type_coerce( - Events.event_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) -) - -SHARED_ATTRS_JSON = type_coerce( - StateAttributes.shared_attrs.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) -) -OLD_FORMAT_ATTRS_JSON = type_coerce( - States.attributes.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) -) - PSUEDO_EVENT_STATE_CHANGED = None # Since we don't store event_types and None diff --git a/homeassistant/components/logbook/queries/devices.py b/homeassistant/components/logbook/queries/devices.py index 5e7827b87a0..64a6477017e 100644 --- a/homeassistant/components/logbook/queries/devices.py +++ b/homeassistant/components/logbook/queries/devices.py @@ -4,24 +4,21 @@ from __future__ import annotations from collections.abc import Iterable from datetime import datetime as dt -from sqlalchemy import Column, lambda_stmt, select, union_all +from sqlalchemy import lambda_stmt, select, union_all from sqlalchemy.orm import Query from sqlalchemy.sql.elements import ClauseList from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import CTE, CompoundSelect -from homeassistant.components.recorder.models import Events, States +from homeassistant.components.recorder.models import DEVICE_ID_IN_EVENT, Events, States from .common import ( - EVENT_DATA_JSON, select_events_context_id_subquery, select_events_context_only, select_events_without_states, select_states_context_only, ) -DEVICE_ID_IN_EVENT: Column = EVENT_DATA_JSON["device_id"] - def _select_device_id_context_ids_sub_query( start_day: dt, diff --git a/homeassistant/components/logbook/queries/entities.py b/homeassistant/components/logbook/queries/entities.py index 844890c23a9..4fb211688f3 100644 --- a/homeassistant/components/logbook/queries/entities.py +++ b/homeassistant/components/logbook/queries/entities.py @@ -5,20 +5,20 @@ from collections.abc import Iterable from datetime import datetime as dt import sqlalchemy -from sqlalchemy import Column, lambda_stmt, select, union_all +from sqlalchemy import lambda_stmt, select, union_all from sqlalchemy.orm import Query from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import CTE, CompoundSelect from homeassistant.components.recorder.models import ( + ENTITY_ID_IN_EVENT, ENTITY_ID_LAST_UPDATED_INDEX, + OLD_ENTITY_ID_IN_EVENT, Events, States, ) from .common import ( - EVENT_DATA_JSON, - OLD_FORMAT_EVENT_DATA_JSON, apply_states_filters, select_events_context_id_subquery, select_events_context_only, @@ -27,9 +27,6 @@ from .common import ( select_states_context_only, ) -ENTITY_ID_IN_EVENT: Column = EVENT_DATA_JSON["entity_id"] -OLD_ENTITY_ID_IN_EVENT: Column = OLD_FORMAT_EVENT_DATA_JSON["entity_id"] - def _select_entities_context_ids_sub_query( start_day: dt, diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index adc746379e6..7f1d0bc597f 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -1,14 +1,18 @@ """Provide pre-made queries on top of the recorder component.""" from __future__ import annotations -from sqlalchemy import not_, or_ +from collections.abc import Callable, Iterable +import json +from typing import Any + +from sqlalchemy import Column, not_, or_ from sqlalchemy.sql.elements import ClauseList from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS from homeassistant.helpers.typing import ConfigType -from .models import States +from .models import ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT, States DOMAIN = "history" HISTORY_FILTERS = "history_filters" @@ -59,50 +63,84 @@ class Filters: or self.included_entity_globs ) - def entity_filter(self) -> ClauseList: - """Generate the entity filter query.""" + def _generate_filter_for_columns( + self, columns: Iterable[Column], encoder: Callable[[Any], Any] + ) -> ClauseList: includes = [] if self.included_domains: - includes.append( - or_( - *[ - States.entity_id.like(f"{domain}.%") - for domain in self.included_domains - ] - ).self_group() - ) + includes.append(_domain_matcher(self.included_domains, columns, encoder)) if self.included_entities: - includes.append(States.entity_id.in_(self.included_entities)) - for glob in self.included_entity_globs: - includes.append(_glob_to_like(glob)) + includes.append(_entity_matcher(self.included_entities, columns, encoder)) + if self.included_entity_globs: + includes.append( + _globs_to_like(self.included_entity_globs, columns, encoder) + ) excludes = [] if self.excluded_domains: - excludes.append( - or_( - *[ - States.entity_id.like(f"{domain}.%") - for domain in self.excluded_domains - ] - ).self_group() - ) + excludes.append(_domain_matcher(self.excluded_domains, columns, encoder)) if self.excluded_entities: - excludes.append(States.entity_id.in_(self.excluded_entities)) - for glob in self.excluded_entity_globs: - excludes.append(_glob_to_like(glob)) + excludes.append(_entity_matcher(self.excluded_entities, columns, encoder)) + if self.excluded_entity_globs: + excludes.append( + _globs_to_like(self.excluded_entity_globs, columns, encoder) + ) if not includes and not excludes: return None if includes and not excludes: - return or_(*includes) + return or_(*includes).self_group() if not includes and excludes: - return not_(or_(*excludes)) + return not_(or_(*excludes).self_group()) - return or_(*includes) & not_(or_(*excludes)) + return or_(*includes).self_group() & not_(or_(*excludes).self_group()) + + def states_entity_filter(self) -> ClauseList: + """Generate the entity filter query.""" + + def _encoder(data: Any) -> Any: + """Nothing to encode for states since there is no json.""" + return data + + return self._generate_filter_for_columns((States.entity_id,), _encoder) + + def events_entity_filter(self) -> ClauseList: + """Generate the entity filter query.""" + _encoder = json.dumps + return or_( + (ENTITY_ID_IN_EVENT == _encoder(None)) + & (OLD_ENTITY_ID_IN_EVENT == _encoder(None)), + self._generate_filter_for_columns( + (ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT), _encoder + ).self_group(), + ) -def _glob_to_like(glob_str: str) -> ClauseList: +def _globs_to_like( + glob_strs: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] +) -> ClauseList: """Translate glob to sql.""" - return States.entity_id.like(glob_str.translate(GLOB_TO_SQL_CHARS)) + return or_( + column.like(encoder(glob_str.translate(GLOB_TO_SQL_CHARS))) + for glob_str in glob_strs + for column in columns + ) + + +def _entity_matcher( + entity_ids: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] +) -> ClauseList: + return or_( + column.in_([encoder(entity_id) for entity_id in entity_ids]) + for column in columns + ) + + +def _domain_matcher( + domains: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] +) -> ClauseList: + return or_( + column.like(encoder(f"{domain}.%")) for domain in domains for column in columns + ) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 845a2af62bf..7e8e97eafd4 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -236,7 +236,7 @@ def _significant_states_stmt( else: stmt += _ignore_domains_filter if filters and filters.has_config: - entity_filter = filters.entity_filter() + entity_filter = filters.states_entity_filter() stmt += lambda q: q.filter(entity_filter) stmt += lambda q: q.filter(States.last_updated > start_time) @@ -528,7 +528,7 @@ def _get_states_for_all_stmt( ) stmt += _ignore_domains_filter if filters and filters.has_config: - entity_filter = filters.entity_filter() + entity_filter = filters.states_entity_filter() stmt += lambda q: q.filter(entity_filter) if join_attributes: stmt += lambda q: q.outerjoin( diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 90c2e5e5616..dff8edde79f 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -1,6 +1,7 @@ """Models for SQLAlchemy.""" from __future__ import annotations +from collections.abc import Callable from datetime import datetime, timedelta import json import logging @@ -9,6 +10,7 @@ from typing import Any, TypedDict, cast, overload import ciso8601 from fnvhash import fnv1a_32 from sqlalchemy import ( + JSON, BigInteger, Boolean, Column, @@ -22,11 +24,12 @@ from sqlalchemy import ( String, Text, distinct, + type_coerce, ) from sqlalchemy.dialects import mysql, oracle, postgresql, sqlite from sqlalchemy.engine.row import Row from sqlalchemy.ext.declarative import declared_attr -from sqlalchemy.orm import declarative_base, relationship +from sqlalchemy.orm import aliased, declarative_base, relationship from sqlalchemy.orm.session import Session from homeassistant.components.websocket_api.const import ( @@ -119,6 +122,21 @@ DOUBLE_TYPE = ( .with_variant(oracle.DOUBLE_PRECISION(), "oracle") .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") ) + + +class JSONLiteral(JSON): # type: ignore[misc] + """Teach SA how to literalize json.""" + + def literal_processor(self, dialect: str) -> Callable[[Any], str]: + """Processor to convert a value to JSON.""" + + def process(value: Any) -> str: + """Dump json.""" + return json.dumps(value) + + return process + + EVENT_ORIGIN_ORDER = [EventOrigin.local, EventOrigin.remote] EVENT_ORIGIN_TO_IDX = {origin: idx for idx, origin in enumerate(EVENT_ORIGIN_ORDER)} @@ -612,6 +630,26 @@ class StatisticsRuns(Base): # type: ignore[misc,valid-type] ) +EVENT_DATA_JSON = type_coerce( + EventData.shared_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) +) +OLD_FORMAT_EVENT_DATA_JSON = type_coerce( + Events.event_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) +) + +SHARED_ATTRS_JSON = type_coerce( + StateAttributes.shared_attrs.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) +) +OLD_FORMAT_ATTRS_JSON = type_coerce( + States.attributes.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) +) + +ENTITY_ID_IN_EVENT: Column = EVENT_DATA_JSON["entity_id"] +OLD_ENTITY_ID_IN_EVENT: Column = OLD_FORMAT_EVENT_DATA_JSON["entity_id"] +DEVICE_ID_IN_EVENT: Column = EVENT_DATA_JSON["device_id"] +OLD_STATE = aliased(States, name="old_state") + + @overload def process_timestamp(ts: None) -> None: ... diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 101fb74e690..2903f29f5dc 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -510,7 +510,7 @@ async def test_exclude_described_event(hass, hass_client, recorder_mock): return { "name": "Test Name", "message": "tested a message", - "entity_id": event.data.get(ATTR_ENTITY_ID), + "entity_id": event.data[ATTR_ENTITY_ID], } def async_describe_events(hass, async_describe_event): @@ -2003,13 +2003,12 @@ async def test_include_events_domain_glob(hass, hass_client, recorder_mock): ) await async_recorder_block_till_done(hass) - # Should get excluded by domain hass.bus.async_fire( logbook.EVENT_LOGBOOK_ENTRY, { logbook.ATTR_NAME: "Alarm", logbook.ATTR_MESSAGE: "is triggered", - logbook.ATTR_DOMAIN: "switch", + logbook.ATTR_ENTITY_ID: "switch.any", }, ) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 8706ccf7617..02fea4f980f 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -14,16 +14,21 @@ from homeassistant.components.logbook import websocket_api from homeassistant.components.script import EVENT_SCRIPT_STARTED from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import ( + ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_NAME, ATTR_UNIT_OF_MEASUREMENT, + CONF_DOMAINS, + CONF_ENTITIES, + CONF_EXCLUDE, EVENT_HOMEASSISTANT_START, STATE_OFF, STATE_ON, ) from homeassistant.core import Event, HomeAssistant, State from homeassistant.helpers import device_registry +from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -457,6 +462,186 @@ async def test_get_events_with_device_ids(hass, hass_ws_client, recorder_mock): assert isinstance(results[3]["when"], float) +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_unsubscribe_logbook_stream_excluded_entities( + hass, recorder_mock, hass_ws_client +): + """Test subscribe/unsubscribe logbook stream with excluded entities.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "automation", "script") + ] + ) + await async_setup_component( + hass, + logbook.DOMAIN, + { + logbook.DOMAIN: { + CONF_EXCLUDE: { + CONF_ENTITIES: ["light.exc"], + CONF_DOMAINS: ["switch"], + CONF_ENTITY_GLOBS: "*.excluded", + } + }, + }, + ) + await hass.async_block_till_done() + init_count = sum(hass.bus.async_listeners().values()) + + hass.states.async_set("light.exc", STATE_ON) + hass.states.async_set("light.exc", STATE_OFF) + hass.states.async_set("switch.any", STATE_ON) + hass.states.async_set("switch.any", STATE_OFF) + hass.states.async_set("cover.excluded", STATE_ON) + hass.states.async_set("cover.excluded", STATE_OFF) + + hass.states.async_set("binary_sensor.is_light", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + state: State = hass.states.get("binary_sensor.is_light") + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + {"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()} + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + { + "entity_id": "binary_sensor.is_light", + "state": "off", + "when": state.last_updated.timestamp(), + } + ] + assert msg["event"]["start_time"] == now.timestamp() + assert msg["event"]["end_time"] > msg["event"]["start_time"] + assert msg["event"]["partial"] is True + + hass.states.async_set("light.exc", STATE_ON) + hass.states.async_set("light.exc", STATE_OFF) + hass.states.async_set("switch.any", STATE_ON) + hass.states.async_set("switch.any", STATE_OFF) + hass.states.async_set("cover.excluded", STATE_ON) + hass.states.async_set("cover.excluded", STATE_OFF) + hass.states.async_set("light.alpha", "on") + hass.states.async_set("light.alpha", "off") + alpha_off_state: State = hass.states.get("light.alpha") + hass.states.async_set("light.zulu", "on", {"color": "blue"}) + hass.states.async_set("light.zulu", "off", {"effect": "help"}) + zulu_off_state: State = hass.states.get("light.zulu") + hass.states.async_set( + "light.zulu", "on", {"effect": "help", "color": ["blue", "green"]} + ) + zulu_on_state: State = hass.states.get("light.zulu") + await hass.async_block_till_done() + + hass.states.async_remove("light.zulu") + await hass.async_block_till_done() + + hass.states.async_set("light.zulu", "on", {"effect": "help", "color": "blue"}) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [ + { + "entity_id": "light.alpha", + "state": "off", + "when": alpha_off_state.last_updated.timestamp(), + }, + { + "entity_id": "light.zulu", + "state": "off", + "when": zulu_off_state.last_updated.timestamp(), + }, + { + "entity_id": "light.zulu", + "state": "on", + "when": zulu_on_state.last_updated.timestamp(), + }, + ] + + await async_wait_recording_done(hass) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation 3", ATTR_ENTITY_ID: "cover.excluded"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + { + ATTR_NAME: "Mock automation switch matching entity", + ATTR_ENTITY_ID: "switch.match_domain", + }, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation switch matching domain", ATTR_DOMAIN: "switch"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation matches nothing"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation 3", ATTR_ENTITY_ID: "light.keep"}, + ) + hass.states.async_set("cover.excluded", STATE_ON) + hass.states.async_set("cover.excluded", STATE_OFF) + await hass.async_block_till_done() + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + { + "context_id": ANY, + "domain": "automation", + "entity_id": None, + "message": "triggered", + "name": "Mock automation matches nothing", + "source": None, + "when": ANY, + }, + { + "context_id": ANY, + "domain": "automation", + "entity_id": "light.keep", + "message": "triggered", + "name": "Mock automation 3", + "source": None, + "when": ANY, + }, + ] + + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) async def test_subscribe_unsubscribe_logbook_stream( hass, recorder_mock, hass_ws_client From 2863c7ee5b2c98fef80aedf17806ba4ac79c0d63 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 26 May 2022 04:31:17 +0200 Subject: [PATCH 0914/3516] Adjust config-flow type hints in sonarr (#72412) * Adjust config-flow type hints in sonarr * Use mapping for reauth * Update init --- .../components/sonarr/config_flow.py | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py index 0bdcca6c033..8e34d7a7ed4 100644 --- a/homeassistant/components/sonarr/config_flow.py +++ b/homeassistant/components/sonarr/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Sonarr.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -10,7 +11,7 @@ from aiopyarr.sonarr_client import SonarrClient import voluptuous as vol import yarl -from homeassistant.config_entries import ConfigFlow, OptionsFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult @@ -28,7 +29,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def validate_input(hass: HomeAssistant, data: dict) -> None: +async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -52,27 +53,28 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 2 - def __init__(self): + def __init__(self) -> None: """Initialize the flow.""" - self.entry = None + self.entry: ConfigEntry | None = None @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow(config_entry: ConfigEntry) -> SonarrOptionsFlowHandler: """Get the options flow for this handler.""" return SonarrOptionsFlowHandler(config_entry) - async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( - self, user_input: dict[str, str] | None = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm reauth dialog.""" if user_input is None: + assert self.entry is not None return self.async_show_form( step_id="reauth_confirm", description_placeholders={"url": self.entry.data[CONF_URL]}, @@ -95,7 +97,7 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): user_input[CONF_VERIFY_SSL] = DEFAULT_VERIFY_SSL try: - await validate_input(self.hass, user_input) + await _validate_input(self.hass, user_input) except ArrAuthenticationException: errors = {"base": "invalid_auth"} except ArrException: @@ -120,19 +122,20 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def _async_reauth_update_entry(self, data: dict) -> FlowResult: + async def _async_reauth_update_entry(self, data: dict[str, Any]) -> FlowResult: """Update existing config entry.""" + assert self.entry is not None self.hass.config_entries.async_update_entry(self.entry, data=data) await self.hass.config_entries.async_reload(self.entry.entry_id) return self.async_abort(reason="reauth_successful") - def _get_user_data_schema(self) -> dict[str, Any]: + def _get_user_data_schema(self) -> dict[vol.Marker, type]: """Get the data schema to display user form.""" if self.entry: return {vol.Required(CONF_API_KEY): str} - data_schema: dict[str, Any] = { + data_schema: dict[vol.Marker, type] = { vol.Required(CONF_URL): str, vol.Required(CONF_API_KEY): str, } @@ -148,11 +151,13 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): class SonarrOptionsFlowHandler(OptionsFlow): """Handle Sonarr client options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input: dict[str, int] | None = None): + async def async_step_init( + self, user_input: dict[str, int] | None = None + ) -> FlowResult: """Manage Sonarr options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) From 3537fa1dab21828d65aee1809d88172081ae7f51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 May 2022 17:02:21 -1000 Subject: [PATCH 0915/3516] Fix flux_led taking a long time to recover after offline (#72507) --- homeassistant/components/flux_led/__init__.py | 21 +++++- .../components/flux_led/config_flow.py | 14 +++- homeassistant/components/flux_led/const.py | 2 + .../components/flux_led/coordinator.py | 5 +- .../components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/flux_led/test_init.py | 70 ++++++++++++++++++- 8 files changed, 110 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 17dc28a5edf..e6c1393154a 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -15,7 +15,10 @@ from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.event import ( async_track_time_change, async_track_time_interval, @@ -27,6 +30,7 @@ from .const import ( DISCOVER_SCAN_TIMEOUT, DOMAIN, FLUX_LED_DISCOVERY, + FLUX_LED_DISCOVERY_SIGNAL, FLUX_LED_EXCEPTIONS, SIGNAL_STATE_UPDATED, STARTUP_SCAN_TIMEOUT, @@ -196,6 +200,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # to avoid a race condition where the add_update_listener is not # in place in time for the check in async_update_entry_from_discovery entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + + async def _async_handle_discovered_device() -> None: + """Handle device discovery.""" + # Force a refresh if the device is now available + if not coordinator.last_update_success: + coordinator.force_next_update = True + await coordinator.async_refresh() + + entry.async_on_unload( + async_dispatcher_connect( + hass, + FLUX_LED_DISCOVERY_SIGNAL.format(entry_id=entry.entry_id), + _async_handle_discovered_device, + ) + ) return True diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index dfb6ff4a174..61395d744b3 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -21,6 +21,7 @@ from homeassistant.const import CONF_HOST from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import DiscoveryInfoType from . import async_wifi_bulb_for_host @@ -31,6 +32,7 @@ from .const import ( DEFAULT_EFFECT_SPEED, DISCOVER_SCAN_TIMEOUT, DOMAIN, + FLUX_LED_DISCOVERY_SIGNAL, FLUX_LED_EXCEPTIONS, TRANSITION_GRADUAL, TRANSITION_JUMP, @@ -109,12 +111,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): and ":" in entry.unique_id and mac_matches_by_one(entry.unique_id, mac) ): - if async_update_entry_from_discovery( - self.hass, entry, device, None, allow_update_mac + if ( + async_update_entry_from_discovery( + self.hass, entry, device, None, allow_update_mac + ) + or entry.state == config_entries.ConfigEntryState.SETUP_RETRY ): self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) ) + else: + async_dispatcher_send( + self.hass, + FLUX_LED_DISCOVERY_SIGNAL.format(entry_id=entry.entry_id), + ) raise AbortFlow("already_configured") async def _async_handle_discovery(self) -> FlowResult: diff --git a/homeassistant/components/flux_led/const.py b/homeassistant/components/flux_led/const.py index 7fa841ec77f..db545aa1e68 100644 --- a/homeassistant/components/flux_led/const.py +++ b/homeassistant/components/flux_led/const.py @@ -74,3 +74,5 @@ EFFECT_SPEED_SUPPORT_MODES: Final = {ColorMode.RGB, ColorMode.RGBW, ColorMode.RG 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_LED_DISCOVERY_SIGNAL = "flux_led_discovery_{entry_id}" diff --git a/homeassistant/components/flux_led/coordinator.py b/homeassistant/components/flux_led/coordinator.py index 5f2c3c097c0..5a7b3c89216 100644 --- a/homeassistant/components/flux_led/coordinator.py +++ b/homeassistant/components/flux_led/coordinator.py @@ -30,6 +30,7 @@ class FluxLedUpdateCoordinator(DataUpdateCoordinator): self.device = device self.title = entry.title self.entry = entry + self.force_next_update = False super().__init__( hass, _LOGGER, @@ -45,6 +46,8 @@ class FluxLedUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> None: """Fetch all device and sensor data from api.""" try: - await self.device.async_update() + await self.device.async_update(force=self.force_next_update) except FLUX_LED_EXCEPTIONS as ex: raise UpdateFailed(ex) from ex + finally: + self.force_next_update = False diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index d2eb4e1e2e0..7ccd708f89b 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.28.29"], + "requirements": ["flux_led==0.28.30"], "quality_scale": "platinum", "codeowners": ["@icemanch", "@bdraco"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index fbcada246a7..054f38972db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -657,7 +657,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.29 +flux_led==0.28.30 # homeassistant.components.homekit # homeassistant.components.recorder diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f347a7ea082..f35d8da8ec8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -466,7 +466,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.29 +flux_led==0.28.30 # homeassistant.components.homekit # homeassistant.components.recorder diff --git a/tests/components/flux_led/test_init.py b/tests/components/flux_led/test_init.py index b0a2c5dd33b..3504dbf3bea 100644 --- a/tests/components/flux_led/test_init.py +++ b/tests/components/flux_led/test_init.py @@ -2,10 +2,11 @@ from __future__ import annotations from datetime import timedelta -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest +from homeassistant import config_entries from homeassistant.components import flux_led from homeassistant.components.flux_led.const import ( CONF_REMOTE_ACCESS_ENABLED, @@ -19,6 +20,8 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STARTED, + STATE_ON, + STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -27,6 +30,7 @@ from homeassistant.util.dt import utcnow from . import ( DEFAULT_ENTRY_TITLE, + DHCP_DISCOVERY, FLUX_DISCOVERY, FLUX_DISCOVERY_PARTIAL, IP_ADDRESS, @@ -113,6 +117,70 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: assert config_entry.state == ConfigEntryState.SETUP_RETRY +async def test_config_entry_retry_right_away_on_discovery(hass: HomeAssistant) -> None: + """Test discovery makes the config entry reload if its in a retry state.""" + 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 + + with _patch_discovery(), _patch_wifibulb(): + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=DHCP_DISCOVERY, + ) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + + +async def test_coordinator_retry_right_away_on_discovery_already_setup( + hass: HomeAssistant, +) -> None: + """Test discovery makes the coordinator force poll if its already setup.""" + 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(), _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.LOADED + + entity_id = "light.bulb_rgbcw_ddeeff" + 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) + async_fire_time_changed(hass, now + timedelta(seconds=50)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + bulb.async_update = AsyncMock() + + with _patch_discovery(), _patch_wifibulb(): + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=DHCP_DISCOVERY, + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + @pytest.mark.parametrize( "discovery,title", [ From c1f62d03a0460955e7a01311289a07330abcfb29 Mon Sep 17 00:00:00 2001 From: Marcio Granzotto Rodrigues Date: Thu, 26 May 2022 01:12:43 -0300 Subject: [PATCH 0916/3516] Fix bond device state with v3 firmwares (#72516) --- homeassistant/components/bond/__init__.py | 2 +- homeassistant/components/bond/button.py | 2 +- homeassistant/components/bond/config_flow.py | 2 +- homeassistant/components/bond/cover.py | 2 +- homeassistant/components/bond/entity.py | 10 +++-- homeassistant/components/bond/fan.py | 2 +- homeassistant/components/bond/light.py | 2 +- homeassistant/components/bond/manifest.json | 4 +- homeassistant/components/bond/switch.py | 2 +- homeassistant/components/bond/utils.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bond/common.py | 2 +- tests/components/bond/test_button.py | 2 +- tests/components/bond/test_cover.py | 2 +- tests/components/bond/test_entity.py | 42 +++++++++++++++----- tests/components/bond/test_fan.py | 2 +- tests/components/bond/test_init.py | 2 +- tests/components/bond/test_light.py | 2 +- tests/components/bond/test_switch.py | 2 +- 20 files changed, 59 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 062c1d844c4..557e68272c2 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -5,7 +5,7 @@ import logging from typing import Any from aiohttp import ClientError, ClientResponseError, ClientTimeout -from bond_api import Bond, BPUPSubscriptions, start_bpup +from bond_async import Bond, BPUPSubscriptions, start_bpup from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( diff --git a/homeassistant/components/bond/button.py b/homeassistant/components/bond/button.py index 0152bedde23..0465e4c51fe 100644 --- a/homeassistant/components/bond/button.py +++ b/homeassistant/components/bond/button.py @@ -5,7 +5,7 @@ from dataclasses import dataclass import logging from typing import Any -from bond_api import Action, BPUPSubscriptions +from bond_async import Action, BPUPSubscriptions from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index d3a7b4adf72..6eba9897468 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -6,7 +6,7 @@ import logging from typing import Any from aiohttp import ClientConnectionError, ClientResponseError -from bond_api import Bond +from bond_async import Bond import voluptuous as vol from homeassistant import config_entries, exceptions diff --git a/homeassistant/components/bond/cover.py b/homeassistant/components/bond/cover.py index a50f7b93bbb..3938de0d4bd 100644 --- a/homeassistant/components/bond/cover.py +++ b/homeassistant/components/bond/cover.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from bond_api import Action, BPUPSubscriptions, DeviceType +from bond_async import Action, BPUPSubscriptions, DeviceType from homeassistant.components.cover import ( ATTR_POSITION, diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index 583f1cd96f7..832e9b5d464 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -8,7 +8,7 @@ import logging from typing import Any from aiohttp import ClientError -from bond_api import BPUPSubscriptions +from bond_async import BPUPSubscriptions from homeassistant.const import ( ATTR_HW_VERSION, @@ -156,9 +156,13 @@ class BondEntity(Entity): self._apply_state(state) @callback - def _async_bpup_callback(self, state: dict) -> None: + def _async_bpup_callback(self, json_msg: dict) -> None: """Process a state change from BPUP.""" - self._async_state_callback(state) + topic = json_msg["t"] + if topic != f"devices/{self._device_id}/state": + return + + self._async_state_callback(json_msg["b"]) self.async_write_ha_state() async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index 9acc7874657..f2f6b15f923 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -6,7 +6,7 @@ import math from typing import Any from aiohttp.client_exceptions import ClientResponseError -from bond_api import Action, BPUPSubscriptions, DeviceType, Direction +from bond_async import Action, BPUPSubscriptions, DeviceType, Direction import voluptuous as vol from homeassistant.components.fan import ( diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index c0c3fc428b8..55084f37b03 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -5,7 +5,7 @@ import logging from typing import Any from aiohttp.client_exceptions import ClientResponseError -from bond_api import Action, BPUPSubscriptions, DeviceType +from bond_async import Action, BPUPSubscriptions, DeviceType import voluptuous as vol from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index 187602057c0..52e9dd1763f 100644 --- a/homeassistant/components/bond/manifest.json +++ b/homeassistant/components/bond/manifest.json @@ -3,10 +3,10 @@ "name": "Bond", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bond", - "requirements": ["bond-api==0.1.18"], + "requirements": ["bond-async==0.1.20"], "zeroconf": ["_bond._tcp.local."], "codeowners": ["@bdraco", "@prystupa", "@joshs85", "@marciogranzotto"], "quality_scale": "platinum", "iot_class": "local_push", - "loggers": ["bond_api"] + "loggers": ["bond_async"] } diff --git a/homeassistant/components/bond/switch.py b/homeassistant/components/bond/switch.py index 01c224d8307..da0b19dd9ff 100644 --- a/homeassistant/components/bond/switch.py +++ b/homeassistant/components/bond/switch.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from aiohttp.client_exceptions import ClientResponseError -from bond_api import Action, BPUPSubscriptions, DeviceType +from bond_async import Action, BPUPSubscriptions, DeviceType import voluptuous as vol from homeassistant.components.switch import SwitchEntity diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index fc78c5758c1..cba213d9450 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -5,7 +5,7 @@ import logging from typing import Any, cast from aiohttp import ClientResponseError -from bond_api import Action, Bond +from bond_async import Action, Bond from homeassistant.util.async_ import gather_with_concurrency diff --git a/requirements_all.txt b/requirements_all.txt index 054f38972db..2404e4331a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -417,7 +417,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bond -bond-api==0.1.18 +bond-async==0.1.20 # homeassistant.components.bosch_shc boschshcpy==0.2.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f35d8da8ec8..0153a1b3323 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -318,7 +318,7 @@ blebox_uniapi==1.3.3 blinkpy==0.19.0 # homeassistant.components.bond -bond-api==0.1.18 +bond-async==0.1.20 # homeassistant.components.bosch_shc boschshcpy==0.2.30 diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index 9c53c0afb8b..4b45a4016c0 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -8,7 +8,7 @@ from typing import Any from unittest.mock import MagicMock, patch from aiohttp.client_exceptions import ClientResponseError -from bond_api import DeviceType +from bond_async import DeviceType from homeassistant import core from homeassistant.components.bond.const import DOMAIN as BOND_DOMAIN diff --git a/tests/components/bond/test_button.py b/tests/components/bond/test_button.py index ee6e98b8462..4411b25657b 100644 --- a/tests/components/bond/test_button.py +++ b/tests/components/bond/test_button.py @@ -1,6 +1,6 @@ """Tests for the Bond button device.""" -from bond_api import Action, DeviceType +from bond_async import Action, DeviceType from homeassistant import core from homeassistant.components.bond.button import STEP_SIZE diff --git a/tests/components/bond/test_cover.py b/tests/components/bond/test_cover.py index ca467d4a38d..ccb44402a3e 100644 --- a/tests/components/bond/test_cover.py +++ b/tests/components/bond/test_cover.py @@ -1,7 +1,7 @@ """Tests for the Bond cover device.""" from datetime import timedelta -from bond_api import Action, DeviceType +from bond_async import Action, DeviceType from homeassistant import core from homeassistant.components.cover import ( diff --git a/tests/components/bond/test_entity.py b/tests/components/bond/test_entity.py index 122e9c2f04e..9245f4513ed 100644 --- a/tests/components/bond/test_entity.py +++ b/tests/components/bond/test_entity.py @@ -3,7 +3,8 @@ import asyncio from datetime import timedelta from unittest.mock import patch -from bond_api import BPUPSubscriptions, DeviceType +from bond_async import BPUPSubscriptions, DeviceType +from bond_async.bpup import BPUP_ALIVE_TIMEOUT from homeassistant import core from homeassistant.components import fan @@ -44,24 +45,47 @@ async def test_bpup_goes_offline_and_recovers_same_entity(hass: core.HomeAssista bpup_subs.notify( { "s": 200, - "t": "bond/test-device-id/update", + "t": "devices/test-device-id/state", "b": {"power": 1, "speed": 3, "direction": 0}, } ) await hass.async_block_till_done() assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 100 + # Send a message for the wrong device to make sure its ignored + # we should never get this callback + bpup_subs.notify( + { + "s": 200, + "t": "devices/other-device-id/state", + "b": {"power": 1, "speed": 1, "direction": 0}, + } + ) + await hass.async_block_till_done() + assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 100 + + # Test we ignore messages for the wrong topic + bpup_subs.notify( + { + "s": 200, + "t": "devices/test-device-id/other_topic", + "b": {"power": 1, "speed": 1, "direction": 0}, + } + ) + await hass.async_block_till_done() + assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 100 + bpup_subs.notify( { "s": 200, - "t": "bond/test-device-id/update", + "t": "devices/test-device-id/state", "b": {"power": 1, "speed": 1, "direction": 0}, } ) await hass.async_block_till_done() assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 33 - bpup_subs.last_message_time = 0 + bpup_subs.last_message_time = -BPUP_ALIVE_TIMEOUT with patch_bond_device_state(side_effect=asyncio.TimeoutError): async_fire_time_changed(hass, utcnow() + timedelta(seconds=230)) await hass.async_block_till_done() @@ -75,7 +99,7 @@ async def test_bpup_goes_offline_and_recovers_same_entity(hass: core.HomeAssista bpup_subs.notify( { "s": 200, - "t": "bond/test-device-id/update", + "t": "devices/test-device-id/state", "b": {"power": 1, "speed": 2, "direction": 0}, } ) @@ -106,7 +130,7 @@ async def test_bpup_goes_offline_and_recovers_different_entity( bpup_subs.notify( { "s": 200, - "t": "bond/test-device-id/update", + "t": "devices/test-device-id/state", "b": {"power": 1, "speed": 3, "direction": 0}, } ) @@ -116,14 +140,14 @@ async def test_bpup_goes_offline_and_recovers_different_entity( bpup_subs.notify( { "s": 200, - "t": "bond/test-device-id/update", + "t": "devices/test-device-id/state", "b": {"power": 1, "speed": 1, "direction": 0}, } ) await hass.async_block_till_done() assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 33 - bpup_subs.last_message_time = 0 + bpup_subs.last_message_time = -BPUP_ALIVE_TIMEOUT with patch_bond_device_state(side_effect=asyncio.TimeoutError): async_fire_time_changed(hass, utcnow() + timedelta(seconds=230)) await hass.async_block_till_done() @@ -133,7 +157,7 @@ async def test_bpup_goes_offline_and_recovers_different_entity( bpup_subs.notify( { "s": 200, - "t": "bond/not-this-device-id/update", + "t": "devices/not-this-device-id/state", "b": {"power": 1, "speed": 2, "direction": 0}, } ) diff --git a/tests/components/bond/test_fan.py b/tests/components/bond/test_fan.py index 061e94595bf..7c860e68efc 100644 --- a/tests/components/bond/test_fan.py +++ b/tests/components/bond/test_fan.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta from unittest.mock import call -from bond_api import Action, DeviceType, Direction +from bond_async import Action, DeviceType, Direction import pytest from homeassistant import core diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 88615d98122..03eb490b65e 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -3,7 +3,7 @@ import asyncio from unittest.mock import MagicMock, Mock from aiohttp import ClientConnectionError, ClientResponseError -from bond_api import DeviceType +from bond_async import DeviceType import pytest from homeassistant.components.bond.const import DOMAIN diff --git a/tests/components/bond/test_light.py b/tests/components/bond/test_light.py index 6556c25efe2..c7d8f195423 100644 --- a/tests/components/bond/test_light.py +++ b/tests/components/bond/test_light.py @@ -1,7 +1,7 @@ """Tests for the Bond light device.""" from datetime import timedelta -from bond_api import Action, DeviceType +from bond_async import Action, DeviceType import pytest from homeassistant import core diff --git a/tests/components/bond/test_switch.py b/tests/components/bond/test_switch.py index 619eac69e71..b63bad2d431 100644 --- a/tests/components/bond/test_switch.py +++ b/tests/components/bond/test_switch.py @@ -1,7 +1,7 @@ """Tests for the Bond switch device.""" from datetime import timedelta -from bond_api import Action, DeviceType +from bond_async import Action, DeviceType import pytest from homeassistant import core From 3a998f1d4608aacb25d12d00d02766dd1157d5f1 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 26 May 2022 03:03:43 -0400 Subject: [PATCH 0917/3516] Update node statistics for zwave_js device diagnostics dump (#72509) --- homeassistant/components/zwave_js/diagnostics.py | 4 +++- tests/components/zwave_js/test_diagnostics.py | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 4e1abe37b1b..3372b0eeec0 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -155,6 +155,8 @@ async def async_get_device_diagnostics( node = driver.controller.nodes[node_id] entities = get_device_entities(hass, node, device) assert client.version + node_state = redact_node_state(async_redact_data(node.data, KEYS_TO_REDACT)) + node_state["statistics"] = node.statistics.data return { "versionInfo": { "driverVersion": client.version.driver_version, @@ -163,5 +165,5 @@ async def async_get_device_diagnostics( "maxSchemaVersion": client.version.max_schema_version, }, "entities": entities, - "state": redact_node_state(async_redact_data(node.data, KEYS_TO_REDACT)), + "state": node_state, } diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index 3fe3bdfeb89..3ac3f32b45a 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -92,7 +92,16 @@ async def test_device_diagnostics( assert len(diagnostics_data["entities"]) == len( list(async_discover_node_values(multisensor_6, device, {device.id: set()})) ) - assert diagnostics_data["state"] == multisensor_6.data + assert diagnostics_data["state"] == { + **multisensor_6.data, + "statistics": { + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "commandsRX": 0, + "commandsTX": 0, + "timeoutResponse": 0, + }, + } async def test_device_diagnostics_error(hass, integration): From e8feecf50b2886e07ec9671ff0b1ffc81d0b6f0f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 26 May 2022 09:58:04 +0200 Subject: [PATCH 0918/3516] Fix androidtv type hint (#72513) --- homeassistant/components/androidtv/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py index 8a34b8aa858..4b203fc3757 100644 --- a/homeassistant/components/androidtv/__init__.py +++ b/homeassistant/components/androidtv/__init__.py @@ -60,7 +60,7 @@ def get_androidtv_mac(dev_props: dict[str, Any]) -> str | None: def _setup_androidtv( - hass: HomeAssistant, config: dict[str, Any] + hass: HomeAssistant, config: Mapping[str, Any] ) -> tuple[str, PythonRSASigner | None, str]: """Generate an ADB key (if needed) and load it.""" adbkey: str = config.get( From 48cc3638fa08f591eebabedf72dd36610c6c3d0d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 26 May 2022 13:17:08 +0200 Subject: [PATCH 0919/3516] Cleanup unused function return values (#72512) --- homeassistant/components/shelly/__init__.py | 5 ++--- homeassistant/components/zwave_js/api.py | 11 ++++------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 4551fee5590..012f692c579 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -766,14 +766,13 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.device.firmware_version, new_version, ) - result = None try: async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): - result = await self.device.trigger_ota_update(beta=beta) + await self.device.trigger_ota_update(beta=beta) except (asyncio.TimeoutError, OSError) as err: LOGGER.exception("Error while perform ota update: %s", err) - LOGGER.debug("Result of OTA update call: %s", result) + LOGGER.debug("OTA update call successful") async def shutdown(self) -> None: """Shutdown the wrapper.""" diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 5d81dc46803..c2e03981686 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -1290,11 +1290,8 @@ async def websocket_remove_failed_node( connection.subscriptions[msg["id"]] = async_cleanup msg[DATA_UNSUBSCRIBE] = unsubs = [controller.on("node removed", node_removed)] - result = await controller.async_remove_failed_node(node.node_id) - connection.send_result( - msg[ID], - result, - ) + await controller.async_remove_failed_node(node.node_id) + connection.send_result(msg[ID]) @websocket_api.require_admin @@ -1469,8 +1466,8 @@ async def websocket_refresh_node_info( node.on("interview failed", forward_event), ] - result = await node.async_refresh_info() - connection.send_result(msg[ID], result) + await node.async_refresh_info() + connection.send_result(msg[ID]) @websocket_api.require_admin From 576fc9dc643fdb98be58ca7e3e56e7c3ecc52a02 Mon Sep 17 00:00:00 2001 From: j-a-n Date: Thu, 26 May 2022 13:23:49 +0200 Subject: [PATCH 0920/3516] Fix Moehlenhoff Alpha2 set_target_temperature and set_heat_area_mode (#72533) Fix set_target_temperature and set_heat_area_mode --- .../components/moehlenhoff_alpha2/__init__.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/moehlenhoff_alpha2/__init__.py b/homeassistant/components/moehlenhoff_alpha2/__init__.py index 93ddaa781ab..86306a56033 100644 --- a/homeassistant/components/moehlenhoff_alpha2/__init__.py +++ b/homeassistant/components/moehlenhoff_alpha2/__init__.py @@ -98,7 +98,7 @@ class Alpha2BaseCoordinator(DataUpdateCoordinator[dict[str, dict]]): update_data = {"T_TARGET": target_temperature} is_cooling = self.get_cooling() - heat_area_mode = self.data[heat_area_id]["HEATAREA_MODE"] + heat_area_mode = self.data["heat_areas"][heat_area_id]["HEATAREA_MODE"] if heat_area_mode == 1: if is_cooling: update_data["T_COOL_DAY"] = target_temperature @@ -116,7 +116,7 @@ class Alpha2BaseCoordinator(DataUpdateCoordinator[dict[str, dict]]): raise HomeAssistantError( "Failed to set target temperature, communication error with alpha2 base" ) from http_err - self.data[heat_area_id].update(update_data) + self.data["heat_areas"][heat_area_id].update(update_data) for update_callback in self._listeners: update_callback() @@ -141,25 +141,25 @@ class Alpha2BaseCoordinator(DataUpdateCoordinator[dict[str, dict]]): "Failed to set heat area mode, communication error with alpha2 base" ) from http_err - self.data[heat_area_id]["HEATAREA_MODE"] = heat_area_mode + self.data["heat_areas"][heat_area_id]["HEATAREA_MODE"] = heat_area_mode is_cooling = self.get_cooling() if heat_area_mode == 1: if is_cooling: - self.data[heat_area_id]["T_TARGET"] = self.data[heat_area_id][ - "T_COOL_DAY" - ] + self.data["heat_areas"][heat_area_id]["T_TARGET"] = self.data[ + "heat_areas" + ][heat_area_id]["T_COOL_DAY"] else: - self.data[heat_area_id]["T_TARGET"] = self.data[heat_area_id][ - "T_HEAT_DAY" - ] + self.data["heat_areas"][heat_area_id]["T_TARGET"] = self.data[ + "heat_areas" + ][heat_area_id]["T_HEAT_DAY"] elif heat_area_mode == 2: if is_cooling: - self.data[heat_area_id]["T_TARGET"] = self.data[heat_area_id][ - "T_COOL_NIGHT" - ] + self.data["heat_areas"][heat_area_id]["T_TARGET"] = self.data[ + "heat_areas" + ][heat_area_id]["T_COOL_NIGHT"] else: - self.data[heat_area_id]["T_TARGET"] = self.data[heat_area_id][ - "T_HEAT_NIGHT" - ] + self.data["heat_areas"][heat_area_id]["T_TARGET"] = self.data[ + "heat_areas" + ][heat_area_id]["T_HEAT_NIGHT"] for update_callback in self._listeners: update_callback() From 33784446f63b7396c88601f48b2f11685d63f13b Mon Sep 17 00:00:00 2001 From: Tom Barbette Date: Thu, 26 May 2022 18:04:22 +0200 Subject: [PATCH 0921/3516] Add nmbs canceled attribute (#57113) * nmbs: Add canceled attribute If a train is canceled, change the state to canceled and also add an attribute that can be matched. Personnaly I look for the attribute and add a "line-through" CSS style to show my train was canceled. I discovered this was not displayed the hard way :) Signed-off-by: Tom Barbette * Update homeassistant/components/nmbs/sensor.py canceled must be compared as an int, as suggested by @MartinHjelmare Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/nmbs/sensor.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 00624748aba..fdb03652756 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -215,10 +215,9 @@ class NMBSSensor(SensorEntity): delay = get_delay_in_minutes(self._attrs["departure"]["delay"]) departure = get_time_until(self._attrs["departure"]["time"]) + canceled = int(self._attrs["departure"]["canceled"]) attrs = { - "departure": f"In {departure} minutes", - "departure_minutes": departure, "destination": self._station_to, "direction": self._attrs["departure"]["direction"]["name"], "platform_arriving": self._attrs["arrival"]["platform"], @@ -227,6 +226,15 @@ class NMBSSensor(SensorEntity): ATTR_ATTRIBUTION: "https://api.irail.be/", } + if canceled != 1: + attrs["departure"] = f"In {departure} minutes" + attrs["departure_minutes"] = departure + attrs["canceled"] = False + else: + attrs["departure"] = None + attrs["departure_minutes"] = None + attrs["canceled"] = True + if self._show_on_map and self.station_coordinates: attrs[ATTR_LATITUDE] = self.station_coordinates[0] attrs[ATTR_LONGITUDE] = self.station_coordinates[1] From f82ec4d233e6ba34acda3708f8fc6921ba725342 Mon Sep 17 00:00:00 2001 From: rappenze Date: Thu, 26 May 2022 20:52:30 +0200 Subject: [PATCH 0922/3516] Address issues from late review in fibaro config flow tests (#72553) --- tests/components/fibaro/test_config_flow.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/components/fibaro/test_config_flow.py b/tests/components/fibaro/test_config_flow.py index 6f3e035a2f7..f056f484a58 100644 --- a/tests/components/fibaro/test_config_flow.py +++ b/tests/components/fibaro/test_config_flow.py @@ -53,7 +53,12 @@ async def test_config_flow_user_initiated_success(hass): login_mock = Mock() login_mock.get.return_value = Mock(status=True) - with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True): + with patch( + "fiblary3.client.v4.client.Client.login", login_mock, create=True + ), patch( + "homeassistant.components.fibaro.async_setup_entry", + return_value=True, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -182,7 +187,12 @@ async def test_config_flow_import(hass): """Test for importing config from configuration.yaml.""" login_mock = Mock() login_mock.get.return_value = Mock(status=True) - with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True): + with patch( + "fiblary3.client.v4.client.Client.login", login_mock, create=True + ), patch( + "homeassistant.components.fibaro.async_setup_entry", + return_value=True, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, From 828fcd0a48ec81d422b9554306d5fb8b24045ab7 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 26 May 2022 15:17:44 -0400 Subject: [PATCH 0923/3516] Fix jitter in nzbget uptime sensor (#72518) --- homeassistant/components/nzbget/sensor.py | 30 +++++++++++++---------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 9e5bd6e4ac9..a1097389020 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -1,7 +1,7 @@ """Monitor the NZBGet API.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta import logging from homeassistant.components.sensor import ( @@ -105,15 +105,16 @@ class NZBGetSensor(NZBGetEntity, SensorEntity): description: SensorEntityDescription, ) -> None: """Initialize a new NZBGet sensor.""" - self.entity_description = description - self._attr_unique_id = f"{entry_id}_{description.key}" - super().__init__( coordinator=coordinator, entry_id=entry_id, name=f"{entry_name} {description.name}", ) + self.entity_description = description + self._attr_unique_id = f"{entry_id}_{description.key}" + self._native_value: datetime | None = None + @property def native_value(self): """Return the state of the sensor.""" @@ -122,14 +123,17 @@ class NZBGetSensor(NZBGetEntity, SensorEntity): if value is None: _LOGGER.warning("Unable to locate value for %s", sensor_type) - return None - - if "DownloadRate" in sensor_type and value > 0: + self._native_value = None + elif "DownloadRate" in sensor_type and value > 0: # Convert download rate from Bytes/s to MBytes/s - return round(value / 2**20, 2) + self._native_value = round(value / 2**20, 2) + elif "UpTimeSec" in sensor_type and value > 0: + uptime = utcnow().replace(microsecond=0) - timedelta(seconds=value) + if not isinstance(self._attr_native_value, datetime) or abs( + uptime - self._attr_native_value + ) > timedelta(seconds=5): + self._native_value = uptime + else: + self._native_value = value - if "UpTimeSec" in sensor_type and value > 0: - uptime = utcnow() - timedelta(seconds=value) - return uptime.replace(microsecond=0) - - return value + return self._native_value From d1578aacf20a67036823a1b9094b64a09a38b856 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 26 May 2022 21:41:17 +0200 Subject: [PATCH 0924/3516] Improve raspberry_pi tests (#72557) --- .../components/raspberry_pi/test_hardware.py | 54 +++++++++++++++---- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/tests/components/raspberry_pi/test_hardware.py b/tests/components/raspberry_pi/test_hardware.py index 748972c8d60..a4e938079d3 100644 --- a/tests/components/raspberry_pi/test_hardware.py +++ b/tests/components/raspberry_pi/test_hardware.py @@ -1,25 +1,41 @@ """Test the Raspberry Pi hardware platform.""" +from unittest.mock import patch + import pytest -from homeassistant.components.hassio import DATA_OS_INFO from homeassistant.components.raspberry_pi.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component -from tests.common import MockModule, mock_integration +from tests.common import MockConfigEntry, MockModule, mock_integration async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: """Test we can get the board info.""" mock_integration(hass, MockModule("hassio")) - hass.data[DATA_OS_INFO] = {"board": "rpi"} - assert await async_setup_component(hass, DOMAIN, {}) + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Raspberry Pi", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.raspberry_pi.get_os_info", + return_value={"board": "rpi"}, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() client = await hass_ws_client(hass) - await client.send_json({"id": 1, "type": "hardware/info"}) - msg = await client.receive_json() + with patch( + "homeassistant.components.raspberry_pi.hardware.get_os_info", + return_value={"board": "rpi"}, + ): + await client.send_json({"id": 1, "type": "hardware/info"}) + msg = await client.receive_json() assert msg["id"] == 1 assert msg["success"] @@ -43,14 +59,30 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: async def test_hardware_info_fail(hass: HomeAssistant, hass_ws_client, os_info) -> None: """Test async_info raises if os_info is not as expected.""" mock_integration(hass, MockModule("hassio")) - hass.data[DATA_OS_INFO] = os_info - assert await async_setup_component(hass, DOMAIN, {}) + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Raspberry Pi", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.raspberry_pi.get_os_info", + return_value={"board": "rpi"}, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() client = await hass_ws_client(hass) - await client.send_json({"id": 1, "type": "hardware/info"}) - msg = await client.receive_json() + with patch( + "homeassistant.components.raspberry_pi.hardware.get_os_info", + return_value=os_info, + ): + await client.send_json({"id": 1, "type": "hardware/info"}) + msg = await client.receive_json() assert msg["id"] == 1 assert msg["success"] From b91a1c1b0aac1f60063dab2ccafb35a2cfd58820 Mon Sep 17 00:00:00 2001 From: jack5mikemotown <72000916+jack5mikemotown@users.noreply.github.com> Date: Thu, 26 May 2022 16:01:23 -0400 Subject: [PATCH 0925/3516] Fix Google Assistant brightness calculation (#72514) Co-authored-by: Paulus Schoutsen --- homeassistant/components/google_assistant/trait.py | 4 ++-- tests/components/google_assistant/test_google_assistant.py | 2 +- tests/components/google_assistant/test_smart_home.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 20191c61668..42fc43197ea 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -249,7 +249,7 @@ class BrightnessTrait(_Trait): if domain == light.DOMAIN: brightness = self.state.attributes.get(light.ATTR_BRIGHTNESS) if brightness is not None: - response["brightness"] = int(100 * (brightness / 255)) + response["brightness"] = round(100 * (brightness / 255)) else: response["brightness"] = 0 @@ -1948,7 +1948,7 @@ class VolumeTrait(_Trait): level = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) if level is not None: # Convert 0.0-1.0 to 0-100 - response["currentVolume"] = int(level * 100) + response["currentVolume"] = round(level * 100) muted = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_MUTED) if muted is not None: diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 8bf0e5573b2..e8a2603cae3 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -225,7 +225,7 @@ async def test_query_request(hass_fixture, assistant_client, auth_header): assert len(devices) == 4 assert devices["light.bed_light"]["on"] is False assert devices["light.ceiling_lights"]["on"] is True - assert devices["light.ceiling_lights"]["brightness"] == 70 + assert devices["light.ceiling_lights"]["brightness"] == 71 assert devices["light.ceiling_lights"]["color"]["temperatureK"] == 2631 assert devices["light.kitchen_lights"]["color"]["spectrumHsv"] == { "hue": 345, diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index c3bbd9336f4..4b11910999a 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -385,7 +385,7 @@ async def test_query_message(hass): "light.another_light": { "on": True, "online": True, - "brightness": 30, + "brightness": 31, "color": { "spectrumHsv": { "hue": 180, @@ -1510,7 +1510,7 @@ async def test_query_recover(hass, caplog): "payload": { "devices": { "light.bad": {"online": False}, - "light.good": {"on": True, "online": True, "brightness": 19}, + "light.good": {"on": True, "online": True, "brightness": 20}, } }, } From d092861926a090350c8d3208c62f598388ddfe94 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 26 May 2022 22:02:39 +0200 Subject: [PATCH 0926/3516] Move manual configuration of MQTT device_tracker to the integration key (#72493) --- homeassistant/components/mqtt/__init__.py | 4 +- .../mqtt/device_tracker/__init__.py | 10 ++- .../mqtt/device_tracker/schema_discovery.py | 26 +++++-- tests/components/mqtt/test_device_tracker.py | 69 +++++++++++++------ 4 files changed, 82 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index df5d52c443a..e8847375584 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -159,6 +159,7 @@ PLATFORMS = [ Platform.BUTTON, Platform.CAMERA, Platform.CLIMATE, + Platform.DEVICE_TRACKER, Platform.COVER, Platform.FAN, Platform.HUMIDIFIER, @@ -196,17 +197,18 @@ PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( vol.Optional(Platform.CAMERA.value): cv.ensure_list, vol.Optional(Platform.CLIMATE.value): cv.ensure_list, vol.Optional(Platform.COVER.value): cv.ensure_list, + vol.Optional(Platform.DEVICE_TRACKER.value): cv.ensure_list, vol.Optional(Platform.FAN.value): cv.ensure_list, vol.Optional(Platform.HUMIDIFIER.value): cv.ensure_list, vol.Optional(Platform.LIGHT.value): cv.ensure_list, vol.Optional(Platform.LOCK.value): cv.ensure_list, + vol.Optional(Platform.NUMBER.value): cv.ensure_list, vol.Optional(Platform.SCENE.value): cv.ensure_list, vol.Optional(Platform.SELECT.value): cv.ensure_list, vol.Optional(Platform.SIREN.value): cv.ensure_list, vol.Optional(Platform.SENSOR.value): cv.ensure_list, vol.Optional(Platform.SWITCH.value): cv.ensure_list, vol.Optional(Platform.VACUUM.value): cv.ensure_list, - vol.Optional(Platform.NUMBER.value): cv.ensure_list, } ) diff --git a/homeassistant/components/mqtt/device_tracker/__init__.py b/homeassistant/components/mqtt/device_tracker/__init__.py index 03574e6554b..bcd5bbd4ee1 100644 --- a/homeassistant/components/mqtt/device_tracker/__init__.py +++ b/homeassistant/components/mqtt/device_tracker/__init__.py @@ -1,7 +1,15 @@ """Support for tracking MQTT enabled devices.""" +import voluptuous as vol + +from homeassistant.components import device_tracker + +from ..mixins import warn_for_legacy_schema from .schema_discovery import async_setup_entry_from_discovery from .schema_yaml import PLATFORM_SCHEMA_YAML, async_setup_scanner_from_yaml -PLATFORM_SCHEMA = PLATFORM_SCHEMA_YAML +# Configuring MQTT Device Trackers under the device_tracker platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA_YAML, warn_for_legacy_schema(device_tracker.DOMAIN) +) async_setup_scanner = async_setup_scanner_from_yaml async_setup_entry = async_setup_entry_from_discovery diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index a7b597d0689..aa7506bd5e3 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -1,4 +1,5 @@ -"""Support for tracking MQTT enabled devices identified through discovery.""" +"""Support for tracking MQTT enabled devices.""" +import asyncio import functools import voluptuous as vol @@ -22,13 +23,18 @@ from .. import MqttValueTemplate, subscription from ... import mqtt from ..const import CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages -from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper +from ..mixins import ( + MQTT_ENTITY_COMMON_SCHEMA, + MqttEntity, + async_get_platform_config_from_yaml, + async_setup_entry_helper, +) CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" CONF_SOURCE_TYPE = "source_type" -PLATFORM_SCHEMA_DISCOVERY = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RO_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, @@ -37,11 +43,21 @@ 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) +DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.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.""" + """Set up MQTT device tracker configuration.yaml and dynamically through MQTT discovery.""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, device_tracker.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index c85fcef7dc4..020fbad6166 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -1,22 +1,17 @@ -"""The tests for the MQTT device tracker platform.""" +"""The tests for the MQTT device tracker platform using configuration.yaml.""" from unittest.mock import patch -import pytest - from homeassistant.components.device_tracker.const import DOMAIN, SOURCE_TYPE_BLUETOOTH from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME from homeassistant.setup import async_setup_component +from .test_common import help_test_setup_manual_entity_from_yaml + from tests.common import async_fire_mqtt_message -@pytest.fixture(autouse=True) -def setup_comp(hass, mqtt_mock): - """Set up mqtt component.""" - pass - - -async def test_ensure_device_tracker_platform_validation(hass): +# Deprecated in HA Core 2022.6 +async def test_legacy_ensure_device_tracker_platform_validation(hass, mqtt_mock): """Test if platform validation was done.""" async def mock_setup_scanner(hass, config, see, discovery_info=None): @@ -37,7 +32,8 @@ async def test_ensure_device_tracker_platform_validation(hass): assert mock_sp.call_count == 1 -async def test_new_message(hass, mock_device_tracker_conf): +# Deprecated in HA Core 2022.6 +async def test_legacy_new_message(hass, mock_device_tracker_conf, mqtt_mock): """Test new message.""" dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" @@ -53,7 +49,10 @@ async def test_new_message(hass, mock_device_tracker_conf): assert hass.states.get(entity_id).state == location -async def test_single_level_wildcard_topic(hass, mock_device_tracker_conf): +# Deprecated in HA Core 2022.6 +async def test_legacy_single_level_wildcard_topic( + hass, mock_device_tracker_conf, mqtt_mock +): """Test single level wildcard topic.""" dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" @@ -72,7 +71,10 @@ async def test_single_level_wildcard_topic(hass, mock_device_tracker_conf): assert hass.states.get(entity_id).state == location -async def test_multi_level_wildcard_topic(hass, mock_device_tracker_conf): +# Deprecated in HA Core 2022.6 +async def test_legacy_multi_level_wildcard_topic( + hass, mock_device_tracker_conf, mqtt_mock +): """Test multi level wildcard topic.""" dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" @@ -91,7 +93,10 @@ async def test_multi_level_wildcard_topic(hass, mock_device_tracker_conf): assert hass.states.get(entity_id).state == location -async def test_single_level_wildcard_topic_not_matching(hass, mock_device_tracker_conf): +# Deprecated in HA Core 2022.6 +async def test_legacy_single_level_wildcard_topic_not_matching( + hass, mock_device_tracker_conf, mqtt_mock +): """Test not matching single level wildcard topic.""" dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" @@ -110,7 +115,10 @@ async def test_single_level_wildcard_topic_not_matching(hass, mock_device_tracke assert hass.states.get(entity_id) is None -async def test_multi_level_wildcard_topic_not_matching(hass, mock_device_tracker_conf): +# Deprecated in HA Core 2022.6 +async def test_legacy_multi_level_wildcard_topic_not_matching( + hass, mock_device_tracker_conf, mqtt_mock +): """Test not matching multi level wildcard topic.""" dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" @@ -129,8 +137,9 @@ async def test_multi_level_wildcard_topic_not_matching(hass, mock_device_tracker assert hass.states.get(entity_id) is None -async def test_matching_custom_payload_for_home_and_not_home( - hass, mock_device_tracker_conf +# Deprecated in HA Core 2022.6 +async def test_legacy_matching_custom_payload_for_home_and_not_home( + hass, mock_device_tracker_conf, mqtt_mock ): """Test custom payload_home sets state to home and custom payload_not_home sets state to not_home.""" dev_id = "paulus" @@ -161,8 +170,9 @@ async def test_matching_custom_payload_for_home_and_not_home( assert hass.states.get(entity_id).state == STATE_NOT_HOME -async def test_not_matching_custom_payload_for_home_and_not_home( - hass, mock_device_tracker_conf +# Deprecated in HA Core 2022.6 +async def test_legacy_not_matching_custom_payload_for_home_and_not_home( + hass, mock_device_tracker_conf, mqtt_mock ): """Test not matching payload does not set state to home or not_home.""" dev_id = "paulus" @@ -191,7 +201,8 @@ async def test_not_matching_custom_payload_for_home_and_not_home( assert hass.states.get(entity_id).state != STATE_NOT_HOME -async def test_matching_source_type(hass, mock_device_tracker_conf): +# Deprecated in HA Core 2022.6 +async def test_legacy_matching_source_type(hass, mock_device_tracker_conf, mqtt_mock): """Test setting source type.""" dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" @@ -215,3 +226,21 @@ async def test_matching_source_type(hass, mock_device_tracker_conf): async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() assert hass.states.get(entity_id).attributes["source_type"] == SOURCE_TYPE_BLUETOOTH + + +async def test_setup_with_modern_schema( + hass, caplog, tmp_path, mock_device_tracker_conf +): + """Test setup using the modern schema.""" + dev_id = "jan" + entity_id = f"{DOMAIN}.{dev_id}" + topic = "/location/jan" + + hass.config.components = {"zone"} + config = {"name": dev_id, "state_topic": topic} + + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, DOMAIN, config + ) + + assert hass.states.get(entity_id) is not None From ff3374b4e0c0552aca84ba823db37c854ec66b25 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 May 2022 13:06:34 -0700 Subject: [PATCH 0927/3516] Use modern WS API for auth integration + add auth provider type to refresh token info (#72552) --- homeassistant/components/auth/__init__.py | 141 +++++++++------------- tests/components/auth/test_init.py | 11 +- 2 files changed, 63 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 30b36a40f32..1dc483eec6e 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -148,43 +148,6 @@ from homeassistant.util import dt as dt_util from . import indieauth, login_flow, mfa_setup_flow DOMAIN = "auth" -WS_TYPE_CURRENT_USER = "auth/current_user" -SCHEMA_WS_CURRENT_USER = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_CURRENT_USER} -) - -WS_TYPE_LONG_LIVED_ACCESS_TOKEN = "auth/long_lived_access_token" -SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - { - vol.Required("type"): WS_TYPE_LONG_LIVED_ACCESS_TOKEN, - vol.Required("lifespan"): int, # days - vol.Required("client_name"): str, - vol.Optional("client_icon"): str, - } -) - -WS_TYPE_REFRESH_TOKENS = "auth/refresh_tokens" -SCHEMA_WS_REFRESH_TOKENS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_REFRESH_TOKENS} -) - -WS_TYPE_DELETE_REFRESH_TOKEN = "auth/delete_refresh_token" -SCHEMA_WS_DELETE_REFRESH_TOKEN = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - { - vol.Required("type"): WS_TYPE_DELETE_REFRESH_TOKEN, - vol.Required("refresh_token_id"): str, - } -) - -WS_TYPE_SIGN_PATH = "auth/sign_path" -SCHEMA_WS_SIGN_PATH = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - { - vol.Required("type"): WS_TYPE_SIGN_PATH, - vol.Required("path"): str, - vol.Optional("expires", default=30): int, - } -) - RESULT_TYPE_CREDENTIALS = "credentials" @@ -204,27 +167,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.http.register_view(LinkUserView(retrieve_result)) hass.http.register_view(OAuth2AuthorizeCallbackView()) - websocket_api.async_register_command( - hass, WS_TYPE_CURRENT_USER, websocket_current_user, SCHEMA_WS_CURRENT_USER - ) - websocket_api.async_register_command( - hass, - WS_TYPE_LONG_LIVED_ACCESS_TOKEN, - websocket_create_long_lived_access_token, - SCHEMA_WS_LONG_LIVED_ACCESS_TOKEN, - ) - websocket_api.async_register_command( - hass, WS_TYPE_REFRESH_TOKENS, websocket_refresh_tokens, SCHEMA_WS_REFRESH_TOKENS - ) - websocket_api.async_register_command( - hass, - WS_TYPE_DELETE_REFRESH_TOKEN, - websocket_delete_refresh_token, - SCHEMA_WS_DELETE_REFRESH_TOKEN, - ) - websocket_api.async_register_command( - hass, WS_TYPE_SIGN_PATH, websocket_sign_path, SCHEMA_WS_SIGN_PATH - ) + websocket_api.async_register_command(hass, websocket_current_user) + websocket_api.async_register_command(hass, websocket_create_long_lived_access_token) + websocket_api.async_register_command(hass, websocket_refresh_tokens) + websocket_api.async_register_command(hass, websocket_delete_refresh_token) + websocket_api.async_register_command(hass, websocket_sign_path) await login_flow.async_setup(hass, store_result) await mfa_setup_flow.async_setup(hass) @@ -476,6 +423,7 @@ def _create_auth_code_store(): return store_result, retrieve_result +@websocket_api.websocket_command({vol.Required("type"): "auth/current_user"}) @websocket_api.ws_require_user() @websocket_api.async_response async def websocket_current_user( @@ -513,6 +461,14 @@ async def websocket_current_user( ) +@websocket_api.websocket_command( + { + vol.Required("type"): "auth/long_lived_access_token", + vol.Required("lifespan"): int, # days + vol.Required("client_name"): str, + vol.Optional("client_icon"): str, + } +) @websocket_api.ws_require_user() @websocket_api.async_response async def websocket_create_long_lived_access_token( @@ -530,13 +486,13 @@ async def websocket_create_long_lived_access_token( try: access_token = hass.auth.async_create_access_token(refresh_token) except InvalidAuthError as exc: - return websocket_api.error_message( - msg["id"], websocket_api.const.ERR_UNAUTHORIZED, str(exc) - ) + connection.send_error(msg["id"], websocket_api.const.ERR_UNAUTHORIZED, str(exc)) + return - connection.send_message(websocket_api.result_message(msg["id"], access_token)) + connection.send_result(msg["id"], access_token) +@websocket_api.websocket_command({vol.Required("type"): "auth/refresh_tokens"}) @websocket_api.ws_require_user() @callback def websocket_refresh_tokens( @@ -544,27 +500,38 @@ def websocket_refresh_tokens( ): """Return metadata of users refresh tokens.""" current_id = connection.refresh_token_id - connection.send_message( - websocket_api.result_message( - msg["id"], - [ - { - "id": refresh.id, - "client_id": refresh.client_id, - "client_name": refresh.client_name, - "client_icon": refresh.client_icon, - "type": refresh.token_type, - "created_at": refresh.created_at, - "is_current": refresh.id == current_id, - "last_used_at": refresh.last_used_at, - "last_used_ip": refresh.last_used_ip, - } - for refresh in connection.user.refresh_tokens.values() - ], + + tokens = [] + for refresh in connection.user.refresh_tokens.values(): + if refresh.credential: + auth_provider_type = refresh.credential.auth_provider_type + else: + auth_provider_type = None + + tokens.append( + { + "id": refresh.id, + "client_id": refresh.client_id, + "client_name": refresh.client_name, + "client_icon": refresh.client_icon, + "type": refresh.token_type, + "created_at": refresh.created_at, + "is_current": refresh.id == current_id, + "last_used_at": refresh.last_used_at, + "last_used_ip": refresh.last_used_ip, + "auth_provider_type": auth_provider_type, + } ) - ) + + connection.send_result(msg["id"], tokens) +@websocket_api.websocket_command( + { + vol.Required("type"): "auth/delete_refresh_token", + vol.Required("refresh_token_id"): str, + } +) @websocket_api.ws_require_user() @websocket_api.async_response async def websocket_delete_refresh_token( @@ -574,15 +541,21 @@ async def websocket_delete_refresh_token( refresh_token = connection.user.refresh_tokens.get(msg["refresh_token_id"]) if refresh_token is None: - return websocket_api.error_message( - msg["id"], "invalid_token_id", "Received invalid token" - ) + connection.send_error(msg["id"], "invalid_token_id", "Received invalid token") + return await hass.auth.async_remove_refresh_token(refresh_token) - connection.send_message(websocket_api.result_message(msg["id"], {})) + connection.send_result(msg["id"], {}) +@websocket_api.websocket_command( + { + vol.Required("type"): "auth/sign_path", + vol.Required("path"): str, + vol.Optional("expires", default=30): int, + } +) @websocket_api.ws_require_user() @callback def websocket_sign_path( diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index f6d0695d97d..ef231950bd9 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -193,7 +193,7 @@ async def test_ws_current_user(hass, hass_ws_client, hass_access_token): user = refresh_token.user client = await hass_ws_client(hass, hass_access_token) - await client.send_json({"id": 5, "type": auth.WS_TYPE_CURRENT_USER}) + await client.send_json({"id": 5, "type": "auth/current_user"}) result = await client.receive_json() assert result["success"], result @@ -410,7 +410,7 @@ async def test_ws_long_lived_access_token(hass, hass_ws_client, hass_access_toke await ws_client.send_json( { "id": 5, - "type": auth.WS_TYPE_LONG_LIVED_ACCESS_TOKEN, + "type": "auth/long_lived_access_token", "client_name": "GPS Logger", "lifespan": 365, } @@ -434,7 +434,7 @@ async def test_ws_refresh_tokens(hass, hass_ws_client, hass_access_token): ws_client = await hass_ws_client(hass, hass_access_token) - await ws_client.send_json({"id": 5, "type": auth.WS_TYPE_REFRESH_TOKENS}) + await ws_client.send_json({"id": 5, "type": "auth/refresh_tokens"}) result = await ws_client.receive_json() assert result["success"], result @@ -450,6 +450,7 @@ async def test_ws_refresh_tokens(hass, hass_ws_client, hass_access_token): assert token["is_current"] is True assert token["last_used_at"] == refresh_token.last_used_at.isoformat() assert token["last_used_ip"] == refresh_token.last_used_ip + assert token["auth_provider_type"] == "homeassistant" async def test_ws_delete_refresh_token( @@ -468,7 +469,7 @@ async def test_ws_delete_refresh_token( await ws_client.send_json( { "id": 5, - "type": auth.WS_TYPE_DELETE_REFRESH_TOKEN, + "type": "auth/delete_refresh_token", "refresh_token_id": refresh_token.id, } ) @@ -490,7 +491,7 @@ async def test_ws_sign_path(hass, hass_ws_client, hass_access_token): await ws_client.send_json( { "id": 5, - "type": auth.WS_TYPE_SIGN_PATH, + "type": "auth/sign_path", "path": "/api/hello", "expires": 20, } From 0cca73fb238906cb2646d764ad92db3480d0ab49 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 26 May 2022 22:15:44 +0200 Subject: [PATCH 0928/3516] Add hardkernel hardware integration (#72489) * Add hardkernel hardware integration * Remove debug prints * Improve tests * Improve test coverage --- CODEOWNERS | 2 + .../components/hardkernel/__init__.py | 22 +++++ .../components/hardkernel/config_flow.py | 22 +++++ homeassistant/components/hardkernel/const.py | 3 + .../components/hardkernel/hardware.py | 39 ++++++++ .../components/hardkernel/manifest.json | 9 ++ script/hassfest/manifest.py | 1 + tests/components/hardkernel/__init__.py | 1 + .../components/hardkernel/test_config_flow.py | 58 ++++++++++++ tests/components/hardkernel/test_hardware.py | 89 +++++++++++++++++++ tests/components/hardkernel/test_init.py | 72 +++++++++++++++ 11 files changed, 318 insertions(+) create mode 100644 homeassistant/components/hardkernel/__init__.py create mode 100644 homeassistant/components/hardkernel/config_flow.py create mode 100644 homeassistant/components/hardkernel/const.py create mode 100644 homeassistant/components/hardkernel/hardware.py create mode 100644 homeassistant/components/hardkernel/manifest.json create mode 100644 tests/components/hardkernel/__init__.py create mode 100644 tests/components/hardkernel/test_config_flow.py create mode 100644 tests/components/hardkernel/test_hardware.py create mode 100644 tests/components/hardkernel/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 3baeb6dda68..259fbc77aab 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -417,6 +417,8 @@ build.json @home-assistant/supervisor /tests/components/guardian/ @bachya /homeassistant/components/habitica/ @ASMfreaK @leikoilja /tests/components/habitica/ @ASMfreaK @leikoilja +/homeassistant/components/hardkernel/ @home-assistant/core +/tests/components/hardkernel/ @home-assistant/core /homeassistant/components/hardware/ @home-assistant/core /tests/components/hardware/ @home-assistant/core /homeassistant/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan diff --git a/homeassistant/components/hardkernel/__init__.py b/homeassistant/components/hardkernel/__init__.py new file mode 100644 index 00000000000..6dfe30b9e75 --- /dev/null +++ b/homeassistant/components/hardkernel/__init__.py @@ -0,0 +1,22 @@ +"""The Hardkernel integration.""" +from __future__ import annotations + +from homeassistant.components.hassio import get_os_info +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a Hardkernel config entry.""" + if (os_info := get_os_info(hass)) is None: + # The hassio integration has not yet fetched data from the supervisor + raise ConfigEntryNotReady + + board: str + if (board := os_info.get("board")) is None or not board.startswith("odroid"): + # Not running on a Hardkernel board, Home Assistant may have been migrated + hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) + return False + + return True diff --git a/homeassistant/components/hardkernel/config_flow.py b/homeassistant/components/hardkernel/config_flow.py new file mode 100644 index 00000000000..b0445fae231 --- /dev/null +++ b/homeassistant/components/hardkernel/config_flow.py @@ -0,0 +1,22 @@ +"""Config flow for the Hardkernel integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class HardkernelConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Hardkernel.""" + + VERSION = 1 + + async def async_step_system(self, data: dict[str, Any] | None = None) -> FlowResult: + """Handle the initial step.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + return self.async_create_entry(title="Hardkernel", data={}) diff --git a/homeassistant/components/hardkernel/const.py b/homeassistant/components/hardkernel/const.py new file mode 100644 index 00000000000..2850f3d4ebb --- /dev/null +++ b/homeassistant/components/hardkernel/const.py @@ -0,0 +1,3 @@ +"""Constants for the Hardkernel integration.""" + +DOMAIN = "hardkernel" diff --git a/homeassistant/components/hardkernel/hardware.py b/homeassistant/components/hardkernel/hardware.py new file mode 100644 index 00000000000..804f105f2ed --- /dev/null +++ b/homeassistant/components/hardkernel/hardware.py @@ -0,0 +1,39 @@ +"""The Hardkernel hardware platform.""" +from __future__ import annotations + +from homeassistant.components.hardware.models import BoardInfo, HardwareInfo +from homeassistant.components.hassio import get_os_info +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN + +BOARD_NAMES = { + "odroid-c2": "Hardkernel Odroid-C2", + "odroid-c4": "Hardkernel Odroid-C4", + "odroid-n2": "Home Assistant Blue / Hardkernel Odroid-N2", + "odroid-xu4": "Hardkernel Odroid-XU4", +} + + +@callback +def async_info(hass: HomeAssistant) -> HardwareInfo: + """Return board info.""" + if (os_info := get_os_info(hass)) is None: + raise HomeAssistantError + board: str + if (board := os_info.get("board")) is None: + raise HomeAssistantError + if not board.startswith("odroid"): + raise HomeAssistantError + + return HardwareInfo( + board=BoardInfo( + hassio_board_id=board, + manufacturer=DOMAIN, + model=board, + revision=None, + ), + name=BOARD_NAMES.get(board, f"Unknown hardkernel Odroid model '{board}'"), + url=None, + ) diff --git a/homeassistant/components/hardkernel/manifest.json b/homeassistant/components/hardkernel/manifest.json new file mode 100644 index 00000000000..366ca245191 --- /dev/null +++ b/homeassistant/components/hardkernel/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "hardkernel", + "name": "Hardkernel", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/hardkernel", + "dependencies": ["hardware", "hassio"], + "codeowners": ["@home-assistant/core"], + "integration_type": "hardware" +} diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index c478d16cf0f..7f2e8e0d477 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -52,6 +52,7 @@ NO_IOT_CLASS = [ "downloader", "ffmpeg", "frontend", + "hardkernel", "hardware", "history", "homeassistant", diff --git a/tests/components/hardkernel/__init__.py b/tests/components/hardkernel/__init__.py new file mode 100644 index 00000000000..d63b70d5cc5 --- /dev/null +++ b/tests/components/hardkernel/__init__.py @@ -0,0 +1 @@ +"""Tests for the Hardkernel integration.""" diff --git a/tests/components/hardkernel/test_config_flow.py b/tests/components/hardkernel/test_config_flow.py new file mode 100644 index 00000000000..f74b4a4e658 --- /dev/null +++ b/tests/components/hardkernel/test_config_flow.py @@ -0,0 +1,58 @@ +"""Test the Hardkernel config flow.""" +from unittest.mock import patch + +from homeassistant.components.hardkernel.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY + +from tests.common import MockConfigEntry, MockModule, mock_integration + + +async def test_config_flow(hass: HomeAssistant) -> None: + """Test the config flow.""" + mock_integration(hass, MockModule("hassio")) + + with patch( + "homeassistant.components.hardkernel.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "system"} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Hardkernel" + assert result["data"] == {} + assert result["options"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == {} + assert config_entry.title == "Hardkernel" + + +async def test_config_flow_single_entry(hass: HomeAssistant) -> None: + """Test only a single entry is allowed.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Hardkernel", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.hardkernel.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "system"} + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + mock_setup_entry.assert_not_called() diff --git a/tests/components/hardkernel/test_hardware.py b/tests/components/hardkernel/test_hardware.py new file mode 100644 index 00000000000..1c71959719c --- /dev/null +++ b/tests/components/hardkernel/test_hardware.py @@ -0,0 +1,89 @@ +"""Test the Hardkernel hardware platform.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.hardkernel.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, MockModule, mock_integration + + +async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can get the board info.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Hardkernel", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.hardkernel.get_os_info", + return_value={"board": "odroid-n2"}, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.hardkernel.hardware.get_os_info", + return_value={"board": "odroid-n2"}, + ): + await client.send_json({"id": 1, "type": "hardware/info"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] == { + "hardware": [ + { + "board": { + "hassio_board_id": "odroid-n2", + "manufacturer": "hardkernel", + "model": "odroid-n2", + "revision": None, + }, + "name": "Home Assistant Blue / Hardkernel Odroid-N2", + "url": None, + } + ] + } + + +@pytest.mark.parametrize("os_info", [None, {"board": None}, {"board": "other"}]) +async def test_hardware_info_fail(hass: HomeAssistant, hass_ws_client, os_info) -> None: + """Test async_info raises if os_info is not as expected.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Hardkernel", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.hardkernel.get_os_info", + return_value={"board": "odroid-n2"}, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.hardkernel.hardware.get_os_info", + return_value=os_info, + ): + await client.send_json({"id": 1, "type": "hardware/info"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] == {"hardware": []} diff --git a/tests/components/hardkernel/test_init.py b/tests/components/hardkernel/test_init.py new file mode 100644 index 00000000000..f202777f530 --- /dev/null +++ b/tests/components/hardkernel/test_init.py @@ -0,0 +1,72 @@ +"""Test the Hardkernel integration.""" +from unittest.mock import patch + +from homeassistant.components.hardkernel.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, MockModule, mock_integration + + +async def test_setup_entry(hass: HomeAssistant) -> None: + """Test setup of a config entry.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Hardkernel", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.hardkernel.get_os_info", + return_value={"board": "odroid-n2"}, + ) as mock_get_os_info: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + + +async def test_setup_entry_wrong_board(hass: HomeAssistant) -> None: + """Test setup of a config entry with wrong board type.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Hardkernel", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.hardkernel.get_os_info", + return_value={"board": "generic-x86-64"}, + ) as mock_get_os_info: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + + +async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None: + """Test setup of a config entry when hassio has not fetched os_info.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Hardkernel", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.hardkernel.get_os_info", + return_value=None, + ) as mock_get_os_info: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + assert config_entry.state == ConfigEntryState.SETUP_RETRY From d39de6e6994f24e2e4961199fae07cc9b14505cc Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 25 May 2022 13:00:48 -0700 Subject: [PATCH 0929/3516] Throw nest climate API errors as HomeAssistantErrors (#72474) --- homeassistant/components/nest/climate_sdm.py | 34 +++++++---- tests/components/nest/test_climate_sdm.py | 60 ++++++++++++++++++++ 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index bbbf83501f7..8a56f78028b 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -6,6 +6,7 @@ from typing import Any, cast from google_nest_sdm.device import Device from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.device_traits import FanTrait, TemperatureTrait +from google_nest_sdm.exceptions import ApiException from google_nest_sdm.thermostat_traits import ( ThermostatEcoTrait, ThermostatHeatCoolTrait, @@ -30,6 +31,7 @@ from homeassistant.components.climate.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -294,7 +296,10 @@ class ThermostatEntity(ClimateEntity): hvac_mode = HVACMode.OFF api_mode = THERMOSTAT_INV_MODE_MAP[hvac_mode] trait = self._device.traits[ThermostatModeTrait.NAME] - await trait.set_mode(api_mode) + try: + await trait.set_mode(api_mode) + except ApiException as err: + raise HomeAssistantError(f"Error setting HVAC mode: {err}") from err async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" @@ -308,20 +313,26 @@ class ThermostatEntity(ClimateEntity): if ThermostatTemperatureSetpointTrait.NAME not in self._device.traits: return trait = self._device.traits[ThermostatTemperatureSetpointTrait.NAME] - if self.preset_mode == PRESET_ECO or hvac_mode == HVACMode.HEAT_COOL: - if low_temp and high_temp: - await trait.set_range(low_temp, high_temp) - elif hvac_mode == HVACMode.COOL and temp: - await trait.set_cool(temp) - elif hvac_mode == HVACMode.HEAT and temp: - await trait.set_heat(temp) + try: + if self.preset_mode == PRESET_ECO or hvac_mode == HVACMode.HEAT_COOL: + if low_temp and high_temp: + await trait.set_range(low_temp, high_temp) + elif hvac_mode == HVACMode.COOL and temp: + await trait.set_cool(temp) + elif hvac_mode == HVACMode.HEAT and temp: + await trait.set_heat(temp) + except ApiException as err: + raise HomeAssistantError(f"Error setting HVAC mode: {err}") from err async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new target preset mode.""" if preset_mode not in self.preset_modes: raise ValueError(f"Unsupported preset_mode '{preset_mode}'") trait = self._device.traits[ThermostatEcoTrait.NAME] - await trait.set_mode(PRESET_INV_MODE_MAP[preset_mode]) + try: + await trait.set_mode(PRESET_INV_MODE_MAP[preset_mode]) + except ApiException as err: + raise HomeAssistantError(f"Error setting HVAC mode: {err}") from err async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" @@ -331,4 +342,7 @@ class ThermostatEntity(ClimateEntity): duration = None if fan_mode != FAN_OFF: duration = MAX_FAN_DURATION - await trait.set_timer(FAN_INV_MODE_MAP[fan_mode], duration=duration) + try: + await trait.set_timer(FAN_INV_MODE_MAP[fan_mode], duration=duration) + except ApiException as err: + raise HomeAssistantError(f"Error setting HVAC mode: {err}") from err diff --git a/tests/components/nest/test_climate_sdm.py b/tests/components/nest/test_climate_sdm.py index 5f3efa362b3..123742607ad 100644 --- a/tests/components/nest/test_climate_sdm.py +++ b/tests/components/nest/test_climate_sdm.py @@ -6,8 +6,10 @@ pubsub subscriber. """ from collections.abc import Awaitable, Callable +from http import HTTPStatus from typing import Any +import aiohttp from google_nest_sdm.auth import AbstractAuth from google_nest_sdm.event import EventMessage import pytest @@ -41,6 +43,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from .common import ( DEVICE_COMMAND, @@ -1380,3 +1383,60 @@ async def test_thermostat_invalid_set_preset_mode( # Preset is unchanged assert thermostat.attributes[ATTR_PRESET_MODE] == PRESET_NONE assert thermostat.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_NONE] + + +async def test_thermostat_hvac_mode_failure( + hass: HomeAssistant, + setup_platform: PlatformSetup, + auth: FakeAuth, + create_device: CreateDevice, +) -> None: + """Test setting an hvac_mode that is not supported.""" + create_device.create( + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "OFF", + }, + "sdm.devices.traits.Fan": { + "timerMode": "OFF", + "timerTimeout": "2019-05-10T03:22:54Z", + }, + "sdm.devices.traits.ThermostatEco": { + "availableModes": ["MANUAL_ECO", "OFF"], + "mode": "OFF", + "heatCelsius": 15.0, + "coolCelsius": 28.0, + }, + } + ) + await setup_platform() + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + + auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)] + with pytest.raises(HomeAssistantError): + await common.async_set_hvac_mode(hass, HVAC_MODE_HEAT) + await hass.async_block_till_done() + + auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)] + with pytest.raises(HomeAssistantError): + await common.async_set_temperature( + hass, hvac_mode=HVAC_MODE_HEAT, temperature=25.0 + ) + await hass.async_block_till_done() + + auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)] + with pytest.raises(HomeAssistantError): + await common.async_set_fan_mode(hass, FAN_ON) + await hass.async_block_till_done() + + auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)] + with pytest.raises(HomeAssistantError): + await common.async_set_preset_mode(hass, PRESET_ECO) + await hass.async_block_till_done() From f9d9c3401897768de4e7fb637bddba24bfbd6de1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 26 May 2022 22:15:44 +0200 Subject: [PATCH 0930/3516] Add hardkernel hardware integration (#72489) * Add hardkernel hardware integration * Remove debug prints * Improve tests * Improve test coverage --- CODEOWNERS | 2 + .../components/hardkernel/__init__.py | 22 +++++ .../components/hardkernel/config_flow.py | 22 +++++ homeassistant/components/hardkernel/const.py | 3 + .../components/hardkernel/hardware.py | 39 ++++++++ .../components/hardkernel/manifest.json | 9 ++ script/hassfest/manifest.py | 1 + tests/components/hardkernel/__init__.py | 1 + .../components/hardkernel/test_config_flow.py | 58 ++++++++++++ tests/components/hardkernel/test_hardware.py | 89 +++++++++++++++++++ tests/components/hardkernel/test_init.py | 72 +++++++++++++++ 11 files changed, 318 insertions(+) create mode 100644 homeassistant/components/hardkernel/__init__.py create mode 100644 homeassistant/components/hardkernel/config_flow.py create mode 100644 homeassistant/components/hardkernel/const.py create mode 100644 homeassistant/components/hardkernel/hardware.py create mode 100644 homeassistant/components/hardkernel/manifest.json create mode 100644 tests/components/hardkernel/__init__.py create mode 100644 tests/components/hardkernel/test_config_flow.py create mode 100644 tests/components/hardkernel/test_hardware.py create mode 100644 tests/components/hardkernel/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 98d60fbfcb7..1b0d0ae4d3c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -416,6 +416,8 @@ build.json @home-assistant/supervisor /tests/components/guardian/ @bachya /homeassistant/components/habitica/ @ASMfreaK @leikoilja /tests/components/habitica/ @ASMfreaK @leikoilja +/homeassistant/components/hardkernel/ @home-assistant/core +/tests/components/hardkernel/ @home-assistant/core /homeassistant/components/hardware/ @home-assistant/core /tests/components/hardware/ @home-assistant/core /homeassistant/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan diff --git a/homeassistant/components/hardkernel/__init__.py b/homeassistant/components/hardkernel/__init__.py new file mode 100644 index 00000000000..6dfe30b9e75 --- /dev/null +++ b/homeassistant/components/hardkernel/__init__.py @@ -0,0 +1,22 @@ +"""The Hardkernel integration.""" +from __future__ import annotations + +from homeassistant.components.hassio import get_os_info +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a Hardkernel config entry.""" + if (os_info := get_os_info(hass)) is None: + # The hassio integration has not yet fetched data from the supervisor + raise ConfigEntryNotReady + + board: str + if (board := os_info.get("board")) is None or not board.startswith("odroid"): + # Not running on a Hardkernel board, Home Assistant may have been migrated + hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) + return False + + return True diff --git a/homeassistant/components/hardkernel/config_flow.py b/homeassistant/components/hardkernel/config_flow.py new file mode 100644 index 00000000000..b0445fae231 --- /dev/null +++ b/homeassistant/components/hardkernel/config_flow.py @@ -0,0 +1,22 @@ +"""Config flow for the Hardkernel integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class HardkernelConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Hardkernel.""" + + VERSION = 1 + + async def async_step_system(self, data: dict[str, Any] | None = None) -> FlowResult: + """Handle the initial step.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + return self.async_create_entry(title="Hardkernel", data={}) diff --git a/homeassistant/components/hardkernel/const.py b/homeassistant/components/hardkernel/const.py new file mode 100644 index 00000000000..2850f3d4ebb --- /dev/null +++ b/homeassistant/components/hardkernel/const.py @@ -0,0 +1,3 @@ +"""Constants for the Hardkernel integration.""" + +DOMAIN = "hardkernel" diff --git a/homeassistant/components/hardkernel/hardware.py b/homeassistant/components/hardkernel/hardware.py new file mode 100644 index 00000000000..804f105f2ed --- /dev/null +++ b/homeassistant/components/hardkernel/hardware.py @@ -0,0 +1,39 @@ +"""The Hardkernel hardware platform.""" +from __future__ import annotations + +from homeassistant.components.hardware.models import BoardInfo, HardwareInfo +from homeassistant.components.hassio import get_os_info +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN + +BOARD_NAMES = { + "odroid-c2": "Hardkernel Odroid-C2", + "odroid-c4": "Hardkernel Odroid-C4", + "odroid-n2": "Home Assistant Blue / Hardkernel Odroid-N2", + "odroid-xu4": "Hardkernel Odroid-XU4", +} + + +@callback +def async_info(hass: HomeAssistant) -> HardwareInfo: + """Return board info.""" + if (os_info := get_os_info(hass)) is None: + raise HomeAssistantError + board: str + if (board := os_info.get("board")) is None: + raise HomeAssistantError + if not board.startswith("odroid"): + raise HomeAssistantError + + return HardwareInfo( + board=BoardInfo( + hassio_board_id=board, + manufacturer=DOMAIN, + model=board, + revision=None, + ), + name=BOARD_NAMES.get(board, f"Unknown hardkernel Odroid model '{board}'"), + url=None, + ) diff --git a/homeassistant/components/hardkernel/manifest.json b/homeassistant/components/hardkernel/manifest.json new file mode 100644 index 00000000000..366ca245191 --- /dev/null +++ b/homeassistant/components/hardkernel/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "hardkernel", + "name": "Hardkernel", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/hardkernel", + "dependencies": ["hardware", "hassio"], + "codeowners": ["@home-assistant/core"], + "integration_type": "hardware" +} diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index c478d16cf0f..7f2e8e0d477 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -52,6 +52,7 @@ NO_IOT_CLASS = [ "downloader", "ffmpeg", "frontend", + "hardkernel", "hardware", "history", "homeassistant", diff --git a/tests/components/hardkernel/__init__.py b/tests/components/hardkernel/__init__.py new file mode 100644 index 00000000000..d63b70d5cc5 --- /dev/null +++ b/tests/components/hardkernel/__init__.py @@ -0,0 +1 @@ +"""Tests for the Hardkernel integration.""" diff --git a/tests/components/hardkernel/test_config_flow.py b/tests/components/hardkernel/test_config_flow.py new file mode 100644 index 00000000000..f74b4a4e658 --- /dev/null +++ b/tests/components/hardkernel/test_config_flow.py @@ -0,0 +1,58 @@ +"""Test the Hardkernel config flow.""" +from unittest.mock import patch + +from homeassistant.components.hardkernel.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY + +from tests.common import MockConfigEntry, MockModule, mock_integration + + +async def test_config_flow(hass: HomeAssistant) -> None: + """Test the config flow.""" + mock_integration(hass, MockModule("hassio")) + + with patch( + "homeassistant.components.hardkernel.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "system"} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Hardkernel" + assert result["data"] == {} + assert result["options"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == {} + assert config_entry.title == "Hardkernel" + + +async def test_config_flow_single_entry(hass: HomeAssistant) -> None: + """Test only a single entry is allowed.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Hardkernel", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.hardkernel.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "system"} + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + mock_setup_entry.assert_not_called() diff --git a/tests/components/hardkernel/test_hardware.py b/tests/components/hardkernel/test_hardware.py new file mode 100644 index 00000000000..1c71959719c --- /dev/null +++ b/tests/components/hardkernel/test_hardware.py @@ -0,0 +1,89 @@ +"""Test the Hardkernel hardware platform.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.hardkernel.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, MockModule, mock_integration + + +async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can get the board info.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Hardkernel", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.hardkernel.get_os_info", + return_value={"board": "odroid-n2"}, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.hardkernel.hardware.get_os_info", + return_value={"board": "odroid-n2"}, + ): + await client.send_json({"id": 1, "type": "hardware/info"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] == { + "hardware": [ + { + "board": { + "hassio_board_id": "odroid-n2", + "manufacturer": "hardkernel", + "model": "odroid-n2", + "revision": None, + }, + "name": "Home Assistant Blue / Hardkernel Odroid-N2", + "url": None, + } + ] + } + + +@pytest.mark.parametrize("os_info", [None, {"board": None}, {"board": "other"}]) +async def test_hardware_info_fail(hass: HomeAssistant, hass_ws_client, os_info) -> None: + """Test async_info raises if os_info is not as expected.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Hardkernel", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.hardkernel.get_os_info", + return_value={"board": "odroid-n2"}, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.hardkernel.hardware.get_os_info", + return_value=os_info, + ): + await client.send_json({"id": 1, "type": "hardware/info"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] == {"hardware": []} diff --git a/tests/components/hardkernel/test_init.py b/tests/components/hardkernel/test_init.py new file mode 100644 index 00000000000..f202777f530 --- /dev/null +++ b/tests/components/hardkernel/test_init.py @@ -0,0 +1,72 @@ +"""Test the Hardkernel integration.""" +from unittest.mock import patch + +from homeassistant.components.hardkernel.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, MockModule, mock_integration + + +async def test_setup_entry(hass: HomeAssistant) -> None: + """Test setup of a config entry.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Hardkernel", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.hardkernel.get_os_info", + return_value={"board": "odroid-n2"}, + ) as mock_get_os_info: + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + + +async def test_setup_entry_wrong_board(hass: HomeAssistant) -> None: + """Test setup of a config entry with wrong board type.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Hardkernel", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.hardkernel.get_os_info", + return_value={"board": "generic-x86-64"}, + ) as mock_get_os_info: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + + +async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None: + """Test setup of a config entry when hassio has not fetched os_info.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Hardkernel", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.hardkernel.get_os_info", + return_value=None, + ) as mock_get_os_info: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + assert config_entry.state == ConfigEntryState.SETUP_RETRY From 6bb78754dd8cdbbba47d83f8dac804437f65c8b6 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 26 May 2022 22:02:39 +0200 Subject: [PATCH 0931/3516] Move manual configuration of MQTT device_tracker to the integration key (#72493) --- homeassistant/components/mqtt/__init__.py | 4 +- .../mqtt/device_tracker/__init__.py | 10 ++- .../mqtt/device_tracker/schema_discovery.py | 26 +++++-- tests/components/mqtt/test_device_tracker.py | 69 +++++++++++++------ 4 files changed, 82 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index df5d52c443a..e8847375584 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -159,6 +159,7 @@ PLATFORMS = [ Platform.BUTTON, Platform.CAMERA, Platform.CLIMATE, + Platform.DEVICE_TRACKER, Platform.COVER, Platform.FAN, Platform.HUMIDIFIER, @@ -196,17 +197,18 @@ PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( vol.Optional(Platform.CAMERA.value): cv.ensure_list, vol.Optional(Platform.CLIMATE.value): cv.ensure_list, vol.Optional(Platform.COVER.value): cv.ensure_list, + vol.Optional(Platform.DEVICE_TRACKER.value): cv.ensure_list, vol.Optional(Platform.FAN.value): cv.ensure_list, vol.Optional(Platform.HUMIDIFIER.value): cv.ensure_list, vol.Optional(Platform.LIGHT.value): cv.ensure_list, vol.Optional(Platform.LOCK.value): cv.ensure_list, + vol.Optional(Platform.NUMBER.value): cv.ensure_list, vol.Optional(Platform.SCENE.value): cv.ensure_list, vol.Optional(Platform.SELECT.value): cv.ensure_list, vol.Optional(Platform.SIREN.value): cv.ensure_list, vol.Optional(Platform.SENSOR.value): cv.ensure_list, vol.Optional(Platform.SWITCH.value): cv.ensure_list, vol.Optional(Platform.VACUUM.value): cv.ensure_list, - vol.Optional(Platform.NUMBER.value): cv.ensure_list, } ) diff --git a/homeassistant/components/mqtt/device_tracker/__init__.py b/homeassistant/components/mqtt/device_tracker/__init__.py index 03574e6554b..bcd5bbd4ee1 100644 --- a/homeassistant/components/mqtt/device_tracker/__init__.py +++ b/homeassistant/components/mqtt/device_tracker/__init__.py @@ -1,7 +1,15 @@ """Support for tracking MQTT enabled devices.""" +import voluptuous as vol + +from homeassistant.components import device_tracker + +from ..mixins import warn_for_legacy_schema from .schema_discovery import async_setup_entry_from_discovery from .schema_yaml import PLATFORM_SCHEMA_YAML, async_setup_scanner_from_yaml -PLATFORM_SCHEMA = PLATFORM_SCHEMA_YAML +# Configuring MQTT Device Trackers under the device_tracker platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA_YAML, warn_for_legacy_schema(device_tracker.DOMAIN) +) async_setup_scanner = async_setup_scanner_from_yaml async_setup_entry = async_setup_entry_from_discovery diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index a7b597d0689..aa7506bd5e3 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -1,4 +1,5 @@ -"""Support for tracking MQTT enabled devices identified through discovery.""" +"""Support for tracking MQTT enabled devices.""" +import asyncio import functools import voluptuous as vol @@ -22,13 +23,18 @@ from .. import MqttValueTemplate, subscription from ... import mqtt from ..const import CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages -from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper +from ..mixins import ( + MQTT_ENTITY_COMMON_SCHEMA, + MqttEntity, + async_get_platform_config_from_yaml, + async_setup_entry_helper, +) CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" CONF_SOURCE_TYPE = "source_type" -PLATFORM_SCHEMA_DISCOVERY = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RO_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, @@ -37,11 +43,21 @@ 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) +DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.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.""" + """Set up MQTT device tracker configuration.yaml and dynamically through MQTT discovery.""" + # load and initialize platform config from configuration.yaml + await asyncio.gather( + *( + _async_setup_entity(hass, async_add_entities, config, config_entry) + for config in await async_get_platform_config_from_yaml( + hass, device_tracker.DOMAIN, PLATFORM_SCHEMA_MODERN + ) + ) + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index c85fcef7dc4..020fbad6166 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -1,22 +1,17 @@ -"""The tests for the MQTT device tracker platform.""" +"""The tests for the MQTT device tracker platform using configuration.yaml.""" from unittest.mock import patch -import pytest - from homeassistant.components.device_tracker.const import DOMAIN, SOURCE_TYPE_BLUETOOTH from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME from homeassistant.setup import async_setup_component +from .test_common import help_test_setup_manual_entity_from_yaml + from tests.common import async_fire_mqtt_message -@pytest.fixture(autouse=True) -def setup_comp(hass, mqtt_mock): - """Set up mqtt component.""" - pass - - -async def test_ensure_device_tracker_platform_validation(hass): +# Deprecated in HA Core 2022.6 +async def test_legacy_ensure_device_tracker_platform_validation(hass, mqtt_mock): """Test if platform validation was done.""" async def mock_setup_scanner(hass, config, see, discovery_info=None): @@ -37,7 +32,8 @@ async def test_ensure_device_tracker_platform_validation(hass): assert mock_sp.call_count == 1 -async def test_new_message(hass, mock_device_tracker_conf): +# Deprecated in HA Core 2022.6 +async def test_legacy_new_message(hass, mock_device_tracker_conf, mqtt_mock): """Test new message.""" dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" @@ -53,7 +49,10 @@ async def test_new_message(hass, mock_device_tracker_conf): assert hass.states.get(entity_id).state == location -async def test_single_level_wildcard_topic(hass, mock_device_tracker_conf): +# Deprecated in HA Core 2022.6 +async def test_legacy_single_level_wildcard_topic( + hass, mock_device_tracker_conf, mqtt_mock +): """Test single level wildcard topic.""" dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" @@ -72,7 +71,10 @@ async def test_single_level_wildcard_topic(hass, mock_device_tracker_conf): assert hass.states.get(entity_id).state == location -async def test_multi_level_wildcard_topic(hass, mock_device_tracker_conf): +# Deprecated in HA Core 2022.6 +async def test_legacy_multi_level_wildcard_topic( + hass, mock_device_tracker_conf, mqtt_mock +): """Test multi level wildcard topic.""" dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" @@ -91,7 +93,10 @@ async def test_multi_level_wildcard_topic(hass, mock_device_tracker_conf): assert hass.states.get(entity_id).state == location -async def test_single_level_wildcard_topic_not_matching(hass, mock_device_tracker_conf): +# Deprecated in HA Core 2022.6 +async def test_legacy_single_level_wildcard_topic_not_matching( + hass, mock_device_tracker_conf, mqtt_mock +): """Test not matching single level wildcard topic.""" dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" @@ -110,7 +115,10 @@ async def test_single_level_wildcard_topic_not_matching(hass, mock_device_tracke assert hass.states.get(entity_id) is None -async def test_multi_level_wildcard_topic_not_matching(hass, mock_device_tracker_conf): +# Deprecated in HA Core 2022.6 +async def test_legacy_multi_level_wildcard_topic_not_matching( + hass, mock_device_tracker_conf, mqtt_mock +): """Test not matching multi level wildcard topic.""" dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" @@ -129,8 +137,9 @@ async def test_multi_level_wildcard_topic_not_matching(hass, mock_device_tracker assert hass.states.get(entity_id) is None -async def test_matching_custom_payload_for_home_and_not_home( - hass, mock_device_tracker_conf +# Deprecated in HA Core 2022.6 +async def test_legacy_matching_custom_payload_for_home_and_not_home( + hass, mock_device_tracker_conf, mqtt_mock ): """Test custom payload_home sets state to home and custom payload_not_home sets state to not_home.""" dev_id = "paulus" @@ -161,8 +170,9 @@ async def test_matching_custom_payload_for_home_and_not_home( assert hass.states.get(entity_id).state == STATE_NOT_HOME -async def test_not_matching_custom_payload_for_home_and_not_home( - hass, mock_device_tracker_conf +# Deprecated in HA Core 2022.6 +async def test_legacy_not_matching_custom_payload_for_home_and_not_home( + hass, mock_device_tracker_conf, mqtt_mock ): """Test not matching payload does not set state to home or not_home.""" dev_id = "paulus" @@ -191,7 +201,8 @@ async def test_not_matching_custom_payload_for_home_and_not_home( assert hass.states.get(entity_id).state != STATE_NOT_HOME -async def test_matching_source_type(hass, mock_device_tracker_conf): +# Deprecated in HA Core 2022.6 +async def test_legacy_matching_source_type(hass, mock_device_tracker_conf, mqtt_mock): """Test setting source type.""" dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" @@ -215,3 +226,21 @@ async def test_matching_source_type(hass, mock_device_tracker_conf): async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() assert hass.states.get(entity_id).attributes["source_type"] == SOURCE_TYPE_BLUETOOTH + + +async def test_setup_with_modern_schema( + hass, caplog, tmp_path, mock_device_tracker_conf +): + """Test setup using the modern schema.""" + dev_id = "jan" + entity_id = f"{DOMAIN}.{dev_id}" + topic = "/location/jan" + + hass.config.components = {"zone"} + config = {"name": dev_id, "state_topic": topic} + + await help_test_setup_manual_entity_from_yaml( + hass, caplog, tmp_path, DOMAIN, config + ) + + assert hass.states.get(entity_id) is not None From ff72c32b20248342cc7ab022cb03662cdb5deb6b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 May 2022 15:17:08 -1000 Subject: [PATCH 0932/3516] Fixes for logbook filtering and add it to the live stream (#72501) --- homeassistant/components/logbook/processor.py | 32 ++- .../components/logbook/queries/__init__.py | 12 +- .../components/logbook/queries/all.py | 18 +- .../components/logbook/queries/common.py | 42 +--- .../components/logbook/queries/devices.py | 7 +- .../components/logbook/queries/entities.py | 9 +- homeassistant/components/recorder/filters.py | 100 +++++++--- homeassistant/components/recorder/history.py | 4 +- homeassistant/components/recorder/models.py | 40 +++- tests/components/logbook/test_init.py | 5 +- .../components/logbook/test_websocket_api.py | 185 ++++++++++++++++++ 11 files changed, 340 insertions(+), 114 deletions(-) diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index 03506695700..ea6002cc62c 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -173,12 +173,6 @@ class EventProcessor: self.filters, self.context_id, ) - if _LOGGER.isEnabledFor(logging.DEBUG): - _LOGGER.debug( - "Literal statement: %s", - stmt.compile(compile_kwargs={"literal_binds": True}), - ) - with session_scope(hass=self.hass) as session: return self.humanify(yield_rows(session.execute(stmt))) @@ -214,20 +208,16 @@ def _humanify( include_entity_name = logbook_run.include_entity_name format_time = logbook_run.format_time - def _keep_row(row: Row | EventAsRow, event_type: str) -> bool: + def _keep_row(row: EventAsRow) -> bool: """Check if the entity_filter rejects a row.""" assert entities_filter is not None - if entity_id := _row_event_data_extract(row, ENTITY_ID_JSON_EXTRACT): + if entity_id := row.entity_id: return entities_filter(entity_id) - - if event_type in external_events: - # If the entity_id isn't described, use the domain that describes - # the event for filtering. - domain: str | None = external_events[event_type][0] - else: - domain = _row_event_data_extract(row, DOMAIN_JSON_EXTRACT) - - return domain is not None and entities_filter(f"{domain}._") + if entity_id := row.data.get(ATTR_ENTITY_ID): + return entities_filter(entity_id) + if domain := row.data.get(ATTR_DOMAIN): + return entities_filter(f"{domain}._") + return True # Process rows for row in rows: @@ -236,12 +226,12 @@ def _humanify( continue event_type = row.event_type if event_type == EVENT_CALL_SERVICE or ( - event_type is not PSUEDO_EVENT_STATE_CHANGED - and entities_filter is not None - and not _keep_row(row, event_type) + entities_filter + # We literally mean is EventAsRow not a subclass of EventAsRow + and type(row) is EventAsRow # pylint: disable=unidiomatic-typecheck + and not _keep_row(row) ): continue - if event_type is PSUEDO_EVENT_STATE_CHANGED: entity_id = row.entity_id assert entity_id is not None diff --git a/homeassistant/components/logbook/queries/__init__.py b/homeassistant/components/logbook/queries/__init__.py index 3672f1e761c..3c027823612 100644 --- a/homeassistant/components/logbook/queries/__init__.py +++ b/homeassistant/components/logbook/queries/__init__.py @@ -27,8 +27,16 @@ def statement_for_request( # No entities: logbook sends everything for the timeframe # limited by the context_id and the yaml configured filter if not entity_ids and not device_ids: - entity_filter = filters.entity_filter() if filters else None - return all_stmt(start_day, end_day, event_types, entity_filter, context_id) + states_entity_filter = filters.states_entity_filter() if filters else None + events_entity_filter = filters.events_entity_filter() if filters else None + return all_stmt( + start_day, + end_day, + event_types, + states_entity_filter, + events_entity_filter, + context_id, + ) # sqlalchemy caches object quoting, the # json quotable ones must be a different diff --git a/homeassistant/components/logbook/queries/all.py b/homeassistant/components/logbook/queries/all.py index da17c7bddeb..d321578f545 100644 --- a/homeassistant/components/logbook/queries/all.py +++ b/homeassistant/components/logbook/queries/all.py @@ -22,7 +22,8 @@ def all_stmt( start_day: dt, end_day: dt, event_types: tuple[str, ...], - entity_filter: ClauseList | None = None, + states_entity_filter: ClauseList | None = None, + events_entity_filter: ClauseList | None = None, context_id: str | None = None, ) -> StatementLambdaElement: """Generate a logbook query for all entities.""" @@ -37,12 +38,17 @@ def all_stmt( _states_query_for_context_id(start_day, end_day, context_id), legacy_select_events_context_id(start_day, end_day, context_id), ) - elif entity_filter is not None: - stmt += lambda s: s.union_all( - _states_query_for_all(start_day, end_day).where(entity_filter) - ) else: - stmt += lambda s: s.union_all(_states_query_for_all(start_day, end_day)) + if events_entity_filter is not None: + stmt += lambda s: s.where(events_entity_filter) + + if states_entity_filter is not None: + stmt += lambda s: s.union_all( + _states_query_for_all(start_day, end_day).where(states_entity_filter) + ) + else: + stmt += lambda s: s.union_all(_states_query_for_all(start_day, end_day)) + stmt += lambda s: s.order_by(Events.time_fired) return stmt diff --git a/homeassistant/components/logbook/queries/common.py b/homeassistant/components/logbook/queries/common.py index 237fde3f653..6049d6beb81 100644 --- a/homeassistant/components/logbook/queries/common.py +++ b/homeassistant/components/logbook/queries/common.py @@ -1,22 +1,20 @@ """Queries for logbook.""" from __future__ import annotations -from collections.abc import Callable from datetime import datetime as dt -import json -from typing import Any import sqlalchemy -from sqlalchemy import JSON, select, type_coerce -from sqlalchemy.orm import Query, aliased +from sqlalchemy import select +from sqlalchemy.orm import Query from sqlalchemy.sql.elements import ClauseList from sqlalchemy.sql.expression import literal from sqlalchemy.sql.selectable import Select from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN from homeassistant.components.recorder.models import ( - JSON_VARIENT_CAST, - JSONB_VARIENT_CAST, + OLD_FORMAT_ATTRS_JSON, + OLD_STATE, + SHARED_ATTRS_JSON, EventData, Events, StateAttributes, @@ -30,36 +28,6 @@ CONTINUOUS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in CONTINUOUS_DOMAINS] UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%" -OLD_STATE = aliased(States, name="old_state") - - -class JSONLiteral(JSON): # type: ignore[misc] - """Teach SA how to literalize json.""" - - def literal_processor(self, dialect: str) -> Callable[[Any], str]: - """Processor to convert a value to JSON.""" - - def process(value: Any) -> str: - """Dump json.""" - return json.dumps(value) - - return process - - -EVENT_DATA_JSON = type_coerce( - EventData.shared_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) -) -OLD_FORMAT_EVENT_DATA_JSON = type_coerce( - Events.event_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) -) - -SHARED_ATTRS_JSON = type_coerce( - StateAttributes.shared_attrs.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) -) -OLD_FORMAT_ATTRS_JSON = type_coerce( - States.attributes.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) -) - PSUEDO_EVENT_STATE_CHANGED = None # Since we don't store event_types and None diff --git a/homeassistant/components/logbook/queries/devices.py b/homeassistant/components/logbook/queries/devices.py index 5e7827b87a0..64a6477017e 100644 --- a/homeassistant/components/logbook/queries/devices.py +++ b/homeassistant/components/logbook/queries/devices.py @@ -4,24 +4,21 @@ from __future__ import annotations from collections.abc import Iterable from datetime import datetime as dt -from sqlalchemy import Column, lambda_stmt, select, union_all +from sqlalchemy import lambda_stmt, select, union_all from sqlalchemy.orm import Query from sqlalchemy.sql.elements import ClauseList from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import CTE, CompoundSelect -from homeassistant.components.recorder.models import Events, States +from homeassistant.components.recorder.models import DEVICE_ID_IN_EVENT, Events, States from .common import ( - EVENT_DATA_JSON, select_events_context_id_subquery, select_events_context_only, select_events_without_states, select_states_context_only, ) -DEVICE_ID_IN_EVENT: Column = EVENT_DATA_JSON["device_id"] - def _select_device_id_context_ids_sub_query( start_day: dt, diff --git a/homeassistant/components/logbook/queries/entities.py b/homeassistant/components/logbook/queries/entities.py index 844890c23a9..4fb211688f3 100644 --- a/homeassistant/components/logbook/queries/entities.py +++ b/homeassistant/components/logbook/queries/entities.py @@ -5,20 +5,20 @@ from collections.abc import Iterable from datetime import datetime as dt import sqlalchemy -from sqlalchemy import Column, lambda_stmt, select, union_all +from sqlalchemy import lambda_stmt, select, union_all from sqlalchemy.orm import Query from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import CTE, CompoundSelect from homeassistant.components.recorder.models import ( + ENTITY_ID_IN_EVENT, ENTITY_ID_LAST_UPDATED_INDEX, + OLD_ENTITY_ID_IN_EVENT, Events, States, ) from .common import ( - EVENT_DATA_JSON, - OLD_FORMAT_EVENT_DATA_JSON, apply_states_filters, select_events_context_id_subquery, select_events_context_only, @@ -27,9 +27,6 @@ from .common import ( select_states_context_only, ) -ENTITY_ID_IN_EVENT: Column = EVENT_DATA_JSON["entity_id"] -OLD_ENTITY_ID_IN_EVENT: Column = OLD_FORMAT_EVENT_DATA_JSON["entity_id"] - def _select_entities_context_ids_sub_query( start_day: dt, diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index adc746379e6..7f1d0bc597f 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -1,14 +1,18 @@ """Provide pre-made queries on top of the recorder component.""" from __future__ import annotations -from sqlalchemy import not_, or_ +from collections.abc import Callable, Iterable +import json +from typing import Any + +from sqlalchemy import Column, not_, or_ from sqlalchemy.sql.elements import ClauseList from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS from homeassistant.helpers.typing import ConfigType -from .models import States +from .models import ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT, States DOMAIN = "history" HISTORY_FILTERS = "history_filters" @@ -59,50 +63,84 @@ class Filters: or self.included_entity_globs ) - def entity_filter(self) -> ClauseList: - """Generate the entity filter query.""" + def _generate_filter_for_columns( + self, columns: Iterable[Column], encoder: Callable[[Any], Any] + ) -> ClauseList: includes = [] if self.included_domains: - includes.append( - or_( - *[ - States.entity_id.like(f"{domain}.%") - for domain in self.included_domains - ] - ).self_group() - ) + includes.append(_domain_matcher(self.included_domains, columns, encoder)) if self.included_entities: - includes.append(States.entity_id.in_(self.included_entities)) - for glob in self.included_entity_globs: - includes.append(_glob_to_like(glob)) + includes.append(_entity_matcher(self.included_entities, columns, encoder)) + if self.included_entity_globs: + includes.append( + _globs_to_like(self.included_entity_globs, columns, encoder) + ) excludes = [] if self.excluded_domains: - excludes.append( - or_( - *[ - States.entity_id.like(f"{domain}.%") - for domain in self.excluded_domains - ] - ).self_group() - ) + excludes.append(_domain_matcher(self.excluded_domains, columns, encoder)) if self.excluded_entities: - excludes.append(States.entity_id.in_(self.excluded_entities)) - for glob in self.excluded_entity_globs: - excludes.append(_glob_to_like(glob)) + excludes.append(_entity_matcher(self.excluded_entities, columns, encoder)) + if self.excluded_entity_globs: + excludes.append( + _globs_to_like(self.excluded_entity_globs, columns, encoder) + ) if not includes and not excludes: return None if includes and not excludes: - return or_(*includes) + return or_(*includes).self_group() if not includes and excludes: - return not_(or_(*excludes)) + return not_(or_(*excludes).self_group()) - return or_(*includes) & not_(or_(*excludes)) + return or_(*includes).self_group() & not_(or_(*excludes).self_group()) + + def states_entity_filter(self) -> ClauseList: + """Generate the entity filter query.""" + + def _encoder(data: Any) -> Any: + """Nothing to encode for states since there is no json.""" + return data + + return self._generate_filter_for_columns((States.entity_id,), _encoder) + + def events_entity_filter(self) -> ClauseList: + """Generate the entity filter query.""" + _encoder = json.dumps + return or_( + (ENTITY_ID_IN_EVENT == _encoder(None)) + & (OLD_ENTITY_ID_IN_EVENT == _encoder(None)), + self._generate_filter_for_columns( + (ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT), _encoder + ).self_group(), + ) -def _glob_to_like(glob_str: str) -> ClauseList: +def _globs_to_like( + glob_strs: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] +) -> ClauseList: """Translate glob to sql.""" - return States.entity_id.like(glob_str.translate(GLOB_TO_SQL_CHARS)) + return or_( + column.like(encoder(glob_str.translate(GLOB_TO_SQL_CHARS))) + for glob_str in glob_strs + for column in columns + ) + + +def _entity_matcher( + entity_ids: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] +) -> ClauseList: + return or_( + column.in_([encoder(entity_id) for entity_id in entity_ids]) + for column in columns + ) + + +def _domain_matcher( + domains: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] +) -> ClauseList: + return or_( + column.like(encoder(f"{domain}.%")) for domain in domains for column in columns + ) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 845a2af62bf..7e8e97eafd4 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -236,7 +236,7 @@ def _significant_states_stmt( else: stmt += _ignore_domains_filter if filters and filters.has_config: - entity_filter = filters.entity_filter() + entity_filter = filters.states_entity_filter() stmt += lambda q: q.filter(entity_filter) stmt += lambda q: q.filter(States.last_updated > start_time) @@ -528,7 +528,7 @@ def _get_states_for_all_stmt( ) stmt += _ignore_domains_filter if filters and filters.has_config: - entity_filter = filters.entity_filter() + entity_filter = filters.states_entity_filter() stmt += lambda q: q.filter(entity_filter) if join_attributes: stmt += lambda q: q.outerjoin( diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 90c2e5e5616..dff8edde79f 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -1,6 +1,7 @@ """Models for SQLAlchemy.""" from __future__ import annotations +from collections.abc import Callable from datetime import datetime, timedelta import json import logging @@ -9,6 +10,7 @@ from typing import Any, TypedDict, cast, overload import ciso8601 from fnvhash import fnv1a_32 from sqlalchemy import ( + JSON, BigInteger, Boolean, Column, @@ -22,11 +24,12 @@ from sqlalchemy import ( String, Text, distinct, + type_coerce, ) from sqlalchemy.dialects import mysql, oracle, postgresql, sqlite from sqlalchemy.engine.row import Row from sqlalchemy.ext.declarative import declared_attr -from sqlalchemy.orm import declarative_base, relationship +from sqlalchemy.orm import aliased, declarative_base, relationship from sqlalchemy.orm.session import Session from homeassistant.components.websocket_api.const import ( @@ -119,6 +122,21 @@ DOUBLE_TYPE = ( .with_variant(oracle.DOUBLE_PRECISION(), "oracle") .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") ) + + +class JSONLiteral(JSON): # type: ignore[misc] + """Teach SA how to literalize json.""" + + def literal_processor(self, dialect: str) -> Callable[[Any], str]: + """Processor to convert a value to JSON.""" + + def process(value: Any) -> str: + """Dump json.""" + return json.dumps(value) + + return process + + EVENT_ORIGIN_ORDER = [EventOrigin.local, EventOrigin.remote] EVENT_ORIGIN_TO_IDX = {origin: idx for idx, origin in enumerate(EVENT_ORIGIN_ORDER)} @@ -612,6 +630,26 @@ class StatisticsRuns(Base): # type: ignore[misc,valid-type] ) +EVENT_DATA_JSON = type_coerce( + EventData.shared_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) +) +OLD_FORMAT_EVENT_DATA_JSON = type_coerce( + Events.event_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) +) + +SHARED_ATTRS_JSON = type_coerce( + StateAttributes.shared_attrs.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) +) +OLD_FORMAT_ATTRS_JSON = type_coerce( + States.attributes.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) +) + +ENTITY_ID_IN_EVENT: Column = EVENT_DATA_JSON["entity_id"] +OLD_ENTITY_ID_IN_EVENT: Column = OLD_FORMAT_EVENT_DATA_JSON["entity_id"] +DEVICE_ID_IN_EVENT: Column = EVENT_DATA_JSON["device_id"] +OLD_STATE = aliased(States, name="old_state") + + @overload def process_timestamp(ts: None) -> None: ... diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 101fb74e690..2903f29f5dc 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -510,7 +510,7 @@ async def test_exclude_described_event(hass, hass_client, recorder_mock): return { "name": "Test Name", "message": "tested a message", - "entity_id": event.data.get(ATTR_ENTITY_ID), + "entity_id": event.data[ATTR_ENTITY_ID], } def async_describe_events(hass, async_describe_event): @@ -2003,13 +2003,12 @@ async def test_include_events_domain_glob(hass, hass_client, recorder_mock): ) await async_recorder_block_till_done(hass) - # Should get excluded by domain hass.bus.async_fire( logbook.EVENT_LOGBOOK_ENTRY, { logbook.ATTR_NAME: "Alarm", logbook.ATTR_MESSAGE: "is triggered", - logbook.ATTR_DOMAIN: "switch", + logbook.ATTR_ENTITY_ID: "switch.any", }, ) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 8706ccf7617..02fea4f980f 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -14,16 +14,21 @@ from homeassistant.components.logbook import websocket_api from homeassistant.components.script import EVENT_SCRIPT_STARTED from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import ( + ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_NAME, ATTR_UNIT_OF_MEASUREMENT, + CONF_DOMAINS, + CONF_ENTITIES, + CONF_EXCLUDE, EVENT_HOMEASSISTANT_START, STATE_OFF, STATE_ON, ) from homeassistant.core import Event, HomeAssistant, State from homeassistant.helpers import device_registry +from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -457,6 +462,186 @@ async def test_get_events_with_device_ids(hass, hass_ws_client, recorder_mock): assert isinstance(results[3]["when"], float) +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_unsubscribe_logbook_stream_excluded_entities( + hass, recorder_mock, hass_ws_client +): + """Test subscribe/unsubscribe logbook stream with excluded entities.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "automation", "script") + ] + ) + await async_setup_component( + hass, + logbook.DOMAIN, + { + logbook.DOMAIN: { + CONF_EXCLUDE: { + CONF_ENTITIES: ["light.exc"], + CONF_DOMAINS: ["switch"], + CONF_ENTITY_GLOBS: "*.excluded", + } + }, + }, + ) + await hass.async_block_till_done() + init_count = sum(hass.bus.async_listeners().values()) + + hass.states.async_set("light.exc", STATE_ON) + hass.states.async_set("light.exc", STATE_OFF) + hass.states.async_set("switch.any", STATE_ON) + hass.states.async_set("switch.any", STATE_OFF) + hass.states.async_set("cover.excluded", STATE_ON) + hass.states.async_set("cover.excluded", STATE_OFF) + + hass.states.async_set("binary_sensor.is_light", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + state: State = hass.states.get("binary_sensor.is_light") + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + {"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()} + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + { + "entity_id": "binary_sensor.is_light", + "state": "off", + "when": state.last_updated.timestamp(), + } + ] + assert msg["event"]["start_time"] == now.timestamp() + assert msg["event"]["end_time"] > msg["event"]["start_time"] + assert msg["event"]["partial"] is True + + hass.states.async_set("light.exc", STATE_ON) + hass.states.async_set("light.exc", STATE_OFF) + hass.states.async_set("switch.any", STATE_ON) + hass.states.async_set("switch.any", STATE_OFF) + hass.states.async_set("cover.excluded", STATE_ON) + hass.states.async_set("cover.excluded", STATE_OFF) + hass.states.async_set("light.alpha", "on") + hass.states.async_set("light.alpha", "off") + alpha_off_state: State = hass.states.get("light.alpha") + hass.states.async_set("light.zulu", "on", {"color": "blue"}) + hass.states.async_set("light.zulu", "off", {"effect": "help"}) + zulu_off_state: State = hass.states.get("light.zulu") + hass.states.async_set( + "light.zulu", "on", {"effect": "help", "color": ["blue", "green"]} + ) + zulu_on_state: State = hass.states.get("light.zulu") + await hass.async_block_till_done() + + hass.states.async_remove("light.zulu") + await hass.async_block_till_done() + + hass.states.async_set("light.zulu", "on", {"effect": "help", "color": "blue"}) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [ + { + "entity_id": "light.alpha", + "state": "off", + "when": alpha_off_state.last_updated.timestamp(), + }, + { + "entity_id": "light.zulu", + "state": "off", + "when": zulu_off_state.last_updated.timestamp(), + }, + { + "entity_id": "light.zulu", + "state": "on", + "when": zulu_on_state.last_updated.timestamp(), + }, + ] + + await async_wait_recording_done(hass) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation 3", ATTR_ENTITY_ID: "cover.excluded"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + { + ATTR_NAME: "Mock automation switch matching entity", + ATTR_ENTITY_ID: "switch.match_domain", + }, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation switch matching domain", ATTR_DOMAIN: "switch"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation matches nothing"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation 3", ATTR_ENTITY_ID: "light.keep"}, + ) + hass.states.async_set("cover.excluded", STATE_ON) + hass.states.async_set("cover.excluded", STATE_OFF) + await hass.async_block_till_done() + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + { + "context_id": ANY, + "domain": "automation", + "entity_id": None, + "message": "triggered", + "name": "Mock automation matches nothing", + "source": None, + "when": ANY, + }, + { + "context_id": ANY, + "domain": "automation", + "entity_id": "light.keep", + "message": "triggered", + "name": "Mock automation 3", + "source": None, + "when": ANY, + }, + ] + + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) async def test_subscribe_unsubscribe_logbook_stream( hass, recorder_mock, hass_ws_client From 180b5cd2bb1c077e52231951b57b3ec871da1c5c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 May 2022 17:02:21 -1000 Subject: [PATCH 0933/3516] Fix flux_led taking a long time to recover after offline (#72507) --- homeassistant/components/flux_led/__init__.py | 21 +++++- .../components/flux_led/config_flow.py | 14 +++- homeassistant/components/flux_led/const.py | 2 + .../components/flux_led/coordinator.py | 5 +- .../components/flux_led/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/flux_led/test_init.py | 70 ++++++++++++++++++- 8 files changed, 110 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 17dc28a5edf..e6c1393154a 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -15,7 +15,10 @@ from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.event import ( async_track_time_change, async_track_time_interval, @@ -27,6 +30,7 @@ from .const import ( DISCOVER_SCAN_TIMEOUT, DOMAIN, FLUX_LED_DISCOVERY, + FLUX_LED_DISCOVERY_SIGNAL, FLUX_LED_EXCEPTIONS, SIGNAL_STATE_UPDATED, STARTUP_SCAN_TIMEOUT, @@ -196,6 +200,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # to avoid a race condition where the add_update_listener is not # in place in time for the check in async_update_entry_from_discovery entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + + async def _async_handle_discovered_device() -> None: + """Handle device discovery.""" + # Force a refresh if the device is now available + if not coordinator.last_update_success: + coordinator.force_next_update = True + await coordinator.async_refresh() + + entry.async_on_unload( + async_dispatcher_connect( + hass, + FLUX_LED_DISCOVERY_SIGNAL.format(entry_id=entry.entry_id), + _async_handle_discovered_device, + ) + ) return True diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index dfb6ff4a174..61395d744b3 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -21,6 +21,7 @@ from homeassistant.const import CONF_HOST from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import DiscoveryInfoType from . import async_wifi_bulb_for_host @@ -31,6 +32,7 @@ from .const import ( DEFAULT_EFFECT_SPEED, DISCOVER_SCAN_TIMEOUT, DOMAIN, + FLUX_LED_DISCOVERY_SIGNAL, FLUX_LED_EXCEPTIONS, TRANSITION_GRADUAL, TRANSITION_JUMP, @@ -109,12 +111,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): and ":" in entry.unique_id and mac_matches_by_one(entry.unique_id, mac) ): - if async_update_entry_from_discovery( - self.hass, entry, device, None, allow_update_mac + if ( + async_update_entry_from_discovery( + self.hass, entry, device, None, allow_update_mac + ) + or entry.state == config_entries.ConfigEntryState.SETUP_RETRY ): self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) ) + else: + async_dispatcher_send( + self.hass, + FLUX_LED_DISCOVERY_SIGNAL.format(entry_id=entry.entry_id), + ) raise AbortFlow("already_configured") async def _async_handle_discovery(self) -> FlowResult: diff --git a/homeassistant/components/flux_led/const.py b/homeassistant/components/flux_led/const.py index 7fa841ec77f..db545aa1e68 100644 --- a/homeassistant/components/flux_led/const.py +++ b/homeassistant/components/flux_led/const.py @@ -74,3 +74,5 @@ EFFECT_SPEED_SUPPORT_MODES: Final = {ColorMode.RGB, ColorMode.RGBW, ColorMode.RG 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_LED_DISCOVERY_SIGNAL = "flux_led_discovery_{entry_id}" diff --git a/homeassistant/components/flux_led/coordinator.py b/homeassistant/components/flux_led/coordinator.py index 5f2c3c097c0..5a7b3c89216 100644 --- a/homeassistant/components/flux_led/coordinator.py +++ b/homeassistant/components/flux_led/coordinator.py @@ -30,6 +30,7 @@ class FluxLedUpdateCoordinator(DataUpdateCoordinator): self.device = device self.title = entry.title self.entry = entry + self.force_next_update = False super().__init__( hass, _LOGGER, @@ -45,6 +46,8 @@ class FluxLedUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> None: """Fetch all device and sensor data from api.""" try: - await self.device.async_update() + await self.device.async_update(force=self.force_next_update) except FLUX_LED_EXCEPTIONS as ex: raise UpdateFailed(ex) from ex + finally: + self.force_next_update = False diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index d2eb4e1e2e0..7ccd708f89b 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.28.29"], + "requirements": ["flux_led==0.28.30"], "quality_scale": "platinum", "codeowners": ["@icemanch", "@bdraco"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index fbcada246a7..054f38972db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -657,7 +657,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.29 +flux_led==0.28.30 # homeassistant.components.homekit # homeassistant.components.recorder diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f347a7ea082..f35d8da8ec8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -466,7 +466,7 @@ fjaraskupan==1.0.2 flipr-api==1.4.2 # homeassistant.components.flux_led -flux_led==0.28.29 +flux_led==0.28.30 # homeassistant.components.homekit # homeassistant.components.recorder diff --git a/tests/components/flux_led/test_init.py b/tests/components/flux_led/test_init.py index b0a2c5dd33b..3504dbf3bea 100644 --- a/tests/components/flux_led/test_init.py +++ b/tests/components/flux_led/test_init.py @@ -2,10 +2,11 @@ from __future__ import annotations from datetime import timedelta -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest +from homeassistant import config_entries from homeassistant.components import flux_led from homeassistant.components.flux_led.const import ( CONF_REMOTE_ACCESS_ENABLED, @@ -19,6 +20,8 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STARTED, + STATE_ON, + STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -27,6 +30,7 @@ from homeassistant.util.dt import utcnow from . import ( DEFAULT_ENTRY_TITLE, + DHCP_DISCOVERY, FLUX_DISCOVERY, FLUX_DISCOVERY_PARTIAL, IP_ADDRESS, @@ -113,6 +117,70 @@ async def test_config_entry_retry(hass: HomeAssistant) -> None: assert config_entry.state == ConfigEntryState.SETUP_RETRY +async def test_config_entry_retry_right_away_on_discovery(hass: HomeAssistant) -> None: + """Test discovery makes the config entry reload if its in a retry state.""" + 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 + + with _patch_discovery(), _patch_wifibulb(): + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=DHCP_DISCOVERY, + ) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + + +async def test_coordinator_retry_right_away_on_discovery_already_setup( + hass: HomeAssistant, +) -> None: + """Test discovery makes the coordinator force poll if its already setup.""" + 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(), _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.LOADED + + entity_id = "light.bulb_rgbcw_ddeeff" + 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) + async_fire_time_changed(hass, now + timedelta(seconds=50)) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + bulb.async_update = AsyncMock() + + with _patch_discovery(), _patch_wifibulb(): + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=DHCP_DISCOVERY, + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + + @pytest.mark.parametrize( "discovery,title", [ From f038d0892a8fd6543f290907822cf5f1f887aac6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 26 May 2022 03:03:43 -0400 Subject: [PATCH 0934/3516] Update node statistics for zwave_js device diagnostics dump (#72509) --- homeassistant/components/zwave_js/diagnostics.py | 4 +++- tests/components/zwave_js/test_diagnostics.py | 11 ++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 4e1abe37b1b..3372b0eeec0 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -155,6 +155,8 @@ async def async_get_device_diagnostics( node = driver.controller.nodes[node_id] entities = get_device_entities(hass, node, device) assert client.version + node_state = redact_node_state(async_redact_data(node.data, KEYS_TO_REDACT)) + node_state["statistics"] = node.statistics.data return { "versionInfo": { "driverVersion": client.version.driver_version, @@ -163,5 +165,5 @@ async def async_get_device_diagnostics( "maxSchemaVersion": client.version.max_schema_version, }, "entities": entities, - "state": redact_node_state(async_redact_data(node.data, KEYS_TO_REDACT)), + "state": node_state, } diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index 3fe3bdfeb89..3ac3f32b45a 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -92,7 +92,16 @@ async def test_device_diagnostics( assert len(diagnostics_data["entities"]) == len( list(async_discover_node_values(multisensor_6, device, {device.id: set()})) ) - assert diagnostics_data["state"] == multisensor_6.data + assert diagnostics_data["state"] == { + **multisensor_6.data, + "statistics": { + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "commandsRX": 0, + "commandsTX": 0, + "timeoutResponse": 0, + }, + } async def test_device_diagnostics_error(hass, integration): From fa98b7e136b3af53906c8197825eb1630fc638b1 Mon Sep 17 00:00:00 2001 From: jack5mikemotown <72000916+jack5mikemotown@users.noreply.github.com> Date: Thu, 26 May 2022 16:01:23 -0400 Subject: [PATCH 0935/3516] Fix Google Assistant brightness calculation (#72514) Co-authored-by: Paulus Schoutsen --- homeassistant/components/google_assistant/trait.py | 4 ++-- tests/components/google_assistant/test_google_assistant.py | 2 +- tests/components/google_assistant/test_smart_home.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 20191c61668..42fc43197ea 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -249,7 +249,7 @@ class BrightnessTrait(_Trait): if domain == light.DOMAIN: brightness = self.state.attributes.get(light.ATTR_BRIGHTNESS) if brightness is not None: - response["brightness"] = int(100 * (brightness / 255)) + response["brightness"] = round(100 * (brightness / 255)) else: response["brightness"] = 0 @@ -1948,7 +1948,7 @@ class VolumeTrait(_Trait): level = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) if level is not None: # Convert 0.0-1.0 to 0-100 - response["currentVolume"] = int(level * 100) + response["currentVolume"] = round(level * 100) muted = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_MUTED) if muted is not None: diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 8bf0e5573b2..e8a2603cae3 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -225,7 +225,7 @@ async def test_query_request(hass_fixture, assistant_client, auth_header): assert len(devices) == 4 assert devices["light.bed_light"]["on"] is False assert devices["light.ceiling_lights"]["on"] is True - assert devices["light.ceiling_lights"]["brightness"] == 70 + assert devices["light.ceiling_lights"]["brightness"] == 71 assert devices["light.ceiling_lights"]["color"]["temperatureK"] == 2631 assert devices["light.kitchen_lights"]["color"]["spectrumHsv"] == { "hue": 345, diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index c3bbd9336f4..4b11910999a 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -385,7 +385,7 @@ async def test_query_message(hass): "light.another_light": { "on": True, "online": True, - "brightness": 30, + "brightness": 31, "color": { "spectrumHsv": { "hue": 180, @@ -1510,7 +1510,7 @@ async def test_query_recover(hass, caplog): "payload": { "devices": { "light.bad": {"online": False}, - "light.good": {"on": True, "online": True, "brightness": 19}, + "light.good": {"on": True, "online": True, "brightness": 20}, } }, } From e1ba0717e21e3336179cf2670aa2c5fc1b4b877b Mon Sep 17 00:00:00 2001 From: Marcio Granzotto Rodrigues Date: Thu, 26 May 2022 01:12:43 -0300 Subject: [PATCH 0936/3516] Fix bond device state with v3 firmwares (#72516) --- homeassistant/components/bond/__init__.py | 2 +- homeassistant/components/bond/button.py | 2 +- homeassistant/components/bond/config_flow.py | 2 +- homeassistant/components/bond/cover.py | 2 +- homeassistant/components/bond/entity.py | 10 +++-- homeassistant/components/bond/fan.py | 2 +- homeassistant/components/bond/light.py | 2 +- homeassistant/components/bond/manifest.json | 4 +- homeassistant/components/bond/switch.py | 2 +- homeassistant/components/bond/utils.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bond/common.py | 2 +- tests/components/bond/test_button.py | 2 +- tests/components/bond/test_cover.py | 2 +- tests/components/bond/test_entity.py | 42 +++++++++++++++----- tests/components/bond/test_fan.py | 2 +- tests/components/bond/test_init.py | 2 +- tests/components/bond/test_light.py | 2 +- tests/components/bond/test_switch.py | 2 +- 20 files changed, 59 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 062c1d844c4..557e68272c2 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -5,7 +5,7 @@ import logging from typing import Any from aiohttp import ClientError, ClientResponseError, ClientTimeout -from bond_api import Bond, BPUPSubscriptions, start_bpup +from bond_async import Bond, BPUPSubscriptions, start_bpup from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( diff --git a/homeassistant/components/bond/button.py b/homeassistant/components/bond/button.py index 0152bedde23..0465e4c51fe 100644 --- a/homeassistant/components/bond/button.py +++ b/homeassistant/components/bond/button.py @@ -5,7 +5,7 @@ from dataclasses import dataclass import logging from typing import Any -from bond_api import Action, BPUPSubscriptions +from bond_async import Action, BPUPSubscriptions from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index d3a7b4adf72..6eba9897468 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -6,7 +6,7 @@ import logging from typing import Any from aiohttp import ClientConnectionError, ClientResponseError -from bond_api import Bond +from bond_async import Bond import voluptuous as vol from homeassistant import config_entries, exceptions diff --git a/homeassistant/components/bond/cover.py b/homeassistant/components/bond/cover.py index a50f7b93bbb..3938de0d4bd 100644 --- a/homeassistant/components/bond/cover.py +++ b/homeassistant/components/bond/cover.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from bond_api import Action, BPUPSubscriptions, DeviceType +from bond_async import Action, BPUPSubscriptions, DeviceType from homeassistant.components.cover import ( ATTR_POSITION, diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index 583f1cd96f7..832e9b5d464 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -8,7 +8,7 @@ import logging from typing import Any from aiohttp import ClientError -from bond_api import BPUPSubscriptions +from bond_async import BPUPSubscriptions from homeassistant.const import ( ATTR_HW_VERSION, @@ -156,9 +156,13 @@ class BondEntity(Entity): self._apply_state(state) @callback - def _async_bpup_callback(self, state: dict) -> None: + def _async_bpup_callback(self, json_msg: dict) -> None: """Process a state change from BPUP.""" - self._async_state_callback(state) + topic = json_msg["t"] + if topic != f"devices/{self._device_id}/state": + return + + self._async_state_callback(json_msg["b"]) self.async_write_ha_state() async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index 9acc7874657..f2f6b15f923 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -6,7 +6,7 @@ import math from typing import Any from aiohttp.client_exceptions import ClientResponseError -from bond_api import Action, BPUPSubscriptions, DeviceType, Direction +from bond_async import Action, BPUPSubscriptions, DeviceType, Direction import voluptuous as vol from homeassistant.components.fan import ( diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index c0c3fc428b8..55084f37b03 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -5,7 +5,7 @@ import logging from typing import Any from aiohttp.client_exceptions import ClientResponseError -from bond_api import Action, BPUPSubscriptions, DeviceType +from bond_async import Action, BPUPSubscriptions, DeviceType import voluptuous as vol from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index 187602057c0..52e9dd1763f 100644 --- a/homeassistant/components/bond/manifest.json +++ b/homeassistant/components/bond/manifest.json @@ -3,10 +3,10 @@ "name": "Bond", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bond", - "requirements": ["bond-api==0.1.18"], + "requirements": ["bond-async==0.1.20"], "zeroconf": ["_bond._tcp.local."], "codeowners": ["@bdraco", "@prystupa", "@joshs85", "@marciogranzotto"], "quality_scale": "platinum", "iot_class": "local_push", - "loggers": ["bond_api"] + "loggers": ["bond_async"] } diff --git a/homeassistant/components/bond/switch.py b/homeassistant/components/bond/switch.py index 01c224d8307..da0b19dd9ff 100644 --- a/homeassistant/components/bond/switch.py +++ b/homeassistant/components/bond/switch.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from aiohttp.client_exceptions import ClientResponseError -from bond_api import Action, BPUPSubscriptions, DeviceType +from bond_async import Action, BPUPSubscriptions, DeviceType import voluptuous as vol from homeassistant.components.switch import SwitchEntity diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index fc78c5758c1..cba213d9450 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -5,7 +5,7 @@ import logging from typing import Any, cast from aiohttp import ClientResponseError -from bond_api import Action, Bond +from bond_async import Action, Bond from homeassistant.util.async_ import gather_with_concurrency diff --git a/requirements_all.txt b/requirements_all.txt index 054f38972db..2404e4331a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -417,7 +417,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bond -bond-api==0.1.18 +bond-async==0.1.20 # homeassistant.components.bosch_shc boschshcpy==0.2.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f35d8da8ec8..0153a1b3323 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -318,7 +318,7 @@ blebox_uniapi==1.3.3 blinkpy==0.19.0 # homeassistant.components.bond -bond-api==0.1.18 +bond-async==0.1.20 # homeassistant.components.bosch_shc boschshcpy==0.2.30 diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index 9c53c0afb8b..4b45a4016c0 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -8,7 +8,7 @@ from typing import Any from unittest.mock import MagicMock, patch from aiohttp.client_exceptions import ClientResponseError -from bond_api import DeviceType +from bond_async import DeviceType from homeassistant import core from homeassistant.components.bond.const import DOMAIN as BOND_DOMAIN diff --git a/tests/components/bond/test_button.py b/tests/components/bond/test_button.py index ee6e98b8462..4411b25657b 100644 --- a/tests/components/bond/test_button.py +++ b/tests/components/bond/test_button.py @@ -1,6 +1,6 @@ """Tests for the Bond button device.""" -from bond_api import Action, DeviceType +from bond_async import Action, DeviceType from homeassistant import core from homeassistant.components.bond.button import STEP_SIZE diff --git a/tests/components/bond/test_cover.py b/tests/components/bond/test_cover.py index ca467d4a38d..ccb44402a3e 100644 --- a/tests/components/bond/test_cover.py +++ b/tests/components/bond/test_cover.py @@ -1,7 +1,7 @@ """Tests for the Bond cover device.""" from datetime import timedelta -from bond_api import Action, DeviceType +from bond_async import Action, DeviceType from homeassistant import core from homeassistant.components.cover import ( diff --git a/tests/components/bond/test_entity.py b/tests/components/bond/test_entity.py index 122e9c2f04e..9245f4513ed 100644 --- a/tests/components/bond/test_entity.py +++ b/tests/components/bond/test_entity.py @@ -3,7 +3,8 @@ import asyncio from datetime import timedelta from unittest.mock import patch -from bond_api import BPUPSubscriptions, DeviceType +from bond_async import BPUPSubscriptions, DeviceType +from bond_async.bpup import BPUP_ALIVE_TIMEOUT from homeassistant import core from homeassistant.components import fan @@ -44,24 +45,47 @@ async def test_bpup_goes_offline_and_recovers_same_entity(hass: core.HomeAssista bpup_subs.notify( { "s": 200, - "t": "bond/test-device-id/update", + "t": "devices/test-device-id/state", "b": {"power": 1, "speed": 3, "direction": 0}, } ) await hass.async_block_till_done() assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 100 + # Send a message for the wrong device to make sure its ignored + # we should never get this callback + bpup_subs.notify( + { + "s": 200, + "t": "devices/other-device-id/state", + "b": {"power": 1, "speed": 1, "direction": 0}, + } + ) + await hass.async_block_till_done() + assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 100 + + # Test we ignore messages for the wrong topic + bpup_subs.notify( + { + "s": 200, + "t": "devices/test-device-id/other_topic", + "b": {"power": 1, "speed": 1, "direction": 0}, + } + ) + await hass.async_block_till_done() + assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 100 + bpup_subs.notify( { "s": 200, - "t": "bond/test-device-id/update", + "t": "devices/test-device-id/state", "b": {"power": 1, "speed": 1, "direction": 0}, } ) await hass.async_block_till_done() assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 33 - bpup_subs.last_message_time = 0 + bpup_subs.last_message_time = -BPUP_ALIVE_TIMEOUT with patch_bond_device_state(side_effect=asyncio.TimeoutError): async_fire_time_changed(hass, utcnow() + timedelta(seconds=230)) await hass.async_block_till_done() @@ -75,7 +99,7 @@ async def test_bpup_goes_offline_and_recovers_same_entity(hass: core.HomeAssista bpup_subs.notify( { "s": 200, - "t": "bond/test-device-id/update", + "t": "devices/test-device-id/state", "b": {"power": 1, "speed": 2, "direction": 0}, } ) @@ -106,7 +130,7 @@ async def test_bpup_goes_offline_and_recovers_different_entity( bpup_subs.notify( { "s": 200, - "t": "bond/test-device-id/update", + "t": "devices/test-device-id/state", "b": {"power": 1, "speed": 3, "direction": 0}, } ) @@ -116,14 +140,14 @@ async def test_bpup_goes_offline_and_recovers_different_entity( bpup_subs.notify( { "s": 200, - "t": "bond/test-device-id/update", + "t": "devices/test-device-id/state", "b": {"power": 1, "speed": 1, "direction": 0}, } ) await hass.async_block_till_done() assert hass.states.get("fan.name_1").attributes[fan.ATTR_PERCENTAGE] == 33 - bpup_subs.last_message_time = 0 + bpup_subs.last_message_time = -BPUP_ALIVE_TIMEOUT with patch_bond_device_state(side_effect=asyncio.TimeoutError): async_fire_time_changed(hass, utcnow() + timedelta(seconds=230)) await hass.async_block_till_done() @@ -133,7 +157,7 @@ async def test_bpup_goes_offline_and_recovers_different_entity( bpup_subs.notify( { "s": 200, - "t": "bond/not-this-device-id/update", + "t": "devices/not-this-device-id/state", "b": {"power": 1, "speed": 2, "direction": 0}, } ) diff --git a/tests/components/bond/test_fan.py b/tests/components/bond/test_fan.py index 061e94595bf..7c860e68efc 100644 --- a/tests/components/bond/test_fan.py +++ b/tests/components/bond/test_fan.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta from unittest.mock import call -from bond_api import Action, DeviceType, Direction +from bond_async import Action, DeviceType, Direction import pytest from homeassistant import core diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 88615d98122..03eb490b65e 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -3,7 +3,7 @@ import asyncio from unittest.mock import MagicMock, Mock from aiohttp import ClientConnectionError, ClientResponseError -from bond_api import DeviceType +from bond_async import DeviceType import pytest from homeassistant.components.bond.const import DOMAIN diff --git a/tests/components/bond/test_light.py b/tests/components/bond/test_light.py index 6556c25efe2..c7d8f195423 100644 --- a/tests/components/bond/test_light.py +++ b/tests/components/bond/test_light.py @@ -1,7 +1,7 @@ """Tests for the Bond light device.""" from datetime import timedelta -from bond_api import Action, DeviceType +from bond_async import Action, DeviceType import pytest from homeassistant import core diff --git a/tests/components/bond/test_switch.py b/tests/components/bond/test_switch.py index 619eac69e71..b63bad2d431 100644 --- a/tests/components/bond/test_switch.py +++ b/tests/components/bond/test_switch.py @@ -1,7 +1,7 @@ """Tests for the Bond switch device.""" from datetime import timedelta -from bond_api import Action, DeviceType +from bond_async import Action, DeviceType import pytest from homeassistant import core From 3be5a354c0e860d2a9d829007e2b04c3c1e7718f Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 26 May 2022 15:17:44 -0400 Subject: [PATCH 0937/3516] Fix jitter in nzbget uptime sensor (#72518) --- homeassistant/components/nzbget/sensor.py | 30 +++++++++++++---------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 9e5bd6e4ac9..a1097389020 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -1,7 +1,7 @@ """Monitor the NZBGet API.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta import logging from homeassistant.components.sensor import ( @@ -105,15 +105,16 @@ class NZBGetSensor(NZBGetEntity, SensorEntity): description: SensorEntityDescription, ) -> None: """Initialize a new NZBGet sensor.""" - self.entity_description = description - self._attr_unique_id = f"{entry_id}_{description.key}" - super().__init__( coordinator=coordinator, entry_id=entry_id, name=f"{entry_name} {description.name}", ) + self.entity_description = description + self._attr_unique_id = f"{entry_id}_{description.key}" + self._native_value: datetime | None = None + @property def native_value(self): """Return the state of the sensor.""" @@ -122,14 +123,17 @@ class NZBGetSensor(NZBGetEntity, SensorEntity): if value is None: _LOGGER.warning("Unable to locate value for %s", sensor_type) - return None - - if "DownloadRate" in sensor_type and value > 0: + self._native_value = None + elif "DownloadRate" in sensor_type and value > 0: # Convert download rate from Bytes/s to MBytes/s - return round(value / 2**20, 2) + self._native_value = round(value / 2**20, 2) + elif "UpTimeSec" in sensor_type and value > 0: + uptime = utcnow().replace(microsecond=0) - timedelta(seconds=value) + if not isinstance(self._attr_native_value, datetime) or abs( + uptime - self._attr_native_value + ) > timedelta(seconds=5): + self._native_value = uptime + else: + self._native_value = value - if "UpTimeSec" in sensor_type and value > 0: - uptime = utcnow() - timedelta(seconds=value) - return uptime.replace(microsecond=0) - - return value + return self._native_value From e1c39d8c4b4d1bf571ca3dbfec6bf585915abfbe Mon Sep 17 00:00:00 2001 From: j-a-n Date: Thu, 26 May 2022 13:23:49 +0200 Subject: [PATCH 0938/3516] Fix Moehlenhoff Alpha2 set_target_temperature and set_heat_area_mode (#72533) Fix set_target_temperature and set_heat_area_mode --- .../components/moehlenhoff_alpha2/__init__.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/moehlenhoff_alpha2/__init__.py b/homeassistant/components/moehlenhoff_alpha2/__init__.py index 93ddaa781ab..86306a56033 100644 --- a/homeassistant/components/moehlenhoff_alpha2/__init__.py +++ b/homeassistant/components/moehlenhoff_alpha2/__init__.py @@ -98,7 +98,7 @@ class Alpha2BaseCoordinator(DataUpdateCoordinator[dict[str, dict]]): update_data = {"T_TARGET": target_temperature} is_cooling = self.get_cooling() - heat_area_mode = self.data[heat_area_id]["HEATAREA_MODE"] + heat_area_mode = self.data["heat_areas"][heat_area_id]["HEATAREA_MODE"] if heat_area_mode == 1: if is_cooling: update_data["T_COOL_DAY"] = target_temperature @@ -116,7 +116,7 @@ class Alpha2BaseCoordinator(DataUpdateCoordinator[dict[str, dict]]): raise HomeAssistantError( "Failed to set target temperature, communication error with alpha2 base" ) from http_err - self.data[heat_area_id].update(update_data) + self.data["heat_areas"][heat_area_id].update(update_data) for update_callback in self._listeners: update_callback() @@ -141,25 +141,25 @@ class Alpha2BaseCoordinator(DataUpdateCoordinator[dict[str, dict]]): "Failed to set heat area mode, communication error with alpha2 base" ) from http_err - self.data[heat_area_id]["HEATAREA_MODE"] = heat_area_mode + self.data["heat_areas"][heat_area_id]["HEATAREA_MODE"] = heat_area_mode is_cooling = self.get_cooling() if heat_area_mode == 1: if is_cooling: - self.data[heat_area_id]["T_TARGET"] = self.data[heat_area_id][ - "T_COOL_DAY" - ] + self.data["heat_areas"][heat_area_id]["T_TARGET"] = self.data[ + "heat_areas" + ][heat_area_id]["T_COOL_DAY"] else: - self.data[heat_area_id]["T_TARGET"] = self.data[heat_area_id][ - "T_HEAT_DAY" - ] + self.data["heat_areas"][heat_area_id]["T_TARGET"] = self.data[ + "heat_areas" + ][heat_area_id]["T_HEAT_DAY"] elif heat_area_mode == 2: if is_cooling: - self.data[heat_area_id]["T_TARGET"] = self.data[heat_area_id][ - "T_COOL_NIGHT" - ] + self.data["heat_areas"][heat_area_id]["T_TARGET"] = self.data[ + "heat_areas" + ][heat_area_id]["T_COOL_NIGHT"] else: - self.data[heat_area_id]["T_TARGET"] = self.data[heat_area_id][ - "T_HEAT_NIGHT" - ] + self.data["heat_areas"][heat_area_id]["T_TARGET"] = self.data[ + "heat_areas" + ][heat_area_id]["T_HEAT_NIGHT"] for update_callback in self._listeners: update_callback() From a7fc1a4d62141ff32e3214def7f09bc6c39d16ee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 May 2022 13:16:31 -0700 Subject: [PATCH 0939/3516] Bumped version to 2022.6.0b1 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e437c171c50..ada9a6bbd06 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index c9b915b229a..c62390253dc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.0b0 +version = 2022.6.0b1 url = https://www.home-assistant.io/ [options] From 86570fba19892927edd73bdc915da52ae89796ad Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 May 2022 13:57:00 -0700 Subject: [PATCH 0940/3516] Convert media player enqueue to an enum (#72406) --- .../components/bluesound/media_player.py | 14 +------ homeassistant/components/heos/media_player.py | 16 +++++--- .../components/media_player/__init__.py | 37 ++++++++++++++++- .../media_player/reproduce_state.py | 3 +- .../components/media_player/services.yaml | 16 ++++++++ .../components/sonos/media_player.py | 9 ++--- .../components/squeezebox/media_player.py | 16 ++++---- tests/components/media_player/test_init.py | 40 +++++++++++++++++++ .../media_player/test_reproduce_state.py | 4 -- 9 files changed, 119 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 32f74743972..4fe89d84cf1 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -24,10 +24,7 @@ from homeassistant.components.media_player import ( from homeassistant.components.media_player.browse_media import ( async_process_play_media_url, ) -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_ENQUEUE, - MEDIA_TYPE_MUSIC, -) +from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, @@ -1023,11 +1020,7 @@ class BluesoundPlayer(MediaPlayerEntity): return await self.send_bluesound_command(f"Play?seek={float(position)}") async def async_play_media(self, media_type, media_id, **kwargs): - """ - Send the play_media command to the media player. - - If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. - """ + """Send the play_media command to the media player.""" if self.is_grouped and not self.is_master: return @@ -1039,9 +1032,6 @@ class BluesoundPlayer(MediaPlayerEntity): url = f"Play?url={media_id}" - if kwargs.get(ATTR_MEDIA_ENQUEUE): - return await self.send_bluesound_command(url) - return await self.send_bluesound_command(url) async def async_volume_up(self): diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index dabe79afb03..29a9b2b2a18 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -12,6 +12,7 @@ from typing_extensions import ParamSpec from homeassistant.components import media_source from homeassistant.components.media_player import ( + MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerEntityFeature, ) @@ -73,6 +74,14 @@ CONTROL_TO_SUPPORT = { heos_const.CONTROL_PLAY_NEXT: MediaPlayerEntityFeature.NEXT_TRACK, } +HA_HEOS_ENQUEUE_MAP = { + None: heos_const.ADD_QUEUE_REPLACE_AND_PLAY, + MediaPlayerEnqueue.ADD: heos_const.ADD_QUEUE_ADD_TO_END, + MediaPlayerEnqueue.REPLACE: heos_const.ADD_QUEUE_REPLACE_AND_PLAY, + MediaPlayerEnqueue.NEXT: heos_const.ADD_QUEUE_PLAY_NEXT, + MediaPlayerEnqueue.PLAY: heos_const.ADD_QUEUE_PLAY_NOW, +} + _LOGGER = logging.getLogger(__name__) @@ -222,11 +231,8 @@ class HeosMediaPlayer(MediaPlayerEntity): playlist = next((p for p in playlists if p.name == media_id), None) if not playlist: raise ValueError(f"Invalid playlist '{media_id}'") - add_queue_option = ( - heos_const.ADD_QUEUE_ADD_TO_END - if kwargs.get(ATTR_MEDIA_ENQUEUE) - else heos_const.ADD_QUEUE_REPLACE_AND_PLAY - ) + add_queue_option = HA_HEOS_ENQUEUE_MAP.get(kwargs.get(ATTR_MEDIA_ENQUEUE)) + await self._player.add_to_queue(playlist, add_queue_option) return diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index bf006e2bd4e..f71f3fc2a1f 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -147,6 +147,19 @@ ENTITY_IMAGE_CACHE = {CACHE_IMAGES: collections.OrderedDict(), CACHE_MAXSIZE: 16 SCAN_INTERVAL = dt.timedelta(seconds=10) +class MediaPlayerEnqueue(StrEnum): + """Enqueue types for playing media.""" + + # add given media item to end of the queue + ADD = "add" + # play the given media item next, keep queue + NEXT = "next" + # play the given media item now, keep queue + PLAY = "play" + # play the given media item now, clear queue + REPLACE = "replace" + + class MediaPlayerDeviceClass(StrEnum): """Device class for media players.""" @@ -169,7 +182,9 @@ DEVICE_CLASS_RECEIVER = MediaPlayerDeviceClass.RECEIVER.value MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = { vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string, vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, - vol.Optional(ATTR_MEDIA_ENQUEUE): cv.boolean, + vol.Optional(ATTR_MEDIA_ENQUEUE): vol.Any( + cv.boolean, vol.Coerce(MediaPlayerEnqueue) + ), vol.Optional(ATTR_MEDIA_EXTRA, default={}): dict, } @@ -350,10 +365,30 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "async_select_sound_mode", [MediaPlayerEntityFeature.SELECT_SOUND_MODE], ) + + # Remove in Home Assistant 2022.9 + def _rewrite_enqueue(value): + """Rewrite the enqueue value.""" + if ATTR_MEDIA_ENQUEUE not in value: + pass + elif value[ATTR_MEDIA_ENQUEUE] is True: + value[ATTR_MEDIA_ENQUEUE] = MediaPlayerEnqueue.ADD + _LOGGER.warning( + "Playing media with enqueue set to True is deprecated. Use 'add' instead" + ) + elif value[ATTR_MEDIA_ENQUEUE] is False: + value[ATTR_MEDIA_ENQUEUE] = MediaPlayerEnqueue.PLAY + _LOGGER.warning( + "Playing media with enqueue set to False is deprecated. Use 'play' instead" + ) + + return value + component.async_register_entity_service( SERVICE_PLAY_MEDIA, vol.All( cv.make_entity_service_schema(MEDIA_PLAYER_PLAY_MEDIA_SCHEMA), + _rewrite_enqueue, _rename_keys( media_type=ATTR_MEDIA_CONTENT_TYPE, media_id=ATTR_MEDIA_CONTENT_ID, diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index 586ac61b4e1..bdfc0bf3acb 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -27,7 +27,6 @@ from .const import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, - ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_SOUND_MODE, @@ -118,7 +117,7 @@ async def _async_reproduce_states( if features & MediaPlayerEntityFeature.PLAY_MEDIA: await call_service( SERVICE_PLAY_MEDIA, - [ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_ENQUEUE], + [ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID], ) already_playing = True diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 2e8585d0127..b2a8ac40262 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -151,6 +151,22 @@ play_media: selector: text: + enqueue: + name: Enqueue + description: If the content should be played now or be added to the queue. + required: false + selector: + select: + options: + - label: "Play now" + value: "play" + - label: "Play next" + value: "next" + - label: "Add to queue" + value: "add" + - label: "Play now and clear queue" + value: "replace" + select_source: name: Select source description: Send the media player the command to change input source. diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 95834938953..e2a63a86b06 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -18,6 +18,7 @@ import voluptuous as vol from homeassistant.components import media_source, spotify from homeassistant.components.media_player import ( + MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerEntityFeature, async_process_play_media_url, @@ -537,8 +538,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): If media_type is "playlist", media_id should be a Sonos Playlist name. Otherwise, media_id should be a URI. - - If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. """ if spotify.is_spotify_media_type(media_type): media_type = spotify.resolve_spotify_media_type(media_type) @@ -573,7 +572,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): ) if result.shuffle: self.set_shuffle(True) - if kwargs.get(ATTR_MEDIA_ENQUEUE): + if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: plex_plugin.add_to_queue(result.media) else: soco.clear_queue() @@ -583,7 +582,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): share_link = self.coordinator.share_link if share_link.is_share_link(media_id): - if kwargs.get(ATTR_MEDIA_ENQUEUE): + if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: share_link.add_share_link_to_queue(media_id) else: soco.clear_queue() @@ -593,7 +592,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): # If media ID is a relative URL, we serve it from HA. media_id = async_process_play_media_url(self.hass, media_id) - if kwargs.get(ATTR_MEDIA_ENQUEUE): + if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: soco.add_uri_to_queue(media_id) else: soco.play_uri(media_id, force_radio=is_radio) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index bd1f29f4e69..eda742281ee 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( + MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerEntityFeature, ) @@ -469,16 +470,17 @@ class SqueezeBoxEntity(MediaPlayerEntity): await self._player.async_set_power(True) async def async_play_media(self, media_type, media_id, **kwargs): - """ - Send the play_media command to the media player. - - If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the current playlist. - """ - cmd = "play" + """Send the play_media command to the media player.""" index = None - if kwargs.get(ATTR_MEDIA_ENQUEUE): + enqueue: MediaPlayerEnqueue | None = kwargs.get(ATTR_MEDIA_ENQUEUE) + + if enqueue == MediaPlayerEnqueue.ADD: cmd = "add" + elif enqueue == MediaPlayerEnqueue.NEXT: + cmd = "insert" + else: + cmd = "play" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_MUSIC diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index aa5e1b164f4..cb095cbcfe0 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -4,6 +4,8 @@ import base64 from http import HTTPStatus from unittest.mock import patch +import pytest + from homeassistant.components import media_player from homeassistant.components.media_player.browse_media import BrowseMedia from homeassistant.components.websocket_api.const import TYPE_RESULT @@ -251,3 +253,41 @@ async def test_group_members_available_when_off(hass): state = hass.states.get("media_player.bedroom") assert state.state == STATE_OFF assert "group_members" in state.attributes + + +@pytest.mark.parametrize( + "input,expected", + ( + (True, media_player.MediaPlayerEnqueue.ADD), + (False, media_player.MediaPlayerEnqueue.PLAY), + ("play", media_player.MediaPlayerEnqueue.PLAY), + ("next", media_player.MediaPlayerEnqueue.NEXT), + ("add", media_player.MediaPlayerEnqueue.ADD), + ("replace", media_player.MediaPlayerEnqueue.REPLACE), + ), +) +async def test_enqueue_rewrite(hass, input, expected): + """Test that group_members are still available when media_player is off.""" + await async_setup_component( + hass, "media_player", {"media_player": {"platform": "demo"}} + ) + await hass.async_block_till_done() + + # Fake group support for DemoYoutubePlayer + with patch( + "homeassistant.components.demo.media_player.DemoYoutubePlayer.play_media", + ) as mock_play_media: + await hass.services.async_call( + "media_player", + "play_media", + { + "entity_id": "media_player.bedroom", + "media_content_type": "music", + "media_content_id": "1234", + "enqueue": input, + }, + blocking=True, + ) + + assert len(mock_play_media.mock_calls) == 1 + assert mock_play_media.mock_calls[0][2]["enqueue"] == expected diff --git a/tests/components/media_player/test_reproduce_state.py b/tests/components/media_player/test_reproduce_state.py index f1a243337e1..f880130d4bd 100644 --- a/tests/components/media_player/test_reproduce_state.py +++ b/tests/components/media_player/test_reproduce_state.py @@ -6,7 +6,6 @@ from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, - ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_SOUND_MODE, @@ -253,7 +252,6 @@ async def test_play_media(hass): value_1 = "dummy_1" value_2 = "dummy_2" - value_3 = "dummy_3" await async_reproduce_states( hass, @@ -275,7 +273,6 @@ async def test_play_media(hass): { ATTR_MEDIA_CONTENT_TYPE: value_1, ATTR_MEDIA_CONTENT_ID: value_2, - ATTR_MEDIA_ENQUEUE: value_3, }, ) ], @@ -294,5 +291,4 @@ async def test_play_media(hass): "entity_id": ENTITY_1, ATTR_MEDIA_CONTENT_TYPE: value_1, ATTR_MEDIA_CONTENT_ID: value_2, - ATTR_MEDIA_ENQUEUE: value_3, } From d8295e2fad5c0ba9f9a6c8b3ecbd6c0b6481422b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 26 May 2022 19:20:05 -0400 Subject: [PATCH 0941/3516] Add logbook entries for zwave_js events (#72508) * Add logbook entries for zwave_js events * Fix test * Update homeassistant/components/zwave_js/logbook.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/logbook.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/logbook.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/logbook.py Co-authored-by: Martin Hjelmare * black * Remove value updated event Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/__init__.py | 6 +- homeassistant/components/zwave_js/logbook.py | 115 +++++++++++++++ tests/components/zwave_js/test_events.py | 2 +- tests/components/zwave_js/test_logbook.py | 132 ++++++++++++++++++ 4 files changed, 251 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/zwave_js/logbook.py create mode 100644 tests/components/zwave_js/test_logbook.py diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 5c583d8321f..4f5756361c8 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -297,8 +297,8 @@ async def setup_driver( # noqa: C901 if not disc_info.assumed_state: return value_updates_disc_info[disc_info.primary_value.value_id] = disc_info - # If this is the first time we found a value we want to watch for updates, - # return early + # If this is not the first time we found a value we want to watch for updates, + # return early because we only need one listener for all values. if len(value_updates_disc_info) != 1: return # add listener for value updated events @@ -503,7 +503,7 @@ async def setup_driver( # noqa: C901 elif isinstance(notification, PowerLevelNotification): event_data.update( { - ATTR_COMMAND_CLASS_NAME: "Power Level", + ATTR_COMMAND_CLASS_NAME: "Powerlevel", ATTR_TEST_NODE_ID: notification.test_node_id, ATTR_STATUS: notification.status, ATTR_ACKNOWLEDGED_FRAMES: notification.acknowledged_frames, diff --git a/homeassistant/components/zwave_js/logbook.py b/homeassistant/components/zwave_js/logbook.py new file mode 100644 index 00000000000..1fe1ff79ec6 --- /dev/null +++ b/homeassistant/components/zwave_js/logbook.py @@ -0,0 +1,115 @@ +"""Describe Z-Wave JS logbook events.""" +from __future__ import annotations + +from collections.abc import Callable + +from zwave_js_server.const import CommandClass + +from homeassistant.components.logbook.const import ( + LOGBOOK_ENTRY_MESSAGE, + LOGBOOK_ENTRY_NAME, +) +from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.core import Event, HomeAssistant, callback +import homeassistant.helpers.device_registry as dr + +from .const import ( + ATTR_COMMAND_CLASS, + ATTR_COMMAND_CLASS_NAME, + ATTR_DATA_TYPE, + ATTR_DIRECTION, + ATTR_EVENT_LABEL, + ATTR_EVENT_TYPE, + ATTR_LABEL, + ATTR_VALUE, + DOMAIN, + ZWAVE_JS_NOTIFICATION_EVENT, + ZWAVE_JS_VALUE_NOTIFICATION_EVENT, +) + + +@callback +def async_describe_events( + hass: HomeAssistant, + async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None], +) -> None: + """Describe logbook events.""" + dev_reg = dr.async_get(hass) + + @callback + def async_describe_zwave_js_notification_event( + event: Event, + ) -> dict[str, str]: + """Describe Z-Wave JS notification event.""" + device = dev_reg.devices[event.data[ATTR_DEVICE_ID]] + # Z-Wave JS devices always have a name + device_name = device.name_by_user or device.name + assert device_name + + command_class = event.data[ATTR_COMMAND_CLASS] + command_class_name = event.data[ATTR_COMMAND_CLASS_NAME] + + data: dict[str, str] = {LOGBOOK_ENTRY_NAME: device_name} + prefix = f"fired {command_class_name} CC 'notification' event" + + if command_class == CommandClass.NOTIFICATION: + label = event.data[ATTR_LABEL] + event_label = event.data[ATTR_EVENT_LABEL] + return { + **data, + LOGBOOK_ENTRY_MESSAGE: f"{prefix} '{label}': '{event_label}'", + } + + if command_class == CommandClass.ENTRY_CONTROL: + event_type = event.data[ATTR_EVENT_TYPE] + data_type = event.data[ATTR_DATA_TYPE] + return { + **data, + LOGBOOK_ENTRY_MESSAGE: ( + f"{prefix} for event type '{event_type}' with data type " + f"'{data_type}'" + ), + } + + if command_class == CommandClass.SWITCH_MULTILEVEL: + event_type = event.data[ATTR_EVENT_TYPE] + direction = event.data[ATTR_DIRECTION] + return { + **data, + LOGBOOK_ENTRY_MESSAGE: ( + f"{prefix} for event type '{event_type}': '{direction}'" + ), + } + + return {**data, LOGBOOK_ENTRY_MESSAGE: prefix} + + @callback + def async_describe_zwave_js_value_notification_event( + event: Event, + ) -> dict[str, str]: + """Describe Z-Wave JS value notification event.""" + device = dev_reg.devices[event.data[ATTR_DEVICE_ID]] + # Z-Wave JS devices always have a name + device_name = device.name_by_user or device.name + assert device_name + + command_class = event.data[ATTR_COMMAND_CLASS_NAME] + label = event.data[ATTR_LABEL] + value = event.data[ATTR_VALUE] + + return { + LOGBOOK_ENTRY_NAME: device_name, + LOGBOOK_ENTRY_MESSAGE: ( + f"fired {command_class} CC 'value notification' event for '{label}': " + f"'{value}'" + ), + } + + async_describe_event( + DOMAIN, ZWAVE_JS_NOTIFICATION_EVENT, async_describe_zwave_js_notification_event + ) + async_describe_event( + DOMAIN, + ZWAVE_JS_VALUE_NOTIFICATION_EVENT, + async_describe_zwave_js_value_notification_event, + ) diff --git a/tests/components/zwave_js/test_events.py b/tests/components/zwave_js/test_events.py index 72da1fcb915..19f38d4aa57 100644 --- a/tests/components/zwave_js/test_events.py +++ b/tests/components/zwave_js/test_events.py @@ -312,7 +312,7 @@ async def test_power_level_notification(hass, hank_binary_switch, integration, c node.receive_event(event) await hass.async_block_till_done() assert len(events) == 1 - assert events[0].data["command_class_name"] == "Power Level" + assert events[0].data["command_class_name"] == "Powerlevel" assert events[0].data["command_class"] == 115 assert events[0].data["test_node_id"] == 1 assert events[0].data["status"] == 0 diff --git a/tests/components/zwave_js/test_logbook.py b/tests/components/zwave_js/test_logbook.py new file mode 100644 index 00000000000..eb02c1bbdcf --- /dev/null +++ b/tests/components/zwave_js/test_logbook.py @@ -0,0 +1,132 @@ +"""The tests for Z-Wave JS logbook.""" +from zwave_js_server.const import CommandClass + +from homeassistant.components.zwave_js.const import ( + ZWAVE_JS_NOTIFICATION_EVENT, + ZWAVE_JS_VALUE_NOTIFICATION_EVENT, +) +from homeassistant.components.zwave_js.helpers import get_device_id +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component + +from tests.components.logbook.common import MockRow, mock_humanify + + +async def test_humanifying_zwave_js_notification_event( + hass, client, lock_schlage_be469, integration +): + """Test humanifying Z-Wave JS notification events.""" + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_device( + identifiers={get_device_id(client.driver, lock_schlage_be469)} + ) + assert device + + hass.config.components.add("recorder") + assert await async_setup_component(hass, "logbook", {}) + + events = mock_humanify( + hass, + [ + MockRow( + ZWAVE_JS_NOTIFICATION_EVENT, + { + "device_id": device.id, + "command_class": CommandClass.NOTIFICATION.value, + "command_class_name": "Notification", + "label": "label", + "event_label": "event_label", + }, + ), + MockRow( + ZWAVE_JS_NOTIFICATION_EVENT, + { + "device_id": device.id, + "command_class": CommandClass.ENTRY_CONTROL.value, + "command_class_name": "Entry Control", + "event_type": 1, + "data_type": 2, + }, + ), + MockRow( + ZWAVE_JS_NOTIFICATION_EVENT, + { + "device_id": device.id, + "command_class": CommandClass.SWITCH_MULTILEVEL.value, + "command_class_name": "Multilevel Switch", + "event_type": 1, + "direction": "up", + }, + ), + MockRow( + ZWAVE_JS_NOTIFICATION_EVENT, + { + "device_id": device.id, + "command_class": CommandClass.POWERLEVEL.value, + "command_class_name": "Powerlevel", + }, + ), + ], + ) + + assert events[0]["name"] == "Touchscreen Deadbolt" + assert events[0]["domain"] == "zwave_js" + assert ( + events[0]["message"] + == "fired Notification CC 'notification' event 'label': 'event_label'" + ) + + assert events[1]["name"] == "Touchscreen Deadbolt" + assert events[1]["domain"] == "zwave_js" + assert ( + events[1]["message"] + == "fired Entry Control CC 'notification' event for event type '1' with data type '2'" + ) + + assert events[2]["name"] == "Touchscreen Deadbolt" + assert events[2]["domain"] == "zwave_js" + assert ( + events[2]["message"] + == "fired Multilevel Switch CC 'notification' event for event type '1': 'up'" + ) + + assert events[3]["name"] == "Touchscreen Deadbolt" + assert events[3]["domain"] == "zwave_js" + assert events[3]["message"] == "fired Powerlevel CC 'notification' event" + + +async def test_humanifying_zwave_js_value_notification_event( + hass, client, lock_schlage_be469, integration +): + """Test humanifying Z-Wave JS value notification events.""" + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_device( + identifiers={get_device_id(client.driver, lock_schlage_be469)} + ) + assert device + + hass.config.components.add("recorder") + assert await async_setup_component(hass, "logbook", {}) + + events = mock_humanify( + hass, + [ + MockRow( + ZWAVE_JS_VALUE_NOTIFICATION_EVENT, + { + "device_id": device.id, + "command_class": CommandClass.SCENE_ACTIVATION.value, + "command_class_name": "Scene Activation", + "label": "Scene ID", + "value": "001", + }, + ), + ], + ) + + assert events[0]["name"] == "Touchscreen Deadbolt" + assert events[0]["domain"] == "zwave_js" + assert ( + events[0]["message"] + == "fired Scene Activation CC 'value notification' event for 'Scene ID': '001'" + ) From 5e52b11050c04d94b02286d260075dfd78d19d98 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 26 May 2022 19:21:50 -0400 Subject: [PATCH 0942/3516] Add additional data to zwave_js device statistics WS API (#72520) * Add additional data to zwave_js device statistics WS API * Rename variables * fix logic * correct typehint * Update homeassistant/components/zwave_js/api.py Co-authored-by: Martin Hjelmare * black Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/api.py | 36 +++++++++-- tests/components/zwave_js/test_api.py | 80 ++++++++++++++++++++---- 2 files changed, 100 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index c2e03981686..e922571f4b1 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -71,6 +71,7 @@ from .const import ( from .helpers import ( async_enable_statistics, async_get_node_from_device_id, + get_device_id, update_data_collection_preference, ) @@ -2107,15 +2108,42 @@ async def websocket_subscribe_controller_statistics( ) -def _get_node_statistics_dict(statistics: NodeStatistics) -> dict[str, int]: +def _get_node_statistics_dict( + hass: HomeAssistant, statistics: NodeStatistics +) -> dict[str, Any]: """Get dictionary of node statistics.""" - return { + dev_reg = dr.async_get(hass) + + def _convert_node_to_device_id(node: Node) -> str: + """Convert a node to a device id.""" + driver = node.client.driver + assert driver + device = dev_reg.async_get_device({get_device_id(driver, node)}) + assert device + return device.id + + data: dict = { "commands_tx": statistics.commands_tx, "commands_rx": statistics.commands_rx, "commands_dropped_tx": statistics.commands_dropped_tx, "commands_dropped_rx": statistics.commands_dropped_rx, "timeout_response": statistics.timeout_response, + "rtt": statistics.rtt, + "rssi": statistics.rssi, + "lwr": statistics.lwr.as_dict() if statistics.lwr else None, + "nlwr": statistics.nlwr.as_dict() if statistics.nlwr else None, } + for key in ("lwr", "nlwr"): + if not data[key]: + continue + for key_2 in ("repeaters", "route_failed_between"): + if not data[key][key_2]: + continue + data[key][key_2] = [ + _convert_node_to_device_id(node) for node in data[key][key_2] + ] + + return data @websocket_api.require_admin @@ -2151,7 +2179,7 @@ async def websocket_subscribe_node_statistics( "event": event["event"], "source": "node", "node_id": node.node_id, - **_get_node_statistics_dict(statistics), + **_get_node_statistics_dict(hass, statistics), }, ) ) @@ -2167,7 +2195,7 @@ async def websocket_subscribe_node_statistics( "event": "statistics updated", "source": "node", "nodeId": node.node_id, - **_get_node_statistics_dict(node.statistics), + **_get_node_statistics_dict(hass, node.statistics), }, ) ) diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index e59a923ff44..7f64ed6d87d 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -3768,18 +3768,26 @@ async def test_subscribe_controller_statistics( async def test_subscribe_node_statistics( - hass, multisensor_6, integration, client, hass_ws_client + hass, + multisensor_6, + wallmote_central_scene, + zen_31, + integration, + client, + hass_ws_client, ): """Test the subscribe_node_statistics command.""" entry = integration ws_client = await hass_ws_client(hass) - device = get_device(hass, multisensor_6) + multisensor_6_device = get_device(hass, multisensor_6) + zen_31_device = get_device(hass, zen_31) + wallmote_central_scene_device = get_device(hass, wallmote_central_scene) await ws_client.send_json( { ID: 1, TYPE: "zwave_js/subscribe_node_statistics", - DEVICE_ID: device.id, + DEVICE_ID: multisensor_6_device.id, } ) @@ -3797,6 +3805,10 @@ async def test_subscribe_node_statistics( "commands_dropped_tx": 0, "commands_dropped_rx": 0, "timeout_response": 0, + "rtt": None, + "rssi": None, + "lwr": None, + "nlwr": None, } # Fire statistics updated @@ -3808,10 +3820,32 @@ async def test_subscribe_node_statistics( "nodeId": multisensor_6.node_id, "statistics": { "commandsTX": 1, - "commandsRX": 1, - "commandsDroppedTX": 1, - "commandsDroppedRX": 1, - "timeoutResponse": 1, + "commandsRX": 2, + "commandsDroppedTX": 3, + "commandsDroppedRX": 4, + "timeoutResponse": 5, + "rtt": 6, + "rssi": 7, + "lwr": { + "protocolDataRate": 1, + "rssi": 1, + "repeaters": [wallmote_central_scene.node_id], + "repeaterRSSI": [1], + "routeFailedBetween": [ + zen_31.node_id, + multisensor_6.node_id, + ], + }, + "nlwr": { + "protocolDataRate": 2, + "rssi": 2, + "repeaters": [], + "repeaterRSSI": [127], + "routeFailedBetween": [ + multisensor_6.node_id, + zen_31.node_id, + ], + }, }, }, ) @@ -3822,10 +3856,32 @@ async def test_subscribe_node_statistics( "source": "node", "node_id": multisensor_6.node_id, "commands_tx": 1, - "commands_rx": 1, - "commands_dropped_tx": 1, - "commands_dropped_rx": 1, - "timeout_response": 1, + "commands_rx": 2, + "commands_dropped_tx": 3, + "commands_dropped_rx": 4, + "timeout_response": 5, + "rtt": 6, + "rssi": 7, + "lwr": { + "protocol_data_rate": 1, + "rssi": 1, + "repeaters": [wallmote_central_scene_device.id], + "repeater_rssi": [1], + "route_failed_between": [ + zen_31_device.id, + multisensor_6_device.id, + ], + }, + "nlwr": { + "protocol_data_rate": 2, + "rssi": 2, + "repeaters": [], + "repeater_rssi": [127], + "route_failed_between": [ + multisensor_6_device.id, + zen_31_device.id, + ], + }, } # Test sending command with improper entry ID fails @@ -3849,7 +3905,7 @@ async def test_subscribe_node_statistics( { ID: 4, TYPE: "zwave_js/subscribe_node_statistics", - DEVICE_ID: device.id, + DEVICE_ID: multisensor_6_device.id, } ) msg = await ws_client.receive_json() From 26d7c3cff81dcf5c88c1fa10d6688e9073af8d0d Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 27 May 2022 00:24:01 +0000 Subject: [PATCH 0943/3516] [ci skip] Translation update --- .../alarm_control_panel/translations/sv.json | 3 +++ .../components/generic/translations/ca.json | 2 ++ .../components/generic/translations/id.json | 2 ++ .../components/generic/translations/ja.json | 2 ++ .../components/generic/translations/tr.json | 2 ++ .../components/ialarm_xr/translations/ca.json | 21 +++++++++++++++++++ .../components/ialarm_xr/translations/et.json | 21 +++++++++++++++++++ .../components/ialarm_xr/translations/id.json | 21 +++++++++++++++++++ .../components/ialarm_xr/translations/ja.json | 21 +++++++++++++++++++ .../components/ialarm_xr/translations/nl.json | 21 +++++++++++++++++++ .../components/ialarm_xr/translations/tr.json | 21 +++++++++++++++++++ .../totalconnect/translations/ca.json | 7 +++++++ .../totalconnect/translations/et.json | 11 ++++++++++ .../totalconnect/translations/id.json | 10 +++++++++ .../totalconnect/translations/ja.json | 11 ++++++++++ .../totalconnect/translations/nl.json | 7 +++++++ .../totalconnect/translations/tr.json | 11 ++++++++++ 17 files changed, 194 insertions(+) create mode 100644 homeassistant/components/ialarm_xr/translations/ca.json create mode 100644 homeassistant/components/ialarm_xr/translations/et.json create mode 100644 homeassistant/components/ialarm_xr/translations/id.json create mode 100644 homeassistant/components/ialarm_xr/translations/ja.json create mode 100644 homeassistant/components/ialarm_xr/translations/nl.json create mode 100644 homeassistant/components/ialarm_xr/translations/tr.json diff --git a/homeassistant/components/alarm_control_panel/translations/sv.json b/homeassistant/components/alarm_control_panel/translations/sv.json index 1f375eb5f1d..cff9e0cbc52 100644 --- a/homeassistant/components/alarm_control_panel/translations/sv.json +++ b/homeassistant/components/alarm_control_panel/translations/sv.json @@ -7,6 +7,9 @@ "disarm": "Avlarma {entity_name}", "trigger": "Utl\u00f6sare {entity_name}" }, + "condition_type": { + "is_triggered": "har utl\u00f6sts" + }, "trigger_type": { "armed_away": "{entity_name} larmad borta", "armed_home": "{entity_name} larmad hemma", diff --git a/homeassistant/components/generic/translations/ca.json b/homeassistant/components/generic/translations/ca.json index 3f2cd6afa47..818a030c8a7 100644 --- a/homeassistant/components/generic/translations/ca.json +++ b/homeassistant/components/generic/translations/ca.json @@ -15,6 +15,7 @@ "stream_no_video": "El flux no cont\u00e9 v\u00eddeo", "stream_not_permitted": "Operaci\u00f3 no permesa mentre s'intentava connectar al flux de dades. Protocol de transport RTSP incorrecte?", "stream_unauthorised": "L'autoritzaci\u00f3 ha fallat mentre s'intentava connectar amb el flux de dades", + "template_error": "Error renderitzant plantilla. Consulta els registres per m\u00e9s informaci\u00f3.", "timeout": "El temps m\u00e0xim de c\u00e0rrega de l'URL ha expirat", "unable_still_load": "No s'ha pogut carregar cap imatge v\u00e0lida des de l'URL d'imatge fixa (pot ser per un amfitri\u00f3 o URL inv\u00e0lid o un error d'autenticaci\u00f3). Revisa els registres per a m\u00e9s informaci\u00f3.", "unknown": "Error inesperat" @@ -57,6 +58,7 @@ "stream_no_video": "El flux no cont\u00e9 v\u00eddeo", "stream_not_permitted": "Operaci\u00f3 no permesa mentre s'intentava connectar al flux de dades. Protocol de transport RTSP incorrecte?", "stream_unauthorised": "L'autoritzaci\u00f3 ha fallat mentre s'intentava connectar amb el flux de dades", + "template_error": "Error renderitzant plantilla. Consulta els registres per m\u00e9s informaci\u00f3.", "timeout": "El temps m\u00e0xim de c\u00e0rrega de l'URL ha expirat", "unable_still_load": "No s'ha pogut carregar cap imatge v\u00e0lida des de l'URL d'imatge fixa (pot ser per un amfitri\u00f3 o URL inv\u00e0lid o un error d'autenticaci\u00f3). Revisa els registres per a m\u00e9s informaci\u00f3.", "unknown": "Error inesperat" diff --git a/homeassistant/components/generic/translations/id.json b/homeassistant/components/generic/translations/id.json index 95c6d6e2ae2..a9c553580ca 100644 --- a/homeassistant/components/generic/translations/id.json +++ b/homeassistant/components/generic/translations/id.json @@ -15,6 +15,7 @@ "stream_no_video": "Streaming tidak memiliki video", "stream_not_permitted": "Operasi tidak diizinkan saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", "stream_unauthorised": "Otorisasi gagal saat mencoba menyambung ke streaming", + "template_error": "Kesalahan saat merender templat. Tinjau log untuk info lebih lanjut.", "timeout": "Tenggang waktu habis saat memuat URL", "unable_still_load": "Tidak dapat memuat gambar yang valid dari URL gambar diam (mis. host yang tidak valid, URL, atau kegagalan autentikasi). Tinjau log untuk info lebih lanjut.", "unknown": "Kesalahan yang tidak diharapkan" @@ -57,6 +58,7 @@ "stream_no_video": "Streaming tidak memiliki video", "stream_not_permitted": "Operasi tidak diizinkan saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", "stream_unauthorised": "Otorisasi gagal saat mencoba menyambung ke streaming", + "template_error": "Kesalahan saat merender templat. Tinjau log untuk info lebih lanjut.", "timeout": "Tenggang waktu habis saat memuat URL", "unable_still_load": "Tidak dapat memuat gambar yang valid dari URL gambar diam (mis. host yang tidak valid, URL, atau kegagalan autentikasi). Tinjau log untuk info lebih lanjut.", "unknown": "Kesalahan yang tidak diharapkan" diff --git a/homeassistant/components/generic/translations/ja.json b/homeassistant/components/generic/translations/ja.json index a106cbf4cdc..f4fd7d8ec46 100644 --- a/homeassistant/components/generic/translations/ja.json +++ b/homeassistant/components/generic/translations/ja.json @@ -15,6 +15,7 @@ "stream_no_video": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u52d5\u753b\u304c\u3042\u308a\u307e\u305b\u3093", "stream_not_permitted": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u9593\u3001\u64cd\u4f5c\u3067\u304d\u307e\u305b\u3093\u3002RTSP\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30d7\u30ed\u30c8\u30b3\u30eb\u3092\u9593\u9055\u3048\u305f\uff1f", "stream_unauthorised": "\u30b9\u30c8\u30ea\u30fc\u30e0\u3078\u306e\u63a5\u7d9a\u6642\u306b\u3001\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "template_error": "\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8\u306e\u30ec\u30f3\u30c0\u30ea\u30f3\u30b0\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "timeout": "URL\u306e\u8aad\u307f\u8fbc\u307f\u4e2d\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", "unable_still_load": "\u9759\u6b62\u753b\u306eURL\u304b\u3089\u6709\u52b9\u306a\u753b\u50cf\u3092\u8aad\u307f\u8fbc\u3080\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\uff08\u4f8b: \u7121\u52b9\u306a\u30db\u30b9\u30c8\u3001URL\u3001\u307e\u305f\u306f\u8a8d\u8a3c\u5931\u6557)\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" @@ -57,6 +58,7 @@ "stream_no_video": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u52d5\u753b\u304c\u3042\u308a\u307e\u305b\u3093", "stream_not_permitted": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u9593\u3001\u64cd\u4f5c\u3067\u304d\u307e\u305b\u3093\u3002RTSP\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30d7\u30ed\u30c8\u30b3\u30eb\u3092\u9593\u9055\u3048\u305f\uff1f", "stream_unauthorised": "\u30b9\u30c8\u30ea\u30fc\u30e0\u3078\u306e\u63a5\u7d9a\u6642\u306b\u3001\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "template_error": "\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8\u306e\u30ec\u30f3\u30c0\u30ea\u30f3\u30b0\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "timeout": "URL\u306e\u8aad\u307f\u8fbc\u307f\u4e2d\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", "unable_still_load": "\u9759\u6b62\u753b\u306eURL\u304b\u3089\u6709\u52b9\u306a\u753b\u50cf\u3092\u8aad\u307f\u8fbc\u3080\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\uff08\u4f8b: \u7121\u52b9\u306a\u30db\u30b9\u30c8\u3001URL\u3001\u307e\u305f\u306f\u8a8d\u8a3c\u5931\u6557)\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/generic/translations/tr.json b/homeassistant/components/generic/translations/tr.json index 7b6ab65f948..d439c559aa5 100644 --- a/homeassistant/components/generic/translations/tr.json +++ b/homeassistant/components/generic/translations/tr.json @@ -15,6 +15,7 @@ "stream_no_video": "Ak\u0131\u015fta video yok", "stream_not_permitted": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken i\u015fleme izin verilmiyor. Yanl\u0131\u015f RTSP aktar\u0131m protokol\u00fc?", "stream_unauthorised": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken yetkilendirme ba\u015far\u0131s\u0131z oldu", + "template_error": "\u015eablon olu\u015fturma hatas\u0131. Daha fazla bilgi i\u00e7in g\u00fcnl\u00fc\u011f\u00fc inceleyin.", "timeout": "URL y\u00fcklenirken zaman a\u015f\u0131m\u0131", "unable_still_load": "Hareketsiz resim URL'sinden ge\u00e7erli resim y\u00fcklenemiyor (\u00f6r. ge\u00e7ersiz ana bilgisayar, URL veya kimlik do\u011frulama hatas\u0131). Daha fazla bilgi i\u00e7in g\u00fcnl\u00fc\u011f\u00fc inceleyin.", "unknown": "Beklenmeyen hata" @@ -57,6 +58,7 @@ "stream_no_video": "Ak\u0131\u015fta video yok", "stream_not_permitted": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken i\u015fleme izin verilmiyor. Yanl\u0131\u015f RTSP aktar\u0131m protokol\u00fc?", "stream_unauthorised": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken yetkilendirme ba\u015far\u0131s\u0131z oldu", + "template_error": "\u015eablon olu\u015fturma hatas\u0131. Daha fazla bilgi i\u00e7in g\u00fcnl\u00fc\u011f\u00fc inceleyin.", "timeout": "URL y\u00fcklenirken zaman a\u015f\u0131m\u0131", "unable_still_load": "Hareketsiz resim URL'sinden ge\u00e7erli resim y\u00fcklenemiyor (\u00f6r. ge\u00e7ersiz ana bilgisayar, URL veya kimlik do\u011frulama hatas\u0131). Daha fazla bilgi i\u00e7in g\u00fcnl\u00fc\u011f\u00fc inceleyin.", "unknown": "Beklenmeyen hata" diff --git a/homeassistant/components/ialarm_xr/translations/ca.json b/homeassistant/components/ialarm_xr/translations/ca.json new file mode 100644 index 00000000000..6c5ca634ccc --- /dev/null +++ b/homeassistant/components/ialarm_xr/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/et.json b/homeassistant/components/ialarm_xr/translations/et.json new file mode 100644 index 00000000000..97fc5aa5a29 --- /dev/null +++ b/homeassistant/components/ialarm_xr/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na", + "port": "Port", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/id.json b/homeassistant/components/ialarm_xr/translations/id.json new file mode 100644 index 00000000000..558b7de6b24 --- /dev/null +++ b/homeassistant/components/ialarm_xr/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "port": "Port", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/ja.json b/homeassistant/components/ialarm_xr/translations/ja.json new file mode 100644 index 00000000000..65aac61f40f --- /dev/null +++ b/homeassistant/components/ialarm_xr/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/nl.json b/homeassistant/components/ialarm_xr/translations/nl.json new file mode 100644 index 00000000000..3ec5fe68d61 --- /dev/null +++ b/homeassistant/components/ialarm_xr/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "port": "Poort", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/tr.json b/homeassistant/components/ialarm_xr/translations/tr.json new file mode 100644 index 00000000000..f15e4339e3c --- /dev/null +++ b/homeassistant/components/ialarm_xr/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Sunucu", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/ca.json b/homeassistant/components/totalconnect/translations/ca.json index 404e07d6b69..0c1a540b8ac 100644 --- a/homeassistant/components/totalconnect/translations/ca.json +++ b/homeassistant/components/totalconnect/translations/ca.json @@ -28,5 +28,12 @@ } } } + }, + "options": { + "step": { + "init": { + "title": "Opcions de TotalConnect" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/et.json b/homeassistant/components/totalconnect/translations/et.json index c4bca75a558..192476efa72 100644 --- a/homeassistant/components/totalconnect/translations/et.json +++ b/homeassistant/components/totalconnect/translations/et.json @@ -28,5 +28,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "Aku t\u00fchjenemise automaatne eiramine" + }, + "description": "Eira tsoone automaatselt kui nad teatavad t\u00fchjast akust.", + "title": "TotalConnecti valikud" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/id.json b/homeassistant/components/totalconnect/translations/id.json index 1702ceb5688..08bce345e75 100644 --- a/homeassistant/components/totalconnect/translations/id.json +++ b/homeassistant/components/totalconnect/translations/id.json @@ -28,5 +28,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "Otomatis dilewatkan saat baterai lemah" + }, + "title": "Opsi TotalConnect" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/ja.json b/homeassistant/components/totalconnect/translations/ja.json index c20b55d5583..1e7750b2442 100644 --- a/homeassistant/components/totalconnect/translations/ja.json +++ b/homeassistant/components/totalconnect/translations/ja.json @@ -28,5 +28,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "\u30d0\u30c3\u30c6\u30ea\u30fc\u6b8b\u91cf(\u4f4e)\u3067\u306e\u81ea\u52d5\u30d0\u30a4\u30d1\u30b9" + }, + "description": "\u30d0\u30c3\u30c6\u30ea\u30fc\u6b8b\u91cf\u304c\u5c11\u306a\u3044\u3068\u5831\u544a\u3055\u308c\u305f\u77ac\u9593\u306b\u3001\u81ea\u52d5\u7684\u306b\u30be\u30fc\u30f3\u3092\u30d0\u30a4\u30d1\u30b9\u3057\u307e\u3059\u3002", + "title": "TotalConnect\u30aa\u30d7\u30b7\u30e7\u30f3" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/nl.json b/homeassistant/components/totalconnect/translations/nl.json index cf9a6bb10a1..aaf06b0b70f 100644 --- a/homeassistant/components/totalconnect/translations/nl.json +++ b/homeassistant/components/totalconnect/translations/nl.json @@ -28,5 +28,12 @@ } } } + }, + "options": { + "step": { + "init": { + "title": "TotalConnect-opties" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/tr.json b/homeassistant/components/totalconnect/translations/tr.json index ef50457f846..f40eba3d429 100644 --- a/homeassistant/components/totalconnect/translations/tr.json +++ b/homeassistant/components/totalconnect/translations/tr.json @@ -28,5 +28,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "D\u00fc\u015f\u00fck pili otomatik atla" + }, + "description": "D\u00fc\u015f\u00fck pil bildirdikleri anda b\u00f6lgeleri otomatik olarak atlay\u0131n.", + "title": "Toplam Ba\u011flant\u0131 Se\u00e7enekleri" + } + } } } \ No newline at end of file From 93f0945772e1bf407001e7c4d899c41d2708d63f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 27 May 2022 05:48:52 +0200 Subject: [PATCH 0944/3516] Update frontend to 20220526.0 (#72567) --- 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 6c8f568c4d2..48488bc8f47 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220525.0"], + "requirements": ["home-assistant-frontend==20220526.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5a6dc9891a6..309d4b89e9f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220525.0 +home-assistant-frontend==20220526.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 2404e4331a1..c7459285fa0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -822,7 +822,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220525.0 +home-assistant-frontend==20220526.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0153a1b3323..7233824a3cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -589,7 +589,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220525.0 +home-assistant-frontend==20220526.0 # homeassistant.components.home_connect homeconnect==0.7.0 From 465210784fcf945e3fea7e7b5c801f935edc0f79 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 27 May 2022 05:51:24 +0200 Subject: [PATCH 0945/3516] fjaraskupan: Don't set hardware filters for service id (#72569) --- homeassistant/components/fjaraskupan/__init__.py | 4 ++-- homeassistant/components/fjaraskupan/config_flow.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 488139b080b..ec4528bc079 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 UUID_SERVICE, Device, State, device_filter +from fjaraskupan import DEVICE_NAME, Device, State, device_filter from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -90,7 +90,7 @@ class EntryState: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Fjäråskupan from a config entry.""" - scanner = BleakScanner(filters={"UUIDs": [str(UUID_SERVICE)]}) + scanner = BleakScanner(filters={"Pattern": DEVICE_NAME, "DuplicateData": True}) state = EntryState(scanner, {}) hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/fjaraskupan/config_flow.py b/homeassistant/components/fjaraskupan/config_flow.py index da0a7f1dd2b..3af34c0eef6 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 UUID_SERVICE, device_filter +from fjaraskupan import DEVICE_NAME, device_filter from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_flow import register_discovery_flow @@ -27,7 +27,8 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: event.set() async with BleakScanner( - detection_callback=detection, filters={"UUIDs": [str(UUID_SERVICE)]} + detection_callback=detection, + filters={"Pattern": DEVICE_NAME, "DuplicateData": True}, ): try: async with async_timeout.timeout(CONST_WAIT_TIME): From 049c06061ce92834b0c82b0e8b06ae7520322e54 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 May 2022 17:54:26 -1000 Subject: [PATCH 0946/3516] Fix memory leak when firing state_changed events (#72571) --- homeassistant/components/recorder/models.py | 2 +- homeassistant/core.py | 45 +++++++++++++++++---- tests/test_core.py | 45 +++++++++++++++++++++ 3 files changed, 84 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index dff8edde79f..70c816c2af5 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -746,7 +746,7 @@ class LazyState(State): def context(self) -> Context: # type: ignore[override] """State context.""" if self._context is None: - self._context = Context(id=None) # type: ignore[arg-type] + self._context = Context(id=None) return self._context @context.setter diff --git a/homeassistant/core.py b/homeassistant/core.py index 2753b801347..d7cae4e411e 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -37,7 +37,6 @@ from typing import ( ) from urllib.parse import urlparse -import attr import voluptuous as vol import yarl @@ -716,14 +715,26 @@ class HomeAssistant: self._stopped.set() -@attr.s(slots=True, frozen=False) class Context: """The context that triggered something.""" - user_id: str | None = attr.ib(default=None) - parent_id: str | None = attr.ib(default=None) - id: str = attr.ib(factory=ulid_util.ulid) - origin_event: Event | None = attr.ib(default=None, eq=False) + __slots__ = ("user_id", "parent_id", "id", "origin_event") + + def __init__( + self, + user_id: str | None = None, + parent_id: str | None = None, + id: str | None = None, # pylint: disable=redefined-builtin + ) -> None: + """Init the context.""" + self.id = id or ulid_util.ulid() + self.user_id = user_id + self.parent_id = parent_id + self.origin_event: Event | None = None + + def __eq__(self, other: Any) -> bool: + """Compare contexts.""" + return bool(self.__class__ == other.__class__ and self.id == other.id) def as_dict(self) -> dict[str, str | None]: """Return a dictionary representation of the context.""" @@ -1163,6 +1174,24 @@ class State: context, ) + def expire(self) -> None: + """Mark the state as old. + + We give up the original reference to the context to ensure + the context can be garbage collected by replacing it with + a new one with the same id to ensure the old state + can still be examined for comparison against the new state. + + Since we are always going to fire a EVENT_STATE_CHANGED event + after we remove a state from the state machine we need to make + sure we don't end up holding a reference to the original context + since it can never be garbage collected as each event would + reference the previous one. + """ + self.context = Context( + self.context.user_id, self.context.parent_id, self.context.id + ) + def __eq__(self, other: Any) -> bool: """Return the comparison of the state.""" return ( # type: ignore[no-any-return] @@ -1303,6 +1332,7 @@ class StateMachine: if old_state is None: return False + old_state.expire() self._bus.async_fire( EVENT_STATE_CHANGED, {"entity_id": entity_id, "old_state": old_state, "new_state": None}, @@ -1396,7 +1426,6 @@ class StateMachine: if context is None: context = Context(id=ulid_util.ulid(dt_util.utc_to_timestamp(now))) - state = State( entity_id, new_state, @@ -1406,6 +1435,8 @@ class StateMachine: context, old_state is None, ) + if old_state is not None: + old_state.expire() self._states[entity_id] = state self._bus.async_fire( EVENT_STATE_CHANGED, diff --git a/tests/test_core.py b/tests/test_core.py index ee1005a60b0..67513ea8b17 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -6,9 +6,11 @@ import array import asyncio from datetime import datetime, timedelta import functools +import gc import logging import os from tempfile import TemporaryDirectory +from typing import Any from unittest.mock import MagicMock, Mock, PropertyMock, patch import pytest @@ -1829,3 +1831,46 @@ async def test_event_context(hass): cancel2() assert dummy_event2.context.origin_event == dummy_event + + +def _get_full_name(obj) -> str: + """Get the full name of an object in memory.""" + objtype = type(obj) + name = objtype.__name__ + if module := getattr(objtype, "__module__", None): + return f"{module}.{name}" + return name + + +def _get_by_type(full_name: str) -> list[Any]: + """Get all objects in memory with a specific type.""" + return [obj for obj in gc.get_objects() if _get_full_name(obj) == full_name] + + +# The logger will hold a strong reference to the event for the life of the tests +# so we must patch it out +@pytest.mark.skipif( + not os.environ.get("DEBUG_MEMORY"), + reason="Takes too long on the CI", +) +@patch.object(ha._LOGGER, "debug", lambda *args: None) +async def test_state_changed_events_to_not_leak_contexts(hass): + """Test state changed events do not leak contexts.""" + gc.collect() + # Other tests can log Contexts which keep them in memory + # so we need to look at how many exist at the start + init_count = len(_get_by_type("homeassistant.core.Context")) + + assert len(_get_by_type("homeassistant.core.Context")) == init_count + for i in range(20): + hass.states.async_set("light.switch", str(i)) + await hass.async_block_till_done() + gc.collect() + + assert len(_get_by_type("homeassistant.core.Context")) == init_count + 2 + + hass.states.async_remove("light.switch") + await hass.async_block_till_done() + gc.collect() + + assert len(_get_by_type("homeassistant.core.Context")) == init_count From a526b2b819919bc872382f95a77250894e22c174 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 May 2022 18:15:56 -1000 Subject: [PATCH 0947/3516] Add support for async_remove_config_entry_device to bond (#72511) --- homeassistant/components/bond/__init__.py | 21 ++++++++ tests/components/bond/common.py | 23 ++++++++ tests/components/bond/test_fan.py | 10 +--- tests/components/bond/test_init.py | 66 ++++++++++++++++++++++- 4 files changed, 110 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 557e68272c2..476423631c3 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -119,3 +119,24 @@ def _async_remove_old_device_identifiers( continue if config_entry_id in dev.config_entries: device_registry.async_remove_device(dev.id) + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove bond config entry from a device.""" + hub: BondHub = hass.data[DOMAIN][config_entry.entry_id][HUB] + for identifier in device_entry.identifiers: + if identifier[0] != DOMAIN or len(identifier) != 3: + continue + bond_id: str = identifier[1] + # Bond still uses the 3 arg tuple before + # the identifiers were typed + device_id: str = identifier[2] # type: ignore[misc] + # If device_id is no longer present on + # the hub, we allow removal. + if hub.bond_id != bond_id or not any( + device_id == device.device_id for device in hub.devices + ): + return True + return False diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index 4b45a4016c0..c5a649ab30a 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -19,6 +19,20 @@ from homeassistant.util import utcnow from tests.common import MockConfigEntry, async_fire_time_changed +async def remove_device(ws_client, device_id, config_entry_id): + """Remove config entry from a device.""" + await ws_client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry_id, + "device_id": device_id, + } + ) + response = await ws_client.receive_json() + return response["success"] + + def ceiling_fan_with_breeze(name: str): """Create a ceiling fan with given name with breeze support.""" return { @@ -246,3 +260,12 @@ async def help_test_entity_available( async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) await hass.async_block_till_done() assert hass.states.get(entity_id).state != STATE_UNAVAILABLE + + +def ceiling_fan(name: str): + """Create a ceiling fan with given name.""" + return { + "name": name, + "type": DeviceType.CEILING_FAN, + "actions": ["SetSpeed", "SetDirection"], + } diff --git a/tests/components/bond/test_fan.py b/tests/components/bond/test_fan.py index 7c860e68efc..305c131125f 100644 --- a/tests/components/bond/test_fan.py +++ b/tests/components/bond/test_fan.py @@ -33,6 +33,7 @@ from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.util import utcnow from .common import ( + ceiling_fan, help_test_entity_available, patch_bond_action, patch_bond_action_returns_clientresponseerror, @@ -43,15 +44,6 @@ from .common import ( from tests.common import async_fire_time_changed -def ceiling_fan(name: str): - """Create a ceiling fan with given name.""" - return { - "name": name, - "type": DeviceType.CEILING_FAN, - "actions": ["SetSpeed", "SetDirection"], - } - - def ceiling_fan_with_breeze(name: str): """Create a ceiling fan with given name with breeze support.""" return { diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 03eb490b65e..5db5d8e65bf 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -7,13 +7,16 @@ from bond_async import DeviceType import pytest from homeassistant.components.bond.const import DOMAIN +from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.setup import async_setup_component from .common import ( + ceiling_fan, patch_bond_bridge, patch_bond_device, patch_bond_device_ids, @@ -22,7 +25,9 @@ from .common import ( patch_bond_version, patch_setup_entry, patch_start_bpup, + remove_device, setup_bond_entity, + setup_platform, ) from tests.common import MockConfigEntry @@ -279,3 +284,62 @@ async def test_bridge_device_suggested_area(hass: HomeAssistant): device = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")}) assert device is not None assert device.suggested_area == "Office" + + +async def test_device_remove_devices(hass, hass_ws_client): + """Test we can only remove a device that no longer exists.""" + assert await async_setup_component(hass, "config", {}) + + config_entry = await setup_platform( + hass, + FAN_DOMAIN, + ceiling_fan("name-1"), + bond_version={"bondid": "test-hub-id"}, + bond_device_id="test-device-id", + ) + + registry: EntityRegistry = er.async_get(hass) + entity = registry.entities["fan.name_1"] + assert entity.unique_id == "test-hub-id_test-device-id" + + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get(entity.device_id) + assert ( + await remove_device( + await hass_ws_client(hass), device_entry.id, config_entry.entry_id + ) + is False + ) + + dead_device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, "test-hub-id", "remove-device-id")}, + ) + assert ( + await remove_device( + await hass_ws_client(hass), dead_device_entry.id, config_entry.entry_id + ) + is True + ) + + dead_device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, "wrong-hub-id", "test-device-id")}, + ) + assert ( + await remove_device( + await hass_ws_client(hass), dead_device_entry.id, config_entry.entry_id + ) + is True + ) + + hub_device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, "test-hub-id")}, + ) + assert ( + await remove_device( + await hass_ws_client(hass), hub_device_entry.id, config_entry.entry_id + ) + is False + ) From cbd0c8976b593927a33b8de26169fa106f4a620d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 May 2022 22:15:20 -0700 Subject: [PATCH 0948/3516] Attach SSL context to SMTP notify and IMAP sensor (#72568) --- .../components/imap_email_content/sensor.py | 26 ++++++++++------ homeassistant/components/smtp/notify.py | 30 +++++++++++++------ tests/components/smtp/test_notify.py | 1 + 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/imap_email_content/sensor.py b/homeassistant/components/imap_email_content/sensor.py index d0d87e0b2d5..a8bd394a159 100644 --- a/homeassistant/components/imap_email_content/sensor.py +++ b/homeassistant/components/imap_email_content/sensor.py @@ -17,12 +17,14 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, CONF_VALUE_TEMPLATE, + CONF_VERIFY_SSL, CONTENT_TYPE_TEXT_PLAIN, ) 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.ssl import client_context _LOGGER = logging.getLogger(__name__) @@ -46,6 +48,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_FOLDER, default="INBOX"): cv.string, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, } ) @@ -58,11 +61,12 @@ def setup_platform( ) -> None: """Set up the Email sensor platform.""" reader = EmailReader( - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD), - config.get(CONF_SERVER), - config.get(CONF_PORT), - config.get(CONF_FOLDER), + config[CONF_USERNAME], + config[CONF_PASSWORD], + config[CONF_SERVER], + config[CONF_PORT], + config[CONF_FOLDER], + config[CONF_VERIFY_SSL], ) if (value_template := config.get(CONF_VALUE_TEMPLATE)) is not None: @@ -70,8 +74,8 @@ def setup_platform( sensor = EmailContentSensor( hass, reader, - config.get(CONF_NAME) or config.get(CONF_USERNAME), - config.get(CONF_SENDERS), + config.get(CONF_NAME) or config[CONF_USERNAME], + config[CONF_SENDERS], value_template, ) @@ -82,21 +86,25 @@ def setup_platform( class EmailReader: """A class to read emails from an IMAP server.""" - def __init__(self, user, password, server, port, folder): + def __init__(self, user, password, server, port, folder, verify_ssl): """Initialize the Email Reader.""" self._user = user self._password = password self._server = server self._port = port self._folder = folder + self._verify_ssl = verify_ssl self._last_id = None self._unread_ids = deque([]) self.connection = None def connect(self): """Login and setup the connection.""" + ssl_context = client_context() if self._verify_ssl else None try: - self.connection = imaplib.IMAP4_SSL(self._server, self._port) + self.connection = imaplib.IMAP4_SSL( + self._server, self._port, ssl_context=ssl_context + ) self.connection.login(self._user, self._password) return True except imaplib.IMAP4.error: diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index 7b8e2dad1ed..866d7980d08 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -25,10 +25,12 @@ from homeassistant.const import ( CONF_SENDER, CONF_TIMEOUT, CONF_USERNAME, + CONF_VERIFY_SSL, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import setup_reload_service import homeassistant.util.dt as dt_util +from homeassistant.util.ssl import client_context from . import DOMAIN, PLATFORMS @@ -65,6 +67,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_SENDER_NAME): cv.string, vol.Optional(CONF_DEBUG, default=DEFAULT_DEBUG): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, } ) @@ -73,16 +76,17 @@ def get_service(hass, config, discovery_info=None): """Get the mail notification service.""" setup_reload_service(hass, DOMAIN, PLATFORMS) mail_service = MailNotificationService( - config.get(CONF_SERVER), - config.get(CONF_PORT), - config.get(CONF_TIMEOUT), - config.get(CONF_SENDER), - config.get(CONF_ENCRYPTION), + config[CONF_SERVER], + config[CONF_PORT], + config[CONF_TIMEOUT], + config[CONF_SENDER], + config[CONF_ENCRYPTION], config.get(CONF_USERNAME), config.get(CONF_PASSWORD), - config.get(CONF_RECIPIENT), + config[CONF_RECIPIENT], config.get(CONF_SENDER_NAME), - config.get(CONF_DEBUG), + config[CONF_DEBUG], + config[CONF_VERIFY_SSL], ) if mail_service.connection_is_valid(): @@ -106,6 +110,7 @@ class MailNotificationService(BaseNotificationService): recipients, sender_name, debug, + verify_ssl, ): """Initialize the SMTP service.""" self._server = server @@ -118,18 +123,25 @@ class MailNotificationService(BaseNotificationService): self.recipients = recipients self._sender_name = sender_name self.debug = debug + self._verify_ssl = verify_ssl self.tries = 2 def connect(self): """Connect/authenticate to SMTP Server.""" + ssl_context = client_context() if self._verify_ssl else None if self.encryption == "tls": - mail = smtplib.SMTP_SSL(self._server, self._port, timeout=self._timeout) + mail = smtplib.SMTP_SSL( + self._server, + self._port, + timeout=self._timeout, + context=ssl_context, + ) else: mail = smtplib.SMTP(self._server, self._port, timeout=self._timeout) mail.set_debuglevel(self.debug) mail.ehlo_or_helo_if_needed() if self.encryption == "starttls": - mail.starttls() + mail.starttls(context=ssl_context) mail.ehlo() if self.username and self.password: mail.login(self.username, self.password) diff --git a/tests/components/smtp/test_notify.py b/tests/components/smtp/test_notify.py index 38f48c169ac..ac742e10ea1 100644 --- a/tests/components/smtp/test_notify.py +++ b/tests/components/smtp/test_notify.py @@ -76,6 +76,7 @@ def message(): ["recip1@example.com", "testrecip@test.com"], "Home Assistant", 0, + True, ) yield mailer From cc42a95100327bb59fe69f1d8d95f1fa19fcd5e7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 27 May 2022 08:36:32 +0200 Subject: [PATCH 0949/3516] Migrate xiaomi_miio light to color_mode (#70998) --- homeassistant/components/xiaomi_miio/light.py | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index e97c6e76503..28feb23d93e 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -19,9 +19,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + ColorMode, LightEntity, ) from homeassistant.config_entries import ConfigEntry @@ -234,6 +232,9 @@ async def async_setup_entry( class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): """Representation of a Abstract Xiaomi Philips Light.""" + _attr_color_mode = ColorMode.BRIGHTNESS + _attr_supported_color_modes = {ColorMode.BRIGHTNESS} + def __init__(self, name, device, entry, unique_id): """Initialize the light device.""" super().__init__(name, device, entry, unique_id) @@ -263,11 +264,6 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): """Return the brightness of this light between 0..255.""" return self._brightness - @property - def supported_features(self): - """Return the supported features.""" - return SUPPORT_BRIGHTNESS - async def _try_command(self, mask_error, func, *args, **kwargs): """Call a light command handling error messages.""" try: @@ -399,6 +395,9 @@ class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): """Representation of a Xiaomi Philips Bulb.""" + _attr_color_mode = ColorMode.COLOR_TEMP + _attr_supported_color_modes = {ColorMode.COLOR_TEMP} + def __init__(self, name, device, entry, unique_id): """Initialize the light device.""" super().__init__(name, device, entry, unique_id) @@ -420,11 +419,6 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): """Return the warmest color_temp that this light supports.""" return 333 - @property - def supported_features(self): - """Return the supported features.""" - return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP - async def async_turn_on(self, **kwargs): """Turn the light on.""" if ATTR_COLOR_TEMP in kwargs: @@ -760,6 +754,8 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): """Representation of a Xiaomi Philips Zhirui Bedside Lamp.""" + _attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} + def __init__(self, name, device, entry, unique_id): """Initialize the light device.""" super().__init__(name, device, entry, unique_id) @@ -792,9 +788,11 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): return self._hs_color @property - def supported_features(self): - """Return the supported features.""" - return SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP + def color_mode(self): + """Return the color mode of the light.""" + if self.hs_color: + return ColorMode.HS + return ColorMode.COLOR_TEMP async def async_turn_on(self, **kwargs): """Turn the light on.""" @@ -935,6 +933,9 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): class XiaomiGatewayLight(LightEntity): """Representation of a gateway device's light.""" + _attr_color_mode = ColorMode.HS + _attr_supported_color_modes = {ColorMode.HS} + def __init__(self, gateway_device, gateway_name, gateway_device_id): """Initialize the XiaomiGatewayLight.""" self._gateway = gateway_device @@ -984,11 +985,6 @@ class XiaomiGatewayLight(LightEntity): """Return the hs color value.""" return self._hs - @property - def supported_features(self): - """Return the supported features.""" - return SUPPORT_BRIGHTNESS | SUPPORT_COLOR - def turn_on(self, **kwargs): """Turn the light on.""" if ATTR_HS_COLOR in kwargs: @@ -1036,6 +1032,9 @@ class XiaomiGatewayLight(LightEntity): class XiaomiGatewayBulb(XiaomiGatewayDevice, LightEntity): """Representation of Xiaomi Gateway Bulb.""" + _attr_color_mode = ColorMode.COLOR_TEMP + _attr_supported_color_modes = {ColorMode.COLOR_TEMP} + @property def brightness(self): """Return the brightness of the light.""" @@ -1061,11 +1060,6 @@ class XiaomiGatewayBulb(XiaomiGatewayDevice, LightEntity): """Return max cct.""" return self._sub_device.status["cct_max"] - @property - def supported_features(self): - """Return the supported features.""" - return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP - async def async_turn_on(self, **kwargs): """Instruct the light to turn on.""" await self.hass.async_add_executor_job(self._sub_device.on) From 01b5f984144987a9042042f1df7b16c888b150c3 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 27 May 2022 17:20:37 +1000 Subject: [PATCH 0950/3516] Bump httpx to 0.23.0 (#72573) Co-authored-by: J. Nick Koston --- homeassistant/package_constraints.txt | 6 +++--- pyproject.toml | 2 +- requirements.txt | 2 +- script/gen_requirements_all.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 309d4b89e9f..f235cc3f02c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 home-assistant-frontend==20220526.0 -httpx==0.22.0 +httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.7 @@ -78,9 +78,9 @@ regex==2021.8.28 # these requirements are quite loose. As the entire stack has some outstanding issues, and # even newer versions seem to introduce new issues, it's useful for us to pin all these # requirements so we can directly link HA versions to these library versions. -anyio==3.5.0 +anyio==3.6.1 h11==0.12.0 -httpcore==0.14.7 +httpcore==0.15.0 # Ensure we have a hyperframe version that works in Python 3.10 # 5.2.0 fixed a collections abc deprecation diff --git a/pyproject.toml b/pyproject.toml index 60551dae997..499376b95f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ dependencies = [ "ciso8601==2.2.0", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all - "httpx==0.22.0", + "httpx==0.23.0", "ifaddr==0.1.7", "jinja2==3.1.2", "PyJWT==2.4.0", diff --git a/requirements.txt b/requirements.txt index 0c13b9c319b..8321e70f8de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ awesomeversion==22.5.1 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 -httpx==0.22.0 +httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 PyJWT==2.4.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index a7b26d297c9..adf57f14f97 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -98,9 +98,9 @@ regex==2021.8.28 # these requirements are quite loose. As the entire stack has some outstanding issues, and # even newer versions seem to introduce new issues, it's useful for us to pin all these # requirements so we can directly link HA versions to these library versions. -anyio==3.5.0 +anyio==3.6.1 h11==0.12.0 -httpcore==0.14.7 +httpcore==0.15.0 # Ensure we have a hyperframe version that works in Python 3.10 # 5.2.0 fixed a collections abc deprecation From 9cd9d06bccfd3e73ec989ba2692229c129507914 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 27 May 2022 02:46:22 -0500 Subject: [PATCH 0951/3516] Avoid network activity during Plex tests (#72499) --- tests/components/plex/test_config_flow.py | 6 ++++++ tests/components/plex/test_init.py | 1 + 2 files changed, 7 insertions(+) diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index c22890ebef3..f02abd834d7 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -193,6 +193,8 @@ async def test_single_available_server( ) assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN + await hass.config_entries.async_unload(result["result"].entry_id) + async def test_multiple_servers_with_selection( hass, @@ -249,6 +251,8 @@ async def test_multiple_servers_with_selection( ) assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN + await hass.config_entries.async_unload(result["result"].entry_id) + async def test_adding_last_unconfigured_server( hass, @@ -305,6 +309,8 @@ async def test_adding_last_unconfigured_server( ) assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN + await hass.config_entries.async_unload(result["result"].entry_id) + async def test_all_available_servers_configured( hass, diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index bbab50a7bbb..94278ca6052 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -184,6 +184,7 @@ async def test_setup_when_certificate_changed( plextv_account, plextv_resources, plextv_shared_users, + mock_websocket, ): """Test setup component when the Plex certificate has changed.""" From 39448009bfb44d70a5df98a3629f6579f65aee3e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 May 2022 22:15:43 -1000 Subject: [PATCH 0952/3516] Revert "Remove sqlite 3.34.1 downgrade workaround by reverting "Downgrade sqlite-libs on docker image (#55591)" (#72342)" (#72578) --- Dockerfile | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Dockerfile b/Dockerfile index 13552d55a3d..1d6ce675e74 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,21 @@ RUN \ -e ./homeassistant --use-deprecated=legacy-resolver \ && python3 -m compileall homeassistant/homeassistant +# Fix Bug with Alpine 3.14 and sqlite 3.35 +# https://gitlab.alpinelinux.org/alpine/aports/-/issues/12524 +ARG BUILD_ARCH +RUN \ + if [ "${BUILD_ARCH}" = "amd64" ]; then \ + export APK_ARCH=x86_64; \ + elif [ "${BUILD_ARCH}" = "i386" ]; then \ + export APK_ARCH=x86; \ + else \ + export APK_ARCH=${BUILD_ARCH}; \ + fi \ + && curl -O http://dl-cdn.alpinelinux.org/alpine/v3.13/main/${APK_ARCH}/sqlite-libs-3.34.1-r0.apk \ + && apk add --no-cache sqlite-libs-3.34.1-r0.apk \ + && rm -f sqlite-libs-3.34.1-r0.apk + # Home Assistant S6-Overlay COPY rootfs / From 9b60b092c625206c255097d3a1fa9726e1e9ca87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 27 May 2022 10:41:40 +0200 Subject: [PATCH 0953/3516] Update aioqsw to v0.1.0 (#72576) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit qnap_qsw: update aioqsw to v0.1.0 Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/qnap_qsw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qnap_qsw/manifest.json b/homeassistant/components/qnap_qsw/manifest.json index 9331a7df468..0dfd0e4793e 100644 --- a/homeassistant/components/qnap_qsw/manifest.json +++ b/homeassistant/components/qnap_qsw/manifest.json @@ -3,7 +3,7 @@ "name": "QNAP QSW", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/qnap_qsw", - "requirements": ["aioqsw==0.0.8"], + "requirements": ["aioqsw==0.1.0"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioqsw"] diff --git a/requirements_all.txt b/requirements_all.txt index c7459285fa0..7e299929590 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -223,7 +223,7 @@ aiopvpc==3.0.0 aiopyarr==22.2.2 # homeassistant.components.qnap_qsw -aioqsw==0.0.8 +aioqsw==0.1.0 # homeassistant.components.recollect_waste aiorecollect==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7233824a3cc..25cba8501e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -192,7 +192,7 @@ aiopvpc==3.0.0 aiopyarr==22.2.2 # homeassistant.components.qnap_qsw -aioqsw==0.0.8 +aioqsw==0.1.0 # homeassistant.components.recollect_waste aiorecollect==1.0.8 From 43e66b3af986c6298bf1a25a4780258032d84443 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 27 May 2022 10:44:31 +0200 Subject: [PATCH 0954/3516] Adjust config-flow type hints in firmata (#72502) --- homeassistant/components/firmata/config_flow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/firmata/config_flow.py b/homeassistant/components/firmata/config_flow.py index b4a9ada2c27..8aa4cfb836c 100644 --- a/homeassistant/components/firmata/config_flow.py +++ b/homeassistant/components/firmata/config_flow.py @@ -1,11 +1,13 @@ """Config flow to configure firmata component.""" import logging +from typing import Any from pymata_express.pymata_express_serial import serial from homeassistant import config_entries from homeassistant.const import CONF_NAME +from homeassistant.data_entry_flow import FlowResult from .board import get_board from .const import CONF_SERIAL_PORT, DOMAIN @@ -18,7 +20,7 @@ class FirmataFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_import(self, import_config: dict): + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: """Import a firmata board as a config entry. This flow is triggered by `async_setup` for configured boards. From 371dfd85c8b61a62c48526778b377c090e6c0982 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 27 May 2022 02:52:24 -0700 Subject: [PATCH 0955/3516] Reduce the scope of the google calendar track deprecation (#72575) --- homeassistant/components/google/__init__.py | 1 - homeassistant/components/google/calendar.py | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index f034d48b9c5..b7263d2e469 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -117,7 +117,6 @@ CONFIG_SCHEMA = vol.Schema( _SINGLE_CALSEARCH_CONFIG = vol.All( cv.deprecated(CONF_MAX_RESULTS), - cv.deprecated(CONF_TRACK), vol.Schema( { vol.Required(CONF_NAME): cv.string, diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 01780702b7f..ba4368fefae 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -93,6 +93,11 @@ def _async_setup_entities( num_entities = len(disc_info[CONF_ENTITIES]) for data in disc_info[CONF_ENTITIES]: entity_enabled = data.get(CONF_TRACK, True) + if not entity_enabled: + _LOGGER.warning( + "The 'track' option in google_calendars.yaml has been deprecated. The setting " + "has been imported to the UI, and should now be removed from google_calendars.yaml" + ) entity_name = data[CONF_DEVICE_ID] entity_id = generate_entity_id(ENTITY_ID_FORMAT, entity_name, hass=hass) calendar_id = disc_info[CONF_CAL_ID] From 35bc6900ea5d80211f343250df7a132b73bcd4cd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 27 May 2022 15:09:43 +0200 Subject: [PATCH 0956/3516] Simplify MQTT PLATFORM_CONFIG_SCHEMA_BASE (#72589) --- homeassistant/components/mqtt/__init__.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e8847375584..78f64387435 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -190,26 +190,7 @@ MQTT_WILL_BIRTH_SCHEMA = vol.Schema( ) PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( - { - vol.Optional(Platform.ALARM_CONTROL_PANEL.value): cv.ensure_list, - vol.Optional(Platform.BINARY_SENSOR.value): cv.ensure_list, - vol.Optional(Platform.BUTTON.value): cv.ensure_list, - vol.Optional(Platform.CAMERA.value): cv.ensure_list, - vol.Optional(Platform.CLIMATE.value): cv.ensure_list, - vol.Optional(Platform.COVER.value): cv.ensure_list, - vol.Optional(Platform.DEVICE_TRACKER.value): cv.ensure_list, - vol.Optional(Platform.FAN.value): cv.ensure_list, - vol.Optional(Platform.HUMIDIFIER.value): cv.ensure_list, - vol.Optional(Platform.LIGHT.value): cv.ensure_list, - vol.Optional(Platform.LOCK.value): cv.ensure_list, - vol.Optional(Platform.NUMBER.value): cv.ensure_list, - vol.Optional(Platform.SCENE.value): cv.ensure_list, - vol.Optional(Platform.SELECT.value): cv.ensure_list, - vol.Optional(Platform.SIREN.value): cv.ensure_list, - vol.Optional(Platform.SENSOR.value): cv.ensure_list, - vol.Optional(Platform.SWITCH.value): cv.ensure_list, - vol.Optional(Platform.VACUUM.value): cv.ensure_list, - } + {vol.Optional(platform.value): cv.ensure_list for platform in PLATFORMS} ) CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend( From 5ca82b2d33b24eaf23b3292cae58ad6e0d704617 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 27 May 2022 15:38:22 +0200 Subject: [PATCH 0957/3516] Migrate zha light to color_mode (#70970) * Migrate zha light to color_mode * Fix restoring color mode * Correct set operations * Derive color mode from group members * Add color mode to color channel * use Zigpy color mode enum Co-authored-by: David Mulcahey --- .../components/zha/core/channels/lighting.py | 6 + homeassistant/components/zha/light.py | 116 +++++++++++------- tests/components/zha/test_light.py | 12 +- 3 files changed, 86 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index 1dbf1d201c8..13d5b4c2742 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -36,6 +36,7 @@ class ColorChannel(ZigbeeChannel): MAX_MIREDS: int = 500 MIN_MIREDS: int = 153 ZCL_INIT_ATTRS = { + "color_mode": False, "color_temp_physical_min": True, "color_temp_physical_max": True, "color_capabilities": True, @@ -51,6 +52,11 @@ class ColorChannel(ZigbeeChannel): return self.CAPABILITIES_COLOR_XY | self.CAPABILITIES_COLOR_TEMP return self.CAPABILITIES_COLOR_XY + @property + def color_mode(self) -> int | None: + """Return cached value of the color_mode attribute.""" + return self.cluster.get("color_mode") + @property def color_loop_active(self) -> int | None: """Return cached value of the color_loop_active attribute.""" diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 2ec507109a2..30ae9688729 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -3,12 +3,11 @@ from __future__ import annotations from collections import Counter from datetime import timedelta -import enum import functools import itertools import logging import random -from typing import Any +from typing import Any, cast from zigpy.zcl.clusters.general import Identify, LevelControl, OnOff from zigpy.zcl.clusters.lighting import Color @@ -17,16 +16,17 @@ from zigpy.zcl.foundation import Status from homeassistant.components import light from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_EFFECT_LIST, ATTR_HS_COLOR, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, - LightEntityFeature, + ATTR_SUPPORTED_COLOR_MODES, + ColorMode, + brightness_supported, + filter_supported_color_modes, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -86,24 +86,14 @@ GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.LIGHT) PARALLEL_UPDATES = 0 SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed" +COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.HS} SUPPORT_GROUP_LIGHT = ( - SUPPORT_BRIGHTNESS - | SUPPORT_COLOR_TEMP - | LightEntityFeature.EFFECT - | LightEntityFeature.FLASH - | SUPPORT_COLOR - | LightEntityFeature.TRANSITION + light.LightEntityFeature.EFFECT + | light.LightEntityFeature.FLASH + | light.LightEntityFeature.TRANSITION ) -class LightColorMode(enum.IntEnum): - """ZCL light color mode enum.""" - - HS_COLOR = 0x00 - XY_COLOR = 0x01 - COLOR_TEMP = 0x02 - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -146,6 +136,7 @@ class BaseLight(LogMixin, light.LightEntity): self._color_channel = None self._identify_channel = None self._default_transition = None + self._color_mode = ColorMode.UNKNOWN # Set by sub classes @property def extra_state_attributes(self) -> dict[str, Any]: @@ -160,6 +151,11 @@ class BaseLight(LogMixin, light.LightEntity): return False return self._state + @property + def color_mode(self): + """Return the color mode of this light.""" + return self._color_mode + @property def brightness(self): """Return the brightness of this light.""" @@ -230,9 +226,9 @@ class BaseLight(LogMixin, light.LightEntity): brightness = self._off_brightness t_log = {} - if ( - brightness is not None or transition - ) and self._supported_features & light.SUPPORT_BRIGHTNESS: + if (brightness is not None or transition) and brightness_supported( + self._attr_supported_color_modes + ): if brightness is not None: level = min(254, brightness) else: @@ -257,10 +253,7 @@ class BaseLight(LogMixin, light.LightEntity): self.debug("turned on: %s", t_log) return self._state = True - if ( - light.ATTR_COLOR_TEMP in kwargs - and self.supported_features & light.SUPPORT_COLOR_TEMP - ): + if light.ATTR_COLOR_TEMP in kwargs: temperature = kwargs[light.ATTR_COLOR_TEMP] result = await self._color_channel.move_to_color_temp(temperature, duration) t_log["move_to_color_temp"] = result @@ -270,10 +263,7 @@ class BaseLight(LogMixin, light.LightEntity): self._color_temp = temperature self._hs_color = None - if ( - light.ATTR_HS_COLOR in kwargs - and self.supported_features & light.SUPPORT_COLOR - ): + if light.ATTR_HS_COLOR in kwargs: hs_color = kwargs[light.ATTR_HS_COLOR] xy_color = color_util.color_hs_to_xy(*hs_color) result = await self._color_channel.move_to_color( @@ -286,10 +276,7 @@ class BaseLight(LogMixin, light.LightEntity): self._hs_color = hs_color self._color_temp = None - if ( - effect == light.EFFECT_COLORLOOP - and self.supported_features & light.LightEntityFeature.EFFECT - ): + if effect == light.EFFECT_COLORLOOP: result = await self._color_channel.color_loop_set( UPDATE_COLORLOOP_ACTION | UPDATE_COLORLOOP_DIRECTION @@ -302,9 +289,7 @@ class BaseLight(LogMixin, light.LightEntity): t_log["color_loop_set"] = result self._effect = light.EFFECT_COLORLOOP elif ( - self._effect == light.EFFECT_COLORLOOP - and effect != light.EFFECT_COLORLOOP - and self.supported_features & light.LightEntityFeature.EFFECT + self._effect == light.EFFECT_COLORLOOP and effect != light.EFFECT_COLORLOOP ): result = await self._color_channel.color_loop_set( UPDATE_COLORLOOP_ACTION, @@ -316,10 +301,7 @@ class BaseLight(LogMixin, light.LightEntity): t_log["color_loop_set"] = result self._effect = None - if ( - flash is not None - and self._supported_features & light.LightEntityFeature.FLASH - ): + if flash is not None: result = await self._identify_channel.trigger_effect( FLASH_EFFECTS[flash], EFFECT_DEFAULT_VARIANT ) @@ -332,7 +314,7 @@ class BaseLight(LogMixin, light.LightEntity): async def async_turn_off(self, **kwargs): """Turn the entity off.""" duration = kwargs.get(light.ATTR_TRANSITION) - supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS + supports_level = brightness_supported(self._attr_supported_color_modes) if duration and supports_level: result = await self._level_channel.move_to_level_with_on_off( @@ -356,6 +338,7 @@ class BaseLight(LogMixin, light.LightEntity): class Light(BaseLight, ZhaEntity): """Representation of a ZHA or ZLL light.""" + _attr_supported_color_modes: set(ColorMode) _REFRESH_INTERVAL = (45, 75) def __init__(self, unique_id, zha_device: ZhaDeviceType, channels, **kwargs): @@ -372,19 +355,20 @@ class Light(BaseLight, ZhaEntity): self._cancel_refresh_handle = None effect_list = [] + self._attr_supported_color_modes = {ColorMode.ONOFF} if self._level_channel: - self._supported_features |= light.SUPPORT_BRIGHTNESS + self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS) self._supported_features |= light.LightEntityFeature.TRANSITION self._brightness = self._level_channel.current_level if self._color_channel: color_capabilities = self._color_channel.color_capabilities if color_capabilities & CAPABILITIES_COLOR_TEMP: - self._supported_features |= light.SUPPORT_COLOR_TEMP + self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP) self._color_temp = self._color_channel.color_temperature if color_capabilities & CAPABILITIES_COLOR_XY: - self._supported_features |= light.SUPPORT_COLOR + self._attr_supported_color_modes.add(ColorMode.HS) curr_x = self._color_channel.current_x curr_y = self._color_channel.current_y if curr_x is not None and curr_y is not None: @@ -399,6 +383,16 @@ class Light(BaseLight, ZhaEntity): effect_list.append(light.EFFECT_COLORLOOP) if self._color_channel.color_loop_active == 1: self._effect = light.EFFECT_COLORLOOP + self._attr_supported_color_modes = filter_supported_color_modes( + self._attr_supported_color_modes + ) + if len(self._attr_supported_color_modes) == 1: + self._color_mode = next(iter(self._attr_supported_color_modes)) + else: # Light supports color_temp + hs, determine which mode the light is in + if self._color_channel.color_mode == Color.ColorMode.Color_temperature: + self._color_mode = ColorMode.COLOR_TEMP + else: + self._color_mode = ColorMode.HS if self._identify_channel: self._supported_features |= light.LightEntityFeature.FLASH @@ -455,6 +449,8 @@ class Light(BaseLight, ZhaEntity): self._brightness = last_state.attributes["brightness"] if "off_brightness" in last_state.attributes: self._off_brightness = last_state.attributes["off_brightness"] + if "color_mode" in last_state.attributes: + self._color_mode = ColorMode(last_state.attributes["color_mode"]) if "color_temp" in last_state.attributes: self._color_temp = last_state.attributes["color_temp"] if "hs_color" in last_state.attributes: @@ -493,12 +489,14 @@ class Light(BaseLight, ZhaEntity): ) if (color_mode := results.get("color_mode")) is not None: - if color_mode == LightColorMode.COLOR_TEMP: + if color_mode == Color.ColorMode.Color_temperature: + self._color_mode = ColorMode.COLOR_TEMP color_temp = results.get("color_temperature") if color_temp is not None and color_mode: self._color_temp = color_temp self._hs_color = None else: + self._color_mode = ColorMode.HS color_x = results.get("current_x") color_y = results.get("current_y") if color_x is not None and color_y is not None: @@ -573,6 +571,7 @@ class LightGroup(BaseLight, ZhaGroupEntity): CONF_DEFAULT_LIGHT_TRANSITION, 0, ) + self._color_mode = None async def async_added_to_hass(self): """Run when about to be added to hass.""" @@ -633,6 +632,29 @@ class LightGroup(BaseLight, ZhaGroupEntity): effects_count = Counter(itertools.chain(all_effects)) self._effect = effects_count.most_common(1)[0][0] + self._attr_color_mode = None + all_color_modes = list( + helpers.find_state_attributes(on_states, ATTR_COLOR_MODE) + ) + if all_color_modes: + # Report the most common color mode, select brightness and onoff last + color_mode_count = Counter(itertools.chain(all_color_modes)) + if ColorMode.ONOFF in color_mode_count: + color_mode_count[ColorMode.ONOFF] = -1 + if ColorMode.BRIGHTNESS in color_mode_count: + color_mode_count[ColorMode.BRIGHTNESS] = 0 + self._attr_color_mode = color_mode_count.most_common(1)[0][0] + + self._attr_supported_color_modes = None + all_supported_color_modes = list( + helpers.find_state_attributes(states, ATTR_SUPPORTED_COLOR_MODES) + ) + if all_supported_color_modes: + # Merge all color modes. + self._attr_supported_color_modes = cast( + set[str], set().union(*all_supported_color_modes) + ) + self._supported_features = 0 for support in helpers.find_state_attributes(states, ATTR_SUPPORTED_FEATURES): # Merge supported features by emulating support for every feature diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 4ac777f5d8e..8cf0e668503 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -12,6 +12,7 @@ from homeassistant.components.light import ( DOMAIN as LIGHT_DOMAIN, FLASH_LONG, FLASH_SHORT, + ColorMode, ) from homeassistant.components.zha.core.group import GroupMember from homeassistant.components.zha.light import FLASH_EFFECTS @@ -580,7 +581,11 @@ async def test_zha_group_light_entity( await async_wait_for_updates(hass) # test that the lights were created and are off - assert hass.states.get(group_entity_id).state == STATE_OFF + group_state = hass.states.get(group_entity_id) + assert group_state.state == STATE_OFF + assert group_state.attributes["supported_color_modes"] == [ColorMode.HS] + # Light which is off has no color mode + assert "color_mode" not in group_state.attributes # test turning the lights on and off from the HA await async_test_on_off_from_hass(hass, group_cluster_on_off, group_entity_id) @@ -603,6 +608,11 @@ async def test_zha_group_light_entity( await async_test_dimmer_from_light( hass, dev1_cluster_level, group_entity_id, 150, STATE_ON ) + # Check state + group_state = hass.states.get(group_entity_id) + assert group_state.state == STATE_ON + assert group_state.attributes["supported_color_modes"] == [ColorMode.HS] + assert group_state.attributes["color_mode"] == ColorMode.HS # test long flashing the lights from the HA await async_test_flash_from_hass( From 60387a417fb82b47700899a6b7e80b30dcc9766f Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 27 May 2022 09:43:39 -0400 Subject: [PATCH 0958/3516] Add support for polled Smart Energy Metering sensors to ZHA (#71527) * Add framework for polled se metering sensors * add model * find attr * type info --- .../zha/core/channels/homeautomation.py | 2 +- .../zha/core/channels/smartenergy.py | 26 +++++++++++- homeassistant/components/zha/sensor.py | 25 ++++++++++- homeassistant/components/zha/switch.py | 42 ++++++++++++------- 4 files changed, 77 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index e1019ed31bf..60c33c93003 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -102,7 +102,7 @@ class ElectricalMeasurementChannel(ZigbeeChannel): for attr, value in result.items(): self.async_send_signal( f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", - self.cluster.attridx.get(attr, attr), + self.cluster.find_attribute(attr).id, attr, value, ) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index b153372a322..927ceb248c5 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -7,7 +7,12 @@ from functools import partialmethod from zigpy.zcl.clusters import smartenergy from .. import registries, typing as zha_typing -from ..const import REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, REPORT_CONFIG_OP +from ..const import ( + REPORT_CONFIG_ASAP, + REPORT_CONFIG_DEFAULT, + REPORT_CONFIG_OP, + SIGNAL_ATTR_UPDATED, +) from .base import ZigbeeChannel @@ -163,6 +168,25 @@ class Metering(ZigbeeChannel): ) # 1 digit to the right, 15 digits to the left self._summa_format = self.get_formatting(fmting) + async def async_force_update(self) -> None: + """Retrieve latest state.""" + self.debug("async_force_update") + + 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, only_cache=False) + if result: + for attr, value in result.items(): + self.async_send_signal( + f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", + self.cluster.find_attribute(attr).id, + attr, + value, + ) + @staticmethod def get_formatting(formatting: int) -> str: """Return a formatting string, given the formatting value. diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 3e3017f6fa9..36ca873188f 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -420,7 +420,10 @@ class Illuminance(Sensor): return round(pow(10, ((value - 1) / 10000)), 1) -@MULTI_MATCH(channel_names=CHANNEL_SMARTENERGY_METERING) +@MULTI_MATCH( + channel_names=CHANNEL_SMARTENERGY_METERING, + stop_on_match_group=CHANNEL_SMARTENERGY_METERING, +) class SmartEnergyMetering(Sensor): """Metering sensor.""" @@ -464,6 +467,26 @@ class SmartEnergyMetering(Sensor): return attrs +@MULTI_MATCH( + channel_names=CHANNEL_SMARTENERGY_METERING, + models={"TS011F"}, + stop_on_match_group=CHANNEL_SMARTENERGY_METERING, +) +class PolledSmartEnergyMetering(SmartEnergyMetering): + """Polled metering sensor.""" + + @property + def should_poll(self) -> bool: + """Poll the entity for current state.""" + return True + + async def async_update(self) -> None: + """Retrieve latest state.""" + if not self.available: + return + await self._channel.async_force_update() + + @MULTI_MATCH(channel_names=CHANNEL_SMARTENERGY_METERING) class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered"): """Smart Energy Metering summation sensor.""" diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index d9199ed77c8..1926b08fc60 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -29,6 +29,7 @@ from .entity import ZhaEntity, ZhaGroupEntity if TYPE_CHECKING: from .core.channels.base import ZigbeeChannel + from .core.channels.general import OnOffChannel from .core.device import ZHADevice STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SWITCH) @@ -62,10 +63,16 @@ async def async_setup_entry( class Switch(ZhaEntity, SwitchEntity): """ZHA switch.""" - def __init__(self, unique_id, zha_device, channels, **kwargs): + def __init__( + self, + unique_id: str, + zha_device: ZHADevice, + channels: list[ZigbeeChannel], + **kwargs: Any, + ) -> None: """Initialize the ZHA switch.""" super().__init__(unique_id, zha_device, channels, **kwargs) - self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) + self._on_off_channel: OnOffChannel = self.cluster_channels.get(CHANNEL_ON_OFF) @property def is_on(self) -> bool: @@ -74,14 +81,14 @@ class Switch(ZhaEntity, SwitchEntity): return False return self._on_off_channel.on_off - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" result = await self._on_off_channel.turn_on() if not result: return self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" result = await self._on_off_channel.turn_off() if not result: @@ -112,7 +119,12 @@ class SwitchGroup(ZhaGroupEntity, SwitchEntity): """Representation of a switch group.""" def __init__( - self, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs + self, + entity_ids: list[str], + unique_id: str, + group_id: int, + zha_device: ZHADevice, + **kwargs: Any, ) -> None: """Initialize a switch group.""" super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) @@ -126,7 +138,7 @@ class SwitchGroup(ZhaGroupEntity, SwitchEntity): """Return if the switch is on based on the statemachine.""" return bool(self._state) - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" result = await self._on_off_channel.on() if isinstance(result, Exception) or result[1] is not Status.SUCCESS: @@ -134,7 +146,7 @@ class SwitchGroup(ZhaGroupEntity, SwitchEntity): self._state = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" result = await self._on_off_channel.off() if isinstance(result, Exception) or result[1] is not Status.SUCCESS: @@ -165,7 +177,7 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> ZhaEntity | None: """Entity Factory. @@ -190,7 +202,7 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this number configuration entity.""" self._channel: ZigbeeChannel = channels[0] @@ -215,7 +227,7 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): invert = bool(self._channel.cluster.get(self._zcl_inverter_attribute)) return (not val) if invert else val - async def async_turn_on_off(self, state) -> None: + async def async_turn_on_off(self, state: bool) -> None: """Turn the entity on or off.""" try: invert = bool(self._channel.cluster.get(self._zcl_inverter_attribute)) @@ -230,11 +242,11 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): ): self.async_write_ha_state() - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" await self.async_turn_on_off(True) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" await self.async_turn_on_off(False) @@ -263,8 +275,8 @@ class OnOffWindowDetectionFunctionConfigurationEntity( ): """Representation of a ZHA window detection configuration entity.""" - _zcl_attribute = "window_detection_function" - _zcl_inverter_attribute = "window_detection_function_inverter" + _zcl_attribute: str = "window_detection_function" + _zcl_inverter_attribute: str = "window_detection_function_inverter" @CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.motion.ac02"}) @@ -273,4 +285,4 @@ class P1MotionTriggerIndicatorSwitch( ): """Representation of a ZHA motion triggering configuration entity.""" - _zcl_attribute = "trigger_indicator" + _zcl_attribute: str = "trigger_indicator" From b6575aa66b8d965c0f24a5b0e46f938f507b279b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 27 May 2022 16:53:49 +0200 Subject: [PATCH 0959/3516] Minor cleanup of test integration's cover platform (#72598) --- tests/testing_config/custom_components/test/cover.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testing_config/custom_components/test/cover.py b/tests/testing_config/custom_components/test/cover.py index edd8965e4e9..98d59a473b1 100644 --- a/tests/testing_config/custom_components/test/cover.py +++ b/tests/testing_config/custom_components/test/cover.py @@ -18,7 +18,7 @@ from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_O from tests.common import MockEntity -ENTITIES = {} +ENTITIES = [] def init(empty=False): From f76afffd5ab12014d1e506ed1ab3c38f2ed3b8f1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 27 May 2022 17:40:55 +0200 Subject: [PATCH 0960/3516] Require passing target player when resolving media (#72593) --- .../components/media_source/__init__.py | 23 +++++++++++++------ .../components/media_source/local_source.py | 4 ++-- .../components/media_source/models.py | 7 ++++-- .../components/dlna_dms/test_media_source.py | 8 +++---- tests/components/media_source/test_init.py | 19 +++++++++++++++ 5 files changed, 46 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 3c42016f8f7..4818934d1dd 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -18,10 +18,11 @@ from homeassistant.components.media_player.browse_media import ( ) from homeassistant.components.websocket_api import ActiveConnection from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.frame import report from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, ) -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType from homeassistant.loader import bind_hass from . import local_source @@ -80,15 +81,15 @@ async def _process_media_source_platform( @callback def _get_media_item( - hass: HomeAssistant, media_content_id: str | None + hass: HomeAssistant, media_content_id: str | None, target_media_player: str | None ) -> MediaSourceItem: """Return media item.""" if media_content_id: - item = MediaSourceItem.from_uri(hass, media_content_id) + item = MediaSourceItem.from_uri(hass, media_content_id, target_media_player) else: # We default to our own domain if its only one registered domain = None if len(hass.data[DOMAIN]) > 1 else DOMAIN - return MediaSourceItem(hass, domain, "") + return MediaSourceItem(hass, domain, "", target_media_player) if item.domain is not None and item.domain not in hass.data[DOMAIN]: raise ValueError("Unknown media source") @@ -108,7 +109,7 @@ async def async_browse_media( raise BrowseError("Media Source not loaded") try: - item = await _get_media_item(hass, media_content_id).async_browse() + item = await _get_media_item(hass, media_content_id, None).async_browse() except ValueError as err: raise BrowseError(str(err)) from err @@ -124,13 +125,21 @@ async def async_browse_media( @bind_hass -async def async_resolve_media(hass: HomeAssistant, media_content_id: str) -> PlayMedia: +async def async_resolve_media( + hass: HomeAssistant, + media_content_id: str, + target_media_player: str | None | UndefinedType = UNDEFINED, +) -> PlayMedia: """Get info to play media.""" if DOMAIN not in hass.data: raise Unresolvable("Media Source not loaded") + if target_media_player is UNDEFINED: + report("calls media_source.async_resolve_media without passing an entity_id") + target_media_player = None + try: - item = _get_media_item(hass, media_content_id) + item = _get_media_item(hass, media_content_id, target_media_player) except ValueError as err: raise Unresolvable(str(err)) from err diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index 89feba5317f..863380b7600 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -264,7 +264,7 @@ class UploadMediaView(http.HomeAssistantView): raise web.HTTPBadRequest() from err try: - item = MediaSourceItem.from_uri(self.hass, data["media_content_id"]) + item = MediaSourceItem.from_uri(self.hass, data["media_content_id"], None) except ValueError as err: LOGGER.error("Received invalid upload data: %s", err) raise web.HTTPBadRequest() from err @@ -328,7 +328,7 @@ async def websocket_remove_media( ) -> None: """Remove media.""" try: - item = MediaSourceItem.from_uri(hass, msg["media_content_id"]) + item = MediaSourceItem.from_uri(hass, msg["media_content_id"], None) except ValueError as err: connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err)) return diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index ceb57ef1fb4..0aee6ad1330 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -50,6 +50,7 @@ class MediaSourceItem: hass: HomeAssistant domain: str | None identifier: str + target_media_player: str | None async def async_browse(self) -> BrowseMediaSource: """Browse this item.""" @@ -94,7 +95,9 @@ class MediaSourceItem: return cast(MediaSource, self.hass.data[DOMAIN][self.domain]) @classmethod - def from_uri(cls, hass: HomeAssistant, uri: str) -> MediaSourceItem: + def from_uri( + cls, hass: HomeAssistant, uri: str, target_media_player: str | None + ) -> MediaSourceItem: """Create an item from a uri.""" if not (match := URI_SCHEME_REGEX.match(uri)): raise ValueError("Invalid media source URI") @@ -102,7 +105,7 @@ class MediaSourceItem: domain = match.group("domain") identifier = match.group("identifier") - return cls(hass, domain, identifier) + return cls(hass, domain, identifier, target_media_player) class MediaSource(ABC): diff --git a/tests/components/dlna_dms/test_media_source.py b/tests/components/dlna_dms/test_media_source.py index f2c3011e274..5f76b061590 100644 --- a/tests/components/dlna_dms/test_media_source.py +++ b/tests/components/dlna_dms/test_media_source.py @@ -49,7 +49,7 @@ async def test_get_media_source(hass: HomeAssistant) -> None: async def test_resolve_media_unconfigured(hass: HomeAssistant) -> None: """Test resolve_media without any devices being configured.""" source = DmsMediaSource(hass) - item = MediaSourceItem(hass, DOMAIN, "source_id/media_id") + item = MediaSourceItem(hass, DOMAIN, "source_id/media_id", None) with pytest.raises(Unresolvable, match="No sources have been configured"): await source.async_resolve_media(item) @@ -116,11 +116,11 @@ async def test_resolve_media_success( async def test_browse_media_unconfigured(hass: HomeAssistant) -> None: """Test browse_media without any devices being configured.""" source = DmsMediaSource(hass) - item = MediaSourceItem(hass, DOMAIN, "source_id/media_id") + item = MediaSourceItem(hass, DOMAIN, "source_id/media_id", None) with pytest.raises(BrowseError, match="No sources have been configured"): await source.async_browse_media(item) - item = MediaSourceItem(hass, DOMAIN, "") + item = MediaSourceItem(hass, DOMAIN, "", None) with pytest.raises(BrowseError, match="No sources have been configured"): await source.async_browse_media(item) @@ -239,7 +239,7 @@ async def test_browse_media_source_id( dms_device_mock.async_browse_metadata.side_effect = UpnpError # Browse by source_id - item = MediaSourceItem(hass, DOMAIN, f"{MOCK_SOURCE_ID}/:media-item-id") + item = MediaSourceItem(hass, DOMAIN, f"{MOCK_SOURCE_ID}/:media-item-id", None) dms_source = DmsMediaSource(hass) with pytest.raises(BrowseError): await dms_source.async_browse_media(item) diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index 491b1972cb6..f2a8ff13533 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -109,6 +109,25 @@ async def test_async_resolve_media(hass): assert media.mime_type == "audio/mpeg" +@patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()) +async def test_async_resolve_media_no_entity(hass, caplog): + """Test browse media.""" + assert await async_setup_component(hass, media_source.DOMAIN, {}) + await hass.async_block_till_done() + + media = await media_source.async_resolve_media( + hass, + media_source.generate_media_source_id(media_source.DOMAIN, "local/test.mp3"), + ) + assert isinstance(media, media_source.models.PlayMedia) + assert media.url == "/media/local/test.mp3" + assert media.mime_type == "audio/mpeg" + assert ( + "calls media_source.async_resolve_media without passing an entity_id" + in caplog.text + ) + + async def test_async_unresolve_media(hass): """Test browse media.""" assert await async_setup_component(hass, media_source.DOMAIN, {}) From 47d0cc9b09b49bed4de7218a902acfe00c2de758 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 27 May 2022 18:05:06 +0200 Subject: [PATCH 0961/3516] Update integrations to pass target player when resolving media (#72597) --- .../components/apple_tv/media_player.py | 4 ++- .../components/bluesound/media_player.py | 4 ++- homeassistant/components/cast/media_player.py | 4 ++- .../components/dlna_dmr/media_player.py | 4 ++- .../components/esphome/media_player.py | 4 ++- .../components/forked_daapd/media_player.py | 4 ++- .../components/gstreamer/media_player.py | 4 ++- homeassistant/components/heos/media_player.py | 4 ++- homeassistant/components/kodi/media_player.py | 4 ++- homeassistant/components/mpd/media_player.py | 4 ++- .../components/openhome/media_player.py | 4 ++- .../panasonic_viera/media_player.py | 4 ++- homeassistant/components/roku/media_player.py | 4 ++- .../components/slimproto/media_player.py | 4 ++- .../components/sonos/media_player.py | 4 ++- .../components/soundtouch/media_player.py | 4 ++- .../components/squeezebox/media_player.py | 4 ++- .../components/unifiprotect/media_player.py | 4 ++- homeassistant/components/vlc/media_player.py | 4 ++- .../components/vlc_telnet/media_player.py | 4 ++- .../yamaha_musiccast/media_player.py | 4 ++- tests/components/camera/test_media_source.py | 10 ++++---- .../dlna_dms/test_device_availability.py | 10 ++++---- .../dlna_dms/test_dms_device_source.py | 2 +- .../components/dlna_dms/test_media_source.py | 12 ++++----- tests/components/google_translate/test_tts.py | 2 +- tests/components/marytts/test_tts.py | 2 +- tests/components/media_source/test_init.py | 11 +++++--- .../components/motioneye/test_media_source.py | 15 ++++++++--- tests/components/nest/test_media_source.py | 25 +++++++++++-------- tests/components/netatmo/test_media_source.py | 2 +- tests/components/tts/test_init.py | 2 +- tests/components/tts/test_media_source.py | 14 +++++++---- tests/components/voicerss/test_tts.py | 2 +- tests/components/yandextts/test_tts.py | 2 +- 35 files changed, 128 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 5a7298dcbee..30a397d953c 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -284,7 +284,9 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): await self.atv.apps.launch_app(media_id) if media_source.is_media_source_id(media_id): - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = play_item.url media_type = MEDIA_TYPE_MUSIC diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 4fe89d84cf1..7f1c6b6553f 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -1025,7 +1025,9 @@ class BluesoundPlayer(MediaPlayerEntity): return if media_source.is_media_source_id(media_id): - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = play_item.url media_id = async_process_play_media_url(self.hass, media_id) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index b64c3372c15..ea21259ccc4 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -605,7 +605,9 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): """Play a piece of media.""" # Handle media_source if media_source.is_media_source_id(media_id): - sourced_media = await media_source.async_resolve_media(self.hass, media_id) + sourced_media = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_type = sourced_media.mime_type media_id = sourced_media.url diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index fd1fc9b2bab..9ecf9f8ad40 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -597,7 +597,9 @@ class DlnaDmrEntity(MediaPlayerEntity): # If media is media_source, resolve it to url and MIME type, and maybe metadata if media_source.is_media_source_id(media_id): - sourced_media = await media_source.async_resolve_media(self.hass, media_id) + sourced_media = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_type = sourced_media.mime_type media_id = sourced_media.url _LOGGER.debug("sourced_media is %s", sourced_media) diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index 6e83d12a427..f9027142ae2 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -95,7 +95,9 @@ class EsphomeMediaPlayer( ) -> None: """Send the play command with media url to the media player.""" if media_source.is_media_source_id(media_id): - sourced_media = await media_source.async_resolve_media(self.hass, media_id) + sourced_media = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = sourced_media.url media_id = async_process_play_media_url(self.hass, media_id) diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index f2c64fa81da..25695dceeb5 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -666,7 +666,9 @@ class ForkedDaapdMaster(MediaPlayerEntity): """Play a URI.""" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_MUSIC - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = play_item.url if media_type == MEDIA_TYPE_MUSIC: diff --git a/homeassistant/components/gstreamer/media_player.py b/homeassistant/components/gstreamer/media_player.py index 545941f2924..723be2880ff 100644 --- a/homeassistant/components/gstreamer/media_player.py +++ b/homeassistant/components/gstreamer/media_player.py @@ -96,7 +96,9 @@ class GstreamerDevice(MediaPlayerEntity): """Play media.""" # Handle media_source if media_source.is_media_source_id(media_id): - sourced_media = await media_source.async_resolve_media(self.hass, media_id) + sourced_media = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = sourced_media.url elif media_type != MEDIA_TYPE_MUSIC: diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 29a9b2b2a18..ad9225d9b21 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -201,7 +201,9 @@ class HeosMediaPlayer(MediaPlayerEntity): """Play a piece of media.""" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_URL - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = play_item.url if media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_MUSIC): diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index cea3adcde00..e19ffc6219c 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -713,7 +713,9 @@ class KodiEntity(MediaPlayerEntity): """Send the play_media command to the media player.""" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_URL - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = play_item.url media_type_lower = media_type.lower() diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index d3262a0d5da..ecee057a653 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -453,7 +453,9 @@ class MpdDevice(MediaPlayerEntity): """Send the media player the command for playing a playlist.""" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_MUSIC - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = async_process_play_media_url(self.hass, play_item.url) if media_type == MEDIA_TYPE_PLAYLIST: diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index fa9cce1cfb6..b6a0b549c40 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -209,7 +209,9 @@ class OpenhomeDevice(MediaPlayerEntity): """Send the play_media command to the media player.""" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_MUSIC - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = play_item.url if media_type != MEDIA_TYPE_MUSIC: diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index fd44c2853f1..7b75809f827 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -188,7 +188,9 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): """Play media.""" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_URL - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = play_item.url if media_type != MEDIA_TYPE_URL: diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index e6fe0d7dcf5..a47432694dd 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -384,7 +384,9 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): # Handle media_source if media_source.is_media_source_id(media_id): - sourced_media = await media_source.async_resolve_media(self.hass, media_id) + sourced_media = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_type = MEDIA_TYPE_URL media_id = sourced_media.url mime_type = sourced_media.mime_type diff --git a/homeassistant/components/slimproto/media_player.py b/homeassistant/components/slimproto/media_player.py index 6b1989830e2..2f85aa4b9df 100644 --- a/homeassistant/components/slimproto/media_player.py +++ b/homeassistant/components/slimproto/media_player.py @@ -180,7 +180,9 @@ class SlimProtoPlayer(MediaPlayerEntity): to_send_media_type: str | None = media_type # Handle media_source if media_source.is_media_source_id(media_id): - sourced_media = await media_source.async_resolve_media(self.hass, media_id) + sourced_media = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = sourced_media.url to_send_media_type = sourced_media.mime_type diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index e2a63a86b06..fd37e546105 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -550,7 +550,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): media_type = MEDIA_TYPE_MUSIC media_id = ( run_coroutine_threadsafe( - media_source.async_resolve_media(self.hass, media_id), + media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ), self.hass.loop, ) .result() diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index 3172eb4aed6..7c9ade3bee1 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -357,7 +357,9 @@ class SoundTouchDevice(MediaPlayerEntity): async def async_play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" if media_source.is_media_source_id(media_id): - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = async_process_play_media_url(self.hass, play_item.url) await self.hass.async_add_executor_job( diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index eda742281ee..cd628a639c5 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -484,7 +484,9 @@ class SqueezeBoxEntity(MediaPlayerEntity): if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_MUSIC - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = play_item.url if media_type in MEDIA_TYPE_MUSIC: diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index 0b7c2a2f60d..1acd14be130 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -118,7 +118,9 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): """Play a piece of media.""" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_MUSIC - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = async_process_play_media_url(self.hass, play_item.url) if media_type != MEDIA_TYPE_MUSIC: diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py index 7312eacd1c6..88b663e09c6 100644 --- a/homeassistant/components/vlc/media_player.py +++ b/homeassistant/components/vlc/media_player.py @@ -168,7 +168,9 @@ class VlcDevice(MediaPlayerEntity): """Play media from a URL or file.""" # Handle media_source if media_source.is_media_source_id(media_id): - sourced_media = await media_source.async_resolve_media(self.hass, media_id) + sourced_media = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = sourced_media.url elif media_type != MEDIA_TYPE_MUSIC: diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 89fa1a3c323..75305acbb0c 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -296,7 +296,9 @@ class VlcDevice(MediaPlayerEntity): """Play media from a URL or file.""" # Handle media_source if media_source.is_media_source_id(media_id): - sourced_media = await media_source.async_resolve_media(self.hass, media_id) + sourced_media = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_type = sourced_media.mime_type media_id = sourced_media.url diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index d0141977f29..954942b2c6b 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -275,7 +275,9 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> None: """Play media.""" if media_source.is_media_source_id(media_id): - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = play_item.url if self.state == STATE_OFF: diff --git a/tests/components/camera/test_media_source.py b/tests/components/camera/test_media_source.py index b7c273bb23a..4134e9b1151 100644 --- a/tests/components/camera/test_media_source.py +++ b/tests/components/camera/test_media_source.py @@ -62,7 +62,7 @@ async def test_resolving(hass, mock_camera_hls): return_value="http://example.com/stream", ): item = await media_source.async_resolve_media( - hass, "media-source://camera/camera.demo_camera" + hass, "media-source://camera/camera.demo_camera", None ) assert item is not None assert item.url == "http://example.com/stream" @@ -74,7 +74,7 @@ async def test_resolving_errors(hass, mock_camera_hls): with pytest.raises(media_source.Unresolvable) as exc_info: await media_source.async_resolve_media( - hass, "media-source://camera/camera.demo_camera" + hass, "media-source://camera/camera.demo_camera", None ) assert str(exc_info.value) == "Stream integration not loaded" @@ -82,7 +82,7 @@ async def test_resolving_errors(hass, mock_camera_hls): with pytest.raises(media_source.Unresolvable) as exc_info: await media_source.async_resolve_media( - hass, "media-source://camera/camera.non_existing" + hass, "media-source://camera/camera.non_existing", None ) assert str(exc_info.value) == "Could not resolve media item: camera.non_existing" @@ -91,13 +91,13 @@ async def test_resolving_errors(hass, mock_camera_hls): new_callable=PropertyMock(return_value=StreamType.WEB_RTC), ): await media_source.async_resolve_media( - hass, "media-source://camera/camera.demo_camera" + hass, "media-source://camera/camera.demo_camera", None ) assert str(exc_info.value) == "Camera does not support MJPEG or HLS streaming." with pytest.raises(media_source.Unresolvable) as exc_info: await media_source.async_resolve_media( - hass, "media-source://camera/camera.demo_camera" + hass, "media-source://camera/camera.demo_camera", None ) assert ( str(exc_info.value) == "camera.demo_camera does not support play stream service" diff --git a/tests/components/dlna_dms/test_device_availability.py b/tests/components/dlna_dms/test_device_availability.py index 67ad1024709..a3ec5326f00 100644 --- a/tests/components/dlna_dms/test_device_availability.py +++ b/tests/components/dlna_dms/test_device_availability.py @@ -152,15 +152,15 @@ async def test_unavailable_device( ) with pytest.raises(Unresolvable, match="DMS is not connected"): await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}//resolve_path" + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}//resolve_path", None ) with pytest.raises(Unresolvable, match="DMS is not connected"): await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:resolve_object" + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:resolve_object", None ) with pytest.raises(Unresolvable): await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/?resolve_search" + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/?resolve_search", None ) @@ -651,7 +651,7 @@ async def test_become_unavailable( # Check async_resolve_object currently works assert await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:object_id" + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:object_id", None ) # Now break the network connection @@ -660,7 +660,7 @@ async def test_become_unavailable( # async_resolve_object should fail with pytest.raises(Unresolvable): await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:object_id" + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:object_id", None ) # The device should now be unavailable diff --git a/tests/components/dlna_dms/test_dms_device_source.py b/tests/components/dlna_dms/test_dms_device_source.py index 5e4021a5dda..622a3b8a4f9 100644 --- a/tests/components/dlna_dms/test_dms_device_source.py +++ b/tests/components/dlna_dms/test_dms_device_source.py @@ -45,7 +45,7 @@ async def async_resolve_media( ) -> DidlPlayMedia: """Call media_source.async_resolve_media with the test source's ID.""" result = await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/{media_content_id}" + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/{media_content_id}", None ) assert isinstance(result, DidlPlayMedia) return result diff --git a/tests/components/dlna_dms/test_media_source.py b/tests/components/dlna_dms/test_media_source.py index 5f76b061590..35f34d0689b 100644 --- a/tests/components/dlna_dms/test_media_source.py +++ b/tests/components/dlna_dms/test_media_source.py @@ -60,31 +60,31 @@ async def test_resolve_media_bad_identifier( """Test trying to resolve an item that has an unresolvable identifier.""" # Empty identifier with pytest.raises(Unresolvable, match="No source ID.*"): - await media_source.async_resolve_media(hass, f"media-source://{DOMAIN}") + await media_source.async_resolve_media(hass, f"media-source://{DOMAIN}", None) # Identifier has media_id but no source_id # media_source.URI_SCHEME_REGEX won't let the ID through to dlna_dms with pytest.raises(Unresolvable, match="Invalid media source URI"): await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}//media_id" + hass, f"media-source://{DOMAIN}//media_id", None ) # Identifier has source_id but no media_id with pytest.raises(Unresolvable, match="No media ID.*"): await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/source_id/" + hass, f"media-source://{DOMAIN}/source_id/", None ) # Identifier is missing source_id/media_id separator with pytest.raises(Unresolvable, match="No media ID.*"): await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/source_id" + hass, f"media-source://{DOMAIN}/source_id", None ) # Identifier has an unknown source_id with pytest.raises(Unresolvable, match="Unknown source ID: unknown_source"): await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/unknown_source/media_id" + hass, f"media-source://{DOMAIN}/unknown_source/media_id", None ) @@ -105,7 +105,7 @@ async def test_resolve_media_success( dms_device_mock.async_browse_metadata.return_value = didl_item result = await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:{object_id}" + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:{object_id}", None ) assert isinstance(result, DidlPlayMedia) assert result.url == f"{MOCK_DEVICE_BASE_URL}/{res_url}" diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index c81cea57090..cc80d9c64b9 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -25,7 +25,7 @@ async def get_media_source_url(hass, media_content_id): if media_source.DOMAIN not in hass.config.components: assert await async_setup_component(hass, media_source.DOMAIN, {}) - resolved = await media_source.async_resolve_media(hass, media_content_id) + resolved = await media_source.async_resolve_media(hass, media_content_id, None) return resolved.url diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index 843b6578746..60211f7dc0c 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -21,7 +21,7 @@ async def get_media_source_url(hass, media_content_id): if media_source.DOMAIN not in hass.config.components: assert await async_setup_component(hass, media_source.DOMAIN, {}) - resolved = await media_source.async_resolve_media(hass, media_content_id) + resolved = await media_source.async_resolve_media(hass, media_content_id, None) return resolved.url diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index f2a8ff13533..33dd263c46c 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -103,6 +103,7 @@ async def test_async_resolve_media(hass): media = await media_source.async_resolve_media( hass, media_source.generate_media_source_id(media_source.DOMAIN, "local/test.mp3"), + None, ) assert isinstance(media, media_source.models.PlayMedia) assert media.url == "/media/local/test.mp3" @@ -135,15 +136,17 @@ async def test_async_unresolve_media(hass): # Test no media content with pytest.raises(media_source.Unresolvable): - await media_source.async_resolve_media(hass, "") + await media_source.async_resolve_media(hass, "", None) # Test invalid media content with pytest.raises(media_source.Unresolvable): - await media_source.async_resolve_media(hass, "invalid") + await media_source.async_resolve_media(hass, "invalid", None) # Test invalid media source with pytest.raises(media_source.Unresolvable): - await media_source.async_resolve_media(hass, "media-source://media_source2") + await media_source.async_resolve_media( + hass, "media-source://media_source2", None + ) async def test_websocket_browse_media(hass, hass_ws_client): @@ -261,4 +264,4 @@ async def test_browse_resolve_without_setup(): await media_source.async_browse_media(Mock(data={}), None) with pytest.raises(media_source.Unresolvable): - await media_source.async_resolve_media(Mock(data={}), None) + await media_source.async_resolve_media(Mock(data={}), None, None) diff --git a/tests/components/motioneye/test_media_source.py b/tests/components/motioneye/test_media_source.py index 9b86b783d43..2cf31c21da7 100644 --- a/tests/components/motioneye/test_media_source.py +++ b/tests/components/motioneye/test_media_source.py @@ -367,6 +367,7 @@ async def test_async_resolve_media_success(hass: HomeAssistant) -> None: f"{const.URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{device.id}#movies#/foo.mp4" ), + None, ) assert media == PlayMedia(url="http://movie-url", mime_type="video/mp4") assert client.get_movie_url.call_args == call(TEST_CAMERA_ID, "/foo.mp4") @@ -379,6 +380,7 @@ async def test_async_resolve_media_success(hass: HomeAssistant) -> None: f"{const.URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{device.id}#images#/foo.jpg" ), + None, ) assert media == PlayMedia(url="http://image-url", mime_type="image/jpeg") assert client.get_image_url.call_args == call(TEST_CAMERA_ID, "/foo.jpg") @@ -409,18 +411,20 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: # URI doesn't contain necessary components. with pytest.raises(Unresolvable): - await media_source.async_resolve_media(hass, f"{const.URI_SCHEME}{DOMAIN}/foo") + await media_source.async_resolve_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/foo", None + ) # Config entry doesn't exist. with pytest.raises(MediaSourceError): await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/1#2#3#4" + hass, f"{const.URI_SCHEME}{DOMAIN}/1#2#3#4", None ) # Device doesn't exist. with pytest.raises(MediaSourceError): await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{TEST_CONFIG_ENTRY_ID}#2#3#4" + hass, f"{const.URI_SCHEME}{DOMAIN}/{TEST_CONFIG_ENTRY_ID}#2#3#4", None ) # Device identifiers are incorrect (no camera id) @@ -431,6 +435,7 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: f"{const.URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{broken_device_1.id}#images#4" ), + None, ) # Device identifiers are incorrect (non integer camera id) @@ -441,6 +446,7 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: f"{const.URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{broken_device_2.id}#images#4" ), + None, ) # Kind is incorrect. @@ -448,6 +454,7 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: await media_source.async_resolve_media( hass, f"{const.URI_SCHEME}{DOMAIN}/{TEST_CONFIG_ENTRY_ID}#{device.id}#games#moo", + None, ) # Playback URL raises exception. @@ -459,6 +466,7 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: f"{const.URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{device.id}#movies#/foo.mp4" ), + None, ) # Media path does not start with '/' @@ -470,6 +478,7 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: f"{const.URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{device.id}#movies#foo.mp4" ), + None, ) # Media missing path. diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 1536d0bee1e..09a3f9f625c 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -361,7 +361,7 @@ async def test_camera_event(hass, auth, hass_client): # Resolving the event links to the media media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier}" assert media.mime_type == "image/jpeg" @@ -374,7 +374,7 @@ async def test_camera_event(hass, auth, hass_client): # Resolving the device id points to the most recent event media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier}" assert media.mime_type == "image/jpeg" @@ -535,7 +535,7 @@ async def test_multiple_image_events_in_session(hass, auth, hass_client): # Resolve the most recent event media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier2}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier2}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier2}" assert media.mime_type == "image/jpeg" @@ -548,7 +548,7 @@ async def test_multiple_image_events_in_session(hass, auth, hass_client): # Resolving the event links to the media media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier1}" assert media.mime_type == "image/jpeg" @@ -632,7 +632,7 @@ async def test_multiple_clip_preview_events_in_session(hass, auth, hass_client): # to the same clip preview media clip object. # Resolve media for the first event media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier1}" assert media.mime_type == "video/mp4" @@ -645,7 +645,7 @@ async def test_multiple_clip_preview_events_in_session(hass, auth, hass_client): # Resolve media for the second event media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier1}" assert media.mime_type == "video/mp4" @@ -712,6 +712,7 @@ async def test_resolve_missing_event_id(hass, auth): await media_source.async_resolve_media( hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}", + None, ) @@ -723,6 +724,7 @@ async def test_resolve_invalid_device_id(hass, auth): await media_source.async_resolve_media( hass, f"{const.URI_SCHEME}{DOMAIN}/invalid-device-id/GXXWRWVeHNUlUU3V3MGV3bUOYW...", + None, ) @@ -740,6 +742,7 @@ async def test_resolve_invalid_event_id(hass, auth): media = await media_source.async_resolve_media( hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/GXXWRWVeHNUlUU3V3MGV3bUOYW...", + None, ) assert ( media.url == f"/api/nest/event_media/{device.id}/GXXWRWVeHNUlUU3V3MGV3bUOYW..." @@ -835,7 +838,7 @@ async def test_camera_event_clip_preview(hass, auth, hass_client, mp4): # Resolving the event links to the media media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier}" assert media.mime_type == "video/mp4" @@ -921,7 +924,7 @@ async def test_event_media_failure(hass, auth, hass_client): # Resolving the event links to the media media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier}" assert media.mime_type == "image/jpeg" @@ -1128,7 +1131,7 @@ async def test_media_store_persistence(hass, auth, hass_client, event_store): event_identifier = browse.children[0].identifier media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{event_identifier}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{event_identifier}", None ) assert media.url == f"/api/nest/event_media/{event_identifier}" assert media.mime_type == "video/mp4" @@ -1182,7 +1185,7 @@ async def test_media_store_persistence(hass, auth, hass_client, event_store): event_identifier = browse.children[0].identifier media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{event_identifier}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{event_identifier}", None ) assert media.url == f"/api/nest/event_media/{event_identifier}" assert media.mime_type == "video/mp4" @@ -1234,7 +1237,7 @@ async def test_media_store_save_filesystem_error(hass, auth, hass_client): event = browse.children[0] media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{event.identifier}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{event.identifier}", None ) assert media.url == f"/api/nest/event_media/{event.identifier}" assert media.mime_type == "video/mp4" diff --git a/tests/components/netatmo/test_media_source.py b/tests/components/netatmo/test_media_source.py index db1a79145b4..390da95496a 100644 --- a/tests/components/netatmo/test_media_source.py +++ b/tests/components/netatmo/test_media_source.py @@ -79,7 +79,7 @@ async def test_async_browse_media(hass): # Test successful event resolve media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672" + hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672", None ) assert media == PlayMedia( url="http:///files/high/index.m3u8", mime_type="application/x-mpegURL" diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 7fd8cc0facb..78fa49a8fc9 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -29,7 +29,7 @@ async def get_media_source_url(hass, media_content_id): if media_source.DOMAIN not in hass.config.components: assert await async_setup_component(hass, media_source.DOMAIN, {}) - resolved = await media_source.async_resolve_media(hass, media_content_id) + resolved = await media_source.async_resolve_media(hass, media_content_id, None) return resolved.url diff --git a/tests/components/tts/test_media_source.py b/tests/components/tts/test_media_source.py index 22edfef5358..8af1ad9d3bb 100644 --- a/tests/components/tts/test_media_source.py +++ b/tests/components/tts/test_media_source.py @@ -68,7 +68,7 @@ async def test_browsing(hass): async def test_resolving(hass, mock_get_tts_audio): """Test resolving.""" media = await media_source.async_resolve_media( - hass, "media-source://tts/demo?message=Hello%20World" + hass, "media-source://tts/demo?message=Hello%20World", None ) assert media.url.startswith("/api/tts_proxy/") assert media.mime_type == "audio/mpeg" @@ -82,7 +82,9 @@ async def test_resolving(hass, mock_get_tts_audio): # Pass language and options mock_get_tts_audio.reset_mock() media = await media_source.async_resolve_media( - hass, "media-source://tts/demo?message=Bye%20World&language=de&voice=Paulus" + hass, + "media-source://tts/demo?message=Bye%20World&language=de&voice=Paulus", + None, ) assert media.url.startswith("/api/tts_proxy/") assert media.mime_type == "audio/mpeg" @@ -98,16 +100,18 @@ async def test_resolving_errors(hass): """Test resolving.""" # No message added with pytest.raises(media_source.Unresolvable): - await media_source.async_resolve_media(hass, "media-source://tts/demo") + await media_source.async_resolve_media(hass, "media-source://tts/demo", None) # Non-existing provider with pytest.raises(media_source.Unresolvable): await media_source.async_resolve_media( - hass, "media-source://tts/non-existing?message=bla" + hass, "media-source://tts/non-existing?message=bla", None ) # Non-existing option with pytest.raises(media_source.Unresolvable): await media_source.async_resolve_media( - hass, "media-source://tts/non-existing?message=bla&non_existing_option=bla" + hass, + "media-source://tts/non-existing?message=bla&non_existing_option=bla", + None, ) diff --git a/tests/components/voicerss/test_tts.py b/tests/components/voicerss/test_tts.py index 3e74d9dc815..099b280625f 100644 --- a/tests/components/voicerss/test_tts.py +++ b/tests/components/voicerss/test_tts.py @@ -33,7 +33,7 @@ async def get_media_source_url(hass, media_content_id): if media_source.DOMAIN not in hass.config.components: assert await async_setup_component(hass, media_source.DOMAIN, {}) - resolved = await media_source.async_resolve_media(hass, media_content_id) + resolved = await media_source.async_resolve_media(hass, media_content_id, None) return resolved.url diff --git a/tests/components/yandextts/test_tts.py b/tests/components/yandextts/test_tts.py index fdc204384a5..8549b51c341 100644 --- a/tests/components/yandextts/test_tts.py +++ b/tests/components/yandextts/test_tts.py @@ -27,7 +27,7 @@ async def get_media_source_url(hass, media_content_id): if media_source.DOMAIN not in hass.config.components: assert await async_setup_component(hass, media_source.DOMAIN, {}) - resolved = await media_source.async_resolve_media(hass, media_content_id) + resolved = await media_source.async_resolve_media(hass, media_content_id, None) return resolved.url From 2a1405c4bdc908520bcf4eab0e15a597b27f90d4 Mon Sep 17 00:00:00 2001 From: xLarry Date: Fri, 27 May 2022 18:19:18 +0200 Subject: [PATCH 0962/3516] Bump laundrify_aio to v1.1.2 (#72605) --- homeassistant/components/laundrify/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/laundrify/manifest.json b/homeassistant/components/laundrify/manifest.json index 6a61446d31c..a5737b9cf97 100644 --- a/homeassistant/components/laundrify/manifest.json +++ b/homeassistant/components/laundrify/manifest.json @@ -3,7 +3,7 @@ "name": "laundrify", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/laundrify", - "requirements": ["laundrify_aio==1.1.1"], + "requirements": ["laundrify_aio==1.1.2"], "codeowners": ["@xLarry"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 7e299929590..ee11241aa51 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -927,7 +927,7 @@ krakenex==2.1.0 lakeside==0.12 # homeassistant.components.laundrify -laundrify_aio==1.1.1 +laundrify_aio==1.1.2 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 25cba8501e9..4a8d2c99107 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -655,7 +655,7 @@ kostal_plenticore==0.2.0 krakenex==2.1.0 # homeassistant.components.laundrify -laundrify_aio==1.1.1 +laundrify_aio==1.1.2 # homeassistant.components.foscam libpyfoscam==1.0 From d59258bd2553ea1c89dd203590cb7e521d6d2000 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 27 May 2022 10:30:40 -0700 Subject: [PATCH 0963/3516] Revert "Add service entity context (#71558)" (#72610) --- homeassistant/helpers/service.py | 11 ----------- tests/helpers/test_service.py | 16 +--------------- 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 9a1e6caa27e..bc3451c24c0 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable, Iterable -from contextvars import ContextVar import dataclasses from functools import partial, wraps import logging @@ -64,15 +63,6 @@ _LOGGER = logging.getLogger(__name__) SERVICE_DESCRIPTION_CACHE = "service_description_cache" -_current_entity: ContextVar[str | None] = ContextVar("current_entity", default=None) - - -@callback -def async_get_current_entity() -> str | None: - """Get the current entity on which the service is called.""" - return _current_entity.get() - - class ServiceParams(TypedDict): """Type for service call parameters.""" @@ -716,7 +706,6 @@ async def _handle_entity_call( ) -> None: """Handle calling service method.""" entity.async_set_context(context) - _current_entity.set(entity.entity_id) if isinstance(func, str): result = hass.async_run_job(partial(getattr(entity, func), **data)) # type: ignore[arg-type] diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 76cf83e31bf..d08477dc917 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -19,12 +19,12 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.helpers import ( - config_validation as cv, device_registry as dev_reg, entity_registry as ent_reg, service, template, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import EntityCategory from homeassistant.setup import async_setup_component @@ -1206,17 +1206,3 @@ async def test_async_extract_config_entry_ids(hass): ) assert await service.async_extract_config_entry_ids(hass, call) == {"abc"} - - -async def test_current_entity_context(hass, mock_entities): - """Test we set the current entity context var.""" - - async def mock_service(entity, call): - assert entity.entity_id == service.async_get_current_entity() - - await service.entity_service_call( - hass, - [Mock(entities=mock_entities)], - mock_service, - ha.ServiceCall("test_domain", "test_service", {"entity_id": "light.kitchen"}), - ) From a733b92389f41034e547f7f8d2adb650ae3499ae Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 27 May 2022 10:31:48 -0700 Subject: [PATCH 0964/3516] Include provider type in auth token response (#72560) --- homeassistant/components/auth/__init__.py | 19 +++++++++++++++---- tests/components/auth/test_init.py | 1 + 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 1dc483eec6e..27fe511182d 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -19,13 +19,15 @@ Exchange the authorization code retrieved from the login flow for tokens. Return value will be the access and refresh tokens. The access token will have a limited expiration. New access tokens can be requested using the refresh -token. +token. The value ha_auth_provider will contain the auth provider type that was +used to authorize the refresh token. { "access_token": "ABCDEFGH", "expires_in": 1800, "refresh_token": "IJKLMNOPQRST", - "token_type": "Bearer" + "token_type": "Bearer", + "ha_auth_provider": "homeassistant" } ## Grant type refresh_token @@ -289,7 +291,12 @@ class TokenView(HomeAssistantView): "expires_in": int( refresh_token.access_token_expiration.total_seconds() ), - } + "ha_auth_provider": credential.auth_provider_type, + }, + headers={ + "Cache-Control": "no-store", + "Pragma": "no-cache", + }, ) async def _async_handle_refresh_token(self, hass, data, remote_addr): @@ -346,7 +353,11 @@ class TokenView(HomeAssistantView): "expires_in": int( refresh_token.access_token_expiration.total_seconds() ), - } + }, + headers={ + "Cache-Control": "no-store", + "Pragma": "no-cache", + }, ) diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index ef231950bd9..706221d9371 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -81,6 +81,7 @@ async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): assert ( await hass.auth.async_validate_access_token(tokens["access_token"]) is not None ) + assert tokens["ha_auth_provider"] == "insecure_example" # Use refresh token to get more tokens. resp = await client.post( From 040e120101ffbdd93dd686928d88a60849b68dfe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 May 2022 07:32:26 -1000 Subject: [PATCH 0965/3516] Fix recorder system health when the db_url is lacking a hostname (#72612) --- .../recorder/system_health/__init__.py | 5 ++- .../components/recorder/test_system_health.py | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/system_health/__init__.py b/homeassistant/components/recorder/system_health/__init__.py index 8ba68a1649b..c4bf2c3bb89 100644 --- a/homeassistant/components/recorder/system_health/__init__.py +++ b/homeassistant/components/recorder/system_health/__init__.py @@ -2,8 +2,7 @@ from __future__ import annotations from typing import Any - -from yarl import URL +from urllib.parse import urlparse from homeassistant.components import system_health from homeassistant.components.recorder.core import Recorder @@ -60,7 +59,7 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: instance = get_instance(hass) run_history = instance.run_history - database_name = URL(instance.db_url).path.lstrip("/") + database_name = urlparse(instance.db_url).path.lstrip("/") db_engine_info = _async_get_db_engine_info(instance) db_stats: dict[str, Any] = {} diff --git a/tests/components/recorder/test_system_health.py b/tests/components/recorder/test_system_health.py index 80997b9df36..b465ee89ebe 100644 --- a/tests/components/recorder/test_system_health.py +++ b/tests/components/recorder/test_system_health.py @@ -53,6 +53,37 @@ async def test_recorder_system_health_alternate_dbms(hass, recorder_mock, dialec } +@pytest.mark.parametrize( + "dialect_name", [SupportedDialect.MYSQL, SupportedDialect.POSTGRESQL] +) +async def test_recorder_system_health_db_url_missing_host( + hass, recorder_mock, dialect_name +): + """Test recorder system health with a db_url without a hostname.""" + assert await async_setup_component(hass, "system_health", {}) + await async_wait_recording_done(hass) + + instance = get_instance(hass) + with patch( + "homeassistant.components.recorder.core.Recorder.dialect_name", dialect_name + ), patch.object( + instance, + "db_url", + "postgresql://homeassistant:blabla@/home_assistant?host=/config/socket", + ), patch( + "sqlalchemy.orm.session.Session.execute", + return_value=Mock(first=Mock(return_value=("1048576",))), + ): + info = await get_system_health_info(hass, "recorder") + assert info == { + "current_recorder_run": instance.run_history.current.start, + "oldest_recorder_run": instance.run_history.first.start, + "estimated_db_size": "1.00 MiB", + "database_engine": dialect_name.value, + "database_version": ANY, + } + + async def test_recorder_system_health_crashed_recorder_runs_table( hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT ): From ea1e40a424a24a99883249f51e90651ec1bff1d1 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 27 May 2022 11:32:38 -0600 Subject: [PATCH 0966/3516] Bump regenmaschine to 2022.05.0 (#72613) --- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 331f191d029..bbe58e263b1 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.01.0"], + "requirements": ["regenmaschine==2022.05.0"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index ee11241aa51..a05e0641723 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2065,7 +2065,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.01.0 +regenmaschine==2022.05.0 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a8d2c99107..bfa4f2eb165 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1364,7 +1364,7 @@ rachiopy==1.0.3 radios==0.1.1 # homeassistant.components.rainmachine -regenmaschine==2022.01.0 +regenmaschine==2022.05.0 # homeassistant.components.renault renault-api==0.1.11 From 34323ce64542ed2b54af4032f1ba99df74f3bb18 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 May 2022 08:11:33 -1000 Subject: [PATCH 0967/3516] Add explict type casts for postgresql filters (#72615) --- homeassistant/components/recorder/filters.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 7f1d0bc597f..0a383d8ef2b 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -5,7 +5,7 @@ from collections.abc import Callable, Iterable import json from typing import Any -from sqlalchemy import Column, not_, or_ +from sqlalchemy import JSON, Column, Text, cast, not_, or_ from sqlalchemy.sql.elements import ClauseList from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE @@ -110,8 +110,7 @@ class Filters: """Generate the entity filter query.""" _encoder = json.dumps return or_( - (ENTITY_ID_IN_EVENT == _encoder(None)) - & (OLD_ENTITY_ID_IN_EVENT == _encoder(None)), + (ENTITY_ID_IN_EVENT == JSON.NULL) & (OLD_ENTITY_ID_IN_EVENT == JSON.NULL), self._generate_filter_for_columns( (ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT), _encoder ).self_group(), @@ -123,7 +122,7 @@ def _globs_to_like( ) -> ClauseList: """Translate glob to sql.""" return or_( - column.like(encoder(glob_str.translate(GLOB_TO_SQL_CHARS))) + cast(column, Text()).like(encoder(glob_str.translate(GLOB_TO_SQL_CHARS))) for glob_str in glob_strs for column in columns ) @@ -133,7 +132,7 @@ def _entity_matcher( entity_ids: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: return or_( - column.in_([encoder(entity_id) for entity_id in entity_ids]) + cast(column, Text()).in_([encoder(entity_id) for entity_id in entity_ids]) for column in columns ) @@ -142,5 +141,7 @@ def _domain_matcher( domains: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: return or_( - column.like(encoder(f"{domain}.%")) for domain in domains for column in columns + cast(column, Text()).like(encoder(f"{domain}.%")) + for domain in domains + for column in columns ) From 370e4c53f37c3ee839a9c4299b9301f4cbf12c36 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 26 May 2022 19:20:05 -0400 Subject: [PATCH 0968/3516] Add logbook entries for zwave_js events (#72508) * Add logbook entries for zwave_js events * Fix test * Update homeassistant/components/zwave_js/logbook.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/logbook.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/logbook.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/zwave_js/logbook.py Co-authored-by: Martin Hjelmare * black * Remove value updated event Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/__init__.py | 6 +- homeassistant/components/zwave_js/logbook.py | 115 +++++++++++++++ tests/components/zwave_js/test_events.py | 2 +- tests/components/zwave_js/test_logbook.py | 132 ++++++++++++++++++ 4 files changed, 251 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/zwave_js/logbook.py create mode 100644 tests/components/zwave_js/test_logbook.py diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 5c583d8321f..4f5756361c8 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -297,8 +297,8 @@ async def setup_driver( # noqa: C901 if not disc_info.assumed_state: return value_updates_disc_info[disc_info.primary_value.value_id] = disc_info - # If this is the first time we found a value we want to watch for updates, - # return early + # If this is not the first time we found a value we want to watch for updates, + # return early because we only need one listener for all values. if len(value_updates_disc_info) != 1: return # add listener for value updated events @@ -503,7 +503,7 @@ async def setup_driver( # noqa: C901 elif isinstance(notification, PowerLevelNotification): event_data.update( { - ATTR_COMMAND_CLASS_NAME: "Power Level", + ATTR_COMMAND_CLASS_NAME: "Powerlevel", ATTR_TEST_NODE_ID: notification.test_node_id, ATTR_STATUS: notification.status, ATTR_ACKNOWLEDGED_FRAMES: notification.acknowledged_frames, diff --git a/homeassistant/components/zwave_js/logbook.py b/homeassistant/components/zwave_js/logbook.py new file mode 100644 index 00000000000..1fe1ff79ec6 --- /dev/null +++ b/homeassistant/components/zwave_js/logbook.py @@ -0,0 +1,115 @@ +"""Describe Z-Wave JS logbook events.""" +from __future__ import annotations + +from collections.abc import Callable + +from zwave_js_server.const import CommandClass + +from homeassistant.components.logbook.const import ( + LOGBOOK_ENTRY_MESSAGE, + LOGBOOK_ENTRY_NAME, +) +from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.core import Event, HomeAssistant, callback +import homeassistant.helpers.device_registry as dr + +from .const import ( + ATTR_COMMAND_CLASS, + ATTR_COMMAND_CLASS_NAME, + ATTR_DATA_TYPE, + ATTR_DIRECTION, + ATTR_EVENT_LABEL, + ATTR_EVENT_TYPE, + ATTR_LABEL, + ATTR_VALUE, + DOMAIN, + ZWAVE_JS_NOTIFICATION_EVENT, + ZWAVE_JS_VALUE_NOTIFICATION_EVENT, +) + + +@callback +def async_describe_events( + hass: HomeAssistant, + async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None], +) -> None: + """Describe logbook events.""" + dev_reg = dr.async_get(hass) + + @callback + def async_describe_zwave_js_notification_event( + event: Event, + ) -> dict[str, str]: + """Describe Z-Wave JS notification event.""" + device = dev_reg.devices[event.data[ATTR_DEVICE_ID]] + # Z-Wave JS devices always have a name + device_name = device.name_by_user or device.name + assert device_name + + command_class = event.data[ATTR_COMMAND_CLASS] + command_class_name = event.data[ATTR_COMMAND_CLASS_NAME] + + data: dict[str, str] = {LOGBOOK_ENTRY_NAME: device_name} + prefix = f"fired {command_class_name} CC 'notification' event" + + if command_class == CommandClass.NOTIFICATION: + label = event.data[ATTR_LABEL] + event_label = event.data[ATTR_EVENT_LABEL] + return { + **data, + LOGBOOK_ENTRY_MESSAGE: f"{prefix} '{label}': '{event_label}'", + } + + if command_class == CommandClass.ENTRY_CONTROL: + event_type = event.data[ATTR_EVENT_TYPE] + data_type = event.data[ATTR_DATA_TYPE] + return { + **data, + LOGBOOK_ENTRY_MESSAGE: ( + f"{prefix} for event type '{event_type}' with data type " + f"'{data_type}'" + ), + } + + if command_class == CommandClass.SWITCH_MULTILEVEL: + event_type = event.data[ATTR_EVENT_TYPE] + direction = event.data[ATTR_DIRECTION] + return { + **data, + LOGBOOK_ENTRY_MESSAGE: ( + f"{prefix} for event type '{event_type}': '{direction}'" + ), + } + + return {**data, LOGBOOK_ENTRY_MESSAGE: prefix} + + @callback + def async_describe_zwave_js_value_notification_event( + event: Event, + ) -> dict[str, str]: + """Describe Z-Wave JS value notification event.""" + device = dev_reg.devices[event.data[ATTR_DEVICE_ID]] + # Z-Wave JS devices always have a name + device_name = device.name_by_user or device.name + assert device_name + + command_class = event.data[ATTR_COMMAND_CLASS_NAME] + label = event.data[ATTR_LABEL] + value = event.data[ATTR_VALUE] + + return { + LOGBOOK_ENTRY_NAME: device_name, + LOGBOOK_ENTRY_MESSAGE: ( + f"fired {command_class} CC 'value notification' event for '{label}': " + f"'{value}'" + ), + } + + async_describe_event( + DOMAIN, ZWAVE_JS_NOTIFICATION_EVENT, async_describe_zwave_js_notification_event + ) + async_describe_event( + DOMAIN, + ZWAVE_JS_VALUE_NOTIFICATION_EVENT, + async_describe_zwave_js_value_notification_event, + ) diff --git a/tests/components/zwave_js/test_events.py b/tests/components/zwave_js/test_events.py index 72da1fcb915..19f38d4aa57 100644 --- a/tests/components/zwave_js/test_events.py +++ b/tests/components/zwave_js/test_events.py @@ -312,7 +312,7 @@ async def test_power_level_notification(hass, hank_binary_switch, integration, c node.receive_event(event) await hass.async_block_till_done() assert len(events) == 1 - assert events[0].data["command_class_name"] == "Power Level" + assert events[0].data["command_class_name"] == "Powerlevel" assert events[0].data["command_class"] == 115 assert events[0].data["test_node_id"] == 1 assert events[0].data["status"] == 0 diff --git a/tests/components/zwave_js/test_logbook.py b/tests/components/zwave_js/test_logbook.py new file mode 100644 index 00000000000..eb02c1bbdcf --- /dev/null +++ b/tests/components/zwave_js/test_logbook.py @@ -0,0 +1,132 @@ +"""The tests for Z-Wave JS logbook.""" +from zwave_js_server.const import CommandClass + +from homeassistant.components.zwave_js.const import ( + ZWAVE_JS_NOTIFICATION_EVENT, + ZWAVE_JS_VALUE_NOTIFICATION_EVENT, +) +from homeassistant.components.zwave_js.helpers import get_device_id +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component + +from tests.components.logbook.common import MockRow, mock_humanify + + +async def test_humanifying_zwave_js_notification_event( + hass, client, lock_schlage_be469, integration +): + """Test humanifying Z-Wave JS notification events.""" + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_device( + identifiers={get_device_id(client.driver, lock_schlage_be469)} + ) + assert device + + hass.config.components.add("recorder") + assert await async_setup_component(hass, "logbook", {}) + + events = mock_humanify( + hass, + [ + MockRow( + ZWAVE_JS_NOTIFICATION_EVENT, + { + "device_id": device.id, + "command_class": CommandClass.NOTIFICATION.value, + "command_class_name": "Notification", + "label": "label", + "event_label": "event_label", + }, + ), + MockRow( + ZWAVE_JS_NOTIFICATION_EVENT, + { + "device_id": device.id, + "command_class": CommandClass.ENTRY_CONTROL.value, + "command_class_name": "Entry Control", + "event_type": 1, + "data_type": 2, + }, + ), + MockRow( + ZWAVE_JS_NOTIFICATION_EVENT, + { + "device_id": device.id, + "command_class": CommandClass.SWITCH_MULTILEVEL.value, + "command_class_name": "Multilevel Switch", + "event_type": 1, + "direction": "up", + }, + ), + MockRow( + ZWAVE_JS_NOTIFICATION_EVENT, + { + "device_id": device.id, + "command_class": CommandClass.POWERLEVEL.value, + "command_class_name": "Powerlevel", + }, + ), + ], + ) + + assert events[0]["name"] == "Touchscreen Deadbolt" + assert events[0]["domain"] == "zwave_js" + assert ( + events[0]["message"] + == "fired Notification CC 'notification' event 'label': 'event_label'" + ) + + assert events[1]["name"] == "Touchscreen Deadbolt" + assert events[1]["domain"] == "zwave_js" + assert ( + events[1]["message"] + == "fired Entry Control CC 'notification' event for event type '1' with data type '2'" + ) + + assert events[2]["name"] == "Touchscreen Deadbolt" + assert events[2]["domain"] == "zwave_js" + assert ( + events[2]["message"] + == "fired Multilevel Switch CC 'notification' event for event type '1': 'up'" + ) + + assert events[3]["name"] == "Touchscreen Deadbolt" + assert events[3]["domain"] == "zwave_js" + assert events[3]["message"] == "fired Powerlevel CC 'notification' event" + + +async def test_humanifying_zwave_js_value_notification_event( + hass, client, lock_schlage_be469, integration +): + """Test humanifying Z-Wave JS value notification events.""" + dev_reg = dr.async_get(hass) + device = dev_reg.async_get_device( + identifiers={get_device_id(client.driver, lock_schlage_be469)} + ) + assert device + + hass.config.components.add("recorder") + assert await async_setup_component(hass, "logbook", {}) + + events = mock_humanify( + hass, + [ + MockRow( + ZWAVE_JS_VALUE_NOTIFICATION_EVENT, + { + "device_id": device.id, + "command_class": CommandClass.SCENE_ACTIVATION.value, + "command_class_name": "Scene Activation", + "label": "Scene ID", + "value": "001", + }, + ), + ], + ) + + assert events[0]["name"] == "Touchscreen Deadbolt" + assert events[0]["domain"] == "zwave_js" + assert ( + events[0]["message"] + == "fired Scene Activation CC 'value notification' event for 'Scene ID': '001'" + ) From f8e300ffbe9318ff66fe977a0594ec85a97ffac7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 27 May 2022 10:31:48 -0700 Subject: [PATCH 0969/3516] Include provider type in auth token response (#72560) --- homeassistant/components/auth/__init__.py | 19 +++++++++++++++---- tests/components/auth/test_init.py | 1 + 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 30b36a40f32..10f974faa28 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -19,13 +19,15 @@ Exchange the authorization code retrieved from the login flow for tokens. Return value will be the access and refresh tokens. The access token will have a limited expiration. New access tokens can be requested using the refresh -token. +token. The value ha_auth_provider will contain the auth provider type that was +used to authorize the refresh token. { "access_token": "ABCDEFGH", "expires_in": 1800, "refresh_token": "IJKLMNOPQRST", - "token_type": "Bearer" + "token_type": "Bearer", + "ha_auth_provider": "homeassistant" } ## Grant type refresh_token @@ -342,7 +344,12 @@ class TokenView(HomeAssistantView): "expires_in": int( refresh_token.access_token_expiration.total_seconds() ), - } + "ha_auth_provider": credential.auth_provider_type, + }, + headers={ + "Cache-Control": "no-store", + "Pragma": "no-cache", + }, ) async def _async_handle_refresh_token(self, hass, data, remote_addr): @@ -399,7 +406,11 @@ class TokenView(HomeAssistantView): "expires_in": int( refresh_token.access_token_expiration.total_seconds() ), - } + }, + headers={ + "Cache-Control": "no-store", + "Pragma": "no-cache", + }, ) diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index f6d0695d97d..3c90d915966 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -81,6 +81,7 @@ async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): assert ( await hass.auth.async_validate_access_token(tokens["access_token"]) is not None ) + assert tokens["ha_auth_provider"] == "insecure_example" # Use refresh token to get more tokens. resp = await client.post( From 16ab7f2bb1ffbdf248c3a15721cfe1a24f06b278 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 27 May 2022 05:48:52 +0200 Subject: [PATCH 0970/3516] Update frontend to 20220526.0 (#72567) --- 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 6c8f568c4d2..48488bc8f47 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220525.0"], + "requirements": ["home-assistant-frontend==20220526.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5a6dc9891a6..309d4b89e9f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220525.0 +home-assistant-frontend==20220526.0 httpx==0.22.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 2404e4331a1..c7459285fa0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -822,7 +822,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220525.0 +home-assistant-frontend==20220526.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0153a1b3323..7233824a3cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -589,7 +589,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220525.0 +home-assistant-frontend==20220526.0 # homeassistant.components.home_connect homeconnect==0.7.0 From bd02c9e5b309b9d9f6078b4e26ea47f9c191e64d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 May 2022 22:15:20 -0700 Subject: [PATCH 0971/3516] Attach SSL context to SMTP notify and IMAP sensor (#72568) --- .../components/imap_email_content/sensor.py | 26 ++++++++++------ homeassistant/components/smtp/notify.py | 30 +++++++++++++------ tests/components/smtp/test_notify.py | 1 + 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/imap_email_content/sensor.py b/homeassistant/components/imap_email_content/sensor.py index d0d87e0b2d5..a8bd394a159 100644 --- a/homeassistant/components/imap_email_content/sensor.py +++ b/homeassistant/components/imap_email_content/sensor.py @@ -17,12 +17,14 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, CONF_VALUE_TEMPLATE, + CONF_VERIFY_SSL, CONTENT_TYPE_TEXT_PLAIN, ) 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.ssl import client_context _LOGGER = logging.getLogger(__name__) @@ -46,6 +48,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_FOLDER, default="INBOX"): cv.string, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, } ) @@ -58,11 +61,12 @@ def setup_platform( ) -> None: """Set up the Email sensor platform.""" reader = EmailReader( - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD), - config.get(CONF_SERVER), - config.get(CONF_PORT), - config.get(CONF_FOLDER), + config[CONF_USERNAME], + config[CONF_PASSWORD], + config[CONF_SERVER], + config[CONF_PORT], + config[CONF_FOLDER], + config[CONF_VERIFY_SSL], ) if (value_template := config.get(CONF_VALUE_TEMPLATE)) is not None: @@ -70,8 +74,8 @@ def setup_platform( sensor = EmailContentSensor( hass, reader, - config.get(CONF_NAME) or config.get(CONF_USERNAME), - config.get(CONF_SENDERS), + config.get(CONF_NAME) or config[CONF_USERNAME], + config[CONF_SENDERS], value_template, ) @@ -82,21 +86,25 @@ def setup_platform( class EmailReader: """A class to read emails from an IMAP server.""" - def __init__(self, user, password, server, port, folder): + def __init__(self, user, password, server, port, folder, verify_ssl): """Initialize the Email Reader.""" self._user = user self._password = password self._server = server self._port = port self._folder = folder + self._verify_ssl = verify_ssl self._last_id = None self._unread_ids = deque([]) self.connection = None def connect(self): """Login and setup the connection.""" + ssl_context = client_context() if self._verify_ssl else None try: - self.connection = imaplib.IMAP4_SSL(self._server, self._port) + self.connection = imaplib.IMAP4_SSL( + self._server, self._port, ssl_context=ssl_context + ) self.connection.login(self._user, self._password) return True except imaplib.IMAP4.error: diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index 7b8e2dad1ed..866d7980d08 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -25,10 +25,12 @@ from homeassistant.const import ( CONF_SENDER, CONF_TIMEOUT, CONF_USERNAME, + CONF_VERIFY_SSL, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import setup_reload_service import homeassistant.util.dt as dt_util +from homeassistant.util.ssl import client_context from . import DOMAIN, PLATFORMS @@ -65,6 +67,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_SENDER_NAME): cv.string, vol.Optional(CONF_DEBUG, default=DEFAULT_DEBUG): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, } ) @@ -73,16 +76,17 @@ def get_service(hass, config, discovery_info=None): """Get the mail notification service.""" setup_reload_service(hass, DOMAIN, PLATFORMS) mail_service = MailNotificationService( - config.get(CONF_SERVER), - config.get(CONF_PORT), - config.get(CONF_TIMEOUT), - config.get(CONF_SENDER), - config.get(CONF_ENCRYPTION), + config[CONF_SERVER], + config[CONF_PORT], + config[CONF_TIMEOUT], + config[CONF_SENDER], + config[CONF_ENCRYPTION], config.get(CONF_USERNAME), config.get(CONF_PASSWORD), - config.get(CONF_RECIPIENT), + config[CONF_RECIPIENT], config.get(CONF_SENDER_NAME), - config.get(CONF_DEBUG), + config[CONF_DEBUG], + config[CONF_VERIFY_SSL], ) if mail_service.connection_is_valid(): @@ -106,6 +110,7 @@ class MailNotificationService(BaseNotificationService): recipients, sender_name, debug, + verify_ssl, ): """Initialize the SMTP service.""" self._server = server @@ -118,18 +123,25 @@ class MailNotificationService(BaseNotificationService): self.recipients = recipients self._sender_name = sender_name self.debug = debug + self._verify_ssl = verify_ssl self.tries = 2 def connect(self): """Connect/authenticate to SMTP Server.""" + ssl_context = client_context() if self._verify_ssl else None if self.encryption == "tls": - mail = smtplib.SMTP_SSL(self._server, self._port, timeout=self._timeout) + mail = smtplib.SMTP_SSL( + self._server, + self._port, + timeout=self._timeout, + context=ssl_context, + ) else: mail = smtplib.SMTP(self._server, self._port, timeout=self._timeout) mail.set_debuglevel(self.debug) mail.ehlo_or_helo_if_needed() if self.encryption == "starttls": - mail.starttls() + mail.starttls(context=ssl_context) mail.ehlo() if self.username and self.password: mail.login(self.username, self.password) diff --git a/tests/components/smtp/test_notify.py b/tests/components/smtp/test_notify.py index 38f48c169ac..ac742e10ea1 100644 --- a/tests/components/smtp/test_notify.py +++ b/tests/components/smtp/test_notify.py @@ -76,6 +76,7 @@ def message(): ["recip1@example.com", "testrecip@test.com"], "Home Assistant", 0, + True, ) yield mailer From 828afd8c0559af30674fb1e4ec008661dec9e869 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 27 May 2022 05:51:24 +0200 Subject: [PATCH 0972/3516] fjaraskupan: Don't set hardware filters for service id (#72569) --- homeassistant/components/fjaraskupan/__init__.py | 4 ++-- homeassistant/components/fjaraskupan/config_flow.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 488139b080b..ec4528bc079 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 UUID_SERVICE, Device, State, device_filter +from fjaraskupan import DEVICE_NAME, Device, State, device_filter from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -90,7 +90,7 @@ class EntryState: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Fjäråskupan from a config entry.""" - scanner = BleakScanner(filters={"UUIDs": [str(UUID_SERVICE)]}) + scanner = BleakScanner(filters={"Pattern": DEVICE_NAME, "DuplicateData": True}) state = EntryState(scanner, {}) hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/fjaraskupan/config_flow.py b/homeassistant/components/fjaraskupan/config_flow.py index da0a7f1dd2b..3af34c0eef6 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 UUID_SERVICE, device_filter +from fjaraskupan import DEVICE_NAME, device_filter from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_flow import register_discovery_flow @@ -27,7 +27,8 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: event.set() async with BleakScanner( - detection_callback=detection, filters={"UUIDs": [str(UUID_SERVICE)]} + detection_callback=detection, + filters={"Pattern": DEVICE_NAME, "DuplicateData": True}, ): try: async with async_timeout.timeout(CONST_WAIT_TIME): From 9b779082d5ffdc31a4139b85a9699d34c49dbfde Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 May 2022 17:54:26 -1000 Subject: [PATCH 0973/3516] Fix memory leak when firing state_changed events (#72571) --- homeassistant/components/recorder/models.py | 2 +- homeassistant/core.py | 45 +++++++++++++++++---- tests/test_core.py | 45 +++++++++++++++++++++ 3 files changed, 84 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index dff8edde79f..70c816c2af5 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -746,7 +746,7 @@ class LazyState(State): def context(self) -> Context: # type: ignore[override] """State context.""" if self._context is None: - self._context = Context(id=None) # type: ignore[arg-type] + self._context = Context(id=None) return self._context @context.setter diff --git a/homeassistant/core.py b/homeassistant/core.py index 2753b801347..d7cae4e411e 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -37,7 +37,6 @@ from typing import ( ) from urllib.parse import urlparse -import attr import voluptuous as vol import yarl @@ -716,14 +715,26 @@ class HomeAssistant: self._stopped.set() -@attr.s(slots=True, frozen=False) class Context: """The context that triggered something.""" - user_id: str | None = attr.ib(default=None) - parent_id: str | None = attr.ib(default=None) - id: str = attr.ib(factory=ulid_util.ulid) - origin_event: Event | None = attr.ib(default=None, eq=False) + __slots__ = ("user_id", "parent_id", "id", "origin_event") + + def __init__( + self, + user_id: str | None = None, + parent_id: str | None = None, + id: str | None = None, # pylint: disable=redefined-builtin + ) -> None: + """Init the context.""" + self.id = id or ulid_util.ulid() + self.user_id = user_id + self.parent_id = parent_id + self.origin_event: Event | None = None + + def __eq__(self, other: Any) -> bool: + """Compare contexts.""" + return bool(self.__class__ == other.__class__ and self.id == other.id) def as_dict(self) -> dict[str, str | None]: """Return a dictionary representation of the context.""" @@ -1163,6 +1174,24 @@ class State: context, ) + def expire(self) -> None: + """Mark the state as old. + + We give up the original reference to the context to ensure + the context can be garbage collected by replacing it with + a new one with the same id to ensure the old state + can still be examined for comparison against the new state. + + Since we are always going to fire a EVENT_STATE_CHANGED event + after we remove a state from the state machine we need to make + sure we don't end up holding a reference to the original context + since it can never be garbage collected as each event would + reference the previous one. + """ + self.context = Context( + self.context.user_id, self.context.parent_id, self.context.id + ) + def __eq__(self, other: Any) -> bool: """Return the comparison of the state.""" return ( # type: ignore[no-any-return] @@ -1303,6 +1332,7 @@ class StateMachine: if old_state is None: return False + old_state.expire() self._bus.async_fire( EVENT_STATE_CHANGED, {"entity_id": entity_id, "old_state": old_state, "new_state": None}, @@ -1396,7 +1426,6 @@ class StateMachine: if context is None: context = Context(id=ulid_util.ulid(dt_util.utc_to_timestamp(now))) - state = State( entity_id, new_state, @@ -1406,6 +1435,8 @@ class StateMachine: context, old_state is None, ) + if old_state is not None: + old_state.expire() self._states[entity_id] = state self._bus.async_fire( EVENT_STATE_CHANGED, diff --git a/tests/test_core.py b/tests/test_core.py index ee1005a60b0..67513ea8b17 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -6,9 +6,11 @@ import array import asyncio from datetime import datetime, timedelta import functools +import gc import logging import os from tempfile import TemporaryDirectory +from typing import Any from unittest.mock import MagicMock, Mock, PropertyMock, patch import pytest @@ -1829,3 +1831,46 @@ async def test_event_context(hass): cancel2() assert dummy_event2.context.origin_event == dummy_event + + +def _get_full_name(obj) -> str: + """Get the full name of an object in memory.""" + objtype = type(obj) + name = objtype.__name__ + if module := getattr(objtype, "__module__", None): + return f"{module}.{name}" + return name + + +def _get_by_type(full_name: str) -> list[Any]: + """Get all objects in memory with a specific type.""" + return [obj for obj in gc.get_objects() if _get_full_name(obj) == full_name] + + +# The logger will hold a strong reference to the event for the life of the tests +# so we must patch it out +@pytest.mark.skipif( + not os.environ.get("DEBUG_MEMORY"), + reason="Takes too long on the CI", +) +@patch.object(ha._LOGGER, "debug", lambda *args: None) +async def test_state_changed_events_to_not_leak_contexts(hass): + """Test state changed events do not leak contexts.""" + gc.collect() + # Other tests can log Contexts which keep them in memory + # so we need to look at how many exist at the start + init_count = len(_get_by_type("homeassistant.core.Context")) + + assert len(_get_by_type("homeassistant.core.Context")) == init_count + for i in range(20): + hass.states.async_set("light.switch", str(i)) + await hass.async_block_till_done() + gc.collect() + + assert len(_get_by_type("homeassistant.core.Context")) == init_count + 2 + + hass.states.async_remove("light.switch") + await hass.async_block_till_done() + gc.collect() + + assert len(_get_by_type("homeassistant.core.Context")) == init_count From 0d03b850293836a055a726b51284b48df2c5ee82 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 27 May 2022 17:20:37 +1000 Subject: [PATCH 0974/3516] Bump httpx to 0.23.0 (#72573) Co-authored-by: J. Nick Koston --- homeassistant/package_constraints.txt | 6 +++--- requirements.txt | 2 +- script/gen_requirements_all.py | 4 ++-- setup.cfg | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 309d4b89e9f..f235cc3f02c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 home-assistant-frontend==20220526.0 -httpx==0.22.0 +httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.7 @@ -78,9 +78,9 @@ regex==2021.8.28 # these requirements are quite loose. As the entire stack has some outstanding issues, and # even newer versions seem to introduce new issues, it's useful for us to pin all these # requirements so we can directly link HA versions to these library versions. -anyio==3.5.0 +anyio==3.6.1 h11==0.12.0 -httpcore==0.14.7 +httpcore==0.15.0 # Ensure we have a hyperframe version that works in Python 3.10 # 5.2.0 fixed a collections abc deprecation diff --git a/requirements.txt b/requirements.txt index 0c13b9c319b..8321e70f8de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ awesomeversion==22.5.1 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 -httpx==0.22.0 +httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 PyJWT==2.4.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 369045e5124..f049080b7fa 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -94,9 +94,9 @@ regex==2021.8.28 # these requirements are quite loose. As the entire stack has some outstanding issues, and # even newer versions seem to introduce new issues, it's useful for us to pin all these # requirements so we can directly link HA versions to these library versions. -anyio==3.5.0 +anyio==3.6.1 h11==0.12.0 -httpcore==0.14.7 +httpcore==0.15.0 # Ensure we have a hyperframe version that works in Python 3.10 # 5.2.0 fixed a collections abc deprecation diff --git a/setup.cfg b/setup.cfg index c62390253dc..205fcd96086 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,7 @@ install_requires = ciso8601==2.2.0 # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all - httpx==0.22.0 + httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 PyJWT==2.4.0 From a35edc67516b0d123d5be9111db9a602ff4ecf2a Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 27 May 2022 02:52:24 -0700 Subject: [PATCH 0975/3516] Reduce the scope of the google calendar track deprecation (#72575) --- homeassistant/components/google/__init__.py | 1 - homeassistant/components/google/calendar.py | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index f034d48b9c5..b7263d2e469 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -117,7 +117,6 @@ CONFIG_SCHEMA = vol.Schema( _SINGLE_CALSEARCH_CONFIG = vol.All( cv.deprecated(CONF_MAX_RESULTS), - cv.deprecated(CONF_TRACK), vol.Schema( { vol.Required(CONF_NAME): cv.string, diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 01780702b7f..ba4368fefae 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -93,6 +93,11 @@ def _async_setup_entities( num_entities = len(disc_info[CONF_ENTITIES]) for data in disc_info[CONF_ENTITIES]: entity_enabled = data.get(CONF_TRACK, True) + if not entity_enabled: + _LOGGER.warning( + "The 'track' option in google_calendars.yaml has been deprecated. The setting " + "has been imported to the UI, and should now be removed from google_calendars.yaml" + ) entity_name = data[CONF_DEVICE_ID] entity_id = generate_entity_id(ENTITY_ID_FORMAT, entity_name, hass=hass) calendar_id = disc_info[CONF_CAL_ID] From 319275bbbd74102274390a843966d1d560dbb433 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 May 2022 22:15:43 -1000 Subject: [PATCH 0976/3516] Revert "Remove sqlite 3.34.1 downgrade workaround by reverting "Downgrade sqlite-libs on docker image (#55591)" (#72342)" (#72578) --- Dockerfile | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Dockerfile b/Dockerfile index 13552d55a3d..1d6ce675e74 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,21 @@ RUN \ -e ./homeassistant --use-deprecated=legacy-resolver \ && python3 -m compileall homeassistant/homeassistant +# Fix Bug with Alpine 3.14 and sqlite 3.35 +# https://gitlab.alpinelinux.org/alpine/aports/-/issues/12524 +ARG BUILD_ARCH +RUN \ + if [ "${BUILD_ARCH}" = "amd64" ]; then \ + export APK_ARCH=x86_64; \ + elif [ "${BUILD_ARCH}" = "i386" ]; then \ + export APK_ARCH=x86; \ + else \ + export APK_ARCH=${BUILD_ARCH}; \ + fi \ + && curl -O http://dl-cdn.alpinelinux.org/alpine/v3.13/main/${APK_ARCH}/sqlite-libs-3.34.1-r0.apk \ + && apk add --no-cache sqlite-libs-3.34.1-r0.apk \ + && rm -f sqlite-libs-3.34.1-r0.apk + # Home Assistant S6-Overlay COPY rootfs / From cc53ad12b3fbcdc491653d713906ffacaab4e5a0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 27 May 2022 15:09:43 +0200 Subject: [PATCH 0977/3516] Simplify MQTT PLATFORM_CONFIG_SCHEMA_BASE (#72589) --- homeassistant/components/mqtt/__init__.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e8847375584..78f64387435 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -190,26 +190,7 @@ MQTT_WILL_BIRTH_SCHEMA = vol.Schema( ) PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( - { - vol.Optional(Platform.ALARM_CONTROL_PANEL.value): cv.ensure_list, - vol.Optional(Platform.BINARY_SENSOR.value): cv.ensure_list, - vol.Optional(Platform.BUTTON.value): cv.ensure_list, - vol.Optional(Platform.CAMERA.value): cv.ensure_list, - vol.Optional(Platform.CLIMATE.value): cv.ensure_list, - vol.Optional(Platform.COVER.value): cv.ensure_list, - vol.Optional(Platform.DEVICE_TRACKER.value): cv.ensure_list, - vol.Optional(Platform.FAN.value): cv.ensure_list, - vol.Optional(Platform.HUMIDIFIER.value): cv.ensure_list, - vol.Optional(Platform.LIGHT.value): cv.ensure_list, - vol.Optional(Platform.LOCK.value): cv.ensure_list, - vol.Optional(Platform.NUMBER.value): cv.ensure_list, - vol.Optional(Platform.SCENE.value): cv.ensure_list, - vol.Optional(Platform.SELECT.value): cv.ensure_list, - vol.Optional(Platform.SIREN.value): cv.ensure_list, - vol.Optional(Platform.SENSOR.value): cv.ensure_list, - vol.Optional(Platform.SWITCH.value): cv.ensure_list, - vol.Optional(Platform.VACUUM.value): cv.ensure_list, - } + {vol.Optional(platform.value): cv.ensure_list for platform in PLATFORMS} ) CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend( From ad6529520160b94c45dbf0e4c2c20ec27ef79a52 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 27 May 2022 17:40:55 +0200 Subject: [PATCH 0978/3516] Require passing target player when resolving media (#72593) --- .../components/media_source/__init__.py | 23 +++++++++++++------ .../components/media_source/local_source.py | 4 ++-- .../components/media_source/models.py | 7 ++++-- .../components/dlna_dms/test_media_source.py | 8 +++---- tests/components/media_source/test_init.py | 19 +++++++++++++++ 5 files changed, 46 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/media_source/__init__.py b/homeassistant/components/media_source/__init__.py index 3c42016f8f7..4818934d1dd 100644 --- a/homeassistant/components/media_source/__init__.py +++ b/homeassistant/components/media_source/__init__.py @@ -18,10 +18,11 @@ from homeassistant.components.media_player.browse_media import ( ) from homeassistant.components.websocket_api import ActiveConnection from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.frame import report from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, ) -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType from homeassistant.loader import bind_hass from . import local_source @@ -80,15 +81,15 @@ async def _process_media_source_platform( @callback def _get_media_item( - hass: HomeAssistant, media_content_id: str | None + hass: HomeAssistant, media_content_id: str | None, target_media_player: str | None ) -> MediaSourceItem: """Return media item.""" if media_content_id: - item = MediaSourceItem.from_uri(hass, media_content_id) + item = MediaSourceItem.from_uri(hass, media_content_id, target_media_player) else: # We default to our own domain if its only one registered domain = None if len(hass.data[DOMAIN]) > 1 else DOMAIN - return MediaSourceItem(hass, domain, "") + return MediaSourceItem(hass, domain, "", target_media_player) if item.domain is not None and item.domain not in hass.data[DOMAIN]: raise ValueError("Unknown media source") @@ -108,7 +109,7 @@ async def async_browse_media( raise BrowseError("Media Source not loaded") try: - item = await _get_media_item(hass, media_content_id).async_browse() + item = await _get_media_item(hass, media_content_id, None).async_browse() except ValueError as err: raise BrowseError(str(err)) from err @@ -124,13 +125,21 @@ async def async_browse_media( @bind_hass -async def async_resolve_media(hass: HomeAssistant, media_content_id: str) -> PlayMedia: +async def async_resolve_media( + hass: HomeAssistant, + media_content_id: str, + target_media_player: str | None | UndefinedType = UNDEFINED, +) -> PlayMedia: """Get info to play media.""" if DOMAIN not in hass.data: raise Unresolvable("Media Source not loaded") + if target_media_player is UNDEFINED: + report("calls media_source.async_resolve_media without passing an entity_id") + target_media_player = None + try: - item = _get_media_item(hass, media_content_id) + item = _get_media_item(hass, media_content_id, target_media_player) except ValueError as err: raise Unresolvable(str(err)) from err diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index 89feba5317f..863380b7600 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -264,7 +264,7 @@ class UploadMediaView(http.HomeAssistantView): raise web.HTTPBadRequest() from err try: - item = MediaSourceItem.from_uri(self.hass, data["media_content_id"]) + item = MediaSourceItem.from_uri(self.hass, data["media_content_id"], None) except ValueError as err: LOGGER.error("Received invalid upload data: %s", err) raise web.HTTPBadRequest() from err @@ -328,7 +328,7 @@ async def websocket_remove_media( ) -> None: """Remove media.""" try: - item = MediaSourceItem.from_uri(hass, msg["media_content_id"]) + item = MediaSourceItem.from_uri(hass, msg["media_content_id"], None) except ValueError as err: connection.send_error(msg["id"], websocket_api.ERR_INVALID_FORMAT, str(err)) return diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index ceb57ef1fb4..0aee6ad1330 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -50,6 +50,7 @@ class MediaSourceItem: hass: HomeAssistant domain: str | None identifier: str + target_media_player: str | None async def async_browse(self) -> BrowseMediaSource: """Browse this item.""" @@ -94,7 +95,9 @@ class MediaSourceItem: return cast(MediaSource, self.hass.data[DOMAIN][self.domain]) @classmethod - def from_uri(cls, hass: HomeAssistant, uri: str) -> MediaSourceItem: + def from_uri( + cls, hass: HomeAssistant, uri: str, target_media_player: str | None + ) -> MediaSourceItem: """Create an item from a uri.""" if not (match := URI_SCHEME_REGEX.match(uri)): raise ValueError("Invalid media source URI") @@ -102,7 +105,7 @@ class MediaSourceItem: domain = match.group("domain") identifier = match.group("identifier") - return cls(hass, domain, identifier) + return cls(hass, domain, identifier, target_media_player) class MediaSource(ABC): diff --git a/tests/components/dlna_dms/test_media_source.py b/tests/components/dlna_dms/test_media_source.py index f2c3011e274..5f76b061590 100644 --- a/tests/components/dlna_dms/test_media_source.py +++ b/tests/components/dlna_dms/test_media_source.py @@ -49,7 +49,7 @@ async def test_get_media_source(hass: HomeAssistant) -> None: async def test_resolve_media_unconfigured(hass: HomeAssistant) -> None: """Test resolve_media without any devices being configured.""" source = DmsMediaSource(hass) - item = MediaSourceItem(hass, DOMAIN, "source_id/media_id") + item = MediaSourceItem(hass, DOMAIN, "source_id/media_id", None) with pytest.raises(Unresolvable, match="No sources have been configured"): await source.async_resolve_media(item) @@ -116,11 +116,11 @@ async def test_resolve_media_success( async def test_browse_media_unconfigured(hass: HomeAssistant) -> None: """Test browse_media without any devices being configured.""" source = DmsMediaSource(hass) - item = MediaSourceItem(hass, DOMAIN, "source_id/media_id") + item = MediaSourceItem(hass, DOMAIN, "source_id/media_id", None) with pytest.raises(BrowseError, match="No sources have been configured"): await source.async_browse_media(item) - item = MediaSourceItem(hass, DOMAIN, "") + item = MediaSourceItem(hass, DOMAIN, "", None) with pytest.raises(BrowseError, match="No sources have been configured"): await source.async_browse_media(item) @@ -239,7 +239,7 @@ async def test_browse_media_source_id( dms_device_mock.async_browse_metadata.side_effect = UpnpError # Browse by source_id - item = MediaSourceItem(hass, DOMAIN, f"{MOCK_SOURCE_ID}/:media-item-id") + item = MediaSourceItem(hass, DOMAIN, f"{MOCK_SOURCE_ID}/:media-item-id", None) dms_source = DmsMediaSource(hass) with pytest.raises(BrowseError): await dms_source.async_browse_media(item) diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index 491b1972cb6..f2a8ff13533 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -109,6 +109,25 @@ async def test_async_resolve_media(hass): assert media.mime_type == "audio/mpeg" +@patch("homeassistant.helpers.frame._REPORTED_INTEGRATIONS", set()) +async def test_async_resolve_media_no_entity(hass, caplog): + """Test browse media.""" + assert await async_setup_component(hass, media_source.DOMAIN, {}) + await hass.async_block_till_done() + + media = await media_source.async_resolve_media( + hass, + media_source.generate_media_source_id(media_source.DOMAIN, "local/test.mp3"), + ) + assert isinstance(media, media_source.models.PlayMedia) + assert media.url == "/media/local/test.mp3" + assert media.mime_type == "audio/mpeg" + assert ( + "calls media_source.async_resolve_media without passing an entity_id" + in caplog.text + ) + + async def test_async_unresolve_media(hass): """Test browse media.""" assert await async_setup_component(hass, media_source.DOMAIN, {}) From 087c0b59edb4f6233849e2cf6eb9057474251934 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 27 May 2022 18:05:06 +0200 Subject: [PATCH 0979/3516] Update integrations to pass target player when resolving media (#72597) --- .../components/apple_tv/media_player.py | 4 ++- .../components/bluesound/media_player.py | 4 ++- homeassistant/components/cast/media_player.py | 4 ++- .../components/dlna_dmr/media_player.py | 4 ++- .../components/esphome/media_player.py | 4 ++- .../components/forked_daapd/media_player.py | 4 ++- .../components/gstreamer/media_player.py | 4 ++- homeassistant/components/heos/media_player.py | 4 ++- homeassistant/components/kodi/media_player.py | 4 ++- homeassistant/components/mpd/media_player.py | 4 ++- .../components/openhome/media_player.py | 4 ++- .../panasonic_viera/media_player.py | 4 ++- homeassistant/components/roku/media_player.py | 4 ++- .../components/slimproto/media_player.py | 4 ++- .../components/sonos/media_player.py | 4 ++- .../components/soundtouch/media_player.py | 4 ++- .../components/squeezebox/media_player.py | 4 ++- .../components/unifiprotect/media_player.py | 4 ++- homeassistant/components/vlc/media_player.py | 4 ++- .../components/vlc_telnet/media_player.py | 4 ++- .../yamaha_musiccast/media_player.py | 4 ++- tests/components/camera/test_media_source.py | 10 ++++---- .../dlna_dms/test_device_availability.py | 10 ++++---- .../dlna_dms/test_dms_device_source.py | 2 +- .../components/dlna_dms/test_media_source.py | 12 ++++----- tests/components/google_translate/test_tts.py | 2 +- tests/components/marytts/test_tts.py | 2 +- tests/components/media_source/test_init.py | 11 +++++--- .../components/motioneye/test_media_source.py | 15 ++++++++--- tests/components/nest/test_media_source.py | 25 +++++++++++-------- tests/components/netatmo/test_media_source.py | 2 +- tests/components/tts/test_init.py | 2 +- tests/components/tts/test_media_source.py | 14 +++++++---- tests/components/voicerss/test_tts.py | 2 +- tests/components/yandextts/test_tts.py | 2 +- 35 files changed, 128 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 5a7298dcbee..30a397d953c 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -284,7 +284,9 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): await self.atv.apps.launch_app(media_id) if media_source.is_media_source_id(media_id): - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = play_item.url media_type = MEDIA_TYPE_MUSIC diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 32f74743972..e22606b795f 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -1032,7 +1032,9 @@ class BluesoundPlayer(MediaPlayerEntity): return if media_source.is_media_source_id(media_id): - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = play_item.url media_id = async_process_play_media_url(self.hass, media_id) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index b64c3372c15..ea21259ccc4 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -605,7 +605,9 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): """Play a piece of media.""" # Handle media_source if media_source.is_media_source_id(media_id): - sourced_media = await media_source.async_resolve_media(self.hass, media_id) + sourced_media = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_type = sourced_media.mime_type media_id = sourced_media.url diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index fd1fc9b2bab..9ecf9f8ad40 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -597,7 +597,9 @@ class DlnaDmrEntity(MediaPlayerEntity): # If media is media_source, resolve it to url and MIME type, and maybe metadata if media_source.is_media_source_id(media_id): - sourced_media = await media_source.async_resolve_media(self.hass, media_id) + sourced_media = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_type = sourced_media.mime_type media_id = sourced_media.url _LOGGER.debug("sourced_media is %s", sourced_media) diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index 6e83d12a427..f9027142ae2 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -95,7 +95,9 @@ class EsphomeMediaPlayer( ) -> None: """Send the play command with media url to the media player.""" if media_source.is_media_source_id(media_id): - sourced_media = await media_source.async_resolve_media(self.hass, media_id) + sourced_media = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = sourced_media.url media_id = async_process_play_media_url(self.hass, media_id) diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index f2c64fa81da..25695dceeb5 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -666,7 +666,9 @@ class ForkedDaapdMaster(MediaPlayerEntity): """Play a URI.""" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_MUSIC - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = play_item.url if media_type == MEDIA_TYPE_MUSIC: diff --git a/homeassistant/components/gstreamer/media_player.py b/homeassistant/components/gstreamer/media_player.py index 545941f2924..723be2880ff 100644 --- a/homeassistant/components/gstreamer/media_player.py +++ b/homeassistant/components/gstreamer/media_player.py @@ -96,7 +96,9 @@ class GstreamerDevice(MediaPlayerEntity): """Play media.""" # Handle media_source if media_source.is_media_source_id(media_id): - sourced_media = await media_source.async_resolve_media(self.hass, media_id) + sourced_media = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = sourced_media.url elif media_type != MEDIA_TYPE_MUSIC: diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index dabe79afb03..4cfbe5fe408 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -192,7 +192,9 @@ class HeosMediaPlayer(MediaPlayerEntity): """Play a piece of media.""" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_URL - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = play_item.url if media_type in (MEDIA_TYPE_URL, MEDIA_TYPE_MUSIC): diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index cea3adcde00..e19ffc6219c 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -713,7 +713,9 @@ class KodiEntity(MediaPlayerEntity): """Send the play_media command to the media player.""" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_URL - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = play_item.url media_type_lower = media_type.lower() diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index d3262a0d5da..ecee057a653 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -453,7 +453,9 @@ class MpdDevice(MediaPlayerEntity): """Send the media player the command for playing a playlist.""" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_MUSIC - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = async_process_play_media_url(self.hass, play_item.url) if media_type == MEDIA_TYPE_PLAYLIST: diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index fa9cce1cfb6..b6a0b549c40 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -209,7 +209,9 @@ class OpenhomeDevice(MediaPlayerEntity): """Send the play_media command to the media player.""" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_MUSIC - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = play_item.url if media_type != MEDIA_TYPE_MUSIC: diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index fd44c2853f1..7b75809f827 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -188,7 +188,9 @@ class PanasonicVieraTVEntity(MediaPlayerEntity): """Play media.""" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_URL - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = play_item.url if media_type != MEDIA_TYPE_URL: diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index e6fe0d7dcf5..a47432694dd 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -384,7 +384,9 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): # Handle media_source if media_source.is_media_source_id(media_id): - sourced_media = await media_source.async_resolve_media(self.hass, media_id) + sourced_media = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_type = MEDIA_TYPE_URL media_id = sourced_media.url mime_type = sourced_media.mime_type diff --git a/homeassistant/components/slimproto/media_player.py b/homeassistant/components/slimproto/media_player.py index 6b1989830e2..2f85aa4b9df 100644 --- a/homeassistant/components/slimproto/media_player.py +++ b/homeassistant/components/slimproto/media_player.py @@ -180,7 +180,9 @@ class SlimProtoPlayer(MediaPlayerEntity): to_send_media_type: str | None = media_type # Handle media_source if media_source.is_media_source_id(media_id): - sourced_media = await media_source.async_resolve_media(self.hass, media_id) + sourced_media = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = sourced_media.url to_send_media_type = sourced_media.mime_type diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 95834938953..45e11a810ae 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -551,7 +551,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): media_type = MEDIA_TYPE_MUSIC media_id = ( run_coroutine_threadsafe( - media_source.async_resolve_media(self.hass, media_id), + media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ), self.hass.loop, ) .result() diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index 3172eb4aed6..7c9ade3bee1 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -357,7 +357,9 @@ class SoundTouchDevice(MediaPlayerEntity): async def async_play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" if media_source.is_media_source_id(media_id): - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = async_process_play_media_url(self.hass, play_item.url) await self.hass.async_add_executor_job( diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index bd1f29f4e69..d0d1cf89739 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -482,7 +482,9 @@ class SqueezeBoxEntity(MediaPlayerEntity): if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_MUSIC - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = play_item.url if media_type in MEDIA_TYPE_MUSIC: diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index 0b7c2a2f60d..1acd14be130 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -118,7 +118,9 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): """Play a piece of media.""" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_MUSIC - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = async_process_play_media_url(self.hass, play_item.url) if media_type != MEDIA_TYPE_MUSIC: diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py index 7312eacd1c6..88b663e09c6 100644 --- a/homeassistant/components/vlc/media_player.py +++ b/homeassistant/components/vlc/media_player.py @@ -168,7 +168,9 @@ class VlcDevice(MediaPlayerEntity): """Play media from a URL or file.""" # Handle media_source if media_source.is_media_source_id(media_id): - sourced_media = await media_source.async_resolve_media(self.hass, media_id) + sourced_media = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = sourced_media.url elif media_type != MEDIA_TYPE_MUSIC: diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 89fa1a3c323..75305acbb0c 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -296,7 +296,9 @@ class VlcDevice(MediaPlayerEntity): """Play media from a URL or file.""" # Handle media_source if media_source.is_media_source_id(media_id): - sourced_media = await media_source.async_resolve_media(self.hass, media_id) + sourced_media = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_type = sourced_media.mime_type media_id = sourced_media.url diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index d0141977f29..954942b2c6b 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -275,7 +275,9 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> None: """Play media.""" if media_source.is_media_source_id(media_id): - play_item = await media_source.async_resolve_media(self.hass, media_id) + play_item = await media_source.async_resolve_media( + self.hass, media_id, self.entity_id + ) media_id = play_item.url if self.state == STATE_OFF: diff --git a/tests/components/camera/test_media_source.py b/tests/components/camera/test_media_source.py index b7c273bb23a..4134e9b1151 100644 --- a/tests/components/camera/test_media_source.py +++ b/tests/components/camera/test_media_source.py @@ -62,7 +62,7 @@ async def test_resolving(hass, mock_camera_hls): return_value="http://example.com/stream", ): item = await media_source.async_resolve_media( - hass, "media-source://camera/camera.demo_camera" + hass, "media-source://camera/camera.demo_camera", None ) assert item is not None assert item.url == "http://example.com/stream" @@ -74,7 +74,7 @@ async def test_resolving_errors(hass, mock_camera_hls): with pytest.raises(media_source.Unresolvable) as exc_info: await media_source.async_resolve_media( - hass, "media-source://camera/camera.demo_camera" + hass, "media-source://camera/camera.demo_camera", None ) assert str(exc_info.value) == "Stream integration not loaded" @@ -82,7 +82,7 @@ async def test_resolving_errors(hass, mock_camera_hls): with pytest.raises(media_source.Unresolvable) as exc_info: await media_source.async_resolve_media( - hass, "media-source://camera/camera.non_existing" + hass, "media-source://camera/camera.non_existing", None ) assert str(exc_info.value) == "Could not resolve media item: camera.non_existing" @@ -91,13 +91,13 @@ async def test_resolving_errors(hass, mock_camera_hls): new_callable=PropertyMock(return_value=StreamType.WEB_RTC), ): await media_source.async_resolve_media( - hass, "media-source://camera/camera.demo_camera" + hass, "media-source://camera/camera.demo_camera", None ) assert str(exc_info.value) == "Camera does not support MJPEG or HLS streaming." with pytest.raises(media_source.Unresolvable) as exc_info: await media_source.async_resolve_media( - hass, "media-source://camera/camera.demo_camera" + hass, "media-source://camera/camera.demo_camera", None ) assert ( str(exc_info.value) == "camera.demo_camera does not support play stream service" diff --git a/tests/components/dlna_dms/test_device_availability.py b/tests/components/dlna_dms/test_device_availability.py index 67ad1024709..a3ec5326f00 100644 --- a/tests/components/dlna_dms/test_device_availability.py +++ b/tests/components/dlna_dms/test_device_availability.py @@ -152,15 +152,15 @@ async def test_unavailable_device( ) with pytest.raises(Unresolvable, match="DMS is not connected"): await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}//resolve_path" + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}//resolve_path", None ) with pytest.raises(Unresolvable, match="DMS is not connected"): await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:resolve_object" + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:resolve_object", None ) with pytest.raises(Unresolvable): await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/?resolve_search" + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/?resolve_search", None ) @@ -651,7 +651,7 @@ async def test_become_unavailable( # Check async_resolve_object currently works assert await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:object_id" + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:object_id", None ) # Now break the network connection @@ -660,7 +660,7 @@ async def test_become_unavailable( # async_resolve_object should fail with pytest.raises(Unresolvable): await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:object_id" + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:object_id", None ) # The device should now be unavailable diff --git a/tests/components/dlna_dms/test_dms_device_source.py b/tests/components/dlna_dms/test_dms_device_source.py index 5e4021a5dda..622a3b8a4f9 100644 --- a/tests/components/dlna_dms/test_dms_device_source.py +++ b/tests/components/dlna_dms/test_dms_device_source.py @@ -45,7 +45,7 @@ async def async_resolve_media( ) -> DidlPlayMedia: """Call media_source.async_resolve_media with the test source's ID.""" result = await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/{media_content_id}" + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/{media_content_id}", None ) assert isinstance(result, DidlPlayMedia) return result diff --git a/tests/components/dlna_dms/test_media_source.py b/tests/components/dlna_dms/test_media_source.py index 5f76b061590..35f34d0689b 100644 --- a/tests/components/dlna_dms/test_media_source.py +++ b/tests/components/dlna_dms/test_media_source.py @@ -60,31 +60,31 @@ async def test_resolve_media_bad_identifier( """Test trying to resolve an item that has an unresolvable identifier.""" # Empty identifier with pytest.raises(Unresolvable, match="No source ID.*"): - await media_source.async_resolve_media(hass, f"media-source://{DOMAIN}") + await media_source.async_resolve_media(hass, f"media-source://{DOMAIN}", None) # Identifier has media_id but no source_id # media_source.URI_SCHEME_REGEX won't let the ID through to dlna_dms with pytest.raises(Unresolvable, match="Invalid media source URI"): await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}//media_id" + hass, f"media-source://{DOMAIN}//media_id", None ) # Identifier has source_id but no media_id with pytest.raises(Unresolvable, match="No media ID.*"): await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/source_id/" + hass, f"media-source://{DOMAIN}/source_id/", None ) # Identifier is missing source_id/media_id separator with pytest.raises(Unresolvable, match="No media ID.*"): await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/source_id" + hass, f"media-source://{DOMAIN}/source_id", None ) # Identifier has an unknown source_id with pytest.raises(Unresolvable, match="Unknown source ID: unknown_source"): await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/unknown_source/media_id" + hass, f"media-source://{DOMAIN}/unknown_source/media_id", None ) @@ -105,7 +105,7 @@ async def test_resolve_media_success( dms_device_mock.async_browse_metadata.return_value = didl_item result = await media_source.async_resolve_media( - hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:{object_id}" + hass, f"media-source://{DOMAIN}/{MOCK_SOURCE_ID}/:{object_id}", None ) assert isinstance(result, DidlPlayMedia) assert result.url == f"{MOCK_DEVICE_BASE_URL}/{res_url}" diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index c81cea57090..cc80d9c64b9 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -25,7 +25,7 @@ async def get_media_source_url(hass, media_content_id): if media_source.DOMAIN not in hass.config.components: assert await async_setup_component(hass, media_source.DOMAIN, {}) - resolved = await media_source.async_resolve_media(hass, media_content_id) + resolved = await media_source.async_resolve_media(hass, media_content_id, None) return resolved.url diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index 843b6578746..60211f7dc0c 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -21,7 +21,7 @@ async def get_media_source_url(hass, media_content_id): if media_source.DOMAIN not in hass.config.components: assert await async_setup_component(hass, media_source.DOMAIN, {}) - resolved = await media_source.async_resolve_media(hass, media_content_id) + resolved = await media_source.async_resolve_media(hass, media_content_id, None) return resolved.url diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index f2a8ff13533..33dd263c46c 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -103,6 +103,7 @@ async def test_async_resolve_media(hass): media = await media_source.async_resolve_media( hass, media_source.generate_media_source_id(media_source.DOMAIN, "local/test.mp3"), + None, ) assert isinstance(media, media_source.models.PlayMedia) assert media.url == "/media/local/test.mp3" @@ -135,15 +136,17 @@ async def test_async_unresolve_media(hass): # Test no media content with pytest.raises(media_source.Unresolvable): - await media_source.async_resolve_media(hass, "") + await media_source.async_resolve_media(hass, "", None) # Test invalid media content with pytest.raises(media_source.Unresolvable): - await media_source.async_resolve_media(hass, "invalid") + await media_source.async_resolve_media(hass, "invalid", None) # Test invalid media source with pytest.raises(media_source.Unresolvable): - await media_source.async_resolve_media(hass, "media-source://media_source2") + await media_source.async_resolve_media( + hass, "media-source://media_source2", None + ) async def test_websocket_browse_media(hass, hass_ws_client): @@ -261,4 +264,4 @@ async def test_browse_resolve_without_setup(): await media_source.async_browse_media(Mock(data={}), None) with pytest.raises(media_source.Unresolvable): - await media_source.async_resolve_media(Mock(data={}), None) + await media_source.async_resolve_media(Mock(data={}), None, None) diff --git a/tests/components/motioneye/test_media_source.py b/tests/components/motioneye/test_media_source.py index 9b86b783d43..2cf31c21da7 100644 --- a/tests/components/motioneye/test_media_source.py +++ b/tests/components/motioneye/test_media_source.py @@ -367,6 +367,7 @@ async def test_async_resolve_media_success(hass: HomeAssistant) -> None: f"{const.URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{device.id}#movies#/foo.mp4" ), + None, ) assert media == PlayMedia(url="http://movie-url", mime_type="video/mp4") assert client.get_movie_url.call_args == call(TEST_CAMERA_ID, "/foo.mp4") @@ -379,6 +380,7 @@ async def test_async_resolve_media_success(hass: HomeAssistant) -> None: f"{const.URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{device.id}#images#/foo.jpg" ), + None, ) assert media == PlayMedia(url="http://image-url", mime_type="image/jpeg") assert client.get_image_url.call_args == call(TEST_CAMERA_ID, "/foo.jpg") @@ -409,18 +411,20 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: # URI doesn't contain necessary components. with pytest.raises(Unresolvable): - await media_source.async_resolve_media(hass, f"{const.URI_SCHEME}{DOMAIN}/foo") + await media_source.async_resolve_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/foo", None + ) # Config entry doesn't exist. with pytest.raises(MediaSourceError): await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/1#2#3#4" + hass, f"{const.URI_SCHEME}{DOMAIN}/1#2#3#4", None ) # Device doesn't exist. with pytest.raises(MediaSourceError): await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{TEST_CONFIG_ENTRY_ID}#2#3#4" + hass, f"{const.URI_SCHEME}{DOMAIN}/{TEST_CONFIG_ENTRY_ID}#2#3#4", None ) # Device identifiers are incorrect (no camera id) @@ -431,6 +435,7 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: f"{const.URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{broken_device_1.id}#images#4" ), + None, ) # Device identifiers are incorrect (non integer camera id) @@ -441,6 +446,7 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: f"{const.URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{broken_device_2.id}#images#4" ), + None, ) # Kind is incorrect. @@ -448,6 +454,7 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: await media_source.async_resolve_media( hass, f"{const.URI_SCHEME}{DOMAIN}/{TEST_CONFIG_ENTRY_ID}#{device.id}#games#moo", + None, ) # Playback URL raises exception. @@ -459,6 +466,7 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: f"{const.URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{device.id}#movies#/foo.mp4" ), + None, ) # Media path does not start with '/' @@ -470,6 +478,7 @@ async def test_async_resolve_media_failure(hass: HomeAssistant) -> None: f"{const.URI_SCHEME}{DOMAIN}" f"/{TEST_CONFIG_ENTRY_ID}#{device.id}#movies#foo.mp4" ), + None, ) # Media missing path. diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 1536d0bee1e..09a3f9f625c 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -361,7 +361,7 @@ async def test_camera_event(hass, auth, hass_client): # Resolving the event links to the media media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier}" assert media.mime_type == "image/jpeg" @@ -374,7 +374,7 @@ async def test_camera_event(hass, auth, hass_client): # Resolving the device id points to the most recent event media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier}" assert media.mime_type == "image/jpeg" @@ -535,7 +535,7 @@ async def test_multiple_image_events_in_session(hass, auth, hass_client): # Resolve the most recent event media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier2}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier2}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier2}" assert media.mime_type == "image/jpeg" @@ -548,7 +548,7 @@ async def test_multiple_image_events_in_session(hass, auth, hass_client): # Resolving the event links to the media media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier1}" assert media.mime_type == "image/jpeg" @@ -632,7 +632,7 @@ async def test_multiple_clip_preview_events_in_session(hass, auth, hass_client): # to the same clip preview media clip object. # Resolve media for the first event media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier1}" assert media.mime_type == "video/mp4" @@ -645,7 +645,7 @@ async def test_multiple_clip_preview_events_in_session(hass, auth, hass_client): # Resolve media for the second event media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier1}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier1}" assert media.mime_type == "video/mp4" @@ -712,6 +712,7 @@ async def test_resolve_missing_event_id(hass, auth): await media_source.async_resolve_media( hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}", + None, ) @@ -723,6 +724,7 @@ async def test_resolve_invalid_device_id(hass, auth): await media_source.async_resolve_media( hass, f"{const.URI_SCHEME}{DOMAIN}/invalid-device-id/GXXWRWVeHNUlUU3V3MGV3bUOYW...", + None, ) @@ -740,6 +742,7 @@ async def test_resolve_invalid_event_id(hass, auth): media = await media_source.async_resolve_media( hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/GXXWRWVeHNUlUU3V3MGV3bUOYW...", + None, ) assert ( media.url == f"/api/nest/event_media/{device.id}/GXXWRWVeHNUlUU3V3MGV3bUOYW..." @@ -835,7 +838,7 @@ async def test_camera_event_clip_preview(hass, auth, hass_client, mp4): # Resolving the event links to the media media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier}" assert media.mime_type == "video/mp4" @@ -921,7 +924,7 @@ async def test_event_media_failure(hass, auth, hass_client): # Resolving the event links to the media media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{device.id}/{event_identifier}", None ) assert media.url == f"/api/nest/event_media/{device.id}/{event_identifier}" assert media.mime_type == "image/jpeg" @@ -1128,7 +1131,7 @@ async def test_media_store_persistence(hass, auth, hass_client, event_store): event_identifier = browse.children[0].identifier media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{event_identifier}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{event_identifier}", None ) assert media.url == f"/api/nest/event_media/{event_identifier}" assert media.mime_type == "video/mp4" @@ -1182,7 +1185,7 @@ async def test_media_store_persistence(hass, auth, hass_client, event_store): event_identifier = browse.children[0].identifier media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{event_identifier}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{event_identifier}", None ) assert media.url == f"/api/nest/event_media/{event_identifier}" assert media.mime_type == "video/mp4" @@ -1234,7 +1237,7 @@ async def test_media_store_save_filesystem_error(hass, auth, hass_client): event = browse.children[0] media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/{event.identifier}" + hass, f"{const.URI_SCHEME}{DOMAIN}/{event.identifier}", None ) assert media.url == f"/api/nest/event_media/{event.identifier}" assert media.mime_type == "video/mp4" diff --git a/tests/components/netatmo/test_media_source.py b/tests/components/netatmo/test_media_source.py index db1a79145b4..390da95496a 100644 --- a/tests/components/netatmo/test_media_source.py +++ b/tests/components/netatmo/test_media_source.py @@ -79,7 +79,7 @@ async def test_async_browse_media(hass): # Test successful event resolve media = await media_source.async_resolve_media( - hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672" + hass, f"{const.URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672", None ) assert media == PlayMedia( url="http:///files/high/index.m3u8", mime_type="application/x-mpegURL" diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 7fd8cc0facb..78fa49a8fc9 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -29,7 +29,7 @@ async def get_media_source_url(hass, media_content_id): if media_source.DOMAIN not in hass.config.components: assert await async_setup_component(hass, media_source.DOMAIN, {}) - resolved = await media_source.async_resolve_media(hass, media_content_id) + resolved = await media_source.async_resolve_media(hass, media_content_id, None) return resolved.url diff --git a/tests/components/tts/test_media_source.py b/tests/components/tts/test_media_source.py index 22edfef5358..8af1ad9d3bb 100644 --- a/tests/components/tts/test_media_source.py +++ b/tests/components/tts/test_media_source.py @@ -68,7 +68,7 @@ async def test_browsing(hass): async def test_resolving(hass, mock_get_tts_audio): """Test resolving.""" media = await media_source.async_resolve_media( - hass, "media-source://tts/demo?message=Hello%20World" + hass, "media-source://tts/demo?message=Hello%20World", None ) assert media.url.startswith("/api/tts_proxy/") assert media.mime_type == "audio/mpeg" @@ -82,7 +82,9 @@ async def test_resolving(hass, mock_get_tts_audio): # Pass language and options mock_get_tts_audio.reset_mock() media = await media_source.async_resolve_media( - hass, "media-source://tts/demo?message=Bye%20World&language=de&voice=Paulus" + hass, + "media-source://tts/demo?message=Bye%20World&language=de&voice=Paulus", + None, ) assert media.url.startswith("/api/tts_proxy/") assert media.mime_type == "audio/mpeg" @@ -98,16 +100,18 @@ async def test_resolving_errors(hass): """Test resolving.""" # No message added with pytest.raises(media_source.Unresolvable): - await media_source.async_resolve_media(hass, "media-source://tts/demo") + await media_source.async_resolve_media(hass, "media-source://tts/demo", None) # Non-existing provider with pytest.raises(media_source.Unresolvable): await media_source.async_resolve_media( - hass, "media-source://tts/non-existing?message=bla" + hass, "media-source://tts/non-existing?message=bla", None ) # Non-existing option with pytest.raises(media_source.Unresolvable): await media_source.async_resolve_media( - hass, "media-source://tts/non-existing?message=bla&non_existing_option=bla" + hass, + "media-source://tts/non-existing?message=bla&non_existing_option=bla", + None, ) diff --git a/tests/components/voicerss/test_tts.py b/tests/components/voicerss/test_tts.py index 3e74d9dc815..099b280625f 100644 --- a/tests/components/voicerss/test_tts.py +++ b/tests/components/voicerss/test_tts.py @@ -33,7 +33,7 @@ async def get_media_source_url(hass, media_content_id): if media_source.DOMAIN not in hass.config.components: assert await async_setup_component(hass, media_source.DOMAIN, {}) - resolved = await media_source.async_resolve_media(hass, media_content_id) + resolved = await media_source.async_resolve_media(hass, media_content_id, None) return resolved.url diff --git a/tests/components/yandextts/test_tts.py b/tests/components/yandextts/test_tts.py index fdc204384a5..8549b51c341 100644 --- a/tests/components/yandextts/test_tts.py +++ b/tests/components/yandextts/test_tts.py @@ -27,7 +27,7 @@ async def get_media_source_url(hass, media_content_id): if media_source.DOMAIN not in hass.config.components: assert await async_setup_component(hass, media_source.DOMAIN, {}) - resolved = await media_source.async_resolve_media(hass, media_content_id) + resolved = await media_source.async_resolve_media(hass, media_content_id, None) return resolved.url From 27908af61eb0c07106fa721c4b20b9b64e988658 Mon Sep 17 00:00:00 2001 From: xLarry Date: Fri, 27 May 2022 18:19:18 +0200 Subject: [PATCH 0980/3516] Bump laundrify_aio to v1.1.2 (#72605) --- homeassistant/components/laundrify/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/laundrify/manifest.json b/homeassistant/components/laundrify/manifest.json index 6a61446d31c..a5737b9cf97 100644 --- a/homeassistant/components/laundrify/manifest.json +++ b/homeassistant/components/laundrify/manifest.json @@ -3,7 +3,7 @@ "name": "laundrify", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/laundrify", - "requirements": ["laundrify_aio==1.1.1"], + "requirements": ["laundrify_aio==1.1.2"], "codeowners": ["@xLarry"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index c7459285fa0..e94929e8d16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -927,7 +927,7 @@ krakenex==2.1.0 lakeside==0.12 # homeassistant.components.laundrify -laundrify_aio==1.1.1 +laundrify_aio==1.1.2 # homeassistant.components.foscam libpyfoscam==1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7233824a3cc..edcd957246f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -655,7 +655,7 @@ kostal_plenticore==0.2.0 krakenex==2.1.0 # homeassistant.components.laundrify -laundrify_aio==1.1.1 +laundrify_aio==1.1.2 # homeassistant.components.foscam libpyfoscam==1.0 From 07c7081adec21fcaa14038acabe8227ca25dc53f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 27 May 2022 10:30:40 -0700 Subject: [PATCH 0981/3516] Revert "Add service entity context (#71558)" (#72610) --- homeassistant/helpers/service.py | 11 ----------- tests/helpers/test_service.py | 16 +--------------- 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 9a1e6caa27e..bc3451c24c0 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable, Iterable -from contextvars import ContextVar import dataclasses from functools import partial, wraps import logging @@ -64,15 +63,6 @@ _LOGGER = logging.getLogger(__name__) SERVICE_DESCRIPTION_CACHE = "service_description_cache" -_current_entity: ContextVar[str | None] = ContextVar("current_entity", default=None) - - -@callback -def async_get_current_entity() -> str | None: - """Get the current entity on which the service is called.""" - return _current_entity.get() - - class ServiceParams(TypedDict): """Type for service call parameters.""" @@ -716,7 +706,6 @@ async def _handle_entity_call( ) -> None: """Handle calling service method.""" entity.async_set_context(context) - _current_entity.set(entity.entity_id) if isinstance(func, str): result = hass.async_run_job(partial(getattr(entity, func), **data)) # type: ignore[arg-type] diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 76cf83e31bf..d08477dc917 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -19,12 +19,12 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.helpers import ( - config_validation as cv, device_registry as dev_reg, entity_registry as ent_reg, service, template, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import EntityCategory from homeassistant.setup import async_setup_component @@ -1206,17 +1206,3 @@ async def test_async_extract_config_entry_ids(hass): ) assert await service.async_extract_config_entry_ids(hass, call) == {"abc"} - - -async def test_current_entity_context(hass, mock_entities): - """Test we set the current entity context var.""" - - async def mock_service(entity, call): - assert entity.entity_id == service.async_get_current_entity() - - await service.entity_service_call( - hass, - [Mock(entities=mock_entities)], - mock_service, - ha.ServiceCall("test_domain", "test_service", {"entity_id": "light.kitchen"}), - ) From 2e2fa208a83c5c543313c6651b61922f177b69ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 May 2022 07:32:26 -1000 Subject: [PATCH 0982/3516] Fix recorder system health when the db_url is lacking a hostname (#72612) --- .../recorder/system_health/__init__.py | 5 ++- .../components/recorder/test_system_health.py | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/system_health/__init__.py b/homeassistant/components/recorder/system_health/__init__.py index 8ba68a1649b..c4bf2c3bb89 100644 --- a/homeassistant/components/recorder/system_health/__init__.py +++ b/homeassistant/components/recorder/system_health/__init__.py @@ -2,8 +2,7 @@ from __future__ import annotations from typing import Any - -from yarl import URL +from urllib.parse import urlparse from homeassistant.components import system_health from homeassistant.components.recorder.core import Recorder @@ -60,7 +59,7 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: instance = get_instance(hass) run_history = instance.run_history - database_name = URL(instance.db_url).path.lstrip("/") + database_name = urlparse(instance.db_url).path.lstrip("/") db_engine_info = _async_get_db_engine_info(instance) db_stats: dict[str, Any] = {} diff --git a/tests/components/recorder/test_system_health.py b/tests/components/recorder/test_system_health.py index 80997b9df36..b465ee89ebe 100644 --- a/tests/components/recorder/test_system_health.py +++ b/tests/components/recorder/test_system_health.py @@ -53,6 +53,37 @@ async def test_recorder_system_health_alternate_dbms(hass, recorder_mock, dialec } +@pytest.mark.parametrize( + "dialect_name", [SupportedDialect.MYSQL, SupportedDialect.POSTGRESQL] +) +async def test_recorder_system_health_db_url_missing_host( + hass, recorder_mock, dialect_name +): + """Test recorder system health with a db_url without a hostname.""" + assert await async_setup_component(hass, "system_health", {}) + await async_wait_recording_done(hass) + + instance = get_instance(hass) + with patch( + "homeassistant.components.recorder.core.Recorder.dialect_name", dialect_name + ), patch.object( + instance, + "db_url", + "postgresql://homeassistant:blabla@/home_assistant?host=/config/socket", + ), patch( + "sqlalchemy.orm.session.Session.execute", + return_value=Mock(first=Mock(return_value=("1048576",))), + ): + info = await get_system_health_info(hass, "recorder") + assert info == { + "current_recorder_run": instance.run_history.current.start, + "oldest_recorder_run": instance.run_history.first.start, + "estimated_db_size": "1.00 MiB", + "database_engine": dialect_name.value, + "database_version": ANY, + } + + async def test_recorder_system_health_crashed_recorder_runs_table( hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT ): From 38c085f86931e08091296138c4bc1b10a8fa7d01 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 27 May 2022 11:32:38 -0600 Subject: [PATCH 0983/3516] Bump regenmaschine to 2022.05.0 (#72613) --- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 331f191d029..bbe58e263b1 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.01.0"], + "requirements": ["regenmaschine==2022.05.0"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index e94929e8d16..6d7ca5cb6b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2065,7 +2065,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.01.0 +regenmaschine==2022.05.0 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index edcd957246f..78caa172df7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1364,7 +1364,7 @@ rachiopy==1.0.3 radios==0.1.1 # homeassistant.components.rainmachine -regenmaschine==2022.01.0 +regenmaschine==2022.05.0 # homeassistant.components.renault renault-api==0.1.11 From 13f953f49d3f13069404ed46f921cde9e4fcc034 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 May 2022 08:11:33 -1000 Subject: [PATCH 0984/3516] Add explict type casts for postgresql filters (#72615) --- homeassistant/components/recorder/filters.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 7f1d0bc597f..0a383d8ef2b 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -5,7 +5,7 @@ from collections.abc import Callable, Iterable import json from typing import Any -from sqlalchemy import Column, not_, or_ +from sqlalchemy import JSON, Column, Text, cast, not_, or_ from sqlalchemy.sql.elements import ClauseList from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE @@ -110,8 +110,7 @@ class Filters: """Generate the entity filter query.""" _encoder = json.dumps return or_( - (ENTITY_ID_IN_EVENT == _encoder(None)) - & (OLD_ENTITY_ID_IN_EVENT == _encoder(None)), + (ENTITY_ID_IN_EVENT == JSON.NULL) & (OLD_ENTITY_ID_IN_EVENT == JSON.NULL), self._generate_filter_for_columns( (ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT), _encoder ).self_group(), @@ -123,7 +122,7 @@ def _globs_to_like( ) -> ClauseList: """Translate glob to sql.""" return or_( - column.like(encoder(glob_str.translate(GLOB_TO_SQL_CHARS))) + cast(column, Text()).like(encoder(glob_str.translate(GLOB_TO_SQL_CHARS))) for glob_str in glob_strs for column in columns ) @@ -133,7 +132,7 @@ def _entity_matcher( entity_ids: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: return or_( - column.in_([encoder(entity_id) for entity_id in entity_ids]) + cast(column, Text()).in_([encoder(entity_id) for entity_id in entity_ids]) for column in columns ) @@ -142,5 +141,7 @@ def _domain_matcher( domains: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: return or_( - column.like(encoder(f"{domain}.%")) for domain in domains for column in columns + cast(column, Text()).like(encoder(f"{domain}.%")) + for domain in domains + for column in columns ) From e974a432aa629acc6093106c4e33f713e2d083a6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 27 May 2022 11:38:00 -0700 Subject: [PATCH 0985/3516] Bumped version to 2022.6.0b2 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ada9a6bbd06..acad8f2675a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 205fcd96086..3d26396deed 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.0b1 +version = 2022.6.0b2 url = https://www.home-assistant.io/ [options] From 9fe4aef4bcb7c041ce71131e1967867f911770ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 27 May 2022 23:37:19 +0200 Subject: [PATCH 0986/3516] Bump awesomeversion from 22.5.1 to 22.5.2 (#72624) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f235cc3f02c..a43b4f99f63 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ async-upnp-client==0.30.1 async_timeout==4.0.2 atomicwrites==1.4.0 attrs==21.2.0 -awesomeversion==22.5.1 +awesomeversion==22.5.2 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/pyproject.toml b/pyproject.toml index 499376b95f5..cc745f58ad6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "async_timeout==4.0.2", "attrs==21.2.0", "atomicwrites==1.4.0", - "awesomeversion==22.5.1", + "awesomeversion==22.5.2", "bcrypt==3.1.7", "certifi>=2021.5.30", "ciso8601==2.2.0", diff --git a/requirements.txt b/requirements.txt index 8321e70f8de..fe2bf87ad25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ astral==2.2 async_timeout==4.0.2 attrs==21.2.0 atomicwrites==1.4.0 -awesomeversion==22.5.1 +awesomeversion==22.5.2 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 From a43d47fa0bb2a7c3d18ec28c7b9e2059f90e0223 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 May 2022 11:38:29 -1000 Subject: [PATCH 0987/3516] Escape % and _ in history/logbook entity_globs, and use ? as _ (#72623) Co-authored-by: pyos --- homeassistant/components/recorder/filters.py | 11 +- tests/components/history/test_init.py | 12 +- .../components/logbook/test_websocket_api.py | 207 ++++++++++++++++++ 3 files changed, 223 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 0a383d8ef2b..5dd1e4b7884 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -18,8 +18,11 @@ DOMAIN = "history" HISTORY_FILTERS = "history_filters" GLOB_TO_SQL_CHARS = { - 42: "%", # * - 46: "_", # . + ord("*"): "%", + ord("?"): "_", + ord("%"): "\\%", + ord("_"): "\\_", + ord("\\"): "\\\\", } @@ -122,7 +125,9 @@ def _globs_to_like( ) -> ClauseList: """Translate glob to sql.""" return or_( - cast(column, Text()).like(encoder(glob_str.translate(GLOB_TO_SQL_CHARS))) + cast(column, Text()).like( + encoder(glob_str).translate(GLOB_TO_SQL_CHARS), escape="\\" + ) for glob_str in glob_strs for column in columns ) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index a2626ab2004..cbc5e86c37e 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -719,7 +719,7 @@ async def test_fetch_period_api_with_entity_glob_exclude( { "history": { "exclude": { - "entity_globs": ["light.k*"], + "entity_globs": ["light.k*", "binary_sensor.*_?"], "domains": "switch", "entities": "media_player.test", }, @@ -731,6 +731,9 @@ async def test_fetch_period_api_with_entity_glob_exclude( hass.states.async_set("light.match", "on") hass.states.async_set("switch.match", "on") hass.states.async_set("media_player.test", "on") + hass.states.async_set("binary_sensor.sensor_l", "on") + hass.states.async_set("binary_sensor.sensor_r", "on") + hass.states.async_set("binary_sensor.sensor", "on") await async_wait_recording_done(hass) @@ -740,9 +743,10 @@ async def test_fetch_period_api_with_entity_glob_exclude( ) assert response.status == HTTPStatus.OK response_json = await response.json() - assert len(response_json) == 2 - assert response_json[0][0]["entity_id"] == "light.cow" - assert response_json[1][0]["entity_id"] == "light.match" + assert len(response_json) == 3 + assert response_json[0][0]["entity_id"] == "binary_sensor.sensor" + assert response_json[1][0]["entity_id"] == "light.cow" + assert response_json[2][0]["entity_id"] == "light.match" async def test_fetch_period_api_with_entity_glob_include_and_exclude( diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 02fea4f980f..9d7146ec96c 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -22,6 +22,7 @@ from homeassistant.const import ( CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, + CONF_INCLUDE, EVENT_HOMEASSISTANT_START, STATE_OFF, STATE_ON, @@ -642,6 +643,212 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities( assert sum(hass.bus.async_listeners().values()) == init_count +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_unsubscribe_logbook_stream_included_entities( + hass, recorder_mock, hass_ws_client +): + """Test subscribe/unsubscribe logbook stream with included entities.""" + test_entities = ( + "light.inc", + "switch.any", + "cover.included", + "cover.not_included", + "automation.not_included", + "binary_sensor.is_light", + ) + + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "automation", "script") + ] + ) + await async_setup_component( + hass, + logbook.DOMAIN, + { + logbook.DOMAIN: { + CONF_INCLUDE: { + CONF_ENTITIES: ["light.inc"], + CONF_DOMAINS: ["switch"], + CONF_ENTITY_GLOBS: "*.included", + } + }, + }, + ) + await hass.async_block_till_done() + init_count = sum(hass.bus.async_listeners().values()) + + for entity_id in test_entities: + hass.states.async_set(entity_id, STATE_ON) + hass.states.async_set(entity_id, STATE_OFF) + + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + {"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()} + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + {"entity_id": "light.inc", "state": "off", "when": ANY}, + {"entity_id": "switch.any", "state": "off", "when": ANY}, + {"entity_id": "cover.included", "state": "off", "when": ANY}, + ] + assert msg["event"]["start_time"] == now.timestamp() + assert msg["event"]["end_time"] > msg["event"]["start_time"] + assert msg["event"]["partial"] is True + + for entity_id in test_entities: + hass.states.async_set(entity_id, STATE_ON) + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + + hass.states.async_remove("light.zulu") + await hass.async_block_till_done() + + hass.states.async_set("light.zulu", "on", {"effect": "help", "color": "blue"}) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [ + {"entity_id": "light.inc", "state": "on", "when": ANY}, + {"entity_id": "light.inc", "state": "off", "when": ANY}, + {"entity_id": "switch.any", "state": "on", "when": ANY}, + {"entity_id": "switch.any", "state": "off", "when": ANY}, + {"entity_id": "cover.included", "state": "on", "when": ANY}, + {"entity_id": "cover.included", "state": "off", "when": ANY}, + ] + + for _ in range(3): + for entity_id in test_entities: + hass.states.async_set(entity_id, STATE_ON) + hass.states.async_set(entity_id, STATE_OFF) + await async_wait_recording_done(hass) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + {"entity_id": "light.inc", "state": "on", "when": ANY}, + {"entity_id": "light.inc", "state": "off", "when": ANY}, + {"entity_id": "switch.any", "state": "on", "when": ANY}, + {"entity_id": "switch.any", "state": "off", "when": ANY}, + {"entity_id": "cover.included", "state": "on", "when": ANY}, + {"entity_id": "cover.included", "state": "off", "when": ANY}, + ] + + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation 3", ATTR_ENTITY_ID: "cover.included"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation 3", ATTR_ENTITY_ID: "cover.excluded"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + { + ATTR_NAME: "Mock automation switch matching entity", + ATTR_ENTITY_ID: "switch.match_domain", + }, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation switch matching domain", ATTR_DOMAIN: "switch"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation matches nothing"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation 3", ATTR_ENTITY_ID: "light.inc"}, + ) + + await hass.async_block_till_done() + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + { + "context_id": ANY, + "domain": "automation", + "entity_id": "cover.included", + "message": "triggered", + "name": "Mock automation 3", + "source": None, + "when": ANY, + }, + { + "context_id": ANY, + "domain": "automation", + "entity_id": "switch.match_domain", + "message": "triggered", + "name": "Mock automation switch matching entity", + "source": None, + "when": ANY, + }, + { + "context_id": ANY, + "domain": "automation", + "entity_id": None, + "message": "triggered", + "name": "Mock automation switch matching domain", + "source": None, + "when": ANY, + }, + { + "context_id": ANY, + "domain": "automation", + "entity_id": None, + "message": "triggered", + "name": "Mock automation matches nothing", + "source": None, + "when": ANY, + }, + { + "context_id": ANY, + "domain": "automation", + "entity_id": "light.inc", + "message": "triggered", + "name": "Mock automation 3", + "source": None, + "when": ANY, + }, + ] + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) async def test_subscribe_unsubscribe_logbook_stream( hass, recorder_mock, hass_ws_client From 327c6964e233d482224831c65a6fe074a79478fa Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 28 May 2022 00:24:05 +0000 Subject: [PATCH 0988/3516] [ci skip] Translation update --- homeassistant/components/totalconnect/translations/ca.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/totalconnect/translations/ca.json b/homeassistant/components/totalconnect/translations/ca.json index 0c1a540b8ac..36c1037d917 100644 --- a/homeassistant/components/totalconnect/translations/ca.json +++ b/homeassistant/components/totalconnect/translations/ca.json @@ -32,6 +32,10 @@ "options": { "step": { "init": { + "data": { + "auto_bypass_low_battery": "Bypass autom\u00e0tic de bateria baixa" + }, + "description": "Bypass autom\u00e0tic de les zones que informin de bateria baixa.", "title": "Opcions de TotalConnect" } } From 4a5679db08e8624ce383c2527ecb4e72799026f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 May 2022 22:49:55 -1000 Subject: [PATCH 0989/3516] Prevent config entries from being reloaded concurrently (#72636) * Prevent config entries being reloaded concurrently - Fixes Config entry has already been setup when two places try to reload the config entry at the same time. - This comes up quite a bit: https://github.com/home-assistant/core/issues?q=is%3Aissue+sort%3Aupdated-desc+%22Config+entry+has+already+been+setup%22+is%3Aclosed * Make sure plex creates mocks in the event loop * drop reload_lock, already inherits --- homeassistant/config_entries.py | 13 ++++++---- tests/components/plex/conftest.py | 2 +- tests/test_config_entries.py | 40 ++++++++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 7dfbb131c1b..0ac02adb8d0 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -186,6 +186,7 @@ class ConfigEntry: "reason", "_async_cancel_retry_setup", "_on_unload", + "reload_lock", ) def __init__( @@ -275,6 +276,9 @@ class ConfigEntry: # Hold list for functions to call on unload. self._on_unload: list[CALLBACK_TYPE] | None = None + # Reload lock to prevent conflicting reloads + self.reload_lock = asyncio.Lock() + async def async_setup( self, hass: HomeAssistant, @@ -1005,12 +1009,13 @@ class ConfigEntries: if (entry := self.async_get_entry(entry_id)) is None: raise UnknownEntry - unload_result = await self.async_unload(entry_id) + async with entry.reload_lock: + unload_result = await self.async_unload(entry_id) - if not unload_result or entry.disabled_by: - return unload_result + if not unload_result or entry.disabled_by: + return unload_result - return await self.async_setup(entry_id) + return await self.async_setup(entry_id) async def async_set_disabled_by( self, entry_id: str, disabled_by: ConfigEntryDisabler | None diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index 47e7d96d2fe..506aadcce61 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -381,7 +381,7 @@ def hubs_music_library_fixture(): @pytest.fixture(name="entry") -def mock_config_entry(): +async def mock_config_entry(): """Return the default mocked config entry.""" return MockConfigEntry( domain=DOMAIN, diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 3611c204ba7..2602887d1d5 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1497,7 +1497,7 @@ async def test_reload_entry_entity_registry_works(hass): ) await hass.async_block_till_done() - assert len(mock_unload_entry.mock_calls) == 1 + assert len(mock_unload_entry.mock_calls) == 2 async def test_unique_id_persisted(hass, manager): @@ -3080,3 +3080,41 @@ async def test_deprecated_disabled_by_str_set(hass, manager, caplog): ) assert entry.disabled_by is config_entries.ConfigEntryDisabler.USER assert " str for config entry disabled_by. This is deprecated " in caplog.text + + +async def test_entry_reload_concurrency(hass, manager): + """Test multiple reload calls do not cause a reload race.""" + entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED) + entry.add_to_hass(hass) + + async_setup = AsyncMock(return_value=True) + loaded = 1 + + async def _async_setup_entry(*args, **kwargs): + await asyncio.sleep(0) + nonlocal loaded + loaded += 1 + return loaded == 1 + + async def _async_unload_entry(*args, **kwargs): + await asyncio.sleep(0) + nonlocal loaded + loaded -= 1 + return loaded == 0 + + mock_integration( + hass, + MockModule( + "comp", + async_setup=async_setup, + async_setup_entry=_async_setup_entry, + async_unload_entry=_async_unload_entry, + ), + ) + mock_entity_platform(hass, "config_flow.comp", None) + tasks = [] + for _ in range(15): + tasks.append(asyncio.create_task(manager.async_reload(entry.entry_id))) + await asyncio.gather(*tasks) + assert entry.state is config_entries.ConfigEntryState.LOADED + assert loaded == 1 From 233f086853eb7edac47b636504205731b80942c1 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Sat, 28 May 2022 19:55:50 +0200 Subject: [PATCH 0990/3516] Bump bimmer_connected to 0.9.2 (#72653) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index d41a87ef2c1..c7130d12698 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.9.0"], + "requirements": ["bimmer_connected==0.9.2"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index a05e0641723..bf7769454f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -394,7 +394,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.0 +bimmer_connected==0.9.2 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bfa4f2eb165..698dc977f2b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -309,7 +309,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.0 +bimmer_connected==0.9.2 # homeassistant.components.blebox blebox_uniapi==1.3.3 From e0614953a23de9ef9becb85ff71c699cbee4f0bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 May 2022 09:47:14 -1000 Subject: [PATCH 0991/3516] Add support for async_remove_config_entry_device to homekit_controller (#72630) --- .../components/homekit_controller/__init__.py | 18 +++++++- tests/components/homekit_controller/common.py | 14 +++++++ .../homekit_controller/test_init.py | 42 +++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 6b538658b23..6909b226556 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -15,9 +15,10 @@ from aiohomekit.model.characteristics import ( from aiohomekit.model.services import Service, ServicesTypes from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ATTR_IDENTIFIERS, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.typing import ConfigType @@ -261,3 +262,18 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: "HomeKit again", entry.title, ) + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove homekit_controller config entry from a device.""" + hkid = config_entry.data["AccessoryPairingID"] + connection: HKDevice = hass.data[KNOWN_DEVICES][hkid] + return not device_entry.identifiers.intersection( + identifier + for accessory in connection.entity_map.accessories + for identifier in connection.device_info_for_accessory(accessory)[ + ATTR_IDENTIFIERS + ] + ) diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 8f59fae8639..749bd4b0f07 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -372,3 +372,17 @@ async def assert_devices_and_entities_created( # Root device must not have a via, otherwise its not the device assert root_device.via_device_id is None + + +async def remove_device(ws_client, device_id, config_entry_id): + """Remove config entry from a device.""" + await ws_client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry_id, + "device_id": device_id, + } + ) + response = await ws_client.receive_json() + return response["success"] diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 03694e7186a..820b89e587d 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -8,9 +8,17 @@ from aiohomekit.model.services import ServicesTypes from homeassistant.components.homekit_controller.const import ENTITY_MAP from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity_registry import EntityRegistry +from homeassistant.setup import async_setup_component + +from .common import Helper, remove_device from tests.components.homekit_controller.common import setup_test_component +ALIVE_DEVICE_NAME = "Light Bulb" +ALIVE_DEEVICE_ENTITY_ID = "light.testdevice" + def create_motion_sensor_service(accessory): """Define motion characteristics as per page 225 of HAP spec.""" @@ -47,3 +55,37 @@ async def test_async_remove_entry(hass: HomeAssistant): assert len(controller.pairings) == 0 assert hkid not in hass.data[ENTITY_MAP].storage_data + + +def create_alive_service(accessory): + """Create a service to validate we can only remove dead devices.""" + service = accessory.add_service(ServicesTypes.LIGHTBULB, name=ALIVE_DEVICE_NAME) + service.add_char(CharacteristicsTypes.ON) + return service + + +async def test_device_remove_devices(hass, hass_ws_client): + """Test we can only remove a device that no longer exists.""" + assert await async_setup_component(hass, "config", {}) + helper: Helper = await setup_test_component(hass, create_alive_service) + config_entry = helper.config_entry + entry_id = config_entry.entry_id + + registry: EntityRegistry = er.async_get(hass) + entity = registry.entities[ALIVE_DEEVICE_ENTITY_ID] + device_registry = dr.async_get(hass) + + live_device_entry = device_registry.async_get(entity.device_id) + assert ( + await remove_device(await hass_ws_client(hass), live_device_entry.id, entry_id) + is False + ) + + dead_device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={("homekit_controller:accessory-id", "E9:88:E7:B8:B4:40:aid:1")}, + ) + assert ( + await remove_device(await hass_ws_client(hass), dead_device_entry.id, entry_id) + is True + ) From a598cdfeb30396bf266e3654abe79d64e3f81760 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 28 May 2022 12:51:40 -0700 Subject: [PATCH 0992/3516] Don't import google calendar user pref for disabling new entities (#72652) --- homeassistant/components/google/__init__.py | 29 ++++---- tests/components/google/test_init.py | 82 ++++++++------------- 2 files changed, 42 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index b7263d2e469..1336e9991e3 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -199,11 +199,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.warning( "Configuration of Google Calendar in YAML in configuration.yaml is " "is deprecated and will be removed in a future release; Your existing " - "OAuth Application Credentials and other settings have been imported " + "OAuth Application Credentials and access settings have been imported " "into the UI automatically and can be safely removed from your " "configuration.yaml file" ) - + if conf.get(CONF_TRACK_NEW) is False: + # The track_new as False would previously result in new entries + # in google_calendars.yaml with track set to Fasle which is + # handled at calendar entity creation time. + _LOGGER.warning( + "You must manually set the integration System Options in the " + "UI to disable newly discovered entities going forward" + ) return True @@ -260,23 +267,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def async_upgrade_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Upgrade the config entry if needed.""" - if DATA_CONFIG not in hass.data[DOMAIN] and entry.options: + if entry.options: return - - options = ( - entry.options - if entry.options - else { - CONF_CALENDAR_ACCESS: get_feature_access(hass).name, - } - ) - disable_new_entities = ( - not hass.data[DOMAIN].get(DATA_CONFIG, {}).get(CONF_TRACK_NEW, True) - ) hass.config_entries.async_update_entry( entry, - options=options, - pref_disable_new_entities=disable_new_entities, + options={ + CONF_CALENDAR_ACCESS: get_feature_access(hass).name, + }, ) diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 93c0642514e..b6f7a6b4cbc 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -154,57 +154,6 @@ async def test_calendar_yaml_error( assert hass.states.get(TEST_API_ENTITY) -@pytest.mark.parametrize( - "google_config_track_new,calendars_config,expected_state", - [ - ( - None, - [], - State( - TEST_API_ENTITY, - STATE_OFF, - attributes={ - "offset_reached": False, - "friendly_name": TEST_API_ENTITY_NAME, - }, - ), - ), - ( - True, - [], - State( - TEST_API_ENTITY, - STATE_OFF, - attributes={ - "offset_reached": False, - "friendly_name": TEST_API_ENTITY_NAME, - }, - ), - ), - (False, [], None), - ], - ids=["default", "True", "False"], -) -async def test_track_new( - hass: HomeAssistant, - component_setup: ComponentSetup, - mock_calendars_list: ApiResult, - test_api_calendar: dict[str, Any], - mock_events_list: ApiResult, - mock_calendars_yaml: None, - expected_state: State, - setup_config_entry: MockConfigEntry, -) -> None: - """Test behavior of configuration.yaml settings for tracking new calendars not in the config.""" - - mock_calendars_list({"items": [test_api_calendar]}) - mock_events_list({}) - assert await component_setup() - - state = hass.states.get(TEST_API_ENTITY) - assert_state(state, expected_state) - - @pytest.mark.parametrize("calendars_config", [[]]) async def test_found_calendar_from_api( hass: HomeAssistant, @@ -263,7 +212,7 @@ async def test_load_application_credentials( @pytest.mark.parametrize( - "calendars_config_track,expected_state", + "calendars_config_track,expected_state,google_config_track_new", [ ( True, @@ -275,8 +224,35 @@ async def test_load_application_credentials( "friendly_name": TEST_YAML_ENTITY_NAME, }, ), + None, ), - (False, None), + ( + True, + State( + TEST_YAML_ENTITY, + STATE_OFF, + attributes={ + "offset_reached": False, + "friendly_name": TEST_YAML_ENTITY_NAME, + }, + ), + True, + ), + ( + True, + State( + TEST_YAML_ENTITY, + STATE_OFF, + attributes={ + "offset_reached": False, + "friendly_name": TEST_YAML_ENTITY_NAME, + }, + ), + False, # Has no effect + ), + (False, None, None), + (False, None, True), + (False, None, False), ], ) async def test_calendar_config_track_new( From a4f678e7c9364d1962f3911530d67ee82483f1fe Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 28 May 2022 22:31:03 +0200 Subject: [PATCH 0993/3516] Manage stations via integrations configuration in Tankerkoenig (#72654) --- .../components/tankerkoenig/config_flow.py | 64 +++++++++++++------ .../components/tankerkoenig/strings.json | 2 +- .../tankerkoenig/translations/en.json | 4 +- .../tankerkoenig/test_config_flow.py | 23 +++++-- 4 files changed, 68 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/tankerkoenig/config_flow.py b/homeassistant/components/tankerkoenig/config_flow.py index 65c367d1ba4..af3b5273b16 100644 --- a/homeassistant/components/tankerkoenig/config_flow.py +++ b/homeassistant/components/tankerkoenig/config_flow.py @@ -17,7 +17,7 @@ from homeassistant.const import ( CONF_SHOW_ON_MAP, LENGTH_KILOMETERS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.selector import ( @@ -29,6 +29,24 @@ from homeassistant.helpers.selector import ( from .const import CONF_FUEL_TYPES, CONF_STATIONS, DEFAULT_RADIUS, DOMAIN, FUEL_TYPES +async def async_get_nearby_stations( + hass: HomeAssistant, data: dict[str, Any] +) -> dict[str, Any]: + """Fetch nearby stations.""" + try: + return await hass.async_add_executor_job( + getNearbyStations, + data[CONF_API_KEY], + data[CONF_LOCATION][CONF_LATITUDE], + data[CONF_LOCATION][CONF_LONGITUDE], + data[CONF_RADIUS], + "all", + "dist", + ) + except customException as err: + return {"ok": False, "message": err, "exception": True} + + class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" @@ -57,7 +75,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): selected_station_ids: list[str] = [] # add all nearby stations - nearby_stations = await self._get_nearby_stations(config) + nearby_stations = await async_get_nearby_stations(self.hass, config) for station in nearby_stations.get("stations", []): selected_station_ids.append(station["id"]) @@ -91,7 +109,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) self._abort_if_unique_id_configured() - data = await self._get_nearby_stations(user_input) + data = await async_get_nearby_stations(self.hass, user_input) if not data.get("ok"): return self._show_form_user( user_input, errors={CONF_API_KEY: "invalid_auth"} @@ -182,21 +200,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): options=options, ) - async def _get_nearby_stations(self, data: dict[str, Any]) -> dict[str, Any]: - """Fetch nearby stations.""" - try: - return await self.hass.async_add_executor_job( - getNearbyStations, - data[CONF_API_KEY], - data[CONF_LOCATION][CONF_LATITUDE], - data[CONF_LOCATION][CONF_LONGITUDE], - data[CONF_RADIUS], - "all", - "dist", - ) - except customException as err: - return {"ok": False, "message": err, "exception": True} - class OptionsFlowHandler(config_entries.OptionsFlow): """Handle an options flow.""" @@ -204,14 +207,36 @@ class OptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry + self._stations: dict[str, str] = {} async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle options flow.""" if user_input is not None: + self.hass.config_entries.async_update_entry( + self.config_entry, + data={ + **self.config_entry.data, + CONF_STATIONS: user_input.pop(CONF_STATIONS), + }, + ) return self.async_create_entry(title="", data=user_input) + nearby_stations = await async_get_nearby_stations( + self.hass, dict(self.config_entry.data) + ) + if stations := nearby_stations.get("stations"): + for station in stations: + self._stations[ + station["id"] + ] = f"{station['brand']} {station['street']} {station['houseNumber']} - ({station['dist']}km)" + + # add possible extra selected stations from import + for selected_station in self.config_entry.data[CONF_STATIONS]: + if selected_station not in self._stations: + self._stations[selected_station] = f"id: {selected_station}" + return self.async_show_form( step_id="init", data_schema=vol.Schema( @@ -220,6 +245,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_SHOW_ON_MAP, default=self.config_entry.options[CONF_SHOW_ON_MAP], ): bool, + vol.Required( + CONF_STATIONS, default=self.config_entry.data[CONF_STATIONS] + ): cv.multi_select(self._stations), } ), ) diff --git a/homeassistant/components/tankerkoenig/strings.json b/homeassistant/components/tankerkoenig/strings.json index 7c1ba54fcc0..5e0c367c192 100644 --- a/homeassistant/components/tankerkoenig/strings.json +++ b/homeassistant/components/tankerkoenig/strings.json @@ -32,7 +32,7 @@ "init": { "title": "Tankerkoenig options", "data": { - "scan_interval": "Update Interval", + "stations": "Stations", "show_on_map": "Show stations on map" } } diff --git a/homeassistant/components/tankerkoenig/translations/en.json b/homeassistant/components/tankerkoenig/translations/en.json index 399788de8f4..83cc36fd4c8 100644 --- a/homeassistant/components/tankerkoenig/translations/en.json +++ b/homeassistant/components/tankerkoenig/translations/en.json @@ -31,8 +31,8 @@ "step": { "init": { "data": { - "scan_interval": "Update Interval", - "show_on_map": "Show stations on map" + "show_on_map": "Show stations on map", + "stations": "Stations" }, "title": "Tankerkoenig options" } diff --git a/tests/components/tankerkoenig/test_config_flow.py b/tests/components/tankerkoenig/test_config_flow.py index 0a90b424b73..b18df0eed24 100644 --- a/tests/components/tankerkoenig/test_config_flow.py +++ b/tests/components/tankerkoenig/test_config_flow.py @@ -42,6 +42,15 @@ MOCK_STATIONS_DATA = { ], } +MOCK_OPTIONS_DATA = { + **MOCK_USER_DATA, + CONF_STATIONS: [ + "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", + "36b4b812-xxxx-xxxx-xxxx-c51735325858", + "54e2b642-xxxx-xxxx-xxxx-87cd4e9867f1", + ], +} + MOCK_IMPORT_DATA = { CONF_API_KEY: "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx", CONF_FUEL_TYPES: ["e5"], @@ -217,7 +226,7 @@ async def test_options_flow(hass: HomeAssistant): mock_config = MockConfigEntry( domain=DOMAIN, - data=MOCK_USER_DATA, + data=MOCK_OPTIONS_DATA, options={CONF_SHOW_ON_MAP: True}, unique_id=f"{DOMAIN}_{MOCK_USER_DATA[CONF_LOCATION][CONF_LATITUDE]}_{MOCK_USER_DATA[CONF_LOCATION][CONF_LONGITUDE]}", ) @@ -225,17 +234,23 @@ async def test_options_flow(hass: HomeAssistant): with patch( "homeassistant.components.tankerkoenig.async_setup_entry" - ) as mock_setup_entry: + ) as mock_setup_entry, patch( + "homeassistant.components.tankerkoenig.config_flow.getNearbyStations", + return_value=MOCK_NEARVY_STATIONS_OK, + ): await mock_config.async_setup(hass) await hass.async_block_till_done() assert mock_setup_entry.called - result = await hass.config_entries.options.async_init(mock_config.entry_id) + result = await hass.config_entries.options.async_init(mock_config.entry_id) assert result["type"] == 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}, + user_input={ + CONF_SHOW_ON_MAP: False, + CONF_STATIONS: MOCK_OPTIONS_DATA[CONF_STATIONS], + }, ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert not mock_config.options[CONF_SHOW_ON_MAP] From 24c34c0ef0e38d37cd94e4806d1d0a5b715cfe7d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 29 May 2022 01:26:50 +0200 Subject: [PATCH 0994/3516] Strict typing Sensibo (#72454) --- .strict-typing | 1 + .../components/sensibo/binary_sensor.py | 22 +++++--- homeassistant/components/sensibo/climate.py | 54 +++++++++++++------ .../components/sensibo/config_flow.py | 4 +- homeassistant/components/sensibo/entity.py | 6 ++- .../components/sensibo/manifest.json | 3 +- homeassistant/components/sensibo/number.py | 5 +- homeassistant/components/sensibo/select.py | 7 ++- homeassistant/components/sensibo/sensor.py | 21 +++++--- homeassistant/components/sensibo/update.py | 2 + homeassistant/components/sensibo/util.py | 2 +- mypy.ini | 11 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sensibo/test_climate.py | 34 ++++++++++++ 15 files changed, 136 insertions(+), 40 deletions(-) diff --git a/.strict-typing b/.strict-typing index e07d8b9cfc8..7fe03203583 100644 --- a/.strict-typing +++ b/.strict-typing @@ -196,6 +196,7 @@ homeassistant.components.rtsp_to_webrtc.* homeassistant.components.samsungtv.* homeassistant.components.scene.* homeassistant.components.select.* +homeassistant.components.sensibo.* homeassistant.components.sensor.* homeassistant.components.senseme.* homeassistant.components.senz.* diff --git a/homeassistant/components/sensibo/binary_sensor.py b/homeassistant/components/sensibo/binary_sensor.py index 27e551a51c8..e8d83f04593 100644 --- a/homeassistant/components/sensibo/binary_sensor.py +++ b/homeassistant/components/sensibo/binary_sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from typing import TYPE_CHECKING from pysensibo.model import MotionSensor, SensiboDevice @@ -20,6 +21,8 @@ from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator from .entity import SensiboDeviceBaseEntity, SensiboMotionBaseEntity +PARALLEL_UPDATES = 0 + @dataclass class MotionBaseEntityDescriptionMixin: @@ -93,13 +96,16 @@ async def async_setup_entry( coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] entities: list[SensiboMotionSensor | SensiboDeviceSensor] = [] - entities.extend( - SensiboMotionSensor(coordinator, device_id, sensor_id, sensor_data, description) - for device_id, device_data in coordinator.data.parsed.items() - for sensor_id, sensor_data in device_data.motion_sensors.items() - for description in MOTION_SENSOR_TYPES - if device_data.motion_sensors - ) + + for device_id, device_data in coordinator.data.parsed.items(): + if device_data.motion_sensors: + entities.extend( + SensiboMotionSensor( + coordinator, device_id, sensor_id, sensor_data, description + ) + for sensor_id, sensor_data in device_data.motion_sensors.items() + for description in MOTION_SENSOR_TYPES + ) entities.extend( SensiboDeviceSensor(coordinator, device_id, description) for description in DEVICE_SENSOR_TYPES @@ -140,6 +146,8 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, BinarySensorEntity): @property def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" + if TYPE_CHECKING: + assert self.sensor_data return self.entity_description.value_fn(self.sensor_data) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index c1e690cd28a..4b0e797a5b7 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -1,6 +1,8 @@ """Support for Sensibo wifi-enabled home thermostats.""" from __future__ import annotations +from typing import TYPE_CHECKING, Any + import voluptuous as vol from homeassistant.components.climate import ClimateEntity @@ -24,6 +26,7 @@ from .coordinator import SensiboDataUpdateCoordinator from .entity import SensiboDeviceBaseEntity SERVICE_ASSUME_STATE = "assume_state" +PARALLEL_UPDATES = 0 FIELD_TO_FLAG = { "fanLevel": ClimateEntityFeature.FAN_MODE, @@ -107,70 +110,87 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): @property def hvac_mode(self) -> HVACMode: """Return hvac operation.""" - if self.device_data.device_on: + if self.device_data.device_on and self.device_data.hvac_mode: return SENSIBO_TO_HA[self.device_data.hvac_mode] return HVACMode.OFF @property def hvac_modes(self) -> list[HVACMode]: """Return the list of available hvac operation modes.""" - return [SENSIBO_TO_HA[mode] for mode in self.device_data.hvac_modes] + hvac_modes = [] + if TYPE_CHECKING: + assert self.device_data.hvac_modes + for mode in self.device_data.hvac_modes: + hvac_modes.append(SENSIBO_TO_HA[mode]) + return hvac_modes if hvac_modes else [HVACMode.OFF] @property def current_temperature(self) -> float | None: """Return the current temperature.""" - return convert_temperature( - self.device_data.temp, - TEMP_CELSIUS, - self.temperature_unit, - ) + if self.device_data.temp: + return convert_temperature( + self.device_data.temp, + TEMP_CELSIUS, + self.temperature_unit, + ) + return None @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" - return self.device_data.target_temp + target_temp: int | None = self.device_data.target_temp + return target_temp @property def target_temperature_step(self) -> float | None: """Return the supported step of target temperature.""" - return self.device_data.temp_step + target_temp_step: int = self.device_data.temp_step + return target_temp_step @property def fan_mode(self) -> str | None: """Return the fan setting.""" - return self.device_data.fan_mode + fan_mode: str | None = self.device_data.fan_mode + return fan_mode @property def fan_modes(self) -> list[str] | None: """Return the list of available fan modes.""" - return self.device_data.fan_modes + if self.device_data.fan_modes: + return self.device_data.fan_modes + return None @property def swing_mode(self) -> str | None: """Return the swing setting.""" - return self.device_data.swing_mode + swing_mode: str | None = self.device_data.swing_mode + return swing_mode @property def swing_modes(self) -> list[str] | None: """Return the list of available swing modes.""" - return self.device_data.swing_modes + if self.device_data.swing_modes: + return self.device_data.swing_modes + return None @property def min_temp(self) -> float: """Return the minimum temperature.""" - return self.device_data.temp_list[0] + min_temp: int = self.device_data.temp_list[0] + return min_temp @property def max_temp(self) -> float: """Return the maximum temperature.""" - return self.device_data.temp_list[-1] + max_temp: int = self.device_data.temp_list[-1] + return max_temp @property def available(self) -> bool: """Return True if entity is available.""" return self.device_data.available and super().available - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if "targetTemperature" not in self.device_data.active_features: raise HomeAssistantError( @@ -255,7 +275,7 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): f"Could not set state for device {self.name} due to reason {failure}" ) - async def async_assume_state(self, state) -> None: + async def async_assume_state(self, state: str) -> None: """Sync state with api.""" await self._async_set_ac_state_property("on", state != HVACMode.OFF, True) await self.coordinator.async_refresh() diff --git a/homeassistant/components/sensibo/config_flow.py b/homeassistant/components/sensibo/config_flow.py index c4b637e4439..a3254a01839 100644 --- a/homeassistant/components/sensibo/config_flow.py +++ b/homeassistant/components/sensibo/config_flow.py @@ -75,7 +75,9 @@ class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - 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 the initial step.""" errors: dict[str, str] = {} diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index ce85ecf2a38..c2f4869a4e6 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -1,7 +1,7 @@ """Base entity for Sensibo integration.""" from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import async_timeout from pysensibo.model import MotionSensor, SensiboDevice @@ -119,6 +119,8 @@ class SensiboMotionBaseEntity(SensiboBaseEntity): ) @property - def sensor_data(self) -> MotionSensor: + def sensor_data(self) -> MotionSensor | None: """Return data for device.""" + if TYPE_CHECKING: + assert self.device_data.motion_sensors return self.device_data.motion_sensors[self._sensor_id] diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 308d991e675..c289322d584 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,10 +2,11 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": ["pysensibo==1.0.14"], + "requirements": ["pysensibo==1.0.15"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", + "quality_scale": "platinum", "homekit": { "models": ["Sensibo"] }, diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py index fc18e28f1a3..89bd9b270a9 100644 --- a/homeassistant/components/sensibo/number.py +++ b/homeassistant/components/sensibo/number.py @@ -14,6 +14,8 @@ from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator from .entity import SensiboDeviceBaseEntity +PARALLEL_UPDATES = 0 + @dataclass class SensiboEntityDescriptionMixin: @@ -89,7 +91,8 @@ class SensiboNumber(SensiboDeviceBaseEntity, NumberEntity): @property def value(self) -> float | None: """Return the value from coordinator data.""" - return getattr(self.device_data, self.entity_description.key) + value: float | None = getattr(self.device_data, self.entity_description.key) + return value async def async_set_value(self, value: float) -> None: """Set value for calibration.""" diff --git a/homeassistant/components/sensibo/select.py b/homeassistant/components/sensibo/select.py index 56b8fbac4fd..f64411ff4dc 100644 --- a/homeassistant/components/sensibo/select.py +++ b/homeassistant/components/sensibo/select.py @@ -13,6 +13,8 @@ from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator from .entity import SensiboDeviceBaseEntity +PARALLEL_UPDATES = 0 + @dataclass class SensiboSelectDescriptionMixin: @@ -82,7 +84,10 @@ class SensiboSelect(SensiboDeviceBaseEntity, SelectEntity): @property def current_option(self) -> str | None: """Return the current selected option.""" - return getattr(self.device_data, self.entity_description.remote_key) + option: str | None = getattr( + self.device_data, self.entity_description.remote_key + ) + return option @property def options(self) -> list[str]: diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 7ac871a61eb..7254948bdad 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from typing import TYPE_CHECKING from pysensibo.model import MotionSensor, SensiboDevice @@ -29,6 +30,8 @@ from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator from .entity import SensiboDeviceBaseEntity, SensiboMotionBaseEntity +PARALLEL_UPDATES = 0 + @dataclass class MotionBaseEntityDescriptionMixin: @@ -127,13 +130,15 @@ async def async_setup_entry( entities: list[SensiboMotionSensor | SensiboDeviceSensor] = [] - entities.extend( - SensiboMotionSensor(coordinator, device_id, sensor_id, sensor_data, description) - for device_id, device_data in coordinator.data.parsed.items() - for sensor_id, sensor_data in device_data.motion_sensors.items() - for description in MOTION_SENSOR_TYPES - if device_data.motion_sensors - ) + for device_id, device_data in coordinator.data.parsed.items(): + if device_data.motion_sensors: + entities.extend( + SensiboMotionSensor( + coordinator, device_id, sensor_id, sensor_data, description + ) + for sensor_id, sensor_data in device_data.motion_sensors.items() + for description in MOTION_SENSOR_TYPES + ) entities.extend( SensiboDeviceSensor(coordinator, device_id, description) for device_id, device_data in coordinator.data.parsed.items() @@ -173,6 +178,8 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, SensorEntity): @property def native_value(self) -> StateType: """Return value of sensor.""" + if TYPE_CHECKING: + assert self.sensor_data return self.entity_description.value_fn(self.sensor_data) diff --git a/homeassistant/components/sensibo/update.py b/homeassistant/components/sensibo/update.py index 6e227a891b0..48304cbd3c5 100644 --- a/homeassistant/components/sensibo/update.py +++ b/homeassistant/components/sensibo/update.py @@ -20,6 +20,8 @@ from .const import DOMAIN from .coordinator import SensiboDataUpdateCoordinator from .entity import SensiboDeviceBaseEntity +PARALLEL_UPDATES = 0 + @dataclass class DeviceBaseEntityDescriptionMixin: diff --git a/homeassistant/components/sensibo/util.py b/homeassistant/components/sensibo/util.py index fda9d4a210e..8a181cbe568 100644 --- a/homeassistant/components/sensibo/util.py +++ b/homeassistant/components/sensibo/util.py @@ -31,7 +31,7 @@ async def async_validate_api(hass: HomeAssistant, api_key: str) -> str: raise ConnectionError from err devices = device_query["result"] - user = user_query["result"].get("username") + user: str = user_query["result"].get("username") if not devices: LOGGER.error("Could not retrieve any devices from Sensibo servers") raise NoDevicesError diff --git a/mypy.ini b/mypy.ini index e0c512782fb..e2159766fb4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1919,6 +1919,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.sensibo.*] +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.sensor.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index bf7769454f8..93c151fd843 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1792,7 +1792,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.14 +pysensibo==1.0.15 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 698dc977f2b..718e876aa2f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1211,7 +1211,7 @@ pyruckus==0.12 pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.14 +pysensibo==1.0.15 # homeassistant.components.serial # homeassistant.components.zha diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index 0b6c043240c..79813727c15 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -624,3 +624,37 @@ async def test_climate_assumed_state( state2 = hass.states.get("climate.hallway") assert state2.state == "off" + + +async def test_climate_no_fan_no_swing( + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: pytest.MonkeyPatch, + get_data: SensiboData, +) -> None: + """Test the Sensibo climate fan service.""" + + state = hass.states.get("climate.hallway") + assert state.attributes["fan_mode"] == "high" + assert state.attributes["swing_mode"] == "stopped" + + monkeypatch.setattr(get_data.parsed["ABC999111"], "fan_mode", None) + monkeypatch.setattr(get_data.parsed["ABC999111"], "swing_mode", None) + monkeypatch.setattr(get_data.parsed["ABC999111"], "fan_modes", None) + monkeypatch.setattr(get_data.parsed["ABC999111"], "swing_modes", None) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state = hass.states.get("climate.hallway") + assert state.attributes["fan_mode"] is None + assert state.attributes["swing_mode"] is None + assert state.attributes["fan_modes"] is None + assert state.attributes["swing_modes"] is None From 7a0657c386e2ec28ad1390a339b6b2e8033f1fb1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 29 May 2022 00:23:25 +0000 Subject: [PATCH 0995/3516] [ci skip] Translation update --- .../accuweather/translations/he.json | 3 +++ .../translations/zh-Hans.json | 3 +++ .../arcam_fmj/translations/zh-Hans.json | 5 +++++ .../button/translations/zh-Hans.json | 2 +- .../components/fan/translations/zh-Hans.json | 2 ++ .../components/group/translations/he.json | 16 +++++++------- .../here_travel_time/translations/he.json | 18 ++++++++++++++++ .../components/hue/translations/zh-Hans.json | 13 ++++++------ .../humidifier/translations/zh-Hans.json | 1 + .../components/ialarm_xr/translations/he.json | 21 +++++++++++++++++++ .../integration/translations/he.json | 6 +++--- .../intellifire/translations/he.json | 3 ++- .../components/kodi/translations/zh-Hans.json | 4 ++-- .../components/laundrify/translations/he.json | 17 +++++++++++++++ .../light/translations/zh-Hans.json | 1 + .../litterrobot/translations/sensor.he.json | 3 ++- .../media_player/translations/zh-Hans.json | 3 +++ .../remote/translations/zh-Hans.json | 1 + .../sensor/translations/zh-Hans.json | 13 ++++++++++++ .../simplisafe/translations/he.json | 5 +++++ .../tankerkoenig/translations/de.json | 3 ++- .../tankerkoenig/translations/en.json | 1 + .../tankerkoenig/translations/pt-BR.json | 3 ++- .../xiaomi_aqara/translations/he.json | 2 +- .../components/zha/translations/zh-Hans.json | 15 +++++++++---- .../zwave_js/translations/zh-Hans.json | 7 +++++++ 26 files changed, 142 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/here_travel_time/translations/he.json create mode 100644 homeassistant/components/ialarm_xr/translations/he.json create mode 100644 homeassistant/components/laundrify/translations/he.json create mode 100644 homeassistant/components/zwave_js/translations/zh-Hans.json diff --git a/homeassistant/components/accuweather/translations/he.json b/homeassistant/components/accuweather/translations/he.json index 0f054ff11fe..f4b95268a25 100644 --- a/homeassistant/components/accuweather/translations/he.json +++ b/homeassistant/components/accuweather/translations/he.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." }, + "create_entry": { + "default": "\u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05de\u05e1\u05d5\u05d9\u05de\u05d9\u05dd \u05d0\u05d9\u05e0\u05dd \u05de\u05d5\u05e4\u05e2\u05dc\u05d9\u05dd \u05db\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc. \u05d1\u05d9\u05db\u05d5\u05dc\u05ea\u05da \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05d0\u05d5\u05ea\u05dd \u05d1\u05e8\u05d9\u05e9\u05d5\u05dd \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05dc\u05d0\u05d7\u05e8 \u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1.\n \u05ea\u05d7\u05d6\u05d9\u05ea \u05de\u05d6\u05d2 \u05d4\u05d0\u05d5\u05d5\u05d9\u05e8 \u05d0\u05d9\u05e0\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc\u05ea \u05db\u05d1\u05e8\u05d9\u05e8\u05ea \u05de\u05d7\u05d3\u05dc. \u05d1\u05d9\u05db\u05d5\u05dc\u05ea\u05da \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05d0\u05ea \u05d6\u05d4 \u05d1\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1." + }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", diff --git a/homeassistant/components/alarm_control_panel/translations/zh-Hans.json b/homeassistant/components/alarm_control_panel/translations/zh-Hans.json index e955d21afdb..1e9694ddb14 100644 --- a/homeassistant/components/alarm_control_panel/translations/zh-Hans.json +++ b/homeassistant/components/alarm_control_panel/translations/zh-Hans.json @@ -4,6 +4,7 @@ "arm_away": "{entity_name} \u79bb\u5bb6\u8b66\u6212", "arm_home": "{entity_name} \u5728\u5bb6\u8b66\u6212", "arm_night": "{entity_name} \u591c\u95f4\u8b66\u6212", + "arm_vacation": "{entity_name} \u5ea6\u5047\u8b66\u6212", "disarm": "\u89e3\u9664 {entity_name} \u8b66\u6212", "trigger": "\u89e6\u53d1 {entity_name}" }, @@ -11,6 +12,7 @@ "is_armed_away": "{entity_name} \u79bb\u5bb6\u8b66\u6212", "is_armed_home": "{entity_name} \u5728\u5bb6\u8b66\u6212", "is_armed_night": "{entity_name} \u591c\u95f4\u8b66\u6212", + "is_armed_vacation": "{entity_name} \u5ea6\u5047\u8b66\u6212", "is_disarmed": "{entity_name} \u8b66\u6212\u5df2\u89e3\u9664", "is_triggered": "{entity_name} \u8b66\u62a5\u5df2\u89e6\u53d1" }, @@ -18,6 +20,7 @@ "armed_away": "{entity_name} \u79bb\u5bb6\u8b66\u6212", "armed_home": "{entity_name} \u5728\u5bb6\u8b66\u6212", "armed_night": "{entity_name} \u591c\u95f4\u8b66\u6212", + "armed_vacation": "{entity_name} \u5ea6\u5047\u8b66\u6212", "disarmed": "{entity_name} \u8b66\u6212\u89e3\u9664", "triggered": "{entity_name} \u89e6\u53d1\u8b66\u62a5" } diff --git a/homeassistant/components/arcam_fmj/translations/zh-Hans.json b/homeassistant/components/arcam_fmj/translations/zh-Hans.json index 6e842e66fab..68057bbb8a1 100644 --- a/homeassistant/components/arcam_fmj/translations/zh-Hans.json +++ b/homeassistant/components/arcam_fmj/translations/zh-Hans.json @@ -3,5 +3,10 @@ "abort": { "cannot_connect": "\u8fde\u63a5\u5931\u8d25" } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} \u88ab\u8981\u6c42\u6253\u5f00" + } } } \ No newline at end of file diff --git a/homeassistant/components/button/translations/zh-Hans.json b/homeassistant/components/button/translations/zh-Hans.json index 88c70556aa1..12fddc42e13 100644 --- a/homeassistant/components/button/translations/zh-Hans.json +++ b/homeassistant/components/button/translations/zh-Hans.json @@ -1,7 +1,7 @@ { "device_automation": { "action_type": { - "press": "\u6309\u4e0b {entity_name} \u6309\u94ae" + "press": "\u6309\u4e0b {entity_name} \u7684\u6309\u94ae" }, "trigger_type": { "pressed": "{entity_name} \u88ab\u6309\u4e0b" diff --git a/homeassistant/components/fan/translations/zh-Hans.json b/homeassistant/components/fan/translations/zh-Hans.json index 13b6917f4ad..0bcc6440627 100644 --- a/homeassistant/components/fan/translations/zh-Hans.json +++ b/homeassistant/components/fan/translations/zh-Hans.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "\u5207\u6362 {entity_name} \u5f00\u5173", "turn_off": "\u5173\u95ed {entity_name}", "turn_on": "\u6253\u5f00 {entity_name}" }, @@ -9,6 +10,7 @@ "is_on": "{entity_name} \u5df2\u5f00\u542f" }, "trigger_type": { + "changed_states": "{entity_name} \u88ab\u6253\u5f00\u6216\u5173\u95ed", "turned_off": "{entity_name} \u88ab\u5173\u95ed", "turned_on": "{entity_name} \u88ab\u6253\u5f00" } diff --git a/homeassistant/components/group/translations/he.json b/homeassistant/components/group/translations/he.json index 354a435f491..a2507082dda 100644 --- a/homeassistant/components/group/translations/he.json +++ b/homeassistant/components/group/translations/he.json @@ -9,7 +9,7 @@ "name": "\u05e9\u05dd" }, "description": "\u05d0\u05dd \u05d4\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d6\u05de\u05d9\u05e0\u05d4, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05e8\u05e7 \u05d0\u05dd \u05db\u05dc \u05d4\u05d7\u05d1\u05e8\u05d9\u05dd \u05e4\u05d5\u05e2\u05dc\u05d9\u05dd. \u05d0\u05dd \"\u05db\u05dc \u05d4\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea\" \u05d0\u05d9\u05e0\u05d5 \u05d6\u05de\u05d9\u05df, \u05de\u05e6\u05d1 \u05d4\u05e7\u05d1\u05d5\u05e6\u05d4 \u05de\u05d5\u05e4\u05e2\u05dc \u05d0\u05dd \u05d7\u05d1\u05e8 \u05db\u05dc\u05e9\u05d4\u05d5 \u05e4\u05d5\u05e2\u05dc.", - "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + "title": "\u05d4\u05d5\u05e1\u05e4\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" }, "cover": { "data": { @@ -17,7 +17,7 @@ "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd", "name": "\u05e9\u05dd" }, - "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + "title": "\u05d4\u05d5\u05e1\u05e4\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" }, "fan": { "data": { @@ -25,7 +25,7 @@ "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd", "name": "\u05e9\u05dd" }, - "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + "title": "\u05d4\u05d5\u05e1\u05e4\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" }, "light": { "data": { @@ -33,7 +33,7 @@ "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd", "name": "\u05e9\u05dd" }, - "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + "title": "\u05d4\u05d5\u05e1\u05e4\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" }, "lock": { "data": { @@ -41,7 +41,7 @@ "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd", "name": "\u05e9\u05dd" }, - "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + "title": "\u05d4\u05d5\u05e1\u05e4\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" }, "media_player": { "data": { @@ -49,7 +49,7 @@ "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd", "name": "\u05e9\u05dd" }, - "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + "title": "\u05d4\u05d5\u05e1\u05e4\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" }, "switch": { "data": { @@ -57,7 +57,7 @@ "hide_members": "\u05d4\u05e1\u05ea\u05e8\u05ea \u05d7\u05d1\u05e8\u05d9\u05dd", "name": "\u05e9\u05dd" }, - "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + "title": "\u05d4\u05d5\u05e1\u05e4\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" }, "user": { "description": "\u05e7\u05d1\u05d5\u05e6\u05d5\u05ea \u05de\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05da \u05dc\u05d9\u05e6\u05d5\u05e8 \u05d9\u05e9\u05d5\u05ea \u05d7\u05d3\u05e9\u05d4 \u05d4\u05de\u05d9\u05d9\u05e6\u05d2\u05ea \u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05de\u05e8\u05d5\u05d1\u05d5\u05ea \u05de\u05d0\u05d5\u05ea\u05d5 \u05e1\u05d5\u05d2.", @@ -68,7 +68,7 @@ "light": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05ea\u05d0\u05d5\u05e8\u05d4", "media_player": "\u05e7\u05d1\u05d5\u05e6\u05ea \u05e0\u05d2\u05e0\u05d9 \u05de\u05d3\u05d9\u05d4" }, - "title": "\u05e7\u05d1\u05d5\u05e6\u05d4 \u05d7\u05d3\u05e9\u05d4" + "title": "\u05d4\u05d5\u05e1\u05e4\u05ea \u05e7\u05d1\u05d5\u05e6\u05d4" } } }, diff --git a/homeassistant/components/here_travel_time/translations/he.json b/homeassistant/components/here_travel_time/translations/he.json new file mode 100644 index 00000000000..dc5eb786f67 --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "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/hue/translations/zh-Hans.json b/homeassistant/components/hue/translations/zh-Hans.json index b883262a8d6..d7ec6ebaeb4 100644 --- a/homeassistant/components/hue/translations/zh-Hans.json +++ b/homeassistant/components/hue/translations/zh-Hans.json @@ -32,19 +32,20 @@ "2": "\u7b2c\u4e8c\u952e", "3": "\u7b2c\u4e09\u952e", "4": "\u7b2c\u56db\u952e", - "turn_off": "\u5173\u95ed" + "turn_off": "\u5173\u95ed", + "turn_on": "\u6253\u5f00" }, "trigger_type": { - "double_short_release": "\u201c{subtype}\u201d\u4e24\u952e\u540c\u65f6\u677e\u5f00", - "initial_press": "\u201c{subtype}\u201d\u9996\u6b21\u6309\u4e0b", - "long_release": "\u201c{subtype}\u201d\u957f\u6309\u540e\u677e\u5f00", + "double_short_release": "\"{subtype}\" \u4e24\u952e\u540c\u65f6\u677e\u5f00", + "initial_press": "\"{subtype}\" \u9996\u6b21\u6309\u4e0b", + "long_release": "\"{subtype}\" \u957f\u6309\u540e\u677e\u5f00", "remote_button_long_release": "\"{subtype}\" \u957f\u6309\u540e\u677e\u5f00", "remote_button_short_press": "\"{subtype}\" \u5355\u51fb", "remote_button_short_release": "\"{subtype}\" \u677e\u5f00", "remote_double_button_long_press": "\"{subtype}\" \u4e24\u952e\u540c\u65f6\u957f\u6309\u540e\u677e\u5f00", "remote_double_button_short_press": "\"{subtype}\" \u4e24\u952e\u540c\u65f6\u677e\u5f00", - "repeat": "\u201c{subtype}\u201d\u6309\u4f4f\u4e0d\u653e", - "short_release": "\u201c{subtype}\u201d\u77ed\u6309\u540e\u677e\u5f00" + "repeat": "\"{subtype}\" \u6309\u4f4f\u4e0d\u653e", + "short_release": "\"{subtype}\" \u77ed\u6309\u540e\u677e\u5f00" } }, "options": { diff --git a/homeassistant/components/humidifier/translations/zh-Hans.json b/homeassistant/components/humidifier/translations/zh-Hans.json index d21c7bf61f7..230afd3e4ab 100644 --- a/homeassistant/components/humidifier/translations/zh-Hans.json +++ b/homeassistant/components/humidifier/translations/zh-Hans.json @@ -13,6 +13,7 @@ "is_on": "{entity_name} \u5df2\u6253\u5f00" }, "trigger_type": { + "changed_states": "{entity_name} \u88ab\u6253\u5f00\u6216\u5173\u95ed", "target_humidity_changed": "{entity_name} \u7684\u8bbe\u5b9a\u6e7f\u5ea6\u53d8\u5316", "turned_off": "{entity_name} \u88ab\u5173\u95ed", "turned_on": "{entity_name} \u88ab\u6253\u5f00" diff --git a/homeassistant/components/ialarm_xr/translations/he.json b/homeassistant/components/ialarm_xr/translations/he.json new file mode 100644 index 00000000000..b3fb785d55e --- /dev/null +++ b/homeassistant/components/ialarm_xr/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "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": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/he.json b/homeassistant/components/integration/translations/he.json index 219c75605bb..b4aa653fd20 100644 --- a/homeassistant/components/integration/translations/he.json +++ b/homeassistant/components/integration/translations/he.json @@ -8,13 +8,13 @@ "round": "\u05d3\u05d9\u05d5\u05e7", "source": "\u05d7\u05d9\u05d9\u05e9\u05df \u05e7\u05dc\u05d8", "unit_prefix": "\u05e7\u05d9\u05d3\u05d5\u05de\u05ea \u05de\u05d8\u05e8\u05d9\u05ea", - "unit_time": "\u05d6\u05de\u05df \u05e9\u05d9\u05dc\u05d5\u05d1" + "unit_time": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05d6\u05de\u05df" }, "data_description": { "round": "\u05e9\u05dc\u05d9\u05d8\u05d4 \u05d1\u05de\u05e1\u05e4\u05e8 \u05d4\u05e1\u05e4\u05e8\u05d5\u05ea \u05d4\u05e2\u05e9\u05e8\u05d5\u05e0\u05d9\u05d5\u05ea \u05d1\u05e4\u05dc\u05d8." }, - "description": "\u05d3\u05d9\u05d5\u05e7 \u05e9\u05d5\u05dc\u05d8 \u05d1\u05de\u05e1\u05e4\u05e8 \u05d4\u05e1\u05e4\u05e8\u05d5\u05ea \u05d4\u05e2\u05e9\u05e8\u05d5\u05e0\u05d9\u05d5\u05ea \u05d1\u05e4\u05dc\u05d8.\n\u05d4\u05e1\u05db\u05d5\u05dd \u05d9\u05e9\u05ea\u05e0\u05d4 \u05d1\u05d4\u05ea\u05d0\u05dd \u05dc\u05e7\u05d9\u05d3\u05d5\u05de\u05ea \u05d4\u05de\u05d8\u05e8\u05d9\u05ea \u05e9\u05e0\u05d1\u05d7\u05e8\u05d4 \u05d5\u05d6\u05de\u05df \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1.", - "title": "\u05d7\u05d9\u05d9\u05e9\u05df \u05e9\u05d9\u05dc\u05d5\u05d1 \u05d7\u05d3\u05e9" + "description": "\u05e6\u05d5\u05e8 \u05d7\u05d9\u05d9\u05e9\u05df \u05d4\u05de\u05d7\u05e9\u05d1 \u05e1\u05db\u05d5\u05dd Riemann \u05db\u05d3\u05d9 \u05dc\u05d4\u05e2\u05e8\u05d9\u05da \u05d0\u05ea \u05d4\u05d0\u05d9\u05e0\u05d8\u05d2\u05e8\u05dc \u05e9\u05dc \u05d7\u05d9\u05d9\u05e9\u05df.", + "title": "\u05d4\u05d5\u05e1\u05e3 \u05d7\u05d9\u05d9\u05e9\u05df \u05e1\u05db\u05d5\u05dd \u05d0\u05d9\u05e0\u05d8\u05d2\u05e8\u05dc\u05d9 \u05e9\u05dc Riemann" } } }, diff --git a/homeassistant/components/intellifire/translations/he.json b/homeassistant/components/intellifire/translations/he.json index 18cf71bf358..bb6f3c30c76 100644 --- a/homeassistant/components/intellifire/translations/he.json +++ b/homeassistant/components/intellifire/translations/he.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, + "flow_title": "{serial} ({host})", "step": { "api_config": { "data": { @@ -16,7 +17,7 @@ }, "manual_device_entry": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7" + "host": "\u05de\u05d0\u05e8\u05d7 (\u05db\u05ea\u05d5\u05d1\u05ea IP)" } }, "pick_device": { diff --git a/homeassistant/components/kodi/translations/zh-Hans.json b/homeassistant/components/kodi/translations/zh-Hans.json index 12915ccdb9b..b76fc3c861c 100644 --- a/homeassistant/components/kodi/translations/zh-Hans.json +++ b/homeassistant/components/kodi/translations/zh-Hans.json @@ -43,8 +43,8 @@ }, "device_automation": { "trigger_type": { - "turn_off": "[entity_name} \u88ab\u8981\u6c42\u5173\u95ed", - "turn_on": "[entity_name} \u88ab\u8981\u6c42\u6253\u5f00" + "turn_off": "{entity_name} \u88ab\u8981\u6c42\u5173\u95ed", + "turn_on": "{entity_name} \u88ab\u8981\u6c42\u6253\u5f00" } } } \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/he.json b/homeassistant/components/laundrify/translations/he.json new file mode 100644 index 00000000000..9c8c4c3b8c8 --- /dev/null +++ b/homeassistant/components/laundrify/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "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": { + "reauth_confirm": { + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/translations/zh-Hans.json b/homeassistant/components/light/translations/zh-Hans.json index 1054820c6bb..93f16833984 100644 --- a/homeassistant/components/light/translations/zh-Hans.json +++ b/homeassistant/components/light/translations/zh-Hans.json @@ -13,6 +13,7 @@ "is_on": "{entity_name} \u5df2\u6253\u5f00" }, "trigger_type": { + "changed_states": "{entity_name} \u88ab\u6253\u5f00\u6216\u5173\u95ed", "turned_off": "{entity_name} \u88ab\u5173\u95ed", "turned_on": "{entity_name} \u88ab\u6253\u5f00" } diff --git a/homeassistant/components/litterrobot/translations/sensor.he.json b/homeassistant/components/litterrobot/translations/sensor.he.json index 0c693d10157..73f622d7a1a 100644 --- a/homeassistant/components/litterrobot/translations/sensor.he.json +++ b/homeassistant/components/litterrobot/translations/sensor.he.json @@ -2,7 +2,8 @@ "state": { "litterrobot__status_code": { "off": "\u05db\u05d1\u05d5\u05d9", - "p": "\u05de\u05d5\u05e9\u05d4\u05d4" + "p": "\u05de\u05d5\u05e9\u05d4\u05d4", + "rdy": "\u05de\u05d5\u05db\u05df" } } } \ No newline at end of file diff --git a/homeassistant/components/media_player/translations/zh-Hans.json b/homeassistant/components/media_player/translations/zh-Hans.json index 0fa034898c3..fe1ec28d8a1 100644 --- a/homeassistant/components/media_player/translations/zh-Hans.json +++ b/homeassistant/components/media_player/translations/zh-Hans.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_buffering": "{entity_name} \u6b63\u5728\u7f13\u51b2", "is_idle": "{entity_name} \u7a7a\u95f2", "is_off": "{entity_name} \u5df2\u5173\u95ed", "is_on": "{entity_name} \u5df2\u5f00\u542f", @@ -8,6 +9,8 @@ "is_playing": "{entity_name} \u6b63\u5728\u64ad\u653e" }, "trigger_type": { + "buffering": "{entity_name} \u5f00\u59cb\u7f13\u51b2", + "changed_states": "{entity_name} \u72b6\u6001\u53d8\u5316", "idle": "{entity_name} \u7a7a\u95f2", "paused": "{entity_name} \u6682\u505c", "playing": "{entity_name} \u5f00\u59cb\u64ad\u653e", diff --git a/homeassistant/components/remote/translations/zh-Hans.json b/homeassistant/components/remote/translations/zh-Hans.json index f6c509d4a08..4e9430f84e1 100644 --- a/homeassistant/components/remote/translations/zh-Hans.json +++ b/homeassistant/components/remote/translations/zh-Hans.json @@ -10,6 +10,7 @@ "is_on": "{entity_name} \u5df2\u6253\u5f00" }, "trigger_type": { + "changed_states": "{entity_name} \u88ab\u6253\u5f00\u6216\u5173\u95ed", "turned_off": "{entity_name} \u88ab\u5173\u95ed", "turned_on": "{entity_name} \u88ab\u6253\u5f00" } diff --git a/homeassistant/components/sensor/translations/zh-Hans.json b/homeassistant/components/sensor/translations/zh-Hans.json index 4fd2ec4db9d..910aa129864 100644 --- a/homeassistant/components/sensor/translations/zh-Hans.json +++ b/homeassistant/components/sensor/translations/zh-Hans.json @@ -3,17 +3,30 @@ "condition_type": { "is_apparent_power": "{entity_name} \u5f53\u524d\u7684\u89c6\u5728\u529f\u7387", "is_battery_level": "{entity_name} \u5f53\u524d\u7684\u7535\u6c60\u7535\u91cf", + "is_carbon_dioxide": "{entity_name} \u5f53\u524d\u7684\u4e8c\u6c27\u5316\u78b3\u6d53\u5ea6\u6c34\u5e73", + "is_carbon_monoxide": "{entity_name} \u5f53\u524d\u7684\u4e00\u6c27\u5316\u78b3\u6d53\u5ea6\u6c34\u5e73", "is_current": "{entity_name} \u5f53\u524d\u7684\u7535\u6d41", "is_energy": "{entity_name} \u5f53\u524d\u7528\u7535\u91cf", + "is_frequency": "{entity_name} \u5f53\u524d\u7684\u9891\u7387", + "is_gas": "{entity_name} \u5f53\u524d\u7684\u71c3\u6c14", "is_humidity": "{entity_name} \u5f53\u524d\u7684\u6e7f\u5ea6", "is_illuminance": "{entity_name} \u5f53\u524d\u7684\u5149\u7167\u5f3a\u5ea6", + "is_nitrogen_dioxide": "{entity_name} \u5f53\u524d\u7684\u4e8c\u6c27\u5316\u6c2e\u6d53\u5ea6\u6c34\u5e73", + "is_nitrogen_monoxide": "{entity_name} \u5f53\u524d\u7684\u4e00\u6c27\u5316\u6c2e\u6d53\u5ea6\u6c34\u5e73", + "is_nitrous_oxide": "{entity_name} \u5f53\u524d\u7684\u4e00\u6c27\u5316\u4e8c\u6c2e\u6d53\u5ea6\u6c34\u5e73", + "is_ozone": "{entity_name} \u5f53\u524d\u7684\u81ed\u6c27\u6d53\u5ea6\u6c34\u5e73", + "is_pm1": "{entity_name} \u5f53\u524d\u7684 PM1 \u6d53\u5ea6\u6c34\u5e73", + "is_pm10": "{entity_name} \u5f53\u524d\u7684 PM10 \u6d53\u5ea6\u6c34\u5e73", + "is_pm25": "{entity_name} \u5f53\u524d\u7684 PM2.5 \u6d53\u5ea6\u6c34\u5e73", "is_power": "{entity_name} \u5f53\u524d\u7684\u529f\u7387", "is_power_factor": "{entity_name} \u5f53\u524d\u7684\u529f\u7387\u56e0\u6570", "is_pressure": "{entity_name} \u5f53\u524d\u7684\u538b\u529b", "is_reactive_power": "{entity_name} \u5f53\u524d\u7684\u65e0\u529f\u529f\u7387", "is_signal_strength": "{entity_name} \u5f53\u524d\u7684\u4fe1\u53f7\u5f3a\u5ea6", + "is_sulphur_dioxide": "{entity_name} \u5f53\u524d\u7684\u4e8c\u6c27\u5316\u786b\u6d53\u5ea6\u6c34\u5e73", "is_temperature": "{entity_name} \u5f53\u524d\u7684\u6e29\u5ea6", "is_value": "{entity_name} \u5f53\u524d\u7684\u503c", + "is_volatile_organic_compounds": "{entity_name} \u5f53\u524d\u7684\u6325\u53d1\u6027\u6709\u673a\u7269\u6d53\u5ea6\u6c34\u5e73", "is_voltage": "{entity_name} \u5f53\u524d\u7684\u7535\u538b" }, "trigger_type": { diff --git a/homeassistant/components/simplisafe/translations/he.json b/homeassistant/components/simplisafe/translations/he.json index dda9553f48d..85a9e002b77 100644 --- a/homeassistant/components/simplisafe/translations/he.json +++ b/homeassistant/components/simplisafe/translations/he.json @@ -15,6 +15,11 @@ "description": "\u05ea\u05d5\u05e7\u05e3 \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d4\u05d2\u05d9\u05e9\u05d4 \u05e9\u05dc\u05da \u05e4\u05d2 \u05d0\u05d5 \u05d1\u05d5\u05d8\u05dc. \u05d9\u05e9 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05e7\u05e9\u05e8 \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da.", "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" }, + "sms_2fa": { + "data": { + "code": "\u05e7\u05d5\u05d3" + } + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/tankerkoenig/translations/de.json b/homeassistant/components/tankerkoenig/translations/de.json index 3c2a5f1ec72..421521a30da 100644 --- a/homeassistant/components/tankerkoenig/translations/de.json +++ b/homeassistant/components/tankerkoenig/translations/de.json @@ -32,7 +32,8 @@ "init": { "data": { "scan_interval": "Update-Intervall", - "show_on_map": "Stationen auf der Karte anzeigen" + "show_on_map": "Stationen auf der Karte anzeigen", + "stations": "Stationen" }, "title": "Tankerkoenig Optionen" } diff --git a/homeassistant/components/tankerkoenig/translations/en.json b/homeassistant/components/tankerkoenig/translations/en.json index 83cc36fd4c8..432ad4481c8 100644 --- a/homeassistant/components/tankerkoenig/translations/en.json +++ b/homeassistant/components/tankerkoenig/translations/en.json @@ -31,6 +31,7 @@ "step": { "init": { "data": { + "scan_interval": "Update Interval", "show_on_map": "Show stations on map", "stations": "Stations" }, diff --git a/homeassistant/components/tankerkoenig/translations/pt-BR.json b/homeassistant/components/tankerkoenig/translations/pt-BR.json index b24e0b1dca8..699b98812d4 100644 --- a/homeassistant/components/tankerkoenig/translations/pt-BR.json +++ b/homeassistant/components/tankerkoenig/translations/pt-BR.json @@ -32,7 +32,8 @@ "init": { "data": { "scan_interval": "Intervalo de atualiza\u00e7\u00e3o", - "show_on_map": "Mostrar postos no mapa" + "show_on_map": "Mostrar postos no mapa", + "stations": "Esta\u00e7\u00f5es" }, "title": "Op\u00e7\u00f5es de Tankerkoenig" } diff --git a/homeassistant/components/xiaomi_aqara/translations/he.json b/homeassistant/components/xiaomi_aqara/translations/he.json index 8d123517560..304d45f4cda 100644 --- a/homeassistant/components/xiaomi_aqara/translations/he.json +++ b/homeassistant/components/xiaomi_aqara/translations/he.json @@ -18,7 +18,7 @@ "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" + "description": "\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": { "data": { diff --git a/homeassistant/components/zha/translations/zh-Hans.json b/homeassistant/components/zha/translations/zh-Hans.json index 1dd51cd7e62..1d40d80d8a2 100644 --- a/homeassistant/components/zha/translations/zh-Hans.json +++ b/homeassistant/components/zha/translations/zh-Hans.json @@ -48,6 +48,13 @@ "close": "\u5173\u95ed", "dim_down": "\u8c03\u6697", "dim_up": "\u8c03\u4eae", + "face_1": "\u5e76\u4e14\u7b2c 1 \u9762\u6fc0\u6d3b", + "face_2": "\u5e76\u4e14\u7b2c 2 \u9762\u6fc0\u6d3b", + "face_3": "\u5e76\u4e14\u7b2c 3 \u9762\u6fc0\u6d3b", + "face_4": "\u5e76\u4e14\u7b2c 4 \u9762\u6fc0\u6d3b", + "face_5": "\u5e76\u4e14\u7b2c 5 \u9762\u6fc0\u6d3b", + "face_6": "\u5e76\u4e14\u7b2c 6 \u9762\u6fc0\u6d3b", + "face_any": "\u5e76\u4e14\u4efb\u610f\u6216\u6307\u5b9a\u9762\u6fc0\u6d3b", "left": "\u5de6", "open": "\u5f00\u542f", "right": "\u53f3", @@ -56,12 +63,12 @@ }, "trigger_type": { "device_dropped": "\u8bbe\u5907\u81ea\u7531\u843d\u4f53", - "device_flipped": "\u8bbe\u5907\u7ffb\u8f6c \"{subtype}\"", - "device_knocked": "\u8bbe\u5907\u8f7b\u6572 \"{subtype}\"", + "device_flipped": "\u8bbe\u5907\u7ffb\u8f6c{subtype}", + "device_knocked": "\u8bbe\u5907\u8f7b\u6572{subtype}", "device_offline": "\u8bbe\u5907\u79bb\u7ebf", - "device_rotated": "\u8bbe\u5907\u65cb\u8f6c \"{subtype}\"", + "device_rotated": "\u8bbe\u5907\u65cb\u8f6c{subtype}", "device_shaken": "\u8bbe\u5907\u6447\u4e00\u6447", - "device_slid": "\u8bbe\u5907\u5e73\u79fb \"{subtype}\"", + "device_slid": "\u8bbe\u5907\u5e73\u79fb{subtype}", "device_tilted": "\u8bbe\u5907\u503e\u659c", "remote_button_alt_double_press": "\"{subtype}\" \u53cc\u51fb(\u5907\u7528)", "remote_button_alt_long_press": "\"{subtype}\" \u957f\u6309(\u5907\u7528)", diff --git a/homeassistant/components/zwave_js/translations/zh-Hans.json b/homeassistant/components/zwave_js/translations/zh-Hans.json new file mode 100644 index 00000000000..815453d9b62 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "ping": "Ping \u8bbe\u5907" + } + } +} \ No newline at end of file From 46031aff8d60b5f4836e3ee51b7250eea415414a Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sat, 28 May 2022 21:03:13 -0500 Subject: [PATCH 0996/3516] Avoid swallowing Roku errors (#72517) --- homeassistant/components/roku/helpers.py | 17 +++---- tests/components/roku/test_select.py | 63 +++++++++++++++--------- 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/roku/helpers.py b/homeassistant/components/roku/helpers.py index f5a68f44ab8..6b3c02a5fab 100644 --- a/homeassistant/components/roku/helpers.py +++ b/homeassistant/components/roku/helpers.py @@ -3,15 +3,14 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine from functools import wraps -import logging from typing import Any, TypeVar from rokuecp import RokuConnectionError, RokuConnectionTimeoutError, RokuError from typing_extensions import Concatenate, ParamSpec -from .entity import RokuEntity +from homeassistant.exceptions import HomeAssistantError -_LOGGER = logging.getLogger(__name__) +from .entity import RokuEntity _RokuEntityT = TypeVar("_RokuEntityT", bound=RokuEntity) _P = ParamSpec("_P") @@ -43,14 +42,14 @@ def roku_exception_handler( try: await func(self, *args, **kwargs) except RokuConnectionTimeoutError as error: - if not ignore_timeout and self.available: - _LOGGER.error("Error communicating with API: %s", error) + if not ignore_timeout: + raise HomeAssistantError( + "Timeout communicating with Roku API" + ) from error except RokuConnectionError as error: - if self.available: - _LOGGER.error("Error communicating with API: %s", error) + raise HomeAssistantError("Error communicating with Roku API") from error except RokuError as error: - if self.available: - _LOGGER.error("Invalid response from API: %s", error) + raise HomeAssistantError("Invalid response from Roku API") from error return wrapper diff --git a/tests/components/roku/test_select.py b/tests/components/roku/test_select.py index e82a13c8511..003487c0adf 100644 --- a/tests/components/roku/test_select.py +++ b/tests/components/roku/test_select.py @@ -2,7 +2,13 @@ from unittest.mock import MagicMock import pytest -from rokuecp import Application, Device as RokuDevice, RokuError +from rokuecp import ( + Application, + Device as RokuDevice, + RokuConnectionError, + RokuConnectionTimeoutError, + RokuError, +) from homeassistant.components.roku.const import DOMAIN from homeassistant.components.roku.coordinator import SCAN_INTERVAL @@ -10,6 +16,7 @@ from homeassistant.components.select import DOMAIN as SELECT_DOMAIN from homeassistant.components.select.const import ATTR_OPTION, ATTR_OPTIONS from homeassistant.const import ATTR_ENTITY_ID, ATTR_ICON, SERVICE_SELECT_OPTION from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util @@ -102,11 +109,20 @@ async def test_application_state( assert state.state == "Home" +@pytest.mark.parametrize( + "error, error_string", + [ + (RokuConnectionError, "Error communicating with Roku API"), + (RokuConnectionTimeoutError, "Timeout communicating with Roku API"), + (RokuError, "Invalid response from Roku API"), + ], +) async def test_application_select_error( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_roku: MagicMock, - caplog: pytest.LogCaptureFixture, + error: RokuError, + error_string: str, ) -> None: """Test error handling of the Roku selects.""" entity_registry = er.async_get(hass) @@ -123,22 +139,22 @@ async def test_application_select_error( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - mock_roku.launch.side_effect = RokuError + mock_roku.launch.side_effect = error - await hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - { - ATTR_ENTITY_ID: "select.my_roku_3_application", - ATTR_OPTION: "Netflix", - }, - blocking=True, - ) + with pytest.raises(HomeAssistantError, match=error_string): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.my_roku_3_application", + ATTR_OPTION: "Netflix", + }, + blocking=True, + ) state = hass.states.get("select.my_roku_3_application") assert state assert state.state == "Home" - assert "Invalid response from API" in caplog.text assert mock_roku.launch.call_count == 1 mock_roku.launch.assert_called_with("12") @@ -218,24 +234,23 @@ async def test_channel_select_error( hass: HomeAssistant, init_integration: MockConfigEntry, mock_roku: MagicMock, - caplog: pytest.LogCaptureFixture, ) -> None: """Test error handling of the Roku selects.""" mock_roku.tune.side_effect = RokuError - await hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - { - ATTR_ENTITY_ID: "select.58_onn_roku_tv_channel", - ATTR_OPTION: "99.1", - }, - blocking=True, - ) + with pytest.raises(HomeAssistantError, match="Invalid response from Roku API"): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.58_onn_roku_tv_channel", + ATTR_OPTION: "99.1", + }, + blocking=True, + ) state = hass.states.get("select.58_onn_roku_tv_channel") assert state assert state.state == "getTV (14.3)" - assert "Invalid response from API" in caplog.text assert mock_roku.tune.call_count == 1 mock_roku.tune.assert_called_with("99.1") From 7d391846ffaa3e52886068b9b4645807e46a882b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 May 2022 16:38:38 -1000 Subject: [PATCH 0997/3516] Retry right away on discovery for WiZ (#72659) --- homeassistant/components/wiz/config_flow.py | 14 +++++++-- tests/components/wiz/test_config_flow.py | 33 ++++++++++++++++++++- tests/components/wiz/test_init.py | 21 ++++++++++--- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/wiz/config_flow.py b/homeassistant/components/wiz/config_flow.py index 04a0884059f..6b5f5be027f 100644 --- a/homeassistant/components/wiz/config_flow.py +++ b/homeassistant/components/wiz/config_flow.py @@ -9,8 +9,8 @@ from pywizlight.discovery import DiscoveredBulb from pywizlight.exceptions import WizLightConnectionError, WizLightTimeOutError import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import dhcp +from homeassistant.config_entries import ConfigEntryState, ConfigFlow from homeassistant.const import CONF_HOST from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.util.network import is_ip_address @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) CONF_DEVICE = "device" -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class WizConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for WiZ.""" VERSION = 1 @@ -58,7 +58,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.debug("Discovered device: %s", device) ip_address = device.ip_address mac = device.mac_address - await self.async_set_unique_id(mac) + if current_entry := await self.async_set_unique_id(mac): + if ( + current_entry.state is ConfigEntryState.SETUP_RETRY + and current_entry.data[CONF_HOST] == ip_address + ): + self.hass.async_create_task( + self.hass.config_entries.async_reload(current_entry.entry_id) + ) + return self.async_abort(reason="already_configured") self._abort_if_unique_id_configured(updates={CONF_HOST: ip_address}) await self._async_connect_discovered_or_abort() return await self.async_step_discovery_confirm() diff --git a/tests/components/wiz/test_config_flow.py b/tests/components/wiz/test_config_flow.py index f8426ece56d..58b46bbea9d 100644 --- a/tests/components/wiz/test_config_flow.py +++ b/tests/components/wiz/test_config_flow.py @@ -1,5 +1,5 @@ """Test the WiZ Platform config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest from pywizlight.exceptions import WizLightConnectionError, WizLightTimeOutError @@ -21,8 +21,10 @@ from . import ( FAKE_SOCKET, TEST_CONNECTION, TEST_SYSTEM_INFO, + _mocked_wizlight, _patch_discovery, _patch_wizlight, + async_setup_integration, ) from tests.common import MockConfigEntry @@ -309,6 +311,35 @@ async def test_discovered_by_dhcp_or_integration_discovery_updates_host( assert entry.data[CONF_HOST] == FAKE_IP +@pytest.mark.parametrize( + "source, data", + [ + (config_entries.SOURCE_DHCP, DHCP_DISCOVERY), + (config_entries.SOURCE_INTEGRATION_DISCOVERY, INTEGRATION_DISCOVERY), + ], +) +async def test_discovered_by_dhcp_or_integration_discovery_avoid_waiting_for_retry( + hass, source, data +): + """Test dhcp or discovery kicks off setup when in retry.""" + bulb = _mocked_wizlight(None, None, FAKE_SOCKET) + bulb.getMac = AsyncMock(side_effect=OSError) + _, entry = await async_setup_integration(hass, wizlight=bulb) + assert entry.data[CONF_HOST] == FAKE_IP + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + bulb.getMac = AsyncMock(return_value=FAKE_MAC) + + with _patch_wizlight(): + 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 entry.state is config_entries.ConfigEntryState.LOADED + + async def test_setup_via_discovery(hass): """Test setting up via discovery.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/wiz/test_init.py b/tests/components/wiz/test_init.py index 30340f78e49..96ae5e20ba3 100644 --- a/tests/components/wiz/test_init.py +++ b/tests/components/wiz/test_init.py @@ -1,13 +1,16 @@ """Tests for wiz integration.""" import datetime -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from homeassistant import config_entries -from homeassistant.const import ATTR_FRIENDLY_NAME, EVENT_HOMEASSISTANT_STOP +from homeassistant.components.wiz.const import DOMAIN +from homeassistant.const import ATTR_FRIENDLY_NAME, CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from . import ( + FAKE_IP, FAKE_MAC, FAKE_SOCKET, _mocked_wizlight, @@ -16,7 +19,7 @@ from . import ( async_setup_integration, ) -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed async def test_setup_retry(hass: HomeAssistant) -> None: @@ -47,7 +50,17 @@ async def test_cleanup_on_failed_first_update(hass: HomeAssistant) -> None: """Test the socket is cleaned up on failed first update.""" bulb = _mocked_wizlight(None, None, FAKE_SOCKET) bulb.updateState = AsyncMock(side_effect=OSError) - _, entry = await async_setup_integration(hass, wizlight=bulb) + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=FAKE_MAC, + data={CONF_HOST: FAKE_IP}, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.wiz.discovery.find_wizlights", return_value=[] + ), _patch_wizlight(device=bulb): + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY bulb.async_close.assert_called_once() From 6a3d2e54a2df71ea9588f0379aeddadc3763f6d7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 May 2022 20:23:16 -0700 Subject: [PATCH 0998/3516] Handle OAuth2 rejection (#72040) --- .../helpers/config_entry_oauth2_flow.py | 30 +++++++--- homeassistant/strings.json | 1 + script/scaffold/generate.py | 1 + .../helpers/test_config_entry_oauth2_flow.py | 55 +++++++++++++++++++ 4 files changed, 79 insertions(+), 8 deletions(-) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index d369b872eb9..365ced24929 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -271,9 +271,10 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): ) -> FlowResult: """Create an entry for auth.""" # Flow has been triggered by external data - if user_input: + if user_input is not None: self.external_data = user_input - return self.async_external_step_done(next_step_id="creation") + next_step = "authorize_rejected" if "error" in user_input else "creation" + return self.async_external_step_done(next_step_id=next_step) try: async with async_timeout.timeout(10): @@ -311,6 +312,13 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): {"auth_implementation": self.flow_impl.domain, "token": token} ) + async def async_step_authorize_rejected(self, data: None = None) -> FlowResult: + """Step to handle flow rejection.""" + return self.async_abort( + reason="user_rejected_authorize", + description_placeholders={"error": self.external_data["error"]}, + ) + async def async_oauth_create_entry(self, data: dict) -> FlowResult: """Create an entry for the flow. @@ -400,10 +408,8 @@ class OAuth2AuthorizeCallbackView(http.HomeAssistantView): async def get(self, request: web.Request) -> web.Response: """Receive authorization code.""" - if "code" not in request.query or "state" not in request.query: - return web.Response( - text=f"Missing code or state parameter in {request.url}" - ) + if "state" not in request.query: + return web.Response(text="Missing state parameter") hass = request.app["hass"] @@ -412,9 +418,17 @@ class OAuth2AuthorizeCallbackView(http.HomeAssistantView): if state is None: return web.Response(text="Invalid state") + user_input: dict[str, Any] = {"state": state} + + if "code" in request.query: + user_input["code"] = request.query["code"] + elif "error" in request.query: + user_input["error"] = request.query["error"] + else: + return web.Response(text="Missing code or error parameter") + await hass.config_entries.flow.async_configure( - flow_id=state["flow_id"], - user_input={"state": state, "code": request.query["code"]}, + flow_id=state["flow_id"], user_input=user_input ) return web.Response( diff --git a/homeassistant/strings.json b/homeassistant/strings.json index e4d363c22be..9ae30becaee 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -74,6 +74,7 @@ "oauth2_missing_credentials": "The integration requires application credentials.", "oauth2_authorize_url_timeout": "Timeout generating authorize URL.", "oauth2_no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", + "oauth2_user_rejected_authorize": "Account linking rejected: {error}", "reauth_successful": "Re-authentication was successful", "unknown_authorize_url_generation": "Unknown error generating an authorize URL.", "cloud_not_connected": "Not connected to Home Assistant Cloud." diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index 7f418868463..b7e4c58d1a1 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -188,6 +188,7 @@ def _custom_tasks(template, info: Info) -> None: "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 248f3b8dbb0..e5d220c55df 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -223,6 +223,61 @@ async def test_abort_if_oauth_error( assert result["reason"] == "oauth_error" +async def test_abort_if_oauth_rejected( + hass, + flow_handler, + local_impl, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, +): + """Check bad oauth token.""" + flow_handler.async_register_implementation(hass, local_impl) + config_entry_oauth2_flow.async_register_implementation( + hass, TEST_DOMAIN, MockOAuth2Implementation() + ) + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "pick_implementation" + + # Pick implementation + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"implementation": TEST_DOMAIN} + ) + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["url"] == ( + f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=read+write" + ) + + client = await hass_client_no_auth() + resp = await client.get( + f"/auth/external/callback?error=access_denied&state={state}" + ) + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "user_rejected_authorize" + assert result["description_placeholders"] == {"error": "access_denied"} + + async def test_step_discovery(hass, flow_handler, local_impl): """Check flow triggers from discovery.""" flow_handler.async_register_implementation(hass, local_impl) From e7e48cd9f68f8523fe1630ff48078a820b5fbbf6 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 28 May 2022 20:28:22 -0700 Subject: [PATCH 0999/3516] Defer google calendar integration reload to a task to avoid races of reload during setup (#72608) --- homeassistant/components/google/__init__.py | 29 ++++------ homeassistant/components/google/api.py | 12 +++- tests/components/google/test_config_flow.py | 31 +++++----- tests/components/google/test_init.py | 64 +++++++++++++++++++++ 4 files changed, 101 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 1336e9991e3..2a40bfe7043 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -217,7 +217,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Google from a config entry.""" hass.data.setdefault(DOMAIN, {}) - async_upgrade_entry(hass, entry) implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( hass, entry @@ -240,10 +239,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except aiohttp.ClientError as err: raise ConfigEntryNotReady from err - access = FeatureAccess[entry.options[CONF_CALENDAR_ACCESS]] - token_scopes = session.token.get("scope", []) - if access.scope not in token_scopes: - _LOGGER.debug("Scope '%s' not in scopes '%s'", access.scope, token_scopes) + if not async_entry_has_scopes(hass, entry): raise ConfigEntryAuthFailed( "Required scopes are not available, reauth required" ) @@ -254,27 +250,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_setup_services(hass, calendar_service) # Only expose the add event service if we have the correct permissions - if access is FeatureAccess.read_write: + if get_feature_access(hass, entry) is FeatureAccess.read_write: await async_setup_add_event_service(hass, calendar_service) hass.config_entries.async_setup_platforms(entry, PLATFORMS) - # Reload entry when options are updated entry.async_on_unload(entry.add_update_listener(async_reload_entry)) return True -def async_upgrade_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Upgrade the config entry if needed.""" - if entry.options: - return - hass.config_entries.async_update_entry( - entry, - options={ - CONF_CALENDAR_ACCESS: get_feature_access(hass).name, - }, - ) +def async_entry_has_scopes(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Verify that the config entry desired scope is present in the oauth token.""" + access = get_feature_access(hass, entry) + token_scopes = entry.data.get("token", {}).get("scope", []) + return access.scope in token_scopes async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -283,8 +273,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Reload the config entry when it changed.""" - await hass.config_entries.async_reload(entry.entry_id) + """Reload config entry if the access options change.""" + if not async_entry_has_scopes(hass, entry): + await hass.config_entries.async_reload(entry.entry_id) async def async_setup_services( diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index eeac854a2ae..4bb9de5d581 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -19,6 +19,7 @@ from oauth2client.client import ( ) from homeassistant.components.application_credentials import AuthImplementation +from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.event import async_track_time_interval @@ -127,8 +128,17 @@ class DeviceFlow: ) -def get_feature_access(hass: HomeAssistant) -> FeatureAccess: +def get_feature_access( + hass: HomeAssistant, config_entry: ConfigEntry | None = None +) -> FeatureAccess: """Return the desired calendar feature access.""" + if ( + config_entry + and config_entry.options + and CONF_CALENDAR_ACCESS in config_entry.options + ): + return FeatureAccess[config_entry.options[CONF_CALENDAR_ACCESS]] + # This may be called during config entry setup without integration setup running when there # is no google entry in configuration.yaml return cast( diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index 77ebe1e56cd..8ac017fcba4 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -540,9 +540,15 @@ async def test_options_flow_triggers_reauth( ) -> None: """Test load and unload of a ConfigEntry.""" config_entry.add_to_hass(hass) - await component_setup() + + with patch( + "homeassistant.components.google.async_setup_entry", return_value=True + ) as mock_setup: + await component_setup() + mock_setup.assert_called_once() + assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.options == {"calendar_access": "read_write"} + assert config_entry.options == {} # Default is read_write result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == "form" @@ -557,14 +563,7 @@ async def test_options_flow_triggers_reauth( }, ) assert result["type"] == "create_entry" - - await hass.async_block_till_done() assert config_entry.options == {"calendar_access": "read_only"} - # Re-auth flow was initiated because access level changed - assert config_entry.state is ConfigEntryState.SETUP_ERROR - flows = hass.config_entries.flow.async_progress() - assert len(flows) == 1 - assert flows[0]["step_id"] == "reauth_confirm" async def test_options_flow_no_changes( @@ -574,9 +573,15 @@ async def test_options_flow_no_changes( ) -> None: """Test load and unload of a ConfigEntry.""" config_entry.add_to_hass(hass) - await component_setup() + + with patch( + "homeassistant.components.google.async_setup_entry", return_value=True + ) as mock_setup: + await component_setup() + mock_setup.assert_called_once() + assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.options == {"calendar_access": "read_write"} + assert config_entry.options == {} # Default is read_write result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == "form" @@ -589,8 +594,4 @@ async def test_options_flow_no_changes( }, ) assert result["type"] == "create_entry" - - await hass.async_block_till_done() assert config_entry.options == {"calendar_access": "read_write"} - # Re-auth flow was initiated because access level changed - assert config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index b6f7a6b4cbc..f2cf067f7bb 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -102,6 +102,24 @@ async def test_existing_token_missing_scope( assert flows[0]["step_id"] == "reauth_confirm" +@pytest.mark.parametrize("config_entry_options", [{CONF_CALENDAR_ACCESS: "read_only"}]) +async def test_config_entry_scope_reauth( + hass: HomeAssistant, + token_scopes: list[str], + component_setup: ComponentSetup, + config_entry: MockConfigEntry, +) -> None: + """Test setup where the config entry options requires reauth to match the scope.""" + config_entry.add_to_hass(hass) + assert await component_setup() + + assert config_entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" + + @pytest.mark.parametrize("calendars_config", [[{"cal_id": "invalid-schema"}]]) async def test_calendar_yaml_missing_required_fields( hass: HomeAssistant, @@ -629,3 +647,49 @@ async def test_calendar_yaml_update( # No yaml config loaded that overwrites the entity name assert not hass.states.get(TEST_YAML_ENTITY) + + +async def test_update_will_reload( + hass: HomeAssistant, + component_setup: ComponentSetup, + setup_config_entry: Any, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + mock_events_list: ApiResult, + config_entry: MockConfigEntry, +) -> None: + """Test updating config entry options will trigger a reload.""" + mock_calendars_list({"items": [test_api_calendar]}) + mock_events_list({}) + await component_setup() + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.options == {} # read_write is default + + with patch( + "homeassistant.config_entries.ConfigEntries.async_reload", + return_value=None, + ) as mock_reload: + # No-op does not reload + hass.config_entries.async_update_entry( + config_entry, options={CONF_CALENDAR_ACCESS: "read_write"} + ) + await hass.async_block_till_done() + mock_reload.assert_not_called() + + # Data change does not trigger reload + hass.config_entries.async_update_entry( + config_entry, + data={ + **config_entry.data, + "example": "field", + }, + ) + await hass.async_block_till_done() + mock_reload.assert_not_called() + + # Reload when options changed + hass.config_entries.async_update_entry( + config_entry, options={CONF_CALENDAR_ACCESS: "read_only"} + ) + await hass.async_block_till_done() + mock_reload.assert_called_once() From afcc8679dd3b9782fd478abb6fe94cbf2ec892ff Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 May 2022 20:23:16 -0700 Subject: [PATCH 1000/3516] Handle OAuth2 rejection (#72040) --- .../helpers/config_entry_oauth2_flow.py | 30 +++++++--- homeassistant/strings.json | 1 + script/scaffold/generate.py | 1 + .../helpers/test_config_entry_oauth2_flow.py | 55 +++++++++++++++++++ 4 files changed, 79 insertions(+), 8 deletions(-) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index d369b872eb9..365ced24929 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -271,9 +271,10 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): ) -> FlowResult: """Create an entry for auth.""" # Flow has been triggered by external data - if user_input: + if user_input is not None: self.external_data = user_input - return self.async_external_step_done(next_step_id="creation") + next_step = "authorize_rejected" if "error" in user_input else "creation" + return self.async_external_step_done(next_step_id=next_step) try: async with async_timeout.timeout(10): @@ -311,6 +312,13 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): {"auth_implementation": self.flow_impl.domain, "token": token} ) + async def async_step_authorize_rejected(self, data: None = None) -> FlowResult: + """Step to handle flow rejection.""" + return self.async_abort( + reason="user_rejected_authorize", + description_placeholders={"error": self.external_data["error"]}, + ) + async def async_oauth_create_entry(self, data: dict) -> FlowResult: """Create an entry for the flow. @@ -400,10 +408,8 @@ class OAuth2AuthorizeCallbackView(http.HomeAssistantView): async def get(self, request: web.Request) -> web.Response: """Receive authorization code.""" - if "code" not in request.query or "state" not in request.query: - return web.Response( - text=f"Missing code or state parameter in {request.url}" - ) + if "state" not in request.query: + return web.Response(text="Missing state parameter") hass = request.app["hass"] @@ -412,9 +418,17 @@ class OAuth2AuthorizeCallbackView(http.HomeAssistantView): if state is None: return web.Response(text="Invalid state") + user_input: dict[str, Any] = {"state": state} + + if "code" in request.query: + user_input["code"] = request.query["code"] + elif "error" in request.query: + user_input["error"] = request.query["error"] + else: + return web.Response(text="Missing code or error parameter") + await hass.config_entries.flow.async_configure( - flow_id=state["flow_id"], - user_input={"state": state, "code": request.query["code"]}, + flow_id=state["flow_id"], user_input=user_input ) return web.Response( diff --git a/homeassistant/strings.json b/homeassistant/strings.json index e4d363c22be..9ae30becaee 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -74,6 +74,7 @@ "oauth2_missing_credentials": "The integration requires application credentials.", "oauth2_authorize_url_timeout": "Timeout generating authorize URL.", "oauth2_no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", + "oauth2_user_rejected_authorize": "Account linking rejected: {error}", "reauth_successful": "Re-authentication was successful", "unknown_authorize_url_generation": "Unknown error generating an authorize URL.", "cloud_not_connected": "Not connected to Home Assistant Cloud." diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index 7f418868463..b7e4c58d1a1 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -188,6 +188,7 @@ def _custom_tasks(template, info: Info) -> None: "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]", }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 248f3b8dbb0..e5d220c55df 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -223,6 +223,61 @@ async def test_abort_if_oauth_error( assert result["reason"] == "oauth_error" +async def test_abort_if_oauth_rejected( + hass, + flow_handler, + local_impl, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, +): + """Check bad oauth token.""" + flow_handler.async_register_implementation(hass, local_impl) + config_entry_oauth2_flow.async_register_implementation( + hass, TEST_DOMAIN, MockOAuth2Implementation() + ) + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "pick_implementation" + + # Pick implementation + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"implementation": TEST_DOMAIN} + ) + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["url"] == ( + f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=read+write" + ) + + client = await hass_client_no_auth() + resp = await client.get( + f"/auth/external/callback?error=access_denied&state={state}" + ) + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "user_rejected_authorize" + assert result["description_placeholders"] == {"error": "access_denied"} + + async def test_step_discovery(hass, flow_handler, local_impl): """Check flow triggers from discovery.""" flow_handler.async_register_implementation(hass, local_impl) From 79340f85d28befe32a6d91695f533a45e5cb7258 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 28 May 2022 12:51:40 -0700 Subject: [PATCH 1001/3516] Don't import google calendar user pref for disabling new entities (#72652) --- homeassistant/components/google/__init__.py | 29 ++++---- tests/components/google/test_init.py | 82 ++++++++------------- 2 files changed, 42 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index b7263d2e469..1336e9991e3 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -199,11 +199,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.warning( "Configuration of Google Calendar in YAML in configuration.yaml is " "is deprecated and will be removed in a future release; Your existing " - "OAuth Application Credentials and other settings have been imported " + "OAuth Application Credentials and access settings have been imported " "into the UI automatically and can be safely removed from your " "configuration.yaml file" ) - + if conf.get(CONF_TRACK_NEW) is False: + # The track_new as False would previously result in new entries + # in google_calendars.yaml with track set to Fasle which is + # handled at calendar entity creation time. + _LOGGER.warning( + "You must manually set the integration System Options in the " + "UI to disable newly discovered entities going forward" + ) return True @@ -260,23 +267,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def async_upgrade_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Upgrade the config entry if needed.""" - if DATA_CONFIG not in hass.data[DOMAIN] and entry.options: + if entry.options: return - - options = ( - entry.options - if entry.options - else { - CONF_CALENDAR_ACCESS: get_feature_access(hass).name, - } - ) - disable_new_entities = ( - not hass.data[DOMAIN].get(DATA_CONFIG, {}).get(CONF_TRACK_NEW, True) - ) hass.config_entries.async_update_entry( entry, - options=options, - pref_disable_new_entities=disable_new_entities, + options={ + CONF_CALENDAR_ACCESS: get_feature_access(hass).name, + }, ) diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 93c0642514e..b6f7a6b4cbc 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -154,57 +154,6 @@ async def test_calendar_yaml_error( assert hass.states.get(TEST_API_ENTITY) -@pytest.mark.parametrize( - "google_config_track_new,calendars_config,expected_state", - [ - ( - None, - [], - State( - TEST_API_ENTITY, - STATE_OFF, - attributes={ - "offset_reached": False, - "friendly_name": TEST_API_ENTITY_NAME, - }, - ), - ), - ( - True, - [], - State( - TEST_API_ENTITY, - STATE_OFF, - attributes={ - "offset_reached": False, - "friendly_name": TEST_API_ENTITY_NAME, - }, - ), - ), - (False, [], None), - ], - ids=["default", "True", "False"], -) -async def test_track_new( - hass: HomeAssistant, - component_setup: ComponentSetup, - mock_calendars_list: ApiResult, - test_api_calendar: dict[str, Any], - mock_events_list: ApiResult, - mock_calendars_yaml: None, - expected_state: State, - setup_config_entry: MockConfigEntry, -) -> None: - """Test behavior of configuration.yaml settings for tracking new calendars not in the config.""" - - mock_calendars_list({"items": [test_api_calendar]}) - mock_events_list({}) - assert await component_setup() - - state = hass.states.get(TEST_API_ENTITY) - assert_state(state, expected_state) - - @pytest.mark.parametrize("calendars_config", [[]]) async def test_found_calendar_from_api( hass: HomeAssistant, @@ -263,7 +212,7 @@ async def test_load_application_credentials( @pytest.mark.parametrize( - "calendars_config_track,expected_state", + "calendars_config_track,expected_state,google_config_track_new", [ ( True, @@ -275,8 +224,35 @@ async def test_load_application_credentials( "friendly_name": TEST_YAML_ENTITY_NAME, }, ), + None, ), - (False, None), + ( + True, + State( + TEST_YAML_ENTITY, + STATE_OFF, + attributes={ + "offset_reached": False, + "friendly_name": TEST_YAML_ENTITY_NAME, + }, + ), + True, + ), + ( + True, + State( + TEST_YAML_ENTITY, + STATE_OFF, + attributes={ + "offset_reached": False, + "friendly_name": TEST_YAML_ENTITY_NAME, + }, + ), + False, # Has no effect + ), + (False, None, None), + (False, None, True), + (False, None, False), ], ) async def test_calendar_config_track_new( From 301f7647d1da2316dadc0aa0ac657c096313bc24 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 28 May 2022 20:28:22 -0700 Subject: [PATCH 1002/3516] Defer google calendar integration reload to a task to avoid races of reload during setup (#72608) --- homeassistant/components/google/__init__.py | 29 ++++------ homeassistant/components/google/api.py | 12 +++- tests/components/google/test_config_flow.py | 31 +++++----- tests/components/google/test_init.py | 64 +++++++++++++++++++++ 4 files changed, 101 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 1336e9991e3..2a40bfe7043 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -217,7 +217,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Google from a config entry.""" hass.data.setdefault(DOMAIN, {}) - async_upgrade_entry(hass, entry) implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( hass, entry @@ -240,10 +239,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except aiohttp.ClientError as err: raise ConfigEntryNotReady from err - access = FeatureAccess[entry.options[CONF_CALENDAR_ACCESS]] - token_scopes = session.token.get("scope", []) - if access.scope not in token_scopes: - _LOGGER.debug("Scope '%s' not in scopes '%s'", access.scope, token_scopes) + if not async_entry_has_scopes(hass, entry): raise ConfigEntryAuthFailed( "Required scopes are not available, reauth required" ) @@ -254,27 +250,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_setup_services(hass, calendar_service) # Only expose the add event service if we have the correct permissions - if access is FeatureAccess.read_write: + if get_feature_access(hass, entry) is FeatureAccess.read_write: await async_setup_add_event_service(hass, calendar_service) hass.config_entries.async_setup_platforms(entry, PLATFORMS) - # Reload entry when options are updated entry.async_on_unload(entry.add_update_listener(async_reload_entry)) return True -def async_upgrade_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Upgrade the config entry if needed.""" - if entry.options: - return - hass.config_entries.async_update_entry( - entry, - options={ - CONF_CALENDAR_ACCESS: get_feature_access(hass).name, - }, - ) +def async_entry_has_scopes(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Verify that the config entry desired scope is present in the oauth token.""" + access = get_feature_access(hass, entry) + token_scopes = entry.data.get("token", {}).get("scope", []) + return access.scope in token_scopes async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -283,8 +273,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Reload the config entry when it changed.""" - await hass.config_entries.async_reload(entry.entry_id) + """Reload config entry if the access options change.""" + if not async_entry_has_scopes(hass, entry): + await hass.config_entries.async_reload(entry.entry_id) async def async_setup_services( diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index eeac854a2ae..4bb9de5d581 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -19,6 +19,7 @@ from oauth2client.client import ( ) from homeassistant.components.application_credentials import AuthImplementation +from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.event import async_track_time_interval @@ -127,8 +128,17 @@ class DeviceFlow: ) -def get_feature_access(hass: HomeAssistant) -> FeatureAccess: +def get_feature_access( + hass: HomeAssistant, config_entry: ConfigEntry | None = None +) -> FeatureAccess: """Return the desired calendar feature access.""" + if ( + config_entry + and config_entry.options + and CONF_CALENDAR_ACCESS in config_entry.options + ): + return FeatureAccess[config_entry.options[CONF_CALENDAR_ACCESS]] + # This may be called during config entry setup without integration setup running when there # is no google entry in configuration.yaml return cast( diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index 77ebe1e56cd..8ac017fcba4 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -540,9 +540,15 @@ async def test_options_flow_triggers_reauth( ) -> None: """Test load and unload of a ConfigEntry.""" config_entry.add_to_hass(hass) - await component_setup() + + with patch( + "homeassistant.components.google.async_setup_entry", return_value=True + ) as mock_setup: + await component_setup() + mock_setup.assert_called_once() + assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.options == {"calendar_access": "read_write"} + assert config_entry.options == {} # Default is read_write result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == "form" @@ -557,14 +563,7 @@ async def test_options_flow_triggers_reauth( }, ) assert result["type"] == "create_entry" - - await hass.async_block_till_done() assert config_entry.options == {"calendar_access": "read_only"} - # Re-auth flow was initiated because access level changed - assert config_entry.state is ConfigEntryState.SETUP_ERROR - flows = hass.config_entries.flow.async_progress() - assert len(flows) == 1 - assert flows[0]["step_id"] == "reauth_confirm" async def test_options_flow_no_changes( @@ -574,9 +573,15 @@ async def test_options_flow_no_changes( ) -> None: """Test load and unload of a ConfigEntry.""" config_entry.add_to_hass(hass) - await component_setup() + + with patch( + "homeassistant.components.google.async_setup_entry", return_value=True + ) as mock_setup: + await component_setup() + mock_setup.assert_called_once() + assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.options == {"calendar_access": "read_write"} + assert config_entry.options == {} # Default is read_write result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == "form" @@ -589,8 +594,4 @@ async def test_options_flow_no_changes( }, ) assert result["type"] == "create_entry" - - await hass.async_block_till_done() assert config_entry.options == {"calendar_access": "read_write"} - # Re-auth flow was initiated because access level changed - assert config_entry.state is ConfigEntryState.LOADED diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index b6f7a6b4cbc..f2cf067f7bb 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -102,6 +102,24 @@ async def test_existing_token_missing_scope( assert flows[0]["step_id"] == "reauth_confirm" +@pytest.mark.parametrize("config_entry_options", [{CONF_CALENDAR_ACCESS: "read_only"}]) +async def test_config_entry_scope_reauth( + hass: HomeAssistant, + token_scopes: list[str], + component_setup: ComponentSetup, + config_entry: MockConfigEntry, +) -> None: + """Test setup where the config entry options requires reauth to match the scope.""" + config_entry.add_to_hass(hass) + assert await component_setup() + + assert config_entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" + + @pytest.mark.parametrize("calendars_config", [[{"cal_id": "invalid-schema"}]]) async def test_calendar_yaml_missing_required_fields( hass: HomeAssistant, @@ -629,3 +647,49 @@ async def test_calendar_yaml_update( # No yaml config loaded that overwrites the entity name assert not hass.states.get(TEST_YAML_ENTITY) + + +async def test_update_will_reload( + hass: HomeAssistant, + component_setup: ComponentSetup, + setup_config_entry: Any, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + mock_events_list: ApiResult, + config_entry: MockConfigEntry, +) -> None: + """Test updating config entry options will trigger a reload.""" + mock_calendars_list({"items": [test_api_calendar]}) + mock_events_list({}) + await component_setup() + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.options == {} # read_write is default + + with patch( + "homeassistant.config_entries.ConfigEntries.async_reload", + return_value=None, + ) as mock_reload: + # No-op does not reload + hass.config_entries.async_update_entry( + config_entry, options={CONF_CALENDAR_ACCESS: "read_write"} + ) + await hass.async_block_till_done() + mock_reload.assert_not_called() + + # Data change does not trigger reload + hass.config_entries.async_update_entry( + config_entry, + data={ + **config_entry.data, + "example": "field", + }, + ) + await hass.async_block_till_done() + mock_reload.assert_not_called() + + # Reload when options changed + hass.config_entries.async_update_entry( + config_entry, options={CONF_CALENDAR_ACCESS: "read_only"} + ) + await hass.async_block_till_done() + mock_reload.assert_called_once() From c45dc492705f92241f63b4fab941eb34cc4500b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 May 2022 11:38:29 -1000 Subject: [PATCH 1003/3516] Escape % and _ in history/logbook entity_globs, and use ? as _ (#72623) Co-authored-by: pyos --- homeassistant/components/recorder/filters.py | 11 +- tests/components/history/test_init.py | 12 +- .../components/logbook/test_websocket_api.py | 207 ++++++++++++++++++ 3 files changed, 223 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 0a383d8ef2b..5dd1e4b7884 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -18,8 +18,11 @@ DOMAIN = "history" HISTORY_FILTERS = "history_filters" GLOB_TO_SQL_CHARS = { - 42: "%", # * - 46: "_", # . + ord("*"): "%", + ord("?"): "_", + ord("%"): "\\%", + ord("_"): "\\_", + ord("\\"): "\\\\", } @@ -122,7 +125,9 @@ def _globs_to_like( ) -> ClauseList: """Translate glob to sql.""" return or_( - cast(column, Text()).like(encoder(glob_str.translate(GLOB_TO_SQL_CHARS))) + cast(column, Text()).like( + encoder(glob_str).translate(GLOB_TO_SQL_CHARS), escape="\\" + ) for glob_str in glob_strs for column in columns ) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index a2626ab2004..cbc5e86c37e 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -719,7 +719,7 @@ async def test_fetch_period_api_with_entity_glob_exclude( { "history": { "exclude": { - "entity_globs": ["light.k*"], + "entity_globs": ["light.k*", "binary_sensor.*_?"], "domains": "switch", "entities": "media_player.test", }, @@ -731,6 +731,9 @@ async def test_fetch_period_api_with_entity_glob_exclude( hass.states.async_set("light.match", "on") hass.states.async_set("switch.match", "on") hass.states.async_set("media_player.test", "on") + hass.states.async_set("binary_sensor.sensor_l", "on") + hass.states.async_set("binary_sensor.sensor_r", "on") + hass.states.async_set("binary_sensor.sensor", "on") await async_wait_recording_done(hass) @@ -740,9 +743,10 @@ async def test_fetch_period_api_with_entity_glob_exclude( ) assert response.status == HTTPStatus.OK response_json = await response.json() - assert len(response_json) == 2 - assert response_json[0][0]["entity_id"] == "light.cow" - assert response_json[1][0]["entity_id"] == "light.match" + assert len(response_json) == 3 + assert response_json[0][0]["entity_id"] == "binary_sensor.sensor" + assert response_json[1][0]["entity_id"] == "light.cow" + assert response_json[2][0]["entity_id"] == "light.match" async def test_fetch_period_api_with_entity_glob_include_and_exclude( diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 02fea4f980f..9d7146ec96c 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -22,6 +22,7 @@ from homeassistant.const import ( CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, + CONF_INCLUDE, EVENT_HOMEASSISTANT_START, STATE_OFF, STATE_ON, @@ -642,6 +643,212 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities( assert sum(hass.bus.async_listeners().values()) == init_count +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_unsubscribe_logbook_stream_included_entities( + hass, recorder_mock, hass_ws_client +): + """Test subscribe/unsubscribe logbook stream with included entities.""" + test_entities = ( + "light.inc", + "switch.any", + "cover.included", + "cover.not_included", + "automation.not_included", + "binary_sensor.is_light", + ) + + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "automation", "script") + ] + ) + await async_setup_component( + hass, + logbook.DOMAIN, + { + logbook.DOMAIN: { + CONF_INCLUDE: { + CONF_ENTITIES: ["light.inc"], + CONF_DOMAINS: ["switch"], + CONF_ENTITY_GLOBS: "*.included", + } + }, + }, + ) + await hass.async_block_till_done() + init_count = sum(hass.bus.async_listeners().values()) + + for entity_id in test_entities: + hass.states.async_set(entity_id, STATE_ON) + hass.states.async_set(entity_id, STATE_OFF) + + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + {"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()} + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + {"entity_id": "light.inc", "state": "off", "when": ANY}, + {"entity_id": "switch.any", "state": "off", "when": ANY}, + {"entity_id": "cover.included", "state": "off", "when": ANY}, + ] + assert msg["event"]["start_time"] == now.timestamp() + assert msg["event"]["end_time"] > msg["event"]["start_time"] + assert msg["event"]["partial"] is True + + for entity_id in test_entities: + hass.states.async_set(entity_id, STATE_ON) + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + + hass.states.async_remove("light.zulu") + await hass.async_block_till_done() + + hass.states.async_set("light.zulu", "on", {"effect": "help", "color": "blue"}) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [ + {"entity_id": "light.inc", "state": "on", "when": ANY}, + {"entity_id": "light.inc", "state": "off", "when": ANY}, + {"entity_id": "switch.any", "state": "on", "when": ANY}, + {"entity_id": "switch.any", "state": "off", "when": ANY}, + {"entity_id": "cover.included", "state": "on", "when": ANY}, + {"entity_id": "cover.included", "state": "off", "when": ANY}, + ] + + for _ in range(3): + for entity_id in test_entities: + hass.states.async_set(entity_id, STATE_ON) + hass.states.async_set(entity_id, STATE_OFF) + await async_wait_recording_done(hass) + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + {"entity_id": "light.inc", "state": "on", "when": ANY}, + {"entity_id": "light.inc", "state": "off", "when": ANY}, + {"entity_id": "switch.any", "state": "on", "when": ANY}, + {"entity_id": "switch.any", "state": "off", "when": ANY}, + {"entity_id": "cover.included", "state": "on", "when": ANY}, + {"entity_id": "cover.included", "state": "off", "when": ANY}, + ] + + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation 3", ATTR_ENTITY_ID: "cover.included"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation 3", ATTR_ENTITY_ID: "cover.excluded"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + { + ATTR_NAME: "Mock automation switch matching entity", + ATTR_ENTITY_ID: "switch.match_domain", + }, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation switch matching domain", ATTR_DOMAIN: "switch"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation matches nothing"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation 3", ATTR_ENTITY_ID: "light.inc"}, + ) + + await hass.async_block_till_done() + + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + { + "context_id": ANY, + "domain": "automation", + "entity_id": "cover.included", + "message": "triggered", + "name": "Mock automation 3", + "source": None, + "when": ANY, + }, + { + "context_id": ANY, + "domain": "automation", + "entity_id": "switch.match_domain", + "message": "triggered", + "name": "Mock automation switch matching entity", + "source": None, + "when": ANY, + }, + { + "context_id": ANY, + "domain": "automation", + "entity_id": None, + "message": "triggered", + "name": "Mock automation switch matching domain", + "source": None, + "when": ANY, + }, + { + "context_id": ANY, + "domain": "automation", + "entity_id": None, + "message": "triggered", + "name": "Mock automation matches nothing", + "source": None, + "when": ANY, + }, + { + "context_id": ANY, + "domain": "automation", + "entity_id": "light.inc", + "message": "triggered", + "name": "Mock automation 3", + "source": None, + "when": ANY, + }, + ] + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) async def test_subscribe_unsubscribe_logbook_stream( hass, recorder_mock, hass_ws_client From 3a06b5f3202d47bdc5bf2edb6bcf5a075726d4c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 27 May 2022 23:37:19 +0200 Subject: [PATCH 1004/3516] Bump awesomeversion from 22.5.1 to 22.5.2 (#72624) --- homeassistant/package_constraints.txt | 2 +- requirements.txt | 2 +- setup.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f235cc3f02c..a43b4f99f63 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ async-upnp-client==0.30.1 async_timeout==4.0.2 atomicwrites==1.4.0 attrs==21.2.0 -awesomeversion==22.5.1 +awesomeversion==22.5.2 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements.txt b/requirements.txt index 8321e70f8de..fe2bf87ad25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ astral==2.2 async_timeout==4.0.2 attrs==21.2.0 atomicwrites==1.4.0 -awesomeversion==22.5.1 +awesomeversion==22.5.2 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/setup.cfg b/setup.cfg index 3d26396deed..b7841c53361 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,7 +10,7 @@ install_requires = async_timeout==4.0.2 attrs==21.2.0 atomicwrites==1.4.0 - awesomeversion==22.5.1 + awesomeversion==22.5.2 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 From bd222a1fe00430508816b6a3e4304c67b8ac142f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 May 2022 22:49:55 -1000 Subject: [PATCH 1005/3516] Prevent config entries from being reloaded concurrently (#72636) * Prevent config entries being reloaded concurrently - Fixes Config entry has already been setup when two places try to reload the config entry at the same time. - This comes up quite a bit: https://github.com/home-assistant/core/issues?q=is%3Aissue+sort%3Aupdated-desc+%22Config+entry+has+already+been+setup%22+is%3Aclosed * Make sure plex creates mocks in the event loop * drop reload_lock, already inherits --- homeassistant/config_entries.py | 13 ++++++---- tests/components/plex/conftest.py | 2 +- tests/test_config_entries.py | 40 ++++++++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 7dfbb131c1b..0ac02adb8d0 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -186,6 +186,7 @@ class ConfigEntry: "reason", "_async_cancel_retry_setup", "_on_unload", + "reload_lock", ) def __init__( @@ -275,6 +276,9 @@ class ConfigEntry: # Hold list for functions to call on unload. self._on_unload: list[CALLBACK_TYPE] | None = None + # Reload lock to prevent conflicting reloads + self.reload_lock = asyncio.Lock() + async def async_setup( self, hass: HomeAssistant, @@ -1005,12 +1009,13 @@ class ConfigEntries: if (entry := self.async_get_entry(entry_id)) is None: raise UnknownEntry - unload_result = await self.async_unload(entry_id) + async with entry.reload_lock: + unload_result = await self.async_unload(entry_id) - if not unload_result or entry.disabled_by: - return unload_result + if not unload_result or entry.disabled_by: + return unload_result - return await self.async_setup(entry_id) + return await self.async_setup(entry_id) async def async_set_disabled_by( self, entry_id: str, disabled_by: ConfigEntryDisabler | None diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index 47e7d96d2fe..506aadcce61 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -381,7 +381,7 @@ def hubs_music_library_fixture(): @pytest.fixture(name="entry") -def mock_config_entry(): +async def mock_config_entry(): """Return the default mocked config entry.""" return MockConfigEntry( domain=DOMAIN, diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 3611c204ba7..2602887d1d5 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1497,7 +1497,7 @@ async def test_reload_entry_entity_registry_works(hass): ) await hass.async_block_till_done() - assert len(mock_unload_entry.mock_calls) == 1 + assert len(mock_unload_entry.mock_calls) == 2 async def test_unique_id_persisted(hass, manager): @@ -3080,3 +3080,41 @@ async def test_deprecated_disabled_by_str_set(hass, manager, caplog): ) assert entry.disabled_by is config_entries.ConfigEntryDisabler.USER assert " str for config entry disabled_by. This is deprecated " in caplog.text + + +async def test_entry_reload_concurrency(hass, manager): + """Test multiple reload calls do not cause a reload race.""" + entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED) + entry.add_to_hass(hass) + + async_setup = AsyncMock(return_value=True) + loaded = 1 + + async def _async_setup_entry(*args, **kwargs): + await asyncio.sleep(0) + nonlocal loaded + loaded += 1 + return loaded == 1 + + async def _async_unload_entry(*args, **kwargs): + await asyncio.sleep(0) + nonlocal loaded + loaded -= 1 + return loaded == 0 + + mock_integration( + hass, + MockModule( + "comp", + async_setup=async_setup, + async_setup_entry=_async_setup_entry, + async_unload_entry=_async_unload_entry, + ), + ) + mock_entity_platform(hass, "config_flow.comp", None) + tasks = [] + for _ in range(15): + tasks.append(asyncio.create_task(manager.async_reload(entry.entry_id))) + await asyncio.gather(*tasks) + assert entry.state is config_entries.ConfigEntryState.LOADED + assert loaded == 1 From 50eaf2f47599cae0b5c8b7e26bd4c3494d841f1c Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Sat, 28 May 2022 19:55:50 +0200 Subject: [PATCH 1006/3516] Bump bimmer_connected to 0.9.2 (#72653) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index d41a87ef2c1..c7130d12698 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.9.0"], + "requirements": ["bimmer_connected==0.9.2"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 6d7ca5cb6b2..f0cc4f6fb67 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -394,7 +394,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.0 +bimmer_connected==0.9.2 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 78caa172df7..783542a1c69 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -309,7 +309,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.0 +bimmer_connected==0.9.2 # homeassistant.components.blebox blebox_uniapi==1.3.3 From b360f0280b8b9c6f7057b11fbda457731472c7c4 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 28 May 2022 22:31:03 +0200 Subject: [PATCH 1007/3516] Manage stations via integrations configuration in Tankerkoenig (#72654) --- .../components/tankerkoenig/config_flow.py | 64 +++++++++++++------ .../components/tankerkoenig/strings.json | 2 +- .../tankerkoenig/translations/en.json | 4 +- .../tankerkoenig/test_config_flow.py | 23 +++++-- 4 files changed, 68 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/tankerkoenig/config_flow.py b/homeassistant/components/tankerkoenig/config_flow.py index 65c367d1ba4..af3b5273b16 100644 --- a/homeassistant/components/tankerkoenig/config_flow.py +++ b/homeassistant/components/tankerkoenig/config_flow.py @@ -17,7 +17,7 @@ from homeassistant.const import ( CONF_SHOW_ON_MAP, LENGTH_KILOMETERS, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.selector import ( @@ -29,6 +29,24 @@ from homeassistant.helpers.selector import ( from .const import CONF_FUEL_TYPES, CONF_STATIONS, DEFAULT_RADIUS, DOMAIN, FUEL_TYPES +async def async_get_nearby_stations( + hass: HomeAssistant, data: dict[str, Any] +) -> dict[str, Any]: + """Fetch nearby stations.""" + try: + return await hass.async_add_executor_job( + getNearbyStations, + data[CONF_API_KEY], + data[CONF_LOCATION][CONF_LATITUDE], + data[CONF_LOCATION][CONF_LONGITUDE], + data[CONF_RADIUS], + "all", + "dist", + ) + except customException as err: + return {"ok": False, "message": err, "exception": True} + + class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" @@ -57,7 +75,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): selected_station_ids: list[str] = [] # add all nearby stations - nearby_stations = await self._get_nearby_stations(config) + nearby_stations = await async_get_nearby_stations(self.hass, config) for station in nearby_stations.get("stations", []): selected_station_ids.append(station["id"]) @@ -91,7 +109,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) self._abort_if_unique_id_configured() - data = await self._get_nearby_stations(user_input) + data = await async_get_nearby_stations(self.hass, user_input) if not data.get("ok"): return self._show_form_user( user_input, errors={CONF_API_KEY: "invalid_auth"} @@ -182,21 +200,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): options=options, ) - async def _get_nearby_stations(self, data: dict[str, Any]) -> dict[str, Any]: - """Fetch nearby stations.""" - try: - return await self.hass.async_add_executor_job( - getNearbyStations, - data[CONF_API_KEY], - data[CONF_LOCATION][CONF_LATITUDE], - data[CONF_LOCATION][CONF_LONGITUDE], - data[CONF_RADIUS], - "all", - "dist", - ) - except customException as err: - return {"ok": False, "message": err, "exception": True} - class OptionsFlowHandler(config_entries.OptionsFlow): """Handle an options flow.""" @@ -204,14 +207,36 @@ class OptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry + self._stations: dict[str, str] = {} async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle options flow.""" if user_input is not None: + self.hass.config_entries.async_update_entry( + self.config_entry, + data={ + **self.config_entry.data, + CONF_STATIONS: user_input.pop(CONF_STATIONS), + }, + ) return self.async_create_entry(title="", data=user_input) + nearby_stations = await async_get_nearby_stations( + self.hass, dict(self.config_entry.data) + ) + if stations := nearby_stations.get("stations"): + for station in stations: + self._stations[ + station["id"] + ] = f"{station['brand']} {station['street']} {station['houseNumber']} - ({station['dist']}km)" + + # add possible extra selected stations from import + for selected_station in self.config_entry.data[CONF_STATIONS]: + if selected_station not in self._stations: + self._stations[selected_station] = f"id: {selected_station}" + return self.async_show_form( step_id="init", data_schema=vol.Schema( @@ -220,6 +245,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_SHOW_ON_MAP, default=self.config_entry.options[CONF_SHOW_ON_MAP], ): bool, + vol.Required( + CONF_STATIONS, default=self.config_entry.data[CONF_STATIONS] + ): cv.multi_select(self._stations), } ), ) diff --git a/homeassistant/components/tankerkoenig/strings.json b/homeassistant/components/tankerkoenig/strings.json index 7c1ba54fcc0..5e0c367c192 100644 --- a/homeassistant/components/tankerkoenig/strings.json +++ b/homeassistant/components/tankerkoenig/strings.json @@ -32,7 +32,7 @@ "init": { "title": "Tankerkoenig options", "data": { - "scan_interval": "Update Interval", + "stations": "Stations", "show_on_map": "Show stations on map" } } diff --git a/homeassistant/components/tankerkoenig/translations/en.json b/homeassistant/components/tankerkoenig/translations/en.json index 399788de8f4..83cc36fd4c8 100644 --- a/homeassistant/components/tankerkoenig/translations/en.json +++ b/homeassistant/components/tankerkoenig/translations/en.json @@ -31,8 +31,8 @@ "step": { "init": { "data": { - "scan_interval": "Update Interval", - "show_on_map": "Show stations on map" + "show_on_map": "Show stations on map", + "stations": "Stations" }, "title": "Tankerkoenig options" } diff --git a/tests/components/tankerkoenig/test_config_flow.py b/tests/components/tankerkoenig/test_config_flow.py index 0a90b424b73..b18df0eed24 100644 --- a/tests/components/tankerkoenig/test_config_flow.py +++ b/tests/components/tankerkoenig/test_config_flow.py @@ -42,6 +42,15 @@ MOCK_STATIONS_DATA = { ], } +MOCK_OPTIONS_DATA = { + **MOCK_USER_DATA, + CONF_STATIONS: [ + "3bcd61da-xxxx-xxxx-xxxx-19d5523a7ae8", + "36b4b812-xxxx-xxxx-xxxx-c51735325858", + "54e2b642-xxxx-xxxx-xxxx-87cd4e9867f1", + ], +} + MOCK_IMPORT_DATA = { CONF_API_KEY: "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx", CONF_FUEL_TYPES: ["e5"], @@ -217,7 +226,7 @@ async def test_options_flow(hass: HomeAssistant): mock_config = MockConfigEntry( domain=DOMAIN, - data=MOCK_USER_DATA, + data=MOCK_OPTIONS_DATA, options={CONF_SHOW_ON_MAP: True}, unique_id=f"{DOMAIN}_{MOCK_USER_DATA[CONF_LOCATION][CONF_LATITUDE]}_{MOCK_USER_DATA[CONF_LOCATION][CONF_LONGITUDE]}", ) @@ -225,17 +234,23 @@ async def test_options_flow(hass: HomeAssistant): with patch( "homeassistant.components.tankerkoenig.async_setup_entry" - ) as mock_setup_entry: + ) as mock_setup_entry, patch( + "homeassistant.components.tankerkoenig.config_flow.getNearbyStations", + return_value=MOCK_NEARVY_STATIONS_OK, + ): await mock_config.async_setup(hass) await hass.async_block_till_done() assert mock_setup_entry.called - result = await hass.config_entries.options.async_init(mock_config.entry_id) + result = await hass.config_entries.options.async_init(mock_config.entry_id) assert result["type"] == 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}, + user_input={ + CONF_SHOW_ON_MAP: False, + CONF_STATIONS: MOCK_OPTIONS_DATA[CONF_STATIONS], + }, ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert not mock_config.options[CONF_SHOW_ON_MAP] From da62e2cc23207faec24f2f4cdc44baa66f4390e9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 28 May 2022 20:46:51 -0700 Subject: [PATCH 1008/3516] Bumped version to 2022.6.0b3 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index acad8f2675a..8d2bcd33f4e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index b7841c53361..db64c7330ce 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.0b2 +version = 2022.6.0b3 url = https://www.home-assistant.io/ [options] From d59ecc4c963ffbeb80c626de6c600170ed85e165 Mon Sep 17 00:00:00 2001 From: Khole Date: Sun, 29 May 2022 11:08:50 +0100 Subject: [PATCH 1009/3516] Refactor hive entity (#72311) * Add hive category entity changes * Updates based on PR feedback * Revert libary bump * Update after PR feedback * Update Binary device class for smoke sensor Co-authored-by: Martin Hjelmare * Remove entity category Co-authored-by: Martin Hjelmare * Updates after PR review * Remove unused import * Update light based on PR feedback * Update light code from PR review Co-authored-by: Martin Hjelmare --- homeassistant/components/hive/__init__.py | 13 +- .../components/hive/alarm_control_panel.py | 41 +------ .../components/hive/binary_sensor.py | 99 +++++++--------- homeassistant/components/hive/climate.py | 99 +++------------- homeassistant/components/hive/light.py | 111 ++++-------------- homeassistant/components/hive/sensor.py | 72 ++++-------- homeassistant/components/hive/switch.py | 69 ++++------- homeassistant/components/hive/water_heater.py | 50 ++------ 8 files changed, 152 insertions(+), 402 deletions(-) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 00c3a327578..af32d39ae8f 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__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 homeassistant.helpers.typing import ConfigType from .const import DOMAIN, PLATFORM_LOOKUP, PLATFORMS @@ -132,8 +132,17 @@ class HiveEntity(Entity): """Initialize the instance.""" self.hive = hive self.device = hive_device + self._attr_name = self.device["haName"] + self._attr_unique_id = f'{self.device["hiveID"]}-{self.device["hiveType"]}' + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.device["device_id"])}, + model=self.device["deviceData"]["model"], + manufacturer=self.device["deviceData"]["manufacturer"], + name=self.device["device_name"], + sw_version=self.device["deviceData"]["version"], + via_device=(DOMAIN, self.device["parentDevice"]), + ) self.attributes = {} - self._unique_id = f'{self.device["hiveID"]}-{self.device["hiveType"]}' async def async_added_to_hass(self): """When entity is added to Home Assistant.""" diff --git a/homeassistant/components/hive/alarm_control_panel.py b/homeassistant/components/hive/alarm_control_panel.py index a3509fce66f..f8f35e20ffa 100644 --- a/homeassistant/components/hive/alarm_control_panel.py +++ b/homeassistant/components/hive/alarm_control_panel.py @@ -13,7 +13,6 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HiveEntity @@ -50,40 +49,6 @@ class HiveAlarmControlPanelEntity(HiveEntity, AlarmControlPanelEntity): | AlarmControlPanelEntityFeature.ARM_AWAY ) - @property - def unique_id(self): - """Return unique ID of entity.""" - return self._unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return device information about this AdGuard Home instance.""" - return DeviceInfo( - identifiers={(DOMAIN, self.device["device_id"])}, - model=self.device["deviceData"]["model"], - manufacturer=self.device["deviceData"]["manufacturer"], - name=self.device["device_name"], - sw_version=self.device["deviceData"]["version"], - via_device=(DOMAIN, self.device["parentDevice"]), - ) - - @property - def name(self): - """Return the name of the alarm.""" - return self.device["haName"] - - @property - def available(self): - """Return if the device is available.""" - return self.device["deviceData"]["online"] - - @property - def state(self): - """Return state of alarm.""" - if self.device["status"]["state"]: - return STATE_ALARM_TRIGGERED - return HIVETOHA[self.device["status"]["mode"]] - async def async_alarm_disarm(self, code=None): """Send disarm command.""" await self.hive.alarm.setMode(self.device, "home") @@ -100,3 +65,9 @@ class HiveAlarmControlPanelEntity(HiveEntity, AlarmControlPanelEntity): """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) self.device = await self.hive.alarm.getAlarm(self.device) + self._attr_available = self.device["deviceData"].get("online") + if self._attr_available: + if self.device["status"]["state"]: + self._attr_state = STATE_ALARM_TRIGGERED + else: + self._attr_state = HIVETOHA[self.device["status"]["mode"]] diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index 934974d3c1e..313c78275e7 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -4,27 +4,46 @@ from datetime import timedelta from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HiveEntity -from .const import ATTR_MODE, DOMAIN +from .const import DOMAIN -DEVICETYPE = { - "contactsensor": BinarySensorDeviceClass.OPENING, - "motionsensor": BinarySensorDeviceClass.MOTION, - "Connectivity": BinarySensorDeviceClass.CONNECTIVITY, - "SMOKE_CO": BinarySensorDeviceClass.SMOKE, - "DOG_BARK": BinarySensorDeviceClass.SOUND, - "GLASS_BREAK": BinarySensorDeviceClass.SOUND, -} PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=15) +BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key="contactsensor", device_class=BinarySensorDeviceClass.OPENING + ), + BinarySensorEntityDescription( + key="motionsensor", + device_class=BinarySensorDeviceClass.MOTION, + ), + BinarySensorEntityDescription( + key="Connectivity", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + ), + BinarySensorEntityDescription( + key="SMOKE_CO", + device_class=BinarySensorDeviceClass.SMOKE, + ), + BinarySensorEntityDescription( + key="DOG_BARK", + device_class=BinarySensorDeviceClass.SOUND, + ), + BinarySensorEntityDescription( + key="GLASS_BREAK", + device_class=BinarySensorDeviceClass.SOUND, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -34,62 +53,28 @@ async def async_setup_entry( devices = hive.session.deviceList.get("binary_sensor") entities = [] if devices: - for dev in devices: - entities.append(HiveBinarySensorEntity(hive, dev)) + for description in BINARY_SENSOR_TYPES: + for dev in devices: + if dev["hiveType"] == description.key: + entities.append(HiveBinarySensorEntity(hive, dev, description)) async_add_entities(entities, True) class HiveBinarySensorEntity(HiveEntity, BinarySensorEntity): """Representation of a Hive binary sensor.""" - @property - def unique_id(self): - """Return unique ID of entity.""" - return self._unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return device information.""" - return DeviceInfo( - identifiers={(DOMAIN, self.device["device_id"])}, - 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"]), - ) - - @property - def device_class(self): - """Return the class of this sensor.""" - return DEVICETYPE.get(self.device["hiveType"]) - - @property - def name(self): - """Return the name of the binary sensor.""" - return self.device["haName"] - - @property - def available(self): - """Return if the device is available.""" - if self.device["hiveType"] != "Connectivity": - return self.device["deviceData"]["online"] - return True - - @property - def extra_state_attributes(self): - """Show Device Attributes.""" - return { - ATTR_MODE: self.attributes.get(ATTR_MODE), - } - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self.device["status"]["state"] + def __init__(self, hive, hive_device, entity_description): + """Initialise hive binary sensor.""" + super().__init__(hive, hive_device) + self.entity_description = entity_description async def async_update(self): """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) self.device = await self.hive.sensor.getSensor(self.device) self.attributes = self.device.get("attributes", {}) + self._attr_is_on = self.device["status"]["state"] + if self.device["hiveType"] != "Connectivity": + self._attr_available = self.device["deviceData"].get("online") + else: + self._attr_available = True diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index d094ca9eace..d6dfcfa6b2c 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -16,7 +16,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HiveEntity, refresh_system @@ -46,8 +45,6 @@ HIVE_TO_HASS_HVAC_ACTION = { } TEMP_UNIT = {"C": TEMP_CELSIUS, "F": TEMP_FAHRENHEIT} - -SUPPORT_PRESET = [PRESET_NONE, PRESET_BOOST] PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=15) _LOGGER = logging.getLogger() @@ -105,6 +102,7 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): """Hive Climate Device.""" _attr_hvac_modes = [HVACMode.AUTO, HVACMode.HEAT, HVACMode.OFF] + _attr_preset_modes = [PRESET_BOOST, PRESET_NONE] _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE ) @@ -113,84 +111,7 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): """Initialize the Climate device.""" super().__init__(hive_session, hive_device) self.thermostat_node_id = hive_device["device_id"] - self.temperature_type = TEMP_UNIT.get(hive_device["temperatureunit"]) - - @property - def unique_id(self): - """Return unique ID of entity.""" - return self._unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return device information.""" - return DeviceInfo( - identifiers={(DOMAIN, self.device["device_id"])}, - 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"]), - ) - - @property - def name(self): - """Return the name of the Climate device.""" - return self.device["haName"] - - @property - def available(self): - """Return if the device is available.""" - return self.device["deviceData"]["online"] - - @property - def hvac_mode(self) -> HVACMode: - """Return hvac operation ie. heat, cool mode. - - Need to be one of HVAC_MODE_*. - """ - return HIVE_TO_HASS_STATE[self.device["status"]["mode"]] - - @property - def hvac_action(self) -> HVACAction: - """Return current HVAC action.""" - return HIVE_TO_HASS_HVAC_ACTION[self.device["status"]["action"]] - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return self.temperature_type - - @property - def current_temperature(self): - """Return the current temperature.""" - return self.device["status"]["current_temperature"] - - @property - def target_temperature(self): - """Return the target temperature.""" - return self.device["status"]["target_temperature"] - - @property - def min_temp(self): - """Return minimum temperature.""" - return self.device["min_temp"] - - @property - def max_temp(self): - """Return the maximum temperature.""" - return self.device["max_temp"] - - @property - def preset_mode(self): - """Return the current preset mode, e.g., home, away, temp.""" - if self.device["status"]["boost"] == "ON": - return PRESET_BOOST - return PRESET_NONE - - @property - def preset_modes(self): - """Return a list of available preset modes.""" - return SUPPORT_PRESET + self._attr_temperature_unit = TEMP_UNIT.get(hive_device["temperatureunit"]) @refresh_system async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: @@ -236,3 +157,19 @@ class HiveClimateEntity(HiveEntity, ClimateEntity): """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) self.device = await self.hive.heating.getClimate(self.device) + self._attr_available = self.device["deviceData"].get("online") + if self._attr_available: + self._attr_hvac_mode = HIVE_TO_HASS_STATE[self.device["status"]["mode"]] + self._attr_hvac_action = HIVE_TO_HASS_HVAC_ACTION[ + self.device["status"]["action"] + ] + self._attr_current_temperature = self.device["status"][ + "current_temperature" + ] + self._attr_target_temperature = self.device["status"]["target_temperature"] + self._attr_min_temp = self.device["min_temp"] + self._attr_max_temp = self.device["max_temp"] + if self.device["status"]["boost"] == "ON": + self._attr_preset_mode = PRESET_BOOST + else: + self._attr_preset_mode = PRESET_NONE diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index ba095896b64..c06237f3709 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -12,7 +12,6 @@ from homeassistant.components.light import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util @@ -40,72 +39,18 @@ async def async_setup_entry( class HiveDeviceLight(HiveEntity, LightEntity): """Hive Active Light Device.""" - @property - def unique_id(self): - """Return unique ID of entity.""" - return self._unique_id + def __init__(self, hive, hive_device): + """Initialise hive light.""" + super().__init__(hive, hive_device) + if self.device["hiveType"] == "warmwhitelight": + self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} + elif self.device["hiveType"] == "tuneablelight": + self._attr_supported_color_modes = {ColorMode.COLOR_TEMP} + elif self.device["hiveType"] == "colourtuneablelight": + self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} - @property - def device_info(self) -> DeviceInfo: - """Return device information.""" - return DeviceInfo( - identifiers={(DOMAIN, self.device["device_id"])}, - 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"]), - ) - - @property - def name(self): - """Return the display name of this light.""" - return self.device["haName"] - - @property - def available(self): - """Return if the device is available.""" - return self.device["deviceData"]["online"] - - @property - def extra_state_attributes(self): - """Show Device Attributes.""" - return { - ATTR_MODE: self.attributes.get(ATTR_MODE), - } - - @property - def brightness(self): - """Brightness of the light (an integer in the range 1-255).""" - return self.device["status"]["brightness"] - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - return self.device.get("min_mireds") - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - return self.device.get("max_mireds") - - @property - def color_temp(self): - """Return the CT color value in mireds.""" - return self.device["status"].get("color_temp") - - @property - def hs_color(self): - """Return the hs color value.""" - if self.device["status"]["mode"] == "COLOUR": - rgb = self.device["status"].get("hs_color") - return color_util.color_RGB_to_hs(*rgb) - return None - - @property - def is_on(self): - """Return true if light is on.""" - return self.device["status"]["state"] + self._attr_min_mireds = self.device.get("min_mireds") + self._attr_max_mireds = self.device.get("max_mireds") @refresh_system async def async_turn_on(self, **kwargs): @@ -137,32 +82,18 @@ class HiveDeviceLight(HiveEntity, LightEntity): """Instruct the light to turn off.""" await self.hive.light.turnOff(self.device) - @property - def color_mode(self) -> str: - """Return the color mode of the light.""" - if self.device["hiveType"] == "warmwhitelight": - return ColorMode.BRIGHTNESS - if self.device["hiveType"] == "tuneablelight": - return ColorMode.COLOR_TEMP - if self.device["hiveType"] == "colourtuneablelight": - if self.device["status"]["mode"] == "COLOUR": - return ColorMode.HS - return ColorMode.COLOR_TEMP - return ColorMode.ONOFF - - @property - def supported_color_modes(self) -> set[str] | None: - """Flag supported color modes.""" - if self.device["hiveType"] == "warmwhitelight": - return {ColorMode.BRIGHTNESS} - if self.device["hiveType"] == "tuneablelight": - return {ColorMode.COLOR_TEMP} - if self.device["hiveType"] == "colourtuneablelight": - return {ColorMode.COLOR_TEMP, ColorMode.HS} - return {ColorMode.ONOFF} - async def async_update(self): """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) self.device = await self.hive.light.getLight(self.device) self.attributes.update(self.device.get("attributes", {})) + self._attr_extra_state_attributes = { + ATTR_MODE: self.attributes.get(ATTR_MODE), + } + self._attr_available = self.device["deviceData"].get("online") + if self._attr_available: + self._attr_is_on = self.device["status"]["state"] + self._attr_brightness = self.device["status"]["brightness"] + if self.device["hiveType"] == "colourtuneablelight": + rgb = self.device["status"]["hs_color"] + self._attr_hs_color = color_util.color_RGB_to_hs(*rgb) diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index 68de137dee7..bab8648407a 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -1,10 +1,14 @@ """Support for the Hive sensors.""" from datetime import timedelta -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HiveEntity @@ -12,71 +16,41 @@ from .const import DOMAIN PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=15) -DEVICETYPE = { - "Battery": {"unit": " % ", "type": SensorDeviceClass.BATTERY}, -} + +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="Battery", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, + ), +) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Hive thermostat based on a config entry.""" - hive = hass.data[DOMAIN][entry.entry_id] devices = hive.session.deviceList.get("sensor") entities = [] if devices: - for dev in devices: - entities.append(HiveSensorEntity(hive, dev)) + for description in SENSOR_TYPES: + for dev in devices: + if dev["hiveType"] == description.key: + entities.append(HiveSensorEntity(hive, dev, description)) async_add_entities(entities, True) class HiveSensorEntity(HiveEntity, SensorEntity): """Hive Sensor Entity.""" - @property - def unique_id(self): - """Return unique ID of entity.""" - return self._unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return device information.""" - return DeviceInfo( - identifiers={(DOMAIN, self.device["device_id"])}, - 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"]), - ) - - @property - def available(self): - """Return if sensor is available.""" - return self.device.get("deviceData", {}).get("online") - - @property - def device_class(self): - """Device class of the entity.""" - return DEVICETYPE[self.device["hiveType"]].get("type") - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement.""" - return DEVICETYPE[self.device["hiveType"]].get("unit") - - @property - def name(self): - """Return the name of the sensor.""" - return self.device["haName"] - - @property - def native_value(self): - """Return the state of the sensor.""" - return self.device["status"]["state"] + def __init__(self, hive, hive_device, entity_description): + """Initialise hive sensor.""" + super().__init__(hive, hive_device) + self.entity_description = entity_description async def async_update(self): """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) self.device = await self.hive.sensor.getSensor(self.device) + self._attr_native_value = self.device["status"]["state"] diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index cb9ac79d51e..64a8276521a 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -3,10 +3,9 @@ from __future__ import annotations from datetime import timedelta -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HiveEntity, refresh_system @@ -16,6 +15,14 @@ PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=15) +SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( + SwitchEntityDescription( + key="activeplug", + ), + SwitchEntityDescription(key="Heating_Heat_On_Demand"), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -25,54 +32,20 @@ async def async_setup_entry( devices = hive.session.deviceList.get("switch") entities = [] if devices: - for dev in devices: - entities.append(HiveDevicePlug(hive, dev)) + for description in SWITCH_TYPES: + for dev in devices: + if dev["hiveType"] == description.key: + entities.append(HiveSwitch(hive, dev, description)) async_add_entities(entities, True) -class HiveDevicePlug(HiveEntity, SwitchEntity): +class HiveSwitch(HiveEntity, SwitchEntity): """Hive Active Plug.""" - @property - def unique_id(self): - """Return unique ID of entity.""" - return self._unique_id - - @property - def device_info(self) -> DeviceInfo | None: - """Return device information.""" - if self.device["hiveType"] == "activeplug": - return DeviceInfo( - identifiers={(DOMAIN, self.device["device_id"])}, - 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"]), - ) - return None - - @property - def name(self): - """Return the name of this Switch device if any.""" - return self.device["haName"] - - @property - def available(self): - """Return if the device is available.""" - return self.device["deviceData"].get("online") - - @property - def extra_state_attributes(self): - """Show Device Attributes.""" - return { - ATTR_MODE: self.attributes.get(ATTR_MODE), - } - - @property - def is_on(self): - """Return true if switch is on.""" - return self.device["status"]["state"] + def __init__(self, hive, hive_device, entity_description): + """Initialise hive switch.""" + super().__init__(hive, hive_device) + self.entity_description = entity_description @refresh_system async def async_turn_on(self, **kwargs): @@ -89,3 +62,9 @@ class HiveDevicePlug(HiveEntity, SwitchEntity): await self.hive.session.updateData(self.device) self.device = await self.hive.switch.getSwitch(self.device) self.attributes.update(self.device.get("attributes", {})) + self._attr_extra_state_attributes = { + ATTR_MODE: self.attributes.get(ATTR_MODE), + } + self._attr_available = self.device["deviceData"].get("online") + if self._attr_available: + self._attr_is_on = self.device["status"]["state"] diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index 5e3c18fce69..0e7f2453c92 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -12,7 +12,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HiveEntity, refresh_system @@ -75,48 +74,8 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity): """Hive Water Heater Device.""" _attr_supported_features = WaterHeaterEntityFeature.OPERATION_MODE - - @property - def unique_id(self): - """Return unique ID of entity.""" - return self._unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return device information.""" - return DeviceInfo( - identifiers={(DOMAIN, self.device["device_id"])}, - 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"]), - ) - - @property - def name(self): - """Return the name of the water heater.""" - return HOTWATER_NAME - - @property - def available(self): - """Return if the device is available.""" - return self.device["deviceData"]["online"] - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def current_operation(self): - """Return current operation.""" - return HIVE_TO_HASS_STATE[self.device["status"]["current_operation"]] - - @property - def operation_list(self): - """List of available operation modes.""" - return SUPPORT_WATER_HEATER + _attr_temperature_unit = TEMP_CELSIUS + _attr_operation_list = SUPPORT_WATER_HEATER @refresh_system async def async_turn_on(self, **kwargs): @@ -146,3 +105,8 @@ class HiveWaterHeater(HiveEntity, WaterHeaterEntity): """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) self.device = await self.hive.hotwater.getWaterHeater(self.device) + self._attr_available = self.device["deviceData"].get("online") + if self._attr_available: + self._attr_current_operation = HIVE_TO_HASS_STATE[ + self.device["status"]["current_operation"] + ] From d6039528723a5fbcfbad5ecf01c65e311fc1e651 Mon Sep 17 00:00:00 2001 From: shbatm Date: Sun, 29 May 2022 11:00:18 -0500 Subject: [PATCH 1010/3516] Check ISY994 climate for unknown humidity on Z-Wave Thermostat (#72670) --- homeassistant/components/isy994/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 1276207f23c..d68395f14da 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -6,6 +6,7 @@ from typing import Any from pyisy.constants import ( CMD_CLIMATE_FAN_SETTING, CMD_CLIMATE_MODE, + ISY_VALUE_UNKNOWN, PROP_HEAT_COOL_STATE, PROP_HUMIDITY, PROP_SETPOINT_COOL, @@ -116,6 +117,8 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): """Return the current humidity.""" if not (humidity := self._node.aux_properties.get(PROP_HUMIDITY)): return None + if humidity == ISY_VALUE_UNKNOWN: + return None return int(humidity.value) @property From 237ef6419b5d77adf7dc09d7dd554965406103bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 May 2022 06:27:32 -1000 Subject: [PATCH 1011/3516] Add basic typing to emulated_hue (#72663) * Add basic typing to emulated_hue * type a few more places * fixes * numbers are always stringified * numbers are always stringified * coverage * drop assert --- .../components/emulated_hue/__init__.py | 233 +++--------------- .../components/emulated_hue/config.py | 213 ++++++++++++++++ .../components/emulated_hue/const.py | 2 + .../components/emulated_hue/hue_api.py | 96 +++++--- tests/components/emulated_hue/test_init.py | 16 +- tests/components/emulated_hue/test_upnp.py | 13 +- 6 files changed, 324 insertions(+), 249 deletions(-) create mode 100644 homeassistant/components/emulated_hue/config.py diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index d3586ab5fcf..71f98abed80 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -1,4 +1,6 @@ """Support for local control of entities by emulating a Philips Hue bridge.""" +from __future__ import annotations + import logging from aiohttp import web @@ -12,10 +14,29 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import storage import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType +from .config import ( + CONF_ADVERTISE_IP, + CONF_ADVERTISE_PORT, + CONF_ENTITY_HIDDEN, + CONF_ENTITY_NAME, + CONF_EXPOSE_BY_DEFAULT, + CONF_EXPOSED_DOMAINS, + CONF_HOST_IP, + CONF_LIGHTS_ALL_DIMMABLE, + CONF_LISTEN_PORT, + CONF_OFF_MAPS_TO_ON_DOMAINS, + CONF_UPNP_BIND_MULTICAST, + DEFAULT_LIGHTS_ALL_DIMMABLE, + DEFAULT_LISTEN_PORT, + DEFAULT_TYPE, + TYPE_ALEXA, + TYPE_GOOGLE, + Config, +) +from .const import DOMAIN from .hue_api import ( HueAllGroupsStateView, HueAllLightsStateView, @@ -27,46 +48,14 @@ from .hue_api import ( HueUnauthorizedUser, HueUsernameView, ) -from .upnp import DescriptionXmlView, create_upnp_datagram_endpoint - -DOMAIN = "emulated_hue" +from .upnp import ( + DescriptionXmlView, + UPNPResponderProtocol, + create_upnp_datagram_endpoint, +) _LOGGER = logging.getLogger(__name__) -NUMBERS_FILE = "emulated_hue_ids.json" -DATA_KEY = "emulated_hue.ids" -DATA_VERSION = "1" -SAVE_DELAY = 60 - -CONF_ADVERTISE_IP = "advertise_ip" -CONF_ADVERTISE_PORT = "advertise_port" -CONF_ENTITY_HIDDEN = "hidden" -CONF_ENTITY_NAME = "name" -CONF_EXPOSE_BY_DEFAULT = "expose_by_default" -CONF_EXPOSED_DOMAINS = "exposed_domains" -CONF_HOST_IP = "host_ip" -CONF_LIGHTS_ALL_DIMMABLE = "lights_all_dimmable" -CONF_LISTEN_PORT = "listen_port" -CONF_OFF_MAPS_TO_ON_DOMAINS = "off_maps_to_on_domains" -CONF_UPNP_BIND_MULTICAST = "upnp_bind_multicast" - -TYPE_ALEXA = "alexa" -TYPE_GOOGLE = "google_home" - -DEFAULT_LIGHTS_ALL_DIMMABLE = False -DEFAULT_LISTEN_PORT = 8300 -DEFAULT_UPNP_BIND_MULTICAST = True -DEFAULT_OFF_MAPS_TO_ON_DOMAINS = ["script", "scene"] -DEFAULT_EXPOSE_BY_DEFAULT = True -DEFAULT_EXPOSED_DOMAINS = [ - "switch", - "light", - "group", - "input_boolean", - "media_player", - "fan", -] -DEFAULT_TYPE = TYPE_GOOGLE CONFIG_ENTITY_SCHEMA = vol.Schema( { @@ -75,6 +64,7 @@ CONFIG_ENTITY_SCHEMA = vol.Schema( } ) + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -102,8 +92,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -ATTR_EMULATED_HUE_NAME = "emulated_hue_name" - async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: """Activate the emulated_hue component.""" @@ -140,7 +128,7 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: config.advertise_ip, config.advertise_port or config.listen_port, ) - protocol = None + protocol: UPNPResponderProtocol | None = None async def stop_emulated_hue_bridge(event): """Stop the emulated hue bridge.""" @@ -161,7 +149,8 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: nonlocal site nonlocal runner - _, protocol = await listen + transport_protocol = await listen + protocol = transport_protocol[1] runner = web.AppRunner(app) await runner.setup() @@ -184,163 +173,3 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge) return True - - -class Config: - """Hold configuration variables for the emulated hue bridge.""" - - def __init__(self, hass, conf, local_ip): - """Initialize the instance.""" - self.hass = hass - self.type = conf.get(CONF_TYPE) - self.numbers = None - self.store = None - self.cached_states = {} - self._exposed_cache = {} - - if self.type == TYPE_ALEXA: - _LOGGER.warning( - "Emulated Hue running in legacy mode because type has been " - "specified. More info at https://goo.gl/M6tgz8" - ) - - # Get the IP address that will be passed to the Echo during discovery - self.host_ip_addr = conf.get(CONF_HOST_IP) - if self.host_ip_addr is None: - self.host_ip_addr = local_ip - - # Get the port that the Hue bridge will listen on - self.listen_port = conf.get(CONF_LISTEN_PORT) - if not isinstance(self.listen_port, int): - self.listen_port = DEFAULT_LISTEN_PORT - _LOGGER.info( - "Listen port not specified, defaulting to %s", self.listen_port - ) - - # Get whether or not UPNP binds to multicast address (239.255.255.250) - # or to the unicast address (host_ip_addr) - self.upnp_bind_multicast = conf.get( - CONF_UPNP_BIND_MULTICAST, DEFAULT_UPNP_BIND_MULTICAST - ) - - # Get domains that cause both "on" and "off" commands to map to "on" - # This is primarily useful for things like scenes or scripts, which - # don't really have a concept of being off - self.off_maps_to_on_domains = conf.get(CONF_OFF_MAPS_TO_ON_DOMAINS) - if not isinstance(self.off_maps_to_on_domains, list): - self.off_maps_to_on_domains = DEFAULT_OFF_MAPS_TO_ON_DOMAINS - - # Get whether or not entities should be exposed by default, or if only - # explicitly marked ones will be exposed - self.expose_by_default = conf.get( - CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT - ) - - # Get domains that are exposed by default when expose_by_default is - # True - self.exposed_domains = set( - conf.get(CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS) - ) - - # Calculated effective advertised IP and port for network isolation - self.advertise_ip = conf.get(CONF_ADVERTISE_IP) or self.host_ip_addr - - self.advertise_port = conf.get(CONF_ADVERTISE_PORT) or self.listen_port - - self.entities = conf.get(CONF_ENTITIES, {}) - - self._entities_with_hidden_attr_in_config = {} - for entity_id in self.entities: - hidden_value = self.entities[entity_id].get(CONF_ENTITY_HIDDEN) - if hidden_value is not None: - self._entities_with_hidden_attr_in_config[entity_id] = hidden_value - - # Get whether all non-dimmable lights should be reported as dimmable - # for compatibility with older installations. - self.lights_all_dimmable = conf.get(CONF_LIGHTS_ALL_DIMMABLE) - - async def async_setup(self): - """Set up and migrate to storage.""" - self.store = storage.Store(self.hass, DATA_VERSION, DATA_KEY) - self.numbers = ( - await storage.async_migrator( - self.hass, self.hass.config.path(NUMBERS_FILE), self.store - ) - or {} - ) - - def entity_id_to_number(self, entity_id): - """Get a unique number for the entity id.""" - if self.type == TYPE_ALEXA: - return entity_id - - # Google Home - for number, ent_id in self.numbers.items(): - if entity_id == ent_id: - return number - - number = "1" - if self.numbers: - number = str(max(int(k) for k in self.numbers) + 1) - self.numbers[number] = entity_id - self.store.async_delay_save(lambda: self.numbers, SAVE_DELAY) - return number - - def number_to_entity_id(self, number): - """Convert unique number to entity id.""" - if self.type == TYPE_ALEXA: - return number - - # Google Home - assert isinstance(number, str) - return self.numbers.get(number) - - def get_entity_name(self, entity): - """Get the name of an entity.""" - if ( - entity.entity_id in self.entities - and CONF_ENTITY_NAME in self.entities[entity.entity_id] - ): - return self.entities[entity.entity_id][CONF_ENTITY_NAME] - - return entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name) - - def is_entity_exposed(self, entity): - """Cache determine if an entity should be exposed on the emulated bridge.""" - entity_id = entity.entity_id - if entity_id not in self._exposed_cache: - self._exposed_cache[entity_id] = self._is_entity_exposed(entity) - return self._exposed_cache[entity_id] - - def filter_exposed_entities(self, states): - """Filter a list of all states down to exposed entities.""" - exposed = [] - for entity in states: - entity_id = entity.entity_id - if entity_id not in self._exposed_cache: - self._exposed_cache[entity_id] = self._is_entity_exposed(entity) - if self._exposed_cache[entity_id]: - exposed.append(entity) - return exposed - - def _is_entity_exposed(self, entity): - """Determine if an entity should be exposed on the emulated bridge. - - Async friendly. - """ - if entity.attributes.get("view") is not None: - # Ignore entities that are views - return False - - if entity.entity_id in self._entities_with_hidden_attr_in_config: - return not self._entities_with_hidden_attr_in_config[entity.entity_id] - - if not self.expose_by_default: - return False - # Expose an entity if the entity's domain is exposed by default and - # the configuration doesn't explicitly exclude it from being - # exposed, or if the entity is explicitly exposed - if entity.domain in self.exposed_domains: - return True - - return False diff --git a/homeassistant/components/emulated_hue/config.py b/homeassistant/components/emulated_hue/config.py new file mode 100644 index 00000000000..e39ec9839c8 --- /dev/null +++ b/homeassistant/components/emulated_hue/config.py @@ -0,0 +1,213 @@ +"""Support for local control of entities by emulating a Philips Hue bridge.""" +from __future__ import annotations + +from collections.abc import Iterable +import logging + +from homeassistant.const import CONF_ENTITIES, CONF_TYPE +from homeassistant.core import HomeAssistant, State +from homeassistant.helpers import storage +from homeassistant.helpers.typing import ConfigType + +TYPE_ALEXA = "alexa" +TYPE_GOOGLE = "google_home" + + +NUMBERS_FILE = "emulated_hue_ids.json" +DATA_KEY = "emulated_hue.ids" +DATA_VERSION = "1" +SAVE_DELAY = 60 + +CONF_ADVERTISE_IP = "advertise_ip" +CONF_ADVERTISE_PORT = "advertise_port" +CONF_ENTITY_HIDDEN = "hidden" +CONF_ENTITY_NAME = "name" +CONF_EXPOSE_BY_DEFAULT = "expose_by_default" +CONF_EXPOSED_DOMAINS = "exposed_domains" +CONF_HOST_IP = "host_ip" +CONF_LIGHTS_ALL_DIMMABLE = "lights_all_dimmable" +CONF_LISTEN_PORT = "listen_port" +CONF_OFF_MAPS_TO_ON_DOMAINS = "off_maps_to_on_domains" +CONF_UPNP_BIND_MULTICAST = "upnp_bind_multicast" + + +DEFAULT_LIGHTS_ALL_DIMMABLE = False +DEFAULT_LISTEN_PORT = 8300 +DEFAULT_UPNP_BIND_MULTICAST = True +DEFAULT_OFF_MAPS_TO_ON_DOMAINS = {"script", "scene"} +DEFAULT_EXPOSE_BY_DEFAULT = True +DEFAULT_EXPOSED_DOMAINS = [ + "switch", + "light", + "group", + "input_boolean", + "media_player", + "fan", +] +DEFAULT_TYPE = TYPE_GOOGLE + +ATTR_EMULATED_HUE_NAME = "emulated_hue_name" + + +_LOGGER = logging.getLogger(__name__) + + +class Config: + """Hold configuration variables for the emulated hue bridge.""" + + def __init__( + self, hass: HomeAssistant, conf: ConfigType, local_ip: str | None + ) -> None: + """Initialize the instance.""" + self.hass = hass + self.type = conf.get(CONF_TYPE) + self.numbers: dict[str, str] = {} + self.store: storage.Store | None = None + self.cached_states: dict[str, list] = {} + self._exposed_cache: dict[str, bool] = {} + + if self.type == TYPE_ALEXA: + _LOGGER.warning( + "Emulated Hue running in legacy mode because type has been " + "specified. More info at https://goo.gl/M6tgz8" + ) + + # Get the IP address that will be passed to the Echo during discovery + self.host_ip_addr = conf.get(CONF_HOST_IP) + if self.host_ip_addr is None: + self.host_ip_addr = local_ip + + # Get the port that the Hue bridge will listen on + self.listen_port = conf.get(CONF_LISTEN_PORT) + if not isinstance(self.listen_port, int): + self.listen_port = DEFAULT_LISTEN_PORT + _LOGGER.info( + "Listen port not specified, defaulting to %s", self.listen_port + ) + + # Get whether or not UPNP binds to multicast address (239.255.255.250) + # or to the unicast address (host_ip_addr) + self.upnp_bind_multicast = conf.get( + CONF_UPNP_BIND_MULTICAST, DEFAULT_UPNP_BIND_MULTICAST + ) + + # Get domains that cause both "on" and "off" commands to map to "on" + # This is primarily useful for things like scenes or scripts, which + # don't really have a concept of being off + off_maps_to_on_domains = conf.get(CONF_OFF_MAPS_TO_ON_DOMAINS) + if isinstance(off_maps_to_on_domains, list): + self.off_maps_to_on_domains = set(off_maps_to_on_domains) + else: + self.off_maps_to_on_domains = DEFAULT_OFF_MAPS_TO_ON_DOMAINS + + # Get whether or not entities should be exposed by default, or if only + # explicitly marked ones will be exposed + self.expose_by_default = conf.get( + CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT + ) + + # Get domains that are exposed by default when expose_by_default is + # True + self.exposed_domains = set( + conf.get(CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS) + ) + + # Calculated effective advertised IP and port for network isolation + self.advertise_ip = conf.get(CONF_ADVERTISE_IP) or self.host_ip_addr + + self.advertise_port = conf.get(CONF_ADVERTISE_PORT) or self.listen_port + + self.entities = conf.get(CONF_ENTITIES, {}) + + self._entities_with_hidden_attr_in_config = {} + for entity_id in self.entities: + hidden_value = self.entities[entity_id].get(CONF_ENTITY_HIDDEN) + if hidden_value is not None: + self._entities_with_hidden_attr_in_config[entity_id] = hidden_value + + # Get whether all non-dimmable lights should be reported as dimmable + # for compatibility with older installations. + self.lights_all_dimmable = conf.get(CONF_LIGHTS_ALL_DIMMABLE) + + async def async_setup(self) -> None: + """Set up and migrate to storage.""" + self.store = storage.Store(self.hass, DATA_VERSION, DATA_KEY) # type: ignore[arg-type] + self.numbers = ( + await storage.async_migrator( + self.hass, self.hass.config.path(NUMBERS_FILE), self.store + ) + or {} + ) + + def entity_id_to_number(self, entity_id: str) -> str: + """Get a unique number for the entity id.""" + if self.type == TYPE_ALEXA: + return entity_id + + # Google Home + for number, ent_id in self.numbers.items(): + if entity_id == ent_id: + return number + + number = "1" + if self.numbers: + number = str(max(int(k) for k in self.numbers) + 1) + self.numbers[number] = entity_id + assert self.store is not None + self.store.async_delay_save(lambda: self.numbers, SAVE_DELAY) + return number + + def number_to_entity_id(self, number: str) -> str | None: + """Convert unique number to entity id.""" + if self.type == TYPE_ALEXA: + return number + + # Google Home + return self.numbers.get(number) + + def get_entity_name(self, entity: State) -> str: + """Get the name of an entity.""" + if ( + entity.entity_id in self.entities + and CONF_ENTITY_NAME in self.entities[entity.entity_id] + ): + return self.entities[entity.entity_id][CONF_ENTITY_NAME] + + return entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name) + + def is_entity_exposed(self, entity: State) -> bool: + """Cache determine if an entity should be exposed on the emulated bridge.""" + if (exposed := self._exposed_cache.get(entity.entity_id)) is not None: + return exposed + exposed = self._is_entity_exposed(entity) + self._exposed_cache[entity.entity_id] = exposed + return exposed + + def filter_exposed_entities(self, states: Iterable[State]) -> list[State]: + """Filter a list of all states down to exposed entities.""" + exposed: list[State] = [ + state for state in states if self.is_entity_exposed(state) + ] + return exposed + + def _is_entity_exposed(self, entity: State) -> bool: + """Determine if an entity should be exposed on the emulated bridge. + + Async friendly. + """ + if entity.attributes.get("view") is not None: + # Ignore entities that are views + return False + + if entity.entity_id in self._entities_with_hidden_attr_in_config: + return not self._entities_with_hidden_attr_in_config[entity.entity_id] + + if not self.expose_by_default: + return False + # Expose an entity if the entity's domain is exposed by default and + # the configuration doesn't explicitly exclude it from being + # exposed, or if the entity is explicitly exposed + if entity.domain in self.exposed_domains: + return True + + return False diff --git a/homeassistant/components/emulated_hue/const.py b/homeassistant/components/emulated_hue/const.py index bfd58c5a0e1..2bcd8cbac19 100644 --- a/homeassistant/components/emulated_hue/const.py +++ b/homeassistant/components/emulated_hue/const.py @@ -2,3 +2,5 @@ HUE_SERIAL_NUMBER = "001788FFFE23BFC2" HUE_UUID = "2f402f80-da50-11e1-9b23-001788255acc" + +DOMAIN = "emulated_hue" diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index b4f926afd31..e7a4876730c 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -1,10 +1,15 @@ """Support for a Hue API to control Home Assistant.""" +from __future__ import annotations + import asyncio import hashlib from http import HTTPStatus from ipaddress import ip_address import logging import time +from typing import Any + +from aiohttp import web from homeassistant import core from homeassistant.components import ( @@ -58,9 +63,12 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.core import State from homeassistant.helpers.event import async_track_state_change_event from homeassistant.util.network import is_local +from .config import Config + _LOGGER = logging.getLogger(__name__) # How long to wait for a state change to happen @@ -111,7 +119,7 @@ class HueUnauthorizedUser(HomeAssistantView): extra_urls = ["/api/"] requires_auth = False - async def get(self, request): + async def get(self, request: web.Request) -> web.Response: """Handle a GET request.""" return self.json(UNAUTHORIZED_USER) @@ -124,8 +132,9 @@ class HueUsernameView(HomeAssistantView): extra_urls = ["/api/"] requires_auth = False - async def post(self, request): + async def post(self, request: web.Request) -> web.Response: """Handle a POST request.""" + assert request.remote is not None if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTPStatus.UNAUTHORIZED) @@ -147,13 +156,14 @@ class HueAllGroupsStateView(HomeAssistantView): name = "emulated_hue:all_groups:state" requires_auth = False - def __init__(self, config): + def __init__(self, config: Config) -> None: """Initialize the instance of the view.""" self.config = config @core.callback - def get(self, request, username): + def get(self, request: web.Request, username: str) -> web.Response: """Process a request to make the Brilliant Lightpad work.""" + assert request.remote is not None if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTPStatus.UNAUTHORIZED) @@ -167,13 +177,14 @@ class HueGroupView(HomeAssistantView): name = "emulated_hue:groups:state" requires_auth = False - def __init__(self, config): + def __init__(self, config: Config) -> None: """Initialize the instance of the view.""" self.config = config @core.callback - def put(self, request, username): + def put(self, request: web.Request, username: str) -> web.Response: """Process a request to make the Logitech Pop working.""" + assert request.remote is not None if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTPStatus.UNAUTHORIZED) @@ -197,13 +208,14 @@ class HueAllLightsStateView(HomeAssistantView): name = "emulated_hue:lights:state" requires_auth = False - def __init__(self, config): + def __init__(self, config: Config) -> None: """Initialize the instance of the view.""" self.config = config @core.callback - def get(self, request, username): + def get(self, request: web.Request, username: str) -> web.Response: """Process a request to get the list of available lights.""" + assert request.remote is not None if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTPStatus.UNAUTHORIZED) @@ -217,13 +229,14 @@ class HueFullStateView(HomeAssistantView): name = "emulated_hue:username:state" requires_auth = False - def __init__(self, config): + def __init__(self, config: Config) -> None: """Initialize the instance of the view.""" self.config = config @core.callback - def get(self, request, username): + def get(self, request: web.Request, username: str) -> web.Response: """Process a request to get the list of available lights.""" + assert request.remote is not None if not is_local(ip_address(request.remote)): return self.json_message("only local IPs allowed", HTTPStatus.UNAUTHORIZED) if username != HUE_API_USERNAME: @@ -245,13 +258,14 @@ class HueConfigView(HomeAssistantView): name = "emulated_hue:username:config" requires_auth = False - def __init__(self, config): + def __init__(self, config: Config) -> None: """Initialize the instance of the view.""" self.config = config @core.callback - def get(self, request, username=""): + def get(self, request: web.Request, username: str = "") -> web.Response: """Process a request to get the configuration.""" + assert request.remote is not None if not is_local(ip_address(request.remote)): return self.json_message("only local IPs allowed", HTTPStatus.UNAUTHORIZED) @@ -267,17 +281,18 @@ class HueOneLightStateView(HomeAssistantView): name = "emulated_hue:light:state" requires_auth = False - def __init__(self, config): + def __init__(self, config: Config) -> None: """Initialize the instance of the view.""" self.config = config @core.callback - def get(self, request, username, entity_id): + def get(self, request: web.Request, username: str, entity_id: str) -> web.Response: """Process a request to get the state of an individual light.""" + assert request.remote is not None if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTPStatus.UNAUTHORIZED) - hass = request.app["hass"] + hass: core.HomeAssistant = request.app["hass"] hass_entity_id = self.config.number_to_entity_id(entity_id) if hass_entity_id is None: @@ -307,17 +322,20 @@ class HueOneLightChangeView(HomeAssistantView): name = "emulated_hue:light:state" requires_auth = False - def __init__(self, config): + def __init__(self, config: Config) -> None: """Initialize the instance of the view.""" self.config = config - async def put(self, request, username, entity_number): # noqa: C901 + async def put( # noqa: C901 + self, request: web.Request, username: str, entity_number: str + ) -> web.Response: """Process a request to set the state of an individual light.""" + assert request.remote is not None if not is_local(ip_address(request.remote)): return self.json_message("Only local IPs allowed", HTTPStatus.UNAUTHORIZED) config = self.config - hass = request.app["hass"] + hass: core.HomeAssistant = request.app["hass"] entity_id = config.number_to_entity_id(entity_number) if entity_id is None: @@ -344,7 +362,7 @@ class HueOneLightChangeView(HomeAssistantView): color_modes = entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) # Parse the request - parsed = { + parsed: dict[str, Any] = { STATE_ON: False, STATE_BRIGHTNESS: None, STATE_HUE: None, @@ -416,10 +434,10 @@ class HueOneLightChangeView(HomeAssistantView): turn_on_needed = False # Convert the resulting "on" status into the service we need to call - service = SERVICE_TURN_ON if parsed[STATE_ON] else SERVICE_TURN_OFF + service: str | None = SERVICE_TURN_ON if parsed[STATE_ON] else SERVICE_TURN_OFF # Construct what we need to send to the service - data = {ATTR_ENTITY_ID: entity_id} + data: dict[str, Any] = {ATTR_ENTITY_ID: entity_id} # If the requested entity is a light, set the brightness, hue, # saturation and color temp @@ -596,7 +614,7 @@ class HueOneLightChangeView(HomeAssistantView): return self.json(json_response) -def get_entity_state(config, entity): +def get_entity_state(config: Config, entity: State) -> dict[str, Any]: """Retrieve and convert state and brightness values for an entity.""" cached_state_entry = config.cached_states.get(entity.entity_id, None) cached_state = None @@ -617,7 +635,7 @@ def get_entity_state(config, entity): # Remove the now stale cached entry. config.cached_states.pop(entity.entity_id) - data = { + data: dict[str, Any] = { STATE_ON: False, STATE_BRIGHTNESS: None, STATE_HUE: None, @@ -700,7 +718,7 @@ def get_entity_state(config, entity): return data -def entity_to_json(config, entity): +def entity_to_json(config: Config, entity: State) -> dict[str, Any]: """Convert an entity to its Hue bridge JSON representation.""" entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) color_modes = entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) @@ -709,7 +727,7 @@ def entity_to_json(config, entity): state = get_entity_state(config, entity) - retval = { + retval: dict[str, Any] = { "state": { HUE_API_STATE_ON: state[STATE_ON], "reachable": entity.state != STATE_UNAVAILABLE, @@ -793,13 +811,15 @@ def entity_to_json(config, entity): return retval -def create_hue_success_response(entity_number, attr, value): +def create_hue_success_response( + entity_number: str, attr: str, value: str +) -> dict[str, Any]: """Create a success response for an attribute set on a light.""" success_key = f"/lights/{entity_number}/state/{attr}" return {"success": {success_key: value}} -def create_config_model(config, request): +def create_config_model(config: Config, request: web.Request) -> dict[str, Any]: """Create a config resource.""" return { "mac": "00:00:00:00:00:00", @@ -811,29 +831,29 @@ def create_config_model(config, request): } -def create_list_of_entities(config, request): +def create_list_of_entities(config: Config, request: web.Request) -> dict[str, Any]: """Create a list of all entities.""" - hass = request.app["hass"] - json_response = {} - - for entity in config.filter_exposed_entities(hass.states.async_all()): - number = config.entity_id_to_number(entity.entity_id) - json_response[number] = entity_to_json(config, entity) - + hass: core.HomeAssistant = request.app["hass"] + json_response: dict[str, Any] = { + config.entity_id_to_number(entity.entity_id): entity_to_json(config, entity) + for entity in config.filter_exposed_entities(hass.states.async_all()) + } return json_response -def hue_brightness_to_hass(value): +def hue_brightness_to_hass(value: int) -> int: """Convert hue brightness 1..254 to hass format 0..255.""" return min(255, round((value / HUE_API_STATE_BRI_MAX) * 255)) -def hass_to_hue_brightness(value): +def hass_to_hue_brightness(value: int) -> int: """Convert hass brightness 0..255 to hue 1..254 scale.""" return max(1, round((value / 255) * HUE_API_STATE_BRI_MAX)) -async def wait_for_state_change_or_timeout(hass, entity_id, timeout): +async def wait_for_state_change_or_timeout( + hass: core.HomeAssistant, entity_id: str, timeout: float +) -> None: """Wait for an entity to change state.""" ev = asyncio.Event() diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 93bf8c0631f..024b0f3ddf7 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -1,13 +1,14 @@ """Test the Emulated Hue component.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import AsyncMock, patch -from homeassistant.components.emulated_hue import ( +from homeassistant.components.emulated_hue.config import ( DATA_KEY, DATA_VERSION, SAVE_DELAY, Config, ) +from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component from homeassistant.util import utcnow @@ -121,6 +122,13 @@ async def test_setup_works(hass): """Test setup works.""" hass.config.components.add("network") with patch( - "homeassistant.components.emulated_hue.create_upnp_datagram_endpoint" - ), patch("homeassistant.components.emulated_hue.async_get_source_ip"): + "homeassistant.components.emulated_hue.create_upnp_datagram_endpoint", + AsyncMock(), + ) as mock_create_upnp_datagram_endpoint, patch( + "homeassistant.components.emulated_hue.async_get_source_ip" + ): assert await async_setup_component(hass, "emulated_hue", {}) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert len(mock_create_upnp_datagram_endpoint.mock_calls) == 2 diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index ec04ee7e19c..79daaadbbc9 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -2,6 +2,7 @@ from http import HTTPStatus import json import unittest +from unittest.mock import patch from aiohttp import web import defusedxml.ElementTree as ET @@ -52,11 +53,13 @@ def hue_client(aiohttp_client): async def setup_hue(hass): """Set up the emulated_hue integration.""" - assert await setup.async_setup_component( - hass, - emulated_hue.DOMAIN, - {emulated_hue.DOMAIN: {emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT}}, - ) + with patch("homeassistant.components.emulated_hue.create_upnp_datagram_endpoint"): + assert await setup.async_setup_component( + hass, + emulated_hue.DOMAIN, + {emulated_hue.DOMAIN: {emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT}}, + ) + await hass.async_block_till_done() def test_upnp_discovery_basic(): From 92be8b4f8e9c947e687aff9b60db024c8ceda0d4 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 29 May 2022 12:29:21 -0400 Subject: [PATCH 1012/3516] Make tomorrowio API rate limit handling more robust (#70412) * Use the max request limit when setting tomorrowio update interval * tests * reduce lines * simplify * refactor * Make Coordinator.async_setup_entry more efficient at determining when to refresh data and schedule refresh * clean up * clean up * Remove unnecessary type definition * typo * fix logic ot be more deterministic * Another fix * Comment * Reduce wasted API calls by doing partial updates when new entries get added with a new key * Simplify and use asyncio event so that config entries only load after initial coordinator refresh * Remove commented out piece * Comment * Remove unnecessary variable * More cleanup * Make future merge easier * remove dupe * switch order * add comment * Remove unnecessary error handling * make code easier to read * review feedback for code * Fix logic * Update test based on review * Tweak comments * reset mock so asertions are more clear * Remove update interval check --- .../components/tomorrowio/__init__.py | 243 +++++++++++------- homeassistant/components/tomorrowio/sensor.py | 3 +- .../components/tomorrowio/weather.py | 9 +- tests/components/tomorrowio/conftest.py | 14 +- tests/components/tomorrowio/test_init.py | 69 ++++- 5 files changed, 231 insertions(+), 107 deletions(-) diff --git a/homeassistant/components/tomorrowio/__init__.py b/homeassistant/components/tomorrowio/__init__.py index d8decc1aea3..cef4662d1a4 100644 --- a/homeassistant/components/tomorrowio/__init__.py +++ b/homeassistant/components/tomorrowio/__init__.py @@ -1,6 +1,7 @@ """The Tomorrow.io integration.""" from __future__ import annotations +import asyncio from datetime import timedelta import logging from math import ceil @@ -23,7 +24,6 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, - CONF_NAME, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -40,7 +40,6 @@ from .const import ( CONF_TIMESTEP, DOMAIN, INTEGRATION_NAME, - MAX_REQUESTS_PER_DAY, TMRW_ATTR_CARBON_MONOXIDE, TMRW_ATTR_CHINA_AQI, TMRW_ATTR_CHINA_HEALTH_CONCERN, @@ -85,36 +84,33 @@ PLATFORMS = [SENSOR_DOMAIN, WEATHER_DOMAIN] @callback -def async_set_update_interval( - hass: HomeAssistant, current_entry: ConfigEntry -) -> timedelta: - """Recalculate update_interval based on existing Tomorrow.io instances and update them.""" - api_calls = 2 - # We check how many Tomorrow.io configured instances are using the same API key and - # calculate interval to not exceed allowed numbers of requests. Divide 90% of - # MAX_REQUESTS_PER_DAY by the number of API calls because we want a buffer in the - # number of API calls left at the end of the day. - other_instance_entry_ids = [ - entry.entry_id +def async_get_entries_by_api_key( + hass: HomeAssistant, api_key: str, exclude_entry: ConfigEntry | None = None +) -> list[ConfigEntry]: + """Get all entries for a given API key.""" + return [ + entry for entry in hass.config_entries.async_entries(DOMAIN) - if entry.entry_id != current_entry.entry_id - and entry.data[CONF_API_KEY] == current_entry.data[CONF_API_KEY] + if entry.data[CONF_API_KEY] == api_key + and (exclude_entry is None or exclude_entry != entry) ] - interval = timedelta( - minutes=( - ceil( - (24 * 60 * (len(other_instance_entry_ids) + 1) * api_calls) - / (MAX_REQUESTS_PER_DAY * 0.9) - ) - ) + +@callback +def async_set_update_interval( + hass: HomeAssistant, api: TomorrowioV4, exclude_entry: ConfigEntry | None = None +) -> timedelta: + """Calculate update_interval.""" + # We check how many Tomorrow.io configured instances are using the same API key and + # calculate interval to not exceed allowed numbers of requests. Divide 90% of + # max_requests by the number of API calls because we want a buffer in the + # number of API calls left at the end of the day. + entries = async_get_entries_by_api_key(hass, api.api_key, exclude_entry) + minutes = ceil( + (24 * 60 * len(entries) * api.num_api_requests) + / (api.max_requests_per_day * 0.9) ) - - for entry_id in other_instance_entry_ids: - if entry_id in hass.data[DOMAIN]: - hass.data[DOMAIN][entry_id].update_interval = interval - - return interval + return timedelta(minutes=minutes) @callback @@ -197,24 +193,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry.source == SOURCE_IMPORT and "old_config_entry_id" in entry.data: async_migrate_entry_from_climacell(hass, dev_reg, entry, device) - api = TomorrowioV4( - entry.data[CONF_API_KEY], - entry.data[CONF_LOCATION][CONF_LATITUDE], - entry.data[CONF_LOCATION][CONF_LONGITUDE], - unit_system="metric", - session=async_get_clientsession(hass), - ) + api_key = entry.data[CONF_API_KEY] + # If coordinator already exists for this API key, we'll use that, otherwise + # we have to create a new one + if not (coordinator := hass.data[DOMAIN].get(api_key)): + session = async_get_clientsession(hass) + # we will not use the class's lat and long so we can pass in garbage + # lats and longs + api = TomorrowioV4(api_key, 361.0, 361.0, unit_system="metric", session=session) + coordinator = TomorrowioDataUpdateCoordinator(hass, api) + hass.data[DOMAIN][api_key] = coordinator - coordinator = TomorrowioDataUpdateCoordinator( - hass, - entry, - api, - async_set_update_interval(hass, entry), - ) - - await coordinator.async_config_entry_first_refresh() - - hass.data[DOMAIN][entry.entry_id] = coordinator + await coordinator.async_setup_entry(entry) hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -227,9 +217,13 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> config_entry, PLATFORMS ) - hass.data[DOMAIN].pop(config_entry.entry_id) - if not hass.data[DOMAIN]: - hass.data.pop(DOMAIN) + api_key = config_entry.data[CONF_API_KEY] + coordinator: TomorrowioDataUpdateCoordinator = hass.data[DOMAIN][api_key] + # If this is true, we can remove the coordinator + if await coordinator.async_unload_entry(config_entry): + hass.data[DOMAIN].pop(api_key) + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) return unload_ok @@ -237,44 +231,90 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator): """Define an object to hold Tomorrow.io data.""" - def __init__( - self, - hass: HomeAssistant, - config_entry: ConfigEntry, - api: TomorrowioV4, - update_interval: timedelta, - ) -> None: + def __init__(self, hass: HomeAssistant, api: TomorrowioV4) -> None: """Initialize.""" - - self._config_entry = config_entry self._api = api - self.name = config_entry.data[CONF_NAME] self.data = {CURRENT: {}, FORECASTS: {}} + self.entry_id_to_location_dict: dict[str, str] = {} + self._coordinator_ready: asyncio.Event | None = None - super().__init__( - hass, - _LOGGER, - name=config_entry.data[CONF_NAME], - update_interval=update_interval, - ) + super().__init__(hass, _LOGGER, name=f"{DOMAIN}_{self._api.api_key}") + + def add_entry_to_location_dict(self, entry: ConfigEntry) -> None: + """Add an entry to the location dict.""" + latitude = entry.data[CONF_LOCATION][CONF_LATITUDE] + longitude = entry.data[CONF_LOCATION][CONF_LONGITUDE] + self.entry_id_to_location_dict[entry.entry_id] = f"{latitude},{longitude}" + + async def async_setup_entry(self, entry: ConfigEntry) -> None: + """Load config entry into coordinator.""" + # If we haven't loaded any data yet, register all entries with this API key and + # get the initial data for all of them. We do this because another config entry + # may start setup before we finish setting the initial data and we don't want + # to do multiple refreshes on startup. + if self._coordinator_ready is None: + self._coordinator_ready = asyncio.Event() + for entry_ in async_get_entries_by_api_key(self.hass, self._api.api_key): + self.add_entry_to_location_dict(entry_) + await self.async_config_entry_first_refresh() + self._coordinator_ready.set() + else: + # If we have an event, we need to wait for it to be set before we proceed + await self._coordinator_ready.wait() + # If we're not getting new data because we already know this entry, we + # don't need to schedule a refresh + if entry.entry_id in self.entry_id_to_location_dict: + return + # We need a refresh, but it's going to be a partial refresh so we can + # minimize repeat API calls + self.add_entry_to_location_dict(entry) + await self.async_refresh() + + self.update_interval = async_set_update_interval(self.hass, self._api) + self._schedule_refresh() + + async def async_unload_entry(self, entry: ConfigEntry) -> bool | None: + """ + Unload a config entry from coordinator. + + Returns whether coordinator can be removed as well because there are no + config entries tied to it anymore. + """ + self.entry_id_to_location_dict.pop(entry.entry_id) + self.update_interval = async_set_update_interval(self.hass, self._api, entry) + return not self.entry_id_to_location_dict async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" - try: - return await self._api.realtime_and_all_forecasts( - [ - TMRW_ATTR_TEMPERATURE, - TMRW_ATTR_HUMIDITY, - TMRW_ATTR_PRESSURE, - TMRW_ATTR_WIND_SPEED, - TMRW_ATTR_WIND_DIRECTION, - TMRW_ATTR_CONDITION, - TMRW_ATTR_VISIBILITY, - TMRW_ATTR_OZONE, - TMRW_ATTR_WIND_GUST, - TMRW_ATTR_CLOUD_COVER, - TMRW_ATTR_PRECIPITATION_TYPE, - *( + data = {} + # If we are refreshing because of a new config entry that's not already in our + # data, we do a partial refresh to avoid wasted API calls. + if self.data and any( + entry_id not in self.data for entry_id in self.entry_id_to_location_dict + ): + data = self.data + + for entry_id, location in self.entry_id_to_location_dict.items(): + if entry_id in data: + continue + entry = self.hass.config_entries.async_get_entry(entry_id) + assert entry + try: + data[entry_id] = await self._api.realtime_and_all_forecasts( + [ + # Weather + TMRW_ATTR_TEMPERATURE, + TMRW_ATTR_HUMIDITY, + TMRW_ATTR_PRESSURE, + TMRW_ATTR_WIND_SPEED, + TMRW_ATTR_WIND_DIRECTION, + TMRW_ATTR_CONDITION, + TMRW_ATTR_VISIBILITY, + TMRW_ATTR_OZONE, + TMRW_ATTR_WIND_GUST, + TMRW_ATTR_CLOUD_COVER, + TMRW_ATTR_PRECIPITATION_TYPE, + # Sensors TMRW_ATTR_CARBON_MONOXIDE, TMRW_ATTR_CHINA_AQI, TMRW_ATTR_CHINA_HEALTH_CONCERN, @@ -300,26 +340,28 @@ class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator): TMRW_ATTR_SOLAR_GHI, TMRW_ATTR_SULPHUR_DIOXIDE, TMRW_ATTR_WIND_GUST, - ), - ], - [ - TMRW_ATTR_TEMPERATURE_LOW, - TMRW_ATTR_TEMPERATURE_HIGH, - TMRW_ATTR_WIND_SPEED, - TMRW_ATTR_WIND_DIRECTION, - TMRW_ATTR_CONDITION, - TMRW_ATTR_PRECIPITATION, - TMRW_ATTR_PRECIPITATION_PROBABILITY, - ], - nowcast_timestep=self._config_entry.options[CONF_TIMESTEP], - ) - except ( - CantConnectException, - InvalidAPIKeyException, - RateLimitedException, - UnknownException, - ) as error: - raise UpdateFailed from error + ], + [ + TMRW_ATTR_TEMPERATURE_LOW, + TMRW_ATTR_TEMPERATURE_HIGH, + TMRW_ATTR_WIND_SPEED, + TMRW_ATTR_WIND_DIRECTION, + TMRW_ATTR_CONDITION, + TMRW_ATTR_PRECIPITATION, + TMRW_ATTR_PRECIPITATION_PROBABILITY, + ], + nowcast_timestep=entry.options[CONF_TIMESTEP], + location=location, + ) + except ( + CantConnectException, + InvalidAPIKeyException, + RateLimitedException, + UnknownException, + ) as error: + raise UpdateFailed from error + + return data class TomorrowioEntity(CoordinatorEntity[TomorrowioDataUpdateCoordinator]): @@ -349,7 +391,8 @@ class TomorrowioEntity(CoordinatorEntity[TomorrowioDataUpdateCoordinator]): Used for V4 API. """ - return self.coordinator.data.get(CURRENT, {}).get(property_name) + entry_id = self._config_entry.entry_id + return self.coordinator.data[entry_id].get(CURRENT, {}).get(property_name) @property def attribution(self): diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py index d221922df54..ed4ae915c1c 100644 --- a/homeassistant/components/tomorrowio/sensor.py +++ b/homeassistant/components/tomorrowio/sensor.py @@ -23,6 +23,7 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, + CONF_API_KEY, CONF_NAME, IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, IRRADIATION_WATTS_PER_SQUARE_METER, @@ -286,7 +287,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = hass.data[DOMAIN][config_entry.data[CONF_API_KEY]] entities = [ TomorrowioSensorEntity(hass, config_entry, coordinator, 4, description) for description in SENSOR_TYPES diff --git a/homeassistant/components/tomorrowio/weather.py b/homeassistant/components/tomorrowio/weather.py index bf687f8bdca..bde6e6b996b 100644 --- a/homeassistant/components/tomorrowio/weather.py +++ b/homeassistant/components/tomorrowio/weather.py @@ -19,6 +19,7 @@ from homeassistant.components.weather import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONF_API_KEY, CONF_NAME, LENGTH_KILOMETERS, LENGTH_MILLIMETERS, @@ -61,7 +62,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator = hass.data[DOMAIN][config_entry.data[CONF_API_KEY]] entities = [ TomorrowioWeatherEntity(config_entry, coordinator, 4, forecast_type) @@ -190,7 +191,11 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): def forecast(self): """Return the forecast.""" # Check if forecasts are available - raw_forecasts = self.coordinator.data.get(FORECASTS, {}).get(self.forecast_type) + raw_forecasts = ( + self.coordinator.data.get(self._config_entry.entry_id, {}) + .get(FORECASTS, {}) + .get(self.forecast_type) + ) if not raw_forecasts: return None diff --git a/tests/components/tomorrowio/conftest.py b/tests/components/tomorrowio/conftest.py index 65c69209f0e..9c1fa5baa06 100644 --- a/tests/components/tomorrowio/conftest.py +++ b/tests/components/tomorrowio/conftest.py @@ -1,6 +1,6 @@ """Configure py.test.""" import json -from unittest.mock import patch +from unittest.mock import PropertyMock, patch import pytest @@ -23,8 +23,16 @@ def tomorrowio_config_entry_update_fixture(): with patch( "homeassistant.components.tomorrowio.TomorrowioV4.realtime_and_all_forecasts", return_value=json.loads(load_fixture("v4.json", "tomorrowio")), - ): - yield + ) as mock_update, patch( + "homeassistant.components.tomorrowio.TomorrowioV4.max_requests_per_day", + new_callable=PropertyMock, + ) as mock_max_requests_per_day, patch( + "homeassistant.components.tomorrowio.TomorrowioV4.num_api_requests", + new_callable=PropertyMock, + ) as mock_num_api_requests: + mock_max_requests_per_day.return_value = 100 + mock_num_api_requests.return_value = 2 + yield mock_update @pytest.fixture(name="climacell_config_entry_update") diff --git a/tests/components/tomorrowio/test_init.py b/tests/components/tomorrowio/test_init.py index c9914bf95be..27372094092 100644 --- a/tests/components/tomorrowio/test_init.py +++ b/tests/components/tomorrowio/test_init.py @@ -1,4 +1,7 @@ """Tests for Tomorrow.io init.""" +from datetime import timedelta +from unittest.mock import patch + from homeassistant.components.climacell.const import CONF_TIMESTEP, DOMAIN as CC_DOMAIN from homeassistant.components.tomorrowio.config_flow import ( _get_config_schema, @@ -17,10 +20,11 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util import dt as dt_util from .const import MIN_CONFIG -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed from tests.components.climacell.const import API_V3_ENTRY_DATA NEW_NAME = "New Name" @@ -47,6 +51,69 @@ async def test_load_and_unload(hass: HomeAssistant) -> None: assert len(hass.states.async_entity_ids(WEATHER_DOMAIN)) == 0 +async def test_update_intervals( + hass: HomeAssistant, tomorrowio_config_entry_update +) -> None: + """Test coordinator update intervals.""" + now = dt_util.utcnow() + data = _get_config_schema(hass, SOURCE_USER)(MIN_CONFIG) + data[CONF_NAME] = "test" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=data, + options={CONF_TIMESTEP: 1}, + unique_id=_get_unique_id(hass, data), + version=1, + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(tomorrowio_config_entry_update.call_args_list) == 1 + tomorrowio_config_entry_update.reset_mock() + + # Before the update interval, no updates yet + async_fire_time_changed(hass, now + timedelta(minutes=30)) + await hass.async_block_till_done() + assert len(tomorrowio_config_entry_update.call_args_list) == 0 + + # On the update interval, we get a new update + async_fire_time_changed(hass, now + timedelta(minutes=32)) + await hass.async_block_till_done() + assert len(tomorrowio_config_entry_update.call_args_list) == 1 + tomorrowio_config_entry_update.reset_mock() + + with patch( + "homeassistant.helpers.update_coordinator.utcnow", + return_value=now + timedelta(minutes=32), + ): + # Adding a second config entry should cause the update interval to double + config_entry_2 = MockConfigEntry( + domain=DOMAIN, + data=data, + options={CONF_TIMESTEP: 1}, + unique_id=f"{_get_unique_id(hass, data)}_1", + version=1, + ) + config_entry_2.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry_2.entry_id) + await hass.async_block_till_done() + assert config_entry.data[CONF_API_KEY] == config_entry_2.data[CONF_API_KEY] + # We should get an immediate call once the new config entry is setup for a + # partial update + assert len(tomorrowio_config_entry_update.call_args_list) == 1 + tomorrowio_config_entry_update.reset_mock() + + # We should get no new calls on our old interval + async_fire_time_changed(hass, now + timedelta(minutes=64)) + await hass.async_block_till_done() + assert len(tomorrowio_config_entry_update.call_args_list) == 0 + + # We should get two calls on our new interval, one for each entry + async_fire_time_changed(hass, now + timedelta(minutes=96)) + await hass.async_block_till_done() + assert len(tomorrowio_config_entry_update.call_args_list) == 2 + + async def test_climacell_migration_logic( hass: HomeAssistant, climacell_config_entry_update ) -> None: From 5031c3c8b46ee7d01d696fd23c70b0c2cd7c97b4 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 29 May 2022 12:30:00 -0400 Subject: [PATCH 1013/3516] Fix zwave_js custom trigger validation bug (#72656) * Fix zwave_js custom trigger validation bug * update comments * Switch to ValueError * Switch to ValueError --- .../components/zwave_js/triggers/event.py | 36 +-- .../zwave_js/triggers/value_updated.py | 12 +- tests/components/zwave_js/test_trigger.py | 217 ++++++++++++++++++ 3 files changed, 241 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py index 17bb52fb392..784ae74777b 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -8,7 +8,7 @@ import voluptuous as vol from zwave_js_server.client import Client from zwave_js_server.model.controller import CONTROLLER_EVENT_MODEL_MAP from zwave_js_server.model.driver import DRIVER_EVENT_MODEL_MAP -from zwave_js_server.model.node import NODE_EVENT_MODEL_MAP, Node +from zwave_js_server.model.node import NODE_EVENT_MODEL_MAP from homeassistant.components.automation import ( AutomationActionType, @@ -20,7 +20,6 @@ from homeassistant.components.zwave_js.const import ( ATTR_EVENT_DATA, ATTR_EVENT_SOURCE, ATTR_NODE_ID, - ATTR_NODES, ATTR_PARTIAL_DICT_MATCH, DATA_CLIENT, DOMAIN, @@ -116,22 +115,20 @@ async def async_validate_trigger_config( """Validate config.""" config = TRIGGER_SCHEMA(config) + if ATTR_CONFIG_ENTRY_ID in config: + entry_id = config[ATTR_CONFIG_ENTRY_ID] + if hass.config_entries.async_get_entry(entry_id) is None: + raise vol.Invalid(f"Config entry '{entry_id}' not found") + if async_bypass_dynamic_config_validation(hass, config): return config - if config[ATTR_EVENT_SOURCE] == "node": - config[ATTR_NODES] = async_get_nodes_from_targets(hass, config) - if not config[ATTR_NODES]: - raise vol.Invalid( - f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s." - ) - - if ATTR_CONFIG_ENTRY_ID not in config: - return config - - entry_id = config[ATTR_CONFIG_ENTRY_ID] - if hass.config_entries.async_get_entry(entry_id) is None: - raise vol.Invalid(f"Config entry '{entry_id}' not found") + if config[ATTR_EVENT_SOURCE] == "node" and not async_get_nodes_from_targets( + hass, config + ): + raise vol.Invalid( + f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s." + ) return config @@ -145,7 +142,12 @@ async def async_attach_trigger( platform_type: str = PLATFORM_TYPE, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - nodes: set[Node] = config.get(ATTR_NODES, {}) + dev_reg = dr.async_get(hass) + nodes = async_get_nodes_from_targets(hass, config, dev_reg=dev_reg) + if config[ATTR_EVENT_SOURCE] == "node" and not nodes: + raise ValueError( + f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s." + ) event_source = config[ATTR_EVENT_SOURCE] event_name = config[ATTR_EVENT] @@ -200,8 +202,6 @@ async def async_attach_trigger( hass.async_run_hass_job(job, {"trigger": payload}) - dev_reg = dr.async_get(hass) - if not nodes: entry_id = config[ATTR_CONFIG_ENTRY_ID] client: Client = hass.data[DOMAIN][entry_id][DATA_CLIENT] diff --git a/homeassistant/components/zwave_js/triggers/value_updated.py b/homeassistant/components/zwave_js/triggers/value_updated.py index 4f15b87a6db..29b4b4d06d6 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -5,7 +5,6 @@ import functools import voluptuous as vol from zwave_js_server.const import CommandClass -from zwave_js_server.model.node import Node from zwave_js_server.model.value import Value, get_value_id from homeassistant.components.automation import ( @@ -20,7 +19,6 @@ from homeassistant.components.zwave_js.const import ( ATTR_CURRENT_VALUE_RAW, ATTR_ENDPOINT, ATTR_NODE_ID, - ATTR_NODES, ATTR_PREVIOUS_VALUE, ATTR_PREVIOUS_VALUE_RAW, ATTR_PROPERTY, @@ -79,8 +77,7 @@ async def async_validate_trigger_config( if async_bypass_dynamic_config_validation(hass, config): return config - config[ATTR_NODES] = async_get_nodes_from_targets(hass, config) - if not config[ATTR_NODES]: + if not async_get_nodes_from_targets(hass, config): raise vol.Invalid( f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s." ) @@ -96,7 +93,11 @@ async def async_attach_trigger( platform_type: str = PLATFORM_TYPE, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - nodes: set[Node] = config[ATTR_NODES] + dev_reg = dr.async_get(hass) + if not (nodes := async_get_nodes_from_targets(hass, config, dev_reg=dev_reg)): + raise ValueError( + f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s." + ) from_value = config[ATTR_FROM] to_value = config[ATTR_TO] @@ -163,7 +164,6 @@ async def async_attach_trigger( hass.async_run_hass_job(job, {"trigger": payload}) - dev_reg = dr.async_get(hass) for node in nodes: driver = node.client.driver assert driver is not None # The node comes from the driver. diff --git a/tests/components/zwave_js/test_trigger.py b/tests/components/zwave_js/test_trigger.py index 9758f566d81..48439eede0f 100644 --- a/tests/components/zwave_js/test_trigger.py +++ b/tests/components/zwave_js/test_trigger.py @@ -269,6 +269,122 @@ async def test_zwave_js_value_updated(hass, client, lock_schlage_be469, integrat await hass.services.async_call(automation.DOMAIN, SERVICE_RELOAD, blocking=True) +async def test_zwave_js_value_updated_bypass_dynamic_validation( + hass, client, lock_schlage_be469, integration +): + """Test zwave_js.value_updated trigger when bypassing dynamic validation.""" + trigger_type = f"{DOMAIN}.value_updated" + node: Node = lock_schlage_be469 + + no_value_filter = async_capture_events(hass, "no_value_filter") + + with patch( + "homeassistant.components.zwave_js.triggers.value_updated.async_bypass_dynamic_config_validation", + return_value=True, + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + # no value filter + { + "trigger": { + "platform": trigger_type, + "entity_id": SCHLAGE_BE469_LOCK_ENTITY, + "command_class": CommandClass.DOOR_LOCK.value, + "property": "latchStatus", + }, + "action": { + "event": "no_value_filter", + }, + }, + ] + }, + ) + + # Test that no value filter is triggered + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "latchStatus", + "newValue": "boo", + "prevValue": "hiss", + "propertyName": "latchStatus", + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + + assert len(no_value_filter) == 1 + + +async def test_zwave_js_value_updated_bypass_dynamic_validation_no_nodes( + hass, client, lock_schlage_be469, integration +): + """Test value_updated trigger when bypassing dynamic validation with no nodes.""" + trigger_type = f"{DOMAIN}.value_updated" + node: Node = lock_schlage_be469 + + no_value_filter = async_capture_events(hass, "no_value_filter") + + with patch( + "homeassistant.components.zwave_js.triggers.value_updated.async_bypass_dynamic_config_validation", + return_value=True, + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + # no value filter + { + "trigger": { + "platform": trigger_type, + "entity_id": "sensor.test", + "command_class": CommandClass.DOOR_LOCK.value, + "property": "latchStatus", + }, + "action": { + "event": "no_value_filter", + }, + }, + ] + }, + ) + + # Test that no value filter is NOT triggered because automation failed setup + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "latchStatus", + "newValue": "boo", + "prevValue": "hiss", + "propertyName": "latchStatus", + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + + assert len(no_value_filter) == 0 + + async def test_zwave_js_event(hass, client, lock_schlage_be469, integration): """Test for zwave_js.event automation trigger.""" trigger_type = f"{DOMAIN}.event" @@ -644,6 +760,107 @@ async def test_zwave_js_event(hass, client, lock_schlage_be469, integration): await hass.services.async_call(automation.DOMAIN, SERVICE_RELOAD, blocking=True) +async def test_zwave_js_event_bypass_dynamic_validation( + hass, client, lock_schlage_be469, integration +): + """Test zwave_js.event trigger when bypassing dynamic config validation.""" + trigger_type = f"{DOMAIN}.event" + node: Node = lock_schlage_be469 + + node_no_event_data_filter = async_capture_events(hass, "node_no_event_data_filter") + + with patch( + "homeassistant.components.zwave_js.triggers.event.async_bypass_dynamic_config_validation", + return_value=True, + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + # node filter: no event data + { + "trigger": { + "platform": trigger_type, + "entity_id": SCHLAGE_BE469_LOCK_ENTITY, + "event_source": "node", + "event": "interview stage completed", + }, + "action": { + "event": "node_no_event_data_filter", + }, + }, + ] + }, + ) + + # Test that `node no event data filter` is triggered and `node event data filter` is not + event = Event( + type="interview stage completed", + data={ + "source": "node", + "event": "interview stage completed", + "stageName": "NodeInfo", + "nodeId": node.node_id, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + + assert len(node_no_event_data_filter) == 1 + + +async def test_zwave_js_event_bypass_dynamic_validation_no_nodes( + hass, client, lock_schlage_be469, integration +): + """Test event trigger when bypassing dynamic validation with no nodes.""" + trigger_type = f"{DOMAIN}.event" + node: Node = lock_schlage_be469 + + node_no_event_data_filter = async_capture_events(hass, "node_no_event_data_filter") + + with patch( + "homeassistant.components.zwave_js.triggers.event.async_bypass_dynamic_config_validation", + return_value=True, + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + # node filter: no event data + { + "trigger": { + "platform": trigger_type, + "entity_id": "sensor.fake", + "event_source": "node", + "event": "interview stage completed", + }, + "action": { + "event": "node_no_event_data_filter", + }, + }, + ] + }, + ) + + # Test that `node no event data filter` is NOT triggered because automation failed + # setup + event = Event( + type="interview stage completed", + data={ + "source": "node", + "event": "interview stage completed", + "stageName": "NodeInfo", + "nodeId": node.node_id, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + + assert len(node_no_event_data_filter) == 0 + + async def test_zwave_js_event_invalid_config_entry_id( hass, client, integration, caplog ): From 1d57626ff022fb2017a0e23ff53deca38c9583f2 Mon Sep 17 00:00:00 2001 From: Shawn Saenger Date: Sun, 29 May 2022 10:33:33 -0600 Subject: [PATCH 1014/3516] Incorporate various improvements for the ws66i integration (#71717) * Improve readability and remove unused code * Remove ws66i custom services. Scenes can be used instead. * Unmute WS66i Zone when volume changes * Raise CannotConnect instead of ConnectionError in validation method * Move _verify_connection() method to module level --- homeassistant/components/ws66i/__init__.py | 5 +- homeassistant/components/ws66i/config_flow.py | 48 +- homeassistant/components/ws66i/const.py | 6 +- homeassistant/components/ws66i/coordinator.py | 11 +- .../components/ws66i/media_player.py | 67 +-- homeassistant/components/ws66i/models.py | 2 - homeassistant/components/ws66i/services.yaml | 15 - homeassistant/components/ws66i/strings.json | 3 - .../components/ws66i/translations/en.json | 3 - tests/components/ws66i/test_config_flow.py | 6 +- tests/components/ws66i/test_init.py | 80 ++++ tests/components/ws66i/test_media_player.py | 414 +++++------------- 12 files changed, 251 insertions(+), 409 deletions(-) delete mode 100644 homeassistant/components/ws66i/services.yaml create mode 100644 tests/components/ws66i/test_init.py diff --git a/homeassistant/components/ws66i/__init__.py b/homeassistant/components/ws66i/__init__.py index 232c4390f19..dea1b470b9e 100644 --- a/homeassistant/components/ws66i/__init__.py +++ b/homeassistant/components/ws66i/__init__.py @@ -94,8 +94,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: zones=zones, ) + @callback def shutdown(event): - """Close the WS66i connection to the amplifier and save snapshots.""" + """Close the WS66i connection to the amplifier.""" ws66i.close() entry.async_on_unload(entry.add_update_listener(_update_listener)) @@ -119,6 +120,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/ws66i/config_flow.py b/homeassistant/components/ws66i/config_flow.py index a8f098faadd..b84872da036 100644 --- a/homeassistant/components/ws66i/config_flow.py +++ b/homeassistant/components/ws66i/config_flow.py @@ -1,5 +1,6 @@ """Config flow for WS66i 6-Zone Amplifier integration.""" import logging +from typing import Any from pyws66i import WS66i, get_ws66i import voluptuous as vol @@ -50,22 +51,34 @@ def _sources_from_config(data): } -async def validate_input(hass: core.HomeAssistant, input_data): - """Validate the user input allows us to connect. +def _verify_connection(ws66i: WS66i) -> bool: + """Verify a connection can be made to the WS66i.""" + try: + ws66i.open() + except ConnectionError as err: + raise CannotConnect from err + + # Connection successful. Verify correct port was opened + # Test on FIRST_ZONE because this zone will always be valid + ret_val = ws66i.zone_status(FIRST_ZONE) + + ws66i.close() + + return bool(ret_val) + + +async def validate_input( + hass: core.HomeAssistant, input_data: dict[str, Any] +) -> dict[str, Any]: + """Validate the user input. Data has the keys from DATA_SCHEMA with values provided by the user. """ ws66i: WS66i = get_ws66i(input_data[CONF_IP_ADDRESS]) - await hass.async_add_executor_job(ws66i.open) - # No exception. run a simple test to make sure we opened correct port - # Test on FIRST_ZONE because this zone will always be valid - ret_val = await hass.async_add_executor_job(ws66i.zone_status, FIRST_ZONE) - if ret_val is None: - ws66i.close() - raise ConnectionError("Not a valid WS66i connection") - # Validation done. No issues. Close the connection - ws66i.close() + is_valid: bool = await hass.async_add_executor_job(_verify_connection, ws66i) + if not is_valid: + raise CannotConnect("Not a valid WS66i connection") # Return info that you want to store in the config entry. return {CONF_IP_ADDRESS: input_data[CONF_IP_ADDRESS]} @@ -82,17 +95,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: try: info = await validate_input(self.hass, user_input) - # Data is valid. Add default values for options flow. + except CannotConnect: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + # Data is valid. Create a config entry. return self.async_create_entry( title="WS66i Amp", data=info, options={CONF_SOURCES: INIT_OPTIONS_DEFAULT}, ) - except ConnectionError: - errors["base"] = "cannot_connect" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors diff --git a/homeassistant/components/ws66i/const.py b/homeassistant/components/ws66i/const.py index ec4439a690d..f824d991c1d 100644 --- a/homeassistant/components/ws66i/const.py +++ b/homeassistant/components/ws66i/const.py @@ -1,4 +1,5 @@ """Constants for the Soundavo WS66i 6-Zone Amplifier Media Player component.""" +from datetime import timedelta DOMAIN = "ws66i" @@ -20,5 +21,6 @@ INIT_OPTIONS_DEFAULT = { "6": "Source 6", } -SERVICE_SNAPSHOT = "snapshot" -SERVICE_RESTORE = "restore" +POLL_INTERVAL = timedelta(seconds=30) + +MAX_VOL = 38 diff --git a/homeassistant/components/ws66i/coordinator.py b/homeassistant/components/ws66i/coordinator.py index a9a274756b5..be8ae3aad38 100644 --- a/homeassistant/components/ws66i/coordinator.py +++ b/homeassistant/components/ws66i/coordinator.py @@ -1,7 +1,6 @@ """Coordinator for WS66i.""" from __future__ import annotations -from datetime import timedelta import logging from pyws66i import WS66i, ZoneStatus @@ -9,12 +8,12 @@ from pyws66i import WS66i, ZoneStatus from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from .const import POLL_INTERVAL + _LOGGER = logging.getLogger(__name__) -POLL_INTERVAL = timedelta(seconds=30) - -class Ws66iDataUpdateCoordinator(DataUpdateCoordinator): +class Ws66iDataUpdateCoordinator(DataUpdateCoordinator[list[ZoneStatus]]): """DataUpdateCoordinator to gather data for WS66i Zones.""" def __init__( @@ -43,11 +42,9 @@ class Ws66iDataUpdateCoordinator(DataUpdateCoordinator): data.append(data_zone) - # HA will call my entity's _handle_coordinator_update() return data async def _async_update_data(self) -> list[ZoneStatus]: """Fetch data for each of the zones.""" - # HA will call my entity's _handle_coordinator_update() - # The data I pass back here can be accessed through coordinator.data. + # The data that is returned here can be accessed through coordinator.data. return await self.hass.async_add_executor_job(self._update_all_zones) diff --git a/homeassistant/components/ws66i/media_player.py b/homeassistant/components/ws66i/media_player.py index c0e62fe773c..7cd897e9c1a 100644 --- a/homeassistant/components/ws66i/media_player.py +++ b/homeassistant/components/ws66i/media_player.py @@ -1,6 +1,4 @@ """Support for interfacing with WS66i 6 zone home audio controller.""" -from copy import deepcopy - from pyws66i import WS66i, ZoneStatus from homeassistant.components.media_player import ( @@ -10,22 +8,16 @@ from homeassistant.components.media_player import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.entity_platform import ( - AddEntitiesCallback, - async_get_current_platform, -) +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT +from .const import DOMAIN, MAX_VOL from .coordinator import Ws66iDataUpdateCoordinator from .models import Ws66iData PARALLEL_UPDATES = 1 -MAX_VOL = 38 - async def async_setup_entry( hass: HomeAssistant, @@ -48,23 +40,8 @@ async def async_setup_entry( for idx, zone_id in enumerate(ws66i_data.zones) ) - # Set up services - platform = async_get_current_platform() - platform.async_register_entity_service( - SERVICE_SNAPSHOT, - {}, - "snapshot", - ) - - platform.async_register_entity_service( - SERVICE_RESTORE, - {}, - "async_restore", - ) - - -class Ws66iZone(CoordinatorEntity, MediaPlayerEntity): +class Ws66iZone(CoordinatorEntity[Ws66iDataUpdateCoordinator], MediaPlayerEntity): """Representation of a WS66i amplifier zone.""" def __init__( @@ -82,8 +59,6 @@ class Ws66iZone(CoordinatorEntity, MediaPlayerEntity): self._ws66i_data: Ws66iData = ws66i_data self._zone_id: int = zone_id self._zone_id_idx: int = data_idx - self._coordinator = coordinator - self._snapshot: ZoneStatus = None self._status: ZoneStatus = coordinator.data[data_idx] self._attr_source_list = ws66i_data.sources.name_list self._attr_unique_id = f"{entry_id}_{self._zone_id}" @@ -131,20 +106,6 @@ class Ws66iZone(CoordinatorEntity, MediaPlayerEntity): self._set_attrs_from_status() self.async_write_ha_state() - @callback - def snapshot(self): - """Save zone's current state.""" - self._snapshot = deepcopy(self._status) - - async def async_restore(self): - """Restore saved state.""" - if not self._snapshot: - raise HomeAssistantError("There is no snapshot to restore") - - await self.hass.async_add_executor_job(self._ws66i.restore_zone, self._snapshot) - self._status = self._snapshot - self._async_update_attrs_write_ha_state() - async def async_select_source(self, source): """Set input source.""" idx = self._ws66i_data.sources.name_id[source] @@ -180,24 +141,30 @@ class Ws66iZone(CoordinatorEntity, MediaPlayerEntity): async def async_set_volume_level(self, volume): """Set volume level, range 0..1.""" - await self.hass.async_add_executor_job( - self._ws66i.set_volume, self._zone_id, int(volume * MAX_VOL) - ) - self._status.volume = int(volume * MAX_VOL) + await self.hass.async_add_executor_job(self._set_volume, int(volume * MAX_VOL)) self._async_update_attrs_write_ha_state() async def async_volume_up(self): """Volume up the media player.""" await self.hass.async_add_executor_job( - self._ws66i.set_volume, self._zone_id, min(self._status.volume + 1, MAX_VOL) + self._set_volume, min(self._status.volume + 1, MAX_VOL) ) - self._status.volume = min(self._status.volume + 1, MAX_VOL) self._async_update_attrs_write_ha_state() async def async_volume_down(self): """Volume down media player.""" await self.hass.async_add_executor_job( - self._ws66i.set_volume, self._zone_id, max(self._status.volume - 1, 0) + self._set_volume, max(self._status.volume - 1, 0) ) - self._status.volume = max(self._status.volume - 1, 0) self._async_update_attrs_write_ha_state() + + def _set_volume(self, volume: int) -> None: + """Set the volume of the media player.""" + # Can't set a new volume level when this zone is muted. + # Follow behavior of keypads, where zone is unmuted when volume changes. + if self._status.mute: + self._ws66i.set_mute(self._zone_id, False) + self._status.mute = False + + self._ws66i.set_volume(self._zone_id, volume) + self._status.volume = volume diff --git a/homeassistant/components/ws66i/models.py b/homeassistant/components/ws66i/models.py index d84ee56a4a1..84f481b9a4a 100644 --- a/homeassistant/components/ws66i/models.py +++ b/homeassistant/components/ws66i/models.py @@ -7,8 +7,6 @@ from pyws66i import WS66i from .coordinator import Ws66iDataUpdateCoordinator -# A dataclass is basically a struct in C/C++ - @dataclass class SourceRep: diff --git a/homeassistant/components/ws66i/services.yaml b/homeassistant/components/ws66i/services.yaml deleted file mode 100644 index cedd1d3546a..00000000000 --- a/homeassistant/components/ws66i/services.yaml +++ /dev/null @@ -1,15 +0,0 @@ -snapshot: - name: Snapshot - description: Take a snapshot of the media player zone. - target: - entity: - integration: ws66i - domain: media_player - -restore: - name: Restore - description: Restore a snapshot of the media player zone. - target: - entity: - integration: ws66i - domain: media_player diff --git a/homeassistant/components/ws66i/strings.json b/homeassistant/components/ws66i/strings.json index fcfa64d7e22..ec5bc621a89 100644 --- a/homeassistant/components/ws66i/strings.json +++ b/homeassistant/components/ws66i/strings.json @@ -11,9 +11,6 @@ "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%]" } }, "options": { diff --git a/homeassistant/components/ws66i/translations/en.json b/homeassistant/components/ws66i/translations/en.json index 30ef1e4205a..fd4b170b378 100644 --- a/homeassistant/components/ws66i/translations/en.json +++ b/homeassistant/components/ws66i/translations/en.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Device is already configured" - }, "error": { "cannot_connect": "Failed to connect", "unknown": "Unexpected error" diff --git a/tests/components/ws66i/test_config_flow.py b/tests/components/ws66i/test_config_flow.py index d426e62c012..4fe3554941d 100644 --- a/tests/components/ws66i/test_config_flow.py +++ b/tests/components/ws66i/test_config_flow.py @@ -1,7 +1,7 @@ """Test the WS66i 6-Zone Amplifier 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.ws66i.const import ( CONF_SOURCE_1, CONF_SOURCE_2, @@ -15,15 +15,15 @@ from homeassistant.components.ws66i.const import ( ) from homeassistant.const import CONF_IP_ADDRESS +from .test_media_player import AttrDict + from tests.common import MockConfigEntry -from tests.components.ws66i.test_media_player import AttrDict CONFIG = {CONF_IP_ADDRESS: "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/ws66i/test_init.py b/tests/components/ws66i/test_init.py new file mode 100644 index 00000000000..557c53e97aa --- /dev/null +++ b/tests/components/ws66i/test_init.py @@ -0,0 +1,80 @@ +"""Test the WS66i 6-Zone Amplifier init file.""" +from unittest.mock import patch + +from homeassistant.components.ws66i.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState + +from .test_media_player import ( + MOCK_CONFIG, + MOCK_DEFAULT_OPTIONS, + MOCK_OPTIONS, + MockWs66i, +) + +from tests.common import MockConfigEntry + +ZONE_1_ID = "media_player.zone_11" + + +async def test_cannot_connect(hass): + """Test connection error.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.ws66i.get_ws66i", + new=lambda *a: MockWs66i(fail_open=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 hass.states.get(ZONE_1_ID) is None + + +async def test_cannot_connect_2(hass): + """Test connection error pt 2.""" + # Another way to test same case as test_cannot_connect + ws66i = MockWs66i() + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_DEFAULT_OPTIONS + ) + config_entry.add_to_hass(hass) + + with patch.object(MockWs66i, "open", side_effect=ConnectionError): + with patch( + "homeassistant.components.ws66i.get_ws66i", + new=lambda *a: ws66i, + ): + 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 hass.states.get(ZONE_1_ID) is None + + +async def test_unload_config_entry(hass): + """Test unloading config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.ws66i.get_ws66i", + new=lambda *a: MockWs66i(), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN][config_entry.entry_id] + + with patch.object(MockWs66i, "close") as method_call: + await config_entry.async_unload(hass) + await hass.async_block_till_done() + + assert method_call.called + + assert not hass.data[DOMAIN] diff --git a/tests/components/ws66i/test_media_player.py b/tests/components/ws66i/test_media_player.py index 6fc1e00d827..fbe6a7b2782 100644 --- a/tests/components/ws66i/test_media_player.py +++ b/tests/components/ws66i/test_media_player.py @@ -2,28 +2,22 @@ from collections import defaultdict from unittest.mock import patch -import pytest - +from homeassistant.components.media_player import MediaPlayerEntityFeature from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_VOLUME_LEVEL, DOMAIN as MEDIA_PLAYER_DOMAIN, SERVICE_SELECT_SOURCE, - SUPPORT_SELECT_SOURCE, - SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, ) from homeassistant.components.ws66i.const import ( CONF_SOURCES, DOMAIN, INIT_OPTIONS_DEFAULT, - SERVICE_RESTORE, - SERVICE_SNAPSHOT, + MAX_VOL, + POLL_INTERVAL, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_IP_ADDRESS, SERVICE_TURN_OFF, @@ -35,10 +29,10 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed MOCK_SOURCE_DIC = { "1": "one", @@ -125,47 +119,52 @@ class MockWs66i: async def test_setup_success(hass): """Test connection success.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + with patch( "homeassistant.components.ws66i.get_ws66i", new=lambda *a: MockWs66i(), ): - config_entry = MockConfigEntry( - domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS - ) - config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(ZONE_1_ID) is not None + + assert config_entry.state is ConfigEntryState.LOADED + assert hass.states.get(ZONE_1_ID) is not None async def _setup_ws66i(hass, ws66i) -> MockConfigEntry: + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_DEFAULT_OPTIONS + ) + config_entry.add_to_hass(hass) + with patch( "homeassistant.components.ws66i.get_ws66i", new=lambda *a: ws66i, ): - config_entry = MockConfigEntry( - domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_DEFAULT_OPTIONS - ) - 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 + return config_entry async def _setup_ws66i_with_options(hass, ws66i) -> MockConfigEntry: + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + with patch( "homeassistant.components.ws66i.get_ws66i", new=lambda *a: ws66i, ): - config_entry = MockConfigEntry( - domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS - ) - 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 + return config_entry async def _call_media_player_service(hass, name, data): @@ -174,172 +173,10 @@ async def _call_media_player_service(hass, name, data): ) -async def _call_ws66i_service(hass, name, data): - await hass.services.async_call(DOMAIN, name, service_data=data, blocking=True) - - -async def test_cannot_connect(hass): - """Test connection error.""" - with patch( - "homeassistant.components.ws66i.get_ws66i", - new=lambda *a: MockWs66i(fail_open=True), - ): - config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert hass.states.get(ZONE_1_ID) is None - - -async def test_cannot_connect_2(hass): - """Test connection error pt 2.""" - # Another way to test same case as test_cannot_connect - ws66i = MockWs66i() - - with patch.object(MockWs66i, "open", side_effect=ConnectionError): - await _setup_ws66i(hass, ws66i) - assert hass.states.get(ZONE_1_ID) is None - - -async def test_service_calls_with_entity_id(hass): - """Test snapshot save/restore service calls.""" - _ = await _setup_ws66i_with_options(hass, MockWs66i()) - - # Changing media player to new state - await _call_media_player_service( - hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} - ) - await _call_media_player_service( - hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} - ) - - # Saving existing values - await _call_ws66i_service(hass, SERVICE_SNAPSHOT, {"entity_id": ZONE_1_ID}) - - # Changing media player to new state - await _call_media_player_service( - hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} - ) - await _call_media_player_service( - hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "three"} - ) - await hass.async_block_till_done() - - # Restoring other media player to its previous state - # The zone should not be restored - with pytest.raises(HomeAssistantError): - await _call_ws66i_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_2_ID}) - await hass.async_block_till_done() - - # Checking that values were not (!) restored - state = hass.states.get(ZONE_1_ID) - - assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 1.0 - assert state.attributes[ATTR_INPUT_SOURCE] == "three" - - # Restoring media player to its previous state - await _call_ws66i_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID}) - await hass.async_block_till_done() - - state = hass.states.get(ZONE_1_ID) - - assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.0 - assert state.attributes[ATTR_INPUT_SOURCE] == "one" - - -async def test_service_calls_with_all_entities(hass): - """Test snapshot save/restore service calls with entity id all.""" - _ = await _setup_ws66i_with_options(hass, MockWs66i()) - - # Changing media player to new state - await _call_media_player_service( - hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} - ) - await _call_media_player_service( - hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} - ) - - # Saving existing values - await _call_ws66i_service(hass, SERVICE_SNAPSHOT, {"entity_id": "all"}) - - # Changing media player to new state - await _call_media_player_service( - hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} - ) - await _call_media_player_service( - hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "three"} - ) - - # await coordinator.async_refresh() - # await hass.async_block_till_done() - - # Restoring media player to its previous state - await _call_ws66i_service(hass, SERVICE_RESTORE, {"entity_id": "all"}) - await hass.async_block_till_done() - - state = hass.states.get(ZONE_1_ID) - - assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.0 - assert state.attributes[ATTR_INPUT_SOURCE] == "one" - - -async def test_service_calls_without_relevant_entities(hass): - """Test snapshot save/restore service calls with bad entity id.""" - config_entry = await _setup_ws66i_with_options(hass, MockWs66i()) - - # Changing media player to new state - await _call_media_player_service( - hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} - ) - await _call_media_player_service( - hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} - ) - - ws66i_data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = ws66i_data.coordinator - await coordinator.async_refresh() - await hass.async_block_till_done() - - # Saving existing values - await _call_ws66i_service(hass, SERVICE_SNAPSHOT, {"entity_id": "all"}) - - # Changing media player to new state - await _call_media_player_service( - hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} - ) - await _call_media_player_service( - hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "three"} - ) - - await coordinator.async_refresh() - await hass.async_block_till_done() - - # Restoring media player to its previous state - await _call_ws66i_service(hass, SERVICE_RESTORE, {"entity_id": "light.demo"}) - await hass.async_block_till_done() - - state = hass.states.get(ZONE_1_ID) - - assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 1.0 - assert state.attributes[ATTR_INPUT_SOURCE] == "three" - - -async def test_restore_without_snapshot(hass): - """Test restore when snapshot wasn't called.""" - await _setup_ws66i(hass, MockWs66i()) - - with patch.object(MockWs66i, "restore_zone") as method_call: - with pytest.raises(HomeAssistantError): - await _call_ws66i_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID}) - await hass.async_block_till_done() - - assert not method_call.called - - async def test_update(hass): """Test updating values from ws66i.""" ws66i = MockWs66i() - config_entry = await _setup_ws66i_with_options(hass, ws66i) + _ = await _setup_ws66i_with_options(hass, ws66i) # Changing media player to new state await _call_media_player_service( @@ -350,13 +187,10 @@ async def test_update(hass): ) ws66i.set_source(11, 3) - ws66i.set_volume(11, 38) - - ws66i_data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = ws66i_data.coordinator + ws66i.set_volume(11, MAX_VOL) with patch.object(MockWs66i, "open") as method_call: - await coordinator.async_refresh() + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() assert not method_call.called @@ -371,7 +205,7 @@ async def test_update(hass): async def test_failed_update(hass): """Test updating failure from ws66i.""" ws66i = MockWs66i() - config_entry = await _setup_ws66i_with_options(hass, ws66i) + _ = await _setup_ws66i_with_options(hass, ws66i) # Changing media player to new state await _call_media_player_service( @@ -382,26 +216,25 @@ async def test_failed_update(hass): ) ws66i.set_source(11, 3) - ws66i.set_volume(11, 38) - ws66i_data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = ws66i_data.coordinator - await coordinator.async_refresh() + ws66i.set_volume(11, MAX_VOL) + + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() # Failed update, close called with patch.object(MockWs66i, "zone_status", return_value=None): - await coordinator.async_refresh() + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() assert hass.states.is_state(ZONE_1_ID, STATE_UNAVAILABLE) # A connection re-attempt fails with patch.object(MockWs66i, "zone_status", return_value=None): - await coordinator.async_refresh() + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() # A connection re-attempt succeeds - await coordinator.async_refresh() + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() # confirm entity is back on @@ -418,12 +251,12 @@ async def test_supported_features(hass): state = hass.states.get(ZONE_1_ID) assert ( - SUPPORT_VOLUME_MUTE - | SUPPORT_VOLUME_SET - | SUPPORT_VOLUME_STEP - | SUPPORT_TURN_ON - | SUPPORT_TURN_OFF - | SUPPORT_SELECT_SOURCE + MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.VOLUME_STEP + | MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.SELECT_SOURCE == state.attributes["supported_features"] ) @@ -462,15 +295,13 @@ async def test_select_source(hass): async def test_source_select(hass): - """Test behavior when device has unknown source.""" + """Test source selection simulated from keypad.""" ws66i = MockWs66i() - config_entry = await _setup_ws66i_with_options(hass, ws66i) + _ = await _setup_ws66i_with_options(hass, ws66i) ws66i.set_source(11, 5) - ws66i_data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = ws66i_data.coordinator - await coordinator.async_refresh() + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() state = hass.states.get(ZONE_1_ID) @@ -512,10 +343,7 @@ async def test_mute_volume(hass): async def test_volume_up_down(hass): """Test increasing volume by one.""" ws66i = MockWs66i() - config_entry = await _setup_ws66i(hass, ws66i) - - ws66i_data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = ws66i_data.coordinator + _ = await _setup_ws66i(hass, ws66i) await _call_media_player_service( hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} @@ -525,34 +353,89 @@ async def test_volume_up_down(hass): await _call_media_player_service( hass, SERVICE_VOLUME_DOWN, {"entity_id": ZONE_1_ID} ) - await coordinator.async_refresh() + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() # should not go below zero assert ws66i.zones[11].volume == 0 await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID}) - await coordinator.async_refresh() + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() assert ws66i.zones[11].volume == 1 await _call_media_player_service( hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} ) - await coordinator.async_refresh() + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() - assert ws66i.zones[11].volume == 38 + assert ws66i.zones[11].volume == MAX_VOL await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID}) - await coordinator.async_refresh() + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() - # should not go above 38 - assert ws66i.zones[11].volume == 38 + # should not go above 38 (MAX_VOL) + assert ws66i.zones[11].volume == MAX_VOL await _call_media_player_service( hass, SERVICE_VOLUME_DOWN, {"entity_id": ZONE_1_ID} ) - assert ws66i.zones[11].volume == 37 + assert ws66i.zones[11].volume == MAX_VOL - 1 + + +async def test_volume_while_mute(hass): + """Test increasing volume by one.""" + ws66i = MockWs66i() + _ = await _setup_ws66i(hass, ws66i) + + # Set vol to a known value + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} + ) + assert ws66i.zones[11].volume == 0 + + # Set mute to a known value, False + await _call_media_player_service( + hass, SERVICE_VOLUME_MUTE, {"entity_id": ZONE_1_ID, "is_volume_muted": False} + ) + assert not ws66i.zones[11].mute + + # Mute the zone + await _call_media_player_service( + hass, SERVICE_VOLUME_MUTE, {"entity_id": ZONE_1_ID, "is_volume_muted": True} + ) + assert ws66i.zones[11].mute + + # Increase volume. Mute state should go back to unmutted + await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID}) + assert ws66i.zones[11].volume == 1 + assert not ws66i.zones[11].mute + + # Mute the zone again + await _call_media_player_service( + hass, SERVICE_VOLUME_MUTE, {"entity_id": ZONE_1_ID, "is_volume_muted": True} + ) + assert ws66i.zones[11].mute + + # Decrease volume. Mute state should go back to unmutted + await _call_media_player_service( + hass, SERVICE_VOLUME_DOWN, {"entity_id": ZONE_1_ID} + ) + assert ws66i.zones[11].volume == 0 + assert not ws66i.zones[11].mute + + # Mute the zone again + await _call_media_player_service( + hass, SERVICE_VOLUME_MUTE, {"entity_id": ZONE_1_ID, "is_volume_muted": True} + ) + assert ws66i.zones[11].mute + + # Set to max volume. Mute state should go back to unmutted + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} + ) + assert ws66i.zones[11].volume == MAX_VOL + assert not ws66i.zones[11].mute async def test_first_run_with_available_zones(hass): @@ -611,82 +494,3 @@ async def test_register_entities_in_1_amp_only(hass): entry = registry.async_get(ZONE_7_ID) assert entry is None - - -async def test_unload_config_entry(hass): - """Test unloading config entry.""" - with patch( - "homeassistant.components.ws66i.get_ws66i", - new=lambda *a: MockWs66i(), - ): - config_entry = MockConfigEntry( - domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS - ) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert hass.data[DOMAIN][config_entry.entry_id] - - with patch.object(MockWs66i, "close") as method_call: - await config_entry.async_unload(hass) - await hass.async_block_till_done() - - assert method_call.called - - assert not hass.data[DOMAIN] - - -async def test_restore_snapshot_on_reconnect(hass): - """Test restoring a saved snapshot when reconnecting to amp.""" - ws66i = MockWs66i() - config_entry = await _setup_ws66i_with_options(hass, ws66i) - - # Changing media player to new state - await _call_media_player_service( - hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} - ) - await _call_media_player_service( - hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} - ) - - # Save a snapshot - await _call_ws66i_service(hass, SERVICE_SNAPSHOT, {"entity_id": ZONE_1_ID}) - - ws66i_data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = ws66i_data.coordinator - - # Failed update, - with patch.object(MockWs66i, "zone_status", return_value=None): - await coordinator.async_refresh() - await hass.async_block_till_done() - - assert hass.states.is_state(ZONE_1_ID, STATE_UNAVAILABLE) - - # A connection re-attempt succeeds - await coordinator.async_refresh() - await hass.async_block_till_done() - - # confirm entity is back on - state = hass.states.get(ZONE_1_ID) - - assert hass.states.is_state(ZONE_1_ID, STATE_ON) - assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.0 - assert state.attributes[ATTR_INPUT_SOURCE] == "one" - - # Change states - await _call_media_player_service( - hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} - ) - await _call_media_player_service( - hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "six"} - ) - - # Now confirm that the snapshot before the disconnect works - await _call_ws66i_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID}) - await hass.async_block_till_done() - - state = hass.states.get(ZONE_1_ID) - - assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.0 - assert state.attributes[ATTR_INPUT_SOURCE] == "one" From 7ff1b53d4fadc488fcfcfd2b631c8bbc2148b8e7 Mon Sep 17 00:00:00 2001 From: Matrix Date: Mon, 30 May 2022 02:54:23 +0800 Subject: [PATCH 1015/3516] Fix yolink device unavailable on startup (#72579) * fetch device state on startup * Suggest change * suggest fix * fix * fix * Fix suggest * suggest fix --- homeassistant/components/yolink/__init__.py | 68 ++++++++++++--- .../components/yolink/binary_sensor.py | 44 ++++++---- homeassistant/components/yolink/const.py | 2 +- .../components/yolink/coordinator.py | 86 +++---------------- homeassistant/components/yolink/entity.py | 20 +++-- homeassistant/components/yolink/sensor.py | 38 ++++---- homeassistant/components/yolink/siren.py | 39 +++++---- homeassistant/components/yolink/switch.py | 41 +++++---- 8 files changed, 177 insertions(+), 161 deletions(-) diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 2c85344c54b..7eb6b0229f0 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -1,25 +1,28 @@ """The yolink integration.""" from __future__ import annotations +import asyncio from datetime import timedelta -import logging +import async_timeout from yolink.client import YoLinkClient +from yolink.device import YoLinkDevice +from yolink.exception import YoLinkAuthFailError, YoLinkClientError +from yolink.model import BRDP from yolink.mqtt_client import MqttClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow from . import api -from .const import ATTR_CLIENT, ATTR_COORDINATOR, ATTR_MQTT_CLIENT, DOMAIN +from .const import ATTR_CLIENT, ATTR_COORDINATORS, ATTR_DEVICE, ATTR_MQTT_CLIENT, DOMAIN from .coordinator import YoLinkCoordinator SCAN_INTERVAL = timedelta(minutes=5) -_LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SIREN, Platform.SWITCH] @@ -41,18 +44,63 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: yolink_http_client = YoLinkClient(auth_mgr) yolink_mqtt_client = MqttClient(auth_mgr) - coordinator = YoLinkCoordinator(hass, yolink_http_client, yolink_mqtt_client) - await coordinator.init_coordinator() + + def on_message_callback(message: tuple[str, BRDP]) -> None: + data = message[1] + device_id = message[0] + if data.event is None: + return + event_param = data.event.split(".") + event_type = event_param[len(event_param) - 1] + if event_type not in ( + "Report", + "Alert", + "StatusChange", + "getState", + ): + return + resolved_state = data.data + if resolved_state is None: + return + entry_data = hass.data[DOMAIN].get(entry.entry_id) + if entry_data is None: + return + device_coordinators = entry_data.get(ATTR_COORDINATORS) + if device_coordinators is None: + return + device_coordinator = device_coordinators.get(device_id) + if device_coordinator is None: + return + device_coordinator.async_set_updated_data(resolved_state) + try: - await coordinator.async_config_entry_first_refresh() - except ConfigEntryNotReady as ex: - _LOGGER.error("Fetching initial data failed: %s", ex) + async with async_timeout.timeout(10): + device_response = await yolink_http_client.get_auth_devices() + home_info = await yolink_http_client.get_general_info() + await yolink_mqtt_client.init_home_connection( + home_info.data["id"], on_message_callback + ) + except YoLinkAuthFailError as yl_auth_err: + raise ConfigEntryAuthFailed from yl_auth_err + except (YoLinkClientError, asyncio.TimeoutError) as err: + raise ConfigEntryNotReady from err hass.data[DOMAIN][entry.entry_id] = { ATTR_CLIENT: yolink_http_client, ATTR_MQTT_CLIENT: yolink_mqtt_client, - ATTR_COORDINATOR: coordinator, } + auth_devices = device_response.data[ATTR_DEVICE] + device_coordinators = {} + for device_info in auth_devices: + device = YoLinkDevice(device_info, yolink_http_client) + device_coordinator = YoLinkCoordinator(hass, device) + try: + await device_coordinator.async_config_entry_first_refresh() + except ConfigEntryNotReady: + # Not failure by fetching device state + device_coordinator.data = {} + device_coordinators[device.device_id] = device_coordinator + hass.data[DOMAIN][entry.entry_id][ATTR_COORDINATORS] = device_coordinators hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/yolink/binary_sensor.py b/homeassistant/components/yolink/binary_sensor.py index 42899e08a2c..cacba484fe9 100644 --- a/homeassistant/components/yolink/binary_sensor.py +++ b/homeassistant/components/yolink/binary_sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from typing import Any from yolink.device import YoLinkDevice @@ -16,7 +17,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( - ATTR_COORDINATOR, + ATTR_COORDINATORS, ATTR_DEVICE_DOOR_SENSOR, ATTR_DEVICE_LEAK_SENSOR, ATTR_DEVICE_MOTION_SENSOR, @@ -32,7 +33,7 @@ class YoLinkBinarySensorEntityDescription(BinarySensorEntityDescription): exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True state_key: str = "state" - value: Callable[[str], bool | None] = lambda _: None + value: Callable[[Any], bool | None] = lambda _: None SENSOR_DEVICE_TYPE = [ @@ -47,14 +48,14 @@ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( icon="mdi:door", device_class=BinarySensorDeviceClass.DOOR, name="State", - value=lambda value: value == "open", + value=lambda value: value == "open" if value is not None else None, exists_fn=lambda device: device.device_type in [ATTR_DEVICE_DOOR_SENSOR], ), YoLinkBinarySensorEntityDescription( key="motion_state", device_class=BinarySensorDeviceClass.MOTION, name="Motion", - value=lambda value: value == "alert", + value=lambda value: value == "alert" if value is not None else None, exists_fn=lambda device: device.device_type in [ATTR_DEVICE_MOTION_SENSOR], ), YoLinkBinarySensorEntityDescription( @@ -62,7 +63,7 @@ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( name="Leak", icon="mdi:water", device_class=BinarySensorDeviceClass.MOISTURE, - value=lambda value: value == "alert", + value=lambda value: value == "alert" if value is not None else None, exists_fn=lambda device: device.device_type in [ATTR_DEVICE_LEAK_SENSOR], ), ) @@ -74,18 +75,20 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up YoLink Sensor from a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATOR] - sensor_devices = [ - device - for device in coordinator.yl_devices - if device.device_type in SENSOR_DEVICE_TYPE + device_coordinators = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATORS] + binary_sensor_device_coordinators = [ + device_coordinator + for device_coordinator in device_coordinators.values() + if device_coordinator.device.device_type in SENSOR_DEVICE_TYPE ] entities = [] - for sensor_device in sensor_devices: + for binary_sensor_device_coordinator in binary_sensor_device_coordinators: for description in SENSOR_TYPES: - if description.exists_fn(sensor_device): + if description.exists_fn(binary_sensor_device_coordinator.device): entities.append( - YoLinkBinarySensorEntity(coordinator, description, sensor_device) + YoLinkBinarySensorEntity( + binary_sensor_device_coordinator, description + ) ) async_add_entities(entities) @@ -99,18 +102,21 @@ class YoLinkBinarySensorEntity(YoLinkEntity, BinarySensorEntity): self, coordinator: YoLinkCoordinator, description: YoLinkBinarySensorEntityDescription, - device: YoLinkDevice, ) -> None: """Init YoLink Sensor.""" - super().__init__(coordinator, device) + super().__init__(coordinator) self.entity_description = description - self._attr_unique_id = f"{device.device_id} {self.entity_description.key}" - self._attr_name = f"{device.device_name} ({self.entity_description.name})" + self._attr_unique_id = ( + f"{coordinator.device.device_id} {self.entity_description.key}" + ) + self._attr_name = ( + f"{coordinator.device.device_name} ({self.entity_description.name})" + ) @callback - def update_entity_state(self, state: dict) -> None: + def update_entity_state(self, state: dict[str, Any]) -> None: """Update HA Entity State.""" self._attr_is_on = self.entity_description.value( - state[self.entity_description.state_key] + state.get(self.entity_description.state_key) ) self.async_write_ha_state() diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index 00d6d6d028e..97252c5c989 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -5,7 +5,7 @@ MANUFACTURER = "YoLink" HOME_ID = "homeId" HOME_SUBSCRIPTION = "home_subscription" ATTR_PLATFORM_SENSOR = "sensor" -ATTR_COORDINATOR = "coordinator" +ATTR_COORDINATORS = "coordinators" ATTR_DEVICE = "devices" ATTR_DEVICE_TYPE = "type" ATTR_DEVICE_NAME = "name" diff --git a/homeassistant/components/yolink/coordinator.py b/homeassistant/components/yolink/coordinator.py index e5578eae4b2..68a1aef42f7 100644 --- a/homeassistant/components/yolink/coordinator.py +++ b/homeassistant/components/yolink/coordinator.py @@ -1,22 +1,18 @@ """YoLink DataUpdateCoordinator.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging import async_timeout -from yolink.client import YoLinkClient from yolink.device import YoLinkDevice from yolink.exception import YoLinkAuthFailError, YoLinkClientError -from yolink.model import BRDP -from yolink.mqtt_client import MqttClient from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ATTR_DEVICE, ATTR_DEVICE_STATE, DOMAIN +from .const import ATTR_DEVICE_STATE, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -24,9 +20,7 @@ _LOGGER = logging.getLogger(__name__) class YoLinkCoordinator(DataUpdateCoordinator[dict]): """YoLink DataUpdateCoordinator.""" - def __init__( - self, hass: HomeAssistant, yl_client: YoLinkClient, yl_mqtt_client: MqttClient - ) -> None: + def __init__(self, hass: HomeAssistant, device: YoLinkDevice) -> None: """Init YoLink DataUpdateCoordinator. fetch state every 30 minutes base on yolink device heartbeat interval @@ -35,75 +29,17 @@ class YoLinkCoordinator(DataUpdateCoordinator[dict]): super().__init__( hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=30) ) - self._client = yl_client - self._mqtt_client = yl_mqtt_client - self.yl_devices: list[YoLinkDevice] = [] - self.data = {} + self.device = device - def on_message_callback(self, message: tuple[str, BRDP]): - """On message callback.""" - data = message[1] - if data.event is None: - return - event_param = data.event.split(".") - event_type = event_param[len(event_param) - 1] - if event_type not in ( - "Report", - "Alert", - "StatusChange", - "getState", - ): - return - resolved_state = data.data - if resolved_state is None: - return - self.data[message[0]] = resolved_state - self.async_set_updated_data(self.data) - - async def init_coordinator(self): - """Init coordinator.""" + async def _async_update_data(self) -> dict: + """Fetch device state.""" try: async with async_timeout.timeout(10): - home_info = await self._client.get_general_info() - await self._mqtt_client.init_home_connection( - home_info.data["id"], self.on_message_callback - ) - async with async_timeout.timeout(10): - device_response = await self._client.get_auth_devices() - - except YoLinkAuthFailError as yl_auth_err: - raise ConfigEntryAuthFailed from yl_auth_err - - except (YoLinkClientError, asyncio.TimeoutError) as err: - raise ConfigEntryNotReady from err - - yl_devices: list[YoLinkDevice] = [] - - for device_info in device_response.data[ATTR_DEVICE]: - yl_devices.append(YoLinkDevice(device_info, self._client)) - - self.yl_devices = yl_devices - - async def fetch_device_state(self, device: YoLinkDevice): - """Fetch Device State.""" - try: - async with async_timeout.timeout(10): - device_state_resp = await device.fetch_state_with_api() - if ATTR_DEVICE_STATE in device_state_resp.data: - self.data[device.device_id] = device_state_resp.data[ - ATTR_DEVICE_STATE - ] + device_state_resp = await self.device.fetch_state_with_api() except YoLinkAuthFailError as yl_auth_err: raise ConfigEntryAuthFailed from yl_auth_err except YoLinkClientError as yl_client_err: - raise UpdateFailed( - f"Error communicating with API: {yl_client_err}" - ) from yl_client_err - - async def _async_update_data(self) -> dict: - fetch_tasks = [] - for yl_device in self.yl_devices: - fetch_tasks.append(self.fetch_device_state(yl_device)) - if fetch_tasks: - await asyncio.gather(*fetch_tasks) - return self.data + raise UpdateFailed from yl_client_err + if ATTR_DEVICE_STATE in device_state_resp.data: + return device_state_resp.data[ATTR_DEVICE_STATE] + return {} diff --git a/homeassistant/components/yolink/entity.py b/homeassistant/components/yolink/entity.py index 6954b117728..5365681739e 100644 --- a/homeassistant/components/yolink/entity.py +++ b/homeassistant/components/yolink/entity.py @@ -3,8 +3,6 @@ from __future__ import annotations from abc import abstractmethod -from yolink.device import YoLinkDevice - from homeassistant.core import callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -19,20 +17,24 @@ class YoLinkEntity(CoordinatorEntity[YoLinkCoordinator]): def __init__( self, coordinator: YoLinkCoordinator, - device_info: YoLinkDevice, ) -> None: """Init YoLink Entity.""" super().__init__(coordinator) - self.device = device_info @property def device_id(self) -> str: """Return the device id of the YoLink device.""" - return self.device.device_id + return self.coordinator.device.device_id + + async def async_added_to_hass(self) -> None: + """Update state.""" + await super().async_added_to_hass() + return self._handle_coordinator_update() @callback def _handle_coordinator_update(self) -> None: - data = self.coordinator.data.get(self.device.device_id) + """Update state.""" + data = self.coordinator.data if data is not None: self.update_entity_state(data) @@ -40,10 +42,10 @@ class YoLinkEntity(CoordinatorEntity[YoLinkCoordinator]): def device_info(self) -> DeviceInfo: """Return the device info for HA.""" return DeviceInfo( - identifiers={(DOMAIN, self.device.device_id)}, + identifiers={(DOMAIN, self.coordinator.device.device_id)}, manufacturer=MANUFACTURER, - model=self.device.device_type, - name=self.device.device_name, + model=self.coordinator.device.device_type, + name=self.coordinator.device.device_name, ) @callback diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index e33772c24be..463d8b14da4 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import percentage from .const import ( - ATTR_COORDINATOR, + ATTR_COORDINATORS, ATTR_DEVICE_DOOR_SENSOR, ATTR_DEVICE_MOTION_SENSOR, ATTR_DEVICE_TH_SENSOR, @@ -54,7 +54,9 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda value: percentage.ordered_list_item_to_percentage( [1, 2, 3, 4], value - ), + ) + if value is not None + else None, exists_fn=lambda device: device.device_type in [ATTR_DEVICE_DOOR_SENSOR, ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_MOTION_SENSOR], ), @@ -89,18 +91,21 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up YoLink Sensor from a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATOR] - sensor_devices = [ - device - for device in coordinator.yl_devices - if device.device_type in SENSOR_DEVICE_TYPE + device_coordinators = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATORS] + sensor_device_coordinators = [ + device_coordinator + for device_coordinator in device_coordinators.values() + if device_coordinator.device.device_type in SENSOR_DEVICE_TYPE ] entities = [] - for sensor_device in sensor_devices: + for sensor_device_coordinator in sensor_device_coordinators: for description in SENSOR_TYPES: - if description.exists_fn(sensor_device): + if description.exists_fn(sensor_device_coordinator.device): entities.append( - YoLinkSensorEntity(coordinator, description, sensor_device) + YoLinkSensorEntity( + sensor_device_coordinator, + description, + ) ) async_add_entities(entities) @@ -114,18 +119,21 @@ class YoLinkSensorEntity(YoLinkEntity, SensorEntity): self, coordinator: YoLinkCoordinator, description: YoLinkSensorEntityDescription, - device: YoLinkDevice, ) -> None: """Init YoLink Sensor.""" - super().__init__(coordinator, device) + super().__init__(coordinator) self.entity_description = description - self._attr_unique_id = f"{device.device_id} {self.entity_description.key}" - self._attr_name = f"{device.device_name} ({self.entity_description.name})" + self._attr_unique_id = ( + f"{coordinator.device.device_id} {self.entity_description.key}" + ) + self._attr_name = ( + f"{coordinator.device.device_name} ({self.entity_description.name})" + ) @callback def update_entity_state(self, state: dict) -> None: """Update HA Entity State.""" self._attr_native_value = self.entity_description.value( - state[self.entity_description.key] + state.get(self.entity_description.key) ) self.async_write_ha_state() diff --git a/homeassistant/components/yolink/siren.py b/homeassistant/components/yolink/siren.py index 7a621db6eca..7e67dfb12f1 100644 --- a/homeassistant/components/yolink/siren.py +++ b/homeassistant/components/yolink/siren.py @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_COORDINATOR, ATTR_DEVICE_SIREN, DOMAIN +from .const import ATTR_COORDINATORS, ATTR_DEVICE_SIREN, DOMAIN from .coordinator import YoLinkCoordinator from .entity import YoLinkEntity @@ -28,14 +28,14 @@ class YoLinkSirenEntityDescription(SirenEntityDescription): """YoLink SirenEntityDescription.""" exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True - value: Callable[[str], bool | None] = lambda _: None + value: Callable[[Any], bool | None] = lambda _: None DEVICE_TYPES: tuple[YoLinkSirenEntityDescription, ...] = ( YoLinkSirenEntityDescription( key="state", name="State", - value=lambda value: value == "alert", + value=lambda value: value == "alert" if value is not None else None, exists_fn=lambda device: device.device_type in [ATTR_DEVICE_SIREN], ), ) @@ -49,16 +49,20 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up YoLink siren from a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATOR] - devices = [ - device for device in coordinator.yl_devices if device.device_type in DEVICE_TYPE + device_coordinators = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATORS] + siren_device_coordinators = [ + device_coordinator + for device_coordinator in device_coordinators.values() + if device_coordinator.device.device_type in DEVICE_TYPE ] entities = [] - for device in devices: + for siren_device_coordinator in siren_device_coordinators: for description in DEVICE_TYPES: - if description.exists_fn(device): + if description.exists_fn(siren_device_coordinator.device): entities.append( - YoLinkSirenEntity(config_entry, coordinator, description, device) + YoLinkSirenEntity( + config_entry, siren_device_coordinator, description + ) ) async_add_entities(entities) @@ -73,23 +77,26 @@ class YoLinkSirenEntity(YoLinkEntity, SirenEntity): config_entry: ConfigEntry, coordinator: YoLinkCoordinator, description: YoLinkSirenEntityDescription, - device: YoLinkDevice, ) -> None: """Init YoLink Siren.""" - super().__init__(coordinator, device) + super().__init__(coordinator) self.config_entry = config_entry self.entity_description = description - self._attr_unique_id = f"{device.device_id} {self.entity_description.key}" - self._attr_name = f"{device.device_name} ({self.entity_description.name})" + self._attr_unique_id = ( + f"{coordinator.device.device_id} {self.entity_description.key}" + ) + self._attr_name = ( + f"{coordinator.device.device_name} ({self.entity_description.name})" + ) self._attr_supported_features = ( SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF ) @callback - def update_entity_state(self, state: dict) -> None: + def update_entity_state(self, state: dict[str, Any]) -> None: """Update HA Entity State.""" self._attr_is_on = self.entity_description.value( - state[self.entity_description.key] + state.get(self.entity_description.key) ) self.async_write_ha_state() @@ -97,7 +104,7 @@ class YoLinkSirenEntity(YoLinkEntity, SirenEntity): """Call setState api to change siren state.""" try: # call_device_http_api will check result, fail by raise YoLinkClientError - await self.device.call_device_http_api( + await self.coordinator.device.call_device_http_api( "setState", {"state": {"alarm": state}} ) except YoLinkAuthFailError as yl_auth_err: diff --git a/homeassistant/components/yolink/switch.py b/homeassistant/components/yolink/switch.py index b3756efb74c..f16dc781a9c 100644 --- a/homeassistant/components/yolink/switch.py +++ b/homeassistant/components/yolink/switch.py @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_COORDINATOR, ATTR_DEVICE_OUTLET, DOMAIN +from .const import ATTR_COORDINATORS, ATTR_DEVICE_OUTLET, DOMAIN from .coordinator import YoLinkCoordinator from .entity import YoLinkEntity @@ -28,7 +28,7 @@ class YoLinkSwitchEntityDescription(SwitchEntityDescription): """YoLink SwitchEntityDescription.""" exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True - value: Callable[[str], bool | None] = lambda _: None + value: Callable[[Any], bool | None] = lambda _: None DEVICE_TYPES: tuple[YoLinkSwitchEntityDescription, ...] = ( @@ -36,7 +36,7 @@ DEVICE_TYPES: tuple[YoLinkSwitchEntityDescription, ...] = ( key="state", device_class=SwitchDeviceClass.OUTLET, name="State", - value=lambda value: value == "open", + value=lambda value: value == "open" if value is not None else None, exists_fn=lambda device: device.device_type in [ATTR_DEVICE_OUTLET], ), ) @@ -50,16 +50,20 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up YoLink Sensor from a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATOR] - devices = [ - device for device in coordinator.yl_devices if device.device_type in DEVICE_TYPE + device_coordinators = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATORS] + switch_device_coordinators = [ + device_coordinator + for device_coordinator in device_coordinators.values() + if device_coordinator.device.device_type in DEVICE_TYPE ] entities = [] - for device in devices: + for switch_device_coordinator in switch_device_coordinators: for description in DEVICE_TYPES: - if description.exists_fn(device): + if description.exists_fn(switch_device_coordinator.device): entities.append( - YoLinkSwitchEntity(config_entry, coordinator, description, device) + YoLinkSwitchEntity( + config_entry, switch_device_coordinator, description + ) ) async_add_entities(entities) @@ -74,20 +78,23 @@ class YoLinkSwitchEntity(YoLinkEntity, SwitchEntity): config_entry: ConfigEntry, coordinator: YoLinkCoordinator, description: YoLinkSwitchEntityDescription, - device: YoLinkDevice, ) -> None: """Init YoLink Outlet.""" - super().__init__(coordinator, device) + super().__init__(coordinator) self.config_entry = config_entry self.entity_description = description - self._attr_unique_id = f"{device.device_id} {self.entity_description.key}" - self._attr_name = f"{device.device_name} ({self.entity_description.name})" + self._attr_unique_id = ( + f"{coordinator.device.device_id} {self.entity_description.key}" + ) + self._attr_name = ( + f"{coordinator.device.device_name} ({self.entity_description.name})" + ) @callback - def update_entity_state(self, state: dict) -> None: + def update_entity_state(self, state: dict[str, Any]) -> None: """Update HA Entity State.""" self._attr_is_on = self.entity_description.value( - state[self.entity_description.key] + state.get(self.entity_description.key) ) self.async_write_ha_state() @@ -95,7 +102,9 @@ class YoLinkSwitchEntity(YoLinkEntity, SwitchEntity): """Call setState api to change outlet state.""" try: # call_device_http_api will check result, fail by raise YoLinkClientError - await self.device.call_device_http_api("setState", {"state": state}) + await self.coordinator.device.call_device_http_api( + "setState", {"state": state} + ) except YoLinkAuthFailError as yl_auth_err: self.config_entry.async_start_reauth(self.hass) raise HomeAssistantError(yl_auth_err) from yl_auth_err From 1ed7e226c6d331d6b22abb57f46d57a8f4a28900 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 29 May 2022 20:57:47 +0200 Subject: [PATCH 1016/3516] Address late review comments for Tankerkoenig (#72672) * address late review comment from #72654 * use entry_id instead of unique_id * remove not needed `_hass` property * fix skiping failing stations * remove not neccessary error log * set DeviceEntryType.SERVICE * fix use entry_id instead of unique_id * apply suggestions on tests * add return value also to other tests * invert data check to early return user form --- .../components/tankerkoenig/__init__.py | 45 +++++++++++++------ .../components/tankerkoenig/binary_sensor.py | 18 +++----- .../components/tankerkoenig/config_flow.py | 19 ++++---- .../components/tankerkoenig/sensor.py | 18 ++------ .../tankerkoenig/test_config_flow.py | 9 ++-- 5 files changed, 55 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index 08520c8f5cc..e63add83fad 100644 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + ATTR_ID, CONF_API_KEY, CONF_LATITUDE, CONF_LOCATION, @@ -24,8 +25,14 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import ( CONF_FUEL_TYPES, @@ -109,9 +116,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set a tankerkoenig configuration entry up.""" hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][ - entry.unique_id - ] = coordinator = TankerkoenigDataUpdateCoordinator( + hass.data[DOMAIN][entry.entry_id] = coordinator = TankerkoenigDataUpdateCoordinator( hass, entry, _LOGGER, @@ -140,7 +145,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Tankerkoenig config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN].pop(entry.unique_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok @@ -172,7 +177,6 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): self._api_key: str = entry.data[CONF_API_KEY] self._selected_stations: list[str] = entry.data[CONF_STATIONS] - self._hass = hass self.stations: dict[str, dict] = {} self.fuel_types: list[str] = entry.data[CONF_FUEL_TYPES] self.show_on_map: bool = entry.options[CONF_SHOW_ON_MAP] @@ -195,7 +199,7 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): station_id, station_data["message"], ) - return False + continue self.add_station(station_data["station"]) if len(self.stations) > 10: _LOGGER.warning( @@ -215,7 +219,7 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): # The API seems to only return at most 10 results, so split the list in chunks of 10 # and merge it together. for index in range(ceil(len(station_ids) / 10)): - data = await self._hass.async_add_executor_job( + data = await self.hass.async_add_executor_job( pytankerkoenig.getPriceList, self._api_key, station_ids[index * 10 : (index + 1) * 10], @@ -223,13 +227,11 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): _LOGGER.debug("Received data: %s", data) if not data["ok"]: - _LOGGER.error( - "Error fetching data from tankerkoenig.de: %s", data["message"] - ) raise UpdateFailed(data["message"]) if "prices" not in data: - _LOGGER.error("Did not receive price information from tankerkoenig.de") - raise UpdateFailed("No prices in data") + raise UpdateFailed( + "Did not receive price information from tankerkoenig.de" + ) prices.update(data["prices"]) return prices @@ -244,3 +246,20 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): self.stations[station_id] = station _LOGGER.debug("add_station called for station: %s", station) + + +class TankerkoenigCoordinatorEntity(CoordinatorEntity): + """Tankerkoenig base entity.""" + + def __init__( + self, coordinator: TankerkoenigDataUpdateCoordinator, station: dict + ) -> None: + """Initialize the Tankerkoenig base entity.""" + super().__init__(coordinator) + self._attr_device_info = DeviceInfo( + identifiers={(ATTR_ID, station["id"])}, + name=f"{station['brand']} {station['street']} {station['houseNumber']}", + model=station["brand"], + configuration_url="https://www.tankerkoenig.de", + entry_type=DeviceEntryType.SERVICE, + ) diff --git a/homeassistant/components/tankerkoenig/binary_sensor.py b/homeassistant/components/tankerkoenig/binary_sensor.py index 9a2b048e0b8..5f10b54f704 100644 --- a/homeassistant/components/tankerkoenig/binary_sensor.py +++ b/homeassistant/components/tankerkoenig/binary_sensor.py @@ -8,13 +8,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE 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 . import TankerkoenigDataUpdateCoordinator +from . import TankerkoenigCoordinatorEntity, TankerkoenigDataUpdateCoordinator from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -25,7 +23,7 @@ async def async_setup_entry( ) -> None: """Set up the tankerkoenig binary sensors.""" - coordinator: TankerkoenigDataUpdateCoordinator = hass.data[DOMAIN][entry.unique_id] + coordinator: TankerkoenigDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] stations = coordinator.stations.values() entities = [] @@ -41,7 +39,7 @@ async def async_setup_entry( async_add_entities(entities) -class StationOpenBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): +class StationOpenBinarySensorEntity(TankerkoenigCoordinatorEntity, BinarySensorEntity): """Shows if a station is open or closed.""" _attr_device_class = BinarySensorDeviceClass.DOOR @@ -53,18 +51,12 @@ class StationOpenBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): show_on_map: bool, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator) + super().__init__(coordinator, station) self._station_id = station["id"] self._attr_name = ( f"{station['brand']} {station['street']} {station['houseNumber']} status" ) self._attr_unique_id = f"{station['id']}_status" - self._attr_device_info = DeviceInfo( - identifiers={(ATTR_ID, station["id"])}, - name=f"{station['brand']} {station['street']} {station['houseNumber']}", - model=station["brand"], - configuration_url="https://www.tankerkoenig.de", - ) if show_on_map: self._attr_extra_state_attributes = { ATTR_LATITUDE: station["lat"], diff --git a/homeassistant/components/tankerkoenig/config_flow.py b/homeassistant/components/tankerkoenig/config_flow.py index af3b5273b16..345b034b027 100644 --- a/homeassistant/components/tankerkoenig/config_flow.py +++ b/homeassistant/components/tankerkoenig/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Tankerkoenig.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any from pytankerkoenig import customException, getNearbyStations @@ -30,7 +31,7 @@ from .const import CONF_FUEL_TYPES, CONF_STATIONS, DEFAULT_RADIUS, DOMAIN, FUEL_ async def async_get_nearby_stations( - hass: HomeAssistant, data: dict[str, Any] + hass: HomeAssistant, data: Mapping[str, Any] ) -> dict[str, Any]: """Fetch nearby stations.""" try: @@ -114,14 +115,12 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self._show_form_user( user_input, errors={CONF_API_KEY: "invalid_auth"} ) - if stations := data.get("stations"): - for station in stations: - self._stations[ - station["id"] - ] = f"{station['brand']} {station['street']} {station['houseNumber']} - ({station['dist']}km)" - - else: + if len(stations := data.get("stations", [])) == 0: return self._show_form_user(user_input, errors={CONF_RADIUS: "no_stations"}) + for station in stations: + self._stations[ + station["id"] + ] = f"{station['brand']} {station['street']} {station['houseNumber']} - ({station['dist']}km)" self._data = user_input @@ -180,7 +179,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_RADIUS, default=user_input.get(CONF_RADIUS, DEFAULT_RADIUS) ): NumberSelector( NumberSelectorConfig( - min=0.1, + min=1.0, max=25, step=0.1, unit_of_measurement=LENGTH_KILOMETERS, @@ -224,7 +223,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_create_entry(title="", data=user_input) nearby_stations = await async_get_nearby_stations( - self.hass, dict(self.config_entry.data) + self.hass, self.config_entry.data ) if stations := nearby_stations.get("stations"): for station in stations: diff --git a/homeassistant/components/tankerkoenig/sensor.py b/homeassistant/components/tankerkoenig/sensor.py index 898a38c3c14..c63b0ea0e7e 100644 --- a/homeassistant/components/tankerkoenig/sensor.py +++ b/homeassistant/components/tankerkoenig/sensor.py @@ -7,17 +7,14 @@ from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, - ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, CURRENCY_EURO, ) 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 . import TankerkoenigDataUpdateCoordinator +from . import TankerkoenigCoordinatorEntity, TankerkoenigDataUpdateCoordinator from .const import ( ATTR_BRAND, ATTR_CITY, @@ -39,7 +36,7 @@ async def async_setup_entry( ) -> None: """Set up the tankerkoenig sensors.""" - coordinator: TankerkoenigDataUpdateCoordinator = hass.data[DOMAIN][entry.unique_id] + coordinator: TankerkoenigDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] stations = coordinator.stations.values() entities = [] @@ -62,7 +59,7 @@ async def async_setup_entry( async_add_entities(entities) -class FuelPriceSensor(CoordinatorEntity, SensorEntity): +class FuelPriceSensor(TankerkoenigCoordinatorEntity, SensorEntity): """Contains prices for fuel in a given station.""" _attr_state_class = SensorStateClass.MEASUREMENT @@ -70,19 +67,12 @@ class FuelPriceSensor(CoordinatorEntity, SensorEntity): def __init__(self, fuel_type, station, coordinator, show_on_map): """Initialize the sensor.""" - super().__init__(coordinator) + super().__init__(coordinator, station) self._station_id = station["id"] self._fuel_type = fuel_type self._attr_name = f"{station['brand']} {station['street']} {station['houseNumber']} {FUEL_TYPES[fuel_type]}" self._attr_native_unit_of_measurement = CURRENCY_EURO self._attr_unique_id = f"{station['id']}_{fuel_type}" - self._attr_device_info = DeviceInfo( - identifiers={(ATTR_ID, station["id"])}, - name=f"{station['brand']} {station['street']} {station['houseNumber']}", - model=station["brand"], - configuration_url="https://www.tankerkoenig.de", - ) - attrs = { ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_BRAND: station["brand"], diff --git a/tests/components/tankerkoenig/test_config_flow.py b/tests/components/tankerkoenig/test_config_flow.py index b18df0eed24..f48a09fd64b 100644 --- a/tests/components/tankerkoenig/test_config_flow.py +++ b/tests/components/tankerkoenig/test_config_flow.py @@ -95,7 +95,7 @@ async def test_user(hass: HomeAssistant): assert result["step_id"] == "user" with patch( - "homeassistant.components.tankerkoenig.async_setup_entry" + "homeassistant.components.tankerkoenig.async_setup_entry", return_value=True ) as mock_setup_entry, patch( "homeassistant.components.tankerkoenig.config_flow.getNearbyStations", return_value=MOCK_NEARVY_STATIONS_OK, @@ -147,6 +147,7 @@ async def test_user_already_configured(hass: HomeAssistant): ) assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" async def test_exception_security(hass: HomeAssistant): @@ -193,7 +194,7 @@ async def test_user_no_stations(hass: HomeAssistant): async def test_import(hass: HomeAssistant): """Test starting a flow by import.""" with patch( - "homeassistant.components.tankerkoenig.async_setup_entry" + "homeassistant.components.tankerkoenig.async_setup_entry", return_value=True ) as mock_setup_entry, patch( "homeassistant.components.tankerkoenig.config_flow.getNearbyStations", return_value=MOCK_NEARVY_STATIONS_OK, @@ -233,12 +234,12 @@ async def test_options_flow(hass: HomeAssistant): mock_config.add_to_hass(hass) with patch( - "homeassistant.components.tankerkoenig.async_setup_entry" + "homeassistant.components.tankerkoenig.async_setup_entry", return_value=True ) as mock_setup_entry, patch( "homeassistant.components.tankerkoenig.config_flow.getNearbyStations", return_value=MOCK_NEARVY_STATIONS_OK, ): - await mock_config.async_setup(hass) + await hass.config_entries.async_setup(mock_config.entry_id) await hass.async_block_till_done() assert mock_setup_entry.called From 3c5b778ee3507067d3a27b12c8ee06c48fa48e17 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 30 May 2022 00:27:06 +0000 Subject: [PATCH 1017/3516] [ci skip] Translation update --- .../components/generic/translations/el.json | 2 ++ .../components/google/translations/el.json | 9 ++++++++ .../components/hassio/translations/el.json | 1 + .../components/ialarm_xr/translations/el.json | 21 +++++++++++++++++++ .../components/recorder/translations/el.json | 2 ++ .../steam_online/translations/el.json | 3 +++ .../tankerkoenig/translations/ca.json | 3 ++- .../tankerkoenig/translations/el.json | 3 ++- .../tankerkoenig/translations/et.json | 3 ++- .../tankerkoenig/translations/hu.json | 3 ++- .../tankerkoenig/translations/id.json | 3 ++- .../tankerkoenig/translations/it.json | 3 ++- .../totalconnect/translations/el.json | 11 ++++++++++ .../components/ws66i/translations/en.json | 3 +++ .../components/zha/translations/zh-Hans.json | 2 +- 15 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/ialarm_xr/translations/el.json diff --git a/homeassistant/components/generic/translations/el.json b/homeassistant/components/generic/translations/el.json index be1c963740a..f97714a53c1 100644 --- a/homeassistant/components/generic/translations/el.json +++ b/homeassistant/components/generic/translations/el.json @@ -15,6 +15,7 @@ "stream_no_video": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b2\u03af\u03bd\u03c4\u03b5\u03bf", "stream_not_permitted": "\u0394\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03c1\u03bf\u03ae. \u039b\u03ac\u03b8\u03bf\u03c2 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP;", "stream_unauthorised": "\u0397 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", + "template_error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b1\u03c0\u03cc\u03b4\u03bf\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03c4\u03cd\u03c0\u03bf\u03c5. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2.", "timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL", "unable_still_load": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7\u03c2 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b1\u03ba\u03af\u03bd\u03b7\u03c4\u03b7\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 (\u03c0.\u03c7. \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2, \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03ae \u03b1\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2). \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2.", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" @@ -57,6 +58,7 @@ "stream_no_video": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b2\u03af\u03bd\u03c4\u03b5\u03bf", "stream_not_permitted": "\u0394\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03c1\u03bf\u03ae. \u039b\u03ac\u03b8\u03bf\u03c2 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP;", "stream_unauthorised": "\u0397 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", + "template_error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b1\u03c0\u03cc\u03b4\u03bf\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03c4\u03cd\u03c0\u03bf\u03c5. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2.", "timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL", "unable_still_load": "\u0391\u03b4\u03c5\u03bd\u03b1\u03bc\u03af\u03b1 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7\u03c2 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b1\u03ba\u03af\u03bd\u03b7\u03c4\u03b7\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 (\u03c0.\u03c7. \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2, \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03ae \u03b1\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2). \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03b1\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2.", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" diff --git a/homeassistant/components/google/translations/el.json b/homeassistant/components/google/translations/el.json index 11c78f96a93..dd93a5ab3c8 100644 --- a/homeassistant/components/google/translations/el.json +++ b/homeassistant/components/google/translations/el.json @@ -27,5 +27,14 @@ "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" } } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "\u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c4\u03bf\u03c5 Home Assistant \u03c3\u03c4\u03bf \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03cc\u03b3\u03b9\u03bf Google" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/el.json b/homeassistant/components/hassio/translations/el.json index ba3a52f4bbd..9e9b32d7ce3 100644 --- a/homeassistant/components/hassio/translations/el.json +++ b/homeassistant/components/hassio/translations/el.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "\u0388\u03ba\u03b4\u03bf\u03c3\u03b7 Agent", "board": "\u03a0\u03bb\u03b1\u03ba\u03ad\u03c4\u03b1", "disk_total": "\u03a3\u03cd\u03bd\u03bf\u03bb\u03bf \u03b4\u03af\u03c3\u03ba\u03bf\u03c5", "disk_used": "\u0394\u03af\u03c3\u03ba\u03bf\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9", diff --git a/homeassistant/components/ialarm_xr/translations/el.json b/homeassistant/components/ialarm_xr/translations/el.json new file mode 100644 index 00000000000..067055d6654 --- /dev/null +++ b/homeassistant/components/ialarm_xr/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/el.json b/homeassistant/components/recorder/translations/el.json index 6d541820c55..46c585c816d 100644 --- a/homeassistant/components/recorder/translations/el.json +++ b/homeassistant/components/recorder/translations/el.json @@ -2,6 +2,8 @@ "system_health": { "info": { "current_recorder_run": "\u03a4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1 \u03ce\u03c1\u03b1 \u03ad\u03bd\u03b1\u03c1\u03be\u03b7\u03c2 \u03b5\u03ba\u03c4\u03ad\u03bb\u03b5\u03c3\u03b7\u03c2", + "database_engine": "\u039c\u03b7\u03c7\u03b1\u03bd\u03ae \u03b2\u03ac\u03c3\u03b7\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd", + "database_version": "\u0388\u03ba\u03b4\u03bf\u03c3\u03b7 \u03b2\u03ac\u03c3\u03b7\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd", "estimated_db_size": "\u0395\u03ba\u03c4\u03b9\u03bc\u03ce\u03bc\u03b5\u03bd\u03bf \u03bc\u03ad\u03b3\u03b5\u03b8\u03bf\u03c2 \u03b2\u03ac\u03c3\u03b7\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd (MiB)", "oldest_recorder_run": "\u03a0\u03b1\u03bb\u03b1\u03b9\u03cc\u03c4\u03b5\u03c1\u03b7 \u03ce\u03c1\u03b1 \u03ad\u03bd\u03b1\u03c1\u03be\u03b7\u03c2 \u03b5\u03ba\u03c4\u03ad\u03bb\u03b5\u03c3\u03b7\u03c2" } diff --git a/homeassistant/components/steam_online/translations/el.json b/homeassistant/components/steam_online/translations/el.json index 0f598dbc395..02405dc0215 100644 --- a/homeassistant/components/steam_online/translations/el.json +++ b/homeassistant/components/steam_online/translations/el.json @@ -25,6 +25,9 @@ } }, "options": { + "error": { + "unauthorized": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1 \u03c6\u03af\u03bb\u03c9\u03bd: \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03b4\u03b5\u03af\u03c4\u03b5 \u03cc\u03bb\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5\u03c2 \u03ac\u03bb\u03bb\u03bf\u03c5\u03c2 \u03c6\u03af\u03bb\u03bf\u03c5\u03c2" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/tankerkoenig/translations/ca.json b/homeassistant/components/tankerkoenig/translations/ca.json index 676bb1ccb55..4935e817ad4 100644 --- a/homeassistant/components/tankerkoenig/translations/ca.json +++ b/homeassistant/components/tankerkoenig/translations/ca.json @@ -32,7 +32,8 @@ "init": { "data": { "scan_interval": "Interval d'actualitzaci\u00f3", - "show_on_map": "Mostra les estacions al mapa" + "show_on_map": "Mostra les estacions al mapa", + "stations": "Estacions" }, "title": "Opcions de Tankerkoenig" } diff --git a/homeassistant/components/tankerkoenig/translations/el.json b/homeassistant/components/tankerkoenig/translations/el.json index 7f814b9760c..078001974ab 100644 --- a/homeassistant/components/tankerkoenig/translations/el.json +++ b/homeassistant/components/tankerkoenig/translations/el.json @@ -32,7 +32,8 @@ "init": { "data": { "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7\u03c2", - "show_on_map": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c3\u03c4\u03b1\u03b8\u03bc\u03ce\u03bd \u03c3\u03c4\u03bf \u03c7\u03ac\u03c1\u03c4\u03b7" + "show_on_map": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03c3\u03c4\u03b1\u03b8\u03bc\u03ce\u03bd \u03c3\u03c4\u03bf \u03c7\u03ac\u03c1\u03c4\u03b7", + "stations": "\u03a3\u03c4\u03b1\u03b8\u03bc\u03bf\u03af" }, "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 Tankerkoenig" } diff --git a/homeassistant/components/tankerkoenig/translations/et.json b/homeassistant/components/tankerkoenig/translations/et.json index c15e4b78ddf..b2cd4b42e06 100644 --- a/homeassistant/components/tankerkoenig/translations/et.json +++ b/homeassistant/components/tankerkoenig/translations/et.json @@ -32,7 +32,8 @@ "init": { "data": { "scan_interval": "V\u00e4rskendamise intervall", - "show_on_map": "N\u00e4ita jaamu kaardil" + "show_on_map": "N\u00e4ita jaamu kaardil", + "stations": "Tanklad" }, "title": "Tankerkoenig valikud" } diff --git a/homeassistant/components/tankerkoenig/translations/hu.json b/homeassistant/components/tankerkoenig/translations/hu.json index e2c31e9e354..369f336f96f 100644 --- a/homeassistant/components/tankerkoenig/translations/hu.json +++ b/homeassistant/components/tankerkoenig/translations/hu.json @@ -32,7 +32,8 @@ "init": { "data": { "scan_interval": "Friss\u00edt\u00e9si id\u0151k\u00f6z", - "show_on_map": "\u00c1llom\u00e1sok megjelen\u00edt\u00e9se a t\u00e9rk\u00e9pen" + "show_on_map": "\u00c1llom\u00e1sok megjelen\u00edt\u00e9se a t\u00e9rk\u00e9pen", + "stations": "\u00c1llom\u00e1sok" }, "title": "Tankerkoenig be\u00e1ll\u00edt\u00e1sok" } diff --git a/homeassistant/components/tankerkoenig/translations/id.json b/homeassistant/components/tankerkoenig/translations/id.json index cddeb02b17f..8ac50c9760f 100644 --- a/homeassistant/components/tankerkoenig/translations/id.json +++ b/homeassistant/components/tankerkoenig/translations/id.json @@ -32,7 +32,8 @@ "init": { "data": { "scan_interval": "Interval pembaruan", - "show_on_map": "Tampilkan SPBU di peta" + "show_on_map": "Tampilkan SPBU di peta", + "stations": "SPBU" }, "title": "Opsi Tankerkoenig" } diff --git a/homeassistant/components/tankerkoenig/translations/it.json b/homeassistant/components/tankerkoenig/translations/it.json index e24c353d4f1..4b4cbf6d390 100644 --- a/homeassistant/components/tankerkoenig/translations/it.json +++ b/homeassistant/components/tankerkoenig/translations/it.json @@ -32,7 +32,8 @@ "init": { "data": { "scan_interval": "Intervallo di aggiornamento", - "show_on_map": "Mostra stazioni sulla mappa" + "show_on_map": "Mostra stazioni sulla mappa", + "stations": "Stazioni" }, "title": "Opzioni Tankerkoenig" } diff --git a/homeassistant/components/totalconnect/translations/el.json b/homeassistant/components/totalconnect/translations/el.json index 4f60b082484..323fd58274c 100644 --- a/homeassistant/components/totalconnect/translations/el.json +++ b/homeassistant/components/totalconnect/translations/el.json @@ -28,5 +28,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03c7\u03b1\u03bc\u03b7\u03bb\u03ae\u03c2 \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1\u03c2" + }, + "description": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03c0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03c4\u03c9\u03bd \u03b6\u03c9\u03bd\u03ce\u03bd \u03c4\u03b7 \u03c3\u03c4\u03b9\u03b3\u03bc\u03ae \u03c0\u03bf\u03c5 \u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03bf\u03c5\u03bd \u03c7\u03b1\u03bc\u03b7\u03bb\u03ae \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1.", + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 TotalConnect" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/en.json b/homeassistant/components/ws66i/translations/en.json index fd4b170b378..30ef1e4205a 100644 --- a/homeassistant/components/ws66i/translations/en.json +++ b/homeassistant/components/ws66i/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Device is already configured" + }, "error": { "cannot_connect": "Failed to connect", "unknown": "Unexpected error" diff --git a/homeassistant/components/zha/translations/zh-Hans.json b/homeassistant/components/zha/translations/zh-Hans.json index 1d40d80d8a2..ab4b69efbb0 100644 --- a/homeassistant/components/zha/translations/zh-Hans.json +++ b/homeassistant/components/zha/translations/zh-Hans.json @@ -66,7 +66,7 @@ "device_flipped": "\u8bbe\u5907\u7ffb\u8f6c{subtype}", "device_knocked": "\u8bbe\u5907\u8f7b\u6572{subtype}", "device_offline": "\u8bbe\u5907\u79bb\u7ebf", - "device_rotated": "\u8bbe\u5907\u65cb\u8f6c{subtype}", + "device_rotated": "\u8bbe\u5907\u5411{subtype}\u65cb\u8f6c", "device_shaken": "\u8bbe\u5907\u6447\u4e00\u6447", "device_slid": "\u8bbe\u5907\u5e73\u79fb{subtype}", "device_tilted": "\u8bbe\u5907\u503e\u659c", From b9e93207e39cf23d7a14a23bdef4777ee77dc493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 30 May 2022 03:14:43 +0200 Subject: [PATCH 1018/3516] Switch severity for gesture logging (#72668) --- homeassistant/components/nanoleaf/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nanoleaf/__init__.py b/homeassistant/components/nanoleaf/__init__.py index 9e9cf1d6ca4..f6fb2f8112b 100644 --- a/homeassistant/components/nanoleaf/__init__.py +++ b/homeassistant/components/nanoleaf/__init__.py @@ -85,9 +85,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Receive touch event.""" gesture_type = TOUCH_GESTURE_TRIGGER_MAP.get(event.gesture_id) if gesture_type is None: - _LOGGER.debug("Received unknown touch gesture ID %s", event.gesture_id) + _LOGGER.warning( + "Received unknown touch gesture ID %s", event.gesture_id + ) return - _LOGGER.warning("Received touch gesture %s", gesture_type) + _LOGGER.debug("Received touch gesture %s", gesture_type) hass.bus.async_fire( NANOLEAF_EVENT, {CONF_DEVICE_ID: device_entry.id, CONF_TYPE: gesture_type}, From 75669dba6e6be226f6ea512cf09d413608e77897 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 30 May 2022 07:25:35 +0200 Subject: [PATCH 1019/3516] Use `pysnmplib` instead of `pysnmp` (#72645) * Use pysnmp and bump brother * Fix mypy errors * Bump brother version --- homeassistant/components/brother/config_flow.py | 2 +- homeassistant/components/brother/manifest.json | 2 +- homeassistant/components/snmp/manifest.json | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index 39a196aa6cb..24e7d701ed0 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -42,7 +42,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize.""" - self.brother: Brother = None + self.brother: Brother self.host: str | None = None async def async_step_user( diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index aaf1af72db9..e14079f6dd9 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.1.0"], + "requirements": ["brother==1.2.3"], "zeroconf": [ { "type": "_printer._tcp.local.", diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index 76df9e18606..1ffcb04ebda 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -2,7 +2,7 @@ "domain": "snmp", "name": "SNMP", "documentation": "https://www.home-assistant.io/integrations/snmp", - "requirements": ["pysnmp==4.4.12"], + "requirements": ["pysnmplib==5.0.15"], "codeowners": [], "iot_class": "local_polling", "loggers": ["pyasn1", "pysmi", "pysnmp"] diff --git a/requirements_all.txt b/requirements_all.txt index 93c151fd843..53e6e28788e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -433,7 +433,7 @@ bravia-tv==1.0.11 broadlink==0.18.2 # homeassistant.components.brother -brother==1.1.0 +brother==1.2.3 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 @@ -1838,7 +1838,7 @@ pysmarty==0.8 pysml==0.0.7 # homeassistant.components.snmp -pysnmp==4.4.12 +pysnmplib==5.0.15 # homeassistant.components.soma pysoma==0.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 718e876aa2f..2688d1aa5c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -330,7 +330,7 @@ bravia-tv==1.0.11 broadlink==0.18.2 # homeassistant.components.brother -brother==1.1.0 +brother==1.2.3 # homeassistant.components.brunt brunt==1.2.0 From 6e355e1074b3c72f88008ea74ccd8e93571e1bbd Mon Sep 17 00:00:00 2001 From: BigMoby Date: Mon, 30 May 2022 08:26:05 +0200 Subject: [PATCH 1020/3516] iAlarm XR integration refinements (#72616) * fixing after MartinHjelmare review * fixing after MartinHjelmare review conversion alarm state to hass state * fixing after MartinHjelmare review conversion alarm state to hass state * manage the status in the alarm control * simplyfing return function --- .../components/ialarm_xr/__init__.py | 6 ++--- .../ialarm_xr/alarm_control_panel.py | 22 ++++++++++++++++--- .../components/ialarm_xr/config_flow.py | 4 ++-- homeassistant/components/ialarm_xr/const.py | 15 ------------- .../components/ialarm_xr/manifest.json | 4 ++-- .../components/ialarm_xr/strings.json | 1 + .../components/ialarm_xr/translations/en.json | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/ialarm_xr/test_config_flow.py | 22 ++----------------- tests/components/ialarm_xr/test_init.py | 10 --------- 11 files changed, 32 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/ialarm_xr/__init__.py b/homeassistant/components/ialarm_xr/__init__.py index 9a41b5ebab7..193bbe4fffc 100644 --- a/homeassistant/components/ialarm_xr/__init__.py +++ b/homeassistant/components/ialarm_xr/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN, IALARMXR_TO_HASS +from .const import DOMAIN from .utils import async_get_ialarmxr_mac PLATFORMS = [Platform.ALARM_CONTROL_PANEL] @@ -74,7 +74,7 @@ class IAlarmXRDataUpdateCoordinator(DataUpdateCoordinator): def __init__(self, hass: HomeAssistant, ialarmxr: IAlarmXR, mac: str) -> None: """Initialize global iAlarm data updater.""" self.ialarmxr: IAlarmXR = ialarmxr - self.state: str | None = None + self.state: int | None = None self.host: str = ialarmxr.host self.mac: str = mac @@ -90,7 +90,7 @@ class IAlarmXRDataUpdateCoordinator(DataUpdateCoordinator): status: int = self.ialarmxr.get_status() _LOGGER.debug("iAlarmXR status: %s", status) - self.state = IALARMXR_TO_HASS.get(status) + self.state = status async def _async_update_data(self) -> None: """Fetch data from iAlarmXR.""" diff --git a/homeassistant/components/ialarm_xr/alarm_control_panel.py b/homeassistant/components/ialarm_xr/alarm_control_panel.py index 7b47ce3d7fa..b64edb74391 100644 --- a/homeassistant/components/ialarm_xr/alarm_control_panel.py +++ b/homeassistant/components/ialarm_xr/alarm_control_panel.py @@ -1,11 +1,19 @@ """Interfaces with iAlarmXR control panels.""" from __future__ import annotations +from pyialarmxr import IAlarmXR + from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntity, AlarmControlPanelEntityFeature, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry from homeassistant.helpers.entity import DeviceInfo @@ -15,6 +23,13 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import IAlarmXRDataUpdateCoordinator from .const import DOMAIN +IALARMXR_TO_HASS = { + IAlarmXR.ARMED_AWAY: STATE_ALARM_ARMED_AWAY, + IAlarmXR.ARMED_STAY: STATE_ALARM_ARMED_HOME, + IAlarmXR.DISARMED: STATE_ALARM_DISARMED, + IAlarmXR.TRIGGERED: STATE_ALARM_TRIGGERED, +} + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -24,7 +39,9 @@ async def async_setup_entry( async_add_entities([IAlarmXRPanel(coordinator)]) -class IAlarmXRPanel(CoordinatorEntity, AlarmControlPanelEntity): +class IAlarmXRPanel( + CoordinatorEntity[IAlarmXRDataUpdateCoordinator], AlarmControlPanelEntity +): """Representation of an iAlarmXR device.""" _attr_supported_features = ( @@ -37,7 +54,6 @@ class IAlarmXRPanel(CoordinatorEntity, AlarmControlPanelEntity): def __init__(self, coordinator: IAlarmXRDataUpdateCoordinator) -> None: """Initialize the alarm panel.""" super().__init__(coordinator) - self.coordinator: IAlarmXRDataUpdateCoordinator = coordinator self._attr_unique_id = coordinator.mac self._attr_device_info = DeviceInfo( manufacturer="Antifurto365 - Meian", @@ -48,7 +64,7 @@ class IAlarmXRPanel(CoordinatorEntity, AlarmControlPanelEntity): @property def state(self) -> str | None: """Return the state of the device.""" - return self.coordinator.state + return IALARMXR_TO_HASS.get(self.coordinator.state) def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" diff --git a/homeassistant/components/ialarm_xr/config_flow.py b/homeassistant/components/ialarm_xr/config_flow.py index 06509a82eb5..2a9cc406733 100644 --- a/homeassistant/components/ialarm_xr/config_flow.py +++ b/homeassistant/components/ialarm_xr/config_flow.py @@ -72,13 +72,13 @@ class IAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): "IAlarmXRGenericException with message: [ %s ]", ialarmxr_exception.message, ) - errors["base"] = "unknown" + errors["base"] = "cannot_connect" except IAlarmXRSocketTimeoutException as ialarmxr_socket_timeout_exception: _LOGGER.debug( "IAlarmXRSocketTimeoutException with message: [ %s ]", ialarmxr_socket_timeout_exception.message, ) - errors["base"] = "unknown" + errors["base"] = "timeout" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" diff --git a/homeassistant/components/ialarm_xr/const.py b/homeassistant/components/ialarm_xr/const.py index a208f5290b6..12122277340 100644 --- a/homeassistant/components/ialarm_xr/const.py +++ b/homeassistant/components/ialarm_xr/const.py @@ -1,18 +1,3 @@ """Constants for the iAlarmXR integration.""" -from pyialarmxr import IAlarmXR - -from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED, -) DOMAIN = "ialarm_xr" - -IALARMXR_TO_HASS = { - IAlarmXR.ARMED_AWAY: STATE_ALARM_ARMED_AWAY, - IAlarmXR.ARMED_STAY: STATE_ALARM_ARMED_HOME, - IAlarmXR.DISARMED: STATE_ALARM_DISARMED, - IAlarmXR.TRIGGERED: STATE_ALARM_TRIGGERED, -} diff --git a/homeassistant/components/ialarm_xr/manifest.json b/homeassistant/components/ialarm_xr/manifest.json index 4861e9c901f..f863f360242 100644 --- a/homeassistant/components/ialarm_xr/manifest.json +++ b/homeassistant/components/ialarm_xr/manifest.json @@ -1,8 +1,8 @@ { "domain": "ialarm_xr", "name": "Antifurto365 iAlarmXR", - "documentation": "https://www.home-assistant.io/integrations/ialarmxr", - "requirements": ["pyialarmxr==1.0.13"], + "documentation": "https://www.home-assistant.io/integrations/ialarm_xr", + "requirements": ["pyialarmxr==1.0.18"], "codeowners": ["@bigmoby"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/ialarm_xr/strings.json b/homeassistant/components/ialarm_xr/strings.json index 1650ae28c84..ea4f91fdbb9 100644 --- a/homeassistant/components/ialarm_xr/strings.json +++ b/homeassistant/components/ialarm_xr/strings.json @@ -12,6 +12,7 @@ }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout": "[%key:common::config_flow::error::timeout_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { diff --git a/homeassistant/components/ialarm_xr/translations/en.json b/homeassistant/components/ialarm_xr/translations/en.json index bf2bf989dcd..be59a5a1dc4 100644 --- a/homeassistant/components/ialarm_xr/translations/en.json +++ b/homeassistant/components/ialarm_xr/translations/en.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Failed to connect", + "timeout": "Timeout establishing connection", "unknown": "Unexpected error" }, "step": { diff --git a/requirements_all.txt b/requirements_all.txt index 53e6e28788e..c28967caa33 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1550,7 +1550,7 @@ pyhomeworks==0.0.6 pyialarm==1.9.0 # homeassistant.components.ialarm_xr -pyialarmxr==1.0.13 +pyialarmxr==1.0.18 # homeassistant.components.icloud pyicloud==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2688d1aa5c1..3025404fc62 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1038,7 +1038,7 @@ pyhomematic==0.1.77 pyialarm==1.9.0 # homeassistant.components.ialarm_xr -pyialarmxr==1.0.13 +pyialarmxr==1.0.18 # homeassistant.components.icloud pyicloud==1.0.0 diff --git a/tests/components/ialarm_xr/test_config_flow.py b/tests/components/ialarm_xr/test_config_flow.py index 22a70bda067..804249dd5cb 100644 --- a/tests/components/ialarm_xr/test_config_flow.py +++ b/tests/components/ialarm_xr/test_config_flow.py @@ -56,24 +56,6 @@ async def test_form(hass): 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} - ) - - with patch( - "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", - side_effect=ConnectionError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_DATA - ) - - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["errors"] == {"base": "cannot_connect"} - - async def test_form_exception(hass): """Test we handle unknown exception.""" result = await hass.config_entries.flow.async_init( @@ -125,7 +107,7 @@ async def test_form_cannot_connect_throwing_socket_timeout_exception(hass): ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["errors"] == {"base": "unknown"} + assert result2["errors"] == {"base": "timeout"} async def test_form_cannot_connect_throwing_generic_exception(hass): @@ -143,7 +125,7 @@ async def test_form_cannot_connect_throwing_generic_exception(hass): ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["errors"] == {"base": "unknown"} + assert result2["errors"] == {"base": "cannot_connect"} async def test_form_already_exists(hass): diff --git a/tests/components/ialarm_xr/test_init.py b/tests/components/ialarm_xr/test_init.py index 8486b7049e6..0898b6bebf8 100644 --- a/tests/components/ialarm_xr/test_init.py +++ b/tests/components/ialarm_xr/test_init.py @@ -48,16 +48,6 @@ async def test_setup_entry(hass, ialarmxr_api, mock_config_entry): assert mock_config_entry.state is ConfigEntryState.LOADED -async def test_setup_not_ready(hass, ialarmxr_api, mock_config_entry): - """Test setup failed because we can't connect to the alarm system.""" - ialarmxr_api.return_value.get_mac = Mock(side_effect=ConnectionError) - - mock_config_entry.add_to_hass(hass) - assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY - - async def test_unload_entry(hass, ialarmxr_api, mock_config_entry): """Test being able to unload an entry.""" ialarmxr_api.return_value.get_mac = Mock(return_value="00:00:54:12:34:56") From 1c334605b6dd092683303107361ea78034484608 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 May 2022 20:49:37 -1000 Subject: [PATCH 1021/3516] Enable strict typing to emulated_hue (#72676) * Add typing to emulated_hue part 2 * cleanups * adjust targets in test --- .strict-typing | 1 + .../components/emulated_hue/__init__.py | 97 ++++------ .../components/emulated_hue/config.py | 21 +-- .../components/emulated_hue/hue_api.py | 2 +- homeassistant/components/emulated_hue/upnp.py | 175 ++++++++++-------- mypy.ini | 11 ++ tests/components/emulated_hue/test_hue_api.py | 8 +- tests/components/emulated_hue/test_init.py | 8 +- tests/components/emulated_hue/test_upnp.py | 4 +- 9 files changed, 165 insertions(+), 162 deletions(-) diff --git a/.strict-typing b/.strict-typing index 7fe03203583..a2c6cd2d9da 100644 --- a/.strict-typing +++ b/.strict-typing @@ -83,6 +83,7 @@ homeassistant.components.dunehd.* homeassistant.components.efergy.* homeassistant.components.elgato.* homeassistant.components.elkm1.* +homeassistant.components.emulated_hue.* homeassistant.components.esphome.* homeassistant.components.energy.* homeassistant.components.evil_genius_labs.* diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 71f98abed80..ec06f70a3cc 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -13,7 +13,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -48,11 +48,7 @@ from .hue_api import ( HueUnauthorizedUser, HueUsernameView, ) -from .upnp import ( - DescriptionXmlView, - UPNPResponderProtocol, - create_upnp_datagram_endpoint, -) +from .upnp import DescriptionXmlView, async_create_upnp_datagram_endpoint _LOGGER = logging.getLogger(__name__) @@ -93,6 +89,40 @@ CONFIG_SCHEMA = vol.Schema( ) +async def start_emulated_hue_bridge( + hass: HomeAssistant, config: Config, app: web.Application +) -> None: + """Start the emulated hue bridge.""" + protocol = await async_create_upnp_datagram_endpoint( + config.host_ip_addr, + config.upnp_bind_multicast, + config.advertise_ip, + config.advertise_port or config.listen_port, + ) + + runner = web.AppRunner(app) + await runner.setup() + + site = web.TCPSite(runner, config.host_ip_addr, config.listen_port) + + try: + await site.start() + except OSError as error: + _LOGGER.error( + "Failed to create HTTP server at port %d: %s", config.listen_port, error + ) + protocol.close() + return + + async def stop_emulated_hue_bridge(event: Event) -> None: + """Stop the emulated hue bridge.""" + protocol.close() + await site.stop() + await runner.cleanup() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge) + + async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: """Activate the emulated_hue component.""" local_ip = await async_get_source_ip(hass) @@ -108,9 +138,6 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: app._on_startup.freeze() await app.startup() - runner = None - site = None - DescriptionXmlView(config).register(app, app.router) HueUsernameView().register(app, app.router) HueConfigView(config).register(app, app.router) @@ -122,54 +149,10 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: HueGroupView(config).register(app, app.router) HueFullStateView(config).register(app, app.router) - listen = create_upnp_datagram_endpoint( - config.host_ip_addr, - config.upnp_bind_multicast, - config.advertise_ip, - config.advertise_port or config.listen_port, - ) - protocol: UPNPResponderProtocol | None = None + async def _start(event: Event) -> None: + """Start the bridge.""" + await start_emulated_hue_bridge(hass, config, app) - async def stop_emulated_hue_bridge(event): - """Stop the emulated hue bridge.""" - nonlocal protocol - nonlocal site - nonlocal runner - - if protocol: - protocol.close() - if site: - await site.stop() - if runner: - await runner.cleanup() - - async def start_emulated_hue_bridge(event): - """Start the emulated hue bridge.""" - nonlocal protocol - nonlocal site - nonlocal runner - - transport_protocol = await listen - protocol = transport_protocol[1] - - runner = web.AppRunner(app) - await runner.setup() - - site = web.TCPSite(runner, config.host_ip_addr, config.listen_port) - - try: - await site.start() - except OSError as error: - _LOGGER.error( - "Failed to create HTTP server at port %d: %s", config.listen_port, error - ) - if protocol: - protocol.close() - else: - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, stop_emulated_hue_bridge - ) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_emulated_hue_bridge) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _start) return True diff --git a/homeassistant/components/emulated_hue/config.py b/homeassistant/components/emulated_hue/config.py index e39ec9839c8..fce521eee55 100644 --- a/homeassistant/components/emulated_hue/config.py +++ b/homeassistant/components/emulated_hue/config.py @@ -55,9 +55,7 @@ _LOGGER = logging.getLogger(__name__) class Config: """Hold configuration variables for the emulated hue bridge.""" - def __init__( - self, hass: HomeAssistant, conf: ConfigType, local_ip: str | None - ) -> None: + def __init__(self, hass: HomeAssistant, conf: ConfigType, local_ip: str) -> None: """Initialize the instance.""" self.hass = hass self.type = conf.get(CONF_TYPE) @@ -73,17 +71,10 @@ class Config: ) # Get the IP address that will be passed to the Echo during discovery - self.host_ip_addr = conf.get(CONF_HOST_IP) - if self.host_ip_addr is None: - self.host_ip_addr = local_ip + self.host_ip_addr: str = conf.get(CONF_HOST_IP) or local_ip # Get the port that the Hue bridge will listen on - self.listen_port = conf.get(CONF_LISTEN_PORT) - if not isinstance(self.listen_port, int): - self.listen_port = DEFAULT_LISTEN_PORT - _LOGGER.info( - "Listen port not specified, defaulting to %s", self.listen_port - ) + self.listen_port: int = conf.get(CONF_LISTEN_PORT) or DEFAULT_LISTEN_PORT # Get whether or not UPNP binds to multicast address (239.255.255.250) # or to the unicast address (host_ip_addr) @@ -113,11 +104,11 @@ class Config: ) # Calculated effective advertised IP and port for network isolation - self.advertise_ip = conf.get(CONF_ADVERTISE_IP) or self.host_ip_addr + self.advertise_ip: str = conf.get(CONF_ADVERTISE_IP) or self.host_ip_addr - self.advertise_port = conf.get(CONF_ADVERTISE_PORT) or self.listen_port + self.advertise_port: int = conf.get(CONF_ADVERTISE_PORT) or self.listen_port - self.entities = conf.get(CONF_ENTITIES, {}) + self.entities: dict[str, dict[str, str]] = conf.get(CONF_ENTITIES, {}) self._entities_with_hidden_attr_in_config = {} for entity_id in self.entities: diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index e7a4876730c..d6ac67b6984 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -858,7 +858,7 @@ async def wait_for_state_change_or_timeout( ev = asyncio.Event() @core.callback - def _async_event_changed(_): + def _async_event_changed(event: core.Event) -> None: ev.set() unsub = async_track_state_change_event(hass, [entity_id], _async_event_changed) diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py index 797b22c22f7..ca8c0a45281 100644 --- a/homeassistant/components/emulated_hue/upnp.py +++ b/homeassistant/components/emulated_hue/upnp.py @@ -1,13 +1,17 @@ """Support UPNP discovery method that mimics Hue hubs.""" +from __future__ import annotations + import asyncio import logging import socket +from typing import cast from aiohttp import web from homeassistant import core from homeassistant.components.http import HomeAssistantView +from .config import Config from .const import HUE_SERIAL_NUMBER, HUE_UUID _LOGGER = logging.getLogger(__name__) @@ -23,12 +27,12 @@ class DescriptionXmlView(HomeAssistantView): name = "description:xml" requires_auth = False - def __init__(self, config): + def __init__(self, config: Config) -> None: """Initialize the instance of the view.""" self.config = config @core.callback - def get(self, request): + def get(self, request: web.Request) -> web.Response: """Handle a GET request.""" resp_text = f""" @@ -55,13 +59,91 @@ class DescriptionXmlView(HomeAssistantView): return web.Response(text=resp_text, content_type="text/xml") -@core.callback -def create_upnp_datagram_endpoint( - host_ip_addr, - upnp_bind_multicast, - advertise_ip, - advertise_port, -): +class UPNPResponderProtocol(asyncio.Protocol): + """Handle responding to UPNP/SSDP discovery requests.""" + + def __init__( + self, + loop: asyncio.AbstractEventLoop, + ssdp_socket: socket.socket, + advertise_ip: str, + advertise_port: int, + ) -> None: + """Initialize the class.""" + self.transport: asyncio.DatagramTransport | None = None + self._loop = loop + self._sock = ssdp_socket + self.advertise_ip = advertise_ip + self.advertise_port = advertise_port + self._upnp_root_response = self._prepare_response( + "upnp:rootdevice", f"uuid:{HUE_UUID}::upnp:rootdevice" + ) + self._upnp_device_response = self._prepare_response( + "urn:schemas-upnp-org:device:basic:1", f"uuid:{HUE_UUID}" + ) + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + """Set the transport.""" + self.transport = cast(asyncio.DatagramTransport, transport) + + def connection_lost(self, exc: Exception | None) -> None: + """Handle connection lost.""" + + def datagram_received(self, data: bytes, addr: tuple[str, int]) -> None: + """Respond to msearch packets.""" + decoded_data = data.decode("utf-8", errors="ignore") + + if "M-SEARCH" not in decoded_data: + return + + _LOGGER.debug("UPNP Responder M-SEARCH method received: %s", data) + # SSDP M-SEARCH method received, respond to it with our info + response = self._handle_request(decoded_data) + _LOGGER.debug("UPNP Responder responding with: %s", response) + assert self.transport is not None + self.transport.sendto(response, addr) + + def error_received(self, exc: Exception) -> None: + """Log UPNP errors.""" + _LOGGER.error("UPNP Error received: %s", exc) + + def close(self) -> None: + """Stop the server.""" + _LOGGER.info("UPNP responder shutting down") + if self.transport: + self.transport.close() + self._loop.remove_writer(self._sock.fileno()) + self._loop.remove_reader(self._sock.fileno()) + self._sock.close() + + def _handle_request(self, decoded_data: str) -> bytes: + if "upnp:rootdevice" in decoded_data: + return self._upnp_root_response + + return self._upnp_device_response + + def _prepare_response(self, search_target: str, unique_service_name: str) -> bytes: + # Note that the double newline at the end of + # this string is required per the SSDP spec + response = f"""HTTP/1.1 200 OK +CACHE-CONTROL: max-age=60 +EXT: +LOCATION: http://{self.advertise_ip}:{self.advertise_port}/description.xml +SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.16.0 +hue-bridgeid: {HUE_SERIAL_NUMBER} +ST: {search_target} +USN: {unique_service_name} + +""" + return response.replace("\n", "\r\n").encode("utf-8") + + +async def async_create_upnp_datagram_endpoint( + host_ip_addr: str, + upnp_bind_multicast: bool, + advertise_ip: str, + advertise_port: int, +) -> UPNPResponderProtocol: """Create the UPNP socket and protocol.""" # Listen for UDP port 1900 packets sent to SSDP multicast address ssdp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -84,79 +166,8 @@ def create_upnp_datagram_endpoint( loop = asyncio.get_event_loop() - return loop.create_datagram_endpoint( + transport_protocol = await loop.create_datagram_endpoint( lambda: UPNPResponderProtocol(loop, ssdp_socket, advertise_ip, advertise_port), sock=ssdp_socket, ) - - -class UPNPResponderProtocol: - """Handle responding to UPNP/SSDP discovery requests.""" - - def __init__(self, loop, ssdp_socket, advertise_ip, advertise_port): - """Initialize the class.""" - self.transport = None - self._loop = loop - self._sock = ssdp_socket - self.advertise_ip = advertise_ip - self.advertise_port = advertise_port - self._upnp_root_response = self._prepare_response( - "upnp:rootdevice", f"uuid:{HUE_UUID}::upnp:rootdevice" - ) - self._upnp_device_response = self._prepare_response( - "urn:schemas-upnp-org:device:basic:1", f"uuid:{HUE_UUID}" - ) - - def connection_made(self, transport): - """Set the transport.""" - self.transport = transport - - def connection_lost(self, exc): - """Handle connection lost.""" - - def datagram_received(self, data, addr): - """Respond to msearch packets.""" - decoded_data = data.decode("utf-8", errors="ignore") - - if "M-SEARCH" not in decoded_data: - return - - _LOGGER.debug("UPNP Responder M-SEARCH method received: %s", data) - # SSDP M-SEARCH method received, respond to it with our info - response = self._handle_request(decoded_data) - _LOGGER.debug("UPNP Responder responding with: %s", response) - self.transport.sendto(response, addr) - - def error_received(self, exc): - """Log UPNP errors.""" - _LOGGER.error("UPNP Error received: %s", exc) - - def close(self): - """Stop the server.""" - _LOGGER.info("UPNP responder shutting down") - if self.transport: - self.transport.close() - self._loop.remove_writer(self._sock.fileno()) - self._loop.remove_reader(self._sock.fileno()) - self._sock.close() - - def _handle_request(self, decoded_data): - if "upnp:rootdevice" in decoded_data: - return self._upnp_root_response - - return self._upnp_device_response - - def _prepare_response(self, search_target, unique_service_name): - # Note that the double newline at the end of - # this string is required per the SSDP spec - response = f"""HTTP/1.1 200 OK -CACHE-CONTROL: max-age=60 -EXT: -LOCATION: http://{self.advertise_ip}:{self.advertise_port}/description.xml -SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.16.0 -hue-bridgeid: {HUE_SERIAL_NUMBER} -ST: {search_target} -USN: {unique_service_name} - -""" - return response.replace("\n", "\r\n").encode("utf-8") + return transport_protocol[1] diff --git a/mypy.ini b/mypy.ini index e2159766fb4..fb47638d59d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -676,6 +676,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.emulated_hue.*] +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.esphome.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index dd27eed9771..87893f66e1f 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -108,7 +108,9 @@ def hass_hue(loop, hass): ) ) - with patch("homeassistant.components.emulated_hue.create_upnp_datagram_endpoint"): + with patch( + "homeassistant.components.emulated_hue.async_create_upnp_datagram_endpoint" + ): loop.run_until_complete( setup.async_setup_component( hass, @@ -314,7 +316,9 @@ async def test_lights_all_dimmable(hass, hass_client_no_auth): emulated_hue.CONF_EXPOSE_BY_DEFAULT: True, emulated_hue.CONF_LIGHTS_ALL_DIMMABLE: True, } - with patch("homeassistant.components.emulated_hue.create_upnp_datagram_endpoint"): + with patch( + "homeassistant.components.emulated_hue.async_create_upnp_datagram_endpoint" + ): await setup.async_setup_component( hass, emulated_hue.DOMAIN, diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 024b0f3ddf7..408a44cda00 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -122,13 +122,13 @@ async def test_setup_works(hass): """Test setup works.""" hass.config.components.add("network") with patch( - "homeassistant.components.emulated_hue.create_upnp_datagram_endpoint", + "homeassistant.components.emulated_hue.async_create_upnp_datagram_endpoint", AsyncMock(), ) as mock_create_upnp_datagram_endpoint, patch( "homeassistant.components.emulated_hue.async_get_source_ip" ): assert await async_setup_component(hass, "emulated_hue", {}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - assert len(mock_create_upnp_datagram_endpoint.mock_calls) == 2 + assert len(mock_create_upnp_datagram_endpoint.mock_calls) == 1 diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 79daaadbbc9..f392cfaf90d 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -53,7 +53,9 @@ def hue_client(aiohttp_client): async def setup_hue(hass): """Set up the emulated_hue integration.""" - with patch("homeassistant.components.emulated_hue.create_upnp_datagram_endpoint"): + with patch( + "homeassistant.components.emulated_hue.async_create_upnp_datagram_endpoint" + ): assert await setup.async_setup_component( hass, emulated_hue.DOMAIN, From 8d72891d83c0ea0e258ae40217cd526637ef17ec Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Mon, 30 May 2022 08:52:58 +0200 Subject: [PATCH 1022/3516] Bump bimmer_connected to 0.9.3 (#72677) Bump bimmer_connected to 0.9.3, fix retrieved units Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/coordinator.py | 3 ++- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- homeassistant/components/bmw_connected_drive/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/coordinator.py b/homeassistant/components/bmw_connected_drive/coordinator.py index cff532ae3cb..47d1f358686 100644 --- a/homeassistant/components/bmw_connected_drive/coordinator.py +++ b/homeassistant/components/bmw_connected_drive/coordinator.py @@ -6,7 +6,7 @@ import logging from bimmer_connected.account import MyBMWAccount from bimmer_connected.api.regions import get_region_from_name -from bimmer_connected.vehicle.models import GPSPosition +from bimmer_connected.models import GPSPosition from httpx import HTTPError, TimeoutException from homeassistant.config_entries import ConfigEntry @@ -32,6 +32,7 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator): entry.data[CONF_PASSWORD], get_region_from_name(entry.data[CONF_REGION]), observer_position=GPSPosition(hass.config.latitude, hass.config.longitude), + use_metric_units=hass.config.units.is_metric, ) self.read_only = entry.options[CONF_READ_ONLY] self._entry = entry diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index c7130d12698..75ac3e982e8 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.9.2"], + "requirements": ["bimmer_connected==0.9.3"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 3021e180158..9f19673c398 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -6,8 +6,8 @@ from dataclasses import dataclass import logging from typing import cast +from bimmer_connected.models import ValueWithUnit from bimmer_connected.vehicle import MyBMWVehicle -from bimmer_connected.vehicle.models import ValueWithUnit from homeassistant.components.sensor import ( SensorDeviceClass, diff --git a/requirements_all.txt b/requirements_all.txt index c28967caa33..a7d649b4702 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -394,7 +394,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.2 +bimmer_connected==0.9.3 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3025404fc62..f461428c796 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -309,7 +309,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.2 +bimmer_connected==0.9.3 # homeassistant.components.blebox blebox_uniapi==1.3.3 From 6bc09741c7d965c2be96c647fd1932b2af4531ba Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 30 May 2022 08:54:29 +0200 Subject: [PATCH 1023/3516] Adjust config-flow type hints in gogogate2 (#72445) --- .../components/gogogate2/config_flow.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/gogogate2/config_flow.py b/homeassistant/components/gogogate2/config_flow.py index e97b62102c4..344e8473984 100644 --- a/homeassistant/components/gogogate2/config_flow.py +++ b/homeassistant/components/gogogate2/config_flow.py @@ -1,12 +1,14 @@ """Config flow for Gogogate2.""" +from __future__ import annotations + import dataclasses import re +from typing import Any from ismartgate.common import AbstractInfoResponse, ApiError from ismartgate.const import GogoGate2ApiErrorCode, ISmartGateApiErrorCode import voluptuous as vol -from homeassistant import data_entry_flow from homeassistant.components import dhcp, zeroconf from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( @@ -15,6 +17,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, ) +from homeassistant.data_entry_flow import AbortFlow, FlowResult from .common import get_api from .const import DEVICE_TYPE_GOGOGATE2, DEVICE_TYPE_ISMARTGATE, DOMAIN @@ -30,28 +33,26 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize the config flow.""" - self._ip_address = None - self._device_type = None + self._ip_address: str | None = None + self._device_type: str | None = None async def async_step_homekit( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> data_entry_flow.FlowResult: + ) -> FlowResult: """Handle homekit discovery.""" await self.async_set_unique_id( discovery_info.properties[zeroconf.ATTR_PROPERTIES_ID] ) return await self._async_discovery_handler(discovery_info.host) - async def async_step_dhcp( - self, discovery_info: dhcp.DhcpServiceInfo - ) -> data_entry_flow.FlowResult: + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" await self.async_set_unique_id(discovery_info.macaddress) return await self._async_discovery_handler(discovery_info.ip) - async def _async_discovery_handler(self, ip_address): + async def _async_discovery_handler(self, ip_address: str) -> FlowResult: """Start the user flow from any discovery.""" self.context[CONF_IP_ADDRESS] = ip_address self._abort_if_unique_id_configured({CONF_IP_ADDRESS: ip_address}) @@ -61,12 +62,14 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): self._ip_address = ip_address for progress in self._async_in_progress(): if progress.get("context", {}).get(CONF_IP_ADDRESS) == self._ip_address: - raise data_entry_flow.AbortFlow("already_in_progress") + raise AbortFlow("already_in_progress") self._device_type = DEVICE_TYPE_ISMARTGATE return await self.async_step_user() - async def async_step_user(self, user_input: dict = None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle user initiated flow.""" user_input = user_input or {} errors = {} From b417ae72e571b9b33acb591ca6da2a192fbc80f6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 30 May 2022 09:22:37 +0200 Subject: [PATCH 1024/3516] Add generic parameters to HassJob (#70973) --- homeassistant/core.py | 20 +++++++++++--------- homeassistant/helpers/event.py | 34 +++++++++++++++++++++------------- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index d7cae4e411e..b8f509abef3 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -37,6 +37,7 @@ from typing import ( ) from urllib.parse import urlparse +from typing_extensions import ParamSpec import voluptuous as vol import yarl @@ -98,6 +99,7 @@ block_async_io.enable() _T = TypeVar("_T") _R = TypeVar("_R") _R_co = TypeVar("_R_co", covariant=True) +_P = ParamSpec("_P") # Internal; not helpers.typing.UNDEFINED due to circular dependency _UNDEF: dict[Any, Any] = {} _CallableT = TypeVar("_CallableT", bound=Callable[..., Any]) @@ -182,7 +184,7 @@ class HassJobType(enum.Enum): Executor = 3 -class HassJob(Generic[_R_co]): +class HassJob(Generic[_P, _R_co]): """Represent a job to be run later. We check the callable type in advance @@ -192,7 +194,7 @@ class HassJob(Generic[_R_co]): __slots__ = ("job_type", "target") - def __init__(self, target: Callable[..., _R_co]) -> None: + def __init__(self, target: Callable[_P, _R_co]) -> None: """Create a job object.""" self.target = target self.job_type = _get_hassjob_callable_job_type(target) @@ -416,20 +418,20 @@ class HomeAssistant: @overload @callback def async_add_hass_job( - self, hassjob: HassJob[Coroutine[Any, Any, _R]], *args: Any + self, hassjob: HassJob[..., Coroutine[Any, Any, _R]], *args: Any ) -> asyncio.Future[_R] | None: ... @overload @callback def async_add_hass_job( - self, hassjob: HassJob[Coroutine[Any, Any, _R] | _R], *args: Any + self, hassjob: HassJob[..., Coroutine[Any, Any, _R] | _R], *args: Any ) -> asyncio.Future[_R] | None: ... @callback def async_add_hass_job( - self, hassjob: HassJob[Coroutine[Any, Any, _R] | _R], *args: Any + self, hassjob: HassJob[..., Coroutine[Any, Any, _R] | _R], *args: Any ) -> asyncio.Future[_R] | None: """Add a HassJob from within the event loop. @@ -512,20 +514,20 @@ class HomeAssistant: @overload @callback def async_run_hass_job( - self, hassjob: HassJob[Coroutine[Any, Any, _R]], *args: Any + self, hassjob: HassJob[..., Coroutine[Any, Any, _R]], *args: Any ) -> asyncio.Future[_R] | None: ... @overload @callback def async_run_hass_job( - self, hassjob: HassJob[Coroutine[Any, Any, _R] | _R], *args: Any + self, hassjob: HassJob[..., Coroutine[Any, Any, _R] | _R], *args: Any ) -> asyncio.Future[_R] | None: ... @callback def async_run_hass_job( - self, hassjob: HassJob[Coroutine[Any, Any, _R] | _R], *args: Any + self, hassjob: HassJob[..., Coroutine[Any, Any, _R] | _R], *args: Any ) -> asyncio.Future[_R] | None: """Run a HassJob from within the event loop. @@ -814,7 +816,7 @@ class Event: class _FilterableJob(NamedTuple): """Event listener job to be executed with optional filter.""" - job: HassJob[None | Awaitable[None]] + job: HassJob[[Event], None | Awaitable[None]] event_filter: Callable[[Event], bool] | None run_immediately: bool diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index c1229dc3e7c..c9b569c6601 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -258,7 +258,9 @@ def _async_track_state_change_event( action: Callable[[Event], Any], ) -> CALLBACK_TYPE: """async_track_state_change_event without lowercasing.""" - entity_callbacks = hass.data.setdefault(TRACK_STATE_CHANGE_CALLBACKS, {}) + entity_callbacks: dict[str, list[HassJob[[Event], Any]]] = hass.data.setdefault( + TRACK_STATE_CHANGE_CALLBACKS, {} + ) if TRACK_STATE_CHANGE_LISTENER not in hass.data: @@ -319,10 +321,10 @@ def _async_remove_indexed_listeners( data_key: str, listener_key: str, storage_keys: Iterable[str], - job: HassJob[Any], + job: HassJob[[Event], Any], ) -> None: """Remove a listener.""" - callbacks = hass.data[data_key] + callbacks: dict[str, list[HassJob[[Event], Any]]] = hass.data[data_key] for storage_key in storage_keys: callbacks[storage_key].remove(job) @@ -347,7 +349,9 @@ def async_track_entity_registry_updated_event( if not (entity_ids := _async_string_to_lower_list(entity_ids)): return _remove_empty_listener - entity_callbacks = hass.data.setdefault(TRACK_ENTITY_REGISTRY_UPDATED_CALLBACKS, {}) + entity_callbacks: dict[str, list[HassJob[[Event], Any]]] = hass.data.setdefault( + TRACK_ENTITY_REGISTRY_UPDATED_CALLBACKS, {} + ) if TRACK_ENTITY_REGISTRY_UPDATED_LISTENER not in hass.data: @@ -401,7 +405,7 @@ def async_track_entity_registry_updated_event( @callback def _async_dispatch_domain_event( - hass: HomeAssistant, event: Event, callbacks: dict[str, list[HassJob[Any]]] + hass: HomeAssistant, event: Event, callbacks: dict[str, list[HassJob[[Event], Any]]] ) -> None: domain = split_entity_id(event.data["entity_id"])[0] @@ -438,7 +442,9 @@ def _async_track_state_added_domain( action: Callable[[Event], Any], ) -> CALLBACK_TYPE: """async_track_state_added_domain without lowercasing.""" - domain_callbacks = hass.data.setdefault(TRACK_STATE_ADDED_DOMAIN_CALLBACKS, {}) + domain_callbacks: dict[str, list[HassJob[[Event], Any]]] = hass.data.setdefault( + TRACK_STATE_ADDED_DOMAIN_CALLBACKS, {} + ) if TRACK_STATE_ADDED_DOMAIN_LISTENER not in hass.data: @@ -490,7 +496,9 @@ def async_track_state_removed_domain( if not (domains := _async_string_to_lower_list(domains)): return _remove_empty_listener - domain_callbacks = hass.data.setdefault(TRACK_STATE_REMOVED_DOMAIN_CALLBACKS, {}) + domain_callbacks: dict[str, list[HassJob[[Event], Any]]] = hass.data.setdefault( + TRACK_STATE_REMOVED_DOMAIN_CALLBACKS, {} + ) if TRACK_STATE_REMOVED_DOMAIN_LISTENER not in hass.data: @@ -1249,7 +1257,7 @@ track_same_state = threaded_listener_factory(async_track_same_state) @bind_hass def async_track_point_in_time( hass: HomeAssistant, - action: HassJob[Awaitable[None] | None] + action: HassJob[[datetime], Awaitable[None] | None] | Callable[[datetime], Awaitable[None] | None], point_in_time: datetime, ) -> CALLBACK_TYPE: @@ -1271,7 +1279,7 @@ track_point_in_time = threaded_listener_factory(async_track_point_in_time) @bind_hass def async_track_point_in_utc_time( hass: HomeAssistant, - action: HassJob[Awaitable[None] | None] + action: HassJob[[datetime], Awaitable[None] | None] | Callable[[datetime], Awaitable[None] | None], point_in_time: datetime, ) -> CALLBACK_TYPE: @@ -1284,7 +1292,7 @@ def async_track_point_in_utc_time( cancel_callback: asyncio.TimerHandle | None = None @callback - def run_action(job: HassJob[Awaitable[None] | None]) -> None: + def run_action(job: HassJob[[datetime], Awaitable[None] | None]) -> None: """Call the action.""" nonlocal cancel_callback @@ -1324,7 +1332,7 @@ track_point_in_utc_time = threaded_listener_factory(async_track_point_in_utc_tim def async_call_later( hass: HomeAssistant, delay: float | timedelta, - action: HassJob[Awaitable[None] | None] + action: HassJob[[datetime], Awaitable[None] | None] | Callable[[datetime], Awaitable[None] | None], ) -> CALLBACK_TYPE: """Add a listener that is called in .""" @@ -1345,7 +1353,7 @@ def async_track_time_interval( ) -> CALLBACK_TYPE: """Add a listener that fires repetitively at every timedelta interval.""" remove: CALLBACK_TYPE - interval_listener_job: HassJob[None] + interval_listener_job: HassJob[[datetime], None] job = HassJob(action) @@ -1382,7 +1390,7 @@ class SunListener: """Helper class to help listen to sun events.""" hass: HomeAssistant = attr.ib() - job: HassJob[Awaitable[None] | None] = attr.ib() + job: HassJob[[], Awaitable[None] | None] = attr.ib() event: str = attr.ib() offset: timedelta | None = attr.ib() _unsub_sun: CALLBACK_TYPE | None = attr.ib(default=None) From 7e2f4ebd5c5547ac4a94c6b5f849737b69326338 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Mon, 30 May 2022 09:45:33 +0200 Subject: [PATCH 1025/3516] Plugwise: correct config_flow strings (#72554) --- .../components/plugwise/strings.json | 20 +------------------ .../components/plugwise/translations/en.json | 20 +------------------ 2 files changed, 2 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index a350543ee07..42dcee96196 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -1,24 +1,7 @@ { - "options": { - "step": { - "init": { - "description": "Adjust Plugwise Options", - "data": { - "scan_interval": "Scan Interval (seconds)" - } - } - } - }, "config": { "step": { "user": { - "title": "Plugwise type", - "description": "Product:", - "data": { - "flow_type": "Connection type" - } - }, - "user_gateway": { "title": "Connect to the Smile", "description": "Please enter", "data": { @@ -37,7 +20,6 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" - }, - "flow_title": "{name}" + } } } diff --git a/homeassistant/components/plugwise/translations/en.json b/homeassistant/components/plugwise/translations/en.json index 616e450cb11..48d3d2d0e46 100644 --- a/homeassistant/components/plugwise/translations/en.json +++ b/homeassistant/components/plugwise/translations/en.json @@ -9,16 +9,8 @@ "invalid_setup": "Add your Adam instead of your Anna, see the Home Assistant Plugwise integration documentation for more information", "unknown": "Unexpected error" }, - "flow_title": "{name}", "step": { "user": { - "data": { - "flow_type": "Connection type" - }, - "description": "Product:", - "title": "Plugwise type" - }, - "user_gateway": { "data": { "host": "IP Address", "password": "Smile ID", @@ -29,15 +21,5 @@ "title": "Connect to the Smile" } } - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Scan Interval (seconds)" - }, - "description": "Adjust Plugwise Options" - } - } } -} \ No newline at end of file +} From c8f677ce4c8b19c2cf1cc028f2fa27a37bf34563 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 30 May 2022 11:40:36 +0200 Subject: [PATCH 1026/3516] Bump hatasmota to 0.5.1 (#72696) --- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_cover.py | 45 ++++++++++++------- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 772105043fe..4268c4198b2 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.5.0"], + "requirements": ["hatasmota==0.5.1"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index a7d649b4702..de4bcbef89e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -795,7 +795,7 @@ hass-nabucasa==0.54.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.5.0 +hatasmota==0.5.1 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f461428c796..c236a33697b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -571,7 +571,7 @@ hangups==0.4.18 hass-nabucasa==0.54.0 # homeassistant.components.tasmota -hatasmota==0.5.0 +hatasmota==0.5.1 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py index 843fd72ecf1..06471e11757 100644 --- a/tests/components/tasmota/test_cover.py +++ b/tests/components/tasmota/test_cover.py @@ -111,7 +111,7 @@ async def test_tilt_support(hass, mqtt_mock, setup_tasmota): assert state.attributes["supported_features"] == COVER_SUPPORT -async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): +async def test_controlling_state_via_mqtt_tilt(hass, mqtt_mock, setup_tasmota): """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) config["rl"][0] = 3 @@ -281,7 +281,10 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): assert state.attributes["current_position"] == 100 -async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmota): +@pytest.mark.parametrize("tilt", ("", ',"Tilt":0')) +async def test_controlling_state_via_mqtt_inverted( + hass, mqtt_mock, setup_tasmota, tilt +): """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) config["rl"][0] = 3 @@ -310,7 +313,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/SENSOR", - '{"Shutter1":{"Position":54,"Direction":-1}}', + '{"Shutter1":{"Position":54,"Direction":-1' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "opening" @@ -319,21 +322,25 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/SENSOR", - '{"Shutter1":{"Position":100,"Direction":1}}', + '{"Shutter1":{"Position":100,"Direction":1' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "closing" assert state.attributes["current_position"] == 0 async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/SENSOR", '{"Shutter1":{"Position":0,"Direction":0}}' + hass, + "tasmota_49A3BC/tele/SENSOR", + '{"Shutter1":{"Position":0,"Direction":0' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "open" assert state.attributes["current_position"] == 100 async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/SENSOR", '{"Shutter1":{"Position":99,"Direction":0}}' + hass, + "tasmota_49A3BC/tele/SENSOR", + '{"Shutter1":{"Position":99,"Direction":0' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "open" @@ -342,7 +349,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/SENSOR", - '{"Shutter1":{"Position":100,"Direction":0}}', + '{"Shutter1":{"Position":100,"Direction":0' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "closed" @@ -352,7 +359,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"Shutter1":{"Position":54,"Direction":-1}}}', + '{"StatusSNS":{"Shutter1":{"Position":54,"Direction":-1' + tilt + "}}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "opening" @@ -361,7 +368,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"Shutter1":{"Position":100,"Direction":1}}}', + '{"StatusSNS":{"Shutter1":{"Position":100,"Direction":1' + tilt + "}}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "closing" @@ -370,7 +377,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"Shutter1":{"Position":0,"Direction":0}}}', + '{"StatusSNS":{"Shutter1":{"Position":0,"Direction":0' + tilt + "}}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "open" @@ -379,7 +386,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"Shutter1":{"Position":99,"Direction":0}}}', + '{"StatusSNS":{"Shutter1":{"Position":99,"Direction":0' + tilt + "}}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "open" @@ -388,7 +395,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"Shutter1":{"Position":100,"Direction":0}}}', + '{"StatusSNS":{"Shutter1":{"Position":100,"Direction":0' + tilt + "}}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "closed" @@ -398,7 +405,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", - '{"Shutter1":{"Position":54,"Direction":-1}}', + '{"Shutter1":{"Position":54,"Direction":-1' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "opening" @@ -407,21 +414,25 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", - '{"Shutter1":{"Position":100,"Direction":1}}', + '{"Shutter1":{"Position":100,"Direction":1' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "closing" assert state.attributes["current_position"] == 0 async_fire_mqtt_message( - hass, "tasmota_49A3BC/stat/RESULT", '{"Shutter1":{"Position":0,"Direction":0}}' + hass, + "tasmota_49A3BC/stat/RESULT", + '{"Shutter1":{"Position":0,"Direction":0' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "open" assert state.attributes["current_position"] == 100 async_fire_mqtt_message( - hass, "tasmota_49A3BC/stat/RESULT", '{"Shutter1":{"Position":1,"Direction":0}}' + hass, + "tasmota_49A3BC/stat/RESULT", + '{"Shutter1":{"Position":1,"Direction":0' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "open" @@ -430,7 +441,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", - '{"Shutter1":{"Position":100,"Direction":0}}', + '{"Shutter1":{"Position":100,"Direction":0' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "closed" From 3a0111e65de0150f3b487918944eb6067d38f139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 30 May 2022 12:00:13 +0200 Subject: [PATCH 1027/3516] Use supervisor envs instead of hassio (#72601) --- homeassistant/bootstrap.py | 2 +- homeassistant/components/hassio/__init__.py | 4 ++-- homeassistant/components/hassio/auth.py | 2 +- homeassistant/components/hassio/handler.py | 2 +- homeassistant/components/hassio/http.py | 2 +- homeassistant/components/hassio/ingress.py | 2 +- homeassistant/components/hassio/system_health.py | 4 ++-- tests/components/hassio/__init__.py | 2 +- tests/components/hassio/conftest.py | 8 ++++---- tests/components/hassio/test_binary_sensor.py | 2 +- tests/components/hassio/test_diagnostics.py | 2 +- tests/components/hassio/test_init.py | 4 ++-- tests/components/hassio/test_sensor.py | 2 +- tests/components/hassio/test_update.py | 2 +- tests/components/http/test_ban.py | 4 ++-- tests/components/onboarding/test_views.py | 4 ++-- tests/test_bootstrap.py | 2 +- 17 files changed, 25 insertions(+), 25 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 986171cbee7..eabbbb49362 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -398,7 +398,7 @@ def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]: domains.update(hass.config_entries.async_domains()) # Make sure the Hass.io component is loaded - if "HASSIO" in os.environ: + if "SUPERVISOR" in os.environ: domains.add("hassio") return domains diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 93d902e4bae..cab17c94f0c 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -505,7 +505,7 @@ def get_supervisor_ip() -> str: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901 """Set up the Hass.io component.""" # Check local setup - for env in ("HASSIO", "HASSIO_TOKEN"): + for env in ("SUPERVISOR", "SUPERVISOR_TOKEN"): if os.environ.get(env): continue _LOGGER.error("Missing %s environment variable", env) @@ -517,7 +517,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: async_load_websocket_api(hass) - host = os.environ["HASSIO"] + host = os.environ["SUPERVISOR"] websession = async_get_clientsession(hass) hass.data[DOMAIN] = hassio = HassIO(hass.loop, websession, host) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 2d76a758096..f52a8ef0617 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -42,7 +42,7 @@ class HassIOBaseAuth(HomeAssistantView): def _check_access(self, request: web.Request): """Check if this call is from Supervisor.""" # Check caller IP - hassio_ip = os.environ["HASSIO"].split(":")[0] + hassio_ip = os.environ["SUPERVISOR"].split(":")[0] if ip_address(request.transport.get_extra_info("peername")[0]) != ip_address( hassio_ip ): diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 4146753b753..ba1b3bfaf35 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -246,7 +246,7 @@ class HassIO: method, f"http://{self._ip}{command}", json=payload, - headers={X_HASSIO: os.environ.get("HASSIO_TOKEN", "")}, + headers={X_HASSIO: os.environ.get("SUPERVISOR_TOKEN", "")}, timeout=aiohttp.ClientTimeout(total=timeout), ) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 532b947ac49..63ac1521cc5 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -124,7 +124,7 @@ class HassIOView(HomeAssistantView): def _init_header(request: web.Request) -> dict[str, str]: """Create initial header.""" headers = { - X_HASSIO: os.environ.get("HASSIO_TOKEN", ""), + X_HASSIO: os.environ.get("SUPERVISOR_TOKEN", ""), CONTENT_TYPE: request.content_type, } diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 284ba42b3c1..c8b56e6f1bb 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -183,7 +183,7 @@ def _init_header(request: web.Request, token: str) -> CIMultiDict | dict[str, st headers[name] = value # Inject token / cleanup later on Supervisor - headers[X_HASSIO] = os.environ.get("HASSIO_TOKEN", "") + headers[X_HASSIO] = os.environ.get("SUPERVISOR_TOKEN", "") # Ingress information headers[X_INGRESS_PATH] = f"/api/hassio_ingress/{token}" diff --git a/homeassistant/components/hassio/system_health.py b/homeassistant/components/hassio/system_health.py index 1039a0237a8..b1fc208de80 100644 --- a/homeassistant/components/hassio/system_health.py +++ b/homeassistant/components/hassio/system_health.py @@ -6,8 +6,8 @@ from homeassistant.core import HomeAssistant, callback from . import get_host_info, get_info, get_os_info, get_supervisor_info -SUPERVISOR_PING = f"http://{os.environ['HASSIO']}/supervisor/ping" -OBSERVER_URL = f"http://{os.environ['HASSIO']}:4357" +SUPERVISOR_PING = f"http://{os.environ['SUPERVISOR']}/supervisor/ping" +OBSERVER_URL = f"http://{os.environ['SUPERVISOR']}:4357" @callback diff --git a/tests/components/hassio/__init__.py b/tests/components/hassio/__init__.py index 79520c6fd12..76aecd64098 100644 --- a/tests/components/hassio/__init__.py +++ b/tests/components/hassio/__init__.py @@ -1,2 +1,2 @@ """Tests for Hass.io component.""" -HASSIO_TOKEN = "123456" +SUPERVISOR_TOKEN = "123456" diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index 89a8c6f5c51..a6cd956c95e 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -9,16 +9,16 @@ from homeassistant.core import CoreState from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.setup import async_setup_component -from . import HASSIO_TOKEN +from . import SUPERVISOR_TOKEN @pytest.fixture def hassio_env(): """Fixture to inject hassio env.""" - with patch.dict(os.environ, {"HASSIO": "127.0.0.1"}), patch( + with patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}), patch( "homeassistant.components.hassio.HassIO.is_connected", return_value={"result": "ok", "data": {}}, - ), patch.dict(os.environ, {"HASSIO_TOKEN": HASSIO_TOKEN}), patch( + ), patch.dict(os.environ, {"SUPERVISOR_TOKEN": SUPERVISOR_TOKEN}), patch( "homeassistant.components.hassio.HassIO.get_info", Mock(side_effect=HassioAPIError()), ): @@ -75,5 +75,5 @@ def hassio_handler(hass, aioclient_mock): websession = hass.loop.run_until_complete(get_client_session()) - with patch.dict(os.environ, {"HASSIO_TOKEN": HASSIO_TOKEN}): + with patch.dict(os.environ, {"SUPERVISOR_TOKEN": SUPERVISOR_TOKEN}): yield HassIO(hass.loop, websession, "127.0.0.1") diff --git a/tests/components/hassio/test_binary_sensor.py b/tests/components/hassio/test_binary_sensor.py index 0f4691e2795..ba9bcb2afdf 100644 --- a/tests/components/hassio/test_binary_sensor.py +++ b/tests/components/hassio/test_binary_sensor.py @@ -11,7 +11,7 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"} +MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"} @pytest.fixture(autouse=True) diff --git a/tests/components/hassio/test_diagnostics.py b/tests/components/hassio/test_diagnostics.py index 7bbc768681a..1f915e17e61 100644 --- a/tests/components/hassio/test_diagnostics.py +++ b/tests/components/hassio/test_diagnostics.py @@ -13,7 +13,7 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry -MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"} +MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"} @pytest.fixture(autouse=True) diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index ff595aaa602..c47b3bfbeca 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -17,7 +17,7 @@ from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed -MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"} +MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"} @pytest.fixture() @@ -336,7 +336,7 @@ async def test_setup_core_push_timezone(hass, aioclient_mock): async def test_setup_hassio_no_additional_data(hass, aioclient_mock): """Test setup with API push default data.""" with patch.dict(os.environ, MOCK_ENVIRON), patch.dict( - os.environ, {"HASSIO_TOKEN": "123456"} + os.environ, {"SUPERVISOR_TOKEN": "123456"} ): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result diff --git a/tests/components/hassio/test_sensor.py b/tests/components/hassio/test_sensor.py index 382d804eaac..868448cec2d 100644 --- a/tests/components/hassio/test_sensor.py +++ b/tests/components/hassio/test_sensor.py @@ -11,7 +11,7 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"} +MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"} @pytest.fixture(autouse=True) diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index 14d1d06ef38..48f6d894de0 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -12,7 +12,7 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"} +MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"} @pytest.fixture(autouse=True) diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index fbd545e0506..5e482d16248 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -33,10 +33,10 @@ BANNED_IPS_WITH_SUPERVISOR = BANNED_IPS + [SUPERVISOR_IP] @pytest.fixture(name="hassio_env") def hassio_env_fixture(): """Fixture to inject hassio env.""" - with patch.dict(os.environ, {"HASSIO": "127.0.0.1"}), patch( + with patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}), patch( "homeassistant.components.hassio.HassIO.is_connected", return_value={"result": "ok", "data": {}}, - ), patch.dict(os.environ, {"HASSIO_TOKEN": "123456"}): + ), patch.dict(os.environ, {"SUPERVISOR_TOKEN": "123456"}): yield diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 025459e73b7..982f5b86e65 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -57,7 +57,7 @@ async def mock_supervisor_fixture(hass, aioclient_mock): """Mock supervisor.""" aioclient_mock.post("http://127.0.0.1/homeassistant/options", json={"result": "ok"}) aioclient_mock.post("http://127.0.0.1/supervisor/options", json={"result": "ok"}) - with patch.dict(os.environ, {"HASSIO": "127.0.0.1"}), patch( + with patch.dict(os.environ, {"SUPERVISOR": "127.0.0.1"}), patch( "homeassistant.components.hassio.HassIO.is_connected", return_value=True, ), patch( @@ -79,7 +79,7 @@ async def mock_supervisor_fixture(hass, aioclient_mock): "homeassistant.components.hassio.HassIO.get_ingress_panels", return_value={"panels": {}}, ), patch.dict( - os.environ, {"HASSIO_TOKEN": "123456"} + os.environ, {"SUPERVISOR_TOKEN": "123456"} ): yield diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index c6c507a0f73..232d8fb6bbf 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -84,7 +84,7 @@ async def test_load_hassio(hass): with patch.dict(os.environ, {}, clear=True): assert bootstrap._get_domains(hass, {}) == set() - with patch.dict(os.environ, {"HASSIO": "1"}): + with patch.dict(os.environ, {"SUPERVISOR": "1"}): assert bootstrap._get_domains(hass, {}) == {"hassio"} From 42bcd0263ca3357d40e9c29cde9ab706494e841a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 May 2022 03:38:52 -0700 Subject: [PATCH 1028/3516] Allow removing a ring device (#72665) --- homeassistant/components/ring/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index a1ed1ac017b..0d8f87eef3c 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -16,6 +16,7 @@ from ring_doorbell import Auth, Ring from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform, __version__ from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from homeassistant.util.async_ import run_callback_threadsafe @@ -146,6 +147,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove a config entry from a device.""" + return True + + class GlobalDataUpdater: """Data storage for single API endpoint.""" From 342ccb5bf1d0c134b4aea2309764566c7ea296e1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 30 May 2022 14:21:20 +0200 Subject: [PATCH 1029/3516] Improve handling of MQTT overridden settings (#72698) * Improve handling of MQTT overridden settings * Don't warn unless config entry overrides yaml --- homeassistant/components/mqtt/__init__.py | 13 +++++++------ tests/components/mqtt/test_init.py | 5 ----- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 78f64387435..1728dd7f2c7 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -685,14 +685,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # User has configuration.yaml config, warn about config entry overrides elif any(key in conf for key in entry.data): shared_keys = conf.keys() & entry.data.keys() - override = {k: entry.data[k] for k in shared_keys} + override = {k: entry.data[k] for k in shared_keys if conf[k] != entry.data[k]} if CONF_PASSWORD in override: override[CONF_PASSWORD] = "********" - _LOGGER.warning( - "Deprecated configuration settings found in configuration.yaml. " - "These settings from your configuration entry will override: %s", - override, - ) + if override: + _LOGGER.warning( + "Deprecated configuration settings found in configuration.yaml. " + "These settings from your configuration entry will override: %s", + override, + ) # Merge advanced configuration values from configuration.yaml conf = _merge_extended_config(entry, conf) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index a370bd67ec1..07c39d70df0 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1715,11 +1715,6 @@ async def test_update_incomplete_entry( "The 'broker' option is deprecated, please remove it from your configuration" in caplog.text ) - assert ( - "Deprecated configuration settings found in configuration.yaml. These settings " - "from your configuration entry will override: {'broker': 'yaml_broker'}" - in caplog.text - ) # Discover a device to verify the entry was setup correctly async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) From 84243cf560fef865d51167256c7b2c0d09ad5b8d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 30 May 2022 14:25:36 +0200 Subject: [PATCH 1030/3516] Tweak MQTT hassio discovery flow (#72699) --- homeassistant/components/mqtt/config_flow.py | 1 - tests/components/mqtt/test_config_flow.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 6697b17dfdc..0a763e850e5 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -120,7 +120,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_PORT: data[CONF_PORT], CONF_USERNAME: data.get(CONF_USERNAME), CONF_PASSWORD: data.get(CONF_PASSWORD), - CONF_PROTOCOL: data.get(CONF_PROTOCOL), CONF_DISCOVERY: DEFAULT_DISCOVERY, }, ) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index f0e02ad8a3a..565fa7fda53 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -235,7 +235,8 @@ async def test_hassio_confirm(hass, mock_try_connection_success, mock_finish_set "port": 1883, "username": "mock-user", "password": "mock-pass", - "protocol": "3.1.1", + "protocol": "3.1.1", # Set by the addon's discovery, ignored by HA + "ssl": False, # Set by the addon's discovery, ignored by HA } ), context={"source": config_entries.SOURCE_HASSIO}, @@ -255,7 +256,6 @@ async def test_hassio_confirm(hass, mock_try_connection_success, mock_finish_set "port": 1883, "username": "mock-user", "password": "mock-pass", - "protocol": "3.1.1", "discovery": True, } # Check we tried the connection From b7040efef6f07c38c39fed27cddb3ec9d6e34778 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 30 May 2022 14:26:01 +0200 Subject: [PATCH 1031/3516] Cleanup and use new MQTT_BASE_SCHEMA constants (#72283) * Use new MQTT_BASE_SCHEMA constants * Update constants for mqtt_room and manual_mqtt * Revert removing platform key --- .../manual_mqtt/alarm_control_panel.py | 2 +- homeassistant/components/mqtt/__init__.py | 18 ------------------ .../components/mqtt/device_automation.py | 6 ++++-- .../components/mqtt/device_trigger.py | 19 +++++++++++++------ homeassistant/components/mqtt/tag.py | 2 +- homeassistant/components/mqtt_room/sensor.py | 2 +- 6 files changed, 20 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index 730d7ae1f9e..5b74af49a91 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -110,7 +110,7 @@ def _state_schema(state): PLATFORM_SCHEMA = vol.Schema( vol.All( - mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( + mqtt.MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "manual_mqtt", vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 1728dd7f2c7..46eb7052f4f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -261,24 +261,6 @@ SCHEMA_BASE = { MQTT_BASE_SCHEMA = vol.Schema(SCHEMA_BASE) -# Will be removed when all platforms support a modern platform schema -MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE) -# Will be removed when all platforms support a modern platform schema -MQTT_RO_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - } -) -# Will be removed when all platforms support a modern platform schema -MQTT_RW_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, - } -) - # Sensor type platforms subscribe to MQTT events MQTT_RO_SCHEMA = MQTT_BASE_SCHEMA.extend( { diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py index cafbd66b098..002ae6e3991 100644 --- a/homeassistant/components/mqtt/device_automation.py +++ b/homeassistant/components/mqtt/device_automation.py @@ -3,6 +3,8 @@ import functools import voluptuous as vol +import homeassistant.helpers.config_validation as cv + from . import device_trigger from .. import mqtt from .mixins import async_setup_entry_helper @@ -12,10 +14,10 @@ AUTOMATION_TYPES = [AUTOMATION_TYPE_TRIGGER] AUTOMATION_TYPES_SCHEMA = vol.In(AUTOMATION_TYPES) CONF_AUTOMATION_TYPE = "automation_type" -PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( {vol.Required(CONF_AUTOMATION_TYPE): AUTOMATION_TYPES_SCHEMA}, extra=vol.ALLOW_EXTRA, -) +).extend(mqtt.MQTT_BASE_SCHEMA.schema) async def async_setup_entry(hass, config_entry): diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 42ffcee1644..2c6c6ecc3ba 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -30,7 +30,14 @@ from homeassistant.helpers.typing import ConfigType from . import debug_info, trigger as mqtt_trigger from .. import mqtt -from .const import ATTR_DISCOVERY_HASH, CONF_PAYLOAD, CONF_QOS, CONF_TOPIC, DOMAIN +from .const import ( + ATTR_DISCOVERY_HASH, + CONF_ENCODING, + CONF_PAYLOAD, + CONF_QOS, + CONF_TOPIC, + DOMAIN, +) from .discovery import MQTT_DISCOVERY_DONE from .mixins import ( MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -64,7 +71,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( } ) -TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( +TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_AUTOMATION_TYPE): str, vol.Required(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -94,10 +101,10 @@ class TriggerInstance: async def async_attach_trigger(self) -> None: """Attach MQTT trigger.""" mqtt_config = { - mqtt_trigger.CONF_PLATFORM: mqtt.DOMAIN, - mqtt_trigger.CONF_TOPIC: self.trigger.topic, - mqtt_trigger.CONF_ENCODING: DEFAULT_ENCODING, - mqtt_trigger.CONF_QOS: self.trigger.qos, + CONF_PLATFORM: mqtt.DOMAIN, + CONF_TOPIC: self.trigger.topic, + CONF_ENCODING: DEFAULT_ENCODING, + CONF_QOS: self.trigger.qos, } if self.trigger.payload: mqtt_config[CONF_PAYLOAD] = self.trigger.payload diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 5bfbbd73bce..25e49524b8f 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -30,7 +30,7 @@ LOG_NAME = "Tag" TAG = "tag" TAGS = "mqtt_tags" -PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = mqtt.MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_PLATFORM): "mqtt", diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index 8f7455eb998..54de561c11e 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_AWAY_TIMEOUT, default=DEFAULT_AWAY_TIMEOUT): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, } -).extend(mqtt.MQTT_RO_PLATFORM_SCHEMA.schema) +).extend(mqtt.MQTT_RO_SCHEMA.schema) MQTT_PAYLOAD = vol.Schema( vol.All( From ce94168c50fadd5549516db99d632c3e0de6fe22 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Mon, 30 May 2022 16:25:02 +0300 Subject: [PATCH 1032/3516] Remove YAML support for glances (#72706) --- homeassistant/components/glances/__init__.py | 52 ++----------------- .../components/glances/config_flow.py | 5 -- 2 files changed, 3 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index 6272015e73c..571214deb20 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -3,17 +3,12 @@ from datetime import timedelta import logging from glances_api import Glances, exceptions -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_NAME, - CONF_PASSWORD, - CONF_PORT, CONF_SCAN_INTERVAL, - CONF_SSL, - CONF_USERNAME, CONF_VERIFY_SSL, Platform, ) @@ -23,55 +18,14 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.httpx_client import get_async_client -from homeassistant.helpers.typing import ConfigType -from .const import ( - CONF_VERSION, - DATA_UPDATED, - DEFAULT_HOST, - DEFAULT_NAME, - DEFAULT_PORT, - DEFAULT_SCAN_INTERVAL, - DEFAULT_VERSION, - DOMAIN, -) +from .const import DATA_UPDATED, DEFAULT_SCAN_INTERVAL, DOMAIN _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR] -GLANCES_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): vol.In([2, 3]), - } - ) -) - -CONFIG_SCHEMA = vol.Schema( - vol.All(cv.deprecated(DOMAIN), {DOMAIN: vol.All(cv.ensure_list, [GLANCES_SCHEMA])}), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Configure Glances using config flow only.""" - if DOMAIN in config: - for entry in config[DOMAIN]: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=entry - ) - ) - - return True +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/glances/config_flow.py b/homeassistant/components/glances/config_flow.py index 1ee7c2fa476..72bfa6dd917 100644 --- a/homeassistant/components/glances/config_flow.py +++ b/homeassistant/components/glances/config_flow.py @@ -82,11 +82,6 @@ class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_import(self, import_config): - """Import from Glances sensor config.""" - - return await self.async_step_user(user_input=import_config) - class GlancesOptionsFlowHandler(config_entries.OptionsFlow): """Handle Glances client options.""" From dd5b1681e7bbc26da963809b861dbe20edf5d287 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Mon, 30 May 2022 16:34:28 +0300 Subject: [PATCH 1033/3516] Remove YAML configuration from mikrotik (#72581) --- homeassistant/components/mikrotik/__init__.py | 65 +---------------- .../components/mikrotik/config_flow.py | 8 --- tests/components/mikrotik/__init__.py | 33 ++++++--- tests/components/mikrotik/test_config_flow.py | 70 ++++++++----------- tests/components/mikrotik/test_hub.py | 26 ++++--- 5 files changed, 73 insertions(+), 129 deletions(-) diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index 1ef250a3f4e..25aa2eb1468 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -1,71 +1,12 @@ """The Mikrotik component.""" -import voluptuous as vol - -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - CONF_VERIFY_SSL, -) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, device_registry as dr -from homeassistant.helpers.typing import ConfigType -from .const import ( - ATTR_MANUFACTURER, - CONF_ARP_PING, - CONF_DETECTION_TIME, - CONF_FORCE_DHCP, - DEFAULT_API_PORT, - DEFAULT_DETECTION_TIME, - DEFAULT_NAME, - DOMAIN, - PLATFORMS, -) +from .const import ATTR_MANUFACTURER, DOMAIN, PLATFORMS from .hub import MikrotikHub -MIKROTIK_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_API_PORT): cv.port, - vol.Optional(CONF_VERIFY_SSL, default=False): cv.boolean, - vol.Optional(CONF_ARP_PING, default=False): cv.boolean, - vol.Optional(CONF_FORCE_DHCP, default=False): cv.boolean, - vol.Optional( - CONF_DETECTION_TIME, default=DEFAULT_DETECTION_TIME - ): cv.time_period, - } - ) -) - - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), {DOMAIN: vol.All(cv.ensure_list, [MIKROTIK_SCHEMA])} - ), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Import the Mikrotik component from config.""" - - if DOMAIN in config: - for entry in config[DOMAIN]: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=entry - ) - ) - - return True +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/mikrotik/config_flow.py b/homeassistant/components/mikrotik/config_flow.py index 922df221d5a..11117d22842 100644 --- a/homeassistant/components/mikrotik/config_flow.py +++ b/homeassistant/components/mikrotik/config_flow.py @@ -74,14 +74,6 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, import_config): - """Import Miktortik from config.""" - - import_config[CONF_DETECTION_TIME] = import_config[ - CONF_DETECTION_TIME - ].total_seconds() - return await self.async_step_user(user_input=import_config) - class MikrotikOptionsFlowHandler(config_entries.OptionsFlow): """Handle Mikrotik options.""" diff --git a/tests/components/mikrotik/__init__.py b/tests/components/mikrotik/__init__.py index ae8013eff4b..885fc9c8d83 100644 --- a/tests/components/mikrotik/__init__.py +++ b/tests/components/mikrotik/__init__.py @@ -1,19 +1,32 @@ """Tests for the Mikrotik component.""" -from homeassistant.components import mikrotik +from homeassistant.components.mikrotik.const import ( + CONF_ARP_PING, + CONF_DETECTION_TIME, + CONF_FORCE_DHCP, + DEFAULT_DETECTION_TIME, +) +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) MOCK_DATA = { - mikrotik.CONF_NAME: "Mikrotik", - mikrotik.CONF_HOST: "0.0.0.0", - mikrotik.CONF_USERNAME: "user", - mikrotik.CONF_PASSWORD: "pass", - mikrotik.CONF_PORT: 8278, - mikrotik.CONF_VERIFY_SSL: False, + CONF_NAME: "Mikrotik", + CONF_HOST: "0.0.0.0", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_PORT: 8278, + CONF_VERIFY_SSL: False, } MOCK_OPTIONS = { - mikrotik.CONF_ARP_PING: False, - mikrotik.const.CONF_FORCE_DHCP: False, - mikrotik.CONF_DETECTION_TIME: mikrotik.DEFAULT_DETECTION_TIME, + CONF_ARP_PING: False, + CONF_FORCE_DHCP: False, + CONF_DETECTION_TIME: DEFAULT_DETECTION_TIME, } DEVICE_1_DHCP = { diff --git a/tests/components/mikrotik/test_config_flow.py b/tests/components/mikrotik/test_config_flow.py index 411408e8c98..b4c087a436d 100644 --- a/tests/components/mikrotik/test_config_flow.py +++ b/tests/components/mikrotik/test_config_flow.py @@ -6,7 +6,12 @@ import librouteros import pytest from homeassistant import config_entries, data_entry_flow -from homeassistant.components import mikrotik +from homeassistant.components.mikrotik.const import ( + CONF_ARP_PING, + CONF_DETECTION_TIME, + CONF_FORCE_DHCP, + DOMAIN, +) from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -34,9 +39,9 @@ DEMO_CONFIG = { CONF_PASSWORD: "password", CONF_PORT: 8278, CONF_VERIFY_SSL: False, - mikrotik.const.CONF_FORCE_DHCP: False, - mikrotik.CONF_ARP_PING: False, - mikrotik.CONF_DETECTION_TIME: timedelta(seconds=30), + CONF_FORCE_DHCP: False, + CONF_ARP_PING: False, + CONF_DETECTION_TIME: timedelta(seconds=30), } DEMO_CONFIG_ENTRY = { @@ -46,9 +51,9 @@ DEMO_CONFIG_ENTRY = { CONF_PASSWORD: "password", CONF_PORT: 8278, CONF_VERIFY_SSL: False, - mikrotik.const.CONF_FORCE_DHCP: False, - mikrotik.CONF_ARP_PING: False, - mikrotik.CONF_DETECTION_TIME: 30, + CONF_FORCE_DHCP: False, + CONF_ARP_PING: False, + CONF_DETECTION_TIME: 30, } @@ -78,29 +83,11 @@ def mock_api_connection_error(): yield -async def test_import(hass, api): - """Test import step.""" - result = await hass.config_entries.flow.async_init( - mikrotik.DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=DEMO_CONFIG, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "Home router" - assert result["data"][CONF_NAME] == "Home router" - assert result["data"][CONF_HOST] == "0.0.0.0" - assert result["data"][CONF_USERNAME] == "username" - assert result["data"][CONF_PASSWORD] == "password" - assert result["data"][CONF_PORT] == 8278 - assert result["data"][CONF_VERIFY_SSL] is False - - async def test_flow_works(hass, api): """Test config flow.""" result = await hass.config_entries.flow.async_init( - mikrotik.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -118,11 +105,14 @@ async def test_flow_works(hass, api): assert result["data"][CONF_PORT] == 8278 -async def test_options(hass): +async def test_options(hass, api): """Test updating options.""" - entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=DEMO_CONFIG_ENTRY) + entry = MockConfigEntry(domain=DOMAIN, data=DEMO_CONFIG_ENTRY) entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(entry.entry_id) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -131,28 +121,28 @@ async def test_options(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - mikrotik.CONF_DETECTION_TIME: 30, - mikrotik.CONF_ARP_PING: True, - mikrotik.const.CONF_FORCE_DHCP: False, + CONF_DETECTION_TIME: 30, + CONF_ARP_PING: True, + CONF_FORCE_DHCP: False, }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == { - mikrotik.CONF_DETECTION_TIME: 30, - mikrotik.CONF_ARP_PING: True, - mikrotik.const.CONF_FORCE_DHCP: False, + CONF_DETECTION_TIME: 30, + CONF_ARP_PING: True, + CONF_FORCE_DHCP: False, } async def test_host_already_configured(hass, auth_error): """Test host already configured.""" - entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=DEMO_CONFIG_ENTRY) + entry = MockConfigEntry(domain=DOMAIN, data=DEMO_CONFIG_ENTRY) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - mikrotik.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT @@ -164,13 +154,13 @@ async def test_host_already_configured(hass, auth_error): async def test_name_exists(hass, api): """Test name already configured.""" - entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=DEMO_CONFIG_ENTRY) + entry = MockConfigEntry(domain=DOMAIN, data=DEMO_CONFIG_ENTRY) entry.add_to_hass(hass) user_input = DEMO_USER_INPUT.copy() user_input[CONF_HOST] = "0.0.0.1" result = await hass.config_entries.flow.async_init( - mikrotik.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=user_input @@ -184,7 +174,7 @@ async def test_connection_error(hass, conn_error): """Test error when connection is unsuccessful.""" result = await hass.config_entries.flow.async_init( - mikrotik.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT @@ -197,7 +187,7 @@ async def test_wrong_credentials(hass, auth_error): """Test error when credentials are wrong.""" result = await hass.config_entries.flow.async_init( - mikrotik.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT diff --git a/tests/components/mikrotik/test_hub.py b/tests/components/mikrotik/test_hub.py index 2159b58293b..2116d73826f 100644 --- a/tests/components/mikrotik/test_hub.py +++ b/tests/components/mikrotik/test_hub.py @@ -5,6 +5,14 @@ import librouteros from homeassistant import config_entries from homeassistant.components import mikrotik +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) from . import ARP_DATA, DHCP_DATA, MOCK_DATA, MOCK_OPTIONS, WIRELESS_DATA @@ -56,17 +64,17 @@ async def test_hub_setup_successful(hass): hub = await setup_mikrotik_entry(hass) assert hub.config_entry.data == { - mikrotik.CONF_NAME: "Mikrotik", - mikrotik.CONF_HOST: "0.0.0.0", - mikrotik.CONF_USERNAME: "user", - mikrotik.CONF_PASSWORD: "pass", - mikrotik.CONF_PORT: 8278, - mikrotik.CONF_VERIFY_SSL: False, + CONF_NAME: "Mikrotik", + CONF_HOST: "0.0.0.0", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_PORT: 8278, + CONF_VERIFY_SSL: False, } assert hub.config_entry.options == { - mikrotik.hub.CONF_FORCE_DHCP: False, - mikrotik.CONF_ARP_PING: False, - mikrotik.CONF_DETECTION_TIME: 300, + mikrotik.const.CONF_FORCE_DHCP: False, + mikrotik.const.CONF_ARP_PING: False, + mikrotik.const.CONF_DETECTION_TIME: 300, } assert hub.api.available is True From c10a52305539869493d158ade3ba32884f352ceb Mon Sep 17 00:00:00 2001 From: rappenze Date: Mon, 30 May 2022 15:53:57 +0200 Subject: [PATCH 1034/3516] Sync fibaro entity visible state (#72379) --- homeassistant/components/fibaro/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index fdb0f894a40..bd7c3a09ec0 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -490,6 +490,9 @@ class FibaroDevice(Entity): self.ha_id = fibaro_device.ha_id self._attr_name = fibaro_device.friendly_name self._attr_unique_id = fibaro_device.unique_id_str + # propagate hidden attribute set in fibaro home center to HA + if "visible" in fibaro_device and fibaro_device.visible is False: + self._attr_entity_registry_visible_default = False async def async_added_to_hass(self): """Call when entity is added to hass.""" From 30e71dd96f6fdfa875b2d209f67c10252e5d832a Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 30 May 2022 09:09:14 -0500 Subject: [PATCH 1035/3516] Add support for Sonos loudness switch (#72572) --- homeassistant/components/sonos/speaker.py | 4 ++++ homeassistant/components/sonos/switch.py | 4 ++++ tests/components/sonos/conftest.py | 1 + tests/components/sonos/test_switch.py | 5 +++++ 4 files changed, 14 insertions(+) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 5d4199ec905..bd217cc9029 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -137,6 +137,7 @@ class SonosSpeaker: self.cross_fade: bool | None = None self.bass: int | None = None self.treble: int | None = None + self.loudness: bool | None = None # Home theater self.audio_delay: int | None = None @@ -506,6 +507,9 @@ class SonosSpeaker: if "mute" in variables: self.muted = variables["mute"]["Master"] == "1" + if loudness := variables.get("loudness"): + self.loudness = loudness["Master"] == "1" + for bool_var in ( "dialog_level", "night_mode", diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index b5fea08e418..29abb097df7 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -38,6 +38,7 @@ ATTR_VOLUME = "volume" ATTR_INCLUDE_LINKED_ZONES = "include_linked_zones" ATTR_CROSSFADE = "cross_fade" +ATTR_LOUDNESS = "loudness" ATTR_NIGHT_SOUND = "night_mode" ATTR_SPEECH_ENHANCEMENT = "dialog_level" ATTR_STATUS_LIGHT = "status_light" @@ -48,6 +49,7 @@ ATTR_TOUCH_CONTROLS = "buttons_enabled" ALL_FEATURES = ( ATTR_TOUCH_CONTROLS, ATTR_CROSSFADE, + ATTR_LOUDNESS, ATTR_NIGHT_SOUND, ATTR_SPEECH_ENHANCEMENT, ATTR_SUB_ENABLED, @@ -64,6 +66,7 @@ POLL_REQUIRED = ( FRIENDLY_NAMES = { ATTR_CROSSFADE: "Crossfade", + ATTR_LOUDNESS: "Loudness", ATTR_NIGHT_SOUND: "Night Sound", ATTR_SPEECH_ENHANCEMENT: "Speech Enhancement", ATTR_STATUS_LIGHT: "Status Light", @@ -73,6 +76,7 @@ FRIENDLY_NAMES = { } FEATURE_ICONS = { + ATTR_LOUDNESS: "mdi:bullhorn-variant", ATTR_NIGHT_SOUND: "mdi:chat-sleep", ATTR_SPEECH_ENHANCEMENT: "mdi:ear-hearing", ATTR_CROSSFADE: "mdi:swap-horizontal", diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 8c804f466d4..d493c9d50c9 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -108,6 +108,7 @@ def soco_fixture( mock_soco.mute = False mock_soco.night_mode = True mock_soco.dialog_level = True + mock_soco.loudness = True mock_soco.volume = 19 mock_soco.audio_delay = 2 mock_soco.bass = 1 diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index 586ccf213b8..f224a1e187e 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -29,6 +29,7 @@ async def test_entity_registry(hass, async_autosetup_sonos): assert "media_player.zone_a" in entity_registry.entities assert "switch.sonos_alarm_14" in entity_registry.entities assert "switch.zone_a_status_light" in entity_registry.entities + assert "switch.zone_a_loudness" in entity_registry.entities assert "switch.zone_a_night_sound" in entity_registry.entities assert "switch.zone_a_speech_enhancement" in entity_registry.entities assert "switch.zone_a_subwoofer_enabled" in entity_registry.entities @@ -55,6 +56,10 @@ async def test_switch_attributes(hass, async_autosetup_sonos, soco): night_sound_state = hass.states.get(night_sound.entity_id) assert night_sound_state.state == STATE_ON + loudness = entity_registry.entities["switch.zone_a_loudness"] + loudness_state = hass.states.get(loudness.entity_id) + assert loudness_state.state == STATE_ON + speech_enhancement = entity_registry.entities["switch.zone_a_speech_enhancement"] speech_enhancement_state = hass.states.get(speech_enhancement.entity_id) assert speech_enhancement_state.state == STATE_ON From 3d19d2d24fe4a54e2032f2758bbefac9fcff52ff Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 30 May 2022 16:29:47 +0200 Subject: [PATCH 1036/3516] Adjust config flow type hints in withings (#72504) --- homeassistant/components/withings/config_flow.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index b447973af97..a4ac6597248 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -1,12 +1,15 @@ """Config flow for Withings.""" from __future__ import annotations +from collections.abc import Mapping import logging +from typing import Any import voluptuous as vol from withings_api.common import AuthScope from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util import slugify @@ -29,7 +32,7 @@ class WithingsFlowHandler( return logging.getLogger(__name__) @property - def extra_authorize_data(self) -> dict: + def extra_authorize_data(self) -> dict[str, str]: """Extra data that needs to be appended to the authorize url.""" return { "scope": ",".join( @@ -42,12 +45,12 @@ class WithingsFlowHandler( ) } - async def async_oauth_create_entry(self, data: dict) -> dict: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Override the create entry so user can select a profile.""" self._current_data = data return await self.async_step_profile(data) - async def async_step_profile(self, data: dict) -> dict: + async def async_step_profile(self, data: dict[str, Any]) -> FlowResult: """Prompt the user to select a user profile.""" errors = {} reauth_profile = ( @@ -77,7 +80,7 @@ class WithingsFlowHandler( errors=errors, ) - async def async_step_reauth(self, data: dict = None) -> dict: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Prompt user to re-authenticate.""" if data is not None: return await self.async_step_user() @@ -91,7 +94,7 @@ class WithingsFlowHandler( description_placeholders=placeholders, ) - async def async_step_finish(self, data: dict) -> dict: + async def async_step_finish(self, data: dict[str, Any]) -> FlowResult: """Finish the flow.""" self._current_data = {} From c48591ff29360ccf935d745da99f2d3740b058fb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 30 May 2022 16:30:11 +0200 Subject: [PATCH 1037/3516] Adjust config-flow type hints in denonavr (#72477) --- homeassistant/components/denonavr/config_flow.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py index 238c87bbf5e..fe6c05b3aca 100644 --- a/homeassistant/components/denonavr/config_flow.py +++ b/homeassistant/components/denonavr/config_flow.py @@ -48,7 +48,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Init object.""" self.config_entry = config_entry - async def async_step_init(self, user_input: dict[str, Any] | None = None): + 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) @@ -86,7 +88,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize the Denon AVR flow.""" self.host = None self.serial_number = None @@ -105,7 +107,9 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow.""" return OptionsFlowHandler(config_entry) - async def async_step_user(self, user_input: dict[str, Any] | None = None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: From 5273e3ea9d7cb7c5e5f1ed262db98d2695a4c0bb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 30 May 2022 16:30:41 +0200 Subject: [PATCH 1038/3516] Adjust config-flow type hints in motion_blinds (#72444) --- .../components/motion_blinds/config_flow.py | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py index e40f22296cb..d861c989ee0 100644 --- a/homeassistant/components/motion_blinds/config_flow.py +++ b/homeassistant/components/motion_blinds/config_flow.py @@ -1,4 +1,8 @@ """Config flow to configure Motion Blinds using their WLAN API.""" +from __future__ import annotations + +from typing import Any + from motionblinds import MotionDiscovery import voluptuous as vol @@ -33,9 +37,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Init object.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the options.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -60,15 +66,17 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize the Motion Blinds flow.""" - self._host = None - self._ips = [] + self._host: str | None = None + self._ips: list[str] = [] self._config_settings = None @staticmethod @callback - def async_get_options_flow(config_entry) -> OptionsFlowHandler: + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow.""" return OptionsFlowHandler(config_entry) @@ -87,7 +95,9 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._host = discovery_info.ip return await self.async_step_connect() - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: @@ -114,7 +124,9 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=CONFIG_SCHEMA, errors=errors ) - async def async_step_select(self, user_input=None): + async def async_step_select( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle multiple motion gateways found.""" if user_input is not None: self._host = user_input["select_ip"] @@ -124,9 +136,11 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="select", data_schema=select_schema) - async def async_step_connect(self, user_input=None): + async def async_step_connect( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Connect to the Motion Gateway.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: key = user_input[CONF_API_KEY] From f2fde5c1f91f408c2d761f9b548ee5451e3fa654 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 30 May 2022 16:35:50 +0200 Subject: [PATCH 1039/3516] Adjust config-flow type hints in sharkiq (#72688) --- .../components/sharkiq/config_flow.py | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sharkiq/config_flow.py b/homeassistant/components/sharkiq/config_flow.py index 4875e2b25e1..b0aae5259dd 100644 --- a/homeassistant/components/sharkiq/config_flow.py +++ b/homeassistant/components/sharkiq/config_flow.py @@ -2,6 +2,8 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping +from typing import Any import aiohttp import async_timeout @@ -10,6 +12,7 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN, LOGGER @@ -19,7 +22,9 @@ SHARKIQ_SCHEMA = vol.Schema( ) -async def validate_input(hass: core.HomeAssistant, data): +async def _validate_input( + hass: core.HomeAssistant, data: Mapping[str, Any] +) -> dict[str, str]: """Validate the user input allows us to connect.""" ayla_api = get_ayla_api( username=data[CONF_USERNAME], @@ -45,14 +50,16 @@ class SharkIqConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def _async_validate_input(self, user_input): + async def _async_validate_input( + self, user_input: Mapping[str, Any] + ) -> tuple[dict[str, str] | None, dict[str, str]]: """Validate form input.""" errors = {} info = None # noinspection PyBroadException try: - info = await validate_input(self.hass, user_input) + info = await _validate_input(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: @@ -62,9 +69,11 @@ class SharkIqConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" return info, errors - async def async_step_user(self, user_input: dict | None = None): + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Handle the initial step.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: info, errors = await self._async_validate_input(user_input) if info: @@ -76,9 +85,9 @@ class SharkIqConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=SHARKIQ_SCHEMA, errors=errors ) - async def async_step_reauth(self, user_input: dict | None = None): + async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: """Handle re-auth if login is invalid.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: _, errors = await self._async_validate_input(user_input) From 57ed66725775c62ea9b7754cbb2ef043f7b270ff Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 30 May 2022 16:46:55 +0200 Subject: [PATCH 1040/3516] Remove YAML configuration from nzbget (#72424) --- homeassistant/components/nzbget/__init__.py | 60 ++----------------- .../components/nzbget/config_flow.py | 31 +++------- tests/components/nzbget/__init__.py | 18 ------ tests/components/nzbget/test_config_flow.py | 9 +-- tests/components/nzbget/test_init.py | 26 +------- 5 files changed, 18 insertions(+), 126 deletions(-) diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index cb906495d58..a29ea829bbc 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -1,31 +1,18 @@ """The NZBGet integration.""" import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_SCAN_INTERVAL, - CONF_SSL, - CONF_USERNAME, - Platform, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_SCAN_INTERVAL, Platform from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_SPEED, DATA_COORDINATOR, DATA_UNDO_UPDATE_LISTENER, - DEFAULT_NAME, - DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DEFAULT_SPEED_LIMIT, - DEFAULT_SSL, DOMAIN, SERVICE_PAUSE, SERVICE_RESUME, @@ -35,54 +22,17 @@ from .coordinator import NZBGetDataUpdateCoordinator PLATFORMS = [Platform.SENSOR, Platform.SWITCH] -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) SPEED_LIMIT_SCHEMA = vol.Schema( {vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.positive_int} ) -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the NZBGet integration.""" - hass.data.setdefault(DOMAIN, {}) - - if hass.config_entries.async_entries(DOMAIN): - return True - - if DOMAIN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config[DOMAIN], - ) - ) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up NZBGet from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + if not entry.options: options = { CONF_SCAN_INTERVAL: entry.data.get( diff --git a/homeassistant/components/nzbget/config_flow.py b/homeassistant/components/nzbget/config_flow.py index c7a1699a86c..732ef879762 100644 --- a/homeassistant/components/nzbget/config_flow.py +++ b/homeassistant/components/nzbget/config_flow.py @@ -6,7 +6,7 @@ from typing import Any import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, OptionsFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -17,7 +17,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from .const import ( @@ -33,7 +33,7 @@ from .coordinator import NZBGetAPI, NZBGetAPIException _LOGGER = logging.getLogger(__name__) -def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: +def _validate_input(data: dict[str, Any]) -> None: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -49,8 +49,6 @@ def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: nzbget_api.version() - return True - class NZBGetConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for NZBGet.""" @@ -59,21 +57,10 @@ class NZBGetConfigFlow(ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow(config_entry: ConfigEntry) -> NZBGetOptionsFlowHandler: """Get the options flow for this handler.""" return NZBGetOptionsFlowHandler(config_entry) - async def async_step_import( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle a flow initiated by configuration file.""" - if CONF_SCAN_INTERVAL in user_input: - user_input[CONF_SCAN_INTERVAL] = user_input[ - CONF_SCAN_INTERVAL - ].total_seconds() - - return await self.async_step_user(user_input) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -88,9 +75,7 @@ class NZBGetConfigFlow(ConfigFlow, domain=DOMAIN): user_input[CONF_VERIFY_SSL] = DEFAULT_VERIFY_SSL try: - await self.hass.async_add_executor_job( - validate_input, self.hass, user_input - ) + await self.hass.async_add_executor_job(_validate_input, user_input) except NZBGetAPIException: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except @@ -126,11 +111,13 @@ class NZBGetConfigFlow(ConfigFlow, domain=DOMAIN): class NZBGetOptionsFlowHandler(OptionsFlow): """Handle NZBGet client options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input: dict[str, Any] | None = None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage NZBGet options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/tests/components/nzbget/__init__.py b/tests/components/nzbget/__init__.py index 9993bdaff1e..331b45e3de8 100644 --- a/tests/components/nzbget/__init__.py +++ b/tests/components/nzbget/__init__.py @@ -1,5 +1,4 @@ """Tests for the NZBGet integration.""" -from datetime import timedelta from unittest.mock import patch from homeassistant.components.nzbget.const import DOMAIN @@ -37,16 +36,6 @@ USER_INPUT = { CONF_USERNAME: "", } -YAML_CONFIG = { - CONF_HOST: "10.10.10.30", - CONF_NAME: "GetNZBsTest", - CONF_PASSWORD: "", - CONF_PORT: 6789, - CONF_SCAN_INTERVAL: timedelta(seconds=5), - CONF_SSL: False, - CONF_USERNAME: "", -} - MOCK_VERSION = "21.0" MOCK_STATUS = { @@ -84,13 +73,6 @@ async def init_integration( return entry -def _patch_async_setup(return_value=True): - return patch( - "homeassistant.components.nzbget.async_setup", - return_value=return_value, - ) - - def _patch_async_setup_entry(return_value=True): return patch( "homeassistant.components.nzbget.async_setup_entry", diff --git a/tests/components/nzbget/test_config_flow.py b/tests/components/nzbget/test_config_flow.py index f6e91d13d9e..8799e3adcf0 100644 --- a/tests/components/nzbget/test_config_flow.py +++ b/tests/components/nzbget/test_config_flow.py @@ -15,7 +15,6 @@ from homeassistant.data_entry_flow import ( from . import ( ENTRY_CONFIG, USER_INPUT, - _patch_async_setup, _patch_async_setup_entry, _patch_history, _patch_status, @@ -34,7 +33,7 @@ async def test_user_form(hass): assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {} - with _patch_version(), _patch_status(), _patch_history(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: + with _patch_version(), _patch_status(), _patch_history(), _patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT, @@ -45,7 +44,6 @@ async def test_user_form(hass): assert result["title"] == "10.10.10.30" assert result["data"] == {**USER_INPUT, CONF_VERIFY_SSL: False} - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -63,7 +61,7 @@ async def test_user_form_show_advanced_options(hass): CONF_VERIFY_SSL: True, } - with _patch_version(), _patch_status(), _patch_history(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: + with _patch_version(), _patch_status(), _patch_history(), _patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input_advanced, @@ -74,7 +72,6 @@ async def test_user_form_show_advanced_options(hass): assert result["title"] == "10.10.10.30" assert result["data"] == {**USER_INPUT, CONF_VERIFY_SSL: True} - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -149,7 +146,7 @@ async def test_options_flow(hass, nzbget_api): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "init" - with _patch_async_setup(), _patch_async_setup_entry(): + with _patch_async_setup_entry(): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SCAN_INTERVAL: 15}, diff --git a/tests/components/nzbget/test_init.py b/tests/components/nzbget/test_init.py index e83672769da..fbb65a4f8b2 100644 --- a/tests/components/nzbget/test_init.py +++ b/tests/components/nzbget/test_init.py @@ -5,36 +5,12 @@ from pynzbgetapi import NZBGetAPIException from homeassistant.components.nzbget.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT -from homeassistant.setup import async_setup_component -from . import ( - ENTRY_CONFIG, - YAML_CONFIG, - _patch_async_setup_entry, - _patch_history, - _patch_status, - _patch_version, - init_integration, -) +from . import ENTRY_CONFIG, _patch_version, init_integration from tests.common import MockConfigEntry -async def test_import_from_yaml(hass) -> None: - """Test import from YAML.""" - with _patch_version(), _patch_status(), _patch_history(), _patch_async_setup_entry(): - assert await async_setup_component(hass, DOMAIN, {DOMAIN: YAML_CONFIG}) - await hass.async_block_till_done() - - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - - assert entries[0].data[CONF_NAME] == "GetNZBsTest" - assert entries[0].data[CONF_HOST] == "10.10.10.30" - assert entries[0].data[CONF_PORT] == 6789 - - async def test_unload_entry(hass, nzbget_api): """Test successful unload of entry.""" entry = await init_integration(hass) From 640f53ce21b26ca42989100273612945fd744bf5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 30 May 2022 17:07:18 +0200 Subject: [PATCH 1041/3516] Remove YAML configuration from upnp (#72410) --- homeassistant/components/upnp/__init__.py | 42 +-------- homeassistant/components/upnp/config_flow.py | 87 +----------------- tests/components/upnp/conftest.py | 5 +- tests/components/upnp/test_config_flow.py | 96 +------------------- 4 files changed, 11 insertions(+), 219 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 07560f7413f..27d69f9c509 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -5,13 +5,10 @@ import asyncio from collections.abc import Mapping from dataclasses import dataclass from datetime import timedelta -from ipaddress import ip_address from typing import Any from async_upnp_client.exceptions import UpnpCommunicationError, UpnpConnectionError -import voluptuous as vol -from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.binary_sensor import BinarySensorEntityDescription from homeassistant.components.sensor import SensorEntityDescription @@ -19,10 +16,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -30,7 +25,6 @@ from homeassistant.helpers.update_coordinator import ( ) from .const import ( - CONF_LOCAL_IP, CONFIG_ENTRY_MAC_ADDRESS, CONFIG_ENTRY_ORIGINAL_UDN, CONFIG_ENTRY_ST, @@ -46,43 +40,15 @@ NOTIFICATION_TITLE = "UPnP/IGD Setup" PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - vol.All( - cv.deprecated(CONF_LOCAL_IP), - { - vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string), - }, - ) - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up UPnP component.""" - hass.data[DOMAIN] = {} - - # Only start if set up via configuration.yaml. - if DOMAIN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} - ) - ) - - return True +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up UPnP/IGD device from a config entry.""" LOGGER.debug("Setting up config entry: %s", entry.entry_id) + hass.data.setdefault(DOMAIN, {}) + udn = entry.data[CONFIG_ENTRY_UDN] st = entry.data[CONFIG_ENTRY_ST] # pylint: disable=invalid-name usn = f"{udn}::{st}" diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index b54098b6566..7fa37f589bf 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -1,7 +1,6 @@ """Config flow for UPNP.""" from __future__ import annotations -import asyncio from collections.abc import Mapping from typing import Any, cast @@ -9,7 +8,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp -from homeassistant.components.ssdp import SsdpChange, SsdpServiceInfo +from homeassistant.components.ssdp import SsdpServiceInfo from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult @@ -21,7 +20,6 @@ from .const import ( CONFIG_ENTRY_UDN, DOMAIN, LOGGER, - SSDP_SEARCH_TIMEOUT, ST_IGD_V1, ST_IGD_V2, ) @@ -48,47 +46,6 @@ def _is_complete_discovery(discovery_info: ssdp.SsdpServiceInfo) -> bool: ) -async def _async_wait_for_discoveries(hass: HomeAssistant) -> bool: - """Wait for a device to be discovered.""" - device_discovered_event = asyncio.Event() - - async def device_discovered(info: SsdpServiceInfo, change: SsdpChange) -> None: - if change != SsdpChange.BYEBYE: - LOGGER.debug( - "Device discovered: %s, at: %s", - info.ssdp_usn, - info.ssdp_location, - ) - device_discovered_event.set() - - cancel_discovered_callback_1 = await ssdp.async_register_callback( - hass, - device_discovered, - { - ssdp.ATTR_SSDP_ST: ST_IGD_V1, - }, - ) - cancel_discovered_callback_2 = await ssdp.async_register_callback( - hass, - device_discovered, - { - ssdp.ATTR_SSDP_ST: ST_IGD_V2, - }, - ) - - try: - await asyncio.wait_for( - device_discovered_event.wait(), timeout=SSDP_SEARCH_TIMEOUT - ) - except asyncio.TimeoutError: - return False - finally: - cancel_discovered_callback_1() - cancel_discovered_callback_2() - - return True - - async def _async_discover_igd_devices( hass: HomeAssistant, ) -> list[ssdp.SsdpServiceInfo]: @@ -120,7 +77,9 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize the UPnP/IGD config flow.""" self._discoveries: list[SsdpServiceInfo] | None = None - async def async_step_user(self, user_input: Mapping | None = None) -> FlowResult: + async def async_step_user( + self, user_input: Mapping[str, Any] | None = None + ) -> FlowResult: """Handle a flow start.""" LOGGER.debug("async_step_user: user_input: %s", user_input) @@ -172,42 +131,6 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=data_schema, ) - async def async_step_import(self, import_info: Mapping | None) -> Mapping[str, Any]: - """Import a new UPnP/IGD device as a config entry. - - This flow is triggered by `async_setup`. If no device has been - configured before, find any device and create a config_entry for it. - Otherwise, do nothing. - """ - LOGGER.debug("async_step_import: import_info: %s", import_info) - - # Landed here via configuration.yaml entry. - # Any device already added, then abort. - if self._async_current_entries(): - LOGGER.debug("Already configured, aborting") - return self.async_abort(reason="already_configured") - - # Discover devices. - await _async_wait_for_discoveries(self.hass) - discoveries = await _async_discover_igd_devices(self.hass) - - # Ensure anything to add. If not, silently abort. - if not discoveries: - LOGGER.info("No UPnP devices discovered, aborting") - return self.async_abort(reason="no_devices_found") - - # Ensure complete discovery. - discovery = discoveries[0] - if not _is_complete_discovery(discovery): - LOGGER.debug("Incomplete discovery, ignoring") - return self.async_abort(reason="incomplete_discovery") - - # Ensure not already configuring/configured. - unique_id = discovery.ssdp_usn - await self.async_set_unique_id(unique_id) - - return await self._async_create_entry_from_discovery(discovery) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered UPnP/IGD device. @@ -275,7 +198,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_ssdp_confirm() async def async_step_ssdp_confirm( - self, user_input: Mapping | None = None + self, user_input: Mapping[str, Any] | None = None ) -> FlowResult: """Confirm integration via SSDP.""" LOGGER.debug("async_step_ssdp_confirm: user_input: %s", user_input) diff --git a/tests/components/upnp/conftest.py b/tests/components/upnp/conftest.py index 687518bb46d..0d3e869db35 100644 --- a/tests/components/upnp/conftest.py +++ b/tests/components/upnp/conftest.py @@ -156,10 +156,7 @@ async def ssdp_no_discovery(): ) as mock_register, patch( "homeassistant.components.ssdp.async_get_discovery_info_by_st", return_value=[], - ) as mock_get_info, patch( - "homeassistant.components.upnp.config_flow.SSDP_SEARCH_TIMEOUT", - 0.1, - ): + ) as mock_get_info: yield (mock_register, mock_get_info) diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index ea8b3381cd1..80847ec2737 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -1,7 +1,7 @@ """Test UPnP/IGD config flow.""" from copy import deepcopy -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest @@ -341,97 +341,3 @@ async def test_flow_user_no_discovery(hass: HomeAssistant): ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "no_devices_found" - - -@pytest.mark.usefixtures( - "ssdp_instant_discovery", - "mock_setup_entry", - "mock_get_source_ip", - "mock_mac_address_from_host", -) -async def test_flow_import(hass: HomeAssistant): - """Test config flow: configured through configuration.yaml.""" - # Discovered via step import. - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == TEST_FRIENDLY_NAME - assert result["data"] == { - CONFIG_ENTRY_ST: TEST_ST, - CONFIG_ENTRY_UDN: TEST_UDN, - CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, - CONFIG_ENTRY_LOCATION: TEST_LOCATION, - CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, - } - - -@pytest.mark.usefixtures( - "mock_get_source_ip", -) -async def test_flow_import_incomplete_discovery(hass: HomeAssistant): - """Test config flow: configured through configuration.yaml, but incomplete discovery.""" - incomplete_discovery = ssdp.SsdpServiceInfo( - ssdp_usn=TEST_USN, - ssdp_st=TEST_ST, - ssdp_location=TEST_LOCATION, - upnp={ - # ssdp.ATTR_UPNP_UDN: TEST_UDN, # Not provided. - }, - ) - - async def register_callback(hass, callback, match_dict): - """Immediately do callback.""" - await callback(incomplete_discovery, ssdp.SsdpChange.ALIVE) - return MagicMock() - - with patch( - "homeassistant.components.ssdp.async_register_callback", - side_effect=register_callback, - ), patch( - "homeassistant.components.upnp.ssdp.async_get_discovery_info_by_st", - return_value=[incomplete_discovery], - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "incomplete_discovery" - - -@pytest.mark.usefixtures("ssdp_instant_discovery", "mock_get_source_ip") -async def test_flow_import_already_configured(hass: HomeAssistant): - """Test config flow: configured through configuration.yaml, but existing config entry.""" - # Existing entry. - entry = MockConfigEntry( - domain=DOMAIN, - unique_id=TEST_USN, - data={ - CONFIG_ENTRY_ST: TEST_ST, - CONFIG_ENTRY_UDN: TEST_UDN, - CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, - CONFIG_ENTRY_LOCATION: TEST_LOCATION, - CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, - }, - state=config_entries.ConfigEntryState.LOADED, - ) - entry.add_to_hass(hass) - - # Discovered via step import. - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -@pytest.mark.usefixtures("ssdp_no_discovery", "mock_get_source_ip") -async def test_flow_import_no_devices_found(hass: HomeAssistant): - """Test config flow: no devices found, configured through configuration.yaml.""" - # Discovered via step import. - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "no_devices_found" From d5fc7e3174332e0d605865b1847a973fb5298637 Mon Sep 17 00:00:00 2001 From: Thijs W Date: Mon, 30 May 2022 18:31:57 +0200 Subject: [PATCH 1042/3516] Upgrade frontier_silicon library to AFSAPI 0.2.4 (#69371) --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/frontier_silicon/const.py | 6 + .../components/frontier_silicon/manifest.json | 6 +- .../frontier_silicon/media_player.py | 138 ++++++++++-------- requirements_all.txt | 2 +- 6 files changed, 92 insertions(+), 62 deletions(-) create mode 100644 homeassistant/components/frontier_silicon/const.py diff --git a/.coveragerc b/.coveragerc index aced69a4714..6f96cccaf5f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -407,6 +407,7 @@ omit = homeassistant/components/fritzbox_callmonitor/const.py homeassistant/components/fritzbox_callmonitor/base.py homeassistant/components/fritzbox_callmonitor/sensor.py + homeassistant/components/frontier_silicon/const.py homeassistant/components/frontier_silicon/media_player.py homeassistant/components/futurenow/light.py homeassistant/components/garadget/cover.py diff --git a/CODEOWNERS b/CODEOWNERS index 259fbc77aab..3772a7f2dfa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -364,6 +364,7 @@ build.json @home-assistant/supervisor /tests/components/fronius/ @nielstron @farmio /homeassistant/components/frontend/ @home-assistant/frontend /tests/components/frontend/ @home-assistant/frontend +/homeassistant/components/frontier_silicon/ @wlcrs /homeassistant/components/garages_amsterdam/ @klaasnicolaas /tests/components/garages_amsterdam/ @klaasnicolaas /homeassistant/components/gdacs/ @exxamalte diff --git a/homeassistant/components/frontier_silicon/const.py b/homeassistant/components/frontier_silicon/const.py new file mode 100644 index 00000000000..4638e63c2f2 --- /dev/null +++ b/homeassistant/components/frontier_silicon/const.py @@ -0,0 +1,6 @@ +"""Constants for the Frontier Silicon Media Player integration.""" + +DOMAIN = "frontier_silicon" + +DEFAULT_PIN = "1234" +DEFAULT_PORT = 80 diff --git a/homeassistant/components/frontier_silicon/manifest.json b/homeassistant/components/frontier_silicon/manifest.json index 3eb982e8118..20092b941a9 100644 --- a/homeassistant/components/frontier_silicon/manifest.json +++ b/homeassistant/components/frontier_silicon/manifest.json @@ -2,7 +2,7 @@ "domain": "frontier_silicon", "name": "Frontier Silicon", "documentation": "https://www.home-assistant.io/integrations/frontier_silicon", - "requirements": ["afsapi==0.0.4"], - "codeowners": [], - "iot_class": "local_push" + "requirements": ["afsapi==0.2.4"], + "codeowners": ["@wlcrs"], + "iot_class": "local_polling" } diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 034762a09ac..66d1f304b1e 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -3,8 +3,7 @@ from __future__ import annotations import logging -from afsapi import AFSAPI -import requests +from afsapi import AFSAPI, ConnectionError as FSConnectionError, PlayState import voluptuous as vol from homeassistant.components.media_player import ( @@ -20,25 +19,27 @@ from homeassistant.const import ( CONF_PORT, STATE_IDLE, STATE_OFF, + STATE_OPENING, STATE_PAUSED, STATE_PLAYING, + STATE_UNAVAILABLE, STATE_UNKNOWN, ) 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 -_LOGGER = logging.getLogger(__name__) +from .const import DEFAULT_PIN, DEFAULT_PORT, DOMAIN -DEFAULT_PORT = 80 -DEFAULT_PASSWORD = "1234" +_LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PIN): cv.string, vol.Optional(CONF_NAME): cv.string, } ) @@ -52,8 +53,14 @@ async def async_setup_platform( ) -> None: """Set up the Frontier Silicon platform.""" if discovery_info is not None: + webfsapi_url = await AFSAPI.get_webfsapi_endpoint( + discovery_info["ssdp_description"] + ) + afsapi = AFSAPI(webfsapi_url, DEFAULT_PIN) + + name = await afsapi.get_friendly_name() async_add_entities( - [AFSAPIDevice(discovery_info["ssdp_description"], DEFAULT_PASSWORD, None)], + [AFSAPIDevice(name, afsapi)], True, ) return @@ -64,11 +71,12 @@ async def async_setup_platform( name = config.get(CONF_NAME) try: - async_add_entities( - [AFSAPIDevice(f"http://{host}:{port}/device", password, name)], True + webfsapi_url = await AFSAPI.get_webfsapi_endpoint( + f"http://{host}:{port}/device" ) - _LOGGER.debug("FSAPI device %s:%s -> %s", host, port, password) - except requests.exceptions.RequestException: + afsapi = AFSAPI(webfsapi_url, password) + async_add_entities([AFSAPIDevice(name, afsapi)], True) + except FSConnectionError: _LOGGER.error( "Could not add the FSAPI device at %s:%s -> %s", host, port, password ) @@ -93,10 +101,15 @@ class AFSAPIDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOURCE ) - def __init__(self, device_url, password, name): + def __init__(self, name: str | None, afsapi: AFSAPI) -> None: """Initialize the Frontier Silicon API device.""" - self._device_url = device_url - self._password = password + self.fs_device = afsapi + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, afsapi.webfsapi_endpoint)}, + name=name, + ) + self._state = None self._name = name @@ -110,17 +123,7 @@ class AFSAPIDevice(MediaPlayerEntity): self._max_volume = None self._volume_level = None - # Properties - @property - def fs_device(self): - """ - Create a fresh fsapi session. - - A new session is created for each request in case someone else - connected to the device in between the updates and invalidated the - existing session (i.e UNDOK). - """ - return AFSAPI(self._device_url, self._password) + self.__modes_by_label = None @property def name(self): @@ -175,43 +178,61 @@ class AFSAPIDevice(MediaPlayerEntity): async def async_update(self): """Get the latest date and update device state.""" - fs_device = self.fs_device - - if not self._name: - self._name = await fs_device.get_friendly_name() - - if not self._source_list: - self._source_list = await fs_device.get_mode_list() - - # The API seems to include 'zero' in the number of steps (e.g. if the range is - # 0-40 then get_volume_steps returns 41) subtract one to get the max volume. - # If call to get_volume fails set to 0 and try again next time. - if not self._max_volume: - self._max_volume = int(await fs_device.get_volume_steps() or 1) - 1 - - if await fs_device.get_power(): - status = await fs_device.get_play_status() - self._state = { - "playing": STATE_PLAYING, - "paused": STATE_PAUSED, - "stopped": STATE_IDLE, - "unknown": STATE_UNKNOWN, - None: STATE_IDLE, - }.get(status, STATE_UNKNOWN) + afsapi = self.fs_device + try: + if await afsapi.get_power(): + status = await afsapi.get_play_status() + self._state = { + PlayState.PLAYING: STATE_PLAYING, + PlayState.PAUSED: STATE_PAUSED, + PlayState.STOPPED: STATE_IDLE, + PlayState.LOADING: STATE_OPENING, + None: STATE_IDLE, + }.get(status, STATE_UNKNOWN) + else: + self._state = STATE_OFF + except FSConnectionError: + if self._attr_available: + _LOGGER.warning( + "Could not connect to %s. Did it go offline?", + self._name or afsapi.webfsapi_endpoint, + ) + self._state = STATE_UNAVAILABLE + self._attr_available = False else: - self._state = STATE_OFF + if not self._attr_available: + _LOGGER.info( + "Reconnected to %s", + self._name or afsapi.webfsapi_endpoint, + ) - if self._state != STATE_OFF: - info_name = await fs_device.get_play_name() - info_text = await fs_device.get_play_text() + self._attr_available = True + if not self._name: + self._name = await afsapi.get_friendly_name() + + if not self._source_list: + self.__modes_by_label = { + mode.label: mode.key for mode in await afsapi.get_modes() + } + self._source_list = list(self.__modes_by_label.keys()) + + # The API seems to include 'zero' in the number of steps (e.g. if the range is + # 0-40 then get_volume_steps returns 41) subtract one to get the max volume. + # If call to get_volume fails set to 0 and try again next time. + if not self._max_volume: + self._max_volume = int(await afsapi.get_volume_steps() or 1) - 1 + + if self._state not in [STATE_OFF, STATE_UNAVAILABLE]: + info_name = await afsapi.get_play_name() + info_text = await afsapi.get_play_text() self._title = " - ".join(filter(None, [info_name, info_text])) - self._artist = await fs_device.get_play_artist() - self._album_name = await fs_device.get_play_album() + self._artist = await afsapi.get_play_artist() + self._album_name = await afsapi.get_play_album() - self._source = await fs_device.get_mode() - self._mute = await fs_device.get_mute() - self._media_image_url = await fs_device.get_play_graphic() + self._source = (await afsapi.get_mode()).label + self._mute = await afsapi.get_mute() + self._media_image_url = await afsapi.get_play_graphic() volume = await self.fs_device.get_volume() @@ -296,4 +317,5 @@ class AFSAPIDevice(MediaPlayerEntity): async def async_select_source(self, source): """Select input source.""" - await self.fs_device.set_mode(source) + await self.fs_device.set_power(True) + await self.fs_device.set_mode(self.__modes_by_label.get(source)) diff --git a/requirements_all.txt b/requirements_all.txt index de4bcbef89e..16b059d385e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -86,7 +86,7 @@ adguardhome==0.5.1 advantage_air==0.3.1 # homeassistant.components.frontier_silicon -afsapi==0.0.4 +afsapi==0.2.4 # homeassistant.components.agent_dvr agent-py==0.0.23 From f06ceef43deac49a956e76aac3ba4a9cee2bdf19 Mon Sep 17 00:00:00 2001 From: Dimiter Geelen Date: Mon, 30 May 2022 18:36:01 +0200 Subject: [PATCH 1043/3516] Bump PyVLX to 0.2.20 (#72678) --- homeassistant/components/velux/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/velux/manifest.json b/homeassistant/components/velux/manifest.json index 4a5ea07dc82..d86b607cdc6 100644 --- a/homeassistant/components/velux/manifest.json +++ b/homeassistant/components/velux/manifest.json @@ -2,7 +2,7 @@ "domain": "velux", "name": "Velux", "documentation": "https://www.home-assistant.io/integrations/velux", - "requirements": ["pyvlx==0.2.19"], + "requirements": ["pyvlx==0.2.20"], "codeowners": ["@Julius2342"], "iot_class": "local_polling", "loggers": ["pyvlx"] diff --git a/requirements_all.txt b/requirements_all.txt index 16b059d385e..f66db8e37f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2014,7 +2014,7 @@ pyvesync==2.0.3 pyvizio==0.1.57 # homeassistant.components.velux -pyvlx==0.2.19 +pyvlx==0.2.20 # homeassistant.components.volumio pyvolumio==0.1.5 From 3399be2dad7fc9115914b8b8c169c8e2fe3f8fdb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 May 2022 06:41:07 -1000 Subject: [PATCH 1044/3516] Retry bond setup when zeroconf discovery happens (#72687) --- homeassistant/components/bond/config_flow.py | 22 +++++------ tests/components/bond/test_config_flow.py | 39 ++++++++++++++++++++ 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 6eba9897468..9670782d2a6 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 +import contextlib from http import HTTPStatus import logging from typing import Any @@ -33,10 +34,9 @@ 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 + response: dict[str, str] = {} + with contextlib.suppress(ClientConnectionError): + response = await bond.token() return response.get("token") @@ -101,6 +101,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): host: str = discovery_info.host bond_id = name.partition(".")[0] await self.async_set_unique_id(bond_id) + hass = self.hass for entry in self._async_current_entries(): if entry.unique_id != bond_id: continue @@ -110,13 +111,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ): updates[CONF_ACCESS_TOKEN] = token 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) - ) + changed = new_data != dict(entry.data) + if changed: + hass.config_entries.async_update_entry(entry, data=new_data) + if changed or entry.state is ConfigEntryState.SETUP_RETRY: + entry_id = entry.entry_id + hass.async_create_task(hass.config_entries.async_reload(entry_id)) raise AbortFlow("already_configured") self._discovered = {CONF_HOST: host, CONF_NAME: bond_id} diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index 5d3b357b9f7..67910af7b6c 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -381,6 +381,45 @@ async def test_zeroconf_already_configured(hass: core.HomeAssistant): assert len(mock_setup_entry.mock_calls) == 1 +async def test_zeroconf_in_setup_retry_state(hass: core.HomeAssistant): + """Test we retry right away on zeroconf discovery.""" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="already-registered-bond-id", + data={CONF_HOST: "stored-host", CONF_ACCESS_TOKEN: "test-token"}, + ) + entry.add_to_hass(hass) + + with patch_bond_version(side_effect=OSError): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.SETUP_RETRY + + with _patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="updated-host", + addresses=["updated-host"], + hostname="mock_hostname", + name="already-registered-bond-id.some-other-tail-info", + port=None, + properties={}, + type="mock_type", + ), + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data["host"] == "updated-host" + assert len(mock_setup_entry.mock_calls) == 1 + assert entry.state is ConfigEntryState.LOADED + + 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( From 45e4dd379b54847174b1f69ca138ba5fe73d24f9 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Tue, 31 May 2022 03:54:39 +1000 Subject: [PATCH 1045/3516] Add support for topdown shades to hunterdouglas_powerview (#62788) Co-authored-by: J. Nick Koston --- .coveragerc | 7 +- CODEOWNERS | 4 +- .../hunterdouglas_powerview/__init__.py | 43 +- .../hunterdouglas_powerview/const.py | 9 +- .../hunterdouglas_powerview/coordinator.py | 44 ++ .../hunterdouglas_powerview/cover.py | 572 +++++++++++------- .../hunterdouglas_powerview/entity.py | 42 +- .../hunterdouglas_powerview/manifest.json | 2 +- .../hunterdouglas_powerview/sensor.py | 12 +- .../hunterdouglas_powerview/shade_data.py | 113 ++++ .../hunterdouglas_powerview/util.py | 15 + 11 files changed, 602 insertions(+), 261 deletions(-) create mode 100644 homeassistant/components/hunterdouglas_powerview/coordinator.py create mode 100644 homeassistant/components/hunterdouglas_powerview/shade_data.py create mode 100644 homeassistant/components/hunterdouglas_powerview/util.py diff --git a/.coveragerc b/.coveragerc index 6f96cccaf5f..19f305dbf3b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -506,10 +506,13 @@ omit = homeassistant/components/huawei_lte/switch.py homeassistant/components/hue/light.py homeassistant/components/hunterdouglas_powerview/__init__.py - homeassistant/components/hunterdouglas_powerview/scene.py - homeassistant/components/hunterdouglas_powerview/sensor.py + homeassistant/components/hunterdouglas_powerview/coordinator.py homeassistant/components/hunterdouglas_powerview/cover.py homeassistant/components/hunterdouglas_powerview/entity.py + homeassistant/components/hunterdouglas_powerview/scene.py + homeassistant/components/hunterdouglas_powerview/sensor.py + homeassistant/components/hunterdouglas_powerview/shade_data.py + homeassistant/components/hunterdouglas_powerview/util.py homeassistant/components/hvv_departures/binary_sensor.py homeassistant/components/hvv_departures/sensor.py homeassistant/components/hvv_departures/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 3772a7f2dfa..545d5027ecb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -467,8 +467,8 @@ build.json @home-assistant/supervisor /tests/components/huisbaasje/ @dennisschroer /homeassistant/components/humidifier/ @home-assistant/core @Shulyaka /tests/components/humidifier/ @home-assistant/core @Shulyaka -/homeassistant/components/hunterdouglas_powerview/ @bdraco @trullock -/tests/components/hunterdouglas_powerview/ @bdraco @trullock +/homeassistant/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock +/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock /homeassistant/components/hvv_departures/ @vigonotion /tests/components/hvv_departures/ @vigonotion /homeassistant/components/hydrawise/ @ptcryan diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 17dd580f5cc..97f7f8de931 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -1,10 +1,8 @@ """The Hunter Douglas PowerView integration.""" -from datetime import timedelta import logging from aiopvapi.helpers.aiorequest import AioRequest from aiopvapi.helpers.api_base import ApiEntryPoint -from aiopvapi.helpers.constants import ATTR_ID from aiopvapi.helpers.tools import base64_to_unicode from aiopvapi.rooms import Rooms from aiopvapi.scenes import Scenes @@ -14,11 +12,10 @@ import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( API_PATH_FWVERSION, @@ -50,6 +47,9 @@ from .const import ( SHADE_DATA, USER_DATA, ) +from .coordinator import PowerviewShadeUpdateCoordinator +from .shade_data import PowerviewShadeData +from .util import async_map_data_by_id PARALLEL_UPDATES = 1 @@ -64,7 +64,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: config = entry.data - hub_address = config.get(CONF_HOST) + hub_address = config[CONF_HOST] websession = async_get_clientsession(hass) pv_request = AioRequest(hub_address, loop=hass.loop, websession=websession) @@ -75,17 +75,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async with async_timeout.timeout(10): rooms = Rooms(pv_request) - room_data = _async_map_data_by_id((await rooms.get_resources())[ROOM_DATA]) + room_data = async_map_data_by_id((await rooms.get_resources())[ROOM_DATA]) async with async_timeout.timeout(10): scenes = Scenes(pv_request) - scene_data = _async_map_data_by_id( + scene_data = async_map_data_by_id( (await scenes.get_resources())[SCENE_DATA] ) async with async_timeout.timeout(10): shades = Shades(pv_request) - shade_data = _async_map_data_by_id( + shade_data = async_map_data_by_id( (await shades.get_resources())[SHADE_DATA] ) except HUB_EXCEPTIONS as err: @@ -95,24 +95,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not device_info: raise ConfigEntryNotReady(f"Unable to initialize PowerView hub: {hub_address}") - async def async_update_data(): - """Fetch data from shade endpoint.""" - async with async_timeout.timeout(10): - shade_entries = await shades.get_resources() - if not shade_entries: - raise UpdateFailed("Failed to fetch new shade data.") - return _async_map_data_by_id(shade_entries[SHADE_DATA]) - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name="powerview hub", - update_method=async_update_data, - update_interval=timedelta(seconds=60), - ) - - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { + coordinator = PowerviewShadeUpdateCoordinator(hass, shades, hub_address) + coordinator.async_set_updated_data(PowerviewShadeData()) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { PV_API: pv_request, PV_ROOM_DATA: room_data, PV_SCENE_DATA: scene_data, @@ -155,12 +140,6 @@ async def async_get_device_info(pv_request): } -@callback -def _async_map_data_by_id(data): - """Return a dict with the key being the id for a list of entries.""" - return {entry[ATTR_ID]: entry for entry in data} - - 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/hunterdouglas_powerview/const.py b/homeassistant/components/hunterdouglas_powerview/const.py index ea87150a9ca..1cc9f79df40 100644 --- a/homeassistant/components/hunterdouglas_powerview/const.py +++ b/homeassistant/components/hunterdouglas_powerview/const.py @@ -7,7 +7,6 @@ from aiopvapi.helpers.aiorequest import PvApiConnectionError, PvApiResponseStatu DOMAIN = "hunterdouglas_powerview" - MANUFACTURER = "Hunter Douglas" HUB_ADDRESS = "address" @@ -48,7 +47,6 @@ ROOM_NAME = "name" ROOM_NAME_UNICODE = "name_unicode" ROOM_ID = "id" -SHADE_RESPONSE = "shade" SHADE_BATTERY_LEVEL = "batteryStrength" SHADE_BATTERY_LEVEL_MAX = 200 @@ -81,5 +79,10 @@ DEFAULT_LEGACY_MAINPROCESSOR = { FIRMWARE_NAME: LEGACY_DEVICE_MODEL, } - API_PATH_FWVERSION = "api/fwversion" + +POS_KIND_NONE = 0 +POS_KIND_PRIMARY = 1 +POS_KIND_SECONDARY = 2 +POS_KIND_VANE = 3 +POS_KIND_ERROR = 4 diff --git a/homeassistant/components/hunterdouglas_powerview/coordinator.py b/homeassistant/components/hunterdouglas_powerview/coordinator.py new file mode 100644 index 00000000000..bf3d6eb7a54 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/coordinator.py @@ -0,0 +1,44 @@ +"""Coordinate data for powerview devices.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from aiopvapi.shades import Shades +import async_timeout + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import SHADE_DATA +from .shade_data import PowerviewShadeData + +_LOGGER = logging.getLogger(__name__) + + +class PowerviewShadeUpdateCoordinator(DataUpdateCoordinator[PowerviewShadeData]): + """DataUpdateCoordinator to gather data from a powerview hub.""" + + def __init__( + self, + hass: HomeAssistant, + shades: Shades, + hub_address: str, + ) -> None: + """Initialize DataUpdateCoordinator to gather data for specific SmartPlug.""" + self.shades = shades + super().__init__( + hass, + _LOGGER, + name=f"powerview hub {hub_address}", + update_interval=timedelta(seconds=60), + ) + + async def _async_update_data(self) -> PowerviewShadeData: + """Fetch data from shade endpoint.""" + async with async_timeout.timeout(10): + shade_entries = await self.shades.get_resources() + if not shade_entries: + raise UpdateFailed("Failed to fetch new shade data") + self.data.store_group_data(shade_entries[SHADE_DATA]) + return self.data diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 493b5d53639..565bac6a5c8 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -1,14 +1,24 @@ """Support for hunter douglas shades.""" -from abc import abstractmethod +from __future__ import annotations + import asyncio +from collections.abc import Iterable from contextlib import suppress import logging +from typing import Any -from aiopvapi.helpers.constants import ATTR_POSITION1, ATTR_POSITION_DATA +from aiopvapi.helpers.constants import ( + ATTR_POSITION1, + ATTR_POSITION2, + ATTR_POSITION_DATA, +) from aiopvapi.resources.shade import ( ATTR_POSKIND1, + ATTR_POSKIND2, MAX_POSITION, MIN_POSITION, + BaseShade, + ShadeTdbu, Silhouette, factory as PvShade, ) @@ -22,7 +32,7 @@ from homeassistant.components.cover import ( CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later @@ -32,15 +42,19 @@ from .const import ( DEVICE_MODEL, DOMAIN, LEGACY_DEVICE_MODEL, + POS_KIND_PRIMARY, + POS_KIND_SECONDARY, + POS_KIND_VANE, PV_API, PV_ROOM_DATA, PV_SHADE_DATA, ROOM_ID_IN_SHADE, ROOM_NAME_UNICODE, - SHADE_RESPONSE, STATE_ATTRIBUTE_ROOM_NAME, ) +from .coordinator import PowerviewShadeUpdateCoordinator from .entity import ShadeEntity +from .shade_data import PowerviewShadeMove _LOGGER = logging.getLogger(__name__) @@ -52,11 +66,13 @@ PARALLEL_UPDATES = 1 RESYNC_DELAY = 60 -POSKIND_NONE = 0 -POSKIND_PRIMARY = 1 -POSKIND_SECONDARY = 2 -POSKIND_VANE = 3 -POSKIND_ERROR = 4 +# this equates to 0.75/100 in terms of hass blind position +# some blinds in a closed position report less than 655.35 (1%) +# but larger than 0 even though they are clearly closed +# Find 1 percent of MAX_POSITION, then find 75% of that number +# The means currently 491.5125 or less is closed position +# implemented for top/down shades, but also works fine with normal shades +CLOSED_POSITION = (0.75 / 100) * (MAX_POSITION - MIN_POSITION) async def async_setup_entry( @@ -65,18 +81,17 @@ async def async_setup_entry( """Set up the hunter douglas shades.""" pv_data = hass.data[DOMAIN][entry.entry_id] - room_data = pv_data[PV_ROOM_DATA] + room_data: dict[str | int, Any] = pv_data[PV_ROOM_DATA] shade_data = pv_data[PV_SHADE_DATA] pv_request = pv_data[PV_API] - coordinator = pv_data[COORDINATOR] - device_info = pv_data[DEVICE_INFO] + coordinator: PowerviewShadeUpdateCoordinator = pv_data[COORDINATOR] + device_info: dict[str, Any] = pv_data[DEVICE_INFO] - entities = [] + entities: list[ShadeEntity] = [] for raw_shade in shade_data.values(): # The shade may be out of sync with the hub - # so we force a refresh when we add it if - # possible - shade = PvShade(raw_shade, pv_request) + # so we force a refresh when we add it if possible + shade: BaseShade = PvShade(raw_shade, pv_request) name_before_refresh = shade.name with suppress(asyncio.TimeoutError): async with async_timeout.timeout(1): @@ -88,9 +103,10 @@ async def async_setup_entry( name_before_refresh, ) continue + coordinator.data.update_shade_positions(shade.raw_data) room_id = shade.raw_data.get(ROOM_ID_IN_SHADE) room_name = room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "") - entities.append( + entities.extend( create_powerview_shade_entity( coordinator, device_info, room_name, shade, name_before_refresh ) @@ -99,26 +115,36 @@ async def async_setup_entry( def create_powerview_shade_entity( - coordinator, device_info, room_name, shade, name_before_refresh -): + coordinator: PowerviewShadeUpdateCoordinator, + device_info: dict[str, Any], + room_name: str, + shade: BaseShade, + name_before_refresh: str, +) -> Iterable[ShadeEntity]: """Create a PowerViewShade entity.""" - - if isinstance(shade, Silhouette): - return PowerViewShadeSilhouette( - coordinator, device_info, room_name, shade, name_before_refresh - ) - - return PowerViewShade( - coordinator, device_info, room_name, shade, name_before_refresh - ) + classes: list[BaseShade] = [] + # order here is important as both ShadeTDBU are listed in aiovapi as can_tilt + # and both require their own class here to work + if isinstance(shade, ShadeTdbu): + classes.extend([PowerViewShadeTDBUTop, PowerViewShadeTDBUBottom]) + elif isinstance(shade, Silhouette): + classes.append(PowerViewShadeSilhouette) + elif shade.can_tilt: + classes.append(PowerViewShadeWithTilt) + else: + classes.append(PowerViewShade) + return [ + cls(coordinator, device_info, room_name, shade, name_before_refresh) + for cls in classes + ] -def hd_position_to_hass(hd_position, max_val): +def hd_position_to_hass(hd_position: int, max_val: int = MAX_POSITION) -> int: """Convert hunter douglas position to hass position.""" return round((hd_position / max_val) * 100) -def hass_position_to_hd(hass_position, max_val): +def hass_position_to_hd(hass_position: int, max_val: int = MAX_POSITION) -> int: """Convert hass position to hunter douglas position.""" return int(hass_position / 100 * max_val) @@ -128,130 +154,126 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): # The hub frequently reports stale states _attr_assumed_state = True + _attr_device_class = CoverDeviceClass.SHADE + _attr_supported_features = 0 - def __init__(self, coordinator, device_info, room_name, shade, name): + def __init__( + self, + coordinator: PowerviewShadeUpdateCoordinator, + device_info: dict[str, Any], + room_name: str, + shade: BaseShade, + name: str, + ) -> None: """Initialize the shade.""" super().__init__(coordinator, device_info, room_name, shade, name) - self._shade = shade - self._is_opening = False - self._is_closing = False - self._last_action_timestamp = 0 - self._scheduled_transition_update = None - self._current_hd_cover_position = MIN_POSITION + self._shade: BaseShade = shade + self._attr_name = self._shade_name + self._scheduled_transition_update: CALLBACK_TYPE | None = None if self._device_info[DEVICE_MODEL] != LEGACY_DEVICE_MODEL: self._attr_supported_features |= CoverEntityFeature.STOP self._forced_resync = None @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, str]: """Return the state attributes.""" return {STATE_ATTRIBUTE_ROOM_NAME: self._room_name} @property def is_closed(self): """Return if the cover is closed.""" - return self._current_hd_cover_position == MIN_POSITION + return self.positions.primary <= CLOSED_POSITION @property - def is_opening(self): - """Return if the cover is opening.""" - return self._is_opening - - @property - def is_closing(self): - """Return if the cover is closing.""" - return self._is_closing - - @property - def current_cover_position(self): + def current_cover_position(self) -> int: """Return the current position of cover.""" - return hd_position_to_hass(self._current_hd_cover_position, MAX_POSITION) + return hd_position_to_hass(self.positions.primary, MAX_POSITION) @property - def device_class(self): - """Return device class.""" - return CoverDeviceClass.SHADE + def transition_steps(self) -> int: + """Return the steps to make a move.""" + return hd_position_to_hass(self.positions.primary, MAX_POSITION) @property - def name(self): - """Return the name of the shade.""" - return self._shade_name + def open_position(self) -> PowerviewShadeMove: + """Return the open position and required additional positions.""" + return PowerviewShadeMove(self._shade.open_position, {}) - async def async_close_cover(self, **kwargs): + @property + def close_position(self) -> PowerviewShadeMove: + """Return the close position and required additional positions.""" + return PowerviewShadeMove(self._shade.close_position, {}) + + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" - await self._async_move(0) + self._async_schedule_update_for_transition(self.transition_steps) + await self._async_execute_move(self.close_position) + self._attr_is_opening = False + self._attr_is_closing = True + self.async_write_ha_state() - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" - await self._async_move(100) + self._async_schedule_update_for_transition(100 - self.transition_steps) + await self._async_execute_move(self.open_position) + self._attr_is_opening = True + self._attr_is_closing = False + self.async_write_ha_state() - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" - # Cancel any previous updates self._async_cancel_scheduled_transition_update() - self._async_update_from_command(await self._shade.stop()) + self.data.update_from_response(await self._shade.stop()) await self._async_force_refresh_state() - async def async_set_cover_position(self, **kwargs): - """Move the shade to a specific position.""" - if ATTR_POSITION not in kwargs: - return - await self._async_move(kwargs[ATTR_POSITION]) + @callback + def _clamp_cover_limit(self, target_hass_position: int) -> int: + """Dont allow a cover to go into an impossbile position.""" + # no override required in base + return target_hass_position - async def _async_move(self, target_hass_position): + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Move the shade to a specific position.""" + await self._async_set_cover_position(kwargs[ATTR_POSITION]) + + @callback + def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove: + position_one = hass_position_to_hd(target_hass_position) + return PowerviewShadeMove( + {ATTR_POSITION1: position_one, ATTR_POSKIND1: POS_KIND_PRIMARY}, {} + ) + + async def _async_execute_move(self, move: PowerviewShadeMove) -> None: + """Execute a move that can affect multiple positions.""" + response = await self._shade.move(move.request) + # Process any positions we know will update as result + # of the request since the hub won't return them + for kind, position in move.new_positions.items(): + self.data.update_shade_position(self._shade.id, position, kind) + # Finally process the response + self.data.update_from_response(response) + + async def _async_set_cover_position(self, target_hass_position: int) -> None: """Move the shade to a position.""" - current_hass_position = hd_position_to_hass( - self._current_hd_cover_position, MAX_POSITION + target_hass_position = self._clamp_cover_limit(target_hass_position) + current_hass_position = self.current_cover_position + self._async_schedule_update_for_transition( + abs(current_hass_position - target_hass_position) ) - steps_to_move = abs(current_hass_position - target_hass_position) - self._async_schedule_update_for_transition(steps_to_move) - self._async_update_from_command( - await self._shade.move( - { - ATTR_POSITION1: hass_position_to_hd( - target_hass_position, MAX_POSITION - ), - ATTR_POSKIND1: POSKIND_PRIMARY, - } - ) - ) - self._is_opening = False - self._is_closing = False - if target_hass_position > current_hass_position: - self._is_opening = True - elif target_hass_position < current_hass_position: - self._is_closing = True + await self._async_execute_move(self._get_shade_move(target_hass_position)) + self._attr_is_opening = target_hass_position > current_hass_position + self._attr_is_closing = target_hass_position < current_hass_position self.async_write_ha_state() @callback - def _async_update_from_command(self, raw_data): - """Update the shade state after a command.""" - if not raw_data or SHADE_RESPONSE not in raw_data: - return - self._async_process_new_shade_data(raw_data[SHADE_RESPONSE]) - - @callback - def _async_process_new_shade_data(self, data): - """Process new data from an update.""" - self._shade.raw_data = data - self._async_update_current_cover_position() - - @callback - def _async_update_current_cover_position(self): + def _async_update_shade_data(self, shade_data: dict[str | int, Any]) -> None: """Update the current cover position from the data.""" - _LOGGER.debug("Raw data update: %s", self._shade.raw_data) - position_data = self._shade.raw_data.get(ATTR_POSITION_DATA, {}) - self._async_process_updated_position_data(position_data) - self._is_opening = False - self._is_closing = False + self.data.update_shade_positions(shade_data) + self._attr_is_opening = False + self._attr_is_closing = False @callback - @abstractmethod - def _async_process_updated_position_data(self, position_data): - """Process position data.""" - - @callback - def _async_cancel_scheduled_transition_update(self): + def _async_cancel_scheduled_transition_update(self) -> None: """Cancel any previous updates.""" if self._scheduled_transition_update: self._scheduled_transition_update() @@ -261,9 +283,7 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): self._forced_resync = None @callback - def _async_schedule_update_for_transition(self, steps): - self.async_write_ha_state() - + def _async_schedule_update_for_transition(self, steps: int) -> None: # Cancel any previous updates self._async_cancel_scheduled_transition_update() @@ -278,7 +298,7 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): est_time_to_complete_transition, ) - # Schedule an update for when we expect the transition + # Schedule an forced update for when we expect the transition # to be completed. self._scheduled_transition_update = async_call_later( self.hass, @@ -295,139 +315,281 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): self.hass, RESYNC_DELAY, self._async_force_resync ) - async def _async_force_resync(self, *_): + async def _async_force_resync(self, *_: Any) -> None: """Force a resync after an update since the hub may have stale state.""" self._forced_resync = None + _LOGGER.debug("Force resync of shade %s", self.name) await self._async_force_refresh_state() - async def _async_force_refresh_state(self): + async def _async_force_refresh_state(self) -> None: """Refresh the cover state and force the device cache to be bypassed.""" await self._shade.refresh() - self._async_update_current_cover_position() + self._async_update_shade_data(self._shade.raw_data) self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """When entity is added to hass.""" - self._async_update_current_cover_position() self.async_on_remove( self.coordinator.async_add_listener(self._async_update_shade_from_group) ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Cancel any pending refreshes.""" self._async_cancel_scheduled_transition_update() @callback - def _async_update_shade_from_group(self): + def _async_update_shade_from_group(self) -> None: """Update with new data from the coordinator.""" if self._scheduled_transition_update or self._forced_resync: - # If a transition in in progress - # the data will be wrong + # If a transition is in progress the data will be wrong return - self._async_process_new_shade_data(self.coordinator.data[self._shade.id]) + self.data.update_from_group_data(self._shade.id) self.async_write_ha_state() class PowerViewShade(PowerViewShadeBase): """Represent a standard shade.""" - _attr_supported_features = ( - CoverEntityFeature.OPEN - | CoverEntityFeature.CLOSE - | CoverEntityFeature.SET_POSITION - ) + def __init__( + self, + coordinator: PowerviewShadeUpdateCoordinator, + device_info: dict[str, Any], + room_name: str, + shade: BaseShade, + name: str, + ) -> None: + """Initialize the shade.""" + super().__init__(coordinator, device_info, room_name, shade, name) + self._attr_supported_features |= ( + CoverEntityFeature.OPEN + | CoverEntityFeature.CLOSE + | CoverEntityFeature.SET_POSITION + ) + + +class PowerViewShadeTDBU(PowerViewShade): + """Representation of a PowerView shade with top/down bottom/up capabilities.""" + + @property + def transition_steps(self) -> int: + """Return the steps to make a move.""" + return hd_position_to_hass( + self.positions.primary, MAX_POSITION + ) + hd_position_to_hass(self.positions.secondary, MAX_POSITION) + + +class PowerViewShadeTDBUBottom(PowerViewShadeTDBU): + """Representation of a top down bottom up powerview shade.""" + + def __init__( + self, + coordinator: PowerviewShadeUpdateCoordinator, + device_info: dict[str, Any], + room_name: str, + shade: BaseShade, + name: str, + ) -> None: + """Initialize the shade.""" + super().__init__(coordinator, device_info, room_name, shade, name) + self._attr_unique_id = f"{self._shade.id}_bottom" + self._attr_name = f"{self._shade_name} Bottom" @callback - def _async_process_updated_position_data(self, position_data): - """Process position data.""" - if ATTR_POSITION1 in position_data: - self._current_hd_cover_position = int(position_data[ATTR_POSITION1]) + def _clamp_cover_limit(self, target_hass_position: int) -> int: + """Dont allow a cover to go into an impossbile position.""" + cover_top = hd_position_to_hass(self.positions.secondary, MAX_POSITION) + return min(target_hass_position, (100 - cover_top)) + + @callback + def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove: + position_bottom = hass_position_to_hd(target_hass_position) + position_top = self.positions.secondary + return PowerviewShadeMove( + { + ATTR_POSITION1: position_bottom, + ATTR_POSITION2: position_top, + ATTR_POSKIND1: POS_KIND_PRIMARY, + ATTR_POSKIND2: POS_KIND_SECONDARY, + }, + {}, + ) + + +class PowerViewShadeTDBUTop(PowerViewShadeTDBU): + """Representation of a top down bottom up powerview shade.""" + + def __init__( + self, + coordinator: PowerviewShadeUpdateCoordinator, + device_info: dict[str, Any], + room_name: str, + shade: BaseShade, + name: str, + ) -> None: + """Initialize the shade.""" + super().__init__(coordinator, device_info, room_name, shade, name) + self._attr_unique_id = f"{self._shade.id}_top" + self._attr_name = f"{self._shade_name} Top" + # these shades share a class in parent API + # override open position for top shade + self._shade.open_position = { + ATTR_POSITION1: MIN_POSITION, + ATTR_POSITION2: MAX_POSITION, + ATTR_POSKIND1: POS_KIND_PRIMARY, + ATTR_POSKIND2: POS_KIND_SECONDARY, + } + + @property + def is_closed(self): + """Return if the cover is closed.""" + # top shade needs to check other motor + return self.positions.secondary <= CLOSED_POSITION + + @property + def current_cover_position(self) -> int: + """Return the current position of cover.""" + # these need to be inverted to report state correctly in HA + return hd_position_to_hass(self.positions.secondary, MAX_POSITION) + + @callback + def _clamp_cover_limit(self, target_hass_position: int) -> int: + """Dont allow a cover to go into an impossbile position.""" + cover_bottom = hd_position_to_hass(self.positions.primary, MAX_POSITION) + return min(target_hass_position, (100 - cover_bottom)) + + @callback + def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove: + position_bottom = self.positions.primary + position_top = hass_position_to_hd(target_hass_position, MAX_POSITION) + return PowerviewShadeMove( + { + ATTR_POSITION1: position_bottom, + ATTR_POSITION2: position_top, + ATTR_POSKIND1: POS_KIND_PRIMARY, + ATTR_POSKIND2: POS_KIND_SECONDARY, + }, + {}, + ) class PowerViewShadeWithTilt(PowerViewShade): """Representation of a PowerView shade with tilt capabilities.""" - _attr_supported_features = ( - CoverEntityFeature.OPEN - | CoverEntityFeature.CLOSE - | CoverEntityFeature.SET_POSITION - | CoverEntityFeature.OPEN_TILT - | CoverEntityFeature.CLOSE_TILT - | CoverEntityFeature.STOP_TILT - | CoverEntityFeature.SET_TILT_POSITION - ) - _max_tilt = MAX_POSITION - _tilt_steps = 10 - def __init__(self, coordinator, device_info, room_name, shade, name): + def __init__( + self, + coordinator: PowerviewShadeUpdateCoordinator, + device_info: dict[str, Any], + room_name: str, + shade: BaseShade, + name: str, + ) -> None: """Initialize the shade.""" super().__init__(coordinator, device_info, room_name, shade, name) - self._attr_current_cover_tilt_position = 0 - - async def async_open_cover_tilt(self, **kwargs): - """Open the cover tilt.""" - current_hass_position = hd_position_to_hass( - self._current_hd_cover_position, MAX_POSITION + self._attr_supported_features |= ( + CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.SET_TILT_POSITION ) - steps_to_move = current_hass_position + self._tilt_steps - self._async_schedule_update_for_transition(steps_to_move) - self._async_update_from_command(await self._shade.tilt_open()) + if self._device_info[DEVICE_MODEL] != LEGACY_DEVICE_MODEL: + self._attr_supported_features |= CoverEntityFeature.STOP_TILT - async def async_close_cover_tilt(self, **kwargs): + @property + def current_cover_tilt_position(self) -> int: + """Return the current cover tile position.""" + return hd_position_to_hass(self.positions.vane, self._max_tilt) + + @property + def transition_steps(self): + """Return the steps to make a move.""" + return hd_position_to_hass( + self.positions.primary, MAX_POSITION + ) + hd_position_to_hass(self.positions.vane, self._max_tilt) + + @property + def open_position(self) -> PowerviewShadeMove: + """Return the open position and required additional positions.""" + return PowerviewShadeMove( + self._shade.open_position, {POS_KIND_VANE: MIN_POSITION} + ) + + @property + def close_position(self) -> PowerviewShadeMove: + """Return the close position and required additional positions.""" + return PowerviewShadeMove( + self._shade.close_position, {POS_KIND_VANE: MIN_POSITION} + ) + + @property + def open_tilt_position(self) -> PowerviewShadeMove: + """Return the open tilt position and required additional positions.""" + # next upstream api release to include self._shade.open_tilt_position + return PowerviewShadeMove( + {ATTR_POSKIND1: POS_KIND_VANE, ATTR_POSITION1: self._max_tilt}, + {POS_KIND_PRIMARY: MIN_POSITION}, + ) + + @property + def close_tilt_position(self) -> PowerviewShadeMove: + """Return the close tilt position and required additional positions.""" + # next upstream api release to include self._shade.close_tilt_position + return PowerviewShadeMove( + {ATTR_POSKIND1: POS_KIND_VANE, ATTR_POSITION1: MIN_POSITION}, + {POS_KIND_PRIMARY: MIN_POSITION}, + ) + + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" - current_hass_position = hd_position_to_hass( - self._current_hd_cover_position, MAX_POSITION - ) - steps_to_move = current_hass_position + self._tilt_steps - self._async_schedule_update_for_transition(steps_to_move) - self._async_update_from_command(await self._shade.tilt_close()) + self._async_schedule_update_for_transition(self.transition_steps) + await self._async_execute_move(self.close_tilt_position) + self.async_write_ha_state() - async def async_set_cover_tilt_position(self, **kwargs): - """Move the cover tilt to a specific position.""" - target_hass_tilt_position = kwargs[ATTR_TILT_POSITION] - current_hass_position = hd_position_to_hass( - self._current_hd_cover_position, MAX_POSITION - ) - steps_to_move = current_hass_position + self._tilt_steps + async def async_open_cover_tilt(self, **kwargs: Any) -> None: + """Open the cover tilt.""" + self._async_schedule_update_for_transition(100 - self.transition_steps) + await self._async_execute_move(self.open_tilt_position) + self.async_write_ha_state() - self._async_schedule_update_for_transition(steps_to_move) - self._async_update_from_command( - await self._shade.move( - { - ATTR_POSITION1: hass_position_to_hd( - target_hass_tilt_position, self._max_tilt - ), - ATTR_POSKIND1: POSKIND_VANE, - } - ) - ) + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: + """Move the vane to a specific position.""" + await self._async_set_cover_tilt_position(kwargs[ATTR_TILT_POSITION]) - async def async_stop_cover_tilt(self, **kwargs): - """Stop the cover tilting.""" - # Cancel any previous updates - await self.async_stop_cover() + async def _async_set_cover_tilt_position( + self, target_hass_tilt_position: int + ) -> None: + """Move the vane to a specific position.""" + final_position = self.current_cover_position + target_hass_tilt_position + self._async_schedule_update_for_transition( + abs(self.transition_steps - final_position) + ) + await self._async_execute_move(self._get_shade_tilt(target_hass_tilt_position)) + self.async_write_ha_state() @callback - def _async_process_updated_position_data(self, position_data): - """Process position data.""" - if ATTR_POSKIND1 not in position_data: - return - if int(position_data[ATTR_POSKIND1]) == POSKIND_PRIMARY: - self._current_hd_cover_position = int(position_data[ATTR_POSITION1]) - self._attr_current_cover_tilt_position = 0 - if int(position_data[ATTR_POSKIND1]) == POSKIND_VANE: - self._current_hd_cover_position = MIN_POSITION - self._attr_current_cover_tilt_position = hd_position_to_hass( - int(position_data[ATTR_POSITION1]), self._max_tilt - ) + def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove: + """Return a PowerviewShadeMove.""" + position_shade = hass_position_to_hd(target_hass_position) + return PowerviewShadeMove( + {ATTR_POSITION1: position_shade, ATTR_POSKIND1: POS_KIND_PRIMARY}, + {POS_KIND_VANE: MIN_POSITION}, + ) + + @callback + def _get_shade_tilt(self, target_hass_tilt_position: int) -> PowerviewShadeMove: + """Return a PowerviewShadeMove.""" + position_vane = hass_position_to_hd(target_hass_tilt_position, self._max_tilt) + return PowerviewShadeMove( + {ATTR_POSITION1: position_vane, ATTR_POSKIND1: POS_KIND_VANE}, + {POS_KIND_PRIMARY: MIN_POSITION}, + ) + + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: + """Stop the cover tilting.""" + await self.async_stop_cover() class PowerViewShadeSilhouette(PowerViewShadeWithTilt): """Representation of a Silhouette PowerView shade.""" - def __init__(self, coordinator, device_info, room_name, shade, name): - """Initialize the shade.""" - super().__init__(coordinator, device_info, room_name, shade, name) - self._max_tilt = 32767 - self._tilt_steps = 4 + _max_tilt = 32767 diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py index 50894d59f8b..174e3def2d6 100644 --- a/homeassistant/components/hunterdouglas_powerview/entity.py +++ b/homeassistant/components/hunterdouglas_powerview/entity.py @@ -1,6 +1,8 @@ -"""The nexia integration base entity.""" +"""The powerview integration base entity.""" -from aiopvapi.resources.shade import ATTR_TYPE +from typing import Any + +from aiopvapi.resources.shade import ATTR_TYPE, BaseShade from homeassistant.const import ATTR_MODEL, ATTR_SW_VERSION import homeassistant.helpers.device_registry as dr @@ -20,22 +22,30 @@ from .const import ( FIRMWARE_SUB_REVISION, MANUFACTURER, ) +from .coordinator import PowerviewShadeUpdateCoordinator +from .shade_data import PowerviewShadeData, PowerviewShadePositions -class HDEntity(CoordinatorEntity): +class HDEntity(CoordinatorEntity[PowerviewShadeUpdateCoordinator]): """Base class for hunter douglas entities.""" - def __init__(self, coordinator, device_info, room_name, unique_id): + def __init__( + self, + coordinator: PowerviewShadeUpdateCoordinator, + device_info: dict[str, Any], + room_name: str, + unique_id: str, + ) -> None: """Initialize the entity.""" super().__init__(coordinator) self._room_name = room_name - self._unique_id = unique_id + self._attr_unique_id = unique_id self._device_info = device_info @property - def unique_id(self): - """Return the unique id.""" - return self._unique_id + def data(self) -> PowerviewShadeData: + """Return the PowerviewShadeData.""" + return self.coordinator.data @property def device_info(self) -> DeviceInfo: @@ -58,12 +68,24 @@ class HDEntity(CoordinatorEntity): class ShadeEntity(HDEntity): """Base class for hunter douglas shade entities.""" - def __init__(self, coordinator, device_info, room_name, shade, shade_name): + def __init__( + self, + coordinator: PowerviewShadeUpdateCoordinator, + device_info: dict[str, Any], + room_name: str, + shade: BaseShade, + shade_name: str, + ) -> None: """Initialize the shade.""" super().__init__(coordinator, device_info, room_name, shade.id) self._shade_name = shade_name self._shade = shade + @property + def positions(self) -> PowerviewShadePositions: + """Return the PowerviewShadeData.""" + return self.data.get_shade_positions(self._shade.id) + @property def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" @@ -78,7 +100,7 @@ class ShadeEntity(HDEntity): ) for shade in self._shade.shade_types: - if shade.shade_type == device_info[ATTR_MODEL]: + if str(shade.shade_type) == device_info[ATTR_MODEL]: device_info[ATTR_MODEL] = shade.description break diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index af6aea17de3..c571056be23 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -3,7 +3,7 @@ "name": "Hunter Douglas PowerView", "documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview", "requirements": ["aiopvapi==1.6.19"], - "codeowners": ["@bdraco", "@trullock"], + "codeowners": ["@bdraco", "@kingy444", "@trullock"], "config_flow": true, "homekit": { "models": ["PowerView"] diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py index 43e438041f2..8fd492ddb1d 100644 --- a/homeassistant/components/hunterdouglas_powerview/sensor.py +++ b/homeassistant/components/hunterdouglas_powerview/sensor.py @@ -63,16 +63,16 @@ class PowerViewShadeBatterySensor(ShadeEntity, SensorEntity): _attr_device_class = SensorDeviceClass.BATTERY _attr_state_class = SensorStateClass.MEASUREMENT + def __init__(self, coordinator, device_info, room_name, shade, name): + """Initialize the shade.""" + super().__init__(coordinator, device_info, room_name, shade, name) + self._attr_unique_id = f"{self._attr_unique_id}_charge" + @property def name(self): """Name of the shade battery.""" return f"{self._shade_name} Battery" - @property - def unique_id(self): - """Shade battery Uniqueid.""" - return f"{self._unique_id}_charge" - @property def native_value(self): """Get the current value in percentage.""" @@ -89,5 +89,5 @@ class PowerViewShadeBatterySensor(ShadeEntity, SensorEntity): @callback def _async_update_shade_from_group(self): """Update with new data from the coordinator.""" - self._shade.raw_data = self.coordinator.data[self._shade.id] + self._shade.raw_data = self.data.get_raw_data(self._shade.id) self.async_write_ha_state() diff --git a/homeassistant/components/hunterdouglas_powerview/shade_data.py b/homeassistant/components/hunterdouglas_powerview/shade_data.py new file mode 100644 index 00000000000..4a7b7be0945 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/shade_data.py @@ -0,0 +1,113 @@ +"""Shade data for the Hunter Douglas PowerView integration.""" +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass +import logging +from typing import Any + +from aiopvapi.helpers.constants import ( + ATTR_ID, + ATTR_POSITION1, + ATTR_POSITION2, + ATTR_POSITION_DATA, + ATTR_POSKIND1, + ATTR_POSKIND2, + ATTR_SHADE, +) +from aiopvapi.resources.shade import MIN_POSITION + +from .const import POS_KIND_PRIMARY, POS_KIND_SECONDARY, POS_KIND_VANE +from .util import async_map_data_by_id + +POSITIONS = ((ATTR_POSITION1, ATTR_POSKIND1), (ATTR_POSITION2, ATTR_POSKIND2)) + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class PowerviewShadeMove: + """Request to move a powerview shade.""" + + # The positions to request on the hub + request: dict[str, int] + + # The positions that will also change + # as a result of the request that the + # hub will not send back + new_positions: dict[int, int] + + +@dataclass +class PowerviewShadePositions: + """Positions for a powerview shade.""" + + primary: int = MIN_POSITION + secondary: int = MIN_POSITION + vane: int = MIN_POSITION + + +class PowerviewShadeData: + """Coordinate shade data between multiple api calls.""" + + def __init__(self): + """Init the shade data.""" + self._group_data_by_id: dict[int, dict[str | int, Any]] = {} + self.positions: dict[int, PowerviewShadePositions] = {} + + def get_raw_data(self, shade_id: int) -> dict[str | int, Any]: + """Get data for the shade.""" + return self._group_data_by_id[shade_id] + + def get_shade_positions(self, shade_id: int) -> PowerviewShadePositions: + """Get positions for a shade.""" + if shade_id not in self.positions: + self.positions[shade_id] = PowerviewShadePositions() + return self.positions[shade_id] + + def update_from_group_data(self, shade_id: int) -> None: + """Process an update from the group data.""" + self.update_shade_positions(self._group_data_by_id[shade_id]) + + def store_group_data(self, shade_data: Iterable[dict[str | int, Any]]) -> None: + """Store data from the all shades endpoint. + + This does not update the shades or positions + as the data may be stale. update_from_group_data + with a shade_id will update a specific shade + from the group data. + """ + self._group_data_by_id = async_map_data_by_id(shade_data) + + def update_shade_position(self, shade_id: int, position: int, kind: int) -> None: + """Update a single shade position.""" + positions = self.get_shade_positions(shade_id) + if kind == POS_KIND_PRIMARY: + positions.primary = position + elif kind == POS_KIND_SECONDARY: + positions.secondary = position + elif kind == POS_KIND_VANE: + positions.vane = position + + def update_from_position_data( + self, shade_id: int, position_data: dict[str, Any] + ) -> None: + """Update the shade positions from the position data.""" + for position_key, kind_key in POSITIONS: + if position_key in position_data: + self.update_shade_position( + shade_id, position_data[position_key], position_data[kind_key] + ) + + def update_shade_positions(self, data: dict[int | str, Any]) -> None: + """Update a shades from data dict.""" + _LOGGER.debug("Raw data update: %s", data) + shade_id = data[ATTR_ID] + position_data = data[ATTR_POSITION_DATA] + self.update_from_position_data(shade_id, position_data) + + def update_from_response(self, response: dict[str, Any]) -> None: + """Update from the response to a command.""" + if response and ATTR_SHADE in response: + shade_data: dict[int | str, Any] = response[ATTR_SHADE] + self.update_shade_positions(shade_data) diff --git a/homeassistant/components/hunterdouglas_powerview/util.py b/homeassistant/components/hunterdouglas_powerview/util.py new file mode 100644 index 00000000000..15330f30bdb --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/util.py @@ -0,0 +1,15 @@ +"""Coordinate data for powerview devices.""" +from __future__ import annotations + +from collections.abc import Iterable +from typing import Any + +from aiopvapi.helpers.constants import ATTR_ID + +from homeassistant.core import callback + + +@callback +def async_map_data_by_id(data: Iterable[dict[str | int, Any]]): + """Return a dict with the key being the id for a list of entries.""" + return {entry[ATTR_ID]: entry for entry in data} From 7ecb527648876f25f41a76dc2ab9a45839f760ba Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 30 May 2022 21:55:44 +0200 Subject: [PATCH 1046/3516] Remove unneeded token_request override in Geocaching (#72713) --- homeassistant/components/geocaching/oauth.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/homeassistant/components/geocaching/oauth.py b/homeassistant/components/geocaching/oauth.py index e0120344cdb..848c4fce66c 100644 --- a/homeassistant/components/geocaching/oauth.py +++ b/homeassistant/components/geocaching/oauth.py @@ -1,7 +1,7 @@ """oAuth2 functions and classes for Geocaching API integration.""" from __future__ import annotations -from typing import Any, cast +from typing import Any from homeassistant.components.application_credentials import ( AuthImplementation, @@ -9,7 +9,6 @@ from homeassistant.components.application_credentials import ( ClientCredential, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ENVIRONMENT, ENVIRONMENT_URLS @@ -65,13 +64,3 @@ class GeocachingOAuth2Implementation(AuthImplementation): new_token = await self._token_request(data) return {**token, **new_token} - - async def _token_request(self, data: dict) -> dict: - """Make a token request.""" - data["client_id"] = self.client_id - if self.client_secret is not None: - data["client_secret"] = self.client_secret - session = async_get_clientsession(self.hass) - resp = await session.post(ENVIRONMENT_URLS[ENVIRONMENT]["token_url"], data=data) - resp.raise_for_status() - return cast(dict, await resp.json()) From 8c16ac2e47ef73e644ed8ec16e767e880994dee4 Mon Sep 17 00:00:00 2001 From: Ethan Madden Date: Mon, 30 May 2022 13:13:53 -0700 Subject: [PATCH 1047/3516] Vesync air quality (#72658) --- homeassistant/components/vesync/common.py | 25 +-- homeassistant/components/vesync/const.py | 28 +++ homeassistant/components/vesync/fan.py | 50 +---- homeassistant/components/vesync/sensor.py | 218 ++++++++++++---------- homeassistant/components/vesync/switch.py | 12 +- 5 files changed, 169 insertions(+), 164 deletions(-) diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index 1104a84e6b6..acee8e20961 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -20,6 +20,8 @@ async def async_process_devices(hass, manager): if manager.fans: devices[VS_FANS].extend(manager.fans) + # Expose fan sensors separately + devices[VS_SENSORS].extend(manager.fans) _LOGGER.info("%d VeSync fans found", len(manager.fans)) if manager.bulbs: @@ -49,31 +51,25 @@ class VeSyncBaseEntity(Entity): def __init__(self, device): """Initialize the VeSync device.""" self.device = device + self._attr_unique_id = self.base_unique_id + self._attr_name = self.base_name @property def base_unique_id(self): """Return the ID of this device.""" + # The unique_id property may be overridden in subclasses, such as in + # sensors. Maintaining base_unique_id allows us to group related + # entities under a single device. if isinstance(self.device.sub_device_no, int): return f"{self.device.cid}{str(self.device.sub_device_no)}" return self.device.cid - @property - def unique_id(self): - """Return the ID of this device.""" - # The unique_id property may be overridden in subclasses, such as in sensors. Maintaining base_unique_id allows - # us to group related entities under a single device. - return self.base_unique_id - @property def base_name(self): """Return the name of the device.""" + # Same story here as `base_unique_id` above return self.device.device_name - @property - def name(self): - """Return the name of the entity (may be overridden).""" - return self.base_name - @property def available(self) -> bool: """Return True if device is available.""" @@ -98,6 +94,11 @@ class VeSyncBaseEntity(Entity): class VeSyncDevice(VeSyncBaseEntity, ToggleEntity): """Base class for VeSync Device Representations.""" + @property + def details(self): + """Provide access to the device details dictionary.""" + return self.device.details + @property def is_on(self): """Return True if device is on.""" diff --git a/homeassistant/components/vesync/const.py b/homeassistant/components/vesync/const.py index fceeff81ae4..b20a04b8a1c 100644 --- a/homeassistant/components/vesync/const.py +++ b/homeassistant/components/vesync/const.py @@ -9,3 +9,31 @@ VS_FANS = "fans" VS_LIGHTS = "lights" VS_SENSORS = "sensors" VS_MANAGER = "manager" + +DEV_TYPE_TO_HA = { + "wifi-switch-1.3": "outlet", + "ESW03-USA": "outlet", + "ESW01-EU": "outlet", + "ESW15-USA": "outlet", + "ESWL01": "switch", + "ESWL03": "switch", + "ESO15-TB": "outlet", +} + +SKU_TO_BASE_DEVICE = { + "LV-PUR131S": "LV-PUR131S", + "LV-RH131S": "LV-PUR131S", # Alt ID Model LV-PUR131S + "Core200S": "Core200S", + "LAP-C201S-AUSR": "Core200S", # Alt ID Model Core200S + "LAP-C202S-WUSR": "Core200S", # Alt ID Model Core200S + "Core300S": "Core300S", + "LAP-C301S-WJP": "Core300S", # Alt ID Model Core300S + "Core400S": "Core400S", + "LAP-C401S-WJP": "Core400S", # Alt ID Model Core400S + "LAP-C401S-WUSR": "Core400S", # Alt ID Model Core400S + "LAP-C401S-WAAA": "Core400S", # Alt ID Model Core400S + "Core600S": "Core600S", + "LAP-C601S-WUS": "Core600S", # Alt ID Model Core600S + "LAP-C601S-WUSR": "Core600S", # Alt ID Model Core600S + "LAP-C601S-WEU": "Core600S", # Alt ID Model Core600S +} diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index f16a785ee1e..44e74209c30 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -14,26 +14,16 @@ from homeassistant.util.percentage import ( ) from .common import VeSyncDevice -from .const import DOMAIN, VS_DISCOVERY, VS_FANS +from .const import DOMAIN, SKU_TO_BASE_DEVICE, VS_DISCOVERY, VS_FANS _LOGGER = logging.getLogger(__name__) DEV_TYPE_TO_HA = { "LV-PUR131S": "fan", - "LV-RH131S": "fan", # Alt ID Model LV-PUR131S "Core200S": "fan", - "LAP-C201S-AUSR": "fan", # Alt ID Model Core200S - "LAP-C202S-WUSR": "fan", # Alt ID Model Core200S "Core300S": "fan", - "LAP-C301S-WJP": "fan", # Alt ID Model Core300S "Core400S": "fan", - "LAP-C401S-WJP": "fan", # Alt ID Model Core400S - "LAP-C401S-WUSR": "fan", # Alt ID Model Core400S - "LAP-C401S-WAAA": "fan", # Alt ID Model Core400S "Core600S": "fan", - "LAP-C601S-WUS": "fan", # Alt ID Model Core600S - "LAP-C601S-WUSR": "fan", # Alt ID Model Core600S - "LAP-C601S-WEU": "fan", # Alt ID Model Core600S } FAN_MODE_AUTO = "auto" @@ -41,37 +31,17 @@ FAN_MODE_SLEEP = "sleep" PRESET_MODES = { "LV-PUR131S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], - "LV-RH131S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model LV-PUR131S "Core200S": [FAN_MODE_SLEEP], - "LAP-C201S-AUSR": [FAN_MODE_SLEEP], # Alt ID Model Core200S - "LAP-C202S-WUSR": [FAN_MODE_SLEEP], # Alt ID Model Core200S "Core300S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], - "LAP-C301S-WJP": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core300S "Core400S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], - "LAP-C401S-WJP": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core400S - "LAP-C401S-WUSR": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core400S - "LAP-C401S-WAAA": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core400S "Core600S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], - "LAP-C601S-WUS": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core600S - "LAP-C601S-WUSR": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core600S - "LAP-C601S-WEU": [FAN_MODE_AUTO, FAN_MODE_SLEEP], # Alt ID Model Core600S } SPEED_RANGE = { # off is not included "LV-PUR131S": (1, 3), - "LV-RH131S": (1, 3), # ALt ID Model LV-PUR131S "Core200S": (1, 3), - "LAP-C201S-AUSR": (1, 3), # ALt ID Model Core200S - "LAP-C202S-WUSR": (1, 3), # ALt ID Model Core200S "Core300S": (1, 3), - "LAP-C301S-WJP": (1, 3), # ALt ID Model Core300S "Core400S": (1, 4), - "LAP-C401S-WJP": (1, 4), # ALt ID Model Core400S - "LAP-C401S-WUSR": (1, 4), # ALt ID Model Core400S - "LAP-C401S-WAAA": (1, 4), # ALt ID Model Core400S "Core600S": (1, 4), - "LAP-C601S-WUS": (1, 4), # ALt ID Model Core600S - "LAP-C601S-WUSR": (1, 4), # ALt ID Model Core600S - "LAP-C601S-WEU": (1, 4), # ALt ID Model Core600S } @@ -99,7 +69,7 @@ def _setup_entities(devices, async_add_entities): """Check if device is online and add entity.""" entities = [] for dev in devices: - if DEV_TYPE_TO_HA.get(dev.device_type) == "fan": + if DEV_TYPE_TO_HA.get(SKU_TO_BASE_DEVICE.get(dev.device_type)) == "fan": entities.append(VeSyncFanHA(dev)) else: _LOGGER.warning( @@ -128,19 +98,21 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): and (current_level := self.smartfan.fan_level) is not None ): return ranged_value_to_percentage( - SPEED_RANGE[self.device.device_type], current_level + SPEED_RANGE[SKU_TO_BASE_DEVICE[self.device.device_type]], current_level ) return None @property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" - return int_states_in_range(SPEED_RANGE[self.device.device_type]) + return int_states_in_range( + SPEED_RANGE[SKU_TO_BASE_DEVICE[self.device.device_type]] + ) @property def preset_modes(self): """Get the list of available preset modes.""" - return PRESET_MODES[self.device.device_type] + return PRESET_MODES[SKU_TO_BASE_DEVICE.get(self.device.device_type)] @property def preset_mode(self): @@ -171,15 +143,9 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): if hasattr(self.smartfan, "night_light"): attr["night_light"] = self.smartfan.night_light - if self.smartfan.details.get("air_quality_value") is not None: - attr["air_quality"] = self.smartfan.details["air_quality_value"] - if hasattr(self.smartfan, "mode"): attr["mode"] = self.smartfan.mode - if hasattr(self.smartfan, "filter_life"): - attr["filter_life"] = self.smartfan.filter_life - return attr def set_percentage(self, percentage): @@ -195,7 +161,7 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): self.smartfan.change_fan_speed( math.ceil( percentage_to_ranged_value( - SPEED_RANGE[self.device.device_type], percentage + SPEED_RANGE[SKU_TO_BASE_DEVICE[self.device.device_type]], percentage ) ) ) diff --git a/homeassistant/components/vesync/sensor.py b/homeassistant/components/vesync/sensor.py index cc69bf36fa6..24ba6f2f0a0 100644 --- a/homeassistant/components/vesync/sensor.py +++ b/homeassistant/components/vesync/sensor.py @@ -1,25 +1,119 @@ """Support for power & energy sensors for VeSync outlets.""" +from collections.abc import Callable +from dataclasses import dataclass import logging from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ENERGY_KILO_WATT_HOUR, + PERCENTAGE, + POWER_WATT, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType -from .common import VeSyncBaseEntity -from .const import DOMAIN, VS_DISCOVERY, VS_SENSORS -from .switch import DEV_TYPE_TO_HA +from .common import VeSyncBaseEntity, VeSyncDevice +from .const import DEV_TYPE_TO_HA, DOMAIN, SKU_TO_BASE_DEVICE, VS_DISCOVERY, VS_SENSORS _LOGGER = logging.getLogger(__name__) +@dataclass +class VeSyncSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[VeSyncDevice], StateType] + + +@dataclass +class VeSyncSensorEntityDescription( + SensorEntityDescription, VeSyncSensorEntityDescriptionMixin +): + """Describe VeSync sensor entity.""" + + exists_fn: Callable[[VeSyncDevice], bool] = lambda _: True + update_fn: Callable[[VeSyncDevice], None] = lambda _: None + + +def update_energy(device): + """Update outlet details and energy usage.""" + device.update() + device.update_energy() + + +def sku_supported(device, supported): + """Get the base device of which a device is an instance.""" + return SKU_TO_BASE_DEVICE.get(device.device_type) in supported + + +def ha_dev_type(device): + """Get the homeassistant device_type for a given device.""" + return DEV_TYPE_TO_HA.get(device.device_type) + + +FILTER_LIFE_SUPPORTED = ["LV-PUR131S", "Core200S", "Core300S", "Core400S", "Core600S"] +AIR_QUALITY_SUPPORTED = ["LV-PUR131S", "Core400S", "Core600S"] +PM25_SUPPORTED = ["Core400S", "Core600S"] + +SENSORS: tuple[VeSyncSensorEntityDescription, ...] = ( + VeSyncSensorEntityDescription( + key="filter-life", + name="Filter Life", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.details["filter_life"], + exists_fn=lambda device: sku_supported(device, FILTER_LIFE_SUPPORTED), + ), + VeSyncSensorEntityDescription( + key="air-quality", + name="Air Quality", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda device: device.details["air_quality"], + exists_fn=lambda device: sku_supported(device, AIR_QUALITY_SUPPORTED), + ), + VeSyncSensorEntityDescription( + key="pm25", + name="PM2.5", + device_class=SensorDeviceClass.PM25, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda device: device.details["air_quality_value"], + exists_fn=lambda device: sku_supported(device, PM25_SUPPORTED), + ), + VeSyncSensorEntityDescription( + key="power", + name="current power", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=POWER_WATT, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda device: device.details["power"], + update_fn=update_energy, + exists_fn=lambda device: ha_dev_type(device) == "outlet", + ), + VeSyncSensorEntityDescription( + key="energy", + name="energy use today", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda device: device.details["energy"], + update_fn=update_energy, + exists_fn=lambda device: ha_dev_type(device) == "outlet", + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -44,107 +138,33 @@ def _setup_entities(devices, async_add_entities): """Check if device is online and add entity.""" entities = [] for dev in devices: - if DEV_TYPE_TO_HA.get(dev.device_type) != "outlet": - # Not an outlet that supports energy/power, so do not create sensor entities - continue - entities.append(VeSyncPowerSensor(dev)) - entities.append(VeSyncEnergySensor(dev)) - + for description in SENSORS: + if description.exists_fn(dev): + entities.append(VeSyncSensorEntity(dev, description)) async_add_entities(entities, update_before_add=True) class VeSyncSensorEntity(VeSyncBaseEntity, SensorEntity): - """Representation of a sensor describing diagnostics of a VeSync outlet.""" + """Representation of a sensor describing a VeSync device.""" - def __init__(self, plug): + entity_description: VeSyncSensorEntityDescription + + def __init__( + self, + device: VeSyncDevice, + description: VeSyncSensorEntityDescription, + ) -> None: """Initialize the VeSync outlet device.""" - super().__init__(plug) - self.smartplug = plug + super().__init__(device) + self.entity_description = description + self._attr_name = f"{super().name} {description.name}" + self._attr_unique_id = f"{super().unique_id}-{description.key}" @property - def entity_category(self): - """Return the diagnostic entity category.""" - return EntityCategory.DIAGNOSTIC + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.device) - -class VeSyncPowerSensor(VeSyncSensorEntity): - """Representation of current power use for a VeSync outlet.""" - - @property - def unique_id(self): - """Return unique ID for power sensor on device.""" - return f"{super().unique_id}-power" - - @property - def name(self): - """Return sensor name.""" - return f"{super().name} current power" - - @property - def device_class(self): - """Return the power device class.""" - return SensorDeviceClass.POWER - - @property - def native_value(self): - """Return the current power usage in W.""" - return self.smartplug.power - - @property - def native_unit_of_measurement(self): - """Return the Watt unit of measurement.""" - return POWER_WATT - - @property - def state_class(self): - """Return the measurement state class.""" - return SensorStateClass.MEASUREMENT - - def update(self): - """Update outlet details and energy usage.""" - self.smartplug.update() - self.smartplug.update_energy() - - -class VeSyncEnergySensor(VeSyncSensorEntity): - """Representation of current day's energy use for a VeSync outlet.""" - - def __init__(self, plug): - """Initialize the VeSync outlet device.""" - super().__init__(plug) - self.smartplug = plug - - @property - def unique_id(self): - """Return unique ID for power sensor on device.""" - return f"{super().unique_id}-energy" - - @property - def name(self): - """Return sensor name.""" - return f"{super().name} energy use today" - - @property - def device_class(self): - """Return the energy device class.""" - return SensorDeviceClass.ENERGY - - @property - def native_value(self): - """Return the today total energy usage in kWh.""" - return self.smartplug.energy_today - - @property - def native_unit_of_measurement(self): - """Return the kWh unit of measurement.""" - return ENERGY_KILO_WATT_HOUR - - @property - def state_class(self): - """Return the total_increasing state class.""" - return SensorStateClass.TOTAL_INCREASING - - def update(self): - """Update outlet details and energy usage.""" - self.smartplug.update() - self.smartplug.update_energy() + def update(self) -> None: + """Run the update function defined for the sensor.""" + return self.entity_description.update_fn(self.device) diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index 282f8d99817..e5fd4c829fe 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -8,20 +8,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .common import VeSyncDevice -from .const import DOMAIN, VS_DISCOVERY, VS_SWITCHES +from .const import DEV_TYPE_TO_HA, DOMAIN, VS_DISCOVERY, VS_SWITCHES _LOGGER = logging.getLogger(__name__) -DEV_TYPE_TO_HA = { - "wifi-switch-1.3": "outlet", - "ESW03-USA": "outlet", - "ESW01-EU": "outlet", - "ESW15-USA": "outlet", - "ESWL01": "switch", - "ESWL03": "switch", - "ESO15-TB": "outlet", -} - async def async_setup_entry( hass: HomeAssistant, From 8e75547ca461f66135909cfb016aa682121cd15a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 30 May 2022 15:36:58 -0600 Subject: [PATCH 1048/3516] Guard against missing data in 1st generation RainMachine controllers (#72632) --- .../components/rainmachine/binary_sensor.py | 16 +++---- .../components/rainmachine/sensor.py | 2 +- .../components/rainmachine/switch.py | 43 +++++++++++-------- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index fb404adb199..730b51c142a 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -158,17 +158,17 @@ class CurrentRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity): def update_from_latest_data(self) -> None: """Update the state.""" if self.entity_description.key == TYPE_FREEZE: - self._attr_is_on = self.coordinator.data["freeze"] + self._attr_is_on = self.coordinator.data.get("freeze") elif self.entity_description.key == TYPE_HOURLY: - self._attr_is_on = self.coordinator.data["hourly"] + self._attr_is_on = self.coordinator.data.get("hourly") elif self.entity_description.key == TYPE_MONTH: - self._attr_is_on = self.coordinator.data["month"] + self._attr_is_on = self.coordinator.data.get("month") elif self.entity_description.key == TYPE_RAINDELAY: - self._attr_is_on = self.coordinator.data["rainDelay"] + self._attr_is_on = self.coordinator.data.get("rainDelay") elif self.entity_description.key == TYPE_RAINSENSOR: - self._attr_is_on = self.coordinator.data["rainSensor"] + self._attr_is_on = self.coordinator.data.get("rainSensor") elif self.entity_description.key == TYPE_WEEKDAY: - self._attr_is_on = self.coordinator.data["weekDay"] + self._attr_is_on = self.coordinator.data.get("weekDay") class ProvisionSettingsBinarySensor(RainMachineEntity, BinarySensorEntity): @@ -188,6 +188,6 @@ class UniversalRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity): def update_from_latest_data(self) -> None: """Update the state.""" if self.entity_description.key == TYPE_FREEZE_PROTECTION: - self._attr_is_on = self.coordinator.data["freezeProtectEnabled"] + self._attr_is_on = self.coordinator.data.get("freezeProtectEnabled") elif self.entity_description.key == TYPE_HOT_DAYS: - self._attr_is_on = self.coordinator.data["hotDaysExtraWatering"] + self._attr_is_on = self.coordinator.data.get("hotDaysExtraWatering") diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index b825faca7e1..a2b0f7cd539 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -198,7 +198,7 @@ class UniversalRestrictionsSensor(RainMachineEntity, SensorEntity): def update_from_latest_data(self) -> None: """Update the state.""" if self.entity_description.key == TYPE_FREEZE_TEMP: - self._attr_native_value = self.coordinator.data["freezeProtectTemp"] + self._attr_native_value = self.coordinator.data.get("freezeProtectTemp") class ZoneTimeRemainingSensor(RainMachineEntity, SensorEntity): diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 007aec97a3e..a220aafa2a5 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -389,23 +389,32 @@ class RainMachineZone(RainMachineActivitySwitch): self._attr_is_on = bool(data["state"]) - self._attr_extra_state_attributes.update( - { - ATTR_AREA: round(data["waterSense"]["area"], 2), - ATTR_CURRENT_CYCLE: data["cycle"], - ATTR_FIELD_CAPACITY: round(data["waterSense"]["fieldCapacity"], 2), - ATTR_ID: data["uid"], - ATTR_NO_CYCLES: data["noOfCycles"], - ATTR_PRECIP_RATE: round(data["waterSense"]["precipitationRate"], 2), - ATTR_RESTRICTIONS: data["restriction"], - ATTR_SLOPE: SLOPE_TYPE_MAP.get(data["slope"], 99), - ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(data["soil"], 99), - ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(data["group_id"], 99), - ATTR_STATUS: RUN_STATE_MAP[data["state"]], - ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(data.get("sun")), - ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(data["type"], 99), - } - ) + attrs = { + ATTR_CURRENT_CYCLE: data["cycle"], + ATTR_ID: data["uid"], + ATTR_NO_CYCLES: data["noOfCycles"], + ATTR_RESTRICTIONS: data("restriction"), + ATTR_SLOPE: SLOPE_TYPE_MAP.get(data["slope"], 99), + ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(data["soil"], 99), + ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(data["group_id"], 99), + ATTR_STATUS: RUN_STATE_MAP[data["state"]], + ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(data.get("sun")), + ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(data["type"], 99), + } + + if "waterSense" in data: + if "area" in data["waterSense"]: + attrs[ATTR_AREA] = round(data["waterSense"]["area"], 2) + if "fieldCapacity" in data["waterSense"]: + attrs[ATTR_FIELD_CAPACITY] = round( + data["waterSense"]["fieldCapacity"], 2 + ) + if "precipitationRate" in data["waterSense"]: + attrs[ATTR_PRECIP_RATE] = round( + data["waterSense"]["precipitationRate"], 2 + ) + + self._attr_extra_state_attributes.update(attrs) class RainMachineZoneEnabled(RainMachineEnabledSwitch): From 59f155b4825d0b94d8a2841cf0109e009ccb81ca Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Mon, 30 May 2022 23:37:28 +0200 Subject: [PATCH 1049/3516] Fix homewizard diagnostics and add tests (#72611) --- .coveragerc | 1 - .../components/homewizard/diagnostics.py | 9 ++-- tests/components/homewizard/conftest.py | 49 ++++++++++++++++++- .../components/homewizard/fixtures/data.json | 16 ++++++ .../homewizard/fixtures/device.json | 7 +++ .../components/homewizard/fixtures/state.json | 5 ++ .../components/homewizard/test_diagnostics.py | 47 ++++++++++++++++++ 7 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 tests/components/homewizard/fixtures/data.json create mode 100644 tests/components/homewizard/fixtures/device.json create mode 100644 tests/components/homewizard/fixtures/state.json create mode 100644 tests/components/homewizard/test_diagnostics.py diff --git a/.coveragerc b/.coveragerc index 19f305dbf3b..4d05e704065 100644 --- a/.coveragerc +++ b/.coveragerc @@ -492,7 +492,6 @@ omit = homeassistant/components/homematic/* homeassistant/components/home_plus_control/api.py homeassistant/components/home_plus_control/switch.py - homeassistant/components/homewizard/diagnostics.py homeassistant/components/homeworks/* homeassistant/components/honeywell/__init__.py homeassistant/components/honeywell/climate.py diff --git a/homeassistant/components/homewizard/diagnostics.py b/homeassistant/components/homewizard/diagnostics.py index 3dd55933291..a97d2507098 100644 --- a/homeassistant/components/homewizard/diagnostics.py +++ b/homeassistant/components/homewizard/diagnostics.py @@ -1,6 +1,7 @@ """Diagnostics support for P1 Monitor.""" from __future__ import annotations +from dataclasses import asdict from typing import Any from homeassistant.components.diagnostics import async_redact_data @@ -21,10 +22,10 @@ async def async_get_config_entry_diagnostics( coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] meter_data = { - "device": coordinator.api.device.todict(), - "data": coordinator.api.data.todict(), - "state": coordinator.api.state.todict() - if coordinator.api.state is not None + "device": asdict(coordinator.data["device"]), + "data": asdict(coordinator.data["data"]), + "state": asdict(coordinator.data["state"]) + if coordinator.data["state"] is not None else None, } diff --git a/tests/components/homewizard/conftest.py b/tests/components/homewizard/conftest.py index 15993aa35ed..1617db35458 100644 --- a/tests/components/homewizard/conftest.py +++ b/tests/components/homewizard/conftest.py @@ -1,10 +1,15 @@ """Fixtures for HomeWizard integration tests.""" +import json +from unittest.mock import AsyncMock, patch + +from homewizard_energy.models import Data, Device, State import pytest from homeassistant.components.homewizard.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture @pytest.fixture @@ -25,6 +30,46 @@ def mock_config_entry() -> MockConfigEntry: return MockConfigEntry( title="Product Name (aabbccddeeff)", domain=DOMAIN, - data={}, + data={CONF_IP_ADDRESS: "1.2.3.4"}, unique_id="aabbccddeeff", ) + + +@pytest.fixture +def mock_homewizardenergy(): + """Return a mocked P1 meter.""" + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + ) as device: + client = device.return_value + client.device = AsyncMock( + return_value=Device.from_dict( + json.loads(load_fixture("homewizard/device.json")) + ) + ) + client.data = AsyncMock( + return_value=Data.from_dict( + json.loads(load_fixture("homewizard/data.json")) + ) + ) + client.state = AsyncMock( + return_value=State.from_dict( + json.loads(load_fixture("homewizard/state.json")) + ) + ) + yield device + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homewizardenergy: AsyncMock, +) -> MockConfigEntry: + """Set up the HomeWizard integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/homewizard/fixtures/data.json b/tests/components/homewizard/fixtures/data.json new file mode 100644 index 00000000000..b6eada38038 --- /dev/null +++ b/tests/components/homewizard/fixtures/data.json @@ -0,0 +1,16 @@ +{ + "smr_version": 50, + "meter_model": "ISKRA 2M550T-101", + "wifi_ssid": "My Wi-Fi", + "wifi_strength": 100, + "total_power_import_t1_kwh": 1234.111, + "total_power_import_t2_kwh": 5678.222, + "total_power_export_t1_kwh": 4321.333, + "total_power_export_t2_kwh": 8765.444, + "active_power_w": -123, + "active_power_l1_w": -123, + "active_power_l2_w": 456, + "active_power_l3_w": 123.456, + "total_gas_m3": 1122.333, + "gas_timestamp": 210314112233 +} diff --git a/tests/components/homewizard/fixtures/device.json b/tests/components/homewizard/fixtures/device.json new file mode 100644 index 00000000000..493daa12b94 --- /dev/null +++ b/tests/components/homewizard/fixtures/device.json @@ -0,0 +1,7 @@ +{ + "product_type": "HWE-P1", + "product_name": "P1 Meter", + "serial": "3c39e7aabbcc", + "firmware_version": "2.11", + "api_version": "v1" +} diff --git a/tests/components/homewizard/fixtures/state.json b/tests/components/homewizard/fixtures/state.json new file mode 100644 index 00000000000..bbc0242ed58 --- /dev/null +++ b/tests/components/homewizard/fixtures/state.json @@ -0,0 +1,5 @@ +{ + "power_on": true, + "switch_lock": false, + "brightness": 255 +} diff --git a/tests/components/homewizard/test_diagnostics.py b/tests/components/homewizard/test_diagnostics.py new file mode 100644 index 00000000000..e477c94d914 --- /dev/null +++ b/tests/components/homewizard/test_diagnostics.py @@ -0,0 +1,47 @@ +"""Tests for diagnostics data.""" +from aiohttp import ClientSession + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, + init_integration: MockConfigEntry, +): + """Test diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, init_integration + ) == { + "entry": {"ip_address": REDACTED}, + "data": { + "device": { + "product_name": "P1 Meter", + "product_type": "HWE-P1", + "serial": REDACTED, + "api_version": "v1", + "firmware_version": "2.11", + }, + "data": { + "smr_version": 50, + "meter_model": "ISKRA 2M550T-101", + "wifi_ssid": REDACTED, + "wifi_strength": 100, + "total_power_import_t1_kwh": 1234.111, + "total_power_import_t2_kwh": 5678.222, + "total_power_export_t1_kwh": 4321.333, + "total_power_export_t2_kwh": 8765.444, + "active_power_w": -123, + "active_power_l1_w": -123, + "active_power_l2_w": 456, + "active_power_l3_w": 123.456, + "total_gas_m3": 1122.333, + "gas_timestamp": "2021-03-14T11:22:33", + }, + "state": {"power_on": True, "switch_lock": False, "brightness": 255}, + }, + } From f33517ef2c0635e601ebc3b1e484ef647211f71b Mon Sep 17 00:00:00 2001 From: Shawn Saenger Date: Sun, 29 May 2022 10:33:33 -0600 Subject: [PATCH 1050/3516] Incorporate various improvements for the ws66i integration (#71717) * Improve readability and remove unused code * Remove ws66i custom services. Scenes can be used instead. * Unmute WS66i Zone when volume changes * Raise CannotConnect instead of ConnectionError in validation method * Move _verify_connection() method to module level --- homeassistant/components/ws66i/__init__.py | 5 +- homeassistant/components/ws66i/config_flow.py | 48 +- homeassistant/components/ws66i/const.py | 6 +- homeassistant/components/ws66i/coordinator.py | 11 +- .../components/ws66i/media_player.py | 67 +-- homeassistant/components/ws66i/models.py | 2 - homeassistant/components/ws66i/services.yaml | 15 - homeassistant/components/ws66i/strings.json | 3 - .../components/ws66i/translations/en.json | 3 - tests/components/ws66i/test_config_flow.py | 6 +- tests/components/ws66i/test_init.py | 80 ++++ tests/components/ws66i/test_media_player.py | 414 +++++------------- 12 files changed, 251 insertions(+), 409 deletions(-) delete mode 100644 homeassistant/components/ws66i/services.yaml create mode 100644 tests/components/ws66i/test_init.py diff --git a/homeassistant/components/ws66i/__init__.py b/homeassistant/components/ws66i/__init__.py index 232c4390f19..dea1b470b9e 100644 --- a/homeassistant/components/ws66i/__init__.py +++ b/homeassistant/components/ws66i/__init__.py @@ -94,8 +94,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: zones=zones, ) + @callback def shutdown(event): - """Close the WS66i connection to the amplifier and save snapshots.""" + """Close the WS66i connection to the amplifier.""" ws66i.close() entry.async_on_unload(entry.add_update_listener(_update_listener)) @@ -119,6 +120,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/ws66i/config_flow.py b/homeassistant/components/ws66i/config_flow.py index a8f098faadd..b84872da036 100644 --- a/homeassistant/components/ws66i/config_flow.py +++ b/homeassistant/components/ws66i/config_flow.py @@ -1,5 +1,6 @@ """Config flow for WS66i 6-Zone Amplifier integration.""" import logging +from typing import Any from pyws66i import WS66i, get_ws66i import voluptuous as vol @@ -50,22 +51,34 @@ def _sources_from_config(data): } -async def validate_input(hass: core.HomeAssistant, input_data): - """Validate the user input allows us to connect. +def _verify_connection(ws66i: WS66i) -> bool: + """Verify a connection can be made to the WS66i.""" + try: + ws66i.open() + except ConnectionError as err: + raise CannotConnect from err + + # Connection successful. Verify correct port was opened + # Test on FIRST_ZONE because this zone will always be valid + ret_val = ws66i.zone_status(FIRST_ZONE) + + ws66i.close() + + return bool(ret_val) + + +async def validate_input( + hass: core.HomeAssistant, input_data: dict[str, Any] +) -> dict[str, Any]: + """Validate the user input. Data has the keys from DATA_SCHEMA with values provided by the user. """ ws66i: WS66i = get_ws66i(input_data[CONF_IP_ADDRESS]) - await hass.async_add_executor_job(ws66i.open) - # No exception. run a simple test to make sure we opened correct port - # Test on FIRST_ZONE because this zone will always be valid - ret_val = await hass.async_add_executor_job(ws66i.zone_status, FIRST_ZONE) - if ret_val is None: - ws66i.close() - raise ConnectionError("Not a valid WS66i connection") - # Validation done. No issues. Close the connection - ws66i.close() + is_valid: bool = await hass.async_add_executor_job(_verify_connection, ws66i) + if not is_valid: + raise CannotConnect("Not a valid WS66i connection") # Return info that you want to store in the config entry. return {CONF_IP_ADDRESS: input_data[CONF_IP_ADDRESS]} @@ -82,17 +95,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: try: info = await validate_input(self.hass, user_input) - # Data is valid. Add default values for options flow. + except CannotConnect: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + # Data is valid. Create a config entry. return self.async_create_entry( title="WS66i Amp", data=info, options={CONF_SOURCES: INIT_OPTIONS_DEFAULT}, ) - except ConnectionError: - errors["base"] = "cannot_connect" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors diff --git a/homeassistant/components/ws66i/const.py b/homeassistant/components/ws66i/const.py index ec4439a690d..f824d991c1d 100644 --- a/homeassistant/components/ws66i/const.py +++ b/homeassistant/components/ws66i/const.py @@ -1,4 +1,5 @@ """Constants for the Soundavo WS66i 6-Zone Amplifier Media Player component.""" +from datetime import timedelta DOMAIN = "ws66i" @@ -20,5 +21,6 @@ INIT_OPTIONS_DEFAULT = { "6": "Source 6", } -SERVICE_SNAPSHOT = "snapshot" -SERVICE_RESTORE = "restore" +POLL_INTERVAL = timedelta(seconds=30) + +MAX_VOL = 38 diff --git a/homeassistant/components/ws66i/coordinator.py b/homeassistant/components/ws66i/coordinator.py index a9a274756b5..be8ae3aad38 100644 --- a/homeassistant/components/ws66i/coordinator.py +++ b/homeassistant/components/ws66i/coordinator.py @@ -1,7 +1,6 @@ """Coordinator for WS66i.""" from __future__ import annotations -from datetime import timedelta import logging from pyws66i import WS66i, ZoneStatus @@ -9,12 +8,12 @@ from pyws66i import WS66i, ZoneStatus from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from .const import POLL_INTERVAL + _LOGGER = logging.getLogger(__name__) -POLL_INTERVAL = timedelta(seconds=30) - -class Ws66iDataUpdateCoordinator(DataUpdateCoordinator): +class Ws66iDataUpdateCoordinator(DataUpdateCoordinator[list[ZoneStatus]]): """DataUpdateCoordinator to gather data for WS66i Zones.""" def __init__( @@ -43,11 +42,9 @@ class Ws66iDataUpdateCoordinator(DataUpdateCoordinator): data.append(data_zone) - # HA will call my entity's _handle_coordinator_update() return data async def _async_update_data(self) -> list[ZoneStatus]: """Fetch data for each of the zones.""" - # HA will call my entity's _handle_coordinator_update() - # The data I pass back here can be accessed through coordinator.data. + # The data that is returned here can be accessed through coordinator.data. return await self.hass.async_add_executor_job(self._update_all_zones) diff --git a/homeassistant/components/ws66i/media_player.py b/homeassistant/components/ws66i/media_player.py index c0e62fe773c..7cd897e9c1a 100644 --- a/homeassistant/components/ws66i/media_player.py +++ b/homeassistant/components/ws66i/media_player.py @@ -1,6 +1,4 @@ """Support for interfacing with WS66i 6 zone home audio controller.""" -from copy import deepcopy - from pyws66i import WS66i, ZoneStatus from homeassistant.components.media_player import ( @@ -10,22 +8,16 @@ from homeassistant.components.media_player import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.entity_platform import ( - AddEntitiesCallback, - async_get_current_platform, -) +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT +from .const import DOMAIN, MAX_VOL from .coordinator import Ws66iDataUpdateCoordinator from .models import Ws66iData PARALLEL_UPDATES = 1 -MAX_VOL = 38 - async def async_setup_entry( hass: HomeAssistant, @@ -48,23 +40,8 @@ async def async_setup_entry( for idx, zone_id in enumerate(ws66i_data.zones) ) - # Set up services - platform = async_get_current_platform() - platform.async_register_entity_service( - SERVICE_SNAPSHOT, - {}, - "snapshot", - ) - - platform.async_register_entity_service( - SERVICE_RESTORE, - {}, - "async_restore", - ) - - -class Ws66iZone(CoordinatorEntity, MediaPlayerEntity): +class Ws66iZone(CoordinatorEntity[Ws66iDataUpdateCoordinator], MediaPlayerEntity): """Representation of a WS66i amplifier zone.""" def __init__( @@ -82,8 +59,6 @@ class Ws66iZone(CoordinatorEntity, MediaPlayerEntity): self._ws66i_data: Ws66iData = ws66i_data self._zone_id: int = zone_id self._zone_id_idx: int = data_idx - self._coordinator = coordinator - self._snapshot: ZoneStatus = None self._status: ZoneStatus = coordinator.data[data_idx] self._attr_source_list = ws66i_data.sources.name_list self._attr_unique_id = f"{entry_id}_{self._zone_id}" @@ -131,20 +106,6 @@ class Ws66iZone(CoordinatorEntity, MediaPlayerEntity): self._set_attrs_from_status() self.async_write_ha_state() - @callback - def snapshot(self): - """Save zone's current state.""" - self._snapshot = deepcopy(self._status) - - async def async_restore(self): - """Restore saved state.""" - if not self._snapshot: - raise HomeAssistantError("There is no snapshot to restore") - - await self.hass.async_add_executor_job(self._ws66i.restore_zone, self._snapshot) - self._status = self._snapshot - self._async_update_attrs_write_ha_state() - async def async_select_source(self, source): """Set input source.""" idx = self._ws66i_data.sources.name_id[source] @@ -180,24 +141,30 @@ class Ws66iZone(CoordinatorEntity, MediaPlayerEntity): async def async_set_volume_level(self, volume): """Set volume level, range 0..1.""" - await self.hass.async_add_executor_job( - self._ws66i.set_volume, self._zone_id, int(volume * MAX_VOL) - ) - self._status.volume = int(volume * MAX_VOL) + await self.hass.async_add_executor_job(self._set_volume, int(volume * MAX_VOL)) self._async_update_attrs_write_ha_state() async def async_volume_up(self): """Volume up the media player.""" await self.hass.async_add_executor_job( - self._ws66i.set_volume, self._zone_id, min(self._status.volume + 1, MAX_VOL) + self._set_volume, min(self._status.volume + 1, MAX_VOL) ) - self._status.volume = min(self._status.volume + 1, MAX_VOL) self._async_update_attrs_write_ha_state() async def async_volume_down(self): """Volume down media player.""" await self.hass.async_add_executor_job( - self._ws66i.set_volume, self._zone_id, max(self._status.volume - 1, 0) + self._set_volume, max(self._status.volume - 1, 0) ) - self._status.volume = max(self._status.volume - 1, 0) self._async_update_attrs_write_ha_state() + + def _set_volume(self, volume: int) -> None: + """Set the volume of the media player.""" + # Can't set a new volume level when this zone is muted. + # Follow behavior of keypads, where zone is unmuted when volume changes. + if self._status.mute: + self._ws66i.set_mute(self._zone_id, False) + self._status.mute = False + + self._ws66i.set_volume(self._zone_id, volume) + self._status.volume = volume diff --git a/homeassistant/components/ws66i/models.py b/homeassistant/components/ws66i/models.py index d84ee56a4a1..84f481b9a4a 100644 --- a/homeassistant/components/ws66i/models.py +++ b/homeassistant/components/ws66i/models.py @@ -7,8 +7,6 @@ from pyws66i import WS66i from .coordinator import Ws66iDataUpdateCoordinator -# A dataclass is basically a struct in C/C++ - @dataclass class SourceRep: diff --git a/homeassistant/components/ws66i/services.yaml b/homeassistant/components/ws66i/services.yaml deleted file mode 100644 index cedd1d3546a..00000000000 --- a/homeassistant/components/ws66i/services.yaml +++ /dev/null @@ -1,15 +0,0 @@ -snapshot: - name: Snapshot - description: Take a snapshot of the media player zone. - target: - entity: - integration: ws66i - domain: media_player - -restore: - name: Restore - description: Restore a snapshot of the media player zone. - target: - entity: - integration: ws66i - domain: media_player diff --git a/homeassistant/components/ws66i/strings.json b/homeassistant/components/ws66i/strings.json index fcfa64d7e22..ec5bc621a89 100644 --- a/homeassistant/components/ws66i/strings.json +++ b/homeassistant/components/ws66i/strings.json @@ -11,9 +11,6 @@ "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%]" } }, "options": { diff --git a/homeassistant/components/ws66i/translations/en.json b/homeassistant/components/ws66i/translations/en.json index 30ef1e4205a..fd4b170b378 100644 --- a/homeassistant/components/ws66i/translations/en.json +++ b/homeassistant/components/ws66i/translations/en.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "already_configured": "Device is already configured" - }, "error": { "cannot_connect": "Failed to connect", "unknown": "Unexpected error" diff --git a/tests/components/ws66i/test_config_flow.py b/tests/components/ws66i/test_config_flow.py index d426e62c012..4fe3554941d 100644 --- a/tests/components/ws66i/test_config_flow.py +++ b/tests/components/ws66i/test_config_flow.py @@ -1,7 +1,7 @@ """Test the WS66i 6-Zone Amplifier 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.ws66i.const import ( CONF_SOURCE_1, CONF_SOURCE_2, @@ -15,15 +15,15 @@ from homeassistant.components.ws66i.const import ( ) from homeassistant.const import CONF_IP_ADDRESS +from .test_media_player import AttrDict + from tests.common import MockConfigEntry -from tests.components.ws66i.test_media_player import AttrDict CONFIG = {CONF_IP_ADDRESS: "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/ws66i/test_init.py b/tests/components/ws66i/test_init.py new file mode 100644 index 00000000000..557c53e97aa --- /dev/null +++ b/tests/components/ws66i/test_init.py @@ -0,0 +1,80 @@ +"""Test the WS66i 6-Zone Amplifier init file.""" +from unittest.mock import patch + +from homeassistant.components.ws66i.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState + +from .test_media_player import ( + MOCK_CONFIG, + MOCK_DEFAULT_OPTIONS, + MOCK_OPTIONS, + MockWs66i, +) + +from tests.common import MockConfigEntry + +ZONE_1_ID = "media_player.zone_11" + + +async def test_cannot_connect(hass): + """Test connection error.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.ws66i.get_ws66i", + new=lambda *a: MockWs66i(fail_open=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 hass.states.get(ZONE_1_ID) is None + + +async def test_cannot_connect_2(hass): + """Test connection error pt 2.""" + # Another way to test same case as test_cannot_connect + ws66i = MockWs66i() + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_DEFAULT_OPTIONS + ) + config_entry.add_to_hass(hass) + + with patch.object(MockWs66i, "open", side_effect=ConnectionError): + with patch( + "homeassistant.components.ws66i.get_ws66i", + new=lambda *a: ws66i, + ): + 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 hass.states.get(ZONE_1_ID) is None + + +async def test_unload_config_entry(hass): + """Test unloading config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.ws66i.get_ws66i", + new=lambda *a: MockWs66i(), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN][config_entry.entry_id] + + with patch.object(MockWs66i, "close") as method_call: + await config_entry.async_unload(hass) + await hass.async_block_till_done() + + assert method_call.called + + assert not hass.data[DOMAIN] diff --git a/tests/components/ws66i/test_media_player.py b/tests/components/ws66i/test_media_player.py index 6fc1e00d827..fbe6a7b2782 100644 --- a/tests/components/ws66i/test_media_player.py +++ b/tests/components/ws66i/test_media_player.py @@ -2,28 +2,22 @@ from collections import defaultdict from unittest.mock import patch -import pytest - +from homeassistant.components.media_player import MediaPlayerEntityFeature from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_VOLUME_LEVEL, DOMAIN as MEDIA_PLAYER_DOMAIN, SERVICE_SELECT_SOURCE, - SUPPORT_SELECT_SOURCE, - SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, ) from homeassistant.components.ws66i.const import ( CONF_SOURCES, DOMAIN, INIT_OPTIONS_DEFAULT, - SERVICE_RESTORE, - SERVICE_SNAPSHOT, + MAX_VOL, + POLL_INTERVAL, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_IP_ADDRESS, SERVICE_TURN_OFF, @@ -35,10 +29,10 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed MOCK_SOURCE_DIC = { "1": "one", @@ -125,47 +119,52 @@ class MockWs66i: async def test_setup_success(hass): """Test connection success.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + with patch( "homeassistant.components.ws66i.get_ws66i", new=lambda *a: MockWs66i(), ): - config_entry = MockConfigEntry( - domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS - ) - config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(ZONE_1_ID) is not None + + assert config_entry.state is ConfigEntryState.LOADED + assert hass.states.get(ZONE_1_ID) is not None async def _setup_ws66i(hass, ws66i) -> MockConfigEntry: + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_DEFAULT_OPTIONS + ) + config_entry.add_to_hass(hass) + with patch( "homeassistant.components.ws66i.get_ws66i", new=lambda *a: ws66i, ): - config_entry = MockConfigEntry( - domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_DEFAULT_OPTIONS - ) - 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 + return config_entry async def _setup_ws66i_with_options(hass, ws66i) -> MockConfigEntry: + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + with patch( "homeassistant.components.ws66i.get_ws66i", new=lambda *a: ws66i, ): - config_entry = MockConfigEntry( - domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS - ) - 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 + return config_entry async def _call_media_player_service(hass, name, data): @@ -174,172 +173,10 @@ async def _call_media_player_service(hass, name, data): ) -async def _call_ws66i_service(hass, name, data): - await hass.services.async_call(DOMAIN, name, service_data=data, blocking=True) - - -async def test_cannot_connect(hass): - """Test connection error.""" - with patch( - "homeassistant.components.ws66i.get_ws66i", - new=lambda *a: MockWs66i(fail_open=True), - ): - config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert hass.states.get(ZONE_1_ID) is None - - -async def test_cannot_connect_2(hass): - """Test connection error pt 2.""" - # Another way to test same case as test_cannot_connect - ws66i = MockWs66i() - - with patch.object(MockWs66i, "open", side_effect=ConnectionError): - await _setup_ws66i(hass, ws66i) - assert hass.states.get(ZONE_1_ID) is None - - -async def test_service_calls_with_entity_id(hass): - """Test snapshot save/restore service calls.""" - _ = await _setup_ws66i_with_options(hass, MockWs66i()) - - # Changing media player to new state - await _call_media_player_service( - hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} - ) - await _call_media_player_service( - hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} - ) - - # Saving existing values - await _call_ws66i_service(hass, SERVICE_SNAPSHOT, {"entity_id": ZONE_1_ID}) - - # Changing media player to new state - await _call_media_player_service( - hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} - ) - await _call_media_player_service( - hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "three"} - ) - await hass.async_block_till_done() - - # Restoring other media player to its previous state - # The zone should not be restored - with pytest.raises(HomeAssistantError): - await _call_ws66i_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_2_ID}) - await hass.async_block_till_done() - - # Checking that values were not (!) restored - state = hass.states.get(ZONE_1_ID) - - assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 1.0 - assert state.attributes[ATTR_INPUT_SOURCE] == "three" - - # Restoring media player to its previous state - await _call_ws66i_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID}) - await hass.async_block_till_done() - - state = hass.states.get(ZONE_1_ID) - - assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.0 - assert state.attributes[ATTR_INPUT_SOURCE] == "one" - - -async def test_service_calls_with_all_entities(hass): - """Test snapshot save/restore service calls with entity id all.""" - _ = await _setup_ws66i_with_options(hass, MockWs66i()) - - # Changing media player to new state - await _call_media_player_service( - hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} - ) - await _call_media_player_service( - hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} - ) - - # Saving existing values - await _call_ws66i_service(hass, SERVICE_SNAPSHOT, {"entity_id": "all"}) - - # Changing media player to new state - await _call_media_player_service( - hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} - ) - await _call_media_player_service( - hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "three"} - ) - - # await coordinator.async_refresh() - # await hass.async_block_till_done() - - # Restoring media player to its previous state - await _call_ws66i_service(hass, SERVICE_RESTORE, {"entity_id": "all"}) - await hass.async_block_till_done() - - state = hass.states.get(ZONE_1_ID) - - assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.0 - assert state.attributes[ATTR_INPUT_SOURCE] == "one" - - -async def test_service_calls_without_relevant_entities(hass): - """Test snapshot save/restore service calls with bad entity id.""" - config_entry = await _setup_ws66i_with_options(hass, MockWs66i()) - - # Changing media player to new state - await _call_media_player_service( - hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} - ) - await _call_media_player_service( - hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} - ) - - ws66i_data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = ws66i_data.coordinator - await coordinator.async_refresh() - await hass.async_block_till_done() - - # Saving existing values - await _call_ws66i_service(hass, SERVICE_SNAPSHOT, {"entity_id": "all"}) - - # Changing media player to new state - await _call_media_player_service( - hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} - ) - await _call_media_player_service( - hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "three"} - ) - - await coordinator.async_refresh() - await hass.async_block_till_done() - - # Restoring media player to its previous state - await _call_ws66i_service(hass, SERVICE_RESTORE, {"entity_id": "light.demo"}) - await hass.async_block_till_done() - - state = hass.states.get(ZONE_1_ID) - - assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 1.0 - assert state.attributes[ATTR_INPUT_SOURCE] == "three" - - -async def test_restore_without_snapshot(hass): - """Test restore when snapshot wasn't called.""" - await _setup_ws66i(hass, MockWs66i()) - - with patch.object(MockWs66i, "restore_zone") as method_call: - with pytest.raises(HomeAssistantError): - await _call_ws66i_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID}) - await hass.async_block_till_done() - - assert not method_call.called - - async def test_update(hass): """Test updating values from ws66i.""" ws66i = MockWs66i() - config_entry = await _setup_ws66i_with_options(hass, ws66i) + _ = await _setup_ws66i_with_options(hass, ws66i) # Changing media player to new state await _call_media_player_service( @@ -350,13 +187,10 @@ async def test_update(hass): ) ws66i.set_source(11, 3) - ws66i.set_volume(11, 38) - - ws66i_data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = ws66i_data.coordinator + ws66i.set_volume(11, MAX_VOL) with patch.object(MockWs66i, "open") as method_call: - await coordinator.async_refresh() + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() assert not method_call.called @@ -371,7 +205,7 @@ async def test_update(hass): async def test_failed_update(hass): """Test updating failure from ws66i.""" ws66i = MockWs66i() - config_entry = await _setup_ws66i_with_options(hass, ws66i) + _ = await _setup_ws66i_with_options(hass, ws66i) # Changing media player to new state await _call_media_player_service( @@ -382,26 +216,25 @@ async def test_failed_update(hass): ) ws66i.set_source(11, 3) - ws66i.set_volume(11, 38) - ws66i_data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = ws66i_data.coordinator - await coordinator.async_refresh() + ws66i.set_volume(11, MAX_VOL) + + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() # Failed update, close called with patch.object(MockWs66i, "zone_status", return_value=None): - await coordinator.async_refresh() + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() assert hass.states.is_state(ZONE_1_ID, STATE_UNAVAILABLE) # A connection re-attempt fails with patch.object(MockWs66i, "zone_status", return_value=None): - await coordinator.async_refresh() + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() # A connection re-attempt succeeds - await coordinator.async_refresh() + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() # confirm entity is back on @@ -418,12 +251,12 @@ async def test_supported_features(hass): state = hass.states.get(ZONE_1_ID) assert ( - SUPPORT_VOLUME_MUTE - | SUPPORT_VOLUME_SET - | SUPPORT_VOLUME_STEP - | SUPPORT_TURN_ON - | SUPPORT_TURN_OFF - | SUPPORT_SELECT_SOURCE + MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.VOLUME_STEP + | MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.SELECT_SOURCE == state.attributes["supported_features"] ) @@ -462,15 +295,13 @@ async def test_select_source(hass): async def test_source_select(hass): - """Test behavior when device has unknown source.""" + """Test source selection simulated from keypad.""" ws66i = MockWs66i() - config_entry = await _setup_ws66i_with_options(hass, ws66i) + _ = await _setup_ws66i_with_options(hass, ws66i) ws66i.set_source(11, 5) - ws66i_data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = ws66i_data.coordinator - await coordinator.async_refresh() + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() state = hass.states.get(ZONE_1_ID) @@ -512,10 +343,7 @@ async def test_mute_volume(hass): async def test_volume_up_down(hass): """Test increasing volume by one.""" ws66i = MockWs66i() - config_entry = await _setup_ws66i(hass, ws66i) - - ws66i_data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = ws66i_data.coordinator + _ = await _setup_ws66i(hass, ws66i) await _call_media_player_service( hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} @@ -525,34 +353,89 @@ async def test_volume_up_down(hass): await _call_media_player_service( hass, SERVICE_VOLUME_DOWN, {"entity_id": ZONE_1_ID} ) - await coordinator.async_refresh() + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() # should not go below zero assert ws66i.zones[11].volume == 0 await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID}) - await coordinator.async_refresh() + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() assert ws66i.zones[11].volume == 1 await _call_media_player_service( hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} ) - await coordinator.async_refresh() + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() - assert ws66i.zones[11].volume == 38 + assert ws66i.zones[11].volume == MAX_VOL await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID}) - await coordinator.async_refresh() + async_fire_time_changed(hass, utcnow() + POLL_INTERVAL) await hass.async_block_till_done() - # should not go above 38 - assert ws66i.zones[11].volume == 38 + # should not go above 38 (MAX_VOL) + assert ws66i.zones[11].volume == MAX_VOL await _call_media_player_service( hass, SERVICE_VOLUME_DOWN, {"entity_id": ZONE_1_ID} ) - assert ws66i.zones[11].volume == 37 + assert ws66i.zones[11].volume == MAX_VOL - 1 + + +async def test_volume_while_mute(hass): + """Test increasing volume by one.""" + ws66i = MockWs66i() + _ = await _setup_ws66i(hass, ws66i) + + # Set vol to a known value + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} + ) + assert ws66i.zones[11].volume == 0 + + # Set mute to a known value, False + await _call_media_player_service( + hass, SERVICE_VOLUME_MUTE, {"entity_id": ZONE_1_ID, "is_volume_muted": False} + ) + assert not ws66i.zones[11].mute + + # Mute the zone + await _call_media_player_service( + hass, SERVICE_VOLUME_MUTE, {"entity_id": ZONE_1_ID, "is_volume_muted": True} + ) + assert ws66i.zones[11].mute + + # Increase volume. Mute state should go back to unmutted + await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID}) + assert ws66i.zones[11].volume == 1 + assert not ws66i.zones[11].mute + + # Mute the zone again + await _call_media_player_service( + hass, SERVICE_VOLUME_MUTE, {"entity_id": ZONE_1_ID, "is_volume_muted": True} + ) + assert ws66i.zones[11].mute + + # Decrease volume. Mute state should go back to unmutted + await _call_media_player_service( + hass, SERVICE_VOLUME_DOWN, {"entity_id": ZONE_1_ID} + ) + assert ws66i.zones[11].volume == 0 + assert not ws66i.zones[11].mute + + # Mute the zone again + await _call_media_player_service( + hass, SERVICE_VOLUME_MUTE, {"entity_id": ZONE_1_ID, "is_volume_muted": True} + ) + assert ws66i.zones[11].mute + + # Set to max volume. Mute state should go back to unmutted + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} + ) + assert ws66i.zones[11].volume == MAX_VOL + assert not ws66i.zones[11].mute async def test_first_run_with_available_zones(hass): @@ -611,82 +494,3 @@ async def test_register_entities_in_1_amp_only(hass): entry = registry.async_get(ZONE_7_ID) assert entry is None - - -async def test_unload_config_entry(hass): - """Test unloading config entry.""" - with patch( - "homeassistant.components.ws66i.get_ws66i", - new=lambda *a: MockWs66i(), - ): - config_entry = MockConfigEntry( - domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS - ) - config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert hass.data[DOMAIN][config_entry.entry_id] - - with patch.object(MockWs66i, "close") as method_call: - await config_entry.async_unload(hass) - await hass.async_block_till_done() - - assert method_call.called - - assert not hass.data[DOMAIN] - - -async def test_restore_snapshot_on_reconnect(hass): - """Test restoring a saved snapshot when reconnecting to amp.""" - ws66i = MockWs66i() - config_entry = await _setup_ws66i_with_options(hass, ws66i) - - # Changing media player to new state - await _call_media_player_service( - hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} - ) - await _call_media_player_service( - hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} - ) - - # Save a snapshot - await _call_ws66i_service(hass, SERVICE_SNAPSHOT, {"entity_id": ZONE_1_ID}) - - ws66i_data = hass.data[DOMAIN][config_entry.entry_id] - coordinator = ws66i_data.coordinator - - # Failed update, - with patch.object(MockWs66i, "zone_status", return_value=None): - await coordinator.async_refresh() - await hass.async_block_till_done() - - assert hass.states.is_state(ZONE_1_ID, STATE_UNAVAILABLE) - - # A connection re-attempt succeeds - await coordinator.async_refresh() - await hass.async_block_till_done() - - # confirm entity is back on - state = hass.states.get(ZONE_1_ID) - - assert hass.states.is_state(ZONE_1_ID, STATE_ON) - assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.0 - assert state.attributes[ATTR_INPUT_SOURCE] == "one" - - # Change states - await _call_media_player_service( - hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} - ) - await _call_media_player_service( - hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "six"} - ) - - # Now confirm that the snapshot before the disconnect works - await _call_ws66i_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID}) - await hass.async_block_till_done() - - state = hass.states.get(ZONE_1_ID) - - assert state.attributes[ATTR_MEDIA_VOLUME_LEVEL] == 0.0 - assert state.attributes[ATTR_INPUT_SOURCE] == "one" From 6bf6a0f7bc292d175807d31b6d80e9b55bb1a76a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 May 2022 13:57:00 -0700 Subject: [PATCH 1051/3516] Convert media player enqueue to an enum (#72406) --- .../components/bluesound/media_player.py | 14 +------ homeassistant/components/heos/media_player.py | 16 +++++--- .../components/media_player/__init__.py | 37 ++++++++++++++++- .../media_player/reproduce_state.py | 3 +- .../components/media_player/services.yaml | 16 ++++++++ .../components/sonos/media_player.py | 9 ++--- .../components/squeezebox/media_player.py | 16 ++++---- tests/components/media_player/test_init.py | 40 +++++++++++++++++++ .../media_player/test_reproduce_state.py | 4 -- 9 files changed, 119 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index e22606b795f..7f1c6b6553f 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -24,10 +24,7 @@ from homeassistant.components.media_player import ( from homeassistant.components.media_player.browse_media import ( async_process_play_media_url, ) -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_ENQUEUE, - MEDIA_TYPE_MUSIC, -) +from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, @@ -1023,11 +1020,7 @@ class BluesoundPlayer(MediaPlayerEntity): return await self.send_bluesound_command(f"Play?seek={float(position)}") async def async_play_media(self, media_type, media_id, **kwargs): - """ - Send the play_media command to the media player. - - If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. - """ + """Send the play_media command to the media player.""" if self.is_grouped and not self.is_master: return @@ -1041,9 +1034,6 @@ class BluesoundPlayer(MediaPlayerEntity): url = f"Play?url={media_id}" - if kwargs.get(ATTR_MEDIA_ENQUEUE): - return await self.send_bluesound_command(url) - return await self.send_bluesound_command(url) async def async_volume_up(self): diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 4cfbe5fe408..ad9225d9b21 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -12,6 +12,7 @@ from typing_extensions import ParamSpec from homeassistant.components import media_source from homeassistant.components.media_player import ( + MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerEntityFeature, ) @@ -73,6 +74,14 @@ CONTROL_TO_SUPPORT = { heos_const.CONTROL_PLAY_NEXT: MediaPlayerEntityFeature.NEXT_TRACK, } +HA_HEOS_ENQUEUE_MAP = { + None: heos_const.ADD_QUEUE_REPLACE_AND_PLAY, + MediaPlayerEnqueue.ADD: heos_const.ADD_QUEUE_ADD_TO_END, + MediaPlayerEnqueue.REPLACE: heos_const.ADD_QUEUE_REPLACE_AND_PLAY, + MediaPlayerEnqueue.NEXT: heos_const.ADD_QUEUE_PLAY_NEXT, + MediaPlayerEnqueue.PLAY: heos_const.ADD_QUEUE_PLAY_NOW, +} + _LOGGER = logging.getLogger(__name__) @@ -224,11 +233,8 @@ class HeosMediaPlayer(MediaPlayerEntity): playlist = next((p for p in playlists if p.name == media_id), None) if not playlist: raise ValueError(f"Invalid playlist '{media_id}'") - add_queue_option = ( - heos_const.ADD_QUEUE_ADD_TO_END - if kwargs.get(ATTR_MEDIA_ENQUEUE) - else heos_const.ADD_QUEUE_REPLACE_AND_PLAY - ) + add_queue_option = HA_HEOS_ENQUEUE_MAP.get(kwargs.get(ATTR_MEDIA_ENQUEUE)) + await self._player.add_to_queue(playlist, add_queue_option) return diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index bf006e2bd4e..f71f3fc2a1f 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -147,6 +147,19 @@ ENTITY_IMAGE_CACHE = {CACHE_IMAGES: collections.OrderedDict(), CACHE_MAXSIZE: 16 SCAN_INTERVAL = dt.timedelta(seconds=10) +class MediaPlayerEnqueue(StrEnum): + """Enqueue types for playing media.""" + + # add given media item to end of the queue + ADD = "add" + # play the given media item next, keep queue + NEXT = "next" + # play the given media item now, keep queue + PLAY = "play" + # play the given media item now, clear queue + REPLACE = "replace" + + class MediaPlayerDeviceClass(StrEnum): """Device class for media players.""" @@ -169,7 +182,9 @@ DEVICE_CLASS_RECEIVER = MediaPlayerDeviceClass.RECEIVER.value MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = { vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string, vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, - vol.Optional(ATTR_MEDIA_ENQUEUE): cv.boolean, + vol.Optional(ATTR_MEDIA_ENQUEUE): vol.Any( + cv.boolean, vol.Coerce(MediaPlayerEnqueue) + ), vol.Optional(ATTR_MEDIA_EXTRA, default={}): dict, } @@ -350,10 +365,30 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "async_select_sound_mode", [MediaPlayerEntityFeature.SELECT_SOUND_MODE], ) + + # Remove in Home Assistant 2022.9 + def _rewrite_enqueue(value): + """Rewrite the enqueue value.""" + if ATTR_MEDIA_ENQUEUE not in value: + pass + elif value[ATTR_MEDIA_ENQUEUE] is True: + value[ATTR_MEDIA_ENQUEUE] = MediaPlayerEnqueue.ADD + _LOGGER.warning( + "Playing media with enqueue set to True is deprecated. Use 'add' instead" + ) + elif value[ATTR_MEDIA_ENQUEUE] is False: + value[ATTR_MEDIA_ENQUEUE] = MediaPlayerEnqueue.PLAY + _LOGGER.warning( + "Playing media with enqueue set to False is deprecated. Use 'play' instead" + ) + + return value + component.async_register_entity_service( SERVICE_PLAY_MEDIA, vol.All( cv.make_entity_service_schema(MEDIA_PLAYER_PLAY_MEDIA_SCHEMA), + _rewrite_enqueue, _rename_keys( media_type=ATTR_MEDIA_CONTENT_TYPE, media_id=ATTR_MEDIA_CONTENT_ID, diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index 586ac61b4e1..bdfc0bf3acb 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -27,7 +27,6 @@ from .const import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, - ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_SOUND_MODE, @@ -118,7 +117,7 @@ async def _async_reproduce_states( if features & MediaPlayerEntityFeature.PLAY_MEDIA: await call_service( SERVICE_PLAY_MEDIA, - [ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_ENQUEUE], + [ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID], ) already_playing = True diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index 2e8585d0127..b2a8ac40262 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -151,6 +151,22 @@ play_media: selector: text: + enqueue: + name: Enqueue + description: If the content should be played now or be added to the queue. + required: false + selector: + select: + options: + - label: "Play now" + value: "play" + - label: "Play next" + value: "next" + - label: "Add to queue" + value: "add" + - label: "Play now and clear queue" + value: "replace" + select_source: name: Select source description: Send the media player the command to change input source. diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 45e11a810ae..fd37e546105 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -18,6 +18,7 @@ import voluptuous as vol from homeassistant.components import media_source, spotify from homeassistant.components.media_player import ( + MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerEntityFeature, async_process_play_media_url, @@ -537,8 +538,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): If media_type is "playlist", media_id should be a Sonos Playlist name. Otherwise, media_id should be a URI. - - If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. """ if spotify.is_spotify_media_type(media_type): media_type = spotify.resolve_spotify_media_type(media_type) @@ -575,7 +574,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): ) if result.shuffle: self.set_shuffle(True) - if kwargs.get(ATTR_MEDIA_ENQUEUE): + if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: plex_plugin.add_to_queue(result.media) else: soco.clear_queue() @@ -585,7 +584,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): share_link = self.coordinator.share_link if share_link.is_share_link(media_id): - if kwargs.get(ATTR_MEDIA_ENQUEUE): + if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: share_link.add_share_link_to_queue(media_id) else: soco.clear_queue() @@ -595,7 +594,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): # If media ID is a relative URL, we serve it from HA. media_id = async_process_play_media_url(self.hass, media_id) - if kwargs.get(ATTR_MEDIA_ENQUEUE): + if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: soco.add_uri_to_queue(media_id) else: soco.play_uri(media_id, force_radio=is_radio) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index d0d1cf89739..cd628a639c5 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( + MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerEntityFeature, ) @@ -469,16 +470,17 @@ class SqueezeBoxEntity(MediaPlayerEntity): await self._player.async_set_power(True) async def async_play_media(self, media_type, media_id, **kwargs): - """ - Send the play_media command to the media player. - - If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the current playlist. - """ - cmd = "play" + """Send the play_media command to the media player.""" index = None - if kwargs.get(ATTR_MEDIA_ENQUEUE): + enqueue: MediaPlayerEnqueue | None = kwargs.get(ATTR_MEDIA_ENQUEUE) + + if enqueue == MediaPlayerEnqueue.ADD: cmd = "add" + elif enqueue == MediaPlayerEnqueue.NEXT: + cmd = "insert" + else: + cmd = "play" if media_source.is_media_source_id(media_id): media_type = MEDIA_TYPE_MUSIC diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index aa5e1b164f4..cb095cbcfe0 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -4,6 +4,8 @@ import base64 from http import HTTPStatus from unittest.mock import patch +import pytest + from homeassistant.components import media_player from homeassistant.components.media_player.browse_media import BrowseMedia from homeassistant.components.websocket_api.const import TYPE_RESULT @@ -251,3 +253,41 @@ async def test_group_members_available_when_off(hass): state = hass.states.get("media_player.bedroom") assert state.state == STATE_OFF assert "group_members" in state.attributes + + +@pytest.mark.parametrize( + "input,expected", + ( + (True, media_player.MediaPlayerEnqueue.ADD), + (False, media_player.MediaPlayerEnqueue.PLAY), + ("play", media_player.MediaPlayerEnqueue.PLAY), + ("next", media_player.MediaPlayerEnqueue.NEXT), + ("add", media_player.MediaPlayerEnqueue.ADD), + ("replace", media_player.MediaPlayerEnqueue.REPLACE), + ), +) +async def test_enqueue_rewrite(hass, input, expected): + """Test that group_members are still available when media_player is off.""" + await async_setup_component( + hass, "media_player", {"media_player": {"platform": "demo"}} + ) + await hass.async_block_till_done() + + # Fake group support for DemoYoutubePlayer + with patch( + "homeassistant.components.demo.media_player.DemoYoutubePlayer.play_media", + ) as mock_play_media: + await hass.services.async_call( + "media_player", + "play_media", + { + "entity_id": "media_player.bedroom", + "media_content_type": "music", + "media_content_id": "1234", + "enqueue": input, + }, + blocking=True, + ) + + assert len(mock_play_media.mock_calls) == 1 + assert mock_play_media.mock_calls[0][2]["enqueue"] == expected diff --git a/tests/components/media_player/test_reproduce_state.py b/tests/components/media_player/test_reproduce_state.py index f1a243337e1..f880130d4bd 100644 --- a/tests/components/media_player/test_reproduce_state.py +++ b/tests/components/media_player/test_reproduce_state.py @@ -6,7 +6,6 @@ from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, - ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, ATTR_SOUND_MODE, @@ -253,7 +252,6 @@ async def test_play_media(hass): value_1 = "dummy_1" value_2 = "dummy_2" - value_3 = "dummy_3" await async_reproduce_states( hass, @@ -275,7 +273,6 @@ async def test_play_media(hass): { ATTR_MEDIA_CONTENT_TYPE: value_1, ATTR_MEDIA_CONTENT_ID: value_2, - ATTR_MEDIA_ENQUEUE: value_3, }, ) ], @@ -294,5 +291,4 @@ async def test_play_media(hass): "entity_id": ENTITY_1, ATTR_MEDIA_CONTENT_TYPE: value_1, ATTR_MEDIA_CONTENT_ID: value_2, - ATTR_MEDIA_ENQUEUE: value_3, } From ce4825c9e2886081ce8606336c2c7cd07ca918d0 Mon Sep 17 00:00:00 2001 From: Matrix Date: Mon, 30 May 2022 02:54:23 +0800 Subject: [PATCH 1052/3516] Fix yolink device unavailable on startup (#72579) * fetch device state on startup * Suggest change * suggest fix * fix * fix * Fix suggest * suggest fix --- homeassistant/components/yolink/__init__.py | 68 ++++++++++++--- .../components/yolink/binary_sensor.py | 44 ++++++---- homeassistant/components/yolink/const.py | 2 +- .../components/yolink/coordinator.py | 86 +++---------------- homeassistant/components/yolink/entity.py | 20 +++-- homeassistant/components/yolink/sensor.py | 38 ++++---- homeassistant/components/yolink/siren.py | 39 +++++---- homeassistant/components/yolink/switch.py | 41 +++++---- 8 files changed, 177 insertions(+), 161 deletions(-) diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 2c85344c54b..7eb6b0229f0 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -1,25 +1,28 @@ """The yolink integration.""" from __future__ import annotations +import asyncio from datetime import timedelta -import logging +import async_timeout from yolink.client import YoLinkClient +from yolink.device import YoLinkDevice +from yolink.exception import YoLinkAuthFailError, YoLinkClientError +from yolink.model import BRDP from yolink.mqtt_client import MqttClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow from . import api -from .const import ATTR_CLIENT, ATTR_COORDINATOR, ATTR_MQTT_CLIENT, DOMAIN +from .const import ATTR_CLIENT, ATTR_COORDINATORS, ATTR_DEVICE, ATTR_MQTT_CLIENT, DOMAIN from .coordinator import YoLinkCoordinator SCAN_INTERVAL = timedelta(minutes=5) -_LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SIREN, Platform.SWITCH] @@ -41,18 +44,63 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: yolink_http_client = YoLinkClient(auth_mgr) yolink_mqtt_client = MqttClient(auth_mgr) - coordinator = YoLinkCoordinator(hass, yolink_http_client, yolink_mqtt_client) - await coordinator.init_coordinator() + + def on_message_callback(message: tuple[str, BRDP]) -> None: + data = message[1] + device_id = message[0] + if data.event is None: + return + event_param = data.event.split(".") + event_type = event_param[len(event_param) - 1] + if event_type not in ( + "Report", + "Alert", + "StatusChange", + "getState", + ): + return + resolved_state = data.data + if resolved_state is None: + return + entry_data = hass.data[DOMAIN].get(entry.entry_id) + if entry_data is None: + return + device_coordinators = entry_data.get(ATTR_COORDINATORS) + if device_coordinators is None: + return + device_coordinator = device_coordinators.get(device_id) + if device_coordinator is None: + return + device_coordinator.async_set_updated_data(resolved_state) + try: - await coordinator.async_config_entry_first_refresh() - except ConfigEntryNotReady as ex: - _LOGGER.error("Fetching initial data failed: %s", ex) + async with async_timeout.timeout(10): + device_response = await yolink_http_client.get_auth_devices() + home_info = await yolink_http_client.get_general_info() + await yolink_mqtt_client.init_home_connection( + home_info.data["id"], on_message_callback + ) + except YoLinkAuthFailError as yl_auth_err: + raise ConfigEntryAuthFailed from yl_auth_err + except (YoLinkClientError, asyncio.TimeoutError) as err: + raise ConfigEntryNotReady from err hass.data[DOMAIN][entry.entry_id] = { ATTR_CLIENT: yolink_http_client, ATTR_MQTT_CLIENT: yolink_mqtt_client, - ATTR_COORDINATOR: coordinator, } + auth_devices = device_response.data[ATTR_DEVICE] + device_coordinators = {} + for device_info in auth_devices: + device = YoLinkDevice(device_info, yolink_http_client) + device_coordinator = YoLinkCoordinator(hass, device) + try: + await device_coordinator.async_config_entry_first_refresh() + except ConfigEntryNotReady: + # Not failure by fetching device state + device_coordinator.data = {} + device_coordinators[device.device_id] = device_coordinator + hass.data[DOMAIN][entry.entry_id][ATTR_COORDINATORS] = device_coordinators hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/yolink/binary_sensor.py b/homeassistant/components/yolink/binary_sensor.py index 42899e08a2c..cacba484fe9 100644 --- a/homeassistant/components/yolink/binary_sensor.py +++ b/homeassistant/components/yolink/binary_sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from typing import Any from yolink.device import YoLinkDevice @@ -16,7 +17,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( - ATTR_COORDINATOR, + ATTR_COORDINATORS, ATTR_DEVICE_DOOR_SENSOR, ATTR_DEVICE_LEAK_SENSOR, ATTR_DEVICE_MOTION_SENSOR, @@ -32,7 +33,7 @@ class YoLinkBinarySensorEntityDescription(BinarySensorEntityDescription): exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True state_key: str = "state" - value: Callable[[str], bool | None] = lambda _: None + value: Callable[[Any], bool | None] = lambda _: None SENSOR_DEVICE_TYPE = [ @@ -47,14 +48,14 @@ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( icon="mdi:door", device_class=BinarySensorDeviceClass.DOOR, name="State", - value=lambda value: value == "open", + value=lambda value: value == "open" if value is not None else None, exists_fn=lambda device: device.device_type in [ATTR_DEVICE_DOOR_SENSOR], ), YoLinkBinarySensorEntityDescription( key="motion_state", device_class=BinarySensorDeviceClass.MOTION, name="Motion", - value=lambda value: value == "alert", + value=lambda value: value == "alert" if value is not None else None, exists_fn=lambda device: device.device_type in [ATTR_DEVICE_MOTION_SENSOR], ), YoLinkBinarySensorEntityDescription( @@ -62,7 +63,7 @@ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( name="Leak", icon="mdi:water", device_class=BinarySensorDeviceClass.MOISTURE, - value=lambda value: value == "alert", + value=lambda value: value == "alert" if value is not None else None, exists_fn=lambda device: device.device_type in [ATTR_DEVICE_LEAK_SENSOR], ), ) @@ -74,18 +75,20 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up YoLink Sensor from a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATOR] - sensor_devices = [ - device - for device in coordinator.yl_devices - if device.device_type in SENSOR_DEVICE_TYPE + device_coordinators = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATORS] + binary_sensor_device_coordinators = [ + device_coordinator + for device_coordinator in device_coordinators.values() + if device_coordinator.device.device_type in SENSOR_DEVICE_TYPE ] entities = [] - for sensor_device in sensor_devices: + for binary_sensor_device_coordinator in binary_sensor_device_coordinators: for description in SENSOR_TYPES: - if description.exists_fn(sensor_device): + if description.exists_fn(binary_sensor_device_coordinator.device): entities.append( - YoLinkBinarySensorEntity(coordinator, description, sensor_device) + YoLinkBinarySensorEntity( + binary_sensor_device_coordinator, description + ) ) async_add_entities(entities) @@ -99,18 +102,21 @@ class YoLinkBinarySensorEntity(YoLinkEntity, BinarySensorEntity): self, coordinator: YoLinkCoordinator, description: YoLinkBinarySensorEntityDescription, - device: YoLinkDevice, ) -> None: """Init YoLink Sensor.""" - super().__init__(coordinator, device) + super().__init__(coordinator) self.entity_description = description - self._attr_unique_id = f"{device.device_id} {self.entity_description.key}" - self._attr_name = f"{device.device_name} ({self.entity_description.name})" + self._attr_unique_id = ( + f"{coordinator.device.device_id} {self.entity_description.key}" + ) + self._attr_name = ( + f"{coordinator.device.device_name} ({self.entity_description.name})" + ) @callback - def update_entity_state(self, state: dict) -> None: + def update_entity_state(self, state: dict[str, Any]) -> None: """Update HA Entity State.""" self._attr_is_on = self.entity_description.value( - state[self.entity_description.state_key] + state.get(self.entity_description.state_key) ) self.async_write_ha_state() diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index 00d6d6d028e..97252c5c989 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -5,7 +5,7 @@ MANUFACTURER = "YoLink" HOME_ID = "homeId" HOME_SUBSCRIPTION = "home_subscription" ATTR_PLATFORM_SENSOR = "sensor" -ATTR_COORDINATOR = "coordinator" +ATTR_COORDINATORS = "coordinators" ATTR_DEVICE = "devices" ATTR_DEVICE_TYPE = "type" ATTR_DEVICE_NAME = "name" diff --git a/homeassistant/components/yolink/coordinator.py b/homeassistant/components/yolink/coordinator.py index e5578eae4b2..68a1aef42f7 100644 --- a/homeassistant/components/yolink/coordinator.py +++ b/homeassistant/components/yolink/coordinator.py @@ -1,22 +1,18 @@ """YoLink DataUpdateCoordinator.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging import async_timeout -from yolink.client import YoLinkClient from yolink.device import YoLinkDevice from yolink.exception import YoLinkAuthFailError, YoLinkClientError -from yolink.model import BRDP -from yolink.mqtt_client import MqttClient from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ATTR_DEVICE, ATTR_DEVICE_STATE, DOMAIN +from .const import ATTR_DEVICE_STATE, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -24,9 +20,7 @@ _LOGGER = logging.getLogger(__name__) class YoLinkCoordinator(DataUpdateCoordinator[dict]): """YoLink DataUpdateCoordinator.""" - def __init__( - self, hass: HomeAssistant, yl_client: YoLinkClient, yl_mqtt_client: MqttClient - ) -> None: + def __init__(self, hass: HomeAssistant, device: YoLinkDevice) -> None: """Init YoLink DataUpdateCoordinator. fetch state every 30 minutes base on yolink device heartbeat interval @@ -35,75 +29,17 @@ class YoLinkCoordinator(DataUpdateCoordinator[dict]): super().__init__( hass, _LOGGER, name=DOMAIN, update_interval=timedelta(minutes=30) ) - self._client = yl_client - self._mqtt_client = yl_mqtt_client - self.yl_devices: list[YoLinkDevice] = [] - self.data = {} + self.device = device - def on_message_callback(self, message: tuple[str, BRDP]): - """On message callback.""" - data = message[1] - if data.event is None: - return - event_param = data.event.split(".") - event_type = event_param[len(event_param) - 1] - if event_type not in ( - "Report", - "Alert", - "StatusChange", - "getState", - ): - return - resolved_state = data.data - if resolved_state is None: - return - self.data[message[0]] = resolved_state - self.async_set_updated_data(self.data) - - async def init_coordinator(self): - """Init coordinator.""" + async def _async_update_data(self) -> dict: + """Fetch device state.""" try: async with async_timeout.timeout(10): - home_info = await self._client.get_general_info() - await self._mqtt_client.init_home_connection( - home_info.data["id"], self.on_message_callback - ) - async with async_timeout.timeout(10): - device_response = await self._client.get_auth_devices() - - except YoLinkAuthFailError as yl_auth_err: - raise ConfigEntryAuthFailed from yl_auth_err - - except (YoLinkClientError, asyncio.TimeoutError) as err: - raise ConfigEntryNotReady from err - - yl_devices: list[YoLinkDevice] = [] - - for device_info in device_response.data[ATTR_DEVICE]: - yl_devices.append(YoLinkDevice(device_info, self._client)) - - self.yl_devices = yl_devices - - async def fetch_device_state(self, device: YoLinkDevice): - """Fetch Device State.""" - try: - async with async_timeout.timeout(10): - device_state_resp = await device.fetch_state_with_api() - if ATTR_DEVICE_STATE in device_state_resp.data: - self.data[device.device_id] = device_state_resp.data[ - ATTR_DEVICE_STATE - ] + device_state_resp = await self.device.fetch_state_with_api() except YoLinkAuthFailError as yl_auth_err: raise ConfigEntryAuthFailed from yl_auth_err except YoLinkClientError as yl_client_err: - raise UpdateFailed( - f"Error communicating with API: {yl_client_err}" - ) from yl_client_err - - async def _async_update_data(self) -> dict: - fetch_tasks = [] - for yl_device in self.yl_devices: - fetch_tasks.append(self.fetch_device_state(yl_device)) - if fetch_tasks: - await asyncio.gather(*fetch_tasks) - return self.data + raise UpdateFailed from yl_client_err + if ATTR_DEVICE_STATE in device_state_resp.data: + return device_state_resp.data[ATTR_DEVICE_STATE] + return {} diff --git a/homeassistant/components/yolink/entity.py b/homeassistant/components/yolink/entity.py index 6954b117728..5365681739e 100644 --- a/homeassistant/components/yolink/entity.py +++ b/homeassistant/components/yolink/entity.py @@ -3,8 +3,6 @@ from __future__ import annotations from abc import abstractmethod -from yolink.device import YoLinkDevice - from homeassistant.core import callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -19,20 +17,24 @@ class YoLinkEntity(CoordinatorEntity[YoLinkCoordinator]): def __init__( self, coordinator: YoLinkCoordinator, - device_info: YoLinkDevice, ) -> None: """Init YoLink Entity.""" super().__init__(coordinator) - self.device = device_info @property def device_id(self) -> str: """Return the device id of the YoLink device.""" - return self.device.device_id + return self.coordinator.device.device_id + + async def async_added_to_hass(self) -> None: + """Update state.""" + await super().async_added_to_hass() + return self._handle_coordinator_update() @callback def _handle_coordinator_update(self) -> None: - data = self.coordinator.data.get(self.device.device_id) + """Update state.""" + data = self.coordinator.data if data is not None: self.update_entity_state(data) @@ -40,10 +42,10 @@ class YoLinkEntity(CoordinatorEntity[YoLinkCoordinator]): def device_info(self) -> DeviceInfo: """Return the device info for HA.""" return DeviceInfo( - identifiers={(DOMAIN, self.device.device_id)}, + identifiers={(DOMAIN, self.coordinator.device.device_id)}, manufacturer=MANUFACTURER, - model=self.device.device_type, - name=self.device.device_name, + model=self.coordinator.device.device_type, + name=self.coordinator.device.device_name, ) @callback diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index e33772c24be..463d8b14da4 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -19,7 +19,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import percentage from .const import ( - ATTR_COORDINATOR, + ATTR_COORDINATORS, ATTR_DEVICE_DOOR_SENSOR, ATTR_DEVICE_MOTION_SENSOR, ATTR_DEVICE_TH_SENSOR, @@ -54,7 +54,9 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda value: percentage.ordered_list_item_to_percentage( [1, 2, 3, 4], value - ), + ) + if value is not None + else None, exists_fn=lambda device: device.device_type in [ATTR_DEVICE_DOOR_SENSOR, ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_MOTION_SENSOR], ), @@ -89,18 +91,21 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up YoLink Sensor from a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATOR] - sensor_devices = [ - device - for device in coordinator.yl_devices - if device.device_type in SENSOR_DEVICE_TYPE + device_coordinators = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATORS] + sensor_device_coordinators = [ + device_coordinator + for device_coordinator in device_coordinators.values() + if device_coordinator.device.device_type in SENSOR_DEVICE_TYPE ] entities = [] - for sensor_device in sensor_devices: + for sensor_device_coordinator in sensor_device_coordinators: for description in SENSOR_TYPES: - if description.exists_fn(sensor_device): + if description.exists_fn(sensor_device_coordinator.device): entities.append( - YoLinkSensorEntity(coordinator, description, sensor_device) + YoLinkSensorEntity( + sensor_device_coordinator, + description, + ) ) async_add_entities(entities) @@ -114,18 +119,21 @@ class YoLinkSensorEntity(YoLinkEntity, SensorEntity): self, coordinator: YoLinkCoordinator, description: YoLinkSensorEntityDescription, - device: YoLinkDevice, ) -> None: """Init YoLink Sensor.""" - super().__init__(coordinator, device) + super().__init__(coordinator) self.entity_description = description - self._attr_unique_id = f"{device.device_id} {self.entity_description.key}" - self._attr_name = f"{device.device_name} ({self.entity_description.name})" + self._attr_unique_id = ( + f"{coordinator.device.device_id} {self.entity_description.key}" + ) + self._attr_name = ( + f"{coordinator.device.device_name} ({self.entity_description.name})" + ) @callback def update_entity_state(self, state: dict) -> None: """Update HA Entity State.""" self._attr_native_value = self.entity_description.value( - state[self.entity_description.key] + state.get(self.entity_description.key) ) self.async_write_ha_state() diff --git a/homeassistant/components/yolink/siren.py b/homeassistant/components/yolink/siren.py index 7a621db6eca..7e67dfb12f1 100644 --- a/homeassistant/components/yolink/siren.py +++ b/homeassistant/components/yolink/siren.py @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_COORDINATOR, ATTR_DEVICE_SIREN, DOMAIN +from .const import ATTR_COORDINATORS, ATTR_DEVICE_SIREN, DOMAIN from .coordinator import YoLinkCoordinator from .entity import YoLinkEntity @@ -28,14 +28,14 @@ class YoLinkSirenEntityDescription(SirenEntityDescription): """YoLink SirenEntityDescription.""" exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True - value: Callable[[str], bool | None] = lambda _: None + value: Callable[[Any], bool | None] = lambda _: None DEVICE_TYPES: tuple[YoLinkSirenEntityDescription, ...] = ( YoLinkSirenEntityDescription( key="state", name="State", - value=lambda value: value == "alert", + value=lambda value: value == "alert" if value is not None else None, exists_fn=lambda device: device.device_type in [ATTR_DEVICE_SIREN], ), ) @@ -49,16 +49,20 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up YoLink siren from a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATOR] - devices = [ - device for device in coordinator.yl_devices if device.device_type in DEVICE_TYPE + device_coordinators = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATORS] + siren_device_coordinators = [ + device_coordinator + for device_coordinator in device_coordinators.values() + if device_coordinator.device.device_type in DEVICE_TYPE ] entities = [] - for device in devices: + for siren_device_coordinator in siren_device_coordinators: for description in DEVICE_TYPES: - if description.exists_fn(device): + if description.exists_fn(siren_device_coordinator.device): entities.append( - YoLinkSirenEntity(config_entry, coordinator, description, device) + YoLinkSirenEntity( + config_entry, siren_device_coordinator, description + ) ) async_add_entities(entities) @@ -73,23 +77,26 @@ class YoLinkSirenEntity(YoLinkEntity, SirenEntity): config_entry: ConfigEntry, coordinator: YoLinkCoordinator, description: YoLinkSirenEntityDescription, - device: YoLinkDevice, ) -> None: """Init YoLink Siren.""" - super().__init__(coordinator, device) + super().__init__(coordinator) self.config_entry = config_entry self.entity_description = description - self._attr_unique_id = f"{device.device_id} {self.entity_description.key}" - self._attr_name = f"{device.device_name} ({self.entity_description.name})" + self._attr_unique_id = ( + f"{coordinator.device.device_id} {self.entity_description.key}" + ) + self._attr_name = ( + f"{coordinator.device.device_name} ({self.entity_description.name})" + ) self._attr_supported_features = ( SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF ) @callback - def update_entity_state(self, state: dict) -> None: + def update_entity_state(self, state: dict[str, Any]) -> None: """Update HA Entity State.""" self._attr_is_on = self.entity_description.value( - state[self.entity_description.key] + state.get(self.entity_description.key) ) self.async_write_ha_state() @@ -97,7 +104,7 @@ class YoLinkSirenEntity(YoLinkEntity, SirenEntity): """Call setState api to change siren state.""" try: # call_device_http_api will check result, fail by raise YoLinkClientError - await self.device.call_device_http_api( + await self.coordinator.device.call_device_http_api( "setState", {"state": {"alarm": state}} ) except YoLinkAuthFailError as yl_auth_err: diff --git a/homeassistant/components/yolink/switch.py b/homeassistant/components/yolink/switch.py index b3756efb74c..f16dc781a9c 100644 --- a/homeassistant/components/yolink/switch.py +++ b/homeassistant/components/yolink/switch.py @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_COORDINATOR, ATTR_DEVICE_OUTLET, DOMAIN +from .const import ATTR_COORDINATORS, ATTR_DEVICE_OUTLET, DOMAIN from .coordinator import YoLinkCoordinator from .entity import YoLinkEntity @@ -28,7 +28,7 @@ class YoLinkSwitchEntityDescription(SwitchEntityDescription): """YoLink SwitchEntityDescription.""" exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True - value: Callable[[str], bool | None] = lambda _: None + value: Callable[[Any], bool | None] = lambda _: None DEVICE_TYPES: tuple[YoLinkSwitchEntityDescription, ...] = ( @@ -36,7 +36,7 @@ DEVICE_TYPES: tuple[YoLinkSwitchEntityDescription, ...] = ( key="state", device_class=SwitchDeviceClass.OUTLET, name="State", - value=lambda value: value == "open", + value=lambda value: value == "open" if value is not None else None, exists_fn=lambda device: device.device_type in [ATTR_DEVICE_OUTLET], ), ) @@ -50,16 +50,20 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up YoLink Sensor from a config entry.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATOR] - devices = [ - device for device in coordinator.yl_devices if device.device_type in DEVICE_TYPE + device_coordinators = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATORS] + switch_device_coordinators = [ + device_coordinator + for device_coordinator in device_coordinators.values() + if device_coordinator.device.device_type in DEVICE_TYPE ] entities = [] - for device in devices: + for switch_device_coordinator in switch_device_coordinators: for description in DEVICE_TYPES: - if description.exists_fn(device): + if description.exists_fn(switch_device_coordinator.device): entities.append( - YoLinkSwitchEntity(config_entry, coordinator, description, device) + YoLinkSwitchEntity( + config_entry, switch_device_coordinator, description + ) ) async_add_entities(entities) @@ -74,20 +78,23 @@ class YoLinkSwitchEntity(YoLinkEntity, SwitchEntity): config_entry: ConfigEntry, coordinator: YoLinkCoordinator, description: YoLinkSwitchEntityDescription, - device: YoLinkDevice, ) -> None: """Init YoLink Outlet.""" - super().__init__(coordinator, device) + super().__init__(coordinator) self.config_entry = config_entry self.entity_description = description - self._attr_unique_id = f"{device.device_id} {self.entity_description.key}" - self._attr_name = f"{device.device_name} ({self.entity_description.name})" + self._attr_unique_id = ( + f"{coordinator.device.device_id} {self.entity_description.key}" + ) + self._attr_name = ( + f"{coordinator.device.device_name} ({self.entity_description.name})" + ) @callback - def update_entity_state(self, state: dict) -> None: + def update_entity_state(self, state: dict[str, Any]) -> None: """Update HA Entity State.""" self._attr_is_on = self.entity_description.value( - state[self.entity_description.key] + state.get(self.entity_description.key) ) self.async_write_ha_state() @@ -95,7 +102,9 @@ class YoLinkSwitchEntity(YoLinkEntity, SwitchEntity): """Call setState api to change outlet state.""" try: # call_device_http_api will check result, fail by raise YoLinkClientError - await self.device.call_device_http_api("setState", {"state": state}) + await self.coordinator.device.call_device_http_api( + "setState", {"state": state} + ) except YoLinkAuthFailError as yl_auth_err: self.config_entry.async_start_reauth(self.hass) raise HomeAssistantError(yl_auth_err) from yl_auth_err From f41b2fa2cf9bda58690507ef08909f32055c934e Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Mon, 30 May 2022 23:37:28 +0200 Subject: [PATCH 1053/3516] Fix homewizard diagnostics and add tests (#72611) --- .coveragerc | 1 - .../components/homewizard/diagnostics.py | 9 ++-- tests/components/homewizard/conftest.py | 49 ++++++++++++++++++- .../components/homewizard/fixtures/data.json | 16 ++++++ .../homewizard/fixtures/device.json | 7 +++ .../components/homewizard/fixtures/state.json | 5 ++ .../components/homewizard/test_diagnostics.py | 47 ++++++++++++++++++ 7 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 tests/components/homewizard/fixtures/data.json create mode 100644 tests/components/homewizard/fixtures/device.json create mode 100644 tests/components/homewizard/fixtures/state.json create mode 100644 tests/components/homewizard/test_diagnostics.py diff --git a/.coveragerc b/.coveragerc index aced69a4714..62e6d3cb94e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -491,7 +491,6 @@ omit = homeassistant/components/homematic/* homeassistant/components/home_plus_control/api.py homeassistant/components/home_plus_control/switch.py - homeassistant/components/homewizard/diagnostics.py homeassistant/components/homeworks/* homeassistant/components/honeywell/__init__.py homeassistant/components/honeywell/climate.py diff --git a/homeassistant/components/homewizard/diagnostics.py b/homeassistant/components/homewizard/diagnostics.py index 3dd55933291..a97d2507098 100644 --- a/homeassistant/components/homewizard/diagnostics.py +++ b/homeassistant/components/homewizard/diagnostics.py @@ -1,6 +1,7 @@ """Diagnostics support for P1 Monitor.""" from __future__ import annotations +from dataclasses import asdict from typing import Any from homeassistant.components.diagnostics import async_redact_data @@ -21,10 +22,10 @@ async def async_get_config_entry_diagnostics( coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] meter_data = { - "device": coordinator.api.device.todict(), - "data": coordinator.api.data.todict(), - "state": coordinator.api.state.todict() - if coordinator.api.state is not None + "device": asdict(coordinator.data["device"]), + "data": asdict(coordinator.data["data"]), + "state": asdict(coordinator.data["state"]) + if coordinator.data["state"] is not None else None, } diff --git a/tests/components/homewizard/conftest.py b/tests/components/homewizard/conftest.py index 15993aa35ed..1617db35458 100644 --- a/tests/components/homewizard/conftest.py +++ b/tests/components/homewizard/conftest.py @@ -1,10 +1,15 @@ """Fixtures for HomeWizard integration tests.""" +import json +from unittest.mock import AsyncMock, patch + +from homewizard_energy.models import Data, Device, State import pytest from homeassistant.components.homewizard.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture @pytest.fixture @@ -25,6 +30,46 @@ def mock_config_entry() -> MockConfigEntry: return MockConfigEntry( title="Product Name (aabbccddeeff)", domain=DOMAIN, - data={}, + data={CONF_IP_ADDRESS: "1.2.3.4"}, unique_id="aabbccddeeff", ) + + +@pytest.fixture +def mock_homewizardenergy(): + """Return a mocked P1 meter.""" + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + ) as device: + client = device.return_value + client.device = AsyncMock( + return_value=Device.from_dict( + json.loads(load_fixture("homewizard/device.json")) + ) + ) + client.data = AsyncMock( + return_value=Data.from_dict( + json.loads(load_fixture("homewizard/data.json")) + ) + ) + client.state = AsyncMock( + return_value=State.from_dict( + json.loads(load_fixture("homewizard/state.json")) + ) + ) + yield device + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homewizardenergy: AsyncMock, +) -> MockConfigEntry: + """Set up the HomeWizard integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/homewizard/fixtures/data.json b/tests/components/homewizard/fixtures/data.json new file mode 100644 index 00000000000..b6eada38038 --- /dev/null +++ b/tests/components/homewizard/fixtures/data.json @@ -0,0 +1,16 @@ +{ + "smr_version": 50, + "meter_model": "ISKRA 2M550T-101", + "wifi_ssid": "My Wi-Fi", + "wifi_strength": 100, + "total_power_import_t1_kwh": 1234.111, + "total_power_import_t2_kwh": 5678.222, + "total_power_export_t1_kwh": 4321.333, + "total_power_export_t2_kwh": 8765.444, + "active_power_w": -123, + "active_power_l1_w": -123, + "active_power_l2_w": 456, + "active_power_l3_w": 123.456, + "total_gas_m3": 1122.333, + "gas_timestamp": 210314112233 +} diff --git a/tests/components/homewizard/fixtures/device.json b/tests/components/homewizard/fixtures/device.json new file mode 100644 index 00000000000..493daa12b94 --- /dev/null +++ b/tests/components/homewizard/fixtures/device.json @@ -0,0 +1,7 @@ +{ + "product_type": "HWE-P1", + "product_name": "P1 Meter", + "serial": "3c39e7aabbcc", + "firmware_version": "2.11", + "api_version": "v1" +} diff --git a/tests/components/homewizard/fixtures/state.json b/tests/components/homewizard/fixtures/state.json new file mode 100644 index 00000000000..bbc0242ed58 --- /dev/null +++ b/tests/components/homewizard/fixtures/state.json @@ -0,0 +1,5 @@ +{ + "power_on": true, + "switch_lock": false, + "brightness": 255 +} diff --git a/tests/components/homewizard/test_diagnostics.py b/tests/components/homewizard/test_diagnostics.py new file mode 100644 index 00000000000..e477c94d914 --- /dev/null +++ b/tests/components/homewizard/test_diagnostics.py @@ -0,0 +1,47 @@ +"""Tests for diagnostics data.""" +from aiohttp import ClientSession + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, + init_integration: MockConfigEntry, +): + """Test diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, init_integration + ) == { + "entry": {"ip_address": REDACTED}, + "data": { + "device": { + "product_name": "P1 Meter", + "product_type": "HWE-P1", + "serial": REDACTED, + "api_version": "v1", + "firmware_version": "2.11", + }, + "data": { + "smr_version": 50, + "meter_model": "ISKRA 2M550T-101", + "wifi_ssid": REDACTED, + "wifi_strength": 100, + "total_power_import_t1_kwh": 1234.111, + "total_power_import_t2_kwh": 5678.222, + "total_power_export_t1_kwh": 4321.333, + "total_power_export_t2_kwh": 8765.444, + "active_power_w": -123, + "active_power_l1_w": -123, + "active_power_l2_w": 456, + "active_power_l3_w": 123.456, + "total_gas_m3": 1122.333, + "gas_timestamp": "2021-03-14T11:22:33", + }, + "state": {"power_on": True, "switch_lock": False, "brightness": 255}, + }, + } From 4b524c077602db576e15c27ed813dbb0bc6c6774 Mon Sep 17 00:00:00 2001 From: BigMoby Date: Mon, 30 May 2022 08:26:05 +0200 Subject: [PATCH 1054/3516] iAlarm XR integration refinements (#72616) * fixing after MartinHjelmare review * fixing after MartinHjelmare review conversion alarm state to hass state * fixing after MartinHjelmare review conversion alarm state to hass state * manage the status in the alarm control * simplyfing return function --- .../components/ialarm_xr/__init__.py | 6 ++--- .../ialarm_xr/alarm_control_panel.py | 22 ++++++++++++++++--- .../components/ialarm_xr/config_flow.py | 4 ++-- homeassistant/components/ialarm_xr/const.py | 15 ------------- .../components/ialarm_xr/manifest.json | 4 ++-- .../components/ialarm_xr/strings.json | 1 + .../components/ialarm_xr/translations/en.json | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/ialarm_xr/test_config_flow.py | 22 ++----------------- tests/components/ialarm_xr/test_init.py | 10 --------- 11 files changed, 32 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/ialarm_xr/__init__.py b/homeassistant/components/ialarm_xr/__init__.py index 9a41b5ebab7..193bbe4fffc 100644 --- a/homeassistant/components/ialarm_xr/__init__.py +++ b/homeassistant/components/ialarm_xr/__init__.py @@ -24,7 +24,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN, IALARMXR_TO_HASS +from .const import DOMAIN from .utils import async_get_ialarmxr_mac PLATFORMS = [Platform.ALARM_CONTROL_PANEL] @@ -74,7 +74,7 @@ class IAlarmXRDataUpdateCoordinator(DataUpdateCoordinator): def __init__(self, hass: HomeAssistant, ialarmxr: IAlarmXR, mac: str) -> None: """Initialize global iAlarm data updater.""" self.ialarmxr: IAlarmXR = ialarmxr - self.state: str | None = None + self.state: int | None = None self.host: str = ialarmxr.host self.mac: str = mac @@ -90,7 +90,7 @@ class IAlarmXRDataUpdateCoordinator(DataUpdateCoordinator): status: int = self.ialarmxr.get_status() _LOGGER.debug("iAlarmXR status: %s", status) - self.state = IALARMXR_TO_HASS.get(status) + self.state = status async def _async_update_data(self) -> None: """Fetch data from iAlarmXR.""" diff --git a/homeassistant/components/ialarm_xr/alarm_control_panel.py b/homeassistant/components/ialarm_xr/alarm_control_panel.py index 7b47ce3d7fa..b64edb74391 100644 --- a/homeassistant/components/ialarm_xr/alarm_control_panel.py +++ b/homeassistant/components/ialarm_xr/alarm_control_panel.py @@ -1,11 +1,19 @@ """Interfaces with iAlarmXR control panels.""" from __future__ import annotations +from pyialarmxr import IAlarmXR + from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntity, AlarmControlPanelEntityFeature, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry from homeassistant.helpers.entity import DeviceInfo @@ -15,6 +23,13 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import IAlarmXRDataUpdateCoordinator from .const import DOMAIN +IALARMXR_TO_HASS = { + IAlarmXR.ARMED_AWAY: STATE_ALARM_ARMED_AWAY, + IAlarmXR.ARMED_STAY: STATE_ALARM_ARMED_HOME, + IAlarmXR.DISARMED: STATE_ALARM_DISARMED, + IAlarmXR.TRIGGERED: STATE_ALARM_TRIGGERED, +} + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -24,7 +39,9 @@ async def async_setup_entry( async_add_entities([IAlarmXRPanel(coordinator)]) -class IAlarmXRPanel(CoordinatorEntity, AlarmControlPanelEntity): +class IAlarmXRPanel( + CoordinatorEntity[IAlarmXRDataUpdateCoordinator], AlarmControlPanelEntity +): """Representation of an iAlarmXR device.""" _attr_supported_features = ( @@ -37,7 +54,6 @@ class IAlarmXRPanel(CoordinatorEntity, AlarmControlPanelEntity): def __init__(self, coordinator: IAlarmXRDataUpdateCoordinator) -> None: """Initialize the alarm panel.""" super().__init__(coordinator) - self.coordinator: IAlarmXRDataUpdateCoordinator = coordinator self._attr_unique_id = coordinator.mac self._attr_device_info = DeviceInfo( manufacturer="Antifurto365 - Meian", @@ -48,7 +64,7 @@ class IAlarmXRPanel(CoordinatorEntity, AlarmControlPanelEntity): @property def state(self) -> str | None: """Return the state of the device.""" - return self.coordinator.state + return IALARMXR_TO_HASS.get(self.coordinator.state) def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" diff --git a/homeassistant/components/ialarm_xr/config_flow.py b/homeassistant/components/ialarm_xr/config_flow.py index 06509a82eb5..2a9cc406733 100644 --- a/homeassistant/components/ialarm_xr/config_flow.py +++ b/homeassistant/components/ialarm_xr/config_flow.py @@ -72,13 +72,13 @@ class IAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): "IAlarmXRGenericException with message: [ %s ]", ialarmxr_exception.message, ) - errors["base"] = "unknown" + errors["base"] = "cannot_connect" except IAlarmXRSocketTimeoutException as ialarmxr_socket_timeout_exception: _LOGGER.debug( "IAlarmXRSocketTimeoutException with message: [ %s ]", ialarmxr_socket_timeout_exception.message, ) - errors["base"] = "unknown" + errors["base"] = "timeout" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" diff --git a/homeassistant/components/ialarm_xr/const.py b/homeassistant/components/ialarm_xr/const.py index a208f5290b6..12122277340 100644 --- a/homeassistant/components/ialarm_xr/const.py +++ b/homeassistant/components/ialarm_xr/const.py @@ -1,18 +1,3 @@ """Constants for the iAlarmXR integration.""" -from pyialarmxr import IAlarmXR - -from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED, -) DOMAIN = "ialarm_xr" - -IALARMXR_TO_HASS = { - IAlarmXR.ARMED_AWAY: STATE_ALARM_ARMED_AWAY, - IAlarmXR.ARMED_STAY: STATE_ALARM_ARMED_HOME, - IAlarmXR.DISARMED: STATE_ALARM_DISARMED, - IAlarmXR.TRIGGERED: STATE_ALARM_TRIGGERED, -} diff --git a/homeassistant/components/ialarm_xr/manifest.json b/homeassistant/components/ialarm_xr/manifest.json index 4861e9c901f..f863f360242 100644 --- a/homeassistant/components/ialarm_xr/manifest.json +++ b/homeassistant/components/ialarm_xr/manifest.json @@ -1,8 +1,8 @@ { "domain": "ialarm_xr", "name": "Antifurto365 iAlarmXR", - "documentation": "https://www.home-assistant.io/integrations/ialarmxr", - "requirements": ["pyialarmxr==1.0.13"], + "documentation": "https://www.home-assistant.io/integrations/ialarm_xr", + "requirements": ["pyialarmxr==1.0.18"], "codeowners": ["@bigmoby"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/ialarm_xr/strings.json b/homeassistant/components/ialarm_xr/strings.json index 1650ae28c84..ea4f91fdbb9 100644 --- a/homeassistant/components/ialarm_xr/strings.json +++ b/homeassistant/components/ialarm_xr/strings.json @@ -12,6 +12,7 @@ }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout": "[%key:common::config_flow::error::timeout_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { diff --git a/homeassistant/components/ialarm_xr/translations/en.json b/homeassistant/components/ialarm_xr/translations/en.json index bf2bf989dcd..be59a5a1dc4 100644 --- a/homeassistant/components/ialarm_xr/translations/en.json +++ b/homeassistant/components/ialarm_xr/translations/en.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Failed to connect", + "timeout": "Timeout establishing connection", "unknown": "Unexpected error" }, "step": { diff --git a/requirements_all.txt b/requirements_all.txt index f0cc4f6fb67..63f0f929c36 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1550,7 +1550,7 @@ pyhomeworks==0.0.6 pyialarm==1.9.0 # homeassistant.components.ialarm_xr -pyialarmxr==1.0.13 +pyialarmxr==1.0.18 # homeassistant.components.icloud pyicloud==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 783542a1c69..cb5f6a242e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1038,7 +1038,7 @@ pyhomematic==0.1.77 pyialarm==1.9.0 # homeassistant.components.ialarm_xr -pyialarmxr==1.0.13 +pyialarmxr==1.0.18 # homeassistant.components.icloud pyicloud==1.0.0 diff --git a/tests/components/ialarm_xr/test_config_flow.py b/tests/components/ialarm_xr/test_config_flow.py index 22a70bda067..804249dd5cb 100644 --- a/tests/components/ialarm_xr/test_config_flow.py +++ b/tests/components/ialarm_xr/test_config_flow.py @@ -56,24 +56,6 @@ async def test_form(hass): 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} - ) - - with patch( - "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", - side_effect=ConnectionError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_DATA - ) - - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["errors"] == {"base": "cannot_connect"} - - async def test_form_exception(hass): """Test we handle unknown exception.""" result = await hass.config_entries.flow.async_init( @@ -125,7 +107,7 @@ async def test_form_cannot_connect_throwing_socket_timeout_exception(hass): ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["errors"] == {"base": "unknown"} + assert result2["errors"] == {"base": "timeout"} async def test_form_cannot_connect_throwing_generic_exception(hass): @@ -143,7 +125,7 @@ async def test_form_cannot_connect_throwing_generic_exception(hass): ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["errors"] == {"base": "unknown"} + assert result2["errors"] == {"base": "cannot_connect"} async def test_form_already_exists(hass): diff --git a/tests/components/ialarm_xr/test_init.py b/tests/components/ialarm_xr/test_init.py index 8486b7049e6..0898b6bebf8 100644 --- a/tests/components/ialarm_xr/test_init.py +++ b/tests/components/ialarm_xr/test_init.py @@ -48,16 +48,6 @@ async def test_setup_entry(hass, ialarmxr_api, mock_config_entry): assert mock_config_entry.state is ConfigEntryState.LOADED -async def test_setup_not_ready(hass, ialarmxr_api, mock_config_entry): - """Test setup failed because we can't connect to the alarm system.""" - ialarmxr_api.return_value.get_mac = Mock(side_effect=ConnectionError) - - mock_config_entry.add_to_hass(hass) - assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY - - async def test_unload_entry(hass, ialarmxr_api, mock_config_entry): """Test being able to unload an entry.""" ialarmxr_api.return_value.get_mac = Mock(return_value="00:00:54:12:34:56") From c62692dff14103e546fbc182270cff051464dae9 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 30 May 2022 15:36:58 -0600 Subject: [PATCH 1055/3516] Guard against missing data in 1st generation RainMachine controllers (#72632) --- .../components/rainmachine/binary_sensor.py | 16 +++---- .../components/rainmachine/sensor.py | 2 +- .../components/rainmachine/switch.py | 43 +++++++++++-------- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index fb404adb199..730b51c142a 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -158,17 +158,17 @@ class CurrentRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity): def update_from_latest_data(self) -> None: """Update the state.""" if self.entity_description.key == TYPE_FREEZE: - self._attr_is_on = self.coordinator.data["freeze"] + self._attr_is_on = self.coordinator.data.get("freeze") elif self.entity_description.key == TYPE_HOURLY: - self._attr_is_on = self.coordinator.data["hourly"] + self._attr_is_on = self.coordinator.data.get("hourly") elif self.entity_description.key == TYPE_MONTH: - self._attr_is_on = self.coordinator.data["month"] + self._attr_is_on = self.coordinator.data.get("month") elif self.entity_description.key == TYPE_RAINDELAY: - self._attr_is_on = self.coordinator.data["rainDelay"] + self._attr_is_on = self.coordinator.data.get("rainDelay") elif self.entity_description.key == TYPE_RAINSENSOR: - self._attr_is_on = self.coordinator.data["rainSensor"] + self._attr_is_on = self.coordinator.data.get("rainSensor") elif self.entity_description.key == TYPE_WEEKDAY: - self._attr_is_on = self.coordinator.data["weekDay"] + self._attr_is_on = self.coordinator.data.get("weekDay") class ProvisionSettingsBinarySensor(RainMachineEntity, BinarySensorEntity): @@ -188,6 +188,6 @@ class UniversalRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity): def update_from_latest_data(self) -> None: """Update the state.""" if self.entity_description.key == TYPE_FREEZE_PROTECTION: - self._attr_is_on = self.coordinator.data["freezeProtectEnabled"] + self._attr_is_on = self.coordinator.data.get("freezeProtectEnabled") elif self.entity_description.key == TYPE_HOT_DAYS: - self._attr_is_on = self.coordinator.data["hotDaysExtraWatering"] + self._attr_is_on = self.coordinator.data.get("hotDaysExtraWatering") diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index b825faca7e1..a2b0f7cd539 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -198,7 +198,7 @@ class UniversalRestrictionsSensor(RainMachineEntity, SensorEntity): def update_from_latest_data(self) -> None: """Update the state.""" if self.entity_description.key == TYPE_FREEZE_TEMP: - self._attr_native_value = self.coordinator.data["freezeProtectTemp"] + self._attr_native_value = self.coordinator.data.get("freezeProtectTemp") class ZoneTimeRemainingSensor(RainMachineEntity, SensorEntity): diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 007aec97a3e..a220aafa2a5 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -389,23 +389,32 @@ class RainMachineZone(RainMachineActivitySwitch): self._attr_is_on = bool(data["state"]) - self._attr_extra_state_attributes.update( - { - ATTR_AREA: round(data["waterSense"]["area"], 2), - ATTR_CURRENT_CYCLE: data["cycle"], - ATTR_FIELD_CAPACITY: round(data["waterSense"]["fieldCapacity"], 2), - ATTR_ID: data["uid"], - ATTR_NO_CYCLES: data["noOfCycles"], - ATTR_PRECIP_RATE: round(data["waterSense"]["precipitationRate"], 2), - ATTR_RESTRICTIONS: data["restriction"], - ATTR_SLOPE: SLOPE_TYPE_MAP.get(data["slope"], 99), - ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(data["soil"], 99), - ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(data["group_id"], 99), - ATTR_STATUS: RUN_STATE_MAP[data["state"]], - ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(data.get("sun")), - ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(data["type"], 99), - } - ) + attrs = { + ATTR_CURRENT_CYCLE: data["cycle"], + ATTR_ID: data["uid"], + ATTR_NO_CYCLES: data["noOfCycles"], + ATTR_RESTRICTIONS: data("restriction"), + ATTR_SLOPE: SLOPE_TYPE_MAP.get(data["slope"], 99), + ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(data["soil"], 99), + ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(data["group_id"], 99), + ATTR_STATUS: RUN_STATE_MAP[data["state"]], + ATTR_SUN_EXPOSURE: SUN_EXPOSURE_MAP.get(data.get("sun")), + ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(data["type"], 99), + } + + if "waterSense" in data: + if "area" in data["waterSense"]: + attrs[ATTR_AREA] = round(data["waterSense"]["area"], 2) + if "fieldCapacity" in data["waterSense"]: + attrs[ATTR_FIELD_CAPACITY] = round( + data["waterSense"]["fieldCapacity"], 2 + ) + if "precipitationRate" in data["waterSense"]: + attrs[ATTR_PRECIP_RATE] = round( + data["waterSense"]["precipitationRate"], 2 + ) + + self._attr_extra_state_attributes.update(attrs) class RainMachineZoneEnabled(RainMachineEnabledSwitch): From f039aac31c106c533c6907828f46fbfb0f3f9633 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 29 May 2022 12:30:00 -0400 Subject: [PATCH 1056/3516] Fix zwave_js custom trigger validation bug (#72656) * Fix zwave_js custom trigger validation bug * update comments * Switch to ValueError * Switch to ValueError --- .../components/zwave_js/triggers/event.py | 36 +-- .../zwave_js/triggers/value_updated.py | 12 +- tests/components/zwave_js/test_trigger.py | 217 ++++++++++++++++++ 3 files changed, 241 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py index 17bb52fb392..784ae74777b 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -8,7 +8,7 @@ import voluptuous as vol from zwave_js_server.client import Client from zwave_js_server.model.controller import CONTROLLER_EVENT_MODEL_MAP from zwave_js_server.model.driver import DRIVER_EVENT_MODEL_MAP -from zwave_js_server.model.node import NODE_EVENT_MODEL_MAP, Node +from zwave_js_server.model.node import NODE_EVENT_MODEL_MAP from homeassistant.components.automation import ( AutomationActionType, @@ -20,7 +20,6 @@ from homeassistant.components.zwave_js.const import ( ATTR_EVENT_DATA, ATTR_EVENT_SOURCE, ATTR_NODE_ID, - ATTR_NODES, ATTR_PARTIAL_DICT_MATCH, DATA_CLIENT, DOMAIN, @@ -116,22 +115,20 @@ async def async_validate_trigger_config( """Validate config.""" config = TRIGGER_SCHEMA(config) + if ATTR_CONFIG_ENTRY_ID in config: + entry_id = config[ATTR_CONFIG_ENTRY_ID] + if hass.config_entries.async_get_entry(entry_id) is None: + raise vol.Invalid(f"Config entry '{entry_id}' not found") + if async_bypass_dynamic_config_validation(hass, config): return config - if config[ATTR_EVENT_SOURCE] == "node": - config[ATTR_NODES] = async_get_nodes_from_targets(hass, config) - if not config[ATTR_NODES]: - raise vol.Invalid( - f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s." - ) - - if ATTR_CONFIG_ENTRY_ID not in config: - return config - - entry_id = config[ATTR_CONFIG_ENTRY_ID] - if hass.config_entries.async_get_entry(entry_id) is None: - raise vol.Invalid(f"Config entry '{entry_id}' not found") + if config[ATTR_EVENT_SOURCE] == "node" and not async_get_nodes_from_targets( + hass, config + ): + raise vol.Invalid( + f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s." + ) return config @@ -145,7 +142,12 @@ async def async_attach_trigger( platform_type: str = PLATFORM_TYPE, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - nodes: set[Node] = config.get(ATTR_NODES, {}) + dev_reg = dr.async_get(hass) + nodes = async_get_nodes_from_targets(hass, config, dev_reg=dev_reg) + if config[ATTR_EVENT_SOURCE] == "node" and not nodes: + raise ValueError( + f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s." + ) event_source = config[ATTR_EVENT_SOURCE] event_name = config[ATTR_EVENT] @@ -200,8 +202,6 @@ async def async_attach_trigger( hass.async_run_hass_job(job, {"trigger": payload}) - dev_reg = dr.async_get(hass) - if not nodes: entry_id = config[ATTR_CONFIG_ENTRY_ID] client: Client = hass.data[DOMAIN][entry_id][DATA_CLIENT] diff --git a/homeassistant/components/zwave_js/triggers/value_updated.py b/homeassistant/components/zwave_js/triggers/value_updated.py index 4f15b87a6db..29b4b4d06d6 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -5,7 +5,6 @@ import functools import voluptuous as vol from zwave_js_server.const import CommandClass -from zwave_js_server.model.node import Node from zwave_js_server.model.value import Value, get_value_id from homeassistant.components.automation import ( @@ -20,7 +19,6 @@ from homeassistant.components.zwave_js.const import ( ATTR_CURRENT_VALUE_RAW, ATTR_ENDPOINT, ATTR_NODE_ID, - ATTR_NODES, ATTR_PREVIOUS_VALUE, ATTR_PREVIOUS_VALUE_RAW, ATTR_PROPERTY, @@ -79,8 +77,7 @@ async def async_validate_trigger_config( if async_bypass_dynamic_config_validation(hass, config): return config - config[ATTR_NODES] = async_get_nodes_from_targets(hass, config) - if not config[ATTR_NODES]: + if not async_get_nodes_from_targets(hass, config): raise vol.Invalid( f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s." ) @@ -96,7 +93,11 @@ async def async_attach_trigger( platform_type: str = PLATFORM_TYPE, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - nodes: set[Node] = config[ATTR_NODES] + dev_reg = dr.async_get(hass) + if not (nodes := async_get_nodes_from_targets(hass, config, dev_reg=dev_reg)): + raise ValueError( + f"No nodes found for given {ATTR_DEVICE_ID}s or {ATTR_ENTITY_ID}s." + ) from_value = config[ATTR_FROM] to_value = config[ATTR_TO] @@ -163,7 +164,6 @@ async def async_attach_trigger( hass.async_run_hass_job(job, {"trigger": payload}) - dev_reg = dr.async_get(hass) for node in nodes: driver = node.client.driver assert driver is not None # The node comes from the driver. diff --git a/tests/components/zwave_js/test_trigger.py b/tests/components/zwave_js/test_trigger.py index 9758f566d81..48439eede0f 100644 --- a/tests/components/zwave_js/test_trigger.py +++ b/tests/components/zwave_js/test_trigger.py @@ -269,6 +269,122 @@ async def test_zwave_js_value_updated(hass, client, lock_schlage_be469, integrat await hass.services.async_call(automation.DOMAIN, SERVICE_RELOAD, blocking=True) +async def test_zwave_js_value_updated_bypass_dynamic_validation( + hass, client, lock_schlage_be469, integration +): + """Test zwave_js.value_updated trigger when bypassing dynamic validation.""" + trigger_type = f"{DOMAIN}.value_updated" + node: Node = lock_schlage_be469 + + no_value_filter = async_capture_events(hass, "no_value_filter") + + with patch( + "homeassistant.components.zwave_js.triggers.value_updated.async_bypass_dynamic_config_validation", + return_value=True, + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + # no value filter + { + "trigger": { + "platform": trigger_type, + "entity_id": SCHLAGE_BE469_LOCK_ENTITY, + "command_class": CommandClass.DOOR_LOCK.value, + "property": "latchStatus", + }, + "action": { + "event": "no_value_filter", + }, + }, + ] + }, + ) + + # Test that no value filter is triggered + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "latchStatus", + "newValue": "boo", + "prevValue": "hiss", + "propertyName": "latchStatus", + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + + assert len(no_value_filter) == 1 + + +async def test_zwave_js_value_updated_bypass_dynamic_validation_no_nodes( + hass, client, lock_schlage_be469, integration +): + """Test value_updated trigger when bypassing dynamic validation with no nodes.""" + trigger_type = f"{DOMAIN}.value_updated" + node: Node = lock_schlage_be469 + + no_value_filter = async_capture_events(hass, "no_value_filter") + + with patch( + "homeassistant.components.zwave_js.triggers.value_updated.async_bypass_dynamic_config_validation", + return_value=True, + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + # no value filter + { + "trigger": { + "platform": trigger_type, + "entity_id": "sensor.test", + "command_class": CommandClass.DOOR_LOCK.value, + "property": "latchStatus", + }, + "action": { + "event": "no_value_filter", + }, + }, + ] + }, + ) + + # Test that no value filter is NOT triggered because automation failed setup + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": node.node_id, + "args": { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "latchStatus", + "newValue": "boo", + "prevValue": "hiss", + "propertyName": "latchStatus", + }, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + + assert len(no_value_filter) == 0 + + async def test_zwave_js_event(hass, client, lock_schlage_be469, integration): """Test for zwave_js.event automation trigger.""" trigger_type = f"{DOMAIN}.event" @@ -644,6 +760,107 @@ async def test_zwave_js_event(hass, client, lock_schlage_be469, integration): await hass.services.async_call(automation.DOMAIN, SERVICE_RELOAD, blocking=True) +async def test_zwave_js_event_bypass_dynamic_validation( + hass, client, lock_schlage_be469, integration +): + """Test zwave_js.event trigger when bypassing dynamic config validation.""" + trigger_type = f"{DOMAIN}.event" + node: Node = lock_schlage_be469 + + node_no_event_data_filter = async_capture_events(hass, "node_no_event_data_filter") + + with patch( + "homeassistant.components.zwave_js.triggers.event.async_bypass_dynamic_config_validation", + return_value=True, + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + # node filter: no event data + { + "trigger": { + "platform": trigger_type, + "entity_id": SCHLAGE_BE469_LOCK_ENTITY, + "event_source": "node", + "event": "interview stage completed", + }, + "action": { + "event": "node_no_event_data_filter", + }, + }, + ] + }, + ) + + # Test that `node no event data filter` is triggered and `node event data filter` is not + event = Event( + type="interview stage completed", + data={ + "source": "node", + "event": "interview stage completed", + "stageName": "NodeInfo", + "nodeId": node.node_id, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + + assert len(node_no_event_data_filter) == 1 + + +async def test_zwave_js_event_bypass_dynamic_validation_no_nodes( + hass, client, lock_schlage_be469, integration +): + """Test event trigger when bypassing dynamic validation with no nodes.""" + trigger_type = f"{DOMAIN}.event" + node: Node = lock_schlage_be469 + + node_no_event_data_filter = async_capture_events(hass, "node_no_event_data_filter") + + with patch( + "homeassistant.components.zwave_js.triggers.event.async_bypass_dynamic_config_validation", + return_value=True, + ): + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + # node filter: no event data + { + "trigger": { + "platform": trigger_type, + "entity_id": "sensor.fake", + "event_source": "node", + "event": "interview stage completed", + }, + "action": { + "event": "node_no_event_data_filter", + }, + }, + ] + }, + ) + + # Test that `node no event data filter` is NOT triggered because automation failed + # setup + event = Event( + type="interview stage completed", + data={ + "source": "node", + "event": "interview stage completed", + "stageName": "NodeInfo", + "nodeId": node.node_id, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + + assert len(node_no_event_data_filter) == 0 + + async def test_zwave_js_event_invalid_config_entry_id( hass, client, integration, caplog ): From f8b7527bf0f526d2ccb42f1454b9b24a5dc44e06 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 May 2022 03:38:52 -0700 Subject: [PATCH 1057/3516] Allow removing a ring device (#72665) --- homeassistant/components/ring/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index a1ed1ac017b..0d8f87eef3c 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -16,6 +16,7 @@ from ring_doorbell import Auth, Ring from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform, __version__ from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from homeassistant.util.async_ import run_callback_threadsafe @@ -146,6 +147,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove a config entry from a device.""" + return True + + class GlobalDataUpdater: """Data storage for single API endpoint.""" From 6f01c13845e53c11498887374460aacbd1469f43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 30 May 2022 03:14:43 +0200 Subject: [PATCH 1058/3516] Switch severity for gesture logging (#72668) --- homeassistant/components/nanoleaf/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nanoleaf/__init__.py b/homeassistant/components/nanoleaf/__init__.py index 9e9cf1d6ca4..f6fb2f8112b 100644 --- a/homeassistant/components/nanoleaf/__init__.py +++ b/homeassistant/components/nanoleaf/__init__.py @@ -85,9 +85,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Receive touch event.""" gesture_type = TOUCH_GESTURE_TRIGGER_MAP.get(event.gesture_id) if gesture_type is None: - _LOGGER.debug("Received unknown touch gesture ID %s", event.gesture_id) + _LOGGER.warning( + "Received unknown touch gesture ID %s", event.gesture_id + ) return - _LOGGER.warning("Received touch gesture %s", gesture_type) + _LOGGER.debug("Received touch gesture %s", gesture_type) hass.bus.async_fire( NANOLEAF_EVENT, {CONF_DEVICE_ID: device_entry.id, CONF_TYPE: gesture_type}, From 952433d16e4149a6cbb6236d3f491b866e7bb271 Mon Sep 17 00:00:00 2001 From: shbatm Date: Sun, 29 May 2022 11:00:18 -0500 Subject: [PATCH 1059/3516] Check ISY994 climate for unknown humidity on Z-Wave Thermostat (#72670) --- homeassistant/components/isy994/climate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 1276207f23c..d68395f14da 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -6,6 +6,7 @@ from typing import Any from pyisy.constants import ( CMD_CLIMATE_FAN_SETTING, CMD_CLIMATE_MODE, + ISY_VALUE_UNKNOWN, PROP_HEAT_COOL_STATE, PROP_HUMIDITY, PROP_SETPOINT_COOL, @@ -116,6 +117,8 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): """Return the current humidity.""" if not (humidity := self._node.aux_properties.get(PROP_HUMIDITY)): return None + if humidity == ISY_VALUE_UNKNOWN: + return None return int(humidity.value) @property From 67ef3229fd11148d4f66712e412ee4102987515d Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 29 May 2022 20:57:47 +0200 Subject: [PATCH 1060/3516] Address late review comments for Tankerkoenig (#72672) * address late review comment from #72654 * use entry_id instead of unique_id * remove not needed `_hass` property * fix skiping failing stations * remove not neccessary error log * set DeviceEntryType.SERVICE * fix use entry_id instead of unique_id * apply suggestions on tests * add return value also to other tests * invert data check to early return user form --- .../components/tankerkoenig/__init__.py | 45 +++++++++++++------ .../components/tankerkoenig/binary_sensor.py | 18 +++----- .../components/tankerkoenig/config_flow.py | 19 ++++---- .../components/tankerkoenig/sensor.py | 18 ++------ .../tankerkoenig/test_config_flow.py | 9 ++-- 5 files changed, 55 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index 08520c8f5cc..e63add83fad 100644 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + ATTR_ID, CONF_API_KEY, CONF_LATITUDE, CONF_LOCATION, @@ -24,8 +25,14 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from .const import ( CONF_FUEL_TYPES, @@ -109,9 +116,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set a tankerkoenig configuration entry up.""" hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][ - entry.unique_id - ] = coordinator = TankerkoenigDataUpdateCoordinator( + hass.data[DOMAIN][entry.entry_id] = coordinator = TankerkoenigDataUpdateCoordinator( hass, entry, _LOGGER, @@ -140,7 +145,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Tankerkoenig config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN].pop(entry.unique_id) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok @@ -172,7 +177,6 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): self._api_key: str = entry.data[CONF_API_KEY] self._selected_stations: list[str] = entry.data[CONF_STATIONS] - self._hass = hass self.stations: dict[str, dict] = {} self.fuel_types: list[str] = entry.data[CONF_FUEL_TYPES] self.show_on_map: bool = entry.options[CONF_SHOW_ON_MAP] @@ -195,7 +199,7 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): station_id, station_data["message"], ) - return False + continue self.add_station(station_data["station"]) if len(self.stations) > 10: _LOGGER.warning( @@ -215,7 +219,7 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): # The API seems to only return at most 10 results, so split the list in chunks of 10 # and merge it together. for index in range(ceil(len(station_ids) / 10)): - data = await self._hass.async_add_executor_job( + data = await self.hass.async_add_executor_job( pytankerkoenig.getPriceList, self._api_key, station_ids[index * 10 : (index + 1) * 10], @@ -223,13 +227,11 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): _LOGGER.debug("Received data: %s", data) if not data["ok"]: - _LOGGER.error( - "Error fetching data from tankerkoenig.de: %s", data["message"] - ) raise UpdateFailed(data["message"]) if "prices" not in data: - _LOGGER.error("Did not receive price information from tankerkoenig.de") - raise UpdateFailed("No prices in data") + raise UpdateFailed( + "Did not receive price information from tankerkoenig.de" + ) prices.update(data["prices"]) return prices @@ -244,3 +246,20 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): self.stations[station_id] = station _LOGGER.debug("add_station called for station: %s", station) + + +class TankerkoenigCoordinatorEntity(CoordinatorEntity): + """Tankerkoenig base entity.""" + + def __init__( + self, coordinator: TankerkoenigDataUpdateCoordinator, station: dict + ) -> None: + """Initialize the Tankerkoenig base entity.""" + super().__init__(coordinator) + self._attr_device_info = DeviceInfo( + identifiers={(ATTR_ID, station["id"])}, + name=f"{station['brand']} {station['street']} {station['houseNumber']}", + model=station["brand"], + configuration_url="https://www.tankerkoenig.de", + entry_type=DeviceEntryType.SERVICE, + ) diff --git a/homeassistant/components/tankerkoenig/binary_sensor.py b/homeassistant/components/tankerkoenig/binary_sensor.py index 9a2b048e0b8..5f10b54f704 100644 --- a/homeassistant/components/tankerkoenig/binary_sensor.py +++ b/homeassistant/components/tankerkoenig/binary_sensor.py @@ -8,13 +8,11 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE 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 . import TankerkoenigDataUpdateCoordinator +from . import TankerkoenigCoordinatorEntity, TankerkoenigDataUpdateCoordinator from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -25,7 +23,7 @@ async def async_setup_entry( ) -> None: """Set up the tankerkoenig binary sensors.""" - coordinator: TankerkoenigDataUpdateCoordinator = hass.data[DOMAIN][entry.unique_id] + coordinator: TankerkoenigDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] stations = coordinator.stations.values() entities = [] @@ -41,7 +39,7 @@ async def async_setup_entry( async_add_entities(entities) -class StationOpenBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): +class StationOpenBinarySensorEntity(TankerkoenigCoordinatorEntity, BinarySensorEntity): """Shows if a station is open or closed.""" _attr_device_class = BinarySensorDeviceClass.DOOR @@ -53,18 +51,12 @@ class StationOpenBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): show_on_map: bool, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator) + super().__init__(coordinator, station) self._station_id = station["id"] self._attr_name = ( f"{station['brand']} {station['street']} {station['houseNumber']} status" ) self._attr_unique_id = f"{station['id']}_status" - self._attr_device_info = DeviceInfo( - identifiers={(ATTR_ID, station["id"])}, - name=f"{station['brand']} {station['street']} {station['houseNumber']}", - model=station["brand"], - configuration_url="https://www.tankerkoenig.de", - ) if show_on_map: self._attr_extra_state_attributes = { ATTR_LATITUDE: station["lat"], diff --git a/homeassistant/components/tankerkoenig/config_flow.py b/homeassistant/components/tankerkoenig/config_flow.py index af3b5273b16..345b034b027 100644 --- a/homeassistant/components/tankerkoenig/config_flow.py +++ b/homeassistant/components/tankerkoenig/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Tankerkoenig.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any from pytankerkoenig import customException, getNearbyStations @@ -30,7 +31,7 @@ from .const import CONF_FUEL_TYPES, CONF_STATIONS, DEFAULT_RADIUS, DOMAIN, FUEL_ async def async_get_nearby_stations( - hass: HomeAssistant, data: dict[str, Any] + hass: HomeAssistant, data: Mapping[str, Any] ) -> dict[str, Any]: """Fetch nearby stations.""" try: @@ -114,14 +115,12 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self._show_form_user( user_input, errors={CONF_API_KEY: "invalid_auth"} ) - if stations := data.get("stations"): - for station in stations: - self._stations[ - station["id"] - ] = f"{station['brand']} {station['street']} {station['houseNumber']} - ({station['dist']}km)" - - else: + if len(stations := data.get("stations", [])) == 0: return self._show_form_user(user_input, errors={CONF_RADIUS: "no_stations"}) + for station in stations: + self._stations[ + station["id"] + ] = f"{station['brand']} {station['street']} {station['houseNumber']} - ({station['dist']}km)" self._data = user_input @@ -180,7 +179,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_RADIUS, default=user_input.get(CONF_RADIUS, DEFAULT_RADIUS) ): NumberSelector( NumberSelectorConfig( - min=0.1, + min=1.0, max=25, step=0.1, unit_of_measurement=LENGTH_KILOMETERS, @@ -224,7 +223,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): return self.async_create_entry(title="", data=user_input) nearby_stations = await async_get_nearby_stations( - self.hass, dict(self.config_entry.data) + self.hass, self.config_entry.data ) if stations := nearby_stations.get("stations"): for station in stations: diff --git a/homeassistant/components/tankerkoenig/sensor.py b/homeassistant/components/tankerkoenig/sensor.py index 898a38c3c14..c63b0ea0e7e 100644 --- a/homeassistant/components/tankerkoenig/sensor.py +++ b/homeassistant/components/tankerkoenig/sensor.py @@ -7,17 +7,14 @@ from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, - ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, CURRENCY_EURO, ) 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 . import TankerkoenigDataUpdateCoordinator +from . import TankerkoenigCoordinatorEntity, TankerkoenigDataUpdateCoordinator from .const import ( ATTR_BRAND, ATTR_CITY, @@ -39,7 +36,7 @@ async def async_setup_entry( ) -> None: """Set up the tankerkoenig sensors.""" - coordinator: TankerkoenigDataUpdateCoordinator = hass.data[DOMAIN][entry.unique_id] + coordinator: TankerkoenigDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] stations = coordinator.stations.values() entities = [] @@ -62,7 +59,7 @@ async def async_setup_entry( async_add_entities(entities) -class FuelPriceSensor(CoordinatorEntity, SensorEntity): +class FuelPriceSensor(TankerkoenigCoordinatorEntity, SensorEntity): """Contains prices for fuel in a given station.""" _attr_state_class = SensorStateClass.MEASUREMENT @@ -70,19 +67,12 @@ class FuelPriceSensor(CoordinatorEntity, SensorEntity): def __init__(self, fuel_type, station, coordinator, show_on_map): """Initialize the sensor.""" - super().__init__(coordinator) + super().__init__(coordinator, station) self._station_id = station["id"] self._fuel_type = fuel_type self._attr_name = f"{station['brand']} {station['street']} {station['houseNumber']} {FUEL_TYPES[fuel_type]}" self._attr_native_unit_of_measurement = CURRENCY_EURO self._attr_unique_id = f"{station['id']}_{fuel_type}" - self._attr_device_info = DeviceInfo( - identifiers={(ATTR_ID, station["id"])}, - name=f"{station['brand']} {station['street']} {station['houseNumber']}", - model=station["brand"], - configuration_url="https://www.tankerkoenig.de", - ) - attrs = { ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_BRAND: station["brand"], diff --git a/tests/components/tankerkoenig/test_config_flow.py b/tests/components/tankerkoenig/test_config_flow.py index b18df0eed24..f48a09fd64b 100644 --- a/tests/components/tankerkoenig/test_config_flow.py +++ b/tests/components/tankerkoenig/test_config_flow.py @@ -95,7 +95,7 @@ async def test_user(hass: HomeAssistant): assert result["step_id"] == "user" with patch( - "homeassistant.components.tankerkoenig.async_setup_entry" + "homeassistant.components.tankerkoenig.async_setup_entry", return_value=True ) as mock_setup_entry, patch( "homeassistant.components.tankerkoenig.config_flow.getNearbyStations", return_value=MOCK_NEARVY_STATIONS_OK, @@ -147,6 +147,7 @@ async def test_user_already_configured(hass: HomeAssistant): ) assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" async def test_exception_security(hass: HomeAssistant): @@ -193,7 +194,7 @@ async def test_user_no_stations(hass: HomeAssistant): async def test_import(hass: HomeAssistant): """Test starting a flow by import.""" with patch( - "homeassistant.components.tankerkoenig.async_setup_entry" + "homeassistant.components.tankerkoenig.async_setup_entry", return_value=True ) as mock_setup_entry, patch( "homeassistant.components.tankerkoenig.config_flow.getNearbyStations", return_value=MOCK_NEARVY_STATIONS_OK, @@ -233,12 +234,12 @@ async def test_options_flow(hass: HomeAssistant): mock_config.add_to_hass(hass) with patch( - "homeassistant.components.tankerkoenig.async_setup_entry" + "homeassistant.components.tankerkoenig.async_setup_entry", return_value=True ) as mock_setup_entry, patch( "homeassistant.components.tankerkoenig.config_flow.getNearbyStations", return_value=MOCK_NEARVY_STATIONS_OK, ): - await mock_config.async_setup(hass) + await hass.config_entries.async_setup(mock_config.entry_id) await hass.async_block_till_done() assert mock_setup_entry.called From 2942986a7b08571c3b90e0bece7fb37b02072060 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Mon, 30 May 2022 08:52:58 +0200 Subject: [PATCH 1061/3516] Bump bimmer_connected to 0.9.3 (#72677) Bump bimmer_connected to 0.9.3, fix retrieved units Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/coordinator.py | 3 ++- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- homeassistant/components/bmw_connected_drive/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/coordinator.py b/homeassistant/components/bmw_connected_drive/coordinator.py index cff532ae3cb..47d1f358686 100644 --- a/homeassistant/components/bmw_connected_drive/coordinator.py +++ b/homeassistant/components/bmw_connected_drive/coordinator.py @@ -6,7 +6,7 @@ import logging from bimmer_connected.account import MyBMWAccount from bimmer_connected.api.regions import get_region_from_name -from bimmer_connected.vehicle.models import GPSPosition +from bimmer_connected.models import GPSPosition from httpx import HTTPError, TimeoutException from homeassistant.config_entries import ConfigEntry @@ -32,6 +32,7 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator): entry.data[CONF_PASSWORD], get_region_from_name(entry.data[CONF_REGION]), observer_position=GPSPosition(hass.config.latitude, hass.config.longitude), + use_metric_units=hass.config.units.is_metric, ) self.read_only = entry.options[CONF_READ_ONLY] self._entry = entry diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index c7130d12698..75ac3e982e8 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.9.2"], + "requirements": ["bimmer_connected==0.9.3"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 3021e180158..9f19673c398 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -6,8 +6,8 @@ from dataclasses import dataclass import logging from typing import cast +from bimmer_connected.models import ValueWithUnit from bimmer_connected.vehicle import MyBMWVehicle -from bimmer_connected.vehicle.models import ValueWithUnit from homeassistant.components.sensor import ( SensorDeviceClass, diff --git a/requirements_all.txt b/requirements_all.txt index 63f0f929c36..7ecc9b6769c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -394,7 +394,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.2 +bimmer_connected==0.9.3 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb5f6a242e5..eaf81f1b07f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -309,7 +309,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.2 +bimmer_connected==0.9.3 # homeassistant.components.blebox blebox_uniapi==1.3.3 From da7446bf520a9d6cfcf282fac5810f6f1b685111 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 30 May 2022 11:40:36 +0200 Subject: [PATCH 1062/3516] Bump hatasmota to 0.5.1 (#72696) --- .../components/tasmota/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tasmota/test_cover.py | 45 ++++++++++++------- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 772105043fe..4268c4198b2 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.5.0"], + "requirements": ["hatasmota==0.5.1"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/requirements_all.txt b/requirements_all.txt index 7ecc9b6769c..e15aff574d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -795,7 +795,7 @@ hass-nabucasa==0.54.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.5.0 +hatasmota==0.5.1 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eaf81f1b07f..773091643a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -571,7 +571,7 @@ hangups==0.4.18 hass-nabucasa==0.54.0 # homeassistant.components.tasmota -hatasmota==0.5.0 +hatasmota==0.5.1 # homeassistant.components.jewish_calendar hdate==0.10.4 diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py index 843fd72ecf1..06471e11757 100644 --- a/tests/components/tasmota/test_cover.py +++ b/tests/components/tasmota/test_cover.py @@ -111,7 +111,7 @@ async def test_tilt_support(hass, mqtt_mock, setup_tasmota): assert state.attributes["supported_features"] == COVER_SUPPORT -async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): +async def test_controlling_state_via_mqtt_tilt(hass, mqtt_mock, setup_tasmota): """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) config["rl"][0] = 3 @@ -281,7 +281,10 @@ async def test_controlling_state_via_mqtt(hass, mqtt_mock, setup_tasmota): assert state.attributes["current_position"] == 100 -async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmota): +@pytest.mark.parametrize("tilt", ("", ',"Tilt":0')) +async def test_controlling_state_via_mqtt_inverted( + hass, mqtt_mock, setup_tasmota, tilt +): """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) config["rl"][0] = 3 @@ -310,7 +313,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/SENSOR", - '{"Shutter1":{"Position":54,"Direction":-1}}', + '{"Shutter1":{"Position":54,"Direction":-1' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "opening" @@ -319,21 +322,25 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/SENSOR", - '{"Shutter1":{"Position":100,"Direction":1}}', + '{"Shutter1":{"Position":100,"Direction":1' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "closing" assert state.attributes["current_position"] == 0 async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/SENSOR", '{"Shutter1":{"Position":0,"Direction":0}}' + hass, + "tasmota_49A3BC/tele/SENSOR", + '{"Shutter1":{"Position":0,"Direction":0' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "open" assert state.attributes["current_position"] == 100 async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/SENSOR", '{"Shutter1":{"Position":99,"Direction":0}}' + hass, + "tasmota_49A3BC/tele/SENSOR", + '{"Shutter1":{"Position":99,"Direction":0' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "open" @@ -342,7 +349,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/tele/SENSOR", - '{"Shutter1":{"Position":100,"Direction":0}}', + '{"Shutter1":{"Position":100,"Direction":0' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "closed" @@ -352,7 +359,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"Shutter1":{"Position":54,"Direction":-1}}}', + '{"StatusSNS":{"Shutter1":{"Position":54,"Direction":-1' + tilt + "}}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "opening" @@ -361,7 +368,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"Shutter1":{"Position":100,"Direction":1}}}', + '{"StatusSNS":{"Shutter1":{"Position":100,"Direction":1' + tilt + "}}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "closing" @@ -370,7 +377,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"Shutter1":{"Position":0,"Direction":0}}}', + '{"StatusSNS":{"Shutter1":{"Position":0,"Direction":0' + tilt + "}}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "open" @@ -379,7 +386,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"Shutter1":{"Position":99,"Direction":0}}}', + '{"StatusSNS":{"Shutter1":{"Position":99,"Direction":0' + tilt + "}}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "open" @@ -388,7 +395,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/STATUS10", - '{"StatusSNS":{"Shutter1":{"Position":100,"Direction":0}}}', + '{"StatusSNS":{"Shutter1":{"Position":100,"Direction":0' + tilt + "}}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "closed" @@ -398,7 +405,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", - '{"Shutter1":{"Position":54,"Direction":-1}}', + '{"Shutter1":{"Position":54,"Direction":-1' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "opening" @@ -407,21 +414,25 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", - '{"Shutter1":{"Position":100,"Direction":1}}', + '{"Shutter1":{"Position":100,"Direction":1' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "closing" assert state.attributes["current_position"] == 0 async_fire_mqtt_message( - hass, "tasmota_49A3BC/stat/RESULT", '{"Shutter1":{"Position":0,"Direction":0}}' + hass, + "tasmota_49A3BC/stat/RESULT", + '{"Shutter1":{"Position":0,"Direction":0' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "open" assert state.attributes["current_position"] == 100 async_fire_mqtt_message( - hass, "tasmota_49A3BC/stat/RESULT", '{"Shutter1":{"Position":1,"Direction":0}}' + hass, + "tasmota_49A3BC/stat/RESULT", + '{"Shutter1":{"Position":1,"Direction":0' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "open" @@ -430,7 +441,7 @@ async def test_controlling_state_via_mqtt_inverted(hass, mqtt_mock, setup_tasmot async_fire_mqtt_message( hass, "tasmota_49A3BC/stat/RESULT", - '{"Shutter1":{"Position":100,"Direction":0}}', + '{"Shutter1":{"Position":100,"Direction":0' + tilt + "}}", ) state = hass.states.get("cover.tasmota_cover_1") assert state.state == "closed" From 2809592e71729473aeca358a5b7678f2a09de213 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 30 May 2022 14:21:20 +0200 Subject: [PATCH 1063/3516] Improve handling of MQTT overridden settings (#72698) * Improve handling of MQTT overridden settings * Don't warn unless config entry overrides yaml --- homeassistant/components/mqtt/__init__.py | 13 +++++++------ tests/components/mqtt/test_init.py | 5 ----- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 78f64387435..1728dd7f2c7 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -685,14 +685,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # User has configuration.yaml config, warn about config entry overrides elif any(key in conf for key in entry.data): shared_keys = conf.keys() & entry.data.keys() - override = {k: entry.data[k] for k in shared_keys} + override = {k: entry.data[k] for k in shared_keys if conf[k] != entry.data[k]} if CONF_PASSWORD in override: override[CONF_PASSWORD] = "********" - _LOGGER.warning( - "Deprecated configuration settings found in configuration.yaml. " - "These settings from your configuration entry will override: %s", - override, - ) + if override: + _LOGGER.warning( + "Deprecated configuration settings found in configuration.yaml. " + "These settings from your configuration entry will override: %s", + override, + ) # Merge advanced configuration values from configuration.yaml conf = _merge_extended_config(entry, conf) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index a370bd67ec1..07c39d70df0 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1715,11 +1715,6 @@ async def test_update_incomplete_entry( "The 'broker' option is deprecated, please remove it from your configuration" in caplog.text ) - assert ( - "Deprecated configuration settings found in configuration.yaml. These settings " - "from your configuration entry will override: {'broker': 'yaml_broker'}" - in caplog.text - ) # Discover a device to verify the entry was setup correctly async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) From 72a79736a6b2cef5e44719e15af886c909308a1f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 May 2022 14:40:55 -0700 Subject: [PATCH 1064/3516] Bumped version to 2022.6.0b4 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8d2bcd33f4e..fe454dbc8a1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index db64c7330ce..19aefbb6f90 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.0b3 +version = 2022.6.0b4 url = https://www.home-assistant.io/ [options] From 285a7251dff3058c1e6bc021b466e9247065f9f6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 31 May 2022 00:10:38 +0200 Subject: [PATCH 1065/3516] Adjust config-flow type hints in zwave_me (#72714) --- .../components/zwave_me/config_flow.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zwave_me/config_flow.py b/homeassistant/components/zwave_me/config_flow.py index 4fee380ca48..9089e5514f5 100644 --- a/homeassistant/components/zwave_me/config_flow.py +++ b/homeassistant/components/zwave_me/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure ZWaveMe integration.""" +from __future__ import annotations import logging @@ -6,7 +7,9 @@ from url_normalize import url_normalize import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.const import CONF_TOKEN, CONF_URL +from homeassistant.data_entry_flow import FlowResult from . import helpers from .const import DOMAIN @@ -17,13 +20,15 @@ _LOGGER = logging.getLogger(__name__) class ZWaveMeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """ZWaveMe integration config flow.""" - def __init__(self): + def __init__(self) -> None: """Initialize flow.""" - self.url = None - self.token = None - self.uuid = None + self.url: str | None = None + self.token: str | None = None + self.uuid: str | None = None - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Handle a flow initialized by the user or started with zeroconf.""" errors = {} placeholders = { @@ -55,6 +60,7 @@ class ZWaveMeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not self.url.startswith(("ws://", "wss://")): self.url = f"ws://{self.url}" self.url = url_normalize(self.url, default_scheme="ws") + assert self.url if self.uuid is None: self.uuid = await helpers.get_uuid(self.url, self.token) if self.uuid is not None: @@ -76,7 +82,9 @@ class ZWaveMeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_zeroconf(self, discovery_info): + async def async_step_zeroconf( + self, discovery_info: ZeroconfServiceInfo + ) -> FlowResult: """ Handle a discovered Z-Wave accessory - get url to pass into user step. From 6f8ba7ee2f7240c7b4a76e424f116005cd55595b Mon Sep 17 00:00:00 2001 From: eyager1 <44526531+eyager1@users.noreply.github.com> Date: Mon, 30 May 2022 18:32:52 -0400 Subject: [PATCH 1066/3516] Add empty string to list of invalid states (#72590) Add null state to list of invalid states --- homeassistant/components/statistics/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index ac62b63e8ca..3f33fa015b9 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -350,7 +350,7 @@ class StatisticsSensor(SensorEntity): if new_state.state == STATE_UNAVAILABLE: self.attributes[STAT_SOURCE_VALUE_VALID] = None return - if new_state.state in (STATE_UNKNOWN, None): + if new_state.state in (STATE_UNKNOWN, None, ""): self.attributes[STAT_SOURCE_VALUE_VALID] = False return From 565b60210d8f68991d3b9736e1e6179c932999d1 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 30 May 2022 18:41:30 -0400 Subject: [PATCH 1067/3516] Add @lymanepp as codeowner to tomorrowio (#72725) --- CODEOWNERS | 4 ++-- homeassistant/components/tomorrowio/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 545d5027ecb..db9c0ff69d8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1063,8 +1063,8 @@ build.json @home-assistant/supervisor /tests/components/todoist/ @boralyl /homeassistant/components/tolo/ @MatthiasLohr /tests/components/tolo/ @MatthiasLohr -/homeassistant/components/tomorrowio/ @raman325 -/tests/components/tomorrowio/ @raman325 +/homeassistant/components/tomorrowio/ @raman325 @lymanepp +/tests/components/tomorrowio/ @raman325 @lymanepp /homeassistant/components/totalconnect/ @austinmroczek /tests/components/totalconnect/ @austinmroczek /homeassistant/components/tplink/ @rytilahti @thegardenmonkey @bdraco diff --git a/homeassistant/components/tomorrowio/manifest.json b/homeassistant/components/tomorrowio/manifest.json index a577ec517c1..5447b90d1ce 100644 --- a/homeassistant/components/tomorrowio/manifest.json +++ b/homeassistant/components/tomorrowio/manifest.json @@ -4,6 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tomorrowio", "requirements": ["pytomorrowio==0.3.3"], - "codeowners": ["@raman325"], + "codeowners": ["@raman325", "@lymanepp"], "iot_class": "cloud_polling" } From 6b3f6e22d0e0807b633bd33311c300443fcf89bf Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 30 May 2022 17:48:42 -0600 Subject: [PATCH 1068/3516] Fix invalid RainMachine syntax (#72732) --- homeassistant/components/rainmachine/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index a220aafa2a5..8d339682305 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -393,7 +393,7 @@ class RainMachineZone(RainMachineActivitySwitch): ATTR_CURRENT_CYCLE: data["cycle"], ATTR_ID: data["uid"], ATTR_NO_CYCLES: data["noOfCycles"], - ATTR_RESTRICTIONS: data("restriction"), + ATTR_RESTRICTIONS: data["restriction"], ATTR_SLOPE: SLOPE_TYPE_MAP.get(data["slope"], 99), ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(data["soil"], 99), ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(data["group_id"], 99), From 362f5720edfc2df4c5cd4ff8252efe57f54d99f4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 31 May 2022 00:23:11 +0000 Subject: [PATCH 1069/3516] [ci skip] Translation update --- .../components/aurora/translations/ja.json | 2 +- .../components/generic/translations/no.json | 2 ++ .../components/google/translations/bg.json | 9 +++++++ .../here_travel_time/translations/bg.json | 19 +++++++++++++++ .../components/ialarm_xr/translations/bg.json | 21 ++++++++++++++++ .../components/ialarm_xr/translations/de.json | 1 + .../components/ialarm_xr/translations/el.json | 1 + .../components/ialarm_xr/translations/fr.json | 1 + .../components/ialarm_xr/translations/hu.json | 1 + .../components/ialarm_xr/translations/id.json | 1 + .../components/ialarm_xr/translations/nl.json | 1 + .../components/ialarm_xr/translations/no.json | 22 +++++++++++++++++ .../components/ialarm_xr/translations/pl.json | 1 + .../ialarm_xr/translations/pt-BR.json | 1 + .../components/laundrify/translations/bg.json | 22 +++++++++++++++++ .../components/min_max/translations/ja.json | 4 ++-- .../components/plugwise/translations/bg.json | 4 ++++ .../components/plugwise/translations/de.json | 10 +++++--- .../components/plugwise/translations/el.json | 6 ++++- .../components/plugwise/translations/en.json | 24 ++++++++++++++++++- .../components/plugwise/translations/fr.json | 10 +++++--- .../components/plugwise/translations/hu.json | 6 ++++- .../components/plugwise/translations/id.json | 9 ++++--- .../components/plugwise/translations/nl.json | 6 ++++- .../components/plugwise/translations/no.json | 10 +++++--- .../plugwise/translations/pt-BR.json | 10 +++++--- .../components/plugwise/translations/tr.json | 4 +++- .../components/recorder/translations/bg.json | 7 ++++++ .../tankerkoenig/translations/fr.json | 3 ++- .../tankerkoenig/translations/ja.json | 3 ++- .../tankerkoenig/translations/nl.json | 3 ++- .../tankerkoenig/translations/no.json | 3 ++- .../tankerkoenig/translations/pl.json | 3 ++- .../components/threshold/translations/ja.json | 6 ++--- .../totalconnect/translations/no.json | 11 +++++++++ .../components/zwave_js/translations/ja.json | 2 +- 36 files changed, 217 insertions(+), 32 deletions(-) create mode 100644 homeassistant/components/here_travel_time/translations/bg.json create mode 100644 homeassistant/components/ialarm_xr/translations/bg.json create mode 100644 homeassistant/components/ialarm_xr/translations/no.json create mode 100644 homeassistant/components/laundrify/translations/bg.json create mode 100644 homeassistant/components/recorder/translations/bg.json diff --git a/homeassistant/components/aurora/translations/ja.json b/homeassistant/components/aurora/translations/ja.json index a4d9b830968..455ceceacac 100644 --- a/homeassistant/components/aurora/translations/ja.json +++ b/homeassistant/components/aurora/translations/ja.json @@ -17,7 +17,7 @@ "step": { "init": { "data": { - "threshold": "\u3057\u304d\u3044\u5024(%)" + "threshold": "\u95be\u5024(%)" } } } diff --git a/homeassistant/components/generic/translations/no.json b/homeassistant/components/generic/translations/no.json index 0c228e314d2..72355d002ed 100644 --- a/homeassistant/components/generic/translations/no.json +++ b/homeassistant/components/generic/translations/no.json @@ -15,6 +15,7 @@ "stream_no_video": "Stream har ingen video", "stream_not_permitted": "Operasjon er ikke tillatt mens du pr\u00f8ver \u00e5 koble til str\u00f8m. Feil RTSP-transportprotokoll?", "stream_unauthorised": "Autorisasjonen mislyktes under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8mmen", + "template_error": "Feil ved gjengivelse av mal. Se gjennom loggen for mer informasjon.", "timeout": "Tidsavbrudd under innlasting av URL", "unable_still_load": "Kan ikke laste inn gyldig bilde fra URL-adresse for stillbilde (f.eks. ugyldig verts-, URL- eller godkjenningsfeil). Se gjennom loggen hvis du vil ha mer informasjon.", "unknown": "Uventet feil" @@ -57,6 +58,7 @@ "stream_no_video": "Stream har ingen video", "stream_not_permitted": "Operasjon er ikke tillatt mens du pr\u00f8ver \u00e5 koble til str\u00f8m. Feil RTSP-transportprotokoll?", "stream_unauthorised": "Autorisasjonen mislyktes under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8mmen", + "template_error": "Feil ved gjengivelse av mal. Se gjennom loggen for mer informasjon.", "timeout": "Tidsavbrudd under innlasting av URL", "unable_still_load": "Kan ikke laste inn gyldig bilde fra URL-adresse for stillbilde (f.eks. ugyldig verts-, URL- eller godkjenningsfeil). Se gjennom loggen hvis du vil ha mer informasjon.", "unknown": "Uventet feil" diff --git a/homeassistant/components/google/translations/bg.json b/homeassistant/components/google/translations/bg.json index 38b08fc3616..0d82b088635 100644 --- a/homeassistant/components/google/translations/bg.json +++ b/homeassistant/components/google/translations/bg.json @@ -16,5 +16,14 @@ "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" } } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "\u0414\u043e\u0441\u0442\u044a\u043f \u043d\u0430 Home Assistant \u0434\u043e Google \u041a\u0430\u043b\u0435\u043d\u0434\u0430\u0440" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/bg.json b/homeassistant/components/here_travel_time/translations/bg.json new file mode 100644 index 00000000000..73cda8df2bb --- /dev/null +++ b/homeassistant/components/here_travel_time/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": { + "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", + "name": "\u0418\u043c\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/bg.json b/homeassistant/components/ialarm_xr/translations/bg.json new file mode 100644 index 00000000000..2189a9653ed --- /dev/null +++ b/homeassistant/components/ialarm_xr/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\u0448\u043d\u043e \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", + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/de.json b/homeassistant/components/ialarm_xr/translations/de.json index ccda778dcf6..32b35294072 100644 --- a/homeassistant/components/ialarm_xr/translations/de.json +++ b/homeassistant/components/ialarm_xr/translations/de.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "timeout": "Zeit\u00fcberschreitung beim Verbindungsaufbau", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/ialarm_xr/translations/el.json b/homeassistant/components/ialarm_xr/translations/el.json index 067055d6654..acc75012a67 100644 --- a/homeassistant/components/ialarm_xr/translations/el.json +++ b/homeassistant/components/ialarm_xr/translations/el.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { diff --git a/homeassistant/components/ialarm_xr/translations/fr.json b/homeassistant/components/ialarm_xr/translations/fr.json index dec09505d3e..2ade23c9f4e 100644 --- a/homeassistant/components/ialarm_xr/translations/fr.json +++ b/homeassistant/components/ialarm_xr/translations/fr.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", + "timeout": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9", "unknown": "Erreur inattendue" }, "step": { diff --git a/homeassistant/components/ialarm_xr/translations/hu.json b/homeassistant/components/ialarm_xr/translations/hu.json index 015de9b4bf6..6f72253aae6 100644 --- a/homeassistant/components/ialarm_xr/translations/hu.json +++ b/homeassistant/components/ialarm_xr/translations/hu.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { diff --git a/homeassistant/components/ialarm_xr/translations/id.json b/homeassistant/components/ialarm_xr/translations/id.json index 558b7de6b24..e4688af2b37 100644 --- a/homeassistant/components/ialarm_xr/translations/id.json +++ b/homeassistant/components/ialarm_xr/translations/id.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Gagal terhubung", + "timeout": "Tenggang waktu membuat koneksi habis", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/ialarm_xr/translations/nl.json b/homeassistant/components/ialarm_xr/translations/nl.json index 3ec5fe68d61..a32ee7ccfbe 100644 --- a/homeassistant/components/ialarm_xr/translations/nl.json +++ b/homeassistant/components/ialarm_xr/translations/nl.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Kan geen verbinding maken", + "timeout": "Time-out bij het maken van verbinding", "unknown": "Onverwachte fout" }, "step": { diff --git a/homeassistant/components/ialarm_xr/translations/no.json b/homeassistant/components/ialarm_xr/translations/no.json new file mode 100644 index 00000000000..ccadf2f9972 --- /dev/null +++ b/homeassistant/components/ialarm_xr/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "timeout": "Tidsavbrudd oppretter forbindelse", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/pl.json b/homeassistant/components/ialarm_xr/translations/pl.json index 137973010ca..3880b4a6e09 100644 --- a/homeassistant/components/ialarm_xr/translations/pl.json +++ b/homeassistant/components/ialarm_xr/translations/pl.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "timeout": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { diff --git a/homeassistant/components/ialarm_xr/translations/pt-BR.json b/homeassistant/components/ialarm_xr/translations/pt-BR.json index f18e4820eac..37dd49dc9d8 100644 --- a/homeassistant/components/ialarm_xr/translations/pt-BR.json +++ b/homeassistant/components/ialarm_xr/translations/pt-BR.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Falhou ao se conectar", + "timeout": "Tempo limite estabelecendo conex\u00e3o", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/laundrify/translations/bg.json b/homeassistant/components/laundrify/translations/bg.json new file mode 100644 index 00000000000..4721ecf584e --- /dev/null +++ b/homeassistant/components/laundrify/translations/bg.json @@ -0,0 +1,22 @@ +{ + "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\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": { + "init": { + "data": { + "code": "\u041a\u043e\u0434 \u0437\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 (xxx-xxx)" + } + }, + "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/min_max/translations/ja.json b/homeassistant/components/min_max/translations/ja.json index f9e767082d6..072e7a9b487 100644 --- a/homeassistant/components/min_max/translations/ja.json +++ b/homeassistant/components/min_max/translations/ja.json @@ -12,7 +12,7 @@ "round_digits": "\u7d71\u8a08\u7684\u7279\u6027\u304c\u5e73\u5747\u5024\u307e\u305f\u306f\u4e2d\u592e\u5024\u306e\u5834\u5408\u306b\u3001\u51fa\u529b\u306b\u542b\u307e\u308c\u308b\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002" }, "description": "\u7d71\u8a08\u7684\u7279\u6027\u304c\u5e73\u5747\u307e\u305f\u306f\u4e2d\u592e\u5024\u306a\u5834\u5408\u306e\u7cbe\u5ea6\u3067\u3001\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002", - "title": "\u6700\u5c0f/\u6700\u5927/\u5e73\u5747/\u4e2d\u592e\u5024 \u30bb\u30f3\u30b5\u30fc\u3092\u8ffd\u52a0" + "title": "\u6700\u5c0f/\u6700\u5927/\u5e73\u5747/\u4e2d\u592e\u5024\u30bb\u30f3\u30b5\u30fc\u3092\u8ffd\u52a0" } } }, @@ -30,5 +30,5 @@ } } }, - "title": "\u6700\u5c0f/\u6700\u5927/\u5e73\u5747/\u4e2d\u592e\u5024 \u30bb\u30f3\u30b5\u30fc" + "title": "\u6700\u5c0f/\u6700\u5927/\u5e73\u5747/\u4e2d\u592e\u5024\u30bb\u30f3\u30b5\u30fc" } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/bg.json b/homeassistant/components/plugwise/translations/bg.json index 0c1cf067319..18450edfce7 100644 --- a/homeassistant/components/plugwise/translations/bg.json +++ b/homeassistant/components/plugwise/translations/bg.json @@ -11,6 +11,10 @@ "flow_title": "{name}", "step": { "user": { + "data": { + "host": "IP \u0430\u0434\u0440\u0435\u0441", + "port": "\u041f\u043e\u0440\u0442" + }, "description": "\u041f\u0440\u043e\u0434\u0443\u043a\u0442:" }, "user_gateway": { diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index f6ce5fb1b2d..2b9d112977a 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -13,10 +13,14 @@ "step": { "user": { "data": { - "flow_type": "Verbindungstyp" + "flow_type": "Verbindungstyp", + "host": "IP-Adresse", + "password": "Smile ID", + "port": "Port", + "username": "Smile-Benutzername" }, - "description": "Produkt:", - "title": "Plugwise Typ" + "description": "Bitte eingeben", + "title": "Stelle eine Verbindung zu Smile her" }, "user_gateway": { "data": { diff --git a/homeassistant/components/plugwise/translations/el.json b/homeassistant/components/plugwise/translations/el.json index 8407cb38cb1..caee7bb5a88 100644 --- a/homeassistant/components/plugwise/translations/el.json +++ b/homeassistant/components/plugwise/translations/el.json @@ -13,7 +13,11 @@ "step": { "user": { "data": { - "flow_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + "flow_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP", + "password": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc Smile", + "port": "\u0398\u03cd\u03c1\u03b1", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 Smile" }, "description": "\u03a0\u03c1\u03bf\u03ca\u03cc\u03bd:", "title": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03b2\u03cd\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2" diff --git a/homeassistant/components/plugwise/translations/en.json b/homeassistant/components/plugwise/translations/en.json index 48d3d2d0e46..3f365bfa25e 100644 --- a/homeassistant/components/plugwise/translations/en.json +++ b/homeassistant/components/plugwise/translations/en.json @@ -9,8 +9,20 @@ "invalid_setup": "Add your Adam instead of your Anna, see the Home Assistant Plugwise integration documentation for more information", "unknown": "Unexpected error" }, + "flow_title": "{name}", "step": { "user": { + "data": { + "flow_type": "Connection type", + "host": "IP Address", + "password": "Smile ID", + "port": "Port", + "username": "Smile Username" + }, + "description": "Please enter", + "title": "Connect to the Smile" + }, + "user_gateway": { "data": { "host": "IP Address", "password": "Smile ID", @@ -21,5 +33,15 @@ "title": "Connect to the Smile" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Scan Interval (seconds)" + }, + "description": "Adjust Plugwise Options" + } + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/fr.json b/homeassistant/components/plugwise/translations/fr.json index baa6074e68a..0306437b405 100644 --- a/homeassistant/components/plugwise/translations/fr.json +++ b/homeassistant/components/plugwise/translations/fr.json @@ -13,9 +13,13 @@ "step": { "user": { "data": { - "flow_type": "Type de connexion" + "flow_type": "Type de connexion", + "host": "Adresse IP", + "password": "ID Smile", + "port": "Port", + "username": "Nom d'utilisateur Smile" }, - "description": "Veuillez saisir :", + "description": "Veuillez saisir", "title": "Se connecter \u00e0 Smile" }, "user_gateway": { @@ -23,7 +27,7 @@ "host": "Adresse IP", "password": "ID Smile", "port": "Port", - "username": "Nom d'utilisateur de sourire" + "username": "Nom d'utilisateur Smile" }, "description": "Veuillez saisir :", "title": "Se connecter \u00e0 Smile" diff --git a/homeassistant/components/plugwise/translations/hu.json b/homeassistant/components/plugwise/translations/hu.json index 265a99186ba..8b9b619f728 100644 --- a/homeassistant/components/plugwise/translations/hu.json +++ b/homeassistant/components/plugwise/translations/hu.json @@ -13,7 +13,11 @@ "step": { "user": { "data": { - "flow_type": "Kapcsolat t\u00edpusa" + "flow_type": "Kapcsolat t\u00edpusa", + "host": "IP c\u00edm", + "password": "Smile azonos\u00edt\u00f3", + "port": "Port", + "username": "Smile Felhaszn\u00e1l\u00f3n\u00e9v" }, "description": "Term\u00e9k:", "title": "Plugwise t\u00edpus" diff --git a/homeassistant/components/plugwise/translations/id.json b/homeassistant/components/plugwise/translations/id.json index 8878a4e775d..436514d1a82 100644 --- a/homeassistant/components/plugwise/translations/id.json +++ b/homeassistant/components/plugwise/translations/id.json @@ -13,10 +13,13 @@ "step": { "user": { "data": { - "flow_type": "Jenis koneksi" + "flow_type": "Jenis koneksi", + "host": "Alamat IP", + "password": "ID Smile", + "port": "Port" }, - "description": "Produk:", - "title": "Jenis Plugwise" + "description": "Masukkan", + "title": "Hubungkan ke Smile" }, "user_gateway": { "data": { diff --git a/homeassistant/components/plugwise/translations/nl.json b/homeassistant/components/plugwise/translations/nl.json index 8109386013c..14d25d6716e 100644 --- a/homeassistant/components/plugwise/translations/nl.json +++ b/homeassistant/components/plugwise/translations/nl.json @@ -13,7 +13,11 @@ "step": { "user": { "data": { - "flow_type": "Verbindingstype" + "flow_type": "Verbindingstype", + "host": "IP-adres", + "password": "Smile-ID", + "port": "Poort", + "username": "Smile-gebruikersnaam" }, "description": "Product:", "title": "Plugwise type" diff --git a/homeassistant/components/plugwise/translations/no.json b/homeassistant/components/plugwise/translations/no.json index d8ce2d8956a..d9fbb15b2e8 100644 --- a/homeassistant/components/plugwise/translations/no.json +++ b/homeassistant/components/plugwise/translations/no.json @@ -13,10 +13,14 @@ "step": { "user": { "data": { - "flow_type": "Tilkoblingstype" + "flow_type": "Tilkoblingstype", + "host": "IP adresse", + "password": "Smile ID", + "port": "Port", + "username": "Smile brukernavn" }, - "description": "Produkt:", - "title": "" + "description": "Vennligst skriv inn", + "title": "Koble til Smile" }, "user_gateway": { "data": { diff --git a/homeassistant/components/plugwise/translations/pt-BR.json b/homeassistant/components/plugwise/translations/pt-BR.json index 35a08d93114..12f8070f074 100644 --- a/homeassistant/components/plugwise/translations/pt-BR.json +++ b/homeassistant/components/plugwise/translations/pt-BR.json @@ -13,10 +13,14 @@ "step": { "user": { "data": { - "flow_type": "Tipo de conex\u00e3o" + "flow_type": "Tipo de conex\u00e3o", + "host": "Endere\u00e7o IP", + "password": "ID do Smile", + "port": "Porta", + "username": "Nome de usu\u00e1rio do Smile" }, - "description": "Produto:", - "title": "Tipo Plugwise" + "description": "Por favor, insira", + "title": "Conecte-se ao Smile" }, "user_gateway": { "data": { diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json index ae756bf15d4..b4a07700d6f 100644 --- a/homeassistant/components/plugwise/translations/tr.json +++ b/homeassistant/components/plugwise/translations/tr.json @@ -13,7 +13,9 @@ "step": { "user": { "data": { - "flow_type": "Ba\u011flant\u0131 t\u00fcr\u00fc" + "flow_type": "Ba\u011flant\u0131 t\u00fcr\u00fc", + "host": "IP Adresi", + "port": "Port" }, "description": "\u00dcr\u00fcn:", "title": "Plugwise tipi" diff --git a/homeassistant/components/recorder/translations/bg.json b/homeassistant/components/recorder/translations/bg.json new file mode 100644 index 00000000000..2098a389361 --- /dev/null +++ b/homeassistant/components/recorder/translations/bg.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "database_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 \u0431\u0430\u0437\u0430\u0442\u0430 \u0434\u0430\u043d\u043d\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/fr.json b/homeassistant/components/tankerkoenig/translations/fr.json index 1a52eee5d39..865fdae66a1 100644 --- a/homeassistant/components/tankerkoenig/translations/fr.json +++ b/homeassistant/components/tankerkoenig/translations/fr.json @@ -32,7 +32,8 @@ "init": { "data": { "scan_interval": "Intervalle de mise \u00e0 jour", - "show_on_map": "Afficher les stations-services sur la carte" + "show_on_map": "Afficher les stations-services sur la carte", + "stations": "Stations-services" }, "title": "Options Tankerkoenig" } diff --git a/homeassistant/components/tankerkoenig/translations/ja.json b/homeassistant/components/tankerkoenig/translations/ja.json index 687e05322d5..8aa88b83f4a 100644 --- a/homeassistant/components/tankerkoenig/translations/ja.json +++ b/homeassistant/components/tankerkoenig/translations/ja.json @@ -32,7 +32,8 @@ "init": { "data": { "scan_interval": "\u66f4\u65b0\u9593\u9694", - "show_on_map": "\u5730\u56f3\u4e0a\u306b\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u3092\u8868\u793a\u3059\u308b" + "show_on_map": "\u5730\u56f3\u4e0a\u306b\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u3092\u8868\u793a\u3059\u308b", + "stations": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3" }, "title": "Tankerkoenig\u30aa\u30d7\u30b7\u30e7\u30f3" } diff --git a/homeassistant/components/tankerkoenig/translations/nl.json b/homeassistant/components/tankerkoenig/translations/nl.json index 66d442a71f6..ea4aea3ff95 100644 --- a/homeassistant/components/tankerkoenig/translations/nl.json +++ b/homeassistant/components/tankerkoenig/translations/nl.json @@ -32,7 +32,8 @@ "init": { "data": { "scan_interval": "Update Interval", - "show_on_map": "Toon stations op kaart" + "show_on_map": "Toon stations op kaart", + "stations": "Stations" }, "title": "Tankerkoenig opties" } diff --git a/homeassistant/components/tankerkoenig/translations/no.json b/homeassistant/components/tankerkoenig/translations/no.json index 9d0b6ddab52..bbca71d933b 100644 --- a/homeassistant/components/tankerkoenig/translations/no.json +++ b/homeassistant/components/tankerkoenig/translations/no.json @@ -32,7 +32,8 @@ "init": { "data": { "scan_interval": "Oppdateringsintervall", - "show_on_map": "Vis stasjoner p\u00e5 kart" + "show_on_map": "Vis stasjoner p\u00e5 kart", + "stations": "Stasjoner" }, "title": "Tankerkoenig alternativer" } diff --git a/homeassistant/components/tankerkoenig/translations/pl.json b/homeassistant/components/tankerkoenig/translations/pl.json index e13ae2c4783..282c87cf3c8 100644 --- a/homeassistant/components/tankerkoenig/translations/pl.json +++ b/homeassistant/components/tankerkoenig/translations/pl.json @@ -32,7 +32,8 @@ "init": { "data": { "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji", - "show_on_map": "Poka\u017c stacje na mapie" + "show_on_map": "Poka\u017c stacje na mapie", + "stations": "Stacje" }, "title": "Opcje Tankerkoenig" } diff --git a/homeassistant/components/threshold/translations/ja.json b/homeassistant/components/threshold/translations/ja.json index 1978fa4f3c5..821de593499 100644 --- a/homeassistant/components/threshold/translations/ja.json +++ b/homeassistant/components/threshold/translations/ja.json @@ -12,7 +12,7 @@ "name": "\u540d\u524d", "upper": "\u4e0a\u9650\u5024" }, - "description": "\u30bb\u30f3\u30b5\u30fc\u306e\u30aa\u30f3\u3068\u30aa\u30d5\u3092\u5207\u308a\u66ff\u3048\u308b\u30bf\u30a4\u30df\u30f3\u30b0\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\n\n\u4e0b\u9650\u306e\u307f\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c\u4e0b\u9650\u5024\u3088\u308a\u5c0f\u3055\u3044\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002\n\u4e0a\u9650\u306e\u307f\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c\u4e0a\u9650\u5024\u3088\u308a\u5927\u304d\u3044\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002\n\u4e0b\u9650\u3068\u4e0a\u9650\u306e\u4e21\u65b9\u3092\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c[\u4e0b\u9650..\u4e0a\u9650]\u306e\u7bc4\u56f2\u306b\u3042\u308b\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002", + "description": "\u30bb\u30f3\u30b5\u30fc\u306e\u30aa\u30f3\u3068\u30aa\u30d5\u3092\u5207\u308a\u66ff\u3048\u308b\u30bf\u30a4\u30df\u30f3\u30b0\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\n\n\u4e0b\u9650\u306e\u307f\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c\u4e0b\u9650\u5024\u3088\u308a\u5c0f\u3055\u3044\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002\n\u4e0a\u9650\u306e\u307f\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c\u4e0a\u9650\u5024\u3088\u308a\u5927\u304d\u3044\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002\n\u4e0b\u9650\u3068\u4e0a\u9650\u306e\u4e21\u65b9\u3092\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c[\u4e0b\u9650..\u4e0a\u9650]\u306e\u7bc4\u56f2\u5185\u306b\u3042\u308b\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002", "title": "\u65b0\u3057\u3044\u3057\u304d\u3044\u5024\u30bb\u30f3\u30b5\u30fc" } } @@ -30,9 +30,9 @@ "name": "\u540d\u524d", "upper": "\u4e0a\u9650\u5024" }, - "description": "\u30bb\u30f3\u30b5\u30fc\u306e\u30aa\u30f3\u3068\u30aa\u30d5\u3092\u5207\u308a\u66ff\u3048\u308b\u30bf\u30a4\u30df\u30f3\u30b0\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\n\n\u4e0b\u9650\u306e\u307f\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c\u4e0b\u9650\u5024\u3088\u308a\u5c0f\u3055\u3044\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002\n\u4e0a\u9650\u306e\u307f\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c\u4e0a\u9650\u5024\u3088\u308a\u5927\u304d\u3044\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002\n\u4e0b\u9650\u3068\u4e0a\u9650\u306e\u4e21\u65b9\u3092\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c[\u4e0b\u9650..\u4e0a\u9650]\u306e\u7bc4\u56f2\u306b\u3042\u308b\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002" + "description": "\u30bb\u30f3\u30b5\u30fc\u306e\u30aa\u30f3\u3068\u30aa\u30d5\u3092\u5207\u308a\u66ff\u3048\u308b\u30bf\u30a4\u30df\u30f3\u30b0\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\n\n\u4e0b\u9650\u306e\u307f\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c\u4e0b\u9650\u5024\u3088\u308a\u5c0f\u3055\u3044\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002\n\u4e0a\u9650\u306e\u307f\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c\u4e0a\u9650\u5024\u3088\u308a\u5927\u304d\u3044\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002\n\u4e0b\u9650\u3068\u4e0a\u9650\u306e\u4e21\u65b9\u3092\u8a2d\u5b9a - \u5165\u529b\u30bb\u30f3\u30b5\u30fc\u306e\u5024\u304c[\u4e0b\u9650..\u4e0a\u9650]\u306e\u7bc4\u56f2\u5185\u306b\u3042\u308b\u3068\u304d\u306b\u30aa\u30f3\u306b\u3057\u307e\u3059\u3002" } } }, - "title": "\u3057\u304d\u3044\u5024\u30bb\u30f3\u30b5\u30fc" + "title": "\u95be\u5024\u30bb\u30f3\u30b5\u30fc" } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/no.json b/homeassistant/components/totalconnect/translations/no.json index 86cfedf51d4..4ea6b791b23 100644 --- a/homeassistant/components/totalconnect/translations/no.json +++ b/homeassistant/components/totalconnect/translations/no.json @@ -28,5 +28,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "Auto bypass lavt batteri" + }, + "description": "Omg\u00e5 automatisk soner i det \u00f8yeblikket de rapporterer lavt batteri.", + "title": "TotalConnect-alternativer" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index 902955c1b9e..f4c5a62b050 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -88,7 +88,7 @@ "event.value_notification.scene_activation": "{subtype} \u3067\u306e\u30b7\u30fc\u30f3\u306e\u30a2\u30af\u30c6\u30a3\u30d6\u5316", "state.node_status": "\u30ce\u30fc\u30c9\u30b9\u30c6\u30fc\u30bf\u30b9\u304c\u5909\u5316\u3057\u307e\u3057\u305f", "zwave_js.value_updated.config_parameter": "\u30b3\u30f3\u30d5\u30a3\u30b0\u30d1\u30e9\u30e1\u30fc\u30bf {subtype} \u306e\u5024\u306e\u5909\u66f4", - "zwave_js.value_updated.value": "Z-Wave JS\u5024\u306e\u5024\u306e\u5909\u66f4" + "zwave_js.value_updated.value": "Z-Wave JS\u5024\u306e\u5024\u3092\u5909\u66f4" } }, "options": { From 587fd05603b7e2a7d9adacc20075638e6a7e3715 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 May 2022 14:34:32 -1000 Subject: [PATCH 1070/3516] Make logbook inherit the recorder filter (#72728) --- homeassistant/components/logbook/__init__.py | 16 +- homeassistant/components/recorder/filters.py | 46 ++++- .../components/logbook/test_websocket_api.py | 192 +++++++++++++++++- tests/components/recorder/test_filters.py | 114 +++++++++++ 4 files changed, 357 insertions(+), 11 deletions(-) create mode 100644 tests/components/recorder/test_filters.py diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index f66f1d5e920..1abfcaba6ff 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -7,7 +7,10 @@ from typing import Any import voluptuous as vol from homeassistant.components import frontend +from homeassistant.components.recorder.const import DOMAIN as RECORDER_DOMAIN from homeassistant.components.recorder.filters import ( + extract_include_exclude_filter_conf, + merge_include_exclude_filters, sqlalchemy_filter_from_include_exclude_conf, ) from homeassistant.const import ( @@ -115,9 +118,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, "logbook", "logbook", "hass:format-list-bulleted-type" ) - if conf := config.get(DOMAIN, {}): - filters = sqlalchemy_filter_from_include_exclude_conf(conf) - entities_filter = convert_include_exclude_filter(conf) + recorder_conf = config.get(RECORDER_DOMAIN, {}) + logbook_conf = config.get(DOMAIN, {}) + recorder_filter = extract_include_exclude_filter_conf(recorder_conf) + logbook_filter = extract_include_exclude_filter_conf(logbook_conf) + merged_filter = merge_include_exclude_filters(recorder_filter, logbook_filter) + + possible_merged_entities_filter = convert_include_exclude_filter(merged_filter) + if not possible_merged_entities_filter.empty_filter: + filters = sqlalchemy_filter_from_include_exclude_conf(merged_filter) + entities_filter = possible_merged_entities_filter else: filters = None entities_filter = None diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 5dd1e4b7884..0ceb013d8c5 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -25,6 +25,40 @@ GLOB_TO_SQL_CHARS = { ord("\\"): "\\\\", } +FILTER_TYPES = (CONF_EXCLUDE, CONF_INCLUDE) +FITLER_MATCHERS = (CONF_ENTITIES, CONF_DOMAINS, CONF_ENTITY_GLOBS) + + +def extract_include_exclude_filter_conf(conf: ConfigType) -> dict[str, Any]: + """Extract an include exclude filter from configuration. + + This makes a copy so we do not alter the original data. + """ + return { + filter_type: { + matcher: set(conf.get(filter_type, {}).get(matcher, [])) + for matcher in FITLER_MATCHERS + } + for filter_type in FILTER_TYPES + } + + +def merge_include_exclude_filters( + base_filter: dict[str, Any], add_filter: dict[str, Any] +) -> dict[str, Any]: + """Merge two filters. + + This makes a copy so we do not alter the original data. + """ + return { + filter_type: { + matcher: base_filter[filter_type][matcher] + | add_filter[filter_type][matcher] + for matcher in FITLER_MATCHERS + } + for filter_type in FILTER_TYPES + } + def sqlalchemy_filter_from_include_exclude_conf(conf: ConfigType) -> Filters | None: """Build a sql filter from config.""" @@ -46,13 +80,13 @@ class Filters: def __init__(self) -> None: """Initialise the include and exclude filters.""" - self.excluded_entities: list[str] = [] - self.excluded_domains: list[str] = [] - self.excluded_entity_globs: list[str] = [] + self.excluded_entities: Iterable[str] = [] + self.excluded_domains: Iterable[str] = [] + self.excluded_entity_globs: Iterable[str] = [] - self.included_entities: list[str] = [] - self.included_domains: list[str] = [] - self.included_entity_globs: list[str] = [] + self.included_entities: Iterable[str] = [] + self.included_domains: Iterable[str] = [] + self.included_entity_globs: Iterable[str] = [] @property def has_config(self) -> bool: diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 9d7146ec96c..1d35d6d897d 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -483,7 +483,7 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities( CONF_EXCLUDE: { CONF_ENTITIES: ["light.exc"], CONF_DOMAINS: ["switch"], - CONF_ENTITY_GLOBS: "*.excluded", + CONF_ENTITY_GLOBS: ["*.excluded"], } }, }, @@ -672,7 +672,7 @@ async def test_subscribe_unsubscribe_logbook_stream_included_entities( CONF_INCLUDE: { CONF_ENTITIES: ["light.inc"], CONF_DOMAINS: ["switch"], - CONF_ENTITY_GLOBS: "*.included", + CONF_ENTITY_GLOBS: ["*.included"], } }, }, @@ -849,6 +849,194 @@ async def test_subscribe_unsubscribe_logbook_stream_included_entities( assert sum(hass.bus.async_listeners().values()) == init_count +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_logbook_stream_excluded_entities_inherits_filters_from_recorder( + hass, recorder_mock, hass_ws_client +): + """Test subscribe/unsubscribe logbook stream inherts filters from recorder.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "automation", "script") + ] + ) + await async_setup_component( + hass, + logbook.DOMAIN, + { + logbook.DOMAIN: { + CONF_EXCLUDE: { + CONF_ENTITIES: ["light.additional_excluded"], + } + }, + recorder.DOMAIN: { + CONF_EXCLUDE: { + CONF_ENTITIES: ["light.exc"], + CONF_DOMAINS: ["switch"], + CONF_ENTITY_GLOBS: ["*.excluded", "*.no_matches"], + } + }, + }, + ) + await hass.async_block_till_done() + init_count = sum(hass.bus.async_listeners().values()) + + hass.states.async_set("light.exc", STATE_ON) + hass.states.async_set("light.exc", STATE_OFF) + hass.states.async_set("switch.any", STATE_ON) + hass.states.async_set("switch.any", STATE_OFF) + hass.states.async_set("cover.excluded", STATE_ON) + hass.states.async_set("cover.excluded", STATE_OFF) + hass.states.async_set("light.additional_excluded", STATE_ON) + hass.states.async_set("light.additional_excluded", STATE_OFF) + hass.states.async_set("binary_sensor.is_light", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + state: State = hass.states.get("binary_sensor.is_light") + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + {"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()} + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + { + "entity_id": "binary_sensor.is_light", + "state": "off", + "when": state.last_updated.timestamp(), + } + ] + assert msg["event"]["start_time"] == now.timestamp() + assert msg["event"]["end_time"] > msg["event"]["start_time"] + assert msg["event"]["partial"] is True + + hass.states.async_set("light.exc", STATE_ON) + hass.states.async_set("light.exc", STATE_OFF) + hass.states.async_set("switch.any", STATE_ON) + hass.states.async_set("switch.any", STATE_OFF) + hass.states.async_set("cover.excluded", STATE_ON) + hass.states.async_set("cover.excluded", STATE_OFF) + hass.states.async_set("light.additional_excluded", STATE_ON) + hass.states.async_set("light.additional_excluded", STATE_OFF) + hass.states.async_set("light.alpha", "on") + hass.states.async_set("light.alpha", "off") + alpha_off_state: State = hass.states.get("light.alpha") + hass.states.async_set("light.zulu", "on", {"color": "blue"}) + hass.states.async_set("light.zulu", "off", {"effect": "help"}) + zulu_off_state: State = hass.states.get("light.zulu") + hass.states.async_set( + "light.zulu", "on", {"effect": "help", "color": ["blue", "green"]} + ) + zulu_on_state: State = hass.states.get("light.zulu") + await hass.async_block_till_done() + + hass.states.async_remove("light.zulu") + await hass.async_block_till_done() + + hass.states.async_set("light.zulu", "on", {"effect": "help", "color": "blue"}) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [ + { + "entity_id": "light.alpha", + "state": "off", + "when": alpha_off_state.last_updated.timestamp(), + }, + { + "entity_id": "light.zulu", + "state": "off", + "when": zulu_off_state.last_updated.timestamp(), + }, + { + "entity_id": "light.zulu", + "state": "on", + "when": zulu_on_state.last_updated.timestamp(), + }, + ] + + await async_wait_recording_done(hass) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation 3", ATTR_ENTITY_ID: "cover.excluded"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + { + ATTR_NAME: "Mock automation switch matching entity", + ATTR_ENTITY_ID: "switch.match_domain", + }, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation switch matching domain", ATTR_DOMAIN: "switch"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation matches nothing"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation 3", ATTR_ENTITY_ID: "light.keep"}, + ) + hass.states.async_set("cover.excluded", STATE_ON) + hass.states.async_set("cover.excluded", STATE_OFF) + await hass.async_block_till_done() + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + { + "context_id": ANY, + "domain": "automation", + "entity_id": None, + "message": "triggered", + "name": "Mock automation matches nothing", + "source": None, + "when": ANY, + }, + { + "context_id": ANY, + "domain": "automation", + "entity_id": "light.keep", + "message": "triggered", + "name": "Mock automation 3", + "source": None, + "when": ANY, + }, + ] + + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) async def test_subscribe_unsubscribe_logbook_stream( hass, recorder_mock, hass_ws_client diff --git a/tests/components/recorder/test_filters.py b/tests/components/recorder/test_filters.py new file mode 100644 index 00000000000..fa80df6e345 --- /dev/null +++ b/tests/components/recorder/test_filters.py @@ -0,0 +1,114 @@ +"""The tests for recorder filters.""" + +from homeassistant.components.recorder.filters import ( + extract_include_exclude_filter_conf, + merge_include_exclude_filters, +) +from homeassistant.helpers.entityfilter import ( + CONF_DOMAINS, + CONF_ENTITIES, + CONF_ENTITY_GLOBS, + CONF_EXCLUDE, + CONF_INCLUDE, +) + +SIMPLE_INCLUDE_FILTER = { + CONF_INCLUDE: { + CONF_DOMAINS: ["homeassistant"], + CONF_ENTITIES: ["sensor.one"], + CONF_ENTITY_GLOBS: ["climate.*"], + } +} +SIMPLE_INCLUDE_FILTER_DIFFERENT_ENTITIES = { + CONF_INCLUDE: { + CONF_DOMAINS: ["other"], + CONF_ENTITIES: ["not_sensor.one"], + CONF_ENTITY_GLOBS: ["not_climate.*"], + } +} +SIMPLE_EXCLUDE_FILTER = { + CONF_EXCLUDE: { + CONF_DOMAINS: ["homeassistant"], + CONF_ENTITIES: ["sensor.one"], + CONF_ENTITY_GLOBS: ["climate.*"], + } +} +SIMPLE_INCLUDE_EXCLUDE_FILTER = {**SIMPLE_INCLUDE_FILTER, **SIMPLE_EXCLUDE_FILTER} + + +def test_extract_include_exclude_filter_conf(): + """Test we can extract a filter from configuration without altering it.""" + include_filter = extract_include_exclude_filter_conf(SIMPLE_INCLUDE_FILTER) + assert include_filter == { + CONF_EXCLUDE: { + CONF_DOMAINS: set(), + CONF_ENTITIES: set(), + CONF_ENTITY_GLOBS: set(), + }, + CONF_INCLUDE: { + CONF_DOMAINS: {"homeassistant"}, + CONF_ENTITIES: {"sensor.one"}, + CONF_ENTITY_GLOBS: {"climate.*"}, + }, + } + + exclude_filter = extract_include_exclude_filter_conf(SIMPLE_EXCLUDE_FILTER) + assert exclude_filter == { + CONF_INCLUDE: { + CONF_DOMAINS: set(), + CONF_ENTITIES: set(), + CONF_ENTITY_GLOBS: set(), + }, + CONF_EXCLUDE: { + CONF_DOMAINS: {"homeassistant"}, + CONF_ENTITIES: {"sensor.one"}, + CONF_ENTITY_GLOBS: {"climate.*"}, + }, + } + + include_exclude_filter = extract_include_exclude_filter_conf( + SIMPLE_INCLUDE_EXCLUDE_FILTER + ) + assert include_exclude_filter == { + CONF_INCLUDE: { + CONF_DOMAINS: {"homeassistant"}, + CONF_ENTITIES: {"sensor.one"}, + CONF_ENTITY_GLOBS: {"climate.*"}, + }, + CONF_EXCLUDE: { + CONF_DOMAINS: {"homeassistant"}, + CONF_ENTITIES: {"sensor.one"}, + CONF_ENTITY_GLOBS: {"climate.*"}, + }, + } + + include_exclude_filter[CONF_EXCLUDE][CONF_ENTITIES] = {"cover.altered"} + # verify it really is a copy + assert SIMPLE_INCLUDE_EXCLUDE_FILTER[CONF_EXCLUDE][CONF_ENTITIES] != { + "cover.altered" + } + + +def test_merge_include_exclude_filters(): + """Test we can merge two filters together.""" + include_exclude_filter_base = extract_include_exclude_filter_conf( + SIMPLE_INCLUDE_EXCLUDE_FILTER + ) + include_filter_add = extract_include_exclude_filter_conf( + SIMPLE_INCLUDE_FILTER_DIFFERENT_ENTITIES + ) + merged_filter = merge_include_exclude_filters( + include_exclude_filter_base, include_filter_add + ) + assert merged_filter == { + CONF_EXCLUDE: { + CONF_DOMAINS: {"homeassistant"}, + CONF_ENTITIES: {"sensor.one"}, + CONF_ENTITY_GLOBS: {"climate.*"}, + }, + CONF_INCLUDE: { + CONF_DOMAINS: {"other", "homeassistant"}, + CONF_ENTITIES: {"not_sensor.one", "sensor.one"}, + CONF_ENTITY_GLOBS: {"climate.*", "not_climate.*"}, + }, + } From ec44a63a84b7b7e9c38a0e62753d34a0a7a759c3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 30 May 2022 18:58:08 -0600 Subject: [PATCH 1071/3516] Bump regenmaschine to 2022.05.1 (#72735) --- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index bbe58e263b1..98dc9a6c877 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.05.0"], + "requirements": ["regenmaschine==2022.05.1"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index f66db8e37f9..e0ccf63b91a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2065,7 +2065,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.05.0 +regenmaschine==2022.05.1 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c236a33697b..c04219f93da 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1364,7 +1364,7 @@ rachiopy==1.0.3 radios==0.1.1 # homeassistant.components.rainmachine -regenmaschine==2022.05.0 +regenmaschine==2022.05.1 # homeassistant.components.renault renault-api==0.1.11 From f9bd384e6c85d80b232d7ba8fe46406655e2d442 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 May 2022 17:24:34 -1000 Subject: [PATCH 1072/3516] Stop waiting for setup retry upon discovery (#72738) --- homeassistant/config_entries.py | 52 +++++++++++++--------- tests/test_config_entries.py | 76 ++++++++++++++++++++++++++++++++- 2 files changed, 106 insertions(+), 22 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 0ac02adb8d0..49b2059b2a2 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -108,7 +108,7 @@ class ConfigEntryState(Enum): DEFAULT_DISCOVERY_UNIQUE_ID = "default_discovery_unique_id" DISCOVERY_NOTIFICATION_ID = "config_entry_discovery" -DISCOVERY_SOURCES = ( +DISCOVERY_SOURCES = { SOURCE_DHCP, SOURCE_DISCOVERY, SOURCE_HOMEKIT, @@ -119,7 +119,7 @@ DISCOVERY_SOURCES = ( SOURCE_UNIGNORE, SOURCE_USB, SOURCE_ZEROCONF, -) +} RECONFIGURE_NOTIFICATION_ID = "config_entry_reconfigure" @@ -1242,24 +1242,36 @@ class ConfigFlow(data_entry_flow.FlowHandler): return for entry in self._async_current_entries(include_ignore=True): - if entry.unique_id == self.unique_id: - if updates is not None: - changed = self.hass.config_entries.async_update_entry( - entry, data={**entry.data, **updates} - ) - if ( - changed - and reload_on_update - and entry.state - in (ConfigEntryState.LOADED, ConfigEntryState.SETUP_RETRY) - ): - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) - ) - # Allow ignored entries to be configured on manual user step - if entry.source == SOURCE_IGNORE and self.source == SOURCE_USER: - continue - raise data_entry_flow.AbortFlow("already_configured") + if entry.unique_id != self.unique_id: + continue + should_reload = False + if ( + updates is not None + and self.hass.config_entries.async_update_entry( + entry, data={**entry.data, **updates} + ) + and reload_on_update + and entry.state + in (ConfigEntryState.LOADED, ConfigEntryState.SETUP_RETRY) + ): + # Existing config entry present, and the + # entry data just changed + should_reload = True + elif ( + self.source in DISCOVERY_SOURCES + and entry.state is ConfigEntryState.SETUP_RETRY + ): + # Existing config entry present in retry state, and we + # just discovered the unique id so we know its online + should_reload = True + # Allow ignored entries to be configured on manual user step + if entry.source == SOURCE_IGNORE and self.source == SOURCE_USER: + continue + if should_reload: + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + raise data_entry_flow.AbortFlow("already_configured") async def async_set_unique_id( self, unique_id: str | None = None, *, raise_on_progress: bool = True diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 2602887d1d5..09951b4f34e 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1,4 +1,6 @@ """Test the config manager.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging @@ -7,6 +9,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant import config_entries, data_entry_flow, loader +from homeassistant.components import dhcp from homeassistant.components.hassio import HassioServiceInfo from homeassistant.const import ( EVENT_COMPONENT_LOADED, @@ -14,7 +17,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import CoreState, Event, callback -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, BaseServiceInfo +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, BaseServiceInfo, FlowResult from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryNotReady, @@ -1644,7 +1647,7 @@ async def test_unique_id_update_existing_entry_without_reload(hass, manager): async def async_step_user(self, user_input=None): """Test user step.""" await self.async_set_unique_id("mock-unique-id") - await self._abort_if_unique_id_configured( + self._abort_if_unique_id_configured( updates={"host": "1.1.1.1"}, reload_on_update=False ) @@ -1725,6 +1728,75 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager): assert len(async_reload.mock_calls) == 0 +async def test_unique_id_from_discovery_in_setup_retry(hass, manager): + """Test that we reload when in a setup retry state from discovery.""" + hass.config.components.add("comp") + unique_id = "34:ea:34:b4:3b:5a" + host = "0.0.0.0" + entry = MockConfigEntry( + domain="comp", + data={"additional": "data", "host": host}, + unique_id=unique_id, + state=config_entries.ConfigEntryState.SETUP_RETRY, + ) + entry.add_to_hass(hass) + + mock_integration( + hass, + MockModule("comp"), + ) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + VERSION = 1 + + async def async_step_dhcp( + self, discovery_info: dhcp.DhcpServiceInfo + ) -> FlowResult: + """Test dhcp step.""" + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + async def async_step_user(self, user_input: dict | None = None) -> FlowResult: + """Test user step.""" + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + # Verify we do not reload from a user source + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as async_reload: + result = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert len(async_reload.mock_calls) == 0 + + # Verify do reload from a discovery source + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as async_reload: + discovery_result = await manager.flow.async_init( + "comp", + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + hostname="any", + ip=host, + macaddress=unique_id, + ), + ) + await hass.async_block_till_done() + + assert discovery_result["type"] == RESULT_TYPE_ABORT + assert discovery_result["reason"] == "already_configured" + assert len(async_reload.mock_calls) == 1 + + async def test_unique_id_not_update_existing_entry(hass, manager): """Test that we do not update an entry if existing entry has the data.""" hass.config.components.add("comp") From 99f3ca1f08e706abbc090abf8f68a7578d60ef81 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 May 2022 20:41:05 -0700 Subject: [PATCH 1073/3516] Add support for announce to play_media (#72566) --- .../components/media_player/__init__.py | 4 +++- .../components/media_player/const.py | 1 + .../components/media_player/services.yaml | 7 ++++++ homeassistant/components/tts/__init__.py | 2 ++ tests/components/media_player/test_init.py | 23 +++++++++++++++++++ tests/components/tts/test_init.py | 2 ++ 6 files changed, 38 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index f71f3fc2a1f..dc2f3624a0e 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -76,6 +76,7 @@ from .const import ( # noqa: F401 ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_ALBUM_ARTIST, ATTR_MEDIA_ALBUM_NAME, + ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_ARTIST, ATTR_MEDIA_CHANNEL, ATTR_MEDIA_CONTENT_ID, @@ -182,9 +183,10 @@ DEVICE_CLASS_RECEIVER = MediaPlayerDeviceClass.RECEIVER.value MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = { vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string, vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, - vol.Optional(ATTR_MEDIA_ENQUEUE): vol.Any( + vol.Exclusive(ATTR_MEDIA_ENQUEUE, "enqueue_announce"): vol.Any( cv.boolean, vol.Coerce(MediaPlayerEnqueue) ), + vol.Exclusive(ATTR_MEDIA_ANNOUNCE, "enqueue_announce"): cv.boolean, vol.Optional(ATTR_MEDIA_EXTRA, default={}): dict, } diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index b12f0c4ae01..4d534467ad6 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -10,6 +10,7 @@ ATTR_ENTITY_PICTURE_LOCAL = "entity_picture_local" ATTR_GROUP_MEMBERS = "group_members" ATTR_INPUT_SOURCE = "source" ATTR_INPUT_SOURCE_LIST = "source_list" +ATTR_MEDIA_ANNOUNCE = "announce" ATTR_MEDIA_ALBUM_ARTIST = "media_album_artist" ATTR_MEDIA_ALBUM_NAME = "media_album_name" ATTR_MEDIA_ARTIST = "media_artist" diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index b2a8ac40262..b698b87aec6 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -166,6 +166,13 @@ play_media: value: "add" - label: "Play now and clear queue" value: "replace" + announce: + name: Announce + description: If the media should be played as an announcement. + required: false + example: "true" + selector: + boolean: select_source: name: Select source diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 4628ec8768a..706122c174c 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -21,6 +21,7 @@ import yarl from homeassistant.components.http import HomeAssistantView from homeassistant.components.media_player.const import ( + ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, DOMAIN as DOMAIN_MP, @@ -224,6 +225,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: str(yarl.URL.build(path=p_type, query=params)), ), ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_ANNOUNCE: True, }, blocking=True, context=service.context, diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index cb095cbcfe0..eceb7e9ec4f 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -5,6 +5,7 @@ from http import HTTPStatus from unittest.mock import patch import pytest +import voluptuous as vol from homeassistant.components import media_player from homeassistant.components.media_player.browse_media import BrowseMedia @@ -291,3 +292,25 @@ async def test_enqueue_rewrite(hass, input, expected): assert len(mock_play_media.mock_calls) == 1 assert mock_play_media.mock_calls[0][2]["enqueue"] == expected + + +async def test_enqueue_alert_exclusive(hass): + """Test that alert and enqueue cannot be used together.""" + await async_setup_component( + hass, "media_player", {"media_player": {"platform": "demo"}} + ) + await hass.async_block_till_done() + + with pytest.raises(vol.Invalid): + await hass.services.async_call( + "media_player", + "play_media", + { + "entity_id": "media_player.bedroom", + "media_content_type": "music", + "media_content_id": "1234", + "enqueue": "play", + "announce": True, + }, + blocking=True, + ) diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 78fa49a8fc9..7b348489059 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.components import media_source, tts from homeassistant.components.demo.tts import DemoProvider from homeassistant.components.media_player.const import ( + ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, DOMAIN as DOMAIN_MP, @@ -91,6 +92,7 @@ async def test_setup_component_and_test_service(hass, empty_cache_dir): ) assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_ANNOUNCE] is True assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert ( await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) From 17abbd7f51ac89641e4dee14347ab519f6d68375 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 30 May 2022 22:56:59 -0500 Subject: [PATCH 1074/3516] Bump plexapi to 4.11.2 (#72729) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 2e2db01de77..912732efe98 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.11.1", + "plexapi==4.11.2", "plexauth==0.0.6", "plexwebsocket==0.0.13" ], diff --git a/requirements_all.txt b/requirements_all.txt index e0ccf63b91a..dc3efa7f441 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1236,7 +1236,7 @@ pillow==9.1.1 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.11.1 +plexapi==4.11.2 # homeassistant.components.plex plexauth==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c04219f93da..4816a800f87 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -838,7 +838,7 @@ pilight==0.1.1 pillow==9.1.1 # homeassistant.components.plex -plexapi==4.11.1 +plexapi==4.11.2 # homeassistant.components.plex plexauth==0.0.6 From 969b7bd448a4645cedc411ca28cd9c58c38beb8a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 30 May 2022 23:04:53 -0600 Subject: [PATCH 1075/3516] Bump simplisafe-python to 2022.05.2 (#72740) --- 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 cb1b02e37ae..f62da735f92 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==2022.05.1"], + "requirements": ["simplisafe-python==2022.05.2"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index dc3efa7f441..90ea8e4f432 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2168,7 +2168,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.05.1 +simplisafe-python==2022.05.2 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4816a800f87..df2d2fa73b9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1425,7 +1425,7 @@ sharkiq==0.0.1 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2022.05.1 +simplisafe-python==2022.05.2 # homeassistant.components.slack slackclient==2.5.0 From 5fdc6943259d19586cab54251bfd7fa6afcac1d1 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 31 May 2022 01:05:09 -0400 Subject: [PATCH 1076/3516] Bump zwave-js-server-python to 0.37.1 (#72731) Co-authored-by: Paulus Schoutsen --- homeassistant/components/zwave_js/api.py | 6 +++--- homeassistant/components/zwave_js/climate.py | 2 +- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index e922571f4b1..f9f1ad40f9a 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -1232,7 +1232,7 @@ async def websocket_replace_failed_node( try: result = await controller.async_replace_failed_node( - node_id, + controller.nodes[node_id], INCLUSION_STRATEGY_NOT_SMART_START[inclusion_strategy.value], force_security=force_security, provisioning=provisioning, @@ -1291,7 +1291,7 @@ async def websocket_remove_failed_node( connection.subscriptions[msg["id"]] = async_cleanup msg[DATA_UNSUBSCRIBE] = unsubs = [controller.on("node removed", node_removed)] - await controller.async_remove_failed_node(node.node_id) + await controller.async_remove_failed_node(node) connection.send_result(msg[ID]) @@ -1414,7 +1414,7 @@ async def websocket_heal_node( assert driver is not None # The node comes from the driver instance. controller = driver.controller - result = await controller.async_heal_node(node.node_id) + result = await controller.async_heal_node(node) connection.send_result( msg[ID], result, diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 7b07eb09619..d8037643488 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -66,7 +66,7 @@ ZW_HVAC_MODE_MAP: dict[int, HVACMode] = { ThermostatMode.AUTO: HVACMode.HEAT_COOL, ThermostatMode.AUXILIARY: HVACMode.HEAT, ThermostatMode.FAN: HVACMode.FAN_ONLY, - ThermostatMode.FURNANCE: HVACMode.HEAT, + ThermostatMode.FURNACE: HVACMode.HEAT, ThermostatMode.DRY: HVACMode.DRY, ThermostatMode.AUTO_CHANGE_OVER: HVACMode.HEAT_COOL, ThermostatMode.HEATING_ECON: HVACMode.HEAT, diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 555da5fe954..1c7eabb4e86 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.37.0"], + "requirements": ["zwave-js-server-python==0.37.1"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 90ea8e4f432..871980bffae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2528,7 +2528,7 @@ zigpy==0.45.1 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.37.0 +zwave-js-server-python==0.37.1 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df2d2fa73b9..ba8a7cefd9c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1665,7 +1665,7 @@ zigpy-znp==0.7.0 zigpy==0.45.1 # homeassistant.components.zwave_js -zwave-js-server-python==0.37.0 +zwave-js-server-python==0.37.1 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 From b3682a5c81de837bba8f3c9f20877054831c2561 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 May 2022 19:51:35 -1000 Subject: [PATCH 1077/3516] Revert bond reload on setup_retry discovery (#72744) --- homeassistant/components/bond/config_flow.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 9670782d2a6..09386c3587d 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -114,7 +114,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): changed = new_data != dict(entry.data) if changed: hass.config_entries.async_update_entry(entry, data=new_data) - if changed or entry.state is ConfigEntryState.SETUP_RETRY: entry_id = entry.entry_id hass.async_create_task(hass.config_entries.async_reload(entry_id)) raise AbortFlow("already_configured") From 4ae3929a006d0bb309f6e5d4cf85f94605687b08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 May 2022 19:52:05 -1000 Subject: [PATCH 1078/3516] Revert wiz reload on setup_retry discovery (#72743) --- homeassistant/components/wiz/config_flow.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/wiz/config_flow.py b/homeassistant/components/wiz/config_flow.py index 6b5f5be027f..b2173ccda97 100644 --- a/homeassistant/components/wiz/config_flow.py +++ b/homeassistant/components/wiz/config_flow.py @@ -10,7 +10,7 @@ from pywizlight.exceptions import WizLightConnectionError, WizLightTimeOutError import voluptuous as vol from homeassistant.components import dhcp -from homeassistant.config_entries import ConfigEntryState, ConfigFlow +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.util.network import is_ip_address @@ -58,15 +58,7 @@ class WizConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.debug("Discovered device: %s", device) ip_address = device.ip_address mac = device.mac_address - if current_entry := await self.async_set_unique_id(mac): - if ( - current_entry.state is ConfigEntryState.SETUP_RETRY - and current_entry.data[CONF_HOST] == ip_address - ): - self.hass.async_create_task( - self.hass.config_entries.async_reload(current_entry.entry_id) - ) - return self.async_abort(reason="already_configured") + await self.async_set_unique_id(mac) self._abort_if_unique_id_configured(updates={CONF_HOST: ip_address}) await self._async_connect_discovered_or_abort() return await self.async_step_discovery_confirm() From 77e4c86c079c65327c0761e8d703cc9d91fa146f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 May 2022 20:41:05 -0700 Subject: [PATCH 1079/3516] Add support for announce to play_media (#72566) --- .../components/media_player/__init__.py | 4 +++- .../components/media_player/const.py | 1 + .../components/media_player/services.yaml | 7 ++++++ homeassistant/components/tts/__init__.py | 2 ++ tests/components/media_player/test_init.py | 23 +++++++++++++++++++ tests/components/tts/test_init.py | 2 ++ 6 files changed, 38 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index f71f3fc2a1f..dc2f3624a0e 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -76,6 +76,7 @@ from .const import ( # noqa: F401 ATTR_INPUT_SOURCE_LIST, ATTR_MEDIA_ALBUM_ARTIST, ATTR_MEDIA_ALBUM_NAME, + ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_ARTIST, ATTR_MEDIA_CHANNEL, ATTR_MEDIA_CONTENT_ID, @@ -182,9 +183,10 @@ DEVICE_CLASS_RECEIVER = MediaPlayerDeviceClass.RECEIVER.value MEDIA_PLAYER_PLAY_MEDIA_SCHEMA = { vol.Required(ATTR_MEDIA_CONTENT_TYPE): cv.string, vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, - vol.Optional(ATTR_MEDIA_ENQUEUE): vol.Any( + vol.Exclusive(ATTR_MEDIA_ENQUEUE, "enqueue_announce"): vol.Any( cv.boolean, vol.Coerce(MediaPlayerEnqueue) ), + vol.Exclusive(ATTR_MEDIA_ANNOUNCE, "enqueue_announce"): cv.boolean, vol.Optional(ATTR_MEDIA_EXTRA, default={}): dict, } diff --git a/homeassistant/components/media_player/const.py b/homeassistant/components/media_player/const.py index b12f0c4ae01..4d534467ad6 100644 --- a/homeassistant/components/media_player/const.py +++ b/homeassistant/components/media_player/const.py @@ -10,6 +10,7 @@ ATTR_ENTITY_PICTURE_LOCAL = "entity_picture_local" ATTR_GROUP_MEMBERS = "group_members" ATTR_INPUT_SOURCE = "source" ATTR_INPUT_SOURCE_LIST = "source_list" +ATTR_MEDIA_ANNOUNCE = "announce" ATTR_MEDIA_ALBUM_ARTIST = "media_album_artist" ATTR_MEDIA_ALBUM_NAME = "media_album_name" ATTR_MEDIA_ARTIST = "media_artist" diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index b2a8ac40262..b698b87aec6 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -166,6 +166,13 @@ play_media: value: "add" - label: "Play now and clear queue" value: "replace" + announce: + name: Announce + description: If the media should be played as an announcement. + required: false + example: "true" + selector: + boolean: select_source: name: Select source diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 4628ec8768a..706122c174c 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -21,6 +21,7 @@ import yarl from homeassistant.components.http import HomeAssistantView from homeassistant.components.media_player.const import ( + ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, DOMAIN as DOMAIN_MP, @@ -224,6 +225,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: str(yarl.URL.build(path=p_type, query=params)), ), ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_ANNOUNCE: True, }, blocking=True, context=service.context, diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index cb095cbcfe0..eceb7e9ec4f 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -5,6 +5,7 @@ from http import HTTPStatus from unittest.mock import patch import pytest +import voluptuous as vol from homeassistant.components import media_player from homeassistant.components.media_player.browse_media import BrowseMedia @@ -291,3 +292,25 @@ async def test_enqueue_rewrite(hass, input, expected): assert len(mock_play_media.mock_calls) == 1 assert mock_play_media.mock_calls[0][2]["enqueue"] == expected + + +async def test_enqueue_alert_exclusive(hass): + """Test that alert and enqueue cannot be used together.""" + await async_setup_component( + hass, "media_player", {"media_player": {"platform": "demo"}} + ) + await hass.async_block_till_done() + + with pytest.raises(vol.Invalid): + await hass.services.async_call( + "media_player", + "play_media", + { + "entity_id": "media_player.bedroom", + "media_content_type": "music", + "media_content_id": "1234", + "enqueue": "play", + "announce": True, + }, + blocking=True, + ) diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 78fa49a8fc9..7b348489059 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.components import media_source, tts from homeassistant.components.demo.tts import DemoProvider from homeassistant.components.media_player.const import ( + ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, DOMAIN as DOMAIN_MP, @@ -91,6 +92,7 @@ async def test_setup_component_and_test_service(hass, empty_cache_dir): ) assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_ANNOUNCE] is True assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert ( await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) From a202ffe4c10b8092489b584396f142d78cdd757b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 May 2022 14:34:32 -1000 Subject: [PATCH 1080/3516] Make logbook inherit the recorder filter (#72728) --- homeassistant/components/logbook/__init__.py | 16 +- homeassistant/components/recorder/filters.py | 46 ++++- .../components/logbook/test_websocket_api.py | 192 +++++++++++++++++- tests/components/recorder/test_filters.py | 114 +++++++++++ 4 files changed, 357 insertions(+), 11 deletions(-) create mode 100644 tests/components/recorder/test_filters.py diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index f66f1d5e920..1abfcaba6ff 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -7,7 +7,10 @@ from typing import Any import voluptuous as vol from homeassistant.components import frontend +from homeassistant.components.recorder.const import DOMAIN as RECORDER_DOMAIN from homeassistant.components.recorder.filters import ( + extract_include_exclude_filter_conf, + merge_include_exclude_filters, sqlalchemy_filter_from_include_exclude_conf, ) from homeassistant.const import ( @@ -115,9 +118,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, "logbook", "logbook", "hass:format-list-bulleted-type" ) - if conf := config.get(DOMAIN, {}): - filters = sqlalchemy_filter_from_include_exclude_conf(conf) - entities_filter = convert_include_exclude_filter(conf) + recorder_conf = config.get(RECORDER_DOMAIN, {}) + logbook_conf = config.get(DOMAIN, {}) + recorder_filter = extract_include_exclude_filter_conf(recorder_conf) + logbook_filter = extract_include_exclude_filter_conf(logbook_conf) + merged_filter = merge_include_exclude_filters(recorder_filter, logbook_filter) + + possible_merged_entities_filter = convert_include_exclude_filter(merged_filter) + if not possible_merged_entities_filter.empty_filter: + filters = sqlalchemy_filter_from_include_exclude_conf(merged_filter) + entities_filter = possible_merged_entities_filter else: filters = None entities_filter = None diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 5dd1e4b7884..0ceb013d8c5 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -25,6 +25,40 @@ GLOB_TO_SQL_CHARS = { ord("\\"): "\\\\", } +FILTER_TYPES = (CONF_EXCLUDE, CONF_INCLUDE) +FITLER_MATCHERS = (CONF_ENTITIES, CONF_DOMAINS, CONF_ENTITY_GLOBS) + + +def extract_include_exclude_filter_conf(conf: ConfigType) -> dict[str, Any]: + """Extract an include exclude filter from configuration. + + This makes a copy so we do not alter the original data. + """ + return { + filter_type: { + matcher: set(conf.get(filter_type, {}).get(matcher, [])) + for matcher in FITLER_MATCHERS + } + for filter_type in FILTER_TYPES + } + + +def merge_include_exclude_filters( + base_filter: dict[str, Any], add_filter: dict[str, Any] +) -> dict[str, Any]: + """Merge two filters. + + This makes a copy so we do not alter the original data. + """ + return { + filter_type: { + matcher: base_filter[filter_type][matcher] + | add_filter[filter_type][matcher] + for matcher in FITLER_MATCHERS + } + for filter_type in FILTER_TYPES + } + def sqlalchemy_filter_from_include_exclude_conf(conf: ConfigType) -> Filters | None: """Build a sql filter from config.""" @@ -46,13 +80,13 @@ class Filters: def __init__(self) -> None: """Initialise the include and exclude filters.""" - self.excluded_entities: list[str] = [] - self.excluded_domains: list[str] = [] - self.excluded_entity_globs: list[str] = [] + self.excluded_entities: Iterable[str] = [] + self.excluded_domains: Iterable[str] = [] + self.excluded_entity_globs: Iterable[str] = [] - self.included_entities: list[str] = [] - self.included_domains: list[str] = [] - self.included_entity_globs: list[str] = [] + self.included_entities: Iterable[str] = [] + self.included_domains: Iterable[str] = [] + self.included_entity_globs: Iterable[str] = [] @property def has_config(self) -> bool: diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 9d7146ec96c..1d35d6d897d 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -483,7 +483,7 @@ async def test_subscribe_unsubscribe_logbook_stream_excluded_entities( CONF_EXCLUDE: { CONF_ENTITIES: ["light.exc"], CONF_DOMAINS: ["switch"], - CONF_ENTITY_GLOBS: "*.excluded", + CONF_ENTITY_GLOBS: ["*.excluded"], } }, }, @@ -672,7 +672,7 @@ async def test_subscribe_unsubscribe_logbook_stream_included_entities( CONF_INCLUDE: { CONF_ENTITIES: ["light.inc"], CONF_DOMAINS: ["switch"], - CONF_ENTITY_GLOBS: "*.included", + CONF_ENTITY_GLOBS: ["*.included"], } }, }, @@ -849,6 +849,194 @@ async def test_subscribe_unsubscribe_logbook_stream_included_entities( assert sum(hass.bus.async_listeners().values()) == init_count +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_logbook_stream_excluded_entities_inherits_filters_from_recorder( + hass, recorder_mock, hass_ws_client +): + """Test subscribe/unsubscribe logbook stream inherts filters from recorder.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "automation", "script") + ] + ) + await async_setup_component( + hass, + logbook.DOMAIN, + { + logbook.DOMAIN: { + CONF_EXCLUDE: { + CONF_ENTITIES: ["light.additional_excluded"], + } + }, + recorder.DOMAIN: { + CONF_EXCLUDE: { + CONF_ENTITIES: ["light.exc"], + CONF_DOMAINS: ["switch"], + CONF_ENTITY_GLOBS: ["*.excluded", "*.no_matches"], + } + }, + }, + ) + await hass.async_block_till_done() + init_count = sum(hass.bus.async_listeners().values()) + + hass.states.async_set("light.exc", STATE_ON) + hass.states.async_set("light.exc", STATE_OFF) + hass.states.async_set("switch.any", STATE_ON) + hass.states.async_set("switch.any", STATE_OFF) + hass.states.async_set("cover.excluded", STATE_ON) + hass.states.async_set("cover.excluded", STATE_OFF) + hass.states.async_set("light.additional_excluded", STATE_ON) + hass.states.async_set("light.additional_excluded", STATE_OFF) + hass.states.async_set("binary_sensor.is_light", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + state: State = hass.states.get("binary_sensor.is_light") + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + {"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()} + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + { + "entity_id": "binary_sensor.is_light", + "state": "off", + "when": state.last_updated.timestamp(), + } + ] + assert msg["event"]["start_time"] == now.timestamp() + assert msg["event"]["end_time"] > msg["event"]["start_time"] + assert msg["event"]["partial"] is True + + hass.states.async_set("light.exc", STATE_ON) + hass.states.async_set("light.exc", STATE_OFF) + hass.states.async_set("switch.any", STATE_ON) + hass.states.async_set("switch.any", STATE_OFF) + hass.states.async_set("cover.excluded", STATE_ON) + hass.states.async_set("cover.excluded", STATE_OFF) + hass.states.async_set("light.additional_excluded", STATE_ON) + hass.states.async_set("light.additional_excluded", STATE_OFF) + hass.states.async_set("light.alpha", "on") + hass.states.async_set("light.alpha", "off") + alpha_off_state: State = hass.states.get("light.alpha") + hass.states.async_set("light.zulu", "on", {"color": "blue"}) + hass.states.async_set("light.zulu", "off", {"effect": "help"}) + zulu_off_state: State = hass.states.get("light.zulu") + hass.states.async_set( + "light.zulu", "on", {"effect": "help", "color": ["blue", "green"]} + ) + zulu_on_state: State = hass.states.get("light.zulu") + await hass.async_block_till_done() + + hass.states.async_remove("light.zulu") + await hass.async_block_till_done() + + hass.states.async_set("light.zulu", "on", {"effect": "help", "color": "blue"}) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [ + { + "entity_id": "light.alpha", + "state": "off", + "when": alpha_off_state.last_updated.timestamp(), + }, + { + "entity_id": "light.zulu", + "state": "off", + "when": zulu_off_state.last_updated.timestamp(), + }, + { + "entity_id": "light.zulu", + "state": "on", + "when": zulu_on_state.last_updated.timestamp(), + }, + ] + + await async_wait_recording_done(hass) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation 3", ATTR_ENTITY_ID: "cover.excluded"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + { + ATTR_NAME: "Mock automation switch matching entity", + ATTR_ENTITY_ID: "switch.match_domain", + }, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation switch matching domain", ATTR_DOMAIN: "switch"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation matches nothing"}, + ) + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation 3", ATTR_ENTITY_ID: "light.keep"}, + ) + hass.states.async_set("cover.excluded", STATE_ON) + hass.states.async_set("cover.excluded", STATE_OFF) + await hass.async_block_till_done() + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + { + "context_id": ANY, + "domain": "automation", + "entity_id": None, + "message": "triggered", + "name": "Mock automation matches nothing", + "source": None, + "when": ANY, + }, + { + "context_id": ANY, + "domain": "automation", + "entity_id": "light.keep", + "message": "triggered", + "name": "Mock automation 3", + "source": None, + "when": ANY, + }, + ] + + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) async def test_subscribe_unsubscribe_logbook_stream( hass, recorder_mock, hass_ws_client diff --git a/tests/components/recorder/test_filters.py b/tests/components/recorder/test_filters.py new file mode 100644 index 00000000000..fa80df6e345 --- /dev/null +++ b/tests/components/recorder/test_filters.py @@ -0,0 +1,114 @@ +"""The tests for recorder filters.""" + +from homeassistant.components.recorder.filters import ( + extract_include_exclude_filter_conf, + merge_include_exclude_filters, +) +from homeassistant.helpers.entityfilter import ( + CONF_DOMAINS, + CONF_ENTITIES, + CONF_ENTITY_GLOBS, + CONF_EXCLUDE, + CONF_INCLUDE, +) + +SIMPLE_INCLUDE_FILTER = { + CONF_INCLUDE: { + CONF_DOMAINS: ["homeassistant"], + CONF_ENTITIES: ["sensor.one"], + CONF_ENTITY_GLOBS: ["climate.*"], + } +} +SIMPLE_INCLUDE_FILTER_DIFFERENT_ENTITIES = { + CONF_INCLUDE: { + CONF_DOMAINS: ["other"], + CONF_ENTITIES: ["not_sensor.one"], + CONF_ENTITY_GLOBS: ["not_climate.*"], + } +} +SIMPLE_EXCLUDE_FILTER = { + CONF_EXCLUDE: { + CONF_DOMAINS: ["homeassistant"], + CONF_ENTITIES: ["sensor.one"], + CONF_ENTITY_GLOBS: ["climate.*"], + } +} +SIMPLE_INCLUDE_EXCLUDE_FILTER = {**SIMPLE_INCLUDE_FILTER, **SIMPLE_EXCLUDE_FILTER} + + +def test_extract_include_exclude_filter_conf(): + """Test we can extract a filter from configuration without altering it.""" + include_filter = extract_include_exclude_filter_conf(SIMPLE_INCLUDE_FILTER) + assert include_filter == { + CONF_EXCLUDE: { + CONF_DOMAINS: set(), + CONF_ENTITIES: set(), + CONF_ENTITY_GLOBS: set(), + }, + CONF_INCLUDE: { + CONF_DOMAINS: {"homeassistant"}, + CONF_ENTITIES: {"sensor.one"}, + CONF_ENTITY_GLOBS: {"climate.*"}, + }, + } + + exclude_filter = extract_include_exclude_filter_conf(SIMPLE_EXCLUDE_FILTER) + assert exclude_filter == { + CONF_INCLUDE: { + CONF_DOMAINS: set(), + CONF_ENTITIES: set(), + CONF_ENTITY_GLOBS: set(), + }, + CONF_EXCLUDE: { + CONF_DOMAINS: {"homeassistant"}, + CONF_ENTITIES: {"sensor.one"}, + CONF_ENTITY_GLOBS: {"climate.*"}, + }, + } + + include_exclude_filter = extract_include_exclude_filter_conf( + SIMPLE_INCLUDE_EXCLUDE_FILTER + ) + assert include_exclude_filter == { + CONF_INCLUDE: { + CONF_DOMAINS: {"homeassistant"}, + CONF_ENTITIES: {"sensor.one"}, + CONF_ENTITY_GLOBS: {"climate.*"}, + }, + CONF_EXCLUDE: { + CONF_DOMAINS: {"homeassistant"}, + CONF_ENTITIES: {"sensor.one"}, + CONF_ENTITY_GLOBS: {"climate.*"}, + }, + } + + include_exclude_filter[CONF_EXCLUDE][CONF_ENTITIES] = {"cover.altered"} + # verify it really is a copy + assert SIMPLE_INCLUDE_EXCLUDE_FILTER[CONF_EXCLUDE][CONF_ENTITIES] != { + "cover.altered" + } + + +def test_merge_include_exclude_filters(): + """Test we can merge two filters together.""" + include_exclude_filter_base = extract_include_exclude_filter_conf( + SIMPLE_INCLUDE_EXCLUDE_FILTER + ) + include_filter_add = extract_include_exclude_filter_conf( + SIMPLE_INCLUDE_FILTER_DIFFERENT_ENTITIES + ) + merged_filter = merge_include_exclude_filters( + include_exclude_filter_base, include_filter_add + ) + assert merged_filter == { + CONF_EXCLUDE: { + CONF_DOMAINS: {"homeassistant"}, + CONF_ENTITIES: {"sensor.one"}, + CONF_ENTITY_GLOBS: {"climate.*"}, + }, + CONF_INCLUDE: { + CONF_DOMAINS: {"other", "homeassistant"}, + CONF_ENTITIES: {"not_sensor.one", "sensor.one"}, + CONF_ENTITY_GLOBS: {"climate.*", "not_climate.*"}, + }, + } From a98528c93f52e717b324bb658d379e61cd0ca5a2 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 30 May 2022 22:56:59 -0500 Subject: [PATCH 1081/3516] Bump plexapi to 4.11.2 (#72729) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 2e2db01de77..912732efe98 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.11.1", + "plexapi==4.11.2", "plexauth==0.0.6", "plexwebsocket==0.0.13" ], diff --git a/requirements_all.txt b/requirements_all.txt index e15aff574d5..544c16a58dd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1236,7 +1236,7 @@ pillow==9.1.1 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.11.1 +plexapi==4.11.2 # homeassistant.components.plex plexauth==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 773091643a4..58be8ff0459 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -838,7 +838,7 @@ pilight==0.1.1 pillow==9.1.1 # homeassistant.components.plex -plexapi==4.11.1 +plexapi==4.11.2 # homeassistant.components.plex plexauth==0.0.6 From b842c76fbd59ed212b6ea85e18d569089c436486 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 31 May 2022 01:05:09 -0400 Subject: [PATCH 1082/3516] Bump zwave-js-server-python to 0.37.1 (#72731) Co-authored-by: Paulus Schoutsen --- homeassistant/components/zwave_js/api.py | 11 ++++------- homeassistant/components/zwave_js/climate.py | 2 +- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 5d81dc46803..5b8b95e951c 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -1231,7 +1231,7 @@ async def websocket_replace_failed_node( try: result = await controller.async_replace_failed_node( - node_id, + controller.nodes[node_id], INCLUSION_STRATEGY_NOT_SMART_START[inclusion_strategy.value], force_security=force_security, provisioning=provisioning, @@ -1290,11 +1290,8 @@ async def websocket_remove_failed_node( connection.subscriptions[msg["id"]] = async_cleanup msg[DATA_UNSUBSCRIBE] = unsubs = [controller.on("node removed", node_removed)] - result = await controller.async_remove_failed_node(node.node_id) - connection.send_result( - msg[ID], - result, - ) + await controller.async_remove_failed_node(node) + connection.send_result(msg[ID]) @websocket_api.require_admin @@ -1416,7 +1413,7 @@ async def websocket_heal_node( assert driver is not None # The node comes from the driver instance. controller = driver.controller - result = await controller.async_heal_node(node.node_id) + result = await controller.async_heal_node(node) connection.send_result( msg[ID], result, diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 7b07eb09619..d8037643488 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -66,7 +66,7 @@ ZW_HVAC_MODE_MAP: dict[int, HVACMode] = { ThermostatMode.AUTO: HVACMode.HEAT_COOL, ThermostatMode.AUXILIARY: HVACMode.HEAT, ThermostatMode.FAN: HVACMode.FAN_ONLY, - ThermostatMode.FURNANCE: HVACMode.HEAT, + ThermostatMode.FURNACE: HVACMode.HEAT, ThermostatMode.DRY: HVACMode.DRY, ThermostatMode.AUTO_CHANGE_OVER: HVACMode.HEAT_COOL, ThermostatMode.HEATING_ECON: HVACMode.HEAT, diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 555da5fe954..1c7eabb4e86 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.37.0"], + "requirements": ["zwave-js-server-python==0.37.1"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 544c16a58dd..dedebd00dc8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2528,7 +2528,7 @@ zigpy==0.45.1 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.37.0 +zwave-js-server-python==0.37.1 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 58be8ff0459..adbad6fa960 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1665,7 +1665,7 @@ zigpy-znp==0.7.0 zigpy==0.45.1 # homeassistant.components.zwave_js -zwave-js-server-python==0.37.0 +zwave-js-server-python==0.37.1 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 From 15bdfb2a45a849f243282bc7b6f3b27812552070 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 30 May 2022 17:48:42 -0600 Subject: [PATCH 1083/3516] Fix invalid RainMachine syntax (#72732) --- homeassistant/components/rainmachine/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index a220aafa2a5..8d339682305 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -393,7 +393,7 @@ class RainMachineZone(RainMachineActivitySwitch): ATTR_CURRENT_CYCLE: data["cycle"], ATTR_ID: data["uid"], ATTR_NO_CYCLES: data["noOfCycles"], - ATTR_RESTRICTIONS: data("restriction"), + ATTR_RESTRICTIONS: data["restriction"], ATTR_SLOPE: SLOPE_TYPE_MAP.get(data["slope"], 99), ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(data["soil"], 99), ATTR_SPRINKLER_TYPE: SPRINKLER_TYPE_MAP.get(data["group_id"], 99), From a4e2d31a199a6e5dd7e4fd01d3227911098c648c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 30 May 2022 18:58:08 -0600 Subject: [PATCH 1084/3516] Bump regenmaschine to 2022.05.1 (#72735) --- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index bbe58e263b1..98dc9a6c877 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.05.0"], + "requirements": ["regenmaschine==2022.05.1"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index dedebd00dc8..464bf2997b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2065,7 +2065,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.05.0 +regenmaschine==2022.05.1 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index adbad6fa960..0686886aa9f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1364,7 +1364,7 @@ rachiopy==1.0.3 radios==0.1.1 # homeassistant.components.rainmachine -regenmaschine==2022.05.0 +regenmaschine==2022.05.1 # homeassistant.components.renault renault-api==0.1.11 From 48d36e49f0f61608d0d3c379d146620f65fed32b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 30 May 2022 23:04:53 -0600 Subject: [PATCH 1085/3516] Bump simplisafe-python to 2022.05.2 (#72740) --- 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 cb1b02e37ae..f62da735f92 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==2022.05.1"], + "requirements": ["simplisafe-python==2022.05.2"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 464bf2997b7..3203dd79f6a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2168,7 +2168,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.05.1 +simplisafe-python==2022.05.2 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0686886aa9f..171ae908991 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1425,7 +1425,7 @@ sharkiq==0.0.1 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2022.05.1 +simplisafe-python==2022.05.2 # homeassistant.components.slack slackclient==2.5.0 From 103f324c52199435eaef382a4c4943c458f6beb5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 May 2022 22:57:22 -0700 Subject: [PATCH 1086/3516] Bumped version to 2022.6.0b5 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index fe454dbc8a1..bd49bb0e1e2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 19aefbb6f90..3d52bda7738 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.0b4 +version = 2022.6.0b5 url = https://www.home-assistant.io/ [options] From 635d7085cf42dfaf8e60d1e262f096827d56e6e1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 31 May 2022 09:32:44 +0200 Subject: [PATCH 1087/3516] Move MQTT config schemas and client to separate modules (#71995) * Move MQTT config schemas and client to separate modules * Update integrations depending on MQTT --- .../manual_mqtt/alarm_control_panel.py | 2 +- homeassistant/components/mqtt/__init__.py | 958 +----------------- .../components/mqtt/alarm_control_panel.py | 14 +- .../components/mqtt/binary_sensor.py | 7 +- homeassistant/components/mqtt/button.py | 11 +- homeassistant/components/mqtt/camera.py | 7 +- homeassistant/components/mqtt/client.py | 659 ++++++++++++ homeassistant/components/mqtt/climate.py | 58 +- homeassistant/components/mqtt/config.py | 148 +++ homeassistant/components/mqtt/config_flow.py | 2 +- homeassistant/components/mqtt/const.py | 26 +- homeassistant/components/mqtt/cover.py | 20 +- .../components/mqtt/device_automation.py | 4 +- .../mqtt/device_tracker/schema_discovery.py | 7 +- .../mqtt/device_tracker/schema_yaml.py | 10 +- .../components/mqtt/device_trigger.py | 6 +- homeassistant/components/mqtt/fan.py | 24 +- homeassistant/components/mqtt/humidifier.py | 18 +- .../components/mqtt/light/schema_basic.py | 48 +- .../components/mqtt/light/schema_json.py | 11 +- .../components/mqtt/light/schema_template.py | 7 +- homeassistant/components/mqtt/lock.py | 7 +- homeassistant/components/mqtt/mixins.py | 14 +- homeassistant/components/mqtt/models.py | 126 ++- homeassistant/components/mqtt/number.py | 7 +- homeassistant/components/mqtt/scene.py | 10 +- homeassistant/components/mqtt/select.py | 7 +- homeassistant/components/mqtt/sensor.py | 10 +- homeassistant/components/mqtt/siren.py | 7 +- homeassistant/components/mqtt/switch.py | 7 +- homeassistant/components/mqtt/tag.py | 8 +- .../components/mqtt/vacuum/schema_legacy.py | 28 +- .../components/mqtt/vacuum/schema_state.py | 15 +- .../components/mqtt_json/device_tracker.py | 2 +- homeassistant/components/mqtt_room/sensor.py | 2 +- tests/components/mqtt/test_cover.py | 2 +- tests/components/mqtt/test_init.py | 18 +- tests/components/mqtt/test_legacy_vacuum.py | 2 +- tests/components/mqtt/test_state_vacuum.py | 2 +- 39 files changed, 1213 insertions(+), 1108 deletions(-) create mode 100644 homeassistant/components/mqtt/client.py create mode 100644 homeassistant/components/mqtt/config.py diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index 5b74af49a91..67675a44e22 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -110,7 +110,7 @@ def _state_schema(state): PLATFORM_SCHEMA = vol.Schema( vol.All( - mqtt.MQTT_BASE_SCHEMA.extend( + mqtt.config.MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "manual_mqtt", vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 46eb7052f4f..e21885d2585 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1,23 +1,13 @@ """Support for MQTT message handling.""" from __future__ import annotations -from ast import literal_eval import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Callable from dataclasses import dataclass import datetime as dt -from functools import lru_cache, partial, wraps -import inspect -from itertools import groupby import logging -from operator import attrgetter -import ssl -import time -from typing import TYPE_CHECKING, Any, Union, cast -import uuid +from typing import Any, cast -import attr -import certifi import jinja2 import voluptuous as vol @@ -25,120 +15,73 @@ from homeassistant import config_entries from homeassistant.components import websocket_api from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_NAME, - CONF_CLIENT_ID, CONF_DISCOVERY, CONF_PASSWORD, CONF_PAYLOAD, CONF_PORT, - CONF_PROTOCOL, CONF_USERNAME, - CONF_VALUE_TEMPLATE, - EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, - Platform, -) -from homeassistant.core import ( - CoreState, - Event, - HassJob, - HomeAssistant, - ServiceCall, - callback, ) +from homeassistant.core import Event, HassJob, HomeAssistant, ServiceCall, callback from homeassistant.data_entry_flow import BaseServiceInfo -from homeassistant.exceptions import HomeAssistantError, TemplateError, Unauthorized +from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.device_registry import DeviceEntry -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from homeassistant.loader import bind_hass -from homeassistant.util import dt as dt_util -from homeassistant.util.async_ import run_callback_threadsafe -from homeassistant.util.logging import catch_log_exception +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import ConfigType # Loading the config flow file will register the flow from . import debug_info, discovery -from .const import ( +from .client import ( # noqa: F401 + MQTT, + async_publish, + async_subscribe, + publish, + subscribe, +) +from .config import CONFIG_SCHEMA_BASE, DEFAULT_VALUES, DEPRECATED_CONFIG_KEYS +from .const import ( # noqa: F401 ATTR_PAYLOAD, ATTR_QOS, ATTR_RETAIN, ATTR_TOPIC, CONF_BIRTH_MESSAGE, CONF_BROKER, - CONF_CERTIFICATE, - CONF_CLIENT_CERT, - CONF_CLIENT_KEY, CONF_COMMAND_TOPIC, - CONF_ENCODING, + CONF_DISCOVERY_PREFIX, CONF_QOS, - CONF_RETAIN, CONF_STATE_TOPIC, - CONF_TLS_INSECURE, CONF_TLS_VERSION, CONF_TOPIC, CONF_WILL_MESSAGE, CONFIG_ENTRY_IS_SETUP, DATA_CONFIG_ENTRY_LOCK, + DATA_MQTT, DATA_MQTT_CONFIG, DATA_MQTT_RELOAD_NEEDED, - DEFAULT_BIRTH, - DEFAULT_DISCOVERY, DEFAULT_ENCODING, - DEFAULT_PREFIX, DEFAULT_QOS, DEFAULT_RETAIN, - DEFAULT_WILL, DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, - PROTOCOL_31, - PROTOCOL_311, + PLATFORMS, ) -from .discovery import LAST_DISCOVERY -from .models import ( - AsyncMessageCallbackType, - MessageCallbackType, - PublishMessage, +from .models import ( # noqa: F401 + MqttCommandTemplate, + MqttValueTemplate, PublishPayloadType, ReceiveMessage, ReceivePayloadType, ) from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic -if TYPE_CHECKING: - # Only import for paho-mqtt type checking here, imports are done locally - # because integrations should be able to optionally rely on MQTT. - import paho.mqtt.client as mqtt - _LOGGER = logging.getLogger(__name__) -_SENTINEL = object() - -DATA_MQTT = "mqtt" - SERVICE_PUBLISH = "publish" SERVICE_DUMP = "dump" -CONF_DISCOVERY_PREFIX = "discovery_prefix" -CONF_KEEPALIVE = "keepalive" - -DEFAULT_PORT = 1883 -DEFAULT_KEEPALIVE = 60 -DEFAULT_PROTOCOL = PROTOCOL_311 -DEFAULT_TLS_PROTOCOL = "auto" - -DEFAULT_VALUES = { - CONF_BIRTH_MESSAGE: DEFAULT_BIRTH, - CONF_DISCOVERY: DEFAULT_DISCOVERY, - CONF_PORT: DEFAULT_PORT, - CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL, - CONF_WILL_MESSAGE: DEFAULT_WILL, -} - MANDATORY_DEFAULT_VALUES = (CONF_PORT,) ATTR_TOPIC_TEMPLATE = "topic_template" @@ -150,93 +93,6 @@ CONNECTION_SUCCESS = "connection_success" CONNECTION_FAILED = "connection_failed" CONNECTION_FAILED_RECOVERABLE = "connection_failed_recoverable" -DISCOVERY_COOLDOWN = 2 -TIMEOUT_ACK = 10 - -PLATFORMS = [ - Platform.ALARM_CONTROL_PANEL, - Platform.BINARY_SENSOR, - Platform.BUTTON, - Platform.CAMERA, - Platform.CLIMATE, - Platform.DEVICE_TRACKER, - Platform.COVER, - Platform.FAN, - Platform.HUMIDIFIER, - Platform.LIGHT, - Platform.LOCK, - Platform.NUMBER, - Platform.SELECT, - Platform.SCENE, - Platform.SENSOR, - Platform.SIREN, - Platform.SWITCH, - Platform.VACUUM, -] - -CLIENT_KEY_AUTH_MSG = ( - "client_key and client_cert must both be present in " - "the MQTT broker configuration" -) - -MQTT_WILL_BIRTH_SCHEMA = vol.Schema( - { - vol.Inclusive(ATTR_TOPIC, "topic_payload"): valid_publish_topic, - vol.Inclusive(ATTR_PAYLOAD, "topic_payload"): cv.string, - vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, - vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - }, - required=True, -) - -PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( - {vol.Optional(platform.value): cv.ensure_list for platform in PLATFORMS} -) - -CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend( - { - vol.Optional(CONF_CLIENT_ID): cv.string, - vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( - vol.Coerce(int), vol.Range(min=15) - ), - vol.Optional(CONF_BROKER): cv.string, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), - vol.Inclusive( - CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG - ): cv.isfile, - vol.Inclusive( - CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG - ): cv.isfile, - vol.Optional(CONF_TLS_INSECURE): cv.boolean, - vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"), - vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( - cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) - ), - vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_DISCOVERY): cv.boolean, - # discovery_prefix must be a valid publish topic because if no - # state topic is specified, it will be created with the given prefix. - vol.Optional( - CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX - ): valid_publish_topic, - } -) - -DEPRECATED_CONFIG_KEYS = [ - CONF_BIRTH_MESSAGE, - CONF_BROKER, - CONF_DISCOVERY, - CONF_PASSWORD, - CONF_PORT, - CONF_TLS_VERSION, - CONF_USERNAME, - CONF_WILL_MESSAGE, -] - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( @@ -254,29 +110,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SCHEMA_BASE = { - vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, - vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, -} - -MQTT_BASE_SCHEMA = vol.Schema(SCHEMA_BASE) - -# Sensor type platforms subscribe to MQTT events -MQTT_RO_SCHEMA = MQTT_BASE_SCHEMA.extend( - { - vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - } -) - -# Switch type platforms publish to MQTT and may subscribe -MQTT_RW_SCHEMA = MQTT_BASE_SCHEMA.extend( - { - vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, - } -) # Service call validation schema MQTT_PUBLISH_SCHEMA = vol.All( @@ -295,124 +128,6 @@ MQTT_PUBLISH_SCHEMA = vol.All( ) -SubscribePayloadType = Union[str, bytes] # Only bytes if encoding is None - - -class MqttCommandTemplate: - """Class for rendering MQTT payload with command templates.""" - - def __init__( - self, - command_template: template.Template | None, - *, - hass: HomeAssistant | None = None, - entity: Entity | None = None, - ) -> None: - """Instantiate a command template.""" - self._attr_command_template = command_template - if command_template is None: - return - - self._entity = entity - - command_template.hass = hass - - if entity: - command_template.hass = entity.hass - - @callback - def async_render( - self, - value: PublishPayloadType = None, - variables: TemplateVarsType = None, - ) -> PublishPayloadType: - """Render or convert the command template with given value or variables.""" - - def _convert_outgoing_payload( - payload: PublishPayloadType, - ) -> PublishPayloadType: - """Ensure correct raw MQTT payload is passed as bytes for publishing.""" - if isinstance(payload, str): - try: - native_object = literal_eval(payload) - if isinstance(native_object, bytes): - return native_object - - except (ValueError, TypeError, SyntaxError, MemoryError): - pass - - return payload - - if self._attr_command_template is None: - return value - - values = {"value": value} - if self._entity: - values[ATTR_ENTITY_ID] = self._entity.entity_id - values[ATTR_NAME] = self._entity.name - if variables is not None: - values.update(variables) - return _convert_outgoing_payload( - self._attr_command_template.async_render(values, parse_result=False) - ) - - -class MqttValueTemplate: - """Class for rendering MQTT value template with possible json values.""" - - def __init__( - self, - value_template: template.Template | None, - *, - hass: HomeAssistant | None = None, - entity: Entity | None = None, - config_attributes: TemplateVarsType = None, - ) -> None: - """Instantiate a value template.""" - self._value_template = value_template - self._config_attributes = config_attributes - if value_template is None: - return - - value_template.hass = hass - self._entity = entity - - if entity: - value_template.hass = entity.hass - - @callback - def async_render_with_possible_json_value( - self, - payload: ReceivePayloadType, - default: ReceivePayloadType | object = _SENTINEL, - variables: TemplateVarsType = None, - ) -> ReceivePayloadType: - """Render with possible json value or pass-though a received MQTT value.""" - if self._value_template is None: - return payload - - values: dict[str, Any] = {} - - if variables is not None: - values.update(variables) - - if self._config_attributes is not None: - values.update(self._config_attributes) - - if self._entity: - values[ATTR_ENTITY_ID] = self._entity.entity_id - values[ATTR_NAME] = self._entity.name - - if default == _SENTINEL: - return self._value_template.async_render_with_possible_json_value( - payload, variables=values - ) - - return self._value_template.async_render_with_possible_json_value( - payload, default, variables=values - ) - - @dataclass class MqttServiceInfo(BaseServiceInfo): """Prepared info from mqtt entries.""" @@ -425,163 +140,6 @@ class MqttServiceInfo(BaseServiceInfo): timestamp: dt.datetime -def publish( - hass: HomeAssistant, - topic: str, - payload: PublishPayloadType, - qos: int | None = 0, - retain: bool | None = False, - encoding: str | None = DEFAULT_ENCODING, -) -> None: - """Publish message to a MQTT topic.""" - hass.add_job(async_publish, hass, topic, payload, qos, retain, encoding) - - -async def async_publish( - hass: HomeAssistant, - topic: str, - payload: PublishPayloadType, - qos: int | None = 0, - retain: bool | None = False, - encoding: str | None = DEFAULT_ENCODING, -) -> None: - """Publish message to a MQTT topic.""" - - outgoing_payload = payload - if not isinstance(payload, bytes): - if not encoding: - _LOGGER.error( - "Can't pass-through payload for publishing %s on %s with no encoding set, need 'bytes' got %s", - payload, - topic, - type(payload), - ) - return - outgoing_payload = str(payload) - if encoding != DEFAULT_ENCODING: - # a string is encoded as utf-8 by default, other encoding requires bytes as payload - try: - outgoing_payload = outgoing_payload.encode(encoding) - except (AttributeError, LookupError, UnicodeEncodeError): - _LOGGER.error( - "Can't encode payload for publishing %s on %s with encoding %s", - payload, - topic, - encoding, - ) - return - - await hass.data[DATA_MQTT].async_publish(topic, outgoing_payload, qos, retain) - - -AsyncDeprecatedMessageCallbackType = Callable[ - [str, ReceivePayloadType, int], Awaitable[None] -] -DeprecatedMessageCallbackType = Callable[[str, ReceivePayloadType, int], None] - - -def wrap_msg_callback( - msg_callback: AsyncDeprecatedMessageCallbackType | DeprecatedMessageCallbackType, -) -> AsyncMessageCallbackType | MessageCallbackType: - """Wrap an MQTT message callback to support deprecated signature.""" - # Check for partials to properly determine if coroutine function - check_func = msg_callback - while isinstance(check_func, partial): - check_func = check_func.func - - wrapper_func: AsyncMessageCallbackType | MessageCallbackType - if asyncio.iscoroutinefunction(check_func): - - @wraps(msg_callback) - async def async_wrapper(msg: ReceiveMessage) -> None: - """Call with deprecated signature.""" - await cast(AsyncDeprecatedMessageCallbackType, msg_callback)( - msg.topic, msg.payload, msg.qos - ) - - wrapper_func = async_wrapper - else: - - @wraps(msg_callback) - def wrapper(msg: ReceiveMessage) -> None: - """Call with deprecated signature.""" - msg_callback(msg.topic, msg.payload, msg.qos) - - wrapper_func = wrapper - return wrapper_func - - -@bind_hass -async def async_subscribe( - hass: HomeAssistant, - topic: str, - msg_callback: AsyncMessageCallbackType - | MessageCallbackType - | DeprecatedMessageCallbackType - | AsyncDeprecatedMessageCallbackType, - qos: int = DEFAULT_QOS, - encoding: str | None = "utf-8", -): - """Subscribe to an MQTT topic. - - Call the return value to unsubscribe. - """ - # Count callback parameters which don't have a default value - non_default = 0 - if msg_callback: - non_default = sum( - p.default == inspect.Parameter.empty - for _, p in inspect.signature(msg_callback).parameters.items() - ) - - wrapped_msg_callback = msg_callback - # If we have 3 parameters with no default value, wrap the callback - if non_default == 3: - module = inspect.getmodule(msg_callback) - _LOGGER.warning( - "Signature of MQTT msg_callback '%s.%s' is deprecated", - module.__name__ if module else "", - msg_callback.__name__, - ) - wrapped_msg_callback = wrap_msg_callback( - cast(DeprecatedMessageCallbackType, msg_callback) - ) - - async_remove = await hass.data[DATA_MQTT].async_subscribe( - topic, - catch_log_exception( - wrapped_msg_callback, - lambda msg: ( - f"Exception in {msg_callback.__name__} when handling msg on " - f"'{msg.topic}': '{msg.payload}'" - ), - ), - qos, - encoding, - ) - return async_remove - - -@bind_hass -def subscribe( - hass: HomeAssistant, - topic: str, - msg_callback: MessageCallbackType, - qos: int = DEFAULT_QOS, - encoding: str = "utf-8", -) -> Callable[[], None]: - """Subscribe to an MQTT topic.""" - async_remove = asyncio.run_coroutine_threadsafe( - async_subscribe(hass, topic, msg_callback, qos, encoding), hass.loop - ).result() - - def remove(): - """Remove listener convert.""" - run_callback_threadsafe(hass.loop, async_remove).result() - - return remove - - async def _async_setup_discovery( hass: HomeAssistant, conf: ConfigType, config_entry ) -> None: @@ -649,6 +207,26 @@ def _merge_extended_config(entry, conf): return {**conf, **entry.data} +async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle signals of config entry being updated. + + Causes for this is config entry options changing. + """ + mqtt_client = hass.data[DATA_MQTT] + + if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: + conf = CONFIG_SCHEMA_BASE(dict(entry.data)) + + mqtt_client.conf = _merge_extended_config(entry, conf) + await mqtt_client.async_disconnect() + mqtt_client.init_client() + await mqtt_client.async_connect() + + await discovery.async_stop(hass) + if mqtt_client.conf.get(CONF_DISCOVERY): + await _async_setup_discovery(hass, mqtt_client.conf, entry) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load a config entry.""" # Merge basic configuration, and add missing defaults for basic options @@ -685,6 +263,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, conf, ) + entry.add_update_listener(_async_config_entry_updated) await hass.data[DATA_MQTT].async_connect() @@ -813,459 +392,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -@attr.s(slots=True, frozen=True) -class Subscription: - """Class to hold data about an active subscription.""" - - topic: str = attr.ib() - matcher: Any = attr.ib() - job: HassJob = attr.ib() - qos: int = attr.ib(default=0) - encoding: str | None = attr.ib(default="utf-8") - - -class MqttClientSetup: - """Helper class to setup the paho mqtt client from config.""" - - def __init__(self, config: ConfigType) -> None: - """Initialize the MQTT client setup helper.""" - - # We don't import on the top because some integrations - # should be able to optionally rely on MQTT. - import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel - - if config[CONF_PROTOCOL] == PROTOCOL_31: - proto = mqtt.MQTTv31 - else: - proto = mqtt.MQTTv311 - - if (client_id := config.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) - self._client = mqtt.Client(client_id, protocol=proto) - - # Enable logging - self._client.enable_logger() - - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - if username is not None: - self._client.username_pw_set(username, password) - - if (certificate := config.get(CONF_CERTIFICATE)) == "auto": - certificate = certifi.where() - - client_key = config.get(CONF_CLIENT_KEY) - client_cert = config.get(CONF_CLIENT_CERT) - tls_insecure = config.get(CONF_TLS_INSECURE) - if certificate is not None: - self._client.tls_set( - certificate, - certfile=client_cert, - keyfile=client_key, - tls_version=ssl.PROTOCOL_TLS, - ) - - if tls_insecure is not None: - self._client.tls_insecure_set(tls_insecure) - - @property - def client(self) -> mqtt.Client: - """Return the paho MQTT client.""" - return self._client - - -class MQTT: - """Home Assistant MQTT client.""" - - def __init__( - self, - hass: HomeAssistant, - config_entry, - conf, - ) -> None: - """Initialize Home Assistant MQTT client.""" - # We don't import on the top because some integrations - # should be able to optionally rely on MQTT. - import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel - - self.hass = hass - self.config_entry = config_entry - self.conf = conf - self.subscriptions: list[Subscription] = [] - self.connected = False - self._ha_started = asyncio.Event() - self._last_subscribe = time.time() - self._mqttc: mqtt.Client = None - self._paho_lock = asyncio.Lock() - - self._pending_operations: dict[str, asyncio.Event] = {} - - if self.hass.state == CoreState.running: - self._ha_started.set() - else: - - @callback - def ha_started(_): - self._ha_started.set() - - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, ha_started) - - self.init_client() - self.config_entry.add_update_listener(self.async_config_entry_updated) - - @staticmethod - async def async_config_entry_updated( - hass: HomeAssistant, entry: ConfigEntry - ) -> None: - """Handle signals of config entry being updated. - - This is a static method because a class method (bound method), can not be used with weak references. - Causes for this is config entry options changing. - """ - self = hass.data[DATA_MQTT] - - if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: - conf = CONFIG_SCHEMA_BASE(dict(entry.data)) - - self.conf = _merge_extended_config(entry, conf) - await self.async_disconnect() - self.init_client() - await self.async_connect() - - await discovery.async_stop(hass) - if self.conf.get(CONF_DISCOVERY): - await _async_setup_discovery(hass, self.conf, entry) - - def init_client(self): - """Initialize paho client.""" - self._mqttc = MqttClientSetup(self.conf).client - self._mqttc.on_connect = self._mqtt_on_connect - self._mqttc.on_disconnect = self._mqtt_on_disconnect - self._mqttc.on_message = self._mqtt_on_message - self._mqttc.on_publish = self._mqtt_on_callback - self._mqttc.on_subscribe = self._mqtt_on_callback - self._mqttc.on_unsubscribe = self._mqtt_on_callback - - if ( - CONF_WILL_MESSAGE in self.conf - and ATTR_TOPIC in self.conf[CONF_WILL_MESSAGE] - ): - will_message = PublishMessage(**self.conf[CONF_WILL_MESSAGE]) - else: - will_message = None - - if will_message is not None: - self._mqttc.will_set( - topic=will_message.topic, - payload=will_message.payload, - qos=will_message.qos, - retain=will_message.retain, - ) - - async def async_publish( - self, topic: str, payload: PublishPayloadType, qos: int, retain: bool - ) -> None: - """Publish a MQTT message.""" - async with self._paho_lock: - msg_info = await self.hass.async_add_executor_job( - self._mqttc.publish, topic, payload, qos, retain - ) - _LOGGER.debug( - "Transmitting message on %s: '%s', mid: %s", - topic, - payload, - msg_info.mid, - ) - _raise_on_error(msg_info.rc) - await self._wait_for_mid(msg_info.mid) - - async def async_connect(self) -> None: - """Connect to the host. Does not process messages yet.""" - # pylint: disable-next=import-outside-toplevel - import paho.mqtt.client as mqtt - - result: int | None = None - try: - result = await self.hass.async_add_executor_job( - self._mqttc.connect, - self.conf[CONF_BROKER], - self.conf[CONF_PORT], - self.conf[CONF_KEEPALIVE], - ) - except OSError as err: - _LOGGER.error("Failed to connect to MQTT server due to exception: %s", err) - - if result is not None and result != 0: - _LOGGER.error( - "Failed to connect to MQTT server: %s", mqtt.error_string(result) - ) - - self._mqttc.loop_start() - - async def async_disconnect(self): - """Stop the MQTT client.""" - - def stop(): - """Stop the MQTT client.""" - # Do not disconnect, we want the broker to always publish will - self._mqttc.loop_stop() - - await self.hass.async_add_executor_job(stop) - - async def async_subscribe( - self, - topic: str, - msg_callback: MessageCallbackType, - qos: int, - encoding: str | None = None, - ) -> Callable[[], None]: - """Set up a subscription to a topic with the provided qos. - - This method is a coroutine. - """ - if not isinstance(topic, str): - raise HomeAssistantError("Topic needs to be a string!") - - subscription = Subscription( - topic, _matcher_for_topic(topic), HassJob(msg_callback), qos, encoding - ) - self.subscriptions.append(subscription) - self._matching_subscriptions.cache_clear() - - # Only subscribe if currently connected. - if self.connected: - self._last_subscribe = time.time() - await self._async_perform_subscription(topic, qos) - - @callback - def async_remove() -> None: - """Remove subscription.""" - if subscription not in self.subscriptions: - raise HomeAssistantError("Can't remove subscription twice") - self.subscriptions.remove(subscription) - self._matching_subscriptions.cache_clear() - - # Only unsubscribe if currently connected. - if self.connected: - self.hass.async_create_task(self._async_unsubscribe(topic)) - - return async_remove - - async def _async_unsubscribe(self, topic: str) -> None: - """Unsubscribe from a topic. - - This method is a coroutine. - """ - if any(other.topic == topic for other in self.subscriptions): - # Other subscriptions on topic remaining - don't unsubscribe. - return - - async with self._paho_lock: - result: int | None = None - result, mid = await self.hass.async_add_executor_job( - self._mqttc.unsubscribe, topic - ) - _LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid) - _raise_on_error(result) - await self._wait_for_mid(mid) - - async def _async_perform_subscription(self, topic: str, qos: int) -> None: - """Perform a paho-mqtt subscription.""" - async with self._paho_lock: - result: int | None = None - result, mid = await self.hass.async_add_executor_job( - self._mqttc.subscribe, topic, qos - ) - _LOGGER.debug("Subscribing to %s, mid: %s", topic, mid) - _raise_on_error(result) - await self._wait_for_mid(mid) - - def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code: int) -> None: - """On connect callback. - - Resubscribe to all topics we were subscribed to and publish birth - message. - """ - # pylint: disable-next=import-outside-toplevel - import paho.mqtt.client as mqtt - - if result_code != mqtt.CONNACK_ACCEPTED: - _LOGGER.error( - "Unable to connect to the MQTT broker: %s", - mqtt.connack_string(result_code), - ) - return - - self.connected = True - dispatcher_send(self.hass, MQTT_CONNECTED) - _LOGGER.info( - "Connected to MQTT server %s:%s (%s)", - self.conf[CONF_BROKER], - self.conf[CONF_PORT], - result_code, - ) - - # Group subscriptions to only re-subscribe once for each topic. - keyfunc = attrgetter("topic") - for topic, subs in groupby(sorted(self.subscriptions, key=keyfunc), keyfunc): - # Re-subscribe with the highest requested qos - max_qos = max(subscription.qos for subscription in subs) - self.hass.add_job(self._async_perform_subscription, topic, max_qos) - - if ( - CONF_BIRTH_MESSAGE in self.conf - and ATTR_TOPIC in self.conf[CONF_BIRTH_MESSAGE] - ): - - async def publish_birth_message(birth_message): - await self._ha_started.wait() # Wait for Home Assistant to start - await self._discovery_cooldown() # Wait for MQTT discovery to cool down - await self.async_publish( - topic=birth_message.topic, - payload=birth_message.payload, - qos=birth_message.qos, - retain=birth_message.retain, - ) - - birth_message = PublishMessage(**self.conf[CONF_BIRTH_MESSAGE]) - asyncio.run_coroutine_threadsafe( - publish_birth_message(birth_message), self.hass.loop - ) - - def _mqtt_on_message(self, _mqttc, _userdata, msg) -> None: - """Message received callback.""" - self.hass.add_job(self._mqtt_handle_message, msg) - - @lru_cache(2048) - def _matching_subscriptions(self, topic): - subscriptions = [] - for subscription in self.subscriptions: - if subscription.matcher(topic): - subscriptions.append(subscription) - return subscriptions - - @callback - def _mqtt_handle_message(self, msg) -> None: - _LOGGER.debug( - "Received message on %s%s: %s", - msg.topic, - " (retained)" if msg.retain else "", - msg.payload[0:8192], - ) - timestamp = dt_util.utcnow() - - subscriptions = self._matching_subscriptions(msg.topic) - - for subscription in subscriptions: - - payload: SubscribePayloadType = msg.payload - if subscription.encoding is not None: - try: - payload = msg.payload.decode(subscription.encoding) - except (AttributeError, UnicodeDecodeError): - _LOGGER.warning( - "Can't decode payload %s on %s with encoding %s (for %s)", - msg.payload[0:8192], - msg.topic, - subscription.encoding, - subscription.job, - ) - continue - - self.hass.async_run_hass_job( - subscription.job, - ReceiveMessage( - msg.topic, - payload, - msg.qos, - msg.retain, - subscription.topic, - timestamp, - ), - ) - - def _mqtt_on_callback(self, _mqttc, _userdata, mid, _granted_qos=None) -> None: - """Publish / Subscribe / Unsubscribe callback.""" - self.hass.add_job(self._mqtt_handle_mid, mid) - - @callback - def _mqtt_handle_mid(self, mid) -> None: - # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid - # may be executed first. - if mid not in self._pending_operations: - self._pending_operations[mid] = asyncio.Event() - self._pending_operations[mid].set() - - def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None: - """Disconnected callback.""" - self.connected = False - dispatcher_send(self.hass, MQTT_DISCONNECTED) - _LOGGER.warning( - "Disconnected from MQTT server %s:%s (%s)", - self.conf[CONF_BROKER], - self.conf[CONF_PORT], - result_code, - ) - - async def _wait_for_mid(self, mid): - """Wait for ACK from broker.""" - # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid - # may be executed first. - if mid not in self._pending_operations: - self._pending_operations[mid] = asyncio.Event() - try: - await asyncio.wait_for(self._pending_operations[mid].wait(), TIMEOUT_ACK) - except asyncio.TimeoutError: - _LOGGER.warning( - "No ACK from MQTT server in %s seconds (mid: %s)", TIMEOUT_ACK, mid - ) - finally: - del self._pending_operations[mid] - - async def _discovery_cooldown(self): - now = time.time() - # Reset discovery and subscribe cooldowns - self.hass.data[LAST_DISCOVERY] = now - self._last_subscribe = now - - last_discovery = self.hass.data[LAST_DISCOVERY] - last_subscribe = self._last_subscribe - wait_until = max( - last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN - ) - while now < wait_until: - await asyncio.sleep(wait_until - now) - now = time.time() - last_discovery = self.hass.data[LAST_DISCOVERY] - last_subscribe = self._last_subscribe - wait_until = max( - last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN - ) - - -def _raise_on_error(result_code: int | None) -> None: - """Raise error if error result.""" - # pylint: disable-next=import-outside-toplevel - import paho.mqtt.client as mqtt - - if result_code is not None and result_code != 0: - raise HomeAssistantError( - f"Error talking to MQTT: {mqtt.error_string(result_code)}" - ) - - -def _matcher_for_topic(subscription: str) -> Any: - # pylint: disable-next=import-outside-toplevel - from paho.mqtt.matcher import MQTTMatcher - - matcher = MQTTMatcher() - matcher[subscription] = True - - return lambda topic: next(matcher.iter_match(topic), False) - - @websocket_api.websocket_command( {vol.Required("type"): "mqtt/device/debug_info", vol.Required("device_id"): str} ) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 06c013ec744..c20fbb7c657 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -31,8 +31,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -50,6 +50,8 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate +from .util import valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -85,7 +87,7 @@ DEFAULT_NAME = "MQTT Alarm" REMOTE_CODE = "REMOTE_CODE" REMOTE_CODE_TEXT = "REMOTE_CODE_TEXT" -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_CODE): cv.string, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, @@ -94,7 +96,7 @@ PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( vol.Optional( CONF_COMMAND_TEMPLATE, default=DEFAULT_COMMAND_TEMPLATE ): cv.template, - vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string, @@ -107,8 +109,8 @@ PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( ): cv.string, vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string, vol.Optional(CONF_PAYLOAD_TRIGGER, default=DEFAULT_TRIGGER): cv.string, - vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, - vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index b9ab190cc9b..1cb90d6c903 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -34,8 +34,8 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util -from . import MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RO_SCHEMA from .const import CONF_ENCODING, CONF_QOS, CONF_STATE_TOPIC, PAYLOAD_NONE from .debug_info import log_messages from .mixins import ( @@ -47,6 +47,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttValueTemplate _LOGGER = logging.getLogger(__name__) @@ -57,7 +58,7 @@ DEFAULT_PAYLOAD_ON = "ON" DEFAULT_FORCE_UPDATE = False CONF_EXPIRE_AFTER = "expire_after" -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RO_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( { vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index 47e96ff3e1a..b50856d20c1 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -15,8 +15,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate -from .. import mqtt +from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -32,19 +31,21 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate +from .util import valid_publish_topic CONF_PAYLOAD_PRESS = "payload_press" DEFAULT_NAME = "MQTT Button" DEFAULT_PAYLOAD_PRESS = "PRESS" -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, - vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_DEVICE_CLASS): button.DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_PRESS, default=DEFAULT_PAYLOAD_PRESS): cv.string, - vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 2e5d95ebda4..ae38e07d17a 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -17,7 +17,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription -from .. import mqtt +from .config import MQTT_BASE_SCHEMA from .const import CONF_ENCODING, CONF_QOS, CONF_TOPIC from .debug_info import log_messages from .mixins import ( @@ -28,6 +28,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .util import valid_subscribe_topic DEFAULT_NAME = "MQTT Camera" @@ -40,10 +41,10 @@ MQTT_CAMERA_ATTRIBUTES_BLOCKED = frozenset( } ) -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, + vol.Required(CONF_TOPIC): valid_subscribe_topic, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py new file mode 100644 index 00000000000..66699372516 --- /dev/null +++ b/homeassistant/components/mqtt/client.py @@ -0,0 +1,659 @@ +"""Support for MQTT message handling.""" +from __future__ import annotations + +import asyncio +from collections.abc import Awaitable, Callable +from functools import lru_cache, partial, wraps +import inspect +from itertools import groupby +import logging +from operator import attrgetter +import ssl +import time +from typing import TYPE_CHECKING, Any, Union, cast +import uuid + +import attr +import certifi + +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STARTED, +) +from homeassistant.core import CoreState, HassJob, HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.typing import ConfigType +from homeassistant.loader import bind_hass +from homeassistant.util import dt as dt_util +from homeassistant.util.async_ import run_callback_threadsafe +from homeassistant.util.logging import catch_log_exception + +from .const import ( + ATTR_TOPIC, + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_CERTIFICATE, + CONF_CLIENT_CERT, + CONF_CLIENT_KEY, + CONF_KEEPALIVE, + CONF_TLS_INSECURE, + CONF_WILL_MESSAGE, + DATA_MQTT, + DEFAULT_ENCODING, + DEFAULT_QOS, + MQTT_CONNECTED, + MQTT_DISCONNECTED, + PROTOCOL_31, +) +from .discovery import LAST_DISCOVERY +from .models import ( + AsyncMessageCallbackType, + MessageCallbackType, + PublishMessage, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) + +if TYPE_CHECKING: + # Only import for paho-mqtt type checking here, imports are done locally + # because integrations should be able to optionally rely on MQTT. + import paho.mqtt.client as mqtt + +_LOGGER = logging.getLogger(__name__) + +DISCOVERY_COOLDOWN = 2 +TIMEOUT_ACK = 10 + +SubscribePayloadType = Union[str, bytes] # Only bytes if encoding is None + + +def publish( + hass: HomeAssistant, + topic: str, + payload: PublishPayloadType, + qos: int | None = 0, + retain: bool | None = False, + encoding: str | None = DEFAULT_ENCODING, +) -> None: + """Publish message to a MQTT topic.""" + hass.add_job(async_publish, hass, topic, payload, qos, retain, encoding) + + +async def async_publish( + hass: HomeAssistant, + topic: str, + payload: PublishPayloadType, + qos: int | None = 0, + retain: bool | None = False, + encoding: str | None = DEFAULT_ENCODING, +) -> None: + """Publish message to a MQTT topic.""" + + outgoing_payload = payload + if not isinstance(payload, bytes): + if not encoding: + _LOGGER.error( + "Can't pass-through payload for publishing %s on %s with no encoding set, need 'bytes' got %s", + payload, + topic, + type(payload), + ) + return + outgoing_payload = str(payload) + if encoding != DEFAULT_ENCODING: + # a string is encoded as utf-8 by default, other encoding requires bytes as payload + try: + outgoing_payload = outgoing_payload.encode(encoding) + except (AttributeError, LookupError, UnicodeEncodeError): + _LOGGER.error( + "Can't encode payload for publishing %s on %s with encoding %s", + payload, + topic, + encoding, + ) + return + + await hass.data[DATA_MQTT].async_publish(topic, outgoing_payload, qos, retain) + + +AsyncDeprecatedMessageCallbackType = Callable[ + [str, ReceivePayloadType, int], Awaitable[None] +] +DeprecatedMessageCallbackType = Callable[[str, ReceivePayloadType, int], None] + + +def wrap_msg_callback( + msg_callback: AsyncDeprecatedMessageCallbackType | DeprecatedMessageCallbackType, +) -> AsyncMessageCallbackType | MessageCallbackType: + """Wrap an MQTT message callback to support deprecated signature.""" + # Check for partials to properly determine if coroutine function + check_func = msg_callback + while isinstance(check_func, partial): + check_func = check_func.func + + wrapper_func: AsyncMessageCallbackType | MessageCallbackType + if asyncio.iscoroutinefunction(check_func): + + @wraps(msg_callback) + async def async_wrapper(msg: ReceiveMessage) -> None: + """Call with deprecated signature.""" + await cast(AsyncDeprecatedMessageCallbackType, msg_callback)( + msg.topic, msg.payload, msg.qos + ) + + wrapper_func = async_wrapper + else: + + @wraps(msg_callback) + def wrapper(msg: ReceiveMessage) -> None: + """Call with deprecated signature.""" + msg_callback(msg.topic, msg.payload, msg.qos) + + wrapper_func = wrapper + return wrapper_func + + +@bind_hass +async def async_subscribe( + hass: HomeAssistant, + topic: str, + msg_callback: AsyncMessageCallbackType + | MessageCallbackType + | DeprecatedMessageCallbackType + | AsyncDeprecatedMessageCallbackType, + qos: int = DEFAULT_QOS, + encoding: str | None = "utf-8", +): + """Subscribe to an MQTT topic. + + Call the return value to unsubscribe. + """ + # Count callback parameters which don't have a default value + non_default = 0 + if msg_callback: + non_default = sum( + p.default == inspect.Parameter.empty + for _, p in inspect.signature(msg_callback).parameters.items() + ) + + wrapped_msg_callback = msg_callback + # If we have 3 parameters with no default value, wrap the callback + if non_default == 3: + module = inspect.getmodule(msg_callback) + _LOGGER.warning( + "Signature of MQTT msg_callback '%s.%s' is deprecated", + module.__name__ if module else "", + msg_callback.__name__, + ) + wrapped_msg_callback = wrap_msg_callback( + cast(DeprecatedMessageCallbackType, msg_callback) + ) + + async_remove = await hass.data[DATA_MQTT].async_subscribe( + topic, + catch_log_exception( + wrapped_msg_callback, + lambda msg: ( + f"Exception in {msg_callback.__name__} when handling msg on " + f"'{msg.topic}': '{msg.payload}'" + ), + ), + qos, + encoding, + ) + return async_remove + + +@bind_hass +def subscribe( + hass: HomeAssistant, + topic: str, + msg_callback: MessageCallbackType, + qos: int = DEFAULT_QOS, + encoding: str = "utf-8", +) -> Callable[[], None]: + """Subscribe to an MQTT topic.""" + async_remove = asyncio.run_coroutine_threadsafe( + async_subscribe(hass, topic, msg_callback, qos, encoding), hass.loop + ).result() + + def remove(): + """Remove listener convert.""" + run_callback_threadsafe(hass.loop, async_remove).result() + + return remove + + +@attr.s(slots=True, frozen=True) +class Subscription: + """Class to hold data about an active subscription.""" + + topic: str = attr.ib() + matcher: Any = attr.ib() + job: HassJob = attr.ib() + qos: int = attr.ib(default=0) + encoding: str | None = attr.ib(default="utf-8") + + +class MqttClientSetup: + """Helper class to setup the paho mqtt client from config.""" + + def __init__(self, config: ConfigType) -> None: + """Initialize the MQTT client setup helper.""" + + # We don't import on the top because some integrations + # should be able to optionally rely on MQTT. + import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + + if config[CONF_PROTOCOL] == PROTOCOL_31: + proto = mqtt.MQTTv31 + else: + proto = mqtt.MQTTv311 + + if (client_id := config.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) + self._client = mqtt.Client(client_id, protocol=proto) + + # Enable logging + self._client.enable_logger() + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + if username is not None: + self._client.username_pw_set(username, password) + + if (certificate := config.get(CONF_CERTIFICATE)) == "auto": + certificate = certifi.where() + + client_key = config.get(CONF_CLIENT_KEY) + client_cert = config.get(CONF_CLIENT_CERT) + tls_insecure = config.get(CONF_TLS_INSECURE) + if certificate is not None: + self._client.tls_set( + certificate, + certfile=client_cert, + keyfile=client_key, + tls_version=ssl.PROTOCOL_TLS, + ) + + if tls_insecure is not None: + self._client.tls_insecure_set(tls_insecure) + + @property + def client(self) -> mqtt.Client: + """Return the paho MQTT client.""" + return self._client + + +class MQTT: + """Home Assistant MQTT client.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry, + conf, + ) -> None: + """Initialize Home Assistant MQTT client.""" + # We don't import on the top because some integrations + # should be able to optionally rely on MQTT. + import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + + self.hass = hass + self.config_entry = config_entry + self.conf = conf + self.subscriptions: list[Subscription] = [] + self.connected = False + self._ha_started = asyncio.Event() + self._last_subscribe = time.time() + self._mqttc: mqtt.Client = None + self._paho_lock = asyncio.Lock() + + self._pending_operations: dict[str, asyncio.Event] = {} + + if self.hass.state == CoreState.running: + self._ha_started.set() + else: + + @callback + def ha_started(_): + self._ha_started.set() + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, ha_started) + + self.init_client() + + def init_client(self): + """Initialize paho client.""" + self._mqttc = MqttClientSetup(self.conf).client + self._mqttc.on_connect = self._mqtt_on_connect + self._mqttc.on_disconnect = self._mqtt_on_disconnect + self._mqttc.on_message = self._mqtt_on_message + self._mqttc.on_publish = self._mqtt_on_callback + self._mqttc.on_subscribe = self._mqtt_on_callback + self._mqttc.on_unsubscribe = self._mqtt_on_callback + + if ( + CONF_WILL_MESSAGE in self.conf + and ATTR_TOPIC in self.conf[CONF_WILL_MESSAGE] + ): + will_message = PublishMessage(**self.conf[CONF_WILL_MESSAGE]) + else: + will_message = None + + if will_message is not None: + self._mqttc.will_set( + topic=will_message.topic, + payload=will_message.payload, + qos=will_message.qos, + retain=will_message.retain, + ) + + async def async_publish( + self, topic: str, payload: PublishPayloadType, qos: int, retain: bool + ) -> None: + """Publish a MQTT message.""" + async with self._paho_lock: + msg_info = await self.hass.async_add_executor_job( + self._mqttc.publish, topic, payload, qos, retain + ) + _LOGGER.debug( + "Transmitting message on %s: '%s', mid: %s", + topic, + payload, + msg_info.mid, + ) + _raise_on_error(msg_info.rc) + await self._wait_for_mid(msg_info.mid) + + async def async_connect(self) -> None: + """Connect to the host. Does not process messages yet.""" + # pylint: disable-next=import-outside-toplevel + import paho.mqtt.client as mqtt + + result: int | None = None + try: + result = await self.hass.async_add_executor_job( + self._mqttc.connect, + self.conf[CONF_BROKER], + self.conf[CONF_PORT], + self.conf[CONF_KEEPALIVE], + ) + except OSError as err: + _LOGGER.error("Failed to connect to MQTT server due to exception: %s", err) + + if result is not None and result != 0: + _LOGGER.error( + "Failed to connect to MQTT server: %s", mqtt.error_string(result) + ) + + self._mqttc.loop_start() + + async def async_disconnect(self): + """Stop the MQTT client.""" + + def stop(): + """Stop the MQTT client.""" + # Do not disconnect, we want the broker to always publish will + self._mqttc.loop_stop() + + await self.hass.async_add_executor_job(stop) + + async def async_subscribe( + self, + topic: str, + msg_callback: MessageCallbackType, + qos: int, + encoding: str | None = None, + ) -> Callable[[], None]: + """Set up a subscription to a topic with the provided qos. + + This method is a coroutine. + """ + if not isinstance(topic, str): + raise HomeAssistantError("Topic needs to be a string!") + + subscription = Subscription( + topic, _matcher_for_topic(topic), HassJob(msg_callback), qos, encoding + ) + self.subscriptions.append(subscription) + self._matching_subscriptions.cache_clear() + + # Only subscribe if currently connected. + if self.connected: + self._last_subscribe = time.time() + await self._async_perform_subscription(topic, qos) + + @callback + def async_remove() -> None: + """Remove subscription.""" + if subscription not in self.subscriptions: + raise HomeAssistantError("Can't remove subscription twice") + self.subscriptions.remove(subscription) + self._matching_subscriptions.cache_clear() + + # Only unsubscribe if currently connected. + if self.connected: + self.hass.async_create_task(self._async_unsubscribe(topic)) + + return async_remove + + async def _async_unsubscribe(self, topic: str) -> None: + """Unsubscribe from a topic. + + This method is a coroutine. + """ + if any(other.topic == topic for other in self.subscriptions): + # Other subscriptions on topic remaining - don't unsubscribe. + return + + async with self._paho_lock: + result: int | None = None + result, mid = await self.hass.async_add_executor_job( + self._mqttc.unsubscribe, topic + ) + _LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid) + _raise_on_error(result) + await self._wait_for_mid(mid) + + async def _async_perform_subscription(self, topic: str, qos: int) -> None: + """Perform a paho-mqtt subscription.""" + async with self._paho_lock: + result: int | None = None + result, mid = await self.hass.async_add_executor_job( + self._mqttc.subscribe, topic, qos + ) + _LOGGER.debug("Subscribing to %s, mid: %s", topic, mid) + _raise_on_error(result) + await self._wait_for_mid(mid) + + def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code: int) -> None: + """On connect callback. + + Resubscribe to all topics we were subscribed to and publish birth + message. + """ + # pylint: disable-next=import-outside-toplevel + import paho.mqtt.client as mqtt + + if result_code != mqtt.CONNACK_ACCEPTED: + _LOGGER.error( + "Unable to connect to the MQTT broker: %s", + mqtt.connack_string(result_code), + ) + return + + self.connected = True + dispatcher_send(self.hass, MQTT_CONNECTED) + _LOGGER.info( + "Connected to MQTT server %s:%s (%s)", + self.conf[CONF_BROKER], + self.conf[CONF_PORT], + result_code, + ) + + # Group subscriptions to only re-subscribe once for each topic. + keyfunc = attrgetter("topic") + for topic, subs in groupby(sorted(self.subscriptions, key=keyfunc), keyfunc): + # Re-subscribe with the highest requested qos + max_qos = max(subscription.qos for subscription in subs) + self.hass.add_job(self._async_perform_subscription, topic, max_qos) + + if ( + CONF_BIRTH_MESSAGE in self.conf + and ATTR_TOPIC in self.conf[CONF_BIRTH_MESSAGE] + ): + + async def publish_birth_message(birth_message): + await self._ha_started.wait() # Wait for Home Assistant to start + await self._discovery_cooldown() # Wait for MQTT discovery to cool down + await self.async_publish( + topic=birth_message.topic, + payload=birth_message.payload, + qos=birth_message.qos, + retain=birth_message.retain, + ) + + birth_message = PublishMessage(**self.conf[CONF_BIRTH_MESSAGE]) + asyncio.run_coroutine_threadsafe( + publish_birth_message(birth_message), self.hass.loop + ) + + def _mqtt_on_message(self, _mqttc, _userdata, msg) -> None: + """Message received callback.""" + self.hass.add_job(self._mqtt_handle_message, msg) + + @lru_cache(2048) + def _matching_subscriptions(self, topic): + subscriptions = [] + for subscription in self.subscriptions: + if subscription.matcher(topic): + subscriptions.append(subscription) + return subscriptions + + @callback + def _mqtt_handle_message(self, msg) -> None: + _LOGGER.debug( + "Received message on %s%s: %s", + msg.topic, + " (retained)" if msg.retain else "", + msg.payload[0:8192], + ) + timestamp = dt_util.utcnow() + + subscriptions = self._matching_subscriptions(msg.topic) + + for subscription in subscriptions: + + payload: SubscribePayloadType = msg.payload + if subscription.encoding is not None: + try: + payload = msg.payload.decode(subscription.encoding) + except (AttributeError, UnicodeDecodeError): + _LOGGER.warning( + "Can't decode payload %s on %s with encoding %s (for %s)", + msg.payload[0:8192], + msg.topic, + subscription.encoding, + subscription.job, + ) + continue + + self.hass.async_run_hass_job( + subscription.job, + ReceiveMessage( + msg.topic, + payload, + msg.qos, + msg.retain, + subscription.topic, + timestamp, + ), + ) + + def _mqtt_on_callback(self, _mqttc, _userdata, mid, _granted_qos=None) -> None: + """Publish / Subscribe / Unsubscribe callback.""" + self.hass.add_job(self._mqtt_handle_mid, mid) + + @callback + def _mqtt_handle_mid(self, mid) -> None: + # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid + # may be executed first. + if mid not in self._pending_operations: + self._pending_operations[mid] = asyncio.Event() + self._pending_operations[mid].set() + + def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None: + """Disconnected callback.""" + self.connected = False + dispatcher_send(self.hass, MQTT_DISCONNECTED) + _LOGGER.warning( + "Disconnected from MQTT server %s:%s (%s)", + self.conf[CONF_BROKER], + self.conf[CONF_PORT], + result_code, + ) + + async def _wait_for_mid(self, mid): + """Wait for ACK from broker.""" + # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid + # may be executed first. + if mid not in self._pending_operations: + self._pending_operations[mid] = asyncio.Event() + try: + await asyncio.wait_for(self._pending_operations[mid].wait(), TIMEOUT_ACK) + except asyncio.TimeoutError: + _LOGGER.warning( + "No ACK from MQTT server in %s seconds (mid: %s)", TIMEOUT_ACK, mid + ) + finally: + del self._pending_operations[mid] + + async def _discovery_cooldown(self): + now = time.time() + # Reset discovery and subscribe cooldowns + self.hass.data[LAST_DISCOVERY] = now + self._last_subscribe = now + + last_discovery = self.hass.data[LAST_DISCOVERY] + last_subscribe = self._last_subscribe + wait_until = max( + last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN + ) + while now < wait_until: + await asyncio.sleep(wait_until - now) + now = time.time() + last_discovery = self.hass.data[LAST_DISCOVERY] + last_subscribe = self._last_subscribe + wait_until = max( + last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN + ) + + +def _raise_on_error(result_code: int | None) -> None: + """Raise error if error result.""" + # pylint: disable-next=import-outside-toplevel + import paho.mqtt.client as mqtt + + if result_code is not None and result_code != 0: + raise HomeAssistantError( + f"Error talking to MQTT: {mqtt.error_string(result_code)}" + ) + + +def _matcher_for_topic(subscription: str) -> Any: + # pylint: disable-next=import-outside-toplevel + from paho.mqtt.matcher import MQTTMatcher + + matcher = MQTTMatcher() + matcher[subscription] = True + + return lambda topic: next(matcher.iter_match(topic), False) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 52465bbba24..64b462359be 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -44,8 +44,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA from .const import CONF_ENCODING, CONF_QOS, CONF_RETAIN, PAYLOAD_NONE from .debug_info import log_messages from .mixins import ( @@ -56,6 +56,8 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate +from .util import valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -232,33 +234,33 @@ def valid_preset_mode_configuration(config): return config -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( { - vol.Optional(CONF_AUX_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_AUX_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_AUX_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_AUX_STATE_TOPIC): valid_subscribe_topic, # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_AWAY_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template, - vol.Optional(CONF_CURRENT_TEMP_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_CURRENT_TEMP_TOPIC): valid_subscribe_topic, vol.Optional(CONF_FAN_MODE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): valid_publish_topic, vol.Optional( CONF_FAN_MODE_LIST, default=[FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH], ): cv.ensure_list, vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_FAN_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_FAN_MODE_STATE_TOPIC): valid_subscribe_topic, # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 vol.Optional(CONF_HOLD_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_HOLD_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_HOLD_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_HOLD_LIST): cv.ensure_list, vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_MODE_COMMAND_TOPIC): valid_publish_topic, vol.Optional( CONF_MODE_LIST, default=[ @@ -271,54 +273,54 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_SCHEMA.extend( ], ): cv.ensure_list, vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, - vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_POWER_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_POWER_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_POWER_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_POWER_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_PRECISION): vol.In( [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] ), - vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 vol.Optional(CONF_SEND_IF_OFF): cv.boolean, vol.Optional(CONF_ACTION_TEMPLATE): cv.template, - vol.Optional(CONF_ACTION_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_ACTION_TOPIC): valid_subscribe_topic, # 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, + ): 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_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_PRESET_MODE_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_SWING_MODE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): valid_publish_topic, vol.Optional( CONF_SWING_MODE_LIST, default=[SWING_ON, SWING_OFF] ): cv.ensure_list, vol.Optional(CONF_SWING_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_SWING_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_SWING_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_TEMP_INITIAL, default=21): cv.positive_int, vol.Optional(CONF_TEMP_MIN, default=DEFAULT_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_TEMP_MAX, default=DEFAULT_MAX_TEMP): vol.Coerce(float), vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float), vol.Optional(CONF_TEMP_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_TEMP_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_TEMP_HIGH_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_HIGH_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_TEMP_HIGH_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_TEMP_HIGH_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_TEMP_HIGH_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_TEMP_HIGH_STATE_TEMPLATE): cv.template, vol.Optional(CONF_TEMP_LOW_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_LOW_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_TEMP_LOW_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_TEMP_LOW_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_LOW_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_TEMP_LOW_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_TEMP_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_TEMP_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_TEMPERATURE_UNIT): cv.temperature_unit, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } diff --git a/homeassistant/components/mqtt/config.py b/homeassistant/components/mqtt/config.py new file mode 100644 index 00000000000..4f84d911418 --- /dev/null +++ b/homeassistant/components/mqtt/config.py @@ -0,0 +1,148 @@ +"""Support for MQTT message handling.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_DISCOVERY, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, + CONF_VALUE_TEMPLATE, +) +from homeassistant.helpers import config_validation as cv + +from .const import ( + ATTR_PAYLOAD, + ATTR_QOS, + ATTR_RETAIN, + ATTR_TOPIC, + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_CERTIFICATE, + CONF_CLIENT_CERT, + CONF_CLIENT_KEY, + CONF_COMMAND_TOPIC, + CONF_DISCOVERY_PREFIX, + CONF_ENCODING, + CONF_KEEPALIVE, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + CONF_TLS_INSECURE, + CONF_TLS_VERSION, + CONF_WILL_MESSAGE, + DEFAULT_BIRTH, + DEFAULT_DISCOVERY, + DEFAULT_ENCODING, + DEFAULT_PREFIX, + DEFAULT_QOS, + DEFAULT_RETAIN, + DEFAULT_WILL, + PLATFORMS, + PROTOCOL_31, + PROTOCOL_311, +) +from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic + +DEFAULT_PORT = 1883 +DEFAULT_KEEPALIVE = 60 +DEFAULT_PROTOCOL = PROTOCOL_311 +DEFAULT_TLS_PROTOCOL = "auto" + +DEFAULT_VALUES = { + CONF_BIRTH_MESSAGE: DEFAULT_BIRTH, + CONF_DISCOVERY: DEFAULT_DISCOVERY, + CONF_PORT: DEFAULT_PORT, + CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL, + CONF_WILL_MESSAGE: DEFAULT_WILL, +} + +CLIENT_KEY_AUTH_MSG = ( + "client_key and client_cert must both be present in " + "the MQTT broker configuration" +) + +MQTT_WILL_BIRTH_SCHEMA = vol.Schema( + { + vol.Inclusive(ATTR_TOPIC, "topic_payload"): valid_publish_topic, + vol.Inclusive(ATTR_PAYLOAD, "topic_payload"): cv.string, + vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + }, + required=True, +) + +PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( + {vol.Optional(platform.value): cv.ensure_list for platform in PLATFORMS} +) + +CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend( + { + vol.Optional(CONF_CLIENT_ID): cv.string, + vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( + vol.Coerce(int), vol.Range(min=15) + ), + vol.Optional(CONF_BROKER): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), + vol.Inclusive( + CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG + ): cv.isfile, + vol.Inclusive( + CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG + ): cv.isfile, + vol.Optional(CONF_TLS_INSECURE): cv.boolean, + vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"), + vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( + cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) + ), + vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_DISCOVERY): cv.boolean, + # discovery_prefix must be a valid publish topic because if no + # state topic is specified, it will be created with the given prefix. + vol.Optional( + CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX + ): valid_publish_topic, + } +) + +DEPRECATED_CONFIG_KEYS = [ + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_DISCOVERY, + CONF_PASSWORD, + CONF_PORT, + CONF_TLS_VERSION, + CONF_USERNAME, + CONF_WILL_MESSAGE, +] + +SCHEMA_BASE = { + vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, +} + +MQTT_BASE_SCHEMA = vol.Schema(SCHEMA_BASE) + +# Sensor type platforms subscribe to MQTT events +MQTT_RO_SCHEMA = MQTT_BASE_SCHEMA.extend( + { + vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + } +) + +# Switch type platforms publish to MQTT and may subscribe +MQTT_RW_SCHEMA = MQTT_BASE_SCHEMA.extend( + { + vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, + } +) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 0a763e850e5..822ae712573 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -17,7 +17,7 @@ from homeassistant.const import ( ) from homeassistant.data_entry_flow import FlowResult -from . import MqttClientSetup +from .client import MqttClientSetup from .const import ( ATTR_PAYLOAD, ATTR_QOS, diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 106d0310158..2f7e27e7252 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -1,5 +1,5 @@ """Constants used by multiple MQTT modules.""" -from homeassistant.const import CONF_PAYLOAD +from homeassistant.const import CONF_PAYLOAD, Platform ATTR_DISCOVERY_HASH = "discovery_hash" ATTR_DISCOVERY_PAYLOAD = "discovery_payload" @@ -14,7 +14,9 @@ CONF_BROKER = "broker" CONF_BIRTH_MESSAGE = "birth_message" CONF_COMMAND_TEMPLATE = "command_template" CONF_COMMAND_TOPIC = "command_topic" +CONF_DISCOVERY_PREFIX = "discovery_prefix" CONF_ENCODING = "encoding" +CONF_KEEPALIVE = "keepalive" CONF_QOS = ATTR_QOS CONF_RETAIN = ATTR_RETAIN CONF_STATE_TOPIC = "state_topic" @@ -30,6 +32,7 @@ CONF_TLS_VERSION = "tls_version" CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" +DATA_MQTT = "mqtt" DATA_MQTT_CONFIG = "mqtt_config" DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" @@ -66,3 +69,24 @@ PAYLOAD_NONE = "None" PROTOCOL_31 = "3.1" PROTOCOL_311 = "3.1.1" + +PLATFORMS = [ + Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.CAMERA, + Platform.CLIMATE, + Platform.DEVICE_TRACKER, + Platform.COVER, + Platform.FAN, + Platform.HUMIDIFIER, + Platform.LIGHT, + Platform.LOCK, + Platform.NUMBER, + Platform.SELECT, + Platform.SCENE, + Platform.SENSOR, + Platform.SIREN, + Platform.SWITCH, + Platform.VACUUM, +] diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 8e36329946a..5814f3e43f7 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -33,8 +33,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_BASE_SCHEMA from .const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -51,6 +51,8 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate +from .util import valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -152,11 +154,11 @@ def validate_options(value): return value -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( { - vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_GET_POSITION_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_GET_POSITION_TOPIC): 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( @@ -172,24 +174,24 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_SCHEMA.extend( 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_SET_POSITION_TOPIC): 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_STATE_TOPIC): 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_COMMAND_TOPIC): 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_TOPIC): 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, diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py index 002ae6e3991..0646a5bda0c 100644 --- a/homeassistant/components/mqtt/device_automation.py +++ b/homeassistant/components/mqtt/device_automation.py @@ -6,7 +6,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from . import device_trigger -from .. import mqtt +from .config import MQTT_BASE_SCHEMA from .mixins import async_setup_entry_helper AUTOMATION_TYPE_TRIGGER = "trigger" @@ -17,7 +17,7 @@ CONF_AUTOMATION_TYPE = "automation_type" PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( {vol.Required(CONF_AUTOMATION_TYPE): AUTOMATION_TYPES_SCHEMA}, extra=vol.ALLOW_EXTRA, -).extend(mqtt.MQTT_BASE_SCHEMA.schema) +).extend(MQTT_BASE_SCHEMA.schema) async def async_setup_entry(hass, config_entry): diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index aa7506bd5e3..1b48e15b80e 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -19,8 +19,8 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .. import MqttValueTemplate, subscription -from ... import mqtt +from .. import subscription +from ..config import MQTT_RO_SCHEMA from ..const import CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages from ..mixins import ( @@ -29,12 +29,13 @@ from ..mixins import ( async_get_platform_config_from_yaml, async_setup_entry_helper, ) +from ..models import MqttValueTemplate CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" CONF_SOURCE_TYPE = "source_type" -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RO_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, diff --git a/homeassistant/components/mqtt/device_tracker/schema_yaml.py b/homeassistant/components/mqtt/device_tracker/schema_yaml.py index f871ac89c2d..2dfa5b7134c 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_yaml.py +++ b/homeassistant/components/mqtt/device_tracker/schema_yaml.py @@ -7,16 +7,18 @@ from homeassistant.const import CONF_DEVICES, STATE_HOME, STATE_NOT_HOME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from ... import mqtt +from ..client import async_subscribe +from ..config import SCHEMA_BASE from ..const import CONF_QOS +from ..util import valid_subscribe_topic CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" CONF_SOURCE_TYPE = "source_type" -PLATFORM_SCHEMA_YAML = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( +PLATFORM_SCHEMA_YAML = PLATFORM_SCHEMA.extend(SCHEMA_BASE).extend( { - vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}, + vol.Required(CONF_DEVICES): {cv.string: valid_subscribe_topic}, vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string, vol.Optional(CONF_SOURCE_TYPE): vol.In(SOURCE_TYPES), @@ -50,6 +52,6 @@ async def async_setup_scanner_from_yaml(hass, config, async_see, discovery_info= hass.async_create_task(async_see(**see_args)) - await mqtt.async_subscribe(hass, topic, async_message_received, qos) + await async_subscribe(hass, topic, async_message_received, qos) return True diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 2c6c6ecc3ba..0b4bcbfcbc2 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -29,7 +29,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType from . import debug_info, trigger as mqtt_trigger -from .. import mqtt +from .config import MQTT_BASE_SCHEMA from .const import ( ATTR_DISCOVERY_HASH, CONF_ENCODING, @@ -71,7 +71,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( } ) -TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_SCHEMA.extend( +TRIGGER_DISCOVERY_SCHEMA = MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_AUTOMATION_TYPE): str, vol.Required(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -101,7 +101,7 @@ class TriggerInstance: async def async_attach_trigger(self) -> None: """Attach MQTT trigger.""" mqtt_config = { - CONF_PLATFORM: mqtt.DOMAIN, + CONF_PLATFORM: DOMAIN, CONF_TOPIC: self.trigger.topic, CONF_ENCODING: DEFAULT_ENCODING, CONF_QOS: self.trigger.qos, diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index f2b738cd2bb..f72b0bdf689 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -34,8 +34,8 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -55,6 +55,8 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate +from .util import valid_publish_topic, valid_subscribe_topic CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic" CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic" @@ -125,28 +127,28 @@ def valid_preset_mode_configuration(config): return config -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = MQTT_RW_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_TOPIC): 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_STATE_TOPIC): 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_TOPIC): 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_STATE_TOPIC): 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, + ): 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_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_PRESET_MODE_VALUE_TEMPLATE): cv.template, vol.Optional( CONF_SPEED_RANGE_MIN, default=DEFAULT_SPEED_RANGE_MIN @@ -168,8 +170,8 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( 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_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_SPEED_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_SPEED_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_SPEED_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, } diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index f6d4aa01dab..000a9b9700e 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -30,8 +30,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -51,6 +51,8 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate +from .util import valid_publish_topic, valid_subscribe_topic CONF_AVAILABLE_MODES_LIST = "modes" CONF_DEVICE_CLASS = "device_class" @@ -103,15 +105,13 @@ def valid_humidity_range_configuration(config): return config -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = MQTT_RW_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.Inclusive(CONF_MODE_COMMAND_TOPIC, "available_modes"): valid_publish_topic, vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional( CONF_DEVICE_CLASS, default=HumidifierDeviceClass.HUMIDIFIER @@ -119,14 +119,14 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( [HumidifierDeviceClass.HUMIDIFIER, HumidifierDeviceClass.DEHUMIDIFIER] ), vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_MODE_STATE_TOPIC): 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.Required(CONF_TARGET_HUMIDITY_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE): cv.template, vol.Optional( CONF_TARGET_HUMIDITY_MAX, default=DEFAULT_MAX_HUMIDITY @@ -135,7 +135,7 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( 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_TARGET_HUMIDITY_STATE_TOPIC): valid_subscribe_topic, vol.Optional( CONF_PAYLOAD_RESET_HUMIDITY, default=DEFAULT_PAYLOAD_RESET ): cv.string, diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index eb4ec264981..1c94fa82f73 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -42,8 +42,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util -from .. import MqttCommandTemplate, MqttValueTemplate, subscription -from ... import mqtt +from .. import subscription +from ..config import MQTT_RW_SCHEMA from ..const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -55,6 +55,8 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity +from ..models import MqttCommandTemplate, MqttValueTemplate +from ..util import valid_publish_topic, valid_subscribe_topic from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -156,28 +158,28 @@ VALUE_TEMPLATE_KEYS = [ ] _PLATFORM_SCHEMA_BASE = ( - mqtt.MQTT_RW_SCHEMA.extend( + MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_BRIGHTNESS_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): valid_publish_topic, vol.Optional( CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE ): vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_BRIGHTNESS_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_COLOR_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_COLOR_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_COLOR_MODE_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_COLOR_TEMP_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_COLOR_TEMP_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_EFFECT_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_EFFECT_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_EFFECT_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_EFFECT_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_EFFECT_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_EFFECT_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_HS_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_HS_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_HS_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_HS_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_HS_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_MAX_MIREDS): cv.positive_int, vol.Optional(CONF_MIN_MIREDS): cv.positive_int, @@ -189,30 +191,30 @@ _PLATFORM_SCHEMA_BASE = ( 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_RGB_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_RGB_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_RGB_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_RGB_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_RGB_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_RGBW_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_RGBW_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_RGBW_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_RGBW_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_RGBW_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_RGBW_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_RGBWW_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_RGBWW_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_RGBWW_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_RGBWW_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_RGBWW_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_RGBWW_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_WHITE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_WHITE_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_WHITE_SCALE, default=DEFAULT_WHITE_SCALE): vol.All( vol.Coerce(int), vol.Range(min=1) ), - vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): valid_publish_topic, vol.Optional( CONF_WHITE_VALUE_SCALE, default=DEFAULT_WHITE_VALUE_SCALE ): vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Optional(CONF_WHITE_VALUE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_WHITE_VALUE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_XY_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_XY_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_XY_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_XY_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_XY_VALUE_TEMPLATE): cv.template, }, ) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 2049818ab31..be49f1ad2e3 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -51,7 +51,7 @@ from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util from .. import subscription -from ... import mqtt +from ..config import DEFAULT_QOS, DEFAULT_RETAIN, MQTT_RW_SCHEMA from ..const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -61,6 +61,7 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity +from ..util import valid_subscribe_topic from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import CONF_BRIGHTNESS_SCALE, MQTT_LIGHT_ATTRIBUTES_BLOCKED @@ -103,7 +104,7 @@ def valid_color_configuration(config): _PLATFORM_SCHEMA_BASE = ( - mqtt.MQTT_RW_SCHEMA.extend( + MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_BRIGHTNESS, default=DEFAULT_BRIGHTNESS): cv.boolean, vol.Optional( @@ -126,12 +127,12 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_MIN_MIREDS): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS): vol.All( + vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All( vol.Coerce(int), vol.In([0, 1, 2]) ), - vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_RGB, default=DEFAULT_RGB): cv.boolean, - vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, vol.Inclusive(CONF_SUPPORTED_COLOR_MODES, "color_mode"): vol.All( cv.ensure_list, [vol.In(VALID_COLOR_MODES)], diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 0165bfc8efa..779f2f17e24 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -31,8 +31,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util -from .. import MqttValueTemplate, subscription -from ... import mqtt +from .. import subscription +from ..config import MQTT_RW_SCHEMA from ..const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -43,6 +43,7 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity +from ..models import MqttValueTemplate from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import MQTT_LIGHT_ATTRIBUTES_BLOCKED @@ -67,7 +68,7 @@ CONF_RED_TEMPLATE = "red_template" CONF_WHITE_VALUE_TEMPLATE = "white_value_template" _PLATFORM_SCHEMA_BASE = ( - mqtt.MQTT_RW_SCHEMA.extend( + MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_BLUE_TEMPLATE): cv.template, vol.Optional(CONF_BRIGHTNESS_TEMPLATE): cv.template, diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 5dc0a974d26..0cfd1d2b70f 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -15,8 +15,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -33,6 +33,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttValueTemplate CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_UNLOCK = "payload_unlock" @@ -56,7 +57,7 @@ MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset( } ) -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index a46debeae54..694fae0b3c0 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -49,14 +49,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import ( - DATA_MQTT, - PLATFORMS, - MqttValueTemplate, - async_publish, - debug_info, - subscription, -) +from . import debug_info, subscription +from .client import async_publish from .const import ( ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_PAYLOAD, @@ -65,6 +59,7 @@ from .const import ( CONF_ENCODING, CONF_QOS, CONF_TOPIC, + DATA_MQTT, DATA_MQTT_CONFIG, DATA_MQTT_RELOAD_NEEDED, DEFAULT_ENCODING, @@ -73,6 +68,7 @@ from .const import ( DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, + PLATFORMS, ) from .debug_info import log_message, log_messages from .discovery import ( @@ -82,7 +78,7 @@ from .discovery import ( clear_discovery_hash, set_discovery_hash, ) -from .models import PublishPayloadType, ReceiveMessage +from .models import MqttValueTemplate, PublishPayloadType, ReceiveMessage from .subscription import ( async_prepare_subscribe_topics, async_subscribe_topics, diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 9cec65d7254..9bce6baab8b 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -1,12 +1,21 @@ """Models used by multiple MQTT modules.""" from __future__ import annotations +from ast import literal_eval from collections.abc import Awaitable, Callable import datetime as dt -from typing import Union +from typing import Any, Union import attr +from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import template +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import TemplateVarsType + +_SENTINEL = object() + PublishPayloadType = Union[str, bytes, int, float, None] ReceivePayloadType = Union[str, bytes] @@ -35,3 +44,118 @@ class ReceiveMessage: AsyncMessageCallbackType = Callable[[ReceiveMessage], Awaitable[None]] MessageCallbackType = Callable[[ReceiveMessage], None] + + +class MqttCommandTemplate: + """Class for rendering MQTT payload with command templates.""" + + def __init__( + self, + command_template: template.Template | None, + *, + hass: HomeAssistant | None = None, + entity: Entity | None = None, + ) -> None: + """Instantiate a command template.""" + self._attr_command_template = command_template + if command_template is None: + return + + self._entity = entity + + command_template.hass = hass + + if entity: + command_template.hass = entity.hass + + @callback + def async_render( + self, + value: PublishPayloadType = None, + variables: TemplateVarsType = None, + ) -> PublishPayloadType: + """Render or convert the command template with given value or variables.""" + + def _convert_outgoing_payload( + payload: PublishPayloadType, + ) -> PublishPayloadType: + """Ensure correct raw MQTT payload is passed as bytes for publishing.""" + if isinstance(payload, str): + try: + native_object = literal_eval(payload) + if isinstance(native_object, bytes): + return native_object + + except (ValueError, TypeError, SyntaxError, MemoryError): + pass + + return payload + + if self._attr_command_template is None: + return value + + values = {"value": value} + if self._entity: + values[ATTR_ENTITY_ID] = self._entity.entity_id + values[ATTR_NAME] = self._entity.name + if variables is not None: + values.update(variables) + return _convert_outgoing_payload( + self._attr_command_template.async_render(values, parse_result=False) + ) + + +class MqttValueTemplate: + """Class for rendering MQTT value template with possible json values.""" + + def __init__( + self, + value_template: template.Template | None, + *, + hass: HomeAssistant | None = None, + entity: Entity | None = None, + config_attributes: TemplateVarsType = None, + ) -> None: + """Instantiate a value template.""" + self._value_template = value_template + self._config_attributes = config_attributes + if value_template is None: + return + + value_template.hass = hass + self._entity = entity + + if entity: + value_template.hass = entity.hass + + @callback + def async_render_with_possible_json_value( + self, + payload: ReceivePayloadType, + default: ReceivePayloadType | object = _SENTINEL, + variables: TemplateVarsType = None, + ) -> ReceivePayloadType: + """Render with possible json value or pass-though a received MQTT value.""" + if self._value_template is None: + return payload + + values: dict[str, Any] = {} + + if variables is not None: + values.update(variables) + + if self._config_attributes is not None: + values.update(self._config_attributes) + + if self._entity: + values[ATTR_ENTITY_ID] = self._entity.entity_id + values[ATTR_NAME] = self._entity.name + + if default == _SENTINEL: + return self._value_template.async_render_with_possible_json_value( + payload, variables=values + ) + + return self._value_template.async_render_with_possible_json_value( + payload, default, variables=values + ) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 001f9f4f668..6ea1f0959f6 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -27,8 +27,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -46,6 +46,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate _LOGGER = logging.getLogger(__name__) @@ -75,7 +76,7 @@ def validate_config(config): return config -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): vol.Coerce(float), diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 98c692ceaff..ce8f0b0a3e8 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -15,7 +15,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .. import mqtt +from .client import async_publish +from .config import MQTT_BASE_SCHEMA from .const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN from .mixins import ( CONF_ENABLED_BY_DEFAULT, @@ -27,13 +28,14 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .util import valid_publish_topic DEFAULT_NAME = "MQTT Scene" DEFAULT_RETAIN = False -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { - vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_ON): cv.string, @@ -128,7 +130,7 @@ class MqttScene( This method is a coroutine. """ - await mqtt.async_publish( + await async_publish( self.hass, self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_ON], diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 0765eb7f176..75e1b4e8efd 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -17,8 +17,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -36,6 +36,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate _LOGGER = logging.getLogger(__name__) @@ -51,7 +52,7 @@ MQTT_SELECT_ATTRIBUTES_BLOCKED = frozenset( ) -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index d865d90c4ee..4dd1ad4d95f 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -34,8 +34,8 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util -from . import MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RO_SCHEMA from .const import CONF_ENCODING, CONF_QOS, CONF_STATE_TOPIC from .debug_info import log_messages from .mixins import ( @@ -47,6 +47,8 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttValueTemplate +from .util import valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -89,12 +91,12 @@ def validate_options(conf): return conf -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RO_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = MQTT_RO_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_TOPIC): 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, diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index c3a41c3618e..1ecf2c37dbf 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -35,8 +35,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -57,6 +57,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate DEFAULT_NAME = "MQTT Siren" DEFAULT_PAYLOAD_ON = "ON" @@ -74,7 +75,7 @@ CONF_SUPPORT_VOLUME_SET = "support_volume_set" STATE = "state" -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_AVAILABLE_TONES): cv.ensure_list, vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index f5f8363eb33..c20ddfe5151 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -24,8 +24,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -43,6 +43,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttValueTemplate DEFAULT_NAME = "MQTT Switch" DEFAULT_PAYLOAD_ON = "ON" @@ -51,7 +52,7 @@ DEFAULT_OPTIMISTIC = False CONF_STATE_ON = "state_on" CONF_STATE_OFF = "state_off" -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 25e49524b8f..9452d5fc259 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -11,8 +11,8 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from . import MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_BASE_SCHEMA from .const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_TOPIC from .mixins import ( MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -21,7 +21,7 @@ from .mixins import ( send_discovery_done, update_device, ) -from .models import ReceiveMessage +from .models import MqttValueTemplate, ReceiveMessage from .subscription import EntitySubscription from .util import valid_subscribe_topic @@ -30,7 +30,7 @@ LOG_NAME = "Tag" TAG = "tag" TAGS = "mqtt_tags" -PLATFORM_SCHEMA = mqtt.MQTT_BASE_SCHEMA.extend( +PLATFORM_SCHEMA = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_PLATFORM): "mqtt", diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index eb5e01b6251..f25131c43b7 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -15,11 +15,13 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.icon import icon_for_battery_level -from .. import MqttValueTemplate, subscription -from ... import mqtt +from .. import subscription +from ..config import MQTT_BASE_SCHEMA from ..const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema +from ..models import MqttValueTemplate +from ..util import valid_publish_topic from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -96,25 +98,23 @@ MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED = MQTT_VACUUM_ATTRIBUTES_BLOCKED | frozens ) PLATFORM_SCHEMA_LEGACY_MODERN = ( - mqtt.MQTT_BASE_SCHEMA.extend( + MQTT_BASE_SCHEMA.extend( { vol.Inclusive(CONF_BATTERY_LEVEL_TEMPLATE, "battery"): cv.template, - vol.Inclusive( - CONF_BATTERY_LEVEL_TOPIC, "battery" - ): mqtt.valid_publish_topic, + vol.Inclusive(CONF_BATTERY_LEVEL_TOPIC, "battery"): valid_publish_topic, vol.Inclusive(CONF_CHARGING_TEMPLATE, "charging"): cv.template, - vol.Inclusive(CONF_CHARGING_TOPIC, "charging"): mqtt.valid_publish_topic, + vol.Inclusive(CONF_CHARGING_TOPIC, "charging"): valid_publish_topic, vol.Inclusive(CONF_CLEANING_TEMPLATE, "cleaning"): cv.template, - vol.Inclusive(CONF_CLEANING_TOPIC, "cleaning"): mqtt.valid_publish_topic, + vol.Inclusive(CONF_CLEANING_TOPIC, "cleaning"): valid_publish_topic, vol.Inclusive(CONF_DOCKED_TEMPLATE, "docked"): cv.template, - vol.Inclusive(CONF_DOCKED_TOPIC, "docked"): mqtt.valid_publish_topic, + vol.Inclusive(CONF_DOCKED_TOPIC, "docked"): valid_publish_topic, vol.Inclusive(CONF_ERROR_TEMPLATE, "error"): cv.template, - vol.Inclusive(CONF_ERROR_TOPIC, "error"): mqtt.valid_publish_topic, + vol.Inclusive(CONF_ERROR_TOPIC, "error"): valid_publish_topic, vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All( cv.ensure_list, [cv.string] ), vol.Inclusive(CONF_FAN_SPEED_TEMPLATE, "fan_speed"): cv.template, - vol.Inclusive(CONF_FAN_SPEED_TOPIC, "fan_speed"): mqtt.valid_publish_topic, + vol.Inclusive(CONF_FAN_SPEED_TOPIC, "fan_speed"): valid_publish_topic, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional( CONF_PAYLOAD_CLEAN_SPOT, default=DEFAULT_PAYLOAD_CLEAN_SPOT @@ -135,12 +135,12 @@ PLATFORM_SCHEMA_LEGACY_MODERN = ( vol.Optional( CONF_PAYLOAD_TURN_ON, default=DEFAULT_PAYLOAD_TURN_ON ): cv.string, - vol.Optional(CONF_SEND_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_SET_FAN_SPEED_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_SEND_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_SET_FAN_SPEED_TOPIC): valid_publish_topic, vol.Optional( CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS ): vol.All(cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())]), - vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } ) diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 7aa7be07797..3d670780994 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -23,7 +23,7 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from .. import subscription -from ... import mqtt +from ..config import MQTT_BASE_SCHEMA from ..const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -33,6 +33,7 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema +from ..util import valid_publish_topic from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -105,7 +106,7 @@ DEFAULT_PAYLOAD_START = "start" DEFAULT_PAYLOAD_PAUSE = "pause" PLATFORM_SCHEMA_STATE_MODERN = ( - mqtt.MQTT_BASE_SCHEMA.extend( + MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All( cv.ensure_list, [cv.string] @@ -123,13 +124,13 @@ PLATFORM_SCHEMA_STATE_MODERN = ( vol.Optional(CONF_PAYLOAD_START, default=DEFAULT_PAYLOAD_START): cv.string, vol.Optional(CONF_PAYLOAD_PAUSE, default=DEFAULT_PAYLOAD_PAUSE): cv.string, vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string, - vol.Optional(CONF_SEND_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_SET_FAN_SPEED_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_STATE_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_SEND_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_SET_FAN_SPEED_TOPIC): valid_publish_topic, + vol.Optional(CONF_STATE_TOPIC): valid_publish_topic, vol.Optional( CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS ): vol.All(cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())]), - vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } ) @@ -178,7 +179,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): supported_feature_strings, STRING_TO_SERVICE ) self._fan_speed_list = config[CONF_FAN_SPEED_LIST] - 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_json/device_tracker.py b/homeassistant/components/mqtt_json/device_tracker.py index 505fa3bd809..1d99e6d7b6f 100644 --- a/homeassistant/components/mqtt_json/device_tracker.py +++ b/homeassistant/components/mqtt_json/device_tracker.py @@ -35,7 +35,7 @@ GPS_JSON_PAYLOAD_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(mqtt.config.SCHEMA_BASE).extend( {vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}} ) diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index 54de561c11e..276695d8edd 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_AWAY_TIMEOUT, default=DEFAULT_AWAY_TIMEOUT): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, } -).extend(mqtt.MQTT_RO_SCHEMA.schema) +).extend(mqtt.config.MQTT_RO_SCHEMA.schema) MQTT_PAYLOAD = vol.Schema( vol.All( diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 285af765ab4..e130b820c1b 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -12,7 +12,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, ) -from homeassistant.components.mqtt import CONF_STATE_TOPIC +from homeassistant.components.mqtt.const import CONF_STATE_TOPIC from homeassistant.components.mqtt.cover import ( CONF_GET_POSITION_TEMPLATE, CONF_GET_POSITION_TOPIC, diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 07c39d70df0..aa0bfb82608 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1123,7 +1123,7 @@ async def test_restore_subscriptions_on_reconnect(hass, mqtt_client_mock, mqtt_m assert mqtt_client_mock.subscribe.call_count == 1 mqtt_client_mock.on_disconnect(None, None, 0) - with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0): + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0): mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() assert mqtt_client_mock.subscribe.call_count == 2 @@ -1157,7 +1157,7 @@ async def test_restore_all_active_subscriptions_on_reconnect( assert mqtt_client_mock.unsubscribe.call_count == 0 mqtt_client_mock.on_disconnect(None, None, 0) - with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0): + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0): mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() @@ -1188,7 +1188,7 @@ async def test_logs_error_if_no_connect_broker( ) -@patch("homeassistant.components.mqtt.TIMEOUT_ACK", 0.3) +@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.3) async def test_handle_mqtt_on_callback(hass, caplog, mqtt_mock, mqtt_client_mock): """Test receiving an ACK callback before waiting for it.""" # Simulate an ACK for mid == 1, this will call mqtt_mock._mqtt_handle_mid(mid) @@ -1331,7 +1331,7 @@ async def test_setup_mqtt_client_protocol(hass): """Test MQTT client protocol setup.""" entry = MockConfigEntry( domain=mqtt.DOMAIN, - data={mqtt.CONF_BROKER: "test-broker", mqtt.CONF_PROTOCOL: "3.1"}, + data={mqtt.CONF_BROKER: "test-broker", mqtt.config.CONF_PROTOCOL: "3.1"}, ) with patch("paho.mqtt.client.Client") as mock_client: mock_client.on_connect(return_value=0) @@ -1341,7 +1341,7 @@ async def test_setup_mqtt_client_protocol(hass): assert mock_client.call_args[1]["protocol"] == 3 -@patch("homeassistant.components.mqtt.TIMEOUT_ACK", 0.2) +@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.2) async def test_handle_mqtt_timeout_on_callback(hass, caplog): """Test publish without receiving an ACK callback.""" mid = 0 @@ -1486,7 +1486,7 @@ async def test_custom_birth_message(hass, mqtt_client_mock, mqtt_mock): """Handle birth message.""" birth.set() - with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0.1): + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): await mqtt.async_subscribe(hass, "birth", wait_birth) mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() @@ -1516,7 +1516,7 @@ async def test_default_birth_message(hass, mqtt_client_mock, mqtt_mock): """Handle birth message.""" birth.set() - with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0.1): + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() @@ -1532,7 +1532,7 @@ async def test_default_birth_message(hass, mqtt_client_mock, mqtt_mock): ) async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock): """Test disabling birth message.""" - with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0.1): + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() await asyncio.sleep(0.2) @@ -1580,7 +1580,7 @@ async def test_delayed_birth_message(hass, mqtt_client_mock, mqtt_config, mqtt_m """Handle birth message.""" birth.set() - with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0.1): + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index f451079e0f0..b7a3b5f2118 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest from homeassistant.components import vacuum -from homeassistant.components.mqtt import CONF_COMMAND_TOPIC +from homeassistant.components.mqtt.const import CONF_COMMAND_TOPIC from homeassistant.components.mqtt.vacuum import schema_legacy as mqttvacuum from homeassistant.components.mqtt.vacuum.schema import services_to_strings from homeassistant.components.mqtt.vacuum.schema_legacy import ( diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 3f752f1b528..c1017446eff 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest from homeassistant.components import vacuum -from homeassistant.components.mqtt import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC +from homeassistant.components.mqtt.const import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC from homeassistant.components.mqtt.vacuum import CONF_SCHEMA, schema_state as mqttvacuum from homeassistant.components.mqtt.vacuum.const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from homeassistant.components.mqtt.vacuum.schema import services_to_strings From db9c586404d8fb0e520e731ccb0229d08ffd7161 Mon Sep 17 00:00:00 2001 From: Thijs W Date: Tue, 31 May 2022 09:56:25 +0200 Subject: [PATCH 1088/3516] Address late comments for frontier silicon (#72745) Co-authored-by: Martin Hjelmare --- .../frontier_silicon/media_player.py | 53 +++++++++---------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 66d1f304b1e..e4a67fa5cc4 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -22,8 +22,6 @@ from homeassistant.const import ( STATE_OPENING, STATE_PAUSED, STATE_PLAYING, - STATE_UNAVAILABLE, - STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -74,12 +72,13 @@ async def async_setup_platform( webfsapi_url = await AFSAPI.get_webfsapi_endpoint( f"http://{host}:{port}/device" ) - afsapi = AFSAPI(webfsapi_url, password) - async_add_entities([AFSAPIDevice(name, afsapi)], True) except FSConnectionError: _LOGGER.error( "Could not add the FSAPI device at %s:%s -> %s", host, port, password ) + return + afsapi = AFSAPI(webfsapi_url, password) + async_add_entities([AFSAPIDevice(name, afsapi)], True) class AFSAPIDevice(MediaPlayerEntity): @@ -188,7 +187,7 @@ class AFSAPIDevice(MediaPlayerEntity): PlayState.STOPPED: STATE_IDLE, PlayState.LOADING: STATE_OPENING, None: STATE_IDLE, - }.get(status, STATE_UNKNOWN) + }.get(status) else: self._state = STATE_OFF except FSConnectionError: @@ -197,32 +196,32 @@ class AFSAPIDevice(MediaPlayerEntity): "Could not connect to %s. Did it go offline?", self._name or afsapi.webfsapi_endpoint, ) - self._state = STATE_UNAVAILABLE self._attr_available = False - else: - if not self._attr_available: - _LOGGER.info( - "Reconnected to %s", - self._name or afsapi.webfsapi_endpoint, - ) + return - self._attr_available = True - if not self._name: - self._name = await afsapi.get_friendly_name() + if not self._attr_available: + _LOGGER.info( + "Reconnected to %s", + self._name or afsapi.webfsapi_endpoint, + ) - if not self._source_list: - self.__modes_by_label = { - mode.label: mode.key for mode in await afsapi.get_modes() - } - self._source_list = list(self.__modes_by_label.keys()) + self._attr_available = True + if not self._name: + self._name = await afsapi.get_friendly_name() - # The API seems to include 'zero' in the number of steps (e.g. if the range is - # 0-40 then get_volume_steps returns 41) subtract one to get the max volume. - # If call to get_volume fails set to 0 and try again next time. - if not self._max_volume: - self._max_volume = int(await afsapi.get_volume_steps() or 1) - 1 + if not self._source_list: + self.__modes_by_label = { + mode.label: mode.key for mode in await afsapi.get_modes() + } + self._source_list = list(self.__modes_by_label) - if self._state not in [STATE_OFF, STATE_UNAVAILABLE]: + # The API seems to include 'zero' in the number of steps (e.g. if the range is + # 0-40 then get_volume_steps returns 41) subtract one to get the max volume. + # If call to get_volume fails set to 0 and try again next time. + if not self._max_volume: + self._max_volume = int(await afsapi.get_volume_steps() or 1) - 1 + + if self._state != STATE_OFF: info_name = await afsapi.get_play_name() info_text = await afsapi.get_play_text() @@ -269,7 +268,7 @@ class AFSAPIDevice(MediaPlayerEntity): async def async_media_play_pause(self): """Send play/pause command.""" - if "playing" in self._state: + if self._state == STATE_PLAYING: await self.fs_device.pause() else: await self.fs_device.play() From 627d6f7803728cbd188f7a7060d8b2c747a18555 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 31 May 2022 10:33:34 +0200 Subject: [PATCH 1089/3516] Ensure description_placeholders is always typed (#72716) --- homeassistant/auth/providers/__init__.py | 2 +- .../components/homewizard/config_flow.py | 8 +++--- .../components/tankerkoenig/config_flow.py | 2 +- homeassistant/config_entries.py | 7 ++++-- homeassistant/data_entry_flow.py | 25 ++++++++++++------- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index 63389059051..6feb4b26759 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -272,7 +272,7 @@ class LoginFlow(data_entry_flow.FlowHandler): if not errors: return await self.async_finish(self.credential) - description_placeholders: dict[str, str | None] = { + description_placeholders: dict[str, str] = { "mfa_module_name": auth_module.name, "mfa_module_id": auth_module.id, } diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index df883baf3b1..7d06f08ce74 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, cast from homewizard_energy import HomeWizardEnergy from homewizard_energy.errors import DisabledError, UnsupportedError @@ -160,9 +160,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="discovery_confirm", description_placeholders={ - CONF_PRODUCT_TYPE: self.config[CONF_PRODUCT_TYPE], - CONF_SERIAL: self.config[CONF_SERIAL], - CONF_IP_ADDRESS: self.config[CONF_IP_ADDRESS], + CONF_PRODUCT_TYPE: cast(str, self.config[CONF_PRODUCT_TYPE]), + CONF_SERIAL: cast(str, self.config[CONF_SERIAL]), + CONF_IP_ADDRESS: cast(str, self.config[CONF_IP_ADDRESS]), }, ) diff --git a/homeassistant/components/tankerkoenig/config_flow.py b/homeassistant/components/tankerkoenig/config_flow.py index 345b034b027..dd5893fe35f 100644 --- a/homeassistant/components/tankerkoenig/config_flow.py +++ b/homeassistant/components/tankerkoenig/config_flow.py @@ -133,7 +133,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not user_input: return self.async_show_form( step_id="select_station", - description_placeholders={"stations_count": len(self._stations)}, + description_placeholders={"stations_count": str(len(self._stations))}, data_schema=vol.Schema( {vol.Required(CONF_STATIONS): cv.multi_select(self._stations)} ), diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 49b2059b2a2..df633009138 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1403,7 +1403,10 @@ class ConfigFlow(data_entry_flow.FlowHandler): @callback def async_abort( - self, *, reason: str, description_placeholders: dict | None = None + self, + *, + reason: str, + description_placeholders: Mapping[str, str] | None = None, ) -> data_entry_flow.FlowResult: """Abort the config flow.""" # Remove reauth notification if no reauth flows are in progress @@ -1477,7 +1480,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): title: str, data: Mapping[str, Any], description: str | None = None, - description_placeholders: dict | None = None, + description_placeholders: Mapping[str, str] | None = None, options: Mapping[str, Any] | None = None, ) -> data_entry_flow.FlowResult: """Finish config flow and create a config entry.""" diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 628a89dd89b..714cea07044 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -52,7 +52,7 @@ class AbortFlow(FlowError): """Exception to indicate a flow needs to be aborted.""" def __init__( - self, reason: str, description_placeholders: dict | None = None + self, reason: str, description_placeholders: Mapping[str, str] | None = None ) -> None: """Initialize an abort flow exception.""" super().__init__(f"Flow aborted: {reason}") @@ -75,7 +75,7 @@ class FlowResult(TypedDict, total=False): required: bool errors: dict[str, str] | None description: str | None - description_placeholders: dict[str, Any] | None + description_placeholders: Mapping[str, str | None] | None progress_action: str url: str reason: str @@ -422,7 +422,7 @@ class FlowHandler: step_id: str, data_schema: vol.Schema | None = None, errors: dict[str, str] | None = None, - description_placeholders: dict[str, Any] | None = None, + description_placeholders: Mapping[str, str | None] | None = None, last_step: bool | None = None, ) -> FlowResult: """Return the definition of a form to gather user input.""" @@ -444,7 +444,7 @@ class FlowHandler: title: str, data: Mapping[str, Any], description: str | None = None, - description_placeholders: dict | None = None, + description_placeholders: Mapping[str, str] | None = None, ) -> FlowResult: """Finish config flow and create a config entry.""" return { @@ -460,7 +460,10 @@ class FlowHandler: @callback def async_abort( - self, *, reason: str, description_placeholders: dict | None = None + self, + *, + reason: str, + description_placeholders: Mapping[str, str] | None = None, ) -> FlowResult: """Abort the config flow.""" return _create_abort_data( @@ -469,7 +472,11 @@ class FlowHandler: @callback def async_external_step( - self, *, step_id: str, url: str, description_placeholders: dict | None = None + self, + *, + step_id: str, + url: str, + description_placeholders: Mapping[str, str] | None = None, ) -> FlowResult: """Return the definition of an external step for the user to take.""" return { @@ -497,7 +504,7 @@ class FlowHandler: *, step_id: str, progress_action: str, - description_placeholders: dict | None = None, + description_placeholders: Mapping[str, str] | None = None, ) -> FlowResult: """Show a progress message to the user, without user input allowed.""" return { @@ -525,7 +532,7 @@ class FlowHandler: *, step_id: str, menu_options: list[str] | dict[str, str], - description_placeholders: dict | None = None, + description_placeholders: Mapping[str, str] | None = None, ) -> FlowResult: """Show a navigation menu to the user. @@ -547,7 +554,7 @@ def _create_abort_data( flow_id: str, handler: str, reason: str, - description_placeholders: dict | None = None, + description_placeholders: Mapping[str, str] | None = None, ) -> FlowResult: """Return the definition of an external step for the user to take.""" return { From ca5f13b576b8424cf66c59a0c9481576d9be1a34 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 31 May 2022 10:40:08 +0200 Subject: [PATCH 1090/3516] Allow removing a onewire device (#72710) --- homeassistant/components/onewire/__init__.py | 11 ++++ tests/components/onewire/__init__.py | 7 ++- tests/components/onewire/test_init.py | 66 +++++++++++++++++++- 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index c6f3d7dfa3f..e908a52a071 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -6,6 +6,7 @@ from pyownet import protocol from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from .const import DOMAIN, PLATFORMS from .onewirehub import CannotConnect, OneWireHub @@ -35,6 +36,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove a config entry from a device.""" + onewirehub: OneWireHub = hass.data[DOMAIN][config_entry.entry_id] + return not device_entry.identifiers.intersection( + (DOMAIN, device.id) for device in onewirehub.devices or [] + ) + + async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms( diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index d189db8af1a..c916c777248 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import ( ATTR_NAME, ATTR_STATE, ATTR_VIA_DEVICE, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceRegistry @@ -90,7 +91,7 @@ def check_entities( def setup_owproxy_mock_devices( - owproxy: MagicMock, platform: str, device_ids: list[str] + owproxy: MagicMock, platform: Platform, device_ids: list[str] ) -> None: """Set up mock for owproxy.""" main_dir_return_value = [] @@ -125,7 +126,7 @@ def _setup_owproxy_mock_device( main_read_side_effect: list, sub_read_side_effect: list, device_id: str, - platform: str, + platform: Platform, ) -> None: """Set up mock for owproxy.""" mock_device = MOCK_OWPROXY_DEVICES[device_id] @@ -167,7 +168,7 @@ def _setup_owproxy_mock_device_reads( sub_read_side_effect: list, mock_device: Any, device_id: str, - platform: str, + platform: Platform, ) -> None: """Set up mock for owproxy.""" # Setup device reads diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py index fecade521a8..bc09432ea5c 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -1,12 +1,35 @@ """Tests for 1-Wire config flow.""" -from unittest.mock import MagicMock +from collections.abc import Awaitable, Callable +from unittest.mock import MagicMock, patch +import aiohttp from pyownet import protocol import pytest from homeassistant.components.onewire.const import DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component + +from . import setup_owproxy_mock_devices + + +async def remove_device( + ws_client: aiohttp.ClientWebSocketResponse, device_id: str, config_entry_id: str +) -> bool: + """Remove config entry from a device.""" + await ws_client.send_json( + { + "id": 1, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry_id, + "device_id": device_id, + } + ) + response = await ws_client.receive_json() + return response["success"] @pytest.mark.usefixtures("owproxy_with_connerror") @@ -48,3 +71,44 @@ async def test_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): assert config_entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) + + +@patch("homeassistant.components.onewire.PLATFORMS", [Platform.SENSOR]) +async def test_registry_cleanup( + hass: HomeAssistant, + config_entry: ConfigEntry, + owproxy: MagicMock, + hass_ws_client: Callable[ + [HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse] + ], +): + """Test being able to remove a disconnected device.""" + assert await async_setup_component(hass, "config", {}) + + entry_id = config_entry.entry_id + device_registry = dr.async_get(hass) + live_id = "10.111111111111" + dead_id = "28.111111111111" + + # Initialise with two components + setup_owproxy_mock_devices(owproxy, Platform.SENSOR, [live_id, dead_id]) + await hass.config_entries.async_setup(entry_id) + await hass.async_block_till_done() + + # Reload with a device no longer on bus + setup_owproxy_mock_devices(owproxy, Platform.SENSOR, [live_id]) + await hass.config_entries.async_reload(entry_id) + await hass.async_block_till_done() + assert len(dr.async_entries_for_config_entry(device_registry, entry_id)) == 2 + + # Try to remove "10.111111111111" - fails as it is live + device = device_registry.async_get_device(identifiers={(DOMAIN, live_id)}) + assert await remove_device(await hass_ws_client(hass), device.id, entry_id) is False + assert len(dr.async_entries_for_config_entry(device_registry, entry_id)) == 2 + assert device_registry.async_get_device(identifiers={(DOMAIN, live_id)}) is not None + + # Try to remove "28.111111111111" - succeeds as it is dead + device = device_registry.async_get_device(identifiers={(DOMAIN, dead_id)}) + assert await remove_device(await hass_ws_client(hass), device.id, entry_id) is True + assert len(dr.async_entries_for_config_entry(device_registry, entry_id)) == 1 + assert device_registry.async_get_device(identifiers={(DOMAIN, dead_id)}) is None From 5dc4c89acc32eecfb5be37b7e9a8b15eb50fe010 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 May 2022 22:41:33 -1000 Subject: [PATCH 1091/3516] Small performance improvement for matching logbook rows (#72750) --- homeassistant/components/logbook/processor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index ea6002cc62c..b3a43c2ca35 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -407,7 +407,8 @@ class ContextAugmenter: def _rows_match(row: Row | EventAsRow, other_row: Row | EventAsRow) -> bool: """Check of rows match by using the same method as Events __hash__.""" if ( - (state_id := row.state_id) is not None + row is other_row + or (state_id := row.state_id) is not None and state_id == other_row.state_id or (event_id := row.event_id) is not None and event_id == other_row.event_id From cf17169b0ebf4c7a069a8a93b62a2a67786898c5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 31 May 2022 11:20:31 +0200 Subject: [PATCH 1092/3516] Refactor type-hint pylint plugin (#72692) * Cleanup unused variable * Adjust tests * Refactor _METHOD_MATCH dict * Remove duplicate function * Early exit * Undo object hint * METHOD > FUNCTION * Add comment * Remove extend * Remove break * Extract __any_platform__ * Add tests * Cache _PLATFORMS * Adjust tests * Review comments * mypy * shorthand --- pylint/plugins/hass_enforce_type_hints.py | 749 +++++++++++----------- tests/pylint/test_enforce_type_hints.py | 57 +- 2 files changed, 405 insertions(+), 401 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index e230c27b4ee..24b32f2e238 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -13,18 +13,19 @@ from homeassistant.const import Platform UNDEFINED = object() +_PLATFORMS: set[str] = {platform.value for platform in Platform} + @dataclass class TypeHintMatch: """Class for pattern matching.""" - module_filter: re.Pattern function_name: str arg_types: dict[int, str] - return_type: list[str] | str | None + return_type: list[str] | str | None | object -_TYPE_HINT_MATCHERS: dict[str, re.Pattern] = { +_TYPE_HINT_MATCHERS: dict[str, re.Pattern[str]] = { # a_or_b matches items such as "DiscoveryInfoType | None" "a_or_b": re.compile(r"^(\w+) \| (\w+)$"), # x_of_y matches items such as "Awaitable[None]" @@ -35,373 +36,333 @@ _TYPE_HINT_MATCHERS: dict[str, re.Pattern] = { "x_of_y_of_z_comma_a": re.compile(r"^(\w+)\[(\w+)\[(.*?]*), (.*?]*)\]\]$"), } -_MODULE_FILTERS: dict[str, re.Pattern] = { - # init matches only in the package root (__init__.py) - "init": re.compile(r"^homeassistant\.components\.\w+$"), - # any_platform matches any platform in the package root ({platform}.py) - "any_platform": re.compile( - f"^homeassistant\\.components\\.\\w+\\.({'|'.join([platform.value for platform in Platform])})$" - ), - # application_credentials matches only in the package root (application_credentials.py) - "application_credentials": re.compile( - r"^homeassistant\.components\.\w+\.(application_credentials)$" - ), - # backup matches only in the package root (backup.py) - "backup": re.compile(r"^homeassistant\.components\.\w+\.(backup)$"), - # cast matches only in the package root (cast.py) - "cast": re.compile(r"^homeassistant\.components\.\w+\.(cast)$"), - # config_flow matches only in the package root (config_flow.py) - "config_flow": re.compile(r"^homeassistant\.components\.\w+\.(config_flow)$"), - # device_action matches only in the package root (device_action.py) - "device_action": re.compile(r"^homeassistant\.components\.\w+\.(device_action)$"), - # device_condition matches only in the package root (device_condition.py) - "device_condition": re.compile( - r"^homeassistant\.components\.\w+\.(device_condition)$" - ), - # device_tracker matches only in the package root (device_tracker.py) - "device_tracker": re.compile(r"^homeassistant\.components\.\w+\.(device_tracker)$"), - # device_trigger matches only in the package root (device_trigger.py) - "device_trigger": re.compile(r"^homeassistant\.components\.\w+\.(device_trigger)$"), - # diagnostics matches only in the package root (diagnostics.py) - "diagnostics": re.compile(r"^homeassistant\.components\.\w+\.(diagnostics)$"), +_MODULE_REGEX: re.Pattern[str] = re.compile(r"^homeassistant\.components\.\w+(\.\w+)?$") + +_FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { + "__init__": [ + TypeHintMatch( + function_name="setup", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + }, + return_type="bool", + ), + TypeHintMatch( + function_name="async_setup", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + }, + return_type="bool", + ), + TypeHintMatch( + function_name="async_setup_entry", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigEntry", + }, + return_type="bool", + ), + TypeHintMatch( + function_name="async_remove_entry", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigEntry", + }, + return_type=None, + ), + TypeHintMatch( + function_name="async_unload_entry", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigEntry", + }, + return_type="bool", + ), + TypeHintMatch( + function_name="async_migrate_entry", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigEntry", + }, + return_type="bool", + ), + ], + "__any_platform__": [ + TypeHintMatch( + function_name="setup_platform", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + 2: "AddEntitiesCallback", + 3: "DiscoveryInfoType | None", + }, + return_type=None, + ), + TypeHintMatch( + function_name="async_setup_platform", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + 2: "AddEntitiesCallback", + 3: "DiscoveryInfoType | None", + }, + return_type=None, + ), + TypeHintMatch( + function_name="async_setup_entry", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigEntry", + 2: "AddEntitiesCallback", + }, + return_type=None, + ), + ], + "application_credentials": [ + TypeHintMatch( + function_name="async_get_auth_implementation", + arg_types={ + 0: "HomeAssistant", + 1: "str", + 2: "ClientCredential", + }, + return_type="AbstractOAuth2Implementation", + ), + TypeHintMatch( + function_name="async_get_authorization_server", + arg_types={ + 0: "HomeAssistant", + }, + return_type="AuthorizationServer", + ), + ], + "backup": [ + TypeHintMatch( + function_name="async_pre_backup", + arg_types={ + 0: "HomeAssistant", + }, + return_type=None, + ), + TypeHintMatch( + function_name="async_post_backup", + arg_types={ + 0: "HomeAssistant", + }, + return_type=None, + ), + ], + "cast": [ + TypeHintMatch( + function_name="async_get_media_browser_root_object", + arg_types={ + 0: "HomeAssistant", + 1: "str", + }, + return_type="list[BrowseMedia]", + ), + TypeHintMatch( + function_name="async_browse_media", + arg_types={ + 0: "HomeAssistant", + 1: "str", + 2: "str", + 3: "str", + }, + return_type=["BrowseMedia", "BrowseMedia | None"], + ), + TypeHintMatch( + function_name="async_play_media", + arg_types={ + 0: "HomeAssistant", + 1: "str", + 2: "Chromecast", + 3: "str", + 4: "str", + }, + return_type="bool", + ), + ], + "config_flow": [ + TypeHintMatch( + function_name="_async_has_devices", + arg_types={ + 0: "HomeAssistant", + }, + return_type="bool", + ), + ], + "device_action": [ + TypeHintMatch( + function_name="async_validate_action_config", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + }, + return_type="ConfigType", + ), + TypeHintMatch( + function_name="async_call_action_from_config", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + 2: "TemplateVarsType", + 3: "Context | None", + }, + return_type=None, + ), + TypeHintMatch( + function_name="async_get_action_capabilities", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + }, + return_type="dict[str, Schema]", + ), + TypeHintMatch( + function_name="async_get_actions", + arg_types={ + 0: "HomeAssistant", + 1: "str", + }, + return_type=["list[dict[str, str]]", "list[dict[str, Any]]"], + ), + ], + "device_condition": [ + TypeHintMatch( + function_name="async_validate_condition_config", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + }, + return_type="ConfigType", + ), + TypeHintMatch( + function_name="async_condition_from_config", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + }, + return_type="ConditionCheckerType", + ), + TypeHintMatch( + function_name="async_get_condition_capabilities", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + }, + return_type="dict[str, Schema]", + ), + TypeHintMatch( + function_name="async_get_conditions", + arg_types={ + 0: "HomeAssistant", + 1: "str", + }, + return_type=["list[dict[str, str]]", "list[dict[str, Any]]"], + ), + ], + "device_tracker": [ + TypeHintMatch( + function_name="setup_scanner", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + 2: "Callable[..., None]", + 3: "DiscoveryInfoType | None", + }, + return_type="bool", + ), + TypeHintMatch( + function_name="async_setup_scanner", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + 2: "Callable[..., Awaitable[None]]", + 3: "DiscoveryInfoType | None", + }, + return_type="bool", + ), + TypeHintMatch( + function_name="get_scanner", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + }, + return_type=["DeviceScanner", "DeviceScanner | None"], + ), + TypeHintMatch( + function_name="async_get_scanner", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + }, + return_type=["DeviceScanner", "DeviceScanner | None"], + ), + ], + "device_trigger": [ + TypeHintMatch( + function_name="async_validate_condition_config", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + }, + return_type="ConfigType", + ), + TypeHintMatch( + function_name="async_attach_trigger", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + 2: "AutomationActionType", + 3: "AutomationTriggerInfo", + }, + return_type="CALLBACK_TYPE", + ), + TypeHintMatch( + function_name="async_get_trigger_capabilities", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigType", + }, + return_type="dict[str, Schema]", + ), + TypeHintMatch( + function_name="async_get_triggers", + arg_types={ + 0: "HomeAssistant", + 1: "str", + }, + return_type=["list[dict[str, str]]", "list[dict[str, Any]]"], + ), + ], + "diagnostics": [ + TypeHintMatch( + function_name="async_get_config_entry_diagnostics", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigEntry", + }, + return_type=UNDEFINED, + ), + TypeHintMatch( + function_name="async_get_device_diagnostics", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigEntry", + 2: "DeviceEntry", + }, + return_type=UNDEFINED, + ), + ], } -_METHOD_MATCH: list[TypeHintMatch] = [ - TypeHintMatch( - module_filter=_MODULE_FILTERS["init"], - function_name="setup", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - }, - return_type="bool", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["init"], - function_name="async_setup", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - }, - return_type="bool", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["init"], - function_name="async_setup_entry", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigEntry", - }, - return_type="bool", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["init"], - function_name="async_remove_entry", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigEntry", - }, - return_type=None, - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["init"], - function_name="async_unload_entry", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigEntry", - }, - return_type="bool", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["init"], - function_name="async_migrate_entry", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigEntry", - }, - return_type="bool", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["any_platform"], - function_name="setup_platform", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - 2: "AddEntitiesCallback", - 3: "DiscoveryInfoType | None", - }, - return_type=None, - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["any_platform"], - function_name="async_setup_platform", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - 2: "AddEntitiesCallback", - 3: "DiscoveryInfoType | None", - }, - return_type=None, - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["any_platform"], - function_name="async_setup_entry", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigEntry", - 2: "AddEntitiesCallback", - }, - return_type=None, - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["application_credentials"], - function_name="async_get_auth_implementation", - arg_types={ - 0: "HomeAssistant", - 1: "str", - 2: "ClientCredential", - }, - return_type="AbstractOAuth2Implementation", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["application_credentials"], - function_name="async_get_authorization_server", - arg_types={ - 0: "HomeAssistant", - }, - return_type="AuthorizationServer", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["backup"], - function_name="async_pre_backup", - arg_types={ - 0: "HomeAssistant", - }, - return_type=None, - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["backup"], - function_name="async_post_backup", - arg_types={ - 0: "HomeAssistant", - }, - return_type=None, - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["cast"], - function_name="async_get_media_browser_root_object", - arg_types={ - 0: "HomeAssistant", - 1: "str", - }, - return_type="list[BrowseMedia]", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["cast"], - function_name="async_browse_media", - arg_types={ - 0: "HomeAssistant", - 1: "str", - 2: "str", - 3: "str", - }, - return_type=["BrowseMedia", "BrowseMedia | None"], - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["cast"], - function_name="async_play_media", - arg_types={ - 0: "HomeAssistant", - 1: "str", - 2: "Chromecast", - 3: "str", - 4: "str", - }, - return_type="bool", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["config_flow"], - function_name="_async_has_devices", - arg_types={ - 0: "HomeAssistant", - }, - return_type="bool", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["device_action"], - function_name="async_validate_action_config", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - }, - return_type="ConfigType", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["device_action"], - function_name="async_call_action_from_config", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - 2: "TemplateVarsType", - 3: "Context | None", - }, - return_type=None, - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["device_action"], - function_name="async_get_action_capabilities", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - }, - return_type="dict[str, Schema]", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["device_action"], - function_name="async_get_actions", - arg_types={ - 0: "HomeAssistant", - 1: "str", - }, - return_type=["list[dict[str, str]]", "list[dict[str, Any]]"], - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["device_condition"], - function_name="async_validate_condition_config", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - }, - return_type="ConfigType", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["device_condition"], - function_name="async_condition_from_config", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - }, - return_type="ConditionCheckerType", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["device_condition"], - function_name="async_get_condition_capabilities", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - }, - return_type="dict[str, Schema]", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["device_condition"], - function_name="async_get_conditions", - arg_types={ - 0: "HomeAssistant", - 1: "str", - }, - return_type=["list[dict[str, str]]", "list[dict[str, Any]]"], - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["device_tracker"], - function_name="setup_scanner", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - 2: "Callable[..., None]", - 3: "DiscoveryInfoType | None", - }, - return_type="bool", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["device_tracker"], - function_name="async_setup_scanner", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - 2: "Callable[..., Awaitable[None]]", - 3: "DiscoveryInfoType | None", - }, - return_type="bool", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["device_tracker"], - function_name="get_scanner", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - }, - return_type=["DeviceScanner", "DeviceScanner | None"], - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["device_tracker"], - function_name="async_get_scanner", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - }, - return_type=["DeviceScanner", "DeviceScanner | None"], - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["device_trigger"], - function_name="async_validate_condition_config", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - }, - return_type="ConfigType", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["device_trigger"], - function_name="async_attach_trigger", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - 2: "AutomationActionType", - 3: "AutomationTriggerInfo", - }, - return_type="CALLBACK_TYPE", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["device_trigger"], - function_name="async_get_trigger_capabilities", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - }, - return_type="dict[str, Schema]", - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["device_trigger"], - function_name="async_get_triggers", - arg_types={ - 0: "HomeAssistant", - 1: "str", - }, - return_type=["list[dict[str, str]]", "list[dict[str, Any]]"], - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["diagnostics"], - function_name="async_get_config_entry_diagnostics", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigEntry", - }, - return_type=UNDEFINED, - ), - TypeHintMatch( - module_filter=_MODULE_FILTERS["diagnostics"], - function_name="async_get_device_diagnostics", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigEntry", - 2: "DeviceEntry", - }, - return_type=UNDEFINED, - ), -] - -def _is_valid_type(expected_type: list[str] | str | None, node: astroid.NodeNG) -> bool: +def _is_valid_type( + expected_type: list[str] | str | None | object, node: astroid.NodeNG +) -> bool: """Check the argument node against the expected type.""" if expected_type is UNDEFINED: return True @@ -416,6 +377,8 @@ def _is_valid_type(expected_type: list[str] | str | None, node: astroid.NodeNG) if expected_type is None or expected_type == "None": return isinstance(node, astroid.Const) and node.value is None + assert isinstance(expected_type, str) + # Const occurs when the type is an Ellipsis if expected_type == "...": return isinstance(node, astroid.Const) and node.value == Ellipsis @@ -487,6 +450,17 @@ def _has_valid_annotations( return False +def _get_module_platform(module_name: str) -> str | None: + """Called when a Module node is visited.""" + if not (module_match := _MODULE_REGEX.match(module_name)): + # Ensure `homeassistant.components.` + # Or `homeassistant.components..` + return None + + platform = module_match.groups()[0] + return platform.lstrip(".") if platform else "__init__" + + class HassTypeHintChecker(BaseChecker): # type: ignore[misc] """Checker for setup type hints.""" @@ -510,38 +484,31 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] def __init__(self, linter: PyLinter | None = None) -> None: super().__init__(linter) - self.current_package: str | None = None - self.module: str | None = None + self._function_matchers: list[TypeHintMatch] = [] def visit_module(self, node: astroid.Module) -> None: """Called when a Module node is visited.""" - self.module = node.name - if node.package: - self.current_package = node.name - else: - # Strip name of the current module - self.current_package = node.name[: node.name.rfind(".")] + self._function_matchers = [] + + if (module_platform := _get_module_platform(node.name)) is None: + return + + if module_platform in _PLATFORMS: + self._function_matchers.extend(_FUNCTION_MATCH["__any_platform__"]) + + if matches := _FUNCTION_MATCH.get(module_platform): + self._function_matchers.extend(matches) def visit_functiondef(self, node: astroid.FunctionDef) -> None: """Called when a FunctionDef node is visited.""" - for match in _METHOD_MATCH: - self._visit_functiondef(node, match) + for match in self._function_matchers: + if node.name != match.function_name or node.is_method(): + continue + self._check_function(node, match) - def visit_asyncfunctiondef(self, node: astroid.AsyncFunctionDef) -> None: - """Called when an AsyncFunctionDef node is visited.""" - for match in _METHOD_MATCH: - self._visit_functiondef(node, match) - - def _visit_functiondef( - self, node: astroid.FunctionDef, match: TypeHintMatch - ) -> None: - if node.name != match.function_name: - return - if node.is_method(): - return - if not match.module_filter.match(self.module): - return + visit_asyncfunctiondef = visit_functiondef + def _check_function(self, node: astroid.FunctionDef, match: TypeHintMatch) -> None: # Check that at least one argument is annotated. annotations = _get_all_annotations(node) if node.returns is None and not _has_valid_annotations(annotations): diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index feb3b6b341c..0bd273985e3 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -1,5 +1,6 @@ """Tests for pylint hass_enforce_type_hints plugin.""" # pylint:disable=protected-access +from __future__ import annotations import re from types import ModuleType @@ -14,6 +15,30 @@ import pytest from . import assert_adds_messages, assert_no_messages +@pytest.mark.parametrize( + ("module_name", "expected_platform", "in_platforms"), + [ + ("homeassistant", None, False), + ("homeassistant.components", None, False), + ("homeassistant.components.pylint_test", "__init__", False), + ("homeassistant.components.pylint_test.config_flow", "config_flow", False), + ("homeassistant.components.pylint_test.light", "light", True), + ("homeassistant.components.pylint_test.light.v1", None, False), + ], +) +def test_regex_get_module_platform( + hass_enforce_type_hints: ModuleType, + module_name: str, + expected_platform: str | None, + in_platforms: bool, +) -> None: + """Test _get_module_platform regex.""" + platform = hass_enforce_type_hints._get_module_platform(module_name) + + assert platform == expected_platform + assert (platform in hass_enforce_type_hints._PLATFORMS) == in_platforms + + @pytest.mark.parametrize( ("string", "expected_x", "expected_y", "expected_z", "expected_a"), [ @@ -95,7 +120,11 @@ def test_ignore_not_annotations( hass_enforce_type_hints: ModuleType, type_hint_checker: BaseChecker, code: str ) -> None: """Ensure that _is_valid_type is not run if there are no annotations.""" - func_node = astroid.extract_node(code) + func_node = astroid.extract_node( + code, + "homeassistant.components.pylint_test", + ) + type_hint_checker.visit_module(func_node.parent) with patch.object( hass_enforce_type_hints, "_is_valid_type", return_value=True @@ -131,7 +160,11 @@ def test_dont_ignore_partial_annotations( hass_enforce_type_hints: ModuleType, type_hint_checker: BaseChecker, code: str ) -> None: """Ensure that _is_valid_type is run if there is at least one annotation.""" - func_node = astroid.extract_node(code) + func_node = astroid.extract_node( + code, + "homeassistant.components.pylint_test", + ) + type_hint_checker.visit_module(func_node.parent) with patch.object( hass_enforce_type_hints, "_is_valid_type", return_value=True @@ -144,7 +177,6 @@ def test_invalid_discovery_info( linter: UnittestLinter, type_hint_checker: BaseChecker ) -> None: """Ensure invalid hints are rejected for discovery_info.""" - type_hint_checker.module = "homeassistant.components.pylint_test.device_tracker" func_node, discovery_info_node = astroid.extract_node( """ async def async_setup_scanner( #@ @@ -154,8 +186,10 @@ def test_invalid_discovery_info( discovery_info: dict[str, Any] | None = None, #@ ) -> bool: pass - """ + """, + "homeassistant.components.pylint_test.device_tracker", ) + type_hint_checker.visit_module(func_node.parent) with assert_adds_messages( linter, @@ -176,7 +210,6 @@ def test_valid_discovery_info( linter: UnittestLinter, type_hint_checker: BaseChecker ) -> None: """Ensure valid hints are accepted for discovery_info.""" - type_hint_checker.module = "homeassistant.components.pylint_test.device_tracker" func_node = astroid.extract_node( """ async def async_setup_scanner( #@ @@ -186,8 +219,10 @@ def test_valid_discovery_info( discovery_info: DiscoveryInfoType | None = None, ) -> bool: pass - """ + """, + "homeassistant.components.pylint_test.device_tracker", ) + type_hint_checker.visit_module(func_node.parent) with assert_no_messages(linter): type_hint_checker.visit_asyncfunctiondef(func_node) @@ -197,7 +232,6 @@ def test_invalid_list_dict_str_any( linter: UnittestLinter, type_hint_checker: BaseChecker ) -> None: """Ensure invalid hints are rejected for discovery_info.""" - type_hint_checker.module = "homeassistant.components.pylint_test.device_trigger" func_node = astroid.extract_node( """ async def async_get_triggers( #@ @@ -205,8 +239,10 @@ def test_invalid_list_dict_str_any( device_id: str ) -> list: pass - """ + """, + "homeassistant.components.pylint_test.device_trigger", ) + type_hint_checker.visit_module(func_node.parent) with assert_adds_messages( linter, @@ -227,7 +263,6 @@ def test_valid_list_dict_str_any( linter: UnittestLinter, type_hint_checker: BaseChecker ) -> None: """Ensure valid hints are accepted for discovery_info.""" - type_hint_checker.module = "homeassistant.components.pylint_test.device_trigger" func_node = astroid.extract_node( """ async def async_get_triggers( #@ @@ -235,8 +270,10 @@ def test_valid_list_dict_str_any( device_id: str ) -> list[dict[str, Any]]: pass - """ + """, + "homeassistant.components.pylint_test.device_trigger", ) + type_hint_checker.visit_module(func_node.parent) with assert_no_messages(linter): type_hint_checker.visit_asyncfunctiondef(func_node) From 3ea304aaf10fb0d288aa9a56955c961442b5c388 Mon Sep 17 00:00:00 2001 From: Thijs W Date: Tue, 31 May 2022 11:56:44 +0200 Subject: [PATCH 1093/3516] Improve frontier_silicon style (#72752) --- .../frontier_silicon/media_player.py | 121 +++++------------- 1 file changed, 29 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index e4a67fa5cc4..555e0517d4c 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -84,6 +84,8 @@ async def async_setup_platform( class AFSAPIDevice(MediaPlayerEntity): """Representation of a Frontier Silicon device on the network.""" + _attr_media_content_type: str = MEDIA_TYPE_MUSIC + _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.VOLUME_SET @@ -108,80 +110,19 @@ class AFSAPIDevice(MediaPlayerEntity): identifiers={(DOMAIN, afsapi.webfsapi_endpoint)}, name=name, ) + self._attr_name = name - self._state = None - - self._name = name - self._title = None - self._artist = None - self._album_name = None - self._mute = None - self._source = None - self._source_list = None - self._media_image_url = None self._max_volume = None - self._volume_level = None self.__modes_by_label = None - @property - def name(self): - """Return the device name.""" - return self._name - - @property - def media_title(self): - """Title of current playing media.""" - return self._title - - @property - def media_artist(self): - """Artist of current playing media, music track only.""" - return self._artist - - @property - def media_album_name(self): - """Album name of current playing media, music track only.""" - return self._album_name - - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - - @property - def state(self): - """Return the state of the player.""" - return self._state - - # source - @property - def source_list(self): - """List of available input sources.""" - return self._source_list - - @property - def source(self): - """Name of the current input source.""" - return self._source - - @property - def media_image_url(self): - """Image url of current playing media.""" - return self._media_image_url - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._volume_level - async def async_update(self): """Get the latest date and update device state.""" afsapi = self.fs_device try: if await afsapi.get_power(): status = await afsapi.get_play_status() - self._state = { + self._attr_state = { PlayState.PLAYING: STATE_PLAYING, PlayState.PAUSED: STATE_PAUSED, PlayState.STOPPED: STATE_IDLE, @@ -189,12 +130,12 @@ class AFSAPIDevice(MediaPlayerEntity): None: STATE_IDLE, }.get(status) else: - self._state = STATE_OFF + self._attr_state = STATE_OFF except FSConnectionError: if self._attr_available: _LOGGER.warning( "Could not connect to %s. Did it go offline?", - self._name or afsapi.webfsapi_endpoint, + self.name or afsapi.webfsapi_endpoint, ) self._attr_available = False return @@ -202,18 +143,18 @@ class AFSAPIDevice(MediaPlayerEntity): if not self._attr_available: _LOGGER.info( "Reconnected to %s", - self._name or afsapi.webfsapi_endpoint, + self.name or afsapi.webfsapi_endpoint, ) self._attr_available = True - if not self._name: - self._name = await afsapi.get_friendly_name() + if not self._attr_name: + self._attr_name = await afsapi.get_friendly_name() - if not self._source_list: + if not self._attr_source_list: self.__modes_by_label = { mode.label: mode.key for mode in await afsapi.get_modes() } - self._source_list = list(self.__modes_by_label) + self._attr_source_list = list(self.__modes_by_label) # The API seems to include 'zero' in the number of steps (e.g. if the range is # 0-40 then get_volume_steps returns 41) subtract one to get the max volume. @@ -221,32 +162,34 @@ class AFSAPIDevice(MediaPlayerEntity): if not self._max_volume: self._max_volume = int(await afsapi.get_volume_steps() or 1) - 1 - if self._state != STATE_OFF: + if self._attr_state != STATE_OFF: info_name = await afsapi.get_play_name() info_text = await afsapi.get_play_text() - self._title = " - ".join(filter(None, [info_name, info_text])) - self._artist = await afsapi.get_play_artist() - self._album_name = await afsapi.get_play_album() + self._attr_media_title = " - ".join(filter(None, [info_name, info_text])) + self._attr_media_artist = await afsapi.get_play_artist() + self._attr_media_album_name = await afsapi.get_play_album() - self._source = (await afsapi.get_mode()).label - self._mute = await afsapi.get_mute() - self._media_image_url = await afsapi.get_play_graphic() + self._attr_source = (await afsapi.get_mode()).label + + self._attr_is_volume_muted = await afsapi.get_mute() + self._attr_media_image_url = await afsapi.get_play_graphic() volume = await self.fs_device.get_volume() # Prevent division by zero if max_volume not known yet - self._volume_level = float(volume or 0) / (self._max_volume or 1) + self._attr_volume_level = float(volume or 0) / (self._max_volume or 1) else: - self._title = None - self._artist = None - self._album_name = None + self._attr_media_title = None + self._attr_media_artist = None + self._attr_media_album_name = None - self._source = None - self._mute = None - self._media_image_url = None + self._attr_source = None - self._volume_level = None + self._attr_is_volume_muted = None + self._attr_media_image_url = None + + self._attr_volume_level = None # Management actions # power control @@ -268,7 +211,7 @@ class AFSAPIDevice(MediaPlayerEntity): async def async_media_play_pause(self): """Send play/pause command.""" - if self._state == STATE_PLAYING: + if self._attr_state == STATE_PLAYING: await self.fs_device.pause() else: await self.fs_device.play() @@ -285,12 +228,6 @@ class AFSAPIDevice(MediaPlayerEntity): """Send next track command (results in fast-forward).""" await self.fs_device.forward() - # mute - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self._mute - async def async_mute_volume(self, mute): """Send mute command.""" await self.fs_device.set_mute(mute) From cf27b82d2fdce406fda3b1b9cd52d42d7f7d00d6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 31 May 2022 12:26:15 +0200 Subject: [PATCH 1094/3516] Separate words with underscore in onewire (#72758) --- homeassistant/components/onewire/__init__.py | 10 +++++----- .../components/onewire/binary_sensor.py | 12 ++++++------ homeassistant/components/onewire/diagnostics.py | 6 +++--- homeassistant/components/onewire/sensor.py | 16 ++++++++-------- homeassistant/components/onewire/switch.py | 12 ++++++------ 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index e908a52a071..b836d7e3298 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -18,16 +18,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a 1-Wire proxy for a config entry.""" hass.data.setdefault(DOMAIN, {}) - onewirehub = OneWireHub(hass) + onewire_hub = OneWireHub(hass) try: - await onewirehub.initialize(entry) + await onewire_hub.initialize(entry) except ( CannotConnect, # Failed to connect to the server protocol.OwnetError, # Connected to server, but failed to list the devices ) as exc: raise ConfigEntryNotReady() from exc - hass.data[DOMAIN][entry.entry_id] = onewirehub + hass.data[DOMAIN][entry.entry_id] = onewire_hub hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -40,9 +40,9 @@ async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry ) -> bool: """Remove a config entry from a device.""" - onewirehub: OneWireHub = hass.data[DOMAIN][config_entry.entry_id] + onewire_hub: OneWireHub = hass.data[DOMAIN][config_entry.entry_id] return not device_entry.identifiers.intersection( - (DOMAIN, device.id) for device in onewirehub.devices or [] + (DOMAIN, device.id) for device in onewire_hub.devices or [] ) diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index 307f38ceea0..4d6baef353e 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -94,19 +94,19 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up 1-Wire platform.""" - onewirehub = hass.data[DOMAIN][config_entry.entry_id] + onewire_hub = hass.data[DOMAIN][config_entry.entry_id] - entities = await hass.async_add_executor_job(get_entities, onewirehub) + entities = await hass.async_add_executor_job(get_entities, onewire_hub) async_add_entities(entities, True) -def get_entities(onewirehub: OneWireHub) -> list[OneWireBinarySensor]: +def get_entities(onewire_hub: OneWireHub) -> list[OneWireBinarySensor]: """Get a list of entities.""" - if not onewirehub.devices: + if not onewire_hub.devices: return [] entities: list[OneWireBinarySensor] = [] - for device in onewirehub.devices: + for device in onewire_hub.devices: family = device.family device_id = device.id device_type = device.type @@ -128,7 +128,7 @@ def get_entities(onewirehub: OneWireHub) -> list[OneWireBinarySensor]: device_file=device_file, device_info=device_info, name=name, - owproxy=onewirehub.owproxy, + owproxy=onewire_hub.owproxy, ) ) diff --git a/homeassistant/components/onewire/diagnostics.py b/homeassistant/components/onewire/diagnostics.py index a02ff2d8e47..36db7fd5360 100644 --- a/homeassistant/components/onewire/diagnostics.py +++ b/homeassistant/components/onewire/diagnostics.py @@ -19,7 +19,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - onewirehub: OneWireHub = hass.data[DOMAIN][entry.entry_id] + onewire_hub: OneWireHub = hass.data[DOMAIN][entry.entry_id] return { "entry": { @@ -27,7 +27,7 @@ async def async_get_config_entry_diagnostics( "data": async_redact_data(entry.data, TO_REDACT), "options": {**entry.options}, }, - "devices": [asdict(device_details) for device_details in onewirehub.devices] - if onewirehub.devices + "devices": [asdict(device_details) for device_details in onewire_hub.devices] + if onewire_hub.devices else [], } diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 9f376a6df7a..f6c88201dc7 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -367,23 +367,23 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up 1-Wire platform.""" - onewirehub = hass.data[DOMAIN][config_entry.entry_id] + onewire_hub = hass.data[DOMAIN][config_entry.entry_id] entities = await hass.async_add_executor_job( - get_entities, onewirehub, config_entry.options + get_entities, onewire_hub, config_entry.options ) async_add_entities(entities, True) def get_entities( - onewirehub: OneWireHub, options: MappingProxyType[str, Any] + onewire_hub: OneWireHub, options: MappingProxyType[str, Any] ) -> list[OneWireSensor]: """Get a list of entities.""" - if not onewirehub.devices: + if not onewire_hub.devices: return [] entities: list[OneWireSensor] = [] - assert onewirehub.owproxy - for device in onewirehub.devices: + assert onewire_hub.owproxy + for device in onewire_hub.devices: family = device.family device_type = device.type device_id = device.id @@ -403,7 +403,7 @@ def get_entities( if description.key.startswith("moisture/"): s_id = description.key.split(".")[1] is_leaf = int( - onewirehub.owproxy.read( + onewire_hub.owproxy.read( f"{device_path}moisture/is_leaf.{s_id}" ).decode() ) @@ -427,7 +427,7 @@ def get_entities( device_file=device_file, device_info=device_info, name=name, - owproxy=onewirehub.owproxy, + owproxy=onewire_hub.owproxy, ) ) return entities diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index 764cc403681..8a6e6ff3736 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -150,20 +150,20 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up 1-Wire platform.""" - onewirehub = hass.data[DOMAIN][config_entry.entry_id] + onewire_hub = hass.data[DOMAIN][config_entry.entry_id] - entities = await hass.async_add_executor_job(get_entities, onewirehub) + entities = await hass.async_add_executor_job(get_entities, onewire_hub) async_add_entities(entities, True) -def get_entities(onewirehub: OneWireHub) -> list[OneWireSwitch]: +def get_entities(onewire_hub: OneWireHub) -> list[OneWireSwitch]: """Get a list of entities.""" - if not onewirehub.devices: + if not onewire_hub.devices: return [] entities: list[OneWireSwitch] = [] - for device in onewirehub.devices: + for device in onewire_hub.devices: family = device.family device_type = device.type device_id = device.id @@ -185,7 +185,7 @@ def get_entities(onewirehub: OneWireHub) -> list[OneWireSwitch]: device_file=device_file, device_info=device_info, name=name, - owproxy=onewirehub.owproxy, + owproxy=onewire_hub.owproxy, ) ) From 8140ed724c6998164e0ff43197c25d97a123df64 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 31 May 2022 15:22:31 +0200 Subject: [PATCH 1095/3516] Remove mysensors yaml (#72761) --- .../components/mysensors/__init__.py | 150 +------- .../components/mysensors/config_flow.py | 46 +-- homeassistant/components/mysensors/const.py | 3 - tests/components/mysensors/conftest.py | 7 +- .../components/mysensors/test_config_flow.py | 189 ++++------ tests/components/mysensors/test_init.py | 343 +----------------- 6 files changed, 87 insertions(+), 651 deletions(-) diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index d392624bbe4..3313a70808c 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -7,12 +7,9 @@ from functools import partial import logging from mysensors import BaseAsyncGateway -import voluptuous as vol -from homeassistant import config_entries -from homeassistant.components.mqtt import valid_publish_topic, valid_subscribe_topic from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_OPTIMISTIC, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntry @@ -22,17 +19,6 @@ from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_DEVICES, - CONF_BAUD_RATE, - CONF_DEVICE, - CONF_GATEWAYS, - CONF_NODES, - CONF_PERSISTENCE, - CONF_PERSISTENCE_FILE, - CONF_RETAIN, - CONF_TCP_PORT, - CONF_TOPIC_IN_PREFIX, - CONF_TOPIC_OUT_PREFIX, - CONF_VERSION, DOMAIN, MYSENSORS_DISCOVERY, MYSENSORS_GATEWAYS, @@ -48,147 +34,17 @@ from .helpers import on_unload _LOGGER = logging.getLogger(__name__) -CONF_DEBUG = "debug" -CONF_NODE_NAME = "name" - DATA_HASS_CONFIG = "hass_config" -DEFAULT_BAUD_RATE = 115200 -DEFAULT_TCP_PORT = 5003 -DEFAULT_VERSION = "1.4" - -def set_default_persistence_file(value: dict) -> dict: - """Set default persistence file.""" - for idx, gateway in enumerate(value): - if gateway.get(CONF_PERSISTENCE_FILE) is not None: - continue - new_name = f"mysensors{idx + 1}.pickle" - gateway[CONF_PERSISTENCE_FILE] = new_name - - return value - - -def has_all_unique_files(value: list[dict]) -> list[dict]: - """Validate that all persistence files are unique and set if any is set.""" - persistence_files = [gateway[CONF_PERSISTENCE_FILE] for gateway in value] - schema = vol.Schema(vol.Unique()) - schema(persistence_files) - return value - - -def is_persistence_file(value: str) -> str: - """Validate that persistence file path ends in either .pickle or .json.""" - if value.endswith((".json", ".pickle")): - return value - raise vol.Invalid(f"{value} does not end in either `.json` or `.pickle`") - - -def deprecated(key: str) -> Callable[[dict], dict]: - """Mark key as deprecated in configuration.""" - - def validator(config: dict) -> dict: - """Check if key is in config, log warning and remove key.""" - if key not in config: - return config - _LOGGER.warning( - "%s option for %s is deprecated. Please remove %s from your " - "configuration file", - key, - DOMAIN, - key, - ) - config.pop(key) - return config - - return validator - - -NODE_SCHEMA = vol.Schema({cv.positive_int: {vol.Required(CONF_NODE_NAME): cv.string}}) - -GATEWAY_SCHEMA = vol.Schema( - vol.All( - deprecated(CONF_NODES), - { - vol.Required(CONF_DEVICE): cv.string, - vol.Optional(CONF_PERSISTENCE_FILE): vol.All( - cv.string, is_persistence_file - ), - vol.Optional(CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE): cv.positive_int, - vol.Optional(CONF_TCP_PORT, default=DEFAULT_TCP_PORT): cv.port, - vol.Optional(CONF_TOPIC_IN_PREFIX): valid_subscribe_topic, - vol.Optional(CONF_TOPIC_OUT_PREFIX): valid_publish_topic, - vol.Optional(CONF_NODES, default={}): NODE_SCHEMA, - }, - ) -) - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - vol.All( - deprecated(CONF_DEBUG), - deprecated(CONF_OPTIMISTIC), - deprecated(CONF_PERSISTENCE), - { - vol.Required(CONF_GATEWAYS): vol.All( - cv.ensure_list, - set_default_persistence_file, - has_all_unique_files, - [GATEWAY_SCHEMA], - ), - vol.Optional(CONF_RETAIN, default=True): cv.boolean, - vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, - vol.Optional(CONF_PERSISTENCE, default=True): cv.boolean, - }, - ) - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the MySensors component.""" + # This is needed to set up the notify platform via discovery. hass.data[DOMAIN] = {DATA_HASS_CONFIG: config} - if DOMAIN not in config or bool(hass.config_entries.async_entries(DOMAIN)): - return True - - config = config[DOMAIN] - user_inputs = [ - { - CONF_DEVICE: gw[CONF_DEVICE], - CONF_BAUD_RATE: gw[CONF_BAUD_RATE], - CONF_TCP_PORT: gw[CONF_TCP_PORT], - CONF_TOPIC_OUT_PREFIX: gw.get(CONF_TOPIC_OUT_PREFIX, ""), - CONF_TOPIC_IN_PREFIX: gw.get(CONF_TOPIC_IN_PREFIX, ""), - CONF_RETAIN: config[CONF_RETAIN], - CONF_VERSION: config[CONF_VERSION], - CONF_PERSISTENCE_FILE: gw[CONF_PERSISTENCE_FILE] - # nodes config ignored at this time. renaming nodes can now be done from the frontend. - } - for gw in config[CONF_GATEWAYS] - ] - user_inputs = [ - {k: v for k, v in userinput.items() if v is not None} - for userinput in user_inputs - ] - - # there is an actual configuration in configuration.yaml, so we have to process it - for user_input in user_inputs: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=user_input, - ) - ) - return True diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index 9d992e172b0..5409e3c9a85 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -22,7 +22,6 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv -from . import DEFAULT_BAUD_RATE, DEFAULT_TCP_PORT, DEFAULT_VERSION, is_persistence_file from .const import ( CONF_BAUD_RATE, CONF_DEVICE, @@ -42,6 +41,17 @@ from .const import ( ) from .gateway import MQTT_COMPONENT, is_serial_port, is_socket_address, try_connect +DEFAULT_BAUD_RATE = 115200 +DEFAULT_TCP_PORT = 5003 +DEFAULT_VERSION = "1.4" + + +def is_persistence_file(value: str) -> str: + """Validate that persistence file path ends in either .pickle or .json.""" + if value.endswith((".json", ".pickle")): + return value + raise vol.Invalid(f"{value} does not end in either `.json` or `.pickle`") + def _get_schema_common(user_input: dict[str, str]) -> dict: """Create a schema with options common to all gateway types.""" @@ -105,31 +115,6 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Set up config flow.""" self._gw_type: str | None = None - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Import a config entry. - - This method is called by async_setup and it has already - prepared the dict to be compatible with what a user would have - entered from the frontend. - Therefore we process it as though it came from the frontend. - """ - if user_input[CONF_DEVICE] == MQTT_COMPONENT: - user_input[CONF_GATEWAY_TYPE] = CONF_GATEWAY_TYPE_MQTT - else: - try: - await self.hass.async_add_executor_job( - is_serial_port, user_input[CONF_DEVICE] - ) - except vol.Invalid: - user_input[CONF_GATEWAY_TYPE] = CONF_GATEWAY_TYPE_TCP - else: - user_input[CONF_GATEWAY_TYPE] = CONF_GATEWAY_TYPE_SERIAL - - result: FlowResult = await self.async_step_user(user_input=user_input) - if errors := result.get("errors"): - return self.async_abort(reason=next(iter(errors.values()))) - return result - async def async_step_user( self, user_input: dict[str, str] | None = None ) -> FlowResult: @@ -335,10 +320,11 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors[CONF_PERSISTENCE_FILE] = "duplicate_persistence_file" break - for other_entry in self._async_current_entries(): - if _is_same_device(gw_type, user_input, other_entry): - errors["base"] = "already_configured" - break + if not errors: + for other_entry in self._async_current_entries(): + if _is_same_device(gw_type, user_input, other_entry): + errors["base"] = "already_configured" + break # if no errors so far, try to connect if not errors and not await try_connect(self.hass, gw_type, user_input): diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index 5f3cb6aed96..32e2110dd95 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -11,9 +11,6 @@ ATTR_GATEWAY_ID: Final = "gateway_id" CONF_BAUD_RATE: Final = "baud_rate" CONF_DEVICE: Final = "device" -CONF_GATEWAYS: Final = "gateways" -CONF_NODES: Final = "nodes" -CONF_PERSISTENCE: Final = "persistence" CONF_PERSISTENCE_FILE: Final = "persistence_file" CONF_RETAIN: Final = "retain" CONF_TCP_PORT: Final = "tcp_port" diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py index 54ae88cdccb..7c73ce1a389 100644 --- a/tests/components/mysensors/conftest.py +++ b/tests/components/mysensors/conftest.py @@ -13,13 +13,13 @@ import pytest from homeassistant.components.device_tracker.legacy import Device from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN -from homeassistant.components.mysensors import CONF_VERSION, DEFAULT_BAUD_RATE +from homeassistant.components.mysensors.config_flow import DEFAULT_BAUD_RATE from homeassistant.components.mysensors.const import ( CONF_BAUD_RATE, CONF_DEVICE, CONF_GATEWAY_TYPE, CONF_GATEWAY_TYPE_SERIAL, - CONF_GATEWAYS, + CONF_VERSION, DOMAIN, ) from homeassistant.core import HomeAssistant @@ -141,8 +141,7 @@ async def integration_fixture( hass: HomeAssistant, transport: MagicMock, config_entry: MockConfigEntry ) -> AsyncGenerator[MockConfigEntry, None]: """Set up the mysensors integration with a config entry.""" - device = config_entry.data[CONF_DEVICE] - config: dict[str, Any] = {DOMAIN: {CONF_GATEWAYS: [{CONF_DEVICE: device}]}} + config: dict[str, Any] = {} config_entry.add_to_hass(hass) with patch("homeassistant.components.mysensors.device.UPDATE_DELAY", new=0): diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py index ca13a6d9cef..e7808162043 100644 --- a/tests/components/mysensors/test_config_flow.py +++ b/tests/components/mysensors/test_config_flow.py @@ -14,7 +14,6 @@ from homeassistant.components.mysensors.const import ( CONF_GATEWAY_TYPE_MQTT, CONF_GATEWAY_TYPE_SERIAL, CONF_GATEWAY_TYPE_TCP, - CONF_PERSISTENCE, CONF_PERSISTENCE_FILE, CONF_RETAIN, CONF_TCP_PORT, @@ -399,333 +398,268 @@ async def test_config_invalid( assert len(mock_setup_entry.mock_calls) == 0 -@pytest.mark.parametrize( - "user_input", - [ - { - CONF_DEVICE: "COM5", - CONF_BAUD_RATE: 57600, - CONF_TCP_PORT: 5003, - CONF_RETAIN: True, - CONF_VERSION: "2.3", - CONF_PERSISTENCE_FILE: "bla.json", - }, - { - CONF_DEVICE: "COM5", - CONF_PERSISTENCE_FILE: "bla.json", - CONF_BAUD_RATE: 57600, - CONF_TCP_PORT: 5003, - CONF_VERSION: "2.3", - CONF_PERSISTENCE: False, - CONF_RETAIN: True, - }, - { - CONF_DEVICE: "mqtt", - CONF_BAUD_RATE: 115200, - CONF_TCP_PORT: 5003, - CONF_TOPIC_IN_PREFIX: "intopic", - CONF_TOPIC_OUT_PREFIX: "outtopic", - CONF_VERSION: "2.4", - CONF_PERSISTENCE: False, - CONF_RETAIN: False, - }, - { - CONF_DEVICE: "127.0.0.1", - CONF_PERSISTENCE_FILE: "blub.pickle", - CONF_BAUD_RATE: 115200, - CONF_TCP_PORT: 343, - CONF_VERSION: "2.4", - CONF_PERSISTENCE: False, - CONF_RETAIN: False, - }, - ], -) -async def test_import(hass: HomeAssistant, mqtt: None, user_input: dict) -> None: - """Test importing a gateway.""" - - with patch("sys.platform", "win32"), patch( - "homeassistant.components.mysensors.config_flow.try_connect", return_value=True - ), patch( - "homeassistant.components.mysensors.async_setup_entry", - return_value=True, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, data=user_input, context={"source": config_entries.SOURCE_IMPORT} - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - - @pytest.mark.parametrize( "first_input, second_input, expected_result", [ ( { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, CONF_DEVICE: "mqtt", CONF_VERSION: "2.3", CONF_TOPIC_IN_PREFIX: "same1", CONF_TOPIC_OUT_PREFIX: "same2", }, { - CONF_DEVICE: "mqtt", + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, CONF_VERSION: "2.3", CONF_TOPIC_IN_PREFIX: "same1", CONF_TOPIC_OUT_PREFIX: "same2", }, - (CONF_TOPIC_IN_PREFIX, "duplicate_topic"), + FlowResult(type="form", errors={CONF_TOPIC_IN_PREFIX: "duplicate_topic"}), ), ( { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, CONF_DEVICE: "mqtt", CONF_VERSION: "2.3", CONF_TOPIC_IN_PREFIX: "different1", CONF_TOPIC_OUT_PREFIX: "different2", }, { - CONF_DEVICE: "mqtt", + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, CONF_VERSION: "2.3", CONF_TOPIC_IN_PREFIX: "different3", CONF_TOPIC_OUT_PREFIX: "different4", }, - None, + FlowResult(type="create_entry"), ), ( { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, CONF_DEVICE: "mqtt", CONF_VERSION: "2.3", CONF_TOPIC_IN_PREFIX: "same1", CONF_TOPIC_OUT_PREFIX: "different2", }, { - CONF_DEVICE: "mqtt", + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, CONF_VERSION: "2.3", CONF_TOPIC_IN_PREFIX: "same1", CONF_TOPIC_OUT_PREFIX: "different4", }, - (CONF_TOPIC_IN_PREFIX, "duplicate_topic"), + FlowResult(type="form", errors={CONF_TOPIC_IN_PREFIX: "duplicate_topic"}), ), ( { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, CONF_DEVICE: "mqtt", CONF_VERSION: "2.3", CONF_TOPIC_IN_PREFIX: "same1", CONF_TOPIC_OUT_PREFIX: "different2", }, { - CONF_DEVICE: "mqtt", + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, CONF_VERSION: "2.3", CONF_TOPIC_IN_PREFIX: "different1", CONF_TOPIC_OUT_PREFIX: "same1", }, - (CONF_TOPIC_OUT_PREFIX, "duplicate_topic"), + FlowResult(type="form", errors={CONF_TOPIC_OUT_PREFIX: "duplicate_topic"}), ), ( { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, CONF_DEVICE: "mqtt", CONF_VERSION: "2.3", CONF_TOPIC_IN_PREFIX: "same1", CONF_TOPIC_OUT_PREFIX: "different2", }, { - CONF_DEVICE: "mqtt", + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, CONF_VERSION: "2.3", CONF_TOPIC_IN_PREFIX: "same1", CONF_TOPIC_OUT_PREFIX: "different1", }, - (CONF_TOPIC_IN_PREFIX, "duplicate_topic"), + FlowResult(type="form", errors={CONF_TOPIC_IN_PREFIX: "duplicate_topic"}), ), ( { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, CONF_DEVICE: "127.0.0.1", CONF_PERSISTENCE_FILE: "same.json", CONF_TCP_PORT: 343, CONF_VERSION: "2.3", - CONF_PERSISTENCE: False, - CONF_RETAIN: False, }, { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, CONF_DEVICE: "192.168.1.2", CONF_PERSISTENCE_FILE: "same.json", CONF_TCP_PORT: 343, CONF_VERSION: "2.3", - CONF_PERSISTENCE: False, - CONF_RETAIN: False, }, - ("persistence_file", "duplicate_persistence_file"), + FlowResult( + type="form", errors={"persistence_file": "duplicate_persistence_file"} + ), ), ( { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, CONF_DEVICE: "127.0.0.1", CONF_TCP_PORT: 343, CONF_VERSION: "2.3", - CONF_PERSISTENCE: False, - CONF_RETAIN: False, }, { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, CONF_DEVICE: "192.168.1.2", CONF_PERSISTENCE_FILE: "same.json", CONF_TCP_PORT: 343, CONF_VERSION: "2.3", - CONF_PERSISTENCE: False, - CONF_RETAIN: False, }, - None, + FlowResult(type="create_entry"), ), ( { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, CONF_DEVICE: "127.0.0.1", CONF_TCP_PORT: 343, CONF_VERSION: "2.3", - CONF_PERSISTENCE: False, - CONF_RETAIN: False, }, { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, CONF_DEVICE: "192.168.1.2", CONF_TCP_PORT: 343, CONF_VERSION: "2.3", - CONF_PERSISTENCE: False, - CONF_RETAIN: False, }, - None, + FlowResult(type="create_entry"), ), ( { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, CONF_DEVICE: "192.168.1.2", CONF_PERSISTENCE_FILE: "different1.json", CONF_TCP_PORT: 343, CONF_VERSION: "2.3", - CONF_PERSISTENCE: False, - CONF_RETAIN: False, }, { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, CONF_DEVICE: "192.168.1.2", CONF_PERSISTENCE_FILE: "different2.json", CONF_TCP_PORT: 343, CONF_VERSION: "2.3", - CONF_PERSISTENCE: False, - CONF_RETAIN: False, }, - ("base", "already_configured"), + FlowResult(type="form", errors={"base": "already_configured"}), ), ( { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, CONF_DEVICE: "192.168.1.2", CONF_PERSISTENCE_FILE: "different1.json", CONF_TCP_PORT: 343, CONF_VERSION: "2.3", - CONF_PERSISTENCE: False, - CONF_RETAIN: False, }, { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, CONF_DEVICE: "192.168.1.2", CONF_PERSISTENCE_FILE: "different2.json", CONF_TCP_PORT: 5003, CONF_VERSION: "2.3", - CONF_PERSISTENCE: False, - CONF_RETAIN: False, }, - None, + FlowResult(type="create_entry"), ), ( { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, CONF_DEVICE: "192.168.1.2", CONF_TCP_PORT: 5003, CONF_VERSION: "2.3", - CONF_PERSISTENCE: False, - CONF_RETAIN: False, }, { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, CONF_DEVICE: "192.168.1.3", CONF_TCP_PORT: 5003, CONF_VERSION: "2.3", - CONF_PERSISTENCE: False, - CONF_RETAIN: False, }, - None, + FlowResult(type="create_entry"), ), ( { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, CONF_DEVICE: "COM5", - CONF_TCP_PORT: 5003, - CONF_RETAIN: True, CONF_VERSION: "2.3", CONF_PERSISTENCE_FILE: "different1.json", }, { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, CONF_DEVICE: "COM5", - CONF_TCP_PORT: 5003, - CONF_RETAIN: True, CONF_VERSION: "2.3", CONF_PERSISTENCE_FILE: "different2.json", }, - ("base", "already_configured"), + FlowResult(type="form", errors={"base": "already_configured"}), ), ( { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, CONF_DEVICE: "COM6", CONF_BAUD_RATE: 57600, - CONF_RETAIN: True, CONF_VERSION: "2.3", }, { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, CONF_DEVICE: "COM5", - CONF_TCP_PORT: 5003, - CONF_RETAIN: True, CONF_VERSION: "2.3", }, - None, + FlowResult(type="create_entry"), ), ( { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, CONF_DEVICE: "COM5", CONF_BAUD_RATE: 115200, - CONF_RETAIN: True, CONF_VERSION: "2.3", CONF_PERSISTENCE_FILE: "different1.json", }, { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, CONF_DEVICE: "COM5", CONF_BAUD_RATE: 57600, - CONF_RETAIN: True, CONF_VERSION: "2.3", CONF_PERSISTENCE_FILE: "different2.json", }, - ("base", "already_configured"), + FlowResult(type="form", errors={"base": "already_configured"}), ), ( { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, CONF_DEVICE: "COM5", CONF_BAUD_RATE: 115200, - CONF_RETAIN: True, CONF_VERSION: "2.3", CONF_PERSISTENCE_FILE: "same.json", }, { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, CONF_DEVICE: "COM6", CONF_BAUD_RATE: 57600, - CONF_RETAIN: True, CONF_VERSION: "2.3", CONF_PERSISTENCE_FILE: "same.json", }, - ("persistence_file", "duplicate_persistence_file"), + FlowResult( + type="form", errors={"persistence_file": "duplicate_persistence_file"} + ), ), ( { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, CONF_DEVICE: "mqtt", CONF_PERSISTENCE_FILE: "bla.json", - CONF_BAUD_RATE: 115200, - CONF_TCP_PORT: 5003, CONF_VERSION: "1.4", }, { + CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, CONF_DEVICE: "COM6", CONF_PERSISTENCE_FILE: "bla2.json", CONF_BAUD_RATE: 115200, - CONF_TCP_PORT: 5003, CONF_VERSION: "1.4", }, - None, + FlowResult(type="create_entry"), ), ], ) @@ -734,7 +668,7 @@ async def test_duplicate( mqtt: None, first_input: dict, second_input: dict, - expected_result: tuple[str, str] | None, + expected_result: FlowResult, ) -> None: """Test duplicate detection.""" @@ -746,12 +680,17 @@ async def test_duplicate( ): MockConfigEntry(domain=DOMAIN, data=first_input).add_to_hass(hass) + second_gateway_type = second_input.pop(CONF_GATEWAY_TYPE) result = await hass.config_entries.flow.async_init( - DOMAIN, data=second_input, context={"source": config_entries.SOURCE_IMPORT} + DOMAIN, + data={CONF_GATEWAY_TYPE: second_gateway_type}, + context={"source": config_entries.SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + second_input, ) await hass.async_block_till_done() - if expected_result is None: - assert result["type"] == "create_entry" - else: - assert result["type"] == "abort" - assert result["reason"] == expected_result[1] + + for key, val in expected_result.items(): + assert result[key] == val # type: ignore[literal-required] diff --git a/tests/components/mysensors/test_init.py b/tests/components/mysensors/test_init.py index bb5d77dc7e3..5d44cdbdb3c 100644 --- a/tests/components/mysensors/test_init.py +++ b/tests/components/mysensors/test_init.py @@ -2,360 +2,19 @@ from __future__ import annotations from collections.abc import Awaitable, Callable -from typing import Any -from unittest.mock import patch from aiohttp import ClientWebSocketResponse from mysensors import BaseSyncGateway from mysensors.sensor import Sensor -import pytest -from homeassistant.components.mysensors import ( - CONF_BAUD_RATE, - CONF_DEVICE, - CONF_GATEWAYS, - CONF_PERSISTENCE, - CONF_PERSISTENCE_FILE, - CONF_RETAIN, - CONF_TCP_PORT, - CONF_VERSION, - DEFAULT_VERSION, - DOMAIN, -) -from homeassistant.components.mysensors.const import ( - CONF_GATEWAY_TYPE, - CONF_GATEWAY_TYPE_MQTT, - CONF_GATEWAY_TYPE_SERIAL, - CONF_GATEWAY_TYPE_TCP, - CONF_TOPIC_IN_PREFIX, - CONF_TOPIC_OUT_PREFIX, -) +from homeassistant.components.mysensors import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -@pytest.mark.parametrize( - "config, expected_calls, expected_to_succeed, expected_config_entry_data", - [ - ( - { - DOMAIN: { - CONF_GATEWAYS: [ - { - CONF_DEVICE: "COM5", - CONF_PERSISTENCE_FILE: "bla.json", - CONF_BAUD_RATE: 57600, - CONF_TCP_PORT: 5003, - } - ], - CONF_VERSION: "2.3", - CONF_PERSISTENCE: False, - CONF_RETAIN: True, - } - }, - 1, - True, - [ - { - CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, - CONF_DEVICE: "COM5", - CONF_PERSISTENCE_FILE: "bla.json", - CONF_BAUD_RATE: 57600, - CONF_VERSION: "2.3", - CONF_TCP_PORT: 5003, - CONF_TOPIC_IN_PREFIX: "", - CONF_TOPIC_OUT_PREFIX: "", - CONF_RETAIN: True, - } - ], - ), - ( - { - DOMAIN: { - CONF_GATEWAYS: [ - { - CONF_DEVICE: "127.0.0.1", - CONF_PERSISTENCE_FILE: "blub.pickle", - CONF_BAUD_RATE: 115200, - CONF_TCP_PORT: 343, - } - ], - CONF_VERSION: "2.4", - CONF_PERSISTENCE: False, - CONF_RETAIN: False, - } - }, - 1, - True, - [ - { - CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, - CONF_DEVICE: "127.0.0.1", - CONF_PERSISTENCE_FILE: "blub.pickle", - CONF_TCP_PORT: 343, - CONF_VERSION: "2.4", - CONF_BAUD_RATE: 115200, - CONF_TOPIC_IN_PREFIX: "", - CONF_TOPIC_OUT_PREFIX: "", - CONF_RETAIN: False, - } - ], - ), - ( - { - DOMAIN: { - CONF_GATEWAYS: [ - { - CONF_DEVICE: "127.0.0.1", - } - ], - CONF_PERSISTENCE: False, - CONF_RETAIN: False, - } - }, - 1, - True, - [ - { - CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_TCP, - CONF_DEVICE: "127.0.0.1", - CONF_TCP_PORT: 5003, - CONF_VERSION: DEFAULT_VERSION, - CONF_BAUD_RATE: 115200, - CONF_TOPIC_IN_PREFIX: "", - CONF_TOPIC_OUT_PREFIX: "", - CONF_RETAIN: False, - CONF_PERSISTENCE_FILE: "mysensors1.pickle", - } - ], - ), - ( - { - DOMAIN: { - CONF_GATEWAYS: [ - { - CONF_DEVICE: "mqtt", - CONF_BAUD_RATE: 115200, - CONF_TCP_PORT: 5003, - CONF_TOPIC_IN_PREFIX: "intopic", - CONF_TOPIC_OUT_PREFIX: "outtopic", - } - ], - CONF_PERSISTENCE: False, - CONF_RETAIN: False, - } - }, - 1, - True, - [ - { - CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, - CONF_DEVICE: "mqtt", - CONF_VERSION: DEFAULT_VERSION, - CONF_BAUD_RATE: 115200, - CONF_TCP_PORT: 5003, - CONF_TOPIC_OUT_PREFIX: "outtopic", - CONF_TOPIC_IN_PREFIX: "intopic", - CONF_RETAIN: False, - CONF_PERSISTENCE_FILE: "mysensors1.pickle", - } - ], - ), - ( - { - DOMAIN: { - CONF_GATEWAYS: [ - { - CONF_DEVICE: "mqtt", - CONF_BAUD_RATE: 115200, - CONF_TCP_PORT: 5003, - } - ], - CONF_PERSISTENCE: False, - CONF_RETAIN: False, - } - }, - 0, - True, - [{}], - ), - ( - { - DOMAIN: { - CONF_GATEWAYS: [ - { - CONF_DEVICE: "mqtt", - CONF_PERSISTENCE_FILE: "bla.json", - CONF_TOPIC_OUT_PREFIX: "out", - CONF_TOPIC_IN_PREFIX: "in", - CONF_BAUD_RATE: 115200, - CONF_TCP_PORT: 5003, - }, - { - CONF_DEVICE: "COM6", - CONF_PERSISTENCE_FILE: "bla2.json", - CONF_BAUD_RATE: 115200, - CONF_TCP_PORT: 5003, - }, - ], - CONF_VERSION: "2.4", - CONF_PERSISTENCE: False, - CONF_RETAIN: False, - } - }, - 2, - True, - [ - { - CONF_DEVICE: "mqtt", - CONF_PERSISTENCE_FILE: "bla.json", - CONF_TOPIC_OUT_PREFIX: "out", - CONF_TOPIC_IN_PREFIX: "in", - CONF_BAUD_RATE: 115200, - CONF_TCP_PORT: 5003, - CONF_VERSION: "2.4", - CONF_RETAIN: False, - CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT, - }, - { - CONF_DEVICE: "COM6", - CONF_PERSISTENCE_FILE: "bla2.json", - CONF_TOPIC_OUT_PREFIX: "", - CONF_TOPIC_IN_PREFIX: "", - CONF_BAUD_RATE: 115200, - CONF_TCP_PORT: 5003, - CONF_VERSION: "2.4", - CONF_RETAIN: False, - CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, - }, - ], - ), - ( - { - DOMAIN: { - CONF_GATEWAYS: [ - { - CONF_DEVICE: "mqtt", - CONF_PERSISTENCE_FILE: "bla.json", - CONF_BAUD_RATE: 115200, - CONF_TCP_PORT: 5003, - }, - { - CONF_DEVICE: "COM6", - CONF_PERSISTENCE_FILE: "bla.json", - CONF_BAUD_RATE: 115200, - CONF_TCP_PORT: 5003, - }, - ], - CONF_VERSION: "2.4", - CONF_PERSISTENCE: False, - CONF_RETAIN: False, - } - }, - 0, - False, - [{}], - ), - ( - { - DOMAIN: { - CONF_GATEWAYS: [ - { - CONF_DEVICE: "COMx", - CONF_PERSISTENCE_FILE: "bla.json", - CONF_BAUD_RATE: 115200, - CONF_TCP_PORT: 5003, - }, - ], - CONF_VERSION: "2.4", - CONF_PERSISTENCE: False, - CONF_RETAIN: False, - } - }, - 0, - True, - [{}], - ), - ( - { - DOMAIN: { - CONF_GATEWAYS: [ - { - CONF_DEVICE: "COM1", - }, - { - CONF_DEVICE: "COM2", - }, - ], - } - }, - 2, - True, - [ - { - CONF_DEVICE: "COM1", - CONF_PERSISTENCE_FILE: "mysensors1.pickle", - CONF_TOPIC_OUT_PREFIX: "", - CONF_TOPIC_IN_PREFIX: "", - CONF_BAUD_RATE: 115200, - CONF_TCP_PORT: 5003, - CONF_VERSION: "1.4", - CONF_RETAIN: True, - CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, - }, - { - CONF_DEVICE: "COM2", - CONF_PERSISTENCE_FILE: "mysensors2.pickle", - CONF_TOPIC_OUT_PREFIX: "", - CONF_TOPIC_IN_PREFIX: "", - CONF_BAUD_RATE: 115200, - CONF_TCP_PORT: 5003, - CONF_VERSION: "1.4", - CONF_RETAIN: True, - CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_SERIAL, - }, - ], - ), - ], -) -async def test_import( - hass: HomeAssistant, - mqtt: None, - config: ConfigType, - expected_calls: int, - expected_to_succeed: bool, - expected_config_entry_data: list[dict[str, Any]], -) -> None: - """Test importing a gateway.""" - - with patch("sys.platform", "win32"), patch( - "homeassistant.components.mysensors.config_flow.try_connect", return_value=True - ), patch( - "homeassistant.components.mysensors.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await async_setup_component(hass, DOMAIN, config) - assert result == expected_to_succeed - await hass.async_block_till_done() - - assert len(mock_setup_entry.mock_calls) == expected_calls - - for idx in range(expected_calls): - config_entry = mock_setup_entry.mock_calls[idx][1][1] - expected_persistence_file = expected_config_entry_data[idx].pop( - CONF_PERSISTENCE_FILE - ) - expected_persistence_path = hass.config.path(expected_persistence_file) - config_entry_data = dict(config_entry.data) - persistence_path = config_entry_data.pop(CONF_PERSISTENCE_FILE) - assert persistence_path == expected_persistence_path - assert config_entry_data == expected_config_entry_data[idx] - - async def test_remove_config_entry_device( hass: HomeAssistant, gps_sensor: Sensor, From 78cb0e24bc440a9e7a5835a43441a79bbf2dac59 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 31 May 2022 15:51:38 +0200 Subject: [PATCH 1096/3516] Improve integration sensor's time unit handling (#72759) --- .../components/integration/sensor.py | 17 +++++-- tests/components/integration/test_sensor.py | 46 ++++++++++++++++++- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 5c982c6ec5e..5d0dde3e4de 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -154,17 +154,26 @@ class IntegrationSensor(RestoreEntity, SensorEntity): self._method = integration_method self._attr_name = name if name is not None else f"{source_entity} integral" - self._unit_template = ( - f"{'' if unit_prefix is None else unit_prefix}{{}}{unit_time}" - ) + self._unit_template = f"{'' if unit_prefix is None else unit_prefix}{{}}" self._unit_of_measurement = None self._unit_prefix = UNIT_PREFIXES[unit_prefix] self._unit_time = UNIT_TIME[unit_time] + self._unit_time_str = unit_time self._attr_state_class = SensorStateClass.TOTAL self._attr_icon = "mdi:chart-histogram" self._attr_should_poll = False self._attr_extra_state_attributes = {ATTR_SOURCE_ID: source_entity} + def _unit(self, source_unit: str) -> str: + """Derive unit from the source sensor, SI prefix and time unit.""" + unit_time = self._unit_time_str + if source_unit.endswith(f"/{unit_time}"): + integral_unit = source_unit[0 : (-(1 + len(unit_time)))] + else: + integral_unit = f"{source_unit}{unit_time}" + + return self._unit_template.format(integral_unit) + async def async_added_to_hass(self): """Handle entity which will be added.""" await super().async_added_to_hass() @@ -203,7 +212,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity): update_state = False unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if unit is not None: - new_unit_of_measurement = self._unit_template.format(unit) + new_unit_of_measurement = self._unit(unit) if self._unit_of_measurement != new_unit_of_measurement: self._unit_of_measurement = new_unit_of_measurement update_state = True diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index e4173d62eb4..8999c1f8d04 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -5,12 +5,15 @@ from unittest.mock import patch from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + DATA_KILOBYTES, + DATA_RATE_BYTES_PER_SECOND, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, POWER_KILO_WATT, POWER_WATT, STATE_UNAVAILABLE, STATE_UNKNOWN, + TIME_HOURS, TIME_SECONDS, ) from homeassistant.core import HomeAssistant, State @@ -300,7 +303,9 @@ async def test_suffix(hass): assert await async_setup_component(hass, "sensor", config) entity_id = config["sensor"]["source"] - hass.states.async_set(entity_id, 1000, {ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT}) + hass.states.async_set( + entity_id, 1000, {ATTR_UNIT_OF_MEASUREMENT: DATA_RATE_BYTES_PER_SECOND} + ) await hass.async_block_till_done() now = dt_util.utcnow() + timedelta(seconds=10) @@ -308,7 +313,7 @@ async def test_suffix(hass): hass.states.async_set( entity_id, 1000, - {ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT}, + {ATTR_UNIT_OF_MEASUREMENT: DATA_RATE_BYTES_PER_SECOND}, force_update=True, ) await hass.async_block_till_done() @@ -318,6 +323,43 @@ async def test_suffix(hass): # Testing a network speed sensor at 1000 bytes/s over 10s = 10kbytes assert round(float(state.state)) == 10 + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == DATA_KILOBYTES + + +async def test_suffix_2(hass): + """Test integration sensor state.""" + config = { + "sensor": { + "platform": "integration", + "name": "integration", + "source": "sensor.cubic_meters_per_hour", + "round": 2, + "unit_time": TIME_HOURS, + } + } + + assert await async_setup_component(hass, "sensor", config) + + entity_id = config["sensor"]["source"] + hass.states.async_set(entity_id, 1000, {ATTR_UNIT_OF_MEASUREMENT: "m³/h"}) + await hass.async_block_till_done() + + now = dt_util.utcnow() + timedelta(hours=1) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.states.async_set( + entity_id, + 1000, + {ATTR_UNIT_OF_MEASUREMENT: "m³/h"}, + force_update=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.integration") + assert state is not None + + # Testing a flow sensor at 1000 m³/h over 1h = 1000 m³ + assert round(float(state.state)) == 1000 + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "m³" async def test_units(hass): From aab3fcad7b1664153b5718eb69aec57dbdde8203 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Tue, 31 May 2022 16:35:29 +0200 Subject: [PATCH 1097/3516] SmartThings issue with unique_id (#72715) Co-authored-by: Jan Bouwhuis --- homeassistant/components/smartthings/smartapp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 28b60b57447..fbd63d41373 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -405,7 +405,7 @@ async def _continue_flow( ( flow for flow in hass.config_entries.flow.async_progress_by_handler(DOMAIN) - if flow["context"]["unique_id"] == unique_id + if flow["context"].get("unique_id") == unique_id ), None, ) From d31e43b98049921950fed9cce6c5b45fd1d0c59c Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 31 May 2022 08:53:36 -0700 Subject: [PATCH 1098/3516] Bump google-nest-sdm to `2.0.0` and cleanup nest auth implementation in config flow (#72770) Cleanup nest auth implementaton in config flow --- homeassistant/components/nest/api.py | 61 +++++++++++++++----- homeassistant/components/nest/config_flow.py | 12 ++-- homeassistant/components/nest/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 56 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/nest/api.py b/homeassistant/components/nest/api.py index 3934b0b3cf1..830db926d9a 100644 --- a/homeassistant/components/nest/api.py +++ b/homeassistant/components/nest/api.py @@ -72,6 +72,36 @@ class AsyncConfigEntryAuth(AbstractAuth): return creds +class AccessTokenAuthImpl(AbstractAuth): + """Authentication implementation used during config flow, without refresh. + + This exists to allow the config flow to use the API before it has fully + created a config entry required by OAuth2Session. This does not support + refreshing tokens, which is fine since it should have been just created. + """ + + def __init__( + self, + websession: ClientSession, + access_token: str, + ) -> None: + """Init the Nest client library auth implementation.""" + super().__init__(websession, API_URL) + self._access_token = access_token + + async def async_get_access_token(self) -> str: + """Return the access token.""" + return self._access_token + + async def async_get_creds(self) -> Credentials: + """Return an OAuth credential for Pub/Sub Subscriber.""" + return Credentials( + token=self._access_token, + token_uri=OAUTH2_TOKEN, + scopes=SDM_SCOPES, + ) + + async def new_subscriber( hass: HomeAssistant, entry: ConfigEntry ) -> GoogleNestSubscriber | None: @@ -89,22 +119,27 @@ async def new_subscriber( ): _LOGGER.error("Configuration option 'subscriber_id' required") return None - return await new_subscriber_with_impl(hass, entry, subscriber_id, implementation) - - -async def new_subscriber_with_impl( - hass: HomeAssistant, - entry: ConfigEntry, - subscriber_id: str, - implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, -) -> GoogleNestSubscriber: - """Create a GoogleNestSubscriber, used during ConfigFlow.""" - config = hass.data[DOMAIN][DATA_NEST_CONFIG] - session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) auth = AsyncConfigEntryAuth( aiohttp_client.async_get_clientsession(hass), - session, + config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation), config[CONF_CLIENT_ID], config[CONF_CLIENT_SECRET], ) return GoogleNestSubscriber(auth, config[CONF_PROJECT_ID], subscriber_id) + + +def new_subscriber_with_token( + hass: HomeAssistant, + access_token: str, + project_id: str, + subscriber_id: str, +) -> GoogleNestSubscriber: + """Create a GoogleNestSubscriber with an access token.""" + return GoogleNestSubscriber( + AccessTokenAuthImpl( + aiohttp_client.async_get_clientsession(hass), + access_token, + ), + project_id, + subscriber_id, + ) diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index aeebd48abb4..61a61f6c8e0 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -43,7 +43,6 @@ from google_nest_sdm.exceptions import ( from google_nest_sdm.structure import InfoTrait, Structure import voluptuous as vol -from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult @@ -319,12 +318,11 @@ class NestFlowHandler( if not (subscriber_id := data.get(CONF_SUBSCRIBER_ID, "")): subscriber_id = _generate_subscription_id(cloud_project_id) _LOGGER.debug("Creating subscriber id '%s'", subscriber_id) - # Create a placeholder ConfigEntry to use since with the auth we've already created. - entry = ConfigEntry( - version=1, domain=DOMAIN, title="", data=self._data, source="" - ) - subscriber = await api.new_subscriber_with_impl( - self.hass, entry, subscriber_id, self.flow_impl + subscriber = api.new_subscriber_with_token( + self.hass, + self._data["token"]["access_token"], + config[CONF_PROJECT_ID], + subscriber_id, ) try: await subscriber.create_subscription() diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index ce0b68c782a..4f768e08843 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -5,7 +5,7 @@ "dependencies": ["ffmpeg", "http", "auth"], "after_dependencies": ["media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.2.0", "google-nest-sdm==1.8.0"], + "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.0.0"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 871980bffae..e7a00e1709c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -741,7 +741,7 @@ google-cloud-pubsub==2.11.0 google-cloud-texttospeech==2.11.0 # homeassistant.components.nest -google-nest-sdm==1.8.0 +google-nest-sdm==2.0.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba8a7cefd9c..c6154c71985 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -535,7 +535,7 @@ goodwe==0.2.15 google-cloud-pubsub==2.11.0 # homeassistant.components.nest -google-nest-sdm==1.8.0 +google-nest-sdm==2.0.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 From a53aaf696c04ac1fc03f7e77bce40e62d82acbfe Mon Sep 17 00:00:00 2001 From: Khole Date: Tue, 31 May 2022 16:55:00 +0100 Subject: [PATCH 1099/3516] Fix hive authentication process (#72719) * Fix hive authentication process * Update hive test scripts to add new data --- homeassistant/components/hive/__init__.py | 7 ++- homeassistant/components/hive/config_flow.py | 1 + homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hive/test_config_flow.py | 60 +++++++++++++++++++- 6 files changed, 68 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index af32d39ae8f..475bb95eeb1 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -76,8 +76,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Hive from a config entry.""" websession = aiohttp_client.async_get_clientsession(hass) - hive = Hive(websession) hive_config = dict(entry.data) + hive = Hive( + websession, + deviceGroupKey=hive_config["device_data"][0], + deviceKey=hive_config["device_data"][1], + devicePassword=hive_config["device_data"][2], + ) hive_config["options"] = {} hive_config["options"].update( diff --git a/homeassistant/components/hive/config_flow.py b/homeassistant/components/hive/config_flow.py index 2632a24e360..9c391f13294 100644 --- a/homeassistant/components/hive/config_flow.py +++ b/homeassistant/components/hive/config_flow.py @@ -103,6 +103,7 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Setup the config entry self.data["tokens"] = self.tokens + self.data["device_data"] = await self.hive_auth.getDeviceData() if self.context["source"] == config_entries.SOURCE_REAUTH: self.hass.config_entries.async_update_entry( self.entry, title=self.data["username"], data=self.data diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 19958b51bd7..472adc137ba 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,7 +3,7 @@ "name": "Hive", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": ["pyhiveapi==0.4.2"], + "requirements": ["pyhiveapi==0.5.4"], "codeowners": ["@Rendili", "@KJonline"], "iot_class": "cloud_polling", "loggers": ["apyhiveapi"] diff --git a/requirements_all.txt b/requirements_all.txt index e7a00e1709c..ae39f092709 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1538,7 +1538,7 @@ pyheos==0.7.2 pyhik==0.3.0 # homeassistant.components.hive -pyhiveapi==0.4.2 +pyhiveapi==0.5.4 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6154c71985..0e80fa6e15a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1029,7 +1029,7 @@ pyhaversion==22.4.1 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.4.2 +pyhiveapi==0.5.4 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/tests/components/hive/test_config_flow.py b/tests/components/hive/test_config_flow.py index ce13e52fe96..bb567b0bdfc 100644 --- a/tests/components/hive/test_config_flow.py +++ b/tests/components/hive/test_config_flow.py @@ -13,7 +13,7 @@ USERNAME = "username@home-assistant.com" UPDATED_USERNAME = "updated_username@home-assistant.com" PASSWORD = "test-password" UPDATED_PASSWORD = "updated-password" -INCORRECT_PASSWORD = "incoreect-password" +INCORRECT_PASSWORD = "incorrect-password" SCAN_INTERVAL = 120 UPDATED_SCAN_INTERVAL = 60 MFA_CODE = "1234" @@ -33,6 +33,13 @@ async def test_import_flow(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.getDeviceData", + return_value=[ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], ), patch( "homeassistant.components.hive.async_setup", return_value=True ) as mock_setup, patch( @@ -57,6 +64,11 @@ async def test_import_flow(hass): }, "ChallengeName": "SUCCESS", }, + "device_data": [ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], } assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 @@ -81,6 +93,13 @@ async def test_user_flow(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.getDeviceData", + return_value=[ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], ), patch( "homeassistant.components.hive.async_setup", return_value=True ) as mock_setup, patch( @@ -105,6 +124,11 @@ async def test_user_flow(hass): }, "ChallengeName": "SUCCESS", }, + "device_data": [ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], } assert len(mock_setup.mock_calls) == 1 @@ -148,6 +172,13 @@ async def test_user_flow_2fa(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.getDeviceData", + return_value=[ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], ), patch( "homeassistant.components.hive.async_setup", return_value=True ) as mock_setup, patch( @@ -171,6 +202,11 @@ async def test_user_flow_2fa(hass): }, "ChallengeName": "SUCCESS", }, + "device_data": [ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], } assert len(mock_setup.mock_calls) == 1 @@ -243,7 +279,15 @@ async def test_option_flow(hass): entry = MockConfigEntry( domain=DOMAIN, title=USERNAME, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + data={ + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + "device_data": [ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], + }, ) entry.add_to_hass(hass) @@ -317,6 +361,13 @@ async def test_user_flow_2fa_send_new_code(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.getDeviceData", + return_value=[ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], ), patch( "homeassistant.components.hive.async_setup", return_value=True ) as mock_setup, patch( @@ -340,6 +391,11 @@ async def test_user_flow_2fa_send_new_code(hass): }, "ChallengeName": "SUCCESS", }, + "device_data": [ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 From 52643d9abce34509beea213ce34ffd05274735a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 May 2022 08:11:44 -1000 Subject: [PATCH 1100/3516] Add support for async_remove_config_entry_device to isy994 (#72737) --- .coveragerc | 1 + homeassistant/components/isy994/__init__.py | 13 +++++++ homeassistant/components/isy994/services.py | 32 +++-------------- homeassistant/components/isy994/util.py | 39 +++++++++++++++++++++ 4 files changed, 57 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/isy994/util.py diff --git a/.coveragerc b/.coveragerc index 4d05e704065..c556a24e6fa 100644 --- a/.coveragerc +++ b/.coveragerc @@ -579,6 +579,7 @@ omit = homeassistant/components/isy994/sensor.py homeassistant/components/isy994/services.py homeassistant/components/isy994/switch.py + homeassistant/components/isy994/util.py homeassistant/components/itach/remote.py homeassistant/components/itunes/media_player.py homeassistant/components/jellyfin/__init__.py diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index e94b8215746..3cee445b587 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -45,6 +45,7 @@ from .const import ( ) from .helpers import _categorize_nodes, _categorize_programs, _categorize_variables from .services import async_setup_services, async_unload_services +from .util import unique_ids_for_config_entry_id CONFIG_SCHEMA = vol.Schema( { @@ -296,3 +297,15 @@ async def async_unload_entry( async_unload_services(hass) return unload_ok + + +async def async_remove_config_entry_device( + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + device_entry: dr.DeviceEntry, +) -> bool: + """Remove isy994 config entry from a device.""" + return not device_entry.identifiers.intersection( + (DOMAIN, unique_id) + for unique_id in unique_ids_for_config_entry_id(hass, config_entry.entry_id) + ) diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 654b65309fc..30b5f121df3 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -21,16 +21,8 @@ from homeassistant.helpers.entity_platform import async_get_platforms import homeassistant.helpers.entity_registry as er from homeassistant.helpers.service import entity_service_call -from .const import ( - _LOGGER, - DOMAIN, - ISY994_ISY, - ISY994_NODES, - ISY994_PROGRAMS, - ISY994_VARIABLES, - PLATFORMS, - PROGRAM_PLATFORMS, -) +from .const import _LOGGER, DOMAIN, ISY994_ISY +from .util import unique_ids_for_config_entry_id # Common Services for All Platforms: SERVICE_SYSTEM_QUERY = "system_query" @@ -282,7 +274,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 """Remove extra entities that are no longer part of the integration.""" entity_registry = er.async_get(hass) config_ids = [] - current_unique_ids = [] + current_unique_ids: set[str] = set() for config_entry_id in hass.data[DOMAIN]: entries_for_this_config = er.async_entries_for_config_entry( @@ -294,23 +286,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 for entity in entries_for_this_config ] ) - - hass_isy_data = hass.data[DOMAIN][config_entry_id] - uuid = hass_isy_data[ISY994_ISY].configuration["uuid"] - - for platform in PLATFORMS: - for node in hass_isy_data[ISY994_NODES][platform]: - if hasattr(node, "address"): - current_unique_ids.append(f"{uuid}_{node.address}") - - for platform in PROGRAM_PLATFORMS: - for _, node, _ in hass_isy_data[ISY994_PROGRAMS][platform]: - if hasattr(node, "address"): - current_unique_ids.append(f"{uuid}_{node.address}") - - for node in hass_isy_data[ISY994_VARIABLES]: - if hasattr(node, "address"): - current_unique_ids.append(f"{uuid}_{node.address}") + current_unique_ids |= unique_ids_for_config_entry_id(hass, config_entry_id) extra_entities = [ entity_id diff --git a/homeassistant/components/isy994/util.py b/homeassistant/components/isy994/util.py new file mode 100644 index 00000000000..196801c58ce --- /dev/null +++ b/homeassistant/components/isy994/util.py @@ -0,0 +1,39 @@ +"""ISY utils.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant + +from .const import ( + DOMAIN, + ISY994_ISY, + ISY994_NODES, + ISY994_PROGRAMS, + ISY994_VARIABLES, + PLATFORMS, + PROGRAM_PLATFORMS, +) + + +def unique_ids_for_config_entry_id( + hass: HomeAssistant, config_entry_id: str +) -> set[str]: + """Find all the unique ids for a config entry id.""" + hass_isy_data = hass.data[DOMAIN][config_entry_id] + uuid = hass_isy_data[ISY994_ISY].configuration["uuid"] + current_unique_ids: set[str] = {uuid} + + for platform in PLATFORMS: + for node in hass_isy_data[ISY994_NODES][platform]: + if hasattr(node, "address"): + current_unique_ids.add(f"{uuid}_{node.address}") + + for platform in PROGRAM_PLATFORMS: + for _, node, _ in hass_isy_data[ISY994_PROGRAMS][platform]: + if hasattr(node, "address"): + current_unique_ids.add(f"{uuid}_{node.address}") + + for node in hass_isy_data[ISY994_VARIABLES]: + if hasattr(node, "address"): + current_unique_ids.add(f"{uuid}_{node.address}") + + return current_unique_ids From eda2be8489184408bd7baf4aed311178805fe2fb Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 31 May 2022 20:30:33 +0200 Subject: [PATCH 1101/3516] Update frontend to 20220531.0 (#72775) --- 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 48488bc8f47..d9e80b4eff8 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220526.0"], + "requirements": ["home-assistant-frontend==20220531.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a43b4f99f63..a3d8a00bcfb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220526.0 +home-assistant-frontend==20220531.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index ae39f092709..97f204e32b4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -822,7 +822,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220526.0 +home-assistant-frontend==20220531.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0e80fa6e15a..c13b5c5e91d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -589,7 +589,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220526.0 +home-assistant-frontend==20220531.0 # homeassistant.components.home_connect homeconnect==0.7.0 From 638992f9c4044d26d75bb498b72bfe212fc114fc Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 1 Jun 2022 04:34:52 +1000 Subject: [PATCH 1102/3516] Make zone condition more robust by ignoring unavailable and unknown entities (#72751) * ignore entities with state unavailable or unknown * test for unavailable entity --- homeassistant/helpers/condition.py | 6 +++ tests/components/geo_location/test_trigger.py | 42 ++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 8985b7b721c..a628cdefff4 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -829,6 +829,12 @@ def zone( else: entity_id = entity.entity_id + if entity.state in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + return False + latitude = entity.attributes.get(ATTR_LATITUDE) longitude = entity.attributes.get(ATTR_LONGITUDE) diff --git a/tests/components/geo_location/test_trigger.py b/tests/components/geo_location/test_trigger.py index bbf5f42ed60..de6276545b7 100644 --- a/tests/components/geo_location/test_trigger.py +++ b/tests/components/geo_location/test_trigger.py @@ -4,7 +4,12 @@ import logging import pytest from homeassistant.components import automation, zone -from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF +from homeassistant.const import ( + ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, + SERVICE_TURN_OFF, + STATE_UNAVAILABLE, +) from homeassistant.core import Context from homeassistant.setup import async_setup_component @@ -189,6 +194,41 @@ async def test_if_fires_on_zone_leave(hass, calls): assert len(calls) == 1 +async def test_if_fires_on_zone_leave_2(hass, calls): + """Test for firing on zone leave for unavailable entity.""" + hass.states.async_set( + "geo_location.entity", + "hello", + {"latitude": 32.880586, "longitude": -117.237564, "source": "test_source"}, + ) + await hass.async_block_till_done() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "geo_location", + "source": "test_source", + "zone": "zone.test", + "event": "enter", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + hass.states.async_set( + "geo_location.entity", + STATE_UNAVAILABLE, + {"source": "test_source"}, + ) + await hass.async_block_till_done() + + assert len(calls) == 0 + + async def test_if_not_fires_for_leave_on_zone_enter(hass, calls): """Test for not firing on zone enter.""" hass.states.async_set( From a3e1b285cf35b9c500c1caf8f1d1d7db17aecdc2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 31 May 2022 13:09:07 -0600 Subject: [PATCH 1103/3516] Alter RainMachine to not create entities if the underlying data is missing (#72733) --- .coveragerc | 1 + .../components/rainmachine/__init__.py | 3 -- .../components/rainmachine/binary_sensor.py | 44 ++++++++----------- homeassistant/components/rainmachine/model.py | 1 + .../components/rainmachine/sensor.py | 32 ++++++-------- homeassistant/components/rainmachine/util.py | 14 ++++++ 6 files changed, 49 insertions(+), 46 deletions(-) create mode 100644 homeassistant/components/rainmachine/util.py diff --git a/.coveragerc b/.coveragerc index c556a24e6fa..204353ffe87 100644 --- a/.coveragerc +++ b/.coveragerc @@ -970,6 +970,7 @@ omit = homeassistant/components/rainmachine/model.py homeassistant/components/rainmachine/sensor.py homeassistant/components/rainmachine/switch.py + homeassistant/components/rainmachine/util.py homeassistant/components/raspyrfm/* homeassistant/components/recollect_waste/__init__.py homeassistant/components/recollect_waste/sensor.py diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index d212f1638b4..6d51be9d921 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -50,10 +50,7 @@ from .const import ( LOGGER, ) -DEFAULT_ATTRIBUTION = "Data provided by Green Electronics LLC" -DEFAULT_ICON = "mdi:water" DEFAULT_SSL = True -DEFAULT_UPDATE_INTERVAL = timedelta(seconds=15) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 730b51c142a..1818222a8f4 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -1,6 +1,5 @@ """This platform provides binary sensors for key RainMachine data.""" from dataclasses import dataclass -from functools import partial from homeassistant.components.binary_sensor import ( BinarySensorEntity, @@ -21,6 +20,7 @@ from .const import ( DOMAIN, ) from .model import RainMachineDescriptionMixinApiCategory +from .util import key_exists TYPE_FLOW_SENSOR = "flow_sensor" TYPE_FREEZE = "freeze" @@ -46,6 +46,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Flow Sensor", icon="mdi:water-pump", api_category=DATA_PROVISION_SETTINGS, + data_key="useFlowSensor", ), RainMachineBinarySensorDescription( key=TYPE_FREEZE, @@ -53,6 +54,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_CURRENT, + data_key="freeze", ), RainMachineBinarySensorDescription( key=TYPE_FREEZE_PROTECTION, @@ -60,6 +62,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( icon="mdi:weather-snowy", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_UNIVERSAL, + data_key="freezeProtectEnabled", ), RainMachineBinarySensorDescription( key=TYPE_HOT_DAYS, @@ -67,6 +70,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( icon="mdi:thermometer-lines", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_UNIVERSAL, + data_key="hotDaysExtraWatering", ), RainMachineBinarySensorDescription( key=TYPE_HOURLY, @@ -75,6 +79,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, + data_key="hourly", ), RainMachineBinarySensorDescription( key=TYPE_MONTH, @@ -83,6 +88,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, + data_key="month", ), RainMachineBinarySensorDescription( key=TYPE_RAINDELAY, @@ -91,6 +97,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, + data_key="rainDelay", ), RainMachineBinarySensorDescription( key=TYPE_RAINSENSOR, @@ -99,6 +106,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, + data_key="rainSensor", ), RainMachineBinarySensorDescription( key=TYPE_WEEKDAY, @@ -107,6 +115,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, + data_key="weekDay", ), ) @@ -118,35 +127,20 @@ async def async_setup_entry( controller = hass.data[DOMAIN][entry.entry_id][DATA_CONTROLLER] coordinators = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] - @callback - def async_get_sensor_by_api_category(api_category: str) -> partial: - """Generate the appropriate sensor object for an API category.""" - 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], - ) + api_category_sensor_map = { + DATA_PROVISION_SETTINGS: ProvisionSettingsBinarySensor, + DATA_RESTRICTIONS_CURRENT: CurrentRestrictionsBinarySensor, + DATA_RESTRICTIONS_UNIVERSAL: UniversalRestrictionsBinarySensor, + } async_add_entities( [ - async_get_sensor_by_api_category(description.api_category)( - controller, description + api_category_sensor_map[description.api_category]( + entry, coordinator, controller, description ) for description in BINARY_SENSOR_DESCRIPTIONS + if (coordinator := coordinators[description.api_category]) is not None + and key_exists(coordinator.data, description.data_key) ] ) diff --git a/homeassistant/components/rainmachine/model.py b/homeassistant/components/rainmachine/model.py index 9f638d486aa..680a47c5d42 100644 --- a/homeassistant/components/rainmachine/model.py +++ b/homeassistant/components/rainmachine/model.py @@ -7,6 +7,7 @@ class RainMachineDescriptionMixinApiCategory: """Define an entity description mixin for binary and regular sensors.""" api_category: str + data_key: str @dataclass diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index a2b0f7cd539..522c57cf7a2 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -3,7 +3,6 @@ from __future__ import annotations from dataclasses import dataclass from datetime import datetime, timedelta -from functools import partial from homeassistant.components.sensor import ( SensorDeviceClass, @@ -33,6 +32,7 @@ from .model import ( RainMachineDescriptionMixinApiCategory, RainMachineDescriptionMixinUid, ) +from .util import key_exists DEFAULT_ZONE_COMPLETION_TIME_WOBBLE_TOLERANCE = timedelta(seconds=5) @@ -68,6 +68,7 @@ SENSOR_DESCRIPTIONS = ( entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, api_category=DATA_PROVISION_SETTINGS, + data_key="flowSensorClicksPerCubicMeter", ), RainMachineSensorDescriptionApiCategory( key=TYPE_FLOW_SENSOR_CONSUMED_LITERS, @@ -78,6 +79,7 @@ SENSOR_DESCRIPTIONS = ( entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, api_category=DATA_PROVISION_SETTINGS, + data_key="flowSensorWateringClicks", ), RainMachineSensorDescriptionApiCategory( key=TYPE_FLOW_SENSOR_START_INDEX, @@ -87,6 +89,7 @@ SENSOR_DESCRIPTIONS = ( native_unit_of_measurement="index", entity_registry_enabled_default=False, api_category=DATA_PROVISION_SETTINGS, + data_key="flowSensorStartIndex", ), RainMachineSensorDescriptionApiCategory( key=TYPE_FLOW_SENSOR_WATERING_CLICKS, @@ -97,6 +100,7 @@ SENSOR_DESCRIPTIONS = ( entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, api_category=DATA_PROVISION_SETTINGS, + data_key="flowSensorWateringClicks", ), RainMachineSensorDescriptionApiCategory( key=TYPE_FREEZE_TEMP, @@ -107,6 +111,7 @@ SENSOR_DESCRIPTIONS = ( device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, api_category=DATA_RESTRICTIONS_UNIVERSAL, + data_key="freezeProtectTemp", ), ) @@ -118,27 +123,18 @@ async def async_setup_entry( controller = hass.data[DOMAIN][entry.entry_id][DATA_CONTROLLER] coordinators = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] - @callback - def async_get_sensor_by_api_category(api_category: str) -> partial: - """Generate the appropriate sensor object for an API category.""" - if api_category == DATA_PROVISION_SETTINGS: - return partial( - ProvisionSettingsSensor, - entry, - coordinators[DATA_PROVISION_SETTINGS], - ) - - return partial( - UniversalRestrictionsSensor, - entry, - coordinators[DATA_RESTRICTIONS_UNIVERSAL], - ) + api_category_sensor_map = { + DATA_PROVISION_SETTINGS: ProvisionSettingsSensor, + DATA_RESTRICTIONS_UNIVERSAL: UniversalRestrictionsSensor, + } sensors = [ - async_get_sensor_by_api_category(description.api_category)( - controller, description + api_category_sensor_map[description.api_category]( + entry, coordinator, controller, description ) for description in SENSOR_DESCRIPTIONS + if (coordinator := coordinators[description.api_category]) is not None + and key_exists(coordinator.data, description.data_key) ] zone_coordinator = coordinators[DATA_ZONES] diff --git a/homeassistant/components/rainmachine/util.py b/homeassistant/components/rainmachine/util.py new file mode 100644 index 00000000000..27a0636688e --- /dev/null +++ b/homeassistant/components/rainmachine/util.py @@ -0,0 +1,14 @@ +"""Define RainMachine utilities.""" +from __future__ import annotations + +from typing import Any + + +def key_exists(data: dict[str, Any], search_key: str) -> bool: + """Return whether a key exists in a nested dict.""" + for key, value in data.items(): + if key == search_key: + return True + if isinstance(value, dict): + return key_exists(value, search_key) + return False From d9d22a95563c745ce6a50095f7de902eb078805d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 May 2022 09:18:11 -1000 Subject: [PATCH 1104/3516] Initial orjson support (#72754) --- homeassistant/components/history/__init__.py | 2 +- .../components/logbook/websocket_api.py | 2 +- homeassistant/components/recorder/const.py | 8 +-- homeassistant/components/recorder/core.py | 10 ++-- homeassistant/components/recorder/models.py | 59 ++++++++++--------- .../components/websocket_api/commands.py | 18 +++--- .../components/websocket_api/connection.py | 3 +- .../components/websocket_api/const.py | 7 --- .../components/websocket_api/messages.py | 7 ++- homeassistant/helpers/aiohttp_client.py | 2 + homeassistant/helpers/json.py | 47 ++++++++++++++- homeassistant/package_constraints.txt | 1 + homeassistant/scripts/benchmark/__init__.py | 3 +- homeassistant/util/json.py | 9 ++- pyproject.toml | 2 + requirements.txt | 1 + tests/components/energy/test_validate.py | 7 ++- .../components/websocket_api/test_commands.py | 6 +- 18 files changed, 127 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 27acff54f99..77301532d3d 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -24,10 +24,10 @@ from homeassistant.components.recorder.statistics import ( ) from homeassistant.components.recorder.util import session_scope from homeassistant.components.websocket_api import messages -from homeassistant.components.websocket_api.const import JSON_DUMP from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA +from homeassistant.helpers.json import JSON_DUMP from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index 0bb7877b95b..7265bcbae86 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -14,9 +14,9 @@ from homeassistant.components import websocket_api from homeassistant.components.recorder import get_instance from homeassistant.components.websocket_api import messages from homeassistant.components.websocket_api.connection import ActiveConnection -from homeassistant.components.websocket_api.const import JSON_DUMP from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.json import JSON_DUMP import homeassistant.util.dt as dt_util from .helpers import ( diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index e558d19b530..e94092d2154 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -1,12 +1,11 @@ """Recorder constants.""" -from functools import partial -import json -from typing import Final from homeassistant.backports.enum import StrEnum from homeassistant.const import ATTR_ATTRIBUTION, ATTR_RESTORED, ATTR_SUPPORTED_FEATURES -from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.json import ( # noqa: F401 pylint: disable=unused-import + JSON_DUMP, +) DATA_INSTANCE = "recorder_instance" SQLITE_URL_PREFIX = "sqlite://" @@ -27,7 +26,6 @@ MAX_ROWS_TO_PURGE = 998 DB_WORKER_PREFIX = "DbWorker" -JSON_DUMP: Final = partial(json.dumps, cls=JSONEncoder, separators=(",", ":")) ALL_DOMAIN_EXCLUDE_ATTRS = {ATTR_ATTRIBUTION, ATTR_RESTORED, ATTR_SUPPORTED_FEATURES} diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 7df4cf57e56..8b15e15042f 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -744,11 +744,12 @@ class Recorder(threading.Thread): return try: - shared_data = EventData.shared_data_from_event(event) + shared_data_bytes = EventData.shared_data_bytes_from_event(event) except (TypeError, ValueError) as ex: _LOGGER.warning("Event is not JSON serializable: %s: %s", event, ex) return + shared_data = shared_data_bytes.decode("utf-8") # Matching attributes found in the pending commit if pending_event_data := self._pending_event_data.get(shared_data): dbevent.event_data_rel = pending_event_data @@ -756,7 +757,7 @@ class Recorder(threading.Thread): elif data_id := self._event_data_ids.get(shared_data): dbevent.data_id = data_id else: - data_hash = EventData.hash_shared_data(shared_data) + data_hash = EventData.hash_shared_data_bytes(shared_data_bytes) # Matching attributes found in the database if data_id := self._find_shared_data_in_db(data_hash, shared_data): self._event_data_ids[shared_data] = dbevent.data_id = data_id @@ -775,7 +776,7 @@ class Recorder(threading.Thread): assert self.event_session is not None try: dbstate = States.from_event(event) - shared_attrs = StateAttributes.shared_attrs_from_event( + shared_attrs_bytes = StateAttributes.shared_attrs_bytes_from_event( event, self._exclude_attributes_by_domain ) except (TypeError, ValueError) as ex: @@ -786,6 +787,7 @@ class Recorder(threading.Thread): ) return + shared_attrs = shared_attrs_bytes.decode("utf-8") dbstate.attributes = None # Matching attributes found in the pending commit if pending_attributes := self._pending_state_attributes.get(shared_attrs): @@ -794,7 +796,7 @@ class Recorder(threading.Thread): elif attributes_id := self._state_attributes_ids.get(shared_attrs): dbstate.attributes_id = attributes_id else: - attr_hash = StateAttributes.hash_shared_attrs(shared_attrs) + attr_hash = StateAttributes.hash_shared_attrs_bytes(shared_attrs_bytes) # Matching attributes found in the database if attributes_id := self._find_shared_attr_in_db(attr_hash, shared_attrs): dbstate.attributes_id = attributes_id diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 70c816c2af5..e0a22184cc8 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -3,12 +3,12 @@ from __future__ import annotations from collections.abc import Callable from datetime import datetime, timedelta -import json import logging from typing import Any, TypedDict, cast, overload import ciso8601 from fnvhash import fnv1a_32 +import orjson from sqlalchemy import ( JSON, BigInteger, @@ -46,9 +46,10 @@ from homeassistant.const import ( MAX_LENGTH_STATE_STATE, ) from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id +from homeassistant.helpers.json import JSON_DUMP, json_bytes import homeassistant.util.dt as dt_util -from .const import ALL_DOMAIN_EXCLUDE_ATTRS, JSON_DUMP +from .const import ALL_DOMAIN_EXCLUDE_ATTRS # SQLAlchemy Schema # pylint: disable=invalid-name @@ -132,7 +133,7 @@ class JSONLiteral(JSON): # type: ignore[misc] def process(value: Any) -> str: """Dump json.""" - return json.dumps(value) + return JSON_DUMP(value) return process @@ -199,7 +200,7 @@ class Events(Base): # type: ignore[misc,valid-type] try: return Event( self.event_type, - json.loads(self.event_data) if self.event_data else {}, + orjson.loads(self.event_data) if self.event_data else {}, EventOrigin(self.origin) if self.origin else EVENT_ORIGIN_ORDER[self.origin_idx], @@ -207,7 +208,7 @@ class Events(Base): # type: ignore[misc,valid-type] context=context, ) except ValueError: - # When json.loads fails + # When orjson.loads fails _LOGGER.exception("Error converting to event: %s", self) return None @@ -235,25 +236,26 @@ class EventData(Base): # type: ignore[misc,valid-type] @staticmethod def from_event(event: Event) -> EventData: """Create object from an event.""" - shared_data = JSON_DUMP(event.data) + shared_data = json_bytes(event.data) return EventData( - shared_data=shared_data, hash=EventData.hash_shared_data(shared_data) + shared_data=shared_data.decode("utf-8"), + hash=EventData.hash_shared_data_bytes(shared_data), ) @staticmethod - def shared_data_from_event(event: Event) -> str: - """Create shared_attrs from an event.""" - return JSON_DUMP(event.data) + def shared_data_bytes_from_event(event: Event) -> bytes: + """Create shared_data from an event.""" + return json_bytes(event.data) @staticmethod - def hash_shared_data(shared_data: str) -> int: + def hash_shared_data_bytes(shared_data_bytes: bytes) -> int: """Return the hash of json encoded shared data.""" - return cast(int, fnv1a_32(shared_data.encode("utf-8"))) + return cast(int, fnv1a_32(shared_data_bytes)) def to_native(self) -> dict[str, Any]: """Convert to an HA state object.""" try: - return cast(dict[str, Any], json.loads(self.shared_data)) + return cast(dict[str, Any], orjson.loads(self.shared_data)) except ValueError: _LOGGER.exception("Error converting row to event data: %s", self) return {} @@ -340,9 +342,9 @@ class States(Base): # type: ignore[misc,valid-type] parent_id=self.context_parent_id, ) try: - attrs = json.loads(self.attributes) if self.attributes else {} + attrs = orjson.loads(self.attributes) if self.attributes else {} except ValueError: - # When json.loads fails + # When orjson.loads fails _LOGGER.exception("Error converting row to state: %s", self) return None if self.last_changed is None or self.last_changed == self.last_updated: @@ -388,40 +390,39 @@ class StateAttributes(Base): # type: ignore[misc,valid-type] """Create object from a state_changed event.""" state: State | None = event.data.get("new_state") # None state means the state was removed from the state machine - dbstate = StateAttributes( - shared_attrs="{}" if state is None else JSON_DUMP(state.attributes) - ) - dbstate.hash = StateAttributes.hash_shared_attrs(dbstate.shared_attrs) + attr_bytes = b"{}" if state is None else json_bytes(state.attributes) + dbstate = StateAttributes(shared_attrs=attr_bytes.decode("utf-8")) + dbstate.hash = StateAttributes.hash_shared_attrs_bytes(attr_bytes) return dbstate @staticmethod - def shared_attrs_from_event( + def shared_attrs_bytes_from_event( event: Event, exclude_attrs_by_domain: dict[str, set[str]] - ) -> str: + ) -> bytes: """Create shared_attrs from a state_changed event.""" state: State | None = event.data.get("new_state") # None state means the state was removed from the state machine if state is None: - return "{}" + return b"{}" domain = split_entity_id(state.entity_id)[0] exclude_attrs = ( exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS ) - return JSON_DUMP( + return json_bytes( {k: v for k, v in state.attributes.items() if k not in exclude_attrs} ) @staticmethod - def hash_shared_attrs(shared_attrs: str) -> int: - """Return the hash of json encoded shared attributes.""" - return cast(int, fnv1a_32(shared_attrs.encode("utf-8"))) + def hash_shared_attrs_bytes(shared_attrs_bytes: bytes) -> int: + """Return the hash of orjson encoded shared attributes.""" + return cast(int, fnv1a_32(shared_attrs_bytes)) def to_native(self) -> dict[str, Any]: """Convert to an HA state object.""" try: - return cast(dict[str, Any], json.loads(self.shared_attrs)) + return cast(dict[str, Any], orjson.loads(self.shared_attrs)) except ValueError: - # When json.loads fails + # When orjson.loads fails _LOGGER.exception("Error converting row to state attributes: %s", self) return {} @@ -835,7 +836,7 @@ def decode_attributes_from_row( if not source or source == EMPTY_JSON_OBJECT: return {} try: - attr_cache[source] = attributes = json.loads(source) + attr_cache[source] = attributes = orjson.loads(source) except ValueError: _LOGGER.exception("Error converting row to state attributes: %s", source) attr_cache[source] = attributes = {} diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 61bcb8badf0..bea08722eb0 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -29,7 +29,7 @@ from homeassistant.helpers.event import ( TrackTemplateResult, async_track_template_result, ) -from homeassistant.helpers.json import ExtendedJSONEncoder +from homeassistant.helpers.json import JSON_DUMP, ExtendedJSONEncoder from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import IntegrationNotFound, async_get_integration from homeassistant.setup import DATA_SETUP_TIME, async_get_loaded_integrations @@ -241,13 +241,13 @@ def handle_get_states( # to succeed for the UI to show. response = messages.result_message(msg["id"], states) try: - connection.send_message(const.JSON_DUMP(response)) + connection.send_message(JSON_DUMP(response)) return except (ValueError, TypeError): connection.logger.error( "Unable to serialize to JSON. Bad data found at %s", format_unserializable_data( - find_paths_unserializable_data(response, dump=const.JSON_DUMP) + find_paths_unserializable_data(response, dump=JSON_DUMP) ), ) del response @@ -256,13 +256,13 @@ def handle_get_states( serialized = [] for state in states: try: - serialized.append(const.JSON_DUMP(state)) + serialized.append(JSON_DUMP(state)) except (ValueError, TypeError): # Error is already logged above pass # We now have partially serialized states. Craft some JSON. - response2 = const.JSON_DUMP(messages.result_message(msg["id"], ["TO_REPLACE"])) + response2 = JSON_DUMP(messages.result_message(msg["id"], ["TO_REPLACE"])) response2 = response2.replace('"TO_REPLACE"', ", ".join(serialized)) connection.send_message(response2) @@ -315,13 +315,13 @@ def handle_subscribe_entities( # to succeed for the UI to show. response = messages.event_message(msg["id"], data) try: - connection.send_message(const.JSON_DUMP(response)) + connection.send_message(JSON_DUMP(response)) return except (ValueError, TypeError): connection.logger.error( "Unable to serialize to JSON. Bad data found at %s", format_unserializable_data( - find_paths_unserializable_data(response, dump=const.JSON_DUMP) + find_paths_unserializable_data(response, dump=JSON_DUMP) ), ) del response @@ -330,14 +330,14 @@ def handle_subscribe_entities( cannot_serialize: list[str] = [] for entity_id, state_dict in add_entities.items(): try: - const.JSON_DUMP(state_dict) + JSON_DUMP(state_dict) except (ValueError, TypeError): cannot_serialize.append(entity_id) for entity_id in cannot_serialize: del add_entities[entity_id] - connection.send_message(const.JSON_DUMP(messages.event_message(msg["id"], data))) + connection.send_message(JSON_DUMP(messages.event_message(msg["id"], data))) @decorators.websocket_command({vol.Required("type"): "get_services"}) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 0280863f83e..26c4c6f8321 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant.auth.models import RefreshToken, User from homeassistant.core import Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized +from homeassistant.helpers.json import JSON_DUMP from . import const, messages @@ -56,7 +57,7 @@ class ActiveConnection: async def send_big_result(self, msg_id: int, result: Any) -> None: """Send a result message that would be expensive to JSON serialize.""" content = await self.hass.async_add_executor_job( - const.JSON_DUMP, messages.result_message(msg_id, result) + JSON_DUMP, messages.result_message(msg_id, result) ) self.send_message(content) diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 107cf6d0270..60a00126092 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -4,12 +4,9 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable from concurrent import futures -from functools import partial -import json from typing import TYPE_CHECKING, Any, Final from homeassistant.core import HomeAssistant -from homeassistant.helpers.json import JSONEncoder if TYPE_CHECKING: from .connection import ActiveConnection # noqa: F401 @@ -53,10 +50,6 @@ SIGNAL_WEBSOCKET_DISCONNECTED: Final = "websocket_disconnected" # Data used to store the current connection list DATA_CONNECTIONS: Final = f"{DOMAIN}.connections" -JSON_DUMP: Final = partial( - json.dumps, cls=JSONEncoder, allow_nan=False, separators=(",", ":") -) - COMPRESSED_STATE_STATE = "s" COMPRESSED_STATE_ATTRIBUTES = "a" COMPRESSED_STATE_CONTEXT = "c" diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index f546ba5eec6..c3e5f6bb5f5 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.core import Event, State from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.json import JSON_DUMP from homeassistant.util.json import ( find_paths_unserializable_data, format_unserializable_data, @@ -193,15 +194,15 @@ def compressed_state_dict_add(state: State) -> dict[str, Any]: def message_to_json(message: dict[str, Any]) -> str: """Serialize a websocket message to json.""" try: - return const.JSON_DUMP(message) + return JSON_DUMP(message) except (ValueError, TypeError): _LOGGER.error( "Unable to serialize to JSON. Bad data found at %s", format_unserializable_data( - find_paths_unserializable_data(message, dump=const.JSON_DUMP) + find_paths_unserializable_data(message, dump=JSON_DUMP) ), ) - return const.JSON_DUMP( + return JSON_DUMP( error_message( message["id"], const.ERR_UNKNOWN_ERROR, "Invalid JSON in response" ) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index eaabb002b0a..2e56698db41 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -14,6 +14,7 @@ from aiohttp import web from aiohttp.hdrs import CONTENT_TYPE, USER_AGENT from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout import async_timeout +import orjson from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ @@ -97,6 +98,7 @@ def _async_create_clientsession( """Create a new ClientSession with kwargs, i.e. for cookies.""" clientsession = aiohttp.ClientSession( connector=_async_get_connector(hass, verify_ssl), + json_serialize=lambda x: orjson.dumps(x).decode("utf-8"), **kwargs, ) # Prevent packages accidentally overriding our default headers diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index c581e5a9361..912667a13b5 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -1,7 +1,10 @@ """Helpers to help with encoding Home Assistant objects in JSON.""" import datetime import json -from typing import Any +from pathlib import Path +from typing import Any, Final + +import orjson class JSONEncoder(json.JSONEncoder): @@ -22,6 +25,20 @@ class JSONEncoder(json.JSONEncoder): return json.JSONEncoder.default(self, o) +def json_encoder_default(obj: Any) -> Any: + """Convert Home Assistant objects. + + Hand other objects to the original method. + """ + if isinstance(obj, set): + return list(obj) + if hasattr(obj, "as_dict"): + return obj.as_dict() + if isinstance(obj, Path): + return obj.as_posix() + raise TypeError + + class ExtendedJSONEncoder(JSONEncoder): """JSONEncoder that supports Home Assistant objects and falls back to repr(o).""" @@ -40,3 +57,31 @@ class ExtendedJSONEncoder(JSONEncoder): return super().default(o) except TypeError: return {"__type": str(type(o)), "repr": repr(o)} + + +def json_bytes(data: Any) -> bytes: + """Dump json bytes.""" + return orjson.dumps( + data, option=orjson.OPT_NON_STR_KEYS, default=json_encoder_default + ) + + +def json_dumps(data: Any) -> str: + """Dump json string. + + orjson supports serializing dataclasses natively which + eliminates the need to implement as_dict in many places + when the data is already in a dataclass. This works + well as long as all the data in the dataclass can also + be serialized. + + If it turns out to be a problem we can disable this + with option |= orjson.OPT_PASSTHROUGH_DATACLASS and it + will fallback to as_dict + """ + return orjson.dumps( + data, option=orjson.OPT_NON_STR_KEYS, default=json_encoder_default + ).decode("utf-8") + + +JSON_DUMP: Final = json_dumps diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a3d8a00bcfb..c158d26a9aa 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,6 +20,7 @@ httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.7 +orjson==3.6.8 paho-mqtt==1.6.1 pillow==9.1.1 pip>=21.0,<22.2 diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index a681b3e210d..efbfec5e961 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -12,14 +12,13 @@ from timeit import default_timer as timer from typing import TypeVar from homeassistant import core -from homeassistant.components.websocket_api.const import JSON_DUMP from homeassistant.const import EVENT_STATE_CHANGED from homeassistant.helpers.entityfilter import convert_include_exclude_filter from homeassistant.helpers.event import ( async_track_state_change, async_track_state_change_event, ) -from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.json import JSON_DUMP, JSONEncoder # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs # mypy: no-warn-return-any diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index fdee7a7a90f..82ecfd34d6d 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -7,6 +7,8 @@ import json import logging from typing import Any +import orjson + from homeassistant.core import Event, State from homeassistant.exceptions import HomeAssistantError @@ -30,7 +32,7 @@ def load_json(filename: str, default: list | dict | None = None) -> list | dict: """ try: with open(filename, encoding="utf-8") as fdesc: - return json.loads(fdesc.read()) # type: ignore[no-any-return] + return orjson.loads(fdesc.read()) # type: ignore[no-any-return] except FileNotFoundError: # This is not a fatal error _LOGGER.debug("JSON file not found: %s", filename) @@ -56,7 +58,10 @@ def save_json( Returns True on success. """ try: - json_data = json.dumps(data, indent=4, cls=encoder) + if encoder: + json_data = json.dumps(data, indent=2, cls=encoder) + else: + json_data = orjson.dumps(data, option=orjson.OPT_INDENT_2).decode("utf-8") except TypeError as error: msg = f"Failed to serialize to JSON: {filename}. Bad data at {format_unserializable_data(find_paths_unserializable_data(data))}" _LOGGER.error(msg) diff --git a/pyproject.toml b/pyproject.toml index cc745f58ad6..7e62bafd6af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ dependencies = [ "PyJWT==2.4.0", # PyJWT has loose dependency. We want the latest one. "cryptography==36.0.2", + "orjson==3.6.8", "pip>=21.0,<22.2", "python-slugify==4.0.1", "pyyaml==6.0", @@ -119,6 +120,7 @@ extension-pkg-allow-list = [ "av.audio.stream", "av.stream", "ciso8601", + "orjson", "cv2", ] diff --git a/requirements.txt b/requirements.txt index fe2bf87ad25..9805ae7cd47 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,7 @@ ifaddr==0.1.7 jinja2==3.1.2 PyJWT==2.4.0 cryptography==36.0.2 +orjson==3.6.8 pip>=21.0,<22.2 python-slugify==4.0.1 pyyaml==6.0 diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py index 37ebe4147c5..e802688daaf 100644 --- a/tests/components/energy/test_validate.py +++ b/tests/components/energy/test_validate.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest from homeassistant.components.energy import async_get_manager, validate +from homeassistant.helpers.json import JSON_DUMP from homeassistant.setup import async_setup_component @@ -408,7 +409,11 @@ async def test_validation_grid( }, ) - assert (await validate.async_validate(hass)).as_dict() == { + result = await validate.async_validate(hass) + # verify its also json serializable + JSON_DUMP(result) + + assert result.as_dict() == { "energy_sources": [ [ { diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 4d3302f7c13..0f4695596fc 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -619,12 +619,15 @@ async def test_states_filters_visible(hass, hass_admin_user, websocket_client): async def test_get_states_not_allows_nan(hass, websocket_client): - """Test get_states command not allows NaN floats.""" + """Test get_states command converts NaN to None.""" hass.states.async_set("greeting.hello", "world") hass.states.async_set("greeting.bad", "data", {"hello": float("NaN")}) hass.states.async_set("greeting.bye", "universe") await websocket_client.send_json({"id": 5, "type": "get_states"}) + bad = dict(hass.states.get("greeting.bad").as_dict()) + bad["attributes"] = dict(bad["attributes"]) + bad["attributes"]["hello"] = None msg = await websocket_client.receive_json() assert msg["id"] == 5 @@ -632,6 +635,7 @@ async def test_get_states_not_allows_nan(hass, websocket_client): assert msg["success"] assert msg["result"] == [ hass.states.get("greeting.hello").as_dict(), + bad, hass.states.get("greeting.bye").as_dict(), ] From 935ef79156742a8e3ef58e85df8d1029b3f8b8ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 May 2022 09:24:18 -1000 Subject: [PATCH 1105/3516] Fix queries for logbook context_ids running in the wrong executor (#72778) --- homeassistant/components/logbook/websocket_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index 7265bcbae86..461ed018090 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -475,7 +475,7 @@ async def ws_get_events( ) connection.send_message( - await hass.async_add_executor_job( + await get_instance(hass).async_add_executor_job( _ws_formatted_get_events, msg["id"], start_time, From 7854aaa74698e04a82b29f2f1106bb693d33c702 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 31 May 2022 15:24:35 -0400 Subject: [PATCH 1106/3516] Bump ZHA quirks lib to 0.0.75 (#72765) --- 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 8d6e6162d76..4f16b1c113e 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.30.0", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.74", + "zha-quirks==0.0.75", "zigpy-deconz==0.16.0", "zigpy==0.45.1", "zigpy-xbee==0.14.0", diff --git a/requirements_all.txt b/requirements_all.txt index 97f204e32b4..b6d24b95d5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2501,7 +2501,7 @@ zengge==0.2 zeroconf==0.38.6 # homeassistant.components.zha -zha-quirks==0.0.74 +zha-quirks==0.0.75 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c13b5c5e91d..230ceae7fa0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1647,7 +1647,7 @@ youless-api==0.16 zeroconf==0.38.6 # homeassistant.components.zha -zha-quirks==0.0.74 +zha-quirks==0.0.75 # homeassistant.components.zha zigpy-deconz==0.16.0 From 35ee4ad55b6ddacc130f85d1390b13a142abb9bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 May 2022 10:08:04 -1000 Subject: [PATCH 1107/3516] Prevent live logbook from sending state changed events when we only want device ids (#72780) --- homeassistant/components/logbook/helpers.py | 6 ++++++ tests/components/logbook/test_websocket_api.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index cc9ea238f8b..de021994b8d 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -132,6 +132,12 @@ def async_subscribe_events( if not _is_state_filtered(ent_reg, state): target(event) + if device_ids and not entity_ids: + # No entities to subscribe to but we are filtering + # on device ids so we do not want to get any state + # changed events + return + if entity_ids: subscriptions.append( async_track_state_change_event( diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 1d35d6d897d..291c487b35b 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -1743,6 +1743,8 @@ async def test_subscribe_unsubscribe_logbook_stream_device( assert msg["type"] == "event" assert msg["event"]["events"] == [] + hass.states.async_set("binary_sensor.should_not_appear", STATE_ON) + hass.states.async_set("binary_sensor.should_not_appear", STATE_OFF) hass.bus.async_fire("mock_event", {"device_id": device.id}) await hass.async_block_till_done() From 84779482b83aed5326821d2e6518dbd5177faef2 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 31 May 2022 22:08:50 +0200 Subject: [PATCH 1108/3516] Don't set headers kwargs multiple times (#72779) --- homeassistant/helpers/config_entry_oauth2_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 365ced24929..9322d6e9dc1 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -493,13 +493,13 @@ async def async_oauth2_request( This method will not refresh tokens. Use OAuth2 session for that. """ session = async_get_clientsession(hass) - + headers = kwargs.pop("headers", {}) return await session.request( method, url, **kwargs, headers={ - **(kwargs.get("headers") or {}), + **headers, "authorization": f"Bearer {token['access_token']}", }, ) From 6e06b6c9ede838883c14737b7f6d35ef14082caa Mon Sep 17 00:00:00 2001 From: eyager1 <44526531+eyager1@users.noreply.github.com> Date: Mon, 30 May 2022 18:32:52 -0400 Subject: [PATCH 1109/3516] Add empty string to list of invalid states (#72590) Add null state to list of invalid states --- homeassistant/components/statistics/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index ac62b63e8ca..3f33fa015b9 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -350,7 +350,7 @@ class StatisticsSensor(SensorEntity): if new_state.state == STATE_UNAVAILABLE: self.attributes[STAT_SOURCE_VALUE_VALID] = None return - if new_state.state in (STATE_UNKNOWN, None): + if new_state.state in (STATE_UNKNOWN, None, ""): self.attributes[STAT_SOURCE_VALUE_VALID] = False return From 4bf5132a06fc2adb33a90ede356c22448662e20a Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Tue, 31 May 2022 16:35:29 +0200 Subject: [PATCH 1110/3516] SmartThings issue with unique_id (#72715) Co-authored-by: Jan Bouwhuis --- homeassistant/components/smartthings/smartapp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 28b60b57447..fbd63d41373 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -405,7 +405,7 @@ async def _continue_flow( ( flow for flow in hass.config_entries.flow.async_progress_by_handler(DOMAIN) - if flow["context"]["unique_id"] == unique_id + if flow["context"].get("unique_id") == unique_id ), None, ) From 1b2cb4eab73db6164c1bcb82f501d3d3404872fa Mon Sep 17 00:00:00 2001 From: Khole Date: Tue, 31 May 2022 16:55:00 +0100 Subject: [PATCH 1111/3516] Fix hive authentication process (#72719) * Fix hive authentication process * Update hive test scripts to add new data --- homeassistant/components/hive/__init__.py | 7 ++- homeassistant/components/hive/config_flow.py | 1 + homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hive/test_config_flow.py | 60 +++++++++++++++++++- 6 files changed, 68 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 00c3a327578..292bbe62ae1 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -76,8 +76,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Hive from a config entry.""" websession = aiohttp_client.async_get_clientsession(hass) - hive = Hive(websession) hive_config = dict(entry.data) + hive = Hive( + websession, + deviceGroupKey=hive_config["device_data"][0], + deviceKey=hive_config["device_data"][1], + devicePassword=hive_config["device_data"][2], + ) hive_config["options"] = {} hive_config["options"].update( diff --git a/homeassistant/components/hive/config_flow.py b/homeassistant/components/hive/config_flow.py index 2632a24e360..9c391f13294 100644 --- a/homeassistant/components/hive/config_flow.py +++ b/homeassistant/components/hive/config_flow.py @@ -103,6 +103,7 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Setup the config entry self.data["tokens"] = self.tokens + self.data["device_data"] = await self.hive_auth.getDeviceData() if self.context["source"] == config_entries.SOURCE_REAUTH: self.hass.config_entries.async_update_entry( self.entry, title=self.data["username"], data=self.data diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 19958b51bd7..472adc137ba 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,7 +3,7 @@ "name": "Hive", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": ["pyhiveapi==0.4.2"], + "requirements": ["pyhiveapi==0.5.4"], "codeowners": ["@Rendili", "@KJonline"], "iot_class": "cloud_polling", "loggers": ["apyhiveapi"] diff --git a/requirements_all.txt b/requirements_all.txt index 3203dd79f6a..1821fb8bbc9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1538,7 +1538,7 @@ pyheos==0.7.2 pyhik==0.3.0 # homeassistant.components.hive -pyhiveapi==0.4.2 +pyhiveapi==0.5.4 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 171ae908991..1f89c83485b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1029,7 +1029,7 @@ pyhaversion==22.4.1 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.4.2 +pyhiveapi==0.5.4 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/tests/components/hive/test_config_flow.py b/tests/components/hive/test_config_flow.py index ce13e52fe96..bb567b0bdfc 100644 --- a/tests/components/hive/test_config_flow.py +++ b/tests/components/hive/test_config_flow.py @@ -13,7 +13,7 @@ USERNAME = "username@home-assistant.com" UPDATED_USERNAME = "updated_username@home-assistant.com" PASSWORD = "test-password" UPDATED_PASSWORD = "updated-password" -INCORRECT_PASSWORD = "incoreect-password" +INCORRECT_PASSWORD = "incorrect-password" SCAN_INTERVAL = 120 UPDATED_SCAN_INTERVAL = 60 MFA_CODE = "1234" @@ -33,6 +33,13 @@ async def test_import_flow(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.getDeviceData", + return_value=[ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], ), patch( "homeassistant.components.hive.async_setup", return_value=True ) as mock_setup, patch( @@ -57,6 +64,11 @@ async def test_import_flow(hass): }, "ChallengeName": "SUCCESS", }, + "device_data": [ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], } assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 @@ -81,6 +93,13 @@ async def test_user_flow(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.getDeviceData", + return_value=[ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], ), patch( "homeassistant.components.hive.async_setup", return_value=True ) as mock_setup, patch( @@ -105,6 +124,11 @@ async def test_user_flow(hass): }, "ChallengeName": "SUCCESS", }, + "device_data": [ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], } assert len(mock_setup.mock_calls) == 1 @@ -148,6 +172,13 @@ async def test_user_flow_2fa(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.getDeviceData", + return_value=[ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], ), patch( "homeassistant.components.hive.async_setup", return_value=True ) as mock_setup, patch( @@ -171,6 +202,11 @@ async def test_user_flow_2fa(hass): }, "ChallengeName": "SUCCESS", }, + "device_data": [ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], } assert len(mock_setup.mock_calls) == 1 @@ -243,7 +279,15 @@ async def test_option_flow(hass): entry = MockConfigEntry( domain=DOMAIN, title=USERNAME, - data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + data={ + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + "device_data": [ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], + }, ) entry.add_to_hass(hass) @@ -317,6 +361,13 @@ async def test_user_flow_2fa_send_new_code(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.getDeviceData", + return_value=[ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], ), patch( "homeassistant.components.hive.async_setup", return_value=True ) as mock_setup, patch( @@ -340,6 +391,11 @@ async def test_user_flow_2fa_send_new_code(hass): }, "ChallengeName": "SUCCESS", }, + "device_data": [ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 From 7c2f73ddbaf4ade60be3206e4abcc672a432ec61 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 31 May 2022 13:09:07 -0600 Subject: [PATCH 1112/3516] Alter RainMachine to not create entities if the underlying data is missing (#72733) --- .coveragerc | 1 + .../components/rainmachine/__init__.py | 3 -- .../components/rainmachine/binary_sensor.py | 44 ++++++++----------- homeassistant/components/rainmachine/model.py | 1 + .../components/rainmachine/sensor.py | 32 ++++++-------- homeassistant/components/rainmachine/util.py | 14 ++++++ 6 files changed, 49 insertions(+), 46 deletions(-) create mode 100644 homeassistant/components/rainmachine/util.py diff --git a/.coveragerc b/.coveragerc index 62e6d3cb94e..9e2c1e8c678 100644 --- a/.coveragerc +++ b/.coveragerc @@ -965,6 +965,7 @@ omit = homeassistant/components/rainmachine/model.py homeassistant/components/rainmachine/sensor.py homeassistant/components/rainmachine/switch.py + homeassistant/components/rainmachine/util.py homeassistant/components/raspyrfm/* homeassistant/components/recollect_waste/__init__.py homeassistant/components/recollect_waste/sensor.py diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index d212f1638b4..6d51be9d921 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -50,10 +50,7 @@ from .const import ( LOGGER, ) -DEFAULT_ATTRIBUTION = "Data provided by Green Electronics LLC" -DEFAULT_ICON = "mdi:water" DEFAULT_SSL = True -DEFAULT_UPDATE_INTERVAL = timedelta(seconds=15) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 730b51c142a..1818222a8f4 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -1,6 +1,5 @@ """This platform provides binary sensors for key RainMachine data.""" from dataclasses import dataclass -from functools import partial from homeassistant.components.binary_sensor import ( BinarySensorEntity, @@ -21,6 +20,7 @@ from .const import ( DOMAIN, ) from .model import RainMachineDescriptionMixinApiCategory +from .util import key_exists TYPE_FLOW_SENSOR = "flow_sensor" TYPE_FREEZE = "freeze" @@ -46,6 +46,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Flow Sensor", icon="mdi:water-pump", api_category=DATA_PROVISION_SETTINGS, + data_key="useFlowSensor", ), RainMachineBinarySensorDescription( key=TYPE_FREEZE, @@ -53,6 +54,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_CURRENT, + data_key="freeze", ), RainMachineBinarySensorDescription( key=TYPE_FREEZE_PROTECTION, @@ -60,6 +62,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( icon="mdi:weather-snowy", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_UNIVERSAL, + data_key="freezeProtectEnabled", ), RainMachineBinarySensorDescription( key=TYPE_HOT_DAYS, @@ -67,6 +70,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( icon="mdi:thermometer-lines", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_UNIVERSAL, + data_key="hotDaysExtraWatering", ), RainMachineBinarySensorDescription( key=TYPE_HOURLY, @@ -75,6 +79,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, + data_key="hourly", ), RainMachineBinarySensorDescription( key=TYPE_MONTH, @@ -83,6 +88,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, + data_key="month", ), RainMachineBinarySensorDescription( key=TYPE_RAINDELAY, @@ -91,6 +97,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, + data_key="rainDelay", ), RainMachineBinarySensorDescription( key=TYPE_RAINSENSOR, @@ -99,6 +106,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, + data_key="rainSensor", ), RainMachineBinarySensorDescription( key=TYPE_WEEKDAY, @@ -107,6 +115,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, + data_key="weekDay", ), ) @@ -118,35 +127,20 @@ async def async_setup_entry( controller = hass.data[DOMAIN][entry.entry_id][DATA_CONTROLLER] coordinators = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] - @callback - def async_get_sensor_by_api_category(api_category: str) -> partial: - """Generate the appropriate sensor object for an API category.""" - 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], - ) + api_category_sensor_map = { + DATA_PROVISION_SETTINGS: ProvisionSettingsBinarySensor, + DATA_RESTRICTIONS_CURRENT: CurrentRestrictionsBinarySensor, + DATA_RESTRICTIONS_UNIVERSAL: UniversalRestrictionsBinarySensor, + } async_add_entities( [ - async_get_sensor_by_api_category(description.api_category)( - controller, description + api_category_sensor_map[description.api_category]( + entry, coordinator, controller, description ) for description in BINARY_SENSOR_DESCRIPTIONS + if (coordinator := coordinators[description.api_category]) is not None + and key_exists(coordinator.data, description.data_key) ] ) diff --git a/homeassistant/components/rainmachine/model.py b/homeassistant/components/rainmachine/model.py index 9f638d486aa..680a47c5d42 100644 --- a/homeassistant/components/rainmachine/model.py +++ b/homeassistant/components/rainmachine/model.py @@ -7,6 +7,7 @@ class RainMachineDescriptionMixinApiCategory: """Define an entity description mixin for binary and regular sensors.""" api_category: str + data_key: str @dataclass diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index a2b0f7cd539..522c57cf7a2 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -3,7 +3,6 @@ from __future__ import annotations from dataclasses import dataclass from datetime import datetime, timedelta -from functools import partial from homeassistant.components.sensor import ( SensorDeviceClass, @@ -33,6 +32,7 @@ from .model import ( RainMachineDescriptionMixinApiCategory, RainMachineDescriptionMixinUid, ) +from .util import key_exists DEFAULT_ZONE_COMPLETION_TIME_WOBBLE_TOLERANCE = timedelta(seconds=5) @@ -68,6 +68,7 @@ SENSOR_DESCRIPTIONS = ( entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, api_category=DATA_PROVISION_SETTINGS, + data_key="flowSensorClicksPerCubicMeter", ), RainMachineSensorDescriptionApiCategory( key=TYPE_FLOW_SENSOR_CONSUMED_LITERS, @@ -78,6 +79,7 @@ SENSOR_DESCRIPTIONS = ( entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, api_category=DATA_PROVISION_SETTINGS, + data_key="flowSensorWateringClicks", ), RainMachineSensorDescriptionApiCategory( key=TYPE_FLOW_SENSOR_START_INDEX, @@ -87,6 +89,7 @@ SENSOR_DESCRIPTIONS = ( native_unit_of_measurement="index", entity_registry_enabled_default=False, api_category=DATA_PROVISION_SETTINGS, + data_key="flowSensorStartIndex", ), RainMachineSensorDescriptionApiCategory( key=TYPE_FLOW_SENSOR_WATERING_CLICKS, @@ -97,6 +100,7 @@ SENSOR_DESCRIPTIONS = ( entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, api_category=DATA_PROVISION_SETTINGS, + data_key="flowSensorWateringClicks", ), RainMachineSensorDescriptionApiCategory( key=TYPE_FREEZE_TEMP, @@ -107,6 +111,7 @@ SENSOR_DESCRIPTIONS = ( device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, api_category=DATA_RESTRICTIONS_UNIVERSAL, + data_key="freezeProtectTemp", ), ) @@ -118,27 +123,18 @@ async def async_setup_entry( controller = hass.data[DOMAIN][entry.entry_id][DATA_CONTROLLER] coordinators = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] - @callback - def async_get_sensor_by_api_category(api_category: str) -> partial: - """Generate the appropriate sensor object for an API category.""" - if api_category == DATA_PROVISION_SETTINGS: - return partial( - ProvisionSettingsSensor, - entry, - coordinators[DATA_PROVISION_SETTINGS], - ) - - return partial( - UniversalRestrictionsSensor, - entry, - coordinators[DATA_RESTRICTIONS_UNIVERSAL], - ) + api_category_sensor_map = { + DATA_PROVISION_SETTINGS: ProvisionSettingsSensor, + DATA_RESTRICTIONS_UNIVERSAL: UniversalRestrictionsSensor, + } sensors = [ - async_get_sensor_by_api_category(description.api_category)( - controller, description + api_category_sensor_map[description.api_category]( + entry, coordinator, controller, description ) for description in SENSOR_DESCRIPTIONS + if (coordinator := coordinators[description.api_category]) is not None + and key_exists(coordinator.data, description.data_key) ] zone_coordinator = coordinators[DATA_ZONES] diff --git a/homeassistant/components/rainmachine/util.py b/homeassistant/components/rainmachine/util.py new file mode 100644 index 00000000000..27a0636688e --- /dev/null +++ b/homeassistant/components/rainmachine/util.py @@ -0,0 +1,14 @@ +"""Define RainMachine utilities.""" +from __future__ import annotations + +from typing import Any + + +def key_exists(data: dict[str, Any], search_key: str) -> bool: + """Return whether a key exists in a nested dict.""" + for key, value in data.items(): + if key == search_key: + return True + if isinstance(value, dict): + return key_exists(value, search_key) + return False From ca8c750a5aa4bdb2ce52b051314e56b6766f3844 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 May 2022 22:41:33 -1000 Subject: [PATCH 1113/3516] Small performance improvement for matching logbook rows (#72750) --- homeassistant/components/logbook/processor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index ea6002cc62c..b3a43c2ca35 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -407,7 +407,8 @@ class ContextAugmenter: def _rows_match(row: Row | EventAsRow, other_row: Row | EventAsRow) -> bool: """Check of rows match by using the same method as Events __hash__.""" if ( - (state_id := row.state_id) is not None + row is other_row + or (state_id := row.state_id) is not None and state_id == other_row.state_id or (event_id := row.event_id) is not None and event_id == other_row.event_id From 6b3a284135a9712379b5c11cd3a3c8a3236d3f95 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 1 Jun 2022 04:34:52 +1000 Subject: [PATCH 1114/3516] Make zone condition more robust by ignoring unavailable and unknown entities (#72751) * ignore entities with state unavailable or unknown * test for unavailable entity --- homeassistant/helpers/condition.py | 6 +++ tests/components/geo_location/test_trigger.py | 42 ++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 8985b7b721c..a628cdefff4 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -829,6 +829,12 @@ def zone( else: entity_id = entity.entity_id + if entity.state in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + return False + latitude = entity.attributes.get(ATTR_LATITUDE) longitude = entity.attributes.get(ATTR_LONGITUDE) diff --git a/tests/components/geo_location/test_trigger.py b/tests/components/geo_location/test_trigger.py index bbf5f42ed60..de6276545b7 100644 --- a/tests/components/geo_location/test_trigger.py +++ b/tests/components/geo_location/test_trigger.py @@ -4,7 +4,12 @@ import logging import pytest from homeassistant.components import automation, zone -from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF +from homeassistant.const import ( + ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, + SERVICE_TURN_OFF, + STATE_UNAVAILABLE, +) from homeassistant.core import Context from homeassistant.setup import async_setup_component @@ -189,6 +194,41 @@ async def test_if_fires_on_zone_leave(hass, calls): assert len(calls) == 1 +async def test_if_fires_on_zone_leave_2(hass, calls): + """Test for firing on zone leave for unavailable entity.""" + hass.states.async_set( + "geo_location.entity", + "hello", + {"latitude": 32.880586, "longitude": -117.237564, "source": "test_source"}, + ) + await hass.async_block_till_done() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": { + "platform": "geo_location", + "source": "test_source", + "zone": "zone.test", + "event": "enter", + }, + "action": {"service": "test.automation"}, + } + }, + ) + + hass.states.async_set( + "geo_location.entity", + STATE_UNAVAILABLE, + {"source": "test_source"}, + ) + await hass.async_block_till_done() + + assert len(calls) == 0 + + async def test_if_not_fires_for_leave_on_zone_enter(hass, calls): """Test for not firing on zone enter.""" hass.states.async_set( From 82ed6869d05bfaa38214b2e6de80f24406928b35 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 31 May 2022 15:51:38 +0200 Subject: [PATCH 1115/3516] Improve integration sensor's time unit handling (#72759) --- .../components/integration/sensor.py | 17 +++++-- tests/components/integration/test_sensor.py | 46 ++++++++++++++++++- 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 5c982c6ec5e..5d0dde3e4de 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -154,17 +154,26 @@ class IntegrationSensor(RestoreEntity, SensorEntity): self._method = integration_method self._attr_name = name if name is not None else f"{source_entity} integral" - self._unit_template = ( - f"{'' if unit_prefix is None else unit_prefix}{{}}{unit_time}" - ) + self._unit_template = f"{'' if unit_prefix is None else unit_prefix}{{}}" self._unit_of_measurement = None self._unit_prefix = UNIT_PREFIXES[unit_prefix] self._unit_time = UNIT_TIME[unit_time] + self._unit_time_str = unit_time self._attr_state_class = SensorStateClass.TOTAL self._attr_icon = "mdi:chart-histogram" self._attr_should_poll = False self._attr_extra_state_attributes = {ATTR_SOURCE_ID: source_entity} + def _unit(self, source_unit: str) -> str: + """Derive unit from the source sensor, SI prefix and time unit.""" + unit_time = self._unit_time_str + if source_unit.endswith(f"/{unit_time}"): + integral_unit = source_unit[0 : (-(1 + len(unit_time)))] + else: + integral_unit = f"{source_unit}{unit_time}" + + return self._unit_template.format(integral_unit) + async def async_added_to_hass(self): """Handle entity which will be added.""" await super().async_added_to_hass() @@ -203,7 +212,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity): update_state = False unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if unit is not None: - new_unit_of_measurement = self._unit_template.format(unit) + new_unit_of_measurement = self._unit(unit) if self._unit_of_measurement != new_unit_of_measurement: self._unit_of_measurement = new_unit_of_measurement update_state = True diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index e4173d62eb4..8999c1f8d04 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -5,12 +5,15 @@ from unittest.mock import patch from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + DATA_KILOBYTES, + DATA_RATE_BYTES_PER_SECOND, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, POWER_KILO_WATT, POWER_WATT, STATE_UNAVAILABLE, STATE_UNKNOWN, + TIME_HOURS, TIME_SECONDS, ) from homeassistant.core import HomeAssistant, State @@ -300,7 +303,9 @@ async def test_suffix(hass): assert await async_setup_component(hass, "sensor", config) entity_id = config["sensor"]["source"] - hass.states.async_set(entity_id, 1000, {ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT}) + hass.states.async_set( + entity_id, 1000, {ATTR_UNIT_OF_MEASUREMENT: DATA_RATE_BYTES_PER_SECOND} + ) await hass.async_block_till_done() now = dt_util.utcnow() + timedelta(seconds=10) @@ -308,7 +313,7 @@ async def test_suffix(hass): hass.states.async_set( entity_id, 1000, - {ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT}, + {ATTR_UNIT_OF_MEASUREMENT: DATA_RATE_BYTES_PER_SECOND}, force_update=True, ) await hass.async_block_till_done() @@ -318,6 +323,43 @@ async def test_suffix(hass): # Testing a network speed sensor at 1000 bytes/s over 10s = 10kbytes assert round(float(state.state)) == 10 + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == DATA_KILOBYTES + + +async def test_suffix_2(hass): + """Test integration sensor state.""" + config = { + "sensor": { + "platform": "integration", + "name": "integration", + "source": "sensor.cubic_meters_per_hour", + "round": 2, + "unit_time": TIME_HOURS, + } + } + + assert await async_setup_component(hass, "sensor", config) + + entity_id = config["sensor"]["source"] + hass.states.async_set(entity_id, 1000, {ATTR_UNIT_OF_MEASUREMENT: "m³/h"}) + await hass.async_block_till_done() + + now = dt_util.utcnow() + timedelta(hours=1) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.states.async_set( + entity_id, + 1000, + {ATTR_UNIT_OF_MEASUREMENT: "m³/h"}, + force_update=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.integration") + assert state is not None + + # Testing a flow sensor at 1000 m³/h over 1h = 1000 m³ + assert round(float(state.state)) == 1000 + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "m³" async def test_units(hass): From d268c828ee87db85867936c931c5c6e8bf22d78a Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 31 May 2022 15:24:35 -0400 Subject: [PATCH 1116/3516] Bump ZHA quirks lib to 0.0.75 (#72765) --- 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 8d6e6162d76..4f16b1c113e 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,7 +7,7 @@ "bellows==0.30.0", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.74", + "zha-quirks==0.0.75", "zigpy-deconz==0.16.0", "zigpy==0.45.1", "zigpy-xbee==0.14.0", diff --git a/requirements_all.txt b/requirements_all.txt index 1821fb8bbc9..d9fd7a6a34e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2501,7 +2501,7 @@ zengge==0.2 zeroconf==0.38.6 # homeassistant.components.zha -zha-quirks==0.0.74 +zha-quirks==0.0.75 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f89c83485b..52e6d0a0ea9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1647,7 +1647,7 @@ youless-api==0.16 zeroconf==0.38.6 # homeassistant.components.zha -zha-quirks==0.0.74 +zha-quirks==0.0.75 # homeassistant.components.zha zigpy-deconz==0.16.0 From f4d280b59db28dda196a93b69112368de5e9d24e Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 31 May 2022 20:30:33 +0200 Subject: [PATCH 1117/3516] Update frontend to 20220531.0 (#72775) --- 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 48488bc8f47..d9e80b4eff8 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220526.0"], + "requirements": ["home-assistant-frontend==20220531.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a43b4f99f63..a3d8a00bcfb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220526.0 +home-assistant-frontend==20220531.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index d9fd7a6a34e..23b333113b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -822,7 +822,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220526.0 +home-assistant-frontend==20220531.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 52e6d0a0ea9..3eabedeb942 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -589,7 +589,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220526.0 +home-assistant-frontend==20220531.0 # homeassistant.components.home_connect homeconnect==0.7.0 From a54a5b2d2098fe962010cc47ebb84beb62e91115 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 May 2022 09:24:18 -1000 Subject: [PATCH 1118/3516] Fix queries for logbook context_ids running in the wrong executor (#72778) --- homeassistant/components/logbook/websocket_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index 0bb7877b95b..82b1db1081c 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -475,7 +475,7 @@ async def ws_get_events( ) connection.send_message( - await hass.async_add_executor_job( + await get_instance(hass).async_add_executor_job( _ws_formatted_get_events, msg["id"], start_time, From 647df29a0038a225ca57831631baf91a95f5196a Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 31 May 2022 22:08:50 +0200 Subject: [PATCH 1119/3516] Don't set headers kwargs multiple times (#72779) --- homeassistant/helpers/config_entry_oauth2_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 365ced24929..9322d6e9dc1 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -493,13 +493,13 @@ async def async_oauth2_request( This method will not refresh tokens. Use OAuth2 session for that. """ session = async_get_clientsession(hass) - + headers = kwargs.pop("headers", {}) return await session.request( method, url, **kwargs, headers={ - **(kwargs.get("headers") or {}), + **headers, "authorization": f"Bearer {token['access_token']}", }, ) From 9effb78a7fd4c154b931c5c4d412b0038dea4883 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 May 2022 10:08:04 -1000 Subject: [PATCH 1120/3516] Prevent live logbook from sending state changed events when we only want device ids (#72780) --- homeassistant/components/logbook/helpers.py | 6 ++++++ tests/components/logbook/test_websocket_api.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index cc9ea238f8b..de021994b8d 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -132,6 +132,12 @@ def async_subscribe_events( if not _is_state_filtered(ent_reg, state): target(event) + if device_ids and not entity_ids: + # No entities to subscribe to but we are filtering + # on device ids so we do not want to get any state + # changed events + return + if entity_ids: subscriptions.append( async_track_state_change_event( diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 1d35d6d897d..291c487b35b 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -1743,6 +1743,8 @@ async def test_subscribe_unsubscribe_logbook_stream_device( assert msg["type"] == "event" assert msg["event"]["events"] == [] + hass.states.async_set("binary_sensor.should_not_appear", STATE_ON) + hass.states.async_set("binary_sensor.should_not_appear", STATE_OFF) hass.bus.async_fire("mock_event", {"device_id": device.id}) await hass.async_block_till_done() From c3acdcb2c8614c87db11cf4160fa4aab9aa053b2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 May 2022 13:22:38 -0700 Subject: [PATCH 1121/3516] Bumped version to 2022.6.0b6 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index bd49bb0e1e2..1eb39c69f9d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 3d52bda7738..d7bef63ded2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.0b5 +version = 2022.6.0b6 url = https://www.home-assistant.io/ [options] From 9cea936c22581a88fe1eecfeabb836a8fffd1048 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 31 May 2022 22:44:06 +0200 Subject: [PATCH 1122/3516] Use Mapping for async_step_reauth (t-z) (#72767) * Adjust tailscale * Adjust tautulli * Adjust tile * Adjust tractive * Adjust trafikverket_ferry * Adjust trafikverket_train * Adjust unifiprotect * Adjust uptimerobot * Adjust verisure * Adjust vlc_telnet * Adjust wallbox * Adjust watttime * Adjust yale_smart_alarm --- homeassistant/components/tailscale/config_flow.py | 3 ++- homeassistant/components/tautulli/config_flow.py | 3 ++- homeassistant/components/tile/config_flow.py | 3 ++- homeassistant/components/tractive/config_flow.py | 3 ++- homeassistant/components/trafikverket_ferry/config_flow.py | 5 ++--- homeassistant/components/trafikverket_train/config_flow.py | 5 ++--- homeassistant/components/unifiprotect/config_flow.py | 3 ++- homeassistant/components/uptimerobot/config_flow.py | 5 ++--- homeassistant/components/verisure/config_flow.py | 3 ++- homeassistant/components/vlc_telnet/config_flow.py | 3 ++- homeassistant/components/wallbox/config_flow.py | 5 ++--- homeassistant/components/watttime/config_flow.py | 3 ++- homeassistant/components/yale_smart_alarm/config_flow.py | 5 ++--- 13 files changed, 26 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/tailscale/config_flow.py b/homeassistant/components/tailscale/config_flow.py index f1180db5254..a51cb722988 100644 --- a/homeassistant/components/tailscale/config_flow.py +++ b/homeassistant/components/tailscale/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure the Tailscale integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any from tailscale import Tailscale, TailscaleAuthenticationError, TailscaleError @@ -81,7 +82,7 @@ class TailscaleFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle initiation of re-authentication with Tailscale.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/tautulli/config_flow.py b/homeassistant/components/tautulli/config_flow.py index ea470e2e1d0..b4f3e3985ec 100644 --- a/homeassistant/components/tautulli/config_flow.py +++ b/homeassistant/components/tautulli/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Tautulli.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any from pytautulli import ( @@ -70,7 +71,7 @@ class TautulliConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: """Handle a reauthorization flow request.""" return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/tile/config_flow.py b/homeassistant/components/tile/config_flow.py index e1424453075..c47a46b3b10 100644 --- a/homeassistant/components/tile/config_flow.py +++ b/homeassistant/components/tile/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure the Tile integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any from pytile import async_login @@ -74,7 +75,7 @@ class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_config) - async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._username = config[CONF_USERNAME] return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/tractive/config_flow.py b/homeassistant/components/tractive/config_flow.py index 7ba6602a520..647d97f7179 100644 --- a/homeassistant/components/tractive/config_flow.py +++ b/homeassistant/components/tractive/config_flow.py @@ -1,6 +1,7 @@ """Config flow for tractive integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -69,7 +70,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=USER_DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, _: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/trafikverket_ferry/config_flow.py b/homeassistant/components/trafikverket_ferry/config_flow.py index 7f9737cf686..2b0a1dec655 100644 --- a/homeassistant/components/trafikverket_ferry/config_flow.py +++ b/homeassistant/components/trafikverket_ferry/config_flow.py @@ -1,6 +1,7 @@ """Adds config flow for Trafikverket Ferry integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any from pytrafikverket import TrafikverketFerry @@ -59,9 +60,7 @@ class TVFerryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ferry_api = TrafikverketFerry(web_session, api_key) await ferry_api.async_get_next_ferry_stop(ferry_from, ferry_to) - async def async_step_reauth( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle re-authentication with Trafikverket.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) diff --git a/homeassistant/components/trafikverket_train/config_flow.py b/homeassistant/components/trafikverket_train/config_flow.py index 823b393f7b1..521e499ec5d 100644 --- a/homeassistant/components/trafikverket_train/config_flow.py +++ b/homeassistant/components/trafikverket_train/config_flow.py @@ -1,6 +1,7 @@ """Adds config flow for Trafikverket Train integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any from pytrafikverket import TrafikverketTrain @@ -54,9 +55,7 @@ class TVTrainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await train_api.async_get_train_station(train_from) await train_api.async_get_train_station(train_to) - async def async_step_reauth( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle re-authentication with Trafikverket.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index daaae214df9..23e2541e6d8 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -1,6 +1,7 @@ """Config Flow to configure UniFi Protect Integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -236,7 +237,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return nvr_data, errors - async def async_step_reauth(self, user_input: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) diff --git a/homeassistant/components/uptimerobot/config_flow.py b/homeassistant/components/uptimerobot/config_flow.py index 5b6ac1d4880..83371bdd4a7 100644 --- a/homeassistant/components/uptimerobot/config_flow.py +++ b/homeassistant/components/uptimerobot/config_flow.py @@ -1,6 +1,7 @@ """Config flow for UptimeRobot integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any from pyuptimerobot import ( @@ -84,9 +85,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_reauth( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Return the reauth confirm step.""" return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py index 612d42bdf25..91bde6db219 100644 --- a/homeassistant/components/verisure/config_flow.py +++ b/homeassistant/components/verisure/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Verisure integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any, cast from verisure import ( @@ -108,7 +109,7 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle initiation of re-authentication with Verisure.""" self.entry = cast( ConfigEntry, diff --git a/homeassistant/components/vlc_telnet/config_flow.py b/homeassistant/components/vlc_telnet/config_flow.py index 29508ad1120..9c97e876e1b 100644 --- a/homeassistant/components/vlc_telnet/config_flow.py +++ b/homeassistant/components/vlc_telnet/config_flow.py @@ -1,6 +1,7 @@ """Config flow for VLC media player Telnet integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -104,7 +105,7 @@ class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=user_form_schema(user_input), errors=errors ) - async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle reauth flow.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) assert self.entry diff --git a/homeassistant/components/wallbox/config_flow.py b/homeassistant/components/wallbox/config_flow.py index d2c0a048fa1..dbd1f3612a5 100644 --- a/homeassistant/components/wallbox/config_flow.py +++ b/homeassistant/components/wallbox/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Wallbox integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any import voluptuous as vol @@ -47,9 +48,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=COMPONENT_DOMAIN): """Start the Wallbox config flow.""" self._reauth_entry: config_entries.ConfigEntry | None = None - async def async_step_reauth( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/watttime/config_flow.py b/homeassistant/components/watttime/config_flow.py index 993e070ffe8..4d6985ec616 100644 --- a/homeassistant/components/watttime/config_flow.py +++ b/homeassistant/components/watttime/config_flow.py @@ -1,6 +1,7 @@ """Config flow for WattTime integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import TYPE_CHECKING, Any from aiowatttime import Client @@ -189,7 +190,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_step_coordinates() - async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._data = {**config} return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/yale_smart_alarm/config_flow.py b/homeassistant/components/yale_smart_alarm/config_flow.py index ae5f492bc6a..a3f350cef23 100644 --- a/homeassistant/components/yale_smart_alarm/config_flow.py +++ b/homeassistant/components/yale_smart_alarm/config_flow.py @@ -1,6 +1,7 @@ """Adds config flow for Yale Smart Alarm integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any import voluptuous as vol @@ -53,9 +54,7 @@ class YaleConfigFlow(ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return YaleOptionsFlowHandler(config_entry) - async def async_step_reauth( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle initiation of re-authentication with Yale.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() From c365454afb1799c7b45cf452d67b13ede5300df0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 May 2022 10:51:55 -1000 Subject: [PATCH 1123/3516] Revert "Initial orjson support (#72754)" (#72789) This was causing the wheels to fail to build. We need to workout why when we don't have release pressure This reverts commit d9d22a95563c745ce6a50095f7de902eb078805d. --- homeassistant/components/history/__init__.py | 2 +- .../components/logbook/websocket_api.py | 2 +- homeassistant/components/recorder/const.py | 8 ++- homeassistant/components/recorder/core.py | 10 ++-- homeassistant/components/recorder/models.py | 59 +++++++++---------- .../components/websocket_api/commands.py | 18 +++--- .../components/websocket_api/connection.py | 3 +- .../components/websocket_api/const.py | 7 +++ .../components/websocket_api/messages.py | 7 +-- homeassistant/helpers/aiohttp_client.py | 2 - homeassistant/helpers/json.py | 47 +-------------- homeassistant/package_constraints.txt | 1 - homeassistant/scripts/benchmark/__init__.py | 3 +- homeassistant/util/json.py | 9 +-- pyproject.toml | 2 - requirements.txt | 1 - tests/components/energy/test_validate.py | 7 +-- .../components/websocket_api/test_commands.py | 6 +- 18 files changed, 67 insertions(+), 127 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 77301532d3d..27acff54f99 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -24,10 +24,10 @@ from homeassistant.components.recorder.statistics import ( ) from homeassistant.components.recorder.util import session_scope from homeassistant.components.websocket_api import messages +from homeassistant.components.websocket_api.const import JSON_DUMP from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA -from homeassistant.helpers.json import JSON_DUMP from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index 461ed018090..82b1db1081c 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -14,9 +14,9 @@ from homeassistant.components import websocket_api from homeassistant.components.recorder import get_instance from homeassistant.components.websocket_api import messages from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.components.websocket_api.const import JSON_DUMP from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.json import JSON_DUMP import homeassistant.util.dt as dt_util from .helpers import ( diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index e94092d2154..e558d19b530 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -1,11 +1,12 @@ """Recorder constants.""" +from functools import partial +import json +from typing import Final from homeassistant.backports.enum import StrEnum from homeassistant.const import ATTR_ATTRIBUTION, ATTR_RESTORED, ATTR_SUPPORTED_FEATURES -from homeassistant.helpers.json import ( # noqa: F401 pylint: disable=unused-import - JSON_DUMP, -) +from homeassistant.helpers.json import JSONEncoder DATA_INSTANCE = "recorder_instance" SQLITE_URL_PREFIX = "sqlite://" @@ -26,6 +27,7 @@ MAX_ROWS_TO_PURGE = 998 DB_WORKER_PREFIX = "DbWorker" +JSON_DUMP: Final = partial(json.dumps, cls=JSONEncoder, separators=(",", ":")) ALL_DOMAIN_EXCLUDE_ATTRS = {ATTR_ATTRIBUTION, ATTR_RESTORED, ATTR_SUPPORTED_FEATURES} diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 8b15e15042f..7df4cf57e56 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -744,12 +744,11 @@ class Recorder(threading.Thread): return try: - shared_data_bytes = EventData.shared_data_bytes_from_event(event) + shared_data = EventData.shared_data_from_event(event) except (TypeError, ValueError) as ex: _LOGGER.warning("Event is not JSON serializable: %s: %s", event, ex) return - shared_data = shared_data_bytes.decode("utf-8") # Matching attributes found in the pending commit if pending_event_data := self._pending_event_data.get(shared_data): dbevent.event_data_rel = pending_event_data @@ -757,7 +756,7 @@ class Recorder(threading.Thread): elif data_id := self._event_data_ids.get(shared_data): dbevent.data_id = data_id else: - data_hash = EventData.hash_shared_data_bytes(shared_data_bytes) + data_hash = EventData.hash_shared_data(shared_data) # Matching attributes found in the database if data_id := self._find_shared_data_in_db(data_hash, shared_data): self._event_data_ids[shared_data] = dbevent.data_id = data_id @@ -776,7 +775,7 @@ class Recorder(threading.Thread): assert self.event_session is not None try: dbstate = States.from_event(event) - shared_attrs_bytes = StateAttributes.shared_attrs_bytes_from_event( + shared_attrs = StateAttributes.shared_attrs_from_event( event, self._exclude_attributes_by_domain ) except (TypeError, ValueError) as ex: @@ -787,7 +786,6 @@ class Recorder(threading.Thread): ) return - shared_attrs = shared_attrs_bytes.decode("utf-8") dbstate.attributes = None # Matching attributes found in the pending commit if pending_attributes := self._pending_state_attributes.get(shared_attrs): @@ -796,7 +794,7 @@ class Recorder(threading.Thread): elif attributes_id := self._state_attributes_ids.get(shared_attrs): dbstate.attributes_id = attributes_id else: - attr_hash = StateAttributes.hash_shared_attrs_bytes(shared_attrs_bytes) + attr_hash = StateAttributes.hash_shared_attrs(shared_attrs) # Matching attributes found in the database if attributes_id := self._find_shared_attr_in_db(attr_hash, shared_attrs): dbstate.attributes_id = attributes_id diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index e0a22184cc8..70c816c2af5 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -3,12 +3,12 @@ from __future__ import annotations from collections.abc import Callable from datetime import datetime, timedelta +import json import logging from typing import Any, TypedDict, cast, overload import ciso8601 from fnvhash import fnv1a_32 -import orjson from sqlalchemy import ( JSON, BigInteger, @@ -46,10 +46,9 @@ from homeassistant.const import ( MAX_LENGTH_STATE_STATE, ) from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id -from homeassistant.helpers.json import JSON_DUMP, json_bytes import homeassistant.util.dt as dt_util -from .const import ALL_DOMAIN_EXCLUDE_ATTRS +from .const import ALL_DOMAIN_EXCLUDE_ATTRS, JSON_DUMP # SQLAlchemy Schema # pylint: disable=invalid-name @@ -133,7 +132,7 @@ class JSONLiteral(JSON): # type: ignore[misc] def process(value: Any) -> str: """Dump json.""" - return JSON_DUMP(value) + return json.dumps(value) return process @@ -200,7 +199,7 @@ class Events(Base): # type: ignore[misc,valid-type] try: return Event( self.event_type, - orjson.loads(self.event_data) if self.event_data else {}, + json.loads(self.event_data) if self.event_data else {}, EventOrigin(self.origin) if self.origin else EVENT_ORIGIN_ORDER[self.origin_idx], @@ -208,7 +207,7 @@ class Events(Base): # type: ignore[misc,valid-type] context=context, ) except ValueError: - # When orjson.loads fails + # When json.loads fails _LOGGER.exception("Error converting to event: %s", self) return None @@ -236,26 +235,25 @@ class EventData(Base): # type: ignore[misc,valid-type] @staticmethod def from_event(event: Event) -> EventData: """Create object from an event.""" - shared_data = json_bytes(event.data) + shared_data = JSON_DUMP(event.data) return EventData( - shared_data=shared_data.decode("utf-8"), - hash=EventData.hash_shared_data_bytes(shared_data), + shared_data=shared_data, hash=EventData.hash_shared_data(shared_data) ) @staticmethod - def shared_data_bytes_from_event(event: Event) -> bytes: - """Create shared_data from an event.""" - return json_bytes(event.data) + def shared_data_from_event(event: Event) -> str: + """Create shared_attrs from an event.""" + return JSON_DUMP(event.data) @staticmethod - def hash_shared_data_bytes(shared_data_bytes: bytes) -> int: + def hash_shared_data(shared_data: str) -> int: """Return the hash of json encoded shared data.""" - return cast(int, fnv1a_32(shared_data_bytes)) + return cast(int, fnv1a_32(shared_data.encode("utf-8"))) def to_native(self) -> dict[str, Any]: """Convert to an HA state object.""" try: - return cast(dict[str, Any], orjson.loads(self.shared_data)) + return cast(dict[str, Any], json.loads(self.shared_data)) except ValueError: _LOGGER.exception("Error converting row to event data: %s", self) return {} @@ -342,9 +340,9 @@ class States(Base): # type: ignore[misc,valid-type] parent_id=self.context_parent_id, ) try: - attrs = orjson.loads(self.attributes) if self.attributes else {} + attrs = json.loads(self.attributes) if self.attributes else {} except ValueError: - # When orjson.loads fails + # When json.loads fails _LOGGER.exception("Error converting row to state: %s", self) return None if self.last_changed is None or self.last_changed == self.last_updated: @@ -390,39 +388,40 @@ class StateAttributes(Base): # type: ignore[misc,valid-type] """Create object from a state_changed event.""" state: State | None = event.data.get("new_state") # None state means the state was removed from the state machine - attr_bytes = b"{}" if state is None else json_bytes(state.attributes) - dbstate = StateAttributes(shared_attrs=attr_bytes.decode("utf-8")) - dbstate.hash = StateAttributes.hash_shared_attrs_bytes(attr_bytes) + dbstate = StateAttributes( + shared_attrs="{}" if state is None else JSON_DUMP(state.attributes) + ) + dbstate.hash = StateAttributes.hash_shared_attrs(dbstate.shared_attrs) return dbstate @staticmethod - def shared_attrs_bytes_from_event( + def shared_attrs_from_event( event: Event, exclude_attrs_by_domain: dict[str, set[str]] - ) -> bytes: + ) -> str: """Create shared_attrs from a state_changed event.""" state: State | None = event.data.get("new_state") # None state means the state was removed from the state machine if state is None: - return b"{}" + return "{}" domain = split_entity_id(state.entity_id)[0] exclude_attrs = ( exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS ) - return json_bytes( + return JSON_DUMP( {k: v for k, v in state.attributes.items() if k not in exclude_attrs} ) @staticmethod - def hash_shared_attrs_bytes(shared_attrs_bytes: bytes) -> int: - """Return the hash of orjson encoded shared attributes.""" - return cast(int, fnv1a_32(shared_attrs_bytes)) + def hash_shared_attrs(shared_attrs: str) -> int: + """Return the hash of json encoded shared attributes.""" + return cast(int, fnv1a_32(shared_attrs.encode("utf-8"))) def to_native(self) -> dict[str, Any]: """Convert to an HA state object.""" try: - return cast(dict[str, Any], orjson.loads(self.shared_attrs)) + return cast(dict[str, Any], json.loads(self.shared_attrs)) except ValueError: - # When orjson.loads fails + # When json.loads fails _LOGGER.exception("Error converting row to state attributes: %s", self) return {} @@ -836,7 +835,7 @@ def decode_attributes_from_row( if not source or source == EMPTY_JSON_OBJECT: return {} try: - attr_cache[source] = attributes = orjson.loads(source) + attr_cache[source] = attributes = json.loads(source) except ValueError: _LOGGER.exception("Error converting row to state attributes: %s", source) attr_cache[source] = attributes = {} diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index bea08722eb0..61bcb8badf0 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -29,7 +29,7 @@ from homeassistant.helpers.event import ( TrackTemplateResult, async_track_template_result, ) -from homeassistant.helpers.json import JSON_DUMP, ExtendedJSONEncoder +from homeassistant.helpers.json import ExtendedJSONEncoder from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import IntegrationNotFound, async_get_integration from homeassistant.setup import DATA_SETUP_TIME, async_get_loaded_integrations @@ -241,13 +241,13 @@ def handle_get_states( # to succeed for the UI to show. response = messages.result_message(msg["id"], states) try: - connection.send_message(JSON_DUMP(response)) + connection.send_message(const.JSON_DUMP(response)) return except (ValueError, TypeError): connection.logger.error( "Unable to serialize to JSON. Bad data found at %s", format_unserializable_data( - find_paths_unserializable_data(response, dump=JSON_DUMP) + find_paths_unserializable_data(response, dump=const.JSON_DUMP) ), ) del response @@ -256,13 +256,13 @@ def handle_get_states( serialized = [] for state in states: try: - serialized.append(JSON_DUMP(state)) + serialized.append(const.JSON_DUMP(state)) except (ValueError, TypeError): # Error is already logged above pass # We now have partially serialized states. Craft some JSON. - response2 = JSON_DUMP(messages.result_message(msg["id"], ["TO_REPLACE"])) + response2 = const.JSON_DUMP(messages.result_message(msg["id"], ["TO_REPLACE"])) response2 = response2.replace('"TO_REPLACE"', ", ".join(serialized)) connection.send_message(response2) @@ -315,13 +315,13 @@ def handle_subscribe_entities( # to succeed for the UI to show. response = messages.event_message(msg["id"], data) try: - connection.send_message(JSON_DUMP(response)) + connection.send_message(const.JSON_DUMP(response)) return except (ValueError, TypeError): connection.logger.error( "Unable to serialize to JSON. Bad data found at %s", format_unserializable_data( - find_paths_unserializable_data(response, dump=JSON_DUMP) + find_paths_unserializable_data(response, dump=const.JSON_DUMP) ), ) del response @@ -330,14 +330,14 @@ def handle_subscribe_entities( cannot_serialize: list[str] = [] for entity_id, state_dict in add_entities.items(): try: - JSON_DUMP(state_dict) + const.JSON_DUMP(state_dict) except (ValueError, TypeError): cannot_serialize.append(entity_id) for entity_id in cannot_serialize: del add_entities[entity_id] - connection.send_message(JSON_DUMP(messages.event_message(msg["id"], data))) + connection.send_message(const.JSON_DUMP(messages.event_message(msg["id"], data))) @decorators.websocket_command({vol.Required("type"): "get_services"}) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 26c4c6f8321..0280863f83e 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -11,7 +11,6 @@ import voluptuous as vol from homeassistant.auth.models import RefreshToken, User from homeassistant.core import Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized -from homeassistant.helpers.json import JSON_DUMP from . import const, messages @@ -57,7 +56,7 @@ class ActiveConnection: async def send_big_result(self, msg_id: int, result: Any) -> None: """Send a result message that would be expensive to JSON serialize.""" content = await self.hass.async_add_executor_job( - JSON_DUMP, messages.result_message(msg_id, result) + const.JSON_DUMP, messages.result_message(msg_id, result) ) self.send_message(content) diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 60a00126092..107cf6d0270 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -4,9 +4,12 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable from concurrent import futures +from functools import partial +import json from typing import TYPE_CHECKING, Any, Final from homeassistant.core import HomeAssistant +from homeassistant.helpers.json import JSONEncoder if TYPE_CHECKING: from .connection import ActiveConnection # noqa: F401 @@ -50,6 +53,10 @@ SIGNAL_WEBSOCKET_DISCONNECTED: Final = "websocket_disconnected" # Data used to store the current connection list DATA_CONNECTIONS: Final = f"{DOMAIN}.connections" +JSON_DUMP: Final = partial( + json.dumps, cls=JSONEncoder, allow_nan=False, separators=(",", ":") +) + COMPRESSED_STATE_STATE = "s" COMPRESSED_STATE_ATTRIBUTES = "a" COMPRESSED_STATE_CONTEXT = "c" diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index c3e5f6bb5f5..f546ba5eec6 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -9,7 +9,6 @@ import voluptuous as vol from homeassistant.core import Event, State from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.json import JSON_DUMP from homeassistant.util.json import ( find_paths_unserializable_data, format_unserializable_data, @@ -194,15 +193,15 @@ def compressed_state_dict_add(state: State) -> dict[str, Any]: def message_to_json(message: dict[str, Any]) -> str: """Serialize a websocket message to json.""" try: - return JSON_DUMP(message) + return const.JSON_DUMP(message) except (ValueError, TypeError): _LOGGER.error( "Unable to serialize to JSON. Bad data found at %s", format_unserializable_data( - find_paths_unserializable_data(message, dump=JSON_DUMP) + find_paths_unserializable_data(message, dump=const.JSON_DUMP) ), ) - return JSON_DUMP( + return const.JSON_DUMP( error_message( message["id"], const.ERR_UNKNOWN_ERROR, "Invalid JSON in response" ) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 2e56698db41..eaabb002b0a 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -14,7 +14,6 @@ from aiohttp import web from aiohttp.hdrs import CONTENT_TYPE, USER_AGENT from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout import async_timeout -import orjson from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ @@ -98,7 +97,6 @@ def _async_create_clientsession( """Create a new ClientSession with kwargs, i.e. for cookies.""" clientsession = aiohttp.ClientSession( connector=_async_get_connector(hass, verify_ssl), - json_serialize=lambda x: orjson.dumps(x).decode("utf-8"), **kwargs, ) # Prevent packages accidentally overriding our default headers diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index 912667a13b5..c581e5a9361 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -1,10 +1,7 @@ """Helpers to help with encoding Home Assistant objects in JSON.""" import datetime import json -from pathlib import Path -from typing import Any, Final - -import orjson +from typing import Any class JSONEncoder(json.JSONEncoder): @@ -25,20 +22,6 @@ class JSONEncoder(json.JSONEncoder): return json.JSONEncoder.default(self, o) -def json_encoder_default(obj: Any) -> Any: - """Convert Home Assistant objects. - - Hand other objects to the original method. - """ - if isinstance(obj, set): - return list(obj) - if hasattr(obj, "as_dict"): - return obj.as_dict() - if isinstance(obj, Path): - return obj.as_posix() - raise TypeError - - class ExtendedJSONEncoder(JSONEncoder): """JSONEncoder that supports Home Assistant objects and falls back to repr(o).""" @@ -57,31 +40,3 @@ class ExtendedJSONEncoder(JSONEncoder): return super().default(o) except TypeError: return {"__type": str(type(o)), "repr": repr(o)} - - -def json_bytes(data: Any) -> bytes: - """Dump json bytes.""" - return orjson.dumps( - data, option=orjson.OPT_NON_STR_KEYS, default=json_encoder_default - ) - - -def json_dumps(data: Any) -> str: - """Dump json string. - - orjson supports serializing dataclasses natively which - eliminates the need to implement as_dict in many places - when the data is already in a dataclass. This works - well as long as all the data in the dataclass can also - be serialized. - - If it turns out to be a problem we can disable this - with option |= orjson.OPT_PASSTHROUGH_DATACLASS and it - will fallback to as_dict - """ - return orjson.dumps( - data, option=orjson.OPT_NON_STR_KEYS, default=json_encoder_default - ).decode("utf-8") - - -JSON_DUMP: Final = json_dumps diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c158d26a9aa..a3d8a00bcfb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,7 +20,6 @@ httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.7 -orjson==3.6.8 paho-mqtt==1.6.1 pillow==9.1.1 pip>=21.0,<22.2 diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index efbfec5e961..a681b3e210d 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -12,13 +12,14 @@ from timeit import default_timer as timer from typing import TypeVar from homeassistant import core +from homeassistant.components.websocket_api.const import JSON_DUMP from homeassistant.const import EVENT_STATE_CHANGED from homeassistant.helpers.entityfilter import convert_include_exclude_filter from homeassistant.helpers.event import ( async_track_state_change, async_track_state_change_event, ) -from homeassistant.helpers.json import JSON_DUMP, JSONEncoder +from homeassistant.helpers.json import JSONEncoder # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs # mypy: no-warn-return-any diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 82ecfd34d6d..fdee7a7a90f 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -7,8 +7,6 @@ import json import logging from typing import Any -import orjson - from homeassistant.core import Event, State from homeassistant.exceptions import HomeAssistantError @@ -32,7 +30,7 @@ def load_json(filename: str, default: list | dict | None = None) -> list | dict: """ try: with open(filename, encoding="utf-8") as fdesc: - return orjson.loads(fdesc.read()) # type: ignore[no-any-return] + return json.loads(fdesc.read()) # type: ignore[no-any-return] except FileNotFoundError: # This is not a fatal error _LOGGER.debug("JSON file not found: %s", filename) @@ -58,10 +56,7 @@ def save_json( Returns True on success. """ try: - if encoder: - json_data = json.dumps(data, indent=2, cls=encoder) - else: - json_data = orjson.dumps(data, option=orjson.OPT_INDENT_2).decode("utf-8") + json_data = json.dumps(data, indent=4, cls=encoder) except TypeError as error: msg = f"Failed to serialize to JSON: {filename}. Bad data at {format_unserializable_data(find_paths_unserializable_data(data))}" _LOGGER.error(msg) diff --git a/pyproject.toml b/pyproject.toml index 7e62bafd6af..cc745f58ad6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,6 @@ dependencies = [ "PyJWT==2.4.0", # PyJWT has loose dependency. We want the latest one. "cryptography==36.0.2", - "orjson==3.6.8", "pip>=21.0,<22.2", "python-slugify==4.0.1", "pyyaml==6.0", @@ -120,7 +119,6 @@ extension-pkg-allow-list = [ "av.audio.stream", "av.stream", "ciso8601", - "orjson", "cv2", ] diff --git a/requirements.txt b/requirements.txt index 9805ae7cd47..fe2bf87ad25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,6 @@ ifaddr==0.1.7 jinja2==3.1.2 PyJWT==2.4.0 cryptography==36.0.2 -orjson==3.6.8 pip>=21.0,<22.2 python-slugify==4.0.1 pyyaml==6.0 diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py index e802688daaf..37ebe4147c5 100644 --- a/tests/components/energy/test_validate.py +++ b/tests/components/energy/test_validate.py @@ -4,7 +4,6 @@ from unittest.mock import patch import pytest from homeassistant.components.energy import async_get_manager, validate -from homeassistant.helpers.json import JSON_DUMP from homeassistant.setup import async_setup_component @@ -409,11 +408,7 @@ async def test_validation_grid( }, ) - result = await validate.async_validate(hass) - # verify its also json serializable - JSON_DUMP(result) - - assert result.as_dict() == { + assert (await validate.async_validate(hass)).as_dict() == { "energy_sources": [ [ { diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 0f4695596fc..4d3302f7c13 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -619,15 +619,12 @@ async def test_states_filters_visible(hass, hass_admin_user, websocket_client): async def test_get_states_not_allows_nan(hass, websocket_client): - """Test get_states command converts NaN to None.""" + """Test get_states command not allows NaN floats.""" hass.states.async_set("greeting.hello", "world") hass.states.async_set("greeting.bad", "data", {"hello": float("NaN")}) hass.states.async_set("greeting.bye", "universe") await websocket_client.send_json({"id": 5, "type": "get_states"}) - bad = dict(hass.states.get("greeting.bad").as_dict()) - bad["attributes"] = dict(bad["attributes"]) - bad["attributes"]["hello"] = None msg = await websocket_client.receive_json() assert msg["id"] == 5 @@ -635,7 +632,6 @@ async def test_get_states_not_allows_nan(hass, websocket_client): assert msg["success"] assert msg["result"] == [ hass.states.get("greeting.hello").as_dict(), - bad, hass.states.get("greeting.bye").as_dict(), ] From a8da0eedd32ac8198f06d4e32622d0f8b40b4a41 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 31 May 2022 23:04:47 +0200 Subject: [PATCH 1124/3516] Add comment for editable installs (#72782) --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index dbf815a56e9..b1a2172f8f1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,6 @@ +# Setuptools v62.3 doesn't support editable installs with just 'pyproject.toml' (PEP 660). +# Keep this file until it does! + [metadata] url = https://www.home-assistant.io/ From 856e1144c992b07f3d656b2419738fa38302aae7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 May 2022 11:35:28 -1000 Subject: [PATCH 1125/3516] Ensure the statistics_meta table is using the dynamic row format (#72784) --- homeassistant/components/recorder/migration.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index eadfc543b59..bc636d34b10 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -712,6 +712,17 @@ def _apply_update( # noqa: C901 elif new_version == 29: # Recreate statistics_meta index to block duplicated statistic_id _drop_index(session_maker, "statistics_meta", "ix_statistics_meta_statistic_id") + if engine.dialect.name == SupportedDialect.MYSQL: + # Ensure the row format is dynamic or the index + # unique will be too large + with session_scope(session=session_maker()) as session: + connection = session.connection() + # This is safe to run multiple times and fast since the table is small + connection.execute( + text( + "ALTER TABLE statistics_meta ENGINE=InnoDB, ROW_FORMAT=DYNAMIC" + ) + ) try: _create_index( session_maker, "statistics_meta", "ix_statistics_meta_statistic_id" From 3258d572b04e6d304b64709b24ca50ab7b358c72 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 31 May 2022 23:46:12 +0200 Subject: [PATCH 1126/3516] Add re-auth flow to Tankerkoenig (#72682) * add reauth flow * add test * use Mapping for async_step_reauth arguments * only update changed data * improve tests * use different api key to test reauth --- .../components/tankerkoenig/__init__.py | 4 +- .../components/tankerkoenig/config_flow.py | 41 ++++++++++++++ .../components/tankerkoenig/strings.json | 8 ++- .../tankerkoenig/translations/en.json | 9 ++- .../tankerkoenig/test_config_flow.py | 55 ++++++++++++++++++- 5 files changed, 112 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index e63add83fad..b7a15e82ea8 100644 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -23,7 +23,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -187,6 +187,8 @@ class TankerkoenigDataUpdateCoordinator(DataUpdateCoordinator): try: station_data = pytankerkoenig.getStationData(self._api_key, station_id) except pytankerkoenig.customException as err: + if any(x in str(err).lower() for x in ("api-key", "apikey")): + raise ConfigEntryAuthFailed(err) from err station_data = { "ok": False, "message": err, diff --git a/homeassistant/components/tankerkoenig/config_flow.py b/homeassistant/components/tankerkoenig/config_flow.py index dd5893fe35f..77baeddfce9 100644 --- a/homeassistant/components/tankerkoenig/config_flow.py +++ b/homeassistant/components/tankerkoenig/config_flow.py @@ -144,6 +144,28 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): options={CONF_SHOW_ON_MAP: True}, ) + async def async_step_reauth(self, data: Mapping[str, Any]) -> 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[str, Any] | None = None + ) -> FlowResult: + """Perform reauth confirm upon an API authentication error.""" + if not user_input: + return self._show_form_reauth() + + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert entry + user_input = {**entry.data, **user_input} + data = await async_get_nearby_stations(self.hass, user_input) + if not data.get("ok"): + return self._show_form_reauth(user_input, {CONF_API_KEY: "invalid_auth"}) + + 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") + def _show_form_user( self, user_input: dict[str, Any] | None = None, @@ -190,6 +212,25 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) + def _show_form_reauth( + self, + user_input: dict[str, Any] | None = None, + errors: dict[str, Any] | None = None, + ) -> FlowResult: + if user_input is None: + user_input = {} + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required( + CONF_API_KEY, default=user_input.get(CONF_API_KEY, "") + ): cv.string, + } + ), + errors=errors, + ) + def _create_entry( self, data: dict[str, Any], options: dict[str, Any] ) -> FlowResult: diff --git a/homeassistant/components/tankerkoenig/strings.json b/homeassistant/components/tankerkoenig/strings.json index 5e0c367c192..e0b9b3d53e8 100644 --- a/homeassistant/components/tankerkoenig/strings.json +++ b/homeassistant/components/tankerkoenig/strings.json @@ -11,6 +11,11 @@ "radius": "Search radius" } }, + "reauth_confirm": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + }, "select_station": { "title": "Select stations to add", "description": "found {stations_count} stations in radius", @@ -20,7 +25,8 @@ } }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", diff --git a/homeassistant/components/tankerkoenig/translations/en.json b/homeassistant/components/tankerkoenig/translations/en.json index 432ad4481c8..9a69c8c812e 100644 --- a/homeassistant/components/tankerkoenig/translations/en.json +++ b/homeassistant/components/tankerkoenig/translations/en.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "Location is already configured" + "already_configured": "Location is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "invalid_auth": "Invalid authentication", "no_stations": "Could not find any station in range." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API Key" + } + }, "select_station": { "data": { "stations": "Stations" @@ -31,7 +37,6 @@ "step": { "init": { "data": { - "scan_interval": "Update Interval", "show_on_map": "Show stations on map", "stations": "Stations" }, diff --git a/tests/components/tankerkoenig/test_config_flow.py b/tests/components/tankerkoenig/test_config_flow.py index f48a09fd64b..cae78a447f8 100644 --- a/tests/components/tankerkoenig/test_config_flow.py +++ b/tests/components/tankerkoenig/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components.tankerkoenig.const import ( CONF_STATIONS, DOMAIN, ) -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, @@ -222,6 +222,59 @@ async def test_import(hass: HomeAssistant): assert mock_setup_entry.called +async def test_reauth(hass: HomeAssistant): + """Test starting a flow by user to re-auth.""" + + mock_config = MockConfigEntry( + domain=DOMAIN, + data={**MOCK_USER_DATA, **MOCK_STATIONS_DATA}, + unique_id=f"{MOCK_USER_DATA[CONF_LOCATION][CONF_LATITUDE]}_{MOCK_USER_DATA[CONF_LOCATION][CONF_LONGITUDE]}", + ) + mock_config.add_to_hass(hass) + + with patch( + "homeassistant.components.tankerkoenig.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.tankerkoenig.config_flow.getNearbyStations", + ) as mock_nearby_stations: + # re-auth initialized + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, + data=mock_config.data, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + # re-auth unsuccessful + mock_nearby_stations.return_value = {"ok": False} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "269534f6-aaaa-bbbb-cccc-yyyyzzzzxxxx", + }, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {CONF_API_KEY: "invalid_auth"} + + # re-auth successful + mock_nearby_stations.return_value = MOCK_NEARVY_STATIONS_OK + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "269534f6-aaaa-bbbb-cccc-yyyyzzzzxxxx", + }, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + + mock_setup_entry.assert_called() + + entry = hass.config_entries.async_get_entry(mock_config.entry_id) + assert entry.data[CONF_API_KEY] == "269534f6-aaaa-bbbb-cccc-yyyyzzzzxxxx" + + async def test_options_flow(hass: HomeAssistant): """Test options flow.""" From 02068a201321f54ec26d46137ffa669ce09d970b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 31 May 2022 23:57:16 +0200 Subject: [PATCH 1127/3516] Stringify mikrotik device_tracker name (#72788) Co-authored-by: J. Nick Koston --- homeassistant/components/mikrotik/device_tracker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index dee4a5de08d..16c3ed233d8 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -102,7 +102,8 @@ class MikrotikHubTracker(ScannerEntity): @property def name(self) -> str: """Return the name of the client.""" - return self.device.name + # Stringify to ensure we return a string + return str(self.device.name) @property def hostname(self) -> str: From 6d74149f22e7211173412682d999b500ccbeff42 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 May 2022 14:58:45 -0700 Subject: [PATCH 1128/3516] Sync entities when enabling/disabling Google Assistant (#72791) --- homeassistant/components/cloud/google_config.py | 9 ++++++++- homeassistant/components/google_assistant/helpers.py | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 8f190103e87..f30be66cb42 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -195,6 +195,8 @@ class CloudGoogleConfig(AbstractConfig): ): await async_setup_component(self.hass, GOOGLE_DOMAIN, {}) + sync_entities = False + if self.should_report_state != self.is_reporting_state: if self.should_report_state: self.async_enable_report_state() @@ -203,7 +205,7 @@ class CloudGoogleConfig(AbstractConfig): # State reporting is reported as a property on entities. # So when we change it, we need to sync all entities. - await self.async_sync_entities_all() + sync_entities = True # If entity prefs are the same or we have filter in config.yaml, # don't sync. @@ -215,12 +217,17 @@ class CloudGoogleConfig(AbstractConfig): if self.enabled and not self.is_local_sdk_active: self.async_enable_local_sdk() + sync_entities = True elif not self.enabled and self.is_local_sdk_active: self.async_disable_local_sdk() + sync_entities = True self._cur_entity_prefs = prefs.google_entity_configs self._cur_default_expose = prefs.google_default_expose + if sync_entities: + await self.async_sync_entities_all() + @callback def _handle_entity_registry_updated(self, event: Event) -> None: """Handle when entity registry updated.""" diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index b425367f5c3..15a8d832403 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -213,6 +213,9 @@ class AbstractConfig(ABC): async def async_sync_entities_all(self): """Sync all entities to Google for all registered agents.""" + if not self._store.agent_user_ids: + return 204 + res = await gather( *( self.async_sync_entities(agent_user_id) From 0df9cc907ad936df700ae61f6a4dd47514772203 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 1 Jun 2022 00:27:00 +0000 Subject: [PATCH 1129/3516] [ci skip] Translation update --- .../components/generic/translations/es.json | 1 + .../generic/translations/zh-Hant.json | 2 ++ .../components/hassio/translations/es.json | 1 + .../components/ialarm_xr/translations/ca.json | 1 + .../components/ialarm_xr/translations/es.json | 21 ++++++++++++++++++ .../components/ialarm_xr/translations/it.json | 1 + .../components/ialarm_xr/translations/ja.json | 1 + .../ialarm_xr/translations/zh-Hant.json | 22 +++++++++++++++++++ .../isy994/translations/zh-Hant.json | 2 +- .../components/plugwise/translations/ca.json | 6 ++++- .../components/plugwise/translations/es.json | 6 ++++- .../components/plugwise/translations/it.json | 10 ++++++--- .../components/plugwise/translations/ja.json | 6 ++++- .../plugwise/translations/zh-Hant.json | 10 ++++++--- .../components/recorder/translations/es.json | 1 + .../tankerkoenig/translations/en.json | 1 + .../tankerkoenig/translations/es.json | 3 ++- .../tankerkoenig/translations/pt-BR.json | 8 ++++++- .../tankerkoenig/translations/zh-Hant.json | 3 ++- .../totalconnect/translations/es.json | 11 ++++++++++ .../totalconnect/translations/it.json | 2 +- .../totalconnect/translations/zh-Hant.json | 11 ++++++++++ 22 files changed, 116 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/ialarm_xr/translations/es.json create mode 100644 homeassistant/components/ialarm_xr/translations/zh-Hant.json diff --git a/homeassistant/components/generic/translations/es.json b/homeassistant/components/generic/translations/es.json index 4d8019002bb..d0e2087acf7 100644 --- a/homeassistant/components/generic/translations/es.json +++ b/homeassistant/components/generic/translations/es.json @@ -39,6 +39,7 @@ "already_exists": "Ya hay una c\u00e1mara con esa URL de configuraci\u00f3n.", "stream_no_route_to_host": "No se pudo encontrar el anfitri\u00f3n mientras intentaba conectar al flujo de datos", "stream_unauthorised": "La autorizaci\u00f3n ha fallado mientras se intentaba conectar con el flujo de datos", + "template_error": "Error al renderizar la plantilla. Revise el registro para m\u00e1s informaci\u00f3n.", "timeout": "El tiempo m\u00e1ximo de carga de la URL ha expirado", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/generic/translations/zh-Hant.json b/homeassistant/components/generic/translations/zh-Hant.json index d1079724252..595bf019f64 100644 --- a/homeassistant/components/generic/translations/zh-Hant.json +++ b/homeassistant/components/generic/translations/zh-Hant.json @@ -15,6 +15,7 @@ "stream_no_video": "\u4e32\u6d41\u6c92\u6709\u5f71\u50cf", "stream_not_permitted": "\u5617\u8a66\u4e32\u6d41\u9023\u7dda\u6642\u4e0d\u5141\u8a31\u64cd\u4f5c\u3002\u8f38\u5165\u932f\u8aa4\u7684 RTSP \u50b3\u8f38\u5354\u5b9a\uff1f", "stream_unauthorised": "\u5617\u8a66\u4e32\u6d41\u9023\u7dda\u6642\u8a8d\u8b49\u5931\u6557", + "template_error": "\u6a21\u7248\u6e32\u67d3\u932f\u8aa4\u3001\u8acb\u53c3\u95b1\u65e5\u8a8c\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u6599\u3002", "timeout": "\u8f09\u5165 URL \u903e\u6642\u6642\u9593", "unable_still_load": "\u7121\u6cd5\u7531\u8a2d\u5b9a\u975c\u614b\u5f71\u50cf URL \u8f09\u5165\u6709\u6548\u5f71\u50cf\uff08\u4f8b\u5982\uff1a\u7121\u6548\u4e3b\u6a5f\u3001URL \u6216\u8a8d\u8b49\u5931\u6557\uff09\u3002\u8acb\u53c3\u95b1\u65e5\u8a8c\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8a0a\u606f\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" @@ -57,6 +58,7 @@ "stream_no_video": "\u4e32\u6d41\u6c92\u6709\u5f71\u50cf", "stream_not_permitted": "\u5617\u8a66\u4e32\u6d41\u9023\u7dda\u6642\u4e0d\u5141\u8a31\u64cd\u4f5c\u3002\u8f38\u5165\u932f\u8aa4\u7684 RTSP \u50b3\u8f38\u5354\u5b9a\uff1f", "stream_unauthorised": "\u5617\u8a66\u4e32\u6d41\u9023\u7dda\u6642\u8a8d\u8b49\u5931\u6557", + "template_error": "\u6a21\u7248\u6e32\u67d3\u932f\u8aa4\u3001\u8acb\u53c3\u95b1\u65e5\u8a8c\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u6599\u3002", "timeout": "\u8f09\u5165 URL \u903e\u6642\u6642\u9593", "unable_still_load": "\u7121\u6cd5\u7531\u8a2d\u5b9a\u975c\u614b\u5f71\u50cf URL \u8f09\u5165\u6709\u6548\u5f71\u50cf\uff08\u4f8b\u5982\uff1a\u7121\u6548\u4e3b\u6a5f\u3001URL \u6216\u8a8d\u8b49\u5931\u6557\uff09\u3002\u8acb\u53c3\u95b1\u65e5\u8a8c\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8a0a\u606f\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" diff --git a/homeassistant/components/hassio/translations/es.json b/homeassistant/components/hassio/translations/es.json index da3730fa45b..4a6fda89d84 100644 --- a/homeassistant/components/hassio/translations/es.json +++ b/homeassistant/components/hassio/translations/es.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "Versi\u00f3n del agente", "board": "Placa", "disk_total": "Disco total", "disk_used": "Disco usado", diff --git a/homeassistant/components/ialarm_xr/translations/ca.json b/homeassistant/components/ialarm_xr/translations/ca.json index 6c5ca634ccc..957004fc152 100644 --- a/homeassistant/components/ialarm_xr/translations/ca.json +++ b/homeassistant/components/ialarm_xr/translations/ca.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", + "timeout": "Temps m\u00e0xim d'espera per establir la connexi\u00f3 esgotat", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/ialarm_xr/translations/es.json b/homeassistant/components/ialarm_xr/translations/es.json new file mode 100644 index 00000000000..f5755835d3d --- /dev/null +++ b/homeassistant/components/ialarm_xr/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Fallo en la conexi\u00f3n", + "timeout": "Tiempo de espera para establecer la conexi\u00f3n", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/it.json b/homeassistant/components/ialarm_xr/translations/it.json index 52fb89a1d54..ae8437aaa86 100644 --- a/homeassistant/components/ialarm_xr/translations/it.json +++ b/homeassistant/components/ialarm_xr/translations/it.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Impossibile connettersi", + "timeout": "Tempo scaduto per stabile la connessione.", "unknown": "Errore imprevisto" }, "step": { diff --git a/homeassistant/components/ialarm_xr/translations/ja.json b/homeassistant/components/ialarm_xr/translations/ja.json index 65aac61f40f..e6f384ed615 100644 --- a/homeassistant/components/ialarm_xr/translations/ja.json +++ b/homeassistant/components/ialarm_xr/translations/ja.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "timeout": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { diff --git a/homeassistant/components/ialarm_xr/translations/zh-Hant.json b/homeassistant/components/ialarm_xr/translations/zh-Hant.json new file mode 100644 index 00000000000..b47b6268af1 --- /dev/null +++ b/homeassistant/components/ialarm_xr/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", + "timeout": "\u5efa\u7acb\u9023\u7dda\u903e\u6642", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ 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 c22bbd1b58d..f6625c0bb60 100644 --- a/homeassistant/components/isy994/translations/zh-Hant.json +++ b/homeassistant/components/isy994/translations/zh-Hant.json @@ -17,7 +17,7 @@ "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "{username} \u6191\u8b49\u4e0d\u518d\u6709\u6548\u3002", + "description": "{host} \u6191\u8b49\u4e0d\u518d\u6709\u6548\u3002", "title": "\u91cd\u65b0\u8a8d\u8b49 ISY \u5e33\u865f" }, "user": { diff --git a/homeassistant/components/plugwise/translations/ca.json b/homeassistant/components/plugwise/translations/ca.json index 85f7c357803..bd7371210ee 100644 --- a/homeassistant/components/plugwise/translations/ca.json +++ b/homeassistant/components/plugwise/translations/ca.json @@ -13,7 +13,11 @@ "step": { "user": { "data": { - "flow_type": "Tipus de connexi\u00f3" + "flow_type": "Tipus de connexi\u00f3", + "host": "Adre\u00e7a IP", + "password": "ID de Smile", + "port": "Port", + "username": "Usuari de Smile" }, "description": "Producte:", "title": "Tipus de Plugwise" diff --git a/homeassistant/components/plugwise/translations/es.json b/homeassistant/components/plugwise/translations/es.json index e1d1bf3359b..8fed1713398 100644 --- a/homeassistant/components/plugwise/translations/es.json +++ b/homeassistant/components/plugwise/translations/es.json @@ -12,7 +12,11 @@ "step": { "user": { "data": { - "flow_type": "Tipo de conexi\u00f3n" + "flow_type": "Tipo de conexi\u00f3n", + "host": "Direcci\u00f3n IP", + "password": "ID de sonrisa", + "port": "Puerto", + "username": "Nombre de usuario de la sonrisa" }, "description": "Producto:", "title": "Conectarse a Smile" diff --git a/homeassistant/components/plugwise/translations/it.json b/homeassistant/components/plugwise/translations/it.json index 9e051a29e13..24e75f3e846 100644 --- a/homeassistant/components/plugwise/translations/it.json +++ b/homeassistant/components/plugwise/translations/it.json @@ -13,10 +13,14 @@ "step": { "user": { "data": { - "flow_type": "Tipo di connessione" + "flow_type": "Tipo di connessione", + "host": "Indirizzo IP", + "password": "ID Smile", + "port": "Porta", + "username": "Nome utente Smile" }, - "description": "Prodotto:", - "title": "Tipo di Plugwise" + "description": "Inserisci", + "title": "Connettiti allo Smile" }, "user_gateway": { "data": { diff --git a/homeassistant/components/plugwise/translations/ja.json b/homeassistant/components/plugwise/translations/ja.json index f15d62d38d1..87b8d501e0d 100644 --- a/homeassistant/components/plugwise/translations/ja.json +++ b/homeassistant/components/plugwise/translations/ja.json @@ -13,7 +13,11 @@ "step": { "user": { "data": { - "flow_type": "\u63a5\u7d9a\u30bf\u30a4\u30d7" + "flow_type": "\u63a5\u7d9a\u30bf\u30a4\u30d7", + "host": "IP\u30a2\u30c9\u30ec\u30b9", + "password": "Smile\u306eID", + "port": "\u30dd\u30fc\u30c8", + "username": "Smile\u306e\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "\u30d7\u30ed\u30c0\u30af\u30c8:", "title": "Plugwise type" diff --git a/homeassistant/components/plugwise/translations/zh-Hant.json b/homeassistant/components/plugwise/translations/zh-Hant.json index 57a7add22e0..9d37a79462b 100644 --- a/homeassistant/components/plugwise/translations/zh-Hant.json +++ b/homeassistant/components/plugwise/translations/zh-Hant.json @@ -13,10 +13,14 @@ "step": { "user": { "data": { - "flow_type": "\u9023\u7dda\u985e\u5225" + "flow_type": "\u9023\u7dda\u985e\u5225", + "host": "IP \u4f4d\u5740", + "password": "Smile ID", + "port": "\u901a\u8a0a\u57e0", + "username": "Smile \u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u7522\u54c1\uff1a", - "title": "Plugwise \u985e\u5225" + "description": "\u8acb\u8f38\u5165", + "title": "\u9023\u7dda\u81f3 Smile" }, "user_gateway": { "data": { diff --git a/homeassistant/components/recorder/translations/es.json b/homeassistant/components/recorder/translations/es.json index 81bcf29d548..ef195cd52c9 100644 --- a/homeassistant/components/recorder/translations/es.json +++ b/homeassistant/components/recorder/translations/es.json @@ -2,6 +2,7 @@ "system_health": { "info": { "current_recorder_run": "Hora de inicio de la ejecuci\u00f3n actual", + "database_version": "Versi\u00f3n de la base de datos", "estimated_db_size": "Mida estimada de la base de datos (MiB)" } } diff --git a/homeassistant/components/tankerkoenig/translations/en.json b/homeassistant/components/tankerkoenig/translations/en.json index 9a69c8c812e..64c585f838b 100644 --- a/homeassistant/components/tankerkoenig/translations/en.json +++ b/homeassistant/components/tankerkoenig/translations/en.json @@ -37,6 +37,7 @@ "step": { "init": { "data": { + "scan_interval": "Update Interval", "show_on_map": "Show stations on map", "stations": "Stations" }, diff --git a/homeassistant/components/tankerkoenig/translations/es.json b/homeassistant/components/tankerkoenig/translations/es.json index ec97b5886d3..a9d235710da 100644 --- a/homeassistant/components/tankerkoenig/translations/es.json +++ b/homeassistant/components/tankerkoenig/translations/es.json @@ -25,7 +25,8 @@ "step": { "init": { "data": { - "show_on_map": "Muestra las estaciones en el mapa" + "show_on_map": "Muestra las estaciones en el mapa", + "stations": "Estaciones" } } } diff --git a/homeassistant/components/tankerkoenig/translations/pt-BR.json b/homeassistant/components/tankerkoenig/translations/pt-BR.json index 699b98812d4..cad98a4283f 100644 --- a/homeassistant/components/tankerkoenig/translations/pt-BR.json +++ b/homeassistant/components/tankerkoenig/translations/pt-BR.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "no_stations": "N\u00e3o foi poss\u00edvel encontrar nenhum posto ao alcance." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave API" + } + }, "select_station": { "data": { "stations": "Postos de combustiveis" diff --git a/homeassistant/components/tankerkoenig/translations/zh-Hant.json b/homeassistant/components/tankerkoenig/translations/zh-Hant.json index 059d07ccdc6..2a0e5b5d5c6 100644 --- a/homeassistant/components/tankerkoenig/translations/zh-Hant.json +++ b/homeassistant/components/tankerkoenig/translations/zh-Hant.json @@ -32,7 +32,8 @@ "init": { "data": { "scan_interval": "\u66f4\u65b0\u983b\u7387", - "show_on_map": "\u65bc\u5730\u5716\u986f\u793a\u52a0\u6cb9\u7ad9" + "show_on_map": "\u65bc\u5730\u5716\u986f\u793a\u52a0\u6cb9\u7ad9", + "stations": "\u52a0\u6cb9\u7ad9" }, "title": "Tankerkoenig \u9078\u9805" } diff --git a/homeassistant/components/totalconnect/translations/es.json b/homeassistant/components/totalconnect/translations/es.json index 7983aa3a11e..66f5be9ebc2 100644 --- a/homeassistant/components/totalconnect/translations/es.json +++ b/homeassistant/components/totalconnect/translations/es.json @@ -28,5 +28,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "Ignorar autom\u00e1ticamente bater\u00eda baja" + }, + "description": "Ignorar autom\u00e1ticamente las zonas en el momento en que informan de que la bater\u00eda est\u00e1 baja.", + "title": "Opciones de TotalConnect" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/it.json b/homeassistant/components/totalconnect/translations/it.json index 13a95fa9cff..dc5c1362e9c 100644 --- a/homeassistant/components/totalconnect/translations/it.json +++ b/homeassistant/components/totalconnect/translations/it.json @@ -33,7 +33,7 @@ "step": { "init": { "data": { - "auto_bypass_low_battery": "Bypass automatico della batteria scarica" + "auto_bypass_low_battery": "Esclusione automatica della batteria scarica" }, "description": "Esclusione automatica delle zone nel momento in cui segnalano una batteria scarica.", "title": "Opzioni TotalConnect" diff --git a/homeassistant/components/totalconnect/translations/zh-Hant.json b/homeassistant/components/totalconnect/translations/zh-Hant.json index 3b960a8bc43..5e471fb1746 100644 --- a/homeassistant/components/totalconnect/translations/zh-Hant.json +++ b/homeassistant/components/totalconnect/translations/zh-Hant.json @@ -28,5 +28,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "\u81ea\u52d5\u5ffd\u7565\u4f4e\u96fb\u91cf" + }, + "description": "\u7576\u56de\u5831\u4f4e\u96fb\u91cf\u6642\u81ea\u52d5\u5ffd\u7565\u5340\u57df\u3002", + "title": "TotalConnect \u9078\u9805" + } + } } } \ No newline at end of file From 44f332ed5f03fcb9bf7f8a8ed5b91715bcee75b4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 1 Jun 2022 05:35:56 +0200 Subject: [PATCH 1130/3516] Improve cast HLS detection (#72787) --- homeassistant/components/cast/helpers.py | 9 +++++---- tests/components/cast/test_helpers.py | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index dfeb9fce25b..d7419f69563 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -266,10 +266,8 @@ async def parse_m3u(hass, url): hls_content_types = ( # https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-10 "application/vnd.apple.mpegurl", - # Some sites serve these as the informal HLS m3u type. - "application/x-mpegurl", - "audio/mpegurl", - "audio/x-mpegurl", + # Additional informal types used by Mozilla gecko not included as they + # don't reliably indicate HLS streams ) m3u_data = await _fetch_playlist(hass, url, hls_content_types) m3u_lines = m3u_data.splitlines() @@ -292,6 +290,9 @@ async def parse_m3u(hass, url): elif line.startswith("#EXT-X-VERSION:"): # HLS stream, supported by cast devices raise PlaylistSupported("HLS") + elif line.startswith("#EXT-X-STREAM-INF:"): + # HLS stream, supported by cast devices + raise PlaylistSupported("HLS") elif line.startswith("#"): # Ignore other extensions continue diff --git a/tests/components/cast/test_helpers.py b/tests/components/cast/test_helpers.py index d729d36a225..8ae73449b43 100644 --- a/tests/components/cast/test_helpers.py +++ b/tests/components/cast/test_helpers.py @@ -27,6 +27,11 @@ from tests.common import load_fixture "rthkaudio2.m3u8", "application/vnd.apple.mpegurl", ), + ( + "https://rthkaudio2-lh.akamaihd.net/i/radio2_1@355865/master.m3u8", + "rthkaudio2.m3u8", + None, + ), ), ) async def test_hls_playlist_supported(hass, aioclient_mock, url, fixture, content_type): @@ -38,11 +43,12 @@ async def test_hls_playlist_supported(hass, aioclient_mock, url, fixture, conten @pytest.mark.parametrize( - "url,fixture,expected_playlist", + "url,fixture,content_type,expected_playlist", ( ( "https://sverigesradio.se/topsy/direkt/209-hi-mp3.m3u", "209-hi-mp3.m3u", + "audio/x-mpegurl", [ PlaylistItem( length=["-1"], @@ -54,6 +60,7 @@ async def test_hls_playlist_supported(hass, aioclient_mock, url, fixture, conten ( "https://sverigesradio.se/topsy/direkt/209-hi-mp3.m3u", "209-hi-mp3_bad_extinf.m3u", + "audio/x-mpegurl", [ PlaylistItem( length=None, @@ -65,6 +72,7 @@ async def test_hls_playlist_supported(hass, aioclient_mock, url, fixture, conten ( "https://sverigesradio.se/topsy/direkt/209-hi-mp3.m3u", "209-hi-mp3_no_extinf.m3u", + "audio/x-mpegurl", [ PlaylistItem( length=None, @@ -76,6 +84,7 @@ async def test_hls_playlist_supported(hass, aioclient_mock, url, fixture, conten ( "http://sverigesradio.se/topsy/direkt/164-hi-aac.pls", "164-hi-aac.pls", + "audio/x-mpegurl", [ PlaylistItem( length="-1", @@ -86,9 +95,12 @@ async def test_hls_playlist_supported(hass, aioclient_mock, url, fixture, conten ), ), ) -async def test_parse_playlist(hass, aioclient_mock, url, fixture, expected_playlist): +async def test_parse_playlist( + hass, aioclient_mock, url, fixture, content_type, expected_playlist +): """Test playlist parsing of HLS playlist.""" - aioclient_mock.get(url, text=load_fixture(fixture, "cast")) + headers = {"content-type": content_type} + aioclient_mock.get(url, text=load_fixture(fixture, "cast"), headers=headers) playlist = await parse_playlist(hass, url) assert expected_playlist == playlist From 275ea5b150bc6fb22ced52619fd50f6f8511ff9a Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 31 May 2022 22:36:45 -0500 Subject: [PATCH 1131/3516] Support add/next/play/replace enqueue options in Sonos (#72800) --- .../components/sonos/media_player.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index fd37e546105..b970b32b87a 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -576,6 +576,14 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): self.set_shuffle(True) if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: plex_plugin.add_to_queue(result.media) + elif kwargs.get(ATTR_MEDIA_ENQUEUE) in ( + MediaPlayerEnqueue.NEXT, + MediaPlayerEnqueue.PLAY, + ): + pos = (self.media.queue_position or 0) + 1 + new_pos = plex_plugin.add_to_queue(result.media, position=pos) + if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.PLAY: + soco.play_from_queue(new_pos - 1) else: soco.clear_queue() plex_plugin.add_to_queue(result.media) @@ -586,6 +594,14 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if share_link.is_share_link(media_id): if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: share_link.add_share_link_to_queue(media_id) + elif kwargs.get(ATTR_MEDIA_ENQUEUE) in ( + MediaPlayerEnqueue.NEXT, + MediaPlayerEnqueue.PLAY, + ): + pos = (self.media.queue_position or 0) + 1 + new_pos = share_link.add_share_link_to_queue(media_id, position=pos) + if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.PLAY: + soco.play_from_queue(new_pos - 1) else: soco.clear_queue() share_link.add_share_link_to_queue(media_id) @@ -596,6 +612,14 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: soco.add_uri_to_queue(media_id) + elif kwargs.get(ATTR_MEDIA_ENQUEUE) in ( + MediaPlayerEnqueue.NEXT, + MediaPlayerEnqueue.PLAY, + ): + pos = (self.media.queue_position or 0) + 1 + new_pos = soco.add_uri_to_queue(media_id, position=pos) + if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.PLAY: + soco.play_from_queue(new_pos - 1) else: soco.play_uri(media_id, force_radio=is_radio) elif media_type == MEDIA_TYPE_PLAYLIST: From f6517884b15ca09b17da63705ff0b4af4640ec20 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 1 Jun 2022 04:40:42 +0100 Subject: [PATCH 1132/3516] Fix #72749 (#72794) --- homeassistant/components/utility_meter/sensor.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 8df86b3e5a8..d2a2d2ba8ca 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -99,6 +99,13 @@ PAUSED = "paused" COLLECTING = "collecting" +def validate_is_number(value): + """Validate value is a number.""" + if is_number(value): + return value + raise vol.Invalid("Value is not a number") + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -167,7 +174,7 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_CALIBRATE_METER, - {vol.Required(ATTR_VALUE): vol.Coerce(Decimal)}, + {vol.Required(ATTR_VALUE): validate_is_number}, "async_calibrate", ) @@ -244,7 +251,7 @@ async def async_setup_platform( platform.async_register_entity_service( SERVICE_CALIBRATE_METER, - {vol.Required(ATTR_VALUE): vol.Coerce(Decimal)}, + {vol.Required(ATTR_VALUE): validate_is_number}, "async_calibrate", ) @@ -446,8 +453,8 @@ class UtilityMeterSensor(RestoreSensor): async def async_calibrate(self, value): """Calibrate the Utility Meter with a given value.""" - _LOGGER.debug("Calibrate %s = %s", self._name, value) - self._state = value + _LOGGER.debug("Calibrate %s = %s type(%s)", self._name, value, type(value)) + self._state = Decimal(str(value)) self.async_write_ha_state() async def async_added_to_hass(self): From de0c672cc24054ad03709c67172d776f6a9aff21 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 May 2022 11:35:28 -1000 Subject: [PATCH 1133/3516] Ensure the statistics_meta table is using the dynamic row format (#72784) --- homeassistant/components/recorder/migration.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index eadfc543b59..bc636d34b10 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -712,6 +712,17 @@ def _apply_update( # noqa: C901 elif new_version == 29: # Recreate statistics_meta index to block duplicated statistic_id _drop_index(session_maker, "statistics_meta", "ix_statistics_meta_statistic_id") + if engine.dialect.name == SupportedDialect.MYSQL: + # Ensure the row format is dynamic or the index + # unique will be too large + with session_scope(session=session_maker()) as session: + connection = session.connection() + # This is safe to run multiple times and fast since the table is small + connection.execute( + text( + "ALTER TABLE statistics_meta ENGINE=InnoDB, ROW_FORMAT=DYNAMIC" + ) + ) try: _create_index( session_maker, "statistics_meta", "ix_statistics_meta_statistic_id" From 860644784860a72ef0a312434e348940ee8929af Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 1 Jun 2022 05:35:56 +0200 Subject: [PATCH 1134/3516] Improve cast HLS detection (#72787) --- homeassistant/components/cast/helpers.py | 9 +++++---- tests/components/cast/test_helpers.py | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index dfeb9fce25b..d7419f69563 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -266,10 +266,8 @@ async def parse_m3u(hass, url): hls_content_types = ( # https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-10 "application/vnd.apple.mpegurl", - # Some sites serve these as the informal HLS m3u type. - "application/x-mpegurl", - "audio/mpegurl", - "audio/x-mpegurl", + # Additional informal types used by Mozilla gecko not included as they + # don't reliably indicate HLS streams ) m3u_data = await _fetch_playlist(hass, url, hls_content_types) m3u_lines = m3u_data.splitlines() @@ -292,6 +290,9 @@ async def parse_m3u(hass, url): elif line.startswith("#EXT-X-VERSION:"): # HLS stream, supported by cast devices raise PlaylistSupported("HLS") + elif line.startswith("#EXT-X-STREAM-INF:"): + # HLS stream, supported by cast devices + raise PlaylistSupported("HLS") elif line.startswith("#"): # Ignore other extensions continue diff --git a/tests/components/cast/test_helpers.py b/tests/components/cast/test_helpers.py index d729d36a225..8ae73449b43 100644 --- a/tests/components/cast/test_helpers.py +++ b/tests/components/cast/test_helpers.py @@ -27,6 +27,11 @@ from tests.common import load_fixture "rthkaudio2.m3u8", "application/vnd.apple.mpegurl", ), + ( + "https://rthkaudio2-lh.akamaihd.net/i/radio2_1@355865/master.m3u8", + "rthkaudio2.m3u8", + None, + ), ), ) async def test_hls_playlist_supported(hass, aioclient_mock, url, fixture, content_type): @@ -38,11 +43,12 @@ async def test_hls_playlist_supported(hass, aioclient_mock, url, fixture, conten @pytest.mark.parametrize( - "url,fixture,expected_playlist", + "url,fixture,content_type,expected_playlist", ( ( "https://sverigesradio.se/topsy/direkt/209-hi-mp3.m3u", "209-hi-mp3.m3u", + "audio/x-mpegurl", [ PlaylistItem( length=["-1"], @@ -54,6 +60,7 @@ async def test_hls_playlist_supported(hass, aioclient_mock, url, fixture, conten ( "https://sverigesradio.se/topsy/direkt/209-hi-mp3.m3u", "209-hi-mp3_bad_extinf.m3u", + "audio/x-mpegurl", [ PlaylistItem( length=None, @@ -65,6 +72,7 @@ async def test_hls_playlist_supported(hass, aioclient_mock, url, fixture, conten ( "https://sverigesradio.se/topsy/direkt/209-hi-mp3.m3u", "209-hi-mp3_no_extinf.m3u", + "audio/x-mpegurl", [ PlaylistItem( length=None, @@ -76,6 +84,7 @@ async def test_hls_playlist_supported(hass, aioclient_mock, url, fixture, conten ( "http://sverigesradio.se/topsy/direkt/164-hi-aac.pls", "164-hi-aac.pls", + "audio/x-mpegurl", [ PlaylistItem( length="-1", @@ -86,9 +95,12 @@ async def test_hls_playlist_supported(hass, aioclient_mock, url, fixture, conten ), ), ) -async def test_parse_playlist(hass, aioclient_mock, url, fixture, expected_playlist): +async def test_parse_playlist( + hass, aioclient_mock, url, fixture, content_type, expected_playlist +): """Test playlist parsing of HLS playlist.""" - aioclient_mock.get(url, text=load_fixture(fixture, "cast")) + headers = {"content-type": content_type} + aioclient_mock.get(url, text=load_fixture(fixture, "cast"), headers=headers) playlist = await parse_playlist(hass, url) assert expected_playlist == playlist From e60dc1b503963b9b18396d6e793865668133c53d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 31 May 2022 23:57:16 +0200 Subject: [PATCH 1135/3516] Stringify mikrotik device_tracker name (#72788) Co-authored-by: J. Nick Koston --- homeassistant/components/mikrotik/device_tracker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index dee4a5de08d..16c3ed233d8 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -102,7 +102,8 @@ class MikrotikHubTracker(ScannerEntity): @property def name(self) -> str: """Return the name of the client.""" - return self.device.name + # Stringify to ensure we return a string + return str(self.device.name) @property def hostname(self) -> str: From 0db986374634a892b1fb3e46662e81eb77ac3a99 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 May 2022 14:58:45 -0700 Subject: [PATCH 1136/3516] Sync entities when enabling/disabling Google Assistant (#72791) --- homeassistant/components/cloud/google_config.py | 9 ++++++++- homeassistant/components/google_assistant/helpers.py | 3 +++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 8f190103e87..f30be66cb42 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -195,6 +195,8 @@ class CloudGoogleConfig(AbstractConfig): ): await async_setup_component(self.hass, GOOGLE_DOMAIN, {}) + sync_entities = False + if self.should_report_state != self.is_reporting_state: if self.should_report_state: self.async_enable_report_state() @@ -203,7 +205,7 @@ class CloudGoogleConfig(AbstractConfig): # State reporting is reported as a property on entities. # So when we change it, we need to sync all entities. - await self.async_sync_entities_all() + sync_entities = True # If entity prefs are the same or we have filter in config.yaml, # don't sync. @@ -215,12 +217,17 @@ class CloudGoogleConfig(AbstractConfig): if self.enabled and not self.is_local_sdk_active: self.async_enable_local_sdk() + sync_entities = True elif not self.enabled and self.is_local_sdk_active: self.async_disable_local_sdk() + sync_entities = True self._cur_entity_prefs = prefs.google_entity_configs self._cur_default_expose = prefs.google_default_expose + if sync_entities: + await self.async_sync_entities_all() + @callback def _handle_entity_registry_updated(self, event: Event) -> None: """Handle when entity registry updated.""" diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index b425367f5c3..15a8d832403 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -213,6 +213,9 @@ class AbstractConfig(ABC): async def async_sync_entities_all(self): """Sync all entities to Google for all registered agents.""" + if not self._store.agent_user_ids: + return 204 + res = await gather( *( self.async_sync_entities(agent_user_id) From 668f56f103e87d904aeed989b1d58fca1cc1d01b Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 1 Jun 2022 04:40:42 +0100 Subject: [PATCH 1137/3516] Fix #72749 (#72794) --- homeassistant/components/utility_meter/sensor.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 8df86b3e5a8..d2a2d2ba8ca 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -99,6 +99,13 @@ PAUSED = "paused" COLLECTING = "collecting" +def validate_is_number(value): + """Validate value is a number.""" + if is_number(value): + return value + raise vol.Invalid("Value is not a number") + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -167,7 +174,7 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_CALIBRATE_METER, - {vol.Required(ATTR_VALUE): vol.Coerce(Decimal)}, + {vol.Required(ATTR_VALUE): validate_is_number}, "async_calibrate", ) @@ -244,7 +251,7 @@ async def async_setup_platform( platform.async_register_entity_service( SERVICE_CALIBRATE_METER, - {vol.Required(ATTR_VALUE): vol.Coerce(Decimal)}, + {vol.Required(ATTR_VALUE): validate_is_number}, "async_calibrate", ) @@ -446,8 +453,8 @@ class UtilityMeterSensor(RestoreSensor): async def async_calibrate(self, value): """Calibrate the Utility Meter with a given value.""" - _LOGGER.debug("Calibrate %s = %s", self._name, value) - self._state = value + _LOGGER.debug("Calibrate %s = %s type(%s)", self._name, value, type(value)) + self._state = Decimal(str(value)) self.async_write_ha_state() async def async_added_to_hass(self): From 17a3c628217c3a1b9e94181831dfd8b1a0c5160a Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 31 May 2022 22:36:45 -0500 Subject: [PATCH 1138/3516] Support add/next/play/replace enqueue options in Sonos (#72800) --- .../components/sonos/media_player.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index fd37e546105..b970b32b87a 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -576,6 +576,14 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): self.set_shuffle(True) if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: plex_plugin.add_to_queue(result.media) + elif kwargs.get(ATTR_MEDIA_ENQUEUE) in ( + MediaPlayerEnqueue.NEXT, + MediaPlayerEnqueue.PLAY, + ): + pos = (self.media.queue_position or 0) + 1 + new_pos = plex_plugin.add_to_queue(result.media, position=pos) + if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.PLAY: + soco.play_from_queue(new_pos - 1) else: soco.clear_queue() plex_plugin.add_to_queue(result.media) @@ -586,6 +594,14 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if share_link.is_share_link(media_id): if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: share_link.add_share_link_to_queue(media_id) + elif kwargs.get(ATTR_MEDIA_ENQUEUE) in ( + MediaPlayerEnqueue.NEXT, + MediaPlayerEnqueue.PLAY, + ): + pos = (self.media.queue_position or 0) + 1 + new_pos = share_link.add_share_link_to_queue(media_id, position=pos) + if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.PLAY: + soco.play_from_queue(new_pos - 1) else: soco.clear_queue() share_link.add_share_link_to_queue(media_id) @@ -596,6 +612,14 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: soco.add_uri_to_queue(media_id) + elif kwargs.get(ATTR_MEDIA_ENQUEUE) in ( + MediaPlayerEnqueue.NEXT, + MediaPlayerEnqueue.PLAY, + ): + pos = (self.media.queue_position or 0) + 1 + new_pos = soco.add_uri_to_queue(media_id, position=pos) + if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.PLAY: + soco.play_from_queue(new_pos - 1) else: soco.play_uri(media_id, force_radio=is_radio) elif media_type == MEDIA_TYPE_PLAYLIST: From 354149e43c320409e2e5909fcd39abe4dedba2a0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 May 2022 20:41:59 -0700 Subject: [PATCH 1139/3516] Bumped version to 2022.6.0b7 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1eb39c69f9d..0562776d589 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0b7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index d7bef63ded2..88cf80344f6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.0b6 +version = 2022.6.0b7 url = https://www.home-assistant.io/ [options] From 1ef59d1e73c39db329ddf9dab9105ef16829b2a2 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 31 May 2022 23:39:07 -0500 Subject: [PATCH 1140/3516] Cleanup handling of new enqueue & announce features in Sonos (#72801) --- .../components/sonos/media_player.py | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index b970b32b87a..cd129d82843 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -25,6 +25,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, + ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, @@ -527,7 +528,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): self.coordinator.soco.clear_queue() @soco_error() - def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: + def play_media( # noqa: C901 + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """ Send the play_media command to the media player. @@ -539,6 +542,12 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): If media_type is "playlist", media_id should be a Sonos Playlist name. Otherwise, media_id should be a URI. """ + # Use 'replace' as the default enqueue option + enqueue = kwargs.get(ATTR_MEDIA_ENQUEUE, MediaPlayerEnqueue.REPLACE) + if kwargs.get(ATTR_MEDIA_ANNOUNCE): + # Temporary workaround until announce support is added + enqueue = MediaPlayerEnqueue.PLAY + if spotify.is_spotify_media_type(media_type): media_type = spotify.resolve_spotify_media_type(media_type) media_id = spotify.spotify_uri_from_media_browser_url(media_id) @@ -574,17 +583,17 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): ) if result.shuffle: self.set_shuffle(True) - if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: + if enqueue == MediaPlayerEnqueue.ADD: plex_plugin.add_to_queue(result.media) - elif kwargs.get(ATTR_MEDIA_ENQUEUE) in ( + elif enqueue in ( MediaPlayerEnqueue.NEXT, MediaPlayerEnqueue.PLAY, ): pos = (self.media.queue_position or 0) + 1 new_pos = plex_plugin.add_to_queue(result.media, position=pos) - if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.PLAY: + if enqueue == MediaPlayerEnqueue.PLAY: soco.play_from_queue(new_pos - 1) - else: + elif enqueue == MediaPlayerEnqueue.REPLACE: soco.clear_queue() plex_plugin.add_to_queue(result.media) soco.play_from_queue(0) @@ -592,17 +601,17 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): share_link = self.coordinator.share_link if share_link.is_share_link(media_id): - if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: + if enqueue == MediaPlayerEnqueue.ADD: share_link.add_share_link_to_queue(media_id) - elif kwargs.get(ATTR_MEDIA_ENQUEUE) in ( + elif enqueue in ( MediaPlayerEnqueue.NEXT, MediaPlayerEnqueue.PLAY, ): pos = (self.media.queue_position or 0) + 1 new_pos = share_link.add_share_link_to_queue(media_id, position=pos) - if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.PLAY: + if enqueue == MediaPlayerEnqueue.PLAY: soco.play_from_queue(new_pos - 1) - else: + elif enqueue == MediaPlayerEnqueue.REPLACE: soco.clear_queue() share_link.add_share_link_to_queue(media_id) soco.play_from_queue(0) @@ -610,17 +619,17 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): # If media ID is a relative URL, we serve it from HA. media_id = async_process_play_media_url(self.hass, media_id) - if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: + if enqueue == MediaPlayerEnqueue.ADD: soco.add_uri_to_queue(media_id) - elif kwargs.get(ATTR_MEDIA_ENQUEUE) in ( + elif enqueue in ( MediaPlayerEnqueue.NEXT, MediaPlayerEnqueue.PLAY, ): pos = (self.media.queue_position or 0) + 1 new_pos = soco.add_uri_to_queue(media_id, position=pos) - if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.PLAY: + if enqueue == MediaPlayerEnqueue.PLAY: soco.play_from_queue(new_pos - 1) - else: + elif enqueue == MediaPlayerEnqueue.REPLACE: soco.play_uri(media_id, force_radio=is_radio) elif media_type == MEDIA_TYPE_PLAYLIST: if media_id.startswith("S:"): From 394442e8a906a327eea1d3713f6d10eda043fdc7 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 1 Jun 2022 00:42:07 -0400 Subject: [PATCH 1141/3516] Use device_id for zwave_js/replace_failed_node command (#72785) --- homeassistant/components/zwave_js/api.py | 15 +++++------- tests/components/zwave_js/test_api.py | 31 ++++++++++-------------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index f9f1ad40f9a..e6ce08a78a5 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -1083,8 +1083,7 @@ async def websocket_remove_node( @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/replace_failed_node", - vol.Required(ENTRY_ID): str, - vol.Required(NODE_ID): int, + vol.Required(DEVICE_ID): str, vol.Optional(INCLUSION_STRATEGY, default=InclusionStrategy.DEFAULT): vol.All( vol.Coerce(int), vol.In( @@ -1107,18 +1106,16 @@ async def websocket_remove_node( ) @websocket_api.async_response @async_handle_failed_command -@async_get_entry +@async_get_node async def websocket_replace_failed_node( hass: HomeAssistant, connection: ActiveConnection, msg: dict, - entry: ConfigEntry, - client: Client, - driver: Driver, + node: Node, ) -> None: """Replace a failed node with a new node.""" - controller = driver.controller - node_id = msg[NODE_ID] + assert node.client.driver + controller = node.client.driver.controller inclusion_strategy = InclusionStrategy(msg[INCLUSION_STRATEGY]) force_security = msg.get(FORCE_SECURITY) provisioning = ( @@ -1232,7 +1229,7 @@ async def websocket_replace_failed_node( try: result = await controller.async_replace_failed_node( - controller.nodes[node_id], + node, INCLUSION_STRATEGY_NOT_SMART_START[inclusion_strategy.value], force_security=force_security, provisioning=provisioning, diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 7f64ed6d87d..da8cfad9624 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -1511,7 +1511,7 @@ async def test_replace_failed_node( dev_reg = dr.async_get(hass) # Create device registry entry for mock node - dev_reg.async_get_or_create( + device = dev_reg.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, "3245146787-67")}, name="Node 67", @@ -1526,8 +1526,7 @@ async def test_replace_failed_node( { ID: 1, TYPE: "zwave_js/replace_failed_node", - ENTRY_ID: entry.entry_id, - NODE_ID: 67, + DEVICE_ID: device.id, INCLUSION_STRATEGY: InclusionStrategy.DEFAULT.value, } ) @@ -1607,10 +1606,12 @@ async def test_replace_failed_node( assert msg["event"]["event"] == "node removed" # Verify device was removed from device registry - device = dev_reg.async_get_device( - identifiers={(DOMAIN, "3245146787-67")}, + assert ( + dev_reg.async_get_device( + identifiers={(DOMAIN, "3245146787-67")}, + ) + is None ) - assert device is None client.driver.receive_event(nortek_thermostat_added_event) msg = await ws_client.receive_json() @@ -1686,8 +1687,7 @@ async def test_replace_failed_node( { ID: 2, TYPE: "zwave_js/replace_failed_node", - ENTRY_ID: entry.entry_id, - NODE_ID: 67, + DEVICE_ID: device.id, INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value, PLANNED_PROVISIONING_ENTRY: { DSK: "test", @@ -1719,8 +1719,7 @@ async def test_replace_failed_node( { ID: 3, TYPE: "zwave_js/replace_failed_node", - ENTRY_ID: entry.entry_id, - NODE_ID: 67, + DEVICE_ID: device.id, INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value, QR_PROVISIONING_INFORMATION: { VERSION: 0, @@ -1772,8 +1771,7 @@ async def test_replace_failed_node( { ID: 4, TYPE: "zwave_js/replace_failed_node", - ENTRY_ID: entry.entry_id, - NODE_ID: 67, + DEVICE_ID: device.id, INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value, QR_CODE_STRING: "90testtesttesttesttesttesttesttesttesttesttesttesttest", } @@ -1800,8 +1798,7 @@ async def test_replace_failed_node( { ID: 6, TYPE: "zwave_js/replace_failed_node", - ENTRY_ID: entry.entry_id, - NODE_ID: 67, + DEVICE_ID: device.id, INCLUSION_STRATEGY: InclusionStrategy.DEFAULT.value, QR_CODE_STRING: "90testtesttesttesttesttesttesttesttesttesttesttesttest", } @@ -1821,8 +1818,7 @@ async def test_replace_failed_node( { ID: 7, TYPE: "zwave_js/replace_failed_node", - ENTRY_ID: entry.entry_id, - NODE_ID: 67, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() @@ -1839,8 +1835,7 @@ async def test_replace_failed_node( { ID: 8, TYPE: "zwave_js/replace_failed_node", - ENTRY_ID: entry.entry_id, - NODE_ID: 67, + DEVICE_ID: device.id, } ) msg = await ws_client.receive_json() From a6db25219d5c94c1e19b5170145e47daf8c02b91 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Jun 2022 06:58:29 +0200 Subject: [PATCH 1142/3516] Use Mapping for async_step_reauth in motioneye (#72769) --- homeassistant/components/motioneye/config_flow.py | 5 +++-- tests/components/motioneye/test_config_flow.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index 0361f4562c4..6b88f47a588 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -1,6 +1,7 @@ """Config flow for motionEye integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any, cast from motioneye_client.client import ( @@ -158,10 +159,10 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth( self, - config_data: dict[str, Any] | None = None, + config_data: Mapping[str, Any], ) -> FlowResult: """Handle a reauthentication flow.""" - return await self.async_step_user(config_data) + return await self.async_step_user() async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: """Handle Supervisor discovery.""" diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index 9ef0f78874d..269a2b8a4c4 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -259,6 +259,7 @@ async def test_reauth(hass: HomeAssistant) -> None: "source": config_entries.SOURCE_REAUTH, "entry_id": config_entry.entry_id, }, + data=config_entry.data, ) assert result["type"] == "form" assert not result["errors"] From 133cb7ccef96592479b3a867b09e69095e180998 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 1 Jun 2022 02:04:35 -0400 Subject: [PATCH 1143/3516] Add package constraint for pydantic (#72799) Co-authored-by: J. Nick Koston --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a3d8a00bcfb..c1bbe51755f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -106,3 +106,7 @@ authlib<1.0 # Pin backoff for compatibility until most libraries have been updated # https://github.com/home-assistant/core/pull/70817 backoff<2.0 + +# Breaking change in version +# https://github.com/samuelcolvin/pydantic/issues/4092 +pydantic!=1.9.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index adf57f14f97..88524ab63de 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -126,6 +126,10 @@ authlib<1.0 # Pin backoff for compatibility until most libraries have been updated # https://github.com/home-assistant/core/pull/70817 backoff<2.0 + +# Breaking change in version +# https://github.com/samuelcolvin/pydantic/issues/4092 +pydantic!=1.9.1 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From 577be70da9554993162de9c726b1404c93307b2a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Jun 2022 08:38:48 +0200 Subject: [PATCH 1144/3516] Add new method to pylint type-hint plugin (#72757) Enforce type hints on remove_config_entry_device --- pylint/plugins/hass_enforce_type_hints.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 24b32f2e238..f3dd7a106c6 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -88,6 +88,15 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { }, return_type="bool", ), + TypeHintMatch( + function_name="async_remove_config_entry_device", + arg_types={ + 0: "HomeAssistant", + 1: "ConfigEntry", + 2: "DeviceEntry", + }, + return_type="bool", + ), ], "__any_platform__": [ TypeHintMatch( From 4902af2f4e7761ddff6706b475ac31c1fd8b5366 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 1 Jun 2022 09:22:47 +0200 Subject: [PATCH 1145/3516] Fix conftest for pylint plugin (#72777) --- tests/pylint/conftest.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/pylint/conftest.py b/tests/pylint/conftest.py index 887f50fb628..edbcec27375 100644 --- a/tests/pylint/conftest.py +++ b/tests/pylint/conftest.py @@ -1,17 +1,21 @@ """Configuration for pylint tests.""" from importlib.machinery import SourceFileLoader +from pathlib import Path from types import ModuleType from pylint.checkers import BaseChecker from pylint.testutils.unittest_linter import UnittestLinter import pytest +BASE_PATH = Path(__file__).parents[2] -@pytest.fixture(name="hass_enforce_type_hints") + +@pytest.fixture(name="hass_enforce_type_hints", scope="session") def hass_enforce_type_hints_fixture() -> ModuleType: """Fixture to provide a requests mocker.""" loader = SourceFileLoader( - "hass_enforce_type_hints", "pylint/plugins/hass_enforce_type_hints.py" + "hass_enforce_type_hints", + str(BASE_PATH.joinpath("pylint/plugins/hass_enforce_type_hints.py")), ) return loader.load_module(None) From 9ac0c5907f8e7a4a0906559b482ac450f42892c0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 1 Jun 2022 09:39:56 +0200 Subject: [PATCH 1146/3516] Add test for mikrotik device tracker with numerical device name (#72808) Add mikrotik test for numerical device name --- tests/components/mikrotik/__init__.py | 44 +++++++------------ .../mikrotik/test_device_tracker.py | 29 +++++++++++- 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/tests/components/mikrotik/__init__.py b/tests/components/mikrotik/__init__.py index 885fc9c8d83..6f67eea1a0a 100644 --- a/tests/components/mikrotik/__init__.py +++ b/tests/components/mikrotik/__init__.py @@ -45,6 +45,14 @@ DEVICE_2_DHCP = { "host-name": "Device_2", "comment": "PC", } +DEVICE_3_DHCP_NUMERIC_NAME = { + ".id": "*1C", + "address": "0.0.0.3", + "mac-address": "00:00:00:00:00:03", + "active-address": "0.0.0.3", + "host-name": 123, + "comment": "Mobile", +} DEVICE_1_WIRELESS = { ".id": "*264", "interface": "wlan1", @@ -81,38 +89,16 @@ DEVICE_1_WIRELESS = { } DEVICE_2_WIRELESS = { + **DEVICE_1_WIRELESS, ".id": "*265", - "interface": "wlan1", "mac-address": "00:00:00:00:00:02", - "ap": False, - "wds": False, - "bridge": False, - "rx-rate": "72.2Mbps-20MHz/1S/SGI", - "tx-rate": "72.2Mbps-20MHz/1S/SGI", - "packets": "59542,17464", - "bytes": "17536671,2966351", - "frames": "59542,17472", - "frame-bytes": "17655785,2862445", - "hw-frames": "78935,38395", - "hw-frame-bytes": "25636019,4063445", - "tx-frames-timed-out": 0, - "uptime": "5h49m36s", - "last-activity": "170ms", - "signal-strength": "-62@1Mbps", - "signal-to-noise": 52, - "signal-strength-ch0": -63, - "signal-strength-ch1": -69, - "strength-at-rates": "-62@1Mbps 16s330ms,-64@6Mbps 13s560ms,-65@HT20-3 52m6s30ms,-66@HT20-4 52m4s350ms,-66@HT20-5 51m58s580ms,-65@HT20-6 51m24s780ms,-65@HT20-7 5s680ms", - "tx-ccq": 93, - "p-throughput": 54928, "last-ip": "0.0.0.2", - "802.1x-port-enabled": True, - "authentication-type": "wpa2-psk", - "encryption": "aes-ccm", - "group-encryption": "aes-ccm", - "management-protection": False, - "wmm-enabled": True, - "tx-rate-set": "OFDM:6-54 BW:1x SGI:1x HT:0-7", +} +DEVICE_3_WIRELESS = { + **DEVICE_1_WIRELESS, + ".id": "*266", + "mac-address": "00:00:00:00:00:03", + "last-ip": "0.0.0.3", } DHCP_DATA = [DEVICE_1_DHCP, DEVICE_2_DHCP] diff --git a/tests/components/mikrotik/test_device_tracker.py b/tests/components/mikrotik/test_device_tracker.py index 715826e69d6..fd2f71b2589 100644 --- a/tests/components/mikrotik/test_device_tracker.py +++ b/tests/components/mikrotik/test_device_tracker.py @@ -9,7 +9,15 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from . import DEVICE_2_WIRELESS, DHCP_DATA, MOCK_DATA, MOCK_OPTIONS, WIRELESS_DATA +from . import ( + DEVICE_2_WIRELESS, + DEVICE_3_DHCP_NUMERIC_NAME, + DEVICE_3_WIRELESS, + DHCP_DATA, + MOCK_DATA, + MOCK_OPTIONS, + WIRELESS_DATA, +) from .test_hub import setup_mikrotik_entry from tests.common import MockConfigEntry, patch @@ -27,6 +35,7 @@ def mock_device_registry_devices(hass): ( "00:00:00:00:00:01", "00:00:00:00:00:02", + "00:00:00:00:00:03", ) ): dev_reg.async_get_or_create( @@ -115,6 +124,24 @@ async def test_device_trackers(hass, mock_device_registry_devices): assert device_2.state == "not_home" +async def test_device_trackers_numerical_name(hass, mock_device_registry_devices): + """Test device_trackers created by mikrotik with numerical device name.""" + + await setup_mikrotik_entry( + hass, dhcp_data=[DEVICE_3_DHCP_NUMERIC_NAME], wireless_data=[DEVICE_3_WIRELESS] + ) + + device_3 = hass.states.get("device_tracker.123") + assert device_3 is not None + assert device_3.state == "home" + assert device_3.attributes["friendly_name"] == "123" + assert device_3.attributes["ip"] == "0.0.0.3" + assert "ip_address" not in device_3.attributes + assert device_3.attributes["mac"] == "00:00:00:00:00:03" + assert device_3.attributes["host_name"] == 123 + assert "mac_address" not in device_3.attributes + + async def test_restoring_devices(hass): """Test restoring existing device_tracker entities if not detected on startup.""" config_entry = MockConfigEntry( From 1aeba8a9bdbf44c12bf10af564d1901c3137151c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Jun 2022 09:44:22 +0200 Subject: [PATCH 1147/3516] Use Mapping for async_step_reauth in discord (#72812) * Fix tests * Cleanup code accordingly --- homeassistant/components/discord/config_flow.py | 10 ++++------ tests/components/discord/test_config_flow.py | 7 +------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/discord/config_flow.py b/homeassistant/components/discord/config_flow.py index bce27feced3..93027132850 100644 --- a/homeassistant/components/discord/config_flow.py +++ b/homeassistant/components/discord/config_flow.py @@ -1,7 +1,9 @@ """Config flow for Discord integration.""" from __future__ import annotations +from collections.abc import Mapping import logging +from typing import Any from aiohttp.client_exceptions import ClientConnectorError import nextcord @@ -21,13 +23,9 @@ CONFIG_SCHEMA = vol.Schema({vol.Required(CONF_API_TOKEN): str}) class DiscordFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Discord.""" - async def async_step_reauth(self, user_input: dict | None = None) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle a reauthorization flow request.""" - if user_input is not None: - return await self.async_step_reauth_confirm() - - self._set_confirm_only() - return self.async_show_form(step_id="reauth") + return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, str] | None = None diff --git a/tests/components/discord/test_config_flow.py b/tests/components/discord/test_config_flow.py index 59030187866..9d4966929be 100644 --- a/tests/components/discord/test_config_flow.py +++ b/tests/components/discord/test_config_flow.py @@ -128,14 +128,9 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: "entry_id": entry.entry_id, "unique_id": entry.unique_id, }, + data=entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth" - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={}, - ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "reauth_confirm" From e874a043197f7cb539a4d8b7f1d0d36ab5259d9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 May 2022 22:56:05 -1000 Subject: [PATCH 1148/3516] Bump sqlalchemy to 1.4.37 (#72809) Fixes a bug where reconnects might fail with MySQL 8.0.24+ Changelog: https://docs.sqlalchemy.org/en/14/changelog/changelog_14.html#change-1.4.37 --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 0fb44f99ae2..38897c42e1a 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.36", "fnvhash==0.1.0", "lru-dict==1.1.7"], + "requirements": ["sqlalchemy==1.4.37", "fnvhash==0.1.0", "lru-dict==1.1.7"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index c779e4567cd..4562b945008 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,7 +2,7 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.4.36"], + "requirements": ["sqlalchemy==1.4.37"], "codeowners": ["@dgomes", "@gjohansson-ST"], "config_flow": true, "iot_class": "local_polling" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c1bbe51755f..ad2539233f4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ pyudev==0.22.0 pyyaml==6.0 requests==2.27.1 scapy==2.4.5 -sqlalchemy==1.4.36 +sqlalchemy==1.4.37 typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index b6d24b95d5c..f3c55964e5c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2223,7 +2223,7 @@ spotipy==2.19.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.36 +sqlalchemy==1.4.37 # homeassistant.components.srp_energy srpenergy==1.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 230ceae7fa0..dca09840bf3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1465,7 +1465,7 @@ spotipy==2.19.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.36 +sqlalchemy==1.4.37 # homeassistant.components.srp_energy srpenergy==1.3.6 From ee861c8ea5e7c4db1289c1c95f7da47009f7afd2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jun 2022 11:35:36 +0200 Subject: [PATCH 1149/3516] Bump actions/cache from 3.0.2 to 3.0.3 (#72817) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6264c1379fd..b8d81856f3c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -172,7 +172,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.3 with: path: venv key: >- @@ -189,7 +189,7 @@ jobs: # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.3 with: path: ${{ env.PIP_CACHE }} key: >- @@ -212,7 +212,7 @@ jobs: hashFiles('.pre-commit-config.yaml') }}" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -241,7 +241,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.3 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -253,7 +253,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -291,7 +291,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.3 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -303,7 +303,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -342,7 +342,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.3 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -354,7 +354,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -384,7 +384,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.3 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -396,7 +396,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -502,7 +502,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.3 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -531,7 +531,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.3 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -573,7 +573,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.3 with: path: venv key: >- @@ -590,7 +590,7 @@ jobs: # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}- - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.3 with: path: ${{ env.PIP_CACHE }} key: >- @@ -629,7 +629,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.3 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -671,7 +671,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.3 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -715,7 +715,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.3 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -758,7 +758,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.2 + uses: actions/cache@v3.0.3 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ From d8b037694284cf388b9751b4d54eecfd5452280f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 May 2022 23:56:06 -1000 Subject: [PATCH 1150/3516] Fix purge of legacy database events that are not state changed (#72815) --- homeassistant/components/recorder/queries.py | 2 +- tests/components/recorder/test_purge.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/queries.py b/homeassistant/components/recorder/queries.py index 5532c5c0703..e27d3d692cc 100644 --- a/homeassistant/components/recorder/queries.py +++ b/homeassistant/components/recorder/queries.py @@ -631,7 +631,7 @@ def find_legacy_event_state_and_attributes_and_data_ids_to_purge( lambda: select( Events.event_id, Events.data_id, States.state_id, States.attributes_id ) - .join(States, Events.event_id == States.event_id) + .outerjoin(States, Events.event_id == States.event_id) .filter(Events.time_fired < purge_before) .limit(MAX_ROWS_TO_PURGE) ) diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index c8ba5e9d076..f4e998c5388 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -26,7 +26,7 @@ from homeassistant.components.recorder.services import ( ) from homeassistant.components.recorder.tasks import PurgeTask from homeassistant.components.recorder.util import session_scope -from homeassistant.const import EVENT_STATE_CHANGED, STATE_ON +from homeassistant.const import EVENT_STATE_CHANGED, EVENT_THEMES_UPDATED, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util @@ -925,6 +925,15 @@ async def test_purge_without_state_attributes_filtered_states_to_empty( time_fired=timestamp, ) ) + session.add( + Events( + event_id=event_id + 1, + event_type=EVENT_THEMES_UPDATED, + event_data="{}", + origin="LOCAL", + time_fired=timestamp, + ) + ) service_data = {"keep_days": 10} _add_db_entries(hass) From d57a650290181c4f35da96b0bd58df01c1ae8752 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 1 Jun 2022 03:12:54 -0700 Subject: [PATCH 1151/3516] Don't trigger entity sync when Google Assistant gets disabled (#72805) --- homeassistant/components/cloud/google_config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index f30be66cb42..a0a68aaf84a 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -220,7 +220,6 @@ class CloudGoogleConfig(AbstractConfig): sync_entities = True elif not self.enabled and self.is_local_sdk_active: self.async_disable_local_sdk() - sync_entities = True self._cur_entity_prefs = prefs.google_entity_configs self._cur_default_expose = prefs.google_default_expose From 023990577ceccffeb0596daab4702d1e4f265aec Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 1 Jun 2022 12:33:13 +0200 Subject: [PATCH 1152/3516] Add Motionblinds WoodShutter support (#72814) --- .../components/motion_blinds/cover.py | 58 +++++++++++++++++++ .../components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index a9f6df82ae0..7bac3a5fb20 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -9,6 +9,7 @@ from homeassistant.components.cover import ( ATTR_TILT_POSITION, CoverDeviceClass, CoverEntity, + CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -64,6 +65,10 @@ TILT_DEVICE_MAP = { BlindType.VerticalBlindRight: CoverDeviceClass.BLIND, } +TILT_ONLY_DEVICE_MAP = { + BlindType.WoodShutter: CoverDeviceClass.BLIND, +} + TDBU_DEVICE_MAP = { BlindType.TopDownBottomUp: CoverDeviceClass.SHADE, } @@ -108,6 +113,16 @@ async def async_setup_entry( ) ) + elif blind.type in TILT_ONLY_DEVICE_MAP: + entities.append( + MotionTiltOnlyDevice( + coordinator, + blind, + TILT_ONLY_DEVICE_MAP[blind.type], + sw_version, + ) + ) + elif blind.type in TDBU_DEVICE_MAP: entities.append( MotionTDBUDevice( @@ -356,6 +371,49 @@ class MotionTiltDevice(MotionPositionDevice): await self.hass.async_add_executor_job(self._blind.Stop) +class MotionTiltOnlyDevice(MotionTiltDevice): + """Representation of a Motion Blind Device.""" + + _restore_tilt = False + + @property + def supported_features(self): + """Flag supported features.""" + supported_features = ( + CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT + ) + + if self.current_cover_tilt_position is not None: + supported_features |= CoverEntityFeature.SET_TILT_POSITION + + return supported_features + + @property + def current_cover_position(self): + """Return current position of cover.""" + return None + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + if self._blind.angle is None: + return None + return self._blind.angle == 0 + + async def async_set_absolute_position(self, **kwargs): + """Move the cover to a specific absolute position (see TDBU).""" + angle = kwargs.get(ATTR_TILT_POSITION) + if angle is not None: + angle = angle * 180 / 100 + async with self._api_lock: + await self.hass.async_add_executor_job( + self._blind.Set_angle, + angle, + ) + + class MotionTDBUDevice(MotionPositionDevice): """Representation of a Motion Top Down Bottom Up blind Device.""" diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 1e8ad0eb0a1..bc09d3e9e38 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,7 +3,7 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.6.7"], + "requirements": ["motionblinds==0.6.8"], "dependencies": ["network"], "dhcp": [ { "registered_devices": true }, diff --git a/requirements_all.txt b/requirements_all.txt index f3c55964e5c..603ad612ae3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1038,7 +1038,7 @@ mitemp_bt==0.0.5 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.7 +motionblinds==0.6.8 # homeassistant.components.motioneye motioneye-client==0.3.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dca09840bf3..a1fa8d0fdc3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -715,7 +715,7 @@ minio==5.0.10 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.7 +motionblinds==0.6.8 # homeassistant.components.motioneye motioneye-client==0.3.12 From 5d2326386d15ae422d3ebc2d03f714929c842d4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Jun 2022 00:33:46 -1000 Subject: [PATCH 1153/3516] Fix logbook spinner never disappearing when all entities are filtered (#72816) --- .../components/logbook/websocket_api.py | 4 +- .../components/logbook/test_websocket_api.py | 49 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index 82b1db1081c..1af44440803 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -356,7 +356,7 @@ async def ws_event_stream( ) await _async_wait_for_recorder_sync(hass) - if not subscriptions: + if msg_id not in connection.subscriptions: # Unsubscribe happened while waiting for recorder return @@ -388,6 +388,8 @@ async def ws_event_stream( if not subscriptions: # Unsubscribe happened while waiting for formatted events + # or there are no supported entities (all UOM or state class) + # or devices return live_stream.task = asyncio.create_task( diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 291c487b35b..2dd08ec44ce 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -2089,3 +2089,52 @@ async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplo assert msg["success"] assert "Recorder is behind" in caplog.text + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_client): + """Test subscribe/unsubscribe logbook stream with entities that are always filtered.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + await async_wait_recording_done(hass) + + init_count = sum(hass.bus.async_listeners().values()) + hass.states.async_set("sensor.uom", "1", {ATTR_UNIT_OF_MEASUREMENT: "any"}) + hass.states.async_set("sensor.uom", "2", {ATTR_UNIT_OF_MEASUREMENT: "any"}) + hass.states.async_set("sensor.uom", "3", {ATTR_UNIT_OF_MEASUREMENT: "any"}) + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "entity_ids": ["sensor.uom"], + } + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + hass.states.async_set("sensor.uom", "1", {ATTR_UNIT_OF_MEASUREMENT: "any"}) + hass.states.async_set("sensor.uom", "2", {ATTR_UNIT_OF_MEASUREMENT: "any"}) + hass.states.async_set("sensor.uom", "3", {ATTR_UNIT_OF_MEASUREMENT: "any"}) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [] + + await websocket_client.close() + await hass.async_block_till_done() + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count From 1274448de13629f33f1f87d7ecd953d6835c623c Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 1 Jun 2022 02:04:35 -0400 Subject: [PATCH 1154/3516] Add package constraint for pydantic (#72799) Co-authored-by: J. Nick Koston --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a3d8a00bcfb..c1bbe51755f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -106,3 +106,7 @@ authlib<1.0 # Pin backoff for compatibility until most libraries have been updated # https://github.com/home-assistant/core/pull/70817 backoff<2.0 + +# Breaking change in version +# https://github.com/samuelcolvin/pydantic/issues/4092 +pydantic!=1.9.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index f049080b7fa..0fd31430aa4 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -122,6 +122,10 @@ authlib<1.0 # Pin backoff for compatibility until most libraries have been updated # https://github.com/home-assistant/core/pull/70817 backoff<2.0 + +# Breaking change in version +# https://github.com/samuelcolvin/pydantic/issues/4092 +pydantic!=1.9.1 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From 384cb44d15e480d9b95382221fa39afba7324da6 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 31 May 2022 23:39:07 -0500 Subject: [PATCH 1155/3516] Cleanup handling of new enqueue & announce features in Sonos (#72801) --- .../components/sonos/media_player.py | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index b970b32b87a..cd129d82843 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -25,6 +25,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, + ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, @@ -527,7 +528,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): self.coordinator.soco.clear_queue() @soco_error() - def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: + def play_media( # noqa: C901 + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """ Send the play_media command to the media player. @@ -539,6 +542,12 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): If media_type is "playlist", media_id should be a Sonos Playlist name. Otherwise, media_id should be a URI. """ + # Use 'replace' as the default enqueue option + enqueue = kwargs.get(ATTR_MEDIA_ENQUEUE, MediaPlayerEnqueue.REPLACE) + if kwargs.get(ATTR_MEDIA_ANNOUNCE): + # Temporary workaround until announce support is added + enqueue = MediaPlayerEnqueue.PLAY + if spotify.is_spotify_media_type(media_type): media_type = spotify.resolve_spotify_media_type(media_type) media_id = spotify.spotify_uri_from_media_browser_url(media_id) @@ -574,17 +583,17 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): ) if result.shuffle: self.set_shuffle(True) - if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: + if enqueue == MediaPlayerEnqueue.ADD: plex_plugin.add_to_queue(result.media) - elif kwargs.get(ATTR_MEDIA_ENQUEUE) in ( + elif enqueue in ( MediaPlayerEnqueue.NEXT, MediaPlayerEnqueue.PLAY, ): pos = (self.media.queue_position or 0) + 1 new_pos = plex_plugin.add_to_queue(result.media, position=pos) - if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.PLAY: + if enqueue == MediaPlayerEnqueue.PLAY: soco.play_from_queue(new_pos - 1) - else: + elif enqueue == MediaPlayerEnqueue.REPLACE: soco.clear_queue() plex_plugin.add_to_queue(result.media) soco.play_from_queue(0) @@ -592,17 +601,17 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): share_link = self.coordinator.share_link if share_link.is_share_link(media_id): - if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: + if enqueue == MediaPlayerEnqueue.ADD: share_link.add_share_link_to_queue(media_id) - elif kwargs.get(ATTR_MEDIA_ENQUEUE) in ( + elif enqueue in ( MediaPlayerEnqueue.NEXT, MediaPlayerEnqueue.PLAY, ): pos = (self.media.queue_position or 0) + 1 new_pos = share_link.add_share_link_to_queue(media_id, position=pos) - if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.PLAY: + if enqueue == MediaPlayerEnqueue.PLAY: soco.play_from_queue(new_pos - 1) - else: + elif enqueue == MediaPlayerEnqueue.REPLACE: soco.clear_queue() share_link.add_share_link_to_queue(media_id) soco.play_from_queue(0) @@ -610,17 +619,17 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): # If media ID is a relative URL, we serve it from HA. media_id = async_process_play_media_url(self.hass, media_id) - if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.ADD: + if enqueue == MediaPlayerEnqueue.ADD: soco.add_uri_to_queue(media_id) - elif kwargs.get(ATTR_MEDIA_ENQUEUE) in ( + elif enqueue in ( MediaPlayerEnqueue.NEXT, MediaPlayerEnqueue.PLAY, ): pos = (self.media.queue_position or 0) + 1 new_pos = soco.add_uri_to_queue(media_id, position=pos) - if kwargs.get(ATTR_MEDIA_ENQUEUE) == MediaPlayerEnqueue.PLAY: + if enqueue == MediaPlayerEnqueue.PLAY: soco.play_from_queue(new_pos - 1) - else: + elif enqueue == MediaPlayerEnqueue.REPLACE: soco.play_uri(media_id, force_radio=is_radio) elif media_type == MEDIA_TYPE_PLAYLIST: if media_id.startswith("S:"): From 9bd2e3ad7c4d614d419547d9cd765527b9fe8315 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 1 Jun 2022 03:12:54 -0700 Subject: [PATCH 1156/3516] Don't trigger entity sync when Google Assistant gets disabled (#72805) --- homeassistant/components/cloud/google_config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index f30be66cb42..a0a68aaf84a 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -220,7 +220,6 @@ class CloudGoogleConfig(AbstractConfig): sync_entities = True elif not self.enabled and self.is_local_sdk_active: self.async_disable_local_sdk() - sync_entities = True self._cur_entity_prefs = prefs.google_entity_configs self._cur_default_expose = prefs.google_default_expose From 9e723f9b6d3ba79687e66df312bf4ffce6c4d782 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 May 2022 22:56:05 -1000 Subject: [PATCH 1157/3516] Bump sqlalchemy to 1.4.37 (#72809) Fixes a bug where reconnects might fail with MySQL 8.0.24+ Changelog: https://docs.sqlalchemy.org/en/14/changelog/changelog_14.html#change-1.4.37 --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 0fb44f99ae2..38897c42e1a 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.36", "fnvhash==0.1.0", "lru-dict==1.1.7"], + "requirements": ["sqlalchemy==1.4.37", "fnvhash==0.1.0", "lru-dict==1.1.7"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index c779e4567cd..4562b945008 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,7 +2,7 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.4.36"], + "requirements": ["sqlalchemy==1.4.37"], "codeowners": ["@dgomes", "@gjohansson-ST"], "config_flow": true, "iot_class": "local_polling" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c1bbe51755f..ad2539233f4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -29,7 +29,7 @@ pyudev==0.22.0 pyyaml==6.0 requests==2.27.1 scapy==2.4.5 -sqlalchemy==1.4.36 +sqlalchemy==1.4.37 typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index 23b333113b0..d1bffba6971 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2223,7 +2223,7 @@ spotipy==2.19.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.36 +sqlalchemy==1.4.37 # homeassistant.components.srp_energy srpenergy==1.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3eabedeb942..2c819e1ef3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1465,7 +1465,7 @@ spotipy==2.19.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.36 +sqlalchemy==1.4.37 # homeassistant.components.srp_energy srpenergy==1.3.6 From 1139136365b8624e4f4f242140952e27153ef25b Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 1 Jun 2022 12:33:13 +0200 Subject: [PATCH 1158/3516] Add Motionblinds WoodShutter support (#72814) --- .../components/motion_blinds/cover.py | 58 +++++++++++++++++++ .../components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index a9f6df82ae0..7bac3a5fb20 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -9,6 +9,7 @@ from homeassistant.components.cover import ( ATTR_TILT_POSITION, CoverDeviceClass, CoverEntity, + CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -64,6 +65,10 @@ TILT_DEVICE_MAP = { BlindType.VerticalBlindRight: CoverDeviceClass.BLIND, } +TILT_ONLY_DEVICE_MAP = { + BlindType.WoodShutter: CoverDeviceClass.BLIND, +} + TDBU_DEVICE_MAP = { BlindType.TopDownBottomUp: CoverDeviceClass.SHADE, } @@ -108,6 +113,16 @@ async def async_setup_entry( ) ) + elif blind.type in TILT_ONLY_DEVICE_MAP: + entities.append( + MotionTiltOnlyDevice( + coordinator, + blind, + TILT_ONLY_DEVICE_MAP[blind.type], + sw_version, + ) + ) + elif blind.type in TDBU_DEVICE_MAP: entities.append( MotionTDBUDevice( @@ -356,6 +371,49 @@ class MotionTiltDevice(MotionPositionDevice): await self.hass.async_add_executor_job(self._blind.Stop) +class MotionTiltOnlyDevice(MotionTiltDevice): + """Representation of a Motion Blind Device.""" + + _restore_tilt = False + + @property + def supported_features(self): + """Flag supported features.""" + supported_features = ( + CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT + ) + + if self.current_cover_tilt_position is not None: + supported_features |= CoverEntityFeature.SET_TILT_POSITION + + return supported_features + + @property + def current_cover_position(self): + """Return current position of cover.""" + return None + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + if self._blind.angle is None: + return None + return self._blind.angle == 0 + + async def async_set_absolute_position(self, **kwargs): + """Move the cover to a specific absolute position (see TDBU).""" + angle = kwargs.get(ATTR_TILT_POSITION) + if angle is not None: + angle = angle * 180 / 100 + async with self._api_lock: + await self.hass.async_add_executor_job( + self._blind.Set_angle, + angle, + ) + + class MotionTDBUDevice(MotionPositionDevice): """Representation of a Motion Top Down Bottom Up blind Device.""" diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 1e8ad0eb0a1..bc09d3e9e38 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,7 +3,7 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.6.7"], + "requirements": ["motionblinds==0.6.8"], "dependencies": ["network"], "dhcp": [ { "registered_devices": true }, diff --git a/requirements_all.txt b/requirements_all.txt index d1bffba6971..6341528b09a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1038,7 +1038,7 @@ mitemp_bt==0.0.5 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.7 +motionblinds==0.6.8 # homeassistant.components.motioneye motioneye-client==0.3.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2c819e1ef3c..b5767c1e468 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -715,7 +715,7 @@ minio==5.0.10 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.7 +motionblinds==0.6.8 # homeassistant.components.motioneye motioneye-client==0.3.12 From 2f3359f376bc9e69e8118ef7e7f63cef8b81964d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 May 2022 23:56:06 -1000 Subject: [PATCH 1159/3516] Fix purge of legacy database events that are not state changed (#72815) --- homeassistant/components/recorder/queries.py | 2 +- tests/components/recorder/test_purge.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/queries.py b/homeassistant/components/recorder/queries.py index 5532c5c0703..e27d3d692cc 100644 --- a/homeassistant/components/recorder/queries.py +++ b/homeassistant/components/recorder/queries.py @@ -631,7 +631,7 @@ def find_legacy_event_state_and_attributes_and_data_ids_to_purge( lambda: select( Events.event_id, Events.data_id, States.state_id, States.attributes_id ) - .join(States, Events.event_id == States.event_id) + .outerjoin(States, Events.event_id == States.event_id) .filter(Events.time_fired < purge_before) .limit(MAX_ROWS_TO_PURGE) ) diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index c8ba5e9d076..f4e998c5388 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -26,7 +26,7 @@ from homeassistant.components.recorder.services import ( ) from homeassistant.components.recorder.tasks import PurgeTask from homeassistant.components.recorder.util import session_scope -from homeassistant.const import EVENT_STATE_CHANGED, STATE_ON +from homeassistant.const import EVENT_STATE_CHANGED, EVENT_THEMES_UPDATED, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util @@ -925,6 +925,15 @@ async def test_purge_without_state_attributes_filtered_states_to_empty( time_fired=timestamp, ) ) + session.add( + Events( + event_id=event_id + 1, + event_type=EVENT_THEMES_UPDATED, + event_data="{}", + origin="LOCAL", + time_fired=timestamp, + ) + ) service_data = {"keep_days": 10} _add_db_entries(hass) From bf47d86d30f374e6e2cd08b3c7e747eb9e30a083 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Jun 2022 00:33:46 -1000 Subject: [PATCH 1160/3516] Fix logbook spinner never disappearing when all entities are filtered (#72816) --- .../components/logbook/websocket_api.py | 4 +- .../components/logbook/test_websocket_api.py | 49 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index 82b1db1081c..1af44440803 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -356,7 +356,7 @@ async def ws_event_stream( ) await _async_wait_for_recorder_sync(hass) - if not subscriptions: + if msg_id not in connection.subscriptions: # Unsubscribe happened while waiting for recorder return @@ -388,6 +388,8 @@ async def ws_event_stream( if not subscriptions: # Unsubscribe happened while waiting for formatted events + # or there are no supported entities (all UOM or state class) + # or devices return live_stream.task = asyncio.create_task( diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 291c487b35b..2dd08ec44ce 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -2089,3 +2089,52 @@ async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplo assert msg["success"] assert "Recorder is behind" in caplog.text + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_client): + """Test subscribe/unsubscribe logbook stream with entities that are always filtered.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + await async_wait_recording_done(hass) + + init_count = sum(hass.bus.async_listeners().values()) + hass.states.async_set("sensor.uom", "1", {ATTR_UNIT_OF_MEASUREMENT: "any"}) + hass.states.async_set("sensor.uom", "2", {ATTR_UNIT_OF_MEASUREMENT: "any"}) + hass.states.async_set("sensor.uom", "3", {ATTR_UNIT_OF_MEASUREMENT: "any"}) + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "entity_ids": ["sensor.uom"], + } + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + hass.states.async_set("sensor.uom", "1", {ATTR_UNIT_OF_MEASUREMENT: "any"}) + hass.states.async_set("sensor.uom", "2", {ATTR_UNIT_OF_MEASUREMENT: "any"}) + hass.states.async_set("sensor.uom", "3", {ATTR_UNIT_OF_MEASUREMENT: "any"}) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [] + + await websocket_client.close() + await hass.async_block_till_done() + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count From 39da7a93ecc15cb414b0a13c2b0ee48deb17756e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Jun 2022 13:04:12 +0200 Subject: [PATCH 1161/3516] Bumped version to 2022.6.0 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0562776d589..428aa84d5fb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 88cf80344f6..7f1e739803b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.0b7 +version = 2022.6.0 url = https://www.home-assistant.io/ [options] From 4c7837a5762a741100b07d89803340f53b1d04b1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Jun 2022 13:09:53 +0200 Subject: [PATCH 1162/3516] Enforce type hints for config_flow (#72756) * Enforce type hints for config_flow * Keep astroid migration for another PR * Defer elif case * Adjust tests * Use ancestors * Match on single base_class * Invert for loops * Review comments * slots is new in 3.10 --- pylint/plugins/hass_enforce_type_hints.py | 93 ++++++++++++++++++++++- tests/pylint/test_enforce_type_hints.py | 72 ++++++++++++++++++ 2 files changed, 163 insertions(+), 2 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index f3dd7a106c6..31b396c3196 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -25,6 +25,14 @@ class TypeHintMatch: return_type: list[str] | str | None | object +@dataclass +class ClassTypeHintMatch: + """Class for pattern matching.""" + + base_class: str + matches: list[TypeHintMatch] + + _TYPE_HINT_MATCHERS: dict[str, re.Pattern[str]] = { # a_or_b matches items such as "DiscoveryInfoType | None" "a_or_b": re.compile(r"^(\w+) \| (\w+)$"), @@ -368,6 +376,65 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { ], } +_CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { + "config_flow": [ + ClassTypeHintMatch( + base_class="ConfigFlow", + matches=[ + TypeHintMatch( + function_name="async_step_dhcp", + arg_types={ + 1: "DhcpServiceInfo", + }, + return_type="FlowResult", + ), + TypeHintMatch( + function_name="async_step_hassio", + arg_types={ + 1: "HassioServiceInfo", + }, + return_type="FlowResult", + ), + TypeHintMatch( + function_name="async_step_homekit", + arg_types={ + 1: "ZeroconfServiceInfo", + }, + return_type="FlowResult", + ), + TypeHintMatch( + function_name="async_step_mqtt", + arg_types={ + 1: "MqttServiceInfo", + }, + return_type="FlowResult", + ), + TypeHintMatch( + function_name="async_step_ssdp", + arg_types={ + 1: "SsdpServiceInfo", + }, + return_type="FlowResult", + ), + TypeHintMatch( + function_name="async_step_usb", + arg_types={ + 1: "UsbServiceInfo", + }, + return_type="FlowResult", + ), + TypeHintMatch( + function_name="async_step_zeroconf", + arg_types={ + 1: "ZeroconfServiceInfo", + }, + return_type="FlowResult", + ), + ], + ), + ] +} + def _is_valid_type( expected_type: list[str] | str | None | object, node: astroid.NodeNG @@ -494,10 +561,12 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] def __init__(self, linter: PyLinter | None = None) -> None: super().__init__(linter) self._function_matchers: list[TypeHintMatch] = [] + self._class_matchers: list[ClassTypeHintMatch] = [] def visit_module(self, node: astroid.Module) -> None: """Called when a Module node is visited.""" self._function_matchers = [] + self._class_matchers = [] if (module_platform := _get_module_platform(node.name)) is None: return @@ -505,8 +574,28 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] if module_platform in _PLATFORMS: self._function_matchers.extend(_FUNCTION_MATCH["__any_platform__"]) - if matches := _FUNCTION_MATCH.get(module_platform): - self._function_matchers.extend(matches) + if function_matches := _FUNCTION_MATCH.get(module_platform): + self._function_matchers.extend(function_matches) + + if class_matches := _CLASS_MATCH.get(module_platform): + self._class_matchers = class_matches + + def visit_classdef(self, node: astroid.ClassDef) -> None: + """Called when a ClassDef node is visited.""" + ancestor: astroid.ClassDef + for ancestor in node.ancestors(): + for class_matches in self._class_matchers: + if ancestor.name == class_matches.base_class: + self._visit_class_functions(node, class_matches.matches) + + def _visit_class_functions( + self, node: astroid.ClassDef, matches: list[TypeHintMatch] + ) -> None: + for match in matches: + for function_node in node.mymethods(): + function_name: str | None = function_node.name + if match.function_name == function_name: + self._check_function(function_node, match) def visit_functiondef(self, node: astroid.FunctionDef) -> None: """Called when a FunctionDef node is visited.""" diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 0bd273985e3..fa3d32dcb04 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -277,3 +277,75 @@ def test_valid_list_dict_str_any( with assert_no_messages(linter): type_hint_checker.visit_asyncfunctiondef(func_node) + + +def test_invalid_config_flow_step( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Ensure invalid hints are rejected for ConfigFlow step.""" + class_node, func_node, arg_node = astroid.extract_node( + """ + class ConfigFlow(): + pass + + class AxisFlowHandler( #@ + ConfigFlow, domain=AXIS_DOMAIN + ): + async def async_step_zeroconf( #@ + self, + device_config: dict #@ + ): + pass + """, + "homeassistant.components.pylint_test.config_flow", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_adds_messages( + linter, + pylint.testutils.MessageTest( + msg_id="hass-argument-type", + node=arg_node, + args=(2, "ZeroconfServiceInfo"), + line=10, + col_offset=8, + end_line=10, + end_col_offset=27, + ), + pylint.testutils.MessageTest( + msg_id="hass-return-type", + node=func_node, + args="FlowResult", + line=8, + col_offset=4, + end_line=8, + end_col_offset=33, + ), + ): + type_hint_checker.visit_classdef(class_node) + + +def test_valid_config_flow_step( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Ensure valid hints are accepted for ConfigFlow step.""" + class_node = astroid.extract_node( + """ + class ConfigFlow(): + pass + + class AxisFlowHandler( #@ + ConfigFlow, domain=AXIS_DOMAIN + ): + async def async_step_zeroconf( + self, + device_config: ZeroconfServiceInfo + ) -> FlowResult: + pass + """, + "homeassistant.components.pylint_test.config_flow", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_no_messages(linter): + type_hint_checker.visit_classdef(class_node) From df5285f68138f066c92aabcc8d6deae8fcc2fba0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Jun 2022 16:49:43 +0200 Subject: [PATCH 1163/3516] Improve pylint disable rule in zha (#72835) --- .../components/zha/core/channels/manufacturerspecific.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 3bcab38e026..0ec9c7c2f4e 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -60,7 +60,7 @@ class OppleRemote(ZigbeeChannel): """Initialize Opple channel.""" super().__init__(cluster, ch_pool) if self.cluster.endpoint.model == "lumi.motion.ac02": - self.ZCL_INIT_ATTRS = { # pylint: disable=C0103 + self.ZCL_INIT_ATTRS = { # pylint: disable=invalid-name "detection_interval": True, "motion_sensitivity": True, "trigger_indicator": True, From 74e2d5c5c312cf3ba154b5206ceb19ba884c6fb4 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Wed, 1 Jun 2022 18:25:49 +0300 Subject: [PATCH 1164/3516] Remove deprecated YAML for `transmission` (#72832) --- .../components/transmission/__init__.py | 38 +------------ .../components/transmission/config_flow.py | 7 --- .../transmission/test_config_flow.py | 56 +------------------ 3 files changed, 3 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index a824185b13e..ac6659b048e 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -8,7 +8,7 @@ import transmissionrpc from transmissionrpc.error import TransmissionError import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_ID, @@ -24,7 +24,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_DELETE_DATA, @@ -34,9 +33,7 @@ from .const import ( DATA_UPDATED, DEFAULT_DELETE_DATA, DEFAULT_LIMIT, - DEFAULT_NAME, DEFAULT_ORDER, - DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN, EVENT_DOWNLOADED_TORRENT, @@ -78,42 +75,11 @@ SERVICE_STOP_TORRENT_SCHEMA = vol.Schema( } ) -TRANS_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - } - ) -) - -CONFIG_SCHEMA = vol.Schema( - vol.All(cv.deprecated(DOMAIN), {DOMAIN: vol.All(cv.ensure_list, [TRANS_SCHEMA])}), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.SENSOR, Platform.SWITCH] -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Import the Transmission Component from config.""" - if DOMAIN in config: - for entry in config[DOMAIN]: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=entry - ) - ) - - return True - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up the Transmission Component.""" client = TransmissionClient(hass, config_entry) diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index d5c63aa736f..ce62475b2eb 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -83,13 +83,6 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, import_config): - """Import from Transmission client config.""" - import_config[CONF_SCAN_INTERVAL] = import_config[ - CONF_SCAN_INTERVAL - ].total_seconds() - return await self.async_step_user(user_input=import_config) - class TransmissionOptionsFlowHandler(config_entries.OptionsFlow): """Handle Transmission client options.""" diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 91dfa25fd35..4e3a7c73d6c 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -1,5 +1,4 @@ """Tests for Transmission config flow.""" -from datetime import timedelta from unittest.mock import patch import pytest @@ -8,15 +7,7 @@ from transmissionrpc.error import TransmissionError from homeassistant import config_entries, data_entry_flow from homeassistant.components import transmission from homeassistant.components.transmission import config_flow -from homeassistant.components.transmission.const import ( - CONF_LIMIT, - CONF_ORDER, - DEFAULT_LIMIT, - DEFAULT_NAME, - DEFAULT_ORDER, - DEFAULT_PORT, - DEFAULT_SCAN_INTERVAL, -) +from homeassistant.components.transmission.const import DEFAULT_SCAN_INTERVAL from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -153,51 +144,6 @@ async def test_options(hass): assert result["data"][CONF_SCAN_INTERVAL] == 10 -async def test_import(hass, api): - """Test import step.""" - flow = init_config_flow(hass) - - # import with minimum fields only - result = await flow.async_step_import( - { - CONF_NAME: DEFAULT_NAME, - CONF_HOST: HOST, - CONF_PORT: DEFAULT_PORT, - CONF_SCAN_INTERVAL: timedelta(seconds=DEFAULT_SCAN_INTERVAL), - CONF_LIMIT: DEFAULT_LIMIT, - CONF_ORDER: DEFAULT_ORDER, - } - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == DEFAULT_NAME - assert result["data"][CONF_NAME] == DEFAULT_NAME - assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_PORT] == DEFAULT_PORT - assert result["data"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL - - # import with all - result = await flow.async_step_import( - { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_PORT: PORT, - CONF_SCAN_INTERVAL: timedelta(seconds=SCAN_INTERVAL), - CONF_LIMIT: DEFAULT_LIMIT, - CONF_ORDER: DEFAULT_ORDER, - } - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == NAME - assert result["data"][CONF_NAME] == NAME - assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_PORT] == PORT - assert result["data"][CONF_SCAN_INTERVAL] == SCAN_INTERVAL - - async def test_host_already_configured(hass, api): """Test host is already configured.""" entry = MockConfigEntry( From d6e5c26b2482623d43eef10a4690a47a5719d1d9 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Thu, 2 Jun 2022 03:41:20 +1000 Subject: [PATCH 1165/3516] Add configuration_url to hunterdouglas_powerview (#72837) Co-authored-by: J. Nick Koston --- homeassistant/components/hunterdouglas_powerview/__init__.py | 3 +++ homeassistant/components/hunterdouglas_powerview/const.py | 1 + homeassistant/components/hunterdouglas_powerview/entity.py | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 97f7f8de931..587bf8aef12 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -37,6 +37,7 @@ from .const import ( HUB_NAME, MAC_ADDRESS_IN_USERDATA, PV_API, + PV_HUB_ADDRESS, PV_ROOM_DATA, PV_SCENE_DATA, PV_SHADE_DATA, @@ -72,6 +73,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: async with async_timeout.timeout(10): device_info = await async_get_device_info(pv_request) + device_info[PV_HUB_ADDRESS] = hub_address async with async_timeout.timeout(10): rooms = Rooms(pv_request) @@ -97,6 +99,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = PowerviewShadeUpdateCoordinator(hass, shades, hub_address) coordinator.async_set_updated_data(PowerviewShadeData()) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { PV_API: pv_request, PV_ROOM_DATA: room_data, diff --git a/homeassistant/components/hunterdouglas_powerview/const.py b/homeassistant/components/hunterdouglas_powerview/const.py index 1cc9f79df40..7146d40c737 100644 --- a/homeassistant/components/hunterdouglas_powerview/const.py +++ b/homeassistant/components/hunterdouglas_powerview/const.py @@ -54,6 +54,7 @@ STATE_ATTRIBUTE_ROOM_NAME = "roomName" PV_API = "pv_api" PV_HUB = "pv_hub" +PV_HUB_ADDRESS = "pv_hub_address" PV_SHADES = "pv_shades" PV_SCENE_DATA = "pv_scene_data" PV_SHADE_DATA = "pv_shade_data" diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py index 174e3def2d6..04885fa576e 100644 --- a/homeassistant/components/hunterdouglas_powerview/entity.py +++ b/homeassistant/components/hunterdouglas_powerview/entity.py @@ -21,6 +21,7 @@ from .const import ( FIRMWARE_REVISION, FIRMWARE_SUB_REVISION, MANUFACTURER, + PV_HUB_ADDRESS, ) from .coordinator import PowerviewShadeUpdateCoordinator from .shade_data import PowerviewShadeData, PowerviewShadePositions @@ -40,6 +41,7 @@ class HDEntity(CoordinatorEntity[PowerviewShadeUpdateCoordinator]): super().__init__(coordinator) self._room_name = room_name self._attr_unique_id = unique_id + self._hub_address = device_info[PV_HUB_ADDRESS] self._device_info = device_info @property @@ -62,6 +64,7 @@ class HDEntity(CoordinatorEntity[PowerviewShadeUpdateCoordinator]): name=self._device_info[DEVICE_NAME], suggested_area=self._room_name, sw_version=sw_version, + configuration_url=f"http://{self._hub_address}/api/shades", ) @@ -97,6 +100,7 @@ class ShadeEntity(HDEntity): manufacturer=MANUFACTURER, model=str(self._shade.raw_data[ATTR_TYPE]), via_device=(DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER]), + configuration_url=f"http://{self._hub_address}/api/shades/{self._shade.id}", ) for shade in self._shade.shade_types: From 2ba45a9f99cdb3a01942e230e997c90b383c44c9 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Wed, 1 Jun 2022 22:54:22 +0100 Subject: [PATCH 1166/3516] System Bridge 3.x.x (#71218) * Change to new package and tcp * Rework integration pt1 * Show by default * Handle auth error * Use const * New version avaliable (to be replaced in future by update entity) * Remove visible * Version latest * Filesystem space use * Dev package * Fix sensor * Add services * Update package * Add temperature and voltage * GPU * Bump package version * Update config flow * Add displays * Fix displays connected * Round to whole number * GPU fan speed in RPM * Handle disconnections * Update package * Fix * Update tests * Handle more errors * Check submodule and return missing uuid in test * Handle auth error on config flow * Fix test * Bump package version * Handle key errors * Update package to release version * Client session in config flow * Log * Increase timeout and use similar logic in config flow to init * 30 secs * Add test for timeout error * Cleanup logs Co-authored-by: Martin Hjelmare * Update tests/components/system_bridge/test_config_flow.py Co-authored-by: Martin Hjelmare * uuid raise specific error * Type * Lambda to functions for complex logic * Unknown error test * Bump package to 3.0.5 * Bump package to 3.0.6 * Use typings from package and pydantic * Use dict() * Use data listener function and map to models * Use passed module handler * Use lists from models * Update to 3.1.0 * Update coordinator to use passed module * Improve coordinator * Add debug * Bump package and avaliable -> available * Add version check Co-authored-by: Martin Hjelmare --- .../components/system_bridge/__init__.py | 230 +++---- .../components/system_bridge/binary_sensor.py | 29 +- .../components/system_bridge/config_flow.py | 124 ++-- .../components/system_bridge/const.py | 25 +- .../components/system_bridge/coordinator.py | 205 ++++--- .../components/system_bridge/manifest.json | 6 +- .../components/system_bridge/sensor.py | 569 +++++++++--------- .../components/system_bridge/services.yaml | 64 +- homeassistant/generated/zeroconf.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../system_bridge/test_config_flow.py | 365 +++++++---- 12 files changed, 892 insertions(+), 731 deletions(-) diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index c6edf5b61ea..68a017628b8 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -3,102 +3,114 @@ from __future__ import annotations import asyncio import logging -import shlex import async_timeout -from systembridge import Bridge -from systembridge.client import BridgeClient -from systembridge.exceptions import BridgeAuthenticationException -from systembridge.objects.command.response import CommandResponse -from systembridge.objects.keyboard.payload import KeyboardPayload +from systembridgeconnector.exceptions import ( + AuthenticationException, + ConnectionClosedException, + ConnectionErrorException, +) +from systembridgeconnector.version import SUPPORTED_VERSION, Version import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, - CONF_COMMAND, CONF_HOST, CONF_PATH, CONF_PORT, + CONF_URL, Platform, ) from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ( - ConfigEntryAuthFailed, - ConfigEntryNotReady, - HomeAssistantError, -) -from homeassistant.helpers import ( - aiohttp_client, - config_validation as cv, - device_registry as dr, -) +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import BRIDGE_CONNECTION_ERRORS, DOMAIN +from .const import DOMAIN, MODULES from .coordinator import SystemBridgeDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.SENSOR, +] -CONF_ARGUMENTS = "arguments" CONF_BRIDGE = "bridge" CONF_KEY = "key" -CONF_MODIFIERS = "modifiers" CONF_TEXT = "text" -CONF_WAIT = "wait" -SERVICE_SEND_COMMAND = "send_command" -SERVICE_OPEN = "open" +SERVICE_OPEN_PATH = "open_path" +SERVICE_OPEN_URL = "open_url" SERVICE_SEND_KEYPRESS = "send_keypress" SERVICE_SEND_TEXT = "send_text" async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up System Bridge from a config entry.""" - bridge = Bridge( - BridgeClient(aiohttp_client.async_get_clientsession(hass)), - f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}", - entry.data[CONF_API_KEY], - ) + # Check version before initialising + version = Version( + entry.data[CONF_HOST], + entry.data[CONF_PORT], + entry.data[CONF_API_KEY], + session=async_get_clientsession(hass), + ) try: - async with async_timeout.timeout(30): - await bridge.async_get_information() - except BridgeAuthenticationException as exception: - raise ConfigEntryAuthFailed( - f"Authentication failed for {entry.title} ({entry.data[CONF_HOST]})" - ) from exception - except BRIDGE_CONNECTION_ERRORS as exception: + if not await version.check_supported(): + raise ConfigEntryNotReady( + f"You are not running a supported version of System Bridge. Please update to {SUPPORTED_VERSION} or higher." + ) + except AuthenticationException as exception: + _LOGGER.error("Authentication failed for %s: %s", entry.title, exception) + raise ConfigEntryAuthFailed from exception + except (ConnectionClosedException, ConnectionErrorException) as exception: raise ConfigEntryNotReady( f"Could not connect to {entry.title} ({entry.data[CONF_HOST]})." ) from exception + except asyncio.TimeoutError as exception: + raise ConfigEntryNotReady( + f"Timed out waiting for {entry.title} ({entry.data[CONF_HOST]})." + ) from exception + + coordinator = SystemBridgeDataUpdateCoordinator( + hass, + _LOGGER, + entry=entry, + ) + try: + async with async_timeout.timeout(30): + await coordinator.async_get_data(MODULES) + except AuthenticationException as exception: + _LOGGER.error("Authentication failed for %s: %s", entry.title, exception) + raise ConfigEntryAuthFailed from exception + except (ConnectionClosedException, ConnectionErrorException) as exception: + raise ConfigEntryNotReady( + f"Could not connect to {entry.title} ({entry.data[CONF_HOST]})." + ) from exception + except asyncio.TimeoutError as exception: + raise ConfigEntryNotReady( + f"Timed out waiting for {entry.title} ({entry.data[CONF_HOST]})." + ) from exception - coordinator = SystemBridgeDataUpdateCoordinator(hass, bridge, _LOGGER, entry=entry) await coordinator.async_config_entry_first_refresh() - # Wait for initial data + _LOGGER.debug("Data: %s", coordinator.data) + try: - async with async_timeout.timeout(60): - 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 - or coordinator.bridge.memory is None - or coordinator.bridge.network is None - or coordinator.bridge.os is None - or coordinator.bridge.processes is None - or coordinator.bridge.system is None + # Wait for initial data + async with async_timeout.timeout(30): + while coordinator.data is None or all( + getattr(coordinator.data, module) is None for module in MODULES ): _LOGGER.debug( - "Waiting for initial data from %s (%s)", + "Waiting for initial data from %s (%s): %s", entry.title, entry.data[CONF_HOST], + coordinator.data, ) await asyncio.sleep(1) except asyncio.TimeoutError as exception: @@ -111,7 +123,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) - if hass.services.has_service(DOMAIN, SERVICE_SEND_COMMAND): + if hass.services.has_service(DOMAIN, SERVICE_OPEN_URL): return True def valid_device(device: str): @@ -129,104 +141,56 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise vol.Invalid from exception raise vol.Invalid(f"Device {device} does not exist") - async def handle_send_command(call: ServiceCall) -> None: - """Handle the send_command service call.""" + async def handle_open_path(call: ServiceCall) -> None: + """Handle the open path service call.""" + _LOGGER.info("Open: %s", call.data) coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ call.data[CONF_BRIDGE] ] - bridge: Bridge = coordinator.bridge + await coordinator.websocket_client.open_path(call.data[CONF_PATH]) - command = call.data[CONF_COMMAND] - arguments = shlex.split(call.data[CONF_ARGUMENTS]) - - _LOGGER.debug( - "Command payload: %s", - {CONF_COMMAND: command, CONF_ARGUMENTS: arguments, CONF_WAIT: False}, - ) - try: - response: CommandResponse = await bridge.async_send_command( - {CONF_COMMAND: command, CONF_ARGUMENTS: arguments, CONF_WAIT: False} - ) - if not response.success: - raise HomeAssistantError( - f"Error sending command. Response message was: {response.message}" - ) - except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception: - raise HomeAssistantError("Error sending command") from exception - _LOGGER.debug("Sent command. Response message was: %s", response.message) - - async def handle_open(call: ServiceCall) -> None: - """Handle the open service call.""" + async def handle_open_url(call: ServiceCall) -> None: + """Handle the open url service call.""" + _LOGGER.info("Open: %s", call.data) coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ call.data[CONF_BRIDGE] ] - bridge: Bridge = coordinator.bridge - - path = call.data[CONF_PATH] - - _LOGGER.debug("Open payload: %s", {CONF_PATH: path}) - try: - await bridge.async_open({CONF_PATH: path}) - except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception: - raise HomeAssistantError("Error sending") from exception - _LOGGER.debug("Sent open request") + await coordinator.websocket_client.open_url(call.data[CONF_URL]) async def handle_send_keypress(call: ServiceCall) -> None: """Handle the send_keypress service call.""" coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ call.data[CONF_BRIDGE] ] - bridge: Bridge = coordinator.data - - keyboard_payload: KeyboardPayload = { - CONF_KEY: call.data[CONF_KEY], - CONF_MODIFIERS: shlex.split(call.data.get(CONF_MODIFIERS, "")), - } - - _LOGGER.debug("Keypress payload: %s", keyboard_payload) - try: - await bridge.async_send_keypress(keyboard_payload) - except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception: - raise HomeAssistantError("Error sending") from exception - _LOGGER.debug("Sent keypress request") + await coordinator.websocket_client.keyboard_keypress(call.data[CONF_KEY]) async def handle_send_text(call: ServiceCall) -> None: """Handle the send_keypress service call.""" coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ call.data[CONF_BRIDGE] ] - bridge: Bridge = coordinator.data - - keyboard_payload: KeyboardPayload = {CONF_TEXT: call.data[CONF_TEXT]} - - _LOGGER.debug("Text payload: %s", keyboard_payload) - try: - await bridge.async_send_keypress(keyboard_payload) - except (BridgeAuthenticationException, *BRIDGE_CONNECTION_ERRORS) as exception: - raise HomeAssistantError("Error sending") from exception - _LOGGER.debug("Sent text request") + await coordinator.websocket_client.keyboard_text(call.data[CONF_TEXT]) hass.services.async_register( DOMAIN, - SERVICE_SEND_COMMAND, - handle_send_command, + SERVICE_OPEN_PATH, + handle_open_path, schema=vol.Schema( { vol.Required(CONF_BRIDGE): valid_device, - vol.Required(CONF_COMMAND): cv.string, - vol.Optional(CONF_ARGUMENTS, ""): cv.string, + vol.Required(CONF_PATH): cv.string, }, ), ) hass.services.async_register( DOMAIN, - SERVICE_OPEN, - handle_open, + SERVICE_OPEN_URL, + handle_open_url, schema=vol.Schema( { vol.Required(CONF_BRIDGE): valid_device, - vol.Required(CONF_PATH): cv.string, + vol.Required(CONF_URL): cv.string, }, ), ) @@ -239,7 +203,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: { vol.Required(CONF_BRIDGE): valid_device, vol.Required(CONF_KEY): cv.string, - vol.Optional(CONF_MODIFIERS): cv.string, }, ), ) @@ -271,15 +234,17 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ] # Ensure disconnected and cleanup stop sub - await coordinator.bridge.async_close_websocket() + await coordinator.websocket_client.close() if coordinator.unsub: coordinator.unsub() del hass.data[DOMAIN][entry.entry_id] if not hass.data[DOMAIN]: - hass.services.async_remove(DOMAIN, SERVICE_SEND_COMMAND) - hass.services.async_remove(DOMAIN, SERVICE_OPEN) + hass.services.async_remove(DOMAIN, SERVICE_OPEN_PATH) + hass.services.async_remove(DOMAIN, SERVICE_OPEN_URL) + hass.services.async_remove(DOMAIN, SERVICE_SEND_KEYPRESS) + hass.services.async_remove(DOMAIN, SERVICE_SEND_TEXT) return unload_ok @@ -295,20 +260,21 @@ class SystemBridgeEntity(CoordinatorEntity[SystemBridgeDataUpdateCoordinator]): def __init__( self, coordinator: SystemBridgeDataUpdateCoordinator, + api_port: int, key: str, name: str | None, ) -> None: """Initialize the System Bridge entity.""" super().__init__(coordinator) - bridge: Bridge = coordinator.data - self._key = f"{bridge.information.host}_{key}" - self._name = f"{bridge.information.host} {name}" - self._configuration_url = bridge.get_configuration_url() - self._hostname = bridge.information.host - self._mac = bridge.information.mac - self._manufacturer = bridge.system.system.manufacturer - self._model = bridge.system.system.model - self._version = bridge.system.system.version + + self._hostname = coordinator.data.system.hostname + self._key = f"{self._hostname}_{key}" + self._name = f"{self._hostname} {name}" + self._configuration_url = ( + f"http://{self._hostname}:{api_port}/app/settings.html" + ) + self._mac_address = coordinator.data.system.mac_address + self._version = coordinator.data.system.version @property def unique_id(self) -> str: @@ -329,9 +295,7 @@ class SystemBridgeDeviceEntity(SystemBridgeEntity): """Return device information about this System Bridge instance.""" return DeviceInfo( configuration_url=self._configuration_url, - connections={(dr.CONNECTION_NETWORK_MAC, self._mac)}, - manufacturer=self._manufacturer, - model=self._model, + connections={(dr.CONNECTION_NETWORK_MAC, self._mac_address)}, name=self._hostname, sw_version=self._version, ) diff --git a/homeassistant/components/system_bridge/binary_sensor.py b/homeassistant/components/system_bridge/binary_sensor.py index e592c8e82e4..9225aebf492 100644 --- a/homeassistant/components/system_bridge/binary_sensor.py +++ b/homeassistant/components/system_bridge/binary_sensor.py @@ -4,14 +4,13 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from systembridge import Bridge - from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -32,7 +31,7 @@ BASE_BINARY_SENSOR_TYPES: tuple[SystemBridgeBinarySensorEntityDescription, ...] key="version_available", name="New Version Available", device_class=BinarySensorDeviceClass.UPDATE, - value=lambda bridge: bridge.information.updates.available, + value=lambda data: data.system.version_newer_available, ), ) @@ -41,7 +40,7 @@ BATTERY_BINARY_SENSOR_TYPES: tuple[SystemBridgeBinarySensorEntityDescription, .. key="battery_is_charging", name="Battery Is Charging", device_class=BinarySensorDeviceClass.BATTERY_CHARGING, - value=lambda bridge: bridge.battery.isCharging, + value=lambda data: data.battery.is_charging, ), ) @@ -51,15 +50,24 @@ async def async_setup_entry( ) -> None: """Set up System Bridge binary sensor based on a config entry.""" coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - bridge: Bridge = coordinator.data entities = [] for description in BASE_BINARY_SENSOR_TYPES: - entities.append(SystemBridgeBinarySensor(coordinator, description)) + entities.append( + SystemBridgeBinarySensor(coordinator, description, entry.data[CONF_PORT]) + ) - if bridge.battery and bridge.battery.hasBattery: + if ( + coordinator.data.battery + and coordinator.data.battery.percentage + and coordinator.data.battery.percentage > -1 + ): for description in BATTERY_BINARY_SENSOR_TYPES: - entities.append(SystemBridgeBinarySensor(coordinator, description)) + entities.append( + SystemBridgeBinarySensor( + coordinator, description, entry.data[CONF_PORT] + ) + ) async_add_entities(entities) @@ -73,10 +81,12 @@ class SystemBridgeBinarySensor(SystemBridgeDeviceEntity, BinarySensorEntity): self, coordinator: SystemBridgeDataUpdateCoordinator, description: SystemBridgeBinarySensorEntityDescription, + api_port: int, ) -> None: """Initialize.""" super().__init__( coordinator, + api_port, description.key, description.name, ) @@ -85,5 +95,4 @@ class SystemBridgeBinarySensor(SystemBridgeDeviceEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return the boolean state of the binary sensor.""" - bridge: Bridge = self.coordinator.data - return self.entity_description.value(bridge) + return self.entity_description.value(self.coordinator.data) diff --git a/homeassistant/components/system_bridge/config_flow.py b/homeassistant/components/system_bridge/config_flow.py index 26ccf83c345..0c1241fcf2c 100644 --- a/homeassistant/components/system_bridge/config_flow.py +++ b/homeassistant/components/system_bridge/config_flow.py @@ -1,13 +1,18 @@ """Config flow for System Bridge integration.""" from __future__ import annotations +import asyncio import logging from typing import Any import async_timeout -from systembridge import Bridge -from systembridge.client import BridgeClient -from systembridge.exceptions import BridgeAuthenticationException +from systembridgeconnector.const import EVENT_MODULE, EVENT_TYPE, TYPE_DATA_UPDATE +from systembridgeconnector.exceptions import ( + AuthenticationException, + ConnectionClosedException, + ConnectionErrorException, +) +from systembridgeconnector.websocket_client import WebSocketClient import voluptuous as vol from homeassistant import config_entries, exceptions @@ -15,9 +20,10 @@ from homeassistant.components import zeroconf from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import BRIDGE_CONNECTION_ERRORS, DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -31,39 +37,84 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: +async def validate_input( + hass: HomeAssistant, + data: dict[str, Any], +) -> 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. """ - bridge = Bridge( - BridgeClient(aiohttp_client.async_get_clientsession(hass)), - f"http://{data[CONF_HOST]}:{data[CONF_PORT]}", + host = data[CONF_HOST] + + websocket_client = WebSocketClient( + host, + data[CONF_PORT], data[CONF_API_KEY], ) - - hostname = data[CONF_HOST] try: async with async_timeout.timeout(30): - await bridge.async_get_information() - if ( - bridge.information is not None - and bridge.information.host is not None - and bridge.information.uuid is not None - ): - hostname = bridge.information.host - uuid = bridge.information.uuid - except BridgeAuthenticationException as exception: - _LOGGER.info(exception) + await websocket_client.connect(session=async_get_clientsession(hass)) + await websocket_client.get_data(["system"]) + while True: + message = await websocket_client.receive_message() + _LOGGER.debug("Message: %s", message) + if ( + message[EVENT_TYPE] == TYPE_DATA_UPDATE + and message[EVENT_MODULE] == "system" + ): + break + except AuthenticationException as exception: + _LOGGER.warning( + "Authentication error when connecting to %s: %s", data[CONF_HOST], exception + ) raise InvalidAuth from exception - except BRIDGE_CONNECTION_ERRORS as exception: - _LOGGER.info(exception) + except ( + ConnectionClosedException, + ConnectionErrorException, + ) as exception: + _LOGGER.warning( + "Connection error when connecting to %s: %s", data[CONF_HOST], exception + ) + raise CannotConnect from exception + except asyncio.TimeoutError as exception: + _LOGGER.warning("Timed out connecting to %s: %s", data[CONF_HOST], exception) raise CannotConnect from exception - return {"hostname": hostname, "uuid": uuid} + _LOGGER.debug("%s Message: %s", TYPE_DATA_UPDATE, message) + + if "uuid" not in message["data"]: + error = "No UUID in result!" + raise CannotConnect(error) + + return {"hostname": host, "uuid": message["data"]["uuid"]} -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +async def _async_get_info( + hass: HomeAssistant, + user_input: dict[str, Any], +) -> tuple[dict[str, str], dict[str, str] | None]: + errors = {} + + try: + info = await validate_input(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 errors, info + + return errors, None + + +class ConfigFlow( + config_entries.ConfigFlow, + domain=DOMAIN, +): """Handle a config flow for System Bridge.""" VERSION = 1 @@ -74,25 +125,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._input: dict[str, Any] = {} self._reauth = False - async def _async_get_info( - self, user_input: dict[str, Any] - ) -> tuple[dict[str, str], dict[str, str] | None]: - 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 errors, info - - return errors, None - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -102,7 +134,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA ) - errors, info = await self._async_get_info(user_input) + errors, info = await _async_get_info(self.hass, user_input) if not errors and info is not None: # Check if already configured await self.async_set_unique_id(info["uuid"], raise_on_progress=False) @@ -122,7 +154,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: user_input = {**self._input, **user_input} - errors, info = await self._async_get_info(user_input) + errors, info = await _async_get_info(self.hass, user_input) if not errors and info is not None: # Check if already configured existing_entry = await self.async_set_unique_id(info["uuid"]) diff --git a/homeassistant/components/system_bridge/const.py b/homeassistant/components/system_bridge/const.py index f2e83ceb186..c71ee86c920 100644 --- a/homeassistant/components/system_bridge/const.py +++ b/homeassistant/components/system_bridge/const.py @@ -1,20 +1,13 @@ """Constants for the System Bridge integration.""" -import asyncio - -from aiohttp.client_exceptions import ( - ClientConnectionError, - ClientConnectorError, - ClientResponseError, -) -from systembridge.exceptions import BridgeException DOMAIN = "system_bridge" -BRIDGE_CONNECTION_ERRORS = ( - asyncio.TimeoutError, - BridgeException, - ClientConnectionError, - ClientConnectorError, - ClientResponseError, - OSError, -) +MODULES = [ + "battery", + "cpu", + "disk", + "display", + "gpu", + "memory", + "system", +] diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py index 896309f2593..89a0c85c1d9 100644 --- a/homeassistant/components/system_bridge/coordinator.py +++ b/homeassistant/components/system_bridge/coordinator.py @@ -6,40 +6,71 @@ from collections.abc import Callable from datetime import timedelta import logging -from systembridge import Bridge -from systembridge.exceptions import ( - BridgeAuthenticationException, - BridgeConnectionClosedException, - BridgeException, +import async_timeout +from pydantic import BaseModel # pylint: disable=no-name-in-module +from systembridgeconnector.exceptions import ( + AuthenticationException, + ConnectionClosedException, + ConnectionErrorException, ) -from systembridge.objects.events import Event +from systembridgeconnector.models.battery import Battery +from systembridgeconnector.models.cpu import Cpu +from systembridgeconnector.models.disk import Disk +from systembridgeconnector.models.display import Display +from systembridgeconnector.models.gpu import Gpu +from systembridgeconnector.models.memory import Memory +from systembridgeconnector.models.system import System +from systembridgeconnector.websocket_client import WebSocketClient from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_STOP, +) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import BRIDGE_CONNECTION_ERRORS, DOMAIN +from .const import DOMAIN, MODULES -class SystemBridgeDataUpdateCoordinator(DataUpdateCoordinator[Bridge]): +class SystemBridgeCoordinatorData(BaseModel): + """System Bridge Coordianator Data.""" + + battery: Battery = None + cpu: Cpu = None + disk: Disk = None + display: Display = None + gpu: Gpu = None + memory: Memory = None + system: System = None + + +class SystemBridgeDataUpdateCoordinator( + DataUpdateCoordinator[SystemBridgeCoordinatorData] +): """Class to manage fetching System Bridge data from single endpoint.""" def __init__( self, hass: HomeAssistant, - bridge: Bridge, LOGGER: logging.Logger, *, entry: ConfigEntry, ) -> None: """Initialize global System Bridge data updater.""" - self.bridge = bridge self.title = entry.title - self.host = entry.data[CONF_HOST] self.unsub: Callable | None = None + self.systembridge_data = SystemBridgeCoordinatorData() + self.websocket_client = WebSocketClient( + entry.data[CONF_HOST], + entry.data[CONF_PORT], + entry.data[CONF_API_KEY], + ) + super().__init__( hass, LOGGER, name=DOMAIN, update_interval=timedelta(seconds=30) ) @@ -49,97 +80,117 @@ class SystemBridgeDataUpdateCoordinator(DataUpdateCoordinator[Bridge]): for update_callback in self._listeners: update_callback() - async def async_handle_event(self, event: Event): - """Handle System Bridge events from the WebSocket.""" - # No need to update anything, as everything is updated in the caller - self.logger.debug( - "New event from %s (%s): %s", self.title, self.host, event.name - ) - self.async_set_updated_data(self.bridge) + async def async_get_data( + self, + modules: list[str], + ) -> None: + """Get data from WebSocket.""" + if not self.websocket_client.connected: + await self._setup_websocket() - async def _listen_for_events(self) -> None: + await self.websocket_client.get_data(modules) + + async def async_handle_module( + self, + module_name: str, + module, + ) -> None: + """Handle data from the WebSocket client.""" + self.logger.debug("Set new data for: %s", module_name) + setattr(self.systembridge_data, module_name, module) + self.async_set_updated_data(self.systembridge_data) + + async def _listen_for_data(self) -> None: """Listen for events from the WebSocket.""" + try: - await self.bridge.async_send_event( - "get-data", - [ - {"service": "battery", "method": "findAll", "observe": True}, - {"service": "cpu", "method": "findAll", "observe": True}, - {"service": "display", "method": "findAll", "observe": True}, - {"service": "filesystem", "method": "findSizes", "observe": True}, - {"service": "graphics", "method": "findAll", "observe": True}, - {"service": "memory", "method": "findAll", "observe": True}, - {"service": "network", "method": "findAll", "observe": True}, - {"service": "os", "method": "findAll", "observe": False}, - { - "service": "processes", - "method": "findCurrentLoad", - "observe": True, - }, - {"service": "system", "method": "findAll", "observe": False}, - ], - ) - await self.bridge.listen_for_events(callback=self.async_handle_event) - except BridgeConnectionClosedException as exception: + await self.websocket_client.register_data_listener(MODULES) + await self.websocket_client.listen(callback=self.async_handle_module) + except AuthenticationException as exception: self.last_update_success = False - self.logger.info( - "Websocket Connection Closed for %s (%s). Will retry: %s", - self.title, - self.host, - exception, - ) - except BridgeException as exception: + self.logger.error("Authentication failed for %s: %s", self.title, exception) + if self.unsub: + self.unsub() + self.unsub = None self.last_update_success = False self.update_listeners() - self.logger.warning( - "Exception occurred for %s (%s). Will retry: %s", + except (ConnectionClosedException, ConnectionResetError) as exception: + self.logger.info( + "Websocket connection closed for %s. Will retry: %s", self.title, - self.host, exception, ) + if self.unsub: + self.unsub() + self.unsub = None + self.last_update_success = False + self.update_listeners() + except ConnectionErrorException as exception: + self.logger.warning( + "Connection error occurred for %s. Will retry: %s", + self.title, + exception, + ) + if self.unsub: + self.unsub() + self.unsub = None + self.last_update_success = False + self.update_listeners() async def _setup_websocket(self) -> None: """Use WebSocket for updates.""" - try: - self.logger.debug( - "Connecting to ws://%s:%s", - self.host, - self.bridge.information.websocketPort, - ) - await self.bridge.async_connect_websocket( - self.host, self.bridge.information.websocketPort - ) - except BridgeAuthenticationException as exception: + async with async_timeout.timeout(20): + await self.websocket_client.connect( + session=async_get_clientsession(self.hass), + ) + except AuthenticationException as exception: + self.last_update_success = False + self.logger.error("Authentication failed for %s: %s", self.title, exception) if self.unsub: self.unsub() self.unsub = None - raise ConfigEntryAuthFailed() from exception - except (*BRIDGE_CONNECTION_ERRORS, ConnectionRefusedError) as exception: - if self.unsub: - self.unsub() - self.unsub = None - raise UpdateFailed( - f"Could not connect to {self.title} ({self.host})." - ) from exception - asyncio.create_task(self._listen_for_events()) + self.last_update_success = False + self.update_listeners() + except ConnectionErrorException as exception: + self.logger.warning( + "Connection error occurred for %s. Will retry: %s", + self.title, + exception, + ) + self.last_update_success = False + self.update_listeners() + except asyncio.TimeoutError as exception: + self.logger.warning( + "Timed out waiting for %s. Will retry: %s", + self.title, + exception, + ) + self.last_update_success = False + self.update_listeners() + + self.hass.async_create_task(self._listen_for_data()) + self.last_update_success = True + self.update_listeners() async def close_websocket(_) -> None: """Close WebSocket connection.""" - await self.bridge.async_close_websocket() + await self.websocket_client.close() # Clean disconnect WebSocket on Home Assistant shutdown self.unsub = self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, close_websocket ) - async def _async_update_data(self) -> Bridge: + async def _async_update_data(self) -> SystemBridgeCoordinatorData: """Update System Bridge data from WebSocket.""" self.logger.debug( "_async_update_data - WebSocket Connected: %s", - self.bridge.websocket_connected, + self.websocket_client.connected, ) - if not self.bridge.websocket_connected: + if not self.websocket_client.connected: await self._setup_websocket() - return self.bridge + self.logger.debug("_async_update_data done") + + return self.systembridge_data diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 8fba9dd30cf..76449e3f3ac 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -3,11 +3,11 @@ "name": "System Bridge", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/system_bridge", - "requirements": ["systembridge==2.3.1"], + "requirements": ["systembridgeconnector==3.1.3"], "codeowners": ["@timmo001"], - "zeroconf": ["_system-bridge._udp.local."], + "zeroconf": ["_system-bridge._tcp.local."], "after_dependencies": ["zeroconf"], "quality_scale": "silver", "iot_class": "local_push", - "loggers": ["systembridge"] + "loggers": ["systembridgeconnector"] } diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index e66749820a7..b37ff66896e 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -6,8 +6,6 @@ from dataclasses import dataclass from datetime import datetime, timedelta from typing import Final, cast -from systembridge import Bridge - from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -16,6 +14,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONF_PORT, DATA_GIGABYTES, ELECTRIC_POTENTIAL_VOLT, FREQUENCY_GIGAHERTZ, @@ -28,10 +27,11 @@ 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 utcnow from . import SystemBridgeDeviceEntity from .const import DOMAIN -from .coordinator import SystemBridgeDataUpdateCoordinator +from .coordinator import SystemBridgeCoordinatorData, SystemBridgeDataUpdateCoordinator ATTR_AVAILABLE: Final = "available" ATTR_FILESYSTEM: Final = "filesystem" @@ -41,6 +41,7 @@ ATTR_TYPE: Final = "type" ATTR_USED: Final = "used" PIXELS: Final = "px" +RPM: Final = "RPM" @dataclass @@ -50,21 +51,87 @@ class SystemBridgeSensorEntityDescription(SensorEntityDescription): value: Callable = round +def battery_time_remaining(data: SystemBridgeCoordinatorData) -> datetime | None: + """Return the battery time remaining.""" + if data.battery.sensors_secsleft is not None: + return utcnow() + timedelta(seconds=data.battery.sensors_secsleft) + return None + + +def cpu_speed(data: SystemBridgeCoordinatorData) -> float | None: + """Return the CPU speed.""" + if data.cpu.frequency_current is not None: + return round(data.cpu.frequency_current / 1000, 2) + return None + + +def gpu_core_clock_speed(data: SystemBridgeCoordinatorData, key: str) -> float | None: + """Return the GPU core clock speed.""" + if getattr(data.gpu, f"{key}_core_clock") is not None: + return round(getattr(data.gpu, f"{key}_core_clock")) + return None + + +def gpu_memory_clock_speed(data: SystemBridgeCoordinatorData, key: str) -> float | None: + """Return the GPU memory clock speed.""" + if getattr(data.gpu, f"{key}_memory_clock") is not None: + return round(getattr(data.gpu, f"{key}_memory_clock")) + return None + + +def gpu_memory_free(data: SystemBridgeCoordinatorData, key: str) -> float | None: + """Return the free GPU memory.""" + if getattr(data.gpu, f"{key}_memory_free") is not None: + return round(getattr(data.gpu, f"{key}_memory_free") / 10**3, 2) + return None + + +def gpu_memory_used(data: SystemBridgeCoordinatorData, key: str) -> float | None: + """Return the used GPU memory.""" + if getattr(data.gpu, f"{key}_memory_used") is not None: + return round(getattr(data.gpu, f"{key}_memory_used") / 10**3, 2) + return None + + +def gpu_memory_used_percentage( + data: SystemBridgeCoordinatorData, key: str +) -> float | None: + """Return the used GPU memory percentage.""" + if ( + getattr(data.gpu, f"{key}_memory_used") is not None + and getattr(data.gpu, f"{key}_memory_total") is not None + ): + return round( + getattr(data.gpu, f"{key}_memory_used") + / getattr(data.gpu, f"{key}_memory_total") + * 100, + 2, + ) + return None + + +def memory_free(data: SystemBridgeCoordinatorData) -> float | None: + """Return the free memory.""" + if data.memory.virtual_free is not None: + return round(data.memory.virtual_free / 1000**3, 2) + return None + + +def memory_used(data: SystemBridgeCoordinatorData) -> float | None: + """Return the used memory.""" + if data.memory.virtual_used is not None: + return round(data.memory.virtual_used / 1000**3, 2) + return None + + BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( - SystemBridgeSensorEntityDescription( - key="bios_version", - name="BIOS Version", - entity_registry_enabled_default=False, - icon="mdi:chip", - value=lambda bridge: bridge.system.bios.version, - ), SystemBridgeSensorEntityDescription( key="cpu_speed", name="CPU Speed", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=FREQUENCY_GIGAHERTZ, icon="mdi:speedometer", - value=lambda bridge: bridge.cpu.currentSpeed.avg, + value=cpu_speed, ), SystemBridgeSensorEntityDescription( key="cpu_temperature", @@ -73,7 +140,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, - value=lambda bridge: bridge.cpu.temperature.main, + value=lambda data: data.cpu.temperature, ), SystemBridgeSensorEntityDescription( key="cpu_voltage", @@ -82,21 +149,14 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - value=lambda bridge: bridge.cpu.cpu.voltage, - ), - SystemBridgeSensorEntityDescription( - key="displays_connected", - name="Displays Connected", - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:monitor", - value=lambda bridge: len(bridge.display.displays), + value=lambda data: data.cpu.voltage, ), SystemBridgeSensorEntityDescription( key="kernel", name="Kernel", state_class=SensorStateClass.MEASUREMENT, icon="mdi:devices", - value=lambda bridge: bridge.os.kernel, + value=lambda data: data.system.platform, ), SystemBridgeSensorEntityDescription( key="memory_free", @@ -104,7 +164,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=DATA_GIGABYTES, icon="mdi:memory", - value=lambda bridge: round(bridge.memory.free / 1000**3, 2), + value=memory_free, ), SystemBridgeSensorEntityDescription( key="memory_used_percentage", @@ -112,7 +172,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:memory", - value=lambda bridge: round((bridge.memory.used / bridge.memory.total) * 100, 2), + value=lambda data: data.memory.virtual_percent, ), SystemBridgeSensorEntityDescription( key="memory_used", @@ -121,14 +181,14 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=DATA_GIGABYTES, icon="mdi:memory", - value=lambda bridge: round(bridge.memory.used / 1000**3, 2), + value=memory_used, ), SystemBridgeSensorEntityDescription( key="os", name="Operating System", state_class=SensorStateClass.MEASUREMENT, icon="mdi:devices", - value=lambda bridge: f"{bridge.os.distro} {bridge.os.release}", + value=lambda data: f"{data.system.platform} {data.system.platform_version}", ), SystemBridgeSensorEntityDescription( key="processes_load", @@ -136,46 +196,19 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:percent", - value=lambda bridge: round(bridge.processes.load.currentLoad, 2), - ), - SystemBridgeSensorEntityDescription( - key="processes_load_idle", - name="Idle Load", - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - icon="mdi:percent", - value=lambda bridge: round(bridge.processes.load.currentLoadIdle, 2), - ), - SystemBridgeSensorEntityDescription( - key="processes_load_system", - name="System Load", - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - icon="mdi:percent", - value=lambda bridge: round(bridge.processes.load.currentLoadSystem, 2), - ), - SystemBridgeSensorEntityDescription( - key="processes_load_user", - name="User Load", - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - icon="mdi:percent", - value=lambda bridge: round(bridge.processes.load.currentLoadUser, 2), + value=lambda data: data.cpu.usage, ), SystemBridgeSensorEntityDescription( key="version", name="Version", icon="mdi:counter", - value=lambda bridge: bridge.information.version, + value=lambda data: data.system.version, ), SystemBridgeSensorEntityDescription( key="version_latest", name="Latest Version", icon="mdi:counter", - value=lambda bridge: bridge.information.updates.version.new, + value=lambda data: data.system.version_latest, ), ) @@ -186,238 +219,270 @@ BATTERY_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, - value=lambda bridge: bridge.battery.percent, + value=lambda data: data.battery.percentage, ), SystemBridgeSensorEntityDescription( key="battery_time_remaining", name="Battery Time Remaining", device_class=SensorDeviceClass.TIMESTAMP, state_class=SensorStateClass.MEASUREMENT, - value=lambda bridge: str( - datetime.now() + timedelta(minutes=bridge.battery.timeRemaining) - ), + value=battery_time_remaining, ), ) async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up System Bridge sensor based on a config entry.""" coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] entities = [] for description in BASE_SENSOR_TYPES: - entities.append(SystemBridgeSensor(coordinator, description)) + entities.append( + SystemBridgeSensor(coordinator, description, entry.data[CONF_PORT]) + ) - for key, _ in coordinator.data.filesystem.fsSize.items(): - uid = key.replace(":", "") + for partition in coordinator.data.disk.partitions: entities.append( SystemBridgeSensor( coordinator, SystemBridgeSensorEntityDescription( - key=f"filesystem_{uid}", - name=f"{key} Space Used", + key=f"filesystem_{partition.replace(':', '')}", + name=f"{partition} Space Used", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:harddisk", - value=lambda bridge, i=key: round( - bridge.filesystem.fsSize[i]["use"], 2 + value=lambda data, p=partition: getattr( + data.disk, f"usage_{p}_percent" ), ), + entry.data[CONF_PORT], ) ) - if coordinator.data.battery.hasBattery: + if ( + coordinator.data.battery + and coordinator.data.battery.percentage + and coordinator.data.battery.percentage > -1 + ): for description in BATTERY_SENSOR_TYPES: - entities.append(SystemBridgeSensor(coordinator, description)) + entities.append( + SystemBridgeSensor(coordinator, description, entry.data[CONF_PORT]) + ) - for index, _ in enumerate(coordinator.data.display.displays): - name = index + 1 + displays = [] + for display in coordinator.data.display.displays: + displays.append( + { + "key": display, + "name": getattr(coordinator.data.display, f"{display}_name").replace( + "Display ", "" + ), + }, + ) + display_count = len(displays) + + entities.append( + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key="displays_connected", + name="Displays Connected", + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:monitor", + value=lambda _, count=display_count: count, + ), + entry.data[CONF_PORT], + ) + ) + + for _, display in enumerate(displays): entities = [ *entities, SystemBridgeSensor( coordinator, SystemBridgeSensorEntityDescription( - key=f"display_{name}_resolution_x", - name=f"Display {name} Resolution X", + key=f"display_{display['name']}_resolution_x", + name=f"Display {display['name']} Resolution X", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PIXELS, icon="mdi:monitor", - value=lambda bridge, i=index: bridge.display.displays[ - i - ].resolutionX, + value=lambda data, k=display["key"]: getattr( + data.display, f"{k}_resolution_horizontal" + ), ), + entry.data[CONF_PORT], ), SystemBridgeSensor( coordinator, SystemBridgeSensorEntityDescription( - key=f"display_{name}_resolution_y", - name=f"Display {name} Resolution Y", + key=f"display_{display['name']}_resolution_y", + name=f"Display {display['name']} Resolution Y", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PIXELS, icon="mdi:monitor", - value=lambda bridge, i=index: bridge.display.displays[ - i - ].resolutionY, + value=lambda data, k=display["key"]: getattr( + data.display, f"{k}_resolution_vertical" + ), ), + entry.data[CONF_PORT], ), SystemBridgeSensor( coordinator, SystemBridgeSensorEntityDescription( - key=f"display_{name}_refresh_rate", - name=f"Display {name} Refresh Rate", + key=f"display_{display['name']}_refresh_rate", + name=f"Display {display['name']} Refresh Rate", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=FREQUENCY_HERTZ, icon="mdi:monitor", - value=lambda bridge, i=index: bridge.display.displays[ - i - ].currentRefreshRate, + value=lambda data, k=display["key"]: getattr( + data.display, f"{k}_refresh_rate" + ), ), + entry.data[CONF_PORT], ), ] - for index, _ in enumerate(coordinator.data.graphics.controllers): - if coordinator.data.graphics.controllers[index].name is not None: - # Remove vendor from name - name = ( - coordinator.data.graphics.controllers[index] - .name.replace(coordinator.data.graphics.controllers[index].vendor, "") - .strip() - ) - entities = [ - *entities, - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"gpu_{index}_core_clock_speed", - name=f"{name} Clock Speed", - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=FREQUENCY_MEGAHERTZ, - icon="mdi:speedometer", - value=lambda bridge, i=index: bridge.graphics.controllers[ - i - ].clockCore, - ), - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"gpu_{index}_memory_clock_speed", - name=f"{name} Memory Clock Speed", - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=FREQUENCY_MEGAHERTZ, - icon="mdi:speedometer", - value=lambda bridge, i=index: bridge.graphics.controllers[ - i - ].clockMemory, - ), - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"gpu_{index}_memory_free", - name=f"{name} Memory Free", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=DATA_GIGABYTES, - icon="mdi:memory", - value=lambda bridge, i=index: round( - bridge.graphics.controllers[i].memoryFree / 10**3, 2 - ), - ), - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"gpu_{index}_memory_used_percentage", - name=f"{name} Memory Used %", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - icon="mdi:memory", - value=lambda bridge, i=index: round( - ( - bridge.graphics.controllers[i].memoryUsed - / bridge.graphics.controllers[i].memoryTotal - ) - * 100, - 2, - ), - ), - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"gpu_{index}_memory_used", - name=f"{name} Memory Used", - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=DATA_GIGABYTES, - icon="mdi:memory", - value=lambda bridge, i=index: round( - bridge.graphics.controllers[i].memoryUsed / 10**3, 2 - ), - ), - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"gpu_{index}_fan_speed", - name=f"{name} Fan Speed", - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - icon="mdi:fan", - value=lambda bridge, i=index: bridge.graphics.controllers[ - i - ].fanSpeed, - ), - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"gpu_{index}_power_usage", - name=f"{name} Power Usage", - entity_registry_enabled_default=False, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=POWER_WATT, - value=lambda bridge, i=index: bridge.graphics.controllers[ - i - ].powerDraw, - ), - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"gpu_{index}_temperature", - name=f"{name} Temperature", - entity_registry_enabled_default=False, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=TEMP_CELSIUS, - value=lambda bridge, i=index: bridge.graphics.controllers[ - i - ].temperatureGpu, - ), - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"gpu_{index}_usage_percentage", - name=f"{name} Usage %", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - icon="mdi:percent", - value=lambda bridge, i=index: bridge.graphics.controllers[ - i - ].utilizationGpu, - ), - ), - ] + gpus = [] + for gpu in coordinator.data.gpu.gpus: + gpus.append( + { + "key": gpu, + "name": getattr(coordinator.data.gpu, f"{gpu}_name"), + }, + ) - for index, _ in enumerate(coordinator.data.processes.load.cpus): + for index, gpu in enumerate(gpus): + entities = [ + *entities, + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"gpu_{index}_core_clock_speed", + name=f"{gpu['name']} Clock Speed", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=FREQUENCY_MEGAHERTZ, + icon="mdi:speedometer", + value=lambda data, k=gpu["key"]: gpu_core_clock_speed(data, k), + ), + entry.data[CONF_PORT], + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"gpu_{index}_memory_clock_speed", + name=f"{gpu['name']} Memory Clock Speed", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=FREQUENCY_MEGAHERTZ, + icon="mdi:speedometer", + value=lambda data, k=gpu["key"]: gpu_memory_clock_speed(data, k), + ), + entry.data[CONF_PORT], + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"gpu_{index}_memory_free", + name=f"{gpu['name']} Memory Free", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=DATA_GIGABYTES, + icon="mdi:memory", + value=lambda data, k=gpu["key"]: gpu_memory_free(data, k), + ), + entry.data[CONF_PORT], + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"gpu_{index}_memory_used_percentage", + name=f"{gpu['name']} Memory Used %", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + icon="mdi:memory", + value=lambda data, k=gpu["key"]: gpu_memory_used_percentage( + data, k + ), + ), + entry.data[CONF_PORT], + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"gpu_{index}_memory_used", + name=f"{gpu['name']} Memory Used", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=DATA_GIGABYTES, + icon="mdi:memory", + value=lambda data, k=gpu["key"]: gpu_memory_used(data, k), + ), + entry.data[CONF_PORT], + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"gpu_{index}_fan_speed", + name=f"{gpu['name']} Fan Speed", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=RPM, + icon="mdi:fan", + value=lambda data, k=gpu["key"]: getattr( + data.gpu, f"{k}_fan_speed" + ), + ), + entry.data[CONF_PORT], + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"gpu_{index}_power_usage", + name=f"{gpu['name']} Power Usage", + entity_registry_enabled_default=False, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=POWER_WATT, + value=lambda data, k=gpu["key"]: getattr(data.gpu, f"{k}_power"), + ), + entry.data[CONF_PORT], + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"gpu_{index}_temperature", + name=f"{gpu['name']} Temperature", + entity_registry_enabled_default=False, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + value=lambda data, k=gpu["key"]: getattr( + data.gpu, f"{k}_temperature" + ), + ), + entry.data[CONF_PORT], + ), + SystemBridgeSensor( + coordinator, + SystemBridgeSensorEntityDescription( + key=f"gpu_{index}_usage_percentage", + name=f"{gpu['name']} Usage %", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=PERCENTAGE, + icon="mdi:percent", + value=lambda data, k=gpu["key"]: getattr( + data.gpu, f"{k}_core_load" + ), + ), + entry.data[CONF_PORT], + ), + ] + + for index in range(coordinator.data.cpu.count): entities = [ *entities, SystemBridgeSensor( @@ -429,52 +494,9 @@ async def async_setup_entry( state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, icon="mdi:percent", - value=lambda bridge, index=index: round( - bridge.processes.load.cpus[index].load, 2 - ), - ), - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"processes_load_cpu_{index}_idle", - name=f"Idle Load CPU {index}", - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - icon="mdi:percent", - value=lambda bridge, index=index: round( - bridge.processes.load.cpus[index].loadIdle, 2 - ), - ), - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"processes_load_cpu_{index}_system", - name=f"System Load CPU {index}", - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - icon="mdi:percent", - value=lambda bridge, index=index: round( - bridge.processes.load.cpus[index].loadSystem, 2 - ), - ), - ), - SystemBridgeSensor( - coordinator, - SystemBridgeSensorEntityDescription( - key=f"processes_load_cpu_{index}_user", - name=f"User Load CPU {index}", - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - icon="mdi:percent", - value=lambda bridge, index=index: round( - bridge.processes.load.cpus[index].loadUser, 2 - ), + value=lambda data, k=index: getattr(data.cpu, f"usage_{k}"), ), + entry.data[CONF_PORT], ), ] @@ -490,10 +512,12 @@ class SystemBridgeSensor(SystemBridgeDeviceEntity, SensorEntity): self, coordinator: SystemBridgeDataUpdateCoordinator, description: SystemBridgeSensorEntityDescription, + api_port: int, ) -> None: """Initialize.""" super().__init__( coordinator, + api_port, description.key, description.name, ) @@ -502,8 +526,7 @@ class SystemBridgeSensor(SystemBridgeDeviceEntity, SensorEntity): @property def native_value(self) -> StateType: """Return the state.""" - bridge: Bridge = self.coordinator.data try: - return cast(StateType, self.entity_description.value(bridge)) + return cast(StateType, self.entity_description.value(self.coordinator.data)) except TypeError: return None diff --git a/homeassistant/components/system_bridge/services.yaml b/homeassistant/components/system_bridge/services.yaml index aff0094501e..d33235ffba4 100644 --- a/homeassistant/components/system_bridge/services.yaml +++ b/homeassistant/components/system_bridge/services.yaml @@ -1,32 +1,6 @@ -send_command: - name: Send Command - description: Sends a command to the server to run. - fields: - bridge: - name: Bridge - description: The server to send the command to. - required: true - selector: - device: - integration: system_bridge - command: - name: Command - description: Command to send to the server. - required: true - example: "echo" - selector: - text: - arguments: - name: Arguments - description: Arguments to send to the server. - required: false - default: "" - example: "hello" - selector: - text: -open: - name: Open Path/URL - description: Open a URL or file on the server using the default application. +open_path: + name: Open Path + description: Open a file on the server using the default application. fields: bridge: name: Bridge @@ -36,8 +10,26 @@ open: device: integration: system_bridge path: - name: Path/URL - description: Path/URL to open. + name: Path + description: Path to open. + required: true + example: "C:\\test\\image.png" + selector: + text: +open_url: + name: Open URL + description: Open a URL on the server using the default application. + fields: + bridge: + name: Bridge + description: The server to talk to. + required: true + selector: + device: + integration: system_bridge + url: + name: URL + description: URL to open. required: true example: "https://www.home-assistant.io" selector: @@ -60,16 +52,6 @@ send_keypress: example: "audio_play" selector: text: - modifiers: - name: Modifiers - description: "List of modifier(s). Accepts alt, command/win, control, and shift." - required: false - default: "" - example: - - "control" - - "shift" - selector: - text: send_text: name: Send Keyboard Text description: Sends text for the server to type. diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 415e2746c6d..692132c9a75 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -370,7 +370,7 @@ ZEROCONF = { "name": "smappee50*" } ], - "_system-bridge._udp.local.": [ + "_system-bridge._tcp.local.": [ { "domain": "system_bridge" } diff --git a/requirements_all.txt b/requirements_all.txt index 603ad612ae3..d2082f7c965 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2271,7 +2271,7 @@ swisshydrodata==0.1.0 synology-srm==0.2.0 # homeassistant.components.system_bridge -systembridge==2.3.1 +systembridgeconnector==3.1.3 # homeassistant.components.tailscale tailscale==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a1fa8d0fdc3..154d64e4cf6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1498,7 +1498,7 @@ sunwatcher==0.2.1 surepy==0.7.2 # homeassistant.components.system_bridge -systembridge==2.3.1 +systembridgeconnector==3.1.3 # homeassistant.components.tailscale tailscale==0.2.0 diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py index 94d116bbd36..515146bc16c 100644 --- a/tests/components/system_bridge/test_config_flow.py +++ b/tests/components/system_bridge/test_config_flow.py @@ -1,13 +1,28 @@ """Test the System Bridge config flow.""" +import asyncio from unittest.mock import patch -from aiohttp.client_exceptions import ClientConnectionError -from systembridge.exceptions import BridgeAuthenticationException +from systembridgeconnector.const import ( + EVENT_DATA, + EVENT_MESSAGE, + EVENT_MODULE, + EVENT_SUBTYPE, + EVENT_TYPE, + SUBTYPE_BAD_API_KEY, + TYPE_DATA_UPDATE, + TYPE_ERROR, +) +from systembridgeconnector.exceptions import ( + AuthenticationException, + ConnectionClosedException, + ConnectionErrorException, +) from homeassistant import config_entries, data_entry_flow from homeassistant.components import zeroconf from homeassistant.components.system_bridge.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -29,7 +44,7 @@ FIXTURE_ZEROCONF_INPUT = { } FIXTURE_ZEROCONF = zeroconf.ZeroconfServiceInfo( - host="1.1.1.1", + host="test-bridge", addresses=["1.1.1.1"], port=9170, hostname="test-bridge.local.", @@ -58,37 +73,40 @@ FIXTURE_ZEROCONF_BAD = zeroconf.ZeroconfServiceInfo( }, ) - -FIXTURE_INFORMATION = { - "address": "http://test-bridge:9170", - "apiPort": 9170, - "fqdn": "test-bridge", - "host": "test-bridge", - "ip": "1.1.1.1", - "mac": FIXTURE_MAC_ADDRESS, - "updates": { - "available": False, - "newer": False, - "url": "https://github.com/timmo001/system-bridge/releases/tag/v2.3.2", - "version": {"current": "2.3.2", "new": "2.3.2"}, +FIXTURE_DATA_SYSTEM = { + EVENT_TYPE: TYPE_DATA_UPDATE, + EVENT_MESSAGE: "Data changed", + EVENT_MODULE: "system", + EVENT_DATA: { + "uuid": FIXTURE_UUID, }, - "uuid": FIXTURE_UUID, - "version": "2.3.2", - "websocketAddress": "ws://test-bridge:9172", - "websocketPort": 9172, +} + +FIXTURE_DATA_SYSTEM_BAD = { + EVENT_TYPE: TYPE_DATA_UPDATE, + EVENT_MESSAGE: "Data changed", + EVENT_MODULE: "system", + EVENT_DATA: {}, +} + +FIXTURE_DATA_AUTH_ERROR = { + EVENT_TYPE: TYPE_ERROR, + EVENT_SUBTYPE: SUBTYPE_BAD_API_KEY, + EVENT_MESSAGE: "Invalid api-key", } -FIXTURE_BASE_URL = ( - f"http://{FIXTURE_USER_INPUT[CONF_HOST]}:{FIXTURE_USER_INPUT[CONF_PORT]}" -) +async def test_show_user_form(hass: HomeAssistant) -> None: + """Test that the setup form is served.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) -FIXTURE_ZEROCONF_BASE_URL = f"http://{FIXTURE_ZEROCONF.host}:{FIXTURE_ZEROCONF.port}" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" -async def test_user_flow( - hass, aiohttp_client, aioclient_mock, current_request_with_host -) -> None: +async def test_user_flow(hass: HomeAssistant) -> None: """Test full user flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -97,20 +115,19 @@ async def test_user_flow( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] is None - aioclient_mock.get( - f"{FIXTURE_BASE_URL}/information", - headers={"Content-Type": "application/json"}, - json=FIXTURE_INFORMATION, - ) - with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" + ), patch("systembridgeconnector.websocket_client.WebSocketClient.get_data"), patch( + "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + return_value=FIXTURE_DATA_SYSTEM, + ), patch( "homeassistant.components.system_bridge.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["title"] == "test-bridge" @@ -118,34 +135,7 @@ async def test_user_flow( assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_invalid_auth( - hass, aiohttp_client, aioclient_mock, current_request_with_host -) -> None: - """Test we handle invalid auth.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] is None - - aioclient_mock.get( - f"{FIXTURE_BASE_URL}/information", exc=BridgeAuthenticationException - ) - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], FIXTURE_USER_INPUT - ) - await hass.async_block_till_done() - - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "invalid_auth"} - - -async def test_form_cannot_connect( - hass, aiohttp_client, aioclient_mock, current_request_with_host -) -> None: +async def test_form_cannot_connect(hass: HomeAssistant) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -154,11 +144,13 @@ async def test_form_cannot_connect( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] is None - aioclient_mock.get(f"{FIXTURE_BASE_URL}/information", exc=ClientConnectionError) - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], FIXTURE_USER_INPUT - ) + with patch( + "systembridgeconnector.websocket_client.WebSocketClient.connect", + side_effect=ConnectionErrorException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_USER_INPUT + ) await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -166,10 +158,8 @@ async def test_form_cannot_connect( assert result2["errors"] == {"base": "cannot_connect"} -async def test_form_unknown_error( - hass, aiohttp_client, aioclient_mock, current_request_with_host -) -> None: - """Test we handle unknown error.""" +async def test_form_connection_closed_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle connection closed cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -177,23 +167,123 @@ async def test_form_unknown_error( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] is None - with patch( - "homeassistant.components.system_bridge.config_flow.Bridge.async_get_information", - side_effect=Exception("Boom"), + with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data" + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + side_effect=ConnectionClosedException, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT ) - await hass.async_block_till_done() + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_timeout_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle timeout cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + + with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data" + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + side_effect=asyncio.TimeoutError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_USER_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + + with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data" + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + side_effect=AuthenticationException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_USER_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_uuid_error(hass: HomeAssistant) -> None: + """Test we handle error from bad uuid.""" + 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"] is None + + with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data" + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + return_value=FIXTURE_DATA_SYSTEM_BAD, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_USER_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unknown_error(hass: HomeAssistant) -> None: + """Test we handle unknown errors.""" + 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"] is None + + with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data" + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_USER_INPUT + ) + await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} -async def test_reauth_authorization_error( - hass, aiohttp_client, aioclient_mock, current_request_with_host -) -> None: +async def test_reauth_authorization_error(hass: HomeAssistant) -> None: """Test we show user form on authorization error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT @@ -202,13 +292,15 @@ async def test_reauth_authorization_error( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "authenticate" - aioclient_mock.get( - f"{FIXTURE_BASE_URL}/information", exc=BridgeAuthenticationException - ) - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], FIXTURE_AUTH_INPUT - ) + with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data" + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + side_effect=AuthenticationException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_AUTH_INPUT + ) await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -216,9 +308,7 @@ async def test_reauth_authorization_error( assert result2["errors"] == {"base": "invalid_auth"} -async def test_reauth_connection_error( - hass, aiohttp_client, aioclient_mock, current_request_with_host -) -> None: +async def test_reauth_connection_error(hass: HomeAssistant) -> None: """Test we show user form on connection error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT @@ -227,11 +317,13 @@ async def test_reauth_connection_error( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "authenticate" - aioclient_mock.get(f"{FIXTURE_BASE_URL}/information", exc=ClientConnectionError) - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], FIXTURE_AUTH_INPUT - ) + with patch( + "systembridgeconnector.websocket_client.WebSocketClient.connect", + side_effect=ConnectionErrorException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_AUTH_INPUT + ) await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -239,9 +331,32 @@ async def test_reauth_connection_error( assert result2["errors"] == {"base": "cannot_connect"} -async def test_reauth_flow( - hass, aiohttp_client, aioclient_mock, current_request_with_host -) -> None: +async def test_reauth_connection_closed_error(hass: HomeAssistant) -> None: + """Test we show user form on connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "authenticate" + + with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data" + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + side_effect=ConnectionClosedException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_AUTH_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "authenticate" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_reauth_flow(hass: HomeAssistant) -> None: """Test reauth flow.""" mock_config = MockConfigEntry( domain=DOMAIN, unique_id=FIXTURE_UUID, data=FIXTURE_USER_INPUT @@ -255,20 +370,19 @@ async def test_reauth_flow( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "authenticate" - aioclient_mock.get( - f"{FIXTURE_BASE_URL}/information", - headers={"Content-Type": "application/json"}, - json=FIXTURE_INFORMATION, - ) - - with patch( + with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data" + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + return_value=FIXTURE_DATA_SYSTEM, + ), patch( "homeassistant.components.system_bridge.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_AUTH_INPUT ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result2["reason"] == "reauth_successful" @@ -276,9 +390,7 @@ async def test_reauth_flow( assert len(mock_setup_entry.mock_calls) == 1 -async def test_zeroconf_flow( - hass, aiohttp_client, aioclient_mock, current_request_with_host -) -> None: +async def test_zeroconf_flow(hass: HomeAssistant) -> None: """Test zeroconf flow.""" result = await hass.config_entries.flow.async_init( @@ -290,30 +402,27 @@ async def test_zeroconf_flow( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert not result["errors"] - aioclient_mock.get( - f"{FIXTURE_ZEROCONF_BASE_URL}/information", - headers={"Content-Type": "application/json"}, - json=FIXTURE_INFORMATION, - ) - - with patch( + with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data" + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + return_value=FIXTURE_DATA_SYSTEM, + ), patch( "homeassistant.components.system_bridge.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_AUTH_INPUT ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "test-bridge" + assert result2["title"] == "1.1.1.1" assert result2["data"] == FIXTURE_ZEROCONF_INPUT assert len(mock_setup_entry.mock_calls) == 1 -async def test_zeroconf_cannot_connect( - hass, aiohttp_client, aioclient_mock, current_request_with_host -) -> None: +async def test_zeroconf_cannot_connect(hass: HomeAssistant) -> None: """Test zeroconf cannot connect flow.""" result = await hass.config_entries.flow.async_init( @@ -325,13 +434,13 @@ async def test_zeroconf_cannot_connect( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert not result["errors"] - aioclient_mock.get( - f"{FIXTURE_ZEROCONF_BASE_URL}/information", exc=ClientConnectionError - ) - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], FIXTURE_AUTH_INPUT - ) + with patch( + "systembridgeconnector.websocket_client.WebSocketClient.connect", + side_effect=ConnectionErrorException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_AUTH_INPUT + ) await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -339,9 +448,7 @@ async def test_zeroconf_cannot_connect( assert result2["errors"] == {"base": "cannot_connect"} -async def test_zeroconf_bad_zeroconf_info( - hass, aiohttp_client, aioclient_mock, current_request_with_host -) -> None: +async def test_zeroconf_bad_zeroconf_info(hass: HomeAssistant) -> None: """Test zeroconf cannot connect flow.""" result = await hass.config_entries.flow.async_init( From 321394d3e2403454e63eb58174906697af8760e3 Mon Sep 17 00:00:00 2001 From: Arne Mauer Date: Thu, 2 Jun 2022 00:00:58 +0200 Subject: [PATCH 1167/3516] Add Particulate Matter 2.5 of ZCL concentration clusters to ZHA component (#72826) * Add Particulate Matter 2.5 of ZCL concentration clusters to ZHA component * Fixed black and flake8 test --- .../components/zha/core/channels/measurement.py | 12 ++++++++++++ homeassistant/components/zha/sensor.py | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py index 093c04245c4..7368309cf99 100644 --- a/homeassistant/components/zha/core/channels/measurement.py +++ b/homeassistant/components/zha/core/channels/measurement.py @@ -128,6 +128,18 @@ class CarbonDioxideConcentration(ZigbeeChannel): ] +@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.PM25.cluster_id) +class PM25(ZigbeeChannel): + """Particulate Matter 2.5 microns or less measurement channel.""" + + REPORT_CONFIG = [ + { + "attr": "measured_value", + "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 0.1), + } + ] + + @registries.ZIGBEE_CHANNEL_REGISTRY.register( measurement.FormaldehydeConcentration.cluster_id ) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 36ca873188f..cdc37876889 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -607,6 +607,17 @@ class PPBVOCLevel(Sensor): _unit = CONCENTRATION_PARTS_PER_BILLION +@MULTI_MATCH(channel_names="pm25") +class PM25(Sensor): + """Particulate Matter 2.5 microns or less sensor.""" + + SENSOR_ATTR = "measured_value" + _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _decimals = 0 + _multiplier = 1 + _unit = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + + @MULTI_MATCH(channel_names="formaldehyde_concentration") class FormaldehydeConcentration(Sensor): """Formaldehyde Concentration sensor.""" From d1a8f1ae40d652c78f39d5e1471b1b915a77339a Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 2 Jun 2022 00:04:14 +0200 Subject: [PATCH 1168/3516] Update frontend to 20220601.0 (#72855) --- 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 d9e80b4eff8..7d07bbd543c 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220531.0"], + "requirements": ["home-assistant-frontend==20220601.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ad2539233f4..076b58b5185 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220531.0 +home-assistant-frontend==20220601.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index d2082f7c965..e50da9b1758 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -822,7 +822,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220531.0 +home-assistant-frontend==20220601.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 154d64e4cf6..d3fefd79973 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -589,7 +589,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220531.0 +home-assistant-frontend==20220601.0 # homeassistant.components.home_connect homeconnect==0.7.0 From fe5fe148fa8e56b3d8cbd2db36d2c5676cf0258a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 2 Jun 2022 00:06:19 +0200 Subject: [PATCH 1169/3516] Add mypy checks to pylint plugins (#72821) --- .github/workflows/ci.yaml | 2 +- .pre-commit-config.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b8d81856f3c..30756ddb501 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -689,7 +689,7 @@ jobs: run: | . venv/bin/activate python --version - mypy homeassistant + mypy homeassistant pylint - name: Run mypy (partially) if: needs.changes.outputs.test_full_suite == 'false' shell: bash diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ff00ce07e0c..1104ecf07e9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -93,7 +93,7 @@ repos: language: script types: [python] require_serial: true - files: ^homeassistant/.+\.py$ + files: ^(homeassistant|pylint)/.+\.py$ - id: pylint name: pylint entry: script/run-in-env.sh pylint -j 0 From 77467155905d5633349085097e21592d7ef071f1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 2 Jun 2022 00:27:51 +0000 Subject: [PATCH 1170/3516] [ci skip] Translation update --- .../accuweather/translations/es.json | 3 + .../aladdin_connect/translations/es.json | 1 + .../components/apple_tv/translations/es.json | 1 + .../components/asuswrt/translations/es.json | 3 +- .../aussie_broadband/translations/es.json | 12 ++- .../components/bsblan/translations/es.json | 3 +- .../components/coinbase/translations/es.json | 1 + .../components/deluge/translations/es.json | 6 +- .../components/denonavr/translations/es.json | 3 + .../derivative/translations/es.json | 9 ++ .../components/discord/translations/es.json | 8 +- .../components/dlna_dms/translations/es.json | 11 ++- .../components/doorbird/translations/es.json | 3 + .../components/elkm1/translations/es.json | 1 + .../components/fibaro/translations/es.json | 8 +- .../components/filesize/translations/es.json | 16 +++- .../components/fritz/translations/es.json | 6 +- .../components/generic/translations/es.json | 27 +++++- .../geocaching/translations/es.json | 1 + .../components/geofency/translations/es.json | 1 + .../components/github/translations/es.json | 3 + .../components/google/translations/es.json | 20 +++++ .../components/group/translations/es.json | 72 +++++++++++----- .../here_travel_time/translations/es.json | 82 +++++++++++++++++++ .../components/homekit/translations/es.json | 9 +- .../translations/select.es.json | 1 + .../components/honeywell/translations/es.json | 6 +- .../components/ialarm_xr/translations/es.json | 1 + .../components/ifttt/translations/es.json | 1 + .../integration/translations/es.json | 5 +- .../intellifire/translations/es.json | 3 +- .../components/iss/translations/es.json | 3 +- .../components/isy994/translations/es.json | 1 + .../kaleidescape/translations/es.json | 5 ++ .../components/knx/translations/es.json | 32 ++++++-- .../components/laundrify/translations/es.json | 25 ++++++ .../litterrobot/translations/sensor.es.json | 16 +++- .../components/meater/translations/es.json | 10 ++- .../meteoclimatic/translations/es.json | 3 + .../components/min_max/translations/es.json | 2 + .../components/nanoleaf/translations/es.json | 5 +- .../components/onewire/translations/es.json | 6 +- .../components/plugwise/translations/es.json | 5 +- .../components/powerwall/translations/es.json | 5 +- .../components/ps4/translations/es.json | 6 ++ .../components/recorder/translations/es.json | 4 +- .../rtsp_to_webrtc/translations/es.json | 2 + .../components/samsungtv/translations/es.json | 3 + .../components/season/translations/es.json | 14 ++++ .../components/sense/translations/es.json | 9 +- .../components/senseme/translations/es.json | 3 +- .../components/sensibo/translations/es.json | 3 +- .../components/shelly/translations/es.json | 1 + .../simplisafe/translations/es.json | 1 + .../components/siren/translations/es.json | 3 + .../components/sleepiq/translations/es.json | 1 + .../components/solax/translations/es.json | 2 + .../components/sql/translations/es.json | 5 +- .../steam_online/translations/es.json | 7 +- .../components/subaru/translations/es.json | 5 +- .../components/sun/translations/es.json | 10 +++ .../switch_as_x/translations/es.json | 3 +- .../tankerkoenig/translations/ca.json | 8 +- .../tankerkoenig/translations/de.json | 8 +- .../tankerkoenig/translations/es.json | 20 ++++- .../tankerkoenig/translations/fr.json | 8 +- .../tankerkoenig/translations/ja.json | 8 +- .../tankerkoenig/translations/no.json | 8 +- .../tankerkoenig/translations/zh-Hant.json | 8 +- .../components/tautulli/translations/es.json | 9 +- .../components/threshold/translations/es.json | 20 ++++- .../components/tod/translations/es.json | 6 +- .../tomorrowio/translations/es.json | 5 +- .../tomorrowio/translations/sensor.es.json | 11 +++ .../components/traccar/translations/es.json | 1 + .../trafikverket_ferry/translations/es.json | 6 +- .../trafikverket_train/translations/es.json | 19 ++++- .../tuya/translations/select.es.json | 10 ++- .../ukraine_alarm/translations/es.json | 8 ++ .../unifiprotect/translations/es.json | 7 +- .../components/uptime/translations/es.json | 13 +++ .../uptimerobot/translations/sensor.es.json | 2 + .../utility_meter/translations/es.json | 8 +- .../components/vera/translations/es.json | 3 + .../components/vulcan/translations/es.json | 37 +++++++-- .../components/webostv/translations/es.json | 1 + .../components/whois/translations/es.json | 2 + .../translations/select.pt.json | 22 +++++ .../components/yolink/translations/es.json | 25 ++++++ 89 files changed, 716 insertions(+), 94 deletions(-) create mode 100644 homeassistant/components/here_travel_time/translations/es.json create mode 100644 homeassistant/components/laundrify/translations/es.json create mode 100644 homeassistant/components/season/translations/es.json create mode 100644 homeassistant/components/siren/translations/es.json create mode 100644 homeassistant/components/uptime/translations/es.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.pt.json create mode 100644 homeassistant/components/yolink/translations/es.json diff --git a/homeassistant/components/accuweather/translations/es.json b/homeassistant/components/accuweather/translations/es.json index 0c3d5560d67..ef91348a727 100644 --- a/homeassistant/components/accuweather/translations/es.json +++ b/homeassistant/components/accuweather/translations/es.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, + "create_entry": { + "default": "Algunos sensores no est\u00e1n habilitados de forma predeterminada. Puede habilitarlos en el registro de la entidad despu\u00e9s de la configuraci\u00f3n de la integraci\u00f3n.\n El pron\u00f3stico del tiempo no est\u00e1 habilitado de forma predeterminada. Puedes habilitarlo en las opciones de integraci\u00f3n." + }, "error": { "cannot_connect": "No se pudo conectar", "invalid_api_key": "Clave API no v\u00e1lida", diff --git a/homeassistant/components/aladdin_connect/translations/es.json b/homeassistant/components/aladdin_connect/translations/es.json index 67e509e2626..ac10503ab3c 100644 --- a/homeassistant/components/aladdin_connect/translations/es.json +++ b/homeassistant/components/aladdin_connect/translations/es.json @@ -13,6 +13,7 @@ "data": { "password": "Contrase\u00f1a" }, + "description": "La integraci\u00f3n de Aladdin Connect necesita volver a autenticar su cuenta", "title": "Reautenticaci\u00f3n de la integraci\u00f3n" }, "user": { diff --git a/homeassistant/components/apple_tv/translations/es.json b/homeassistant/components/apple_tv/translations/es.json index 1a0ed773169..3fe2345d6e1 100644 --- a/homeassistant/components/apple_tv/translations/es.json +++ b/homeassistant/components/apple_tv/translations/es.json @@ -7,6 +7,7 @@ "device_did_not_pair": "No se ha intentado finalizar el proceso de emparejamiento desde el dispositivo.", "device_not_found": "No se ha encontrado el dispositivo durante la detecci\u00f3n, por favor, intente a\u00f1adirlo de nuevo.", "inconsistent_device": "No se encontraron los protocolos esperados durante el descubrimiento. Esto normalmente indica un problema con el DNS de multidifusi\u00f3n (Zeroconf). Por favor, intente a\u00f1adir el dispositivo de nuevo.", + "ipv6_not_supported": "IPv6 no est\u00e1 soportado.", "no_devices_found": "No se encontraron dispositivos en la red", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "setup_failed": "No se ha podido configurar el dispositivo.", diff --git a/homeassistant/components/asuswrt/translations/es.json b/homeassistant/components/asuswrt/translations/es.json index 9a2e0485aa7..a2e899ef113 100644 --- a/homeassistant/components/asuswrt/translations/es.json +++ b/homeassistant/components/asuswrt/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "invalid_unique_id": "No se pudo determinar ning\u00fan identificador \u00fanico v\u00e1lido del dispositivo" + "invalid_unique_id": "No se pudo determinar ning\u00fan identificador \u00fanico v\u00e1lido del dispositivo", + "no_unique_id": "Un dispositivo sin una identificaci\u00f3n \u00fanica v\u00e1lida ya est\u00e1 configurado. La configuraci\u00f3n de una instancia m\u00faltiple no es posible" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/aussie_broadband/translations/es.json b/homeassistant/components/aussie_broadband/translations/es.json index 1410af6ad76..497215525cb 100644 --- a/homeassistant/components/aussie_broadband/translations/es.json +++ b/homeassistant/components/aussie_broadband/translations/es.json @@ -1,12 +1,14 @@ { "config": { "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", "no_services_found": "No se han encontrado servicios para esta cuenta", "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" }, "error": { "cannot_connect": "Fall\u00f3 la conexi\u00f3n", - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" }, "step": { "reauth_confirm": { @@ -31,8 +33,16 @@ } }, "options": { + "abort": { + "cannot_connect": "Fallo en la conexi\u00f3n", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, "step": { "init": { + "data": { + "services": "Servicios" + }, "title": "Selecciona servicios" } } diff --git a/homeassistant/components/bsblan/translations/es.json b/homeassistant/components/bsblan/translations/es.json index 830752dd863..6e533b5916b 100644 --- a/homeassistant/components/bsblan/translations/es.json +++ b/homeassistant/components/bsblan/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "Fallo en la conexi\u00f3n" }, "error": { "cannot_connect": "No se pudo conectar" diff --git a/homeassistant/components/coinbase/translations/es.json b/homeassistant/components/coinbase/translations/es.json index 5454aca8ec6..7ecd0a753c9 100644 --- a/homeassistant/components/coinbase/translations/es.json +++ b/homeassistant/components/coinbase/translations/es.json @@ -23,6 +23,7 @@ }, "options": { "error": { + "currency_unavailable": "La API de Coinbase no proporciona uno o m\u00e1s de los saldos de divisas solicitados.", "exchange_rate_unavailable": "El API de Coinbase no proporciona alguno/s de los tipos de cambio que has solicitado.", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/deluge/translations/es.json b/homeassistant/components/deluge/translations/es.json index 7ba1b8c3bf9..1724791221f 100644 --- a/homeassistant/components/deluge/translations/es.json +++ b/homeassistant/components/deluge/translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado" + }, "error": { "cannot_connect": "Fall\u00f3 la conexi\u00f3n", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" @@ -12,7 +15,8 @@ "port": "Puerto", "username": "Usuario", "web_port": "Puerto web (para el servicio de visita)" - } + }, + "description": "Para poder usar esta integraci\u00f3n, debe habilitar la siguiente opci\u00f3n en la configuraci\u00f3n de diluvio: Daemon > Permitir controles remotos" } } } diff --git a/homeassistant/components/denonavr/translations/es.json b/homeassistant/components/denonavr/translations/es.json index 3cfe69aeac4..f5993f46cf7 100644 --- a/homeassistant/components/denonavr/translations/es.json +++ b/homeassistant/components/denonavr/translations/es.json @@ -25,6 +25,9 @@ "user": { "data": { "host": "Direcci\u00f3n IP" + }, + "data_description": { + "host": "D\u00e9jelo en blanco para usar el descubrimiento autom\u00e1tico" } } } diff --git a/homeassistant/components/derivative/translations/es.json b/homeassistant/components/derivative/translations/es.json index e6df75ba4d6..e9b0919f06f 100644 --- a/homeassistant/components/derivative/translations/es.json +++ b/homeassistant/components/derivative/translations/es.json @@ -4,11 +4,17 @@ "user": { "data": { "name": "Nombre", + "round": "Precisi\u00f3n", "source": "Sensor de entrada", "time_window": "Ventana de tiempo", "unit_prefix": "Prefijo m\u00e9trico", "unit_time": "Unidad de tiempo" }, + "data_description": { + "round": "Controla el n\u00famero de d\u00edgitos decimales en la salida.", + "time_window": "Si se establece, el valor del sensor es un promedio m\u00f3vil ponderado en el tiempo de las derivadas dentro de esta ventana.", + "unit_prefix": "La salida se escalar\u00e1 seg\u00fan el prefijo m\u00e9trico seleccionado y la unidad de tiempo de la derivada." + }, "description": "Crea un sensor que ama la derivada de otro sensor.", "title": "A\u00f1ade sensor derivativo" } @@ -20,11 +26,14 @@ "data": { "name": "Nombre", "round": "Precisi\u00f3n", + "source": "Sensor de entrada", + "time_window": "Ventana de tiempo", "unit_prefix": "Prefijo m\u00e9trico", "unit_time": "Unidad de tiempo" }, "data_description": { "round": "Controla el n\u00famero de d\u00edgitos decimales en la salida.", + "time_window": "Si se establece, el valor del sensor es una media m\u00f3vil ponderada en el tiempo de las derivadas dentro de esta ventana.", "unit_prefix": "a salida se escalar\u00e1 seg\u00fan el prefijo m\u00e9trico y la unidad de tiempo de la derivada seleccionados." } } diff --git a/homeassistant/components/discord/translations/es.json b/homeassistant/components/discord/translations/es.json index 768afb877f3..f4f7bd49fc5 100644 --- a/homeassistant/components/discord/translations/es.json +++ b/homeassistant/components/discord/translations/es.json @@ -1,10 +1,13 @@ { "config": { "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n" + "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" }, "step": { "reauth_confirm": { @@ -16,7 +19,8 @@ "user": { "data": { "api_token": "Token API" - } + }, + "description": "Consulte la documentaci\u00f3n sobre c\u00f3mo obtener su clave de bot de Discord. \n\n {url}" } } } diff --git a/homeassistant/components/dlna_dms/translations/es.json b/homeassistant/components/dlna_dms/translations/es.json index 1ce13967ea5..f9d08f90451 100644 --- a/homeassistant/components/dlna_dms/translations/es.json +++ b/homeassistant/components/dlna_dms/translations/es.json @@ -1,16 +1,23 @@ { "config": { "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El proceso de configuraci\u00f3n ya est\u00e1 en curso", "bad_ssdp": "Falta un valor necesario en los datos SSDP", - "no_devices_found": "No se han encontrado dispositivos en la red" + "no_devices_found": "No se han encontrado dispositivos en la red", + "not_dms": "El dispositivo no es un servidor multimedia compatible" }, "flow_title": "{name}", "step": { + "confirm": { + "description": "\u00bfQuiere empezar a configurar?" + }, "user": { "data": { "host": "Host" }, - "description": "Escoge un dispositivo a configurar" + "description": "Escoge un dispositivo a configurar", + "title": "Dispositivos DLNA DMA descubiertos" } } } diff --git a/homeassistant/components/doorbird/translations/es.json b/homeassistant/components/doorbird/translations/es.json index 68de2419d2a..355a48e9191 100644 --- a/homeassistant/components/doorbird/translations/es.json +++ b/homeassistant/components/doorbird/translations/es.json @@ -27,6 +27,9 @@ "init": { "data": { "events": "Lista de eventos separados por comas." + }, + "data_description": { + "events": "A\u00f1ade un nombre de evento separado por comas para cada evento que desee rastrear. Despu\u00e9s de ingresarlos aqu\u00ed, use la aplicaci\u00f3n DoorBird para asignarlos a un evento espec\u00edfico. \n\n Ejemplo: alguien_puls\u00f3_el_bot\u00f3n, movimiento" } } } diff --git a/homeassistant/components/elkm1/translations/es.json b/homeassistant/components/elkm1/translations/es.json index 2bd75f83e2f..9df8e1f4ddf 100644 --- a/homeassistant/components/elkm1/translations/es.json +++ b/homeassistant/components/elkm1/translations/es.json @@ -5,6 +5,7 @@ "already_configured": "Ya est\u00e1 configurado un Elk-M1 con este prefijo", "already_in_progress": "La configuraci\u00f3n ya se encuentra en proceso", "cannot_connect": "Error al conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/fibaro/translations/es.json b/homeassistant/components/fibaro/translations/es.json index 00a7eeb8ece..99f29f3bee5 100644 --- a/homeassistant/components/fibaro/translations/es.json +++ b/homeassistant/components/fibaro/translations/es.json @@ -1,11 +1,17 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n" + "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" }, "step": { "user": { "data": { + "import_plugins": "\u00bfImportar entidades desde los plugins de fibaro?", "password": "Contrase\u00f1a", "url": "URL en el format http://HOST/api/", "username": "Usuario" diff --git a/homeassistant/components/filesize/translations/es.json b/homeassistant/components/filesize/translations/es.json index e2bc079b961..ee030e86cf9 100644 --- a/homeassistant/components/filesize/translations/es.json +++ b/homeassistant/components/filesize/translations/es.json @@ -1,7 +1,19 @@ { "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado" + }, "error": { - "not_allowed": "Ruta no permitida" + "not_allowed": "Ruta no permitida", + "not_valid": "La ruta no es v\u00e1lida" + }, + "step": { + "user": { + "data": { + "file_path": "Ruta al archivo" + } + } } - } + }, + "title": "Tama\u00f1o del archivo" } \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/es.json b/homeassistant/components/fritz/translations/es.json index 964db5b5325..f1386a9e87f 100644 --- a/homeassistant/components/fritz/translations/es.json +++ b/homeassistant/components/fritz/translations/es.json @@ -10,7 +10,8 @@ "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", - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "upnp_not_configured": "Falta la configuraci\u00f3n de UPnP en el dispositivo." }, "flow_title": "FRITZ!Box Tools: {name}", "step": { @@ -46,7 +47,8 @@ "step": { "init": { "data": { - "consider_home": "Segundos para considerar un dispositivo en 'casa'" + "consider_home": "Segundos para considerar un dispositivo en 'casa'", + "old_discovery": "Habilitar m\u00e9todo de descubrimiento antiguo" } } } diff --git a/homeassistant/components/generic/translations/es.json b/homeassistant/components/generic/translations/es.json index d0e2087acf7..8e1189f30f1 100644 --- a/homeassistant/components/generic/translations/es.json +++ b/homeassistant/components/generic/translations/es.json @@ -1,20 +1,29 @@ { "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." }, "error": { "already_exists": "Ya hay una c\u00e1mara con esa URL de configuraci\u00f3n.", "invalid_still_image": "La URL no ha devuelto una imagen fija v\u00e1lida", "no_still_image_or_stream_url": "Tienes que especificar al menos una imagen una URL de flujo", + "stream_file_not_found": "No se encontr\u00f3 el archivo al intentar conectarse a la transmisi\u00f3n (\u00bfest\u00e1 instalado ffmpeg?)", "stream_http_not_found": "HTTP 404 'Not found' al intentar conectarse al flujo de datos ('stream')", + "stream_io_error": "Error de entrada/salida al intentar conectarse a la transmisi\u00f3n. \u00bfProtocolo de transporte RTSP incorrecto?", "stream_no_route_to_host": "No se pudo encontrar el anfitri\u00f3n mientras intentaba conectar al flujo de datos", "stream_no_video": "El flujo no contiene v\u00eddeo", + "stream_not_permitted": "Operaci\u00f3n no permitida al intentar conectarse a la transmisi\u00f3n. \u00bfProtocolo de transporte RTSP incorrecto?", "stream_unauthorised": "La autorizaci\u00f3n ha fallado mientras se intentaba conectar con el flujo de datos", + "template_error": "Error al renderizar la plantilla. Revise el registro para m\u00e1s informaci\u00f3n.", "timeout": "El tiempo m\u00e1ximo de carga de la URL ha expirado", + "unable_still_load": "No se puede cargar una imagen v\u00e1lida desde la URL de la imagen fija (p. ej., host no v\u00e1lido, URL o error de autenticaci\u00f3n). Revise el registro para obtener m\u00e1s informaci\u00f3n.", "unknown": "Error inesperado" }, "step": { + "confirm": { + "description": "\u00bfQuiere empezar a configurar?" + }, "content_type": { "data": { "content_type": "Tipos de contenido" @@ -26,21 +35,32 @@ "authentication": "Autenticaci\u00f3n", "framerate": "Frecuencia de visualizaci\u00f3n (Hz)", "limit_refetch_to_url_change": "Limita la lectura al cambio de URL", + "password": "Contrase\u00f1a", + "rtsp_transport": "Protocolo de transporte RTSP", "still_image_url": "URL de imagen fija (ej. http://...)", "stream_source": "URL origen del flux (p. ex. rtsp://...)", "username": "Usuario", "verify_ssl": "Verifica el certificat SSL" - } + }, + "description": "Introduzca los ajustes para conectarse a la c\u00e1mara." } } }, "options": { "error": { "already_exists": "Ya hay una c\u00e1mara con esa URL de configuraci\u00f3n.", + "invalid_still_image": "La URL no devolvi\u00f3 una imagen fija v\u00e1lida", + "no_still_image_or_stream_url": "Debe especificar al menos una imagen fija o URL de transmisi\u00f3n", + "stream_file_not_found": "No se encontr\u00f3 el archivo al intentar conectarse a la transmisi\u00f3n (\u00bfest\u00e1 instalado ffmpeg?)", + "stream_http_not_found": "HTTP 404 No encontrado al intentar conectarse a la transmisi\u00f3n", + "stream_io_error": "Error de entrada/salida al intentar conectarse a la transmisi\u00f3n. \u00bfProtocolo de transporte RTSP incorrecto?", "stream_no_route_to_host": "No se pudo encontrar el anfitri\u00f3n mientras intentaba conectar al flujo de datos", + "stream_no_video": "La transmisi\u00f3n no tiene video", + "stream_not_permitted": "Operaci\u00f3n no permitida al intentar conectarse a la transmisi\u00f3n. \u00bfProtocolo de transporte RTSP incorrecto?", "stream_unauthorised": "La autorizaci\u00f3n ha fallado mientras se intentaba conectar con el flujo de datos", "template_error": "Error al renderizar la plantilla. Revise el registro para m\u00e1s informaci\u00f3n.", "timeout": "El tiempo m\u00e1ximo de carga de la URL ha expirado", + "unable_still_load": "No se puede cargar una imagen v\u00e1lida desde la URL de la imagen fija (por ejemplo, host no v\u00e1lido, URL o error de autenticaci\u00f3n). Revise el registro para obtener m\u00e1s informaci\u00f3n.", "unknown": "Error inesperado" }, "step": { @@ -58,8 +78,13 @@ "password": "Contrase\u00f1a", "rtsp_transport": "Protocolo de transporte RTSP", "still_image_url": "URL de imagen fija (ej. http://...)", + "stream_source": "URL de origen de la transmisi\u00f3n (por ejemplo, rtsp://...)", + "use_wallclock_as_timestamps": "Usar el reloj de pared como marca de tiempo", "username": "Usuario", "verify_ssl": "Verifica el certificado SSL" + }, + "data_description": { + "use_wallclock_as_timestamps": "Esta opci\u00f3n puede corregir los problemas de segmentaci\u00f3n o bloqueo que surgen de las implementaciones de marcas de tiempo defectuosas en algunas c\u00e1maras" } } } diff --git a/homeassistant/components/geocaching/translations/es.json b/homeassistant/components/geocaching/translations/es.json index 8b03adca234..8fd4800c588 100644 --- a/homeassistant/components/geocaching/translations/es.json +++ b/homeassistant/components/geocaching/translations/es.json @@ -5,6 +5,7 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Mira su documentaci\u00f3n.", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [compruebe la secci\u00f3n de ayuda]({docs_url})", "oauth_error": "Se han recibido datos token inv\u00e1lidos.", "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" }, diff --git a/homeassistant/components/geofency/translations/es.json b/homeassistant/components/geofency/translations/es.json index 22aa30cc182..6726a4d2e3b 100644 --- a/homeassistant/components/geofency/translations/es.json +++ b/homeassistant/components/geofency/translations/es.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "No est\u00e1 conectado a Home Assistant Cloud.", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, diff --git a/homeassistant/components/github/translations/es.json b/homeassistant/components/github/translations/es.json index d53004c3cb5..37bf1d4689e 100644 --- a/homeassistant/components/github/translations/es.json +++ b/homeassistant/components/github/translations/es.json @@ -4,6 +4,9 @@ "already_configured": "El servicio ya est\u00e1 configurado", "could_not_register": "No se pudo registrar la integraci\u00f3n con GitHub" }, + "progress": { + "wait_for_device": "1. Abra {url}\n 2.Pegue la siguiente clave para autorizar la integraci\u00f3n:\n ```\n {code}\n ```\n" + }, "step": { "repositories": { "data": { diff --git a/homeassistant/components/google/translations/es.json b/homeassistant/components/google/translations/es.json index c6d990c2caa..aed1ec5d9ad 100644 --- a/homeassistant/components/google/translations/es.json +++ b/homeassistant/components/google/translations/es.json @@ -1,6 +1,10 @@ { "config": { "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", + "already_in_progress": "El proceso de configuraci\u00f3n ya est\u00e1 en curso", + "code_expired": "El c\u00f3digo de autenticaci\u00f3n caduc\u00f3 o la configuraci\u00f3n de la credencial no es v\u00e1lida, int\u00e9ntelo de nuevo.", + "invalid_access_token": "Token de acceso no v\u00e1lido", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "oauth_error": "Se han recibido datos token inv\u00e1lidos.", "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" @@ -8,12 +12,28 @@ "create_entry": { "default": "Autenticaci\u00f3n exitosa" }, + "progress": { + "exchange": "Para vincular su cuenta de Google, visite [ {url} ]( {url} ) e ingrese el c\u00f3digo: \n\n {user_code}" + }, "step": { "auth": { "title": "Vincular cuenta de Google" }, "pick_implementation": { "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" + }, + "reauth_confirm": { + "description": "La integraci\u00f3n de Google Calendar necesita volver a autenticar su cuenta", + "title": "Integraci\u00f3n de la reautenticaci\u00f3n" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Acceso de Home Assistant a Google Calendar" + } } } } diff --git a/homeassistant/components/group/translations/es.json b/homeassistant/components/group/translations/es.json index 67742954447..c58cb744a92 100644 --- a/homeassistant/components/group/translations/es.json +++ b/homeassistant/components/group/translations/es.json @@ -3,34 +3,43 @@ "step": { "binary_sensor": { "data": { - "hide_members": "Esconde miembros" + "all": "Todas las entidades", + "entities": "Miembros", + "hide_members": "Esconde miembros", + "name": "Nombre" }, + "description": "Si \"todas las entidades\" est\u00e1n habilitadas, el estado del grupo est\u00e1 activado solo si todos los miembros est\u00e1n activados. Si \"todas las entidades\" est\u00e1n deshabilitadas, el estado del grupo es activado si alg\u00fan miembro est\u00e1 activado.", "title": "Agregar grupo" }, "cover": { "data": { + "entities": "Miembros", "hide_members": "Esconde miembros", "name": "Nombre del Grupo" - } + }, + "title": "A\u00f1adir grupo" }, "fan": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros" + "hide_members": "Esconde miembros", + "name": "Nombre" }, "title": "Agregar grupo" }, "light": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros" + "hide_members": "Esconde miembros", + "name": "Nombre" }, "title": "Agregar grupo" }, "lock": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros" + "hide_members": "Esconde miembros", + "name": "Nombre" }, "title": "Agregar grupo" }, @@ -44,16 +53,24 @@ }, "switch": { "data": { - "entities": "Miembros" - } + "entities": "Miembros", + "hide_members": "Ocultar miembros", + "name": "Nombre" + }, + "title": "A\u00f1adir grupo" }, "user": { + "description": "Los grupos permiten crear una nueva entidad que representa a varias entidades del mismo tipo.", "menu_options": { "binary_sensor": "Grupo de sensores binarios", "cover": "Grupo de cubiertas", "fan": "Grupo de ventiladores", + "light": "Grupo de luz", + "lock": "Bloquear el grupo", + "media_player": "Grupo de reproductores multimedia", "switch": "Grupo de conmutadores" - } + }, + "title": "A\u00f1adir grupo" } } }, @@ -62,28 +79,35 @@ "binary_sensor": { "data": { "all": "Todas las entidades", + "entities": "Miembros", "hide_members": "Esconde miembros" - } + }, + "description": "Si \"todas las entidades\" est\u00e1 habilitado, el estado del grupo est\u00e1 activado solo si todos los miembros est\u00e1n activados. Si \"todas las entidades\" est\u00e1 deshabilitado, el estado del grupo es activado si alg\u00fan miembro est\u00e1 activado." }, "cover": { - "data": { - "hide_members": "Esconde miembros" - } - }, - "fan": { - "data": { - "hide_members": "Esconde miembros" - } - }, - "light": { "data": { "entities": "Miembros", "hide_members": "Esconde miembros" } }, + "fan": { + "data": { + "entities": "Miembros", + "hide_members": "Esconde miembros" + } + }, + "light": { + "data": { + "all": "Todas las entidades", + "entities": "Miembros", + "hide_members": "Esconde miembros" + }, + "description": "Si \"todas las entidades\" est\u00e1 habilitado, el estado del grupo est\u00e1 activado solo si todos los miembros est\u00e1n activados. Si \"todas las entidades\" est\u00e1 deshabilitado, el estado del grupo es activado si alg\u00fan miembro est\u00e1 activado." + }, "lock": { "data": { - "entities": "Miembros" + "entities": "Miembros", + "hide_members": "Ocultar miembros" } }, "media_player": { @@ -91,6 +115,14 @@ "entities": "Miembros", "hide_members": "Esconde miembros" } + }, + "switch": { + "data": { + "all": "Todas las entidades", + "entities": "Miembros", + "hide_members": "Ocultar miembros" + }, + "description": "Si \"todas las entidades\" est\u00e1 habilitado, el estado del grupo est\u00e1 activado solo si todos los miembros est\u00e1n activados. Si \"todas las entidades\" est\u00e1 deshabilitado, el estado del grupo es activado si alg\u00fan miembro est\u00e1 activado." } } }, diff --git a/homeassistant/components/here_travel_time/translations/es.json b/homeassistant/components/here_travel_time/translations/es.json new file mode 100644 index 00000000000..c1a8d9cef11 --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/es.json @@ -0,0 +1,82 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "destination_coordinates": { + "data": { + "destination": "Destino como coordenadas GPS" + }, + "title": "Elija el destino" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "Destino usando una entidad" + }, + "title": "Elija el destino" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "Usando una ubicaci\u00f3n en el mapa", + "destination_entity": "Usando una entidad" + }, + "title": "Elija el destino" + }, + "origin_coordinates": { + "data": { + "origin": "Origen como coordenadas GPS" + }, + "title": "Elija el origen" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "Origen usando una entidad" + }, + "title": "Elija el origen" + }, + "user": { + "data": { + "api_key": "Clave API", + "mode": "Modo de viaje", + "name": "Nombre" + } + } + } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "Hora de llegada" + }, + "title": "Elija la hora de llegada" + }, + "departure_time": { + "data": { + "departure_time": "Hora de salida" + }, + "title": "Elija la hora de salida" + }, + "init": { + "data": { + "route_mode": "Modo de ruta", + "traffic_mode": "Modo de tr\u00e1fico", + "unit_system": "Sistema de unidades" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "Configurar una hora de llegada", + "departure_time": "Configurar una hora de salida", + "no_time": "No configurar una hora" + }, + "title": "Elija el tipo de hora" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index 9aa71faef12..5076e7b8800 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -22,7 +22,8 @@ "accessory": { "data": { "entities": "Entidad" - } + }, + "title": "Seleccione la entidad para el accesorio" }, "advanced": { "data": { @@ -43,16 +44,20 @@ "data": { "entities": "Entidades" }, + "description": "Se incluir\u00e1n todas las entidades de \" {domains} \" excepto las entidades excluidas y las entidades categorizadas.", "title": "Selecciona las entidades a excluir" }, "include": { "data": { "entities": "Entidades" - } + }, + "description": "Se incluir\u00e1n todas las entidades de \" {domains} \" a menos que se seleccionen entidades espec\u00edficas.", + "title": "Seleccione las entidades a incluir" }, "init": { "data": { "domains": "Dominios a incluir", + "include_exclude_mode": "Modo de inclusi\u00f3n", "mode": "Mode de HomeKit" }, "description": "Las entidades de los \"Dominios que se van a incluir\" se establecer\u00e1n en HomeKit. Podr\u00e1 seleccionar qu\u00e9 entidades excluir de esta lista en la siguiente pantalla.", diff --git a/homeassistant/components/homekit_controller/translations/select.es.json b/homeassistant/components/homekit_controller/translations/select.es.json index 0cbfbc71373..13c45f8e538 100644 --- a/homeassistant/components/homekit_controller/translations/select.es.json +++ b/homeassistant/components/homekit_controller/translations/select.es.json @@ -2,6 +2,7 @@ "state": { "homekit_controller__ecobee_mode": { "away": "Afuera", + "home": "Inicio", "sleep": "Durmiendo" } } diff --git a/homeassistant/components/honeywell/translations/es.json b/homeassistant/components/honeywell/translations/es.json index f30e9606a4d..98ad6871b81 100644 --- a/homeassistant/components/honeywell/translations/es.json +++ b/homeassistant/components/honeywell/translations/es.json @@ -17,8 +17,10 @@ "step": { "init": { "data": { - "away_cool_temperature": "Temperatura fria, modo fuera" - } + "away_cool_temperature": "Temperatura fria, modo fuera", + "away_heat_temperature": "Temperatura del calor exterior" + }, + "description": "Opciones de configuraci\u00f3n adicionales de Honeywell. Las temperaturas se establecen en Fahrenheit." } } } diff --git a/homeassistant/components/ialarm_xr/translations/es.json b/homeassistant/components/ialarm_xr/translations/es.json index f5755835d3d..41cc88c7e6f 100644 --- a/homeassistant/components/ialarm_xr/translations/es.json +++ b/homeassistant/components/ialarm_xr/translations/es.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "host": "Anfitri\u00f3n", "password": "Contrase\u00f1a", "port": "Puerto", "username": "Nombre de usuario" diff --git a/homeassistant/components/ifttt/translations/es.json b/homeassistant/components/ifttt/translations/es.json index b29862c38e2..e65f12b6295 100644 --- a/homeassistant/components/ifttt/translations/es.json +++ b/homeassistant/components/ifttt/translations/es.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "No est\u00e1 conectado a Home Assistant Cloud.", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, diff --git a/homeassistant/components/integration/translations/es.json b/homeassistant/components/integration/translations/es.json index 8034e4746fe..4b4f1306dc9 100644 --- a/homeassistant/components/integration/translations/es.json +++ b/homeassistant/components/integration/translations/es.json @@ -4,6 +4,7 @@ "user": { "data": { "method": "M\u00e9todo de integraci\u00f3n", + "name": "Nombre", "round": "Precisi\u00f3n", "source": "Sensor de entrada", "unit_prefix": "Prefijo m\u00e9trico", @@ -13,7 +14,9 @@ "round": "Controla el n\u00famero de d\u00edgitos decimales en la salida.", "unit_prefix": "La salida se escalar\u00e1 seg\u00fan el prefijo m\u00e9trico seleccionado.", "unit_time": "La salida se escalar\u00e1 seg\u00fan la unidad de tiempo seleccionada." - } + }, + "description": "Cree un sensor que calcule una suma de Riemann para estimar la integral de un sensor.", + "title": "A\u00f1adir sensor integral de suma de Riemann" } } }, diff --git a/homeassistant/components/intellifire/translations/es.json b/homeassistant/components/intellifire/translations/es.json index 8d19b2ba3bf..4b61f7f4b3e 100644 --- a/homeassistant/components/intellifire/translations/es.json +++ b/homeassistant/components/intellifire/translations/es.json @@ -24,7 +24,8 @@ "manual_device_entry": { "data": { "host": "Host (direcci\u00f3n IP)" - } + }, + "description": "Configuraci\u00f3n local" }, "pick_device": { "data": { diff --git a/homeassistant/components/iss/translations/es.json b/homeassistant/components/iss/translations/es.json index 6ca3e875615..02a03ee5995 100644 --- a/homeassistant/components/iss/translations/es.json +++ b/homeassistant/components/iss/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "latitude_longitude_not_defined": "La latitud y la longitud no est\u00e1n definidas en Home Assistant." + "latitude_longitude_not_defined": "La latitud y la longitud no est\u00e1n definidas en Home Assistant.", + "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." }, "step": { "user": { diff --git a/homeassistant/components/isy994/translations/es.json b/homeassistant/components/isy994/translations/es.json index b9b2c1f1ed5..7fd765fb437 100644 --- a/homeassistant/components/isy994/translations/es.json +++ b/homeassistant/components/isy994/translations/es.json @@ -7,6 +7,7 @@ "cannot_connect": "Error al conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_host": "La entrada del host no estaba en formato URL completo, por ejemplo, http://192.168.10.100:80", + "reauth_successful": "La reautenticaci\u00f3n fue exitosa", "unknown": "Error inesperado" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/kaleidescape/translations/es.json b/homeassistant/components/kaleidescape/translations/es.json index 6586a20f202..5cb7047f4f5 100644 --- a/homeassistant/components/kaleidescape/translations/es.json +++ b/homeassistant/components/kaleidescape/translations/es.json @@ -3,8 +3,13 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "unknown": "Error inesperado", "unsupported": "Dispositiu no compatible" }, + "error": { + "cannot_connect": "Fallo en la conexi\u00f3n", + "unsupported": "Dispositivo no compatible" + }, "flow_title": "{model} ({name})", "step": { "discovery_confirm": { diff --git a/homeassistant/components/knx/translations/es.json b/homeassistant/components/knx/translations/es.json index 6c8ec3e2eb6..d982d96b330 100644 --- a/homeassistant/components/knx/translations/es.json +++ b/homeassistant/components/knx/translations/es.json @@ -6,17 +6,21 @@ }, "error": { "cannot_connect": "Error al conectar", + "file_not_found": "El archivo `.knxkeys` especificado no se encontr\u00f3 en la ruta config/.storage/knx/", "invalid_individual_address": "El valor no coincide con el patr\u00f3n de direcci\u00f3n KNX individual. 'area.line.device'", - "invalid_ip_address": "Direcci\u00f3n IPv4 inv\u00e1lida." + "invalid_ip_address": "Direcci\u00f3n IPv4 inv\u00e1lida.", + "invalid_signature": "La contrase\u00f1a para descifrar el archivo `.knxkeys` es incorrecta." }, "step": { "manual_tunnel": { "data": { "host": "Host", "local_ip": "IP local de Home Assistant", - "port": "Puerto" + "port": "Puerto", + "tunneling_type": "Tipo de t\u00fanel KNX" }, "data_description": { + "host": "Direcci\u00f3n IP del dispositivo de t\u00fanel KNX/IP.", "local_ip": "D\u00e9jalo en blanco para utilizar el descubrimiento autom\u00e1tico.", "port": "Puerto del dispositivo de tunelizaci\u00f3n KNX/IP.Puerto del dispositivo de tunelizaci\u00f3n KNX/IP." }, @@ -29,29 +33,40 @@ "multicast_group": "El grupo de multidifusi\u00f3n utilizado para el enrutamiento", "multicast_port": "El puerto de multidifusi\u00f3n utilizado para el enrutamiento" }, + "data_description": { + "individual_address": "Direcci\u00f3n KNX que usar\u00e1 Home Assistant, por ejemplo, `0.0.4`", + "local_ip": "D\u00e9jelo en blanco para usar el descubrimiento autom\u00e1tico." + }, "description": "Por favor, configure las opciones de enrutamiento." }, "secure_knxkeys": { "data": { + "knxkeys_filename": "El nombre de su archivo `.knxkeys` (incluyendo la extensi\u00f3n)", "knxkeys_password": "Contrase\u00f1a para descifrar el archivo `.knxkeys`." }, "data_description": { + "knxkeys_filename": "Se espera que el archivo se encuentre en su directorio de configuraci\u00f3n en `.storage/knx/`.\n En el sistema operativo Home Assistant, ser\u00eda `/config/.storage/knx/`\n Ejemplo: `mi_proyecto.knxkeys`", "knxkeys_password": "Se ha definido durante la exportaci\u00f3n del archivo desde ETS.Se ha definido durante la exportaci\u00f3n del archivo desde ETS." }, "description": "Introduce la informaci\u00f3n de tu archivo `.knxkeys`." }, "secure_manual": { "data": { - "user_id": "ID de usuario" + "device_authentication": "Contrase\u00f1a de autenticaci\u00f3n del dispositivo", + "user_id": "ID de usuario", + "user_password": "Contrase\u00f1a del usuario" }, "data_description": { - "user_id": "A menudo, es el n\u00famero del t\u00fanel +1. Por tanto, 'T\u00fanel 2' tendr\u00eda el ID de usuario '3'." + "device_authentication": "Esto se configura en el panel 'IP' de la interfaz en ETS.", + "user_id": "A menudo, es el n\u00famero del t\u00fanel +1. Por tanto, 'T\u00fanel 2' tendr\u00eda el ID de usuario '3'.", + "user_password": "Contrase\u00f1a para la conexi\u00f3n espec\u00edfica del t\u00fanel establecida en el panel de \"Propiedades\" del t\u00fanel en ETS." }, "description": "Introduce la informaci\u00f3n de seguridad IP (IP Secure)." }, "secure_tunneling": { "description": "Selecciona c\u00f3mo quieres configurar KNX/IP Secure.", "menu_options": { + "secure_knxkeys": "Use un archivo `.knxkeys` que contenga claves seguras de IP", "secure_manual": "Configura manualmente las claves de seguridad IP (IP Secure)" } }, @@ -83,13 +98,18 @@ }, "data_description": { "individual_address": "Direcci\u00f3n KNX para utilizar con Home Assistant, ej. `0.0.4`", - "rate_limit": "Telegramas de salida m\u00e1ximos por segundo. \nRecomendado: de 20 a 40" + "local_ip": "Usar `0.0.0.0` para el descubrimiento autom\u00e1tico.", + "multicast_group": "Se usa para el enrutamiento y el descubrimiento. Predeterminado: `224.0.23.12`", + "multicast_port": "Se usa para el enrutamiento y el descubrimiento. Predeterminado: `3671`", + "rate_limit": "Telegramas de salida m\u00e1ximos por segundo. \nRecomendado: de 20 a 40", + "state_updater": "Establece los valores predeterminados para leer los estados del bus KNX. Cuando est\u00e1 deshabilitado, Home Assistant no recuperar\u00e1 activamente estados de entidad del bus KNX. Puede ser anulado por las opciones de entidad `sync_state`." } }, "tunnel": { "data": { "host": "Host", - "port": "Puerto" + "port": "Puerto", + "tunneling_type": "Tipo de t\u00fanel KNX" }, "data_description": { "host": "Direcci\u00f3n IP del dispositivo de tunelizaci\u00f3n KNX/IP.", diff --git a/homeassistant/components/laundrify/translations/es.json b/homeassistant/components/laundrify/translations/es.json new file mode 100644 index 00000000000..05c019700bc --- /dev/null +++ b/homeassistant/components/laundrify/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." + }, + "error": { + "cannot_connect": "Fallo en la conexi\u00f3n", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_format": "Formato no v\u00e1lido. Por favor, especif\u00edquelo como xxx-xxx.", + "unknown": "Error inesperado" + }, + "step": { + "init": { + "data": { + "code": "C\u00f3digo de autenticaci\u00f3n (xxx-xxx)" + }, + "description": "Por favor, introduzca su c\u00f3digo de autenticaci\u00f3n personal que se muestra en la aplicaci\u00f3n Laundrify." + }, + "reauth_confirm": { + "description": "La integraci\u00f3n de laundrify necesita volver a autentificarse.", + "title": "Integraci\u00f3n de la reautenticaci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sensor.es.json b/homeassistant/components/litterrobot/translations/sensor.es.json index 1bf023d68bc..ca5695d1347 100644 --- a/homeassistant/components/litterrobot/translations/sensor.es.json +++ b/homeassistant/components/litterrobot/translations/sensor.es.json @@ -4,11 +4,25 @@ "br": "Bolsa extra\u00edda", "ccc": "Ciclo de limpieza completado", "ccp": "Ciclo de limpieza en curso", + "csf": "Fallo del sensor de gatos", + "csi": "Sensor de gatos interrumpido", + "cst": "Tiempo del sensor de gatos", + "df1": "Caj\u00f3n casi lleno - Quedan 2 ciclos", + "df2": "Caj\u00f3n casi lleno - Queda 1 ciclo", + "dfs": "Caj\u00f3n lleno", "dhf": "Error de posici\u00f3n de vertido + inicio", + "dpf": "Fallo de posici\u00f3n de descarga", "ec": "Ciclo vac\u00edo", + "hpf": "Fallo de posici\u00f3n inicial", "off": "Apagado", "offline": "Desconectado", - "rdy": "Listo" + "otf": "Fallo de par excesivo", + "p": "Pausada", + "pd": "Detecci\u00f3n de pellizcos", + "rdy": "Listo", + "scf": "Fallo del sensor de gatos al inicio", + "sdf": "Caj\u00f3n lleno al inicio", + "spf": "Detecci\u00f3n de pellizco al inicio" } } } \ No newline at end of file diff --git a/homeassistant/components/meater/translations/es.json b/homeassistant/components/meater/translations/es.json index 39d35b38d4a..44e5f3984f4 100644 --- a/homeassistant/components/meater/translations/es.json +++ b/homeassistant/components/meater/translations/es.json @@ -1,18 +1,26 @@ { "config": { "error": { + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "service_unavailable_error": "La API no est\u00e1 disponible actualmente, vuelva a intentarlo m\u00e1s tarde.", "unknown_auth_error": "Error inesperado" }, "step": { "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, "description": "Confirma la contrase\u00f1a de la cuenta de Meater Cloud {username}." }, "user": { "data": { "password": "Contrase\u00f1a", "username": "Usuario" - } + }, + "data_description": { + "username": "Nombre de usuario de Meater Cloud, normalmente una direcci\u00f3n de correo electr\u00f3nico." + }, + "description": "Configure su cuenta de Meater Cloud." } } } diff --git a/homeassistant/components/meteoclimatic/translations/es.json b/homeassistant/components/meteoclimatic/translations/es.json index 027f85c60df..1b6e29770cc 100644 --- a/homeassistant/components/meteoclimatic/translations/es.json +++ b/homeassistant/components/meteoclimatic/translations/es.json @@ -11,6 +11,9 @@ "user": { "data": { "code": "C\u00f3digo de la estaci\u00f3n" + }, + "data_description": { + "code": "Parece ESCAT4300000043206B" } } } diff --git a/homeassistant/components/min_max/translations/es.json b/homeassistant/components/min_max/translations/es.json index aceaa287a3b..fadad650eaa 100644 --- a/homeassistant/components/min_max/translations/es.json +++ b/homeassistant/components/min_max/translations/es.json @@ -5,11 +5,13 @@ "data": { "entity_ids": "Entidades de entrada", "name": "Nombre", + "round_digits": "Precisi\u00f3n", "type": "Caracter\u00edstica estad\u00edstica" }, "data_description": { "round_digits": "Controla el n\u00famero de d\u00edgitos decimales cuando la caracter\u00edstica estad\u00edstica es la media o mediana." }, + "description": "Cree un sensor que calcule un valor m\u00ednimo, m\u00e1ximo, medio o mediano a partir de una lista de sensores de entrada.", "title": "Agregar sensor m\u00edn / m\u00e1x / media / mediana" } } diff --git a/homeassistant/components/nanoleaf/translations/es.json b/homeassistant/components/nanoleaf/translations/es.json index 187517ed478..9899d30d36f 100644 --- a/homeassistant/components/nanoleaf/translations/es.json +++ b/homeassistant/components/nanoleaf/translations/es.json @@ -27,7 +27,10 @@ }, "device_automation": { "trigger_type": { - "swipe_down": "Desliza hacia abajo" + "swipe_down": "Desliza hacia abajo", + "swipe_left": "Deslizar a la izquierda", + "swipe_right": "Deslizar a la derecha", + "swipe_up": "Deslizar hacia arriba" } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/es.json b/homeassistant/components/onewire/translations/es.json index 9fa4912166f..3617f4c8fd4 100644 --- a/homeassistant/components/onewire/translations/es.json +++ b/homeassistant/components/onewire/translations/es.json @@ -25,11 +25,13 @@ "data": { "precision": "Precisi\u00f3n del sensor" }, - "description": "Selecciona la precisi\u00f3n del sensor {sensor_id}" + "description": "Selecciona la precisi\u00f3n del sensor {sensor_id}", + "title": "Precisi\u00f3n del sensor OneWire" }, "device_selection": { "data": { - "clear_device_options": "Borra todas las configuraciones de dispositivo" + "clear_device_options": "Borra todas las configuraciones de dispositivo", + "device_selection": "Seleccionar los dispositivos a configurar" }, "description": "Seleccione los pasos de configuraci\u00f3n a procesar", "title": "Opciones de dispositivo OneWire" diff --git a/homeassistant/components/plugwise/translations/es.json b/homeassistant/components/plugwise/translations/es.json index 8fed1713398..16fae70586d 100644 --- a/homeassistant/components/plugwise/translations/es.json +++ b/homeassistant/components/plugwise/translations/es.json @@ -5,7 +5,8 @@ }, "error": { "cannot_connect": "Fall\u00f3 la conexi\u00f3n", - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida, comprueba los 8 caracteres de tu Smile ID", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_setup": "Agregue su Adam en lugar de su Anna, consulte la documentaci\u00f3n de integraci\u00f3n de Home Assistant Plugwise para obtener m\u00e1s informaci\u00f3n", "unknown": "Error inesperado" }, "flow_title": "{name}", @@ -14,7 +15,7 @@ "data": { "flow_type": "Tipo de conexi\u00f3n", "host": "Direcci\u00f3n IP", - "password": "ID de sonrisa", + "password": "ID Smile", "port": "Puerto", "username": "Nombre de usuario de la sonrisa" }, diff --git a/homeassistant/components/powerwall/translations/es.json b/homeassistant/components/powerwall/translations/es.json index c0f73c2ac99..ae578b37e50 100644 --- a/homeassistant/components/powerwall/translations/es.json +++ b/homeassistant/components/powerwall/translations/es.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "El powerwall ya est\u00e1 configurado", + "cannot_connect": "Fallo en la conexi\u00f3n", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { @@ -13,12 +14,14 @@ "flow_title": "Powerwall de Tesla ({ip_address})", "step": { "confirm_discovery": { - "description": "\u00bfQuieres configurar {name} ({ip_address})?" + "description": "\u00bfQuieres configurar {name} ({ip_address})?", + "title": "Conectar al powerwall" }, "reauth_confim": { "data": { "password": "Contrase\u00f1a" }, + "description": "La contrase\u00f1a suele ser los \u00faltimos 5 caracteres del n\u00famero de serie de Backup Gateway y se puede encontrar en la aplicaci\u00f3n de Tesla o los \u00faltimos 5 caracteres de la contrase\u00f1a que se encuentra dentro de la puerta de Backup Gateway 2.", "title": "Reautorizar la powerwall" }, "user": { diff --git a/homeassistant/components/ps4/translations/es.json b/homeassistant/components/ps4/translations/es.json index a2c6e6fd1f4..4eb7636d0a2 100644 --- a/homeassistant/components/ps4/translations/es.json +++ b/homeassistant/components/ps4/translations/es.json @@ -23,12 +23,18 @@ "ip_address": "Direcci\u00f3n IP", "name": "Nombre", "region": "Regi\u00f3n" + }, + "data_description": { + "code": "Vaya a 'Configuraci\u00f3n' en su consola PlayStation 4. Luego navegue hasta 'Configuraci\u00f3n de conexi\u00f3n de la aplicaci\u00f3n m\u00f3vil' y seleccione 'Agregar dispositivo' para obtener el pin." } }, "mode": { "data": { "ip_address": "Direcci\u00f3n IP (d\u00e9jalo en blanco si usas la detecci\u00f3n autom\u00e1tica).", "mode": "Modo configuraci\u00f3n" + }, + "data_description": { + "ip_address": "D\u00e9jelo en blanco si selecciona la detecci\u00f3n autom\u00e1tica." } } } diff --git a/homeassistant/components/recorder/translations/es.json b/homeassistant/components/recorder/translations/es.json index ef195cd52c9..86bdd0abec8 100644 --- a/homeassistant/components/recorder/translations/es.json +++ b/homeassistant/components/recorder/translations/es.json @@ -2,8 +2,10 @@ "system_health": { "info": { "current_recorder_run": "Hora de inicio de la ejecuci\u00f3n actual", + "database_engine": "Motor de la base de datos", "database_version": "Versi\u00f3n de la base de datos", - "estimated_db_size": "Mida estimada de la base de datos (MiB)" + "estimated_db_size": "Mida estimada de la base de datos (MiB)", + "oldest_recorder_run": "Hora de inicio de ejecuci\u00f3n m\u00e1s antigua" } } } \ No newline at end of file diff --git a/homeassistant/components/rtsp_to_webrtc/translations/es.json b/homeassistant/components/rtsp_to_webrtc/translations/es.json index 13fe65bfc5b..c74f0b6b34d 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/es.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/es.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "server_failure": "El servidor RTSPtoWebRTC devolvi\u00f3 un error. Consulte los registros para obtener m\u00e1s informaci\u00f3n.", + "server_unreachable": "No se puede comunicar con el servidor RTSPtoWebRTC. Consulte los registros para obtener m\u00e1s informaci\u00f3n.", "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json index 1c2a81cb7c0..29b2a97027d 100644 --- a/homeassistant/components/samsungtv/translations/es.json +++ b/homeassistant/components/samsungtv/translations/es.json @@ -22,6 +22,9 @@ "encrypted_pairing": { "description": "Introduce el PIN que se muestra en {device}." }, + "pairing": { + "description": "\u00bfQuiere configurar {device}? Si nunca conect\u00f3 Home Assistant antes, deber\u00eda ver una ventana emergente en su televisor solicitando autorizaci\u00f3n." + }, "reauth_confirm": { "description": "Despu\u00e9s de enviarlo, acepte la ventana emergente en {device} solicitando autorizaci\u00f3n dentro de los 30 segundos." }, diff --git a/homeassistant/components/season/translations/es.json b/homeassistant/components/season/translations/es.json new file mode 100644 index 00000000000..0d22ef0bc1c --- /dev/null +++ b/homeassistant/components/season/translations/es.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "type": "Definici\u00f3n del tipo de estaci\u00f3n" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/es.json b/homeassistant/components/sense/translations/es.json index 5621988c60b..3593b08f17c 100644 --- a/homeassistant/components/sense/translations/es.json +++ b/homeassistant/components/sense/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n fue exitosa" }, "error": { "cannot_connect": "No se pudo conectar", @@ -13,7 +14,8 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "La integraci\u00f3n Sense debe volver a autenticar la cuenta {email}." + "description": "La integraci\u00f3n Sense debe volver a autenticar la cuenta {email}.", + "title": "Reautenticar la integraci\u00f3n" }, "user": { "data": { @@ -26,7 +28,8 @@ "validation": { "data": { "code": "C\u00f3digo de verificaci\u00f3n" - } + }, + "title": "Detecci\u00f3n de autenticaci\u00f3n multifactor" } } } diff --git a/homeassistant/components/senseme/translations/es.json b/homeassistant/components/senseme/translations/es.json index fefa2c8d70c..a10e4422a82 100644 --- a/homeassistant/components/senseme/translations/es.json +++ b/homeassistant/components/senseme/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "Fallo en la conexi\u00f3n" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/sensibo/translations/es.json b/homeassistant/components/sensibo/translations/es.json index b91e1b63f9b..43d9acc2645 100644 --- a/homeassistant/components/sensibo/translations/es.json +++ b/homeassistant/components/sensibo/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "La cuenta ya est\u00e1 configurada" + "already_configured": "La cuenta ya est\u00e1 configurada", + "reauth_successful": "La reautenticaci\u00f3n fue exitosa" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index 25cbb8eb30b..0c0011f7297 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/translations/es.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", + "firmware_not_fully_provisioned": "El dispositivo no est\u00e1 completamente aprovisionado. P\u00f3ngase en contacto con el servicio de asistencia de Shelly", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/simplisafe/translations/es.json b/homeassistant/components/simplisafe/translations/es.json index 44716b87cec..25d8de8bbb3 100644 --- a/homeassistant/components/simplisafe/translations/es.json +++ b/homeassistant/components/simplisafe/translations/es.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Esta cuenta SimpliSafe ya est\u00e1 en uso.", + "email_2fa_timed_out": "Se ha agotado el tiempo de espera de la autenticaci\u00f3n de dos factores basada en el correo electr\u00f3nico.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/siren/translations/es.json b/homeassistant/components/siren/translations/es.json new file mode 100644 index 00000000000..f3c1468be31 --- /dev/null +++ b/homeassistant/components/siren/translations/es.json @@ -0,0 +1,3 @@ +{ + "title": "Sirena" +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/es.json b/homeassistant/components/sleepiq/translations/es.json index 57b8f421844..033941b21d2 100644 --- a/homeassistant/components/sleepiq/translations/es.json +++ b/homeassistant/components/sleepiq/translations/es.json @@ -13,6 +13,7 @@ "data": { "password": "Contrase\u00f1a" }, + "description": "La integraci\u00f3n de SleepIQ necesita volver a autenticar su cuenta {username} .", "title": "Reautenticaci\u00f3n de la integraci\u00f3n" }, "user": { diff --git a/homeassistant/components/solax/translations/es.json b/homeassistant/components/solax/translations/es.json index f6658c63353..da4765b897d 100644 --- a/homeassistant/components/solax/translations/es.json +++ b/homeassistant/components/solax/translations/es.json @@ -7,6 +7,8 @@ "step": { "user": { "data": { + "ip_address": "Direcci\u00f3n IP", + "password": "Contrase\u00f1a", "port": "Puerto" } } diff --git a/homeassistant/components/sql/translations/es.json b/homeassistant/components/sql/translations/es.json index 7117115efc7..6811fc498f9 100644 --- a/homeassistant/components/sql/translations/es.json +++ b/homeassistant/components/sql/translations/es.json @@ -4,11 +4,14 @@ "already_configured": "La cuenta ya est\u00e1 configurada" }, "error": { - "db_url_invalid": "URL de la base de datos inv\u00e1lido" + "db_url_invalid": "URL de la base de datos inv\u00e1lido", + "query_invalid": "Consulta SQL no v\u00e1lida" }, "step": { "user": { "data": { + "column": "Columna", + "db_url": "URL de la base de datos", "name": "Nombre", "query": "Selecciona la consulta", "unit_of_measurement": "Unidad de medida", diff --git a/homeassistant/components/steam_online/translations/es.json b/homeassistant/components/steam_online/translations/es.json index 9636fc04a54..26ee994acde 100644 --- a/homeassistant/components/steam_online/translations/es.json +++ b/homeassistant/components/steam_online/translations/es.json @@ -7,10 +7,12 @@ "error": { "cannot_connect": "Fall\u00f3 la conexi\u00f3n", "invalid_account": "ID de la cuenta inv\u00e1lida", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" }, "step": { "reauth_confirm": { + "description": "La integraci\u00f3n de Steam debe volver a autenticarse manualmente \n\n Puede encontrar su clave aqu\u00ed: {api_key_url}", "title": "Volver a autenticar la integraci\u00f3n" }, "user": { @@ -23,6 +25,9 @@ } }, "options": { + "error": { + "unauthorized": "Lista de amigos restringida: Por favor, consulte la documentaci\u00f3n sobre c\u00f3mo ver a todos los dem\u00e1s amigos" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/subaru/translations/es.json b/homeassistant/components/subaru/translations/es.json index 170983e8df4..7b3a32c0213 100644 --- a/homeassistant/components/subaru/translations/es.json +++ b/homeassistant/components/subaru/translations/es.json @@ -6,9 +6,12 @@ }, "error": { "bad_pin_format": "El PIN debe tener 4 d\u00edgitos", + "bad_validation_code_format": "El c\u00f3digo de validaci\u00f3n debe tener 6 d\u00edgitos", "cannot_connect": "No se pudo conectar", "incorrect_pin": "PIN incorrecto", - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + "incorrect_validation_code": "C\u00f3digo de validaci\u00f3n incorrecto", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "two_factor_request_failed": "La solicitud del c\u00f3digo 2FA fall\u00f3, int\u00e9ntalo de nuevo" }, "step": { "pin": { diff --git a/homeassistant/components/sun/translations/es.json b/homeassistant/components/sun/translations/es.json index d8ce466236e..68db12462b1 100644 --- a/homeassistant/components/sun/translations/es.json +++ b/homeassistant/components/sun/translations/es.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "Ya configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." + }, + "step": { + "user": { + "description": "\u00bfQuiere empezar a configurar?" + } + } + }, "state": { "_": { "above_horizon": "Sobre el horizonte", diff --git a/homeassistant/components/switch_as_x/translations/es.json b/homeassistant/components/switch_as_x/translations/es.json index 7e91d3217a4..ad0f9f52bbf 100644 --- a/homeassistant/components/switch_as_x/translations/es.json +++ b/homeassistant/components/switch_as_x/translations/es.json @@ -5,7 +5,8 @@ "data": { "entity_id": "Conmutador", "target_domain": "Nuevo tipo" - } + }, + "description": "Elija un interruptor que desee que aparezca en Home Assistant como luz, cubierta o cualquier otra cosa. El interruptor original se ocultar\u00e1." } } }, diff --git a/homeassistant/components/tankerkoenig/translations/ca.json b/homeassistant/components/tankerkoenig/translations/ca.json index 4935e817ad4..46d523276e7 100644 --- a/homeassistant/components/tankerkoenig/translations/ca.json +++ b/homeassistant/components/tankerkoenig/translations/ca.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "no_stations": "No s'ha pogut trobar cap estaci\u00f3 a l'abast." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Clau API" + } + }, "select_station": { "data": { "stations": "Estacions" diff --git a/homeassistant/components/tankerkoenig/translations/de.json b/homeassistant/components/tankerkoenig/translations/de.json index 421521a30da..f0ad25857a5 100644 --- a/homeassistant/components/tankerkoenig/translations/de.json +++ b/homeassistant/components/tankerkoenig/translations/de.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "Standort ist bereits konfiguriert" + "already_configured": "Standort ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung", "no_stations": "Konnte keine Station in Reichweite finden." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-Schl\u00fcssel" + } + }, "select_station": { "data": { "stations": "Stationen" diff --git a/homeassistant/components/tankerkoenig/translations/es.json b/homeassistant/components/tankerkoenig/translations/es.json index a9d235710da..bda6c43ce6e 100644 --- a/homeassistant/components/tankerkoenig/translations/es.json +++ b/homeassistant/components/tankerkoenig/translations/es.json @@ -1,22 +1,34 @@ { "config": { "abort": { - "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada" + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada", + "reauth_successful": "La reautenticaci\u00f3n fue exitosa" }, "error": { "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "no_stations": "No se pudo encontrar ninguna estaci\u00f3n al alcance." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Clave API" + } + }, "select_station": { "data": { "stations": "Estaciones" }, + "description": "encontr\u00f3 {stations_count} estaciones en el radio", "title": "Selecciona las estaciones a a\u00f1adir" }, "user": { "data": { - "name": "Nombre de la regi\u00f3n" + "api_key": "Clave API", + "fuel_types": "Tipos de combustible", + "location": "Ubicaci\u00f3n", + "name": "Nombre de la regi\u00f3n", + "radius": "Radio de b\u00fasqueda", + "stations": "Estaciones de servicio adicionales" } } } @@ -25,9 +37,11 @@ "step": { "init": { "data": { + "scan_interval": "Intervalo de actualizaci\u00f3n", "show_on_map": "Muestra las estaciones en el mapa", "stations": "Estaciones" - } + }, + "title": "Opciones de Tankerkoenig" } } } diff --git a/homeassistant/components/tankerkoenig/translations/fr.json b/homeassistant/components/tankerkoenig/translations/fr.json index 865fdae66a1..410150263ab 100644 --- a/homeassistant/components/tankerkoenig/translations/fr.json +++ b/homeassistant/components/tankerkoenig/translations/fr.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "invalid_auth": "Authentification non valide", "no_stations": "Aucune station-service n'a \u00e9t\u00e9 trouv\u00e9e dans le rayon indiqu\u00e9." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Cl\u00e9 d'API" + } + }, "select_station": { "data": { "stations": "Stations-services" diff --git a/homeassistant/components/tankerkoenig/translations/ja.json b/homeassistant/components/tankerkoenig/translations/ja.json index 8aa88b83f4a..8ee4aae3b88 100644 --- a/homeassistant/components/tankerkoenig/translations/ja.json +++ b/homeassistant/components/tankerkoenig/translations/ja.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "no_stations": "\u7bc4\u56f2\u5185\u306b\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API\u30ad\u30fc" + } + }, "select_station": { "data": { "stations": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3" diff --git a/homeassistant/components/tankerkoenig/translations/no.json b/homeassistant/components/tankerkoenig/translations/no.json index bbca71d933b..369ac4d3ce4 100644 --- a/homeassistant/components/tankerkoenig/translations/no.json +++ b/homeassistant/components/tankerkoenig/translations/no.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "Plasseringen er allerede konfigurert" + "already_configured": "Plasseringen er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning", "no_stations": "Kunne ikke finne noen stasjon innen rekkevidde." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-n\u00f8kkel" + } + }, "select_station": { "data": { "stations": "Stasjoner" diff --git a/homeassistant/components/tankerkoenig/translations/zh-Hant.json b/homeassistant/components/tankerkoenig/translations/zh-Hant.json index 2a0e5b5d5c6..1e1a5d6e15a 100644 --- a/homeassistant/components/tankerkoenig/translations/zh-Hant.json +++ b/homeassistant/components/tankerkoenig/translations/zh-Hant.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "no_stations": "\u7bc4\u570d\u5167\u627e\u4e0d\u5230\u4efb\u4f55\u52a0\u6cb9\u7ad9\u3002" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u91d1\u9470" + } + }, "select_station": { "data": { "stations": "\u52a0\u6cb9\u7ad9" diff --git a/homeassistant/components/tautulli/translations/es.json b/homeassistant/components/tautulli/translations/es.json index 0d6fd1d3b9e..295683bf358 100644 --- a/homeassistant/components/tautulli/translations/es.json +++ b/homeassistant/components/tautulli/translations/es.json @@ -5,20 +5,25 @@ "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" + "cannot_connect": "Fallo en la conexi\u00f3n", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" }, "step": { "reauth_confirm": { "data": { "api_key": "Clave API" }, + "description": "Para encontrar su clave API, abra la p\u00e1gina web de Tautulli y navegue a Configuraci\u00f3n y luego a la interfaz web. La clave API estar\u00e1 en la parte inferior de esa p\u00e1gina.", "title": "Re-autenticaci\u00f3n de Tautulli" }, "user": { "data": { + "api_key": "Clave API", "url": "URL", "verify_ssl": "Verifica el certificat SSL" - } + }, + "description": "Para encontrar su clave de API, abra la p\u00e1gina web de Tautulli y navegue hasta Configuraci\u00f3n y luego hasta Interfaz web. La clave API estar\u00e1 en la parte inferior de esa p\u00e1gina.\n\nEjemplo de la URL: ```http://192.168.0.10:8181`` con 8181 como puerto predeterminado." } } } diff --git a/homeassistant/components/threshold/translations/es.json b/homeassistant/components/threshold/translations/es.json index 0200c840243..585a690108f 100644 --- a/homeassistant/components/threshold/translations/es.json +++ b/homeassistant/components/threshold/translations/es.json @@ -1,22 +1,36 @@ { "config": { + "error": { + "need_lower_upper": "Los l\u00edmites superior e inferior no pueden estar vac\u00edos" + }, "step": { "user": { "data": { "entity_id": "Sensor de entrada", "hysteresis": "Hist\u00e9resis", "lower": "L\u00edmite inferior", + "name": "Nombre", "upper": "L\u00edmite superior" - } + }, + "description": "Cree un sensor binario que se encienda y apague dependiendo del valor de un sensor \n\n Solo l\u00edmite inferior configurado: se enciende cuando el valor del sensor de entrada es menor que el l\u00edmite inferior.\n Solo l\u00edmite superior configurado: se enciende cuando el valor del sensor de entrada es mayor que el l\u00edmite superior.\n Ambos l\u00edmites inferior y superior configurados: se activa cuando el valor del sensor de entrada est\u00e1 en el rango [l\u00edmite inferior ... l\u00edmite superior].", + "title": "A\u00f1adir sensor de umbral" } } }, "options": { + "error": { + "need_lower_upper": "Los l\u00edmites superior e inferior no pueden estar vac\u00edos" + }, "step": { "init": { "data": { - "hysteresis": "Hist\u00e9resis" - } + "entity_id": "Sensor de entrada", + "hysteresis": "Hist\u00e9resis", + "lower": "L\u00edmite inferior", + "name": "Nombre", + "upper": "L\u00edmite superior" + }, + "description": "Solo l\u00edmite inferior configurado: se enciende cuando el valor del sensor de entrada es menor que el l\u00edmite inferior.\n Solo l\u00edmite superior configurado: se enciende cuando el valor del sensor de entrada es mayor que el l\u00edmite superior.\n Ambos l\u00edmites inferior y superior configurados: se activa cuando el valor del sensor de entrada est\u00e1 en el rango [l\u00edmite inferior ... l\u00edmite superior]." } } }, diff --git a/homeassistant/components/tod/translations/es.json b/homeassistant/components/tod/translations/es.json index 302dc6cfdd9..c1ea525e10c 100644 --- a/homeassistant/components/tod/translations/es.json +++ b/homeassistant/components/tod/translations/es.json @@ -4,7 +4,8 @@ "user": { "data": { "after_time": "Tiempo de activaci\u00f3n", - "before_time": "Tiempo de desactivaci\u00f3n" + "before_time": "Tiempo de desactivaci\u00f3n", + "name": "Nombre" }, "description": "Crea un sensor binario que se activa o desactiva en funci\u00f3n de la hora.", "title": "A\u00f1ade sensor tiempo del d\u00eda" @@ -15,7 +16,8 @@ "step": { "init": { "data": { - "after_time": "Tiempo de activaci\u00f3n" + "after_time": "Tiempo de activaci\u00f3n", + "before_time": "Tiempo apagado" } } } diff --git a/homeassistant/components/tomorrowio/translations/es.json b/homeassistant/components/tomorrowio/translations/es.json index e2f36a0949e..bacc07bcdfc 100644 --- a/homeassistant/components/tomorrowio/translations/es.json +++ b/homeassistant/components/tomorrowio/translations/es.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Fall\u00f3 la conexi\u00f3n", "invalid_api_key": "Clave API inv\u00e1lida", + "rate_limited": "Actualmente la tarifa est\u00e1 limitada, por favor int\u00e9ntelo m\u00e1s tarde.", "unknown": "Error inesperado" }, "step": { @@ -21,7 +22,9 @@ "init": { "data": { "timestep": "Min. entre previsiones de NowCast" - } + }, + "description": "Si elige habilitar la entidad de pron\u00f3stico \"nowcast\", puede configurar el n\u00famero de minutos entre cada pron\u00f3stico. El n\u00famero de pron\u00f3sticos proporcionados depende del n\u00famero de minutos elegidos entre los pron\u00f3sticos.", + "title": "Actualizar las opciones de Tomorrow.io" } } } diff --git a/homeassistant/components/tomorrowio/translations/sensor.es.json b/homeassistant/components/tomorrowio/translations/sensor.es.json index 03820d30265..3967aeba2e2 100644 --- a/homeassistant/components/tomorrowio/translations/sensor.es.json +++ b/homeassistant/components/tomorrowio/translations/sensor.es.json @@ -1,15 +1,26 @@ { "state": { "tomorrowio__health_concern": { + "good": "Bueno", + "hazardous": "Peligroso", + "moderate": "Moderado", "unhealthy": "Poco saludable", "unhealthy_for_sensitive_groups": "No saludable para grupos sensibles", "very_unhealthy": "Nada saludable" }, "tomorrowio__pollen_index": { + "high": "Alto", + "low": "Bajo", "medium": "Medio", + "none": "Ninguna", + "very_high": "Muy alto", "very_low": "Muy bajo" }, "tomorrowio__precipitation_type": { + "freezing_rain": "Lluvia g\u00e9lida", + "ice_pellets": "Perdigones de hielo", + "none": "Ninguna", + "rain": "Lluvia", "snow": "Nieve" } } diff --git a/homeassistant/components/traccar/translations/es.json b/homeassistant/components/traccar/translations/es.json index 851984b0024..d23aa678816 100644 --- a/homeassistant/components/traccar/translations/es.json +++ b/homeassistant/components/traccar/translations/es.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "No est\u00e1 conectado a Home Assistant Cloud.", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, diff --git a/homeassistant/components/trafikverket_ferry/translations/es.json b/homeassistant/components/trafikverket_ferry/translations/es.json index 26532fbce5e..0ab89ac0fa8 100644 --- a/homeassistant/components/trafikverket_ferry/translations/es.json +++ b/homeassistant/components/trafikverket_ferry/translations/es.json @@ -1,10 +1,13 @@ { "config": { "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" }, "error": { + "cannot_connect": "Fallo en la conexi\u00f3n", "incorrect_api_key": "Clave API inv\u00e1lida para la cuenta seleccionada", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_route": "No se pudo encontrar la ruta con la informaci\u00f3n proporcionada" }, "step": { @@ -18,7 +21,8 @@ "api_key": "Clave API", "from": "Des del puerto", "time": "Hora", - "to": "Al puerto" + "to": "Al puerto", + "weekday": "D\u00edas entre semana" } } } diff --git a/homeassistant/components/trafikverket_train/translations/es.json b/homeassistant/components/trafikverket_train/translations/es.json index 4ce1da04b02..2aca5e59745 100644 --- a/homeassistant/components/trafikverket_train/translations/es.json +++ b/homeassistant/components/trafikverket_train/translations/es.json @@ -1,15 +1,30 @@ { "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", + "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + }, "error": { "cannot_connect": "Fall\u00f3 la conexi\u00f3n", "incorrect_api_key": "Clave API inv\u00e1lida para la cuenta seleccionada", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_station": "No se pudo encontrar una estaci\u00f3n con el nombre especificado", + "invalid_time": "Hora proporcionada no v\u00e1lida", + "more_stations": "Se encontraron varias estaciones con el nombre especificado" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Clave API" + } + }, "user": { "data": { + "api_key": "Clave API", "from": "Desde la estaci\u00f3n", - "time": "Hora (opcional)" + "time": "Hora (opcional)", + "to": "A la estaci\u00f3n", + "weekday": "D\u00edas" } } } diff --git a/homeassistant/components/tuya/translations/select.es.json b/homeassistant/components/tuya/translations/select.es.json index e29ccdc381f..b6ab1f8ce1b 100644 --- a/homeassistant/components/tuya/translations/select.es.json +++ b/homeassistant/components/tuya/translations/select.es.json @@ -23,12 +23,17 @@ "morning": "Ma\u00f1ana", "night": "Noche" }, + "tuya__curtain_motor_mode": { + "back": "Volver", + "forward": "Adelante" + }, "tuya__decibel_sensitivity": { "0": "Sensibilidad baja", "1": "Sensibilidad alta" }, "tuya__fan_angle": { "30": "30\u00b0", + "60": "60\u00b0", "90": "90\u00b0" }, "tuya__fingerbot_mode": { @@ -48,8 +53,11 @@ "level_9": "Nivel 9" }, "tuya__humidifier_moodlighting": { + "1": "Estado de \u00e1nimo 1", + "2": "Estado de \u00e1nimo 2", "3": "Estado 3", - "4": "Estado 4" + "4": "Estado 4", + "5": "Estado de \u00e1nimo 5" }, "tuya__humidifier_spray_mode": { "auto": "Autom\u00e1tico", diff --git a/homeassistant/components/ukraine_alarm/translations/es.json b/homeassistant/components/ukraine_alarm/translations/es.json index a9efcd0d536..3e1b8f322bb 100644 --- a/homeassistant/components/ukraine_alarm/translations/es.json +++ b/homeassistant/components/ukraine_alarm/translations/es.json @@ -1,12 +1,20 @@ { "config": { "abort": { + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada", "cannot_connect": "Fall\u00f3 la conexi\u00f3n", "max_regions": "Se pueden configurar un m\u00e1ximo de 5 regiones", + "rate_limit": "Demasiadas peticiones", "timeout": "Tiempo m\u00e1ximo de espera para establecer la conexi\u00f3n agotado", "unknown": "Error inesperado" }, "step": { + "community": { + "data": { + "region": "Regi\u00f3n" + }, + "description": "Si desea monitorear no solo el estado y el distrito, elija su comunidad espec\u00edfica" + }, "district": { "data": { "region": "Regi\u00f3n" diff --git a/homeassistant/components/unifiprotect/translations/es.json b/homeassistant/components/unifiprotect/translations/es.json index 9ca1b56cf01..eb52d0222fc 100644 --- a/homeassistant/components/unifiprotect/translations/es.json +++ b/homeassistant/components/unifiprotect/translations/es.json @@ -9,11 +9,15 @@ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "protect_version": "La versi\u00f3n m\u00ednima requerida es v1.20.0. Actualice UniFi Protect y vuelva a intentarlo." }, + "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { + "password": "Contrase\u00f1a", "username": "Usuario" - } + }, + "description": "\u00bfQuieres configurar {name} ({ip_address})? Necesitar\u00e1 un usuario local creado en su consola UniFi OS para iniciar sesi\u00f3n. Los usuarios de Ubiquiti Cloud no funcionar\u00e1n. Para m\u00e1s informaci\u00f3n: {local_user_documentation_url}", + "title": "UniFi Protect descubierto" }, "reauth_confirm": { "data": { @@ -32,6 +36,7 @@ "username": "Nombre de usuario", "verify_ssl": "Verificar el certificado SSL" }, + "description": "Necesitar\u00e1 un usuario local creado en su consola UniFi OS para iniciar sesi\u00f3n. Los usuarios de Ubiquiti Cloud no funcionar\u00e1n. Para m\u00e1s informaci\u00f3n: {local_user_documentation_url}", "title": "Configuraci\u00f3n de UniFi Protect" } } diff --git a/homeassistant/components/uptime/translations/es.json b/homeassistant/components/uptime/translations/es.json new file mode 100644 index 00000000000..84e840f453f --- /dev/null +++ b/homeassistant/components/uptime/translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." + }, + "step": { + "user": { + "description": "\u00bfQuiere empezar a configurar?" + } + } + }, + "title": "Tiempo de funcionamiento" +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.es.json b/homeassistant/components/uptimerobot/translations/sensor.es.json index 1f037738b42..2adb0ff18c5 100644 --- a/homeassistant/components/uptimerobot/translations/sensor.es.json +++ b/homeassistant/components/uptimerobot/translations/sensor.es.json @@ -1,8 +1,10 @@ { "state": { "uptimerobot__monitor_status": { + "down": "No disponible", "not_checked_yet": "No comprobado", "pause": "En pausa", + "seems_down": "Parece no disponible", "up": "Funcionante" } } diff --git a/homeassistant/components/utility_meter/translations/es.json b/homeassistant/components/utility_meter/translations/es.json index bea05df125d..a38687741d8 100644 --- a/homeassistant/components/utility_meter/translations/es.json +++ b/homeassistant/components/utility_meter/translations/es.json @@ -5,14 +5,20 @@ "data": { "cycle": "Ciclo de reinicio del contador", "delta_values": "Valores delta", + "name": "Nombre", + "net_consumption": "Consumo neto", + "offset": "Compensaci\u00f3n de reinicio del medidor", "source": "Sensor de entrada", "tariffs": "Tarifas soportadas" }, "data_description": { + "delta_values": "Habilitar si los valores de origen son valores delta desde la \u00faltima lectura en lugar de valores absolutos.", "net_consumption": "Act\u00edvalo si es un contador limpio, es decir, puede aumentar y disminuir.", "offset": "Desplaza el d\u00eda de restablecimiento mensual del contador.", "tariffs": "Lista de tarifas admitidas, d\u00e9jala en blanco si utilizas una \u00fanica tarifa." - } + }, + "description": "Cree un sensor que rastree el consumo de varios servicios p\u00fablicos (p. ej., energ\u00eda, gas, agua, calefacci\u00f3n) durante un per\u00edodo de tiempo configurado, generalmente mensual. El sensor del medidor de servicios admite opcionalmente dividir el consumo por tarifas, en ese caso se crea un sensor para cada tarifa, as\u00ed como una entidad de selecci\u00f3n para elegir la tarifa actual.", + "title": "A\u00f1adir medidor de utilidades" } } }, diff --git a/homeassistant/components/vera/translations/es.json b/homeassistant/components/vera/translations/es.json index 0cccacaa88f..8299b2ea6c9 100644 --- a/homeassistant/components/vera/translations/es.json +++ b/homeassistant/components/vera/translations/es.json @@ -9,6 +9,9 @@ "exclude": "Identificadores de dispositivos Vera a excluir de Home Assistant", "lights": "Identificadores de interruptores Vera que deben ser tratados como luces en Home Assistant", "vera_controller_url": "URL del controlador" + }, + "data_description": { + "vera_controller_url": "Deber\u00eda verse as\u00ed: http://192.168.1.161:3480" } } } diff --git a/homeassistant/components/vulcan/translations/es.json b/homeassistant/components/vulcan/translations/es.json index a92a8878730..7538c6411df 100644 --- a/homeassistant/components/vulcan/translations/es.json +++ b/homeassistant/components/vulcan/translations/es.json @@ -1,29 +1,54 @@ { "config": { "abort": { + "all_student_already_configured": "Ya se han a\u00f1adido todos los estudiantes.", "already_configured": "Ya se ha a\u00f1adido a este alumno.", + "no_matching_entries": "No se encontraron entradas que coincidan, use una cuenta diferente o elimine la integraci\u00f3n con el estudiante obsoleto.", "reauth_successful": "Re-autenticaci\u00f3n exitosa" }, "error": { + "cannot_connect": "Error de conexi\u00f3n - compruebe su conexi\u00f3n a Internet", + "expired_credentials": "Credenciales caducadas - cree nuevas en la p\u00e1gina de registro de la aplicaci\u00f3n m\u00f3vil de Vulcan", "expired_token": "Token caducado, genera un nuevo token", + "invalid_pin": "Pin no v\u00e1lido", "invalid_symbol": "S\u00edmbolo inv\u00e1lido", - "invalid_token": "Token inv\u00e1lido" + "invalid_token": "Token inv\u00e1lido", + "unknown": "Se produjo un error desconocido" }, "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "Usar credenciales guardadas" + }, + "description": "A\u00f1adir otro estudiante." + }, "auth": { "data": { - "pin": "PIN" - } + "pin": "PIN", + "region": "S\u00edmbolo", + "token": "Token" + }, + "description": "Acceda a su cuenta de Vulcan a trav\u00e9s de la p\u00e1gina de registro de la aplicaci\u00f3n m\u00f3vil." }, "reauth_confirm": { "data": { - "region": "S\u00edmbolo" - } + "pin": "Pin", + "region": "S\u00edmbolo", + "token": "Token" + }, + "description": "Acceda a su cuenta de Vulcan a trav\u00e9s de la p\u00e1gina de registro de la aplicaci\u00f3n m\u00f3vil." + }, + "select_saved_credentials": { + "data": { + "credentials": "Inicio de sesi\u00f3n" + }, + "description": "Seleccione las credenciales guardadas." }, "select_student": { "data": { "student_name": "Selecciona al alumno" - } + }, + "description": "Seleccione el estudiante, puede a\u00f1adir m\u00e1s estudiantes a\u00f1adiendo de nuevo la integraci\u00f3n." } } } diff --git a/homeassistant/components/webostv/translations/es.json b/homeassistant/components/webostv/translations/es.json index 712de905ddc..db23caa048b 100644 --- a/homeassistant/components/webostv/translations/es.json +++ b/homeassistant/components/webostv/translations/es.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "host": "Anfitri\u00f3n", "name": "Nombre" }, "description": "Encienda la televisi\u00f3n, rellene los siguientes campos y haga clic en enviar", diff --git a/homeassistant/components/whois/translations/es.json b/homeassistant/components/whois/translations/es.json index 0da233e02ad..e2803c251d6 100644 --- a/homeassistant/components/whois/translations/es.json +++ b/homeassistant/components/whois/translations/es.json @@ -4,7 +4,9 @@ "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { + "unexpected_response": "Respuesta inesperada del servidor whois", "unknown_date_format": "Formato de fecha desconocido en la respuesta del servidor whois", + "unknown_tld": "El TLD dado es desconocido o no est\u00e1 disponible para esta integraci\u00f3n", "whois_command_failed": "El comando whois ha fallado: no se pudo obtener la informaci\u00f3n whois" }, "step": { diff --git a/homeassistant/components/yamaha_musiccast/translations/select.pt.json b/homeassistant/components/yamaha_musiccast/translations/select.pt.json new file mode 100644 index 00000000000..059993c8829 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.pt.json @@ -0,0 +1,22 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "Autom\u00e1tico" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "Autom\u00e1tico", + "bypass": "Desviar", + "manual": "Manual" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync_off": "Sincroniza\u00e7\u00e3o de \u00e1udio desligada", + "audio_sync_on": "Sincroniza\u00e7\u00e3o de \u00e1udio ativada", + "balanced": "Equilibrado", + "lip_sync": "Sincroniza\u00e7\u00e3o labial" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "Comprimido", + "uncompressed": "Incomprimido" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/es.json b/homeassistant/components/yolink/translations/es.json new file mode 100644 index 00000000000..71df6ac31e3 --- /dev/null +++ b/homeassistant/components/yolink/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en progreso", + "authorize_url_timeout": "Tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, siga la documentaci\u00f3n.", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [compruebe la secci\u00f3n de ayuda]({docs_url})", + "oauth_error": "Se han recibido datos no v\u00e1lidos del token.", + "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + }, + "create_entry": { + "default": "Autentificado con \u00e9xito" + }, + "step": { + "pick_implementation": { + "title": "Elija el m\u00e9todo de autenticaci\u00f3n" + }, + "reauth_confirm": { + "description": "La integraci\u00f3n de yolink necesita volver a autenticar su cuenta", + "title": "Integraci\u00f3n de la reautenticaci\u00f3n" + } + } + } +} \ No newline at end of file From c6e56c26b33b282470e07913349a0ab91f38a6d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Jun 2022 19:12:00 -1000 Subject: [PATCH 1171/3516] Fix logbook not setting up with an recorder filter that has empty fields (#72869) --- homeassistant/components/recorder/filters.py | 2 +- tests/components/logbook/test_init.py | 38 +++++++++++++++++++- tests/components/recorder/test_filters.py | 20 +++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 0ceb013d8c5..3077f7f57f3 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -36,7 +36,7 @@ def extract_include_exclude_filter_conf(conf: ConfigType) -> dict[str, Any]: """ return { filter_type: { - matcher: set(conf.get(filter_type, {}).get(matcher, [])) + matcher: set(conf.get(filter_type, {}).get(matcher) or []) for matcher in FITLER_MATCHERS } for filter_type in FILTER_TYPES diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 2903f29f5dc..d33bbd5b8ac 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -11,7 +11,7 @@ from unittest.mock import Mock, patch import pytest import voluptuous as vol -from homeassistant.components import logbook +from homeassistant.components import logbook, recorder from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED from homeassistant.components.logbook.models import LazyEventPartialState @@ -2796,3 +2796,39 @@ async def test_get_events_with_context_state(hass, hass_ws_client, recorder_mock assert results[3]["context_state"] == "off" assert results[3]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" assert "context_event_type" not in results[3] + + +async def test_logbook_with_empty_config(hass, recorder_mock): + """Test we handle a empty configuration.""" + assert await async_setup_component( + hass, + logbook.DOMAIN, + { + logbook.DOMAIN: {}, + recorder.DOMAIN: {}, + }, + ) + await hass.async_block_till_done() + + +async def test_logbook_with_non_iterable_entity_filter(hass, recorder_mock): + """Test we handle a non-iterable entity filter.""" + assert await async_setup_component( + hass, + logbook.DOMAIN, + { + logbook.DOMAIN: { + CONF_EXCLUDE: { + CONF_ENTITIES: ["light.additional_excluded"], + } + }, + recorder.DOMAIN: { + CONF_EXCLUDE: { + CONF_ENTITIES: None, + CONF_DOMAINS: None, + CONF_ENTITY_GLOBS: None, + } + }, + }, + ) + await hass.async_block_till_done() diff --git a/tests/components/recorder/test_filters.py b/tests/components/recorder/test_filters.py index fa80df6e345..5c0afa10f9d 100644 --- a/tests/components/recorder/test_filters.py +++ b/tests/components/recorder/test_filters.py @@ -12,6 +12,13 @@ from homeassistant.helpers.entityfilter import ( CONF_INCLUDE, ) +EMPTY_INCLUDE_FILTER = { + CONF_INCLUDE: { + CONF_DOMAINS: None, + CONF_ENTITIES: None, + CONF_ENTITY_GLOBS: None, + } +} SIMPLE_INCLUDE_FILTER = { CONF_INCLUDE: { CONF_DOMAINS: ["homeassistant"], @@ -87,6 +94,19 @@ def test_extract_include_exclude_filter_conf(): assert SIMPLE_INCLUDE_EXCLUDE_FILTER[CONF_EXCLUDE][CONF_ENTITIES] != { "cover.altered" } + empty_include_filter = extract_include_exclude_filter_conf(EMPTY_INCLUDE_FILTER) + assert empty_include_filter == { + CONF_EXCLUDE: { + CONF_DOMAINS: set(), + CONF_ENTITIES: set(), + CONF_ENTITY_GLOBS: set(), + }, + CONF_INCLUDE: { + CONF_DOMAINS: set(), + CONF_ENTITIES: set(), + CONF_ENTITY_GLOBS: set(), + }, + } def test_merge_include_exclude_filters(): From d368b9e24f279d21bafc201c1a8a1b74a049a48e Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 2 Jun 2022 00:12:38 -0500 Subject: [PATCH 1172/3516] Remove announce workaround for Sonos (#72854) --- homeassistant/components/sonos/media_player.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index cd129d82843..f331f980bb4 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -25,7 +25,6 @@ from homeassistant.components.media_player import ( ) from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, - ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, @@ -544,9 +543,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """ # Use 'replace' as the default enqueue option enqueue = kwargs.get(ATTR_MEDIA_ENQUEUE, MediaPlayerEnqueue.REPLACE) - if kwargs.get(ATTR_MEDIA_ANNOUNCE): - # Temporary workaround until announce support is added - enqueue = MediaPlayerEnqueue.PLAY if spotify.is_spotify_media_type(media_type): media_type = spotify.resolve_spotify_media_type(media_type) From f79e5e002bc5c4a691682c5894f0e649ab943d33 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Jun 2022 19:13:09 -1000 Subject: [PATCH 1173/3516] Ensure recorder shuts down when its startup future is canceled out from under it (#72866) --- homeassistant/components/recorder/core.py | 14 +++++++++++--- tests/components/recorder/test_init.py | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 7df4cf57e56..7a096a9c404 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Iterable +from concurrent.futures import CancelledError import contextlib from datetime import datetime, timedelta import logging @@ -518,9 +519,16 @@ class Recorder(threading.Thread): def _wait_startup_or_shutdown(self) -> object | None: """Wait for startup or shutdown before starting.""" - return asyncio.run_coroutine_threadsafe( - self._async_wait_for_started(), self.hass.loop - ).result() + try: + return asyncio.run_coroutine_threadsafe( + self._async_wait_for_started(), self.hass.loop + ).result() + except CancelledError as ex: + _LOGGER.warning( + "Recorder startup was externally canceled before it could complete: %s", + ex, + ) + return SHUTDOWN_TASK def run(self) -> None: """Start processing events to save.""" diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 41c4428ee5e..87dbce3ba3b 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -118,6 +118,26 @@ async def test_shutdown_before_startup_finishes( assert run_info.end is not None +async def test_canceled_before_startup_finishes( + hass: HomeAssistant, + async_setup_recorder_instance: SetupRecorderInstanceT, + caplog: pytest.LogCaptureFixture, +): + """Test recorder shuts down when its startup future is canceled out from under it.""" + hass.state = CoreState.not_running + await async_setup_recorder_instance(hass) + instance = get_instance(hass) + await instance.async_db_ready + instance._hass_started.cancel() + with patch.object(instance, "engine"): + await hass.async_block_till_done() + await hass.async_add_executor_job(instance.join) + assert ( + "Recorder startup was externally canceled before it could complete" + in caplog.text + ) + + async def test_shutdown_closes_connections(hass, recorder_mock): """Test shutdown closes connections.""" From 999b3a4f7b6ee9410dde5a72aa97b39e10f6e555 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 2 Jun 2022 07:48:59 +0200 Subject: [PATCH 1174/3516] Adjust astroid import in pylint plugin (#72841) * import nodes from astroid * Update remaining pylint plugins Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- pylint/plugins/hass_constructor.py | 8 +++-- pylint/plugins/hass_enforce_type_hints.py | 44 +++++++++++------------ pylint/plugins/hass_imports.py | 10 +++--- pylint/plugins/hass_logger.py | 12 ++++--- 4 files changed, 39 insertions(+), 35 deletions(-) diff --git a/pylint/plugins/hass_constructor.py b/pylint/plugins/hass_constructor.py index f0f23ef4c95..525dcfed2e2 100644 --- a/pylint/plugins/hass_constructor.py +++ b/pylint/plugins/hass_constructor.py @@ -1,5 +1,7 @@ """Plugin for constructor definitions.""" -from astroid import Const, FunctionDef +from __future__ import annotations + +from astroid import nodes from pylint.checkers import BaseChecker from pylint.interfaces import IAstroidChecker from pylint.lint import PyLinter @@ -22,7 +24,7 @@ class HassConstructorFormatChecker(BaseChecker): # type: ignore[misc] } options = () - def visit_functiondef(self, node: FunctionDef) -> None: + def visit_functiondef(self, node: nodes.FunctionDef) -> None: """Called when a FunctionDef node is visited.""" if not node.is_method() or node.name != "__init__": return @@ -43,7 +45,7 @@ class HassConstructorFormatChecker(BaseChecker): # type: ignore[misc] return # Check that return type is specified and it is "None". - if not isinstance(node.returns, Const) or node.returns.value is not None: + if not isinstance(node.returns, nodes.Const) or node.returns.value is not None: self.add_message("hass-constructor-return", node=node) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 31b396c3196..05a52faab17 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass import re -import astroid +from astroid import nodes from pylint.checkers import BaseChecker from pylint.interfaces import IAstroidChecker from pylint.lint import PyLinter @@ -437,7 +437,7 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { def _is_valid_type( - expected_type: list[str] | str | None | object, node: astroid.NodeNG + expected_type: list[str] | str | None | object, node: nodes.NodeNG ) -> bool: """Check the argument node against the expected type.""" if expected_type is UNDEFINED: @@ -451,18 +451,18 @@ def _is_valid_type( # Const occurs when the type is None if expected_type is None or expected_type == "None": - return isinstance(node, astroid.Const) and node.value is None + return isinstance(node, nodes.Const) and node.value is None assert isinstance(expected_type, str) # Const occurs when the type is an Ellipsis if expected_type == "...": - return isinstance(node, astroid.Const) and node.value == Ellipsis + return isinstance(node, nodes.Const) and node.value == Ellipsis # Special case for `xxx | yyy` if match := _TYPE_HINT_MATCHERS["a_or_b"].match(expected_type): return ( - isinstance(node, astroid.BinOp) + isinstance(node, nodes.BinOp) and _is_valid_type(match.group(1), node.left) and _is_valid_type(match.group(2), node.right) ) @@ -470,11 +470,11 @@ def _is_valid_type( # Special case for xxx[yyy[zzz, aaa]]` if match := _TYPE_HINT_MATCHERS["x_of_y_of_z_comma_a"].match(expected_type): return ( - isinstance(node, astroid.Subscript) + isinstance(node, nodes.Subscript) and _is_valid_type(match.group(1), node.value) - and isinstance(subnode := node.slice, astroid.Subscript) + and isinstance(subnode := node.slice, nodes.Subscript) and _is_valid_type(match.group(2), subnode.value) - and isinstance(subnode.slice, astroid.Tuple) + and isinstance(subnode.slice, nodes.Tuple) and _is_valid_type(match.group(3), subnode.slice.elts[0]) and _is_valid_type(match.group(4), subnode.slice.elts[1]) ) @@ -482,9 +482,9 @@ def _is_valid_type( # Special case for xxx[yyy, zzz]` if match := _TYPE_HINT_MATCHERS["x_of_y_comma_z"].match(expected_type): return ( - isinstance(node, astroid.Subscript) + isinstance(node, nodes.Subscript) and _is_valid_type(match.group(1), node.value) - and isinstance(node.slice, astroid.Tuple) + and isinstance(node.slice, nodes.Tuple) and _is_valid_type(match.group(2), node.slice.elts[0]) and _is_valid_type(match.group(3), node.slice.elts[1]) ) @@ -492,22 +492,22 @@ def _is_valid_type( # Special case for xxx[yyy]` if match := _TYPE_HINT_MATCHERS["x_of_y"].match(expected_type): return ( - isinstance(node, astroid.Subscript) + isinstance(node, nodes.Subscript) and _is_valid_type(match.group(1), node.value) and _is_valid_type(match.group(2), node.slice) ) # Name occurs when a namespace is not used, eg. "HomeAssistant" - if isinstance(node, astroid.Name) and node.name == expected_type: + if isinstance(node, nodes.Name) and node.name == expected_type: return True # Attribute occurs when a namespace is used, eg. "core.HomeAssistant" - return isinstance(node, astroid.Attribute) and node.attrname == expected_type + return isinstance(node, nodes.Attribute) and node.attrname == expected_type -def _get_all_annotations(node: astroid.FunctionDef) -> list[astroid.NodeNG | None]: +def _get_all_annotations(node: nodes.FunctionDef) -> list[nodes.NodeNG | None]: args = node.args - annotations: list[astroid.NodeNG | None] = ( + annotations: list[nodes.NodeNG | None] = ( args.posonlyargs_annotations + args.annotations + args.kwonlyargs_annotations ) if args.vararg is not None: @@ -518,7 +518,7 @@ def _get_all_annotations(node: astroid.FunctionDef) -> list[astroid.NodeNG | Non def _has_valid_annotations( - annotations: list[astroid.NodeNG | None], + annotations: list[nodes.NodeNG | None], ) -> bool: for annotation in annotations: if annotation is not None: @@ -563,7 +563,7 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] self._function_matchers: list[TypeHintMatch] = [] self._class_matchers: list[ClassTypeHintMatch] = [] - def visit_module(self, node: astroid.Module) -> None: + def visit_module(self, node: nodes.Module) -> None: """Called when a Module node is visited.""" self._function_matchers = [] self._class_matchers = [] @@ -580,16 +580,16 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] if class_matches := _CLASS_MATCH.get(module_platform): self._class_matchers = class_matches - def visit_classdef(self, node: astroid.ClassDef) -> None: + def visit_classdef(self, node: nodes.ClassDef) -> None: """Called when a ClassDef node is visited.""" - ancestor: astroid.ClassDef + ancestor: nodes.ClassDef for ancestor in node.ancestors(): for class_matches in self._class_matchers: if ancestor.name == class_matches.base_class: self._visit_class_functions(node, class_matches.matches) def _visit_class_functions( - self, node: astroid.ClassDef, matches: list[TypeHintMatch] + self, node: nodes.ClassDef, matches: list[TypeHintMatch] ) -> None: for match in matches: for function_node in node.mymethods(): @@ -597,7 +597,7 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] if match.function_name == function_name: self._check_function(function_node, match) - def visit_functiondef(self, node: astroid.FunctionDef) -> None: + def visit_functiondef(self, node: nodes.FunctionDef) -> None: """Called when a FunctionDef node is visited.""" for match in self._function_matchers: if node.name != match.function_name or node.is_method(): @@ -606,7 +606,7 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] visit_asyncfunctiondef = visit_functiondef - def _check_function(self, node: astroid.FunctionDef, match: TypeHintMatch) -> None: + def _check_function(self, node: nodes.FunctionDef, match: TypeHintMatch) -> None: # Check that at least one argument is annotated. annotations = _get_all_annotations(node) if node.returns is None and not _has_valid_annotations(annotations): diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index c6f6c25c7b6..a8b3fa8fc76 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass import re -from astroid import Import, ImportFrom, Module +from astroid import nodes from pylint.checkers import BaseChecker from pylint.interfaces import IAstroidChecker from pylint.lint import PyLinter @@ -14,7 +14,7 @@ from pylint.lint import PyLinter class ObsoleteImportMatch: """Class for pattern matching.""" - constant: re.Pattern + constant: re.Pattern[str] reason: str @@ -255,7 +255,7 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] super().__init__(linter) self.current_package: str | None = None - def visit_module(self, node: Module) -> None: + def visit_module(self, node: nodes.Module) -> None: """Called when a Module node is visited.""" if node.package: self.current_package = node.name @@ -263,13 +263,13 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] # Strip name of the current module self.current_package = node.name[: node.name.rfind(".")] - def visit_import(self, node: Import) -> None: + def visit_import(self, node: nodes.Import) -> None: """Called when a Import node is visited.""" for module, _alias in node.names: if module.startswith(f"{self.current_package}."): self.add_message("hass-relative-import", node=node) - def visit_importfrom(self, node: ImportFrom) -> None: + def visit_importfrom(self, node: nodes.ImportFrom) -> None: """Called when a ImportFrom node is visited.""" if node.level is not None: return diff --git a/pylint/plugins/hass_logger.py b/pylint/plugins/hass_logger.py index 0ca57b8da19..125c927ec42 100644 --- a/pylint/plugins/hass_logger.py +++ b/pylint/plugins/hass_logger.py @@ -1,5 +1,7 @@ """Plugin for logger invocations.""" -import astroid +from __future__ import annotations + +from astroid import nodes from pylint.checkers import BaseChecker from pylint.interfaces import IAstroidChecker from pylint.lint import PyLinter @@ -29,10 +31,10 @@ class HassLoggerFormatChecker(BaseChecker): # type: ignore[misc] } options = () - def visit_call(self, node: astroid.Call) -> None: + def visit_call(self, node: nodes.Call) -> None: """Called when a Call node is visited.""" - if not isinstance(node.func, astroid.Attribute) or not isinstance( - node.func.expr, astroid.Name + if not isinstance(node.func, nodes.Attribute) or not isinstance( + node.func.expr, nodes.Name ): return @@ -44,7 +46,7 @@ class HassLoggerFormatChecker(BaseChecker): # type: ignore[misc] first_arg = node.args[0] - if not isinstance(first_arg, astroid.Const) or not first_arg.value: + if not isinstance(first_arg, nodes.Const) or not first_arg.value: return log_message = first_arg.value From c2fdac20146e58cebf02259b946f11e1d6ed3ab9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 2 Jun 2022 09:06:22 +0200 Subject: [PATCH 1175/3516] Allow non-async functions in device automation (#72147) * Remove async requirement for get_capabilities_func * Add comment * Remove async requirement for get_automations_func * Update homeassistant/components/device_automation/__init__.py Co-authored-by: Erik Montnemery * Update homeassistant/components/device_automation/__init__.py Co-authored-by: Erik Montnemery * Add Exception to type hint Co-authored-by: Erik Montnemery --- .../components/device_automation/__init__.py | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 61fd93354fe..99629f3dd23 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -189,22 +189,42 @@ def _async_set_entity_device_automation_metadata( automation["metadata"]["secondary"] = bool(entry.entity_category or entry.hidden_by) +async def _async_get_automation_for_device( + hass: HomeAssistant, + platform: DeviceAutomationPlatformType, + function_name: str, + device_id: str, +) -> list[dict[str, Any]]: + """List device automations.""" + automations = getattr(platform, function_name)(hass, device_id) + if asyncio.iscoroutine(automations): + # Using a coroutine to get device automations is deprecated + # enable warning when core is fully migrated + # then remove in Home Assistant Core xxxx.xx + return await automations # type: ignore[no-any-return] + return automations # type: ignore[no-any-return] + + async def _async_get_device_automations_from_domain( - hass, domain, automation_type, device_ids, return_exceptions -): + hass: HomeAssistant, + domain: str, + automation_type: DeviceAutomationType, + device_ids: Iterable[str], + return_exceptions: bool, +) -> list[list[dict[str, Any]] | Exception]: """List device automations.""" try: platform = await async_get_device_automation_platform( hass, domain, automation_type ) except InvalidDeviceAutomationConfig: - return {} + return [] function_name = automation_type.value.get_automations_func - return await asyncio.gather( + return await asyncio.gather( # type: ignore[no-any-return] *( - getattr(platform, function_name)(hass, device_id) + _async_get_automation_for_device(hass, platform, function_name, device_id) for device_id in device_ids ), return_exceptions=return_exceptions, @@ -290,7 +310,12 @@ async def _async_get_device_automation_capabilities( return {} try: - capabilities = await getattr(platform, function_name)(hass, automation) + capabilities = getattr(platform, function_name)(hass, automation) + if asyncio.iscoroutine(capabilities): + # Using a coroutine to get device automation capabitilites is deprecated + # enable warning when core is fully migrated + # then remove in Home Assistant Core xxxx.xx + capabilities = await capabilities except InvalidDeviceAutomationConfig: return {} From 6ccaf33bdfebefa47680cc51c412ac9e7e44570d Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 2 Jun 2022 03:16:00 -0400 Subject: [PATCH 1176/3516] Attempt to fix flaky tomorrowio test (#72890) * Fix flaky tomorrowio test * reset mock outside context manager * add to hass outside of context manager --- tests/components/tomorrowio/test_init.py | 56 +++++++++++++++--------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/tests/components/tomorrowio/test_init.py b/tests/components/tomorrowio/test_init.py index 27372094092..2c0a882ff5a 100644 --- a/tests/components/tomorrowio/test_init.py +++ b/tests/components/tomorrowio/test_init.py @@ -66,26 +66,31 @@ async def test_update_intervals( version=1, ) config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - assert len(tomorrowio_config_entry_update.call_args_list) == 1 + with patch("homeassistant.helpers.update_coordinator.utcnow", return_value=now): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(tomorrowio_config_entry_update.call_args_list) == 1 + tomorrowio_config_entry_update.reset_mock() # Before the update interval, no updates yet - async_fire_time_changed(hass, now + timedelta(minutes=30)) - await hass.async_block_till_done() - assert len(tomorrowio_config_entry_update.call_args_list) == 0 + future = now + timedelta(minutes=30) + with patch("homeassistant.helpers.update_coordinator.utcnow", return_value=future): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + assert len(tomorrowio_config_entry_update.call_args_list) == 0 - # On the update interval, we get a new update - async_fire_time_changed(hass, now + timedelta(minutes=32)) - await hass.async_block_till_done() - assert len(tomorrowio_config_entry_update.call_args_list) == 1 tomorrowio_config_entry_update.reset_mock() - with patch( - "homeassistant.helpers.update_coordinator.utcnow", - return_value=now + timedelta(minutes=32), - ): + # On the update interval, we get a new update + future = now + timedelta(minutes=32) + with patch("homeassistant.helpers.update_coordinator.utcnow", return_value=future): + async_fire_time_changed(hass, now + timedelta(minutes=32)) + await hass.async_block_till_done() + assert len(tomorrowio_config_entry_update.call_args_list) == 1 + + tomorrowio_config_entry_update.reset_mock() + # Adding a second config entry should cause the update interval to double config_entry_2 = MockConfigEntry( domain=DOMAIN, @@ -101,17 +106,26 @@ async def test_update_intervals( # We should get an immediate call once the new config entry is setup for a # partial update assert len(tomorrowio_config_entry_update.call_args_list) == 1 - tomorrowio_config_entry_update.reset_mock() + + tomorrowio_config_entry_update.reset_mock() # We should get no new calls on our old interval - async_fire_time_changed(hass, now + timedelta(minutes=64)) - await hass.async_block_till_done() - assert len(tomorrowio_config_entry_update.call_args_list) == 0 + future = now + timedelta(minutes=64) + with patch("homeassistant.helpers.update_coordinator.utcnow", return_value=future): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + assert len(tomorrowio_config_entry_update.call_args_list) == 0 + + tomorrowio_config_entry_update.reset_mock() # We should get two calls on our new interval, one for each entry - async_fire_time_changed(hass, now + timedelta(minutes=96)) - await hass.async_block_till_done() - assert len(tomorrowio_config_entry_update.call_args_list) == 2 + future = now + timedelta(minutes=96) + with patch("homeassistant.helpers.update_coordinator.utcnow", return_value=future): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + assert len(tomorrowio_config_entry_update.call_args_list) == 2 + + tomorrowio_config_entry_update.reset_mock() async def test_climacell_migration_logic( From 62a5854e40cb554fecb1eec897d7bcb4c94628fe Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 2 Jun 2022 13:58:04 +0200 Subject: [PATCH 1177/3516] Fix bare except (#72906) --- homeassistant/components/feedreader/__init__.py | 4 ++-- homeassistant/components/qnap/sensor.py | 2 +- tests/components/emulated_hue/test_upnp.py | 2 +- tests/components/system_log/test_init.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index a4cd546aa16..11a3d4b0498 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -199,7 +199,7 @@ class StoredData: with self._lock, open(self._data_file, "rb") as myfile: self._data = pickle.load(myfile) or {} self._cache_outdated = False - except: # noqa: E722 pylint: disable=bare-except + except Exception: # pylint: disable=broad-except _LOGGER.error( "Error loading data from pickled file %s", self._data_file ) @@ -221,6 +221,6 @@ class StoredData: ) try: pickle.dump(self._data, myfile) - except: # noqa: E722 pylint: disable=bare-except + except Exception: # pylint: disable=broad-except _LOGGER.error("Error saving pickled data to %s", self._data_file) self._cache_outdated = True diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index 1e2bf5b6892..7366dc5dc41 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -306,7 +306,7 @@ class QNAPStatsAPI: self.data["smart_drive_health"] = self._api.get_smart_disk_health() self.data["volumes"] = self._api.get_volumes() self.data["bandwidth"] = self._api.get_bandwidth() - except: # noqa: E722 pylint: disable=bare-except + except Exception: # pylint: disable=broad-except _LOGGER.exception("Failed to fetch QNAP stats from the NAS") diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index f392cfaf90d..ce7f013963c 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -162,7 +162,7 @@ async def test_description_xml(hass, hue_client): root = ET.fromstring(await result.text()) ns = {"s": "urn:schemas-upnp-org:device-1-0"} assert root.find("./s:device/s:serialNumber", ns).text == "001788FFFE23BFC2" - except: # noqa: E722 pylint: disable=bare-except + except Exception: # pylint: disable=broad-except pytest.fail("description.xml is not valid XML!") diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 8b9284a4b32..121c29d2eed 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -53,7 +53,7 @@ async def get_error_log(hass_ws_client): def _generate_and_log_exception(exception, log): try: raise Exception(exception) - except: # noqa: E722 pylint: disable=bare-except + except Exception: # pylint: disable=broad-except _LOGGER.exception(log) From 219200b3403e7750fafe838541ffb46548e9152a Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 2 Jun 2022 13:59:29 +0200 Subject: [PATCH 1178/3516] Fix test_hass_stop in devolo Home Network (#72833) Fix test_hass_stop --- tests/components/devolo_home_network/test_init.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/devolo_home_network/test_init.py b/tests/components/devolo_home_network/test_init.py index 66d32e8974d..4f0c5b3fb58 100644 --- a/tests/components/devolo_home_network/test_init.py +++ b/tests/components/devolo_home_network/test_init.py @@ -58,4 +58,5 @@ async def test_hass_stop(hass: HomeAssistant): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - assert async_disconnect.assert_called_once + await hass.async_block_till_done() + async_disconnect.assert_called_once() From cd590c79e2a67f953a3631d783aa466915fc9ad2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Jun 2022 02:01:06 -1000 Subject: [PATCH 1179/3516] Fix migration of MySQL data when InnoDB is not being used (#72893) Fixes #72883 --- homeassistant/components/recorder/migration.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index bc636d34b10..cc5af684566 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -715,14 +715,13 @@ def _apply_update( # noqa: C901 if engine.dialect.name == SupportedDialect.MYSQL: # Ensure the row format is dynamic or the index # unique will be too large - with session_scope(session=session_maker()) as session: - connection = session.connection() - # This is safe to run multiple times and fast since the table is small - connection.execute( - text( - "ALTER TABLE statistics_meta ENGINE=InnoDB, ROW_FORMAT=DYNAMIC" + with contextlib.suppress(SQLAlchemyError): + with session_scope(session=session_maker()) as session: + connection = session.connection() + # This is safe to run multiple times and fast since the table is small + connection.execute( + text("ALTER TABLE statistics_meta ROW_FORMAT=DYNAMIC") ) - ) try: _create_index( session_maker, "statistics_meta", "ix_statistics_meta_statistic_id" From 14f47c7450fbdfb1beec6059a495b9a1f970b5a0 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 2 Jun 2022 08:06:59 -0400 Subject: [PATCH 1180/3516] Bump aiopyarr to 2022.6.0 (#72870) --- homeassistant/components/sonarr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonarr/manifest.json b/homeassistant/components/sonarr/manifest.json index 6a9b00d2041..6b34b077888 100644 --- a/homeassistant/components/sonarr/manifest.json +++ b/homeassistant/components/sonarr/manifest.json @@ -3,7 +3,7 @@ "name": "Sonarr", "documentation": "https://www.home-assistant.io/integrations/sonarr", "codeowners": ["@ctalkington"], - "requirements": ["aiopyarr==22.2.2"], + "requirements": ["aiopyarr==22.6.0"], "config_flow": true, "quality_scale": "silver", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index e50da9b1758..250551caa47 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -220,7 +220,7 @@ aiopvapi==1.6.19 aiopvpc==3.0.0 # homeassistant.components.sonarr -aiopyarr==22.2.2 +aiopyarr==22.6.0 # homeassistant.components.qnap_qsw aioqsw==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3fefd79973..5e9aa909f3d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -189,7 +189,7 @@ aiopvapi==1.6.19 aiopvpc==3.0.0 # homeassistant.components.sonarr -aiopyarr==22.2.2 +aiopyarr==22.6.0 # homeassistant.components.qnap_qsw aioqsw==0.1.0 From 756988fe200d625bfda8ccb628220323884faa6c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 2 Jun 2022 14:17:09 +0200 Subject: [PATCH 1181/3516] Use Mapping for async_step_reauth (f-o) (#72764) --- homeassistant/components/fritz/config_flow.py | 3 ++- homeassistant/components/fritzbox/config_flow.py | 3 ++- homeassistant/components/hyperion/config_flow.py | 3 ++- homeassistant/components/isy994/config_flow.py | 3 ++- homeassistant/components/laundrify/config_flow.py | 5 ++--- homeassistant/components/meater/config_flow.py | 5 ++++- homeassistant/components/nam/config_flow.py | 3 ++- homeassistant/components/nanoleaf/config_flow.py | 3 ++- homeassistant/components/notion/config_flow.py | 3 ++- homeassistant/components/overkiz/config_flow.py | 7 +++---- 10 files changed, 23 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index 3e6961f585d..afb1708cac1 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure the FRITZ!Box Tools integration.""" from __future__ import annotations +from collections.abc import Mapping import ipaddress import logging import socket @@ -230,7 +231,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_create_entry() - async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle flow upon an API authentication error.""" self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) self._host = data[CONF_HOST] diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index bf290cb28f7..d183c4a8d5e 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -1,6 +1,7 @@ """Config flow for AVM FRITZ!SmartHome.""" from __future__ import annotations +from collections.abc import Mapping import ipaddress from typing import Any from urllib.parse import urlparse @@ -175,7 +176,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, data: dict[str, str]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Trigger a reauthentication flow.""" entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) assert entry is not None diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index 4d6253cb161..8b8f8b62c47 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping from contextlib import suppress import logging from typing import Any @@ -142,7 +143,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth( self, - config_data: dict[str, Any], + config_data: Mapping[str, Any], ) -> FlowResult: """Handle a reauthentication flow.""" self._data = dict(config_data) diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 34d4738db68..ff14c9bfc33 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Universal Devices ISY994 integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any from urllib.parse import urlparse, urlunparse @@ -254,7 +255,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() async def async_step_reauth( - self, user_input: dict[str, Any] | None = None + self, data: Mapping[str, Any] ) -> data_entry_flow.FlowResult: """Handle reauth.""" self._existing_entry = await self.async_set_unique_id(self.context["unique_id"]) diff --git a/homeassistant/components/laundrify/config_flow.py b/homeassistant/components/laundrify/config_flow.py index d8230863d7c..c091324d9a7 100644 --- a/homeassistant/components/laundrify/config_flow.py +++ b/homeassistant/components/laundrify/config_flow.py @@ -1,6 +1,7 @@ """Config flow for laundrify integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -76,9 +77,7 @@ class LaundrifyConfigFlow(ConfigFlow, domain=DOMAIN): step_id="init", data_schema=CONFIG_SCHEMA, errors=errors ) - async def async_step_reauth( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/meater/config_flow.py b/homeassistant/components/meater/config_flow.py index 07dbd4bd4a5..8d5e459fbce 100644 --- a/homeassistant/components/meater/config_flow.py +++ b/homeassistant/components/meater/config_flow.py @@ -1,6 +1,9 @@ """Config flow for Meater.""" from __future__ import annotations +from collections.abc import Mapping +from typing import Any + from meater import AuthenticationError, MeaterApi, ServiceUnavailableError import voluptuous as vol @@ -42,7 +45,7 @@ class MeaterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self._try_connect_meater("user", None, username, password) - async def async_step_reauth(self, data: dict[str, str]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._data_schema = REAUTH_SCHEMA self._username = data[CONF_USERNAME] diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py index 1727ddff162..df41eb7c5f1 100644 --- a/homeassistant/components/nam/config_flow.py +++ b/homeassistant/components/nam/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping import logging from typing import Any @@ -164,7 +165,7 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" if entry := self.hass.config_entries.async_get_entry(self.context["entry_id"]): self.entry = entry diff --git a/homeassistant/components/nanoleaf/config_flow.py b/homeassistant/components/nanoleaf/config_flow.py index ed63754697a..eb8bbb1ec66 100644 --- a/homeassistant/components/nanoleaf/config_flow.py +++ b/homeassistant/components/nanoleaf/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Nanoleaf integration.""" from __future__ import annotations +from collections.abc import Mapping import logging import os from typing import Any, Final, cast @@ -77,7 +78,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_step_link() - async def async_step_reauth(self, data: dict[str, str]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle Nanoleaf reauth flow if token is invalid.""" self.reauth_entry = cast( config_entries.ConfigEntry, diff --git a/homeassistant/components/notion/config_flow.py b/homeassistant/components/notion/config_flow.py index c9e59107c1b..56093067711 100644 --- a/homeassistant/components/notion/config_flow.py +++ b/homeassistant/components/notion/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure the Notion integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import TYPE_CHECKING, Any from aionotion import async_get_client @@ -73,7 +74,7 @@ class NotionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=self._username, data=data) - async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._username = config[CONF_USERNAME] return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/overkiz/config_flow.py b/homeassistant/components/overkiz/config_flow.py index 479e212e317..c70a551f4c7 100644 --- a/homeassistant/components/overkiz/config_flow.py +++ b/homeassistant/components/overkiz/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Overkiz (by Somfy) integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any, cast from aiohttp import ClientError @@ -154,9 +155,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() - async def async_step_reauth( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle reauth.""" self._config_entry = cast( ConfigEntry, @@ -170,4 +169,4 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._default_user = self._config_entry.data[CONF_USERNAME] self._default_hub = self._config_entry.data[CONF_HUB] - return await self.async_step_user(user_input) + return await self.async_step_user(dict(data)) From 52561ce0769ddcf1e8688c8909692b66495e524b Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 2 Jun 2022 14:24:46 +0200 Subject: [PATCH 1182/3516] Update MQTT tests to use the config entry setup (#72373) * New testframework and tests for fan platform * Merge test_common_new to test_common * Add alarm_control_panel * Add binary_sensor * Add button * Add camera * Add climate * Add config_flow * Add cover * Add device_tracker_disovery * Add device_trigger * Add diagnostics * Add discovery * Add humidifier * Add init * Add lecacy_vacuum * Add light_json * Add light_template * Add light * Add lock * Add number * Add scene * Add select * Add sensor * Add siren * Add state_vacuum * Add subscription * Add switch * Add tag * Add trigger * Add missed tests * Add another missed test * Add device_tracker * Remove commented out code * Correct tests according comments * Improve mqtt_mock_entry and recover tests * Split fixtures with and without yaml setup * Update fixtures manual_mqtt * Update fixtures mqtt_json * Fix test tasmota * Update fixture mqtt_room * Revert fixture changes, improve test * re-add test --- .../manual_mqtt/test_alarm_control_panel.py | 117 ++++-- .../mqtt/test_alarm_control_panel.py | 248 +++++++---- tests/components/mqtt/test_binary_sensor.py | 231 +++++++---- tests/components/mqtt/test_button.py | 146 ++++--- tests/components/mqtt/test_camera.py | 133 +++--- tests/components/mqtt/test_climate.py | 296 +++++++++----- tests/components/mqtt/test_common.py | 145 +++++-- tests/components/mqtt/test_config_flow.py | 11 +- tests/components/mqtt/test_cover.py | 385 ++++++++++++------ tests/components/mqtt/test_device_tracker.py | 34 +- .../mqtt/test_device_tracker_discovery.py | 58 ++- tests/components/mqtt/test_device_trigger.py | 102 +++-- tests/components/mqtt/test_diagnostics.py | 10 +- tests/components/mqtt/test_discovery.py | 111 +++-- tests/components/mqtt/test_fan.py | 214 +++++++--- tests/components/mqtt/test_humidifier.py | 202 ++++++--- tests/components/mqtt/test_init.py | 280 +++++++++---- tests/components/mqtt/test_legacy_vacuum.py | 204 ++++++---- tests/components/mqtt/test_light.py | 290 +++++++++---- tests/components/mqtt/test_light_json.py | 247 +++++++---- tests/components/mqtt/test_light_template.py | 260 +++++++----- tests/components/mqtt/test_lock.py | 169 +++++--- tests/components/mqtt/test_number.py | 171 +++++--- tests/components/mqtt/test_scene.py | 70 +++- tests/components/mqtt/test_select.py | 170 +++++--- tests/components/mqtt/test_sensor.py | 280 ++++++++----- tests/components/mqtt/test_siren.py | 181 +++++--- tests/components/mqtt/test_state_vacuum.py | 153 ++++--- tests/components/mqtt/test_subscription.py | 19 +- tests/components/mqtt/test_switch.py | 174 +++++--- tests/components/mqtt/test_tag.py | 72 +++- tests/components/mqtt/test_trigger.py | 11 +- .../mqtt_json/test_device_tracker.py | 2 +- tests/components/mqtt_room/test_sensor.py | 2 +- tests/components/tasmota/test_discovery.py | 6 +- tests/conftest.py | 100 ++++- 36 files changed, 3612 insertions(+), 1692 deletions(-) diff --git a/tests/components/manual_mqtt/test_alarm_control_panel.py b/tests/components/manual_mqtt/test_alarm_control_panel.py index 3572077bd8e..4296f76d741 100644 --- a/tests/components/manual_mqtt/test_alarm_control_panel.py +++ b/tests/components/manual_mqtt/test_alarm_control_panel.py @@ -26,9 +26,9 @@ from tests.components.alarm_control_panel import common CODE = "HELLO_CODE" -async def test_fail_setup_without_state_topic(hass, mqtt_mock): +async def test_fail_setup_without_state_topic(hass, mqtt_mock_entry_with_yaml_config): """Test for failing with no state topic.""" - with assert_setup_component(0) as config: + with assert_setup_component(0, alarm_control_panel.DOMAIN) as config: assert await async_setup_component( hass, alarm_control_panel.DOMAIN, @@ -42,9 +42,9 @@ async def test_fail_setup_without_state_topic(hass, mqtt_mock): assert not config[alarm_control_panel.DOMAIN] -async def test_fail_setup_without_command_topic(hass, mqtt_mock): +async def test_fail_setup_without_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test failing with no command topic.""" - with assert_setup_component(0): + with assert_setup_component(0, alarm_control_panel.DOMAIN): assert await async_setup_component( hass, alarm_control_panel.DOMAIN, @@ -57,7 +57,7 @@ async def test_fail_setup_without_command_topic(hass, mqtt_mock): ) -async def test_arm_home_no_pending(hass, mqtt_mock): +async def test_arm_home_no_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -86,7 +86,9 @@ async def test_arm_home_no_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME -async def test_arm_home_no_pending_when_code_not_req(hass, mqtt_mock): +async def test_arm_home_no_pending_when_code_not_req( + hass, mqtt_mock_entry_with_yaml_config +): """Test arm home method.""" assert await async_setup_component( hass, @@ -116,7 +118,7 @@ async def test_arm_home_no_pending_when_code_not_req(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME -async def test_arm_home_with_pending(hass, mqtt_mock): +async def test_arm_home_with_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -158,7 +160,7 @@ async def test_arm_home_with_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME -async def test_arm_home_with_invalid_code(hass, mqtt_mock): +async def test_arm_home_with_invalid_code(hass, mqtt_mock_entry_with_yaml_config): """Attempt to arm home without a valid code.""" assert await async_setup_component( hass, @@ -187,7 +189,7 @@ async def test_arm_home_with_invalid_code(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_arm_away_no_pending(hass, mqtt_mock): +async def test_arm_away_no_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -216,7 +218,9 @@ async def test_arm_away_no_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY -async def test_arm_away_no_pending_when_code_not_req(hass, mqtt_mock): +async def test_arm_away_no_pending_when_code_not_req( + hass, mqtt_mock_entry_with_yaml_config +): """Test arm home method.""" assert await async_setup_component( hass, @@ -246,7 +250,7 @@ async def test_arm_away_no_pending_when_code_not_req(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY -async def test_arm_home_with_template_code(hass, mqtt_mock): +async def test_arm_home_with_template_code(hass, mqtt_mock_entry_with_yaml_config): """Attempt to arm with a template-based code.""" assert await async_setup_component( hass, @@ -276,7 +280,7 @@ async def test_arm_home_with_template_code(hass, mqtt_mock): assert state.state == STATE_ALARM_ARMED_HOME -async def test_arm_away_with_pending(hass, mqtt_mock): +async def test_arm_away_with_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -318,7 +322,7 @@ async def test_arm_away_with_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY -async def test_arm_away_with_invalid_code(hass, mqtt_mock): +async def test_arm_away_with_invalid_code(hass, mqtt_mock_entry_with_yaml_config): """Attempt to arm away without a valid code.""" assert await async_setup_component( hass, @@ -347,7 +351,7 @@ async def test_arm_away_with_invalid_code(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_arm_night_no_pending(hass, mqtt_mock): +async def test_arm_night_no_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm night method.""" assert await async_setup_component( hass, @@ -376,7 +380,9 @@ async def test_arm_night_no_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT -async def test_arm_night_no_pending_when_code_not_req(hass, mqtt_mock): +async def test_arm_night_no_pending_when_code_not_req( + hass, mqtt_mock_entry_with_yaml_config +): """Test arm night method.""" assert await async_setup_component( hass, @@ -406,7 +412,7 @@ async def test_arm_night_no_pending_when_code_not_req(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT -async def test_arm_night_with_pending(hass, mqtt_mock): +async def test_arm_night_with_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm night method.""" assert await async_setup_component( hass, @@ -454,7 +460,7 @@ async def test_arm_night_with_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT -async def test_arm_night_with_invalid_code(hass, mqtt_mock): +async def test_arm_night_with_invalid_code(hass, mqtt_mock_entry_with_yaml_config): """Attempt to arm night without a valid code.""" assert await async_setup_component( hass, @@ -483,7 +489,7 @@ async def test_arm_night_with_invalid_code(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_no_pending(hass, mqtt_mock): +async def test_trigger_no_pending(hass, mqtt_mock_entry_with_yaml_config): """Test triggering when no pending submitted method.""" assert await async_setup_component( hass, @@ -521,7 +527,7 @@ async def test_trigger_no_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED -async def test_trigger_with_delay(hass, mqtt_mock): +async def test_trigger_with_delay(hass, mqtt_mock_entry_with_yaml_config): """Test trigger method and switch from pending to triggered.""" assert await async_setup_component( hass, @@ -569,7 +575,7 @@ async def test_trigger_with_delay(hass, mqtt_mock): assert state.state == STATE_ALARM_TRIGGERED -async def test_trigger_zero_trigger_time(hass, mqtt_mock): +async def test_trigger_zero_trigger_time(hass, mqtt_mock_entry_with_yaml_config): """Test disabled trigger.""" assert await async_setup_component( hass, @@ -598,7 +604,9 @@ async def test_trigger_zero_trigger_time(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_zero_trigger_time_with_pending(hass, mqtt_mock): +async def test_trigger_zero_trigger_time_with_pending( + hass, mqtt_mock_entry_with_yaml_config +): """Test disabled trigger.""" assert await async_setup_component( hass, @@ -627,7 +635,7 @@ async def test_trigger_zero_trigger_time_with_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_with_pending(hass, mqtt_mock): +async def test_trigger_with_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -679,7 +687,9 @@ async def test_trigger_with_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_with_disarm_after_trigger(hass, mqtt_mock): +async def test_trigger_with_disarm_after_trigger( + hass, mqtt_mock_entry_with_yaml_config +): """Test disarm after trigger.""" assert await async_setup_component( hass, @@ -718,7 +728,9 @@ async def test_trigger_with_disarm_after_trigger(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_with_zero_specific_trigger_time(hass, mqtt_mock): +async def test_trigger_with_zero_specific_trigger_time( + hass, mqtt_mock_entry_with_yaml_config +): """Test trigger method.""" assert await async_setup_component( hass, @@ -748,7 +760,9 @@ async def test_trigger_with_zero_specific_trigger_time(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_with_unused_zero_specific_trigger_time(hass, mqtt_mock): +async def test_trigger_with_unused_zero_specific_trigger_time( + hass, mqtt_mock_entry_with_yaml_config +): """Test disarm after trigger.""" assert await async_setup_component( hass, @@ -788,7 +802,9 @@ async def test_trigger_with_unused_zero_specific_trigger_time(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_with_specific_trigger_time(hass, mqtt_mock): +async def test_trigger_with_specific_trigger_time( + hass, mqtt_mock_entry_with_yaml_config +): """Test disarm after trigger.""" assert await async_setup_component( hass, @@ -827,7 +843,9 @@ async def test_trigger_with_specific_trigger_time(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass, mqtt_mock): +async def test_back_to_back_trigger_with_no_disarm_after_trigger( + hass, mqtt_mock_entry_with_yaml_config +): """Test no disarm after back to back trigger.""" assert await async_setup_component( hass, @@ -886,7 +904,7 @@ async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass, mqtt_mock assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY -async def test_disarm_while_pending_trigger(hass, mqtt_mock): +async def test_disarm_while_pending_trigger(hass, mqtt_mock_entry_with_yaml_config): """Test disarming while pending state.""" assert await async_setup_component( hass, @@ -929,7 +947,9 @@ async def test_disarm_while_pending_trigger(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_disarm_during_trigger_with_invalid_code(hass, mqtt_mock): +async def test_disarm_during_trigger_with_invalid_code( + hass, mqtt_mock_entry_with_yaml_config +): """Test disarming while code is invalid.""" assert await async_setup_component( hass, @@ -973,7 +993,9 @@ async def test_disarm_during_trigger_with_invalid_code(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED -async def test_trigger_with_unused_specific_delay(hass, mqtt_mock): +async def test_trigger_with_unused_specific_delay( + hass, mqtt_mock_entry_with_yaml_config +): """Test trigger method and switch from pending to triggered.""" assert await async_setup_component( hass, @@ -1022,7 +1044,7 @@ async def test_trigger_with_unused_specific_delay(hass, mqtt_mock): assert state.state == STATE_ALARM_TRIGGERED -async def test_trigger_with_specific_delay(hass, mqtt_mock): +async def test_trigger_with_specific_delay(hass, mqtt_mock_entry_with_yaml_config): """Test trigger method and switch from pending to triggered.""" assert await async_setup_component( hass, @@ -1071,7 +1093,7 @@ async def test_trigger_with_specific_delay(hass, mqtt_mock): assert state.state == STATE_ALARM_TRIGGERED -async def test_trigger_with_pending_and_delay(hass, mqtt_mock): +async def test_trigger_with_pending_and_delay(hass, mqtt_mock_entry_with_yaml_config): """Test trigger method and switch from pending to triggered.""" assert await async_setup_component( hass, @@ -1132,7 +1154,9 @@ async def test_trigger_with_pending_and_delay(hass, mqtt_mock): assert state.state == STATE_ALARM_TRIGGERED -async def test_trigger_with_pending_and_specific_delay(hass, mqtt_mock): +async def test_trigger_with_pending_and_specific_delay( + hass, mqtt_mock_entry_with_yaml_config +): """Test trigger method and switch from pending to triggered.""" assert await async_setup_component( hass, @@ -1194,7 +1218,7 @@ async def test_trigger_with_pending_and_specific_delay(hass, mqtt_mock): assert state.state == STATE_ALARM_TRIGGERED -async def test_armed_home_with_specific_pending(hass, mqtt_mock): +async def test_armed_home_with_specific_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -1230,7 +1254,7 @@ async def test_armed_home_with_specific_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME -async def test_armed_away_with_specific_pending(hass, mqtt_mock): +async def test_armed_away_with_specific_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -1266,7 +1290,9 @@ async def test_armed_away_with_specific_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY -async def test_armed_night_with_specific_pending(hass, mqtt_mock): +async def test_armed_night_with_specific_pending( + hass, mqtt_mock_entry_with_yaml_config +): """Test arm home method.""" assert await async_setup_component( hass, @@ -1302,7 +1328,7 @@ async def test_armed_night_with_specific_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT -async def test_trigger_with_specific_pending(hass, mqtt_mock): +async def test_trigger_with_specific_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -1350,7 +1376,7 @@ async def test_trigger_with_specific_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_arm_away_after_disabled_disarmed(hass, mqtt_mock): +async def test_arm_away_after_disabled_disarmed(hass, mqtt_mock_entry_with_yaml_config): """Test pending state with and without zero trigger time.""" assert await async_setup_component( hass, @@ -1417,7 +1443,7 @@ async def test_arm_away_after_disabled_disarmed(hass, mqtt_mock): assert state.state == STATE_ALARM_TRIGGERED -async def test_disarm_with_template_code(hass, mqtt_mock): +async def test_disarm_with_template_code(hass, mqtt_mock_entry_with_yaml_config): """Attempt to disarm with a valid or invalid template-based code.""" assert await async_setup_component( hass, @@ -1459,7 +1485,7 @@ async def test_disarm_with_template_code(hass, mqtt_mock): assert state.state == STATE_ALARM_DISARMED -async def test_arm_home_via_command_topic(hass, mqtt_mock): +async def test_arm_home_via_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test arming home via command topic.""" assert await async_setup_component( hass, @@ -1498,7 +1524,7 @@ async def test_arm_home_via_command_topic(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME -async def test_arm_away_via_command_topic(hass, mqtt_mock): +async def test_arm_away_via_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test arming away via command topic.""" assert await async_setup_component( hass, @@ -1537,7 +1563,7 @@ async def test_arm_away_via_command_topic(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY -async def test_arm_night_via_command_topic(hass, mqtt_mock): +async def test_arm_night_via_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test arming night via command topic.""" assert await async_setup_component( hass, @@ -1576,7 +1602,7 @@ async def test_arm_night_via_command_topic(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT -async def test_disarm_pending_via_command_topic(hass, mqtt_mock): +async def test_disarm_pending_via_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test disarming pending alarm via command topic.""" assert await async_setup_component( hass, @@ -1610,7 +1636,9 @@ async def test_disarm_pending_via_command_topic(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_state_changes_are_published_to_mqtt(hass, mqtt_mock): +async def test_state_changes_are_published_to_mqtt( + hass, mqtt_mock_entry_with_yaml_config +): """Test publishing of MQTT messages when state changes.""" assert await async_setup_component( hass, @@ -1630,6 +1658,7 @@ async def test_state_changes_are_published_to_mqtt(hass, mqtt_mock): # Component should send disarmed alarm state on startup await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() mqtt_mock.async_publish.assert_called_once_with( "alarm/state", STATE_ALARM_DISARMED, 0, True ) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index f4d76d5474c..2b013ddf8dd 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -112,9 +112,9 @@ DEFAULT_CONFIG_REMOTE_CODE_TEXT = { } -async def test_fail_setup_without_state_topic(hass, mqtt_mock): +async def test_fail_setup_without_state_topic(hass, mqtt_mock_entry_no_yaml_config): """Test for failing with no state topic.""" - with assert_setup_component(0) as config: + with assert_setup_component(0, alarm_control_panel.DOMAIN) as config: assert await async_setup_component( hass, alarm_control_panel.DOMAIN, @@ -125,12 +125,14 @@ async def test_fail_setup_without_state_topic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert not config[alarm_control_panel.DOMAIN] -async def test_fail_setup_without_command_topic(hass, mqtt_mock): +async def test_fail_setup_without_command_topic(hass, mqtt_mock_entry_no_yaml_config): """Test failing with no command topic.""" - with assert_setup_component(0): + with assert_setup_component(0, alarm_control_panel.DOMAIN) as config: assert await async_setup_component( hass, alarm_control_panel.DOMAIN, @@ -141,9 +143,12 @@ async def test_fail_setup_without_command_topic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() + assert not config[alarm_control_panel.DOMAIN] -async def test_update_state_via_state_topic(hass, mqtt_mock): +async def test_update_state_via_state_topic(hass, mqtt_mock_entry_with_yaml_config): """Test updating with via state topic.""" assert await async_setup_component( hass, @@ -151,6 +156,7 @@ async def test_update_state_via_state_topic(hass, mqtt_mock): DEFAULT_CONFIG, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() entity_id = "alarm_control_panel.test" @@ -172,7 +178,9 @@ async def test_update_state_via_state_topic(hass, mqtt_mock): assert hass.states.get(entity_id).state == state -async def test_ignore_update_state_if_unknown_via_state_topic(hass, mqtt_mock): +async def test_ignore_update_state_if_unknown_via_state_topic( + hass, mqtt_mock_entry_with_yaml_config +): """Test ignoring updates via state topic.""" assert await async_setup_component( hass, @@ -180,6 +188,7 @@ async def test_ignore_update_state_if_unknown_via_state_topic(hass, mqtt_mock): DEFAULT_CONFIG, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() entity_id = "alarm_control_panel.test" @@ -201,7 +210,9 @@ async def test_ignore_update_state_if_unknown_via_state_topic(hass, mqtt_mock): (SERVICE_ALARM_TRIGGER, "TRIGGER"), ], ) -async def test_publish_mqtt_no_code(hass, mqtt_mock, service, payload): +async def test_publish_mqtt_no_code( + hass, mqtt_mock_entry_with_yaml_config, service, payload +): """Test publishing of MQTT messages when no code is configured.""" assert await async_setup_component( hass, @@ -209,6 +220,7 @@ async def test_publish_mqtt_no_code(hass, mqtt_mock, service, payload): DEFAULT_CONFIG, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( alarm_control_panel.DOMAIN, @@ -232,7 +244,9 @@ async def test_publish_mqtt_no_code(hass, mqtt_mock, service, payload): (SERVICE_ALARM_TRIGGER, "TRIGGER"), ], ) -async def test_publish_mqtt_with_code(hass, mqtt_mock, service, payload): +async def test_publish_mqtt_with_code( + hass, mqtt_mock_entry_with_yaml_config, service, payload +): """Test publishing of MQTT messages when code is configured.""" assert await async_setup_component( hass, @@ -240,6 +254,7 @@ async def test_publish_mqtt_with_code(hass, mqtt_mock, service, payload): DEFAULT_CONFIG_CODE, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() call_count = mqtt_mock.async_publish.call_count # No code provided, should not publish @@ -282,7 +297,9 @@ async def test_publish_mqtt_with_code(hass, mqtt_mock, service, payload): (SERVICE_ALARM_TRIGGER, "TRIGGER"), ], ) -async def test_publish_mqtt_with_remote_code(hass, mqtt_mock, service, payload): +async def test_publish_mqtt_with_remote_code( + hass, mqtt_mock_entry_with_yaml_config, service, payload +): """Test publishing of MQTT messages when remode code is configured.""" assert await async_setup_component( hass, @@ -290,6 +307,7 @@ async def test_publish_mqtt_with_remote_code(hass, mqtt_mock, service, payload): DEFAULT_CONFIG_REMOTE_CODE, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() call_count = mqtt_mock.async_publish.call_count # No code provided, should not publish @@ -323,7 +341,9 @@ async def test_publish_mqtt_with_remote_code(hass, mqtt_mock, service, payload): (SERVICE_ALARM_TRIGGER, "TRIGGER"), ], ) -async def test_publish_mqtt_with_remote_code_text(hass, mqtt_mock, service, payload): +async def test_publish_mqtt_with_remote_code_text( + hass, mqtt_mock_entry_with_yaml_config, service, payload +): """Test publishing of MQTT messages when remote text code is configured.""" assert await async_setup_component( hass, @@ -331,6 +351,7 @@ async def test_publish_mqtt_with_remote_code_text(hass, mqtt_mock, service, payl DEFAULT_CONFIG_REMOTE_CODE_TEXT, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() call_count = mqtt_mock.async_publish.call_count # No code provided, should not publish @@ -365,7 +386,7 @@ async def test_publish_mqtt_with_remote_code_text(hass, mqtt_mock, service, payl ], ) async def test_publish_mqtt_with_code_required_false( - hass, mqtt_mock, service, payload, disable_code + hass, mqtt_mock_entry_with_yaml_config, service, payload, disable_code ): """Test publishing of MQTT messages when code is configured. @@ -380,6 +401,7 @@ async def test_publish_mqtt_with_code_required_false( config, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() # No code provided, should publish await hass.services.async_call( @@ -412,7 +434,9 @@ async def test_publish_mqtt_with_code_required_false( mqtt_mock.reset_mock() -async def test_disarm_publishes_mqtt_with_template(hass, mqtt_mock): +async def test_disarm_publishes_mqtt_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test publishing of MQTT messages while disarmed. When command_template set to output json @@ -428,6 +452,7 @@ async def test_disarm_publishes_mqtt_with_template(hass, mqtt_mock): config, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_alarm_disarm(hass, "0123") mqtt_mock.async_publish.assert_called_once_with( @@ -435,7 +460,9 @@ async def test_disarm_publishes_mqtt_with_template(hass, mqtt_mock): ) -async def test_update_state_via_state_topic_template(hass, mqtt_mock): +async def test_update_state_via_state_topic_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test updating with template_value via state topic.""" assert await async_setup_component( hass, @@ -456,6 +483,7 @@ async def test_update_state_via_state_topic_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("alarm_control_panel.test") assert state.state == STATE_UNKNOWN @@ -466,13 +494,14 @@ async def test_update_state_via_state_topic_template(hass, mqtt_mock): assert state.state == STATE_ALARM_ARMED_AWAY -async def test_attributes_code_number(hass, mqtt_mock): +async def test_attributes_code_number(hass, mqtt_mock_entry_with_yaml_config): """Test attributes which are not supported by the vacuum.""" config = copy.deepcopy(DEFAULT_CONFIG) config[alarm_control_panel.DOMAIN]["code"] = CODE_NUMBER assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("alarm_control_panel.test") assert ( @@ -481,13 +510,14 @@ async def test_attributes_code_number(hass, mqtt_mock): ) -async def test_attributes_remote_code_number(hass, mqtt_mock): +async def test_attributes_remote_code_number(hass, mqtt_mock_entry_with_yaml_config): """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() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("alarm_control_panel.test") assert ( @@ -496,13 +526,14 @@ async def test_attributes_remote_code_number(hass, mqtt_mock): ) -async def test_attributes_code_text(hass, mqtt_mock): +async def test_attributes_code_text(hass, mqtt_mock_entry_with_yaml_config): """Test attributes which are not supported by the vacuum.""" config = copy.deepcopy(DEFAULT_CONFIG) config[alarm_control_panel.DOMAIN]["code"] = CODE_TEXT assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("alarm_control_panel.test") assert ( @@ -511,81 +542,121 @@ async def test_attributes_code_text(hass, mqtt_mock): ) -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_CODE, ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_CODE, ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_CODE, ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_CODE, ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, DEFAULT_CONFIG, MQTT_ALARM_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one alarm per unique_id.""" config = { alarm_control_panel.DOMAIN: [ @@ -605,18 +676,22 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, alarm_control_panel.DOMAIN, config) - - -async def test_discovery_removal_alarm(hass, mqtt_mock, caplog): - """Test removal of discovered alarm_control_panel.""" - data = json.dumps(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, data + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, config ) -async def test_discovery_update_alarm_topic_and_template(hass, mqtt_mock, caplog): +async def test_discovery_removal_alarm(hass, mqtt_mock_entry_no_yaml_config, caplog): + """Test removal of discovered alarm_control_panel.""" + data = json.dumps(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, alarm_control_panel.DOMAIN, data + ) + + +async def test_discovery_update_alarm_topic_and_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered alarm_control_panel.""" config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) @@ -639,7 +714,7 @@ async def test_discovery_update_alarm_topic_and_template(hass, mqtt_mock, caplog await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, alarm_control_panel.DOMAIN, config1, @@ -649,7 +724,9 @@ async def test_discovery_update_alarm_topic_and_template(hass, mqtt_mock, caplog ) -async def test_discovery_update_alarm_template(hass, mqtt_mock, caplog): +async def test_discovery_update_alarm_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered alarm_control_panel.""" config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) @@ -670,7 +747,7 @@ async def test_discovery_update_alarm_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, alarm_control_panel.DOMAIN, config1, @@ -680,7 +757,9 @@ async def test_discovery_update_alarm_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_unchanged_alarm(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_alarm( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered alarm_control_panel.""" config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) config1["name"] = "Beer" @@ -690,12 +769,17 @@ async def test_discovery_update_unchanged_alarm(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.alarm_control_panel.MqttAlarm.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + alarm_control_panel.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -704,7 +788,12 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ' "command_topic": "test_topic" }' ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, data1, data2 + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + alarm_control_panel.DOMAIN, + data1, + data2, ) @@ -715,11 +804,13 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ("state_topic", "disarmed"), ], ) -async def test_encoding_subscribable_topics(hass, mqtt_mock, caplog, topic, value): +async def test_encoding_subscribable_topics( + hass, mqtt_mock_entry_with_yaml_config, caplog, topic, value +): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, alarm_control_panel.DOMAIN, DEFAULT_CONFIG[alarm_control_panel.DOMAIN], @@ -728,53 +819,62 @@ async def test_encoding_subscribable_topics(hass, mqtt_mock, caplog, topic, valu ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT alarm control panel device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT alarm control panel device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, DEFAULT_CONFIG, alarm_control_panel.SERVICE_ALARM_DISARM, @@ -807,7 +907,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -823,7 +923,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -837,11 +937,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = alarm_control_panel.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index e4a48b07940..37bb783d354 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -62,7 +62,9 @@ DEFAULT_CONFIG = { } -async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, caplog): +async def test_setting_sensor_value_expires_availability_topic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the expiration of the value.""" assert await async_setup_component( hass, @@ -79,6 +81,7 @@ async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNAVAILABLE @@ -89,10 +92,12 @@ async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNAVAILABLE - await expires_helper(hass, mqtt_mock, caplog) + await expires_helper(hass) -async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): +async def test_setting_sensor_value_expires( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the expiration of the value.""" assert await async_setup_component( hass, @@ -108,15 +113,16 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() # State should be unavailable since expire_after is defined and > 0 state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNAVAILABLE - await expires_helper(hass, mqtt_mock, caplog) + await expires_helper(hass) -async def expires_helper(hass, mqtt_mock, caplog): +async def expires_helper(hass): """Run the basic expiry code.""" realnow = dt_util.utcnow() now = datetime(realnow.year + 1, 1, 1, 1, tzinfo=dt_util.UTC) @@ -168,9 +174,10 @@ async def expires_helper(hass, mqtt_mock, caplog): async def test_expiration_on_discovery_and_discovery_update_of_binary_sensor( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test that binary_sensor with expire_after set behaves correctly on discovery and discovery update.""" + await mqtt_mock_entry_no_yaml_config() config = { "name": "Test", "state_topic": "test-topic", @@ -247,7 +254,9 @@ async def test_expiration_on_discovery_and_discovery_update_of_binary_sensor( assert state.state == STATE_UNAVAILABLE -async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): +async def test_setting_sensor_value_via_mqtt_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, @@ -263,6 +272,7 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") @@ -281,7 +291,9 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_invalid_sensor_value_via_mqtt_message(hass, mqtt_mock, caplog): +async def test_invalid_sensor_value_via_mqtt_message( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, @@ -297,6 +309,7 @@ async def test_invalid_sensor_value_via_mqtt_message(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") @@ -319,7 +332,9 @@ async def test_invalid_sensor_value_via_mqtt_message(hass, mqtt_mock, caplog): assert "No matching payload found for entity" in caplog.text -async def test_setting_sensor_value_via_mqtt_message_and_template(hass, mqtt_mock): +async def test_setting_sensor_value_via_mqtt_message_and_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, @@ -337,6 +352,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template(hass, mqtt_moc }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNKNOWN @@ -351,7 +367,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template(hass, mqtt_moc async def test_setting_sensor_value_via_mqtt_message_and_template2( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the setting of the value via MQTT.""" assert await async_setup_component( @@ -369,6 +385,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template2( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNKNOWN @@ -388,7 +405,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template2( async def test_setting_sensor_value_via_mqtt_message_and_template_and_raw_state_encoding( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test processing a raw value via MQTT.""" assert await async_setup_component( @@ -407,6 +424,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template_and_raw_state_ }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNKNOWN @@ -421,7 +439,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template_and_raw_state_ async def test_setting_sensor_value_via_mqtt_message_empty_template( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the setting of the value via MQTT.""" assert await async_setup_component( @@ -439,6 +457,7 @@ async def test_setting_sensor_value_via_mqtt_message_empty_template( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNKNOWN @@ -453,7 +472,7 @@ async def test_setting_sensor_value_via_mqtt_message_empty_template( assert state.state == STATE_ON -async def test_valid_device_class(hass, mqtt_mock): +async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of a valid sensor class.""" assert await async_setup_component( hass, @@ -468,12 +487,13 @@ async def test_valid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") assert state.attributes.get("device_class") == "motion" -async def test_invalid_device_class(hass, mqtt_mock): +async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): """Test the setting of an invalid sensor class.""" assert await async_setup_component( hass, @@ -488,40 +508,43 @@ async def test_invalid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("binary_sensor.test") assert state is None -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_force_update_disabled(hass, mqtt_mock): +async def test_force_update_disabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, @@ -537,6 +560,7 @@ async def test_force_update_disabled(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() events = [] @@ -556,7 +580,7 @@ async def test_force_update_disabled(hass, mqtt_mock): assert len(events) == 1 -async def test_force_update_enabled(hass, mqtt_mock): +async def test_force_update_enabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, @@ -573,6 +597,7 @@ async def test_force_update_enabled(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() events = [] @@ -592,7 +617,7 @@ async def test_force_update_enabled(hass, mqtt_mock): assert len(events) == 2 -async def test_off_delay(hass, mqtt_mock): +async def test_off_delay(hass, mqtt_mock_entry_with_yaml_config): """Test off_delay option.""" assert await async_setup_component( hass, @@ -610,6 +635,7 @@ async def test_off_delay(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() events = [] @@ -639,42 +665,60 @@ async def test_off_delay(hass, mqtt_mock): assert len(events) == 3 -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + binary_sensor.DOMAIN, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + binary_sensor.DOMAIN, + DEFAULT_CONFIG, ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + binary_sensor.DOMAIN, + DEFAULT_CONFIG, ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one sensor per unique_id.""" config = { binary_sensor.DOMAIN: [ @@ -692,18 +736,24 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, binary_sensor.DOMAIN, config) - - -async def test_discovery_removal_binary_sensor(hass, mqtt_mock, caplog): - """Test removal of discovered binary_sensor.""" - data = json.dumps(DEFAULT_CONFIG[binary_sensor.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, data + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, config ) -async def test_discovery_update_binary_sensor_topic_template(hass, mqtt_mock, caplog): +async def test_discovery_removal_binary_sensor( + hass, mqtt_mock_entry_no_yaml_config, caplog +): + """Test removal of discovered binary_sensor.""" + data = json.dumps(DEFAULT_CONFIG[binary_sensor.DOMAIN]) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, binary_sensor.DOMAIN, data + ) + + +async def test_discovery_update_binary_sensor_topic_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered binary_sensor.""" config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) @@ -728,7 +778,7 @@ async def test_discovery_update_binary_sensor_topic_template(hass, mqtt_mock, ca await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, binary_sensor.DOMAIN, config1, @@ -738,7 +788,9 @@ async def test_discovery_update_binary_sensor_topic_template(hass, mqtt_mock, ca ) -async def test_discovery_update_binary_sensor_template(hass, mqtt_mock, caplog): +async def test_discovery_update_binary_sensor_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered binary_sensor.""" config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) @@ -761,7 +813,7 @@ async def test_discovery_update_binary_sensor_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, binary_sensor.DOMAIN, config1, @@ -785,12 +837,18 @@ async def test_discovery_update_binary_sensor_template(hass, mqtt_mock, caplog): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, binary_sensor.DOMAIN, DEFAULT_CONFIG[binary_sensor.DOMAIN], @@ -801,7 +859,9 @@ async def test_encoding_subscribable_topics( ) -async def test_discovery_update_unchanged_binary_sensor(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_binary_sensor( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered binary_sensor.""" config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) config1["name"] = "Beer" @@ -811,74 +871,90 @@ async def test_discovery_update_unchanged_binary_sensor(hass, mqtt_mock, caplog) "homeassistant.components.mqtt.binary_sensor.MqttBinarySensor.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + binary_sensor.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer",' ' "off_delay": -1 }' data2 = '{ "name": "Milk",' ' "state_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, data1, data2 + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + binary_sensor.DOMAIN, + data1, + data2, ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT binary sensor device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT binary sensor device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG, None + hass, + mqtt_mock_entry_no_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG, + None, ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = binary_sensor.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -893,7 +969,15 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): [("ON", "on", "OFF", "off"), ("OFF", "off", "ON", "on")], ) async def test_cleanup_triggers_and_restoring_state( - hass, mqtt_mock, caplog, tmp_path, freezer, payload1, state1, payload2, state2 + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + tmp_path, + freezer, + payload1, + state1, + payload2, + state2, ): """Test cleanup old triggers at reloading and restoring the state.""" domain = binary_sensor.DOMAIN @@ -914,6 +998,8 @@ async def test_cleanup_triggers_and_restoring_state( {binary_sensor.DOMAIN: [config1, config2]}, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + async_fire_mqtt_message(hass, "test-topic1", payload1) state = hass.states.get("binary_sensor.test1") assert state.state == state1 @@ -951,7 +1037,7 @@ async def test_cleanup_triggers_and_restoring_state( async def test_skip_restoring_state_with_over_due_expire_trigger( - hass, mqtt_mock, caplog, freezer + hass, mqtt_mock_entry_with_yaml_config, caplog, freezer ): """Test restoring a state with over due expire timer.""" @@ -973,6 +1059,7 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( ), assert_setup_component(1, domain): assert await async_setup_component(hass, domain, {domain: config3}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert "Skip state recovery after reload for binary_sensor.test3" in caplog.text diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 941f08e541c..35deccf2bfe 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -42,7 +42,7 @@ DEFAULT_CONFIG = { @pytest.mark.freeze_time("2021-11-08 13:31:44+00:00") -async def test_sending_mqtt_commands(hass, mqtt_mock): +async def test_sending_mqtt_commands(hass, mqtt_mock_entry_with_yaml_config): """Test the sending MQTT commands.""" assert await async_setup_component( hass, @@ -59,6 +59,7 @@ async def test_sending_mqtt_commands(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("button.test_button") assert state.state == STATE_UNKNOWN @@ -79,7 +80,7 @@ async def test_sending_mqtt_commands(hass, mqtt_mock): assert state.state == "2021-11-08T13:31:44+00:00" -async def test_command_template(hass, mqtt_mock): +async def test_command_template(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of MQTT commands through a command template.""" assert await async_setup_component( hass, @@ -95,6 +96,7 @@ async def test_command_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("button.test") assert state.state == STATE_UNKNOWN @@ -113,21 +115,23 @@ async def test_command_template(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { button.DOMAIN: { @@ -139,11 +143,17 @@ async def test_default_availability_payload(hass, mqtt_mock): } await help_test_default_availability_payload( - hass, mqtt_mock, button.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + button.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { button.DOMAIN: { @@ -155,53 +165,67 @@ async def test_custom_availability_payload(hass, mqtt_mock): } await help_test_custom_availability_payload( - hass, mqtt_mock, button.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + button.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG, None + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG, None ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, button.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, button.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, button.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one button per unique_id.""" config = { button.DOMAIN: [ @@ -219,16 +243,20 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, button.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, config + ) -async def test_discovery_removal_button(hass, mqtt_mock, caplog): +async def test_discovery_removal_button(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered button.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, button.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, button.DOMAIN, data + ) -async def test_discovery_update_button(hass, mqtt_mock, caplog): +async def test_discovery_update_button(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered button.""" config1 = copy.deepcopy(DEFAULT_CONFIG[button.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[button.DOMAIN]) @@ -237,7 +265,7 @@ async def test_discovery_update_button(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, button.DOMAIN, config1, @@ -245,7 +273,9 @@ async def test_discovery_update_button(hass, mqtt_mock, caplog): ) -async def test_discovery_update_unchanged_button(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_button( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered button.""" data1 = ( '{ "name": "Beer",' @@ -256,60 +286,65 @@ async def test_discovery_update_unchanged_button(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.button.MqttButton.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, button.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + button.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, button.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, button.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT button device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT button device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG, button.SERVICE_PRESS, @@ -318,7 +353,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) -async def test_invalid_device_class(hass, mqtt_mock): +async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): """Test device_class option with invalid value.""" assert await async_setup_component( hass, @@ -333,12 +368,13 @@ async def test_invalid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("button.test") assert state is None -async def test_valid_device_class(hass, mqtt_mock): +async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test device_class option with valid values.""" assert await async_setup_component( hass, @@ -366,6 +402,7 @@ async def test_valid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("button.test_1") assert state.attributes["device_class"] == button.ButtonDeviceClass.UPDATE @@ -382,7 +419,14 @@ async def test_valid_device_class(hass, mqtt_mock): ], ) async def test_publishing_with_custom_encoding( - hass, mqtt_mock, caplog, service, topic, parameters, payload, template + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + service, + topic, + parameters, + payload, + template, ): """Test publishing MQTT payload with different encoding.""" domain = button.DOMAIN @@ -390,7 +434,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -402,11 +446,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = button.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 204103152a7..54d829ce9f9 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -46,7 +46,9 @@ DEFAULT_CONFIG = { } -async def test_run_camera_setup(hass, hass_client_no_auth, mqtt_mock): +async def test_run_camera_setup( + hass, hass_client_no_auth, mqtt_mock_entry_with_yaml_config +): """Test that it fetches the given payload.""" topic = "test/camera" await async_setup_component( @@ -55,6 +57,7 @@ async def test_run_camera_setup(hass, hass_client_no_auth, mqtt_mock): {"camera": {"platform": "mqtt", "topic": topic, "name": "Test Camera"}}, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() url = hass.states.get("camera.test_camera").attributes["entity_picture"] @@ -67,7 +70,9 @@ async def test_run_camera_setup(hass, hass_client_no_auth, mqtt_mock): assert body == "beer" -async def test_run_camera_b64_encoded(hass, hass_client_no_auth, mqtt_mock): +async def test_run_camera_b64_encoded( + hass, hass_client_no_auth, mqtt_mock_entry_with_yaml_config +): """Test that it fetches the given encoded payload.""" topic = "test/camera" await async_setup_component( @@ -83,6 +88,7 @@ async def test_run_camera_b64_encoded(hass, hass_client_no_auth, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() url = hass.states.get("camera.test_camera").attributes["entity_picture"] @@ -95,77 +101,91 @@ async def test_run_camera_b64_encoded(hass, hass_client_no_auth, mqtt_mock): assert body == "grass" -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG, MQTT_CAMERA_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + camera.DOMAIN, + DEFAULT_CONFIG, + MQTT_CAMERA_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one camera per unique_id.""" config = { camera.DOMAIN: [ @@ -183,94 +203,109 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, camera.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, config + ) -async def test_discovery_removal_camera(hass, mqtt_mock, caplog): +async def test_discovery_removal_camera(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered camera.""" data = json.dumps(DEFAULT_CONFIG[camera.DOMAIN]) - await help_test_discovery_removal(hass, mqtt_mock, caplog, camera.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, camera.DOMAIN, data + ) -async def test_discovery_update_camera(hass, mqtt_mock, caplog): +async def test_discovery_update_camera(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered camera.""" config1 = {"name": "Beer", "topic": "test_topic"} config2 = {"name": "Milk", "topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock, caplog, camera.DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, camera.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_camera(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_camera( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered camera.""" data1 = '{ "name": "Beer", "topic": "test_topic"}' with patch( "homeassistant.components.mqtt.camera.MqttCamera.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, camera.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + camera.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "topic": "test_topic"}' await help_test_discovery_broken( - hass, mqtt_mock, caplog, camera.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, camera.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT camera device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT camera device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG, ["test_topic"] + hass, + mqtt_mock_entry_with_yaml_config, + camera.DOMAIN, + DEFAULT_CONFIG, + ["test_topic"], ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG, None, @@ -279,11 +314,13 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = camera.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 98af86248e4..77843cee777 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -106,10 +106,11 @@ DEFAULT_LEGACY_CONFIG = { } -async def test_setup_params(hass, mqtt_mock): +async def test_setup_params(hass, mqtt_mock_entry_with_yaml_config): """Test the initial parameters.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("temperature") == 21 @@ -120,12 +121,15 @@ async def test_setup_params(hass, mqtt_mock): assert state.attributes.get("max_temp") == DEFAULT_MAX_TEMP -async def test_preset_none_in_preset_modes(hass, mqtt_mock, caplog): +async def test_preset_none_in_preset_modes( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test the preset mode payload reset configuration.""" config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) config["preset_modes"].append("none") assert await async_setup_component(hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert "Invalid config for [climate.mqtt]: not a valid value" in caplog.text state = hass.states.get(ENTITY_CLIMATE) assert state is None @@ -145,21 +149,23 @@ async def test_preset_none_in_preset_modes(hass, mqtt_mock, caplog): ], ) async def test_preset_modes_deprecation_guard( - hass, mqtt_mock, caplog, parameter, config_value + hass, mqtt_mock_entry_no_yaml_config, caplog, parameter, config_value ): """Test the configuration for invalid legacy parameters.""" config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) config[parameter] = config_value assert await async_setup_component(hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state is None -async def test_supported_features(hass, mqtt_mock): +async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test the supported_features.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) support = ( @@ -174,10 +180,11 @@ async def test_supported_features(hass, mqtt_mock): assert state.attributes.get("supported_features") == support -async def test_get_hvac_modes(hass, mqtt_mock): +async def test_get_hvac_modes(hass, mqtt_mock_entry_with_yaml_config): """Test that the operation list returns the correct modes.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) modes = state.attributes.get("hvac_modes") @@ -191,13 +198,16 @@ async def test_get_hvac_modes(hass, mqtt_mock): ] == modes -async def test_set_operation_bad_attr_and_state(hass, mqtt_mock, caplog): +async def test_set_operation_bad_attr_and_state( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test setting operation mode without required attribute. Also check the state. """ assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.state == "off" @@ -210,10 +220,11 @@ async def test_set_operation_bad_attr_and_state(hass, mqtt_mock, caplog): assert state.state == "off" -async def test_set_operation(hass, mqtt_mock): +async def test_set_operation(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new operation mode.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.state == "off" @@ -224,12 +235,13 @@ async def test_set_operation(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("mode-topic", "cool", 0, False) -async def test_set_operation_pessimistic(hass, mqtt_mock): +async def test_set_operation_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting operation mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["mode_state_topic"] = "mode-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.state == "unknown" @@ -247,12 +259,13 @@ async def test_set_operation_pessimistic(hass, mqtt_mock): assert state.state == "cool" -async def test_set_operation_with_power_command(hass, mqtt_mock): +async def test_set_operation_with_power_command(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new operation mode with power command enabled.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["power_command_topic"] = "power-command" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.state == "off" @@ -273,10 +286,11 @@ async def test_set_operation_with_power_command(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_set_fan_mode_bad_attr(hass, mqtt_mock, caplog): +async def test_set_fan_mode_bad_attr(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test setting fan mode without required attribute.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("fan_mode") == "low" @@ -289,12 +303,13 @@ async def test_set_fan_mode_bad_attr(hass, mqtt_mock, caplog): assert state.attributes.get("fan_mode") == "low" -async def test_set_fan_mode_pessimistic(hass, mqtt_mock): +async def test_set_fan_mode_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new fan mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["fan_mode_state_topic"] = "fan-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("fan_mode") is None @@ -312,10 +327,11 @@ async def test_set_fan_mode_pessimistic(hass, mqtt_mock): assert state.attributes.get("fan_mode") == "high" -async def test_set_fan_mode(hass, mqtt_mock): +async def test_set_fan_mode(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new fan mode.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("fan_mode") == "low" @@ -335,13 +351,14 @@ async def test_set_fan_mode(hass, mqtt_mock): ], ) async def test_set_fan_mode_send_if_off( - hass, mqtt_mock, send_if_off, assert_async_publish + hass, mqtt_mock_entry_with_yaml_config, send_if_off, assert_async_publish ): """Test setting of fan mode if the hvac is off.""" config = copy.deepcopy(DEFAULT_CONFIG) config[CLIMATE_DOMAIN].update(send_if_off) assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() assert hass.states.get(ENTITY_CLIMATE) is not None # Turn on HVAC @@ -362,10 +379,11 @@ async def test_set_fan_mode_send_if_off( mqtt_mock.async_publish.assert_has_calls(assert_async_publish) -async def test_set_swing_mode_bad_attr(hass, mqtt_mock, caplog): +async def test_set_swing_mode_bad_attr(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test setting swing mode without required attribute.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("swing_mode") == "off" @@ -378,12 +396,13 @@ async def test_set_swing_mode_bad_attr(hass, mqtt_mock, caplog): assert state.attributes.get("swing_mode") == "off" -async def test_set_swing_pessimistic(hass, mqtt_mock): +async def test_set_swing_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting swing mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["swing_mode_state_topic"] = "swing-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("swing_mode") is None @@ -401,10 +420,11 @@ async def test_set_swing_pessimistic(hass, mqtt_mock): assert state.attributes.get("swing_mode") == "on" -async def test_set_swing(hass, mqtt_mock): +async def test_set_swing(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new swing mode.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("swing_mode") == "off" @@ -424,13 +444,14 @@ async def test_set_swing(hass, mqtt_mock): ], ) async def test_set_swing_mode_send_if_off( - hass, mqtt_mock, send_if_off, assert_async_publish + hass, mqtt_mock_entry_with_yaml_config, send_if_off, assert_async_publish ): """Test setting of swing mode if the hvac is off.""" config = copy.deepcopy(DEFAULT_CONFIG) config[CLIMATE_DOMAIN].update(send_if_off) assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() assert hass.states.get(ENTITY_CLIMATE) is not None # Turn on HVAC @@ -451,10 +472,11 @@ async def test_set_swing_mode_send_if_off( mqtt_mock.async_publish.assert_has_calls(assert_async_publish) -async def test_set_target_temperature(hass, mqtt_mock): +async def test_set_target_temperature(hass, mqtt_mock_entry_with_yaml_config): """Test setting the target temperature.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("temperature") == 21 @@ -497,13 +519,14 @@ async def test_set_target_temperature(hass, mqtt_mock): ], ) async def test_set_target_temperature_send_if_off( - hass, mqtt_mock, send_if_off, assert_async_publish + hass, mqtt_mock_entry_with_yaml_config, send_if_off, assert_async_publish ): """Test setting of target temperature if the hvac is off.""" config = copy.deepcopy(DEFAULT_CONFIG) config[CLIMATE_DOMAIN].update(send_if_off) assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() assert hass.states.get(ENTITY_CLIMATE) is not None # Turn on HVAC @@ -526,12 +549,15 @@ async def test_set_target_temperature_send_if_off( mqtt_mock.async_publish.assert_has_calls(assert_async_publish) -async def test_set_target_temperature_pessimistic(hass, mqtt_mock): +async def test_set_target_temperature_pessimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting the target temperature.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["temperature_state_topic"] = "temperature-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("temperature") is None @@ -549,10 +575,11 @@ async def test_set_target_temperature_pessimistic(hass, mqtt_mock): assert state.attributes.get("temperature") == 1701 -async def test_set_target_temperature_low_high(hass, mqtt_mock): +async def test_set_target_temperature_low_high(hass, mqtt_mock_entry_with_yaml_config): """Test setting the low/high target temperature.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_set_temperature( hass, target_temp_low=20, target_temp_high=23, entity_id=ENTITY_CLIMATE @@ -564,13 +591,16 @@ async def test_set_target_temperature_low_high(hass, mqtt_mock): mqtt_mock.async_publish.assert_any_call("temperature-high-topic", "23.0", 0, False) -async def test_set_target_temperature_low_highpessimistic(hass, mqtt_mock): +async def test_set_target_temperature_low_highpessimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting the low/high target temperature.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["temperature_low_state_topic"] = "temperature-low-state" config["climate"]["temperature_high_state_topic"] = "temperature-high-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("target_temp_low") is None @@ -601,24 +631,26 @@ async def test_set_target_temperature_low_highpessimistic(hass, mqtt_mock): assert state.attributes.get("target_temp_high") == 1703 -async def test_receive_mqtt_temperature(hass, mqtt_mock): +async def test_receive_mqtt_temperature(hass, mqtt_mock_entry_with_yaml_config): """Test getting the current temperature via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["current_temperature_topic"] = "current_temperature" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "current_temperature", "47") state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("current_temperature") == 47 -async def test_handle_action_received(hass, mqtt_mock): +async def test_handle_action_received(hass, mqtt_mock_entry_with_yaml_config): """Test getting the action received via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["action_topic"] = "action" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() # Cycle through valid modes and also check for wrong input such as "None" (str(None)) async_fire_mqtt_message(hass, "action", "None") @@ -635,11 +667,14 @@ async def test_handle_action_received(hass, mqtt_mock): assert hvac_action == action -async def test_set_preset_mode_optimistic(hass, mqtt_mock, caplog): +async def test_set_preset_mode_optimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test setting of the preset mode.""" config = copy.deepcopy(DEFAULT_CONFIG) assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "none" @@ -680,12 +715,15 @@ async def test_set_preset_mode_optimistic(hass, mqtt_mock, caplog): assert "'invalid' is not a valid preset mode" in caplog.text -async def test_set_preset_mode_pessimistic(hass, mqtt_mock, caplog): +async def test_set_preset_mode_pessimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test setting of the preset mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["preset_mode_state_topic"] = "preset-mode-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "none" @@ -725,12 +763,13 @@ async def test_set_preset_mode_pessimistic(hass, mqtt_mock, caplog): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_away_mode_pessimistic(hass, mqtt_mock): +async def test_set_away_mode_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the away mode.""" config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) config["climate"]["away_mode_state_topic"] = "away-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "none" @@ -753,7 +792,7 @@ async def test_set_away_mode_pessimistic(hass, mqtt_mock): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_away_mode(hass, mqtt_mock): +async def test_set_away_mode(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the away mode.""" config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) config["climate"]["payload_on"] = "AN" @@ -761,6 +800,7 @@ async def test_set_away_mode(hass, mqtt_mock): assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "none" @@ -795,12 +835,13 @@ async def test_set_away_mode(hass, mqtt_mock): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_hold_pessimistic(hass, mqtt_mock): +async def test_set_hold_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting the hold mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) config["climate"]["hold_state_topic"] = "hold-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("hold_mode") is None @@ -819,10 +860,11 @@ async def test_set_hold_pessimistic(hass, mqtt_mock): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_hold(hass, mqtt_mock): +async def test_set_hold(hass, mqtt_mock_entry_with_yaml_config): """Test setting the hold mode.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "none" @@ -851,10 +893,11 @@ async def test_set_hold(hass, mqtt_mock): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_preset_away(hass, mqtt_mock): +async def test_set_preset_away(hass, mqtt_mock_entry_with_yaml_config): """Test setting the hold mode and away mode.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == PRESET_NONE @@ -885,13 +928,14 @@ async def test_set_preset_away(hass, mqtt_mock): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_preset_away_pessimistic(hass, mqtt_mock): +async def test_set_preset_away_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting the hold mode and away mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) config["climate"]["hold_state_topic"] = "hold-state" config["climate"]["away_mode_state_topic"] = "away-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == PRESET_NONE @@ -936,10 +980,11 @@ async def test_set_preset_away_pessimistic(hass, mqtt_mock): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_preset_mode_twice(hass, mqtt_mock): +async def test_set_preset_mode_twice(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the same mode twice only publishes once.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "none" @@ -952,12 +997,13 @@ async def test_set_preset_mode_twice(hass, mqtt_mock): assert state.attributes.get("preset_mode") == "hold-on" -async def test_set_aux_pessimistic(hass, mqtt_mock): +async def test_set_aux_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the aux heating in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["aux_state_topic"] = "aux-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("aux_heat") == "off" @@ -979,10 +1025,11 @@ async def test_set_aux_pessimistic(hass, mqtt_mock): assert state.attributes.get("aux_heat") == "off" -async def test_set_aux(hass, mqtt_mock): +async def test_set_aux(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the aux heating.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("aux_heat") == "off" @@ -998,35 +1045,39 @@ async def test_set_aux(hass, mqtt_mock): assert state.attributes.get("aux_heat") == "off" -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_get_target_temperature_low_high_with_templates(hass, mqtt_mock, caplog): +async def test_get_target_temperature_low_high_with_templates( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test getting temperature high/low with templates.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["temperature_low_state_topic"] = "temperature-state" @@ -1036,6 +1087,7 @@ async def test_get_target_temperature_low_high_with_templates(hass, mqtt_mock, c assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) @@ -1060,7 +1112,7 @@ async def test_get_target_temperature_low_high_with_templates(hass, mqtt_mock, c assert state.attributes.get("target_temp_high") == 1032 -async def test_get_with_templates(hass, mqtt_mock, caplog): +async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test getting various attributes with templates.""" config = copy.deepcopy(DEFAULT_CONFIG) # By default, just unquote the JSON-strings @@ -1081,6 +1133,7 @@ async def test_get_with_templates(hass, mqtt_mock, caplog): config["climate"]["preset_mode_state_topic"] = "current-preset-mode" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() # Operation Mode state = hass.states.get(ENTITY_CLIMATE) @@ -1159,7 +1212,9 @@ async def test_get_with_templates(hass, mqtt_mock, caplog): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_get_with_hold_and_away_mode_and_templates(hass, mqtt_mock, caplog): +async def test_get_with_hold_and_away_mode_and_templates( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test getting various for hold and away mode attributes with templates.""" config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) config["climate"]["mode_state_topic"] = "mode-state" @@ -1172,6 +1227,7 @@ async def test_get_with_hold_and_away_mode_and_templates(hass, mqtt_mock, caplog assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() # Operation Mode state = hass.states.get(ENTITY_CLIMATE) @@ -1206,7 +1262,7 @@ async def test_get_with_hold_and_away_mode_and_templates(hass, mqtt_mock, caplog assert state.attributes.get("preset_mode") == "somemode" -async def test_set_and_templates(hass, mqtt_mock, caplog): +async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test setting various attributes with templates.""" config = copy.deepcopy(DEFAULT_CONFIG) # Create simple templates @@ -1220,6 +1276,7 @@ async def test_set_and_templates(hass, mqtt_mock, caplog): assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() # Fan Mode await common.async_set_fan_mode(hass, "high", ENTITY_CLIMATE) @@ -1284,7 +1341,9 @@ async def test_set_and_templates(hass, mqtt_mock, caplog): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_with_away_and_hold_modes_and_templates(hass, mqtt_mock, caplog): +async def test_set_with_away_and_hold_modes_and_templates( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test setting various attributes on hold and away mode with templates.""" config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) # Create simple templates @@ -1292,6 +1351,7 @@ async def test_set_with_away_and_hold_modes_and_templates(hass, mqtt_mock, caplo assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() # Hold Mode await common.async_set_preset_mode(hass, PRESET_ECO, ENTITY_CLIMATE) @@ -1303,13 +1363,14 @@ async def test_set_with_away_and_hold_modes_and_templates(hass, mqtt_mock, caplo assert state.attributes.get("preset_mode") == PRESET_ECO -async def test_min_temp_custom(hass, mqtt_mock): +async def test_min_temp_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom min temp.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["min_temp"] = 26 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) min_temp = state.attributes.get("min_temp") @@ -1318,13 +1379,14 @@ async def test_min_temp_custom(hass, mqtt_mock): assert state.attributes.get("min_temp") == 26 -async def test_max_temp_custom(hass, mqtt_mock): +async def test_max_temp_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom max temp.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["max_temp"] = 60 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) max_temp = state.attributes.get("max_temp") @@ -1333,13 +1395,14 @@ async def test_max_temp_custom(hass, mqtt_mock): assert max_temp == 60 -async def test_temp_step_custom(hass, mqtt_mock): +async def test_temp_step_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom temp step.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["temp_step"] = 0.01 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) temp_step = state.attributes.get("target_temp_step") @@ -1348,7 +1411,7 @@ async def test_temp_step_custom(hass, mqtt_mock): assert temp_step == 0.01 -async def test_temperature_unit(hass, mqtt_mock): +async def test_temperature_unit(hass, mqtt_mock_entry_with_yaml_config): """Test that setting temperature unit converts temperature values.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["temperature_unit"] = "F" @@ -1356,6 +1419,7 @@ async def test_temperature_unit(hass, mqtt_mock): assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "current_temperature", "77") @@ -1363,49 +1427,61 @@ async def test_temperature_unit(hass, mqtt_mock): assert state.attributes.get("current_temperature") == 25 -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG, MQTT_CLIMATE_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + CLIMATE_DOMAIN, + DEFAULT_CONFIG, + MQTT_CLIMATE_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one climate per unique_id.""" config = { CLIMATE_DOMAIN: [ @@ -1425,7 +1501,9 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, CLIMATE_DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, config + ) @pytest.mark.parametrize( @@ -1449,7 +1527,13 @@ async def test_unique_id(hass, mqtt_mock): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) @@ -1460,7 +1544,7 @@ async def test_encoding_subscribable_topics( del config["preset_mode_command_topic"] await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, CLIMATE_DOMAIN, config, @@ -1471,71 +1555,80 @@ async def test_encoding_subscribable_topics( ) -async def test_discovery_removal_climate(hass, mqtt_mock, caplog): +async def test_discovery_removal_climate(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered climate.""" data = json.dumps(DEFAULT_CONFIG[CLIMATE_DOMAIN]) - await help_test_discovery_removal(hass, mqtt_mock, caplog, CLIMATE_DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, CLIMATE_DOMAIN, data + ) -async def test_discovery_update_climate(hass, mqtt_mock, caplog): +async def test_discovery_update_climate(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered climate.""" config1 = {"name": "Beer"} config2 = {"name": "Milk"} await help_test_discovery_update( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, CLIMATE_DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_climate(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_climate( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered climate.""" data1 = '{ "name": "Beer" }' with patch( "homeassistant.components.mqtt.climate.MqttClimate.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + CLIMATE_DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "power_command_topic": "test_topic#" }' data2 = '{ "name": "Milk", "power_command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, CLIMATE_DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT climate device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT climate device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { CLIMATE_DOMAIN: { @@ -1546,18 +1639,22 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock): } } await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, CLIMATE_DOMAIN, config, ["test-topic", "avty-topic"] + hass, + mqtt_mock_entry_with_yaml_config, + CLIMATE_DOMAIN, + config, + ["test-topic", "avty-topic"], ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" config = { CLIMATE_DOMAIN: { @@ -1569,7 +1666,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): } await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, config, climate.SERVICE_TURN_ON, @@ -1579,10 +1676,11 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) -async def test_precision_default(hass, mqtt_mock): +async def test_precision_default(hass, mqtt_mock_entry_with_yaml_config): """Test that setting precision to tenths works as intended.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_set_temperature( hass, temperature=23.67, entity_id=ENTITY_CLIMATE @@ -1592,12 +1690,13 @@ async def test_precision_default(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_precision_halves(hass, mqtt_mock): +async def test_precision_halves(hass, mqtt_mock_entry_with_yaml_config): """Test that setting precision to halves works as intended.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["precision"] = 0.5 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_set_temperature( hass, temperature=23.67, entity_id=ENTITY_CLIMATE @@ -1607,12 +1706,13 @@ async def test_precision_halves(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_precision_whole(hass, mqtt_mock): +async def test_precision_whole(hass, mqtt_mock_entry_with_yaml_config): """Test that setting precision to whole works as intended.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["precision"] = 1.0 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_set_temperature( hass, temperature=23.67, entity_id=ENTITY_CLIMATE @@ -1721,7 +1821,7 @@ async def test_precision_whole(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -1738,7 +1838,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -1750,11 +1850,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = CLIMATE_DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index b5bb5732617..50cf7beb0e0 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -46,10 +46,13 @@ DEFAULT_CONFIG_DEVICE_INFO_MAC = { _SENTINEL = object() -async def help_test_availability_when_connection_lost(hass, mqtt_mock, domain, config): +async def help_test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config, domain, config +): """Test availability after MQTT disconnection.""" assert await async_setup_component(hass, domain, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state != STATE_UNAVAILABLE @@ -62,11 +65,14 @@ async def help_test_availability_when_connection_lost(hass, mqtt_mock, domain, c assert state.state == STATE_UNAVAILABLE -async def help_test_availability_without_topic(hass, mqtt_mock, domain, config): +async def help_test_availability_without_topic( + hass, mqtt_mock_entry_with_yaml_config, domain, config +): """Test availability without defined availability topic.""" assert "availability_topic" not in config[domain] assert await async_setup_component(hass, domain, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state != STATE_UNAVAILABLE @@ -74,7 +80,7 @@ async def help_test_availability_without_topic(hass, mqtt_mock, domain, config): async def help_test_default_availability_payload( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, domain, config, no_assumed_state=False, @@ -94,6 +100,7 @@ async def help_test_default_availability_payload( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state == STATE_UNAVAILABLE @@ -124,7 +131,7 @@ async def help_test_default_availability_payload( async def help_test_default_availability_list_payload( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, domain, config, no_assumed_state=False, @@ -147,6 +154,7 @@ async def help_test_default_availability_list_payload( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state == STATE_UNAVAILABLE @@ -189,7 +197,7 @@ async def help_test_default_availability_list_payload( async def help_test_default_availability_list_payload_all( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, domain, config, no_assumed_state=False, @@ -213,6 +221,7 @@ async def help_test_default_availability_list_payload_all( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state == STATE_UNAVAILABLE @@ -256,7 +265,7 @@ async def help_test_default_availability_list_payload_all( async def help_test_default_availability_list_payload_any( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, domain, config, no_assumed_state=False, @@ -280,6 +289,7 @@ async def help_test_default_availability_list_payload_any( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state == STATE_UNAVAILABLE @@ -318,7 +328,7 @@ async def help_test_default_availability_list_payload_any( async def help_test_default_availability_list_single( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -342,6 +352,7 @@ async def help_test_default_availability_list_single( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state is None @@ -353,7 +364,7 @@ async def help_test_default_availability_list_single( async def help_test_custom_availability_payload( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, domain, config, no_assumed_state=False, @@ -375,6 +386,7 @@ async def help_test_custom_availability_payload( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state == STATE_UNAVAILABLE @@ -405,7 +417,7 @@ async def help_test_custom_availability_payload( async def help_test_discovery_update_availability( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, domain, config, no_assumed_state=False, @@ -416,6 +428,7 @@ async def help_test_discovery_update_availability( This is a test helper for the MQTTAvailability mixin. """ + await mqtt_mock_entry_no_yaml_config() # Add availability settings to config config1 = copy.deepcopy(config) config1[domain]["availability_topic"] = "availability-topic1" @@ -484,7 +497,7 @@ async def help_test_discovery_update_availability( async def help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, domain, config + hass, mqtt_mock_entry_with_yaml_config, domain, config ): """Test the setting of attribute via MQTT with JSON payload. @@ -499,6 +512,7 @@ async def help_test_setting_attribute_via_mqtt_json_message( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "attr-topic", '{ "val": "100" }') state = hass.states.get(f"{domain}.test") @@ -507,12 +521,13 @@ async def help_test_setting_attribute_via_mqtt_json_message( async def help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, domain, config, extra_blocked_attributes + hass, mqtt_mock_entry_no_yaml_config, domain, config, extra_blocked_attributes ): """Test the setting of blocked attribute via MQTT with JSON payload. This is a test helper for the MqttAttributes mixin. """ + await mqtt_mock_entry_no_yaml_config() extra_blocked_attributes = extra_blocked_attributes or [] # Add JSON attributes settings to config @@ -534,7 +549,9 @@ async def help_test_setting_blocked_attribute_via_mqtt_json_message( assert state.attributes.get(attr) != val -async def help_test_setting_attribute_with_template(hass, mqtt_mock, domain, config): +async def help_test_setting_attribute_with_template( + hass, mqtt_mock_entry_with_yaml_config, domain, config +): """Test the setting of attribute via MQTT with JSON payload. This is a test helper for the MqttAttributes mixin. @@ -549,6 +566,7 @@ async def help_test_setting_attribute_with_template(hass, mqtt_mock, domain, con config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message( hass, "attr-topic", json.dumps({"Timer1": {"Arm": 0, "Time": "22:18"}}) @@ -560,7 +578,7 @@ async def help_test_setting_attribute_with_template(hass, mqtt_mock, domain, con async def help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, domain, config + hass, mqtt_mock_entry_with_yaml_config, caplog, domain, config ): """Test attributes get extracted from a JSON result. @@ -575,6 +593,7 @@ async def help_test_update_with_json_attrs_not_dict( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "attr-topic", '[ "list", "of", "things"]') state = hass.states.get(f"{domain}.test") @@ -584,7 +603,7 @@ async def help_test_update_with_json_attrs_not_dict( async def help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, domain, config + hass, mqtt_mock_entry_with_yaml_config, caplog, domain, config ): """Test JSON validation of attributes. @@ -599,6 +618,7 @@ async def help_test_update_with_json_attrs_bad_JSON( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "attr-topic", "This is not JSON") @@ -607,11 +627,14 @@ async def help_test_update_with_json_attrs_bad_JSON( assert "Erroneous JSON: This is not JSON" in caplog.text -async def help_test_discovery_update_attr(hass, mqtt_mock, caplog, domain, config): +async def help_test_discovery_update_attr( + hass, mqtt_mock_entry_no_yaml_config, caplog, domain, config +): """Test update of discovered MQTTAttributes. This is a test helper for the MqttAttributes mixin. """ + await mqtt_mock_entry_no_yaml_config() # Add JSON attributes settings to config config1 = copy.deepcopy(config) config1[domain]["json_attributes_topic"] = "attr-topic1" @@ -641,18 +664,22 @@ async def help_test_discovery_update_attr(hass, mqtt_mock, caplog, domain, confi assert state.attributes.get("val") == "75" -async def help_test_unique_id(hass, mqtt_mock, domain, config): +async def help_test_unique_id(hass, mqtt_mock_entry_with_yaml_config, domain, config): """Test unique id option only creates one entity per unique_id.""" assert await async_setup_component(hass, domain, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert len(hass.states.async_entity_ids(domain)) == 1 -async def help_test_discovery_removal(hass, mqtt_mock, caplog, domain, data): +async def help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, domain, data +): """Test removal of discovered component. This is a test helper for the MqttDiscoveryUpdate mixin. """ + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() @@ -669,7 +696,7 @@ async def help_test_discovery_removal(hass, mqtt_mock, caplog, domain, data): async def help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, domain, discovery_config1, @@ -681,6 +708,7 @@ async def help_test_discovery_update( This is a test helper for the MqttDiscoveryUpdate mixin. """ + await mqtt_mock_entry_no_yaml_config() # Add some future configuration to the configurations config1 = copy.deepcopy(discovery_config1) config1["some_future_option_1"] = "future_option_1" @@ -730,12 +758,13 @@ async def help_test_discovery_update( async def help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, domain, data1, discovery_update + hass, mqtt_mock_entry_no_yaml_config, caplog, domain, data1, discovery_update ): """Test update of discovered component without changes. This is a test helper for the MqttDiscoveryUpdate mixin. """ + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data1) await hass.async_block_till_done() @@ -749,8 +778,11 @@ async def help_test_discovery_update_unchanged( assert not discovery_update.called -async def help_test_discovery_broken(hass, mqtt_mock, caplog, domain, data1, data2): +async def help_test_discovery_broken( + hass, mqtt_mock_entry_no_yaml_config, caplog, domain, data1, data2 +): """Test handling of bad discovery message.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data1) await hass.async_block_till_done() @@ -769,7 +801,7 @@ async def help_test_discovery_broken(hass, mqtt_mock, caplog, domain, data1, dat async def help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -849,6 +881,7 @@ async def help_test_encoding_subscribable_topics( hass, domain, {domain: [config1, config2, config3]} ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() expected_result = attribute_value or value @@ -899,11 +932,14 @@ async def help_test_encoding_subscribable_topics( pass -async def help_test_entity_device_info_with_identifier(hass, mqtt_mock, domain, config): +async def help_test_entity_device_info_with_identifier( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test device registry integration. This is a test helper for the MqttDiscoveryUpdate mixin. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -926,11 +962,14 @@ async def help_test_entity_device_info_with_identifier(hass, mqtt_mock, domain, assert device.configuration_url == "http://example.com" -async def help_test_entity_device_info_with_connection(hass, mqtt_mock, domain, config): +async def help_test_entity_device_info_with_connection( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test device registry integration. This is a test helper for the MqttDiscoveryUpdate mixin. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_MAC) @@ -955,8 +994,11 @@ async def help_test_entity_device_info_with_connection(hass, mqtt_mock, domain, assert device.configuration_url == "http://example.com" -async def help_test_entity_device_info_remove(hass, mqtt_mock, domain, config): +async def help_test_entity_device_info_remove( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test device registry remove.""" + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -981,11 +1023,14 @@ async def help_test_entity_device_info_remove(hass, mqtt_mock, domain, config): assert not ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique") -async def help_test_entity_device_info_update(hass, mqtt_mock, domain, config): +async def help_test_entity_device_info_update( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test device registry update. This is a test helper for the MqttDiscoveryUpdate mixin. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1012,7 +1057,7 @@ async def help_test_entity_device_info_update(hass, mqtt_mock, domain, config): async def help_test_entity_id_update_subscriptions( - hass, mqtt_mock, domain, config, topics=None + hass, mqtt_mock_entry_with_yaml_config, domain, config, topics=None ): """Test MQTT subscriptions are managed when entity_id is updated.""" # Add unique_id to config @@ -1026,16 +1071,18 @@ async def help_test_entity_id_update_subscriptions( topics = ["avty-topic", "test-topic"] assert len(topics) > 0 registry = mock_registry(hass, {}) + assert await async_setup_component( hass, domain, config, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state is not None - assert mqtt_mock.async_subscribe.call_count == len(topics) + assert mqtt_mock.async_subscribe.call_count == len(topics) + 3 for topic in topics: mqtt_mock.async_subscribe.assert_any_call(topic, ANY, ANY, ANY) mqtt_mock.async_subscribe.reset_mock() @@ -1053,10 +1100,11 @@ async def help_test_entity_id_update_subscriptions( async def help_test_entity_id_update_discovery_update( - hass, mqtt_mock, domain, config, topic=None + hass, mqtt_mock_entry_no_yaml_config, domain, config, topic=None ): """Test MQTT discovery update after entity_id is updated.""" # Add unique_id to config + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(config) config[domain]["unique_id"] = "TOTALLY_UNIQUE" @@ -1093,11 +1141,14 @@ async def help_test_entity_id_update_discovery_update( assert state.state != STATE_UNAVAILABLE -async def help_test_entity_debug_info(hass, mqtt_mock, domain, config): +async def help_test_entity_debug_info( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test debug_info. This is a test helper for MQTT debug_info. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1127,11 +1178,14 @@ async def help_test_entity_debug_info(hass, mqtt_mock, domain, config): assert len(debug_info_data["triggers"]) == 0 -async def help_test_entity_debug_info_max_messages(hass, mqtt_mock, domain, config): +async def help_test_entity_debug_info_max_messages( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test debug_info message overflow. This is a test helper for MQTT debug_info. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1181,7 +1235,7 @@ async def help_test_entity_debug_info_max_messages(hass, mqtt_mock, domain, conf async def help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, domain, config, service, @@ -1196,6 +1250,7 @@ async def help_test_entity_debug_info_message( This is a test helper for MQTT debug_info. """ # Add device settings to config + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" @@ -1290,11 +1345,14 @@ async def help_test_entity_debug_info_message( assert debug_info_data["entities"][0]["transmitted"] == expected_transmissions -async def help_test_entity_debug_info_remove(hass, mqtt_mock, domain, config): +async def help_test_entity_debug_info_remove( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test debug_info. This is a test helper for MQTT debug_info. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1333,11 +1391,14 @@ async def help_test_entity_debug_info_remove(hass, mqtt_mock, domain, config): assert entity_id not in hass.data[debug_info.DATA_MQTT_DEBUG_INFO]["entities"] -async def help_test_entity_debug_info_update_entity_id(hass, mqtt_mock, domain, config): +async def help_test_entity_debug_info_update_entity_id( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test debug_info. This is a test helper for MQTT debug_info. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1389,8 +1450,11 @@ async def help_test_entity_debug_info_update_entity_id(hass, mqtt_mock, domain, ) -async def help_test_entity_disabled_by_default(hass, mqtt_mock, domain, config): +async def help_test_entity_disabled_by_default( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test device registry remove.""" + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1425,8 +1489,11 @@ async def help_test_entity_disabled_by_default(hass, mqtt_mock, domain, config): assert not dev_registry.async_get_device({("mqtt", "helloworld")}) -async def help_test_entity_category(hass, mqtt_mock, domain, config): +async def help_test_entity_category( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test device registry remove.""" + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1468,7 +1535,7 @@ async def help_test_entity_category(hass, mqtt_mock, domain, config): async def help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -1519,6 +1586,7 @@ async def help_test_publishing_with_custom_encoding( {domain: setup_config}, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() # 1) test with default encoding await hass.services.async_call( @@ -1602,7 +1670,9 @@ async def help_test_reload_with_config(hass, caplog, tmp_path, domain, config): assert "" in caplog.text -async def help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config): +async def help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config +): """Test reloading an MQTT platform.""" # Create and test an old config of 2 entities based on the config supplied old_config_1 = copy.deepcopy(config) @@ -1614,6 +1684,7 @@ async def help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config hass, domain, {domain: [old_config_1, old_config_2]} ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get(f"{domain}.test_old_1") assert hass.states.get(f"{domain}.test_old_2") diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 565fa7fda53..6fe781335f0 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -264,8 +264,9 @@ async def test_hassio_confirm(hass, mock_try_connection_success, mock_finish_set assert len(mock_finish_setup.mock_calls) == 1 -async def test_option_flow(hass, mqtt_mock, mock_try_connection): +async def test_option_flow(hass, mqtt_mock_entry_no_yaml_config, mock_try_connection): """Test config flow options.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() mock_try_connection.return_value = True config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] config_entry.data = { @@ -336,8 +337,11 @@ async def test_option_flow(hass, mqtt_mock, mock_try_connection): assert mqtt_mock.async_connect.call_count == 1 -async def test_disable_birth_will(hass, mqtt_mock, mock_try_connection): +async def test_disable_birth_will( + hass, mqtt_mock_entry_no_yaml_config, mock_try_connection +): """Test disabling birth and will.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() mock_try_connection.return_value = True config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] config_entry.data = { @@ -417,9 +421,10 @@ def get_suggested(schema, key): async def test_option_flow_default_suggested_values( - hass, mqtt_mock, mock_try_connection_success + hass, mqtt_mock_entry_no_yaml_config, mock_try_connection_success ): """Test config flow options has default/suggested values.""" + await mqtt_mock_entry_no_yaml_config() config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] config_entry.data = { mqtt.CONF_BROKER: "test-broker", diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index e130b820c1b..5796c12f3cf 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -83,7 +83,7 @@ DEFAULT_CONFIG = { } -async def test_state_via_state_topic(hass, mqtt_mock): +async def test_state_via_state_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -102,6 +102,7 @@ async def test_state_via_state_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -118,7 +119,9 @@ async def test_state_via_state_topic(hass, mqtt_mock): assert state.state == STATE_OPEN -async def test_opening_and_closing_state_via_custom_state_payload(hass, mqtt_mock): +async def test_opening_and_closing_state_via_custom_state_payload( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling opening and closing state via a custom payload.""" assert await async_setup_component( hass, @@ -139,6 +142,7 @@ async def test_opening_and_closing_state_via_custom_state_payload(hass, mqtt_moc }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -160,7 +164,9 @@ async def test_opening_and_closing_state_via_custom_state_payload(hass, mqtt_moc assert state.state == STATE_CLOSED -async def test_open_closed_state_from_position_optimistic(hass, mqtt_mock): +async def test_open_closed_state_from_position_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the state after setting the position using optimistic mode.""" assert await async_setup_component( hass, @@ -180,6 +186,7 @@ async def test_open_closed_state_from_position_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -207,7 +214,7 @@ async def test_open_closed_state_from_position_optimistic(hass, mqtt_mock): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_position_via_position_topic(hass, mqtt_mock): +async def test_position_via_position_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -228,6 +235,7 @@ async def test_position_via_position_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -244,7 +252,7 @@ async def test_position_via_position_topic(hass, mqtt_mock): assert state.state == STATE_OPEN -async def test_state_via_template(hass, mqtt_mock): +async def test_state_via_template(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -266,6 +274,7 @@ async def test_state_via_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -281,7 +290,7 @@ async def test_state_via_template(hass, mqtt_mock): assert state.state == STATE_CLOSED -async def test_state_via_template_and_entity_id(hass, mqtt_mock): +async def test_state_via_template_and_entity_id(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -303,6 +312,7 @@ async def test_state_via_template_and_entity_id(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -320,7 +330,9 @@ async def test_state_via_template_and_entity_id(hass, mqtt_mock): assert state.state == STATE_CLOSED -async def test_state_via_template_with_json_value(hass, mqtt_mock, caplog): +async def test_state_via_template_with_json_value( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling state via topic with JSON value.""" assert await async_setup_component( hass, @@ -337,6 +349,7 @@ async def test_state_via_template_with_json_value(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -359,7 +372,9 @@ async def test_state_via_template_with_json_value(hass, mqtt_mock, caplog): ) in caplog.text -async def test_position_via_template_and_entity_id(hass, mqtt_mock): +async def test_position_via_template_and_entity_id( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -381,6 +396,7 @@ async def test_position_via_template_and_entity_id(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -411,7 +427,9 @@ async def test_position_via_template_and_entity_id(hass, mqtt_mock): ({"tilt_command_topic": "abc", "tilt_status_topic": "abc"}, False), ], ) -async def test_optimistic_flag(hass, mqtt_mock, config, assumed_state): +async def test_optimistic_flag( + hass, mqtt_mock_entry_with_yaml_config, config, assumed_state +): """Test assumed_state is set correctly.""" assert await async_setup_component( hass, @@ -419,6 +437,7 @@ async def test_optimistic_flag(hass, mqtt_mock, config, assumed_state): {cover.DOMAIN: {**config, "platform": "mqtt", "name": "test", "qos": 0}}, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -428,7 +447,7 @@ async def test_optimistic_flag(hass, mqtt_mock, config, assumed_state): assert ATTR_ASSUMED_STATE not in state.attributes -async def test_optimistic_state_change(hass, mqtt_mock): +async def test_optimistic_state_change(hass, mqtt_mock_entry_with_yaml_config): """Test changing state optimistically.""" assert await async_setup_component( hass, @@ -443,6 +462,7 @@ async def test_optimistic_state_change(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -484,7 +504,9 @@ async def test_optimistic_state_change(hass, mqtt_mock): assert state.state == STATE_CLOSED -async def test_optimistic_state_change_with_position(hass, mqtt_mock): +async def test_optimistic_state_change_with_position( + hass, mqtt_mock_entry_with_yaml_config +): """Test changing state optimistically.""" assert await async_setup_component( hass, @@ -501,6 +523,7 @@ async def test_optimistic_state_change_with_position(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -547,7 +570,7 @@ async def test_optimistic_state_change_with_position(hass, mqtt_mock): assert state.attributes.get(ATTR_CURRENT_POSITION) == 0 -async def test_send_open_cover_command(hass, mqtt_mock): +async def test_send_open_cover_command(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of open_cover.""" assert await async_setup_component( hass, @@ -563,6 +586,7 @@ async def test_send_open_cover_command(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -576,7 +600,7 @@ async def test_send_open_cover_command(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_send_close_cover_command(hass, mqtt_mock): +async def test_send_close_cover_command(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of close_cover.""" assert await async_setup_component( hass, @@ -592,6 +616,7 @@ async def test_send_close_cover_command(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -605,7 +630,7 @@ async def test_send_close_cover_command(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_send_stop__cover_command(hass, mqtt_mock): +async def test_send_stop__cover_command(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of stop_cover.""" assert await async_setup_component( hass, @@ -621,6 +646,7 @@ async def test_send_stop__cover_command(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -634,7 +660,7 @@ async def test_send_stop__cover_command(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_current_cover_position(hass, mqtt_mock): +async def test_current_cover_position(hass, mqtt_mock_entry_with_yaml_config): """Test the current cover position.""" assert await async_setup_component( hass, @@ -654,6 +680,7 @@ async def test_current_cover_position(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state_attributes_dict = hass.states.get("cover.test").attributes assert ATTR_CURRENT_POSITION not in state_attributes_dict @@ -685,7 +712,7 @@ async def test_current_cover_position(hass, mqtt_mock): assert current_cover_position == 100 -async def test_current_cover_position_inverted(hass, mqtt_mock): +async def test_current_cover_position_inverted(hass, mqtt_mock_entry_with_yaml_config): """Test the current cover position.""" assert await async_setup_component( hass, @@ -705,6 +732,7 @@ async def test_current_cover_position_inverted(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state_attributes_dict = hass.states.get("cover.test").attributes assert ATTR_CURRENT_POSITION not in state_attributes_dict @@ -747,7 +775,7 @@ async def test_current_cover_position_inverted(hass, mqtt_mock): assert hass.states.get("cover.test").state == STATE_CLOSED -async def test_optimistic_position(hass, mqtt_mock): +async def test_optimistic_position(hass, mqtt_mock_entry_no_yaml_config): """Test optimistic position is not supported.""" assert await async_setup_component( hass, @@ -762,12 +790,13 @@ async def test_optimistic_position(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("cover.test") assert state is None -async def test_position_update(hass, mqtt_mock): +async def test_position_update(hass, mqtt_mock_entry_with_yaml_config): """Test cover position update from received MQTT message.""" assert await async_setup_component( hass, @@ -788,6 +817,7 @@ async def test_position_update(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state_attributes_dict = hass.states.get("cover.test").attributes assert ATTR_CURRENT_POSITION not in state_attributes_dict @@ -809,7 +839,7 @@ async def test_position_update(hass, mqtt_mock): [("{{position-1}}", 43, "42"), ("{{100-62}}", 100, "38")], ) async def test_set_position_templated( - hass, mqtt_mock, pos_template, pos_call, pos_message + hass, mqtt_mock_entry_with_yaml_config, pos_template, pos_call, pos_message ): """Test setting cover position via template.""" assert await async_setup_component( @@ -832,6 +862,7 @@ async def test_set_position_templated( }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -845,7 +876,9 @@ async def test_set_position_templated( ) -async def test_set_position_templated_and_attributes(hass, mqtt_mock): +async def test_set_position_templated_and_attributes( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting cover position via template and using entities attributes.""" assert await async_setup_component( hass, @@ -876,6 +909,7 @@ async def test_set_position_templated_and_attributes(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -887,7 +921,7 @@ async def test_set_position_templated_and_attributes(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("set-position-topic", "5", 0, False) -async def test_set_tilt_templated(hass, mqtt_mock): +async def test_set_tilt_templated(hass, mqtt_mock_entry_with_yaml_config): """Test setting cover tilt position via template.""" assert await async_setup_component( hass, @@ -911,6 +945,7 @@ async def test_set_tilt_templated(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -924,7 +959,9 @@ async def test_set_tilt_templated(hass, mqtt_mock): ) -async def test_set_tilt_templated_and_attributes(hass, mqtt_mock): +async def test_set_tilt_templated_and_attributes( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting cover tilt position via template and using entities attributes.""" assert await async_setup_component( hass, @@ -952,6 +989,7 @@ async def test_set_tilt_templated_and_attributes(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1010,7 +1048,7 @@ async def test_set_tilt_templated_and_attributes(hass, mqtt_mock): ) -async def test_set_position_untemplated(hass, mqtt_mock): +async def test_set_position_untemplated(hass, mqtt_mock_entry_with_yaml_config): """Test setting cover position via template.""" assert await async_setup_component( hass, @@ -1029,6 +1067,7 @@ async def test_set_position_untemplated(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1040,7 +1079,9 @@ async def test_set_position_untemplated(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("position-topic", "62", 0, False) -async def test_set_position_untemplated_custom_percentage_range(hass, mqtt_mock): +async def test_set_position_untemplated_custom_percentage_range( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting cover position via template.""" assert await async_setup_component( hass, @@ -1061,6 +1102,7 @@ async def test_set_position_untemplated_custom_percentage_range(hass, mqtt_mock) }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1072,7 +1114,7 @@ async def test_set_position_untemplated_custom_percentage_range(hass, mqtt_mock) mqtt_mock.async_publish.assert_called_once_with("position-topic", "62", 0, False) -async def test_no_command_topic(hass, mqtt_mock): +async def test_no_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test with no command topic.""" assert await async_setup_component( hass, @@ -1091,11 +1133,12 @@ async def test_no_command_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get("cover.test").attributes["supported_features"] == 240 -async def test_no_payload_close(hass, mqtt_mock): +async def test_no_payload_close(hass, mqtt_mock_entry_with_yaml_config): """Test with no close payload.""" assert await async_setup_component( hass, @@ -1113,11 +1156,12 @@ async def test_no_payload_close(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get("cover.test").attributes["supported_features"] == 9 -async def test_no_payload_open(hass, mqtt_mock): +async def test_no_payload_open(hass, mqtt_mock_entry_with_yaml_config): """Test with no open payload.""" assert await async_setup_component( hass, @@ -1135,11 +1179,12 @@ async def test_no_payload_open(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get("cover.test").attributes["supported_features"] == 10 -async def test_no_payload_stop(hass, mqtt_mock): +async def test_no_payload_stop(hass, mqtt_mock_entry_with_yaml_config): """Test with no stop payload.""" assert await async_setup_component( hass, @@ -1157,11 +1202,12 @@ async def test_no_payload_stop(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get("cover.test").attributes["supported_features"] == 3 -async def test_with_command_topic_and_tilt(hass, mqtt_mock): +async def test_with_command_topic_and_tilt(hass, mqtt_mock_entry_with_yaml_config): """Test with command topic and tilt config.""" assert await async_setup_component( hass, @@ -1181,11 +1227,12 @@ async def test_with_command_topic_and_tilt(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get("cover.test").attributes["supported_features"] == 251 -async def test_tilt_defaults(hass, mqtt_mock): +async def test_tilt_defaults(hass, mqtt_mock_entry_with_yaml_config): """Test the defaults.""" assert await async_setup_component( hass, @@ -1206,6 +1253,7 @@ async def test_tilt_defaults(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state_attributes_dict = hass.states.get("cover.test").attributes assert ATTR_CURRENT_TILT_POSITION in state_attributes_dict @@ -1216,7 +1264,7 @@ async def test_tilt_defaults(hass, mqtt_mock): assert current_cover_position == STATE_UNKNOWN -async def test_tilt_via_invocation_defaults(hass, mqtt_mock): +async def test_tilt_via_invocation_defaults(hass, mqtt_mock_entry_with_yaml_config): """Test tilt defaults on close/open.""" assert await async_setup_component( hass, @@ -1237,6 +1285,7 @@ async def test_tilt_via_invocation_defaults(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1298,7 +1347,7 @@ async def test_tilt_via_invocation_defaults(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("tilt-command-topic", "0", 0, False) -async def test_tilt_given_value(hass, mqtt_mock): +async def test_tilt_given_value(hass, mqtt_mock_entry_with_yaml_config): """Test tilting to a given value.""" assert await async_setup_component( hass, @@ -1321,6 +1370,7 @@ async def test_tilt_given_value(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1386,7 +1436,7 @@ async def test_tilt_given_value(hass, mqtt_mock): ) -async def test_tilt_given_value_optimistic(hass, mqtt_mock): +async def test_tilt_given_value_optimistic(hass, mqtt_mock_entry_with_yaml_config): """Test tilting to a given value.""" assert await async_setup_component( hass, @@ -1410,6 +1460,7 @@ async def test_tilt_given_value_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1462,7 +1513,7 @@ async def test_tilt_given_value_optimistic(hass, mqtt_mock): ) -async def test_tilt_given_value_altered_range(hass, mqtt_mock): +async def test_tilt_given_value_altered_range(hass, mqtt_mock_entry_with_yaml_config): """Test tilting to a given value.""" assert await async_setup_component( hass, @@ -1488,6 +1539,7 @@ async def test_tilt_given_value_altered_range(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1538,7 +1590,7 @@ async def test_tilt_given_value_altered_range(hass, mqtt_mock): ) -async def test_tilt_via_topic(hass, mqtt_mock): +async def test_tilt_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test tilt by updating status via MQTT.""" assert await async_setup_component( hass, @@ -1559,6 +1611,7 @@ async def test_tilt_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "0") @@ -1575,7 +1628,7 @@ async def test_tilt_via_topic(hass, mqtt_mock): assert current_cover_tilt_position == 50 -async def test_tilt_via_topic_template(hass, mqtt_mock): +async def test_tilt_via_topic_template(hass, mqtt_mock_entry_with_yaml_config): """Test tilt by updating status via MQTT and template.""" assert await async_setup_component( hass, @@ -1599,6 +1652,7 @@ async def test_tilt_via_topic_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "99") @@ -1615,7 +1669,9 @@ async def test_tilt_via_topic_template(hass, mqtt_mock): assert current_cover_tilt_position == 50 -async def test_tilt_via_topic_template_json_value(hass, mqtt_mock, caplog): +async def test_tilt_via_topic_template_json_value( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test tilt by updating status via MQTT and template with JSON value.""" assert await async_setup_component( hass, @@ -1639,6 +1695,7 @@ async def test_tilt_via_topic_template_json_value(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", '{"Var1": 9, "Var2": 30}') @@ -1661,7 +1718,7 @@ async def test_tilt_via_topic_template_json_value(hass, mqtt_mock, caplog): ) in caplog.text -async def test_tilt_via_topic_altered_range(hass, mqtt_mock): +async def test_tilt_via_topic_altered_range(hass, mqtt_mock_entry_with_yaml_config): """Test tilt status via MQTT with altered tilt range.""" assert await async_setup_component( hass, @@ -1684,6 +1741,7 @@ async def test_tilt_via_topic_altered_range(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "0") @@ -1707,7 +1765,9 @@ async def test_tilt_via_topic_altered_range(hass, mqtt_mock): assert current_cover_tilt_position == 50 -async def test_tilt_status_out_of_range_warning(hass, caplog, mqtt_mock): +async def test_tilt_status_out_of_range_warning( + hass, caplog, mqtt_mock_entry_with_yaml_config +): """Test tilt status via MQTT tilt out of range warning message.""" assert await async_setup_component( hass, @@ -1730,6 +1790,7 @@ async def test_tilt_status_out_of_range_warning(hass, caplog, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "60") @@ -1738,7 +1799,9 @@ async def test_tilt_status_out_of_range_warning(hass, caplog, mqtt_mock): ) in caplog.text -async def test_tilt_status_not_numeric_warning(hass, caplog, mqtt_mock): +async def test_tilt_status_not_numeric_warning( + hass, caplog, mqtt_mock_entry_with_yaml_config +): """Test tilt status via MQTT tilt not numeric warning message.""" assert await async_setup_component( hass, @@ -1761,13 +1824,16 @@ async def test_tilt_status_not_numeric_warning(hass, caplog, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "abc") assert ("Payload 'abc' is not numeric") in caplog.text -async def test_tilt_via_topic_altered_range_inverted(hass, mqtt_mock): +async def test_tilt_via_topic_altered_range_inverted( + hass, mqtt_mock_entry_with_yaml_config +): """Test tilt status via MQTT with altered tilt range and inverted tilt position.""" assert await async_setup_component( hass, @@ -1790,6 +1856,7 @@ async def test_tilt_via_topic_altered_range_inverted(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "0") @@ -1813,7 +1880,9 @@ async def test_tilt_via_topic_altered_range_inverted(hass, mqtt_mock): assert current_cover_tilt_position == 50 -async def test_tilt_via_topic_template_altered_range(hass, mqtt_mock): +async def test_tilt_via_topic_template_altered_range( + hass, mqtt_mock_entry_with_yaml_config +): """Test tilt status via MQTT and template with altered tilt range.""" assert await async_setup_component( hass, @@ -1839,6 +1908,7 @@ async def test_tilt_via_topic_template_altered_range(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "99") @@ -1862,7 +1932,7 @@ async def test_tilt_via_topic_template_altered_range(hass, mqtt_mock): assert current_cover_tilt_position == 50 -async def test_tilt_position(hass, mqtt_mock): +async def test_tilt_position(hass, mqtt_mock_entry_with_yaml_config): """Test tilt via method invocation.""" assert await async_setup_component( hass, @@ -1883,6 +1953,7 @@ async def test_tilt_position(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1896,7 +1967,7 @@ async def test_tilt_position(hass, mqtt_mock): ) -async def test_tilt_position_templated(hass, mqtt_mock): +async def test_tilt_position_templated(hass, mqtt_mock_entry_with_yaml_config): """Test tilt position via template.""" assert await async_setup_component( hass, @@ -1918,6 +1989,7 @@ async def test_tilt_position_templated(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1931,7 +2003,7 @@ async def test_tilt_position_templated(hass, mqtt_mock): ) -async def test_tilt_position_altered_range(hass, mqtt_mock): +async def test_tilt_position_altered_range(hass, mqtt_mock_entry_with_yaml_config): """Test tilt via method invocation with altered range.""" assert await async_setup_component( hass, @@ -1956,6 +2028,7 @@ async def test_tilt_position_altered_range(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1969,7 +2042,7 @@ async def test_tilt_position_altered_range(hass, mqtt_mock): ) -async def test_find_percentage_in_range_defaults(hass, mqtt_mock): +async def test_find_percentage_in_range_defaults(hass): """Test find percentage in range with default range.""" mqtt_cover = MqttCover( hass, @@ -2012,7 +2085,7 @@ async def test_find_percentage_in_range_defaults(hass, mqtt_mock): assert mqtt_cover.find_percentage_in_range(44, "cover") == 44 -async def test_find_percentage_in_range_altered(hass, mqtt_mock): +async def test_find_percentage_in_range_altered(hass): """Test find percentage in range with altered range.""" mqtt_cover = MqttCover( hass, @@ -2055,7 +2128,7 @@ async def test_find_percentage_in_range_altered(hass, mqtt_mock): assert mqtt_cover.find_percentage_in_range(120, "cover") == 40 -async def test_find_percentage_in_range_defaults_inverted(hass, mqtt_mock): +async def test_find_percentage_in_range_defaults_inverted(hass): """Test find percentage in range with default range but inverted.""" mqtt_cover = MqttCover( hass, @@ -2098,7 +2171,7 @@ async def test_find_percentage_in_range_defaults_inverted(hass, mqtt_mock): assert mqtt_cover.find_percentage_in_range(44, "cover") == 56 -async def test_find_percentage_in_range_altered_inverted(hass, mqtt_mock): +async def test_find_percentage_in_range_altered_inverted(hass): """Test find percentage in range with altered range and inverted.""" mqtt_cover = MqttCover( hass, @@ -2141,7 +2214,7 @@ async def test_find_percentage_in_range_altered_inverted(hass, mqtt_mock): assert mqtt_cover.find_percentage_in_range(120, "cover") == 60 -async def test_find_in_range_defaults(hass, mqtt_mock): +async def test_find_in_range_defaults(hass): """Test find in range with default range.""" mqtt_cover = MqttCover( hass, @@ -2184,7 +2257,7 @@ async def test_find_in_range_defaults(hass, mqtt_mock): assert mqtt_cover.find_in_range_from_percent(44, "cover") == 44 -async def test_find_in_range_altered(hass, mqtt_mock): +async def test_find_in_range_altered(hass): """Test find in range with altered range.""" mqtt_cover = MqttCover( hass, @@ -2227,7 +2300,7 @@ async def test_find_in_range_altered(hass, mqtt_mock): assert mqtt_cover.find_in_range_from_percent(40, "cover") == 120 -async def test_find_in_range_defaults_inverted(hass, mqtt_mock): +async def test_find_in_range_defaults_inverted(hass): """Test find in range with default range but inverted.""" mqtt_cover = MqttCover( hass, @@ -2270,7 +2343,7 @@ async def test_find_in_range_defaults_inverted(hass, mqtt_mock): assert mqtt_cover.find_in_range_from_percent(56, "cover") == 44 -async def test_find_in_range_altered_inverted(hass, mqtt_mock): +async def test_find_in_range_altered_inverted(hass): """Test find in range with altered range and inverted.""" mqtt_cover = MqttCover( hass, @@ -2313,35 +2386,37 @@ async def test_find_in_range_altered_inverted(hass, mqtt_mock): assert mqtt_cover.find_in_range_from_percent(60, "cover") == 120 -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_valid_device_class(hass, mqtt_mock): +async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of a valid device class.""" assert await async_setup_component( hass, @@ -2356,12 +2431,13 @@ async def test_valid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.attributes.get("device_class") == "garage" -async def test_invalid_device_class(hass, mqtt_mock): +async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): """Test the setting of an invalid device class.""" assert await async_setup_component( hass, @@ -2376,54 +2452,67 @@ async def test_invalid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("cover.test") assert state is None -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG, MQTT_COVER_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + cover.DOMAIN, + DEFAULT_CONFIG, + MQTT_COVER_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_json( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique_id option only creates one cover per id.""" config = { cover.DOMAIN: [ @@ -2441,92 +2530,103 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, cover.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, config + ) -async def test_discovery_removal_cover(hass, mqtt_mock, caplog): +async def test_discovery_removal_cover(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered cover.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, cover.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, cover.DOMAIN, data + ) -async def test_discovery_update_cover(hass, mqtt_mock, caplog): +async def test_discovery_update_cover(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered cover.""" 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, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, cover.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_cover(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_cover( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered cover.""" data1 = '{ "name": "Beer", "command_topic": "test_topic" }' with patch( "homeassistant.components.mqtt.cover.MqttCover.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, cover.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + cover.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "command_topic": "test_topic#" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, cover.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, cover.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT cover device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT cover device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG, SERVICE_OPEN_COVER, @@ -2535,7 +2635,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): async def test_state_and_position_topics_state_not_set_via_position_topic( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test state is not set via position topic when both state and position topics are set.""" assert await async_setup_component( @@ -2557,6 +2657,7 @@ async def test_state_and_position_topics_state_not_set_via_position_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -2593,7 +2694,9 @@ async def test_state_and_position_topics_state_not_set_via_position_topic( assert state.state == STATE_CLOSED -async def test_set_state_via_position_using_stopped_state(hass, mqtt_mock): +async def test_set_state_via_position_using_stopped_state( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via position topic using stopped state.""" assert await async_setup_component( hass, @@ -2615,6 +2718,7 @@ async def test_set_state_via_position_using_stopped_state(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -2646,7 +2750,9 @@ async def test_set_state_via_position_using_stopped_state(hass, mqtt_mock): assert state.state == STATE_OPEN -async def test_position_via_position_topic_template(hass, mqtt_mock): +async def test_position_via_position_topic_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test position by updating status via position template.""" assert await async_setup_component( hass, @@ -2664,6 +2770,7 @@ async def test_position_via_position_topic_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "99") @@ -2680,7 +2787,9 @@ async def test_position_via_position_topic_template(hass, mqtt_mock): assert current_cover_position_position == 50 -async def test_position_via_position_topic_template_json_value(hass, mqtt_mock, caplog): +async def test_position_via_position_topic_template_json_value( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test position by updating status via position template with a JSON value.""" assert await async_setup_component( hass, @@ -2698,6 +2807,7 @@ async def test_position_via_position_topic_template_json_value(hass, mqtt_mock, }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", '{"Var1": 9, "Var2": 60}') @@ -2720,7 +2830,7 @@ async def test_position_via_position_topic_template_json_value(hass, mqtt_mock, ) in caplog.text -async def test_position_template_with_entity_id(hass, mqtt_mock): +async def test_position_template_with_entity_id(hass, mqtt_mock_entry_with_yaml_config): """Test position by updating status via position template.""" assert await async_setup_component( hass, @@ -2743,6 +2853,7 @@ async def test_position_template_with_entity_id(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "10") @@ -2759,7 +2870,9 @@ async def test_position_template_with_entity_id(hass, mqtt_mock): assert current_cover_position_position == 20 -async def test_position_via_position_topic_template_return_json(hass, mqtt_mock): +async def test_position_via_position_topic_template_return_json( + hass, mqtt_mock_entry_with_yaml_config +): """Test position by updating status via position template and returning json.""" assert await async_setup_component( hass, @@ -2777,6 +2890,7 @@ async def test_position_via_position_topic_template_return_json(hass, mqtt_mock) }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "55") @@ -2787,7 +2901,7 @@ async def test_position_via_position_topic_template_return_json(hass, mqtt_mock) async def test_position_via_position_topic_template_return_json_warning( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_with_yaml_config ): """Test position by updating status via position template returning json without position attribute.""" assert await async_setup_component( @@ -2806,6 +2920,7 @@ async def test_position_via_position_topic_template_return_json_warning( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "55") @@ -2816,7 +2931,7 @@ async def test_position_via_position_topic_template_return_json_warning( async def test_position_and_tilt_via_position_topic_template_return_json( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test position and tilt by updating the position via position template.""" assert await async_setup_component( @@ -2836,6 +2951,7 @@ async def test_position_and_tilt_via_position_topic_template_return_json( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "0") @@ -2857,7 +2973,9 @@ async def test_position_and_tilt_via_position_topic_template_return_json( assert current_cover_position == 99 and current_tilt_position == 49 -async def test_position_via_position_topic_template_all_variables(hass, mqtt_mock): +async def test_position_via_position_topic_template_all_variables( + hass, mqtt_mock_entry_with_yaml_config +): """Test position by updating status via position template.""" assert await async_setup_component( hass, @@ -2886,6 +3004,7 @@ async def test_position_via_position_topic_template_all_variables(hass, mqtt_moc }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "0") @@ -2901,7 +3020,9 @@ async def test_position_via_position_topic_template_all_variables(hass, mqtt_moc assert current_cover_position == 100 -async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock): +async def test_set_state_via_stopped_state_no_position_topic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via stopped state when no position topic.""" assert await async_setup_component( hass, @@ -2923,6 +3044,7 @@ async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "state-topic", "OPEN") @@ -2951,7 +3073,7 @@ async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock): async def test_position_via_position_topic_template_return_invalid_json( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_with_yaml_config ): """Test position by updating status via position template and returning invalid json.""" assert await async_setup_component( @@ -2970,6 +3092,7 @@ async def test_position_via_position_topic_template_return_invalid_json( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "55") @@ -2977,7 +3100,7 @@ async def test_position_via_position_topic_template_return_invalid_json( async def test_set_position_topic_without_get_position_topic_error( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_no_yaml_config ): """Test error when set_position_topic is used without position_topic.""" assert await async_setup_component( @@ -2994,13 +3117,16 @@ async def test_set_position_topic_without_get_position_topic_error( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert ( f"'{CONF_SET_POSITION_TOPIC}' must be set together with '{CONF_GET_POSITION_TOPIC}'." ) in caplog.text -async def test_value_template_without_state_topic_error(hass, caplog, mqtt_mock): +async def test_value_template_without_state_topic_error( + hass, caplog, mqtt_mock_entry_no_yaml_config +): """Test error when value_template is used and state_topic is missing.""" assert await async_setup_component( hass, @@ -3015,13 +3141,16 @@ async def test_value_template_without_state_topic_error(hass, caplog, mqtt_mock) }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert ( f"'{CONF_VALUE_TEMPLATE}' must be set together with '{CONF_STATE_TOPIC}'." ) in caplog.text -async def test_position_template_without_position_topic_error(hass, caplog, mqtt_mock): +async def test_position_template_without_position_topic_error( + hass, caplog, mqtt_mock_entry_no_yaml_config +): """Test error when position_template is used and position_topic is missing.""" assert await async_setup_component( hass, @@ -3036,6 +3165,7 @@ async def test_position_template_without_position_topic_error(hass, caplog, mqtt }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert ( f"'{CONF_GET_POSITION_TEMPLATE}' must be set together with '{CONF_GET_POSITION_TOPIC}'." @@ -3044,7 +3174,7 @@ async def test_position_template_without_position_topic_error(hass, caplog, mqtt async def test_set_position_template_without_set_position_topic( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_no_yaml_config ): """Test error when set_position_template is used and set_position_topic is missing.""" assert await async_setup_component( @@ -3060,6 +3190,7 @@ async def test_set_position_template_without_set_position_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert ( f"'{CONF_SET_POSITION_TEMPLATE}' must be set together with '{CONF_SET_POSITION_TOPIC}'." @@ -3068,7 +3199,7 @@ async def test_set_position_template_without_set_position_topic( async def test_tilt_command_template_without_tilt_command_topic( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_no_yaml_config ): """Test error when tilt_command_template is used and tilt_command_topic is missing.""" assert await async_setup_component( @@ -3084,6 +3215,7 @@ async def test_tilt_command_template_without_tilt_command_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert ( f"'{CONF_TILT_COMMAND_TEMPLATE}' must be set together with '{CONF_TILT_COMMAND_TOPIC}'." @@ -3092,7 +3224,7 @@ async def test_tilt_command_template_without_tilt_command_topic( async def test_tilt_status_template_without_tilt_status_topic_topic( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_no_yaml_config ): """Test error when tilt_status_template is used and tilt_status_topic is missing.""" assert await async_setup_component( @@ -3108,6 +3240,7 @@ async def test_tilt_status_template_without_tilt_status_topic_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert ( f"'{CONF_TILT_STATUS_TEMPLATE}' must be set together with '{CONF_TILT_STATUS_TOPIC}'." @@ -3143,7 +3276,7 @@ async def test_tilt_status_template_without_tilt_status_topic_topic( ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -3158,7 +3291,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -3170,11 +3303,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = cover.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -3194,12 +3329,18 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, cover.DOMAIN, DEFAULT_CONFIG[cover.DOMAIN], diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 020fbad6166..34042105af2 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -11,7 +11,9 @@ from tests.common import async_fire_mqtt_message # Deprecated in HA Core 2022.6 -async def test_legacy_ensure_device_tracker_platform_validation(hass, mqtt_mock): +async def test_legacy_ensure_device_tracker_platform_validation( + hass, mqtt_mock_entry_with_yaml_config +): """Test if platform validation was done.""" async def mock_setup_scanner(hass, config, see, discovery_info=None): @@ -29,12 +31,17 @@ async def test_legacy_ensure_device_tracker_platform_validation(hass, mqtt_mock) assert await async_setup_component( hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}}} ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert mock_sp.call_count == 1 # Deprecated in HA Core 2022.6 -async def test_legacy_new_message(hass, mock_device_tracker_conf, mqtt_mock): +async def test_legacy_new_message( + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config +): """Test new message.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" topic = "/location/paulus" @@ -51,9 +58,10 @@ async def test_legacy_new_message(hass, mock_device_tracker_conf, mqtt_mock): # Deprecated in HA Core 2022.6 async def test_legacy_single_level_wildcard_topic( - hass, mock_device_tracker_conf, mqtt_mock + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config ): """Test single level wildcard topic.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" subscription = "/location/+/paulus" @@ -73,9 +81,10 @@ async def test_legacy_single_level_wildcard_topic( # Deprecated in HA Core 2022.6 async def test_legacy_multi_level_wildcard_topic( - hass, mock_device_tracker_conf, mqtt_mock + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config ): """Test multi level wildcard topic.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" subscription = "/location/#" @@ -95,9 +104,10 @@ async def test_legacy_multi_level_wildcard_topic( # Deprecated in HA Core 2022.6 async def test_legacy_single_level_wildcard_topic_not_matching( - hass, mock_device_tracker_conf, mqtt_mock + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config ): """Test not matching single level wildcard topic.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" subscription = "/location/+/paulus" @@ -117,9 +127,10 @@ async def test_legacy_single_level_wildcard_topic_not_matching( # Deprecated in HA Core 2022.6 async def test_legacy_multi_level_wildcard_topic_not_matching( - hass, mock_device_tracker_conf, mqtt_mock + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config ): """Test not matching multi level wildcard topic.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" subscription = "/location/#" @@ -139,9 +150,10 @@ async def test_legacy_multi_level_wildcard_topic_not_matching( # Deprecated in HA Core 2022.6 async def test_legacy_matching_custom_payload_for_home_and_not_home( - hass, mock_device_tracker_conf, mqtt_mock + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config ): """Test custom payload_home sets state to home and custom payload_not_home sets state to not_home.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" topic = "/location/paulus" @@ -172,9 +184,10 @@ async def test_legacy_matching_custom_payload_for_home_and_not_home( # Deprecated in HA Core 2022.6 async def test_legacy_not_matching_custom_payload_for_home_and_not_home( - hass, mock_device_tracker_conf, mqtt_mock + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config ): """Test not matching payload does not set state to home or not_home.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" topic = "/location/paulus" @@ -202,8 +215,11 @@ async def test_legacy_not_matching_custom_payload_for_home_and_not_home( # Deprecated in HA Core 2022.6 -async def test_legacy_matching_source_type(hass, mock_device_tracker_conf, mqtt_mock): +async def test_legacy_matching_source_type( + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config +): """Test setting source type.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" topic = "/location/paulus" diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py index f8ee94b58f9..31853ad1dee 100644 --- a/tests/components/mqtt/test_device_tracker_discovery.py +++ b/tests/components/mqtt/test_device_tracker_discovery.py @@ -33,8 +33,9 @@ def entity_reg(hass): return mock_registry(hass) -async def test_discover_device_tracker(hass, mqtt_mock, caplog): +async def test_discover_device_tracker(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test discovering an MQTT device tracker component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -50,8 +51,9 @@ async def test_discover_device_tracker(hass, mqtt_mock, caplog): @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -74,8 +76,11 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): assert state.name == "Beer" -async def test_non_duplicate_device_tracker_discovery(hass, mqtt_mock, caplog): +async def test_non_duplicate_device_tracker_discovery( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test for a non duplicate component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -97,8 +102,9 @@ async def test_non_duplicate_device_tracker_discovery(hass, mqtt_mock, caplog): assert "Component has already been discovered: device_tracker bla" in caplog.text -async def test_device_tracker_removal(hass, mqtt_mock, caplog): +async def test_device_tracker_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of component through empty discovery message.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -114,8 +120,9 @@ async def test_device_tracker_removal(hass, mqtt_mock, caplog): assert state is None -async def test_device_tracker_rediscover(hass, mqtt_mock, caplog): +async def test_device_tracker_rediscover(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test rediscover of removed component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -140,8 +147,11 @@ async def test_device_tracker_rediscover(hass, mqtt_mock, caplog): assert state is not None -async def test_duplicate_device_tracker_removal(hass, mqtt_mock, caplog): +async def test_duplicate_device_tracker_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test for a non duplicate component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -160,8 +170,11 @@ async def test_duplicate_device_tracker_removal(hass, mqtt_mock, caplog): ) -async def test_device_tracker_discovery_update(hass, mqtt_mock, caplog): +async def test_device_tracker_discovery_update( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test for a discovery update event.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -186,10 +199,12 @@ async def test_device_tracker_discovery_update(hass, mqtt_mock, caplog): async def test_cleanup_device_tracker( - hass, hass_ws_client, device_reg, entity_reg, mqtt_mock + hass, hass_ws_client, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config ): """Test discovered device is cleaned up when removed from registry.""" assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_no_yaml_config() ws_client = await hass_ws_client(hass) async_fire_mqtt_message( @@ -242,8 +257,11 @@ async def test_cleanup_device_tracker( ) -async def test_setting_device_tracker_value_via_mqtt_message(hass, mqtt_mock, caplog): +async def test_setting_device_tracker_value_via_mqtt_message( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test the setting of the value via MQTT.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -266,9 +284,10 @@ async def test_setting_device_tracker_value_via_mqtt_message(hass, mqtt_mock, ca async def test_setting_device_tracker_value_via_mqtt_message_and_template( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test the setting of the value via MQTT.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -290,9 +309,10 @@ async def test_setting_device_tracker_value_via_mqtt_message_and_template( async def test_setting_device_tracker_value_via_mqtt_message_and_template2( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test the setting of the value via MQTT.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -317,9 +337,10 @@ async def test_setting_device_tracker_value_via_mqtt_message_and_template2( async def test_setting_device_tracker_location_via_mqtt_message( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test the setting of the location via MQTT.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -337,9 +358,10 @@ async def test_setting_device_tracker_location_via_mqtt_message( async def test_setting_device_tracker_location_via_lat_lon_message( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test the setting of the latitude and longitude via MQTT.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -391,8 +413,14 @@ async def test_setting_device_tracker_location_via_lat_lon_message( assert state.state == STATE_UNKNOWN -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, device_tracker.DOMAIN, DEFAULT_CONFIG, None + hass, + mqtt_mock_entry_no_yaml_config, + device_tracker.DOMAIN, + DEFAULT_CONFIG, + None, ) diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index ef5dd22692f..fe08c85a853 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -39,8 +39,11 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_triggers(hass, device_reg, entity_reg, mqtt_mock): +async def test_get_triggers( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test we get the expected triggers from a discovered mqtt device.""" + await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -70,8 +73,11 @@ async def test_get_triggers(hass, device_reg, entity_reg, mqtt_mock): assert_lists_same(triggers, expected_triggers) -async def test_get_unknown_triggers(hass, device_reg, entity_reg, mqtt_mock): +async def test_get_unknown_triggers( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test we don't get unknown triggers.""" + await mqtt_mock_entry_no_yaml_config() # Discover a sensor (without device triggers) data1 = ( '{ "device":{"identifiers":["0AFFD2"]},' @@ -112,8 +118,11 @@ async def test_get_unknown_triggers(hass, device_reg, entity_reg, mqtt_mock): assert_lists_same(triggers, []) -async def test_get_non_existing_triggers(hass, device_reg, entity_reg, mqtt_mock): +async def test_get_non_existing_triggers( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test getting non existing triggers.""" + await mqtt_mock_entry_no_yaml_config() # Discover a sensor (without device triggers) data1 = ( '{ "device":{"identifiers":["0AFFD2"]},' @@ -131,8 +140,11 @@ async def test_get_non_existing_triggers(hass, device_reg, entity_reg, mqtt_mock @pytest.mark.no_fail_on_log_exception -async def test_discover_bad_triggers(hass, device_reg, entity_reg, mqtt_mock): +async def test_discover_bad_triggers( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test bad discovery message.""" + await mqtt_mock_entry_no_yaml_config() # Test sending bad data data0 = ( '{ "automation_type":"trigger",' @@ -176,8 +188,11 @@ async def test_discover_bad_triggers(hass, device_reg, entity_reg, mqtt_mock): assert_lists_same(triggers, expected_triggers) -async def test_update_remove_triggers(hass, device_reg, entity_reg, mqtt_mock): +async def test_update_remove_triggers( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test triggers can be updated and removed.""" + await mqtt_mock_entry_no_yaml_config() config1 = { "automation_type": "trigger", "device": {"identifiers": ["0AFFD2"]}, @@ -240,8 +255,11 @@ async def test_update_remove_triggers(hass, device_reg, entity_reg, mqtt_mock): assert device_entry is None -async def test_if_fires_on_mqtt_message(hass, device_reg, calls, mqtt_mock): +async def test_if_fires_on_mqtt_message( + hass, device_reg, calls, mqtt_mock_entry_no_yaml_config +): """Test triggers firing.""" + await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -313,8 +331,11 @@ async def test_if_fires_on_mqtt_message(hass, device_reg, calls, mqtt_mock): assert calls[1].data["some"] == "long_press" -async def test_if_fires_on_mqtt_message_template(hass, device_reg, calls, mqtt_mock): +async def test_if_fires_on_mqtt_message_template( + hass, device_reg, calls, mqtt_mock_entry_no_yaml_config +): """Test triggers firing.""" + await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -389,9 +410,10 @@ async def test_if_fires_on_mqtt_message_template(hass, device_reg, calls, mqtt_m async def test_if_fires_on_mqtt_message_late_discover( - hass, device_reg, calls, mqtt_mock + hass, device_reg, calls, mqtt_mock_entry_no_yaml_config ): """Test triggers firing of MQTT device triggers discovered after setup.""" + await mqtt_mock_entry_no_yaml_config() data0 = ( '{ "device":{"identifiers":["0AFFD2"]},' ' "state_topic": "foobar/sensor",' @@ -472,9 +494,10 @@ async def test_if_fires_on_mqtt_message_late_discover( async def test_if_fires_on_mqtt_message_after_update( - hass, device_reg, calls, mqtt_mock + hass, device_reg, calls, mqtt_mock_entry_no_yaml_config ): """Test triggers firing after update.""" + await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -515,6 +538,7 @@ async def test_if_fires_on_mqtt_message_after_update( ] }, ) + await hass.async_block_till_done() # Fake short press. async_fire_mqtt_message(hass, "foobar/triggers/button1", "") @@ -546,8 +570,11 @@ async def test_if_fires_on_mqtt_message_after_update( assert len(calls) == 3 -async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock): +async def test_no_resubscribe_same_topic( + hass, device_reg, mqtt_mock_entry_no_yaml_config +): """Test subscription to topics without change.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -589,9 +616,10 @@ async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock): async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( - hass, device_reg, calls, mqtt_mock + hass, device_reg, calls, mqtt_mock_entry_no_yaml_config ): """Test triggers not firing after removal.""" + await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -625,6 +653,7 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( ] }, ) + await hass.async_block_till_done() # Fake short press. async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") @@ -649,10 +678,13 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( async def test_not_fires_on_mqtt_message_after_remove_from_registry( - hass, hass_ws_client, device_reg, calls, mqtt_mock + hass, hass_ws_client, device_reg, calls, mqtt_mock_entry_no_yaml_config ): """Test triggers not firing after removal.""" assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() + ws_client = await hass_ws_client(hass) data1 = ( @@ -713,8 +745,9 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( assert len(calls) == 1 -async def test_attach_remove(hass, device_reg, mqtt_mock): +async def test_attach_remove(hass, device_reg, mqtt_mock_entry_no_yaml_config): """Test attach and removal of trigger.""" + await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -766,8 +799,9 @@ async def test_attach_remove(hass, device_reg, mqtt_mock): assert len(calls) == 1 -async def test_attach_remove_late(hass, device_reg, mqtt_mock): +async def test_attach_remove_late(hass, device_reg, mqtt_mock_entry_no_yaml_config): """Test attach and removal of trigger .""" + await mqtt_mock_entry_no_yaml_config() data0 = ( '{ "device":{"identifiers":["0AFFD2"]},' ' "state_topic": "foobar/sensor",' @@ -827,8 +861,9 @@ async def test_attach_remove_late(hass, device_reg, mqtt_mock): assert len(calls) == 1 -async def test_attach_remove_late2(hass, device_reg, mqtt_mock): +async def test_attach_remove_late2(hass, device_reg, mqtt_mock_entry_no_yaml_config): """Test attach and removal of trigger .""" + await mqtt_mock_entry_no_yaml_config() data0 = ( '{ "device":{"identifiers":["0AFFD2"]},' ' "state_topic": "foobar/sensor",' @@ -882,8 +917,9 @@ async def test_attach_remove_late2(hass, device_reg, mqtt_mock): assert len(calls) == 0 -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT device registry integration.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) data = json.dumps( @@ -915,8 +951,9 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock): assert device.sw_version == "0.1-beta" -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT device registry integration.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) data = json.dumps( @@ -946,8 +983,9 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): assert device.sw_version == "0.1-beta" -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) config = { @@ -983,8 +1021,11 @@ async def test_entity_device_info_update(hass, mqtt_mock): assert device.name == "Milk" -async def test_cleanup_trigger(hass, hass_ws_client, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_trigger( + hass, hass_ws_client, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test trigger discovery topic is cleaned when device is removed from registry.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) @@ -1034,8 +1075,11 @@ async def test_cleanup_trigger(hass, hass_ws_client, device_reg, entity_reg, mqt ) -async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test removal from device registry when trigger is removed.""" + await mqtt_mock_entry_no_yaml_config() config = { "automation_type": "trigger", "topic": "test-topic", @@ -1065,8 +1109,11 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): assert device_entry is None -async def test_cleanup_device_several_triggers(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device_several_triggers( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test removal from device registry when the last trigger is removed.""" + await mqtt_mock_entry_no_yaml_config() config1 = { "automation_type": "trigger", "topic": "test-topic", @@ -1122,11 +1169,14 @@ async def test_cleanup_device_several_triggers(hass, device_reg, entity_reg, mqt assert device_entry is None -async def test_cleanup_device_with_entity1(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device_with_entity1( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test removal from device registry for device with entity. Trigger removed first, then entity. """ + await mqtt_mock_entry_no_yaml_config() config1 = { "automation_type": "trigger", "topic": "test-topic", @@ -1178,11 +1228,14 @@ async def test_cleanup_device_with_entity1(hass, device_reg, entity_reg, mqtt_mo assert device_entry is None -async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device_with_entity2( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test removal from device registry for device with entity. Entity removed first, then trigger. """ + await mqtt_mock_entry_no_yaml_config() config1 = { "automation_type": "trigger", "topic": "test-topic", @@ -1234,11 +1287,12 @@ async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mo assert device_entry is None -async def test_trigger_debug_info(hass, mqtt_mock): +async def test_trigger_debug_info(hass, mqtt_mock_entry_no_yaml_config): """Test debug_info. This is a test helper for MQTT debug_info. """ + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) config1 = { diff --git a/tests/components/mqtt/test_diagnostics.py b/tests/components/mqtt/test_diagnostics.py index bbd42a20c87..65399a22f70 100644 --- a/tests/components/mqtt/test_diagnostics.py +++ b/tests/components/mqtt/test_diagnostics.py @@ -37,8 +37,11 @@ def device_reg(hass): return mock_device_registry(hass) -async def test_entry_diagnostics(hass, device_reg, hass_client, mqtt_mock): +async def test_entry_diagnostics( + hass, device_reg, hass_client, mqtt_mock_entry_no_yaml_config +): """Test config entry diagnostics.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] mqtt_mock.connected = True @@ -154,8 +157,11 @@ async def test_entry_diagnostics(hass, device_reg, hass_client, mqtt_mock): } ], ) -async def test_redact_diagnostics(hass, device_reg, hass_client, mqtt_mock): +async def test_redact_diagnostics( + hass, device_reg, hass_client, mqtt_mock_entry_no_yaml_config +): """Test redacting diagnostics.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() expected_config = dict(default_config) expected_config["password"] = "**REDACTED**" expected_config["username"] = "**REDACTED**" diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 9215ab651b2..df20dc031d0 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -48,8 +48,9 @@ def entity_reg(hass): "mqtt_config", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) -async def test_subscribing_config_topic(hass, mqtt_mock): +async def test_subscribing_config_topic(hass, mqtt_mock_entry_no_yaml_config): """Test setting up discovery.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] discovery_topic = "homeassistant" @@ -71,8 +72,9 @@ async def test_subscribing_config_topic(hass, mqtt_mock): ("homeassistant/binary_sensor/rörkrökare/config", True), ], ) -async def test_invalid_topic(hass, mqtt_mock, caplog, topic, log): +async def test_invalid_topic(hass, mqtt_mock_entry_no_yaml_config, caplog, topic, log): """Test sending to invalid topic.""" + await mqtt_mock_entry_no_yaml_config() with patch( "homeassistant.components.mqtt.discovery.async_dispatcher_send" ) as mock_dispatcher_send: @@ -90,8 +92,9 @@ async def test_invalid_topic(hass, mqtt_mock, caplog, topic, log): caplog.clear() -async def test_invalid_json(hass, mqtt_mock, caplog): +async def test_invalid_json(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test sending in invalid JSON.""" + await mqtt_mock_entry_no_yaml_config() with patch( "homeassistant.components.mqtt.discovery.async_dispatcher_send" ) as mock_dispatcher_send: @@ -106,8 +109,9 @@ async def test_invalid_json(hass, mqtt_mock, caplog): assert not mock_dispatcher_send.called -async def test_only_valid_components(hass, mqtt_mock, caplog): +async def test_only_valid_components(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test for a valid component.""" + await mqtt_mock_entry_no_yaml_config() with patch( "homeassistant.components.mqtt.discovery.async_dispatcher_send" ) as mock_dispatcher_send: @@ -127,8 +131,9 @@ async def test_only_valid_components(hass, mqtt_mock, caplog): assert not mock_dispatcher_send.called -async def test_correct_config_discovery(hass, mqtt_mock, caplog): +async def test_correct_config_discovery(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test sending in correct JSON.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/binary_sensor/bla/config", @@ -143,8 +148,9 @@ async def test_correct_config_discovery(hass, mqtt_mock, caplog): assert ("binary_sensor", "bla") in hass.data[ALREADY_DISCOVERED] -async def test_discover_fan(hass, mqtt_mock, caplog): +async def test_discover_fan(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test discovering an MQTT fan.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/fan/bla/config", @@ -159,8 +165,9 @@ async def test_discover_fan(hass, mqtt_mock, caplog): assert ("fan", "bla") in hass.data[ALREADY_DISCOVERED] -async def test_discover_climate(hass, mqtt_mock, caplog): +async def test_discover_climate(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test discovering an MQTT climate component.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "name": "ClimateTest",' ' "current_temperature_topic": "climate/bla/current_temp",' @@ -177,8 +184,11 @@ async def test_discover_climate(hass, mqtt_mock, caplog): assert ("climate", "bla") in hass.data[ALREADY_DISCOVERED] -async def test_discover_alarm_control_panel(hass, mqtt_mock, caplog): +async def test_discover_alarm_control_panel( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test discovering an MQTT alarm control panel component.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "name": "AlarmControlPanelTest",' ' "state_topic": "test_topic",' @@ -341,9 +351,10 @@ async def test_discover_alarm_control_panel(hass, mqtt_mock, caplog): ], ) async def test_discovery_with_object_id( - hass, mqtt_mock, caplog, topic, config, entity_id, name, domain + hass, mqtt_mock_entry_no_yaml_config, caplog, topic, config, entity_id, name, domain ): """Test discovering an MQTT entity with object_id.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message(hass, topic, config) await hass.async_block_till_done() @@ -354,8 +365,9 @@ async def test_discovery_with_object_id( assert (domain, "object bla") in hass.data[ALREADY_DISCOVERED] -async def test_discovery_incl_nodeid(hass, mqtt_mock, caplog): +async def test_discovery_incl_nodeid(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test sending in correct JSON with optional node_id included.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/binary_sensor/my_node_id/bla/config", @@ -370,8 +382,9 @@ async def test_discovery_incl_nodeid(hass, mqtt_mock, caplog): assert ("binary_sensor", "my_node_id bla") in hass.data[ALREADY_DISCOVERED] -async def test_non_duplicate_discovery(hass, mqtt_mock, caplog): +async def test_non_duplicate_discovery(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test for a non duplicate component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/binary_sensor/bla/config", @@ -393,8 +406,9 @@ async def test_non_duplicate_discovery(hass, mqtt_mock, caplog): assert "Component has already been discovered: binary_sensor bla" in caplog.text -async def test_removal(hass, mqtt_mock, caplog): +async def test_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of component through empty discovery message.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/binary_sensor/bla/config", @@ -410,8 +424,9 @@ async def test_removal(hass, mqtt_mock, caplog): assert state is None -async def test_rediscover(hass, mqtt_mock, caplog): +async def test_rediscover(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test rediscover of removed component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/binary_sensor/bla/config", @@ -436,9 +451,9 @@ async def test_rediscover(hass, mqtt_mock, caplog): assert state is not None -async def test_rapid_rediscover(hass, mqtt_mock, caplog): +async def test_rapid_rediscover(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test immediate rediscover of removed component.""" - + await mqtt_mock_entry_no_yaml_config() events = async_capture_events(hass, EVENT_STATE_CHANGED) async_fire_mqtt_message( @@ -485,9 +500,9 @@ async def test_rapid_rediscover(hass, mqtt_mock, caplog): assert events[4].data["old_state"] is None -async def test_rapid_rediscover_unique(hass, mqtt_mock, caplog): +async def test_rapid_rediscover_unique(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test immediate rediscover of removed component.""" - + await mqtt_mock_entry_no_yaml_config() events = [] @ha.callback @@ -544,9 +559,9 @@ async def test_rapid_rediscover_unique(hass, mqtt_mock, caplog): assert events[3].data["old_state"] is None -async def test_rapid_reconfigure(hass, mqtt_mock, caplog): +async def test_rapid_reconfigure(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test immediate reconfigure of added component.""" - + await mqtt_mock_entry_no_yaml_config() events = [] @ha.callback @@ -596,8 +611,9 @@ async def test_rapid_reconfigure(hass, mqtt_mock, caplog): assert events[2].data["new_state"].attributes["friendly_name"] == "Wine" -async def test_duplicate_removal(hass, mqtt_mock, caplog): +async def test_duplicate_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test for a non duplicate component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/binary_sensor/bla/config", @@ -614,8 +630,11 @@ async def test_duplicate_removal(hass, mqtt_mock, caplog): assert "Component has already been discovered: binary_sensor bla" not in caplog.text -async def test_cleanup_device(hass, hass_ws_client, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device( + hass, hass_ws_client, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test discvered device is cleaned up when entry removed from device.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) @@ -669,8 +688,11 @@ async def test_cleanup_device(hass, hass_ws_client, device_reg, entity_reg, mqtt ) -async def test_cleanup_device_mqtt(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device_mqtt( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test discvered device is cleaned up when removed through MQTT.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() data = ( '{ "device":{"identifiers":["0AFFD2"]},' ' "state_topic": "foobar/sensor",' @@ -709,10 +731,12 @@ async def test_cleanup_device_mqtt(hass, device_reg, entity_reg, mqtt_mock): async def test_cleanup_device_multiple_config_entries( - hass, hass_ws_client, device_reg, entity_reg, mqtt_mock + hass, hass_ws_client, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config ): """Test discovered device is cleaned up when entry removed from device.""" assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_no_yaml_config() ws_client = await hass_ws_client(hass) config_entry = MockConfigEntry(domain="test", data={}) @@ -804,9 +828,10 @@ async def test_cleanup_device_multiple_config_entries( async def test_cleanup_device_multiple_config_entries_mqtt( - hass, device_reg, entity_reg, mqtt_mock + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config ): """Test discovered device is cleaned up when removed through MQTT.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( @@ -880,8 +905,9 @@ async def test_cleanup_device_multiple_config_entries_mqtt( mqtt_mock.async_publish.assert_not_called() -async def test_discovery_expansion(hass, mqtt_mock, caplog): +async def test_discovery_expansion(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test expansion of abbreviated discovery payload.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "~": "some/base/topic",' ' "name": "DiscoveryExpansionTest1",' @@ -937,8 +963,9 @@ async def test_discovery_expansion(hass, mqtt_mock, caplog): assert state.state == STATE_UNAVAILABLE -async def test_discovery_expansion_2(hass, mqtt_mock, caplog): +async def test_discovery_expansion_2(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test expansion of abbreviated discovery payload.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "~": "some/base/topic",' ' "name": "DiscoveryExpansionTest1",' @@ -977,8 +1004,9 @@ async def test_discovery_expansion_2(hass, mqtt_mock, caplog): @pytest.mark.no_fail_on_log_exception -async def test_discovery_expansion_3(hass, mqtt_mock, caplog): +async def test_discovery_expansion_3(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test expansion of broken discovery payload.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "~": "some/base/topic",' ' "name": "DiscoveryExpansionTest1",' @@ -1008,9 +1036,10 @@ async def test_discovery_expansion_3(hass, mqtt_mock, caplog): async def test_discovery_expansion_without_encoding_and_value_template_1( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test expansion of raw availability payload with a template as list.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "~": "some/base/topic",' ' "name": "DiscoveryExpansionTest1",' @@ -1056,9 +1085,10 @@ async def test_discovery_expansion_without_encoding_and_value_template_1( async def test_discovery_expansion_without_encoding_and_value_template_2( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test expansion of raw availability payload with a template directly.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "~": "some/base/topic",' ' "name": "DiscoveryExpansionTest1",' @@ -1133,8 +1163,11 @@ ABBREVIATIONS_WHITE_LIST = [ ] -async def test_missing_discover_abbreviations(hass, mqtt_mock, caplog): +async def test_missing_discover_abbreviations( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Check MQTT platforms for missing abbreviations.""" + await mqtt_mock_entry_no_yaml_config() missing = [] regex = re.compile(r"(CONF_[a-zA-Z\d_]*) *= *[\'\"]([a-zA-Z\d_]*)[\'\"]") for fil in Path(mqtt.__file__).parent.rglob("*.py"): @@ -1157,8 +1190,11 @@ async def test_missing_discover_abbreviations(hass, mqtt_mock, caplog): assert not missing -async def test_no_implicit_state_topic_switch(hass, mqtt_mock, caplog): +async def test_no_implicit_state_topic_switch( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test no implicit state topic for switch.""" + await mqtt_mock_entry_no_yaml_config() data = '{ "name": "Test1",' ' "command_topic": "cmnd"' "}" async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data) @@ -1187,8 +1223,11 @@ async def test_no_implicit_state_topic_switch(hass, mqtt_mock, caplog): } ], ) -async def test_complex_discovery_topic_prefix(hass, mqtt_mock, caplog): +async def test_complex_discovery_topic_prefix( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Tests handling of discovery topic prefix with multiple slashes.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, ("my_home/homeassistant/register/binary_sensor/node1/object1/config"), @@ -1204,9 +1243,10 @@ async def test_complex_discovery_topic_prefix(hass, mqtt_mock, caplog): async def test_mqtt_integration_discovery_subscribe_unsubscribe( - hass, mqtt_client_mock, mqtt_mock + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config ): """Check MQTT integration discovery subscribe and unsubscribe.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() mock_entity_platform(hass, "config_flow.comp", None) entry = hass.config_entries.async_entries("mqtt")[0] @@ -1243,8 +1283,11 @@ async def test_mqtt_integration_discovery_subscribe_unsubscribe( assert not mqtt_client_mock.unsubscribe.called -async def test_mqtt_discovery_unsubscribe_once(hass, mqtt_client_mock, mqtt_mock): +async def test_mqtt_discovery_unsubscribe_once( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Check MQTT integration discovery unsubscribe once.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() mock_entity_platform(hass, "config_flow.comp", None) entry = hass.config_entries.async_entries("mqtt")[0] diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 1a533db63c0..145edf5ac7d 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -74,16 +74,25 @@ DEFAULT_CONFIG = { } -async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): +async def test_fail_setup_if_no_command_topic( + hass, caplog, mqtt_mock_entry_no_yaml_config +): """Test if command fails with command topic.""" assert await async_setup_component( hass, fan.DOMAIN, {fan.DOMAIN: {"platform": "mqtt", "name": "test"}} ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert hass.states.get("fan.test") is None + assert ( + "Invalid config for [fan.mqtt]: required key not provided @ data['command_topic']" + in caplog.text + ) -async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -120,6 +129,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -202,7 +212,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): async def test_controlling_state_via_topic_with_different_speed_range( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the controlling state via topic using an alternate speed range.""" assert await async_setup_component( @@ -241,6 +251,7 @@ async def test_controlling_state_via_topic_with_different_speed_range( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "percentage-state-topic1", "100") state = hass.states.get("fan.test1") @@ -264,7 +275,7 @@ async def test_controlling_state_via_topic_with_different_speed_range( async def test_controlling_state_via_topic_no_percentage_topics( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the controlling state via topic without percentage topics.""" assert await async_setup_component( @@ -289,6 +300,7 @@ async def test_controlling_state_via_topic_no_percentage_topics( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -318,7 +330,9 @@ async def test_controlling_state_via_topic_no_percentage_topics( caplog.clear() -async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic_and_json_message( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling state via topic and JSON message (percentage mode).""" assert await async_setup_component( hass, @@ -353,6 +367,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -421,7 +436,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap async def test_controlling_state_via_topic_and_json_message_shared_topic( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the controlling state via topic and JSON message using a shared topic.""" assert await async_setup_component( @@ -457,6 +472,7 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -509,7 +525,9 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( caplog.clear() -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -535,6 +553,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -630,7 +649,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_commands_with_alternate_speed_range(hass, mqtt_mock): +async def test_sending_mqtt_commands_with_alternate_speed_range( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via topic using an alternate speed range.""" assert await async_setup_component( hass, @@ -668,6 +689,7 @@ async def test_sending_mqtt_commands_with_alternate_speed_range(hass, mqtt_mock) }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_set_percentage(hass, "fan.test1", 0) mqtt_mock.async_publish.assert_called_once_with( @@ -734,7 +756,9 @@ async def test_sending_mqtt_commands_with_alternate_speed_range(hass, mqtt_mock) assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, caplog): +async def test_sending_mqtt_commands_and_optimistic_no_legacy( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test optimistic mode without state topic without legacy speed command topic.""" assert await async_setup_component( hass, @@ -755,6 +779,7 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -862,7 +887,9 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c await common.async_turn_on(hass, "fan.test", preset_mode="freaking-high") -async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): +async def test_sending_mqtt_command_templates_( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test optimistic mode without state topic without legacy speed command topic.""" assert await async_setup_component( hass, @@ -888,6 +915,7 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -1002,7 +1030,7 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test optimistic mode without state topic without percentage command topic.""" assert await async_setup_component( @@ -1025,6 +1053,7 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -1061,7 +1090,9 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, caplog): +async def test_sending_mqtt_commands_and_explicit_optimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test optimistic mode with state topic and turn on attributes.""" assert await async_setup_component( hass, @@ -1088,6 +1119,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -1305,7 +1337,13 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[fan.DOMAIN]) @@ -1315,7 +1353,7 @@ async def test_encoding_subscribable_topics( config[CONF_OSCILLATION_COMMAND_TOPIC] = "fan/some_oscillation_command_topic" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, fan.DOMAIN, config, @@ -1326,7 +1364,7 @@ async def test_encoding_subscribable_topics( ) -async def test_attributes(hass, mqtt_mock, caplog): +async def test_attributes(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test attributes.""" assert await async_setup_component( hass, @@ -1347,6 +1385,7 @@ async def test_attributes(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -1376,7 +1415,7 @@ async def test_attributes(hass, mqtt_mock, caplog): assert state.attributes.get(fan.ATTR_OSCILLATING) is False -async def test_supported_features(hass, mqtt_mock): +async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -1498,6 +1537,7 @@ async def test_supported_features(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test1") assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 @@ -1548,77 +1588,103 @@ async def test_supported_features(hass, mqtt_mock): assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + fan.DOMAIN, + DEFAULT_CONFIG, + True, + "state-topic", + "1", ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + fan.DOMAIN, + DEFAULT_CONFIG, + True, + "state-topic", + "1", ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG, MQTT_FAN_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + fan.DOMAIN, + DEFAULT_CONFIG, + MQTT_FAN_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_json( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique_id option only creates one fan per id.""" config = { fan.DOMAIN: [ @@ -1638,89 +1704,107 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, fan.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, config + ) -async def test_discovery_removal_fan(hass, mqtt_mock, caplog): +async def test_discovery_removal_fan(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered fan.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, fan.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, fan.DOMAIN, data + ) -async def test_discovery_update_fan(hass, mqtt_mock, caplog): +async def test_discovery_update_fan(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered fan.""" 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 + hass, mqtt_mock_entry_no_yaml_config, caplog, fan.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_fan(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_fan( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered fan.""" data1 = '{ "name": "Beer", "command_topic": "test_topic" }' with patch( "homeassistant.components.mqtt.fan.MqttFan.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, fan.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + fan.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' - await help_test_discovery_broken(hass, mqtt_mock, caplog, fan.DOMAIN, data1, data2) + + await help_test_discovery_broken( + hass, mqtt_mock_entry_no_yaml_config, caplog, fan.DOMAIN, data1, data2 + ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG, fan.SERVICE_TURN_ON + hass, + mqtt_mock_entry_no_yaml_config, + fan.DOMAIN, + DEFAULT_CONFIG, + fan.SERVICE_TURN_ON, ) @@ -1766,7 +1850,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -1782,7 +1866,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -1794,11 +1878,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = fan.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index bc00d3afffb..ea9a6edf0e3 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -116,7 +116,7 @@ async def async_set_humidity( await hass.services.async_call(DOMAIN, SERVICE_SET_HUMIDITY, data, blocking=True) -async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): +async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_config): """Test if command fails with command topic.""" assert await async_setup_component( hass, @@ -124,10 +124,13 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): {humidifier.DOMAIN: {"platform": "mqtt", "name": "test"}}, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert hass.states.get("humidifier.test") is None -async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -158,6 +161,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -228,7 +232,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): assert state.state == STATE_UNKNOWN -async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic_and_json_message( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, @@ -255,6 +261,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -314,7 +321,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap async def test_controlling_state_via_topic_and_json_message_shared_topic( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the controlling state via topic and JSON message using a shared topic.""" assert await async_setup_component( @@ -342,6 +349,7 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -390,7 +398,9 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( caplog.clear() -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -413,6 +423,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -483,7 +494,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): +async def test_sending_mqtt_command_templates_( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Testing command templates with optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -507,6 +520,7 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -577,7 +591,9 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, caplog): +async def test_sending_mqtt_commands_and_explicit_optimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test optimistic mode with state topic and turn on attributes.""" assert await async_setup_component( hass, @@ -602,6 +618,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -701,7 +718,13 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[humidifier.DOMAIN]) @@ -709,7 +732,7 @@ async def test_encoding_subscribable_topics( config[CONF_MODE_COMMAND_TOPIC] = "humidifier/some_mode_command_topic" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, humidifier.DOMAIN, config, @@ -720,7 +743,7 @@ async def test_encoding_subscribable_topics( ) -async def test_attributes(hass, mqtt_mock, caplog): +async def test_attributes(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test attributes.""" assert await async_setup_component( hass, @@ -740,6 +763,7 @@ async def test_attributes(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -765,7 +789,7 @@ async def test_attributes(hass, mqtt_mock, caplog): assert state.attributes.get(humidifier.ATTR_MODE) is None -async def test_invalid_configurations(hass, mqtt_mock, caplog): +async def test_invalid_configurations(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test invalid configurations.""" assert await async_setup_component( hass, @@ -834,6 +858,7 @@ async def test_invalid_configurations(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get("humidifier.test_valid_1") is not None assert hass.states.get("humidifier.test_valid_2") is not None assert hass.states.get("humidifier.test_valid_3") is not None @@ -847,7 +872,7 @@ async def test_invalid_configurations(hass, mqtt_mock, caplog): assert hass.states.get("humidifier.test_invalid_mode_is_reset") is None -async def test_supported_features(hass, mqtt_mock): +async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test supported features.""" assert await async_setup_component( hass, @@ -896,6 +921,7 @@ async def test_supported_features(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test1") assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 @@ -916,81 +942,111 @@ async def test_supported_features(hass, mqtt_mock): assert state is None -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + humidifier.DOMAIN, + DEFAULT_CONFIG, + True, + "state-topic", + "1", ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + humidifier.DOMAIN, + DEFAULT_CONFIG, + True, + "state-topic", + "1", ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG, MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, humidifier.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + humidifier.DOMAIN, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_json( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, humidifier.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + humidifier.DOMAIN, + DEFAULT_CONFIG, ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique_id option only creates one fan per id.""" config = { humidifier.DOMAIN: [ @@ -1012,16 +1068,24 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, humidifier.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, config + ) -async def test_discovery_removal_humidifier(hass, mqtt_mock, caplog): +async def test_discovery_removal_humidifier( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test removal of discovered humidifier.""" data = '{ "name": "test", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, humidifier.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, humidifier.DOMAIN, data + ) -async def test_discovery_update_humidifier(hass, mqtt_mock, caplog): +async def test_discovery_update_humidifier( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered humidifier.""" config1 = { "name": "Beer", @@ -1034,77 +1098,93 @@ async def test_discovery_update_humidifier(hass, mqtt_mock, caplog): "target_humidity_command_topic": "test-topic2", } await help_test_discovery_update( - hass, mqtt_mock, caplog, humidifier.DOMAIN, config1, config2 + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + humidifier.DOMAIN, + config1, + config2, ) -async def test_discovery_update_unchanged_humidifier(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_humidifier( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered humidifier.""" data1 = '{ "name": "Beer", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' with patch( "homeassistant.components.mqtt.fan.MqttFan.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, humidifier.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + humidifier.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, humidifier.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, humidifier.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG, humidifier.SERVICE_TURN_ON + hass, + mqtt_mock_entry_no_yaml_config, + humidifier.DOMAIN, + DEFAULT_CONFIG, + humidifier.SERVICE_TURN_ON, ) @@ -1143,7 +1223,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -1159,7 +1239,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -1171,11 +1251,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = humidifier.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index aa0bfb82608..861b50d2f71 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -96,22 +96,27 @@ def record_calls(calls): async def test_mqtt_connects_on_home_assistant_mqtt_setup( - hass, mqtt_client_mock, mqtt_mock + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config ): """Test if client is connected after mqtt init on bootstrap.""" + await mqtt_mock_entry_no_yaml_config() assert mqtt_client_mock.connect.call_count == 1 -async def test_mqtt_disconnects_on_home_assistant_stop(hass, mqtt_mock): +async def test_mqtt_disconnects_on_home_assistant_stop( + hass, mqtt_mock_entry_no_yaml_config +): """Test if client stops on HA stop.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() hass.bus.fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() await hass.async_block_till_done() assert mqtt_mock.async_disconnect.called -async def test_publish(hass, mqtt_mock): +async def test_publish(hass, mqtt_mock_entry_no_yaml_config): """Test the publish function.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await mqtt.async_publish(hass, "test-topic", "test-payload") await hass.async_block_till_done() assert mqtt_mock.async_publish.called @@ -208,7 +213,7 @@ async def test_command_template_value(hass): assert cmd_tpl.async_render(None, variables=variables) == "beer" -async def test_command_template_variables(hass, mqtt_mock): +async def test_command_template_variables(hass, mqtt_mock_entry_with_yaml_config): """Test the rendering of enitity_variables.""" topic = "test/select" @@ -232,6 +237,7 @@ async def test_command_template_variables(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("select.test_select") assert state.state == "milk" @@ -291,8 +297,11 @@ async def test_value_template_value(hass): assert val_tpl.async_render_with_possible_json_value('{"id": 4321}') == "4321" -async def test_service_call_without_topic_does_not_publish(hass, mqtt_mock): +async def test_service_call_without_topic_does_not_publish( + hass, mqtt_mock_entry_no_yaml_config +): """Test the service call if topic is missing.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() with pytest.raises(vol.Invalid): await hass.services.async_call( mqtt.DOMAIN, @@ -304,12 +313,13 @@ async def test_service_call_without_topic_does_not_publish(hass, mqtt_mock): async def test_service_call_with_topic_and_topic_template_does_not_publish( - hass, mqtt_mock + hass, mqtt_mock_entry_no_yaml_config ): """Test the service call with topic/topic template. If both 'topic' and 'topic_template' are provided then fail. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() topic = "test/topic" topic_template = "test/{{ 'topic' }}" with pytest.raises(vol.Invalid): @@ -327,9 +337,10 @@ async def test_service_call_with_topic_and_topic_template_does_not_publish( async def test_service_call_with_invalid_topic_template_does_not_publish( - hass, mqtt_mock + hass, mqtt_mock_entry_no_yaml_config ): """Test the service call with a problematic topic template.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, @@ -342,11 +353,14 @@ async def test_service_call_with_invalid_topic_template_does_not_publish( assert not mqtt_mock.async_publish.called -async def test_service_call_with_template_topic_renders_template(hass, mqtt_mock): +async def test_service_call_with_template_topic_renders_template( + hass, mqtt_mock_entry_no_yaml_config +): """Test the service call with rendered topic template. If 'topic_template' is provided and 'topic' is not, then render it. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, @@ -360,11 +374,14 @@ async def test_service_call_with_template_topic_renders_template(hass, mqtt_mock assert mqtt_mock.async_publish.call_args[0][0] == "test/2" -async def test_service_call_with_template_topic_renders_invalid_topic(hass, mqtt_mock): +async def test_service_call_with_template_topic_renders_invalid_topic( + hass, mqtt_mock_entry_no_yaml_config +): """Test the service call with rendered, invalid topic template. If a wildcard topic is rendered, then fail. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, @@ -378,12 +395,13 @@ async def test_service_call_with_template_topic_renders_invalid_topic(hass, mqtt async def test_service_call_with_invalid_rendered_template_topic_doesnt_render_template( - hass, mqtt_mock + hass, mqtt_mock_entry_no_yaml_config ): """Test the service call with unrendered template. If both 'payload' and 'payload_template' are provided then fail. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() payload = "not a template" payload_template = "a template" with pytest.raises(vol.Invalid): @@ -400,11 +418,14 @@ async def test_service_call_with_invalid_rendered_template_topic_doesnt_render_t assert not mqtt_mock.async_publish.called -async def test_service_call_with_template_payload_renders_template(hass, mqtt_mock): +async def test_service_call_with_template_payload_renders_template( + hass, mqtt_mock_entry_no_yaml_config +): """Test the service call with rendered template. If 'payload_template' is provided and 'payload' is not, then render it. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, @@ -429,8 +450,9 @@ async def test_service_call_with_template_payload_renders_template(hass, mqtt_mo mqtt_mock.reset_mock() -async def test_service_call_with_bad_template(hass, mqtt_mock): +async def test_service_call_with_bad_template(hass, mqtt_mock_entry_no_yaml_config): """Test the service call with a bad template does not publish.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, @@ -440,11 +462,14 @@ async def test_service_call_with_bad_template(hass, mqtt_mock): assert not mqtt_mock.async_publish.called -async def test_service_call_with_payload_doesnt_render_template(hass, mqtt_mock): +async def test_service_call_with_payload_doesnt_render_template( + hass, mqtt_mock_entry_no_yaml_config +): """Test the service call with unrendered template. If both 'payload' and 'payload_template' are provided then fail. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() payload = "not a template" payload_template = "a template" with pytest.raises(vol.Invalid): @@ -461,11 +486,14 @@ async def test_service_call_with_payload_doesnt_render_template(hass, mqtt_mock) assert not mqtt_mock.async_publish.called -async def test_service_call_with_ascii_qos_retain_flags(hass, mqtt_mock): +async def test_service_call_with_ascii_qos_retain_flags( + hass, mqtt_mock_entry_no_yaml_config +): """Test the service call with args that can be misinterpreted. Empty payload message and ascii formatted qos and retain flags. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, @@ -665,9 +693,10 @@ def test_entity_device_info_schema(): async def test_receiving_non_utf8_message_gets_logged( - hass, mqtt_mock, calls, record_calls, caplog + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls, caplog ): """Test receiving a non utf8 encoded message.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic", record_calls) async_fire_mqtt_message(hass, "test-topic", b"\x9a") @@ -679,9 +708,10 @@ async def test_receiving_non_utf8_message_gets_logged( async def test_all_subscriptions_run_when_decode_fails( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test all other subscriptions still run when decode fails for one.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic", record_calls, encoding="ascii") await mqtt.async_subscribe(hass, "test-topic", record_calls) @@ -691,8 +721,11 @@ async def test_all_subscriptions_run_when_decode_fails( assert len(calls) == 1 -async def test_subscribe_topic(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_topic( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test the subscription of a topic.""" + await mqtt_mock_entry_no_yaml_config() unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) async_fire_mqtt_message(hass, "test-topic", "test-payload") @@ -714,8 +747,11 @@ async def test_subscribe_topic(hass, mqtt_mock, calls, record_calls): unsub() -async def test_subscribe_topic_non_async(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_topic_non_async( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test the subscription of a topic using the non-async function.""" + await mqtt_mock_entry_no_yaml_config() unsub = await hass.async_add_executor_job( mqtt.subscribe, hass, "test-topic", record_calls ) @@ -736,14 +772,18 @@ async def test_subscribe_topic_non_async(hass, mqtt_mock, calls, record_calls): assert len(calls) == 1 -async def test_subscribe_bad_topic(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_bad_topic( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test the subscription of a topic.""" + await mqtt_mock_entry_no_yaml_config() with pytest.raises(HomeAssistantError): await mqtt.async_subscribe(hass, 55, record_calls) -async def test_subscribe_deprecated(hass, mqtt_mock): +async def test_subscribe_deprecated(hass, mqtt_mock_entry_no_yaml_config): """Test the subscription of a topic using deprecated callback signature.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() @callback def record_calls(topic, payload, qos): @@ -789,8 +829,9 @@ async def test_subscribe_deprecated(hass, mqtt_mock): assert len(calls) == 1 -async def test_subscribe_deprecated_async(hass, mqtt_mock): +async def test_subscribe_deprecated_async(hass, mqtt_mock_entry_no_yaml_config): """Test the subscription of a topic using deprecated coroutine signature.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() def async_record_calls(topic, payload, qos): """Record calls.""" @@ -835,8 +876,11 @@ async def test_subscribe_deprecated_async(hass, mqtt_mock): assert len(calls) == 1 -async def test_subscribe_topic_not_match(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_topic_not_match( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test if subscribed topic is not a match.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic", record_calls) async_fire_mqtt_message(hass, "another-test-topic", "test-payload") @@ -845,8 +889,11 @@ async def test_subscribe_topic_not_match(hass, mqtt_mock, calls, record_calls): assert len(calls) == 0 -async def test_subscribe_topic_level_wildcard(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_topic_level_wildcard( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") @@ -858,9 +905,10 @@ async def test_subscribe_topic_level_wildcard(hass, mqtt_mock, calls, record_cal async def test_subscribe_topic_level_wildcard_no_subtree_match( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) async_fire_mqtt_message(hass, "test-topic/bier", "test-payload") @@ -870,9 +918,10 @@ async def test_subscribe_topic_level_wildcard_no_subtree_match( async def test_subscribe_topic_level_wildcard_root_topic_no_subtree_match( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic/#", record_calls) async_fire_mqtt_message(hass, "test-topic-123", "test-payload") @@ -882,9 +931,10 @@ async def test_subscribe_topic_level_wildcard_root_topic_no_subtree_match( async def test_subscribe_topic_subtree_wildcard_subtree_topic( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic/#", record_calls) async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") @@ -896,9 +946,10 @@ async def test_subscribe_topic_subtree_wildcard_subtree_topic( async def test_subscribe_topic_subtree_wildcard_root_topic( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic/#", record_calls) async_fire_mqtt_message(hass, "test-topic", "test-payload") @@ -910,9 +961,10 @@ async def test_subscribe_topic_subtree_wildcard_root_topic( async def test_subscribe_topic_subtree_wildcard_no_match( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic/#", record_calls) async_fire_mqtt_message(hass, "another-test-topic", "test-payload") @@ -922,9 +974,10 @@ async def test_subscribe_topic_subtree_wildcard_no_match( async def test_subscribe_topic_level_wildcard_and_wildcard_root_topic( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) async_fire_mqtt_message(hass, "hi/test-topic", "test-payload") @@ -936,9 +989,10 @@ async def test_subscribe_topic_level_wildcard_and_wildcard_root_topic( async def test_subscribe_topic_level_wildcard_and_wildcard_subtree_topic( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) async_fire_mqtt_message(hass, "hi/test-topic/here-iam", "test-payload") @@ -950,9 +1004,10 @@ async def test_subscribe_topic_level_wildcard_and_wildcard_subtree_topic( async def test_subscribe_topic_level_wildcard_and_wildcard_level_no_match( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) async_fire_mqtt_message(hass, "hi/here-iam/test-topic", "test-payload") @@ -962,9 +1017,10 @@ async def test_subscribe_topic_level_wildcard_and_wildcard_level_no_match( async def test_subscribe_topic_level_wildcard_and_wildcard_no_match( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) async_fire_mqtt_message(hass, "hi/another-test-topic", "test-payload") @@ -973,8 +1029,11 @@ async def test_subscribe_topic_level_wildcard_and_wildcard_no_match( assert len(calls) == 0 -async def test_subscribe_topic_sys_root(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_topic_sys_root( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test the subscription of $ root topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "$test-topic/subtree/on", record_calls) async_fire_mqtt_message(hass, "$test-topic/subtree/on", "test-payload") @@ -986,9 +1045,10 @@ async def test_subscribe_topic_sys_root(hass, mqtt_mock, calls, record_calls): async def test_subscribe_topic_sys_root_and_wildcard_topic( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of $ root and wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "$test-topic/#", record_calls) async_fire_mqtt_message(hass, "$test-topic/some-topic", "test-payload") @@ -1000,9 +1060,10 @@ async def test_subscribe_topic_sys_root_and_wildcard_topic( async def test_subscribe_topic_sys_root_and_wildcard_subtree_topic( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of $ root and wildcard subtree topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "$test-topic/subtree/#", record_calls) async_fire_mqtt_message(hass, "$test-topic/subtree/some-topic", "test-payload") @@ -1013,8 +1074,11 @@ async def test_subscribe_topic_sys_root_and_wildcard_subtree_topic( assert calls[0][0].payload == "test-payload" -async def test_subscribe_special_characters(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_special_characters( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test the subscription to topics with special characters.""" + await mqtt_mock_entry_no_yaml_config() topic = "/test-topic/$(.)[^]{-}" payload = "p4y.l[]a|> ?" @@ -1027,13 +1091,16 @@ async def test_subscribe_special_characters(hass, mqtt_mock, calls, record_calls assert calls[0][0].payload == payload -async def test_subscribe_same_topic(hass, mqtt_client_mock, mqtt_mock): +async def test_subscribe_same_topic( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """ Test subscring to same topic twice and simulate retained messages. When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again for it to resend any retained messages. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() # Fake that the client is connected mqtt_mock().connected = True @@ -1061,9 +1128,10 @@ async def test_subscribe_same_topic(hass, mqtt_client_mock, mqtt_mock): async def test_not_calling_unsubscribe_with_active_subscribers( - hass, mqtt_client_mock, mqtt_mock + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config ): """Test not calling unsubscribe() when other subscribers are active.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() # Fake that the client is connected mqtt_mock().connected = True @@ -1077,8 +1145,9 @@ async def test_not_calling_unsubscribe_with_active_subscribers( assert not mqtt_client_mock.unsubscribe.called -async def test_unsubscribe_race(hass, mqtt_client_mock, mqtt_mock): +async def test_unsubscribe_race(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config): """Test not calling unsubscribe() when other subscribers are active.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() # Fake that the client is connected mqtt_mock().connected = True @@ -1113,8 +1182,11 @@ async def test_unsubscribe_race(hass, mqtt_client_mock, mqtt_mock): "mqtt_config", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) -async def test_restore_subscriptions_on_reconnect(hass, mqtt_client_mock, mqtt_mock): +async def test_restore_subscriptions_on_reconnect( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Test subscriptions are restored on reconnect.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() # Fake that the client is connected mqtt_mock().connected = True @@ -1134,9 +1206,10 @@ async def test_restore_subscriptions_on_reconnect(hass, mqtt_client_mock, mqtt_m [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) async def test_restore_all_active_subscriptions_on_reconnect( - hass, mqtt_client_mock, mqtt_mock + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config ): """Test active subscriptions are restored correctly on reconnect.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() # Fake that the client is connected mqtt_mock().connected = True @@ -1176,9 +1249,10 @@ async def test_initial_setup_logs_error(hass, caplog, mqtt_client_mock): async def test_logs_error_if_no_connect_broker( - hass, caplog, mqtt_mock, mqtt_client_mock + hass, caplog, mqtt_mock_entry_no_yaml_config, mqtt_client_mock ): """Test for setup failure if connection to broker is missing.""" + await mqtt_mock_entry_no_yaml_config() # test with rc = 3 -> broker unavailable mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 3) await hass.async_block_till_done() @@ -1189,8 +1263,11 @@ async def test_logs_error_if_no_connect_broker( @patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.3) -async def test_handle_mqtt_on_callback(hass, caplog, mqtt_mock, mqtt_client_mock): +async def test_handle_mqtt_on_callback( + hass, caplog, mqtt_mock_entry_no_yaml_config, mqtt_client_mock +): """Test receiving an ACK callback before waiting for it.""" + await mqtt_mock_entry_no_yaml_config() # Simulate an ACK for mid == 1, this will call mqtt_mock._mqtt_handle_mid(mid) mqtt_client_mock.on_publish(mqtt_client_mock, None, 1) await hass.async_block_till_done() @@ -1224,8 +1301,11 @@ async def test_publish_error(hass, caplog): assert "Failed to connect to MQTT server: Out of memory." in caplog.text -async def test_handle_message_callback(hass, caplog, mqtt_mock, mqtt_client_mock): +async def test_handle_message_callback( + hass, caplog, mqtt_mock_entry_no_yaml_config, mqtt_client_mock +): """Test for handling an incoming message callback.""" + await mqtt_mock_entry_no_yaml_config() msg = ReceiveMessage("some-topic", b"test-payload", 0, False) mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 0) await mqtt.async_subscribe(hass, "some-topic", lambda *args: 0) @@ -1478,8 +1558,11 @@ async def test_setup_without_tls_config_uses_tlsv1_under_python36(hass): } ], ) -async def test_custom_birth_message(hass, mqtt_client_mock, mqtt_mock): +async def test_custom_birth_message( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Test sending birth message.""" + await mqtt_mock_entry_no_yaml_config() birth = asyncio.Event() async def wait_birth(topic, payload, qos): @@ -1508,8 +1591,11 @@ async def test_custom_birth_message(hass, mqtt_client_mock, mqtt_mock): } ], ) -async def test_default_birth_message(hass, mqtt_client_mock, mqtt_mock): +async def test_default_birth_message( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Test sending birth message.""" + await mqtt_mock_entry_no_yaml_config() birth = asyncio.Event() async def wait_birth(topic, payload, qos): @@ -1530,8 +1616,9 @@ async def test_default_birth_message(hass, mqtt_client_mock, mqtt_mock): "mqtt_config", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], ) -async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock): +async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config): """Test disabling birth message.""" + await mqtt_mock_entry_no_yaml_config() with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() @@ -1553,8 +1640,12 @@ async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock): } ], ) -async def test_delayed_birth_message(hass, mqtt_client_mock, mqtt_config, mqtt_mock): +async def test_delayed_birth_message( + hass, mqtt_client_mock, mqtt_config, mqtt_mock_entry_no_yaml_config +): """Test sending birth message does not happen until Home Assistant starts.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() + hass.state = CoreState.starting birth = asyncio.Event() @@ -1610,15 +1701,23 @@ async def test_delayed_birth_message(hass, mqtt_client_mock, mqtt_config, mqtt_m } ], ) -async def test_custom_will_message(hass, mqtt_client_mock, mqtt_mock): +async def test_custom_will_message( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Test will message.""" + await mqtt_mock_entry_no_yaml_config() + mqtt_client_mock.will_set.assert_called_with( topic="death", payload="death", qos=0, retain=False ) -async def test_default_will_message(hass, mqtt_client_mock, mqtt_mock): +async def test_default_will_message( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Test will message.""" + await mqtt_mock_entry_no_yaml_config() + mqtt_client_mock.will_set.assert_called_with( topic="homeassistant/status", payload="offline", qos=0, retain=False ) @@ -1628,8 +1727,10 @@ async def test_default_will_message(hass, mqtt_client_mock, mqtt_mock): "mqtt_config", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_WILL_MESSAGE: {}}], ) -async def test_no_will_message(hass, mqtt_client_mock, mqtt_mock): +async def test_no_will_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config): """Test will message.""" + await mqtt_mock_entry_no_yaml_config() + mqtt_client_mock.will_set.assert_not_called() @@ -1643,8 +1744,12 @@ async def test_no_will_message(hass, mqtt_client_mock, mqtt_mock): } ], ) -async def test_mqtt_subscribes_topics_on_connect(hass, mqtt_client_mock, mqtt_mock): +async def test_mqtt_subscribes_topics_on_connect( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Test subscription to topic on connect.""" + await mqtt_mock_entry_no_yaml_config() + await mqtt.async_subscribe(hass, "topic/test", None) await mqtt.async_subscribe(hass, "home/sensor", None, 2) await mqtt.async_subscribe(hass, "still/pending", None) @@ -1662,7 +1767,9 @@ async def test_mqtt_subscribes_topics_on_connect(hass, mqtt_client_mock, mqtt_mo assert calls == expected -async def test_setup_entry_with_config_override(hass, device_reg, mqtt_client_mock): +async def test_setup_entry_with_config_override( + hass, device_reg, mqtt_mock_entry_with_yaml_config +): """Test if the MQTT component loads with no config and config entry can be setup.""" data = ( '{ "device":{"identifiers":["0AFFD2"]},' @@ -1672,6 +1779,8 @@ async def test_setup_entry_with_config_override(hass, device_reg, mqtt_client_mo # mqtt present in yaml config assert await async_setup_component(hass, mqtt.DOMAIN, {}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() # User sets up a config entry entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) @@ -1734,8 +1843,11 @@ async def test_fail_no_broker(hass, device_reg, mqtt_client_mock, caplog): @pytest.mark.no_fail_on_log_exception -async def test_message_callback_exception_gets_logged(hass, caplog, mqtt_mock): +async def test_message_callback_exception_gets_logged( + hass, caplog, mqtt_mock_entry_no_yaml_config +): """Test exception raised by message handler.""" + await mqtt_mock_entry_no_yaml_config() @callback def bad_handler(*args): @@ -1752,8 +1864,11 @@ async def test_message_callback_exception_gets_logged(hass, caplog, mqtt_mock): ) -async def test_mqtt_ws_subscription(hass, hass_ws_client, mqtt_mock): +async def test_mqtt_ws_subscription( + hass, hass_ws_client, mqtt_mock_entry_no_yaml_config +): """Test MQTT websocket subscription.""" + await mqtt_mock_entry_no_yaml_config() client = await hass_ws_client(hass) await client.send_json({"id": 5, "type": "mqtt/subscribe", "topic": "test-topic"}) response = await client.receive_json() @@ -1782,9 +1897,10 @@ async def test_mqtt_ws_subscription(hass, hass_ws_client, mqtt_mock): async def test_mqtt_ws_subscription_not_admin( - hass, hass_ws_client, mqtt_mock, hass_read_only_access_token + hass, hass_ws_client, mqtt_mock_entry_no_yaml_config, hass_read_only_access_token ): """Test MQTT websocket user is not admin.""" + await mqtt_mock_entry_no_yaml_config() client = await hass_ws_client(hass, access_token=hass_read_only_access_token) await client.send_json({"id": 5, "type": "mqtt/subscribe", "topic": "test-topic"}) response = await client.receive_json() @@ -1793,8 +1909,9 @@ async def test_mqtt_ws_subscription_not_admin( assert response["error"]["message"] == "Unauthorized" -async def test_dump_service(hass, mqtt_mock): +async def test_dump_service(hass, mqtt_mock_entry_no_yaml_config): """Test that we can dump a topic.""" + await mqtt_mock_entry_no_yaml_config() mopen = mock_open() await hass.services.async_call( @@ -1814,10 +1931,12 @@ async def test_dump_service(hass, mqtt_mock): async def test_mqtt_ws_remove_discovered_device( - hass, device_reg, entity_reg, hass_ws_client, mqtt_mock + hass, device_reg, entity_reg, hass_ws_client, mqtt_mock_entry_no_yaml_config ): """Test MQTT websocket device removal.""" assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() data = ( '{ "device":{"identifiers":["0AFFD2"]},' @@ -1851,9 +1970,10 @@ async def test_mqtt_ws_remove_discovered_device( async def test_mqtt_ws_get_device_debug_info( - hass, device_reg, hass_ws_client, mqtt_mock + hass, device_reg, hass_ws_client, mqtt_mock_entry_no_yaml_config ): """Test MQTT websocket device debug info.""" + await mqtt_mock_entry_no_yaml_config() config_sensor = { "device": {"identifiers": ["0AFFD2"]}, "platform": "mqtt", @@ -1913,9 +2033,10 @@ async def test_mqtt_ws_get_device_debug_info( async def test_mqtt_ws_get_device_debug_info_binary( - hass, device_reg, hass_ws_client, mqtt_mock + hass, device_reg, hass_ws_client, mqtt_mock_entry_no_yaml_config ): """Test MQTT websocket device debug info.""" + await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["0AFFD2"]}, "platform": "mqtt", @@ -1975,8 +2096,9 @@ async def test_mqtt_ws_get_device_debug_info_binary( assert response["result"] == expected_result -async def test_debug_info_multiple_devices(hass, mqtt_mock): +async def test_debug_info_multiple_devices(hass, mqtt_mock_entry_no_yaml_config): """Test we get correct debug_info when multiple devices are present.""" + await mqtt_mock_entry_no_yaml_config() devices = [ { "domain": "sensor", @@ -2054,8 +2176,11 @@ async def test_debug_info_multiple_devices(hass, mqtt_mock): assert discovery_data["payload"] == d["config"] -async def test_debug_info_multiple_entities_triggers(hass, mqtt_mock): +async def test_debug_info_multiple_entities_triggers( + hass, mqtt_mock_entry_no_yaml_config +): """Test we get correct debug_info for a device with multiple entities and triggers.""" + await mqtt_mock_entry_no_yaml_config() config = [ { "domain": "sensor", @@ -2137,8 +2262,11 @@ async def test_debug_info_multiple_entities_triggers(hass, mqtt_mock): } in discovery_data -async def test_debug_info_non_mqtt(hass, device_reg, entity_reg, mqtt_mock): +async def test_debug_info_non_mqtt( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test we get empty debug_info for a device with non MQTT entities.""" + await mqtt_mock_entry_no_yaml_config() DOMAIN = "sensor" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -2164,8 +2292,9 @@ async def test_debug_info_non_mqtt(hass, device_reg, entity_reg, mqtt_mock): assert len(debug_info_data["triggers"]) == 0 -async def test_debug_info_wildcard(hass, mqtt_mock): +async def test_debug_info_wildcard(hass, mqtt_mock_entry_no_yaml_config): """Test debug info.""" + await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, "platform": "mqtt", @@ -2210,8 +2339,9 @@ async def test_debug_info_wildcard(hass, mqtt_mock): } in debug_info_data["entities"][0]["subscriptions"] -async def test_debug_info_filter_same(hass, mqtt_mock): +async def test_debug_info_filter_same(hass, mqtt_mock_entry_no_yaml_config): """Test debug info removes messages with same timestamp.""" + await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, "platform": "mqtt", @@ -2268,8 +2398,9 @@ async def test_debug_info_filter_same(hass, mqtt_mock): } == debug_info_data["entities"][0]["subscriptions"][0] -async def test_debug_info_same_topic(hass, mqtt_mock): +async def test_debug_info_same_topic(hass, mqtt_mock_entry_no_yaml_config): """Test debug info.""" + await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, "platform": "mqtt", @@ -2320,8 +2451,9 @@ async def test_debug_info_same_topic(hass, mqtt_mock): async_fire_mqtt_message(hass, "sensor/status", "123", qos=0, retain=False) -async def test_debug_info_qos_retain(hass, mqtt_mock): +async def test_debug_info_qos_retain(hass, mqtt_mock_entry_no_yaml_config): """Test debug info.""" + await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, "platform": "mqtt", @@ -2377,8 +2509,10 @@ async def test_debug_info_qos_retain(hass, mqtt_mock): } in debug_info_data["entities"][0]["subscriptions"][0]["messages"] -async def test_publish_json_from_template(hass, mqtt_mock): +async def test_publish_json_from_template(hass, mqtt_mock_entry_no_yaml_config): """Test the publishing of call to services.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() + test_str = "{'valid': 'python', 'invalid': 'json'}" test_str_tpl = "{'valid': '{{ \"python\" }}', 'invalid': 'json'}" @@ -2424,8 +2558,11 @@ async def test_publish_json_from_template(hass, mqtt_mock): assert mqtt_mock.async_publish.call_args[0][1] == test_str -async def test_subscribe_connection_status(hass, mqtt_mock, mqtt_client_mock): +async def test_subscribe_connection_status( + hass, mqtt_mock_entry_no_yaml_config, mqtt_client_mock +): """Test connextion status subscription.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() mqtt_connected_calls = [] @callback @@ -2459,7 +2596,9 @@ async def test_subscribe_connection_status(hass, mqtt_mock, mqtt_client_mock): assert mqtt_connected_calls[1] is False -async def test_one_deprecation_warning_per_platform(hass, mqtt_mock, caplog): +async def test_one_deprecation_warning_per_platform( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test a deprecation warning is is logged once per platform.""" platform = "light" config = {"platform": "mqtt", "command_topic": "test-topic"} @@ -2469,6 +2608,7 @@ async def test_one_deprecation_warning_per_platform(hass, mqtt_mock, caplog): config2["name"] = "test2" await async_setup_component(hass, platform, {platform: [config1, config2]}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() count = 0 for record in caplog.records: if record.levelname == "WARNING" and ( diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index b7a3b5f2118..43b6e839904 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -89,12 +89,13 @@ DEFAULT_CONFIG = { DEFAULT_CONFIG_2 = {vacuum.DOMAIN: {"platform": "mqtt", "name": "test"}} -async def test_default_supported_features(hass, mqtt_mock): +async def test_default_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test that the correct supported features.""" assert await async_setup_component( hass, vacuum.DOMAIN, {vacuum.DOMAIN: DEFAULT_CONFIG} ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() entity = hass.states.get("vacuum.mqtttest") entity_features = entity.attributes.get(mqttvacuum.CONF_SUPPORTED_FEATURES, 0) assert sorted(services_to_strings(entity_features, SERVICE_TO_STRING)) == sorted( @@ -110,7 +111,7 @@ async def test_default_supported_features(hass, mqtt_mock): ) -async def test_all_commands(hass, mqtt_mock): +async def test_all_commands(hass, mqtt_mock_entry_with_yaml_config): """Test simple commands to the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -119,6 +120,7 @@ async def test_all_commands(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_turn_on(hass, "vacuum.mqtttest") mqtt_mock.async_publish.assert_called_once_with( @@ -189,7 +191,9 @@ async def test_all_commands(hass, mqtt_mock): } -async def test_commands_without_supported_features(hass, mqtt_mock): +async def test_commands_without_supported_features( + hass, mqtt_mock_entry_with_yaml_config +): """Test commands which are not supported by the vacuum.""" config = deepcopy(DEFAULT_CONFIG) services = mqttvacuum.STRING_TO_SERVICE["status"] @@ -199,6 +203,7 @@ async def test_commands_without_supported_features(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_turn_on(hass, "vacuum.mqtttest") mqtt_mock.async_publish.assert_not_called() @@ -237,7 +242,9 @@ async def test_commands_without_supported_features(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_attributes_without_supported_features(hass, mqtt_mock): +async def test_attributes_without_supported_features( + hass, mqtt_mock_entry_with_yaml_config +): """Test attributes which are not supported by the vacuum.""" config = deepcopy(DEFAULT_CONFIG) services = mqttvacuum.STRING_TO_SERVICE["turn_on"] @@ -247,6 +254,7 @@ async def test_attributes_without_supported_features(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "battery_level": 54, @@ -264,7 +272,7 @@ async def test_attributes_without_supported_features(hass, mqtt_mock): assert state.attributes.get(ATTR_FAN_SPEED_LIST) is None -async def test_status(hass, mqtt_mock): +async def test_status(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -273,6 +281,7 @@ async def test_status(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "battery_level": 54, @@ -304,7 +313,7 @@ async def test_status(hass, mqtt_mock): assert state.attributes.get(ATTR_FAN_SPEED) == "min" -async def test_status_battery(hass, mqtt_mock): +async def test_status_battery(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -313,6 +322,7 @@ async def test_status_battery(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "battery_level": 54 @@ -322,7 +332,7 @@ async def test_status_battery(hass, mqtt_mock): assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-50" -async def test_status_cleaning(hass, mqtt_mock): +async def test_status_cleaning(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -331,6 +341,7 @@ async def test_status_cleaning(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "cleaning": true @@ -340,7 +351,7 @@ async def test_status_cleaning(hass, mqtt_mock): assert state.state == STATE_ON -async def test_status_docked(hass, mqtt_mock): +async def test_status_docked(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -349,6 +360,7 @@ async def test_status_docked(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "docked": true @@ -358,7 +370,7 @@ async def test_status_docked(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_status_charging(hass, mqtt_mock): +async def test_status_charging(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -367,6 +379,7 @@ async def test_status_charging(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "charging": true @@ -376,7 +389,7 @@ async def test_status_charging(hass, mqtt_mock): assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-outline" -async def test_status_fan_speed(hass, mqtt_mock): +async def test_status_fan_speed(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -385,6 +398,7 @@ async def test_status_fan_speed(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "fan_speed": "max" @@ -394,7 +408,7 @@ async def test_status_fan_speed(hass, mqtt_mock): assert state.attributes.get(ATTR_FAN_SPEED) == "max" -async def test_status_fan_speed_list(hass, mqtt_mock): +async def test_status_fan_speed_list(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -403,12 +417,13 @@ async def test_status_fan_speed_list(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state.attributes.get(ATTR_FAN_SPEED_LIST) == ["min", "medium", "high", "max"] -async def test_status_no_fan_speed_list(hass, mqtt_mock): +async def test_status_no_fan_speed_list(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum. If the vacuum doesn't support fan speed, fan speed list should be None. @@ -421,12 +436,13 @@ async def test_status_no_fan_speed_list(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state.attributes.get(ATTR_FAN_SPEED_LIST) is None -async def test_status_error(hass, mqtt_mock): +async def test_status_error(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -435,6 +451,7 @@ async def test_status_error(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "error": "Error1" @@ -451,7 +468,7 @@ async def test_status_error(hass, mqtt_mock): assert state.attributes.get(ATTR_STATUS) == "Stopped" -async def test_battery_template(hass, mqtt_mock): +async def test_battery_template(hass, mqtt_mock_entry_with_yaml_config): """Test that you can use non-default templates for battery_level.""" config = deepcopy(DEFAULT_CONFIG) config.update( @@ -466,6 +483,7 @@ async def test_battery_template(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "retroroomba/battery_level", "54") state = hass.states.get("vacuum.mqtttest") @@ -473,7 +491,7 @@ async def test_battery_template(hass, mqtt_mock): assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-50" -async def test_status_invalid_json(hass, mqtt_mock): +async def test_status_invalid_json(hass, mqtt_mock_entry_with_yaml_config): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -482,6 +500,7 @@ async def test_status_invalid_json(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "vacuum/state", '{"asdfasas false}') state = hass.states.get("vacuum.mqtttest") @@ -489,153 +508,169 @@ async def test_status_invalid_json(hass, mqtt_mock): assert state.attributes.get(ATTR_STATUS) == "Stopped" -async def test_missing_battery_template(hass, mqtt_mock): +async def test_missing_battery_template(hass, mqtt_mock_entry_no_yaml_config): """Test to make sure missing template is not allowed.""" config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_BATTERY_LEVEL_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state is None -async def test_missing_charging_template(hass, mqtt_mock): +async def test_missing_charging_template(hass, mqtt_mock_entry_no_yaml_config): """Test to make sure missing template is not allowed.""" config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_CHARGING_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state is None -async def test_missing_cleaning_template(hass, mqtt_mock): +async def test_missing_cleaning_template(hass, mqtt_mock_entry_no_yaml_config): """Test to make sure missing template is not allowed.""" config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_CLEANING_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state is None -async def test_missing_docked_template(hass, mqtt_mock): +async def test_missing_docked_template(hass, mqtt_mock_entry_no_yaml_config): """Test to make sure missing template is not allowed.""" config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_DOCKED_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state is None -async def test_missing_error_template(hass, mqtt_mock): +async def test_missing_error_template(hass, mqtt_mock_entry_no_yaml_config): """Test to make sure missing template is not allowed.""" config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_ERROR_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state is None -async def test_missing_fan_speed_template(hass, mqtt_mock): +async def test_missing_fan_speed_template(hass, mqtt_mock_entry_no_yaml_config): """Test to make sure missing template is not allowed.""" config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_FAN_SPEED_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state is None -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2, MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one vacuum per unique_id.""" config = { vacuum.DOMAIN: [ @@ -653,74 +688,85 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, vacuum.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, config + ) -async def test_discovery_removal_vacuum(hass, mqtt_mock, caplog): +async def test_discovery_removal_vacuum(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered vacuum.""" data = json.dumps(DEFAULT_CONFIG_2[vacuum.DOMAIN]) - await help_test_discovery_removal(hass, mqtt_mock, caplog, vacuum.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, data + ) -async def test_discovery_update_vacuum(hass, mqtt_mock, caplog): +async def test_discovery_update_vacuum(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered vacuum.""" 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, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_vacuum(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_vacuum( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered vacuum.""" data1 = '{ "name": "Beer", "command_topic": "test_topic" }' with patch( "homeassistant.components.mqtt.vacuum.schema_legacy.MqttVacuum.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + vacuum.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer",' ' "command_topic": "test_topic#" }' data2 = '{ "name": "Milk",' ' "command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { vacuum.DOMAIN: { @@ -733,18 +779,22 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock): } } await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, vacuum.DOMAIN, config, ["test-topic", "avty-topic"] + hass, + mqtt_mock_entry_with_yaml_config, + vacuum.DOMAIN, + config, + ["test-topic", "avty-topic"], ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" config = { vacuum.DOMAIN: { @@ -757,7 +807,11 @@ async def test_entity_debug_info_message(hass, mqtt_mock): } } await help_test_entity_debug_info_message( - hass, mqtt_mock, vacuum.DOMAIN, config, vacuum.SERVICE_TURN_ON + hass, + mqtt_mock_entry_no_yaml_config, + vacuum.DOMAIN, + config, + vacuum.SERVICE_TURN_ON, ) @@ -803,7 +857,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -824,7 +878,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -836,11 +890,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = vacuum.DOMAIN config = DEFAULT_CONFIG - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -872,7 +928,13 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" config = deepcopy(DEFAULT_CONFIG) @@ -892,7 +954,7 @@ async def test_encoding_subscribable_topics( await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, config, diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 957178da14f..08d5432ba27 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -251,16 +251,17 @@ DEFAULT_CONFIG = { } -async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): +async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_config): """Test if command fails with command topic.""" assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {"platform": "mqtt", "name": "test"}} ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert hass.states.get("light.test") is None -async def test_legacy_rgb_white_light(hass, mqtt_mock): +async def test_legacy_rgb_white_light(hass, mqtt_mock_entry_with_yaml_config): """Test legacy RGB + white light flags brightness support.""" assert await async_setup_component( hass, @@ -276,6 +277,7 @@ async def test_legacy_rgb_white_light(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") expected_features = ( @@ -286,7 +288,9 @@ async def test_legacy_rgb_white_light(hass, mqtt_mock): assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == ["hs", "rgbw"] -async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqtt_mock): +async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics( + hass, mqtt_mock_entry_with_yaml_config +): """Test if there is no color and brightness if no topic.""" assert await async_setup_component( hass, @@ -301,6 +305,7 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqt }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -343,7 +348,9 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqt assert state.state == STATE_UNKNOWN -async def test_legacy_controlling_state_via_topic(hass, mqtt_mock): +async def test_legacy_controlling_state_via_topic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling of the state via topic for legacy light (white_value).""" config = { light.DOMAIN: { @@ -374,6 +381,7 @@ async def test_legacy_controlling_state_via_topic(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -472,7 +480,7 @@ async def test_legacy_controlling_state_via_topic(hass, mqtt_mock): assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_controlling_state_via_topic(hass, mqtt_mock): +async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling of the state via topic.""" config = { light.DOMAIN: { @@ -505,6 +513,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -593,7 +602,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_legacy_invalid_state_via_topic(hass, mqtt_mock, caplog): +async def test_legacy_invalid_state_via_topic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test handling of empty data via topic.""" config = { light.DOMAIN: { @@ -623,6 +634,7 @@ async def test_legacy_invalid_state_via_topic(hass, mqtt_mock, caplog): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -709,7 +721,7 @@ async def test_legacy_invalid_state_via_topic(hass, mqtt_mock, caplog): assert light_state.attributes["white_value"] == 255 -async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): +async def test_invalid_state_via_topic(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test handling of empty data via topic.""" config = { light.DOMAIN: { @@ -742,6 +754,7 @@ async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -843,7 +856,7 @@ async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): assert light_state.attributes["color_temp"] == 153 -async def test_brightness_controlling_scale(hass, mqtt_mock): +async def test_brightness_controlling_scale(hass, mqtt_mock_entry_with_yaml_config): """Test the brightness controlling scale.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -865,6 +878,7 @@ async def test_brightness_controlling_scale(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -890,7 +904,9 @@ async def test_brightness_controlling_scale(hass, mqtt_mock): assert light_state.attributes["brightness"] == 255 -async def test_brightness_from_rgb_controlling_scale(hass, mqtt_mock): +async def test_brightness_from_rgb_controlling_scale( + hass, mqtt_mock_entry_with_yaml_config +): """Test the brightness controlling scale.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -911,6 +927,7 @@ async def test_brightness_from_rgb_controlling_scale(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -929,7 +946,9 @@ async def test_brightness_from_rgb_controlling_scale(hass, mqtt_mock): assert state.attributes.get("brightness") == 127 -async def test_legacy_white_value_controlling_scale(hass, mqtt_mock): +async def test_legacy_white_value_controlling_scale( + hass, mqtt_mock_entry_with_yaml_config +): """Test the white_value controlling scale.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -951,6 +970,7 @@ async def test_legacy_white_value_controlling_scale(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -976,7 +996,9 @@ async def test_legacy_white_value_controlling_scale(hass, mqtt_mock): assert light_state.attributes["white_value"] == 255 -async def test_legacy_controlling_state_via_topic_with_templates(hass, mqtt_mock): +async def test_legacy_controlling_state_via_topic_with_templates( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the state with a template.""" config = { light.DOMAIN: { @@ -1011,6 +1033,7 @@ async def test_legacy_controlling_state_via_topic_with_templates(hass, mqtt_mock assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1065,7 +1088,9 @@ async def test_legacy_controlling_state_via_topic_with_templates(hass, mqtt_mock assert state.state == STATE_UNKNOWN -async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): +async def test_controlling_state_via_topic_with_templates( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the state with a template.""" config = { light.DOMAIN: { @@ -1104,6 +1129,7 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1165,7 +1191,9 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_legacy_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_legacy_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of command in optimistic mode.""" config = { light.DOMAIN: { @@ -1204,6 +1232,7 @@ async def test_legacy_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): ), assert_setup_component(1, light.DOMAIN): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -1295,7 +1324,9 @@ async def test_legacy_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes["color_temp"] == 125 -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of command in optimistic mode.""" config = { light.DOMAIN: { @@ -1334,6 +1365,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): ), assert_setup_component(1, light.DOMAIN): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -1484,7 +1516,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): +async def test_sending_mqtt_rgb_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of RGB command with template.""" config = { light.DOMAIN: { @@ -1502,6 +1536,7 @@ async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1521,7 +1556,9 @@ async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): assert state.attributes["rgb_color"] == (255, 128, 64) -async def test_sending_mqtt_rgbw_command_with_template(hass, mqtt_mock): +async def test_sending_mqtt_rgbw_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of RGBW command with template.""" config = { light.DOMAIN: { @@ -1539,6 +1576,7 @@ async def test_sending_mqtt_rgbw_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1558,7 +1596,9 @@ async def test_sending_mqtt_rgbw_command_with_template(hass, mqtt_mock): assert state.attributes["rgbw_color"] == (255, 128, 64, 32) -async def test_sending_mqtt_rgbww_command_with_template(hass, mqtt_mock): +async def test_sending_mqtt_rgbww_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of RGBWW command with template.""" config = { light.DOMAIN: { @@ -1576,6 +1616,7 @@ async def test_sending_mqtt_rgbww_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1595,7 +1636,9 @@ async def test_sending_mqtt_rgbww_command_with_template(hass, mqtt_mock): assert state.attributes["rgbww_color"] == (255, 128, 64, 32, 16) -async def test_sending_mqtt_color_temp_command_with_template(hass, mqtt_mock): +async def test_sending_mqtt_color_temp_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of Color Temp command with template.""" config = { light.DOMAIN: { @@ -1612,6 +1655,7 @@ async def test_sending_mqtt_color_temp_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1631,7 +1675,7 @@ async def test_sending_mqtt_color_temp_command_with_template(hass, mqtt_mock): assert state.attributes["color_temp"] == 100 -async def test_on_command_first(hass, mqtt_mock): +async def test_on_command_first(hass, mqtt_mock_entry_with_yaml_config): """Test on command being sent before brightness.""" config = { light.DOMAIN: { @@ -1645,6 +1689,7 @@ async def test_on_command_first(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1667,7 +1712,7 @@ async def test_on_command_first(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) -async def test_on_command_last(hass, mqtt_mock): +async def test_on_command_last(hass, mqtt_mock_entry_with_yaml_config): """Test on command being sent after brightness.""" config = { light.DOMAIN: { @@ -1680,6 +1725,7 @@ async def test_on_command_last(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1702,7 +1748,7 @@ async def test_on_command_last(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) -async def test_on_command_brightness(hass, mqtt_mock): +async def test_on_command_brightness(hass, mqtt_mock_entry_with_yaml_config): """Test on command being sent as only brightness.""" config = { light.DOMAIN: { @@ -1717,6 +1763,7 @@ async def test_on_command_brightness(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1757,7 +1804,7 @@ async def test_on_command_brightness(hass, mqtt_mock): ) -async def test_on_command_brightness_scaled(hass, mqtt_mock): +async def test_on_command_brightness_scaled(hass, mqtt_mock_entry_with_yaml_config): """Test brightness scale.""" config = { light.DOMAIN: { @@ -1773,6 +1820,7 @@ async def test_on_command_brightness_scaled(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1827,7 +1875,7 @@ async def test_on_command_brightness_scaled(hass, mqtt_mock): ) -async def test_legacy_on_command_rgb(hass, mqtt_mock): +async def test_legacy_on_command_rgb(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGB brightness mode.""" config = { light.DOMAIN: { @@ -1841,6 +1889,7 @@ async def test_legacy_on_command_rgb(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1918,7 +1967,7 @@ async def test_legacy_on_command_rgb(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_on_command_rgb(hass, mqtt_mock): +async def test_on_command_rgb(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGB brightness mode.""" config = { light.DOMAIN: { @@ -1931,6 +1980,7 @@ async def test_on_command_rgb(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2008,7 +2058,7 @@ async def test_on_command_rgb(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_on_command_rgbw(hass, mqtt_mock): +async def test_on_command_rgbw(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGBW brightness mode.""" config = { light.DOMAIN: { @@ -2021,6 +2071,7 @@ async def test_on_command_rgbw(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2098,7 +2149,7 @@ async def test_on_command_rgbw(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_on_command_rgbww(hass, mqtt_mock): +async def test_on_command_rgbww(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGBWW brightness mode.""" config = { light.DOMAIN: { @@ -2111,6 +2162,7 @@ async def test_on_command_rgbww(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2188,7 +2240,7 @@ async def test_on_command_rgbww(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_on_command_rgb_template(hass, mqtt_mock): +async def test_on_command_rgb_template(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGB brightness mode with RGB template.""" config = { light.DOMAIN: { @@ -2202,6 +2254,7 @@ async def test_on_command_rgb_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2225,7 +2278,7 @@ async def test_on_command_rgb_template(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) -async def test_on_command_rgbw_template(hass, mqtt_mock): +async def test_on_command_rgbw_template(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGBW brightness mode with RGBW template.""" config = { light.DOMAIN: { @@ -2239,6 +2292,7 @@ async def test_on_command_rgbw_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2261,7 +2315,7 @@ async def test_on_command_rgbw_template(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) -async def test_on_command_rgbww_template(hass, mqtt_mock): +async def test_on_command_rgbww_template(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGBWW brightness mode with RGBWW template.""" config = { light.DOMAIN: { @@ -2275,6 +2329,7 @@ async def test_on_command_rgbww_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2298,7 +2353,7 @@ async def test_on_command_rgbww_template(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) -async def test_on_command_white(hass, mqtt_mock): +async def test_on_command_white(hass, mqtt_mock_entry_with_yaml_config): """Test sending commands for RGB + white light.""" config = { light.DOMAIN: { @@ -2324,6 +2379,7 @@ async def test_on_command_white(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2375,7 +2431,7 @@ async def test_on_command_white(hass, mqtt_mock): ) -async def test_explicit_color_mode(hass, mqtt_mock): +async def test_explicit_color_mode(hass, mqtt_mock_entry_with_yaml_config): """Test explicit color mode over mqtt.""" config = { light.DOMAIN: { @@ -2409,6 +2465,7 @@ async def test_explicit_color_mode(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2525,7 +2582,7 @@ async def test_explicit_color_mode(hass, mqtt_mock): assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_explicit_color_mode_templated(hass, mqtt_mock): +async def test_explicit_color_mode_templated(hass, mqtt_mock_entry_with_yaml_config): """Test templated explicit color mode over mqtt.""" config = { light.DOMAIN: { @@ -2550,6 +2607,7 @@ async def test_explicit_color_mode_templated(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2606,7 +2664,7 @@ async def test_explicit_color_mode_templated(hass, mqtt_mock): assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_white_state_update(hass, mqtt_mock): +async def test_white_state_update(hass, mqtt_mock_entry_with_yaml_config): """Test state updates for RGB + white light.""" config = { light.DOMAIN: { @@ -2636,6 +2694,7 @@ async def test_white_state_update(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2670,7 +2729,7 @@ async def test_white_state_update(hass, mqtt_mock): assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_effect(hass, mqtt_mock): +async def test_effect(hass, mqtt_mock_entry_with_yaml_config): """Test effect.""" config = { light.DOMAIN: { @@ -2684,6 +2743,7 @@ async def test_effect(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2707,77 +2767,91 @@ async def test_effect(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, + MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one light per unique_id.""" config = { light.DOMAIN: [ @@ -2797,21 +2871,26 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, light.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, config + ) -async def test_discovery_removal_light(hass, mqtt_mock, caplog): +async def test_discovery_removal_light(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered light.""" data = ( '{ "name": "test",' ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_removal(hass, mqtt_mock, caplog, light.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, data + ) -async def test_discovery_deprecated(hass, mqtt_mock, caplog): +async def test_discovery_deprecated(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test discovery of mqtt light with deprecated platform option.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "name": "Beer",' ' "platform": "mqtt",' ' "command_topic": "test_topic"}' ) @@ -2822,7 +2901,9 @@ async def test_discovery_deprecated(hass, mqtt_mock, caplog): assert state.name == "Beer" -async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog): +async def test_discovery_update_light_topic_and_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered light.""" config1 = { "name": "Beer", @@ -3073,7 +3154,7 @@ async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, config1, @@ -3083,7 +3164,9 @@ async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog ) -async def test_discovery_update_light_template(hass, mqtt_mock, caplog): +async def test_discovery_update_light_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered light.""" config1 = { "name": "Beer", @@ -3292,7 +3375,7 @@ async def test_discovery_update_light_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, config1, @@ -3302,7 +3385,9 @@ async def test_discovery_update_light_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_light( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered light.""" data1 = ( '{ "name": "Beer",' @@ -3313,12 +3398,17 @@ async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.light.schema_basic.MqttLight.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, light.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -3327,60 +3417,64 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ' "command_topic": "test_topic" }' ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, light.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG, light.SERVICE_TURN_ON + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, + light.SERVICE_TURN_ON, ) -async def test_max_mireds(hass, mqtt_mock): +async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): """Test setting min_mireds and max_mireds.""" config = { light.DOMAIN: { @@ -3394,6 +3488,7 @@ async def test_max_mireds(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.attributes.get("min_mireds") == 153 @@ -3488,7 +3583,7 @@ async def test_max_mireds(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -3508,7 +3603,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -3522,11 +3617,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = light.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -3568,7 +3665,14 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value, init_payload + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, + init_payload, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[light.DOMAIN]) @@ -3587,7 +3691,7 @@ async def test_encoding_subscribable_topics( await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, config, @@ -3599,7 +3703,9 @@ async def test_encoding_subscribable_topics( ) -async def test_sending_mqtt_brightness_command_with_template(hass, mqtt_mock): +async def test_sending_mqtt_brightness_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of Brightness command with template.""" config = { light.DOMAIN: { @@ -3616,6 +3722,7 @@ async def test_sending_mqtt_brightness_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -3635,7 +3742,9 @@ async def test_sending_mqtt_brightness_command_with_template(hass, mqtt_mock): assert state.attributes["brightness"] == 100 -async def test_sending_mqtt_effect_command_with_template(hass, mqtt_mock): +async def test_sending_mqtt_effect_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of Effect command with template.""" config = { light.DOMAIN: { @@ -3654,6 +3763,7 @@ async def test_sending_mqtt_effect_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 962bf534370..c24c5e87937 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -162,7 +162,7 @@ class JsonValidator: return json.loads(self.jsondata) == json.loads(other) -async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): +async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_config): """Test if setup fails with no command topic.""" assert await async_setup_component( hass, @@ -170,11 +170,14 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): {light.DOMAIN: {"platform": "mqtt", "schema": "json", "name": "test"}}, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert hass.states.get("light.test") is None @pytest.mark.parametrize("deprecated", ("color_temp", "hs", "rgb", "white_value", "xy")) -async def test_fail_setup_if_color_mode_deprecated(hass, mqtt_mock, deprecated): +async def test_fail_setup_if_color_mode_deprecated( + hass, mqtt_mock_entry_no_yaml_config, deprecated +): """Test if setup fails if color mode is combined with deprecated config keys.""" supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] @@ -196,6 +199,7 @@ async def test_fail_setup_if_color_mode_deprecated(hass, mqtt_mock, deprecated): config, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert hass.states.get("light.test") is None @@ -203,7 +207,7 @@ async def test_fail_setup_if_color_mode_deprecated(hass, mqtt_mock, deprecated): "supported_color_modes", [["onoff", "rgb"], ["brightness", "rgb"], ["unknown"]] ) async def test_fail_setup_if_color_modes_invalid( - hass, mqtt_mock, supported_color_modes + hass, mqtt_mock_entry_no_yaml_config, supported_color_modes ): """Test if setup fails if supported color modes is invalid.""" config = { @@ -223,10 +227,11 @@ async def test_fail_setup_if_color_modes_invalid( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert hass.states.get("light.test") is None -async def test_rgb_light(hass, mqtt_mock): +async def test_rgb_light(hass, mqtt_mock_entry_with_yaml_config): """Test RGB light flags brightness support.""" assert await async_setup_component( hass, @@ -242,6 +247,7 @@ async def test_rgb_light(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") expected_features = ( @@ -253,7 +259,9 @@ async def test_rgb_light(hass, mqtt_mock): assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features -async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_mock): +async def test_no_color_brightness_color_temp_white_val_if_no_topics( + hass, mqtt_mock_entry_with_yaml_config +): """Test for no RGB, brightness, color temp, effect, white val or XY.""" assert await async_setup_component( hass, @@ -269,6 +277,7 @@ async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_ }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -305,7 +314,7 @@ async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_ assert state.state == STATE_UNKNOWN -async def test_controlling_state_via_topic(hass, mqtt_mock): +async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling of the state via topic.""" assert await async_setup_component( hass, @@ -329,6 +338,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -434,7 +444,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert light_state.attributes.get("white_value") == 155 -async def test_controlling_state_via_topic2(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic2( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling of the state via topic for a light supporting color mode.""" supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] @@ -457,6 +469,7 @@ async def test_controlling_state_via_topic2(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -602,7 +615,9 @@ async def test_controlling_state_via_topic2(hass, mqtt_mock, caplog): assert "Invalid or incomplete color value received" in caplog.text -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of command in optimistic mode.""" fake_state = ha.State( "light.test", @@ -641,6 +656,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -745,7 +761,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes["xy_color"] == (0.611, 0.375) -async def test_sending_mqtt_commands_and_optimistic2(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic2( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of command in optimistic mode for a light supporting color mode.""" supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] fake_state = ha.State( @@ -783,6 +801,7 @@ async def test_sending_mqtt_commands_and_optimistic2(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -953,7 +972,7 @@ async def test_sending_mqtt_commands_and_optimistic2(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_sending_hs_color(hass, mqtt_mock): +async def test_sending_hs_color(hass, mqtt_mock_entry_with_yaml_config): """Test light.turn_on with hs color sends hs color parameters.""" assert await async_setup_component( hass, @@ -971,6 +990,7 @@ async def test_sending_hs_color(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1018,7 +1038,7 @@ async def test_sending_hs_color(hass, mqtt_mock): ) -async def test_sending_rgb_color_no_brightness(hass, mqtt_mock): +async def test_sending_rgb_color_no_brightness(hass, mqtt_mock_entry_with_yaml_config): """Test light.turn_on with hs color sends rgb color parameters.""" assert await async_setup_component( hass, @@ -1034,6 +1054,7 @@ async def test_sending_rgb_color_no_brightness(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1071,7 +1092,7 @@ async def test_sending_rgb_color_no_brightness(hass, mqtt_mock): ) -async def test_sending_rgb_color_no_brightness2(hass, mqtt_mock): +async def test_sending_rgb_color_no_brightness2(hass, mqtt_mock_entry_with_yaml_config): """Test light.turn_on with hs color sends rgb color parameters.""" supported_color_modes = ["rgb", "rgbw", "rgbww"] assert await async_setup_component( @@ -1089,6 +1110,7 @@ async def test_sending_rgb_color_no_brightness2(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1148,7 +1170,9 @@ async def test_sending_rgb_color_no_brightness2(hass, mqtt_mock): ) -async def test_sending_rgb_color_with_brightness(hass, mqtt_mock): +async def test_sending_rgb_color_with_brightness( + hass, mqtt_mock_entry_with_yaml_config +): """Test light.turn_on with hs color sends rgb color parameters.""" assert await async_setup_component( hass, @@ -1166,6 +1190,7 @@ async def test_sending_rgb_color_with_brightness(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1218,7 +1243,9 @@ async def test_sending_rgb_color_with_brightness(hass, mqtt_mock): ) -async def test_sending_rgb_color_with_scaled_brightness(hass, mqtt_mock): +async def test_sending_rgb_color_with_scaled_brightness( + hass, mqtt_mock_entry_with_yaml_config +): """Test light.turn_on with hs color sends rgb color parameters.""" assert await async_setup_component( hass, @@ -1237,6 +1264,7 @@ async def test_sending_rgb_color_with_scaled_brightness(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1289,7 +1317,7 @@ async def test_sending_rgb_color_with_scaled_brightness(hass, mqtt_mock): ) -async def test_sending_xy_color(hass, mqtt_mock): +async def test_sending_xy_color(hass, mqtt_mock_entry_with_yaml_config): """Test light.turn_on with hs color sends xy color parameters.""" assert await async_setup_component( hass, @@ -1307,6 +1335,7 @@ async def test_sending_xy_color(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1353,7 +1382,7 @@ async def test_sending_xy_color(hass, mqtt_mock): ) -async def test_effect(hass, mqtt_mock): +async def test_effect(hass, mqtt_mock_entry_with_yaml_config): """Test for effect being sent when included.""" assert await async_setup_component( hass, @@ -1370,6 +1399,7 @@ async def test_effect(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1415,7 +1445,7 @@ async def test_effect(hass, mqtt_mock): assert state.attributes.get("effect") == "colorloop" -async def test_flash_short_and_long(hass, mqtt_mock): +async def test_flash_short_and_long(hass, mqtt_mock_entry_with_yaml_config): """Test for flash length being sent when included.""" assert await async_setup_component( hass, @@ -1433,6 +1463,7 @@ async def test_flash_short_and_long(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1476,7 +1507,7 @@ async def test_flash_short_and_long(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_transition(hass, mqtt_mock): +async def test_transition(hass, mqtt_mock_entry_with_yaml_config): """Test for transition time being sent when included.""" assert await async_setup_component( hass, @@ -1492,6 +1523,7 @@ async def test_transition(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1522,7 +1554,7 @@ async def test_transition(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_brightness_scale(hass, mqtt_mock): +async def test_brightness_scale(hass, mqtt_mock_entry_with_yaml_config): """Test for brightness scaling.""" assert await async_setup_component( hass, @@ -1540,6 +1572,7 @@ async def test_brightness_scale(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1563,7 +1596,7 @@ async def test_brightness_scale(hass, mqtt_mock): assert state.attributes.get("brightness") == 255 -async def test_invalid_values(hass, mqtt_mock): +async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): """Test that invalid color/brightness/white/etc. values are ignored.""" assert await async_setup_component( hass, @@ -1584,6 +1617,7 @@ async def test_invalid_values(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1700,77 +1734,95 @@ async def test_invalid_values(hass, mqtt_mock): assert state.attributes.get("color_temp") == 100 -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, + MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one light per unique_id.""" config = { light.DOMAIN: [ @@ -1792,16 +1844,24 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, light.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, config + ) -async def test_discovery_removal(hass, mqtt_mock, caplog): +async def test_discovery_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered mqtt_json lights.""" data = '{ "name": "test",' ' "schema": "json",' ' "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, light.DOMAIN, data) + await help_test_discovery_removal( + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + data, + ) -async def test_discovery_update_light(hass, mqtt_mock, caplog): +async def test_discovery_update_light(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered light.""" config1 = { "name": "Beer", @@ -1816,11 +1876,18 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): "command_topic": "test_topic", } await help_test_discovery_update( - hass, mqtt_mock, caplog, light.DOMAIN, config1, config2 + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + config1, + config2, ) -async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_light( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered light.""" data1 = ( '{ "name": "Beer",' @@ -1832,12 +1899,17 @@ async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.light.schema_json.MqttLightJson.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, light.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -1847,57 +1919,80 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ' "command_topic": "test_topic" }' ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, light.DOMAIN, data1, data2 + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + data1, + data2, ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG, light.SERVICE_TURN_ON, @@ -1906,7 +2001,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) -async def test_max_mireds(hass, mqtt_mock): +async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): """Test setting min_mireds and max_mireds.""" config = { light.DOMAIN: { @@ -1921,6 +2016,7 @@ async def test_max_mireds(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.attributes.get("min_mireds") == 153 @@ -1952,7 +2048,7 @@ async def test_max_mireds(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -1972,7 +2068,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -1986,11 +2082,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = light.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -2013,7 +2111,14 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value, init_payload + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, + init_payload, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[light.DOMAIN]) @@ -2028,7 +2133,7 @@ async def test_encoding_subscribable_topics( ] await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, config, diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index a88fc094f6d..0d4b95e9152 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -90,69 +90,53 @@ DEFAULT_CONFIG = { } -async def test_setup_fails(hass, mqtt_mock): +@pytest.mark.parametrize( + "test_config", + [ + ({"platform": "mqtt", "schema": "template", "name": "test"},), + ( + { + "platform": "mqtt", + "schema": "template", + "name": "test", + "command_topic": "test_topic", + }, + ), + ( + { + "platform": "mqtt", + "schema": "template", + "name": "test", + "command_topic": "test_topic", + "command_on_template": "on", + }, + ), + ( + { + "platform": "mqtt", + "schema": "template", + "name": "test", + "command_topic": "test_topic", + "command_off_template": "off", + }, + ), + ], +) +async def test_setup_fails(hass, mqtt_mock_entry_no_yaml_config, test_config): """Test that setup fails with missing required configuration items.""" - with assert_setup_component(0, light.DOMAIN): + with assert_setup_component(0, light.DOMAIN) as setup_config: assert await async_setup_component( hass, light.DOMAIN, - {light.DOMAIN: {"platform": "mqtt", "schema": "template", "name": "test"}}, - ) - await hass.async_block_till_done() - assert hass.states.get("light.test") is None - - with assert_setup_component(0, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "schema": "template", - "name": "test", - "command_topic": "test_topic", - } - }, - ) - await hass.async_block_till_done() - assert hass.states.get("light.test") is None - - with assert_setup_component(0, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "schema": "template", - "name": "test", - "command_topic": "test_topic", - "command_on_template": "on", - } - }, - ) - await hass.async_block_till_done() - assert hass.states.get("light.test") is None - - with assert_setup_component(0, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "schema": "template", - "name": "test", - "command_topic": "test_topic", - "command_off_template": "off", - } - }, + {light.DOMAIN: test_config}, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() + assert not setup_config[light.DOMAIN] assert hass.states.get("light.test") is None -async def test_rgb_light(hass, mqtt_mock): +async def test_rgb_light(hass, mqtt_mock_entry_with_yaml_config): """Test RGB light flags brightness support.""" assert await async_setup_component( hass, @@ -172,6 +156,7 @@ async def test_rgb_light(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -184,7 +169,7 @@ async def test_rgb_light(hass, mqtt_mock): assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features -async def test_state_change_via_topic(hass, mqtt_mock): +async def test_state_change_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test state change via topic.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -210,6 +195,7 @@ async def test_state_change_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -240,7 +226,7 @@ async def test_state_change_via_topic(hass, mqtt_mock): async def test_state_brightness_color_effect_temp_white_change_via_topic( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test state, bri, color, effect, color temp, white val change.""" with assert_setup_component(1, light.DOMAIN): @@ -276,6 +262,7 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -340,7 +327,9 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( assert light_state.attributes.get("effect") == "rainbow" -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of command in optimistic mode.""" fake_state = ha.State( "light.test", @@ -391,6 +380,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -495,7 +485,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): async def test_sending_mqtt_commands_non_optimistic_brightness_template( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test the sending of command in optimistic mode.""" with assert_setup_component(1, light.DOMAIN): @@ -532,6 +522,7 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -626,7 +617,7 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( state = hass.states.get("light.test") -async def test_effect(hass, mqtt_mock): +async def test_effect(hass, mqtt_mock_entry_with_yaml_config): """Test effect sent over MQTT in optimistic mode.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -646,6 +637,7 @@ async def test_effect(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -678,7 +670,7 @@ async def test_effect(hass, mqtt_mock): assert state.attributes.get("effect") == "colorloop" -async def test_flash(hass, mqtt_mock): +async def test_flash(hass, mqtt_mock_entry_with_yaml_config): """Test flash sent over MQTT in optimistic mode.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -697,6 +689,7 @@ async def test_flash(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -726,7 +719,7 @@ async def test_flash(hass, mqtt_mock): assert state.state == STATE_ON -async def test_transition(hass, mqtt_mock): +async def test_transition(hass, mqtt_mock_entry_with_yaml_config): """Test for transition time being sent when included.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -745,6 +738,7 @@ async def test_transition(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -767,7 +761,7 @@ async def test_transition(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_invalid_values(hass, mqtt_mock): +async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): """Test that invalid values are ignored.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -801,6 +795,7 @@ async def test_invalid_values(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -867,77 +862,91 @@ async def test_invalid_values(hass, mqtt_mock): assert state.attributes.get("effect") == "rainbow" -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, + MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one light per unique_id.""" config = { light.DOMAIN: [ @@ -961,10 +970,12 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, light.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, config + ) -async def test_discovery_removal(hass, mqtt_mock, caplog): +async def test_discovery_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered mqtt_json lights.""" data = ( '{ "name": "test",' @@ -973,10 +984,12 @@ async def test_discovery_removal(hass, mqtt_mock, caplog): ' "command_on_template": "on",' ' "command_off_template": "off"}' ) - await help_test_discovery_removal(hass, mqtt_mock, caplog, light.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, data + ) -async def test_discovery_update_light(hass, mqtt_mock, caplog): +async def test_discovery_update_light(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered light.""" config1 = { "name": "Beer", @@ -995,11 +1008,13 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): "command_off_template": "off", } await help_test_discovery_update( - hass, mqtt_mock, caplog, light.DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_light( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered light.""" data1 = ( '{ "name": "Beer",' @@ -1013,12 +1028,17 @@ async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.light.schema_template.MqttLightTemplate.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, light.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -1030,53 +1050,53 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ' "command_off_template": "off"}' ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, light.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" config = { light.DOMAIN: { @@ -1090,11 +1110,15 @@ async def test_entity_debug_info_message(hass, mqtt_mock): } } await help_test_entity_debug_info_message( - hass, mqtt_mock, light.DOMAIN, config, light.SERVICE_TURN_ON + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + config, + light.SERVICE_TURN_ON, ) -async def test_max_mireds(hass, mqtt_mock): +async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): """Test setting min_mireds and max_mireds.""" config = { light.DOMAIN: { @@ -1111,6 +1135,7 @@ async def test_max_mireds(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.attributes.get("min_mireds") == 153 @@ -1142,7 +1167,7 @@ async def test_max_mireds(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -1162,7 +1187,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -1176,11 +1201,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = light.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -1197,14 +1224,21 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value, init_payload + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, + init_payload, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[light.DOMAIN]) config["state_template"] = "{{ value }}" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, config, diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index ef752ef8749..b48557efc8f 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -58,7 +58,7 @@ DEFAULT_CONFIG = { } -async def test_controlling_state_via_topic(hass, mqtt_mock): +async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -77,6 +77,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -94,7 +95,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert state.state is STATE_UNLOCKED -async def test_controlling_non_default_state_via_topic(hass, mqtt_mock): +async def test_controlling_non_default_state_via_topic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -113,6 +116,7 @@ async def test_controlling_non_default_state_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -129,7 +133,9 @@ async def test_controlling_non_default_state_via_topic(hass, mqtt_mock): assert state.state is STATE_UNLOCKED -async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): +async def test_controlling_state_via_topic_and_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, @@ -149,6 +155,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -165,7 +172,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): async def test_controlling_non_default_state_via_topic_and_json_message( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( @@ -186,6 +193,7 @@ async def test_controlling_non_default_state_via_topic_and_json_message( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -201,7 +209,9 @@ async def test_controlling_non_default_state_via_topic_and_json_message( assert state.state is STATE_UNLOCKED -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -219,6 +229,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -245,7 +256,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_explicit_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -265,6 +278,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -291,7 +305,9 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_commands_support_open_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_support_open_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test open function of the lock without state topic.""" assert await async_setup_component( hass, @@ -310,6 +326,7 @@ async def test_sending_mqtt_commands_support_open_and_optimistic(hass, mqtt_mock }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -348,7 +365,7 @@ async def test_sending_mqtt_commands_support_open_and_optimistic(hass, mqtt_mock async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test open function of the lock without state topic.""" assert await async_setup_component( @@ -370,6 +387,7 @@ async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -407,77 +425,91 @@ async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG, MQTT_LOCK_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + LOCK_DOMAIN, + DEFAULT_CONFIG, + MQTT_LOCK_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_json( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one lock per unique_id.""" config = { LOCK_DOMAIN: [ @@ -497,16 +529,20 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, LOCK_DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, config + ) -async def test_discovery_removal_lock(hass, mqtt_mock, caplog): +async def test_discovery_removal_lock(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered lock.""" data = '{ "name": "test",' ' "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, LOCK_DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, LOCK_DOMAIN, data + ) -async def test_discovery_update_lock(hass, mqtt_mock, caplog): +async def test_discovery_update_lock(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered lock.""" config1 = { "name": "Beer", @@ -521,11 +557,13 @@ async def test_discovery_update_lock(hass, mqtt_mock, caplog): "availability_topic": "availability_topic2", } await help_test_discovery_update( - hass, mqtt_mock, caplog, LOCK_DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, LOCK_DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_lock(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_lock( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered lock.""" data1 = ( '{ "name": "Beer",' @@ -536,65 +574,72 @@ async def test_discovery_update_unchanged_lock(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.lock.MqttLock.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, LOCK_DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + LOCK_DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk",' ' "command_topic": "test_topic" }' - await help_test_discovery_broken(hass, mqtt_mock, caplog, LOCK_DOMAIN, data1, data2) + await help_test_discovery_broken( + hass, mqtt_mock_entry_no_yaml_config, caplog, LOCK_DOMAIN, data1, data2 + ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT lock device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT lock device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG, SERVICE_LOCK, @@ -616,7 +661,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -630,7 +675,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -642,11 +687,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = LOCK_DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -663,12 +710,18 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, LOCK_DOMAIN, DEFAULT_CONFIG[LOCK_DOMAIN], diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 4eb8fdec351..a49b6de198d 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -64,7 +64,7 @@ DEFAULT_CONFIG = { } -async def test_run_number_setup(hass, mqtt_mock): +async def test_run_number_setup(hass, mqtt_mock_entry_with_yaml_config): """Test that it fetches the given payload.""" topic = "test/number" await async_setup_component( @@ -82,6 +82,7 @@ async def test_run_number_setup(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, "10") @@ -108,7 +109,7 @@ async def test_run_number_setup(hass, mqtt_mock): assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "my unit" -async def test_value_template(hass, mqtt_mock): +async def test_value_template(hass, mqtt_mock_entry_with_yaml_config): """Test that it fetches the given payload with a template.""" topic = "test/number" await async_setup_component( @@ -125,6 +126,7 @@ async def test_value_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, '{"val":10}') @@ -148,7 +150,7 @@ async def test_value_template(hass, mqtt_mock): assert state.state == "unknown" -async def test_run_number_service_optimistic(hass, mqtt_mock): +async def test_run_number_service_optimistic(hass, mqtt_mock_entry_with_yaml_config): """Test that set_value service works in optimistic mode.""" topic = "test/number" @@ -170,6 +172,7 @@ async def test_run_number_service_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("number.test_number") assert state.state == "3" @@ -215,7 +218,9 @@ async def test_run_number_service_optimistic(hass, mqtt_mock): assert state.state == "42.1" -async def test_run_number_service_optimistic_with_command_template(hass, mqtt_mock): +async def test_run_number_service_optimistic_with_command_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test that set_value service works in optimistic mode and with a command_template.""" topic = "test/number" @@ -238,6 +243,7 @@ async def test_run_number_service_optimistic_with_command_template(hass, mqtt_mo }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("number.test_number") assert state.state == "3" @@ -285,7 +291,7 @@ async def test_run_number_service_optimistic_with_command_template(hass, mqtt_mo assert state.state == "42.1" -async def test_run_number_service(hass, mqtt_mock): +async def test_run_number_service(hass, mqtt_mock_entry_with_yaml_config): """Test that set_value service works in non optimistic mode.""" cmd_topic = "test/number/set" state_topic = "test/number" @@ -303,6 +309,7 @@ async def test_run_number_service(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, state_topic, "32") state = hass.states.get("number.test_number") @@ -319,7 +326,9 @@ async def test_run_number_service(hass, mqtt_mock): assert state.state == "32" -async def test_run_number_service_with_command_template(hass, mqtt_mock): +async def test_run_number_service_with_command_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test that set_value service works in non optimistic mode and with a command_template.""" cmd_topic = "test/number/set" state_topic = "test/number" @@ -338,6 +347,7 @@ async def test_run_number_service_with_command_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, state_topic, "32") state = hass.states.get("number.test_number") @@ -356,77 +366,91 @@ async def test_run_number_service_with_command_template(hass, mqtt_mock): assert state.state == "32" -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG, MQTT_NUMBER_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + number.DOMAIN, + DEFAULT_CONFIG, + MQTT_NUMBER_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, number.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, number.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, number.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one number per unique_id.""" config = { number.DOMAIN: [ @@ -446,16 +470,20 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, number.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, config + ) -async def test_discovery_removal_number(hass, mqtt_mock, caplog): +async def test_discovery_removal_number(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered number.""" data = json.dumps(DEFAULT_CONFIG[number.DOMAIN]) - await help_test_discovery_removal(hass, mqtt_mock, caplog, number.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, number.DOMAIN, data + ) -async def test_discovery_update_number(hass, mqtt_mock, caplog): +async def test_discovery_update_number(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered number.""" config1 = { "name": "Beer", @@ -469,11 +497,13 @@ async def test_discovery_update_number(hass, mqtt_mock, caplog): } await help_test_discovery_update( - hass, mqtt_mock, caplog, number.DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, number.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_number(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_number( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered number.""" data1 = ( '{ "name": "Beer", "state_topic": "test-topic", "command_topic": "test-topic"}' @@ -482,12 +512,17 @@ async def test_discovery_update_unchanged_number(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.number.MqttNumber.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, number.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + number.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -495,57 +530,57 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, number.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, number.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT number device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT number device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG, SERVICE_SET_VALUE, @@ -555,7 +590,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) -async def test_min_max_step_attributes(hass, mqtt_mock): +async def test_min_max_step_attributes(hass, mqtt_mock_entry_with_yaml_config): """Test min/max/step attributes.""" topic = "test/number" await async_setup_component( @@ -574,6 +609,7 @@ async def test_min_max_step_attributes(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("number.test_number") assert state.attributes.get(ATTR_MIN) == 5 @@ -581,7 +617,7 @@ async def test_min_max_step_attributes(hass, mqtt_mock): assert state.attributes.get(ATTR_STEP) == 20 -async def test_invalid_min_max_attributes(hass, caplog, mqtt_mock): +async def test_invalid_min_max_attributes(hass, caplog, mqtt_mock_entry_no_yaml_config): """Test invalid min/max attributes.""" topic = "test/number" await async_setup_component( @@ -599,11 +635,14 @@ async def test_invalid_min_max_attributes(hass, caplog, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert f"'{CONF_MAX}' must be > '{CONF_MIN}'" in caplog.text -async def test_mqtt_payload_not_a_number_warning(hass, caplog, mqtt_mock): +async def test_mqtt_payload_not_a_number_warning( + hass, caplog, mqtt_mock_entry_with_yaml_config +): """Test warning for MQTT payload which is not a number.""" topic = "test/number" await async_setup_component( @@ -619,6 +658,7 @@ async def test_mqtt_payload_not_a_number_warning(hass, caplog, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, "not_a_number") @@ -627,7 +667,9 @@ async def test_mqtt_payload_not_a_number_warning(hass, caplog, mqtt_mock): assert "Payload 'not_a_number' is not a Number" in caplog.text -async def test_mqtt_payload_out_of_range_error(hass, caplog, mqtt_mock): +async def test_mqtt_payload_out_of_range_error( + hass, caplog, mqtt_mock_entry_with_yaml_config +): """Test error when MQTT payload is out of min/max range.""" topic = "test/number" await async_setup_component( @@ -645,6 +687,7 @@ async def test_mqtt_payload_out_of_range_error(hass, caplog, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, "115.5") @@ -669,7 +712,7 @@ async def test_mqtt_payload_out_of_range_error(hass, caplog, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -683,7 +726,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -695,11 +738,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = number.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -717,12 +762,18 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, "number", DEFAULT_CONFIG["number"], diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index 15bbd3964e6..eb5cb94df2d 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -34,7 +34,7 @@ DEFAULT_CONFIG = { } -async def test_sending_mqtt_commands(hass, mqtt_mock): +async def test_sending_mqtt_commands(hass, mqtt_mock_entry_with_yaml_config): """Test the sending MQTT commands.""" fake_state = ha.State("scene.test", STATE_UNKNOWN) @@ -55,6 +55,7 @@ async def test_sending_mqtt_commands(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("scene.test") assert state.state == STATE_UNKNOWN @@ -67,21 +68,23 @@ async def test_sending_mqtt_commands(hass, mqtt_mock): ) -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, scene.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, scene.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { scene.DOMAIN: { @@ -93,11 +96,17 @@ async def test_default_availability_payload(hass, mqtt_mock): } await help_test_default_availability_payload( - hass, mqtt_mock, scene.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + scene.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { scene.DOMAIN: { @@ -109,11 +118,17 @@ async def test_custom_availability_payload(hass, mqtt_mock): } await help_test_custom_availability_payload( - hass, mqtt_mock, scene.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + scene.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one scene per unique_id.""" config = { scene.DOMAIN: [ @@ -131,16 +146,20 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, scene.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, config + ) -async def test_discovery_removal_scene(hass, mqtt_mock, caplog): +async def test_discovery_removal_scene(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered scene.""" data = '{ "name": "test",' ' "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, scene.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, scene.DOMAIN, data + ) -async def test_discovery_update_payload(hass, mqtt_mock, caplog): +async def test_discovery_update_payload(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered scene.""" config1 = copy.deepcopy(DEFAULT_CONFIG[scene.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[scene.DOMAIN]) @@ -151,7 +170,7 @@ async def test_discovery_update_payload(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, scene.DOMAIN, config1, @@ -159,32 +178,41 @@ async def test_discovery_update_payload(hass, mqtt_mock, caplog): ) -async def test_discovery_update_unchanged_scene(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_scene( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered scene.""" data1 = '{ "name": "Beer",' ' "command_topic": "test_topic" }' with patch( "homeassistant.components.mqtt.scene.MqttScene.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, scene.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + scene.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk",' ' "command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, scene.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, scene.DOMAIN, data1, data2 ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = scene.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index cf5abf55854..888dd301018 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -59,7 +59,7 @@ DEFAULT_CONFIG = { } -async def test_run_select_setup(hass, mqtt_mock): +async def test_run_select_setup(hass, mqtt_mock_entry_with_yaml_config): """Test that it fetches the given payload.""" topic = "test/select" await async_setup_component( @@ -76,6 +76,7 @@ async def test_run_select_setup(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, "milk") @@ -92,7 +93,7 @@ async def test_run_select_setup(hass, mqtt_mock): assert state.state == "beer" -async def test_value_template(hass, mqtt_mock): +async def test_value_template(hass, mqtt_mock_entry_with_yaml_config): """Test that it fetches the given payload with a template.""" topic = "test/select" await async_setup_component( @@ -110,6 +111,7 @@ async def test_value_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, '{"val":"milk"}') @@ -133,7 +135,7 @@ async def test_value_template(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_run_select_service_optimistic(hass, mqtt_mock): +async def test_run_select_service_optimistic(hass, mqtt_mock_entry_with_yaml_config): """Test that set_value service works in optimistic mode.""" topic = "test/select" @@ -156,6 +158,7 @@ async def test_run_select_service_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("select.test_select") assert state.state == "milk" @@ -174,7 +177,9 @@ async def test_run_select_service_optimistic(hass, mqtt_mock): assert state.state == "beer" -async def test_run_select_service_optimistic_with_command_template(hass, mqtt_mock): +async def test_run_select_service_optimistic_with_command_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test that set_value service works in optimistic mode and with a command_template.""" topic = "test/select" @@ -198,6 +203,7 @@ async def test_run_select_service_optimistic_with_command_template(hass, mqtt_mo }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("select.test_select") assert state.state == "milk" @@ -218,7 +224,7 @@ async def test_run_select_service_optimistic_with_command_template(hass, mqtt_mo assert state.state == "beer" -async def test_run_select_service(hass, mqtt_mock): +async def test_run_select_service(hass, mqtt_mock_entry_with_yaml_config): """Test that set_value service works in non optimistic mode.""" cmd_topic = "test/select/set" state_topic = "test/select" @@ -237,6 +243,7 @@ async def test_run_select_service(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, state_topic, "beer") state = hass.states.get("select.test_select") @@ -253,7 +260,9 @@ async def test_run_select_service(hass, mqtt_mock): assert state.state == "beer" -async def test_run_select_service_with_command_template(hass, mqtt_mock): +async def test_run_select_service_with_command_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test that set_value service works in non optimistic mode and with a command_template.""" cmd_topic = "test/select/set" state_topic = "test/select" @@ -273,6 +282,7 @@ async def test_run_select_service_with_command_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, state_topic, "beer") state = hass.states.get("select.test_select") @@ -289,77 +299,91 @@ async def test_run_select_service_with_command_template(hass, mqtt_mock): ) -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG, MQTT_SELECT_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + select.DOMAIN, + DEFAULT_CONFIG, + MQTT_SELECT_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, select.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, select.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, select.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one select per unique_id.""" config = { select.DOMAIN: [ @@ -381,16 +405,20 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, select.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, config + ) -async def test_discovery_removal_select(hass, mqtt_mock, caplog): +async def test_discovery_removal_select(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered select.""" data = json.dumps(DEFAULT_CONFIG[select.DOMAIN]) - await help_test_discovery_removal(hass, mqtt_mock, caplog, select.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, select.DOMAIN, data + ) -async def test_discovery_update_select(hass, mqtt_mock, caplog): +async def test_discovery_update_select(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered select.""" config1 = { "name": "Beer", @@ -406,79 +434,86 @@ async def test_discovery_update_select(hass, mqtt_mock, caplog): } await help_test_discovery_update( - hass, mqtt_mock, caplog, select.DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, select.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_select(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_select( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered select.""" data1 = '{ "name": "Beer", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}' with patch( "homeassistant.components.mqtt.select.MqttSelect.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, select.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + select.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}' await help_test_discovery_broken( - hass, mqtt_mock, caplog, select.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, select.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT select device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT select device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG, select.SERVICE_SELECT_OPTION, @@ -489,7 +524,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): @pytest.mark.parametrize("options", [["milk", "beer"], ["milk"], []]) -async def test_options_attributes(hass, mqtt_mock, options): +async def test_options_attributes(hass, mqtt_mock_entry_with_yaml_config, options): """Test options attribute.""" topic = "test/select" await async_setup_component( @@ -506,12 +541,15 @@ async def test_options_attributes(hass, mqtt_mock, options): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("select.test_select") assert state.attributes.get(ATTR_OPTIONS) == options -async def test_mqtt_payload_not_an_option_warning(hass, caplog, mqtt_mock): +async def test_mqtt_payload_not_an_option_warning( + hass, caplog, mqtt_mock_entry_with_yaml_config +): """Test warning for MQTT payload which is not a valid option.""" topic = "test/select" await async_setup_component( @@ -528,6 +566,7 @@ async def test_mqtt_payload_not_an_option_warning(hass, caplog, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, "öl") @@ -552,7 +591,14 @@ async def test_mqtt_payload_not_an_option_warning(hass, caplog, mqtt_mock): ], ) async def test_publishing_with_custom_encoding( - hass, mqtt_mock, caplog, service, topic, parameters, payload, template + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + service, + topic, + parameters, + payload, + template, ): """Test publishing MQTT payload with different encoding.""" domain = select.DOMAIN @@ -561,7 +607,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -573,11 +619,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = select.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -595,14 +643,20 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG["select"]) config["options"] = ["milk", "beer"] await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, "select", config, diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index befb5785cdd..894ecc32ecc 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -72,7 +72,9 @@ DEFAULT_CONFIG = { } -async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): +async def test_setting_sensor_value_via_mqtt_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, @@ -87,6 +89,7 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "test-topic", "100") state = hass.states.get("sensor.test") @@ -122,7 +125,13 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): ], ) async def test_setting_sensor_native_value_handling_via_mqtt_message( - hass, mqtt_mock, caplog, device_class, native_value, state_value, log + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + device_class, + native_value, + state_value, + log, ): """Test the setting of the value via MQTT.""" assert await async_setup_component( @@ -138,6 +147,7 @@ async def test_setting_sensor_native_value_handling_via_mqtt_message( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "test-topic", native_value) state = hass.states.get("sensor.test") @@ -147,7 +157,9 @@ async def test_setting_sensor_native_value_handling_via_mqtt_message( assert log == ("Invalid state message" in caplog.text) -async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, caplog): +async def test_setting_sensor_value_expires_availability_topic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the expiration of the value.""" assert await async_setup_component( hass, @@ -164,6 +176,7 @@ async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("sensor.test") assert state.state == STATE_UNAVAILABLE @@ -174,10 +187,12 @@ async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, state = hass.states.get("sensor.test") assert state.state == STATE_UNAVAILABLE - await expires_helper(hass, mqtt_mock, caplog) + await expires_helper(hass, caplog) -async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): +async def test_setting_sensor_value_expires( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the expiration of the value.""" assert await async_setup_component( hass, @@ -194,15 +209,16 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() # State should be unavailable since expire_after is defined and > 0 state = hass.states.get("sensor.test") assert state.state == STATE_UNAVAILABLE - await expires_helper(hass, mqtt_mock, caplog) + await expires_helper(hass, caplog) -async def expires_helper(hass, mqtt_mock, caplog): +async def expires_helper(hass, caplog): """Run the basic expiry code.""" realnow = dt_util.utcnow() now = datetime(realnow.year + 1, 1, 1, 1, tzinfo=dt_util.UTC) @@ -253,7 +269,9 @@ async def expires_helper(hass, mqtt_mock, caplog): assert state.state == STATE_UNAVAILABLE -async def test_setting_sensor_value_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_sensor_value_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the value via MQTT with JSON payload.""" assert await async_setup_component( hass, @@ -269,6 +287,7 @@ async def test_setting_sensor_value_via_mqtt_json_message(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "test-topic", '{ "val": "100" }') state = hass.states.get("sensor.test") @@ -277,7 +296,7 @@ async def test_setting_sensor_value_via_mqtt_json_message(hass, mqtt_mock): async def test_setting_sensor_value_via_mqtt_json_message_and_default_current_state( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test the setting of the value via MQTT with fall back to current state.""" assert await async_setup_component( @@ -294,6 +313,7 @@ async def test_setting_sensor_value_via_mqtt_json_message_and_default_current_st }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message( hass, "test-topic", '{ "val": "valcontent", "par": "parcontent" }' @@ -308,7 +328,9 @@ async def test_setting_sensor_value_via_mqtt_json_message_and_default_current_st assert state.state == "valcontent-parcontent" -async def test_setting_sensor_last_reset_via_mqtt_message(hass, mqtt_mock, caplog): +async def test_setting_sensor_last_reset_via_mqtt_message( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the setting of the last_reset property via MQTT.""" assert await async_setup_component( hass, @@ -325,6 +347,7 @@ async def test_setting_sensor_last_reset_via_mqtt_message(hass, mqtt_mock, caplo }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "last-reset-topic", "2020-01-02 08:11:00") state = hass.states.get("sensor.test") @@ -338,7 +361,7 @@ async def test_setting_sensor_last_reset_via_mqtt_message(hass, mqtt_mock, caplo @pytest.mark.parametrize("datestring", ["2020-21-02 08:11:00", "Hello there!"]) async def test_setting_sensor_bad_last_reset_via_mqtt_message( - hass, caplog, datestring, mqtt_mock + hass, caplog, datestring, mqtt_mock_entry_with_yaml_config ): """Test the setting of the last_reset property via MQTT.""" assert await async_setup_component( @@ -356,6 +379,7 @@ async def test_setting_sensor_bad_last_reset_via_mqtt_message( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "last-reset-topic", datestring) state = hass.states.get("sensor.test") @@ -364,7 +388,7 @@ async def test_setting_sensor_bad_last_reset_via_mqtt_message( async def test_setting_sensor_empty_last_reset_via_mqtt_message( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_with_yaml_config ): """Test the setting of the last_reset property via MQTT.""" assert await async_setup_component( @@ -382,6 +406,7 @@ async def test_setting_sensor_empty_last_reset_via_mqtt_message( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "last-reset-topic", "") state = hass.states.get("sensor.test") @@ -389,7 +414,9 @@ async def test_setting_sensor_empty_last_reset_via_mqtt_message( assert "Ignoring empty last_reset message" in caplog.text -async def test_setting_sensor_last_reset_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_sensor_last_reset_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the value via MQTT with JSON payload.""" assert await async_setup_component( hass, @@ -407,6 +434,7 @@ async def test_setting_sensor_last_reset_via_mqtt_json_message(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message( hass, "last-reset-topic", '{ "last_reset": "2020-01-02 08:11:00" }' @@ -417,7 +445,7 @@ async def test_setting_sensor_last_reset_via_mqtt_json_message(hass, mqtt_mock): @pytest.mark.parametrize("extra", [{}, {"last_reset_topic": "test-topic"}]) async def test_setting_sensor_last_reset_via_mqtt_json_message_2( - hass, mqtt_mock, caplog, extra + hass, mqtt_mock_entry_with_yaml_config, caplog, extra ): """Test the setting of the value via MQTT with JSON payload.""" assert await async_setup_component( @@ -439,6 +467,7 @@ async def test_setting_sensor_last_reset_via_mqtt_json_message_2( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message( hass, @@ -455,7 +484,7 @@ async def test_setting_sensor_last_reset_via_mqtt_json_message_2( ) -async def test_force_update_disabled(hass, mqtt_mock): +async def test_force_update_disabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, @@ -470,6 +499,7 @@ async def test_force_update_disabled(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() events = [] @@ -488,7 +518,7 @@ async def test_force_update_disabled(hass, mqtt_mock): assert len(events) == 1 -async def test_force_update_enabled(hass, mqtt_mock): +async def test_force_update_enabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, @@ -504,6 +534,7 @@ async def test_force_update_enabled(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() events = [] @@ -522,70 +553,80 @@ async def test_force_update_enabled(hass, mqtt_mock): assert len(events) == 2 -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_list_payload(hass, mqtt_mock): +async def test_default_availability_list_payload( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_list_payload_all(hass, mqtt_mock): +async def test_default_availability_list_payload_all( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload_all( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_list_payload_any(hass, mqtt_mock): +async def test_default_availability_list_payload_any( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload_any( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_list_single(hass, mqtt_mock, caplog): +async def test_default_availability_list_single( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test availability list and availability_topic are mutually exclusive.""" await help_test_default_availability_list_single( - hass, mqtt_mock, caplog, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_availability(hass, mqtt_mock): +async def test_discovery_update_availability(hass, mqtt_mock_entry_no_yaml_config): """Test availability discovery update.""" await help_test_discovery_update_availability( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_invalid_device_class(hass, mqtt_mock): +async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): """Test device_class option with invalid value.""" assert await async_setup_component( hass, @@ -600,12 +641,13 @@ async def test_invalid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("sensor.test") assert state is None -async def test_valid_device_class(hass, mqtt_mock): +async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test device_class option with valid values.""" assert await async_setup_component( hass, @@ -623,6 +665,7 @@ async def test_valid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("sensor.test_1") assert state.attributes["device_class"] == "temperature" @@ -630,7 +673,7 @@ async def test_valid_device_class(hass, mqtt_mock): assert "device_class" not in state.attributes -async def test_invalid_state_class(hass, mqtt_mock): +async def test_invalid_state_class(hass, mqtt_mock_entry_no_yaml_config): """Test state_class option with invalid value.""" assert await async_setup_component( hass, @@ -645,12 +688,13 @@ async def test_invalid_state_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("sensor.test") assert state is None -async def test_valid_state_class(hass, mqtt_mock): +async def test_valid_state_class(hass, mqtt_mock_entry_with_yaml_config): """Test state_class option with valid values.""" assert await async_setup_component( hass, @@ -668,6 +712,7 @@ async def test_valid_state_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("sensor.test_1") assert state.attributes["state_class"] == "measurement" @@ -675,49 +720,61 @@ async def test_valid_state_class(hass, mqtt_mock): assert "state_class" not in state.attributes -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG, MQTT_SENSOR_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + sensor.DOMAIN, + DEFAULT_CONFIG, + MQTT_SENSOR_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one sensor per unique_id.""" config = { sensor.DOMAIN: [ @@ -735,16 +792,22 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, sensor.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, config + ) -async def test_discovery_removal_sensor(hass, mqtt_mock, caplog): +async def test_discovery_removal_sensor(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered sensor.""" data = '{ "name": "test", "state_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, sensor.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, data + ) -async def test_discovery_update_sensor_topic_template(hass, mqtt_mock, caplog): +async def test_discovery_update_sensor_topic_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered sensor.""" config = {"name": "test", "state_topic": "test_topic"} config1 = copy.deepcopy(config) @@ -767,7 +830,7 @@ async def test_discovery_update_sensor_topic_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, config1, @@ -777,7 +840,9 @@ async def test_discovery_update_sensor_topic_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_sensor_template(hass, mqtt_mock, caplog): +async def test_discovery_update_sensor_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered sensor.""" config = {"name": "test", "state_topic": "test_topic"} config1 = copy.deepcopy(config) @@ -798,7 +863,7 @@ async def test_discovery_update_sensor_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, config1, @@ -808,71 +873,79 @@ async def test_discovery_update_sensor_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_unchanged_sensor(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_sensor( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered sensor.""" data1 = '{ "name": "Beer", "state_topic": "test_topic" }' with patch( "homeassistant.components.mqtt.sensor.MqttSensor.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, sensor.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + sensor.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "state_topic": "test_topic#" }' data2 = '{ "name": "Milk", "state_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, sensor.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_hub(hass, mqtt_mock): +async def test_entity_device_info_with_hub(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor device registry integration.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) hub = registry.async_get_or_create( config_entry_id="123", @@ -899,53 +972,57 @@ async def test_entity_device_info_with_hub(hass, mqtt_mock): assert device.via_device_id == hub.id -async def test_entity_debug_info(hass, mqtt_mock): +async def test_entity_debug_info(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" - await help_test_entity_debug_info(hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG) + await help_test_entity_debug_info( + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + ) -async def test_entity_debug_info_max_messages(hass, mqtt_mock): +async def test_entity_debug_info_max_messages(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_max_messages( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG, None + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG, None ) -async def test_entity_debug_info_remove(hass, mqtt_mock): +async def test_entity_debug_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_remove( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_update_entity_id(hass, mqtt_mock): +async def test_entity_debug_info_update_entity_id(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_update_entity_id( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_disabled_by_default(hass, mqtt_mock): +async def test_entity_disabled_by_default(hass, mqtt_mock_entry_no_yaml_config): """Test entity disabled by default.""" await help_test_entity_disabled_by_default( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @pytest.mark.no_fail_on_log_exception -async def test_entity_category(hass, mqtt_mock): +async def test_entity_category(hass, mqtt_mock_entry_no_yaml_config): """Test entity category.""" - await help_test_entity_category(hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG) + await help_test_entity_category( + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + ) -async def test_value_template_with_entity_id(hass, mqtt_mock): +async def test_value_template_with_entity_id(hass, mqtt_mock_entry_with_yaml_config): """Test the access to attributes in value_template via the entity_id.""" assert await async_setup_component( hass, @@ -966,6 +1043,7 @@ async def test_value_template_with_entity_id(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "test-topic", "100") state = hass.states.get("sensor.test") @@ -973,11 +1051,13 @@ async def test_value_template_with_entity_id(hass, mqtt_mock): assert state.state == "101" -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = sensor.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -988,7 +1068,7 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): async def test_cleanup_triggers_and_restoring_state( - hass, mqtt_mock, caplog, tmp_path, freezer + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, freezer ): """Test cleanup old triggers at reloading and restoring the state.""" domain = sensor.DOMAIN @@ -1014,6 +1094,7 @@ async def test_cleanup_triggers_and_restoring_state( {domain: [config1, config2]}, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "test-topic1", "100") state = hass.states.get("sensor.test1") assert state.state == "38" # 100 °F -> 38 °C @@ -1053,7 +1134,7 @@ async def test_cleanup_triggers_and_restoring_state( async def test_skip_restoring_state_with_over_due_expire_trigger( - hass, mqtt_mock, caplog, freezer + hass, mqtt_mock_entry_with_yaml_config, caplog, freezer ): """Test restoring a state with over due expire timer.""" @@ -1081,6 +1162,7 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( ): assert await async_setup_component(hass, domain, {domain: config3}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert "Skip state recovery after reload for sensor.test3" in caplog.text @@ -1092,12 +1174,18 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG[sensor.DOMAIN], diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index 197ed34b7e4..2db2060c133 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -70,7 +70,7 @@ async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL) -> None: await hass.services.async_call(siren.DOMAIN, SERVICE_TURN_OFF, data, blocking=True) -async def test_controlling_state_via_topic(hass, mqtt_mock): +async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -87,6 +87,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("siren.test") assert state.state == STATE_UNKNOWN @@ -103,7 +104,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending MQTT commands in optimistic mode.""" assert await async_setup_component( hass, @@ -120,6 +123,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("siren.test") assert state.state == STATE_OFF @@ -143,7 +147,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic_and_json_message( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, @@ -161,6 +167,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("siren.test") assert state.state == STATE_UNKNOWN @@ -181,7 +188,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap async def test_controlling_state_and_attributes_with_json_message_without_template( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the controlling state via topic and JSON message without a value template.""" assert await async_setup_component( @@ -200,6 +207,7 @@ async def test_controlling_state_and_attributes_with_json_message_without_templa }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("siren.test") assert state.state == STATE_UNKNOWN @@ -262,7 +270,9 @@ async def test_controlling_state_and_attributes_with_json_message_without_templa ) -async def test_filtering_not_supported_attributes_optimistic(hass, mqtt_mock): +async def test_filtering_not_supported_attributes_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting attributes with support flags optimistic.""" config = { "platform": "mqtt", @@ -285,6 +295,7 @@ async def test_filtering_not_supported_attributes_optimistic(hass, mqtt_mock): {siren.DOMAIN: [config1, config2, config3]}, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state1 = hass.states.get("siren.test1") assert state1.state == STATE_OFF @@ -345,7 +356,9 @@ async def test_filtering_not_supported_attributes_optimistic(hass, mqtt_mock): assert state3.attributes.get(siren.ATTR_VOLUME_LEVEL) == 0.88 -async def test_filtering_not_supported_attributes_via_state(hass, mqtt_mock): +async def test_filtering_not_supported_attributes_via_state( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting attributes with support flags via state.""" config = { "platform": "mqtt", @@ -371,6 +384,7 @@ async def test_filtering_not_supported_attributes_via_state(hass, mqtt_mock): {siren.DOMAIN: [config1, config2, config3]}, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state1 = hass.states.get("siren.test1") assert state1.state == STATE_UNKNOWN @@ -422,21 +436,23 @@ async def test_filtering_not_supported_attributes_via_state(hass, mqtt_mock): assert state3.attributes.get(siren.ATTR_VOLUME_LEVEL) == 0.88 -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { siren.DOMAIN: { @@ -450,11 +466,17 @@ async def test_default_availability_payload(hass, mqtt_mock): } await help_test_default_availability_payload( - hass, mqtt_mock, siren.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + siren.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { siren.DOMAIN: { @@ -468,11 +490,17 @@ async def test_custom_availability_payload(hass, mqtt_mock): } await help_test_custom_availability_payload( - hass, mqtt_mock, siren.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + siren.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_custom_state_payload(hass, mqtt_mock): +async def test_custom_state_payload(hass, mqtt_mock_entry_with_yaml_config): """Test the state payload.""" assert await async_setup_component( hass, @@ -491,6 +519,7 @@ async def test_custom_state_payload(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("siren.test") assert state.state == STATE_UNKNOWN @@ -507,49 +536,57 @@ async def test_custom_state_payload(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG, {} + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG, {} ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one siren per unique_id.""" config = { siren.DOMAIN: [ @@ -569,20 +606,26 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, siren.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, config + ) -async def test_discovery_removal_siren(hass, mqtt_mock, caplog): +async def test_discovery_removal_siren(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered siren.""" data = ( '{ "name": "test",' ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_removal(hass, mqtt_mock, caplog, siren.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, siren.DOMAIN, data + ) -async def test_discovery_update_siren_topic_template(hass, mqtt_mock, caplog): +async def test_discovery_update_siren_topic_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered siren.""" config1 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) @@ -607,7 +650,7 @@ async def test_discovery_update_siren_topic_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, siren.DOMAIN, config1, @@ -617,7 +660,9 @@ async def test_discovery_update_siren_topic_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_siren_template(hass, mqtt_mock, caplog): +async def test_discovery_update_siren_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered siren.""" config1 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) @@ -640,7 +685,7 @@ async def test_discovery_update_siren_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, siren.DOMAIN, config1, @@ -650,7 +695,7 @@ async def test_discovery_update_siren_template(hass, mqtt_mock, caplog): ) -async def test_command_templates(hass, mqtt_mock, caplog): +async def test_command_templates(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test siren with command templates optimistic.""" config1 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) config1["name"] = "Beer" @@ -669,6 +714,7 @@ async def test_command_templates(hass, mqtt_mock, caplog): {siren.DOMAIN: [config1, config2]}, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state1 = hass.states.get("siren.beer") assert state1.state == STATE_OFF @@ -729,7 +775,9 @@ async def test_command_templates(hass, mqtt_mock, caplog): mqtt_mock.reset_mock() -async def test_discovery_update_unchanged_siren(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_siren( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered siren.""" data1 = ( '{ "name": "Beer",' @@ -741,12 +789,17 @@ async def test_discovery_update_unchanged_siren(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.siren.MqttSiren.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, siren.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + siren.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -755,57 +808,57 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ' "command_topic": "test_topic" }' ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, siren.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, siren.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT siren device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT siren device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG, siren.SERVICE_TURN_ON, @@ -834,7 +887,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -849,7 +902,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -861,11 +914,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = siren.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -882,12 +937,18 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, siren.DOMAIN, DEFAULT_CONFIG[siren.DOMAIN], diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index c1017446eff..f20a881dda1 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -87,12 +87,13 @@ DEFAULT_CONFIG_2 = { } -async def test_default_supported_features(hass, mqtt_mock): +async def test_default_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test that the correct supported features.""" assert await async_setup_component( hass, vacuum.DOMAIN, {vacuum.DOMAIN: DEFAULT_CONFIG} ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() entity = hass.states.get("vacuum.mqtttest") entity_features = entity.attributes.get(mqttvacuum.CONF_SUPPORTED_FEATURES, 0) assert sorted(services_to_strings(entity_features, SERVICE_TO_STRING)) == sorted( @@ -100,7 +101,7 @@ async def test_default_supported_features(hass, mqtt_mock): ) -async def test_all_commands(hass, mqtt_mock): +async def test_all_commands(hass, mqtt_mock_entry_with_yaml_config): """Test simple commands send to the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -109,6 +110,7 @@ async def test_all_commands(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( DOMAIN, SERVICE_START, {"entity_id": ENTITY_MATCH_ALL}, blocking=True @@ -171,7 +173,9 @@ async def test_all_commands(hass, mqtt_mock): } -async def test_commands_without_supported_features(hass, mqtt_mock): +async def test_commands_without_supported_features( + hass, mqtt_mock_entry_with_yaml_config +): """Test commands which are not supported by the vacuum.""" config = deepcopy(DEFAULT_CONFIG) services = mqttvacuum.STRING_TO_SERVICE["status"] @@ -181,6 +185,7 @@ async def test_commands_without_supported_features(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( DOMAIN, SERVICE_START, {"entity_id": ENTITY_MATCH_ALL}, blocking=True @@ -228,7 +233,7 @@ async def test_commands_without_supported_features(hass, mqtt_mock): mqtt_mock.async_publish.assert_not_called() -async def test_status(hass, mqtt_mock): +async def test_status(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -237,6 +242,7 @@ async def test_status(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state.state == STATE_UNKNOWN @@ -272,7 +278,7 @@ async def test_status(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_no_fan_vacuum(hass, mqtt_mock): +async def test_no_fan_vacuum(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum when fan is not supported.""" config = deepcopy(DEFAULT_CONFIG) del config[mqttvacuum.CONF_FAN_SPEED_LIST] @@ -282,6 +288,7 @@ async def test_no_fan_vacuum(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "battery_level": 54, @@ -323,7 +330,7 @@ async def test_no_fan_vacuum(hass, mqtt_mock): @pytest.mark.no_fail_on_log_exception -async def test_status_invalid_json(hass, mqtt_mock): +async def test_status_invalid_json(hass, mqtt_mock_entry_with_yaml_config): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -332,83 +339,98 @@ async def test_status_invalid_json(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "vacuum/state", '{"asdfasas false}') state = hass.states.get("vacuum.mqtttest") assert state.state == STATE_UNKNOWN -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2, MQTT_VACUUM_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + vacuum.DOMAIN, + DEFAULT_CONFIG_2, + MQTT_VACUUM_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_json( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one vacuum per unique_id.""" config = { vacuum.DOMAIN: [ @@ -428,92 +450,103 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, vacuum.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, config + ) -async def test_discovery_removal_vacuum(hass, mqtt_mock, caplog): +async def test_discovery_removal_vacuum(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered vacuum.""" data = '{ "schema": "state", "name": "test", "command_topic": "test_topic"}' - await help_test_discovery_removal(hass, mqtt_mock, caplog, vacuum.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, data + ) -async def test_discovery_update_vacuum(hass, mqtt_mock, caplog): +async def test_discovery_update_vacuum(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered vacuum.""" 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, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_vacuum(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_vacuum( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered vacuum.""" data1 = '{ "schema": "state", "name": "Beer", "command_topic": "test_topic"}' with patch( "homeassistant.components.mqtt.vacuum.schema_state.MqttStateVacuum.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + vacuum.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "schema": "state", "name": "Beer", "command_topic": "test_topic#"}' data2 = '{ "schema": "state", "name": "Milk", "command_topic": "test_topic"}' await help_test_discovery_broken( - hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2, vacuum.SERVICE_START, @@ -564,7 +597,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -590,7 +623,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -602,11 +635,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = vacuum.DOMAIN config = DEFAULT_CONFIG - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -634,12 +669,18 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG, diff --git a/tests/components/mqtt/test_subscription.py b/tests/components/mqtt/test_subscription.py index e2ffc602ddd..7c1663b9c09 100644 --- a/tests/components/mqtt/test_subscription.py +++ b/tests/components/mqtt/test_subscription.py @@ -11,8 +11,9 @@ from homeassistant.core import callback from tests.common import async_fire_mqtt_message -async def test_subscribe_topics(hass, mqtt_mock, caplog): +async def test_subscribe_topics(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test subscription to topics.""" + await mqtt_mock_entry_no_yaml_config() calls1 = [] @callback @@ -59,8 +60,9 @@ async def test_subscribe_topics(hass, mqtt_mock, caplog): assert len(calls2) == 1 -async def test_modify_topics(hass, mqtt_mock, caplog): +async def test_modify_topics(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test modification of topics.""" + await mqtt_mock_entry_no_yaml_config() calls1 = [] @callback @@ -121,8 +123,9 @@ async def test_modify_topics(hass, mqtt_mock, caplog): assert len(calls2) == 1 -async def test_qos_encoding_default(hass, mqtt_mock, caplog): +async def test_qos_encoding_default(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test default qos and encoding.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() @callback def msg_callback(*args): @@ -136,11 +139,12 @@ async def test_qos_encoding_default(hass, mqtt_mock, caplog): {"test_topic1": {"topic": "test-topic1", "msg_callback": msg_callback}}, ) await async_subscribe_topics(hass, sub_state) - mqtt_mock.async_subscribe.assert_called_once_with("test-topic1", ANY, 0, "utf-8") + mqtt_mock.async_subscribe.assert_called_with("test-topic1", ANY, 0, "utf-8") -async def test_qos_encoding_custom(hass, mqtt_mock, caplog): +async def test_qos_encoding_custom(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test custom qos and encoding.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() @callback def msg_callback(*args): @@ -161,11 +165,12 @@ async def test_qos_encoding_custom(hass, mqtt_mock, caplog): }, ) await async_subscribe_topics(hass, sub_state) - mqtt_mock.async_subscribe.assert_called_once_with("test-topic1", ANY, 1, "utf-16") + mqtt_mock.async_subscribe.assert_called_with("test-topic1", ANY, 1, "utf-16") -async def test_no_change(hass, mqtt_mock, caplog): +async def test_no_change(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test subscription to topics without change.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() calls = [] diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 699f0de87f0..b217bf40c22 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -53,7 +53,7 @@ DEFAULT_CONFIG = { } -async def test_controlling_state_via_topic(hass, mqtt_mock): +async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -71,6 +71,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("switch.test") assert state.state == STATE_UNKNOWN @@ -93,7 +94,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending MQTT commands in optimistic mode.""" fake_state = ha.State("switch.test", "on") @@ -116,6 +119,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("switch.test") assert state.state == STATE_ON @@ -139,7 +143,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_sending_inital_state_and_optimistic(hass, mqtt_mock): +async def test_sending_inital_state_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the initial state in optimistic mode.""" assert await async_setup_component( hass, @@ -153,13 +159,16 @@ async def test_sending_inital_state_and_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("switch.test") assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): +async def test_controlling_state_via_topic_and_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, @@ -177,6 +186,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("switch.test") assert state.state == STATE_UNKNOWN @@ -197,21 +207,23 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { switch.DOMAIN: { @@ -225,11 +237,17 @@ async def test_default_availability_payload(hass, mqtt_mock): } await help_test_default_availability_payload( - hass, mqtt_mock, switch.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + switch.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { switch.DOMAIN: { @@ -243,11 +261,17 @@ async def test_custom_availability_payload(hass, mqtt_mock): } await help_test_custom_availability_payload( - hass, mqtt_mock, switch.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + switch.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_custom_state_payload(hass, mqtt_mock): +async def test_custom_state_payload(hass, mqtt_mock_entry_with_yaml_config): """Test the state payload.""" assert await async_setup_component( hass, @@ -266,6 +290,7 @@ async def test_custom_state_payload(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("switch.test") assert state.state == STATE_UNKNOWN @@ -282,49 +307,57 @@ async def test_custom_state_payload(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG, {} + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG, {} ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one switch per unique_id.""" config = { switch.DOMAIN: [ @@ -344,20 +377,26 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, switch.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, config + ) -async def test_discovery_removal_switch(hass, mqtt_mock, caplog): +async def test_discovery_removal_switch(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered switch.""" data = ( '{ "name": "test",' ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_removal(hass, mqtt_mock, caplog, switch.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, switch.DOMAIN, data + ) -async def test_discovery_update_switch_topic_template(hass, mqtt_mock, caplog): +async def test_discovery_update_switch_topic_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered switch.""" config1 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) @@ -382,7 +421,7 @@ async def test_discovery_update_switch_topic_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, switch.DOMAIN, config1, @@ -392,7 +431,9 @@ async def test_discovery_update_switch_topic_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_switch_template(hass, mqtt_mock, caplog): +async def test_discovery_update_switch_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered switch.""" config1 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) @@ -415,7 +456,7 @@ async def test_discovery_update_switch_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, switch.DOMAIN, config1, @@ -425,7 +466,9 @@ async def test_discovery_update_switch_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_unchanged_switch(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_switch( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered switch.""" data1 = ( '{ "name": "Beer",' @@ -437,12 +480,17 @@ async def test_discovery_update_unchanged_switch(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.switch.MqttSwitch.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, switch.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + switch.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -451,56 +499,60 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ' "command_topic": "test_topic" }' ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, switch.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, switch.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT switch device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT switch device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG, switch.SERVICE_TURN_ON + hass, + mqtt_mock_entry_no_yaml_config, + switch.DOMAIN, + DEFAULT_CONFIG, + switch.SERVICE_TURN_ON, ) @@ -525,7 +577,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -539,7 +591,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -551,11 +603,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = switch.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -572,12 +626,18 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, switch.DOMAIN, DEFAULT_CONFIG[switch.DOMAIN], diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index 99e2fffc085..09be31011f2 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -62,8 +62,11 @@ def tag_mock(): @pytest.mark.no_fail_on_log_exception -async def test_discover_bad_tag(hass, device_reg, entity_reg, mqtt_mock, tag_mock): +async def test_discover_bad_tag( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config, tag_mock +): """Test bad discovery message.""" + await mqtt_mock_entry_no_yaml_config() config1 = copy.deepcopy(DEFAULT_CONFIG_DEVICE) # Test sending bad data @@ -84,9 +87,10 @@ async def test_discover_bad_tag(hass, device_reg, entity_reg, mqtt_mock, tag_moc async def test_if_fires_on_mqtt_message_with_device( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning, with device.""" + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG_DEVICE) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -100,9 +104,10 @@ async def test_if_fires_on_mqtt_message_with_device( async def test_if_fires_on_mqtt_message_without_device( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning, without device.""" + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -115,9 +120,10 @@ async def test_if_fires_on_mqtt_message_without_device( async def test_if_fires_on_mqtt_message_with_template( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning, with device.""" + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG_JSON) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -130,8 +136,9 @@ async def test_if_fires_on_mqtt_message_with_template( tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id) -async def test_strip_tag_id(hass, device_reg, mqtt_mock, tag_mock): +async def test_strip_tag_id(hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock): """Test strip whitespace from tag_id.""" + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -144,9 +151,10 @@ async def test_strip_tag_id(hass, device_reg, mqtt_mock, tag_mock): async def test_if_fires_on_mqtt_message_after_update_with_device( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning after update.""" + await mqtt_mock_entry_no_yaml_config() config1 = copy.deepcopy(DEFAULT_CONFIG_DEVICE) config1["some_future_option_1"] = "future_option_1" config2 = copy.deepcopy(DEFAULT_CONFIG_DEVICE) @@ -190,9 +198,10 @@ async def test_if_fires_on_mqtt_message_after_update_with_device( async def test_if_fires_on_mqtt_message_after_update_without_device( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning after update.""" + await mqtt_mock_entry_no_yaml_config() config1 = copy.deepcopy(DEFAULT_CONFIG) config2 = copy.deepcopy(DEFAULT_CONFIG) config2["topic"] = "foobar/tag_scanned2" @@ -233,9 +242,10 @@ async def test_if_fires_on_mqtt_message_after_update_without_device( async def test_if_fires_on_mqtt_message_after_update_with_template( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning after update.""" + await mqtt_mock_entry_no_yaml_config() config1 = copy.deepcopy(DEFAULT_CONFIG_JSON) config2 = copy.deepcopy(DEFAULT_CONFIG_JSON) config2["value_template"] = "{{ value_json.RDM6300.UID }}" @@ -277,8 +287,11 @@ async def test_if_fires_on_mqtt_message_after_update_with_template( tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id) -async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock): +async def test_no_resubscribe_same_topic( + hass, device_reg, mqtt_mock_entry_no_yaml_config +): """Test subscription to topics without change.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG_DEVICE) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -292,9 +305,10 @@ async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock): async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_with_device( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning after removal.""" + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG_DEVICE) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -325,9 +339,10 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_with_device( async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_without_device( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning not firing after removal.""" + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -360,11 +375,13 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( hass, hass_ws_client, device_reg, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, tag_mock, ): """Test tag scanning after removal.""" assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() ws_client = await hass_ws_client(hass) config = copy.deepcopy(DEFAULT_CONFIG_DEVICE) @@ -397,8 +414,9 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( tag_mock.assert_not_called() -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT device registry integration.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) data = json.dumps( @@ -427,8 +445,9 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock): assert device.sw_version == "0.1-beta" -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT device registry integration.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) data = json.dumps( @@ -455,8 +474,9 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): assert device.sw_version == "0.1-beta" -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) config = { @@ -489,9 +509,13 @@ async def test_entity_device_info_update(hass, mqtt_mock): assert device.name == "Milk" -async def test_cleanup_tag(hass, hass_ws_client, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_tag( + hass, hass_ws_client, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test tag discovery topic is cleaned when device is removed from registry.""" assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_no_yaml_config() ws_client = await hass_ws_client(hass) mqtt_entry = hass.config_entries.async_entries("mqtt")[0] @@ -566,8 +590,11 @@ async def test_cleanup_tag(hass, hass_ws_client, device_reg, entity_reg, mqtt_mo ) -async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test removal from device registry when tag is removed.""" + await mqtt_mock_entry_no_yaml_config() config = { "topic": "test-topic", "device": {"identifiers": ["helloworld"]}, @@ -590,9 +617,10 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): async def test_cleanup_device_several_tags( - hass, device_reg, entity_reg, mqtt_mock, tag_mock + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test removal from device registry when the last tag is removed.""" + await mqtt_mock_entry_no_yaml_config() config1 = { "topic": "test-topic1", "device": {"identifiers": ["helloworld"]}, @@ -634,12 +662,13 @@ async def test_cleanup_device_several_tags( async def test_cleanup_device_with_entity_and_trigger_1( - hass, device_reg, entity_reg, mqtt_mock + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config ): """Test removal from device registry for device with tag, entity and trigger. Tag removed first, then trigger and entity. """ + await mqtt_mock_entry_no_yaml_config() config1 = { "topic": "test-topic", "device": {"identifiers": ["helloworld"]}, @@ -697,11 +726,14 @@ async def test_cleanup_device_with_entity_and_trigger_1( assert device_entry is None -async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device_with_entity2( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test removal from device registry for device with tag, entity and trigger. Trigger and entity removed first, then tag. """ + await mqtt_mock_entry_no_yaml_config() config1 = { "topic": "test-topic", "device": {"identifiers": ["helloworld"]}, diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py index c2c77dcddd8..a4079558c34 100644 --- a/tests/components/mqtt/test_trigger.py +++ b/tests/components/mqtt/test_trigger.py @@ -18,9 +18,10 @@ def calls(hass): @pytest.fixture(autouse=True) -def setup_comp(hass, mqtt_mock): +async def setup_comp(hass, mqtt_mock_entry_no_yaml_config): """Initialize components.""" mock_component(hass, "group") + return await mqtt_mock_entry_no_yaml_config() async def test_if_fires_on_topic_match(hass, calls): @@ -213,7 +214,7 @@ async def test_if_not_fires_on_topic_but_no_payload_match(hass, calls): assert len(calls) == 0 -async def test_encoding_default(hass, calls, mqtt_mock): +async def test_encoding_default(hass, calls, setup_comp): """Test default encoding.""" assert await async_setup_component( hass, @@ -226,10 +227,10 @@ async def test_encoding_default(hass, calls, mqtt_mock): }, ) - mqtt_mock.async_subscribe.assert_called_once_with("test-topic", ANY, 0, "utf-8") + setup_comp.async_subscribe.assert_called_with("test-topic", ANY, 0, "utf-8") -async def test_encoding_custom(hass, calls, mqtt_mock): +async def test_encoding_custom(hass, calls, setup_comp): """Test default encoding.""" assert await async_setup_component( hass, @@ -242,4 +243,4 @@ async def test_encoding_custom(hass, calls, mqtt_mock): }, ) - mqtt_mock.async_subscribe.assert_called_once_with("test-topic", ANY, 0, None) + setup_comp.async_subscribe.assert_called_with("test-topic", ANY, 0, None) diff --git a/tests/components/mqtt_json/test_device_tracker.py b/tests/components/mqtt_json/test_device_tracker.py index d17484cc5e9..b0cba664250 100644 --- a/tests/components/mqtt_json/test_device_tracker.py +++ b/tests/components/mqtt_json/test_device_tracker.py @@ -26,7 +26,7 @@ LOCATION_MESSAGE_INCOMPLETE = {"longitude": 2.0} @pytest.fixture(autouse=True) -def setup_comp(hass, mqtt_mock): +async def setup_comp(hass, mqtt_mock_entry_with_yaml_config): """Initialize components.""" yaml_devices = hass.config.path(YAML_DEVICES) yield diff --git a/tests/components/mqtt_room/test_sensor.py b/tests/components/mqtt_room/test_sensor.py index c3b8704c754..b17a2bed457 100644 --- a/tests/components/mqtt_room/test_sensor.py +++ b/tests/components/mqtt_room/test_sensor.py @@ -47,7 +47,7 @@ async def assert_distance(hass, distance): assert state.attributes.get("distance") == distance -async def test_room_update(hass, mqtt_mock): +async def test_room_update(hass, mqtt_mock_entry_with_yaml_config): """Test the updating between rooms.""" assert await async_setup_component( hass, diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index 0b7e3726482..a97b877d819 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -1,7 +1,7 @@ """The tests for the MQTT discovery.""" import copy import json -from unittest.mock import patch +from unittest.mock import ANY, patch from homeassistant.components.tasmota.const import DEFAULT_PREFIX from homeassistant.components.tasmota.discovery import ALREADY_DISCOVERED @@ -19,9 +19,7 @@ async def test_subscribing_config_topic(hass, mqtt_mock, setup_tasmota): discovery_topic = DEFAULT_PREFIX assert mqtt_mock.async_subscribe.called - call_args = mqtt_mock.async_subscribe.mock_calls[0][1] - assert call_args[0] == discovery_topic + "/#" - assert call_args[2] == 0 + mqtt_mock.async_subscribe.assert_any_call(discovery_topic + "/#", ANY, 0, "utf-8") async def test_future_discovery_message(hass, mqtt_mock, caplog): diff --git a/tests/conftest.py b/tests/conftest.py index 8ea6e114e9a..97b1a959d2c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager import functools import logging import ssl @@ -547,8 +548,19 @@ def mqtt_client_mock(hass): @pytest.fixture -async def mqtt_mock(hass, mqtt_client_mock, mqtt_config): +async def mqtt_mock( + hass, + mqtt_client_mock, + mqtt_config, + mqtt_mock_entry_no_yaml_config, +): """Fixture to mock MQTT component.""" + return await mqtt_mock_entry_no_yaml_config() + + +@asynccontextmanager +async def _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config): + """Fixture to mock a delayed setup of the MQTT config entry.""" if mqtt_config is None: mqtt_config = {mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}} @@ -557,29 +569,79 @@ async def mqtt_mock(hass, mqtt_client_mock, mqtt_config): entry = MockConfigEntry( data=mqtt_config, domain=mqtt.DOMAIN, - title="Tasmota", + title="MQTT", ) - entry.add_to_hass(hass) - # Do not forward the entry setup to the components here - with patch("homeassistant.components.mqtt.PLATFORMS", []): - assert await hass.config_entries.async_setup(entry.entry_id) + + real_mqtt = mqtt.MQTT + real_mqtt_instance = None + mock_mqtt_instance = None + + async def _setup_mqtt_entry(setup_entry): + """Set up the MQTT config entry.""" + assert await setup_entry(hass, entry) + + # Assert that MQTT is setup + assert real_mqtt_instance is not None, "MQTT was not setup correctly" + mock_mqtt_instance.conf = real_mqtt_instance.conf # For diagnostics + mock_mqtt_instance._mqttc = mqtt_client_mock + + # connected set to True to get a more realistic behavior when subscribing + mock_mqtt_instance.connected = True + + hass.helpers.dispatcher.async_dispatcher_send(mqtt.MQTT_CONNECTED) await hass.async_block_till_done() - mqtt_component_mock = MagicMock( - return_value=hass.data["mqtt"], - spec_set=hass.data["mqtt"], - wraps=hass.data["mqtt"], - ) - mqtt_component_mock.conf = hass.data["mqtt"].conf # For diagnostics - mqtt_component_mock._mqttc = mqtt_client_mock - # connected set to True to get a more realistics behavior when subscribing - hass.data["mqtt"].connected = True + return mock_mqtt_instance - hass.data["mqtt"] = mqtt_component_mock - component = hass.data["mqtt"] - component.reset_mock() - return component + def create_mock_mqtt(*args, **kwargs): + """Create a mock based on mqtt.MQTT.""" + nonlocal mock_mqtt_instance + nonlocal real_mqtt_instance + real_mqtt_instance = real_mqtt(*args, **kwargs) + mock_mqtt_instance = MagicMock( + return_value=real_mqtt_instance, + spec_set=real_mqtt_instance, + wraps=real_mqtt_instance, + ) + return mock_mqtt_instance + + with patch("homeassistant.components.mqtt.MQTT", side_effect=create_mock_mqtt): + yield _setup_mqtt_entry + + +@pytest.fixture +async def mqtt_mock_entry_no_yaml_config(hass, mqtt_client_mock, mqtt_config): + """Set up an MQTT config entry without MQTT yaml config.""" + + async def _async_setup_config_entry(hass, entry): + """Help set up the config entry.""" + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + return True + + async def _setup_mqtt_entry(): + """Set up the MQTT config entry.""" + return await mqtt_mock_entry(_async_setup_config_entry) + + async with _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config) as mqtt_mock_entry: + yield _setup_mqtt_entry + + +@pytest.fixture +async def mqtt_mock_entry_with_yaml_config(hass, mqtt_client_mock, mqtt_config): + """Set up an MQTT config entry with MQTT yaml config.""" + + async def _async_do_not_setup_config_entry(hass, entry): + """Do nothing.""" + return True + + async def _setup_mqtt_entry(): + """Set up the MQTT config entry.""" + return await mqtt_mock_entry(_async_do_not_setup_config_entry) + + async with _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config) as mqtt_mock_entry: + yield _setup_mqtt_entry @pytest.fixture(autouse=True) From fc8727454aa11320a196aeca4ba109d7734bd86f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 2 Jun 2022 14:28:14 +0200 Subject: [PATCH 1183/3516] Use Mapping for async_step_reauth (p-s) (#72766) --- homeassistant/components/powerwall/config_flow.py | 3 ++- homeassistant/components/pvoutput/config_flow.py | 3 ++- homeassistant/components/renault/config_flow.py | 7 ++++--- homeassistant/components/ridwell/config_flow.py | 3 ++- homeassistant/components/samsungtv/config_flow.py | 4 ++-- homeassistant/components/sensibo/config_flow.py | 5 ++--- homeassistant/components/simplisafe/config_flow.py | 3 ++- homeassistant/components/sleepiq/config_flow.py | 5 +++-- homeassistant/components/steam_online/config_flow.py | 3 ++- homeassistant/components/surepetcare/config_flow.py | 3 ++- homeassistant/components/synology_dsm/config_flow.py | 7 ++++--- homeassistant/components/system_bridge/config_flow.py | 3 ++- 12 files changed, 29 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index 836aa46e2a4..b541c1b4bf7 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Tesla Powerwall integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -205,7 +206,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, data: dict[str, str]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/pvoutput/config_flow.py b/homeassistant/components/pvoutput/config_flow.py index 4349a79593e..25cc68acc24 100644 --- a/homeassistant/components/pvoutput/config_flow.py +++ b/homeassistant/components/pvoutput/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure the PVOutput integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any from pvo import PVOutput, PVOutputAuthenticationError, PVOutputError @@ -83,7 +84,7 @@ class PVOutputFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle initiation of re-authentication with PVOutput.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/renault/config_flow.py b/homeassistant/components/renault/config_flow.py index 47832cdbe93..539ba7549b3 100644 --- a/homeassistant/components/renault/config_flow.py +++ b/homeassistant/components/renault/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure Renault component.""" from __future__ import annotations +from collections.abc import Mapping from typing import TYPE_CHECKING, Any from renault_api.const import AVAILABLE_LOCALES @@ -21,7 +22,7 @@ class RenaultFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the Renault config flow.""" - self._original_data: dict[str, Any] | None = None + self._original_data: Mapping[str, Any] | None = None self.renault_config: dict[str, Any] = {} self.renault_hub: RenaultHub | None = None @@ -92,9 +93,9 @@ class RenaultFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ), ) - async def async_step_reauth(self, user_input: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" - self._original_data = user_input.copy() + self._original_data = data return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( diff --git a/homeassistant/components/ridwell/config_flow.py b/homeassistant/components/ridwell/config_flow.py index 405474f5875..2d6444fede9 100644 --- a/homeassistant/components/ridwell/config_flow.py +++ b/homeassistant/components/ridwell/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Ridwell integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import TYPE_CHECKING, Any from aioridwell import async_get_client @@ -80,7 +81,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data={CONF_USERNAME: self._username, CONF_PASSWORD: self._password}, ) - async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._username = config[CONF_USERNAME] return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index bfa2482f617..130b1d28d5f 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -1,9 +1,9 @@ """Config flow for Samsung TV.""" from __future__ import annotations +from collections.abc import Mapping from functools import partial import socket -from types import MappingProxyType from typing import Any from urllib.parse import urlparse @@ -525,7 +525,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_reauth( - self, data: MappingProxyType[str, Any] + self, data: Mapping[str, Any] ) -> data_entry_flow.FlowResult: """Handle configuration by re-auth.""" self._reauth_entry = self.hass.config_entries.async_get_entry( diff --git a/homeassistant/components/sensibo/config_flow.py b/homeassistant/components/sensibo/config_flow.py index a3254a01839..a3214bdad56 100644 --- a/homeassistant/components/sensibo/config_flow.py +++ b/homeassistant/components/sensibo/config_flow.py @@ -1,6 +1,7 @@ """Adds config flow for Sensibo integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any from pysensibo.exceptions import AuthenticationError @@ -28,9 +29,7 @@ class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): entry: config_entries.ConfigEntry | None - async def async_step_reauth( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle re-authentication with Sensibo.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 14afc743b23..3fcab1f3966 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping from typing import Any import async_timeout @@ -97,7 +98,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Define the config flow to handle options.""" return SimpliSafeOptionsFlowHandler(config_entry) - async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._reauth = True diff --git a/homeassistant/components/sleepiq/config_flow.py b/homeassistant/components/sleepiq/config_flow.py index 49f14eff0b9..c78daa76fbc 100644 --- a/homeassistant/components/sleepiq/config_flow.py +++ b/homeassistant/components/sleepiq/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure SleepIQ component.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -79,12 +80,12 @@ class SleepIQFlowHandler(ConfigFlow, domain=DOMAIN): last_step=True, ) - async def async_step_reauth(self, user_input: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] ) - return await self.async_step_reauth_confirm(user_input) + return await self.async_step_reauth_confirm(dict(data)) async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/steam_online/config_flow.py b/homeassistant/components/steam_online/config_flow.py index 338b0a80fb6..94b3e8d809c 100644 --- a/homeassistant/components/steam_online/config_flow.py +++ b/homeassistant/components/steam_online/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Steam integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any import steam @@ -125,7 +126,7 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): import_config[CONF_ACCOUNT] = import_config[CONF_ACCOUNTS][0] return await self.async_step_user(import_config) - async def async_step_reauth(self, user_input: dict[str, str]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle a reauthorization flow request.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) diff --git a/homeassistant/components/surepetcare/config_flow.py b/homeassistant/components/surepetcare/config_flow.py index 30f20257e8c..b8b3d690a8b 100644 --- a/homeassistant/components/surepetcare/config_flow.py +++ b/homeassistant/components/surepetcare/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Sure Petcare integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -86,7 +87,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=USER_DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._username = config[CONF_USERNAME] return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 256ad5eef8e..bc35caf300e 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 collections.abc import Mapping from ipaddress import ip_address import logging from typing import Any @@ -120,7 +121,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): """Initialize the synology_dsm config flow.""" self.saved_user_input: dict[str, Any] = {} self.discovered_conf: dict[str, Any] = {} - self.reauth_conf: dict[str, Any] = {} + self.reauth_conf: Mapping[str, Any] = {} self.reauth_reason: str | None = None def _show_form( @@ -299,9 +300,9 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): user_input = {**self.discovered_conf, **user_input} return await self.async_validate_input_create_entry(user_input, step_id=step) - async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" - self.reauth_conf = data.copy() + self.reauth_conf = data return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( diff --git a/homeassistant/components/system_bridge/config_flow.py b/homeassistant/components/system_bridge/config_flow.py index 0c1241fcf2c..9d89cf83288 100644 --- a/homeassistant/components/system_bridge/config_flow.py +++ b/homeassistant/components/system_bridge/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping import logging from typing import Any @@ -202,7 +203,7 @@ class ConfigFlow( return await self.async_step_authenticate() - async def async_step_reauth(self, entry_data: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" self._name = entry_data[CONF_HOST] self._input = { From b89cd37de8f1d8500473ca2db1baa70ff83e92c1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 2 Jun 2022 17:19:15 +0200 Subject: [PATCH 1184/3516] Remove dead code from template fan (#72917) --- homeassistant/components/template/fan.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 2c8d7247967..55f81b697bf 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -54,7 +54,6 @@ CONF_DIRECTION_TEMPLATE = "direction_template" CONF_ON_ACTION = "turn_on" CONF_OFF_ACTION = "turn_off" CONF_SET_PERCENTAGE_ACTION = "set_percentage" -CONF_SET_SPEED_ACTION = "set_speed" CONF_SET_OSCILLATING_ACTION = "set_oscillating" CONF_SET_DIRECTION_ACTION = "set_direction" CONF_SET_PRESET_MODE_ACTION = "set_preset_mode" @@ -153,12 +152,6 @@ class TemplateFan(TemplateEntity, FanEntity): self._on_script = Script(hass, config[CONF_ON_ACTION], friendly_name, DOMAIN) self._off_script = Script(hass, config[CONF_OFF_ACTION], friendly_name, DOMAIN) - self._set_speed_script = None - if set_speed_action := config.get(CONF_SET_SPEED_ACTION): - self._set_speed_script = Script( - hass, set_speed_action, friendly_name, DOMAIN - ) - self._set_percentage_script = None if set_percentage_action := config.get(CONF_SET_PERCENTAGE_ACTION): self._set_percentage_script = Script( From 9192d0e972a495d9d14e2a290a61851d980b7e1a Mon Sep 17 00:00:00 2001 From: Matrix Date: Thu, 2 Jun 2022 23:21:22 +0800 Subject: [PATCH 1185/3516] Bump yolink-api to 0.0.6 (#72903) * Bump yolink-api to 0.0.6 * update testcase --- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/yolink/test_config_flow.py | 6 ++---- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index a89934154e9..7fb78a4974b 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -3,7 +3,7 @@ "name": "YoLink", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yolink", - "requirements": ["yolink-api==0.0.5"], + "requirements": ["yolink-api==0.0.6"], "dependencies": ["auth", "application_credentials"], "codeowners": ["@matrixd2"], "iot_class": "cloud_push" diff --git a/requirements_all.txt b/requirements_all.txt index 250551caa47..845af95f0b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2486,7 +2486,7 @@ yeelight==0.7.10 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.0.5 +yolink-api==0.0.6 # homeassistant.components.youless youless-api==0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e9aa909f3d..3458d0326e3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1638,7 +1638,7 @@ yalexs==1.1.25 yeelight==0.7.10 # homeassistant.components.yolink -yolink-api==0.0.5 +yolink-api==0.0.6 # homeassistant.components.youless youless-api==0.16 diff --git a/tests/components/yolink/test_config_flow.py b/tests/components/yolink/test_config_flow.py index 5d6bb8fd727..e224bc3e1d2 100644 --- a/tests/components/yolink/test_config_flow.py +++ b/tests/components/yolink/test_config_flow.py @@ -3,6 +3,8 @@ import asyncio from http import HTTPStatus from unittest.mock import patch +from yolink.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components import application_credentials from homeassistant.core import HomeAssistant @@ -12,11 +14,7 @@ from tests.common import MockConfigEntry CLIENT_ID = "12345" CLIENT_SECRET = "6789" -YOLINK_HOST = "api.yosmart.com" -YOLINK_HTTP_HOST = f"http://{YOLINK_HOST}" DOMAIN = "yolink" -OAUTH2_AUTHORIZE = f"{YOLINK_HTTP_HOST}/oauth/v2/authorization.htm" -OAUTH2_TOKEN = f"{YOLINK_HTTP_HOST}/open/yolink/token" async def test_abort_if_no_configuration(hass): From f1a31d8d333f6c88b7e61719284accf38bea209e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Jun 2022 05:26:08 -1000 Subject: [PATCH 1186/3516] Add support for async_remove_config_entry_device to unifiprotect (#72742) * Add support for async_remove_config_entry_device to unifiprotect * tweaks * tweaks * more cleanups * more cleanups * fix unhelpful auto import * add coverage * fix mac formatting * collapse logic --- .../components/unifiprotect/__init__.py | 22 +++++- homeassistant/components/unifiprotect/data.py | 34 ++++++---- .../components/unifiprotect/entity.py | 6 +- .../components/unifiprotect/services.py | 16 +---- .../components/unifiprotect/utils.py | 38 +++++++++++ tests/components/unifiprotect/test_init.py | 68 ++++++++++++++++++- 6 files changed, 152 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index c28f2639e00..4ec11a899e3 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -21,7 +21,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.aiohttp_client import async_create_clientsession from .const import ( @@ -35,9 +35,10 @@ from .const import ( OUTDATED_LOG_MESSAGE, PLATFORMS, ) -from .data import ProtectData +from .data import ProtectData, async_ufp_instance_for_config_entry_ids from .discovery import async_start_discovery from .services import async_cleanup_services, async_setup_services +from .utils import _async_unifi_mac_from_hass, async_get_devices _LOGGER = logging.getLogger(__name__) @@ -166,3 +167,20 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_cleanup_services(hass) return bool(unload_ok) + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove ufp config entry from a device.""" + unifi_macs = { + _async_unifi_mac_from_hass(connection[1]) + for connection in device_entry.connections + if connection[0] == dr.CONNECTION_NETWORK_MAC + } + api = async_ufp_instance_for_config_entry_ids(hass, {config_entry.entry_id}) + assert api is not None + return api.bootstrap.nvr.mac not in unifi_macs and not any( + device.mac in unifi_macs + for device in async_get_devices(api, DEVICES_THAT_ADOPT) + ) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 371c1c7831b..68c8873c17e 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -14,13 +14,14 @@ from pyunifiprotect.data import ( ModelType, WSSubscriptionMessage, ) -from pyunifiprotect.data.base import ProtectAdoptableDeviceModel, ProtectDeviceModel +from pyunifiprotect.data.base import ProtectAdoptableDeviceModel from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.event import async_track_time_interval -from .const import CONF_DISABLE_RTSP, DEVICES_THAT_ADOPT, DEVICES_WITH_ENTITIES +from .const import CONF_DISABLE_RTSP, DEVICES_THAT_ADOPT, DEVICES_WITH_ENTITIES, DOMAIN +from .utils import async_get_adoptable_devices_by_type, async_get_devices _LOGGER = logging.getLogger(__name__) @@ -58,13 +59,10 @@ class ProtectData: self, device_types: Iterable[ModelType] ) -> Generator[ProtectAdoptableDeviceModel, None, None]: """Get all devices matching types.""" - for device_type in device_types: - attr = f"{device_type.value}s" - devices: dict[str, ProtectAdoptableDeviceModel] = getattr( - self.api.bootstrap, attr - ) - yield from devices.values() + yield from async_get_adoptable_devices_by_type( + self.api, device_type + ).values() async def async_setup(self) -> None: """Subscribe and do the refresh.""" @@ -145,11 +143,8 @@ class ProtectData: return self.async_signal_device_id_update(self.api.bootstrap.nvr.id) - for device_type in DEVICES_THAT_ADOPT: - attr = f"{device_type.value}s" - devices: dict[str, ProtectDeviceModel] = getattr(self.api.bootstrap, attr) - for device_id in devices.keys(): - self.async_signal_device_id_update(device_id) + for device in async_get_devices(self.api, DEVICES_THAT_ADOPT): + self.async_signal_device_id_update(device.id) @callback def async_subscribe_device_id( @@ -188,3 +183,16 @@ class ProtectData: _LOGGER.debug("Updating device: %s", device_id) for update_callback in self._subscriptions[device_id]: update_callback() + + +@callback +def async_ufp_instance_for_config_entry_ids( + hass: HomeAssistant, config_entry_ids: set[str] +) -> ProtectApiClient | None: + """Find the UFP instance for the config entry ids.""" + domain_data = hass.data[DOMAIN] + for config_entry_id in config_entry_ids: + if config_entry_id in domain_data: + protect_data: ProtectData = domain_data[config_entry_id] + return protect_data.api + return None diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index f8ceaeec9e6..2911a861535 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -26,7 +26,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from .const import ATTR_EVENT_SCORE, DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN from .data import ProtectData from .models import ProtectRequiredKeysMixin -from .utils import get_nested_attr +from .utils import async_get_adoptable_devices_by_type, get_nested_attr _LOGGER = logging.getLogger(__name__) @@ -153,7 +153,9 @@ class ProtectDeviceEntity(Entity): """Update Entity object from Protect device.""" if self.data.last_update_success: assert self.device.model - devices = getattr(self.data.api.bootstrap, f"{self.device.model.value}s") + devices = async_get_adoptable_devices_by_type( + self.data.api, self.device.model + ) self.device = devices[self.device.id] is_connected = ( diff --git a/homeassistant/components/unifiprotect/services.py b/homeassistant/components/unifiprotect/services.py index f8aa446f857..828aa9ecfd7 100644 --- a/homeassistant/components/unifiprotect/services.py +++ b/homeassistant/components/unifiprotect/services.py @@ -24,7 +24,7 @@ from homeassistant.helpers.service import async_extract_referenced_entity_ids from homeassistant.util.read_only_dict import ReadOnlyDict from .const import ATTR_MESSAGE, DOMAIN -from .data import ProtectData +from .data import async_ufp_instance_for_config_entry_ids SERVICE_ADD_DOORBELL_TEXT = "add_doorbell_text" SERVICE_REMOVE_DOORBELL_TEXT = "remove_doorbell_text" @@ -59,18 +59,6 @@ CHIME_PAIRED_SCHEMA = vol.All( ) -def _async_ufp_instance_for_config_entry_ids( - hass: HomeAssistant, config_entry_ids: set[str] -) -> ProtectApiClient | None: - """Find the UFP instance for the config entry ids.""" - domain_data = hass.data[DOMAIN] - for config_entry_id in config_entry_ids: - if config_entry_id in domain_data: - protect_data: ProtectData = domain_data[config_entry_id] - return protect_data.api - return None - - @callback def _async_get_ufp_instance(hass: HomeAssistant, device_id: str) -> ProtectApiClient: device_registry = dr.async_get(hass) @@ -81,7 +69,7 @@ def _async_get_ufp_instance(hass: HomeAssistant, device_id: str) -> ProtectApiCl return _async_get_ufp_instance(hass, device_entry.via_device_id) config_entry_ids = device_entry.config_entries - if ufp_instance := _async_ufp_instance_for_config_entry_ids(hass, config_entry_ids): + if ufp_instance := async_ufp_instance_for_config_entry_ids(hass, config_entry_ids): return ufp_instance raise HomeAssistantError(f"No device found for device id: {device_id}") diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index 559cfd37660..fffe987db0f 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -1,13 +1,19 @@ """UniFi Protect Integration utils.""" from __future__ import annotations +from collections.abc import Generator, Iterable import contextlib from enum import Enum import socket from typing import Any +from pyunifiprotect import ProtectApiClient +from pyunifiprotect.data.base import ProtectAdoptableDeviceModel, ProtectDeviceModel + from homeassistant.core import HomeAssistant, callback +from .const import ModelType + def get_nested_attr(obj: Any, attr: str) -> Any: """Fetch a nested attribute.""" @@ -51,3 +57,35 @@ async def _async_resolve(hass: HomeAssistant, host: str) -> str | None: None, ) return None + + +def async_get_devices_by_type( + api: ProtectApiClient, device_type: ModelType +) -> dict[str, ProtectDeviceModel]: + """Get devices by type.""" + devices: dict[str, ProtectDeviceModel] = getattr( + api.bootstrap, f"{device_type.value}s" + ) + return devices + + +def async_get_adoptable_devices_by_type( + api: ProtectApiClient, device_type: ModelType +) -> dict[str, ProtectAdoptableDeviceModel]: + """Get adoptable devices by type.""" + devices: dict[str, ProtectAdoptableDeviceModel] = getattr( + api.bootstrap, f"{device_type.value}s" + ) + return devices + + +@callback +def async_get_devices( + api: ProtectApiClient, model_type: Iterable[ModelType] +) -> Generator[ProtectDeviceModel, None, None]: + """Return all device by type.""" + return ( + device + for device_type in model_type + for device in async_get_devices_by_type(api, device_type).values() + ) diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 95c2ee0b511..cf899d854fd 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -2,8 +2,10 @@ # pylint: disable=protected-access from __future__ import annotations +from collections.abc import Awaitable, Callable from unittest.mock import AsyncMock, patch +import aiohttp from pyunifiprotect import NotAuthorized, NvrError from pyunifiprotect.data import NVR, Light @@ -11,7 +13,8 @@ from homeassistant.components.unifiprotect.const import CONF_DISABLE_RTSP, DOMAI from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import Platform 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 . import _patch_discovery from .conftest import MockBootstrap, MockEntityFixture @@ -19,6 +22,22 @@ from .conftest import MockBootstrap, MockEntityFixture from tests.common import MockConfigEntry +async def remove_device( + ws_client: aiohttp.ClientWebSocketResponse, device_id: str, config_entry_id: str +) -> bool: + """Remove config entry from a device.""" + await ws_client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry_id, + "device_id": device_id, + } + ) + response = await ws_client.receive_json() + return response["success"] + + async def test_setup(hass: HomeAssistant, mock_entry: MockEntityFixture): """Test working setup of unifiprotect entry.""" @@ -321,3 +340,50 @@ async def test_migrate_reboot_button_fail( light = registry.async_get(f"{Platform.BUTTON}.test_light_1") assert light is not None assert light.unique_id == f"{light1.id}" + + +async def test_device_remove_devices( + hass: HomeAssistant, + mock_entry: MockEntityFixture, + mock_light: Light, + hass_ws_client: Callable[ + [HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse] + ], +) -> None: + """Test we can only remove a device that no longer exists.""" + assert await async_setup_component(hass, "config", {}) + + light1 = mock_light.copy() + light1._api = mock_entry.api + light1.name = "Test Light 1" + light1.id = "lightid1" + light1.mac = "AABBCCDDEEFF" + + mock_entry.api.bootstrap.lights = { + light1.id: light1, + } + + mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) + light_entity_id = "light.test_light_1" + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + entry_id = mock_entry.entry.entry_id + + registry: er.EntityRegistry = er.async_get(hass) + entity = registry.entities[light_entity_id] + device_registry = dr.async_get(hass) + + live_device_entry = device_registry.async_get(entity.device_id) + assert ( + await remove_device(await hass_ws_client(hass), live_device_entry.id, entry_id) + is False + ) + + dead_device_entry = device_registry.async_get_or_create( + config_entry_id=entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "e9:88:e7:b8:b4:40")}, + ) + assert ( + await remove_device(await hass_ws_client(hass), dead_device_entry.id, entry_id) + is True + ) From 1c38c20cacbcae68582f30fc799a0426cabfd46e Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 2 Jun 2022 11:27:12 -0400 Subject: [PATCH 1187/3516] Bump ZHA dependency zigpy from 0.45.1 to 0.46.0 (#72877) --- 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 4f16b1c113e..023af7a8a0e 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.75", "zigpy-deconz==0.16.0", - "zigpy==0.45.1", + "zigpy==0.46.0", "zigpy-xbee==0.14.0", "zigpy-zigate==0.7.4", "zigpy-znp==0.7.0" diff --git a/requirements_all.txt b/requirements_all.txt index 845af95f0b5..81cdb84632d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2522,7 +2522,7 @@ zigpy-zigate==0.7.4 zigpy-znp==0.7.0 # homeassistant.components.zha -zigpy==0.45.1 +zigpy==0.46.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3458d0326e3..f5b92cc28d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1662,7 +1662,7 @@ zigpy-zigate==0.7.4 zigpy-znp==0.7.0 # homeassistant.components.zha -zigpy==0.45.1 +zigpy==0.46.0 # homeassistant.components.zwave_js zwave-js-server-python==0.37.1 From d3b1896a0616b0cbb91b39fbbe5f086e0acc4d7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Jun 2022 05:39:53 -1000 Subject: [PATCH 1188/3516] Only present history_stats state as unknown if the time is in the future (#72880) --- homeassistant/components/history_stats/data.py | 9 +++------ tests/components/history_stats/test_sensor.py | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 8153557422d..5466498fc32 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -73,7 +73,8 @@ class HistoryStats: # History cannot tell the future self._history_current_period = [] self._previous_run_before_start = True - + self._state = HistoryStatsState(None, None, self._period) + return self._state # # We avoid querying the database if the below did NOT happen: # @@ -82,7 +83,7 @@ class HistoryStats: # - The period shrank in size # - The previous period ended before now # - elif ( + if ( not self._previous_run_before_start and current_period_start_timestamp == previous_period_start_timestamp and ( @@ -117,10 +118,6 @@ class HistoryStats: ) self._previous_run_before_start = False - if not self._history_current_period: - self._state = HistoryStatsState(None, None, self._period) - return self._state - hours_matched, match_count = self._async_compute_hours_and_changes( now_timestamp, current_period_start_timestamp, diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index b375a8f63c4..f824ee552ca 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -357,7 +357,7 @@ async def test_measure_multiple(hass, recorder_mock): await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "0.5" - assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN + assert hass.states.get("sensor.sensor2").state == "0.0" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "50.0" From 8c50c7fbd4a4638b4bfcd81bc5ba8866a7bd11ef Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 2 Jun 2022 08:40:13 -0700 Subject: [PATCH 1189/3516] Fix bug in caldav and avoid unnecessary copy of dataclass (#72922) --- homeassistant/components/caldav/calendar.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index c5b2b3a790a..17a8d5deb2f 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -1,7 +1,6 @@ """Support for WebDav Calendar.""" from __future__ import annotations -import copy from datetime import datetime, timedelta import logging import re @@ -143,15 +142,13 @@ class WebDavCalendarEntity(CalendarEntity): def update(self): """Update event data.""" self.data.update() - event = copy.deepcopy(self.data.event) - if event is None: - self._event = event - return - (summary, offset) = extract_offset(event.summary, OFFSET) - event.summary = summary - self._event = event + self._event = self.data.event self._attr_extra_state_attributes = { - "offset_reached": is_offset_reached(event.start_datetime_local, offset) + "offset_reached": is_offset_reached( + self._event.start_datetime_local, self.data.offset + ) + if self._event + else False } @@ -165,6 +162,7 @@ class WebDavCalendarData: self.include_all_day = include_all_day self.search = search self.event = None + self.offset = None async def async_get_events( self, hass: HomeAssistant, start_date: datetime, end_date: datetime @@ -264,13 +262,15 @@ class WebDavCalendarData: return # Populate the entity attributes with the event values + (summary, offset) = extract_offset(vevent.summary.value, OFFSET) self.event = CalendarEvent( - summary=vevent.summary.value, + summary=summary, start=vevent.dtstart.value, end=self.get_end_date(vevent), location=self.get_attr_value(vevent, "location"), description=self.get_attr_value(vevent, "description"), ) + self.offset = offset @staticmethod def is_matching(vevent, search): From 8e4321af59308ebc62cfbedfff88800c4aa8164b Mon Sep 17 00:00:00 2001 From: nojocodex <76249511+nojocodex@users.noreply.github.com> Date: Thu, 2 Jun 2022 19:49:08 +0200 Subject: [PATCH 1190/3516] Fix logging & exit code reporting to S6 on HA shutdown (#72921) --- rootfs/etc/services.d/home-assistant/finish | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/rootfs/etc/services.d/home-assistant/finish b/rootfs/etc/services.d/home-assistant/finish index 115d8352618..057957a9c03 100755 --- a/rootfs/etc/services.d/home-assistant/finish +++ b/rootfs/etc/services.d/home-assistant/finish @@ -2,24 +2,24 @@ # ============================================================================== # Take down the S6 supervision tree when Home Assistant fails # ============================================================================== -declare RESTART_EXIT_CODE 100 -declare SIGNAL_EXIT_CODE 256 -declare SIGTERM 15 +declare RESTART_EXIT_CODE=100 +declare SIGNAL_EXIT_CODE=256 +declare SIGTERM=15 declare APP_EXIT_CODE=${1} -declare SYS_EXIT_CODE=${2+x} +declare SIGNAL_NO=${2} declare NEW_EXIT_CODE= -bashio::log.info "Home Assistant Core finish process exit code ${1}" +bashio::log.info "Home Assistant Core finish process exit code ${APP_EXIT_CODE}" if [[ ${APP_EXIT_CODE} -eq ${RESTART_EXIT_CODE} ]]; then exit 0 elif [[ ${APP_EXIT_CODE} -eq ${SIGNAL_EXIT_CODE} ]]; then - bashio::log.info "Home Assistant Core finish process received signal ${APP_EXIT_CODE}" + bashio::log.info "Home Assistant Core finish process received signal ${SIGNAL_NO}" - NEW_EXIT_CODE=$((128 + SYS_EXIT_CODE)) + NEW_EXIT_CODE=$((128 + SIGNAL_NO)) echo ${NEW_EXIT_CODE} > /run/s6-linux-init-container-results/exitcode - if [[ ${NEW_EXIT_CODE} -eq ${SIGTERM} ]]; then + if [[ ${SIGNAL_NO} -eq ${SIGTERM} ]]; then /run/s6/basedir/bin/halt fi else From b97d346df799d1c16e99dfead628d42cd1fc37ad Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 2 Jun 2022 20:32:31 +0200 Subject: [PATCH 1191/3516] Fix reload of MQTT yaml config (#72901) --- .../components/mqtt/alarm_control_panel.py | 18 +++--- .../components/mqtt/binary_sensor.py | 18 +++--- homeassistant/components/mqtt/button.py | 18 +++--- homeassistant/components/mqtt/camera.py | 18 +++--- homeassistant/components/mqtt/climate.py | 18 +++--- homeassistant/components/mqtt/cover.py | 18 +++--- homeassistant/components/mqtt/fan.py | 18 +++--- homeassistant/components/mqtt/humidifier.py | 21 +++---- .../components/mqtt/light/__init__.py | 18 +++--- homeassistant/components/mqtt/lock.py | 18 +++--- homeassistant/components/mqtt/mixins.py | 58 ++++++++++++++++--- homeassistant/components/mqtt/number.py | 18 +++--- homeassistant/components/mqtt/scene.py | 18 +++--- homeassistant/components/mqtt/select.py | 18 +++--- homeassistant/components/mqtt/sensor.py | 18 +++--- homeassistant/components/mqtt/siren.py | 18 +++--- homeassistant/components/mqtt/switch.py | 18 +++--- .../components/mqtt/vacuum/__init__.py | 18 +++--- tests/components/mqtt/test_binary_sensor.py | 2 +- tests/components/mqtt/test_common.py | 53 ++++++++++++----- tests/components/mqtt/test_sensor.py | 2 +- 21 files changed, 241 insertions(+), 183 deletions(-) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index c20fbb7c657..c0c6f9732d7 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -1,7 +1,6 @@ """This platform enables the possibility to control a MQTT alarm.""" from __future__ import annotations -import asyncio import functools import logging import re @@ -45,8 +44,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -133,7 +132,11 @@ async def async_setup_platform( """Set up MQTT alarm control panel configured under the alarm_control_panel key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, alarm.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + alarm.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -144,13 +147,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT alarm control panel through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, alarm.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, alarm.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 1cb90d6c903..cec065e20f2 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -1,7 +1,6 @@ """Support for MQTT binary sensors.""" from __future__ import annotations -import asyncio from datetime import timedelta import functools import logging @@ -42,8 +41,8 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -88,7 +87,11 @@ async def async_setup_platform( """Set up MQTT binary sensor configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, binary_sensor.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + binary_sensor.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -99,12 +102,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT binary sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, binary_sensor.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, binary_sensor.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index b50856d20c1..afa9900db35 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -1,7 +1,6 @@ """Support for MQTT buttons.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -26,8 +25,8 @@ from .const import ( from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -68,7 +67,11 @@ async def async_setup_platform( """Set up MQTT button configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, button.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + button.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -79,12 +82,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT button through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, button.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, button.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index ae38e07d17a..86db828b111 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -1,7 +1,6 @@ """Camera that loads a picture from an MQTT topic.""" from __future__ import annotations -import asyncio from base64 import b64decode import functools @@ -23,8 +22,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -66,7 +65,11 @@ async def async_setup_platform( """Set up MQTT camera configured under the camera platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, camera.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + camera.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -77,12 +80,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT camera through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, camera.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, camera.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 64b462359be..bdcc82f2c39 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -1,7 +1,6 @@ """Support for MQTT climate devices.""" from __future__ import annotations -import asyncio import functools import logging @@ -51,8 +50,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -377,7 +376,11 @@ async def async_setup_platform( """Set up MQTT climate configured under the fan platform key (deprecated).""" # The use of PLATFORM_SCHEMA is deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, climate.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + climate.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -388,12 +391,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT climate device through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, climate.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, climate.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 5814f3e43f7..325433817c0 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -1,7 +1,6 @@ """Support for MQTT cover devices.""" from __future__ import annotations -import asyncio import functools from json import JSONDecodeError, loads as json_loads import logging @@ -46,8 +45,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -227,7 +226,11 @@ async def async_setup_platform( """Set up MQTT covers configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, cover.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + cover.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -238,13 +241,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT cover through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, cover.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, cover.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index f72b0bdf689..d0b4ff10692 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -1,7 +1,6 @@ """Support for MQTT fans.""" from __future__ import annotations -import asyncio import functools import logging import math @@ -50,8 +49,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -217,7 +216,11 @@ async def async_setup_platform( """Set up MQTT fans configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, fan.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + fan.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -228,13 +231,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, fan.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, fan.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 000a9b9700e..1c9ec5dc201 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -1,7 +1,6 @@ """Support for MQTT humidifiers.""" from __future__ import annotations -import asyncio import functools import logging @@ -46,8 +45,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -173,7 +172,11 @@ async def async_setup_platform( """Set up MQTT humidifier configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, humidifier.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + humidifier.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -184,14 +187,12 @@ async def async_setup_entry( ) -> None: """Set up MQTT humidifier through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, humidifier.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, humidifier.DOMAIN, PLATFORM_SCHEMA_MODERN ) - ) # setup for discovery + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index ab2a3462615..158ea6ffa0d 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -1,7 +1,6 @@ """Support for MQTT lights.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -14,8 +13,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ..mixins import ( - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -97,7 +96,11 @@ async def async_setup_platform( """Set up MQTT light through configuration.yaml (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, light.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + light.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -108,13 +111,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT lights configured under the light platform key (deprecated).""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, light.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, light.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 0cfd1d2b70f..862e76635f7 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -1,7 +1,6 @@ """Support for MQTT locks.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -28,8 +27,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -88,7 +87,11 @@ async def async_setup_platform( """Set up MQTT locks configured under the lock platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, lock.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + lock.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -99,13 +102,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT lock through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, lock.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, lock.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 694fae0b3c0..b0f17cc335b 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -2,6 +2,7 @@ from __future__ import annotations from abc import abstractmethod +import asyncio from collections.abc import Callable import json import logging @@ -27,10 +28,11 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, + discovery, entity_registry as er, ) from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED @@ -46,7 +48,10 @@ from homeassistant.helpers.entity import ( async_generate_entity_id, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.reload import ( + async_integration_yaml_config, + async_setup_reload_service, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import debug_info, subscription @@ -260,8 +265,44 @@ class SetupEntity(Protocol): """Define setup_entities type.""" +async def async_setup_platform_discovery( + hass: HomeAssistant, platform_domain: str, schema: vol.Schema +) -> CALLBACK_TYPE: + """Set up platform discovery for manual config.""" + + async def _async_discover_entities(event: Event | None) -> None: + """Discover entities for a platform.""" + if event: + # The platform has been reloaded + config_yaml = await async_integration_yaml_config(hass, DOMAIN) + if not config_yaml: + return + config_yaml = config_yaml.get(DOMAIN, {}) + else: + config_yaml = hass.data.get(DATA_MQTT_CONFIG, {}) + if not config_yaml: + return + if platform_domain not in config_yaml: + return + await asyncio.gather( + *( + discovery.async_load_platform(hass, platform_domain, DOMAIN, config, {}) + for config in await async_get_platform_config_from_yaml( + hass, platform_domain, schema, config_yaml + ) + ) + ) + + unsub = hass.bus.async_listen("event_mqtt_reloaded", _async_discover_entities) + await _async_discover_entities(None) + return unsub + + async def async_get_platform_config_from_yaml( - hass: HomeAssistant, domain: str, schema: vol.Schema + hass: HomeAssistant, + platform_domain: str, + schema: vol.Schema, + config_yaml: ConfigType = None, ) -> list[ConfigType]: """Return a list of validated configurations for the domain.""" @@ -275,12 +316,15 @@ async def async_get_platform_config_from_yaml( try: validated_config.append(schema(config_item)) except vol.MultipleInvalid as err: - async_log_exception(err, domain, config_item, hass) + async_log_exception(err, platform_domain, config_item, hass) return validated_config - config_yaml: ConfigType = hass.data.get(DATA_MQTT_CONFIG, {}) - if not (platform_configs := config_yaml.get(domain)): + if config_yaml is None: + config_yaml = hass.data.get(DATA_MQTT_CONFIG) + if not config_yaml: + return [] + if not (platform_configs := config_yaml.get(platform_domain)): return [] return async_validate_config(hass, platform_configs) @@ -310,7 +354,7 @@ async def async_setup_entry_helper(hass, domain, async_setup, schema): async def async_setup_platform_helper( hass: HomeAssistant, platform_domain: str, - config: ConfigType, + config: ConfigType | DiscoveryInfoType, async_add_entities: AddEntitiesCallback, async_setup_entities: SetupEntity, ) -> None: diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 6ea1f0959f6..1404dc86a3c 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -1,7 +1,6 @@ """Configure number in a device through MQTT topic.""" from __future__ import annotations -import asyncio import functools import logging @@ -41,8 +40,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -119,7 +118,11 @@ async def async_setup_platform( """Set up MQTT number configured under the number platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, number.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + number.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -130,12 +133,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT number through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, number.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, number.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index ce8f0b0a3e8..9c4a212bd8e 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -1,7 +1,6 @@ """Support for MQTT scenes.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -23,8 +22,8 @@ from .mixins import ( CONF_OBJECT_ID, MQTT_AVAILABILITY_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -65,7 +64,11 @@ async def async_setup_platform( """Set up MQTT scene configured under the scene platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, scene.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + scene.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -76,13 +79,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT scene through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, scene.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, scene.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 75e1b4e8efd..994c11653b7 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -1,7 +1,6 @@ """Configure select in a device through MQTT topic.""" from __future__ import annotations -import asyncio import functools import logging @@ -31,8 +30,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -80,7 +79,11 @@ async def async_setup_platform( """Set up MQTT select configured under the select platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, select.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + select.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -91,12 +94,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT select through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, select.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, select.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 4dd1ad4d95f..f9e0b5151bb 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -1,7 +1,6 @@ """Support for MQTT sensors.""" from __future__ import annotations -import asyncio from datetime import timedelta import functools import logging @@ -42,8 +41,8 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -133,7 +132,11 @@ async def async_setup_platform( """Set up MQTT sensors configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, sensor.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + sensor.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -144,12 +147,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, sensor.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, sensor.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index 1ecf2c37dbf..fef2a4fb3dd 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -1,7 +1,6 @@ """Support for MQTT sirens.""" from __future__ import annotations -import asyncio import copy import functools import json @@ -52,8 +51,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -129,7 +128,11 @@ async def async_setup_platform( """Set up MQTT sirens configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, siren.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + siren.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -140,13 +143,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT siren through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, siren.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, siren.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index c20ddfe5151..be7fc655e1e 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -1,7 +1,6 @@ """Support for MQTT switches.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -38,8 +37,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -83,7 +82,11 @@ async def async_setup_platform( """Set up MQTT switch configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, switch.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + switch.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -94,12 +97,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT switch through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, switch.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, switch.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 34205ab7780..206a15a024a 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -1,7 +1,6 @@ """Support for MQTT vacuums.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -13,8 +12,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ..mixins import ( - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, ) from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE @@ -77,7 +76,11 @@ async def async_setup_platform( """Set up MQTT vacuum through configuration.yaml.""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, vacuum.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + vacuum.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -88,12 +91,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT vacuum through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, vacuum.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, vacuum.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 37bb783d354..ebb1d78138f 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -1011,7 +1011,7 @@ async def test_cleanup_triggers_and_restoring_state( freezer.move_to("2022-02-02 12:01:10+01:00") await help_test_reload_with_config( - hass, caplog, tmp_path, domain, [config1, config2] + hass, caplog, tmp_path, {domain: [config1, config2]} ) assert "Clean up expire after trigger for binary_sensor.test1" in caplog.text assert "Clean up expire after trigger for binary_sensor.test2" not in caplog.text diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 50cf7beb0e0..24482129f3d 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -1651,10 +1651,10 @@ async def help_test_publishing_with_custom_encoding( mqtt_mock.async_publish.reset_mock() -async def help_test_reload_with_config(hass, caplog, tmp_path, domain, config): +async def help_test_reload_with_config(hass, caplog, tmp_path, config): """Test reloading with supplied config.""" new_yaml_config_file = tmp_path / "configuration.yaml" - new_yaml_config = yaml.dump({domain: config}) + new_yaml_config = yaml.dump(config) new_yaml_config_file.write_text(new_yaml_config) assert new_yaml_config_file.read_text() == new_yaml_config @@ -1679,16 +1679,27 @@ async def help_test_reloadable( old_config_1["name"] = "test_old_1" old_config_2 = copy.deepcopy(config) old_config_2["name"] = "test_old_2" + old_config_3 = copy.deepcopy(config) + old_config_3["name"] = "test_old_3" + old_config_3.pop("platform") + old_config_4 = copy.deepcopy(config) + old_config_4["name"] = "test_old_4" + old_config_4.pop("platform") - assert await async_setup_component( - hass, domain, {domain: [old_config_1, old_config_2]} - ) + old_config = { + domain: [old_config_1, old_config_2], + "mqtt": {domain: [old_config_3, old_config_4]}, + } + + assert await async_setup_component(hass, domain, old_config) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() assert hass.states.get(f"{domain}.test_old_1") assert hass.states.get(f"{domain}.test_old_2") - assert len(hass.states.async_all(domain)) == 2 + assert hass.states.get(f"{domain}.test_old_3") + assert hass.states.get(f"{domain}.test_old_4") + assert len(hass.states.async_all(domain)) == 4 # Create temporary fixture for configuration.yaml based on the supplied config and # test a reload with this new config @@ -1698,16 +1709,31 @@ async def help_test_reloadable( new_config_2["name"] = "test_new_2" new_config_3 = copy.deepcopy(config) new_config_3["name"] = "test_new_3" + new_config_3.pop("platform") + new_config_4 = copy.deepcopy(config) + new_config_4["name"] = "test_new_4" + new_config_4.pop("platform") + new_config_5 = copy.deepcopy(config) + new_config_5["name"] = "test_new_5" + new_config_6 = copy.deepcopy(config) + new_config_6["name"] = "test_new_6" + new_config_6.pop("platform") - await help_test_reload_with_config( - hass, caplog, tmp_path, domain, [new_config_1, new_config_2, new_config_3] - ) + new_config = { + domain: [new_config_1, new_config_2, new_config_5], + "mqtt": {domain: [new_config_3, new_config_4, new_config_6]}, + } - assert len(hass.states.async_all(domain)) == 3 + await help_test_reload_with_config(hass, caplog, tmp_path, new_config) + + assert len(hass.states.async_all(domain)) == 6 assert hass.states.get(f"{domain}.test_new_1") assert hass.states.get(f"{domain}.test_new_2") assert hass.states.get(f"{domain}.test_new_3") + assert hass.states.get(f"{domain}.test_new_4") + assert hass.states.get(f"{domain}.test_new_5") + assert hass.states.get(f"{domain}.test_new_6") async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): @@ -1752,9 +1778,10 @@ async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): new_config_3 = copy.deepcopy(config) new_config_3["name"] = "test_new_3" - await help_test_reload_with_config( - hass, caplog, tmp_path, domain, [new_config_1, new_config_2, new_config_3] - ) + new_config = { + domain: [new_config_1, new_config_2, new_config_3], + } + await help_test_reload_with_config(hass, caplog, tmp_path, new_config) assert len(hass.states.async_all(domain)) == 3 diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 894ecc32ecc..7081ae45993 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -1106,7 +1106,7 @@ async def test_cleanup_triggers_and_restoring_state( freezer.move_to("2022-02-02 12:01:10+01:00") await help_test_reload_with_config( - hass, caplog, tmp_path, domain, [config1, config2] + hass, caplog, tmp_path, {domain: [config1, config2]} ) await hass.async_block_till_done() From 9fbde245d01b9465c7a489962b2142f554a5dbf5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Jun 2022 11:54:06 -1000 Subject: [PATCH 1192/3516] Fix performance of logbook entity and devices queries with large MySQL databases (#72898) --- .../components/logbook/queries/common.py | 20 +++++++-- .../components/logbook/queries/devices.py | 32 +++++++++----- .../components/logbook/queries/entities.py | 39 ++++++++++------ .../logbook/queries/entities_and_devices.py | 44 ++++++++++++------- homeassistant/components/recorder/models.py | 2 + 5 files changed, 93 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/logbook/queries/common.py b/homeassistant/components/logbook/queries/common.py index 6049d6beb81..a7a4f84a59e 100644 --- a/homeassistant/components/logbook/queries/common.py +++ b/homeassistant/components/logbook/queries/common.py @@ -12,9 +12,11 @@ from sqlalchemy.sql.selectable import Select from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN from homeassistant.components.recorder.models import ( + EVENTS_CONTEXT_ID_INDEX, OLD_FORMAT_ATTRS_JSON, OLD_STATE, SHARED_ATTRS_JSON, + STATES_CONTEXT_ID_INDEX, EventData, Events, StateAttributes, @@ -121,9 +123,7 @@ def select_events_context_only() -> Select: By marking them as context_only we know they are only for linking context ids and we can avoid processing them. """ - return select(*EVENT_ROWS_NO_STATES, CONTEXT_ONLY).outerjoin( - EventData, (Events.data_id == EventData.data_id) - ) + return select(*EVENT_ROWS_NO_STATES, CONTEXT_ONLY) def select_states_context_only() -> Select: @@ -252,3 +252,17 @@ def _not_uom_attributes_matcher() -> ClauseList: return ~StateAttributes.shared_attrs.like( UNIT_OF_MEASUREMENT_JSON_LIKE ) | ~States.attributes.like(UNIT_OF_MEASUREMENT_JSON_LIKE) + + +def apply_states_context_hints(query: Query) -> Query: + """Force mysql to use the right index on large context_id selects.""" + return query.with_hint( + States, f"FORCE INDEX ({STATES_CONTEXT_ID_INDEX})", dialect_name="mysql" + ) + + +def apply_events_context_hints(query: Query) -> Query: + """Force mysql to use the right index on large context_id selects.""" + return query.with_hint( + Events, f"FORCE INDEX ({EVENTS_CONTEXT_ID_INDEX})", dialect_name="mysql" + ) diff --git a/homeassistant/components/logbook/queries/devices.py b/homeassistant/components/logbook/queries/devices.py index 64a6477017e..88e9f50a42c 100644 --- a/homeassistant/components/logbook/queries/devices.py +++ b/homeassistant/components/logbook/queries/devices.py @@ -4,15 +4,22 @@ from __future__ import annotations from collections.abc import Iterable from datetime import datetime as dt -from sqlalchemy import lambda_stmt, select, union_all +from sqlalchemy import lambda_stmt, select from sqlalchemy.orm import Query from sqlalchemy.sql.elements import ClauseList from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import CTE, CompoundSelect -from homeassistant.components.recorder.models import DEVICE_ID_IN_EVENT, Events, States +from homeassistant.components.recorder.models import ( + DEVICE_ID_IN_EVENT, + EventData, + Events, + States, +) from .common import ( + apply_events_context_hints, + apply_states_context_hints, select_events_context_id_subquery, select_events_context_only, select_events_without_states, @@ -27,13 +34,10 @@ def _select_device_id_context_ids_sub_query( json_quotable_device_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple devices.""" - return select( - union_all( - select_events_context_id_subquery(start_day, end_day, event_types).where( - apply_event_device_id_matchers(json_quotable_device_ids) - ), - ).c.context_id + inner = select_events_context_id_subquery(start_day, end_day, event_types).where( + apply_event_device_id_matchers(json_quotable_device_ids) ) + return select(inner.c.context_id).group_by(inner.c.context_id) def _apply_devices_context_union( @@ -51,8 +55,16 @@ def _apply_devices_context_union( json_quotable_device_ids, ).cte() return query.union_all( - select_events_context_only().where(Events.context_id.in_(devices_cte.select())), - select_states_context_only().where(States.context_id.in_(devices_cte.select())), + apply_events_context_hints( + select_events_context_only() + .select_from(devices_cte) + .outerjoin(Events, devices_cte.c.context_id == Events.context_id) + ).outerjoin(EventData, (Events.data_id == EventData.data_id)), + apply_states_context_hints( + select_states_context_only() + .select_from(devices_cte) + .outerjoin(States, devices_cte.c.context_id == States.context_id) + ), ) diff --git a/homeassistant/components/logbook/queries/entities.py b/homeassistant/components/logbook/queries/entities.py index 4fb211688f3..8de4a5eaf64 100644 --- a/homeassistant/components/logbook/queries/entities.py +++ b/homeassistant/components/logbook/queries/entities.py @@ -14,11 +14,14 @@ from homeassistant.components.recorder.models import ( ENTITY_ID_IN_EVENT, ENTITY_ID_LAST_UPDATED_INDEX, OLD_ENTITY_ID_IN_EVENT, + EventData, Events, States, ) from .common import ( + apply_events_context_hints, + apply_states_context_hints, apply_states_filters, select_events_context_id_subquery, select_events_context_only, @@ -36,16 +39,15 @@ def _select_entities_context_ids_sub_query( json_quotable_entity_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple entities.""" - return select( - union_all( - select_events_context_id_subquery(start_day, end_day, event_types).where( - apply_event_entity_id_matchers(json_quotable_entity_ids) - ), - apply_entities_hints(select(States.context_id)) - .filter((States.last_updated > start_day) & (States.last_updated < end_day)) - .where(States.entity_id.in_(entity_ids)), - ).c.context_id + union = union_all( + select_events_context_id_subquery(start_day, end_day, event_types).where( + apply_event_entity_id_matchers(json_quotable_entity_ids) + ), + apply_entities_hints(select(States.context_id)) + .filter((States.last_updated > start_day) & (States.last_updated < end_day)) + .where(States.entity_id.in_(entity_ids)), ) + return select(union.c.context_id).group_by(union.c.context_id) def _apply_entities_context_union( @@ -64,14 +66,23 @@ def _apply_entities_context_union( entity_ids, json_quotable_entity_ids, ).cte() + # We used to optimize this to exclude rows we already in the union with + # a States.entity_id.not_in(entity_ids) but that made the + # query much slower on MySQL, and since we already filter them away + # in the python code anyways since they will have context_only + # set on them the impact is minimal. return query.union_all( states_query_for_entity_ids(start_day, end_day, entity_ids), - select_events_context_only().where( - Events.context_id.in_(entities_cte.select()) + apply_events_context_hints( + select_events_context_only() + .select_from(entities_cte) + .outerjoin(Events, entities_cte.c.context_id == Events.context_id) + ).outerjoin(EventData, (Events.data_id == EventData.data_id)), + apply_states_context_hints( + select_states_context_only() + .select_from(entities_cte) + .outerjoin(States, entities_cte.c.context_id == States.context_id) ), - select_states_context_only() - .where(States.entity_id.not_in(entity_ids)) - .where(States.context_id.in_(entities_cte.select())), ) diff --git a/homeassistant/components/logbook/queries/entities_and_devices.py b/homeassistant/components/logbook/queries/entities_and_devices.py index d1c86ddbec5..1c4271422b7 100644 --- a/homeassistant/components/logbook/queries/entities_and_devices.py +++ b/homeassistant/components/logbook/queries/entities_and_devices.py @@ -10,9 +10,11 @@ from sqlalchemy.orm import Query from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import CTE, CompoundSelect -from homeassistant.components.recorder.models import Events, States +from homeassistant.components.recorder.models import EventData, Events, States from .common import ( + apply_events_context_hints, + apply_states_context_hints, select_events_context_id_subquery, select_events_context_only, select_events_without_states, @@ -35,18 +37,17 @@ def _select_entities_device_id_context_ids_sub_query( json_quotable_device_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple entities and multiple devices.""" - return select( - union_all( - select_events_context_id_subquery(start_day, end_day, event_types).where( - _apply_event_entity_id_device_id_matchers( - json_quotable_entity_ids, json_quotable_device_ids - ) - ), - apply_entities_hints(select(States.context_id)) - .filter((States.last_updated > start_day) & (States.last_updated < end_day)) - .where(States.entity_id.in_(entity_ids)), - ).c.context_id + union = union_all( + select_events_context_id_subquery(start_day, end_day, event_types).where( + _apply_event_entity_id_device_id_matchers( + json_quotable_entity_ids, json_quotable_device_ids + ) + ), + apply_entities_hints(select(States.context_id)) + .filter((States.last_updated > start_day) & (States.last_updated < end_day)) + .where(States.entity_id.in_(entity_ids)), ) + return select(union.c.context_id).group_by(union.c.context_id) def _apply_entities_devices_context_union( @@ -66,14 +67,23 @@ def _apply_entities_devices_context_union( json_quotable_entity_ids, json_quotable_device_ids, ).cte() + # We used to optimize this to exclude rows we already in the union with + # a States.entity_id.not_in(entity_ids) but that made the + # query much slower on MySQL, and since we already filter them away + # in the python code anyways since they will have context_only + # set on them the impact is minimal. return query.union_all( states_query_for_entity_ids(start_day, end_day, entity_ids), - select_events_context_only().where( - Events.context_id.in_(devices_entities_cte.select()) + apply_events_context_hints( + select_events_context_only() + .select_from(devices_entities_cte) + .outerjoin(Events, devices_entities_cte.c.context_id == Events.context_id) + ).outerjoin(EventData, (Events.data_id == EventData.data_id)), + apply_states_context_hints( + select_states_context_only() + .select_from(devices_entities_cte) + .outerjoin(States, devices_entities_cte.c.context_id == States.context_id) ), - select_states_context_only() - .where(States.entity_id.not_in(entity_ids)) - .where(States.context_id.in_(devices_entities_cte.select())), ) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 70c816c2af5..8db648f15a8 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -93,6 +93,8 @@ TABLES_TO_CHECK = [ LAST_UPDATED_INDEX = "ix_states_last_updated" ENTITY_ID_LAST_UPDATED_INDEX = "ix_states_entity_id_last_updated" +EVENTS_CONTEXT_ID_INDEX = "ix_events_context_id" +STATES_CONTEXT_ID_INDEX = "ix_states_context_id" EMPTY_JSON_OBJECT = "{}" From a4c3585448f582508cfa9b1167a5814c04125ad6 Mon Sep 17 00:00:00 2001 From: Khole Date: Thu, 2 Jun 2022 22:54:26 +0100 Subject: [PATCH 1193/3516] Fix Hive authentication (#72929) --- homeassistant/components/hive/__init__.py | 9 ++------- homeassistant/components/hive/config_flow.py | 1 + homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hive/test_config_flow.py | 15 +++++++++++++++ 6 files changed, 21 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 475bb95eeb1..52cf7f719e6 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -75,14 +75,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Hive from a config entry.""" - websession = aiohttp_client.async_get_clientsession(hass) + web_session = aiohttp_client.async_get_clientsession(hass) hive_config = dict(entry.data) - hive = Hive( - websession, - deviceGroupKey=hive_config["device_data"][0], - deviceKey=hive_config["device_data"][1], - devicePassword=hive_config["device_data"][2], - ) + hive = Hive(web_session) hive_config["options"] = {} hive_config["options"].update( diff --git a/homeassistant/components/hive/config_flow.py b/homeassistant/components/hive/config_flow.py index 9c391f13294..c713a3011f4 100644 --- a/homeassistant/components/hive/config_flow.py +++ b/homeassistant/components/hive/config_flow.py @@ -102,6 +102,7 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): raise UnknownHiveError # Setup the config entry + await self.hive_auth.device_registration("Home Assistant") self.data["tokens"] = self.tokens self.data["device_data"] = await self.hive_auth.getDeviceData() if self.context["source"] == config_entries.SOURCE_REAUTH: diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 472adc137ba..d8cd56abe0b 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,7 +3,7 @@ "name": "Hive", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": ["pyhiveapi==0.5.4"], + "requirements": ["pyhiveapi==0.5.5"], "codeowners": ["@Rendili", "@KJonline"], "iot_class": "cloud_polling", "loggers": ["apyhiveapi"] diff --git a/requirements_all.txt b/requirements_all.txt index 81cdb84632d..3abb93968bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1538,7 +1538,7 @@ pyheos==0.7.2 pyhik==0.3.0 # homeassistant.components.hive -pyhiveapi==0.5.4 +pyhiveapi==0.5.5 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f5b92cc28d9..148ce2191ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1029,7 +1029,7 @@ pyhaversion==22.4.1 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.5.4 +pyhiveapi==0.5.5 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/tests/components/hive/test_config_flow.py b/tests/components/hive/test_config_flow.py index bb567b0bdfc..51ceec43ad2 100644 --- a/tests/components/hive/test_config_flow.py +++ b/tests/components/hive/test_config_flow.py @@ -33,6 +33,9 @@ async def test_import_flow(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.device_registration", + return_value=True, ), patch( "homeassistant.components.hive.config_flow.Auth.getDeviceData", return_value=[ @@ -93,6 +96,9 @@ async def test_user_flow(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.device_registration", + return_value=True, ), patch( "homeassistant.components.hive.config_flow.Auth.getDeviceData", return_value=[ @@ -172,6 +178,9 @@ async def test_user_flow_2fa(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.device_registration", + return_value=True, ), patch( "homeassistant.components.hive.config_flow.Auth.getDeviceData", return_value=[ @@ -256,6 +265,9 @@ async def test_reauth_flow(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.device_registration", + return_value=True, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -361,6 +373,9 @@ async def test_user_flow_2fa_send_new_code(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.device_registration", + return_value=True, ), patch( "homeassistant.components.hive.config_flow.Auth.getDeviceData", return_value=[ From fbb08994f44ba98842b572a578a6b2da74ee1eff Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 2 Jun 2022 16:15:04 -0700 Subject: [PATCH 1194/3516] Only sync when HA is started up as we already sync at startup (#72940) --- homeassistant/components/cloud/google_config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index a0a68aaf84a..81f00b69b23 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -39,7 +39,6 @@ class CloudGoogleConfig(AbstractConfig): self._cur_entity_prefs = self._prefs.google_entity_configs self._cur_default_expose = self._prefs.google_default_expose self._sync_entities_lock = asyncio.Lock() - self._sync_on_started = False @property def enabled(self): @@ -224,7 +223,7 @@ class CloudGoogleConfig(AbstractConfig): self._cur_entity_prefs = prefs.google_entity_configs self._cur_default_expose = prefs.google_default_expose - if sync_entities: + if sync_entities and self.hass.is_running: await self.async_sync_entities_all() @callback From 24e148ab8ea8b497c7b5589f3e323e7df11c395f Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 30 May 2022 14:26:01 +0200 Subject: [PATCH 1195/3516] Cleanup and use new MQTT_BASE_SCHEMA constants (#72283) * Use new MQTT_BASE_SCHEMA constants * Update constants for mqtt_room and manual_mqtt * Revert removing platform key --- .../manual_mqtt/alarm_control_panel.py | 2 +- homeassistant/components/mqtt/__init__.py | 18 ------------------ .../components/mqtt/device_automation.py | 6 ++++-- .../components/mqtt/device_trigger.py | 19 +++++++++++++------ homeassistant/components/mqtt/tag.py | 2 +- homeassistant/components/mqtt_room/sensor.py | 2 +- 6 files changed, 20 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index 730d7ae1f9e..5b74af49a91 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -110,7 +110,7 @@ def _state_schema(state): PLATFORM_SCHEMA = vol.Schema( vol.All( - mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( + mqtt.MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "manual_mqtt", vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 1728dd7f2c7..46eb7052f4f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -261,24 +261,6 @@ SCHEMA_BASE = { MQTT_BASE_SCHEMA = vol.Schema(SCHEMA_BASE) -# Will be removed when all platforms support a modern platform schema -MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE) -# Will be removed when all platforms support a modern platform schema -MQTT_RO_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - } -) -# Will be removed when all platforms support a modern platform schema -MQTT_RW_PLATFORM_SCHEMA = MQTT_BASE_PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, - } -) - # Sensor type platforms subscribe to MQTT events MQTT_RO_SCHEMA = MQTT_BASE_SCHEMA.extend( { diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py index cafbd66b098..002ae6e3991 100644 --- a/homeassistant/components/mqtt/device_automation.py +++ b/homeassistant/components/mqtt/device_automation.py @@ -3,6 +3,8 @@ import functools import voluptuous as vol +import homeassistant.helpers.config_validation as cv + from . import device_trigger from .. import mqtt from .mixins import async_setup_entry_helper @@ -12,10 +14,10 @@ AUTOMATION_TYPES = [AUTOMATION_TYPE_TRIGGER] AUTOMATION_TYPES_SCHEMA = vol.In(AUTOMATION_TYPES) CONF_AUTOMATION_TYPE = "automation_type" -PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( {vol.Required(CONF_AUTOMATION_TYPE): AUTOMATION_TYPES_SCHEMA}, extra=vol.ALLOW_EXTRA, -) +).extend(mqtt.MQTT_BASE_SCHEMA.schema) async def async_setup_entry(hass, config_entry): diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 42ffcee1644..2c6c6ecc3ba 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -30,7 +30,14 @@ from homeassistant.helpers.typing import ConfigType from . import debug_info, trigger as mqtt_trigger from .. import mqtt -from .const import ATTR_DISCOVERY_HASH, CONF_PAYLOAD, CONF_QOS, CONF_TOPIC, DOMAIN +from .const import ( + ATTR_DISCOVERY_HASH, + CONF_ENCODING, + CONF_PAYLOAD, + CONF_QOS, + CONF_TOPIC, + DOMAIN, +) from .discovery import MQTT_DISCOVERY_DONE from .mixins import ( MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -64,7 +71,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( } ) -TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( +TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_AUTOMATION_TYPE): str, vol.Required(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -94,10 +101,10 @@ class TriggerInstance: async def async_attach_trigger(self) -> None: """Attach MQTT trigger.""" mqtt_config = { - mqtt_trigger.CONF_PLATFORM: mqtt.DOMAIN, - mqtt_trigger.CONF_TOPIC: self.trigger.topic, - mqtt_trigger.CONF_ENCODING: DEFAULT_ENCODING, - mqtt_trigger.CONF_QOS: self.trigger.qos, + CONF_PLATFORM: mqtt.DOMAIN, + CONF_TOPIC: self.trigger.topic, + CONF_ENCODING: DEFAULT_ENCODING, + CONF_QOS: self.trigger.qos, } if self.trigger.payload: mqtt_config[CONF_PAYLOAD] = self.trigger.payload diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 5bfbbd73bce..25e49524b8f 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -30,7 +30,7 @@ LOG_NAME = "Tag" TAG = "tag" TAGS = "mqtt_tags" -PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = mqtt.MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_PLATFORM): "mqtt", diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index 8f7455eb998..54de561c11e 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_AWAY_TIMEOUT, default=DEFAULT_AWAY_TIMEOUT): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, } -).extend(mqtt.MQTT_RO_PLATFORM_SCHEMA.schema) +).extend(mqtt.MQTT_RO_SCHEMA.schema) MQTT_PAYLOAD = vol.Schema( vol.All( From 9295cc4df9633ef2b5c8dde79ea5a1cdeab24357 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 31 May 2022 09:32:44 +0200 Subject: [PATCH 1196/3516] Move MQTT config schemas and client to separate modules (#71995) * Move MQTT config schemas and client to separate modules * Update integrations depending on MQTT --- .../manual_mqtt/alarm_control_panel.py | 2 +- homeassistant/components/mqtt/__init__.py | 958 +----------------- .../components/mqtt/alarm_control_panel.py | 14 +- .../components/mqtt/binary_sensor.py | 7 +- homeassistant/components/mqtt/button.py | 11 +- homeassistant/components/mqtt/camera.py | 7 +- homeassistant/components/mqtt/client.py | 659 ++++++++++++ homeassistant/components/mqtt/climate.py | 58 +- homeassistant/components/mqtt/config.py | 148 +++ homeassistant/components/mqtt/config_flow.py | 2 +- homeassistant/components/mqtt/const.py | 26 +- homeassistant/components/mqtt/cover.py | 20 +- .../components/mqtt/device_automation.py | 4 +- .../mqtt/device_tracker/schema_discovery.py | 7 +- .../mqtt/device_tracker/schema_yaml.py | 10 +- .../components/mqtt/device_trigger.py | 6 +- homeassistant/components/mqtt/fan.py | 24 +- homeassistant/components/mqtt/humidifier.py | 18 +- .../components/mqtt/light/schema_basic.py | 48 +- .../components/mqtt/light/schema_json.py | 11 +- .../components/mqtt/light/schema_template.py | 7 +- homeassistant/components/mqtt/lock.py | 7 +- homeassistant/components/mqtt/mixins.py | 14 +- homeassistant/components/mqtt/models.py | 126 ++- homeassistant/components/mqtt/number.py | 7 +- homeassistant/components/mqtt/scene.py | 10 +- homeassistant/components/mqtt/select.py | 7 +- homeassistant/components/mqtt/sensor.py | 10 +- homeassistant/components/mqtt/siren.py | 7 +- homeassistant/components/mqtt/switch.py | 7 +- homeassistant/components/mqtt/tag.py | 8 +- .../components/mqtt/vacuum/schema_legacy.py | 28 +- .../components/mqtt/vacuum/schema_state.py | 15 +- .../components/mqtt_json/device_tracker.py | 2 +- homeassistant/components/mqtt_room/sensor.py | 2 +- tests/components/mqtt/test_cover.py | 2 +- tests/components/mqtt/test_init.py | 18 +- tests/components/mqtt/test_legacy_vacuum.py | 2 +- tests/components/mqtt/test_state_vacuum.py | 2 +- 39 files changed, 1213 insertions(+), 1108 deletions(-) create mode 100644 homeassistant/components/mqtt/client.py create mode 100644 homeassistant/components/mqtt/config.py diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index 5b74af49a91..67675a44e22 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -110,7 +110,7 @@ def _state_schema(state): PLATFORM_SCHEMA = vol.Schema( vol.All( - mqtt.MQTT_BASE_SCHEMA.extend( + mqtt.config.MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "manual_mqtt", vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string, diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 46eb7052f4f..e21885d2585 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1,23 +1,13 @@ """Support for MQTT message handling.""" from __future__ import annotations -from ast import literal_eval import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Callable from dataclasses import dataclass import datetime as dt -from functools import lru_cache, partial, wraps -import inspect -from itertools import groupby import logging -from operator import attrgetter -import ssl -import time -from typing import TYPE_CHECKING, Any, Union, cast -import uuid +from typing import Any, cast -import attr -import certifi import jinja2 import voluptuous as vol @@ -25,120 +15,73 @@ from homeassistant import config_entries from homeassistant.components import websocket_api from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_NAME, - CONF_CLIENT_ID, CONF_DISCOVERY, CONF_PASSWORD, CONF_PAYLOAD, CONF_PORT, - CONF_PROTOCOL, CONF_USERNAME, - CONF_VALUE_TEMPLATE, - EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, - Platform, -) -from homeassistant.core import ( - CoreState, - Event, - HassJob, - HomeAssistant, - ServiceCall, - callback, ) +from homeassistant.core import Event, HassJob, HomeAssistant, ServiceCall, callback from homeassistant.data_entry_flow import BaseServiceInfo -from homeassistant.exceptions import HomeAssistantError, TemplateError, Unauthorized +from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.device_registry import DeviceEntry -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from homeassistant.loader import bind_hass -from homeassistant.util import dt as dt_util -from homeassistant.util.async_ import run_callback_threadsafe -from homeassistant.util.logging import catch_log_exception +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import ConfigType # Loading the config flow file will register the flow from . import debug_info, discovery -from .const import ( +from .client import ( # noqa: F401 + MQTT, + async_publish, + async_subscribe, + publish, + subscribe, +) +from .config import CONFIG_SCHEMA_BASE, DEFAULT_VALUES, DEPRECATED_CONFIG_KEYS +from .const import ( # noqa: F401 ATTR_PAYLOAD, ATTR_QOS, ATTR_RETAIN, ATTR_TOPIC, CONF_BIRTH_MESSAGE, CONF_BROKER, - CONF_CERTIFICATE, - CONF_CLIENT_CERT, - CONF_CLIENT_KEY, CONF_COMMAND_TOPIC, - CONF_ENCODING, + CONF_DISCOVERY_PREFIX, CONF_QOS, - CONF_RETAIN, CONF_STATE_TOPIC, - CONF_TLS_INSECURE, CONF_TLS_VERSION, CONF_TOPIC, CONF_WILL_MESSAGE, CONFIG_ENTRY_IS_SETUP, DATA_CONFIG_ENTRY_LOCK, + DATA_MQTT, DATA_MQTT_CONFIG, DATA_MQTT_RELOAD_NEEDED, - DEFAULT_BIRTH, - DEFAULT_DISCOVERY, DEFAULT_ENCODING, - DEFAULT_PREFIX, DEFAULT_QOS, DEFAULT_RETAIN, - DEFAULT_WILL, DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, - PROTOCOL_31, - PROTOCOL_311, + PLATFORMS, ) -from .discovery import LAST_DISCOVERY -from .models import ( - AsyncMessageCallbackType, - MessageCallbackType, - PublishMessage, +from .models import ( # noqa: F401 + MqttCommandTemplate, + MqttValueTemplate, PublishPayloadType, ReceiveMessage, ReceivePayloadType, ) from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic -if TYPE_CHECKING: - # Only import for paho-mqtt type checking here, imports are done locally - # because integrations should be able to optionally rely on MQTT. - import paho.mqtt.client as mqtt - _LOGGER = logging.getLogger(__name__) -_SENTINEL = object() - -DATA_MQTT = "mqtt" - SERVICE_PUBLISH = "publish" SERVICE_DUMP = "dump" -CONF_DISCOVERY_PREFIX = "discovery_prefix" -CONF_KEEPALIVE = "keepalive" - -DEFAULT_PORT = 1883 -DEFAULT_KEEPALIVE = 60 -DEFAULT_PROTOCOL = PROTOCOL_311 -DEFAULT_TLS_PROTOCOL = "auto" - -DEFAULT_VALUES = { - CONF_BIRTH_MESSAGE: DEFAULT_BIRTH, - CONF_DISCOVERY: DEFAULT_DISCOVERY, - CONF_PORT: DEFAULT_PORT, - CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL, - CONF_WILL_MESSAGE: DEFAULT_WILL, -} - MANDATORY_DEFAULT_VALUES = (CONF_PORT,) ATTR_TOPIC_TEMPLATE = "topic_template" @@ -150,93 +93,6 @@ CONNECTION_SUCCESS = "connection_success" CONNECTION_FAILED = "connection_failed" CONNECTION_FAILED_RECOVERABLE = "connection_failed_recoverable" -DISCOVERY_COOLDOWN = 2 -TIMEOUT_ACK = 10 - -PLATFORMS = [ - Platform.ALARM_CONTROL_PANEL, - Platform.BINARY_SENSOR, - Platform.BUTTON, - Platform.CAMERA, - Platform.CLIMATE, - Platform.DEVICE_TRACKER, - Platform.COVER, - Platform.FAN, - Platform.HUMIDIFIER, - Platform.LIGHT, - Platform.LOCK, - Platform.NUMBER, - Platform.SELECT, - Platform.SCENE, - Platform.SENSOR, - Platform.SIREN, - Platform.SWITCH, - Platform.VACUUM, -] - -CLIENT_KEY_AUTH_MSG = ( - "client_key and client_cert must both be present in " - "the MQTT broker configuration" -) - -MQTT_WILL_BIRTH_SCHEMA = vol.Schema( - { - vol.Inclusive(ATTR_TOPIC, "topic_payload"): valid_publish_topic, - vol.Inclusive(ATTR_PAYLOAD, "topic_payload"): cv.string, - vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, - vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - }, - required=True, -) - -PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( - {vol.Optional(platform.value): cv.ensure_list for platform in PLATFORMS} -) - -CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend( - { - vol.Optional(CONF_CLIENT_ID): cv.string, - vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( - vol.Coerce(int), vol.Range(min=15) - ), - vol.Optional(CONF_BROKER): cv.string, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), - vol.Inclusive( - CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG - ): cv.isfile, - vol.Inclusive( - CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG - ): cv.isfile, - vol.Optional(CONF_TLS_INSECURE): cv.boolean, - vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"), - vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( - cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) - ), - vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_DISCOVERY): cv.boolean, - # discovery_prefix must be a valid publish topic because if no - # state topic is specified, it will be created with the given prefix. - vol.Optional( - CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX - ): valid_publish_topic, - } -) - -DEPRECATED_CONFIG_KEYS = [ - CONF_BIRTH_MESSAGE, - CONF_BROKER, - CONF_DISCOVERY, - CONF_PASSWORD, - CONF_PORT, - CONF_TLS_VERSION, - CONF_USERNAME, - CONF_WILL_MESSAGE, -] - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( @@ -254,29 +110,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SCHEMA_BASE = { - vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, - vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, -} - -MQTT_BASE_SCHEMA = vol.Schema(SCHEMA_BASE) - -# Sensor type platforms subscribe to MQTT events -MQTT_RO_SCHEMA = MQTT_BASE_SCHEMA.extend( - { - vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - } -) - -# Switch type platforms publish to MQTT and may subscribe -MQTT_RW_SCHEMA = MQTT_BASE_SCHEMA.extend( - { - vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, - } -) # Service call validation schema MQTT_PUBLISH_SCHEMA = vol.All( @@ -295,124 +128,6 @@ MQTT_PUBLISH_SCHEMA = vol.All( ) -SubscribePayloadType = Union[str, bytes] # Only bytes if encoding is None - - -class MqttCommandTemplate: - """Class for rendering MQTT payload with command templates.""" - - def __init__( - self, - command_template: template.Template | None, - *, - hass: HomeAssistant | None = None, - entity: Entity | None = None, - ) -> None: - """Instantiate a command template.""" - self._attr_command_template = command_template - if command_template is None: - return - - self._entity = entity - - command_template.hass = hass - - if entity: - command_template.hass = entity.hass - - @callback - def async_render( - self, - value: PublishPayloadType = None, - variables: TemplateVarsType = None, - ) -> PublishPayloadType: - """Render or convert the command template with given value or variables.""" - - def _convert_outgoing_payload( - payload: PublishPayloadType, - ) -> PublishPayloadType: - """Ensure correct raw MQTT payload is passed as bytes for publishing.""" - if isinstance(payload, str): - try: - native_object = literal_eval(payload) - if isinstance(native_object, bytes): - return native_object - - except (ValueError, TypeError, SyntaxError, MemoryError): - pass - - return payload - - if self._attr_command_template is None: - return value - - values = {"value": value} - if self._entity: - values[ATTR_ENTITY_ID] = self._entity.entity_id - values[ATTR_NAME] = self._entity.name - if variables is not None: - values.update(variables) - return _convert_outgoing_payload( - self._attr_command_template.async_render(values, parse_result=False) - ) - - -class MqttValueTemplate: - """Class for rendering MQTT value template with possible json values.""" - - def __init__( - self, - value_template: template.Template | None, - *, - hass: HomeAssistant | None = None, - entity: Entity | None = None, - config_attributes: TemplateVarsType = None, - ) -> None: - """Instantiate a value template.""" - self._value_template = value_template - self._config_attributes = config_attributes - if value_template is None: - return - - value_template.hass = hass - self._entity = entity - - if entity: - value_template.hass = entity.hass - - @callback - def async_render_with_possible_json_value( - self, - payload: ReceivePayloadType, - default: ReceivePayloadType | object = _SENTINEL, - variables: TemplateVarsType = None, - ) -> ReceivePayloadType: - """Render with possible json value or pass-though a received MQTT value.""" - if self._value_template is None: - return payload - - values: dict[str, Any] = {} - - if variables is not None: - values.update(variables) - - if self._config_attributes is not None: - values.update(self._config_attributes) - - if self._entity: - values[ATTR_ENTITY_ID] = self._entity.entity_id - values[ATTR_NAME] = self._entity.name - - if default == _SENTINEL: - return self._value_template.async_render_with_possible_json_value( - payload, variables=values - ) - - return self._value_template.async_render_with_possible_json_value( - payload, default, variables=values - ) - - @dataclass class MqttServiceInfo(BaseServiceInfo): """Prepared info from mqtt entries.""" @@ -425,163 +140,6 @@ class MqttServiceInfo(BaseServiceInfo): timestamp: dt.datetime -def publish( - hass: HomeAssistant, - topic: str, - payload: PublishPayloadType, - qos: int | None = 0, - retain: bool | None = False, - encoding: str | None = DEFAULT_ENCODING, -) -> None: - """Publish message to a MQTT topic.""" - hass.add_job(async_publish, hass, topic, payload, qos, retain, encoding) - - -async def async_publish( - hass: HomeAssistant, - topic: str, - payload: PublishPayloadType, - qos: int | None = 0, - retain: bool | None = False, - encoding: str | None = DEFAULT_ENCODING, -) -> None: - """Publish message to a MQTT topic.""" - - outgoing_payload = payload - if not isinstance(payload, bytes): - if not encoding: - _LOGGER.error( - "Can't pass-through payload for publishing %s on %s with no encoding set, need 'bytes' got %s", - payload, - topic, - type(payload), - ) - return - outgoing_payload = str(payload) - if encoding != DEFAULT_ENCODING: - # a string is encoded as utf-8 by default, other encoding requires bytes as payload - try: - outgoing_payload = outgoing_payload.encode(encoding) - except (AttributeError, LookupError, UnicodeEncodeError): - _LOGGER.error( - "Can't encode payload for publishing %s on %s with encoding %s", - payload, - topic, - encoding, - ) - return - - await hass.data[DATA_MQTT].async_publish(topic, outgoing_payload, qos, retain) - - -AsyncDeprecatedMessageCallbackType = Callable[ - [str, ReceivePayloadType, int], Awaitable[None] -] -DeprecatedMessageCallbackType = Callable[[str, ReceivePayloadType, int], None] - - -def wrap_msg_callback( - msg_callback: AsyncDeprecatedMessageCallbackType | DeprecatedMessageCallbackType, -) -> AsyncMessageCallbackType | MessageCallbackType: - """Wrap an MQTT message callback to support deprecated signature.""" - # Check for partials to properly determine if coroutine function - check_func = msg_callback - while isinstance(check_func, partial): - check_func = check_func.func - - wrapper_func: AsyncMessageCallbackType | MessageCallbackType - if asyncio.iscoroutinefunction(check_func): - - @wraps(msg_callback) - async def async_wrapper(msg: ReceiveMessage) -> None: - """Call with deprecated signature.""" - await cast(AsyncDeprecatedMessageCallbackType, msg_callback)( - msg.topic, msg.payload, msg.qos - ) - - wrapper_func = async_wrapper - else: - - @wraps(msg_callback) - def wrapper(msg: ReceiveMessage) -> None: - """Call with deprecated signature.""" - msg_callback(msg.topic, msg.payload, msg.qos) - - wrapper_func = wrapper - return wrapper_func - - -@bind_hass -async def async_subscribe( - hass: HomeAssistant, - topic: str, - msg_callback: AsyncMessageCallbackType - | MessageCallbackType - | DeprecatedMessageCallbackType - | AsyncDeprecatedMessageCallbackType, - qos: int = DEFAULT_QOS, - encoding: str | None = "utf-8", -): - """Subscribe to an MQTT topic. - - Call the return value to unsubscribe. - """ - # Count callback parameters which don't have a default value - non_default = 0 - if msg_callback: - non_default = sum( - p.default == inspect.Parameter.empty - for _, p in inspect.signature(msg_callback).parameters.items() - ) - - wrapped_msg_callback = msg_callback - # If we have 3 parameters with no default value, wrap the callback - if non_default == 3: - module = inspect.getmodule(msg_callback) - _LOGGER.warning( - "Signature of MQTT msg_callback '%s.%s' is deprecated", - module.__name__ if module else "", - msg_callback.__name__, - ) - wrapped_msg_callback = wrap_msg_callback( - cast(DeprecatedMessageCallbackType, msg_callback) - ) - - async_remove = await hass.data[DATA_MQTT].async_subscribe( - topic, - catch_log_exception( - wrapped_msg_callback, - lambda msg: ( - f"Exception in {msg_callback.__name__} when handling msg on " - f"'{msg.topic}': '{msg.payload}'" - ), - ), - qos, - encoding, - ) - return async_remove - - -@bind_hass -def subscribe( - hass: HomeAssistant, - topic: str, - msg_callback: MessageCallbackType, - qos: int = DEFAULT_QOS, - encoding: str = "utf-8", -) -> Callable[[], None]: - """Subscribe to an MQTT topic.""" - async_remove = asyncio.run_coroutine_threadsafe( - async_subscribe(hass, topic, msg_callback, qos, encoding), hass.loop - ).result() - - def remove(): - """Remove listener convert.""" - run_callback_threadsafe(hass.loop, async_remove).result() - - return remove - - async def _async_setup_discovery( hass: HomeAssistant, conf: ConfigType, config_entry ) -> None: @@ -649,6 +207,26 @@ def _merge_extended_config(entry, conf): return {**conf, **entry.data} +async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle signals of config entry being updated. + + Causes for this is config entry options changing. + """ + mqtt_client = hass.data[DATA_MQTT] + + if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: + conf = CONFIG_SCHEMA_BASE(dict(entry.data)) + + mqtt_client.conf = _merge_extended_config(entry, conf) + await mqtt_client.async_disconnect() + mqtt_client.init_client() + await mqtt_client.async_connect() + + await discovery.async_stop(hass) + if mqtt_client.conf.get(CONF_DISCOVERY): + await _async_setup_discovery(hass, mqtt_client.conf, entry) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load a config entry.""" # Merge basic configuration, and add missing defaults for basic options @@ -685,6 +263,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, conf, ) + entry.add_update_listener(_async_config_entry_updated) await hass.data[DATA_MQTT].async_connect() @@ -813,459 +392,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -@attr.s(slots=True, frozen=True) -class Subscription: - """Class to hold data about an active subscription.""" - - topic: str = attr.ib() - matcher: Any = attr.ib() - job: HassJob = attr.ib() - qos: int = attr.ib(default=0) - encoding: str | None = attr.ib(default="utf-8") - - -class MqttClientSetup: - """Helper class to setup the paho mqtt client from config.""" - - def __init__(self, config: ConfigType) -> None: - """Initialize the MQTT client setup helper.""" - - # We don't import on the top because some integrations - # should be able to optionally rely on MQTT. - import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel - - if config[CONF_PROTOCOL] == PROTOCOL_31: - proto = mqtt.MQTTv31 - else: - proto = mqtt.MQTTv311 - - if (client_id := config.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) - self._client = mqtt.Client(client_id, protocol=proto) - - # Enable logging - self._client.enable_logger() - - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - if username is not None: - self._client.username_pw_set(username, password) - - if (certificate := config.get(CONF_CERTIFICATE)) == "auto": - certificate = certifi.where() - - client_key = config.get(CONF_CLIENT_KEY) - client_cert = config.get(CONF_CLIENT_CERT) - tls_insecure = config.get(CONF_TLS_INSECURE) - if certificate is not None: - self._client.tls_set( - certificate, - certfile=client_cert, - keyfile=client_key, - tls_version=ssl.PROTOCOL_TLS, - ) - - if tls_insecure is not None: - self._client.tls_insecure_set(tls_insecure) - - @property - def client(self) -> mqtt.Client: - """Return the paho MQTT client.""" - return self._client - - -class MQTT: - """Home Assistant MQTT client.""" - - def __init__( - self, - hass: HomeAssistant, - config_entry, - conf, - ) -> None: - """Initialize Home Assistant MQTT client.""" - # We don't import on the top because some integrations - # should be able to optionally rely on MQTT. - import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel - - self.hass = hass - self.config_entry = config_entry - self.conf = conf - self.subscriptions: list[Subscription] = [] - self.connected = False - self._ha_started = asyncio.Event() - self._last_subscribe = time.time() - self._mqttc: mqtt.Client = None - self._paho_lock = asyncio.Lock() - - self._pending_operations: dict[str, asyncio.Event] = {} - - if self.hass.state == CoreState.running: - self._ha_started.set() - else: - - @callback - def ha_started(_): - self._ha_started.set() - - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, ha_started) - - self.init_client() - self.config_entry.add_update_listener(self.async_config_entry_updated) - - @staticmethod - async def async_config_entry_updated( - hass: HomeAssistant, entry: ConfigEntry - ) -> None: - """Handle signals of config entry being updated. - - This is a static method because a class method (bound method), can not be used with weak references. - Causes for this is config entry options changing. - """ - self = hass.data[DATA_MQTT] - - if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None: - conf = CONFIG_SCHEMA_BASE(dict(entry.data)) - - self.conf = _merge_extended_config(entry, conf) - await self.async_disconnect() - self.init_client() - await self.async_connect() - - await discovery.async_stop(hass) - if self.conf.get(CONF_DISCOVERY): - await _async_setup_discovery(hass, self.conf, entry) - - def init_client(self): - """Initialize paho client.""" - self._mqttc = MqttClientSetup(self.conf).client - self._mqttc.on_connect = self._mqtt_on_connect - self._mqttc.on_disconnect = self._mqtt_on_disconnect - self._mqttc.on_message = self._mqtt_on_message - self._mqttc.on_publish = self._mqtt_on_callback - self._mqttc.on_subscribe = self._mqtt_on_callback - self._mqttc.on_unsubscribe = self._mqtt_on_callback - - if ( - CONF_WILL_MESSAGE in self.conf - and ATTR_TOPIC in self.conf[CONF_WILL_MESSAGE] - ): - will_message = PublishMessage(**self.conf[CONF_WILL_MESSAGE]) - else: - will_message = None - - if will_message is not None: - self._mqttc.will_set( - topic=will_message.topic, - payload=will_message.payload, - qos=will_message.qos, - retain=will_message.retain, - ) - - async def async_publish( - self, topic: str, payload: PublishPayloadType, qos: int, retain: bool - ) -> None: - """Publish a MQTT message.""" - async with self._paho_lock: - msg_info = await self.hass.async_add_executor_job( - self._mqttc.publish, topic, payload, qos, retain - ) - _LOGGER.debug( - "Transmitting message on %s: '%s', mid: %s", - topic, - payload, - msg_info.mid, - ) - _raise_on_error(msg_info.rc) - await self._wait_for_mid(msg_info.mid) - - async def async_connect(self) -> None: - """Connect to the host. Does not process messages yet.""" - # pylint: disable-next=import-outside-toplevel - import paho.mqtt.client as mqtt - - result: int | None = None - try: - result = await self.hass.async_add_executor_job( - self._mqttc.connect, - self.conf[CONF_BROKER], - self.conf[CONF_PORT], - self.conf[CONF_KEEPALIVE], - ) - except OSError as err: - _LOGGER.error("Failed to connect to MQTT server due to exception: %s", err) - - if result is not None and result != 0: - _LOGGER.error( - "Failed to connect to MQTT server: %s", mqtt.error_string(result) - ) - - self._mqttc.loop_start() - - async def async_disconnect(self): - """Stop the MQTT client.""" - - def stop(): - """Stop the MQTT client.""" - # Do not disconnect, we want the broker to always publish will - self._mqttc.loop_stop() - - await self.hass.async_add_executor_job(stop) - - async def async_subscribe( - self, - topic: str, - msg_callback: MessageCallbackType, - qos: int, - encoding: str | None = None, - ) -> Callable[[], None]: - """Set up a subscription to a topic with the provided qos. - - This method is a coroutine. - """ - if not isinstance(topic, str): - raise HomeAssistantError("Topic needs to be a string!") - - subscription = Subscription( - topic, _matcher_for_topic(topic), HassJob(msg_callback), qos, encoding - ) - self.subscriptions.append(subscription) - self._matching_subscriptions.cache_clear() - - # Only subscribe if currently connected. - if self.connected: - self._last_subscribe = time.time() - await self._async_perform_subscription(topic, qos) - - @callback - def async_remove() -> None: - """Remove subscription.""" - if subscription not in self.subscriptions: - raise HomeAssistantError("Can't remove subscription twice") - self.subscriptions.remove(subscription) - self._matching_subscriptions.cache_clear() - - # Only unsubscribe if currently connected. - if self.connected: - self.hass.async_create_task(self._async_unsubscribe(topic)) - - return async_remove - - async def _async_unsubscribe(self, topic: str) -> None: - """Unsubscribe from a topic. - - This method is a coroutine. - """ - if any(other.topic == topic for other in self.subscriptions): - # Other subscriptions on topic remaining - don't unsubscribe. - return - - async with self._paho_lock: - result: int | None = None - result, mid = await self.hass.async_add_executor_job( - self._mqttc.unsubscribe, topic - ) - _LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid) - _raise_on_error(result) - await self._wait_for_mid(mid) - - async def _async_perform_subscription(self, topic: str, qos: int) -> None: - """Perform a paho-mqtt subscription.""" - async with self._paho_lock: - result: int | None = None - result, mid = await self.hass.async_add_executor_job( - self._mqttc.subscribe, topic, qos - ) - _LOGGER.debug("Subscribing to %s, mid: %s", topic, mid) - _raise_on_error(result) - await self._wait_for_mid(mid) - - def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code: int) -> None: - """On connect callback. - - Resubscribe to all topics we were subscribed to and publish birth - message. - """ - # pylint: disable-next=import-outside-toplevel - import paho.mqtt.client as mqtt - - if result_code != mqtt.CONNACK_ACCEPTED: - _LOGGER.error( - "Unable to connect to the MQTT broker: %s", - mqtt.connack_string(result_code), - ) - return - - self.connected = True - dispatcher_send(self.hass, MQTT_CONNECTED) - _LOGGER.info( - "Connected to MQTT server %s:%s (%s)", - self.conf[CONF_BROKER], - self.conf[CONF_PORT], - result_code, - ) - - # Group subscriptions to only re-subscribe once for each topic. - keyfunc = attrgetter("topic") - for topic, subs in groupby(sorted(self.subscriptions, key=keyfunc), keyfunc): - # Re-subscribe with the highest requested qos - max_qos = max(subscription.qos for subscription in subs) - self.hass.add_job(self._async_perform_subscription, topic, max_qos) - - if ( - CONF_BIRTH_MESSAGE in self.conf - and ATTR_TOPIC in self.conf[CONF_BIRTH_MESSAGE] - ): - - async def publish_birth_message(birth_message): - await self._ha_started.wait() # Wait for Home Assistant to start - await self._discovery_cooldown() # Wait for MQTT discovery to cool down - await self.async_publish( - topic=birth_message.topic, - payload=birth_message.payload, - qos=birth_message.qos, - retain=birth_message.retain, - ) - - birth_message = PublishMessage(**self.conf[CONF_BIRTH_MESSAGE]) - asyncio.run_coroutine_threadsafe( - publish_birth_message(birth_message), self.hass.loop - ) - - def _mqtt_on_message(self, _mqttc, _userdata, msg) -> None: - """Message received callback.""" - self.hass.add_job(self._mqtt_handle_message, msg) - - @lru_cache(2048) - def _matching_subscriptions(self, topic): - subscriptions = [] - for subscription in self.subscriptions: - if subscription.matcher(topic): - subscriptions.append(subscription) - return subscriptions - - @callback - def _mqtt_handle_message(self, msg) -> None: - _LOGGER.debug( - "Received message on %s%s: %s", - msg.topic, - " (retained)" if msg.retain else "", - msg.payload[0:8192], - ) - timestamp = dt_util.utcnow() - - subscriptions = self._matching_subscriptions(msg.topic) - - for subscription in subscriptions: - - payload: SubscribePayloadType = msg.payload - if subscription.encoding is not None: - try: - payload = msg.payload.decode(subscription.encoding) - except (AttributeError, UnicodeDecodeError): - _LOGGER.warning( - "Can't decode payload %s on %s with encoding %s (for %s)", - msg.payload[0:8192], - msg.topic, - subscription.encoding, - subscription.job, - ) - continue - - self.hass.async_run_hass_job( - subscription.job, - ReceiveMessage( - msg.topic, - payload, - msg.qos, - msg.retain, - subscription.topic, - timestamp, - ), - ) - - def _mqtt_on_callback(self, _mqttc, _userdata, mid, _granted_qos=None) -> None: - """Publish / Subscribe / Unsubscribe callback.""" - self.hass.add_job(self._mqtt_handle_mid, mid) - - @callback - def _mqtt_handle_mid(self, mid) -> None: - # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid - # may be executed first. - if mid not in self._pending_operations: - self._pending_operations[mid] = asyncio.Event() - self._pending_operations[mid].set() - - def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None: - """Disconnected callback.""" - self.connected = False - dispatcher_send(self.hass, MQTT_DISCONNECTED) - _LOGGER.warning( - "Disconnected from MQTT server %s:%s (%s)", - self.conf[CONF_BROKER], - self.conf[CONF_PORT], - result_code, - ) - - async def _wait_for_mid(self, mid): - """Wait for ACK from broker.""" - # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid - # may be executed first. - if mid not in self._pending_operations: - self._pending_operations[mid] = asyncio.Event() - try: - await asyncio.wait_for(self._pending_operations[mid].wait(), TIMEOUT_ACK) - except asyncio.TimeoutError: - _LOGGER.warning( - "No ACK from MQTT server in %s seconds (mid: %s)", TIMEOUT_ACK, mid - ) - finally: - del self._pending_operations[mid] - - async def _discovery_cooldown(self): - now = time.time() - # Reset discovery and subscribe cooldowns - self.hass.data[LAST_DISCOVERY] = now - self._last_subscribe = now - - last_discovery = self.hass.data[LAST_DISCOVERY] - last_subscribe = self._last_subscribe - wait_until = max( - last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN - ) - while now < wait_until: - await asyncio.sleep(wait_until - now) - now = time.time() - last_discovery = self.hass.data[LAST_DISCOVERY] - last_subscribe = self._last_subscribe - wait_until = max( - last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN - ) - - -def _raise_on_error(result_code: int | None) -> None: - """Raise error if error result.""" - # pylint: disable-next=import-outside-toplevel - import paho.mqtt.client as mqtt - - if result_code is not None and result_code != 0: - raise HomeAssistantError( - f"Error talking to MQTT: {mqtt.error_string(result_code)}" - ) - - -def _matcher_for_topic(subscription: str) -> Any: - # pylint: disable-next=import-outside-toplevel - from paho.mqtt.matcher import MQTTMatcher - - matcher = MQTTMatcher() - matcher[subscription] = True - - return lambda topic: next(matcher.iter_match(topic), False) - - @websocket_api.websocket_command( {vol.Required("type"): "mqtt/device/debug_info", vol.Required("device_id"): str} ) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 06c013ec744..c20fbb7c657 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -31,8 +31,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -50,6 +50,8 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate +from .util import valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -85,7 +87,7 @@ DEFAULT_NAME = "MQTT Alarm" REMOTE_CODE = "REMOTE_CODE" REMOTE_CODE_TEXT = "REMOTE_CODE_TEXT" -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_CODE): cv.string, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, @@ -94,7 +96,7 @@ PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( vol.Optional( CONF_COMMAND_TEMPLATE, default=DEFAULT_COMMAND_TEMPLATE ): cv.template, - vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string, @@ -107,8 +109,8 @@ PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( ): cv.string, vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string, vol.Optional(CONF_PAYLOAD_TRIGGER, default=DEFAULT_TRIGGER): cv.string, - vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, - vol.Required(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index b9ab190cc9b..1cb90d6c903 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -34,8 +34,8 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util -from . import MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RO_SCHEMA from .const import CONF_ENCODING, CONF_QOS, CONF_STATE_TOPIC, PAYLOAD_NONE from .debug_info import log_messages from .mixins import ( @@ -47,6 +47,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttValueTemplate _LOGGER = logging.getLogger(__name__) @@ -57,7 +58,7 @@ DEFAULT_PAYLOAD_ON = "ON" DEFAULT_FORCE_UPDATE = False CONF_EXPIRE_AFTER = "expire_after" -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RO_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( { vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index 47e96ff3e1a..b50856d20c1 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -15,8 +15,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate -from .. import mqtt +from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -32,19 +31,21 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate +from .util import valid_publish_topic CONF_PAYLOAD_PRESS = "payload_press" DEFAULT_NAME = "MQTT Button" DEFAULT_PAYLOAD_PRESS = "PRESS" -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, - vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_DEVICE_CLASS): button.DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_PRESS, default=DEFAULT_PAYLOAD_PRESS): cv.string, - vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 2e5d95ebda4..ae38e07d17a 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -17,7 +17,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription -from .. import mqtt +from .config import MQTT_BASE_SCHEMA from .const import CONF_ENCODING, CONF_QOS, CONF_TOPIC from .debug_info import log_messages from .mixins import ( @@ -28,6 +28,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .util import valid_subscribe_topic DEFAULT_NAME = "MQTT Camera" @@ -40,10 +41,10 @@ MQTT_CAMERA_ATTRIBUTES_BLOCKED = frozenset( } ) -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, + vol.Required(CONF_TOPIC): valid_subscribe_topic, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py new file mode 100644 index 00000000000..66699372516 --- /dev/null +++ b/homeassistant/components/mqtt/client.py @@ -0,0 +1,659 @@ +"""Support for MQTT message handling.""" +from __future__ import annotations + +import asyncio +from collections.abc import Awaitable, Callable +from functools import lru_cache, partial, wraps +import inspect +from itertools import groupby +import logging +from operator import attrgetter +import ssl +import time +from typing import TYPE_CHECKING, Any, Union, cast +import uuid + +import attr +import certifi + +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, + EVENT_HOMEASSISTANT_STARTED, +) +from homeassistant.core import CoreState, HassJob, HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.typing import ConfigType +from homeassistant.loader import bind_hass +from homeassistant.util import dt as dt_util +from homeassistant.util.async_ import run_callback_threadsafe +from homeassistant.util.logging import catch_log_exception + +from .const import ( + ATTR_TOPIC, + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_CERTIFICATE, + CONF_CLIENT_CERT, + CONF_CLIENT_KEY, + CONF_KEEPALIVE, + CONF_TLS_INSECURE, + CONF_WILL_MESSAGE, + DATA_MQTT, + DEFAULT_ENCODING, + DEFAULT_QOS, + MQTT_CONNECTED, + MQTT_DISCONNECTED, + PROTOCOL_31, +) +from .discovery import LAST_DISCOVERY +from .models import ( + AsyncMessageCallbackType, + MessageCallbackType, + PublishMessage, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) + +if TYPE_CHECKING: + # Only import for paho-mqtt type checking here, imports are done locally + # because integrations should be able to optionally rely on MQTT. + import paho.mqtt.client as mqtt + +_LOGGER = logging.getLogger(__name__) + +DISCOVERY_COOLDOWN = 2 +TIMEOUT_ACK = 10 + +SubscribePayloadType = Union[str, bytes] # Only bytes if encoding is None + + +def publish( + hass: HomeAssistant, + topic: str, + payload: PublishPayloadType, + qos: int | None = 0, + retain: bool | None = False, + encoding: str | None = DEFAULT_ENCODING, +) -> None: + """Publish message to a MQTT topic.""" + hass.add_job(async_publish, hass, topic, payload, qos, retain, encoding) + + +async def async_publish( + hass: HomeAssistant, + topic: str, + payload: PublishPayloadType, + qos: int | None = 0, + retain: bool | None = False, + encoding: str | None = DEFAULT_ENCODING, +) -> None: + """Publish message to a MQTT topic.""" + + outgoing_payload = payload + if not isinstance(payload, bytes): + if not encoding: + _LOGGER.error( + "Can't pass-through payload for publishing %s on %s with no encoding set, need 'bytes' got %s", + payload, + topic, + type(payload), + ) + return + outgoing_payload = str(payload) + if encoding != DEFAULT_ENCODING: + # a string is encoded as utf-8 by default, other encoding requires bytes as payload + try: + outgoing_payload = outgoing_payload.encode(encoding) + except (AttributeError, LookupError, UnicodeEncodeError): + _LOGGER.error( + "Can't encode payload for publishing %s on %s with encoding %s", + payload, + topic, + encoding, + ) + return + + await hass.data[DATA_MQTT].async_publish(topic, outgoing_payload, qos, retain) + + +AsyncDeprecatedMessageCallbackType = Callable[ + [str, ReceivePayloadType, int], Awaitable[None] +] +DeprecatedMessageCallbackType = Callable[[str, ReceivePayloadType, int], None] + + +def wrap_msg_callback( + msg_callback: AsyncDeprecatedMessageCallbackType | DeprecatedMessageCallbackType, +) -> AsyncMessageCallbackType | MessageCallbackType: + """Wrap an MQTT message callback to support deprecated signature.""" + # Check for partials to properly determine if coroutine function + check_func = msg_callback + while isinstance(check_func, partial): + check_func = check_func.func + + wrapper_func: AsyncMessageCallbackType | MessageCallbackType + if asyncio.iscoroutinefunction(check_func): + + @wraps(msg_callback) + async def async_wrapper(msg: ReceiveMessage) -> None: + """Call with deprecated signature.""" + await cast(AsyncDeprecatedMessageCallbackType, msg_callback)( + msg.topic, msg.payload, msg.qos + ) + + wrapper_func = async_wrapper + else: + + @wraps(msg_callback) + def wrapper(msg: ReceiveMessage) -> None: + """Call with deprecated signature.""" + msg_callback(msg.topic, msg.payload, msg.qos) + + wrapper_func = wrapper + return wrapper_func + + +@bind_hass +async def async_subscribe( + hass: HomeAssistant, + topic: str, + msg_callback: AsyncMessageCallbackType + | MessageCallbackType + | DeprecatedMessageCallbackType + | AsyncDeprecatedMessageCallbackType, + qos: int = DEFAULT_QOS, + encoding: str | None = "utf-8", +): + """Subscribe to an MQTT topic. + + Call the return value to unsubscribe. + """ + # Count callback parameters which don't have a default value + non_default = 0 + if msg_callback: + non_default = sum( + p.default == inspect.Parameter.empty + for _, p in inspect.signature(msg_callback).parameters.items() + ) + + wrapped_msg_callback = msg_callback + # If we have 3 parameters with no default value, wrap the callback + if non_default == 3: + module = inspect.getmodule(msg_callback) + _LOGGER.warning( + "Signature of MQTT msg_callback '%s.%s' is deprecated", + module.__name__ if module else "", + msg_callback.__name__, + ) + wrapped_msg_callback = wrap_msg_callback( + cast(DeprecatedMessageCallbackType, msg_callback) + ) + + async_remove = await hass.data[DATA_MQTT].async_subscribe( + topic, + catch_log_exception( + wrapped_msg_callback, + lambda msg: ( + f"Exception in {msg_callback.__name__} when handling msg on " + f"'{msg.topic}': '{msg.payload}'" + ), + ), + qos, + encoding, + ) + return async_remove + + +@bind_hass +def subscribe( + hass: HomeAssistant, + topic: str, + msg_callback: MessageCallbackType, + qos: int = DEFAULT_QOS, + encoding: str = "utf-8", +) -> Callable[[], None]: + """Subscribe to an MQTT topic.""" + async_remove = asyncio.run_coroutine_threadsafe( + async_subscribe(hass, topic, msg_callback, qos, encoding), hass.loop + ).result() + + def remove(): + """Remove listener convert.""" + run_callback_threadsafe(hass.loop, async_remove).result() + + return remove + + +@attr.s(slots=True, frozen=True) +class Subscription: + """Class to hold data about an active subscription.""" + + topic: str = attr.ib() + matcher: Any = attr.ib() + job: HassJob = attr.ib() + qos: int = attr.ib(default=0) + encoding: str | None = attr.ib(default="utf-8") + + +class MqttClientSetup: + """Helper class to setup the paho mqtt client from config.""" + + def __init__(self, config: ConfigType) -> None: + """Initialize the MQTT client setup helper.""" + + # We don't import on the top because some integrations + # should be able to optionally rely on MQTT. + import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + + if config[CONF_PROTOCOL] == PROTOCOL_31: + proto = mqtt.MQTTv31 + else: + proto = mqtt.MQTTv311 + + if (client_id := config.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) + self._client = mqtt.Client(client_id, protocol=proto) + + # Enable logging + self._client.enable_logger() + + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + if username is not None: + self._client.username_pw_set(username, password) + + if (certificate := config.get(CONF_CERTIFICATE)) == "auto": + certificate = certifi.where() + + client_key = config.get(CONF_CLIENT_KEY) + client_cert = config.get(CONF_CLIENT_CERT) + tls_insecure = config.get(CONF_TLS_INSECURE) + if certificate is not None: + self._client.tls_set( + certificate, + certfile=client_cert, + keyfile=client_key, + tls_version=ssl.PROTOCOL_TLS, + ) + + if tls_insecure is not None: + self._client.tls_insecure_set(tls_insecure) + + @property + def client(self) -> mqtt.Client: + """Return the paho MQTT client.""" + return self._client + + +class MQTT: + """Home Assistant MQTT client.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry, + conf, + ) -> None: + """Initialize Home Assistant MQTT client.""" + # We don't import on the top because some integrations + # should be able to optionally rely on MQTT. + import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel + + self.hass = hass + self.config_entry = config_entry + self.conf = conf + self.subscriptions: list[Subscription] = [] + self.connected = False + self._ha_started = asyncio.Event() + self._last_subscribe = time.time() + self._mqttc: mqtt.Client = None + self._paho_lock = asyncio.Lock() + + self._pending_operations: dict[str, asyncio.Event] = {} + + if self.hass.state == CoreState.running: + self._ha_started.set() + else: + + @callback + def ha_started(_): + self._ha_started.set() + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, ha_started) + + self.init_client() + + def init_client(self): + """Initialize paho client.""" + self._mqttc = MqttClientSetup(self.conf).client + self._mqttc.on_connect = self._mqtt_on_connect + self._mqttc.on_disconnect = self._mqtt_on_disconnect + self._mqttc.on_message = self._mqtt_on_message + self._mqttc.on_publish = self._mqtt_on_callback + self._mqttc.on_subscribe = self._mqtt_on_callback + self._mqttc.on_unsubscribe = self._mqtt_on_callback + + if ( + CONF_WILL_MESSAGE in self.conf + and ATTR_TOPIC in self.conf[CONF_WILL_MESSAGE] + ): + will_message = PublishMessage(**self.conf[CONF_WILL_MESSAGE]) + else: + will_message = None + + if will_message is not None: + self._mqttc.will_set( + topic=will_message.topic, + payload=will_message.payload, + qos=will_message.qos, + retain=will_message.retain, + ) + + async def async_publish( + self, topic: str, payload: PublishPayloadType, qos: int, retain: bool + ) -> None: + """Publish a MQTT message.""" + async with self._paho_lock: + msg_info = await self.hass.async_add_executor_job( + self._mqttc.publish, topic, payload, qos, retain + ) + _LOGGER.debug( + "Transmitting message on %s: '%s', mid: %s", + topic, + payload, + msg_info.mid, + ) + _raise_on_error(msg_info.rc) + await self._wait_for_mid(msg_info.mid) + + async def async_connect(self) -> None: + """Connect to the host. Does not process messages yet.""" + # pylint: disable-next=import-outside-toplevel + import paho.mqtt.client as mqtt + + result: int | None = None + try: + result = await self.hass.async_add_executor_job( + self._mqttc.connect, + self.conf[CONF_BROKER], + self.conf[CONF_PORT], + self.conf[CONF_KEEPALIVE], + ) + except OSError as err: + _LOGGER.error("Failed to connect to MQTT server due to exception: %s", err) + + if result is not None and result != 0: + _LOGGER.error( + "Failed to connect to MQTT server: %s", mqtt.error_string(result) + ) + + self._mqttc.loop_start() + + async def async_disconnect(self): + """Stop the MQTT client.""" + + def stop(): + """Stop the MQTT client.""" + # Do not disconnect, we want the broker to always publish will + self._mqttc.loop_stop() + + await self.hass.async_add_executor_job(stop) + + async def async_subscribe( + self, + topic: str, + msg_callback: MessageCallbackType, + qos: int, + encoding: str | None = None, + ) -> Callable[[], None]: + """Set up a subscription to a topic with the provided qos. + + This method is a coroutine. + """ + if not isinstance(topic, str): + raise HomeAssistantError("Topic needs to be a string!") + + subscription = Subscription( + topic, _matcher_for_topic(topic), HassJob(msg_callback), qos, encoding + ) + self.subscriptions.append(subscription) + self._matching_subscriptions.cache_clear() + + # Only subscribe if currently connected. + if self.connected: + self._last_subscribe = time.time() + await self._async_perform_subscription(topic, qos) + + @callback + def async_remove() -> None: + """Remove subscription.""" + if subscription not in self.subscriptions: + raise HomeAssistantError("Can't remove subscription twice") + self.subscriptions.remove(subscription) + self._matching_subscriptions.cache_clear() + + # Only unsubscribe if currently connected. + if self.connected: + self.hass.async_create_task(self._async_unsubscribe(topic)) + + return async_remove + + async def _async_unsubscribe(self, topic: str) -> None: + """Unsubscribe from a topic. + + This method is a coroutine. + """ + if any(other.topic == topic for other in self.subscriptions): + # Other subscriptions on topic remaining - don't unsubscribe. + return + + async with self._paho_lock: + result: int | None = None + result, mid = await self.hass.async_add_executor_job( + self._mqttc.unsubscribe, topic + ) + _LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid) + _raise_on_error(result) + await self._wait_for_mid(mid) + + async def _async_perform_subscription(self, topic: str, qos: int) -> None: + """Perform a paho-mqtt subscription.""" + async with self._paho_lock: + result: int | None = None + result, mid = await self.hass.async_add_executor_job( + self._mqttc.subscribe, topic, qos + ) + _LOGGER.debug("Subscribing to %s, mid: %s", topic, mid) + _raise_on_error(result) + await self._wait_for_mid(mid) + + def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code: int) -> None: + """On connect callback. + + Resubscribe to all topics we were subscribed to and publish birth + message. + """ + # pylint: disable-next=import-outside-toplevel + import paho.mqtt.client as mqtt + + if result_code != mqtt.CONNACK_ACCEPTED: + _LOGGER.error( + "Unable to connect to the MQTT broker: %s", + mqtt.connack_string(result_code), + ) + return + + self.connected = True + dispatcher_send(self.hass, MQTT_CONNECTED) + _LOGGER.info( + "Connected to MQTT server %s:%s (%s)", + self.conf[CONF_BROKER], + self.conf[CONF_PORT], + result_code, + ) + + # Group subscriptions to only re-subscribe once for each topic. + keyfunc = attrgetter("topic") + for topic, subs in groupby(sorted(self.subscriptions, key=keyfunc), keyfunc): + # Re-subscribe with the highest requested qos + max_qos = max(subscription.qos for subscription in subs) + self.hass.add_job(self._async_perform_subscription, topic, max_qos) + + if ( + CONF_BIRTH_MESSAGE in self.conf + and ATTR_TOPIC in self.conf[CONF_BIRTH_MESSAGE] + ): + + async def publish_birth_message(birth_message): + await self._ha_started.wait() # Wait for Home Assistant to start + await self._discovery_cooldown() # Wait for MQTT discovery to cool down + await self.async_publish( + topic=birth_message.topic, + payload=birth_message.payload, + qos=birth_message.qos, + retain=birth_message.retain, + ) + + birth_message = PublishMessage(**self.conf[CONF_BIRTH_MESSAGE]) + asyncio.run_coroutine_threadsafe( + publish_birth_message(birth_message), self.hass.loop + ) + + def _mqtt_on_message(self, _mqttc, _userdata, msg) -> None: + """Message received callback.""" + self.hass.add_job(self._mqtt_handle_message, msg) + + @lru_cache(2048) + def _matching_subscriptions(self, topic): + subscriptions = [] + for subscription in self.subscriptions: + if subscription.matcher(topic): + subscriptions.append(subscription) + return subscriptions + + @callback + def _mqtt_handle_message(self, msg) -> None: + _LOGGER.debug( + "Received message on %s%s: %s", + msg.topic, + " (retained)" if msg.retain else "", + msg.payload[0:8192], + ) + timestamp = dt_util.utcnow() + + subscriptions = self._matching_subscriptions(msg.topic) + + for subscription in subscriptions: + + payload: SubscribePayloadType = msg.payload + if subscription.encoding is not None: + try: + payload = msg.payload.decode(subscription.encoding) + except (AttributeError, UnicodeDecodeError): + _LOGGER.warning( + "Can't decode payload %s on %s with encoding %s (for %s)", + msg.payload[0:8192], + msg.topic, + subscription.encoding, + subscription.job, + ) + continue + + self.hass.async_run_hass_job( + subscription.job, + ReceiveMessage( + msg.topic, + payload, + msg.qos, + msg.retain, + subscription.topic, + timestamp, + ), + ) + + def _mqtt_on_callback(self, _mqttc, _userdata, mid, _granted_qos=None) -> None: + """Publish / Subscribe / Unsubscribe callback.""" + self.hass.add_job(self._mqtt_handle_mid, mid) + + @callback + def _mqtt_handle_mid(self, mid) -> None: + # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid + # may be executed first. + if mid not in self._pending_operations: + self._pending_operations[mid] = asyncio.Event() + self._pending_operations[mid].set() + + def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None: + """Disconnected callback.""" + self.connected = False + dispatcher_send(self.hass, MQTT_DISCONNECTED) + _LOGGER.warning( + "Disconnected from MQTT server %s:%s (%s)", + self.conf[CONF_BROKER], + self.conf[CONF_PORT], + result_code, + ) + + async def _wait_for_mid(self, mid): + """Wait for ACK from broker.""" + # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid + # may be executed first. + if mid not in self._pending_operations: + self._pending_operations[mid] = asyncio.Event() + try: + await asyncio.wait_for(self._pending_operations[mid].wait(), TIMEOUT_ACK) + except asyncio.TimeoutError: + _LOGGER.warning( + "No ACK from MQTT server in %s seconds (mid: %s)", TIMEOUT_ACK, mid + ) + finally: + del self._pending_operations[mid] + + async def _discovery_cooldown(self): + now = time.time() + # Reset discovery and subscribe cooldowns + self.hass.data[LAST_DISCOVERY] = now + self._last_subscribe = now + + last_discovery = self.hass.data[LAST_DISCOVERY] + last_subscribe = self._last_subscribe + wait_until = max( + last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN + ) + while now < wait_until: + await asyncio.sleep(wait_until - now) + now = time.time() + last_discovery = self.hass.data[LAST_DISCOVERY] + last_subscribe = self._last_subscribe + wait_until = max( + last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN + ) + + +def _raise_on_error(result_code: int | None) -> None: + """Raise error if error result.""" + # pylint: disable-next=import-outside-toplevel + import paho.mqtt.client as mqtt + + if result_code is not None and result_code != 0: + raise HomeAssistantError( + f"Error talking to MQTT: {mqtt.error_string(result_code)}" + ) + + +def _matcher_for_topic(subscription: str) -> Any: + # pylint: disable-next=import-outside-toplevel + from paho.mqtt.matcher import MQTTMatcher + + matcher = MQTTMatcher() + matcher[subscription] = True + + return lambda topic: next(matcher.iter_match(topic), False) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 52465bbba24..64b462359be 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -44,8 +44,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA from .const import CONF_ENCODING, CONF_QOS, CONF_RETAIN, PAYLOAD_NONE from .debug_info import log_messages from .mixins import ( @@ -56,6 +56,8 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate +from .util import valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -232,33 +234,33 @@ def valid_preset_mode_configuration(config): return config -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( { - vol.Optional(CONF_AUX_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_AUX_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_AUX_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_AUX_STATE_TOPIC): valid_subscribe_topic, # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_AWAY_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template, - vol.Optional(CONF_CURRENT_TEMP_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_CURRENT_TEMP_TOPIC): valid_subscribe_topic, vol.Optional(CONF_FAN_MODE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): valid_publish_topic, vol.Optional( CONF_FAN_MODE_LIST, default=[FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH], ): cv.ensure_list, vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_FAN_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_FAN_MODE_STATE_TOPIC): valid_subscribe_topic, # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 vol.Optional(CONF_HOLD_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_HOLD_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_HOLD_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_HOLD_LIST): cv.ensure_list, vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_MODE_COMMAND_TOPIC): valid_publish_topic, vol.Optional( CONF_MODE_LIST, default=[ @@ -271,54 +273,54 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_SCHEMA.extend( ], ): cv.ensure_list, vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, - vol.Optional(CONF_POWER_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_POWER_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_POWER_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_POWER_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_POWER_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_PRECISION): vol.In( [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] ), - vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 vol.Optional(CONF_SEND_IF_OFF): cv.boolean, vol.Optional(CONF_ACTION_TEMPLATE): cv.template, - vol.Optional(CONF_ACTION_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_ACTION_TOPIC): valid_subscribe_topic, # 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, + ): 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_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_PRESET_MODE_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_SWING_MODE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): valid_publish_topic, vol.Optional( CONF_SWING_MODE_LIST, default=[SWING_ON, SWING_OFF] ): cv.ensure_list, vol.Optional(CONF_SWING_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_SWING_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_SWING_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_TEMP_INITIAL, default=21): cv.positive_int, vol.Optional(CONF_TEMP_MIN, default=DEFAULT_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_TEMP_MAX, default=DEFAULT_MAX_TEMP): vol.Coerce(float), vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float), vol.Optional(CONF_TEMP_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_TEMP_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_TEMP_HIGH_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_HIGH_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_TEMP_HIGH_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_TEMP_HIGH_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_TEMP_HIGH_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_TEMP_HIGH_STATE_TEMPLATE): cv.template, vol.Optional(CONF_TEMP_LOW_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_LOW_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_TEMP_LOW_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_TEMP_LOW_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_LOW_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_TEMP_LOW_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_TEMP_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_TEMP_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_TEMPERATURE_UNIT): cv.temperature_unit, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } diff --git a/homeassistant/components/mqtt/config.py b/homeassistant/components/mqtt/config.py new file mode 100644 index 00000000000..4f84d911418 --- /dev/null +++ b/homeassistant/components/mqtt/config.py @@ -0,0 +1,148 @@ +"""Support for MQTT message handling.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_DISCOVERY, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, + CONF_VALUE_TEMPLATE, +) +from homeassistant.helpers import config_validation as cv + +from .const import ( + ATTR_PAYLOAD, + ATTR_QOS, + ATTR_RETAIN, + ATTR_TOPIC, + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_CERTIFICATE, + CONF_CLIENT_CERT, + CONF_CLIENT_KEY, + CONF_COMMAND_TOPIC, + CONF_DISCOVERY_PREFIX, + CONF_ENCODING, + CONF_KEEPALIVE, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + CONF_TLS_INSECURE, + CONF_TLS_VERSION, + CONF_WILL_MESSAGE, + DEFAULT_BIRTH, + DEFAULT_DISCOVERY, + DEFAULT_ENCODING, + DEFAULT_PREFIX, + DEFAULT_QOS, + DEFAULT_RETAIN, + DEFAULT_WILL, + PLATFORMS, + PROTOCOL_31, + PROTOCOL_311, +) +from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic + +DEFAULT_PORT = 1883 +DEFAULT_KEEPALIVE = 60 +DEFAULT_PROTOCOL = PROTOCOL_311 +DEFAULT_TLS_PROTOCOL = "auto" + +DEFAULT_VALUES = { + CONF_BIRTH_MESSAGE: DEFAULT_BIRTH, + CONF_DISCOVERY: DEFAULT_DISCOVERY, + CONF_PORT: DEFAULT_PORT, + CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL, + CONF_WILL_MESSAGE: DEFAULT_WILL, +} + +CLIENT_KEY_AUTH_MSG = ( + "client_key and client_cert must both be present in " + "the MQTT broker configuration" +) + +MQTT_WILL_BIRTH_SCHEMA = vol.Schema( + { + vol.Inclusive(ATTR_TOPIC, "topic_payload"): valid_publish_topic, + vol.Inclusive(ATTR_PAYLOAD, "topic_payload"): cv.string, + vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + }, + required=True, +) + +PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( + {vol.Optional(platform.value): cv.ensure_list for platform in PLATFORMS} +) + +CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend( + { + vol.Optional(CONF_CLIENT_ID): cv.string, + vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( + vol.Coerce(int), vol.Range(min=15) + ), + vol.Optional(CONF_BROKER): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), + vol.Inclusive( + CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG + ): cv.isfile, + vol.Inclusive( + CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG + ): cv.isfile, + vol.Optional(CONF_TLS_INSECURE): cv.boolean, + vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"), + vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( + cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) + ), + vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_DISCOVERY): cv.boolean, + # discovery_prefix must be a valid publish topic because if no + # state topic is specified, it will be created with the given prefix. + vol.Optional( + CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX + ): valid_publish_topic, + } +) + +DEPRECATED_CONFIG_KEYS = [ + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_DISCOVERY, + CONF_PASSWORD, + CONF_PORT, + CONF_TLS_VERSION, + CONF_USERNAME, + CONF_WILL_MESSAGE, +] + +SCHEMA_BASE = { + vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, +} + +MQTT_BASE_SCHEMA = vol.Schema(SCHEMA_BASE) + +# Sensor type platforms subscribe to MQTT events +MQTT_RO_SCHEMA = MQTT_BASE_SCHEMA.extend( + { + vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + } +) + +# Switch type platforms publish to MQTT and may subscribe +MQTT_RW_SCHEMA = MQTT_BASE_SCHEMA.extend( + { + vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, + } +) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 6697b17dfdc..7137b93118f 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -17,7 +17,7 @@ from homeassistant.const import ( ) from homeassistant.data_entry_flow import FlowResult -from . import MqttClientSetup +from .client import MqttClientSetup from .const import ( ATTR_PAYLOAD, ATTR_QOS, diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 106d0310158..2f7e27e7252 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -1,5 +1,5 @@ """Constants used by multiple MQTT modules.""" -from homeassistant.const import CONF_PAYLOAD +from homeassistant.const import CONF_PAYLOAD, Platform ATTR_DISCOVERY_HASH = "discovery_hash" ATTR_DISCOVERY_PAYLOAD = "discovery_payload" @@ -14,7 +14,9 @@ CONF_BROKER = "broker" CONF_BIRTH_MESSAGE = "birth_message" CONF_COMMAND_TEMPLATE = "command_template" CONF_COMMAND_TOPIC = "command_topic" +CONF_DISCOVERY_PREFIX = "discovery_prefix" CONF_ENCODING = "encoding" +CONF_KEEPALIVE = "keepalive" CONF_QOS = ATTR_QOS CONF_RETAIN = ATTR_RETAIN CONF_STATE_TOPIC = "state_topic" @@ -30,6 +32,7 @@ CONF_TLS_VERSION = "tls_version" CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" +DATA_MQTT = "mqtt" DATA_MQTT_CONFIG = "mqtt_config" DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" @@ -66,3 +69,24 @@ PAYLOAD_NONE = "None" PROTOCOL_31 = "3.1" PROTOCOL_311 = "3.1.1" + +PLATFORMS = [ + Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.CAMERA, + Platform.CLIMATE, + Platform.DEVICE_TRACKER, + Platform.COVER, + Platform.FAN, + Platform.HUMIDIFIER, + Platform.LIGHT, + Platform.LOCK, + Platform.NUMBER, + Platform.SELECT, + Platform.SCENE, + Platform.SENSOR, + Platform.SIREN, + Platform.SWITCH, + Platform.VACUUM, +] diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 8e36329946a..5814f3e43f7 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -33,8 +33,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_BASE_SCHEMA from .const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -51,6 +51,8 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate +from .util import valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -152,11 +154,11 @@ def validate_options(value): return value -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( { - vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_GET_POSITION_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_GET_POSITION_TOPIC): 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( @@ -172,24 +174,24 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_BASE_SCHEMA.extend( 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_SET_POSITION_TOPIC): 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_STATE_TOPIC): 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_COMMAND_TOPIC): 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_TOPIC): 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, diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py index 002ae6e3991..0646a5bda0c 100644 --- a/homeassistant/components/mqtt/device_automation.py +++ b/homeassistant/components/mqtt/device_automation.py @@ -6,7 +6,7 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from . import device_trigger -from .. import mqtt +from .config import MQTT_BASE_SCHEMA from .mixins import async_setup_entry_helper AUTOMATION_TYPE_TRIGGER = "trigger" @@ -17,7 +17,7 @@ CONF_AUTOMATION_TYPE = "automation_type" PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( {vol.Required(CONF_AUTOMATION_TYPE): AUTOMATION_TYPES_SCHEMA}, extra=vol.ALLOW_EXTRA, -).extend(mqtt.MQTT_BASE_SCHEMA.schema) +).extend(MQTT_BASE_SCHEMA.schema) async def async_setup_entry(hass, config_entry): diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index aa7506bd5e3..1b48e15b80e 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -19,8 +19,8 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .. import MqttValueTemplate, subscription -from ... import mqtt +from .. import subscription +from ..config import MQTT_RO_SCHEMA from ..const import CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages from ..mixins import ( @@ -29,12 +29,13 @@ from ..mixins import ( async_get_platform_config_from_yaml, async_setup_entry_helper, ) +from ..models import MqttValueTemplate CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" CONF_SOURCE_TYPE = "source_type" -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RO_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, diff --git a/homeassistant/components/mqtt/device_tracker/schema_yaml.py b/homeassistant/components/mqtt/device_tracker/schema_yaml.py index f871ac89c2d..2dfa5b7134c 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_yaml.py +++ b/homeassistant/components/mqtt/device_tracker/schema_yaml.py @@ -7,16 +7,18 @@ from homeassistant.const import CONF_DEVICES, STATE_HOME, STATE_NOT_HOME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from ... import mqtt +from ..client import async_subscribe +from ..config import SCHEMA_BASE from ..const import CONF_QOS +from ..util import valid_subscribe_topic CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" CONF_SOURCE_TYPE = "source_type" -PLATFORM_SCHEMA_YAML = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( +PLATFORM_SCHEMA_YAML = PLATFORM_SCHEMA.extend(SCHEMA_BASE).extend( { - vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}, + vol.Required(CONF_DEVICES): {cv.string: valid_subscribe_topic}, vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string, vol.Optional(CONF_SOURCE_TYPE): vol.In(SOURCE_TYPES), @@ -50,6 +52,6 @@ async def async_setup_scanner_from_yaml(hass, config, async_see, discovery_info= hass.async_create_task(async_see(**see_args)) - await mqtt.async_subscribe(hass, topic, async_message_received, qos) + await async_subscribe(hass, topic, async_message_received, qos) return True diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 2c6c6ecc3ba..0b4bcbfcbc2 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -29,7 +29,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType from . import debug_info, trigger as mqtt_trigger -from .. import mqtt +from .config import MQTT_BASE_SCHEMA from .const import ( ATTR_DISCOVERY_HASH, CONF_ENCODING, @@ -71,7 +71,7 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( } ) -TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_SCHEMA.extend( +TRIGGER_DISCOVERY_SCHEMA = MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_AUTOMATION_TYPE): str, vol.Required(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -101,7 +101,7 @@ class TriggerInstance: async def async_attach_trigger(self) -> None: """Attach MQTT trigger.""" mqtt_config = { - CONF_PLATFORM: mqtt.DOMAIN, + CONF_PLATFORM: DOMAIN, CONF_TOPIC: self.trigger.topic, CONF_ENCODING: DEFAULT_ENCODING, CONF_QOS: self.trigger.qos, diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index f2b738cd2bb..f72b0bdf689 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -34,8 +34,8 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -55,6 +55,8 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate +from .util import valid_publish_topic, valid_subscribe_topic CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic" CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic" @@ -125,28 +127,28 @@ def valid_preset_mode_configuration(config): return config -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = MQTT_RW_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_TOPIC): 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_STATE_TOPIC): 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_TOPIC): 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_STATE_TOPIC): 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, + ): 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_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_PRESET_MODE_VALUE_TEMPLATE): cv.template, vol.Optional( CONF_SPEED_RANGE_MIN, default=DEFAULT_SPEED_RANGE_MIN @@ -168,8 +170,8 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( 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_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_SPEED_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_SPEED_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_SPEED_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, } diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index f6d4aa01dab..000a9b9700e 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -30,8 +30,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -51,6 +51,8 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate +from .util import valid_publish_topic, valid_subscribe_topic CONF_AVAILABLE_MODES_LIST = "modes" CONF_DEVICE_CLASS = "device_class" @@ -103,15 +105,13 @@ def valid_humidity_range_configuration(config): return config -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = MQTT_RW_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.Inclusive(CONF_MODE_COMMAND_TOPIC, "available_modes"): valid_publish_topic, vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional( CONF_DEVICE_CLASS, default=HumidifierDeviceClass.HUMIDIFIER @@ -119,14 +119,14 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( [HumidifierDeviceClass.HUMIDIFIER, HumidifierDeviceClass.DEHUMIDIFIER] ), vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_MODE_STATE_TOPIC): 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.Required(CONF_TARGET_HUMIDITY_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_TARGET_HUMIDITY_COMMAND_TEMPLATE): cv.template, vol.Optional( CONF_TARGET_HUMIDITY_MAX, default=DEFAULT_MAX_HUMIDITY @@ -135,7 +135,7 @@ _PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( 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_TARGET_HUMIDITY_STATE_TOPIC): valid_subscribe_topic, vol.Optional( CONF_PAYLOAD_RESET_HUMIDITY, default=DEFAULT_PAYLOAD_RESET ): cv.string, diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index eb4ec264981..1c94fa82f73 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -42,8 +42,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util -from .. import MqttCommandTemplate, MqttValueTemplate, subscription -from ... import mqtt +from .. import subscription +from ..config import MQTT_RW_SCHEMA from ..const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -55,6 +55,8 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity +from ..models import MqttCommandTemplate, MqttValueTemplate +from ..util import valid_publish_topic, valid_subscribe_topic from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -156,28 +158,28 @@ VALUE_TEMPLATE_KEYS = [ ] _PLATFORM_SCHEMA_BASE = ( - mqtt.MQTT_RW_SCHEMA.extend( + MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_BRIGHTNESS_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_BRIGHTNESS_COMMAND_TOPIC): valid_publish_topic, vol.Optional( CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE ): vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_BRIGHTNESS_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_BRIGHTNESS_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_COLOR_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_COLOR_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_COLOR_MODE_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_COLOR_TEMP_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_COLOR_TEMP_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_EFFECT_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_EFFECT_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_EFFECT_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_EFFECT_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_EFFECT_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_EFFECT_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_HS_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_HS_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_HS_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_HS_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_HS_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_MAX_MIREDS): cv.positive_int, vol.Optional(CONF_MIN_MIREDS): cv.positive_int, @@ -189,30 +191,30 @@ _PLATFORM_SCHEMA_BASE = ( 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_RGB_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_RGB_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_RGB_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_RGB_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_RGB_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_RGB_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_RGBW_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_RGBW_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_RGBW_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_RGBW_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_RGBW_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_RGBW_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_RGBWW_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_RGBWW_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_RGBWW_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_RGBWW_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_RGBWW_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_RGBWW_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_WHITE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_WHITE_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_WHITE_SCALE, default=DEFAULT_WHITE_SCALE): vol.All( vol.Coerce(int), vol.Range(min=1) ), - vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): valid_publish_topic, vol.Optional( CONF_WHITE_VALUE_SCALE, default=DEFAULT_WHITE_VALUE_SCALE ): vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Optional(CONF_WHITE_VALUE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_WHITE_VALUE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_XY_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_XY_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_XY_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_XY_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_XY_VALUE_TEMPLATE): cv.template, }, ) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 2049818ab31..be49f1ad2e3 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -51,7 +51,7 @@ from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util from .. import subscription -from ... import mqtt +from ..config import DEFAULT_QOS, DEFAULT_RETAIN, MQTT_RW_SCHEMA from ..const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -61,6 +61,7 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity +from ..util import valid_subscribe_topic from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import CONF_BRIGHTNESS_SCALE, MQTT_LIGHT_ATTRIBUTES_BLOCKED @@ -103,7 +104,7 @@ def valid_color_configuration(config): _PLATFORM_SCHEMA_BASE = ( - mqtt.MQTT_RW_SCHEMA.extend( + MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_BRIGHTNESS, default=DEFAULT_BRIGHTNESS): cv.boolean, vol.Optional( @@ -126,12 +127,12 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_MIN_MIREDS): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_QOS, default=mqtt.DEFAULT_QOS): vol.All( + vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All( vol.Coerce(int), vol.In([0, 1, 2]) ), - vol.Optional(CONF_RETAIN, default=mqtt.DEFAULT_RETAIN): cv.boolean, + vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_RGB, default=DEFAULT_RGB): cv.boolean, - vol.Optional(CONF_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, vol.Inclusive(CONF_SUPPORTED_COLOR_MODES, "color_mode"): vol.All( cv.ensure_list, [vol.In(VALID_COLOR_MODES)], diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 0165bfc8efa..779f2f17e24 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -31,8 +31,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util -from .. import MqttValueTemplate, subscription -from ... import mqtt +from .. import subscription +from ..config import MQTT_RW_SCHEMA from ..const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -43,6 +43,7 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity +from ..models import MqttValueTemplate from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import MQTT_LIGHT_ATTRIBUTES_BLOCKED @@ -67,7 +68,7 @@ CONF_RED_TEMPLATE = "red_template" CONF_WHITE_VALUE_TEMPLATE = "white_value_template" _PLATFORM_SCHEMA_BASE = ( - mqtt.MQTT_RW_SCHEMA.extend( + MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_BLUE_TEMPLATE): cv.template, vol.Optional(CONF_BRIGHTNESS_TEMPLATE): cv.template, diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 5dc0a974d26..0cfd1d2b70f 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -15,8 +15,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -33,6 +33,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttValueTemplate CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_UNLOCK = "payload_unlock" @@ -56,7 +57,7 @@ MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset( } ) -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index a46debeae54..694fae0b3c0 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -49,14 +49,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import ( - DATA_MQTT, - PLATFORMS, - MqttValueTemplate, - async_publish, - debug_info, - subscription, -) +from . import debug_info, subscription +from .client import async_publish from .const import ( ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_PAYLOAD, @@ -65,6 +59,7 @@ from .const import ( CONF_ENCODING, CONF_QOS, CONF_TOPIC, + DATA_MQTT, DATA_MQTT_CONFIG, DATA_MQTT_RELOAD_NEEDED, DEFAULT_ENCODING, @@ -73,6 +68,7 @@ from .const import ( DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, + PLATFORMS, ) from .debug_info import log_message, log_messages from .discovery import ( @@ -82,7 +78,7 @@ from .discovery import ( clear_discovery_hash, set_discovery_hash, ) -from .models import PublishPayloadType, ReceiveMessage +from .models import MqttValueTemplate, PublishPayloadType, ReceiveMessage from .subscription import ( async_prepare_subscribe_topics, async_subscribe_topics, diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 9cec65d7254..9bce6baab8b 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -1,12 +1,21 @@ """Models used by multiple MQTT modules.""" from __future__ import annotations +from ast import literal_eval from collections.abc import Awaitable, Callable import datetime as dt -from typing import Union +from typing import Any, Union import attr +from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import template +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import TemplateVarsType + +_SENTINEL = object() + PublishPayloadType = Union[str, bytes, int, float, None] ReceivePayloadType = Union[str, bytes] @@ -35,3 +44,118 @@ class ReceiveMessage: AsyncMessageCallbackType = Callable[[ReceiveMessage], Awaitable[None]] MessageCallbackType = Callable[[ReceiveMessage], None] + + +class MqttCommandTemplate: + """Class for rendering MQTT payload with command templates.""" + + def __init__( + self, + command_template: template.Template | None, + *, + hass: HomeAssistant | None = None, + entity: Entity | None = None, + ) -> None: + """Instantiate a command template.""" + self._attr_command_template = command_template + if command_template is None: + return + + self._entity = entity + + command_template.hass = hass + + if entity: + command_template.hass = entity.hass + + @callback + def async_render( + self, + value: PublishPayloadType = None, + variables: TemplateVarsType = None, + ) -> PublishPayloadType: + """Render or convert the command template with given value or variables.""" + + def _convert_outgoing_payload( + payload: PublishPayloadType, + ) -> PublishPayloadType: + """Ensure correct raw MQTT payload is passed as bytes for publishing.""" + if isinstance(payload, str): + try: + native_object = literal_eval(payload) + if isinstance(native_object, bytes): + return native_object + + except (ValueError, TypeError, SyntaxError, MemoryError): + pass + + return payload + + if self._attr_command_template is None: + return value + + values = {"value": value} + if self._entity: + values[ATTR_ENTITY_ID] = self._entity.entity_id + values[ATTR_NAME] = self._entity.name + if variables is not None: + values.update(variables) + return _convert_outgoing_payload( + self._attr_command_template.async_render(values, parse_result=False) + ) + + +class MqttValueTemplate: + """Class for rendering MQTT value template with possible json values.""" + + def __init__( + self, + value_template: template.Template | None, + *, + hass: HomeAssistant | None = None, + entity: Entity | None = None, + config_attributes: TemplateVarsType = None, + ) -> None: + """Instantiate a value template.""" + self._value_template = value_template + self._config_attributes = config_attributes + if value_template is None: + return + + value_template.hass = hass + self._entity = entity + + if entity: + value_template.hass = entity.hass + + @callback + def async_render_with_possible_json_value( + self, + payload: ReceivePayloadType, + default: ReceivePayloadType | object = _SENTINEL, + variables: TemplateVarsType = None, + ) -> ReceivePayloadType: + """Render with possible json value or pass-though a received MQTT value.""" + if self._value_template is None: + return payload + + values: dict[str, Any] = {} + + if variables is not None: + values.update(variables) + + if self._config_attributes is not None: + values.update(self._config_attributes) + + if self._entity: + values[ATTR_ENTITY_ID] = self._entity.entity_id + values[ATTR_NAME] = self._entity.name + + if default == _SENTINEL: + return self._value_template.async_render_with_possible_json_value( + payload, variables=values + ) + + return self._value_template.async_render_with_possible_json_value( + payload, default, variables=values + ) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 001f9f4f668..6ea1f0959f6 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -27,8 +27,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -46,6 +46,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate _LOGGER = logging.getLogger(__name__) @@ -75,7 +76,7 @@ def validate_config(config): return config -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RW_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): vol.Coerce(float), diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 98c692ceaff..ce8f0b0a3e8 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -15,7 +15,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .. import mqtt +from .client import async_publish +from .config import MQTT_BASE_SCHEMA from .const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN from .mixins import ( CONF_ENABLED_BY_DEFAULT, @@ -27,13 +28,14 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .util import valid_publish_topic DEFAULT_NAME = "MQTT Scene" DEFAULT_RETAIN = False -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_BASE_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( { - vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_ON): cv.string, @@ -128,7 +130,7 @@ class MqttScene( This method is a coroutine. """ - await mqtt.async_publish( + await async_publish( self.hass, self._config[CONF_COMMAND_TOPIC], self._config[CONF_PAYLOAD_ON], diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 0765eb7f176..75e1b4e8efd 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -17,8 +17,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -36,6 +36,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate _LOGGER = logging.getLogger(__name__) @@ -51,7 +52,7 @@ MQTT_SELECT_ATTRIBUTES_BLOCKED = frozenset( ) -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index d865d90c4ee..4dd1ad4d95f 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -34,8 +34,8 @@ from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import dt as dt_util -from . import MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RO_SCHEMA from .const import CONF_ENCODING, CONF_QOS, CONF_STATE_TOPIC from .debug_info import log_messages from .mixins import ( @@ -47,6 +47,8 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttValueTemplate +from .util import valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -89,12 +91,12 @@ def validate_options(conf): return conf -_PLATFORM_SCHEMA_BASE = mqtt.MQTT_RO_SCHEMA.extend( +_PLATFORM_SCHEMA_BASE = MQTT_RO_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_TOPIC): 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, diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index c3a41c3618e..1ecf2c37dbf 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -35,8 +35,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttCommandTemplate, MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, @@ -57,6 +57,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttCommandTemplate, MqttValueTemplate DEFAULT_NAME = "MQTT Siren" DEFAULT_PAYLOAD_ON = "ON" @@ -74,7 +75,7 @@ CONF_SUPPORT_VOLUME_SET = "support_volume_set" STATE = "state" -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_AVAILABLE_TONES): cv.ensure_list, vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index f5f8363eb33..c20ddfe5151 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -24,8 +24,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_RW_SCHEMA from .const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -43,6 +43,7 @@ from .mixins import ( async_setup_platform_helper, warn_for_legacy_schema, ) +from .models import MqttValueTemplate DEFAULT_NAME = "MQTT Switch" DEFAULT_PAYLOAD_ON = "ON" @@ -51,7 +52,7 @@ DEFAULT_OPTIMISTIC = False CONF_STATE_ON = "state_on" CONF_STATE_OFF = "state_off" -PLATFORM_SCHEMA_MODERN = mqtt.MQTT_RW_SCHEMA.extend( +PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 25e49524b8f..9452d5fc259 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -11,8 +11,8 @@ from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from . import MqttValueTemplate, subscription -from .. import mqtt +from . import subscription +from .config import MQTT_BASE_SCHEMA from .const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_TOPIC from .mixins import ( MQTT_ENTITY_DEVICE_INFO_SCHEMA, @@ -21,7 +21,7 @@ from .mixins import ( send_discovery_done, update_device, ) -from .models import ReceiveMessage +from .models import MqttValueTemplate, ReceiveMessage from .subscription import EntitySubscription from .util import valid_subscribe_topic @@ -30,7 +30,7 @@ LOG_NAME = "Tag" TAG = "tag" TAGS = "mqtt_tags" -PLATFORM_SCHEMA = mqtt.MQTT_BASE_SCHEMA.extend( +PLATFORM_SCHEMA = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_PLATFORM): "mqtt", diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index eb5e01b6251..f25131c43b7 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -15,11 +15,13 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.icon import icon_for_battery_level -from .. import MqttValueTemplate, subscription -from ... import mqtt +from .. import subscription +from ..config import MQTT_BASE_SCHEMA from ..const import CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, CONF_RETAIN from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema +from ..models import MqttValueTemplate +from ..util import valid_publish_topic from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -96,25 +98,23 @@ MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED = MQTT_VACUUM_ATTRIBUTES_BLOCKED | frozens ) PLATFORM_SCHEMA_LEGACY_MODERN = ( - mqtt.MQTT_BASE_SCHEMA.extend( + MQTT_BASE_SCHEMA.extend( { vol.Inclusive(CONF_BATTERY_LEVEL_TEMPLATE, "battery"): cv.template, - vol.Inclusive( - CONF_BATTERY_LEVEL_TOPIC, "battery" - ): mqtt.valid_publish_topic, + vol.Inclusive(CONF_BATTERY_LEVEL_TOPIC, "battery"): valid_publish_topic, vol.Inclusive(CONF_CHARGING_TEMPLATE, "charging"): cv.template, - vol.Inclusive(CONF_CHARGING_TOPIC, "charging"): mqtt.valid_publish_topic, + vol.Inclusive(CONF_CHARGING_TOPIC, "charging"): valid_publish_topic, vol.Inclusive(CONF_CLEANING_TEMPLATE, "cleaning"): cv.template, - vol.Inclusive(CONF_CLEANING_TOPIC, "cleaning"): mqtt.valid_publish_topic, + vol.Inclusive(CONF_CLEANING_TOPIC, "cleaning"): valid_publish_topic, vol.Inclusive(CONF_DOCKED_TEMPLATE, "docked"): cv.template, - vol.Inclusive(CONF_DOCKED_TOPIC, "docked"): mqtt.valid_publish_topic, + vol.Inclusive(CONF_DOCKED_TOPIC, "docked"): valid_publish_topic, vol.Inclusive(CONF_ERROR_TEMPLATE, "error"): cv.template, - vol.Inclusive(CONF_ERROR_TOPIC, "error"): mqtt.valid_publish_topic, + vol.Inclusive(CONF_ERROR_TOPIC, "error"): valid_publish_topic, vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All( cv.ensure_list, [cv.string] ), vol.Inclusive(CONF_FAN_SPEED_TEMPLATE, "fan_speed"): cv.template, - vol.Inclusive(CONF_FAN_SPEED_TOPIC, "fan_speed"): mqtt.valid_publish_topic, + vol.Inclusive(CONF_FAN_SPEED_TOPIC, "fan_speed"): valid_publish_topic, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional( CONF_PAYLOAD_CLEAN_SPOT, default=DEFAULT_PAYLOAD_CLEAN_SPOT @@ -135,12 +135,12 @@ PLATFORM_SCHEMA_LEGACY_MODERN = ( vol.Optional( CONF_PAYLOAD_TURN_ON, default=DEFAULT_PAYLOAD_TURN_ON ): cv.string, - vol.Optional(CONF_SEND_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_SET_FAN_SPEED_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_SEND_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_SET_FAN_SPEED_TOPIC): valid_publish_topic, vol.Optional( CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS ): vol.All(cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())]), - vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } ) diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 7aa7be07797..3d670780994 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -23,7 +23,7 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from .. import subscription -from ... import mqtt +from ..config import MQTT_BASE_SCHEMA from ..const import ( CONF_COMMAND_TOPIC, CONF_ENCODING, @@ -33,6 +33,7 @@ from ..const import ( ) from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, warn_for_legacy_schema +from ..util import valid_publish_topic from .const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services @@ -105,7 +106,7 @@ DEFAULT_PAYLOAD_START = "start" DEFAULT_PAYLOAD_PAUSE = "pause" PLATFORM_SCHEMA_STATE_MODERN = ( - mqtt.MQTT_BASE_SCHEMA.extend( + MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All( cv.ensure_list, [cv.string] @@ -123,13 +124,13 @@ PLATFORM_SCHEMA_STATE_MODERN = ( vol.Optional(CONF_PAYLOAD_START, default=DEFAULT_PAYLOAD_START): cv.string, vol.Optional(CONF_PAYLOAD_PAUSE, default=DEFAULT_PAYLOAD_PAUSE): cv.string, vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string, - vol.Optional(CONF_SEND_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_SET_FAN_SPEED_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_STATE_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_SEND_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_SET_FAN_SPEED_TOPIC): valid_publish_topic, + vol.Optional(CONF_STATE_TOPIC): valid_publish_topic, vol.Optional( CONF_SUPPORTED_FEATURES, default=DEFAULT_SERVICE_STRINGS ): vol.All(cv.ensure_list, [vol.In(STRING_TO_SERVICE.keys())]), - vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } ) @@ -178,7 +179,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): supported_feature_strings, STRING_TO_SERVICE ) self._fan_speed_list = config[CONF_FAN_SPEED_LIST] - 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_json/device_tracker.py b/homeassistant/components/mqtt_json/device_tracker.py index 505fa3bd809..1d99e6d7b6f 100644 --- a/homeassistant/components/mqtt_json/device_tracker.py +++ b/homeassistant/components/mqtt_json/device_tracker.py @@ -35,7 +35,7 @@ GPS_JSON_PAYLOAD_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(mqtt.config.SCHEMA_BASE).extend( {vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}} ) diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index 54de561c11e..276695d8edd 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_AWAY_TIMEOUT, default=DEFAULT_AWAY_TIMEOUT): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, } -).extend(mqtt.MQTT_RO_SCHEMA.schema) +).extend(mqtt.config.MQTT_RO_SCHEMA.schema) MQTT_PAYLOAD = vol.Schema( vol.All( diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 285af765ab4..e130b820c1b 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -12,7 +12,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, ) -from homeassistant.components.mqtt import CONF_STATE_TOPIC +from homeassistant.components.mqtt.const import CONF_STATE_TOPIC from homeassistant.components.mqtt.cover import ( CONF_GET_POSITION_TEMPLATE, CONF_GET_POSITION_TOPIC, diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 07c39d70df0..aa0bfb82608 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1123,7 +1123,7 @@ async def test_restore_subscriptions_on_reconnect(hass, mqtt_client_mock, mqtt_m assert mqtt_client_mock.subscribe.call_count == 1 mqtt_client_mock.on_disconnect(None, None, 0) - with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0): + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0): mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() assert mqtt_client_mock.subscribe.call_count == 2 @@ -1157,7 +1157,7 @@ async def test_restore_all_active_subscriptions_on_reconnect( assert mqtt_client_mock.unsubscribe.call_count == 0 mqtt_client_mock.on_disconnect(None, None, 0) - with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0): + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0): mqtt_client_mock.on_connect(None, None, None, 0) await hass.async_block_till_done() @@ -1188,7 +1188,7 @@ async def test_logs_error_if_no_connect_broker( ) -@patch("homeassistant.components.mqtt.TIMEOUT_ACK", 0.3) +@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.3) async def test_handle_mqtt_on_callback(hass, caplog, mqtt_mock, mqtt_client_mock): """Test receiving an ACK callback before waiting for it.""" # Simulate an ACK for mid == 1, this will call mqtt_mock._mqtt_handle_mid(mid) @@ -1331,7 +1331,7 @@ async def test_setup_mqtt_client_protocol(hass): """Test MQTT client protocol setup.""" entry = MockConfigEntry( domain=mqtt.DOMAIN, - data={mqtt.CONF_BROKER: "test-broker", mqtt.CONF_PROTOCOL: "3.1"}, + data={mqtt.CONF_BROKER: "test-broker", mqtt.config.CONF_PROTOCOL: "3.1"}, ) with patch("paho.mqtt.client.Client") as mock_client: mock_client.on_connect(return_value=0) @@ -1341,7 +1341,7 @@ async def test_setup_mqtt_client_protocol(hass): assert mock_client.call_args[1]["protocol"] == 3 -@patch("homeassistant.components.mqtt.TIMEOUT_ACK", 0.2) +@patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.2) async def test_handle_mqtt_timeout_on_callback(hass, caplog): """Test publish without receiving an ACK callback.""" mid = 0 @@ -1486,7 +1486,7 @@ async def test_custom_birth_message(hass, mqtt_client_mock, mqtt_mock): """Handle birth message.""" birth.set() - with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0.1): + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): await mqtt.async_subscribe(hass, "birth", wait_birth) mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() @@ -1516,7 +1516,7 @@ async def test_default_birth_message(hass, mqtt_client_mock, mqtt_mock): """Handle birth message.""" birth.set() - with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0.1): + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() @@ -1532,7 +1532,7 @@ async def test_default_birth_message(hass, mqtt_client_mock, mqtt_mock): ) async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock): """Test disabling birth message.""" - with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0.1): + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() await asyncio.sleep(0.2) @@ -1580,7 +1580,7 @@ async def test_delayed_birth_message(hass, mqtt_client_mock, mqtt_config, mqtt_m """Handle birth message.""" birth.set() - with patch("homeassistant.components.mqtt.DISCOVERY_COOLDOWN", 0.1): + with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): await mqtt.async_subscribe(hass, "homeassistant/status", wait_birth) mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index f451079e0f0..b7a3b5f2118 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest from homeassistant.components import vacuum -from homeassistant.components.mqtt import CONF_COMMAND_TOPIC +from homeassistant.components.mqtt.const import CONF_COMMAND_TOPIC from homeassistant.components.mqtt.vacuum import schema_legacy as mqttvacuum from homeassistant.components.mqtt.vacuum.schema import services_to_strings from homeassistant.components.mqtt.vacuum.schema_legacy import ( diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 3f752f1b528..c1017446eff 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -6,7 +6,7 @@ from unittest.mock import patch import pytest from homeassistant.components import vacuum -from homeassistant.components.mqtt import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC +from homeassistant.components.mqtt.const import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC from homeassistant.components.mqtt.vacuum import CONF_SCHEMA, schema_state as mqttvacuum from homeassistant.components.mqtt.vacuum.const import MQTT_VACUUM_ATTRIBUTES_BLOCKED from homeassistant.components.mqtt.vacuum.schema import services_to_strings From caa79d8462358e1f07b8d3685a2acb5c10ac97d1 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 2 Jun 2022 14:24:46 +0200 Subject: [PATCH 1197/3516] Update MQTT tests to use the config entry setup (#72373) * New testframework and tests for fan platform * Merge test_common_new to test_common * Add alarm_control_panel * Add binary_sensor * Add button * Add camera * Add climate * Add config_flow * Add cover * Add device_tracker_disovery * Add device_trigger * Add diagnostics * Add discovery * Add humidifier * Add init * Add lecacy_vacuum * Add light_json * Add light_template * Add light * Add lock * Add number * Add scene * Add select * Add sensor * Add siren * Add state_vacuum * Add subscription * Add switch * Add tag * Add trigger * Add missed tests * Add another missed test * Add device_tracker * Remove commented out code * Correct tests according comments * Improve mqtt_mock_entry and recover tests * Split fixtures with and without yaml setup * Update fixtures manual_mqtt * Update fixtures mqtt_json * Fix test tasmota * Update fixture mqtt_room * Revert fixture changes, improve test * re-add test --- .../manual_mqtt/test_alarm_control_panel.py | 117 ++++-- .../mqtt/test_alarm_control_panel.py | 248 +++++++---- tests/components/mqtt/test_binary_sensor.py | 231 +++++++---- tests/components/mqtt/test_button.py | 146 ++++--- tests/components/mqtt/test_camera.py | 133 +++--- tests/components/mqtt/test_climate.py | 296 +++++++++----- tests/components/mqtt/test_common.py | 145 +++++-- tests/components/mqtt/test_config_flow.py | 11 +- tests/components/mqtt/test_cover.py | 385 ++++++++++++------ tests/components/mqtt/test_device_tracker.py | 34 +- .../mqtt/test_device_tracker_discovery.py | 58 ++- tests/components/mqtt/test_device_trigger.py | 102 +++-- tests/components/mqtt/test_diagnostics.py | 10 +- tests/components/mqtt/test_discovery.py | 111 +++-- tests/components/mqtt/test_fan.py | 214 +++++++--- tests/components/mqtt/test_humidifier.py | 202 ++++++--- tests/components/mqtt/test_init.py | 280 +++++++++---- tests/components/mqtt/test_legacy_vacuum.py | 204 ++++++---- tests/components/mqtt/test_light.py | 290 +++++++++---- tests/components/mqtt/test_light_json.py | 247 +++++++---- tests/components/mqtt/test_light_template.py | 260 +++++++----- tests/components/mqtt/test_lock.py | 169 +++++--- tests/components/mqtt/test_number.py | 171 +++++--- tests/components/mqtt/test_scene.py | 70 +++- tests/components/mqtt/test_select.py | 170 +++++--- tests/components/mqtt/test_sensor.py | 280 ++++++++----- tests/components/mqtt/test_siren.py | 181 +++++--- tests/components/mqtt/test_state_vacuum.py | 153 ++++--- tests/components/mqtt/test_subscription.py | 19 +- tests/components/mqtt/test_switch.py | 174 +++++--- tests/components/mqtt/test_tag.py | 72 +++- tests/components/mqtt/test_trigger.py | 11 +- .../mqtt_json/test_device_tracker.py | 2 +- tests/components/mqtt_room/test_sensor.py | 2 +- tests/components/tasmota/test_discovery.py | 6 +- tests/conftest.py | 100 ++++- 36 files changed, 3612 insertions(+), 1692 deletions(-) diff --git a/tests/components/manual_mqtt/test_alarm_control_panel.py b/tests/components/manual_mqtt/test_alarm_control_panel.py index 3572077bd8e..4296f76d741 100644 --- a/tests/components/manual_mqtt/test_alarm_control_panel.py +++ b/tests/components/manual_mqtt/test_alarm_control_panel.py @@ -26,9 +26,9 @@ from tests.components.alarm_control_panel import common CODE = "HELLO_CODE" -async def test_fail_setup_without_state_topic(hass, mqtt_mock): +async def test_fail_setup_without_state_topic(hass, mqtt_mock_entry_with_yaml_config): """Test for failing with no state topic.""" - with assert_setup_component(0) as config: + with assert_setup_component(0, alarm_control_panel.DOMAIN) as config: assert await async_setup_component( hass, alarm_control_panel.DOMAIN, @@ -42,9 +42,9 @@ async def test_fail_setup_without_state_topic(hass, mqtt_mock): assert not config[alarm_control_panel.DOMAIN] -async def test_fail_setup_without_command_topic(hass, mqtt_mock): +async def test_fail_setup_without_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test failing with no command topic.""" - with assert_setup_component(0): + with assert_setup_component(0, alarm_control_panel.DOMAIN): assert await async_setup_component( hass, alarm_control_panel.DOMAIN, @@ -57,7 +57,7 @@ async def test_fail_setup_without_command_topic(hass, mqtt_mock): ) -async def test_arm_home_no_pending(hass, mqtt_mock): +async def test_arm_home_no_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -86,7 +86,9 @@ async def test_arm_home_no_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME -async def test_arm_home_no_pending_when_code_not_req(hass, mqtt_mock): +async def test_arm_home_no_pending_when_code_not_req( + hass, mqtt_mock_entry_with_yaml_config +): """Test arm home method.""" assert await async_setup_component( hass, @@ -116,7 +118,7 @@ async def test_arm_home_no_pending_when_code_not_req(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME -async def test_arm_home_with_pending(hass, mqtt_mock): +async def test_arm_home_with_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -158,7 +160,7 @@ async def test_arm_home_with_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME -async def test_arm_home_with_invalid_code(hass, mqtt_mock): +async def test_arm_home_with_invalid_code(hass, mqtt_mock_entry_with_yaml_config): """Attempt to arm home without a valid code.""" assert await async_setup_component( hass, @@ -187,7 +189,7 @@ async def test_arm_home_with_invalid_code(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_arm_away_no_pending(hass, mqtt_mock): +async def test_arm_away_no_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -216,7 +218,9 @@ async def test_arm_away_no_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY -async def test_arm_away_no_pending_when_code_not_req(hass, mqtt_mock): +async def test_arm_away_no_pending_when_code_not_req( + hass, mqtt_mock_entry_with_yaml_config +): """Test arm home method.""" assert await async_setup_component( hass, @@ -246,7 +250,7 @@ async def test_arm_away_no_pending_when_code_not_req(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY -async def test_arm_home_with_template_code(hass, mqtt_mock): +async def test_arm_home_with_template_code(hass, mqtt_mock_entry_with_yaml_config): """Attempt to arm with a template-based code.""" assert await async_setup_component( hass, @@ -276,7 +280,7 @@ async def test_arm_home_with_template_code(hass, mqtt_mock): assert state.state == STATE_ALARM_ARMED_HOME -async def test_arm_away_with_pending(hass, mqtt_mock): +async def test_arm_away_with_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -318,7 +322,7 @@ async def test_arm_away_with_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY -async def test_arm_away_with_invalid_code(hass, mqtt_mock): +async def test_arm_away_with_invalid_code(hass, mqtt_mock_entry_with_yaml_config): """Attempt to arm away without a valid code.""" assert await async_setup_component( hass, @@ -347,7 +351,7 @@ async def test_arm_away_with_invalid_code(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_arm_night_no_pending(hass, mqtt_mock): +async def test_arm_night_no_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm night method.""" assert await async_setup_component( hass, @@ -376,7 +380,9 @@ async def test_arm_night_no_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT -async def test_arm_night_no_pending_when_code_not_req(hass, mqtt_mock): +async def test_arm_night_no_pending_when_code_not_req( + hass, mqtt_mock_entry_with_yaml_config +): """Test arm night method.""" assert await async_setup_component( hass, @@ -406,7 +412,7 @@ async def test_arm_night_no_pending_when_code_not_req(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT -async def test_arm_night_with_pending(hass, mqtt_mock): +async def test_arm_night_with_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm night method.""" assert await async_setup_component( hass, @@ -454,7 +460,7 @@ async def test_arm_night_with_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT -async def test_arm_night_with_invalid_code(hass, mqtt_mock): +async def test_arm_night_with_invalid_code(hass, mqtt_mock_entry_with_yaml_config): """Attempt to arm night without a valid code.""" assert await async_setup_component( hass, @@ -483,7 +489,7 @@ async def test_arm_night_with_invalid_code(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_no_pending(hass, mqtt_mock): +async def test_trigger_no_pending(hass, mqtt_mock_entry_with_yaml_config): """Test triggering when no pending submitted method.""" assert await async_setup_component( hass, @@ -521,7 +527,7 @@ async def test_trigger_no_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED -async def test_trigger_with_delay(hass, mqtt_mock): +async def test_trigger_with_delay(hass, mqtt_mock_entry_with_yaml_config): """Test trigger method and switch from pending to triggered.""" assert await async_setup_component( hass, @@ -569,7 +575,7 @@ async def test_trigger_with_delay(hass, mqtt_mock): assert state.state == STATE_ALARM_TRIGGERED -async def test_trigger_zero_trigger_time(hass, mqtt_mock): +async def test_trigger_zero_trigger_time(hass, mqtt_mock_entry_with_yaml_config): """Test disabled trigger.""" assert await async_setup_component( hass, @@ -598,7 +604,9 @@ async def test_trigger_zero_trigger_time(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_zero_trigger_time_with_pending(hass, mqtt_mock): +async def test_trigger_zero_trigger_time_with_pending( + hass, mqtt_mock_entry_with_yaml_config +): """Test disabled trigger.""" assert await async_setup_component( hass, @@ -627,7 +635,7 @@ async def test_trigger_zero_trigger_time_with_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_with_pending(hass, mqtt_mock): +async def test_trigger_with_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -679,7 +687,9 @@ async def test_trigger_with_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_with_disarm_after_trigger(hass, mqtt_mock): +async def test_trigger_with_disarm_after_trigger( + hass, mqtt_mock_entry_with_yaml_config +): """Test disarm after trigger.""" assert await async_setup_component( hass, @@ -718,7 +728,9 @@ async def test_trigger_with_disarm_after_trigger(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_with_zero_specific_trigger_time(hass, mqtt_mock): +async def test_trigger_with_zero_specific_trigger_time( + hass, mqtt_mock_entry_with_yaml_config +): """Test trigger method.""" assert await async_setup_component( hass, @@ -748,7 +760,9 @@ async def test_trigger_with_zero_specific_trigger_time(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_with_unused_zero_specific_trigger_time(hass, mqtt_mock): +async def test_trigger_with_unused_zero_specific_trigger_time( + hass, mqtt_mock_entry_with_yaml_config +): """Test disarm after trigger.""" assert await async_setup_component( hass, @@ -788,7 +802,9 @@ async def test_trigger_with_unused_zero_specific_trigger_time(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_trigger_with_specific_trigger_time(hass, mqtt_mock): +async def test_trigger_with_specific_trigger_time( + hass, mqtt_mock_entry_with_yaml_config +): """Test disarm after trigger.""" assert await async_setup_component( hass, @@ -827,7 +843,9 @@ async def test_trigger_with_specific_trigger_time(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass, mqtt_mock): +async def test_back_to_back_trigger_with_no_disarm_after_trigger( + hass, mqtt_mock_entry_with_yaml_config +): """Test no disarm after back to back trigger.""" assert await async_setup_component( hass, @@ -886,7 +904,7 @@ async def test_back_to_back_trigger_with_no_disarm_after_trigger(hass, mqtt_mock assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY -async def test_disarm_while_pending_trigger(hass, mqtt_mock): +async def test_disarm_while_pending_trigger(hass, mqtt_mock_entry_with_yaml_config): """Test disarming while pending state.""" assert await async_setup_component( hass, @@ -929,7 +947,9 @@ async def test_disarm_while_pending_trigger(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_disarm_during_trigger_with_invalid_code(hass, mqtt_mock): +async def test_disarm_during_trigger_with_invalid_code( + hass, mqtt_mock_entry_with_yaml_config +): """Test disarming while code is invalid.""" assert await async_setup_component( hass, @@ -973,7 +993,9 @@ async def test_disarm_during_trigger_with_invalid_code(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED -async def test_trigger_with_unused_specific_delay(hass, mqtt_mock): +async def test_trigger_with_unused_specific_delay( + hass, mqtt_mock_entry_with_yaml_config +): """Test trigger method and switch from pending to triggered.""" assert await async_setup_component( hass, @@ -1022,7 +1044,7 @@ async def test_trigger_with_unused_specific_delay(hass, mqtt_mock): assert state.state == STATE_ALARM_TRIGGERED -async def test_trigger_with_specific_delay(hass, mqtt_mock): +async def test_trigger_with_specific_delay(hass, mqtt_mock_entry_with_yaml_config): """Test trigger method and switch from pending to triggered.""" assert await async_setup_component( hass, @@ -1071,7 +1093,7 @@ async def test_trigger_with_specific_delay(hass, mqtt_mock): assert state.state == STATE_ALARM_TRIGGERED -async def test_trigger_with_pending_and_delay(hass, mqtt_mock): +async def test_trigger_with_pending_and_delay(hass, mqtt_mock_entry_with_yaml_config): """Test trigger method and switch from pending to triggered.""" assert await async_setup_component( hass, @@ -1132,7 +1154,9 @@ async def test_trigger_with_pending_and_delay(hass, mqtt_mock): assert state.state == STATE_ALARM_TRIGGERED -async def test_trigger_with_pending_and_specific_delay(hass, mqtt_mock): +async def test_trigger_with_pending_and_specific_delay( + hass, mqtt_mock_entry_with_yaml_config +): """Test trigger method and switch from pending to triggered.""" assert await async_setup_component( hass, @@ -1194,7 +1218,7 @@ async def test_trigger_with_pending_and_specific_delay(hass, mqtt_mock): assert state.state == STATE_ALARM_TRIGGERED -async def test_armed_home_with_specific_pending(hass, mqtt_mock): +async def test_armed_home_with_specific_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -1230,7 +1254,7 @@ async def test_armed_home_with_specific_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME -async def test_armed_away_with_specific_pending(hass, mqtt_mock): +async def test_armed_away_with_specific_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -1266,7 +1290,9 @@ async def test_armed_away_with_specific_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY -async def test_armed_night_with_specific_pending(hass, mqtt_mock): +async def test_armed_night_with_specific_pending( + hass, mqtt_mock_entry_with_yaml_config +): """Test arm home method.""" assert await async_setup_component( hass, @@ -1302,7 +1328,7 @@ async def test_armed_night_with_specific_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT -async def test_trigger_with_specific_pending(hass, mqtt_mock): +async def test_trigger_with_specific_pending(hass, mqtt_mock_entry_with_yaml_config): """Test arm home method.""" assert await async_setup_component( hass, @@ -1350,7 +1376,7 @@ async def test_trigger_with_specific_pending(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_arm_away_after_disabled_disarmed(hass, mqtt_mock): +async def test_arm_away_after_disabled_disarmed(hass, mqtt_mock_entry_with_yaml_config): """Test pending state with and without zero trigger time.""" assert await async_setup_component( hass, @@ -1417,7 +1443,7 @@ async def test_arm_away_after_disabled_disarmed(hass, mqtt_mock): assert state.state == STATE_ALARM_TRIGGERED -async def test_disarm_with_template_code(hass, mqtt_mock): +async def test_disarm_with_template_code(hass, mqtt_mock_entry_with_yaml_config): """Attempt to disarm with a valid or invalid template-based code.""" assert await async_setup_component( hass, @@ -1459,7 +1485,7 @@ async def test_disarm_with_template_code(hass, mqtt_mock): assert state.state == STATE_ALARM_DISARMED -async def test_arm_home_via_command_topic(hass, mqtt_mock): +async def test_arm_home_via_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test arming home via command topic.""" assert await async_setup_component( hass, @@ -1498,7 +1524,7 @@ async def test_arm_home_via_command_topic(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_HOME -async def test_arm_away_via_command_topic(hass, mqtt_mock): +async def test_arm_away_via_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test arming away via command topic.""" assert await async_setup_component( hass, @@ -1537,7 +1563,7 @@ async def test_arm_away_via_command_topic(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_AWAY -async def test_arm_night_via_command_topic(hass, mqtt_mock): +async def test_arm_night_via_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test arming night via command topic.""" assert await async_setup_component( hass, @@ -1576,7 +1602,7 @@ async def test_arm_night_via_command_topic(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_ARMED_NIGHT -async def test_disarm_pending_via_command_topic(hass, mqtt_mock): +async def test_disarm_pending_via_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test disarming pending alarm via command topic.""" assert await async_setup_component( hass, @@ -1610,7 +1636,9 @@ async def test_disarm_pending_via_command_topic(hass, mqtt_mock): assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED -async def test_state_changes_are_published_to_mqtt(hass, mqtt_mock): +async def test_state_changes_are_published_to_mqtt( + hass, mqtt_mock_entry_with_yaml_config +): """Test publishing of MQTT messages when state changes.""" assert await async_setup_component( hass, @@ -1630,6 +1658,7 @@ async def test_state_changes_are_published_to_mqtt(hass, mqtt_mock): # Component should send disarmed alarm state on startup await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() mqtt_mock.async_publish.assert_called_once_with( "alarm/state", STATE_ALARM_DISARMED, 0, True ) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index f4d76d5474c..2b013ddf8dd 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -112,9 +112,9 @@ DEFAULT_CONFIG_REMOTE_CODE_TEXT = { } -async def test_fail_setup_without_state_topic(hass, mqtt_mock): +async def test_fail_setup_without_state_topic(hass, mqtt_mock_entry_no_yaml_config): """Test for failing with no state topic.""" - with assert_setup_component(0) as config: + with assert_setup_component(0, alarm_control_panel.DOMAIN) as config: assert await async_setup_component( hass, alarm_control_panel.DOMAIN, @@ -125,12 +125,14 @@ async def test_fail_setup_without_state_topic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert not config[alarm_control_panel.DOMAIN] -async def test_fail_setup_without_command_topic(hass, mqtt_mock): +async def test_fail_setup_without_command_topic(hass, mqtt_mock_entry_no_yaml_config): """Test failing with no command topic.""" - with assert_setup_component(0): + with assert_setup_component(0, alarm_control_panel.DOMAIN) as config: assert await async_setup_component( hass, alarm_control_panel.DOMAIN, @@ -141,9 +143,12 @@ async def test_fail_setup_without_command_topic(hass, mqtt_mock): } }, ) + await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() + assert not config[alarm_control_panel.DOMAIN] -async def test_update_state_via_state_topic(hass, mqtt_mock): +async def test_update_state_via_state_topic(hass, mqtt_mock_entry_with_yaml_config): """Test updating with via state topic.""" assert await async_setup_component( hass, @@ -151,6 +156,7 @@ async def test_update_state_via_state_topic(hass, mqtt_mock): DEFAULT_CONFIG, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() entity_id = "alarm_control_panel.test" @@ -172,7 +178,9 @@ async def test_update_state_via_state_topic(hass, mqtt_mock): assert hass.states.get(entity_id).state == state -async def test_ignore_update_state_if_unknown_via_state_topic(hass, mqtt_mock): +async def test_ignore_update_state_if_unknown_via_state_topic( + hass, mqtt_mock_entry_with_yaml_config +): """Test ignoring updates via state topic.""" assert await async_setup_component( hass, @@ -180,6 +188,7 @@ async def test_ignore_update_state_if_unknown_via_state_topic(hass, mqtt_mock): DEFAULT_CONFIG, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() entity_id = "alarm_control_panel.test" @@ -201,7 +210,9 @@ async def test_ignore_update_state_if_unknown_via_state_topic(hass, mqtt_mock): (SERVICE_ALARM_TRIGGER, "TRIGGER"), ], ) -async def test_publish_mqtt_no_code(hass, mqtt_mock, service, payload): +async def test_publish_mqtt_no_code( + hass, mqtt_mock_entry_with_yaml_config, service, payload +): """Test publishing of MQTT messages when no code is configured.""" assert await async_setup_component( hass, @@ -209,6 +220,7 @@ async def test_publish_mqtt_no_code(hass, mqtt_mock, service, payload): DEFAULT_CONFIG, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( alarm_control_panel.DOMAIN, @@ -232,7 +244,9 @@ async def test_publish_mqtt_no_code(hass, mqtt_mock, service, payload): (SERVICE_ALARM_TRIGGER, "TRIGGER"), ], ) -async def test_publish_mqtt_with_code(hass, mqtt_mock, service, payload): +async def test_publish_mqtt_with_code( + hass, mqtt_mock_entry_with_yaml_config, service, payload +): """Test publishing of MQTT messages when code is configured.""" assert await async_setup_component( hass, @@ -240,6 +254,7 @@ async def test_publish_mqtt_with_code(hass, mqtt_mock, service, payload): DEFAULT_CONFIG_CODE, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() call_count = mqtt_mock.async_publish.call_count # No code provided, should not publish @@ -282,7 +297,9 @@ async def test_publish_mqtt_with_code(hass, mqtt_mock, service, payload): (SERVICE_ALARM_TRIGGER, "TRIGGER"), ], ) -async def test_publish_mqtt_with_remote_code(hass, mqtt_mock, service, payload): +async def test_publish_mqtt_with_remote_code( + hass, mqtt_mock_entry_with_yaml_config, service, payload +): """Test publishing of MQTT messages when remode code is configured.""" assert await async_setup_component( hass, @@ -290,6 +307,7 @@ async def test_publish_mqtt_with_remote_code(hass, mqtt_mock, service, payload): DEFAULT_CONFIG_REMOTE_CODE, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() call_count = mqtt_mock.async_publish.call_count # No code provided, should not publish @@ -323,7 +341,9 @@ async def test_publish_mqtt_with_remote_code(hass, mqtt_mock, service, payload): (SERVICE_ALARM_TRIGGER, "TRIGGER"), ], ) -async def test_publish_mqtt_with_remote_code_text(hass, mqtt_mock, service, payload): +async def test_publish_mqtt_with_remote_code_text( + hass, mqtt_mock_entry_with_yaml_config, service, payload +): """Test publishing of MQTT messages when remote text code is configured.""" assert await async_setup_component( hass, @@ -331,6 +351,7 @@ async def test_publish_mqtt_with_remote_code_text(hass, mqtt_mock, service, payl DEFAULT_CONFIG_REMOTE_CODE_TEXT, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() call_count = mqtt_mock.async_publish.call_count # No code provided, should not publish @@ -365,7 +386,7 @@ async def test_publish_mqtt_with_remote_code_text(hass, mqtt_mock, service, payl ], ) async def test_publish_mqtt_with_code_required_false( - hass, mqtt_mock, service, payload, disable_code + hass, mqtt_mock_entry_with_yaml_config, service, payload, disable_code ): """Test publishing of MQTT messages when code is configured. @@ -380,6 +401,7 @@ async def test_publish_mqtt_with_code_required_false( config, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() # No code provided, should publish await hass.services.async_call( @@ -412,7 +434,9 @@ async def test_publish_mqtt_with_code_required_false( mqtt_mock.reset_mock() -async def test_disarm_publishes_mqtt_with_template(hass, mqtt_mock): +async def test_disarm_publishes_mqtt_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test publishing of MQTT messages while disarmed. When command_template set to output json @@ -428,6 +452,7 @@ async def test_disarm_publishes_mqtt_with_template(hass, mqtt_mock): config, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_alarm_disarm(hass, "0123") mqtt_mock.async_publish.assert_called_once_with( @@ -435,7 +460,9 @@ async def test_disarm_publishes_mqtt_with_template(hass, mqtt_mock): ) -async def test_update_state_via_state_topic_template(hass, mqtt_mock): +async def test_update_state_via_state_topic_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test updating with template_value via state topic.""" assert await async_setup_component( hass, @@ -456,6 +483,7 @@ async def test_update_state_via_state_topic_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("alarm_control_panel.test") assert state.state == STATE_UNKNOWN @@ -466,13 +494,14 @@ async def test_update_state_via_state_topic_template(hass, mqtt_mock): assert state.state == STATE_ALARM_ARMED_AWAY -async def test_attributes_code_number(hass, mqtt_mock): +async def test_attributes_code_number(hass, mqtt_mock_entry_with_yaml_config): """Test attributes which are not supported by the vacuum.""" config = copy.deepcopy(DEFAULT_CONFIG) config[alarm_control_panel.DOMAIN]["code"] = CODE_NUMBER assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("alarm_control_panel.test") assert ( @@ -481,13 +510,14 @@ async def test_attributes_code_number(hass, mqtt_mock): ) -async def test_attributes_remote_code_number(hass, mqtt_mock): +async def test_attributes_remote_code_number(hass, mqtt_mock_entry_with_yaml_config): """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() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("alarm_control_panel.test") assert ( @@ -496,13 +526,14 @@ async def test_attributes_remote_code_number(hass, mqtt_mock): ) -async def test_attributes_code_text(hass, mqtt_mock): +async def test_attributes_code_text(hass, mqtt_mock_entry_with_yaml_config): """Test attributes which are not supported by the vacuum.""" config = copy.deepcopy(DEFAULT_CONFIG) config[alarm_control_panel.DOMAIN]["code"] = CODE_TEXT assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("alarm_control_panel.test") assert ( @@ -511,81 +542,121 @@ async def test_attributes_code_text(hass, mqtt_mock): ) -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_CODE, ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_CODE, ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_CODE, ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG_CODE, ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, DEFAULT_CONFIG, MQTT_ALARM_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one alarm per unique_id.""" config = { alarm_control_panel.DOMAIN: [ @@ -605,18 +676,22 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, alarm_control_panel.DOMAIN, config) - - -async def test_discovery_removal_alarm(hass, mqtt_mock, caplog): - """Test removal of discovered alarm_control_panel.""" - data = json.dumps(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, data + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, alarm_control_panel.DOMAIN, config ) -async def test_discovery_update_alarm_topic_and_template(hass, mqtt_mock, caplog): +async def test_discovery_removal_alarm(hass, mqtt_mock_entry_no_yaml_config, caplog): + """Test removal of discovered alarm_control_panel.""" + data = json.dumps(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, alarm_control_panel.DOMAIN, data + ) + + +async def test_discovery_update_alarm_topic_and_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered alarm_control_panel.""" config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) @@ -639,7 +714,7 @@ async def test_discovery_update_alarm_topic_and_template(hass, mqtt_mock, caplog await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, alarm_control_panel.DOMAIN, config1, @@ -649,7 +724,9 @@ async def test_discovery_update_alarm_topic_and_template(hass, mqtt_mock, caplog ) -async def test_discovery_update_alarm_template(hass, mqtt_mock, caplog): +async def test_discovery_update_alarm_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered alarm_control_panel.""" config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) @@ -670,7 +747,7 @@ async def test_discovery_update_alarm_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, alarm_control_panel.DOMAIN, config1, @@ -680,7 +757,9 @@ async def test_discovery_update_alarm_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_unchanged_alarm(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_alarm( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered alarm_control_panel.""" config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) config1["name"] = "Beer" @@ -690,12 +769,17 @@ async def test_discovery_update_unchanged_alarm(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.alarm_control_panel.MqttAlarm.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + alarm_control_panel.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -704,7 +788,12 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ' "command_topic": "test_topic" }' ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, data1, data2 + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + alarm_control_panel.DOMAIN, + data1, + data2, ) @@ -715,11 +804,13 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ("state_topic", "disarmed"), ], ) -async def test_encoding_subscribable_topics(hass, mqtt_mock, caplog, topic, value): +async def test_encoding_subscribable_topics( + hass, mqtt_mock_entry_with_yaml_config, caplog, topic, value +): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, alarm_control_panel.DOMAIN, DEFAULT_CONFIG[alarm_control_panel.DOMAIN], @@ -728,53 +819,62 @@ async def test_encoding_subscribable_topics(hass, mqtt_mock, caplog, topic, valu ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT alarm control panel device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT alarm control panel device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + alarm_control_panel.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, alarm_control_panel.DOMAIN, DEFAULT_CONFIG, alarm_control_panel.SERVICE_ALARM_DISARM, @@ -807,7 +907,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -823,7 +923,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -837,11 +937,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = alarm_control_panel.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index e4a48b07940..37bb783d354 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -62,7 +62,9 @@ DEFAULT_CONFIG = { } -async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, caplog): +async def test_setting_sensor_value_expires_availability_topic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the expiration of the value.""" assert await async_setup_component( hass, @@ -79,6 +81,7 @@ async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNAVAILABLE @@ -89,10 +92,12 @@ async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNAVAILABLE - await expires_helper(hass, mqtt_mock, caplog) + await expires_helper(hass) -async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): +async def test_setting_sensor_value_expires( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the expiration of the value.""" assert await async_setup_component( hass, @@ -108,15 +113,16 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() # State should be unavailable since expire_after is defined and > 0 state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNAVAILABLE - await expires_helper(hass, mqtt_mock, caplog) + await expires_helper(hass) -async def expires_helper(hass, mqtt_mock, caplog): +async def expires_helper(hass): """Run the basic expiry code.""" realnow = dt_util.utcnow() now = datetime(realnow.year + 1, 1, 1, 1, tzinfo=dt_util.UTC) @@ -168,9 +174,10 @@ async def expires_helper(hass, mqtt_mock, caplog): async def test_expiration_on_discovery_and_discovery_update_of_binary_sensor( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test that binary_sensor with expire_after set behaves correctly on discovery and discovery update.""" + await mqtt_mock_entry_no_yaml_config() config = { "name": "Test", "state_topic": "test-topic", @@ -247,7 +254,9 @@ async def test_expiration_on_discovery_and_discovery_update_of_binary_sensor( assert state.state == STATE_UNAVAILABLE -async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): +async def test_setting_sensor_value_via_mqtt_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, @@ -263,6 +272,7 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") @@ -281,7 +291,9 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_invalid_sensor_value_via_mqtt_message(hass, mqtt_mock, caplog): +async def test_invalid_sensor_value_via_mqtt_message( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, @@ -297,6 +309,7 @@ async def test_invalid_sensor_value_via_mqtt_message(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") @@ -319,7 +332,9 @@ async def test_invalid_sensor_value_via_mqtt_message(hass, mqtt_mock, caplog): assert "No matching payload found for entity" in caplog.text -async def test_setting_sensor_value_via_mqtt_message_and_template(hass, mqtt_mock): +async def test_setting_sensor_value_via_mqtt_message_and_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, @@ -337,6 +352,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template(hass, mqtt_moc }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNKNOWN @@ -351,7 +367,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template(hass, mqtt_moc async def test_setting_sensor_value_via_mqtt_message_and_template2( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the setting of the value via MQTT.""" assert await async_setup_component( @@ -369,6 +385,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template2( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNKNOWN @@ -388,7 +405,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template2( async def test_setting_sensor_value_via_mqtt_message_and_template_and_raw_state_encoding( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test processing a raw value via MQTT.""" assert await async_setup_component( @@ -407,6 +424,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template_and_raw_state_ }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNKNOWN @@ -421,7 +439,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template_and_raw_state_ async def test_setting_sensor_value_via_mqtt_message_empty_template( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the setting of the value via MQTT.""" assert await async_setup_component( @@ -439,6 +457,7 @@ async def test_setting_sensor_value_via_mqtt_message_empty_template( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNKNOWN @@ -453,7 +472,7 @@ async def test_setting_sensor_value_via_mqtt_message_empty_template( assert state.state == STATE_ON -async def test_valid_device_class(hass, mqtt_mock): +async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of a valid sensor class.""" assert await async_setup_component( hass, @@ -468,12 +487,13 @@ async def test_valid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("binary_sensor.test") assert state.attributes.get("device_class") == "motion" -async def test_invalid_device_class(hass, mqtt_mock): +async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): """Test the setting of an invalid sensor class.""" assert await async_setup_component( hass, @@ -488,40 +508,43 @@ async def test_invalid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("binary_sensor.test") assert state is None -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_force_update_disabled(hass, mqtt_mock): +async def test_force_update_disabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, @@ -537,6 +560,7 @@ async def test_force_update_disabled(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() events = [] @@ -556,7 +580,7 @@ async def test_force_update_disabled(hass, mqtt_mock): assert len(events) == 1 -async def test_force_update_enabled(hass, mqtt_mock): +async def test_force_update_enabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, @@ -573,6 +597,7 @@ async def test_force_update_enabled(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() events = [] @@ -592,7 +617,7 @@ async def test_force_update_enabled(hass, mqtt_mock): assert len(events) == 2 -async def test_off_delay(hass, mqtt_mock): +async def test_off_delay(hass, mqtt_mock_entry_with_yaml_config): """Test off_delay option.""" assert await async_setup_component( hass, @@ -610,6 +635,7 @@ async def test_off_delay(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() events = [] @@ -639,42 +665,60 @@ async def test_off_delay(hass, mqtt_mock): assert len(events) == 3 -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + binary_sensor.DOMAIN, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + binary_sensor.DOMAIN, + DEFAULT_CONFIG, ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + binary_sensor.DOMAIN, + DEFAULT_CONFIG, ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one sensor per unique_id.""" config = { binary_sensor.DOMAIN: [ @@ -692,18 +736,24 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, binary_sensor.DOMAIN, config) - - -async def test_discovery_removal_binary_sensor(hass, mqtt_mock, caplog): - """Test removal of discovered binary_sensor.""" - data = json.dumps(DEFAULT_CONFIG[binary_sensor.DOMAIN]) - await help_test_discovery_removal( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, data + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, config ) -async def test_discovery_update_binary_sensor_topic_template(hass, mqtt_mock, caplog): +async def test_discovery_removal_binary_sensor( + hass, mqtt_mock_entry_no_yaml_config, caplog +): + """Test removal of discovered binary_sensor.""" + data = json.dumps(DEFAULT_CONFIG[binary_sensor.DOMAIN]) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, binary_sensor.DOMAIN, data + ) + + +async def test_discovery_update_binary_sensor_topic_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered binary_sensor.""" config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) @@ -728,7 +778,7 @@ async def test_discovery_update_binary_sensor_topic_template(hass, mqtt_mock, ca await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, binary_sensor.DOMAIN, config1, @@ -738,7 +788,9 @@ async def test_discovery_update_binary_sensor_topic_template(hass, mqtt_mock, ca ) -async def test_discovery_update_binary_sensor_template(hass, mqtt_mock, caplog): +async def test_discovery_update_binary_sensor_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered binary_sensor.""" config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) @@ -761,7 +813,7 @@ async def test_discovery_update_binary_sensor_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, binary_sensor.DOMAIN, config1, @@ -785,12 +837,18 @@ async def test_discovery_update_binary_sensor_template(hass, mqtt_mock, caplog): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, binary_sensor.DOMAIN, DEFAULT_CONFIG[binary_sensor.DOMAIN], @@ -801,7 +859,9 @@ async def test_encoding_subscribable_topics( ) -async def test_discovery_update_unchanged_binary_sensor(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_binary_sensor( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered binary_sensor.""" config1 = copy.deepcopy(DEFAULT_CONFIG[binary_sensor.DOMAIN]) config1["name"] = "Beer" @@ -811,74 +871,90 @@ async def test_discovery_update_unchanged_binary_sensor(hass, mqtt_mock, caplog) "homeassistant.components.mqtt.binary_sensor.MqttBinarySensor.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + binary_sensor.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer",' ' "off_delay": -1 }' data2 = '{ "name": "Milk",' ' "state_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, data1, data2 + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + binary_sensor.DOMAIN, + data1, + data2, ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT binary sensor device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT binary sensor device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, binary_sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG, None + hass, + mqtt_mock_entry_no_yaml_config, + binary_sensor.DOMAIN, + DEFAULT_CONFIG, + None, ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = binary_sensor.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -893,7 +969,15 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): [("ON", "on", "OFF", "off"), ("OFF", "off", "ON", "on")], ) async def test_cleanup_triggers_and_restoring_state( - hass, mqtt_mock, caplog, tmp_path, freezer, payload1, state1, payload2, state2 + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + tmp_path, + freezer, + payload1, + state1, + payload2, + state2, ): """Test cleanup old triggers at reloading and restoring the state.""" domain = binary_sensor.DOMAIN @@ -914,6 +998,8 @@ async def test_cleanup_triggers_and_restoring_state( {binary_sensor.DOMAIN: [config1, config2]}, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + async_fire_mqtt_message(hass, "test-topic1", payload1) state = hass.states.get("binary_sensor.test1") assert state.state == state1 @@ -951,7 +1037,7 @@ async def test_cleanup_triggers_and_restoring_state( async def test_skip_restoring_state_with_over_due_expire_trigger( - hass, mqtt_mock, caplog, freezer + hass, mqtt_mock_entry_with_yaml_config, caplog, freezer ): """Test restoring a state with over due expire timer.""" @@ -973,6 +1059,7 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( ), assert_setup_component(1, domain): assert await async_setup_component(hass, domain, {domain: config3}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert "Skip state recovery after reload for binary_sensor.test3" in caplog.text diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 941f08e541c..35deccf2bfe 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -42,7 +42,7 @@ DEFAULT_CONFIG = { @pytest.mark.freeze_time("2021-11-08 13:31:44+00:00") -async def test_sending_mqtt_commands(hass, mqtt_mock): +async def test_sending_mqtt_commands(hass, mqtt_mock_entry_with_yaml_config): """Test the sending MQTT commands.""" assert await async_setup_component( hass, @@ -59,6 +59,7 @@ async def test_sending_mqtt_commands(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("button.test_button") assert state.state == STATE_UNKNOWN @@ -79,7 +80,7 @@ async def test_sending_mqtt_commands(hass, mqtt_mock): assert state.state == "2021-11-08T13:31:44+00:00" -async def test_command_template(hass, mqtt_mock): +async def test_command_template(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of MQTT commands through a command template.""" assert await async_setup_component( hass, @@ -95,6 +96,7 @@ async def test_command_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("button.test") assert state.state == STATE_UNKNOWN @@ -113,21 +115,23 @@ async def test_command_template(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { button.DOMAIN: { @@ -139,11 +143,17 @@ async def test_default_availability_payload(hass, mqtt_mock): } await help_test_default_availability_payload( - hass, mqtt_mock, button.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + button.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { button.DOMAIN: { @@ -155,53 +165,67 @@ async def test_custom_availability_payload(hass, mqtt_mock): } await help_test_custom_availability_payload( - hass, mqtt_mock, button.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + button.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG, None + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG, None ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, button.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, button.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, button.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one button per unique_id.""" config = { button.DOMAIN: [ @@ -219,16 +243,20 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, button.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, button.DOMAIN, config + ) -async def test_discovery_removal_button(hass, mqtt_mock, caplog): +async def test_discovery_removal_button(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered button.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, button.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, button.DOMAIN, data + ) -async def test_discovery_update_button(hass, mqtt_mock, caplog): +async def test_discovery_update_button(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered button.""" config1 = copy.deepcopy(DEFAULT_CONFIG[button.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[button.DOMAIN]) @@ -237,7 +265,7 @@ async def test_discovery_update_button(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, button.DOMAIN, config1, @@ -245,7 +273,9 @@ async def test_discovery_update_button(hass, mqtt_mock, caplog): ) -async def test_discovery_update_unchanged_button(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_button( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered button.""" data1 = ( '{ "name": "Beer",' @@ -256,60 +286,65 @@ async def test_discovery_update_unchanged_button(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.button.MqttButton.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, button.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + button.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, button.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, button.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT button device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT button device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, button.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, button.DOMAIN, DEFAULT_CONFIG, button.SERVICE_PRESS, @@ -318,7 +353,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) -async def test_invalid_device_class(hass, mqtt_mock): +async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): """Test device_class option with invalid value.""" assert await async_setup_component( hass, @@ -333,12 +368,13 @@ async def test_invalid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("button.test") assert state is None -async def test_valid_device_class(hass, mqtt_mock): +async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test device_class option with valid values.""" assert await async_setup_component( hass, @@ -366,6 +402,7 @@ async def test_valid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("button.test_1") assert state.attributes["device_class"] == button.ButtonDeviceClass.UPDATE @@ -382,7 +419,14 @@ async def test_valid_device_class(hass, mqtt_mock): ], ) async def test_publishing_with_custom_encoding( - hass, mqtt_mock, caplog, service, topic, parameters, payload, template + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + service, + topic, + parameters, + payload, + template, ): """Test publishing MQTT payload with different encoding.""" domain = button.DOMAIN @@ -390,7 +434,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -402,11 +446,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = button.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 204103152a7..54d829ce9f9 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -46,7 +46,9 @@ DEFAULT_CONFIG = { } -async def test_run_camera_setup(hass, hass_client_no_auth, mqtt_mock): +async def test_run_camera_setup( + hass, hass_client_no_auth, mqtt_mock_entry_with_yaml_config +): """Test that it fetches the given payload.""" topic = "test/camera" await async_setup_component( @@ -55,6 +57,7 @@ async def test_run_camera_setup(hass, hass_client_no_auth, mqtt_mock): {"camera": {"platform": "mqtt", "topic": topic, "name": "Test Camera"}}, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() url = hass.states.get("camera.test_camera").attributes["entity_picture"] @@ -67,7 +70,9 @@ async def test_run_camera_setup(hass, hass_client_no_auth, mqtt_mock): assert body == "beer" -async def test_run_camera_b64_encoded(hass, hass_client_no_auth, mqtt_mock): +async def test_run_camera_b64_encoded( + hass, hass_client_no_auth, mqtt_mock_entry_with_yaml_config +): """Test that it fetches the given encoded payload.""" topic = "test/camera" await async_setup_component( @@ -83,6 +88,7 @@ async def test_run_camera_b64_encoded(hass, hass_client_no_auth, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() url = hass.states.get("camera.test_camera").attributes["entity_picture"] @@ -95,77 +101,91 @@ async def test_run_camera_b64_encoded(hass, hass_client_no_auth, mqtt_mock): assert body == "grass" -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG, MQTT_CAMERA_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + camera.DOMAIN, + DEFAULT_CONFIG, + MQTT_CAMERA_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one camera per unique_id.""" config = { camera.DOMAIN: [ @@ -183,94 +203,109 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, camera.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, camera.DOMAIN, config + ) -async def test_discovery_removal_camera(hass, mqtt_mock, caplog): +async def test_discovery_removal_camera(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered camera.""" data = json.dumps(DEFAULT_CONFIG[camera.DOMAIN]) - await help_test_discovery_removal(hass, mqtt_mock, caplog, camera.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, camera.DOMAIN, data + ) -async def test_discovery_update_camera(hass, mqtt_mock, caplog): +async def test_discovery_update_camera(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered camera.""" config1 = {"name": "Beer", "topic": "test_topic"} config2 = {"name": "Milk", "topic": "test_topic"} await help_test_discovery_update( - hass, mqtt_mock, caplog, camera.DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, camera.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_camera(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_camera( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered camera.""" data1 = '{ "name": "Beer", "topic": "test_topic"}' with patch( "homeassistant.components.mqtt.camera.MqttCamera.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, camera.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + camera.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "topic": "test_topic"}' await help_test_discovery_broken( - hass, mqtt_mock, caplog, camera.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, camera.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT camera device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT camera device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG, ["test_topic"] + hass, + mqtt_mock_entry_with_yaml_config, + camera.DOMAIN, + DEFAULT_CONFIG, + ["test_topic"], ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, camera.DOMAIN, DEFAULT_CONFIG, None, @@ -279,11 +314,13 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = camera.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 98af86248e4..77843cee777 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -106,10 +106,11 @@ DEFAULT_LEGACY_CONFIG = { } -async def test_setup_params(hass, mqtt_mock): +async def test_setup_params(hass, mqtt_mock_entry_with_yaml_config): """Test the initial parameters.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("temperature") == 21 @@ -120,12 +121,15 @@ async def test_setup_params(hass, mqtt_mock): assert state.attributes.get("max_temp") == DEFAULT_MAX_TEMP -async def test_preset_none_in_preset_modes(hass, mqtt_mock, caplog): +async def test_preset_none_in_preset_modes( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test the preset mode payload reset configuration.""" config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) config["preset_modes"].append("none") assert await async_setup_component(hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert "Invalid config for [climate.mqtt]: not a valid value" in caplog.text state = hass.states.get(ENTITY_CLIMATE) assert state is None @@ -145,21 +149,23 @@ async def test_preset_none_in_preset_modes(hass, mqtt_mock, caplog): ], ) async def test_preset_modes_deprecation_guard( - hass, mqtt_mock, caplog, parameter, config_value + hass, mqtt_mock_entry_no_yaml_config, caplog, parameter, config_value ): """Test the configuration for invalid legacy parameters.""" config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) config[parameter] = config_value assert await async_setup_component(hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state is None -async def test_supported_features(hass, mqtt_mock): +async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test the supported_features.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) support = ( @@ -174,10 +180,11 @@ async def test_supported_features(hass, mqtt_mock): assert state.attributes.get("supported_features") == support -async def test_get_hvac_modes(hass, mqtt_mock): +async def test_get_hvac_modes(hass, mqtt_mock_entry_with_yaml_config): """Test that the operation list returns the correct modes.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) modes = state.attributes.get("hvac_modes") @@ -191,13 +198,16 @@ async def test_get_hvac_modes(hass, mqtt_mock): ] == modes -async def test_set_operation_bad_attr_and_state(hass, mqtt_mock, caplog): +async def test_set_operation_bad_attr_and_state( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test setting operation mode without required attribute. Also check the state. """ assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.state == "off" @@ -210,10 +220,11 @@ async def test_set_operation_bad_attr_and_state(hass, mqtt_mock, caplog): assert state.state == "off" -async def test_set_operation(hass, mqtt_mock): +async def test_set_operation(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new operation mode.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.state == "off" @@ -224,12 +235,13 @@ async def test_set_operation(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("mode-topic", "cool", 0, False) -async def test_set_operation_pessimistic(hass, mqtt_mock): +async def test_set_operation_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting operation mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["mode_state_topic"] = "mode-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.state == "unknown" @@ -247,12 +259,13 @@ async def test_set_operation_pessimistic(hass, mqtt_mock): assert state.state == "cool" -async def test_set_operation_with_power_command(hass, mqtt_mock): +async def test_set_operation_with_power_command(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new operation mode with power command enabled.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["power_command_topic"] = "power-command" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.state == "off" @@ -273,10 +286,11 @@ async def test_set_operation_with_power_command(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_set_fan_mode_bad_attr(hass, mqtt_mock, caplog): +async def test_set_fan_mode_bad_attr(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test setting fan mode without required attribute.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("fan_mode") == "low" @@ -289,12 +303,13 @@ async def test_set_fan_mode_bad_attr(hass, mqtt_mock, caplog): assert state.attributes.get("fan_mode") == "low" -async def test_set_fan_mode_pessimistic(hass, mqtt_mock): +async def test_set_fan_mode_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new fan mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["fan_mode_state_topic"] = "fan-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("fan_mode") is None @@ -312,10 +327,11 @@ async def test_set_fan_mode_pessimistic(hass, mqtt_mock): assert state.attributes.get("fan_mode") == "high" -async def test_set_fan_mode(hass, mqtt_mock): +async def test_set_fan_mode(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new fan mode.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("fan_mode") == "low" @@ -335,13 +351,14 @@ async def test_set_fan_mode(hass, mqtt_mock): ], ) async def test_set_fan_mode_send_if_off( - hass, mqtt_mock, send_if_off, assert_async_publish + hass, mqtt_mock_entry_with_yaml_config, send_if_off, assert_async_publish ): """Test setting of fan mode if the hvac is off.""" config = copy.deepcopy(DEFAULT_CONFIG) config[CLIMATE_DOMAIN].update(send_if_off) assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() assert hass.states.get(ENTITY_CLIMATE) is not None # Turn on HVAC @@ -362,10 +379,11 @@ async def test_set_fan_mode_send_if_off( mqtt_mock.async_publish.assert_has_calls(assert_async_publish) -async def test_set_swing_mode_bad_attr(hass, mqtt_mock, caplog): +async def test_set_swing_mode_bad_attr(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test setting swing mode without required attribute.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("swing_mode") == "off" @@ -378,12 +396,13 @@ async def test_set_swing_mode_bad_attr(hass, mqtt_mock, caplog): assert state.attributes.get("swing_mode") == "off" -async def test_set_swing_pessimistic(hass, mqtt_mock): +async def test_set_swing_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting swing mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["swing_mode_state_topic"] = "swing-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("swing_mode") is None @@ -401,10 +420,11 @@ async def test_set_swing_pessimistic(hass, mqtt_mock): assert state.attributes.get("swing_mode") == "on" -async def test_set_swing(hass, mqtt_mock): +async def test_set_swing(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new swing mode.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("swing_mode") == "off" @@ -424,13 +444,14 @@ async def test_set_swing(hass, mqtt_mock): ], ) async def test_set_swing_mode_send_if_off( - hass, mqtt_mock, send_if_off, assert_async_publish + hass, mqtt_mock_entry_with_yaml_config, send_if_off, assert_async_publish ): """Test setting of swing mode if the hvac is off.""" config = copy.deepcopy(DEFAULT_CONFIG) config[CLIMATE_DOMAIN].update(send_if_off) assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() assert hass.states.get(ENTITY_CLIMATE) is not None # Turn on HVAC @@ -451,10 +472,11 @@ async def test_set_swing_mode_send_if_off( mqtt_mock.async_publish.assert_has_calls(assert_async_publish) -async def test_set_target_temperature(hass, mqtt_mock): +async def test_set_target_temperature(hass, mqtt_mock_entry_with_yaml_config): """Test setting the target temperature.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("temperature") == 21 @@ -497,13 +519,14 @@ async def test_set_target_temperature(hass, mqtt_mock): ], ) async def test_set_target_temperature_send_if_off( - hass, mqtt_mock, send_if_off, assert_async_publish + hass, mqtt_mock_entry_with_yaml_config, send_if_off, assert_async_publish ): """Test setting of target temperature if the hvac is off.""" config = copy.deepcopy(DEFAULT_CONFIG) config[CLIMATE_DOMAIN].update(send_if_off) assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() assert hass.states.get(ENTITY_CLIMATE) is not None # Turn on HVAC @@ -526,12 +549,15 @@ async def test_set_target_temperature_send_if_off( mqtt_mock.async_publish.assert_has_calls(assert_async_publish) -async def test_set_target_temperature_pessimistic(hass, mqtt_mock): +async def test_set_target_temperature_pessimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting the target temperature.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["temperature_state_topic"] = "temperature-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("temperature") is None @@ -549,10 +575,11 @@ async def test_set_target_temperature_pessimistic(hass, mqtt_mock): assert state.attributes.get("temperature") == 1701 -async def test_set_target_temperature_low_high(hass, mqtt_mock): +async def test_set_target_temperature_low_high(hass, mqtt_mock_entry_with_yaml_config): """Test setting the low/high target temperature.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_set_temperature( hass, target_temp_low=20, target_temp_high=23, entity_id=ENTITY_CLIMATE @@ -564,13 +591,16 @@ async def test_set_target_temperature_low_high(hass, mqtt_mock): mqtt_mock.async_publish.assert_any_call("temperature-high-topic", "23.0", 0, False) -async def test_set_target_temperature_low_highpessimistic(hass, mqtt_mock): +async def test_set_target_temperature_low_highpessimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting the low/high target temperature.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["temperature_low_state_topic"] = "temperature-low-state" config["climate"]["temperature_high_state_topic"] = "temperature-high-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("target_temp_low") is None @@ -601,24 +631,26 @@ async def test_set_target_temperature_low_highpessimistic(hass, mqtt_mock): assert state.attributes.get("target_temp_high") == 1703 -async def test_receive_mqtt_temperature(hass, mqtt_mock): +async def test_receive_mqtt_temperature(hass, mqtt_mock_entry_with_yaml_config): """Test getting the current temperature via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["current_temperature_topic"] = "current_temperature" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "current_temperature", "47") state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("current_temperature") == 47 -async def test_handle_action_received(hass, mqtt_mock): +async def test_handle_action_received(hass, mqtt_mock_entry_with_yaml_config): """Test getting the action received via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["action_topic"] = "action" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() # Cycle through valid modes and also check for wrong input such as "None" (str(None)) async_fire_mqtt_message(hass, "action", "None") @@ -635,11 +667,14 @@ async def test_handle_action_received(hass, mqtt_mock): assert hvac_action == action -async def test_set_preset_mode_optimistic(hass, mqtt_mock, caplog): +async def test_set_preset_mode_optimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test setting of the preset mode.""" config = copy.deepcopy(DEFAULT_CONFIG) assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "none" @@ -680,12 +715,15 @@ async def test_set_preset_mode_optimistic(hass, mqtt_mock, caplog): assert "'invalid' is not a valid preset mode" in caplog.text -async def test_set_preset_mode_pessimistic(hass, mqtt_mock, caplog): +async def test_set_preset_mode_pessimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test setting of the preset mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["preset_mode_state_topic"] = "preset-mode-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "none" @@ -725,12 +763,13 @@ async def test_set_preset_mode_pessimistic(hass, mqtt_mock, caplog): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_away_mode_pessimistic(hass, mqtt_mock): +async def test_set_away_mode_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the away mode.""" config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) config["climate"]["away_mode_state_topic"] = "away-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "none" @@ -753,7 +792,7 @@ async def test_set_away_mode_pessimistic(hass, mqtt_mock): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_away_mode(hass, mqtt_mock): +async def test_set_away_mode(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the away mode.""" config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) config["climate"]["payload_on"] = "AN" @@ -761,6 +800,7 @@ async def test_set_away_mode(hass, mqtt_mock): assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "none" @@ -795,12 +835,13 @@ async def test_set_away_mode(hass, mqtt_mock): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_hold_pessimistic(hass, mqtt_mock): +async def test_set_hold_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting the hold mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) config["climate"]["hold_state_topic"] = "hold-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("hold_mode") is None @@ -819,10 +860,11 @@ async def test_set_hold_pessimistic(hass, mqtt_mock): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_hold(hass, mqtt_mock): +async def test_set_hold(hass, mqtt_mock_entry_with_yaml_config): """Test setting the hold mode.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "none" @@ -851,10 +893,11 @@ async def test_set_hold(hass, mqtt_mock): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_preset_away(hass, mqtt_mock): +async def test_set_preset_away(hass, mqtt_mock_entry_with_yaml_config): """Test setting the hold mode and away mode.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == PRESET_NONE @@ -885,13 +928,14 @@ async def test_set_preset_away(hass, mqtt_mock): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_preset_away_pessimistic(hass, mqtt_mock): +async def test_set_preset_away_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting the hold mode and away mode in pessimistic mode.""" config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) config["climate"]["hold_state_topic"] = "hold-state" config["climate"]["away_mode_state_topic"] = "away-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == PRESET_NONE @@ -936,10 +980,11 @@ async def test_set_preset_away_pessimistic(hass, mqtt_mock): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_preset_mode_twice(hass, mqtt_mock): +async def test_set_preset_mode_twice(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the same mode twice only publishes once.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("preset_mode") == "none" @@ -952,12 +997,13 @@ async def test_set_preset_mode_twice(hass, mqtt_mock): assert state.attributes.get("preset_mode") == "hold-on" -async def test_set_aux_pessimistic(hass, mqtt_mock): +async def test_set_aux_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the aux heating in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["aux_state_topic"] = "aux-state" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("aux_heat") == "off" @@ -979,10 +1025,11 @@ async def test_set_aux_pessimistic(hass, mqtt_mock): assert state.attributes.get("aux_heat") == "off" -async def test_set_aux(hass, mqtt_mock): +async def test_set_aux(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the aux heating.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("aux_heat") == "off" @@ -998,35 +1045,39 @@ async def test_set_aux(hass, mqtt_mock): assert state.attributes.get("aux_heat") == "off" -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_get_target_temperature_low_high_with_templates(hass, mqtt_mock, caplog): +async def test_get_target_temperature_low_high_with_templates( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test getting temperature high/low with templates.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["temperature_low_state_topic"] = "temperature-state" @@ -1036,6 +1087,7 @@ async def test_get_target_temperature_low_high_with_templates(hass, mqtt_mock, c assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) @@ -1060,7 +1112,7 @@ async def test_get_target_temperature_low_high_with_templates(hass, mqtt_mock, c assert state.attributes.get("target_temp_high") == 1032 -async def test_get_with_templates(hass, mqtt_mock, caplog): +async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test getting various attributes with templates.""" config = copy.deepcopy(DEFAULT_CONFIG) # By default, just unquote the JSON-strings @@ -1081,6 +1133,7 @@ async def test_get_with_templates(hass, mqtt_mock, caplog): config["climate"]["preset_mode_state_topic"] = "current-preset-mode" assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() # Operation Mode state = hass.states.get(ENTITY_CLIMATE) @@ -1159,7 +1212,9 @@ async def test_get_with_templates(hass, mqtt_mock, caplog): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_get_with_hold_and_away_mode_and_templates(hass, mqtt_mock, caplog): +async def test_get_with_hold_and_away_mode_and_templates( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test getting various for hold and away mode attributes with templates.""" config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) config["climate"]["mode_state_topic"] = "mode-state" @@ -1172,6 +1227,7 @@ async def test_get_with_hold_and_away_mode_and_templates(hass, mqtt_mock, caplog assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() # Operation Mode state = hass.states.get(ENTITY_CLIMATE) @@ -1206,7 +1262,7 @@ async def test_get_with_hold_and_away_mode_and_templates(hass, mqtt_mock, caplog assert state.attributes.get("preset_mode") == "somemode" -async def test_set_and_templates(hass, mqtt_mock, caplog): +async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test setting various attributes with templates.""" config = copy.deepcopy(DEFAULT_CONFIG) # Create simple templates @@ -1220,6 +1276,7 @@ async def test_set_and_templates(hass, mqtt_mock, caplog): assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() # Fan Mode await common.async_set_fan_mode(hass, "high", ENTITY_CLIMATE) @@ -1284,7 +1341,9 @@ async def test_set_and_templates(hass, mqtt_mock, caplog): # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_with_away_and_hold_modes_and_templates(hass, mqtt_mock, caplog): +async def test_set_with_away_and_hold_modes_and_templates( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test setting various attributes on hold and away mode with templates.""" config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) # Create simple templates @@ -1292,6 +1351,7 @@ async def test_set_with_away_and_hold_modes_and_templates(hass, mqtt_mock, caplo assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() # Hold Mode await common.async_set_preset_mode(hass, PRESET_ECO, ENTITY_CLIMATE) @@ -1303,13 +1363,14 @@ async def test_set_with_away_and_hold_modes_and_templates(hass, mqtt_mock, caplo assert state.attributes.get("preset_mode") == PRESET_ECO -async def test_min_temp_custom(hass, mqtt_mock): +async def test_min_temp_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom min temp.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["min_temp"] = 26 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) min_temp = state.attributes.get("min_temp") @@ -1318,13 +1379,14 @@ async def test_min_temp_custom(hass, mqtt_mock): assert state.attributes.get("min_temp") == 26 -async def test_max_temp_custom(hass, mqtt_mock): +async def test_max_temp_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom max temp.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["max_temp"] = 60 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) max_temp = state.attributes.get("max_temp") @@ -1333,13 +1395,14 @@ async def test_max_temp_custom(hass, mqtt_mock): assert max_temp == 60 -async def test_temp_step_custom(hass, mqtt_mock): +async def test_temp_step_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom temp step.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["temp_step"] = 0.01 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(ENTITY_CLIMATE) temp_step = state.attributes.get("target_temp_step") @@ -1348,7 +1411,7 @@ async def test_temp_step_custom(hass, mqtt_mock): assert temp_step == 0.01 -async def test_temperature_unit(hass, mqtt_mock): +async def test_temperature_unit(hass, mqtt_mock_entry_with_yaml_config): """Test that setting temperature unit converts temperature values.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["temperature_unit"] = "F" @@ -1356,6 +1419,7 @@ async def test_temperature_unit(hass, mqtt_mock): assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "current_temperature", "77") @@ -1363,49 +1427,61 @@ async def test_temperature_unit(hass, mqtt_mock): assert state.attributes.get("current_temperature") == 25 -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG, MQTT_CLIMATE_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + CLIMATE_DOMAIN, + DEFAULT_CONFIG, + MQTT_CLIMATE_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one climate per unique_id.""" config = { CLIMATE_DOMAIN: [ @@ -1425,7 +1501,9 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, CLIMATE_DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, CLIMATE_DOMAIN, config + ) @pytest.mark.parametrize( @@ -1449,7 +1527,13 @@ async def test_unique_id(hass, mqtt_mock): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) @@ -1460,7 +1544,7 @@ async def test_encoding_subscribable_topics( del config["preset_mode_command_topic"] await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, CLIMATE_DOMAIN, config, @@ -1471,71 +1555,80 @@ async def test_encoding_subscribable_topics( ) -async def test_discovery_removal_climate(hass, mqtt_mock, caplog): +async def test_discovery_removal_climate(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered climate.""" data = json.dumps(DEFAULT_CONFIG[CLIMATE_DOMAIN]) - await help_test_discovery_removal(hass, mqtt_mock, caplog, CLIMATE_DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, CLIMATE_DOMAIN, data + ) -async def test_discovery_update_climate(hass, mqtt_mock, caplog): +async def test_discovery_update_climate(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered climate.""" config1 = {"name": "Beer"} config2 = {"name": "Milk"} await help_test_discovery_update( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, CLIMATE_DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_climate(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_climate( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered climate.""" data1 = '{ "name": "Beer" }' with patch( "homeassistant.components.mqtt.climate.MqttClimate.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + CLIMATE_DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "power_command_topic": "test_topic#" }' data2 = '{ "name": "Milk", "power_command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, CLIMATE_DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT climate device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT climate device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { CLIMATE_DOMAIN: { @@ -1546,18 +1639,22 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock): } } await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, CLIMATE_DOMAIN, config, ["test-topic", "avty-topic"] + hass, + mqtt_mock_entry_with_yaml_config, + CLIMATE_DOMAIN, + config, + ["test-topic", "avty-topic"], ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" config = { CLIMATE_DOMAIN: { @@ -1569,7 +1666,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): } await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, CLIMATE_DOMAIN, config, climate.SERVICE_TURN_ON, @@ -1579,10 +1676,11 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) -async def test_precision_default(hass, mqtt_mock): +async def test_precision_default(hass, mqtt_mock_entry_with_yaml_config): """Test that setting precision to tenths works as intended.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_set_temperature( hass, temperature=23.67, entity_id=ENTITY_CLIMATE @@ -1592,12 +1690,13 @@ async def test_precision_default(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_precision_halves(hass, mqtt_mock): +async def test_precision_halves(hass, mqtt_mock_entry_with_yaml_config): """Test that setting precision to halves works as intended.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["precision"] = 0.5 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_set_temperature( hass, temperature=23.67, entity_id=ENTITY_CLIMATE @@ -1607,12 +1706,13 @@ async def test_precision_halves(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_precision_whole(hass, mqtt_mock): +async def test_precision_whole(hass, mqtt_mock_entry_with_yaml_config): """Test that setting precision to whole works as intended.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["precision"] = 1.0 assert await async_setup_component(hass, CLIMATE_DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_set_temperature( hass, temperature=23.67, entity_id=ENTITY_CLIMATE @@ -1721,7 +1821,7 @@ async def test_precision_whole(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -1738,7 +1838,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -1750,11 +1850,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = CLIMATE_DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index b5bb5732617..50cf7beb0e0 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -46,10 +46,13 @@ DEFAULT_CONFIG_DEVICE_INFO_MAC = { _SENTINEL = object() -async def help_test_availability_when_connection_lost(hass, mqtt_mock, domain, config): +async def help_test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config, domain, config +): """Test availability after MQTT disconnection.""" assert await async_setup_component(hass, domain, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state != STATE_UNAVAILABLE @@ -62,11 +65,14 @@ async def help_test_availability_when_connection_lost(hass, mqtt_mock, domain, c assert state.state == STATE_UNAVAILABLE -async def help_test_availability_without_topic(hass, mqtt_mock, domain, config): +async def help_test_availability_without_topic( + hass, mqtt_mock_entry_with_yaml_config, domain, config +): """Test availability without defined availability topic.""" assert "availability_topic" not in config[domain] assert await async_setup_component(hass, domain, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state != STATE_UNAVAILABLE @@ -74,7 +80,7 @@ async def help_test_availability_without_topic(hass, mqtt_mock, domain, config): async def help_test_default_availability_payload( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, domain, config, no_assumed_state=False, @@ -94,6 +100,7 @@ async def help_test_default_availability_payload( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state == STATE_UNAVAILABLE @@ -124,7 +131,7 @@ async def help_test_default_availability_payload( async def help_test_default_availability_list_payload( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, domain, config, no_assumed_state=False, @@ -147,6 +154,7 @@ async def help_test_default_availability_list_payload( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state == STATE_UNAVAILABLE @@ -189,7 +197,7 @@ async def help_test_default_availability_list_payload( async def help_test_default_availability_list_payload_all( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, domain, config, no_assumed_state=False, @@ -213,6 +221,7 @@ async def help_test_default_availability_list_payload_all( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state == STATE_UNAVAILABLE @@ -256,7 +265,7 @@ async def help_test_default_availability_list_payload_all( async def help_test_default_availability_list_payload_any( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, domain, config, no_assumed_state=False, @@ -280,6 +289,7 @@ async def help_test_default_availability_list_payload_any( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state == STATE_UNAVAILABLE @@ -318,7 +328,7 @@ async def help_test_default_availability_list_payload_any( async def help_test_default_availability_list_single( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -342,6 +352,7 @@ async def help_test_default_availability_list_single( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state is None @@ -353,7 +364,7 @@ async def help_test_default_availability_list_single( async def help_test_custom_availability_payload( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, domain, config, no_assumed_state=False, @@ -375,6 +386,7 @@ async def help_test_custom_availability_payload( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state.state == STATE_UNAVAILABLE @@ -405,7 +417,7 @@ async def help_test_custom_availability_payload( async def help_test_discovery_update_availability( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, domain, config, no_assumed_state=False, @@ -416,6 +428,7 @@ async def help_test_discovery_update_availability( This is a test helper for the MQTTAvailability mixin. """ + await mqtt_mock_entry_no_yaml_config() # Add availability settings to config config1 = copy.deepcopy(config) config1[domain]["availability_topic"] = "availability-topic1" @@ -484,7 +497,7 @@ async def help_test_discovery_update_availability( async def help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, domain, config + hass, mqtt_mock_entry_with_yaml_config, domain, config ): """Test the setting of attribute via MQTT with JSON payload. @@ -499,6 +512,7 @@ async def help_test_setting_attribute_via_mqtt_json_message( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "attr-topic", '{ "val": "100" }') state = hass.states.get(f"{domain}.test") @@ -507,12 +521,13 @@ async def help_test_setting_attribute_via_mqtt_json_message( async def help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, domain, config, extra_blocked_attributes + hass, mqtt_mock_entry_no_yaml_config, domain, config, extra_blocked_attributes ): """Test the setting of blocked attribute via MQTT with JSON payload. This is a test helper for the MqttAttributes mixin. """ + await mqtt_mock_entry_no_yaml_config() extra_blocked_attributes = extra_blocked_attributes or [] # Add JSON attributes settings to config @@ -534,7 +549,9 @@ async def help_test_setting_blocked_attribute_via_mqtt_json_message( assert state.attributes.get(attr) != val -async def help_test_setting_attribute_with_template(hass, mqtt_mock, domain, config): +async def help_test_setting_attribute_with_template( + hass, mqtt_mock_entry_with_yaml_config, domain, config +): """Test the setting of attribute via MQTT with JSON payload. This is a test helper for the MqttAttributes mixin. @@ -549,6 +566,7 @@ async def help_test_setting_attribute_with_template(hass, mqtt_mock, domain, con config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message( hass, "attr-topic", json.dumps({"Timer1": {"Arm": 0, "Time": "22:18"}}) @@ -560,7 +578,7 @@ async def help_test_setting_attribute_with_template(hass, mqtt_mock, domain, con async def help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, domain, config + hass, mqtt_mock_entry_with_yaml_config, caplog, domain, config ): """Test attributes get extracted from a JSON result. @@ -575,6 +593,7 @@ async def help_test_update_with_json_attrs_not_dict( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "attr-topic", '[ "list", "of", "things"]') state = hass.states.get(f"{domain}.test") @@ -584,7 +603,7 @@ async def help_test_update_with_json_attrs_not_dict( async def help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, domain, config + hass, mqtt_mock_entry_with_yaml_config, caplog, domain, config ): """Test JSON validation of attributes. @@ -599,6 +618,7 @@ async def help_test_update_with_json_attrs_bad_JSON( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "attr-topic", "This is not JSON") @@ -607,11 +627,14 @@ async def help_test_update_with_json_attrs_bad_JSON( assert "Erroneous JSON: This is not JSON" in caplog.text -async def help_test_discovery_update_attr(hass, mqtt_mock, caplog, domain, config): +async def help_test_discovery_update_attr( + hass, mqtt_mock_entry_no_yaml_config, caplog, domain, config +): """Test update of discovered MQTTAttributes. This is a test helper for the MqttAttributes mixin. """ + await mqtt_mock_entry_no_yaml_config() # Add JSON attributes settings to config config1 = copy.deepcopy(config) config1[domain]["json_attributes_topic"] = "attr-topic1" @@ -641,18 +664,22 @@ async def help_test_discovery_update_attr(hass, mqtt_mock, caplog, domain, confi assert state.attributes.get("val") == "75" -async def help_test_unique_id(hass, mqtt_mock, domain, config): +async def help_test_unique_id(hass, mqtt_mock_entry_with_yaml_config, domain, config): """Test unique id option only creates one entity per unique_id.""" assert await async_setup_component(hass, domain, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert len(hass.states.async_entity_ids(domain)) == 1 -async def help_test_discovery_removal(hass, mqtt_mock, caplog, domain, data): +async def help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, domain, data +): """Test removal of discovered component. This is a test helper for the MqttDiscoveryUpdate mixin. """ + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() @@ -669,7 +696,7 @@ async def help_test_discovery_removal(hass, mqtt_mock, caplog, domain, data): async def help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, domain, discovery_config1, @@ -681,6 +708,7 @@ async def help_test_discovery_update( This is a test helper for the MqttDiscoveryUpdate mixin. """ + await mqtt_mock_entry_no_yaml_config() # Add some future configuration to the configurations config1 = copy.deepcopy(discovery_config1) config1["some_future_option_1"] = "future_option_1" @@ -730,12 +758,13 @@ async def help_test_discovery_update( async def help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, domain, data1, discovery_update + hass, mqtt_mock_entry_no_yaml_config, caplog, domain, data1, discovery_update ): """Test update of discovered component without changes. This is a test helper for the MqttDiscoveryUpdate mixin. """ + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data1) await hass.async_block_till_done() @@ -749,8 +778,11 @@ async def help_test_discovery_update_unchanged( assert not discovery_update.called -async def help_test_discovery_broken(hass, mqtt_mock, caplog, domain, data1, data2): +async def help_test_discovery_broken( + hass, mqtt_mock_entry_no_yaml_config, caplog, domain, data1, data2 +): """Test handling of bad discovery message.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data1) await hass.async_block_till_done() @@ -769,7 +801,7 @@ async def help_test_discovery_broken(hass, mqtt_mock, caplog, domain, data1, dat async def help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -849,6 +881,7 @@ async def help_test_encoding_subscribable_topics( hass, domain, {domain: [config1, config2, config3]} ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() expected_result = attribute_value or value @@ -899,11 +932,14 @@ async def help_test_encoding_subscribable_topics( pass -async def help_test_entity_device_info_with_identifier(hass, mqtt_mock, domain, config): +async def help_test_entity_device_info_with_identifier( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test device registry integration. This is a test helper for the MqttDiscoveryUpdate mixin. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -926,11 +962,14 @@ async def help_test_entity_device_info_with_identifier(hass, mqtt_mock, domain, assert device.configuration_url == "http://example.com" -async def help_test_entity_device_info_with_connection(hass, mqtt_mock, domain, config): +async def help_test_entity_device_info_with_connection( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test device registry integration. This is a test helper for the MqttDiscoveryUpdate mixin. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_MAC) @@ -955,8 +994,11 @@ async def help_test_entity_device_info_with_connection(hass, mqtt_mock, domain, assert device.configuration_url == "http://example.com" -async def help_test_entity_device_info_remove(hass, mqtt_mock, domain, config): +async def help_test_entity_device_info_remove( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test device registry remove.""" + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -981,11 +1023,14 @@ async def help_test_entity_device_info_remove(hass, mqtt_mock, domain, config): assert not ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique") -async def help_test_entity_device_info_update(hass, mqtt_mock, domain, config): +async def help_test_entity_device_info_update( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test device registry update. This is a test helper for the MqttDiscoveryUpdate mixin. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1012,7 +1057,7 @@ async def help_test_entity_device_info_update(hass, mqtt_mock, domain, config): async def help_test_entity_id_update_subscriptions( - hass, mqtt_mock, domain, config, topics=None + hass, mqtt_mock_entry_with_yaml_config, domain, config, topics=None ): """Test MQTT subscriptions are managed when entity_id is updated.""" # Add unique_id to config @@ -1026,16 +1071,18 @@ async def help_test_entity_id_update_subscriptions( topics = ["avty-topic", "test-topic"] assert len(topics) > 0 registry = mock_registry(hass, {}) + assert await async_setup_component( hass, domain, config, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get(f"{domain}.test") assert state is not None - assert mqtt_mock.async_subscribe.call_count == len(topics) + assert mqtt_mock.async_subscribe.call_count == len(topics) + 3 for topic in topics: mqtt_mock.async_subscribe.assert_any_call(topic, ANY, ANY, ANY) mqtt_mock.async_subscribe.reset_mock() @@ -1053,10 +1100,11 @@ async def help_test_entity_id_update_subscriptions( async def help_test_entity_id_update_discovery_update( - hass, mqtt_mock, domain, config, topic=None + hass, mqtt_mock_entry_no_yaml_config, domain, config, topic=None ): """Test MQTT discovery update after entity_id is updated.""" # Add unique_id to config + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(config) config[domain]["unique_id"] = "TOTALLY_UNIQUE" @@ -1093,11 +1141,14 @@ async def help_test_entity_id_update_discovery_update( assert state.state != STATE_UNAVAILABLE -async def help_test_entity_debug_info(hass, mqtt_mock, domain, config): +async def help_test_entity_debug_info( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test debug_info. This is a test helper for MQTT debug_info. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1127,11 +1178,14 @@ async def help_test_entity_debug_info(hass, mqtt_mock, domain, config): assert len(debug_info_data["triggers"]) == 0 -async def help_test_entity_debug_info_max_messages(hass, mqtt_mock, domain, config): +async def help_test_entity_debug_info_max_messages( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test debug_info message overflow. This is a test helper for MQTT debug_info. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1181,7 +1235,7 @@ async def help_test_entity_debug_info_max_messages(hass, mqtt_mock, domain, conf async def help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, domain, config, service, @@ -1196,6 +1250,7 @@ async def help_test_entity_debug_info_message( This is a test helper for MQTT debug_info. """ # Add device settings to config + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) config["unique_id"] = "veryunique" @@ -1290,11 +1345,14 @@ async def help_test_entity_debug_info_message( assert debug_info_data["entities"][0]["transmitted"] == expected_transmissions -async def help_test_entity_debug_info_remove(hass, mqtt_mock, domain, config): +async def help_test_entity_debug_info_remove( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test debug_info. This is a test helper for MQTT debug_info. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1333,11 +1391,14 @@ async def help_test_entity_debug_info_remove(hass, mqtt_mock, domain, config): assert entity_id not in hass.data[debug_info.DATA_MQTT_DEBUG_INFO]["entities"] -async def help_test_entity_debug_info_update_entity_id(hass, mqtt_mock, domain, config): +async def help_test_entity_debug_info_update_entity_id( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test debug_info. This is a test helper for MQTT debug_info. """ + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1389,8 +1450,11 @@ async def help_test_entity_debug_info_update_entity_id(hass, mqtt_mock, domain, ) -async def help_test_entity_disabled_by_default(hass, mqtt_mock, domain, config): +async def help_test_entity_disabled_by_default( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test device registry remove.""" + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1425,8 +1489,11 @@ async def help_test_entity_disabled_by_default(hass, mqtt_mock, domain, config): assert not dev_registry.async_get_device({("mqtt", "helloworld")}) -async def help_test_entity_category(hass, mqtt_mock, domain, config): +async def help_test_entity_category( + hass, mqtt_mock_entry_no_yaml_config, domain, config +): """Test device registry remove.""" + await mqtt_mock_entry_no_yaml_config() # Add device settings to config config = copy.deepcopy(config[domain]) config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) @@ -1468,7 +1535,7 @@ async def help_test_entity_category(hass, mqtt_mock, domain, config): async def help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -1519,6 +1586,7 @@ async def help_test_publishing_with_custom_encoding( {domain: setup_config}, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() # 1) test with default encoding await hass.services.async_call( @@ -1602,7 +1670,9 @@ async def help_test_reload_with_config(hass, caplog, tmp_path, domain, config): assert "" in caplog.text -async def help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config): +async def help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config +): """Test reloading an MQTT platform.""" # Create and test an old config of 2 entities based on the config supplied old_config_1 = copy.deepcopy(config) @@ -1614,6 +1684,7 @@ async def help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config hass, domain, {domain: [old_config_1, old_config_2]} ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get(f"{domain}.test_old_1") assert hass.states.get(f"{domain}.test_old_2") diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index f0e02ad8a3a..c9784a81f80 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -264,8 +264,9 @@ async def test_hassio_confirm(hass, mock_try_connection_success, mock_finish_set assert len(mock_finish_setup.mock_calls) == 1 -async def test_option_flow(hass, mqtt_mock, mock_try_connection): +async def test_option_flow(hass, mqtt_mock_entry_no_yaml_config, mock_try_connection): """Test config flow options.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() mock_try_connection.return_value = True config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] config_entry.data = { @@ -336,8 +337,11 @@ async def test_option_flow(hass, mqtt_mock, mock_try_connection): assert mqtt_mock.async_connect.call_count == 1 -async def test_disable_birth_will(hass, mqtt_mock, mock_try_connection): +async def test_disable_birth_will( + hass, mqtt_mock_entry_no_yaml_config, mock_try_connection +): """Test disabling birth and will.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() mock_try_connection.return_value = True config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] config_entry.data = { @@ -417,9 +421,10 @@ def get_suggested(schema, key): async def test_option_flow_default_suggested_values( - hass, mqtt_mock, mock_try_connection_success + hass, mqtt_mock_entry_no_yaml_config, mock_try_connection_success ): """Test config flow options has default/suggested values.""" + await mqtt_mock_entry_no_yaml_config() config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] config_entry.data = { mqtt.CONF_BROKER: "test-broker", diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index e130b820c1b..5796c12f3cf 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -83,7 +83,7 @@ DEFAULT_CONFIG = { } -async def test_state_via_state_topic(hass, mqtt_mock): +async def test_state_via_state_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -102,6 +102,7 @@ async def test_state_via_state_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -118,7 +119,9 @@ async def test_state_via_state_topic(hass, mqtt_mock): assert state.state == STATE_OPEN -async def test_opening_and_closing_state_via_custom_state_payload(hass, mqtt_mock): +async def test_opening_and_closing_state_via_custom_state_payload( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling opening and closing state via a custom payload.""" assert await async_setup_component( hass, @@ -139,6 +142,7 @@ async def test_opening_and_closing_state_via_custom_state_payload(hass, mqtt_moc }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -160,7 +164,9 @@ async def test_opening_and_closing_state_via_custom_state_payload(hass, mqtt_moc assert state.state == STATE_CLOSED -async def test_open_closed_state_from_position_optimistic(hass, mqtt_mock): +async def test_open_closed_state_from_position_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the state after setting the position using optimistic mode.""" assert await async_setup_component( hass, @@ -180,6 +186,7 @@ async def test_open_closed_state_from_position_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -207,7 +214,7 @@ async def test_open_closed_state_from_position_optimistic(hass, mqtt_mock): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_position_via_position_topic(hass, mqtt_mock): +async def test_position_via_position_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -228,6 +235,7 @@ async def test_position_via_position_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -244,7 +252,7 @@ async def test_position_via_position_topic(hass, mqtt_mock): assert state.state == STATE_OPEN -async def test_state_via_template(hass, mqtt_mock): +async def test_state_via_template(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -266,6 +274,7 @@ async def test_state_via_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -281,7 +290,7 @@ async def test_state_via_template(hass, mqtt_mock): assert state.state == STATE_CLOSED -async def test_state_via_template_and_entity_id(hass, mqtt_mock): +async def test_state_via_template_and_entity_id(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -303,6 +312,7 @@ async def test_state_via_template_and_entity_id(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -320,7 +330,9 @@ async def test_state_via_template_and_entity_id(hass, mqtt_mock): assert state.state == STATE_CLOSED -async def test_state_via_template_with_json_value(hass, mqtt_mock, caplog): +async def test_state_via_template_with_json_value( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling state via topic with JSON value.""" assert await async_setup_component( hass, @@ -337,6 +349,7 @@ async def test_state_via_template_with_json_value(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -359,7 +372,9 @@ async def test_state_via_template_with_json_value(hass, mqtt_mock, caplog): ) in caplog.text -async def test_position_via_template_and_entity_id(hass, mqtt_mock): +async def test_position_via_template_and_entity_id( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -381,6 +396,7 @@ async def test_position_via_template_and_entity_id(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -411,7 +427,9 @@ async def test_position_via_template_and_entity_id(hass, mqtt_mock): ({"tilt_command_topic": "abc", "tilt_status_topic": "abc"}, False), ], ) -async def test_optimistic_flag(hass, mqtt_mock, config, assumed_state): +async def test_optimistic_flag( + hass, mqtt_mock_entry_with_yaml_config, config, assumed_state +): """Test assumed_state is set correctly.""" assert await async_setup_component( hass, @@ -419,6 +437,7 @@ async def test_optimistic_flag(hass, mqtt_mock, config, assumed_state): {cover.DOMAIN: {**config, "platform": "mqtt", "name": "test", "qos": 0}}, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -428,7 +447,7 @@ async def test_optimistic_flag(hass, mqtt_mock, config, assumed_state): assert ATTR_ASSUMED_STATE not in state.attributes -async def test_optimistic_state_change(hass, mqtt_mock): +async def test_optimistic_state_change(hass, mqtt_mock_entry_with_yaml_config): """Test changing state optimistically.""" assert await async_setup_component( hass, @@ -443,6 +462,7 @@ async def test_optimistic_state_change(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -484,7 +504,9 @@ async def test_optimistic_state_change(hass, mqtt_mock): assert state.state == STATE_CLOSED -async def test_optimistic_state_change_with_position(hass, mqtt_mock): +async def test_optimistic_state_change_with_position( + hass, mqtt_mock_entry_with_yaml_config +): """Test changing state optimistically.""" assert await async_setup_component( hass, @@ -501,6 +523,7 @@ async def test_optimistic_state_change_with_position(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -547,7 +570,7 @@ async def test_optimistic_state_change_with_position(hass, mqtt_mock): assert state.attributes.get(ATTR_CURRENT_POSITION) == 0 -async def test_send_open_cover_command(hass, mqtt_mock): +async def test_send_open_cover_command(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of open_cover.""" assert await async_setup_component( hass, @@ -563,6 +586,7 @@ async def test_send_open_cover_command(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -576,7 +600,7 @@ async def test_send_open_cover_command(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_send_close_cover_command(hass, mqtt_mock): +async def test_send_close_cover_command(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of close_cover.""" assert await async_setup_component( hass, @@ -592,6 +616,7 @@ async def test_send_close_cover_command(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -605,7 +630,7 @@ async def test_send_close_cover_command(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_send_stop__cover_command(hass, mqtt_mock): +async def test_send_stop__cover_command(hass, mqtt_mock_entry_with_yaml_config): """Test the sending of stop_cover.""" assert await async_setup_component( hass, @@ -621,6 +646,7 @@ async def test_send_stop__cover_command(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -634,7 +660,7 @@ async def test_send_stop__cover_command(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_current_cover_position(hass, mqtt_mock): +async def test_current_cover_position(hass, mqtt_mock_entry_with_yaml_config): """Test the current cover position.""" assert await async_setup_component( hass, @@ -654,6 +680,7 @@ async def test_current_cover_position(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state_attributes_dict = hass.states.get("cover.test").attributes assert ATTR_CURRENT_POSITION not in state_attributes_dict @@ -685,7 +712,7 @@ async def test_current_cover_position(hass, mqtt_mock): assert current_cover_position == 100 -async def test_current_cover_position_inverted(hass, mqtt_mock): +async def test_current_cover_position_inverted(hass, mqtt_mock_entry_with_yaml_config): """Test the current cover position.""" assert await async_setup_component( hass, @@ -705,6 +732,7 @@ async def test_current_cover_position_inverted(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state_attributes_dict = hass.states.get("cover.test").attributes assert ATTR_CURRENT_POSITION not in state_attributes_dict @@ -747,7 +775,7 @@ async def test_current_cover_position_inverted(hass, mqtt_mock): assert hass.states.get("cover.test").state == STATE_CLOSED -async def test_optimistic_position(hass, mqtt_mock): +async def test_optimistic_position(hass, mqtt_mock_entry_no_yaml_config): """Test optimistic position is not supported.""" assert await async_setup_component( hass, @@ -762,12 +790,13 @@ async def test_optimistic_position(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("cover.test") assert state is None -async def test_position_update(hass, mqtt_mock): +async def test_position_update(hass, mqtt_mock_entry_with_yaml_config): """Test cover position update from received MQTT message.""" assert await async_setup_component( hass, @@ -788,6 +817,7 @@ async def test_position_update(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state_attributes_dict = hass.states.get("cover.test").attributes assert ATTR_CURRENT_POSITION not in state_attributes_dict @@ -809,7 +839,7 @@ async def test_position_update(hass, mqtt_mock): [("{{position-1}}", 43, "42"), ("{{100-62}}", 100, "38")], ) async def test_set_position_templated( - hass, mqtt_mock, pos_template, pos_call, pos_message + hass, mqtt_mock_entry_with_yaml_config, pos_template, pos_call, pos_message ): """Test setting cover position via template.""" assert await async_setup_component( @@ -832,6 +862,7 @@ async def test_set_position_templated( }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -845,7 +876,9 @@ async def test_set_position_templated( ) -async def test_set_position_templated_and_attributes(hass, mqtt_mock): +async def test_set_position_templated_and_attributes( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting cover position via template and using entities attributes.""" assert await async_setup_component( hass, @@ -876,6 +909,7 @@ async def test_set_position_templated_and_attributes(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -887,7 +921,7 @@ async def test_set_position_templated_and_attributes(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("set-position-topic", "5", 0, False) -async def test_set_tilt_templated(hass, mqtt_mock): +async def test_set_tilt_templated(hass, mqtt_mock_entry_with_yaml_config): """Test setting cover tilt position via template.""" assert await async_setup_component( hass, @@ -911,6 +945,7 @@ async def test_set_tilt_templated(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -924,7 +959,9 @@ async def test_set_tilt_templated(hass, mqtt_mock): ) -async def test_set_tilt_templated_and_attributes(hass, mqtt_mock): +async def test_set_tilt_templated_and_attributes( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting cover tilt position via template and using entities attributes.""" assert await async_setup_component( hass, @@ -952,6 +989,7 @@ async def test_set_tilt_templated_and_attributes(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1010,7 +1048,7 @@ async def test_set_tilt_templated_and_attributes(hass, mqtt_mock): ) -async def test_set_position_untemplated(hass, mqtt_mock): +async def test_set_position_untemplated(hass, mqtt_mock_entry_with_yaml_config): """Test setting cover position via template.""" assert await async_setup_component( hass, @@ -1029,6 +1067,7 @@ async def test_set_position_untemplated(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1040,7 +1079,9 @@ async def test_set_position_untemplated(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("position-topic", "62", 0, False) -async def test_set_position_untemplated_custom_percentage_range(hass, mqtt_mock): +async def test_set_position_untemplated_custom_percentage_range( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting cover position via template.""" assert await async_setup_component( hass, @@ -1061,6 +1102,7 @@ async def test_set_position_untemplated_custom_percentage_range(hass, mqtt_mock) }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1072,7 +1114,7 @@ async def test_set_position_untemplated_custom_percentage_range(hass, mqtt_mock) mqtt_mock.async_publish.assert_called_once_with("position-topic", "62", 0, False) -async def test_no_command_topic(hass, mqtt_mock): +async def test_no_command_topic(hass, mqtt_mock_entry_with_yaml_config): """Test with no command topic.""" assert await async_setup_component( hass, @@ -1091,11 +1133,12 @@ async def test_no_command_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get("cover.test").attributes["supported_features"] == 240 -async def test_no_payload_close(hass, mqtt_mock): +async def test_no_payload_close(hass, mqtt_mock_entry_with_yaml_config): """Test with no close payload.""" assert await async_setup_component( hass, @@ -1113,11 +1156,12 @@ async def test_no_payload_close(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get("cover.test").attributes["supported_features"] == 9 -async def test_no_payload_open(hass, mqtt_mock): +async def test_no_payload_open(hass, mqtt_mock_entry_with_yaml_config): """Test with no open payload.""" assert await async_setup_component( hass, @@ -1135,11 +1179,12 @@ async def test_no_payload_open(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get("cover.test").attributes["supported_features"] == 10 -async def test_no_payload_stop(hass, mqtt_mock): +async def test_no_payload_stop(hass, mqtt_mock_entry_with_yaml_config): """Test with no stop payload.""" assert await async_setup_component( hass, @@ -1157,11 +1202,12 @@ async def test_no_payload_stop(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get("cover.test").attributes["supported_features"] == 3 -async def test_with_command_topic_and_tilt(hass, mqtt_mock): +async def test_with_command_topic_and_tilt(hass, mqtt_mock_entry_with_yaml_config): """Test with command topic and tilt config.""" assert await async_setup_component( hass, @@ -1181,11 +1227,12 @@ async def test_with_command_topic_and_tilt(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get("cover.test").attributes["supported_features"] == 251 -async def test_tilt_defaults(hass, mqtt_mock): +async def test_tilt_defaults(hass, mqtt_mock_entry_with_yaml_config): """Test the defaults.""" assert await async_setup_component( hass, @@ -1206,6 +1253,7 @@ async def test_tilt_defaults(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state_attributes_dict = hass.states.get("cover.test").attributes assert ATTR_CURRENT_TILT_POSITION in state_attributes_dict @@ -1216,7 +1264,7 @@ async def test_tilt_defaults(hass, mqtt_mock): assert current_cover_position == STATE_UNKNOWN -async def test_tilt_via_invocation_defaults(hass, mqtt_mock): +async def test_tilt_via_invocation_defaults(hass, mqtt_mock_entry_with_yaml_config): """Test tilt defaults on close/open.""" assert await async_setup_component( hass, @@ -1237,6 +1285,7 @@ async def test_tilt_via_invocation_defaults(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1298,7 +1347,7 @@ async def test_tilt_via_invocation_defaults(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("tilt-command-topic", "0", 0, False) -async def test_tilt_given_value(hass, mqtt_mock): +async def test_tilt_given_value(hass, mqtt_mock_entry_with_yaml_config): """Test tilting to a given value.""" assert await async_setup_component( hass, @@ -1321,6 +1370,7 @@ async def test_tilt_given_value(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1386,7 +1436,7 @@ async def test_tilt_given_value(hass, mqtt_mock): ) -async def test_tilt_given_value_optimistic(hass, mqtt_mock): +async def test_tilt_given_value_optimistic(hass, mqtt_mock_entry_with_yaml_config): """Test tilting to a given value.""" assert await async_setup_component( hass, @@ -1410,6 +1460,7 @@ async def test_tilt_given_value_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1462,7 +1513,7 @@ async def test_tilt_given_value_optimistic(hass, mqtt_mock): ) -async def test_tilt_given_value_altered_range(hass, mqtt_mock): +async def test_tilt_given_value_altered_range(hass, mqtt_mock_entry_with_yaml_config): """Test tilting to a given value.""" assert await async_setup_component( hass, @@ -1488,6 +1539,7 @@ async def test_tilt_given_value_altered_range(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1538,7 +1590,7 @@ async def test_tilt_given_value_altered_range(hass, mqtt_mock): ) -async def test_tilt_via_topic(hass, mqtt_mock): +async def test_tilt_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test tilt by updating status via MQTT.""" assert await async_setup_component( hass, @@ -1559,6 +1611,7 @@ async def test_tilt_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "0") @@ -1575,7 +1628,7 @@ async def test_tilt_via_topic(hass, mqtt_mock): assert current_cover_tilt_position == 50 -async def test_tilt_via_topic_template(hass, mqtt_mock): +async def test_tilt_via_topic_template(hass, mqtt_mock_entry_with_yaml_config): """Test tilt by updating status via MQTT and template.""" assert await async_setup_component( hass, @@ -1599,6 +1652,7 @@ async def test_tilt_via_topic_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "99") @@ -1615,7 +1669,9 @@ async def test_tilt_via_topic_template(hass, mqtt_mock): assert current_cover_tilt_position == 50 -async def test_tilt_via_topic_template_json_value(hass, mqtt_mock, caplog): +async def test_tilt_via_topic_template_json_value( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test tilt by updating status via MQTT and template with JSON value.""" assert await async_setup_component( hass, @@ -1639,6 +1695,7 @@ async def test_tilt_via_topic_template_json_value(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", '{"Var1": 9, "Var2": 30}') @@ -1661,7 +1718,7 @@ async def test_tilt_via_topic_template_json_value(hass, mqtt_mock, caplog): ) in caplog.text -async def test_tilt_via_topic_altered_range(hass, mqtt_mock): +async def test_tilt_via_topic_altered_range(hass, mqtt_mock_entry_with_yaml_config): """Test tilt status via MQTT with altered tilt range.""" assert await async_setup_component( hass, @@ -1684,6 +1741,7 @@ async def test_tilt_via_topic_altered_range(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "0") @@ -1707,7 +1765,9 @@ async def test_tilt_via_topic_altered_range(hass, mqtt_mock): assert current_cover_tilt_position == 50 -async def test_tilt_status_out_of_range_warning(hass, caplog, mqtt_mock): +async def test_tilt_status_out_of_range_warning( + hass, caplog, mqtt_mock_entry_with_yaml_config +): """Test tilt status via MQTT tilt out of range warning message.""" assert await async_setup_component( hass, @@ -1730,6 +1790,7 @@ async def test_tilt_status_out_of_range_warning(hass, caplog, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "60") @@ -1738,7 +1799,9 @@ async def test_tilt_status_out_of_range_warning(hass, caplog, mqtt_mock): ) in caplog.text -async def test_tilt_status_not_numeric_warning(hass, caplog, mqtt_mock): +async def test_tilt_status_not_numeric_warning( + hass, caplog, mqtt_mock_entry_with_yaml_config +): """Test tilt status via MQTT tilt not numeric warning message.""" assert await async_setup_component( hass, @@ -1761,13 +1824,16 @@ async def test_tilt_status_not_numeric_warning(hass, caplog, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "abc") assert ("Payload 'abc' is not numeric") in caplog.text -async def test_tilt_via_topic_altered_range_inverted(hass, mqtt_mock): +async def test_tilt_via_topic_altered_range_inverted( + hass, mqtt_mock_entry_with_yaml_config +): """Test tilt status via MQTT with altered tilt range and inverted tilt position.""" assert await async_setup_component( hass, @@ -1790,6 +1856,7 @@ async def test_tilt_via_topic_altered_range_inverted(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "0") @@ -1813,7 +1880,9 @@ async def test_tilt_via_topic_altered_range_inverted(hass, mqtt_mock): assert current_cover_tilt_position == 50 -async def test_tilt_via_topic_template_altered_range(hass, mqtt_mock): +async def test_tilt_via_topic_template_altered_range( + hass, mqtt_mock_entry_with_yaml_config +): """Test tilt status via MQTT and template with altered tilt range.""" assert await async_setup_component( hass, @@ -1839,6 +1908,7 @@ async def test_tilt_via_topic_template_altered_range(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "tilt-status-topic", "99") @@ -1862,7 +1932,7 @@ async def test_tilt_via_topic_template_altered_range(hass, mqtt_mock): assert current_cover_tilt_position == 50 -async def test_tilt_position(hass, mqtt_mock): +async def test_tilt_position(hass, mqtt_mock_entry_with_yaml_config): """Test tilt via method invocation.""" assert await async_setup_component( hass, @@ -1883,6 +1953,7 @@ async def test_tilt_position(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1896,7 +1967,7 @@ async def test_tilt_position(hass, mqtt_mock): ) -async def test_tilt_position_templated(hass, mqtt_mock): +async def test_tilt_position_templated(hass, mqtt_mock_entry_with_yaml_config): """Test tilt position via template.""" assert await async_setup_component( hass, @@ -1918,6 +1989,7 @@ async def test_tilt_position_templated(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1931,7 +2003,7 @@ async def test_tilt_position_templated(hass, mqtt_mock): ) -async def test_tilt_position_altered_range(hass, mqtt_mock): +async def test_tilt_position_altered_range(hass, mqtt_mock_entry_with_yaml_config): """Test tilt via method invocation with altered range.""" assert await async_setup_component( hass, @@ -1956,6 +2028,7 @@ async def test_tilt_position_altered_range(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( cover.DOMAIN, @@ -1969,7 +2042,7 @@ async def test_tilt_position_altered_range(hass, mqtt_mock): ) -async def test_find_percentage_in_range_defaults(hass, mqtt_mock): +async def test_find_percentage_in_range_defaults(hass): """Test find percentage in range with default range.""" mqtt_cover = MqttCover( hass, @@ -2012,7 +2085,7 @@ async def test_find_percentage_in_range_defaults(hass, mqtt_mock): assert mqtt_cover.find_percentage_in_range(44, "cover") == 44 -async def test_find_percentage_in_range_altered(hass, mqtt_mock): +async def test_find_percentage_in_range_altered(hass): """Test find percentage in range with altered range.""" mqtt_cover = MqttCover( hass, @@ -2055,7 +2128,7 @@ async def test_find_percentage_in_range_altered(hass, mqtt_mock): assert mqtt_cover.find_percentage_in_range(120, "cover") == 40 -async def test_find_percentage_in_range_defaults_inverted(hass, mqtt_mock): +async def test_find_percentage_in_range_defaults_inverted(hass): """Test find percentage in range with default range but inverted.""" mqtt_cover = MqttCover( hass, @@ -2098,7 +2171,7 @@ async def test_find_percentage_in_range_defaults_inverted(hass, mqtt_mock): assert mqtt_cover.find_percentage_in_range(44, "cover") == 56 -async def test_find_percentage_in_range_altered_inverted(hass, mqtt_mock): +async def test_find_percentage_in_range_altered_inverted(hass): """Test find percentage in range with altered range and inverted.""" mqtt_cover = MqttCover( hass, @@ -2141,7 +2214,7 @@ async def test_find_percentage_in_range_altered_inverted(hass, mqtt_mock): assert mqtt_cover.find_percentage_in_range(120, "cover") == 60 -async def test_find_in_range_defaults(hass, mqtt_mock): +async def test_find_in_range_defaults(hass): """Test find in range with default range.""" mqtt_cover = MqttCover( hass, @@ -2184,7 +2257,7 @@ async def test_find_in_range_defaults(hass, mqtt_mock): assert mqtt_cover.find_in_range_from_percent(44, "cover") == 44 -async def test_find_in_range_altered(hass, mqtt_mock): +async def test_find_in_range_altered(hass): """Test find in range with altered range.""" mqtt_cover = MqttCover( hass, @@ -2227,7 +2300,7 @@ async def test_find_in_range_altered(hass, mqtt_mock): assert mqtt_cover.find_in_range_from_percent(40, "cover") == 120 -async def test_find_in_range_defaults_inverted(hass, mqtt_mock): +async def test_find_in_range_defaults_inverted(hass): """Test find in range with default range but inverted.""" mqtt_cover = MqttCover( hass, @@ -2270,7 +2343,7 @@ async def test_find_in_range_defaults_inverted(hass, mqtt_mock): assert mqtt_cover.find_in_range_from_percent(56, "cover") == 44 -async def test_find_in_range_altered_inverted(hass, mqtt_mock): +async def test_find_in_range_altered_inverted(hass): """Test find in range with altered range and inverted.""" mqtt_cover = MqttCover( hass, @@ -2313,35 +2386,37 @@ async def test_find_in_range_altered_inverted(hass, mqtt_mock): assert mqtt_cover.find_in_range_from_percent(60, "cover") == 120 -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_valid_device_class(hass, mqtt_mock): +async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of a valid device class.""" assert await async_setup_component( hass, @@ -2356,12 +2431,13 @@ async def test_valid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.attributes.get("device_class") == "garage" -async def test_invalid_device_class(hass, mqtt_mock): +async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): """Test the setting of an invalid device class.""" assert await async_setup_component( hass, @@ -2376,54 +2452,67 @@ async def test_invalid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("cover.test") assert state is None -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG, MQTT_COVER_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + cover.DOMAIN, + DEFAULT_CONFIG, + MQTT_COVER_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_json( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique_id option only creates one cover per id.""" config = { cover.DOMAIN: [ @@ -2441,92 +2530,103 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, cover.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, config + ) -async def test_discovery_removal_cover(hass, mqtt_mock, caplog): +async def test_discovery_removal_cover(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered cover.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, cover.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, cover.DOMAIN, data + ) -async def test_discovery_update_cover(hass, mqtt_mock, caplog): +async def test_discovery_update_cover(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered cover.""" 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, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, cover.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_cover(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_cover( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered cover.""" data1 = '{ "name": "Beer", "command_topic": "test_topic" }' with patch( "homeassistant.components.mqtt.cover.MqttCover.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, cover.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + cover.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "command_topic": "test_topic#" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, cover.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, cover.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT cover device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT cover device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, cover.DOMAIN, DEFAULT_CONFIG, SERVICE_OPEN_COVER, @@ -2535,7 +2635,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): async def test_state_and_position_topics_state_not_set_via_position_topic( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test state is not set via position topic when both state and position topics are set.""" assert await async_setup_component( @@ -2557,6 +2657,7 @@ async def test_state_and_position_topics_state_not_set_via_position_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -2593,7 +2694,9 @@ async def test_state_and_position_topics_state_not_set_via_position_topic( assert state.state == STATE_CLOSED -async def test_set_state_via_position_using_stopped_state(hass, mqtt_mock): +async def test_set_state_via_position_using_stopped_state( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via position topic using stopped state.""" assert await async_setup_component( hass, @@ -2615,6 +2718,7 @@ async def test_set_state_via_position_using_stopped_state(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN @@ -2646,7 +2750,9 @@ async def test_set_state_via_position_using_stopped_state(hass, mqtt_mock): assert state.state == STATE_OPEN -async def test_position_via_position_topic_template(hass, mqtt_mock): +async def test_position_via_position_topic_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test position by updating status via position template.""" assert await async_setup_component( hass, @@ -2664,6 +2770,7 @@ async def test_position_via_position_topic_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "99") @@ -2680,7 +2787,9 @@ async def test_position_via_position_topic_template(hass, mqtt_mock): assert current_cover_position_position == 50 -async def test_position_via_position_topic_template_json_value(hass, mqtt_mock, caplog): +async def test_position_via_position_topic_template_json_value( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test position by updating status via position template with a JSON value.""" assert await async_setup_component( hass, @@ -2698,6 +2807,7 @@ async def test_position_via_position_topic_template_json_value(hass, mqtt_mock, }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", '{"Var1": 9, "Var2": 60}') @@ -2720,7 +2830,7 @@ async def test_position_via_position_topic_template_json_value(hass, mqtt_mock, ) in caplog.text -async def test_position_template_with_entity_id(hass, mqtt_mock): +async def test_position_template_with_entity_id(hass, mqtt_mock_entry_with_yaml_config): """Test position by updating status via position template.""" assert await async_setup_component( hass, @@ -2743,6 +2853,7 @@ async def test_position_template_with_entity_id(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "10") @@ -2759,7 +2870,9 @@ async def test_position_template_with_entity_id(hass, mqtt_mock): assert current_cover_position_position == 20 -async def test_position_via_position_topic_template_return_json(hass, mqtt_mock): +async def test_position_via_position_topic_template_return_json( + hass, mqtt_mock_entry_with_yaml_config +): """Test position by updating status via position template and returning json.""" assert await async_setup_component( hass, @@ -2777,6 +2890,7 @@ async def test_position_via_position_topic_template_return_json(hass, mqtt_mock) }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "55") @@ -2787,7 +2901,7 @@ async def test_position_via_position_topic_template_return_json(hass, mqtt_mock) async def test_position_via_position_topic_template_return_json_warning( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_with_yaml_config ): """Test position by updating status via position template returning json without position attribute.""" assert await async_setup_component( @@ -2806,6 +2920,7 @@ async def test_position_via_position_topic_template_return_json_warning( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "55") @@ -2816,7 +2931,7 @@ async def test_position_via_position_topic_template_return_json_warning( async def test_position_and_tilt_via_position_topic_template_return_json( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test position and tilt by updating the position via position template.""" assert await async_setup_component( @@ -2836,6 +2951,7 @@ async def test_position_and_tilt_via_position_topic_template_return_json( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "0") @@ -2857,7 +2973,9 @@ async def test_position_and_tilt_via_position_topic_template_return_json( assert current_cover_position == 99 and current_tilt_position == 49 -async def test_position_via_position_topic_template_all_variables(hass, mqtt_mock): +async def test_position_via_position_topic_template_all_variables( + hass, mqtt_mock_entry_with_yaml_config +): """Test position by updating status via position template.""" assert await async_setup_component( hass, @@ -2886,6 +3004,7 @@ async def test_position_via_position_topic_template_all_variables(hass, mqtt_moc }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "0") @@ -2901,7 +3020,9 @@ async def test_position_via_position_topic_template_all_variables(hass, mqtt_moc assert current_cover_position == 100 -async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock): +async def test_set_state_via_stopped_state_no_position_topic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via stopped state when no position topic.""" assert await async_setup_component( hass, @@ -2923,6 +3044,7 @@ async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "state-topic", "OPEN") @@ -2951,7 +3073,7 @@ async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock): async def test_position_via_position_topic_template_return_invalid_json( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_with_yaml_config ): """Test position by updating status via position template and returning invalid json.""" assert await async_setup_component( @@ -2970,6 +3092,7 @@ async def test_position_via_position_topic_template_return_invalid_json( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "get-position-topic", "55") @@ -2977,7 +3100,7 @@ async def test_position_via_position_topic_template_return_invalid_json( async def test_set_position_topic_without_get_position_topic_error( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_no_yaml_config ): """Test error when set_position_topic is used without position_topic.""" assert await async_setup_component( @@ -2994,13 +3117,16 @@ async def test_set_position_topic_without_get_position_topic_error( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert ( f"'{CONF_SET_POSITION_TOPIC}' must be set together with '{CONF_GET_POSITION_TOPIC}'." ) in caplog.text -async def test_value_template_without_state_topic_error(hass, caplog, mqtt_mock): +async def test_value_template_without_state_topic_error( + hass, caplog, mqtt_mock_entry_no_yaml_config +): """Test error when value_template is used and state_topic is missing.""" assert await async_setup_component( hass, @@ -3015,13 +3141,16 @@ async def test_value_template_without_state_topic_error(hass, caplog, mqtt_mock) }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert ( f"'{CONF_VALUE_TEMPLATE}' must be set together with '{CONF_STATE_TOPIC}'." ) in caplog.text -async def test_position_template_without_position_topic_error(hass, caplog, mqtt_mock): +async def test_position_template_without_position_topic_error( + hass, caplog, mqtt_mock_entry_no_yaml_config +): """Test error when position_template is used and position_topic is missing.""" assert await async_setup_component( hass, @@ -3036,6 +3165,7 @@ async def test_position_template_without_position_topic_error(hass, caplog, mqtt }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert ( f"'{CONF_GET_POSITION_TEMPLATE}' must be set together with '{CONF_GET_POSITION_TOPIC}'." @@ -3044,7 +3174,7 @@ async def test_position_template_without_position_topic_error(hass, caplog, mqtt async def test_set_position_template_without_set_position_topic( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_no_yaml_config ): """Test error when set_position_template is used and set_position_topic is missing.""" assert await async_setup_component( @@ -3060,6 +3190,7 @@ async def test_set_position_template_without_set_position_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert ( f"'{CONF_SET_POSITION_TEMPLATE}' must be set together with '{CONF_SET_POSITION_TOPIC}'." @@ -3068,7 +3199,7 @@ async def test_set_position_template_without_set_position_topic( async def test_tilt_command_template_without_tilt_command_topic( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_no_yaml_config ): """Test error when tilt_command_template is used and tilt_command_topic is missing.""" assert await async_setup_component( @@ -3084,6 +3215,7 @@ async def test_tilt_command_template_without_tilt_command_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert ( f"'{CONF_TILT_COMMAND_TEMPLATE}' must be set together with '{CONF_TILT_COMMAND_TOPIC}'." @@ -3092,7 +3224,7 @@ async def test_tilt_command_template_without_tilt_command_topic( async def test_tilt_status_template_without_tilt_status_topic_topic( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_no_yaml_config ): """Test error when tilt_status_template is used and tilt_status_topic is missing.""" assert await async_setup_component( @@ -3108,6 +3240,7 @@ async def test_tilt_status_template_without_tilt_status_topic_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert ( f"'{CONF_TILT_STATUS_TEMPLATE}' must be set together with '{CONF_TILT_STATUS_TOPIC}'." @@ -3143,7 +3276,7 @@ async def test_tilt_status_template_without_tilt_status_topic_topic( ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -3158,7 +3291,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -3170,11 +3303,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = cover.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -3194,12 +3329,18 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, cover.DOMAIN, DEFAULT_CONFIG[cover.DOMAIN], diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 020fbad6166..34042105af2 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -11,7 +11,9 @@ from tests.common import async_fire_mqtt_message # Deprecated in HA Core 2022.6 -async def test_legacy_ensure_device_tracker_platform_validation(hass, mqtt_mock): +async def test_legacy_ensure_device_tracker_platform_validation( + hass, mqtt_mock_entry_with_yaml_config +): """Test if platform validation was done.""" async def mock_setup_scanner(hass, config, see, discovery_info=None): @@ -29,12 +31,17 @@ async def test_legacy_ensure_device_tracker_platform_validation(hass, mqtt_mock) assert await async_setup_component( hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}}} ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert mock_sp.call_count == 1 # Deprecated in HA Core 2022.6 -async def test_legacy_new_message(hass, mock_device_tracker_conf, mqtt_mock): +async def test_legacy_new_message( + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config +): """Test new message.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" topic = "/location/paulus" @@ -51,9 +58,10 @@ async def test_legacy_new_message(hass, mock_device_tracker_conf, mqtt_mock): # Deprecated in HA Core 2022.6 async def test_legacy_single_level_wildcard_topic( - hass, mock_device_tracker_conf, mqtt_mock + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config ): """Test single level wildcard topic.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" subscription = "/location/+/paulus" @@ -73,9 +81,10 @@ async def test_legacy_single_level_wildcard_topic( # Deprecated in HA Core 2022.6 async def test_legacy_multi_level_wildcard_topic( - hass, mock_device_tracker_conf, mqtt_mock + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config ): """Test multi level wildcard topic.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" subscription = "/location/#" @@ -95,9 +104,10 @@ async def test_legacy_multi_level_wildcard_topic( # Deprecated in HA Core 2022.6 async def test_legacy_single_level_wildcard_topic_not_matching( - hass, mock_device_tracker_conf, mqtt_mock + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config ): """Test not matching single level wildcard topic.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" subscription = "/location/+/paulus" @@ -117,9 +127,10 @@ async def test_legacy_single_level_wildcard_topic_not_matching( # Deprecated in HA Core 2022.6 async def test_legacy_multi_level_wildcard_topic_not_matching( - hass, mock_device_tracker_conf, mqtt_mock + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config ): """Test not matching multi level wildcard topic.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" subscription = "/location/#" @@ -139,9 +150,10 @@ async def test_legacy_multi_level_wildcard_topic_not_matching( # Deprecated in HA Core 2022.6 async def test_legacy_matching_custom_payload_for_home_and_not_home( - hass, mock_device_tracker_conf, mqtt_mock + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config ): """Test custom payload_home sets state to home and custom payload_not_home sets state to not_home.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" topic = "/location/paulus" @@ -172,9 +184,10 @@ async def test_legacy_matching_custom_payload_for_home_and_not_home( # Deprecated in HA Core 2022.6 async def test_legacy_not_matching_custom_payload_for_home_and_not_home( - hass, mock_device_tracker_conf, mqtt_mock + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config ): """Test not matching payload does not set state to home or not_home.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" topic = "/location/paulus" @@ -202,8 +215,11 @@ async def test_legacy_not_matching_custom_payload_for_home_and_not_home( # Deprecated in HA Core 2022.6 -async def test_legacy_matching_source_type(hass, mock_device_tracker_conf, mqtt_mock): +async def test_legacy_matching_source_type( + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config +): """Test setting source type.""" + await mqtt_mock_entry_no_yaml_config() dev_id = "paulus" entity_id = f"{DOMAIN}.{dev_id}" topic = "/location/paulus" diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py index f8ee94b58f9..31853ad1dee 100644 --- a/tests/components/mqtt/test_device_tracker_discovery.py +++ b/tests/components/mqtt/test_device_tracker_discovery.py @@ -33,8 +33,9 @@ def entity_reg(hass): return mock_registry(hass) -async def test_discover_device_tracker(hass, mqtt_mock, caplog): +async def test_discover_device_tracker(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test discovering an MQTT device tracker component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -50,8 +51,9 @@ async def test_discover_device_tracker(hass, mqtt_mock, caplog): @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -74,8 +76,11 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): assert state.name == "Beer" -async def test_non_duplicate_device_tracker_discovery(hass, mqtt_mock, caplog): +async def test_non_duplicate_device_tracker_discovery( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test for a non duplicate component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -97,8 +102,9 @@ async def test_non_duplicate_device_tracker_discovery(hass, mqtt_mock, caplog): assert "Component has already been discovered: device_tracker bla" in caplog.text -async def test_device_tracker_removal(hass, mqtt_mock, caplog): +async def test_device_tracker_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of component through empty discovery message.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -114,8 +120,9 @@ async def test_device_tracker_removal(hass, mqtt_mock, caplog): assert state is None -async def test_device_tracker_rediscover(hass, mqtt_mock, caplog): +async def test_device_tracker_rediscover(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test rediscover of removed component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -140,8 +147,11 @@ async def test_device_tracker_rediscover(hass, mqtt_mock, caplog): assert state is not None -async def test_duplicate_device_tracker_removal(hass, mqtt_mock, caplog): +async def test_duplicate_device_tracker_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test for a non duplicate component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -160,8 +170,11 @@ async def test_duplicate_device_tracker_removal(hass, mqtt_mock, caplog): ) -async def test_device_tracker_discovery_update(hass, mqtt_mock, caplog): +async def test_device_tracker_discovery_update( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test for a discovery update event.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -186,10 +199,12 @@ async def test_device_tracker_discovery_update(hass, mqtt_mock, caplog): async def test_cleanup_device_tracker( - hass, hass_ws_client, device_reg, entity_reg, mqtt_mock + hass, hass_ws_client, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config ): """Test discovered device is cleaned up when removed from registry.""" assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_no_yaml_config() ws_client = await hass_ws_client(hass) async_fire_mqtt_message( @@ -242,8 +257,11 @@ async def test_cleanup_device_tracker( ) -async def test_setting_device_tracker_value_via_mqtt_message(hass, mqtt_mock, caplog): +async def test_setting_device_tracker_value_via_mqtt_message( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test the setting of the value via MQTT.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -266,9 +284,10 @@ async def test_setting_device_tracker_value_via_mqtt_message(hass, mqtt_mock, ca async def test_setting_device_tracker_value_via_mqtt_message_and_template( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test the setting of the value via MQTT.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -290,9 +309,10 @@ async def test_setting_device_tracker_value_via_mqtt_message_and_template( async def test_setting_device_tracker_value_via_mqtt_message_and_template2( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test the setting of the value via MQTT.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -317,9 +337,10 @@ async def test_setting_device_tracker_value_via_mqtt_message_and_template2( async def test_setting_device_tracker_location_via_mqtt_message( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test the setting of the location via MQTT.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -337,9 +358,10 @@ async def test_setting_device_tracker_location_via_mqtt_message( async def test_setting_device_tracker_location_via_lat_lon_message( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test the setting of the latitude and longitude via MQTT.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/device_tracker/bla/config", @@ -391,8 +413,14 @@ async def test_setting_device_tracker_location_via_lat_lon_message( assert state.state == STATE_UNKNOWN -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, device_tracker.DOMAIN, DEFAULT_CONFIG, None + hass, + mqtt_mock_entry_no_yaml_config, + device_tracker.DOMAIN, + DEFAULT_CONFIG, + None, ) diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index ef5dd22692f..fe08c85a853 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -39,8 +39,11 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_triggers(hass, device_reg, entity_reg, mqtt_mock): +async def test_get_triggers( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test we get the expected triggers from a discovered mqtt device.""" + await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -70,8 +73,11 @@ async def test_get_triggers(hass, device_reg, entity_reg, mqtt_mock): assert_lists_same(triggers, expected_triggers) -async def test_get_unknown_triggers(hass, device_reg, entity_reg, mqtt_mock): +async def test_get_unknown_triggers( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test we don't get unknown triggers.""" + await mqtt_mock_entry_no_yaml_config() # Discover a sensor (without device triggers) data1 = ( '{ "device":{"identifiers":["0AFFD2"]},' @@ -112,8 +118,11 @@ async def test_get_unknown_triggers(hass, device_reg, entity_reg, mqtt_mock): assert_lists_same(triggers, []) -async def test_get_non_existing_triggers(hass, device_reg, entity_reg, mqtt_mock): +async def test_get_non_existing_triggers( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test getting non existing triggers.""" + await mqtt_mock_entry_no_yaml_config() # Discover a sensor (without device triggers) data1 = ( '{ "device":{"identifiers":["0AFFD2"]},' @@ -131,8 +140,11 @@ async def test_get_non_existing_triggers(hass, device_reg, entity_reg, mqtt_mock @pytest.mark.no_fail_on_log_exception -async def test_discover_bad_triggers(hass, device_reg, entity_reg, mqtt_mock): +async def test_discover_bad_triggers( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test bad discovery message.""" + await mqtt_mock_entry_no_yaml_config() # Test sending bad data data0 = ( '{ "automation_type":"trigger",' @@ -176,8 +188,11 @@ async def test_discover_bad_triggers(hass, device_reg, entity_reg, mqtt_mock): assert_lists_same(triggers, expected_triggers) -async def test_update_remove_triggers(hass, device_reg, entity_reg, mqtt_mock): +async def test_update_remove_triggers( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test triggers can be updated and removed.""" + await mqtt_mock_entry_no_yaml_config() config1 = { "automation_type": "trigger", "device": {"identifiers": ["0AFFD2"]}, @@ -240,8 +255,11 @@ async def test_update_remove_triggers(hass, device_reg, entity_reg, mqtt_mock): assert device_entry is None -async def test_if_fires_on_mqtt_message(hass, device_reg, calls, mqtt_mock): +async def test_if_fires_on_mqtt_message( + hass, device_reg, calls, mqtt_mock_entry_no_yaml_config +): """Test triggers firing.""" + await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -313,8 +331,11 @@ async def test_if_fires_on_mqtt_message(hass, device_reg, calls, mqtt_mock): assert calls[1].data["some"] == "long_press" -async def test_if_fires_on_mqtt_message_template(hass, device_reg, calls, mqtt_mock): +async def test_if_fires_on_mqtt_message_template( + hass, device_reg, calls, mqtt_mock_entry_no_yaml_config +): """Test triggers firing.""" + await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -389,9 +410,10 @@ async def test_if_fires_on_mqtt_message_template(hass, device_reg, calls, mqtt_m async def test_if_fires_on_mqtt_message_late_discover( - hass, device_reg, calls, mqtt_mock + hass, device_reg, calls, mqtt_mock_entry_no_yaml_config ): """Test triggers firing of MQTT device triggers discovered after setup.""" + await mqtt_mock_entry_no_yaml_config() data0 = ( '{ "device":{"identifiers":["0AFFD2"]},' ' "state_topic": "foobar/sensor",' @@ -472,9 +494,10 @@ async def test_if_fires_on_mqtt_message_late_discover( async def test_if_fires_on_mqtt_message_after_update( - hass, device_reg, calls, mqtt_mock + hass, device_reg, calls, mqtt_mock_entry_no_yaml_config ): """Test triggers firing after update.""" + await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -515,6 +538,7 @@ async def test_if_fires_on_mqtt_message_after_update( ] }, ) + await hass.async_block_till_done() # Fake short press. async_fire_mqtt_message(hass, "foobar/triggers/button1", "") @@ -546,8 +570,11 @@ async def test_if_fires_on_mqtt_message_after_update( assert len(calls) == 3 -async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock): +async def test_no_resubscribe_same_topic( + hass, device_reg, mqtt_mock_entry_no_yaml_config +): """Test subscription to topics without change.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -589,9 +616,10 @@ async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock): async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( - hass, device_reg, calls, mqtt_mock + hass, device_reg, calls, mqtt_mock_entry_no_yaml_config ): """Test triggers not firing after removal.""" + await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -625,6 +653,7 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( ] }, ) + await hass.async_block_till_done() # Fake short press. async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") @@ -649,10 +678,13 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( async def test_not_fires_on_mqtt_message_after_remove_from_registry( - hass, hass_ws_client, device_reg, calls, mqtt_mock + hass, hass_ws_client, device_reg, calls, mqtt_mock_entry_no_yaml_config ): """Test triggers not firing after removal.""" assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() + ws_client = await hass_ws_client(hass) data1 = ( @@ -713,8 +745,9 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( assert len(calls) == 1 -async def test_attach_remove(hass, device_reg, mqtt_mock): +async def test_attach_remove(hass, device_reg, mqtt_mock_entry_no_yaml_config): """Test attach and removal of trigger.""" + await mqtt_mock_entry_no_yaml_config() data1 = ( '{ "automation_type":"trigger",' ' "device":{"identifiers":["0AFFD2"]},' @@ -766,8 +799,9 @@ async def test_attach_remove(hass, device_reg, mqtt_mock): assert len(calls) == 1 -async def test_attach_remove_late(hass, device_reg, mqtt_mock): +async def test_attach_remove_late(hass, device_reg, mqtt_mock_entry_no_yaml_config): """Test attach and removal of trigger .""" + await mqtt_mock_entry_no_yaml_config() data0 = ( '{ "device":{"identifiers":["0AFFD2"]},' ' "state_topic": "foobar/sensor",' @@ -827,8 +861,9 @@ async def test_attach_remove_late(hass, device_reg, mqtt_mock): assert len(calls) == 1 -async def test_attach_remove_late2(hass, device_reg, mqtt_mock): +async def test_attach_remove_late2(hass, device_reg, mqtt_mock_entry_no_yaml_config): """Test attach and removal of trigger .""" + await mqtt_mock_entry_no_yaml_config() data0 = ( '{ "device":{"identifiers":["0AFFD2"]},' ' "state_topic": "foobar/sensor",' @@ -882,8 +917,9 @@ async def test_attach_remove_late2(hass, device_reg, mqtt_mock): assert len(calls) == 0 -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT device registry integration.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) data = json.dumps( @@ -915,8 +951,9 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock): assert device.sw_version == "0.1-beta" -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT device registry integration.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) data = json.dumps( @@ -946,8 +983,9 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): assert device.sw_version == "0.1-beta" -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) config = { @@ -983,8 +1021,11 @@ async def test_entity_device_info_update(hass, mqtt_mock): assert device.name == "Milk" -async def test_cleanup_trigger(hass, hass_ws_client, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_trigger( + hass, hass_ws_client, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test trigger discovery topic is cleaned when device is removed from registry.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) @@ -1034,8 +1075,11 @@ async def test_cleanup_trigger(hass, hass_ws_client, device_reg, entity_reg, mqt ) -async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test removal from device registry when trigger is removed.""" + await mqtt_mock_entry_no_yaml_config() config = { "automation_type": "trigger", "topic": "test-topic", @@ -1065,8 +1109,11 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): assert device_entry is None -async def test_cleanup_device_several_triggers(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device_several_triggers( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test removal from device registry when the last trigger is removed.""" + await mqtt_mock_entry_no_yaml_config() config1 = { "automation_type": "trigger", "topic": "test-topic", @@ -1122,11 +1169,14 @@ async def test_cleanup_device_several_triggers(hass, device_reg, entity_reg, mqt assert device_entry is None -async def test_cleanup_device_with_entity1(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device_with_entity1( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test removal from device registry for device with entity. Trigger removed first, then entity. """ + await mqtt_mock_entry_no_yaml_config() config1 = { "automation_type": "trigger", "topic": "test-topic", @@ -1178,11 +1228,14 @@ async def test_cleanup_device_with_entity1(hass, device_reg, entity_reg, mqtt_mo assert device_entry is None -async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device_with_entity2( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test removal from device registry for device with entity. Entity removed first, then trigger. """ + await mqtt_mock_entry_no_yaml_config() config1 = { "automation_type": "trigger", "topic": "test-topic", @@ -1234,11 +1287,12 @@ async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mo assert device_entry is None -async def test_trigger_debug_info(hass, mqtt_mock): +async def test_trigger_debug_info(hass, mqtt_mock_entry_no_yaml_config): """Test debug_info. This is a test helper for MQTT debug_info. """ + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) config1 = { diff --git a/tests/components/mqtt/test_diagnostics.py b/tests/components/mqtt/test_diagnostics.py index bbd42a20c87..65399a22f70 100644 --- a/tests/components/mqtt/test_diagnostics.py +++ b/tests/components/mqtt/test_diagnostics.py @@ -37,8 +37,11 @@ def device_reg(hass): return mock_device_registry(hass) -async def test_entry_diagnostics(hass, device_reg, hass_client, mqtt_mock): +async def test_entry_diagnostics( + hass, device_reg, hass_client, mqtt_mock_entry_no_yaml_config +): """Test config entry diagnostics.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] mqtt_mock.connected = True @@ -154,8 +157,11 @@ async def test_entry_diagnostics(hass, device_reg, hass_client, mqtt_mock): } ], ) -async def test_redact_diagnostics(hass, device_reg, hass_client, mqtt_mock): +async def test_redact_diagnostics( + hass, device_reg, hass_client, mqtt_mock_entry_no_yaml_config +): """Test redacting diagnostics.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() expected_config = dict(default_config) expected_config["password"] = "**REDACTED**" expected_config["username"] = "**REDACTED**" diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 9215ab651b2..df20dc031d0 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -48,8 +48,9 @@ def entity_reg(hass): "mqtt_config", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) -async def test_subscribing_config_topic(hass, mqtt_mock): +async def test_subscribing_config_topic(hass, mqtt_mock_entry_no_yaml_config): """Test setting up discovery.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] discovery_topic = "homeassistant" @@ -71,8 +72,9 @@ async def test_subscribing_config_topic(hass, mqtt_mock): ("homeassistant/binary_sensor/rörkrökare/config", True), ], ) -async def test_invalid_topic(hass, mqtt_mock, caplog, topic, log): +async def test_invalid_topic(hass, mqtt_mock_entry_no_yaml_config, caplog, topic, log): """Test sending to invalid topic.""" + await mqtt_mock_entry_no_yaml_config() with patch( "homeassistant.components.mqtt.discovery.async_dispatcher_send" ) as mock_dispatcher_send: @@ -90,8 +92,9 @@ async def test_invalid_topic(hass, mqtt_mock, caplog, topic, log): caplog.clear() -async def test_invalid_json(hass, mqtt_mock, caplog): +async def test_invalid_json(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test sending in invalid JSON.""" + await mqtt_mock_entry_no_yaml_config() with patch( "homeassistant.components.mqtt.discovery.async_dispatcher_send" ) as mock_dispatcher_send: @@ -106,8 +109,9 @@ async def test_invalid_json(hass, mqtt_mock, caplog): assert not mock_dispatcher_send.called -async def test_only_valid_components(hass, mqtt_mock, caplog): +async def test_only_valid_components(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test for a valid component.""" + await mqtt_mock_entry_no_yaml_config() with patch( "homeassistant.components.mqtt.discovery.async_dispatcher_send" ) as mock_dispatcher_send: @@ -127,8 +131,9 @@ async def test_only_valid_components(hass, mqtt_mock, caplog): assert not mock_dispatcher_send.called -async def test_correct_config_discovery(hass, mqtt_mock, caplog): +async def test_correct_config_discovery(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test sending in correct JSON.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/binary_sensor/bla/config", @@ -143,8 +148,9 @@ async def test_correct_config_discovery(hass, mqtt_mock, caplog): assert ("binary_sensor", "bla") in hass.data[ALREADY_DISCOVERED] -async def test_discover_fan(hass, mqtt_mock, caplog): +async def test_discover_fan(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test discovering an MQTT fan.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/fan/bla/config", @@ -159,8 +165,9 @@ async def test_discover_fan(hass, mqtt_mock, caplog): assert ("fan", "bla") in hass.data[ALREADY_DISCOVERED] -async def test_discover_climate(hass, mqtt_mock, caplog): +async def test_discover_climate(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test discovering an MQTT climate component.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "name": "ClimateTest",' ' "current_temperature_topic": "climate/bla/current_temp",' @@ -177,8 +184,11 @@ async def test_discover_climate(hass, mqtt_mock, caplog): assert ("climate", "bla") in hass.data[ALREADY_DISCOVERED] -async def test_discover_alarm_control_panel(hass, mqtt_mock, caplog): +async def test_discover_alarm_control_panel( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test discovering an MQTT alarm control panel component.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "name": "AlarmControlPanelTest",' ' "state_topic": "test_topic",' @@ -341,9 +351,10 @@ async def test_discover_alarm_control_panel(hass, mqtt_mock, caplog): ], ) async def test_discovery_with_object_id( - hass, mqtt_mock, caplog, topic, config, entity_id, name, domain + hass, mqtt_mock_entry_no_yaml_config, caplog, topic, config, entity_id, name, domain ): """Test discovering an MQTT entity with object_id.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message(hass, topic, config) await hass.async_block_till_done() @@ -354,8 +365,9 @@ async def test_discovery_with_object_id( assert (domain, "object bla") in hass.data[ALREADY_DISCOVERED] -async def test_discovery_incl_nodeid(hass, mqtt_mock, caplog): +async def test_discovery_incl_nodeid(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test sending in correct JSON with optional node_id included.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/binary_sensor/my_node_id/bla/config", @@ -370,8 +382,9 @@ async def test_discovery_incl_nodeid(hass, mqtt_mock, caplog): assert ("binary_sensor", "my_node_id bla") in hass.data[ALREADY_DISCOVERED] -async def test_non_duplicate_discovery(hass, mqtt_mock, caplog): +async def test_non_duplicate_discovery(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test for a non duplicate component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/binary_sensor/bla/config", @@ -393,8 +406,9 @@ async def test_non_duplicate_discovery(hass, mqtt_mock, caplog): assert "Component has already been discovered: binary_sensor bla" in caplog.text -async def test_removal(hass, mqtt_mock, caplog): +async def test_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of component through empty discovery message.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/binary_sensor/bla/config", @@ -410,8 +424,9 @@ async def test_removal(hass, mqtt_mock, caplog): assert state is None -async def test_rediscover(hass, mqtt_mock, caplog): +async def test_rediscover(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test rediscover of removed component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/binary_sensor/bla/config", @@ -436,9 +451,9 @@ async def test_rediscover(hass, mqtt_mock, caplog): assert state is not None -async def test_rapid_rediscover(hass, mqtt_mock, caplog): +async def test_rapid_rediscover(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test immediate rediscover of removed component.""" - + await mqtt_mock_entry_no_yaml_config() events = async_capture_events(hass, EVENT_STATE_CHANGED) async_fire_mqtt_message( @@ -485,9 +500,9 @@ async def test_rapid_rediscover(hass, mqtt_mock, caplog): assert events[4].data["old_state"] is None -async def test_rapid_rediscover_unique(hass, mqtt_mock, caplog): +async def test_rapid_rediscover_unique(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test immediate rediscover of removed component.""" - + await mqtt_mock_entry_no_yaml_config() events = [] @ha.callback @@ -544,9 +559,9 @@ async def test_rapid_rediscover_unique(hass, mqtt_mock, caplog): assert events[3].data["old_state"] is None -async def test_rapid_reconfigure(hass, mqtt_mock, caplog): +async def test_rapid_reconfigure(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test immediate reconfigure of added component.""" - + await mqtt_mock_entry_no_yaml_config() events = [] @ha.callback @@ -596,8 +611,9 @@ async def test_rapid_reconfigure(hass, mqtt_mock, caplog): assert events[2].data["new_state"].attributes["friendly_name"] == "Wine" -async def test_duplicate_removal(hass, mqtt_mock, caplog): +async def test_duplicate_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test for a non duplicate component.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, "homeassistant/binary_sensor/bla/config", @@ -614,8 +630,11 @@ async def test_duplicate_removal(hass, mqtt_mock, caplog): assert "Component has already been discovered: binary_sensor bla" not in caplog.text -async def test_cleanup_device(hass, hass_ws_client, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device( + hass, hass_ws_client, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test discvered device is cleaned up when entry removed from device.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) @@ -669,8 +688,11 @@ async def test_cleanup_device(hass, hass_ws_client, device_reg, entity_reg, mqtt ) -async def test_cleanup_device_mqtt(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device_mqtt( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test discvered device is cleaned up when removed through MQTT.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() data = ( '{ "device":{"identifiers":["0AFFD2"]},' ' "state_topic": "foobar/sensor",' @@ -709,10 +731,12 @@ async def test_cleanup_device_mqtt(hass, device_reg, entity_reg, mqtt_mock): async def test_cleanup_device_multiple_config_entries( - hass, hass_ws_client, device_reg, entity_reg, mqtt_mock + hass, hass_ws_client, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config ): """Test discovered device is cleaned up when entry removed from device.""" assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_no_yaml_config() ws_client = await hass_ws_client(hass) config_entry = MockConfigEntry(domain="test", data={}) @@ -804,9 +828,10 @@ async def test_cleanup_device_multiple_config_entries( async def test_cleanup_device_multiple_config_entries_mqtt( - hass, device_reg, entity_reg, mqtt_mock + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config ): """Test discovered device is cleaned up when removed through MQTT.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( @@ -880,8 +905,9 @@ async def test_cleanup_device_multiple_config_entries_mqtt( mqtt_mock.async_publish.assert_not_called() -async def test_discovery_expansion(hass, mqtt_mock, caplog): +async def test_discovery_expansion(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test expansion of abbreviated discovery payload.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "~": "some/base/topic",' ' "name": "DiscoveryExpansionTest1",' @@ -937,8 +963,9 @@ async def test_discovery_expansion(hass, mqtt_mock, caplog): assert state.state == STATE_UNAVAILABLE -async def test_discovery_expansion_2(hass, mqtt_mock, caplog): +async def test_discovery_expansion_2(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test expansion of abbreviated discovery payload.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "~": "some/base/topic",' ' "name": "DiscoveryExpansionTest1",' @@ -977,8 +1004,9 @@ async def test_discovery_expansion_2(hass, mqtt_mock, caplog): @pytest.mark.no_fail_on_log_exception -async def test_discovery_expansion_3(hass, mqtt_mock, caplog): +async def test_discovery_expansion_3(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test expansion of broken discovery payload.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "~": "some/base/topic",' ' "name": "DiscoveryExpansionTest1",' @@ -1008,9 +1036,10 @@ async def test_discovery_expansion_3(hass, mqtt_mock, caplog): async def test_discovery_expansion_without_encoding_and_value_template_1( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test expansion of raw availability payload with a template as list.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "~": "some/base/topic",' ' "name": "DiscoveryExpansionTest1",' @@ -1056,9 +1085,10 @@ async def test_discovery_expansion_without_encoding_and_value_template_1( async def test_discovery_expansion_without_encoding_and_value_template_2( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_no_yaml_config, caplog ): """Test expansion of raw availability payload with a template directly.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "~": "some/base/topic",' ' "name": "DiscoveryExpansionTest1",' @@ -1133,8 +1163,11 @@ ABBREVIATIONS_WHITE_LIST = [ ] -async def test_missing_discover_abbreviations(hass, mqtt_mock, caplog): +async def test_missing_discover_abbreviations( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Check MQTT platforms for missing abbreviations.""" + await mqtt_mock_entry_no_yaml_config() missing = [] regex = re.compile(r"(CONF_[a-zA-Z\d_]*) *= *[\'\"]([a-zA-Z\d_]*)[\'\"]") for fil in Path(mqtt.__file__).parent.rglob("*.py"): @@ -1157,8 +1190,11 @@ async def test_missing_discover_abbreviations(hass, mqtt_mock, caplog): assert not missing -async def test_no_implicit_state_topic_switch(hass, mqtt_mock, caplog): +async def test_no_implicit_state_topic_switch( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test no implicit state topic for switch.""" + await mqtt_mock_entry_no_yaml_config() data = '{ "name": "Test1",' ' "command_topic": "cmnd"' "}" async_fire_mqtt_message(hass, "homeassistant/switch/bla/config", data) @@ -1187,8 +1223,11 @@ async def test_no_implicit_state_topic_switch(hass, mqtt_mock, caplog): } ], ) -async def test_complex_discovery_topic_prefix(hass, mqtt_mock, caplog): +async def test_complex_discovery_topic_prefix( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Tests handling of discovery topic prefix with multiple slashes.""" + await mqtt_mock_entry_no_yaml_config() async_fire_mqtt_message( hass, ("my_home/homeassistant/register/binary_sensor/node1/object1/config"), @@ -1204,9 +1243,10 @@ async def test_complex_discovery_topic_prefix(hass, mqtt_mock, caplog): async def test_mqtt_integration_discovery_subscribe_unsubscribe( - hass, mqtt_client_mock, mqtt_mock + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config ): """Check MQTT integration discovery subscribe and unsubscribe.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() mock_entity_platform(hass, "config_flow.comp", None) entry = hass.config_entries.async_entries("mqtt")[0] @@ -1243,8 +1283,11 @@ async def test_mqtt_integration_discovery_subscribe_unsubscribe( assert not mqtt_client_mock.unsubscribe.called -async def test_mqtt_discovery_unsubscribe_once(hass, mqtt_client_mock, mqtt_mock): +async def test_mqtt_discovery_unsubscribe_once( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Check MQTT integration discovery unsubscribe once.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() mock_entity_platform(hass, "config_flow.comp", None) entry = hass.config_entries.async_entries("mqtt")[0] diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 1a533db63c0..145edf5ac7d 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -74,16 +74,25 @@ DEFAULT_CONFIG = { } -async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): +async def test_fail_setup_if_no_command_topic( + hass, caplog, mqtt_mock_entry_no_yaml_config +): """Test if command fails with command topic.""" assert await async_setup_component( hass, fan.DOMAIN, {fan.DOMAIN: {"platform": "mqtt", "name": "test"}} ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert hass.states.get("fan.test") is None + assert ( + "Invalid config for [fan.mqtt]: required key not provided @ data['command_topic']" + in caplog.text + ) -async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -120,6 +129,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -202,7 +212,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): async def test_controlling_state_via_topic_with_different_speed_range( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the controlling state via topic using an alternate speed range.""" assert await async_setup_component( @@ -241,6 +251,7 @@ async def test_controlling_state_via_topic_with_different_speed_range( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "percentage-state-topic1", "100") state = hass.states.get("fan.test1") @@ -264,7 +275,7 @@ async def test_controlling_state_via_topic_with_different_speed_range( async def test_controlling_state_via_topic_no_percentage_topics( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the controlling state via topic without percentage topics.""" assert await async_setup_component( @@ -289,6 +300,7 @@ async def test_controlling_state_via_topic_no_percentage_topics( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -318,7 +330,9 @@ async def test_controlling_state_via_topic_no_percentage_topics( caplog.clear() -async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic_and_json_message( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling state via topic and JSON message (percentage mode).""" assert await async_setup_component( hass, @@ -353,6 +367,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -421,7 +436,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap async def test_controlling_state_via_topic_and_json_message_shared_topic( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the controlling state via topic and JSON message using a shared topic.""" assert await async_setup_component( @@ -457,6 +472,7 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -509,7 +525,9 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( caplog.clear() -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -535,6 +553,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -630,7 +649,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_commands_with_alternate_speed_range(hass, mqtt_mock): +async def test_sending_mqtt_commands_with_alternate_speed_range( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via topic using an alternate speed range.""" assert await async_setup_component( hass, @@ -668,6 +689,7 @@ async def test_sending_mqtt_commands_with_alternate_speed_range(hass, mqtt_mock) }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_set_percentage(hass, "fan.test1", 0) mqtt_mock.async_publish.assert_called_once_with( @@ -734,7 +756,9 @@ async def test_sending_mqtt_commands_with_alternate_speed_range(hass, mqtt_mock) assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, caplog): +async def test_sending_mqtt_commands_and_optimistic_no_legacy( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test optimistic mode without state topic without legacy speed command topic.""" assert await async_setup_component( hass, @@ -755,6 +779,7 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -862,7 +887,9 @@ async def test_sending_mqtt_commands_and_optimistic_no_legacy(hass, mqtt_mock, c await common.async_turn_on(hass, "fan.test", preset_mode="freaking-high") -async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): +async def test_sending_mqtt_command_templates_( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test optimistic mode without state topic without legacy speed command topic.""" assert await async_setup_component( hass, @@ -888,6 +915,7 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -1002,7 +1030,7 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test optimistic mode without state topic without percentage command topic.""" assert await async_setup_component( @@ -1025,6 +1053,7 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -1061,7 +1090,9 @@ async def test_sending_mqtt_commands_and_optimistic_no_percentage_topic( assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, caplog): +async def test_sending_mqtt_commands_and_explicit_optimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test optimistic mode with state topic and turn on attributes.""" assert await async_setup_component( hass, @@ -1088,6 +1119,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -1305,7 +1337,13 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[fan.DOMAIN]) @@ -1315,7 +1353,7 @@ async def test_encoding_subscribable_topics( config[CONF_OSCILLATION_COMMAND_TOPIC] = "fan/some_oscillation_command_topic" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, fan.DOMAIN, config, @@ -1326,7 +1364,7 @@ async def test_encoding_subscribable_topics( ) -async def test_attributes(hass, mqtt_mock, caplog): +async def test_attributes(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test attributes.""" assert await async_setup_component( hass, @@ -1347,6 +1385,7 @@ async def test_attributes(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test") assert state.state == STATE_UNKNOWN @@ -1376,7 +1415,7 @@ async def test_attributes(hass, mqtt_mock, caplog): assert state.attributes.get(fan.ATTR_OSCILLATING) is False -async def test_supported_features(hass, mqtt_mock): +async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -1498,6 +1537,7 @@ async def test_supported_features(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("fan.test1") assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 @@ -1548,77 +1588,103 @@ async def test_supported_features(hass, mqtt_mock): assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + fan.DOMAIN, + DEFAULT_CONFIG, + True, + "state-topic", + "1", ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + fan.DOMAIN, + DEFAULT_CONFIG, + True, + "state-topic", + "1", ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG, MQTT_FAN_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + fan.DOMAIN, + DEFAULT_CONFIG, + MQTT_FAN_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_json( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique_id option only creates one fan per id.""" config = { fan.DOMAIN: [ @@ -1638,89 +1704,107 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, fan.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, config + ) -async def test_discovery_removal_fan(hass, mqtt_mock, caplog): +async def test_discovery_removal_fan(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered fan.""" data = '{ "name": "test", "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, fan.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, fan.DOMAIN, data + ) -async def test_discovery_update_fan(hass, mqtt_mock, caplog): +async def test_discovery_update_fan(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered fan.""" 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 + hass, mqtt_mock_entry_no_yaml_config, caplog, fan.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_fan(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_fan( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered fan.""" data1 = '{ "name": "Beer", "command_topic": "test_topic" }' with patch( "homeassistant.components.mqtt.fan.MqttFan.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, fan.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + fan.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic" }' - await help_test_discovery_broken(hass, mqtt_mock, caplog, fan.DOMAIN, data1, data2) + + await help_test_discovery_broken( + hass, mqtt_mock_entry_no_yaml_config, caplog, fan.DOMAIN, data1, data2 + ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, fan.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG, fan.SERVICE_TURN_ON + hass, + mqtt_mock_entry_no_yaml_config, + fan.DOMAIN, + DEFAULT_CONFIG, + fan.SERVICE_TURN_ON, ) @@ -1766,7 +1850,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -1782,7 +1866,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -1794,11 +1878,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = fan.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index bc00d3afffb..ea9a6edf0e3 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -116,7 +116,7 @@ async def async_set_humidity( await hass.services.async_call(DOMAIN, SERVICE_SET_HUMIDITY, data, blocking=True) -async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): +async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_config): """Test if command fails with command topic.""" assert await async_setup_component( hass, @@ -124,10 +124,13 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): {humidifier.DOMAIN: {"platform": "mqtt", "name": "test"}}, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert hass.states.get("humidifier.test") is None -async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -158,6 +161,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -228,7 +232,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): assert state.state == STATE_UNKNOWN -async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic_and_json_message( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, @@ -255,6 +261,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -314,7 +321,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap async def test_controlling_state_via_topic_and_json_message_shared_topic( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the controlling state via topic and JSON message using a shared topic.""" assert await async_setup_component( @@ -342,6 +349,7 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -390,7 +398,9 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( caplog.clear() -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -413,6 +423,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -483,7 +494,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock, caplog): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): +async def test_sending_mqtt_command_templates_( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Testing command templates with optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -507,6 +520,7 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -577,7 +591,9 @@ async def test_sending_mqtt_command_templates_(hass, mqtt_mock, caplog): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, caplog): +async def test_sending_mqtt_commands_and_explicit_optimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test optimistic mode with state topic and turn on attributes.""" assert await async_setup_component( hass, @@ -602,6 +618,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -701,7 +718,13 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock, ca ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[humidifier.DOMAIN]) @@ -709,7 +732,7 @@ async def test_encoding_subscribable_topics( config[CONF_MODE_COMMAND_TOPIC] = "humidifier/some_mode_command_topic" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, humidifier.DOMAIN, config, @@ -720,7 +743,7 @@ async def test_encoding_subscribable_topics( ) -async def test_attributes(hass, mqtt_mock, caplog): +async def test_attributes(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test attributes.""" assert await async_setup_component( hass, @@ -740,6 +763,7 @@ async def test_attributes(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test") assert state.state == STATE_UNKNOWN @@ -765,7 +789,7 @@ async def test_attributes(hass, mqtt_mock, caplog): assert state.attributes.get(humidifier.ATTR_MODE) is None -async def test_invalid_configurations(hass, mqtt_mock, caplog): +async def test_invalid_configurations(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test invalid configurations.""" assert await async_setup_component( hass, @@ -834,6 +858,7 @@ async def test_invalid_configurations(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert hass.states.get("humidifier.test_valid_1") is not None assert hass.states.get("humidifier.test_valid_2") is not None assert hass.states.get("humidifier.test_valid_3") is not None @@ -847,7 +872,7 @@ async def test_invalid_configurations(hass, mqtt_mock, caplog): assert hass.states.get("humidifier.test_invalid_mode_is_reset") is None -async def test_supported_features(hass, mqtt_mock): +async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test supported features.""" assert await async_setup_component( hass, @@ -896,6 +921,7 @@ async def test_supported_features(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("humidifier.test1") assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 @@ -916,81 +942,111 @@ async def test_supported_features(hass, mqtt_mock): assert state is None -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + humidifier.DOMAIN, + DEFAULT_CONFIG, + True, + "state-topic", + "1", ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + humidifier.DOMAIN, + DEFAULT_CONFIG, + True, + "state-topic", + "1", ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG, MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, humidifier.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + humidifier.DOMAIN, + DEFAULT_CONFIG, ) -async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_json( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, humidifier.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + humidifier.DOMAIN, + DEFAULT_CONFIG, ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique_id option only creates one fan per id.""" config = { humidifier.DOMAIN: [ @@ -1012,16 +1068,24 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, humidifier.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, config + ) -async def test_discovery_removal_humidifier(hass, mqtt_mock, caplog): +async def test_discovery_removal_humidifier( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test removal of discovered humidifier.""" data = '{ "name": "test", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, humidifier.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, humidifier.DOMAIN, data + ) -async def test_discovery_update_humidifier(hass, mqtt_mock, caplog): +async def test_discovery_update_humidifier( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered humidifier.""" config1 = { "name": "Beer", @@ -1034,77 +1098,93 @@ async def test_discovery_update_humidifier(hass, mqtt_mock, caplog): "target_humidity_command_topic": "test-topic2", } await help_test_discovery_update( - hass, mqtt_mock, caplog, humidifier.DOMAIN, config1, config2 + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + humidifier.DOMAIN, + config1, + config2, ) -async def test_discovery_update_unchanged_humidifier(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_humidifier( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered humidifier.""" data1 = '{ "name": "Beer", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' with patch( "homeassistant.components.mqtt.fan.MqttFan.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, humidifier.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + humidifier.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "command_topic": "test_topic", "target_humidity_command_topic": "test-topic2" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, humidifier.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, humidifier.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, humidifier.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, humidifier.DOMAIN, DEFAULT_CONFIG, humidifier.SERVICE_TURN_ON + hass, + mqtt_mock_entry_no_yaml_config, + humidifier.DOMAIN, + DEFAULT_CONFIG, + humidifier.SERVICE_TURN_ON, ) @@ -1143,7 +1223,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -1159,7 +1239,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -1171,11 +1251,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = humidifier.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index aa0bfb82608..861b50d2f71 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -96,22 +96,27 @@ def record_calls(calls): async def test_mqtt_connects_on_home_assistant_mqtt_setup( - hass, mqtt_client_mock, mqtt_mock + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config ): """Test if client is connected after mqtt init on bootstrap.""" + await mqtt_mock_entry_no_yaml_config() assert mqtt_client_mock.connect.call_count == 1 -async def test_mqtt_disconnects_on_home_assistant_stop(hass, mqtt_mock): +async def test_mqtt_disconnects_on_home_assistant_stop( + hass, mqtt_mock_entry_no_yaml_config +): """Test if client stops on HA stop.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() hass.bus.fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() await hass.async_block_till_done() assert mqtt_mock.async_disconnect.called -async def test_publish(hass, mqtt_mock): +async def test_publish(hass, mqtt_mock_entry_no_yaml_config): """Test the publish function.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await mqtt.async_publish(hass, "test-topic", "test-payload") await hass.async_block_till_done() assert mqtt_mock.async_publish.called @@ -208,7 +213,7 @@ async def test_command_template_value(hass): assert cmd_tpl.async_render(None, variables=variables) == "beer" -async def test_command_template_variables(hass, mqtt_mock): +async def test_command_template_variables(hass, mqtt_mock_entry_with_yaml_config): """Test the rendering of enitity_variables.""" topic = "test/select" @@ -232,6 +237,7 @@ async def test_command_template_variables(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("select.test_select") assert state.state == "milk" @@ -291,8 +297,11 @@ async def test_value_template_value(hass): assert val_tpl.async_render_with_possible_json_value('{"id": 4321}') == "4321" -async def test_service_call_without_topic_does_not_publish(hass, mqtt_mock): +async def test_service_call_without_topic_does_not_publish( + hass, mqtt_mock_entry_no_yaml_config +): """Test the service call if topic is missing.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() with pytest.raises(vol.Invalid): await hass.services.async_call( mqtt.DOMAIN, @@ -304,12 +313,13 @@ async def test_service_call_without_topic_does_not_publish(hass, mqtt_mock): async def test_service_call_with_topic_and_topic_template_does_not_publish( - hass, mqtt_mock + hass, mqtt_mock_entry_no_yaml_config ): """Test the service call with topic/topic template. If both 'topic' and 'topic_template' are provided then fail. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() topic = "test/topic" topic_template = "test/{{ 'topic' }}" with pytest.raises(vol.Invalid): @@ -327,9 +337,10 @@ async def test_service_call_with_topic_and_topic_template_does_not_publish( async def test_service_call_with_invalid_topic_template_does_not_publish( - hass, mqtt_mock + hass, mqtt_mock_entry_no_yaml_config ): """Test the service call with a problematic topic template.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, @@ -342,11 +353,14 @@ async def test_service_call_with_invalid_topic_template_does_not_publish( assert not mqtt_mock.async_publish.called -async def test_service_call_with_template_topic_renders_template(hass, mqtt_mock): +async def test_service_call_with_template_topic_renders_template( + hass, mqtt_mock_entry_no_yaml_config +): """Test the service call with rendered topic template. If 'topic_template' is provided and 'topic' is not, then render it. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, @@ -360,11 +374,14 @@ async def test_service_call_with_template_topic_renders_template(hass, mqtt_mock assert mqtt_mock.async_publish.call_args[0][0] == "test/2" -async def test_service_call_with_template_topic_renders_invalid_topic(hass, mqtt_mock): +async def test_service_call_with_template_topic_renders_invalid_topic( + hass, mqtt_mock_entry_no_yaml_config +): """Test the service call with rendered, invalid topic template. If a wildcard topic is rendered, then fail. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, @@ -378,12 +395,13 @@ async def test_service_call_with_template_topic_renders_invalid_topic(hass, mqtt async def test_service_call_with_invalid_rendered_template_topic_doesnt_render_template( - hass, mqtt_mock + hass, mqtt_mock_entry_no_yaml_config ): """Test the service call with unrendered template. If both 'payload' and 'payload_template' are provided then fail. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() payload = "not a template" payload_template = "a template" with pytest.raises(vol.Invalid): @@ -400,11 +418,14 @@ async def test_service_call_with_invalid_rendered_template_topic_doesnt_render_t assert not mqtt_mock.async_publish.called -async def test_service_call_with_template_payload_renders_template(hass, mqtt_mock): +async def test_service_call_with_template_payload_renders_template( + hass, mqtt_mock_entry_no_yaml_config +): """Test the service call with rendered template. If 'payload_template' is provided and 'payload' is not, then render it. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, @@ -429,8 +450,9 @@ async def test_service_call_with_template_payload_renders_template(hass, mqtt_mo mqtt_mock.reset_mock() -async def test_service_call_with_bad_template(hass, mqtt_mock): +async def test_service_call_with_bad_template(hass, mqtt_mock_entry_no_yaml_config): """Test the service call with a bad template does not publish.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, @@ -440,11 +462,14 @@ async def test_service_call_with_bad_template(hass, mqtt_mock): assert not mqtt_mock.async_publish.called -async def test_service_call_with_payload_doesnt_render_template(hass, mqtt_mock): +async def test_service_call_with_payload_doesnt_render_template( + hass, mqtt_mock_entry_no_yaml_config +): """Test the service call with unrendered template. If both 'payload' and 'payload_template' are provided then fail. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() payload = "not a template" payload_template = "a template" with pytest.raises(vol.Invalid): @@ -461,11 +486,14 @@ async def test_service_call_with_payload_doesnt_render_template(hass, mqtt_mock) assert not mqtt_mock.async_publish.called -async def test_service_call_with_ascii_qos_retain_flags(hass, mqtt_mock): +async def test_service_call_with_ascii_qos_retain_flags( + hass, mqtt_mock_entry_no_yaml_config +): """Test the service call with args that can be misinterpreted. Empty payload message and ascii formatted qos and retain flags. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() await hass.services.async_call( mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, @@ -665,9 +693,10 @@ def test_entity_device_info_schema(): async def test_receiving_non_utf8_message_gets_logged( - hass, mqtt_mock, calls, record_calls, caplog + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls, caplog ): """Test receiving a non utf8 encoded message.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic", record_calls) async_fire_mqtt_message(hass, "test-topic", b"\x9a") @@ -679,9 +708,10 @@ async def test_receiving_non_utf8_message_gets_logged( async def test_all_subscriptions_run_when_decode_fails( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test all other subscriptions still run when decode fails for one.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic", record_calls, encoding="ascii") await mqtt.async_subscribe(hass, "test-topic", record_calls) @@ -691,8 +721,11 @@ async def test_all_subscriptions_run_when_decode_fails( assert len(calls) == 1 -async def test_subscribe_topic(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_topic( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test the subscription of a topic.""" + await mqtt_mock_entry_no_yaml_config() unsub = await mqtt.async_subscribe(hass, "test-topic", record_calls) async_fire_mqtt_message(hass, "test-topic", "test-payload") @@ -714,8 +747,11 @@ async def test_subscribe_topic(hass, mqtt_mock, calls, record_calls): unsub() -async def test_subscribe_topic_non_async(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_topic_non_async( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test the subscription of a topic using the non-async function.""" + await mqtt_mock_entry_no_yaml_config() unsub = await hass.async_add_executor_job( mqtt.subscribe, hass, "test-topic", record_calls ) @@ -736,14 +772,18 @@ async def test_subscribe_topic_non_async(hass, mqtt_mock, calls, record_calls): assert len(calls) == 1 -async def test_subscribe_bad_topic(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_bad_topic( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test the subscription of a topic.""" + await mqtt_mock_entry_no_yaml_config() with pytest.raises(HomeAssistantError): await mqtt.async_subscribe(hass, 55, record_calls) -async def test_subscribe_deprecated(hass, mqtt_mock): +async def test_subscribe_deprecated(hass, mqtt_mock_entry_no_yaml_config): """Test the subscription of a topic using deprecated callback signature.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() @callback def record_calls(topic, payload, qos): @@ -789,8 +829,9 @@ async def test_subscribe_deprecated(hass, mqtt_mock): assert len(calls) == 1 -async def test_subscribe_deprecated_async(hass, mqtt_mock): +async def test_subscribe_deprecated_async(hass, mqtt_mock_entry_no_yaml_config): """Test the subscription of a topic using deprecated coroutine signature.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() def async_record_calls(topic, payload, qos): """Record calls.""" @@ -835,8 +876,11 @@ async def test_subscribe_deprecated_async(hass, mqtt_mock): assert len(calls) == 1 -async def test_subscribe_topic_not_match(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_topic_not_match( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test if subscribed topic is not a match.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic", record_calls) async_fire_mqtt_message(hass, "another-test-topic", "test-payload") @@ -845,8 +889,11 @@ async def test_subscribe_topic_not_match(hass, mqtt_mock, calls, record_calls): assert len(calls) == 0 -async def test_subscribe_topic_level_wildcard(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_topic_level_wildcard( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") @@ -858,9 +905,10 @@ async def test_subscribe_topic_level_wildcard(hass, mqtt_mock, calls, record_cal async def test_subscribe_topic_level_wildcard_no_subtree_match( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic/+/on", record_calls) async_fire_mqtt_message(hass, "test-topic/bier", "test-payload") @@ -870,9 +918,10 @@ async def test_subscribe_topic_level_wildcard_no_subtree_match( async def test_subscribe_topic_level_wildcard_root_topic_no_subtree_match( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic/#", record_calls) async_fire_mqtt_message(hass, "test-topic-123", "test-payload") @@ -882,9 +931,10 @@ async def test_subscribe_topic_level_wildcard_root_topic_no_subtree_match( async def test_subscribe_topic_subtree_wildcard_subtree_topic( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic/#", record_calls) async_fire_mqtt_message(hass, "test-topic/bier/on", "test-payload") @@ -896,9 +946,10 @@ async def test_subscribe_topic_subtree_wildcard_subtree_topic( async def test_subscribe_topic_subtree_wildcard_root_topic( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic/#", record_calls) async_fire_mqtt_message(hass, "test-topic", "test-payload") @@ -910,9 +961,10 @@ async def test_subscribe_topic_subtree_wildcard_root_topic( async def test_subscribe_topic_subtree_wildcard_no_match( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "test-topic/#", record_calls) async_fire_mqtt_message(hass, "another-test-topic", "test-payload") @@ -922,9 +974,10 @@ async def test_subscribe_topic_subtree_wildcard_no_match( async def test_subscribe_topic_level_wildcard_and_wildcard_root_topic( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) async_fire_mqtt_message(hass, "hi/test-topic", "test-payload") @@ -936,9 +989,10 @@ async def test_subscribe_topic_level_wildcard_and_wildcard_root_topic( async def test_subscribe_topic_level_wildcard_and_wildcard_subtree_topic( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) async_fire_mqtt_message(hass, "hi/test-topic/here-iam", "test-payload") @@ -950,9 +1004,10 @@ async def test_subscribe_topic_level_wildcard_and_wildcard_subtree_topic( async def test_subscribe_topic_level_wildcard_and_wildcard_level_no_match( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) async_fire_mqtt_message(hass, "hi/here-iam/test-topic", "test-payload") @@ -962,9 +1017,10 @@ async def test_subscribe_topic_level_wildcard_and_wildcard_level_no_match( async def test_subscribe_topic_level_wildcard_and_wildcard_no_match( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "+/test-topic/#", record_calls) async_fire_mqtt_message(hass, "hi/another-test-topic", "test-payload") @@ -973,8 +1029,11 @@ async def test_subscribe_topic_level_wildcard_and_wildcard_no_match( assert len(calls) == 0 -async def test_subscribe_topic_sys_root(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_topic_sys_root( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test the subscription of $ root topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "$test-topic/subtree/on", record_calls) async_fire_mqtt_message(hass, "$test-topic/subtree/on", "test-payload") @@ -986,9 +1045,10 @@ async def test_subscribe_topic_sys_root(hass, mqtt_mock, calls, record_calls): async def test_subscribe_topic_sys_root_and_wildcard_topic( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of $ root and wildcard topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "$test-topic/#", record_calls) async_fire_mqtt_message(hass, "$test-topic/some-topic", "test-payload") @@ -1000,9 +1060,10 @@ async def test_subscribe_topic_sys_root_and_wildcard_topic( async def test_subscribe_topic_sys_root_and_wildcard_subtree_topic( - hass, mqtt_mock, calls, record_calls + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls ): """Test the subscription of $ root and wildcard subtree topics.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_subscribe(hass, "$test-topic/subtree/#", record_calls) async_fire_mqtt_message(hass, "$test-topic/subtree/some-topic", "test-payload") @@ -1013,8 +1074,11 @@ async def test_subscribe_topic_sys_root_and_wildcard_subtree_topic( assert calls[0][0].payload == "test-payload" -async def test_subscribe_special_characters(hass, mqtt_mock, calls, record_calls): +async def test_subscribe_special_characters( + hass, mqtt_mock_entry_no_yaml_config, calls, record_calls +): """Test the subscription to topics with special characters.""" + await mqtt_mock_entry_no_yaml_config() topic = "/test-topic/$(.)[^]{-}" payload = "p4y.l[]a|> ?" @@ -1027,13 +1091,16 @@ async def test_subscribe_special_characters(hass, mqtt_mock, calls, record_calls assert calls[0][0].payload == payload -async def test_subscribe_same_topic(hass, mqtt_client_mock, mqtt_mock): +async def test_subscribe_same_topic( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """ Test subscring to same topic twice and simulate retained messages. When subscribing to the same topic again, SUBSCRIBE must be sent to the broker again for it to resend any retained messages. """ + mqtt_mock = await mqtt_mock_entry_no_yaml_config() # Fake that the client is connected mqtt_mock().connected = True @@ -1061,9 +1128,10 @@ async def test_subscribe_same_topic(hass, mqtt_client_mock, mqtt_mock): async def test_not_calling_unsubscribe_with_active_subscribers( - hass, mqtt_client_mock, mqtt_mock + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config ): """Test not calling unsubscribe() when other subscribers are active.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() # Fake that the client is connected mqtt_mock().connected = True @@ -1077,8 +1145,9 @@ async def test_not_calling_unsubscribe_with_active_subscribers( assert not mqtt_client_mock.unsubscribe.called -async def test_unsubscribe_race(hass, mqtt_client_mock, mqtt_mock): +async def test_unsubscribe_race(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config): """Test not calling unsubscribe() when other subscribers are active.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() # Fake that the client is connected mqtt_mock().connected = True @@ -1113,8 +1182,11 @@ async def test_unsubscribe_race(hass, mqtt_client_mock, mqtt_mock): "mqtt_config", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) -async def test_restore_subscriptions_on_reconnect(hass, mqtt_client_mock, mqtt_mock): +async def test_restore_subscriptions_on_reconnect( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Test subscriptions are restored on reconnect.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() # Fake that the client is connected mqtt_mock().connected = True @@ -1134,9 +1206,10 @@ async def test_restore_subscriptions_on_reconnect(hass, mqtt_client_mock, mqtt_m [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) async def test_restore_all_active_subscriptions_on_reconnect( - hass, mqtt_client_mock, mqtt_mock + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config ): """Test active subscriptions are restored correctly on reconnect.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() # Fake that the client is connected mqtt_mock().connected = True @@ -1176,9 +1249,10 @@ async def test_initial_setup_logs_error(hass, caplog, mqtt_client_mock): async def test_logs_error_if_no_connect_broker( - hass, caplog, mqtt_mock, mqtt_client_mock + hass, caplog, mqtt_mock_entry_no_yaml_config, mqtt_client_mock ): """Test for setup failure if connection to broker is missing.""" + await mqtt_mock_entry_no_yaml_config() # test with rc = 3 -> broker unavailable mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 3) await hass.async_block_till_done() @@ -1189,8 +1263,11 @@ async def test_logs_error_if_no_connect_broker( @patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.3) -async def test_handle_mqtt_on_callback(hass, caplog, mqtt_mock, mqtt_client_mock): +async def test_handle_mqtt_on_callback( + hass, caplog, mqtt_mock_entry_no_yaml_config, mqtt_client_mock +): """Test receiving an ACK callback before waiting for it.""" + await mqtt_mock_entry_no_yaml_config() # Simulate an ACK for mid == 1, this will call mqtt_mock._mqtt_handle_mid(mid) mqtt_client_mock.on_publish(mqtt_client_mock, None, 1) await hass.async_block_till_done() @@ -1224,8 +1301,11 @@ async def test_publish_error(hass, caplog): assert "Failed to connect to MQTT server: Out of memory." in caplog.text -async def test_handle_message_callback(hass, caplog, mqtt_mock, mqtt_client_mock): +async def test_handle_message_callback( + hass, caplog, mqtt_mock_entry_no_yaml_config, mqtt_client_mock +): """Test for handling an incoming message callback.""" + await mqtt_mock_entry_no_yaml_config() msg = ReceiveMessage("some-topic", b"test-payload", 0, False) mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 0) await mqtt.async_subscribe(hass, "some-topic", lambda *args: 0) @@ -1478,8 +1558,11 @@ async def test_setup_without_tls_config_uses_tlsv1_under_python36(hass): } ], ) -async def test_custom_birth_message(hass, mqtt_client_mock, mqtt_mock): +async def test_custom_birth_message( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Test sending birth message.""" + await mqtt_mock_entry_no_yaml_config() birth = asyncio.Event() async def wait_birth(topic, payload, qos): @@ -1508,8 +1591,11 @@ async def test_custom_birth_message(hass, mqtt_client_mock, mqtt_mock): } ], ) -async def test_default_birth_message(hass, mqtt_client_mock, mqtt_mock): +async def test_default_birth_message( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Test sending birth message.""" + await mqtt_mock_entry_no_yaml_config() birth = asyncio.Event() async def wait_birth(topic, payload, qos): @@ -1530,8 +1616,9 @@ async def test_default_birth_message(hass, mqtt_client_mock, mqtt_mock): "mqtt_config", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], ) -async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock): +async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config): """Test disabling birth message.""" + await mqtt_mock_entry_no_yaml_config() with patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.1): mqtt_client_mock.on_connect(None, None, 0, 0) await hass.async_block_till_done() @@ -1553,8 +1640,12 @@ async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock): } ], ) -async def test_delayed_birth_message(hass, mqtt_client_mock, mqtt_config, mqtt_mock): +async def test_delayed_birth_message( + hass, mqtt_client_mock, mqtt_config, mqtt_mock_entry_no_yaml_config +): """Test sending birth message does not happen until Home Assistant starts.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() + hass.state = CoreState.starting birth = asyncio.Event() @@ -1610,15 +1701,23 @@ async def test_delayed_birth_message(hass, mqtt_client_mock, mqtt_config, mqtt_m } ], ) -async def test_custom_will_message(hass, mqtt_client_mock, mqtt_mock): +async def test_custom_will_message( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Test will message.""" + await mqtt_mock_entry_no_yaml_config() + mqtt_client_mock.will_set.assert_called_with( topic="death", payload="death", qos=0, retain=False ) -async def test_default_will_message(hass, mqtt_client_mock, mqtt_mock): +async def test_default_will_message( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Test will message.""" + await mqtt_mock_entry_no_yaml_config() + mqtt_client_mock.will_set.assert_called_with( topic="homeassistant/status", payload="offline", qos=0, retain=False ) @@ -1628,8 +1727,10 @@ async def test_default_will_message(hass, mqtt_client_mock, mqtt_mock): "mqtt_config", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_WILL_MESSAGE: {}}], ) -async def test_no_will_message(hass, mqtt_client_mock, mqtt_mock): +async def test_no_will_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config): """Test will message.""" + await mqtt_mock_entry_no_yaml_config() + mqtt_client_mock.will_set.assert_not_called() @@ -1643,8 +1744,12 @@ async def test_no_will_message(hass, mqtt_client_mock, mqtt_mock): } ], ) -async def test_mqtt_subscribes_topics_on_connect(hass, mqtt_client_mock, mqtt_mock): +async def test_mqtt_subscribes_topics_on_connect( + hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config +): """Test subscription to topic on connect.""" + await mqtt_mock_entry_no_yaml_config() + await mqtt.async_subscribe(hass, "topic/test", None) await mqtt.async_subscribe(hass, "home/sensor", None, 2) await mqtt.async_subscribe(hass, "still/pending", None) @@ -1662,7 +1767,9 @@ async def test_mqtt_subscribes_topics_on_connect(hass, mqtt_client_mock, mqtt_mo assert calls == expected -async def test_setup_entry_with_config_override(hass, device_reg, mqtt_client_mock): +async def test_setup_entry_with_config_override( + hass, device_reg, mqtt_mock_entry_with_yaml_config +): """Test if the MQTT component loads with no config and config entry can be setup.""" data = ( '{ "device":{"identifiers":["0AFFD2"]},' @@ -1672,6 +1779,8 @@ async def test_setup_entry_with_config_override(hass, device_reg, mqtt_client_mo # mqtt present in yaml config assert await async_setup_component(hass, mqtt.DOMAIN, {}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() # User sets up a config entry entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) @@ -1734,8 +1843,11 @@ async def test_fail_no_broker(hass, device_reg, mqtt_client_mock, caplog): @pytest.mark.no_fail_on_log_exception -async def test_message_callback_exception_gets_logged(hass, caplog, mqtt_mock): +async def test_message_callback_exception_gets_logged( + hass, caplog, mqtt_mock_entry_no_yaml_config +): """Test exception raised by message handler.""" + await mqtt_mock_entry_no_yaml_config() @callback def bad_handler(*args): @@ -1752,8 +1864,11 @@ async def test_message_callback_exception_gets_logged(hass, caplog, mqtt_mock): ) -async def test_mqtt_ws_subscription(hass, hass_ws_client, mqtt_mock): +async def test_mqtt_ws_subscription( + hass, hass_ws_client, mqtt_mock_entry_no_yaml_config +): """Test MQTT websocket subscription.""" + await mqtt_mock_entry_no_yaml_config() client = await hass_ws_client(hass) await client.send_json({"id": 5, "type": "mqtt/subscribe", "topic": "test-topic"}) response = await client.receive_json() @@ -1782,9 +1897,10 @@ async def test_mqtt_ws_subscription(hass, hass_ws_client, mqtt_mock): async def test_mqtt_ws_subscription_not_admin( - hass, hass_ws_client, mqtt_mock, hass_read_only_access_token + hass, hass_ws_client, mqtt_mock_entry_no_yaml_config, hass_read_only_access_token ): """Test MQTT websocket user is not admin.""" + await mqtt_mock_entry_no_yaml_config() client = await hass_ws_client(hass, access_token=hass_read_only_access_token) await client.send_json({"id": 5, "type": "mqtt/subscribe", "topic": "test-topic"}) response = await client.receive_json() @@ -1793,8 +1909,9 @@ async def test_mqtt_ws_subscription_not_admin( assert response["error"]["message"] == "Unauthorized" -async def test_dump_service(hass, mqtt_mock): +async def test_dump_service(hass, mqtt_mock_entry_no_yaml_config): """Test that we can dump a topic.""" + await mqtt_mock_entry_no_yaml_config() mopen = mock_open() await hass.services.async_call( @@ -1814,10 +1931,12 @@ async def test_dump_service(hass, mqtt_mock): async def test_mqtt_ws_remove_discovered_device( - hass, device_reg, entity_reg, hass_ws_client, mqtt_mock + hass, device_reg, entity_reg, hass_ws_client, mqtt_mock_entry_no_yaml_config ): """Test MQTT websocket device removal.""" assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() data = ( '{ "device":{"identifiers":["0AFFD2"]},' @@ -1851,9 +1970,10 @@ async def test_mqtt_ws_remove_discovered_device( async def test_mqtt_ws_get_device_debug_info( - hass, device_reg, hass_ws_client, mqtt_mock + hass, device_reg, hass_ws_client, mqtt_mock_entry_no_yaml_config ): """Test MQTT websocket device debug info.""" + await mqtt_mock_entry_no_yaml_config() config_sensor = { "device": {"identifiers": ["0AFFD2"]}, "platform": "mqtt", @@ -1913,9 +2033,10 @@ async def test_mqtt_ws_get_device_debug_info( async def test_mqtt_ws_get_device_debug_info_binary( - hass, device_reg, hass_ws_client, mqtt_mock + hass, device_reg, hass_ws_client, mqtt_mock_entry_no_yaml_config ): """Test MQTT websocket device debug info.""" + await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["0AFFD2"]}, "platform": "mqtt", @@ -1975,8 +2096,9 @@ async def test_mqtt_ws_get_device_debug_info_binary( assert response["result"] == expected_result -async def test_debug_info_multiple_devices(hass, mqtt_mock): +async def test_debug_info_multiple_devices(hass, mqtt_mock_entry_no_yaml_config): """Test we get correct debug_info when multiple devices are present.""" + await mqtt_mock_entry_no_yaml_config() devices = [ { "domain": "sensor", @@ -2054,8 +2176,11 @@ async def test_debug_info_multiple_devices(hass, mqtt_mock): assert discovery_data["payload"] == d["config"] -async def test_debug_info_multiple_entities_triggers(hass, mqtt_mock): +async def test_debug_info_multiple_entities_triggers( + hass, mqtt_mock_entry_no_yaml_config +): """Test we get correct debug_info for a device with multiple entities and triggers.""" + await mqtt_mock_entry_no_yaml_config() config = [ { "domain": "sensor", @@ -2137,8 +2262,11 @@ async def test_debug_info_multiple_entities_triggers(hass, mqtt_mock): } in discovery_data -async def test_debug_info_non_mqtt(hass, device_reg, entity_reg, mqtt_mock): +async def test_debug_info_non_mqtt( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test we get empty debug_info for a device with non MQTT entities.""" + await mqtt_mock_entry_no_yaml_config() DOMAIN = "sensor" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -2164,8 +2292,9 @@ async def test_debug_info_non_mqtt(hass, device_reg, entity_reg, mqtt_mock): assert len(debug_info_data["triggers"]) == 0 -async def test_debug_info_wildcard(hass, mqtt_mock): +async def test_debug_info_wildcard(hass, mqtt_mock_entry_no_yaml_config): """Test debug info.""" + await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, "platform": "mqtt", @@ -2210,8 +2339,9 @@ async def test_debug_info_wildcard(hass, mqtt_mock): } in debug_info_data["entities"][0]["subscriptions"] -async def test_debug_info_filter_same(hass, mqtt_mock): +async def test_debug_info_filter_same(hass, mqtt_mock_entry_no_yaml_config): """Test debug info removes messages with same timestamp.""" + await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, "platform": "mqtt", @@ -2268,8 +2398,9 @@ async def test_debug_info_filter_same(hass, mqtt_mock): } == debug_info_data["entities"][0]["subscriptions"][0] -async def test_debug_info_same_topic(hass, mqtt_mock): +async def test_debug_info_same_topic(hass, mqtt_mock_entry_no_yaml_config): """Test debug info.""" + await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, "platform": "mqtt", @@ -2320,8 +2451,9 @@ async def test_debug_info_same_topic(hass, mqtt_mock): async_fire_mqtt_message(hass, "sensor/status", "123", qos=0, retain=False) -async def test_debug_info_qos_retain(hass, mqtt_mock): +async def test_debug_info_qos_retain(hass, mqtt_mock_entry_no_yaml_config): """Test debug info.""" + await mqtt_mock_entry_no_yaml_config() config = { "device": {"identifiers": ["helloworld"]}, "platform": "mqtt", @@ -2377,8 +2509,10 @@ async def test_debug_info_qos_retain(hass, mqtt_mock): } in debug_info_data["entities"][0]["subscriptions"][0]["messages"] -async def test_publish_json_from_template(hass, mqtt_mock): +async def test_publish_json_from_template(hass, mqtt_mock_entry_no_yaml_config): """Test the publishing of call to services.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() + test_str = "{'valid': 'python', 'invalid': 'json'}" test_str_tpl = "{'valid': '{{ \"python\" }}', 'invalid': 'json'}" @@ -2424,8 +2558,11 @@ async def test_publish_json_from_template(hass, mqtt_mock): assert mqtt_mock.async_publish.call_args[0][1] == test_str -async def test_subscribe_connection_status(hass, mqtt_mock, mqtt_client_mock): +async def test_subscribe_connection_status( + hass, mqtt_mock_entry_no_yaml_config, mqtt_client_mock +): """Test connextion status subscription.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() mqtt_connected_calls = [] @callback @@ -2459,7 +2596,9 @@ async def test_subscribe_connection_status(hass, mqtt_mock, mqtt_client_mock): assert mqtt_connected_calls[1] is False -async def test_one_deprecation_warning_per_platform(hass, mqtt_mock, caplog): +async def test_one_deprecation_warning_per_platform( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test a deprecation warning is is logged once per platform.""" platform = "light" config = {"platform": "mqtt", "command_topic": "test-topic"} @@ -2469,6 +2608,7 @@ async def test_one_deprecation_warning_per_platform(hass, mqtt_mock, caplog): config2["name"] = "test2" await async_setup_component(hass, platform, {platform: [config1, config2]}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() count = 0 for record in caplog.records: if record.levelname == "WARNING" and ( diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index b7a3b5f2118..43b6e839904 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -89,12 +89,13 @@ DEFAULT_CONFIG = { DEFAULT_CONFIG_2 = {vacuum.DOMAIN: {"platform": "mqtt", "name": "test"}} -async def test_default_supported_features(hass, mqtt_mock): +async def test_default_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test that the correct supported features.""" assert await async_setup_component( hass, vacuum.DOMAIN, {vacuum.DOMAIN: DEFAULT_CONFIG} ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() entity = hass.states.get("vacuum.mqtttest") entity_features = entity.attributes.get(mqttvacuum.CONF_SUPPORTED_FEATURES, 0) assert sorted(services_to_strings(entity_features, SERVICE_TO_STRING)) == sorted( @@ -110,7 +111,7 @@ async def test_default_supported_features(hass, mqtt_mock): ) -async def test_all_commands(hass, mqtt_mock): +async def test_all_commands(hass, mqtt_mock_entry_with_yaml_config): """Test simple commands to the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -119,6 +120,7 @@ async def test_all_commands(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_turn_on(hass, "vacuum.mqtttest") mqtt_mock.async_publish.assert_called_once_with( @@ -189,7 +191,9 @@ async def test_all_commands(hass, mqtt_mock): } -async def test_commands_without_supported_features(hass, mqtt_mock): +async def test_commands_without_supported_features( + hass, mqtt_mock_entry_with_yaml_config +): """Test commands which are not supported by the vacuum.""" config = deepcopy(DEFAULT_CONFIG) services = mqttvacuum.STRING_TO_SERVICE["status"] @@ -199,6 +203,7 @@ async def test_commands_without_supported_features(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await common.async_turn_on(hass, "vacuum.mqtttest") mqtt_mock.async_publish.assert_not_called() @@ -237,7 +242,9 @@ async def test_commands_without_supported_features(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_attributes_without_supported_features(hass, mqtt_mock): +async def test_attributes_without_supported_features( + hass, mqtt_mock_entry_with_yaml_config +): """Test attributes which are not supported by the vacuum.""" config = deepcopy(DEFAULT_CONFIG) services = mqttvacuum.STRING_TO_SERVICE["turn_on"] @@ -247,6 +254,7 @@ async def test_attributes_without_supported_features(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "battery_level": 54, @@ -264,7 +272,7 @@ async def test_attributes_without_supported_features(hass, mqtt_mock): assert state.attributes.get(ATTR_FAN_SPEED_LIST) is None -async def test_status(hass, mqtt_mock): +async def test_status(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -273,6 +281,7 @@ async def test_status(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "battery_level": 54, @@ -304,7 +313,7 @@ async def test_status(hass, mqtt_mock): assert state.attributes.get(ATTR_FAN_SPEED) == "min" -async def test_status_battery(hass, mqtt_mock): +async def test_status_battery(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -313,6 +322,7 @@ async def test_status_battery(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "battery_level": 54 @@ -322,7 +332,7 @@ async def test_status_battery(hass, mqtt_mock): assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-50" -async def test_status_cleaning(hass, mqtt_mock): +async def test_status_cleaning(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -331,6 +341,7 @@ async def test_status_cleaning(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "cleaning": true @@ -340,7 +351,7 @@ async def test_status_cleaning(hass, mqtt_mock): assert state.state == STATE_ON -async def test_status_docked(hass, mqtt_mock): +async def test_status_docked(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -349,6 +360,7 @@ async def test_status_docked(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "docked": true @@ -358,7 +370,7 @@ async def test_status_docked(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_status_charging(hass, mqtt_mock): +async def test_status_charging(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -367,6 +379,7 @@ async def test_status_charging(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "charging": true @@ -376,7 +389,7 @@ async def test_status_charging(hass, mqtt_mock): assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-outline" -async def test_status_fan_speed(hass, mqtt_mock): +async def test_status_fan_speed(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -385,6 +398,7 @@ async def test_status_fan_speed(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "fan_speed": "max" @@ -394,7 +408,7 @@ async def test_status_fan_speed(hass, mqtt_mock): assert state.attributes.get(ATTR_FAN_SPEED) == "max" -async def test_status_fan_speed_list(hass, mqtt_mock): +async def test_status_fan_speed_list(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -403,12 +417,13 @@ async def test_status_fan_speed_list(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state.attributes.get(ATTR_FAN_SPEED_LIST) == ["min", "medium", "high", "max"] -async def test_status_no_fan_speed_list(hass, mqtt_mock): +async def test_status_no_fan_speed_list(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum. If the vacuum doesn't support fan speed, fan speed list should be None. @@ -421,12 +436,13 @@ async def test_status_no_fan_speed_list(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state.attributes.get(ATTR_FAN_SPEED_LIST) is None -async def test_status_error(hass, mqtt_mock): +async def test_status_error(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -435,6 +451,7 @@ async def test_status_error(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "error": "Error1" @@ -451,7 +468,7 @@ async def test_status_error(hass, mqtt_mock): assert state.attributes.get(ATTR_STATUS) == "Stopped" -async def test_battery_template(hass, mqtt_mock): +async def test_battery_template(hass, mqtt_mock_entry_with_yaml_config): """Test that you can use non-default templates for battery_level.""" config = deepcopy(DEFAULT_CONFIG) config.update( @@ -466,6 +483,7 @@ async def test_battery_template(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "retroroomba/battery_level", "54") state = hass.states.get("vacuum.mqtttest") @@ -473,7 +491,7 @@ async def test_battery_template(hass, mqtt_mock): assert state.attributes.get(ATTR_BATTERY_ICON) == "mdi:battery-50" -async def test_status_invalid_json(hass, mqtt_mock): +async def test_status_invalid_json(hass, mqtt_mock_entry_with_yaml_config): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -482,6 +500,7 @@ async def test_status_invalid_json(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "vacuum/state", '{"asdfasas false}') state = hass.states.get("vacuum.mqtttest") @@ -489,153 +508,169 @@ async def test_status_invalid_json(hass, mqtt_mock): assert state.attributes.get(ATTR_STATUS) == "Stopped" -async def test_missing_battery_template(hass, mqtt_mock): +async def test_missing_battery_template(hass, mqtt_mock_entry_no_yaml_config): """Test to make sure missing template is not allowed.""" config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_BATTERY_LEVEL_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state is None -async def test_missing_charging_template(hass, mqtt_mock): +async def test_missing_charging_template(hass, mqtt_mock_entry_no_yaml_config): """Test to make sure missing template is not allowed.""" config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_CHARGING_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state is None -async def test_missing_cleaning_template(hass, mqtt_mock): +async def test_missing_cleaning_template(hass, mqtt_mock_entry_no_yaml_config): """Test to make sure missing template is not allowed.""" config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_CLEANING_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state is None -async def test_missing_docked_template(hass, mqtt_mock): +async def test_missing_docked_template(hass, mqtt_mock_entry_no_yaml_config): """Test to make sure missing template is not allowed.""" config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_DOCKED_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state is None -async def test_missing_error_template(hass, mqtt_mock): +async def test_missing_error_template(hass, mqtt_mock_entry_no_yaml_config): """Test to make sure missing template is not allowed.""" config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_ERROR_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state is None -async def test_missing_fan_speed_template(hass, mqtt_mock): +async def test_missing_fan_speed_template(hass, mqtt_mock_entry_no_yaml_config): """Test to make sure missing template is not allowed.""" config = deepcopy(DEFAULT_CONFIG) config.pop(mqttvacuum.CONF_FAN_SPEED_TEMPLATE) assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state is None -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2, MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one vacuum per unique_id.""" config = { vacuum.DOMAIN: [ @@ -653,74 +688,85 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, vacuum.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, config + ) -async def test_discovery_removal_vacuum(hass, mqtt_mock, caplog): +async def test_discovery_removal_vacuum(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered vacuum.""" data = json.dumps(DEFAULT_CONFIG_2[vacuum.DOMAIN]) - await help_test_discovery_removal(hass, mqtt_mock, caplog, vacuum.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, data + ) -async def test_discovery_update_vacuum(hass, mqtt_mock, caplog): +async def test_discovery_update_vacuum(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered vacuum.""" 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, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_vacuum(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_vacuum( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered vacuum.""" data1 = '{ "name": "Beer", "command_topic": "test_topic" }' with patch( "homeassistant.components.mqtt.vacuum.schema_legacy.MqttVacuum.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + vacuum.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer",' ' "command_topic": "test_topic#" }' data2 = '{ "name": "Milk",' ' "command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { vacuum.DOMAIN: { @@ -733,18 +779,22 @@ async def test_entity_id_update_subscriptions(hass, mqtt_mock): } } await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, vacuum.DOMAIN, config, ["test-topic", "avty-topic"] + hass, + mqtt_mock_entry_with_yaml_config, + vacuum.DOMAIN, + config, + ["test-topic", "avty-topic"], ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" config = { vacuum.DOMAIN: { @@ -757,7 +807,11 @@ async def test_entity_debug_info_message(hass, mqtt_mock): } } await help_test_entity_debug_info_message( - hass, mqtt_mock, vacuum.DOMAIN, config, vacuum.SERVICE_TURN_ON + hass, + mqtt_mock_entry_no_yaml_config, + vacuum.DOMAIN, + config, + vacuum.SERVICE_TURN_ON, ) @@ -803,7 +857,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -824,7 +878,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -836,11 +890,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = vacuum.DOMAIN config = DEFAULT_CONFIG - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -872,7 +928,13 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" config = deepcopy(DEFAULT_CONFIG) @@ -892,7 +954,7 @@ async def test_encoding_subscribable_topics( await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, config, diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 957178da14f..08d5432ba27 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -251,16 +251,17 @@ DEFAULT_CONFIG = { } -async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): +async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_config): """Test if command fails with command topic.""" assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {"platform": "mqtt", "name": "test"}} ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert hass.states.get("light.test") is None -async def test_legacy_rgb_white_light(hass, mqtt_mock): +async def test_legacy_rgb_white_light(hass, mqtt_mock_entry_with_yaml_config): """Test legacy RGB + white light flags brightness support.""" assert await async_setup_component( hass, @@ -276,6 +277,7 @@ async def test_legacy_rgb_white_light(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") expected_features = ( @@ -286,7 +288,9 @@ async def test_legacy_rgb_white_light(hass, mqtt_mock): assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == ["hs", "rgbw"] -async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqtt_mock): +async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics( + hass, mqtt_mock_entry_with_yaml_config +): """Test if there is no color and brightness if no topic.""" assert await async_setup_component( hass, @@ -301,6 +305,7 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqt }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -343,7 +348,9 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqt assert state.state == STATE_UNKNOWN -async def test_legacy_controlling_state_via_topic(hass, mqtt_mock): +async def test_legacy_controlling_state_via_topic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling of the state via topic for legacy light (white_value).""" config = { light.DOMAIN: { @@ -374,6 +381,7 @@ async def test_legacy_controlling_state_via_topic(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -472,7 +480,7 @@ async def test_legacy_controlling_state_via_topic(hass, mqtt_mock): assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_controlling_state_via_topic(hass, mqtt_mock): +async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling of the state via topic.""" config = { light.DOMAIN: { @@ -505,6 +513,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -593,7 +602,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_legacy_invalid_state_via_topic(hass, mqtt_mock, caplog): +async def test_legacy_invalid_state_via_topic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test handling of empty data via topic.""" config = { light.DOMAIN: { @@ -623,6 +634,7 @@ async def test_legacy_invalid_state_via_topic(hass, mqtt_mock, caplog): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -709,7 +721,7 @@ async def test_legacy_invalid_state_via_topic(hass, mqtt_mock, caplog): assert light_state.attributes["white_value"] == 255 -async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): +async def test_invalid_state_via_topic(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test handling of empty data via topic.""" config = { light.DOMAIN: { @@ -742,6 +754,7 @@ async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -843,7 +856,7 @@ async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): assert light_state.attributes["color_temp"] == 153 -async def test_brightness_controlling_scale(hass, mqtt_mock): +async def test_brightness_controlling_scale(hass, mqtt_mock_entry_with_yaml_config): """Test the brightness controlling scale.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -865,6 +878,7 @@ async def test_brightness_controlling_scale(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -890,7 +904,9 @@ async def test_brightness_controlling_scale(hass, mqtt_mock): assert light_state.attributes["brightness"] == 255 -async def test_brightness_from_rgb_controlling_scale(hass, mqtt_mock): +async def test_brightness_from_rgb_controlling_scale( + hass, mqtt_mock_entry_with_yaml_config +): """Test the brightness controlling scale.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -911,6 +927,7 @@ async def test_brightness_from_rgb_controlling_scale(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -929,7 +946,9 @@ async def test_brightness_from_rgb_controlling_scale(hass, mqtt_mock): assert state.attributes.get("brightness") == 127 -async def test_legacy_white_value_controlling_scale(hass, mqtt_mock): +async def test_legacy_white_value_controlling_scale( + hass, mqtt_mock_entry_with_yaml_config +): """Test the white_value controlling scale.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -951,6 +970,7 @@ async def test_legacy_white_value_controlling_scale(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -976,7 +996,9 @@ async def test_legacy_white_value_controlling_scale(hass, mqtt_mock): assert light_state.attributes["white_value"] == 255 -async def test_legacy_controlling_state_via_topic_with_templates(hass, mqtt_mock): +async def test_legacy_controlling_state_via_topic_with_templates( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the state with a template.""" config = { light.DOMAIN: { @@ -1011,6 +1033,7 @@ async def test_legacy_controlling_state_via_topic_with_templates(hass, mqtt_mock assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1065,7 +1088,9 @@ async def test_legacy_controlling_state_via_topic_with_templates(hass, mqtt_mock assert state.state == STATE_UNKNOWN -async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): +async def test_controlling_state_via_topic_with_templates( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the state with a template.""" config = { light.DOMAIN: { @@ -1104,6 +1129,7 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1165,7 +1191,9 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_legacy_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_legacy_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of command in optimistic mode.""" config = { light.DOMAIN: { @@ -1204,6 +1232,7 @@ async def test_legacy_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): ), assert_setup_component(1, light.DOMAIN): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -1295,7 +1324,9 @@ async def test_legacy_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes["color_temp"] == 125 -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of command in optimistic mode.""" config = { light.DOMAIN: { @@ -1334,6 +1365,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): ), assert_setup_component(1, light.DOMAIN): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -1484,7 +1516,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): +async def test_sending_mqtt_rgb_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of RGB command with template.""" config = { light.DOMAIN: { @@ -1502,6 +1536,7 @@ async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1521,7 +1556,9 @@ async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): assert state.attributes["rgb_color"] == (255, 128, 64) -async def test_sending_mqtt_rgbw_command_with_template(hass, mqtt_mock): +async def test_sending_mqtt_rgbw_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of RGBW command with template.""" config = { light.DOMAIN: { @@ -1539,6 +1576,7 @@ async def test_sending_mqtt_rgbw_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1558,7 +1596,9 @@ async def test_sending_mqtt_rgbw_command_with_template(hass, mqtt_mock): assert state.attributes["rgbw_color"] == (255, 128, 64, 32) -async def test_sending_mqtt_rgbww_command_with_template(hass, mqtt_mock): +async def test_sending_mqtt_rgbww_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of RGBWW command with template.""" config = { light.DOMAIN: { @@ -1576,6 +1616,7 @@ async def test_sending_mqtt_rgbww_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1595,7 +1636,9 @@ async def test_sending_mqtt_rgbww_command_with_template(hass, mqtt_mock): assert state.attributes["rgbww_color"] == (255, 128, 64, 32, 16) -async def test_sending_mqtt_color_temp_command_with_template(hass, mqtt_mock): +async def test_sending_mqtt_color_temp_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of Color Temp command with template.""" config = { light.DOMAIN: { @@ -1612,6 +1655,7 @@ async def test_sending_mqtt_color_temp_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1631,7 +1675,7 @@ async def test_sending_mqtt_color_temp_command_with_template(hass, mqtt_mock): assert state.attributes["color_temp"] == 100 -async def test_on_command_first(hass, mqtt_mock): +async def test_on_command_first(hass, mqtt_mock_entry_with_yaml_config): """Test on command being sent before brightness.""" config = { light.DOMAIN: { @@ -1645,6 +1689,7 @@ async def test_on_command_first(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1667,7 +1712,7 @@ async def test_on_command_first(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) -async def test_on_command_last(hass, mqtt_mock): +async def test_on_command_last(hass, mqtt_mock_entry_with_yaml_config): """Test on command being sent after brightness.""" config = { light.DOMAIN: { @@ -1680,6 +1725,7 @@ async def test_on_command_last(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1702,7 +1748,7 @@ async def test_on_command_last(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) -async def test_on_command_brightness(hass, mqtt_mock): +async def test_on_command_brightness(hass, mqtt_mock_entry_with_yaml_config): """Test on command being sent as only brightness.""" config = { light.DOMAIN: { @@ -1717,6 +1763,7 @@ async def test_on_command_brightness(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1757,7 +1804,7 @@ async def test_on_command_brightness(hass, mqtt_mock): ) -async def test_on_command_brightness_scaled(hass, mqtt_mock): +async def test_on_command_brightness_scaled(hass, mqtt_mock_entry_with_yaml_config): """Test brightness scale.""" config = { light.DOMAIN: { @@ -1773,6 +1820,7 @@ async def test_on_command_brightness_scaled(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1827,7 +1875,7 @@ async def test_on_command_brightness_scaled(hass, mqtt_mock): ) -async def test_legacy_on_command_rgb(hass, mqtt_mock): +async def test_legacy_on_command_rgb(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGB brightness mode.""" config = { light.DOMAIN: { @@ -1841,6 +1889,7 @@ async def test_legacy_on_command_rgb(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1918,7 +1967,7 @@ async def test_legacy_on_command_rgb(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_on_command_rgb(hass, mqtt_mock): +async def test_on_command_rgb(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGB brightness mode.""" config = { light.DOMAIN: { @@ -1931,6 +1980,7 @@ async def test_on_command_rgb(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2008,7 +2058,7 @@ async def test_on_command_rgb(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_on_command_rgbw(hass, mqtt_mock): +async def test_on_command_rgbw(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGBW brightness mode.""" config = { light.DOMAIN: { @@ -2021,6 +2071,7 @@ async def test_on_command_rgbw(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2098,7 +2149,7 @@ async def test_on_command_rgbw(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_on_command_rgbww(hass, mqtt_mock): +async def test_on_command_rgbww(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGBWW brightness mode.""" config = { light.DOMAIN: { @@ -2111,6 +2162,7 @@ async def test_on_command_rgbww(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2188,7 +2240,7 @@ async def test_on_command_rgbww(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_on_command_rgb_template(hass, mqtt_mock): +async def test_on_command_rgb_template(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGB brightness mode with RGB template.""" config = { light.DOMAIN: { @@ -2202,6 +2254,7 @@ async def test_on_command_rgb_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2225,7 +2278,7 @@ async def test_on_command_rgb_template(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) -async def test_on_command_rgbw_template(hass, mqtt_mock): +async def test_on_command_rgbw_template(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGBW brightness mode with RGBW template.""" config = { light.DOMAIN: { @@ -2239,6 +2292,7 @@ async def test_on_command_rgbw_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2261,7 +2315,7 @@ async def test_on_command_rgbw_template(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) -async def test_on_command_rgbww_template(hass, mqtt_mock): +async def test_on_command_rgbww_template(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGBWW brightness mode with RGBWW template.""" config = { light.DOMAIN: { @@ -2275,6 +2329,7 @@ async def test_on_command_rgbww_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2298,7 +2353,7 @@ async def test_on_command_rgbww_template(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) -async def test_on_command_white(hass, mqtt_mock): +async def test_on_command_white(hass, mqtt_mock_entry_with_yaml_config): """Test sending commands for RGB + white light.""" config = { light.DOMAIN: { @@ -2324,6 +2379,7 @@ async def test_on_command_white(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2375,7 +2431,7 @@ async def test_on_command_white(hass, mqtt_mock): ) -async def test_explicit_color_mode(hass, mqtt_mock): +async def test_explicit_color_mode(hass, mqtt_mock_entry_with_yaml_config): """Test explicit color mode over mqtt.""" config = { light.DOMAIN: { @@ -2409,6 +2465,7 @@ async def test_explicit_color_mode(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2525,7 +2582,7 @@ async def test_explicit_color_mode(hass, mqtt_mock): assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_explicit_color_mode_templated(hass, mqtt_mock): +async def test_explicit_color_mode_templated(hass, mqtt_mock_entry_with_yaml_config): """Test templated explicit color mode over mqtt.""" config = { light.DOMAIN: { @@ -2550,6 +2607,7 @@ async def test_explicit_color_mode_templated(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2606,7 +2664,7 @@ async def test_explicit_color_mode_templated(hass, mqtt_mock): assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_white_state_update(hass, mqtt_mock): +async def test_white_state_update(hass, mqtt_mock_entry_with_yaml_config): """Test state updates for RGB + white light.""" config = { light.DOMAIN: { @@ -2636,6 +2694,7 @@ async def test_white_state_update(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2670,7 +2729,7 @@ async def test_white_state_update(hass, mqtt_mock): assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_effect(hass, mqtt_mock): +async def test_effect(hass, mqtt_mock_entry_with_yaml_config): """Test effect.""" config = { light.DOMAIN: { @@ -2684,6 +2743,7 @@ async def test_effect(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -2707,77 +2767,91 @@ async def test_effect(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, + MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one light per unique_id.""" config = { light.DOMAIN: [ @@ -2797,21 +2871,26 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, light.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, config + ) -async def test_discovery_removal_light(hass, mqtt_mock, caplog): +async def test_discovery_removal_light(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered light.""" data = ( '{ "name": "test",' ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_removal(hass, mqtt_mock, caplog, light.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, data + ) -async def test_discovery_deprecated(hass, mqtt_mock, caplog): +async def test_discovery_deprecated(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test discovery of mqtt light with deprecated platform option.""" + await mqtt_mock_entry_no_yaml_config() data = ( '{ "name": "Beer",' ' "platform": "mqtt",' ' "command_topic": "test_topic"}' ) @@ -2822,7 +2901,9 @@ async def test_discovery_deprecated(hass, mqtt_mock, caplog): assert state.name == "Beer" -async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog): +async def test_discovery_update_light_topic_and_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered light.""" config1 = { "name": "Beer", @@ -3073,7 +3154,7 @@ async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, config1, @@ -3083,7 +3164,9 @@ async def test_discovery_update_light_topic_and_template(hass, mqtt_mock, caplog ) -async def test_discovery_update_light_template(hass, mqtt_mock, caplog): +async def test_discovery_update_light_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered light.""" config1 = { "name": "Beer", @@ -3292,7 +3375,7 @@ async def test_discovery_update_light_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, config1, @@ -3302,7 +3385,9 @@ async def test_discovery_update_light_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_light( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered light.""" data1 = ( '{ "name": "Beer",' @@ -3313,12 +3398,17 @@ async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.light.schema_basic.MqttLight.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, light.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -3327,60 +3417,64 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ' "command_topic": "test_topic" }' ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, light.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG, light.SERVICE_TURN_ON + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, + light.SERVICE_TURN_ON, ) -async def test_max_mireds(hass, mqtt_mock): +async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): """Test setting min_mireds and max_mireds.""" config = { light.DOMAIN: { @@ -3394,6 +3488,7 @@ async def test_max_mireds(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.attributes.get("min_mireds") == 153 @@ -3488,7 +3583,7 @@ async def test_max_mireds(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -3508,7 +3603,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -3522,11 +3617,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = light.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -3568,7 +3665,14 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value, init_payload + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, + init_payload, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[light.DOMAIN]) @@ -3587,7 +3691,7 @@ async def test_encoding_subscribable_topics( await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, config, @@ -3599,7 +3703,9 @@ async def test_encoding_subscribable_topics( ) -async def test_sending_mqtt_brightness_command_with_template(hass, mqtt_mock): +async def test_sending_mqtt_brightness_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of Brightness command with template.""" config = { light.DOMAIN: { @@ -3616,6 +3722,7 @@ async def test_sending_mqtt_brightness_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -3635,7 +3742,9 @@ async def test_sending_mqtt_brightness_command_with_template(hass, mqtt_mock): assert state.attributes["brightness"] == 100 -async def test_sending_mqtt_effect_command_with_template(hass, mqtt_mock): +async def test_sending_mqtt_effect_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of Effect command with template.""" config = { light.DOMAIN: { @@ -3654,6 +3763,7 @@ async def test_sending_mqtt_effect_command_with_template(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 962bf534370..c24c5e87937 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -162,7 +162,7 @@ class JsonValidator: return json.loads(self.jsondata) == json.loads(other) -async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): +async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_config): """Test if setup fails with no command topic.""" assert await async_setup_component( hass, @@ -170,11 +170,14 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): {light.DOMAIN: {"platform": "mqtt", "schema": "json", "name": "test"}}, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert hass.states.get("light.test") is None @pytest.mark.parametrize("deprecated", ("color_temp", "hs", "rgb", "white_value", "xy")) -async def test_fail_setup_if_color_mode_deprecated(hass, mqtt_mock, deprecated): +async def test_fail_setup_if_color_mode_deprecated( + hass, mqtt_mock_entry_no_yaml_config, deprecated +): """Test if setup fails if color mode is combined with deprecated config keys.""" supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] @@ -196,6 +199,7 @@ async def test_fail_setup_if_color_mode_deprecated(hass, mqtt_mock, deprecated): config, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert hass.states.get("light.test") is None @@ -203,7 +207,7 @@ async def test_fail_setup_if_color_mode_deprecated(hass, mqtt_mock, deprecated): "supported_color_modes", [["onoff", "rgb"], ["brightness", "rgb"], ["unknown"]] ) async def test_fail_setup_if_color_modes_invalid( - hass, mqtt_mock, supported_color_modes + hass, mqtt_mock_entry_no_yaml_config, supported_color_modes ): """Test if setup fails if supported color modes is invalid.""" config = { @@ -223,10 +227,11 @@ async def test_fail_setup_if_color_modes_invalid( config, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert hass.states.get("light.test") is None -async def test_rgb_light(hass, mqtt_mock): +async def test_rgb_light(hass, mqtt_mock_entry_with_yaml_config): """Test RGB light flags brightness support.""" assert await async_setup_component( hass, @@ -242,6 +247,7 @@ async def test_rgb_light(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") expected_features = ( @@ -253,7 +259,9 @@ async def test_rgb_light(hass, mqtt_mock): assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features -async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_mock): +async def test_no_color_brightness_color_temp_white_val_if_no_topics( + hass, mqtt_mock_entry_with_yaml_config +): """Test for no RGB, brightness, color temp, effect, white val or XY.""" assert await async_setup_component( hass, @@ -269,6 +277,7 @@ async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_ }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -305,7 +314,7 @@ async def test_no_color_brightness_color_temp_white_val_if_no_topics(hass, mqtt_ assert state.state == STATE_UNKNOWN -async def test_controlling_state_via_topic(hass, mqtt_mock): +async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling of the state via topic.""" assert await async_setup_component( hass, @@ -329,6 +338,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -434,7 +444,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert light_state.attributes.get("white_value") == 155 -async def test_controlling_state_via_topic2(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic2( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling of the state via topic for a light supporting color mode.""" supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] @@ -457,6 +469,7 @@ async def test_controlling_state_via_topic2(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -602,7 +615,9 @@ async def test_controlling_state_via_topic2(hass, mqtt_mock, caplog): assert "Invalid or incomplete color value received" in caplog.text -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of command in optimistic mode.""" fake_state = ha.State( "light.test", @@ -641,6 +656,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -745,7 +761,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes["xy_color"] == (0.611, 0.375) -async def test_sending_mqtt_commands_and_optimistic2(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic2( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of command in optimistic mode for a light supporting color mode.""" supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] fake_state = ha.State( @@ -783,6 +801,7 @@ async def test_sending_mqtt_commands_and_optimistic2(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -953,7 +972,7 @@ async def test_sending_mqtt_commands_and_optimistic2(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() -async def test_sending_hs_color(hass, mqtt_mock): +async def test_sending_hs_color(hass, mqtt_mock_entry_with_yaml_config): """Test light.turn_on with hs color sends hs color parameters.""" assert await async_setup_component( hass, @@ -971,6 +990,7 @@ async def test_sending_hs_color(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1018,7 +1038,7 @@ async def test_sending_hs_color(hass, mqtt_mock): ) -async def test_sending_rgb_color_no_brightness(hass, mqtt_mock): +async def test_sending_rgb_color_no_brightness(hass, mqtt_mock_entry_with_yaml_config): """Test light.turn_on with hs color sends rgb color parameters.""" assert await async_setup_component( hass, @@ -1034,6 +1054,7 @@ async def test_sending_rgb_color_no_brightness(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1071,7 +1092,7 @@ async def test_sending_rgb_color_no_brightness(hass, mqtt_mock): ) -async def test_sending_rgb_color_no_brightness2(hass, mqtt_mock): +async def test_sending_rgb_color_no_brightness2(hass, mqtt_mock_entry_with_yaml_config): """Test light.turn_on with hs color sends rgb color parameters.""" supported_color_modes = ["rgb", "rgbw", "rgbww"] assert await async_setup_component( @@ -1089,6 +1110,7 @@ async def test_sending_rgb_color_no_brightness2(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1148,7 +1170,9 @@ async def test_sending_rgb_color_no_brightness2(hass, mqtt_mock): ) -async def test_sending_rgb_color_with_brightness(hass, mqtt_mock): +async def test_sending_rgb_color_with_brightness( + hass, mqtt_mock_entry_with_yaml_config +): """Test light.turn_on with hs color sends rgb color parameters.""" assert await async_setup_component( hass, @@ -1166,6 +1190,7 @@ async def test_sending_rgb_color_with_brightness(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1218,7 +1243,9 @@ async def test_sending_rgb_color_with_brightness(hass, mqtt_mock): ) -async def test_sending_rgb_color_with_scaled_brightness(hass, mqtt_mock): +async def test_sending_rgb_color_with_scaled_brightness( + hass, mqtt_mock_entry_with_yaml_config +): """Test light.turn_on with hs color sends rgb color parameters.""" assert await async_setup_component( hass, @@ -1237,6 +1264,7 @@ async def test_sending_rgb_color_with_scaled_brightness(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1289,7 +1317,7 @@ async def test_sending_rgb_color_with_scaled_brightness(hass, mqtt_mock): ) -async def test_sending_xy_color(hass, mqtt_mock): +async def test_sending_xy_color(hass, mqtt_mock_entry_with_yaml_config): """Test light.turn_on with hs color sends xy color parameters.""" assert await async_setup_component( hass, @@ -1307,6 +1335,7 @@ async def test_sending_xy_color(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1353,7 +1382,7 @@ async def test_sending_xy_color(hass, mqtt_mock): ) -async def test_effect(hass, mqtt_mock): +async def test_effect(hass, mqtt_mock_entry_with_yaml_config): """Test for effect being sent when included.""" assert await async_setup_component( hass, @@ -1370,6 +1399,7 @@ async def test_effect(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1415,7 +1445,7 @@ async def test_effect(hass, mqtt_mock): assert state.attributes.get("effect") == "colorloop" -async def test_flash_short_and_long(hass, mqtt_mock): +async def test_flash_short_and_long(hass, mqtt_mock_entry_with_yaml_config): """Test for flash length being sent when included.""" assert await async_setup_component( hass, @@ -1433,6 +1463,7 @@ async def test_flash_short_and_long(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1476,7 +1507,7 @@ async def test_flash_short_and_long(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_transition(hass, mqtt_mock): +async def test_transition(hass, mqtt_mock_entry_with_yaml_config): """Test for transition time being sent when included.""" assert await async_setup_component( hass, @@ -1492,6 +1523,7 @@ async def test_transition(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1522,7 +1554,7 @@ async def test_transition(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_brightness_scale(hass, mqtt_mock): +async def test_brightness_scale(hass, mqtt_mock_entry_with_yaml_config): """Test for brightness scaling.""" assert await async_setup_component( hass, @@ -1540,6 +1572,7 @@ async def test_brightness_scale(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1563,7 +1596,7 @@ async def test_brightness_scale(hass, mqtt_mock): assert state.attributes.get("brightness") == 255 -async def test_invalid_values(hass, mqtt_mock): +async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): """Test that invalid color/brightness/white/etc. values are ignored.""" assert await async_setup_component( hass, @@ -1584,6 +1617,7 @@ async def test_invalid_values(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -1700,77 +1734,95 @@ async def test_invalid_values(hass, mqtt_mock): assert state.attributes.get("color_temp") == 100 -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, + MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one light per unique_id.""" config = { light.DOMAIN: [ @@ -1792,16 +1844,24 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, light.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, config + ) -async def test_discovery_removal(hass, mqtt_mock, caplog): +async def test_discovery_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered mqtt_json lights.""" data = '{ "name": "test",' ' "schema": "json",' ' "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, light.DOMAIN, data) + await help_test_discovery_removal( + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + data, + ) -async def test_discovery_update_light(hass, mqtt_mock, caplog): +async def test_discovery_update_light(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered light.""" config1 = { "name": "Beer", @@ -1816,11 +1876,18 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): "command_topic": "test_topic", } await help_test_discovery_update( - hass, mqtt_mock, caplog, light.DOMAIN, config1, config2 + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + config1, + config2, ) -async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_light( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered light.""" data1 = ( '{ "name": "Beer",' @@ -1832,12 +1899,17 @@ async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.light.schema_json.MqttLightJson.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, light.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -1847,57 +1919,80 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ' "command_topic": "test_topic" }' ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, light.DOMAIN, data1, data2 + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + data1, + data2, ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_with_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG, light.SERVICE_TURN_ON, @@ -1906,7 +2001,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) -async def test_max_mireds(hass, mqtt_mock): +async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): """Test setting min_mireds and max_mireds.""" config = { light.DOMAIN: { @@ -1921,6 +2016,7 @@ async def test_max_mireds(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.attributes.get("min_mireds") == 153 @@ -1952,7 +2048,7 @@ async def test_max_mireds(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -1972,7 +2068,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -1986,11 +2082,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = light.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -2013,7 +2111,14 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value, init_payload + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, + init_payload, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[light.DOMAIN]) @@ -2028,7 +2133,7 @@ async def test_encoding_subscribable_topics( ] await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, config, diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index a88fc094f6d..0d4b95e9152 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -90,69 +90,53 @@ DEFAULT_CONFIG = { } -async def test_setup_fails(hass, mqtt_mock): +@pytest.mark.parametrize( + "test_config", + [ + ({"platform": "mqtt", "schema": "template", "name": "test"},), + ( + { + "platform": "mqtt", + "schema": "template", + "name": "test", + "command_topic": "test_topic", + }, + ), + ( + { + "platform": "mqtt", + "schema": "template", + "name": "test", + "command_topic": "test_topic", + "command_on_template": "on", + }, + ), + ( + { + "platform": "mqtt", + "schema": "template", + "name": "test", + "command_topic": "test_topic", + "command_off_template": "off", + }, + ), + ], +) +async def test_setup_fails(hass, mqtt_mock_entry_no_yaml_config, test_config): """Test that setup fails with missing required configuration items.""" - with assert_setup_component(0, light.DOMAIN): + with assert_setup_component(0, light.DOMAIN) as setup_config: assert await async_setup_component( hass, light.DOMAIN, - {light.DOMAIN: {"platform": "mqtt", "schema": "template", "name": "test"}}, - ) - await hass.async_block_till_done() - assert hass.states.get("light.test") is None - - with assert_setup_component(0, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "schema": "template", - "name": "test", - "command_topic": "test_topic", - } - }, - ) - await hass.async_block_till_done() - assert hass.states.get("light.test") is None - - with assert_setup_component(0, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "schema": "template", - "name": "test", - "command_topic": "test_topic", - "command_on_template": "on", - } - }, - ) - await hass.async_block_till_done() - assert hass.states.get("light.test") is None - - with assert_setup_component(0, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "schema": "template", - "name": "test", - "command_topic": "test_topic", - "command_off_template": "off", - } - }, + {light.DOMAIN: test_config}, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() + assert not setup_config[light.DOMAIN] assert hass.states.get("light.test") is None -async def test_rgb_light(hass, mqtt_mock): +async def test_rgb_light(hass, mqtt_mock_entry_with_yaml_config): """Test RGB light flags brightness support.""" assert await async_setup_component( hass, @@ -172,6 +156,7 @@ async def test_rgb_light(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -184,7 +169,7 @@ async def test_rgb_light(hass, mqtt_mock): assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features -async def test_state_change_via_topic(hass, mqtt_mock): +async def test_state_change_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test state change via topic.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -210,6 +195,7 @@ async def test_state_change_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -240,7 +226,7 @@ async def test_state_change_via_topic(hass, mqtt_mock): async def test_state_brightness_color_effect_temp_white_change_via_topic( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test state, bri, color, effect, color temp, white val change.""" with assert_setup_component(1, light.DOMAIN): @@ -276,6 +262,7 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -340,7 +327,9 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( assert light_state.attributes.get("effect") == "rainbow" -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending of command in optimistic mode.""" fake_state = ha.State( "light.test", @@ -391,6 +380,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_ON @@ -495,7 +485,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): async def test_sending_mqtt_commands_non_optimistic_brightness_template( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test the sending of command in optimistic mode.""" with assert_setup_component(1, light.DOMAIN): @@ -532,6 +522,7 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -626,7 +617,7 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( state = hass.states.get("light.test") -async def test_effect(hass, mqtt_mock): +async def test_effect(hass, mqtt_mock_entry_with_yaml_config): """Test effect sent over MQTT in optimistic mode.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -646,6 +637,7 @@ async def test_effect(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -678,7 +670,7 @@ async def test_effect(hass, mqtt_mock): assert state.attributes.get("effect") == "colorloop" -async def test_flash(hass, mqtt_mock): +async def test_flash(hass, mqtt_mock_entry_with_yaml_config): """Test flash sent over MQTT in optimistic mode.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -697,6 +689,7 @@ async def test_flash(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -726,7 +719,7 @@ async def test_flash(hass, mqtt_mock): assert state.state == STATE_ON -async def test_transition(hass, mqtt_mock): +async def test_transition(hass, mqtt_mock_entry_with_yaml_config): """Test for transition time being sent when included.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -745,6 +738,7 @@ async def test_transition(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -767,7 +761,7 @@ async def test_transition(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_invalid_values(hass, mqtt_mock): +async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): """Test that invalid values are ignored.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -801,6 +795,7 @@ async def test_invalid_values(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN @@ -867,77 +862,91 @@ async def test_invalid_values(hass, mqtt_mock): assert state.attributes.get("effect") == "rainbow" -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG, MQTT_LIGHT_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + DEFAULT_CONFIG, + MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one light per unique_id.""" config = { light.DOMAIN: [ @@ -961,10 +970,12 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, light.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, config + ) -async def test_discovery_removal(hass, mqtt_mock, caplog): +async def test_discovery_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered mqtt_json lights.""" data = ( '{ "name": "test",' @@ -973,10 +984,12 @@ async def test_discovery_removal(hass, mqtt_mock, caplog): ' "command_on_template": "on",' ' "command_off_template": "off"}' ) - await help_test_discovery_removal(hass, mqtt_mock, caplog, light.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, data + ) -async def test_discovery_update_light(hass, mqtt_mock, caplog): +async def test_discovery_update_light(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered light.""" config1 = { "name": "Beer", @@ -995,11 +1008,13 @@ async def test_discovery_update_light(hass, mqtt_mock, caplog): "command_off_template": "off", } await help_test_discovery_update( - hass, mqtt_mock, caplog, light.DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_light( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered light.""" data1 = ( '{ "name": "Beer",' @@ -1013,12 +1028,17 @@ async def test_discovery_update_unchanged_light(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.light.schema_template.MqttLightTemplate.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, light.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + light.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -1030,53 +1050,53 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ' "command_off_template": "off"}' ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, light.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, light.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, light.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" config = { light.DOMAIN: { @@ -1090,11 +1110,15 @@ async def test_entity_debug_info_message(hass, mqtt_mock): } } await help_test_entity_debug_info_message( - hass, mqtt_mock, light.DOMAIN, config, light.SERVICE_TURN_ON + hass, + mqtt_mock_entry_no_yaml_config, + light.DOMAIN, + config, + light.SERVICE_TURN_ON, ) -async def test_max_mireds(hass, mqtt_mock): +async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): """Test setting min_mireds and max_mireds.""" config = { light.DOMAIN: { @@ -1111,6 +1135,7 @@ async def test_max_mireds(hass, mqtt_mock): assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("light.test") assert state.attributes.get("min_mireds") == 153 @@ -1142,7 +1167,7 @@ async def test_max_mireds(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -1162,7 +1187,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -1176,11 +1201,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = light.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -1197,14 +1224,21 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value, init_payload + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, + init_payload, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[light.DOMAIN]) config["state_template"] = "{{ value }}" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, light.DOMAIN, config, diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index ef752ef8749..b48557efc8f 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -58,7 +58,7 @@ DEFAULT_CONFIG = { } -async def test_controlling_state_via_topic(hass, mqtt_mock): +async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -77,6 +77,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -94,7 +95,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert state.state is STATE_UNLOCKED -async def test_controlling_non_default_state_via_topic(hass, mqtt_mock): +async def test_controlling_non_default_state_via_topic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -113,6 +116,7 @@ async def test_controlling_non_default_state_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -129,7 +133,9 @@ async def test_controlling_non_default_state_via_topic(hass, mqtt_mock): assert state.state is STATE_UNLOCKED -async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): +async def test_controlling_state_via_topic_and_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, @@ -149,6 +155,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -165,7 +172,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): async def test_controlling_non_default_state_via_topic_and_json_message( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( @@ -186,6 +193,7 @@ async def test_controlling_non_default_state_via_topic_and_json_message( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -201,7 +209,9 @@ async def test_controlling_non_default_state_via_topic_and_json_message( assert state.state is STATE_UNLOCKED -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -219,6 +229,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -245,7 +256,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_explicit_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test optimistic mode without state topic.""" assert await async_setup_component( hass, @@ -265,6 +278,7 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -291,7 +305,9 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_sending_mqtt_commands_support_open_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_support_open_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test open function of the lock without state topic.""" assert await async_setup_component( hass, @@ -310,6 +326,7 @@ async def test_sending_mqtt_commands_support_open_and_optimistic(hass, mqtt_mock }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -348,7 +365,7 @@ async def test_sending_mqtt_commands_support_open_and_optimistic(hass, mqtt_mock async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test open function of the lock without state topic.""" assert await async_setup_component( @@ -370,6 +387,7 @@ async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED @@ -407,77 +425,91 @@ async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG, MQTT_LOCK_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + LOCK_DOMAIN, + DEFAULT_CONFIG, + MQTT_LOCK_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_json( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one lock per unique_id.""" config = { LOCK_DOMAIN: [ @@ -497,16 +529,20 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, LOCK_DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, config + ) -async def test_discovery_removal_lock(hass, mqtt_mock, caplog): +async def test_discovery_removal_lock(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered lock.""" data = '{ "name": "test",' ' "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, LOCK_DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, LOCK_DOMAIN, data + ) -async def test_discovery_update_lock(hass, mqtt_mock, caplog): +async def test_discovery_update_lock(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered lock.""" config1 = { "name": "Beer", @@ -521,11 +557,13 @@ async def test_discovery_update_lock(hass, mqtt_mock, caplog): "availability_topic": "availability_topic2", } await help_test_discovery_update( - hass, mqtt_mock, caplog, LOCK_DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, LOCK_DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_lock(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_lock( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered lock.""" data1 = ( '{ "name": "Beer",' @@ -536,65 +574,72 @@ async def test_discovery_update_unchanged_lock(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.lock.MqttLock.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, LOCK_DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + LOCK_DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk",' ' "command_topic": "test_topic" }' - await help_test_discovery_broken(hass, mqtt_mock, caplog, LOCK_DOMAIN, data1, data2) + await help_test_discovery_broken( + hass, mqtt_mock_entry_no_yaml_config, caplog, LOCK_DOMAIN, data1, data2 + ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT lock device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT lock device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, LOCK_DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, LOCK_DOMAIN, DEFAULT_CONFIG, SERVICE_LOCK, @@ -616,7 +661,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -630,7 +675,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -642,11 +687,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = LOCK_DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -663,12 +710,18 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, LOCK_DOMAIN, DEFAULT_CONFIG[LOCK_DOMAIN], diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 4eb8fdec351..a49b6de198d 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -64,7 +64,7 @@ DEFAULT_CONFIG = { } -async def test_run_number_setup(hass, mqtt_mock): +async def test_run_number_setup(hass, mqtt_mock_entry_with_yaml_config): """Test that it fetches the given payload.""" topic = "test/number" await async_setup_component( @@ -82,6 +82,7 @@ async def test_run_number_setup(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, "10") @@ -108,7 +109,7 @@ async def test_run_number_setup(hass, mqtt_mock): assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "my unit" -async def test_value_template(hass, mqtt_mock): +async def test_value_template(hass, mqtt_mock_entry_with_yaml_config): """Test that it fetches the given payload with a template.""" topic = "test/number" await async_setup_component( @@ -125,6 +126,7 @@ async def test_value_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, '{"val":10}') @@ -148,7 +150,7 @@ async def test_value_template(hass, mqtt_mock): assert state.state == "unknown" -async def test_run_number_service_optimistic(hass, mqtt_mock): +async def test_run_number_service_optimistic(hass, mqtt_mock_entry_with_yaml_config): """Test that set_value service works in optimistic mode.""" topic = "test/number" @@ -170,6 +172,7 @@ async def test_run_number_service_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("number.test_number") assert state.state == "3" @@ -215,7 +218,9 @@ async def test_run_number_service_optimistic(hass, mqtt_mock): assert state.state == "42.1" -async def test_run_number_service_optimistic_with_command_template(hass, mqtt_mock): +async def test_run_number_service_optimistic_with_command_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test that set_value service works in optimistic mode and with a command_template.""" topic = "test/number" @@ -238,6 +243,7 @@ async def test_run_number_service_optimistic_with_command_template(hass, mqtt_mo }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("number.test_number") assert state.state == "3" @@ -285,7 +291,7 @@ async def test_run_number_service_optimistic_with_command_template(hass, mqtt_mo assert state.state == "42.1" -async def test_run_number_service(hass, mqtt_mock): +async def test_run_number_service(hass, mqtt_mock_entry_with_yaml_config): """Test that set_value service works in non optimistic mode.""" cmd_topic = "test/number/set" state_topic = "test/number" @@ -303,6 +309,7 @@ async def test_run_number_service(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, state_topic, "32") state = hass.states.get("number.test_number") @@ -319,7 +326,9 @@ async def test_run_number_service(hass, mqtt_mock): assert state.state == "32" -async def test_run_number_service_with_command_template(hass, mqtt_mock): +async def test_run_number_service_with_command_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test that set_value service works in non optimistic mode and with a command_template.""" cmd_topic = "test/number/set" state_topic = "test/number" @@ -338,6 +347,7 @@ async def test_run_number_service_with_command_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, state_topic, "32") state = hass.states.get("number.test_number") @@ -356,77 +366,91 @@ async def test_run_number_service_with_command_template(hass, mqtt_mock): assert state.state == "32" -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG, MQTT_NUMBER_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + number.DOMAIN, + DEFAULT_CONFIG, + MQTT_NUMBER_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, number.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, number.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, number.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one number per unique_id.""" config = { number.DOMAIN: [ @@ -446,16 +470,20 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, number.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, config + ) -async def test_discovery_removal_number(hass, mqtt_mock, caplog): +async def test_discovery_removal_number(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered number.""" data = json.dumps(DEFAULT_CONFIG[number.DOMAIN]) - await help_test_discovery_removal(hass, mqtt_mock, caplog, number.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, number.DOMAIN, data + ) -async def test_discovery_update_number(hass, mqtt_mock, caplog): +async def test_discovery_update_number(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered number.""" config1 = { "name": "Beer", @@ -469,11 +497,13 @@ async def test_discovery_update_number(hass, mqtt_mock, caplog): } await help_test_discovery_update( - hass, mqtt_mock, caplog, number.DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, number.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_number(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_number( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered number.""" data1 = ( '{ "name": "Beer", "state_topic": "test-topic", "command_topic": "test-topic"}' @@ -482,12 +512,17 @@ async def test_discovery_update_unchanged_number(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.number.MqttNumber.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, number.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + number.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -495,57 +530,57 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, number.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, number.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT number device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT number device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, number.DOMAIN, DEFAULT_CONFIG, SERVICE_SET_VALUE, @@ -555,7 +590,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) -async def test_min_max_step_attributes(hass, mqtt_mock): +async def test_min_max_step_attributes(hass, mqtt_mock_entry_with_yaml_config): """Test min/max/step attributes.""" topic = "test/number" await async_setup_component( @@ -574,6 +609,7 @@ async def test_min_max_step_attributes(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("number.test_number") assert state.attributes.get(ATTR_MIN) == 5 @@ -581,7 +617,7 @@ async def test_min_max_step_attributes(hass, mqtt_mock): assert state.attributes.get(ATTR_STEP) == 20 -async def test_invalid_min_max_attributes(hass, caplog, mqtt_mock): +async def test_invalid_min_max_attributes(hass, caplog, mqtt_mock_entry_no_yaml_config): """Test invalid min/max attributes.""" topic = "test/number" await async_setup_component( @@ -599,11 +635,14 @@ async def test_invalid_min_max_attributes(hass, caplog, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() assert f"'{CONF_MAX}' must be > '{CONF_MIN}'" in caplog.text -async def test_mqtt_payload_not_a_number_warning(hass, caplog, mqtt_mock): +async def test_mqtt_payload_not_a_number_warning( + hass, caplog, mqtt_mock_entry_with_yaml_config +): """Test warning for MQTT payload which is not a number.""" topic = "test/number" await async_setup_component( @@ -619,6 +658,7 @@ async def test_mqtt_payload_not_a_number_warning(hass, caplog, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, "not_a_number") @@ -627,7 +667,9 @@ async def test_mqtt_payload_not_a_number_warning(hass, caplog, mqtt_mock): assert "Payload 'not_a_number' is not a Number" in caplog.text -async def test_mqtt_payload_out_of_range_error(hass, caplog, mqtt_mock): +async def test_mqtt_payload_out_of_range_error( + hass, caplog, mqtt_mock_entry_with_yaml_config +): """Test error when MQTT payload is out of min/max range.""" topic = "test/number" await async_setup_component( @@ -645,6 +687,7 @@ async def test_mqtt_payload_out_of_range_error(hass, caplog, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, "115.5") @@ -669,7 +712,7 @@ async def test_mqtt_payload_out_of_range_error(hass, caplog, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -683,7 +726,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -695,11 +738,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = number.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -717,12 +762,18 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, "number", DEFAULT_CONFIG["number"], diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index 15bbd3964e6..eb5cb94df2d 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -34,7 +34,7 @@ DEFAULT_CONFIG = { } -async def test_sending_mqtt_commands(hass, mqtt_mock): +async def test_sending_mqtt_commands(hass, mqtt_mock_entry_with_yaml_config): """Test the sending MQTT commands.""" fake_state = ha.State("scene.test", STATE_UNKNOWN) @@ -55,6 +55,7 @@ async def test_sending_mqtt_commands(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("scene.test") assert state.state == STATE_UNKNOWN @@ -67,21 +68,23 @@ async def test_sending_mqtt_commands(hass, mqtt_mock): ) -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, scene.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, scene.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { scene.DOMAIN: { @@ -93,11 +96,17 @@ async def test_default_availability_payload(hass, mqtt_mock): } await help_test_default_availability_payload( - hass, mqtt_mock, scene.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + scene.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { scene.DOMAIN: { @@ -109,11 +118,17 @@ async def test_custom_availability_payload(hass, mqtt_mock): } await help_test_custom_availability_payload( - hass, mqtt_mock, scene.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + scene.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one scene per unique_id.""" config = { scene.DOMAIN: [ @@ -131,16 +146,20 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, scene.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, scene.DOMAIN, config + ) -async def test_discovery_removal_scene(hass, mqtt_mock, caplog): +async def test_discovery_removal_scene(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered scene.""" data = '{ "name": "test",' ' "command_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, scene.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, scene.DOMAIN, data + ) -async def test_discovery_update_payload(hass, mqtt_mock, caplog): +async def test_discovery_update_payload(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered scene.""" config1 = copy.deepcopy(DEFAULT_CONFIG[scene.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[scene.DOMAIN]) @@ -151,7 +170,7 @@ async def test_discovery_update_payload(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, scene.DOMAIN, config1, @@ -159,32 +178,41 @@ async def test_discovery_update_payload(hass, mqtt_mock, caplog): ) -async def test_discovery_update_unchanged_scene(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_scene( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered scene.""" data1 = '{ "name": "Beer",' ' "command_topic": "test_topic" }' with patch( "homeassistant.components.mqtt.scene.MqttScene.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, scene.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + scene.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk",' ' "command_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, scene.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, scene.DOMAIN, data1, data2 ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = scene.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index cf5abf55854..888dd301018 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -59,7 +59,7 @@ DEFAULT_CONFIG = { } -async def test_run_select_setup(hass, mqtt_mock): +async def test_run_select_setup(hass, mqtt_mock_entry_with_yaml_config): """Test that it fetches the given payload.""" topic = "test/select" await async_setup_component( @@ -76,6 +76,7 @@ async def test_run_select_setup(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, "milk") @@ -92,7 +93,7 @@ async def test_run_select_setup(hass, mqtt_mock): assert state.state == "beer" -async def test_value_template(hass, mqtt_mock): +async def test_value_template(hass, mqtt_mock_entry_with_yaml_config): """Test that it fetches the given payload with a template.""" topic = "test/select" await async_setup_component( @@ -110,6 +111,7 @@ async def test_value_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, '{"val":"milk"}') @@ -133,7 +135,7 @@ async def test_value_template(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_run_select_service_optimistic(hass, mqtt_mock): +async def test_run_select_service_optimistic(hass, mqtt_mock_entry_with_yaml_config): """Test that set_value service works in optimistic mode.""" topic = "test/select" @@ -156,6 +158,7 @@ async def test_run_select_service_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("select.test_select") assert state.state == "milk" @@ -174,7 +177,9 @@ async def test_run_select_service_optimistic(hass, mqtt_mock): assert state.state == "beer" -async def test_run_select_service_optimistic_with_command_template(hass, mqtt_mock): +async def test_run_select_service_optimistic_with_command_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test that set_value service works in optimistic mode and with a command_template.""" topic = "test/select" @@ -198,6 +203,7 @@ async def test_run_select_service_optimistic_with_command_template(hass, mqtt_mo }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("select.test_select") assert state.state == "milk" @@ -218,7 +224,7 @@ async def test_run_select_service_optimistic_with_command_template(hass, mqtt_mo assert state.state == "beer" -async def test_run_select_service(hass, mqtt_mock): +async def test_run_select_service(hass, mqtt_mock_entry_with_yaml_config): """Test that set_value service works in non optimistic mode.""" cmd_topic = "test/select/set" state_topic = "test/select" @@ -237,6 +243,7 @@ async def test_run_select_service(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, state_topic, "beer") state = hass.states.get("select.test_select") @@ -253,7 +260,9 @@ async def test_run_select_service(hass, mqtt_mock): assert state.state == "beer" -async def test_run_select_service_with_command_template(hass, mqtt_mock): +async def test_run_select_service_with_command_template( + hass, mqtt_mock_entry_with_yaml_config +): """Test that set_value service works in non optimistic mode and with a command_template.""" cmd_topic = "test/select/set" state_topic = "test/select" @@ -273,6 +282,7 @@ async def test_run_select_service_with_command_template(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, state_topic, "beer") state = hass.states.get("select.test_select") @@ -289,77 +299,91 @@ async def test_run_select_service_with_command_template(hass, mqtt_mock): ) -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG, MQTT_SELECT_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + select.DOMAIN, + DEFAULT_CONFIG, + MQTT_SELECT_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, select.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, select.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, select.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one select per unique_id.""" config = { select.DOMAIN: [ @@ -381,16 +405,20 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, select.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, config + ) -async def test_discovery_removal_select(hass, mqtt_mock, caplog): +async def test_discovery_removal_select(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered select.""" data = json.dumps(DEFAULT_CONFIG[select.DOMAIN]) - await help_test_discovery_removal(hass, mqtt_mock, caplog, select.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, select.DOMAIN, data + ) -async def test_discovery_update_select(hass, mqtt_mock, caplog): +async def test_discovery_update_select(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered select.""" config1 = { "name": "Beer", @@ -406,79 +434,86 @@ async def test_discovery_update_select(hass, mqtt_mock, caplog): } await help_test_discovery_update( - hass, mqtt_mock, caplog, select.DOMAIN, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, select.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_select(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_select( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered select.""" data1 = '{ "name": "Beer", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}' with patch( "homeassistant.components.mqtt.select.MqttSelect.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, select.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + select.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}' await help_test_discovery_broken( - hass, mqtt_mock, caplog, select.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, select.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT select device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT select device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, select.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, select.DOMAIN, DEFAULT_CONFIG, select.SERVICE_SELECT_OPTION, @@ -489,7 +524,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): @pytest.mark.parametrize("options", [["milk", "beer"], ["milk"], []]) -async def test_options_attributes(hass, mqtt_mock, options): +async def test_options_attributes(hass, mqtt_mock_entry_with_yaml_config, options): """Test options attribute.""" topic = "test/select" await async_setup_component( @@ -506,12 +541,15 @@ async def test_options_attributes(hass, mqtt_mock, options): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("select.test_select") assert state.attributes.get(ATTR_OPTIONS) == options -async def test_mqtt_payload_not_an_option_warning(hass, caplog, mqtt_mock): +async def test_mqtt_payload_not_an_option_warning( + hass, caplog, mqtt_mock_entry_with_yaml_config +): """Test warning for MQTT payload which is not a valid option.""" topic = "test/select" await async_setup_component( @@ -528,6 +566,7 @@ async def test_mqtt_payload_not_an_option_warning(hass, caplog, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, topic, "öl") @@ -552,7 +591,14 @@ async def test_mqtt_payload_not_an_option_warning(hass, caplog, mqtt_mock): ], ) async def test_publishing_with_custom_encoding( - hass, mqtt_mock, caplog, service, topic, parameters, payload, template + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + service, + topic, + parameters, + payload, + template, ): """Test publishing MQTT payload with different encoding.""" domain = select.DOMAIN @@ -561,7 +607,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -573,11 +619,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = select.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -595,14 +643,20 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG["select"]) config["options"] = ["milk", "beer"] await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, "select", config, diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index befb5785cdd..894ecc32ecc 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -72,7 +72,9 @@ DEFAULT_CONFIG = { } -async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): +async def test_setting_sensor_value_via_mqtt_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the value via MQTT.""" assert await async_setup_component( hass, @@ -87,6 +89,7 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "test-topic", "100") state = hass.states.get("sensor.test") @@ -122,7 +125,13 @@ async def test_setting_sensor_value_via_mqtt_message(hass, mqtt_mock): ], ) async def test_setting_sensor_native_value_handling_via_mqtt_message( - hass, mqtt_mock, caplog, device_class, native_value, state_value, log + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + device_class, + native_value, + state_value, + log, ): """Test the setting of the value via MQTT.""" assert await async_setup_component( @@ -138,6 +147,7 @@ async def test_setting_sensor_native_value_handling_via_mqtt_message( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "test-topic", native_value) state = hass.states.get("sensor.test") @@ -147,7 +157,9 @@ async def test_setting_sensor_native_value_handling_via_mqtt_message( assert log == ("Invalid state message" in caplog.text) -async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, caplog): +async def test_setting_sensor_value_expires_availability_topic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the expiration of the value.""" assert await async_setup_component( hass, @@ -164,6 +176,7 @@ async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("sensor.test") assert state.state == STATE_UNAVAILABLE @@ -174,10 +187,12 @@ async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, state = hass.states.get("sensor.test") assert state.state == STATE_UNAVAILABLE - await expires_helper(hass, mqtt_mock, caplog) + await expires_helper(hass, caplog) -async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): +async def test_setting_sensor_value_expires( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the expiration of the value.""" assert await async_setup_component( hass, @@ -194,15 +209,16 @@ async def test_setting_sensor_value_expires(hass, mqtt_mock, caplog): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() # State should be unavailable since expire_after is defined and > 0 state = hass.states.get("sensor.test") assert state.state == STATE_UNAVAILABLE - await expires_helper(hass, mqtt_mock, caplog) + await expires_helper(hass, caplog) -async def expires_helper(hass, mqtt_mock, caplog): +async def expires_helper(hass, caplog): """Run the basic expiry code.""" realnow = dt_util.utcnow() now = datetime(realnow.year + 1, 1, 1, 1, tzinfo=dt_util.UTC) @@ -253,7 +269,9 @@ async def expires_helper(hass, mqtt_mock, caplog): assert state.state == STATE_UNAVAILABLE -async def test_setting_sensor_value_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_sensor_value_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the value via MQTT with JSON payload.""" assert await async_setup_component( hass, @@ -269,6 +287,7 @@ async def test_setting_sensor_value_via_mqtt_json_message(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "test-topic", '{ "val": "100" }') state = hass.states.get("sensor.test") @@ -277,7 +296,7 @@ async def test_setting_sensor_value_via_mqtt_json_message(hass, mqtt_mock): async def test_setting_sensor_value_via_mqtt_json_message_and_default_current_state( - hass, mqtt_mock + hass, mqtt_mock_entry_with_yaml_config ): """Test the setting of the value via MQTT with fall back to current state.""" assert await async_setup_component( @@ -294,6 +313,7 @@ async def test_setting_sensor_value_via_mqtt_json_message_and_default_current_st }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message( hass, "test-topic", '{ "val": "valcontent", "par": "parcontent" }' @@ -308,7 +328,9 @@ async def test_setting_sensor_value_via_mqtt_json_message_and_default_current_st assert state.state == "valcontent-parcontent" -async def test_setting_sensor_last_reset_via_mqtt_message(hass, mqtt_mock, caplog): +async def test_setting_sensor_last_reset_via_mqtt_message( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the setting of the last_reset property via MQTT.""" assert await async_setup_component( hass, @@ -325,6 +347,7 @@ async def test_setting_sensor_last_reset_via_mqtt_message(hass, mqtt_mock, caplo }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "last-reset-topic", "2020-01-02 08:11:00") state = hass.states.get("sensor.test") @@ -338,7 +361,7 @@ async def test_setting_sensor_last_reset_via_mqtt_message(hass, mqtt_mock, caplo @pytest.mark.parametrize("datestring", ["2020-21-02 08:11:00", "Hello there!"]) async def test_setting_sensor_bad_last_reset_via_mqtt_message( - hass, caplog, datestring, mqtt_mock + hass, caplog, datestring, mqtt_mock_entry_with_yaml_config ): """Test the setting of the last_reset property via MQTT.""" assert await async_setup_component( @@ -356,6 +379,7 @@ async def test_setting_sensor_bad_last_reset_via_mqtt_message( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "last-reset-topic", datestring) state = hass.states.get("sensor.test") @@ -364,7 +388,7 @@ async def test_setting_sensor_bad_last_reset_via_mqtt_message( async def test_setting_sensor_empty_last_reset_via_mqtt_message( - hass, caplog, mqtt_mock + hass, caplog, mqtt_mock_entry_with_yaml_config ): """Test the setting of the last_reset property via MQTT.""" assert await async_setup_component( @@ -382,6 +406,7 @@ async def test_setting_sensor_empty_last_reset_via_mqtt_message( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "last-reset-topic", "") state = hass.states.get("sensor.test") @@ -389,7 +414,9 @@ async def test_setting_sensor_empty_last_reset_via_mqtt_message( assert "Ignoring empty last_reset message" in caplog.text -async def test_setting_sensor_last_reset_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_sensor_last_reset_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of the value via MQTT with JSON payload.""" assert await async_setup_component( hass, @@ -407,6 +434,7 @@ async def test_setting_sensor_last_reset_via_mqtt_json_message(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message( hass, "last-reset-topic", '{ "last_reset": "2020-01-02 08:11:00" }' @@ -417,7 +445,7 @@ async def test_setting_sensor_last_reset_via_mqtt_json_message(hass, mqtt_mock): @pytest.mark.parametrize("extra", [{}, {"last_reset_topic": "test-topic"}]) async def test_setting_sensor_last_reset_via_mqtt_json_message_2( - hass, mqtt_mock, caplog, extra + hass, mqtt_mock_entry_with_yaml_config, caplog, extra ): """Test the setting of the value via MQTT with JSON payload.""" assert await async_setup_component( @@ -439,6 +467,7 @@ async def test_setting_sensor_last_reset_via_mqtt_json_message_2( }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message( hass, @@ -455,7 +484,7 @@ async def test_setting_sensor_last_reset_via_mqtt_json_message_2( ) -async def test_force_update_disabled(hass, mqtt_mock): +async def test_force_update_disabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, @@ -470,6 +499,7 @@ async def test_force_update_disabled(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() events = [] @@ -488,7 +518,7 @@ async def test_force_update_disabled(hass, mqtt_mock): assert len(events) == 1 -async def test_force_update_enabled(hass, mqtt_mock): +async def test_force_update_enabled(hass, mqtt_mock_entry_with_yaml_config): """Test force update option.""" assert await async_setup_component( hass, @@ -504,6 +534,7 @@ async def test_force_update_enabled(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() events = [] @@ -522,70 +553,80 @@ async def test_force_update_enabled(hass, mqtt_mock): assert len(events) == 2 -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_list_payload(hass, mqtt_mock): +async def test_default_availability_list_payload( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_list_payload_all(hass, mqtt_mock): +async def test_default_availability_list_payload_all( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload_all( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_list_payload_any(hass, mqtt_mock): +async def test_default_availability_list_payload_any( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability by default payload with defined topic.""" await help_test_default_availability_list_payload_any( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_list_single(hass, mqtt_mock, caplog): +async def test_default_availability_list_single( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test availability list and availability_topic are mutually exclusive.""" await help_test_default_availability_list_single( - hass, mqtt_mock, caplog, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_availability(hass, mqtt_mock): +async def test_discovery_update_availability(hass, mqtt_mock_entry_no_yaml_config): """Test availability discovery update.""" await help_test_discovery_update_availability( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_invalid_device_class(hass, mqtt_mock): +async def test_invalid_device_class(hass, mqtt_mock_entry_no_yaml_config): """Test device_class option with invalid value.""" assert await async_setup_component( hass, @@ -600,12 +641,13 @@ async def test_invalid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("sensor.test") assert state is None -async def test_valid_device_class(hass, mqtt_mock): +async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config): """Test device_class option with valid values.""" assert await async_setup_component( hass, @@ -623,6 +665,7 @@ async def test_valid_device_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("sensor.test_1") assert state.attributes["device_class"] == "temperature" @@ -630,7 +673,7 @@ async def test_valid_device_class(hass, mqtt_mock): assert "device_class" not in state.attributes -async def test_invalid_state_class(hass, mqtt_mock): +async def test_invalid_state_class(hass, mqtt_mock_entry_no_yaml_config): """Test state_class option with invalid value.""" assert await async_setup_component( hass, @@ -645,12 +688,13 @@ async def test_invalid_state_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() state = hass.states.get("sensor.test") assert state is None -async def test_valid_state_class(hass, mqtt_mock): +async def test_valid_state_class(hass, mqtt_mock_entry_with_yaml_config): """Test state_class option with valid values.""" assert await async_setup_component( hass, @@ -668,6 +712,7 @@ async def test_valid_state_class(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("sensor.test_1") assert state.attributes["state_class"] == "measurement" @@ -675,49 +720,61 @@ async def test_valid_state_class(hass, mqtt_mock): assert "state_class" not in state.attributes -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG, MQTT_SENSOR_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + sensor.DOMAIN, + DEFAULT_CONFIG, + MQTT_SENSOR_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one sensor per unique_id.""" config = { sensor.DOMAIN: [ @@ -735,16 +792,22 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, sensor.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, config + ) -async def test_discovery_removal_sensor(hass, mqtt_mock, caplog): +async def test_discovery_removal_sensor(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered sensor.""" data = '{ "name": "test", "state_topic": "test_topic" }' - await help_test_discovery_removal(hass, mqtt_mock, caplog, sensor.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, data + ) -async def test_discovery_update_sensor_topic_template(hass, mqtt_mock, caplog): +async def test_discovery_update_sensor_topic_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered sensor.""" config = {"name": "test", "state_topic": "test_topic"} config1 = copy.deepcopy(config) @@ -767,7 +830,7 @@ async def test_discovery_update_sensor_topic_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, config1, @@ -777,7 +840,9 @@ async def test_discovery_update_sensor_topic_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_sensor_template(hass, mqtt_mock, caplog): +async def test_discovery_update_sensor_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered sensor.""" config = {"name": "test", "state_topic": "test_topic"} config1 = copy.deepcopy(config) @@ -798,7 +863,7 @@ async def test_discovery_update_sensor_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, config1, @@ -808,71 +873,79 @@ async def test_discovery_update_sensor_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_unchanged_sensor(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_sensor( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered sensor.""" data1 = '{ "name": "Beer", "state_topic": "test_topic" }' with patch( "homeassistant.components.mqtt.sensor.MqttSensor.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, sensor.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + sensor.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer", "state_topic": "test_topic#" }' data2 = '{ "name": "Milk", "state_topic": "test_topic" }' await help_test_discovery_broken( - hass, mqtt_mock, caplog, sensor.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, sensor.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_hub(hass, mqtt_mock): +async def test_entity_device_info_with_hub(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor device registry integration.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) hub = registry.async_get_or_create( config_entry_id="123", @@ -899,53 +972,57 @@ async def test_entity_device_info_with_hub(hass, mqtt_mock): assert device.via_device_id == hub.id -async def test_entity_debug_info(hass, mqtt_mock): +async def test_entity_debug_info(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" - await help_test_entity_debug_info(hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG) + await help_test_entity_debug_info( + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + ) -async def test_entity_debug_info_max_messages(hass, mqtt_mock): +async def test_entity_debug_info_max_messages(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_max_messages( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG, None + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG, None ) -async def test_entity_debug_info_remove(hass, mqtt_mock): +async def test_entity_debug_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_remove( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_update_entity_id(hass, mqtt_mock): +async def test_entity_debug_info_update_entity_id(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT sensor debug info.""" await help_test_entity_debug_info_update_entity_id( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_disabled_by_default(hass, mqtt_mock): +async def test_entity_disabled_by_default(hass, mqtt_mock_entry_no_yaml_config): """Test entity disabled by default.""" await help_test_entity_disabled_by_default( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG ) @pytest.mark.no_fail_on_log_exception -async def test_entity_category(hass, mqtt_mock): +async def test_entity_category(hass, mqtt_mock_entry_no_yaml_config): """Test entity category.""" - await help_test_entity_category(hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG) + await help_test_entity_category( + hass, mqtt_mock_entry_no_yaml_config, sensor.DOMAIN, DEFAULT_CONFIG + ) -async def test_value_template_with_entity_id(hass, mqtt_mock): +async def test_value_template_with_entity_id(hass, mqtt_mock_entry_with_yaml_config): """Test the access to attributes in value_template via the entity_id.""" assert await async_setup_component( hass, @@ -966,6 +1043,7 @@ async def test_value_template_with_entity_id(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "test-topic", "100") state = hass.states.get("sensor.test") @@ -973,11 +1051,13 @@ async def test_value_template_with_entity_id(hass, mqtt_mock): assert state.state == "101" -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = sensor.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -988,7 +1068,7 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): async def test_cleanup_triggers_and_restoring_state( - hass, mqtt_mock, caplog, tmp_path, freezer + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, freezer ): """Test cleanup old triggers at reloading and restoring the state.""" domain = sensor.DOMAIN @@ -1014,6 +1094,7 @@ async def test_cleanup_triggers_and_restoring_state( {domain: [config1, config2]}, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "test-topic1", "100") state = hass.states.get("sensor.test1") assert state.state == "38" # 100 °F -> 38 °C @@ -1053,7 +1134,7 @@ async def test_cleanup_triggers_and_restoring_state( async def test_skip_restoring_state_with_over_due_expire_trigger( - hass, mqtt_mock, caplog, freezer + hass, mqtt_mock_entry_with_yaml_config, caplog, freezer ): """Test restoring a state with over due expire timer.""" @@ -1081,6 +1162,7 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( ): assert await async_setup_component(hass, domain, {domain: config3}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() assert "Skip state recovery after reload for sensor.test3" in caplog.text @@ -1092,12 +1174,18 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, sensor.DOMAIN, DEFAULT_CONFIG[sensor.DOMAIN], diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index 197ed34b7e4..2db2060c133 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -70,7 +70,7 @@ async def async_turn_off(hass, entity_id=ENTITY_MATCH_ALL) -> None: await hass.services.async_call(siren.DOMAIN, SERVICE_TURN_OFF, data, blocking=True) -async def test_controlling_state_via_topic(hass, mqtt_mock): +async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -87,6 +87,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("siren.test") assert state.state == STATE_UNKNOWN @@ -103,7 +104,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending MQTT commands in optimistic mode.""" assert await async_setup_component( hass, @@ -120,6 +123,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("siren.test") assert state.state == STATE_OFF @@ -143,7 +147,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic_and_json_message( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, @@ -161,6 +167,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("siren.test") assert state.state == STATE_UNKNOWN @@ -181,7 +188,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap async def test_controlling_state_and_attributes_with_json_message_without_template( - hass, mqtt_mock, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the controlling state via topic and JSON message without a value template.""" assert await async_setup_component( @@ -200,6 +207,7 @@ async def test_controlling_state_and_attributes_with_json_message_without_templa }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("siren.test") assert state.state == STATE_UNKNOWN @@ -262,7 +270,9 @@ async def test_controlling_state_and_attributes_with_json_message_without_templa ) -async def test_filtering_not_supported_attributes_optimistic(hass, mqtt_mock): +async def test_filtering_not_supported_attributes_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting attributes with support flags optimistic.""" config = { "platform": "mqtt", @@ -285,6 +295,7 @@ async def test_filtering_not_supported_attributes_optimistic(hass, mqtt_mock): {siren.DOMAIN: [config1, config2, config3]}, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state1 = hass.states.get("siren.test1") assert state1.state == STATE_OFF @@ -345,7 +356,9 @@ async def test_filtering_not_supported_attributes_optimistic(hass, mqtt_mock): assert state3.attributes.get(siren.ATTR_VOLUME_LEVEL) == 0.88 -async def test_filtering_not_supported_attributes_via_state(hass, mqtt_mock): +async def test_filtering_not_supported_attributes_via_state( + hass, mqtt_mock_entry_with_yaml_config +): """Test setting attributes with support flags via state.""" config = { "platform": "mqtt", @@ -371,6 +384,7 @@ async def test_filtering_not_supported_attributes_via_state(hass, mqtt_mock): {siren.DOMAIN: [config1, config2, config3]}, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state1 = hass.states.get("siren.test1") assert state1.state == STATE_UNKNOWN @@ -422,21 +436,23 @@ async def test_filtering_not_supported_attributes_via_state(hass, mqtt_mock): assert state3.attributes.get(siren.ATTR_VOLUME_LEVEL) == 0.88 -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { siren.DOMAIN: { @@ -450,11 +466,17 @@ async def test_default_availability_payload(hass, mqtt_mock): } await help_test_default_availability_payload( - hass, mqtt_mock, siren.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + siren.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { siren.DOMAIN: { @@ -468,11 +490,17 @@ async def test_custom_availability_payload(hass, mqtt_mock): } await help_test_custom_availability_payload( - hass, mqtt_mock, siren.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + siren.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_custom_state_payload(hass, mqtt_mock): +async def test_custom_state_payload(hass, mqtt_mock_entry_with_yaml_config): """Test the state payload.""" assert await async_setup_component( hass, @@ -491,6 +519,7 @@ async def test_custom_state_payload(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("siren.test") assert state.state == STATE_UNKNOWN @@ -507,49 +536,57 @@ async def test_custom_state_payload(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG, {} + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG, {} ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one siren per unique_id.""" config = { siren.DOMAIN: [ @@ -569,20 +606,26 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, siren.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, config + ) -async def test_discovery_removal_siren(hass, mqtt_mock, caplog): +async def test_discovery_removal_siren(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered siren.""" data = ( '{ "name": "test",' ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_removal(hass, mqtt_mock, caplog, siren.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, siren.DOMAIN, data + ) -async def test_discovery_update_siren_topic_template(hass, mqtt_mock, caplog): +async def test_discovery_update_siren_topic_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered siren.""" config1 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) @@ -607,7 +650,7 @@ async def test_discovery_update_siren_topic_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, siren.DOMAIN, config1, @@ -617,7 +660,9 @@ async def test_discovery_update_siren_topic_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_siren_template(hass, mqtt_mock, caplog): +async def test_discovery_update_siren_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered siren.""" config1 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) @@ -640,7 +685,7 @@ async def test_discovery_update_siren_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, siren.DOMAIN, config1, @@ -650,7 +695,7 @@ async def test_discovery_update_siren_template(hass, mqtt_mock, caplog): ) -async def test_command_templates(hass, mqtt_mock, caplog): +async def test_command_templates(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test siren with command templates optimistic.""" config1 = copy.deepcopy(DEFAULT_CONFIG[siren.DOMAIN]) config1["name"] = "Beer" @@ -669,6 +714,7 @@ async def test_command_templates(hass, mqtt_mock, caplog): {siren.DOMAIN: [config1, config2]}, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state1 = hass.states.get("siren.beer") assert state1.state == STATE_OFF @@ -729,7 +775,9 @@ async def test_command_templates(hass, mqtt_mock, caplog): mqtt_mock.reset_mock() -async def test_discovery_update_unchanged_siren(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_siren( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered siren.""" data1 = ( '{ "name": "Beer",' @@ -741,12 +789,17 @@ async def test_discovery_update_unchanged_siren(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.siren.MqttSiren.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, siren.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + siren.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -755,57 +808,57 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ' "command_topic": "test_topic" }' ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, siren.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, siren.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT siren device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT siren device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, siren.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, siren.DOMAIN, DEFAULT_CONFIG, siren.SERVICE_TURN_ON, @@ -834,7 +887,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -849,7 +902,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -861,11 +914,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = siren.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -882,12 +937,18 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, siren.DOMAIN, DEFAULT_CONFIG[siren.DOMAIN], diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index c1017446eff..f20a881dda1 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -87,12 +87,13 @@ DEFAULT_CONFIG_2 = { } -async def test_default_supported_features(hass, mqtt_mock): +async def test_default_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test that the correct supported features.""" assert await async_setup_component( hass, vacuum.DOMAIN, {vacuum.DOMAIN: DEFAULT_CONFIG} ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() entity = hass.states.get("vacuum.mqtttest") entity_features = entity.attributes.get(mqttvacuum.CONF_SUPPORTED_FEATURES, 0) assert sorted(services_to_strings(entity_features, SERVICE_TO_STRING)) == sorted( @@ -100,7 +101,7 @@ async def test_default_supported_features(hass, mqtt_mock): ) -async def test_all_commands(hass, mqtt_mock): +async def test_all_commands(hass, mqtt_mock_entry_with_yaml_config): """Test simple commands send to the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -109,6 +110,7 @@ async def test_all_commands(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( DOMAIN, SERVICE_START, {"entity_id": ENTITY_MATCH_ALL}, blocking=True @@ -171,7 +173,9 @@ async def test_all_commands(hass, mqtt_mock): } -async def test_commands_without_supported_features(hass, mqtt_mock): +async def test_commands_without_supported_features( + hass, mqtt_mock_entry_with_yaml_config +): """Test commands which are not supported by the vacuum.""" config = deepcopy(DEFAULT_CONFIG) services = mqttvacuum.STRING_TO_SERVICE["status"] @@ -181,6 +185,7 @@ async def test_commands_without_supported_features(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() await hass.services.async_call( DOMAIN, SERVICE_START, {"entity_id": ENTITY_MATCH_ALL}, blocking=True @@ -228,7 +233,7 @@ async def test_commands_without_supported_features(hass, mqtt_mock): mqtt_mock.async_publish.assert_not_called() -async def test_status(hass, mqtt_mock): +async def test_status(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -237,6 +242,7 @@ async def test_status(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("vacuum.mqtttest") assert state.state == STATE_UNKNOWN @@ -272,7 +278,7 @@ async def test_status(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_no_fan_vacuum(hass, mqtt_mock): +async def test_no_fan_vacuum(hass, mqtt_mock_entry_with_yaml_config): """Test status updates from the vacuum when fan is not supported.""" config = deepcopy(DEFAULT_CONFIG) del config[mqttvacuum.CONF_FAN_SPEED_LIST] @@ -282,6 +288,7 @@ async def test_no_fan_vacuum(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() message = """{ "battery_level": 54, @@ -323,7 +330,7 @@ async def test_no_fan_vacuum(hass, mqtt_mock): @pytest.mark.no_fail_on_log_exception -async def test_status_invalid_json(hass, mqtt_mock): +async def test_status_invalid_json(hass, mqtt_mock_entry_with_yaml_config): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" config = deepcopy(DEFAULT_CONFIG) config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( @@ -332,83 +339,98 @@ async def test_status_invalid_json(hass, mqtt_mock): assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() async_fire_mqtt_message(hass, "vacuum/state", '{"asdfasas false}') state = hass.states.get("vacuum.mqtttest") assert state.state == STATE_UNKNOWN -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" await help_test_default_availability_payload( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" await help_test_custom_availability_payload( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2, MQTT_VACUUM_ATTRIBUTES_BLOCKED + hass, + mqtt_mock_entry_no_yaml_config, + vacuum.DOMAIN, + DEFAULT_CONFIG_2, + MQTT_VACUUM_ATTRIBUTES_BLOCKED, ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_json( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one vacuum per unique_id.""" config = { vacuum.DOMAIN: [ @@ -428,92 +450,103 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, vacuum.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, config + ) -async def test_discovery_removal_vacuum(hass, mqtt_mock, caplog): +async def test_discovery_removal_vacuum(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered vacuum.""" data = '{ "schema": "state", "name": "test", "command_topic": "test_topic"}' - await help_test_discovery_removal(hass, mqtt_mock, caplog, vacuum.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, data + ) -async def test_discovery_update_vacuum(hass, mqtt_mock, caplog): +async def test_discovery_update_vacuum(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered vacuum.""" 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, config1, config2 + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, config1, config2 ) -async def test_discovery_update_unchanged_vacuum(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_vacuum( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered vacuum.""" data1 = '{ "schema": "state", "name": "Beer", "command_topic": "test_topic"}' with patch( "homeassistant.components.mqtt.vacuum.schema_state.MqttStateVacuum.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + vacuum.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "schema": "state", "name": "Beer", "command_topic": "test_topic#"}' data2 = '{ "schema": "state", "name": "Milk", "command_topic": "test_topic"}' await help_test_discovery_broken( - hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, vacuum.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_with_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + hass, mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, vacuum.DOMAIN, DEFAULT_CONFIG_2, vacuum.SERVICE_START, @@ -564,7 +597,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -590,7 +623,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -602,11 +635,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = vacuum.DOMAIN config = DEFAULT_CONFIG - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -634,12 +669,18 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, vacuum.DOMAIN, DEFAULT_CONFIG, diff --git a/tests/components/mqtt/test_subscription.py b/tests/components/mqtt/test_subscription.py index e2ffc602ddd..7c1663b9c09 100644 --- a/tests/components/mqtt/test_subscription.py +++ b/tests/components/mqtt/test_subscription.py @@ -11,8 +11,9 @@ from homeassistant.core import callback from tests.common import async_fire_mqtt_message -async def test_subscribe_topics(hass, mqtt_mock, caplog): +async def test_subscribe_topics(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test subscription to topics.""" + await mqtt_mock_entry_no_yaml_config() calls1 = [] @callback @@ -59,8 +60,9 @@ async def test_subscribe_topics(hass, mqtt_mock, caplog): assert len(calls2) == 1 -async def test_modify_topics(hass, mqtt_mock, caplog): +async def test_modify_topics(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test modification of topics.""" + await mqtt_mock_entry_no_yaml_config() calls1 = [] @callback @@ -121,8 +123,9 @@ async def test_modify_topics(hass, mqtt_mock, caplog): assert len(calls2) == 1 -async def test_qos_encoding_default(hass, mqtt_mock, caplog): +async def test_qos_encoding_default(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test default qos and encoding.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() @callback def msg_callback(*args): @@ -136,11 +139,12 @@ async def test_qos_encoding_default(hass, mqtt_mock, caplog): {"test_topic1": {"topic": "test-topic1", "msg_callback": msg_callback}}, ) await async_subscribe_topics(hass, sub_state) - mqtt_mock.async_subscribe.assert_called_once_with("test-topic1", ANY, 0, "utf-8") + mqtt_mock.async_subscribe.assert_called_with("test-topic1", ANY, 0, "utf-8") -async def test_qos_encoding_custom(hass, mqtt_mock, caplog): +async def test_qos_encoding_custom(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test custom qos and encoding.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() @callback def msg_callback(*args): @@ -161,11 +165,12 @@ async def test_qos_encoding_custom(hass, mqtt_mock, caplog): }, ) await async_subscribe_topics(hass, sub_state) - mqtt_mock.async_subscribe.assert_called_once_with("test-topic1", ANY, 1, "utf-16") + mqtt_mock.async_subscribe.assert_called_with("test-topic1", ANY, 1, "utf-16") -async def test_no_change(hass, mqtt_mock, caplog): +async def test_no_change(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test subscription to topics without change.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() calls = [] diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 699f0de87f0..b217bf40c22 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -53,7 +53,7 @@ DEFAULT_CONFIG = { } -async def test_controlling_state_via_topic(hass, mqtt_mock): +async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -71,6 +71,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("switch.test") assert state.state == STATE_UNKNOWN @@ -93,7 +94,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_sending_mqtt_commands_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the sending MQTT commands in optimistic mode.""" fake_state = ha.State("switch.test", "on") @@ -116,6 +119,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("switch.test") assert state.state == STATE_ON @@ -139,7 +143,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_sending_inital_state_and_optimistic(hass, mqtt_mock): +async def test_sending_inital_state_and_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): """Test the initial state in optimistic mode.""" assert await async_setup_component( hass, @@ -153,13 +159,16 @@ async def test_sending_inital_state_and_optimistic(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("switch.test") assert state.state == STATE_UNKNOWN assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): +async def test_controlling_state_via_topic_and_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( hass, @@ -177,6 +186,7 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("switch.test") assert state.state == STATE_UNKNOWN @@ -197,21 +207,23 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock): assert state.state == STATE_UNKNOWN -async def test_availability_when_connection_lost(hass, mqtt_mock): +async def test_availability_when_connection_lost( + hass, mqtt_mock_entry_with_yaml_config +): """Test availability after MQTT disconnection.""" await help_test_availability_when_connection_lost( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_availability_without_topic(hass, mqtt_mock): +async def test_availability_without_topic(hass, mqtt_mock_entry_with_yaml_config): """Test availability without defined availability topic.""" await help_test_availability_without_topic( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_default_availability_payload(hass, mqtt_mock): +async def test_default_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by default payload with defined topic.""" config = { switch.DOMAIN: { @@ -225,11 +237,17 @@ async def test_default_availability_payload(hass, mqtt_mock): } await help_test_default_availability_payload( - hass, mqtt_mock, switch.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + switch.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_custom_availability_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock_entry_with_yaml_config): """Test availability by custom payload with defined topic.""" config = { switch.DOMAIN: { @@ -243,11 +261,17 @@ async def test_custom_availability_payload(hass, mqtt_mock): } await help_test_custom_availability_payload( - hass, mqtt_mock, switch.DOMAIN, config, True, "state-topic", "1" + hass, + mqtt_mock_entry_with_yaml_config, + switch.DOMAIN, + config, + True, + "state-topic", + "1", ) -async def test_custom_state_payload(hass, mqtt_mock): +async def test_custom_state_payload(hass, mqtt_mock_entry_with_yaml_config): """Test the state payload.""" assert await async_setup_component( hass, @@ -266,6 +290,7 @@ async def test_custom_state_payload(hass, mqtt_mock): }, ) await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() state = hass.states.get("switch.test") assert state.state == STATE_UNKNOWN @@ -282,49 +307,57 @@ async def test_custom_state_payload(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_with_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_setting_blocked_attribute_via_mqtt_json_message(hass, mqtt_mock): +async def test_setting_blocked_attribute_via_mqtt_json_message( + hass, mqtt_mock_entry_no_yaml_config +): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_blocked_attribute_via_mqtt_json_message( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG, {} + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG, {} ) -async def test_setting_attribute_with_template(hass, mqtt_mock): +async def test_setting_attribute_with_template(hass, mqtt_mock_entry_with_yaml_config): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_with_template( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_not_dict( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_not_dict( - hass, mqtt_mock, caplog, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): +async def test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock_entry_with_yaml_config, caplog +): """Test attributes get extracted from a JSON result.""" await help_test_update_with_json_attrs_bad_JSON( - hass, mqtt_mock, caplog, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, caplog, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_discovery_update_attr(hass, mqtt_mock, caplog): +async def test_discovery_update_attr(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test update of discovered MQTTAttributes.""" await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, caplog, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_unique_id(hass, mqtt_mock): +async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): """Test unique id option only creates one switch per unique_id.""" config = { switch.DOMAIN: [ @@ -344,20 +377,26 @@ async def test_unique_id(hass, mqtt_mock): }, ] } - await help_test_unique_id(hass, mqtt_mock, switch.DOMAIN, config) + await help_test_unique_id( + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, config + ) -async def test_discovery_removal_switch(hass, mqtt_mock, caplog): +async def test_discovery_removal_switch(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered switch.""" data = ( '{ "name": "test",' ' "state_topic": "test_topic",' ' "command_topic": "test_topic" }' ) - await help_test_discovery_removal(hass, mqtt_mock, caplog, switch.DOMAIN, data) + await help_test_discovery_removal( + hass, mqtt_mock_entry_no_yaml_config, caplog, switch.DOMAIN, data + ) -async def test_discovery_update_switch_topic_template(hass, mqtt_mock, caplog): +async def test_discovery_update_switch_topic_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered switch.""" config1 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) @@ -382,7 +421,7 @@ async def test_discovery_update_switch_topic_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, switch.DOMAIN, config1, @@ -392,7 +431,9 @@ async def test_discovery_update_switch_topic_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_switch_template(hass, mqtt_mock, caplog): +async def test_discovery_update_switch_template( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered switch.""" config1 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) config2 = copy.deepcopy(DEFAULT_CONFIG[switch.DOMAIN]) @@ -415,7 +456,7 @@ async def test_discovery_update_switch_template(hass, mqtt_mock, caplog): await help_test_discovery_update( hass, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, caplog, switch.DOMAIN, config1, @@ -425,7 +466,9 @@ async def test_discovery_update_switch_template(hass, mqtt_mock, caplog): ) -async def test_discovery_update_unchanged_switch(hass, mqtt_mock, caplog): +async def test_discovery_update_unchanged_switch( + hass, mqtt_mock_entry_no_yaml_config, caplog +): """Test update of discovered switch.""" data1 = ( '{ "name": "Beer",' @@ -437,12 +480,17 @@ async def test_discovery_update_unchanged_switch(hass, mqtt_mock, caplog): "homeassistant.components.mqtt.switch.MqttSwitch.discovery_update" ) as discovery_update: await help_test_discovery_update_unchanged( - hass, mqtt_mock, caplog, switch.DOMAIN, data1, discovery_update + hass, + mqtt_mock_entry_no_yaml_config, + caplog, + switch.DOMAIN, + data1, + discovery_update, ) @pytest.mark.no_fail_on_log_exception -async def test_discovery_broken(hass, mqtt_mock, caplog): +async def test_discovery_broken(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test handling of bad discovery message.""" data1 = '{ "name": "Beer" }' data2 = ( @@ -451,56 +499,60 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ' "command_topic": "test_topic" }' ) await help_test_discovery_broken( - hass, mqtt_mock, caplog, switch.DOMAIN, data1, data2 + hass, mqtt_mock_entry_no_yaml_config, caplog, switch.DOMAIN, data1, data2 ) -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT switch device registry integration.""" await help_test_entity_device_info_with_connection( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT switch device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_device_info_remove(hass, mqtt_mock): +async def test_entity_device_info_remove(hass, mqtt_mock_entry_no_yaml_config): """Test device registry remove.""" await help_test_entity_device_info_remove( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_subscriptions(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT subscriptions are managed when entity_id is updated.""" await help_test_entity_id_update_subscriptions( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_with_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_id_update_discovery_update(hass, mqtt_mock): +async def test_entity_id_update_discovery_update(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT discovery update when entity_id is updated.""" await help_test_entity_id_update_discovery_update( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + hass, mqtt_mock_entry_no_yaml_config, switch.DOMAIN, DEFAULT_CONFIG ) -async def test_entity_debug_info_message(hass, mqtt_mock): +async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT debug info.""" await help_test_entity_debug_info_message( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG, switch.SERVICE_TURN_ON + hass, + mqtt_mock_entry_no_yaml_config, + switch.DOMAIN, + DEFAULT_CONFIG, + switch.SERVICE_TURN_ON, ) @@ -525,7 +577,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) async def test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, service, topic, @@ -539,7 +591,7 @@ async def test_publishing_with_custom_encoding( await help_test_publishing_with_custom_encoding( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, domain, config, @@ -551,11 +603,13 @@ async def test_publishing_with_custom_encoding( ) -async def test_reloadable(hass, mqtt_mock, caplog, tmp_path): +async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = switch.DOMAIN config = DEFAULT_CONFIG[domain] - await help_test_reloadable(hass, mqtt_mock, caplog, tmp_path, domain, config) + await help_test_reloadable( + hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config + ) async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): @@ -572,12 +626,18 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): ], ) async def test_encoding_subscribable_topics( - hass, mqtt_mock, caplog, topic, value, attribute, attribute_value + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, ): """Test handling of incoming encoded payload.""" await help_test_encoding_subscribable_topics( hass, - mqtt_mock, + mqtt_mock_entry_with_yaml_config, caplog, switch.DOMAIN, DEFAULT_CONFIG[switch.DOMAIN], diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index 99e2fffc085..09be31011f2 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -62,8 +62,11 @@ def tag_mock(): @pytest.mark.no_fail_on_log_exception -async def test_discover_bad_tag(hass, device_reg, entity_reg, mqtt_mock, tag_mock): +async def test_discover_bad_tag( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config, tag_mock +): """Test bad discovery message.""" + await mqtt_mock_entry_no_yaml_config() config1 = copy.deepcopy(DEFAULT_CONFIG_DEVICE) # Test sending bad data @@ -84,9 +87,10 @@ async def test_discover_bad_tag(hass, device_reg, entity_reg, mqtt_mock, tag_moc async def test_if_fires_on_mqtt_message_with_device( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning, with device.""" + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG_DEVICE) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -100,9 +104,10 @@ async def test_if_fires_on_mqtt_message_with_device( async def test_if_fires_on_mqtt_message_without_device( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning, without device.""" + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -115,9 +120,10 @@ async def test_if_fires_on_mqtt_message_without_device( async def test_if_fires_on_mqtt_message_with_template( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning, with device.""" + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG_JSON) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -130,8 +136,9 @@ async def test_if_fires_on_mqtt_message_with_template( tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id) -async def test_strip_tag_id(hass, device_reg, mqtt_mock, tag_mock): +async def test_strip_tag_id(hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock): """Test strip whitespace from tag_id.""" + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -144,9 +151,10 @@ async def test_strip_tag_id(hass, device_reg, mqtt_mock, tag_mock): async def test_if_fires_on_mqtt_message_after_update_with_device( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning after update.""" + await mqtt_mock_entry_no_yaml_config() config1 = copy.deepcopy(DEFAULT_CONFIG_DEVICE) config1["some_future_option_1"] = "future_option_1" config2 = copy.deepcopy(DEFAULT_CONFIG_DEVICE) @@ -190,9 +198,10 @@ async def test_if_fires_on_mqtt_message_after_update_with_device( async def test_if_fires_on_mqtt_message_after_update_without_device( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning after update.""" + await mqtt_mock_entry_no_yaml_config() config1 = copy.deepcopy(DEFAULT_CONFIG) config2 = copy.deepcopy(DEFAULT_CONFIG) config2["topic"] = "foobar/tag_scanned2" @@ -233,9 +242,10 @@ async def test_if_fires_on_mqtt_message_after_update_without_device( async def test_if_fires_on_mqtt_message_after_update_with_template( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning after update.""" + await mqtt_mock_entry_no_yaml_config() config1 = copy.deepcopy(DEFAULT_CONFIG_JSON) config2 = copy.deepcopy(DEFAULT_CONFIG_JSON) config2["value_template"] = "{{ value_json.RDM6300.UID }}" @@ -277,8 +287,11 @@ async def test_if_fires_on_mqtt_message_after_update_with_template( tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id) -async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock): +async def test_no_resubscribe_same_topic( + hass, device_reg, mqtt_mock_entry_no_yaml_config +): """Test subscription to topics without change.""" + mqtt_mock = await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG_DEVICE) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -292,9 +305,10 @@ async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock): async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_with_device( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning after removal.""" + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG_DEVICE) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -325,9 +339,10 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_with_device( async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_without_device( - hass, device_reg, mqtt_mock, tag_mock + hass, device_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test tag scanning not firing after removal.""" + await mqtt_mock_entry_no_yaml_config() config = copy.deepcopy(DEFAULT_CONFIG) async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -360,11 +375,13 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( hass, hass_ws_client, device_reg, - mqtt_mock, + mqtt_mock_entry_no_yaml_config, tag_mock, ): """Test tag scanning after removal.""" assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + await mqtt_mock_entry_no_yaml_config() ws_client = await hass_ws_client(hass) config = copy.deepcopy(DEFAULT_CONFIG_DEVICE) @@ -397,8 +414,9 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( tag_mock.assert_not_called() -async def test_entity_device_info_with_connection(hass, mqtt_mock): +async def test_entity_device_info_with_connection(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT device registry integration.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) data = json.dumps( @@ -427,8 +445,9 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock): assert device.sw_version == "0.1-beta" -async def test_entity_device_info_with_identifier(hass, mqtt_mock): +async def test_entity_device_info_with_identifier(hass, mqtt_mock_entry_no_yaml_config): """Test MQTT device registry integration.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) data = json.dumps( @@ -455,8 +474,9 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): assert device.sw_version == "0.1-beta" -async def test_entity_device_info_update(hass, mqtt_mock): +async def test_entity_device_info_update(hass, mqtt_mock_entry_no_yaml_config): """Test device registry update.""" + await mqtt_mock_entry_no_yaml_config() registry = dr.async_get(hass) config = { @@ -489,9 +509,13 @@ async def test_entity_device_info_update(hass, mqtt_mock): assert device.name == "Milk" -async def test_cleanup_tag(hass, hass_ws_client, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_tag( + hass, hass_ws_client, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test tag discovery topic is cleaned when device is removed from registry.""" assert await async_setup_component(hass, "config", {}) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_no_yaml_config() ws_client = await hass_ws_client(hass) mqtt_entry = hass.config_entries.async_entries("mqtt")[0] @@ -566,8 +590,11 @@ async def test_cleanup_tag(hass, hass_ws_client, device_reg, entity_reg, mqtt_mo ) -async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test removal from device registry when tag is removed.""" + await mqtt_mock_entry_no_yaml_config() config = { "topic": "test-topic", "device": {"identifiers": ["helloworld"]}, @@ -590,9 +617,10 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): async def test_cleanup_device_several_tags( - hass, device_reg, entity_reg, mqtt_mock, tag_mock + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config, tag_mock ): """Test removal from device registry when the last tag is removed.""" + await mqtt_mock_entry_no_yaml_config() config1 = { "topic": "test-topic1", "device": {"identifiers": ["helloworld"]}, @@ -634,12 +662,13 @@ async def test_cleanup_device_several_tags( async def test_cleanup_device_with_entity_and_trigger_1( - hass, device_reg, entity_reg, mqtt_mock + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config ): """Test removal from device registry for device with tag, entity and trigger. Tag removed first, then trigger and entity. """ + await mqtt_mock_entry_no_yaml_config() config1 = { "topic": "test-topic", "device": {"identifiers": ["helloworld"]}, @@ -697,11 +726,14 @@ async def test_cleanup_device_with_entity_and_trigger_1( assert device_entry is None -async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mock): +async def test_cleanup_device_with_entity2( + hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config +): """Test removal from device registry for device with tag, entity and trigger. Trigger and entity removed first, then tag. """ + await mqtt_mock_entry_no_yaml_config() config1 = { "topic": "test-topic", "device": {"identifiers": ["helloworld"]}, diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py index c2c77dcddd8..a4079558c34 100644 --- a/tests/components/mqtt/test_trigger.py +++ b/tests/components/mqtt/test_trigger.py @@ -18,9 +18,10 @@ def calls(hass): @pytest.fixture(autouse=True) -def setup_comp(hass, mqtt_mock): +async def setup_comp(hass, mqtt_mock_entry_no_yaml_config): """Initialize components.""" mock_component(hass, "group") + return await mqtt_mock_entry_no_yaml_config() async def test_if_fires_on_topic_match(hass, calls): @@ -213,7 +214,7 @@ async def test_if_not_fires_on_topic_but_no_payload_match(hass, calls): assert len(calls) == 0 -async def test_encoding_default(hass, calls, mqtt_mock): +async def test_encoding_default(hass, calls, setup_comp): """Test default encoding.""" assert await async_setup_component( hass, @@ -226,10 +227,10 @@ async def test_encoding_default(hass, calls, mqtt_mock): }, ) - mqtt_mock.async_subscribe.assert_called_once_with("test-topic", ANY, 0, "utf-8") + setup_comp.async_subscribe.assert_called_with("test-topic", ANY, 0, "utf-8") -async def test_encoding_custom(hass, calls, mqtt_mock): +async def test_encoding_custom(hass, calls, setup_comp): """Test default encoding.""" assert await async_setup_component( hass, @@ -242,4 +243,4 @@ async def test_encoding_custom(hass, calls, mqtt_mock): }, ) - mqtt_mock.async_subscribe.assert_called_once_with("test-topic", ANY, 0, None) + setup_comp.async_subscribe.assert_called_with("test-topic", ANY, 0, None) diff --git a/tests/components/mqtt_json/test_device_tracker.py b/tests/components/mqtt_json/test_device_tracker.py index d17484cc5e9..b0cba664250 100644 --- a/tests/components/mqtt_json/test_device_tracker.py +++ b/tests/components/mqtt_json/test_device_tracker.py @@ -26,7 +26,7 @@ LOCATION_MESSAGE_INCOMPLETE = {"longitude": 2.0} @pytest.fixture(autouse=True) -def setup_comp(hass, mqtt_mock): +async def setup_comp(hass, mqtt_mock_entry_with_yaml_config): """Initialize components.""" yaml_devices = hass.config.path(YAML_DEVICES) yield diff --git a/tests/components/mqtt_room/test_sensor.py b/tests/components/mqtt_room/test_sensor.py index c3b8704c754..b17a2bed457 100644 --- a/tests/components/mqtt_room/test_sensor.py +++ b/tests/components/mqtt_room/test_sensor.py @@ -47,7 +47,7 @@ async def assert_distance(hass, distance): assert state.attributes.get("distance") == distance -async def test_room_update(hass, mqtt_mock): +async def test_room_update(hass, mqtt_mock_entry_with_yaml_config): """Test the updating between rooms.""" assert await async_setup_component( hass, diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index 0b7e3726482..a97b877d819 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -1,7 +1,7 @@ """The tests for the MQTT discovery.""" import copy import json -from unittest.mock import patch +from unittest.mock import ANY, patch from homeassistant.components.tasmota.const import DEFAULT_PREFIX from homeassistant.components.tasmota.discovery import ALREADY_DISCOVERED @@ -19,9 +19,7 @@ async def test_subscribing_config_topic(hass, mqtt_mock, setup_tasmota): discovery_topic = DEFAULT_PREFIX assert mqtt_mock.async_subscribe.called - call_args = mqtt_mock.async_subscribe.mock_calls[0][1] - assert call_args[0] == discovery_topic + "/#" - assert call_args[2] == 0 + mqtt_mock.async_subscribe.assert_any_call(discovery_topic + "/#", ANY, 0, "utf-8") async def test_future_discovery_message(hass, mqtt_mock, caplog): diff --git a/tests/conftest.py b/tests/conftest.py index 8ea6e114e9a..97b1a959d2c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager import functools import logging import ssl @@ -547,8 +548,19 @@ def mqtt_client_mock(hass): @pytest.fixture -async def mqtt_mock(hass, mqtt_client_mock, mqtt_config): +async def mqtt_mock( + hass, + mqtt_client_mock, + mqtt_config, + mqtt_mock_entry_no_yaml_config, +): """Fixture to mock MQTT component.""" + return await mqtt_mock_entry_no_yaml_config() + + +@asynccontextmanager +async def _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config): + """Fixture to mock a delayed setup of the MQTT config entry.""" if mqtt_config is None: mqtt_config = {mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}} @@ -557,29 +569,79 @@ async def mqtt_mock(hass, mqtt_client_mock, mqtt_config): entry = MockConfigEntry( data=mqtt_config, domain=mqtt.DOMAIN, - title="Tasmota", + title="MQTT", ) - entry.add_to_hass(hass) - # Do not forward the entry setup to the components here - with patch("homeassistant.components.mqtt.PLATFORMS", []): - assert await hass.config_entries.async_setup(entry.entry_id) + + real_mqtt = mqtt.MQTT + real_mqtt_instance = None + mock_mqtt_instance = None + + async def _setup_mqtt_entry(setup_entry): + """Set up the MQTT config entry.""" + assert await setup_entry(hass, entry) + + # Assert that MQTT is setup + assert real_mqtt_instance is not None, "MQTT was not setup correctly" + mock_mqtt_instance.conf = real_mqtt_instance.conf # For diagnostics + mock_mqtt_instance._mqttc = mqtt_client_mock + + # connected set to True to get a more realistic behavior when subscribing + mock_mqtt_instance.connected = True + + hass.helpers.dispatcher.async_dispatcher_send(mqtt.MQTT_CONNECTED) await hass.async_block_till_done() - mqtt_component_mock = MagicMock( - return_value=hass.data["mqtt"], - spec_set=hass.data["mqtt"], - wraps=hass.data["mqtt"], - ) - mqtt_component_mock.conf = hass.data["mqtt"].conf # For diagnostics - mqtt_component_mock._mqttc = mqtt_client_mock - # connected set to True to get a more realistics behavior when subscribing - hass.data["mqtt"].connected = True + return mock_mqtt_instance - hass.data["mqtt"] = mqtt_component_mock - component = hass.data["mqtt"] - component.reset_mock() - return component + def create_mock_mqtt(*args, **kwargs): + """Create a mock based on mqtt.MQTT.""" + nonlocal mock_mqtt_instance + nonlocal real_mqtt_instance + real_mqtt_instance = real_mqtt(*args, **kwargs) + mock_mqtt_instance = MagicMock( + return_value=real_mqtt_instance, + spec_set=real_mqtt_instance, + wraps=real_mqtt_instance, + ) + return mock_mqtt_instance + + with patch("homeassistant.components.mqtt.MQTT", side_effect=create_mock_mqtt): + yield _setup_mqtt_entry + + +@pytest.fixture +async def mqtt_mock_entry_no_yaml_config(hass, mqtt_client_mock, mqtt_config): + """Set up an MQTT config entry without MQTT yaml config.""" + + async def _async_setup_config_entry(hass, entry): + """Help set up the config entry.""" + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + return True + + async def _setup_mqtt_entry(): + """Set up the MQTT config entry.""" + return await mqtt_mock_entry(_async_setup_config_entry) + + async with _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config) as mqtt_mock_entry: + yield _setup_mqtt_entry + + +@pytest.fixture +async def mqtt_mock_entry_with_yaml_config(hass, mqtt_client_mock, mqtt_config): + """Set up an MQTT config entry with MQTT yaml config.""" + + async def _async_do_not_setup_config_entry(hass, entry): + """Do nothing.""" + return True + + async def _setup_mqtt_entry(): + """Set up the MQTT config entry.""" + return await mqtt_mock_entry(_async_do_not_setup_config_entry) + + async with _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config) as mqtt_mock_entry: + yield _setup_mqtt_entry @pytest.fixture(autouse=True) From b74bd1aa0a01f1d853c0b2be611b8876e1cb7357 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 2 Jun 2022 00:12:38 -0500 Subject: [PATCH 1198/3516] Remove announce workaround for Sonos (#72854) --- homeassistant/components/sonos/media_player.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index cd129d82843..f331f980bb4 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -25,7 +25,6 @@ from homeassistant.components.media_player import ( ) from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, - ATTR_MEDIA_ANNOUNCE, ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_ALBUM, MEDIA_TYPE_ARTIST, @@ -544,9 +543,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """ # Use 'replace' as the default enqueue option enqueue = kwargs.get(ATTR_MEDIA_ENQUEUE, MediaPlayerEnqueue.REPLACE) - if kwargs.get(ATTR_MEDIA_ANNOUNCE): - # Temporary workaround until announce support is added - enqueue = MediaPlayerEnqueue.PLAY if spotify.is_spotify_media_type(media_type): media_type = spotify.resolve_spotify_media_type(media_type) From 880590da642ab0e5180754c9b19769c0704ab0a4 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 2 Jun 2022 00:04:14 +0200 Subject: [PATCH 1199/3516] Update frontend to 20220601.0 (#72855) --- 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 d9e80b4eff8..7d07bbd543c 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220531.0"], + "requirements": ["home-assistant-frontend==20220601.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ad2539233f4..076b58b5185 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220531.0 +home-assistant-frontend==20220601.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 6341528b09a..e91ff265ef7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -822,7 +822,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220531.0 +home-assistant-frontend==20220601.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b5767c1e468..964290f6cf8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -589,7 +589,7 @@ hole==0.7.0 holidays==0.13 # homeassistant.components.frontend -home-assistant-frontend==20220531.0 +home-assistant-frontend==20220601.0 # homeassistant.components.home_connect homeconnect==0.7.0 From 01b3da1554247fda7eeb4465dca4acbc641df86b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Jun 2022 19:13:09 -1000 Subject: [PATCH 1200/3516] Ensure recorder shuts down when its startup future is canceled out from under it (#72866) --- homeassistant/components/recorder/core.py | 14 +++++++++++--- tests/components/recorder/test_init.py | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 7df4cf57e56..7a096a9c404 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Iterable +from concurrent.futures import CancelledError import contextlib from datetime import datetime, timedelta import logging @@ -518,9 +519,16 @@ class Recorder(threading.Thread): def _wait_startup_or_shutdown(self) -> object | None: """Wait for startup or shutdown before starting.""" - return asyncio.run_coroutine_threadsafe( - self._async_wait_for_started(), self.hass.loop - ).result() + try: + return asyncio.run_coroutine_threadsafe( + self._async_wait_for_started(), self.hass.loop + ).result() + except CancelledError as ex: + _LOGGER.warning( + "Recorder startup was externally canceled before it could complete: %s", + ex, + ) + return SHUTDOWN_TASK def run(self) -> None: """Start processing events to save.""" diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 41c4428ee5e..87dbce3ba3b 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -118,6 +118,26 @@ async def test_shutdown_before_startup_finishes( assert run_info.end is not None +async def test_canceled_before_startup_finishes( + hass: HomeAssistant, + async_setup_recorder_instance: SetupRecorderInstanceT, + caplog: pytest.LogCaptureFixture, +): + """Test recorder shuts down when its startup future is canceled out from under it.""" + hass.state = CoreState.not_running + await async_setup_recorder_instance(hass) + instance = get_instance(hass) + await instance.async_db_ready + instance._hass_started.cancel() + with patch.object(instance, "engine"): + await hass.async_block_till_done() + await hass.async_add_executor_job(instance.join) + assert ( + "Recorder startup was externally canceled before it could complete" + in caplog.text + ) + + async def test_shutdown_closes_connections(hass, recorder_mock): """Test shutdown closes connections.""" From 8558ea2f9ad582327207b428f251505d00308774 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Jun 2022 19:12:00 -1000 Subject: [PATCH 1201/3516] Fix logbook not setting up with an recorder filter that has empty fields (#72869) --- homeassistant/components/recorder/filters.py | 2 +- tests/components/logbook/test_init.py | 38 +++++++++++++++++++- tests/components/recorder/test_filters.py | 20 +++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 0ceb013d8c5..3077f7f57f3 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -36,7 +36,7 @@ def extract_include_exclude_filter_conf(conf: ConfigType) -> dict[str, Any]: """ return { filter_type: { - matcher: set(conf.get(filter_type, {}).get(matcher, [])) + matcher: set(conf.get(filter_type, {}).get(matcher) or []) for matcher in FITLER_MATCHERS } for filter_type in FILTER_TYPES diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 2903f29f5dc..d33bbd5b8ac 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -11,7 +11,7 @@ from unittest.mock import Mock, patch import pytest import voluptuous as vol -from homeassistant.components import logbook +from homeassistant.components import logbook, recorder from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED from homeassistant.components.logbook.models import LazyEventPartialState @@ -2796,3 +2796,39 @@ async def test_get_events_with_context_state(hass, hass_ws_client, recorder_mock assert results[3]["context_state"] == "off" assert results[3]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" assert "context_event_type" not in results[3] + + +async def test_logbook_with_empty_config(hass, recorder_mock): + """Test we handle a empty configuration.""" + assert await async_setup_component( + hass, + logbook.DOMAIN, + { + logbook.DOMAIN: {}, + recorder.DOMAIN: {}, + }, + ) + await hass.async_block_till_done() + + +async def test_logbook_with_non_iterable_entity_filter(hass, recorder_mock): + """Test we handle a non-iterable entity filter.""" + assert await async_setup_component( + hass, + logbook.DOMAIN, + { + logbook.DOMAIN: { + CONF_EXCLUDE: { + CONF_ENTITIES: ["light.additional_excluded"], + } + }, + recorder.DOMAIN: { + CONF_EXCLUDE: { + CONF_ENTITIES: None, + CONF_DOMAINS: None, + CONF_ENTITY_GLOBS: None, + } + }, + }, + ) + await hass.async_block_till_done() diff --git a/tests/components/recorder/test_filters.py b/tests/components/recorder/test_filters.py index fa80df6e345..5c0afa10f9d 100644 --- a/tests/components/recorder/test_filters.py +++ b/tests/components/recorder/test_filters.py @@ -12,6 +12,13 @@ from homeassistant.helpers.entityfilter import ( CONF_INCLUDE, ) +EMPTY_INCLUDE_FILTER = { + CONF_INCLUDE: { + CONF_DOMAINS: None, + CONF_ENTITIES: None, + CONF_ENTITY_GLOBS: None, + } +} SIMPLE_INCLUDE_FILTER = { CONF_INCLUDE: { CONF_DOMAINS: ["homeassistant"], @@ -87,6 +94,19 @@ def test_extract_include_exclude_filter_conf(): assert SIMPLE_INCLUDE_EXCLUDE_FILTER[CONF_EXCLUDE][CONF_ENTITIES] != { "cover.altered" } + empty_include_filter = extract_include_exclude_filter_conf(EMPTY_INCLUDE_FILTER) + assert empty_include_filter == { + CONF_EXCLUDE: { + CONF_DOMAINS: set(), + CONF_ENTITIES: set(), + CONF_ENTITY_GLOBS: set(), + }, + CONF_INCLUDE: { + CONF_DOMAINS: set(), + CONF_ENTITIES: set(), + CONF_ENTITY_GLOBS: set(), + }, + } def test_merge_include_exclude_filters(): From b28b204b8643ec0bf09e371b7c3a2d5366cd0d86 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Jun 2022 05:39:53 -1000 Subject: [PATCH 1202/3516] Only present history_stats state as unknown if the time is in the future (#72880) --- homeassistant/components/history_stats/data.py | 9 +++------ tests/components/history_stats/test_sensor.py | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 8153557422d..5466498fc32 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -73,7 +73,8 @@ class HistoryStats: # History cannot tell the future self._history_current_period = [] self._previous_run_before_start = True - + self._state = HistoryStatsState(None, None, self._period) + return self._state # # We avoid querying the database if the below did NOT happen: # @@ -82,7 +83,7 @@ class HistoryStats: # - The period shrank in size # - The previous period ended before now # - elif ( + if ( not self._previous_run_before_start and current_period_start_timestamp == previous_period_start_timestamp and ( @@ -117,10 +118,6 @@ class HistoryStats: ) self._previous_run_before_start = False - if not self._history_current_period: - self._state = HistoryStatsState(None, None, self._period) - return self._state - hours_matched, match_count = self._async_compute_hours_and_changes( now_timestamp, current_period_start_timestamp, diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index b375a8f63c4..f824ee552ca 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -357,7 +357,7 @@ async def test_measure_multiple(hass, recorder_mock): await hass.async_block_till_done() assert hass.states.get("sensor.sensor1").state == "0.5" - assert hass.states.get("sensor.sensor2").state == STATE_UNKNOWN + assert hass.states.get("sensor.sensor2").state == "0.0" assert hass.states.get("sensor.sensor3").state == "2" assert hass.states.get("sensor.sensor4").state == "50.0" From 54b94c48269f2519a0e1f06b82c82a7b0e839cb4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Jun 2022 02:01:06 -1000 Subject: [PATCH 1203/3516] Fix migration of MySQL data when InnoDB is not being used (#72893) Fixes #72883 --- homeassistant/components/recorder/migration.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index bc636d34b10..cc5af684566 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -715,14 +715,13 @@ def _apply_update( # noqa: C901 if engine.dialect.name == SupportedDialect.MYSQL: # Ensure the row format is dynamic or the index # unique will be too large - with session_scope(session=session_maker()) as session: - connection = session.connection() - # This is safe to run multiple times and fast since the table is small - connection.execute( - text( - "ALTER TABLE statistics_meta ENGINE=InnoDB, ROW_FORMAT=DYNAMIC" + with contextlib.suppress(SQLAlchemyError): + with session_scope(session=session_maker()) as session: + connection = session.connection() + # This is safe to run multiple times and fast since the table is small + connection.execute( + text("ALTER TABLE statistics_meta ROW_FORMAT=DYNAMIC") ) - ) try: _create_index( session_maker, "statistics_meta", "ix_statistics_meta_statistic_id" From a4297c04116c343955b4580cc93483b7f7414806 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Jun 2022 11:54:06 -1000 Subject: [PATCH 1204/3516] Fix performance of logbook entity and devices queries with large MySQL databases (#72898) --- .../components/logbook/queries/common.py | 20 +++++++-- .../components/logbook/queries/devices.py | 32 +++++++++----- .../components/logbook/queries/entities.py | 39 ++++++++++------ .../logbook/queries/entities_and_devices.py | 44 ++++++++++++------- homeassistant/components/recorder/models.py | 2 + 5 files changed, 93 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/logbook/queries/common.py b/homeassistant/components/logbook/queries/common.py index 6049d6beb81..a7a4f84a59e 100644 --- a/homeassistant/components/logbook/queries/common.py +++ b/homeassistant/components/logbook/queries/common.py @@ -12,9 +12,11 @@ from sqlalchemy.sql.selectable import Select from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN from homeassistant.components.recorder.models import ( + EVENTS_CONTEXT_ID_INDEX, OLD_FORMAT_ATTRS_JSON, OLD_STATE, SHARED_ATTRS_JSON, + STATES_CONTEXT_ID_INDEX, EventData, Events, StateAttributes, @@ -121,9 +123,7 @@ def select_events_context_only() -> Select: By marking them as context_only we know they are only for linking context ids and we can avoid processing them. """ - return select(*EVENT_ROWS_NO_STATES, CONTEXT_ONLY).outerjoin( - EventData, (Events.data_id == EventData.data_id) - ) + return select(*EVENT_ROWS_NO_STATES, CONTEXT_ONLY) def select_states_context_only() -> Select: @@ -252,3 +252,17 @@ def _not_uom_attributes_matcher() -> ClauseList: return ~StateAttributes.shared_attrs.like( UNIT_OF_MEASUREMENT_JSON_LIKE ) | ~States.attributes.like(UNIT_OF_MEASUREMENT_JSON_LIKE) + + +def apply_states_context_hints(query: Query) -> Query: + """Force mysql to use the right index on large context_id selects.""" + return query.with_hint( + States, f"FORCE INDEX ({STATES_CONTEXT_ID_INDEX})", dialect_name="mysql" + ) + + +def apply_events_context_hints(query: Query) -> Query: + """Force mysql to use the right index on large context_id selects.""" + return query.with_hint( + Events, f"FORCE INDEX ({EVENTS_CONTEXT_ID_INDEX})", dialect_name="mysql" + ) diff --git a/homeassistant/components/logbook/queries/devices.py b/homeassistant/components/logbook/queries/devices.py index 64a6477017e..88e9f50a42c 100644 --- a/homeassistant/components/logbook/queries/devices.py +++ b/homeassistant/components/logbook/queries/devices.py @@ -4,15 +4,22 @@ from __future__ import annotations from collections.abc import Iterable from datetime import datetime as dt -from sqlalchemy import lambda_stmt, select, union_all +from sqlalchemy import lambda_stmt, select from sqlalchemy.orm import Query from sqlalchemy.sql.elements import ClauseList from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import CTE, CompoundSelect -from homeassistant.components.recorder.models import DEVICE_ID_IN_EVENT, Events, States +from homeassistant.components.recorder.models import ( + DEVICE_ID_IN_EVENT, + EventData, + Events, + States, +) from .common import ( + apply_events_context_hints, + apply_states_context_hints, select_events_context_id_subquery, select_events_context_only, select_events_without_states, @@ -27,13 +34,10 @@ def _select_device_id_context_ids_sub_query( json_quotable_device_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple devices.""" - return select( - union_all( - select_events_context_id_subquery(start_day, end_day, event_types).where( - apply_event_device_id_matchers(json_quotable_device_ids) - ), - ).c.context_id + inner = select_events_context_id_subquery(start_day, end_day, event_types).where( + apply_event_device_id_matchers(json_quotable_device_ids) ) + return select(inner.c.context_id).group_by(inner.c.context_id) def _apply_devices_context_union( @@ -51,8 +55,16 @@ def _apply_devices_context_union( json_quotable_device_ids, ).cte() return query.union_all( - select_events_context_only().where(Events.context_id.in_(devices_cte.select())), - select_states_context_only().where(States.context_id.in_(devices_cte.select())), + apply_events_context_hints( + select_events_context_only() + .select_from(devices_cte) + .outerjoin(Events, devices_cte.c.context_id == Events.context_id) + ).outerjoin(EventData, (Events.data_id == EventData.data_id)), + apply_states_context_hints( + select_states_context_only() + .select_from(devices_cte) + .outerjoin(States, devices_cte.c.context_id == States.context_id) + ), ) diff --git a/homeassistant/components/logbook/queries/entities.py b/homeassistant/components/logbook/queries/entities.py index 4fb211688f3..8de4a5eaf64 100644 --- a/homeassistant/components/logbook/queries/entities.py +++ b/homeassistant/components/logbook/queries/entities.py @@ -14,11 +14,14 @@ from homeassistant.components.recorder.models import ( ENTITY_ID_IN_EVENT, ENTITY_ID_LAST_UPDATED_INDEX, OLD_ENTITY_ID_IN_EVENT, + EventData, Events, States, ) from .common import ( + apply_events_context_hints, + apply_states_context_hints, apply_states_filters, select_events_context_id_subquery, select_events_context_only, @@ -36,16 +39,15 @@ def _select_entities_context_ids_sub_query( json_quotable_entity_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple entities.""" - return select( - union_all( - select_events_context_id_subquery(start_day, end_day, event_types).where( - apply_event_entity_id_matchers(json_quotable_entity_ids) - ), - apply_entities_hints(select(States.context_id)) - .filter((States.last_updated > start_day) & (States.last_updated < end_day)) - .where(States.entity_id.in_(entity_ids)), - ).c.context_id + union = union_all( + select_events_context_id_subquery(start_day, end_day, event_types).where( + apply_event_entity_id_matchers(json_quotable_entity_ids) + ), + apply_entities_hints(select(States.context_id)) + .filter((States.last_updated > start_day) & (States.last_updated < end_day)) + .where(States.entity_id.in_(entity_ids)), ) + return select(union.c.context_id).group_by(union.c.context_id) def _apply_entities_context_union( @@ -64,14 +66,23 @@ def _apply_entities_context_union( entity_ids, json_quotable_entity_ids, ).cte() + # We used to optimize this to exclude rows we already in the union with + # a States.entity_id.not_in(entity_ids) but that made the + # query much slower on MySQL, and since we already filter them away + # in the python code anyways since they will have context_only + # set on them the impact is minimal. return query.union_all( states_query_for_entity_ids(start_day, end_day, entity_ids), - select_events_context_only().where( - Events.context_id.in_(entities_cte.select()) + apply_events_context_hints( + select_events_context_only() + .select_from(entities_cte) + .outerjoin(Events, entities_cte.c.context_id == Events.context_id) + ).outerjoin(EventData, (Events.data_id == EventData.data_id)), + apply_states_context_hints( + select_states_context_only() + .select_from(entities_cte) + .outerjoin(States, entities_cte.c.context_id == States.context_id) ), - select_states_context_only() - .where(States.entity_id.not_in(entity_ids)) - .where(States.context_id.in_(entities_cte.select())), ) diff --git a/homeassistant/components/logbook/queries/entities_and_devices.py b/homeassistant/components/logbook/queries/entities_and_devices.py index d1c86ddbec5..1c4271422b7 100644 --- a/homeassistant/components/logbook/queries/entities_and_devices.py +++ b/homeassistant/components/logbook/queries/entities_and_devices.py @@ -10,9 +10,11 @@ from sqlalchemy.orm import Query from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import CTE, CompoundSelect -from homeassistant.components.recorder.models import Events, States +from homeassistant.components.recorder.models import EventData, Events, States from .common import ( + apply_events_context_hints, + apply_states_context_hints, select_events_context_id_subquery, select_events_context_only, select_events_without_states, @@ -35,18 +37,17 @@ def _select_entities_device_id_context_ids_sub_query( json_quotable_device_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple entities and multiple devices.""" - return select( - union_all( - select_events_context_id_subquery(start_day, end_day, event_types).where( - _apply_event_entity_id_device_id_matchers( - json_quotable_entity_ids, json_quotable_device_ids - ) - ), - apply_entities_hints(select(States.context_id)) - .filter((States.last_updated > start_day) & (States.last_updated < end_day)) - .where(States.entity_id.in_(entity_ids)), - ).c.context_id + union = union_all( + select_events_context_id_subquery(start_day, end_day, event_types).where( + _apply_event_entity_id_device_id_matchers( + json_quotable_entity_ids, json_quotable_device_ids + ) + ), + apply_entities_hints(select(States.context_id)) + .filter((States.last_updated > start_day) & (States.last_updated < end_day)) + .where(States.entity_id.in_(entity_ids)), ) + return select(union.c.context_id).group_by(union.c.context_id) def _apply_entities_devices_context_union( @@ -66,14 +67,23 @@ def _apply_entities_devices_context_union( json_quotable_entity_ids, json_quotable_device_ids, ).cte() + # We used to optimize this to exclude rows we already in the union with + # a States.entity_id.not_in(entity_ids) but that made the + # query much slower on MySQL, and since we already filter them away + # in the python code anyways since they will have context_only + # set on them the impact is minimal. return query.union_all( states_query_for_entity_ids(start_day, end_day, entity_ids), - select_events_context_only().where( - Events.context_id.in_(devices_entities_cte.select()) + apply_events_context_hints( + select_events_context_only() + .select_from(devices_entities_cte) + .outerjoin(Events, devices_entities_cte.c.context_id == Events.context_id) + ).outerjoin(EventData, (Events.data_id == EventData.data_id)), + apply_states_context_hints( + select_states_context_only() + .select_from(devices_entities_cte) + .outerjoin(States, devices_entities_cte.c.context_id == States.context_id) ), - select_states_context_only() - .where(States.entity_id.not_in(entity_ids)) - .where(States.context_id.in_(devices_entities_cte.select())), ) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 70c816c2af5..8db648f15a8 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -93,6 +93,8 @@ TABLES_TO_CHECK = [ LAST_UPDATED_INDEX = "ix_states_last_updated" ENTITY_ID_LAST_UPDATED_INDEX = "ix_states_entity_id_last_updated" +EVENTS_CONTEXT_ID_INDEX = "ix_events_context_id" +STATES_CONTEXT_ID_INDEX = "ix_states_context_id" EMPTY_JSON_OBJECT = "{}" From 2b77db259775fbadaeab032ad73888c42c41cfce Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 2 Jun 2022 20:32:31 +0200 Subject: [PATCH 1205/3516] Fix reload of MQTT yaml config (#72901) --- .../components/mqtt/alarm_control_panel.py | 18 +++--- .../components/mqtt/binary_sensor.py | 18 +++--- homeassistant/components/mqtt/button.py | 18 +++--- homeassistant/components/mqtt/camera.py | 18 +++--- homeassistant/components/mqtt/climate.py | 18 +++--- homeassistant/components/mqtt/cover.py | 18 +++--- homeassistant/components/mqtt/fan.py | 18 +++--- homeassistant/components/mqtt/humidifier.py | 21 +++---- .../components/mqtt/light/__init__.py | 18 +++--- homeassistant/components/mqtt/lock.py | 18 +++--- homeassistant/components/mqtt/mixins.py | 58 ++++++++++++++++--- homeassistant/components/mqtt/number.py | 18 +++--- homeassistant/components/mqtt/scene.py | 18 +++--- homeassistant/components/mqtt/select.py | 18 +++--- homeassistant/components/mqtt/sensor.py | 18 +++--- homeassistant/components/mqtt/siren.py | 18 +++--- homeassistant/components/mqtt/switch.py | 18 +++--- .../components/mqtt/vacuum/__init__.py | 18 +++--- tests/components/mqtt/test_binary_sensor.py | 2 +- tests/components/mqtt/test_common.py | 53 ++++++++++++----- tests/components/mqtt/test_sensor.py | 2 +- 21 files changed, 241 insertions(+), 183 deletions(-) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index c20fbb7c657..c0c6f9732d7 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -1,7 +1,6 @@ """This platform enables the possibility to control a MQTT alarm.""" from __future__ import annotations -import asyncio import functools import logging import re @@ -45,8 +44,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -133,7 +132,11 @@ async def async_setup_platform( """Set up MQTT alarm control panel configured under the alarm_control_panel key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, alarm.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + alarm.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -144,13 +147,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT alarm control panel through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, alarm.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, alarm.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 1cb90d6c903..cec065e20f2 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -1,7 +1,6 @@ """Support for MQTT binary sensors.""" from __future__ import annotations -import asyncio from datetime import timedelta import functools import logging @@ -42,8 +41,8 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -88,7 +87,11 @@ async def async_setup_platform( """Set up MQTT binary sensor configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, binary_sensor.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + binary_sensor.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -99,12 +102,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT binary sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, binary_sensor.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, binary_sensor.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index b50856d20c1..afa9900db35 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -1,7 +1,6 @@ """Support for MQTT buttons.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -26,8 +25,8 @@ from .const import ( from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -68,7 +67,11 @@ async def async_setup_platform( """Set up MQTT button configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, button.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + button.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -79,12 +82,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT button through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, button.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, button.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index ae38e07d17a..86db828b111 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -1,7 +1,6 @@ """Camera that loads a picture from an MQTT topic.""" from __future__ import annotations -import asyncio from base64 import b64decode import functools @@ -23,8 +22,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -66,7 +65,11 @@ async def async_setup_platform( """Set up MQTT camera configured under the camera platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, camera.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + camera.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -77,12 +80,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT camera through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, camera.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, camera.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 64b462359be..bdcc82f2c39 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -1,7 +1,6 @@ """Support for MQTT climate devices.""" from __future__ import annotations -import asyncio import functools import logging @@ -51,8 +50,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -377,7 +376,11 @@ async def async_setup_platform( """Set up MQTT climate configured under the fan platform key (deprecated).""" # The use of PLATFORM_SCHEMA is deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, climate.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + climate.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -388,12 +391,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT climate device through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, climate.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, climate.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 5814f3e43f7..325433817c0 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -1,7 +1,6 @@ """Support for MQTT cover devices.""" from __future__ import annotations -import asyncio import functools from json import JSONDecodeError, loads as json_loads import logging @@ -46,8 +45,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -227,7 +226,11 @@ async def async_setup_platform( """Set up MQTT covers configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, cover.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + cover.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -238,13 +241,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT cover through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, cover.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, cover.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index f72b0bdf689..d0b4ff10692 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -1,7 +1,6 @@ """Support for MQTT fans.""" from __future__ import annotations -import asyncio import functools import logging import math @@ -50,8 +49,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -217,7 +216,11 @@ async def async_setup_platform( """Set up MQTT fans configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, fan.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + fan.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -228,13 +231,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, fan.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, fan.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 000a9b9700e..1c9ec5dc201 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -1,7 +1,6 @@ """Support for MQTT humidifiers.""" from __future__ import annotations -import asyncio import functools import logging @@ -46,8 +45,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -173,7 +172,11 @@ async def async_setup_platform( """Set up MQTT humidifier configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, humidifier.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + humidifier.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -184,14 +187,12 @@ async def async_setup_entry( ) -> None: """Set up MQTT humidifier through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, humidifier.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, humidifier.DOMAIN, PLATFORM_SCHEMA_MODERN ) - ) # setup for discovery + ) + # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index ab2a3462615..158ea6ffa0d 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -1,7 +1,6 @@ """Support for MQTT lights.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -14,8 +13,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ..mixins import ( - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -97,7 +96,11 @@ async def async_setup_platform( """Set up MQTT light through configuration.yaml (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, light.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + light.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -108,13 +111,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT lights configured under the light platform key (deprecated).""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, light.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, light.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 0cfd1d2b70f..862e76635f7 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -1,7 +1,6 @@ """Support for MQTT locks.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -28,8 +27,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -88,7 +87,11 @@ async def async_setup_platform( """Set up MQTT locks configured under the lock platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, lock.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + lock.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -99,13 +102,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT lock through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, lock.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, lock.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 694fae0b3c0..b0f17cc335b 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -2,6 +2,7 @@ from __future__ import annotations from abc import abstractmethod +import asyncio from collections.abc import Callable import json import logging @@ -27,10 +28,11 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, + discovery, entity_registry as er, ) from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED @@ -46,7 +48,10 @@ from homeassistant.helpers.entity import ( async_generate_entity_id, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.reload import ( + async_integration_yaml_config, + async_setup_reload_service, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import debug_info, subscription @@ -260,8 +265,44 @@ class SetupEntity(Protocol): """Define setup_entities type.""" +async def async_setup_platform_discovery( + hass: HomeAssistant, platform_domain: str, schema: vol.Schema +) -> CALLBACK_TYPE: + """Set up platform discovery for manual config.""" + + async def _async_discover_entities(event: Event | None) -> None: + """Discover entities for a platform.""" + if event: + # The platform has been reloaded + config_yaml = await async_integration_yaml_config(hass, DOMAIN) + if not config_yaml: + return + config_yaml = config_yaml.get(DOMAIN, {}) + else: + config_yaml = hass.data.get(DATA_MQTT_CONFIG, {}) + if not config_yaml: + return + if platform_domain not in config_yaml: + return + await asyncio.gather( + *( + discovery.async_load_platform(hass, platform_domain, DOMAIN, config, {}) + for config in await async_get_platform_config_from_yaml( + hass, platform_domain, schema, config_yaml + ) + ) + ) + + unsub = hass.bus.async_listen("event_mqtt_reloaded", _async_discover_entities) + await _async_discover_entities(None) + return unsub + + async def async_get_platform_config_from_yaml( - hass: HomeAssistant, domain: str, schema: vol.Schema + hass: HomeAssistant, + platform_domain: str, + schema: vol.Schema, + config_yaml: ConfigType = None, ) -> list[ConfigType]: """Return a list of validated configurations for the domain.""" @@ -275,12 +316,15 @@ async def async_get_platform_config_from_yaml( try: validated_config.append(schema(config_item)) except vol.MultipleInvalid as err: - async_log_exception(err, domain, config_item, hass) + async_log_exception(err, platform_domain, config_item, hass) return validated_config - config_yaml: ConfigType = hass.data.get(DATA_MQTT_CONFIG, {}) - if not (platform_configs := config_yaml.get(domain)): + if config_yaml is None: + config_yaml = hass.data.get(DATA_MQTT_CONFIG) + if not config_yaml: + return [] + if not (platform_configs := config_yaml.get(platform_domain)): return [] return async_validate_config(hass, platform_configs) @@ -310,7 +354,7 @@ async def async_setup_entry_helper(hass, domain, async_setup, schema): async def async_setup_platform_helper( hass: HomeAssistant, platform_domain: str, - config: ConfigType, + config: ConfigType | DiscoveryInfoType, async_add_entities: AddEntitiesCallback, async_setup_entities: SetupEntity, ) -> None: diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 6ea1f0959f6..1404dc86a3c 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -1,7 +1,6 @@ """Configure number in a device through MQTT topic.""" from __future__ import annotations -import asyncio import functools import logging @@ -41,8 +40,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -119,7 +118,11 @@ async def async_setup_platform( """Set up MQTT number configured under the number platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, number.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + number.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -130,12 +133,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT number through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, number.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, number.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index ce8f0b0a3e8..9c4a212bd8e 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -1,7 +1,6 @@ """Support for MQTT scenes.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -23,8 +22,8 @@ from .mixins import ( CONF_OBJECT_ID, MQTT_AVAILABILITY_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -65,7 +64,11 @@ async def async_setup_platform( """Set up MQTT scene configured under the scene platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, scene.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + scene.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -76,13 +79,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT scene through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, scene.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, scene.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 75e1b4e8efd..994c11653b7 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -1,7 +1,6 @@ """Configure select in a device through MQTT topic.""" from __future__ import annotations -import asyncio import functools import logging @@ -31,8 +30,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -80,7 +79,11 @@ async def async_setup_platform( """Set up MQTT select configured under the select platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, select.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + select.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -91,12 +94,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT select through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, select.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, select.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 4dd1ad4d95f..f9e0b5151bb 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -1,7 +1,6 @@ """Support for MQTT sensors.""" from __future__ import annotations -import asyncio from datetime import timedelta import functools import logging @@ -42,8 +41,8 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -133,7 +132,11 @@ async def async_setup_platform( """Set up MQTT sensors configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, sensor.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + sensor.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -144,12 +147,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, sensor.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, sensor.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index 1ecf2c37dbf..fef2a4fb3dd 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -1,7 +1,6 @@ """Support for MQTT sirens.""" from __future__ import annotations -import asyncio import copy import functools import json @@ -52,8 +51,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -129,7 +128,11 @@ async def async_setup_platform( """Set up MQTT sirens configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, siren.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + siren.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -140,13 +143,8 @@ async def async_setup_entry( ) -> None: """Set up MQTT siren through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, siren.DOMAIN, PLATFORM_SCHEMA_MODERN - ) - ) + config_entry.async_on_unload( + await async_setup_platform_discovery(hass, siren.DOMAIN, PLATFORM_SCHEMA_MODERN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index c20ddfe5151..be7fc655e1e 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -1,7 +1,6 @@ """Support for MQTT switches.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -38,8 +37,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -83,7 +82,11 @@ async def async_setup_platform( """Set up MQTT switch configured under the fan platform key (deprecated).""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, switch.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + switch.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -94,12 +97,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT switch through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, switch.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, switch.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 34205ab7780..206a15a024a 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -1,7 +1,6 @@ """Support for MQTT vacuums.""" from __future__ import annotations -import asyncio import functools import voluptuous as vol @@ -13,8 +12,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ..mixins import ( - async_get_platform_config_from_yaml, async_setup_entry_helper, + async_setup_platform_discovery, async_setup_platform_helper, ) from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE @@ -77,7 +76,11 @@ async def async_setup_platform( """Set up MQTT vacuum through configuration.yaml.""" # Deprecated in HA Core 2022.6 await async_setup_platform_helper( - hass, vacuum.DOMAIN, config, async_add_entities, _async_setup_entity + hass, + vacuum.DOMAIN, + discovery_info or config, + async_add_entities, + _async_setup_entity, ) @@ -88,12 +91,9 @@ async def async_setup_entry( ) -> None: """Set up MQTT vacuum through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - await asyncio.gather( - *( - _async_setup_entity(hass, async_add_entities, config, config_entry) - for config in await async_get_platform_config_from_yaml( - hass, vacuum.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + config_entry.async_on_unload( + await async_setup_platform_discovery( + hass, vacuum.DOMAIN, PLATFORM_SCHEMA_MODERN ) ) # setup for discovery diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 37bb783d354..ebb1d78138f 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -1011,7 +1011,7 @@ async def test_cleanup_triggers_and_restoring_state( freezer.move_to("2022-02-02 12:01:10+01:00") await help_test_reload_with_config( - hass, caplog, tmp_path, domain, [config1, config2] + hass, caplog, tmp_path, {domain: [config1, config2]} ) assert "Clean up expire after trigger for binary_sensor.test1" in caplog.text assert "Clean up expire after trigger for binary_sensor.test2" not in caplog.text diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 50cf7beb0e0..24482129f3d 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -1651,10 +1651,10 @@ async def help_test_publishing_with_custom_encoding( mqtt_mock.async_publish.reset_mock() -async def help_test_reload_with_config(hass, caplog, tmp_path, domain, config): +async def help_test_reload_with_config(hass, caplog, tmp_path, config): """Test reloading with supplied config.""" new_yaml_config_file = tmp_path / "configuration.yaml" - new_yaml_config = yaml.dump({domain: config}) + new_yaml_config = yaml.dump(config) new_yaml_config_file.write_text(new_yaml_config) assert new_yaml_config_file.read_text() == new_yaml_config @@ -1679,16 +1679,27 @@ async def help_test_reloadable( old_config_1["name"] = "test_old_1" old_config_2 = copy.deepcopy(config) old_config_2["name"] = "test_old_2" + old_config_3 = copy.deepcopy(config) + old_config_3["name"] = "test_old_3" + old_config_3.pop("platform") + old_config_4 = copy.deepcopy(config) + old_config_4["name"] = "test_old_4" + old_config_4.pop("platform") - assert await async_setup_component( - hass, domain, {domain: [old_config_1, old_config_2]} - ) + old_config = { + domain: [old_config_1, old_config_2], + "mqtt": {domain: [old_config_3, old_config_4]}, + } + + assert await async_setup_component(hass, domain, old_config) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() assert hass.states.get(f"{domain}.test_old_1") assert hass.states.get(f"{domain}.test_old_2") - assert len(hass.states.async_all(domain)) == 2 + assert hass.states.get(f"{domain}.test_old_3") + assert hass.states.get(f"{domain}.test_old_4") + assert len(hass.states.async_all(domain)) == 4 # Create temporary fixture for configuration.yaml based on the supplied config and # test a reload with this new config @@ -1698,16 +1709,31 @@ async def help_test_reloadable( new_config_2["name"] = "test_new_2" new_config_3 = copy.deepcopy(config) new_config_3["name"] = "test_new_3" + new_config_3.pop("platform") + new_config_4 = copy.deepcopy(config) + new_config_4["name"] = "test_new_4" + new_config_4.pop("platform") + new_config_5 = copy.deepcopy(config) + new_config_5["name"] = "test_new_5" + new_config_6 = copy.deepcopy(config) + new_config_6["name"] = "test_new_6" + new_config_6.pop("platform") - await help_test_reload_with_config( - hass, caplog, tmp_path, domain, [new_config_1, new_config_2, new_config_3] - ) + new_config = { + domain: [new_config_1, new_config_2, new_config_5], + "mqtt": {domain: [new_config_3, new_config_4, new_config_6]}, + } - assert len(hass.states.async_all(domain)) == 3 + await help_test_reload_with_config(hass, caplog, tmp_path, new_config) + + assert len(hass.states.async_all(domain)) == 6 assert hass.states.get(f"{domain}.test_new_1") assert hass.states.get(f"{domain}.test_new_2") assert hass.states.get(f"{domain}.test_new_3") + assert hass.states.get(f"{domain}.test_new_4") + assert hass.states.get(f"{domain}.test_new_5") + assert hass.states.get(f"{domain}.test_new_6") async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): @@ -1752,9 +1778,10 @@ async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): new_config_3 = copy.deepcopy(config) new_config_3["name"] = "test_new_3" - await help_test_reload_with_config( - hass, caplog, tmp_path, domain, [new_config_1, new_config_2, new_config_3] - ) + new_config = { + domain: [new_config_1, new_config_2, new_config_3], + } + await help_test_reload_with_config(hass, caplog, tmp_path, new_config) assert len(hass.states.async_all(domain)) == 3 diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 894ecc32ecc..7081ae45993 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -1106,7 +1106,7 @@ async def test_cleanup_triggers_and_restoring_state( freezer.move_to("2022-02-02 12:01:10+01:00") await help_test_reload_with_config( - hass, caplog, tmp_path, domain, [config1, config2] + hass, caplog, tmp_path, {domain: [config1, config2]} ) await hass.async_block_till_done() From 12e6f143a431f1a26f8ae1897dbe20a7da27516c Mon Sep 17 00:00:00 2001 From: Matrix Date: Thu, 2 Jun 2022 23:21:22 +0800 Subject: [PATCH 1206/3516] Bump yolink-api to 0.0.6 (#72903) * Bump yolink-api to 0.0.6 * update testcase --- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/yolink/test_config_flow.py | 6 ++---- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index a89934154e9..7fb78a4974b 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -3,7 +3,7 @@ "name": "YoLink", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yolink", - "requirements": ["yolink-api==0.0.5"], + "requirements": ["yolink-api==0.0.6"], "dependencies": ["auth", "application_credentials"], "codeowners": ["@matrixd2"], "iot_class": "cloud_push" diff --git a/requirements_all.txt b/requirements_all.txt index e91ff265ef7..d51e776863d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2486,7 +2486,7 @@ yeelight==0.7.10 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.0.5 +yolink-api==0.0.6 # homeassistant.components.youless youless-api==0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 964290f6cf8..f7b246c64d2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1638,7 +1638,7 @@ yalexs==1.1.25 yeelight==0.7.10 # homeassistant.components.yolink -yolink-api==0.0.5 +yolink-api==0.0.6 # homeassistant.components.youless youless-api==0.16 diff --git a/tests/components/yolink/test_config_flow.py b/tests/components/yolink/test_config_flow.py index 5d6bb8fd727..e224bc3e1d2 100644 --- a/tests/components/yolink/test_config_flow.py +++ b/tests/components/yolink/test_config_flow.py @@ -3,6 +3,8 @@ import asyncio from http import HTTPStatus from unittest.mock import patch +from yolink.const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components import application_credentials from homeassistant.core import HomeAssistant @@ -12,11 +14,7 @@ from tests.common import MockConfigEntry CLIENT_ID = "12345" CLIENT_SECRET = "6789" -YOLINK_HOST = "api.yosmart.com" -YOLINK_HTTP_HOST = f"http://{YOLINK_HOST}" DOMAIN = "yolink" -OAUTH2_AUTHORIZE = f"{YOLINK_HTTP_HOST}/oauth/v2/authorization.htm" -OAUTH2_TOKEN = f"{YOLINK_HTTP_HOST}/open/yolink/token" async def test_abort_if_no_configuration(hass): From 0e985284c9a43ecaaf9b7202780cb99072a7af8d Mon Sep 17 00:00:00 2001 From: nojocodex <76249511+nojocodex@users.noreply.github.com> Date: Thu, 2 Jun 2022 19:49:08 +0200 Subject: [PATCH 1207/3516] Fix logging & exit code reporting to S6 on HA shutdown (#72921) --- rootfs/etc/services.d/home-assistant/finish | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/rootfs/etc/services.d/home-assistant/finish b/rootfs/etc/services.d/home-assistant/finish index 115d8352618..057957a9c03 100755 --- a/rootfs/etc/services.d/home-assistant/finish +++ b/rootfs/etc/services.d/home-assistant/finish @@ -2,24 +2,24 @@ # ============================================================================== # Take down the S6 supervision tree when Home Assistant fails # ============================================================================== -declare RESTART_EXIT_CODE 100 -declare SIGNAL_EXIT_CODE 256 -declare SIGTERM 15 +declare RESTART_EXIT_CODE=100 +declare SIGNAL_EXIT_CODE=256 +declare SIGTERM=15 declare APP_EXIT_CODE=${1} -declare SYS_EXIT_CODE=${2+x} +declare SIGNAL_NO=${2} declare NEW_EXIT_CODE= -bashio::log.info "Home Assistant Core finish process exit code ${1}" +bashio::log.info "Home Assistant Core finish process exit code ${APP_EXIT_CODE}" if [[ ${APP_EXIT_CODE} -eq ${RESTART_EXIT_CODE} ]]; then exit 0 elif [[ ${APP_EXIT_CODE} -eq ${SIGNAL_EXIT_CODE} ]]; then - bashio::log.info "Home Assistant Core finish process received signal ${APP_EXIT_CODE}" + bashio::log.info "Home Assistant Core finish process received signal ${SIGNAL_NO}" - NEW_EXIT_CODE=$((128 + SYS_EXIT_CODE)) + NEW_EXIT_CODE=$((128 + SIGNAL_NO)) echo ${NEW_EXIT_CODE} > /run/s6-linux-init-container-results/exitcode - if [[ ${NEW_EXIT_CODE} -eq ${SIGTERM} ]]; then + if [[ ${SIGNAL_NO} -eq ${SIGTERM} ]]; then /run/s6/basedir/bin/halt fi else From f1bcfedf84f3fbbe03eceb456b1f520f4fa8631e Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 2 Jun 2022 08:40:13 -0700 Subject: [PATCH 1208/3516] Fix bug in caldav and avoid unnecessary copy of dataclass (#72922) --- homeassistant/components/caldav/calendar.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index c5b2b3a790a..17a8d5deb2f 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -1,7 +1,6 @@ """Support for WebDav Calendar.""" from __future__ import annotations -import copy from datetime import datetime, timedelta import logging import re @@ -143,15 +142,13 @@ class WebDavCalendarEntity(CalendarEntity): def update(self): """Update event data.""" self.data.update() - event = copy.deepcopy(self.data.event) - if event is None: - self._event = event - return - (summary, offset) = extract_offset(event.summary, OFFSET) - event.summary = summary - self._event = event + self._event = self.data.event self._attr_extra_state_attributes = { - "offset_reached": is_offset_reached(event.start_datetime_local, offset) + "offset_reached": is_offset_reached( + self._event.start_datetime_local, self.data.offset + ) + if self._event + else False } @@ -165,6 +162,7 @@ class WebDavCalendarData: self.include_all_day = include_all_day self.search = search self.event = None + self.offset = None async def async_get_events( self, hass: HomeAssistant, start_date: datetime, end_date: datetime @@ -264,13 +262,15 @@ class WebDavCalendarData: return # Populate the entity attributes with the event values + (summary, offset) = extract_offset(vevent.summary.value, OFFSET) self.event = CalendarEvent( - summary=vevent.summary.value, + summary=summary, start=vevent.dtstart.value, end=self.get_end_date(vevent), location=self.get_attr_value(vevent, "location"), description=self.get_attr_value(vevent, "description"), ) + self.offset = offset @staticmethod def is_matching(vevent, search): From f5e0363117f66458299fdf1ac5f1bbc86d8a97fd Mon Sep 17 00:00:00 2001 From: Khole Date: Thu, 2 Jun 2022 22:54:26 +0100 Subject: [PATCH 1209/3516] Fix Hive authentication (#72929) --- homeassistant/components/hive/__init__.py | 9 ++------- homeassistant/components/hive/config_flow.py | 1 + homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hive/test_config_flow.py | 15 +++++++++++++++ 6 files changed, 21 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 292bbe62ae1..f3ed9674fcd 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -75,14 +75,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Hive from a config entry.""" - websession = aiohttp_client.async_get_clientsession(hass) + web_session = aiohttp_client.async_get_clientsession(hass) hive_config = dict(entry.data) - hive = Hive( - websession, - deviceGroupKey=hive_config["device_data"][0], - deviceKey=hive_config["device_data"][1], - devicePassword=hive_config["device_data"][2], - ) + hive = Hive(web_session) hive_config["options"] = {} hive_config["options"].update( diff --git a/homeassistant/components/hive/config_flow.py b/homeassistant/components/hive/config_flow.py index 9c391f13294..c713a3011f4 100644 --- a/homeassistant/components/hive/config_flow.py +++ b/homeassistant/components/hive/config_flow.py @@ -102,6 +102,7 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): raise UnknownHiveError # Setup the config entry + await self.hive_auth.device_registration("Home Assistant") self.data["tokens"] = self.tokens self.data["device_data"] = await self.hive_auth.getDeviceData() if self.context["source"] == config_entries.SOURCE_REAUTH: diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 472adc137ba..d8cd56abe0b 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,7 +3,7 @@ "name": "Hive", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": ["pyhiveapi==0.5.4"], + "requirements": ["pyhiveapi==0.5.5"], "codeowners": ["@Rendili", "@KJonline"], "iot_class": "cloud_polling", "loggers": ["apyhiveapi"] diff --git a/requirements_all.txt b/requirements_all.txt index d51e776863d..ad38e5c5fb1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1538,7 +1538,7 @@ pyheos==0.7.2 pyhik==0.3.0 # homeassistant.components.hive -pyhiveapi==0.5.4 +pyhiveapi==0.5.5 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7b246c64d2..94b978ece80 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1029,7 +1029,7 @@ pyhaversion==22.4.1 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.5.4 +pyhiveapi==0.5.5 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/tests/components/hive/test_config_flow.py b/tests/components/hive/test_config_flow.py index bb567b0bdfc..51ceec43ad2 100644 --- a/tests/components/hive/test_config_flow.py +++ b/tests/components/hive/test_config_flow.py @@ -33,6 +33,9 @@ async def test_import_flow(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.device_registration", + return_value=True, ), patch( "homeassistant.components.hive.config_flow.Auth.getDeviceData", return_value=[ @@ -93,6 +96,9 @@ async def test_user_flow(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.device_registration", + return_value=True, ), patch( "homeassistant.components.hive.config_flow.Auth.getDeviceData", return_value=[ @@ -172,6 +178,9 @@ async def test_user_flow_2fa(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.device_registration", + return_value=True, ), patch( "homeassistant.components.hive.config_flow.Auth.getDeviceData", return_value=[ @@ -256,6 +265,9 @@ async def test_reauth_flow(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.device_registration", + return_value=True, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -361,6 +373,9 @@ async def test_user_flow_2fa_send_new_code(hass): "AccessToken": "mock-access-token", }, }, + ), patch( + "homeassistant.components.hive.config_flow.Auth.device_registration", + return_value=True, ), patch( "homeassistant.components.hive.config_flow.Auth.getDeviceData", return_value=[ From 6a8a97b57cb7f3789aad5bae4976c6bee488fc34 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 2 Jun 2022 16:15:04 -0700 Subject: [PATCH 1210/3516] Only sync when HA is started up as we already sync at startup (#72940) --- homeassistant/components/cloud/google_config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index a0a68aaf84a..81f00b69b23 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -39,7 +39,6 @@ class CloudGoogleConfig(AbstractConfig): self._cur_entity_prefs = self._prefs.google_entity_configs self._cur_default_expose = self._prefs.google_default_expose self._sync_entities_lock = asyncio.Lock() - self._sync_on_started = False @property def enabled(self): @@ -224,7 +223,7 @@ class CloudGoogleConfig(AbstractConfig): self._cur_entity_prefs = prefs.google_entity_configs self._cur_default_expose = prefs.google_default_expose - if sync_entities: + if sync_entities and self.hass.is_running: await self.async_sync_entities_all() @callback From 69e8f5bb988b615d10ca42ea5e8030076238d142 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 2 Jun 2022 16:20:09 -0700 Subject: [PATCH 1211/3516] Bumped version to 2022.6.1 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 428aa84d5fb..08ab335a33b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 7f1e739803b..aeafbd53565 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.0 +version = 2022.6.1 url = https://www.home-assistant.io/ [options] From 43b802252a0f4ed33c5811004c65800700a98d18 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 3 Jun 2022 00:19:45 +0000 Subject: [PATCH 1212/3516] [ci skip] Translation update --- .../components/ialarm_xr/translations/et.json | 1 + homeassistant/components/knx/translations/fr.json | 2 +- homeassistant/components/plugwise/translations/ca.json | 4 ++-- homeassistant/components/plugwise/translations/et.json | 10 +++++++--- homeassistant/components/plugwise/translations/id.json | 3 ++- .../components/tankerkoenig/translations/el.json | 8 +++++++- .../components/tankerkoenig/translations/et.json | 8 +++++++- .../components/tankerkoenig/translations/id.json | 8 +++++++- .../components/totalconnect/translations/id.json | 1 + 9 files changed, 35 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/ialarm_xr/translations/et.json b/homeassistant/components/ialarm_xr/translations/et.json index 97fc5aa5a29..3679dd47f2e 100644 --- a/homeassistant/components/ialarm_xr/translations/et.json +++ b/homeassistant/components/ialarm_xr/translations/et.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "\u00dchendamine nurjus", + "timeout": "\u00dchenduse ajal\u00f5pp", "unknown": "Ootamatu t\u00f5rge" }, "step": { diff --git a/homeassistant/components/knx/translations/fr.json b/homeassistant/components/knx/translations/fr.json index 803fd71b734..7a1e8e88698 100644 --- a/homeassistant/components/knx/translations/fr.json +++ b/homeassistant/components/knx/translations/fr.json @@ -102,7 +102,7 @@ "multicast_group": "Utilis\u00e9 pour le routage et la d\u00e9couverte. Valeur par d\u00e9faut\u00a0: `224.0.23.12`", "multicast_port": "Utilis\u00e9 pour le routage et la d\u00e9couverte. Valeur par d\u00e9faut\u00a0: `3671`", "rate_limit": "Nombre maximal de t\u00e9l\u00e9grammes sortants par seconde.\nValeur recommand\u00e9e\u00a0: entre 20 et 40", - "state_updater": "Active ou d\u00e9sactive globalement la lecture des \u00e9tats depuis le bus KNX. Lorsqu'elle est d\u00e9sactiv\u00e9e, Home Assistant ne r\u00e9cup\u00e8re pas activement les \u00e9tats depuis le bus KNX et les options d'entit\u00e9 `sync_state` n'ont aucun effet." + "state_updater": "Active ou d\u00e9sactive globalement la lecture des \u00e9tats depuis le bus KNX. Lorsqu'elle est d\u00e9sactiv\u00e9e, Home Assistant ne r\u00e9cup\u00e8re pas activement les \u00e9tats depuis le bus KNX. Peut \u00eatre remplac\u00e9 par les options d'entit\u00e9 `sync_state`." } }, "tunnel": { diff --git a/homeassistant/components/plugwise/translations/ca.json b/homeassistant/components/plugwise/translations/ca.json index bd7371210ee..1bf9efee843 100644 --- a/homeassistant/components/plugwise/translations/ca.json +++ b/homeassistant/components/plugwise/translations/ca.json @@ -19,8 +19,8 @@ "port": "Port", "username": "Usuari de Smile" }, - "description": "Producte:", - "title": "Tipus de Plugwise" + "description": "Introdueix", + "title": "Connexi\u00f3 amb Smile" }, "user_gateway": { "data": { diff --git a/homeassistant/components/plugwise/translations/et.json b/homeassistant/components/plugwise/translations/et.json index f863dd30b19..2d50be06193 100644 --- a/homeassistant/components/plugwise/translations/et.json +++ b/homeassistant/components/plugwise/translations/et.json @@ -13,10 +13,14 @@ "step": { "user": { "data": { - "flow_type": "\u00dchenduse t\u00fc\u00fcp" + "flow_type": "\u00dchenduse t\u00fc\u00fcp", + "host": "IP aadress", + "password": "Smile ID", + "port": "Port", + "username": "Smile kasutajanimi" }, - "description": "Toode:", - "title": "Plugwise t\u00fc\u00fcp" + "description": "Sisesta andmed", + "title": "Loo \u00fchendus Smile-ga" }, "user_gateway": { "data": { diff --git a/homeassistant/components/plugwise/translations/id.json b/homeassistant/components/plugwise/translations/id.json index 436514d1a82..22c871e4f83 100644 --- a/homeassistant/components/plugwise/translations/id.json +++ b/homeassistant/components/plugwise/translations/id.json @@ -16,7 +16,8 @@ "flow_type": "Jenis koneksi", "host": "Alamat IP", "password": "ID Smile", - "port": "Port" + "port": "Port", + "username": "Nama Pengguna Smile" }, "description": "Masukkan", "title": "Hubungkan ke Smile" diff --git a/homeassistant/components/tankerkoenig/translations/el.json b/homeassistant/components/tankerkoenig/translations/el.json index 078001974ab..82dc5b9019b 100644 --- a/homeassistant/components/tankerkoenig/translations/el.json +++ b/homeassistant/components/tankerkoenig/translations/el.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + "already_configured": "\u0397 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "no_stations": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd \u03b5\u03bd\u03c4\u03cc\u03c2 \u03b5\u03bc\u03b2\u03ad\u03bb\u03b5\u03b9\u03b1\u03c2." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API Key" + } + }, "select_station": { "data": { "stations": "\u03a3\u03c4\u03b1\u03b8\u03bc\u03bf\u03af" diff --git a/homeassistant/components/tankerkoenig/translations/et.json b/homeassistant/components/tankerkoenig/translations/et.json index b2cd4b42e06..028bac46d44 100644 --- a/homeassistant/components/tankerkoenig/translations/et.json +++ b/homeassistant/components/tankerkoenig/translations/et.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "Asukoht on juba m\u00e4\u00e4ratud" + "already_configured": "Asukoht on juba m\u00e4\u00e4ratud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "invalid_auth": "Tuvastamine nurjus", "no_stations": "Piirkonnas ei leitud \u00fchtegi tanklat" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API v\u00f5ti" + } + }, "select_station": { "data": { "stations": "Tanklad" diff --git a/homeassistant/components/tankerkoenig/translations/id.json b/homeassistant/components/tankerkoenig/translations/id.json index 8ac50c9760f..ed0e2e15104 100644 --- a/homeassistant/components/tankerkoenig/translations/id.json +++ b/homeassistant/components/tankerkoenig/translations/id.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "Lokasi sudah dikonfigurasi" + "already_configured": "Lokasi sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "invalid_auth": "Autentikasi tidak valid", "no_stations": "Tidak dapat menemukan SPBU dalam jangkauan." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Kunci API" + } + }, "select_station": { "data": { "stations": "SPBU" diff --git a/homeassistant/components/totalconnect/translations/id.json b/homeassistant/components/totalconnect/translations/id.json index 08bce345e75..b8fc7cdd7df 100644 --- a/homeassistant/components/totalconnect/translations/id.json +++ b/homeassistant/components/totalconnect/translations/id.json @@ -35,6 +35,7 @@ "data": { "auto_bypass_low_battery": "Otomatis dilewatkan saat baterai lemah" }, + "description": "Otomatis melewatkan zona saat baterai lemah dilaporkan.", "title": "Opsi TotalConnect" } } From f52fa3599fe74bdaa1682b8f3825256510478313 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Jun 2022 17:51:27 -1000 Subject: [PATCH 1213/3516] Only create auto comfort entities for BAF devices that support them (#72948) --- homeassistant/components/baf/climate.py | 2 +- homeassistant/components/baf/manifest.json | 2 +- homeassistant/components/baf/number.py | 47 ++++++++++++---------- homeassistant/components/baf/sensor.py | 6 ++- homeassistant/components/baf/switch.py | 7 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 40 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/baf/climate.py b/homeassistant/components/baf/climate.py index f785d18e06f..d4ed4ac4337 100644 --- a/homeassistant/components/baf/climate.py +++ b/homeassistant/components/baf/climate.py @@ -26,7 +26,7 @@ async def async_setup_entry( ) -> None: """Set up BAF fan auto comfort.""" data: BAFData = hass.data[DOMAIN][entry.entry_id] - if data.device.has_fan: + if data.device.has_fan and data.device.has_auto_comfort: async_add_entities( [BAFAutoComfort(data.device, f"{data.device.name} Auto Comfort")] ) diff --git a/homeassistant/components/baf/manifest.json b/homeassistant/components/baf/manifest.json index 9dfc35685e3..8143c35410e 100644 --- a/homeassistant/components/baf/manifest.json +++ b/homeassistant/components/baf/manifest.json @@ -3,7 +3,7 @@ "name": "Big Ass Fans", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/baf", - "requirements": ["aiobafi6==0.3.0"], + "requirements": ["aiobafi6==0.5.0"], "codeowners": ["@bdraco", "@jfroy"], "iot_class": "local_push", "zeroconf": [ diff --git a/homeassistant/components/baf/number.py b/homeassistant/components/baf/number.py index 84358e79669..32a3ea5e693 100644 --- a/homeassistant/components/baf/number.py +++ b/homeassistant/components/baf/number.py @@ -36,27 +36,7 @@ class BAFNumberDescription(NumberEntityDescription, BAFNumberDescriptionMixin): """Class describing BAF sensor entities.""" -FAN_NUMBER_DESCRIPTIONS = ( - BAFNumberDescription( - key="return_to_auto_timeout", - name="Return to Auto Timeout", - min_value=ONE_MIN_SECS, - max_value=HALF_DAY_SECS, - entity_category=EntityCategory.CONFIG, - unit_of_measurement=TIME_SECONDS, - value_fn=lambda device: cast(Optional[int], device.return_to_auto_timeout), - mode=NumberMode.SLIDER, - ), - BAFNumberDescription( - key="motion_sense_timeout", - name="Motion Sense Timeout", - min_value=ONE_MIN_SECS, - max_value=ONE_DAY_SECS, - entity_category=EntityCategory.CONFIG, - unit_of_measurement=TIME_SECONDS, - value_fn=lambda device: cast(Optional[int], device.motion_sense_timeout), - mode=NumberMode.SLIDER, - ), +AUTO_COMFORT_NUMBER_DESCRIPTIONS = ( BAFNumberDescription( key="comfort_min_speed", name="Auto Comfort Minimum Speed", @@ -86,6 +66,29 @@ FAN_NUMBER_DESCRIPTIONS = ( ), ) +FAN_NUMBER_DESCRIPTIONS = ( + BAFNumberDescription( + key="return_to_auto_timeout", + name="Return to Auto Timeout", + min_value=ONE_MIN_SECS, + max_value=HALF_DAY_SECS, + entity_category=EntityCategory.CONFIG, + unit_of_measurement=TIME_SECONDS, + value_fn=lambda device: cast(Optional[int], device.return_to_auto_timeout), + mode=NumberMode.SLIDER, + ), + BAFNumberDescription( + key="motion_sense_timeout", + name="Motion Sense Timeout", + min_value=ONE_MIN_SECS, + max_value=ONE_DAY_SECS, + entity_category=EntityCategory.CONFIG, + unit_of_measurement=TIME_SECONDS, + value_fn=lambda device: cast(Optional[int], device.motion_sense_timeout), + mode=NumberMode.SLIDER, + ), +) + LIGHT_NUMBER_DESCRIPTIONS = ( BAFNumberDescription( key="light_return_to_auto_timeout", @@ -125,6 +128,8 @@ async def async_setup_entry( descriptions.extend(FAN_NUMBER_DESCRIPTIONS) if device.has_light: descriptions.extend(LIGHT_NUMBER_DESCRIPTIONS) + if device.has_auto_comfort: + descriptions.extend(AUTO_COMFORT_NUMBER_DESCRIPTIONS) async_add_entities(BAFNumber(device, description) for description in descriptions) diff --git a/homeassistant/components/baf/sensor.py b/homeassistant/components/baf/sensor.py index 0f4239962cf..7b93b22fe2f 100644 --- a/homeassistant/components/baf/sensor.py +++ b/homeassistant/components/baf/sensor.py @@ -39,7 +39,7 @@ class BAFSensorDescription( """Class describing BAF sensor entities.""" -BASE_SENSORS = ( +AUTO_COMFORT_SENSORS = ( BAFSensorDescription( key="temperature", name="Temperature", @@ -103,10 +103,12 @@ async def async_setup_entry( """Set up BAF fan sensors.""" data: BAFData = hass.data[DOMAIN][entry.entry_id] device = data.device - sensors_descriptions = list(BASE_SENSORS) + sensors_descriptions: list[BAFSensorDescription] = [] for description in DEFINED_ONLY_SENSORS: if getattr(device, description.key): sensors_descriptions.append(description) + if device.has_auto_comfort: + sensors_descriptions.extend(AUTO_COMFORT_SENSORS) if device.has_fan: sensors_descriptions.extend(FAN_SENSORS) async_add_entities( diff --git a/homeassistant/components/baf/switch.py b/homeassistant/components/baf/switch.py index 6cefa0db65d..44671e68458 100644 --- a/homeassistant/components/baf/switch.py +++ b/homeassistant/components/baf/switch.py @@ -48,13 +48,16 @@ BASE_SWITCHES = [ ), ] -FAN_SWITCHES = [ +AUTO_COMFORT_SWITCHES = [ BAFSwitchDescription( key="comfort_heat_assist_enable", name="Auto Comfort Heat Assist", entity_category=EntityCategory.CONFIG, value_fn=lambda device: cast(Optional[bool], device.comfort_heat_assist_enable), ), +] + +FAN_SWITCHES = [ BAFSwitchDescription( key="fan_beep_enable", name="Beep", @@ -120,6 +123,8 @@ async def async_setup_entry( descriptions.extend(FAN_SWITCHES) if device.has_light: descriptions.extend(LIGHT_SWITCHES) + if device.has_auto_comfort: + descriptions.extend(AUTO_COMFORT_SWITCHES) async_add_entities(BAFSwitch(device, description) for description in descriptions) diff --git a/requirements_all.txt b/requirements_all.txt index 3abb93968bc..35b82892119 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -122,7 +122,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.3.0 +aiobafi6==0.5.0 # homeassistant.components.aws aiobotocore==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 148ce2191ba..a4753b643fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -109,7 +109,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.3.0 +aiobafi6==0.5.0 # homeassistant.components.aws aiobotocore==2.1.0 From 5b314142254c826f718b5b79f7de8f3da6e7c676 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Jun 2022 17:52:53 -1000 Subject: [PATCH 1214/3516] Fix misalignments between sql based filtering with the entityfilter based filtering (#72936) --- homeassistant/components/recorder/filters.py | 110 +++- homeassistant/components/recorder/history.py | 6 +- tests/components/history/test_init.py | 19 +- tests/components/logbook/test_init.py | 22 +- .../test_filters_with_entityfilter.py | 516 ++++++++++++++++++ 5 files changed, 620 insertions(+), 53 deletions(-) create mode 100644 tests/components/recorder/test_filters_with_entityfilter.py diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 3077f7f57f3..835496c2d6e 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -88,14 +88,32 @@ class Filters: self.included_domains: Iterable[str] = [] self.included_entity_globs: Iterable[str] = [] + def __repr__(self) -> str: + """Return human readable excludes/includes.""" + return ( + f"" + ) + @property def has_config(self) -> bool: """Determine if there is any filter configuration.""" + return bool(self._have_exclude or self._have_include) + + @property + def _have_exclude(self) -> bool: return bool( self.excluded_entities or self.excluded_domains or self.excluded_entity_globs - or self.included_entities + ) + + @property + def _have_include(self) -> bool: + return bool( + self.included_entities or self.included_domains or self.included_entity_globs ) @@ -103,36 +121,67 @@ class Filters: def _generate_filter_for_columns( self, columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: - includes = [] - if self.included_domains: - includes.append(_domain_matcher(self.included_domains, columns, encoder)) - if self.included_entities: - includes.append(_entity_matcher(self.included_entities, columns, encoder)) - if self.included_entity_globs: - includes.append( - _globs_to_like(self.included_entity_globs, columns, encoder) - ) + """Generate a filter from pre-comuted sets and pattern lists. - excludes = [] - if self.excluded_domains: - excludes.append(_domain_matcher(self.excluded_domains, columns, encoder)) - if self.excluded_entities: - excludes.append(_entity_matcher(self.excluded_entities, columns, encoder)) - if self.excluded_entity_globs: - excludes.append( - _globs_to_like(self.excluded_entity_globs, columns, encoder) - ) + This must match exactly how homeassistant.helpers.entityfilter works. + """ + i_domains = _domain_matcher(self.included_domains, columns, encoder) + i_entities = _entity_matcher(self.included_entities, columns, encoder) + i_entity_globs = _globs_to_like(self.included_entity_globs, columns, encoder) + includes = [i_domains, i_entities, i_entity_globs] - if not includes and not excludes: + e_domains = _domain_matcher(self.excluded_domains, columns, encoder) + e_entities = _entity_matcher(self.excluded_entities, columns, encoder) + e_entity_globs = _globs_to_like(self.excluded_entity_globs, columns, encoder) + excludes = [e_domains, e_entities, e_entity_globs] + + have_exclude = self._have_exclude + have_include = self._have_include + + # Case 1 - no includes or excludes - pass all entities + if not have_include and not have_exclude: return None - if includes and not excludes: + # Case 2 - includes, no excludes - only include specified entities + if have_include and not have_exclude: return or_(*includes).self_group() - if not includes and excludes: + # Case 3 - excludes, no includes - only exclude specified entities + if not have_include and have_exclude: return not_(or_(*excludes).self_group()) - return or_(*includes).self_group() & not_(or_(*excludes).self_group()) + # Case 4 - both includes and excludes specified + # Case 4a - include domain or glob specified + # - if domain is included, pass if entity not excluded + # - if glob is included, pass if entity and domain not excluded + # - if domain and glob are not included, pass if entity is included + # note: if both include domain matches then exclude domains ignored. + # If glob matches then exclude domains and glob checked + if self.included_domains or self.included_entity_globs: + return or_( + (i_domains & ~(e_entities | e_entity_globs)), + ( + ~i_domains + & or_( + (i_entity_globs & ~(or_(*excludes))), + (~i_entity_globs & i_entities), + ) + ), + ).self_group() + + # Case 4b - exclude domain or glob specified, include has no domain or glob + # In this one case the traditional include logic is inverted. Even though an + # include is specified since its only a list of entity IDs its used only to + # expose specific entities excluded by domain or glob. Any entities not + # excluded are then presumed included. Logic is as follows + # - if domain or glob is excluded, pass if entity is included + # - if domain is not excluded, pass if entity not excluded by ID + if self.excluded_domains or self.excluded_entity_globs: + return (not_(or_(*excludes)) | i_entities).self_group() + + # Case 4c - neither include or exclude domain specified + # - Only pass if entity is included. Ignore entity excludes. + return i_entities def states_entity_filter(self) -> ClauseList: """Generate the entity filter query.""" @@ -158,29 +207,32 @@ def _globs_to_like( glob_strs: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: """Translate glob to sql.""" - return or_( + matchers = [ cast(column, Text()).like( encoder(glob_str).translate(GLOB_TO_SQL_CHARS), escape="\\" ) for glob_str in glob_strs for column in columns - ) + ] + return or_(*matchers) if matchers else or_(False) def _entity_matcher( entity_ids: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: - return or_( + matchers = [ cast(column, Text()).in_([encoder(entity_id) for entity_id in entity_ids]) for column in columns - ) + ] + return or_(*matchers) if matchers else or_(False) def _domain_matcher( domains: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: - return or_( + matchers = [ cast(column, Text()).like(encoder(f"{domain}.%")) for domain in domains for column in columns - ) + ] + return or_(*matchers) if matchers else or_(False) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 7e8e97eafd4..49796bd0158 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -237,7 +237,9 @@ def _significant_states_stmt( stmt += _ignore_domains_filter if filters and filters.has_config: entity_filter = filters.states_entity_filter() - stmt += lambda q: q.filter(entity_filter) + stmt = stmt.add_criteria( + lambda q: q.filter(entity_filter), track_on=[filters] + ) stmt += lambda q: q.filter(States.last_updated > start_time) if end_time: @@ -529,7 +531,7 @@ def _get_states_for_all_stmt( stmt += _ignore_domains_filter if filters and filters.has_config: entity_filter = filters.states_entity_filter() - stmt += lambda q: q.filter(entity_filter) + stmt = stmt.add_criteria(lambda q: q.filter(entity_filter), track_on=[filters]) if join_attributes: stmt += lambda q: q.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index cbc5e86c37e..9dc7af59a38 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -246,15 +246,11 @@ def test_get_significant_states_exclude(hass_history): def test_get_significant_states_exclude_include_entity(hass_history): """Test significant states when excluding domains and include entities. - We should not get back every thermostat and media player test changes. + We should not get back every thermostat change unless its specifically included """ hass = hass_history zero, four, states = record_states(hass) - del states["media_player.test2"] - del states["media_player.test3"] - del states["thermostat.test"] del states["thermostat.test2"] - del states["script.can_cancel_this_one"] config = history.CONFIG_SCHEMA( { @@ -340,14 +336,12 @@ def test_get_significant_states_include(hass_history): def test_get_significant_states_include_exclude_domain(hass_history): """Test if significant states when excluding and including domains. - We should not get back any changes since we include only the - media_player domain but also exclude it. + We should get back all the media_player domain changes + only since the include wins over the exclude but will + exclude everything else. """ hass = hass_history zero, four, states = record_states(hass) - del states["media_player.test"] - del states["media_player.test2"] - del states["media_player.test3"] del states["thermostat.test"] del states["thermostat.test2"] del states["script.can_cancel_this_one"] @@ -372,7 +366,6 @@ def test_get_significant_states_include_exclude_entity(hass_history): """ hass = hass_history zero, four, states = record_states(hass) - del states["media_player.test"] del states["media_player.test2"] del states["media_player.test3"] del states["thermostat.test"] @@ -394,12 +387,12 @@ def test_get_significant_states_include_exclude_entity(hass_history): def test_get_significant_states_include_exclude(hass_history): """Test if significant states when in/excluding domains and entities. - We should only get back changes of the media_player.test2 entity. + We should get back changes of the media_player.test2, media_player.test3, + and thermostat.test. """ hass = hass_history zero, four, states = record_states(hass) del states["media_player.test"] - del states["thermostat.test"] del states["thermostat.test2"] del states["script.can_cancel_this_one"] diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index d33bbd5b8ac..651a00fb0cf 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -2037,7 +2037,7 @@ async def test_include_events_domain_glob(hass, hass_client, recorder_mock): _assert_entry(entries[3], name="included", entity_id=entity_id3) -async def test_include_exclude_events(hass, hass_client, recorder_mock): +async def test_include_exclude_events_no_globs(hass, hass_client, recorder_mock): """Test if events are filtered if include and exclude is configured.""" entity_id = "switch.bla" entity_id2 = "sensor.blu" @@ -2082,13 +2082,15 @@ async def test_include_exclude_events(hass, hass_client, recorder_mock): client = await hass_client() entries = await _async_fetch_logbook(client) - assert len(entries) == 4 + assert len(entries) == 6 _assert_entry( entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN ) - _assert_entry(entries[1], name="blu", entity_id=entity_id2, state="10") - _assert_entry(entries[2], name="blu", entity_id=entity_id2, state="20") - _assert_entry(entries[3], name="keep", entity_id=entity_id4, state="10") + _assert_entry(entries[1], name="bla", entity_id=entity_id, state="10") + _assert_entry(entries[2], name="blu", entity_id=entity_id2, state="10") + _assert_entry(entries[3], name="bla", entity_id=entity_id, state="20") + _assert_entry(entries[4], name="blu", entity_id=entity_id2, state="20") + _assert_entry(entries[5], name="keep", entity_id=entity_id4, state="10") async def test_include_exclude_events_with_glob_filters( @@ -2145,13 +2147,15 @@ async def test_include_exclude_events_with_glob_filters( client = await hass_client() entries = await _async_fetch_logbook(client) - assert len(entries) == 4 + assert len(entries) == 6 _assert_entry( entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN ) - _assert_entry(entries[1], name="blu", entity_id=entity_id2, state="10") - _assert_entry(entries[2], name="blu", entity_id=entity_id2, state="20") - _assert_entry(entries[3], name="included", entity_id=entity_id4, state="30") + _assert_entry(entries[1], name="bla", entity_id=entity_id, state="10") + _assert_entry(entries[2], name="blu", entity_id=entity_id2, state="10") + _assert_entry(entries[3], name="bla", entity_id=entity_id, state="20") + _assert_entry(entries[4], name="blu", entity_id=entity_id2, state="20") + _assert_entry(entries[5], name="included", entity_id=entity_id4, state="30") async def test_empty_config(hass, hass_client, recorder_mock): diff --git a/tests/components/recorder/test_filters_with_entityfilter.py b/tests/components/recorder/test_filters_with_entityfilter.py new file mode 100644 index 00000000000..0758d6fdc95 --- /dev/null +++ b/tests/components/recorder/test_filters_with_entityfilter.py @@ -0,0 +1,516 @@ +"""The tests for the recorder filter matching the EntityFilter component.""" +import json + +from sqlalchemy import select +from sqlalchemy.engine.row import Row + +from homeassistant.components.recorder import get_instance +from homeassistant.components.recorder.filters import ( + Filters, + extract_include_exclude_filter_conf, + sqlalchemy_filter_from_include_exclude_conf, +) +from homeassistant.components.recorder.models import EventData, States +from homeassistant.components.recorder.util import session_scope +from homeassistant.const import ATTR_ENTITY_ID, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entityfilter import ( + CONF_DOMAINS, + CONF_ENTITIES, + CONF_ENTITY_GLOBS, + CONF_EXCLUDE, + CONF_INCLUDE, + convert_include_exclude_filter, +) + +from .common import async_wait_recording_done + + +async def _async_get_states_and_events_with_filter( + hass: HomeAssistant, sqlalchemy_filter: Filters, entity_ids: set[str] +) -> tuple[list[Row], list[Row]]: + """Get states from the database based on a filter.""" + for entity_id in entity_ids: + hass.states.async_set(entity_id, STATE_ON) + hass.bus.async_fire("any", {ATTR_ENTITY_ID: entity_id}) + + await async_wait_recording_done(hass) + + def _get_states_with_session(): + with session_scope(hass=hass) as session: + return session.execute( + select(States.entity_id).filter( + sqlalchemy_filter.states_entity_filter() + ) + ).all() + + filtered_states_entity_ids = { + row[0] + for row in await get_instance(hass).async_add_executor_job( + _get_states_with_session + ) + } + + def _get_events_with_session(): + with session_scope(hass=hass) as session: + return session.execute( + select(EventData.shared_data).filter( + sqlalchemy_filter.events_entity_filter() + ) + ).all() + + filtered_events_entity_ids = set() + for row in await get_instance(hass).async_add_executor_job( + _get_events_with_session + ): + event_data = json.loads(row[0]) + if ATTR_ENTITY_ID not in event_data: + continue + filtered_events_entity_ids.add(json.loads(row[0])[ATTR_ENTITY_ID]) + + return filtered_states_entity_ids, filtered_events_entity_ids + + +async def test_included_and_excluded_simple_case_no_domains(hass, recorder_mock): + """Test filters with included and excluded without domains.""" + filter_accept = {"sensor.kitchen4", "switch.kitchen"} + filter_reject = { + "light.any", + "switch.other", + "cover.any", + "sensor.weather5", + "light.kitchen", + } + conf = { + CONF_INCLUDE: { + CONF_ENTITY_GLOBS: ["sensor.kitchen*"], + CONF_ENTITIES: ["switch.kitchen"], + }, + CONF_EXCLUDE: { + CONF_ENTITY_GLOBS: ["sensor.weather*"], + CONF_ENTITIES: ["light.kitchen"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + assert not entity_filter.explicitly_included("light.any") + assert not entity_filter.explicitly_included("switch.other") + assert entity_filter.explicitly_included("sensor.kitchen4") + assert entity_filter.explicitly_included("switch.kitchen") + + assert not entity_filter.explicitly_excluded("light.any") + assert not entity_filter.explicitly_excluded("switch.other") + assert entity_filter.explicitly_excluded("sensor.weather5") + assert entity_filter.explicitly_excluded("light.kitchen") + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_included_and_excluded_simple_case_no_globs(hass, recorder_mock): + """Test filters with included and excluded without globs.""" + filter_accept = {"switch.bla", "sensor.blu", "sensor.keep"} + filter_reject = {"sensor.bli"} + conf = { + CONF_INCLUDE: { + CONF_DOMAINS: ["sensor", "homeassistant"], + CONF_ENTITIES: ["switch.bla"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["switch"], + CONF_ENTITIES: ["sensor.bli"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_included_and_excluded_simple_case_without_underscores( + hass, recorder_mock +): + """Test filters with included and excluded without underscores.""" + filter_accept = {"light.any", "sensor.kitchen4", "switch.kitchen"} + filter_reject = {"switch.other", "cover.any", "sensor.weather5", "light.kitchen"} + conf = { + CONF_INCLUDE: { + CONF_DOMAINS: ["light"], + CONF_ENTITY_GLOBS: ["sensor.kitchen*"], + CONF_ENTITIES: ["switch.kitchen"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["cover"], + CONF_ENTITY_GLOBS: ["sensor.weather*"], + CONF_ENTITIES: ["light.kitchen"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + assert not entity_filter.explicitly_included("light.any") + assert not entity_filter.explicitly_included("switch.other") + assert entity_filter.explicitly_included("sensor.kitchen4") + assert entity_filter.explicitly_included("switch.kitchen") + + assert not entity_filter.explicitly_excluded("light.any") + assert not entity_filter.explicitly_excluded("switch.other") + assert entity_filter.explicitly_excluded("sensor.weather5") + assert entity_filter.explicitly_excluded("light.kitchen") + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_included_and_excluded_simple_case_with_underscores(hass, recorder_mock): + """Test filters with included and excluded with underscores.""" + filter_accept = {"light.any", "sensor.kitchen_4", "switch.kitchen"} + filter_reject = {"switch.other", "cover.any", "sensor.weather_5", "light.kitchen"} + conf = { + CONF_INCLUDE: { + CONF_DOMAINS: ["light"], + CONF_ENTITY_GLOBS: ["sensor.kitchen_*"], + CONF_ENTITIES: ["switch.kitchen"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["cover"], + CONF_ENTITY_GLOBS: ["sensor.weather_*"], + CONF_ENTITIES: ["light.kitchen"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + assert not entity_filter.explicitly_included("light.any") + assert not entity_filter.explicitly_included("switch.other") + assert entity_filter.explicitly_included("sensor.kitchen_4") + assert entity_filter.explicitly_included("switch.kitchen") + + assert not entity_filter.explicitly_excluded("light.any") + assert not entity_filter.explicitly_excluded("switch.other") + assert entity_filter.explicitly_excluded("sensor.weather_5") + assert entity_filter.explicitly_excluded("light.kitchen") + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_included_and_excluded_complex_case(hass, recorder_mock): + """Test filters with included and excluded with a complex filter.""" + filter_accept = {"light.any", "sensor.kitchen_4", "switch.kitchen"} + filter_reject = { + "camera.one", + "notify.any", + "automation.update_readme", + "automation.update_utilities_cost", + "binary_sensor.iss", + } + conf = { + CONF_INCLUDE: { + CONF_ENTITIES: ["group.trackers"], + }, + CONF_EXCLUDE: { + CONF_ENTITIES: [ + "automation.update_readme", + "automation.update_utilities_cost", + "binary_sensor.iss", + ], + CONF_DOMAINS: [ + "camera", + "group", + "media_player", + "notify", + "scene", + "sun", + "zone", + ], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_included_entities_and_excluded_domain(hass, recorder_mock): + """Test filters with included entities and excluded domain.""" + filter_accept = { + "media_player.test", + "media_player.test3", + "thermostat.test", + "zone.home", + "script.can_cancel_this_one", + } + filter_reject = { + "thermostat.test2", + } + conf = { + CONF_INCLUDE: { + CONF_ENTITIES: ["media_player.test", "thermostat.test"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["thermostat"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_same_domain_included_excluded(hass, recorder_mock): + """Test filters with the same domain included and excluded.""" + filter_accept = { + "media_player.test", + "media_player.test3", + } + filter_reject = { + "thermostat.test2", + "thermostat.test", + "zone.home", + "script.can_cancel_this_one", + } + conf = { + CONF_INCLUDE: { + CONF_DOMAINS: ["media_player"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["media_player"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_same_entity_included_excluded(hass, recorder_mock): + """Test filters with the same entity included and excluded.""" + filter_accept = { + "media_player.test", + } + filter_reject = { + "media_player.test3", + "thermostat.test2", + "thermostat.test", + "zone.home", + "script.can_cancel_this_one", + } + conf = { + CONF_INCLUDE: { + CONF_ENTITIES: ["media_player.test"], + }, + CONF_EXCLUDE: { + CONF_ENTITIES: ["media_player.test"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_same_entity_included_excluded_include_domain_wins(hass, recorder_mock): + """Test filters with domain and entities and the include domain wins.""" + filter_accept = { + "media_player.test2", + "media_player.test3", + "thermostat.test", + } + filter_reject = { + "thermostat.test2", + "zone.home", + "script.can_cancel_this_one", + } + conf = { + CONF_INCLUDE: { + CONF_DOMAINS: ["media_player"], + CONF_ENTITIES: ["thermostat.test"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["thermostat"], + CONF_ENTITIES: ["media_player.test"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) From 0d9330c39e24ec3723c0150d3f7fe9d4dd8c97b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Jun 2022 17:52:53 -1000 Subject: [PATCH 1215/3516] Fix misalignments between sql based filtering with the entityfilter based filtering (#72936) --- homeassistant/components/recorder/filters.py | 110 +++- homeassistant/components/recorder/history.py | 6 +- tests/components/history/test_init.py | 19 +- tests/components/logbook/test_init.py | 22 +- .../test_filters_with_entityfilter.py | 516 ++++++++++++++++++ 5 files changed, 620 insertions(+), 53 deletions(-) create mode 100644 tests/components/recorder/test_filters_with_entityfilter.py diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 3077f7f57f3..835496c2d6e 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -88,14 +88,32 @@ class Filters: self.included_domains: Iterable[str] = [] self.included_entity_globs: Iterable[str] = [] + def __repr__(self) -> str: + """Return human readable excludes/includes.""" + return ( + f"" + ) + @property def has_config(self) -> bool: """Determine if there is any filter configuration.""" + return bool(self._have_exclude or self._have_include) + + @property + def _have_exclude(self) -> bool: return bool( self.excluded_entities or self.excluded_domains or self.excluded_entity_globs - or self.included_entities + ) + + @property + def _have_include(self) -> bool: + return bool( + self.included_entities or self.included_domains or self.included_entity_globs ) @@ -103,36 +121,67 @@ class Filters: def _generate_filter_for_columns( self, columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: - includes = [] - if self.included_domains: - includes.append(_domain_matcher(self.included_domains, columns, encoder)) - if self.included_entities: - includes.append(_entity_matcher(self.included_entities, columns, encoder)) - if self.included_entity_globs: - includes.append( - _globs_to_like(self.included_entity_globs, columns, encoder) - ) + """Generate a filter from pre-comuted sets and pattern lists. - excludes = [] - if self.excluded_domains: - excludes.append(_domain_matcher(self.excluded_domains, columns, encoder)) - if self.excluded_entities: - excludes.append(_entity_matcher(self.excluded_entities, columns, encoder)) - if self.excluded_entity_globs: - excludes.append( - _globs_to_like(self.excluded_entity_globs, columns, encoder) - ) + This must match exactly how homeassistant.helpers.entityfilter works. + """ + i_domains = _domain_matcher(self.included_domains, columns, encoder) + i_entities = _entity_matcher(self.included_entities, columns, encoder) + i_entity_globs = _globs_to_like(self.included_entity_globs, columns, encoder) + includes = [i_domains, i_entities, i_entity_globs] - if not includes and not excludes: + e_domains = _domain_matcher(self.excluded_domains, columns, encoder) + e_entities = _entity_matcher(self.excluded_entities, columns, encoder) + e_entity_globs = _globs_to_like(self.excluded_entity_globs, columns, encoder) + excludes = [e_domains, e_entities, e_entity_globs] + + have_exclude = self._have_exclude + have_include = self._have_include + + # Case 1 - no includes or excludes - pass all entities + if not have_include and not have_exclude: return None - if includes and not excludes: + # Case 2 - includes, no excludes - only include specified entities + if have_include and not have_exclude: return or_(*includes).self_group() - if not includes and excludes: + # Case 3 - excludes, no includes - only exclude specified entities + if not have_include and have_exclude: return not_(or_(*excludes).self_group()) - return or_(*includes).self_group() & not_(or_(*excludes).self_group()) + # Case 4 - both includes and excludes specified + # Case 4a - include domain or glob specified + # - if domain is included, pass if entity not excluded + # - if glob is included, pass if entity and domain not excluded + # - if domain and glob are not included, pass if entity is included + # note: if both include domain matches then exclude domains ignored. + # If glob matches then exclude domains and glob checked + if self.included_domains or self.included_entity_globs: + return or_( + (i_domains & ~(e_entities | e_entity_globs)), + ( + ~i_domains + & or_( + (i_entity_globs & ~(or_(*excludes))), + (~i_entity_globs & i_entities), + ) + ), + ).self_group() + + # Case 4b - exclude domain or glob specified, include has no domain or glob + # In this one case the traditional include logic is inverted. Even though an + # include is specified since its only a list of entity IDs its used only to + # expose specific entities excluded by domain or glob. Any entities not + # excluded are then presumed included. Logic is as follows + # - if domain or glob is excluded, pass if entity is included + # - if domain is not excluded, pass if entity not excluded by ID + if self.excluded_domains or self.excluded_entity_globs: + return (not_(or_(*excludes)) | i_entities).self_group() + + # Case 4c - neither include or exclude domain specified + # - Only pass if entity is included. Ignore entity excludes. + return i_entities def states_entity_filter(self) -> ClauseList: """Generate the entity filter query.""" @@ -158,29 +207,32 @@ def _globs_to_like( glob_strs: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: """Translate glob to sql.""" - return or_( + matchers = [ cast(column, Text()).like( encoder(glob_str).translate(GLOB_TO_SQL_CHARS), escape="\\" ) for glob_str in glob_strs for column in columns - ) + ] + return or_(*matchers) if matchers else or_(False) def _entity_matcher( entity_ids: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: - return or_( + matchers = [ cast(column, Text()).in_([encoder(entity_id) for entity_id in entity_ids]) for column in columns - ) + ] + return or_(*matchers) if matchers else or_(False) def _domain_matcher( domains: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: - return or_( + matchers = [ cast(column, Text()).like(encoder(f"{domain}.%")) for domain in domains for column in columns - ) + ] + return or_(*matchers) if matchers else or_(False) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 7e8e97eafd4..49796bd0158 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -237,7 +237,9 @@ def _significant_states_stmt( stmt += _ignore_domains_filter if filters and filters.has_config: entity_filter = filters.states_entity_filter() - stmt += lambda q: q.filter(entity_filter) + stmt = stmt.add_criteria( + lambda q: q.filter(entity_filter), track_on=[filters] + ) stmt += lambda q: q.filter(States.last_updated > start_time) if end_time: @@ -529,7 +531,7 @@ def _get_states_for_all_stmt( stmt += _ignore_domains_filter if filters and filters.has_config: entity_filter = filters.states_entity_filter() - stmt += lambda q: q.filter(entity_filter) + stmt = stmt.add_criteria(lambda q: q.filter(entity_filter), track_on=[filters]) if join_attributes: stmt += lambda q: q.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index cbc5e86c37e..9dc7af59a38 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -246,15 +246,11 @@ def test_get_significant_states_exclude(hass_history): def test_get_significant_states_exclude_include_entity(hass_history): """Test significant states when excluding domains and include entities. - We should not get back every thermostat and media player test changes. + We should not get back every thermostat change unless its specifically included """ hass = hass_history zero, four, states = record_states(hass) - del states["media_player.test2"] - del states["media_player.test3"] - del states["thermostat.test"] del states["thermostat.test2"] - del states["script.can_cancel_this_one"] config = history.CONFIG_SCHEMA( { @@ -340,14 +336,12 @@ def test_get_significant_states_include(hass_history): def test_get_significant_states_include_exclude_domain(hass_history): """Test if significant states when excluding and including domains. - We should not get back any changes since we include only the - media_player domain but also exclude it. + We should get back all the media_player domain changes + only since the include wins over the exclude but will + exclude everything else. """ hass = hass_history zero, four, states = record_states(hass) - del states["media_player.test"] - del states["media_player.test2"] - del states["media_player.test3"] del states["thermostat.test"] del states["thermostat.test2"] del states["script.can_cancel_this_one"] @@ -372,7 +366,6 @@ def test_get_significant_states_include_exclude_entity(hass_history): """ hass = hass_history zero, four, states = record_states(hass) - del states["media_player.test"] del states["media_player.test2"] del states["media_player.test3"] del states["thermostat.test"] @@ -394,12 +387,12 @@ def test_get_significant_states_include_exclude_entity(hass_history): def test_get_significant_states_include_exclude(hass_history): """Test if significant states when in/excluding domains and entities. - We should only get back changes of the media_player.test2 entity. + We should get back changes of the media_player.test2, media_player.test3, + and thermostat.test. """ hass = hass_history zero, four, states = record_states(hass) del states["media_player.test"] - del states["thermostat.test"] del states["thermostat.test2"] del states["script.can_cancel_this_one"] diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index d33bbd5b8ac..651a00fb0cf 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -2037,7 +2037,7 @@ async def test_include_events_domain_glob(hass, hass_client, recorder_mock): _assert_entry(entries[3], name="included", entity_id=entity_id3) -async def test_include_exclude_events(hass, hass_client, recorder_mock): +async def test_include_exclude_events_no_globs(hass, hass_client, recorder_mock): """Test if events are filtered if include and exclude is configured.""" entity_id = "switch.bla" entity_id2 = "sensor.blu" @@ -2082,13 +2082,15 @@ async def test_include_exclude_events(hass, hass_client, recorder_mock): client = await hass_client() entries = await _async_fetch_logbook(client) - assert len(entries) == 4 + assert len(entries) == 6 _assert_entry( entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN ) - _assert_entry(entries[1], name="blu", entity_id=entity_id2, state="10") - _assert_entry(entries[2], name="blu", entity_id=entity_id2, state="20") - _assert_entry(entries[3], name="keep", entity_id=entity_id4, state="10") + _assert_entry(entries[1], name="bla", entity_id=entity_id, state="10") + _assert_entry(entries[2], name="blu", entity_id=entity_id2, state="10") + _assert_entry(entries[3], name="bla", entity_id=entity_id, state="20") + _assert_entry(entries[4], name="blu", entity_id=entity_id2, state="20") + _assert_entry(entries[5], name="keep", entity_id=entity_id4, state="10") async def test_include_exclude_events_with_glob_filters( @@ -2145,13 +2147,15 @@ async def test_include_exclude_events_with_glob_filters( client = await hass_client() entries = await _async_fetch_logbook(client) - assert len(entries) == 4 + assert len(entries) == 6 _assert_entry( entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN ) - _assert_entry(entries[1], name="blu", entity_id=entity_id2, state="10") - _assert_entry(entries[2], name="blu", entity_id=entity_id2, state="20") - _assert_entry(entries[3], name="included", entity_id=entity_id4, state="30") + _assert_entry(entries[1], name="bla", entity_id=entity_id, state="10") + _assert_entry(entries[2], name="blu", entity_id=entity_id2, state="10") + _assert_entry(entries[3], name="bla", entity_id=entity_id, state="20") + _assert_entry(entries[4], name="blu", entity_id=entity_id2, state="20") + _assert_entry(entries[5], name="included", entity_id=entity_id4, state="30") async def test_empty_config(hass, hass_client, recorder_mock): diff --git a/tests/components/recorder/test_filters_with_entityfilter.py b/tests/components/recorder/test_filters_with_entityfilter.py new file mode 100644 index 00000000000..0758d6fdc95 --- /dev/null +++ b/tests/components/recorder/test_filters_with_entityfilter.py @@ -0,0 +1,516 @@ +"""The tests for the recorder filter matching the EntityFilter component.""" +import json + +from sqlalchemy import select +from sqlalchemy.engine.row import Row + +from homeassistant.components.recorder import get_instance +from homeassistant.components.recorder.filters import ( + Filters, + extract_include_exclude_filter_conf, + sqlalchemy_filter_from_include_exclude_conf, +) +from homeassistant.components.recorder.models import EventData, States +from homeassistant.components.recorder.util import session_scope +from homeassistant.const import ATTR_ENTITY_ID, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entityfilter import ( + CONF_DOMAINS, + CONF_ENTITIES, + CONF_ENTITY_GLOBS, + CONF_EXCLUDE, + CONF_INCLUDE, + convert_include_exclude_filter, +) + +from .common import async_wait_recording_done + + +async def _async_get_states_and_events_with_filter( + hass: HomeAssistant, sqlalchemy_filter: Filters, entity_ids: set[str] +) -> tuple[list[Row], list[Row]]: + """Get states from the database based on a filter.""" + for entity_id in entity_ids: + hass.states.async_set(entity_id, STATE_ON) + hass.bus.async_fire("any", {ATTR_ENTITY_ID: entity_id}) + + await async_wait_recording_done(hass) + + def _get_states_with_session(): + with session_scope(hass=hass) as session: + return session.execute( + select(States.entity_id).filter( + sqlalchemy_filter.states_entity_filter() + ) + ).all() + + filtered_states_entity_ids = { + row[0] + for row in await get_instance(hass).async_add_executor_job( + _get_states_with_session + ) + } + + def _get_events_with_session(): + with session_scope(hass=hass) as session: + return session.execute( + select(EventData.shared_data).filter( + sqlalchemy_filter.events_entity_filter() + ) + ).all() + + filtered_events_entity_ids = set() + for row in await get_instance(hass).async_add_executor_job( + _get_events_with_session + ): + event_data = json.loads(row[0]) + if ATTR_ENTITY_ID not in event_data: + continue + filtered_events_entity_ids.add(json.loads(row[0])[ATTR_ENTITY_ID]) + + return filtered_states_entity_ids, filtered_events_entity_ids + + +async def test_included_and_excluded_simple_case_no_domains(hass, recorder_mock): + """Test filters with included and excluded without domains.""" + filter_accept = {"sensor.kitchen4", "switch.kitchen"} + filter_reject = { + "light.any", + "switch.other", + "cover.any", + "sensor.weather5", + "light.kitchen", + } + conf = { + CONF_INCLUDE: { + CONF_ENTITY_GLOBS: ["sensor.kitchen*"], + CONF_ENTITIES: ["switch.kitchen"], + }, + CONF_EXCLUDE: { + CONF_ENTITY_GLOBS: ["sensor.weather*"], + CONF_ENTITIES: ["light.kitchen"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + assert not entity_filter.explicitly_included("light.any") + assert not entity_filter.explicitly_included("switch.other") + assert entity_filter.explicitly_included("sensor.kitchen4") + assert entity_filter.explicitly_included("switch.kitchen") + + assert not entity_filter.explicitly_excluded("light.any") + assert not entity_filter.explicitly_excluded("switch.other") + assert entity_filter.explicitly_excluded("sensor.weather5") + assert entity_filter.explicitly_excluded("light.kitchen") + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_included_and_excluded_simple_case_no_globs(hass, recorder_mock): + """Test filters with included and excluded without globs.""" + filter_accept = {"switch.bla", "sensor.blu", "sensor.keep"} + filter_reject = {"sensor.bli"} + conf = { + CONF_INCLUDE: { + CONF_DOMAINS: ["sensor", "homeassistant"], + CONF_ENTITIES: ["switch.bla"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["switch"], + CONF_ENTITIES: ["sensor.bli"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_included_and_excluded_simple_case_without_underscores( + hass, recorder_mock +): + """Test filters with included and excluded without underscores.""" + filter_accept = {"light.any", "sensor.kitchen4", "switch.kitchen"} + filter_reject = {"switch.other", "cover.any", "sensor.weather5", "light.kitchen"} + conf = { + CONF_INCLUDE: { + CONF_DOMAINS: ["light"], + CONF_ENTITY_GLOBS: ["sensor.kitchen*"], + CONF_ENTITIES: ["switch.kitchen"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["cover"], + CONF_ENTITY_GLOBS: ["sensor.weather*"], + CONF_ENTITIES: ["light.kitchen"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + assert not entity_filter.explicitly_included("light.any") + assert not entity_filter.explicitly_included("switch.other") + assert entity_filter.explicitly_included("sensor.kitchen4") + assert entity_filter.explicitly_included("switch.kitchen") + + assert not entity_filter.explicitly_excluded("light.any") + assert not entity_filter.explicitly_excluded("switch.other") + assert entity_filter.explicitly_excluded("sensor.weather5") + assert entity_filter.explicitly_excluded("light.kitchen") + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_included_and_excluded_simple_case_with_underscores(hass, recorder_mock): + """Test filters with included and excluded with underscores.""" + filter_accept = {"light.any", "sensor.kitchen_4", "switch.kitchen"} + filter_reject = {"switch.other", "cover.any", "sensor.weather_5", "light.kitchen"} + conf = { + CONF_INCLUDE: { + CONF_DOMAINS: ["light"], + CONF_ENTITY_GLOBS: ["sensor.kitchen_*"], + CONF_ENTITIES: ["switch.kitchen"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["cover"], + CONF_ENTITY_GLOBS: ["sensor.weather_*"], + CONF_ENTITIES: ["light.kitchen"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + assert not entity_filter.explicitly_included("light.any") + assert not entity_filter.explicitly_included("switch.other") + assert entity_filter.explicitly_included("sensor.kitchen_4") + assert entity_filter.explicitly_included("switch.kitchen") + + assert not entity_filter.explicitly_excluded("light.any") + assert not entity_filter.explicitly_excluded("switch.other") + assert entity_filter.explicitly_excluded("sensor.weather_5") + assert entity_filter.explicitly_excluded("light.kitchen") + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_included_and_excluded_complex_case(hass, recorder_mock): + """Test filters with included and excluded with a complex filter.""" + filter_accept = {"light.any", "sensor.kitchen_4", "switch.kitchen"} + filter_reject = { + "camera.one", + "notify.any", + "automation.update_readme", + "automation.update_utilities_cost", + "binary_sensor.iss", + } + conf = { + CONF_INCLUDE: { + CONF_ENTITIES: ["group.trackers"], + }, + CONF_EXCLUDE: { + CONF_ENTITIES: [ + "automation.update_readme", + "automation.update_utilities_cost", + "binary_sensor.iss", + ], + CONF_DOMAINS: [ + "camera", + "group", + "media_player", + "notify", + "scene", + "sun", + "zone", + ], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_included_entities_and_excluded_domain(hass, recorder_mock): + """Test filters with included entities and excluded domain.""" + filter_accept = { + "media_player.test", + "media_player.test3", + "thermostat.test", + "zone.home", + "script.can_cancel_this_one", + } + filter_reject = { + "thermostat.test2", + } + conf = { + CONF_INCLUDE: { + CONF_ENTITIES: ["media_player.test", "thermostat.test"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["thermostat"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_same_domain_included_excluded(hass, recorder_mock): + """Test filters with the same domain included and excluded.""" + filter_accept = { + "media_player.test", + "media_player.test3", + } + filter_reject = { + "thermostat.test2", + "thermostat.test", + "zone.home", + "script.can_cancel_this_one", + } + conf = { + CONF_INCLUDE: { + CONF_DOMAINS: ["media_player"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["media_player"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_same_entity_included_excluded(hass, recorder_mock): + """Test filters with the same entity included and excluded.""" + filter_accept = { + "media_player.test", + } + filter_reject = { + "media_player.test3", + "thermostat.test2", + "thermostat.test", + "zone.home", + "script.can_cancel_this_one", + } + conf = { + CONF_INCLUDE: { + CONF_ENTITIES: ["media_player.test"], + }, + CONF_EXCLUDE: { + CONF_ENTITIES: ["media_player.test"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_same_entity_included_excluded_include_domain_wins(hass, recorder_mock): + """Test filters with domain and entities and the include domain wins.""" + filter_accept = { + "media_player.test2", + "media_player.test3", + "thermostat.test", + } + filter_reject = { + "thermostat.test2", + "zone.home", + "script.can_cancel_this_one", + } + conf = { + CONF_INCLUDE: { + CONF_DOMAINS: ["media_player"], + CONF_ENTITIES: ["thermostat.test"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["thermostat"], + CONF_ENTITIES: ["media_player.test"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) From ff687a8248124c3bc804e04d650d36386ed9dd2e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Jun 2022 17:51:27 -1000 Subject: [PATCH 1216/3516] Only create auto comfort entities for BAF devices that support them (#72948) --- homeassistant/components/baf/climate.py | 2 +- homeassistant/components/baf/manifest.json | 2 +- homeassistant/components/baf/number.py | 47 ++++++++++++---------- homeassistant/components/baf/sensor.py | 6 ++- homeassistant/components/baf/switch.py | 7 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 40 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/baf/climate.py b/homeassistant/components/baf/climate.py index f785d18e06f..d4ed4ac4337 100644 --- a/homeassistant/components/baf/climate.py +++ b/homeassistant/components/baf/climate.py @@ -26,7 +26,7 @@ async def async_setup_entry( ) -> None: """Set up BAF fan auto comfort.""" data: BAFData = hass.data[DOMAIN][entry.entry_id] - if data.device.has_fan: + if data.device.has_fan and data.device.has_auto_comfort: async_add_entities( [BAFAutoComfort(data.device, f"{data.device.name} Auto Comfort")] ) diff --git a/homeassistant/components/baf/manifest.json b/homeassistant/components/baf/manifest.json index 9dfc35685e3..8143c35410e 100644 --- a/homeassistant/components/baf/manifest.json +++ b/homeassistant/components/baf/manifest.json @@ -3,7 +3,7 @@ "name": "Big Ass Fans", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/baf", - "requirements": ["aiobafi6==0.3.0"], + "requirements": ["aiobafi6==0.5.0"], "codeowners": ["@bdraco", "@jfroy"], "iot_class": "local_push", "zeroconf": [ diff --git a/homeassistant/components/baf/number.py b/homeassistant/components/baf/number.py index 84358e79669..32a3ea5e693 100644 --- a/homeassistant/components/baf/number.py +++ b/homeassistant/components/baf/number.py @@ -36,27 +36,7 @@ class BAFNumberDescription(NumberEntityDescription, BAFNumberDescriptionMixin): """Class describing BAF sensor entities.""" -FAN_NUMBER_DESCRIPTIONS = ( - BAFNumberDescription( - key="return_to_auto_timeout", - name="Return to Auto Timeout", - min_value=ONE_MIN_SECS, - max_value=HALF_DAY_SECS, - entity_category=EntityCategory.CONFIG, - unit_of_measurement=TIME_SECONDS, - value_fn=lambda device: cast(Optional[int], device.return_to_auto_timeout), - mode=NumberMode.SLIDER, - ), - BAFNumberDescription( - key="motion_sense_timeout", - name="Motion Sense Timeout", - min_value=ONE_MIN_SECS, - max_value=ONE_DAY_SECS, - entity_category=EntityCategory.CONFIG, - unit_of_measurement=TIME_SECONDS, - value_fn=lambda device: cast(Optional[int], device.motion_sense_timeout), - mode=NumberMode.SLIDER, - ), +AUTO_COMFORT_NUMBER_DESCRIPTIONS = ( BAFNumberDescription( key="comfort_min_speed", name="Auto Comfort Minimum Speed", @@ -86,6 +66,29 @@ FAN_NUMBER_DESCRIPTIONS = ( ), ) +FAN_NUMBER_DESCRIPTIONS = ( + BAFNumberDescription( + key="return_to_auto_timeout", + name="Return to Auto Timeout", + min_value=ONE_MIN_SECS, + max_value=HALF_DAY_SECS, + entity_category=EntityCategory.CONFIG, + unit_of_measurement=TIME_SECONDS, + value_fn=lambda device: cast(Optional[int], device.return_to_auto_timeout), + mode=NumberMode.SLIDER, + ), + BAFNumberDescription( + key="motion_sense_timeout", + name="Motion Sense Timeout", + min_value=ONE_MIN_SECS, + max_value=ONE_DAY_SECS, + entity_category=EntityCategory.CONFIG, + unit_of_measurement=TIME_SECONDS, + value_fn=lambda device: cast(Optional[int], device.motion_sense_timeout), + mode=NumberMode.SLIDER, + ), +) + LIGHT_NUMBER_DESCRIPTIONS = ( BAFNumberDescription( key="light_return_to_auto_timeout", @@ -125,6 +128,8 @@ async def async_setup_entry( descriptions.extend(FAN_NUMBER_DESCRIPTIONS) if device.has_light: descriptions.extend(LIGHT_NUMBER_DESCRIPTIONS) + if device.has_auto_comfort: + descriptions.extend(AUTO_COMFORT_NUMBER_DESCRIPTIONS) async_add_entities(BAFNumber(device, description) for description in descriptions) diff --git a/homeassistant/components/baf/sensor.py b/homeassistant/components/baf/sensor.py index 0f4239962cf..7b93b22fe2f 100644 --- a/homeassistant/components/baf/sensor.py +++ b/homeassistant/components/baf/sensor.py @@ -39,7 +39,7 @@ class BAFSensorDescription( """Class describing BAF sensor entities.""" -BASE_SENSORS = ( +AUTO_COMFORT_SENSORS = ( BAFSensorDescription( key="temperature", name="Temperature", @@ -103,10 +103,12 @@ async def async_setup_entry( """Set up BAF fan sensors.""" data: BAFData = hass.data[DOMAIN][entry.entry_id] device = data.device - sensors_descriptions = list(BASE_SENSORS) + sensors_descriptions: list[BAFSensorDescription] = [] for description in DEFINED_ONLY_SENSORS: if getattr(device, description.key): sensors_descriptions.append(description) + if device.has_auto_comfort: + sensors_descriptions.extend(AUTO_COMFORT_SENSORS) if device.has_fan: sensors_descriptions.extend(FAN_SENSORS) async_add_entities( diff --git a/homeassistant/components/baf/switch.py b/homeassistant/components/baf/switch.py index 6cefa0db65d..44671e68458 100644 --- a/homeassistant/components/baf/switch.py +++ b/homeassistant/components/baf/switch.py @@ -48,13 +48,16 @@ BASE_SWITCHES = [ ), ] -FAN_SWITCHES = [ +AUTO_COMFORT_SWITCHES = [ BAFSwitchDescription( key="comfort_heat_assist_enable", name="Auto Comfort Heat Assist", entity_category=EntityCategory.CONFIG, value_fn=lambda device: cast(Optional[bool], device.comfort_heat_assist_enable), ), +] + +FAN_SWITCHES = [ BAFSwitchDescription( key="fan_beep_enable", name="Beep", @@ -120,6 +123,8 @@ async def async_setup_entry( descriptions.extend(FAN_SWITCHES) if device.has_light: descriptions.extend(LIGHT_SWITCHES) + if device.has_auto_comfort: + descriptions.extend(AUTO_COMFORT_SWITCHES) async_add_entities(BAFSwitch(device, description) for description in descriptions) diff --git a/requirements_all.txt b/requirements_all.txt index ad38e5c5fb1..791875978ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -122,7 +122,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.3.0 +aiobafi6==0.5.0 # homeassistant.components.aws aiobotocore==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 94b978ece80..4fa7c37963d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -109,7 +109,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.3.0 +aiobafi6==0.5.0 # homeassistant.components.aws aiobotocore==2.1.0 From 3a8a8165845f31afdb7562489c9a538fd68381a0 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 2 Jun 2022 21:42:52 -0700 Subject: [PATCH 1217/3516] Cleanup nest config flow tests to use common setup fixtures (#72878) * Cleanup nest config flow tests to use common setup * Remove some conditionals in test setup --- tests/components/nest/common.py | 16 +- tests/components/nest/conftest.py | 25 +- tests/components/nest/test_config_flow_sdm.py | 367 +++++++----------- 3 files changed, 171 insertions(+), 237 deletions(-) diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index c2a9c6db157..bf8af8db127 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -25,10 +25,15 @@ PlatformSetup = Callable[[], Awaitable[None]] _T = TypeVar("_T") YieldFixture = Generator[_T, None, None] +WEB_AUTH_DOMAIN = DOMAIN +APP_AUTH_DOMAIN = f"{DOMAIN}.installed" + PROJECT_ID = "some-project-id" CLIENT_ID = "some-client-id" CLIENT_SECRET = "some-client-secret" -SUBSCRIBER_ID = "projects/example/subscriptions/subscriber-id-9876" +CLOUD_PROJECT_ID = "cloud-id-9876" +SUBSCRIBER_ID = "projects/cloud-id-9876/subscriptions/subscriber-id-9876" + CONFIG = { "nest": { @@ -79,10 +84,12 @@ TEST_CONFIG_YAML_ONLY = NestTestConfig( config=CONFIG, config_entry_data={ "sdm": {}, - "auth_implementation": "nest", "token": create_token_entry(), }, ) +TEST_CONFIGFLOW_YAML_ONLY = NestTestConfig( + config=TEST_CONFIG_YAML_ONLY.config, config_entry_data=None +) # Exercises mode where subscriber id is created in the config flow, but # all authentication is defined in configuration.yaml @@ -96,11 +103,14 @@ TEST_CONFIG_HYBRID = NestTestConfig( }, config_entry_data={ "sdm": {}, - "auth_implementation": "nest", "token": create_token_entry(), + "cloud_project_id": CLOUD_PROJECT_ID, "subscriber_id": SUBSCRIBER_ID, }, ) +TEST_CONFIGFLOW_HYBRID = NestTestConfig( + TEST_CONFIG_HYBRID.config, config_entry_data=None +) TEST_CONFIG_LEGACY = NestTestConfig( config={ diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index 9b060d38fbe..fafd04c3764 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -5,7 +5,7 @@ from collections.abc import Generator import copy import shutil from typing import Any -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import uuid import aiohttp @@ -24,6 +24,7 @@ from .common import ( SUBSCRIBER_ID, TEST_CONFIG_HYBRID, TEST_CONFIG_YAML_ONLY, + WEB_AUTH_DOMAIN, CreateDevice, FakeSubscriber, NestTestConfig, @@ -114,6 +115,17 @@ def subscriber() -> YieldFixture[FakeSubscriber]: yield subscriber +@pytest.fixture +def mock_subscriber() -> YieldFixture[AsyncMock]: + """Fixture for injecting errors into the subscriber.""" + mock_subscriber = AsyncMock(FakeSubscriber) + with patch( + "homeassistant.components.nest.api.GoogleNestSubscriber", + return_value=mock_subscriber, + ): + yield mock_subscriber + + @pytest.fixture async def device_manager(subscriber: FakeSubscriber) -> DeviceManager: """Set up the DeviceManager.""" @@ -170,6 +182,12 @@ def subscriber_id() -> str: return SUBSCRIBER_ID +@pytest.fixture +def auth_implementation() -> str | None: + """Fixture to let tests override the auth implementation in the config entry.""" + return WEB_AUTH_DOMAIN + + @pytest.fixture( params=[TEST_CONFIG_YAML_ONLY, TEST_CONFIG_HYBRID], ids=["yaml-config-only", "hybrid-config"], @@ -195,7 +213,9 @@ def config( @pytest.fixture def config_entry( - subscriber_id: str | None, nest_test_config: NestTestConfig + subscriber_id: str | None, + auth_implementation: str | None, + nest_test_config: NestTestConfig, ) -> MockConfigEntry | None: """Fixture that sets up the ConfigEntry for the test.""" if nest_test_config.config_entry_data is None: @@ -206,6 +226,7 @@ def config_entry( data[CONF_SUBSCRIBER_ID] = subscriber_id else: del data[CONF_SUBSCRIBER_ID] + data["auth_implementation"] = auth_implementation return MockConfigEntry(domain=DOMAIN, data=data) diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow_sdm.py index ab769d4b57c..ff55c1f518d 100644 --- a/tests/components/nest/test_config_flow_sdm.py +++ b/tests/components/nest/test_config_flow_sdm.py @@ -1,7 +1,6 @@ """Test the Google Nest Device Access config flow.""" -import copy -from unittest.mock import AsyncMock, patch +from unittest.mock import patch from google_nest_sdm.exceptions import ( AuthException, @@ -11,35 +10,27 @@ from google_nest_sdm.exceptions import ( from google_nest_sdm.structure import Structure import pytest -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.nest.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET -from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow -from .common import FakeSubscriber, MockConfigEntry +from .common import ( + APP_AUTH_DOMAIN, + CLIENT_ID, + CLOUD_PROJECT_ID, + FAKE_TOKEN, + PROJECT_ID, + SUBSCRIBER_ID, + TEST_CONFIG_HYBRID, + TEST_CONFIG_YAML_ONLY, + TEST_CONFIGFLOW_HYBRID, + TEST_CONFIGFLOW_YAML_ONLY, + WEB_AUTH_DOMAIN, + MockConfigEntry, +) -CLIENT_ID = "1234" -CLIENT_SECRET = "5678" -PROJECT_ID = "project-id-4321" -SUBSCRIBER_ID = "projects/cloud-id-9876/subscriptions/subscriber-id-9876" -CLOUD_PROJECT_ID = "cloud-id-9876" - -CONFIG = { - DOMAIN: { - "project_id": PROJECT_ID, - "subscriber_id": SUBSCRIBER_ID, - CONF_CLIENT_ID: CLIENT_ID, - CONF_CLIENT_SECRET: CLIENT_SECRET, - }, - "http": {"base_url": "https://example.com"}, -} - -ORIG_AUTH_DOMAIN = DOMAIN -WEB_AUTH_DOMAIN = DOMAIN -APP_AUTH_DOMAIN = f"{DOMAIN}.installed" WEB_REDIRECT_URL = "https://example.com/auth/external/callback" APP_REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob" @@ -49,30 +40,6 @@ FAKE_DHCP_DATA = dhcp.DhcpServiceInfo( ) -@pytest.fixture -def subscriber() -> FakeSubscriber: - """Create FakeSubscriber.""" - return FakeSubscriber() - - -def get_config_entry(hass): - """Return a single config entry.""" - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - return entries[0] - - -def create_config_entry(hass: HomeAssistant, data: dict) -> ConfigEntry: - """Create the ConfigEntry.""" - entry = MockConfigEntry( - domain=DOMAIN, - data=data, - unique_id=DOMAIN, - ) - entry.add_to_hass(hass) - return entry - - class OAuthFixture: """Simulate the oauth flow used by the config flow.""" @@ -196,7 +163,9 @@ class OAuthFixture: def get_config_entry(self) -> ConfigEntry: """Get the config entry.""" - return get_config_entry(self.hass) + entries = self.hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + return entries[0] @pytest.fixture @@ -205,16 +174,10 @@ async def oauth(hass, hass_client_no_auth, aioclient_mock, current_request_with_ return OAuthFixture(hass, hass_client_no_auth, aioclient_mock) -async def async_setup_configflow(hass): - """Set up component so the pubsub subscriber is managed by config flow.""" - config = copy.deepcopy(CONFIG) - del config[DOMAIN]["subscriber_id"] # Create in config flow instead - return await setup.async_setup_component(hass, DOMAIN, config) - - -async def test_web_full_flow(hass, oauth): +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_YAML_ONLY]) +async def test_web_full_flow(hass, oauth, setup_platform): """Check full flow.""" - assert await setup.async_setup_component(hass, DOMAIN, CONFIG) + await setup_platform() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -238,29 +201,15 @@ async def test_web_full_flow(hass, oauth): assert "subscriber_id" not in entry.data -async def test_web_reauth(hass, oauth): +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIG_YAML_ONLY]) +async def test_web_reauth(hass, oauth, setup_platform, config_entry): """Test Nest reauthentication.""" - assert await setup.async_setup_component(hass, DOMAIN, CONFIG) + await setup_platform() - old_entry = create_config_entry( - hass, - { - "auth_implementation": WEB_AUTH_DOMAIN, - "token": { - # Verify this is replaced at end of the test - "access_token": "some-revoked-token", - }, - "sdm": {}, - }, - ) + assert config_entry.data["token"].get("access_token") == FAKE_TOKEN - entry = get_config_entry(hass) - assert entry.data["token"] == { - "access_token": "some-revoked-token", - } - - result = await oauth.async_reauth(old_entry.data) + result = await oauth.async_reauth(config_entry.data) await oauth.async_oauth_web_flow(result) entry = await oauth.async_finish_setup(result) @@ -277,11 +226,9 @@ async def test_web_reauth(hass, oauth): assert "subscriber_id" not in entry.data # not updated -async def test_single_config_entry(hass): +async def test_single_config_entry(hass, setup_platform): """Test that only a single config entry is allowed.""" - create_config_entry(hass, {"auth_implementation": WEB_AUTH_DOMAIN, "sdm": {}}) - - assert await setup.async_setup_component(hass, DOMAIN, CONFIG) + await setup_platform() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -290,18 +237,13 @@ async def test_single_config_entry(hass): assert result["reason"] == "single_instance_allowed" -async def test_unexpected_existing_config_entries(hass, oauth): +async def test_unexpected_existing_config_entries(hass, oauth, setup_platform): """Test Nest reauthentication with multiple existing config entries.""" # Note that this case will not happen in the future since only a single # instance is now allowed, but this may have been allowed in the past. # On reauth, only one entry is kept and the others are deleted. - assert await setup.async_setup_component(hass, DOMAIN, CONFIG) - - old_entry = MockConfigEntry( - domain=DOMAIN, data={"auth_implementation": WEB_AUTH_DOMAIN, "sdm": {}} - ) - old_entry.add_to_hass(hass) + await setup_platform() old_entry = MockConfigEntry( domain=DOMAIN, data={"auth_implementation": WEB_AUTH_DOMAIN, "sdm": {}} @@ -333,9 +275,9 @@ async def test_unexpected_existing_config_entries(hass, oauth): assert "subscriber_id" not in entry.data # not updated -async def test_reauth_missing_config_entry(hass): +async def test_reauth_missing_config_entry(hass, setup_platform): """Test the reauth flow invoked missing existing data.""" - assert await setup.async_setup_component(hass, DOMAIN, CONFIG) + await setup_platform() # Invoke the reauth flow with no existing data result = await hass.config_entries.flow.async_init( @@ -345,9 +287,10 @@ async def test_reauth_missing_config_entry(hass): assert result["reason"] == "missing_configuration" -async def test_app_full_flow(hass, oauth): +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_YAML_ONLY]) +async def test_app_full_flow(hass, oauth, setup_platform): """Check full flow.""" - assert await setup.async_setup_component(hass, DOMAIN, CONFIG) + await setup_platform() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -370,24 +313,15 @@ async def test_app_full_flow(hass, oauth): assert "subscriber_id" not in entry.data -async def test_app_reauth(hass, oauth): +@pytest.mark.parametrize( + "nest_test_config,auth_implementation", [(TEST_CONFIG_YAML_ONLY, APP_AUTH_DOMAIN)] +) +async def test_app_reauth(hass, oauth, setup_platform, config_entry): """Test Nest reauthentication for Installed App Auth.""" - assert await setup.async_setup_component(hass, DOMAIN, CONFIG) + await setup_platform() - old_entry = create_config_entry( - hass, - { - "auth_implementation": APP_AUTH_DOMAIN, - "token": { - # Verify this is replaced at end of the test - "access_token": "some-revoked-token", - }, - "sdm": {}, - }, - ) - - result = await oauth.async_reauth(old_entry.data) + result = await oauth.async_reauth(config_entry.data) await oauth.async_oauth_app_flow(result) # Verify existing tokens are replaced @@ -404,9 +338,10 @@ async def test_app_reauth(hass, oauth): assert "subscriber_id" not in entry.data # not updated -async def test_pubsub_subscription(hass, oauth, subscriber): +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) +async def test_pubsub_subscription(hass, oauth, subscriber, setup_platform): """Check flow that creates a pub/sub subscription.""" - assert await async_setup_configflow(hass) + await setup_platform() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -414,16 +349,11 @@ async def test_pubsub_subscription(hass, oauth, subscriber): result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) await oauth.async_oauth_app_flow(result) - with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber", - return_value=subscriber, - ): - result = await oauth.async_configure(result, {"code": "1234"}) - await oauth.async_pubsub_flow(result) - entry = await oauth.async_finish_setup( - result, {"cloud_project_id": CLOUD_PROJECT_ID} - ) - await hass.async_block_till_done() + result = await oauth.async_configure(result, {"code": "1234"}) + await oauth.async_pubsub_flow(result) + entry = await oauth.async_finish_setup( + result, {"cloud_project_id": CLOUD_PROJECT_ID} + ) assert entry.title == "OAuth for Apps" assert "token" in entry.data @@ -439,9 +369,12 @@ async def test_pubsub_subscription(hass, oauth, subscriber): assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID -async def test_pubsub_subscription_strip_whitespace(hass, oauth, subscriber): +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) +async def test_pubsub_subscription_strip_whitespace( + hass, oauth, subscriber, setup_platform +): """Check that project id has whitespace stripped on entry.""" - assert await async_setup_configflow(hass) + await setup_platform() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -449,16 +382,11 @@ async def test_pubsub_subscription_strip_whitespace(hass, oauth, subscriber): result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) await oauth.async_oauth_app_flow(result) - with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber", - return_value=subscriber, - ): - result = await oauth.async_configure(result, {"code": "1234"}) - await oauth.async_pubsub_flow(result) - entry = await oauth.async_finish_setup( - result, {"cloud_project_id": " " + CLOUD_PROJECT_ID + " "} - ) - await hass.async_block_till_done() + result = await oauth.async_configure(result, {"code": "1234"}) + await oauth.async_pubsub_flow(result) + entry = await oauth.async_finish_setup( + result, {"cloud_project_id": " " + CLOUD_PROJECT_ID + " "} + ) assert entry.title == "OAuth for Apps" assert "token" in entry.data @@ -474,9 +402,12 @@ async def test_pubsub_subscription_strip_whitespace(hass, oauth, subscriber): assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID -async def test_pubsub_subscription_auth_failure(hass, oauth): +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) +async def test_pubsub_subscription_auth_failure( + hass, oauth, setup_platform, mock_subscriber +): """Check flow that creates a pub/sub subscription.""" - assert await async_setup_configflow(hass) + await setup_platform() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -484,23 +415,22 @@ async def test_pubsub_subscription_auth_failure(hass, oauth): result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) await oauth.async_oauth_app_flow(result) result = await oauth.async_configure(result, {"code": "1234"}) - with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber.create_subscription", - side_effect=AuthException(), - ): - await oauth.async_pubsub_flow(result) - result = await oauth.async_configure( - result, {"cloud_project_id": CLOUD_PROJECT_ID} - ) - await hass.async_block_till_done() + + mock_subscriber.create_subscription.side_effect = AuthException() + + await oauth.async_pubsub_flow(result) + result = await oauth.async_configure(result, {"cloud_project_id": CLOUD_PROJECT_ID}) assert result["type"] == "abort" assert result["reason"] == "invalid_access_token" -async def test_pubsub_subscription_failure(hass, oauth): +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) +async def test_pubsub_subscription_failure( + hass, oauth, setup_platform, mock_subscriber +): """Check flow that creates a pub/sub subscription.""" - assert await async_setup_configflow(hass) + await setup_platform() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -509,14 +439,10 @@ async def test_pubsub_subscription_failure(hass, oauth): await oauth.async_oauth_app_flow(result) result = await oauth.async_configure(result, {"code": "1234"}) await oauth.async_pubsub_flow(result) - with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber.create_subscription", - side_effect=SubscriberException(), - ): - result = await oauth.async_configure( - result, {"cloud_project_id": CLOUD_PROJECT_ID} - ) - await hass.async_block_till_done() + + mock_subscriber.create_subscription.side_effect = SubscriberException() + + result = await oauth.async_configure(result, {"cloud_project_id": CLOUD_PROJECT_ID}) assert result["type"] == "form" assert "errors" in result @@ -524,9 +450,12 @@ async def test_pubsub_subscription_failure(hass, oauth): assert result["errors"]["cloud_project_id"] == "subscriber_error" -async def test_pubsub_subscription_configuration_failure(hass, oauth): +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) +async def test_pubsub_subscription_configuration_failure( + hass, oauth, setup_platform, mock_subscriber +): """Check flow that creates a pub/sub subscription.""" - assert await async_setup_configflow(hass) + await setup_platform() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -535,14 +464,9 @@ async def test_pubsub_subscription_configuration_failure(hass, oauth): await oauth.async_oauth_app_flow(result) result = await oauth.async_configure(result, {"code": "1234"}) await oauth.async_pubsub_flow(result) - with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber.create_subscription", - side_effect=ConfigurationException(), - ): - result = await oauth.async_configure( - result, {"cloud_project_id": CLOUD_PROJECT_ID} - ) - await hass.async_block_till_done() + + mock_subscriber.create_subscription.side_effect = ConfigurationException() + result = await oauth.async_configure(result, {"cloud_project_id": CLOUD_PROJECT_ID}) assert result["type"] == "form" assert "errors" in result @@ -550,9 +474,10 @@ async def test_pubsub_subscription_configuration_failure(hass, oauth): assert result["errors"]["cloud_project_id"] == "bad_project_id" -async def test_pubsub_with_wrong_project_id(hass, oauth): +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) +async def test_pubsub_with_wrong_project_id(hass, oauth, setup_platform): """Test a possible common misconfiguration mixing up project ids.""" - assert await async_setup_configflow(hass) + await setup_platform() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -572,23 +497,16 @@ async def test_pubsub_with_wrong_project_id(hass, oauth): assert result["errors"]["cloud_project_id"] == "wrong_project_id" -async def test_pubsub_subscriber_config_entry_reauth(hass, oauth, subscriber): +@pytest.mark.parametrize( + "nest_test_config,auth_implementation", [(TEST_CONFIG_HYBRID, APP_AUTH_DOMAIN)] +) +async def test_pubsub_subscriber_config_entry_reauth( + hass, oauth, setup_platform, subscriber, config_entry +): """Test the pubsub subscriber id is preserved during reauth.""" - assert await async_setup_configflow(hass) + await setup_platform() - old_entry = create_config_entry( - hass, - { - "auth_implementation": APP_AUTH_DOMAIN, - "subscriber_id": SUBSCRIBER_ID, - "cloud_project_id": CLOUD_PROJECT_ID, - "token": { - "access_token": "some-revoked-token", - }, - "sdm": {}, - }, - ) - result = await oauth.async_reauth(old_entry.data) + result = await oauth.async_reauth(config_entry.data) await oauth.async_oauth_app_flow(result) # Entering an updated access token refreshs the config entry. @@ -606,7 +524,8 @@ async def test_pubsub_subscriber_config_entry_reauth(hass, oauth, subscriber): assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID -async def test_config_entry_title_from_home(hass, oauth, subscriber): +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) +async def test_config_entry_title_from_home(hass, oauth, setup_platform, subscriber): """Test that the Google Home name is used for the config entry title.""" device_manager = await subscriber.async_get_device_manager() @@ -623,7 +542,7 @@ async def test_config_entry_title_from_home(hass, oauth, subscriber): ) ) - assert await async_setup_configflow(hass) + await setup_platform() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -631,16 +550,11 @@ async def test_config_entry_title_from_home(hass, oauth, subscriber): result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) await oauth.async_oauth_app_flow(result) - with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber", - return_value=subscriber, - ): - result = await oauth.async_configure(result, {"code": "1234"}) - await oauth.async_pubsub_flow(result) - entry = await oauth.async_finish_setup( - result, {"cloud_project_id": CLOUD_PROJECT_ID} - ) - await hass.async_block_till_done() + result = await oauth.async_configure(result, {"code": "1234"}) + await oauth.async_pubsub_flow(result) + entry = await oauth.async_finish_setup( + result, {"cloud_project_id": CLOUD_PROJECT_ID} + ) assert entry.title == "Example Home" assert "token" in entry.data @@ -648,7 +562,10 @@ async def test_config_entry_title_from_home(hass, oauth, subscriber): assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID -async def test_config_entry_title_multiple_homes(hass, oauth, subscriber): +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) +async def test_config_entry_title_multiple_homes( + hass, oauth, setup_platform, subscriber +): """Test handling of multiple Google Homes authorized.""" device_manager = await subscriber.async_get_device_manager() @@ -677,7 +594,7 @@ async def test_config_entry_title_multiple_homes(hass, oauth, subscriber): ) ) - assert await async_setup_configflow(hass) + await setup_platform() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -685,23 +602,18 @@ async def test_config_entry_title_multiple_homes(hass, oauth, subscriber): result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) await oauth.async_oauth_app_flow(result) - with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber", - return_value=subscriber, - ): - result = await oauth.async_configure(result, {"code": "1234"}) - await oauth.async_pubsub_flow(result) - entry = await oauth.async_finish_setup( - result, {"cloud_project_id": CLOUD_PROJECT_ID} - ) - await hass.async_block_till_done() - + result = await oauth.async_configure(result, {"code": "1234"}) + await oauth.async_pubsub_flow(result) + entry = await oauth.async_finish_setup( + result, {"cloud_project_id": CLOUD_PROJECT_ID} + ) assert entry.title == "Example Home #1, Example Home #2" -async def test_title_failure_fallback(hass, oauth): +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) +async def test_title_failure_fallback(hass, oauth, setup_platform, mock_subscriber): """Test exception handling when determining the structure names.""" - assert await async_setup_configflow(hass) + await setup_platform() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -709,19 +621,13 @@ async def test_title_failure_fallback(hass, oauth): result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) await oauth.async_oauth_app_flow(result) - mock_subscriber = AsyncMock(FakeSubscriber) mock_subscriber.async_get_device_manager.side_effect = AuthException() - with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber", - return_value=mock_subscriber, - ): - result = await oauth.async_configure(result, {"code": "1234"}) - await oauth.async_pubsub_flow(result) - entry = await oauth.async_finish_setup( - result, {"cloud_project_id": CLOUD_PROJECT_ID} - ) - await hass.async_block_till_done() + result = await oauth.async_configure(result, {"code": "1234"}) + await oauth.async_pubsub_flow(result) + entry = await oauth.async_finish_setup( + result, {"cloud_project_id": CLOUD_PROJECT_ID} + ) assert entry.title == "OAuth for Apps" assert "token" in entry.data @@ -729,7 +635,8 @@ async def test_title_failure_fallback(hass, oauth): assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID -async def test_structure_missing_trait(hass, oauth, subscriber): +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) +async def test_structure_missing_trait(hass, oauth, setup_platform, subscriber): """Test handling the case where a structure has no name set.""" device_manager = await subscriber.async_get_device_manager() @@ -743,7 +650,7 @@ async def test_structure_missing_trait(hass, oauth, subscriber): ) ) - assert await async_setup_configflow(hass) + await setup_platform() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -751,16 +658,11 @@ async def test_structure_missing_trait(hass, oauth, subscriber): result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) await oauth.async_oauth_app_flow(result) - with patch( - "homeassistant.components.nest.api.GoogleNestSubscriber", - return_value=subscriber, - ): - result = await oauth.async_configure(result, {"code": "1234"}) - await oauth.async_pubsub_flow(result) - entry = await oauth.async_finish_setup( - result, {"cloud_project_id": CLOUD_PROJECT_ID} - ) - await hass.async_block_till_done() + result = await oauth.async_configure(result, {"code": "1234"}) + await oauth.async_pubsub_flow(result) + entry = await oauth.async_finish_setup( + result, {"cloud_project_id": CLOUD_PROJECT_ID} + ) # Fallback to default name assert entry.title == "OAuth for Apps" @@ -778,9 +680,10 @@ async def test_dhcp_discovery_without_config(hass, oauth): assert result["reason"] == "missing_configuration" -async def test_dhcp_discovery(hass, oauth): +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_YAML_ONLY]) +async def test_dhcp_discovery(hass, oauth, setup_platform): """Discover via dhcp when config is present.""" - assert await setup.async_setup_component(hass, DOMAIN, CONFIG) + await setup_platform() result = await hass.config_entries.flow.async_init( DOMAIN, From fe1c3d3be8cb947b8c60d5c0329425ab74f341ca Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 3 Jun 2022 08:00:47 +0200 Subject: [PATCH 1218/3516] Revert "Allow non-async functions in device automation (#72147)" (#72909) --- .../components/device_automation/__init__.py | 25 ++----------------- .../components/device_automation/action.py | 9 +++---- .../components/device_automation/condition.py | 9 +++---- .../components/device_automation/trigger.py | 9 +++---- 4 files changed, 14 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 99629f3dd23..0a1ec495e70 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -189,22 +189,6 @@ def _async_set_entity_device_automation_metadata( automation["metadata"]["secondary"] = bool(entry.entity_category or entry.hidden_by) -async def _async_get_automation_for_device( - hass: HomeAssistant, - platform: DeviceAutomationPlatformType, - function_name: str, - device_id: str, -) -> list[dict[str, Any]]: - """List device automations.""" - automations = getattr(platform, function_name)(hass, device_id) - if asyncio.iscoroutine(automations): - # Using a coroutine to get device automations is deprecated - # enable warning when core is fully migrated - # then remove in Home Assistant Core xxxx.xx - return await automations # type: ignore[no-any-return] - return automations # type: ignore[no-any-return] - - async def _async_get_device_automations_from_domain( hass: HomeAssistant, domain: str, @@ -224,7 +208,7 @@ async def _async_get_device_automations_from_domain( return await asyncio.gather( # type: ignore[no-any-return] *( - _async_get_automation_for_device(hass, platform, function_name, device_id) + getattr(platform, function_name)(hass, device_id) for device_id in device_ids ), return_exceptions=return_exceptions, @@ -310,12 +294,7 @@ async def _async_get_device_automation_capabilities( return {} try: - capabilities = getattr(platform, function_name)(hass, automation) - if asyncio.iscoroutine(capabilities): - # Using a coroutine to get device automation capabitilites is deprecated - # enable warning when core is fully migrated - # then remove in Home Assistant Core xxxx.xx - capabilities = await capabilities + capabilities = await getattr(platform, function_name)(hass, automation) except InvalidDeviceAutomationConfig: return {} diff --git a/homeassistant/components/device_automation/action.py b/homeassistant/components/device_automation/action.py index 5737fbc5bf3..081b6bb283a 100644 --- a/homeassistant/components/device_automation/action.py +++ b/homeassistant/components/device_automation/action.py @@ -1,7 +1,6 @@ """Device action validator.""" from __future__ import annotations -from collections.abc import Awaitable from typing import Any, Protocol, cast import voluptuous as vol @@ -36,14 +35,14 @@ class DeviceAutomationActionProtocol(Protocol): ) -> None: """Execute a device action.""" - def async_get_action_capabilities( + async def async_get_action_capabilities( self, hass: HomeAssistant, config: ConfigType - ) -> dict[str, vol.Schema] | Awaitable[dict[str, vol.Schema]]: + ) -> dict[str, vol.Schema]: """List action capabilities.""" - def async_get_actions( + async def async_get_actions( self, hass: HomeAssistant, device_id: str - ) -> list[dict[str, Any]] | Awaitable[list[dict[str, Any]]]: + ) -> list[dict[str, Any]]: """List actions.""" diff --git a/homeassistant/components/device_automation/condition.py b/homeassistant/components/device_automation/condition.py index 1f1f8e94832..d656908f4be 100644 --- a/homeassistant/components/device_automation/condition.py +++ b/homeassistant/components/device_automation/condition.py @@ -1,7 +1,6 @@ """Validate device conditions.""" from __future__ import annotations -from collections.abc import Awaitable from typing import TYPE_CHECKING, Any, Protocol, cast import voluptuous as vol @@ -36,14 +35,14 @@ class DeviceAutomationConditionProtocol(Protocol): ) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" - def async_get_condition_capabilities( + async def async_get_condition_capabilities( self, hass: HomeAssistant, config: ConfigType - ) -> dict[str, vol.Schema] | Awaitable[dict[str, vol.Schema]]: + ) -> dict[str, vol.Schema]: """List condition capabilities.""" - def async_get_conditions( + async def async_get_conditions( self, hass: HomeAssistant, device_id: str - ) -> list[dict[str, Any]] | Awaitable[list[dict[str, Any]]]: + ) -> list[dict[str, Any]]: """List conditions.""" diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py index c5f42b3e813..eb39ec383af 100644 --- a/homeassistant/components/device_automation/trigger.py +++ b/homeassistant/components/device_automation/trigger.py @@ -1,7 +1,6 @@ """Offer device oriented automation.""" from __future__ import annotations -from collections.abc import Awaitable from typing import Any, Protocol, cast import voluptuous as vol @@ -46,14 +45,14 @@ class DeviceAutomationTriggerProtocol(Protocol): ) -> CALLBACK_TYPE: """Attach a trigger.""" - def async_get_trigger_capabilities( + async def async_get_trigger_capabilities( self, hass: HomeAssistant, config: ConfigType - ) -> dict[str, vol.Schema] | Awaitable[dict[str, vol.Schema]]: + ) -> dict[str, vol.Schema]: """List trigger capabilities.""" - def async_get_triggers( + async def async_get_triggers( self, hass: HomeAssistant, device_id: str - ) -> list[dict[str, Any]] | Awaitable[list[dict[str, Any]]]: + ) -> list[dict[str, Any]]: """List triggers.""" From f5c6ad24c4094e052c52f3252d535e4a5db3107f Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Fri, 3 Jun 2022 16:09:00 +1000 Subject: [PATCH 1219/3516] Bump aiolifx to 0.8.1 (#72897) Bump aiolifx version to support the latest LIFX devices LIFX added 22 new product definitions to their public product list at the end of January and those new products are defined in aiolifx v0.8.1, so bump the dependency version. Also switched to testing for relays instead of maintaining a seperate list of switch product IDs. Fixes #72894. Signed-off-by: Avi Miller --- homeassistant/components/lifx/light.py | 3 +-- homeassistant/components/lifx/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 31e973874d9..ea9bbeb91a2 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -73,7 +73,6 @@ MESSAGE_RETRIES = 3 UNAVAILABLE_GRACE = 90 FIX_MAC_FW = AwesomeVersion("3.70") -SWITCH_PRODUCT_IDS = [70, 71, 89] SERVICE_LIFX_SET_STATE = "set_state" @@ -403,7 +402,7 @@ class LIFXManager: # Get the product info first so that LIFX Switches # can be ignored. version_resp = await ack(bulb.get_version) - if version_resp and bulb.product in SWITCH_PRODUCT_IDS: + if version_resp and lifx_features(bulb)["relays"]: _LOGGER.debug( "Not connecting to LIFX Switch %s (%s)", str(bulb.mac_addr).replace(":", ""), diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index a7f266b6f7d..06e7b292ac6 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -3,7 +3,7 @@ "name": "LIFX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lifx", - "requirements": ["aiolifx==0.7.1", "aiolifx_effects==0.2.2"], + "requirements": ["aiolifx==0.8.1", "aiolifx_effects==0.2.2"], "dependencies": ["network"], "homekit": { "models": [ diff --git a/requirements_all.txt b/requirements_all.txt index 35b82892119..eaa63c401ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -181,7 +181,7 @@ aiokafka==0.6.0 aiokef==0.2.16 # homeassistant.components.lifx -aiolifx==0.7.1 +aiolifx==0.8.1 # homeassistant.components.lifx aiolifx_effects==0.2.2 From a28fa5377ac8f8291b6c80803491737438175358 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Jun 2022 20:59:00 -1000 Subject: [PATCH 1220/3516] Remove unused code from logbook (#72950) --- homeassistant/components/logbook/processor.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index b3a43c2ca35..e5cc0f124b0 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -417,12 +417,6 @@ def _rows_match(row: Row | EventAsRow, other_row: Row | EventAsRow) -> bool: return False -def _row_event_data_extract(row: Row | EventAsRow, extractor: re.Pattern) -> str | None: - """Extract from event_data row.""" - result = extractor.search(row.shared_data or row.event_data or "") - return result.group(1) if result else None - - def _row_time_fired_isoformat(row: Row | EventAsRow) -> str: """Convert the row timed_fired to isoformat.""" return process_timestamp_to_utc_isoformat(row.time_fired or dt_util.utcnow()) From 8910d265d6cf15fed4e6e98b4344031019c1016d Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 3 Jun 2022 13:55:57 +0200 Subject: [PATCH 1221/3516] Keep track of a context for each listener (#72702) * Remove async_remove_listener This avoids the ambuigity as to what happens if same callback is added multiple times. * Keep track of a context for each listener This allow a update coordinator to adapt what data to request on update from the backing service based on which entities are enabled. * Clone list before calling callbacks The callbacks can end up unregistering and modifying the dict while iterating. * Only yield actual values * Add a test for update context * Factor out iteration of _listeners to helper * Verify context is passed to coordinator * Switch to Any as type instead of object * Remove function which use was dropped earliers The use was removed in 8bee25c938a123f0da7569b4e2753598d478b900 --- .../components/bmw_connected_drive/button.py | 2 +- .../bmw_connected_drive/coordinator.py | 5 -- .../components/modern_forms/__init__.py | 9 +-- .../components/moehlenhoff_alpha2/__init__.py | 10 ++- .../components/philips_js/__init__.py | 26 ++------ .../components/system_bridge/coordinator.py | 19 ++---- homeassistant/components/toon/coordinator.py | 7 +- homeassistant/components/toon/helpers.py | 4 +- homeassistant/components/wemo/wemo_device.py | 6 -- homeassistant/components/wled/coordinator.py | 7 +- homeassistant/components/wled/helpers.py | 4 +- .../yamaha_musiccast/media_player.py | 5 +- homeassistant/helpers/update_coordinator.py | 65 +++++++++++-------- tests/helpers/test_update_coordinator.py | 41 +++++++++--- 14 files changed, 95 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/button.py b/homeassistant/components/bmw_connected_drive/button.py index 9cec9a73ce7..baa7870ee8c 100644 --- a/homeassistant/components/bmw_connected_drive/button.py +++ b/homeassistant/components/bmw_connected_drive/button.py @@ -131,4 +131,4 @@ class BMWButton(BMWBaseEntity, ButtonEntity): # Always update HA states after a button was executed. # BMW remote services that change the vehicle's state update the local object # when executing the service, so only the HA state machine needs further updates. - self.coordinator.notify_listeners() + self.coordinator.async_update_listeners() diff --git a/homeassistant/components/bmw_connected_drive/coordinator.py b/homeassistant/components/bmw_connected_drive/coordinator.py index 47d1f358686..1443a3e1e29 100644 --- a/homeassistant/components/bmw_connected_drive/coordinator.py +++ b/homeassistant/components/bmw_connected_drive/coordinator.py @@ -74,8 +74,3 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator): if not refresh_token: data.pop(CONF_REFRESH_TOKEN) self.hass.config_entries.async_update_entry(self._entry, data=data) - - def notify_listeners(self) -> None: - """Notify all listeners to refresh HA state machine.""" - for update_callback in self._listeners: - update_callback() diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py index af4f05a1536..ed4212d9444 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -74,12 +74,12 @@ def modernforms_exception_handler(func): async def handler(self, *args, **kwargs): try: await func(self, *args, **kwargs) - self.coordinator.update_listeners() + self.coordinator.async_update_listeners() except ModernFormsConnectionError as error: _LOGGER.error("Error communicating with API: %s", error) self.coordinator.last_update_success = False - self.coordinator.update_listeners() + self.coordinator.async_update_listeners() except ModernFormsError as error: _LOGGER.error("Invalid response from API: %s", error) @@ -108,11 +108,6 @@ class ModernFormsDataUpdateCoordinator(DataUpdateCoordinator[ModernFormsDeviceSt update_interval=SCAN_INTERVAL, ) - def update_listeners(self) -> None: - """Call update on all listeners.""" - for update_callback in self._listeners: - update_callback() - async def _async_update_data(self) -> ModernFormsDevice: """Fetch data from Modern Forms.""" try: diff --git a/homeassistant/components/moehlenhoff_alpha2/__init__.py b/homeassistant/components/moehlenhoff_alpha2/__init__.py index 86306a56033..64bdfeb4e6d 100644 --- a/homeassistant/components/moehlenhoff_alpha2/__init__.py +++ b/homeassistant/components/moehlenhoff_alpha2/__init__.py @@ -83,8 +83,7 @@ class Alpha2BaseCoordinator(DataUpdateCoordinator[dict[str, dict]]): async def async_set_cooling(self, enabled: bool) -> None: """Enable or disable cooling mode.""" await self.base.set_cooling(enabled) - for update_callback in self._listeners: - update_callback() + self.async_update_listeners() async def async_set_target_temperature( self, heat_area_id: str, target_temperature: float @@ -117,8 +116,7 @@ class Alpha2BaseCoordinator(DataUpdateCoordinator[dict[str, dict]]): "Failed to set target temperature, communication error with alpha2 base" ) from http_err self.data["heat_areas"][heat_area_id].update(update_data) - for update_callback in self._listeners: - update_callback() + self.async_update_listeners() async def async_set_heat_area_mode( self, heat_area_id: str, heat_area_mode: int @@ -161,5 +159,5 @@ class Alpha2BaseCoordinator(DataUpdateCoordinator[dict[str, dict]]): self.data["heat_areas"][heat_area_id]["T_TARGET"] = self.data[ "heat_areas" ][heat_area_id]["T_HEAT_NIGHT"] - for update_callback in self._listeners: - update_callback() + + self.async_update_listeners() diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 9a317726768..29e92a6ffe3 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -19,14 +19,7 @@ from homeassistant.const import ( CONF_USERNAME, Platform, ) -from homeassistant.core import ( - CALLBACK_TYPE, - Context, - Event, - HassJob, - HomeAssistant, - callback, -) +from homeassistant.core import Context, HassJob, HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -121,12 +114,7 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): self.options = options self._notify_future: asyncio.Task | None = None - @callback - def _update_listeners(): - for update_callback in self._listeners: - update_callback() - - self.turn_on = PluggableAction(_update_listeners) + self.turn_on = PluggableAction(self.async_update_listeners) super().__init__( hass, @@ -193,15 +181,9 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): self._notify_future = asyncio.create_task(self._notify_task()) @callback - def async_remove_listener(self, update_callback: CALLBACK_TYPE) -> None: + def _unschedule_refresh(self) -> None: """Remove data update.""" - super().async_remove_listener(update_callback) - if not self._listeners: - self._async_notify_stop() - - @callback - def _async_stop_refresh(self, event: Event) -> None: - super()._async_stop_refresh(event) + super()._unschedule_refresh() self._async_notify_stop() @callback diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py index 89a0c85c1d9..a7343116cde 100644 --- a/homeassistant/components/system_bridge/coordinator.py +++ b/homeassistant/components/system_bridge/coordinator.py @@ -75,11 +75,6 @@ class SystemBridgeDataUpdateCoordinator( hass, LOGGER, name=DOMAIN, update_interval=timedelta(seconds=30) ) - def update_listeners(self) -> None: - """Call update on all listeners.""" - for update_callback in self._listeners: - update_callback() - async def async_get_data( self, modules: list[str], @@ -113,7 +108,7 @@ class SystemBridgeDataUpdateCoordinator( self.unsub() self.unsub = None self.last_update_success = False - self.update_listeners() + self.async_update_listeners() except (ConnectionClosedException, ConnectionResetError) as exception: self.logger.info( "Websocket connection closed for %s. Will retry: %s", @@ -124,7 +119,7 @@ class SystemBridgeDataUpdateCoordinator( self.unsub() self.unsub = None self.last_update_success = False - self.update_listeners() + self.async_update_listeners() except ConnectionErrorException as exception: self.logger.warning( "Connection error occurred for %s. Will retry: %s", @@ -135,7 +130,7 @@ class SystemBridgeDataUpdateCoordinator( self.unsub() self.unsub = None self.last_update_success = False - self.update_listeners() + self.async_update_listeners() async def _setup_websocket(self) -> None: """Use WebSocket for updates.""" @@ -151,7 +146,7 @@ class SystemBridgeDataUpdateCoordinator( self.unsub() self.unsub = None self.last_update_success = False - self.update_listeners() + self.async_update_listeners() except ConnectionErrorException as exception: self.logger.warning( "Connection error occurred for %s. Will retry: %s", @@ -159,7 +154,7 @@ class SystemBridgeDataUpdateCoordinator( exception, ) self.last_update_success = False - self.update_listeners() + self.async_update_listeners() except asyncio.TimeoutError as exception: self.logger.warning( "Timed out waiting for %s. Will retry: %s", @@ -167,11 +162,11 @@ class SystemBridgeDataUpdateCoordinator( exception, ) self.last_update_success = False - self.update_listeners() + self.async_update_listeners() self.hass.async_create_task(self._listen_for_data()) self.last_update_success = True - self.update_listeners() + self.async_update_listeners() async def close_websocket(_) -> None: """Close WebSocket connection.""" diff --git a/homeassistant/components/toon/coordinator.py b/homeassistant/components/toon/coordinator.py index 81c09931fbd..5819ff12743 100644 --- a/homeassistant/components/toon/coordinator.py +++ b/homeassistant/components/toon/coordinator.py @@ -47,11 +47,6 @@ class ToonDataUpdateCoordinator(DataUpdateCoordinator[Status]): hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL ) - def update_listeners(self) -> None: - """Call update on all listeners.""" - for update_callback in self._listeners: - update_callback() - async def register_webhook(self, event: Event | None = None) -> None: """Register a webhook with Toon to get live updates.""" if CONF_WEBHOOK_ID not in self.entry.data: @@ -128,7 +123,7 @@ class ToonDataUpdateCoordinator(DataUpdateCoordinator[Status]): try: await self.toon.update(data["updateDataSet"]) - self.update_listeners() + self.async_update_listeners() except ToonError as err: _LOGGER.error("Could not process data received from Toon webhook - %s", err) diff --git a/homeassistant/components/toon/helpers.py b/homeassistant/components/toon/helpers.py index 405ecc36d7f..4fb4daede65 100644 --- a/homeassistant/components/toon/helpers.py +++ b/homeassistant/components/toon/helpers.py @@ -16,12 +16,12 @@ def toon_exception_handler(func): async def handler(self, *args, **kwargs): try: await func(self, *args, **kwargs) - self.coordinator.update_listeners() + self.coordinator.async_update_listeners() except ToonConnectionError as error: _LOGGER.error("Error communicating with API: %s", error) self.coordinator.last_update_success = False - self.coordinator.update_listeners() + self.coordinator.async_update_listeners() except ToonError as error: _LOGGER.error("Invalid response from API: %s", error) diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py index 8f5e6864059..1f3e07881c8 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -123,12 +123,6 @@ class DeviceCoordinator(DataUpdateCoordinator): except ActionException as err: raise UpdateFailed("WeMo update failed") from err - @callback - def async_update_listeners(self) -> None: - """Update all listeners.""" - for update_callback in self._listeners: - update_callback() - def _device_info(wemo: WeMoDevice) -> DeviceInfo: return DeviceInfo( diff --git a/homeassistant/components/wled/coordinator.py b/homeassistant/components/wled/coordinator.py index a4cbaade8ba..81017779fbb 100644 --- a/homeassistant/components/wled/coordinator.py +++ b/homeassistant/components/wled/coordinator.py @@ -54,11 +54,6 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]): self.data is not None and len(self.data.state.segments) > 1 ) - def update_listeners(self) -> None: - """Call update on all listeners.""" - for update_callback in self._listeners: - update_callback() - @callback def _use_websocket(self) -> None: """Use WebSocket for updates, instead of polling.""" @@ -81,7 +76,7 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]): self.logger.info(err) except WLEDError as err: self.last_update_success = False - self.update_listeners() + self.async_update_listeners() self.logger.error(err) # Ensure we are disconnected diff --git a/homeassistant/components/wled/helpers.py b/homeassistant/components/wled/helpers.py index 66cd8b13b42..77e288bb34d 100644 --- a/homeassistant/components/wled/helpers.py +++ b/homeassistant/components/wled/helpers.py @@ -15,11 +15,11 @@ def wled_exception_handler(func): async def handler(self, *args, **kwargs): try: await func(self, *args, **kwargs) - self.coordinator.update_listeners() + self.coordinator.async_update_listeners() except WLEDConnectionError as error: self.coordinator.last_update_success = False - self.coordinator.update_listeners() + self.coordinator.async_update_listeners() raise HomeAssistantError("Error communicating with WLED API") from error except WLEDError as error: diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 954942b2c6b..cee6253531b 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -106,7 +106,9 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): self.coordinator.musiccast.register_group_update_callback( self.update_all_mc_entities ) - self.coordinator.async_add_listener(self.async_schedule_check_client_list) + self.async_on_remove( + self.coordinator.async_add_listener(self.async_schedule_check_client_list) + ) async def async_will_remove_from_hass(self): """Entity being removed from hass.""" @@ -116,7 +118,6 @@ class MusicCastMediaPlayer(MusicCastDeviceEntity, MediaPlayerEntity): self.coordinator.musiccast.remove_group_update_callback( self.update_all_mc_entities ) - self.coordinator.async_remove_listener(self.async_schedule_check_client_list) @property def should_poll(self): diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index f7ad8e013cb..f671e1b973a 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Generator from datetime import datetime, timedelta import logging from time import monotonic @@ -13,7 +13,7 @@ import aiohttp import requests from homeassistant import config_entries -from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.util.dt import utcnow @@ -61,7 +61,7 @@ class DataUpdateCoordinator(Generic[_T]): # when it was already checked during setup. self.data: _T = None # type: ignore[assignment] - self._listeners: list[CALLBACK_TYPE] = [] + self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {} self._job = HassJob(self._handle_refresh_interval) self._unsub_refresh: CALLBACK_TYPE | None = None self._request_refresh_task: asyncio.TimerHandle | None = None @@ -82,32 +82,46 @@ class DataUpdateCoordinator(Generic[_T]): self._debounced_refresh = request_refresh_debouncer @callback - def async_add_listener(self, update_callback: CALLBACK_TYPE) -> Callable[[], None]: + def async_add_listener( + self, update_callback: CALLBACK_TYPE, context: Any = None + ) -> Callable[[], None]: """Listen for data updates.""" schedule_refresh = not self._listeners - self._listeners.append(update_callback) + @callback + def remove_listener() -> None: + """Remove update listener.""" + self._listeners.pop(remove_listener) + if not self._listeners: + self._unschedule_refresh() + + self._listeners[remove_listener] = (update_callback, context) # This is the first listener, set up interval. if schedule_refresh: self._schedule_refresh() - @callback - def remove_listener() -> None: - """Remove update listener.""" - self.async_remove_listener(update_callback) - return remove_listener @callback - def async_remove_listener(self, update_callback: CALLBACK_TYPE) -> None: - """Remove data update.""" - self._listeners.remove(update_callback) + def async_update_listeners(self) -> None: + """Update all registered listeners.""" + for update_callback, _ in list(self._listeners.values()): + update_callback() - if not self._listeners and self._unsub_refresh: + @callback + def _unschedule_refresh(self) -> None: + """Unschedule any pending refresh since there is no longer any listeners.""" + if self._unsub_refresh: self._unsub_refresh() self._unsub_refresh = None + def async_contexts(self) -> Generator[Any, None, None]: + """Return all registered contexts.""" + yield from ( + context for _, context in self._listeners.values() if context is not None + ) + @callback def _schedule_refresh(self) -> None: """Schedule a refresh.""" @@ -266,8 +280,7 @@ class DataUpdateCoordinator(Generic[_T]): if not auth_failed and self._listeners and not self.hass.is_stopping: self._schedule_refresh() - for update_callback in self._listeners: - update_callback() + self.async_update_listeners() @callback def async_set_updated_data(self, data: _T) -> None: @@ -288,24 +301,18 @@ class DataUpdateCoordinator(Generic[_T]): if self._listeners: self._schedule_refresh() - for update_callback in self._listeners: - update_callback() - - @callback - def _async_stop_refresh(self, _: Event) -> None: - """Stop refreshing when Home Assistant is stopping.""" - self.update_interval = None - if self._unsub_refresh: - self._unsub_refresh() - self._unsub_refresh = None + self.async_update_listeners() class CoordinatorEntity(entity.Entity, Generic[_DataUpdateCoordinatorT]): """A class for entities using DataUpdateCoordinator.""" - def __init__(self, coordinator: _DataUpdateCoordinatorT) -> None: + def __init__( + self, coordinator: _DataUpdateCoordinatorT, context: Any = None + ) -> None: """Create the entity with a DataUpdateCoordinator.""" self.coordinator = coordinator + self.coordinator_context = context @property def should_poll(self) -> bool: @@ -321,7 +328,9 @@ class CoordinatorEntity(entity.Entity, Generic[_DataUpdateCoordinatorT]): """When entity is added to hass.""" await super().async_added_to_hass() self.async_on_remove( - self.coordinator.async_add_listener(self._handle_coordinator_update) + self.coordinator.async_add_listener( + self._handle_coordinator_update, self.coordinator_context + ) ) @callback diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 7023798f2b4..0d0970a4756 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -109,11 +109,29 @@ async def test_async_refresh(crd): await crd.async_refresh() assert updates == [2] - # Test unsubscribing through method - crd.async_add_listener(update_callback) - crd.async_remove_listener(update_callback) + +async def test_update_context(crd: update_coordinator.DataUpdateCoordinator[int]): + """Test update contexts for the update coordinator.""" await crd.async_refresh() - assert updates == [2] + assert not set(crd.async_contexts()) + + def update_callback1(): + pass + + def update_callback2(): + pass + + unsub1 = crd.async_add_listener(update_callback1, 1) + assert set(crd.async_contexts()) == {1} + + unsub2 = crd.async_add_listener(update_callback2, 2) + assert set(crd.async_contexts()) == {1, 2} + + unsub1() + assert set(crd.async_contexts()) == {2} + + unsub2() + assert not set(crd.async_contexts()) async def test_request_refresh(crd): @@ -191,7 +209,7 @@ async def test_update_interval(hass, crd): # Add subscriber update_callback = Mock() - crd.async_add_listener(update_callback) + unsub = crd.async_add_listener(update_callback) # Test twice we update with subscriber async_fire_time_changed(hass, utcnow() + crd.update_interval) @@ -203,7 +221,7 @@ async def test_update_interval(hass, crd): assert crd.data == 2 # Test removing listener - crd.async_remove_listener(update_callback) + unsub() async_fire_time_changed(hass, utcnow() + crd.update_interval) await hass.async_block_till_done() @@ -222,7 +240,7 @@ async def test_update_interval_not_present(hass, crd_without_update_interval): # Add subscriber update_callback = Mock() - crd.async_add_listener(update_callback) + unsub = crd.async_add_listener(update_callback) # Test twice we don't update with subscriber with no update interval async_fire_time_changed(hass, utcnow() + DEFAULT_UPDATE_INTERVAL) @@ -234,7 +252,7 @@ async def test_update_interval_not_present(hass, crd_without_update_interval): assert crd.data is None # Test removing listener - crd.async_remove_listener(update_callback) + unsub() async_fire_time_changed(hass, utcnow() + DEFAULT_UPDATE_INTERVAL) await hass.async_block_till_done() @@ -253,9 +271,10 @@ async def test_refresh_recover(crd, caplog): assert "Fetching test data recovered" in caplog.text -async def test_coordinator_entity(crd): +async def test_coordinator_entity(crd: update_coordinator.DataUpdateCoordinator[int]): """Test the CoordinatorEntity class.""" - entity = update_coordinator.CoordinatorEntity(crd) + context = object() + entity = update_coordinator.CoordinatorEntity(crd, context) assert entity.should_poll is False @@ -278,6 +297,8 @@ async def test_coordinator_entity(crd): await entity.async_update() assert entity.available is False + assert list(crd.async_contexts()) == [context] + async def test_async_set_updated_data(crd): """Test async_set_updated_data for update coordinator.""" From 52149c442ecc767d7164449700367df749d60bcd Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 3 Jun 2022 13:57:59 +0200 Subject: [PATCH 1222/3516] Bump pynetgear to 0.10.4 (#72965) bump pynetgear to 0.10.4 --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index ae0824c82a5..f65f5aa6686 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.10.0"], + "requirements": ["pynetgear==0.10.4"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index eaa63c401ba..bb8f3f1cf9b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1673,7 +1673,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.10.0 +pynetgear==0.10.4 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4753b643fb..a750e708354 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1131,7 +1131,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.10.0 +pynetgear==0.10.4 # homeassistant.components.nina pynina==0.1.8 From 88129dbe9180a5f5370343b2fd151c1f371599db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Beamonte?= Date: Fri, 3 Jun 2022 09:27:10 -0400 Subject: [PATCH 1223/3516] Allow `log` template function to return specified `default` on math domain error (#72960) Fix regression for logarithm template --- homeassistant/helpers/template.py | 14 +++++++------- tests/helpers/test_template.py | 2 ++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 1a8febf5ac2..053beab307e 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1369,19 +1369,19 @@ def multiply(value, amount, default=_SENTINEL): def logarithm(value, base=math.e, default=_SENTINEL): """Filter and function to get logarithm of the value with a specific base.""" - try: - value_float = float(value) - except (ValueError, TypeError): - if default is _SENTINEL: - raise_no_default("log", value) - return default try: base_float = float(base) except (ValueError, TypeError): if default is _SENTINEL: raise_no_default("log", base) return default - return math.log(value_float, base_float) + try: + value_float = float(value) + return math.log(value_float, base_float) + except (ValueError, TypeError): + if default is _SENTINEL: + raise_no_default("log", value) + return default def sine(value, default=_SENTINEL): diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index ddda17c20ac..a1fd3e73f59 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -447,6 +447,8 @@ def test_logarithm(hass): assert render(hass, "{{ 'no_number' | log(10, default=1) }}") == 1 assert render(hass, "{{ log('no_number', 10, 1) }}") == 1 assert render(hass, "{{ log('no_number', 10, default=1) }}") == 1 + assert render(hass, "{{ log(0, 10, 1) }}") == 1 + assert render(hass, "{{ log(0, 10, default=1) }}") == 1 def test_sine(hass): From beab6e2e5fcc5f9b254ab92c011ad39328072fbf Mon Sep 17 00:00:00 2001 From: w35l3y Date: Fri, 3 Jun 2022 10:32:22 -0300 Subject: [PATCH 1224/3516] Fix ended session when there isn't any response from the user (#72218) * Fix end session when there isn't any response This PR fixes #72153 * Added test case as requested https://github.com/home-assistant/core/pull/72218#discussion_r881584812 --- homeassistant/components/alexa/intent.py | 10 ++-- .../components/intent_script/__init__.py | 12 +++-- tests/components/alexa/test_intent.py | 7 ++- tests/components/intent_script/test_init.py | 48 +++++++++++++++++++ 4 files changed, 65 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index 7352bbd995a..ef145a9ceb8 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -127,11 +127,6 @@ async def async_handle_message(hass, message): @HANDLERS.register("SessionEndedRequest") -async def async_handle_session_end(hass, message): - """Handle a session end request.""" - return None - - @HANDLERS.register("IntentRequest") @HANDLERS.register("LaunchRequest") async def async_handle_intent(hass, message): @@ -151,6 +146,11 @@ async def async_handle_intent(hass, message): intent_name = ( message.get("session", {}).get("application", {}).get("applicationId") ) + elif req["type"] == "SessionEndedRequest": + app_id = message.get("session", {}).get("application", {}).get("applicationId") + intent_name = f"{app_id}.{req['type']}" + alexa_response.variables["reason"] = req["reason"] + alexa_response.variables["error"] = req.get("error") else: intent_name = alexa_intent_info["name"] diff --git a/homeassistant/components/intent_script/__init__.py b/homeassistant/components/intent_script/__init__.py index d14aaf5a68b..e8c5c580708 100644 --- a/homeassistant/components/intent_script/__init__.py +++ b/homeassistant/components/intent_script/__init__.py @@ -99,11 +99,13 @@ class ScriptIntentHandler(intent.IntentHandler): speech[CONF_TYPE], ) - if reprompt is not None and reprompt[CONF_TEXT].template: - response.async_set_reprompt( - reprompt[CONF_TEXT].async_render(slots, parse_result=False), - reprompt[CONF_TYPE], - ) + if reprompt is not None: + text_reprompt = reprompt[CONF_TEXT].async_render(slots, parse_result=False) + if text_reprompt: + response.async_set_reprompt( + text_reprompt, + reprompt[CONF_TYPE], + ) if card is not None: response.async_set_card( diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index f15fa860c7b..9c71bc32e4d 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -490,8 +490,11 @@ async def test_intent_session_ended_request(alexa_client): req = await _intent_req(alexa_client, data) assert req.status == HTTPStatus.OK - text = await req.text() - assert text == "" + data = await req.json() + assert ( + data["response"]["outputSpeech"]["text"] + == "This intent is not yet configured within Home Assistant." + ) async def test_intent_from_built_in_intent_library(alexa_client): diff --git a/tests/components/intent_script/test_init.py b/tests/components/intent_script/test_init.py index 6f345522e63..39f865f4832 100644 --- a/tests/components/intent_script/test_init.py +++ b/tests/components/intent_script/test_init.py @@ -38,6 +38,8 @@ async def test_intent_script(hass): assert response.speech["plain"]["speech"] == "Good morning Paulus" + assert not (response.reprompt) + assert response.card["simple"]["title"] == "Hello Paulus" assert response.card["simple"]["content"] == "Content for Paulus" @@ -85,3 +87,49 @@ async def test_intent_script_wait_response(hass): assert response.card["simple"]["title"] == "Hello Paulus" assert response.card["simple"]["content"] == "Content for Paulus" + + +async def test_intent_script_falsy_reprompt(hass): + """Test intent scripts work.""" + calls = async_mock_service(hass, "test", "service") + + await async_setup_component( + hass, + "intent_script", + { + "intent_script": { + "HelloWorld": { + "action": { + "service": "test.service", + "data_template": {"hello": "{{ name }}"}, + }, + "card": { + "title": "Hello {{ name }}", + "content": "Content for {{ name }}", + }, + "speech": { + "type": "ssml", + "text": 'Good morning {{ name }}', + }, + "reprompt": {"text": "{{ null }}"}, + } + } + }, + ) + + response = await intent.async_handle( + hass, "test", "HelloWorld", {"name": {"value": "Paulus"}} + ) + + assert len(calls) == 1 + assert calls[0].data["hello"] == "Paulus" + + assert ( + response.speech["ssml"]["speech"] + == 'Good morning Paulus' + ) + + assert not (response.reprompt) + + assert response.card["simple"]["title"] == "Hello Paulus" + assert response.card["simple"]["content"] == "Content for Paulus" From 6cadd4f6657ee5f12b5a6d28f61455a3c94cefa0 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 3 Jun 2022 15:33:43 +0200 Subject: [PATCH 1225/3516] MotionBlinds use device_name helper (#72438) * use device_name helper * fix typo * fix import * fix isort * add gateway_test * implement gateway test * correct test blind mac --- .../components/motion_blinds/cover.py | 6 +++--- .../components/motion_blinds/gateway.py | 9 ++++++++- .../components/motion_blinds/sensor.py | 19 ++++--------------- .../components/motion_blinds/test_gateway.py | 19 +++++++++++++++++++ 4 files changed, 34 insertions(+), 19 deletions(-) create mode 100644 tests/components/motion_blinds/test_gateway.py diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 7bac3a5fb20..23e7e3d834a 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -35,6 +35,7 @@ from .const import ( SERVICE_SET_ABSOLUTE_POSITION, UPDATE_INTERVAL_MOVING, ) +from .gateway import device_name _LOGGER = logging.getLogger(__name__) @@ -193,13 +194,12 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): if blind.device_type in DEVICE_TYPES_WIFI: via_device = () connections = {(dr.CONNECTION_NETWORK_MAC, blind.mac)} - name = blind.blind_type else: via_device = (DOMAIN, blind._gateway.mac) connections = {} - name = f"{blind.blind_type} {blind.mac[12:]}" sw_version = None + name = device_name(blind) self._attr_device_class = device_class self._attr_name = name self._attr_unique_id = blind.mac @@ -422,7 +422,7 @@ class MotionTDBUDevice(MotionPositionDevice): super().__init__(coordinator, blind, device_class, sw_version) self._motor = motor self._motor_key = motor[0] - self._attr_name = f"{blind.blind_type} {blind.mac[12:]} {motor}" + self._attr_name = f"{device_name(blind)} {motor}" self._attr_unique_id = f"{blind.mac}-{motor}" if self._motor not in ["Bottom", "Top", "Combined"]: diff --git a/homeassistant/components/motion_blinds/gateway.py b/homeassistant/components/motion_blinds/gateway.py index 218da9f625c..96a85246666 100644 --- a/homeassistant/components/motion_blinds/gateway.py +++ b/homeassistant/components/motion_blinds/gateway.py @@ -3,7 +3,7 @@ import contextlib import logging import socket -from motionblinds import AsyncMotionMulticast, MotionGateway +from motionblinds import DEVICE_TYPES_WIFI, AsyncMotionMulticast, MotionGateway from homeassistant.components import network @@ -12,6 +12,13 @@ from .const import DEFAULT_INTERFACE _LOGGER = logging.getLogger(__name__) +def device_name(blind): + """Construct common name part of a device.""" + if blind.device_type in DEVICE_TYPES_WIFI: + return blind.blind_type + return f"{blind.blind_type} {blind.mac[12:]}" + + class ConnectMotionGateway: """Class to async connect to a Motion Gateway.""" diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index ebaed95bcbf..3a6f775092e 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -10,6 +10,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTR_AVAILABLE, DOMAIN, KEY_COORDINATOR, KEY_GATEWAY +from .gateway import device_name ATTR_BATTERY_VOLTAGE = "battery_voltage" TYPE_BLIND = "blind" @@ -54,14 +55,9 @@ class MotionBatterySensor(CoordinatorEntity, SensorEntity): """Initialize the Motion Battery Sensor.""" super().__init__(coordinator) - if blind.device_type in DEVICE_TYPES_WIFI: - name = f"{blind.blind_type} battery" - else: - name = f"{blind.blind_type} {blind.mac[12:]} battery" - self._blind = blind self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, blind.mac)}) - self._attr_name = name + self._attr_name = f"{device_name(blind)} battery" self._attr_unique_id = f"{blind.mac}-battery" @property @@ -103,14 +99,9 @@ class MotionTDBUBatterySensor(MotionBatterySensor): """Initialize the Motion Battery Sensor.""" super().__init__(coordinator, blind) - if blind.device_type in DEVICE_TYPES_WIFI: - name = f"{blind.blind_type} {motor} battery" - else: - name = f"{blind.blind_type} {blind.mac[12:]} {motor} battery" - self._motor = motor self._attr_unique_id = f"{blind.mac}-{motor}-battery" - self._attr_name = name + self._attr_name = f"{device_name(blind)} {motor} battery" @property def native_value(self): @@ -144,10 +135,8 @@ class MotionSignalStrengthSensor(CoordinatorEntity, SensorEntity): if device_type == TYPE_GATEWAY: name = "Motion gateway signal strength" - elif device.device_type in DEVICE_TYPES_WIFI: - name = f"{device.blind_type} signal strength" else: - name = f"{device.blind_type} {device.mac[12:]} signal strength" + name = f"{device_name(device)} signal strength" self._device = device self._device_type = device_type diff --git a/tests/components/motion_blinds/test_gateway.py b/tests/components/motion_blinds/test_gateway.py new file mode 100644 index 00000000000..66f42c3c444 --- /dev/null +++ b/tests/components/motion_blinds/test_gateway.py @@ -0,0 +1,19 @@ +"""Test the Motion Blinds config flow.""" +from unittest.mock import Mock + +from motionblinds import DEVICE_TYPES_WIFI, BlindType + +from homeassistant.components.motion_blinds.gateway import device_name + +TEST_BLIND_MAC = "abcdefghujkl0001" + + +async def test_device_name(hass): + """test_device_name.""" + blind = Mock() + blind.blind_type = BlindType.RollerBlind.name + blind.mac = TEST_BLIND_MAC + assert device_name(blind) == "RollerBlind 0001" + + blind.device_type = DEVICE_TYPES_WIFI[0] + assert device_name(blind) == "RollerBlind" From a9d45d656d057ce91d19ab268f7d1296aed367ae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Jun 2022 03:49:17 -1000 Subject: [PATCH 1226/3516] Add to codeowners for logbook so I get notifications (#72964) - Adding explictly will get through my filters and I want to watch this one for at least this release --- CODEOWNERS | 4 ++-- homeassistant/components/logbook/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index db9c0ff69d8..fe8ce9a46ad 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -582,8 +582,8 @@ build.json @home-assistant/supervisor /tests/components/local_ip/ @issacg /homeassistant/components/lock/ @home-assistant/core /tests/components/lock/ @home-assistant/core -/homeassistant/components/logbook/ @home-assistant/core -/tests/components/logbook/ @home-assistant/core +/homeassistant/components/logbook/ @home-assistant/core @bdraco +/tests/components/logbook/ @home-assistant/core @bdraco /homeassistant/components/logger/ @home-assistant/core /tests/components/logger/ @home-assistant/core /homeassistant/components/logi_circle/ @evanjd diff --git a/homeassistant/components/logbook/manifest.json b/homeassistant/components/logbook/manifest.json index 66c0348a2ac..26a45e74439 100644 --- a/homeassistant/components/logbook/manifest.json +++ b/homeassistant/components/logbook/manifest.json @@ -3,6 +3,6 @@ "name": "Logbook", "documentation": "https://www.home-assistant.io/integrations/logbook", "dependencies": ["frontend", "http", "recorder"], - "codeowners": ["@home-assistant/core"], + "codeowners": ["@home-assistant/core", "@bdraco"], "quality_scale": "internal" } From 6da409d6e5cd2a6264d6d7b8c1988d00c47cea4b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 3 Jun 2022 17:12:38 +0200 Subject: [PATCH 1227/3516] Remove unused constant from auth (#72953) --- homeassistant/components/auth/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 27fe511182d..897ca037c98 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -150,7 +150,6 @@ from homeassistant.util import dt as dt_util from . import indieauth, login_flow, mfa_setup_flow DOMAIN = "auth" -RESULT_TYPE_CREDENTIALS = "credentials" @bind_hass From 18c26148023050f87b82a1644e2f59f2e076145e Mon Sep 17 00:00:00 2001 From: shbatm Date: Fri, 3 Jun 2022 11:53:23 -0500 Subject: [PATCH 1228/3516] Check ISY994 climate for unknown humidity value on Z-Wave Thermostat (#72990) Check ISY994 climate for unknown humidity on Z-Wave Thermostat Update to #72670 to compare the property value and not the parent object. Should actually fix #72628 --- homeassistant/components/isy994/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index d68395f14da..9f4c52258a7 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -117,7 +117,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): """Return the current humidity.""" if not (humidity := self._node.aux_properties.get(PROP_HUMIDITY)): return None - if humidity == ISY_VALUE_UNKNOWN: + if humidity.value == ISY_VALUE_UNKNOWN: return None return int(humidity.value) From 5ee2f4f438f8acb119308738639169138b15662c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 3 Jun 2022 21:11:57 +0200 Subject: [PATCH 1229/3516] Sensibo Set temperature improvement (#72992) --- homeassistant/components/sensibo/climate.py | 25 ++++++------- tests/components/sensibo/fixtures/data.json | 8 ++--- tests/components/sensibo/test_climate.py | 39 +++++++++++++++------ 3 files changed, 43 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 4b0e797a5b7..c7967d05be0 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -1,6 +1,7 @@ """Support for Sensibo wifi-enabled home thermostats.""" from __future__ import annotations +from bisect import bisect_left from typing import TYPE_CHECKING, Any import voluptuous as vol @@ -54,6 +55,14 @@ AC_STATE_TO_DATA = { } +def _find_valid_target_temp(target: int, valid_targets: list[int]) -> int: + if target <= valid_targets[0]: + return valid_targets[0] + if target >= valid_targets[-1]: + return valid_targets[-1] + return valid_targets[bisect_left(valid_targets, target)] + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -203,20 +212,8 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): if temperature == self.target_temperature: return - if temperature not in self.device_data.temp_list: - # Requested temperature is not supported. - if temperature > self.device_data.temp_list[-1]: - temperature = self.device_data.temp_list[-1] - - elif temperature < self.device_data.temp_list[0]: - temperature = self.device_data.temp_list[0] - - else: - raise ValueError( - f"Target temperature has to be one off {str(self.device_data.temp_list)}" - ) - - await self._async_set_ac_state_property("targetTemperature", int(temperature)) + new_temp = _find_valid_target_temp(temperature, self.device_data.temp_list) + await self._async_set_ac_state_property("targetTemperature", new_temp) async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" diff --git a/tests/components/sensibo/fixtures/data.json b/tests/components/sensibo/fixtures/data.json index c787ea5592c..6c44b44821f 100644 --- a/tests/components/sensibo/fixtures/data.json +++ b/tests/components/sensibo/fixtures/data.json @@ -251,7 +251,7 @@ }, "C": { "isNative": true, - "values": [18, 19, 20] + "values": [10, 16, 17, 18, 19, 20] } }, "fanLevels": ["quiet", "low", "medium"], @@ -267,7 +267,7 @@ }, "C": { "isNative": true, - "values": [17, 18, 19, 20] + "values": [10, 16, 17, 18, 19, 20] } }, "fanLevels": ["quiet", "low", "medium"], @@ -283,7 +283,7 @@ }, "C": { "isNative": true, - "values": [18, 19, 20] + "values": [10, 16, 17, 18, 19, 20] } }, "swing": ["stopped", "fixedTop", "fixedMiddleTop"], @@ -298,7 +298,7 @@ }, "C": { "isNative": true, - "values": [18, 19, 20, 21] + "values": [10, 16, 17, 18, 19, 20] } }, "fanLevels": ["quiet", "low", "medium"], diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index 79813727c15..ead4fb02d88 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -20,7 +20,10 @@ from homeassistant.components.climate.const import ( SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, ) -from homeassistant.components.sensibo.climate import SERVICE_ASSUME_STATE +from homeassistant.components.sensibo.climate import ( + SERVICE_ASSUME_STATE, + _find_valid_target_temp, +) from homeassistant.components.sensibo.const import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -37,6 +40,21 @@ from homeassistant.util import dt from tests.common import async_fire_time_changed +async def test_climate_find_valid_targets(): + """Test function to return temperature from valid targets.""" + + valid_targets = [10, 16, 17, 18, 19, 20] + + assert _find_valid_target_temp(7, valid_targets) == 10 + assert _find_valid_target_temp(10, valid_targets) == 10 + assert _find_valid_target_temp(11, valid_targets) == 16 + assert _find_valid_target_temp(15, valid_targets) == 16 + assert _find_valid_target_temp(16, valid_targets) == 16 + assert _find_valid_target_temp(18.5, valid_targets) == 19 + assert _find_valid_target_temp(20, valid_targets) == 20 + assert _find_valid_target_temp(25, valid_targets) == 20 + + async def test_climate( hass: HomeAssistant, load_int: ConfigEntry, get_data: SensiboData ) -> None: @@ -55,7 +73,7 @@ async def test_climate( "fan_only", "off", ], - "min_temp": 17, + "min_temp": 10, "max_temp": 20, "target_temp_step": 1, "fan_modes": ["quiet", "low", "medium"], @@ -244,23 +262,22 @@ async def test_climate_temperatures( await hass.async_block_till_done() state2 = hass.states.get("climate.hallway") - assert state2.attributes["temperature"] == 17 + assert state2.attributes["temperature"] == 16 with patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", return_value={"result": {"status": "Success"}}, ): - with pytest.raises(ValueError): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 18.5}, - blocking=True, - ) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_TEMPERATURE: 18.5}, + blocking=True, + ) await hass.async_block_till_done() state2 = hass.states.get("climate.hallway") - assert state2.attributes["temperature"] == 17 + assert state2.attributes["temperature"] == 19 with patch( "homeassistant.components.sensibo.util.SensiboClient.async_set_ac_state_property", From 8d0dd1fe8cdf25956b378d78cac1e4e152c5a413 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 3 Jun 2022 21:24:04 +0200 Subject: [PATCH 1230/3516] Config flow for scrape integration (#70476) --- CODEOWNERS | 4 +- homeassistant/components/scrape/__init__.py | 63 +++++ .../components/scrape/config_flow.py | 122 ++++++++++ homeassistant/components/scrape/const.py | 13 ++ homeassistant/components/scrape/manifest.json | 3 +- homeassistant/components/scrape/sensor.py | 101 ++++---- homeassistant/components/scrape/strings.json | 73 ++++++ .../components/scrape/translations/en.json | 73 ++++++ homeassistant/generated/config_flows.py | 1 + tests/components/scrape/__init__.py | 40 +++- tests/components/scrape/test_config_flow.py | 194 ++++++++++++++++ tests/components/scrape/test_init.py | 89 +++++++ tests/components/scrape/test_sensor.py | 219 ++++++++++-------- 13 files changed, 853 insertions(+), 142 deletions(-) create mode 100644 homeassistant/components/scrape/config_flow.py create mode 100644 homeassistant/components/scrape/const.py create mode 100644 homeassistant/components/scrape/strings.json create mode 100644 homeassistant/components/scrape/translations/en.json create mode 100644 tests/components/scrape/test_config_flow.py create mode 100644 tests/components/scrape/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index fe8ce9a46ad..e2e9fc27b5c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -887,8 +887,8 @@ build.json @home-assistant/supervisor /homeassistant/components/scene/ @home-assistant/core /tests/components/scene/ @home-assistant/core /homeassistant/components/schluter/ @prairieapps -/homeassistant/components/scrape/ @fabaff -/tests/components/scrape/ @fabaff +/homeassistant/components/scrape/ @fabaff @gjohansson-ST +/tests/components/scrape/ @fabaff @gjohansson-ST /homeassistant/components/screenlogic/ @dieselrabbit @bdraco /tests/components/screenlogic/ @dieselrabbit @bdraco /homeassistant/components/script/ @home-assistant/core diff --git a/homeassistant/components/scrape/__init__.py b/homeassistant/components/scrape/__init__.py index f9222c126b5..684be76b80d 100644 --- a/homeassistant/components/scrape/__init__.py +++ b/homeassistant/components/scrape/__init__.py @@ -1 +1,64 @@ """The scrape component.""" +from __future__ import annotations + +import httpx + +from homeassistant.components.rest.data import RestData +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_HEADERS, + CONF_PASSWORD, + CONF_RESOURCE, + CONF_USERNAME, + CONF_VERIFY_SSL, + HTTP_DIGEST_AUTHENTICATION, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN, PLATFORMS + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Scrape from a config entry.""" + + resource: str = entry.options[CONF_RESOURCE] + method: str = "GET" + payload: str | None = None + headers: str | None = entry.options.get(CONF_HEADERS) + verify_ssl: bool = entry.options[CONF_VERIFY_SSL] + username: str | None = entry.options.get(CONF_USERNAME) + password: str | None = entry.options.get(CONF_PASSWORD) + + auth: httpx.DigestAuth | tuple[str, str] | None = None + if username and password: + if entry.options.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: + auth = httpx.DigestAuth(username, password) + else: + auth = (username, password) + + rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl) + await rest.async_update() + + if rest.data is None: + raise ConfigEntryNotReady + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = rest + + entry.async_on_unload(entry.add_update_listener(async_update_listener)) + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener for options.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Scrape config entry.""" + + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/scrape/config_flow.py b/homeassistant/components/scrape/config_flow.py new file mode 100644 index 00000000000..a32e371a487 --- /dev/null +++ b/homeassistant/components/scrape/config_flow.py @@ -0,0 +1,122 @@ +"""Adds config flow for Scrape integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +import voluptuous as vol + +from homeassistant.components.sensor import ( + CONF_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import ( + CONF_ATTRIBUTE, + CONF_AUTHENTICATION, + CONF_DEVICE_CLASS, + CONF_HEADERS, + CONF_NAME, + CONF_PASSWORD, + CONF_RESOURCE, + CONF_UNIT_OF_MEASUREMENT, + CONF_USERNAME, + CONF_VALUE_TEMPLATE, + CONF_VERIFY_SSL, + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, +) +from homeassistant.helpers.schema_config_entry_flow import ( + SchemaConfigFlowHandler, + SchemaFlowFormStep, + SchemaFlowMenuStep, + SchemaOptionsFlowHandler, +) +from homeassistant.helpers.selector import ( + BooleanSelector, + NumberSelector, + NumberSelectorConfig, + NumberSelectorMode, + ObjectSelector, + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, + TemplateSelector, + TextSelector, + TextSelectorConfig, + TextSelectorType, +) + +from .const import CONF_INDEX, CONF_SELECT, DEFAULT_NAME, DEFAULT_VERIFY_SSL, DOMAIN + +SCHEMA_SETUP = { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): TextSelector(), + vol.Required(CONF_RESOURCE): TextSelector( + TextSelectorConfig(type=TextSelectorType.URL) + ), + vol.Required(CONF_SELECT): TextSelector(), +} + +SCHEMA_OPT = { + vol.Optional(CONF_ATTRIBUTE): TextSelector(), + vol.Optional(CONF_INDEX, default=0): NumberSelector( + NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX) + ), + vol.Optional(CONF_AUTHENTICATION): SelectSelector( + SelectSelectorConfig( + options=[HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION], + mode=SelectSelectorMode.DROPDOWN, + ) + ), + vol.Optional(CONF_USERNAME): TextSelector(), + vol.Optional(CONF_PASSWORD): TextSelector( + TextSelectorConfig(type=TextSelectorType.PASSWORD) + ), + vol.Optional(CONF_HEADERS): ObjectSelector(), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): TextSelector(), + vol.Optional(CONF_DEVICE_CLASS): SelectSelector( + SelectSelectorConfig( + options=[e.value for e in SensorDeviceClass], + mode=SelectSelectorMode.DROPDOWN, + ) + ), + vol.Optional(CONF_STATE_CLASS): SelectSelector( + SelectSelectorConfig( + options=[e.value for e in SensorStateClass], + mode=SelectSelectorMode.DROPDOWN, + ) + ), + vol.Optional(CONF_VALUE_TEMPLATE): TemplateSelector(), + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): BooleanSelector(), +} + +DATA_SCHEMA = vol.Schema({**SCHEMA_SETUP, **SCHEMA_OPT}) +DATA_SCHEMA_OPT = vol.Schema({**SCHEMA_OPT}) + +CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "user": SchemaFlowFormStep(DATA_SCHEMA), + "import": SchemaFlowFormStep(DATA_SCHEMA), +} +OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = { + "init": SchemaFlowFormStep(DATA_SCHEMA_OPT), +} + + +class ScrapeConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): + """Handle a config flow for Scrape.""" + + config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + def async_config_entry_title(self, options: Mapping[str, Any]) -> str: + """Return config entry title.""" + return options[CONF_NAME] + + def async_config_flow_finished(self, options: Mapping[str, Any]) -> None: + """Check for duplicate records.""" + data: dict[str, Any] = dict(options) + self._async_abort_entries_match(data) + + +class ScrapeOptionsFlowHandler(SchemaOptionsFlowHandler): + """Handle a config flow for Scrape.""" diff --git a/homeassistant/components/scrape/const.py b/homeassistant/components/scrape/const.py new file mode 100644 index 00000000000..88eb661d29a --- /dev/null +++ b/homeassistant/components/scrape/const.py @@ -0,0 +1,13 @@ +"""Constants for Scrape integration.""" +from __future__ import annotations + +from homeassistant.const import Platform + +DOMAIN = "scrape" +DEFAULT_NAME = "Web scrape" +DEFAULT_VERIFY_SSL = True + +PLATFORMS = [Platform.SENSOR] + +CONF_SELECT = "select" +CONF_INDEX = "index" diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index b1ccbb354a9..631af2e6051 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -4,6 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/scrape", "requirements": ["beautifulsoup4==4.11.1", "lxml==4.8.0"], "after_dependencies": ["rest"], - "codeowners": ["@fabaff"], + "codeowners": ["@fabaff", "@gjohansson-ST"], + "config_flow": true, "iot_class": "cloud_polling" } diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index e15f7c5ba97..4fc08cba571 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -5,7 +5,6 @@ import logging from typing import Any from bs4 import BeautifulSoup -import httpx import voluptuous as vol from homeassistant.components.rest.data import RestData @@ -16,7 +15,9 @@ from homeassistant.components.sensor import ( STATE_CLASSES_SCHEMA, SensorEntity, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + CONF_ATTRIBUTE, CONF_AUTHENTICATION, CONF_DEVICE_CLASS, CONF_HEADERS, @@ -31,26 +32,24 @@ from homeassistant.const import ( HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import CONF_INDEX, CONF_SELECT, DEFAULT_NAME, DEFAULT_VERIFY_SSL, DOMAIN + _LOGGER = logging.getLogger(__name__) -CONF_ATTR = "attribute" -CONF_SELECT = "select" -CONF_INDEX = "index" - -DEFAULT_NAME = "Web scrape" -DEFAULT_VERIFY_SSL = True +ICON = "mdi:web" PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCE): cv.string, vol.Required(CONF_SELECT): cv.string, - vol.Optional(CONF_ATTR): cv.string, + vol.Optional(CONF_ATTRIBUTE): cv.string, vol.Optional(CONF_INDEX, default=0): cv.positive_int, vol.Optional(CONF_AUTHENTICATION): vol.In( [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] @@ -62,7 +61,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( 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_VALUE_TEMPLATE): cv.string, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, } ) @@ -75,37 +74,47 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Web scrape sensor.""" - name: str = config[CONF_NAME] - resource: str = config[CONF_RESOURCE] - method: str = "GET" - payload: str | None = None - headers: str | None = config.get(CONF_HEADERS) - verify_ssl: bool = config[CONF_VERIFY_SSL] - select: str | None = config.get(CONF_SELECT) - attr: str | None = config.get(CONF_ATTR) - index: int = config[CONF_INDEX] - unit: str | None = config.get(CONF_UNIT_OF_MEASUREMENT) - device_class: str | None = config.get(CONF_DEVICE_CLASS) - state_class: str | None = config.get(CONF_STATE_CLASS) - username: str | None = config.get(CONF_USERNAME) - password: str | None = config.get(CONF_PASSWORD) - value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) + _LOGGER.warning( + # Config flow added in Home Assistant Core 2022.7, remove import flow in 2022.9 + "Loading Scrape via platform setup has been deprecated in Home Assistant 2022.7 " + "Your configuration has been automatically imported and you can " + "remove it from your configuration.yaml" + ) + if config.get(CONF_VALUE_TEMPLATE): + template: Template = Template(config[CONF_VALUE_TEMPLATE]) + template.ensure_valid() + config[CONF_VALUE_TEMPLATE] = template.template + + 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 Scrape sensor entry.""" + name: str = entry.options[CONF_NAME] + resource: str = entry.options[CONF_RESOURCE] + select: str | None = entry.options.get(CONF_SELECT) + attr: str | None = entry.options.get(CONF_ATTRIBUTE) + index: int = int(entry.options[CONF_INDEX]) + unit: str | None = entry.options.get(CONF_UNIT_OF_MEASUREMENT) + device_class: str | None = entry.options.get(CONF_DEVICE_CLASS) + state_class: str | None = entry.options.get(CONF_STATE_CLASS) + value_template: str | None = entry.options.get(CONF_VALUE_TEMPLATE) + entry_id: str = entry.entry_id + + val_template: Template | None = None if value_template is not None: - value_template.hass = hass + val_template = Template(value_template, hass) - auth: httpx.DigestAuth | tuple[str, str] | None = None - if username and password: - if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: - auth = httpx.DigestAuth(username, password) - else: - auth = (username, password) - - rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl) - await rest.async_update() - - if rest.data is None: - raise PlatformNotReady + rest = hass.data.setdefault(DOMAIN, {})[entry.entry_id] async_add_entities( [ @@ -115,10 +124,12 @@ async def async_setup_platform( select, attr, index, - value_template, + val_template, unit, device_class, state_class, + entry_id, + resource, ) ], True, @@ -128,6 +139,8 @@ async def async_setup_platform( class ScrapeSensor(SensorEntity): """Representation of a web scrape sensor.""" + _attr_icon = ICON + def __init__( self, rest: RestData, @@ -139,6 +152,8 @@ class ScrapeSensor(SensorEntity): unit: str | None, device_class: str | None, state_class: str | None, + entry_id: str, + resource: str, ) -> None: """Initialize a web scrape sensor.""" self.rest = rest @@ -151,6 +166,14 @@ class ScrapeSensor(SensorEntity): self._attr_native_unit_of_measurement = unit self._attr_device_class = device_class self._attr_state_class = state_class + self._attr_unique_id = entry_id + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, entry_id)}, + manufacturer="Scrape", + name=name, + configuration_url=resource, + ) def _extract_value(self) -> Any: """Parse the html extraction in the executor.""" diff --git a/homeassistant/components/scrape/strings.json b/homeassistant/components/scrape/strings.json new file mode 100644 index 00000000000..f328423f5b6 --- /dev/null +++ b/homeassistant/components/scrape/strings.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "step": { + "user": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "resource": "Resource", + "select": "Select", + "attribute": "Attribute", + "index": "Index", + "value_template": "Value Template", + "unit_of_measurement": "Unit of Measurement", + "device_class": "Device Class", + "state_class": "State Class", + "authentication": "Authentication", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "headers": "Headers" + }, + "data_description": { + "resource": "The URL to the website that contains the value", + "select": "Defines what tag to search for. Check Beautifulsoup CSS selectors for details", + "attribute": "Get value of an attribute on the selected tag", + "index": "Defines which of the elements returned by the CSS selector to use", + "value_template": "Defines a template to get the state of the sensor", + "device_class": "The type/class of the sensor to set the icon in the frontend", + "state_class": "The state_class of the sensor", + "authentication": "Type of the HTTP authentication. Either basic or digest", + "verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed", + "headers": "Headers to use for the web request" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "[%key:component::scrape::config::step::user::data::name%]", + "resource": "[%key:component::scrape::config::step::user::data::resource%]", + "select": "[%key:component::scrape::config::step::user::data::select%]", + "attribute": "[%key:component::scrape::config::step::user::data::attribute%]", + "index": "[%key:component::scrape::config::step::user::data::index%]", + "value_template": "[%key:component::scrape::config::step::user::data::value_template%]", + "unit_of_measurement": "[%key:component::scrape::config::step::user::data::unit_of_measurement%]", + "device_class": "[%key:component::scrape::config::step::user::data::device_class%]", + "state_class": "[%key:component::scrape::config::step::user::data::state_class%]", + "authentication": "[%key:component::scrape::config::step::user::data::authentication%]", + "verify_ssl": "[%key:component::scrape::config::step::user::data::verify_ssl%]", + "username": "[%key:component::scrape::config::step::user::data::username%]", + "password": "[%key:component::scrape::config::step::user::data::password%]", + "headers": "[%key:component::scrape::config::step::user::data::headers%]" + }, + "data_description": { + "resource": "[%key:component::scrape::config::step::user::data_description::resource%]", + "select": "[%key:component::scrape::config::step::user::data_description::select%]", + "attribute": "[%key:component::scrape::config::step::user::data_description::attribute%]", + "index": "[%key:component::scrape::config::step::user::data_description::index%]", + "value_template": "[%key:component::scrape::config::step::user::data_description::value_template%]", + "device_class": "[%key:component::scrape::config::step::user::data_description::device_class%]", + "state_class": "[%key:component::scrape::config::step::user::data_description::state_class%]", + "authentication": "[%key:component::scrape::config::step::user::data_description::authentication%]", + "verify_ssl": "[%key:component::scrape::config::step::user::data_description::verify_ssl%]", + "headers": "[%key:component::scrape::config::step::user::data_description::headers%]" + } + } + } + } +} diff --git a/homeassistant/components/scrape/translations/en.json b/homeassistant/components/scrape/translations/en.json new file mode 100644 index 00000000000..20831f5251a --- /dev/null +++ b/homeassistant/components/scrape/translations/en.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "step": { + "user": { + "data": { + "attribute": "Attribute", + "authentication": "Authentication", + "device_class": "Device Class", + "headers": "Headers", + "index": "Index", + "name": "Name", + "password": "Password", + "resource": "Resource", + "select": "Select", + "state_class": "State Class", + "unit_of_measurement": "Unit of Measurement", + "username": "Username", + "value_template": "Value Template", + "verify_ssl": "Verify SSL certificate" + }, + "data_description": { + "attribute": "Get value of an attribute on the selected tag", + "authentication": "Type of the HTTP authentication. Either basic or digest", + "device_class": "The type/class of the sensor to set the icon in the frontend", + "headers": "Headers to use for the web request", + "index": "Defines which of the elements returned by the CSS selector to use", + "resource": "The URL to the website that contains the value", + "select": "Defines what tag to search for. Check Beautifulsoup CSS selectors for details", + "state_class": "The state_class of the sensor", + "value_template": "Defines a template to get the state of the sensor", + "verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "attribute": "Attribute", + "authentication": "Authentication", + "device_class": "Device Class", + "headers": "Headers", + "index": "Index", + "name": "Name", + "password": "Password", + "resource": "Resource", + "select": "Select", + "state_class": "State Class", + "unit_of_measurement": "Unit of Measurement", + "username": "Username", + "value_template": "Value Template", + "verify_ssl": "Verify SSL certificate" + }, + "data_description": { + "attribute": "Get value of an attribute on the selected tag", + "authentication": "Type of the HTTP authentication. Either basic or digest", + "device_class": "The type/class of the sensor to set the icon in the frontend", + "headers": "Headers to use for the web request", + "index": "Defines which of the elements returned by the CSS selector to use", + "resource": "The URL to the website that contains the value", + "select": "Defines what tag to search for. Check Beautifulsoup CSS selectors for details", + "state_class": "The state_class of the sensor", + "value_template": "Defines a template to get the state of the sensor", + "verify_ssl": "Enables/disables verification of SSL/TLS certificate, for example if it is self-signed" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e9ba5971e07..e1e2938c9ff 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -298,6 +298,7 @@ FLOWS = { "ruckus_unleashed", "sabnzbd", "samsungtv", + "scrape", "screenlogic", "season", "sense", diff --git a/tests/components/scrape/__init__.py b/tests/components/scrape/__init__.py index 0ba9266a79d..37abb061e75 100644 --- a/tests/components/scrape/__init__.py +++ b/tests/components/scrape/__init__.py @@ -2,6 +2,42 @@ from __future__ import annotations from typing import Any +from unittest.mock import patch + +from homeassistant.components.scrape.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def init_integration( + hass: HomeAssistant, + config: dict[str, Any], + data: str, + entry_id: str = "1", + source: str = SOURCE_USER, +) -> MockConfigEntry: + """Set up the Scrape integration in Home Assistant.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + source=source, + data={}, + options=config, + entry_id=entry_id, + ) + + config_entry.add_to_hass(hass) + mocker = MockRestData(data) + with patch( + "homeassistant.components.scrape.RestData", + return_value=mocker, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry def return_config( @@ -25,6 +61,8 @@ def return_config( "resource": "https://www.home-assistant.io", "select": select, "name": name, + "index": 0, + "verify_ssl": True, } if attribute: config["attribute"] = attribute @@ -38,7 +76,7 @@ def return_config( config["device_class"] = device_class if state_class: config["state_class"] = state_class - if authentication: + if username: config["authentication"] = authentication config["username"] = username config["password"] = password diff --git a/tests/components/scrape/test_config_flow.py b/tests/components/scrape/test_config_flow.py new file mode 100644 index 00000000000..287004b1dd3 --- /dev/null +++ b/tests/components/scrape/test_config_flow.py @@ -0,0 +1,194 @@ +"""Test the Scrape config flow.""" +from __future__ import annotations + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.scrape.const import CONF_INDEX, CONF_SELECT, DOMAIN +from homeassistant.const import ( + CONF_NAME, + CONF_RESOURCE, + CONF_VALUE_TEMPLATE, + CONF_VERIFY_SSL, +) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import MockRestData + +from tests.common import MockConfigEntry + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=MockRestData("test_scrape_sensor"), + ), patch( + "homeassistant.components.scrape.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_NAME: "Release", + CONF_SELECT: ".current-version h1", + CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Release" + assert result2["options"] == { + "resource": "https://www.home-assistant.io", + "name": "Release", + "select": ".current-version h1", + "value_template": "{{ value.split(':')[1] }}", + "index": 0.0, + "verify_ssl": True, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_flow_success(hass: HomeAssistant) -> None: + """Test a successful import of yaml.""" + + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=MockRestData("test_scrape_sensor"), + ), patch( + "homeassistant.components.scrape.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_NAME: "Release", + CONF_SELECT: ".current-version h1", + CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}", + CONF_INDEX: 0, + CONF_VERIFY_SSL: True, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "Release" + assert result2["options"] == { + "resource": "https://www.home-assistant.io", + "name": "Release", + "select": ".current-version h1", + "value_template": "{{ value.split(':')[1] }}", + "index": 0, + "verify_ssl": True, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_flow_already_exist(hass: HomeAssistant) -> None: + """Test import of yaml already exist.""" + + MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + "resource": "https://www.home-assistant.io", + "name": "Release", + "select": ".current-version h1", + "value_template": "{{ value.split(':')[1] }}", + "index": 0, + "verify_ssl": True, + }, + ).add_to_hass(hass) + + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=MockRestData("test_scrape_sensor"), + ), patch( + "homeassistant.components.scrape.async_setup_entry", + return_value=True, + ): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_RESOURCE: "https://www.home-assistant.io", + CONF_NAME: "Release", + CONF_SELECT: ".current-version h1", + CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}", + CONF_INDEX: 0, + CONF_VERIFY_SSL: True, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "already_configured" + + +async def test_options_form(hass: HomeAssistant) -> None: + """Test we get the form in options.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={ + "resource": "https://www.home-assistant.io", + "name": "Release", + "select": ".current-version h1", + "value_template": "{{ value.split(':')[1] }}", + "index": 0, + "verify_ssl": True, + }, + entry_id="1", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.scrape.RestData", + return_value=MockRestData("test_scrape_sensor"), + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + + with patch( + "homeassistant.components.scrape.RestData", + return_value=MockRestData("test_scrape_sensor"), + ): + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "value_template": "{{ value.split(':')[1] }}", + "index": 1.0, + "verify_ssl": True, + }, + ) + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == { + "resource": "https://www.home-assistant.io", + "name": "Release", + "select": ".current-version h1", + "value_template": "{{ value.split(':')[1] }}", + "index": 1.0, + "verify_ssl": True, + } + entry_check = hass.config_entries.async_get_entry("1") + assert entry_check.state == config_entries.ConfigEntryState.LOADED + assert entry_check.update_listeners is not None diff --git a/tests/components/scrape/test_init.py b/tests/components/scrape/test_init.py new file mode 100644 index 00000000000..021790e65c3 --- /dev/null +++ b/tests/components/scrape/test_init.py @@ -0,0 +1,89 @@ +"""Test Scrape component setup process.""" +from __future__ import annotations + +from unittest.mock import patch + +from homeassistant.components.scrape.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import MockRestData + +from tests.common import MockConfigEntry + +TEST_CONFIG = { + "resource": "https://www.home-assistant.io", + "name": "Release", + "select": ".current-version h1", + "value_template": "{{ value.split(':')[1] }}", + "index": 0, + "verify_ssl": True, +} + + +async def test_setup_entry(hass: HomeAssistant) -> None: + """Test setup entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options=TEST_CONFIG, + title="Release", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.scrape.RestData", + return_value=MockRestData("test_scrape_sensor"), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.release") + assert state + + +async def test_setup_entry_no_data_fails(hass: HomeAssistant) -> None: + """Test setup entry no data fails.""" + entry = MockConfigEntry( + domain=DOMAIN, data={}, options=TEST_CONFIG, title="Release", entry_id="1" + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.scrape.RestData", + return_value=MockRestData("test_scrape_sensor_no_data"), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.ha_version") + assert state is None + entry = hass.config_entries.async_get_entry("1") + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_remove_entry(hass: HomeAssistant) -> None: + """Test remove entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options=TEST_CONFIG, + title="Release", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.scrape.RestData", + return_value=MockRestData("test_scrape_sensor"), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.release") + assert state + + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.release") + assert not state diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index aaf156208ef..cd4e27e88a2 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -3,10 +3,15 @@ from __future__ import annotations from unittest.mock import patch +import pytest + from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.sensor.const import CONF_STATE_CLASS +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_DEVICE_CLASS, + CONF_NAME, + CONF_RESOURCE, CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, @@ -15,22 +20,20 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component -from . import MockRestData, return_config +from . import MockRestData, init_integration, return_config + +from tests.common import MockConfigEntry DOMAIN = "scrape" async def test_scrape_sensor(hass: HomeAssistant) -> None: """Test Scrape sensor minimal.""" - config = {"sensor": return_config(select=".current-version h1", name="HA version")} - - mocker = MockRestData("test_scrape_sensor") - with patch( - "homeassistant.components.scrape.sensor.RestData", - return_value=mocker, - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() + await init_integration( + hass, + return_config(select=".current-version h1", name="HA version"), + "test_scrape_sensor", + ) state = hass.states.get("sensor.ha_version") assert state.state == "Current Version: 2021.12.10" @@ -38,21 +41,15 @@ async def test_scrape_sensor(hass: HomeAssistant) -> None: async def test_scrape_sensor_value_template(hass: HomeAssistant) -> None: """Test Scrape sensor with value template.""" - config = { - "sensor": return_config( + await init_integration( + hass, + return_config( select=".current-version h1", name="HA version", template="{{ value.split(':')[1] }}", - ) - } - - mocker = MockRestData("test_scrape_sensor") - with patch( - "homeassistant.components.scrape.sensor.RestData", - return_value=mocker, - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() + ), + "test_scrape_sensor", + ) state = hass.states.get("sensor.ha_version") assert state.state == "2021.12.10" @@ -60,24 +57,18 @@ async def test_scrape_sensor_value_template(hass: HomeAssistant) -> None: async def test_scrape_uom_and_classes(hass: HomeAssistant) -> None: """Test Scrape sensor for unit of measurement, device class and state class.""" - config = { - "sensor": return_config( + await init_integration( + hass, + return_config( select=".current-temp h3", name="Current Temp", template="{{ value.split(':')[1] }}", uom="°C", device_class="temperature", state_class="measurement", - ) - } - - mocker = MockRestData("test_scrape_uom_and_classes") - with patch( - "homeassistant.components.scrape.sensor.RestData", - return_value=mocker, - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() + ), + "test_scrape_uom_and_classes", + ) state = hass.states.get("sensor.current_temp") assert state.state == "22.1" @@ -88,31 +79,28 @@ async def test_scrape_uom_and_classes(hass: HomeAssistant) -> None: async def test_scrape_sensor_authentication(hass: HomeAssistant) -> None: """Test Scrape sensor with authentication.""" - config = { - "sensor": [ - return_config( - select=".return", - name="Auth page", - username="user@secret.com", - password="12345678", - authentication="digest", - ), - return_config( - select=".return", - name="Auth page2", - username="user@secret.com", - password="12345678", - ), - ] - } - - mocker = MockRestData("test_scrape_sensor_authentication") - with patch( - "homeassistant.components.scrape.sensor.RestData", - return_value=mocker, - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() + await init_integration( + hass, + return_config( + select=".return", + name="Auth page", + username="user@secret.com", + password="12345678", + authentication="digest", + ), + "test_scrape_sensor_authentication", + ) + await init_integration( + hass, + return_config( + select=".return", + name="Auth page2", + username="user@secret.com", + password="12345678", + ), + "test_scrape_sensor_authentication", + entry_id="2", + ) state = hass.states.get("sensor.auth_page") assert state.state == "secret text" @@ -122,15 +110,11 @@ async def test_scrape_sensor_authentication(hass: HomeAssistant) -> None: async def test_scrape_sensor_no_data(hass: HomeAssistant) -> None: """Test Scrape sensor fails on no data.""" - config = {"sensor": return_config(select=".current-version h1", name="HA version")} - - mocker = MockRestData("test_scrape_sensor_no_data") - with patch( - "homeassistant.components.scrape.sensor.RestData", - return_value=mocker, - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() + await init_integration( + hass, + return_config(select=".current-version h1", name="HA version"), + "test_scrape_sensor_no_data", + ) state = hass.states.get("sensor.ha_version") assert state is None @@ -138,14 +122,21 @@ async def test_scrape_sensor_no_data(hass: HomeAssistant) -> None: async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: """Test Scrape sensor no data on refresh.""" - config = {"sensor": return_config(select=".current-version h1", name="HA version")} + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={}, + options=return_config(select=".current-version h1", name="HA version"), + entry_id="1", + ) + config_entry.add_to_hass(hass) mocker = MockRestData("test_scrape_sensor") with patch( - "homeassistant.components.scrape.sensor.RestData", + "homeassistant.components.scrape.RestData", return_value=mocker, ): - assert await async_setup_component(hass, "sensor", config) + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") @@ -162,20 +153,17 @@ async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: async def test_scrape_sensor_attribute_and_tag(hass: HomeAssistant) -> None: """Test Scrape sensor with attribute and tag.""" - config = { - "sensor": [ - return_config(select="div", name="HA class", index=1, attribute="class"), - return_config(select="template", name="HA template"), - ] - } - - mocker = MockRestData("test_scrape_sensor") - with patch( - "homeassistant.components.scrape.sensor.RestData", - return_value=mocker, - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() + await init_integration( + hass, + return_config(select="div", name="HA class", index=1, attribute="class"), + "test_scrape_sensor", + ) + await init_integration( + hass, + return_config(select="template", name="HA template"), + "test_scrape_sensor", + entry_id="2", + ) state = hass.states.get("sensor.ha_class") assert state.state == "['links']" @@ -185,22 +173,55 @@ async def test_scrape_sensor_attribute_and_tag(hass: HomeAssistant) -> None: async def test_scrape_sensor_errors(hass: HomeAssistant) -> None: """Test Scrape sensor handle errors.""" - config = { - "sensor": [ - return_config(select="div", name="HA class", index=5, attribute="class"), - return_config(select="div", name="HA class2", attribute="classes"), - ] - } - - mocker = MockRestData("test_scrape_sensor") - with patch( - "homeassistant.components.scrape.sensor.RestData", - return_value=mocker, - ): - assert await async_setup_component(hass, "sensor", config) - await hass.async_block_till_done() + await init_integration( + hass, + return_config(select="div", name="HA class", index=5, attribute="class"), + "test_scrape_sensor", + ) + await init_integration( + hass, + return_config(select="div", name="HA class2", attribute="classes"), + "test_scrape_sensor", + entry_id="2", + ) state = hass.states.get("sensor.ha_class") assert state.state == STATE_UNKNOWN state2 = hass.states.get("sensor.ha_class2") assert state2.state == STATE_UNKNOWN + + +async def test_import(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None: + """Test the Scrape sensor import.""" + config = { + "sensor": { + "platform": "scrape", + "resource": "https://www.home-assistant.io", + "select": ".current-version h1", + "name": "HA Version", + "index": 0, + "verify_ssl": True, + "value_template": "{{ value.split(':')[1] }}", + } + } + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.scrape.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + assert ( + "Loading Scrape via platform setup has been deprecated in Home Assistant" + in caplog.text + ) + + assert hass.config_entries.async_entries(DOMAIN) + options = hass.config_entries.async_entries(DOMAIN)[0].options + assert options[CONF_NAME] == "HA Version" + assert options[CONF_RESOURCE] == "https://www.home-assistant.io" + + state = hass.states.get("sensor.ha_version") + assert state.state == "2021.12.10" From 91df2db9e012d81b382623f171caddde24dd73f0 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Fri, 3 Jun 2022 21:59:10 +0200 Subject: [PATCH 1231/3516] Bump bimmer_connected to 0.9.4 (#72973) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 75ac3e982e8..cd6daa83705 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.9.3"], + "requirements": ["bimmer_connected==0.9.4"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index bb8f3f1cf9b..7d736fb0d16 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -394,7 +394,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.3 +bimmer_connected==0.9.4 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a750e708354..2b4454dd459 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -309,7 +309,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.3 +bimmer_connected==0.9.4 # homeassistant.components.blebox blebox_uniapi==1.3.3 From 8e8fa0399e1d4b799c6978d381a2e187624deff4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Jun 2022 10:04:46 -1000 Subject: [PATCH 1232/3516] Fix statistics_during_period being incorrectly cached (#72947) --- .../components/recorder/statistics.py | 7 +- tests/components/history/test_init.py | 136 ++++++++++++++++++ 2 files changed, 138 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 4bed39fee4a..39fcb954ee9 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -984,7 +984,6 @@ def _reduce_statistics_per_month( def _statistics_during_period_stmt( start_time: datetime, end_time: datetime | None, - statistic_ids: list[str] | None, metadata_ids: list[int] | None, table: type[Statistics | StatisticsShortTerm], ) -> StatementLambdaElement: @@ -1002,7 +1001,7 @@ def _statistics_during_period_stmt( if end_time is not None: stmt += lambda q: q.filter(table.start < end_time) - if statistic_ids is not None: + if metadata_ids: stmt += lambda q: q.filter(table.metadata_id.in_(metadata_ids)) stmt += lambda q: q.order_by(table.metadata_id, table.start) @@ -1038,9 +1037,7 @@ def statistics_during_period( else: table = Statistics - stmt = _statistics_during_period_stmt( - start_time, end_time, statistic_ids, metadata_ids, table - ) + stmt = _statistics_during_period_stmt(start_time, end_time, metadata_ids, table) stats = execute_stmt_lambda_element(session, stmt) if not stats: diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 9dc7af59a38..8c0a80719a8 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -5,6 +5,7 @@ from http import HTTPStatus import json from unittest.mock import patch, sentinel +from freezegun import freeze_time import pytest from pytest import approx @@ -928,6 +929,141 @@ async def test_statistics_during_period( } +@pytest.mark.parametrize( + "units, attributes, state, value", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), + (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 50), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 10), + (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 14.503774389728312), + (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 100000), + ], +) +async def test_statistics_during_period_in_the_past( + hass, hass_ws_client, recorder_mock, units, attributes, state, value +): + """Test statistics_during_period in the past.""" + hass.config.set_time_zone("UTC") + now = dt_util.utcnow().replace() + + hass.config.units = units + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + + past = now - timedelta(days=3) + + with freeze_time(past): + hass.states.async_set("sensor.test", state, attributes=attributes) + await async_wait_recording_done(hass) + + sensor_state = hass.states.get("sensor.test") + assert sensor_state.last_updated == past + + stats_top_of_hour = past.replace(minute=0, second=0, microsecond=0) + stats_start = past.replace(minute=55) + do_adhoc_statistics(hass, start=stats_start) + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "hour", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + await client.send_json( + { + "id": 2, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + past = now - timedelta(days=3) + await client.send_json( + { + "id": 3, + "type": "history/statistics_during_period", + "start_time": past.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "sensor.test": [ + { + "statistic_id": "sensor.test", + "start": stats_start.isoformat(), + "end": (stats_start + timedelta(minutes=5)).isoformat(), + "mean": approx(value), + "min": approx(value), + "max": approx(value), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + start_of_day = stats_top_of_hour.replace(hour=0, minute=0) + await client.send_json( + { + "id": 4, + "type": "history/statistics_during_period", + "start_time": stats_top_of_hour.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "day", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "sensor.test": [ + { + "statistic_id": "sensor.test", + "start": start_of_day.isoformat(), + "end": (start_of_day + timedelta(days=1)).isoformat(), + "mean": approx(value), + "min": approx(value), + "max": approx(value), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + await client.send_json( + { + "id": 5, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + async def test_statistics_during_period_bad_start_time( hass, hass_ws_client, recorder_mock ): From cc807b4d597daaaadc92df4a93c6e30da4f570c6 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 3 Jun 2022 22:05:37 +0200 Subject: [PATCH 1233/3516] fjaraskupan: Don't filter anything in backend (#72988) --- homeassistant/components/fjaraskupan/__init__.py | 4 ++-- homeassistant/components/fjaraskupan/config_flow.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index ec4528bc079..4c4f19403a6 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_NAME, Device, State, device_filter +from fjaraskupan import Device, State, device_filter from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -90,7 +90,7 @@ class EntryState: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Fjäråskupan from a config entry.""" - scanner = BleakScanner(filters={"Pattern": DEVICE_NAME, "DuplicateData": True}) + scanner = BleakScanner(filters={"DuplicateData": True}) state = EntryState(scanner, {}) hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/fjaraskupan/config_flow.py b/homeassistant/components/fjaraskupan/config_flow.py index 3af34c0eef6..ffac366500b 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_NAME, device_filter +from fjaraskupan import device_filter from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_flow import register_discovery_flow @@ -28,7 +28,7 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: async with BleakScanner( detection_callback=detection, - filters={"Pattern": DEVICE_NAME, "DuplicateData": True}, + filters={"DuplicateData": True}, ): try: async with async_timeout.timeout(CONST_WAIT_TIME): From 14030991cfe8e3aef96aaaa7cf9200ea92fb0b63 Mon Sep 17 00:00:00 2001 From: iAutom8 <20862130+iAutom8@users.noreply.github.com> Date: Fri, 3 Jun 2022 14:57:01 -0700 Subject: [PATCH 1234/3516] Add ViCare additional temperature sensors (#72792) --- homeassistant/components/vicare/sensor.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 60a39b454a2..06ed618ec86 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -77,6 +77,22 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), + ViCareSensorEntityDescription( + key="boiler_supply_temperature", + name="Boiler Supply Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + value_getter=lambda api: api.getBoilerCommonSupplyTemperature(), + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + ViCareSensorEntityDescription( + key="hotwater_out_temperature", + name="Hot Water Out Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + value_getter=lambda api: api.getDomesticHotWaterOutletTemperature(), + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), ViCareSensorEntityDescription( key="hotwater_gas_consumption_today", name="Hot water gas consumption today", From 04b2223f06d9ec0e7b2930d54ec2b02f15de782c Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 3 Jun 2022 17:03:21 -0500 Subject: [PATCH 1235/3516] Provide Sonos media position if duration not available (#73001) --- homeassistant/components/sonos/media.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/media.py b/homeassistant/components/sonos/media.py index e3d8f043d4b..9608356ba64 100644 --- a/homeassistant/components/sonos/media.py +++ b/homeassistant/components/sonos/media.py @@ -205,13 +205,15 @@ class SonosMedia: self, position_info: dict[str, int], force_update: bool = False ) -> None: """Update state when playing music tracks.""" - if (duration := position_info.get(DURATION_SECONDS)) == 0: + duration = position_info.get(DURATION_SECONDS) + current_position = position_info.get(POSITION_SECONDS) + + if not (duration or current_position): self.clear_position() return should_update = force_update self.duration = duration - current_position = position_info.get(POSITION_SECONDS) # player started reporting position? if current_position is not None and self.position is None: From bdc41bf22a368174380f07b0c82fd6ae8c690463 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 3 Jun 2022 16:33:12 -0700 Subject: [PATCH 1236/3516] Fix google calendar bug where expired tokens are not refreshed (#72994) --- homeassistant/components/google/api.py | 7 +++++-- tests/components/google/conftest.py | 6 ++++-- tests/components/google/test_config_flow.py | 14 ++++++++------ 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index 4bb9de5d581..a4cda1ff41a 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -5,7 +5,6 @@ from __future__ import annotations from collections.abc import Awaitable, Callable import datetime import logging -import time from typing import Any, cast import aiohttp @@ -50,12 +49,16 @@ class DeviceAuth(AuthImplementation): async def async_resolve_external_data(self, external_data: Any) -> dict: """Resolve a Google API Credentials object to Home Assistant token.""" creds: Credentials = external_data[DEVICE_AUTH_CREDS] + delta = creds.token_expiry.replace(tzinfo=datetime.timezone.utc) - dt.utcnow() + _LOGGER.debug( + "Token expires at %s (in %s)", creds.token_expiry, delta.total_seconds() + ) return { "access_token": creds.access_token, "refresh_token": creds.refresh_token, "scope": " ".join(creds.scopes), "token_type": "Bearer", - "expires_in": creds.token_expiry.timestamp() - time.time(), + "expires_in": delta.total_seconds(), } diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index b5566450913..68176493445 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -16,7 +16,6 @@ from homeassistant.components.google import CONF_TRACK_NEW, DOMAIN from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -136,7 +135,10 @@ def token_scopes() -> list[str]: @pytest.fixture def token_expiry() -> datetime.datetime: """Expiration time for credentials used in the test.""" - return utcnow() + datetime.timedelta(days=7) + # OAuth library returns an offset-naive timestamp + return datetime.datetime.fromtimestamp( + datetime.datetime.utcnow().timestamp() + ) + datetime.timedelta(hours=1) @pytest.fixture diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index 8ac017fcba4..a346b02e6c2 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -8,6 +8,7 @@ from typing import Any from unittest.mock import Mock, patch from aiohttp.client_exceptions import ClientError +from freezegun.api import FrozenDateTimeFactory from oauth2client.client import ( FlowExchangeError, OAuth2Credentials, @@ -94,11 +95,13 @@ async def fire_alarm(hass, point_in_time): await hass.async_block_till_done() +@pytest.mark.freeze_time("2022-06-03 15:19:59-00:00") async def test_full_flow_yaml_creds( hass: HomeAssistant, mock_code_flow: Mock, mock_exchange: Mock, component_setup: ComponentSetup, + freezer: FrozenDateTimeFactory, ) -> None: """Test successful creds setup.""" assert await component_setup() @@ -115,8 +118,8 @@ async def test_full_flow_yaml_creds( "homeassistant.components.google.async_setup_entry", return_value=True ) as mock_setup: # Run one tick to invoke the credential exchange check - now = utcnow() - await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + freezer.tick(CODE_CHECK_ALARM_TIMEDELTA) + await fire_alarm(hass, datetime.datetime.utcnow()) await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"] @@ -127,12 +130,11 @@ async def test_full_flow_yaml_creds( assert "data" in result data = result["data"] assert "token" in data - assert 0 < data["token"]["expires_in"] < 8 * 86400 assert ( - datetime.datetime.now().timestamp() - <= data["token"]["expires_at"] - < (datetime.datetime.now() + datetime.timedelta(days=8)).timestamp() + data["token"]["expires_in"] + == 60 * 60 - CODE_CHECK_ALARM_TIMEDELTA.total_seconds() ) + assert data["token"]["expires_at"] == 1654273199.0 data["token"].pop("expires_at") data["token"].pop("expires_in") assert data == { From 636f650563a81b23409fa17c12e938ed592bd6d6 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 4 Jun 2022 00:23:28 +0000 Subject: [PATCH 1237/3516] [ci skip] Translation update --- .../components/deconz/translations/ca.json | 2 +- .../hvv_departures/translations/ca.json | 2 +- .../lovelace/translations/pt-BR.json | 2 +- .../components/scrape/translations/ca.json | 73 +++++++++++++++++++ .../components/scrape/translations/id.json | 23 ++++++ .../components/scrape/translations/pt-BR.json | 73 +++++++++++++++++++ .../tankerkoenig/translations/hu.json | 8 +- 7 files changed, 179 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/scrape/translations/ca.json create mode 100644 homeassistant/components/scrape/translations/id.json create mode 100644 homeassistant/components/scrape/translations/pt-BR.json diff --git a/homeassistant/components/deconz/translations/ca.json b/homeassistant/components/deconz/translations/ca.json index f4659557503..c9f599429b4 100644 --- a/homeassistant/components/deconz/translations/ca.json +++ b/homeassistant/components/deconz/translations/ca.json @@ -57,7 +57,7 @@ "side_3": "cara 3", "side_4": "cara 4", "side_5": "cara 5", - "side_6": "cara 6", + "side_6": "Cara 6", "top_buttons": "Botons superiors", "turn_off": "Desactiva", "turn_on": "Activa" diff --git a/homeassistant/components/hvv_departures/translations/ca.json b/homeassistant/components/hvv_departures/translations/ca.json index fad60206c1c..05da353f879 100644 --- a/homeassistant/components/hvv_departures/translations/ca.json +++ b/homeassistant/components/hvv_departures/translations/ca.json @@ -36,7 +36,7 @@ "init": { "data": { "filter": "Selecciona l\u00ednies", - "offset": "\u00d2fset (minuts)", + "offset": "Desfasament (minuts)", "real_time": "Utilitza dades en temps real" }, "description": "Canvia les opcions d'aquest sensor de sortides", diff --git a/homeassistant/components/lovelace/translations/pt-BR.json b/homeassistant/components/lovelace/translations/pt-BR.json index 2ff25d17161..dd8cc7cc32d 100644 --- a/homeassistant/components/lovelace/translations/pt-BR.json +++ b/homeassistant/components/lovelace/translations/pt-BR.json @@ -1,7 +1,7 @@ { "system_health": { "info": { - "dashboards": "Pain\u00e9is", + "dashboards": "Dashboards", "mode": "Modo", "resources": "Recursos", "views": "Visualiza\u00e7\u00f5es" diff --git a/homeassistant/components/scrape/translations/ca.json b/homeassistant/components/scrape/translations/ca.json new file mode 100644 index 00000000000..ff6a0dba168 --- /dev/null +++ b/homeassistant/components/scrape/translations/ca.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat" + }, + "step": { + "user": { + "data": { + "attribute": "Atribut", + "authentication": "Autenticaci\u00f3", + "device_class": "Classe de dispositiu", + "headers": "Cap\u00e7aleres", + "index": "\u00cdndex", + "name": "Nom", + "password": "Contrasenya", + "resource": "Recurs", + "select": "Selecciona", + "state_class": "Classe d'estat", + "unit_of_measurement": "Unitat de mesura", + "username": "Nom d'usuari", + "value_template": "Plantilla de valor", + "verify_ssl": "Verifica el certificat SSL" + }, + "data_description": { + "attribute": "Obt\u00e9 el valor d'un atribut de l'etiqueta seleccionada", + "authentication": "Tipus d'autenticaci\u00f3 HTTP. O b\u00e0sica o 'digest'", + "device_class": "Tipus/classe del sensor per configurar-ne la icona a la interf\u00edcie", + "headers": "Cap\u00e7aleres a utilitzar per a la sol\u00b7licitud web", + "index": "Defineix quins dels elements retornats pel selector CSS utilitzar", + "resource": "URL del lloc web que cont\u00e9 el valor", + "select": "Defineix quina etiqueta s'ha de buscar. Consulta els selectors CSS de Beautifulsoup per m\u00e9s informaci\u00f3", + "state_class": "La state_class del sensor", + "value_template": "Defineix una plantilla per obtenir l'estat del sensor", + "verify_ssl": "Activa/desactiva la verificaci\u00f3 del certificat SSL/TLS, per exemple, si est\u00e0 autosignat" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "attribute": "Atribut", + "authentication": "Autenticaci\u00f3", + "device_class": "Classe de dispositiu", + "headers": "Cap\u00e7aleres", + "index": "\u00cdndex", + "name": "Nom", + "password": "Contrasenya", + "resource": "Recurs", + "select": "Selecciona", + "state_class": "Classe d'estat", + "unit_of_measurement": "Unitat de mesura", + "username": "Nom d'usuari", + "value_template": "Plantilla de valor", + "verify_ssl": "Verifica el certificat SSL" + }, + "data_description": { + "attribute": "Obt\u00e9 el valor d'un atribut de l'etiqueta seleccionada", + "authentication": "Tipus d'autenticaci\u00f3 HTTP. O b\u00e0sica o 'digest'", + "device_class": "Tipus/classe del sensor per configurar-ne la icona a la interf\u00edcie", + "headers": "Cap\u00e7aleres a utilitzar per a la sol\u00b7licitud web", + "index": "Defineix quins dels elements retornats pel selector CSS utilitzar", + "resource": "URL del lloc web que cont\u00e9 el valor", + "select": "Defineix quina etiqueta s'ha de buscar. Consulta els selectors CSS de Beautifulsoup per m\u00e9s informaci\u00f3", + "state_class": "La state_class del sensor", + "value_template": "Defineix una plantilla per obtenir l'estat del sensor", + "verify_ssl": "Activa/desactiva la verificaci\u00f3 del certificat SSL/TLS, per exemple, si est\u00e0 autosignat" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/id.json b/homeassistant/components/scrape/translations/id.json new file mode 100644 index 00000000000..d83b07ac5c3 --- /dev/null +++ b/homeassistant/components/scrape/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "step": { + "user": { + "data": { + "resource": "Sumber daya" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "resource": "Sumber daya" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/pt-BR.json b/homeassistant/components/scrape/translations/pt-BR.json new file mode 100644 index 00000000000..24bbfe1fece --- /dev/null +++ b/homeassistant/components/scrape/translations/pt-BR.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 est\u00e1 configurada" + }, + "step": { + "user": { + "data": { + "attribute": "Atributo", + "authentication": "Autentica\u00e7\u00e3o", + "device_class": "Classe do dispositivo", + "headers": "Cabe\u00e7alhos", + "index": "\u00cdndice", + "name": "Nome", + "password": "Senha", + "resource": "Recurso", + "select": "Selecionar", + "state_class": "Classe de estado", + "unit_of_measurement": "Unidade de medida", + "username": "Nome de usu\u00e1rio", + "value_template": "Modelo de valor", + "verify_ssl": "Verificar certificado SSL" + }, + "data_description": { + "attribute": "Obter valor de um atributo na tag selecionada", + "authentication": "Tipo de autentica\u00e7\u00e3o HTTP. b\u00e1sica ou digerida", + "device_class": "O tipo/classe do sensor para definir o \u00edcone na frontend", + "headers": "Cabe\u00e7alhos a serem usados para a solicita\u00e7\u00e3o da web", + "index": "Define qual dos elementos retornados pelo seletor CSS usar", + "resource": "A URL para o site que cont\u00e9m o valor", + "select": "Define qual tag pesquisar. Verifique os seletores CSS da Beautiful Soup para obter detalhes", + "state_class": "O classe de estado do sensor", + "value_template": "Define um modelo para obter o estado do sensor", + "verify_ssl": "Ativa/desativa a verifica\u00e7\u00e3o do certificado SSL/TLS, por exemplo, se for autoassinado" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "attribute": "Atributo", + "authentication": "Autentica\u00e7\u00e3o", + "device_class": "Classe do dispositivo", + "headers": "Cabe\u00e7alhos", + "index": "\u00cdndice", + "name": "Nome", + "password": "Senha", + "resource": "Recurso", + "select": "Selecionar", + "state_class": "Classe de estado", + "unit_of_measurement": "Unidade de medida", + "username": "Nome de usu\u00e1rio", + "value_template": "Modelo de valor", + "verify_ssl": "Verificar SSL" + }, + "data_description": { + "attribute": "Obter valor de um atributo na tag selecionada", + "authentication": "Tipo de autentica\u00e7\u00e3o HTTP. Ou b\u00e1sico ou digerido", + "device_class": "O tipo/classe do sensor para definir o \u00edcone no frontend", + "headers": "Cabe\u00e7alhos a serem usados para a solicita\u00e7\u00e3o da web", + "index": "Define qual dos elementos retornados pelo seletor CSS usar", + "resource": "A URL para o site que cont\u00e9m o valor", + "select": "Define qual tag pesquisar. Verifique os seletores CSS do Beautifulsoup para obter detalhes", + "state_class": "O classe de estado do sensor", + "value_template": "Define um modelo para obter o estado do sensor", + "verify_ssl": "Ativa/desativa a verifica\u00e7\u00e3o do certificado SSL/TLS, por exemplo, se for autoassinado" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/hu.json b/homeassistant/components/tankerkoenig/translations/hu.json index 369f336f96f..502ccd6fd9c 100644 --- a/homeassistant/components/tankerkoenig/translations/hu.json +++ b/homeassistant/components/tankerkoenig/translations/hu.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "no_stations": "Nem tal\u00e1lhat\u00f3 \u00e1llom\u00e1s a hat\u00f3t\u00e1vols\u00e1gon bel\u00fcl." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API kulcs" + } + }, "select_station": { "data": { "stations": "\u00c1llom\u00e1sok" From b5fe4e8474ca93cbae0de83752c8f421898ca80a Mon Sep 17 00:00:00 2001 From: Clifford Roche Date: Sat, 4 Jun 2022 00:56:37 -0400 Subject: [PATCH 1238/3516] Bump greeclimate to 1.2.0 (#73008) --- 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 23a5c654abc..1b2c8dd6a2a 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==1.1.1"], + "requirements": ["greeclimate==1.2.0"], "codeowners": ["@cmroche"], "iot_class": "local_polling", "loggers": ["greeclimate"] diff --git a/requirements_all.txt b/requirements_all.txt index 7d736fb0d16..bad7eac7e6c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -756,7 +756,7 @@ gpiozero==1.6.2 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.1.1 +greeclimate==1.2.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b4454dd459..54028fac054 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -541,7 +541,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.gree -greeclimate==1.1.1 +greeclimate==1.2.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 From 9d933e732b84a8cef7c54dddb9cecbc395ebae20 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 3 Jun 2022 21:56:53 -0700 Subject: [PATCH 1239/3516] Remove google scan_for_calendars service and simplify platform setup (#73010) * Remove google scan_for_calendars service and simplify platform setup * Update invalid calendar yaml test --- homeassistant/components/google/__init__.py | 65 +--------- homeassistant/components/google/calendar.py | 113 +++++++++--------- homeassistant/components/google/const.py | 2 - homeassistant/components/google/services.yaml | 3 - tests/components/google/test_init.py | 68 ++--------- 5 files changed, 67 insertions(+), 184 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 2a40bfe7043..1ddb44e570b 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -1,7 +1,6 @@ """Support for Google - Calendar Event Devices.""" from __future__ import annotations -import asyncio from collections.abc import Mapping from datetime import datetime, timedelta import logging @@ -9,8 +8,7 @@ from typing import Any import aiohttp from gcal_sync.api import GoogleCalendarService -from gcal_sync.exceptions import ApiException -from gcal_sync.model import Calendar, DateOrDatetime, Event +from gcal_sync.model import DateOrDatetime, Event from oauth2client.file import Storage import voluptuous as vol from voluptuous.error import Error as VoluptuousError @@ -31,15 +29,10 @@ from homeassistant.const import ( CONF_OFFSET, ) from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ( - ConfigEntryAuthFailed, - ConfigEntryNotReady, - HomeAssistantError, -) +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.typing import ConfigType @@ -49,7 +42,6 @@ from .const import ( DATA_CONFIG, DATA_SERVICE, DEVICE_AUTH_IMPL, - DISCOVER_CALENDAR, DOMAIN, FeatureAccess, ) @@ -86,7 +78,6 @@ NOTIFICATION_ID = "google_calendar_notification" NOTIFICATION_TITLE = "Google Calendar Setup" GROUP_NAME_ALL_CALENDARS = "Google Calendar Sensors" -SERVICE_SCAN_CALENDARS = "scan_for_calendars" SERVICE_ADD_EVENT = "add_event" YAML_DEVICES = f"{DOMAIN}_calendars.yaml" @@ -248,7 +239,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data[DOMAIN][DATA_SERVICE] = calendar_service - await async_setup_services(hass, calendar_service) # Only expose the add event service if we have the correct permissions if get_feature_access(hass, entry) is FeatureAccess.read_write: await async_setup_add_event_service(hass, calendar_service) @@ -278,57 +268,6 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: await hass.config_entries.async_reload(entry.entry_id) -async def async_setup_services( - hass: HomeAssistant, - calendar_service: GoogleCalendarService, -) -> None: - """Set up the service listeners.""" - - calendars = await hass.async_add_executor_job( - load_config, hass.config.path(YAML_DEVICES) - ) - calendars_file_lock = asyncio.Lock() - - async def _found_calendar(calendar_item: Calendar) -> None: - calendar = get_calendar_info( - hass, - calendar_item.dict(exclude_unset=True), - ) - calendar_id = calendar_item.id - # If the google_calendars.yaml file already exists, populate it for - # backwards compatibility, but otherwise do not create it if it does - # not exist. - if calendars: - if calendar_id not in calendars: - calendars[calendar_id] = calendar - async with calendars_file_lock: - await hass.async_add_executor_job( - update_config, hass.config.path(YAML_DEVICES), calendar - ) - else: - # Prefer entity/name information from yaml, overriding api - calendar = calendars[calendar_id] - async_dispatcher_send(hass, DISCOVER_CALENDAR, calendar) - - created_calendars = set() - - async def _scan_for_calendars(call: ServiceCall) -> None: - """Scan for new calendars.""" - try: - result = await calendar_service.async_list_calendars() - except ApiException as err: - raise HomeAssistantError(str(err)) from err - tasks = [] - for calendar_item in result.items: - if calendar_item.id in created_calendars: - continue - created_calendars.add(calendar_item.id) - tasks.append(_found_calendar(calendar_item)) - await asyncio.gather(*tasks) - - hass.services.async_register(DOMAIN, SERVICE_SCAN_CALENDARS, _scan_for_calendars) - - async def async_setup_add_event_service( hass: HomeAssistant, calendar_service: GoogleCalendarService, diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index ba4368fefae..78661ed792f 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -20,24 +20,24 @@ from homeassistant.components.calendar import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET -from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError, PlatformNotReady -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import Throttle from . import ( - CONF_CAL_ID, CONF_IGNORE_AVAILABILITY, CONF_SEARCH, CONF_TRACK, DATA_SERVICE, DEFAULT_CONF_OFFSET, DOMAIN, - SERVICE_SCAN_CALENDARS, + YAML_DEVICES, + get_calendar_info, + load_config, + update_config, ) -from .const import DISCOVER_CALENDAR _LOGGER = logging.getLogger(__name__) @@ -59,66 +59,63 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the google calendar platform.""" - - @callback - def async_discover(discovery_info: dict[str, Any]) -> None: - _async_setup_entities( - hass, - entry, - async_add_entities, - discovery_info, - ) - - entry.async_on_unload( - async_dispatcher_connect(hass, DISCOVER_CALENDAR, async_discover) - ) - - # Look for any new calendars + calendar_service = hass.data[DOMAIN][DATA_SERVICE] try: - await hass.services.async_call(DOMAIN, SERVICE_SCAN_CALENDARS, blocking=True) - except HomeAssistantError as err: - # This can happen if there's a connection error during setup. + result = await calendar_service.async_list_calendars() + except ApiException as err: raise PlatformNotReady(str(err)) from err - -@callback -def _async_setup_entities( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, - disc_info: dict[str, Any], -) -> None: - calendar_service = hass.data[DOMAIN][DATA_SERVICE] + # Yaml configuration may override objects from the API + calendars = await hass.async_add_executor_job( + load_config, hass.config.path(YAML_DEVICES) + ) + new_calendars = [] entities = [] - num_entities = len(disc_info[CONF_ENTITIES]) - for data in disc_info[CONF_ENTITIES]: - entity_enabled = data.get(CONF_TRACK, True) - if not entity_enabled: - _LOGGER.warning( - "The 'track' option in google_calendars.yaml has been deprecated. The setting " - "has been imported to the UI, and should now be removed from google_calendars.yaml" - ) - entity_name = data[CONF_DEVICE_ID] - entity_id = generate_entity_id(ENTITY_ID_FORMAT, entity_name, hass=hass) - calendar_id = disc_info[CONF_CAL_ID] - if num_entities > 1: - # The google_calendars.yaml file lets users add multiple entities for - # the same calendar id and needs additional disambiguation - unique_id = f"{calendar_id}-{entity_name}" + for calendar_item in result.items: + calendar_id = calendar_item.id + if calendars and calendar_id in calendars: + calendar_info = calendars[calendar_id] else: - unique_id = calendar_id - entity = GoogleCalendarEntity( - calendar_service, - disc_info[CONF_CAL_ID], - data, - entity_id, - unique_id, - entity_enabled, - ) - entities.append(entity) + calendar_info = get_calendar_info( + hass, calendar_item.dict(exclude_unset=True) + ) + new_calendars.append(calendar_info) + + # Yaml calendar config may map one calendar to multiple entities with extra options like + # offsets or search criteria. + num_entities = len(calendar_info[CONF_ENTITIES]) + for data in calendar_info[CONF_ENTITIES]: + entity_enabled = data.get(CONF_TRACK, True) + if not entity_enabled: + _LOGGER.warning( + "The 'track' option in google_calendars.yaml has been deprecated. The setting " + "has been imported to the UI, and should now be removed from google_calendars.yaml" + ) + entity_name = data[CONF_DEVICE_ID] + entities.append( + GoogleCalendarEntity( + calendar_service, + calendar_id, + data, + generate_entity_id(ENTITY_ID_FORMAT, entity_name, hass=hass), + # The google_calendars.yaml file lets users add multiple entities for + # the same calendar id and needs additional disambiguation + f"{calendar_id}-{entity_name}" if num_entities > 1 else calendar_id, + entity_enabled, + ) + ) async_add_entities(entities, True) + if calendars and new_calendars: + + def append_calendars_to_config() -> None: + path = hass.config.path(YAML_DEVICES) + for calendar in new_calendars: + update_config(path, calendar) + + await hass.async_add_executor_job(append_calendars_to_config) + class GoogleCalendarEntity(CalendarEntity): """A calendar event device.""" diff --git a/homeassistant/components/google/const.py b/homeassistant/components/google/const.py index c01ff1ea48b..fba9b01b600 100644 --- a/homeassistant/components/google/const.py +++ b/homeassistant/components/google/const.py @@ -11,8 +11,6 @@ DATA_CALENDARS = "calendars" DATA_SERVICE = "service" DATA_CONFIG = "config" -DISCOVER_CALENDAR = "google_discover_calendar" - class FeatureAccess(Enum): """Class to represent different access scopes.""" diff --git a/homeassistant/components/google/services.yaml b/homeassistant/components/google/services.yaml index 21df763374f..baa069aaedf 100644 --- a/homeassistant/components/google/services.yaml +++ b/homeassistant/components/google/services.yaml @@ -1,6 +1,3 @@ -scan_for_calendars: - name: Scan for calendars - description: Scan for new calendars. add_event: name: Add event description: Add a new calendar event. diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index f2cf067f7bb..cadb444c26f 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -14,11 +14,7 @@ from homeassistant.components.application_credentials import ( ClientCredential, async_import_client_credential, ) -from homeassistant.components.google import ( - DOMAIN, - SERVICE_ADD_EVENT, - SERVICE_SCAN_CALENDARS, -) +from homeassistant.components.google import DOMAIN, SERVICE_ADD_EVENT from homeassistant.components.google.const import CONF_CALENDAR_ACCESS from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF @@ -140,17 +136,24 @@ async def test_invalid_calendar_yaml( component_setup: ComponentSetup, calendars_config: list[dict[str, Any]], mock_calendars_yaml: None, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + mock_events_list: ApiResult, setup_config_entry: MockConfigEntry, ) -> None: - """Test setup with missing entity id fields fails to setup the config entry.""" + """Test setup with missing entity id fields fails to load the platform.""" + mock_calendars_list({"items": [test_api_calendar]}) + mock_events_list({}) + assert await component_setup() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 entry = entries[0] - assert entry.state is ConfigEntryState.SETUP_ERROR + assert entry.state is ConfigEntryState.LOADED assert not hass.states.get(TEST_YAML_ENTITY) + assert not hass.states.get(TEST_API_ENTITY) async def test_calendar_yaml_error( @@ -470,57 +473,6 @@ async def test_add_event_date_time( } -async def test_scan_calendars( - hass: HomeAssistant, - component_setup: ComponentSetup, - mock_calendars_list: ApiResult, - mock_events_list: ApiResult, - setup_config_entry: MockConfigEntry, - aioclient_mock: AiohttpClientMocker, -) -> None: - """Test finding a calendar from the API.""" - - mock_calendars_list({"items": []}) - assert await component_setup() - - calendar_1 = { - "id": "calendar-id-1", - "summary": "Calendar 1", - } - calendar_2 = { - "id": "calendar-id-2", - "summary": "Calendar 2", - } - - aioclient_mock.clear_requests() - mock_calendars_list({"items": [calendar_1]}) - mock_events_list({}, calendar_id="calendar-id-1") - await hass.services.async_call(DOMAIN, SERVICE_SCAN_CALENDARS, {}, blocking=True) - await hass.async_block_till_done() - - state = hass.states.get("calendar.calendar_1") - assert state - assert state.name == "Calendar 1" - assert state.state == STATE_OFF - assert not hass.states.get("calendar.calendar_2") - - aioclient_mock.clear_requests() - mock_calendars_list({"items": [calendar_1, calendar_2]}) - mock_events_list({}, calendar_id="calendar-id-1") - mock_events_list({}, calendar_id="calendar-id-2") - await hass.services.async_call(DOMAIN, SERVICE_SCAN_CALENDARS, {}, blocking=True) - await hass.async_block_till_done() - - state = hass.states.get("calendar.calendar_1") - assert state - assert state.name == "Calendar 1" - assert state.state == STATE_OFF - state = hass.states.get("calendar.calendar_2") - assert state - assert state.name == "Calendar 2" - assert state.state == STATE_OFF - - @pytest.mark.parametrize( "config_entry_token_expiry", [datetime.datetime.max.timestamp() + 1] ) From 0829bec1c349f5d48e21e39736c0f12e12386df3 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sat, 4 Jun 2022 07:52:39 +0200 Subject: [PATCH 1240/3516] Bump pypck to 0.7.15 (#73009) --- homeassistant/components/lcn/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 412ef74e3b8..eea72a0e508 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -3,7 +3,7 @@ "name": "LCN", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": ["pypck==0.7.14"], + "requirements": ["pypck==0.7.15"], "codeowners": ["@alengwenus"], "iot_class": "local_push", "loggers": ["pypck"] diff --git a/requirements_all.txt b/requirements_all.txt index bad7eac7e6c..ce727d0e31c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1735,7 +1735,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.14 +pypck==0.7.15 # homeassistant.components.pjlink pypjlink2==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 54028fac054..d265ec19a3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1178,7 +1178,7 @@ pyowm==3.2.0 pyownet==0.10.0.post1 # homeassistant.components.lcn -pypck==0.7.14 +pypck==0.7.15 # homeassistant.components.plaato pyplaato==0.0.18 From a1b372e4ca59b9574908f8797607725c6c9328ac Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 4 Jun 2022 12:37:39 +0200 Subject: [PATCH 1241/3516] Minor fixes Trafikverket Train (#72996) * Minor fixes Trafikverket Train * Remove ConfigEntryAuthFailed --- .../components/trafikverket_train/__init__.py | 30 +++++++++++-- .../components/trafikverket_train/sensor.py | 31 ++++--------- .../trafikverket_train/test_config_flow.py | 45 +++++++++++++++++++ 3 files changed, 80 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/trafikverket_train/__init__.py b/homeassistant/components/trafikverket_train/__init__.py index 4411ccab948..ee026371b04 100644 --- a/homeassistant/components/trafikverket_train/__init__.py +++ b/homeassistant/components/trafikverket_train/__init__.py @@ -1,15 +1,39 @@ """The trafikverket_train component.""" from __future__ import annotations -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from pytrafikverket import TrafikverketTrain -from .const import PLATFORMS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_FROM, CONF_TO, DOMAIN, PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Trafikverket Train from a config entry.""" + http_session = async_get_clientsession(hass) + train_api = TrafikverketTrain(http_session, entry.data[CONF_API_KEY]) + + try: + to_station = await train_api.async_get_train_station(entry.data[CONF_TO]) + from_station = await train_api.async_get_train_station(entry.data[CONF_FROM]) + except ValueError as error: + if "Invalid authentication" in error.args[0]: + raise ConfigEntryAuthFailed from error + raise ConfigEntryNotReady( + f"Problem when trying station {entry.data[CONF_FROM]} to {entry.data[CONF_TO]}. Error: {error} " + ) from error + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + CONF_TO: to_station, + CONF_FROM: from_station, + "train_api": train_api, + } + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index 4a419ff3b33..d9674e5373a 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -10,10 +10,8 @@ from pytrafikverket.trafikverket_train import StationInfo, TrainStop from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS +from homeassistant.const import CONF_NAME, CONF_WEEKDAY, WEEKDAYS from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -42,24 +40,11 @@ async def async_setup_entry( ) -> None: """Set up the Trafikverket sensor entry.""" - httpsession = async_get_clientsession(hass) - train_api = TrafikverketTrain(httpsession, entry.data[CONF_API_KEY]) - - try: - to_station = await train_api.async_get_train_station(entry.data[CONF_TO]) - from_station = await train_api.async_get_train_station(entry.data[CONF_FROM]) - except ValueError as error: - if "Invalid authentication" in error.args[0]: - raise ConfigEntryAuthFailed from error - raise ConfigEntryNotReady( - f"Problem when trying station {entry.data[CONF_FROM]} to {entry.data[CONF_TO]}. Error: {error} " - ) from error - - train_time = ( - dt.parse_time(entry.data.get(CONF_TIME, "")) - if entry.data.get(CONF_TIME) - else None - ) + train_api = hass.data[DOMAIN][entry.entry_id]["train_api"] + to_station = hass.data[DOMAIN][entry.entry_id][CONF_TO] + from_station = hass.data[DOMAIN][entry.entry_id][CONF_FROM] + get_time: str | None = entry.data.get(CONF_TIME) + train_time = dt.parse_time(get_time) if get_time else None async_add_entities( [ @@ -157,8 +142,8 @@ class TrainSensor(SensorEntity): _state = await self._train_api.async_get_next_train_stop( self._from_station, self._to_station, when ) - except ValueError as output_error: - _LOGGER.error("Departure %s encountered a problem: %s", when, output_error) + except ValueError as error: + _LOGGER.error("Departure %s encountered a problem: %s", when, error) if not _state: self._attr_available = False diff --git a/tests/components/trafikverket_train/test_config_flow.py b/tests/components/trafikverket_train/test_config_flow.py index 09539584cbc..37788fc285b 100644 --- a/tests/components/trafikverket_train/test_config_flow.py +++ b/tests/components/trafikverket_train/test_config_flow.py @@ -66,6 +66,51 @@ async def test_form(hass: HomeAssistant) -> None: ) +async def test_form_entry_already_exist(hass: HomeAssistant) -> None: + """Test flow aborts when entry already exist.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_API_KEY: "1234567890", + CONF_NAME: "Stockholm C to Uppsala C at 10:00", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + CONF_TIME: "10:00", + CONF_WEEKDAY: WEEKDAYS, + }, + unique_id=f"stockholmc-uppsalac-10:00-{WEEKDAYS}", + ) + entry.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["errors"] == {} + + with patch( + "homeassistant.components.trafikverket_train.config_flow.TrafikverketTrain.async_get_train_station", + ), patch( + "homeassistant.components.trafikverket_train.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "1234567890", + CONF_FROM: "Stockholm C", + CONF_TO: "Uppsala C", + CONF_TIME: "10:00", + CONF_WEEKDAY: WEEKDAYS, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + @pytest.mark.parametrize( "error_message,base_error", [ From c7416c0bb95dd40d7f8bebc6fc5773a849e5c300 Mon Sep 17 00:00:00 2001 From: Matrix Date: Sat, 4 Jun 2022 23:54:39 +0800 Subject: [PATCH 1242/3516] Add yolink vibration sensor (#72926) * Add yolink vibration sensor * add battery entity * fix suggest --- .../components/yolink/binary_sensor.py | 9 +++++++ homeassistant/components/yolink/const.py | 1 + homeassistant/components/yolink/sensor.py | 25 +++++++++++++------ 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/yolink/binary_sensor.py b/homeassistant/components/yolink/binary_sensor.py index cacba484fe9..d5c9ddedb84 100644 --- a/homeassistant/components/yolink/binary_sensor.py +++ b/homeassistant/components/yolink/binary_sensor.py @@ -21,6 +21,7 @@ from .const import ( ATTR_DEVICE_DOOR_SENSOR, ATTR_DEVICE_LEAK_SENSOR, ATTR_DEVICE_MOTION_SENSOR, + ATTR_DEVICE_VIBRATION_SENSOR, DOMAIN, ) from .coordinator import YoLinkCoordinator @@ -40,6 +41,7 @@ SENSOR_DEVICE_TYPE = [ ATTR_DEVICE_DOOR_SENSOR, ATTR_DEVICE_MOTION_SENSOR, ATTR_DEVICE_LEAK_SENSOR, + ATTR_DEVICE_VIBRATION_SENSOR, ] SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( @@ -66,6 +68,13 @@ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( value=lambda value: value == "alert" if value is not None else None, exists_fn=lambda device: device.device_type in [ATTR_DEVICE_LEAK_SENSOR], ), + YoLinkBinarySensorEntityDescription( + key="vibration_state", + name="Vibration", + device_class=BinarySensorDeviceClass.VIBRATION, + value=lambda value: value == "alert" if value is not None else None, + exists_fn=lambda device: device.device_type in [ATTR_DEVICE_VIBRATION_SENSOR], + ), ) diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index 97252c5c989..16304e0de4b 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -17,5 +17,6 @@ ATTR_DEVICE_DOOR_SENSOR = "DoorSensor" ATTR_DEVICE_TH_SENSOR = "THSensor" ATTR_DEVICE_MOTION_SENSOR = "MotionSensor" ATTR_DEVICE_LEAK_SENSOR = "LeakSensor" +ATTR_DEVICE_VIBRATION_SENSOR = "VibrationSensor" ATTR_DEVICE_OUTLET = "Outlet" ATTR_DEVICE_SIREN = "Siren" diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 463d8b14da4..917a93c310d 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -23,6 +23,7 @@ from .const import ( ATTR_DEVICE_DOOR_SENSOR, ATTR_DEVICE_MOTION_SENSOR, ATTR_DEVICE_TH_SENSOR, + ATTR_DEVICE_VIBRATION_SENSOR, DOMAIN, ) from .coordinator import YoLinkCoordinator @@ -45,6 +46,21 @@ class YoLinkSensorEntityDescription( value: Callable = lambda state: state +SENSOR_DEVICE_TYPE = [ + ATTR_DEVICE_DOOR_SENSOR, + ATTR_DEVICE_MOTION_SENSOR, + ATTR_DEVICE_TH_SENSOR, + ATTR_DEVICE_VIBRATION_SENSOR, +] + +BATTERY_POWER_SENSOR = [ + ATTR_DEVICE_DOOR_SENSOR, + ATTR_DEVICE_TH_SENSOR, + ATTR_DEVICE_MOTION_SENSOR, + ATTR_DEVICE_VIBRATION_SENSOR, +] + + SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( YoLinkSensorEntityDescription( key="battery", @@ -57,8 +73,7 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( ) if value is not None else None, - exists_fn=lambda device: device.device_type - in [ATTR_DEVICE_DOOR_SENSOR, ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_MOTION_SENSOR], + exists_fn=lambda device: device.device_type in BATTERY_POWER_SENSOR, ), YoLinkSensorEntityDescription( key="humidity", @@ -78,12 +93,6 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( ), ) -SENSOR_DEVICE_TYPE = [ - ATTR_DEVICE_DOOR_SENSOR, - ATTR_DEVICE_MOTION_SENSOR, - ATTR_DEVICE_TH_SENSOR, -] - async def async_setup_entry( hass: HomeAssistant, From 0a2a166860fcc7e7fd5c693155ccc2af50ab11aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Jun 2022 09:47:30 -1000 Subject: [PATCH 1243/3516] Fix history stats not comparing all times in UTC (#73040) --- homeassistant/components/history_stats/data.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 5466498fc32..3b17c715c97 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -67,9 +67,10 @@ class HistoryStats: current_period_end_timestamp = floored_timestamp(current_period_end) previous_period_start_timestamp = floored_timestamp(previous_period_start) previous_period_end_timestamp = floored_timestamp(previous_period_end) - now_timestamp = floored_timestamp(datetime.datetime.now()) + utc_now = dt_util.utcnow() + now_timestamp = floored_timestamp(utc_now) - if now_timestamp < current_period_start_timestamp: + if current_period_start > utc_now: # History cannot tell the future self._history_current_period = [] self._previous_run_before_start = True From 7ac7af094f0144997aeb455190ff77998ca37694 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Jun 2022 09:54:10 -1000 Subject: [PATCH 1244/3516] Fix missing historical context data in logbook for MySQL and PostgreSQL (#73011) --- homeassistant/components/recorder/filters.py | 29 ++++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 835496c2d6e..90851e9f251 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -5,7 +5,7 @@ from collections.abc import Callable, Iterable import json from typing import Any -from sqlalchemy import JSON, Column, Text, cast, not_, or_ +from sqlalchemy import Column, Text, cast, not_, or_ from sqlalchemy.sql.elements import ClauseList from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE @@ -16,6 +16,7 @@ from .models import ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT, States DOMAIN = "history" HISTORY_FILTERS = "history_filters" +JSON_NULL = json.dumps(None) GLOB_TO_SQL_CHARS = { ord("*"): "%", @@ -196,7 +197,17 @@ class Filters: """Generate the entity filter query.""" _encoder = json.dumps return or_( - (ENTITY_ID_IN_EVENT == JSON.NULL) & (OLD_ENTITY_ID_IN_EVENT == JSON.NULL), + # sqlalchemy's SQLite json implementation always + # wraps everything with JSON_QUOTE so it resolves to 'null' + # when its empty + # + # For MySQL and PostgreSQL it will resolve to a literal + # NULL when its empty + # + ((ENTITY_ID_IN_EVENT == JSON_NULL) | ENTITY_ID_IN_EVENT.is_(None)) + & ( + (OLD_ENTITY_ID_IN_EVENT == JSON_NULL) | OLD_ENTITY_ID_IN_EVENT.is_(None) + ), self._generate_filter_for_columns( (ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT), _encoder ).self_group(), @@ -208,8 +219,11 @@ def _globs_to_like( ) -> ClauseList: """Translate glob to sql.""" matchers = [ - cast(column, Text()).like( - encoder(glob_str).translate(GLOB_TO_SQL_CHARS), escape="\\" + ( + column.is_not(None) + & cast(column, Text()).like( + encoder(glob_str).translate(GLOB_TO_SQL_CHARS), escape="\\" + ) ) for glob_str in glob_strs for column in columns @@ -221,7 +235,10 @@ def _entity_matcher( entity_ids: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: matchers = [ - cast(column, Text()).in_([encoder(entity_id) for entity_id in entity_ids]) + ( + column.is_not(None) + & cast(column, Text()).in_([encoder(entity_id) for entity_id in entity_ids]) + ) for column in columns ] return or_(*matchers) if matchers else or_(False) @@ -231,7 +248,7 @@ def _domain_matcher( domains: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: matchers = [ - cast(column, Text()).like(encoder(f"{domain}.%")) + (column.is_not(None) & cast(column, Text()).like(encoder(f"{domain}.%"))) for domain in domains for column in columns ] From e0ca5bafda8b1288129a7e5675ece48d9490d14e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 3 Jun 2022 10:04:46 -1000 Subject: [PATCH 1245/3516] Fix statistics_during_period being incorrectly cached (#72947) --- .../components/recorder/statistics.py | 7 +- tests/components/history/test_init.py | 136 ++++++++++++++++++ 2 files changed, 138 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 4bed39fee4a..39fcb954ee9 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -984,7 +984,6 @@ def _reduce_statistics_per_month( def _statistics_during_period_stmt( start_time: datetime, end_time: datetime | None, - statistic_ids: list[str] | None, metadata_ids: list[int] | None, table: type[Statistics | StatisticsShortTerm], ) -> StatementLambdaElement: @@ -1002,7 +1001,7 @@ def _statistics_during_period_stmt( if end_time is not None: stmt += lambda q: q.filter(table.start < end_time) - if statistic_ids is not None: + if metadata_ids: stmt += lambda q: q.filter(table.metadata_id.in_(metadata_ids)) stmt += lambda q: q.order_by(table.metadata_id, table.start) @@ -1038,9 +1037,7 @@ def statistics_during_period( else: table = Statistics - stmt = _statistics_during_period_stmt( - start_time, end_time, statistic_ids, metadata_ids, table - ) + stmt = _statistics_during_period_stmt(start_time, end_time, metadata_ids, table) stats = execute_stmt_lambda_element(session, stmt) if not stats: diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 9dc7af59a38..8c0a80719a8 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -5,6 +5,7 @@ from http import HTTPStatus import json from unittest.mock import patch, sentinel +from freezegun import freeze_time import pytest from pytest import approx @@ -928,6 +929,141 @@ async def test_statistics_during_period( } +@pytest.mark.parametrize( + "units, attributes, state, value", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), + (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, 10, 10000), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 50), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, 10, 10), + (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 14.503774389728312), + (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, 1000, 100000), + ], +) +async def test_statistics_during_period_in_the_past( + hass, hass_ws_client, recorder_mock, units, attributes, state, value +): + """Test statistics_during_period in the past.""" + hass.config.set_time_zone("UTC") + now = dt_util.utcnow().replace() + + hass.config.units = units + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + + past = now - timedelta(days=3) + + with freeze_time(past): + hass.states.async_set("sensor.test", state, attributes=attributes) + await async_wait_recording_done(hass) + + sensor_state = hass.states.get("sensor.test") + assert sensor_state.last_updated == past + + stats_top_of_hour = past.replace(minute=0, second=0, microsecond=0) + stats_start = past.replace(minute=55) + do_adhoc_statistics(hass, start=stats_start) + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "hour", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + await client.send_json( + { + "id": 2, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + past = now - timedelta(days=3) + await client.send_json( + { + "id": 3, + "type": "history/statistics_during_period", + "start_time": past.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "sensor.test": [ + { + "statistic_id": "sensor.test", + "start": stats_start.isoformat(), + "end": (stats_start + timedelta(minutes=5)).isoformat(), + "mean": approx(value), + "min": approx(value), + "max": approx(value), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + start_of_day = stats_top_of_hour.replace(hour=0, minute=0) + await client.send_json( + { + "id": 4, + "type": "history/statistics_during_period", + "start_time": stats_top_of_hour.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "day", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "sensor.test": [ + { + "statistic_id": "sensor.test", + "start": start_of_day.isoformat(), + "end": (start_of_day + timedelta(days=1)).isoformat(), + "mean": approx(value), + "min": approx(value), + "max": approx(value), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + await client.send_json( + { + "id": 5, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "statistic_ids": ["sensor.test"], + "period": "5minute", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + async def test_statistics_during_period_bad_start_time( hass, hass_ws_client, recorder_mock ): From 73536c07d7a13e5f7c5a2c5d6c07d8b4ec2c5d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Beamonte?= Date: Fri, 3 Jun 2022 09:27:10 -0400 Subject: [PATCH 1246/3516] Allow `log` template function to return specified `default` on math domain error (#72960) Fix regression for logarithm template --- homeassistant/helpers/template.py | 14 +++++++------- tests/helpers/test_template.py | 2 ++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 1a8febf5ac2..053beab307e 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1369,19 +1369,19 @@ def multiply(value, amount, default=_SENTINEL): def logarithm(value, base=math.e, default=_SENTINEL): """Filter and function to get logarithm of the value with a specific base.""" - try: - value_float = float(value) - except (ValueError, TypeError): - if default is _SENTINEL: - raise_no_default("log", value) - return default try: base_float = float(base) except (ValueError, TypeError): if default is _SENTINEL: raise_no_default("log", base) return default - return math.log(value_float, base_float) + try: + value_float = float(value) + return math.log(value_float, base_float) + except (ValueError, TypeError): + if default is _SENTINEL: + raise_no_default("log", value) + return default def sine(value, default=_SENTINEL): diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index ddda17c20ac..a1fd3e73f59 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -447,6 +447,8 @@ def test_logarithm(hass): assert render(hass, "{{ 'no_number' | log(10, default=1) }}") == 1 assert render(hass, "{{ log('no_number', 10, 1) }}") == 1 assert render(hass, "{{ log('no_number', 10, default=1) }}") == 1 + assert render(hass, "{{ log(0, 10, 1) }}") == 1 + assert render(hass, "{{ log(0, 10, default=1) }}") == 1 def test_sine(hass): From 9f8fe7fca6dfee9fca4533c93ec81899190199b6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 3 Jun 2022 13:57:59 +0200 Subject: [PATCH 1247/3516] Bump pynetgear to 0.10.4 (#72965) bump pynetgear to 0.10.4 --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index ae0824c82a5..f65f5aa6686 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.10.0"], + "requirements": ["pynetgear==0.10.4"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 791875978ef..454423a6dca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1673,7 +1673,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.10.0 +pynetgear==0.10.4 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4fa7c37963d..b6426228b42 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1131,7 +1131,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.10.0 +pynetgear==0.10.4 # homeassistant.components.nina pynina==0.1.8 From 65cb82765baaace47b8933d4413b542fff9062f8 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Fri, 3 Jun 2022 21:59:10 +0200 Subject: [PATCH 1248/3516] Bump bimmer_connected to 0.9.4 (#72973) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 75ac3e982e8..cd6daa83705 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.9.3"], + "requirements": ["bimmer_connected==0.9.4"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 454423a6dca..811d63d8e2b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -394,7 +394,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.3 +bimmer_connected==0.9.4 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6426228b42..67fc6dfe509 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -309,7 +309,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.3 +bimmer_connected==0.9.4 # homeassistant.components.blebox blebox_uniapi==1.3.3 From 5c512ad5cbbaf5315ebbfc5601dcd0c96add4630 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 3 Jun 2022 22:05:37 +0200 Subject: [PATCH 1249/3516] fjaraskupan: Don't filter anything in backend (#72988) --- homeassistant/components/fjaraskupan/__init__.py | 4 ++-- homeassistant/components/fjaraskupan/config_flow.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index ec4528bc079..4c4f19403a6 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_NAME, Device, State, device_filter +from fjaraskupan import Device, State, device_filter from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -90,7 +90,7 @@ class EntryState: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Fjäråskupan from a config entry.""" - scanner = BleakScanner(filters={"Pattern": DEVICE_NAME, "DuplicateData": True}) + scanner = BleakScanner(filters={"DuplicateData": True}) state = EntryState(scanner, {}) hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/fjaraskupan/config_flow.py b/homeassistant/components/fjaraskupan/config_flow.py index 3af34c0eef6..ffac366500b 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_NAME, device_filter +from fjaraskupan import device_filter from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_flow import register_discovery_flow @@ -28,7 +28,7 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: async with BleakScanner( detection_callback=detection, - filters={"Pattern": DEVICE_NAME, "DuplicateData": True}, + filters={"DuplicateData": True}, ): try: async with async_timeout.timeout(CONST_WAIT_TIME): From 6a3b74adf64a848ee20fc1ad2d0c148676792a71 Mon Sep 17 00:00:00 2001 From: shbatm Date: Fri, 3 Jun 2022 11:53:23 -0500 Subject: [PATCH 1250/3516] Check ISY994 climate for unknown humidity value on Z-Wave Thermostat (#72990) Check ISY994 climate for unknown humidity on Z-Wave Thermostat Update to #72670 to compare the property value and not the parent object. Should actually fix #72628 --- homeassistant/components/isy994/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index d68395f14da..9f4c52258a7 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -117,7 +117,7 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): """Return the current humidity.""" if not (humidity := self._node.aux_properties.get(PROP_HUMIDITY)): return None - if humidity == ISY_VALUE_UNKNOWN: + if humidity.value == ISY_VALUE_UNKNOWN: return None return int(humidity.value) From 085eee88c97426fd989260f5845f380c78899c81 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 3 Jun 2022 16:33:12 -0700 Subject: [PATCH 1251/3516] Fix google calendar bug where expired tokens are not refreshed (#72994) --- homeassistant/components/google/api.py | 7 +++++-- tests/components/google/conftest.py | 6 ++++-- tests/components/google/test_config_flow.py | 14 ++++++++------ 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index 4bb9de5d581..a4cda1ff41a 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -5,7 +5,6 @@ from __future__ import annotations from collections.abc import Awaitable, Callable import datetime import logging -import time from typing import Any, cast import aiohttp @@ -50,12 +49,16 @@ class DeviceAuth(AuthImplementation): async def async_resolve_external_data(self, external_data: Any) -> dict: """Resolve a Google API Credentials object to Home Assistant token.""" creds: Credentials = external_data[DEVICE_AUTH_CREDS] + delta = creds.token_expiry.replace(tzinfo=datetime.timezone.utc) - dt.utcnow() + _LOGGER.debug( + "Token expires at %s (in %s)", creds.token_expiry, delta.total_seconds() + ) return { "access_token": creds.access_token, "refresh_token": creds.refresh_token, "scope": " ".join(creds.scopes), "token_type": "Bearer", - "expires_in": creds.token_expiry.timestamp() - time.time(), + "expires_in": delta.total_seconds(), } diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index b5566450913..68176493445 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -16,7 +16,6 @@ from homeassistant.components.google import CONF_TRACK_NEW, DOMAIN from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -136,7 +135,10 @@ def token_scopes() -> list[str]: @pytest.fixture def token_expiry() -> datetime.datetime: """Expiration time for credentials used in the test.""" - return utcnow() + datetime.timedelta(days=7) + # OAuth library returns an offset-naive timestamp + return datetime.datetime.fromtimestamp( + datetime.datetime.utcnow().timestamp() + ) + datetime.timedelta(hours=1) @pytest.fixture diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index 8ac017fcba4..a346b02e6c2 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -8,6 +8,7 @@ from typing import Any from unittest.mock import Mock, patch from aiohttp.client_exceptions import ClientError +from freezegun.api import FrozenDateTimeFactory from oauth2client.client import ( FlowExchangeError, OAuth2Credentials, @@ -94,11 +95,13 @@ async def fire_alarm(hass, point_in_time): await hass.async_block_till_done() +@pytest.mark.freeze_time("2022-06-03 15:19:59-00:00") async def test_full_flow_yaml_creds( hass: HomeAssistant, mock_code_flow: Mock, mock_exchange: Mock, component_setup: ComponentSetup, + freezer: FrozenDateTimeFactory, ) -> None: """Test successful creds setup.""" assert await component_setup() @@ -115,8 +118,8 @@ async def test_full_flow_yaml_creds( "homeassistant.components.google.async_setup_entry", return_value=True ) as mock_setup: # Run one tick to invoke the credential exchange check - now = utcnow() - await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + freezer.tick(CODE_CHECK_ALARM_TIMEDELTA) + await fire_alarm(hass, datetime.datetime.utcnow()) await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"] @@ -127,12 +130,11 @@ async def test_full_flow_yaml_creds( assert "data" in result data = result["data"] assert "token" in data - assert 0 < data["token"]["expires_in"] < 8 * 86400 assert ( - datetime.datetime.now().timestamp() - <= data["token"]["expires_at"] - < (datetime.datetime.now() + datetime.timedelta(days=8)).timestamp() + data["token"]["expires_in"] + == 60 * 60 - CODE_CHECK_ALARM_TIMEDELTA.total_seconds() ) + assert data["token"]["expires_at"] == 1654273199.0 data["token"].pop("expires_at") data["token"].pop("expires_in") assert data == { From f3136c811c41e94f1614331d2208f562292dc90f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 3 Jun 2022 17:03:21 -0500 Subject: [PATCH 1252/3516] Provide Sonos media position if duration not available (#73001) --- homeassistant/components/sonos/media.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/media.py b/homeassistant/components/sonos/media.py index e3d8f043d4b..9608356ba64 100644 --- a/homeassistant/components/sonos/media.py +++ b/homeassistant/components/sonos/media.py @@ -205,13 +205,15 @@ class SonosMedia: self, position_info: dict[str, int], force_update: bool = False ) -> None: """Update state when playing music tracks.""" - if (duration := position_info.get(DURATION_SECONDS)) == 0: + duration = position_info.get(DURATION_SECONDS) + current_position = position_info.get(POSITION_SECONDS) + + if not (duration or current_position): self.clear_position() return should_update = force_update self.duration = duration - current_position = position_info.get(POSITION_SECONDS) # player started reporting position? if current_position is not None and self.position is None: From 10fb3035d6a59fcdb1d6076223779b183f9048b8 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Sat, 4 Jun 2022 07:52:39 +0200 Subject: [PATCH 1253/3516] Bump pypck to 0.7.15 (#73009) --- homeassistant/components/lcn/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 412ef74e3b8..eea72a0e508 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -3,7 +3,7 @@ "name": "LCN", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": ["pypck==0.7.14"], + "requirements": ["pypck==0.7.15"], "codeowners": ["@alengwenus"], "iot_class": "local_push", "loggers": ["pypck"] diff --git a/requirements_all.txt b/requirements_all.txt index 811d63d8e2b..4c848eef637 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1735,7 +1735,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.14 +pypck==0.7.15 # homeassistant.components.pjlink pypjlink2==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67fc6dfe509..3efb7dd9878 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1178,7 +1178,7 @@ pyowm==3.2.0 pyownet==0.10.0.post1 # homeassistant.components.lcn -pypck==0.7.14 +pypck==0.7.15 # homeassistant.components.plaato pyplaato==0.0.18 From 373634cc50d67fb47b444deaab98939f55d0ab59 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Jun 2022 09:54:10 -1000 Subject: [PATCH 1254/3516] Fix missing historical context data in logbook for MySQL and PostgreSQL (#73011) --- homeassistant/components/recorder/filters.py | 29 ++++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 835496c2d6e..90851e9f251 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -5,7 +5,7 @@ from collections.abc import Callable, Iterable import json from typing import Any -from sqlalchemy import JSON, Column, Text, cast, not_, or_ +from sqlalchemy import Column, Text, cast, not_, or_ from sqlalchemy.sql.elements import ClauseList from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE @@ -16,6 +16,7 @@ from .models import ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT, States DOMAIN = "history" HISTORY_FILTERS = "history_filters" +JSON_NULL = json.dumps(None) GLOB_TO_SQL_CHARS = { ord("*"): "%", @@ -196,7 +197,17 @@ class Filters: """Generate the entity filter query.""" _encoder = json.dumps return or_( - (ENTITY_ID_IN_EVENT == JSON.NULL) & (OLD_ENTITY_ID_IN_EVENT == JSON.NULL), + # sqlalchemy's SQLite json implementation always + # wraps everything with JSON_QUOTE so it resolves to 'null' + # when its empty + # + # For MySQL and PostgreSQL it will resolve to a literal + # NULL when its empty + # + ((ENTITY_ID_IN_EVENT == JSON_NULL) | ENTITY_ID_IN_EVENT.is_(None)) + & ( + (OLD_ENTITY_ID_IN_EVENT == JSON_NULL) | OLD_ENTITY_ID_IN_EVENT.is_(None) + ), self._generate_filter_for_columns( (ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT), _encoder ).self_group(), @@ -208,8 +219,11 @@ def _globs_to_like( ) -> ClauseList: """Translate glob to sql.""" matchers = [ - cast(column, Text()).like( - encoder(glob_str).translate(GLOB_TO_SQL_CHARS), escape="\\" + ( + column.is_not(None) + & cast(column, Text()).like( + encoder(glob_str).translate(GLOB_TO_SQL_CHARS), escape="\\" + ) ) for glob_str in glob_strs for column in columns @@ -221,7 +235,10 @@ def _entity_matcher( entity_ids: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: matchers = [ - cast(column, Text()).in_([encoder(entity_id) for entity_id in entity_ids]) + ( + column.is_not(None) + & cast(column, Text()).in_([encoder(entity_id) for entity_id in entity_ids]) + ) for column in columns ] return or_(*matchers) if matchers else or_(False) @@ -231,7 +248,7 @@ def _domain_matcher( domains: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: matchers = [ - cast(column, Text()).like(encoder(f"{domain}.%")) + (column.is_not(None) & cast(column, Text()).like(encoder(f"{domain}.%"))) for domain in domains for column in columns ] From b401f165830b31eb7c17516ab8407ffdfad7fc41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Jun 2022 09:47:30 -1000 Subject: [PATCH 1255/3516] Fix history stats not comparing all times in UTC (#73040) --- homeassistant/components/history_stats/data.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 5466498fc32..3b17c715c97 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -67,9 +67,10 @@ class HistoryStats: current_period_end_timestamp = floored_timestamp(current_period_end) previous_period_start_timestamp = floored_timestamp(previous_period_start) previous_period_end_timestamp = floored_timestamp(previous_period_end) - now_timestamp = floored_timestamp(datetime.datetime.now()) + utc_now = dt_util.utcnow() + now_timestamp = floored_timestamp(utc_now) - if now_timestamp < current_period_start_timestamp: + if current_period_start > utc_now: # History cannot tell the future self._history_current_period = [] self._previous_run_before_start = True From d9a41d10ffbc2a9f7d0aca8673d81e143993b438 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 4 Jun 2022 13:03:51 -0700 Subject: [PATCH 1256/3516] Bumped version to 2022.6.2 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 08ab335a33b..23ada7591ee 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index aeafbd53565..08bd61e7382 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.1 +version = 2022.6.2 url = https://www.home-assistant.io/ [options] From 13734428bbce82e0476f646df6b943e51042fb37 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 5 Jun 2022 00:26:15 +0000 Subject: [PATCH 1257/3516] [ci skip] Translation update --- .../components/scrape/translations/de.json | 73 +++++++++++++++++++ .../components/scrape/translations/el.json | 73 +++++++++++++++++++ .../components/scrape/translations/fr.json | 73 +++++++++++++++++++ .../components/scrape/translations/hu.json | 73 +++++++++++++++++++ .../components/scrape/translations/id.json | 22 +++++- .../components/scrape/translations/ja.json | 58 +++++++++++++++ .../scrape/translations/zh-Hant.json | 73 +++++++++++++++++++ 7 files changed, 443 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/scrape/translations/de.json create mode 100644 homeassistant/components/scrape/translations/el.json create mode 100644 homeassistant/components/scrape/translations/fr.json create mode 100644 homeassistant/components/scrape/translations/hu.json create mode 100644 homeassistant/components/scrape/translations/ja.json create mode 100644 homeassistant/components/scrape/translations/zh-Hant.json diff --git a/homeassistant/components/scrape/translations/de.json b/homeassistant/components/scrape/translations/de.json new file mode 100644 index 00000000000..d4e2f37f88d --- /dev/null +++ b/homeassistant/components/scrape/translations/de.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "attribute": "Attribut", + "authentication": "Authentifizierung", + "device_class": "Ger\u00e4teklasse", + "headers": "Header", + "index": "Index", + "name": "Name", + "password": "Passwort", + "resource": "Ressource", + "select": "Ausw\u00e4hlen", + "state_class": "Zustandsklasse", + "unit_of_measurement": "Ma\u00dfeinheit", + "username": "Benutzername", + "value_template": "Wertvorlage", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + }, + "data_description": { + "attribute": "Wert eines Attributs auf dem ausgew\u00e4hlten Tag abrufen", + "authentication": "Typ der HTTP-Authentifizierung. Entweder basic oder digest", + "device_class": "Der Typ/die Klasse des Sensors, um das Symbol im Frontend festzulegen", + "headers": "F\u00fcr die Webanforderung zu verwendende Header", + "index": "Definiert, welche der vom CSS-Selektor zur\u00fcckgegebenen Elemente verwendet werden sollen", + "resource": "Die URL der Website, die den Wert enth\u00e4lt", + "select": "Legt fest, nach welchem Tag gesucht werden soll. Siehe Beautifulsoup CSS-Selektoren f\u00fcr Details", + "state_class": "Die state_class des Sensors", + "value_template": "Definiert eine Vorlage, um den Zustand des Sensors zu ermitteln", + "verify_ssl": "Aktiviert/deaktiviert die \u00dcberpr\u00fcfung des SSL/TLS-Zertifikats, z.B. wenn es selbst signiert ist" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "attribute": "Attribut", + "authentication": "Authentifizierung", + "device_class": "Ger\u00e4teklasse", + "headers": "Header", + "index": "Index", + "name": "Name", + "password": "Passwort", + "resource": "Ressource", + "select": "Ausw\u00e4hlen", + "state_class": "Zustandsklasse", + "unit_of_measurement": "Ma\u00dfeinheit", + "username": "Benutzername", + "value_template": "Wertvorlage", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" + }, + "data_description": { + "attribute": "Wert eines Attributs auf dem ausgew\u00e4hlten Tag abrufen", + "authentication": "Typ der HTTP-Authentifizierung. Entweder basic oder digest", + "device_class": "Der Typ/die Klasse des Sensors, um das Symbol im Frontend festzulegen", + "headers": "F\u00fcr die Webanforderung zu verwendende Header", + "index": "Definiert, welche der vom CSS-Selektor zur\u00fcckgegebenen Elemente verwendet werden sollen", + "resource": "Die URL der Website, die den Wert enth\u00e4lt", + "select": "Legt fest, nach welchem Tag gesucht werden soll. Siehe Beautifulsoup CSS-Selektoren f\u00fcr Details", + "state_class": "Die state_class des Sensors", + "value_template": "Definiert eine Vorlage, um den Zustand des Sensors zu ermitteln", + "verify_ssl": "Aktiviert/deaktiviert die \u00dcberpr\u00fcfung des SSL/TLS-Zertifikats, z.B. wenn es selbst signiert ist" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/el.json b/homeassistant/components/scrape/translations/el.json new file mode 100644 index 00000000000..8782b2b9f6d --- /dev/null +++ b/homeassistant/components/scrape/translations/el.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "step": { + "user": { + "data": { + "attribute": "\u03a7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc", + "authentication": "\u0395\u03bb\u03ad\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "device_class": "\u039a\u03bb\u03ac\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", + "headers": "\u039a\u03b5\u03c6\u03b1\u03bb\u03af\u03b4\u03b5\u03c2", + "index": "\u0394\u03b5\u03af\u03ba\u03c4\u03b7\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "resource": "\u03a0\u03cc\u03c1\u03bf\u03c2", + "select": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae", + "state_class": "\u039a\u03bb\u03ac\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2", + "unit_of_measurement": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", + "value_template": "\u03a0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf \u03c4\u03b9\u03bc\u03ae\u03c2", + "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" + }, + "data_description": { + "attribute": "\u039b\u03ae\u03c8\u03b7 \u03c4\u03b7\u03c2 \u03c4\u03b9\u03bc\u03ae\u03c2 \u03b5\u03bd\u03cc\u03c2 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b5\u03c4\u03b9\u03ba\u03ad\u03c4\u03b1", + "authentication": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 HTTP. \u0395\u03af\u03c4\u03b5 \u03b2\u03b1\u03c3\u03b9\u03ba\u03cc\u03c2 \u03b5\u03af\u03c4\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2", + "device_class": "\u039f \u03c4\u03cd\u03c0\u03bf\u03c2/\u03ba\u03bb\u03ac\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03bf\u03c1\u03b9\u03c3\u03bc\u03cc \u03c4\u03bf\u03c5 \u03b5\u03b9\u03ba\u03bf\u03bd\u03b9\u03b4\u03af\u03bf\u03c5 \u03c3\u03c4\u03bf frontend", + "headers": "\u039a\u03b5\u03c6\u03b1\u03bb\u03af\u03b4\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 \u03b9\u03c3\u03c4\u03bf\u03cd", + "index": "\u039f\u03c1\u03af\u03b6\u03b5\u03b9 \u03c0\u03bf\u03b9\u03b1 \u03b1\u03c0\u03cc \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c0\u03bf\u03c5 \u03b5\u03c0\u03b9\u03c3\u03c4\u03c1\u03ad\u03c6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03b1 CSS \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd", + "resource": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c3\u03c4\u03bf\u03bd \u03b9\u03c3\u03c4\u03cc\u03c4\u03bf\u03c0\u03bf \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03c4\u03b7\u03bd \u03c4\u03b9\u03bc\u03ae", + "select": "\u039f\u03c1\u03af\u03b6\u03b5\u03b9 \u03c0\u03bf\u03b9\u03b1 \u03b5\u03c4\u03b9\u03ba\u03ad\u03c4\u03b1 \u03b8\u03b1 \u03b1\u03bd\u03b1\u03b6\u03b7\u03c4\u03b7\u03b8\u03b5\u03af. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03b5\u03af\u03c2 CSS \u03c4\u03bf\u03c5 Beautifulsoup \u03b3\u03b9\u03b1 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2", + "state_class": "\u0397 state_class \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2", + "value_template": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03b6\u03b5\u03b9 \u03ad\u03bd\u03b1 \u03c0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03b1\u03bb\u03b1\u03b2\u03ae \u03c4\u03b7\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1", + "verify_ssl": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af/\u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03b7\u03bd \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd SSL/TLS, \u03c0.\u03c7. \u03b1\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c5\u03c4\u03bf-\u03c5\u03c0\u03bf\u03b3\u03b5\u03b3\u03c1\u03b1\u03bc\u03bc\u03ad\u03bd\u03bf." + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "attribute": "\u03a7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc", + "authentication": "\u0395\u03bb\u03ad\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "device_class": "\u039a\u03bb\u03ac\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", + "headers": "\u039a\u03b5\u03c6\u03b1\u03bb\u03af\u03b4\u03b5\u03c2", + "index": "\u0394\u03b5\u03af\u03ba\u03c4\u03b7\u03c2", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "password": "\u039a\u03b5\u03bd\u03cc", + "resource": "\u03a0\u03cc\u03c1\u03bf\u03c2", + "select": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae", + "state_class": "\u039a\u03bb\u03ac\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2", + "unit_of_measurement": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2", + "username": "\u039a\u03b5\u03bd\u03cc", + "value_template": "\u03a0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf \u03c4\u03b9\u03bc\u03ae\u03c2", + "verify_ssl": "\u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc SSL" + }, + "data_description": { + "attribute": "\u039b\u03ae\u03c8\u03b7 \u03c4\u03b7\u03c2 \u03c4\u03b9\u03bc\u03ae\u03c2 \u03b5\u03bd\u03cc\u03c2 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b5\u03c4\u03b9\u03ba\u03ad\u03c4\u03b1", + "authentication": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 HTTP. \u0395\u03af\u03c4\u03b5 \u03b2\u03b1\u03c3\u03b9\u03ba\u03cc\u03c2 \u03b5\u03af\u03c4\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2", + "device_class": "\u039f \u03c4\u03cd\u03c0\u03bf\u03c2/\u03ba\u03bb\u03ac\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03bf\u03c1\u03b9\u03c3\u03bc\u03cc \u03c4\u03bf\u03c5 \u03b5\u03b9\u03ba\u03bf\u03bd\u03b9\u03b4\u03af\u03bf\u03c5 \u03c3\u03c4\u03bf frontend", + "headers": "\u039a\u03b5\u03c6\u03b1\u03bb\u03af\u03b4\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 \u03b9\u03c3\u03c4\u03bf\u03cd", + "index": "\u039f\u03c1\u03af\u03b6\u03b5\u03b9 \u03c0\u03bf\u03b9\u03b1 \u03b1\u03c0\u03cc \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c0\u03bf\u03c5 \u03b5\u03c0\u03b9\u03c3\u03c4\u03c1\u03ad\u03c6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03b1 CSS \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd", + "resource": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c3\u03c4\u03bf\u03bd \u03b9\u03c3\u03c4\u03cc\u03c4\u03bf\u03c0\u03bf \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03c4\u03b7\u03bd \u03c4\u03b9\u03bc\u03ae", + "select": "\u039f\u03c1\u03af\u03b6\u03b5\u03b9 \u03c0\u03bf\u03b9\u03b1 \u03b5\u03c4\u03b9\u03ba\u03ad\u03c4\u03b1 \u03b8\u03b1 \u03b1\u03bd\u03b1\u03b6\u03b7\u03c4\u03b7\u03b8\u03b5\u03af. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03b5\u03af\u03c2 CSS \u03c4\u03bf\u03c5 Beautifulsoup \u03b3\u03b9\u03b1 \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2", + "state_class": "\u0397 state_class \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1", + "value_template": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03b6\u03b5\u03b9 \u03ad\u03bd\u03b1 \u03c0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03b1\u03bb\u03b1\u03b2\u03ae \u03c4\u03b7\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1", + "verify_ssl": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af/\u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03b7\u03bd \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd SSL/TLS, \u03c0.\u03c7. \u03b1\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c5\u03c4\u03bf-\u03c5\u03c0\u03bf\u03b3\u03b5\u03b3\u03c1\u03b1\u03bc\u03bc\u03ad\u03bd\u03bf." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/fr.json b/homeassistant/components/scrape/translations/fr.json new file mode 100644 index 00000000000..f68ce5808b7 --- /dev/null +++ b/homeassistant/components/scrape/translations/fr.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + }, + "step": { + "user": { + "data": { + "attribute": "Attribut", + "authentication": "Authentification", + "device_class": "Classe d'appareil", + "headers": "En-t\u00eates", + "index": "Index", + "name": "Nom", + "password": "Mot de passe", + "resource": "Ressource", + "select": "S\u00e9lectionner", + "state_class": "Classe d'\u00e9tat", + "unit_of_measurement": "Unit\u00e9 de mesure", + "username": "Nom d'utilisateur", + "value_template": "Mod\u00e8le de valeur", + "verify_ssl": "V\u00e9rifier le certificat SSL" + }, + "data_description": { + "attribute": "Obtenir la valeur d'un attribut de la balise s\u00e9lectionn\u00e9e", + "authentication": "M\u00e9thode d'authentification HTTP. \u00ab\u00a0basic\u00a0\u00bb ou \u00ab\u00a0digest\u00a0\u00bb", + "device_class": "Le type (la classe) du capteur qui d\u00e9finira l'ic\u00f4ne dans l'interface", + "headers": "Les en-t\u00eates \u00e0 utiliser pour la requ\u00eate Web", + "index": "D\u00e9finit l'\u00e9l\u00e9ment \u00e0 utiliser parmi ceux renvoy\u00e9s par le s\u00e9lecteur CSS", + "resource": "L'URL du site web qui contient la valeur", + "select": "D\u00e9finit la balise \u00e0 rechercher. Consultez les s\u00e9lecteurs CSS de Beautifulsoup pour plus de d\u00e9tails", + "state_class": "La state_class du capteur", + "value_template": "D\u00e9finit un mod\u00e8le pour obtenir l'\u00e9tat du capteur", + "verify_ssl": "Active ou d\u00e9sactive la v\u00e9rification du certificat SSL/TLS, par exemple s'il est auto-sign\u00e9" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "attribute": "Attribut", + "authentication": "Authentification", + "device_class": "Classe d'appareil", + "headers": "En-t\u00eates", + "index": "Index", + "name": "Nom", + "password": "Mot de passe", + "resource": "Ressource", + "select": "S\u00e9lectionner", + "state_class": "Classe d'\u00e9tat", + "unit_of_measurement": "Unit\u00e9 de mesure", + "username": "Nom d'utilisateur", + "value_template": "Mod\u00e8le de valeur", + "verify_ssl": "V\u00e9rifier le certificat SSL" + }, + "data_description": { + "attribute": "Obtenir la valeur d'un attribut de la balise s\u00e9lectionn\u00e9e", + "authentication": "M\u00e9thode d'authentification HTTP. \u00ab\u00a0basic\u00a0\u00bb ou \u00ab\u00a0digest\u00a0\u00bb", + "device_class": "Le type (la classe) du capteur qui d\u00e9finira l'ic\u00f4ne dans l'interface", + "headers": "Les en-t\u00eates \u00e0 utiliser pour la requ\u00eate Web", + "index": "D\u00e9finit l'\u00e9l\u00e9ment \u00e0 utiliser parmi ceux renvoy\u00e9s par le s\u00e9lecteur CSS", + "resource": "L'URL du site web qui contient la valeur", + "select": "D\u00e9finit la balise \u00e0 rechercher. Consultez les s\u00e9lecteurs CSS de Beautifulsoup pour plus de d\u00e9tails", + "state_class": "La state_class du capteur", + "value_template": "D\u00e9finit un mod\u00e8le pour obtenir l'\u00e9tat du capteur", + "verify_ssl": "Active ou d\u00e9sactive la v\u00e9rification du certificat SSL/TLS, par exemple s'il est auto-sign\u00e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/hu.json b/homeassistant/components/scrape/translations/hu.json new file mode 100644 index 00000000000..7af59751b98 --- /dev/null +++ b/homeassistant/components/scrape/translations/hu.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "user": { + "data": { + "attribute": "Attrib\u00fatum", + "authentication": "Hiteles\u00edt\u00e9s", + "device_class": "Eszk\u00f6zoszt\u00e1ly", + "headers": "Fejl\u00e9cek", + "index": "Index", + "name": "Elnevez\u00e9s", + "password": "Jelsz\u00f3", + "resource": "Er\u0151forr\u00e1s", + "select": "Kiv\u00e1laszt\u00e1s", + "state_class": "\u00c1llapotoszt\u00e1ly", + "unit_of_measurement": "M\u00e9rt\u00e9kegys\u00e9g", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "value_template": "\u00c9rt\u00e9ksablon", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + }, + "data_description": { + "attribute": "Egy attrib\u00fatum \u00e9rt\u00e9k\u00e9nek lek\u00e9r\u00e9se a kiv\u00e1lasztott c\u00edmk\u00e9n", + "authentication": "A HTTP-hiteles\u00edt\u00e9s t\u00edpusa. Basic vagy digest", + "device_class": "Az \u00e9rz\u00e9kel\u0151 t\u00edpusa/oszt\u00e1lya az ikonnak a kezl\u0151fel\u00fcleten val\u00f3 be\u00e1ll\u00edt\u00e1s\u00e1hoz", + "headers": "A webes k\u00e9r\u00e9shez haszn\u00e1land\u00f3 fejl\u00e9cek", + "index": "Meghat\u00e1rozza, hogy a CSS-v\u00e1laszt\u00f3 \u00e1ltal visszaadott elemek k\u00f6z\u00fcl melyiket haszn\u00e1lja.", + "resource": "Az \u00e9rt\u00e9ket tartalmaz\u00f3 weboldal URL c\u00edme", + "select": "Meghat\u00e1rozza, hogy milyen c\u00edmk\u00e9t keressen. N\u00e9zze meg a Beautifulsoup CSS szelektorokat a r\u00e9szletek\u00e9rt", + "state_class": "Az \u00e9rz\u00e9kel\u0151 \u00e1llapot oszt\u00e1lya", + "value_template": "Meghat\u00e1roz egy sablont az \u00e9rz\u00e9kel\u0151 \u00e1llapot\u00e1nak lek\u00e9rdez\u00e9s\u00e9re.", + "verify_ssl": "Az SSL/TLS tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9s\u00e9nek enged\u00e9lyez\u00e9se/letilt\u00e1sa, p\u00e9ld\u00e1ul ha saj\u00e1t al\u00e1\u00edr\u00e1s\u00fa." + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "attribute": "Attrib\u00fatum", + "authentication": "Hiteles\u00edt\u00e9s", + "device_class": "Eszk\u00f6zoszt\u00e1ly", + "headers": "Fejl\u00e9cek", + "index": "Index", + "name": "Elnevez\u00e9s", + "password": "Jelsz\u00f3", + "resource": "Er\u0151forr\u00e1s", + "select": "Kiv\u00e1laszt\u00e1s", + "state_class": "\u00c1llapotoszt\u00e1ly", + "unit_of_measurement": "M\u00e9rt\u00e9kegys\u00e9g", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v", + "value_template": "\u00c9rt\u00e9ksablon", + "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" + }, + "data_description": { + "attribute": "Egy attrib\u00fatum \u00e9rt\u00e9k\u00e9nek lek\u00e9r\u00e9se a kiv\u00e1lasztott c\u00edmk\u00e9n", + "authentication": "A HTTP-hiteles\u00edt\u00e9s t\u00edpusa. Basic vagy digest", + "device_class": "Az \u00e9rz\u00e9kel\u0151 t\u00edpusa/oszt\u00e1lya az ikonnak a kezl\u0151fel\u00fcleten val\u00f3 be\u00e1ll\u00edt\u00e1s\u00e1hoz", + "headers": "A webes k\u00e9r\u00e9shez haszn\u00e1land\u00f3 fejl\u00e9cek", + "index": "Meghat\u00e1rozza, hogy a CSS-v\u00e1laszt\u00f3 \u00e1ltal visszaadott elemek k\u00f6z\u00fcl melyiket haszn\u00e1lja.", + "resource": "Az \u00e9rt\u00e9ket tartalmaz\u00f3 weboldal URL c\u00edme", + "select": "Meghat\u00e1rozza, hogy milyen c\u00edmk\u00e9t keressen. N\u00e9zze meg a Beautifulsoup CSS szelektorokat a r\u00e9szletek\u00e9rt", + "state_class": "Az \u00e9rz\u00e9kel\u0151 \u00e1llapot oszt\u00e1lya", + "value_template": "Meghat\u00e1roz egy sablont az \u00e9rz\u00e9kel\u0151 \u00e1llapot\u00e1nak lek\u00e9rdez\u00e9s\u00e9re.", + "verify_ssl": "Az SSL/TLS tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9s\u00e9nek enged\u00e9lyez\u00e9se/letilt\u00e1sa, p\u00e9ld\u00e1ul ha saj\u00e1t al\u00e1\u00edr\u00e1s\u00fa." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/id.json b/homeassistant/components/scrape/translations/id.json index d83b07ac5c3..b6cecd6007c 100644 --- a/homeassistant/components/scrape/translations/id.json +++ b/homeassistant/components/scrape/translations/id.json @@ -6,7 +6,21 @@ "step": { "user": { "data": { - "resource": "Sumber daya" + "attribute": "Atribut", + "authentication": "Autentikasi", + "device_class": "Kelas Perangkat", + "headers": "Header", + "password": "Kata Sandi", + "resource": "Sumber daya", + "select": "Pilihan", + "unit_of_measurement": "Satuan Pengukuran", + "value_template": "T" + }, + "data_description": { + "attribute": "Dapatkan nilai atribut pada tag yang dipilih", + "authentication": "Jenis autentikasi HTTP. Salah satu dari basic atau digest", + "device_class": "Jenis/kelas sensor untuk mengatur ikon di antarmuka", + "verify_ssl": "Mengaktifkan/menonaktifkan verifikasi sertifikat SSL/TLS, misalnya jika sertifikat ditandatangani sendiri" } } } @@ -15,7 +29,11 @@ "step": { "init": { "data": { - "resource": "Sumber daya" + "resource": "Sumber daya", + "unit_of_measurement": "Satuan Pengukuran" + }, + "data_description": { + "attribute": "Dapatkan nilai atribut pada tag yang dipilih" } } } diff --git a/homeassistant/components/scrape/translations/ja.json b/homeassistant/components/scrape/translations/ja.json new file mode 100644 index 00000000000..3159fa65739 --- /dev/null +++ b/homeassistant/components/scrape/translations/ja.json @@ -0,0 +1,58 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "step": { + "user": { + "data": { + "attribute": "\u5c5e\u6027", + "authentication": "\u8a8d\u8a3c", + "device_class": "\u30c7\u30d0\u30a4\u30b9\u30af\u30e9\u30b9", + "headers": "\u30d8\u30c3\u30c0\u30fc", + "index": "\u30a4\u30f3\u30c7\u30c3\u30af\u30b9", + "name": "\u540d\u524d", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "resource": "\u30ea\u30bd\u30fc\u30b9", + "select": "\u9078\u629e", + "state_class": "\u72b6\u614b\u30af\u30e9\u30b9(State Class)", + "unit_of_measurement": "\u6e2c\u5b9a\u306e\u5358\u4f4d", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d", + "value_template": "\u5024\u306e\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" + }, + "data_description": { + "headers": "Web\u30ea\u30af\u30a8\u30b9\u30c8\u306b\u4f7f\u7528\u3059\u308b\u30d8\u30c3\u30c0\u30fc", + "select": "\u691c\u7d22\u3059\u308b\u30bf\u30b0\u3092\u5b9a\u7fa9\u3057\u307e\u3059\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001Beautifulsoup CSS selectors\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", + "state_class": "\u30bb\u30f3\u30b5\u30fc\u306e\u72b6\u614b\u30af\u30e9\u30b9(state_class)" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "attribute": "\u5c5e\u6027", + "authentication": "\u8a8d\u8a3c", + "device_class": "\u30c7\u30d0\u30a4\u30b9\u30af\u30e9\u30b9", + "headers": "\u30d8\u30c3\u30c0\u30fc", + "index": "\u30a4\u30f3\u30c7\u30c3\u30af\u30b9", + "name": "\u540d\u524d", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "resource": "\u30ea\u30bd\u30fc\u30b9", + "select": "\u9078\u629e", + "state_class": "\u72b6\u614b\u30af\u30e9\u30b9(State Class)", + "unit_of_measurement": "\u6e2c\u5b9a\u306e\u5358\u4f4d", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d", + "value_template": "\u5024\u306e\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8", + "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" + }, + "data_description": { + "headers": "Web\u30ea\u30af\u30a8\u30b9\u30c8\u306b\u4f7f\u7528\u3059\u308b\u30d8\u30c3\u30c0\u30fc", + "state_class": "\u30bb\u30f3\u30b5\u30fc\u306e\u72b6\u614b\u30af\u30e9\u30b9(state_class)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/zh-Hant.json b/homeassistant/components/scrape/translations/zh-Hant.json new file mode 100644 index 00000000000..499ca44d334 --- /dev/null +++ b/homeassistant/components/scrape/translations/zh-Hant.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "attribute": "\u5c6c\u6027", + "authentication": "\u9a57\u8b49", + "device_class": "\u88dd\u7f6e\u985e\u5225", + "headers": "Headers", + "index": "\u6307\u6578", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "resource": "\u4f86\u6e90", + "select": "\u9078\u64c7", + "state_class": "\u72c0\u614b\u985e\u5225", + "unit_of_measurement": "\u6e2c\u91cf\u55ae\u4f4d", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "value_template": "\u6578\u503c\u6a21\u677f", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + }, + "data_description": { + "attribute": "\u7372\u53d6\u6240\u9078\u6a19\u7c64\u5c6c\u6027\u6578\u503c", + "authentication": "HTTP \u9a57\u8b49\u985e\u578b\u3002\u57fa\u672c\u6216\u6458\u8981", + "device_class": "\u65bc Frontend \u4e2d\u8a2d\u5b9a\u4e4b\u50b3\u611f\u5668\u985e\u578b/\u985e\u5225\u5716\u793a", + "headers": "\u7528\u65bc Web \u8acb\u6c42\u4e4b Headers", + "index": "\u5b9a\u7fa9\u4f7f\u7528 CSS selector \u56de\u8986\u5143\u7d20", + "resource": "\u5305\u542b\u6578\u503c\u7684\u7db2\u7ad9 URL", + "select": "\u5b9a\u7fa9\u8981\u7d22\u7684\u6a19\u7c64\u3002\u53c3\u95b1 Beautifulsoup CSS selector \u4ee5\u7372\u5f97\u8a73\u7d30\u8cc7\u8a0a", + "state_class": "\u611f\u6e2c\u5668 state_class", + "value_template": "\u5b9a\u7fa9\u6a21\u677f\u4ee5\u53d6\u5f97\u611f\u6e2c\u5668\u72c0\u614b", + "verify_ssl": "\u958b\u555f/\u95dc\u9589 SSL/TLS \u9a57\u8b49\u8a8d\u8b49\uff0c\u4f8b\u5982\u81ea\u7c3d\u7ae0\u6191\u8b49" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "attribute": "\u5c6c\u6027", + "authentication": "\u9a57\u8b49", + "device_class": "\u88dd\u7f6e\u985e\u5225", + "headers": "Headers", + "index": "\u6307\u6578", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "resource": "\u4f86\u6e90", + "select": "\u9078\u64c7", + "state_class": "\u72c0\u614b\u985e\u5225", + "unit_of_measurement": "\u6e2c\u91cf\u55ae\u4f4d", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "value_template": "\u6578\u503c\u6a21\u677f", + "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" + }, + "data_description": { + "attribute": "\u7372\u53d6\u6240\u9078\u6a19\u7c64\u5c6c\u6027\u6578\u503c", + "authentication": "HTTP \u9a57\u8b49\u985e\u578b\u3002\u57fa\u672c\u6216\u6458\u8981", + "device_class": "\u65bc Frontend \u4e2d\u8a2d\u5b9a\u4e4b\u50b3\u611f\u5668\u985e\u578b/\u985e\u5225\u5716\u793a", + "headers": "\u7528\u65bc Web \u8acb\u6c42\u4e4b Headers", + "index": "\u5b9a\u7fa9\u4f7f\u7528 CSS selector \u56de\u8986\u5143\u7d20", + "resource": "\u5305\u542b\u6578\u503c\u7684\u7db2\u7ad9 URL", + "select": "\u5b9a\u7fa9\u8981\u7d22\u7684\u6a19\u7c64\u3002\u53c3\u95b1 Beautifulsoup CSS selector \u4ee5\u7372\u5f97\u8a73\u7d30\u8cc7\u8a0a", + "state_class": "\u611f\u6e2c\u5668 state_class", + "value_template": "\u5b9a\u7fa9\u6a21\u677f\u4ee5\u53d6\u5f97\u611f\u6e2c\u5668\u72c0\u614b", + "verify_ssl": "\u958b\u555f/\u95dc\u9589 SSL/TLS \u9a57\u8b49\u8a8d\u8b49\uff0c\u4f8b\u5982\u81ea\u7c3d\u7ae0\u6191\u8b49" + } + } + } + } +} \ No newline at end of file From bc22e79c7b034df3d1779038f67042ba1a053b1f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Jun 2022 15:43:23 -1000 Subject: [PATCH 1258/3516] Add a test for a complex entity filter (#73005) --- tests/helpers/test_entityfilter.py | 63 ++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/tests/helpers/test_entityfilter.py b/tests/helpers/test_entityfilter.py index 9576c7d95b6..043fb44a95a 100644 --- a/tests/helpers/test_entityfilter.py +++ b/tests/helpers/test_entityfilter.py @@ -303,3 +303,66 @@ def test_exlictly_included(): assert not filt.explicitly_excluded("switch.other") assert filt.explicitly_excluded("sensor.weather_5") assert filt.explicitly_excluded("light.kitchen") + + +def test_complex_include_exclude_filter(): + """Test a complex include exclude filter.""" + conf = { + "include": { + "domains": ["switch", "person"], + "entities": ["group.family"], + "entity_globs": [ + "sensor.*_sensor_temperature", + "sensor.*_actueel", + "sensor.*_totaal", + "sensor.calculated*", + "sensor.solaredge_*", + "sensor.speedtest*", + "sensor.teller*", + "sensor.zp*", + "binary_sensor.*_sensor_motion", + "binary_sensor.*_door", + "sensor.water_*ly", + "sensor.gas_*ly", + ], + }, + "exclude": { + "domains": [ + "alarm_control_panel", + "alert", + "automation", + "button", + "camera", + "climate", + "counter", + "cover", + "geo_location", + "group", + "input_boolean", + "input_datetime", + "input_number", + "input_select", + "input_text", + "light", + "media_player", + "number", + "proximity", + "remote", + "scene", + "script", + "sun", + "timer", + "updater", + "variable", + "weather", + "zone", + ], + "entities": [ + "sensor.solaredge_last_updatetime", + "sensor.solaredge_last_changed", + ], + "entity_globs": ["switch.*_light_level", "switch.sonos_*"], + }, + } + filt: EntityFilter = INCLUDE_EXCLUDE_FILTER_SCHEMA(conf) + assert filt("switch.espresso_keuken") is True From e98a6413768ad45bec59be633d8d4b734d8f77f8 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 4 Jun 2022 21:50:38 -0400 Subject: [PATCH 1259/3516] Refactor goalzero (#72398) --- homeassistant/components/goalzero/__init__.py | 85 ++----------------- .../components/goalzero/binary_sensor.py | 35 ++------ homeassistant/components/goalzero/const.py | 10 +-- .../components/goalzero/coordinator.py | 34 ++++++++ homeassistant/components/goalzero/entity.py | 48 +++++++++++ homeassistant/components/goalzero/sensor.py | 40 ++------- homeassistant/components/goalzero/switch.py | 39 ++------- 7 files changed, 116 insertions(+), 175 deletions(-) create mode 100644 homeassistant/components/goalzero/coordinator.py create mode 100644 homeassistant/components/goalzero/entity.py diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index e014c4780ad..ea292a651c2 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -1,70 +1,31 @@ """The Goal Zero Yeti integration.""" from __future__ import annotations -import logging - from goalzero import Yeti, exceptions from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_MODEL, CONF_HOST, CONF_NAME, Platform +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant 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 DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) - -from .const import ( - ATTRIBUTION, - DATA_KEY_API, - DATA_KEY_COORDINATOR, - DOMAIN, - MANUFACTURER, - MIN_TIME_BETWEEN_UPDATES, -) - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN +from .coordinator import GoalZeroDataUpdateCoordinator PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Goal Zero Yeti from a config entry.""" - name = entry.data[CONF_NAME] - host = entry.data[CONF_HOST] - - api = Yeti(host, async_get_clientsession(hass)) + api = Yeti(entry.data[CONF_HOST], async_get_clientsession(hass)) try: await api.init_connect() except exceptions.ConnectError as ex: raise ConfigEntryNotReady(f"Failed to connect to device: {ex}") from ex - async def async_update_data() -> None: - """Fetch data from API endpoint.""" - try: - await api.get_state() - except exceptions.ConnectError as err: - raise UpdateFailed("Failed to communicate with device") from err - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - name=name, - update_method=async_update_data, - update_interval=MIN_TIME_BETWEEN_UPDATES, - ) + coordinator = GoalZeroDataUpdateCoordinator(hass, api) await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_KEY_API: api, - DATA_KEY_COORDINATOR: coordinator, - } - + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -72,38 +33,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) return unload_ok - - -class YetiEntity(CoordinatorEntity): - """Representation of a Goal Zero Yeti entity.""" - - _attr_attribution = ATTRIBUTION - - def __init__( - self, - api: Yeti, - coordinator: DataUpdateCoordinator, - name: str, - server_unique_id: str, - ) -> None: - """Initialize a Goal Zero Yeti entity.""" - super().__init__(coordinator) - self.api = api - self._name = name - self._server_unique_id = server_unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return the device information of the entity.""" - return DeviceInfo( - connections={(dr.CONNECTION_NETWORK_MAC, self.api.sysdata["macAddress"])}, - identifiers={(DOMAIN, self._server_unique_id)}, - manufacturer=MANUFACTURER, - model=self.api.sysdata[ATTR_MODEL], - name=self._name, - sw_version=self.api.data["firmwareVersion"], - ) diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py index 56d812c5923..c4219b51e6c 100644 --- a/homeassistant/components/goalzero/binary_sensor.py +++ b/homeassistant/components/goalzero/binary_sensor.py @@ -3,22 +3,18 @@ from __future__ import annotations from typing import cast -from goalzero import Yeti - from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import YetiEntity -from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN +from .const import DOMAIN +from .entity import GoalZeroEntity PARALLEL_UPDATES = 0 @@ -51,38 +47,19 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Goal Zero Yeti sensor.""" - name = entry.data[CONF_NAME] - goalzero_data = hass.data[DOMAIN][entry.entry_id] async_add_entities( - YetiBinarySensor( - goalzero_data[DATA_KEY_API], - goalzero_data[DATA_KEY_COORDINATOR], - name, + GoalZeroBinarySensor( + hass.data[DOMAIN][entry.entry_id], description, - entry.entry_id, ) for description in BINARY_SENSOR_TYPES ) -class YetiBinarySensor(YetiEntity, BinarySensorEntity): +class GoalZeroBinarySensor(GoalZeroEntity, BinarySensorEntity): """Representation of a Goal Zero Yeti sensor.""" - def __init__( - self, - api: Yeti, - coordinator: DataUpdateCoordinator, - name: str, - description: BinarySensorEntityDescription, - server_unique_id: str, - ) -> None: - """Initialize a Goal Zero Yeti sensor.""" - super().__init__(api, coordinator, name, server_unique_id) - self.entity_description = description - self._attr_name = f"{name} {description.name}" - self._attr_unique_id = f"{server_unique_id}/{description.key}" - @property def is_on(self) -> bool: """Return True if the service is on.""" - return cast(bool, self.api.data[self.entity_description.key] == 1) + return cast(bool, self._api.data[self.entity_description.key] == 1) diff --git a/homeassistant/components/goalzero/const.py b/homeassistant/components/goalzero/const.py index fef1636005d..280a70abbf1 100644 --- a/homeassistant/components/goalzero/const.py +++ b/homeassistant/components/goalzero/const.py @@ -1,12 +1,12 @@ """Constants for the Goal Zero Yeti integration.""" -from datetime import timedelta +import logging +from typing import Final ATTRIBUTION = "Data provided by Goal Zero" ATTR_DEFAULT_ENABLED = "default_enabled" -DATA_KEY_COORDINATOR = "coordinator" -DOMAIN = "goalzero" +DOMAIN: Final = "goalzero" DEFAULT_NAME = "Yeti" -DATA_KEY_API = "api" MANUFACTURER = "Goal Zero" -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) + +LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/goalzero/coordinator.py b/homeassistant/components/goalzero/coordinator.py new file mode 100644 index 00000000000..416b420f29d --- /dev/null +++ b/homeassistant/components/goalzero/coordinator.py @@ -0,0 +1,34 @@ +"""Data update coordinator for the Goal zero integration.""" + +from datetime import timedelta + +from goalzero import Yeti, exceptions + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER + + +class GoalZeroDataUpdateCoordinator(DataUpdateCoordinator): + """Data update coordinator for the Goal zero integration.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, api: Yeti) -> None: + """Initialize the coordinator.""" + super().__init__( + hass=hass, + logger=LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=30), + ) + self.api = api + + async def _async_update_data(self) -> None: + """Fetch data from API endpoint.""" + try: + await self.api.get_state() + except exceptions.ConnectError as err: + raise UpdateFailed("Failed to communicate with device") from err diff --git a/homeassistant/components/goalzero/entity.py b/homeassistant/components/goalzero/entity.py new file mode 100644 index 00000000000..fa0d55b0d5f --- /dev/null +++ b/homeassistant/components/goalzero/entity.py @@ -0,0 +1,48 @@ +"""Entity representing a Goal Zero Yeti device.""" + +from goalzero import Yeti + +from homeassistant.const import ATTR_MODEL, CONF_NAME +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ATTRIBUTION, DOMAIN, MANUFACTURER +from .coordinator import GoalZeroDataUpdateCoordinator + + +class GoalZeroEntity(CoordinatorEntity[GoalZeroDataUpdateCoordinator]): + """Representation of a Goal Zero Yeti entity.""" + + _attr_attribution = ATTRIBUTION + + def __init__( + self, + coordinator: GoalZeroDataUpdateCoordinator, + description: EntityDescription, + ) -> None: + """Initialize a Goal Zero Yeti entity.""" + super().__init__(coordinator) + self.coordinator = coordinator + self.entity_description = description + self._attr_name = ( + f"{coordinator.config_entry.data[CONF_NAME]} {description.name}" + ) + self._attr_unique_id = f"{coordinator.config_entry.entry_id}/{description.key}" + + @property + def device_info(self) -> DeviceInfo: + """Return the device information of the entity.""" + return DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, self._api.sysdata["macAddress"])}, + identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)}, + manufacturer=MANUFACTURER, + model=self._api.sysdata[ATTR_MODEL], + name=self.coordinator.config_entry.data[CONF_NAME], + sw_version=self._api.data["firmwareVersion"], + ) + + @property + def _api(self) -> Yeti: + """Return api from coordinator.""" + return self.coordinator.api diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index 464cd7e5f31..ef95578820d 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -3,8 +3,6 @@ from __future__ import annotations from typing import cast -from goalzero import Yeti - from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -13,7 +11,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_NAME, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, @@ -28,10 +25,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import YetiEntity -from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN +from .const import DOMAIN +from .entity import GoalZeroEntity SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( @@ -139,39 +135,19 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Goal Zero Yeti sensor.""" - name = entry.data[CONF_NAME] - goalzero_data = hass.data[DOMAIN][entry.entry_id] - sensors = [ - YetiSensor( - goalzero_data[DATA_KEY_API], - goalzero_data[DATA_KEY_COORDINATOR], - name, + async_add_entities( + GoalZeroSensor( + hass.data[DOMAIN][entry.entry_id], description, - entry.entry_id, ) for description in SENSOR_TYPES - ] - async_add_entities(sensors, True) + ) -class YetiSensor(YetiEntity, SensorEntity): +class GoalZeroSensor(GoalZeroEntity, SensorEntity): """Representation of a Goal Zero Yeti sensor.""" - def __init__( - self, - api: Yeti, - coordinator: DataUpdateCoordinator, - name: str, - description: SensorEntityDescription, - server_unique_id: str, - ) -> None: - """Initialize a Goal Zero Yeti sensor.""" - super().__init__(api, coordinator, name, server_unique_id) - self._attr_name = f"{name} {description.name}" - self.entity_description = description - self._attr_unique_id = f"{server_unique_id}/{description.key}" - @property def native_value(self) -> StateType: """Return the state.""" - return cast(StateType, self.api.data[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 b45e3b0f89a..9a58cb385b6 100644 --- a/homeassistant/components/goalzero/switch.py +++ b/homeassistant/components/goalzero/switch.py @@ -3,17 +3,13 @@ from __future__ import annotations from typing import Any, cast -from goalzero import Yeti - from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import YetiEntity -from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN +from .const import DOMAIN +from .entity import GoalZeroEntity SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SwitchEntityDescription( @@ -35,50 +31,31 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Goal Zero Yeti switch.""" - name = entry.data[CONF_NAME] - goalzero_data = hass.data[DOMAIN][entry.entry_id] async_add_entities( - YetiSwitch( - goalzero_data[DATA_KEY_API], - goalzero_data[DATA_KEY_COORDINATOR], - name, + GoalZeroSwitch( + hass.data[DOMAIN][entry.entry_id], description, - entry.entry_id, ) for description in SWITCH_TYPES ) -class YetiSwitch(YetiEntity, SwitchEntity): +class GoalZeroSwitch(GoalZeroEntity, SwitchEntity): """Representation of a Goal Zero Yeti switch.""" - def __init__( - self, - api: Yeti, - coordinator: DataUpdateCoordinator, - name: str, - description: SwitchEntityDescription, - server_unique_id: str, - ) -> None: - """Initialize a Goal Zero Yeti switch.""" - super().__init__(api, coordinator, name, server_unique_id) - self.entity_description = description - self._attr_name = f"{name} {description.name}" - self._attr_unique_id = f"{server_unique_id}/{description.key}" - @property def is_on(self) -> bool: """Return state of the switch.""" - return cast(bool, self.api.data[self.entity_description.key] == 1) + return cast(bool, self._api.data[self.entity_description.key] == 1) 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) + await self._api.post_state(payload=payload) self.coordinator.async_set_updated_data(data=payload) 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) + await self._api.post_state(payload=payload) self.coordinator.async_set_updated_data(data=payload) From cbea919c3ddd3fd3ad27fc10b5b42ab2e33d0f31 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Jun 2022 16:34:04 -1000 Subject: [PATCH 1260/3516] Bump aiolookup to 0.1.1 (#73048) --- 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 7cf70540372..b58eb254f8f 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", "@bdraco"], - "requirements": ["aiolookin==0.1.0"], + "requirements": ["aiolookin==0.1.1"], "zeroconf": ["_lookin._tcp.local."], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index ce727d0e31c..6b8563f0510 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -187,7 +187,7 @@ aiolifx==0.8.1 aiolifx_effects==0.2.2 # homeassistant.components.lookin -aiolookin==0.1.0 +aiolookin==0.1.1 # homeassistant.components.lyric aiolyric==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d265ec19a3e..6cfd2352b8b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -159,7 +159,7 @@ aiohue==4.4.1 aiokafka==0.6.0 # homeassistant.components.lookin -aiolookin==0.1.0 +aiolookin==0.1.1 # homeassistant.components.lyric aiolyric==1.0.8 From a502a8798ff74eb6185473df7f69553fc4663634 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 4 Jun 2022 22:37:08 -0400 Subject: [PATCH 1261/3516] Add config flow to skybell (#70887) --- .coveragerc | 9 +- CODEOWNERS | 2 + homeassistant/components/skybell/__init__.py | 174 ++++++++++-------- .../components/skybell/binary_sensor.py | 89 ++++----- homeassistant/components/skybell/camera.py | 112 ++++------- .../components/skybell/config_flow.py | 76 ++++++++ homeassistant/components/skybell/const.py | 14 ++ .../components/skybell/coordinator.py | 34 ++++ homeassistant/components/skybell/entity.py | 65 +++++++ homeassistant/components/skybell/light.py | 89 ++++----- .../components/skybell/manifest.json | 7 +- homeassistant/components/skybell/sensor.py | 59 ++---- homeassistant/components/skybell/strings.json | 21 +++ homeassistant/components/skybell/switch.py | 64 +++---- .../components/skybell/translations/en.json | 21 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 6 +- requirements_test_all.txt | 3 + tests/components/skybell/__init__.py | 30 +++ tests/components/skybell/test_config_flow.py | 137 ++++++++++++++ 20 files changed, 664 insertions(+), 349 deletions(-) create mode 100644 homeassistant/components/skybell/config_flow.py create mode 100644 homeassistant/components/skybell/const.py create mode 100644 homeassistant/components/skybell/coordinator.py create mode 100644 homeassistant/components/skybell/entity.py create mode 100644 homeassistant/components/skybell/strings.json create mode 100644 homeassistant/components/skybell/translations/en.json create mode 100644 tests/components/skybell/__init__.py create mode 100644 tests/components/skybell/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 204353ffe87..4cbddd14601 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1064,7 +1064,14 @@ omit = homeassistant/components/sisyphus/* homeassistant/components/sky_hub/* homeassistant/components/skybeacon/sensor.py - homeassistant/components/skybell/* + homeassistant/components/skybell/__init__.py + homeassistant/components/skybell/binary_sensor.py + homeassistant/components/skybell/camera.py + homeassistant/components/skybell/coordinator.py + homeassistant/components/skybell/entity.py + homeassistant/components/skybell/light.py + homeassistant/components/skybell/sensor.py + homeassistant/components/skybell/switch.py homeassistant/components/slack/__init__.py homeassistant/components/slack/notify.py homeassistant/components/sia/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index e2e9fc27b5c..59f3671c475 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -933,6 +933,8 @@ build.json @home-assistant/supervisor /tests/components/siren/ @home-assistant/core @raman325 /homeassistant/components/sisyphus/ @jkeljo /homeassistant/components/sky_hub/ @rogerselwyn +/homeassistant/components/skybell/ @tkdrob +/tests/components/skybell/ @tkdrob /homeassistant/components/slack/ @bachya @tkdrob /tests/components/slack/ @bachya @tkdrob /homeassistant/components/sleepiq/ @mfugate1 @kbickar diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py index 47e22f5b619..00c7a533590 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -1,101 +1,117 @@ """Support for the Skybell HD Doorbell.""" -import logging +from __future__ import annotations -from requests.exceptions import ConnectTimeout, HTTPError -from skybellpy import Skybell +import asyncio +import os + +from aioskybell import Skybell +from aioskybell.exceptions import SkybellAuthenticationException, SkybellException import voluptuous as vol -from homeassistant.components import persistent_notification -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_PASSWORD, - CONF_USERNAME, - __version__, -) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType -_LOGGER = logging.getLogger(__name__) - -ATTRIBUTION = "Data provided by Skybell.com" - -NOTIFICATION_ID = "skybell_notification" -NOTIFICATION_TITLE = "Skybell Sensor Setup" - -DOMAIN = "skybell" -DEFAULT_CACHEDB = "./skybell_cache.pickle" -DEFAULT_ENTITY_NAMESPACE = "skybell" - -AGENT_IDENTIFIER = f"HomeAssistant/{__version__}" +from .const import DEFAULT_CACHEDB, DOMAIN +from .coordinator import SkybellDataUpdateCoordinator CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) - }, + vol.All( + # Deprecated in Home Assistant 2022.6 + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.CAMERA, + Platform.LIGHT, + Platform.SENSOR, + Platform.SWITCH, +] -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Skybell component.""" - conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - try: - cache = hass.config.path(DEFAULT_CACHEDB) - skybell = Skybell( - username=username, - password=password, - get_devices=True, - cache_path=cache, - agent_identifier=AGENT_IDENTIFIER, +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the SkyBell component.""" + hass.data.setdefault(DOMAIN, {}) + + entry_config = {} + if DOMAIN not in config: + return True + for parameter, value in config[DOMAIN].items(): + if parameter == CONF_USERNAME: + entry_config[CONF_EMAIL] = value + else: + entry_config[parameter] = value + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=entry_config, + ) ) - hass.data[DOMAIN] = skybell - except (ConnectTimeout, HTTPError) as ex: - _LOGGER.error("Unable to connect to Skybell service: %s", str(ex)) - persistent_notification.create( - hass, - "Error: {}
" - "You will need to restart hass after fixing." - "".format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - return False + # Clean up unused cache file since we are using an account specific name + # Remove with import + def clean_cache(): + """Clean old cache filename.""" + if os.path.exists(hass.config.path(DEFAULT_CACHEDB)): + os.remove(hass.config.path(DEFAULT_CACHEDB)) + + await hass.async_add_executor_job(clean_cache) + return True -class SkybellDevice(Entity): - """A HA implementation for Skybell devices.""" +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Skybell from a config entry.""" + email = entry.data[CONF_EMAIL] + password = entry.data[CONF_PASSWORD] - def __init__(self, device): - """Initialize a sensor for Skybell device.""" - self._device = device + api = Skybell( + username=email, + password=password, + get_devices=True, + cache_path=hass.config.path(f"./skybell_{entry.unique_id}.pickle"), + session=async_get_clientsession(hass), + ) + try: + devices = await api.async_initialize() + except SkybellAuthenticationException: + return False + except SkybellException as ex: + raise ConfigEntryNotReady(f"Unable to connect to Skybell service: {ex}") from ex - def update(self): - """Update automation state.""" - self._device.refresh() + device_coordinators: list[SkybellDataUpdateCoordinator] = [ + SkybellDataUpdateCoordinator(hass, device) for device in devices + ] + await asyncio.gather( + *[ + coordinator.async_config_entry_first_refresh() + for coordinator in device_coordinators + ] + ) + hass.data[DOMAIN][entry.entry_id] = device_coordinators + hass.config_entries.async_setup_platforms(entry, PLATFORMS) - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - "device_id": self._device.device_id, - "status": self._device.status, - "location": self._device.location, - "wifi_ssid": self._device.wifi_ssid, - "wifi_status": self._device.wifi_status, - "last_check_in": self._device.last_check_in, - "motion_threshold": self._device.motion_threshold, - "video_profile": self._device.video_profile, - } + 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/skybell/binary_sensor.py b/homeassistant/components/skybell/binary_sensor.py index bf8ffcfce9d..dcb5466e479 100644 --- a/homeassistant/components/skybell/binary_sensor.py +++ b/homeassistant/components/skybell/binary_sensor.py @@ -1,9 +1,7 @@ """Binary sensor support for the Skybell HD Doorbell.""" from __future__ import annotations -from datetime import timedelta -from typing import Any - +from aioskybell.helpers import const as CONST import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -12,36 +10,33 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DEFAULT_ENTITY_NAMESPACE, DOMAIN as SKYBELL_DOMAIN, SkybellDevice +from . import DOMAIN +from .coordinator import SkybellDataUpdateCoordinator +from .entity import SkybellEntity -SCAN_INTERVAL = timedelta(seconds=10) - - -BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { - "button": BinarySensorEntityDescription( - key="device:sensor:button", +BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key="button", name="Button", device_class=BinarySensorDeviceClass.OCCUPANCY, ), - "motion": BinarySensorEntityDescription( - key="device:sensor:motion", + BinarySensorEntityDescription( + key="motion", name="Motion", device_class=BinarySensorDeviceClass.MOTION, ), -} - +) +# Deprecated in Home Assistant 2022.6 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Optional( - CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE - ): cv.string, + vol.Optional(CONF_ENTITY_NAMESPACE, default=DOMAIN): cv.string, vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( cv.ensure_list, [vol.In(BINARY_SENSOR_TYPES)] ), @@ -49,53 +44,41 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the platform for a Skybell device.""" - skybell = hass.data[SKYBELL_DOMAIN] - - binary_sensors = [ - SkybellBinarySensor(device, BINARY_SENSOR_TYPES[sensor_type]) - for device in skybell.get_devices() - for sensor_type in config[CONF_MONITORED_CONDITIONS] - ] - - add_entities(binary_sensors, True) + """Set up Skybell switch.""" + async_add_entities( + SkybellBinarySensor(coordinator, sensor) + for sensor in BINARY_SENSOR_TYPES + for coordinator in hass.data[DOMAIN][entry.entry_id] + ) -class SkybellBinarySensor(SkybellDevice, BinarySensorEntity): +class SkybellBinarySensor(SkybellEntity, BinarySensorEntity): """A binary sensor implementation for Skybell devices.""" def __init__( self, - device, + coordinator: SkybellDataUpdateCoordinator, description: BinarySensorEntityDescription, - ): + ) -> None: """Initialize a binary sensor for a Skybell device.""" - super().__init__(device) - self.entity_description = description - self._attr_name = f"{self._device.name} {description.name}" - self._event: dict[Any, Any] = {} + super().__init__(coordinator, description) + self._event: dict[str, str] = {} @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, str | int | tuple[str, str]]: """Return the state attributes.""" attrs = super().extra_state_attributes - - attrs["event_date"] = self._event.get("createdAt") - + if event := self._event.get(CONST.CREATED_AT): + attrs["event_date"] = event return attrs - def update(self): - """Get the latest data and updates the state.""" - super().update() - + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" event = self._device.latest(self.entity_description.key) - - self._attr_is_on = bool(event and event.get("id") != self._event.get("id")) - - self._event = event or {} + self._attr_is_on = bool(event.get(CONST.ID) != self._event.get(CONST.ID)) + self._event = event + super()._handle_coordinator_update() diff --git a/homeassistant/components/skybell/camera.py b/homeassistant/components/skybell/camera.py index 96989fad747..f531e67f2d0 100644 --- a/homeassistant/components/skybell/camera.py +++ b/homeassistant/components/skybell/camera.py @@ -1,31 +1,31 @@ """Camera support for the Skybell HD Doorbell.""" from __future__ import annotations -from datetime import timedelta -import logging - -import requests import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import ( + PLATFORM_SCHEMA, + Camera, + CameraEntityDescription, +) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as SKYBELL_DOMAIN, SkybellDevice - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=90) - -IMAGE_AVATAR = "avatar" -IMAGE_ACTIVITY = "activity" - -CONF_ACTIVITY_NAME = "activity_name" -CONF_AVATAR_NAME = "avatar_name" +from .const import ( + CONF_ACTIVITY_NAME, + CONF_AVATAR_NAME, + DOMAIN, + IMAGE_ACTIVITY, + IMAGE_AVATAR, +) +from .coordinator import SkybellDataUpdateCoordinator +from .entity import SkybellEntity +# Deprecated in Home Assistant 2022.6 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_MONITORED_CONDITIONS, default=[IMAGE_AVATAR]): vol.All( @@ -36,71 +36,37 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +CAMERA_TYPES: tuple[CameraEntityDescription, ...] = ( + CameraEntityDescription(key="activity", name="Last Activity"), + CameraEntityDescription(key="avatar", name="Camera"), +) -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the platform for a Skybell device.""" - cond = config[CONF_MONITORED_CONDITIONS] - names = {} - names[IMAGE_ACTIVITY] = config.get(CONF_ACTIVITY_NAME) - names[IMAGE_AVATAR] = config.get(CONF_AVATAR_NAME) - skybell = hass.data[SKYBELL_DOMAIN] - - sensors = [] - for device in skybell.get_devices(): - for camera_type in cond: - sensors.append(SkybellCamera(device, camera_type, names.get(camera_type))) - - add_entities(sensors, True) + """Set up Skybell switch.""" + async_add_entities( + SkybellCamera(coordinator, description) + for description in CAMERA_TYPES + for coordinator in hass.data[DOMAIN][entry.entry_id] + ) -class SkybellCamera(SkybellDevice, Camera): +class SkybellCamera(SkybellEntity, Camera): """A camera implementation for Skybell devices.""" - def __init__(self, device, camera_type, name=None): + def __init__( + self, + coordinator: SkybellDataUpdateCoordinator, + description: EntityDescription, + ) -> None: """Initialize a camera for a Skybell device.""" - self._type = camera_type - SkybellDevice.__init__(self, device) + super().__init__(coordinator, description) Camera.__init__(self) - if name is not None: - self._name = f"{self._device.name} {name}" - else: - self._name = self._device.name - self._url = None - self._response = None - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def image_url(self): - """Get the camera image url based on type.""" - if self._type == IMAGE_ACTIVITY: - return self._device.activity_image - return self._device.image - - def camera_image( + async def async_camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: """Get the latest camera image.""" - super().update() - - if self._url != self.image_url: - self._url = self.image_url - - try: - self._response = requests.get(self._url, stream=True, timeout=10) - except requests.HTTPError as err: - _LOGGER.warning("Failed to get camera image: %s", err) - self._response = None - - if not self._response: - return None - - return self._response.content + return self._device.images[self.entity_description.key] diff --git a/homeassistant/components/skybell/config_flow.py b/homeassistant/components/skybell/config_flow.py new file mode 100644 index 00000000000..7b7b43788b3 --- /dev/null +++ b/homeassistant/components/skybell/config_flow.py @@ -0,0 +1,76 @@ +"""Config flow for Skybell integration.""" +from __future__ import annotations + +from typing import Any + +from aioskybell import Skybell, exceptions +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType + +from .const import DOMAIN + + +class SkybellFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Skybell.""" + + async def async_step_import(self, user_input: ConfigType) -> FlowResult: + """Import a config entry from configuration.yaml.""" + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + return await self.async_step_user(user_input) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by the user.""" + errors = {} + + if user_input is not None: + email = user_input[CONF_EMAIL].lower() + password = user_input[CONF_PASSWORD] + + self._async_abort_entries_match({CONF_EMAIL: email}) + user_id, error = await self._async_validate_input(email, password) + if error is None: + await self.async_set_unique_id(user_id) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=email, + data={CONF_EMAIL: email, CONF_PASSWORD: password}, + ) + errors["base"] = error + + user_input = user_input or {} + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_EMAIL, default=user_input.get(CONF_EMAIL)): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) + + async def _async_validate_input(self, email: str, password: str) -> tuple: + """Validate login credentials.""" + skybell = Skybell( + username=email, + password=password, + disable_cache=True, + session=async_get_clientsession(self.hass), + ) + try: + await skybell.async_initialize() + except exceptions.SkybellAuthenticationException: + return None, "invalid_auth" + except exceptions.SkybellException: + return None, "cannot_connect" + except Exception: # pylint: disable=broad-except + return None, "unknown" + return skybell.user_id, None diff --git a/homeassistant/components/skybell/const.py b/homeassistant/components/skybell/const.py new file mode 100644 index 00000000000..d8f7e4992d5 --- /dev/null +++ b/homeassistant/components/skybell/const.py @@ -0,0 +1,14 @@ +"""Constants for the Skybell HD Doorbell.""" +import logging +from typing import Final + +CONF_ACTIVITY_NAME = "activity_name" +CONF_AVATAR_NAME = "avatar_name" +DEFAULT_CACHEDB = "./skybell_cache.pickle" +DEFAULT_NAME = "SkyBell" +DOMAIN: Final = "skybell" + +IMAGE_AVATAR = "avatar" +IMAGE_ACTIVITY = "activity" + +LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/skybell/coordinator.py b/homeassistant/components/skybell/coordinator.py new file mode 100644 index 00000000000..26545609bd5 --- /dev/null +++ b/homeassistant/components/skybell/coordinator.py @@ -0,0 +1,34 @@ +"""Data update coordinator for the Skybell integration.""" + +from datetime import timedelta + +from aioskybell import SkybellDevice, SkybellException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import LOGGER + + +class SkybellDataUpdateCoordinator(DataUpdateCoordinator): + """Data update coordinator for the Skybell integration.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, device: SkybellDevice) -> None: + """Initialize the coordinator.""" + super().__init__( + hass=hass, + logger=LOGGER, + name=device.name, + update_interval=timedelta(seconds=30), + ) + self.device = device + + async def _async_update_data(self) -> None: + """Fetch data from API endpoint.""" + try: + await self.device.async_update() + except SkybellException as err: + raise UpdateFailed(f"Failed to communicate with device: {err}") from err diff --git a/homeassistant/components/skybell/entity.py b/homeassistant/components/skybell/entity.py new file mode 100644 index 00000000000..cf728cde069 --- /dev/null +++ b/homeassistant/components/skybell/entity.py @@ -0,0 +1,65 @@ +"""Entity representing a Skybell HD Doorbell.""" +from __future__ import annotations + +from aioskybell import SkybellDevice + +from homeassistant.const import ATTR_CONNECTIONS +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DEFAULT_NAME, DOMAIN +from .coordinator import SkybellDataUpdateCoordinator + + +class SkybellEntity(CoordinatorEntity[SkybellDataUpdateCoordinator]): + """An HA implementation for Skybell entity.""" + + _attr_attribution = "Data provided by Skybell.com" + + def __init__( + self, coordinator: SkybellDataUpdateCoordinator, description: EntityDescription + ) -> None: + """Initialize a SkyBell entity.""" + super().__init__(coordinator) + self.entity_description = description + if description.name != coordinator.device.name: + self._attr_name = f"{self._device.name} {description.name}" + self._attr_unique_id = f"{self._device.device_id}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._device.device_id)}, + manufacturer=DEFAULT_NAME, + model=self._device.type, + name=self._device.name, + sw_version=self._device.firmware_ver, + ) + if self._device.mac: + self._attr_device_info[ATTR_CONNECTIONS] = { + (dr.CONNECTION_NETWORK_MAC, self._device.mac) + } + + @property + def _device(self) -> SkybellDevice: + """Return the device.""" + return self.coordinator.device + + @property + def extra_state_attributes(self) -> dict[str, str | int | tuple[str, str]]: + """Return the state attributes.""" + attr: dict[str, str | int | tuple[str, str]] = { + "device_id": self._device.device_id, + "status": self._device.status, + "location": self._device.location, + "motion_threshold": self._device.motion_threshold, + "video_profile": self._device.video_profile, + } + if self._device.owner: + attr["wifi_ssid"] = self._device.wifi_ssid + attr["wifi_status"] = self._device.wifi_status + attr["last_check_in"] = self._device.last_check_in + return attr + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() diff --git a/homeassistant/components/skybell/light.py b/homeassistant/components/skybell/light.py index 7fbd1519e26..845be44a34b 100644 --- a/homeassistant/components/skybell/light.py +++ b/homeassistant/components/skybell/light.py @@ -1,82 +1,63 @@ """Light/LED support for the Skybell HD Doorbell.""" from __future__ import annotations +from typing import Any + from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_HS_COLOR, + ATTR_RGB_COLOR, ColorMode, LightEntity, + LightEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -import homeassistant.util.color as color_util -from . import DOMAIN as SKYBELL_DOMAIN, SkybellDevice +from .const import DOMAIN +from .entity import SkybellEntity -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the platform for a Skybell device.""" - skybell = hass.data[SKYBELL_DOMAIN] - - sensors = [] - for device in skybell.get_devices(): - sensors.append(SkybellLight(device)) - - add_entities(sensors, True) + """Set up Skybell switch.""" + async_add_entities( + SkybellLight( + coordinator, + LightEntityDescription( + key=coordinator.device.name, + name=coordinator.device.name, + ), + ) + for coordinator in hass.data[DOMAIN][entry.entry_id] + ) -def _to_skybell_level(level): - """Convert the given Home Assistant light level (0-255) to Skybell (0-100).""" - return int((level * 100) / 255) +class SkybellLight(SkybellEntity, LightEntity): + """A light implementation for Skybell devices.""" + _attr_supported_color_modes = {ColorMode.BRIGHTNESS, ColorMode.RGB} -def _to_hass_level(level): - """Convert the given Skybell (0-100) light level to Home Assistant (0-255).""" - return int((level * 255) / 100) - - -class SkybellLight(SkybellDevice, LightEntity): - """A binary sensor implementation for Skybell devices.""" - - _attr_color_mode = ColorMode.HS - _attr_supported_color_modes = {ColorMode.HS} - - def __init__(self, device): - """Initialize a light for a Skybell device.""" - super().__init__(device) - self._attr_name = device.name - - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" - if ATTR_HS_COLOR in kwargs: - rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) - self._device.led_rgb = rgb - elif ATTR_BRIGHTNESS in kwargs: - self._device.led_intensity = _to_skybell_level(kwargs[ATTR_BRIGHTNESS]) - else: - self._device.led_intensity = _to_skybell_level(255) + if ATTR_RGB_COLOR in kwargs: + rgb = kwargs[ATTR_RGB_COLOR] + await self._device.async_set_setting(ATTR_RGB_COLOR, rgb) + if ATTR_BRIGHTNESS in kwargs: + level = int((kwargs.get(ATTR_BRIGHTNESS, 0) * 100) / 255) + await self._device.async_set_setting(ATTR_BRIGHTNESS, level) - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" - self._device.led_intensity = 0 + await self._device.async_set_setting(ATTR_BRIGHTNESS, 0) @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._device.led_intensity > 0 @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of the light.""" - return _to_hass_level(self._device.led_intensity) - - @property - def hs_color(self): - """Return the color of the light.""" - return color_util.color_RGB_to_hs(*self._device.led_rgb) + return int((self._device.led_intensity * 255) / 100) diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index ce166179969..335ff2615f8 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -1,9 +1,10 @@ { "domain": "skybell", "name": "SkyBell", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/skybell", - "requirements": ["skybellpy==0.6.3"], - "codeowners": [], + "requirements": ["aioskybell==22.3.0"], + "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", - "loggers": ["skybellpy"] + "loggers": ["aioskybell"] } diff --git a/homeassistant/components/skybell/sensor.py b/homeassistant/components/skybell/sensor.py index 5922bb05382..c769570b10c 100644 --- a/homeassistant/components/skybell/sensor.py +++ b/homeassistant/components/skybell/sensor.py @@ -1,8 +1,6 @@ """Sensor support for Skybell Doorbells.""" from __future__ import annotations -from datetime import timedelta - import voluptuous as vol from homeassistant.components.sensor import ( @@ -10,15 +8,13 @@ from homeassistant.components.sensor import ( SensorEntity, SensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DEFAULT_ENTITY_NAMESPACE, DOMAIN as SKYBELL_DOMAIN, SkybellDevice - -SCAN_INTERVAL = timedelta(seconds=30) +from .entity import DOMAIN, SkybellEntity SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( @@ -27,14 +23,13 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( icon="mdi:bell-ring", ), ) -MONITORED_CONDITIONS: list[str] = [desc.key for desc in SENSOR_TYPES] +MONITORED_CONDITIONS = SENSOR_TYPES +# Deprecated in Home Assistant 2022.6 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Optional( - CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE - ): cv.string, + vol.Optional(CONF_ENTITY_NAMESPACE, default=DOMAIN): cv.string, vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( cv.ensure_list, [vol.In(MONITORED_CONDITIONS)] ), @@ -42,41 +37,21 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the platform for a Skybell device.""" - skybell = hass.data[SKYBELL_DOMAIN] - - sensors = [ - SkybellSensor(device, description) - for device in skybell.get_devices() + """Set up Skybell sensor.""" + async_add_entities( + SkybellSensor(coordinator, description) + for coordinator in hass.data[DOMAIN][entry.entry_id] for description in SENSOR_TYPES - if description.key in config[CONF_MONITORED_CONDITIONS] - ] - - add_entities(sensors, True) + ) -class SkybellSensor(SkybellDevice, SensorEntity): +class SkybellSensor(SkybellEntity, SensorEntity): """A sensor implementation for Skybell devices.""" - def __init__( - self, - device, - description: SensorEntityDescription, - ): - """Initialize a sensor for a Skybell device.""" - super().__init__(device) - self.entity_description = description - self._attr_name = f"{self._device.name} {description.name}" - - def update(self): - """Get the latest data and updates the state.""" - super().update() - - if self.entity_description.key == "chime_level": - self._attr_native_value = self._device.outdoor_chime_level + @property + def native_value(self) -> int: + """Return the state of the sensor.""" + return self._device.outdoor_chime_level diff --git a/homeassistant/components/skybell/strings.json b/homeassistant/components/skybell/strings.json new file mode 100644 index 00000000000..e48a75c12bd --- /dev/null +++ b/homeassistant/components/skybell/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "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_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + } +} diff --git a/homeassistant/components/skybell/switch.py b/homeassistant/components/skybell/switch.py index 2873ad2c081..d28369e40b0 100644 --- a/homeassistant/components/skybell/switch.py +++ b/homeassistant/components/skybell/switch.py @@ -1,6 +1,8 @@ """Switch support for the Skybell HD Doorbell.""" from __future__ import annotations +from typing import Any, cast + import voluptuous as vol from homeassistant.components.switch import ( @@ -8,13 +10,14 @@ from homeassistant.components.switch import ( SwitchEntity, SwitchEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DEFAULT_ENTITY_NAMESPACE, DOMAIN as SKYBELL_DOMAIN, SkybellDevice +from .const import DOMAIN +from .entity import SkybellEntity SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SwitchEntityDescription( @@ -26,62 +29,41 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( name="Motion Sensor", ), ) -MONITORED_CONDITIONS: list[str] = [desc.key for desc in SWITCH_TYPES] - +# Deprecated in Home Assistant 2022.6 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Optional( - CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE - ): cv.string, + vol.Optional(CONF_ENTITY_NAMESPACE, default=DOMAIN): cv.string, vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( - cv.ensure_list, [vol.In(MONITORED_CONDITIONS)] + cv.ensure_list, [vol.In(SWITCH_TYPES)] ), } ) -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the platform for a Skybell device.""" - skybell = hass.data[SKYBELL_DOMAIN] - - switches = [ - SkybellSwitch(device, description) - for device in skybell.get_devices() + """Set up the SkyBell switch.""" + async_add_entities( + SkybellSwitch(coordinator, description) + for coordinator in hass.data[DOMAIN][entry.entry_id] for description in SWITCH_TYPES - if description.key in config[CONF_MONITORED_CONDITIONS] - ] - - add_entities(switches, True) + ) -class SkybellSwitch(SkybellDevice, SwitchEntity): +class SkybellSwitch(SkybellEntity, SwitchEntity): """A switch implementation for Skybell devices.""" - def __init__( - self, - device, - description: SwitchEntityDescription, - ): - """Initialize a light for a Skybell device.""" - super().__init__(device) - self.entity_description = description - self._attr_name = f"{self._device.name} {description.name}" - - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" - setattr(self._device, self.entity_description.key, True) + await self._device.async_set_setting(self.entity_description.key, True) - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" - setattr(self._device, self.entity_description.key, False) + await self._device.async_set_setting(self.entity_description.key, False) @property - def is_on(self): - """Return true if device is on.""" - return getattr(self._device, self.entity_description.key) + def is_on(self) -> bool: + """Return true if entity is on.""" + return cast(bool, getattr(self._device, self.entity_description.key)) diff --git a/homeassistant/components/skybell/translations/en.json b/homeassistant/components/skybell/translations/en.json new file mode 100644 index 00000000000..b84c4ebc999 --- /dev/null +++ b/homeassistant/components/skybell/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "Email", + "password": "Password" + } + } + }, + "error": { + "invalid_auth": "Invalid authentication", + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e1e2938c9ff..06da1eae90c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -311,6 +311,7 @@ FLOWS = { "shopping_list", "sia", "simplisafe", + "skybell", "slack", "sleepiq", "slimproto", diff --git a/requirements_all.txt b/requirements_all.txt index 6b8563f0510..e74f72d75dc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -240,6 +240,9 @@ aiosenz==1.0.0 # homeassistant.components.shelly aioshelly==2.0.0 +# homeassistant.components.skybell +aioskybell==22.3.0 + # homeassistant.components.slimproto aioslimproto==2.0.1 @@ -2173,9 +2176,6 @@ simplisafe-python==2022.05.2 # homeassistant.components.sisyphus sisyphus-control==3.1.2 -# homeassistant.components.skybell -skybellpy==0.6.3 - # homeassistant.components.slack slackclient==2.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6cfd2352b8b..f68ea51ad97 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -209,6 +209,9 @@ aiosenz==1.0.0 # homeassistant.components.shelly aioshelly==2.0.0 +# homeassistant.components.skybell +aioskybell==22.3.0 + # homeassistant.components.slimproto aioslimproto==2.0.1 diff --git a/tests/components/skybell/__init__.py b/tests/components/skybell/__init__.py new file mode 100644 index 00000000000..dd162ed5d80 --- /dev/null +++ b/tests/components/skybell/__init__.py @@ -0,0 +1,30 @@ +"""Tests for the SkyBell integration.""" + +from unittest.mock import AsyncMock, patch + +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + +USERNAME = "user" +PASSWORD = "password" +USER_ID = "123456789012345678901234" + +CONF_CONFIG_FLOW = { + CONF_EMAIL: USERNAME, + CONF_PASSWORD: PASSWORD, +} + + +def _patch_skybell_devices() -> None: + mocked_skybell = AsyncMock() + mocked_skybell.user_id = USER_ID + return patch( + "homeassistant.components.skybell.config_flow.Skybell.async_get_devices", + return_value=[mocked_skybell], + ) + + +def _patch_skybell() -> None: + return patch( + "homeassistant.components.skybell.config_flow.Skybell.async_send_request", + return_value={"id": USER_ID}, + ) diff --git a/tests/components/skybell/test_config_flow.py b/tests/components/skybell/test_config_flow.py new file mode 100644 index 00000000000..0171a522e50 --- /dev/null +++ b/tests/components/skybell/test_config_flow.py @@ -0,0 +1,137 @@ +"""Test SkyBell config flow.""" +from unittest.mock import patch + +from aioskybell import exceptions + +from homeassistant.components.skybell.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import CONF_CONFIG_FLOW, _patch_skybell, _patch_skybell_devices + +from tests.common import MockConfigEntry + + +def _patch_setup_entry() -> None: + return patch( + "homeassistant.components.skybell.async_setup_entry", + return_value=True, + ) + + +def _patch_setup() -> None: + return patch( + "homeassistant.components.skybell.async_setup", + return_value=True, + ) + + +async def test_flow_user(hass: HomeAssistant) -> None: + """Test that the user step works.""" + with _patch_skybell(), _patch_skybell_devices(), _patch_setup_entry(), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"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_CONFIG_FLOW, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "user" + assert result["data"] == CONF_CONFIG_FLOW + + +async def test_flow_user_already_configured(hass: HomeAssistant) -> None: + """Test user initialized flow with duplicate server.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=CONF_CONFIG_FLOW, + ) + + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: + """Test user initialized flow with unreachable server.""" + with _patch_skybell() as skybell_mock: + skybell_mock.side_effect = exceptions.SkybellException(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_invalid_credentials(hass: HomeAssistant) -> None: + """Test that invalid credentials throws an error.""" + with patch("homeassistant.components.skybell.Skybell.async_login") as skybell_mock: + skybell_mock.side_effect = exceptions.SkybellAuthenticationException(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: + """Test user initialized flow with unreachable server.""" + with _patch_skybell_devices() as skybell_mock: + skybell_mock.side_effect = Exception + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "unknown"} + + +async def test_flow_import(hass: HomeAssistant) -> None: + """Test import step.""" + with _patch_skybell(), _patch_skybell_devices(), _patch_setup_entry(), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_CONFIG_FLOW, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "user" + assert result["data"] == CONF_CONFIG_FLOW + + +async def test_flow_import_already_configured(hass: HomeAssistant) -> None: + """Test import step already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, unique_id="123456789012345678901234", data=CONF_CONFIG_FLOW + ) + + entry.add_to_hass(hass) + + with _patch_skybell(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" From 395d58840ca3ceb057a73cd7ed8220d3710b7386 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Sun, 5 Jun 2022 13:42:21 +1000 Subject: [PATCH 1262/3516] Add Hunter Douglas Powerview Diagnostics (#72918) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + .../hunterdouglas_powerview/__init__.py | 8 +- .../hunterdouglas_powerview/diagnostics.py | 108 ++++++++++++++++++ .../hunterdouglas_powerview/shade_data.py | 4 + 4 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/hunterdouglas_powerview/diagnostics.py diff --git a/.coveragerc b/.coveragerc index 4cbddd14601..f4cdfa1c460 100644 --- a/.coveragerc +++ b/.coveragerc @@ -506,6 +506,7 @@ omit = homeassistant/components/hue/light.py homeassistant/components/hunterdouglas_powerview/__init__.py homeassistant/components/hunterdouglas_powerview/coordinator.py + homeassistant/components/hunterdouglas_powerview/diagnostics.py homeassistant/components/hunterdouglas_powerview/cover.py homeassistant/components/hunterdouglas_powerview/entity.py homeassistant/components/hunterdouglas_powerview/scene.py diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 587bf8aef12..88aa5214c9b 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -87,9 +87,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async with async_timeout.timeout(10): shades = Shades(pv_request) - shade_data = async_map_data_by_id( - (await shades.get_resources())[SHADE_DATA] - ) + shade_entries = await shades.get_resources() + shade_data = async_map_data_by_id(shade_entries[SHADE_DATA]) + except HUB_EXCEPTIONS as err: raise ConfigEntryNotReady( f"Connection error to PowerView hub: {hub_address}: {err}" @@ -99,6 +99,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = PowerviewShadeUpdateCoordinator(hass, shades, hub_address) coordinator.async_set_updated_data(PowerviewShadeData()) + # populate raw shade data into the coordinator for diagnostics + coordinator.data.store_group_data(shade_entries[SHADE_DATA]) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { PV_API: pv_request, diff --git a/homeassistant/components/hunterdouglas_powerview/diagnostics.py b/homeassistant/components/hunterdouglas_powerview/diagnostics.py new file mode 100644 index 00000000000..1b2887c3af2 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/diagnostics.py @@ -0,0 +1,108 @@ +"""Diagnostics support for Powerview Hunter Douglas.""" +from __future__ import annotations + +from typing import Any + +import attr + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.device_registry import DeviceEntry + +from .const import ( + COORDINATOR, + DEVICE_INFO, + DEVICE_MAC_ADDRESS, + DEVICE_SERIAL_NUMBER, + DOMAIN, + PV_HUB_ADDRESS, +) +from .coordinator import PowerviewShadeUpdateCoordinator + +REDACT_CONFIG = { + CONF_HOST, + DEVICE_MAC_ADDRESS, + DEVICE_SERIAL_NUMBER, + PV_HUB_ADDRESS, + "configuration_url", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + data = _async_get_diagnostics(hass, entry) + device_registry = dr.async_get(hass) + data.update( + device_info=[ + _async_device_as_dict(hass, device) + for device in dr.async_entries_for_config_entry( + device_registry, entry.entry_id + ) + ], + ) + return data + + +async def async_get_device_diagnostics( + hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry +) -> dict[str, Any]: + """Return diagnostics for a device entry.""" + data = _async_get_diagnostics(hass, entry) + data["device_info"] = _async_device_as_dict(hass, device) + # try to match on name to restrict to shade if we can + # otherwise just return all shade data + # shade name is unique in powerview + shade_data = data["shade_data"] + for shade in shade_data: + if shade_data[shade]["name_unicode"] == device.name: + data["shade_data"] = shade_data[shade] + return data + + +@callback +def _async_get_diagnostics( + hass: HomeAssistant, + entry: ConfigEntry, +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + pv_data = hass.data[DOMAIN][entry.entry_id] + coordinator: PowerviewShadeUpdateCoordinator = pv_data[COORDINATOR] + shade_data = coordinator.data.get_all_raw_data() + hub_info = async_redact_data(pv_data[DEVICE_INFO], REDACT_CONFIG) + return {"hub_info": hub_info, "shade_data": shade_data} + + +@callback +def _async_device_as_dict(hass: HomeAssistant, device: DeviceEntry) -> dict[str, Any]: + """Represent a Powerview device as a dictionary.""" + + # Gather information how this device is represented in Home Assistant + entity_registry = er.async_get(hass) + + data = async_redact_data(attr.asdict(device), REDACT_CONFIG) + data["entities"] = [] + entities: list[dict[str, Any]] = data["entities"] + + entries = er.async_entries_for_device( + entity_registry, + device_id=device.id, + include_disabled_entities=True, + ) + + for entity_entry in entries: + state = hass.states.get(entity_entry.entity_id) + state_dict = None + if state: + state_dict = dict(state.as_dict()) + state_dict.pop("context", None) + + entity = attr.asdict(entity_entry) + entity["state"] = state_dict + entities.append(entity) + + return data diff --git a/homeassistant/components/hunterdouglas_powerview/shade_data.py b/homeassistant/components/hunterdouglas_powerview/shade_data.py index 4a7b7be0945..b66024aec7f 100644 --- a/homeassistant/components/hunterdouglas_powerview/shade_data.py +++ b/homeassistant/components/hunterdouglas_powerview/shade_data.py @@ -59,6 +59,10 @@ class PowerviewShadeData: """Get data for the shade.""" return self._group_data_by_id[shade_id] + def get_all_raw_data(self) -> dict[int, dict[str | int, Any]]: + """Get data for all shades.""" + return self._group_data_by_id + def get_shade_positions(self, shade_id: int) -> PowerviewShadePositions: """Get positions for a shade.""" if shade_id not in self.positions: From 41f38f10991dd50dd82aa74f29214f0da9047739 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Sun, 5 Jun 2022 14:14:04 +1000 Subject: [PATCH 1263/3516] Use constant in powerview diagnostics (#73059) --- .../components/hunterdouglas_powerview/diagnostics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/diagnostics.py b/homeassistant/components/hunterdouglas_powerview/diagnostics.py index 1b2887c3af2..ca6131b2761 100644 --- a/homeassistant/components/hunterdouglas_powerview/diagnostics.py +++ b/homeassistant/components/hunterdouglas_powerview/diagnostics.py @@ -7,7 +7,7 @@ import attr from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import ATTR_CONFIGURATION_URL, CONF_HOST from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceEntry @@ -27,7 +27,7 @@ REDACT_CONFIG = { DEVICE_MAC_ADDRESS, DEVICE_SERIAL_NUMBER, PV_HUB_ADDRESS, - "configuration_url", + ATTR_CONFIGURATION_URL, } From aad3253ed18987c0aebe5a5f12b484a1d6a53895 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 5 Jun 2022 06:47:08 +0200 Subject: [PATCH 1264/3516] Bump pysensibo to 1.0.16 (#73029) --- homeassistant/components/sensibo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index c289322d584..203eb751dcf 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,7 +2,7 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": ["pysensibo==1.0.15"], + "requirements": ["pysensibo==1.0.16"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index e74f72d75dc..9ebbd7ef32b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1795,7 +1795,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.15 +pysensibo==1.0.16 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f68ea51ad97..f1e8ecd9621 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1214,7 +1214,7 @@ pyruckus==0.12 pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.15 +pysensibo==1.0.16 # homeassistant.components.serial # homeassistant.components.zha From 4c11cc3dbbcae294d3df8acf3c72193fd5ff9eca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Jun 2022 21:32:59 -1000 Subject: [PATCH 1265/3516] Additional cleanups for emulated_hue (#73004) * Additional cleanups for emulated_hue Followup to https://github.com/home-assistant/core/pull/72663#discussion_r884268731 * split long lines --- .../components/emulated_hue/config.py | 34 ++++---- .../components/emulated_hue/hue_api.py | 87 +++++++++++-------- 2 files changed, 66 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/emulated_hue/config.py b/homeassistant/components/emulated_hue/config.py index fce521eee55..c2cf67b43f4 100644 --- a/homeassistant/components/emulated_hue/config.py +++ b/homeassistant/components/emulated_hue/config.py @@ -156,49 +156,49 @@ class Config: # Google Home return self.numbers.get(number) - def get_entity_name(self, entity: State) -> str: + def get_entity_name(self, state: State) -> str: """Get the name of an entity.""" if ( - entity.entity_id in self.entities - and CONF_ENTITY_NAME in self.entities[entity.entity_id] + state.entity_id in self.entities + and CONF_ENTITY_NAME in self.entities[state.entity_id] ): - return self.entities[entity.entity_id][CONF_ENTITY_NAME] + return self.entities[state.entity_id][CONF_ENTITY_NAME] - return entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name) + return state.attributes.get(ATTR_EMULATED_HUE_NAME, state.name) - def is_entity_exposed(self, entity: State) -> bool: + def is_state_exposed(self, state: State) -> bool: """Cache determine if an entity should be exposed on the emulated bridge.""" - if (exposed := self._exposed_cache.get(entity.entity_id)) is not None: + if (exposed := self._exposed_cache.get(state.entity_id)) is not None: return exposed - exposed = self._is_entity_exposed(entity) - self._exposed_cache[entity.entity_id] = exposed + exposed = self._is_state_exposed(state) + self._exposed_cache[state.entity_id] = exposed return exposed - def filter_exposed_entities(self, states: Iterable[State]) -> list[State]: + def filter_exposed_states(self, states: Iterable[State]) -> list[State]: """Filter a list of all states down to exposed entities.""" exposed: list[State] = [ - state for state in states if self.is_entity_exposed(state) + state for state in states if self.is_state_exposed(state) ] return exposed - def _is_entity_exposed(self, entity: State) -> bool: - """Determine if an entity should be exposed on the emulated bridge. + def _is_state_exposed(self, state: State) -> bool: + """Determine if an entity state should be exposed on the emulated bridge. Async friendly. """ - if entity.attributes.get("view") is not None: + if state.attributes.get("view") is not None: # Ignore entities that are views return False - if entity.entity_id in self._entities_with_hidden_attr_in_config: - return not self._entities_with_hidden_attr_in_config[entity.entity_id] + if state.entity_id in self._entities_with_hidden_attr_in_config: + return not self._entities_with_hidden_attr_in_config[state.entity_id] if not self.expose_by_default: return False # Expose an entity if the entity's domain is exposed by default and # the configuration doesn't explicitly exclude it from being # exposed, or if the entity is explicitly exposed - if entity.domain in self.exposed_domains: + if state.domain in self.exposed_domains: return True return False diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index d6ac67b6984..2a9022f909d 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from functools import lru_cache import hashlib from http import HTTPStatus from ipaddress import ip_address @@ -302,15 +303,15 @@ class HueOneLightStateView(HomeAssistantView): ) return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) - if (entity := hass.states.get(hass_entity_id)) is None: + if (state := 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) - if not self.config.is_entity_exposed(entity): + if not self.config.is_state_exposed(state): _LOGGER.error("Entity not exposed: %s", entity_id) return self.json_message("Entity not exposed", HTTPStatus.UNAUTHORIZED) - json_response = entity_to_json(self.config, entity) + json_response = state_to_json(self.config, state) return self.json(json_response) @@ -346,7 +347,7 @@ class HueOneLightChangeView(HomeAssistantView): _LOGGER.error("Entity not found: %s", entity_id) return self.json_message("Entity not found", HTTPStatus.NOT_FOUND) - if not config.is_entity_exposed(entity): + if not config.is_state_exposed(entity): _LOGGER.error("Entity not exposed: %s", entity_id) return self.json_message("Entity not exposed", HTTPStatus.UNAUTHORIZED) @@ -614,7 +615,7 @@ class HueOneLightChangeView(HomeAssistantView): return self.json(json_response) -def get_entity_state(config: Config, entity: State) -> dict[str, Any]: +def get_entity_state_dict(config: Config, entity: State) -> dict[str, Any]: """Retrieve and convert state and brightness values for an entity.""" cached_state_entry = config.cached_states.get(entity.entity_id, None) cached_state = None @@ -718,22 +719,32 @@ def get_entity_state(config: Config, entity: State) -> dict[str, Any]: return data -def entity_to_json(config: Config, entity: State) -> dict[str, Any]: +@lru_cache(maxsize=1024) +def _entity_unique_id(entity_id: str) -> str: + """Return the emulated_hue unique id for the entity_id.""" + unique_id = hashlib.md5(entity_id.encode()).hexdigest() + return ( + f"00:{unique_id[0:2]}:{unique_id[2:4]}:" + f"{unique_id[4:6]}:{unique_id[6:8]}:{unique_id[8:10]}:" + f"{unique_id[10:12]}:{unique_id[12:14]}-{unique_id[14:16]}" + ) + + +def state_to_json(config: Config, state: State) -> dict[str, Any]: """Convert an entity to its Hue bridge JSON representation.""" - entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - color_modes = entity.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) - unique_id = hashlib.md5(entity.entity_id.encode()).hexdigest() - unique_id = f"00:{unique_id[0:2]}:{unique_id[2:4]}:{unique_id[4:6]}:{unique_id[6:8]}:{unique_id[8:10]}:{unique_id[10:12]}:{unique_id[12:14]}-{unique_id[14:16]}" + entity_features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + color_modes = state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES, []) + unique_id = _entity_unique_id(state.entity_id) + state_dict = get_entity_state_dict(config, state) - state = get_entity_state(config, entity) - - retval: dict[str, Any] = { - "state": { - HUE_API_STATE_ON: state[STATE_ON], - "reachable": entity.state != STATE_UNAVAILABLE, - "mode": "homeautomation", - }, - "name": config.get_entity_name(entity), + json_state: dict[str, str | bool | int] = { + HUE_API_STATE_ON: state_dict[STATE_ON], + "reachable": state.state != STATE_UNAVAILABLE, + "mode": "homeautomation", + } + retval: dict[str, str | dict[str, str | bool | int]] = { + "state": json_state, + "name": config.get_entity_name(state), "uniqueid": unique_id, "manufacturername": "Home Assistant", "swversion": "123", @@ -744,30 +755,30 @@ def entity_to_json(config: Config, entity: State) -> dict[str, Any]: # Same as Color light, but which supports additional setting of color temperature retval["type"] = "Extended color light" retval["modelid"] = "HASS231" - retval["state"].update( + json_state.update( { - HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], - HUE_API_STATE_HUE: state[STATE_HUE], - HUE_API_STATE_SAT: state[STATE_SATURATION], - HUE_API_STATE_CT: state[STATE_COLOR_TEMP], + HUE_API_STATE_BRI: state_dict[STATE_BRIGHTNESS], + HUE_API_STATE_HUE: state_dict[STATE_HUE], + HUE_API_STATE_SAT: state_dict[STATE_SATURATION], + HUE_API_STATE_CT: state_dict[STATE_COLOR_TEMP], HUE_API_STATE_EFFECT: "none", } ) - if state[STATE_HUE] > 0 or state[STATE_SATURATION] > 0: - retval["state"][HUE_API_STATE_COLORMODE] = "hs" + if state_dict[STATE_HUE] > 0 or state_dict[STATE_SATURATION] > 0: + json_state[HUE_API_STATE_COLORMODE] = "hs" else: - retval["state"][HUE_API_STATE_COLORMODE] = "ct" + json_state[HUE_API_STATE_COLORMODE] = "ct" elif light.color_supported(color_modes): # Color light (Zigbee Device ID: 0x0200) # Supports on/off, dimming and color control (hue/saturation, enhanced hue, color loop and XY) retval["type"] = "Color light" retval["modelid"] = "HASS213" - retval["state"].update( + json_state.update( { - HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], + HUE_API_STATE_BRI: state_dict[STATE_BRIGHTNESS], HUE_API_STATE_COLORMODE: "hs", - HUE_API_STATE_HUE: state[STATE_HUE], - HUE_API_STATE_SAT: state[STATE_SATURATION], + HUE_API_STATE_HUE: state_dict[STATE_HUE], + HUE_API_STATE_SAT: state_dict[STATE_SATURATION], HUE_API_STATE_EFFECT: "none", } ) @@ -776,11 +787,11 @@ def entity_to_json(config: Config, entity: State) -> dict[str, Any]: # Supports groups, scenes, on/off, dimming, and setting of a color temperature retval["type"] = "Color temperature light" retval["modelid"] = "HASS312" - retval["state"].update( + json_state.update( { HUE_API_STATE_COLORMODE: "ct", - HUE_API_STATE_CT: state[STATE_COLOR_TEMP], - HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], + HUE_API_STATE_CT: state_dict[STATE_COLOR_TEMP], + HUE_API_STATE_BRI: state_dict[STATE_BRIGHTNESS], } ) elif entity_features & ( @@ -793,7 +804,7 @@ def entity_to_json(config: Config, entity: State) -> dict[str, Any]: # Supports groups, scenes, on/off and dimming retval["type"] = "Dimmable light" retval["modelid"] = "HASS123" - retval["state"].update({HUE_API_STATE_BRI: state[STATE_BRIGHTNESS]}) + json_state.update({HUE_API_STATE_BRI: state_dict[STATE_BRIGHTNESS]}) elif not config.lights_all_dimmable: # On/Off light (ZigBee Device ID: 0x0000) # Supports groups, scenes and on/off control @@ -806,7 +817,7 @@ def entity_to_json(config: Config, entity: State) -> dict[str, Any]: # Reports fixed brightness for compatibility with Alexa. retval["type"] = "Dimmable light" retval["modelid"] = "HASS123" - retval["state"].update({HUE_API_STATE_BRI: HUE_API_STATE_BRI_MAX}) + json_state.update({HUE_API_STATE_BRI: HUE_API_STATE_BRI_MAX}) return retval @@ -835,8 +846,8 @@ def create_list_of_entities(config: Config, request: web.Request) -> dict[str, A """Create a list of all entities.""" hass: core.HomeAssistant = request.app["hass"] json_response: dict[str, Any] = { - config.entity_id_to_number(entity.entity_id): entity_to_json(config, entity) - for entity in config.filter_exposed_entities(hass.states.async_all()) + config.entity_id_to_number(entity.entity_id): state_to_json(config, entity) + for entity in config.filter_exposed_states(hass.states.async_all()) } return json_response From 228fc02abb41f2032d7eb10eafaebfdde4f6035d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 5 Jun 2022 09:13:43 -0600 Subject: [PATCH 1266/3516] Bump regenmaschine to 2022.06.0 (#73056) --- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 98dc9a6c877..a61283ea298 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.05.1"], + "requirements": ["regenmaschine==2022.06.0"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 9ebbd7ef32b..e79f2c06b23 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2068,7 +2068,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.05.1 +regenmaschine==2022.06.0 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1e8ecd9621..023521fc4be 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1367,7 +1367,7 @@ rachiopy==1.0.3 radios==0.1.1 # homeassistant.components.rainmachine -regenmaschine==2022.05.1 +regenmaschine==2022.06.0 # homeassistant.components.renault renault-api==0.1.11 From e8cfc747f9f3553a534f12d154407391b374c28c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 5 Jun 2022 09:13:54 -0600 Subject: [PATCH 1267/3516] Bump simplisafe-python to 2022.06.0 (#73054) --- 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 f62da735f92..4da8f09eff8 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==2022.05.2"], + "requirements": ["simplisafe-python==2022.06.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index e79f2c06b23..56411aafceb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2171,7 +2171,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.05.2 +simplisafe-python==2022.06.0 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 023521fc4be..c96a7e1e428 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1428,7 +1428,7 @@ sharkiq==0.0.1 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2022.05.2 +simplisafe-python==2022.06.0 # homeassistant.components.slack slackclient==2.5.0 From b1073fb362092c760f7115afe9097b4f7c62f38d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 5 Jun 2022 18:45:21 +0200 Subject: [PATCH 1268/3516] Remove myself from fixer codeowners (#73070) --- CODEOWNERS | 1 - homeassistant/components/fixer/manifest.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 59f3671c475..077f4ca9063 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -328,7 +328,6 @@ build.json @home-assistant/supervisor /tests/components/firmata/ @DaAwesomeP /homeassistant/components/fivem/ @Sander0542 /tests/components/fivem/ @Sander0542 -/homeassistant/components/fixer/ @fabaff /homeassistant/components/fjaraskupan/ @elupus /tests/components/fjaraskupan/ @elupus /homeassistant/components/flick_electric/ @ZephireNZ diff --git a/homeassistant/components/fixer/manifest.json b/homeassistant/components/fixer/manifest.json index 87f2370aace..4d058f82e22 100644 --- a/homeassistant/components/fixer/manifest.json +++ b/homeassistant/components/fixer/manifest.json @@ -3,7 +3,7 @@ "name": "Fixer", "documentation": "https://www.home-assistant.io/integrations/fixer", "requirements": ["fixerio==1.0.0a0"], - "codeowners": ["@fabaff"], + "codeowners": [], "iot_class": "cloud_polling", "loggers": ["fixerio"] } From 58d4ea0db9160ad69ec4cda025cd9ea976e87be8 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 5 Jun 2022 13:09:44 -0400 Subject: [PATCH 1269/3516] Bump aioskybell to 22.6.0 (#73073) * Bump aioskybell to 22.6.0 * uno mas --- homeassistant/components/skybell/binary_sensor.py | 6 +++++- homeassistant/components/skybell/entity.py | 8 ++++++-- homeassistant/components/skybell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/skybell/binary_sensor.py b/homeassistant/components/skybell/binary_sensor.py index dcb5466e479..1af85398d47 100644 --- a/homeassistant/components/skybell/binary_sensor.py +++ b/homeassistant/components/skybell/binary_sensor.py @@ -1,6 +1,8 @@ """Binary sensor support for the Skybell HD Doorbell.""" from __future__ import annotations +from datetime import datetime + from aioskybell.helpers import const as CONST import voluptuous as vol @@ -68,7 +70,9 @@ class SkybellBinarySensor(SkybellEntity, BinarySensorEntity): self._event: dict[str, str] = {} @property - def extra_state_attributes(self) -> dict[str, str | int | tuple[str, str]]: + def extra_state_attributes( + self, + ) -> dict[str, str | int | datetime | tuple[str, str]]: """Return the state attributes.""" attrs = super().extra_state_attributes if event := self._event.get(CONST.CREATED_AT): diff --git a/homeassistant/components/skybell/entity.py b/homeassistant/components/skybell/entity.py index cf728cde069..6b4683dda6b 100644 --- a/homeassistant/components/skybell/entity.py +++ b/homeassistant/components/skybell/entity.py @@ -1,6 +1,8 @@ """Entity representing a Skybell HD Doorbell.""" from __future__ import annotations +from datetime import datetime + from aioskybell import SkybellDevice from homeassistant.const import ATTR_CONNECTIONS @@ -44,9 +46,11 @@ class SkybellEntity(CoordinatorEntity[SkybellDataUpdateCoordinator]): return self.coordinator.device @property - def extra_state_attributes(self) -> dict[str, str | int | tuple[str, str]]: + def extra_state_attributes( + self, + ) -> dict[str, str | int | datetime | tuple[str, str]]: """Return the state attributes.""" - attr: dict[str, str | int | tuple[str, str]] = { + attr: dict[str, str | int | datetime | tuple[str, str]] = { "device_id": self._device.device_id, "status": self._device.status, "location": self._device.location, diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index 335ff2615f8..23b29a49247 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -3,7 +3,7 @@ "name": "SkyBell", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/skybell", - "requirements": ["aioskybell==22.3.0"], + "requirements": ["aioskybell==22.6.0"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", "loggers": ["aioskybell"] diff --git a/requirements_all.txt b/requirements_all.txt index 56411aafceb..914d02fb421 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -241,7 +241,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.skybell -aioskybell==22.3.0 +aioskybell==22.6.0 # homeassistant.components.slimproto aioslimproto==2.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c96a7e1e428..a18f045207b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -210,7 +210,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.skybell -aioskybell==22.3.0 +aioskybell==22.6.0 # homeassistant.components.slimproto aioslimproto==2.0.1 From c13e55ca025821767aa6e51e3da0cf789f6646e5 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 5 Jun 2022 15:56:31 -0400 Subject: [PATCH 1270/3516] Move Skybell attributes to their own sensors (#73089) --- .../components/skybell/binary_sensor.py | 12 --- homeassistant/components/skybell/entity.py | 20 ----- homeassistant/components/skybell/sensor.py | 80 ++++++++++++++++++- 3 files changed, 77 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/skybell/binary_sensor.py b/homeassistant/components/skybell/binary_sensor.py index 1af85398d47..05f007e9455 100644 --- a/homeassistant/components/skybell/binary_sensor.py +++ b/homeassistant/components/skybell/binary_sensor.py @@ -1,8 +1,6 @@ """Binary sensor support for the Skybell HD Doorbell.""" from __future__ import annotations -from datetime import datetime - from aioskybell.helpers import const as CONST import voluptuous as vol @@ -69,16 +67,6 @@ class SkybellBinarySensor(SkybellEntity, BinarySensorEntity): super().__init__(coordinator, description) self._event: dict[str, str] = {} - @property - def extra_state_attributes( - self, - ) -> dict[str, str | int | datetime | tuple[str, str]]: - """Return the state attributes.""" - attrs = super().extra_state_attributes - if event := self._event.get(CONST.CREATED_AT): - attrs["event_date"] = event - return attrs - @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" diff --git a/homeassistant/components/skybell/entity.py b/homeassistant/components/skybell/entity.py index 6b4683dda6b..0e5c246a8ed 100644 --- a/homeassistant/components/skybell/entity.py +++ b/homeassistant/components/skybell/entity.py @@ -1,8 +1,6 @@ """Entity representing a Skybell HD Doorbell.""" from __future__ import annotations -from datetime import datetime - from aioskybell import SkybellDevice from homeassistant.const import ATTR_CONNECTIONS @@ -45,24 +43,6 @@ class SkybellEntity(CoordinatorEntity[SkybellDataUpdateCoordinator]): """Return the device.""" return self.coordinator.device - @property - def extra_state_attributes( - self, - ) -> dict[str, str | int | datetime | tuple[str, str]]: - """Return the state attributes.""" - attr: dict[str, str | int | datetime | tuple[str, str]] = { - "device_id": self._device.device_id, - "status": self._device.status, - "location": self._device.location, - "motion_threshold": self._device.motion_threshold, - "video_profile": self._device.video_profile, - } - if self._device.owner: - attr["wifi_ssid"] = self._device.wifi_ssid - attr["wifi_status"] = self._device.wifi_status - attr["last_check_in"] = self._device.last_check_in - return attr - async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() diff --git a/homeassistant/components/skybell/sensor.py b/homeassistant/components/skybell/sensor.py index c769570b10c..eeb81e07aaf 100644 --- a/homeassistant/components/skybell/sensor.py +++ b/homeassistant/components/skybell/sensor.py @@ -1,10 +1,17 @@ """Sensor support for Skybell Doorbells.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from aioskybell import SkybellDevice +from aioskybell.helpers import const as CONST import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, + SensorDeviceClass, SensorEntity, SensorEntityDescription, ) @@ -12,15 +19,79 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .entity import DOMAIN, SkybellEntity -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( + +@dataclass +class SkybellSensorEntityDescription(SensorEntityDescription): + """Class to describe a Skybell sensor.""" + + value_fn: Callable[[SkybellDevice], Any] = lambda val: val + + +SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( + SkybellSensorEntityDescription( key="chime_level", name="Chime Level", icon="mdi:bell-ring", + value_fn=lambda device: device.outdoor_chime_level, + ), + SkybellSensorEntityDescription( + key="last_button_event", + name="Last Button Event", + icon="mdi:clock", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda device: device.latest("button").get(CONST.CREATED_AT), + ), + SkybellSensorEntityDescription( + key="last_motion_event", + name="Last Motion Event", + icon="mdi:clock", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda device: device.latest("motion").get(CONST.CREATED_AT), + ), + SkybellSensorEntityDescription( + key=CONST.ATTR_LAST_CHECK_IN, + name="Last Check in", + icon="mdi:clock", + entity_registry_enabled_default=False, + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.last_check_in, + ), + SkybellSensorEntityDescription( + key="motion_threshold", + name="Motion Threshold", + icon="mdi:walk", + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.motion_threshold, + ), + SkybellSensorEntityDescription( + key="video_profile", + name="Video Profile", + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.video_profile, + ), + SkybellSensorEntityDescription( + key=CONST.ATTR_WIFI_SSID, + name="Wifi SSID", + icon="mdi:wifi-settings", + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.wifi_ssid, + ), + SkybellSensorEntityDescription( + key=CONST.ATTR_WIFI_STATUS, + name="Wifi Status", + icon="mdi:wifi-strength-3", + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda device: device.wifi_status, ), ) @@ -45,13 +116,16 @@ async def async_setup_entry( SkybellSensor(coordinator, description) for coordinator in hass.data[DOMAIN][entry.entry_id] for description in SENSOR_TYPES + if coordinator.device.owner or description.key not in CONST.ATTR_OWNER_STATS ) class SkybellSensor(SkybellEntity, SensorEntity): """A sensor implementation for Skybell devices.""" + entity_description: SkybellSensorEntityDescription + @property def native_value(self) -> int: """Return the state of the sensor.""" - return self._device.outdoor_chime_level + return self.entity_description.value_fn(self._device) From b10bbc3e1430d492f0b8d22dcbcda5754258fd06 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 5 Jun 2022 15:56:48 -0400 Subject: [PATCH 1271/3516] Add do not ring switch to Skybell (#73090) --- homeassistant/components/skybell/switch.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/skybell/switch.py b/homeassistant/components/skybell/switch.py index d28369e40b0..d4f2817141c 100644 --- a/homeassistant/components/skybell/switch.py +++ b/homeassistant/components/skybell/switch.py @@ -24,6 +24,10 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( key="do_not_disturb", name="Do Not Disturb", ), + SwitchEntityDescription( + key="do_not_ring", + name="Do Not Ring", + ), SwitchEntityDescription( key="motion_sensor", name="Motion Sensor", From 35a0f59ec9354bda424cbc9223e55bb7a4a01b4a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 5 Jun 2022 22:42:46 +0200 Subject: [PATCH 1272/3516] Bump pysensibo to 1.0.17 (#73092) --- homeassistant/components/sensibo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 203eb751dcf..18e93d5efa6 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,7 +2,7 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": ["pysensibo==1.0.16"], + "requirements": ["pysensibo==1.0.17"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 914d02fb421..f365da06766 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1795,7 +1795,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.16 +pysensibo==1.0.17 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a18f045207b..4331cc6dc36 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1214,7 +1214,7 @@ pyruckus==0.12 pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.16 +pysensibo==1.0.17 # homeassistant.components.serial # homeassistant.components.zha From f7626bd511849101d77f838aedf854c44b49aee2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Jun 2022 12:28:57 -1000 Subject: [PATCH 1273/3516] Speed up camera tokens (#73098) --- homeassistant/components/camera/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 4a6e1546f46..627da2d1872 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -10,7 +10,6 @@ from dataclasses import dataclass from datetime import datetime, timedelta from enum import IntEnum from functools import partial -import hashlib import logging import os from random import SystemRandom @@ -675,9 +674,7 @@ class Camera(Entity): @callback def async_update_token(self) -> None: """Update the used token.""" - self.access_tokens.append( - hashlib.sha256(_RND.getrandbits(256).to_bytes(32, "little")).hexdigest() - ) + self.access_tokens.append(hex(_RND.getrandbits(256))[2:]) async def async_internal_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" From cac84e4160841aa931d826c1f1acf55033042917 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Jun 2022 13:59:52 -1000 Subject: [PATCH 1274/3516] Add config flow to radiotherm (#72874) --- .coveragerc | 3 + CODEOWNERS | 3 +- .../components/radiotherm/__init__.py | 53 +++ .../components/radiotherm/climate.py | 377 ++++++++---------- .../components/radiotherm/config_flow.py | 166 ++++++++ homeassistant/components/radiotherm/const.py | 7 + .../components/radiotherm/coordinator.py | 48 +++ homeassistant/components/radiotherm/data.py | 74 ++++ .../components/radiotherm/manifest.json | 9 +- .../components/radiotherm/strings.json | 31 ++ .../radiotherm/translations/en.json | 31 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/dhcp.py | 2 + requirements_test_all.txt | 3 + tests/components/radiotherm/__init__.py | 1 + .../components/radiotherm/test_config_flow.py | 302 ++++++++++++++ 16 files changed, 900 insertions(+), 211 deletions(-) create mode 100644 homeassistant/components/radiotherm/config_flow.py create mode 100644 homeassistant/components/radiotherm/const.py create mode 100644 homeassistant/components/radiotherm/coordinator.py create mode 100644 homeassistant/components/radiotherm/data.py create mode 100644 homeassistant/components/radiotherm/strings.json create mode 100644 homeassistant/components/radiotherm/translations/en.json create mode 100644 tests/components/radiotherm/__init__.py create mode 100644 tests/components/radiotherm/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index f4cdfa1c460..0f9c3e4998a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -963,7 +963,10 @@ omit = homeassistant/components/radarr/sensor.py homeassistant/components/radio_browser/__init__.py homeassistant/components/radio_browser/media_source.py + homeassistant/components/radiotherm/__init__.py homeassistant/components/radiotherm/climate.py + homeassistant/components/radiotherm/coordinator.py + homeassistant/components/radiotherm/data.py homeassistant/components/rainbird/* homeassistant/components/raincloud/* homeassistant/components/rainmachine/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 077f4ca9063..6db9d92ebb9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -825,7 +825,8 @@ build.json @home-assistant/supervisor /tests/components/rachio/ @bdraco /homeassistant/components/radio_browser/ @frenck /tests/components/radio_browser/ @frenck -/homeassistant/components/radiotherm/ @vinnyfuria +/homeassistant/components/radiotherm/ @bdraco @vinnyfuria +/tests/components/radiotherm/ @bdraco @vinnyfuria /homeassistant/components/rainbird/ @konikvranik /homeassistant/components/raincloud/ @vanstinator /homeassistant/components/rainforest_eagle/ @gtdiehl @jcalbert @hastarin diff --git a/homeassistant/components/radiotherm/__init__.py b/homeassistant/components/radiotherm/__init__.py index adc8cdbd6ee..9e389af6719 100644 --- a/homeassistant/components/radiotherm/__init__.py +++ b/homeassistant/components/radiotherm/__init__.py @@ -1 +1,54 @@ """The radiotherm component.""" +from __future__ import annotations + +from socket import timeout + +from radiotherm.validate import RadiothermTstatError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import CONF_HOLD_TEMP, DOMAIN +from .coordinator import RadioThermUpdateCoordinator +from .data import async_get_init_data + +PLATFORMS: list[Platform] = [Platform.CLIMATE] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Radio Thermostat from a config entry.""" + host = entry.data[CONF_HOST] + try: + init_data = await async_get_init_data(hass, host) + except RadiothermTstatError as ex: + raise ConfigEntryNotReady( + f"{host} was busy (invalid value returned): {ex}" + ) from ex + except timeout as ex: + raise ConfigEntryNotReady( + f"{host} timed out waiting for a response: {ex}" + ) from ex + + hold_temp = entry.options[CONF_HOLD_TEMP] + coordinator = RadioThermUpdateCoordinator(hass, init_data, hold_temp) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(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_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + 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/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index dbf013ffa9a..63ee93c9c84 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -1,8 +1,9 @@ """Support for Radio Thermostat wifi-enabled home thermostats.""" from __future__ import annotations +from functools import partial import logging -from socket import timeout +from typing import Any import radiotherm import voluptuous as vol @@ -18,24 +19,32 @@ from homeassistant.components.climate.const import ( HVACAction, HVACMode, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, PRECISION_HALVES, TEMP_FAHRENHEIT, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr 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 from homeassistant.util import dt as dt_util +from . import DOMAIN +from .const import CONF_HOLD_TEMP +from .coordinator import RadioThermUpdateCoordinator +from .data import RadioThermUpdate + _LOGGER = logging.getLogger(__name__) ATTR_FAN_ACTION = "fan_action" -CONF_HOLD_TEMP = "hold_temp" - PRESET_HOLIDAY = "holiday" PRESET_ALTERNATE = "alternate" @@ -74,12 +83,19 @@ CODE_TO_TEMP_STATE = {0: HVACAction.IDLE, 1: HVACAction.HEATING, 2: HVACAction.C # future this should probably made into a binary sensor for the fan. CODE_TO_FAN_STATE = {0: FAN_OFF, 1: FAN_ON} -PRESET_MODE_TO_CODE = {"home": 0, "alternate": 1, "away": 2, "holiday": 3} +PRESET_MODE_TO_CODE = { + PRESET_HOME: 0, + PRESET_ALTERNATE: 1, + PRESET_AWAY: 2, + PRESET_HOLIDAY: 3, +} -CODE_TO_PRESET_MODE = {0: "home", 1: "alternate", 2: "away", 3: "holiday"} +CODE_TO_PRESET_MODE = {v: k for k, v in PRESET_MODE_TO_CODE.items()} CODE_TO_HOLD_STATE = {0: False, 1: True} +PARALLEL_UPDATES = 1 + def round_temp(temperature): """Round a temperature to the resolution of the thermostat. @@ -98,69 +114,93 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up climate for a radiotherm device.""" + coordinator: RadioThermUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([RadioThermostat(coordinator)]) + + +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Radio Thermostat.""" - hosts = [] + _LOGGER.warning( + # config flow added in 2022.7 and should be removed in 2022.9 + "Configuration of the Radio Thermostat climate platform in YAML is deprecated and " + "will be removed in Home Assistant 2022.9; Your existing configuration " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file" + ) + hosts: list[str] = [] if CONF_HOST in config: hosts = config[CONF_HOST] else: - hosts.append(radiotherm.discover.discover_address()) + hosts.append( + await hass.async_add_executor_job(radiotherm.discover.discover_address) + ) - if hosts is None: + if not hosts: _LOGGER.error("No Radiotherm Thermostats detected") - return False - - hold_temp = config.get(CONF_HOLD_TEMP) - tstats = [] + return + hold_temp: bool = config[CONF_HOLD_TEMP] for host in hosts: - try: - tstat = radiotherm.get_thermostat(host) - tstats.append(RadioThermostat(tstat, hold_temp)) - except OSError: - _LOGGER.exception("Unable to connect to Radio Thermostat: %s", host) - - add_entities(tstats, True) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_HOST: host, CONF_HOLD_TEMP: hold_temp}, + ) + ) -class RadioThermostat(ClimateEntity): +class RadioThermostat(CoordinatorEntity[RadioThermUpdateCoordinator], ClimateEntity): """Representation of a Radio Thermostat.""" _attr_hvac_modes = OPERATION_LIST - _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE - | ClimateEntityFeature.FAN_MODE - | ClimateEntityFeature.PRESET_MODE - ) + _attr_temperature_unit = TEMP_FAHRENHEIT + _attr_precision = PRECISION_HALVES - def __init__(self, device, hold_temp): + def __init__(self, coordinator: RadioThermUpdateCoordinator) -> None: """Initialize the thermostat.""" - self.device = device - self._target_temperature = None - self._current_temperature = None - self._current_humidity = None - self._current_operation = HVACMode.OFF - self._name = None - self._fmode = None - self._fstate = None - self._tmode = None - self._tstate: HVACAction | None = None - self._hold_temp = hold_temp + super().__init__(coordinator) + self.device = coordinator.init_data.tstat + self._attr_name = coordinator.init_data.name + self._hold_temp = coordinator.hold_temp self._hold_set = False - self._prev_temp = None - self._preset_mode = None - self._program_mode = None - self._is_away = False - - # Fan circulate mode is only supported by the CT80 models. + self._attr_unique_id = coordinator.init_data.mac + self._attr_device_info = DeviceInfo( + name=coordinator.init_data.name, + model=coordinator.init_data.model, + manufacturer="Radio Thermostats", + sw_version=coordinator.init_data.fw_version, + connections={(dr.CONNECTION_NETWORK_MAC, coordinator.init_data.mac)}, + ) self._is_model_ct80 = isinstance(self.device, radiotherm.thermostat.CT80) + self._attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ) + self._process_data() + if not self._is_model_ct80: + self._attr_fan_modes = CT30_FAN_OPERATION_LIST + return + self._attr_fan_modes = CT80_FAN_OPERATION_LIST + self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE + self._attr_preset_modes = PRESET_MODES - async def async_added_to_hass(self): + @property + def data(self) -> RadioThermUpdate: + """Returnt the last update.""" + return self.coordinator.data + + async def async_added_to_hass(self) -> None: """Register callbacks.""" # Set the time on the device. This shouldn't be in the # constructor because it's a network call. We can't put it in @@ -168,181 +208,93 @@ class RadioThermostat(ClimateEntity): # temperature in the thermostat. So add it as a future job # for the event loop to run. self.hass.async_add_job(self.set_time) + await super().async_added_to_hass() - @property - def name(self): - """Return the name of the Radio Thermostat.""" - return self._name - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_FAHRENHEIT - - @property - def precision(self): - """Return the precision of the system.""" - return PRECISION_HALVES - - @property - def extra_state_attributes(self): - """Return the device specific state attributes.""" - return {ATTR_FAN_ACTION: self._fstate} - - @property - def fan_modes(self): - """List of available fan modes.""" - if self._is_model_ct80: - return CT80_FAN_OPERATION_LIST - return CT30_FAN_OPERATION_LIST - - @property - def fan_mode(self): - """Return whether the fan is on.""" - return self._fmode - - def set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Turn fan on/off.""" - if (code := FAN_MODE_TO_CODE.get(fan_mode)) is not None: - self.device.fmode = code + if (code := FAN_MODE_TO_CODE.get(fan_mode)) is None: + raise HomeAssistantError(f"{fan_mode} is not a valid fan mode") + await self.hass.async_add_executor_job(self._set_fan_mode, code) + self._attr_fan_mode = fan_mode + self.async_write_ha_state() + await self.coordinator.async_request_refresh() - @property - def current_temperature(self): - """Return the current temperature.""" - return self._current_temperature + def _set_fan_mode(self, code: int) -> None: + """Turn fan on/off.""" + self.device.fmode = code - @property - def current_humidity(self): - """Return the current temperature.""" - return self._current_humidity + @callback + def _handle_coordinator_update(self) -> None: + self._process_data() + return super()._handle_coordinator_update() - @property - def hvac_mode(self) -> HVACMode: - """Return the current operation. head, cool idle.""" - return self._current_operation - - @property - def hvac_action(self) -> HVACAction | None: - """Return the current running hvac operation if supported.""" - if self.hvac_mode == HVACMode.OFF: - return None - return self._tstate - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._target_temperature - - @property - def preset_mode(self): - """Return the current preset mode, e.g., home, away, temp.""" - if self._program_mode == 0: - return PRESET_HOME - if self._program_mode == 1: - return PRESET_ALTERNATE - if self._program_mode == 2: - return PRESET_AWAY - if self._program_mode == 3: - return PRESET_HOLIDAY - - @property - def preset_modes(self): - """Return a list of available preset modes.""" - return PRESET_MODES - - def update(self): + @callback + def _process_data(self) -> None: """Update and validate the data from the thermostat.""" - # Radio thermostats are very slow, and sometimes don't respond - # very quickly. So we need to keep the number of calls to them - # to a bare minimum or we'll hit the Home Assistant 10 sec warning. We - # have to make one call to /tstat to get temps but we'll try and - # keep the other calls to a minimum. Even with this, these - # thermostats tend to time out sometimes when they're actively - # heating or cooling. - - try: - # First time - get the name from the thermostat. This is - # normally set in the radio thermostat web app. - if self._name is None: - self._name = self.device.name["raw"] - - # Request the current state from the thermostat. - data = self.device.tstat["raw"] - - if self._is_model_ct80: - humiditydata = self.device.humidity["raw"] - - except radiotherm.validate.RadiothermTstatError: - _LOGGER.warning( - "%s (%s) was busy (invalid value returned)", - self._name, - self.device.host, - ) - - except timeout: - _LOGGER.warning( - "Timeout waiting for response from %s (%s)", - self._name, - self.device.host, - ) - + data = self.data.tstat + if self._is_model_ct80: + self._attr_current_humidity = self.data.humidity + self._attr_preset_mode = CODE_TO_PRESET_MODE[data["program_mode"]] + # Map thermostat values into various STATE_ flags. + self._attr_current_temperature = data["temp"] + self._attr_fan_mode = CODE_TO_FAN_MODE[data["fmode"]] + self._attr_extra_state_attributes = { + ATTR_FAN_ACTION: CODE_TO_FAN_STATE[data["fstate"]] + } + self._attr_hvac_mode = CODE_TO_TEMP_MODE[data["tmode"]] + self._hold_set = CODE_TO_HOLD_STATE[data["hold"]] + if self.hvac_mode == HVACMode.OFF: + self._attr_hvac_action = None else: - if self._is_model_ct80: - self._current_humidity = humiditydata - self._program_mode = data["program_mode"] - self._preset_mode = CODE_TO_PRESET_MODE[data["program_mode"]] + self._attr_hvac_action = CODE_TO_TEMP_STATE[data["tstate"]] + if self.hvac_mode == HVACMode.COOL: + self._attr_target_temperature = data["t_cool"] + elif self.hvac_mode == HVACMode.HEAT: + self._attr_target_temperature = data["t_heat"] + elif self.hvac_mode == HVACMode.AUTO: + # This doesn't really work - tstate is only set if the HVAC is + # active. If it's idle, we don't know what to do with the target + # temperature. + if self.hvac_action == HVACAction.COOLING: + self._attr_target_temperature = data["t_cool"] + elif self.hvac_action == HVACAction.HEATING: + self._attr_target_temperature = data["t_heat"] - # Map thermostat values into various STATE_ flags. - self._current_temperature = data["temp"] - self._fmode = CODE_TO_FAN_MODE[data["fmode"]] - self._fstate = CODE_TO_FAN_STATE[data["fstate"]] - self._tmode = CODE_TO_TEMP_MODE[data["tmode"]] - self._tstate = CODE_TO_TEMP_STATE[data["tstate"]] - self._hold_set = CODE_TO_HOLD_STATE[data["hold"]] - - self._current_operation = self._tmode - if self._tmode == HVACMode.COOL: - self._target_temperature = data["t_cool"] - elif self._tmode == HVACMode.HEAT: - self._target_temperature = data["t_heat"] - elif self._tmode == HVACMode.AUTO: - # This doesn't really work - tstate is only set if the HVAC is - # active. If it's idle, we don't know what to do with the target - # temperature. - if self._tstate == HVACAction.COOLING: - self._target_temperature = data["t_cool"] - elif self._tstate == HVACAction.HEATING: - self._target_temperature = data["t_heat"] - else: - self._current_operation = HVACMode.OFF - - def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return + hold_changed = kwargs.get("hold_changed", False) + await self.hass.async_add_executor_job( + partial(self._set_temperature, temperature, hold_changed) + ) + self._attr_target_temperature = temperature + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + def _set_temperature(self, temperature: int, hold_changed: bool) -> None: + """Set new target temperature.""" temperature = round_temp(temperature) - - if self._current_operation == HVACMode.COOL: + if self.hvac_mode == HVACMode.COOL: self.device.t_cool = temperature - elif self._current_operation == HVACMode.HEAT: + elif self.hvac_mode == HVACMode.HEAT: self.device.t_heat = temperature - elif self._current_operation == HVACMode.AUTO: - if self._tstate == HVACAction.COOLING: + elif self.hvac_mode == HVACMode.AUTO: + if self.hvac_action == HVACAction.COOLING: self.device.t_cool = temperature - elif self._tstate == HVACAction.HEATING: + elif self.hvac_action == HVACAction.HEATING: self.device.t_heat = temperature # Only change the hold if requested or if hold mode was turned # on and we haven't set it yet. - if kwargs.get("hold_changed", False) or not self._hold_set: + if hold_changed or not self._hold_set: if self._hold_temp: self.device.hold = 1 self._hold_set = True else: self.device.hold = 0 - def set_time(self): + def set_time(self) -> None: """Set device time.""" # Calling this clears any local temperature override and # reverts to the scheduled temperature. @@ -353,23 +305,32 @@ class RadioThermostat(ClimateEntity): "minute": now.minute, } - def set_hvac_mode(self, hvac_mode: HVACMode) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set operation mode (auto, cool, heat, off).""" + await self.hass.async_add_executor_job(self._set_hvac_mode, hvac_mode) + self._attr_hvac_mode = hvac_mode + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + def _set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set operation mode (auto, cool, heat, off).""" if hvac_mode in (HVACMode.OFF, HVACMode.AUTO): self.device.tmode = TEMP_MODE_TO_CODE[hvac_mode] - # Setting t_cool or t_heat automatically changes tmode. elif hvac_mode == HVACMode.COOL: - self.device.t_cool = self._target_temperature + self.device.t_cool = self.target_temperature elif hvac_mode == HVACMode.HEAT: - self.device.t_heat = self._target_temperature + self.device.t_heat = self.target_temperature - def set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set Preset mode (Home, Alternate, Away, Holiday).""" - if preset_mode in PRESET_MODES: - self.device.program_mode = PRESET_MODE_TO_CODE[preset_mode] - else: - _LOGGER.error( - "Preset_mode %s not in PRESET_MODES", - preset_mode, - ) + if preset_mode not in PRESET_MODES: + raise HomeAssistantError("{preset_mode} is not a valid preset_mode") + await self.hass.async_add_executor_job(self._set_preset_mode, preset_mode) + self._attr_preset_mode = preset_mode + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + def _set_preset_mode(self, preset_mode: str) -> None: + """Set Preset mode (Home, Alternate, Away, Holiday).""" + self.device.program_mode = PRESET_MODE_TO_CODE[preset_mode] diff --git a/homeassistant/components/radiotherm/config_flow.py b/homeassistant/components/radiotherm/config_flow.py new file mode 100644 index 00000000000..45fee0f7fd9 --- /dev/null +++ b/homeassistant/components/radiotherm/config_flow.py @@ -0,0 +1,166 @@ +"""Config flow for Radio Thermostat integration.""" +from __future__ import annotations + +import logging +from socket import timeout +from typing import Any + +from radiotherm.validate import RadiothermTstatError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import dhcp +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError + +from .const import CONF_HOLD_TEMP, DOMAIN +from .data import RadioThermInitData, async_get_init_data + +_LOGGER = logging.getLogger(__name__) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + + +async def validate_connection(hass: HomeAssistant, host: str) -> RadioThermInitData: + """Validate the connection.""" + try: + return await async_get_init_data(hass, host) + except (timeout, RadiothermTstatError) as ex: + raise CannotConnect(f"Failed to connect to {host}: {ex}") from ex + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Radio Thermostat.""" + + VERSION = 1 + + def __init__(self): + """Initialize ConfigFlow.""" + self.discovered_ip: str | None = None + self.discovered_init_data: RadioThermInitData | None = None + + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Discover via DHCP.""" + self._async_abort_entries_match({CONF_HOST: discovery_info.ip}) + try: + init_data = await validate_connection(self.hass, discovery_info.ip) + except CannotConnect: + return self.async_abort(reason="cannot_connect") + await self.async_set_unique_id(init_data.mac) + self._abort_if_unique_id_configured( + updates={CONF_HOST: discovery_info.ip}, reload_on_update=False + ) + self.discovered_init_data = init_data + self.discovered_ip = discovery_info.ip + return await self.async_step_confirm() + + async def async_step_confirm(self, user_input=None): + """Attempt to confirm.""" + ip_address = self.discovered_ip + init_data = self.discovered_init_data + assert ip_address is not None + assert init_data is not None + if user_input is not None: + return self.async_create_entry( + title=init_data.name, + data={CONF_HOST: ip_address}, + options={CONF_HOLD_TEMP: False}, + ) + + self._set_confirm_only() + placeholders = { + "name": init_data.name, + "host": self.discovered_ip, + "model": init_data.model or "Unknown", + } + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="confirm", + description_placeholders=placeholders, + ) + + async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult: + """Import from yaml.""" + host = import_info[CONF_HOST] + self._async_abort_entries_match({CONF_HOST: host}) + _LOGGER.debug("Importing entry for host: %s", host) + try: + init_data = await validate_connection(self.hass, host) + except CannotConnect as ex: + _LOGGER.debug("Importing failed for %s", host, exc_info=ex) + return self.async_abort(reason="cannot_connect") + await self.async_set_unique_id(init_data.mac, raise_on_progress=False) + self._abort_if_unique_id_configured( + updates={CONF_HOST: host}, reload_on_update=False + ) + return self.async_create_entry( + title=init_data.name, + data={CONF_HOST: import_info[CONF_HOST]}, + options={CONF_HOLD_TEMP: import_info[CONF_HOLD_TEMP]}, + ) + + 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: + try: + init_data = await validate_connection(self.hass, user_input[CONF_HOST]) + except CannotConnect: + errors[CONF_HOST] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(init_data.mac, raise_on_progress=False) + self._abort_if_unique_id_configured( + updates={CONF_HOST: user_input[CONF_HOST]}, + reload_on_update=False, + ) + return self.async_create_entry( + title=init_data.name, + data=user_input, + options={CONF_HOLD_TEMP: False}, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + errors=errors, + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for radiotherm.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_HOLD_TEMP, + default=self.config_entry.options[CONF_HOLD_TEMP], + ): bool + } + ), + ) diff --git a/homeassistant/components/radiotherm/const.py b/homeassistant/components/radiotherm/const.py new file mode 100644 index 00000000000..398747c6571 --- /dev/null +++ b/homeassistant/components/radiotherm/const.py @@ -0,0 +1,7 @@ +"""Constants for the Radio Thermostat integration.""" + +DOMAIN = "radiotherm" + +CONF_HOLD_TEMP = "hold_temp" + +TIMEOUT = 25 diff --git a/homeassistant/components/radiotherm/coordinator.py b/homeassistant/components/radiotherm/coordinator.py new file mode 100644 index 00000000000..264a4a8d1fd --- /dev/null +++ b/homeassistant/components/radiotherm/coordinator.py @@ -0,0 +1,48 @@ +"""Coordinator for radiotherm.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from socket import timeout + +from radiotherm.validate import RadiothermTstatError + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .data import RadioThermInitData, RadioThermUpdate, async_get_data + +_LOGGER = logging.getLogger(__name__) + +UPDATE_INTERVAL = timedelta(seconds=15) + + +class RadioThermUpdateCoordinator(DataUpdateCoordinator[RadioThermUpdate]): + """DataUpdateCoordinator to gather data for radio thermostats.""" + + def __init__( + self, hass: HomeAssistant, init_data: RadioThermInitData, hold_temp: bool + ) -> None: + """Initialize DataUpdateCoordinator.""" + self.init_data = init_data + self.hold_temp = hold_temp + self._description = f"{init_data.name} ({init_data.host})" + super().__init__( + hass, + _LOGGER, + name=f"radiotherm {self.init_data.name}", + update_interval=UPDATE_INTERVAL, + ) + + async def _async_update_data(self) -> RadioThermUpdate: + """Update data from the thermostat.""" + try: + return await async_get_data(self.hass, self.init_data.tstat) + except RadiothermTstatError as ex: + raise UpdateFailed( + f"{self._description} was busy (invalid value returned): {ex}" + ) from ex + except timeout as ex: + raise UpdateFailed( + f"{self._description}) timed out waiting for a response: {ex}" + ) from ex diff --git a/homeassistant/components/radiotherm/data.py b/homeassistant/components/radiotherm/data.py new file mode 100644 index 00000000000..3aa4e6b7631 --- /dev/null +++ b/homeassistant/components/radiotherm/data.py @@ -0,0 +1,74 @@ +"""The radiotherm component data.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +import radiotherm +from radiotherm.thermostat import CommonThermostat + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .const import TIMEOUT + + +@dataclass +class RadioThermUpdate: + """An update from a radiotherm device.""" + + tstat: dict[str, Any] + humidity: int | None + + +@dataclass +class RadioThermInitData: + """An data needed to init the integration.""" + + tstat: CommonThermostat + host: str + name: str + mac: str + model: str | None + fw_version: str | None + api_version: int | None + + +def _get_init_data(host: str) -> RadioThermInitData: + tstat = radiotherm.get_thermostat(host) + tstat.timeout = TIMEOUT + name: str = tstat.name["raw"] + sys: dict[str, Any] = tstat.sys["raw"] + mac: str = dr.format_mac(sys["uuid"]) + model: str = tstat.model.get("raw") + return RadioThermInitData( + tstat, host, name, mac, model, sys.get("fw_version"), sys.get("api_version") + ) + + +async def async_get_init_data(hass: HomeAssistant, host: str) -> RadioThermInitData: + """Get the RadioInitData.""" + return await hass.async_add_executor_job(_get_init_data, host) + + +def _get_data(device: CommonThermostat) -> RadioThermUpdate: + # Request the current state from the thermostat. + # Radio thermostats are very slow, and sometimes don't respond + # very quickly. So we need to keep the number of calls to them + # to a bare minimum or we'll hit the Home Assistant 10 sec warning. We + # have to make one call to /tstat to get temps but we'll try and + # keep the other calls to a minimum. Even with this, these + # thermostats tend to time out sometimes when they're actively + # heating or cooling. + tstat: dict[str, Any] = device.tstat["raw"] + humidity: int | None = None + if isinstance(device, radiotherm.thermostat.CT80): + humidity = device.humidity["raw"] + return RadioThermUpdate(tstat, humidity) + + +async def async_get_data( + hass: HomeAssistant, device: CommonThermostat +) -> RadioThermUpdate: + """Fetch the data from the thermostat.""" + return await hass.async_add_executor_job(_get_data, device) diff --git a/homeassistant/components/radiotherm/manifest.json b/homeassistant/components/radiotherm/manifest.json index 72c2c8eb300..c6ae4e5bb06 100644 --- a/homeassistant/components/radiotherm/manifest.json +++ b/homeassistant/components/radiotherm/manifest.json @@ -3,7 +3,12 @@ "name": "Radio Thermostat", "documentation": "https://www.home-assistant.io/integrations/radiotherm", "requirements": ["radiotherm==2.1.0"], - "codeowners": ["@vinnyfuria"], + "codeowners": ["@bdraco", "@vinnyfuria"], "iot_class": "local_polling", - "loggers": ["radiotherm"] + "loggers": ["radiotherm"], + "dhcp": [ + { "hostname": "thermostat*", "macaddress": "5CDAD4*" }, + { "registered_devices": true } + ], + "config_flow": true } diff --git a/homeassistant/components/radiotherm/strings.json b/homeassistant/components/radiotherm/strings.json new file mode 100644 index 00000000000..22f17224285 --- /dev/null +++ b/homeassistant/components/radiotherm/strings.json @@ -0,0 +1,31 @@ +{ + "config": { + "flow_title": "{name} {model} ({host})", + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "confirm": { + "description": "Do you want to setup {name} {model} ({host})?" + } + }, + "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%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "Set a permanent hold when adjusting the temperature." + } + } + } + } +} diff --git a/homeassistant/components/radiotherm/translations/en.json b/homeassistant/components/radiotherm/translations/en.json new file mode 100644 index 00000000000..b524f188e59 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/en.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "Do you want to setup {name} {model} ({host})?" + }, + "user": { + "data": { + "host": "Host" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "Set a permanent hold when adjusting the temperature." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 06da1eae90c..843e3f7f7a9 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -280,6 +280,7 @@ FLOWS = { "qnap_qsw", "rachio", "radio_browser", + "radiotherm", "rainforest_eagle", "rainmachine", "rdw", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 015d70e2939..43bf7ca2715 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -77,6 +77,8 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'rachio', 'hostname': 'rachio-*', 'macaddress': '009D6B*'}, {'domain': 'rachio', 'hostname': 'rachio-*', 'macaddress': 'F0038C*'}, {'domain': 'rachio', 'hostname': 'rachio-*', 'macaddress': '74C63B*'}, + {'domain': 'radiotherm', 'hostname': 'thermostat*', 'macaddress': '5CDAD4*'}, + {'domain': 'radiotherm', 'registered_devices': True}, {'domain': 'rainforest_eagle', 'macaddress': 'D8D5B9*'}, {'domain': 'ring', 'hostname': 'ring*', 'macaddress': '0CAE7D*'}, {'domain': 'roomba', 'hostname': 'irobot-*', 'macaddress': '501479*'}, diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4331cc6dc36..c3178a0822f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1366,6 +1366,9 @@ rachiopy==1.0.3 # homeassistant.components.radio_browser radios==0.1.1 +# homeassistant.components.radiotherm +radiotherm==2.1.0 + # homeassistant.components.rainmachine regenmaschine==2022.06.0 diff --git a/tests/components/radiotherm/__init__.py b/tests/components/radiotherm/__init__.py new file mode 100644 index 00000000000..cf8bc0c7cc5 --- /dev/null +++ b/tests/components/radiotherm/__init__.py @@ -0,0 +1 @@ +"""Tests for the Radio Thermostat integration.""" diff --git a/tests/components/radiotherm/test_config_flow.py b/tests/components/radiotherm/test_config_flow.py new file mode 100644 index 00000000000..bc729a27e1c --- /dev/null +++ b/tests/components/radiotherm/test_config_flow.py @@ -0,0 +1,302 @@ +"""Test the Radio Thermostat config flow.""" +import socket +from unittest.mock import MagicMock, patch + +from radiotherm import CommonThermostat +from radiotherm.validate import RadiothermTstatError + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components import dhcp +from homeassistant.components.radiotherm.const import CONF_HOLD_TEMP, DOMAIN +from homeassistant.const import CONF_HOST + +from tests.common import MockConfigEntry + + +def _mock_radiotherm(): + tstat = MagicMock(autospec=CommonThermostat) + tstat.name = {"raw": "My Name"} + tstat.sys = { + "raw": {"uuid": "aabbccddeeff", "fw_version": "1.2.3", "api_version": "4.5.6"} + } + tstat.model = {"raw": "Model"} + return tstat + + +async def test_form(hass): + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.radiotherm.data.radiotherm.get_thermostat", + return_value=_mock_radiotherm(), + ), patch( + "homeassistant.components.radiotherm.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.2.3.4", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "My Name" + assert result2["data"] == { + "host": "1.2.3.4", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_unknown_error(hass): + """Test we handle unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.radiotherm.data.radiotherm.get_thermostat", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.2.3.4", + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.radiotherm.data.radiotherm.get_thermostat", + side_effect=RadiothermTstatError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.2.3.4", + }, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {CONF_HOST: "cannot_connect"} + + +async def test_import(hass): + """Test we get can import from yaml.""" + with patch( + "homeassistant.components.radiotherm.data.radiotherm.get_thermostat", + return_value=_mock_radiotherm(), + ), patch( + "homeassistant.components.radiotherm.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={CONF_HOST: "1.2.3.4", CONF_HOLD_TEMP: True}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "My Name" + assert result["data"] == { + CONF_HOST: "1.2.3.4", + } + assert result["options"] == { + CONF_HOLD_TEMP: True, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_cannot_connect(hass): + """Test we abort if we cannot connect on import from yaml.""" + with patch( + "homeassistant.components.radiotherm.data.radiotherm.get_thermostat", + side_effect=socket.timeout, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_HOST: "1.2.3.4", CONF_HOLD_TEMP: True}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_dhcp_can_confirm(hass): + """Test DHCP discovery flow can confirm right away.""" + + with patch( + "homeassistant.components.radiotherm.data.radiotherm.get_thermostat", + return_value=_mock_radiotherm(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + hostname="radiotherm", + ip="1.2.3.4", + macaddress="aa:bb:cc:dd:ee:ff", + ), + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "confirm" + assert result["description_placeholders"] == { + "host": "1.2.3.4", + "name": "My Name", + "model": "Model", + } + + with patch( + "homeassistant.components.radiotherm.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "My Name" + assert result2["data"] == { + "host": "1.2.3.4", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_dhcp_fails_to_connect(hass): + """Test DHCP discovery flow that fails to connect.""" + + with patch( + "homeassistant.components.radiotherm.data.radiotherm.get_thermostat", + side_effect=RadiothermTstatError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + hostname="radiotherm", + ip="1.2.3.4", + macaddress="aa:bb:cc:dd:ee:ff", + ), + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_dhcp_already_exists(hass): + """Test DHCP discovery flow that fails to connect.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.2.3.4"}, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.radiotherm.data.radiotherm.get_thermostat", + return_value=_mock_radiotherm(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + hostname="radiotherm", + ip="1.2.3.4", + macaddress="aa:bb:cc:dd:ee:ff", + ), + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_user_unique_id_already_exists(hass): + """Test creating an entry where the unique_id already exists.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.2.3.4"}, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.radiotherm.data.radiotherm.get_thermostat", + return_value=_mock_radiotherm(), + ), patch( + "homeassistant.components.radiotherm.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.2.3.4", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + +async def test_options_flow(hass): + """Test config flow options.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.2.3.4"}, + unique_id="aa:bb:cc:dd:ee:ff", + options={CONF_HOLD_TEMP: False}, + ) + + entry.add_to_hass(hass) + with patch( + "homeassistant.components.radiotherm.data.radiotherm.get_thermostat", + return_value=_mock_radiotherm(), + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(entry.entry_id) + await hass.async_block_till_done() + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + 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_HOLD_TEMP: True} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert entry.options == {CONF_HOLD_TEMP: True} From 7f0091280fe6c8b116bc7abae6cd5cca30f2b28d Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 6 Jun 2022 00:21:14 +0000 Subject: [PATCH 1275/3516] [ci skip] Translation update --- .../components/hassio/translations/bg.json | 1 + .../here_travel_time/translations/bg.json | 6 ++ .../components/scrape/translations/bg.json | 33 +++++++++ .../components/scrape/translations/et.json | 73 +++++++++++++++++++ .../components/scrape/translations/ja.json | 19 ++++- .../components/scrape/translations/nl.json | 57 +++++++++++++++ .../components/skybell/translations/bg.json | 21 ++++++ .../components/skybell/translations/ca.json | 21 ++++++ .../components/skybell/translations/de.json | 21 ++++++ .../components/skybell/translations/el.json | 21 ++++++ .../components/skybell/translations/en.json | 36 ++++----- .../components/skybell/translations/et.json | 21 ++++++ .../components/skybell/translations/fr.json | 21 ++++++ .../components/skybell/translations/ja.json | 21 ++++++ .../components/skybell/translations/nl.json | 21 ++++++ .../skybell/translations/pt-BR.json | 21 ++++++ .../skybell/translations/zh-Hant.json | 21 ++++++ .../tankerkoenig/translations/bg.json | 8 +- .../tankerkoenig/translations/nl.json | 8 +- 19 files changed, 429 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/scrape/translations/bg.json create mode 100644 homeassistant/components/scrape/translations/et.json create mode 100644 homeassistant/components/scrape/translations/nl.json create mode 100644 homeassistant/components/skybell/translations/bg.json create mode 100644 homeassistant/components/skybell/translations/ca.json create mode 100644 homeassistant/components/skybell/translations/de.json create mode 100644 homeassistant/components/skybell/translations/el.json create mode 100644 homeassistant/components/skybell/translations/et.json create mode 100644 homeassistant/components/skybell/translations/fr.json create mode 100644 homeassistant/components/skybell/translations/ja.json create mode 100644 homeassistant/components/skybell/translations/nl.json create mode 100644 homeassistant/components/skybell/translations/pt-BR.json create mode 100644 homeassistant/components/skybell/translations/zh-Hant.json diff --git a/homeassistant/components/hassio/translations/bg.json b/homeassistant/components/hassio/translations/bg.json index 941c3601bea..a5581901d78 100644 --- a/homeassistant/components/hassio/translations/bg.json +++ b/homeassistant/components/hassio/translations/bg.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 \u0430\u0433\u0435\u043d\u0442\u0430", "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", "docker_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u043d\u0430 Docker", diff --git a/homeassistant/components/here_travel_time/translations/bg.json b/homeassistant/components/here_travel_time/translations/bg.json index 73cda8df2bb..75bb03c2a1f 100644 --- a/homeassistant/components/here_travel_time/translations/bg.json +++ b/homeassistant/components/here_travel_time/translations/bg.json @@ -8,6 +8,12 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "destination_coordinates": { + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0434\u0435\u0441\u0442\u0438\u043d\u0430\u0446\u0438\u044f" + }, + "destination_entity_id": { + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0434\u0435\u0441\u0442\u0438\u043d\u0430\u0446\u0438\u044f" + }, "user": { "data": { "api_key": "API \u043a\u043b\u044e\u0447", diff --git a/homeassistant/components/scrape/translations/bg.json b/homeassistant/components/scrape/translations/bg.json new file mode 100644 index 00000000000..164dd477cb5 --- /dev/null +++ b/homeassistant/components/scrape/translations/bg.json @@ -0,0 +1,33 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "attribute": "\u0410\u0442\u0440\u0438\u0431\u0443\u0442", + "authentication": "\u0423\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "index": "\u0418\u043d\u0434\u0435\u043a\u0441", + "name": "\u0418\u043c\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0437\u0430 \u0438\u0437\u043c\u0435\u0440\u0432\u0430\u043d\u0435", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "attribute": "\u0410\u0442\u0440\u0438\u0431\u0443\u0442", + "authentication": "\u0423\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "index": "\u0418\u043d\u0434\u0435\u043a\u0441", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0437\u0430 \u0438\u0437\u043c\u0435\u0440\u0432\u0430\u043d\u0435" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/et.json b/homeassistant/components/scrape/translations/et.json new file mode 100644 index 00000000000..14daf835af8 --- /dev/null +++ b/homeassistant/components/scrape/translations/et.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud" + }, + "step": { + "user": { + "data": { + "attribute": "Atribuut", + "authentication": "Tuvastamine", + "device_class": "Seadme klass", + "headers": "P\u00e4ised", + "index": "Indeks", + "name": "Nimi", + "password": "Salas\u00f5na", + "resource": "Resurss", + "select": "Vali", + "state_class": "Oleku klass", + "unit_of_measurement": "M\u00f5\u00f5t\u00fchik", + "username": "Kasutajanimi", + "value_template": "V\u00e4\u00e4rtuse mall", + "verify_ssl": "Kontrolli SSL serti" + }, + "data_description": { + "attribute": "Hangi valitud sildi atribuudi v\u00e4\u00e4rtus", + "authentication": "HTTP-autentimise t\u00fc\u00fcp. Kas basic v\u00f5i digest", + "device_class": "Anduri t\u00fc\u00fcp/klass ikooni seadmiseks kasutajaliideses", + "headers": "Veebip\u00e4ringu jaoks kasutatavad p\u00e4ised", + "index": "M\u00e4\u00e4rab, milliseid CSS selektoriga tagastatud elemente kasutada.", + "resource": "V\u00e4\u00e4rtust sisaldava veebisaidi URL", + "select": "M\u00e4\u00e4rab, millist silti otsida. Lisateavet leiad Beautifulsoup CSS-i valijatest", + "state_class": "Anduri oleku klass", + "value_template": "M\u00e4\u00e4rab malli anduri oleku saamiseks", + "verify_ssl": "Lubab/keelab SSL/TLS-sertifikaadi kontrollimise, n\u00e4iteks kui see on ise allkirjastatud" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "attribute": "Atribuut", + "authentication": "Tuvastamine", + "device_class": "Seadme klss", + "headers": "P\u00e4ised", + "index": "Indeks", + "name": "", + "password": "Salas\u00f5na", + "resource": "Resurss", + "select": "Vali", + "state_class": "Oleku klass", + "unit_of_measurement": "M\u00f5\u00f5t\u00fchik", + "username": "", + "value_template": "V\u00e4\u00e4rtuse mall", + "verify_ssl": "" + }, + "data_description": { + "attribute": "Hangi valitud elemendi atribuudi v\u00e4\u00e4rtus", + "authentication": "HTTP kasutaja tuvastamise meetod; algeline v\u00f5i muu", + "device_class": "Kasutajaliidesesse lisatava anduri ikooni t\u00fc\u00fcp/klass", + "headers": "Veebip\u00e4ringus kasutatav p\u00e4is", + "index": "M\u00e4\u00e4rab milline element tagastatakse kasutatava CSS valiku alusel", + "resource": "Veebilehe URL ei sisalda soovitud v\u00e4\u00e4rtusi", + "select": "M\u00e4\u00e4rab otsitava v\u00f5tmes\u00f5na. Vaata Beatifulsoup CSS valimeid", + "state_class": "Anduri olekuklass", + "value_template": "M\u00e4\u00e4rab anduri oleku saamiseks vajaliku malli", + "verify_ssl": "Lubab v\u00f5i keelab SSL/TLS serdi tuvastamise n\u00e4iteks juhul kui sert on ise allkirjastatud" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/ja.json b/homeassistant/components/scrape/translations/ja.json index 3159fa65739..554a9d2c37b 100644 --- a/homeassistant/components/scrape/translations/ja.json +++ b/homeassistant/components/scrape/translations/ja.json @@ -22,9 +22,16 @@ "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" }, "data_description": { + "attribute": "\u9078\u629e\u3057\u305f\u30bf\u30b0\u306e\u5c5e\u6027\u306e\u5024\u3092\u53d6\u5f97\u3059\u308b", + "authentication": "HTTP\u8a8d\u8a3c\u306e\u7a2e\u985e\u3002\u30d9\u30fc\u30b7\u30c3\u30af\u307e\u305f\u306f\u30c0\u30a4\u30b8\u30a7\u30b9\u30c8\u306e\u3069\u3061\u3089\u304b", + "device_class": "\u30d5\u30ed\u30f3\u30c8\u30a8\u30f3\u30c9\u306b\u30a2\u30a4\u30b3\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u30bb\u30f3\u30b5\u30fc\u306e\u30bf\u30a4\u30d7/\u30af\u30e9\u30b9", "headers": "Web\u30ea\u30af\u30a8\u30b9\u30c8\u306b\u4f7f\u7528\u3059\u308b\u30d8\u30c3\u30c0\u30fc", + "index": "CSS\u30bb\u30ec\u30af\u30bf\u304c\u8fd4\u3059\u8981\u7d20\u306e\u3046\u3061\u3001\u3069\u306e\u8981\u7d20\u3092\u4f7f\u7528\u3059\u308b\u304b\u3092\u5b9a\u7fa9\u3057\u307e\u3059", + "resource": "\u5024\u3092\u542b\u3080\u30a6\u30a7\u30d6\u30b5\u30a4\u30c8\u306eURL", "select": "\u691c\u7d22\u3059\u308b\u30bf\u30b0\u3092\u5b9a\u7fa9\u3057\u307e\u3059\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001Beautifulsoup CSS selectors\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", - "state_class": "\u30bb\u30f3\u30b5\u30fc\u306e\u72b6\u614b\u30af\u30e9\u30b9(state_class)" + "state_class": "\u30bb\u30f3\u30b5\u30fc\u306e\u72b6\u614b\u30af\u30e9\u30b9(state_class)", + "value_template": "\u30bb\u30f3\u30b5\u30fc\u306e\u72b6\u614b\u3092\u53d6\u5f97\u3059\u308b\u305f\u3081\u306e\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8\u3092\u5b9a\u7fa9\u3057\u307e\u3059", + "verify_ssl": "SSL/TLS\u8a3c\u660e\u66f8\u306e\u691c\u8a3c\u3092\u6709\u52b9/\u7121\u52b9\u306b\u3057\u307e\u3059\u3002(\u81ea\u5df1\u7f72\u540d\u306e\u5834\u5408\u306a\u3069)" } } } @@ -49,8 +56,16 @@ "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" }, "data_description": { + "attribute": "\u9078\u629e\u3057\u305f\u30bf\u30b0\u306e\u5c5e\u6027\u306e\u5024\u3092\u53d6\u5f97\u3059\u308b", + "authentication": "HTTP\u8a8d\u8a3c\u306e\u7a2e\u985e\u3002\u30d9\u30fc\u30b7\u30c3\u30af\u307e\u305f\u306f\u30c0\u30a4\u30b8\u30a7\u30b9\u30c8\u306e\u3069\u3061\u3089\u304b", + "device_class": "\u30d5\u30ed\u30f3\u30c8\u30a8\u30f3\u30c9\u306b\u30a2\u30a4\u30b3\u30f3\u3092\u8a2d\u5b9a\u3059\u308b\u30bb\u30f3\u30b5\u30fc\u306e\u30bf\u30a4\u30d7/\u30af\u30e9\u30b9", "headers": "Web\u30ea\u30af\u30a8\u30b9\u30c8\u306b\u4f7f\u7528\u3059\u308b\u30d8\u30c3\u30c0\u30fc", - "state_class": "\u30bb\u30f3\u30b5\u30fc\u306e\u72b6\u614b\u30af\u30e9\u30b9(state_class)" + "index": "CSS\u30bb\u30ec\u30af\u30bf\u304c\u8fd4\u3059\u8981\u7d20\u306e\u3046\u3061\u3001\u3069\u306e\u8981\u7d20\u3092\u4f7f\u7528\u3059\u308b\u304b\u3092\u5b9a\u7fa9\u3057\u307e\u3059", + "resource": "\u5024\u3092\u542b\u3080\u30a6\u30a7\u30d6\u30b5\u30a4\u30c8\u306eURL", + "select": "\u691c\u7d22\u3059\u308b\u30bf\u30b0\u3092\u5b9a\u7fa9\u3057\u307e\u3059\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001Beautifulsoup CSS selectors\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044", + "state_class": "\u30bb\u30f3\u30b5\u30fc\u306e\u72b6\u614b\u30af\u30e9\u30b9(state_class)", + "value_template": "\u30bb\u30f3\u30b5\u30fc\u306e\u72b6\u614b\u3092\u53d6\u5f97\u3059\u308b\u305f\u3081\u306e\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8\u3092\u5b9a\u7fa9\u3057\u307e\u3059", + "verify_ssl": "SSL/TLS\u8a3c\u660e\u66f8\u306e\u691c\u8a3c\u3092\u6709\u52b9/\u7121\u52b9\u306b\u3057\u307e\u3059\u3002(\u81ea\u5df1\u7f72\u540d\u306e\u5834\u5408\u306a\u3069)" } } } diff --git a/homeassistant/components/scrape/translations/nl.json b/homeassistant/components/scrape/translations/nl.json new file mode 100644 index 00000000000..bdc26e94182 --- /dev/null +++ b/homeassistant/components/scrape/translations/nl.json @@ -0,0 +1,57 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd" + }, + "step": { + "user": { + "data": { + "attribute": "Attribuut", + "authentication": "Authenticatie", + "device_class": "Apparaatklasse", + "headers": "Headers", + "index": "Index", + "name": "Naam", + "password": "Wachtwoord", + "resource": "Bron", + "select": "Selecteer", + "state_class": "Staatklasse", + "unit_of_measurement": "Meeteenheid", + "username": "Gebruikersnaam", + "value_template": "Waardetemplate" + }, + "data_description": { + "attribute": "Haal de waarde op van een attribuut op de geselecteerde tag", + "resource": "De URL naar de website die de waarde bevat", + "state_class": "De state_class van de sensor" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "attribute": "Attribuut", + "authentication": "Authenticatie", + "device_class": "Apparaatklasse", + "headers": "Headers", + "index": "Index", + "name": "Naam", + "password": "Wachtwoord", + "resource": "Bron", + "select": "Selecteer", + "state_class": "Staatklasse", + "unit_of_measurement": "Meeteenheid", + "username": "Gebruikersnaam", + "value_template": "Waardetemplate" + }, + "data_description": { + "attribute": "Haal de waarde op van een attribuut op de geselecteerde tag", + "resource": "De URL naar de website die de waarde bevat", + "state_class": "De state_class van de sensor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/bg.json b/homeassistant/components/skybell/translations/bg.json new file mode 100644 index 00000000000..f3f182bbc3a --- /dev/null +++ b/homeassistant/components/skybell/translations/bg.json @@ -0,0 +1,21 @@ +{ + "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", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "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/skybell/translations/ca.json b/homeassistant/components/skybell/translations/ca.json new file mode 100644 index 00000000000..6aea0bdfc8f --- /dev/null +++ b/homeassistant/components/skybell/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El compte 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": { + "email": "Correu electr\u00f2nic", + "password": "Contrasenya" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/de.json b/homeassistant/components/skybell/translations/de.json new file mode 100644 index 00000000000..65a21e4b8f5 --- /dev/null +++ b/homeassistant/components/skybell/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "email": "E-Mail", + "password": "Passwort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/el.json b/homeassistant/components/skybell/translations/el.json new file mode 100644 index 00000000000..870068a34fd --- /dev/null +++ b/homeassistant/components/skybell/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/en.json b/homeassistant/components/skybell/translations/en.json index b84c4ebc999..d996004e5c4 100644 --- a/homeassistant/components/skybell/translations/en.json +++ b/homeassistant/components/skybell/translations/en.json @@ -1,21 +1,21 @@ { - "config": { - "step": { - "user": { - "data": { - "email": "Email", - "password": "Password" + "config": { + "abort": { + "already_configured": "Account 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": { + "email": "Email", + "password": "Password" + } + } } - } - }, - "error": { - "invalid_auth": "Invalid authentication", - "cannot_connect": "Failed to connect", - "unknown": "Unexpected error" - }, - "abort": { - "already_configured": "Account is already configured", - "reauth_successful": "Re-authentication was successful" } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/et.json b/homeassistant/components/skybell/translations/et.json new file mode 100644 index 00000000000..f6f6392d7a0 --- /dev/null +++ b/homeassistant/components/skybell/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "email": "E-posti aadress", + "password": "Salas\u00f5na" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/fr.json b/homeassistant/components/skybell/translations/fr.json new file mode 100644 index 00000000000..457e7c48157 --- /dev/null +++ b/homeassistant/components/skybell/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "email": "Courriel", + "password": "Mot de passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/ja.json b/homeassistant/components/skybell/translations/ja.json new file mode 100644 index 00000000000..9e2d562206f --- /dev/null +++ b/homeassistant/components/skybell/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "email": "E\u30e1\u30fc\u30eb", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/nl.json b/homeassistant/components/skybell/translations/nl.json new file mode 100644 index 00000000000..b937f595704 --- /dev/null +++ b/homeassistant/components/skybell/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" + }, + "error": { + "cannot_connect": "Verbindingsfout", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Wachtwoord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/pt-BR.json b/homeassistant/components/skybell/translations/pt-BR.json new file mode 100644 index 00000000000..7e7af1a011a --- /dev/null +++ b/homeassistant/components/skybell/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 est\u00e1 configurada", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Senha" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/zh-Hant.json b/homeassistant/components/skybell/translations/zh-Hant.json new file mode 100644 index 00000000000..1e614212c45 --- /dev/null +++ b/homeassistant/components/skybell/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\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": { + "email": "\u96fb\u5b50\u90f5\u4ef6", + "password": "\u5bc6\u78bc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/bg.json b/homeassistant/components/tankerkoenig/translations/bg.json index 700c4f3000b..8631e4a1daa 100644 --- a/homeassistant/components/tankerkoenig/translations/bg.json +++ b/homeassistant/components/tankerkoenig/translations/bg.json @@ -1,12 +1,18 @@ { "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" + "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", + "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" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + } + }, "user": { "data": { "api_key": "API \u043a\u043b\u044e\u0447", diff --git a/homeassistant/components/tankerkoenig/translations/nl.json b/homeassistant/components/tankerkoenig/translations/nl.json index ea4aea3ff95..57a8a1fcfe7 100644 --- a/homeassistant/components/tankerkoenig/translations/nl.json +++ b/homeassistant/components/tankerkoenig/translations/nl.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd" + "already_configured": "Locatie is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "invalid_auth": "Ongeldige authenticatie", "no_stations": "Kon geen station in bereik vinden." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API-sleutel" + } + }, "select_station": { "data": { "stations": "Stations" From 9ea504dd7b51f22bcc50455e235487c6a8b862e1 Mon Sep 17 00:00:00 2001 From: hesselonline Date: Mon, 6 Jun 2022 03:31:09 +0200 Subject: [PATCH 1276/3516] Bump wallbox to 0.4.9 (#72978) --- .../components/wallbox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wallbox/__init__.py | 39 ++++++++++++---- tests/components/wallbox/test_config_flow.py | 44 +++++-------------- tests/components/wallbox/test_init.py | 18 ++------ tests/components/wallbox/test_lock.py | 26 ++--------- tests/components/wallbox/test_number.py | 27 ++---------- tests/components/wallbox/test_switch.py | 29 +++--------- 9 files changed, 61 insertions(+), 128 deletions(-) diff --git a/homeassistant/components/wallbox/manifest.json b/homeassistant/components/wallbox/manifest.json index 2a4978b1cc1..914adda980a 100644 --- a/homeassistant/components/wallbox/manifest.json +++ b/homeassistant/components/wallbox/manifest.json @@ -3,7 +3,7 @@ "name": "Wallbox", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wallbox", - "requirements": ["wallbox==0.4.4"], + "requirements": ["wallbox==0.4.9"], "ssdp": [], "zeroconf": [], "homekit": {}, diff --git a/requirements_all.txt b/requirements_all.txt index f365da06766..34c3904aab9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2418,7 +2418,7 @@ vultr==0.1.2 wakeonlan==2.0.1 # homeassistant.components.wallbox -wallbox==0.4.4 +wallbox==0.4.9 # homeassistant.components.waqi waqiasync==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c3178a0822f..6b8b4d0dc35 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1597,7 +1597,7 @@ vultr==0.1.2 wakeonlan==2.0.1 # homeassistant.components.wallbox -wallbox==0.4.4 +wallbox==0.4.9 # homeassistant.components.folder_watcher watchdog==2.1.8 diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py index 2b35bb76b2f..8c979c42ebe 100644 --- a/tests/components/wallbox/__init__.py +++ b/tests/components/wallbox/__init__.py @@ -26,7 +26,7 @@ from homeassistant.components.wallbox.const import ( from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from .const import ERROR, JWT, STATUS, TTL, USER_ID +from .const import ERROR, STATUS, TTL, USER_ID from tests.common import MockConfigEntry @@ -54,11 +54,32 @@ test_response = json.loads( authorisation_response = json.loads( json.dumps( { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 200, + "data": { + "attributes": { + "token": "fakekeyhere", + USER_ID: 12345, + TTL: 145656758, + ERROR: "false", + STATUS: 200, + } + } + } + ) +) + + +authorisation_response_unauthorised = json.loads( + json.dumps( + { + "data": { + "attributes": { + "token": "fakekeyhere", + USER_ID: 12345, + TTL: 145656758, + ERROR: "false", + STATUS: 404, + } + } } ) ) @@ -81,7 +102,7 @@ async def setup_integration(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=HTTPStatus.OK, ) @@ -107,7 +128,7 @@ async def setup_integration_connection_error(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=HTTPStatus.FORBIDDEN, ) @@ -133,7 +154,7 @@ async def setup_integration_read_only(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=HTTPStatus.OK, ) diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py index a3e6a724eef..68f70878592 100644 --- a/tests/components/wallbox/test_config_flow.py +++ b/tests/components/wallbox/test_config_flow.py @@ -18,8 +18,12 @@ from homeassistant.components.wallbox.const import ( ) from homeassistant.core import HomeAssistant -from tests.components.wallbox import entry, setup_integration -from tests.components.wallbox.const import ERROR, JWT, STATUS, TTL, USER_ID +from tests.components.wallbox import ( + authorisation_response, + authorisation_response_unauthorised, + entry, + setup_integration, +) test_response = json.loads( json.dumps( @@ -34,30 +38,6 @@ test_response = json.loads( ) ) -authorisation_response = json.loads( - json.dumps( - { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 200, - } - ) -) - -authorisation_response_unauthorised = json.loads( - json.dumps( - { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 404, - } - ) -) - async def test_show_set_form(hass: HomeAssistant) -> None: """Test that the setup form is served.""" @@ -77,7 +57,7 @@ async def test_form_cannot_authenticate(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=HTTPStatus.FORBIDDEN, ) @@ -107,7 +87,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response_unauthorised, status_code=HTTPStatus.NOT_FOUND, ) @@ -137,7 +117,7 @@ async def test_form_validate_input(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=HTTPStatus.OK, ) @@ -166,8 +146,8 @@ async def test_form_reauth(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", - text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + "https://user-api.wall-box.com/users/signin", + json=authorisation_response, status_code=200, ) mock_request.get( @@ -206,7 +186,7 @@ async def test_form_reauth_invalid(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', status_code=200, ) diff --git a/tests/components/wallbox/test_init.py b/tests/components/wallbox/test_init.py index b8862531a82..5080bab87ea 100644 --- a/tests/components/wallbox/test_init.py +++ b/tests/components/wallbox/test_init.py @@ -11,24 +11,12 @@ from . import test_response from tests.components.wallbox import ( DOMAIN, + authorisation_response, entry, setup_integration, setup_integration_connection_error, setup_integration_read_only, ) -from tests.components.wallbox.const import ERROR, JWT, STATUS, TTL, USER_ID - -authorisation_response = json.loads( - json.dumps( - { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 200, - } - ) -) async def test_wallbox_setup_unload_entry(hass: HomeAssistant) -> None: @@ -59,7 +47,7 @@ async def test_wallbox_refresh_failed_invalid_auth(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=403, ) @@ -85,7 +73,7 @@ async def test_wallbox_refresh_failed_connection_error(hass: HomeAssistant) -> N with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) diff --git a/tests/components/wallbox/test_lock.py b/tests/components/wallbox/test_lock.py index d8b2fcf182c..fbcb07b0e90 100644 --- a/tests/components/wallbox/test_lock.py +++ b/tests/components/wallbox/test_lock.py @@ -10,30 +10,12 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from tests.components.wallbox import ( + authorisation_response, entry, setup_integration, setup_integration_read_only, ) -from tests.components.wallbox.const import ( - ERROR, - JWT, - MOCK_LOCK_ENTITY_ID, - STATUS, - TTL, - USER_ID, -) - -authorisation_response = json.loads( - json.dumps( - { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 200, - } - ) -) +from tests.components.wallbox.const import MOCK_LOCK_ENTITY_ID async def test_wallbox_lock_class(hass: HomeAssistant) -> None: @@ -47,7 +29,7 @@ async def test_wallbox_lock_class(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) @@ -85,7 +67,7 @@ async def test_wallbox_lock_class_connection_error(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) diff --git a/tests/components/wallbox/test_number.py b/tests/components/wallbox/test_number.py index 5c024d0f4ac..c8e8b29f28b 100644 --- a/tests/components/wallbox/test_number.py +++ b/tests/components/wallbox/test_number.py @@ -9,27 +9,8 @@ from homeassistant.components.wallbox import CHARGER_MAX_CHARGING_CURRENT_KEY from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant -from tests.components.wallbox import entry, setup_integration -from tests.components.wallbox.const import ( - ERROR, - JWT, - MOCK_NUMBER_ENTITY_ID, - STATUS, - TTL, - USER_ID, -) - -authorisation_response = json.loads( - json.dumps( - { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 200, - } - ) -) +from tests.components.wallbox import authorisation_response, entry, setup_integration +from tests.components.wallbox.const import MOCK_NUMBER_ENTITY_ID async def test_wallbox_number_class(hass: HomeAssistant) -> None: @@ -39,7 +20,7 @@ async def test_wallbox_number_class(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) @@ -68,7 +49,7 @@ async def test_wallbox_number_class_connection_error(hass: HomeAssistant) -> Non with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) diff --git a/tests/components/wallbox/test_switch.py b/tests/components/wallbox/test_switch.py index 6ade320319a..c57fee0353f 100644 --- a/tests/components/wallbox/test_switch.py +++ b/tests/components/wallbox/test_switch.py @@ -10,27 +10,8 @@ from homeassistant.components.wallbox.const import CHARGER_STATUS_ID_KEY from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant -from tests.components.wallbox import entry, setup_integration -from tests.components.wallbox.const import ( - ERROR, - JWT, - MOCK_SWITCH_ENTITY_ID, - STATUS, - TTL, - USER_ID, -) - -authorisation_response = json.loads( - json.dumps( - { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 200, - } - ) -) +from tests.components.wallbox import authorisation_response, entry, setup_integration +from tests.components.wallbox.const import MOCK_SWITCH_ENTITY_ID async def test_wallbox_switch_class(hass: HomeAssistant) -> None: @@ -44,7 +25,7 @@ async def test_wallbox_switch_class(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) @@ -82,7 +63,7 @@ async def test_wallbox_switch_class_connection_error(hass: HomeAssistant) -> Non with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) @@ -121,7 +102,7 @@ async def test_wallbox_switch_class_authentication_error(hass: HomeAssistant) -> with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) From a6f6f0ac5e662751b90239872e63532b7efe9bf6 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 5 Jun 2022 19:33:27 -0600 Subject: [PATCH 1277/3516] Fix unhandled exception when RainMachine coordinator data doesn't exist (#73055) --- homeassistant/components/rainmachine/binary_sensor.py | 7 +++++-- homeassistant/components/rainmachine/sensor.py | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 1818222a8f4..7a13515db3b 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -139,8 +139,11 @@ async def async_setup_entry( entry, coordinator, controller, description ) for description in BINARY_SENSOR_DESCRIPTIONS - if (coordinator := coordinators[description.api_category]) is not None - and key_exists(coordinator.data, description.data_key) + if ( + (coordinator := coordinators[description.api_category]) is not None + and coordinator.data + and key_exists(coordinator.data, description.data_key) + ) ] ) diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 522c57cf7a2..cc37189aa49 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -133,8 +133,11 @@ async def async_setup_entry( entry, coordinator, controller, description ) for description in SENSOR_DESCRIPTIONS - if (coordinator := coordinators[description.api_category]) is not None - and key_exists(coordinator.data, description.data_key) + if ( + (coordinator := coordinators[description.api_category]) is not None + and coordinator.data + and key_exists(coordinator.data, description.data_key) + ) ] zone_coordinator = coordinators[DATA_ZONES] From 5fe9e8cb1cf04041aca29780f696f1f191d3da3d Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 6 Jun 2022 03:39:42 +0200 Subject: [PATCH 1278/3516] Throttle multiple requests to the velux gateway (#72974) --- homeassistant/components/velux/cover.py | 11 ++++++----- homeassistant/components/velux/light.py | 2 ++ homeassistant/components/velux/scene.py | 2 ++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index f4f449509a0..26cccfce6ce 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -17,6 +17,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DATA_VELUX, VeluxEntity +PARALLEL_UPDATES = 1 + async def async_setup_platform( hass: HomeAssistant, @@ -97,12 +99,11 @@ class VeluxCover(VeluxEntity, CoverEntity): async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" - if ATTR_POSITION in kwargs: - position_percent = 100 - kwargs[ATTR_POSITION] + position_percent = 100 - kwargs[ATTR_POSITION] - await self.node.set_position( - Position(position_percent=position_percent), wait_for_completion=False - ) + await self.node.set_position( + Position(position_percent=position_percent), wait_for_completion=False + ) async def async_stop_cover(self, **kwargs): """Stop the cover.""" diff --git a/homeassistant/components/velux/light.py b/homeassistant/components/velux/light.py index e6ca8ea5c2b..f8a52fc05c1 100644 --- a/homeassistant/components/velux/light.py +++ b/homeassistant/components/velux/light.py @@ -10,6 +10,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DATA_VELUX, VeluxEntity +PARALLEL_UPDATES = 1 + async def async_setup_platform( hass: HomeAssistant, diff --git a/homeassistant/components/velux/scene.py b/homeassistant/components/velux/scene.py index 324bae027fc..20f94c74f0b 100644 --- a/homeassistant/components/velux/scene.py +++ b/homeassistant/components/velux/scene.py @@ -10,6 +10,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import _LOGGER, DATA_VELUX +PARALLEL_UPDATES = 1 + async def async_setup_platform( hass: HomeAssistant, From c4763031ab47d2cd9eaad965c0eed506a8f83306 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Sun, 5 Jun 2022 22:27:21 -0400 Subject: [PATCH 1279/3516] Fix elk attributes not being json serializable (#73096) * Fix jsonifying. * Only serialize Enums --- homeassistant/components/elkm1/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 9a7c7dbc43a..cf1cac3bdb3 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from enum import Enum import logging import re from types import MappingProxyType @@ -481,7 +482,10 @@ class ElkEntity(Entity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return the default attributes of the element.""" - return {**self._element.as_dict(), **self.initial_attrs()} + dict_as_str = {} + for key, val in self._element.as_dict().items(): + dict_as_str[key] = val.value if isinstance(val, Enum) else val + return {**dict_as_str, **self.initial_attrs()} @property def available(self) -> bool: From 1744e7224b126fd1b8bc7c5f2b3eb5f341b7e0b9 Mon Sep 17 00:00:00 2001 From: Igor Loborec Date: Mon, 6 Jun 2022 03:27:46 +0100 Subject: [PATCH 1280/3516] Remove available property from Kodi (#73103) --- homeassistant/components/kodi/media_player.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index e19ffc6219c..2b509ed0e08 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -636,11 +636,6 @@ class KodiEntity(MediaPlayerEntity): return None - @property - def available(self): - """Return True if entity is available.""" - return not self._connect_error - async def async_turn_on(self): """Turn the media player on.""" _LOGGER.debug("Firing event to turn on device") From db53ab5fd0e22fe109ca76953b81d0b074f54950 Mon Sep 17 00:00:00 2001 From: Matrix Date: Mon, 6 Jun 2022 11:58:29 +0800 Subject: [PATCH 1281/3516] Add Yolink lock support (#73069) * Add yolink lock support * Update .coveragerct * extract the commons --- .coveragerc | 1 + homeassistant/components/yolink/__init__.py | 8 ++- .../components/yolink/binary_sensor.py | 5 +- homeassistant/components/yolink/const.py | 1 + homeassistant/components/yolink/entity.py | 18 +++++ homeassistant/components/yolink/lock.py | 65 +++++++++++++++++++ homeassistant/components/yolink/sensor.py | 7 +- homeassistant/components/yolink/siren.py | 17 +---- homeassistant/components/yolink/switch.py | 17 +---- 9 files changed, 105 insertions(+), 34 deletions(-) create mode 100644 homeassistant/components/yolink/lock.py diff --git a/.coveragerc b/.coveragerc index 0f9c3e4998a..75ed4663869 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1495,6 +1495,7 @@ omit = homeassistant/components/yolink/const.py homeassistant/components/yolink/coordinator.py homeassistant/components/yolink/entity.py + homeassistant/components/yolink/lock.py homeassistant/components/yolink/sensor.py homeassistant/components/yolink/siren.py homeassistant/components/yolink/switch.py diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 7eb6b0229f0..21d36d33a30 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -24,7 +24,13 @@ from .coordinator import YoLinkCoordinator SCAN_INTERVAL = timedelta(minutes=5) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SIREN, Platform.SWITCH] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.LOCK, + Platform.SENSOR, + Platform.SIREN, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/yolink/binary_sensor.py b/homeassistant/components/yolink/binary_sensor.py index d5c9ddedb84..17b25c57d94 100644 --- a/homeassistant/components/yolink/binary_sensor.py +++ b/homeassistant/components/yolink/binary_sensor.py @@ -96,7 +96,7 @@ async def async_setup_entry( if description.exists_fn(binary_sensor_device_coordinator.device): entities.append( YoLinkBinarySensorEntity( - binary_sensor_device_coordinator, description + config_entry, binary_sensor_device_coordinator, description ) ) async_add_entities(entities) @@ -109,11 +109,12 @@ class YoLinkBinarySensorEntity(YoLinkEntity, BinarySensorEntity): def __init__( self, + config_entry: ConfigEntry, coordinator: YoLinkCoordinator, description: YoLinkBinarySensorEntityDescription, ) -> None: """Init YoLink Sensor.""" - super().__init__(coordinator) + super().__init__(config_entry, coordinator) self.entity_description = description self._attr_unique_id = ( f"{coordinator.device.device_id} {self.entity_description.key}" diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index 16304e0de4b..44f4f3104f7 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -20,3 +20,4 @@ ATTR_DEVICE_LEAK_SENSOR = "LeakSensor" ATTR_DEVICE_VIBRATION_SENSOR = "VibrationSensor" ATTR_DEVICE_OUTLET = "Outlet" ATTR_DEVICE_SIREN = "Siren" +ATTR_DEVICE_LOCK = "Lock" diff --git a/homeassistant/components/yolink/entity.py b/homeassistant/components/yolink/entity.py index 5365681739e..02f063a282a 100644 --- a/homeassistant/components/yolink/entity.py +++ b/homeassistant/components/yolink/entity.py @@ -3,7 +3,11 @@ from __future__ import annotations from abc import abstractmethod +from yolink.exception import YoLinkAuthFailError, YoLinkClientError + +from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -16,10 +20,12 @@ class YoLinkEntity(CoordinatorEntity[YoLinkCoordinator]): def __init__( self, + config_entry: ConfigEntry, coordinator: YoLinkCoordinator, ) -> None: """Init YoLink Entity.""" super().__init__(coordinator) + self.config_entry = config_entry @property def device_id(self) -> str: @@ -52,3 +58,15 @@ class YoLinkEntity(CoordinatorEntity[YoLinkCoordinator]): @abstractmethod def update_entity_state(self, state: dict) -> None: """Parse and update entity state, should be overridden.""" + + async def call_device_api(self, command: str, params: dict) -> None: + """Call device Api.""" + try: + # call_device_http_api will check result, fail by raise YoLinkClientError + await self.coordinator.device.call_device_http_api(command, params) + except YoLinkAuthFailError as yl_auth_err: + self.config_entry.async_start_reauth(self.hass) + raise HomeAssistantError(yl_auth_err) from yl_auth_err + except YoLinkClientError as yl_client_err: + self.coordinator.last_update_success = False + raise HomeAssistantError(yl_client_err) from yl_client_err diff --git a/homeassistant/components/yolink/lock.py b/homeassistant/components/yolink/lock.py new file mode 100644 index 00000000000..ca340c2e762 --- /dev/null +++ b/homeassistant/components/yolink/lock.py @@ -0,0 +1,65 @@ +"""YoLink Lock.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.lock import LockEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ATTR_COORDINATORS, ATTR_DEVICE_LOCK, DOMAIN +from .coordinator import YoLinkCoordinator +from .entity import YoLinkEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up YoLink lock from a config entry.""" + device_coordinators = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATORS] + entities = [ + YoLinkLockEntity(config_entry, device_coordinator) + for device_coordinator in device_coordinators.values() + if device_coordinator.device.device_type == ATTR_DEVICE_LOCK + ] + async_add_entities(entities) + + +class YoLinkLockEntity(YoLinkEntity, LockEntity): + """YoLink Lock Entity.""" + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: YoLinkCoordinator, + ) -> None: + """Init YoLink Lock.""" + super().__init__(config_entry, coordinator) + self._attr_unique_id = f"{coordinator.device.device_id}_lock_state" + self._attr_name = f"{coordinator.device.device_name}(LockState)" + + @callback + def update_entity_state(self, state: dict[str, Any]) -> None: + """Update HA Entity State.""" + state_value = state.get("state") + self._attr_is_locked = ( + state_value == "locked" if state_value is not None else None + ) + self.async_write_ha_state() + + async def call_lock_state_change(self, state: str) -> None: + """Call setState api to change lock state.""" + await self.call_device_api("setState", {"state": state}) + self._attr_is_locked = state == "lock" + self.async_write_ha_state() + + async def async_lock(self, **kwargs: Any) -> None: + """Lock device.""" + await self.call_lock_state_change("lock") + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock device.""" + await self.call_lock_state_change("unlock") diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 917a93c310d..e0d746219fb 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -21,6 +21,7 @@ from homeassistant.util import percentage from .const import ( ATTR_COORDINATORS, ATTR_DEVICE_DOOR_SENSOR, + ATTR_DEVICE_LOCK, ATTR_DEVICE_MOTION_SENSOR, ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_VIBRATION_SENSOR, @@ -51,6 +52,7 @@ SENSOR_DEVICE_TYPE = [ ATTR_DEVICE_MOTION_SENSOR, ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_VIBRATION_SENSOR, + ATTR_DEVICE_LOCK, ] BATTERY_POWER_SENSOR = [ @@ -58,6 +60,7 @@ BATTERY_POWER_SENSOR = [ ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_MOTION_SENSOR, ATTR_DEVICE_VIBRATION_SENSOR, + ATTR_DEVICE_LOCK, ] @@ -112,6 +115,7 @@ async def async_setup_entry( if description.exists_fn(sensor_device_coordinator.device): entities.append( YoLinkSensorEntity( + config_entry, sensor_device_coordinator, description, ) @@ -126,11 +130,12 @@ class YoLinkSensorEntity(YoLinkEntity, SensorEntity): def __init__( self, + config_entry: ConfigEntry, coordinator: YoLinkCoordinator, description: YoLinkSensorEntityDescription, ) -> None: """Init YoLink Sensor.""" - super().__init__(coordinator) + super().__init__(config_entry, coordinator) self.entity_description = description self._attr_unique_id = ( f"{coordinator.device.device_id} {self.entity_description.key}" diff --git a/homeassistant/components/yolink/siren.py b/homeassistant/components/yolink/siren.py index 7e67dfb12f1..fd1c8e89e07 100644 --- a/homeassistant/components/yolink/siren.py +++ b/homeassistant/components/yolink/siren.py @@ -6,7 +6,6 @@ from dataclasses import dataclass from typing import Any from yolink.device import YoLinkDevice -from yolink.exception import YoLinkAuthFailError, YoLinkClientError from homeassistant.components.siren import ( SirenEntity, @@ -15,7 +14,6 @@ from homeassistant.components.siren import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_COORDINATORS, ATTR_DEVICE_SIREN, DOMAIN @@ -79,8 +77,7 @@ class YoLinkSirenEntity(YoLinkEntity, SirenEntity): description: YoLinkSirenEntityDescription, ) -> None: """Init YoLink Siren.""" - super().__init__(coordinator) - self.config_entry = config_entry + super().__init__(config_entry, coordinator) self.entity_description = description self._attr_unique_id = ( f"{coordinator.device.device_id} {self.entity_description.key}" @@ -102,17 +99,7 @@ class YoLinkSirenEntity(YoLinkEntity, SirenEntity): async def call_state_change(self, state: bool) -> None: """Call setState api to change siren state.""" - try: - # call_device_http_api will check result, fail by raise YoLinkClientError - await self.coordinator.device.call_device_http_api( - "setState", {"state": {"alarm": state}} - ) - except YoLinkAuthFailError as yl_auth_err: - self.config_entry.async_start_reauth(self.hass) - raise HomeAssistantError(yl_auth_err) from yl_auth_err - except YoLinkClientError as yl_client_err: - self.coordinator.last_update_success = False - raise HomeAssistantError(yl_client_err) from yl_client_err + await self.call_device_api("setState", {"state": {"alarm": state}}) self._attr_is_on = self.entity_description.value("alert" if state else "normal") self.async_write_ha_state() diff --git a/homeassistant/components/yolink/switch.py b/homeassistant/components/yolink/switch.py index f16dc781a9c..723043100b3 100644 --- a/homeassistant/components/yolink/switch.py +++ b/homeassistant/components/yolink/switch.py @@ -6,7 +6,6 @@ from dataclasses import dataclass from typing import Any from yolink.device import YoLinkDevice -from yolink.exception import YoLinkAuthFailError, YoLinkClientError from homeassistant.components.switch import ( SwitchDeviceClass, @@ -15,7 +14,6 @@ from homeassistant.components.switch import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_COORDINATORS, ATTR_DEVICE_OUTLET, DOMAIN @@ -80,8 +78,7 @@ class YoLinkSwitchEntity(YoLinkEntity, SwitchEntity): description: YoLinkSwitchEntityDescription, ) -> None: """Init YoLink Outlet.""" - super().__init__(coordinator) - self.config_entry = config_entry + super().__init__(config_entry, coordinator) self.entity_description = description self._attr_unique_id = ( f"{coordinator.device.device_id} {self.entity_description.key}" @@ -100,17 +97,7 @@ class YoLinkSwitchEntity(YoLinkEntity, SwitchEntity): async def call_state_change(self, state: str) -> None: """Call setState api to change outlet state.""" - try: - # call_device_http_api will check result, fail by raise YoLinkClientError - await self.coordinator.device.call_device_http_api( - "setState", {"state": state} - ) - except YoLinkAuthFailError as yl_auth_err: - self.config_entry.async_start_reauth(self.hass) - raise HomeAssistantError(yl_auth_err) from yl_auth_err - except YoLinkClientError as yl_client_err: - self.coordinator.last_update_success = False - raise HomeAssistantError(yl_client_err) from yl_client_err + await self.call_device_api("setState", {"state": state}) self._attr_is_on = self.entity_description.value(state) self.async_write_ha_state() From 3744edc512fb9d4e4f7acf2405fe086105e114ad Mon Sep 17 00:00:00 2001 From: lymanepp <4195527+lymanepp@users.noreply.github.com> Date: Mon, 6 Jun 2022 00:10:33 -0400 Subject: [PATCH 1282/3516] Tomorrowio utc fix (#73102) * Discard past data using local time instead of UTC * Tweak changes to fix tests * Cleanup --- homeassistant/components/tomorrowio/weather.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tomorrowio/weather.py b/homeassistant/components/tomorrowio/weather.py index bde6e6b996b..346f673362e 100644 --- a/homeassistant/components/tomorrowio/weather.py +++ b/homeassistant/components/tomorrowio/weather.py @@ -203,13 +203,16 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): max_forecasts = MAX_FORECASTS[self.forecast_type] forecast_count = 0 + # Convert utcnow to local to be compatible with tests + today = dt_util.as_local(dt_util.utcnow()).date() + # Set default values (in cases where keys don't exist), None will be # returned. Override properties per forecast type as needed for forecast in raw_forecasts: forecast_dt = dt_util.parse_datetime(forecast[TMRW_ATTR_TIMESTAMP]) # Throw out past data - if forecast_dt.date() < dt_util.utcnow().date(): + if dt_util.as_local(forecast_dt).date() < today: continue values = forecast["values"] From c66b000d34b2a22fbaef3dfd4f6ea4b7a807bacc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Jun 2022 18:13:31 -1000 Subject: [PATCH 1283/3516] Reduce branching in generated lambda_stmts (#73042) --- homeassistant/components/recorder/history.py | 33 ++++-- .../components/recorder/statistics.py | 103 ++++++++++++------ tests/components/recorder/test_statistics.py | 62 +++++++++++ 3 files changed, 153 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 49796bd0158..5dd5c0d3040 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -15,6 +15,7 @@ from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Subquery from homeassistant.components import recorder from homeassistant.components.websocket_api.const import ( @@ -485,6 +486,25 @@ def _get_states_for_entites_stmt( return stmt +def _generate_most_recent_states_by_date( + run_start: datetime, + utc_point_in_time: datetime, +) -> Subquery: + """Generate the sub query for the most recent states by data.""" + return ( + select( + States.entity_id.label("max_entity_id"), + func.max(States.last_updated).label("max_last_updated"), + ) + .filter( + (States.last_updated >= run_start) + & (States.last_updated < utc_point_in_time) + ) + .group_by(States.entity_id) + .subquery() + ) + + def _get_states_for_all_stmt( schema_version: int, run_start: datetime, @@ -500,17 +520,8 @@ def _get_states_for_all_stmt( # query, then filter out unwanted domains as well as applying the custom filter. # This filtering can't be done in the inner query because the domain column is # not indexed and we can't control what's in the custom filter. - most_recent_states_by_date = ( - select( - States.entity_id.label("max_entity_id"), - func.max(States.last_updated).label("max_last_updated"), - ) - .filter( - (States.last_updated >= run_start) - & (States.last_updated < utc_point_in_time) - ) - .group_by(States.entity_id) - .subquery() + most_recent_states_by_date = _generate_most_recent_states_by_date( + run_start, utc_point_in_time ) stmt += lambda q: q.where( States.state_id diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 39fcb954ee9..012b34ec0ef 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -20,6 +20,7 @@ from sqlalchemy.exc import SQLAlchemyError, StatementError from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal_column, true from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Subquery import voluptuous as vol from homeassistant.const import ( @@ -484,14 +485,13 @@ def _compile_hourly_statistics_summary_mean_stmt( start_time: datetime, end_time: datetime ) -> StatementLambdaElement: """Generate the summary mean statement for hourly statistics.""" - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SUMMARY_MEAN)) - stmt += ( - lambda q: q.filter(StatisticsShortTerm.start >= start_time) + return lambda_stmt( + lambda: select(*QUERY_STATISTICS_SUMMARY_MEAN) + .filter(StatisticsShortTerm.start >= start_time) .filter(StatisticsShortTerm.start < end_time) .group_by(StatisticsShortTerm.metadata_id) .order_by(StatisticsShortTerm.metadata_id) ) - return stmt def compile_hourly_statistics( @@ -985,26 +985,43 @@ def _statistics_during_period_stmt( start_time: datetime, end_time: datetime | None, metadata_ids: list[int] | None, - table: type[Statistics | StatisticsShortTerm], ) -> StatementLambdaElement: """Prepare a database query for statistics during a given period. This prepares a lambda_stmt query, so we don't insert the parameters yet. """ - if table == StatisticsShortTerm: - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SHORT_TERM)) - else: - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS)) - - stmt += lambda q: q.filter(table.start >= start_time) - + stmt = lambda_stmt( + lambda: select(*QUERY_STATISTICS).filter(Statistics.start >= start_time) + ) if end_time is not None: - stmt += lambda q: q.filter(table.start < end_time) - + stmt += lambda q: q.filter(Statistics.start < end_time) if metadata_ids: - stmt += lambda q: q.filter(table.metadata_id.in_(metadata_ids)) + stmt += lambda q: q.filter(Statistics.metadata_id.in_(metadata_ids)) + stmt += lambda q: q.order_by(Statistics.metadata_id, Statistics.start) + return stmt - stmt += lambda q: q.order_by(table.metadata_id, table.start) + +def _statistics_during_period_stmt_short_term( + start_time: datetime, + end_time: datetime | None, + metadata_ids: list[int] | None, +) -> StatementLambdaElement: + """Prepare a database query for short term statistics during a given period. + + This prepares a lambda_stmt query, so we don't insert the parameters yet. + """ + stmt = lambda_stmt( + lambda: select(*QUERY_STATISTICS_SHORT_TERM).filter( + StatisticsShortTerm.start >= start_time + ) + ) + if end_time is not None: + stmt += lambda q: q.filter(StatisticsShortTerm.start < end_time) + if metadata_ids: + stmt += lambda q: q.filter(StatisticsShortTerm.metadata_id.in_(metadata_ids)) + stmt += lambda q: q.order_by( + StatisticsShortTerm.metadata_id, StatisticsShortTerm.start + ) return stmt @@ -1034,10 +1051,12 @@ def statistics_during_period( if period == "5minute": table = StatisticsShortTerm + stmt = _statistics_during_period_stmt_short_term( + start_time, end_time, metadata_ids + ) else: table = Statistics - - stmt = _statistics_during_period_stmt(start_time, end_time, metadata_ids, table) + stmt = _statistics_during_period_stmt(start_time, end_time, metadata_ids) stats = execute_stmt_lambda_element(session, stmt) if not stats: @@ -1069,19 +1088,27 @@ def statistics_during_period( def _get_last_statistics_stmt( metadata_id: int, number_of_stats: int, - table: type[Statistics | StatisticsShortTerm], ) -> StatementLambdaElement: """Generate a statement for number_of_stats statistics for a given statistic_id.""" - if table == StatisticsShortTerm: - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SHORT_TERM)) - else: - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS)) - stmt += ( - lambda q: q.filter_by(metadata_id=metadata_id) - .order_by(table.metadata_id, table.start.desc()) + return lambda_stmt( + lambda: select(*QUERY_STATISTICS) + .filter_by(metadata_id=metadata_id) + .order_by(Statistics.metadata_id, Statistics.start.desc()) + .limit(number_of_stats) + ) + + +def _get_last_statistics_short_term_stmt( + metadata_id: int, + number_of_stats: int, +) -> StatementLambdaElement: + """Generate a statement for number_of_stats short term statistics for a given statistic_id.""" + return lambda_stmt( + lambda: select(*QUERY_STATISTICS_SHORT_TERM) + .filter_by(metadata_id=metadata_id) + .order_by(StatisticsShortTerm.metadata_id, StatisticsShortTerm.start.desc()) .limit(number_of_stats) ) - return stmt def _get_last_statistics( @@ -1099,7 +1126,10 @@ def _get_last_statistics( if not metadata: return {} metadata_id = metadata[statistic_id][0] - stmt = _get_last_statistics_stmt(metadata_id, number_of_stats, table) + if table == Statistics: + stmt = _get_last_statistics_stmt(metadata_id, number_of_stats) + else: + stmt = _get_last_statistics_short_term_stmt(metadata_id, number_of_stats) stats = execute_stmt_lambda_element(session, stmt) if not stats: @@ -1136,12 +1166,9 @@ def get_last_short_term_statistics( ) -def _latest_short_term_statistics_stmt( - metadata_ids: list[int], -) -> StatementLambdaElement: - """Create the statement for finding the latest short term stat rows.""" - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SHORT_TERM)) - most_recent_statistic_row = ( +def _generate_most_recent_statistic_row(metadata_ids: list[int]) -> Subquery: + """Generate the subquery to find the most recent statistic row.""" + return ( select( StatisticsShortTerm.metadata_id, func.max(StatisticsShortTerm.start).label("start_max"), @@ -1149,6 +1176,14 @@ def _latest_short_term_statistics_stmt( .where(StatisticsShortTerm.metadata_id.in_(metadata_ids)) .group_by(StatisticsShortTerm.metadata_id) ).subquery() + + +def _latest_short_term_statistics_stmt( + metadata_ids: list[int], +) -> StatementLambdaElement: + """Create the statement for finding the latest short term stat rows.""" + stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SHORT_TERM)) + most_recent_statistic_row = _generate_most_recent_statistic_row(metadata_ids) stmt += lambda s: s.join( most_recent_statistic_row, ( diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 882f00d2940..97e64716f49 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -100,6 +100,15 @@ def test_compile_hourly_statistics(hass_recorder): stats = statistics_during_period(hass, zero, period="5minute") assert stats == {"sensor.test1": expected_stats1, "sensor.test2": expected_stats2} + # Test statistics_during_period with a far future start and end date + future = dt_util.as_utc(dt_util.parse_datetime("2221-11-01 00:00:00")) + stats = statistics_during_period(hass, future, end_time=future, period="5minute") + assert stats == {} + + # Test statistics_during_period with a far future end date + stats = statistics_during_period(hass, zero, end_time=future, period="5minute") + assert stats == {"sensor.test1": expected_stats1, "sensor.test2": expected_stats2} + stats = statistics_during_period( hass, zero, statistic_ids=["sensor.test2"], period="5minute" ) @@ -814,6 +823,59 @@ def test_monthly_statistics(hass_recorder, caplog, timezone): ] } + stats = statistics_during_period( + hass, + start_time=zero, + statistic_ids=["not", "the", "same", "test:total_energy_import"], + period="month", + ) + sep_start = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + sep_end = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + oct_start = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + oct_end = dt_util.as_utc(dt_util.parse_datetime("2021-11-01 00:00:00")) + assert stats == { + "test:total_energy_import": [ + { + "statistic_id": "test:total_energy_import", + "start": sep_start.isoformat(), + "end": sep_end.isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, + { + "statistic_id": "test:total_energy_import", + "start": oct_start.isoformat(), + "end": oct_end.isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(3.0), + "sum": approx(5.0), + }, + ] + } + + # Use 5minute to ensure table switch works + stats = statistics_during_period( + hass, + start_time=zero, + statistic_ids=["test:total_energy_import", "with_other"], + period="5minute", + ) + assert stats == {} + + # Ensure future date has not data + future = dt_util.as_utc(dt_util.parse_datetime("2221-11-01 00:00:00")) + stats = statistics_during_period( + hass, start_time=future, end_time=future, period="month" + ) + assert stats == {} + dt_util.set_default_time_zone(dt_util.get_time_zone("UTC")) From 6b2e5858b3a833d2792b20917a8ba787060b5c22 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Jun 2022 18:14:47 -1000 Subject: [PATCH 1284/3516] Send an empty logbook response when all requested entity_ids are filtered away (#73046) --- .../components/logbook/websocket_api.py | 38 ++++- .../components/logbook/test_websocket_api.py | 157 +++++++++++++++++- 2 files changed, 184 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index 1af44440803..b27ae65b70c 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -67,6 +67,23 @@ async def _async_wait_for_recorder_sync(hass: HomeAssistant) -> None: ) +@callback +def _async_send_empty_response( + connection: ActiveConnection, msg_id: int, start_time: dt, end_time: dt | None +) -> None: + """Send an empty response. + + The current case for this is when they ask for entity_ids + that will all be filtered away because they have UOMs or + state_class. + """ + connection.send_result(msg_id) + stream_end_time = end_time or dt_util.utcnow() + empty_stream_message = _generate_stream_message([], start_time, stream_end_time) + empty_response = messages.event_message(msg_id, empty_stream_message) + connection.send_message(JSON_DUMP(empty_response)) + + async def _async_send_historical_events( hass: HomeAssistant, connection: ActiveConnection, @@ -171,6 +188,17 @@ async def _async_get_ws_stream_events( ) +def _generate_stream_message( + events: list[dict[str, Any]], start_day: dt, end_day: dt +) -> dict[str, Any]: + """Generate a logbook stream message response.""" + return { + "events": events, + "start_time": dt_util.utc_to_timestamp(start_day), + "end_time": dt_util.utc_to_timestamp(end_day), + } + + def _ws_stream_get_events( msg_id: int, start_day: dt, @@ -184,11 +212,7 @@ def _ws_stream_get_events( last_time = None if events: last_time = dt_util.utc_from_timestamp(events[-1]["when"]) - message = { - "events": events, - "start_time": dt_util.utc_to_timestamp(start_day), - "end_time": dt_util.utc_to_timestamp(end_day), - } + message = _generate_stream_message(events, start_day, end_day) if partial: # This is a hint to consumers of the api that # we are about to send a another block of historical @@ -275,6 +299,10 @@ async def ws_event_stream( entity_ids = msg.get("entity_ids") if entity_ids: entity_ids = async_filter_entities(hass, entity_ids) + if not entity_ids: + _async_send_empty_response(connection, msg_id, start_time, end_time) + return + event_types = async_determine_event_types(hass, entity_ids, device_ids) event_processor = EventProcessor( hass, diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 2dd08ec44ce..2623a5b17d5 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -2102,11 +2102,17 @@ async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_clie ] ) await async_wait_recording_done(hass) + entity_ids = ("sensor.uom", "sensor.uom_two") + + def _cycle_entities(): + for entity_id in entity_ids: + for state in ("1", "2", "3"): + hass.states.async_set( + entity_id, state, {ATTR_UNIT_OF_MEASUREMENT: "any"} + ) init_count = sum(hass.bus.async_listeners().values()) - hass.states.async_set("sensor.uom", "1", {ATTR_UNIT_OF_MEASUREMENT: "any"}) - hass.states.async_set("sensor.uom", "2", {ATTR_UNIT_OF_MEASUREMENT: "any"}) - hass.states.async_set("sensor.uom", "3", {ATTR_UNIT_OF_MEASUREMENT: "any"}) + _cycle_entities() await async_wait_recording_done(hass) websocket_client = await hass_ws_client() @@ -2124,9 +2130,61 @@ async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_clie assert msg["type"] == TYPE_RESULT assert msg["success"] - hass.states.async_set("sensor.uom", "1", {ATTR_UNIT_OF_MEASUREMENT: "any"}) - hass.states.async_set("sensor.uom", "2", {ATTR_UNIT_OF_MEASUREMENT: "any"}) - hass.states.async_set("sensor.uom", "3", {ATTR_UNIT_OF_MEASUREMENT: "any"}) + _cycle_entities() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [] + + await websocket_client.close() + await hass.async_block_till_done() + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_all_entities_have_uom_multiple( + hass, recorder_mock, hass_ws_client +): + """Test logbook stream with specific request for multiple entities that are always filtered.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + await async_wait_recording_done(hass) + entity_ids = ("sensor.uom", "sensor.uom_two") + + def _cycle_entities(): + for entity_id in entity_ids: + for state in ("1", "2", "3"): + hass.states.async_set( + entity_id, state, {ATTR_UNIT_OF_MEASUREMENT: "any"} + ) + + init_count = sum(hass.bus.async_listeners().values()) + _cycle_entities() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "entity_ids": [*entity_ids], + } + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + _cycle_entities() msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 @@ -2138,3 +2196,90 @@ async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_clie # Check our listener got unsubscribed assert sum(hass.bus.async_listeners().values()) == init_count + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_entities_some_have_uom_multiple( + hass, recorder_mock, hass_ws_client +): + """Test logbook stream with uom filtered entities and non-fitlered entities.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + await async_wait_recording_done(hass) + filtered_entity_ids = ("sensor.uom", "sensor.uom_two") + non_filtered_entity_ids = ("sensor.keep", "sensor.keep_two") + + def _cycle_entities(): + for entity_id in filtered_entity_ids: + for state in ("1", "2", "3"): + hass.states.async_set( + entity_id, state, {ATTR_UNIT_OF_MEASUREMENT: "any"} + ) + for entity_id in non_filtered_entity_ids: + for state in (STATE_ON, STATE_OFF): + hass.states.async_set(entity_id, state) + + init_count = sum(hass.bus.async_listeners().values()) + _cycle_entities() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "entity_ids": [*filtered_entity_ids, *non_filtered_entity_ids], + } + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + _cycle_entities() + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["partial"] is True + assert msg["event"]["events"] == [ + {"entity_id": "sensor.keep", "state": "off", "when": ANY}, + {"entity_id": "sensor.keep_two", "state": "off", "when": ANY}, + ] + + _cycle_entities() + await hass.async_block_till_done() + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [] + assert "partial" not in msg["event"] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + {"entity_id": "sensor.keep", "state": "on", "when": ANY}, + {"entity_id": "sensor.keep", "state": "off", "when": ANY}, + {"entity_id": "sensor.keep_two", "state": "on", "when": ANY}, + {"entity_id": "sensor.keep_two", "state": "off", "when": ANY}, + {"entity_id": "sensor.keep", "state": "on", "when": ANY}, + {"entity_id": "sensor.keep", "state": "off", "when": ANY}, + {"entity_id": "sensor.keep_two", "state": "on", "when": ANY}, + {"entity_id": "sensor.keep_two", "state": "off", "when": ANY}, + ] + assert "partial" not in msg["event"] + + await websocket_client.close() + await hass.async_block_till_done() + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count From 7536586bed51998d86a0df6cabfeed3890b72b70 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 6 Jun 2022 06:58:48 +0200 Subject: [PATCH 1285/3516] Add binary sensors for Pure devices Boost Config (#73032) --- .../components/sensibo/binary_sensor.py | 48 +++++++++++++++++++ homeassistant/components/sensibo/sensor.py | 3 +- .../components/sensibo/strings.sensor.json | 8 ++++ .../sensibo/translations/sensor.en.json | 8 ++++ .../components/sensibo/test_binary_sensor.py | 13 ++++- tests/components/sensibo/test_sensor.py | 2 + 6 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/sensibo/strings.sensor.json create mode 100644 homeassistant/components/sensibo/translations/sensor.en.json diff --git a/homeassistant/components/sensibo/binary_sensor.py b/homeassistant/components/sensibo/binary_sensor.py index e8d83f04593..3a7deadc405 100644 --- a/homeassistant/components/sensibo/binary_sensor.py +++ b/homeassistant/components/sensibo/binary_sensor.py @@ -87,6 +87,48 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( ), ) +PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( + SensiboDeviceBinarySensorEntityDescription( + key="pure_boost_enabled", + device_class=BinarySensorDeviceClass.RUNNING, + name="Pure Boost Enabled", + icon="mdi:wind-power-outline", + value_fn=lambda data: data.pure_boost_enabled, + ), + SensiboDeviceBinarySensorEntityDescription( + key="pure_ac_integration", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + name="Pure Boost linked with AC", + icon="mdi:connection", + value_fn=lambda data: data.pure_ac_integration, + ), + SensiboDeviceBinarySensorEntityDescription( + key="pure_geo_integration", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + name="Pure Boost linked with Presence", + icon="mdi:connection", + value_fn=lambda data: data.pure_geo_integration, + ), + SensiboDeviceBinarySensorEntityDescription( + key="pure_measure_integration", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + name="Pure Boost linked with Indoor Air Quality", + icon="mdi:connection", + value_fn=lambda data: data.pure_measure_integration, + ), + SensiboDeviceBinarySensorEntityDescription( + key="pure_prime_integration", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=BinarySensorDeviceClass.CONNECTIVITY, + name="Pure Boost linked with Outdoor Air Quality", + icon="mdi:connection", + value_fn=lambda data: data.pure_prime_integration, + ), +) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -112,6 +154,12 @@ async def async_setup_entry( for device_id, device_data in coordinator.data.parsed.items() if getattr(device_data, description.key) is not None ) + entities.extend( + SensiboDeviceSensor(coordinator, device_id, description) + for description in PURE_SENSOR_TYPES + for device_id, device_data in coordinator.data.parsed.items() + if device_data.model == "pure" + ) async_add_entities(entities) diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 7254948bdad..f37a054b606 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -116,7 +116,8 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( key="pure_sensitivity", name="Pure Sensitivity", icon="mdi:air-filter", - value_fn=lambda data: data.pure_sensitivity, + value_fn=lambda data: str(data.pure_sensitivity).lower(), + device_class="sensibo__sensitivity", ), ) diff --git a/homeassistant/components/sensibo/strings.sensor.json b/homeassistant/components/sensibo/strings.sensor.json new file mode 100644 index 00000000000..2e4e05fba5b --- /dev/null +++ b/homeassistant/components/sensibo/strings.sensor.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "Normal", + "s": "Sensitive" + } + } +} diff --git a/homeassistant/components/sensibo/translations/sensor.en.json b/homeassistant/components/sensibo/translations/sensor.en.json new file mode 100644 index 00000000000..9ea1818b37c --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.en.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "Normal", + "s": "Sensitive" + } + } +} \ No newline at end of file diff --git a/tests/components/sensibo/test_binary_sensor.py b/tests/components/sensibo/test_binary_sensor.py index 3a84dc99ca5..efa6c5bdb2a 100644 --- a/tests/components/sensibo/test_binary_sensor.py +++ b/tests/components/sensibo/test_binary_sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from datetime import timedelta -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from pysensibo.model import SensiboData from pytest import MonkeyPatch @@ -16,6 +16,7 @@ from tests.common import async_fire_time_changed async def test_binary_sensor( hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, load_int: ConfigEntry, monkeypatch: MonkeyPatch, get_data: SensiboData, @@ -26,10 +27,20 @@ async def test_binary_sensor( state2 = hass.states.get("binary_sensor.hallway_motion_sensor_main_sensor") state3 = hass.states.get("binary_sensor.hallway_motion_sensor_motion") state4 = hass.states.get("binary_sensor.hallway_room_occupied") + state5 = hass.states.get("binary_sensor.kitchen_pure_boost_enabled") + state6 = hass.states.get( + "binary_sensor.kitchen_pure_boost_linked_with_indoor_air_quality" + ) + state7 = hass.states.get( + "binary_sensor.kitchen_pure_boost_linked_with_outdoor_air_quality" + ) assert state1.state == "on" assert state2.state == "on" assert state3.state == "on" assert state4.state == "on" + assert state5.state == "off" + assert state6.state == "on" + assert state7.state == "off" monkeypatch.setattr( get_data.parsed["ABC999111"].motion_sensors["AABBCC"], "alive", False diff --git a/tests/components/sensibo/test_sensor.py b/tests/components/sensibo/test_sensor.py index 413b62f6b9f..426416ae2b9 100644 --- a/tests/components/sensibo/test_sensor.py +++ b/tests/components/sensibo/test_sensor.py @@ -24,8 +24,10 @@ async def test_sensor( state1 = hass.states.get("sensor.hallway_motion_sensor_battery_voltage") state2 = hass.states.get("sensor.kitchen_pm2_5") + state3 = hass.states.get("sensor.kitchen_pure_sensitivity") assert state1.state == "3000" assert state2.state == "1" + assert state3.state == "n" assert state2.attributes == { "state_class": "measurement", "unit_of_measurement": "µg/m³", From 457c7a4ddcf092771cd1cb8eee7636c205031c7d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Jun 2022 19:06:49 -1000 Subject: [PATCH 1286/3516] Fix incompatiblity with live logbook and google_assistant (#73063) --- homeassistant/components/logbook/const.py | 10 +- homeassistant/components/logbook/helpers.py | 179 ++++++++++++------ homeassistant/components/logbook/processor.py | 33 +--- .../components/logbook/websocket_api.py | 14 +- tests/components/logbook/common.py | 1 - .../components/logbook/test_websocket_api.py | 153 +++++++++++++-- 6 files changed, 271 insertions(+), 119 deletions(-) diff --git a/homeassistant/components/logbook/const.py b/homeassistant/components/logbook/const.py index 3f0c6599724..d20acb553cc 100644 --- a/homeassistant/components/logbook/const.py +++ b/homeassistant/components/logbook/const.py @@ -30,13 +30,11 @@ LOGBOOK_ENTRY_NAME = "name" LOGBOOK_ENTRY_STATE = "state" LOGBOOK_ENTRY_WHEN = "when" -ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED = {EVENT_LOGBOOK_ENTRY, EVENT_CALL_SERVICE} -ENTITY_EVENTS_WITHOUT_CONFIG_ENTRY = { - EVENT_LOGBOOK_ENTRY, - EVENT_AUTOMATION_TRIGGERED, - EVENT_SCRIPT_STARTED, -} +# Automation events that can affect an entity_id or device_id +AUTOMATION_EVENTS = {EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED} +# Events that are built-in to the logbook or core +BUILT_IN_EVENTS = {EVENT_LOGBOOK_ENTRY, EVENT_CALL_SERVICE} LOGBOOK_FILTERS = "logbook_filters" LOGBOOK_ENTITIES_FILTER = "entities_filter" diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index de021994b8d..eec60ebe740 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -7,6 +7,7 @@ from typing import Any from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ( ATTR_DEVICE_ID, + ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, EVENT_LOGBOOK_ENTRY, @@ -21,13 +22,10 @@ from homeassistant.core import ( is_callback, ) from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entityfilter import EntityFilter from homeassistant.helpers.event import async_track_state_change_event -from .const import ( - ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, - DOMAIN, - ENTITY_EVENTS_WITHOUT_CONFIG_ENTRY, -) +from .const import AUTOMATION_EVENTS, BUILT_IN_EVENTS, DOMAIN from .models import LazyEventPartialState @@ -41,6 +39,25 @@ def async_filter_entities(hass: HomeAssistant, entity_ids: list[str]) -> list[st ] +@callback +def _async_config_entries_for_ids( + hass: HomeAssistant, entity_ids: list[str] | None, device_ids: list[str] | None +) -> set[str]: + """Find the config entry ids for a set of entities or devices.""" + config_entry_ids: set[str] = set() + if entity_ids: + eng_reg = er.async_get(hass) + for entity_id in entity_ids: + if (entry := eng_reg.async_get(entity_id)) and entry.config_entry_id: + config_entry_ids.add(entry.config_entry_id) + if device_ids: + dev_reg = dr.async_get(hass) + for device_id in device_ids: + if (device := dev_reg.async_get(device_id)) and device.config_entries: + config_entry_ids |= device.config_entries + return config_entry_ids + + def async_determine_event_types( hass: HomeAssistant, entity_ids: list[str] | None, device_ids: list[str] | None ) -> tuple[str, ...]: @@ -49,42 +66,91 @@ def async_determine_event_types( str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] ] = hass.data.get(DOMAIN, {}) if not entity_ids and not device_ids: - return (*ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, *external_events) - config_entry_ids: set[str] = set() - intrested_event_types: set[str] = set() + return (*BUILT_IN_EVENTS, *external_events) + interested_domains: set[str] = set() + for entry_id in _async_config_entries_for_ids(hass, entity_ids, device_ids): + if entry := hass.config_entries.async_get_entry(entry_id): + interested_domains.add(entry.domain) + + # + # automations and scripts can refer to entities or devices + # but they do not have a config entry so we need + # to add them since we have historically included + # them when matching only on entities + # + intrested_event_types: set[str] = { + external_event + for external_event, domain_call in external_events.items() + if domain_call[0] in interested_domains + } | AUTOMATION_EVENTS if entity_ids: - # - # Home Assistant doesn't allow firing events from - # entities so we have a limited list to check - # - # automations and scripts can refer to entities - # but they do not have a config entry so we need - # to add them. - # - # We also allow entity_ids to be recorded via - # manual logbook entries. - # - intrested_event_types |= ENTITY_EVENTS_WITHOUT_CONFIG_ENTRY + # We also allow entity_ids to be recorded via manual logbook entries. + intrested_event_types.add(EVENT_LOGBOOK_ENTRY) - if device_ids: - dev_reg = dr.async_get(hass) - for device_id in device_ids: - if (device := dev_reg.async_get(device_id)) and device.config_entries: - config_entry_ids |= device.config_entries - interested_domains: set[str] = set() - for entry_id in config_entry_ids: - if entry := hass.config_entries.async_get_entry(entry_id): - interested_domains.add(entry.domain) - for external_event, domain_call in external_events.items(): - if domain_call[0] in interested_domains: - intrested_event_types.add(external_event) + return tuple(intrested_event_types) - return tuple( - event_type - for event_type in (EVENT_LOGBOOK_ENTRY, *external_events) - if event_type in intrested_event_types - ) + +@callback +def extract_attr(source: dict[str, Any], attr: str) -> list[str]: + """Extract an attribute as a list or string.""" + if (value := source.get(attr)) is None: + return [] + if isinstance(value, list): + return value + return str(value).split(",") + + +@callback +def event_forwarder_filtered( + target: Callable[[Event], None], + entities_filter: EntityFilter | None, + entity_ids: list[str] | None, + device_ids: list[str] | None, +) -> Callable[[Event], None]: + """Make a callable to filter events.""" + if not entities_filter and not entity_ids and not device_ids: + # No filter + # - Script Trace (context ids) + # - Automation Trace (context ids) + return target + + if entities_filter: + # We have an entity filter: + # - Logbook panel + + @callback + def _forward_events_filtered_by_entities_filter(event: Event) -> None: + assert entities_filter is not None + event_data = event.data + entity_ids = extract_attr(event_data, ATTR_ENTITY_ID) + if entity_ids and not any( + entities_filter(entity_id) for entity_id in entity_ids + ): + return + domain = event_data.get(ATTR_DOMAIN) + if domain and not entities_filter(f"{domain}._"): + return + target(event) + + return _forward_events_filtered_by_entities_filter + + # We are filtering on entity_ids and/or device_ids: + # - Areas + # - Devices + # - Logbook Card + entity_ids_set = set(entity_ids) if entity_ids else set() + device_ids_set = set(device_ids) if device_ids else set() + + @callback + def _forward_events_filtered_by_device_entity_ids(event: Event) -> None: + event_data = event.data + if entity_ids_set.intersection( + extract_attr(event_data, ATTR_ENTITY_ID) + ) or device_ids_set.intersection(extract_attr(event_data, ATTR_DEVICE_ID)): + target(event) + + return _forward_events_filtered_by_device_entity_ids @callback @@ -93,6 +159,7 @@ def async_subscribe_events( subscriptions: list[CALLBACK_TYPE], target: Callable[[Event], None], event_types: tuple[str, ...], + entities_filter: EntityFilter | None, entity_ids: list[str] | None, device_ids: list[str] | None, ) -> None: @@ -103,41 +170,31 @@ def async_subscribe_events( """ ent_reg = er.async_get(hass) assert is_callback(target), "target must be a callback" - event_forwarder = target - - if entity_ids or device_ids: - entity_ids_set = set(entity_ids) if entity_ids else set() - device_ids_set = set(device_ids) if device_ids else set() - - @callback - def _forward_events_filtered(event: Event) -> None: - event_data = event.data - if ( - entity_ids_set and event_data.get(ATTR_ENTITY_ID) in entity_ids_set - ) or (device_ids_set and event_data.get(ATTR_DEVICE_ID) in device_ids_set): - target(event) - - event_forwarder = _forward_events_filtered - + event_forwarder = event_forwarder_filtered( + target, entities_filter, entity_ids, device_ids + ) for event_type in event_types: subscriptions.append( hass.bus.async_listen(event_type, event_forwarder, run_immediately=True) ) - @callback - def _forward_state_events_filtered(event: Event) -> None: - if event.data.get("old_state") is None or event.data.get("new_state") is None: - return - state: State = event.data["new_state"] - if not _is_state_filtered(ent_reg, state): - target(event) - if device_ids and not entity_ids: # No entities to subscribe to but we are filtering # on device ids so we do not want to get any state # changed events return + @callback + def _forward_state_events_filtered(event: Event) -> None: + if event.data.get("old_state") is None or event.data.get("new_state") is None: + return + state: State = event.data["new_state"] + if _is_state_filtered(ent_reg, state) or ( + entities_filter and not entities_filter(state.entity_id) + ): + return + target(event) + if entity_ids: subscriptions.append( async_track_state_change_event( diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index e5cc0f124b0..82225df8364 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -5,8 +5,6 @@ from collections.abc import Callable, Generator from contextlib import suppress from dataclasses import dataclass from datetime import datetime as dt -import logging -import re from typing import Any from sqlalchemy.engine.row import Row @@ -30,7 +28,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entityfilter import EntityFilter import homeassistant.util.dt as dt_util from .const import ( @@ -46,7 +43,6 @@ from .const import ( CONTEXT_STATE, CONTEXT_USER_ID, DOMAIN, - LOGBOOK_ENTITIES_FILTER, LOGBOOK_ENTRY_DOMAIN, LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_ICON, @@ -62,11 +58,6 @@ from .models import EventAsRow, LazyEventPartialState, async_event_to_row from .queries import statement_for_request from .queries.common import PSUEDO_EVENT_STATE_CHANGED -_LOGGER = logging.getLogger(__name__) - -ENTITY_ID_JSON_EXTRACT = re.compile('"entity_id": ?"([^"]+)"') -DOMAIN_JSON_EXTRACT = re.compile('"domain": ?"([^"]+)"') - @dataclass class LogbookRun: @@ -106,10 +97,6 @@ class EventProcessor: self.device_ids = device_ids self.context_id = context_id self.filters: Filters | None = hass.data[LOGBOOK_FILTERS] - if self.limited_select: - self.entities_filter: EntityFilter | Callable[[str], bool] | None = None - else: - self.entities_filter = hass.data[LOGBOOK_ENTITIES_FILTER] format_time = ( _row_time_fired_timestamp if timestamp else _row_time_fired_isoformat ) @@ -183,7 +170,6 @@ class EventProcessor: return list( _humanify( row_generator, - self.entities_filter, self.ent_reg, self.logbook_run, self.context_augmenter, @@ -193,7 +179,6 @@ class EventProcessor: def _humanify( rows: Generator[Row | EventAsRow, None, None], - entities_filter: EntityFilter | Callable[[str], bool] | None, ent_reg: er.EntityRegistry, logbook_run: LogbookRun, context_augmenter: ContextAugmenter, @@ -208,29 +193,13 @@ def _humanify( include_entity_name = logbook_run.include_entity_name format_time = logbook_run.format_time - def _keep_row(row: EventAsRow) -> bool: - """Check if the entity_filter rejects a row.""" - assert entities_filter is not None - if entity_id := row.entity_id: - return entities_filter(entity_id) - if entity_id := row.data.get(ATTR_ENTITY_ID): - return entities_filter(entity_id) - if domain := row.data.get(ATTR_DOMAIN): - return entities_filter(f"{domain}._") - return True - # Process rows for row in rows: context_id = context_lookup.memorize(row) if row.context_only: continue event_type = row.event_type - if event_type == EVENT_CALL_SERVICE or ( - entities_filter - # We literally mean is EventAsRow not a subclass of EventAsRow - and type(row) is EventAsRow # pylint: disable=unidiomatic-typecheck - and not _keep_row(row) - ): + if event_type == EVENT_CALL_SERVICE: continue if event_type is PSUEDO_EVENT_STATE_CHANGED: entity_id = row.entity_id diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index b27ae65b70c..a8f9bc50920 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -16,9 +16,11 @@ from homeassistant.components.websocket_api import messages from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.components.websocket_api.const import JSON_DUMP from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.helpers.entityfilter import EntityFilter from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.dt as dt_util +from .const import LOGBOOK_ENTITIES_FILTER from .helpers import ( async_determine_event_types, async_filter_entities, @@ -365,8 +367,18 @@ async def ws_event_stream( ) _unsub() + entities_filter: EntityFilter | None = None + if not event_processor.limited_select: + entities_filter = hass.data[LOGBOOK_ENTITIES_FILTER] + async_subscribe_events( - hass, subscriptions, _queue_or_cancel, event_types, entity_ids, device_ids + hass, + subscriptions, + _queue_or_cancel, + event_types, + entities_filter, + entity_ids, + device_ids, ) subscriptions_setup_complete_time = dt_util.utcnow() connection.subscriptions[msg_id] = _unsub diff --git a/tests/components/logbook/common.py b/tests/components/logbook/common.py index b88c3854967..a41f983bfed 100644 --- a/tests/components/logbook/common.py +++ b/tests/components/logbook/common.py @@ -68,7 +68,6 @@ def mock_humanify(hass_, rows): return list( processor._humanify( rows, - None, ent_reg, logbook_run, context_augmenter, diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 2623a5b17d5..ae1f7968e3b 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -27,8 +27,8 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import Event, HomeAssistant, State -from homeassistant.helpers import device_registry +from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -51,22 +51,8 @@ def set_utc(hass): hass.config.set_time_zone("UTC") -async def _async_mock_device_with_logbook_platform(hass): - """Mock an integration that provides a device that are described by the logbook.""" - entry = MockConfigEntry(domain="test", data={"first": True}, options=None) - entry.add_to_hass(hass) - dev_reg = device_registry.async_get(hass) - device = dev_reg.async_get_or_create( - config_entry_id=entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - identifiers={("bridgeid", "0123")}, - sw_version="sw-version", - name="device name", - manufacturer="manufacturer", - model="model", - suggested_area="Game Room", - ) - +@callback +async def _async_mock_logbook_platform(hass: HomeAssistant) -> None: class MockLogbookPlatform: """Mock a logbook platform.""" @@ -90,6 +76,40 @@ async def _async_mock_device_with_logbook_platform(hass): async_describe_event("test", "mock_event", async_describe_test_event) await logbook._process_logbook_platform(hass, "test", MockLogbookPlatform) + + +async def _async_mock_entity_with_logbook_platform(hass): + """Mock an integration that provides an entity that are described by the logbook.""" + entry = MockConfigEntry(domain="test", data={"first": True}, options=None) + entry.add_to_hass(hass) + ent_reg = entity_registry.async_get(hass) + entry = ent_reg.async_get_or_create( + platform="test", + domain="sensor", + config_entry=entry, + unique_id="1234", + suggested_object_id="test", + ) + await _async_mock_logbook_platform(hass) + return entry + + +async def _async_mock_device_with_logbook_platform(hass): + """Mock an integration that provides a device that are described by the logbook.""" + entry = MockConfigEntry(domain="test", data={"first": True}, options=None) + entry.add_to_hass(hass) + dev_reg = device_registry.async_get(hass) + device = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + sw_version="sw-version", + name="device name", + manufacturer="manufacturer", + model="model", + suggested_area="Game Room", + ) + await _async_mock_logbook_platform(hass) return device @@ -1786,6 +1806,103 @@ async def test_event_stream_bad_start_time(hass, hass_ws_client, recorder_mock): assert response["error"]["code"] == "invalid_start_time" +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_logbook_stream_match_multiple_entities( + hass, recorder_mock, hass_ws_client +): + """Test logbook stream with a described integration that uses multiple entities.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + entry = await _async_mock_entity_with_logbook_platform(hass) + entity_id = entry.entity_id + hass.states.async_set(entity_id, STATE_ON) + + await hass.async_block_till_done() + init_count = sum(hass.bus.async_listeners().values()) + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "entity_ids": [entity_id], + } + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # There are no answers to our initial query + # so we get an empty reply. This is to ensure + # consumers of the api know there are no results + # and its not a failure case. This is useful + # in the frontend so we can tell the user there + # are no results vs waiting for them to appear + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [] + await async_wait_recording_done(hass) + + hass.states.async_set("binary_sensor.should_not_appear", STATE_ON) + hass.states.async_set("binary_sensor.should_not_appear", STATE_OFF) + context = core.Context( + id="ac5bd62de45711eaaeb351041eec8dd9", + user_id="b400facee45711eaa9308bfd3d19e474", + ) + hass.bus.async_fire( + "mock_event", {"entity_id": ["sensor.any", entity_id]}, context=context + ) + hass.bus.async_fire("mock_event", {"entity_id": [f"sensor.any,{entity_id}"]}) + hass.bus.async_fire("mock_event", {"entity_id": ["sensor.no_match", "light.off"]}) + hass.states.async_set(entity_id, STATE_OFF, context=context) + await hass.async_block_till_done() + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + { + "context_user_id": "b400facee45711eaa9308bfd3d19e474", + "domain": "test", + "message": "is on fire", + "name": "device name", + "when": ANY, + }, + { + "context_domain": "test", + "context_event_type": "mock_event", + "context_message": "is on fire", + "context_name": "device name", + "context_user_id": "b400facee45711eaa9308bfd3d19e474", + "entity_id": "sensor.test", + "state": "off", + "when": ANY, + }, + ] + + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + async def test_event_stream_bad_end_time(hass, hass_ws_client, recorder_mock): """Test event_stream bad end time.""" await async_setup_component(hass, "logbook", {}) From 0b62944148747d6421ced68ed1e41558cdc45408 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Jun 2022 21:25:26 -1000 Subject: [PATCH 1287/3516] Mark counter domain as continuous to exclude it from logbook (#73101) --- homeassistant/components/logbook/const.py | 9 ++++ homeassistant/components/logbook/helpers.py | 9 ++-- .../components/logbook/queries/common.py | 45 +++++++++++++------ homeassistant/components/recorder/filters.py | 9 +++- tests/components/logbook/test_init.py | 6 +++ .../components/logbook/test_websocket_api.py | 8 +++- 6 files changed, 66 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/logbook/const.py b/homeassistant/components/logbook/const.py index d20acb553cc..e1abd987659 100644 --- a/homeassistant/components/logbook/const.py +++ b/homeassistant/components/logbook/const.py @@ -2,9 +2,18 @@ from __future__ import annotations from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED +from homeassistant.components.counter import DOMAIN as COUNTER_DOMAIN +from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN from homeassistant.components.script import EVENT_SCRIPT_STARTED +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import EVENT_CALL_SERVICE, EVENT_LOGBOOK_ENTRY +# Domains that are always continuous +ALWAYS_CONTINUOUS_DOMAINS = {COUNTER_DOMAIN, PROXIMITY_DOMAIN} + +# Domains that are continuous if there is a UOM set on the entity +CONDITIONALLY_CONTINUOUS_DOMAINS = {SENSOR_DOMAIN} + ATTR_MESSAGE = "message" DOMAIN = "logbook" diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index eec60ebe740..ef322c44e05 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -20,12 +20,13 @@ from homeassistant.core import ( State, callback, is_callback, + split_entity_id, ) from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entityfilter import EntityFilter from homeassistant.helpers.event import async_track_state_change_event -from .const import AUTOMATION_EVENTS, BUILT_IN_EVENTS, DOMAIN +from .const import ALWAYS_CONTINUOUS_DOMAINS, AUTOMATION_EVENTS, BUILT_IN_EVENTS, DOMAIN from .models import LazyEventPartialState @@ -235,7 +236,8 @@ def _is_state_filtered(ent_reg: er.EntityRegistry, state: State) -> bool: we only get significant changes (state.last_changed != state.last_updated) """ return bool( - state.last_changed != state.last_updated + split_entity_id(state.entity_id)[0] in ALWAYS_CONTINUOUS_DOMAINS + or state.last_changed != state.last_updated or ATTR_UNIT_OF_MEASUREMENT in state.attributes or is_sensor_continuous(ent_reg, state.entity_id) ) @@ -250,7 +252,8 @@ def _is_entity_id_filtered( from the database when a list of entities is requested. """ return bool( - (state := hass.states.get(entity_id)) + split_entity_id(entity_id)[0] in ALWAYS_CONTINUOUS_DOMAINS + or (state := hass.states.get(entity_id)) and (ATTR_UNIT_OF_MEASUREMENT in state.attributes) or is_sensor_continuous(ent_reg, entity_id) ) diff --git a/homeassistant/components/logbook/queries/common.py b/homeassistant/components/logbook/queries/common.py index a7a4f84a59e..56925b60e62 100644 --- a/homeassistant/components/logbook/queries/common.py +++ b/homeassistant/components/logbook/queries/common.py @@ -10,7 +10,7 @@ from sqlalchemy.sql.elements import ClauseList from sqlalchemy.sql.expression import literal from sqlalchemy.sql.selectable import Select -from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN +from homeassistant.components.recorder.filters import like_domain_matchers from homeassistant.components.recorder.models import ( EVENTS_CONTEXT_ID_INDEX, OLD_FORMAT_ATTRS_JSON, @@ -22,15 +22,19 @@ from homeassistant.components.recorder.models import ( StateAttributes, States, ) -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -CONTINUOUS_DOMAINS = {PROXIMITY_DOMAIN, SENSOR_DOMAIN} -CONTINUOUS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in CONTINUOUS_DOMAINS] +from ..const import ALWAYS_CONTINUOUS_DOMAINS, CONDITIONALLY_CONTINUOUS_DOMAINS + +# Domains that are continuous if there is a UOM set on the entity +CONDITIONALLY_CONTINUOUS_ENTITY_ID_LIKE = like_domain_matchers( + CONDITIONALLY_CONTINUOUS_DOMAINS +) +# Domains that are always continuous +ALWAYS_CONTINUOUS_ENTITY_ID_LIKE = like_domain_matchers(ALWAYS_CONTINUOUS_DOMAINS) UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%" - PSUEDO_EVENT_STATE_CHANGED = None # Since we don't store event_types and None # and we don't store state_changed in events @@ -220,29 +224,44 @@ def _missing_state_matcher() -> sqlalchemy.and_: def _not_continuous_entity_matcher() -> sqlalchemy.or_: """Match non continuous entities.""" return sqlalchemy.or_( - _not_continuous_domain_matcher(), + # First exclude domains that may be continuous + _not_possible_continuous_domain_matcher(), + # But let in the entities in the possible continuous domains + # that are not actually continuous sensors because they lack a UOM sqlalchemy.and_( - _continuous_domain_matcher, _not_uom_attributes_matcher() + _conditionally_continuous_domain_matcher, _not_uom_attributes_matcher() ).self_group(), ) -def _not_continuous_domain_matcher() -> sqlalchemy.and_: - """Match not continuous domains.""" +def _not_possible_continuous_domain_matcher() -> sqlalchemy.and_: + """Match not continuous domains. + + This matches domain that are always considered continuous + and domains that are conditionally (if they have a UOM) + continuous domains. + """ return sqlalchemy.and_( *[ ~States.entity_id.like(entity_domain) - for entity_domain in CONTINUOUS_ENTITY_ID_LIKE + for entity_domain in ( + *ALWAYS_CONTINUOUS_ENTITY_ID_LIKE, + *CONDITIONALLY_CONTINUOUS_ENTITY_ID_LIKE, + ) ], ).self_group() -def _continuous_domain_matcher() -> sqlalchemy.or_: - """Match continuous domains.""" +def _conditionally_continuous_domain_matcher() -> sqlalchemy.or_: + """Match conditionally continuous domains. + + This matches domain that are only considered + continuous if a UOM is set. + """ return sqlalchemy.or_( *[ States.entity_id.like(entity_domain) - for entity_domain in CONTINUOUS_ENTITY_ID_LIKE + for entity_domain in CONDITIONALLY_CONTINUOUS_ENTITY_ID_LIKE ], ).self_group() diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 90851e9f251..0b3e0e68030 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -248,8 +248,13 @@ def _domain_matcher( domains: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: matchers = [ - (column.is_not(None) & cast(column, Text()).like(encoder(f"{domain}.%"))) - for domain in domains + (column.is_not(None) & cast(column, Text()).like(encoder(domain_matcher))) + for domain_matcher in like_domain_matchers(domains) for column in columns ] return or_(*matchers) if matchers else or_(False) + + +def like_domain_matchers(domains: Iterable[str]) -> list[str]: + """Convert a list of domains to sql LIKE matchers.""" + return [f"{domain}.%" for domain in domains] diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 651a00fb0cf..d16b3476d84 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -745,6 +745,12 @@ async def test_filter_continuous_sensor_values( entity_id_third = "light.bla" hass.states.async_set(entity_id_third, STATE_OFF, {"unit_of_measurement": "foo"}) hass.states.async_set(entity_id_third, STATE_ON, {"unit_of_measurement": "foo"}) + entity_id_proximity = "proximity.bla" + hass.states.async_set(entity_id_proximity, STATE_OFF) + hass.states.async_set(entity_id_proximity, STATE_ON) + entity_id_counter = "counter.bla" + hass.states.async_set(entity_id_counter, STATE_OFF) + hass.states.async_set(entity_id_counter, STATE_ON) await async_wait_recording_done(hass) diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index ae1f7968e3b..4df2f456eb6 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -2209,7 +2209,9 @@ async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplo @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) -async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_client): +async def test_subscribe_all_entities_are_continuous( + hass, recorder_mock, hass_ws_client +): """Test subscribe/unsubscribe logbook stream with entities that are always filtered.""" now = dt_util.utcnow() await asyncio.gather( @@ -2227,6 +2229,8 @@ async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_clie hass.states.async_set( entity_id, state, {ATTR_UNIT_OF_MEASUREMENT: "any"} ) + hass.states.async_set("counter.any", state) + hass.states.async_set("proximity.any", state) init_count = sum(hass.bus.async_listeners().values()) _cycle_entities() @@ -2238,7 +2242,7 @@ async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_clie "id": 7, "type": "logbook/event_stream", "start_time": now.isoformat(), - "entity_ids": ["sensor.uom"], + "entity_ids": ["sensor.uom", "counter.any", "proximity.any"], } ) From e5b447839af38d5c10e3205ef095aa205cb6dd16 Mon Sep 17 00:00:00 2001 From: rappenze Date: Mon, 6 Jun 2022 13:20:16 +0200 Subject: [PATCH 1288/3516] Fix fibaro cover detection (#72986) --- homeassistant/components/fibaro/cover.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/fibaro/cover.py b/homeassistant/components/fibaro/cover.py index fc6d0a67d3c..e898cd7ead9 100644 --- a/homeassistant/components/fibaro/cover.py +++ b/homeassistant/components/fibaro/cover.py @@ -46,6 +46,8 @@ class FibaroCover(FibaroDevice, CoverEntity): self._attr_supported_features = ( CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE ) + if "stop" in self.fibaro_device.actions: + self._attr_supported_features |= CoverEntityFeature.STOP @staticmethod def bound(position): From a9e4673affbae34d8ddaeb1f6e8fb98a3bcf259c Mon Sep 17 00:00:00 2001 From: Igor Loborec Date: Mon, 6 Jun 2022 18:07:02 +0100 Subject: [PATCH 1289/3516] Bump holidays to 0.14.2 (#73121) --- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 6028e6e6fc2..186ae56bbdc 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -2,7 +2,7 @@ "domain": "workday", "name": "Workday", "documentation": "https://www.home-assistant.io/integrations/workday", - "requirements": ["holidays==0.13"], + "requirements": ["holidays==0.14.2"], "codeowners": ["@fabaff"], "quality_scale": "internal", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 34c3904aab9..edb9131f9a5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -822,7 +822,7 @@ hlk-sw16==0.0.9 hole==0.7.0 # homeassistant.components.workday -holidays==0.13 +holidays==0.14.2 # homeassistant.components.frontend home-assistant-frontend==20220601.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6b8b4d0dc35..b3ca7cf27b1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -589,7 +589,7 @@ hlk-sw16==0.0.9 hole==0.7.0 # homeassistant.components.workday -holidays==0.13 +holidays==0.14.2 # homeassistant.components.frontend home-assistant-frontend==20220601.0 From ed54cea3f26dfd4475b7be1d2971cd61313cacf3 Mon Sep 17 00:00:00 2001 From: Jan Stienstra <65826735+j-stienstra@users.noreply.github.com> Date: Mon, 6 Jun 2022 21:17:21 +0200 Subject: [PATCH 1290/3516] Jellyfin: Add support for movie collections (#73086) --- homeassistant/components/jellyfin/const.py | 5 +- .../components/jellyfin/manifest.json | 2 +- .../components/jellyfin/media_source.py | 81 ++++++++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 66 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/jellyfin/const.py b/homeassistant/components/jellyfin/const.py index d8379859e54..1f679fd43c8 100644 --- a/homeassistant/components/jellyfin/const.py +++ b/homeassistant/components/jellyfin/const.py @@ -7,7 +7,6 @@ DOMAIN: Final = "jellyfin" CLIENT_VERSION: Final = "1.0" COLLECTION_TYPE_MOVIES: Final = "movies" -COLLECTION_TYPE_TVSHOWS: Final = "tvshows" COLLECTION_TYPE_MUSIC: Final = "music" DATA_CLIENT: Final = "client" @@ -24,6 +23,7 @@ ITEM_TYPE_ALBUM: Final = "MusicAlbum" ITEM_TYPE_ARTIST: Final = "MusicArtist" ITEM_TYPE_AUDIO: Final = "Audio" ITEM_TYPE_LIBRARY: Final = "CollectionFolder" +ITEM_TYPE_MOVIE: Final = "Movie" MAX_IMAGE_WIDTH: Final = 500 MAX_STREAMING_BITRATE: Final = "140000000" @@ -33,8 +33,9 @@ MEDIA_SOURCE_KEY_PATH: Final = "Path" MEDIA_TYPE_AUDIO: Final = "Audio" MEDIA_TYPE_NONE: Final = "" +MEDIA_TYPE_VIDEO: Final = "Video" -SUPPORTED_COLLECTION_TYPES: Final = [COLLECTION_TYPE_MUSIC] +SUPPORTED_COLLECTION_TYPES: Final = [COLLECTION_TYPE_MUSIC, COLLECTION_TYPE_MOVIES] USER_APP_NAME: Final = "Home Assistant" USER_AGENT: Final = f"Home-Assistant/{CLIENT_VERSION}" diff --git a/homeassistant/components/jellyfin/manifest.json b/homeassistant/components/jellyfin/manifest.json index 993d2520484..48f4cf0c837 100644 --- a/homeassistant/components/jellyfin/manifest.json +++ b/homeassistant/components/jellyfin/manifest.json @@ -3,7 +3,7 @@ "name": "Jellyfin", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/jellyfin", - "requirements": ["jellyfin-apiclient-python==1.7.2"], + "requirements": ["jellyfin-apiclient-python==1.8.1"], "iot_class": "local_polling", "codeowners": ["@j-stienstra"], "loggers": ["jellyfin_apiclient_python"] diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index dbd79612378..879f4a4d4c8 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -4,7 +4,6 @@ from __future__ import annotations import logging import mimetypes from typing import Any -import urllib.parse from jellyfin_apiclient_python.api import jellyfin_url from jellyfin_apiclient_python.client import JellyfinClient @@ -13,6 +12,7 @@ from homeassistant.components.media_player.const import ( MEDIA_CLASS_ALBUM, MEDIA_CLASS_ARTIST, MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_MOVIE, MEDIA_CLASS_TRACK, ) from homeassistant.components.media_player.errors import BrowseError @@ -25,6 +25,7 @@ from homeassistant.components.media_source.models import ( from homeassistant.core import HomeAssistant from .const import ( + COLLECTION_TYPE_MOVIES, COLLECTION_TYPE_MUSIC, DATA_CLIENT, DOMAIN, @@ -39,11 +40,12 @@ from .const import ( ITEM_TYPE_ARTIST, ITEM_TYPE_AUDIO, ITEM_TYPE_LIBRARY, + ITEM_TYPE_MOVIE, MAX_IMAGE_WIDTH, - MAX_STREAMING_BITRATE, MEDIA_SOURCE_KEY_PATH, MEDIA_TYPE_AUDIO, MEDIA_TYPE_NONE, + MEDIA_TYPE_VIDEO, SUPPORTED_COLLECTION_TYPES, ) @@ -147,6 +149,8 @@ class JellyfinSource(MediaSource): if collection_type == COLLECTION_TYPE_MUSIC: return await self._build_music_library(library, include_children) + if collection_type == COLLECTION_TYPE_MOVIES: + return await self._build_movie_library(library, include_children) raise BrowseError(f"Unsupported collection type {collection_type}") @@ -270,6 +274,55 @@ class JellyfinSource(MediaSource): return result + async def _build_movie_library( + self, library: dict[str, Any], include_children: bool + ) -> BrowseMediaSource: + """Return a single movie library as a browsable media source.""" + library_id = library[ITEM_KEY_ID] + library_name = library[ITEM_KEY_NAME] + + result = BrowseMediaSource( + domain=DOMAIN, + identifier=library_id, + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=MEDIA_TYPE_NONE, + title=library_name, + can_play=False, + can_expand=True, + ) + + if include_children: + result.children_media_class = MEDIA_CLASS_MOVIE + result.children = await self._build_movies(library_id) # type: ignore[assignment] + + return result + + async def _build_movies(self, library_id: str) -> list[BrowseMediaSource]: + """Return all movies in the movie library.""" + movies = await self._get_children(library_id, ITEM_TYPE_MOVIE) + movies = sorted(movies, key=lambda k: k[ITEM_KEY_NAME]) # type: ignore[no-any-return] + return [self._build_movie(movie) for movie in movies] + + def _build_movie(self, movie: dict[str, Any]) -> BrowseMediaSource: + """Return a single movie as a browsable media source.""" + movie_id = movie[ITEM_KEY_ID] + movie_title = movie[ITEM_KEY_NAME] + mime_type = _media_mime_type(movie) + thumbnail_url = self._get_thumbnail_url(movie) + + result = BrowseMediaSource( + domain=DOMAIN, + identifier=movie_id, + media_class=MEDIA_CLASS_MOVIE, + media_content_type=mime_type, + title=movie_title, + can_play=True, + can_expand=False, + thumbnail=thumbnail_url, + ) + + return result + async def _get_children( self, parent_id: str, item_type: str ) -> list[dict[str, Any]]: @@ -279,7 +332,7 @@ class JellyfinSource(MediaSource): "ParentId": parent_id, "IncludeItemTypes": item_type, } - if item_type == ITEM_TYPE_AUDIO: + if item_type in {ITEM_TYPE_AUDIO, ITEM_TYPE_MOVIE}: params["Fields"] = ITEM_KEY_MEDIA_SOURCES result = await self.hass.async_add_executor_job(self.api.user_items, "", params) @@ -298,29 +351,15 @@ class JellyfinSource(MediaSource): def _get_stream_url(self, media_item: dict[str, Any]) -> str: """Return the stream URL for a media item.""" media_type = media_item[ITEM_KEY_MEDIA_TYPE] + item_id = media_item[ITEM_KEY_ID] if media_type == MEDIA_TYPE_AUDIO: - return self._get_audio_stream_url(media_item) + return self.api.audio_url(item_id) # type: ignore[no-any-return] + if media_type == MEDIA_TYPE_VIDEO: + return self.api.video_url(item_id) # type: ignore[no-any-return] raise BrowseError(f"Unsupported media type {media_type}") - def _get_audio_stream_url(self, media_item: dict[str, Any]) -> str: - """Return the stream URL for a music media item.""" - item_id = media_item[ITEM_KEY_ID] - user_id = self.client.config.data["auth.user_id"] - device_id = self.client.config.data["app.device_id"] - api_key = self.client.config.data["auth.token"] - - params = urllib.parse.urlencode( - { - "UserId": user_id, - "DeviceId": device_id, - "api_key": api_key, - "MaxStreamingBitrate": MAX_STREAMING_BITRATE, - } - ) - return f"{self.url}Audio/{item_id}/universal?{params}" - def _media_mime_type(media_item: dict[str, Any]) -> str: """Return the mime type of a media item.""" diff --git a/requirements_all.txt b/requirements_all.txt index edb9131f9a5..2bea1a28b50 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -903,7 +903,7 @@ iperf3==0.1.11 ismartgate==4.0.4 # homeassistant.components.jellyfin -jellyfin-apiclient-python==1.7.2 +jellyfin-apiclient-python==1.8.1 # homeassistant.components.rest jsonpath==0.82 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3ca7cf27b1..15242eaad94 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -643,7 +643,7 @@ iotawattpy==0.1.0 ismartgate==4.0.4 # homeassistant.components.jellyfin -jellyfin-apiclient-python==1.7.2 +jellyfin-apiclient-python==1.8.1 # homeassistant.components.rest jsonpath==0.82 From 39c6a57c3579ca21d47415ea8c4164d7e95a452b Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 6 Jun 2022 03:39:42 +0200 Subject: [PATCH 1291/3516] Throttle multiple requests to the velux gateway (#72974) --- homeassistant/components/velux/cover.py | 11 ++++++----- homeassistant/components/velux/light.py | 2 ++ homeassistant/components/velux/scene.py | 2 ++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index f4f449509a0..26cccfce6ce 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -17,6 +17,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DATA_VELUX, VeluxEntity +PARALLEL_UPDATES = 1 + async def async_setup_platform( hass: HomeAssistant, @@ -97,12 +99,11 @@ class VeluxCover(VeluxEntity, CoverEntity): async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" - if ATTR_POSITION in kwargs: - position_percent = 100 - kwargs[ATTR_POSITION] + position_percent = 100 - kwargs[ATTR_POSITION] - await self.node.set_position( - Position(position_percent=position_percent), wait_for_completion=False - ) + await self.node.set_position( + Position(position_percent=position_percent), wait_for_completion=False + ) async def async_stop_cover(self, **kwargs): """Stop the cover.""" diff --git a/homeassistant/components/velux/light.py b/homeassistant/components/velux/light.py index e6ca8ea5c2b..f8a52fc05c1 100644 --- a/homeassistant/components/velux/light.py +++ b/homeassistant/components/velux/light.py @@ -10,6 +10,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DATA_VELUX, VeluxEntity +PARALLEL_UPDATES = 1 + async def async_setup_platform( hass: HomeAssistant, diff --git a/homeassistant/components/velux/scene.py b/homeassistant/components/velux/scene.py index 324bae027fc..20f94c74f0b 100644 --- a/homeassistant/components/velux/scene.py +++ b/homeassistant/components/velux/scene.py @@ -10,6 +10,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import _LOGGER, DATA_VELUX +PARALLEL_UPDATES = 1 + async def async_setup_platform( hass: HomeAssistant, From b50e3d5ce7838abe72799087f69756d9a2e0cbfc Mon Sep 17 00:00:00 2001 From: hesselonline Date: Mon, 6 Jun 2022 03:31:09 +0200 Subject: [PATCH 1292/3516] Bump wallbox to 0.4.9 (#72978) --- .../components/wallbox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wallbox/__init__.py | 39 ++++++++++++---- tests/components/wallbox/test_config_flow.py | 44 +++++-------------- tests/components/wallbox/test_init.py | 18 ++------ tests/components/wallbox/test_lock.py | 26 ++--------- tests/components/wallbox/test_number.py | 27 ++---------- tests/components/wallbox/test_switch.py | 29 +++--------- 9 files changed, 61 insertions(+), 128 deletions(-) diff --git a/homeassistant/components/wallbox/manifest.json b/homeassistant/components/wallbox/manifest.json index 2a4978b1cc1..914adda980a 100644 --- a/homeassistant/components/wallbox/manifest.json +++ b/homeassistant/components/wallbox/manifest.json @@ -3,7 +3,7 @@ "name": "Wallbox", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wallbox", - "requirements": ["wallbox==0.4.4"], + "requirements": ["wallbox==0.4.9"], "ssdp": [], "zeroconf": [], "homekit": {}, diff --git a/requirements_all.txt b/requirements_all.txt index 4c848eef637..adf129b1250 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2418,7 +2418,7 @@ vultr==0.1.2 wakeonlan==2.0.1 # homeassistant.components.wallbox -wallbox==0.4.4 +wallbox==0.4.9 # homeassistant.components.waqi waqiasync==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3efb7dd9878..2d9b0100da4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1591,7 +1591,7 @@ vultr==0.1.2 wakeonlan==2.0.1 # homeassistant.components.wallbox -wallbox==0.4.4 +wallbox==0.4.9 # homeassistant.components.folder_watcher watchdog==2.1.8 diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py index 2b35bb76b2f..8c979c42ebe 100644 --- a/tests/components/wallbox/__init__.py +++ b/tests/components/wallbox/__init__.py @@ -26,7 +26,7 @@ from homeassistant.components.wallbox.const import ( from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from .const import ERROR, JWT, STATUS, TTL, USER_ID +from .const import ERROR, STATUS, TTL, USER_ID from tests.common import MockConfigEntry @@ -54,11 +54,32 @@ test_response = json.loads( authorisation_response = json.loads( json.dumps( { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 200, + "data": { + "attributes": { + "token": "fakekeyhere", + USER_ID: 12345, + TTL: 145656758, + ERROR: "false", + STATUS: 200, + } + } + } + ) +) + + +authorisation_response_unauthorised = json.loads( + json.dumps( + { + "data": { + "attributes": { + "token": "fakekeyhere", + USER_ID: 12345, + TTL: 145656758, + ERROR: "false", + STATUS: 404, + } + } } ) ) @@ -81,7 +102,7 @@ async def setup_integration(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=HTTPStatus.OK, ) @@ -107,7 +128,7 @@ async def setup_integration_connection_error(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=HTTPStatus.FORBIDDEN, ) @@ -133,7 +154,7 @@ async def setup_integration_read_only(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=HTTPStatus.OK, ) diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py index a3e6a724eef..68f70878592 100644 --- a/tests/components/wallbox/test_config_flow.py +++ b/tests/components/wallbox/test_config_flow.py @@ -18,8 +18,12 @@ from homeassistant.components.wallbox.const import ( ) from homeassistant.core import HomeAssistant -from tests.components.wallbox import entry, setup_integration -from tests.components.wallbox.const import ERROR, JWT, STATUS, TTL, USER_ID +from tests.components.wallbox import ( + authorisation_response, + authorisation_response_unauthorised, + entry, + setup_integration, +) test_response = json.loads( json.dumps( @@ -34,30 +38,6 @@ test_response = json.loads( ) ) -authorisation_response = json.loads( - json.dumps( - { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 200, - } - ) -) - -authorisation_response_unauthorised = json.loads( - json.dumps( - { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 404, - } - ) -) - async def test_show_set_form(hass: HomeAssistant) -> None: """Test that the setup form is served.""" @@ -77,7 +57,7 @@ async def test_form_cannot_authenticate(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=HTTPStatus.FORBIDDEN, ) @@ -107,7 +87,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response_unauthorised, status_code=HTTPStatus.NOT_FOUND, ) @@ -137,7 +117,7 @@ async def test_form_validate_input(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=HTTPStatus.OK, ) @@ -166,8 +146,8 @@ async def test_form_reauth(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", - text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + "https://user-api.wall-box.com/users/signin", + json=authorisation_response, status_code=200, ) mock_request.get( @@ -206,7 +186,7 @@ async def test_form_reauth_invalid(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', status_code=200, ) diff --git a/tests/components/wallbox/test_init.py b/tests/components/wallbox/test_init.py index b8862531a82..5080bab87ea 100644 --- a/tests/components/wallbox/test_init.py +++ b/tests/components/wallbox/test_init.py @@ -11,24 +11,12 @@ from . import test_response from tests.components.wallbox import ( DOMAIN, + authorisation_response, entry, setup_integration, setup_integration_connection_error, setup_integration_read_only, ) -from tests.components.wallbox.const import ERROR, JWT, STATUS, TTL, USER_ID - -authorisation_response = json.loads( - json.dumps( - { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 200, - } - ) -) async def test_wallbox_setup_unload_entry(hass: HomeAssistant) -> None: @@ -59,7 +47,7 @@ async def test_wallbox_refresh_failed_invalid_auth(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=403, ) @@ -85,7 +73,7 @@ async def test_wallbox_refresh_failed_connection_error(hass: HomeAssistant) -> N with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) diff --git a/tests/components/wallbox/test_lock.py b/tests/components/wallbox/test_lock.py index d8b2fcf182c..fbcb07b0e90 100644 --- a/tests/components/wallbox/test_lock.py +++ b/tests/components/wallbox/test_lock.py @@ -10,30 +10,12 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from tests.components.wallbox import ( + authorisation_response, entry, setup_integration, setup_integration_read_only, ) -from tests.components.wallbox.const import ( - ERROR, - JWT, - MOCK_LOCK_ENTITY_ID, - STATUS, - TTL, - USER_ID, -) - -authorisation_response = json.loads( - json.dumps( - { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 200, - } - ) -) +from tests.components.wallbox.const import MOCK_LOCK_ENTITY_ID async def test_wallbox_lock_class(hass: HomeAssistant) -> None: @@ -47,7 +29,7 @@ async def test_wallbox_lock_class(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) @@ -85,7 +67,7 @@ async def test_wallbox_lock_class_connection_error(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) diff --git a/tests/components/wallbox/test_number.py b/tests/components/wallbox/test_number.py index 5c024d0f4ac..c8e8b29f28b 100644 --- a/tests/components/wallbox/test_number.py +++ b/tests/components/wallbox/test_number.py @@ -9,27 +9,8 @@ from homeassistant.components.wallbox import CHARGER_MAX_CHARGING_CURRENT_KEY from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant -from tests.components.wallbox import entry, setup_integration -from tests.components.wallbox.const import ( - ERROR, - JWT, - MOCK_NUMBER_ENTITY_ID, - STATUS, - TTL, - USER_ID, -) - -authorisation_response = json.loads( - json.dumps( - { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 200, - } - ) -) +from tests.components.wallbox import authorisation_response, entry, setup_integration +from tests.components.wallbox.const import MOCK_NUMBER_ENTITY_ID async def test_wallbox_number_class(hass: HomeAssistant) -> None: @@ -39,7 +20,7 @@ async def test_wallbox_number_class(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) @@ -68,7 +49,7 @@ async def test_wallbox_number_class_connection_error(hass: HomeAssistant) -> Non with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) diff --git a/tests/components/wallbox/test_switch.py b/tests/components/wallbox/test_switch.py index 6ade320319a..c57fee0353f 100644 --- a/tests/components/wallbox/test_switch.py +++ b/tests/components/wallbox/test_switch.py @@ -10,27 +10,8 @@ from homeassistant.components.wallbox.const import CHARGER_STATUS_ID_KEY from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant -from tests.components.wallbox import entry, setup_integration -from tests.components.wallbox.const import ( - ERROR, - JWT, - MOCK_SWITCH_ENTITY_ID, - STATUS, - TTL, - USER_ID, -) - -authorisation_response = json.loads( - json.dumps( - { - JWT: "fakekeyhere", - USER_ID: 12345, - TTL: 145656758, - ERROR: "false", - STATUS: 200, - } - ) -) +from tests.components.wallbox import authorisation_response, entry, setup_integration +from tests.components.wallbox.const import MOCK_SWITCH_ENTITY_ID async def test_wallbox_switch_class(hass: HomeAssistant) -> None: @@ -44,7 +25,7 @@ async def test_wallbox_switch_class(hass: HomeAssistant) -> None: with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) @@ -82,7 +63,7 @@ async def test_wallbox_switch_class_connection_error(hass: HomeAssistant) -> Non with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) @@ -121,7 +102,7 @@ async def test_wallbox_switch_class_authentication_error(hass: HomeAssistant) -> with requests_mock.Mocker() as mock_request: mock_request.get( - "https://api.wall-box.com/auth/token/user", + "https://user-api.wall-box.com/users/signin", json=authorisation_response, status_code=200, ) From bd8424d184ddf2726f851f0ce1cf01614f03fddf Mon Sep 17 00:00:00 2001 From: rappenze Date: Mon, 6 Jun 2022 13:20:16 +0200 Subject: [PATCH 1293/3516] Fix fibaro cover detection (#72986) --- homeassistant/components/fibaro/cover.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/fibaro/cover.py b/homeassistant/components/fibaro/cover.py index fc6d0a67d3c..e898cd7ead9 100644 --- a/homeassistant/components/fibaro/cover.py +++ b/homeassistant/components/fibaro/cover.py @@ -46,6 +46,8 @@ class FibaroCover(FibaroDevice, CoverEntity): self._attr_supported_features = ( CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE ) + if "stop" in self.fibaro_device.actions: + self._attr_supported_features |= CoverEntityFeature.STOP @staticmethod def bound(position): From 854b0dbb2d8260c90eb2855578520a1adf791d9a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Jun 2022 18:13:31 -1000 Subject: [PATCH 1294/3516] Reduce branching in generated lambda_stmts (#73042) --- homeassistant/components/recorder/history.py | 33 ++++-- .../components/recorder/statistics.py | 103 ++++++++++++------ tests/components/recorder/test_statistics.py | 62 +++++++++++ 3 files changed, 153 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 49796bd0158..5dd5c0d3040 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -15,6 +15,7 @@ from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Subquery from homeassistant.components import recorder from homeassistant.components.websocket_api.const import ( @@ -485,6 +486,25 @@ def _get_states_for_entites_stmt( return stmt +def _generate_most_recent_states_by_date( + run_start: datetime, + utc_point_in_time: datetime, +) -> Subquery: + """Generate the sub query for the most recent states by data.""" + return ( + select( + States.entity_id.label("max_entity_id"), + func.max(States.last_updated).label("max_last_updated"), + ) + .filter( + (States.last_updated >= run_start) + & (States.last_updated < utc_point_in_time) + ) + .group_by(States.entity_id) + .subquery() + ) + + def _get_states_for_all_stmt( schema_version: int, run_start: datetime, @@ -500,17 +520,8 @@ def _get_states_for_all_stmt( # query, then filter out unwanted domains as well as applying the custom filter. # This filtering can't be done in the inner query because the domain column is # not indexed and we can't control what's in the custom filter. - most_recent_states_by_date = ( - select( - States.entity_id.label("max_entity_id"), - func.max(States.last_updated).label("max_last_updated"), - ) - .filter( - (States.last_updated >= run_start) - & (States.last_updated < utc_point_in_time) - ) - .group_by(States.entity_id) - .subquery() + most_recent_states_by_date = _generate_most_recent_states_by_date( + run_start, utc_point_in_time ) stmt += lambda q: q.where( States.state_id diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 39fcb954ee9..012b34ec0ef 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -20,6 +20,7 @@ from sqlalchemy.exc import SQLAlchemyError, StatementError from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal_column, true from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Subquery import voluptuous as vol from homeassistant.const import ( @@ -484,14 +485,13 @@ def _compile_hourly_statistics_summary_mean_stmt( start_time: datetime, end_time: datetime ) -> StatementLambdaElement: """Generate the summary mean statement for hourly statistics.""" - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SUMMARY_MEAN)) - stmt += ( - lambda q: q.filter(StatisticsShortTerm.start >= start_time) + return lambda_stmt( + lambda: select(*QUERY_STATISTICS_SUMMARY_MEAN) + .filter(StatisticsShortTerm.start >= start_time) .filter(StatisticsShortTerm.start < end_time) .group_by(StatisticsShortTerm.metadata_id) .order_by(StatisticsShortTerm.metadata_id) ) - return stmt def compile_hourly_statistics( @@ -985,26 +985,43 @@ def _statistics_during_period_stmt( start_time: datetime, end_time: datetime | None, metadata_ids: list[int] | None, - table: type[Statistics | StatisticsShortTerm], ) -> StatementLambdaElement: """Prepare a database query for statistics during a given period. This prepares a lambda_stmt query, so we don't insert the parameters yet. """ - if table == StatisticsShortTerm: - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SHORT_TERM)) - else: - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS)) - - stmt += lambda q: q.filter(table.start >= start_time) - + stmt = lambda_stmt( + lambda: select(*QUERY_STATISTICS).filter(Statistics.start >= start_time) + ) if end_time is not None: - stmt += lambda q: q.filter(table.start < end_time) - + stmt += lambda q: q.filter(Statistics.start < end_time) if metadata_ids: - stmt += lambda q: q.filter(table.metadata_id.in_(metadata_ids)) + stmt += lambda q: q.filter(Statistics.metadata_id.in_(metadata_ids)) + stmt += lambda q: q.order_by(Statistics.metadata_id, Statistics.start) + return stmt - stmt += lambda q: q.order_by(table.metadata_id, table.start) + +def _statistics_during_period_stmt_short_term( + start_time: datetime, + end_time: datetime | None, + metadata_ids: list[int] | None, +) -> StatementLambdaElement: + """Prepare a database query for short term statistics during a given period. + + This prepares a lambda_stmt query, so we don't insert the parameters yet. + """ + stmt = lambda_stmt( + lambda: select(*QUERY_STATISTICS_SHORT_TERM).filter( + StatisticsShortTerm.start >= start_time + ) + ) + if end_time is not None: + stmt += lambda q: q.filter(StatisticsShortTerm.start < end_time) + if metadata_ids: + stmt += lambda q: q.filter(StatisticsShortTerm.metadata_id.in_(metadata_ids)) + stmt += lambda q: q.order_by( + StatisticsShortTerm.metadata_id, StatisticsShortTerm.start + ) return stmt @@ -1034,10 +1051,12 @@ def statistics_during_period( if period == "5minute": table = StatisticsShortTerm + stmt = _statistics_during_period_stmt_short_term( + start_time, end_time, metadata_ids + ) else: table = Statistics - - stmt = _statistics_during_period_stmt(start_time, end_time, metadata_ids, table) + stmt = _statistics_during_period_stmt(start_time, end_time, metadata_ids) stats = execute_stmt_lambda_element(session, stmt) if not stats: @@ -1069,19 +1088,27 @@ def statistics_during_period( def _get_last_statistics_stmt( metadata_id: int, number_of_stats: int, - table: type[Statistics | StatisticsShortTerm], ) -> StatementLambdaElement: """Generate a statement for number_of_stats statistics for a given statistic_id.""" - if table == StatisticsShortTerm: - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SHORT_TERM)) - else: - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS)) - stmt += ( - lambda q: q.filter_by(metadata_id=metadata_id) - .order_by(table.metadata_id, table.start.desc()) + return lambda_stmt( + lambda: select(*QUERY_STATISTICS) + .filter_by(metadata_id=metadata_id) + .order_by(Statistics.metadata_id, Statistics.start.desc()) + .limit(number_of_stats) + ) + + +def _get_last_statistics_short_term_stmt( + metadata_id: int, + number_of_stats: int, +) -> StatementLambdaElement: + """Generate a statement for number_of_stats short term statistics for a given statistic_id.""" + return lambda_stmt( + lambda: select(*QUERY_STATISTICS_SHORT_TERM) + .filter_by(metadata_id=metadata_id) + .order_by(StatisticsShortTerm.metadata_id, StatisticsShortTerm.start.desc()) .limit(number_of_stats) ) - return stmt def _get_last_statistics( @@ -1099,7 +1126,10 @@ def _get_last_statistics( if not metadata: return {} metadata_id = metadata[statistic_id][0] - stmt = _get_last_statistics_stmt(metadata_id, number_of_stats, table) + if table == Statistics: + stmt = _get_last_statistics_stmt(metadata_id, number_of_stats) + else: + stmt = _get_last_statistics_short_term_stmt(metadata_id, number_of_stats) stats = execute_stmt_lambda_element(session, stmt) if not stats: @@ -1136,12 +1166,9 @@ def get_last_short_term_statistics( ) -def _latest_short_term_statistics_stmt( - metadata_ids: list[int], -) -> StatementLambdaElement: - """Create the statement for finding the latest short term stat rows.""" - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SHORT_TERM)) - most_recent_statistic_row = ( +def _generate_most_recent_statistic_row(metadata_ids: list[int]) -> Subquery: + """Generate the subquery to find the most recent statistic row.""" + return ( select( StatisticsShortTerm.metadata_id, func.max(StatisticsShortTerm.start).label("start_max"), @@ -1149,6 +1176,14 @@ def _latest_short_term_statistics_stmt( .where(StatisticsShortTerm.metadata_id.in_(metadata_ids)) .group_by(StatisticsShortTerm.metadata_id) ).subquery() + + +def _latest_short_term_statistics_stmt( + metadata_ids: list[int], +) -> StatementLambdaElement: + """Create the statement for finding the latest short term stat rows.""" + stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SHORT_TERM)) + most_recent_statistic_row = _generate_most_recent_statistic_row(metadata_ids) stmt += lambda s: s.join( most_recent_statistic_row, ( diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 882f00d2940..97e64716f49 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -100,6 +100,15 @@ def test_compile_hourly_statistics(hass_recorder): stats = statistics_during_period(hass, zero, period="5minute") assert stats == {"sensor.test1": expected_stats1, "sensor.test2": expected_stats2} + # Test statistics_during_period with a far future start and end date + future = dt_util.as_utc(dt_util.parse_datetime("2221-11-01 00:00:00")) + stats = statistics_during_period(hass, future, end_time=future, period="5minute") + assert stats == {} + + # Test statistics_during_period with a far future end date + stats = statistics_during_period(hass, zero, end_time=future, period="5minute") + assert stats == {"sensor.test1": expected_stats1, "sensor.test2": expected_stats2} + stats = statistics_during_period( hass, zero, statistic_ids=["sensor.test2"], period="5minute" ) @@ -814,6 +823,59 @@ def test_monthly_statistics(hass_recorder, caplog, timezone): ] } + stats = statistics_during_period( + hass, + start_time=zero, + statistic_ids=["not", "the", "same", "test:total_energy_import"], + period="month", + ) + sep_start = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + sep_end = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + oct_start = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + oct_end = dt_util.as_utc(dt_util.parse_datetime("2021-11-01 00:00:00")) + assert stats == { + "test:total_energy_import": [ + { + "statistic_id": "test:total_energy_import", + "start": sep_start.isoformat(), + "end": sep_end.isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(3.0), + }, + { + "statistic_id": "test:total_energy_import", + "start": oct_start.isoformat(), + "end": oct_end.isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(3.0), + "sum": approx(5.0), + }, + ] + } + + # Use 5minute to ensure table switch works + stats = statistics_during_period( + hass, + start_time=zero, + statistic_ids=["test:total_energy_import", "with_other"], + period="5minute", + ) + assert stats == {} + + # Ensure future date has not data + future = dt_util.as_utc(dt_util.parse_datetime("2221-11-01 00:00:00")) + stats = statistics_during_period( + hass, start_time=future, end_time=future, period="month" + ) + assert stats == {} + dt_util.set_default_time_zone(dt_util.get_time_zone("UTC")) From 06a2fe94d371ae1ff4e053c2ef47135cf9ac3f7a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Jun 2022 18:14:47 -1000 Subject: [PATCH 1295/3516] Send an empty logbook response when all requested entity_ids are filtered away (#73046) --- .../components/logbook/websocket_api.py | 38 ++++- .../components/logbook/test_websocket_api.py | 157 +++++++++++++++++- 2 files changed, 184 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index 1af44440803..b27ae65b70c 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -67,6 +67,23 @@ async def _async_wait_for_recorder_sync(hass: HomeAssistant) -> None: ) +@callback +def _async_send_empty_response( + connection: ActiveConnection, msg_id: int, start_time: dt, end_time: dt | None +) -> None: + """Send an empty response. + + The current case for this is when they ask for entity_ids + that will all be filtered away because they have UOMs or + state_class. + """ + connection.send_result(msg_id) + stream_end_time = end_time or dt_util.utcnow() + empty_stream_message = _generate_stream_message([], start_time, stream_end_time) + empty_response = messages.event_message(msg_id, empty_stream_message) + connection.send_message(JSON_DUMP(empty_response)) + + async def _async_send_historical_events( hass: HomeAssistant, connection: ActiveConnection, @@ -171,6 +188,17 @@ async def _async_get_ws_stream_events( ) +def _generate_stream_message( + events: list[dict[str, Any]], start_day: dt, end_day: dt +) -> dict[str, Any]: + """Generate a logbook stream message response.""" + return { + "events": events, + "start_time": dt_util.utc_to_timestamp(start_day), + "end_time": dt_util.utc_to_timestamp(end_day), + } + + def _ws_stream_get_events( msg_id: int, start_day: dt, @@ -184,11 +212,7 @@ def _ws_stream_get_events( last_time = None if events: last_time = dt_util.utc_from_timestamp(events[-1]["when"]) - message = { - "events": events, - "start_time": dt_util.utc_to_timestamp(start_day), - "end_time": dt_util.utc_to_timestamp(end_day), - } + message = _generate_stream_message(events, start_day, end_day) if partial: # This is a hint to consumers of the api that # we are about to send a another block of historical @@ -275,6 +299,10 @@ async def ws_event_stream( entity_ids = msg.get("entity_ids") if entity_ids: entity_ids = async_filter_entities(hass, entity_ids) + if not entity_ids: + _async_send_empty_response(connection, msg_id, start_time, end_time) + return + event_types = async_determine_event_types(hass, entity_ids, device_ids) event_processor = EventProcessor( hass, diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 2dd08ec44ce..2623a5b17d5 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -2102,11 +2102,17 @@ async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_clie ] ) await async_wait_recording_done(hass) + entity_ids = ("sensor.uom", "sensor.uom_two") + + def _cycle_entities(): + for entity_id in entity_ids: + for state in ("1", "2", "3"): + hass.states.async_set( + entity_id, state, {ATTR_UNIT_OF_MEASUREMENT: "any"} + ) init_count = sum(hass.bus.async_listeners().values()) - hass.states.async_set("sensor.uom", "1", {ATTR_UNIT_OF_MEASUREMENT: "any"}) - hass.states.async_set("sensor.uom", "2", {ATTR_UNIT_OF_MEASUREMENT: "any"}) - hass.states.async_set("sensor.uom", "3", {ATTR_UNIT_OF_MEASUREMENT: "any"}) + _cycle_entities() await async_wait_recording_done(hass) websocket_client = await hass_ws_client() @@ -2124,9 +2130,61 @@ async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_clie assert msg["type"] == TYPE_RESULT assert msg["success"] - hass.states.async_set("sensor.uom", "1", {ATTR_UNIT_OF_MEASUREMENT: "any"}) - hass.states.async_set("sensor.uom", "2", {ATTR_UNIT_OF_MEASUREMENT: "any"}) - hass.states.async_set("sensor.uom", "3", {ATTR_UNIT_OF_MEASUREMENT: "any"}) + _cycle_entities() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [] + + await websocket_client.close() + await hass.async_block_till_done() + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_all_entities_have_uom_multiple( + hass, recorder_mock, hass_ws_client +): + """Test logbook stream with specific request for multiple entities that are always filtered.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + await async_wait_recording_done(hass) + entity_ids = ("sensor.uom", "sensor.uom_two") + + def _cycle_entities(): + for entity_id in entity_ids: + for state in ("1", "2", "3"): + hass.states.async_set( + entity_id, state, {ATTR_UNIT_OF_MEASUREMENT: "any"} + ) + + init_count = sum(hass.bus.async_listeners().values()) + _cycle_entities() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "entity_ids": [*entity_ids], + } + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + _cycle_entities() msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 @@ -2138,3 +2196,90 @@ async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_clie # Check our listener got unsubscribed assert sum(hass.bus.async_listeners().values()) == init_count + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_entities_some_have_uom_multiple( + hass, recorder_mock, hass_ws_client +): + """Test logbook stream with uom filtered entities and non-fitlered entities.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + await async_wait_recording_done(hass) + filtered_entity_ids = ("sensor.uom", "sensor.uom_two") + non_filtered_entity_ids = ("sensor.keep", "sensor.keep_two") + + def _cycle_entities(): + for entity_id in filtered_entity_ids: + for state in ("1", "2", "3"): + hass.states.async_set( + entity_id, state, {ATTR_UNIT_OF_MEASUREMENT: "any"} + ) + for entity_id in non_filtered_entity_ids: + for state in (STATE_ON, STATE_OFF): + hass.states.async_set(entity_id, state) + + init_count = sum(hass.bus.async_listeners().values()) + _cycle_entities() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "entity_ids": [*filtered_entity_ids, *non_filtered_entity_ids], + } + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + _cycle_entities() + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["partial"] is True + assert msg["event"]["events"] == [ + {"entity_id": "sensor.keep", "state": "off", "when": ANY}, + {"entity_id": "sensor.keep_two", "state": "off", "when": ANY}, + ] + + _cycle_entities() + await hass.async_block_till_done() + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [] + assert "partial" not in msg["event"] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + {"entity_id": "sensor.keep", "state": "on", "when": ANY}, + {"entity_id": "sensor.keep", "state": "off", "when": ANY}, + {"entity_id": "sensor.keep_two", "state": "on", "when": ANY}, + {"entity_id": "sensor.keep_two", "state": "off", "when": ANY}, + {"entity_id": "sensor.keep", "state": "on", "when": ANY}, + {"entity_id": "sensor.keep", "state": "off", "when": ANY}, + {"entity_id": "sensor.keep_two", "state": "on", "when": ANY}, + {"entity_id": "sensor.keep_two", "state": "off", "when": ANY}, + ] + assert "partial" not in msg["event"] + + await websocket_client.close() + await hass.async_block_till_done() + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count From 7bdada789854f4f8446e4865aed22a15e031618a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Jun 2022 16:34:04 -1000 Subject: [PATCH 1296/3516] Bump aiolookup to 0.1.1 (#73048) --- 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 7cf70540372..b58eb254f8f 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", "@bdraco"], - "requirements": ["aiolookin==0.1.0"], + "requirements": ["aiolookin==0.1.1"], "zeroconf": ["_lookin._tcp.local."], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index adf129b1250..dc272f3d949 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -187,7 +187,7 @@ aiolifx==0.7.1 aiolifx_effects==0.2.2 # homeassistant.components.lookin -aiolookin==0.1.0 +aiolookin==0.1.1 # homeassistant.components.lyric aiolyric==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2d9b0100da4..ce2396ec6ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -159,7 +159,7 @@ aiohue==4.4.1 aiokafka==0.6.0 # homeassistant.components.lookin -aiolookin==0.1.0 +aiolookin==0.1.1 # homeassistant.components.lyric aiolyric==1.0.8 From 1e59ce19f5e1cd1d87f1c7a3bd201cf5c83173c6 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 5 Jun 2022 09:13:54 -0600 Subject: [PATCH 1297/3516] Bump simplisafe-python to 2022.06.0 (#73054) --- 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 f62da735f92..4da8f09eff8 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==2022.05.2"], + "requirements": ["simplisafe-python==2022.06.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index dc272f3d949..baac079e585 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2168,7 +2168,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.05.2 +simplisafe-python==2022.06.0 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ce2396ec6ab..20fd6b229b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1425,7 +1425,7 @@ sharkiq==0.0.1 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==2022.05.2 +simplisafe-python==2022.06.0 # homeassistant.components.slack slackclient==2.5.0 From ca05cde6ba416592b5bd5cdb0fc2ce29066579e7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 5 Jun 2022 19:33:27 -0600 Subject: [PATCH 1298/3516] Fix unhandled exception when RainMachine coordinator data doesn't exist (#73055) --- homeassistant/components/rainmachine/binary_sensor.py | 7 +++++-- homeassistant/components/rainmachine/sensor.py | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 1818222a8f4..7a13515db3b 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -139,8 +139,11 @@ async def async_setup_entry( entry, coordinator, controller, description ) for description in BINARY_SENSOR_DESCRIPTIONS - if (coordinator := coordinators[description.api_category]) is not None - and key_exists(coordinator.data, description.data_key) + if ( + (coordinator := coordinators[description.api_category]) is not None + and coordinator.data + and key_exists(coordinator.data, description.data_key) + ) ] ) diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 522c57cf7a2..cc37189aa49 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -133,8 +133,11 @@ async def async_setup_entry( entry, coordinator, controller, description ) for description in SENSOR_DESCRIPTIONS - if (coordinator := coordinators[description.api_category]) is not None - and key_exists(coordinator.data, description.data_key) + if ( + (coordinator := coordinators[description.api_category]) is not None + and coordinator.data + and key_exists(coordinator.data, description.data_key) + ) ] zone_coordinator = coordinators[DATA_ZONES] From 22bdeab1e78f7081dedbbba92f457d663e668da7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 5 Jun 2022 09:13:43 -0600 Subject: [PATCH 1299/3516] Bump regenmaschine to 2022.06.0 (#73056) --- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 98dc9a6c877..a61283ea298 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.05.1"], + "requirements": ["regenmaschine==2022.06.0"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index baac079e585..27667d6b0fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2065,7 +2065,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.05.1 +regenmaschine==2022.06.0 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 20fd6b229b5..6867d9d84c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1364,7 +1364,7 @@ rachiopy==1.0.3 radios==0.1.1 # homeassistant.components.rainmachine -regenmaschine==2022.05.1 +regenmaschine==2022.06.0 # homeassistant.components.renault renault-api==0.1.11 From c47774e273531570d3179d899fe0c93327163cf1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Jun 2022 19:06:49 -1000 Subject: [PATCH 1300/3516] Fix incompatiblity with live logbook and google_assistant (#73063) --- homeassistant/components/logbook/const.py | 10 +- homeassistant/components/logbook/helpers.py | 179 ++++++++++++------ homeassistant/components/logbook/processor.py | 33 +--- .../components/logbook/websocket_api.py | 14 +- tests/components/logbook/common.py | 1 - .../components/logbook/test_websocket_api.py | 153 +++++++++++++-- 6 files changed, 271 insertions(+), 119 deletions(-) diff --git a/homeassistant/components/logbook/const.py b/homeassistant/components/logbook/const.py index 3f0c6599724..d20acb553cc 100644 --- a/homeassistant/components/logbook/const.py +++ b/homeassistant/components/logbook/const.py @@ -30,13 +30,11 @@ LOGBOOK_ENTRY_NAME = "name" LOGBOOK_ENTRY_STATE = "state" LOGBOOK_ENTRY_WHEN = "when" -ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED = {EVENT_LOGBOOK_ENTRY, EVENT_CALL_SERVICE} -ENTITY_EVENTS_WITHOUT_CONFIG_ENTRY = { - EVENT_LOGBOOK_ENTRY, - EVENT_AUTOMATION_TRIGGERED, - EVENT_SCRIPT_STARTED, -} +# Automation events that can affect an entity_id or device_id +AUTOMATION_EVENTS = {EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED} +# Events that are built-in to the logbook or core +BUILT_IN_EVENTS = {EVENT_LOGBOOK_ENTRY, EVENT_CALL_SERVICE} LOGBOOK_FILTERS = "logbook_filters" LOGBOOK_ENTITIES_FILTER = "entities_filter" diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index de021994b8d..eec60ebe740 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -7,6 +7,7 @@ from typing import Any from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.const import ( ATTR_DEVICE_ID, + ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, EVENT_LOGBOOK_ENTRY, @@ -21,13 +22,10 @@ from homeassistant.core import ( is_callback, ) from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entityfilter import EntityFilter from homeassistant.helpers.event import async_track_state_change_event -from .const import ( - ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, - DOMAIN, - ENTITY_EVENTS_WITHOUT_CONFIG_ENTRY, -) +from .const import AUTOMATION_EVENTS, BUILT_IN_EVENTS, DOMAIN from .models import LazyEventPartialState @@ -41,6 +39,25 @@ def async_filter_entities(hass: HomeAssistant, entity_ids: list[str]) -> list[st ] +@callback +def _async_config_entries_for_ids( + hass: HomeAssistant, entity_ids: list[str] | None, device_ids: list[str] | None +) -> set[str]: + """Find the config entry ids for a set of entities or devices.""" + config_entry_ids: set[str] = set() + if entity_ids: + eng_reg = er.async_get(hass) + for entity_id in entity_ids: + if (entry := eng_reg.async_get(entity_id)) and entry.config_entry_id: + config_entry_ids.add(entry.config_entry_id) + if device_ids: + dev_reg = dr.async_get(hass) + for device_id in device_ids: + if (device := dev_reg.async_get(device_id)) and device.config_entries: + config_entry_ids |= device.config_entries + return config_entry_ids + + def async_determine_event_types( hass: HomeAssistant, entity_ids: list[str] | None, device_ids: list[str] | None ) -> tuple[str, ...]: @@ -49,42 +66,91 @@ def async_determine_event_types( str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]] ] = hass.data.get(DOMAIN, {}) if not entity_ids and not device_ids: - return (*ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED, *external_events) - config_entry_ids: set[str] = set() - intrested_event_types: set[str] = set() + return (*BUILT_IN_EVENTS, *external_events) + interested_domains: set[str] = set() + for entry_id in _async_config_entries_for_ids(hass, entity_ids, device_ids): + if entry := hass.config_entries.async_get_entry(entry_id): + interested_domains.add(entry.domain) + + # + # automations and scripts can refer to entities or devices + # but they do not have a config entry so we need + # to add them since we have historically included + # them when matching only on entities + # + intrested_event_types: set[str] = { + external_event + for external_event, domain_call in external_events.items() + if domain_call[0] in interested_domains + } | AUTOMATION_EVENTS if entity_ids: - # - # Home Assistant doesn't allow firing events from - # entities so we have a limited list to check - # - # automations and scripts can refer to entities - # but they do not have a config entry so we need - # to add them. - # - # We also allow entity_ids to be recorded via - # manual logbook entries. - # - intrested_event_types |= ENTITY_EVENTS_WITHOUT_CONFIG_ENTRY + # We also allow entity_ids to be recorded via manual logbook entries. + intrested_event_types.add(EVENT_LOGBOOK_ENTRY) - if device_ids: - dev_reg = dr.async_get(hass) - for device_id in device_ids: - if (device := dev_reg.async_get(device_id)) and device.config_entries: - config_entry_ids |= device.config_entries - interested_domains: set[str] = set() - for entry_id in config_entry_ids: - if entry := hass.config_entries.async_get_entry(entry_id): - interested_domains.add(entry.domain) - for external_event, domain_call in external_events.items(): - if domain_call[0] in interested_domains: - intrested_event_types.add(external_event) + return tuple(intrested_event_types) - return tuple( - event_type - for event_type in (EVENT_LOGBOOK_ENTRY, *external_events) - if event_type in intrested_event_types - ) + +@callback +def extract_attr(source: dict[str, Any], attr: str) -> list[str]: + """Extract an attribute as a list or string.""" + if (value := source.get(attr)) is None: + return [] + if isinstance(value, list): + return value + return str(value).split(",") + + +@callback +def event_forwarder_filtered( + target: Callable[[Event], None], + entities_filter: EntityFilter | None, + entity_ids: list[str] | None, + device_ids: list[str] | None, +) -> Callable[[Event], None]: + """Make a callable to filter events.""" + if not entities_filter and not entity_ids and not device_ids: + # No filter + # - Script Trace (context ids) + # - Automation Trace (context ids) + return target + + if entities_filter: + # We have an entity filter: + # - Logbook panel + + @callback + def _forward_events_filtered_by_entities_filter(event: Event) -> None: + assert entities_filter is not None + event_data = event.data + entity_ids = extract_attr(event_data, ATTR_ENTITY_ID) + if entity_ids and not any( + entities_filter(entity_id) for entity_id in entity_ids + ): + return + domain = event_data.get(ATTR_DOMAIN) + if domain and not entities_filter(f"{domain}._"): + return + target(event) + + return _forward_events_filtered_by_entities_filter + + # We are filtering on entity_ids and/or device_ids: + # - Areas + # - Devices + # - Logbook Card + entity_ids_set = set(entity_ids) if entity_ids else set() + device_ids_set = set(device_ids) if device_ids else set() + + @callback + def _forward_events_filtered_by_device_entity_ids(event: Event) -> None: + event_data = event.data + if entity_ids_set.intersection( + extract_attr(event_data, ATTR_ENTITY_ID) + ) or device_ids_set.intersection(extract_attr(event_data, ATTR_DEVICE_ID)): + target(event) + + return _forward_events_filtered_by_device_entity_ids @callback @@ -93,6 +159,7 @@ def async_subscribe_events( subscriptions: list[CALLBACK_TYPE], target: Callable[[Event], None], event_types: tuple[str, ...], + entities_filter: EntityFilter | None, entity_ids: list[str] | None, device_ids: list[str] | None, ) -> None: @@ -103,41 +170,31 @@ def async_subscribe_events( """ ent_reg = er.async_get(hass) assert is_callback(target), "target must be a callback" - event_forwarder = target - - if entity_ids or device_ids: - entity_ids_set = set(entity_ids) if entity_ids else set() - device_ids_set = set(device_ids) if device_ids else set() - - @callback - def _forward_events_filtered(event: Event) -> None: - event_data = event.data - if ( - entity_ids_set and event_data.get(ATTR_ENTITY_ID) in entity_ids_set - ) or (device_ids_set and event_data.get(ATTR_DEVICE_ID) in device_ids_set): - target(event) - - event_forwarder = _forward_events_filtered - + event_forwarder = event_forwarder_filtered( + target, entities_filter, entity_ids, device_ids + ) for event_type in event_types: subscriptions.append( hass.bus.async_listen(event_type, event_forwarder, run_immediately=True) ) - @callback - def _forward_state_events_filtered(event: Event) -> None: - if event.data.get("old_state") is None or event.data.get("new_state") is None: - return - state: State = event.data["new_state"] - if not _is_state_filtered(ent_reg, state): - target(event) - if device_ids and not entity_ids: # No entities to subscribe to but we are filtering # on device ids so we do not want to get any state # changed events return + @callback + def _forward_state_events_filtered(event: Event) -> None: + if event.data.get("old_state") is None or event.data.get("new_state") is None: + return + state: State = event.data["new_state"] + if _is_state_filtered(ent_reg, state) or ( + entities_filter and not entities_filter(state.entity_id) + ): + return + target(event) + if entity_ids: subscriptions.append( async_track_state_change_event( diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index b3a43c2ca35..595268e5582 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -5,8 +5,6 @@ from collections.abc import Callable, Generator from contextlib import suppress from dataclasses import dataclass from datetime import datetime as dt -import logging -import re from typing import Any from sqlalchemy.engine.row import Row @@ -30,7 +28,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entityfilter import EntityFilter import homeassistant.util.dt as dt_util from .const import ( @@ -46,7 +43,6 @@ from .const import ( CONTEXT_STATE, CONTEXT_USER_ID, DOMAIN, - LOGBOOK_ENTITIES_FILTER, LOGBOOK_ENTRY_DOMAIN, LOGBOOK_ENTRY_ENTITY_ID, LOGBOOK_ENTRY_ICON, @@ -62,11 +58,6 @@ from .models import EventAsRow, LazyEventPartialState, async_event_to_row from .queries import statement_for_request from .queries.common import PSUEDO_EVENT_STATE_CHANGED -_LOGGER = logging.getLogger(__name__) - -ENTITY_ID_JSON_EXTRACT = re.compile('"entity_id": ?"([^"]+)"') -DOMAIN_JSON_EXTRACT = re.compile('"domain": ?"([^"]+)"') - @dataclass class LogbookRun: @@ -106,10 +97,6 @@ class EventProcessor: self.device_ids = device_ids self.context_id = context_id self.filters: Filters | None = hass.data[LOGBOOK_FILTERS] - if self.limited_select: - self.entities_filter: EntityFilter | Callable[[str], bool] | None = None - else: - self.entities_filter = hass.data[LOGBOOK_ENTITIES_FILTER] format_time = ( _row_time_fired_timestamp if timestamp else _row_time_fired_isoformat ) @@ -183,7 +170,6 @@ class EventProcessor: return list( _humanify( row_generator, - self.entities_filter, self.ent_reg, self.logbook_run, self.context_augmenter, @@ -193,7 +179,6 @@ class EventProcessor: def _humanify( rows: Generator[Row | EventAsRow, None, None], - entities_filter: EntityFilter | Callable[[str], bool] | None, ent_reg: er.EntityRegistry, logbook_run: LogbookRun, context_augmenter: ContextAugmenter, @@ -208,29 +193,13 @@ def _humanify( include_entity_name = logbook_run.include_entity_name format_time = logbook_run.format_time - def _keep_row(row: EventAsRow) -> bool: - """Check if the entity_filter rejects a row.""" - assert entities_filter is not None - if entity_id := row.entity_id: - return entities_filter(entity_id) - if entity_id := row.data.get(ATTR_ENTITY_ID): - return entities_filter(entity_id) - if domain := row.data.get(ATTR_DOMAIN): - return entities_filter(f"{domain}._") - return True - # Process rows for row in rows: context_id = context_lookup.memorize(row) if row.context_only: continue event_type = row.event_type - if event_type == EVENT_CALL_SERVICE or ( - entities_filter - # We literally mean is EventAsRow not a subclass of EventAsRow - and type(row) is EventAsRow # pylint: disable=unidiomatic-typecheck - and not _keep_row(row) - ): + if event_type == EVENT_CALL_SERVICE: continue if event_type is PSUEDO_EVENT_STATE_CHANGED: entity_id = row.entity_id diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index b27ae65b70c..a8f9bc50920 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -16,9 +16,11 @@ from homeassistant.components.websocket_api import messages from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.components.websocket_api.const import JSON_DUMP from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.helpers.entityfilter import EntityFilter from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.dt as dt_util +from .const import LOGBOOK_ENTITIES_FILTER from .helpers import ( async_determine_event_types, async_filter_entities, @@ -365,8 +367,18 @@ async def ws_event_stream( ) _unsub() + entities_filter: EntityFilter | None = None + if not event_processor.limited_select: + entities_filter = hass.data[LOGBOOK_ENTITIES_FILTER] + async_subscribe_events( - hass, subscriptions, _queue_or_cancel, event_types, entity_ids, device_ids + hass, + subscriptions, + _queue_or_cancel, + event_types, + entities_filter, + entity_ids, + device_ids, ) subscriptions_setup_complete_time = dt_util.utcnow() connection.subscriptions[msg_id] = _unsub diff --git a/tests/components/logbook/common.py b/tests/components/logbook/common.py index b88c3854967..a41f983bfed 100644 --- a/tests/components/logbook/common.py +++ b/tests/components/logbook/common.py @@ -68,7 +68,6 @@ def mock_humanify(hass_, rows): return list( processor._humanify( rows, - None, ent_reg, logbook_run, context_augmenter, diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 2623a5b17d5..ae1f7968e3b 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -27,8 +27,8 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import Event, HomeAssistant, State -from homeassistant.helpers import device_registry +from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -51,22 +51,8 @@ def set_utc(hass): hass.config.set_time_zone("UTC") -async def _async_mock_device_with_logbook_platform(hass): - """Mock an integration that provides a device that are described by the logbook.""" - entry = MockConfigEntry(domain="test", data={"first": True}, options=None) - entry.add_to_hass(hass) - dev_reg = device_registry.async_get(hass) - device = dev_reg.async_get_or_create( - config_entry_id=entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - identifiers={("bridgeid", "0123")}, - sw_version="sw-version", - name="device name", - manufacturer="manufacturer", - model="model", - suggested_area="Game Room", - ) - +@callback +async def _async_mock_logbook_platform(hass: HomeAssistant) -> None: class MockLogbookPlatform: """Mock a logbook platform.""" @@ -90,6 +76,40 @@ async def _async_mock_device_with_logbook_platform(hass): async_describe_event("test", "mock_event", async_describe_test_event) await logbook._process_logbook_platform(hass, "test", MockLogbookPlatform) + + +async def _async_mock_entity_with_logbook_platform(hass): + """Mock an integration that provides an entity that are described by the logbook.""" + entry = MockConfigEntry(domain="test", data={"first": True}, options=None) + entry.add_to_hass(hass) + ent_reg = entity_registry.async_get(hass) + entry = ent_reg.async_get_or_create( + platform="test", + domain="sensor", + config_entry=entry, + unique_id="1234", + suggested_object_id="test", + ) + await _async_mock_logbook_platform(hass) + return entry + + +async def _async_mock_device_with_logbook_platform(hass): + """Mock an integration that provides a device that are described by the logbook.""" + entry = MockConfigEntry(domain="test", data={"first": True}, options=None) + entry.add_to_hass(hass) + dev_reg = device_registry.async_get(hass) + device = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + sw_version="sw-version", + name="device name", + manufacturer="manufacturer", + model="model", + suggested_area="Game Room", + ) + await _async_mock_logbook_platform(hass) return device @@ -1786,6 +1806,103 @@ async def test_event_stream_bad_start_time(hass, hass_ws_client, recorder_mock): assert response["error"]["code"] == "invalid_start_time" +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_logbook_stream_match_multiple_entities( + hass, recorder_mock, hass_ws_client +): + """Test logbook stream with a described integration that uses multiple entities.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + entry = await _async_mock_entity_with_logbook_platform(hass) + entity_id = entry.entity_id + hass.states.async_set(entity_id, STATE_ON) + + await hass.async_block_till_done() + init_count = sum(hass.bus.async_listeners().values()) + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "entity_ids": [entity_id], + } + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # There are no answers to our initial query + # so we get an empty reply. This is to ensure + # consumers of the api know there are no results + # and its not a failure case. This is useful + # in the frontend so we can tell the user there + # are no results vs waiting for them to appear + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [] + await async_wait_recording_done(hass) + + hass.states.async_set("binary_sensor.should_not_appear", STATE_ON) + hass.states.async_set("binary_sensor.should_not_appear", STATE_OFF) + context = core.Context( + id="ac5bd62de45711eaaeb351041eec8dd9", + user_id="b400facee45711eaa9308bfd3d19e474", + ) + hass.bus.async_fire( + "mock_event", {"entity_id": ["sensor.any", entity_id]}, context=context + ) + hass.bus.async_fire("mock_event", {"entity_id": [f"sensor.any,{entity_id}"]}) + hass.bus.async_fire("mock_event", {"entity_id": ["sensor.no_match", "light.off"]}) + hass.states.async_set(entity_id, STATE_OFF, context=context) + await hass.async_block_till_done() + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + { + "context_user_id": "b400facee45711eaa9308bfd3d19e474", + "domain": "test", + "message": "is on fire", + "name": "device name", + "when": ANY, + }, + { + "context_domain": "test", + "context_event_type": "mock_event", + "context_message": "is on fire", + "context_name": "device name", + "context_user_id": "b400facee45711eaa9308bfd3d19e474", + "entity_id": "sensor.test", + "state": "off", + "when": ANY, + }, + ] + + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count + + async def test_event_stream_bad_end_time(hass, hass_ws_client, recorder_mock): """Test event_stream bad end time.""" await async_setup_component(hass, "logbook", {}) From 792ebbb600059ddb0e31b88ff054274e2156eeb6 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Sun, 5 Jun 2022 22:27:21 -0400 Subject: [PATCH 1301/3516] Fix elk attributes not being json serializable (#73096) * Fix jsonifying. * Only serialize Enums --- homeassistant/components/elkm1/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 9a7c7dbc43a..cf1cac3bdb3 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from enum import Enum import logging import re from types import MappingProxyType @@ -481,7 +482,10 @@ class ElkEntity(Entity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return the default attributes of the element.""" - return {**self._element.as_dict(), **self.initial_attrs()} + dict_as_str = {} + for key, val in self._element.as_dict().items(): + dict_as_str[key] = val.value if isinstance(val, Enum) else val + return {**dict_as_str, **self.initial_attrs()} @property def available(self) -> bool: From 93aad108a7b080b30250da14ad26b725e37edbd2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Jun 2022 21:25:26 -1000 Subject: [PATCH 1302/3516] Mark counter domain as continuous to exclude it from logbook (#73101) --- homeassistant/components/logbook/const.py | 9 ++++ homeassistant/components/logbook/helpers.py | 9 ++-- .../components/logbook/queries/common.py | 45 +++++++++++++------ homeassistant/components/recorder/filters.py | 9 +++- tests/components/logbook/test_init.py | 6 +++ .../components/logbook/test_websocket_api.py | 8 +++- 6 files changed, 66 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/logbook/const.py b/homeassistant/components/logbook/const.py index d20acb553cc..e1abd987659 100644 --- a/homeassistant/components/logbook/const.py +++ b/homeassistant/components/logbook/const.py @@ -2,9 +2,18 @@ from __future__ import annotations from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED +from homeassistant.components.counter import DOMAIN as COUNTER_DOMAIN +from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN from homeassistant.components.script import EVENT_SCRIPT_STARTED +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import EVENT_CALL_SERVICE, EVENT_LOGBOOK_ENTRY +# Domains that are always continuous +ALWAYS_CONTINUOUS_DOMAINS = {COUNTER_DOMAIN, PROXIMITY_DOMAIN} + +# Domains that are continuous if there is a UOM set on the entity +CONDITIONALLY_CONTINUOUS_DOMAINS = {SENSOR_DOMAIN} + ATTR_MESSAGE = "message" DOMAIN = "logbook" diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index eec60ebe740..ef322c44e05 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -20,12 +20,13 @@ from homeassistant.core import ( State, callback, is_callback, + split_entity_id, ) from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entityfilter import EntityFilter from homeassistant.helpers.event import async_track_state_change_event -from .const import AUTOMATION_EVENTS, BUILT_IN_EVENTS, DOMAIN +from .const import ALWAYS_CONTINUOUS_DOMAINS, AUTOMATION_EVENTS, BUILT_IN_EVENTS, DOMAIN from .models import LazyEventPartialState @@ -235,7 +236,8 @@ def _is_state_filtered(ent_reg: er.EntityRegistry, state: State) -> bool: we only get significant changes (state.last_changed != state.last_updated) """ return bool( - state.last_changed != state.last_updated + split_entity_id(state.entity_id)[0] in ALWAYS_CONTINUOUS_DOMAINS + or state.last_changed != state.last_updated or ATTR_UNIT_OF_MEASUREMENT in state.attributes or is_sensor_continuous(ent_reg, state.entity_id) ) @@ -250,7 +252,8 @@ def _is_entity_id_filtered( from the database when a list of entities is requested. """ return bool( - (state := hass.states.get(entity_id)) + split_entity_id(entity_id)[0] in ALWAYS_CONTINUOUS_DOMAINS + or (state := hass.states.get(entity_id)) and (ATTR_UNIT_OF_MEASUREMENT in state.attributes) or is_sensor_continuous(ent_reg, entity_id) ) diff --git a/homeassistant/components/logbook/queries/common.py b/homeassistant/components/logbook/queries/common.py index a7a4f84a59e..56925b60e62 100644 --- a/homeassistant/components/logbook/queries/common.py +++ b/homeassistant/components/logbook/queries/common.py @@ -10,7 +10,7 @@ from sqlalchemy.sql.elements import ClauseList from sqlalchemy.sql.expression import literal from sqlalchemy.sql.selectable import Select -from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN +from homeassistant.components.recorder.filters import like_domain_matchers from homeassistant.components.recorder.models import ( EVENTS_CONTEXT_ID_INDEX, OLD_FORMAT_ATTRS_JSON, @@ -22,15 +22,19 @@ from homeassistant.components.recorder.models import ( StateAttributes, States, ) -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -CONTINUOUS_DOMAINS = {PROXIMITY_DOMAIN, SENSOR_DOMAIN} -CONTINUOUS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in CONTINUOUS_DOMAINS] +from ..const import ALWAYS_CONTINUOUS_DOMAINS, CONDITIONALLY_CONTINUOUS_DOMAINS + +# Domains that are continuous if there is a UOM set on the entity +CONDITIONALLY_CONTINUOUS_ENTITY_ID_LIKE = like_domain_matchers( + CONDITIONALLY_CONTINUOUS_DOMAINS +) +# Domains that are always continuous +ALWAYS_CONTINUOUS_ENTITY_ID_LIKE = like_domain_matchers(ALWAYS_CONTINUOUS_DOMAINS) UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%" - PSUEDO_EVENT_STATE_CHANGED = None # Since we don't store event_types and None # and we don't store state_changed in events @@ -220,29 +224,44 @@ def _missing_state_matcher() -> sqlalchemy.and_: def _not_continuous_entity_matcher() -> sqlalchemy.or_: """Match non continuous entities.""" return sqlalchemy.or_( - _not_continuous_domain_matcher(), + # First exclude domains that may be continuous + _not_possible_continuous_domain_matcher(), + # But let in the entities in the possible continuous domains + # that are not actually continuous sensors because they lack a UOM sqlalchemy.and_( - _continuous_domain_matcher, _not_uom_attributes_matcher() + _conditionally_continuous_domain_matcher, _not_uom_attributes_matcher() ).self_group(), ) -def _not_continuous_domain_matcher() -> sqlalchemy.and_: - """Match not continuous domains.""" +def _not_possible_continuous_domain_matcher() -> sqlalchemy.and_: + """Match not continuous domains. + + This matches domain that are always considered continuous + and domains that are conditionally (if they have a UOM) + continuous domains. + """ return sqlalchemy.and_( *[ ~States.entity_id.like(entity_domain) - for entity_domain in CONTINUOUS_ENTITY_ID_LIKE + for entity_domain in ( + *ALWAYS_CONTINUOUS_ENTITY_ID_LIKE, + *CONDITIONALLY_CONTINUOUS_ENTITY_ID_LIKE, + ) ], ).self_group() -def _continuous_domain_matcher() -> sqlalchemy.or_: - """Match continuous domains.""" +def _conditionally_continuous_domain_matcher() -> sqlalchemy.or_: + """Match conditionally continuous domains. + + This matches domain that are only considered + continuous if a UOM is set. + """ return sqlalchemy.or_( *[ States.entity_id.like(entity_domain) - for entity_domain in CONTINUOUS_ENTITY_ID_LIKE + for entity_domain in CONDITIONALLY_CONTINUOUS_ENTITY_ID_LIKE ], ).self_group() diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 90851e9f251..0b3e0e68030 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -248,8 +248,13 @@ def _domain_matcher( domains: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: matchers = [ - (column.is_not(None) & cast(column, Text()).like(encoder(f"{domain}.%"))) - for domain in domains + (column.is_not(None) & cast(column, Text()).like(encoder(domain_matcher))) + for domain_matcher in like_domain_matchers(domains) for column in columns ] return or_(*matchers) if matchers else or_(False) + + +def like_domain_matchers(domains: Iterable[str]) -> list[str]: + """Convert a list of domains to sql LIKE matchers.""" + return [f"{domain}.%" for domain in domains] diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 651a00fb0cf..d16b3476d84 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -745,6 +745,12 @@ async def test_filter_continuous_sensor_values( entity_id_third = "light.bla" hass.states.async_set(entity_id_third, STATE_OFF, {"unit_of_measurement": "foo"}) hass.states.async_set(entity_id_third, STATE_ON, {"unit_of_measurement": "foo"}) + entity_id_proximity = "proximity.bla" + hass.states.async_set(entity_id_proximity, STATE_OFF) + hass.states.async_set(entity_id_proximity, STATE_ON) + entity_id_counter = "counter.bla" + hass.states.async_set(entity_id_counter, STATE_OFF) + hass.states.async_set(entity_id_counter, STATE_ON) await async_wait_recording_done(hass) diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index ae1f7968e3b..4df2f456eb6 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -2209,7 +2209,9 @@ async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplo @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) -async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_client): +async def test_subscribe_all_entities_are_continuous( + hass, recorder_mock, hass_ws_client +): """Test subscribe/unsubscribe logbook stream with entities that are always filtered.""" now = dt_util.utcnow() await asyncio.gather( @@ -2227,6 +2229,8 @@ async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_clie hass.states.async_set( entity_id, state, {ATTR_UNIT_OF_MEASUREMENT: "any"} ) + hass.states.async_set("counter.any", state) + hass.states.async_set("proximity.any", state) init_count = sum(hass.bus.async_listeners().values()) _cycle_entities() @@ -2238,7 +2242,7 @@ async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_clie "id": 7, "type": "logbook/event_stream", "start_time": now.isoformat(), - "entity_ids": ["sensor.uom"], + "entity_ids": ["sensor.uom", "counter.any", "proximity.any"], } ) From eef79e291218702a6b74a25fe9eac5c250d711e0 Mon Sep 17 00:00:00 2001 From: lymanepp <4195527+lymanepp@users.noreply.github.com> Date: Mon, 6 Jun 2022 00:10:33 -0400 Subject: [PATCH 1303/3516] Tomorrowio utc fix (#73102) * Discard past data using local time instead of UTC * Tweak changes to fix tests * Cleanup --- homeassistant/components/tomorrowio/weather.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/tomorrowio/weather.py b/homeassistant/components/tomorrowio/weather.py index bf687f8bdca..52eedff49b1 100644 --- a/homeassistant/components/tomorrowio/weather.py +++ b/homeassistant/components/tomorrowio/weather.py @@ -198,13 +198,16 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): max_forecasts = MAX_FORECASTS[self.forecast_type] forecast_count = 0 + # Convert utcnow to local to be compatible with tests + today = dt_util.as_local(dt_util.utcnow()).date() + # Set default values (in cases where keys don't exist), None will be # returned. Override properties per forecast type as needed for forecast in raw_forecasts: forecast_dt = dt_util.parse_datetime(forecast[TMRW_ATTR_TIMESTAMP]) # Throw out past data - if forecast_dt.date() < dt_util.utcnow().date(): + if dt_util.as_local(forecast_dt).date() < today: continue values = forecast["values"] From 54ff6ddd416458a5c859c81984ec07eeade48c44 Mon Sep 17 00:00:00 2001 From: Igor Loborec Date: Mon, 6 Jun 2022 03:27:46 +0100 Subject: [PATCH 1304/3516] Remove available property from Kodi (#73103) --- homeassistant/components/kodi/media_player.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index e19ffc6219c..2b509ed0e08 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -636,11 +636,6 @@ class KodiEntity(MediaPlayerEntity): return None - @property - def available(self): - """Return True if entity is available.""" - return not self._connect_error - async def async_turn_on(self): """Turn the media player on.""" _LOGGER.debug("Firing event to turn on device") From 2f3232f087b16b548f9825f2d0aef3cff21cbc4b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Jun 2022 12:18:57 -0700 Subject: [PATCH 1305/3516] Bumped version to 2022.6.3 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 23ada7591ee..4287c7e96f8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 08bd61e7382..a16b51c03d8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.2 +version = 2022.6.3 url = https://www.home-assistant.io/ [options] From 983a76a91ce5ae2ada276886bb5876530761d09d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 6 Jun 2022 21:43:47 +0200 Subject: [PATCH 1306/3516] Update pylint to 2.14.0 (#73119) --- homeassistant/components/hassio/http.py | 2 ++ homeassistant/components/hassio/websocket_api.py | 2 ++ homeassistant/components/sentry/__init__.py | 1 - homeassistant/helpers/update_coordinator.py | 2 +- pylint/plugins/hass_constructor.py | 5 +---- pylint/plugins/hass_enforce_type_hints.py | 7 ++----- pylint/plugins/hass_imports.py | 7 ++----- pylint/plugins/hass_logger.py | 7 ++----- pyproject.toml | 4 +--- requirements_test.txt | 2 +- 10 files changed, 14 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 63ac1521cc5..497e246ea77 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -28,6 +28,7 @@ _LOGGER = logging.getLogger(__name__) MAX_UPLOAD_SIZE = 1024 * 1024 * 1024 +# pylint: disable=implicit-str-concat NO_TIMEOUT = re.compile( r"^(?:" r"|homeassistant/update" @@ -48,6 +49,7 @@ NO_AUTH = re.compile( ) NO_STORE = re.compile(r"^(?:" r"|app/entrypoint.js" r")$") +# pylint: enable=implicit-str-concat class HassIOView(HomeAssistantView): diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index 4af090d7154..7eb037d8432 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -38,9 +38,11 @@ SCHEMA_WEBSOCKET_EVENT = vol.Schema( ) # Endpoints needed for ingress can't require admin because addons can set `panel_admin: false` +# pylint: disable=implicit-str-concat WS_NO_ADMIN_ENDPOINTS = re.compile( r"^(?:" r"|/ingress/(session|validate_session)" r"|/addons/[^/]+/info" r")$" ) +# pylint: enable=implicit-str-concat _LOGGER: logging.Logger = logging.getLogger(__package__) diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py index 3037f1dc374..092358e82f6 100644 --- a/homeassistant/components/sentry/__init__.py +++ b/homeassistant/components/sentry/__init__.py @@ -80,7 +80,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ), } - # pylint: disable-next=abstract-class-instantiated sentry_sdk.init( dsn=entry.data[CONF_DSN], environment=entry.options.get(CONF_ENVIRONMENT), diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index f671e1b973a..fc619469500 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable, Generator from datetime import datetime, timedelta import logging from time import monotonic -from typing import Any, Generic, TypeVar # pylint: disable=unused-import +from typing import Any, Generic, TypeVar import urllib.error import aiohttp diff --git a/pylint/plugins/hass_constructor.py b/pylint/plugins/hass_constructor.py index 525dcfed2e2..23496b68de3 100644 --- a/pylint/plugins/hass_constructor.py +++ b/pylint/plugins/hass_constructor.py @@ -3,19 +3,16 @@ from __future__ import annotations from astroid import nodes from pylint.checkers import BaseChecker -from pylint.interfaces import IAstroidChecker from pylint.lint import PyLinter class HassConstructorFormatChecker(BaseChecker): # type: ignore[misc] """Checker for __init__ definitions.""" - __implements__ = IAstroidChecker - name = "hass_constructor" priority = -1 msgs = { - "W0006": ( + "W7411": ( '__init__ should have explicit return type "None"', "hass-constructor-return", "Used when __init__ has all arguments typed " diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 05a52faab17..d8d3c76a028 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -6,7 +6,6 @@ import re from astroid import nodes from pylint.checkers import BaseChecker -from pylint.interfaces import IAstroidChecker from pylint.lint import PyLinter from homeassistant.const import Platform @@ -540,17 +539,15 @@ def _get_module_platform(module_name: str) -> str | None: class HassTypeHintChecker(BaseChecker): # type: ignore[misc] """Checker for setup type hints.""" - __implements__ = IAstroidChecker - name = "hass_enforce_type_hints" priority = -1 msgs = { - "W0020": ( + "W7431": ( "Argument %d should be of type %s", "hass-argument-type", "Used when method argument type is incorrect", ), - "W0021": ( + "W7432": ( "Return type should be %s", "hass-return-type", "Used when method return type is incorrect", diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index a8b3fa8fc76..cc160c1cfbd 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -6,7 +6,6 @@ import re from astroid import nodes from pylint.checkers import BaseChecker -from pylint.interfaces import IAstroidChecker from pylint.lint import PyLinter @@ -233,17 +232,15 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] """Checker for imports.""" - __implements__ = IAstroidChecker - name = "hass_imports" priority = -1 msgs = { - "W0011": ( + "W7421": ( "Relative import should be used", "hass-relative-import", "Used when absolute import should be replaced with relative import", ), - "W0012": ( + "W7422": ( "%s is deprecated, %s", "hass-deprecated-import", "Used when import is deprecated", diff --git a/pylint/plugins/hass_logger.py b/pylint/plugins/hass_logger.py index 125c927ec42..0135720a792 100644 --- a/pylint/plugins/hass_logger.py +++ b/pylint/plugins/hass_logger.py @@ -3,7 +3,6 @@ from __future__ import annotations from astroid import nodes from pylint.checkers import BaseChecker -from pylint.interfaces import IAstroidChecker from pylint.lint import PyLinter LOGGER_NAMES = ("LOGGER", "_LOGGER") @@ -13,17 +12,15 @@ LOG_LEVEL_ALLOWED_LOWER_START = ("debug",) class HassLoggerFormatChecker(BaseChecker): # type: ignore[misc] """Checker for logger invocations.""" - __implements__ = IAstroidChecker - name = "hass_logger" priority = -1 msgs = { - "W0001": ( + "W7401": ( "User visible logger messages must not end with a period", "hass-logger-period", "Periods are not permitted at the end of logger messages", ), - "W0002": ( + "W7402": ( "User visible logger messages must start with a capital letter or downgrade to debug", "hass-logger-capital", "All logger messages must start with a capital letter", diff --git a/pyproject.toml b/pyproject.toml index cc745f58ad6..cf5ac7a37c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,7 +87,7 @@ forced_separate = [ ] combine_as_imports = true -[tool.pylint.MASTER] +[tool.pylint.MAIN] py-version = "3.9" ignore = [ "tests", @@ -152,7 +152,6 @@ good-names = [ # too-many-ancestors - it's too strict. # wrong-import-order - isort guards this # consider-using-f-string - str.format sometimes more readable -# no-self-use - little added value with too many false-positives # --- # Enable once current issues are fixed: # consider-using-namedtuple-or-dataclass (Pylint CodeStyle extension) @@ -179,7 +178,6 @@ disable = [ "unused-argument", "wrong-import-order", "consider-using-f-string", - "no-self-use", "consider-using-namedtuple-or-dataclass", "consider-using-assignment-expr", ] diff --git a/requirements_test.txt b/requirements_test.txt index e888d715cd4..406ce96d50d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ freezegun==1.2.1 mock-open==1.4.0 mypy==0.960 pre-commit==2.19.0 -pylint==2.13.9 +pylint==2.14.0 pipdeptree==2.2.1 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 From 861de5c0f0761306412e073f4c65cfe922ffdc01 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Jun 2022 12:49:15 -0700 Subject: [PATCH 1307/3516] Point iAlarm XR at PyPI fork (#73143) --- homeassistant/components/ialarm_xr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ialarm_xr/manifest.json b/homeassistant/components/ialarm_xr/manifest.json index f863f360242..5befca3b95d 100644 --- a/homeassistant/components/ialarm_xr/manifest.json +++ b/homeassistant/components/ialarm_xr/manifest.json @@ -2,7 +2,7 @@ "domain": "ialarm_xr", "name": "Antifurto365 iAlarmXR", "documentation": "https://www.home-assistant.io/integrations/ialarm_xr", - "requirements": ["pyialarmxr==1.0.18"], + "requirements": ["pyialarmxr-homeassistant==1.0.18"], "codeowners": ["@bigmoby"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 2bea1a28b50..4c72de261e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1553,7 +1553,7 @@ pyhomeworks==0.0.6 pyialarm==1.9.0 # homeassistant.components.ialarm_xr -pyialarmxr==1.0.18 +pyialarmxr-homeassistant==1.0.18 # homeassistant.components.icloud pyicloud==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 15242eaad94..3a300ea3f78 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1041,7 +1041,7 @@ pyhomematic==0.1.77 pyialarm==1.9.0 # homeassistant.components.ialarm_xr -pyialarmxr==1.0.18 +pyialarmxr-homeassistant==1.0.18 # homeassistant.components.icloud pyicloud==1.0.0 From 33f282af46697df1b90cda1ac449a87c522edb81 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Jun 2022 12:49:15 -0700 Subject: [PATCH 1308/3516] Point iAlarm XR at PyPI fork (#73143) --- homeassistant/components/ialarm_xr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ialarm_xr/manifest.json b/homeassistant/components/ialarm_xr/manifest.json index f863f360242..5befca3b95d 100644 --- a/homeassistant/components/ialarm_xr/manifest.json +++ b/homeassistant/components/ialarm_xr/manifest.json @@ -2,7 +2,7 @@ "domain": "ialarm_xr", "name": "Antifurto365 iAlarmXR", "documentation": "https://www.home-assistant.io/integrations/ialarm_xr", - "requirements": ["pyialarmxr==1.0.18"], + "requirements": ["pyialarmxr-homeassistant==1.0.18"], "codeowners": ["@bigmoby"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 27667d6b0fe..e99b2ce353b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1550,7 +1550,7 @@ pyhomeworks==0.0.6 pyialarm==1.9.0 # homeassistant.components.ialarm_xr -pyialarmxr==1.0.18 +pyialarmxr-homeassistant==1.0.18 # homeassistant.components.icloud pyicloud==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6867d9d84c0..3dfb583ad2e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1038,7 +1038,7 @@ pyhomematic==0.1.77 pyialarm==1.9.0 # homeassistant.components.ialarm_xr -pyialarmxr==1.0.18 +pyialarmxr-homeassistant==1.0.18 # homeassistant.components.icloud pyicloud==1.0.0 From de2e9b6d77adb7f86c6ec4aa0a50428ec8606dc3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Jun 2022 09:50:52 -1000 Subject: [PATCH 1309/3516] Fix state_changes_during_period history query when no entities are passed (#73139) --- homeassistant/components/recorder/history.py | 17 ++++++------ tests/components/recorder/test_history.py | 29 ++++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 5dd5c0d3040..37285f66d1d 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -352,7 +352,8 @@ def _state_changed_during_period_stmt( ) if end_time: stmt += lambda q: q.filter(States.last_updated < end_time) - stmt += lambda q: q.filter(States.entity_id == entity_id) + if entity_id: + stmt += lambda q: q.filter(States.entity_id == entity_id) if join_attributes: stmt += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id @@ -378,6 +379,7 @@ def state_changes_during_period( ) -> MutableMapping[str, list[State]]: """Return states changes during UTC period start_time - end_time.""" entity_id = entity_id.lower() if entity_id is not None else None + entity_ids = [entity_id] if entity_id is not None else None with session_scope(hass=hass) as session: stmt = _state_changed_during_period_stmt( @@ -392,8 +394,6 @@ def state_changes_during_period( states = execute_stmt_lambda_element( session, stmt, None if entity_id else start_time, end_time ) - entity_ids = [entity_id] if entity_id is not None else None - return cast( MutableMapping[str, list[State]], _sorted_states_to_dict( @@ -408,14 +408,16 @@ def state_changes_during_period( def _get_last_state_changes_stmt( - schema_version: int, number_of_states: int, entity_id: str + schema_version: int, number_of_states: int, entity_id: str | None ) -> StatementLambdaElement: stmt, join_attributes = lambda_stmt_and_join_attributes( schema_version, False, include_last_changed=False ) stmt += lambda q: q.filter( (States.last_changed == States.last_updated) | States.last_changed.is_(None) - ).filter(States.entity_id == entity_id) + ) + if entity_id: + stmt += lambda q: q.filter(States.entity_id == entity_id) if join_attributes: stmt += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id @@ -427,19 +429,18 @@ def _get_last_state_changes_stmt( def get_last_state_changes( - hass: HomeAssistant, number_of_states: int, entity_id: str + hass: HomeAssistant, number_of_states: int, entity_id: str | None ) -> MutableMapping[str, list[State]]: """Return the last number_of_states.""" start_time = dt_util.utcnow() entity_id = entity_id.lower() if entity_id is not None else None + entity_ids = [entity_id] if entity_id is not None else None with session_scope(hass=hass) as session: stmt = _get_last_state_changes_stmt( _schema_version(hass), number_of_states, entity_id ) states = list(execute_stmt_lambda_element(session, stmt)) - entity_ids = [entity_id] if entity_id is not None else None - return cast( MutableMapping[str, list[State]], _sorted_states_to_dict( diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index da6c3a8af35..ee02ffbec49 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -878,3 +878,32 @@ async def test_get_full_significant_states_handles_empty_last_changed( assert db_sensor_one_states[0].last_updated is not None assert db_sensor_one_states[1].last_updated is not None assert db_sensor_one_states[0].last_updated != db_sensor_one_states[1].last_updated + + +def test_state_changes_during_period_multiple_entities_single_test(hass_recorder): + """Test state change during period with multiple entities in the same test. + + This test ensures the sqlalchemy query cache does not + generate incorrect results. + """ + hass = hass_recorder() + start = dt_util.utcnow() + test_entites = {f"sensor.{i}": str(i) for i in range(30)} + for entity_id, value in test_entites.items(): + hass.states.set(entity_id, value) + + wait_recording_done(hass) + end = dt_util.utcnow() + + hist = history.state_changes_during_period(hass, start, end, None) + for entity_id, value in test_entites.items(): + hist[entity_id][0].state == value + + for entity_id, value in test_entites.items(): + hist = history.state_changes_during_period(hass, start, end, entity_id) + assert len(hist) == 1 + hist[entity_id][0].state == value + + hist = history.state_changes_during_period(hass, start, end, None) + for entity_id, value in test_entites.items(): + hist[entity_id][0].state == value From a886c6110d920f7b78a35aac64ccd6b0ad6d0a02 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Jun 2022 09:50:52 -1000 Subject: [PATCH 1310/3516] Fix state_changes_during_period history query when no entities are passed (#73139) --- homeassistant/components/recorder/history.py | 17 ++++++------ tests/components/recorder/test_history.py | 29 ++++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 5dd5c0d3040..37285f66d1d 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -352,7 +352,8 @@ def _state_changed_during_period_stmt( ) if end_time: stmt += lambda q: q.filter(States.last_updated < end_time) - stmt += lambda q: q.filter(States.entity_id == entity_id) + if entity_id: + stmt += lambda q: q.filter(States.entity_id == entity_id) if join_attributes: stmt += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id @@ -378,6 +379,7 @@ def state_changes_during_period( ) -> MutableMapping[str, list[State]]: """Return states changes during UTC period start_time - end_time.""" entity_id = entity_id.lower() if entity_id is not None else None + entity_ids = [entity_id] if entity_id is not None else None with session_scope(hass=hass) as session: stmt = _state_changed_during_period_stmt( @@ -392,8 +394,6 @@ def state_changes_during_period( states = execute_stmt_lambda_element( session, stmt, None if entity_id else start_time, end_time ) - entity_ids = [entity_id] if entity_id is not None else None - return cast( MutableMapping[str, list[State]], _sorted_states_to_dict( @@ -408,14 +408,16 @@ def state_changes_during_period( def _get_last_state_changes_stmt( - schema_version: int, number_of_states: int, entity_id: str + schema_version: int, number_of_states: int, entity_id: str | None ) -> StatementLambdaElement: stmt, join_attributes = lambda_stmt_and_join_attributes( schema_version, False, include_last_changed=False ) stmt += lambda q: q.filter( (States.last_changed == States.last_updated) | States.last_changed.is_(None) - ).filter(States.entity_id == entity_id) + ) + if entity_id: + stmt += lambda q: q.filter(States.entity_id == entity_id) if join_attributes: stmt += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id @@ -427,19 +429,18 @@ def _get_last_state_changes_stmt( def get_last_state_changes( - hass: HomeAssistant, number_of_states: int, entity_id: str + hass: HomeAssistant, number_of_states: int, entity_id: str | None ) -> MutableMapping[str, list[State]]: """Return the last number_of_states.""" start_time = dt_util.utcnow() entity_id = entity_id.lower() if entity_id is not None else None + entity_ids = [entity_id] if entity_id is not None else None with session_scope(hass=hass) as session: stmt = _get_last_state_changes_stmt( _schema_version(hass), number_of_states, entity_id ) states = list(execute_stmt_lambda_element(session, stmt)) - entity_ids = [entity_id] if entity_id is not None else None - return cast( MutableMapping[str, list[State]], _sorted_states_to_dict( diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index da6c3a8af35..ee02ffbec49 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -878,3 +878,32 @@ async def test_get_full_significant_states_handles_empty_last_changed( assert db_sensor_one_states[0].last_updated is not None assert db_sensor_one_states[1].last_updated is not None assert db_sensor_one_states[0].last_updated != db_sensor_one_states[1].last_updated + + +def test_state_changes_during_period_multiple_entities_single_test(hass_recorder): + """Test state change during period with multiple entities in the same test. + + This test ensures the sqlalchemy query cache does not + generate incorrect results. + """ + hass = hass_recorder() + start = dt_util.utcnow() + test_entites = {f"sensor.{i}": str(i) for i in range(30)} + for entity_id, value in test_entites.items(): + hass.states.set(entity_id, value) + + wait_recording_done(hass) + end = dt_util.utcnow() + + hist = history.state_changes_during_period(hass, start, end, None) + for entity_id, value in test_entites.items(): + hist[entity_id][0].state == value + + for entity_id, value in test_entites.items(): + hist = history.state_changes_during_period(hass, start, end, entity_id) + assert len(hist) == 1 + hist[entity_id][0].state == value + + hist = history.state_changes_during_period(hass, start, end, None) + for entity_id, value in test_entites.items(): + hist[entity_id][0].state == value From caed0a486f21821bde0176cc03d8280293dabe05 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 6 Jun 2022 22:03:52 +0200 Subject: [PATCH 1311/3516] Update mypy to 0.961 (#73142) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 406ce96d50d..afef38074ee 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,7 +11,7 @@ codecov==2.1.12 coverage==6.4 freezegun==1.2.1 mock-open==1.4.0 -mypy==0.960 +mypy==0.961 pre-commit==2.19.0 pylint==2.14.0 pipdeptree==2.2.1 From 4678466560b7490e8fcfc3206c0db59dd7c4b074 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Jun 2022 20:59:00 -1000 Subject: [PATCH 1312/3516] Remove unused code from logbook (#72950) --- homeassistant/components/logbook/processor.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index 595268e5582..82225df8364 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -386,12 +386,6 @@ def _rows_match(row: Row | EventAsRow, other_row: Row | EventAsRow) -> bool: return False -def _row_event_data_extract(row: Row | EventAsRow, extractor: re.Pattern) -> str | None: - """Extract from event_data row.""" - result = extractor.search(row.shared_data or row.event_data or "") - return result.group(1) if result else None - - def _row_time_fired_isoformat(row: Row | EventAsRow) -> str: """Convert the row timed_fired to isoformat.""" return process_timestamp_to_utc_isoformat(row.time_fired or dt_util.utcnow()) From dbd3ca5ecd8ee0119f6225751f9abf0efc2f0cb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 6 Jun 2022 22:19:15 +0200 Subject: [PATCH 1313/3516] airzone: update aioairzone to v0.4.5 (#73127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index e0d3bd6df09..e189ae741ad 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -3,7 +3,7 @@ "name": "Airzone", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airzone", - "requirements": ["aioairzone==0.4.4"], + "requirements": ["aioairzone==0.4.5"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioairzone"] diff --git a/requirements_all.txt b/requirements_all.txt index 4c72de261e1..296febf7519 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -107,7 +107,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.4 +aioairzone==0.4.5 # homeassistant.components.ambient_station aioambient==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a300ea3f78..b3ad7f54c7b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -94,7 +94,7 @@ aio_geojson_nsw_rfs_incidents==0.4 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.4 +aioairzone==0.4.5 # homeassistant.components.ambient_station aioambient==2021.11.0 From 6c9408aef5cc53852e9ec8077e383cb6f17877c8 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 6 Jun 2022 23:46:52 +0200 Subject: [PATCH 1314/3516] Bump async-upnp-client==0.31.1 (#73135) Co-authored-by: J. Nick Koston --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/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 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 15beea714da..cc72f5d4778 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.30.1"], + "requirements": ["async-upnp-client==0.31.1"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index 21329440788..590d1b8370a 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dms", - "requirements": ["async-upnp-client==0.30.1"], + "requirements": ["async-upnp-client==0.31.1"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index fd97eb12e54..ce65af7d8bb 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -7,7 +7,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.5.0", "wakeonlan==2.0.1", - "async-upnp-client==0.30.1" + "async-upnp-client==0.31.1" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 5cbd3d0d10e..f0db05d9015 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.30.1"], + "requirements": ["async-upnp-client==0.31.1"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 2e76dac4adb..dc87e73fdee 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.30.1", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.31.1", "getmac==0.8.2"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman", "@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 5ec224498be..57d32f315ba 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.10", "async-upnp-client==0.30.1"], + "requirements": ["yeelight==0.7.10", "async-upnp-client==0.31.1"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 076b58b5185..bec79680e0d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.11 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.30.1 +async-upnp-client==0.31.1 async_timeout==4.0.2 atomicwrites==1.4.0 attrs==21.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 296febf7519..c968971cb54 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -339,7 +339,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.30.1 +async-upnp-client==0.31.1 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3ad7f54c7b..47f0c9336c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -281,7 +281,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.30.1 +async-upnp-client==0.31.1 # homeassistant.components.sleepiq asyncsleepiq==1.2.3 From 4f75de2345030786ee5e9849e6219241209be8ec Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 6 Jun 2022 17:18:07 -0500 Subject: [PATCH 1315/3516] Fix errors when unjoining multiple Sonos devices simultaneously (#73133) --- .../components/sonos/media_player.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index f331f980bb4..938a651c34d 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -751,17 +751,23 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): media_content_type, ) - def join_players(self, group_members): + async def async_join_players(self, group_members): """Join `group_members` as a player group with the current player.""" - speakers = [] - for entity_id in group_members: - if speaker := self.hass.data[DATA_SONOS].entity_id_mappings.get(entity_id): - speakers.append(speaker) - else: - raise HomeAssistantError(f"Not a known Sonos entity_id: {entity_id}") + async with self.hass.data[DATA_SONOS].topology_condition: + speakers = [] + for entity_id in group_members: + if speaker := self.hass.data[DATA_SONOS].entity_id_mappings.get( + entity_id + ): + speakers.append(speaker) + else: + raise HomeAssistantError( + f"Not a known Sonos entity_id: {entity_id}" + ) - self.speaker.join(speakers) + await self.hass.async_add_executor_job(self.speaker.join, speakers) - def unjoin_player(self): + async def async_unjoin_player(self): """Remove this player from any group.""" - self.speaker.unjoin() + async with self.hass.data[DATA_SONOS].topology_condition: + await self.hass.async_add_executor_job(self.speaker.unjoin) From ca54eaf40dbe08e407e4ae46f9786cdeb558b15f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 7 Jun 2022 11:02:08 +1200 Subject: [PATCH 1316/3516] Fix KeyError from ESPHome media players on startup (#73149) --- .../components/esphome/media_player.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index f9027142ae2..d7ce73976e7 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -25,7 +25,12 @@ from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import EsphomeEntity, EsphomeEnumMapper, platform_async_setup_entry +from . import ( + EsphomeEntity, + EsphomeEnumMapper, + esphome_state_property, + platform_async_setup_entry, +) async def async_setup_entry( @@ -54,6 +59,10 @@ _STATES: EsphomeEnumMapper[MediaPlayerState, str] = EsphomeEnumMapper( ) +# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property +# pylint: disable=invalid-overridden-method + + class EsphomeMediaPlayer( EsphomeEntity[MediaPlayerInfo, MediaPlayerEntityState], MediaPlayerEntity ): @@ -61,17 +70,17 @@ class EsphomeMediaPlayer( _attr_device_class = MediaPlayerDeviceClass.SPEAKER - @property + @esphome_state_property def state(self) -> str | None: """Return current state.""" return _STATES.from_esphome(self._state.state) - @property + @esphome_state_property def is_volume_muted(self) -> bool: """Return true if volume is muted.""" return self._state.muted - @property + @esphome_state_property def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" return self._state.volume From 05e5dd7baf07ece1e0deae603a4f10c21e508fde Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 7 Jun 2022 00:20:09 +0000 Subject: [PATCH 1317/3516] [ci skip] Translation update --- .../airvisual/translations/sensor.he.json | 3 + .../components/emonitor/translations/fr.json | 2 +- .../components/flux_led/translations/fr.json | 2 +- .../components/harmony/translations/fr.json | 2 +- .../humidifier/translations/he.json | 2 +- .../translations/fr.json | 2 +- .../components/lookin/translations/fr.json | 2 +- .../components/plugwise/translations/hu.json | 4 +- .../components/plugwise/translations/pl.json | 10 ++- .../radiotherm/translations/ca.json | 22 ++++++ .../radiotherm/translations/de.json | 31 ++++++++ .../radiotherm/translations/el.json | 31 ++++++++ .../radiotherm/translations/fr.json | 31 ++++++++ .../radiotherm/translations/hu.json | 31 ++++++++ .../radiotherm/translations/id.json | 31 ++++++++ .../radiotherm/translations/it.json | 31 ++++++++ .../radiotherm/translations/nl.json | 22 ++++++ .../radiotherm/translations/pl.json | 31 ++++++++ .../radiotherm/translations/pt-BR.json | 31 ++++++++ .../radiotherm/translations/zh-Hant.json | 31 ++++++++ .../components/scrape/translations/id.json | 42 +++++++++-- .../components/scrape/translations/it.json | 73 +++++++++++++++++++ .../components/scrape/translations/nl.json | 6 +- .../components/scrape/translations/pl.json | 73 +++++++++++++++++++ .../components/skybell/translations/hu.json | 21 ++++++ .../components/skybell/translations/id.json | 21 ++++++ .../components/skybell/translations/it.json | 21 ++++++ .../components/skybell/translations/pl.json | 21 ++++++ .../synology_dsm/translations/fr.json | 2 +- .../tankerkoenig/translations/it.json | 8 +- .../tankerkoenig/translations/pl.json | 8 +- .../components/tplink/translations/fr.json | 2 +- .../components/twinkly/translations/fr.json | 2 +- .../components/zha/translations/fr.json | 2 +- 34 files changed, 630 insertions(+), 24 deletions(-) create mode 100644 homeassistant/components/radiotherm/translations/ca.json create mode 100644 homeassistant/components/radiotherm/translations/de.json create mode 100644 homeassistant/components/radiotherm/translations/el.json create mode 100644 homeassistant/components/radiotherm/translations/fr.json create mode 100644 homeassistant/components/radiotherm/translations/hu.json create mode 100644 homeassistant/components/radiotherm/translations/id.json create mode 100644 homeassistant/components/radiotherm/translations/it.json create mode 100644 homeassistant/components/radiotherm/translations/nl.json create mode 100644 homeassistant/components/radiotherm/translations/pl.json create mode 100644 homeassistant/components/radiotherm/translations/pt-BR.json create mode 100644 homeassistant/components/radiotherm/translations/zh-Hant.json create mode 100644 homeassistant/components/scrape/translations/it.json create mode 100644 homeassistant/components/scrape/translations/pl.json create mode 100644 homeassistant/components/skybell/translations/hu.json create mode 100644 homeassistant/components/skybell/translations/id.json create mode 100644 homeassistant/components/skybell/translations/it.json create mode 100644 homeassistant/components/skybell/translations/pl.json diff --git a/homeassistant/components/airvisual/translations/sensor.he.json b/homeassistant/components/airvisual/translations/sensor.he.json index 7ed68fa47ca..5745fb051f6 100644 --- a/homeassistant/components/airvisual/translations/sensor.he.json +++ b/homeassistant/components/airvisual/translations/sensor.he.json @@ -1,5 +1,8 @@ { "state": { + "airvisual__pollutant_label": { + "p1": "PM10" + }, "airvisual__pollutant_level": { "good": "\u05d8\u05d5\u05d1", "unhealthy": "\u05dc\u05d0 \u05d1\u05e8\u05d9\u05d0", diff --git a/homeassistant/components/emonitor/translations/fr.json b/homeassistant/components/emonitor/translations/fr.json index aaacc8bf140..ce6070be8b8 100644 --- a/homeassistant/components/emonitor/translations/fr.json +++ b/homeassistant/components/emonitor/translations/fr.json @@ -10,7 +10,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Voulez-vous configurer {name} ( {host} )?", + "description": "Voulez-vous configurer {name} ({host})\u00a0?", "title": "Configurer SiteSage Emonitor" }, "user": { diff --git a/homeassistant/components/flux_led/translations/fr.json b/homeassistant/components/flux_led/translations/fr.json index baea6899999..691c470c6aa 100644 --- a/homeassistant/components/flux_led/translations/fr.json +++ b/homeassistant/components/flux_led/translations/fr.json @@ -11,7 +11,7 @@ "flow_title": "{model} {id} ( {ipaddr} )", "step": { "discovery_confirm": { - "description": "Voulez-vous configurer {model} {id} ( {ipaddr} )\u00a0?" + "description": "Voulez-vous configurer {model} {id} ({ipaddr})\u00a0?" }, "user": { "data": { diff --git a/homeassistant/components/harmony/translations/fr.json b/homeassistant/components/harmony/translations/fr.json index 077405be95f..9c9b8a7b7e1 100644 --- a/homeassistant/components/harmony/translations/fr.json +++ b/homeassistant/components/harmony/translations/fr.json @@ -10,7 +10,7 @@ "flow_title": "{name}", "step": { "link": { - "description": "Voulez-vous configurer {name} ( {host} ) ?", + "description": "Voulez-vous configurer {name} ({host})\u00a0?", "title": "Configuration de Logitech Harmony Hub" }, "user": { diff --git a/homeassistant/components/humidifier/translations/he.json b/homeassistant/components/humidifier/translations/he.json index 5a4c58c7934..4cd7b4d8196 100644 --- a/homeassistant/components/humidifier/translations/he.json +++ b/homeassistant/components/humidifier/translations/he.json @@ -2,7 +2,7 @@ "device_automation": { "action_type": { "set_mode": "\u05e9\u05e0\u05d4 \u05de\u05e6\u05d1 \u05d1-{entity_name}", - "toggle": "\u05d4\u05d7\u05dc\u05e3 \u05d0\u05ea {entity_name}", + "toggle": "\u05d4\u05d7\u05dc\u05e4\u05ea {entity_name}", "turn_off": "\u05db\u05d1\u05d4 \u05d0\u05ea {entity_name}", "turn_on": "\u05d4\u05e4\u05e2\u05dc \u05d0\u05ea {entity_name}" }, diff --git a/homeassistant/components/hunterdouglas_powerview/translations/fr.json b/homeassistant/components/hunterdouglas_powerview/translations/fr.json index 9eb8edda7db..c6ad82dc7ab 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/fr.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/fr.json @@ -10,7 +10,7 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "Voulez-vous configurer {name} ({host})?", + "description": "Voulez-vous configurer {name} ({host})\u00a0?", "title": "Connectez-vous au concentrateur PowerView" }, "user": { diff --git a/homeassistant/components/lookin/translations/fr.json b/homeassistant/components/lookin/translations/fr.json index 7276af22624..2ceb9bd6600 100644 --- a/homeassistant/components/lookin/translations/fr.json +++ b/homeassistant/components/lookin/translations/fr.json @@ -19,7 +19,7 @@ } }, "discovery_confirm": { - "description": "Voulez-vous configurer {name} ( {host} )?" + "description": "Voulez-vous configurer {name} ({host})\u00a0?" }, "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/hu.json b/homeassistant/components/plugwise/translations/hu.json index 8b9b619f728..b622109797c 100644 --- a/homeassistant/components/plugwise/translations/hu.json +++ b/homeassistant/components/plugwise/translations/hu.json @@ -19,8 +19,8 @@ "port": "Port", "username": "Smile Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Term\u00e9k:", - "title": "Plugwise t\u00edpus" + "description": "K\u00e9rem, adja meg", + "title": "Csatlakoz\u00e1s a Smile-hoz" }, "user_gateway": { "data": { diff --git a/homeassistant/components/plugwise/translations/pl.json b/homeassistant/components/plugwise/translations/pl.json index c4a2efe95c3..3d6a3a3b354 100644 --- a/homeassistant/components/plugwise/translations/pl.json +++ b/homeassistant/components/plugwise/translations/pl.json @@ -13,10 +13,14 @@ "step": { "user": { "data": { - "flow_type": "Typ po\u0142\u0105czenia" + "flow_type": "Typ po\u0142\u0105czenia", + "host": "Adres IP", + "password": "Identyfikator Smile", + "port": "Port", + "username": "Nazwa u\u017cytkownika Smile" }, - "description": "Wybierz produkt:", - "title": "Wybierz typ Plugwise" + "description": "Wprowad\u017a:", + "title": "Po\u0142\u0105czenie ze Smile" }, "user_gateway": { "data": { diff --git a/homeassistant/components/radiotherm/translations/ca.json b/homeassistant/components/radiotherm/translations/ca.json new file mode 100644 index 00000000000..1008a0e4988 --- /dev/null +++ b/homeassistant/components/radiotherm/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", + "unknown": "Error inesperat" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "Vols configurar {name} {model} ({host})?" + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/de.json b/homeassistant/components/radiotherm/translations/de.json new file mode 100644 index 00000000000..50315afce52 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/de.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "M\u00f6chtest du {name} {model} ({host}) einrichten?" + }, + "user": { + "data": { + "host": "Host" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "Stelle beim Einstellen der Temperatur eine permanente Sperre ein." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/el.json b/homeassistant/components/radiotherm/translations/el.json new file mode 100644 index 00000000000..9b276da3670 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/el.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} {model} ({host});" + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03bc\u03cc\u03bd\u03b9\u03bc\u03b7 \u03b1\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/fr.json b/homeassistant/components/radiotherm/translations/fr.json new file mode 100644 index 00000000000..47705ce5138 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/fr.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "Voulez-vous configurer {name} {model} ({host})\u00a0?" + }, + "user": { + "data": { + "host": "H\u00f4te" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "D\u00e9finissez un maintien permanent lors du r\u00e9glage de la temp\u00e9rature." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/hu.json b/homeassistant/components/radiotherm/translations/hu.json new file mode 100644 index 00000000000..f55ea666f5f --- /dev/null +++ b/homeassistant/components/radiotherm/translations/hu.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} {model} ({host})?" + }, + "user": { + "data": { + "host": "C\u00edm" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "\u00c1ll\u00edtson be \u00e1lland\u00f3 tart\u00e1st a h\u0151m\u00e9rs\u00e9klet be\u00e1ll\u00edt\u00e1sakor." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/id.json b/homeassistant/components/radiotherm/translations/id.json new file mode 100644 index 00000000000..1e454cc8cc8 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/id.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "Ingin menyiapkan {name} {model} ({host})?" + }, + "user": { + "data": { + "host": "Host" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "Atur penahan permanen saat menyesuaikan suhu." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/it.json b/homeassistant/components/radiotherm/translations/it.json new file mode 100644 index 00000000000..1fd9c7152ff --- /dev/null +++ b/homeassistant/components/radiotherm/translations/it.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "Vuoi configurare {name} {model} ({host})?" + }, + "user": { + "data": { + "host": "Host" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "Impostare una sospensione permanente durante la regolazione della temperatura." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/nl.json b/homeassistant/components/radiotherm/translations/nl.json new file mode 100644 index 00000000000..ec2d0fc6554 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "Wilt u {name} {model} ({host}) instellen?" + }, + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/pl.json b/homeassistant/components/radiotherm/translations/pl.json new file mode 100644 index 00000000000..e69568131d6 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/pl.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name} {model} ({host})?" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "Ustaw podtrzymanie podczas ustawiania temperatury." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/pt-BR.json b/homeassistant/components/radiotherm/translations/pt-BR.json new file mode 100644 index 00000000000..59b4b32d965 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/pt-BR.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "unknown": "Erro inesperado" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "Deseja configurar {name} {model} ({host})?" + }, + "user": { + "data": { + "host": "Host" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "Defina uma reten\u00e7\u00e3o permanente ao ajustar a temperatura." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/zh-Hant.json b/homeassistant/components/radiotherm/translations/zh-Hant.json new file mode 100644 index 00000000000..7a5a40817f7 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/zh-Hant.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} {model} ({host})\uff1f" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "\u8abf\u6574\u6eab\u5ea6\u6642\u8a2d\u5b9a\u6c38\u4e45\u4fdd\u6301\u3002" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/id.json b/homeassistant/components/scrape/translations/id.json index b6cecd6007c..e7761f73a1f 100644 --- a/homeassistant/components/scrape/translations/id.json +++ b/homeassistant/components/scrape/translations/id.json @@ -10,16 +10,27 @@ "authentication": "Autentikasi", "device_class": "Kelas Perangkat", "headers": "Header", + "index": "Indeks", + "name": "Nama", "password": "Kata Sandi", - "resource": "Sumber daya", + "resource": "Sumber Daya", "select": "Pilihan", + "state_class": "Kelas Status", "unit_of_measurement": "Satuan Pengukuran", - "value_template": "T" + "username": "Nama Pengguna", + "value_template": "Nilai Templat", + "verify_ssl": "Verifikasi sertifikat SSL" }, "data_description": { "attribute": "Dapatkan nilai atribut pada tag yang dipilih", "authentication": "Jenis autentikasi HTTP. Salah satu dari basic atau digest", "device_class": "Jenis/kelas sensor untuk mengatur ikon di antarmuka", + "headers": "Header yang digunakan untuk permintaan web", + "index": "Menentukan elemen mana yang dikembalikan oleh selektor CSS untuk digunakan", + "resource": "URL ke situs web yang mengandung nilai", + "select": "Menentukan tag yang harus dicari. Periksa selektor CSS Beautifulsoup untuk melihat detailnya", + "state_class": "Nilai state_class dari sensor", + "value_template": "Mendefinisikan templat untuk mendapatkan status sensor", "verify_ssl": "Mengaktifkan/menonaktifkan verifikasi sertifikat SSL/TLS, misalnya jika sertifikat ditandatangani sendiri" } } @@ -29,11 +40,32 @@ "step": { "init": { "data": { - "resource": "Sumber daya", - "unit_of_measurement": "Satuan Pengukuran" + "attribute": "Atribut", + "authentication": "Autentikasi", + "device_class": "Kelas Perangkat", + "headers": "Header", + "index": "Indeks", + "name": "Nama", + "password": "Kata Sandi", + "resource": "Sumber Daya", + "select": "Pilihan", + "state_class": "Kelas Status", + "unit_of_measurement": "Satuan Pengukuran", + "username": "Nama Pengguna", + "value_template": "Nilai Templat", + "verify_ssl": "Verifikasi sertifikat SSL" }, "data_description": { - "attribute": "Dapatkan nilai atribut pada tag yang dipilih" + "attribute": "Dapatkan nilai atribut pada tag yang dipilih", + "authentication": "Jenis autentikasi HTTP. Salah satu dari basic atau digest", + "device_class": "Jenis/kelas sensor untuk mengatur ikon di antarmuka", + "headers": "Header yang digunakan untuk permintaan web", + "index": "Menentukan elemen mana yang dikembalikan oleh selektor CSS untuk digunakan", + "resource": "URL ke situs web yang mengandung nilai", + "select": "Menentukan tag yang harus dicari. Periksa selektor CSS Beautifulsoup untuk melihat detailnya", + "state_class": "Nilai state_class dari sensor", + "value_template": "Mendefinisikan templat untuk mendapatkan status sensor", + "verify_ssl": "Mengaktifkan/menonaktifkan verifikasi sertifikat SSL/TLS, misalnya jika sertifikat ditandatangani sendiri" } } } diff --git a/homeassistant/components/scrape/translations/it.json b/homeassistant/components/scrape/translations/it.json new file mode 100644 index 00000000000..5fd8428335f --- /dev/null +++ b/homeassistant/components/scrape/translations/it.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato" + }, + "step": { + "user": { + "data": { + "attribute": "Attributo", + "authentication": "Autenticazione", + "device_class": "Classe del dispositivo", + "headers": "Intestazioni", + "index": "Indice", + "name": "Nome", + "password": "Password", + "resource": "Risorsa", + "select": "Seleziona", + "state_class": "Classe di Stato", + "unit_of_measurement": "Unit\u00e0 di misura", + "username": "Nome utente", + "value_template": "Modello di valore", + "verify_ssl": "Verifica il certificato SSL" + }, + "data_description": { + "attribute": "Ottieni il valore di un attributo sull'etichetta selezionata", + "authentication": "Tipo di autenticazione HTTP. Base o digest", + "device_class": "Il tipo/classe del sensore per impostare l'icona nel frontend", + "headers": "Intestazioni da utilizzare per la richiesta web", + "index": "Definisce quale degli elementi restituiti dal selettore CSS utilizzare", + "resource": "L'URL del sito Web che contiene il valore", + "select": "Definisce quale etichetta cercare. Controlla i selettori CSS di Beautifulsoup per i dettagli", + "state_class": "La state_class del sensore", + "value_template": "Definisce un modello per ottenere lo stato del sensore", + "verify_ssl": "Abilita/disabilita la verifica del certificato SSL/TLS, ad esempio se \u00e8 autofirmato" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "attribute": "Attributo", + "authentication": "Autenticazione", + "device_class": "Classe del dispositivo", + "headers": "Intestazioni", + "index": "Indice", + "name": "Nome", + "password": "Password", + "resource": "Risorsa", + "select": "Seleziona", + "state_class": "Classe di Stato", + "unit_of_measurement": "Unit\u00e0 di misura", + "username": "Nome utente", + "value_template": "Modello di valore", + "verify_ssl": "Verifica il certificato SSL" + }, + "data_description": { + "attribute": "Ottieni il valore di un attributo sull'etichetta selezionata", + "authentication": "Tipo di autenticazione HTTP. Base o digest", + "device_class": "Il tipo/classe del sensore per impostare l'icona nel frontend", + "headers": "Intestazioni da utilizzare per la richiesta web", + "index": "Definisce quale degli elementi restituiti dal selettore CSS utilizzare", + "resource": "L'URL del sito Web che contiene il valore", + "select": "Definisce quale etichetta cercare. Controlla i selettori CSS di Beautifulsoup per i dettagli", + "state_class": "La state_class del sensore", + "value_template": "Definisce un modello per ottenere lo stato del sensore", + "verify_ssl": "Abilita/disabilita la verifica del certificato SSL/TLS, ad esempio se \u00e8 autofirmato" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/nl.json b/homeassistant/components/scrape/translations/nl.json index bdc26e94182..81d41d6ff26 100644 --- a/homeassistant/components/scrape/translations/nl.json +++ b/homeassistant/components/scrape/translations/nl.json @@ -18,7 +18,8 @@ "state_class": "Staatklasse", "unit_of_measurement": "Meeteenheid", "username": "Gebruikersnaam", - "value_template": "Waardetemplate" + "value_template": "Waardetemplate", + "verify_ssl": "SSL-certificaat verifi\u00ebren" }, "data_description": { "attribute": "Haal de waarde op van een attribuut op de geselecteerde tag", @@ -44,7 +45,8 @@ "state_class": "Staatklasse", "unit_of_measurement": "Meeteenheid", "username": "Gebruikersnaam", - "value_template": "Waardetemplate" + "value_template": "Waardetemplate", + "verify_ssl": "SSL-certificaat verifi\u00ebren" }, "data_description": { "attribute": "Haal de waarde op van een attribuut op de geselecteerde tag", diff --git a/homeassistant/components/scrape/translations/pl.json b/homeassistant/components/scrape/translations/pl.json new file mode 100644 index 00000000000..67b2a3db685 --- /dev/null +++ b/homeassistant/components/scrape/translations/pl.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane" + }, + "step": { + "user": { + "data": { + "attribute": "Atrybut", + "authentication": "Uwierzytelnianie", + "device_class": "Klasa urz\u0105dzenia", + "headers": "Nag\u0142\u00f3wki", + "index": "Indeks", + "name": "Nazwa", + "password": "Has\u0142o", + "resource": "Zas\u00f3b", + "select": "Wybierz", + "state_class": "Klasa stanu", + "unit_of_measurement": "Jednostka miary", + "username": "Nazwa u\u017cytkownika", + "value_template": "Szablon warto\u015bci", + "verify_ssl": "Weryfikacja certyfikatu SSL" + }, + "data_description": { + "attribute": "Pobierz warto\u015b\u0107 atrybutu w wybranym tagu", + "authentication": "Typ uwierzytelniania HTTP. Podstawowy lub digest.", + "device_class": "Typ/klasa sensora do ustawienia ikony w interfejsie u\u017cytkownika", + "headers": "Nag\u0142\u00f3wki do u\u017cycia w \u017c\u0105daniu internetowym", + "index": "Okre\u015bla, kt\u00f3rego z element\u00f3w zwracanych przez selektor CSS nale\u017cy u\u017cy\u0107", + "resource": "Adres URL strony internetowej zawieraj\u0105cej t\u0105 warto\u015b\u0107", + "select": "Okre\u015bla jakiego taga szuka\u0107. Sprawd\u017a selektory CSS Beautifulsoup, aby uzyska\u0107 szczeg\u00f3\u0142owe informacje.", + "state_class": "state_class sensora", + "value_template": "Szablon, kt\u00f3ry pozwala uzyska\u0107 stan czujnika", + "verify_ssl": "W\u0142\u0105cza/wy\u0142\u0105cza weryfikacj\u0119 certyfikatu SSL/TLS, na przyk\u0142ad, je\u015bli jest on samopodpisany." + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "attribute": "Atrybut", + "authentication": "Uwierzytelnianie", + "device_class": "Klasa urz\u0105dzenia", + "headers": "Nag\u0142\u00f3wki", + "index": "Indeks", + "name": "Nazwa", + "password": "Has\u0142o", + "resource": "Zas\u00f3b", + "select": "Wybierz", + "state_class": "Klasa stanu", + "unit_of_measurement": "Jednostka miary", + "username": "Nazwa u\u017cytkownika", + "value_template": "Szablon warto\u015bci", + "verify_ssl": "Weryfikacja certyfikatu SSL" + }, + "data_description": { + "attribute": "Pobierz warto\u015b\u0107 atrybutu w wybranym tagu", + "authentication": "Typ uwierzytelniania HTTP. Podstawowy lub digest.", + "device_class": "Typ/klasa sensora do ustawienia ikony w interfejsie u\u017cytkownika", + "headers": "Nag\u0142\u00f3wki do u\u017cycia w \u017c\u0105daniu internetowym", + "index": "Okre\u015bla, kt\u00f3rego z element\u00f3w zwracanych przez selektor CSS nale\u017cy u\u017cy\u0107", + "resource": "Adres URL strony internetowej zawieraj\u0105cej t\u0105 warto\u015b\u0107", + "select": "Okre\u015bla jakiego taga szuka\u0107. Sprawd\u017a selektory CSS Beautifulsoup, aby uzyska\u0107 szczeg\u00f3\u0142owe informacje.", + "state_class": "state_class sensora", + "value_template": "Szablon, kt\u00f3ry pozwala uzyska\u0107 stan czujnika", + "verify_ssl": "W\u0142\u0105cza/wy\u0142\u0105cza weryfikacj\u0119 certyfikatu SSL/TLS, na przyk\u0142ad, je\u015bli jest on samopodpisany." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/hu.json b/homeassistant/components/skybell/translations/hu.json new file mode 100644 index 00000000000..98b9ee3f016 --- /dev/null +++ b/homeassistant/components/skybell/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "A fi\u00f3k 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": { + "email": "E-mail", + "password": "Jelsz\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/id.json b/homeassistant/components/skybell/translations/id.json new file mode 100644 index 00000000000..d59082eb80b --- /dev/null +++ b/homeassistant/components/skybell/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "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/skybell/translations/it.json b/homeassistant/components/skybell/translations/it.json new file mode 100644 index 00000000000..39d4856dbe4 --- /dev/null +++ b/homeassistant/components/skybell/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \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": { + "email": "Email", + "password": "Password" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/pl.json b/homeassistant/components/skybell/translations/pl.json new file mode 100644 index 00000000000..32e23d406ab --- /dev/null +++ b/homeassistant/components/skybell/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "email": "Adres e-mail", + "password": "Has\u0142o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/fr.json b/homeassistant/components/synology_dsm/translations/fr.json index 3917d9f800e..1ad2f52199c 100644 --- a/homeassistant/components/synology_dsm/translations/fr.json +++ b/homeassistant/components/synology_dsm/translations/fr.json @@ -28,7 +28,7 @@ "username": "Nom d'utilisateur", "verify_ssl": "V\u00e9rifier le certificat SSL" }, - "description": "Voulez-vous configurer {name} ({host})?" + "description": "Voulez-vous configurer {name} ({host})\u00a0?" }, "reauth_confirm": { "data": { diff --git a/homeassistant/components/tankerkoenig/translations/it.json b/homeassistant/components/tankerkoenig/translations/it.json index 4b4cbf6d390..b98598d10a3 100644 --- a/homeassistant/components/tankerkoenig/translations/it.json +++ b/homeassistant/components/tankerkoenig/translations/it.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "La posizione \u00e8 gi\u00e0 configurata" + "already_configured": "La posizione \u00e8 gi\u00e0 configurata", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "invalid_auth": "Autenticazione non valida", "no_stations": "Impossibile trovare nessuna stazione nel raggio d'azione." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Chiave API" + } + }, "select_station": { "data": { "stations": "Stazioni" diff --git a/homeassistant/components/tankerkoenig/translations/pl.json b/homeassistant/components/tankerkoenig/translations/pl.json index 282c87cf3c8..288b4f8aae7 100644 --- a/homeassistant/components/tankerkoenig/translations/pl.json +++ b/homeassistant/components/tankerkoenig/translations/pl.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "invalid_auth": "Niepoprawne uwierzytelnienie", "no_stations": "Nie mo\u017cna znale\u017a\u0107 \u017cadnej stacji w zasi\u0119gu." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Klucz API" + } + }, "select_station": { "data": { "stations": "Stacje" diff --git a/homeassistant/components/tplink/translations/fr.json b/homeassistant/components/tplink/translations/fr.json index e1105ea00e0..75b15dbacef 100644 --- a/homeassistant/components/tplink/translations/fr.json +++ b/homeassistant/components/tplink/translations/fr.json @@ -10,7 +10,7 @@ "flow_title": "{name} {model} ( {host} )", "step": { "discovery_confirm": { - "description": "Voulez-vous configurer {name} {model} ( {host} )\u00a0?" + "description": "Voulez-vous configurer {name} {model} ({host})\u00a0?" }, "pick_device": { "data": { diff --git a/homeassistant/components/twinkly/translations/fr.json b/homeassistant/components/twinkly/translations/fr.json index cf6fe97ce88..d076be1f399 100644 --- a/homeassistant/components/twinkly/translations/fr.json +++ b/homeassistant/components/twinkly/translations/fr.json @@ -8,7 +8,7 @@ }, "step": { "discovery_confirm": { - "description": "Voulez-vous configurer {name} - {model} ( {host} )\u00a0?" + "description": "Voulez-vous configurer {name} - {model} ({host})\u00a0?" }, "user": { "data": { diff --git a/homeassistant/components/zha/translations/fr.json b/homeassistant/components/zha/translations/fr.json index 1044b726b27..f69e4fb36ff 100644 --- a/homeassistant/components/zha/translations/fr.json +++ b/homeassistant/components/zha/translations/fr.json @@ -11,7 +11,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Voulez-vous configurer {name} ?" + "description": "Voulez-vous configurer {name}\u00a0?" }, "pick_radio": { "data": { From 16bf6903bd2b0fd4a70c961edb929a9a796eb5f6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 7 Jun 2022 00:07:49 -0400 Subject: [PATCH 1318/3516] Bump pyeight to 0.3.0 (#73151) --- homeassistant/components/eight_sleep/__init__.py | 14 ++++++++------ .../components/eight_sleep/binary_sensor.py | 2 +- homeassistant/components/eight_sleep/manifest.json | 2 +- homeassistant/components/eight_sleep/sensor.py | 8 ++++---- requirements_all.txt | 2 +- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index e986a7f6d60..1bf22defd74 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -63,9 +63,10 @@ CONFIG_SCHEMA = vol.Schema( def _get_device_unique_id(eight: EightSleep, user_obj: EightUser | None = None) -> str: """Get the device's unique ID.""" - unique_id = eight.deviceid + unique_id = eight.device_id + assert unique_id if user_obj: - unique_id = f"{unique_id}.{user_obj.userid}.{user_obj.side}" + unique_id = f"{unique_id}.{user_obj.user_id}.{user_obj.side}" return unique_id @@ -133,9 +134,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: for sens in sensor: side = sens.split("_")[1] - userid = eight.fetch_userid(side) - usrobj = eight.users[userid] - await usrobj.set_heating_level(target, duration) + user_id = eight.fetch_user_id(side) + assert user_id + usr_obj = eight.users[user_id] + await usr_obj.set_heating_level(target, duration) await heat_coordinator.async_request_refresh() @@ -163,7 +165,7 @@ class EightSleepBaseEntity(CoordinatorEntity[DataUpdateCoordinator]): self._user_id = user_id self._sensor = sensor self._user_obj: EightUser | None = None - if self._user_id: + if user_id: self._user_obj = self._eight.users[user_id] mapped_name = NAME_MAP.get(sensor, sensor.replace("_", " ").title()) diff --git a/homeassistant/components/eight_sleep/binary_sensor.py b/homeassistant/components/eight_sleep/binary_sensor.py index 868a5177cfe..94ec423390f 100644 --- a/homeassistant/components/eight_sleep/binary_sensor.py +++ b/homeassistant/components/eight_sleep/binary_sensor.py @@ -36,7 +36,7 @@ async def async_setup_platform( entities = [] for user in eight.users.values(): entities.append( - EightHeatSensor(heat_coordinator, eight, user.userid, "bed_presence") + EightHeatSensor(heat_coordinator, eight, user.user_id, "bed_presence") ) async_add_entities(entities) diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index e4c5a1e0029..e83b2977b77 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -2,7 +2,7 @@ "domain": "eight_sleep", "name": "Eight Sleep", "documentation": "https://www.home-assistant.io/integrations/eight_sleep", - "requirements": ["pyeight==0.2.0"], + "requirements": ["pyeight==0.3.0"], "codeowners": ["@mezz64", "@raman325"], "iot_class": "cloud_polling", "loggers": ["pyeight"] diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index b405617e276..b2afa496149 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -73,11 +73,11 @@ async def async_setup_platform( for obj in eight.users.values(): for sensor in EIGHT_USER_SENSORS: all_sensors.append( - EightUserSensor(user_coordinator, eight, obj.userid, sensor) + EightUserSensor(user_coordinator, eight, obj.user_id, sensor) ) for sensor in EIGHT_HEAT_SENSORS: all_sensors.append( - EightHeatSensor(heat_coordinator, eight, obj.userid, sensor) + EightHeatSensor(heat_coordinator, eight, obj.user_id, sensor) ) for sensor in EIGHT_ROOM_SENSORS: all_sensors.append(EightRoomSensor(user_coordinator, eight, sensor)) @@ -109,7 +109,7 @@ class EightHeatSensor(EightSleepBaseEntity, SensorEntity): ) @property - def native_value(self) -> int: + def native_value(self) -> int | None: """Return the state of the sensor.""" assert self._user_obj return self._user_obj.heating_level @@ -270,4 +270,4 @@ class EightRoomSensor(EightSleepBaseEntity, SensorEntity): @property def native_value(self) -> int | float | None: """Return the state of the sensor.""" - return self._eight.room_temperature() + return self._eight.room_temperature diff --git a/requirements_all.txt b/requirements_all.txt index c968971cb54..6ae6cdbaa1a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1468,7 +1468,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.eight_sleep -pyeight==0.2.0 +pyeight==0.3.0 # homeassistant.components.emby pyemby==1.8 From 0c21bf7c25ffe758c25b54bf495cc2e3048bc926 Mon Sep 17 00:00:00 2001 From: BigMoby Date: Tue, 7 Jun 2022 07:23:10 +0200 Subject: [PATCH 1319/3516] Remove iAlarm XR integration (#73083) * fixing after MartinHjelmare review * fixing after MartinHjelmare review conversion alarm state to hass state * fixing after MartinHjelmare review conversion alarm state to hass state * manage the status in the alarm control * simplyfing return function * Removing iAlarm XR integration because of Antifurto365 explicit request to remove after some issues in their cloud service --- .coveragerc | 1 - .strict-typing | 1 - CODEOWNERS | 2 - .../components/ialarm_xr/__init__.py | 101 ----------- .../ialarm_xr/alarm_control_panel.py | 79 --------- .../components/ialarm_xr/config_flow.py | 94 ---------- homeassistant/components/ialarm_xr/const.py | 3 - .../components/ialarm_xr/manifest.json | 10 -- .../components/ialarm_xr/strings.json | 22 --- .../components/ialarm_xr/translations/bg.json | 21 --- .../components/ialarm_xr/translations/ca.json | 22 --- .../components/ialarm_xr/translations/de.json | 22 --- .../components/ialarm_xr/translations/el.json | 22 --- .../components/ialarm_xr/translations/en.json | 22 --- .../components/ialarm_xr/translations/es.json | 22 --- .../components/ialarm_xr/translations/et.json | 22 --- .../components/ialarm_xr/translations/fr.json | 22 --- .../components/ialarm_xr/translations/he.json | 21 --- .../components/ialarm_xr/translations/hu.json | 22 --- .../components/ialarm_xr/translations/id.json | 22 --- .../components/ialarm_xr/translations/it.json | 22 --- .../components/ialarm_xr/translations/ja.json | 22 --- .../components/ialarm_xr/translations/nl.json | 22 --- .../components/ialarm_xr/translations/no.json | 22 --- .../components/ialarm_xr/translations/pl.json | 22 --- .../ialarm_xr/translations/pt-BR.json | 22 --- .../components/ialarm_xr/translations/tr.json | 21 --- .../ialarm_xr/translations/zh-Hant.json | 22 --- homeassistant/components/ialarm_xr/utils.py | 18 -- homeassistant/generated/config_flows.py | 1 - mypy.ini | 11 -- requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/ialarm_xr/__init__.py | 1 - .../components/ialarm_xr/test_config_flow.py | 167 ------------------ tests/components/ialarm_xr/test_init.py | 110 ------------ 36 files changed, 1042 deletions(-) delete mode 100644 homeassistant/components/ialarm_xr/__init__.py delete mode 100644 homeassistant/components/ialarm_xr/alarm_control_panel.py delete mode 100644 homeassistant/components/ialarm_xr/config_flow.py delete mode 100644 homeassistant/components/ialarm_xr/const.py delete mode 100644 homeassistant/components/ialarm_xr/manifest.json delete mode 100644 homeassistant/components/ialarm_xr/strings.json delete mode 100644 homeassistant/components/ialarm_xr/translations/bg.json delete mode 100644 homeassistant/components/ialarm_xr/translations/ca.json delete mode 100644 homeassistant/components/ialarm_xr/translations/de.json delete mode 100644 homeassistant/components/ialarm_xr/translations/el.json delete mode 100644 homeassistant/components/ialarm_xr/translations/en.json delete mode 100644 homeassistant/components/ialarm_xr/translations/es.json delete mode 100644 homeassistant/components/ialarm_xr/translations/et.json delete mode 100644 homeassistant/components/ialarm_xr/translations/fr.json delete mode 100644 homeassistant/components/ialarm_xr/translations/he.json delete mode 100644 homeassistant/components/ialarm_xr/translations/hu.json delete mode 100644 homeassistant/components/ialarm_xr/translations/id.json delete mode 100644 homeassistant/components/ialarm_xr/translations/it.json delete mode 100644 homeassistant/components/ialarm_xr/translations/ja.json delete mode 100644 homeassistant/components/ialarm_xr/translations/nl.json delete mode 100644 homeassistant/components/ialarm_xr/translations/no.json delete mode 100644 homeassistant/components/ialarm_xr/translations/pl.json delete mode 100644 homeassistant/components/ialarm_xr/translations/pt-BR.json delete mode 100644 homeassistant/components/ialarm_xr/translations/tr.json delete mode 100644 homeassistant/components/ialarm_xr/translations/zh-Hant.json delete mode 100644 homeassistant/components/ialarm_xr/utils.py delete mode 100644 tests/components/ialarm_xr/__init__.py delete mode 100644 tests/components/ialarm_xr/test_config_flow.py delete mode 100644 tests/components/ialarm_xr/test_init.py diff --git a/.coveragerc b/.coveragerc index 75ed4663869..5fa747267a5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -518,7 +518,6 @@ omit = homeassistant/components/hvv_departures/__init__.py homeassistant/components/hydrawise/* homeassistant/components/ialarm/alarm_control_panel.py - homeassistant/components/ialarm_xr/alarm_control_panel.py homeassistant/components/iammeter/sensor.py homeassistant/components/iaqualink/binary_sensor.py homeassistant/components/iaqualink/climate.py diff --git a/.strict-typing b/.strict-typing index a2c6cd2d9da..264b28408f9 100644 --- a/.strict-typing +++ b/.strict-typing @@ -128,7 +128,6 @@ homeassistant.components.homewizard.* homeassistant.components.http.* homeassistant.components.huawei_lte.* homeassistant.components.hyperion.* -homeassistant.components.ialarm_xr.* homeassistant.components.image_processing.* homeassistant.components.input_button.* homeassistant.components.input_select.* diff --git a/CODEOWNERS b/CODEOWNERS index 6db9d92ebb9..9d0ba851339 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -475,8 +475,6 @@ build.json @home-assistant/supervisor /tests/components/hyperion/ @dermotduffy /homeassistant/components/ialarm/ @RyuzakiKK /tests/components/ialarm/ @RyuzakiKK -/homeassistant/components/ialarm_xr/ @bigmoby -/tests/components/ialarm_xr/ @bigmoby /homeassistant/components/iammeter/ @lewei50 /homeassistant/components/iaqualink/ @flz /tests/components/iaqualink/ @flz diff --git a/homeassistant/components/ialarm_xr/__init__.py b/homeassistant/components/ialarm_xr/__init__.py deleted file mode 100644 index 193bbe4fffc..00000000000 --- a/homeassistant/components/ialarm_xr/__init__.py +++ /dev/null @@ -1,101 +0,0 @@ -"""iAlarmXR integration.""" -from __future__ import annotations - -import asyncio -import logging - -from async_timeout import timeout -from pyialarmxr import ( - IAlarmXR, - IAlarmXRGenericException, - IAlarmXRSocketTimeoutException, -) - -from homeassistant.components.alarm_control_panel import SCAN_INTERVAL -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - Platform, -) -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed - -from .const import DOMAIN -from .utils import async_get_ialarmxr_mac - -PLATFORMS = [Platform.ALARM_CONTROL_PANEL] -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up iAlarmXR config.""" - host = entry.data[CONF_HOST] - port = entry.data[CONF_PORT] - username = entry.data[CONF_USERNAME] - password = entry.data[CONF_PASSWORD] - - ialarmxr = IAlarmXR(username, password, host, port) - - try: - async with timeout(10): - ialarmxr_mac = await async_get_ialarmxr_mac(hass, ialarmxr) - except ( - asyncio.TimeoutError, - ConnectionError, - IAlarmXRGenericException, - IAlarmXRSocketTimeoutException, - ) as ex: - raise ConfigEntryNotReady from ex - - coordinator = IAlarmXRDataUpdateCoordinator(hass, ialarmxr, ialarmxr_mac) - await coordinator.async_config_entry_first_refresh() - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - - hass.config_entries.async_setup_platforms(entry, PLATFORMS) - - return True - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload iAlarmXR config.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok - - -class IAlarmXRDataUpdateCoordinator(DataUpdateCoordinator): - """Class to manage fetching iAlarmXR data.""" - - def __init__(self, hass: HomeAssistant, ialarmxr: IAlarmXR, mac: str) -> None: - """Initialize global iAlarm data updater.""" - self.ialarmxr: IAlarmXR = ialarmxr - self.state: int | None = None - self.host: str = ialarmxr.host - self.mac: str = mac - - super().__init__( - hass, - _LOGGER, - name=DOMAIN, - update_interval=SCAN_INTERVAL, - ) - - def _update_data(self) -> None: - """Fetch data from iAlarmXR via sync functions.""" - status: int = self.ialarmxr.get_status() - _LOGGER.debug("iAlarmXR status: %s", status) - - self.state = status - - async def _async_update_data(self) -> None: - """Fetch data from iAlarmXR.""" - try: - async with timeout(10): - await self.hass.async_add_executor_job(self._update_data) - except ConnectionError as error: - raise UpdateFailed(error) from error diff --git a/homeassistant/components/ialarm_xr/alarm_control_panel.py b/homeassistant/components/ialarm_xr/alarm_control_panel.py deleted file mode 100644 index b64edb74391..00000000000 --- a/homeassistant/components/ialarm_xr/alarm_control_panel.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Interfaces with iAlarmXR control panels.""" -from __future__ import annotations - -from pyialarmxr import IAlarmXR - -from homeassistant.components.alarm_control_panel import ( - AlarmControlPanelEntity, - AlarmControlPanelEntityFeature, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from . import IAlarmXRDataUpdateCoordinator -from .const import DOMAIN - -IALARMXR_TO_HASS = { - IAlarmXR.ARMED_AWAY: STATE_ALARM_ARMED_AWAY, - IAlarmXR.ARMED_STAY: STATE_ALARM_ARMED_HOME, - IAlarmXR.DISARMED: STATE_ALARM_DISARMED, - IAlarmXR.TRIGGERED: STATE_ALARM_TRIGGERED, -} - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up a iAlarmXR alarm control panel based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([IAlarmXRPanel(coordinator)]) - - -class IAlarmXRPanel( - CoordinatorEntity[IAlarmXRDataUpdateCoordinator], AlarmControlPanelEntity -): - """Representation of an iAlarmXR device.""" - - _attr_supported_features = ( - AlarmControlPanelEntityFeature.ARM_HOME - | AlarmControlPanelEntityFeature.ARM_AWAY - ) - _attr_name = "iAlarm_XR" - _attr_icon = "mdi:security" - - def __init__(self, coordinator: IAlarmXRDataUpdateCoordinator) -> None: - """Initialize the alarm panel.""" - super().__init__(coordinator) - self._attr_unique_id = coordinator.mac - self._attr_device_info = DeviceInfo( - manufacturer="Antifurto365 - Meian", - name=self.name, - connections={(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)}, - ) - - @property - def state(self) -> str | None: - """Return the state of the device.""" - return IALARMXR_TO_HASS.get(self.coordinator.state) - - def alarm_disarm(self, code: str | None = None) -> None: - """Send disarm command.""" - self.coordinator.ialarmxr.disarm() - - def alarm_arm_home(self, code: str | None = None) -> None: - """Send arm home command.""" - self.coordinator.ialarmxr.arm_stay() - - def alarm_arm_away(self, code: str | None = None) -> None: - """Send arm away command.""" - self.coordinator.ialarmxr.arm_away() diff --git a/homeassistant/components/ialarm_xr/config_flow.py b/homeassistant/components/ialarm_xr/config_flow.py deleted file mode 100644 index 2a9cc406733..00000000000 --- a/homeassistant/components/ialarm_xr/config_flow.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Config flow for Antifurto365 iAlarmXR integration.""" -from __future__ import annotations - -import logging -from logging import Logger -from typing import Any - -from pyialarmxr import ( - IAlarmXR, - IAlarmXRGenericException, - IAlarmXRSocketTimeoutException, -) -import voluptuous as vol - -from homeassistant import config_entries, core -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME -from homeassistant.data_entry_flow import FlowResult - -from .const import DOMAIN -from .utils import async_get_ialarmxr_mac - -_LOGGER: Logger = logging.getLogger(__name__) - -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_HOST, default=IAlarmXR.IALARM_P2P_DEFAULT_HOST): str, - vol.Required(CONF_PORT, default=IAlarmXR.IALARM_P2P_DEFAULT_PORT): int, - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - } -) - - -async def _async_get_device_formatted_mac( - hass: core.HomeAssistant, username: str, password: str, host: str, port: int -) -> str: - """Return iAlarmXR mac address.""" - - ialarmxr = IAlarmXR(username, password, host, port) - return await async_get_ialarmxr_mac(hass, ialarmxr) - - -class IAlarmConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for Antifurto365 iAlarmXR.""" - - VERSION = 1 - - 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: - mac = None - host = user_input[CONF_HOST] - port = user_input[CONF_PORT] - username = user_input[CONF_USERNAME] - password = user_input[CONF_PASSWORD] - - try: - # If we are able to get the MAC address, we are able to establish - # a connection to the device. - mac = await _async_get_device_formatted_mac( - self.hass, username, password, host, port - ) - except ConnectionError: - errors["base"] = "cannot_connect" - except IAlarmXRGenericException as ialarmxr_exception: - _LOGGER.debug( - "IAlarmXRGenericException with message: [ %s ]", - ialarmxr_exception.message, - ) - errors["base"] = "cannot_connect" - except IAlarmXRSocketTimeoutException as ialarmxr_socket_timeout_exception: - _LOGGER.debug( - "IAlarmXRSocketTimeoutException with message: [ %s ]", - ialarmxr_socket_timeout_exception.message, - ) - errors["base"] = "timeout" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - - if not errors: - await self.async_set_unique_id(mac) - self._abort_if_unique_id_configured() - return self.async_create_entry( - title=user_input[CONF_HOST], data=user_input - ) - return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors - ) diff --git a/homeassistant/components/ialarm_xr/const.py b/homeassistant/components/ialarm_xr/const.py deleted file mode 100644 index 12122277340..00000000000 --- a/homeassistant/components/ialarm_xr/const.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Constants for the iAlarmXR integration.""" - -DOMAIN = "ialarm_xr" diff --git a/homeassistant/components/ialarm_xr/manifest.json b/homeassistant/components/ialarm_xr/manifest.json deleted file mode 100644 index 5befca3b95d..00000000000 --- a/homeassistant/components/ialarm_xr/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "ialarm_xr", - "name": "Antifurto365 iAlarmXR", - "documentation": "https://www.home-assistant.io/integrations/ialarm_xr", - "requirements": ["pyialarmxr-homeassistant==1.0.18"], - "codeowners": ["@bigmoby"], - "config_flow": true, - "iot_class": "cloud_polling", - "loggers": ["pyialarmxr"] -} diff --git a/homeassistant/components/ialarm_xr/strings.json b/homeassistant/components/ialarm_xr/strings.json deleted file mode 100644 index ea4f91fdbb9..00000000000 --- a/homeassistant/components/ialarm_xr/strings.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "step": { - "user": { - "data": { - "host": "[%key:common::config_flow::data::host%]", - "port": "[%key:common::config_flow::data::port%]", - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" - } - } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "timeout": "[%key:common::config_flow::error::timeout_connect%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" - } - } -} diff --git a/homeassistant/components/ialarm_xr/translations/bg.json b/homeassistant/components/ialarm_xr/translations/bg.json deleted file mode 100644 index 2189a9653ed..00000000000 --- a/homeassistant/components/ialarm_xr/translations/bg.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "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\u0448\u043d\u043e \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", - "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" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/ca.json b/homeassistant/components/ialarm_xr/translations/ca.json deleted file mode 100644 index 957004fc152..00000000000 --- a/homeassistant/components/ialarm_xr/translations/ca.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "El dispositiu ja est\u00e0 configurat" - }, - "error": { - "cannot_connect": "Ha fallat la connexi\u00f3", - "timeout": "Temps m\u00e0xim d'espera per establir la connexi\u00f3 esgotat", - "unknown": "Error inesperat" - }, - "step": { - "user": { - "data": { - "host": "Amfitri\u00f3", - "password": "Contrasenya", - "port": "Port", - "username": "Nom d'usuari" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/de.json b/homeassistant/components/ialarm_xr/translations/de.json deleted file mode 100644 index 32b35294072..00000000000 --- a/homeassistant/components/ialarm_xr/translations/de.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" - }, - "error": { - "cannot_connect": "Verbindung fehlgeschlagen", - "timeout": "Zeit\u00fcberschreitung beim Verbindungsaufbau", - "unknown": "Unerwarteter Fehler" - }, - "step": { - "user": { - "data": { - "host": "Host", - "password": "Passwort", - "port": "Port", - "username": "Benutzername" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/el.json b/homeassistant/components/ialarm_xr/translations/el.json deleted file mode 100644 index acc75012a67..00000000000 --- a/homeassistant/components/ialarm_xr/translations/el.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" - }, - "error": { - "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" - }, - "step": { - "user": { - "data": { - "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", - "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "port": "\u0398\u03cd\u03c1\u03b1", - "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/en.json b/homeassistant/components/ialarm_xr/translations/en.json deleted file mode 100644 index be59a5a1dc4..00000000000 --- a/homeassistant/components/ialarm_xr/translations/en.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Device is already configured" - }, - "error": { - "cannot_connect": "Failed to connect", - "timeout": "Timeout establishing connection", - "unknown": "Unexpected error" - }, - "step": { - "user": { - "data": { - "host": "Host", - "password": "Password", - "port": "Port", - "username": "Username" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/es.json b/homeassistant/components/ialarm_xr/translations/es.json deleted file mode 100644 index 41cc88c7e6f..00000000000 --- a/homeassistant/components/ialarm_xr/translations/es.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" - }, - "error": { - "cannot_connect": "Fallo en la conexi\u00f3n", - "timeout": "Tiempo de espera para establecer la conexi\u00f3n", - "unknown": "Error inesperado" - }, - "step": { - "user": { - "data": { - "host": "Anfitri\u00f3n", - "password": "Contrase\u00f1a", - "port": "Puerto", - "username": "Nombre de usuario" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/et.json b/homeassistant/components/ialarm_xr/translations/et.json deleted file mode 100644 index 3679dd47f2e..00000000000 --- a/homeassistant/components/ialarm_xr/translations/et.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Seade on juba h\u00e4\u00e4lestatud" - }, - "error": { - "cannot_connect": "\u00dchendamine nurjus", - "timeout": "\u00dchenduse ajal\u00f5pp", - "unknown": "Ootamatu t\u00f5rge" - }, - "step": { - "user": { - "data": { - "host": "Host", - "password": "Salas\u00f5na", - "port": "Port", - "username": "Kasutajanimi" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/fr.json b/homeassistant/components/ialarm_xr/translations/fr.json deleted file mode 100644 index 2ade23c9f4e..00000000000 --- a/homeassistant/components/ialarm_xr/translations/fr.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" - }, - "error": { - "cannot_connect": "\u00c9chec de connexion", - "timeout": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9", - "unknown": "Erreur inattendue" - }, - "step": { - "user": { - "data": { - "host": "H\u00f4te", - "password": "Mot de passe", - "port": "Port", - "username": "Nom d'utilisateur" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/he.json b/homeassistant/components/ialarm_xr/translations/he.json deleted file mode 100644 index b3fb785d55e..00000000000 --- a/homeassistant/components/ialarm_xr/translations/he.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "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", - "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" - }, - "step": { - "user": { - "data": { - "host": "\u05de\u05d0\u05e8\u05d7", - "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "port": "\u05e4\u05ea\u05d7\u05d4", - "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/hu.json b/homeassistant/components/ialarm_xr/translations/hu.json deleted file mode 100644 index 6f72253aae6..00000000000 --- a/homeassistant/components/ialarm_xr/translations/hu.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" - }, - "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" - }, - "step": { - "user": { - "data": { - "host": "C\u00edm", - "password": "Jelsz\u00f3", - "port": "Port", - "username": "Felhaszn\u00e1l\u00f3n\u00e9v" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/id.json b/homeassistant/components/ialarm_xr/translations/id.json deleted file mode 100644 index e4688af2b37..00000000000 --- a/homeassistant/components/ialarm_xr/translations/id.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" - }, - "error": { - "cannot_connect": "Gagal terhubung", - "timeout": "Tenggang waktu membuat koneksi habis", - "unknown": "Kesalahan yang tidak diharapkan" - }, - "step": { - "user": { - "data": { - "host": "Host", - "password": "Kata Sandi", - "port": "Port", - "username": "Nama Pengguna" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/it.json b/homeassistant/components/ialarm_xr/translations/it.json deleted file mode 100644 index ae8437aaa86..00000000000 --- a/homeassistant/components/ialarm_xr/translations/it.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" - }, - "error": { - "cannot_connect": "Impossibile connettersi", - "timeout": "Tempo scaduto per stabile la connessione.", - "unknown": "Errore imprevisto" - }, - "step": { - "user": { - "data": { - "host": "Host", - "password": "Password", - "port": "Porta", - "username": "Nome utente" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/ja.json b/homeassistant/components/ialarm_xr/translations/ja.json deleted file mode 100644 index e6f384ed615..00000000000 --- a/homeassistant/components/ialarm_xr/translations/ja.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" - }, - "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "timeout": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" - }, - "step": { - "user": { - "data": { - "host": "\u30db\u30b9\u30c8", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", - "port": "\u30dd\u30fc\u30c8", - "username": "\u30e6\u30fc\u30b6\u30fc\u540d" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/nl.json b/homeassistant/components/ialarm_xr/translations/nl.json deleted file mode 100644 index a32ee7ccfbe..00000000000 --- a/homeassistant/components/ialarm_xr/translations/nl.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Apparaat is al geconfigureerd" - }, - "error": { - "cannot_connect": "Kan geen verbinding maken", - "timeout": "Time-out bij het maken van verbinding", - "unknown": "Onverwachte fout" - }, - "step": { - "user": { - "data": { - "host": "Host", - "password": "Wachtwoord", - "port": "Poort", - "username": "Gebruikersnaam" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/no.json b/homeassistant/components/ialarm_xr/translations/no.json deleted file mode 100644 index ccadf2f9972..00000000000 --- a/homeassistant/components/ialarm_xr/translations/no.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Enheten er allerede konfigurert" - }, - "error": { - "cannot_connect": "Tilkobling mislyktes", - "timeout": "Tidsavbrudd oppretter forbindelse", - "unknown": "Uventet feil" - }, - "step": { - "user": { - "data": { - "host": "Vert", - "password": "Passord", - "port": "Port", - "username": "Brukernavn" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/pl.json b/homeassistant/components/ialarm_xr/translations/pl.json deleted file mode 100644 index 3880b4a6e09..00000000000 --- a/homeassistant/components/ialarm_xr/translations/pl.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" - }, - "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "timeout": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia", - "unknown": "Nieoczekiwany b\u0142\u0105d" - }, - "step": { - "user": { - "data": { - "host": "Nazwa hosta lub adres IP", - "password": "Has\u0142o", - "port": "Port", - "username": "Nazwa u\u017cytkownika" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/pt-BR.json b/homeassistant/components/ialarm_xr/translations/pt-BR.json deleted file mode 100644 index 37dd49dc9d8..00000000000 --- a/homeassistant/components/ialarm_xr/translations/pt-BR.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" - }, - "error": { - "cannot_connect": "Falhou ao se conectar", - "timeout": "Tempo limite estabelecendo conex\u00e3o", - "unknown": "Erro inesperado" - }, - "step": { - "user": { - "data": { - "host": "Host", - "password": "Senha", - "port": "Porta", - "username": "Nome de usu\u00e1rio" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/tr.json b/homeassistant/components/ialarm_xr/translations/tr.json deleted file mode 100644 index f15e4339e3c..00000000000 --- a/homeassistant/components/ialarm_xr/translations/tr.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" - }, - "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131", - "unknown": "Beklenmeyen hata" - }, - "step": { - "user": { - "data": { - "host": "Sunucu", - "password": "Parola", - "port": "Port", - "username": "Kullan\u0131c\u0131 Ad\u0131" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/translations/zh-Hant.json b/homeassistant/components/ialarm_xr/translations/zh-Hant.json deleted file mode 100644 index b47b6268af1..00000000000 --- a/homeassistant/components/ialarm_xr/translations/zh-Hant.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" - }, - "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557", - "timeout": "\u5efa\u7acb\u9023\u7dda\u903e\u6642", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" - }, - "step": { - "user": { - "data": { - "host": "\u4e3b\u6a5f\u7aef", - "password": "\u5bc6\u78bc", - "port": "\u901a\u8a0a\u57e0", - "username": "\u4f7f\u7528\u8005\u540d\u7a31" - } - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ialarm_xr/utils.py b/homeassistant/components/ialarm_xr/utils.py deleted file mode 100644 index db82a3fcd44..00000000000 --- a/homeassistant/components/ialarm_xr/utils.py +++ /dev/null @@ -1,18 +0,0 @@ -"""iAlarmXR utils.""" -import logging - -from pyialarmxr import IAlarmXR - -from homeassistant import core -from homeassistant.helpers.device_registry import format_mac - -_LOGGER = logging.getLogger(__name__) - - -async def async_get_ialarmxr_mac(hass: core.HomeAssistant, ialarmxr: IAlarmXR) -> str: - """Retrieve iAlarmXR MAC address.""" - _LOGGER.debug("Retrieving ialarmxr mac address") - - mac = await hass.async_add_executor_job(ialarmxr.get_mac) - - return format_mac(mac) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 843e3f7f7a9..1cf8ba743bc 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -161,7 +161,6 @@ FLOWS = { "hvv_departures", "hyperion", "ialarm", - "ialarm_xr", "iaqualink", "icloud", "ifttt", diff --git a/mypy.ini b/mypy.ini index fb47638d59d..522cda67e79 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1171,17 +1171,6 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.ialarm_xr.*] -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.image_processing.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 6ae6cdbaa1a..70a29067a7b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1552,9 +1552,6 @@ pyhomeworks==0.0.6 # homeassistant.components.ialarm pyialarm==1.9.0 -# homeassistant.components.ialarm_xr -pyialarmxr-homeassistant==1.0.18 - # homeassistant.components.icloud pyicloud==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 47f0c9336c9..850a379eaf6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1040,9 +1040,6 @@ pyhomematic==0.1.77 # homeassistant.components.ialarm pyialarm==1.9.0 -# homeassistant.components.ialarm_xr -pyialarmxr-homeassistant==1.0.18 - # homeassistant.components.icloud pyicloud==1.0.0 diff --git a/tests/components/ialarm_xr/__init__.py b/tests/components/ialarm_xr/__init__.py deleted file mode 100644 index 4097867f70b..00000000000 --- a/tests/components/ialarm_xr/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Antifurto365 iAlarmXR integration.""" diff --git a/tests/components/ialarm_xr/test_config_flow.py b/tests/components/ialarm_xr/test_config_flow.py deleted file mode 100644 index 804249dd5cb..00000000000 --- a/tests/components/ialarm_xr/test_config_flow.py +++ /dev/null @@ -1,167 +0,0 @@ -"""Test the Antifurto365 iAlarmXR config flow.""" - -from unittest.mock import patch - -from pyialarmxr import IAlarmXRGenericException, IAlarmXRSocketTimeoutException - -from homeassistant import config_entries, data_entry_flow -from homeassistant.components.ialarm_xr.const import DOMAIN -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME - -from tests.common import MockConfigEntry - -TEST_DATA = { - CONF_HOST: "1.1.1.1", - CONF_PORT: 18034, - CONF_USERNAME: "000ZZZ0Z00", - CONF_PASSWORD: "00000000", -} - -TEST_MAC = "00:00:54:12:34:56" - - -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"] == data_entry_flow.RESULT_TYPE_FORM - assert result["handler"] == "ialarm_xr" - assert result["data_schema"].schema.get("host") == str - assert result["data_schema"].schema.get("port") == int - assert result["data_schema"].schema.get("password") == str - assert result["data_schema"].schema.get("username") == str - assert result["errors"] == {} - - with patch( - "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_status", - return_value=1, - ), patch( - "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", - return_value=TEST_MAC, - ), patch( - "homeassistant.components.ialarm_xr.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"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == TEST_DATA["host"] - assert result2["data"] == TEST_DATA - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_exception(hass): - """Test we handle unknown exception.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", - side_effect=Exception, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_DATA - ) - - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["errors"] == {"base": "unknown"} - - -async def test_form_cannot_connect_throwing_connection_error(hass): - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", - side_effect=ConnectionError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_DATA - ) - - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["errors"] == {"base": "cannot_connect"} - - -async def test_form_cannot_connect_throwing_socket_timeout_exception(hass): - """Test we handle cannot connect error because of socket timeout.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", - side_effect=IAlarmXRSocketTimeoutException, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_DATA - ) - - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["errors"] == {"base": "timeout"} - - -async def test_form_cannot_connect_throwing_generic_exception(hass): - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", - side_effect=IAlarmXRGenericException, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_DATA - ) - - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["errors"] == {"base": "cannot_connect"} - - -async def test_form_already_exists(hass): - """Test that a flow with an existing host aborts.""" - entry = MockConfigEntry( - domain=DOMAIN, - unique_id=TEST_MAC, - data=TEST_DATA, - ) - - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.ialarm_xr.config_flow.IAlarmXR.get_mac", - return_value=TEST_MAC, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_DATA - ) - - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result2["reason"] == "already_configured" - - -async def test_flow_user_step_no_input(hass): - """Test appropriate error when no input is provided.""" - _result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result = await hass.config_entries.flow.async_configure( - _result["flow_id"], user_input=None - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == config_entries.SOURCE_USER - assert result["errors"] == {} diff --git a/tests/components/ialarm_xr/test_init.py b/tests/components/ialarm_xr/test_init.py deleted file mode 100644 index 0898b6bebf8..00000000000 --- a/tests/components/ialarm_xr/test_init.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Test the Antifurto365 iAlarmXR init.""" -import asyncio -from datetime import timedelta -from unittest.mock import Mock, patch -from uuid import uuid4 - -import pytest - -from homeassistant.components.ialarm_xr.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME -from homeassistant.util.dt import utcnow - -from tests.common import MockConfigEntry, async_fire_time_changed - - -@pytest.fixture(name="ialarmxr_api") -def ialarmxr_api_fixture(): - """Set up IAlarmXR API fixture.""" - with patch("homeassistant.components.ialarm_xr.IAlarmXR") as mock_ialarm_api: - yield mock_ialarm_api - - -@pytest.fixture(name="mock_config_entry") -def mock_config_fixture(): - """Return a fake config entry.""" - return MockConfigEntry( - domain=DOMAIN, - data={ - CONF_HOST: "192.168.10.20", - CONF_PORT: 18034, - CONF_USERNAME: "000ZZZ0Z00", - CONF_PASSWORD: "00000000", - }, - entry_id=str(uuid4()), - ) - - -async def test_setup_entry(hass, ialarmxr_api, mock_config_entry): - """Test setup entry.""" - ialarmxr_api.return_value.get_mac = Mock(return_value="00:00:54:12:34:56") - - mock_config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - ialarmxr_api.return_value.get_mac.assert_called_once() - assert mock_config_entry.state is ConfigEntryState.LOADED - - -async def test_unload_entry(hass, ialarmxr_api, mock_config_entry): - """Test being able to unload an entry.""" - ialarmxr_api.return_value.get_mac = Mock(return_value="00:00:54:12:34:56") - - mock_config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - assert mock_config_entry.state is ConfigEntryState.LOADED - assert await hass.config_entries.async_unload(mock_config_entry.entry_id) - assert mock_config_entry.state is ConfigEntryState.NOT_LOADED - - -async def test_setup_not_ready_connection_error(hass, ialarmxr_api, mock_config_entry): - """Test setup failed because we can't connect to the alarm system.""" - ialarmxr_api.return_value.get_status = Mock(side_effect=ConnectionError) - - mock_config_entry.add_to_hass(hass) - assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - future = utcnow() + timedelta(seconds=30) - async_fire_time_changed(hass, future) - assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY - - -async def test_setup_not_ready_timeout(hass, ialarmxr_api, mock_config_entry): - """Test setup failed because we can't connect to the alarm system.""" - ialarmxr_api.return_value.get_status = Mock(side_effect=asyncio.TimeoutError) - - mock_config_entry.add_to_hass(hass) - assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - future = utcnow() + timedelta(seconds=30) - async_fire_time_changed(hass, future) - assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY - - -async def test_setup_entry_and_then_fail_on_update( - hass, ialarmxr_api, mock_config_entry -): - """Test setup entry.""" - ialarmxr_api.return_value.get_mac = Mock(return_value="00:00:54:12:34:56") - ialarmxr_api.return_value.get_status = Mock(value=ialarmxr_api.DISARMED) - - mock_config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - ialarmxr_api.return_value.get_mac.assert_called_once() - ialarmxr_api.return_value.get_status.assert_called_once() - assert mock_config_entry.state is ConfigEntryState.LOADED - - ialarmxr_api.return_value.get_status = Mock(side_effect=asyncio.TimeoutError) - future = utcnow() + timedelta(seconds=60) - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - ialarmxr_api.return_value.get_status.assert_called_once() - assert hass.states.get("alarm_control_panel.ialarm_xr").state == "unavailable" From 2e47cee72a12253ee66d5d4bd1c1f2df47b000d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Jun 2022 19:48:49 -1000 Subject: [PATCH 1320/3516] Fix setup race when config entry is in a setup retry state (#73145) --- homeassistant/config_entries.py | 6 +++ tests/test_config_entries.py | 72 ++++++++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index df633009138..bb24b24a7fb 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -90,6 +90,8 @@ class ConfigEntryState(Enum): """The config entry has not been loaded""" FAILED_UNLOAD = "failed_unload", False """An error occurred when trying to unload the entry""" + SETUP_IN_PROGRESS = "setup_in_progress", True + """The config entry is setting up.""" _recoverable: bool @@ -294,6 +296,10 @@ class ConfigEntry: if integration is None: integration = await loader.async_get_integration(hass, self.domain) + # Only store setup result as state if it was not forwarded. + if self.domain == integration.domain: + self.state = ConfigEntryState.SETUP_IN_PROGRESS + self.supports_unload = await support_entry_unload(hass, self.domain) self.supports_remove_device = await support_remove_from_device( hass, self.domain diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 09951b4f34e..dbbf542d36c 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -16,7 +16,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import CoreState, Event, callback +from homeassistant.core import CoreState, Event, HomeAssistant, callback from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, BaseServiceInfo, FlowResult from homeassistant.exceptions import ( ConfigEntryAuthFailed, @@ -3190,3 +3190,73 @@ async def test_entry_reload_concurrency(hass, manager): await asyncio.gather(*tasks) assert entry.state is config_entries.ConfigEntryState.LOADED assert loaded == 1 + + +async def test_unique_id_update_while_setup_in_progress( + hass: HomeAssistant, manager: config_entries.ConfigEntries +) -> None: + """Test we handle the case where the config entry is updated while setup is in progress.""" + + async def mock_setup_entry(hass, entry): + """Mock setting up entry.""" + await asyncio.sleep(0.1) + return True + + async def mock_unload_entry(hass, entry): + """Mock unloading an entry.""" + return True + + hass.config.components.add("comp") + entry = MockConfigEntry( + domain="comp", + data={"additional": "data", "host": "0.0.0.0"}, + unique_id="mock-unique-id", + state=config_entries.ConfigEntryState.SETUP_RETRY, + ) + entry.add_to_hass(hass) + + mock_integration( + hass, + MockModule( + "comp", + async_setup_entry=mock_setup_entry, + async_unload_entry=mock_unload_entry, + ), + ) + mock_entity_platform(hass, "config_flow.comp", None) + updates = {"host": "1.1.1.1"} + + hass.async_create_task(hass.config_entries.async_reload(entry.entry_id)) + await asyncio.sleep(0) + assert entry.state is config_entries.ConfigEntryState.SETUP_IN_PROGRESS + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Test user step.""" + await self.async_set_unique_id("mock-unique-id") + await self._abort_if_unique_id_configured( + updates=updates, reload_on_update=True + ) + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as async_reload: + result = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert entry.data["host"] == "1.1.1.1" + assert entry.data["additional"] == "data" + + # Setup is already in progress, we should not reload + # if it fails it will go into a retry state and try again + assert len(async_reload.mock_calls) == 0 + await hass.async_block_till_done() + assert entry.state is config_entries.ConfigEntryState.LOADED From a8763d74798ecc95d4a2b323235aabea5463da66 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 7 Jun 2022 07:57:41 +0200 Subject: [PATCH 1321/3516] Update pylint to 2.14.1 (#73144) --- homeassistant/components/broadlink/config_flow.py | 8 ++------ requirements_test.txt | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index da8b489a98b..8a32ba02ee8 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -188,9 +188,7 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(device.mac.hex()) _LOGGER.error( - "Failed to authenticate to the device at %s: %s", - device.host[0], - err_msg, # pylint: disable=used-before-assignment + "Failed to authenticate to the device at %s: %s", device.host[0], err_msg ) return self.async_show_form(step_id="auth", errors=errors) @@ -253,9 +251,7 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_finish() _LOGGER.error( - "Failed to unlock the device at %s: %s", - device.host[0], - err_msg, # pylint: disable=used-before-assignment + "Failed to unlock the device at %s: %s", device.host[0], err_msg ) else: diff --git a/requirements_test.txt b/requirements_test.txt index afef38074ee..7c03d2e51c2 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ freezegun==1.2.1 mock-open==1.4.0 mypy==0.961 pre-commit==2.19.0 -pylint==2.14.0 +pylint==2.14.1 pipdeptree==2.2.1 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 From 6971bb8f5bce2183dccb2e0ee7c3845a14dffd94 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 7 Jun 2022 11:15:31 +0200 Subject: [PATCH 1322/3516] Adjust config-flow type hints in vera (#72409) * Adjust config-flow type hints in vera * Reduce size of PR --- homeassistant/components/vera/config_flow.py | 22 +++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index 319dcd031d0..c300f599faa 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -14,6 +14,7 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import entity_registry as er from .const import CONF_CONTROLLER, CONF_LEGACY_UNIQUE_ID, DOMAIN @@ -37,12 +38,14 @@ def list_to_str(data: list[Any]) -> str: return " ".join([str(i) for i in data]) -def new_options(lights: list[int], exclude: list[int]) -> dict: +def new_options(lights: list[int], exclude: list[int]) -> dict[str, list[int]]: """Create a standard options object.""" return {CONF_LIGHTS: lights, CONF_EXCLUDE: exclude} -def options_schema(options: Mapping[str, Any] = None) -> dict: +def options_schema( + options: Mapping[str, Any] | None = None +) -> dict[vol.Optional, type[str]]: """Return options schema.""" options = options or {} return { @@ -57,7 +60,7 @@ def options_schema(options: Mapping[str, Any] = None) -> dict: } -def options_data(user_input: dict) -> dict: +def options_data(user_input: dict[str, str]) -> dict[str, list[int]]: """Return options dict.""" return new_options( str_to_int_list(user_input.get(CONF_LIGHTS, "")), @@ -72,7 +75,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Init object.""" self.config_entry = config_entry - async def async_step_init(self, user_input: dict = None): + async def async_step_init( + self, + user_input: dict[str, str] | None = None, + ) -> FlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry( @@ -95,7 +101,9 @@ class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow.""" return OptionsFlowHandler(config_entry) - async def async_step_user(self, user_input: dict = None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle user initiated flow.""" if user_input is not None: return await self.async_step_finish( @@ -114,7 +122,7 @@ class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ), ) - async def async_step_import(self, config: dict): + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: """Handle a flow initialized by import.""" # If there are entities with the legacy unique_id, then this imported config @@ -139,7 +147,7 @@ class VeraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): } ) - async def async_step_finish(self, config: dict): + async def async_step_finish(self, config: dict[str, Any]) -> FlowResult: """Validate and create config entry.""" base_url = config[CONF_CONTROLLER] = config[CONF_CONTROLLER].rstrip("/") controller = pv.VeraController(base_url) From ab82f71b4315d792d80eb1318ae8899350a8d3c5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 7 Jun 2022 11:30:12 +0200 Subject: [PATCH 1323/3516] Adjust config-flow type hints in xiaomi_miio (#72503) * Adjust config-flow type hints in xiaomi_miio * Use Mapping * Reduce size of PR --- .../components/xiaomi_miio/config_flow.py | 56 ++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index a78e01c7fae..e5b5275757c 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -1,6 +1,10 @@ """Config flow to configure Xiaomi Miio.""" +from __future__ import annotations + +from collections.abc import Mapping import logging from re import search +from typing import Any from micloud import MiCloud from micloud.micloudexception import MiCloudAccessDenied @@ -8,7 +12,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import zeroconf -from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_NAME, CONF_TOKEN from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult @@ -61,7 +65,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Init object.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the options.""" errors = {} if user_input is not None: @@ -105,25 +111,25 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize.""" - self.host = None - self.mac = None + self.host: str | None = None + self.mac: str | None = None self.token = None self.model = None self.name = None self.cloud_username = None self.cloud_password = None self.cloud_country = None - self.cloud_devices = {} + self.cloud_devices: dict[str, dict[str, Any]] = {} @staticmethod @callback - def async_get_options_flow(config_entry) -> OptionsFlowHandler: + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler: """Get the options flow.""" return OptionsFlowHandler(config_entry) - async def async_step_reauth(self, user_input=None): + async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an authentication error or missing cloud credentials.""" self.host = user_input[CONF_HOST] self.token = user_input[CONF_TOKEN] @@ -131,13 +137,15 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.model = user_input.get(CONF_MODEL) return await self.async_step_reauth_confirm() - async def async_step_reauth_confirm(self, user_input=None): + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Dialog that informs the user that reauth is required.""" if user_input is not None: return await self.async_step_cloud() return self.async_show_form(step_id="reauth_confirm") - async def async_step_import(self, conf: dict): + async def async_step_import(self, conf: dict[str, Any]) -> FlowResult: """Import a configuration from config.yaml.""" self.host = conf[CONF_HOST] self.token = conf[CONF_TOKEN] @@ -149,7 +157,9 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_step_connect() - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" return await self.async_step_cloud() @@ -203,7 +213,7 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_abort(reason="not_xiaomi_miio") - def extract_cloud_info(self, cloud_device_info): + def extract_cloud_info(self, cloud_device_info: dict[str, Any]) -> None: """Extract the cloud info.""" if self.host is None: self.host = cloud_device_info["localip"] @@ -215,7 +225,9 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.name = cloud_device_info["name"] self.token = cloud_device_info["token"] - async def async_step_cloud(self, user_input=None): + async def async_step_cloud( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Configure a xiaomi miio device through the Miio Cloud.""" errors = {} if user_input is not None: @@ -283,9 +295,11 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="cloud", data_schema=DEVICE_CLOUD_CONFIG, errors=errors ) - async def async_step_select(self, user_input=None): + async def async_step_select( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle multiple cloud devices found.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: cloud_device = self.cloud_devices[user_input["select_device"]] self.extract_cloud_info(cloud_device) @@ -299,9 +313,11 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="select", data_schema=select_schema, errors=errors ) - async def async_step_manual(self, user_input=None): + async def async_step_manual( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Configure a xiaomi miio device Manually.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: self.token = user_input[CONF_TOKEN] if user_input.get(CONF_HOST): @@ -316,9 +332,11 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="manual", data_schema=schema, errors=errors) - async def async_step_connect(self, user_input=None): + async def async_step_connect( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Connect to a xiaomi miio device.""" - errors = {} + errors: dict[str, str] = {} if self.host is None or self.token is None: return self.async_abort(reason="incomplete_info") From 5f2b4001f31f008ee581c2b1aa046bea3a360915 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 7 Jun 2022 14:41:43 +0200 Subject: [PATCH 1324/3516] Separate recorder database schema from other classes (#72977) * Separate recorder database schema from other classes * fix logbook imports * migrate new tests * few more * last one * fix merge Co-authored-by: J. Nick Koston --- .../components/logbook/queries/all.py | 6 +- .../components/logbook/queries/common.py | 4 +- .../components/logbook/queries/devices.py | 2 +- .../components/logbook/queries/entities.py | 2 +- .../logbook/queries/entities_and_devices.py | 2 +- homeassistant/components/recorder/core.py | 8 +- .../components/recorder/db_schema.py | 600 ++++++++++++++++++ homeassistant/components/recorder/filters.py | 2 +- homeassistant/components/recorder/history.py | 4 +- .../components/recorder/migration.py | 4 +- homeassistant/components/recorder/models.py | 592 +---------------- homeassistant/components/recorder/purge.py | 2 +- homeassistant/components/recorder/queries.py | 2 +- homeassistant/components/recorder/repack.py | 2 +- .../components/recorder/run_history.py | 3 +- .../components/recorder/statistics.py | 5 +- homeassistant/components/recorder/util.py | 5 +- tests/components/automation/test_recorder.py | 2 +- tests/components/camera/test_recorder.py | 2 +- tests/components/climate/test_recorder.py | 2 +- tests/components/fan/test_recorder.py | 2 +- tests/components/group/test_recorder.py | 2 +- tests/components/humidifier/test_recorder.py | 2 +- .../components/input_boolean/test_recorder.py | 2 +- .../components/input_button/test_recorder.py | 2 +- .../input_datetime/test_recorder.py | 2 +- .../components/input_number/test_recorder.py | 2 +- .../components/input_select/test_recorder.py | 2 +- tests/components/input_text/test_recorder.py | 2 +- tests/components/light/test_recorder.py | 2 +- .../components/media_player/test_recorder.py | 2 +- tests/components/number/test_recorder.py | 2 +- tests/components/recorder/common.py | 6 +- .../{models_schema_0.py => db_schema_0.py} | 0 .../{models_schema_16.py => db_schema_16.py} | 0 .../{models_schema_18.py => db_schema_18.py} | 0 .../{models_schema_22.py => db_schema_22.py} | 0 .../{models_schema_23.py => db_schema_23.py} | 0 ....py => db_schema_23_with_newer_columns.py} | 0 .../{models_schema_28.py => db_schema_28.py} | 0 .../test_filters_with_entityfilter.py | 2 +- tests/components/recorder/test_history.py | 5 +- tests/components/recorder/test_init.py | 4 +- tests/components/recorder/test_migrate.py | 20 +- tests/components/recorder/test_models.py | 6 +- tests/components/recorder/test_purge.py | 2 +- tests/components/recorder/test_run_history.py | 3 +- tests/components/recorder/test_statistics.py | 72 ++- .../recorder/test_statistics_v23_migration.py | 82 +-- tests/components/recorder/test_util.py | 3 +- tests/components/script/test_recorder.py | 2 +- tests/components/select/test_recorder.py | 2 +- tests/components/sensor/test_recorder.py | 8 +- tests/components/siren/test_recorder.py | 2 +- tests/components/sun/test_recorder.py | 2 +- tests/components/update/test_recorder.py | 2 +- tests/components/vacuum/test_recorder.py | 2 +- .../components/water_heater/test_recorder.py | 2 +- tests/components/weather/test_recorder.py | 2 +- 59 files changed, 771 insertions(+), 733 deletions(-) create mode 100644 homeassistant/components/recorder/db_schema.py rename tests/components/recorder/{models_schema_0.py => db_schema_0.py} (100%) rename tests/components/recorder/{models_schema_16.py => db_schema_16.py} (100%) rename tests/components/recorder/{models_schema_18.py => db_schema_18.py} (100%) rename tests/components/recorder/{models_schema_22.py => db_schema_22.py} (100%) rename tests/components/recorder/{models_schema_23.py => db_schema_23.py} (100%) rename tests/components/recorder/{models_schema_23_with_newer_columns.py => db_schema_23_with_newer_columns.py} (100%) rename tests/components/recorder/{models_schema_28.py => db_schema_28.py} (100%) diff --git a/homeassistant/components/logbook/queries/all.py b/homeassistant/components/logbook/queries/all.py index d321578f545..da05aa02fff 100644 --- a/homeassistant/components/logbook/queries/all.py +++ b/homeassistant/components/logbook/queries/all.py @@ -8,7 +8,11 @@ from sqlalchemy.orm import Query from sqlalchemy.sql.elements import ClauseList from sqlalchemy.sql.lambdas import StatementLambdaElement -from homeassistant.components.recorder.models import LAST_UPDATED_INDEX, Events, States +from homeassistant.components.recorder.db_schema import ( + LAST_UPDATED_INDEX, + Events, + States, +) from .common import ( apply_states_filters, diff --git a/homeassistant/components/logbook/queries/common.py b/homeassistant/components/logbook/queries/common.py index 56925b60e62..5b79f6e0d32 100644 --- a/homeassistant/components/logbook/queries/common.py +++ b/homeassistant/components/logbook/queries/common.py @@ -10,8 +10,7 @@ from sqlalchemy.sql.elements import ClauseList from sqlalchemy.sql.expression import literal from sqlalchemy.sql.selectable import Select -from homeassistant.components.recorder.filters import like_domain_matchers -from homeassistant.components.recorder.models import ( +from homeassistant.components.recorder.db_schema import ( EVENTS_CONTEXT_ID_INDEX, OLD_FORMAT_ATTRS_JSON, OLD_STATE, @@ -22,6 +21,7 @@ from homeassistant.components.recorder.models import ( StateAttributes, States, ) +from homeassistant.components.recorder.filters import like_domain_matchers from ..const import ALWAYS_CONTINUOUS_DOMAINS, CONDITIONALLY_CONTINUOUS_DOMAINS diff --git a/homeassistant/components/logbook/queries/devices.py b/homeassistant/components/logbook/queries/devices.py index 88e9f50a42c..f750c552bc4 100644 --- a/homeassistant/components/logbook/queries/devices.py +++ b/homeassistant/components/logbook/queries/devices.py @@ -10,7 +10,7 @@ from sqlalchemy.sql.elements import ClauseList from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import CTE, CompoundSelect -from homeassistant.components.recorder.models import ( +from homeassistant.components.recorder.db_schema import ( DEVICE_ID_IN_EVENT, EventData, Events, diff --git a/homeassistant/components/logbook/queries/entities.py b/homeassistant/components/logbook/queries/entities.py index 8de4a5eaf64..4ef96c100d7 100644 --- a/homeassistant/components/logbook/queries/entities.py +++ b/homeassistant/components/logbook/queries/entities.py @@ -10,7 +10,7 @@ from sqlalchemy.orm import Query from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import CTE, CompoundSelect -from homeassistant.components.recorder.models import ( +from homeassistant.components.recorder.db_schema import ( ENTITY_ID_IN_EVENT, ENTITY_ID_LAST_UPDATED_INDEX, OLD_ENTITY_ID_IN_EVENT, diff --git a/homeassistant/components/logbook/queries/entities_and_devices.py b/homeassistant/components/logbook/queries/entities_and_devices.py index 1c4271422b7..591918dd653 100644 --- a/homeassistant/components/logbook/queries/entities_and_devices.py +++ b/homeassistant/components/logbook/queries/entities_and_devices.py @@ -10,7 +10,7 @@ from sqlalchemy.orm import Query from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import CTE, CompoundSelect -from homeassistant.components.recorder.models import EventData, Events, States +from homeassistant.components.recorder.db_schema import EventData, Events, States from .common import ( apply_events_context_hints, diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 7a096a9c404..d8260976ccf 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -48,17 +48,19 @@ from .const import ( SQLITE_URL_PREFIX, SupportedDialect, ) -from .executor import DBInterruptibleThreadPoolExecutor -from .models import ( +from .db_schema import ( SCHEMA_VERSION, Base, EventData, Events, StateAttributes, States, + StatisticsRuns, +) +from .executor import DBInterruptibleThreadPoolExecutor +from .models import ( StatisticData, StatisticMetaData, - StatisticsRuns, UnsupportedDialect, process_timestamp, ) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py new file mode 100644 index 00000000000..642efe2e969 --- /dev/null +++ b/homeassistant/components/recorder/db_schema.py @@ -0,0 +1,600 @@ +"""Models for SQLAlchemy.""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime, timedelta +import json +import logging +from typing import Any, cast + +import ciso8601 +from fnvhash import fnv1a_32 +from sqlalchemy import ( + JSON, + BigInteger, + Boolean, + Column, + DateTime, + Float, + ForeignKey, + Identity, + Index, + Integer, + SmallInteger, + String, + Text, + distinct, + type_coerce, +) +from sqlalchemy.dialects import mysql, oracle, postgresql, sqlite +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.orm import aliased, declarative_base, relationship +from sqlalchemy.orm.session import Session + +from homeassistant.const import ( + MAX_LENGTH_EVENT_CONTEXT_ID, + MAX_LENGTH_EVENT_EVENT_TYPE, + MAX_LENGTH_EVENT_ORIGIN, + MAX_LENGTH_STATE_ENTITY_ID, + MAX_LENGTH_STATE_STATE, +) +from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id +import homeassistant.util.dt as dt_util + +from .const import ALL_DOMAIN_EXCLUDE_ATTRS, JSON_DUMP +from .models import StatisticData, StatisticMetaData, process_timestamp + +# SQLAlchemy Schema +# pylint: disable=invalid-name +Base = declarative_base() + +SCHEMA_VERSION = 29 + +_LOGGER = logging.getLogger(__name__) + +TABLE_EVENTS = "events" +TABLE_EVENT_DATA = "event_data" +TABLE_STATES = "states" +TABLE_STATE_ATTRIBUTES = "state_attributes" +TABLE_RECORDER_RUNS = "recorder_runs" +TABLE_SCHEMA_CHANGES = "schema_changes" +TABLE_STATISTICS = "statistics" +TABLE_STATISTICS_META = "statistics_meta" +TABLE_STATISTICS_RUNS = "statistics_runs" +TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" + +ALL_TABLES = [ + TABLE_STATES, + TABLE_STATE_ATTRIBUTES, + TABLE_EVENTS, + TABLE_EVENT_DATA, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + TABLE_STATISTICS, + TABLE_STATISTICS_META, + TABLE_STATISTICS_RUNS, + TABLE_STATISTICS_SHORT_TERM, +] + +TABLES_TO_CHECK = [ + TABLE_STATES, + TABLE_EVENTS, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, +] + +LAST_UPDATED_INDEX = "ix_states_last_updated" +ENTITY_ID_LAST_UPDATED_INDEX = "ix_states_entity_id_last_updated" +EVENTS_CONTEXT_ID_INDEX = "ix_events_context_id" +STATES_CONTEXT_ID_INDEX = "ix_states_context_id" + + +class FAST_PYSQLITE_DATETIME(sqlite.DATETIME): # type: ignore[misc] + """Use ciso8601 to parse datetimes instead of sqlalchemy built-in regex.""" + + def result_processor(self, dialect, coltype): # type: ignore[no-untyped-def] + """Offload the datetime parsing to ciso8601.""" + return lambda value: None if value is None else ciso8601.parse_datetime(value) + + +JSON_VARIENT_CAST = Text().with_variant( + postgresql.JSON(none_as_null=True), "postgresql" +) +JSONB_VARIENT_CAST = Text().with_variant( + postgresql.JSONB(none_as_null=True), "postgresql" +) +DATETIME_TYPE = ( + DateTime(timezone=True) + .with_variant(mysql.DATETIME(timezone=True, fsp=6), "mysql") + .with_variant(FAST_PYSQLITE_DATETIME(), "sqlite") +) +DOUBLE_TYPE = ( + Float() + .with_variant(mysql.DOUBLE(asdecimal=False), "mysql") + .with_variant(oracle.DOUBLE_PRECISION(), "oracle") + .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") +) + + +class JSONLiteral(JSON): # type: ignore[misc] + """Teach SA how to literalize json.""" + + def literal_processor(self, dialect: str) -> Callable[[Any], str]: + """Processor to convert a value to JSON.""" + + def process(value: Any) -> str: + """Dump json.""" + return json.dumps(value) + + return process + + +EVENT_ORIGIN_ORDER = [EventOrigin.local, EventOrigin.remote] +EVENT_ORIGIN_TO_IDX = {origin: idx for idx, origin in enumerate(EVENT_ORIGIN_ORDER)} + + +class Events(Base): # type: ignore[misc,valid-type] + """Event history data.""" + + __table_args__ = ( + # Used for fetching events at a specific time + # see logbook + Index("ix_events_event_type_time_fired", "event_type", "time_fired"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_EVENTS + event_id = Column(Integer, Identity(), primary_key=True) + event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) + event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) # no longer used for new rows + origin_idx = Column(SmallInteger) + time_fired = Column(DATETIME_TYPE, index=True) + context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) + context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) + data_id = Column(Integer, ForeignKey("event_data.data_id"), index=True) + event_data_rel = relationship("EventData") + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> Events: + """Create an event database object from a native event.""" + return Events( + event_type=event.event_type, + event_data=None, + origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), + time_fired=event.time_fired, + context_id=event.context.id, + context_user_id=event.context.user_id, + context_parent_id=event.context.parent_id, + ) + + def to_native(self, validate_entity_id: bool = True) -> Event | None: + """Convert to a native HA Event.""" + context = Context( + id=self.context_id, + user_id=self.context_user_id, + parent_id=self.context_parent_id, + ) + try: + return Event( + self.event_type, + json.loads(self.event_data) if self.event_data else {}, + EventOrigin(self.origin) + if self.origin + else EVENT_ORIGIN_ORDER[self.origin_idx], + process_timestamp(self.time_fired), + context=context, + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting to event: %s", self) + return None + + +class EventData(Base): # type: ignore[misc,valid-type] + """Event data history.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_EVENT_DATA + data_id = Column(Integer, Identity(), primary_key=True) + hash = Column(BigInteger, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> EventData: + """Create object from an event.""" + shared_data = JSON_DUMP(event.data) + return EventData( + shared_data=shared_data, hash=EventData.hash_shared_data(shared_data) + ) + + @staticmethod + def shared_data_from_event(event: Event) -> str: + """Create shared_attrs from an event.""" + return JSON_DUMP(event.data) + + @staticmethod + def hash_shared_data(shared_data: str) -> int: + """Return the hash of json encoded shared data.""" + return cast(int, fnv1a_32(shared_data.encode("utf-8"))) + + def to_native(self) -> dict[str, Any]: + """Convert to an HA state object.""" + try: + return cast(dict[str, Any], json.loads(self.shared_data)) + except ValueError: + _LOGGER.exception("Error converting row to event data: %s", self) + return {} + + +class States(Base): # type: ignore[misc,valid-type] + """State change history.""" + + __table_args__ = ( + # Used for fetching the state of entities at a specific time + # (get_states in history.py) + Index(ENTITY_ID_LAST_UPDATED_INDEX, "entity_id", "last_updated"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATES + state_id = Column(Integer, Identity(), primary_key=True) + entity_id = Column(String(MAX_LENGTH_STATE_ENTITY_ID)) + state = Column(String(MAX_LENGTH_STATE_STATE)) + attributes = Column( + Text().with_variant(mysql.LONGTEXT, "mysql") + ) # no longer used for new rows + event_id = Column( # no longer used for new rows + Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True + ) + last_changed = Column(DATETIME_TYPE) + last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) + old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) + attributes_id = Column( + Integer, ForeignKey("state_attributes.attributes_id"), index=True + ) + context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) + context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) + origin_idx = Column(SmallInteger) # 0 is local, 1 is remote + old_state = relationship("States", remote_side=[state_id]) + state_attributes = relationship("StateAttributes") + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> States: + """Create object from a state_changed event.""" + entity_id = event.data["entity_id"] + state: State | None = event.data.get("new_state") + dbstate = States( + entity_id=entity_id, + attributes=None, + context_id=event.context.id, + context_user_id=event.context.user_id, + context_parent_id=event.context.parent_id, + origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), + ) + + # None state means the state was removed from the state machine + if state is None: + dbstate.state = "" + dbstate.last_updated = event.time_fired + dbstate.last_changed = None + return dbstate + + dbstate.state = state.state + dbstate.last_updated = state.last_updated + if state.last_updated == state.last_changed: + dbstate.last_changed = None + else: + dbstate.last_changed = state.last_changed + + return dbstate + + def to_native(self, validate_entity_id: bool = True) -> State | None: + """Convert to an HA state object.""" + context = Context( + id=self.context_id, + user_id=self.context_user_id, + parent_id=self.context_parent_id, + ) + try: + attrs = json.loads(self.attributes) if self.attributes else {} + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state: %s", self) + return None + if self.last_changed is None or self.last_changed == self.last_updated: + last_changed = last_updated = process_timestamp(self.last_updated) + else: + last_updated = process_timestamp(self.last_updated) + last_changed = process_timestamp(self.last_changed) + return State( + self.entity_id, + self.state, + # Join the state_attributes table on attributes_id to get the attributes + # for newer states + attrs, + last_changed, + last_updated, + context=context, + validate_entity_id=validate_entity_id, + ) + + +class StateAttributes(Base): # type: ignore[misc,valid-type] + """State attribute change history.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATE_ATTRIBUTES + attributes_id = Column(Integer, Identity(), primary_key=True) + hash = Column(BigInteger, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_attrs = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> StateAttributes: + """Create object from a state_changed event.""" + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + dbstate = StateAttributes( + shared_attrs="{}" if state is None else JSON_DUMP(state.attributes) + ) + dbstate.hash = StateAttributes.hash_shared_attrs(dbstate.shared_attrs) + return dbstate + + @staticmethod + def shared_attrs_from_event( + event: Event, exclude_attrs_by_domain: dict[str, set[str]] + ) -> str: + """Create shared_attrs from a state_changed event.""" + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + if state is None: + return "{}" + domain = split_entity_id(state.entity_id)[0] + exclude_attrs = ( + exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS + ) + return JSON_DUMP( + {k: v for k, v in state.attributes.items() if k not in exclude_attrs} + ) + + @staticmethod + def hash_shared_attrs(shared_attrs: str) -> int: + """Return the hash of json encoded shared attributes.""" + return cast(int, fnv1a_32(shared_attrs.encode("utf-8"))) + + def to_native(self) -> dict[str, Any]: + """Convert to an HA state object.""" + try: + return cast(dict[str, Any], json.loads(self.shared_attrs)) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state attributes: %s", self) + return {} + + +class StatisticsBase: + """Statistics base class.""" + + id = Column(Integer, Identity(), primary_key=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + + @declared_attr # type: ignore[misc] + def metadata_id(self) -> Column: + """Define the metadata_id column for sub classes.""" + return Column( + Integer, + ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), + index=True, + ) + + start = Column(DATETIME_TYPE, index=True) + mean = Column(DOUBLE_TYPE) + min = Column(DOUBLE_TYPE) + max = Column(DOUBLE_TYPE) + last_reset = Column(DATETIME_TYPE) + state = Column(DOUBLE_TYPE) + sum = Column(DOUBLE_TYPE) + + @classmethod + def from_stats(cls, metadata_id: int, stats: StatisticData) -> StatisticsBase: + """Create object from a statistics.""" + return cls( # type: ignore[call-arg,misc] + metadata_id=metadata_id, + **stats, + ) + + +class Statistics(Base, StatisticsBase): # type: ignore[misc,valid-type] + """Long term statistics.""" + + duration = timedelta(hours=1) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index("ix_statistics_statistic_id_start", "metadata_id", "start", unique=True), + ) + __tablename__ = TABLE_STATISTICS + + +class StatisticsShortTerm(Base, StatisticsBase): # type: ignore[misc,valid-type] + """Short term statistics.""" + + duration = timedelta(minutes=5) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index( + "ix_statistics_short_term_statistic_id_start", + "metadata_id", + "start", + unique=True, + ), + ) + __tablename__ = TABLE_STATISTICS_SHORT_TERM + + +class StatisticsMeta(Base): # type: ignore[misc,valid-type] + """Statistics meta data.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATISTICS_META + id = Column(Integer, Identity(), primary_key=True) + statistic_id = Column(String(255), index=True, unique=True) + source = Column(String(32)) + unit_of_measurement = Column(String(255)) + has_mean = Column(Boolean) + has_sum = Column(Boolean) + name = Column(String(255)) + + @staticmethod + def from_meta(meta: StatisticMetaData) -> StatisticsMeta: + """Create object from meta data.""" + return StatisticsMeta(**meta) + + +class RecorderRuns(Base): # type: ignore[misc,valid-type] + """Representation of recorder run.""" + + __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) + __tablename__ = TABLE_RECORDER_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True), default=dt_util.utcnow) + end = Column(DateTime(timezone=True)) + closed_incorrect = Column(Boolean, default=False) + created = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + end = ( + f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None + ) + return ( + f"" + ) + + def entity_ids(self, point_in_time: datetime | None = None) -> list[str]: + """Return the entity ids that existed in this run. + + Specify point_in_time if you want to know which existed at that point + in time inside the run. + """ + session = Session.object_session(self) + + assert session is not None, "RecorderRuns need to be persisted" + + query = session.query(distinct(States.entity_id)).filter( + States.last_updated >= self.start + ) + + if point_in_time is not None: + query = query.filter(States.last_updated < point_in_time) + elif self.end is not None: + query = query.filter(States.last_updated < self.end) + + return [row[0] for row in query] + + def to_native(self, validate_entity_id: bool = True) -> RecorderRuns: + """Return self, native format is this model.""" + return self + + +class SchemaChanges(Base): # type: ignore[misc,valid-type] + """Representation of schema version changes.""" + + __tablename__ = TABLE_SCHEMA_CHANGES + change_id = Column(Integer, Identity(), primary_key=True) + schema_version = Column(Integer) + changed = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +class StatisticsRuns(Base): # type: ignore[misc,valid-type] + """Representation of statistics run.""" + + __tablename__ = TABLE_STATISTICS_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True), index=True) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +EVENT_DATA_JSON = type_coerce( + EventData.shared_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) +) +OLD_FORMAT_EVENT_DATA_JSON = type_coerce( + Events.event_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) +) + +SHARED_ATTRS_JSON = type_coerce( + StateAttributes.shared_attrs.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) +) +OLD_FORMAT_ATTRS_JSON = type_coerce( + States.attributes.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) +) + +ENTITY_ID_IN_EVENT: Column = EVENT_DATA_JSON["entity_id"] +OLD_ENTITY_ID_IN_EVENT: Column = OLD_FORMAT_EVENT_DATA_JSON["entity_id"] +DEVICE_ID_IN_EVENT: Column = EVENT_DATA_JSON["device_id"] +OLD_STATE = aliased(States, name="old_state") diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 0b3e0e68030..02c342441a7 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_ from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS from homeassistant.helpers.typing import ConfigType -from .models import ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT, States +from .db_schema import ENTITY_ID_IN_EVENT, OLD_ENTITY_ID_IN_EVENT, States DOMAIN = "history" HISTORY_FILTERS = "history_filters" diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 37285f66d1d..e1eca282a3a 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -25,12 +25,10 @@ from homeassistant.components.websocket_api.const import ( from homeassistant.core import HomeAssistant, State, split_entity_id import homeassistant.util.dt as dt_util +from .db_schema import RecorderRuns, StateAttributes, States from .filters import Filters from .models import ( LazyState, - RecorderRuns, - StateAttributes, - States, process_datetime_to_timestamp, process_timestamp, process_timestamp_to_utc_isoformat, diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index cc5af684566..7e11e62502d 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -22,7 +22,7 @@ from sqlalchemy.sql.expression import true from homeassistant.core import HomeAssistant from .const import SupportedDialect -from .models import ( +from .db_schema import ( SCHEMA_VERSION, TABLE_STATES, Base, @@ -31,8 +31,8 @@ from .models import ( StatisticsMeta, StatisticsRuns, StatisticsShortTerm, - process_timestamp, ) +from .models import process_timestamp from .statistics import ( delete_statistics_duplicates, delete_statistics_meta_duplicates, diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 8db648f15a8..ef1f76df9fc 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -1,36 +1,12 @@ -"""Models for SQLAlchemy.""" +"""Models for Recorder.""" from __future__ import annotations -from collections.abc import Callable -from datetime import datetime, timedelta +from datetime import datetime import json import logging -from typing import Any, TypedDict, cast, overload +from typing import Any, TypedDict, overload -import ciso8601 -from fnvhash import fnv1a_32 -from sqlalchemy import ( - JSON, - BigInteger, - Boolean, - Column, - DateTime, - Float, - ForeignKey, - Identity, - Index, - Integer, - SmallInteger, - String, - Text, - distinct, - type_coerce, -) -from sqlalchemy.dialects import mysql, oracle, postgresql, sqlite from sqlalchemy.engine.row import Row -from sqlalchemy.ext.declarative import declared_attr -from sqlalchemy.orm import aliased, declarative_base, relationship -from sqlalchemy.orm.session import Session from homeassistant.components.websocket_api.const import ( COMPRESSED_STATE_ATTRIBUTES, @@ -38,396 +14,22 @@ from homeassistant.components.websocket_api.const import ( COMPRESSED_STATE_LAST_UPDATED, COMPRESSED_STATE_STATE, ) -from homeassistant.const import ( - MAX_LENGTH_EVENT_CONTEXT_ID, - MAX_LENGTH_EVENT_EVENT_TYPE, - MAX_LENGTH_EVENT_ORIGIN, - MAX_LENGTH_STATE_ENTITY_ID, - MAX_LENGTH_STATE_STATE, -) -from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id +from homeassistant.core import Context, State import homeassistant.util.dt as dt_util -from .const import ALL_DOMAIN_EXCLUDE_ATTRS, JSON_DUMP - -# SQLAlchemy Schema # pylint: disable=invalid-name -Base = declarative_base() - -SCHEMA_VERSION = 29 _LOGGER = logging.getLogger(__name__) DB_TIMEZONE = "+00:00" -TABLE_EVENTS = "events" -TABLE_EVENT_DATA = "event_data" -TABLE_STATES = "states" -TABLE_STATE_ATTRIBUTES = "state_attributes" -TABLE_RECORDER_RUNS = "recorder_runs" -TABLE_SCHEMA_CHANGES = "schema_changes" -TABLE_STATISTICS = "statistics" -TABLE_STATISTICS_META = "statistics_meta" -TABLE_STATISTICS_RUNS = "statistics_runs" -TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" - -ALL_TABLES = [ - TABLE_STATES, - TABLE_STATE_ATTRIBUTES, - TABLE_EVENTS, - TABLE_EVENT_DATA, - TABLE_RECORDER_RUNS, - TABLE_SCHEMA_CHANGES, - TABLE_STATISTICS, - TABLE_STATISTICS_META, - TABLE_STATISTICS_RUNS, - TABLE_STATISTICS_SHORT_TERM, -] - -TABLES_TO_CHECK = [ - TABLE_STATES, - TABLE_EVENTS, - TABLE_RECORDER_RUNS, - TABLE_SCHEMA_CHANGES, -] - -LAST_UPDATED_INDEX = "ix_states_last_updated" -ENTITY_ID_LAST_UPDATED_INDEX = "ix_states_entity_id_last_updated" -EVENTS_CONTEXT_ID_INDEX = "ix_events_context_id" -STATES_CONTEXT_ID_INDEX = "ix_states_context_id" - EMPTY_JSON_OBJECT = "{}" -class FAST_PYSQLITE_DATETIME(sqlite.DATETIME): # type: ignore[misc] - """Use ciso8601 to parse datetimes instead of sqlalchemy built-in regex.""" - - def result_processor(self, dialect, coltype): # type: ignore[no-untyped-def] - """Offload the datetime parsing to ciso8601.""" - return lambda value: None if value is None else ciso8601.parse_datetime(value) - - -JSON_VARIENT_CAST = Text().with_variant( - postgresql.JSON(none_as_null=True), "postgresql" -) -JSONB_VARIENT_CAST = Text().with_variant( - postgresql.JSONB(none_as_null=True), "postgresql" -) -DATETIME_TYPE = ( - DateTime(timezone=True) - .with_variant(mysql.DATETIME(timezone=True, fsp=6), "mysql") - .with_variant(FAST_PYSQLITE_DATETIME(), "sqlite") -) -DOUBLE_TYPE = ( - Float() - .with_variant(mysql.DOUBLE(asdecimal=False), "mysql") - .with_variant(oracle.DOUBLE_PRECISION(), "oracle") - .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") -) - - -class JSONLiteral(JSON): # type: ignore[misc] - """Teach SA how to literalize json.""" - - def literal_processor(self, dialect: str) -> Callable[[Any], str]: - """Processor to convert a value to JSON.""" - - def process(value: Any) -> str: - """Dump json.""" - return json.dumps(value) - - return process - - -EVENT_ORIGIN_ORDER = [EventOrigin.local, EventOrigin.remote] -EVENT_ORIGIN_TO_IDX = {origin: idx for idx, origin in enumerate(EVENT_ORIGIN_ORDER)} - - class UnsupportedDialect(Exception): """The dialect or its version is not supported.""" -class Events(Base): # type: ignore[misc,valid-type] - """Event history data.""" - - __table_args__ = ( - # Used for fetching events at a specific time - # see logbook - Index("ix_events_event_type_time_fired", "event_type", "time_fired"), - {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, - ) - __tablename__ = TABLE_EVENTS - event_id = Column(Integer, Identity(), primary_key=True) - event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) - event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) - origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) # no longer used for new rows - origin_idx = Column(SmallInteger) - time_fired = Column(DATETIME_TYPE, index=True) - context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) - context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) - context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) - data_id = Column(Integer, ForeignKey("event_data.data_id"), index=True) - event_data_rel = relationship("EventData") - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - @staticmethod - def from_event(event: Event) -> Events: - """Create an event database object from a native event.""" - return Events( - event_type=event.event_type, - event_data=None, - origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), - time_fired=event.time_fired, - context_id=event.context.id, - context_user_id=event.context.user_id, - context_parent_id=event.context.parent_id, - ) - - def to_native(self, validate_entity_id: bool = True) -> Event | None: - """Convert to a native HA Event.""" - context = Context( - id=self.context_id, - user_id=self.context_user_id, - parent_id=self.context_parent_id, - ) - try: - return Event( - self.event_type, - json.loads(self.event_data) if self.event_data else {}, - EventOrigin(self.origin) - if self.origin - else EVENT_ORIGIN_ORDER[self.origin_idx], - process_timestamp(self.time_fired), - context=context, - ) - except ValueError: - # When json.loads fails - _LOGGER.exception("Error converting to event: %s", self) - return None - - -class EventData(Base): # type: ignore[misc,valid-type] - """Event data history.""" - - __table_args__ = ( - {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, - ) - __tablename__ = TABLE_EVENT_DATA - data_id = Column(Integer, Identity(), primary_key=True) - hash = Column(BigInteger, index=True) - # Note that this is not named attributes to avoid confusion with the states table - shared_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - @staticmethod - def from_event(event: Event) -> EventData: - """Create object from an event.""" - shared_data = JSON_DUMP(event.data) - return EventData( - shared_data=shared_data, hash=EventData.hash_shared_data(shared_data) - ) - - @staticmethod - def shared_data_from_event(event: Event) -> str: - """Create shared_attrs from an event.""" - return JSON_DUMP(event.data) - - @staticmethod - def hash_shared_data(shared_data: str) -> int: - """Return the hash of json encoded shared data.""" - return cast(int, fnv1a_32(shared_data.encode("utf-8"))) - - def to_native(self) -> dict[str, Any]: - """Convert to an HA state object.""" - try: - return cast(dict[str, Any], json.loads(self.shared_data)) - except ValueError: - _LOGGER.exception("Error converting row to event data: %s", self) - return {} - - -class States(Base): # type: ignore[misc,valid-type] - """State change history.""" - - __table_args__ = ( - # Used for fetching the state of entities at a specific time - # (get_states in history.py) - Index(ENTITY_ID_LAST_UPDATED_INDEX, "entity_id", "last_updated"), - {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, - ) - __tablename__ = TABLE_STATES - state_id = Column(Integer, Identity(), primary_key=True) - entity_id = Column(String(MAX_LENGTH_STATE_ENTITY_ID)) - state = Column(String(MAX_LENGTH_STATE_STATE)) - attributes = Column( - Text().with_variant(mysql.LONGTEXT, "mysql") - ) # no longer used for new rows - event_id = Column( # no longer used for new rows - Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True - ) - last_changed = Column(DATETIME_TYPE) - last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) - old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) - attributes_id = Column( - Integer, ForeignKey("state_attributes.attributes_id"), index=True - ) - context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) - context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) - context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) - origin_idx = Column(SmallInteger) # 0 is local, 1 is remote - old_state = relationship("States", remote_side=[state_id]) - state_attributes = relationship("StateAttributes") - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - @staticmethod - def from_event(event: Event) -> States: - """Create object from a state_changed event.""" - entity_id = event.data["entity_id"] - state: State | None = event.data.get("new_state") - dbstate = States( - entity_id=entity_id, - attributes=None, - context_id=event.context.id, - context_user_id=event.context.user_id, - context_parent_id=event.context.parent_id, - origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), - ) - - # None state means the state was removed from the state machine - if state is None: - dbstate.state = "" - dbstate.last_updated = event.time_fired - dbstate.last_changed = None - return dbstate - - dbstate.state = state.state - dbstate.last_updated = state.last_updated - if state.last_updated == state.last_changed: - dbstate.last_changed = None - else: - dbstate.last_changed = state.last_changed - - return dbstate - - def to_native(self, validate_entity_id: bool = True) -> State | None: - """Convert to an HA state object.""" - context = Context( - id=self.context_id, - user_id=self.context_user_id, - parent_id=self.context_parent_id, - ) - try: - attrs = json.loads(self.attributes) if self.attributes else {} - except ValueError: - # When json.loads fails - _LOGGER.exception("Error converting row to state: %s", self) - return None - if self.last_changed is None or self.last_changed == self.last_updated: - last_changed = last_updated = process_timestamp(self.last_updated) - else: - last_updated = process_timestamp(self.last_updated) - last_changed = process_timestamp(self.last_changed) - return State( - self.entity_id, - self.state, - # Join the state_attributes table on attributes_id to get the attributes - # for newer states - attrs, - last_changed, - last_updated, - context=context, - validate_entity_id=validate_entity_id, - ) - - -class StateAttributes(Base): # type: ignore[misc,valid-type] - """State attribute change history.""" - - __table_args__ = ( - {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, - ) - __tablename__ = TABLE_STATE_ATTRIBUTES - attributes_id = Column(Integer, Identity(), primary_key=True) - hash = Column(BigInteger, index=True) - # Note that this is not named attributes to avoid confusion with the states table - shared_attrs = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - @staticmethod - def from_event(event: Event) -> StateAttributes: - """Create object from a state_changed event.""" - state: State | None = event.data.get("new_state") - # None state means the state was removed from the state machine - dbstate = StateAttributes( - shared_attrs="{}" if state is None else JSON_DUMP(state.attributes) - ) - dbstate.hash = StateAttributes.hash_shared_attrs(dbstate.shared_attrs) - return dbstate - - @staticmethod - def shared_attrs_from_event( - event: Event, exclude_attrs_by_domain: dict[str, set[str]] - ) -> str: - """Create shared_attrs from a state_changed event.""" - state: State | None = event.data.get("new_state") - # None state means the state was removed from the state machine - if state is None: - return "{}" - domain = split_entity_id(state.entity_id)[0] - exclude_attrs = ( - exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS - ) - return JSON_DUMP( - {k: v for k, v in state.attributes.items() if k not in exclude_attrs} - ) - - @staticmethod - def hash_shared_attrs(shared_attrs: str) -> int: - """Return the hash of json encoded shared attributes.""" - return cast(int, fnv1a_32(shared_attrs.encode("utf-8"))) - - def to_native(self) -> dict[str, Any]: - """Convert to an HA state object.""" - try: - return cast(dict[str, Any], json.loads(self.shared_attrs)) - except ValueError: - # When json.loads fails - _LOGGER.exception("Error converting row to state attributes: %s", self) - return {} - - class StatisticResult(TypedDict): """Statistic result data class. @@ -455,67 +57,6 @@ class StatisticData(StatisticDataBase, total=False): sum: float -class StatisticsBase: - """Statistics base class.""" - - id = Column(Integer, Identity(), primary_key=True) - created = Column(DATETIME_TYPE, default=dt_util.utcnow) - - @declared_attr # type: ignore[misc] - def metadata_id(self) -> Column: - """Define the metadata_id column for sub classes.""" - return Column( - Integer, - ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), - index=True, - ) - - start = Column(DATETIME_TYPE, index=True) - mean = Column(DOUBLE_TYPE) - min = Column(DOUBLE_TYPE) - max = Column(DOUBLE_TYPE) - last_reset = Column(DATETIME_TYPE) - state = Column(DOUBLE_TYPE) - sum = Column(DOUBLE_TYPE) - - @classmethod - def from_stats(cls, metadata_id: int, stats: StatisticData) -> StatisticsBase: - """Create object from a statistics.""" - return cls( # type: ignore[call-arg,misc] - metadata_id=metadata_id, - **stats, - ) - - -class Statistics(Base, StatisticsBase): # type: ignore[misc,valid-type] - """Long term statistics.""" - - duration = timedelta(hours=1) - - __table_args__ = ( - # Used for fetching statistics for a certain entity at a specific time - Index("ix_statistics_statistic_id_start", "metadata_id", "start", unique=True), - ) - __tablename__ = TABLE_STATISTICS - - -class StatisticsShortTerm(Base, StatisticsBase): # type: ignore[misc,valid-type] - """Short term statistics.""" - - duration = timedelta(minutes=5) - - __table_args__ = ( - # Used for fetching statistics for a certain entity at a specific time - Index( - "ix_statistics_short_term_statistic_id_start", - "metadata_id", - "start", - unique=True, - ), - ) - __tablename__ = TABLE_STATISTICS_SHORT_TERM - - class StatisticMetaData(TypedDict): """Statistic meta data class.""" @@ -527,131 +68,6 @@ class StatisticMetaData(TypedDict): unit_of_measurement: str | None -class StatisticsMeta(Base): # type: ignore[misc,valid-type] - """Statistics meta data.""" - - __table_args__ = ( - {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, - ) - __tablename__ = TABLE_STATISTICS_META - id = Column(Integer, Identity(), primary_key=True) - statistic_id = Column(String(255), index=True, unique=True) - source = Column(String(32)) - unit_of_measurement = Column(String(255)) - has_mean = Column(Boolean) - has_sum = Column(Boolean) - name = Column(String(255)) - - @staticmethod - def from_meta(meta: StatisticMetaData) -> StatisticsMeta: - """Create object from meta data.""" - return StatisticsMeta(**meta) - - -class RecorderRuns(Base): # type: ignore[misc,valid-type] - """Representation of recorder run.""" - - __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) - __tablename__ = TABLE_RECORDER_RUNS - run_id = Column(Integer, Identity(), primary_key=True) - start = Column(DateTime(timezone=True), default=dt_util.utcnow) - end = Column(DateTime(timezone=True)) - closed_incorrect = Column(Boolean, default=False) - created = Column(DateTime(timezone=True), default=dt_util.utcnow) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - end = ( - f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None - ) - return ( - f"" - ) - - def entity_ids(self, point_in_time: datetime | None = None) -> list[str]: - """Return the entity ids that existed in this run. - - Specify point_in_time if you want to know which existed at that point - in time inside the run. - """ - session = Session.object_session(self) - - assert session is not None, "RecorderRuns need to be persisted" - - query = session.query(distinct(States.entity_id)).filter( - States.last_updated >= self.start - ) - - if point_in_time is not None: - query = query.filter(States.last_updated < point_in_time) - elif self.end is not None: - query = query.filter(States.last_updated < self.end) - - return [row[0] for row in query] - - def to_native(self, validate_entity_id: bool = True) -> RecorderRuns: - """Return self, native format is this model.""" - return self - - -class SchemaChanges(Base): # type: ignore[misc,valid-type] - """Representation of schema version changes.""" - - __tablename__ = TABLE_SCHEMA_CHANGES - change_id = Column(Integer, Identity(), primary_key=True) - schema_version = Column(Integer) - changed = Column(DateTime(timezone=True), default=dt_util.utcnow) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - -class StatisticsRuns(Base): # type: ignore[misc,valid-type] - """Representation of statistics run.""" - - __tablename__ = TABLE_STATISTICS_RUNS - run_id = Column(Integer, Identity(), primary_key=True) - start = Column(DateTime(timezone=True), index=True) - - def __repr__(self) -> str: - """Return string representation of instance for debugging.""" - return ( - f"" - ) - - -EVENT_DATA_JSON = type_coerce( - EventData.shared_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) -) -OLD_FORMAT_EVENT_DATA_JSON = type_coerce( - Events.event_data.cast(JSONB_VARIENT_CAST), JSONLiteral(none_as_null=True) -) - -SHARED_ATTRS_JSON = type_coerce( - StateAttributes.shared_attrs.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) -) -OLD_FORMAT_ATTRS_JSON = type_coerce( - States.attributes.cast(JSON_VARIENT_CAST), JSON(none_as_null=True) -) - -ENTITY_ID_IN_EVENT: Column = EVENT_DATA_JSON["entity_id"] -OLD_ENTITY_ID_IN_EVENT: Column = OLD_FORMAT_EVENT_DATA_JSON["entity_id"] -DEVICE_ID_IN_EVENT: Column = EVENT_DATA_JSON["device_id"] -OLD_STATE = aliased(States, name="old_state") - - @overload def process_timestamp(ts: None) -> None: ... diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 10136dfb5a6..c470575c5f1 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -14,7 +14,7 @@ from sqlalchemy.sql.expression import distinct from homeassistant.const import EVENT_STATE_CHANGED from .const import MAX_ROWS_TO_PURGE, SupportedDialect -from .models import Events, StateAttributes, States +from .db_schema import Events, StateAttributes, States from .queries import ( attributes_ids_exist_in_states, attributes_ids_exist_in_states_sqlite, diff --git a/homeassistant/components/recorder/queries.py b/homeassistant/components/recorder/queries.py index e27d3d692cc..4b4488d4dad 100644 --- a/homeassistant/components/recorder/queries.py +++ b/homeassistant/components/recorder/queries.py @@ -9,7 +9,7 @@ from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import Select from .const import MAX_ROWS_TO_PURGE -from .models import ( +from .db_schema import ( EventData, Events, RecorderRuns, diff --git a/homeassistant/components/recorder/repack.py b/homeassistant/components/recorder/repack.py index 1b1d59df37e..53c922cf481 100644 --- a/homeassistant/components/recorder/repack.py +++ b/homeassistant/components/recorder/repack.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING from sqlalchemy import text from .const import SupportedDialect -from .models import ALL_TABLES +from .db_schema import ALL_TABLES if TYPE_CHECKING: from . import Recorder diff --git a/homeassistant/components/recorder/run_history.py b/homeassistant/components/recorder/run_history.py index 783aff89c17..fb87d9a1fa2 100644 --- a/homeassistant/components/recorder/run_history.py +++ b/homeassistant/components/recorder/run_history.py @@ -9,7 +9,8 @@ from sqlalchemy.orm.session import Session import homeassistant.util.dt as dt_util -from .models import RecorderRuns, process_timestamp +from .db_schema import RecorderRuns +from .models import process_timestamp def _find_recorder_run_for_start_time( diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 012b34ec0ef..26221aa199b 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -42,14 +42,11 @@ from homeassistant.util.unit_system import UnitSystem import homeassistant.util.volume as volume_util from .const import DATA_INSTANCE, DOMAIN, MAX_ROWS_TO_PURGE, SupportedDialect +from .db_schema import Statistics, StatisticsMeta, StatisticsRuns, StatisticsShortTerm from .models import ( StatisticData, StatisticMetaData, StatisticResult, - Statistics, - StatisticsMeta, - StatisticsRuns, - StatisticsShortTerm, process_timestamp, process_timestamp_to_utc_isoformat, ) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 843f0e4b185..c1fbc831987 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -29,14 +29,13 @@ from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util from .const import DATA_INSTANCE, SQLITE_URL_PREFIX, SupportedDialect -from .models import ( +from .db_schema import ( TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES, TABLES_TO_CHECK, RecorderRuns, - UnsupportedDialect, - process_timestamp, ) +from .models import UnsupportedDialect, process_timestamp if TYPE_CHECKING: from . import Recorder diff --git a/tests/components/automation/test_recorder.py b/tests/components/automation/test_recorder.py index bfb02c0daba..4067393b76c 100644 --- a/tests/components/automation/test_recorder.py +++ b/tests/components/automation/test_recorder.py @@ -11,7 +11,7 @@ from homeassistant.components.automation import ( ATTR_MODE, CONF_ID, ) -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME from homeassistant.core import State diff --git a/tests/components/camera/test_recorder.py b/tests/components/camera/test_recorder.py index 0dc161fb0c0..1217997a996 100644 --- a/tests/components/camera/test_recorder.py +++ b/tests/components/camera/test_recorder.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta from homeassistant.components import camera -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ( ATTR_ATTRIBUTION, diff --git a/tests/components/climate/test_recorder.py b/tests/components/climate/test_recorder.py index 427645fb871..7ed604495dc 100644 --- a/tests/components/climate/test_recorder.py +++ b/tests/components/climate/test_recorder.py @@ -15,7 +15,7 @@ from homeassistant.components.climate.const import ( ATTR_SWING_MODES, ATTR_TARGET_TEMP_STEP, ) -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import State diff --git a/tests/components/fan/test_recorder.py b/tests/components/fan/test_recorder.py index aa5bae45f4c..604f5e3a2e9 100644 --- a/tests/components/fan/test_recorder.py +++ b/tests/components/fan/test_recorder.py @@ -5,7 +5,7 @@ from datetime import timedelta from homeassistant.components import fan from homeassistant.components.fan import ATTR_PRESET_MODES -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import State diff --git a/tests/components/group/test_recorder.py b/tests/components/group/test_recorder.py index fb68d9d3d43..7a4a41839ef 100644 --- a/tests/components/group/test_recorder.py +++ b/tests/components/group/test_recorder.py @@ -5,7 +5,7 @@ from datetime import timedelta from homeassistant.components import group from homeassistant.components.group import ATTR_AUTO, ATTR_ENTITY_ID, ATTR_ORDER -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_ON from homeassistant.core import State diff --git a/tests/components/humidifier/test_recorder.py b/tests/components/humidifier/test_recorder.py index ce694fc221b..28859e6133f 100644 --- a/tests/components/humidifier/test_recorder.py +++ b/tests/components/humidifier/test_recorder.py @@ -9,7 +9,7 @@ from homeassistant.components.humidifier import ( ATTR_MAX_HUMIDITY, ATTR_MIN_HUMIDITY, ) -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import State diff --git a/tests/components/input_boolean/test_recorder.py b/tests/components/input_boolean/test_recorder.py index c01c2532953..e7f68379343 100644 --- a/tests/components/input_boolean/test_recorder.py +++ b/tests/components/input_boolean/test_recorder.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta from homeassistant.components.input_boolean import DOMAIN -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ATTR_EDITABLE from homeassistant.core import HomeAssistant, State diff --git a/tests/components/input_button/test_recorder.py b/tests/components/input_button/test_recorder.py index eb5bcc05cf3..e469536549a 100644 --- a/tests/components/input_button/test_recorder.py +++ b/tests/components/input_button/test_recorder.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta from homeassistant.components.input_button import DOMAIN -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ATTR_EDITABLE from homeassistant.core import HomeAssistant, State diff --git a/tests/components/input_datetime/test_recorder.py b/tests/components/input_datetime/test_recorder.py index e8da8939ea9..bbdd0446e56 100644 --- a/tests/components/input_datetime/test_recorder.py +++ b/tests/components/input_datetime/test_recorder.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta from homeassistant.components.input_datetime import CONF_HAS_DATE, CONF_HAS_TIME, DOMAIN -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ATTR_EDITABLE from homeassistant.core import HomeAssistant, State diff --git a/tests/components/input_number/test_recorder.py b/tests/components/input_number/test_recorder.py index 9db2e2cd9c8..f736d450e7a 100644 --- a/tests/components/input_number/test_recorder.py +++ b/tests/components/input_number/test_recorder.py @@ -10,7 +10,7 @@ from homeassistant.components.input_number import ( ATTR_STEP, DOMAIN, ) -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ATTR_EDITABLE from homeassistant.core import HomeAssistant, State diff --git a/tests/components/input_select/test_recorder.py b/tests/components/input_select/test_recorder.py index 3a5ae4e385f..2931132bafc 100644 --- a/tests/components/input_select/test_recorder.py +++ b/tests/components/input_select/test_recorder.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta from homeassistant.components.input_select import ATTR_OPTIONS, DOMAIN -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ATTR_EDITABLE from homeassistant.core import HomeAssistant, State diff --git a/tests/components/input_text/test_recorder.py b/tests/components/input_text/test_recorder.py index f613bbcebe1..928399cd939 100644 --- a/tests/components/input_text/test_recorder.py +++ b/tests/components/input_text/test_recorder.py @@ -11,7 +11,7 @@ from homeassistant.components.input_text import ( DOMAIN, MODE_TEXT, ) -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ATTR_EDITABLE from homeassistant.core import HomeAssistant, State diff --git a/tests/components/light/test_recorder.py b/tests/components/light/test_recorder.py index 7e004891bb8..b6d26306317 100644 --- a/tests/components/light/test_recorder.py +++ b/tests/components/light/test_recorder.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ( ATTR_MIN_MIREDS, ATTR_SUPPORTED_COLOR_MODES, ) -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import State diff --git a/tests/components/media_player/test_recorder.py b/tests/components/media_player/test_recorder.py index 7f6b15768f2..1d053a23cee 100644 --- a/tests/components/media_player/test_recorder.py +++ b/tests/components/media_player/test_recorder.py @@ -11,7 +11,7 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_POSITION_UPDATED_AT, ATTR_SOUND_MODE_LIST, ) -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME from homeassistant.core import State diff --git a/tests/components/number/test_recorder.py b/tests/components/number/test_recorder.py index 1f5d39ed5e9..f51d3933b5d 100644 --- a/tests/components/number/test_recorder.py +++ b/tests/components/number/test_recorder.py @@ -5,7 +5,7 @@ from datetime import timedelta from homeassistant.components import number from homeassistant.components.number import ATTR_MAX, ATTR_MIN, ATTR_MODE, ATTR_STEP -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import State diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index 39cde4c2e7c..20df89eca5b 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -14,13 +14,13 @@ from homeassistant import core as ha from homeassistant.components import recorder from homeassistant.components.recorder import get_instance, statistics from homeassistant.components.recorder.core import Recorder -from homeassistant.components.recorder.models import RecorderRuns +from homeassistant.components.recorder.db_schema import RecorderRuns from homeassistant.components.recorder.tasks import RecorderTask, StatisticsTask from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed, fire_time_changed -from tests.components.recorder import models_schema_0 +from tests.components.recorder import db_schema_0 DEFAULT_PURGE_TASKS = 3 @@ -122,7 +122,7 @@ def create_engine_test(*args, **kwargs): This simulates an existing db with the old schema. """ engine = create_engine(*args, **kwargs) - models_schema_0.Base.metadata.create_all(engine) + db_schema_0.Base.metadata.create_all(engine) return engine diff --git a/tests/components/recorder/models_schema_0.py b/tests/components/recorder/db_schema_0.py similarity index 100% rename from tests/components/recorder/models_schema_0.py rename to tests/components/recorder/db_schema_0.py diff --git a/tests/components/recorder/models_schema_16.py b/tests/components/recorder/db_schema_16.py similarity index 100% rename from tests/components/recorder/models_schema_16.py rename to tests/components/recorder/db_schema_16.py diff --git a/tests/components/recorder/models_schema_18.py b/tests/components/recorder/db_schema_18.py similarity index 100% rename from tests/components/recorder/models_schema_18.py rename to tests/components/recorder/db_schema_18.py diff --git a/tests/components/recorder/models_schema_22.py b/tests/components/recorder/db_schema_22.py similarity index 100% rename from tests/components/recorder/models_schema_22.py rename to tests/components/recorder/db_schema_22.py diff --git a/tests/components/recorder/models_schema_23.py b/tests/components/recorder/db_schema_23.py similarity index 100% rename from tests/components/recorder/models_schema_23.py rename to tests/components/recorder/db_schema_23.py diff --git a/tests/components/recorder/models_schema_23_with_newer_columns.py b/tests/components/recorder/db_schema_23_with_newer_columns.py similarity index 100% rename from tests/components/recorder/models_schema_23_with_newer_columns.py rename to tests/components/recorder/db_schema_23_with_newer_columns.py diff --git a/tests/components/recorder/models_schema_28.py b/tests/components/recorder/db_schema_28.py similarity index 100% rename from tests/components/recorder/models_schema_28.py rename to tests/components/recorder/db_schema_28.py diff --git a/tests/components/recorder/test_filters_with_entityfilter.py b/tests/components/recorder/test_filters_with_entityfilter.py index 0758d6fdc95..ed4d4efe066 100644 --- a/tests/components/recorder/test_filters_with_entityfilter.py +++ b/tests/components/recorder/test_filters_with_entityfilter.py @@ -5,12 +5,12 @@ from sqlalchemy import select from sqlalchemy.engine.row import Row from homeassistant.components.recorder import get_instance +from homeassistant.components.recorder.db_schema import EventData, States from homeassistant.components.recorder.filters import ( Filters, extract_include_exclude_filter_conf, sqlalchemy_filter_from_include_exclude_conf, ) -from homeassistant.components.recorder.models import EventData, States from homeassistant.components.recorder.util import session_scope from homeassistant.const import ATTR_ENTITY_ID, STATE_ON from homeassistant.core import HomeAssistant diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index ee02ffbec49..cc1d8e7faa7 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -12,14 +12,13 @@ from sqlalchemy import text from homeassistant.components import recorder from homeassistant.components.recorder import history -from homeassistant.components.recorder.models import ( +from homeassistant.components.recorder.db_schema import ( Events, - LazyState, RecorderRuns, StateAttributes, States, - process_timestamp, ) +from homeassistant.components.recorder.models import LazyState, process_timestamp from homeassistant.components.recorder.util import session_scope import homeassistant.core as ha from homeassistant.core import HomeAssistant, State diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 87dbce3ba3b..3e25a54e39d 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -25,7 +25,7 @@ from homeassistant.components.recorder import ( get_instance, ) from homeassistant.components.recorder.const import DATA_INSTANCE, KEEPALIVE_TIME -from homeassistant.components.recorder.models import ( +from homeassistant.components.recorder.db_schema import ( SCHEMA_VERSION, EventData, Events, @@ -33,8 +33,8 @@ from homeassistant.components.recorder.models import ( StateAttributes, States, StatisticsRuns, - process_timestamp, ) +from homeassistant.components.recorder.models import process_timestamp from homeassistant.components.recorder.services import ( SERVICE_DISABLE, SERVICE_ENABLE, diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index fcc35938088..38d6a191809 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -20,9 +20,9 @@ from sqlalchemy.pool import StaticPool from homeassistant.bootstrap import async_setup_component from homeassistant.components import persistent_notification as pn, recorder -from homeassistant.components.recorder import migration, models +from homeassistant.components.recorder import db_schema, migration from homeassistant.components.recorder.const import DATA_INSTANCE -from homeassistant.components.recorder.models import ( +from homeassistant.components.recorder.db_schema import ( SCHEMA_VERSION, RecorderRuns, States, @@ -66,7 +66,7 @@ async def test_schema_update_calls(hass): update.assert_has_calls( [ call(hass, engine, session_maker, version + 1, 0) - for version in range(0, models.SCHEMA_VERSION) + for version in range(0, db_schema.SCHEMA_VERSION) ] ) @@ -267,14 +267,16 @@ async def test_schema_migrate(hass, start_version): This simulates an existing db with the old schema. """ - module = f"tests.components.recorder.models_schema_{str(start_version)}" + module = f"tests.components.recorder.db_schema_{str(start_version)}" importlib.import_module(module) old_models = sys.modules[module] engine = create_engine(*args, **kwargs) old_models.Base.metadata.create_all(engine) if start_version > 0: with Session(engine) as session: - session.add(recorder.models.SchemaChanges(schema_version=start_version)) + session.add( + recorder.db_schema.SchemaChanges(schema_version=start_version) + ) session.commit() return engine @@ -299,8 +301,8 @@ async def test_schema_migrate(hass, start_version): # the recorder will silently create a new database. with session_scope(hass=hass) as session: res = ( - session.query(models.SchemaChanges) - .order_by(models.SchemaChanges.change_id.desc()) + session.query(db_schema.SchemaChanges) + .order_by(db_schema.SchemaChanges.change_id.desc()) .first() ) migration_version = res.schema_version @@ -325,7 +327,7 @@ async def test_schema_migrate(hass, start_version): await hass.async_block_till_done() await hass.async_add_executor_job(migration_done.wait) await async_wait_recording_done(hass) - assert migration_version == models.SCHEMA_VERSION + assert migration_version == db_schema.SCHEMA_VERSION assert setup_run.called assert recorder.util.async_migration_in_progress(hass) is not True @@ -381,7 +383,7 @@ def test_forgiving_add_column(): def test_forgiving_add_index(): """Test that add index will continue if index exists.""" engine = create_engine("sqlite://", poolclass=StaticPool) - models.Base.metadata.create_all(engine) + db_schema.Base.metadata.create_all(engine) with Session(engine) as session: instance = Mock() instance.get_session = Mock(return_value=session) diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index 9d07c33a17a..81469ab1dab 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -7,14 +7,16 @@ import pytest from sqlalchemy import create_engine from sqlalchemy.orm import scoped_session, sessionmaker -from homeassistant.components.recorder.models import ( +from homeassistant.components.recorder.db_schema import ( Base, EventData, Events, - LazyState, RecorderRuns, StateAttributes, States, +) +from homeassistant.components.recorder.models import ( + LazyState, process_datetime_to_timestamp, process_timestamp, process_timestamp_to_utc_isoformat, diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index f4e998c5388..c6c447c01c9 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -10,7 +10,7 @@ from sqlalchemy.orm.session import Session from homeassistant.components import recorder from homeassistant.components.recorder.const import MAX_ROWS_TO_PURGE, SupportedDialect -from homeassistant.components.recorder.models import ( +from homeassistant.components.recorder.db_schema import ( EventData, Events, RecorderRuns, diff --git a/tests/components/recorder/test_run_history.py b/tests/components/recorder/test_run_history.py index 80797c666ec..ff4a5e5d701 100644 --- a/tests/components/recorder/test_run_history.py +++ b/tests/components/recorder/test_run_history.py @@ -3,7 +3,8 @@ from datetime import timedelta from homeassistant.components import recorder -from homeassistant.components.recorder.models import RecorderRuns, process_timestamp +from homeassistant.components.recorder.db_schema import RecorderRuns +from homeassistant.components.recorder.models import process_timestamp from homeassistant.util import dt as dt_util diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 97e64716f49..48639790d0d 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -13,10 +13,8 @@ from sqlalchemy.orm import Session from homeassistant.components import recorder from homeassistant.components.recorder import history, statistics from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX -from homeassistant.components.recorder.models import ( - StatisticsShortTerm, - process_timestamp_to_utc_isoformat, -) +from homeassistant.components.recorder.db_schema import StatisticsShortTerm +from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat from homeassistant.components.recorder.statistics import ( async_add_external_statistics, delete_statistics_duplicates, @@ -390,7 +388,7 @@ def test_rename_entity_collision(hass_recorder, caplog): } with session_scope(hass=hass) as session: - session.add(recorder.models.StatisticsMeta.from_meta(metadata_1)) + session.add(recorder.db_schema.StatisticsMeta.from_meta(metadata_1)) # Rename entity sensor.test1 to sensor.test99 @callback @@ -941,7 +939,7 @@ def test_duplicate_statistics_handle_integrity_error(hass_recorder, caplog): assert insert_statistics_mock.call_count == 3 with session_scope(hass=hass) as session: - tmp = session.query(recorder.models.Statistics).all() + tmp = session.query(recorder.db_schema.Statistics).all() assert len(tmp) == 2 assert "Blocked attempt to insert duplicated statistic rows" in caplog.text @@ -952,15 +950,19 @@ def _create_engine_28(*args, **kwargs): This simulates an existing db with the old schema. """ - module = "tests.components.recorder.models_schema_28" + module = "tests.components.recorder.db_schema_28" importlib.import_module(module) - old_models = sys.modules[module] + old_db_schema = sys.modules[module] engine = create_engine(*args, **kwargs) - old_models.Base.metadata.create_all(engine) + old_db_schema.Base.metadata.create_all(engine) with Session(engine) as session: - session.add(recorder.models.StatisticsRuns(start=statistics.get_start_time())) session.add( - recorder.models.SchemaChanges(schema_version=old_models.SCHEMA_VERSION) + recorder.db_schema.StatisticsRuns(start=statistics.get_start_time()) + ) + session.add( + recorder.db_schema.SchemaChanges( + schema_version=old_db_schema.SCHEMA_VERSION + ) ) session.commit() return engine @@ -971,9 +973,9 @@ def test_delete_metadata_duplicates(caplog, tmpdir): test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" - module = "tests.components.recorder.models_schema_28" + module = "tests.components.recorder.db_schema_28" importlib.import_module(module) - old_models = sys.modules[module] + old_db_schema = sys.modules[module] external_energy_metadata_1 = { "has_mean": False, @@ -1001,8 +1003,8 @@ def test_delete_metadata_duplicates(caplog, tmpdir): } # Create some duplicated statistics_meta with schema version 28 - with patch.object(recorder, "models", old_models), patch.object( - recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION + with patch.object(recorder, "db_schema", old_db_schema), patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch( "homeassistant.components.recorder.core.create_engine", new=_create_engine_28 ): @@ -1013,15 +1015,17 @@ def test_delete_metadata_duplicates(caplog, tmpdir): with session_scope(hass=hass) as session: session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) + recorder.db_schema.StatisticsMeta.from_meta(external_energy_metadata_1) ) session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) + recorder.db_schema.StatisticsMeta.from_meta(external_energy_metadata_2) + ) + session.add( + recorder.db_schema.StatisticsMeta.from_meta(external_co2_metadata) ) - session.add(recorder.models.StatisticsMeta.from_meta(external_co2_metadata)) with session_scope(hass=hass) as session: - tmp = session.query(recorder.models.StatisticsMeta).all() + tmp = session.query(recorder.db_schema.StatisticsMeta).all() assert len(tmp) == 3 assert tmp[0].id == 1 assert tmp[0].statistic_id == "test:total_energy_import_tariff_1" @@ -1042,7 +1046,7 @@ def test_delete_metadata_duplicates(caplog, tmpdir): assert "Deleted 1 duplicated statistics_meta rows" in caplog.text with session_scope(hass=hass) as session: - tmp = session.query(recorder.models.StatisticsMeta).all() + tmp = session.query(recorder.db_schema.StatisticsMeta).all() assert len(tmp) == 2 assert tmp[0].id == 2 assert tmp[0].statistic_id == "test:total_energy_import_tariff_1" @@ -1058,9 +1062,9 @@ def test_delete_metadata_duplicates_many(caplog, tmpdir): test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" - module = "tests.components.recorder.models_schema_28" + module = "tests.components.recorder.db_schema_28" importlib.import_module(module) - old_models = sys.modules[module] + old_db_schema = sys.modules[module] external_energy_metadata_1 = { "has_mean": False, @@ -1088,8 +1092,8 @@ def test_delete_metadata_duplicates_many(caplog, tmpdir): } # Create some duplicated statistics with schema version 28 - with patch.object(recorder, "models", old_models), patch.object( - recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION + with patch.object(recorder, "db_schema", old_db_schema), patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch( "homeassistant.components.recorder.core.create_engine", new=_create_engine_28 ): @@ -1100,20 +1104,26 @@ def test_delete_metadata_duplicates_many(caplog, tmpdir): with session_scope(hass=hass) as session: session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) + recorder.db_schema.StatisticsMeta.from_meta(external_energy_metadata_1) ) for _ in range(3000): session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) + recorder.db_schema.StatisticsMeta.from_meta( + external_energy_metadata_1 + ) ) session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) + recorder.db_schema.StatisticsMeta.from_meta(external_energy_metadata_2) ) session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) + recorder.db_schema.StatisticsMeta.from_meta(external_energy_metadata_2) + ) + session.add( + recorder.db_schema.StatisticsMeta.from_meta(external_co2_metadata) + ) + session.add( + recorder.db_schema.StatisticsMeta.from_meta(external_co2_metadata) ) - session.add(recorder.models.StatisticsMeta.from_meta(external_co2_metadata)) - session.add(recorder.models.StatisticsMeta.from_meta(external_co2_metadata)) hass.stop() dt_util.DEFAULT_TIME_ZONE = ORIG_TZ @@ -1127,7 +1137,7 @@ def test_delete_metadata_duplicates_many(caplog, tmpdir): assert "Deleted 3002 duplicated statistics_meta rows" in caplog.text with session_scope(hass=hass) as session: - tmp = session.query(recorder.models.StatisticsMeta).all() + tmp = session.query(recorder.db_schema.StatisticsMeta).all() assert len(tmp) == 3 assert tmp[0].id == 3001 assert tmp[0].statistic_id == "test:total_energy_import_tariff_1" diff --git a/tests/components/recorder/test_statistics_v23_migration.py b/tests/components/recorder/test_statistics_v23_migration.py index d487743a87f..50311a987d6 100644 --- a/tests/components/recorder/test_statistics_v23_migration.py +++ b/tests/components/recorder/test_statistics_v23_migration.py @@ -25,7 +25,7 @@ from tests.components.recorder.common import wait_recording_done ORIG_TZ = dt_util.DEFAULT_TIME_ZONE CREATE_ENGINE_TARGET = "homeassistant.components.recorder.core.create_engine" -SCHEMA_MODULE = "tests.components.recorder.models_schema_23_with_newer_columns" +SCHEMA_MODULE = "tests.components.recorder.db_schema_23_with_newer_columns" def _create_engine_test(*args, **kwargs): @@ -34,13 +34,17 @@ def _create_engine_test(*args, **kwargs): This simulates an existing db with the old schema. """ importlib.import_module(SCHEMA_MODULE) - old_models = sys.modules[SCHEMA_MODULE] + old_db_schema = sys.modules[SCHEMA_MODULE] engine = create_engine(*args, **kwargs) - old_models.Base.metadata.create_all(engine) + old_db_schema.Base.metadata.create_all(engine) with Session(engine) as session: - session.add(recorder.models.StatisticsRuns(start=statistics.get_start_time())) session.add( - recorder.models.SchemaChanges(schema_version=old_models.SCHEMA_VERSION) + recorder.db_schema.StatisticsRuns(start=statistics.get_start_time()) + ) + session.add( + recorder.db_schema.SchemaChanges( + schema_version=old_db_schema.SCHEMA_VERSION + ) ) session.commit() return engine @@ -52,7 +56,7 @@ def test_delete_duplicates(caplog, tmpdir): dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" importlib.import_module(SCHEMA_MODULE) - old_models = sys.modules[SCHEMA_MODULE] + old_db_schema = sys.modules[SCHEMA_MODULE] period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) @@ -171,8 +175,8 @@ def test_delete_duplicates(caplog, tmpdir): } # Create some duplicated statistics with schema version 23 - with patch.object(recorder, "models", old_models), patch.object( - recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION + with patch.object(recorder, "db_schema", old_db_schema), patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): hass = get_test_home_assistant() setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) @@ -181,19 +185,21 @@ def test_delete_duplicates(caplog, tmpdir): with session_scope(hass=hass) as session: session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) + recorder.db_schema.StatisticsMeta.from_meta(external_energy_metadata_1) ) session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) + recorder.db_schema.StatisticsMeta.from_meta(external_energy_metadata_2) + ) + session.add( + recorder.db_schema.StatisticsMeta.from_meta(external_co2_metadata) ) - session.add(recorder.models.StatisticsMeta.from_meta(external_co2_metadata)) with session_scope(hass=hass) as session: for stat in external_energy_statistics_1: - session.add(recorder.models.Statistics.from_stats(1, stat)) + session.add(recorder.db_schema.Statistics.from_stats(1, stat)) for stat in external_energy_statistics_2: - session.add(recorder.models.Statistics.from_stats(2, stat)) + session.add(recorder.db_schema.Statistics.from_stats(2, stat)) for stat in external_co2_statistics: - session.add(recorder.models.Statistics.from_stats(3, stat)) + session.add(recorder.db_schema.Statistics.from_stats(3, stat)) hass.stop() dt_util.DEFAULT_TIME_ZONE = ORIG_TZ @@ -218,7 +224,7 @@ def test_delete_duplicates_many(caplog, tmpdir): dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" importlib.import_module(SCHEMA_MODULE) - old_models = sys.modules[SCHEMA_MODULE] + old_db_schema = sys.modules[SCHEMA_MODULE] period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) @@ -337,8 +343,8 @@ def test_delete_duplicates_many(caplog, tmpdir): } # Create some duplicated statistics with schema version 23 - with patch.object(recorder, "models", old_models), patch.object( - recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION + with patch.object(recorder, "db_schema", old_db_schema), patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): hass = get_test_home_assistant() setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) @@ -347,25 +353,27 @@ def test_delete_duplicates_many(caplog, tmpdir): with session_scope(hass=hass) as session: session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) + recorder.db_schema.StatisticsMeta.from_meta(external_energy_metadata_1) ) session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) + recorder.db_schema.StatisticsMeta.from_meta(external_energy_metadata_2) + ) + session.add( + recorder.db_schema.StatisticsMeta.from_meta(external_co2_metadata) ) - session.add(recorder.models.StatisticsMeta.from_meta(external_co2_metadata)) with session_scope(hass=hass) as session: for stat in external_energy_statistics_1: - session.add(recorder.models.Statistics.from_stats(1, stat)) + session.add(recorder.db_schema.Statistics.from_stats(1, stat)) for _ in range(3000): session.add( - recorder.models.Statistics.from_stats( + recorder.db_schema.Statistics.from_stats( 1, external_energy_statistics_1[-1] ) ) for stat in external_energy_statistics_2: - session.add(recorder.models.Statistics.from_stats(2, stat)) + session.add(recorder.db_schema.Statistics.from_stats(2, stat)) for stat in external_co2_statistics: - session.add(recorder.models.Statistics.from_stats(3, stat)) + session.add(recorder.db_schema.Statistics.from_stats(3, stat)) hass.stop() dt_util.DEFAULT_TIME_ZONE = ORIG_TZ @@ -391,7 +399,7 @@ def test_delete_duplicates_non_identical(caplog, tmpdir): dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" importlib.import_module(SCHEMA_MODULE) - old_models = sys.modules[SCHEMA_MODULE] + old_db_schema = sys.modules[SCHEMA_MODULE] period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) @@ -480,8 +488,8 @@ def test_delete_duplicates_non_identical(caplog, tmpdir): } # Create some duplicated statistics with schema version 23 - with patch.object(recorder, "models", old_models), patch.object( - recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION + with patch.object(recorder, "db_schema", old_db_schema), patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): hass = get_test_home_assistant() setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) @@ -490,16 +498,16 @@ def test_delete_duplicates_non_identical(caplog, tmpdir): with session_scope(hass=hass) as session: session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) + recorder.db_schema.StatisticsMeta.from_meta(external_energy_metadata_1) ) session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_2) + recorder.db_schema.StatisticsMeta.from_meta(external_energy_metadata_2) ) with session_scope(hass=hass) as session: for stat in external_energy_statistics_1: - session.add(recorder.models.Statistics.from_stats(1, stat)) + session.add(recorder.db_schema.Statistics.from_stats(1, stat)) for stat in external_energy_statistics_2: - session.add(recorder.models.Statistics.from_stats(2, stat)) + session.add(recorder.db_schema.Statistics.from_stats(2, stat)) hass.stop() dt_util.DEFAULT_TIME_ZONE = ORIG_TZ @@ -560,7 +568,7 @@ def test_delete_duplicates_short_term(caplog, tmpdir): dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" importlib.import_module(SCHEMA_MODULE) - old_models = sys.modules[SCHEMA_MODULE] + old_db_schema = sys.modules[SCHEMA_MODULE] period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) @@ -580,8 +588,8 @@ def test_delete_duplicates_short_term(caplog, tmpdir): } # Create some duplicated statistics with schema version 23 - with patch.object(recorder, "models", old_models), patch.object( - recorder.migration, "SCHEMA_VERSION", old_models.SCHEMA_VERSION + with patch.object(recorder, "db_schema", old_db_schema), patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): hass = get_test_home_assistant() setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) @@ -590,14 +598,14 @@ def test_delete_duplicates_short_term(caplog, tmpdir): with session_scope(hass=hass) as session: session.add( - recorder.models.StatisticsMeta.from_meta(external_energy_metadata_1) + recorder.db_schema.StatisticsMeta.from_meta(external_energy_metadata_1) ) with session_scope(hass=hass) as session: session.add( - recorder.models.StatisticsShortTerm.from_stats(1, statistic_row) + recorder.db_schema.StatisticsShortTerm.from_stats(1, statistic_row) ) session.add( - recorder.models.StatisticsShortTerm.from_stats(1, statistic_row) + recorder.db_schema.StatisticsShortTerm.from_stats(1, statistic_row) ) hass.stop() diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 343c57045cf..8624719f951 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -14,7 +14,8 @@ from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components import recorder from homeassistant.components.recorder import history, util from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX -from homeassistant.components.recorder.models import RecorderRuns, UnsupportedDialect +from homeassistant.components.recorder.db_schema import RecorderRuns +from homeassistant.components.recorder.models import UnsupportedDialect from homeassistant.components.recorder.util import ( end_incomplete_runs, is_second_sunday, diff --git a/tests/components/script/test_recorder.py b/tests/components/script/test_recorder.py index 0dc7bd54746..a023212b82b 100644 --- a/tests/components/script/test_recorder.py +++ b/tests/components/script/test_recorder.py @@ -4,7 +4,7 @@ from __future__ import annotations import pytest from homeassistant.components import script -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.components.script import ( ATTR_CUR, diff --git a/tests/components/select/test_recorder.py b/tests/components/select/test_recorder.py index f48679a43f1..083caef3444 100644 --- a/tests/components/select/test_recorder.py +++ b/tests/components/select/test_recorder.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta from homeassistant.components import select -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.components.select import ATTR_OPTIONS from homeassistant.const import ATTR_FRIENDLY_NAME diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 66ed0032201..c62d1309c7a 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -11,10 +11,8 @@ 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 ( - StatisticsMeta, - process_timestamp_to_utc_isoformat, -) +from homeassistant.components.recorder.db_schema import StatisticsMeta +from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat from homeassistant.components.recorder.statistics import ( get_metadata, list_statistic_ids, @@ -2287,7 +2285,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): 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 + "homeassistant.components.recorder.db_schema.dt_util.utcnow", return_value=zero ): hass = hass_recorder() # Remove this after dropping the use of the hass_recorder fixture diff --git a/tests/components/siren/test_recorder.py b/tests/components/siren/test_recorder.py index 46e066e4873..aaf1679478a 100644 --- a/tests/components/siren/test_recorder.py +++ b/tests/components/siren/test_recorder.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta from homeassistant.components import siren -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.components.siren import ATTR_AVAILABLE_TONES from homeassistant.const import ATTR_FRIENDLY_NAME diff --git a/tests/components/sun/test_recorder.py b/tests/components/sun/test_recorder.py index 0b0d0ad48cf..547bf44ec5f 100644 --- a/tests/components/sun/test_recorder.py +++ b/tests/components/sun/test_recorder.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.components.sun import ( DOMAIN, diff --git a/tests/components/update/test_recorder.py b/tests/components/update/test_recorder.py index d340a8dfa3f..d1263a720af 100644 --- a/tests/components/update/test_recorder.py +++ b/tests/components/update/test_recorder.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.components.update.const import ( ATTR_IN_PROGRESS, diff --git a/tests/components/vacuum/test_recorder.py b/tests/components/vacuum/test_recorder.py index 6267091b984..040cc9105aa 100644 --- a/tests/components/vacuum/test_recorder.py +++ b/tests/components/vacuum/test_recorder.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta from homeassistant.components import vacuum -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.components.vacuum import ATTR_FAN_SPEED_LIST from homeassistant.const import ATTR_FRIENDLY_NAME diff --git a/tests/components/water_heater/test_recorder.py b/tests/components/water_heater/test_recorder.py index 4a70fc12c8f..b6670152e3f 100644 --- a/tests/components/water_heater/test_recorder.py +++ b/tests/components/water_heater/test_recorder.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta from homeassistant.components import water_heater -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.components.water_heater import ( ATTR_MAX_TEMP, diff --git a/tests/components/weather/test_recorder.py b/tests/components/weather/test_recorder.py index 9f2e5289013..ef1998f734c 100644 --- a/tests/components/weather/test_recorder.py +++ b/tests/components/weather/test_recorder.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta -from homeassistant.components.recorder.models import StateAttributes, States +from homeassistant.components.recorder.db_schema import StateAttributes, States from homeassistant.components.recorder.util import session_scope from homeassistant.components.weather import ATTR_FORECAST, DOMAIN from homeassistant.core import HomeAssistant, State From 68d67a3e4960f88ed1f1d5e2cb398cac59fcdaf4 Mon Sep 17 00:00:00 2001 From: Matrix Date: Tue, 7 Jun 2022 22:34:12 +0800 Subject: [PATCH 1325/3516] Add yolink valve controller support (#73111) --- homeassistant/components/yolink/const.py | 1 + homeassistant/components/yolink/sensor.py | 20 ++++++++++++----- homeassistant/components/yolink/switch.py | 27 +++++++++++++++++------ 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index 44f4f3104f7..b154fe9178d 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -21,3 +21,4 @@ ATTR_DEVICE_VIBRATION_SENSOR = "VibrationSensor" ATTR_DEVICE_OUTLET = "Outlet" ATTR_DEVICE_SIREN = "Siren" ATTR_DEVICE_LOCK = "Lock" +ATTR_DEVICE_MANIPULATOR = "Manipulator" diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index e0d746219fb..26dee7a493d 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -22,6 +22,7 @@ from .const import ( ATTR_COORDINATORS, ATTR_DEVICE_DOOR_SENSOR, ATTR_DEVICE_LOCK, + ATTR_DEVICE_MANIPULATOR, ATTR_DEVICE_MOTION_SENSOR, ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_VIBRATION_SENSOR, @@ -53,17 +54,28 @@ SENSOR_DEVICE_TYPE = [ ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_VIBRATION_SENSOR, ATTR_DEVICE_LOCK, + ATTR_DEVICE_MANIPULATOR, ] BATTERY_POWER_SENSOR = [ ATTR_DEVICE_DOOR_SENSOR, - ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_MOTION_SENSOR, + ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_VIBRATION_SENSOR, ATTR_DEVICE_LOCK, + ATTR_DEVICE_MANIPULATOR, ] +def cvt_battery(val: int | None) -> int | None: + """Convert battery to percentage.""" + if val is None: + return None + if val > 0: + return percentage.ordered_list_item_to_percentage([1, 2, 3, 4], val) + return 0 + + SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( YoLinkSensorEntityDescription( key="battery", @@ -71,11 +83,7 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, name="Battery", state_class=SensorStateClass.MEASUREMENT, - value=lambda value: percentage.ordered_list_item_to_percentage( - [1, 2, 3, 4], value - ) - if value is not None - else None, + value=cvt_battery, exists_fn=lambda device: device.device_type in BATTERY_POWER_SENSOR, ), YoLinkSensorEntityDescription( diff --git a/homeassistant/components/yolink/switch.py b/homeassistant/components/yolink/switch.py index 723043100b3..6733191b943 100644 --- a/homeassistant/components/yolink/switch.py +++ b/homeassistant/components/yolink/switch.py @@ -16,7 +16,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_COORDINATORS, ATTR_DEVICE_OUTLET, DOMAIN +from .const import ( + ATTR_COORDINATORS, + ATTR_DEVICE_MANIPULATOR, + ATTR_DEVICE_OUTLET, + DOMAIN, +) from .coordinator import YoLinkCoordinator from .entity import YoLinkEntity @@ -27,19 +32,27 @@ class YoLinkSwitchEntityDescription(SwitchEntityDescription): exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True value: Callable[[Any], bool | None] = lambda _: None + state_key: str = "state" DEVICE_TYPES: tuple[YoLinkSwitchEntityDescription, ...] = ( YoLinkSwitchEntityDescription( - key="state", + key="outlet_state", device_class=SwitchDeviceClass.OUTLET, name="State", value=lambda value: value == "open" if value is not None else None, exists_fn=lambda device: device.device_type in [ATTR_DEVICE_OUTLET], ), + YoLinkSwitchEntityDescription( + key="manipulator_state", + device_class=SwitchDeviceClass.SWITCH, + name="State", + value=lambda value: value == "open" if value is not None else None, + exists_fn=lambda device: device.device_type in [ATTR_DEVICE_MANIPULATOR], + ), ) -DEVICE_TYPE = [ATTR_DEVICE_OUTLET] +DEVICE_TYPE = [ATTR_DEVICE_MANIPULATOR, ATTR_DEVICE_OUTLET] async def async_setup_entry( @@ -47,7 +60,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up YoLink Sensor from a config entry.""" + """Set up YoLink switch from a config entry.""" device_coordinators = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATORS] switch_device_coordinators = [ device_coordinator @@ -77,7 +90,7 @@ class YoLinkSwitchEntity(YoLinkEntity, SwitchEntity): coordinator: YoLinkCoordinator, description: YoLinkSwitchEntityDescription, ) -> None: - """Init YoLink Outlet.""" + """Init YoLink switch.""" super().__init__(config_entry, coordinator) self.entity_description = description self._attr_unique_id = ( @@ -91,12 +104,12 @@ class YoLinkSwitchEntity(YoLinkEntity, SwitchEntity): def update_entity_state(self, state: dict[str, Any]) -> None: """Update HA Entity State.""" self._attr_is_on = self.entity_description.value( - state.get(self.entity_description.key) + state.get(self.entity_description.state_key) ) self.async_write_ha_state() async def call_state_change(self, state: str) -> None: - """Call setState api to change outlet state.""" + """Call setState api to change switch state.""" await self.call_device_api("setState", {"state": state}) self._attr_is_on = self.entity_description.value(state) self.async_write_ha_state() From c6b835dd91620484f8a4b4c6593f83f7da3850b7 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 7 Jun 2022 17:02:12 +0200 Subject: [PATCH 1326/3516] Add missing `state_class` to min_max sensors (#73169) Add missing state_class --- homeassistant/components/min_max/sensor.py | 11 ++++++++++- tests/components/min_max/test_sensor.py | 6 ++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index 5c117357729..99aec4e9e7b 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -6,7 +6,11 @@ import statistics import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, @@ -232,6 +236,11 @@ class MinMaxSensor(SensorEntity): """Return the icon to use in the frontend, if any.""" return ICON + @property + def state_class(self) -> SensorStateClass: + """Return the state class.""" + return SensorStateClass.MEASUREMENT + @callback def _async_min_max_sensor_state_listener(self, event, update_state=True): """Handle the sensor state changes.""" diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index e143b26e47f..72728ac20b6 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -4,6 +4,7 @@ from unittest.mock import patch from homeassistant import config as hass_config from homeassistant.components.min_max.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, @@ -76,6 +77,7 @@ async def test_min_sensor(hass): assert str(float(MIN_VALUE)) == state.state assert entity_ids[2] == state.attributes.get("min_entity_id") + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT async def test_max_sensor(hass): @@ -102,6 +104,7 @@ async def test_max_sensor(hass): assert str(float(MAX_VALUE)) == state.state assert entity_ids[1] == state.attributes.get("max_entity_id") + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT async def test_mean_sensor(hass): @@ -127,6 +130,7 @@ async def test_mean_sensor(hass): state = hass.states.get("sensor.test_mean") assert str(float(MEAN)) == state.state + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT async def test_mean_1_digit_sensor(hass): @@ -204,6 +208,7 @@ async def test_median_sensor(hass): state = hass.states.get("sensor.test_median") assert str(float(MEDIAN)) == state.state + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT async def test_not_enough_sensor_value(hass): @@ -327,6 +332,7 @@ async def test_last_sensor(hass): state = hass.states.get("sensor.test_last") assert str(float(value)) == state.state assert entity_id == state.attributes.get("last_entity_id") + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT async def test_reload(hass): From 73f2bca37782e54a19c19e6ca86ddc9eb98791e1 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 8 Jun 2022 02:10:53 +1000 Subject: [PATCH 1327/3516] Make Stream.stop() async (#73107) * Make Stream.start() async * Stop streams concurrently on shutdown Co-authored-by: Martin Hjelmare --- homeassistant/components/camera/__init__.py | 4 +- homeassistant/components/nest/camera_sdm.py | 2 +- homeassistant/components/stream/__init__.py | 67 +++++++++++++-------- homeassistant/components/stream/core.py | 14 +++-- homeassistant/components/stream/hls.py | 4 +- tests/components/camera/test_init.py | 3 +- tests/components/nest/test_camera_sdm.py | 6 +- tests/components/stream/test_hls.py | 27 +++++---- tests/components/stream/test_ll_hls.py | 6 +- tests/components/stream/test_recorder.py | 10 +-- tests/components/stream/test_worker.py | 16 ++--- 11 files changed, 92 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 627da2d1872..2ed8b58232d 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -386,7 +386,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: continue stream.keepalive = True stream.add_provider("hls") - stream.start() + await stream.start() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, preload_stream) @@ -996,7 +996,7 @@ async def _async_stream_endpoint_url( stream.keepalive = camera_prefs.preload_stream stream.add_provider(fmt) - stream.start() + await stream.start() return stream.endpoint_url(fmt) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 6e14100e881..61f8ead4ea3 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -175,7 +175,7 @@ class NestCamera(Camera): # Next attempt to catch a url will get a new one self._stream = None if self.stream: - self.stream.stop() + await self.stream.stop() self.stream = None return # Update the stream worker with the latest valid url diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 895bdaf3201..c33188fd71c 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -16,6 +16,7 @@ to always keep workers active. """ from __future__ import annotations +import asyncio from collections.abc import Callable, Mapping import logging import re @@ -206,13 +207,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Setup Recorder async_setup_recorder(hass) - @callback - def shutdown(event: Event) -> None: + async def shutdown(event: Event) -> None: """Stop all stream workers.""" for stream in hass.data[DOMAIN][ATTR_STREAMS]: stream.keepalive = False - stream.stop() - _LOGGER.info("Stopped stream workers") + if awaitables := [ + asyncio.create_task(stream.stop()) + for stream in hass.data[DOMAIN][ATTR_STREAMS] + ]: + await asyncio.wait(awaitables) + _LOGGER.debug("Stopped stream workers") hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) @@ -236,6 +240,7 @@ class Stream: self._stream_label = stream_label self.keepalive = False self.access_token: str | None = None + self._start_stop_lock = asyncio.Lock() self._thread: threading.Thread | None = None self._thread_quit = threading.Event() self._outputs: dict[str, StreamOutput] = {} @@ -271,12 +276,11 @@ class Stream: """Add provider output stream.""" if not (provider := self._outputs.get(fmt)): - @callback - def idle_callback() -> None: + async def idle_callback() -> None: if ( not self.keepalive or fmt == RECORDER_PROVIDER ) and fmt in self._outputs: - self.remove_provider(self._outputs[fmt]) + await self.remove_provider(self._outputs[fmt]) self.check_idle() provider = PROVIDERS[fmt]( @@ -286,14 +290,14 @@ class Stream: return provider - def remove_provider(self, provider: StreamOutput) -> None: + async def remove_provider(self, provider: StreamOutput) -> None: """Remove provider output stream.""" if provider.name in self._outputs: self._outputs[provider.name].cleanup() del self._outputs[provider.name] if not self._outputs: - self.stop() + await self.stop() def check_idle(self) -> None: """Reset access token if all providers are idle.""" @@ -316,9 +320,14 @@ class Stream: if self._update_callback: self._update_callback() - def start(self) -> None: - """Start a stream.""" - if self._thread is None or not self._thread.is_alive(): + async def start(self) -> None: + """Start a stream. + + Uses an asyncio.Lock to avoid conflicts with _stop(). + """ + async with self._start_stop_lock: + if self._thread and self._thread.is_alive(): + return if self._thread is not None: # The thread must have crashed/exited. Join to clean up the # previous thread. @@ -329,7 +338,7 @@ class Stream: target=self._run_worker, ) self._thread.start() - self._logger.info( + self._logger.debug( "Started stream: %s", redact_credentials(str(self.source)) ) @@ -394,33 +403,39 @@ class Stream: redact_credentials(str(self.source)), ) - @callback - def worker_finished() -> None: + async def worker_finished() -> None: # The worker is no checking availability of the stream and can no longer track # availability so mark it as available, otherwise the frontend may not be able to # interact with the stream. if not self.available: self._async_update_state(True) + # We can call remove_provider() sequentially as the wrapped _stop() function + # which blocks internally is only called when the last provider is removed. for provider in self.outputs().values(): - self.remove_provider(provider) + await self.remove_provider(provider) - self.hass.loop.call_soon_threadsafe(worker_finished) + self.hass.create_task(worker_finished()) - def stop(self) -> None: + async def stop(self) -> None: """Remove outputs and access token.""" self._outputs = {} self.access_token = None if not self.keepalive: - self._stop() + await self._stop() - def _stop(self) -> None: - """Stop worker thread.""" - if self._thread is not None: + async def _stop(self) -> None: + """Stop worker thread. + + Uses an asyncio.Lock to avoid conflicts with start(). + """ + async with self._start_stop_lock: + if self._thread is None: + return self._thread_quit.set() - self._thread.join() + await self.hass.async_add_executor_job(self._thread.join) self._thread = None - self._logger.info( + self._logger.debug( "Stopped stream: %s", redact_credentials(str(self.source)) ) @@ -448,7 +463,7 @@ class Stream: ) recorder.video_path = video_path - self.start() + await self.start() self._logger.debug("Started a stream recording of %s seconds", duration) # Take advantage of lookback @@ -473,7 +488,7 @@ class Stream: """ self.add_provider(HLS_PROVIDER) - self.start() + await self.start() return await self._keyframe_converter.async_get_image( width=width, height=height ) diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 8c0b867752e..da18a5a6a08 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -3,9 +3,9 @@ from __future__ import annotations import asyncio from collections import deque -from collections.abc import Iterable +from collections.abc import Callable, Coroutine, Iterable import datetime -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from aiohttp import web import async_timeout @@ -192,7 +192,10 @@ class IdleTimer: """ def __init__( - self, hass: HomeAssistant, timeout: int, idle_callback: CALLBACK_TYPE + self, + hass: HomeAssistant, + timeout: int, + idle_callback: Callable[[], Coroutine[Any, Any, None]], ) -> None: """Initialize IdleTimer.""" self._hass = hass @@ -219,11 +222,12 @@ class IdleTimer: if self._unsub is not None: self._unsub() + @callback def fire(self, _now: datetime.datetime) -> None: """Invoke the idle timeout callback, called when the alarm fires.""" self.idle = True self._unsub = None - self._callback() + self._hass.async_create_task(self._callback()) class StreamOutput: @@ -349,7 +353,7 @@ class StreamView(HomeAssistantView): raise web.HTTPNotFound() # Start worker if not already started - stream.start() + await stream.start() return await self.handle(request, stream, sequence, part_num) diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 23584b59fb9..8e78093d07a 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -117,7 +117,7 @@ class HlsMasterPlaylistView(StreamView): ) -> web.Response: """Return m3u8 playlist.""" track = stream.add_provider(HLS_PROVIDER) - stream.start() + await stream.start() # Make sure at least two segments are ready (last one may not be complete) if not track.sequences and not await track.recv(): return web.HTTPNotFound() @@ -232,7 +232,7 @@ class HlsPlaylistView(StreamView): track: HlsStreamOutput = cast( HlsStreamOutput, stream.add_provider(HLS_PROVIDER) ) - stream.start() + await stream.start() hls_msn: str | int | None = request.query.get("_HLS_msn") hls_part: str | int | None = request.query.get("_HLS_part") diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 1b30facf1de..ba13bbd6c52 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -3,7 +3,7 @@ import asyncio import base64 from http import HTTPStatus import io -from unittest.mock import Mock, PropertyMock, mock_open, patch +from unittest.mock import AsyncMock, Mock, PropertyMock, mock_open, patch import pytest @@ -410,6 +410,7 @@ async def test_preload_stream(hass, mock_stream): "homeassistant.components.demo.camera.DemoCamera.stream_source", return_value="http://example.com", ): + mock_create_stream.return_value.start = AsyncMock() assert await async_setup_component( hass, "camera", {DOMAIN: {"platform": "demo"}} ) diff --git a/tests/components/nest/test_camera_sdm.py b/tests/components/nest/test_camera_sdm.py index 42b236fda7c..5c4194f46f6 100644 --- a/tests/components/nest/test_camera_sdm.py +++ b/tests/components/nest/test_camera_sdm.py @@ -158,6 +158,7 @@ async def mock_create_stream(hass) -> Mock: ) mock_stream.return_value.async_get_image = AsyncMock() mock_stream.return_value.async_get_image.return_value = IMAGE_BYTES_FROM_STREAM + mock_stream.return_value.start = AsyncMock() yield mock_stream @@ -370,6 +371,7 @@ async def test_refresh_expired_stream_token( # Request a stream for the camera entity to exercise nest cam + camera interaction # and shutdown on url expiration with patch("homeassistant.components.camera.create_stream") as create_stream: + create_stream.return_value.start = AsyncMock() hls_url = await camera.async_request_stream(hass, "camera.my_camera", fmt="hls") assert hls_url.startswith("/api/hls/") # Includes access token assert create_stream.called @@ -536,7 +538,8 @@ async def test_refresh_expired_stream_failure( # Request an HLS stream with patch("homeassistant.components.camera.create_stream") as create_stream: - + create_stream.return_value.start = AsyncMock() + create_stream.return_value.stop = AsyncMock() hls_url = await camera.async_request_stream(hass, "camera.my_camera", fmt="hls") assert hls_url.startswith("/api/hls/") # Includes access token assert create_stream.called @@ -555,6 +558,7 @@ async def test_refresh_expired_stream_failure( # Requesting an HLS stream will create an entirely new stream with patch("homeassistant.components.camera.create_stream") as create_stream: + create_stream.return_value.start = AsyncMock() # The HLS stream endpoint was invalidated, with a new auth token hls_url2 = await camera.async_request_stream( hass, "camera.my_camera", fmt="hls" diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 8e01c55de84..7343b96ef9a 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -144,7 +144,7 @@ async def test_hls_stream( # Request stream stream.add_provider(HLS_PROVIDER) - stream.start() + await stream.start() hls_client = await hls_stream(stream) @@ -171,7 +171,7 @@ async def test_hls_stream( stream_worker_sync.resume() # Stop stream, if it hasn't quit already - stream.stop() + await stream.stop() # Ensure playlist not accessible after stream ends fail_response = await hls_client.get() @@ -205,7 +205,7 @@ async def test_stream_timeout( # Request stream stream.add_provider(HLS_PROVIDER) - stream.start() + await stream.start() url = stream.endpoint_url(HLS_PROVIDER) http_client = await hass_client() @@ -218,6 +218,7 @@ async def test_stream_timeout( # Wait a minute future = dt_util.utcnow() + timedelta(minutes=1) async_fire_time_changed(hass, future) + await hass.async_block_till_done() # Fetch again to reset timer playlist_response = await http_client.get(parsed_url.path) @@ -249,10 +250,10 @@ async def test_stream_timeout_after_stop( # Request stream stream.add_provider(HLS_PROVIDER) - stream.start() + await stream.start() stream_worker_sync.resume() - stream.stop() + await stream.stop() # Wait 5 minutes and fire callback. Stream should already have been # stopped so this is a no-op. @@ -297,14 +298,14 @@ async def test_stream_retries(hass, setup_component, should_retry): mock_time.time.side_effect = time_side_effect # Request stream. Enable retries which are disabled by default in tests. should_retry.return_value = True - stream.start() + await stream.start() stream._thread.join() stream._thread = None assert av_open.call_count == 2 await hass.async_block_till_done() # Stop stream, if it hasn't quit already - stream.stop() + await stream.stop() # Stream marked initially available, then marked as failed, then marked available # before the final failure that exits the stream. @@ -351,7 +352,7 @@ async def test_hls_playlist_view(hass, setup_component, hls_stream, stream_worke ) stream_worker_sync.resume() - stream.stop() + await stream.stop() async def test_hls_max_segments(hass, setup_component, hls_stream, stream_worker_sync): @@ -400,7 +401,7 @@ async def test_hls_max_segments(hass, setup_component, hls_stream, stream_worker assert segment_response.status == HTTPStatus.OK stream_worker_sync.resume() - stream.stop() + await stream.stop() async def test_hls_playlist_view_discontinuity( @@ -438,7 +439,7 @@ async def test_hls_playlist_view_discontinuity( ) stream_worker_sync.resume() - stream.stop() + await stream.stop() async def test_hls_max_segments_discontinuity( @@ -481,7 +482,7 @@ async def test_hls_max_segments_discontinuity( ) stream_worker_sync.resume() - stream.stop() + await stream.stop() async def test_remove_incomplete_segment_on_exit( @@ -490,7 +491,7 @@ async def test_remove_incomplete_segment_on_exit( """Test that the incomplete segment gets removed when the worker thread quits.""" stream = create_stream(hass, STREAM_SOURCE, {}) stream_worker_sync.pause() - stream.start() + await stream.start() hls = stream.add_provider(HLS_PROVIDER) segment = Segment(sequence=0, stream_id=0, duration=SEGMENT_DURATION) @@ -511,4 +512,4 @@ async def test_remove_incomplete_segment_on_exit( await hass.async_block_till_done() assert segments[-1].complete assert len(segments) == 2 - stream.stop() + await stream.stop() diff --git a/tests/components/stream/test_ll_hls.py b/tests/components/stream/test_ll_hls.py index 9a0d94136b9..4aaec93d646 100644 --- a/tests/components/stream/test_ll_hls.py +++ b/tests/components/stream/test_ll_hls.py @@ -144,7 +144,7 @@ async def test_ll_hls_stream(hass, hls_stream, stream_worker_sync): # Request stream stream.add_provider(HLS_PROVIDER) - stream.start() + await stream.start() hls_client = await hls_stream(stream) @@ -243,7 +243,7 @@ async def test_ll_hls_stream(hass, hls_stream, stream_worker_sync): stream_worker_sync.resume() # Stop stream, if it hasn't quit already - stream.stop() + await stream.stop() # Ensure playlist not accessible after stream ends fail_response = await hls_client.get() @@ -316,7 +316,7 @@ async def test_ll_hls_playlist_view(hass, hls_stream, stream_worker_sync): ) stream_worker_sync.resume() - stream.stop() + await stream.stop() async def test_ll_hls_msn(hass, hls_stream, stream_worker_sync, hls_sync): diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index 50aa4df3f1c..9433cbd449d 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -46,7 +46,7 @@ async def test_record_stream(hass, hass_client, record_worker_sync, h264_video): # thread completes and is shutdown completely to avoid thread leaks. await record_worker_sync.join() - stream.stop() + await stream.stop() async def test_record_lookback( @@ -59,14 +59,14 @@ async def test_record_lookback( # Start an HLS feed to enable lookback stream.add_provider(HLS_PROVIDER) - stream.start() + await stream.start() with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path", lookback=4) # This test does not need recorder cleanup since it is not fully exercised - stream.stop() + await stream.stop() async def test_recorder_timeout(hass, hass_client, stream_worker_sync, h264_video): @@ -97,7 +97,7 @@ async def test_recorder_timeout(hass, hass_client, stream_worker_sync, h264_vide assert mock_timeout.called stream_worker_sync.resume() - stream.stop() + await stream.stop() await hass.async_block_till_done() await hass.async_block_till_done() @@ -229,7 +229,7 @@ async def test_record_stream_audio( assert len(result.streams.audio) == expected_audio_streams result.close() - stream.stop() + await stream.stop() await hass.async_block_till_done() # Verify that the save worker was invoked, then block until its diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 2a44dd64455..a70f2be81b8 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -651,12 +651,12 @@ async def test_stream_stopped_while_decoding(hass): return py_av.open(stream_source, args, kwargs) with patch("av.open", new=blocking_open): - stream.start() + await stream.start() assert worker_open.wait(TIMEOUT) # Note: There is a race here where the worker could start as soon # as the wake event is sent, completing all decode work. worker_wake.set() - stream.stop() + await stream.stop() # Stream is still considered available when the worker was still active and asked to stop assert stream.available @@ -688,7 +688,7 @@ async def test_update_stream_source(hass): return py_av.open(stream_source, args, kwargs) with patch("av.open", new=blocking_open): - stream.start() + await stream.start() assert worker_open.wait(TIMEOUT) assert last_stream_source == STREAM_SOURCE assert stream.available @@ -704,7 +704,7 @@ async def test_update_stream_source(hass): assert stream.available # Cleanup - stream.stop() + await stream.stop() async def test_worker_log(hass, caplog): @@ -796,7 +796,7 @@ async def test_durations(hass, record_worker_sync): await record_worker_sync.join() - stream.stop() + await stream.stop() async def test_has_keyframe(hass, record_worker_sync, h264_video): @@ -836,7 +836,7 @@ async def test_has_keyframe(hass, record_worker_sync, h264_video): await record_worker_sync.join() - stream.stop() + await stream.stop() async def test_h265_video_is_hvc1(hass, record_worker_sync): @@ -871,7 +871,7 @@ async def test_h265_video_is_hvc1(hass, record_worker_sync): await record_worker_sync.join() - stream.stop() + await stream.stop() assert stream.get_diagnostics() == { "container_format": "mov,mp4,m4a,3gp,3g2,mj2", @@ -905,4 +905,4 @@ async def test_get_image(hass, record_worker_sync): assert await stream.async_get_image() == EMPTY_8_6_JPEG - stream.stop() + await stream.stop() From a5dc7c5f2850a0e7cdf718393c077f45bb69dd74 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 7 Jun 2022 12:49:40 -0400 Subject: [PATCH 1328/3516] Add logbook describe event support to ZHA (#73077) --- .../components/zha/core/channels/__init__.py | 2 +- homeassistant/components/zha/core/const.py | 1 + homeassistant/components/zha/core/device.py | 12 +- .../components/zha/device_trigger.py | 3 +- homeassistant/components/zha/logbook.py | 81 +++++++ tests/components/zha/test_channels.py | 2 +- tests/components/zha/test_cover.py | 3 +- tests/components/zha/test_logbook.py | 208 ++++++++++++++++++ 8 files changed, 307 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/zha/logbook.py create mode 100644 tests/components/zha/test_logbook.py diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index a0df976486f..33143821f9c 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -163,7 +163,7 @@ class Channels: def zha_send_event(self, event_data: dict[str, str | int]) -> None: """Relay events to hass.""" self.zha_device.hass.bus.async_fire( - "zha_event", + const.ZHA_EVENT, { const.ATTR_DEVICE_IEEE: str(self.zha_device.ieee), const.ATTR_UNIQUE_ID: self.unique_id, diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 1e249ebd52b..c2d9e926453 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -374,6 +374,7 @@ ZHA_CHANNEL_MSG_CFG_RPT = "zha_channel_configure_reporting" ZHA_CHANNEL_MSG_DATA = "zha_channel_msg_data" ZHA_CHANNEL_CFG_DONE = "zha_channel_cfg_done" ZHA_CHANNEL_READS_PER_REQ = 5 +ZHA_EVENT = "zha_event" ZHA_GW_MSG = "zha_gateway_message" ZHA_GW_MSG_DEVICE_FULL_INIT = "device_fully_initialized" ZHA_GW_MSG_DEVICE_INFO = "device_info" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index e5b3403ba54..6e72c17ef42 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -5,6 +5,7 @@ import asyncio from collections.abc import Callable from datetime import timedelta from enum import Enum +from functools import cached_property import logging import random import time @@ -280,7 +281,16 @@ class ZHADevice(LogMixin): """Return the gateway for this device.""" return self._zha_gateway - @property + @cached_property + def device_automation_commands(self) -> dict[str, list[tuple[str, str]]]: + """Return the a lookup of commands to etype/sub_type.""" + commands: dict[str, list[tuple[str, str]]] = {} + for etype_subtype, trigger in self.device_automation_triggers.items(): + if command := trigger.get(ATTR_COMMAND): + commands.setdefault(command, []).append(etype_subtype) + return commands + + @cached_property def device_automation_triggers(self) -> dict[tuple[str, str], dict[str, str]]: """Return the device automation triggers for this device.""" triggers = { diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 670b1cc1477..44682aaa559 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -1,4 +1,5 @@ """Provides device automations for ZHA devices that emit events.""" + import voluptuous as vol from homeassistant.components.automation import ( @@ -16,12 +17,12 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType from . import DOMAIN +from .core.const import ZHA_EVENT from .core.helpers import async_get_zha_device CONF_SUBTYPE = "subtype" DEVICE = "device" DEVICE_IEEE = "device_ieee" -ZHA_EVENT = "zha_event" TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( {vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str} diff --git a/homeassistant/components/zha/logbook.py b/homeassistant/components/zha/logbook.py new file mode 100644 index 00000000000..e2d238ddbe8 --- /dev/null +++ b/homeassistant/components/zha/logbook.py @@ -0,0 +1,81 @@ +"""Describe ZHA logbook events.""" +from __future__ import annotations + +from collections.abc import Callable +from typing import TYPE_CHECKING + +from homeassistant.components.logbook.const import ( + LOGBOOK_ENTRY_MESSAGE, + LOGBOOK_ENTRY_NAME, +) +from homeassistant.const import ATTR_COMMAND, ATTR_DEVICE_ID +from homeassistant.core import Event, HomeAssistant, callback +import homeassistant.helpers.device_registry as dr + +from .core.const import DOMAIN as ZHA_DOMAIN, ZHA_EVENT +from .core.helpers import async_get_zha_device + +if TYPE_CHECKING: + from .core.device import ZHADevice + + +@callback +def async_describe_events( + hass: HomeAssistant, + async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None], +) -> None: + """Describe logbook events.""" + device_registry = dr.async_get(hass) + + @callback + def async_describe_zha_event(event: Event) -> dict[str, str]: + """Describe zha logbook event.""" + device: dr.DeviceEntry | None = None + device_name: str = "Unknown device" + zha_device: ZHADevice | None = None + event_data: dict = event.data + event_type: str | None = None + event_subtype: str | None = None + + try: + device = device_registry.devices[event.data[ATTR_DEVICE_ID]] + if device: + device_name = device.name_by_user or device.name or "Unknown device" + zha_device = async_get_zha_device(hass, event.data[ATTR_DEVICE_ID]) + except (KeyError, AttributeError): + pass + + if ( + zha_device + and (command := event_data.get(ATTR_COMMAND)) + and (command_to_etype_subtype := zha_device.device_automation_commands) + and (etype_subtypes := command_to_etype_subtype.get(command)) + ): + all_triggers = zha_device.device_automation_triggers + for etype_subtype in etype_subtypes: + trigger = all_triggers[etype_subtype] + if not all( + event_data.get(key) == value for key, value in trigger.items() + ): + continue + event_type, event_subtype = etype_subtype + break + + if event_type is None: + event_type = event_data[ATTR_COMMAND] + + if event_subtype is not None and event_subtype != event_type: + event_type = f"{event_type} - {event_subtype}" + + event_type = event_type.replace("_", " ").title() + + message = f"{event_type} event was fired" + if event_data["params"]: + message = f"{message} with parameters: {event_data['params']}" + + return { + LOGBOOK_ENTRY_NAME: device_name, + LOGBOOK_ENTRY_MESSAGE: message, + } + + async_describe_event(ZHA_DOMAIN, ZHA_EVENT, async_describe_zha_event) diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index 79b8dbc6a71..e55e10bd7ae 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -510,7 +510,7 @@ async def test_poll_control_cluster_command(hass, poll_control_device): checkin_mock = AsyncMock() poll_control_ch = poll_control_device.channels.pools[0].all_channels["1:0x0020"] cluster = poll_control_ch.cluster - events = async_capture_events(hass, "zha_event") + events = async_capture_events(hass, zha_const.ZHA_EVENT) with mock.patch.object(poll_control_ch, "check_in_response", checkin_mock): tsn = 22 diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 3c00b5d3109..d5b07e16685 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -17,6 +17,7 @@ from homeassistant.components.cover import ( SERVICE_SET_COVER_POSITION, SERVICE_STOP_COVER, ) +from homeassistant.components.zha.core.const import ZHA_EVENT from homeassistant.const import ( ATTR_COMMAND, STATE_CLOSED, @@ -410,7 +411,7 @@ async def test_cover_remote(hass, zha_device_joined_restored, zigpy_cover_remote cluster = zigpy_cover_remote.endpoints[1].out_clusters[ closures.WindowCovering.cluster_id ] - zha_events = async_capture_events(hass, "zha_event") + zha_events = async_capture_events(hass, ZHA_EVENT) # up command hdr = make_zcl_header(0, global_command=False) diff --git a/tests/components/zha/test_logbook.py b/tests/components/zha/test_logbook.py new file mode 100644 index 00000000000..00e1cc28ea6 --- /dev/null +++ b/tests/components/zha/test_logbook.py @@ -0,0 +1,208 @@ +"""ZHA logbook describe events tests.""" + +import pytest +import zigpy.profiles.zha +import zigpy.zcl.clusters.general as general + +from homeassistant.components.zha.core.const import ZHA_EVENT +from homeassistant.const import CONF_DEVICE_ID, CONF_UNIQUE_ID +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component + +from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE + +from tests.components.logbook.common import MockRow, mock_humanify + +ON = 1 +OFF = 0 +SHAKEN = "device_shaken" +COMMAND = "command" +COMMAND_SHAKE = "shake" +COMMAND_HOLD = "hold" +COMMAND_SINGLE = "single" +COMMAND_DOUBLE = "double" +DOUBLE_PRESS = "remote_button_double_press" +SHORT_PRESS = "remote_button_short_press" +LONG_PRESS = "remote_button_long_press" +LONG_RELEASE = "remote_button_long_release" +UP = "up" +DOWN = "down" + + +@pytest.fixture +async def mock_devices(hass, zigpy_device_mock, zha_device_joined): + """IAS device fixture.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [general.Basic.cluster_id], + SIG_EP_OUTPUT: [general.OnOff.cluster_id], + SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH, + SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, + } + } + ) + + zha_device = await zha_device_joined(zigpy_device) + zha_device.update_available(True) + await hass.async_block_till_done() + return zigpy_device, zha_device + + +async def test_zha_logbook_event_device_with_triggers(hass, mock_devices): + """Test zha logbook events with device and triggers.""" + + zigpy_device, zha_device = mock_devices + + zigpy_device.device_automation_triggers = { + (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, + (UP, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE, "endpoint_id": 1}, + (DOWN, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE, "endpoint_id": 2}, + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}, + (LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD}, + (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, + } + + ieee_address = str(zha_device.ieee) + + ha_device_registry = dr.async_get(hass) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) + + hass.config.components.add("recorder") + assert await async_setup_component(hass, "logbook", {}) + + events = mock_humanify( + hass, + [ + MockRow( + ZHA_EVENT, + { + CONF_DEVICE_ID: reg_device.id, + COMMAND: COMMAND_SHAKE, + "device_ieee": str(ieee_address), + CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + "params": { + "test": "test", + }, + }, + ), + MockRow( + ZHA_EVENT, + { + CONF_DEVICE_ID: reg_device.id, + COMMAND: COMMAND_DOUBLE, + "device_ieee": str(ieee_address), + CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + "params": { + "test": "test", + }, + }, + ), + MockRow( + ZHA_EVENT, + { + CONF_DEVICE_ID: reg_device.id, + COMMAND: COMMAND_DOUBLE, + "device_ieee": str(ieee_address), + CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006", + "endpoint_id": 2, + "cluster_id": 6, + "params": { + "test": "test", + }, + }, + ), + ], + ) + + assert events[0]["name"] == "FakeManufacturer FakeModel" + assert events[0]["domain"] == "zha" + assert ( + events[0]["message"] + == "Device Shaken event was fired with parameters: {'test': 'test'}" + ) + + assert events[1]["name"] == "FakeManufacturer FakeModel" + assert events[1]["domain"] == "zha" + assert ( + events[1]["message"] + == "Up - Remote Button Double Press event was fired with parameters: {'test': 'test'}" + ) + + +async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): + """Test zha logbook events with device and without triggers.""" + + zigpy_device, zha_device = mock_devices + ieee_address = str(zha_device.ieee) + ha_device_registry = dr.async_get(hass) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) + + hass.config.components.add("recorder") + assert await async_setup_component(hass, "logbook", {}) + + events = mock_humanify( + hass, + [ + MockRow( + ZHA_EVENT, + { + CONF_DEVICE_ID: reg_device.id, + COMMAND: COMMAND_SHAKE, + "device_ieee": str(ieee_address), + CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + "params": { + "test": "test", + }, + }, + ), + ], + ) + + assert events[0]["name"] == "FakeManufacturer FakeModel" + assert events[0]["domain"] == "zha" + assert ( + events[0]["message"] + == "Shake event was fired with parameters: {'test': 'test'}" + ) + + +async def test_zha_logbook_event_device_no_device(hass, mock_devices): + """Test zha logbook events without device and without triggers.""" + + hass.config.components.add("recorder") + assert await async_setup_component(hass, "logbook", {}) + + events = mock_humanify( + hass, + [ + MockRow( + ZHA_EVENT, + { + CONF_DEVICE_ID: "non-existing-device", + COMMAND: COMMAND_SHAKE, + "device_ieee": "90:fd:9f:ff:fe:fe:d8:a1", + CONF_UNIQUE_ID: "90:fd:9f:ff:fe:fe:d8:a1:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + "params": { + "test": "test", + }, + }, + ), + ], + ) + + assert events[0]["name"] == "Unknown device" + assert events[0]["domain"] == "zha" + assert ( + events[0]["message"] + == "Shake event was fired with parameters: {'test': 'test'}" + ) From 0b5c0f82496f9a74098b51c44a60af7668c3334d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 7 Jun 2022 18:56:11 +0200 Subject: [PATCH 1329/3516] Bump `nam` backend library (#72771) * Update config flow * Fix discovery with auth * Call check_credentials() on init * Update tests * Bump library version * Cleaning * Return dataclass instead of tuple * Fix pylint error --- homeassistant/components/nam/__init__.py | 7 +- homeassistant/components/nam/config_flow.py | 61 +++++++++---- homeassistant/components/nam/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nam/test_config_flow.py | 98 ++++++++++----------- tests/components/nam/test_init.py | 2 +- 7 files changed, 99 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 6b0f9db3757..021b46e2f38 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -52,11 +52,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: options = ConnectionOptions(host=host, username=username, password=password) try: nam = await NettigoAirMonitor.create(websession, options) - except AuthFailed as err: - raise ConfigEntryAuthFailed from err except (ApiError, ClientError, ClientConnectorError, asyncio.TimeoutError) as err: raise ConfigEntryNotReady from err + try: + await nam.async_check_credentials() + except AuthFailed as err: + raise ConfigEntryAuthFailed from err + coordinator = NAMDataUpdateCoordinator(hass, nam, entry.unique_id) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py index df41eb7c5f1..451148c22fe 100644 --- a/homeassistant/components/nam/config_flow.py +++ b/homeassistant/components/nam/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Mapping +from dataclasses import dataclass import logging from typing import Any @@ -27,6 +28,15 @@ from homeassistant.helpers.device_registry import format_mac from .const import DOMAIN + +@dataclass +class NamConfig: + """NAM device configuration class.""" + + mac_address: str + auth_enabled: bool + + _LOGGER = logging.getLogger(__name__) AUTH_SCHEMA = vol.Schema( @@ -34,15 +44,31 @@ AUTH_SCHEMA = vol.Schema( ) -async def async_get_mac(hass: HomeAssistant, host: str, data: dict[str, Any]) -> str: - """Get device MAC address.""" +async def async_get_config(hass: HomeAssistant, host: str) -> NamConfig: + """Get device MAC address and auth_enabled property.""" websession = async_get_clientsession(hass) - options = ConnectionOptions(host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD)) + options = ConnectionOptions(host) nam = await NettigoAirMonitor.create(websession, options) async with async_timeout.timeout(10): - return await nam.async_get_mac_address() + mac = await nam.async_get_mac_address() + + return NamConfig(mac, nam.auth_enabled) + + +async def async_check_credentials( + hass: HomeAssistant, host: str, data: dict[str, Any] +) -> None: + """Check if credentials are valid.""" + websession = async_get_clientsession(hass) + + options = ConnectionOptions(host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD)) + + nam = await NettigoAirMonitor.create(websession, options) + + async with async_timeout.timeout(10): + await nam.async_check_credentials() class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -54,6 +80,7 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize flow.""" self.host: str self.entry: config_entries.ConfigEntry + self._config: NamConfig async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -65,9 +92,7 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.host = user_input[CONF_HOST] try: - mac = await async_get_mac(self.hass, self.host, {}) - except AuthFailed: - return await self.async_step_credentials() + config = await async_get_config(self.hass, self.host) except (ApiError, ClientConnectorError, asyncio.TimeoutError): errors["base"] = "cannot_connect" except CannotGetMac: @@ -76,9 +101,12 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - await self.async_set_unique_id(format_mac(mac)) + await self.async_set_unique_id(format_mac(config.mac_address)) self._abort_if_unique_id_configured({CONF_HOST: self.host}) + if config.auth_enabled is True: + return await self.async_step_credentials() + return self.async_create_entry( title=self.host, data=user_input, @@ -98,19 +126,15 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: try: - mac = await async_get_mac(self.hass, self.host, user_input) + await async_check_credentials(self.hass, self.host, user_input) except AuthFailed: errors["base"] = "invalid_auth" except (ApiError, ClientConnectorError, asyncio.TimeoutError): errors["base"] = "cannot_connect" - except CannotGetMac: - return self.async_abort(reason="device_unsupported") except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - await self.async_set_unique_id(format_mac(mac)) - self._abort_if_unique_id_configured({CONF_HOST: self.host}) return self.async_create_entry( title=self.host, @@ -132,15 +156,13 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._async_abort_entries_match({CONF_HOST: self.host}) try: - mac = await async_get_mac(self.hass, self.host, {}) - except AuthFailed: - return await self.async_step_credentials() + self._config = await async_get_config(self.hass, self.host) except (ApiError, ClientConnectorError, asyncio.TimeoutError): return self.async_abort(reason="cannot_connect") except CannotGetMac: return self.async_abort(reason="device_unsupported") - await self.async_set_unique_id(format_mac(mac)) + await self.async_set_unique_id(format_mac(self._config.mac_address)) self._abort_if_unique_id_configured({CONF_HOST: self.host}) return await self.async_step_confirm_discovery() @@ -157,6 +179,9 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data={CONF_HOST: self.host}, ) + if self._config.auth_enabled is True: + return await self.async_step_credentials() + self._set_confirm_only() return self.async_show_form( @@ -181,7 +206,7 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: try: - await async_get_mac(self.hass, self.host, user_input) + await async_check_credentials(self.hass, self.host, user_input) except (ApiError, AuthFailed, ClientConnectorError, asyncio.TimeoutError): return self.async_abort(reason="reauth_unsuccessful") else: diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index a842af46f84..88048b59162 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.2.4"], + "requirements": ["nettigo-air-monitor==1.3.0"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 70a29067a7b..8a804d3ae90 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.2.4 +nettigo-air-monitor==1.3.0 # homeassistant.components.neurio_energy neurio==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 850a379eaf6..48b20ee778f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -745,7 +745,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.2.4 +nettigo-air-monitor==1.3.0 # homeassistant.components.nexia nexia==1.0.1 diff --git a/tests/components/nam/test_config_flow.py b/tests/components/nam/test_config_flow.py index 9479e29cdea..67274cf1c78 100644 --- a/tests/components/nam/test_config_flow.py +++ b/tests/components/nam/test_config_flow.py @@ -23,6 +23,8 @@ DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( ) VALID_CONFIG = {"host": "10.10.2.3"} VALID_AUTH = {"username": "fake_username", "password": "fake_password"} +DEVICE_CONFIG = {"www_basicauth_enabled": False} +DEVICE_CONFIG_AUTH = {"www_basicauth_enabled": True} async def test_form_create_entry_without_auth(hass): @@ -34,7 +36,10 @@ async def test_form_create_entry_without_auth(hass): assert result["step_id"] == SOURCE_USER assert result["errors"] == {} - with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( + with patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + return_value=DEVICE_CONFIG, + ), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ), patch( @@ -62,24 +67,22 @@ async def test_form_create_entry_with_auth(hass): assert result["errors"] == {} with patch( - "homeassistant.components.nam.NettigoAirMonitor.initialize", - side_effect=AuthFailed("Auth Error"), - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - VALID_CONFIG, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "credentials" - - with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + return_value=DEVICE_CONFIG_AUTH, + ), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ), patch( "homeassistant.components.nam.async_setup_entry", return_value=True ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "credentials" + result = await hass.config_entries.flow.async_configure( result["flow_id"], VALID_AUTH, @@ -104,7 +107,10 @@ async def test_reauth_successful(hass): ) entry.add_to_hass(hass) - with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( + with patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + return_value=DEVICE_CONFIG_AUTH, + ), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ): @@ -137,7 +143,7 @@ async def test_reauth_unsuccessful(hass): entry.add_to_hass(hass) with patch( - "homeassistant.components.nam.NettigoAirMonitor.initialize", + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", side_effect=ApiError("API Error"), ): result = await hass.config_entries.flow.async_init( @@ -171,8 +177,11 @@ async def test_form_with_auth_errors(hass, error): """Test we handle errors when auth is required.""" exc, base_error = error with patch( - "homeassistant.components.nam.NettigoAirMonitor.initialize", + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", side_effect=AuthFailed("Auth Error"), + ), patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -221,26 +230,13 @@ async def test_form_errors(hass, error): async def test_form_abort(hass): - """Test we handle abort after error.""" - with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( - "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", - side_effect=CannotGetMac("Cannot get MAC address from device"), - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data=VALID_CONFIG, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "device_unsupported" - - -async def test_form_with_auth_abort(hass): """Test we handle abort after error.""" with patch( - "homeassistant.components.nam.NettigoAirMonitor.initialize", - side_effect=AuthFailed("Auth Error"), + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + return_value=DEVICE_CONFIG, + ), patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + side_effect=CannotGetMac("Cannot get MAC address from device"), ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -248,18 +244,6 @@ async def test_form_with_auth_abort(hass): data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "credentials" - - with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( - "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", - side_effect=CannotGetMac("Cannot get MAC address from device"), - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - VALID_AUTH, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "device_unsupported" @@ -275,7 +259,10 @@ async def test_form_already_configured(hass): DOMAIN, context={"source": SOURCE_USER} ) - with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( + with patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + return_value=DEVICE_CONFIG, + ), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ): @@ -293,7 +280,10 @@ async def test_form_already_configured(hass): async def test_zeroconf(hass): """Test we get the form.""" - with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( + with patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + return_value=DEVICE_CONFIG, + ), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ): @@ -332,8 +322,11 @@ async def test_zeroconf(hass): async def test_zeroconf_with_auth(hass): """Test that the zeroconf step with auth works.""" with patch( - "homeassistant.components.nam.NettigoAirMonitor.initialize", + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", side_effect=AuthFailed("Auth Error"), + ), patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -351,7 +344,10 @@ async def test_zeroconf_with_auth(hass): assert result["errors"] == {} assert context["title_placeholders"]["host"] == "10.10.2.3" - with patch("homeassistant.components.nam.NettigoAirMonitor.initialize"), patch( + with patch( + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", + return_value=DEVICE_CONFIG_AUTH, + ), patch( "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", return_value="aa:bb:cc:dd:ee:ff", ), patch( diff --git a/tests/components/nam/test_init.py b/tests/components/nam/test_init.py index 3223a394f68..9eac901d693 100644 --- a/tests/components/nam/test_init.py +++ b/tests/components/nam/test_init.py @@ -51,7 +51,7 @@ async def test_config_auth_failed(hass): ) with patch( - "homeassistant.components.nam.NettigoAirMonitor.initialize", + "homeassistant.components.nam.NettigoAirMonitor.async_check_credentials", side_effect=AuthFailed("Authorization has failed"), ): entry.add_to_hass(hass) From 981c34f88d9aa7b9c68db47d1c1b3a8f9af7d1e1 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 7 Jun 2022 19:15:25 +0200 Subject: [PATCH 1330/3516] Use class attribute instead of property in min_max integration (#73175) --- homeassistant/components/min_max/sensor.py | 28 +++++----------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index 99aec4e9e7b..c5a51cdda7a 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -161,6 +161,10 @@ def calc_median(sensor_values, round_digits): class MinMaxSensor(SensorEntity): """Representation of a min/max sensor.""" + _attr_icon = ICON + _attr_should_poll = False + _attr_state_class = SensorStateClass.MEASUREMENT + def __init__(self, entity_ids, name, sensor_type, round_digits, unique_id): """Initialize the min/max sensor.""" self._attr_unique_id = unique_id @@ -169,9 +173,9 @@ class MinMaxSensor(SensorEntity): self._round_digits = round_digits if name: - self._name = name + self._attr_name = name else: - self._name = f"{sensor_type} sensor".capitalize() + self._attr_name = f"{sensor_type} sensor".capitalize() self._sensor_attr = SENSOR_TYPE_TO_ATTR[self._sensor_type] self._unit_of_measurement = None self._unit_of_measurement_mismatch = False @@ -196,11 +200,6 @@ class MinMaxSensor(SensorEntity): self._calc_values() - @property - def name(self): - """Return the name of the sensor.""" - return self._name - @property def native_value(self): """Return the state of the sensor.""" @@ -215,11 +214,6 @@ class MinMaxSensor(SensorEntity): return "ERR" return self._unit_of_measurement - @property - def should_poll(self): - """No polling needed.""" - return False - @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" @@ -231,16 +225,6 @@ class MinMaxSensor(SensorEntity): return {ATTR_LAST_ENTITY_ID: self.last_entity_id} return None - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return ICON - - @property - def state_class(self) -> SensorStateClass: - """Return the state class.""" - return SensorStateClass.MEASUREMENT - @callback def _async_min_max_sensor_state_listener(self, event, update_state=True): """Handle the sensor state changes.""" From f10cae105241af452e306d0afcc1d4693dcaf42f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 7 Jun 2022 19:57:29 +0200 Subject: [PATCH 1331/3516] Add missing `state_class` to xiaomi_aqara sensors (#73167) Add missing state_class --- homeassistant/components/xiaomi_aqara/sensor.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py index 9c295c3fe0a..3e0d49d628f 100644 --- a/homeassistant/components/xiaomi_aqara/sensor.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -30,36 +31,43 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { key="temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), "humidity": SensorEntityDescription( key="humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), "illumination": SensorEntityDescription( key="illumination", native_unit_of_measurement="lm", device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), "lux": SensorEntityDescription( key="lux", native_unit_of_measurement=LIGHT_LUX, device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, ), "pressure": SensorEntityDescription( key="pressure", native_unit_of_measurement=PRESSURE_HPA, device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, ), "bed_activity": SensorEntityDescription( key="bed_activity", native_unit_of_measurement="μm", device_class=None, + state_class=SensorStateClass.MEASUREMENT, ), "load_power": SensorEntityDescription( key="load_power", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), "final_tilt_angle": SensorEntityDescription( key="final_tilt_angle", @@ -69,6 +77,7 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { ), "Battery": SensorEntityDescription( key="Battery", + state_class=SensorStateClass.MEASUREMENT, ), } From 1bc986794087f0b7a114a509b35819570a4c76b3 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Tue, 7 Jun 2022 14:19:39 -0400 Subject: [PATCH 1332/3516] Bump version of pyunifiprotect to 3.9.0 (#73168) Co-authored-by: J. Nick Koston --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifiprotect/test_binary_sensor.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 898abd73a6f..52f69abea00 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.6.0", "unifi-discovery==1.1.3"], + "requirements": ["pyunifiprotect==3.9.0", "unifi-discovery==1.1.3"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 8a804d3ae90..6b71729bb53 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1993,7 +1993,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.6.0 +pyunifiprotect==3.9.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 48b20ee778f..a48dce2c79f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1322,7 +1322,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.6.0 +pyunifiprotect==3.9.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 60a3d5a8126..88b42d36994 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -168,7 +168,7 @@ async def sensor_fixture( sensor_obj.motion_detected_at = now - timedelta(hours=1) sensor_obj.open_status_changed_at = now - timedelta(hours=1) sensor_obj.alarm_triggered_at = now - timedelta(hours=1) - sensor_obj.tampering_detected_at = now - timedelta(hours=1) + sensor_obj.tampering_detected_at = None mock_entry.api.bootstrap.reset_objects() mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] @@ -204,7 +204,7 @@ async def sensor_none_fixture( sensor_obj.mount_type = MountType.LEAK sensor_obj.battery_status.is_low = False sensor_obj.alarm_settings.is_enabled = False - sensor_obj.tampering_detected_at = now - timedelta(hours=1) + sensor_obj.tampering_detected_at = None mock_entry.api.bootstrap.reset_objects() mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] From 94c037605a50dc211b5f90cc79226af814b308d8 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Tue, 7 Jun 2022 16:35:59 -0400 Subject: [PATCH 1333/3516] Address late comment on Goalzero refactor (#73180) --- homeassistant/components/goalzero/entity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/goalzero/entity.py b/homeassistant/components/goalzero/entity.py index fa0d55b0d5f..8c696ce1377 100644 --- a/homeassistant/components/goalzero/entity.py +++ b/homeassistant/components/goalzero/entity.py @@ -23,7 +23,6 @@ class GoalZeroEntity(CoordinatorEntity[GoalZeroDataUpdateCoordinator]): ) -> None: """Initialize a Goal Zero Yeti entity.""" super().__init__(coordinator) - self.coordinator = coordinator self.entity_description = description self._attr_name = ( f"{coordinator.config_entry.data[CONF_NAME]} {description.name}" From d587e4769ad0bb3b7106fcf7a1213614ddd20b8f Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 7 Jun 2022 14:39:15 -0700 Subject: [PATCH 1334/3516] Bump pywemo to 0.9.1 (#73186) --- homeassistant/components/wemo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 40bb8161d90..b324ba060ea 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -3,7 +3,7 @@ "name": "Belkin WeMo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wemo", - "requirements": ["pywemo==0.8.1"], + "requirements": ["pywemo==0.9.1"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index 6b71729bb53..b95e0b7280c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2023,7 +2023,7 @@ pyvolumio==0.1.5 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.8.1 +pywemo==0.9.1 # homeassistant.components.wilight pywilight==0.0.70 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a48dce2c79f..5a9399b0e3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1343,7 +1343,7 @@ pyvolumio==0.1.5 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.8.1 +pywemo==0.9.1 # homeassistant.components.wilight pywilight==0.0.70 From eca67680164037cadbaf524ed02a1c7b35fe3938 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 8 Jun 2022 01:01:44 +0200 Subject: [PATCH 1335/3516] Use default None for voltage property of FritzDevice in Fritz!Smarthome (#73141) use default None for device.voltage --- homeassistant/components/fritzbox/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index e590f14ce89..2ae7f9dccc8 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -96,7 +96,9 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] - native_value=lambda device: device.voltage / 1000 if device.voltage else 0.0, + native_value=lambda device: device.voltage / 1000 + if getattr(device, "voltage", None) + else 0.0, ), FritzSensorEntityDescription( key="electric_current", @@ -106,7 +108,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] native_value=lambda device: device.power / device.voltage - if device.power and device.voltage + if device.power and getattr(device, "voltage", None) else 0.0, ), FritzSensorEntityDescription( From 8c34067f17f0506d56d61e095be5d99523468e83 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 8 Jun 2022 01:11:38 +0200 Subject: [PATCH 1336/3516] Fix creating unique IDs for WiFi switches in Fritz!Tools (#73183) --- homeassistant/components/fritz/switch.py | 11 +- tests/components/fritz/conftest.py | 12 +- tests/components/fritz/const.py | 3 +- tests/components/fritz/test_button.py | 2 +- tests/components/fritz/test_config_flow.py | 2 +- tests/components/fritz/test_diagnostics.py | 2 +- tests/components/fritz/test_init.py | 2 +- tests/components/fritz/test_sensor.py | 2 +- tests/components/fritz/test_switch.py | 189 +++++++++++++++++++++ tests/components/fritz/test_update.py | 2 +- 10 files changed, 216 insertions(+), 11 deletions(-) create mode 100644 tests/components/fritz/test_switch.py diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index cac6e735a81..a45cd347463 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -169,7 +169,16 @@ def wifi_entities_list( } for i, network in networks.copy().items(): networks[i]["switch_name"] = network["ssid"] - if len([j for j, n in networks.items() if n["ssid"] == network["ssid"]]) > 1: + if ( + len( + [ + j + for j, n in networks.items() + if slugify(n["ssid"]) == slugify(network["ssid"]) + ] + ) + > 1 + ): networks[i]["switch_name"] += f" ({WIFI_STANDARD[i]})" _LOGGER.debug("WiFi networks list: %s", networks) diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py index b073335f20a..edbde883f56 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -1,4 +1,4 @@ -"""Common stuff for AVM Fritz!Box tests.""" +"""Common stuff for Fritz!Tools tests.""" import logging from unittest.mock import MagicMock, patch @@ -73,13 +73,19 @@ class FritzHostMock(FritzHosts): return MOCK_MESH_DATA +@pytest.fixture(name="fc_data") +def fc_data_mock(): + """Fixture for default fc_data.""" + return MOCK_FB_SERVICES + + @pytest.fixture() -def fc_class_mock(): +def fc_class_mock(fc_data): """Fixture that sets up a mocked FritzConnection class.""" with patch( "homeassistant.components.fritz.common.FritzConnection", autospec=True ) as result: - result.return_value = FritzConnectionMock(MOCK_FB_SERVICES) + result.return_value = FritzConnectionMock(fc_data) yield result diff --git a/tests/components/fritz/const.py b/tests/components/fritz/const.py index f8f6f8370d7..32f2211d16b 100644 --- a/tests/components/fritz/const.py +++ b/tests/components/fritz/const.py @@ -1,4 +1,4 @@ -"""Common stuff for AVM Fritz!Box tests.""" +"""Common stuff for Fritz!Tools tests.""" from homeassistant.components import ssdp from homeassistant.components.fritz.const import DOMAIN from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN @@ -194,6 +194,7 @@ MOCK_FB_SERVICES: dict[str, dict] = { }, } + MOCK_MESH_DATA = { "schema_version": "1.9", "nodes": [ diff --git a/tests/components/fritz/test_button.py b/tests/components/fritz/test_button.py index a6ff579958a..a2bd6132731 100644 --- a/tests/components/fritz/test_button.py +++ b/tests/components/fritz/test_button.py @@ -1,4 +1,4 @@ -"""Tests for Shelly button platform.""" +"""Tests for Fritz!Tools button platform.""" from unittest.mock import patch import pytest diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 6d014782842..619f06f5493 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -1,4 +1,4 @@ -"""Tests for AVM Fritz!Box config flow.""" +"""Tests for Fritz!Tools config flow.""" import dataclasses from unittest.mock import patch diff --git a/tests/components/fritz/test_diagnostics.py b/tests/components/fritz/test_diagnostics.py index a4b4942c375..2c0b42d6bdf 100644 --- a/tests/components/fritz/test_diagnostics.py +++ b/tests/components/fritz/test_diagnostics.py @@ -1,4 +1,4 @@ -"""Tests for the AVM Fritz!Box integration.""" +"""Tests for Fritz!Tools diagnostics platform.""" from __future__ import annotations from aiohttp import ClientSession diff --git a/tests/components/fritz/test_init.py b/tests/components/fritz/test_init.py index fd67321d235..9f7a17de900 100644 --- a/tests/components/fritz/test_init.py +++ b/tests/components/fritz/test_init.py @@ -1,4 +1,4 @@ -"""Tests for AVM Fritz!Box.""" +"""Tests for Fritz!Tools.""" from unittest.mock import patch from fritzconnection.core.exceptions import FritzSecurityError diff --git a/tests/components/fritz/test_sensor.py b/tests/components/fritz/test_sensor.py index 31e142a3e47..73a7c1068ae 100644 --- a/tests/components/fritz/test_sensor.py +++ b/tests/components/fritz/test_sensor.py @@ -1,4 +1,4 @@ -"""Tests for Shelly button platform.""" +"""Tests for Fritz!Tools sensor platform.""" from __future__ import annotations from datetime import timedelta diff --git a/tests/components/fritz/test_switch.py b/tests/components/fritz/test_switch.py new file mode 100644 index 00000000000..1e744bb62e6 --- /dev/null +++ b/tests/components/fritz/test_switch.py @@ -0,0 +1,189 @@ +"""Tests for Fritz!Tools switch platform.""" +from __future__ import annotations + +import pytest + +from homeassistant.components.fritz.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import MOCK_FB_SERVICES, MOCK_USER_DATA + +from tests.common import MockConfigEntry + +MOCK_WLANCONFIGS_SAME_SSID: dict[str, dict] = { + "WLANConfiguration1": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewMaxBitRate": "Auto", + "NewChannel": 13, + "NewSSID": "WiFi", + "NewBeaconType": "11iandWPA3", + "NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3", + "NewMACAddressControlEnabled": False, + "NewStandard": "ax", + "NewBSSID": "1C:ED:6F:12:34:12", + "NewBasicEncryptionModes": "None", + "NewBasicAuthenticationMode": "None", + "NewMaxCharsSSID": 32, + "NewMinCharsSSID": 1, + "NewAllowedCharsSSID": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", + "NewMinCharsPSK": 64, + "NewMaxCharsPSK": 64, + "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", + } + }, + "WLANConfiguration2": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewMaxBitRate": "Auto", + "NewChannel": 52, + "NewSSID": "WiFi", + "NewBeaconType": "11iandWPA3", + "NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3", + "NewMACAddressControlEnabled": False, + "NewStandard": "ax", + "NewBSSID": "1C:ED:6F:12:34:13", + "NewBasicEncryptionModes": "None", + "NewBasicAuthenticationMode": "None", + "NewMaxCharsSSID": 32, + "NewMinCharsSSID": 1, + "NewAllowedCharsSSID": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", + "NewMinCharsPSK": 64, + "NewMaxCharsPSK": 64, + "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", + } + }, +} +MOCK_WLANCONFIGS_DIFF_SSID: dict[str, dict] = { + "WLANConfiguration1": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewMaxBitRate": "Auto", + "NewChannel": 13, + "NewSSID": "WiFi", + "NewBeaconType": "11iandWPA3", + "NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3", + "NewMACAddressControlEnabled": False, + "NewStandard": "ax", + "NewBSSID": "1C:ED:6F:12:34:12", + "NewBasicEncryptionModes": "None", + "NewBasicAuthenticationMode": "None", + "NewMaxCharsSSID": 32, + "NewMinCharsSSID": 1, + "NewAllowedCharsSSID": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", + "NewMinCharsPSK": 64, + "NewMaxCharsPSK": 64, + "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", + } + }, + "WLANConfiguration2": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewMaxBitRate": "Auto", + "NewChannel": 52, + "NewSSID": "WiFi2", + "NewBeaconType": "11iandWPA3", + "NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3", + "NewMACAddressControlEnabled": False, + "NewStandard": "ax", + "NewBSSID": "1C:ED:6F:12:34:13", + "NewBasicEncryptionModes": "None", + "NewBasicAuthenticationMode": "None", + "NewMaxCharsSSID": 32, + "NewMinCharsSSID": 1, + "NewAllowedCharsSSID": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", + "NewMinCharsPSK": 64, + "NewMaxCharsPSK": 64, + "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", + } + }, +} +MOCK_WLANCONFIGS_DIFF2_SSID: dict[str, dict] = { + "WLANConfiguration1": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewMaxBitRate": "Auto", + "NewChannel": 13, + "NewSSID": "WiFi", + "NewBeaconType": "11iandWPA3", + "NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3", + "NewMACAddressControlEnabled": False, + "NewStandard": "ax", + "NewBSSID": "1C:ED:6F:12:34:12", + "NewBasicEncryptionModes": "None", + "NewBasicAuthenticationMode": "None", + "NewMaxCharsSSID": 32, + "NewMinCharsSSID": 1, + "NewAllowedCharsSSID": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", + "NewMinCharsPSK": 64, + "NewMaxCharsPSK": 64, + "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", + } + }, + "WLANConfiguration2": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewMaxBitRate": "Auto", + "NewChannel": 52, + "NewSSID": "WiFi+", + "NewBeaconType": "11iandWPA3", + "NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3", + "NewMACAddressControlEnabled": False, + "NewStandard": "ax", + "NewBSSID": "1C:ED:6F:12:34:13", + "NewBasicEncryptionModes": "None", + "NewBasicAuthenticationMode": "None", + "NewMaxCharsSSID": 32, + "NewMinCharsSSID": 1, + "NewAllowedCharsSSID": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", + "NewMinCharsPSK": 64, + "NewMaxCharsPSK": 64, + "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", + } + }, +} + + +@pytest.mark.parametrize( + "fc_data, expected_wifi_names", + [ + ( + {**MOCK_FB_SERVICES, **MOCK_WLANCONFIGS_SAME_SSID}, + ["WiFi (2.4Ghz)", "WiFi (5Ghz)"], + ), + ({**MOCK_FB_SERVICES, **MOCK_WLANCONFIGS_DIFF_SSID}, ["WiFi", "WiFi2"]), + ( + {**MOCK_FB_SERVICES, **MOCK_WLANCONFIGS_DIFF2_SSID}, + ["WiFi (2.4Ghz)", "WiFi+ (5Ghz)"], + ), + ], +) +async def test_switch_setup( + hass: HomeAssistant, + expected_wifi_names: list[str], + fc_class_mock, + fh_class_mock, +): + """Test setup of Fritz!Tools switches.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + switches = hass.states.async_all(Platform.SWITCH) + assert len(switches) == 3 + assert switches[0].name == f"Mock Title Wi-Fi {expected_wifi_names[0]}" + assert switches[1].name == f"Mock Title Wi-Fi {expected_wifi_names[1]}" + assert switches[2].name == "printer Internet Access" diff --git a/tests/components/fritz/test_update.py b/tests/components/fritz/test_update.py index 2261ef3ef9b..32bdf361847 100644 --- a/tests/components/fritz/test_update.py +++ b/tests/components/fritz/test_update.py @@ -1,4 +1,4 @@ -"""The tests for the Fritzbox update entity.""" +"""Tests for Fritz!Tools update platform.""" from unittest.mock import patch From db0f089a2e3532f1e061e47926eccd520a28f7f5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 7 Jun 2022 17:14:40 -0600 Subject: [PATCH 1337/3516] Fix bugs with RainMachine zone run time sensors (#73179) --- .../components/rainmachine/sensor.py | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index cc37189aa49..1144ceea159 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -4,6 +4,8 @@ from __future__ import annotations from dataclasses import dataclass from datetime import datetime, timedelta +from regenmaschine.controller import Controller + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -13,8 +15,9 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS, VOLUME_CUBIC_METERS from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity import EntityCategory, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.dt import utcnow from . import RainMachineEntity @@ -205,16 +208,33 @@ class ZoneTimeRemainingSensor(RainMachineEntity, SensorEntity): entity_description: RainMachineSensorDescriptionUid + def __init__( + self, + entry: ConfigEntry, + coordinator: DataUpdateCoordinator, + controller: Controller, + description: EntityDescription, + ) -> None: + """Initialize.""" + super().__init__(entry, coordinator, controller, description) + + self._running_or_queued: bool = False + @callback def update_from_latest_data(self) -> None: """Update the state.""" data = self.coordinator.data[self.entity_description.uid] now = utcnow() - if RUN_STATE_MAP.get(data["state"]) != RunStates.RUNNING: - # If the zone isn't actively running, return immediately: + if RUN_STATE_MAP.get(data["state"]) == RunStates.NOT_RUNNING: + if self._running_or_queued: + # If we go from running to not running, update the state to be right + # now (i.e., the time the zone stopped running): + self._attr_native_value = now + self._running_or_queued = False return + self._running_or_queued = True new_timestamp = now + timedelta(seconds=data["remaining"]) if self._attr_native_value: From 7ae8bd5137c4dc76bfd9c4a069280044b09425ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Jun 2022 13:15:50 -1000 Subject: [PATCH 1338/3516] Remove sqlalchemy lambda_stmt usage from history, logbook, and statistics (#73191) --- .../components/logbook/queries/__init__.py | 26 ++-- .../components/logbook/queries/all.py | 20 ++- .../components/logbook/queries/devices.py | 42 +++--- .../components/logbook/queries/entities.py | 47 +++---- .../logbook/queries/entities_and_devices.py | 59 ++++---- homeassistant/components/recorder/history.py | 130 ++++++++---------- .../components/recorder/statistics.py | 99 ++++++------- homeassistant/components/recorder/util.py | 10 +- tests/components/recorder/test_util.py | 23 ++-- 9 files changed, 203 insertions(+), 253 deletions(-) diff --git a/homeassistant/components/logbook/queries/__init__.py b/homeassistant/components/logbook/queries/__init__.py index 3c027823612..a59ebc94b87 100644 --- a/homeassistant/components/logbook/queries/__init__.py +++ b/homeassistant/components/logbook/queries/__init__.py @@ -2,8 +2,9 @@ from __future__ import annotations from datetime import datetime as dt +import json -from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Select from homeassistant.components.recorder.filters import Filters @@ -21,7 +22,7 @@ def statement_for_request( device_ids: list[str] | None = None, filters: Filters | None = None, context_id: str | None = None, -) -> StatementLambdaElement: +) -> Select: """Generate the logbook statement for a logbook request.""" # No entities: logbook sends everything for the timeframe @@ -38,41 +39,36 @@ def statement_for_request( context_id, ) - # sqlalchemy caches object quoting, the - # json quotable ones must be a different - # object from the non-json ones to prevent - # sqlalchemy from quoting them incorrectly - # entities and devices: logbook sends everything for the timeframe for the entities and devices if entity_ids and device_ids: - json_quotable_entity_ids = list(entity_ids) - json_quotable_device_ids = list(device_ids) + json_quoted_entity_ids = [json.dumps(entity_id) for entity_id in entity_ids] + json_quoted_device_ids = [json.dumps(device_id) for device_id in device_ids] return entities_devices_stmt( start_day, end_day, event_types, entity_ids, - json_quotable_entity_ids, - json_quotable_device_ids, + json_quoted_entity_ids, + json_quoted_device_ids, ) # entities: logbook sends everything for the timeframe for the entities if entity_ids: - json_quotable_entity_ids = list(entity_ids) + json_quoted_entity_ids = [json.dumps(entity_id) for entity_id in entity_ids] return entities_stmt( start_day, end_day, event_types, entity_ids, - json_quotable_entity_ids, + json_quoted_entity_ids, ) # devices: logbook sends everything for the timeframe for the devices assert device_ids is not None - json_quotable_device_ids = list(device_ids) + json_quoted_device_ids = [json.dumps(device_id) for device_id in device_ids] return devices_stmt( start_day, end_day, event_types, - json_quotable_device_ids, + json_quoted_device_ids, ) diff --git a/homeassistant/components/logbook/queries/all.py b/homeassistant/components/logbook/queries/all.py index da05aa02fff..e0a651c7972 100644 --- a/homeassistant/components/logbook/queries/all.py +++ b/homeassistant/components/logbook/queries/all.py @@ -3,10 +3,9 @@ from __future__ import annotations from datetime import datetime as dt -from sqlalchemy import lambda_stmt from sqlalchemy.orm import Query from sqlalchemy.sql.elements import ClauseList -from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Select from homeassistant.components.recorder.db_schema import ( LAST_UPDATED_INDEX, @@ -29,32 +28,29 @@ def all_stmt( states_entity_filter: ClauseList | None = None, events_entity_filter: ClauseList | None = None, context_id: str | None = None, -) -> StatementLambdaElement: +) -> Select: """Generate a logbook query for all entities.""" - stmt = lambda_stmt( - lambda: select_events_without_states(start_day, end_day, event_types) - ) + stmt = select_events_without_states(start_day, end_day, event_types) if context_id is not None: # Once all the old `state_changed` events # are gone from the database remove the # _legacy_select_events_context_id() - stmt += lambda s: s.where(Events.context_id == context_id).union_all( + stmt = stmt.where(Events.context_id == context_id).union_all( _states_query_for_context_id(start_day, end_day, context_id), legacy_select_events_context_id(start_day, end_day, context_id), ) else: if events_entity_filter is not None: - stmt += lambda s: s.where(events_entity_filter) + stmt = stmt.where(events_entity_filter) if states_entity_filter is not None: - stmt += lambda s: s.union_all( + stmt = stmt.union_all( _states_query_for_all(start_day, end_day).where(states_entity_filter) ) else: - stmt += lambda s: s.union_all(_states_query_for_all(start_day, end_day)) + stmt = stmt.union_all(_states_query_for_all(start_day, end_day)) - stmt += lambda s: s.order_by(Events.time_fired) - return stmt + return stmt.order_by(Events.time_fired) def _states_query_for_all(start_day: dt, end_day: dt) -> Query: diff --git a/homeassistant/components/logbook/queries/devices.py b/homeassistant/components/logbook/queries/devices.py index f750c552bc4..cbe766fb02c 100644 --- a/homeassistant/components/logbook/queries/devices.py +++ b/homeassistant/components/logbook/queries/devices.py @@ -4,11 +4,10 @@ from __future__ import annotations from collections.abc import Iterable from datetime import datetime as dt -from sqlalchemy import lambda_stmt, select +from sqlalchemy import select from sqlalchemy.orm import Query from sqlalchemy.sql.elements import ClauseList -from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import CTE, CompoundSelect +from sqlalchemy.sql.selectable import CTE, CompoundSelect, Select from homeassistant.components.recorder.db_schema import ( DEVICE_ID_IN_EVENT, @@ -31,11 +30,11 @@ def _select_device_id_context_ids_sub_query( start_day: dt, end_day: dt, event_types: tuple[str, ...], - json_quotable_device_ids: list[str], + json_quoted_device_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple devices.""" inner = select_events_context_id_subquery(start_day, end_day, event_types).where( - apply_event_device_id_matchers(json_quotable_device_ids) + apply_event_device_id_matchers(json_quoted_device_ids) ) return select(inner.c.context_id).group_by(inner.c.context_id) @@ -45,14 +44,14 @@ def _apply_devices_context_union( start_day: dt, end_day: dt, event_types: tuple[str, ...], - json_quotable_device_ids: list[str], + json_quoted_device_ids: list[str], ) -> CompoundSelect: """Generate a CTE to find the device context ids and a query to find linked row.""" devices_cte: CTE = _select_device_id_context_ids_sub_query( start_day, end_day, event_types, - json_quotable_device_ids, + json_quoted_device_ids, ).cte() return query.union_all( apply_events_context_hints( @@ -72,25 +71,22 @@ def devices_stmt( start_day: dt, end_day: dt, event_types: tuple[str, ...], - json_quotable_device_ids: list[str], -) -> StatementLambdaElement: + json_quoted_device_ids: list[str], +) -> Select: """Generate a logbook query for multiple devices.""" - stmt = lambda_stmt( - lambda: _apply_devices_context_union( - select_events_without_states(start_day, end_day, event_types).where( - apply_event_device_id_matchers(json_quotable_device_ids) - ), - start_day, - end_day, - event_types, - json_quotable_device_ids, - ).order_by(Events.time_fired) - ) - return stmt + return _apply_devices_context_union( + select_events_without_states(start_day, end_day, event_types).where( + apply_event_device_id_matchers(json_quoted_device_ids) + ), + start_day, + end_day, + event_types, + json_quoted_device_ids, + ).order_by(Events.time_fired) def apply_event_device_id_matchers( - json_quotable_device_ids: Iterable[str], + json_quoted_device_ids: Iterable[str], ) -> ClauseList: """Create matchers for the device_ids in the event_data.""" - return DEVICE_ID_IN_EVENT.in_(json_quotable_device_ids) + return DEVICE_ID_IN_EVENT.in_(json_quoted_device_ids) diff --git a/homeassistant/components/logbook/queries/entities.py b/homeassistant/components/logbook/queries/entities.py index 4ef96c100d7..4d250fbb0f1 100644 --- a/homeassistant/components/logbook/queries/entities.py +++ b/homeassistant/components/logbook/queries/entities.py @@ -5,10 +5,9 @@ from collections.abc import Iterable from datetime import datetime as dt import sqlalchemy -from sqlalchemy import lambda_stmt, select, union_all +from sqlalchemy import select, union_all from sqlalchemy.orm import Query -from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import CTE, CompoundSelect +from sqlalchemy.sql.selectable import CTE, CompoundSelect, Select from homeassistant.components.recorder.db_schema import ( ENTITY_ID_IN_EVENT, @@ -36,12 +35,12 @@ def _select_entities_context_ids_sub_query( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], + json_quoted_entity_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple entities.""" union = union_all( select_events_context_id_subquery(start_day, end_day, event_types).where( - apply_event_entity_id_matchers(json_quotable_entity_ids) + apply_event_entity_id_matchers(json_quoted_entity_ids) ), apply_entities_hints(select(States.context_id)) .filter((States.last_updated > start_day) & (States.last_updated < end_day)) @@ -56,7 +55,7 @@ def _apply_entities_context_union( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], + json_quoted_entity_ids: list[str], ) -> CompoundSelect: """Generate a CTE to find the entity and device context ids and a query to find linked row.""" entities_cte: CTE = _select_entities_context_ids_sub_query( @@ -64,7 +63,7 @@ def _apply_entities_context_union( end_day, event_types, entity_ids, - json_quotable_entity_ids, + json_quoted_entity_ids, ).cte() # We used to optimize this to exclude rows we already in the union with # a States.entity_id.not_in(entity_ids) but that made the @@ -91,21 +90,19 @@ def entities_stmt( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], -) -> StatementLambdaElement: + json_quoted_entity_ids: list[str], +) -> Select: """Generate a logbook query for multiple entities.""" - return lambda_stmt( - lambda: _apply_entities_context_union( - select_events_without_states(start_day, end_day, event_types).where( - apply_event_entity_id_matchers(json_quotable_entity_ids) - ), - start_day, - end_day, - event_types, - entity_ids, - json_quotable_entity_ids, - ).order_by(Events.time_fired) - ) + return _apply_entities_context_union( + select_events_without_states(start_day, end_day, event_types).where( + apply_event_entity_id_matchers(json_quoted_entity_ids) + ), + start_day, + end_day, + event_types, + entity_ids, + json_quoted_entity_ids, + ).order_by(Events.time_fired) def states_query_for_entity_ids( @@ -118,12 +115,12 @@ def states_query_for_entity_ids( def apply_event_entity_id_matchers( - json_quotable_entity_ids: Iterable[str], + json_quoted_entity_ids: Iterable[str], ) -> sqlalchemy.or_: """Create matchers for the entity_id in the event_data.""" - return ENTITY_ID_IN_EVENT.in_( - json_quotable_entity_ids - ) | OLD_ENTITY_ID_IN_EVENT.in_(json_quotable_entity_ids) + return ENTITY_ID_IN_EVENT.in_(json_quoted_entity_ids) | OLD_ENTITY_ID_IN_EVENT.in_( + json_quoted_entity_ids + ) def apply_entities_hints(query: Query) -> Query: diff --git a/homeassistant/components/logbook/queries/entities_and_devices.py b/homeassistant/components/logbook/queries/entities_and_devices.py index 591918dd653..8b8051e2966 100644 --- a/homeassistant/components/logbook/queries/entities_and_devices.py +++ b/homeassistant/components/logbook/queries/entities_and_devices.py @@ -5,10 +5,9 @@ from collections.abc import Iterable from datetime import datetime as dt import sqlalchemy -from sqlalchemy import lambda_stmt, select, union_all +from sqlalchemy import select, union_all from sqlalchemy.orm import Query -from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import CTE, CompoundSelect +from sqlalchemy.sql.selectable import CTE, CompoundSelect, Select from homeassistant.components.recorder.db_schema import EventData, Events, States @@ -33,14 +32,14 @@ def _select_entities_device_id_context_ids_sub_query( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], - json_quotable_device_ids: list[str], + json_quoted_entity_ids: list[str], + json_quoted_device_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple entities and multiple devices.""" union = union_all( select_events_context_id_subquery(start_day, end_day, event_types).where( _apply_event_entity_id_device_id_matchers( - json_quotable_entity_ids, json_quotable_device_ids + json_quoted_entity_ids, json_quoted_device_ids ) ), apply_entities_hints(select(States.context_id)) @@ -56,16 +55,16 @@ def _apply_entities_devices_context_union( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], - json_quotable_device_ids: list[str], + json_quoted_entity_ids: list[str], + json_quoted_device_ids: list[str], ) -> CompoundSelect: devices_entities_cte: CTE = _select_entities_device_id_context_ids_sub_query( start_day, end_day, event_types, entity_ids, - json_quotable_entity_ids, - json_quotable_device_ids, + json_quoted_entity_ids, + json_quoted_device_ids, ).cte() # We used to optimize this to exclude rows we already in the union with # a States.entity_id.not_in(entity_ids) but that made the @@ -92,32 +91,30 @@ def entities_devices_stmt( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], - json_quotable_device_ids: list[str], -) -> StatementLambdaElement: + json_quoted_entity_ids: list[str], + json_quoted_device_ids: list[str], +) -> Select: """Generate a logbook query for multiple entities.""" - stmt = lambda_stmt( - lambda: _apply_entities_devices_context_union( - select_events_without_states(start_day, end_day, event_types).where( - _apply_event_entity_id_device_id_matchers( - json_quotable_entity_ids, json_quotable_device_ids - ) - ), - start_day, - end_day, - event_types, - entity_ids, - json_quotable_entity_ids, - json_quotable_device_ids, - ).order_by(Events.time_fired) - ) + stmt = _apply_entities_devices_context_union( + select_events_without_states(start_day, end_day, event_types).where( + _apply_event_entity_id_device_id_matchers( + json_quoted_entity_ids, json_quoted_device_ids + ) + ), + start_day, + end_day, + event_types, + entity_ids, + json_quoted_entity_ids, + json_quoted_device_ids, + ).order_by(Events.time_fired) return stmt def _apply_event_entity_id_device_id_matchers( - json_quotable_entity_ids: Iterable[str], json_quotable_device_ids: Iterable[str] + json_quoted_entity_ids: Iterable[str], json_quoted_device_ids: Iterable[str] ) -> sqlalchemy.or_: """Create matchers for the device_id and entity_id in the event_data.""" return apply_event_entity_id_matchers( - json_quotable_entity_ids - ) | apply_event_device_id_matchers(json_quotable_device_ids) + json_quoted_entity_ids + ) | apply_event_device_id_matchers(json_quoted_device_ids) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index e1eca282a3a..1238b63f3c9 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -9,13 +9,11 @@ import logging import time from typing import Any, cast -from sqlalchemy import Column, Text, and_, func, lambda_stmt, or_, select +from sqlalchemy import Column, Text, and_, func, or_, select from sqlalchemy.engine.row import Row -from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal -from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import Subquery +from sqlalchemy.sql.selectable import Select, Subquery from homeassistant.components import recorder from homeassistant.components.websocket_api.const import ( @@ -34,7 +32,7 @@ from .models import ( process_timestamp_to_utc_isoformat, row_to_compressed_state, ) -from .util import execute_stmt_lambda_element, session_scope +from .util import execute_stmt, session_scope # mypy: allow-untyped-defs, no-check-untyped-defs @@ -114,22 +112,18 @@ def _schema_version(hass: HomeAssistant) -> int: return recorder.get_instance(hass).schema_version -def lambda_stmt_and_join_attributes( +def stmt_and_join_attributes( schema_version: int, no_attributes: bool, include_last_changed: bool = True -) -> tuple[StatementLambdaElement, bool]: - """Return the lambda_stmt and if StateAttributes should be joined. - - Because these are lambda_stmt the values inside the lambdas need - to be explicitly written out to avoid caching the wrong values. - """ +) -> tuple[Select, bool]: + """Return the stmt and if StateAttributes should be joined.""" # If no_attributes was requested we do the query # without the attributes fields and do not join the # state_attributes table if no_attributes: if include_last_changed: - return lambda_stmt(lambda: select(*QUERY_STATE_NO_ATTR)), False + return select(*QUERY_STATE_NO_ATTR), False return ( - lambda_stmt(lambda: select(*QUERY_STATE_NO_ATTR_NO_LAST_CHANGED)), + select(*QUERY_STATE_NO_ATTR_NO_LAST_CHANGED), False, ) # If we in the process of migrating schema we do @@ -138,19 +132,19 @@ def lambda_stmt_and_join_attributes( if schema_version < 25: if include_last_changed: return ( - lambda_stmt(lambda: select(*QUERY_STATES_PRE_SCHEMA_25)), + select(*QUERY_STATES_PRE_SCHEMA_25), False, ) return ( - lambda_stmt(lambda: select(*QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED)), + select(*QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED), False, ) # Finally if no migration is in progress and no_attributes # was not requested, we query both attributes columns and # join state_attributes if include_last_changed: - return lambda_stmt(lambda: select(*QUERY_STATES)), True - return lambda_stmt(lambda: select(*QUERY_STATES_NO_LAST_CHANGED)), True + return select(*QUERY_STATES), True + return select(*QUERY_STATES_NO_LAST_CHANGED), True def get_significant_states( @@ -182,7 +176,7 @@ def get_significant_states( ) -def _ignore_domains_filter(query: Query) -> Query: +def _ignore_domains_filter(query: Select) -> Select: """Add a filter to ignore domains we do not fetch history for.""" return query.filter( and_( @@ -202,9 +196,9 @@ def _significant_states_stmt( filters: Filters | None, significant_changes_only: bool, no_attributes: bool, -) -> StatementLambdaElement: +) -> Select: """Query the database for significant state changes.""" - stmt, join_attributes = lambda_stmt_and_join_attributes( + stmt, join_attributes = stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=not significant_changes_only ) if ( @@ -213,11 +207,11 @@ def _significant_states_stmt( and significant_changes_only and split_entity_id(entity_ids[0])[0] not in SIGNIFICANT_DOMAINS ): - stmt += lambda q: q.filter( + stmt = stmt.filter( (States.last_changed == States.last_updated) | States.last_changed.is_(None) ) elif significant_changes_only: - stmt += lambda q: q.filter( + stmt = stmt.filter( or_( *[ States.entity_id.like(entity_domain) @@ -231,25 +225,22 @@ def _significant_states_stmt( ) if entity_ids: - stmt += lambda q: q.filter(States.entity_id.in_(entity_ids)) + stmt = stmt.filter(States.entity_id.in_(entity_ids)) else: - stmt += _ignore_domains_filter + stmt = _ignore_domains_filter(stmt) if filters and filters.has_config: entity_filter = filters.states_entity_filter() - stmt = stmt.add_criteria( - lambda q: q.filter(entity_filter), track_on=[filters] - ) + stmt = stmt.filter(entity_filter) - stmt += lambda q: q.filter(States.last_updated > start_time) + stmt = stmt.filter(States.last_updated > start_time) if end_time: - stmt += lambda q: q.filter(States.last_updated < end_time) + stmt = stmt.filter(States.last_updated < end_time) if join_attributes: - stmt += lambda q: q.outerjoin( + stmt = stmt.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) - stmt += lambda q: q.order_by(States.entity_id, States.last_updated) - return stmt + return stmt.order_by(States.entity_id, States.last_updated) def get_significant_states_with_session( @@ -286,9 +277,7 @@ def get_significant_states_with_session( significant_changes_only, no_attributes, ) - states = execute_stmt_lambda_element( - session, stmt, None if entity_ids else start_time, end_time - ) + states = execute_stmt(session, stmt, None if entity_ids else start_time, end_time) return _sorted_states_to_dict( hass, session, @@ -340,28 +329,28 @@ def _state_changed_during_period_stmt( no_attributes: bool, descending: bool, limit: int | None, -) -> StatementLambdaElement: - stmt, join_attributes = lambda_stmt_and_join_attributes( +) -> Select: + stmt, join_attributes = stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=False ) - stmt += lambda q: q.filter( + stmt = stmt.filter( ((States.last_changed == States.last_updated) | States.last_changed.is_(None)) & (States.last_updated > start_time) ) if end_time: - stmt += lambda q: q.filter(States.last_updated < end_time) + stmt = stmt.filter(States.last_updated < end_time) if entity_id: - stmt += lambda q: q.filter(States.entity_id == entity_id) + stmt = stmt.filter(States.entity_id == entity_id) if join_attributes: - stmt += lambda q: q.outerjoin( + stmt = stmt.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) if descending: - stmt += lambda q: q.order_by(States.entity_id, States.last_updated.desc()) + stmt = stmt.order_by(States.entity_id, States.last_updated.desc()) else: - stmt += lambda q: q.order_by(States.entity_id, States.last_updated) + stmt = stmt.order_by(States.entity_id, States.last_updated) if limit: - stmt += lambda q: q.limit(limit) + stmt = stmt.limit(limit) return stmt @@ -389,7 +378,7 @@ def state_changes_during_period( descending, limit, ) - states = execute_stmt_lambda_element( + states = execute_stmt( session, stmt, None if entity_id else start_time, end_time ) return cast( @@ -407,23 +396,22 @@ def state_changes_during_period( def _get_last_state_changes_stmt( schema_version: int, number_of_states: int, entity_id: str | None -) -> StatementLambdaElement: - stmt, join_attributes = lambda_stmt_and_join_attributes( +) -> Select: + stmt, join_attributes = stmt_and_join_attributes( schema_version, False, include_last_changed=False ) - stmt += lambda q: q.filter( + stmt = stmt.filter( (States.last_changed == States.last_updated) | States.last_changed.is_(None) ) if entity_id: - stmt += lambda q: q.filter(States.entity_id == entity_id) + stmt = stmt.filter(States.entity_id == entity_id) if join_attributes: - stmt += lambda q: q.outerjoin( + stmt = stmt.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) - stmt += lambda q: q.order_by(States.entity_id, States.last_updated.desc()).limit( + return stmt.order_by(States.entity_id, States.last_updated.desc()).limit( number_of_states ) - return stmt def get_last_state_changes( @@ -438,7 +426,7 @@ def get_last_state_changes( stmt = _get_last_state_changes_stmt( _schema_version(hass), number_of_states, entity_id ) - states = list(execute_stmt_lambda_element(session, stmt)) + states = list(execute_stmt(session, stmt)) return cast( MutableMapping[str, list[State]], _sorted_states_to_dict( @@ -458,14 +446,14 @@ def _get_states_for_entites_stmt( utc_point_in_time: datetime, entity_ids: list[str], no_attributes: bool, -) -> StatementLambdaElement: +) -> Select: """Baked query to get states for specific entities.""" - stmt, join_attributes = lambda_stmt_and_join_attributes( + stmt, join_attributes = stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=True ) # We got an include-list of entities, accelerate the query by filtering already # in the inner query. - stmt += lambda q: q.where( + stmt = stmt.where( States.state_id == ( select(func.max(States.state_id).label("max_state_id")) @@ -479,7 +467,7 @@ def _get_states_for_entites_stmt( ).c.max_state_id ) if join_attributes: - stmt += lambda q: q.outerjoin( + stmt = stmt.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) return stmt @@ -510,9 +498,9 @@ def _get_states_for_all_stmt( utc_point_in_time: datetime, filters: Filters | None, no_attributes: bool, -) -> StatementLambdaElement: +) -> Select: """Baked query to get states for all entities.""" - stmt, join_attributes = lambda_stmt_and_join_attributes( + stmt, join_attributes = stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=True ) # We did not get an include-list of entities, query all states in the inner @@ -522,7 +510,7 @@ def _get_states_for_all_stmt( most_recent_states_by_date = _generate_most_recent_states_by_date( run_start, utc_point_in_time ) - stmt += lambda q: q.where( + stmt = stmt.where( States.state_id == ( select(func.max(States.state_id).label("max_state_id")) @@ -538,12 +526,12 @@ def _get_states_for_all_stmt( .subquery() ).c.max_state_id, ) - stmt += _ignore_domains_filter + stmt = _ignore_domains_filter(stmt) if filters and filters.has_config: entity_filter = filters.states_entity_filter() - stmt = stmt.add_criteria(lambda q: q.filter(entity_filter), track_on=[filters]) + stmt = stmt.filter(entity_filter) if join_attributes: - stmt += lambda q: q.outerjoin( + stmt = stmt.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) return stmt @@ -561,7 +549,7 @@ def _get_rows_with_session( """Return the states at a specific point in time.""" schema_version = _schema_version(hass) if entity_ids and len(entity_ids) == 1: - return execute_stmt_lambda_element( + return execute_stmt( session, _get_single_entity_states_stmt( schema_version, utc_point_in_time, entity_ids[0], no_attributes @@ -586,7 +574,7 @@ def _get_rows_with_session( schema_version, run.start, utc_point_in_time, filters, no_attributes ) - return execute_stmt_lambda_element(session, stmt) + return execute_stmt(session, stmt) def _get_single_entity_states_stmt( @@ -594,14 +582,14 @@ def _get_single_entity_states_stmt( utc_point_in_time: datetime, entity_id: str, no_attributes: bool = False, -) -> StatementLambdaElement: +) -> Select: # Use an entirely different (and extremely fast) query if we only # have a single entity id - stmt, join_attributes = lambda_stmt_and_join_attributes( + stmt, join_attributes = stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=True ) - stmt += ( - lambda q: q.filter( + stmt = ( + stmt.filter( States.last_updated < utc_point_in_time, States.entity_id == entity_id, ) @@ -609,7 +597,7 @@ def _get_single_entity_states_stmt( .limit(1) ) if join_attributes: - stmt += lambda q: q.outerjoin( + stmt = stmt.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) return stmt diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 26221aa199b..8d314830ec4 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -14,13 +14,12 @@ import re from statistics import mean from typing import TYPE_CHECKING, Any, Literal, overload -from sqlalchemy import bindparam, func, lambda_stmt, select +from sqlalchemy import bindparam, func, select from sqlalchemy.engine.row import Row from sqlalchemy.exc import SQLAlchemyError, StatementError from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal_column, true -from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import Subquery +from sqlalchemy.sql.selectable import Select, Subquery import voluptuous as vol from homeassistant.const import ( @@ -50,12 +49,7 @@ from .models import ( process_timestamp, process_timestamp_to_utc_isoformat, ) -from .util import ( - execute, - execute_stmt_lambda_element, - retryable_database_job, - session_scope, -) +from .util import execute, execute_stmt, retryable_database_job, session_scope if TYPE_CHECKING: from . import Recorder @@ -480,10 +474,10 @@ def delete_statistics_meta_duplicates(session: Session) -> None: def _compile_hourly_statistics_summary_mean_stmt( start_time: datetime, end_time: datetime -) -> StatementLambdaElement: +) -> Select: """Generate the summary mean statement for hourly statistics.""" - return lambda_stmt( - lambda: select(*QUERY_STATISTICS_SUMMARY_MEAN) + return ( + select(*QUERY_STATISTICS_SUMMARY_MEAN) .filter(StatisticsShortTerm.start >= start_time) .filter(StatisticsShortTerm.start < end_time) .group_by(StatisticsShortTerm.metadata_id) @@ -506,7 +500,7 @@ def compile_hourly_statistics( # Compute last hour's average, min, max summary: dict[str, StatisticData] = {} stmt = _compile_hourly_statistics_summary_mean_stmt(start_time, end_time) - stats = execute_stmt_lambda_element(session, stmt) + stats = execute_stmt(session, stmt) if stats: for stat in stats: @@ -688,17 +682,17 @@ def _generate_get_metadata_stmt( statistic_ids: list[str] | tuple[str] | None = None, statistic_type: Literal["mean"] | Literal["sum"] | None = None, statistic_source: str | None = None, -) -> StatementLambdaElement: +) -> Select: """Generate a statement to fetch metadata.""" - stmt = lambda_stmt(lambda: select(*QUERY_STATISTIC_META)) + stmt = select(*QUERY_STATISTIC_META) if statistic_ids is not None: - stmt += lambda q: q.where(StatisticsMeta.statistic_id.in_(statistic_ids)) + stmt = stmt.where(StatisticsMeta.statistic_id.in_(statistic_ids)) if statistic_source is not None: - stmt += lambda q: q.where(StatisticsMeta.source == statistic_source) + stmt = stmt.where(StatisticsMeta.source == statistic_source) if statistic_type == "mean": - stmt += lambda q: q.where(StatisticsMeta.has_mean == true()) + stmt = stmt.where(StatisticsMeta.has_mean == true()) elif statistic_type == "sum": - stmt += lambda q: q.where(StatisticsMeta.has_sum == true()) + stmt = stmt.where(StatisticsMeta.has_sum == true()) return stmt @@ -720,7 +714,7 @@ def get_metadata_with_session( # Fetch metatadata from the database stmt = _generate_get_metadata_stmt(statistic_ids, statistic_type, statistic_source) - result = execute_stmt_lambda_element(session, stmt) + result = execute_stmt(session, stmt) if not result: return {} @@ -982,44 +976,30 @@ def _statistics_during_period_stmt( start_time: datetime, end_time: datetime | None, metadata_ids: list[int] | None, -) -> StatementLambdaElement: - """Prepare a database query for statistics during a given period. - - This prepares a lambda_stmt query, so we don't insert the parameters yet. - """ - stmt = lambda_stmt( - lambda: select(*QUERY_STATISTICS).filter(Statistics.start >= start_time) - ) +) -> Select: + """Prepare a database query for statistics during a given period.""" + stmt = select(*QUERY_STATISTICS).filter(Statistics.start >= start_time) if end_time is not None: - stmt += lambda q: q.filter(Statistics.start < end_time) + stmt = stmt.filter(Statistics.start < end_time) if metadata_ids: - stmt += lambda q: q.filter(Statistics.metadata_id.in_(metadata_ids)) - stmt += lambda q: q.order_by(Statistics.metadata_id, Statistics.start) - return stmt + stmt = stmt.filter(Statistics.metadata_id.in_(metadata_ids)) + return stmt.order_by(Statistics.metadata_id, Statistics.start) def _statistics_during_period_stmt_short_term( start_time: datetime, end_time: datetime | None, metadata_ids: list[int] | None, -) -> StatementLambdaElement: - """Prepare a database query for short term statistics during a given period. - - This prepares a lambda_stmt query, so we don't insert the parameters yet. - """ - stmt = lambda_stmt( - lambda: select(*QUERY_STATISTICS_SHORT_TERM).filter( - StatisticsShortTerm.start >= start_time - ) +) -> Select: + """Prepare a database query for short term statistics during a given period.""" + stmt = select(*QUERY_STATISTICS_SHORT_TERM).filter( + StatisticsShortTerm.start >= start_time ) if end_time is not None: - stmt += lambda q: q.filter(StatisticsShortTerm.start < end_time) + stmt = stmt.filter(StatisticsShortTerm.start < end_time) if metadata_ids: - stmt += lambda q: q.filter(StatisticsShortTerm.metadata_id.in_(metadata_ids)) - stmt += lambda q: q.order_by( - StatisticsShortTerm.metadata_id, StatisticsShortTerm.start - ) - return stmt + stmt = stmt.filter(StatisticsShortTerm.metadata_id.in_(metadata_ids)) + return stmt.order_by(StatisticsShortTerm.metadata_id, StatisticsShortTerm.start) def statistics_during_period( @@ -1054,7 +1034,7 @@ def statistics_during_period( else: table = Statistics stmt = _statistics_during_period_stmt(start_time, end_time, metadata_ids) - stats = execute_stmt_lambda_element(session, stmt) + stats = execute_stmt(session, stmt) if not stats: return {} @@ -1085,10 +1065,10 @@ def statistics_during_period( def _get_last_statistics_stmt( metadata_id: int, number_of_stats: int, -) -> StatementLambdaElement: +) -> Select: """Generate a statement for number_of_stats statistics for a given statistic_id.""" - return lambda_stmt( - lambda: select(*QUERY_STATISTICS) + return ( + select(*QUERY_STATISTICS) .filter_by(metadata_id=metadata_id) .order_by(Statistics.metadata_id, Statistics.start.desc()) .limit(number_of_stats) @@ -1098,10 +1078,10 @@ def _get_last_statistics_stmt( def _get_last_statistics_short_term_stmt( metadata_id: int, number_of_stats: int, -) -> StatementLambdaElement: +) -> Select: """Generate a statement for number_of_stats short term statistics for a given statistic_id.""" - return lambda_stmt( - lambda: select(*QUERY_STATISTICS_SHORT_TERM) + return ( + select(*QUERY_STATISTICS_SHORT_TERM) .filter_by(metadata_id=metadata_id) .order_by(StatisticsShortTerm.metadata_id, StatisticsShortTerm.start.desc()) .limit(number_of_stats) @@ -1127,7 +1107,7 @@ def _get_last_statistics( stmt = _get_last_statistics_stmt(metadata_id, number_of_stats) else: stmt = _get_last_statistics_short_term_stmt(metadata_id, number_of_stats) - stats = execute_stmt_lambda_element(session, stmt) + stats = execute_stmt(session, stmt) if not stats: return {} @@ -1177,11 +1157,11 @@ def _generate_most_recent_statistic_row(metadata_ids: list[int]) -> Subquery: def _latest_short_term_statistics_stmt( metadata_ids: list[int], -) -> StatementLambdaElement: +) -> Select: """Create the statement for finding the latest short term stat rows.""" - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SHORT_TERM)) + stmt = select(*QUERY_STATISTICS_SHORT_TERM) most_recent_statistic_row = _generate_most_recent_statistic_row(metadata_ids) - stmt += lambda s: s.join( + return stmt.join( most_recent_statistic_row, ( StatisticsShortTerm.metadata_id # pylint: disable=comparison-with-callable @@ -1189,7 +1169,6 @@ def _latest_short_term_statistics_stmt( ) & (StatisticsShortTerm.start == most_recent_statistic_row.c.start_max), ) - return stmt def get_latest_short_term_statistics( @@ -1212,7 +1191,7 @@ def get_latest_short_term_statistics( if statistic_id in metadata ] stmt = _latest_short_term_statistics_stmt(metadata_ids) - stats = execute_stmt_lambda_element(session, stmt) + stats = execute_stmt(session, stmt) if not stats: return {} diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index c1fbc831987..7e183f7f64f 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -22,7 +22,6 @@ from sqlalchemy.engine.row import Row from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session -from sqlalchemy.sql.lambdas import StatementLambdaElement from typing_extensions import Concatenate, ParamSpec from homeassistant.core import HomeAssistant @@ -166,9 +165,9 @@ def execute( assert False # unreachable # pragma: no cover -def execute_stmt_lambda_element( +def execute_stmt( session: Session, - stmt: StatementLambdaElement, + query: Query, start_time: datetime | None = None, end_time: datetime | None = None, yield_per: int | None = DEFAULT_YIELD_STATES_ROWS, @@ -184,11 +183,12 @@ def execute_stmt_lambda_element( specific entities) since they are usually faster with .all(). """ - executed = session.execute(stmt) use_all = not start_time or ((end_time or dt_util.utcnow()) - start_time).days <= 1 for tryno in range(0, RETRIES): try: - return executed.all() if use_all else executed.yield_per(yield_per) # type: ignore[no-any-return] + if use_all: + return session.execute(query).all() # type: ignore[no-any-return] + return session.execute(query).yield_per(yield_per) # type: ignore[no-any-return] except SQLAlchemyError as err: _LOGGER.error("Error executing query: %s", err) if tryno == RETRIES - 1: diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 8624719f951..97cf4a58b5c 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -9,7 +9,6 @@ from sqlalchemy import text from sqlalchemy.engine.result import ChunkedIteratorResult from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.sql.elements import TextClause -from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components import recorder from homeassistant.components.recorder import history, util @@ -713,8 +712,8 @@ def test_build_mysqldb_conv(): @patch("homeassistant.components.recorder.util.QUERY_RETRY_WAIT", 0) -def test_execute_stmt_lambda_element(hass_recorder): - """Test executing with execute_stmt_lambda_element.""" +def test_execute_stmt(hass_recorder): + """Test executing with execute_stmt.""" hass = hass_recorder() instance = recorder.get_instance(hass) hass.states.set("sensor.on", "on") @@ -725,13 +724,15 @@ def test_execute_stmt_lambda_element(hass_recorder): one_week_from_now = now + timedelta(days=7) class MockExecutor: + + _calls = 0 + def __init__(self, stmt): - assert isinstance(stmt, StatementLambdaElement) - self.calls = 0 + """Init the mock.""" def all(self): - self.calls += 1 - if self.calls == 2: + MockExecutor._calls += 1 + if MockExecutor._calls == 2: return ["mock_row"] raise SQLAlchemyError @@ -740,24 +741,24 @@ def test_execute_stmt_lambda_element(hass_recorder): stmt = history._get_single_entity_states_stmt( instance.schema_version, dt_util.utcnow(), "sensor.on", False ) - rows = util.execute_stmt_lambda_element(session, stmt) + rows = util.execute_stmt(session, stmt) assert isinstance(rows, list) assert rows[0].state == new_state.state assert rows[0].entity_id == new_state.entity_id # Time window >= 2 days, we get a ChunkedIteratorResult - rows = util.execute_stmt_lambda_element(session, stmt, now, one_week_from_now) + rows = util.execute_stmt(session, stmt, now, one_week_from_now) assert isinstance(rows, ChunkedIteratorResult) row = next(rows) assert row.state == new_state.state assert row.entity_id == new_state.entity_id # Time window < 2 days, we get a list - rows = util.execute_stmt_lambda_element(session, stmt, now, tomorrow) + rows = util.execute_stmt(session, stmt, now, tomorrow) assert isinstance(rows, list) assert rows[0].state == new_state.state assert rows[0].entity_id == new_state.entity_id with patch.object(session, "execute", MockExecutor): - rows = util.execute_stmt_lambda_element(session, stmt, now, tomorrow) + rows = util.execute_stmt(session, stmt, now, tomorrow) assert rows == ["mock_row"] From 70473df2fe4d8df1c2921b9af055035a399968de Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 6 Jun 2022 17:18:07 -0500 Subject: [PATCH 1339/3516] Fix errors when unjoining multiple Sonos devices simultaneously (#73133) --- .../components/sonos/media_player.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index f331f980bb4..938a651c34d 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -751,17 +751,23 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): media_content_type, ) - def join_players(self, group_members): + async def async_join_players(self, group_members): """Join `group_members` as a player group with the current player.""" - speakers = [] - for entity_id in group_members: - if speaker := self.hass.data[DATA_SONOS].entity_id_mappings.get(entity_id): - speakers.append(speaker) - else: - raise HomeAssistantError(f"Not a known Sonos entity_id: {entity_id}") + async with self.hass.data[DATA_SONOS].topology_condition: + speakers = [] + for entity_id in group_members: + if speaker := self.hass.data[DATA_SONOS].entity_id_mappings.get( + entity_id + ): + speakers.append(speaker) + else: + raise HomeAssistantError( + f"Not a known Sonos entity_id: {entity_id}" + ) - self.speaker.join(speakers) + await self.hass.async_add_executor_job(self.speaker.join, speakers) - def unjoin_player(self): + async def async_unjoin_player(self): """Remove this player from any group.""" - self.speaker.unjoin() + async with self.hass.data[DATA_SONOS].topology_condition: + await self.hass.async_add_executor_job(self.speaker.unjoin) From f4ed7720de3ccdc893a67c9308251f6585da5017 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Mon, 6 Jun 2022 23:46:52 +0200 Subject: [PATCH 1340/3516] Bump async-upnp-client==0.31.1 (#73135) Co-authored-by: J. Nick Koston --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/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 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 15beea714da..cc72f5d4778 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.30.1"], + "requirements": ["async-upnp-client==0.31.1"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index 21329440788..590d1b8370a 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dms", - "requirements": ["async-upnp-client==0.30.1"], + "requirements": ["async-upnp-client==0.31.1"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index fd97eb12e54..ce65af7d8bb 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -7,7 +7,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.5.0", "wakeonlan==2.0.1", - "async-upnp-client==0.30.1" + "async-upnp-client==0.31.1" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 5cbd3d0d10e..f0db05d9015 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.30.1"], + "requirements": ["async-upnp-client==0.31.1"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 2e76dac4adb..dc87e73fdee 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.30.1", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.31.1", "getmac==0.8.2"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman", "@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 5ec224498be..57d32f315ba 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.10", "async-upnp-client==0.30.1"], + "requirements": ["yeelight==0.7.10", "async-upnp-client==0.31.1"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 076b58b5185..bec79680e0d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.11 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.30.1 +async-upnp-client==0.31.1 async_timeout==4.0.2 atomicwrites==1.4.0 attrs==21.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index e99b2ce353b..9da35195c7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -336,7 +336,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.30.1 +async-upnp-client==0.31.1 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3dfb583ad2e..49613212cf7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -278,7 +278,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.30.1 +async-upnp-client==0.31.1 # homeassistant.components.sleepiq asyncsleepiq==1.2.3 From e886d3712471a42ac101098caacb72a5070746a2 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 8 Jun 2022 01:01:44 +0200 Subject: [PATCH 1341/3516] Use default None for voltage property of FritzDevice in Fritz!Smarthome (#73141) use default None for device.voltage --- homeassistant/components/fritzbox/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index e590f14ce89..2ae7f9dccc8 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -96,7 +96,9 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] - native_value=lambda device: device.voltage / 1000 if device.voltage else 0.0, + native_value=lambda device: device.voltage / 1000 + if getattr(device, "voltage", None) + else 0.0, ), FritzSensorEntityDescription( key="electric_current", @@ -106,7 +108,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] native_value=lambda device: device.power / device.voltage - if device.power and device.voltage + if device.power and getattr(device, "voltage", None) else 0.0, ), FritzSensorEntityDescription( From fa56e3633d383c508ac358f75b9f531133afabd8 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 7 Jun 2022 11:02:08 +1200 Subject: [PATCH 1342/3516] Fix KeyError from ESPHome media players on startup (#73149) --- .../components/esphome/media_player.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index f9027142ae2..d7ce73976e7 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -25,7 +25,12 @@ from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import EsphomeEntity, EsphomeEnumMapper, platform_async_setup_entry +from . import ( + EsphomeEntity, + EsphomeEnumMapper, + esphome_state_property, + platform_async_setup_entry, +) async def async_setup_entry( @@ -54,6 +59,10 @@ _STATES: EsphomeEnumMapper[MediaPlayerState, str] = EsphomeEnumMapper( ) +# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property +# pylint: disable=invalid-overridden-method + + class EsphomeMediaPlayer( EsphomeEntity[MediaPlayerInfo, MediaPlayerEntityState], MediaPlayerEntity ): @@ -61,17 +70,17 @@ class EsphomeMediaPlayer( _attr_device_class = MediaPlayerDeviceClass.SPEAKER - @property + @esphome_state_property def state(self) -> str | None: """Return current state.""" return _STATES.from_esphome(self._state.state) - @property + @esphome_state_property def is_volume_muted(self) -> bool: """Return true if volume is muted.""" return self._state.muted - @property + @esphome_state_property def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" return self._state.volume From af248fa386ba0de4bb7131927bd2a244941bfcfa Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 7 Jun 2022 17:14:40 -0600 Subject: [PATCH 1343/3516] Fix bugs with RainMachine zone run time sensors (#73179) --- .../components/rainmachine/sensor.py | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index cc37189aa49..1144ceea159 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -4,6 +4,8 @@ from __future__ import annotations from dataclasses import dataclass from datetime import datetime, timedelta +from regenmaschine.controller import Controller + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -13,8 +15,9 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS, VOLUME_CUBIC_METERS from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity import EntityCategory, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.dt import utcnow from . import RainMachineEntity @@ -205,16 +208,33 @@ class ZoneTimeRemainingSensor(RainMachineEntity, SensorEntity): entity_description: RainMachineSensorDescriptionUid + def __init__( + self, + entry: ConfigEntry, + coordinator: DataUpdateCoordinator, + controller: Controller, + description: EntityDescription, + ) -> None: + """Initialize.""" + super().__init__(entry, coordinator, controller, description) + + self._running_or_queued: bool = False + @callback def update_from_latest_data(self) -> None: """Update the state.""" data = self.coordinator.data[self.entity_description.uid] now = utcnow() - if RUN_STATE_MAP.get(data["state"]) != RunStates.RUNNING: - # If the zone isn't actively running, return immediately: + if RUN_STATE_MAP.get(data["state"]) == RunStates.NOT_RUNNING: + if self._running_or_queued: + # If we go from running to not running, update the state to be right + # now (i.e., the time the zone stopped running): + self._attr_native_value = now + self._running_or_queued = False return + self._running_or_queued = True new_timestamp = now + timedelta(seconds=data["remaining"]) if self._attr_native_value: From d6b1a7ca68735f568dcd7b1a0c14c7c2c902d71c Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 8 Jun 2022 01:11:38 +0200 Subject: [PATCH 1344/3516] Fix creating unique IDs for WiFi switches in Fritz!Tools (#73183) --- homeassistant/components/fritz/switch.py | 11 +- tests/components/fritz/conftest.py | 12 +- tests/components/fritz/const.py | 3 +- tests/components/fritz/test_button.py | 2 +- tests/components/fritz/test_config_flow.py | 2 +- tests/components/fritz/test_diagnostics.py | 2 +- tests/components/fritz/test_init.py | 2 +- tests/components/fritz/test_sensor.py | 2 +- tests/components/fritz/test_switch.py | 189 +++++++++++++++++++++ tests/components/fritz/test_update.py | 2 +- 10 files changed, 216 insertions(+), 11 deletions(-) create mode 100644 tests/components/fritz/test_switch.py diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index cac6e735a81..a45cd347463 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -169,7 +169,16 @@ def wifi_entities_list( } for i, network in networks.copy().items(): networks[i]["switch_name"] = network["ssid"] - if len([j for j, n in networks.items() if n["ssid"] == network["ssid"]]) > 1: + if ( + len( + [ + j + for j, n in networks.items() + if slugify(n["ssid"]) == slugify(network["ssid"]) + ] + ) + > 1 + ): networks[i]["switch_name"] += f" ({WIFI_STANDARD[i]})" _LOGGER.debug("WiFi networks list: %s", networks) diff --git a/tests/components/fritz/conftest.py b/tests/components/fritz/conftest.py index b073335f20a..edbde883f56 100644 --- a/tests/components/fritz/conftest.py +++ b/tests/components/fritz/conftest.py @@ -1,4 +1,4 @@ -"""Common stuff for AVM Fritz!Box tests.""" +"""Common stuff for Fritz!Tools tests.""" import logging from unittest.mock import MagicMock, patch @@ -73,13 +73,19 @@ class FritzHostMock(FritzHosts): return MOCK_MESH_DATA +@pytest.fixture(name="fc_data") +def fc_data_mock(): + """Fixture for default fc_data.""" + return MOCK_FB_SERVICES + + @pytest.fixture() -def fc_class_mock(): +def fc_class_mock(fc_data): """Fixture that sets up a mocked FritzConnection class.""" with patch( "homeassistant.components.fritz.common.FritzConnection", autospec=True ) as result: - result.return_value = FritzConnectionMock(MOCK_FB_SERVICES) + result.return_value = FritzConnectionMock(fc_data) yield result diff --git a/tests/components/fritz/const.py b/tests/components/fritz/const.py index f8f6f8370d7..32f2211d16b 100644 --- a/tests/components/fritz/const.py +++ b/tests/components/fritz/const.py @@ -1,4 +1,4 @@ -"""Common stuff for AVM Fritz!Box tests.""" +"""Common stuff for Fritz!Tools tests.""" from homeassistant.components import ssdp from homeassistant.components.fritz.const import DOMAIN from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN @@ -194,6 +194,7 @@ MOCK_FB_SERVICES: dict[str, dict] = { }, } + MOCK_MESH_DATA = { "schema_version": "1.9", "nodes": [ diff --git a/tests/components/fritz/test_button.py b/tests/components/fritz/test_button.py index a6ff579958a..a2bd6132731 100644 --- a/tests/components/fritz/test_button.py +++ b/tests/components/fritz/test_button.py @@ -1,4 +1,4 @@ -"""Tests for Shelly button platform.""" +"""Tests for Fritz!Tools button platform.""" from unittest.mock import patch import pytest diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 6d014782842..619f06f5493 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -1,4 +1,4 @@ -"""Tests for AVM Fritz!Box config flow.""" +"""Tests for Fritz!Tools config flow.""" import dataclasses from unittest.mock import patch diff --git a/tests/components/fritz/test_diagnostics.py b/tests/components/fritz/test_diagnostics.py index a4b4942c375..2c0b42d6bdf 100644 --- a/tests/components/fritz/test_diagnostics.py +++ b/tests/components/fritz/test_diagnostics.py @@ -1,4 +1,4 @@ -"""Tests for the AVM Fritz!Box integration.""" +"""Tests for Fritz!Tools diagnostics platform.""" from __future__ import annotations from aiohttp import ClientSession diff --git a/tests/components/fritz/test_init.py b/tests/components/fritz/test_init.py index fd67321d235..9f7a17de900 100644 --- a/tests/components/fritz/test_init.py +++ b/tests/components/fritz/test_init.py @@ -1,4 +1,4 @@ -"""Tests for AVM Fritz!Box.""" +"""Tests for Fritz!Tools.""" from unittest.mock import patch from fritzconnection.core.exceptions import FritzSecurityError diff --git a/tests/components/fritz/test_sensor.py b/tests/components/fritz/test_sensor.py index 31e142a3e47..73a7c1068ae 100644 --- a/tests/components/fritz/test_sensor.py +++ b/tests/components/fritz/test_sensor.py @@ -1,4 +1,4 @@ -"""Tests for Shelly button platform.""" +"""Tests for Fritz!Tools sensor platform.""" from __future__ import annotations from datetime import timedelta diff --git a/tests/components/fritz/test_switch.py b/tests/components/fritz/test_switch.py new file mode 100644 index 00000000000..1e744bb62e6 --- /dev/null +++ b/tests/components/fritz/test_switch.py @@ -0,0 +1,189 @@ +"""Tests for Fritz!Tools switch platform.""" +from __future__ import annotations + +import pytest + +from homeassistant.components.fritz.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .const import MOCK_FB_SERVICES, MOCK_USER_DATA + +from tests.common import MockConfigEntry + +MOCK_WLANCONFIGS_SAME_SSID: dict[str, dict] = { + "WLANConfiguration1": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewMaxBitRate": "Auto", + "NewChannel": 13, + "NewSSID": "WiFi", + "NewBeaconType": "11iandWPA3", + "NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3", + "NewMACAddressControlEnabled": False, + "NewStandard": "ax", + "NewBSSID": "1C:ED:6F:12:34:12", + "NewBasicEncryptionModes": "None", + "NewBasicAuthenticationMode": "None", + "NewMaxCharsSSID": 32, + "NewMinCharsSSID": 1, + "NewAllowedCharsSSID": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", + "NewMinCharsPSK": 64, + "NewMaxCharsPSK": 64, + "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", + } + }, + "WLANConfiguration2": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewMaxBitRate": "Auto", + "NewChannel": 52, + "NewSSID": "WiFi", + "NewBeaconType": "11iandWPA3", + "NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3", + "NewMACAddressControlEnabled": False, + "NewStandard": "ax", + "NewBSSID": "1C:ED:6F:12:34:13", + "NewBasicEncryptionModes": "None", + "NewBasicAuthenticationMode": "None", + "NewMaxCharsSSID": 32, + "NewMinCharsSSID": 1, + "NewAllowedCharsSSID": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", + "NewMinCharsPSK": 64, + "NewMaxCharsPSK": 64, + "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", + } + }, +} +MOCK_WLANCONFIGS_DIFF_SSID: dict[str, dict] = { + "WLANConfiguration1": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewMaxBitRate": "Auto", + "NewChannel": 13, + "NewSSID": "WiFi", + "NewBeaconType": "11iandWPA3", + "NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3", + "NewMACAddressControlEnabled": False, + "NewStandard": "ax", + "NewBSSID": "1C:ED:6F:12:34:12", + "NewBasicEncryptionModes": "None", + "NewBasicAuthenticationMode": "None", + "NewMaxCharsSSID": 32, + "NewMinCharsSSID": 1, + "NewAllowedCharsSSID": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", + "NewMinCharsPSK": 64, + "NewMaxCharsPSK": 64, + "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", + } + }, + "WLANConfiguration2": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewMaxBitRate": "Auto", + "NewChannel": 52, + "NewSSID": "WiFi2", + "NewBeaconType": "11iandWPA3", + "NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3", + "NewMACAddressControlEnabled": False, + "NewStandard": "ax", + "NewBSSID": "1C:ED:6F:12:34:13", + "NewBasicEncryptionModes": "None", + "NewBasicAuthenticationMode": "None", + "NewMaxCharsSSID": 32, + "NewMinCharsSSID": 1, + "NewAllowedCharsSSID": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", + "NewMinCharsPSK": 64, + "NewMaxCharsPSK": 64, + "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", + } + }, +} +MOCK_WLANCONFIGS_DIFF2_SSID: dict[str, dict] = { + "WLANConfiguration1": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewMaxBitRate": "Auto", + "NewChannel": 13, + "NewSSID": "WiFi", + "NewBeaconType": "11iandWPA3", + "NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3", + "NewMACAddressControlEnabled": False, + "NewStandard": "ax", + "NewBSSID": "1C:ED:6F:12:34:12", + "NewBasicEncryptionModes": "None", + "NewBasicAuthenticationMode": "None", + "NewMaxCharsSSID": 32, + "NewMinCharsSSID": 1, + "NewAllowedCharsSSID": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", + "NewMinCharsPSK": 64, + "NewMaxCharsPSK": 64, + "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", + } + }, + "WLANConfiguration2": { + "GetInfo": { + "NewEnable": True, + "NewStatus": "Up", + "NewMaxBitRate": "Auto", + "NewChannel": 52, + "NewSSID": "WiFi+", + "NewBeaconType": "11iandWPA3", + "NewX_AVM-DE_PossibleBeaconTypes": "None,11i,11iandWPA3", + "NewMACAddressControlEnabled": False, + "NewStandard": "ax", + "NewBSSID": "1C:ED:6F:12:34:13", + "NewBasicEncryptionModes": "None", + "NewBasicAuthenticationMode": "None", + "NewMaxCharsSSID": 32, + "NewMinCharsSSID": 1, + "NewAllowedCharsSSID": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", + "NewMinCharsPSK": 64, + "NewMaxCharsPSK": 64, + "NewAllowedCharsPSK": "0123456789ABCDEFabcdef", + } + }, +} + + +@pytest.mark.parametrize( + "fc_data, expected_wifi_names", + [ + ( + {**MOCK_FB_SERVICES, **MOCK_WLANCONFIGS_SAME_SSID}, + ["WiFi (2.4Ghz)", "WiFi (5Ghz)"], + ), + ({**MOCK_FB_SERVICES, **MOCK_WLANCONFIGS_DIFF_SSID}, ["WiFi", "WiFi2"]), + ( + {**MOCK_FB_SERVICES, **MOCK_WLANCONFIGS_DIFF2_SSID}, + ["WiFi (2.4Ghz)", "WiFi+ (5Ghz)"], + ), + ], +) +async def test_switch_setup( + hass: HomeAssistant, + expected_wifi_names: list[str], + fc_class_mock, + fh_class_mock, +): + """Test setup of Fritz!Tools switches.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + switches = hass.states.async_all(Platform.SWITCH) + assert len(switches) == 3 + assert switches[0].name == f"Mock Title Wi-Fi {expected_wifi_names[0]}" + assert switches[1].name == f"Mock Title Wi-Fi {expected_wifi_names[1]}" + assert switches[2].name == "printer Internet Access" diff --git a/tests/components/fritz/test_update.py b/tests/components/fritz/test_update.py index 2261ef3ef9b..32bdf361847 100644 --- a/tests/components/fritz/test_update.py +++ b/tests/components/fritz/test_update.py @@ -1,4 +1,4 @@ -"""The tests for the Fritzbox update entity.""" +"""Tests for Fritz!Tools update platform.""" from unittest.mock import patch From bc7cf1f6493cec7d71d752bb2134fd9913bb47b6 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Tue, 7 Jun 2022 14:39:15 -0700 Subject: [PATCH 1345/3516] Bump pywemo to 0.9.1 (#73186) --- homeassistant/components/wemo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 40bb8161d90..b324ba060ea 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -3,7 +3,7 @@ "name": "Belkin WeMo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wemo", - "requirements": ["pywemo==0.8.1"], + "requirements": ["pywemo==0.9.1"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/requirements_all.txt b/requirements_all.txt index 9da35195c7e..d0ac6af8c74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2023,7 +2023,7 @@ pyvolumio==0.1.5 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.8.1 +pywemo==0.9.1 # homeassistant.components.wilight pywilight==0.0.70 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 49613212cf7..24916a4ac0d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1343,7 +1343,7 @@ pyvolumio==0.1.5 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.8.1 +pywemo==0.9.1 # homeassistant.components.wilight pywilight==0.0.70 From d63569da82f7188ec6a5c8051a6def0dbb19b25e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Jun 2022 13:15:50 -1000 Subject: [PATCH 1346/3516] Remove sqlalchemy lambda_stmt usage from history, logbook, and statistics (#73191) --- .../components/logbook/queries/__init__.py | 26 ++-- .../components/logbook/queries/all.py | 20 ++- .../components/logbook/queries/devices.py | 42 +++--- .../components/logbook/queries/entities.py | 47 +++---- .../logbook/queries/entities_and_devices.py | 59 ++++---- homeassistant/components/recorder/history.py | 130 ++++++++---------- .../components/recorder/statistics.py | 99 ++++++------- homeassistant/components/recorder/util.py | 10 +- tests/components/recorder/test_util.py | 23 ++-- 9 files changed, 203 insertions(+), 253 deletions(-) diff --git a/homeassistant/components/logbook/queries/__init__.py b/homeassistant/components/logbook/queries/__init__.py index 3c027823612..a59ebc94b87 100644 --- a/homeassistant/components/logbook/queries/__init__.py +++ b/homeassistant/components/logbook/queries/__init__.py @@ -2,8 +2,9 @@ from __future__ import annotations from datetime import datetime as dt +import json -from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Select from homeassistant.components.recorder.filters import Filters @@ -21,7 +22,7 @@ def statement_for_request( device_ids: list[str] | None = None, filters: Filters | None = None, context_id: str | None = None, -) -> StatementLambdaElement: +) -> Select: """Generate the logbook statement for a logbook request.""" # No entities: logbook sends everything for the timeframe @@ -38,41 +39,36 @@ def statement_for_request( context_id, ) - # sqlalchemy caches object quoting, the - # json quotable ones must be a different - # object from the non-json ones to prevent - # sqlalchemy from quoting them incorrectly - # entities and devices: logbook sends everything for the timeframe for the entities and devices if entity_ids and device_ids: - json_quotable_entity_ids = list(entity_ids) - json_quotable_device_ids = list(device_ids) + json_quoted_entity_ids = [json.dumps(entity_id) for entity_id in entity_ids] + json_quoted_device_ids = [json.dumps(device_id) for device_id in device_ids] return entities_devices_stmt( start_day, end_day, event_types, entity_ids, - json_quotable_entity_ids, - json_quotable_device_ids, + json_quoted_entity_ids, + json_quoted_device_ids, ) # entities: logbook sends everything for the timeframe for the entities if entity_ids: - json_quotable_entity_ids = list(entity_ids) + json_quoted_entity_ids = [json.dumps(entity_id) for entity_id in entity_ids] return entities_stmt( start_day, end_day, event_types, entity_ids, - json_quotable_entity_ids, + json_quoted_entity_ids, ) # devices: logbook sends everything for the timeframe for the devices assert device_ids is not None - json_quotable_device_ids = list(device_ids) + json_quoted_device_ids = [json.dumps(device_id) for device_id in device_ids] return devices_stmt( start_day, end_day, event_types, - json_quotable_device_ids, + json_quoted_device_ids, ) diff --git a/homeassistant/components/logbook/queries/all.py b/homeassistant/components/logbook/queries/all.py index d321578f545..730b66eef52 100644 --- a/homeassistant/components/logbook/queries/all.py +++ b/homeassistant/components/logbook/queries/all.py @@ -3,10 +3,9 @@ from __future__ import annotations from datetime import datetime as dt -from sqlalchemy import lambda_stmt from sqlalchemy.orm import Query from sqlalchemy.sql.elements import ClauseList -from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Select from homeassistant.components.recorder.models import LAST_UPDATED_INDEX, Events, States @@ -25,32 +24,29 @@ def all_stmt( states_entity_filter: ClauseList | None = None, events_entity_filter: ClauseList | None = None, context_id: str | None = None, -) -> StatementLambdaElement: +) -> Select: """Generate a logbook query for all entities.""" - stmt = lambda_stmt( - lambda: select_events_without_states(start_day, end_day, event_types) - ) + stmt = select_events_without_states(start_day, end_day, event_types) if context_id is not None: # Once all the old `state_changed` events # are gone from the database remove the # _legacy_select_events_context_id() - stmt += lambda s: s.where(Events.context_id == context_id).union_all( + stmt = stmt.where(Events.context_id == context_id).union_all( _states_query_for_context_id(start_day, end_day, context_id), legacy_select_events_context_id(start_day, end_day, context_id), ) else: if events_entity_filter is not None: - stmt += lambda s: s.where(events_entity_filter) + stmt = stmt.where(events_entity_filter) if states_entity_filter is not None: - stmt += lambda s: s.union_all( + stmt = stmt.union_all( _states_query_for_all(start_day, end_day).where(states_entity_filter) ) else: - stmt += lambda s: s.union_all(_states_query_for_all(start_day, end_day)) + stmt = stmt.union_all(_states_query_for_all(start_day, end_day)) - stmt += lambda s: s.order_by(Events.time_fired) - return stmt + return stmt.order_by(Events.time_fired) def _states_query_for_all(start_day: dt, end_day: dt) -> Query: diff --git a/homeassistant/components/logbook/queries/devices.py b/homeassistant/components/logbook/queries/devices.py index 88e9f50a42c..4c09720348f 100644 --- a/homeassistant/components/logbook/queries/devices.py +++ b/homeassistant/components/logbook/queries/devices.py @@ -4,11 +4,10 @@ from __future__ import annotations from collections.abc import Iterable from datetime import datetime as dt -from sqlalchemy import lambda_stmt, select +from sqlalchemy import select from sqlalchemy.orm import Query from sqlalchemy.sql.elements import ClauseList -from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import CTE, CompoundSelect +from sqlalchemy.sql.selectable import CTE, CompoundSelect, Select from homeassistant.components.recorder.models import ( DEVICE_ID_IN_EVENT, @@ -31,11 +30,11 @@ def _select_device_id_context_ids_sub_query( start_day: dt, end_day: dt, event_types: tuple[str, ...], - json_quotable_device_ids: list[str], + json_quoted_device_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple devices.""" inner = select_events_context_id_subquery(start_day, end_day, event_types).where( - apply_event_device_id_matchers(json_quotable_device_ids) + apply_event_device_id_matchers(json_quoted_device_ids) ) return select(inner.c.context_id).group_by(inner.c.context_id) @@ -45,14 +44,14 @@ def _apply_devices_context_union( start_day: dt, end_day: dt, event_types: tuple[str, ...], - json_quotable_device_ids: list[str], + json_quoted_device_ids: list[str], ) -> CompoundSelect: """Generate a CTE to find the device context ids and a query to find linked row.""" devices_cte: CTE = _select_device_id_context_ids_sub_query( start_day, end_day, event_types, - json_quotable_device_ids, + json_quoted_device_ids, ).cte() return query.union_all( apply_events_context_hints( @@ -72,25 +71,22 @@ def devices_stmt( start_day: dt, end_day: dt, event_types: tuple[str, ...], - json_quotable_device_ids: list[str], -) -> StatementLambdaElement: + json_quoted_device_ids: list[str], +) -> Select: """Generate a logbook query for multiple devices.""" - stmt = lambda_stmt( - lambda: _apply_devices_context_union( - select_events_without_states(start_day, end_day, event_types).where( - apply_event_device_id_matchers(json_quotable_device_ids) - ), - start_day, - end_day, - event_types, - json_quotable_device_ids, - ).order_by(Events.time_fired) - ) - return stmt + return _apply_devices_context_union( + select_events_without_states(start_day, end_day, event_types).where( + apply_event_device_id_matchers(json_quoted_device_ids) + ), + start_day, + end_day, + event_types, + json_quoted_device_ids, + ).order_by(Events.time_fired) def apply_event_device_id_matchers( - json_quotable_device_ids: Iterable[str], + json_quoted_device_ids: Iterable[str], ) -> ClauseList: """Create matchers for the device_ids in the event_data.""" - return DEVICE_ID_IN_EVENT.in_(json_quotable_device_ids) + return DEVICE_ID_IN_EVENT.in_(json_quoted_device_ids) diff --git a/homeassistant/components/logbook/queries/entities.py b/homeassistant/components/logbook/queries/entities.py index 8de4a5eaf64..a13a0f154e6 100644 --- a/homeassistant/components/logbook/queries/entities.py +++ b/homeassistant/components/logbook/queries/entities.py @@ -5,10 +5,9 @@ from collections.abc import Iterable from datetime import datetime as dt import sqlalchemy -from sqlalchemy import lambda_stmt, select, union_all +from sqlalchemy import select, union_all from sqlalchemy.orm import Query -from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import CTE, CompoundSelect +from sqlalchemy.sql.selectable import CTE, CompoundSelect, Select from homeassistant.components.recorder.models import ( ENTITY_ID_IN_EVENT, @@ -36,12 +35,12 @@ def _select_entities_context_ids_sub_query( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], + json_quoted_entity_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple entities.""" union = union_all( select_events_context_id_subquery(start_day, end_day, event_types).where( - apply_event_entity_id_matchers(json_quotable_entity_ids) + apply_event_entity_id_matchers(json_quoted_entity_ids) ), apply_entities_hints(select(States.context_id)) .filter((States.last_updated > start_day) & (States.last_updated < end_day)) @@ -56,7 +55,7 @@ def _apply_entities_context_union( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], + json_quoted_entity_ids: list[str], ) -> CompoundSelect: """Generate a CTE to find the entity and device context ids and a query to find linked row.""" entities_cte: CTE = _select_entities_context_ids_sub_query( @@ -64,7 +63,7 @@ def _apply_entities_context_union( end_day, event_types, entity_ids, - json_quotable_entity_ids, + json_quoted_entity_ids, ).cte() # We used to optimize this to exclude rows we already in the union with # a States.entity_id.not_in(entity_ids) but that made the @@ -91,21 +90,19 @@ def entities_stmt( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], -) -> StatementLambdaElement: + json_quoted_entity_ids: list[str], +) -> Select: """Generate a logbook query for multiple entities.""" - return lambda_stmt( - lambda: _apply_entities_context_union( - select_events_without_states(start_day, end_day, event_types).where( - apply_event_entity_id_matchers(json_quotable_entity_ids) - ), - start_day, - end_day, - event_types, - entity_ids, - json_quotable_entity_ids, - ).order_by(Events.time_fired) - ) + return _apply_entities_context_union( + select_events_without_states(start_day, end_day, event_types).where( + apply_event_entity_id_matchers(json_quoted_entity_ids) + ), + start_day, + end_day, + event_types, + entity_ids, + json_quoted_entity_ids, + ).order_by(Events.time_fired) def states_query_for_entity_ids( @@ -118,12 +115,12 @@ def states_query_for_entity_ids( def apply_event_entity_id_matchers( - json_quotable_entity_ids: Iterable[str], + json_quoted_entity_ids: Iterable[str], ) -> sqlalchemy.or_: """Create matchers for the entity_id in the event_data.""" - return ENTITY_ID_IN_EVENT.in_( - json_quotable_entity_ids - ) | OLD_ENTITY_ID_IN_EVENT.in_(json_quotable_entity_ids) + return ENTITY_ID_IN_EVENT.in_(json_quoted_entity_ids) | OLD_ENTITY_ID_IN_EVENT.in_( + json_quoted_entity_ids + ) def apply_entities_hints(query: Query) -> Query: diff --git a/homeassistant/components/logbook/queries/entities_and_devices.py b/homeassistant/components/logbook/queries/entities_and_devices.py index 1c4271422b7..7514074cc85 100644 --- a/homeassistant/components/logbook/queries/entities_and_devices.py +++ b/homeassistant/components/logbook/queries/entities_and_devices.py @@ -5,10 +5,9 @@ from collections.abc import Iterable from datetime import datetime as dt import sqlalchemy -from sqlalchemy import lambda_stmt, select, union_all +from sqlalchemy import select, union_all from sqlalchemy.orm import Query -from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import CTE, CompoundSelect +from sqlalchemy.sql.selectable import CTE, CompoundSelect, Select from homeassistant.components.recorder.models import EventData, Events, States @@ -33,14 +32,14 @@ def _select_entities_device_id_context_ids_sub_query( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], - json_quotable_device_ids: list[str], + json_quoted_entity_ids: list[str], + json_quoted_device_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple entities and multiple devices.""" union = union_all( select_events_context_id_subquery(start_day, end_day, event_types).where( _apply_event_entity_id_device_id_matchers( - json_quotable_entity_ids, json_quotable_device_ids + json_quoted_entity_ids, json_quoted_device_ids ) ), apply_entities_hints(select(States.context_id)) @@ -56,16 +55,16 @@ def _apply_entities_devices_context_union( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], - json_quotable_device_ids: list[str], + json_quoted_entity_ids: list[str], + json_quoted_device_ids: list[str], ) -> CompoundSelect: devices_entities_cte: CTE = _select_entities_device_id_context_ids_sub_query( start_day, end_day, event_types, entity_ids, - json_quotable_entity_ids, - json_quotable_device_ids, + json_quoted_entity_ids, + json_quoted_device_ids, ).cte() # We used to optimize this to exclude rows we already in the union with # a States.entity_id.not_in(entity_ids) but that made the @@ -92,32 +91,30 @@ def entities_devices_stmt( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], - json_quotable_device_ids: list[str], -) -> StatementLambdaElement: + json_quoted_entity_ids: list[str], + json_quoted_device_ids: list[str], +) -> Select: """Generate a logbook query for multiple entities.""" - stmt = lambda_stmt( - lambda: _apply_entities_devices_context_union( - select_events_without_states(start_day, end_day, event_types).where( - _apply_event_entity_id_device_id_matchers( - json_quotable_entity_ids, json_quotable_device_ids - ) - ), - start_day, - end_day, - event_types, - entity_ids, - json_quotable_entity_ids, - json_quotable_device_ids, - ).order_by(Events.time_fired) - ) + stmt = _apply_entities_devices_context_union( + select_events_without_states(start_day, end_day, event_types).where( + _apply_event_entity_id_device_id_matchers( + json_quoted_entity_ids, json_quoted_device_ids + ) + ), + start_day, + end_day, + event_types, + entity_ids, + json_quoted_entity_ids, + json_quoted_device_ids, + ).order_by(Events.time_fired) return stmt def _apply_event_entity_id_device_id_matchers( - json_quotable_entity_ids: Iterable[str], json_quotable_device_ids: Iterable[str] + json_quoted_entity_ids: Iterable[str], json_quoted_device_ids: Iterable[str] ) -> sqlalchemy.or_: """Create matchers for the device_id and entity_id in the event_data.""" return apply_event_entity_id_matchers( - json_quotable_entity_ids - ) | apply_event_device_id_matchers(json_quotable_device_ids) + json_quoted_entity_ids + ) | apply_event_device_id_matchers(json_quoted_device_ids) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 37285f66d1d..b3fff62ae03 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -9,13 +9,11 @@ import logging import time from typing import Any, cast -from sqlalchemy import Column, Text, and_, func, lambda_stmt, or_, select +from sqlalchemy import Column, Text, and_, func, or_, select from sqlalchemy.engine.row import Row -from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal -from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import Subquery +from sqlalchemy.sql.selectable import Select, Subquery from homeassistant.components import recorder from homeassistant.components.websocket_api.const import ( @@ -36,7 +34,7 @@ from .models import ( process_timestamp_to_utc_isoformat, row_to_compressed_state, ) -from .util import execute_stmt_lambda_element, session_scope +from .util import execute_stmt, session_scope # mypy: allow-untyped-defs, no-check-untyped-defs @@ -116,22 +114,18 @@ def _schema_version(hass: HomeAssistant) -> int: return recorder.get_instance(hass).schema_version -def lambda_stmt_and_join_attributes( +def stmt_and_join_attributes( schema_version: int, no_attributes: bool, include_last_changed: bool = True -) -> tuple[StatementLambdaElement, bool]: - """Return the lambda_stmt and if StateAttributes should be joined. - - Because these are lambda_stmt the values inside the lambdas need - to be explicitly written out to avoid caching the wrong values. - """ +) -> tuple[Select, bool]: + """Return the stmt and if StateAttributes should be joined.""" # If no_attributes was requested we do the query # without the attributes fields and do not join the # state_attributes table if no_attributes: if include_last_changed: - return lambda_stmt(lambda: select(*QUERY_STATE_NO_ATTR)), False + return select(*QUERY_STATE_NO_ATTR), False return ( - lambda_stmt(lambda: select(*QUERY_STATE_NO_ATTR_NO_LAST_CHANGED)), + select(*QUERY_STATE_NO_ATTR_NO_LAST_CHANGED), False, ) # If we in the process of migrating schema we do @@ -140,19 +134,19 @@ def lambda_stmt_and_join_attributes( if schema_version < 25: if include_last_changed: return ( - lambda_stmt(lambda: select(*QUERY_STATES_PRE_SCHEMA_25)), + select(*QUERY_STATES_PRE_SCHEMA_25), False, ) return ( - lambda_stmt(lambda: select(*QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED)), + select(*QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED), False, ) # Finally if no migration is in progress and no_attributes # was not requested, we query both attributes columns and # join state_attributes if include_last_changed: - return lambda_stmt(lambda: select(*QUERY_STATES)), True - return lambda_stmt(lambda: select(*QUERY_STATES_NO_LAST_CHANGED)), True + return select(*QUERY_STATES), True + return select(*QUERY_STATES_NO_LAST_CHANGED), True def get_significant_states( @@ -184,7 +178,7 @@ def get_significant_states( ) -def _ignore_domains_filter(query: Query) -> Query: +def _ignore_domains_filter(query: Select) -> Select: """Add a filter to ignore domains we do not fetch history for.""" return query.filter( and_( @@ -204,9 +198,9 @@ def _significant_states_stmt( filters: Filters | None, significant_changes_only: bool, no_attributes: bool, -) -> StatementLambdaElement: +) -> Select: """Query the database for significant state changes.""" - stmt, join_attributes = lambda_stmt_and_join_attributes( + stmt, join_attributes = stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=not significant_changes_only ) if ( @@ -215,11 +209,11 @@ def _significant_states_stmt( and significant_changes_only and split_entity_id(entity_ids[0])[0] not in SIGNIFICANT_DOMAINS ): - stmt += lambda q: q.filter( + stmt = stmt.filter( (States.last_changed == States.last_updated) | States.last_changed.is_(None) ) elif significant_changes_only: - stmt += lambda q: q.filter( + stmt = stmt.filter( or_( *[ States.entity_id.like(entity_domain) @@ -233,25 +227,22 @@ def _significant_states_stmt( ) if entity_ids: - stmt += lambda q: q.filter(States.entity_id.in_(entity_ids)) + stmt = stmt.filter(States.entity_id.in_(entity_ids)) else: - stmt += _ignore_domains_filter + stmt = _ignore_domains_filter(stmt) if filters and filters.has_config: entity_filter = filters.states_entity_filter() - stmt = stmt.add_criteria( - lambda q: q.filter(entity_filter), track_on=[filters] - ) + stmt = stmt.filter(entity_filter) - stmt += lambda q: q.filter(States.last_updated > start_time) + stmt = stmt.filter(States.last_updated > start_time) if end_time: - stmt += lambda q: q.filter(States.last_updated < end_time) + stmt = stmt.filter(States.last_updated < end_time) if join_attributes: - stmt += lambda q: q.outerjoin( + stmt = stmt.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) - stmt += lambda q: q.order_by(States.entity_id, States.last_updated) - return stmt + return stmt.order_by(States.entity_id, States.last_updated) def get_significant_states_with_session( @@ -288,9 +279,7 @@ def get_significant_states_with_session( significant_changes_only, no_attributes, ) - states = execute_stmt_lambda_element( - session, stmt, None if entity_ids else start_time, end_time - ) + states = execute_stmt(session, stmt, None if entity_ids else start_time, end_time) return _sorted_states_to_dict( hass, session, @@ -342,28 +331,28 @@ def _state_changed_during_period_stmt( no_attributes: bool, descending: bool, limit: int | None, -) -> StatementLambdaElement: - stmt, join_attributes = lambda_stmt_and_join_attributes( +) -> Select: + stmt, join_attributes = stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=False ) - stmt += lambda q: q.filter( + stmt = stmt.filter( ((States.last_changed == States.last_updated) | States.last_changed.is_(None)) & (States.last_updated > start_time) ) if end_time: - stmt += lambda q: q.filter(States.last_updated < end_time) + stmt = stmt.filter(States.last_updated < end_time) if entity_id: - stmt += lambda q: q.filter(States.entity_id == entity_id) + stmt = stmt.filter(States.entity_id == entity_id) if join_attributes: - stmt += lambda q: q.outerjoin( + stmt = stmt.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) if descending: - stmt += lambda q: q.order_by(States.entity_id, States.last_updated.desc()) + stmt = stmt.order_by(States.entity_id, States.last_updated.desc()) else: - stmt += lambda q: q.order_by(States.entity_id, States.last_updated) + stmt = stmt.order_by(States.entity_id, States.last_updated) if limit: - stmt += lambda q: q.limit(limit) + stmt = stmt.limit(limit) return stmt @@ -391,7 +380,7 @@ def state_changes_during_period( descending, limit, ) - states = execute_stmt_lambda_element( + states = execute_stmt( session, stmt, None if entity_id else start_time, end_time ) return cast( @@ -409,23 +398,22 @@ def state_changes_during_period( def _get_last_state_changes_stmt( schema_version: int, number_of_states: int, entity_id: str | None -) -> StatementLambdaElement: - stmt, join_attributes = lambda_stmt_and_join_attributes( +) -> Select: + stmt, join_attributes = stmt_and_join_attributes( schema_version, False, include_last_changed=False ) - stmt += lambda q: q.filter( + stmt = stmt.filter( (States.last_changed == States.last_updated) | States.last_changed.is_(None) ) if entity_id: - stmt += lambda q: q.filter(States.entity_id == entity_id) + stmt = stmt.filter(States.entity_id == entity_id) if join_attributes: - stmt += lambda q: q.outerjoin( + stmt = stmt.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) - stmt += lambda q: q.order_by(States.entity_id, States.last_updated.desc()).limit( + return stmt.order_by(States.entity_id, States.last_updated.desc()).limit( number_of_states ) - return stmt def get_last_state_changes( @@ -440,7 +428,7 @@ def get_last_state_changes( stmt = _get_last_state_changes_stmt( _schema_version(hass), number_of_states, entity_id ) - states = list(execute_stmt_lambda_element(session, stmt)) + states = list(execute_stmt(session, stmt)) return cast( MutableMapping[str, list[State]], _sorted_states_to_dict( @@ -460,14 +448,14 @@ def _get_states_for_entites_stmt( utc_point_in_time: datetime, entity_ids: list[str], no_attributes: bool, -) -> StatementLambdaElement: +) -> Select: """Baked query to get states for specific entities.""" - stmt, join_attributes = lambda_stmt_and_join_attributes( + stmt, join_attributes = stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=True ) # We got an include-list of entities, accelerate the query by filtering already # in the inner query. - stmt += lambda q: q.where( + stmt = stmt.where( States.state_id == ( select(func.max(States.state_id).label("max_state_id")) @@ -481,7 +469,7 @@ def _get_states_for_entites_stmt( ).c.max_state_id ) if join_attributes: - stmt += lambda q: q.outerjoin( + stmt = stmt.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) return stmt @@ -512,9 +500,9 @@ def _get_states_for_all_stmt( utc_point_in_time: datetime, filters: Filters | None, no_attributes: bool, -) -> StatementLambdaElement: +) -> Select: """Baked query to get states for all entities.""" - stmt, join_attributes = lambda_stmt_and_join_attributes( + stmt, join_attributes = stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=True ) # We did not get an include-list of entities, query all states in the inner @@ -524,7 +512,7 @@ def _get_states_for_all_stmt( most_recent_states_by_date = _generate_most_recent_states_by_date( run_start, utc_point_in_time ) - stmt += lambda q: q.where( + stmt = stmt.where( States.state_id == ( select(func.max(States.state_id).label("max_state_id")) @@ -540,12 +528,12 @@ def _get_states_for_all_stmt( .subquery() ).c.max_state_id, ) - stmt += _ignore_domains_filter + stmt = _ignore_domains_filter(stmt) if filters and filters.has_config: entity_filter = filters.states_entity_filter() - stmt = stmt.add_criteria(lambda q: q.filter(entity_filter), track_on=[filters]) + stmt = stmt.filter(entity_filter) if join_attributes: - stmt += lambda q: q.outerjoin( + stmt = stmt.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) return stmt @@ -563,7 +551,7 @@ def _get_rows_with_session( """Return the states at a specific point in time.""" schema_version = _schema_version(hass) if entity_ids and len(entity_ids) == 1: - return execute_stmt_lambda_element( + return execute_stmt( session, _get_single_entity_states_stmt( schema_version, utc_point_in_time, entity_ids[0], no_attributes @@ -588,7 +576,7 @@ def _get_rows_with_session( schema_version, run.start, utc_point_in_time, filters, no_attributes ) - return execute_stmt_lambda_element(session, stmt) + return execute_stmt(session, stmt) def _get_single_entity_states_stmt( @@ -596,14 +584,14 @@ def _get_single_entity_states_stmt( utc_point_in_time: datetime, entity_id: str, no_attributes: bool = False, -) -> StatementLambdaElement: +) -> Select: # Use an entirely different (and extremely fast) query if we only # have a single entity id - stmt, join_attributes = lambda_stmt_and_join_attributes( + stmt, join_attributes = stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=True ) - stmt += ( - lambda q: q.filter( + stmt = ( + stmt.filter( States.last_updated < utc_point_in_time, States.entity_id == entity_id, ) @@ -611,7 +599,7 @@ def _get_single_entity_states_stmt( .limit(1) ) if join_attributes: - stmt += lambda q: q.outerjoin( + stmt = stmt.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) return stmt diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 012b34ec0ef..9a52a36ab5f 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -14,13 +14,12 @@ import re from statistics import mean from typing import TYPE_CHECKING, Any, Literal, overload -from sqlalchemy import bindparam, func, lambda_stmt, select +from sqlalchemy import bindparam, func, select from sqlalchemy.engine.row import Row from sqlalchemy.exc import SQLAlchemyError, StatementError from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal_column, true -from sqlalchemy.sql.lambdas import StatementLambdaElement -from sqlalchemy.sql.selectable import Subquery +from sqlalchemy.sql.selectable import Select, Subquery import voluptuous as vol from homeassistant.const import ( @@ -53,12 +52,7 @@ from .models import ( process_timestamp, process_timestamp_to_utc_isoformat, ) -from .util import ( - execute, - execute_stmt_lambda_element, - retryable_database_job, - session_scope, -) +from .util import execute, execute_stmt, retryable_database_job, session_scope if TYPE_CHECKING: from . import Recorder @@ -483,10 +477,10 @@ def delete_statistics_meta_duplicates(session: Session) -> None: def _compile_hourly_statistics_summary_mean_stmt( start_time: datetime, end_time: datetime -) -> StatementLambdaElement: +) -> Select: """Generate the summary mean statement for hourly statistics.""" - return lambda_stmt( - lambda: select(*QUERY_STATISTICS_SUMMARY_MEAN) + return ( + select(*QUERY_STATISTICS_SUMMARY_MEAN) .filter(StatisticsShortTerm.start >= start_time) .filter(StatisticsShortTerm.start < end_time) .group_by(StatisticsShortTerm.metadata_id) @@ -509,7 +503,7 @@ def compile_hourly_statistics( # Compute last hour's average, min, max summary: dict[str, StatisticData] = {} stmt = _compile_hourly_statistics_summary_mean_stmt(start_time, end_time) - stats = execute_stmt_lambda_element(session, stmt) + stats = execute_stmt(session, stmt) if stats: for stat in stats: @@ -691,17 +685,17 @@ def _generate_get_metadata_stmt( statistic_ids: list[str] | tuple[str] | None = None, statistic_type: Literal["mean"] | Literal["sum"] | None = None, statistic_source: str | None = None, -) -> StatementLambdaElement: +) -> Select: """Generate a statement to fetch metadata.""" - stmt = lambda_stmt(lambda: select(*QUERY_STATISTIC_META)) + stmt = select(*QUERY_STATISTIC_META) if statistic_ids is not None: - stmt += lambda q: q.where(StatisticsMeta.statistic_id.in_(statistic_ids)) + stmt = stmt.where(StatisticsMeta.statistic_id.in_(statistic_ids)) if statistic_source is not None: - stmt += lambda q: q.where(StatisticsMeta.source == statistic_source) + stmt = stmt.where(StatisticsMeta.source == statistic_source) if statistic_type == "mean": - stmt += lambda q: q.where(StatisticsMeta.has_mean == true()) + stmt = stmt.where(StatisticsMeta.has_mean == true()) elif statistic_type == "sum": - stmt += lambda q: q.where(StatisticsMeta.has_sum == true()) + stmt = stmt.where(StatisticsMeta.has_sum == true()) return stmt @@ -723,7 +717,7 @@ def get_metadata_with_session( # Fetch metatadata from the database stmt = _generate_get_metadata_stmt(statistic_ids, statistic_type, statistic_source) - result = execute_stmt_lambda_element(session, stmt) + result = execute_stmt(session, stmt) if not result: return {} @@ -985,44 +979,30 @@ def _statistics_during_period_stmt( start_time: datetime, end_time: datetime | None, metadata_ids: list[int] | None, -) -> StatementLambdaElement: - """Prepare a database query for statistics during a given period. - - This prepares a lambda_stmt query, so we don't insert the parameters yet. - """ - stmt = lambda_stmt( - lambda: select(*QUERY_STATISTICS).filter(Statistics.start >= start_time) - ) +) -> Select: + """Prepare a database query for statistics during a given period.""" + stmt = select(*QUERY_STATISTICS).filter(Statistics.start >= start_time) if end_time is not None: - stmt += lambda q: q.filter(Statistics.start < end_time) + stmt = stmt.filter(Statistics.start < end_time) if metadata_ids: - stmt += lambda q: q.filter(Statistics.metadata_id.in_(metadata_ids)) - stmt += lambda q: q.order_by(Statistics.metadata_id, Statistics.start) - return stmt + stmt = stmt.filter(Statistics.metadata_id.in_(metadata_ids)) + return stmt.order_by(Statistics.metadata_id, Statistics.start) def _statistics_during_period_stmt_short_term( start_time: datetime, end_time: datetime | None, metadata_ids: list[int] | None, -) -> StatementLambdaElement: - """Prepare a database query for short term statistics during a given period. - - This prepares a lambda_stmt query, so we don't insert the parameters yet. - """ - stmt = lambda_stmt( - lambda: select(*QUERY_STATISTICS_SHORT_TERM).filter( - StatisticsShortTerm.start >= start_time - ) +) -> Select: + """Prepare a database query for short term statistics during a given period.""" + stmt = select(*QUERY_STATISTICS_SHORT_TERM).filter( + StatisticsShortTerm.start >= start_time ) if end_time is not None: - stmt += lambda q: q.filter(StatisticsShortTerm.start < end_time) + stmt = stmt.filter(StatisticsShortTerm.start < end_time) if metadata_ids: - stmt += lambda q: q.filter(StatisticsShortTerm.metadata_id.in_(metadata_ids)) - stmt += lambda q: q.order_by( - StatisticsShortTerm.metadata_id, StatisticsShortTerm.start - ) - return stmt + stmt = stmt.filter(StatisticsShortTerm.metadata_id.in_(metadata_ids)) + return stmt.order_by(StatisticsShortTerm.metadata_id, StatisticsShortTerm.start) def statistics_during_period( @@ -1057,7 +1037,7 @@ def statistics_during_period( else: table = Statistics stmt = _statistics_during_period_stmt(start_time, end_time, metadata_ids) - stats = execute_stmt_lambda_element(session, stmt) + stats = execute_stmt(session, stmt) if not stats: return {} @@ -1088,10 +1068,10 @@ def statistics_during_period( def _get_last_statistics_stmt( metadata_id: int, number_of_stats: int, -) -> StatementLambdaElement: +) -> Select: """Generate a statement for number_of_stats statistics for a given statistic_id.""" - return lambda_stmt( - lambda: select(*QUERY_STATISTICS) + return ( + select(*QUERY_STATISTICS) .filter_by(metadata_id=metadata_id) .order_by(Statistics.metadata_id, Statistics.start.desc()) .limit(number_of_stats) @@ -1101,10 +1081,10 @@ def _get_last_statistics_stmt( def _get_last_statistics_short_term_stmt( metadata_id: int, number_of_stats: int, -) -> StatementLambdaElement: +) -> Select: """Generate a statement for number_of_stats short term statistics for a given statistic_id.""" - return lambda_stmt( - lambda: select(*QUERY_STATISTICS_SHORT_TERM) + return ( + select(*QUERY_STATISTICS_SHORT_TERM) .filter_by(metadata_id=metadata_id) .order_by(StatisticsShortTerm.metadata_id, StatisticsShortTerm.start.desc()) .limit(number_of_stats) @@ -1130,7 +1110,7 @@ def _get_last_statistics( stmt = _get_last_statistics_stmt(metadata_id, number_of_stats) else: stmt = _get_last_statistics_short_term_stmt(metadata_id, number_of_stats) - stats = execute_stmt_lambda_element(session, stmt) + stats = execute_stmt(session, stmt) if not stats: return {} @@ -1180,11 +1160,11 @@ def _generate_most_recent_statistic_row(metadata_ids: list[int]) -> Subquery: def _latest_short_term_statistics_stmt( metadata_ids: list[int], -) -> StatementLambdaElement: +) -> Select: """Create the statement for finding the latest short term stat rows.""" - stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SHORT_TERM)) + stmt = select(*QUERY_STATISTICS_SHORT_TERM) most_recent_statistic_row = _generate_most_recent_statistic_row(metadata_ids) - stmt += lambda s: s.join( + return stmt.join( most_recent_statistic_row, ( StatisticsShortTerm.metadata_id # pylint: disable=comparison-with-callable @@ -1192,7 +1172,6 @@ def _latest_short_term_statistics_stmt( ) & (StatisticsShortTerm.start == most_recent_statistic_row.c.start_max), ) - return stmt def get_latest_short_term_statistics( @@ -1215,7 +1194,7 @@ def get_latest_short_term_statistics( if statistic_id in metadata ] stmt = _latest_short_term_statistics_stmt(metadata_ids) - stats = execute_stmt_lambda_element(session, stmt) + stats = execute_stmt(session, stmt) if not stats: return {} diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 843f0e4b185..9c8a0c1eae3 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -22,7 +22,6 @@ from sqlalchemy.engine.row import Row from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session -from sqlalchemy.sql.lambdas import StatementLambdaElement from typing_extensions import Concatenate, ParamSpec from homeassistant.core import HomeAssistant @@ -167,9 +166,9 @@ def execute( assert False # unreachable # pragma: no cover -def execute_stmt_lambda_element( +def execute_stmt( session: Session, - stmt: StatementLambdaElement, + query: Query, start_time: datetime | None = None, end_time: datetime | None = None, yield_per: int | None = DEFAULT_YIELD_STATES_ROWS, @@ -185,11 +184,12 @@ def execute_stmt_lambda_element( specific entities) since they are usually faster with .all(). """ - executed = session.execute(stmt) use_all = not start_time or ((end_time or dt_util.utcnow()) - start_time).days <= 1 for tryno in range(0, RETRIES): try: - return executed.all() if use_all else executed.yield_per(yield_per) # type: ignore[no-any-return] + if use_all: + return session.execute(query).all() # type: ignore[no-any-return] + return session.execute(query).yield_per(yield_per) # type: ignore[no-any-return] except SQLAlchemyError as err: _LOGGER.error("Error executing query: %s", err) if tryno == RETRIES - 1: diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 343c57045cf..6ac6aabd023 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -9,7 +9,6 @@ from sqlalchemy import text from sqlalchemy.engine.result import ChunkedIteratorResult from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.sql.elements import TextClause -from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components import recorder from homeassistant.components.recorder import history, util @@ -712,8 +711,8 @@ def test_build_mysqldb_conv(): @patch("homeassistant.components.recorder.util.QUERY_RETRY_WAIT", 0) -def test_execute_stmt_lambda_element(hass_recorder): - """Test executing with execute_stmt_lambda_element.""" +def test_execute_stmt(hass_recorder): + """Test executing with execute_stmt.""" hass = hass_recorder() instance = recorder.get_instance(hass) hass.states.set("sensor.on", "on") @@ -724,13 +723,15 @@ def test_execute_stmt_lambda_element(hass_recorder): one_week_from_now = now + timedelta(days=7) class MockExecutor: + + _calls = 0 + def __init__(self, stmt): - assert isinstance(stmt, StatementLambdaElement) - self.calls = 0 + """Init the mock.""" def all(self): - self.calls += 1 - if self.calls == 2: + MockExecutor._calls += 1 + if MockExecutor._calls == 2: return ["mock_row"] raise SQLAlchemyError @@ -739,24 +740,24 @@ def test_execute_stmt_lambda_element(hass_recorder): stmt = history._get_single_entity_states_stmt( instance.schema_version, dt_util.utcnow(), "sensor.on", False ) - rows = util.execute_stmt_lambda_element(session, stmt) + rows = util.execute_stmt(session, stmt) assert isinstance(rows, list) assert rows[0].state == new_state.state assert rows[0].entity_id == new_state.entity_id # Time window >= 2 days, we get a ChunkedIteratorResult - rows = util.execute_stmt_lambda_element(session, stmt, now, one_week_from_now) + rows = util.execute_stmt(session, stmt, now, one_week_from_now) assert isinstance(rows, ChunkedIteratorResult) row = next(rows) assert row.state == new_state.state assert row.entity_id == new_state.entity_id # Time window < 2 days, we get a list - rows = util.execute_stmt_lambda_element(session, stmt, now, tomorrow) + rows = util.execute_stmt(session, stmt, now, tomorrow) assert isinstance(rows, list) assert rows[0].state == new_state.state assert rows[0].entity_id == new_state.entity_id with patch.object(session, "execute", MockExecutor): - rows = util.execute_stmt_lambda_element(session, stmt, now, tomorrow) + rows = util.execute_stmt(session, stmt, now, tomorrow) assert rows == ["mock_row"] From 2ee4cd02c7fe0c3cfaa8e0c2fc7b4ea5a7c3c384 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Jun 2022 16:16:30 -0700 Subject: [PATCH 1347/3516] Bumped version to 2022.6.4 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4287c7e96f8..7d887d1a4d7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index a16b51c03d8..ed1fdb27e4f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.3 +version = 2022.6.4 url = https://www.home-assistant.io/ [options] From 1331c75ec2dacd094fe0223f1457b69499239a35 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 8 Jun 2022 00:22:19 +0000 Subject: [PATCH 1348/3516] [ci skip] Translation update --- .../radiotherm/translations/bg.json | 22 ++++++ .../radiotherm/translations/et.json | 31 ++++++++ .../radiotherm/translations/ja.json | 31 ++++++++ .../radiotherm/translations/no.json | 31 ++++++++ .../components/scrape/translations/bg.json | 1 + .../components/scrape/translations/no.json | 73 +++++++++++++++++++ .../sensibo/translations/sensor.ca.json | 8 ++ .../sensibo/translations/sensor.de.json | 8 ++ .../sensibo/translations/sensor.el.json | 8 ++ .../sensibo/translations/sensor.et.json | 8 ++ .../sensibo/translations/sensor.fr.json | 8 ++ .../sensibo/translations/sensor.id.json | 8 ++ .../sensibo/translations/sensor.it.json | 8 ++ .../sensibo/translations/sensor.ja.json | 8 ++ .../sensibo/translations/sensor.nl.json | 8 ++ .../sensibo/translations/sensor.no.json | 8 ++ .../sensibo/translations/sensor.pl.json | 8 ++ .../sensibo/translations/sensor.pt-BR.json | 8 ++ .../sensibo/translations/sensor.zh-Hant.json | 8 ++ .../components/skybell/translations/no.json | 21 ++++++ .../switch_as_x/translations/cs.json | 2 +- 21 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/radiotherm/translations/bg.json create mode 100644 homeassistant/components/radiotherm/translations/et.json create mode 100644 homeassistant/components/radiotherm/translations/ja.json create mode 100644 homeassistant/components/radiotherm/translations/no.json create mode 100644 homeassistant/components/scrape/translations/no.json create mode 100644 homeassistant/components/sensibo/translations/sensor.ca.json create mode 100644 homeassistant/components/sensibo/translations/sensor.de.json create mode 100644 homeassistant/components/sensibo/translations/sensor.el.json create mode 100644 homeassistant/components/sensibo/translations/sensor.et.json create mode 100644 homeassistant/components/sensibo/translations/sensor.fr.json create mode 100644 homeassistant/components/sensibo/translations/sensor.id.json create mode 100644 homeassistant/components/sensibo/translations/sensor.it.json create mode 100644 homeassistant/components/sensibo/translations/sensor.ja.json create mode 100644 homeassistant/components/sensibo/translations/sensor.nl.json create mode 100644 homeassistant/components/sensibo/translations/sensor.no.json create mode 100644 homeassistant/components/sensibo/translations/sensor.pl.json create mode 100644 homeassistant/components/sensibo/translations/sensor.pt-BR.json create mode 100644 homeassistant/components/sensibo/translations/sensor.zh-Hant.json create mode 100644 homeassistant/components/skybell/translations/no.json diff --git a/homeassistant/components/radiotherm/translations/bg.json b/homeassistant/components/radiotherm/translations/bg.json new file mode 100644 index 00000000000..e588b6014ae --- /dev/null +++ b/homeassistant/components/radiotherm/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\u0448\u043d\u043e \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} {model} ({host})", + "step": { + "confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name} {model} ({host})?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/et.json b/homeassistant/components/radiotherm/translations/et.json new file mode 100644 index 00000000000..f8a6a7303ab --- /dev/null +++ b/homeassistant/components/radiotherm/translations/et.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "{name} {model} ( {host} )", + "step": { + "confirm": { + "description": "Kas seadistada {name}{model} ({host})?" + }, + "user": { + "data": { + "host": "Host" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "Temperatuuri reguleerimisel m\u00e4\u00e4ra p\u00fcsiv hoidmine." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/ja.json b/homeassistant/components/radiotherm/translations/ja.json new file mode 100644 index 00000000000..b792d0a1c9b --- /dev/null +++ b/homeassistant/components/radiotherm/translations/ja.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "{name} {model} ({host}) \u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u884c\u3044\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "\u6e29\u5ea6\u3092\u8abf\u6574\u3059\u308b\u3068\u304d\u306f\u3001\u6c38\u7d9a\u7684\u306a\u30db\u30fc\u30eb\u30c9(Permanent hold)\u3092\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/no.json b/homeassistant/components/radiotherm/translations/no.json new file mode 100644 index 00000000000..fc05e672cbe --- /dev/null +++ b/homeassistant/components/radiotherm/translations/no.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "flow_title": "{name} {model} ( {host} )", + "step": { + "confirm": { + "description": "Vil du konfigurere {name} {model} ( {host} )?" + }, + "user": { + "data": { + "host": "Vert" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "Still inn et permanent hold n\u00e5r du justerer temperaturen." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/bg.json b/homeassistant/components/scrape/translations/bg.json index 164dd477cb5..f22c3fd3b26 100644 --- a/homeassistant/components/scrape/translations/bg.json +++ b/homeassistant/components/scrape/translations/bg.json @@ -24,6 +24,7 @@ "attribute": "\u0410\u0442\u0440\u0438\u0431\u0443\u0442", "authentication": "\u0423\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "index": "\u0418\u043d\u0434\u0435\u043a\u0441", + "name": "\u0418\u043c\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0437\u0430 \u0438\u0437\u043c\u0435\u0440\u0432\u0430\u043d\u0435" } diff --git a/homeassistant/components/scrape/translations/no.json b/homeassistant/components/scrape/translations/no.json new file mode 100644 index 00000000000..6738c8a630a --- /dev/null +++ b/homeassistant/components/scrape/translations/no.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, + "step": { + "user": { + "data": { + "attribute": "Attributt", + "authentication": "Godkjenning", + "device_class": "Enhetsklasse", + "headers": "Overskrifter", + "index": "Indeks", + "name": "Navn", + "password": "Passord", + "resource": "Ressurs", + "select": "Velg", + "state_class": "Statsklasse", + "unit_of_measurement": "M\u00e5leenhet", + "username": "Brukernavn", + "value_template": "Verdimal", + "verify_ssl": "Verifisere SSL-sertifikat" + }, + "data_description": { + "attribute": "F\u00e5 verdien av et attributt p\u00e5 den valgte taggen", + "authentication": "Type HTTP-godkjenning. Enten grunnleggende eller ufullstendig", + "device_class": "Typen/klassen til sensoren for \u00e5 angi ikonet i frontend", + "headers": "Overskrifter som skal brukes for nettforesp\u00f8rselen", + "index": "Definerer hvilke av elementene som returneres av CSS-velgeren som skal brukes", + "resource": "URL-en til nettstedet som inneholder verdien", + "select": "Definerer hvilken tag som skal s\u00f8kes etter. Sjekk Beautifulsoup CSS-velgere for detaljer", + "state_class": "Sensorens state_class", + "value_template": "Definerer en mal for \u00e5 f\u00e5 tilstanden til sensoren", + "verify_ssl": "Aktiverer/deaktiverer verifisering av SSL/TLS-sertifikat, for eksempel hvis det er selvsignert" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "attribute": "Attributt", + "authentication": "Godkjenning", + "device_class": "Enhetsklasse", + "headers": "Overskrifter", + "index": "Indeks", + "name": "Navn", + "password": "Passord", + "resource": "Ressurs", + "select": "Velg", + "state_class": "Statsklasse", + "unit_of_measurement": "M\u00e5leenhet", + "username": "Brukernavn", + "value_template": "Verdimal", + "verify_ssl": "Verifisere SSL-sertifikat" + }, + "data_description": { + "attribute": "F\u00e5 verdien av et attributt p\u00e5 den valgte taggen", + "authentication": "Type HTTP-godkjenning. Enten grunnleggende eller ufullstendig", + "device_class": "Typen/klassen til sensoren for \u00e5 angi ikonet i frontend", + "headers": "Overskrifter som skal brukes for nettforesp\u00f8rselen", + "index": "Definerer hvilke av elementene som returneres av CSS-velgeren som skal brukes", + "resource": "URL-en til nettstedet som inneholder verdien", + "select": "Definerer hvilken tag som skal s\u00f8kes etter. Sjekk Beautifulsoup CSS-velgere for detaljer", + "state_class": "Sensorens state_class", + "value_template": "Definerer en mal for \u00e5 f\u00e5 tilstanden til sensoren", + "verify_ssl": "Aktiverer/deaktiverer verifisering av SSL/TLS-sertifikat, for eksempel hvis det er selvsignert" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.ca.json b/homeassistant/components/sensibo/translations/sensor.ca.json new file mode 100644 index 00000000000..1b251c70f7d --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.ca.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "Normal", + "s": "Sensible" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.de.json b/homeassistant/components/sensibo/translations/sensor.de.json new file mode 100644 index 00000000000..ab456f555af --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.de.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "Normal", + "s": "Empfindlich" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.el.json b/homeassistant/components/sensibo/translations/sensor.el.json new file mode 100644 index 00000000000..b4e595db882 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.el.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "\u039a\u03b1\u03bd\u03bf\u03bd\u03b9\u03ba\u03cc", + "s": "\u0395\u03c5\u03b1\u03af\u03c3\u03b8\u03b7\u03c4\u03bf" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.et.json b/homeassistant/components/sensibo/translations/sensor.et.json new file mode 100644 index 00000000000..44bdfe9183a --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.et.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "Tavaline", + "s": "Tundlik" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.fr.json b/homeassistant/components/sensibo/translations/sensor.fr.json new file mode 100644 index 00000000000..1b251c70f7d --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.fr.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "Normal", + "s": "Sensible" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.id.json b/homeassistant/components/sensibo/translations/sensor.id.json new file mode 100644 index 00000000000..54a0554ce41 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.id.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "Normal", + "s": "Sensitif" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.it.json b/homeassistant/components/sensibo/translations/sensor.it.json new file mode 100644 index 00000000000..85550808ee9 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.it.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "Normale", + "s": "Sensibile" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.ja.json b/homeassistant/components/sensibo/translations/sensor.ja.json new file mode 100644 index 00000000000..2921289358f --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.ja.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "\u30ce\u30fc\u30de\u30eb", + "s": "\u30bb\u30f3\u30b7\u30c6\u30a3\u30d6" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.nl.json b/homeassistant/components/sensibo/translations/sensor.nl.json new file mode 100644 index 00000000000..bd7b06dc940 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.nl.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "Normaal", + "s": "Gevoelig" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.no.json b/homeassistant/components/sensibo/translations/sensor.no.json new file mode 100644 index 00000000000..e3de20b4636 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.no.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "Vanlig", + "s": "F\u00f8lsom" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.pl.json b/homeassistant/components/sensibo/translations/sensor.pl.json new file mode 100644 index 00000000000..1b8ed8a8ed7 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.pl.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "normalna", + "s": "wysoka" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.pt-BR.json b/homeassistant/components/sensibo/translations/sensor.pt-BR.json new file mode 100644 index 00000000000..91d092a1760 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.pt-BR.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "Normal", + "s": "Sens\u00edvel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.zh-Hant.json b/homeassistant/components/sensibo/translations/sensor.zh-Hant.json new file mode 100644 index 00000000000..5144fdcc699 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.zh-Hant.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "\u6b63\u5e38", + "s": "\u654f\u611f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/no.json b/homeassistant/components/skybell/translations/no.json new file mode 100644 index 00000000000..8701b272f12 --- /dev/null +++ b/homeassistant/components/skybell/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen 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": { + "email": "E-post", + "password": "Passord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/cs.json b/homeassistant/components/switch_as_x/translations/cs.json index c521a79e1d9..b7dc53680d9 100644 --- a/homeassistant/components/switch_as_x/translations/cs.json +++ b/homeassistant/components/switch_as_x/translations/cs.json @@ -8,5 +8,5 @@ } } }, - "title": "Zm\u011bna typy vyp\u00edna\u010de" + "title": "Zm\u011bna typu vyp\u00edna\u010de" } \ No newline at end of file From 329595bf73af5c4bacaf0581ca0ad58f689149f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Jun 2022 14:26:06 -1000 Subject: [PATCH 1349/3516] Make radiotherm hold mode a switch (#73104) --- .coveragerc | 3 + .../components/radiotherm/__init__.py | 48 +++++++--- .../components/radiotherm/climate.py | 92 +++---------------- .../components/radiotherm/config_flow.py | 38 +------- homeassistant/components/radiotherm/const.py | 2 - .../components/radiotherm/coordinator.py | 15 +-- homeassistant/components/radiotherm/entity.py | 44 +++++++++ homeassistant/components/radiotherm/switch.py | 65 +++++++++++++ homeassistant/components/radiotherm/util.py | 24 +++++ .../components/radiotherm/test_config_flow.py | 45 +-------- 10 files changed, 195 insertions(+), 181 deletions(-) create mode 100644 homeassistant/components/radiotherm/entity.py create mode 100644 homeassistant/components/radiotherm/switch.py create mode 100644 homeassistant/components/radiotherm/util.py diff --git a/.coveragerc b/.coveragerc index 5fa747267a5..3db90953f74 100644 --- a/.coveragerc +++ b/.coveragerc @@ -963,9 +963,12 @@ omit = homeassistant/components/radio_browser/__init__.py homeassistant/components/radio_browser/media_source.py homeassistant/components/radiotherm/__init__.py + homeassistant/components/radiotherm/entity.py homeassistant/components/radiotherm/climate.py homeassistant/components/radiotherm/coordinator.py homeassistant/components/radiotherm/data.py + homeassistant/components/radiotherm/switch.py + homeassistant/components/radiotherm/util.py homeassistant/components/rainbird/* homeassistant/components/raincloud/* homeassistant/components/rainmachine/__init__.py diff --git a/homeassistant/components/radiotherm/__init__.py b/homeassistant/components/radiotherm/__init__.py index 9e389af6719..091d2bb8005 100644 --- a/homeassistant/components/radiotherm/__init__.py +++ b/homeassistant/components/radiotherm/__init__.py @@ -1,7 +1,9 @@ """The radiotherm component.""" from __future__ import annotations +from collections.abc import Coroutine from socket import timeout +from typing import Any, TypeVar from radiotherm.validate import RadiothermTstatError @@ -10,30 +12,46 @@ from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import CONF_HOLD_TEMP, DOMAIN +from .const import DOMAIN from .coordinator import RadioThermUpdateCoordinator from .data import async_get_init_data +from .util import async_set_time -PLATFORMS: list[Platform] = [Platform.CLIMATE] +PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SWITCH] + +_T = TypeVar("_T") + + +async def _async_call_or_raise_not_ready( + coro: Coroutine[Any, Any, _T], host: str +) -> _T: + """Call a coro or raise ConfigEntryNotReady.""" + try: + return await coro + except RadiothermTstatError as ex: + msg = f"{host} was busy (invalid value returned): {ex}" + raise ConfigEntryNotReady(msg) from ex + except timeout as ex: + msg = f"{host} timed out waiting for a response: {ex}" + raise ConfigEntryNotReady(msg) from ex async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Radio Thermostat from a config entry.""" host = entry.data[CONF_HOST] - try: - init_data = await async_get_init_data(hass, host) - except RadiothermTstatError as ex: - raise ConfigEntryNotReady( - f"{host} was busy (invalid value returned): {ex}" - ) from ex - except timeout as ex: - raise ConfigEntryNotReady( - f"{host} timed out waiting for a response: {ex}" - ) from ex - - hold_temp = entry.options[CONF_HOLD_TEMP] - coordinator = RadioThermUpdateCoordinator(hass, init_data, hold_temp) + init_coro = async_get_init_data(hass, host) + init_data = await _async_call_or_raise_not_ready(init_coro, host) + coordinator = RadioThermUpdateCoordinator(hass, init_data) await coordinator.async_config_entry_first_refresh() + + # Only set the time if the thermostat is + # not in hold mode since setting the time + # clears the hold for some strange design + # choice + if not coordinator.data.tstat["hold"]: + time_coro = async_set_time(hass, init_data.tstat) + await _async_call_or_raise_not_ready(time_coro, host) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index 63ee93c9c84..3f8e87e74a4 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -1,7 +1,6 @@ """Support for Radio Thermostat wifi-enabled home thermostats.""" from __future__ import annotations -from functools import partial import logging from typing import Any @@ -27,19 +26,13 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import device_registry as dr 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 -from homeassistant.util import dt as dt_util from . import DOMAIN -from .const import CONF_HOLD_TEMP from .coordinator import RadioThermUpdateCoordinator -from .data import RadioThermUpdate +from .entity import RadioThermostatEntity _LOGGER = logging.getLogger(__name__) @@ -92,10 +85,11 @@ PRESET_MODE_TO_CODE = { CODE_TO_PRESET_MODE = {v: k for k, v in PRESET_MODE_TO_CODE.items()} -CODE_TO_HOLD_STATE = {0: False, 1: True} PARALLEL_UPDATES = 1 +CONF_HOLD_TEMP = "hold_temp" + def round_temp(temperature): """Round a temperature to the resolution of the thermostat. @@ -150,18 +144,17 @@ async def async_setup_platform( _LOGGER.error("No Radiotherm Thermostats detected") return - hold_temp: bool = config[CONF_HOLD_TEMP] for host in hosts: hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, - data={CONF_HOST: host, CONF_HOLD_TEMP: hold_temp}, + data={CONF_HOST: host}, ) ) -class RadioThermostat(CoordinatorEntity[RadioThermUpdateCoordinator], ClimateEntity): +class RadioThermostat(RadioThermostatEntity, ClimateEntity): """Representation of a Radio Thermostat.""" _attr_hvac_modes = OPERATION_LIST @@ -171,49 +164,22 @@ class RadioThermostat(CoordinatorEntity[RadioThermUpdateCoordinator], ClimateEnt def __init__(self, coordinator: RadioThermUpdateCoordinator) -> None: """Initialize the thermostat.""" super().__init__(coordinator) - self.device = coordinator.init_data.tstat - self._attr_name = coordinator.init_data.name - self._hold_temp = coordinator.hold_temp - self._hold_set = False - self._attr_unique_id = coordinator.init_data.mac - self._attr_device_info = DeviceInfo( - name=coordinator.init_data.name, - model=coordinator.init_data.model, - manufacturer="Radio Thermostats", - sw_version=coordinator.init_data.fw_version, - connections={(dr.CONNECTION_NETWORK_MAC, coordinator.init_data.mac)}, - ) - self._is_model_ct80 = isinstance(self.device, radiotherm.thermostat.CT80) + self._attr_name = self.init_data.name + self._attr_unique_id = self.init_data.mac + self._attr_fan_modes = CT30_FAN_OPERATION_LIST self._attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE ) - self._process_data() - if not self._is_model_ct80: - self._attr_fan_modes = CT30_FAN_OPERATION_LIST + if not isinstance(self.device, radiotherm.thermostat.CT80): return self._attr_fan_modes = CT80_FAN_OPERATION_LIST self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE self._attr_preset_modes = PRESET_MODES - @property - def data(self) -> RadioThermUpdate: - """Returnt the last update.""" - return self.coordinator.data - - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - # Set the time on the device. This shouldn't be in the - # constructor because it's a network call. We can't put it in - # update() because calling it will clear any temporary mode or - # temperature in the thermostat. So add it as a future job - # for the event loop to run. - self.hass.async_add_job(self.set_time) - await super().async_added_to_hass() - async def async_set_fan_mode(self, fan_mode: str) -> None: """Turn fan on/off.""" if (code := FAN_MODE_TO_CODE.get(fan_mode)) is None: - raise HomeAssistantError(f"{fan_mode} is not a valid fan mode") + raise ValueError(f"{fan_mode} is not a valid fan mode") await self.hass.async_add_executor_job(self._set_fan_mode, code) self._attr_fan_mode = fan_mode self.async_write_ha_state() @@ -223,16 +189,11 @@ class RadioThermostat(CoordinatorEntity[RadioThermUpdateCoordinator], ClimateEnt """Turn fan on/off.""" self.device.fmode = code - @callback - def _handle_coordinator_update(self) -> None: - self._process_data() - return super()._handle_coordinator_update() - @callback def _process_data(self) -> None: """Update and validate the data from the thermostat.""" data = self.data.tstat - if self._is_model_ct80: + if isinstance(self.device, radiotherm.thermostat.CT80): self._attr_current_humidity = self.data.humidity self._attr_preset_mode = CODE_TO_PRESET_MODE[data["program_mode"]] # Map thermostat values into various STATE_ flags. @@ -242,7 +203,6 @@ class RadioThermostat(CoordinatorEntity[RadioThermUpdateCoordinator], ClimateEnt ATTR_FAN_ACTION: CODE_TO_FAN_STATE[data["fstate"]] } self._attr_hvac_mode = CODE_TO_TEMP_MODE[data["tmode"]] - self._hold_set = CODE_TO_HOLD_STATE[data["hold"]] if self.hvac_mode == HVACMode.OFF: self._attr_hvac_action = None else: @@ -264,15 +224,12 @@ class RadioThermostat(CoordinatorEntity[RadioThermUpdateCoordinator], ClimateEnt """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return - hold_changed = kwargs.get("hold_changed", False) - await self.hass.async_add_executor_job( - partial(self._set_temperature, temperature, hold_changed) - ) + await self.hass.async_add_executor_job(self._set_temperature, temperature) self._attr_target_temperature = temperature self.async_write_ha_state() await self.coordinator.async_request_refresh() - def _set_temperature(self, temperature: int, hold_changed: bool) -> None: + def _set_temperature(self, temperature: int) -> None: """Set new target temperature.""" temperature = round_temp(temperature) if self.hvac_mode == HVACMode.COOL: @@ -285,26 +242,6 @@ class RadioThermostat(CoordinatorEntity[RadioThermUpdateCoordinator], ClimateEnt elif self.hvac_action == HVACAction.HEATING: self.device.t_heat = temperature - # Only change the hold if requested or if hold mode was turned - # on and we haven't set it yet. - if hold_changed or not self._hold_set: - if self._hold_temp: - self.device.hold = 1 - self._hold_set = True - else: - self.device.hold = 0 - - def set_time(self) -> None: - """Set device time.""" - # Calling this clears any local temperature override and - # reverts to the scheduled temperature. - now = dt_util.now() - self.device.time = { - "day": now.weekday(), - "hour": now.hour, - "minute": now.minute, - } - async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set operation mode (auto, cool, heat, off).""" await self.hass.async_add_executor_job(self._set_hvac_mode, hvac_mode) @@ -325,7 +262,7 @@ class RadioThermostat(CoordinatorEntity[RadioThermUpdateCoordinator], ClimateEnt async def async_set_preset_mode(self, preset_mode: str) -> None: """Set Preset mode (Home, Alternate, Away, Holiday).""" if preset_mode not in PRESET_MODES: - raise HomeAssistantError("{preset_mode} is not a valid preset_mode") + raise ValueError(f"{preset_mode} is not a valid preset_mode") await self.hass.async_add_executor_job(self._set_preset_mode, preset_mode) self._attr_preset_mode = preset_mode self.async_write_ha_state() @@ -333,4 +270,5 @@ class RadioThermostat(CoordinatorEntity[RadioThermUpdateCoordinator], ClimateEnt def _set_preset_mode(self, preset_mode: str) -> None: """Set Preset mode (Home, Alternate, Away, Holiday).""" + assert isinstance(self.device, radiotherm.thermostat.CT80) self.device.program_mode = PRESET_MODE_TO_CODE[preset_mode] diff --git a/homeassistant/components/radiotherm/config_flow.py b/homeassistant/components/radiotherm/config_flow.py index 45fee0f7fd9..030a3e6c022 100644 --- a/homeassistant/components/radiotherm/config_flow.py +++ b/homeassistant/components/radiotherm/config_flow.py @@ -11,11 +11,11 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.const import CONF_HOST -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError -from .const import CONF_HOLD_TEMP, DOMAIN +from .const import DOMAIN from .data import RadioThermInitData, async_get_init_data _LOGGER = logging.getLogger(__name__) @@ -68,7 +68,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=init_data.name, data={CONF_HOST: ip_address}, - options={CONF_HOLD_TEMP: False}, ) self._set_confirm_only() @@ -100,7 +99,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=init_data.name, data={CONF_HOST: import_info[CONF_HOST]}, - options={CONF_HOLD_TEMP: import_info[CONF_HOLD_TEMP]}, ) async def async_step_user( @@ -125,7 +123,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=init_data.name, data=user_input, - options={CONF_HOLD_TEMP: False}, ) return self.async_show_form( @@ -133,34 +130,3 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema({vol.Required(CONF_HOST): str}), errors=errors, ) - - @staticmethod - @callback - def async_get_options_flow(config_entry): - """Get the options flow for this handler.""" - return OptionsFlowHandler(config_entry) - - -class OptionsFlowHandler(config_entries.OptionsFlow): - """Handle a option flow for radiotherm.""" - - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: - """Initialize options flow.""" - self.config_entry = config_entry - - async def async_step_init(self, user_input=None): - """Handle options flow.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - return self.async_show_form( - step_id="init", - data_schema=vol.Schema( - { - vol.Optional( - CONF_HOLD_TEMP, - default=self.config_entry.options[CONF_HOLD_TEMP], - ): bool - } - ), - ) diff --git a/homeassistant/components/radiotherm/const.py b/homeassistant/components/radiotherm/const.py index 398747c6571..db097d40665 100644 --- a/homeassistant/components/radiotherm/const.py +++ b/homeassistant/components/radiotherm/const.py @@ -2,6 +2,4 @@ DOMAIN = "radiotherm" -CONF_HOLD_TEMP = "hold_temp" - TIMEOUT = 25 diff --git a/homeassistant/components/radiotherm/coordinator.py b/homeassistant/components/radiotherm/coordinator.py index 264a4a8d1fd..4afa2c0662b 100644 --- a/homeassistant/components/radiotherm/coordinator.py +++ b/homeassistant/components/radiotherm/coordinator.py @@ -20,12 +20,9 @@ UPDATE_INTERVAL = timedelta(seconds=15) class RadioThermUpdateCoordinator(DataUpdateCoordinator[RadioThermUpdate]): """DataUpdateCoordinator to gather data for radio thermostats.""" - def __init__( - self, hass: HomeAssistant, init_data: RadioThermInitData, hold_temp: bool - ) -> None: + def __init__(self, hass: HomeAssistant, init_data: RadioThermInitData) -> None: """Initialize DataUpdateCoordinator.""" self.init_data = init_data - self.hold_temp = hold_temp self._description = f"{init_data.name} ({init_data.host})" super().__init__( hass, @@ -39,10 +36,8 @@ class RadioThermUpdateCoordinator(DataUpdateCoordinator[RadioThermUpdate]): try: return await async_get_data(self.hass, self.init_data.tstat) except RadiothermTstatError as ex: - raise UpdateFailed( - f"{self._description} was busy (invalid value returned): {ex}" - ) from ex + msg = f"{self._description} was busy (invalid value returned): {ex}" + raise UpdateFailed(msg) from ex except timeout as ex: - raise UpdateFailed( - f"{self._description}) timed out waiting for a response: {ex}" - ) from ex + msg = f"{self._description}) timed out waiting for a response: {ex}" + raise UpdateFailed(msg) from ex diff --git a/homeassistant/components/radiotherm/entity.py b/homeassistant/components/radiotherm/entity.py new file mode 100644 index 00000000000..203d17a5dc2 --- /dev/null +++ b/homeassistant/components/radiotherm/entity.py @@ -0,0 +1,44 @@ +"""The radiotherm integration base entity.""" + +from abc import abstractmethod + +from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .coordinator import RadioThermUpdateCoordinator +from .data import RadioThermUpdate + + +class RadioThermostatEntity(CoordinatorEntity[RadioThermUpdateCoordinator]): + """Base class for radiotherm entities.""" + + def __init__(self, coordinator: RadioThermUpdateCoordinator) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self.init_data = coordinator.init_data + self.device = coordinator.init_data.tstat + self._attr_device_info = DeviceInfo( + name=self.init_data.name, + model=self.init_data.model, + manufacturer="Radio Thermostats", + sw_version=self.init_data.fw_version, + connections={(dr.CONNECTION_NETWORK_MAC, self.init_data.mac)}, + ) + self._process_data() + + @property + def data(self) -> RadioThermUpdate: + """Returnt the last update.""" + return self.coordinator.data + + @callback + @abstractmethod + def _process_data(self) -> None: + """Update and validate the data from the thermostat.""" + + @callback + def _handle_coordinator_update(self) -> None: + self._process_data() + return super()._handle_coordinator_update() diff --git a/homeassistant/components/radiotherm/switch.py b/homeassistant/components/radiotherm/switch.py new file mode 100644 index 00000000000..2cf0602a3fa --- /dev/null +++ b/homeassistant/components/radiotherm/switch.py @@ -0,0 +1,65 @@ +"""Support for radiotherm switches.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import RadioThermUpdateCoordinator +from .entity import RadioThermostatEntity + +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up switches for a radiotherm device.""" + coordinator: RadioThermUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([RadioThermHoldSwitch(coordinator)]) + + +class RadioThermHoldSwitch(RadioThermostatEntity, SwitchEntity): + """Provides radiotherm hold switch support.""" + + def __init__(self, coordinator: RadioThermUpdateCoordinator) -> None: + """Initialize the hold mode switch.""" + super().__init__(coordinator) + self._attr_name = f"{coordinator.init_data.name} Hold" + self._attr_unique_id = f"{coordinator.init_data.mac}_hold" + + @property + def icon(self) -> str: + """Return the icon for the switch.""" + return "mdi:timer-off" if self.is_on else "mdi:timer" + + @callback + def _process_data(self) -> None: + """Update and validate the data from the thermostat.""" + data = self.data.tstat + self._attr_is_on = bool(data["hold"]) + + def _set_hold(self, hold: bool) -> None: + """Set hold mode.""" + self.device.hold = int(hold) + + async def _async_set_hold(self, hold: bool) -> None: + """Set hold mode.""" + await self.hass.async_add_executor_job(self._set_hold, hold) + self._attr_is_on = hold + self.async_write_ha_state() + await self.coordinator.async_request_refresh() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Enable permanent hold.""" + await self._async_set_hold(True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Disable permanent hold.""" + await self._async_set_hold(False) diff --git a/homeassistant/components/radiotherm/util.py b/homeassistant/components/radiotherm/util.py new file mode 100644 index 00000000000..85b927d7935 --- /dev/null +++ b/homeassistant/components/radiotherm/util.py @@ -0,0 +1,24 @@ +"""Utils for radiotherm.""" +from __future__ import annotations + +from radiotherm.thermostat import CommonThermostat + +from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util + + +async def async_set_time(hass: HomeAssistant, device: CommonThermostat) -> None: + """Sync time to the thermostat.""" + await hass.async_add_executor_job(_set_time, device) + + +def _set_time(device: CommonThermostat) -> None: + """Set device time.""" + # Calling this clears any local temperature override and + # reverts to the scheduled temperature. + now = dt_util.now() + device.time = { + "day": now.weekday(), + "hour": now.hour, + "minute": now.minute, + } diff --git a/tests/components/radiotherm/test_config_flow.py b/tests/components/radiotherm/test_config_flow.py index bc729a27e1c..56a361404f8 100644 --- a/tests/components/radiotherm/test_config_flow.py +++ b/tests/components/radiotherm/test_config_flow.py @@ -7,7 +7,7 @@ from radiotherm.validate import RadiothermTstatError from homeassistant import config_entries, data_entry_flow from homeassistant.components import dhcp -from homeassistant.components.radiotherm.const import CONF_HOLD_TEMP, DOMAIN +from homeassistant.components.radiotherm.const import DOMAIN from homeassistant.const import CONF_HOST from tests.common import MockConfigEntry @@ -110,17 +110,12 @@ async def test_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_HOST: "1.2.3.4", CONF_HOLD_TEMP: True}, + data={CONF_HOST: "1.2.3.4"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "My Name" - assert result["data"] == { - CONF_HOST: "1.2.3.4", - } - assert result["options"] == { - CONF_HOLD_TEMP: True, - } + assert result["data"] == {CONF_HOST: "1.2.3.4"} assert len(mock_setup_entry.mock_calls) == 1 @@ -133,7 +128,7 @@ async def test_import_cannot_connect(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_HOST: "1.2.3.4", CONF_HOLD_TEMP: True}, + data={CONF_HOST: "1.2.3.4"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -268,35 +263,3 @@ async def test_user_unique_id_already_exists(hass): assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result2["reason"] == "already_configured" - - -async def test_options_flow(hass): - """Test config flow options.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_HOST: "1.2.3.4"}, - unique_id="aa:bb:cc:dd:ee:ff", - options={CONF_HOLD_TEMP: False}, - ) - - entry.add_to_hass(hass) - with patch( - "homeassistant.components.radiotherm.data.radiotherm.get_thermostat", - return_value=_mock_radiotherm(), - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - result = await hass.config_entries.options.async_init(entry.entry_id) - await hass.async_block_till_done() - assert await hass.config_entries.async_unload(entry.entry_id) - await hass.async_block_till_done() - - 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_HOLD_TEMP: True} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert entry.options == {CONF_HOLD_TEMP: True} From f91aa33c5f7bc13ed031a95f946f70e11af1e2f3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 8 Jun 2022 07:02:44 +0200 Subject: [PATCH 1350/3516] Add FlowResultType enum to data entry flow (#72955) --- homeassistant/auth/__init__.py | 2 +- .../components/almond/config_flow.py | 2 +- homeassistant/components/auth/login_flow.py | 10 +- .../components/auth/mfa_setup_flow.py | 4 +- .../components/config/config_entries.py | 2 +- homeassistant/components/mqtt/discovery.py | 4 +- homeassistant/config_entries.py | 4 +- homeassistant/data_entry_flow.py | 94 +++++++++++-------- homeassistant/helpers/data_entry_flow.py | 2 +- .../helpers/schema_config_entry_flow.py | 2 +- pylint/plugins/hass_imports.py | 6 ++ 11 files changed, 79 insertions(+), 53 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index b3f57656dd7..23fdb775a8a 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -103,7 +103,7 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager): """Return a user as result of login flow.""" flow = cast(LoginFlow, flow) - if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY: return result # we got final result diff --git a/homeassistant/components/almond/config_flow.py b/homeassistant/components/almond/config_flow.py index dfbdae219ca..11c883f4e0a 100644 --- a/homeassistant/components/almond/config_flow.py +++ b/homeassistant/components/almond/config_flow.py @@ -64,7 +64,7 @@ class AlmondFlowHandler( """Handle authorize step.""" result = await super().async_step_auth(user_input) - if result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP: + if result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP: self.host = str(URL(result["url"]).with_path("me")) return result diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index cd6a405d42c..b24da92afdd 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -52,7 +52,7 @@ flow for details. Progress the flow. Most flows will be 1 page, but could optionally add extra login challenges, like TFA. Once the flow has finished, the returned step will -have type RESULT_TYPE_CREATE_ENTRY and "result" key will contain an authorization code. +have type FlowResultType.CREATE_ENTRY and "result" key will contain an authorization code. The authorization code associated with an authorized user by default, it will associate with an credential if "type" set to "link_user" in "/auth/login_flow" @@ -123,13 +123,13 @@ class AuthProvidersView(HomeAssistantView): def _prepare_result_json(result): """Convert result to JSON.""" - if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: data = result.copy() data.pop("result") data.pop("data") return data - if result["type"] != data_entry_flow.RESULT_TYPE_FORM: + if result["type"] != data_entry_flow.FlowResultType.FORM: return result data = result.copy() @@ -154,11 +154,11 @@ class LoginFlowBaseView(HomeAssistantView): async def _async_flow_result_to_response(self, request, client_id, result): """Convert the flow result to a response.""" - if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY: # @log_invalid_auth does not work here since it returns HTTP 200. # We need to manually log failed login attempts. if ( - result["type"] == data_entry_flow.RESULT_TYPE_FORM + result["type"] == data_entry_flow.FlowResultType.FORM and (errors := result.get("errors")) and errors.get("base") in ( diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index aa45cc1b028..e288fe33df7 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -129,11 +129,11 @@ def websocket_depose_mfa( def _prepare_result_json(result): """Convert result to JSON.""" - if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: data = result.copy() return data - if result["type"] != data_entry_flow.RESULT_TYPE_FORM: + if result["type"] != data_entry_flow.FlowResultType.FORM: return result data = result.copy() diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 0a093ee4574..b1756b58c3e 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -143,7 +143,7 @@ class ConfigManagerEntryResourceReloadView(HomeAssistantView): def _prepare_config_flow_result_json(result, prepare_result_json): """Convert result to JSON.""" - if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY: return prepare_result_json(result) data = result.copy() diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 8685c790fd2..3bbf8ef61ad 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -11,7 +11,7 @@ import time from homeassistant.const import CONF_DEVICE, CONF_PLATFORM from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT +from homeassistant.data_entry_flow import FlowResultType import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -305,7 +305,7 @@ async def async_start( # noqa: C901 ) if ( result - and result["type"] == RESULT_TYPE_ABORT + and result["type"] == FlowResultType.ABORT and result["reason"] in ("already_configured", "single_instance_allowed") ): diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index bb24b24a7fb..14900153ae4 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -682,7 +682,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): if not self._async_has_other_discovery_flows(flow.flow_id): persistent_notification.async_dismiss(self.hass, DISCOVERY_NOTIFICATION_ID) - if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY: return result # Check if config entry exists with unique ID. Unload it. @@ -1534,7 +1534,7 @@ class OptionsFlowManager(data_entry_flow.FlowManager): """ flow = cast(OptionsFlow, flow) - if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY: return result entry = self.hass.config_entries.async_get_entry(flow.handler) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 714cea07044..abc8061c0d5 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -10,10 +10,27 @@ from typing import Any, TypedDict import voluptuous as vol +from .backports.enum import StrEnum from .core import HomeAssistant, callback from .exceptions import HomeAssistantError +from .helpers.frame import report from .util import uuid as uuid_util + +class FlowResultType(StrEnum): + """Result type for a data entry flow.""" + + FORM = "form" + CREATE_ENTRY = "create_entry" + ABORT = "abort" + EXTERNAL_STEP = "external" + EXTERNAL_STEP_DONE = "external_done" + SHOW_PROGRESS = "progress" + SHOW_PROGRESS_DONE = "progress_done" + MENU = "menu" + + +# RESULT_TYPE_* is deprecated, to be removed in 2022.9 RESULT_TYPE_FORM = "form" RESULT_TYPE_CREATE_ENTRY = "create_entry" RESULT_TYPE_ABORT = "abort" @@ -64,7 +81,7 @@ class FlowResult(TypedDict, total=False): """Typed result dict.""" version: int - type: str + type: FlowResultType flow_id: str handler: str title: str @@ -207,7 +224,7 @@ class FlowManager(abc.ABC): self._initialize_tasks[handler].remove(task) self._initializing[handler].remove(init_done) - if result["type"] != RESULT_TYPE_ABORT: + if result["type"] != FlowResultType.ABORT: await self.async_post_init(flow, result) return result @@ -252,7 +269,7 @@ class FlowManager(abc.ABC): user_input = cur_step["data_schema"](user_input) # Handle a menu navigation choice - if cur_step["type"] == RESULT_TYPE_MENU and user_input: + if cur_step["type"] == FlowResultType.MENU and user_input: result = await self._async_handle_step( flow, user_input["next_step_id"], None ) @@ -261,18 +278,25 @@ class FlowManager(abc.ABC): flow, cur_step["step_id"], user_input ) - if cur_step["type"] in (RESULT_TYPE_EXTERNAL_STEP, RESULT_TYPE_SHOW_PROGRESS): - if cur_step["type"] == RESULT_TYPE_EXTERNAL_STEP and result["type"] not in ( - RESULT_TYPE_EXTERNAL_STEP, - RESULT_TYPE_EXTERNAL_STEP_DONE, + if cur_step["type"] in ( + FlowResultType.EXTERNAL_STEP, + FlowResultType.SHOW_PROGRESS, + ): + if cur_step["type"] == FlowResultType.EXTERNAL_STEP and result[ + "type" + ] not in ( + FlowResultType.EXTERNAL_STEP, + FlowResultType.EXTERNAL_STEP_DONE, ): raise ValueError( "External step can only transition to " "external step or external step done." ) - if cur_step["type"] == RESULT_TYPE_SHOW_PROGRESS and result["type"] not in ( - RESULT_TYPE_SHOW_PROGRESS, - RESULT_TYPE_SHOW_PROGRESS_DONE, + if cur_step["type"] == FlowResultType.SHOW_PROGRESS and result[ + "type" + ] not in ( + FlowResultType.SHOW_PROGRESS, + FlowResultType.SHOW_PROGRESS_DONE, ): raise ValueError( "Show progress can only transition to show progress or show progress done." @@ -282,7 +306,7 @@ class FlowManager(abc.ABC): # the frontend. if ( cur_step["step_id"] != result.get("step_id") - or result["type"] == RESULT_TYPE_SHOW_PROGRESS + or result["type"] == FlowResultType.SHOW_PROGRESS ): # Tell frontend to reload the flow state. self.hass.bus.async_fire( @@ -345,25 +369,21 @@ class FlowManager(abc.ABC): if step_done: step_done.set_result(None) - if result["type"] not in ( - RESULT_TYPE_FORM, - RESULT_TYPE_EXTERNAL_STEP, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_ABORT, - RESULT_TYPE_EXTERNAL_STEP_DONE, - RESULT_TYPE_SHOW_PROGRESS, - RESULT_TYPE_SHOW_PROGRESS_DONE, - RESULT_TYPE_MENU, - ): - raise ValueError(f"Handler returned incorrect type: {result['type']}") + if not isinstance(result["type"], FlowResultType): + result["type"] = FlowResultType(result["type"]) # type: ignore[unreachable] + report( + "does not use FlowResultType enum for data entry flow result type. " + "This is deprecated and will stop working in Home Assistant 2022.9", + error_if_core=False, + ) if result["type"] in ( - RESULT_TYPE_FORM, - RESULT_TYPE_EXTERNAL_STEP, - RESULT_TYPE_EXTERNAL_STEP_DONE, - RESULT_TYPE_SHOW_PROGRESS, - RESULT_TYPE_SHOW_PROGRESS_DONE, - RESULT_TYPE_MENU, + FlowResultType.FORM, + FlowResultType.EXTERNAL_STEP, + FlowResultType.EXTERNAL_STEP_DONE, + FlowResultType.SHOW_PROGRESS, + FlowResultType.SHOW_PROGRESS_DONE, + FlowResultType.MENU, ): flow.cur_step = result return result @@ -372,7 +392,7 @@ class FlowManager(abc.ABC): result = await self.async_finish_flow(flow, result.copy()) # _async_finish_flow may change result type, check it again - if result["type"] == RESULT_TYPE_FORM: + if result["type"] == FlowResultType.FORM: flow.cur_step = result return result @@ -427,7 +447,7 @@ class FlowHandler: ) -> FlowResult: """Return the definition of a form to gather user input.""" return { - "type": RESULT_TYPE_FORM, + "type": FlowResultType.FORM, "flow_id": self.flow_id, "handler": self.handler, "step_id": step_id, @@ -449,7 +469,7 @@ class FlowHandler: """Finish config flow and create a config entry.""" return { "version": self.VERSION, - "type": RESULT_TYPE_CREATE_ENTRY, + "type": FlowResultType.CREATE_ENTRY, "flow_id": self.flow_id, "handler": self.handler, "title": title, @@ -480,7 +500,7 @@ class FlowHandler: ) -> FlowResult: """Return the definition of an external step for the user to take.""" return { - "type": RESULT_TYPE_EXTERNAL_STEP, + "type": FlowResultType.EXTERNAL_STEP, "flow_id": self.flow_id, "handler": self.handler, "step_id": step_id, @@ -492,7 +512,7 @@ class FlowHandler: def async_external_step_done(self, *, next_step_id: str) -> FlowResult: """Return the definition of an external step for the user to take.""" return { - "type": RESULT_TYPE_EXTERNAL_STEP_DONE, + "type": FlowResultType.EXTERNAL_STEP_DONE, "flow_id": self.flow_id, "handler": self.handler, "step_id": next_step_id, @@ -508,7 +528,7 @@ class FlowHandler: ) -> FlowResult: """Show a progress message to the user, without user input allowed.""" return { - "type": RESULT_TYPE_SHOW_PROGRESS, + "type": FlowResultType.SHOW_PROGRESS, "flow_id": self.flow_id, "handler": self.handler, "step_id": step_id, @@ -520,7 +540,7 @@ class FlowHandler: def async_show_progress_done(self, *, next_step_id: str) -> FlowResult: """Mark the progress done.""" return { - "type": RESULT_TYPE_SHOW_PROGRESS_DONE, + "type": FlowResultType.SHOW_PROGRESS_DONE, "flow_id": self.flow_id, "handler": self.handler, "step_id": next_step_id, @@ -539,7 +559,7 @@ class FlowHandler: Options dict maps step_id => i18n label """ return { - "type": RESULT_TYPE_MENU, + "type": FlowResultType.MENU, "flow_id": self.flow_id, "handler": self.handler, "step_id": step_id, @@ -558,7 +578,7 @@ def _create_abort_data( ) -> FlowResult: """Return the definition of an external step for the user to take.""" return { - "type": RESULT_TYPE_ABORT, + "type": FlowResultType.ABORT, "flow_id": flow_id, "handler": handler, "reason": reason, diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 2126e048fc5..444876a7674 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -26,7 +26,7 @@ class _BaseFlowManagerView(HomeAssistantView): self, result: data_entry_flow.FlowResult ) -> data_entry_flow.FlowResult: """Convert result to JSON.""" - if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: data = result.copy() data.pop("result") data.pop("data") diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index 4073421bc2c..12ffa7cc101 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -42,7 +42,7 @@ class SchemaFlowFormStep: # The next_step function is called if the schema validates successfully or if no # schema is defined. The next_step function is passed the union of config entry # options and user input from previous steps. - # If next_step returns None, the flow is ended with RESULT_TYPE_CREATE_ENTRY. + # If next_step returns None, the flow is ended with FlowResultType.CREATE_ENTRY. next_step: Callable[[dict[str, Any]], str | None] = lambda _: None # Optional function to allow amending a form schema. diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index cc160c1cfbd..31fbe8f498e 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -220,6 +220,12 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { constant=re.compile(r"^SOURCE_(\w*)$"), ), ], + "homeassistant.data_entry_flow": [ + ObsoleteImportMatch( + reason="replaced by FlowResultType enum", + constant=re.compile(r"^RESULT_TYPE_(\w*)$"), + ), + ], "homeassistant.helpers.device_registry": [ ObsoleteImportMatch( reason="replaced by DeviceEntryDisabler enum", From 7980e3f4064e57c665a06e7a33b5b13b5e360480 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 8 Jun 2022 02:10:48 -0400 Subject: [PATCH 1351/3516] Tweak zwave_js firmware upload view (#73202) Small tweaks to zwave_js firmware upload view --- homeassistant/components/zwave_js/api.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index e6ce08a78a5..6c186e2f840 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -53,7 +53,6 @@ from homeassistant.components.websocket_api.const import ( ERR_UNKNOWN_ERROR, ) from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import Unauthorized from homeassistant.helpers import config_validation as cv @@ -1943,16 +1942,6 @@ class FirmwareUploadView(HomeAssistantView): raise web_exceptions.HTTPBadRequest raise web_exceptions.HTTPNotFound - if not self._dev_reg: - self._dev_reg = dr.async_get(hass) - device = self._dev_reg.async_get(device_id) - assert device - entry = next( - entry - for entry in hass.config_entries.async_entries(DOMAIN) - if entry.entry_id in device.config_entries - ) - # Increase max payload request._client_max_size = 1024 * 1024 * 10 # pylint: disable=protected-access @@ -1965,14 +1954,14 @@ class FirmwareUploadView(HomeAssistantView): try: await begin_firmware_update( - entry.data[CONF_URL], + node.client.ws_server_url, node, uploaded_file.filename, await hass.async_add_executor_job(uploaded_file.file.read), async_get_clientsession(hass), ) except BaseZwaveJSServerError as err: - raise web_exceptions.HTTPBadRequest from err + raise web_exceptions.HTTPBadRequest(reason=str(err)) from err return self.json(None) From 6c3d402777b78307d79e4b7f5af15e9b1318e121 Mon Sep 17 00:00:00 2001 From: Matrix Date: Wed, 8 Jun 2022 14:11:41 +0800 Subject: [PATCH 1352/3516] Bump yolink-api to 0.0.8 (#73173) * update api libray fix hearbeat message valiation * update yolink-api ignore invalidate message --- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index 7fb78a4974b..d2a02a44c42 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -3,7 +3,7 @@ "name": "YoLink", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yolink", - "requirements": ["yolink-api==0.0.6"], + "requirements": ["yolink-api==0.0.8"], "dependencies": ["auth", "application_credentials"], "codeowners": ["@matrixd2"], "iot_class": "cloud_push" diff --git a/requirements_all.txt b/requirements_all.txt index b95e0b7280c..b72237ffdf1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2483,7 +2483,7 @@ yeelight==0.7.10 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.0.6 +yolink-api==0.0.8 # homeassistant.components.youless youless-api==0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5a9399b0e3e..5e44767eec0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1641,7 +1641,7 @@ yalexs==1.1.25 yeelight==0.7.10 # homeassistant.components.yolink -yolink-api==0.0.6 +yolink-api==0.0.8 # homeassistant.components.youless youless-api==0.16 From 95e9bd106eb2a7077e92aa7c8653d668ebe09533 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jun 2022 09:07:05 +0200 Subject: [PATCH 1353/3516] Bump actions/cache from 3.0.3 to 3.0.4 (#73203) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 30756ddb501..502006d8d2c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -172,7 +172,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.3 + uses: actions/cache@v3.0.4 with: path: venv key: >- @@ -189,7 +189,7 @@ jobs: # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.3 + uses: actions/cache@v3.0.4 with: path: ${{ env.PIP_CACHE }} key: >- @@ -212,7 +212,7 @@ jobs: hashFiles('.pre-commit-config.yaml') }}" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.3 + uses: actions/cache@v3.0.4 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -241,7 +241,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.3 + uses: actions/cache@v3.0.4 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -253,7 +253,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.3 + uses: actions/cache@v3.0.4 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -291,7 +291,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.3 + uses: actions/cache@v3.0.4 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -303,7 +303,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.3 + uses: actions/cache@v3.0.4 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -342,7 +342,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.3 + uses: actions/cache@v3.0.4 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -354,7 +354,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.3 + uses: actions/cache@v3.0.4 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -384,7 +384,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.3 + uses: actions/cache@v3.0.4 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -396,7 +396,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.3 + uses: actions/cache@v3.0.4 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} @@ -502,7 +502,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.3 + uses: actions/cache@v3.0.4 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -531,7 +531,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.3 + uses: actions/cache@v3.0.4 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -573,7 +573,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.3 + uses: actions/cache@v3.0.4 with: path: venv key: >- @@ -590,7 +590,7 @@ jobs: # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}- - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.3 + uses: actions/cache@v3.0.4 with: path: ${{ env.PIP_CACHE }} key: >- @@ -629,7 +629,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.3 + uses: actions/cache@v3.0.4 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -671,7 +671,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.3 + uses: actions/cache@v3.0.4 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -715,7 +715,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.3 + uses: actions/cache@v3.0.4 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ @@ -758,7 +758,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.3 + uses: actions/cache@v3.0.4 with: path: venv key: ${{ runner.os }}-${{ matrix.python-version }}-${{ From 4bc04383d143946c7c7a2be00d8fa9b1736c1648 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 8 Jun 2022 10:52:01 +0200 Subject: [PATCH 1354/3516] Ensure netgear devices are tracked with one enabled config entry (#72969) Co-authored-by: Martin Hjelmare --- homeassistant/components/netgear/__init__.py | 7 +++---- homeassistant/components/netgear/router.py | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index 7a4d0e7a8cd..679a93f8da1 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -18,7 +18,6 @@ from .const import ( KEY_COORDINATOR_SPEED, KEY_COORDINATOR_TRAFFIC, KEY_ROUTER, - MODE_ROUTER, PLATFORMS, ) from .errors import CannotLoginException @@ -72,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update_devices() -> bool: """Fetch data from the router.""" - if router.mode == MODE_ROUTER: + if router.track_devices: return await router.async_update_device_trackers() return False @@ -107,7 +106,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval=SPEED_TEST_INTERVAL, ) - if router.mode == MODE_ROUTER: + if router.track_devices: await coordinator.async_config_entry_first_refresh() await coordinator_traffic_meter.async_config_entry_first_refresh() @@ -134,7 +133,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) - if router.mode != MODE_ROUTER: + if not router.track_devices: router_id = None # Remove devices that are no longer tracked device_registry = dr.async_get(hass) diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 301906f22b6..67e573d0e92 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -80,6 +80,7 @@ class NetgearRouter: self.hardware_version = "" self.serial_number = "" + self.track_devices = True self.method_version = 1 consider_home_int = entry.options.get( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() @@ -112,11 +113,23 @@ class NetgearRouter: self.serial_number = self._info["SerialNumber"] self.mode = self._info.get("DeviceMode", MODE_ROUTER) + enabled_entries = [ + entry + for entry in self.hass.config_entries.async_entries(DOMAIN) + if entry.disabled_by is None + ] + self.track_devices = self.mode == MODE_ROUTER or len(enabled_entries) == 1 + _LOGGER.debug( + "Netgear track_devices = '%s', device mode '%s'", + self.track_devices, + self.mode, + ) + for model in MODELS_V2: if self.model.startswith(model): self.method_version = 2 - if self.method_version == 2 and self.mode == MODE_ROUTER: + if self.method_version == 2 and self.track_devices: if not self._api.get_attached_devices_2(): _LOGGER.error( "Netgear Model '%s' in MODELS_V2 list, but failed to get attached devices using V2", @@ -133,7 +146,7 @@ class NetgearRouter: return False # set already known devices to away instead of unavailable - if self.mode == MODE_ROUTER: + if self.track_devices: device_registry = dr.async_get(self.hass) devices = dr.async_entries_for_config_entry(device_registry, self.entry_id) for device_entry in devices: From 79096864ebe6f187326289a3356341fea4252449 Mon Sep 17 00:00:00 2001 From: Matrix Date: Wed, 8 Jun 2022 17:54:32 +0800 Subject: [PATCH 1355/3516] Add yolink CoSmoke Sensor and Switch (#73209) add CoSmoke Sensor and Switch --- .../components/yolink/binary_sensor.py | 25 ++++++++++++++++--- homeassistant/components/yolink/const.py | 2 ++ homeassistant/components/yolink/sensor.py | 3 +++ homeassistant/components/yolink/switch.py | 16 +++++++++--- 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/yolink/binary_sensor.py b/homeassistant/components/yolink/binary_sensor.py index 17b25c57d94..b296e01fa56 100644 --- a/homeassistant/components/yolink/binary_sensor.py +++ b/homeassistant/components/yolink/binary_sensor.py @@ -18,6 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ATTR_COORDINATORS, + ATTR_DEVICE_CO_SMOKE_SENSOR, ATTR_DEVICE_DOOR_SENSOR, ATTR_DEVICE_LEAK_SENSOR, ATTR_DEVICE_MOTION_SENSOR, @@ -42,8 +43,10 @@ SENSOR_DEVICE_TYPE = [ ATTR_DEVICE_MOTION_SENSOR, ATTR_DEVICE_LEAK_SENSOR, ATTR_DEVICE_VIBRATION_SENSOR, + ATTR_DEVICE_CO_SMOKE_SENSOR, ] + SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( YoLinkBinarySensorEntityDescription( key="door_state", @@ -51,14 +54,14 @@ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.DOOR, name="State", value=lambda value: value == "open" if value is not None else None, - exists_fn=lambda device: device.device_type in [ATTR_DEVICE_DOOR_SENSOR], + exists_fn=lambda device: device.device_type == ATTR_DEVICE_DOOR_SENSOR, ), YoLinkBinarySensorEntityDescription( key="motion_state", device_class=BinarySensorDeviceClass.MOTION, name="Motion", value=lambda value: value == "alert" if value is not None else None, - exists_fn=lambda device: device.device_type in [ATTR_DEVICE_MOTION_SENSOR], + exists_fn=lambda device: device.device_type == ATTR_DEVICE_MOTION_SENSOR, ), YoLinkBinarySensorEntityDescription( key="leak_state", @@ -66,14 +69,28 @@ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( icon="mdi:water", device_class=BinarySensorDeviceClass.MOISTURE, value=lambda value: value == "alert" if value is not None else None, - exists_fn=lambda device: device.device_type in [ATTR_DEVICE_LEAK_SENSOR], + exists_fn=lambda device: device.device_type == ATTR_DEVICE_LEAK_SENSOR, ), YoLinkBinarySensorEntityDescription( key="vibration_state", name="Vibration", device_class=BinarySensorDeviceClass.VIBRATION, value=lambda value: value == "alert" if value is not None else None, - exists_fn=lambda device: device.device_type in [ATTR_DEVICE_VIBRATION_SENSOR], + exists_fn=lambda device: device.device_type == ATTR_DEVICE_VIBRATION_SENSOR, + ), + YoLinkBinarySensorEntityDescription( + key="co_detected", + name="Co Detected", + device_class=BinarySensorDeviceClass.CO, + value=lambda state: state.get("gasAlarm"), + exists_fn=lambda device: device.device_type == ATTR_DEVICE_CO_SMOKE_SENSOR, + ), + YoLinkBinarySensorEntityDescription( + key="smoke_detected", + name="Smoke Detected", + device_class=BinarySensorDeviceClass.SMOKE, + value=lambda state: state.get("smokeAlarm"), + exists_fn=lambda device: device.device_type == ATTR_DEVICE_CO_SMOKE_SENSOR, ), ) diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index b154fe9178d..dba0a0ee221 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -22,3 +22,5 @@ ATTR_DEVICE_OUTLET = "Outlet" ATTR_DEVICE_SIREN = "Siren" ATTR_DEVICE_LOCK = "Lock" ATTR_DEVICE_MANIPULATOR = "Manipulator" +ATTR_DEVICE_CO_SMOKE_SENSOR = "COSmokeSensor" +ATTR_DEVICE_SWITCH = "Switch" diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 26dee7a493d..7c578fbaa73 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -20,6 +20,7 @@ from homeassistant.util import percentage from .const import ( ATTR_COORDINATORS, + ATTR_DEVICE_CO_SMOKE_SENSOR, ATTR_DEVICE_DOOR_SENSOR, ATTR_DEVICE_LOCK, ATTR_DEVICE_MANIPULATOR, @@ -55,6 +56,7 @@ SENSOR_DEVICE_TYPE = [ ATTR_DEVICE_VIBRATION_SENSOR, ATTR_DEVICE_LOCK, ATTR_DEVICE_MANIPULATOR, + ATTR_DEVICE_CO_SMOKE_SENSOR, ] BATTERY_POWER_SENSOR = [ @@ -64,6 +66,7 @@ BATTERY_POWER_SENSOR = [ ATTR_DEVICE_VIBRATION_SENSOR, ATTR_DEVICE_LOCK, ATTR_DEVICE_MANIPULATOR, + ATTR_DEVICE_CO_SMOKE_SENSOR, ] diff --git a/homeassistant/components/yolink/switch.py b/homeassistant/components/yolink/switch.py index 6733191b943..03bb2a26183 100644 --- a/homeassistant/components/yolink/switch.py +++ b/homeassistant/components/yolink/switch.py @@ -20,6 +20,7 @@ from .const import ( ATTR_COORDINATORS, ATTR_DEVICE_MANIPULATOR, ATTR_DEVICE_OUTLET, + ATTR_DEVICE_SWITCH, DOMAIN, ) from .coordinator import YoLinkCoordinator @@ -41,18 +42,25 @@ DEVICE_TYPES: tuple[YoLinkSwitchEntityDescription, ...] = ( device_class=SwitchDeviceClass.OUTLET, name="State", value=lambda value: value == "open" if value is not None else None, - exists_fn=lambda device: device.device_type in [ATTR_DEVICE_OUTLET], + exists_fn=lambda device: device.device_type == ATTR_DEVICE_OUTLET, ), YoLinkSwitchEntityDescription( key="manipulator_state", - device_class=SwitchDeviceClass.SWITCH, name="State", + icon="mdi:pipe", value=lambda value: value == "open" if value is not None else None, - exists_fn=lambda device: device.device_type in [ATTR_DEVICE_MANIPULATOR], + exists_fn=lambda device: device.device_type == ATTR_DEVICE_MANIPULATOR, + ), + YoLinkSwitchEntityDescription( + key="switch_state", + name="State", + device_class=SwitchDeviceClass.SWITCH, + value=lambda value: value == "open" if value is not None else None, + exists_fn=lambda device: device.device_type == ATTR_DEVICE_SWITCH, ), ) -DEVICE_TYPE = [ATTR_DEVICE_MANIPULATOR, ATTR_DEVICE_OUTLET] +DEVICE_TYPE = [ATTR_DEVICE_MANIPULATOR, ATTR_DEVICE_OUTLET, ATTR_DEVICE_SWITCH] async def async_setup_entry( From 5987266e5636cf0ef42ddbe1513e3c907f2d7bdf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 8 Jun 2022 15:55:49 +0200 Subject: [PATCH 1356/3516] Refactor template entity to allow reuse (#72753) * Refactor template entity to allow reuse * Fix schema and default name * Add tests * Update requirements * Improve test * Tweak TemplateSensor initializer * Drop attributes and availability from TemplateEntity * Use rest sensor for proof of concept * Revert changes in SNMP sensor * Don't set _attr_should_poll in mixin class * Update requirements --- homeassistant/components/rest/entity.py | 33 +- homeassistant/components/rest/schema.py | 15 +- homeassistant/components/rest/sensor.py | 78 ++-- .../template/alarm_control_panel.py | 2 + .../components/template/binary_sensor.py | 2 + homeassistant/components/template/button.py | 2 + homeassistant/components/template/cover.py | 2 + homeassistant/components/template/fan.py | 2 + homeassistant/components/template/light.py | 2 + homeassistant/components/template/lock.py | 2 + homeassistant/components/template/number.py | 2 + homeassistant/components/template/select.py | 2 + homeassistant/components/template/sensor.py | 38 +- homeassistant/components/template/switch.py | 7 +- .../components/template/template_entity.py | 383 +--------------- homeassistant/components/template/vacuum.py | 2 + homeassistant/components/template/weather.py | 2 + homeassistant/helpers/template_entity.py | 434 ++++++++++++++++++ tests/components/rest/test_sensor.py | 42 ++ 19 files changed, 580 insertions(+), 472 deletions(-) create mode 100644 homeassistant/helpers/template_entity.py diff --git a/homeassistant/components/rest/entity.py b/homeassistant/components/rest/entity.py index 064396af415..f0476dc7d33 100644 --- a/homeassistant/components/rest/entity.py +++ b/homeassistant/components/rest/entity.py @@ -10,29 +10,21 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .data import RestData -class RestEntity(Entity): +class BaseRestEntity(Entity): """A class for entities using DataUpdateCoordinator or rest data directly.""" def __init__( self, coordinator: DataUpdateCoordinator[Any], rest: RestData, - name, resource_template, force_update, ) -> None: """Create the entity that may have a coordinator.""" self.coordinator = coordinator self.rest = rest - self._name = name self._resource_template = resource_template self._force_update = force_update - super().__init__() - - @property - def name(self): - """Return the name of the sensor.""" - return self._name @property def force_update(self): @@ -41,7 +33,7 @@ class RestEntity(Entity): @property def should_poll(self) -> bool: - """Poll only if we do noty have a coordinator.""" + """Poll only if we do not have a coordinator.""" return not self.coordinator @property @@ -80,3 +72,24 @@ class RestEntity(Entity): @abstractmethod def _update_from_rest_data(self): """Update state from the rest data.""" + + +class RestEntity(BaseRestEntity): + """A class for entities using DataUpdateCoordinator or rest data directly.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator[Any], + rest: RestData, + name, + resource_template, + force_update, + ) -> None: + """Create the entity that may have a coordinator.""" + self._name = name + super().__init__(coordinator, rest, resource_template, force_update) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py index c5b6949bd39..d25bb50167b 100644 --- a/homeassistant/components/rest/schema.py +++ b/homeassistant/components/rest/schema.py @@ -6,12 +6,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA as BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, DOMAIN as BINARY_SENSOR_DOMAIN, ) -from homeassistant.components.sensor import ( - DEVICE_CLASSES_SCHEMA as SENSOR_DEVICE_CLASSES_SCHEMA, - DOMAIN as SENSOR_DOMAIN, - STATE_CLASSES_SCHEMA, -) -from homeassistant.components.sensor.const import CONF_STATE_CLASS +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( CONF_AUTHENTICATION, CONF_DEVICE_CLASS, @@ -26,7 +21,6 @@ from homeassistant.const import ( CONF_RESOURCE_TEMPLATE, CONF_SCAN_INTERVAL, CONF_TIMEOUT, - CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, @@ -34,6 +28,7 @@ from homeassistant.const import ( HTTP_DIGEST_AUTHENTICATION, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.template_entity import TEMPLATE_SENSOR_BASE_SCHEMA from .const import ( CONF_JSON_ATTRS, @@ -41,7 +36,6 @@ from .const import ( DEFAULT_BINARY_SENSOR_NAME, DEFAULT_FORCE_UPDATE, DEFAULT_METHOD, - DEFAULT_SENSOR_NAME, DEFAULT_VERIFY_SSL, DOMAIN, METHODS, @@ -65,10 +59,7 @@ RESOURCE_SCHEMA = { } 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, + **TEMPLATE_SENSOR_BASE_SCHEMA.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 73d65abda7e..2965504dc4a 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -10,31 +10,28 @@ import voluptuous as vol import xmltodict from homeassistant.components.sensor import ( - CONF_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA, SensorDeviceClass, - SensorEntity, ) from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.const import ( - CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, - CONF_NAME, CONF_RESOURCE, CONF_RESOURCE_TEMPLATE, - CONF_UNIT_OF_MEASUREMENT, + CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.template_entity import TemplateSensor from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import async_get_config_and_coordinator, create_rest_data_from_config -from .const import CONF_JSON_ATTRS, CONF_JSON_ATTRS_PATH -from .entity import RestEntity +from .const import CONF_JSON_ATTRS, CONF_JSON_ATTRS_PATH, DEFAULT_SENSOR_NAME +from .entity import BaseRestEntity from .schema import RESOURCE_SCHEMA, SENSOR_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -70,67 +67,54 @@ async def async_setup_platform( raise PlatformNotReady from rest.last_exception raise PlatformNotReady - 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) - force_update = conf.get(CONF_FORCE_UPDATE) - resource_template = conf.get(CONF_RESOURCE_TEMPLATE) - - if value_template is not None: - value_template.hass = hass + unique_id = conf.get(CONF_UNIQUE_ID) async_add_entities( [ RestSensor( + hass, coordinator, rest, - name, - unit, - device_class, - state_class, - value_template, - json_attrs, - force_update, - resource_template, - json_attrs_path, + conf, + unique_id, ) ], ) -class RestSensor(RestEntity, SensorEntity): +class RestSensor(BaseRestEntity, TemplateSensor): """Implementation of a REST sensor.""" def __init__( self, + hass, coordinator, rest, - name, - unit_of_measurement, - device_class, - state_class, - value_template, - json_attrs, - force_update, - resource_template, - json_attrs_path, + config, + unique_id, ): """Initialize the REST sensor.""" - super().__init__(coordinator, rest, name, resource_template, force_update) + BaseRestEntity.__init__( + self, + coordinator, + rest, + config.get(CONF_RESOURCE_TEMPLATE), + config.get(CONF_FORCE_UPDATE), + ) + TemplateSensor.__init__( + self, + hass, + config=config, + fallback_name=DEFAULT_SENSOR_NAME, + unique_id=unique_id, + ) self._state = None - self._unit_of_measurement = unit_of_measurement - self._value_template = value_template - self._json_attrs = json_attrs + self._value_template = config.get(CONF_VALUE_TEMPLATE) + if (value_template := self._value_template) is not None: + value_template.hass = hass + self._json_attrs = config.get(CONF_JSON_ATTRS) self._attributes = None - self._json_attrs_path = json_attrs_path - - self._attr_native_unit_of_measurement = self._unit_of_measurement - self._attr_device_class = device_class - self._attr_state_class = state_class + self._json_attrs_path = config.get(CONF_JSON_ATTRS_PATH) @property def native_value(self): diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index a8a88c57bd3..96cc5a8330e 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -125,6 +125,8 @@ async def async_setup_platform( class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): """Representation of a templated Alarm Control Panel.""" + _attr_should_poll = False + def __init__( self, hass, diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 2a537e2aa6b..e91a2925b06 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -195,6 +195,8 @@ async def async_setup_platform( class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity): """A virtual binary sensor that triggers from another sensor.""" + _attr_should_poll = False + def __init__( self, hass: HomeAssistant, diff --git a/homeassistant/components/template/button.py b/homeassistant/components/template/button.py index ac83f76ca91..2bb2f40d6b4 100644 --- a/homeassistant/components/template/button.py +++ b/homeassistant/components/template/button.py @@ -78,6 +78,8 @@ async def async_setup_platform( class TemplateButtonEntity(TemplateEntity, ButtonEntity): """Representation of a template button.""" + _attr_should_poll = False + def __init__( self, hass: HomeAssistant, diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 82c5cc2578c..53b829cac9e 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -133,6 +133,8 @@ async def async_setup_platform( class CoverTemplate(TemplateEntity, CoverEntity): """Representation of a Template cover.""" + _attr_should_poll = False + def __init__( self, hass, diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 55f81b697bf..ca50f6017bd 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -125,6 +125,8 @@ async def async_setup_platform( class TemplateFan(TemplateEntity, FanEntity): """A template fan component.""" + _attr_should_poll = False + def __init__( self, hass, diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index eafb0b3f4d0..807e3e79ef8 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -136,6 +136,8 @@ async def async_setup_platform( class LightTemplate(TemplateEntity, LightEntity): """Representation of a templated Light, including dimmable.""" + _attr_should_poll = False + def __init__( self, hass, diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 1d94194be63..f76750124ed 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -70,6 +70,8 @@ async def async_setup_platform( class TemplateLock(TemplateEntity, LockEntity): """Representation of a template lock.""" + _attr_should_poll = False + def __init__( self, hass, diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index 54131990a26..bf3dcbb120b 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -100,6 +100,8 @@ async def async_setup_platform( class TemplateNumber(TemplateEntity, NumberEntity): """Representation of a template number.""" + _attr_should_poll = False + def __init__( self, hass: HomeAssistant, diff --git a/homeassistant/components/template/select.py b/homeassistant/components/template/select.py index 19f17096178..4aa36a378ab 100644 --- a/homeassistant/components/template/select.py +++ b/homeassistant/components/template/select.py @@ -94,6 +94,8 @@ async def async_setup_platform( class TemplateSelect(TemplateEntity, SelectEntity): """Representation of a template select.""" + _attr_should_poll = False + def __init__( self, hass: HomeAssistant, diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 126dd551c45..ee1ddfa8c2d 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -12,10 +12,8 @@ from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, - STATE_CLASSES_SCHEMA, RestoreSensor, SensorDeviceClass, - SensorEntity, ) from homeassistant.components.sensor.helpers import async_parse_date_datetime from homeassistant.const import ( @@ -39,6 +37,10 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.template_entity import ( + TEMPLATE_SENSOR_BASE_SCHEMA, + TemplateSensor, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( @@ -49,7 +51,6 @@ from .const import ( ) from .template_entity import ( TEMPLATE_ENTITY_COMMON_SCHEMA, - TemplateEntity, rewrite_common_legacy_to_modern_conf, ) from .trigger_entity import TriggerEntity @@ -61,16 +62,15 @@ LEGACY_FIELDS = { } -SENSOR_SCHEMA = vol.Schema( - { - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_NAME): cv.template, - vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, - vol.Required(CONF_STATE): cv.template, - vol.Optional(CONF_UNIQUE_ID): cv.string, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - } -).extend(TEMPLATE_ENTITY_COMMON_SCHEMA.schema) +SENSOR_SCHEMA = ( + vol.Schema( + { + vol.Required(CONF_STATE): cv.template, + } + ) + .extend(TEMPLATE_SENSOR_BASE_SCHEMA.schema) + .extend(TEMPLATE_ENTITY_COMMON_SCHEMA.schema) +) LEGACY_SENSOR_SCHEMA = vol.All( @@ -192,9 +192,11 @@ async def async_setup_platform( ) -class SensorTemplate(TemplateEntity, SensorEntity): +class SensorTemplate(TemplateSensor): """Representation of a Template Sensor.""" + _attr_should_poll = False + def __init__( self, hass: HomeAssistant, @@ -202,17 +204,13 @@ class SensorTemplate(TemplateEntity, SensorEntity): unique_id: str | None, ) -> None: """Initialize the sensor.""" - super().__init__(hass, config=config, unique_id=unique_id) + super().__init__(hass, config=config, fallback_name=None, unique_id=unique_id) + self._template = config.get(CONF_STATE) if (object_id := config.get(CONF_OBJECT_ID)) is not None: self.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, object_id, hass=hass ) - self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) - self._template = config.get(CONF_STATE) - self._attr_device_class = config.get(CONF_DEVICE_CLASS) - self._attr_state_class = config.get(CONF_STATE_CLASS) - async def async_added_to_hass(self): """Register callbacks.""" self.add_template_attribute( diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index ac01bc66812..f04f2b5ba7a 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -90,6 +90,8 @@ async def async_setup_platform( class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): """Representation of a Template switch.""" + _attr_should_poll = False + def __init__( self, hass, @@ -149,11 +151,6 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): """Return true if device is on.""" return self._state - @property - def should_poll(self): - """Return the polling state.""" - return False - async def async_turn_on(self, **kwargs): """Fire the on action.""" await self.async_run_script(self._on_script, context=self._context) diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 6e0b7f6f48f..901834237c6 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -1,38 +1,23 @@ """TemplateEntity utility class.""" from __future__ import annotations -from collections.abc import Callable -import contextlib import itertools -import logging from typing import Any import voluptuous as vol from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_ENTITY_PICTURE_TEMPLATE, CONF_FRIENDLY_NAME, CONF_ICON, CONF_ICON_TEMPLATE, CONF_NAME, - EVENT_HOMEASSISTANT_START, - STATE_UNKNOWN, ) -from homeassistant.core import Context, CoreState, Event, State, callback -from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import ( - TrackTemplate, - TrackTemplateResult, - async_track_template_result, -) -from homeassistant.helpers.script import Script, _VarsType -from homeassistant.helpers.template import ( - Template, - TemplateStateFromEntityId, - result_as_boolean, +from homeassistant.helpers.template import Template +from homeassistant.helpers.template_entity import ( # noqa: F401 pylint: disable=unused-import + TEMPLATE_ENTITY_BASE_SCHEMA, + TemplateEntity, ) from .const import ( @@ -43,9 +28,6 @@ from .const import ( CONF_PICTURE, ) -_LOGGER = logging.getLogger(__name__) - - TEMPLATE_ENTITY_AVAILABILITY_SCHEMA = vol.Schema( { vol.Optional(CONF_AVAILABILITY): cv.template, @@ -62,10 +44,8 @@ TEMPLATE_ENTITY_COMMON_SCHEMA = vol.Schema( { vol.Optional(CONF_ATTRIBUTES): vol.Schema({cv.string: cv.template}), vol.Optional(CONF_AVAILABILITY): cv.template, - vol.Optional(CONF_ICON): cv.template, - vol.Optional(CONF_PICTURE): cv.template, } -) +).extend(TEMPLATE_ENTITY_BASE_SCHEMA.schema) TEMPLATE_ENTITY_ATTRIBUTES_SCHEMA_LEGACY = vol.Schema( { @@ -121,356 +101,3 @@ def rewrite_common_legacy_to_modern_conf( entity_cfg[CONF_NAME] = Template(entity_cfg[CONF_NAME]) return entity_cfg - - -class _TemplateAttribute: - """Attribute value linked to template result.""" - - def __init__( - self, - entity: Entity, - attribute: str, - template: Template, - validator: Callable[[Any], Any] = None, - on_update: Callable[[Any], None] | None = None, - none_on_template_error: bool | None = False, - ) -> None: - """Template attribute.""" - self._entity = entity - self._attribute = attribute - self.template = template - self.validator = validator - self.on_update = on_update - self.async_update = None - self.none_on_template_error = none_on_template_error - - @callback - def async_setup(self): - """Config update path for the attribute.""" - if self.on_update: - return - - if not hasattr(self._entity, self._attribute): - raise AttributeError(f"Attribute '{self._attribute}' does not exist.") - - self.on_update = self._default_update - - @callback - def _default_update(self, result): - attr_result = None if isinstance(result, TemplateError) else result - setattr(self._entity, self._attribute, attr_result) - - @callback - def handle_result( - self, - event: Event | None, - template: Template, - last_result: str | None | TemplateError, - result: str | TemplateError, - ) -> None: - """Handle a template result event callback.""" - if isinstance(result, TemplateError): - _LOGGER.error( - "TemplateError('%s') " - "while processing template '%s' " - "for attribute '%s' in entity '%s'", - result, - self.template, - self._attribute, - self._entity.entity_id, - ) - if self.none_on_template_error: - self._default_update(result) - else: - assert self.on_update - self.on_update(result) - return - - if not self.validator: - assert self.on_update - self.on_update(result) - return - - try: - validated = self.validator(result) - except vol.Invalid as ex: - _LOGGER.error( - "Error validating template result '%s' " - "from template '%s' " - "for attribute '%s' in entity %s " - "validation message '%s'", - result, - self.template, - self._attribute, - self._entity.entity_id, - ex.msg, - ) - assert self.on_update - self.on_update(None) - return - - assert self.on_update - self.on_update(validated) - return - - -class TemplateEntity(Entity): - """Entity that uses templates to calculate attributes.""" - - _attr_available = True - _attr_entity_picture = None - _attr_icon = None - _attr_should_poll = False - - def __init__( - self, - hass, - *, - availability_template=None, - icon_template=None, - entity_picture_template=None, - attribute_templates=None, - config=None, - fallback_name=None, - unique_id=None, - ): - """Template Entity.""" - self._template_attrs = {} - self._async_update = None - self._attr_extra_state_attributes = {} - self._self_ref_update_count = 0 - self._attr_unique_id = unique_id - if config is None: - self._attribute_templates = attribute_templates - self._availability_template = availability_template - self._icon_template = icon_template - self._entity_picture_template = entity_picture_template - self._friendly_name_template = None - else: - self._attribute_templates = config.get(CONF_ATTRIBUTES) - self._availability_template = config.get(CONF_AVAILABILITY) - self._icon_template = config.get(CONF_ICON) - self._entity_picture_template = config.get(CONF_PICTURE) - self._friendly_name_template = config.get(CONF_NAME) - - class DummyState(State): - """None-state for template entities not yet added to the state machine.""" - - def __init__(self) -> None: - """Initialize a new state.""" - super().__init__("unknown.unknown", STATE_UNKNOWN) - self.entity_id = None # type: ignore[assignment] - - @property - def name(self) -> str: - """Name of this state.""" - return "" - - variables = {"this": DummyState()} - - # Try to render the name as it can influence the entity ID - self._attr_name = fallback_name - if self._friendly_name_template: - self._friendly_name_template.hass = hass - with contextlib.suppress(TemplateError): - self._attr_name = self._friendly_name_template.async_render( - variables=variables, parse_result=False - ) - - # Templates will not render while the entity is unavailable, try to render the - # icon and picture templates. - if self._entity_picture_template: - self._entity_picture_template.hass = hass - with contextlib.suppress(TemplateError): - self._attr_entity_picture = self._entity_picture_template.async_render( - variables=variables, parse_result=False - ) - - if self._icon_template: - self._icon_template.hass = hass - with contextlib.suppress(TemplateError): - self._attr_icon = self._icon_template.async_render( - variables=variables, parse_result=False - ) - - @callback - def _update_available(self, result): - if isinstance(result, TemplateError): - self._attr_available = True - return - - self._attr_available = result_as_boolean(result) - - @callback - def _update_state(self, result): - if self._availability_template: - return - - self._attr_available = not isinstance(result, TemplateError) - - @callback - def _add_attribute_template(self, attribute_key, attribute_template): - """Create a template tracker for the attribute.""" - - def _update_attribute(result): - attr_result = None if isinstance(result, TemplateError) else result - self._attr_extra_state_attributes[attribute_key] = attr_result - - self.add_template_attribute( - attribute_key, attribute_template, None, _update_attribute - ) - - def add_template_attribute( - self, - attribute: str, - template: Template, - validator: Callable[[Any], Any] = None, - on_update: Callable[[Any], None] | None = None, - none_on_template_error: bool = False, - ) -> None: - """ - Call in the constructor to add a template linked to a attribute. - - Parameters - ---------- - attribute - The name of the attribute to link to. This attribute must exist - unless a custom on_update method is supplied. - template - The template to calculate. - validator - Validator function to parse the result and ensure it's valid. - on_update - Called to store the template result rather than storing it - the supplied attribute. Passed the result of the validator, or None - if the template or validator resulted in an error. - - """ - assert self.hass is not None, "hass cannot be None" - template.hass = self.hass - template_attribute = _TemplateAttribute( - self, attribute, template, validator, on_update, none_on_template_error - ) - self._template_attrs.setdefault(template, []) - self._template_attrs[template].append(template_attribute) - - @callback - def _handle_results( - self, - event: Event | None, - updates: list[TrackTemplateResult], - ) -> None: - """Call back the results to the attributes.""" - if event: - self.async_set_context(event.context) - - entity_id = event and event.data.get(ATTR_ENTITY_ID) - - if entity_id and entity_id == self.entity_id: - self._self_ref_update_count += 1 - else: - self._self_ref_update_count = 0 - - if self._self_ref_update_count > len(self._template_attrs): - for update in updates: - _LOGGER.warning( - "Template loop detected while processing event: %s, skipping template render for Template[%s]", - event, - update.template.template, - ) - return - - for update in updates: - for attr in self._template_attrs[update.template]: - attr.handle_result( - event, update.template, update.last_result, update.result - ) - - self.async_write_ha_state() - - async def _async_template_startup(self, *_) -> None: - template_var_tups: list[TrackTemplate] = [] - has_availability_template = False - - variables = {"this": TemplateStateFromEntityId(self.hass, self.entity_id)} - - for template, attributes in self._template_attrs.items(): - template_var_tup = TrackTemplate(template, variables) - is_availability_template = False - for attribute in attributes: - # pylint: disable-next=protected-access - if attribute._attribute == "_attr_available": - has_availability_template = True - is_availability_template = True - attribute.async_setup() - # Insert the availability template first in the list - if is_availability_template: - template_var_tups.insert(0, template_var_tup) - else: - template_var_tups.append(template_var_tup) - - result_info = async_track_template_result( - self.hass, - template_var_tups, - self._handle_results, - has_super_template=has_availability_template, - ) - self.async_on_remove(result_info.async_remove) - self._async_update = result_info.async_refresh - result_info.async_refresh() - - async def async_added_to_hass(self) -> None: - """Run when entity about to be added to hass.""" - if self._availability_template is not None: - self.add_template_attribute( - "_attr_available", - self._availability_template, - None, - self._update_available, - ) - if self._attribute_templates is not None: - for key, value in self._attribute_templates.items(): - self._add_attribute_template(key, value) - if self._icon_template is not None: - self.add_template_attribute( - "_attr_icon", self._icon_template, vol.Or(cv.whitespace, cv.icon) - ) - if self._entity_picture_template is not None: - self.add_template_attribute( - "_attr_entity_picture", self._entity_picture_template - ) - if ( - self._friendly_name_template is not None - and not self._friendly_name_template.is_static - ): - self.add_template_attribute("_attr_name", self._friendly_name_template) - - if self.hass.state == CoreState.running: - await self._async_template_startup() - return - - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, self._async_template_startup - ) - - async def async_update(self) -> None: - """Call for forced update.""" - self._async_update() - - async def async_run_script( - self, - script: Script, - *, - run_variables: _VarsType | None = None, - context: Context | None = None, - ) -> None: - """Run an action script.""" - if run_variables is None: - run_variables = {} - return await script.async_run( - run_variables={ - "this": TemplateStateFromEntityId(self.hass, self.entity_id), - **run_variables, - }, - context=context, - ) diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 4b278ef6aec..5f306bfa5e1 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -126,6 +126,8 @@ async def async_setup_platform( class TemplateVacuum(TemplateEntity, StateVacuumEntity): """A template vacuum component.""" + _attr_should_poll = False + def __init__( self, hass, diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index d4069096553..5d1a48269aa 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -105,6 +105,8 @@ async def async_setup_platform( class WeatherTemplate(TemplateEntity, WeatherEntity): """Representation of a weather condition.""" + _attr_should_poll = False + def __init__( self, hass, diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py new file mode 100644 index 00000000000..e92ba233121 --- /dev/null +++ b/homeassistant/helpers/template_entity.py @@ -0,0 +1,434 @@ +"""TemplateEntity utility class.""" +from __future__ import annotations + +from collections.abc import Callable +import contextlib +import logging +from typing import Any + +import voluptuous as vol + +from homeassistant.components.sensor import ( + CONF_STATE_CLASS, + DEVICE_CLASSES_SCHEMA, + STATE_CLASSES_SCHEMA, + SensorEntity, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DEVICE_CLASS, + CONF_ICON, + CONF_NAME, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + EVENT_HOMEASSISTANT_START, + STATE_UNKNOWN, +) +from homeassistant.core import Context, CoreState, Event, HomeAssistant, State, callback +from homeassistant.exceptions import TemplateError + +from . import config_validation as cv +from .entity import Entity +from .event import TrackTemplate, TrackTemplateResult, async_track_template_result +from .script import Script, _VarsType +from .template import Template, TemplateStateFromEntityId, result_as_boolean +from .typing import ConfigType + +_LOGGER = logging.getLogger(__name__) + +CONF_AVAILABILITY = "availability" +CONF_ATTRIBUTES = "attributes" +CONF_PICTURE = "picture" + +TEMPLATE_ENTITY_BASE_SCHEMA = vol.Schema( + { + vol.Optional(CONF_ICON): cv.template, + vol.Optional(CONF_PICTURE): cv.template, + } +) + +TEMPLATE_SENSOR_BASE_SCHEMA = vol.Schema( + { + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_NAME): cv.template, + vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + } +).extend(TEMPLATE_ENTITY_BASE_SCHEMA.schema) + + +class _TemplateAttribute: + """Attribute value linked to template result.""" + + def __init__( + self, + entity: Entity, + attribute: str, + template: Template, + validator: Callable[[Any], Any] | None = None, + on_update: Callable[[Any], None] | None = None, + none_on_template_error: bool | None = False, + ) -> None: + """Template attribute.""" + self._entity = entity + self._attribute = attribute + self.template = template + self.validator = validator + self.on_update = on_update + self.async_update = None + self.none_on_template_error = none_on_template_error + + @callback + def async_setup(self) -> None: + """Config update path for the attribute.""" + if self.on_update: + return + + if not hasattr(self._entity, self._attribute): + raise AttributeError(f"Attribute '{self._attribute}' does not exist.") + + self.on_update = self._default_update + + @callback + def _default_update(self, result: str | TemplateError) -> None: + attr_result = None if isinstance(result, TemplateError) else result + setattr(self._entity, self._attribute, attr_result) + + @callback + def handle_result( + self, + event: Event | None, + template: Template, + last_result: str | None | TemplateError, + result: str | TemplateError, + ) -> None: + """Handle a template result event callback.""" + if isinstance(result, TemplateError): + _LOGGER.error( + "TemplateError('%s') " + "while processing template '%s' " + "for attribute '%s' in entity '%s'", + result, + self.template, + self._attribute, + self._entity.entity_id, + ) + if self.none_on_template_error: + self._default_update(result) + else: + assert self.on_update + self.on_update(result) + return + + if not self.validator: + assert self.on_update + self.on_update(result) + return + + try: + validated = self.validator(result) + except vol.Invalid as ex: + _LOGGER.error( + "Error validating template result '%s' " + "from template '%s' " + "for attribute '%s' in entity %s " + "validation message '%s'", + result, + self.template, + self._attribute, + self._entity.entity_id, + ex.msg, + ) + assert self.on_update + self.on_update(None) + return + + assert self.on_update + self.on_update(validated) + return + + +class TemplateEntity(Entity): + """Entity that uses templates to calculate attributes.""" + + _attr_available = True + _attr_entity_picture = None + _attr_icon = None + + def __init__( + self, + hass: HomeAssistant, + *, + availability_template: Template | None = None, + icon_template: Template | None = None, + entity_picture_template: Template | None = None, + attribute_templates: dict[str, Template] | None = None, + config: ConfigType | None = None, + fallback_name: str | None = None, + unique_id: str | None = None, + ) -> None: + """Template Entity.""" + self._template_attrs: dict[Template, list[_TemplateAttribute]] = {} + self._async_update: Callable[[], None] | None = None + self._attr_extra_state_attributes = {} + self._self_ref_update_count = 0 + self._attr_unique_id = unique_id + if config is None: + self._attribute_templates = attribute_templates + self._availability_template = availability_template + self._icon_template = icon_template + self._entity_picture_template = entity_picture_template + self._friendly_name_template = None + else: + self._attribute_templates = config.get(CONF_ATTRIBUTES) + self._availability_template = config.get(CONF_AVAILABILITY) + self._icon_template = config.get(CONF_ICON) + self._entity_picture_template = config.get(CONF_PICTURE) + self._friendly_name_template = config.get(CONF_NAME) + + class DummyState(State): + """None-state for template entities not yet added to the state machine.""" + + def __init__(self) -> None: + """Initialize a new state.""" + super().__init__("unknown.unknown", STATE_UNKNOWN) + self.entity_id = None # type: ignore[assignment] + + @property + def name(self) -> str: + """Name of this state.""" + return "" + + variables = {"this": DummyState()} + + # Try to render the name as it can influence the entity ID + self._attr_name = fallback_name + if self._friendly_name_template: + self._friendly_name_template.hass = hass + with contextlib.suppress(TemplateError): + self._attr_name = self._friendly_name_template.async_render( + variables=variables, parse_result=False + ) + + # Templates will not render while the entity is unavailable, try to render the + # icon and picture templates. + if self._entity_picture_template: + self._entity_picture_template.hass = hass + with contextlib.suppress(TemplateError): + self._attr_entity_picture = self._entity_picture_template.async_render( + variables=variables, parse_result=False + ) + + if self._icon_template: + self._icon_template.hass = hass + with contextlib.suppress(TemplateError): + self._attr_icon = self._icon_template.async_render( + variables=variables, parse_result=False + ) + + @callback + def _update_available(self, result: str | TemplateError) -> None: + if isinstance(result, TemplateError): + self._attr_available = True + return + + self._attr_available = result_as_boolean(result) + + @callback + def _update_state(self, result: str | TemplateError) -> None: + if self._availability_template: + return + + self._attr_available = not isinstance(result, TemplateError) + + @callback + def _add_attribute_template( + self, attribute_key: str, attribute_template: Template + ) -> None: + """Create a template tracker for the attribute.""" + + def _update_attribute(result: str | TemplateError) -> None: + attr_result = None if isinstance(result, TemplateError) else result + self._attr_extra_state_attributes[attribute_key] = attr_result + + self.add_template_attribute( + attribute_key, attribute_template, None, _update_attribute + ) + + def add_template_attribute( + self, + attribute: str, + template: Template, + validator: Callable[[Any], Any] | None = None, + on_update: Callable[[Any], None] | None = None, + none_on_template_error: bool = False, + ) -> None: + """ + Call in the constructor to add a template linked to a attribute. + + Parameters + ---------- + attribute + The name of the attribute to link to. This attribute must exist + unless a custom on_update method is supplied. + template + The template to calculate. + validator + Validator function to parse the result and ensure it's valid. + on_update + Called to store the template result rather than storing it + the supplied attribute. Passed the result of the validator, or None + if the template or validator resulted in an error. + + """ + assert self.hass is not None, "hass cannot be None" + template.hass = self.hass + template_attribute = _TemplateAttribute( + self, attribute, template, validator, on_update, none_on_template_error + ) + self._template_attrs.setdefault(template, []) + self._template_attrs[template].append(template_attribute) + + @callback + def _handle_results( + self, + event: Event | None, + updates: list[TrackTemplateResult], + ) -> None: + """Call back the results to the attributes.""" + if event: + self.async_set_context(event.context) + + entity_id = event and event.data.get(ATTR_ENTITY_ID) + + if entity_id and entity_id == self.entity_id: + self._self_ref_update_count += 1 + else: + self._self_ref_update_count = 0 + + if self._self_ref_update_count > len(self._template_attrs): + for update in updates: + _LOGGER.warning( + "Template loop detected while processing event: %s, skipping template render for Template[%s]", + event, + update.template.template, + ) + return + + for update in updates: + for attr in self._template_attrs[update.template]: + attr.handle_result( + event, update.template, update.last_result, update.result + ) + + self.async_write_ha_state() + + async def _async_template_startup(self, *_: Any) -> None: + template_var_tups: list[TrackTemplate] = [] + has_availability_template = False + + variables = {"this": TemplateStateFromEntityId(self.hass, self.entity_id)} + + for template, attributes in self._template_attrs.items(): + template_var_tup = TrackTemplate(template, variables) + is_availability_template = False + for attribute in attributes: + # pylint: disable-next=protected-access + if attribute._attribute == "_attr_available": + has_availability_template = True + is_availability_template = True + attribute.async_setup() + # Insert the availability template first in the list + if is_availability_template: + template_var_tups.insert(0, template_var_tup) + else: + template_var_tups.append(template_var_tup) + + result_info = async_track_template_result( + self.hass, + template_var_tups, + self._handle_results, + has_super_template=has_availability_template, + ) + self.async_on_remove(result_info.async_remove) + self._async_update = result_info.async_refresh + result_info.async_refresh() + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + if self._availability_template is not None: + self.add_template_attribute( + "_attr_available", + self._availability_template, + None, + self._update_available, + ) + if self._attribute_templates is not None: + for key, value in self._attribute_templates.items(): + self._add_attribute_template(key, value) + if self._icon_template is not None: + self.add_template_attribute( + "_attr_icon", self._icon_template, vol.Or(cv.whitespace, cv.icon) + ) + if self._entity_picture_template is not None: + self.add_template_attribute( + "_attr_entity_picture", self._entity_picture_template + ) + if ( + self._friendly_name_template is not None + and not self._friendly_name_template.is_static + ): + self.add_template_attribute("_attr_name", self._friendly_name_template) + + if self.hass.state == CoreState.running: + await self._async_template_startup() + return + + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_START, self._async_template_startup + ) + + async def async_update(self) -> None: + """Call for forced update.""" + assert self._async_update + self._async_update() + + async def async_run_script( + self, + script: Script, + *, + run_variables: _VarsType | None = None, + context: Context | None = None, + ) -> None: + """Run an action script.""" + if run_variables is None: + run_variables = {} + return await script.async_run( + run_variables={ + "this": TemplateStateFromEntityId(self.hass, self.entity_id), + **run_variables, + }, + context=context, + ) + + +class TemplateSensor(TemplateEntity, SensorEntity): + """Representation of a Template Sensor.""" + + def __init__( + self, + hass: HomeAssistant, + *, + config: dict[str, Any], + fallback_name: str | None, + unique_id: str | None, + ) -> None: + """Initialize the sensor.""" + super().__init__( + hass, config=config, fallback_name=fallback_name, unique_id=unique_id + ) + + self._attr_native_unit_of_measurement = config.get(CONF_UNIT_OF_MEASUREMENT) + self._attr_device_class = config.get(CONF_DEVICE_CLASS) + self._attr_state_class = config.get(CONF_STATE_CLASS) diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 86ce816f932..a89d20f2510 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -24,6 +24,8 @@ from homeassistant.const import ( STATE_UNKNOWN, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from tests.common import get_fixture_path @@ -864,3 +866,43 @@ async def test_reload(hass): assert hass.states.get("sensor.mockreset") is None assert hass.states.get("sensor.rollout") + + +@respx.mock +async def test_entity_config(hass: HomeAssistant) -> None: + """Test entity configuration.""" + + config = { + DOMAIN: { + # REST configuration + "platform": "rest", + "method": "GET", + "resource": "http://localhost", + # Entity configuration + "icon": "{{'mdi:one_two_three'}}", + "picture": "{{'blabla.png'}}", + "device_class": "temperature", + "name": "{{'REST' + ' ' + 'Sensor'}}", + "state_class": "measurement", + "unique_id": "very_unique", + "unit_of_measurement": "beardsecond", + }, + } + + respx.get("http://localhost") % HTTPStatus.OK + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + assert entity_registry.async_get("sensor.rest_sensor").unique_id == "very_unique" + + state = hass.states.get("sensor.rest_sensor") + assert state.state == "" + assert state.attributes == { + "device_class": "temperature", + "entity_picture": "blabla.png", + "friendly_name": "REST Sensor", + "icon": "mdi:one_two_three", + "state_class": "measurement", + "unit_of_measurement": "beardsecond", + } From e74c711ef381ef3f95fe4f66cfe759260c2762b7 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 8 Jun 2022 07:09:32 -0700 Subject: [PATCH 1357/3516] Add application credentials description strings (#73014) --- .../application_credentials/__init__.py | 24 ++++++++++++++++--- .../google/application_credentials.py | 9 +++++++ homeassistant/components/google/strings.json | 3 +++ .../components/google/translations/en.json | 3 +++ script/hassfest/translations.py | 3 +++ .../application_credentials/test_init.py | 6 ++++- 6 files changed, 44 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/application_credentials/__init__.py b/homeassistant/components/application_credentials/__init__.py index 1a128c5c378..14ae049cfca 100644 --- a/homeassistant/components/application_credentials/__init__.py +++ b/homeassistant/components/application_credentials/__init__.py @@ -253,6 +253,11 @@ class ApplicationCredentialsProtocol(Protocol): ) -> config_entry_oauth2_flow.AbstractOAuth2Implementation: """Return a custom auth implementation.""" + async def async_get_description_placeholders( + self, hass: HomeAssistant + ) -> dict[str, str]: + """Return description placeholders for the credentials dialog.""" + async def _get_platform( hass: HomeAssistant, integration_domain: str @@ -282,6 +287,14 @@ async def _get_platform( return platform +async def _async_integration_config(hass: HomeAssistant, domain: str) -> dict[str, Any]: + platform = await _get_platform(hass, domain) + if platform and hasattr(platform, "async_get_description_placeholders"): + placeholders = await platform.async_get_description_placeholders(hass) + return {"description_placeholders": placeholders} + return {} + + @websocket_api.websocket_command( {vol.Required("type"): "application_credentials/config"} ) @@ -290,6 +303,11 @@ async def handle_integration_list( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Handle integrations command.""" - connection.send_result( - msg["id"], {"domains": await async_get_application_credentials(hass)} - ) + domains = await async_get_application_credentials(hass) + result = { + "domains": domains, + "integrations": { + domain: await _async_integration_config(hass, domain) for domain in domains + }, + } + connection.send_result(msg["id"], result) diff --git a/homeassistant/components/google/application_credentials.py b/homeassistant/components/google/application_credentials.py index 2f1fcba8084..3d557630b05 100644 --- a/homeassistant/components/google/application_credentials.py +++ b/homeassistant/components/google/application_credentials.py @@ -21,3 +21,12 @@ async def async_get_auth_implementation( ) -> config_entry_oauth2_flow.AbstractOAuth2Implementation: """Return auth implementation.""" return DeviceAuth(hass, auth_domain, credential, AUTHORIZATION_SERVER) + + +async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]: + """Return description placeholders for the credentials dialog.""" + return { + "oauth_consent_url": "https://console.cloud.google.com/apis/credentials/consent", + "more_info_url": "https://www.home-assistant.io/integrations/google/", + "oauth_creds_url": "https://console.cloud.google.com/apis/credentials", + } diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json index e32223627be..6652806cd0f 100644 --- a/homeassistant/components/google/strings.json +++ b/homeassistant/components/google/strings.json @@ -36,5 +36,8 @@ } } } + }, + "application_credentials": { + "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Calendar. You also need to create Application Credentials linked to your Calendar:\n1. Go to [Credentials]({oauth_creds_url}) and click **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **TV and Limited Input devices** for the Application Type.\n\n" } } diff --git a/homeassistant/components/google/translations/en.json b/homeassistant/components/google/translations/en.json index 58c89834ca5..54936b0d81c 100644 --- a/homeassistant/components/google/translations/en.json +++ b/homeassistant/components/google/translations/en.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Calendar. You also need to create Application Credentials linked to your Calendar:\n1. Go to [Credentials]({oauth_creds_url}) page and click **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **TV and Limited Input devices** for the Application Type.\n\n" + }, "config": { "abort": { "already_configured": "Account is already configured", diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 0dcdbc133a6..a1f520808f6 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -224,6 +224,9 @@ def gen_strings_schema(config: Config, integration: Integration): ), slug_validator=vol.Any("_", cv.slug), ), + vol.Optional("application_credentials"): { + vol.Optional("description"): cv.string_with_no_html, + }, } ) diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py index b89a60f42e4..dd5995a8f4f 100644 --- a/tests/components/application_credentials/test_init.py +++ b/tests/components/application_credentials/test_init.py @@ -701,7 +701,11 @@ async def test_websocket_integration_list(ws_client: ClientFixture): "homeassistant.loader.APPLICATION_CREDENTIALS", ["example1", "example2"] ): assert await client.cmd_result("config") == { - "domains": ["example1", "example2"] + "domains": ["example1", "example2"], + "integrations": { + "example1": {}, + "example2": {}, + }, } From 0dc1e7d1e64a0172c49a1114f811797283c12de7 Mon Sep 17 00:00:00 2001 From: b3nj1 Date: Wed, 8 Jun 2022 07:43:24 -0700 Subject: [PATCH 1358/3516] Fix VeSync device to match pyvesync type (#73034) * vesync: change device to match pyvesync type * MartinHjelmare's suggestion for derived classes Co-authored-by: Martin Hjelmare * MartinHjelmare's suggestion for derived classes Co-authored-by: Martin Hjelmare * MartinHjelmare's suggestion for derived classes Co-authored-by: Martin Hjelmare * MartinHjelmare's suggestion for annotations * vesync: fix imports Co-authored-by: Martin Hjelmare --- homeassistant/components/vesync/sensor.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/vesync/sensor.py b/homeassistant/components/vesync/sensor.py index 24ba6f2f0a0..6e0c09b2f60 100644 --- a/homeassistant/components/vesync/sensor.py +++ b/homeassistant/components/vesync/sensor.py @@ -1,8 +1,14 @@ """Support for power & energy sensors for VeSync outlets.""" +from __future__ import annotations + from collections.abc import Callable from dataclasses import dataclass import logging +from pyvesync.vesyncfan import VeSyncAirBypass +from pyvesync.vesyncoutlet import VeSyncOutlet +from pyvesync.vesyncswitch import VeSyncSwitch + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -22,7 +28,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from .common import VeSyncBaseEntity, VeSyncDevice +from .common import VeSyncBaseEntity from .const import DEV_TYPE_TO_HA, DOMAIN, SKU_TO_BASE_DEVICE, VS_DISCOVERY, VS_SENSORS _LOGGER = logging.getLogger(__name__) @@ -32,7 +38,7 @@ _LOGGER = logging.getLogger(__name__) class VeSyncSensorEntityDescriptionMixin: """Mixin for required keys.""" - value_fn: Callable[[VeSyncDevice], StateType] + value_fn: Callable[[VeSyncAirBypass | VeSyncOutlet | VeSyncSwitch], StateType] @dataclass @@ -41,8 +47,12 @@ class VeSyncSensorEntityDescription( ): """Describe VeSync sensor entity.""" - exists_fn: Callable[[VeSyncDevice], bool] = lambda _: True - update_fn: Callable[[VeSyncDevice], None] = lambda _: None + exists_fn: Callable[ + [VeSyncAirBypass | VeSyncOutlet | VeSyncSwitch], bool + ] = lambda _: True + update_fn: Callable[ + [VeSyncAirBypass | VeSyncOutlet | VeSyncSwitch], None + ] = lambda _: None def update_energy(device): @@ -107,7 +117,7 @@ SENSORS: tuple[VeSyncSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda device: device.details["energy"], + value_fn=lambda device: device.energy_today, update_fn=update_energy, exists_fn=lambda device: ha_dev_type(device) == "outlet", ), @@ -151,7 +161,7 @@ class VeSyncSensorEntity(VeSyncBaseEntity, SensorEntity): def __init__( self, - device: VeSyncDevice, + device: VeSyncAirBypass | VeSyncOutlet | VeSyncSwitch, description: VeSyncSensorEntityDescription, ) -> None: """Initialize the VeSync outlet device.""" From 56d28e13f7808a657c1637e584431d8913e7f492 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 8 Jun 2022 20:12:44 +0200 Subject: [PATCH 1359/3516] Update apprise to 0.9.9 (#73218) --- homeassistant/components/apprise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index b4422a49ef4..450e0a964df 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -2,7 +2,7 @@ "domain": "apprise", "name": "Apprise", "documentation": "https://www.home-assistant.io/integrations/apprise", - "requirements": ["apprise==0.9.8.3"], + "requirements": ["apprise==0.9.9"], "codeowners": ["@caronc"], "iot_class": "cloud_push", "loggers": ["apprise"] diff --git a/requirements_all.txt b/requirements_all.txt index b72237ffdf1..866615632f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -313,7 +313,7 @@ anthemav==1.2.0 apcaccess==0.0.13 # homeassistant.components.apprise -apprise==0.9.8.3 +apprise==0.9.9 # homeassistant.components.aprs aprslib==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5e44767eec0..fd396ce2564 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -267,7 +267,7 @@ ambiclimate==0.2.1 androidtv[async]==0.0.67 # homeassistant.components.apprise -apprise==0.9.8.3 +apprise==0.9.9 # homeassistant.components.aprs aprslib==0.7.0 From b4a5abce16a52b44626dcafd2b992916445d705e Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 8 Jun 2022 14:15:07 -0400 Subject: [PATCH 1360/3516] Clean up phone modem (#73181) --- homeassistant/components/modem_callerid/sensor.py | 4 ---- homeassistant/components/modem_callerid/services.yaml | 7 ------- 2 files changed, 11 deletions(-) delete mode 100644 homeassistant/components/modem_callerid/services.yaml diff --git a/homeassistant/components/modem_callerid/sensor.py b/homeassistant/components/modem_callerid/sensor.py index e50eabb17aa..4f84abd4533 100644 --- a/homeassistant/components/modem_callerid/sensor.py +++ b/homeassistant/components/modem_callerid/sensor.py @@ -1,8 +1,6 @@ """A sensor for incoming calls using a USB modem that supports caller ID.""" from __future__ import annotations -import logging - from phone_modem import PhoneModem from homeassistant.components.sensor import SensorEntity @@ -13,8 +11,6 @@ from homeassistant.helpers import entity_platform from .const import CID, DATA_KEY_API, DOMAIN, ICON -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/modem_callerid/services.yaml b/homeassistant/components/modem_callerid/services.yaml deleted file mode 100644 index 7ec8aaf3f94..00000000000 --- a/homeassistant/components/modem_callerid/services.yaml +++ /dev/null @@ -1,7 +0,0 @@ -reject_call: - name: Reject Call - description: Reject incoming call. - target: - entity: - integration: modem_callerid - domain: sensor From f7bd88c952d398fa4e3b8d2510aa61e21db8007a Mon Sep 17 00:00:00 2001 From: d0nni3q84 <62199227+d0nni3q84@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:32:01 -0500 Subject: [PATCH 1361/3516] Fix Feedreader Atom feeds using `updated` date (#73208) * Feedreader: Properly support Atom feeds that use only the `updated` date format and resolve #73207. * Revert "Feedreader: Properly support Atom feeds that use only the `updated` date format and resolve #73207." This reverts commit 4dbd11ee04b4e8f935a22dfb51405b7bdaaba676. * Properly support Atom feeds that use only the `updated` date format and resolve #73207. * Revert "Properly support Atom feeds that use only the `updated` date format and resolve #73207." This reverts commit 14366c6a2491584282b8bb96fe3779fd41849897. * Properly support Atom feeds that use only the `updated` date format and resolve #73207. --- .../components/feedreader/__init__.py | 31 ++++++++++++++---- tests/components/feedreader/test_init.py | 32 ++++++++++++++++++- tests/fixtures/feedreader5.xml | 18 +++++++++++ 3 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 tests/fixtures/feedreader5.xml diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index 11a3d4b0498..b3f1a916012 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -70,6 +70,7 @@ class FeedManager: self._last_entry_timestamp = None self._last_update_successful = False self._has_published_parsed = False + self._has_updated_parsed = False self._event_type = EVENT_FEEDREADER self._feed_id = url hass.bus.listen_once(EVENT_HOMEASSISTANT_START, lambda _: self._update()) @@ -122,7 +123,7 @@ class FeedManager: ) self._filter_entries() self._publish_new_entries() - if self._has_published_parsed: + if self._has_published_parsed or self._has_updated_parsed: self._storage.put_timestamp( self._feed_id, self._last_entry_timestamp ) @@ -143,7 +144,7 @@ class FeedManager: def _update_and_fire_entry(self, entry): """Update last_entry_timestamp and fire entry.""" - # Check if the entry has a published date. + # Check if the entry has a published or updated date. if "published_parsed" in entry and entry.published_parsed: # We are lucky, `published_parsed` data available, let's make use of # it to publish only new available entries since the last run @@ -151,9 +152,20 @@ class FeedManager: self._last_entry_timestamp = max( entry.published_parsed, self._last_entry_timestamp ) + elif "updated_parsed" in entry and entry.updated_parsed: + # We are lucky, `updated_parsed` data available, let's make use of + # it to publish only new available entries since the last run + self._has_updated_parsed = True + self._last_entry_timestamp = max( + entry.updated_parsed, self._last_entry_timestamp + ) else: self._has_published_parsed = False - _LOGGER.debug("No published_parsed info available for entry %s", entry) + self._has_updated_parsed = False + _LOGGER.debug( + "No published_parsed or updated_parsed info available for entry %s", + entry, + ) entry.update({"feed_url": self._url}) self._hass.bus.fire(self._event_type, entry) @@ -167,9 +179,16 @@ class FeedManager: # Set last entry timestamp as epoch time if not available self._last_entry_timestamp = datetime.utcfromtimestamp(0).timetuple() for entry in self._feed.entries: - if self._firstrun or ( - "published_parsed" in entry - and entry.published_parsed > self._last_entry_timestamp + if ( + self._firstrun + or ( + "published_parsed" in entry + and entry.published_parsed > self._last_entry_timestamp + ) + or ( + "updated_parsed" in entry + and entry.updated_parsed > self._last_entry_timestamp + ) ): self._update_and_fire_entry(entry) new_entries = True diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index 34f27c36a6c..be5ebb42a6d 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -23,6 +23,7 @@ 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}} +VALID_CONFIG_5 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_MAX_ENTRIES: 1}} def load_fixture_bytes(src): @@ -56,6 +57,12 @@ def fixture_feed_three_events(hass): return load_fixture_bytes("feedreader3.xml") +@pytest.fixture(name="feed_atom_event") +def fixture_feed_atom_event(hass): + """Load test feed data for atom event.""" + return load_fixture_bytes("feedreader5.xml") + + @pytest.fixture(name="events") async def fixture_events(hass): """Fixture that catches alexa events.""" @@ -98,7 +105,7 @@ async def test_setup_max_entries(hass): async def test_feed(hass, events, feed_one_event): - """Test simple feed with valid data.""" + """Test simple rss feed with valid data.""" with patch( "feedparser.http.get", return_value=feed_one_event, @@ -120,6 +127,29 @@ async def test_feed(hass, events, feed_one_event): assert events[0].data.published_parsed.tm_min == 10 +async def test_atom_feed(hass, events, feed_atom_event): + """Test simple atom feed with valid data.""" + with patch( + "feedparser.http.get", + return_value=feed_atom_event, + ): + assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_5) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data.title == "Atom-Powered Robots Run Amok" + assert events[0].data.description == "Some text." + assert events[0].data.link == "http://example.org/2003/12/13/atom03" + assert events[0].data.id == "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a" + assert events[0].data.updated_parsed.tm_year == 2003 + assert events[0].data.updated_parsed.tm_mon == 12 + assert events[0].data.updated_parsed.tm_mday == 13 + assert events[0].data.updated_parsed.tm_hour == 18 + assert events[0].data.updated_parsed.tm_min == 30 + + async def test_feed_updates(hass, events, feed_one_event, feed_two_event): """Test feed updates.""" side_effect = [ diff --git a/tests/fixtures/feedreader5.xml b/tests/fixtures/feedreader5.xml new file mode 100644 index 00000000000..d9b1dda1ad2 --- /dev/null +++ b/tests/fixtures/feedreader5.xml @@ -0,0 +1,18 @@ + + + Example Feed + + 2003-12-13T18:30:02Z + + John Doe + + urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6 + + Atom-Powered Robots Run Amok + + urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a + 2003-12-13T18:30:02Z + Some text. + + From 921245a490a4aa91fe24751c2feb218fffdf5545 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 8 Jun 2022 20:47:47 +0200 Subject: [PATCH 1362/3516] Remove deprecated temperature conversion of non sensors (#73222) --- homeassistant/helpers/entity.py | 66 +-------------------------------- tests/helpers/test_entity.py | 59 ----------------------------- 2 files changed, 1 insertion(+), 124 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 1c03e2334fe..39af16892f5 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -32,17 +32,8 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) -from homeassistant.core import ( - CALLBACK_TYPE, - Context, - Event, - HomeAssistant, - callback, - split_entity_id, ) +from homeassistant.core import CALLBACK_TYPE, Context, Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, NoEntitySpecifiedError from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util, ensure_unique_string, slugify @@ -259,9 +250,6 @@ class Entity(ABC): # If we reported this entity is updated while disabled _disabled_reported = False - # If we reported this entity is relying on deprecated temperature conversion - _temperature_reported = False - # Protect for multiple updates _update_staged = False @@ -618,58 +606,6 @@ class Entity(ABC): if DATA_CUSTOMIZE in self.hass.data: attr.update(self.hass.data[DATA_CUSTOMIZE].get(self.entity_id)) - def _convert_temperature(state: str, attr: dict[str, Any]) -> str: - # Convert temperature if we detect one - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.sensor import SensorEntity - - unit_of_measure = attr.get(ATTR_UNIT_OF_MEASUREMENT) - units = self.hass.config.units - if unit_of_measure == units.temperature_unit or unit_of_measure not in ( - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - ): - return state - - domain = split_entity_id(self.entity_id)[0] - if domain != "sensor": - if not self._temperature_reported: - self._temperature_reported = True - report_issue = self._suggest_report_issue() - _LOGGER.warning( - "Entity %s (%s) relies on automatic temperature conversion, this will " - "be unsupported in Home Assistant Core 2022.7. Please %s", - self.entity_id, - type(self), - report_issue, - ) - elif not isinstance(self, SensorEntity): - if not self._temperature_reported: - self._temperature_reported = True - report_issue = self._suggest_report_issue() - _LOGGER.warning( - "Temperature sensor %s (%s) does not inherit SensorEntity, " - "this will be unsupported in Home Assistant Core 2022.7." - "Please %s", - self.entity_id, - type(self), - report_issue, - ) - else: - return state - - try: - prec = len(state) - state.index(".") - 1 if "." in state else 0 - temp = units.temperature(float(state), unit_of_measure) - state = str(round(temp) if prec == 0 else round(temp, prec)) - attr[ATTR_UNIT_OF_MEASUREMENT] = units.temperature_unit - except ValueError: - # Could not convert state to float - pass - return state - - state = _convert_temperature(state, attr) - if ( self._context_set is not None and dt_util.utcnow() - self._context_set > self.context_recent_time diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index e345d7d7258..7141c5f0903 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -14,7 +14,6 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_FAHRENHEIT, ) from homeassistant.core import Context, HomeAssistantError from homeassistant.helpers import entity, entity_registry @@ -816,64 +815,6 @@ async def test_float_conversion(hass): assert state.state == "3.6" -async def test_temperature_conversion(hass, caplog): - """Test conversion of temperatures.""" - # Non sensor entity reporting a temperature - with patch.object( - entity.Entity, "state", PropertyMock(return_value=100) - ), patch.object( - entity.Entity, "unit_of_measurement", PropertyMock(return_value=TEMP_FAHRENHEIT) - ): - ent = entity.Entity() - ent.hass = hass - ent.entity_id = "hello.world" - ent.async_write_ha_state() - - state = hass.states.get("hello.world") - assert state is not None - assert state.state == "38" - assert ( - "Entity hello.world () relies on automatic " - "temperature conversion, this will be unsupported in Home Assistant Core 2022.7. " - "Please create a bug report" in caplog.text - ) - - # Sensor entity, not extending SensorEntity, reporting a temperature - with patch.object( - entity.Entity, "state", PropertyMock(return_value=100) - ), patch.object( - entity.Entity, "unit_of_measurement", PropertyMock(return_value=TEMP_FAHRENHEIT) - ): - ent = entity.Entity() - ent.hass = hass - ent.entity_id = "sensor.temp" - ent.async_write_ha_state() - - state = hass.states.get("sensor.temp") - assert state is not None - assert state.state == "38" - assert ( - "Temperature sensor sensor.temp () " - "does not inherit SensorEntity, this will be unsupported in Home Assistant Core " - "2022.7.Please create a bug report" in caplog.text - ) - - # Sensor entity, not extending SensorEntity, not reporting a number - with patch.object( - entity.Entity, "state", PropertyMock(return_value="really warm") - ), patch.object( - entity.Entity, "unit_of_measurement", PropertyMock(return_value=TEMP_FAHRENHEIT) - ): - ent = entity.Entity() - ent.hass = hass - ent.entity_id = "sensor.temp" - ent.async_write_ha_state() - - state = hass.states.get("sensor.temp") - assert state is not None - assert state.state == "really warm" - - async def test_attribution_attribute(hass): """Test attribution attribute.""" mock_entity = entity.Entity() From 6bf219550ef549289e775201408a3242e6795443 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 8 Jun 2022 21:27:40 +0200 Subject: [PATCH 1363/3516] Cleanup some code in SensorEntity (#73241) --- homeassistant/components/sensor/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 6a69c27f9b6..2bbcf3b119d 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -426,7 +426,7 @@ class SensorEntity(Entity): if ( value is not None and native_unit_of_measurement != unit_of_measurement - and self.device_class in UNIT_CONVERSIONS + and device_class in UNIT_CONVERSIONS ): assert unit_of_measurement assert native_unit_of_measurement @@ -439,8 +439,8 @@ class SensorEntity(Entity): ratio_log = max( 0, log10( - UNIT_RATIOS[self.device_class][native_unit_of_measurement] - / UNIT_RATIOS[self.device_class][unit_of_measurement] + UNIT_RATIOS[device_class][native_unit_of_measurement] + / UNIT_RATIOS[device_class][unit_of_measurement] ), ) prec = prec + floor(ratio_log) @@ -448,7 +448,7 @@ class SensorEntity(Entity): # Suppress ValueError (Could not convert sensor_value to float) with suppress(ValueError): value_f = float(value) # type: ignore[arg-type] - value_f_new = UNIT_CONVERSIONS[self.device_class]( + value_f_new = UNIT_CONVERSIONS[device_class]( value_f, native_unit_of_measurement, unit_of_measurement, From 4435c641decd0269e03ba752c35e0aca468c1ab3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 8 Jun 2022 21:36:43 +0200 Subject: [PATCH 1364/3516] Enforce RegistryEntryHider in entity registry (#73219) --- homeassistant/helpers/entity_registry.py | 12 +++++- tests/components/group/test_config_flow.py | 5 ++- tests/components/group/test_init.py | 10 ++--- .../switch_as_x/test_config_flow.py | 4 +- tests/components/switch_as_x/test_init.py | 4 +- tests/helpers/test_entity_registry.py | 42 ++++++++++++++++--- 6 files changed, 60 insertions(+), 17 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index d03d272b1ac..eb5590b7fdf 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -369,6 +369,8 @@ class EntityRegistry: if disabled_by and not isinstance(disabled_by, RegistryEntryDisabler): raise ValueError("disabled_by must be a RegistryEntryDisabler value") + if hidden_by and not isinstance(hidden_by, RegistryEntryHider): + raise ValueError("hidden_by must be a RegistryEntryHider value") if ( disabled_by is None @@ -520,6 +522,12 @@ class EntityRegistry: and not isinstance(disabled_by, RegistryEntryDisabler) ): raise ValueError("disabled_by must be a RegistryEntryDisabler value") + if ( + hidden_by + and hidden_by is not UNDEFINED + and not isinstance(hidden_by, RegistryEntryHider) + ): + raise ValueError("hidden_by must be a RegistryEntryHider value") from .entity import EntityCategory # pylint: disable=import-outside-toplevel @@ -729,7 +737,9 @@ class EntityRegistry: if entity["entity_category"] else None, entity_id=entity["entity_id"], - hidden_by=entity["hidden_by"], + hidden_by=RegistryEntryHider(entity["hidden_by"]) + if entity["hidden_by"] + else None, icon=entity["icon"], id=entity["id"], name=entity["name"], diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 83741a2e851..9d6b099557d 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -348,7 +348,10 @@ async def test_all_options( @pytest.mark.parametrize( "hide_members,hidden_by_initial,hidden_by", - ((False, "integration", None), (True, None, "integration")), + ( + (False, er.RegistryEntryHider.INTEGRATION, None), + (True, None, er.RegistryEntryHider.INTEGRATION), + ), ) @pytest.mark.parametrize( "group_type,extra_input", diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 56553ff263c..945f6555789 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -1421,12 +1421,12 @@ async def test_setup_and_remove_config_entry( @pytest.mark.parametrize( "hide_members,hidden_by_initial,hidden_by", ( - (False, "integration", "integration"), + (False, er.RegistryEntryHider.INTEGRATION, er.RegistryEntryHider.INTEGRATION), (False, None, None), - (False, "user", "user"), - (True, "integration", None), + (False, er.RegistryEntryHider.USER, er.RegistryEntryHider.USER), + (True, er.RegistryEntryHider.INTEGRATION, None), (True, None, None), - (True, "user", "user"), + (True, er.RegistryEntryHider.USER, er.RegistryEntryHider.USER), ), ) @pytest.mark.parametrize( @@ -1444,7 +1444,7 @@ async def test_unhide_members_on_remove( group_type: str, extra_options: dict[str, Any], hide_members: bool, - hidden_by_initial: str, + hidden_by_initial: er.RegistryEntryHider, hidden_by: str, ) -> None: """Test removing a config entry.""" diff --git a/tests/components/switch_as_x/test_config_flow.py b/tests/components/switch_as_x/test_config_flow.py index dc4ca96aa97..d80f7e24bb1 100644 --- a/tests/components/switch_as_x/test_config_flow.py +++ b/tests/components/switch_as_x/test_config_flow.py @@ -65,8 +65,8 @@ async def test_config_flow( @pytest.mark.parametrize( "hidden_by_before,hidden_by_after", ( - (er.RegistryEntryHider.USER.value, er.RegistryEntryHider.USER.value), - (None, er.RegistryEntryHider.INTEGRATION.value), + (er.RegistryEntryHider.USER, er.RegistryEntryHider.USER), + (None, er.RegistryEntryHider.INTEGRATION), ), ) @pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index e2b875b813b..9c3eec1884c 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -365,8 +365,8 @@ async def test_setup_and_remove_config_entry( @pytest.mark.parametrize( "hidden_by_before,hidden_by_after", ( - (er.RegistryEntryHider.USER.value, er.RegistryEntryHider.USER.value), - (er.RegistryEntryHider.INTEGRATION.value, None), + (er.RegistryEntryHider.USER, er.RegistryEntryHider.USER), + (er.RegistryEntryHider.INTEGRATION, None), ), ) @pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 21d29736bd0..8f5b4a7d333 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -79,6 +79,7 @@ def test_get_or_create_updates_data(registry): device_id="mock-dev-id", disabled_by=er.RegistryEntryDisabler.HASS, entity_category=EntityCategory.CONFIG, + hidden_by=er.RegistryEntryHider.INTEGRATION, original_device_class="mock-device-class", original_icon="initial-original_icon", original_name="initial-original_name", @@ -97,6 +98,7 @@ def test_get_or_create_updates_data(registry): device_id="mock-dev-id", disabled_by=er.RegistryEntryDisabler.HASS, entity_category=EntityCategory.CONFIG, + hidden_by=er.RegistryEntryHider.INTEGRATION, icon=None, id=orig_entry.id, name=None, @@ -119,6 +121,7 @@ def test_get_or_create_updates_data(registry): device_id="new-mock-dev-id", disabled_by=er.RegistryEntryDisabler.USER, entity_category=None, + hidden_by=er.RegistryEntryHider.USER, original_device_class="new-mock-device-class", original_icon="updated-original_icon", original_name="updated-original_name", @@ -137,6 +140,7 @@ def test_get_or_create_updates_data(registry): device_id="new-mock-dev-id", disabled_by=er.RegistryEntryDisabler.HASS, # Should not be updated entity_category=EntityCategory.CONFIG, + hidden_by=er.RegistryEntryHider.INTEGRATION, # Should not be updated icon=None, id=orig_entry.id, name=None, @@ -191,6 +195,7 @@ async def test_loading_saving_data(hass, registry): device_id="mock-dev-id", disabled_by=er.RegistryEntryDisabler.HASS, entity_category=EntityCategory.CONFIG, + hidden_by=er.RegistryEntryHider.INTEGRATION, original_device_class="mock-device-class", original_icon="hass:original-icon", original_name="Original Name", @@ -231,6 +236,7 @@ async def test_loading_saving_data(hass, registry): assert new_entry2.disabled_by is er.RegistryEntryDisabler.HASS assert new_entry2.entity_category == "config" assert new_entry2.icon == "hass:user-icon" + assert new_entry2.hidden_by == er.RegistryEntryHider.INTEGRATION assert new_entry2.name == "User Name" assert new_entry2.options == {"light": {"minimum_brightness": 20}} assert new_entry2.original_device_class == "mock-device-class" @@ -261,8 +267,8 @@ def test_is_registered(registry): @pytest.mark.parametrize("load_registries", [False]) -async def test_loading_extra_values(hass, hass_storage): - """Test we load extra data from the registry.""" +async def test_filter_on_load(hass, hass_storage): + """Test we transform some data when loading from storage.""" hass_storage[er.STORAGE_KEY] = { "version": er.STORAGE_VERSION_MAJOR, "minor_version": 1, @@ -274,6 +280,7 @@ async def test_loading_extra_values(hass, hass_storage): "unique_id": "with-name", "name": "registry override", }, + # This entity's name should be None { "entity_id": "test.no_name", "platform": "super_platform", @@ -283,20 +290,22 @@ async def test_loading_extra_values(hass, hass_storage): "entity_id": "test.disabled_user", "platform": "super_platform", "unique_id": "disabled-user", - "disabled_by": er.RegistryEntryDisabler.USER, + "disabled_by": "user", # We store the string representation }, { "entity_id": "test.disabled_hass", "platform": "super_platform", "unique_id": "disabled-hass", - "disabled_by": er.RegistryEntryDisabler.HASS, + "disabled_by": "hass", # We store the string representation }, + # This entry should not be loaded because the entity_id is invalid { "entity_id": "test.invalid__entity", "platform": "super_platform", "unique_id": "invalid-hass", - "disabled_by": er.RegistryEntryDisabler.HASS, + "disabled_by": "hass", # We store the string representation }, + # This entry should have the entity_category reset to None { "entity_id": "test.system_entity", "platform": "super_platform", @@ -311,6 +320,13 @@ async def test_loading_extra_values(hass, hass_storage): registry = er.async_get(hass) assert len(registry.entities) == 5 + assert set(registry.entities.keys()) == { + "test.disabled_hass", + "test.disabled_user", + "test.named", + "test.no_name", + "test.system_entity", + } entry_with_name = registry.async_get_or_create( "test", "super_platform", "with-name" @@ -1221,7 +1237,7 @@ def test_entity_registry_items(): async def test_disabled_by_str_not_allowed(hass): - """Test we need to pass entity category type.""" + """Test we need to pass disabled by type.""" reg = er.async_get(hass) with pytest.raises(ValueError): @@ -1252,6 +1268,20 @@ async def test_entity_category_str_not_allowed(hass): ) +async def test_hidden_by_str_not_allowed(hass): + """Test we need to pass hidden by type.""" + reg = er.async_get(hass) + + with pytest.raises(ValueError): + reg.async_get_or_create( + "light", "hue", "1234", hidden_by=er.RegistryEntryHider.USER.value + ) + + entity_id = reg.async_get_or_create("light", "hue", "1234").entity_id + with pytest.raises(ValueError): + reg.async_update_entity(entity_id, hidden_by=er.RegistryEntryHider.USER.value) + + def test_migrate_entity_to_new_platform(hass, registry): """Test migrate_entity_to_new_platform.""" orig_config_entry = MockConfigEntry(domain="light") From 4c45cb5c5252eebbd00375677ff20fd8e62a2114 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 8 Jun 2022 18:29:46 -0400 Subject: [PATCH 1365/3516] Add UniFi Protect chime button/camera switch (#73195) --- .../components/unifiprotect/button.py | 15 +++++- .../components/unifiprotect/camera.py | 12 ++++- .../components/unifiprotect/switch.py | 8 ++++ tests/components/unifiprotect/test_camera.py | 47 ++++++++++++++++++- tests/components/unifiprotect/test_switch.py | 6 +-- 5 files changed, 81 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 3728b6b4224..9ed5ecc4967 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -43,6 +43,15 @@ ALL_DEVICE_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( ), ) +SENSOR_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( + ProtectButtonEntityDescription( + key="clear_tamper", + name="Clear Tamper", + icon="mdi:notification-clear-all", + ufp_press="clear_tamper", + ), +) + CHIME_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( ProtectButtonEntityDescription( key="play", @@ -69,7 +78,11 @@ async def async_setup_entry( data: ProtectData = hass.data[DOMAIN][entry.entry_id] entities: list[ProtectDeviceEntity] = async_all_device_entities( - data, ProtectButton, all_descs=ALL_DEVICE_BUTTONS, chime_descs=CHIME_BUTTONS + data, + ProtectButton, + all_descs=ALL_DEVICE_BUTTONS, + chime_descs=CHIME_BUTTONS, + sense_descs=SENSOR_BUTTONS, ) async_add_entities(entities) diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index ca076e490a2..8020d5e8aab 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -140,9 +140,9 @@ class ProtectCamera(ProtectDeviceEntity, Camera): def _async_update_device_from_protect(self) -> None: super()._async_update_device_from_protect() self.channel = self.device.channels[self.channel.id] + motion_enabled = self.device.recording_settings.enable_motion_detection self._attr_motion_detection_enabled = ( - self.device.state == StateType.CONNECTED - and self.device.feature_flags.has_motion_zones + motion_enabled if motion_enabled is not None else True ) self._attr_is_recording = ( self.device.state == StateType.CONNECTED and self.device.is_recording @@ -171,3 +171,11 @@ class ProtectCamera(ProtectDeviceEntity, Camera): async def stream_source(self) -> str | None: """Return the Stream Source.""" return self._stream_source + + async def async_enable_motion_detection(self) -> None: + """Call the job and enable motion detection.""" + await self.device.set_motion_detection(True) + + async def async_disable_motion_detection(self) -> None: + """Call the job and disable motion detection.""" + await self.device.set_motion_detection(False) diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 971c637a8c2..d8542da2f7f 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -133,6 +133,14 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_value="osd_settings.is_debug_enabled", ufp_set_method="set_osd_bitrate", ), + ProtectSwitchEntityDescription( + key="motion", + name="Detections: Motion", + icon="mdi:run-fast", + entity_category=EntityCategory.CONFIG, + ufp_value="recording_settings.enable_motion_detection", + ufp_set_method="set_motion_detection", + ), ProtectSwitchEntityDescription( key="smart_person", name="Detections: Person", diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index d7fc2a62325..538b3bf7652 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -48,6 +48,9 @@ async def camera_fixture( ): """Fixture for a single camera for testing the camera platform.""" + # disable pydantic validation so mocking can happen + ProtectCamera.__config__.validate_assignment = False + camera_obj = mock_camera.copy(deep=True) camera_obj._api = mock_entry.api camera_obj.channels[0]._api = mock_entry.api @@ -68,7 +71,9 @@ async def camera_fixture( assert_entity_counts(hass, Platform.CAMERA, 2, 1) - return (camera_obj, "camera.test_camera_high") + yield (camera_obj, "camera.test_camera_high") + + ProtectCamera.__config__.validate_assignment = True @pytest.fixture(name="camera_package") @@ -572,3 +577,43 @@ async def test_camera_ws_update_offline( state = hass.states.get(camera[1]) assert state and state.state == "idle" + + +async def test_camera_enable_motion( + hass: HomeAssistant, + mock_entry: MockEntityFixture, + camera: tuple[ProtectCamera, str], +): + """Tests generic entity update service.""" + + camera[0].__fields__["set_motion_detection"] = Mock() + camera[0].set_motion_detection = AsyncMock() + + await hass.services.async_call( + "camera", + "enable_motion_detection", + {ATTR_ENTITY_ID: camera[1]}, + blocking=True, + ) + + camera[0].set_motion_detection.assert_called_once_with(True) + + +async def test_camera_disable_motion( + hass: HomeAssistant, + mock_entry: MockEntityFixture, + camera: tuple[ProtectCamera, str], +): + """Tests generic entity update service.""" + + camera[0].__fields__["set_motion_detection"] = Mock() + camera[0].set_motion_detection = AsyncMock() + + await hass.services.async_call( + "camera", + "disable_motion_detection", + {ATTR_ENTITY_ID: camera[1]}, + blocking=True, + ) + + camera[0].set_motion_detection.assert_called_once_with(False) diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 7918ea0b6cf..bc0c8387c29 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -118,7 +118,7 @@ async def camera_fixture( await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - assert_entity_counts(hass, Platform.SWITCH, 12, 11) + assert_entity_counts(hass, Platform.SWITCH, 13, 12) yield camera_obj @@ -161,7 +161,7 @@ async def camera_none_fixture( await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - assert_entity_counts(hass, Platform.SWITCH, 5, 4) + assert_entity_counts(hass, Platform.SWITCH, 6, 5) yield camera_obj @@ -205,7 +205,7 @@ async def camera_privacy_fixture( await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - assert_entity_counts(hass, Platform.SWITCH, 6, 5) + assert_entity_counts(hass, Platform.SWITCH, 7, 6) yield camera_obj From 8af0d91676f118c400037c0354de3e5812cd55d4 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 8 Jun 2022 16:31:39 -0600 Subject: [PATCH 1366/3516] Bump regenmaschine to 2022.06.1 (#73250) --- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index a61283ea298..e9df60e4697 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.06.0"], + "requirements": ["regenmaschine==2022.06.1"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 866615632f3..a1a0b387dac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2065,7 +2065,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.06.0 +regenmaschine==2022.06.1 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd396ce2564..17c29461b31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1367,7 +1367,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rainmachine -regenmaschine==2022.06.0 +regenmaschine==2022.06.1 # homeassistant.components.renault renault-api==0.1.11 From 5c49d0a761c3ae52c50aed5cae57c396f95e0c01 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 8 Jun 2022 19:58:06 -0400 Subject: [PATCH 1367/3516] Bumps version of pyunifiprotect to 3.9.1 (#73252) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 52f69abea00..a27c0125da3 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.9.0", "unifi-discovery==1.1.3"], + "requirements": ["pyunifiprotect==3.9.1", "unifi-discovery==1.1.3"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index a1a0b387dac..c1cd7f05b64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1993,7 +1993,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.9.0 +pyunifiprotect==3.9.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 17c29461b31..9d380d8bf96 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1322,7 +1322,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.9.0 +pyunifiprotect==3.9.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 004ff8fb30b0dbf22abab9dbcd94304954431e27 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 8 Jun 2022 20:13:56 -0400 Subject: [PATCH 1368/3516] Overhaul UniFi Protect NVR Disk sensors (#73197) * Overhauls NVR Disk sensors * Updates from latest version of pyunifiprotect --- .../components/unifiprotect/binary_sensor.py | 47 ++++-- .../unifiprotect/fixtures/sample_nvr.json | 152 ++++++++++++++++++ .../unifiprotect/test_binary_sensor.py | 10 +- 3 files changed, 187 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 7613ffb8ebf..34c1119eb60 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -6,6 +6,7 @@ from dataclasses import dataclass import logging from pyunifiprotect.data import NVR, Camera, Event, Light, MountType, Sensor +from pyunifiprotect.data.nvr import UOSDisk from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -13,7 +14,6 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_MODEL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -131,7 +131,6 @@ DOORLOCK_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( DISK_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ProtectBinaryEntityDescription( key="disk_health", - name="Disk {index} Health", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -182,14 +181,18 @@ def _async_nvr_entities( ) -> list[ProtectDeviceEntity]: entities: list[ProtectDeviceEntity] = [] device = data.api.bootstrap.nvr - for index, _ in enumerate(device.system_info.storage.devices): + if device.system_info.ustorage is None: + return entities + + for disk in device.system_info.ustorage.disks: for description in DISK_SENSORS: - entities.append( - ProtectDiskBinarySensor(data, device, description, index=index) - ) + if not disk.has_disk: + continue + + entities.append(ProtectDiskBinarySensor(data, device, description, disk)) _LOGGER.debug( "Adding binary sensor entity %s", - (description.name or "{index}").format(index=index), + f"{disk.type} {disk.slot}", ) return entities @@ -216,6 +219,7 @@ class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity): class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity): """A UniFi Protect NVR Disk Binary Sensor.""" + _disk: UOSDisk entity_description: ProtectBinaryEntityDescription def __init__( @@ -223,26 +227,35 @@ class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity): data: ProtectData, device: NVR, description: ProtectBinaryEntityDescription, - index: int, + disk: UOSDisk, ) -> None: """Initialize the Binary Sensor.""" + self._disk = disk + # backwards compat with old unique IDs + index = self._disk.slot - 1 + description = copy(description) description.key = f"{description.key}_{index}" - description.name = (description.name or "{index}").format(index=index) - self._index = index + description.name = f"{disk.type} {disk.slot}" super().__init__(data, device, description) @callback def _async_update_device_from_protect(self) -> None: super()._async_update_device_from_protect() - disks = self.device.system_info.storage.devices - disk_available = len(disks) > self._index - self._attr_available = self._attr_available and disk_available - if disk_available: - disk = disks[self._index] - self._attr_is_on = not disk.healthy - self._attr_extra_state_attributes = {ATTR_MODEL: disk.model} + slot = self._disk.slot + self._attr_available = False + + if self.device.system_info.ustorage is None: + return + + for disk in self.device.system_info.ustorage.disks: + if disk.slot == slot: + self._disk = disk + self._attr_available = True + break + + self._attr_is_on = not self._disk.is_healthy class ProtectEventBinarySensor(EventThumbnailMixin, ProtectDeviceBinarySensor): diff --git a/tests/components/unifiprotect/fixtures/sample_nvr.json b/tests/components/unifiprotect/fixtures/sample_nvr.json index 728f92c3e32..507e75fec09 100644 --- a/tests/components/unifiprotect/fixtures/sample_nvr.json +++ b/tests/components/unifiprotect/fixtures/sample_nvr.json @@ -117,6 +117,158 @@ } ] }, + "ustorage": { + "disks": [ + { + "slot": 1, + "type": "HDD", + "model": "ST16000VE000-2L2103", + "serial": "ABCD1234", + "firmware": "EV02", + "rpm": 7200, + "ata": "ACS-4", + "sata": "SATA 3.3", + "action": "expanding", + "healthy": "good", + "state": "expanding", + "reason": null, + "temperature": 52, + "poweronhrs": 4242, + "life_span": null, + "bad_sector": 0, + "threshold": 10, + "progress": 21.390607518939174, + "estimate": 234395.73300748435 + }, + { + "slot": 2, + "type": "HDD", + "model": "ST16000VE000-2L2103", + "serial": "ABCD1234", + "firmware": "EV02", + "rpm": 7200, + "ata": "ACS-4", + "sata": "SATA 3.3", + "action": "expanding", + "healthy": "good", + "state": "expanding", + "reason": null, + "temperature": 52, + "poweronhrs": 4242, + "life_span": null, + "bad_sector": 0, + "threshold": 10, + "progress": 21.390607518939174, + "estimate": 234395.73300748435 + }, + { + "slot": 3, + "type": "HDD", + "model": "ST16000VE000-2L2103", + "serial": "ABCD1234", + "firmware": "EV02", + "rpm": 7200, + "ata": "ACS-4", + "sata": "SATA 3.3", + "action": "expanding", + "healthy": "good", + "state": "expanding", + "reason": null, + "temperature": 51, + "poweronhrs": 4242, + "life_span": null, + "bad_sector": 0, + "threshold": 10, + "progress": 21.390607518939174, + "estimate": 234395.73300748435 + }, + { + "slot": 4, + "type": "HDD", + "model": "ST16000VE000-2L2103", + "serial": "ABCD1234", + "firmware": "EV02", + "rpm": 7200, + "ata": "ACS-4", + "sata": "SATA 3.3", + "action": "expanding", + "healthy": "good", + "state": "expanding", + "reason": null, + "temperature": 50, + "poweronhrs": 2443, + "life_span": null, + "bad_sector": 0, + "threshold": 10, + "progress": 21.390607518939174, + "estimate": 234395.73300748435 + }, + { + "slot": 5, + "type": "HDD", + "model": "ST16000VE000-2L2103", + "serial": "ABCD1234", + "firmware": "EV02", + "rpm": 7200, + "ata": "ACS-4", + "sata": "SATA 3.3", + "action": "expanding", + "healthy": "good", + "state": "expanding", + "reason": null, + "temperature": 50, + "poweronhrs": 783, + "life_span": null, + "bad_sector": 0, + "threshold": 10, + "progress": 21.390607518939174, + "estimate": 234395.73300748435 + }, + { + "slot": 6, + "state": "nodisk" + }, + { + "slot": 7, + "type": "HDD", + "model": "ST16000VE002-3BR101", + "serial": "ABCD1234", + "firmware": "EV01", + "rpm": 7200, + "ata": "ACS-4", + "sata": "SATA 3.3", + "action": "expanding", + "healthy": "good", + "state": "expanding", + "reason": null, + "temperature": 45, + "poweronhrs": 18, + "life_span": null, + "bad_sector": 0, + "threshold": 10, + "progress": 21.390607518939174, + "estimate": 234395.73300748435 + } + ], + "space": [ + { + "device": "md3", + "total_bytes": 63713403555840, + "used_bytes": 57006577086464, + "action": "expanding", + "progress": 21.390607518939174, + "estimate": 234395.73300748435 + }, + { + "device": "md0", + "total_bytes": 0, + "used_bytes": 0, + "action": "syncing", + "progress": 0, + "estimate": null + } + ] + }, "tmpfs": { "available": 934204, "total": 1048576, diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 88b42d36994..834f8634ee1 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -71,7 +71,7 @@ async def camera_fixture( await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - assert_entity_counts(hass, Platform.BINARY_SENSOR, 3, 3) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 9, 9) yield camera_obj @@ -103,7 +103,7 @@ async def light_fixture( await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - assert_entity_counts(hass, Platform.BINARY_SENSOR, 2, 2) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 8, 8) yield light_obj @@ -138,7 +138,7 @@ async def camera_none_fixture( await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - assert_entity_counts(hass, Platform.BINARY_SENSOR, 2, 2) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 8, 8) yield camera_obj @@ -179,7 +179,7 @@ async def sensor_fixture( await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - assert_entity_counts(hass, Platform.BINARY_SENSOR, 4, 4) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 10, 10) yield sensor_obj @@ -215,7 +215,7 @@ async def sensor_none_fixture( await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - assert_entity_counts(hass, Platform.BINARY_SENSOR, 4, 4) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 10, 10) yield sensor_obj From 8f8c1348ba5d3ba777b1325a3b4b3ba3369c8dfb Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 9 Jun 2022 00:23:40 +0000 Subject: [PATCH 1369/3516] [ci skip] Translation update --- homeassistant/components/google/translations/ca.json | 3 +++ homeassistant/components/google/translations/en.json | 2 +- homeassistant/components/google/translations/fr.json | 3 +++ homeassistant/components/google/translations/hu.json | 3 +++ homeassistant/components/google/translations/pl.json | 3 +++ .../components/google/translations/zh-Hant.json | 3 +++ homeassistant/components/radiotherm/translations/ca.json | 9 +++++++++ .../components/sensibo/translations/sensor.hu.json | 8 ++++++++ 8 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/sensibo/translations/sensor.hu.json diff --git a/homeassistant/components/google/translations/ca.json b/homeassistant/components/google/translations/ca.json index 2c9190e4bfd..27c9a11f92b 100644 --- a/homeassistant/components/google/translations/ca.json +++ b/homeassistant/components/google/translations/ca.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "Segueix les [instruccions]({more_info_url}) de [la pantalla de consentiment OAuth]({oauth_consent_url}) perqu\u00e8 Home Assistant tingui acc\u00e9s al teu calendari de Google. Tamb\u00e9 has de crear les credencials d'aplicaci\u00f3 enlla\u00e7ades al calendari:\n 1. V\u00e9s a [Credencials]({oauth_creds_url}) i fes clic a **Crear credencials**.\n 2 A la llista desplegable, selecciona **ID de client OAuth**.\n 3. Selecciona **Dispositius TV i d'entrada limitada** al tipus d'aplicaci\u00f3.\n \n " + }, "config": { "abort": { "already_configured": "El compte ja est\u00e0 configurat", diff --git a/homeassistant/components/google/translations/en.json b/homeassistant/components/google/translations/en.json index 54936b0d81c..1bebde4f63a 100644 --- a/homeassistant/components/google/translations/en.json +++ b/homeassistant/components/google/translations/en.json @@ -1,6 +1,6 @@ { "application_credentials": { - "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Calendar. You also need to create Application Credentials linked to your Calendar:\n1. Go to [Credentials]({oauth_creds_url}) page and click **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **TV and Limited Input devices** for the Application Type.\n\n" + "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Calendar. You also need to create Application Credentials linked to your Calendar:\n1. Go to [Credentials]({oauth_creds_url}) and click **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **TV and Limited Input devices** for the Application Type.\n\n" }, "config": { "abort": { diff --git a/homeassistant/components/google/translations/fr.json b/homeassistant/components/google/translations/fr.json index 06903bdff07..ed60ccb1c1b 100644 --- a/homeassistant/components/google/translations/fr.json +++ b/homeassistant/components/google/translations/fr.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "Suivez les [instructions]({more_info_url}) de l'[\u00e9cran d'autorisation OAuth]({oauth_consent_url}) pour permettre \u00e0 Home Assistant d'acc\u00e9der \u00e0 votre agenda Google. Vous devez \u00e9galement cr\u00e9er des informations d'identification d'application li\u00e9es \u00e0 votre agenda\u00a0:\n1. Rendez-vous sur [Informations d'identification]({oauth_creds_url}) et cliquez sur **Cr\u00e9er des informations d'identification**.\n2. Dans la liste d\u00e9roulante, s\u00e9lectionnez **ID client OAuth**.\n3. S\u00e9lectionnez **TV et p\u00e9riph\u00e9riques d'entr\u00e9e limit\u00e9s** comme type d'application.\n\n" + }, "config": { "abort": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", diff --git a/homeassistant/components/google/translations/hu.json b/homeassistant/components/google/translations/hu.json index 66ea71c72ec..6a5c7d2d68c 100644 --- a/homeassistant/components/google/translations/hu.json +++ b/homeassistant/components/google/translations/hu.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "Az [OAuth beleegyez\u00e9si k\u00e9perny\u0151]({oauth_consent_url}) az [utas\u00edt\u00e1sok]({more_info_url}) k\u00f6vet\u00e9s\u00e9vel hozz\u00e1f\u00e9r\u00e9st biztos\u00edthat a Home Assistant-nak a Google Napt\u00e1rhoz. L\u00e9tre kell hoznia a napt\u00e1rhoz csatolt alkalmaz\u00e1s-hiteles\u00edt\u0151 adatokat is:\n1. Nyissa meg a [Credentials]({oauth_creds_url}) webhelyet, \u00e9s kattintson a **Hiteles\u00edt\u0151 adatok l\u00e9trehoz\u00e1sa**-ra.\n1. A leg\u00f6rd\u00fcl\u0151 list\u00e1b\u00f3l v\u00e1lassza a **OAuth \u00fcgyf\u00e9lazonos\u00edt\u00f3**-t.\n1. V\u00e1lassza a **TV \u00e9s a korl\u00e1tozott beviteli eszk\u00f6z\u00f6k** lehet\u0151s\u00e9get az alkalmaz\u00e1st\u00edpushoz." + }, "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", diff --git a/homeassistant/components/google/translations/pl.json b/homeassistant/components/google/translations/pl.json index fff2a20ee39..8013e775e62 100644 --- a/homeassistant/components/google/translations/pl.json +++ b/homeassistant/components/google/translations/pl.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "Post\u0119puj zgodnie z [instrukcjami]( {more_info_url} ) na [ekran akceptacji OAuth]( {oauth_consent_url} ), aby przyzna\u0107 Home Assistantowi dost\u0119p do Twojego Kalendarza Google. Musisz r\u00f3wnie\u017c utworzy\u0107 po\u015bwiadczenia aplikacji po\u0142\u0105czone z Twoim kalendarzem:\n1. Przejd\u017a do [Po\u015bwiadczenia]( {oauth_creds_url} ) i kliknij **Utw\u00f3rz po\u015bwiadczenia**.\n2. Z listy rozwijanej wybierz **ID klienta OAuth**.\n3. Wybierz **TV i urz\u0105dzenia z ograniczonym wej\u015bciem** jako typ aplikacji.\n\n" + }, "config": { "abort": { "already_configured": "Konto jest ju\u017c skonfigurowane", diff --git a/homeassistant/components/google/translations/zh-Hant.json b/homeassistant/components/google/translations/zh-Hant.json index 988c6629af7..7b83154db1a 100644 --- a/homeassistant/components/google/translations/zh-Hant.json +++ b/homeassistant/components/google/translations/zh-Hant.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "\u8ddf\u96a8[\u8aaa\u660e]({more_info_url})\u4ee5\u8a2d\u5b9a\u81f3 [OAuth \u540c\u610f\u756b\u9762]({oauth_consent_url})\u3001\u4f9b Home Assistant \u5b58\u53d6\u60a8\u7684 Google \u65e5\u66c6\u3002\u540c\u6642\u9700\u8981\u65b0\u589e\u9023\u7d50\u81f3\u65e5\u66c6\u7684\u61c9\u7528\u7a0b\u5f0f\u6191\u8b49\uff1a\n1. \u700f\u89bd\u81f3 [\u6191\u8b49]({oauth_creds_url}) \u9801\u9762\u4e26\u9ede\u9078 **\u5efa\u7acb\u6191\u8b49**\u3002\n1. \u7531\u4e0b\u62c9\u9078\u55ae\u4e2d\u9078\u64c7 **OAuth \u7528\u6236\u7aef ID**\u3002\n1. \u61c9\u7528\u7a0b\u5f0f\u985e\u578b\u5247\u9078\u64c7 **\u96fb\u8996\u548c\u53d7\u9650\u5236\u7684\u8f38\u5165\u88dd\u7f6e**\u3002\n\n" + }, "config": { "abort": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", diff --git a/homeassistant/components/radiotherm/translations/ca.json b/homeassistant/components/radiotherm/translations/ca.json index 1008a0e4988..d1249ddd23f 100644 --- a/homeassistant/components/radiotherm/translations/ca.json +++ b/homeassistant/components/radiotherm/translations/ca.json @@ -18,5 +18,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "Configura bloqueig permanent quan s'ajusti la temperatura." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.hu.json b/homeassistant/components/sensibo/translations/sensor.hu.json new file mode 100644 index 00000000000..38a372f0f78 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.hu.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "Norm\u00e1l", + "s": "\u00c9rz\u00e9keny" + } + } +} \ No newline at end of file From 8b735ffabec49379c0c28a52e1f701b6b0b4fe49 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 9 Jun 2022 03:30:13 +0200 Subject: [PATCH 1370/3516] Fix handling of connection error during Synology DSM setup (#73248) * dont reload on conection error during setup * also fetch API errors during update --- .../components/synology_dsm/common.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index e27c7475251..2ca9cbf3ccf 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Callable -from datetime import timedelta import logging from synology_dsm import SynologyDSM @@ -98,7 +97,7 @@ class SynoApi: self._async_setup_api_requests() await self._hass.async_add_executor_job(self._fetch_device_configuration) - await self.async_update() + await self.async_update(first_setup=True) @callback def subscribe(self, api_key: str, unique_id: str) -> Callable[[], None]: @@ -251,7 +250,7 @@ class SynoApi: # ignore API errors during logout pass - async def async_update(self, now: timedelta | None = None) -> None: + async def async_update(self, first_setup: bool = False) -> None: """Update function for updating API information.""" LOGGER.debug("Start data update for '%s'", self._entry.unique_id) self._async_setup_api_requests() @@ -259,14 +258,22 @@ class SynoApi: await self._hass.async_add_executor_job( self.dsm.update, self._with_information ) - except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: - LOGGER.warning( - "Connection error during update, fallback by reloading the entry" - ) + except ( + SynologyDSMLoginFailedException, + SynologyDSMRequestException, + SynologyDSMAPIErrorException, + ) as err: LOGGER.debug( "Connection error during update of '%s' with exception: %s", self._entry.unique_id, err, ) + + if first_setup: + raise err + + LOGGER.warning( + "Connection error during update, fallback by reloading the entry" + ) await self._hass.config_entries.async_reload(self._entry.entry_id) return From d6e7a3e537564c6bfc52580e2271491f4f7dc698 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Thu, 9 Jun 2022 14:46:45 +1000 Subject: [PATCH 1371/3516] Add powerview advanced features (#73061) Co-authored-by: J. Nick Koston --- .coveragerc | 3 +- .../hunterdouglas_powerview/__init__.py | 2 +- .../hunterdouglas_powerview/button.py | 124 ++++++++++++++++++ .../hunterdouglas_powerview/cover.py | 3 +- .../hunterdouglas_powerview/entity.py | 5 + 5 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/hunterdouglas_powerview/button.py diff --git a/.coveragerc b/.coveragerc index 3db90953f74..44b205746df 100644 --- a/.coveragerc +++ b/.coveragerc @@ -505,9 +505,10 @@ omit = homeassistant/components/huawei_lte/switch.py homeassistant/components/hue/light.py homeassistant/components/hunterdouglas_powerview/__init__.py + homeassistant/components/hunterdouglas_powerview/button.py homeassistant/components/hunterdouglas_powerview/coordinator.py - homeassistant/components/hunterdouglas_powerview/diagnostics.py homeassistant/components/hunterdouglas_powerview/cover.py + homeassistant/components/hunterdouglas_powerview/diagnostics.py homeassistant/components/hunterdouglas_powerview/entity.py homeassistant/components/hunterdouglas_powerview/scene.py homeassistant/components/hunterdouglas_powerview/sensor.py diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 88aa5214c9b..fe039964e5d 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -56,7 +56,7 @@ PARALLEL_UPDATES = 1 CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) -PLATFORMS = [Platform.COVER, Platform.SCENE, Platform.SENSOR] +PLATFORMS = [Platform.BUTTON, Platform.COVER, Platform.SCENE, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hunterdouglas_powerview/button.py b/homeassistant/components/hunterdouglas_powerview/button.py new file mode 100644 index 00000000000..131ef279a20 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/button.py @@ -0,0 +1,124 @@ +"""Buttons for Hunter Douglas Powerview advanced features.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any, Final + +from aiopvapi.resources.shade import BaseShade, factory as PvShade + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ( + COORDINATOR, + DEVICE_INFO, + DOMAIN, + PV_API, + PV_ROOM_DATA, + PV_SHADE_DATA, + ROOM_ID_IN_SHADE, + ROOM_NAME_UNICODE, +) +from .coordinator import PowerviewShadeUpdateCoordinator +from .entity import ShadeEntity + + +@dataclass +class PowerviewButtonDescriptionMixin: + """Mixin to describe a Button entity.""" + + press_action: Callable[[BaseShade], Any] + + +@dataclass +class PowerviewButtonDescription( + ButtonEntityDescription, PowerviewButtonDescriptionMixin +): + """Class to describe a Button entity.""" + + +BUTTONS: Final = [ + PowerviewButtonDescription( + key="calibrate", + name="Calibrate", + icon="mdi:swap-vertical-circle-outline", + device_class=ButtonDeviceClass.UPDATE, + entity_category=EntityCategory.DIAGNOSTIC, + press_action=lambda shade: shade.calibrate(), + ), + PowerviewButtonDescription( + key="identify", + name="Identify", + icon="mdi:crosshairs-question", + device_class=ButtonDeviceClass.UPDATE, + entity_category=EntityCategory.DIAGNOSTIC, + press_action=lambda shade: shade.jog(), + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the hunter douglas advanced feature buttons.""" + + pv_data = hass.data[DOMAIN][entry.entry_id] + room_data: dict[str | int, Any] = pv_data[PV_ROOM_DATA] + shade_data = pv_data[PV_SHADE_DATA] + pv_request = pv_data[PV_API] + coordinator: PowerviewShadeUpdateCoordinator = pv_data[COORDINATOR] + device_info: dict[str, Any] = pv_data[DEVICE_INFO] + + entities: list[ButtonEntity] = [] + for raw_shade in shade_data.values(): + shade: BaseShade = PvShade(raw_shade, pv_request) + name_before_refresh = shade.name + room_id = shade.raw_data.get(ROOM_ID_IN_SHADE) + room_name = room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "") + + for description in BUTTONS: + entities.append( + PowerviewButton( + coordinator, + device_info, + room_name, + shade, + name_before_refresh, + description, + ) + ) + + async_add_entities(entities) + + +class PowerviewButton(ShadeEntity, ButtonEntity): + """Representation of an advanced feature button.""" + + def __init__( + self, + coordinator: PowerviewShadeUpdateCoordinator, + device_info: dict[str, Any], + room_name: str, + shade: BaseShade, + name: str, + description: PowerviewButtonDescription, + ) -> None: + """Initialize the button entity.""" + super().__init__(coordinator, device_info, room_name, shade, name) + self.entity_description: PowerviewButtonDescription = description + self._attr_name = f"{self._shade_name} {description.name}" + self._attr_unique_id = f"{self._attr_unique_id}_{description.key}" + + async def async_press(self) -> None: + """Handle the button press.""" + await self.entity_description.press_action(self._shade) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 565bac6a5c8..c3061c75301 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -323,8 +323,7 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): async def _async_force_refresh_state(self) -> None: """Refresh the cover state and force the device cache to be bypassed.""" - await self._shade.refresh() - self._async_update_shade_data(self._shade.raw_data) + await self.async_update() self.async_write_ha_state() async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py index 04885fa576e..7814ba9cb12 100644 --- a/homeassistant/components/hunterdouglas_powerview/entity.py +++ b/homeassistant/components/hunterdouglas_powerview/entity.py @@ -117,3 +117,8 @@ class ShadeEntity(HDEntity): device_info[ATTR_SW_VERSION] = sw_version return device_info + + async def async_update(self) -> None: + """Refresh shade position.""" + await self._shade.refresh() + self.data.update_shade_positions(self._shade.raw_data) From 86723ea02b1a024dbc2d40671ac9e35f4775ea56 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Jun 2022 09:41:55 +0200 Subject: [PATCH 1372/3516] Bump actions/setup-python from 3.1.2 to 4.0.0 (#73265) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 6 +++--- .github/workflows/ci.yaml | 12 ++++++------ .github/workflows/translations.yaml | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 48c0782dafa..664f9b3910e 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -29,7 +29,7 @@ jobs: fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -70,7 +70,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -104,7 +104,7 @@ jobs: - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 502006d8d2c..fb46ab63202 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -155,7 +155,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Generate partial Python venv restore key @@ -235,7 +235,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -285,7 +285,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -336,7 +336,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -378,7 +378,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -525,7 +525,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.0.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 716ae870b50..0d3ddc4ca18 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v3.1.2 + uses: actions/setup-python@v4.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} From a7398b8a733239ea1d27496b9a9905b5445a7404 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 9 Jun 2022 12:06:59 +0300 Subject: [PATCH 1373/3516] Remove deprecated yaml and code cleanup for `nfandroidtv` (#73227) --- .../components/nfandroidtv/__init__.py | 45 +++++-------- .../components/nfandroidtv/config_flow.py | 34 +++------- homeassistant/components/nfandroidtv/const.py | 2 + .../components/nfandroidtv/notify.py | 63 ++++--------------- .../nfandroidtv/test_config_flow.py | 41 +----------- 5 files changed, 38 insertions(+), 147 deletions(-) diff --git a/homeassistant/components/nfandroidtv/__init__.py b/homeassistant/components/nfandroidtv/__init__.py index 458015c5bb6..38622fc0060 100644 --- a/homeassistant/components/nfandroidtv/__init__.py +++ b/homeassistant/components/nfandroidtv/__init__.py @@ -1,58 +1,46 @@ """The NFAndroidTV integration.""" from notifications_android_tv.notifications import ConnectError, Notifications -from homeassistant.components.notify import DOMAIN as NOTIFY -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PLATFORM, Platform +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import DATA_HASS_CONFIG, DOMAIN PLATFORMS = [Platform.NOTIFY] +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the NFAndroidTV component.""" - hass.data.setdefault(DOMAIN, {}) - # Iterate all entries for notify to only get nfandroidtv - if NOTIFY in config: - for entry in config[NOTIFY]: - if entry[CONF_PLATFORM] == DOMAIN: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=entry - ) - ) + hass.data[DATA_HASS_CONFIG] = config return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up NFAndroidTV from a config entry.""" - host = entry.data[CONF_HOST] - name = entry.data[CONF_NAME] - try: - await hass.async_add_executor_job(Notifications, host) + await hass.async_add_executor_job(Notifications, entry.data[CONF_HOST]) except ConnectError as ex: - raise ConfigEntryNotReady("Failed to connect") from ex + raise ConfigEntryNotReady( + f"Failed to connect to host: {entry.data[CONF_HOST]}" + ) from ex hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - CONF_HOST: host, - CONF_NAME: name, - } hass.async_create_task( discovery.async_load_platform( hass, Platform.NOTIFY, DOMAIN, - hass.data[DOMAIN][entry.entry_id], - hass.data[DOMAIN], + dict(entry.data), + hass.data[DATA_HASS_CONFIG], ) ) @@ -61,9 +49,4 @@ 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) - - if unload_ok: - hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/nfandroidtv/config_flow.py b/homeassistant/components/nfandroidtv/config_flow.py index defa4467f3a..88eebe1b4d4 100644 --- a/homeassistant/components/nfandroidtv/config_flow.py +++ b/homeassistant/components/nfandroidtv/config_flow.py @@ -26,46 +26,28 @@ class NFAndroidTVFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - host = user_input[CONF_HOST] - name = user_input[CONF_NAME] - await self.async_set_unique_id(host) - self._abort_if_unique_id_configured() - error = await self._async_try_connect(host) - if error is None: + self._async_abort_entries_match( + {CONF_HOST: user_input[CONF_HOST], CONF_NAME: user_input[CONF_NAME]} + ) + if not (error := await self._async_try_connect(user_input[CONF_HOST])): return self.async_create_entry( - title=name, - data={CONF_HOST: host, CONF_NAME: name}, + title=user_input[CONF_NAME], + data=user_input, ) errors["base"] = error - user_input = user_input or {} return self.async_show_form( step_id="user", data_schema=vol.Schema( { - vol.Required(CONF_HOST, default=user_input.get(CONF_HOST)): str, - vol.Required( - CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) - ): str, + vol.Required(CONF_HOST): str, + vol.Required(CONF_NAME, default=DEFAULT_NAME): str, } ), errors=errors, ) - 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]: - _LOGGER.warning( - "Already configured. This yaml configuration has already been imported. Please remove it" - ) - return self.async_abort(reason="already_configured") - if CONF_NAME not in import_config: - import_config[CONF_NAME] = f"{DEFAULT_NAME} {import_config[CONF_HOST]}" - - return await self.async_step_user(import_config) - async def _async_try_connect(self, host: str) -> str | None: """Try connecting to Android TV / Fire TV.""" try: diff --git a/homeassistant/components/nfandroidtv/const.py b/homeassistant/components/nfandroidtv/const.py index 12449a9b046..4d4a7c82ecb 100644 --- a/homeassistant/components/nfandroidtv/const.py +++ b/homeassistant/components/nfandroidtv/const.py @@ -7,6 +7,8 @@ CONF_TRANSPARENCY = "transparency" CONF_COLOR = "color" CONF_INTERRUPT = "interrupt" +DATA_HASS_CONFIG = "nfandroid_hass_config" + DEFAULT_NAME = "Android TV / Fire TV" DEFAULT_TIMEOUT = 5 diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index b5e4962e9be..c70272d3835 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -14,10 +14,9 @@ from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, BaseNotificationService, ) -from homeassistant.const import CONF_HOST, CONF_TIMEOUT +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -43,53 +42,21 @@ from .const import ( ATTR_INTERRUPT, ATTR_POSITION, ATTR_TRANSPARENCY, - CONF_COLOR, - CONF_DURATION, - CONF_FONTSIZE, - CONF_INTERRUPT, - CONF_POSITION, - CONF_TRANSPARENCY, DEFAULT_TIMEOUT, ) _LOGGER = logging.getLogger(__name__) -# Deprecated in Home Assistant 2021.8 -PLATFORM_SCHEMA = cv.deprecated( - vol.All( - PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_DURATION): vol.Coerce(int), - vol.Optional(CONF_FONTSIZE): vol.In(Notifications.FONTSIZES.keys()), - vol.Optional(CONF_POSITION): vol.In(Notifications.POSITIONS.keys()), - vol.Optional(CONF_TRANSPARENCY): vol.In( - Notifications.TRANSPARENCIES.keys() - ), - vol.Optional(CONF_COLOR): vol.In(Notifications.BKG_COLORS.keys()), - vol.Optional(CONF_TIMEOUT): vol.Coerce(int), - vol.Optional(CONF_INTERRUPT): cv.boolean, - } - ), - ) -) - async def async_get_service( hass: HomeAssistant, config: ConfigType, discovery_info: DiscoveryInfoType | None = None, -) -> NFAndroidTVNotificationService: +) -> NFAndroidTVNotificationService | None: """Get the NFAndroidTV notification service.""" - if discovery_info is not None: - notify = await hass.async_add_executor_job( - Notifications, discovery_info[CONF_HOST] - ) - return NFAndroidTVNotificationService( - notify, - hass.config.is_allowed_path, - ) - notify = await hass.async_add_executor_job(Notifications, config.get(CONF_HOST)) + if discovery_info is None: + return None + notify = await hass.async_add_executor_job(Notifications, discovery_info[CONF_HOST]) return NFAndroidTVNotificationService( notify, hass.config.is_allowed_path, @@ -128,21 +95,21 @@ class NFAndroidTVNotificationService(BaseNotificationService): ) except ValueError: _LOGGER.warning( - "Invalid duration-value: %s", str(data.get(ATTR_DURATION)) + "Invalid duration-value: %s", data.get(ATTR_DURATION) ) if ATTR_FONTSIZE in data: if data.get(ATTR_FONTSIZE) in Notifications.FONTSIZES: fontsize = data.get(ATTR_FONTSIZE) else: _LOGGER.warning( - "Invalid fontsize-value: %s", str(data.get(ATTR_FONTSIZE)) + "Invalid fontsize-value: %s", data.get(ATTR_FONTSIZE) ) if ATTR_POSITION in data: if data.get(ATTR_POSITION) in Notifications.POSITIONS: position = data.get(ATTR_POSITION) else: _LOGGER.warning( - "Invalid position-value: %s", str(data.get(ATTR_POSITION)) + "Invalid position-value: %s", data.get(ATTR_POSITION) ) if ATTR_TRANSPARENCY in data: if data.get(ATTR_TRANSPARENCY) in Notifications.TRANSPARENCIES: @@ -150,24 +117,21 @@ class NFAndroidTVNotificationService(BaseNotificationService): else: _LOGGER.warning( "Invalid transparency-value: %s", - str(data.get(ATTR_TRANSPARENCY)), + data.get(ATTR_TRANSPARENCY), ) if ATTR_COLOR in data: if data.get(ATTR_COLOR) in Notifications.BKG_COLORS: bkgcolor = data.get(ATTR_COLOR) else: - _LOGGER.warning( - "Invalid color-value: %s", str(data.get(ATTR_COLOR)) - ) + _LOGGER.warning("Invalid color-value: %s", data.get(ATTR_COLOR)) if ATTR_INTERRUPT in data: try: interrupt = cv.boolean(data.get(ATTR_INTERRUPT)) except vol.Invalid: _LOGGER.warning( - "Invalid interrupt-value: %s", str(data.get(ATTR_INTERRUPT)) + "Invalid interrupt-value: %s", data.get(ATTR_INTERRUPT) ) - imagedata = data.get(ATTR_IMAGE) if data else None - if imagedata is not None: + if imagedata := data.get(ATTR_IMAGE): image_file = self.load_file( url=imagedata.get(ATTR_IMAGE_URL), local_path=imagedata.get(ATTR_IMAGE_PATH), @@ -175,8 +139,7 @@ class NFAndroidTVNotificationService(BaseNotificationService): password=imagedata.get(ATTR_IMAGE_PASSWORD), auth=imagedata.get(ATTR_IMAGE_AUTH), ) - icondata = data.get(ATTR_ICON) if data else None - if icondata is not None: + if icondata := data.get(ATTR_ICON): icon = self.load_file( url=icondata.get(ATTR_ICON_URL), local_path=icondata.get(ATTR_ICON_PATH), diff --git a/tests/components/nfandroidtv/test_config_flow.py b/tests/components/nfandroidtv/test_config_flow.py index b16b053c70f..a8b7b5fef53 100644 --- a/tests/components/nfandroidtv/test_config_flow.py +++ b/tests/components/nfandroidtv/test_config_flow.py @@ -4,8 +4,7 @@ from unittest.mock import patch from notifications_android_tv.notifications import ConnectError from homeassistant import config_entries, data_entry_flow -from homeassistant.components.nfandroidtv.const import DEFAULT_NAME, DOMAIN -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.components.nfandroidtv.const import DOMAIN from . import ( CONF_CONFIG_FLOW, @@ -95,41 +94,3 @@ async def test_flow_user_unknown_error(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} - - -async def test_flow_import(hass): - """Test an import flow.""" - mocked_tv = await _create_mocked_tv(True) - with _patch_config_flow_tv(mocked_tv), _patch_setup(): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=CONF_CONFIG_FLOW, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"] == CONF_DATA - - with _patch_config_flow_tv(mocked_tv), _patch_setup(): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=CONF_CONFIG_FLOW, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def test_flow_import_missing_optional(hass): - """Test an import flow with missing options.""" - mocked_tv = await _create_mocked_tv(True) - with _patch_config_flow_tv(mocked_tv), _patch_setup(): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_HOST: HOST}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"] == {CONF_HOST: HOST, CONF_NAME: f"{DEFAULT_NAME} {HOST}"} From 69050d59426a18758858015644507dac077b7233 Mon Sep 17 00:00:00 2001 From: b3nj1 Date: Thu, 9 Jun 2022 02:14:18 -0700 Subject: [PATCH 1374/3516] Add Vesync voltage sensor, and yearly, weekly, montly energy sensors (#72570) --- homeassistant/components/vesync/common.py | 2 +- homeassistant/components/vesync/sensor.py | 43 ++++++++++++++++++++++- homeassistant/components/vesync/switch.py | 12 ------- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index acee8e20961..e11897ea9ae 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -30,7 +30,7 @@ async def async_process_devices(hass, manager): if manager.outlets: devices[VS_SWITCHES].extend(manager.outlets) - # Expose outlets' power & energy usage as separate sensors + # Expose outlets' voltage, power & energy usage as separate sensors devices[VS_SENSORS].extend(manager.outlets) _LOGGER.info("%d VeSync outlets found", len(manager.outlets)) diff --git a/homeassistant/components/vesync/sensor.py b/homeassistant/components/vesync/sensor.py index 6e0c09b2f60..2da6d8ea6b7 100644 --- a/homeassistant/components/vesync/sensor.py +++ b/homeassistant/components/vesync/sensor.py @@ -1,4 +1,4 @@ -"""Support for power & energy sensors for VeSync outlets.""" +"""Support for voltage, power & energy sensors for VeSync outlets.""" from __future__ import annotations from collections.abc import Callable @@ -18,6 +18,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT, @@ -121,6 +122,46 @@ SENSORS: tuple[VeSyncSensorEntityDescription, ...] = ( update_fn=update_energy, exists_fn=lambda device: ha_dev_type(device) == "outlet", ), + VeSyncSensorEntityDescription( + key="energy-weekly", + name="energy use weekly", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda device: device.weekly_energy_total, + update_fn=update_energy, + exists_fn=lambda device: ha_dev_type(device) == "outlet", + ), + VeSyncSensorEntityDescription( + key="energy-monthly", + name="energy use monthly", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda device: device.monthly_energy_total, + update_fn=update_energy, + exists_fn=lambda device: ha_dev_type(device) == "outlet", + ), + VeSyncSensorEntityDescription( + key="energy-yearly", + name="energy use yearly", + device_class=SensorDeviceClass.ENERGY, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda device: device.yearly_energy_total, + update_fn=update_energy, + exists_fn=lambda device: ha_dev_type(device) == "outlet", + ), + VeSyncSensorEntityDescription( + key="voltage", + name="current voltage", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda device: device.details["voltage"], + update_fn=update_energy, + exists_fn=lambda device: ha_dev_type(device) == "outlet", + ), ) diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index e5fd4c829fe..68b10e40bcb 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -66,18 +66,6 @@ class VeSyncSwitchHA(VeSyncBaseSwitch, SwitchEntity): super().__init__(plug) self.smartplug = plug - @property - def extra_state_attributes(self): - """Return the state attributes of the device.""" - if not hasattr(self.smartplug, "weekly_energy_total"): - return {} - return { - "voltage": self.smartplug.voltage, - "weekly_energy_total": self.smartplug.weekly_energy_total, - "monthly_energy_total": self.smartplug.monthly_energy_total, - "yearly_energy_total": self.smartplug.yearly_energy_total, - } - def update(self): """Update outlet details and energy usage.""" self.smartplug.update() From b3677cdff69cc8b0a60ba757d788a33278eb5e7c Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Thu, 9 Jun 2022 12:26:20 +0200 Subject: [PATCH 1375/3516] Bump velbus-aio version to 2022.6.1 (#73261) --- 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 f759eea0a34..e627412a00e 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==2022.5.1"], + "requirements": ["velbus-aio==2022.6.1"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "dependencies": ["usb"], diff --git a/requirements_all.txt b/requirements_all.txt index c1cd7f05b64..7aa8f60f961 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2384,7 +2384,7 @@ vallox-websocket-api==2.11.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.5.1 +velbus-aio==2022.6.1 # homeassistant.components.venstar venstarcolortouch==0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9d380d8bf96..f68ac122bd2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1572,7 +1572,7 @@ vallox-websocket-api==2.11.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.5.1 +velbus-aio==2022.6.1 # homeassistant.components.venstar venstarcolortouch==0.15 From 1dc8c085e9ced7147e11c4e674907d1a5528791d Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 9 Jun 2022 13:48:39 +0200 Subject: [PATCH 1376/3516] Improve Netgear logging (#73274) * improve logging * fix black * invert checks --- homeassistant/components/netgear/sensor.py | 44 ++++++++++++++-------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 9dec1ab3390..a1cf134beda 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -5,6 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import date, datetime from decimal import Decimal +import logging from homeassistant.components.sensor import ( RestoreSensor, @@ -34,6 +35,8 @@ from .const import ( ) from .router import NetgearDeviceEntity, NetgearRouter, NetgearRouterEntity +_LOGGER = logging.getLogger(__name__) + SENSOR_TYPES = { "type": SensorEntityDescription( key="type", @@ -114,7 +117,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:upload", index=0, - value=lambda data: data[0] if data is not None else None, + value=lambda data: data[0], ), NetgearSensorEntityDescription( key="NewWeekUpload", @@ -123,7 +126,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:upload", index=1, - value=lambda data: data[1] if data is not None else None, + value=lambda data: data[1], ), NetgearSensorEntityDescription( key="NewWeekDownload", @@ -132,7 +135,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:download", index=0, - value=lambda data: data[0] if data is not None else None, + value=lambda data: data[0], ), NetgearSensorEntityDescription( key="NewWeekDownload", @@ -141,7 +144,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:download", index=1, - value=lambda data: data[1] if data is not None else None, + value=lambda data: data[1], ), NetgearSensorEntityDescription( key="NewMonthUpload", @@ -150,7 +153,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:upload", index=0, - value=lambda data: data[0] if data is not None else None, + value=lambda data: data[0], ), NetgearSensorEntityDescription( key="NewMonthUpload", @@ -159,7 +162,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:upload", index=1, - value=lambda data: data[1] if data is not None else None, + value=lambda data: data[1], ), NetgearSensorEntityDescription( key="NewMonthDownload", @@ -168,7 +171,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:download", index=0, - value=lambda data: data[0] if data is not None else None, + value=lambda data: data[0], ), NetgearSensorEntityDescription( key="NewMonthDownload", @@ -177,7 +180,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:download", index=1, - value=lambda data: data[1] if data is not None else None, + value=lambda data: data[1], ), NetgearSensorEntityDescription( key="NewLastMonthUpload", @@ -186,7 +189,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:upload", index=0, - value=lambda data: data[0] if data is not None else None, + value=lambda data: data[0], ), NetgearSensorEntityDescription( key="NewLastMonthUpload", @@ -195,7 +198,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:upload", index=1, - value=lambda data: data[1] if data is not None else None, + value=lambda data: data[1], ), NetgearSensorEntityDescription( key="NewLastMonthDownload", @@ -204,7 +207,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:download", index=0, - value=lambda data: data[0] if data is not None else None, + value=lambda data: data[0], ), NetgearSensorEntityDescription( key="NewLastMonthDownload", @@ -213,7 +216,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:download", index=1, - value=lambda data: data[1] if data is not None else None, + value=lambda data: data[1], ), ] @@ -372,6 +375,17 @@ class NetgearRouterSensorEntity(NetgearRouterEntity, RestoreSensor): @callback def async_update_device(self) -> None: """Update the Netgear device.""" - if self.coordinator.data is not None: - data = self.coordinator.data.get(self.entity_description.key) - self._value = self.entity_description.value(data) + if self.coordinator.data is None: + return + + data = self.coordinator.data.get(self.entity_description.key) + if data is None: + self._value = None + _LOGGER.debug( + "key '%s' not in Netgear router response '%s'", + self.entity_description.key, + data, + ) + return + + self._value = self.entity_description.value(data) From 91cd61804eb678dd251c0fd6bcbb29bcc82f96aa Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 9 Jun 2022 07:08:08 -0700 Subject: [PATCH 1377/3516] Deprecate google calendar add_event service, replaced with entity service (#72473) * Deprecate google calendar add_event service, replaced with entity service * Fix inconsistencies and bugs in input validation * Update validation rules and exceptions * Resolve merge conflicts --- homeassistant/components/google/__init__.py | 60 ++-- homeassistant/components/google/calendar.py | 131 ++++++++- homeassistant/components/google/const.py | 12 + homeassistant/components/google/services.yaml | 51 ++++ tests/components/google/test_init.py | 261 ++++++++++++++---- 5 files changed, 427 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 1ddb44e570b..e86f1c43ebf 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -43,6 +43,16 @@ from .const import ( DATA_SERVICE, DEVICE_AUTH_IMPL, DOMAIN, + EVENT_DESCRIPTION, + EVENT_END_DATE, + EVENT_END_DATETIME, + EVENT_IN, + EVENT_IN_DAYS, + EVENT_IN_WEEKS, + EVENT_START_DATE, + EVENT_START_DATETIME, + EVENT_SUMMARY, + EVENT_TYPES_CONF, FeatureAccess, ) @@ -61,18 +71,6 @@ CONF_MAX_RESULTS = "max_results" DEFAULT_CONF_OFFSET = "!!" EVENT_CALENDAR_ID = "calendar_id" -EVENT_DESCRIPTION = "description" -EVENT_END_CONF = "end" -EVENT_END_DATE = "end_date" -EVENT_END_DATETIME = "end_date_time" -EVENT_IN = "in" -EVENT_IN_DAYS = "days" -EVENT_IN_WEEKS = "weeks" -EVENT_START_CONF = "start" -EVENT_START_DATE = "start_date" -EVENT_START_DATETIME = "start_date_time" -EVENT_SUMMARY = "summary" -EVENT_TYPES_CONF = "event_types" NOTIFICATION_ID = "google_calendar_notification" NOTIFICATION_TITLE = "Google Calendar Setup" @@ -138,17 +136,31 @@ _EVENT_IN_TYPES = vol.Schema( } ) -ADD_EVENT_SERVICE_SCHEMA = vol.Schema( +ADD_EVENT_SERVICE_SCHEMA = vol.All( + cv.has_at_least_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN), + cv.has_at_most_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN), { vol.Required(EVENT_CALENDAR_ID): cv.string, vol.Required(EVENT_SUMMARY): cv.string, vol.Optional(EVENT_DESCRIPTION, default=""): cv.string, - vol.Exclusive(EVENT_START_DATE, EVENT_START_CONF): cv.date, - vol.Exclusive(EVENT_END_DATE, EVENT_END_CONF): cv.date, - vol.Exclusive(EVENT_START_DATETIME, EVENT_START_CONF): cv.datetime, - vol.Exclusive(EVENT_END_DATETIME, EVENT_END_CONF): cv.datetime, - vol.Exclusive(EVENT_IN, EVENT_START_CONF, EVENT_END_CONF): _EVENT_IN_TYPES, - } + vol.Inclusive( + EVENT_START_DATE, "dates", "Start and end dates must both be specified" + ): cv.date, + vol.Inclusive( + EVENT_END_DATE, "dates", "Start and end dates must both be specified" + ): cv.date, + vol.Inclusive( + EVENT_START_DATETIME, + "datetimes", + "Start and end datetimes must both be specified", + ): cv.datetime, + vol.Inclusive( + EVENT_END_DATETIME, + "datetimes", + "Start and end datetimes must both be specified", + ): cv.datetime, + vol.Optional(EVENT_IN): _EVENT_IN_TYPES, + }, ) @@ -276,6 +288,12 @@ async def async_setup_add_event_service( async def _add_event(call: ServiceCall) -> None: """Add a new event to calendar.""" + _LOGGER.warning( + "The Google Calendar add_event service has been deprecated, and " + "will be removed in a future Home Assistant release. Please move " + "calls to the create_event service" + ) + start: DateOrDatetime | None = None end: DateOrDatetime | None = None @@ -298,11 +316,11 @@ async def async_setup_add_event_service( start = DateOrDatetime(date=start_in) end = DateOrDatetime(date=end_in) - elif EVENT_START_DATE in call.data: + elif EVENT_START_DATE in call.data and EVENT_END_DATE in call.data: start = DateOrDatetime(date=call.data[EVENT_START_DATE]) end = DateOrDatetime(date=call.data[EVENT_END_DATE]) - elif EVENT_START_DATETIME in call.data: + elif EVENT_START_DATETIME in call.data and EVENT_END_DATETIME in call.data: start_dt = call.data[EVENT_START_DATETIME] end_dt = call.data[EVENT_END_DATETIME] start = DateOrDatetime( diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 78661ed792f..39e3d69e6b9 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -9,7 +9,8 @@ from typing import Any from gcal_sync.api import GoogleCalendarService, ListEventsRequest from gcal_sync.exceptions import ApiException -from gcal_sync.model import Event +from gcal_sync.model import DateOrDatetime, Event +import voluptuous as vol from homeassistant.components.calendar import ( ENTITY_ID_FORMAT, @@ -20,8 +21,9 @@ from homeassistant.components.calendar import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import Throttle @@ -38,22 +40,66 @@ from . import ( load_config, update_config, ) +from .api import get_feature_access +from .const import ( + EVENT_DESCRIPTION, + EVENT_END_DATE, + EVENT_END_DATETIME, + EVENT_IN, + EVENT_IN_DAYS, + EVENT_IN_WEEKS, + EVENT_START_DATE, + EVENT_START_DATETIME, + EVENT_SUMMARY, + EVENT_TYPES_CONF, + FeatureAccess, +) _LOGGER = logging.getLogger(__name__) -DEFAULT_GOOGLE_SEARCH_PARAMS = { - "orderBy": "startTime", - "singleEvents": True, -} - MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) # Events have a transparency that determine whether or not they block time on calendar. # When an event is opaque, it means "Show me as busy" which is the default. Events that # are not opaque are ignored by default. -TRANSPARENCY = "transparency" OPAQUE = "opaque" +_EVENT_IN_TYPES = vol.Schema( + { + vol.Exclusive(EVENT_IN_DAYS, EVENT_TYPES_CONF): cv.positive_int, + vol.Exclusive(EVENT_IN_WEEKS, EVENT_TYPES_CONF): cv.positive_int, + } +) + +SERVICE_CREATE_EVENT = "create_event" +CREATE_EVENT_SCHEMA = vol.All( + cv.has_at_least_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN), + cv.has_at_most_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN), + cv.make_entity_service_schema( + { + vol.Required(EVENT_SUMMARY): cv.string, + vol.Optional(EVENT_DESCRIPTION, default=""): cv.string, + vol.Inclusive( + EVENT_START_DATE, "dates", "Start and end dates must both be specified" + ): cv.date, + vol.Inclusive( + EVENT_END_DATE, "dates", "Start and end dates must both be specified" + ): cv.date, + vol.Inclusive( + EVENT_START_DATETIME, + "datetimes", + "Start and end datetimes must both be specified", + ): cv.datetime, + vol.Inclusive( + EVENT_END_DATETIME, + "datetimes", + "Start and end datetimes must both be specified", + ): cv.datetime, + vol.Optional(EVENT_IN): _EVENT_IN_TYPES, + } + ), +) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -116,6 +162,14 @@ async def async_setup_entry( await hass.async_add_executor_job(append_calendars_to_config) + platform = entity_platform.async_get_current_platform() + if get_feature_access(hass, entry) is FeatureAccess.read_write: + platform.async_register_entity_service( + SERVICE_CREATE_EVENT, + CREATE_EVENT_SCHEMA, + async_create_event, + ) + class GoogleCalendarEntity(CalendarEntity): """A calendar event device.""" @@ -130,8 +184,8 @@ class GoogleCalendarEntity(CalendarEntity): entity_enabled: bool, ) -> None: """Create the Calendar event device.""" - self._calendar_service = calendar_service - self._calendar_id = calendar_id + self.calendar_service = calendar_service + self.calendar_id = calendar_id self._search: str | None = data.get(CONF_SEARCH) self._ignore_availability: bool = data.get(CONF_IGNORE_AVAILABILITY, False) self._event: CalendarEvent | None = None @@ -178,14 +232,14 @@ class GoogleCalendarEntity(CalendarEntity): """Get all events in a specific time frame.""" request = ListEventsRequest( - calendar_id=self._calendar_id, + calendar_id=self.calendar_id, start_time=start_date, end_time=end_date, search=self._search, ) result_items = [] try: - result = await self._calendar_service.async_list_events(request) + result = await self.calendar_service.async_list_events(request) async for result_page in result: result_items.extend(result_page.items) except ApiException as err: @@ -199,9 +253,9 @@ class GoogleCalendarEntity(CalendarEntity): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self) -> None: """Get the latest data.""" - request = ListEventsRequest(calendar_id=self._calendar_id, search=self._search) + request = ListEventsRequest(calendar_id=self.calendar_id, search=self._search) try: - result = await self._calendar_service.async_list_events(request) + result = await self.calendar_service.async_list_events(request) except ApiException as err: _LOGGER.error("Unable to connect to Google: %s", err) return @@ -226,3 +280,52 @@ def _get_calendar_event(event: Event) -> CalendarEvent: description=event.description, location=event.location, ) + + +async def async_create_event(entity: GoogleCalendarEntity, call: ServiceCall) -> None: + """Add a new event to calendar.""" + start: DateOrDatetime | None = None + end: DateOrDatetime | None = None + hass = entity.hass + + if EVENT_IN in call.data: + if EVENT_IN_DAYS in call.data[EVENT_IN]: + now = datetime.now() + + start_in = now + timedelta(days=call.data[EVENT_IN][EVENT_IN_DAYS]) + end_in = start_in + timedelta(days=1) + + start = DateOrDatetime(date=start_in) + end = DateOrDatetime(date=end_in) + + elif EVENT_IN_WEEKS in call.data[EVENT_IN]: + now = datetime.now() + + start_in = now + timedelta(weeks=call.data[EVENT_IN][EVENT_IN_WEEKS]) + end_in = start_in + timedelta(days=1) + + start = DateOrDatetime(date=start_in) + end = DateOrDatetime(date=end_in) + + elif EVENT_START_DATE in call.data and EVENT_END_DATE in call.data: + start = DateOrDatetime(date=call.data[EVENT_START_DATE]) + end = DateOrDatetime(date=call.data[EVENT_END_DATE]) + + elif EVENT_START_DATETIME in call.data and EVENT_END_DATETIME in call.data: + start_dt = call.data[EVENT_START_DATETIME] + end_dt = call.data[EVENT_END_DATETIME] + start = DateOrDatetime(date_time=start_dt, timezone=str(hass.config.time_zone)) + end = DateOrDatetime(date_time=end_dt, timezone=str(hass.config.time_zone)) + + if start is None or end is None: + raise ValueError("Missing required fields to set start or end date/datetime") + + await entity.calendar_service.async_create_event( + entity.calendar_id, + Event( + summary=call.data[EVENT_SUMMARY], + description=call.data[EVENT_DESCRIPTION], + start=start, + end=end, + ), + ) diff --git a/homeassistant/components/google/const.py b/homeassistant/components/google/const.py index fba9b01b600..f07958c2e6e 100644 --- a/homeassistant/components/google/const.py +++ b/homeassistant/components/google/const.py @@ -29,3 +29,15 @@ class FeatureAccess(Enum): DEFAULT_FEATURE_ACCESS = FeatureAccess.read_write + + +EVENT_DESCRIPTION = "description" +EVENT_END_DATE = "end_date" +EVENT_END_DATETIME = "end_date_time" +EVENT_IN = "in" +EVENT_IN_DAYS = "days" +EVENT_IN_WEEKS = "weeks" +EVENT_START_DATE = "start_date" +EVENT_START_DATETIME = "start_date_time" +EVENT_SUMMARY = "summary" +EVENT_TYPES_CONF = "event_types" diff --git a/homeassistant/components/google/services.yaml b/homeassistant/components/google/services.yaml index baa069aaedf..a303ad7e18d 100644 --- a/homeassistant/components/google/services.yaml +++ b/homeassistant/components/google/services.yaml @@ -52,3 +52,54 @@ add_event: example: '"days": 2 or "weeks": 2' selector: object: +create_event: + name: Create event + description: Add a new calendar event. + target: + entity: + integration: google + domain: calendar + fields: + summary: + name: Summary + description: Acts as the title of the event. + required: true + example: "Bowling" + selector: + text: + description: + name: Description + description: The description of the event. Optional. + example: "Birthday bowling" + selector: + text: + start_date_time: + name: Start time + description: The date and time the event should start. + example: "2022-03-22 20:00:00" + selector: + text: + end_date_time: + name: End time + description: The date and time the event should end. + example: "2022-03-22 22:00:00" + selector: + text: + start_date: + name: Start date + description: The date the whole day event should start. + example: "2022-03-10" + selector: + text: + end_date: + name: End date + description: The date the whole day event should end. + example: "2022-03-11" + selector: + text: + in: + name: In + description: Days or weeks that you want to create the event in. + example: '"days": 2 or "weeks": 2' + selector: + object: diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index cadb444c26f..b2a81b47718 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -9,12 +9,14 @@ from typing import Any from unittest.mock import Mock, patch import pytest +import voluptuous as vol from homeassistant.components.application_credentials import ( ClientCredential, async_import_client_credential, ) from homeassistant.components.google import DOMAIN, SERVICE_ADD_EVENT +from homeassistant.components.google.calendar import SERVICE_CREATE_EVENT from homeassistant.components.google.const import CONF_CALENDAR_ACCESS from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF @@ -40,6 +42,9 @@ EXPIRED_TOKEN_TIMESTAMP = datetime.datetime(2022, 4, 8).timestamp() # Typing helpers HassApi = Callable[[], Awaitable[dict[str, Any]]] +TEST_EVENT_SUMMARY = "Test Summary" +TEST_EVENT_DESCRIPTION = "Test Description" + def assert_state(actual: State | None, expected: State | None) -> None: """Assert that the two states are equal.""" @@ -59,6 +64,45 @@ def setup_config_entry( config_entry.add_to_hass(hass) +@pytest.fixture( + params=[ + ( + SERVICE_ADD_EVENT, + {"calendar_id": CALENDAR_ID}, + None, + ), + ( + SERVICE_CREATE_EVENT, + {}, + {"entity_id": TEST_YAML_ENTITY}, + ), + ], + ids=("add_event", "create_event"), +) +def add_event_call_service( + hass: HomeAssistant, + request: Any, +) -> Callable[dict[str, Any], Awaitable[None]]: + """Fixture for calling the add or create event service.""" + (service_call, data, target) = request.param + + async def call_service(params: dict[str, Any]) -> None: + await hass.services.async_call( + DOMAIN, + service_call, + { + **data, + **params, + "summary": TEST_EVENT_SUMMARY, + "description": TEST_EVENT_DESCRIPTION, + }, + target=target, + blocking=True, + ) + + return call_service + + async def test_unload_entry( hass: HomeAssistant, component_setup: ComponentSetup, @@ -297,28 +341,145 @@ async def test_calendar_config_track_new( assert_state(state, expected_state) -async def test_add_event_missing_required_fields( +@pytest.mark.parametrize( + "date_fields,expected_error,error_match", + [ + ( + {}, + vol.error.MultipleInvalid, + "must contain at least one of start_date, start_date_time, in", + ), + ( + { + "start_date": "2022-04-01", + }, + vol.error.MultipleInvalid, + "Start and end dates must both be specified", + ), + ( + { + "end_date": "2022-04-02", + }, + vol.error.MultipleInvalid, + "must contain at least one of start_date, start_date_time, in.", + ), + ( + { + "start_date_time": "2022-04-01T06:00:00", + }, + vol.error.MultipleInvalid, + "Start and end datetimes must both be specified", + ), + ( + { + "end_date_time": "2022-04-02T07:00:00", + }, + vol.error.MultipleInvalid, + "must contain at least one of start_date, start_date_time, in.", + ), + ( + { + "start_date": "2022-04-01", + "start_date_time": "2022-04-01T06:00:00", + "end_date_time": "2022-04-02T07:00:00", + }, + vol.error.MultipleInvalid, + "must contain at most one of start_date, start_date_time, in.", + ), + ( + { + "start_date_time": "2022-04-01T06:00:00", + "end_date_time": "2022-04-01T07:00:00", + "end_date": "2022-04-02", + }, + vol.error.MultipleInvalid, + "Start and end dates must both be specified", + ), + ( + { + "start_date": "2022-04-01", + "end_date_time": "2022-04-02T07:00:00", + }, + vol.error.MultipleInvalid, + "Start and end dates must both be specified", + ), + ( + { + "start_date_time": "2022-04-01T07:00:00", + "end_date": "2022-04-02", + }, + vol.error.MultipleInvalid, + "Start and end dates must both be specified", + ), + ( + { + "in": { + "days": 2, + "weeks": 2, + } + }, + vol.error.MultipleInvalid, + "two or more values in the same group of exclusion 'event_types'", + ), + ( + { + "start_date": "2022-04-01", + "end_date": "2022-04-02", + "in": { + "days": 2, + }, + }, + vol.error.MultipleInvalid, + "must contain at most one of start_date, start_date_time, in.", + ), + ( + { + "start_date_time": "2022-04-01T07:00:00", + "end_date_time": "2022-04-01T07:00:00", + "in": { + "days": 2, + }, + }, + vol.error.MultipleInvalid, + "must contain at most one of start_date, start_date_time, in.", + ), + ], + ids=[ + "missing_all", + "missing_end_date", + "missing_start_date", + "missing_end_datetime", + "missing_start_datetime", + "multiple_start", + "multiple_end", + "missing_end_date", + "missing_end_date_time", + "multiple_in", + "unexpected_in_with_date", + "unexpected_in_with_datetime", + ], +) +async def test_add_event_invalid_params( hass: HomeAssistant, component_setup: ComponentSetup, mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], + mock_calendars_yaml: None, + mock_events_list: ApiResult, setup_config_entry: MockConfigEntry, + add_event_call_service: Callable[dict[str, Any], Awaitable[None]], + date_fields: dict[str, Any], + expected_error: type[Exception], + error_match: str | None, ) -> None: - """Test service call that adds an event missing required fields.""" + """Test service calls with incorrect fields.""" + mock_calendars_list({"items": [test_api_calendar]}) + mock_events_list({}) assert await component_setup() - with pytest.raises(ValueError): - await hass.services.async_call( - DOMAIN, - SERVICE_ADD_EVENT, - { - "calendar_id": CALENDAR_ID, - "summary": "Summary", - "description": "Description", - }, - blocking=True, - ) + with pytest.raises(expected_error, match=error_match): + await add_event_call_service(date_fields) @pytest.mark.parametrize( @@ -343,40 +504,35 @@ async def test_add_event_date_in_x( mock_calendars_list: ApiResult, mock_insert_event: Callable[[..., dict[str, Any]], None], test_api_calendar: dict[str, Any], + mock_calendars_yaml: None, + mock_events_list: ApiResult, date_fields: dict[str, Any], start_timedelta: datetime.timedelta, end_timedelta: datetime.timedelta, setup_config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, + add_event_call_service: Callable[dict[str, Any], Awaitable[None]], ) -> None: """Test service call that adds an event with various time ranges.""" - mock_calendars_list({}) + mock_calendars_list({"items": [test_api_calendar]}) + mock_events_list({}) assert await component_setup() now = datetime.datetime.now() start_date = now + start_timedelta end_date = now + end_timedelta + aioclient_mock.clear_requests() mock_insert_event( calendar_id=CALENDAR_ID, ) - await hass.services.async_call( - DOMAIN, - SERVICE_ADD_EVENT, - { - "calendar_id": CALENDAR_ID, - "summary": "Summary", - "description": "Description", - **date_fields, - }, - blocking=True, - ) - assert len(aioclient_mock.mock_calls) == 2 - assert aioclient_mock.mock_calls[1][2] == { - "summary": "Summary", - "description": "Description", + await add_event_call_service(date_fields) + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[0][2] == { + "summary": TEST_EVENT_SUMMARY, + "description": TEST_EVENT_DESCRIPTION, "start": {"date": start_date.date().isoformat()}, "end": {"date": end_date.date().isoformat()}, } @@ -386,39 +542,39 @@ async def test_add_event_date( hass: HomeAssistant, component_setup: ComponentSetup, mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], mock_insert_event: Callable[[str, dict[str, Any]], None], + mock_calendars_yaml: None, + mock_events_list: ApiResult, setup_config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, + add_event_call_service: Callable[dict[str, Any], Awaitable[None]], ) -> None: """Test service call that sets a date range.""" - mock_calendars_list({}) + mock_calendars_list({"items": [test_api_calendar]}) + mock_events_list({}) assert await component_setup() now = utcnow() today = now.date() end_date = today + datetime.timedelta(days=2) + aioclient_mock.clear_requests() mock_insert_event( calendar_id=CALENDAR_ID, ) - await hass.services.async_call( - DOMAIN, - SERVICE_ADD_EVENT, + await add_event_call_service( { - "calendar_id": CALENDAR_ID, - "summary": "Summary", - "description": "Description", "start_date": today.isoformat(), "end_date": end_date.isoformat(), }, - blocking=True, ) - assert len(aioclient_mock.mock_calls) == 2 - assert aioclient_mock.mock_calls[1][2] == { - "summary": "Summary", - "description": "Description", + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[0][2] == { + "summary": TEST_EVENT_SUMMARY, + "description": TEST_EVENT_DESCRIPTION, "start": {"date": today.isoformat()}, "end": {"date": end_date.isoformat()}, } @@ -430,38 +586,37 @@ async def test_add_event_date_time( mock_calendars_list: ApiResult, mock_insert_event: Callable[[str, dict[str, Any]], None], test_api_calendar: dict[str, Any], + mock_calendars_yaml: None, + mock_events_list: ApiResult, setup_config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, + add_event_call_service: Callable[dict[str, Any], Awaitable[None]], ) -> None: """Test service call that adds an event with a date time range.""" - mock_calendars_list({}) + mock_calendars_list({"items": [test_api_calendar]}) + mock_events_list({}) assert await component_setup() start_datetime = datetime.datetime.now() delta = datetime.timedelta(days=3, hours=3) end_datetime = start_datetime + delta + aioclient_mock.clear_requests() mock_insert_event( calendar_id=CALENDAR_ID, ) - await hass.services.async_call( - DOMAIN, - SERVICE_ADD_EVENT, + await add_event_call_service( { - "calendar_id": CALENDAR_ID, - "summary": "Summary", - "description": "Description", "start_date_time": start_datetime.isoformat(), "end_date_time": end_datetime.isoformat(), }, - blocking=True, ) - assert len(aioclient_mock.mock_calls) == 2 - assert aioclient_mock.mock_calls[1][2] == { - "summary": "Summary", - "description": "Description", + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[0][2] == { + "summary": TEST_EVENT_SUMMARY, + "description": TEST_EVENT_DESCRIPTION, "start": { "dateTime": start_datetime.isoformat(timespec="seconds"), "timeZone": "America/Regina", From 542eae1cf3ee92c9b9066f406291baeb3ecabcc5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 9 Jun 2022 16:09:00 +0200 Subject: [PATCH 1378/3516] Add additional board types to hassio (#73267) * Add additional board types to hassio * Remove unsupported boards * Add rpi2 back --- homeassistant/components/hassio/__init__.py | 10 ++++++- tests/components/hassio/test_init.py | 31 +++++++++++++-------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index cab17c94f0c..0df29a6153b 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -206,7 +206,15 @@ MAP_SERVICE_API = { } HARDWARE_INTEGRATIONS = { - "rpi": "raspberry_pi", + "odroid-c2": "hardkernel", + "odroid-c4": "hardkernel", + "odroid-n2": "hardkernel", + "odroid-xu4": "hardkernel", + "rpi2": "raspberry_pi", + "rpi3": "raspberry_pi", + "rpi3-64": "raspberry_pi", + "rpi4": "raspberry_pi", + "rpi4-64": "raspberry_pi", } diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index c47b3bfbeca..1569e834562 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -21,12 +21,18 @@ MOCK_ENVIRON = {"SUPERVISOR": "127.0.0.1", "SUPERVISOR_TOKEN": "abcdefgh"} @pytest.fixture() -def os_info(): +def extra_os_info(): + """Extra os/info.""" + return {} + + +@pytest.fixture() +def os_info(extra_os_info): """Mock os/info.""" return { "json": { "result": "ok", - "data": {"version_latest": "1.0.0", "version": "1.0.0"}, + "data": {"version_latest": "1.0.0", "version": "1.0.0", **extra_os_info}, } } @@ -715,21 +721,24 @@ async def test_coordinator_updates(hass, caplog): @pytest.mark.parametrize( - "os_info", + "extra_os_info, integration", [ - { - "json": { - "result": "ok", - "data": {"version_latest": "1.0.0", "version": "1.0.0", "board": "rpi"}, - } - } + ({"board": "odroid-c2"}, "hardkernel"), + ({"board": "odroid-c4"}, "hardkernel"), + ({"board": "odroid-n2"}, "hardkernel"), + ({"board": "odroid-xu4"}, "hardkernel"), + ({"board": "rpi2"}, "raspberry_pi"), + ({"board": "rpi3"}, "raspberry_pi"), + ({"board": "rpi3-64"}, "raspberry_pi"), + ({"board": "rpi4"}, "raspberry_pi"), + ({"board": "rpi4-64"}, "raspberry_pi"), ], ) -async def test_setup_hardware_integration(hass, aioclient_mock): +async def test_setup_hardware_integration(hass, aioclient_mock, integration): """Test setup initiates hardware integration.""" with patch.dict(os.environ, MOCK_ENVIRON), patch( - "homeassistant.components.raspberry_pi.async_setup_entry", + f"homeassistant.components.{integration}.async_setup_entry", return_value=True, ) as mock_setup_entry: result = await async_setup_component(hass, "hassio", {"hassio": {}}) From 82afbd1d125b75a4296e818758381dfcbf996c7d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 9 Jun 2022 16:12:10 +0200 Subject: [PATCH 1379/3516] Improve raspberry_pi tests (#73269) * Improve raspberry_pi tests * Address review comments --- tests/components/raspberry_pi/test_init.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/components/raspberry_pi/test_init.py b/tests/components/raspberry_pi/test_init.py index dd86da7bce0..4bf64c7999a 100644 --- a/tests/components/raspberry_pi/test_init.py +++ b/tests/components/raspberry_pi/test_init.py @@ -1,6 +1,8 @@ """Test the Raspberry Pi integration.""" from unittest.mock import patch +import pytest + from homeassistant.components.raspberry_pi.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -8,6 +10,16 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, MockModule, mock_integration +@pytest.fixture(autouse=True) +def mock_rpi_power(): + """Mock the rpi_power integration.""" + with patch( + "homeassistant.components.rpi_power.async_setup_entry", + return_value=True, + ): + yield + + async def test_setup_entry(hass: HomeAssistant) -> None: """Test setup of a config entry.""" mock_integration(hass, MockModule("hassio")) @@ -20,14 +32,19 @@ async def test_setup_entry(hass: HomeAssistant) -> None: title="Raspberry Pi", ) config_entry.add_to_hass(hass) + assert not hass.config_entries.async_entries("rpi_power") with patch( "homeassistant.components.raspberry_pi.get_os_info", return_value={"board": "rpi"}, - ) as mock_get_os_info: + ) as mock_get_os_info, patch( + "homeassistant.components.rpi_power.config_flow.new_under_voltage" + ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert len(mock_get_os_info.mock_calls) == 1 + assert len(hass.config_entries.async_entries("rpi_power")) == 1 + async def test_setup_entry_wrong_board(hass: HomeAssistant) -> None: """Test setup of a config entry with wrong board type.""" From f25fdf0d2e787faf1e9ec2096034b2def26e5e8b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 9 Jun 2022 12:46:13 -0700 Subject: [PATCH 1380/3516] Fix reloading themes crashing if no themes configured (#73287) --- homeassistant/components/frontend/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index c1deb02fc6a..b3907143eb9 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -460,7 +460,7 @@ async def _async_setup_themes( async def reload_themes(_: ServiceCall) -> None: """Reload themes.""" config = await async_hass_config_yaml(hass) - new_themes = config[DOMAIN].get(CONF_THEMES, {}) + new_themes = config.get(DOMAIN, {}).get(CONF_THEMES, {}) hass.data[DATA_THEMES] = new_themes if hass.data[DATA_DEFAULT_THEME] not in new_themes: hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME From 1d6068fa0950bbe23efed5c42e9d69a4450f93e9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 9 Jun 2022 21:47:21 +0200 Subject: [PATCH 1381/3516] Update google-cloud-texttospeech to 2.11.1 (#73210) --- homeassistant/components/google_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_cloud/manifest.json b/homeassistant/components/google_cloud/manifest.json index 87da1f55fca..633c5edc453 100644 --- a/homeassistant/components/google_cloud/manifest.json +++ b/homeassistant/components/google_cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "google_cloud", "name": "Google Cloud Platform", "documentation": "https://www.home-assistant.io/integrations/google_cloud", - "requirements": ["google-cloud-texttospeech==2.11.0"], + "requirements": ["google-cloud-texttospeech==2.11.1"], "codeowners": ["@lufton"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 7aa8f60f961..369cedf3d05 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -741,7 +741,7 @@ goodwe==0.2.15 google-cloud-pubsub==2.11.0 # homeassistant.components.google_cloud -google-cloud-texttospeech==2.11.0 +google-cloud-texttospeech==2.11.1 # homeassistant.components.nest google-nest-sdm==2.0.0 From 22daea27c205b9c931de8fb7cb42a630866b4960 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Jun 2022 10:22:16 -1000 Subject: [PATCH 1382/3516] Cleanup coordinators in synology_dsm (#73257) Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com> --- .coveragerc | 1 + .../components/synology_dsm/__init__.py | 170 +++++------------- .../components/synology_dsm/binary_sensor.py | 10 +- .../components/synology_dsm/button.py | 9 +- .../components/synology_dsm/camera.py | 26 +-- .../components/synology_dsm/common.py | 9 +- .../components/synology_dsm/const.py | 30 +++- .../components/synology_dsm/coordinator.py | 148 +++++++++++++++ .../components/synology_dsm/diagnostics.py | 18 +- .../components/synology_dsm/models.py | 21 +++ .../components/synology_dsm/sensor.py | 10 +- .../components/synology_dsm/service.py | 18 +- .../components/synology_dsm/switch.py | 33 ++-- .../components/synology_dsm/update.py | 11 +- tests/components/synology_dsm/test_init.py | 6 +- 15 files changed, 287 insertions(+), 233 deletions(-) create mode 100644 homeassistant/components/synology_dsm/coordinator.py create mode 100644 homeassistant/components/synology_dsm/models.py diff --git a/.coveragerc b/.coveragerc index 44b205746df..ea2c41f7e46 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1200,6 +1200,7 @@ omit = homeassistant/components/synology_dsm/binary_sensor.py homeassistant/components/synology_dsm/button.py homeassistant/components/synology_dsm/camera.py + homeassistant/components/synology_dsm/coordinator.py homeassistant/components/synology_dsm/diagnostics.py homeassistant/components/synology_dsm/common.py homeassistant/components/synology_dsm/entity.py diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index ece38bf7326..d8d768d36e4 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -1,47 +1,32 @@ """The Synology DSM component.""" from __future__ import annotations -from datetime import timedelta import logging -from typing import Any -import async_timeout from synology_dsm.api.surveillance_station import SynoSurveillanceStation -from synology_dsm.api.surveillance_station.camera import SynoCamera -from synology_dsm.exceptions import ( - SynologyDSMAPIErrorException, - SynologyDSMLogin2SARequiredException, - SynologyDSMLoginDisabledAccountException, - SynologyDSMLoginFailedException, - SynologyDSMLoginInvalidException, - SynologyDSMLoginPermissionDeniedException, - SynologyDSMRequestException, -) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MAC, CONF_SCAN_INTERVAL, CONF_VERIFY_SSL +from homeassistant.const import CONF_MAC, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .common import SynoApi from .const import ( - COORDINATOR_CAMERAS, - COORDINATOR_CENTRAL, - COORDINATOR_SWITCHES, - DEFAULT_SCAN_INTERVAL, DEFAULT_VERIFY_SSL, DOMAIN, EXCEPTION_DETAILS, EXCEPTION_UNKNOWN, PLATFORMS, - SIGNAL_CAMERA_SOURCE_CHANGED, - SYNO_API, - SYSTEM_LOADED, - UNDO_UPDATE_LISTENER, + SYNOLOGY_AUTH_FAILED_EXCEPTIONS, + SYNOLOGY_CONNECTION_EXCEPTIONS, ) +from .coordinator import ( + SynologyDSMCameraUpdateCoordinator, + SynologyDSMCentralUpdateCoordinator, + SynologyDSMSwitchUpdateCoordinator, +) +from .models import SynologyDSMData from .service import async_setup_services CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @@ -79,31 +64,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api = SynoApi(hass, entry) try: await api.async_setup() - except ( - SynologyDSMLogin2SARequiredException, - SynologyDSMLoginDisabledAccountException, - SynologyDSMLoginInvalidException, - SynologyDSMLoginPermissionDeniedException, - ) as err: + except SYNOLOGY_AUTH_FAILED_EXCEPTIONS as err: if err.args[0] and isinstance(err.args[0], dict): details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) else: details = EXCEPTION_UNKNOWN raise ConfigEntryAuthFailed(f"reason: {details}") from err - except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: + except SYNOLOGY_CONNECTION_EXCEPTIONS as err: if err.args[0] and isinstance(err.args[0], dict): details = err.args[0].get(EXCEPTION_DETAILS, EXCEPTION_UNKNOWN) else: details = EXCEPTION_UNKNOWN raise ConfigEntryNotReady(details) from err - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.unique_id] = { - UNDO_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener), - SYNO_API: api, - SYSTEM_LOADED: True, - } - # Services await async_setup_services(hass) @@ -114,111 +87,50 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, data={**entry.data, CONF_MAC: network.macs} ) - async def async_coordinator_update_data_cameras() -> dict[ - str, dict[str, SynoCamera] - ] | None: - """Fetch all camera data from api.""" - if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]: - raise UpdateFailed("System not fully loaded") + # These all create executor jobs so we do not gather here + coordinator_central = SynologyDSMCentralUpdateCoordinator(hass, entry, api) + await coordinator_central.async_config_entry_first_refresh() - if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis: - return None + available_apis = api.dsm.apis - surveillance_station = api.surveillance_station - current_data: dict[str, SynoCamera] = { - camera.id: camera for camera in surveillance_station.get_all_cameras() - } + # The central coordinator needs to be refreshed first since + # the next two rely on data from it + coordinator_cameras: SynologyDSMCameraUpdateCoordinator | None = None + if SynoSurveillanceStation.CAMERA_API_KEY in available_apis: + coordinator_cameras = SynologyDSMCameraUpdateCoordinator(hass, entry, api) + await coordinator_cameras.async_config_entry_first_refresh() + coordinator_switches: SynologyDSMSwitchUpdateCoordinator | None = None + if ( + SynoSurveillanceStation.INFO_API_KEY in available_apis + and SynoSurveillanceStation.HOME_MODE_API_KEY in available_apis + ): + coordinator_switches = SynologyDSMSwitchUpdateCoordinator(hass, entry, api) + await coordinator_switches.async_config_entry_first_refresh() try: - async with async_timeout.timeout(30): - await hass.async_add_executor_job(surveillance_station.update) - except SynologyDSMAPIErrorException as err: - raise UpdateFailed(f"Error communicating with API: {err}") from err + await coordinator_switches.async_setup() + except SYNOLOGY_CONNECTION_EXCEPTIONS as ex: + raise ConfigEntryNotReady from ex - new_data: dict[str, SynoCamera] = { - camera.id: camera for camera in surveillance_station.get_all_cameras() - } - - for cam_id, cam_data_new in new_data.items(): - if ( - (cam_data_current := current_data.get(cam_id)) is not None - and cam_data_current.live_view.rtsp != cam_data_new.live_view.rtsp - ): - async_dispatcher_send( - hass, - f"{SIGNAL_CAMERA_SOURCE_CHANGED}_{entry.entry_id}_{cam_id}", - cam_data_new.live_view.rtsp, - ) - - return {"cameras": new_data} - - async def async_coordinator_update_data_central() -> None: - """Fetch all device and sensor data from api.""" - try: - await api.async_update() - except Exception as err: - raise UpdateFailed(f"Error communicating with API: {err}") from err - return None - - async def async_coordinator_update_data_switches() -> dict[ - str, dict[str, Any] - ] | None: - """Fetch all switch data from api.""" - if not hass.data[DOMAIN][entry.unique_id][SYSTEM_LOADED]: - raise UpdateFailed("System not fully loaded") - if SynoSurveillanceStation.HOME_MODE_API_KEY not in api.dsm.apis: - return None - - surveillance_station = api.surveillance_station - - return { - "switches": { - "home_mode": await hass.async_add_executor_job( - surveillance_station.get_home_mode_status - ) - } - } - - hass.data[DOMAIN][entry.unique_id][COORDINATOR_CAMERAS] = DataUpdateCoordinator( - hass, - _LOGGER, - name=f"{entry.unique_id}_cameras", - update_method=async_coordinator_update_data_cameras, - update_interval=timedelta(seconds=30), + synology_data = SynologyDSMData( + api=api, + coordinator_central=coordinator_central, + coordinator_cameras=coordinator_cameras, + coordinator_switches=coordinator_switches, ) - - hass.data[DOMAIN][entry.unique_id][COORDINATOR_CENTRAL] = DataUpdateCoordinator( - hass, - _LOGGER, - name=f"{entry.unique_id}_central", - update_method=async_coordinator_update_data_central, - update_interval=timedelta( - minutes=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - ), - ) - - hass.data[DOMAIN][entry.unique_id][COORDINATOR_SWITCHES] = DataUpdateCoordinator( - hass, - _LOGGER, - name=f"{entry.unique_id}_switches", - update_method=async_coordinator_update_data_switches, - update_interval=timedelta(seconds=30), - ) - + hass.data.setdefault(DOMAIN, {})[entry.unique_id] = synology_data 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 Synology DSM sensors.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - entry_data = hass.data[DOMAIN][entry.unique_id] - entry_data[UNDO_UPDATE_LISTENER]() - await entry_data[SYNO_API].async_unload() + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + entry_data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] + await entry_data.api.async_unload() hass.data[DOMAIN].pop(entry.unique_id) - return unload_ok diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index a5c96575307..b5f5effbb8e 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -22,12 +22,13 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import SynoApi -from .const import COORDINATOR_CENTRAL, DOMAIN, SYNO_API +from .const import DOMAIN from .entity import ( SynologyDSMBaseEntity, SynologyDSMDeviceEntity, SynologyDSMEntityDescription, ) +from .models import SynologyDSMData @dataclass @@ -80,10 +81,9 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Synology NAS binary sensor.""" - - data = hass.data[DOMAIN][entry.unique_id] - api: SynoApi = data[SYNO_API] - coordinator = data[COORDINATOR_CENTRAL] + data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] + api = data.api + coordinator = data.coordinator_central entities: list[ SynoDSMSecurityBinarySensor diff --git a/homeassistant/components/synology_dsm/button.py b/homeassistant/components/synology_dsm/button.py index 58f1a0dfdd7..a1337e672f6 100644 --- a/homeassistant/components/synology_dsm/button.py +++ b/homeassistant/components/synology_dsm/button.py @@ -17,7 +17,8 @@ from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import SynoApi -from .const import DOMAIN, SYNO_API +from .const import DOMAIN +from .models import SynologyDSMData LOGGER = logging.getLogger(__name__) @@ -60,10 +61,8 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set buttons for device.""" - data = hass.data[DOMAIN][entry.unique_id] - syno_api: SynoApi = data[SYNO_API] - - async_add_entities(SynologyDSMButton(syno_api, button) for button in BUTTONS) + data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] + async_add_entities(SynologyDSMButton(data.api, button) for button in BUTTONS) class SynologyDSMButton(ButtonEntity): diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 0a6934b45a7..6dac67cf72d 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -25,13 +25,12 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import SynoApi from .const import ( CONF_SNAPSHOT_QUALITY, - COORDINATOR_CAMERAS, DEFAULT_SNAPSHOT_QUALITY, DOMAIN, SIGNAL_CAMERA_SOURCE_CHANGED, - SYNO_API, ) from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription +from .models import SynologyDSMData _LOGGER = logging.getLogger(__name__) @@ -47,23 +46,12 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Synology NAS cameras.""" - - data = hass.data[DOMAIN][entry.unique_id] - api: SynoApi = data[SYNO_API] - - if SynoSurveillanceStation.CAMERA_API_KEY not in api.dsm.apis: - return - - # initial data fetch - coordinator: DataUpdateCoordinator[dict[str, dict[str, SynoCamera]]] = data[ - COORDINATOR_CAMERAS - ] - await coordinator.async_config_entry_first_refresh() - - async_add_entities( - SynoDSMCamera(api, coordinator, camera_id) - for camera_id in coordinator.data["cameras"] - ) + data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] + if coordinator := data.coordinator_cameras: + async_add_entities( + SynoDSMCamera(data.api, coordinator, camera_id) + for camera_id in coordinator.data["cameras"] + ) class SynoDSMCamera(SynologyDSMBaseEntity, Camera): diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 2ca9cbf3ccf..088686660e4 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -32,7 +32,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback -from .const import CONF_DEVICE_TOKEN, DOMAIN, SYSTEM_LOADED +from .const import CONF_DEVICE_TOKEN LOGGER = logging.getLogger(__name__) @@ -217,11 +217,6 @@ class SynoApi: ) self.surveillance_station = self.dsm.surveillance_station - def _set_system_loaded(self, state: bool = False) -> None: - """Set system loaded flag.""" - dsm_device = self._hass.data[DOMAIN].get(self.information.serial) - dsm_device[SYSTEM_LOADED] = state - async def _syno_api_executer(self, api_call: Callable) -> None: """Synology api call wrapper.""" try: @@ -235,12 +230,10 @@ class SynoApi: async def async_reboot(self) -> None: """Reboot NAS.""" await self._syno_api_executer(self.system.reboot) - self._set_system_loaded() async def async_shutdown(self) -> None: """Shutdown NAS.""" await self._syno_api_executer(self.system.shutdown) - self._set_system_loaded() async def async_unload(self) -> None: """Stop interacting with the NAS and prepare for removal from hass.""" diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index f716130a5e4..c5c9e590684 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -2,6 +2,15 @@ from __future__ import annotations from synology_dsm.api.surveillance_station.const import SNAPSHOT_PROFILE_BALANCED +from synology_dsm.exceptions import ( + SynologyDSMAPIErrorException, + SynologyDSMLogin2SARequiredException, + SynologyDSMLoginDisabledAccountException, + SynologyDSMLoginFailedException, + SynologyDSMLoginInvalidException, + SynologyDSMLoginPermissionDeniedException, + SynologyDSMRequestException, +) from homeassistant.const import Platform @@ -15,17 +24,9 @@ PLATFORMS = [ Platform.SWITCH, Platform.UPDATE, ] -COORDINATOR_CAMERAS = "coordinator_cameras" -COORDINATOR_CENTRAL = "coordinator_central" -COORDINATOR_SWITCHES = "coordinator_switches" -SYSTEM_LOADED = "system_loaded" EXCEPTION_DETAILS = "details" EXCEPTION_UNKNOWN = "unknown" -# Entry keys -SYNO_API = "syno_api" -UNDO_UPDATE_LISTENER = "undo_update_listener" - # Configuration CONF_SERIAL = "serial" CONF_VOLUMES = "volumes" @@ -53,3 +54,16 @@ SERVICES = [ SERVICE_REBOOT, SERVICE_SHUTDOWN, ] + +SYNOLOGY_AUTH_FAILED_EXCEPTIONS = ( + SynologyDSMLogin2SARequiredException, + SynologyDSMLoginDisabledAccountException, + SynologyDSMLoginInvalidException, + SynologyDSMLoginPermissionDeniedException, +) + +SYNOLOGY_CONNECTION_EXCEPTIONS = ( + SynologyDSMAPIErrorException, + SynologyDSMLoginFailedException, + SynologyDSMRequestException, +) diff --git a/homeassistant/components/synology_dsm/coordinator.py b/homeassistant/components/synology_dsm/coordinator.py new file mode 100644 index 00000000000..332efb50bc8 --- /dev/null +++ b/homeassistant/components/synology_dsm/coordinator.py @@ -0,0 +1,148 @@ +"""synology_dsm coordinators.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +import async_timeout +from synology_dsm.api.surveillance_station.camera import SynoCamera +from synology_dsm.exceptions import SynologyDSMAPIErrorException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_SCAN_INTERVAL +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .common import SynoApi +from .const import ( + DEFAULT_SCAN_INTERVAL, + SIGNAL_CAMERA_SOURCE_CHANGED, + SYNOLOGY_CONNECTION_EXCEPTIONS, +) + +_LOGGER = logging.getLogger(__name__) + + +class SynologyDSMUpdateCoordinator(DataUpdateCoordinator): + """DataUpdateCoordinator base class for synology_dsm.""" + + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + api: SynoApi, + update_interval: timedelta, + ) -> None: + """Initialize synology_dsm DataUpdateCoordinator.""" + self.api = api + self.entry = entry + super().__init__( + hass, + _LOGGER, + name=f"{entry.title} {self.__class__.__name__}", + update_interval=update_interval, + ) + + +class SynologyDSMSwitchUpdateCoordinator(SynologyDSMUpdateCoordinator): + """DataUpdateCoordinator to gather data for a synology_dsm switch devices.""" + + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + api: SynoApi, + ) -> None: + """Initialize DataUpdateCoordinator for switch devices.""" + super().__init__(hass, entry, api, timedelta(seconds=30)) + self.version: str | None = None + + async def async_setup(self) -> None: + """Set up the coordinator initial data.""" + info = await self.hass.async_add_executor_job( + self.api.dsm.surveillance_station.get_info + ) + self.version = info["data"]["CMSMinVersion"] + + async def _async_update_data(self) -> dict[str, dict[str, SynoCamera]] | None: + """Fetch all data from api.""" + surveillance_station = self.api.surveillance_station + return { + "switches": { + "home_mode": await self.hass.async_add_executor_job( + surveillance_station.get_home_mode_status + ) + } + } + + +class SynologyDSMCentralUpdateCoordinator(SynologyDSMUpdateCoordinator): + """DataUpdateCoordinator to gather data for a synology_dsm central device.""" + + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + api: SynoApi, + ) -> None: + """Initialize DataUpdateCoordinator for central device.""" + super().__init__( + hass, + entry, + api, + timedelta( + minutes=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + ), + ) + + async def _async_update_data(self) -> dict[str, dict[str, SynoCamera]] | None: + """Fetch all data from api.""" + try: + await self.api.async_update() + except SYNOLOGY_CONNECTION_EXCEPTIONS as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + return None + + +class SynologyDSMCameraUpdateCoordinator(SynologyDSMUpdateCoordinator): + """DataUpdateCoordinator to gather data for a synology_dsm cameras.""" + + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + api: SynoApi, + ) -> None: + """Initialize DataUpdateCoordinator for cameras.""" + super().__init__(hass, entry, api, timedelta(seconds=30)) + + async def _async_update_data(self) -> dict[str, dict[str, SynoCamera]] | None: + """Fetch all camera data from api.""" + surveillance_station = self.api.surveillance_station + current_data: dict[str, SynoCamera] = { + camera.id: camera for camera in surveillance_station.get_all_cameras() + } + + try: + async with async_timeout.timeout(30): + await self.hass.async_add_executor_job(surveillance_station.update) + except SynologyDSMAPIErrorException as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + + new_data: dict[str, SynoCamera] = { + camera.id: camera for camera in surveillance_station.get_all_cameras() + } + + for cam_id, cam_data_new in new_data.items(): + if ( + (cam_data_current := current_data.get(cam_id)) is not None + and cam_data_current.live_view.rtsp != cam_data_new.live_view.rtsp + ): + async_dispatcher_send( + self.hass, + f"{SIGNAL_CAMERA_SOURCE_CHANGED}_{self.entry.entry_id}_{cam_id}", + cam_data_new.live_view.rtsp, + ) + + return {"cameras": new_data} diff --git a/homeassistant/components/synology_dsm/diagnostics.py b/homeassistant/components/synology_dsm/diagnostics.py index 8709170a6f8..485a44b290a 100644 --- a/homeassistant/components/synology_dsm/diagnostics.py +++ b/homeassistant/components/synology_dsm/diagnostics.py @@ -8,8 +8,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from . import SynoApi -from .const import CONF_DEVICE_TOKEN, DOMAIN, SYNO_API, SYSTEM_LOADED +from .const import CONF_DEVICE_TOKEN, DOMAIN +from .models import SynologyDSMData TO_REDACT = {CONF_USERNAME, CONF_PASSWORD, CONF_DEVICE_TOKEN} @@ -18,8 +18,8 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict: """Return diagnostics for a config entry.""" - data: dict = hass.data[DOMAIN][entry.unique_id] - syno_api: SynoApi = data[SYNO_API] + data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] + syno_api = data.api dsm_info = syno_api.dsm.information diag_data = { @@ -36,7 +36,7 @@ async def async_get_config_entry_diagnostics( "surveillance_station": {"cameras": {}}, "upgrade": {}, "utilisation": {}, - "is_system_loaded": data[SYSTEM_LOADED], + "is_system_loaded": True, "api_details": { "fetching_entities": syno_api._fetching_entities, # pylint: disable=protected-access }, @@ -45,7 +45,7 @@ async def async_get_config_entry_diagnostics( if syno_api.network is not None: intf: dict for intf in syno_api.network.interfaces: - diag_data["network"]["interfaces"][intf["id"]] = { + diag_data["network"]["interfaces"][intf["id"]] = { # type: ignore[index] "type": intf["type"], "ip": intf["ip"], } @@ -53,7 +53,7 @@ async def async_get_config_entry_diagnostics( if syno_api.storage is not None: disk: dict for disk in syno_api.storage.disks: - diag_data["storage"]["disks"][disk["id"]] = { + diag_data["storage"]["disks"][disk["id"]] = { # type: ignore[index] "name": disk["name"], "vendor": disk["vendor"], "model": disk["model"], @@ -64,7 +64,7 @@ async def async_get_config_entry_diagnostics( volume: dict for volume in syno_api.storage.volumes: - diag_data["storage"]["volumes"][volume["id"]] = { + diag_data["storage"]["volumes"][volume["id"]] = { # type: ignore[index] "name": volume["fs_type"], "size": volume["size"], } @@ -72,7 +72,7 @@ async def async_get_config_entry_diagnostics( if syno_api.surveillance_station is not None: camera: SynoCamera for camera in syno_api.surveillance_station.get_all_cameras(): - diag_data["surveillance_station"]["cameras"][camera.id] = { + diag_data["surveillance_station"]["cameras"][camera.id] = { # type: ignore[index] "name": camera.name, "is_enabled": camera.is_enabled, "is_motion_detection_enabled": camera.is_motion_detection_enabled, diff --git a/homeassistant/components/synology_dsm/models.py b/homeassistant/components/synology_dsm/models.py new file mode 100644 index 00000000000..8c4341a2d37 --- /dev/null +++ b/homeassistant/components/synology_dsm/models.py @@ -0,0 +1,21 @@ +"""The synology_dsm integration models.""" +from __future__ import annotations + +from dataclasses import dataclass + +from .common import SynoApi +from .coordinator import ( + SynologyDSMCameraUpdateCoordinator, + SynologyDSMCentralUpdateCoordinator, + SynologyDSMSwitchUpdateCoordinator, +) + + +@dataclass +class SynologyDSMData: + """Data for the synology_dsm integration.""" + + api: SynoApi + coordinator_central: SynologyDSMCentralUpdateCoordinator + coordinator_cameras: SynologyDSMCameraUpdateCoordinator | None + coordinator_switches: SynologyDSMSwitchUpdateCoordinator | None diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 6015dc689b7..6a2a92b9fd5 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -31,12 +31,13 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.dt import utcnow from . import SynoApi -from .const import CONF_VOLUMES, COORDINATOR_CENTRAL, DOMAIN, ENTITY_UNIT_LOAD, SYNO_API +from .const import CONF_VOLUMES, DOMAIN, ENTITY_UNIT_LOAD from .entity import ( SynologyDSMBaseEntity, SynologyDSMDeviceEntity, SynologyDSMEntityDescription, ) +from .models import SynologyDSMData @dataclass @@ -279,10 +280,9 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Synology NAS Sensor.""" - - data = hass.data[DOMAIN][entry.unique_id] - api: SynoApi = data[SYNO_API] - coordinator = data[COORDINATOR_CENTRAL] + data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] + api = data.api + coordinator = data.coordinator_central entities: list[SynoDSMUtilSensor | SynoDSMStorageSensor | SynoDSMInfoSensor] = [ SynoDSMUtilSensor(api, coordinator, description) diff --git a/homeassistant/components/synology_dsm/service.py b/homeassistant/components/synology_dsm/service.py index 130ad110b46..0cb2bf7d822 100644 --- a/homeassistant/components/synology_dsm/service.py +++ b/homeassistant/components/synology_dsm/service.py @@ -7,15 +7,8 @@ from synology_dsm.exceptions import SynologyDSMException from homeassistant.core import HomeAssistant, ServiceCall -from .common import SynoApi -from .const import ( - CONF_SERIAL, - DOMAIN, - SERVICE_REBOOT, - SERVICE_SHUTDOWN, - SERVICES, - SYNO_API, -) +from .const import CONF_SERIAL, DOMAIN, SERVICE_REBOOT, SERVICE_SHUTDOWN, SERVICES +from .models import SynologyDSMData LOGGER = logging.getLogger(__name__) @@ -29,7 +22,7 @@ async def async_setup_services(hass: HomeAssistant) -> None: dsm_devices = hass.data[DOMAIN] if serial: - dsm_device = dsm_devices.get(serial) + dsm_device: SynologyDSMData = hass.data[DOMAIN][serial] elif len(dsm_devices) == 1: dsm_device = next(iter(dsm_devices.values())) serial = next(iter(dsm_devices)) @@ -45,7 +38,7 @@ async def async_setup_services(hass: HomeAssistant) -> None: return if call.service in [SERVICE_REBOOT, SERVICE_SHUTDOWN]: - if not (dsm_device := hass.data[DOMAIN].get(serial)): + if serial not in hass.data[DOMAIN]: LOGGER.error("DSM with specified serial %s not found", serial) return LOGGER.debug("%s DSM with serial %s", call.service, serial) @@ -53,7 +46,8 @@ async def async_setup_services(hass: HomeAssistant) -> None: "The %s service is deprecated and will be removed in future release. Please use the corresponding button entity", call.service, ) - dsm_api: SynoApi = dsm_device[SYNO_API] + dsm_device = hass.data[DOMAIN][serial] + dsm_api = dsm_device.api try: await getattr(dsm_api, f"async_{call.service}")() except SynologyDSMException as ex: diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index eb61b8334ca..26909ceddd9 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -15,8 +15,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import SynoApi -from .const import COORDINATOR_SWITCHES, DOMAIN, SYNO_API +from .const import DOMAIN from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription +from .models import SynologyDSMData _LOGGER = logging.getLogger(__name__) @@ -42,30 +43,16 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Synology NAS switch.""" - - data = hass.data[DOMAIN][entry.unique_id] - api: SynoApi = data[SYNO_API] - - entities = [] - - if SynoSurveillanceStation.INFO_API_KEY in api.dsm.apis: - info = await hass.async_add_executor_job(api.dsm.surveillance_station.get_info) - version = info["data"]["CMSMinVersion"] - - # initial data fetch - coordinator: DataUpdateCoordinator = data[COORDINATOR_SWITCHES] - await coordinator.async_refresh() - entities.extend( - [ - SynoDSMSurveillanceHomeModeToggle( - api, version, coordinator, description - ) - for description in SURVEILLANCE_SWITCH - ] + data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] + if coordinator := data.coordinator_switches: + assert coordinator.version is not None + async_add_entities( + SynoDSMSurveillanceHomeModeToggle( + data.api, coordinator.version, coordinator, description + ) + for description in SURVEILLANCE_SWITCH ) - async_add_entities(entities, True) - class SynoDSMSurveillanceHomeModeToggle(SynologyDSMBaseEntity, SwitchEntity): """Representation a Synology Surveillance Station Home Mode toggle.""" diff --git a/homeassistant/components/synology_dsm/update.py b/homeassistant/components/synology_dsm/update.py index 48b3eeca2ed..d3f3cc56eac 100644 --- a/homeassistant/components/synology_dsm/update.py +++ b/homeassistant/components/synology_dsm/update.py @@ -13,9 +13,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import SynoApi -from .const import COORDINATOR_CENTRAL, DOMAIN, SYNO_API +from .const import DOMAIN from .entity import SynologyDSMBaseEntity, SynologyDSMEntityDescription +from .models import SynologyDSMData @dataclass @@ -39,12 +39,9 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Synology DSM update entities.""" - data = hass.data[DOMAIN][entry.unique_id] - api: SynoApi = data[SYNO_API] - coordinator = data[COORDINATOR_CENTRAL] - + data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] async_add_entities( - SynoDSMUpdateEntity(api, coordinator, description) + SynoDSMUpdateEntity(data.api, data.coordinator_central, description) for description in UPDATE_ENTITIES ) diff --git a/tests/components/synology_dsm/test_init.py b/tests/components/synology_dsm/test_init.py index 4d6708a2e79..db373f41656 100644 --- a/tests/components/synology_dsm/test_init.py +++ b/tests/components/synology_dsm/test_init.py @@ -24,9 +24,9 @@ from tests.common import MockConfigEntry @pytest.mark.no_bypass_setup async def test_services_registered(hass: HomeAssistant): """Test if all services are registered.""" - with patch( - "homeassistant.components.synology_dsm.SynoApi.async_setup", return_value=True - ), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]): + with patch("homeassistant.components.synology_dsm.common.SynologyDSM"), patch( + "homeassistant.components.synology_dsm.PLATFORMS", return_value=[] + ): entry = MockConfigEntry( domain=DOMAIN, data={ From e67aa09bf24a90c090fec4cee5d03d708b2218c2 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Thu, 9 Jun 2022 22:40:01 +0100 Subject: [PATCH 1383/3516] Add zeroconf discovery to hive (#73290) Co-authored-by: Dave T --- homeassistant/components/hive/manifest.json | 3 +++ homeassistant/generated/zeroconf.py | 1 + 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index d8cd56abe0b..6e76826d305 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -2,6 +2,9 @@ "domain": "hive", "name": "Hive", "config_flow": true, + "homekit": { + "models": ["HHKBridge*"] + }, "documentation": "https://www.home-assistant.io/integrations/hive", "requirements": ["pyhiveapi==0.5.5"], "codeowners": ["@Rendili", "@KJonline"], diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 692132c9a75..8d4f1148582 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -419,6 +419,7 @@ HOMEKIT = { "C105X": "roku", "C135X": "roku", "EB-*": "ecobee", + "HHKBridge*": "hive", "Healty Home Coach": "netatmo", "Iota": "abode", "LIFX A19": "lifx", From c74159109ac7ad9a24346f5fd7b1628d036cb0d6 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 10 Jun 2022 00:25:26 +0000 Subject: [PATCH 1384/3516] [ci skip] Translation update --- homeassistant/components/google/translations/de.json | 3 +++ homeassistant/components/google/translations/el.json | 3 +++ homeassistant/components/google/translations/ja.json | 3 +++ homeassistant/components/google/translations/no.json | 3 +++ homeassistant/components/google/translations/pt-BR.json | 3 +++ 5 files changed, 15 insertions(+) diff --git a/homeassistant/components/google/translations/de.json b/homeassistant/components/google/translations/de.json index c75cc90eb13..2111e0c8bab 100644 --- a/homeassistant/components/google/translations/de.json +++ b/homeassistant/components/google/translations/de.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "Folge den [Anweisungen]({more_info_url}) f\u00fcr den [OAuth-Zustimmungsbildschirm]({oauth_consent_url}), um Home Assistant Zugriff auf deinen Google-Kalender zu geben. Du musst auch Anwendungsnachweise erstellen, die mit deinem Kalender verkn\u00fcpft sind:\n1. Gehe zu [Credentials]({oauth_creds_url}) und klicke auf **Create Credentials**.\n1. W\u00e4hle in der Dropdown-Liste **OAuth-Client-ID**.\n1. W\u00e4hle **TV und eingeschr\u00e4nkte Eingabeger\u00e4te** f\u00fcr den Anwendungstyp.\n\n" + }, "config": { "abort": { "already_configured": "Konto wurde bereits konfiguriert", diff --git a/homeassistant/components/google/translations/el.json b/homeassistant/components/google/translations/el.json index dd93a5ab3c8..fdcf17d26ce 100644 --- a/homeassistant/components/google/translations/el.json +++ b/homeassistant/components/google/translations/el.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "\u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 [\u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2]({more_info_url}) \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd [\u03bf\u03b8\u03cc\u03bd\u03b7 \u03c3\u03c5\u03bd\u03b1\u03af\u03bd\u03b5\u03c3\u03b7\u03c2 OAuth]({oauth_consent_url}) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03bf\u03bd \u0392\u03bf\u03b7\u03b8\u03cc Home \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03cc\u03b3\u03b9\u03bf Google \u03c3\u03b1\u03c2. \u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03b5\u03c0\u03af\u03c3\u03b7\u03c2 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u0394\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u0395\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03bf \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03cc\u03b3\u03b9\u03cc \u03c3\u03b1\u03c2:\n 1. \u039c\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b1 [\u0394\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1] ({oauth_creds_url}) \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae **\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03b7\u03c1\u03af\u03c9\u03bd**.\n 1. \u0391\u03c0\u03cc \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03c0\u03c4\u03c5\u03c3\u03c3\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 **OAuth \u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7**.\n 1. \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 **\u03a4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 \u03ba\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5** \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03a4\u03cd\u03c0\u03bf \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2. \n\n" + }, "config": { "abort": { "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", diff --git a/homeassistant/components/google/translations/ja.json b/homeassistant/components/google/translations/ja.json index 854e7ba1961..d5b52c197a8 100644 --- a/homeassistant/components/google/translations/ja.json +++ b/homeassistant/components/google/translations/ja.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "[OAuth\u540c\u610f\u753b\u9762] ({more_info_url}) \u306e\u624b\u9806 ({oauth_consent_url}) \u306b\u5f93\u3063\u3066\u3001Home Assistant\u304c\u3042\u306a\u305f\u306eGoogle\u30ab\u30ec\u30f3\u30c0\u30fc\u306b\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002\u307e\u305f\u3001\u30ab\u30ec\u30f3\u30c0\u30fc\u306b\u30ea\u30f3\u30af\u3057\u305f\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3092\u4f5c\u6210\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n 1. [\u8cc7\u683c\u60c5\u5831]({oauth_creds_url} \u306b\u79fb\u52d5\u3057\u3001**Create Credentials** \u3092\u30af\u30ea\u30c3\u30af\u3057\u307e\u3059\u3002\n 1. \u30c9\u30ed\u30c3\u30d7\u30c0\u30a6\u30f3\u30ea\u30b9\u30c8\u304b\u3089 **OAuth client ID** \u3092\u9078\u629e\u3057\u307e\u3059\u3002\n 1. \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u30bf\u30a4\u30d7\u3068\u3057\u3066**TV\u304a\u3088\u3073\u5236\u9650\u4ed8\u304d\u5165\u529b\u30c7\u30d0\u30a4\u30b9** \u3092\u9078\u629e\u3057\u307e\u3059\u3002\n\n" + }, "config": { "abort": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", diff --git a/homeassistant/components/google/translations/no.json b/homeassistant/components/google/translations/no.json index ef65c7fe9a5..4fcbeb1dae8 100644 --- a/homeassistant/components/google/translations/no.json +++ b/homeassistant/components/google/translations/no.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "F\u00f8lg [instruksjonene]( {more_info_url} ) for [OAuth-samtykkeskjermen]( {oauth_consent_url} ) for \u00e5 gi Home Assistant tilgang til Google-kalenderen din. Du m\u00e5 ogs\u00e5 opprette applikasjonslegitimasjon knyttet til kalenderen din:\n 1. G\u00e5 til [Credentials]( {oauth_creds_url} ) og klikk p\u00e5 **Create Credentials**.\n 1. Velg **OAuth-klient-ID** fra rullegardinlisten.\n 1. Velg **TV og begrensede inngangsenheter** for applikasjonstype. " + }, "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", diff --git a/homeassistant/components/google/translations/pt-BR.json b/homeassistant/components/google/translations/pt-BR.json index 85c7254b9a7..381976e0284 100644 --- a/homeassistant/components/google/translations/pt-BR.json +++ b/homeassistant/components/google/translations/pt-BR.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "Siga as [instru\u00e7\u00f5es]( {more_info_url} ) para [tela de consentimento da OAuth]( {oauth_consent_url} ) para conceder ao Home Assistant acesso ao seu Google Agenda. Voc\u00ea tamb\u00e9m precisa criar credenciais de aplicativo vinculadas ao seu calend\u00e1rio:\n 1. Acesse [Credentials]( {oauth_creds_url} ) e clique em **Create Credentials**.\n 1. Na lista suspensa, selecione **ID do cliente OAuth**.\n 1. Selecione **TV e dispositivos de entrada limitada** para o tipo de aplicativo. \n\n" + }, "config": { "abort": { "already_configured": "A conta j\u00e1 foi configurada", From 15aecbb6efd7afdc0e317ca81b02eb423953b826 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 9 Jun 2022 23:32:16 -0400 Subject: [PATCH 1385/3516] Bumps version of pyunifiprotect to 3.9.2 to fix compat with protect 2.1.1 (#73299) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index a27c0125da3..199298d76ca 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.9.1", "unifi-discovery==1.1.3"], + "requirements": ["pyunifiprotect==3.9.2", "unifi-discovery==1.1.3"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 369cedf3d05..4c749f75456 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1993,7 +1993,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.9.1 +pyunifiprotect==3.9.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f68ac122bd2..51df9de4d4f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1322,7 +1322,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.9.1 +pyunifiprotect==3.9.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 2f106112dfd15c4f9b9adef9be7ea686a91807ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Jun 2022 17:48:07 -1000 Subject: [PATCH 1386/3516] Add async_remove_config_entry_device to synology_dsm (#73293) --- .../components/synology_dsm/__init__.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index d8d768d36e4..e6868491eae 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -1,9 +1,11 @@ """The Synology DSM component.""" from __future__ import annotations +from itertools import chain import logging from synology_dsm.api.surveillance_station import SynoSurveillanceStation +from synology_dsm.api.surveillance_station.camera import SynoCamera from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC, CONF_VERIFY_SSL @@ -137,3 +139,28 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) + + +async def async_remove_config_entry_device( + hass: HomeAssistant, entry: ConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove synology_dsm config entry from a device.""" + data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] + api = data.api + serial = api.information.serial + storage = api.storage + # get_all_cameras does not do I/O + all_cameras: list[SynoCamera] = api.surveillance_station.get_all_cameras() + device_ids = chain( + (camera.id for camera in all_cameras), + storage.volumes_ids, + storage.disks_ids, + storage.volumes_ids, + (SynoSurveillanceStation.INFO_API_KEY,), # Camera home/away + ) + return not device_entry.identifiers.intersection( + ( + (DOMAIN, serial), # Base device + *((DOMAIN, f"{serial}_{id}") for id in device_ids), # Storage and cameras + ) + ) From 211e5432ac304a28f3503c42908ced60db3f58f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Jun 2022 17:49:02 -1000 Subject: [PATCH 1387/3516] Add EVENT_USER_UPDATED (#71965) --- homeassistant/auth/__init__.py | 3 +++ tests/auth/test_init.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 23fdb775a8a..12511a7f4a5 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -20,6 +20,7 @@ from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config from .providers import AuthProvider, LoginFlow, auth_provider_from_config EVENT_USER_ADDED = "user_added" +EVENT_USER_UPDATED = "user_updated" EVENT_USER_REMOVED = "user_removed" _MfaModuleDict = dict[str, MultiFactorAuthModule] @@ -338,6 +339,8 @@ class AuthManager: else: await self.async_deactivate_user(user) + self.hass.bus.async_fire(EVENT_USER_UPDATED, {"user_id": user.id}) + async def async_activate_user(self, user: models.User) -> None: """Activate a user.""" await self._store.async_activate_user(user) diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index 53c2a4261ae..22d720da587 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant import auth, data_entry_flow from homeassistant.auth import ( + EVENT_USER_UPDATED, InvalidAuthError, auth_store, const as auth_const, @@ -1097,3 +1098,20 @@ async def test_rename_does_not_change_refresh_token(mock_hass): token_after = list(user.refresh_tokens.values())[0] assert token_before == token_after + + +async def test_event_user_updated_fires(hass): + """Test the user updated event fires.""" + manager = await auth.auth_manager_from_config(hass, [], []) + user = MockUser().add_to_auth_manager(manager) + await manager.async_create_refresh_token(user, CLIENT_ID) + + assert len(list(user.refresh_tokens.values())) == 1 + + events = async_capture_events(hass, EVENT_USER_UPDATED) + + await manager.async_update_user(user, name="new name") + assert user.name == "new name" + + await hass.async_block_till_done() + assert len(events) == 1 From d3f01f7ea92e3a3866db4bc4308374fe131630f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Jun 2022 17:49:37 -1000 Subject: [PATCH 1388/3516] Reduce memory pressure from history_stats with large data sets (#73289) --- .../components/history_stats/data.py | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 3b17c715c97..33f32e72292 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -23,6 +23,14 @@ class HistoryStatsState: period: tuple[datetime.datetime, datetime.datetime] +@dataclass +class HistoryState: + """A minimal state to avoid holding on to State objects.""" + + state: str + last_changed: float + + class HistoryStats: """Manage history stats.""" @@ -40,7 +48,7 @@ class HistoryStats: self.entity_id = entity_id self._period = (MIN_TIME_UTC, MIN_TIME_UTC) self._state: HistoryStatsState = HistoryStatsState(None, None, self._period) - self._history_current_period: list[State] = [] + self._history_current_period: list[HistoryState] = [] self._previous_run_before_start = False self._entity_states = set(entity_states) self._duration = duration @@ -103,20 +111,18 @@ class HistoryStats: <= floored_timestamp(new_state.last_changed) <= current_period_end_timestamp ): - self._history_current_period.append(new_state) + self._history_current_period.append( + HistoryState( + new_state.state, new_state.last_changed.timestamp() + ) + ) new_data = True if not new_data and current_period_end_timestamp < now_timestamp: # If period has not changed and current time after the period end... # Don't compute anything as the value cannot have changed return self._state else: - self._history_current_period = await get_instance( - self.hass - ).async_add_executor_job( - self._update_from_database, - current_period_start, - current_period_end, - ) + await self._async_history_from_db(current_period_start, current_period_end) self._previous_run_before_start = False hours_matched, match_count = self._async_compute_hours_and_changes( @@ -127,7 +133,24 @@ class HistoryStats: self._state = HistoryStatsState(hours_matched, match_count, self._period) return self._state - def _update_from_database( + async def _async_history_from_db( + self, + current_period_start: datetime.datetime, + current_period_end: datetime.datetime, + ) -> None: + """Update history data for the current period from the database.""" + instance = get_instance(self.hass) + states = await instance.async_add_executor_job( + self._state_changes_during_period, + current_period_start, + current_period_end, + ) + self._history_current_period = [ + HistoryState(state.state, state.last_changed.timestamp()) + for state in states + ] + + def _state_changes_during_period( self, start: datetime.datetime, end: datetime.datetime ) -> list[State]: return history.state_changes_during_period( @@ -155,9 +178,9 @@ class HistoryStats: match_count = 1 if previous_state_matches else 0 # Make calculations - for item in self._history_current_period: - current_state_matches = item.state in self._entity_states - state_change_timestamp = item.last_changed.timestamp() + for history_state in self._history_current_period: + current_state_matches = history_state.state in self._entity_states + state_change_timestamp = history_state.last_changed if previous_state_matches: elapsed += state_change_timestamp - last_state_change_timestamp From 1dd7781acc50d4b0bf9b9a2fd36400c94a94ec05 Mon Sep 17 00:00:00 2001 From: Khole Date: Fri, 10 Jun 2022 04:54:24 +0100 Subject: [PATCH 1389/3516] Hive auth fix for users (#73247) --- homeassistant/components/hive/config_flow.py | 7 +++-- homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hive/test_config_flow.py | 30 -------------------- 5 files changed, 8 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/hive/config_flow.py b/homeassistant/components/hive/config_flow.py index c713a3011f4..90c78aefcbd 100644 --- a/homeassistant/components/hive/config_flow.py +++ b/homeassistant/components/hive/config_flow.py @@ -27,6 +27,7 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.data = {} self.tokens = {} self.entry = None + self.device_registration = False async def async_step_user(self, user_input=None): """Prompt user input. Create or edit entry.""" @@ -88,6 +89,7 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not errors: try: + self.device_registration = True return await self.async_setup_hive_entry() except UnknownHiveError: errors["base"] = "unknown" @@ -102,9 +104,10 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): raise UnknownHiveError # Setup the config entry - await self.hive_auth.device_registration("Home Assistant") + if self.device_registration: + await self.hive_auth.device_registration("Home Assistant") + self.data["device_data"] = await self.hive_auth.getDeviceData() self.data["tokens"] = self.tokens - self.data["device_data"] = await self.hive_auth.getDeviceData() if self.context["source"] == config_entries.SOURCE_REAUTH: self.hass.config_entries.async_update_entry( self.entry, title=self.data["username"], data=self.data diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 6e76826d305..341273638bf 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -6,7 +6,7 @@ "models": ["HHKBridge*"] }, "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": ["pyhiveapi==0.5.5"], + "requirements": ["pyhiveapi==0.5.9"], "codeowners": ["@Rendili", "@KJonline"], "iot_class": "cloud_polling", "loggers": ["apyhiveapi"] diff --git a/requirements_all.txt b/requirements_all.txt index 4c749f75456..b71d68bdaea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1541,7 +1541,7 @@ pyheos==0.7.2 pyhik==0.3.0 # homeassistant.components.hive -pyhiveapi==0.5.5 +pyhiveapi==0.5.9 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51df9de4d4f..c7ce45f3db3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1032,7 +1032,7 @@ pyhaversion==22.4.1 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.5.5 +pyhiveapi==0.5.9 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/tests/components/hive/test_config_flow.py b/tests/components/hive/test_config_flow.py index 51ceec43ad2..35e20e8eee3 100644 --- a/tests/components/hive/test_config_flow.py +++ b/tests/components/hive/test_config_flow.py @@ -33,16 +33,6 @@ async def test_import_flow(hass): "AccessToken": "mock-access-token", }, }, - ), patch( - "homeassistant.components.hive.config_flow.Auth.device_registration", - return_value=True, - ), patch( - "homeassistant.components.hive.config_flow.Auth.getDeviceData", - return_value=[ - "mock-device-group-key", - "mock-device-key", - "mock-device-password", - ], ), patch( "homeassistant.components.hive.async_setup", return_value=True ) as mock_setup, patch( @@ -67,11 +57,6 @@ async def test_import_flow(hass): }, "ChallengeName": "SUCCESS", }, - "device_data": [ - "mock-device-group-key", - "mock-device-key", - "mock-device-password", - ], } assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 @@ -96,16 +81,6 @@ async def test_user_flow(hass): "AccessToken": "mock-access-token", }, }, - ), patch( - "homeassistant.components.hive.config_flow.Auth.device_registration", - return_value=True, - ), patch( - "homeassistant.components.hive.config_flow.Auth.getDeviceData", - return_value=[ - "mock-device-group-key", - "mock-device-key", - "mock-device-password", - ], ), patch( "homeassistant.components.hive.async_setup", return_value=True ) as mock_setup, patch( @@ -130,11 +105,6 @@ async def test_user_flow(hass): }, "ChallengeName": "SUCCESS", }, - "device_data": [ - "mock-device-group-key", - "mock-device-key", - "mock-device-password", - ], } assert len(mock_setup.mock_calls) == 1 From 5863d57e734fda4b9146bd6fe8b7d6ce88f7fe9b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Jun 2022 17:56:58 -1000 Subject: [PATCH 1390/3516] Add strict typing to homekit locks (#73264) --- .strict-typing | 1 + homeassistant/components/homekit/type_locks.py | 10 ++++++---- mypy.ini | 11 +++++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.strict-typing b/.strict-typing index 264b28408f9..f81b6249fed 100644 --- a/.strict-typing +++ b/.strict-typing @@ -114,6 +114,7 @@ homeassistant.components.homekit.aidmanager homeassistant.components.homekit.config_flow homeassistant.components.homekit.diagnostics homeassistant.components.homekit.logbook +homeassistant.components.homekit.type_locks homeassistant.components.homekit.type_triggers homeassistant.components.homekit.util homeassistant.components.homekit_controller diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index af7501e1869..18dfe48b2bd 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -1,5 +1,6 @@ """Class to hold all lock accessories.""" import logging +from typing import Any from pyhap.const import CATEGORY_DOOR_LOCK @@ -12,7 +13,7 @@ from homeassistant.components.lock import ( STATE_UNLOCKING, ) from homeassistant.const import ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN -from homeassistant.core import callback +from homeassistant.core import State, callback from .accessories import TYPES, HomeAccessory from .const import CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE, SERV_LOCK @@ -59,11 +60,12 @@ class Lock(HomeAccessory): The lock entity must support: unlock and lock. """ - def __init__(self, *args): + def __init__(self, *args: Any) -> None: """Initialize a Lock accessory object.""" super().__init__(*args, category=CATEGORY_DOOR_LOCK) self._code = self.config.get(ATTR_CODE) state = self.hass.states.get(self.entity_id) + assert state is not None serv_lock_mechanism = self.add_preload_service(SERV_LOCK) self.char_current_state = serv_lock_mechanism.configure_char( @@ -76,7 +78,7 @@ class Lock(HomeAccessory): ) self.async_update_state(state) - def set_state(self, value): + def set_state(self, value: int) -> None: """Set lock state to value if call came from HomeKit.""" _LOGGER.debug("%s: Set state to %d", self.entity_id, value) @@ -89,7 +91,7 @@ class Lock(HomeAccessory): self.async_call_service(DOMAIN, service, params) @callback - def async_update_state(self, new_state): + def async_update_state(self, new_state: State) -> None: """Update lock after state changed.""" hass_state = new_state.state current_lock_state = HASS_TO_HOMEKIT_CURRENT.get( diff --git a/mypy.ini b/mypy.ini index 522cda67e79..11a2d83ec40 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1017,6 +1017,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.homekit.type_locks] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.homekit.type_triggers] check_untyped_defs = true disallow_incomplete_defs = true From a9ab98fb4519975636cb5aa29bab26d9b42c83d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Jun 2022 18:10:15 -1000 Subject: [PATCH 1391/3516] Add power sensor to WiZ (#73260) --- homeassistant/components/wiz/__init__.py | 10 ++++-- homeassistant/components/wiz/entity.py | 9 +++-- homeassistant/components/wiz/manifest.json | 2 +- homeassistant/components/wiz/sensor.py | 38 ++++++++++++++++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wiz/__init__.py | 13 ++++++++ tests/components/wiz/test_sensor.py | 30 +++++++++++++++++ 8 files changed, 95 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/wiz/__init__.py b/homeassistant/components/wiz/__init__.py index 104ecb6f0c5..b47db32d90f 100644 --- a/homeassistant/components/wiz/__init__.py +++ b/homeassistant/components/wiz/__init__.py @@ -1,4 +1,6 @@ """WiZ Platform integration.""" +from __future__ import annotations + import asyncio from datetime import timedelta import logging @@ -80,10 +82,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "Found bulb {bulb.mac} at {ip_address}, expected {entry.unique_id}" ) - async def _async_update() -> None: + async def _async_update() -> float | None: """Update the WiZ device.""" try: await bulb.updateState() + if bulb.power_monitoring is not False: + power: float | None = await bulb.get_power() + return power + return None except WIZ_EXCEPTIONS as ex: raise UpdateFailed(f"Failed to update device at {ip_address}: {ex}") from ex @@ -117,7 +123,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def _async_push_update(state: PilotParser) -> None: """Receive a push update.""" _LOGGER.debug("%s: Got push update: %s", bulb.mac, state.pilotResult) - coordinator.async_set_updated_data(None) + coordinator.async_set_updated_data(coordinator.data) if state.get_source() == PIR_SOURCE: async_dispatcher_send(hass, SIGNAL_WIZ_PIR.format(bulb.mac)) diff --git a/homeassistant/components/wiz/entity.py b/homeassistant/components/wiz/entity.py index 9b22d35de7d..c78f3e3b37b 100644 --- a/homeassistant/components/wiz/entity.py +++ b/homeassistant/components/wiz/entity.py @@ -2,7 +2,7 @@ from __future__ import annotations from abc import abstractmethod -from typing import Any +from typing import Any, Optional from pywizlight.bulblibrary import BulbType @@ -10,12 +10,15 @@ from homeassistant.const import ATTR_HW_VERSION, ATTR_MODEL from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo, Entity, ToggleEntity -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .models import WizData -class WizEntity(CoordinatorEntity, Entity): +class WizEntity(CoordinatorEntity[DataUpdateCoordinator[Optional[float]]], Entity): """Representation of WiZ entity.""" def __init__(self, wiz_data: WizData, name: str) -> None: diff --git a/homeassistant/components/wiz/manifest.json b/homeassistant/components/wiz/manifest.json index a3537d3cbd9..c2eb6fe1b53 100644 --- a/homeassistant/components/wiz/manifest.json +++ b/homeassistant/components/wiz/manifest.json @@ -13,7 +13,7 @@ "dependencies": ["network"], "quality_scale": "platinum", "documentation": "https://www.home-assistant.io/integrations/wiz", - "requirements": ["pywizlight==0.5.13"], + "requirements": ["pywizlight==0.5.14"], "iot_class": "local_push", "codeowners": ["@sbidy"] } diff --git a/homeassistant/components/wiz/sensor.py b/homeassistant/components/wiz/sensor.py index d16130883d5..11f3933fd16 100644 --- a/homeassistant/components/wiz/sensor.py +++ b/homeassistant/components/wiz/sensor.py @@ -8,7 +8,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT +from homeassistant.const import POWER_WATT, SIGNAL_STRENGTH_DECIBELS_MILLIWATT from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -30,6 +30,17 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ) +POWER_SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="power", + name="Current Power", + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=POWER_WATT, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -37,9 +48,17 @@ async def async_setup_entry( ) -> None: """Set up the wiz sensor.""" wiz_data: WizData = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + entities = [ WizSensor(wiz_data, entry.title, description) for description in SENSORS - ) + ] + if wiz_data.coordinator.data is not None: + entities.extend( + [ + WizPowerSensor(wiz_data, entry.title, description) + for description in POWER_SENSORS + ] + ) + async_add_entities(entities) class WizSensor(WizEntity, SensorEntity): @@ -63,3 +82,16 @@ class WizSensor(WizEntity, SensorEntity): self._attr_native_value = self._device.state.pilotResult.get( self.entity_description.key ) + + +class WizPowerSensor(WizSensor): + """Defines a WiZ power sensor.""" + + @callback + def _async_update_attrs(self) -> None: + """Handle updating _attr values.""" + # Newer firmwares will have the power in their state + watts_push = self._device.state.get_power() + # Older firmwares will be polled and in the coordinator data + watts_poll = self.coordinator.data + self._attr_native_value = watts_poll if watts_push is None else watts_push diff --git a/requirements_all.txt b/requirements_all.txt index b71d68bdaea..5f27d8c524a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2029,7 +2029,7 @@ pywemo==0.9.1 pywilight==0.0.70 # homeassistant.components.wiz -pywizlight==0.5.13 +pywizlight==0.5.14 # homeassistant.components.ws66i pyws66i==1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7ce45f3db3..900b8fda255 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1349,7 +1349,7 @@ pywemo==0.9.1 pywilight==0.0.70 # homeassistant.components.wiz -pywizlight==0.5.13 +pywizlight==0.5.14 # homeassistant.components.ws66i pyws66i==1.1 diff --git a/tests/components/wiz/__init__.py b/tests/components/wiz/__init__.py index 62920662c6f..93033d984fa 100644 --- a/tests/components/wiz/__init__.py +++ b/tests/components/wiz/__init__.py @@ -150,6 +150,17 @@ FAKE_SOCKET = BulbType( white_channels=2, white_to_color_ratio=80, ) +FAKE_SOCKET_WITH_POWER_MONITORING = BulbType( + bulb_type=BulbClass.SOCKET, + name="ESP25_SOCKET_01", + features=Features( + color=False, color_tmp=False, effect=False, brightness=False, dual_head=False + ), + kelvin_range=KelvinRange(2700, 6500), + fw_version="1.26.2", + white_channels=2, + white_to_color_ratio=80, +) FAKE_OLD_FIRMWARE_DIMMABLE_BULB = BulbType( bulb_type=BulbClass.DW, name=None, @@ -197,7 +208,9 @@ def _mocked_wizlight(device, extended_white_range, bulb_type) -> wizlight: ) bulb.getMac = AsyncMock(return_value=FAKE_MAC) bulb.turn_on = AsyncMock() + bulb.get_power = AsyncMock(return_value=None) bulb.turn_off = AsyncMock() + bulb.power_monitoring = False bulb.updateState = AsyncMock(return_value=FAKE_STATE) bulb.getSupportedScenes = AsyncMock(return_value=list(SCENES.values())) bulb.start_push = AsyncMock(side_effect=_save_setup_callback) diff --git a/tests/components/wiz/test_sensor.py b/tests/components/wiz/test_sensor.py index 37a6b04dad3..a1eb6ded51d 100644 --- a/tests/components/wiz/test_sensor.py +++ b/tests/components/wiz/test_sensor.py @@ -1,11 +1,15 @@ """Tests for the sensor platform.""" +from unittest.mock import AsyncMock + from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import ( FAKE_DUAL_HEAD_RGBWW_BULB, FAKE_MAC, + FAKE_SOCKET_WITH_POWER_MONITORING, + _mocked_wizlight, _patch_discovery, _patch_wizlight, async_push_update, @@ -35,3 +39,29 @@ async def test_signal_strength(hass: HomeAssistant) -> None: await async_push_update(hass, bulb, {"mac": FAKE_MAC, "rssi": -50}) assert hass.states.get(entity_id).state == "-50" + + +async def test_power_monitoring(hass: HomeAssistant) -> None: + """Test power monitoring.""" + socket = _mocked_wizlight(None, None, FAKE_SOCKET_WITH_POWER_MONITORING) + socket.power_monitoring = None + socket.get_power = AsyncMock(return_value=5.123) + _, entry = await async_setup_integration( + hass, wizlight=socket, bulb_type=FAKE_SOCKET_WITH_POWER_MONITORING + ) + entity_id = "sensor.mock_title_current_power" + entity_registry = er.async_get(hass) + reg_entry = entity_registry.async_get(entity_id) + assert reg_entry.unique_id == f"{FAKE_MAC}_power" + updated_entity = entity_registry.async_update_entity( + entity_id=entity_id, disabled_by=None + ) + assert not updated_entity.disabled + + with _patch_discovery(), _patch_wizlight(device=socket): + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "5.123" + await async_push_update(hass, socket, {"mac": FAKE_MAC, "pc": 800}) + assert hass.states.get(entity_id).state == "0.8" From 0505c596a563c92def54ea8108be09a338a0dd53 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Jun 2022 18:11:23 -1000 Subject: [PATCH 1392/3516] Fix dropouts in history_stats graphs on restart (#73110) --- .../components/history_stats/sensor.py | 5 + tests/components/history_stats/test_sensor.py | 477 +++++++++--------- 2 files changed, 241 insertions(+), 241 deletions(-) diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index b0ce1a8fca5..a42c516f12b 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -21,6 +21,7 @@ from homeassistant.const import ( TIME_HOURS, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.reload import async_setup_reload_service @@ -101,6 +102,9 @@ async def async_setup_platform( history_stats = HistoryStats(hass, entity_id, entity_states, start, end, duration) coordinator = HistoryStatsUpdateCoordinator(hass, history_stats, name) + await coordinator.async_refresh() + if not coordinator.last_update_success: + raise PlatformNotReady from coordinator.last_exception async_add_entities([HistoryStatsSensor(coordinator, sensor_type, name)]) @@ -152,6 +156,7 @@ class HistoryStatsSensor(HistoryStatsSensorBase): super().__init__(coordinator, name) self._attr_native_unit_of_measurement = UNITS[sensor_type] self._type = sensor_type + self._process_update() @callback def _process_update(self) -> None: diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index f824ee552ca..8907f381a6c 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -9,7 +9,7 @@ import pytest from homeassistant import config as hass_config from homeassistant.components.history_stats import DOMAIN -from homeassistant.const import SERVICE_RELOAD, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import SERVICE_RELOAD, STATE_UNKNOWN import homeassistant.core as ha from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component, setup_component @@ -50,7 +50,7 @@ class TestHistoryStatsSensor(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get("sensor.test") - assert state.state == STATE_UNKNOWN + assert state.state == "0.0" def test_setup_multiple_states(self): """Test the history statistics sensor setup for multiple states.""" @@ -71,7 +71,7 @@ class TestHistoryStatsSensor(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get("sensor.test") - assert state.state == STATE_UNKNOWN + assert state.state == "0.0" def test_wrong_duration(self): """Test when duration value is not a timedelta.""" @@ -153,12 +153,12 @@ async def test_invalid_date_for_start(hass, recorder_mock): }, ) await hass.async_block_till_done() - assert hass.states.get("sensor.test").state == STATE_UNKNOWN + assert hass.states.get("sensor.test") is None next_update_time = dt_util.utcnow() + timedelta(minutes=1) with freeze_time(next_update_time): async_fire_time_changed(hass, next_update_time) await hass.async_block_till_done() - assert hass.states.get("sensor.test").state == STATE_UNAVAILABLE + assert hass.states.get("sensor.test") is None async def test_invalid_date_for_end(hass, recorder_mock): @@ -178,12 +178,12 @@ async def test_invalid_date_for_end(hass, recorder_mock): }, ) await hass.async_block_till_done() - assert hass.states.get("sensor.test").state == STATE_UNKNOWN + assert hass.states.get("sensor.test") is None next_update_time = dt_util.utcnow() + timedelta(minutes=1) with freeze_time(next_update_time): async_fire_time_changed(hass, next_update_time) await hass.async_block_till_done() - assert hass.states.get("sensor.test").state == STATE_UNAVAILABLE + assert hass.states.get("sensor.test") is None async def test_invalid_entity_in_template(hass, recorder_mock): @@ -203,12 +203,12 @@ async def test_invalid_entity_in_template(hass, recorder_mock): }, ) await hass.async_block_till_done() - assert hass.states.get("sensor.test").state == STATE_UNKNOWN + assert hass.states.get("sensor.test") is None next_update_time = dt_util.utcnow() + timedelta(minutes=1) with freeze_time(next_update_time): async_fire_time_changed(hass, next_update_time) await hass.async_block_till_done() - assert hass.states.get("sensor.test").state == STATE_UNAVAILABLE + assert hass.states.get("sensor.test") is None async def test_invalid_entity_returning_none_in_template(hass, recorder_mock): @@ -228,12 +228,12 @@ async def test_invalid_entity_returning_none_in_template(hass, recorder_mock): }, ) await hass.async_block_till_done() - assert hass.states.get("sensor.test").state == STATE_UNKNOWN + assert hass.states.get("sensor.test") is None next_update_time = dt_util.utcnow() + timedelta(minutes=1) with freeze_time(next_update_time): async_fire_time_changed(hass, next_update_time) await hass.async_block_till_done() - assert hass.states.get("sensor.test").state == STATE_UNAVAILABLE + assert hass.states.get("sensor.test") is None async def test_reload(hass, recorder_mock): @@ -302,56 +302,55 @@ async def test_measure_multiple(hass, recorder_mock): ] } - await async_setup_component( - hass, - "sensor", - { - "sensor": [ - { - "platform": "history_stats", - "entity_id": "input_select.test_id", - "name": "sensor1", - "state": ["orange", "blue"], - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ now() }}", - "type": "time", - }, - { - "platform": "history_stats", - "entity_id": "unknown.test_id", - "name": "sensor2", - "state": ["orange", "blue"], - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ now() }}", - "type": "time", - }, - { - "platform": "history_stats", - "entity_id": "input_select.test_id", - "name": "sensor3", - "state": ["orange", "blue"], - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ now() }}", - "type": "count", - }, - { - "platform": "history_stats", - "entity_id": "input_select.test_id", - "name": "sensor4", - "state": ["orange", "blue"], - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ now() }}", - "type": "ratio", - }, - ] - }, - ) - await hass.async_block_till_done() - with patch( "homeassistant.components.recorder.history.state_changes_during_period", _fake_states, ): + await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "history_stats", + "entity_id": "input_select.test_id", + "name": "sensor1", + "state": ["orange", "blue"], + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "unknown.test_id", + "name": "sensor2", + "state": ["orange", "blue"], + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "input_select.test_id", + "name": "sensor3", + "state": ["orange", "blue"], + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "count", + }, + { + "platform": "history_stats", + "entity_id": "input_select.test_id", + "name": "sensor4", + "state": ["orange", "blue"], + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "ratio", + }, + ] + }, + ) + await hass.async_block_till_done() for i in range(1, 5): await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() @@ -382,56 +381,55 @@ async def test_measure(hass, recorder_mock): ] } - await async_setup_component( - hass, - "sensor", - { - "sensor": [ - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor1", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ now() }}", - "type": "time", - }, - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor2", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ now() }}", - "type": "time", - }, - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor3", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ now() }}", - "type": "count", - }, - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor4", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ now() }}", - "type": "ratio", - }, - ] - }, - ) - await hass.async_block_till_done() - with patch( "homeassistant.components.recorder.history.state_changes_during_period", _fake_states, ): + await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor1", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor2", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor3", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "count", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor4", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "ratio", + }, + ] + }, + ) + await hass.async_block_till_done() for i in range(1, 5): await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() @@ -463,56 +461,55 @@ async def test_async_on_entire_period(hass, recorder_mock): ] } - await async_setup_component( - hass, - "sensor", - { - "sensor": [ - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_on_id", - "name": "on_sensor1", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ now() }}", - "type": "time", - }, - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_on_id", - "name": "on_sensor2", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ now() }}", - "type": "time", - }, - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_on_id", - "name": "on_sensor3", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ now() }}", - "type": "count", - }, - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_on_id", - "name": "on_sensor4", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ now() }}", - "type": "ratio", - }, - ] - }, - ) - await hass.async_block_till_done() - with patch( "homeassistant.components.recorder.history.state_changes_during_period", _fake_states, ): + await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_on_id", + "name": "on_sensor1", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_on_id", + "name": "on_sensor2", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_on_id", + "name": "on_sensor3", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "count", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_on_id", + "name": "on_sensor4", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "ratio", + }, + ] + }, + ) + await hass.async_block_till_done() for i in range(1, 5): await async_update_entity(hass, f"sensor.on_sensor{i}") await hass.async_block_till_done() @@ -1235,56 +1232,55 @@ async def test_measure_from_end_going_backwards(hass, recorder_mock): ] } - await async_setup_component( - hass, - "sensor", - { - "sensor": [ - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor1", - "state": "on", - "duration": {"hours": 1}, - "end": "{{ now() }}", - "type": "time", - }, - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor2", - "state": "on", - "duration": {"hours": 1}, - "end": "{{ now() }}", - "type": "time", - }, - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor3", - "state": "on", - "duration": {"hours": 1}, - "end": "{{ now() }}", - "type": "count", - }, - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor4", - "state": "on", - "duration": {"hours": 1}, - "end": "{{ now() }}", - "type": "ratio", - }, - ] - }, - ) - await hass.async_block_till_done() - with patch( "homeassistant.components.recorder.history.state_changes_during_period", _fake_states, ), freeze_time(start_time): + await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor1", + "state": "on", + "duration": {"hours": 1}, + "end": "{{ now() }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor2", + "state": "on", + "duration": {"hours": 1}, + "end": "{{ now() }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor3", + "state": "on", + "duration": {"hours": 1}, + "end": "{{ now() }}", + "type": "count", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor4", + "state": "on", + "duration": {"hours": 1}, + "end": "{{ now() }}", + "type": "ratio", + }, + ] + }, + ) + await hass.async_block_till_done() for i in range(1, 5): await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() @@ -1329,56 +1325,55 @@ async def test_measure_cet(hass, recorder_mock): ] } - await async_setup_component( - hass, - "sensor", - { - "sensor": [ - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor1", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ now() }}", - "type": "time", - }, - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor2", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ now() }}", - "type": "time", - }, - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor3", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ now() }}", - "type": "count", - }, - { - "platform": "history_stats", - "entity_id": "binary_sensor.test_id", - "name": "sensor4", - "state": "on", - "start": "{{ as_timestamp(now()) - 3600 }}", - "end": "{{ now() }}", - "type": "ratio", - }, - ] - }, - ) - await hass.async_block_till_done() - with patch( "homeassistant.components.recorder.history.state_changes_during_period", _fake_states, ): + await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor1", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor2", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "time", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor3", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "count", + }, + { + "platform": "history_stats", + "entity_id": "binary_sensor.test_id", + "name": "sensor4", + "state": "on", + "start": "{{ as_timestamp(now()) - 3600 }}", + "end": "{{ now() }}", + "type": "ratio", + }, + ] + }, + ) + await hass.async_block_till_done() for i in range(1, 5): await async_update_entity(hass, f"sensor.sensor{i}") await hass.async_block_till_done() From 0f4080bca3960ce205b5906094519cd1f3bd97bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Jun 2022 18:24:39 -1000 Subject: [PATCH 1393/3516] Fix synology_dsm coordinator typing (#73301) --- homeassistant/components/synology_dsm/coordinator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_dsm/coordinator.py b/homeassistant/components/synology_dsm/coordinator.py index 332efb50bc8..e2f0f0741f4 100644 --- a/homeassistant/components/synology_dsm/coordinator.py +++ b/homeassistant/components/synology_dsm/coordinator.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any import async_timeout from synology_dsm.api.surveillance_station.camera import SynoCamera @@ -65,7 +66,7 @@ class SynologyDSMSwitchUpdateCoordinator(SynologyDSMUpdateCoordinator): ) self.version = info["data"]["CMSMinVersion"] - async def _async_update_data(self) -> dict[str, dict[str, SynoCamera]] | None: + async def _async_update_data(self) -> dict[str, dict[str, Any]]: """Fetch all data from api.""" surveillance_station = self.api.surveillance_station return { @@ -96,7 +97,7 @@ class SynologyDSMCentralUpdateCoordinator(SynologyDSMUpdateCoordinator): ), ) - async def _async_update_data(self) -> dict[str, dict[str, SynoCamera]] | None: + async def _async_update_data(self) -> None: """Fetch all data from api.""" try: await self.api.async_update() @@ -117,7 +118,7 @@ class SynologyDSMCameraUpdateCoordinator(SynologyDSMUpdateCoordinator): """Initialize DataUpdateCoordinator for cameras.""" super().__init__(hass, entry, api, timedelta(seconds=30)) - async def _async_update_data(self) -> dict[str, dict[str, SynoCamera]] | None: + async def _async_update_data(self) -> dict[str, dict[str, SynoCamera]]: """Fetch all camera data from api.""" surveillance_station = self.api.surveillance_station current_data: dict[str, SynoCamera] = { From 15621bee3f304cd1c36a5712318c4ad240faba5b Mon Sep 17 00:00:00 2001 From: Adam Dullage Date: Fri, 10 Jun 2022 05:37:36 +0100 Subject: [PATCH 1394/3516] Fix polling frequency for Starling integration (#73282) --- homeassistant/components/starlingbank/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/starlingbank/sensor.py b/homeassistant/components/starlingbank/sensor.py index 40f1e0ff3fd..0069fe7a65f 100644 --- a/homeassistant/components/starlingbank/sensor.py +++ b/homeassistant/components/starlingbank/sensor.py @@ -1,6 +1,7 @@ """Support for balance data via the Starling Bank API.""" from __future__ import annotations +from datetime import timedelta import logging import requests @@ -26,6 +27,7 @@ DEFAULT_SANDBOX = False DEFAULT_ACCOUNT_NAME = "Starling" ICON = "mdi:currency-gbp" +SCAN_INTERVAL = timedelta(seconds=180) ACCOUNT_SCHEMA = vol.Schema( { From 7a5fa8eb58f49282e73f454826472ba54cd37a30 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 9 Jun 2022 22:14:43 -0700 Subject: [PATCH 1395/3516] Update more nest tests to use common fixtures (#73303) Update nest tests to use fixtures --- tests/components/nest/common.py | 28 -- tests/components/nest/test_device_trigger.py | 137 ++++---- tests/components/nest/test_events.py | 290 +++++++-------- tests/components/nest/test_media_source.py | 349 +++++++++---------- 4 files changed, 357 insertions(+), 447 deletions(-) diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index bf8af8db127..988906606ad 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -5,7 +5,6 @@ import copy from dataclasses import dataclass import time from typing import Any, Generator, TypeVar -from unittest.mock import patch from google_nest_sdm.auth import AbstractAuth from google_nest_sdm.device import Device @@ -16,7 +15,6 @@ from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from homeassistant.components.nest import DOMAIN from homeassistant.components.nest.const import SDM_SCOPES -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -198,29 +196,3 @@ class CreateDevice: data.update(raw_data if raw_data else {}) data["traits"].update(raw_traits if raw_traits else {}) self.device_manager.add_device(Device.MakeDevice(data, auth=self.auth)) - - -async def async_setup_sdm_platform( - hass, - platform, - devices={}, -): - """Set up the platform and prerequisites.""" - create_config_entry().add_to_hass(hass) - subscriber = FakeSubscriber() - device_manager = await subscriber.async_get_device_manager() - if devices: - for device in devices.values(): - device_manager.add_device(device) - platforms = [] - if platform: - platforms = [platform] - with patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" - ), patch("homeassistant.components.nest.PLATFORMS", platforms), patch( - "homeassistant.components.nest.api.GoogleNestSubscriber", - return_value=subscriber, - ): - assert await async_setup_component(hass, DOMAIN, CONFIG) - await hass.async_block_till_done() - return subscriber diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py index ee93323fcd8..3272c2a7c59 100644 --- a/tests/components/nest/test_device_trigger.py +++ b/tests/components/nest/test_device_trigger.py @@ -1,5 +1,4 @@ """The tests for Nest device triggers.""" -from google_nest_sdm.device import Device from google_nest_sdm.event import EventMessage import pytest @@ -10,11 +9,12 @@ from homeassistant.components.device_automation.exceptions import ( ) from homeassistant.components.nest import DOMAIN from homeassistant.components.nest.events import NEST_EVENT +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from .common import async_setup_sdm_platform +from .common import DEVICE_ID, CreateDevice, FakeSubscriber, PlatformSetup from tests.common import ( assert_lists_same, @@ -22,11 +22,16 @@ from tests.common import ( async_mock_service, ) -DEVICE_ID = "some-device-id" DEVICE_NAME = "My Camera" DATA_MESSAGE = {"message": "service-called"} +@pytest.fixture +def platforms() -> list[str]: + """Fixture to setup the platforms to test.""" + return ["camera"] + + def make_camera(device_id, name=DEVICE_NAME, traits={}): """Create a nest camera.""" traits = traits.copy() @@ -45,21 +50,11 @@ def make_camera(device_id, name=DEVICE_NAME, traits={}): }, } ) - return Device.MakeDevice( - { - "name": device_id, - "type": "sdm.devices.types.CAMERA", - "traits": traits, - }, - auth=None, - ) - - -async def async_setup_camera(hass, devices=None): - """Set up the platform and prerequisites for testing available triggers.""" - if not devices: - devices = {DEVICE_ID: make_camera(device_id=DEVICE_ID)} - return await async_setup_sdm_platform(hass, "camera", devices) + return { + "name": device_id, + "type": "sdm.devices.types.CAMERA", + "traits": traits, + } async def setup_automation(hass, device_id, trigger_type): @@ -92,16 +87,20 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_triggers(hass): +async def test_get_triggers( + hass: HomeAssistant, create_device: CreateDevice, setup_platform: PlatformSetup +) -> None: """Test we get the expected triggers from a nest.""" - camera = make_camera( - device_id=DEVICE_ID, - traits={ - "sdm.devices.traits.CameraMotion": {}, - "sdm.devices.traits.CameraPerson": {}, - }, + create_device.create( + raw_data=make_camera( + device_id=DEVICE_ID, + traits={ + "sdm.devices.traits.CameraMotion": {}, + "sdm.devices.traits.CameraPerson": {}, + }, + ) ) - await async_setup_camera(hass, {DEVICE_ID: camera}) + await setup_platform() device_registry = dr.async_get(hass) device_entry = device_registry.async_get_device({("nest", DEVICE_ID)}) @@ -128,23 +127,29 @@ async def test_get_triggers(hass): assert_lists_same(triggers, expected_triggers) -async def test_multiple_devices(hass): +async def test_multiple_devices( + hass: HomeAssistant, create_device: CreateDevice, setup_platform: PlatformSetup +) -> None: """Test we get the expected triggers from a nest.""" - camera1 = make_camera( - device_id="device-id-1", - name="Camera 1", - traits={ - "sdm.devices.traits.CameraSound": {}, - }, + create_device.create( + raw_data=make_camera( + device_id="device-id-1", + name="Camera 1", + traits={ + "sdm.devices.traits.CameraSound": {}, + }, + ) ) - camera2 = make_camera( - device_id="device-id-2", - name="Camera 2", - traits={ - "sdm.devices.traits.DoorbellChime": {}, - }, + create_device.create( + raw_data=make_camera( + device_id="device-id-2", + name="Camera 2", + traits={ + "sdm.devices.traits.DoorbellChime": {}, + }, + ) ) - await async_setup_camera(hass, {"device-id-1": camera1, "device-id-2": camera2}) + await setup_platform() registry = er.async_get(hass) entry1 = registry.async_get("camera.camera_1") @@ -177,16 +182,20 @@ async def test_multiple_devices(hass): } -async def test_triggers_for_invalid_device_id(hass): +async def test_triggers_for_invalid_device_id( + hass: HomeAssistant, create_device: CreateDevice, setup_platform: PlatformSetup +) -> None: """Get triggers for a device not found in the API.""" - camera = make_camera( - device_id=DEVICE_ID, - traits={ - "sdm.devices.traits.CameraMotion": {}, - "sdm.devices.traits.CameraPerson": {}, - }, + create_device.create( + raw_data=make_camera( + device_id=DEVICE_ID, + traits={ + "sdm.devices.traits.CameraMotion": {}, + "sdm.devices.traits.CameraPerson": {}, + }, + ) ) - await async_setup_camera(hass, {DEVICE_ID: camera}) + await setup_platform() device_registry = dr.async_get(hass) device_entry = device_registry.async_get_device({("nest", DEVICE_ID)}) @@ -207,14 +216,16 @@ async def test_triggers_for_invalid_device_id(hass): ) -async def test_no_triggers(hass): +async def test_no_triggers( + hass: HomeAssistant, create_device: CreateDevice, setup_platform: PlatformSetup +) -> None: """Test we get the expected triggers from a nest.""" - camera = make_camera(device_id=DEVICE_ID, traits={}) - await async_setup_camera(hass, {DEVICE_ID: camera}) + create_device.create(raw_data=make_camera(device_id=DEVICE_ID, traits={})) + await setup_platform() registry = er.async_get(hass) entry = registry.async_get("camera.my_camera") - assert entry.unique_id == "some-device-id-camera" + assert entry.unique_id == f"{DEVICE_ID}-camera" triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, entry.device_id @@ -294,15 +305,23 @@ async def test_trigger_for_wrong_event_type(hass, calls): assert len(calls) == 0 -async def test_subscriber_automation(hass, calls): +async def test_subscriber_automation( + hass: HomeAssistant, + calls: list, + create_device: CreateDevice, + setup_platform: PlatformSetup, + subscriber: FakeSubscriber, +) -> None: """Test end to end subscriber triggers automation.""" - camera = make_camera( - device_id=DEVICE_ID, - traits={ - "sdm.devices.traits.CameraMotion": {}, - }, + create_device.create( + raw_data=make_camera( + device_id=DEVICE_ID, + traits={ + "sdm.devices.traits.CameraMotion": {}, + }, + ) ) - subscriber = await async_setup_camera(hass, {DEVICE_ID: camera}) + await setup_platform() device_registry = dr.async_get(hass) device_entry = device_registry.async_get_device({("nest", DEVICE_ID)}) diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index 0ab387a7dea..28550bd57b6 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -13,11 +13,12 @@ from unittest.mock import patch from google_nest_sdm.device import Device from google_nest_sdm.event import EventMessage +import pytest from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util.dt import utcnow -from .common import async_setup_sdm_platform +from .common import CreateDevice from tests.common import async_capture_events @@ -31,26 +32,43 @@ EVENT_ID = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." EVENT_KEYS = {"device_id", "type", "timestamp", "zones"} +@pytest.fixture +def platforms() -> list[str]: + """Fixture for platforms to setup.""" + return [PLATFORM] + + +@pytest.fixture +def device_type() -> str: + """Fixture for the type of device under test.""" + return "sdm.devices.types.DOORBELL" + + +@pytest.fixture +def device_traits() -> list[str]: + """Fixture for the present traits of the device under test.""" + return ["sdm.devices.traits.DoorbellChime"] + + +@pytest.fixture(autouse=True) +def device( + device_type: str, device_traits: dict[str, Any], create_device: CreateDevice +) -> None: + """Fixture to create a device under test.""" + return create_device.create( + raw_data={ + "name": DEVICE_ID, + "type": device_type, + "traits": create_device_traits(device_traits), + } + ) + + def event_view(d: Mapping[str, Any]) -> Mapping[str, Any]: """View of an event with relevant keys for testing.""" return {key: value for key, value in d.items() if key in EVENT_KEYS} -async def async_setup_devices(hass, device_type, traits={}, auth=None): - """Set up the platform and prerequisites.""" - devices = { - DEVICE_ID: Device.MakeDevice( - { - "name": DEVICE_ID, - "type": device_type, - "traits": traits, - }, - auth=auth, - ), - } - return await async_setup_sdm_platform(hass, PLATFORM, devices=devices) - - def create_device_traits(event_traits=[]): """Create fake traits for a device.""" result = { @@ -98,15 +116,45 @@ def create_events(events, device_id=DEVICE_ID, timestamp=None): ) -async def test_doorbell_chime_event(hass, auth): +@pytest.mark.parametrize( + "device_type,device_traits,event_trait,expected_model,expected_type", + [ + ( + "sdm.devices.types.DOORBELL", + ["sdm.devices.traits.DoorbellChime"], + "sdm.devices.events.DoorbellChime.Chime", + "Doorbell", + "doorbell_chime", + ), + ( + "sdm.devices.types.CAMERA", + ["sdm.devices.traits.CameraMotion"], + "sdm.devices.events.CameraMotion.Motion", + "Camera", + "camera_motion", + ), + ( + "sdm.devices.types.CAMERA", + ["sdm.devices.traits.CameraPerson"], + "sdm.devices.events.CameraPerson.Person", + "Camera", + "camera_person", + ), + ( + "sdm.devices.types.CAMERA", + ["sdm.devices.traits.CameraSound"], + "sdm.devices.events.CameraSound.Sound", + "Camera", + "camera_sound", + ), + ], +) +async def test_event( + hass, auth, setup_platform, subscriber, event_trait, expected_model, expected_type +): """Test a pubsub message for a doorbell event.""" events = async_capture_events(hass, NEST_EVENT) - subscriber = await async_setup_devices( - hass, - "sdm.devices.types.DOORBELL", - create_device_traits(["sdm.devices.traits.DoorbellChime"]), - auth, - ) + await setup_platform() registry = er.async_get(hass) entry = registry.async_get("camera.front") @@ -118,115 +166,32 @@ async def test_doorbell_chime_event(hass, auth): device_registry = dr.async_get(hass) device = device_registry.async_get(entry.device_id) assert device.name == "Front" - assert device.model == "Doorbell" + assert device.model == expected_model assert device.identifiers == {("nest", DEVICE_ID)} timestamp = utcnow() - await subscriber.async_receive_event( - create_event("sdm.devices.events.DoorbellChime.Chime", timestamp=timestamp) - ) + await subscriber.async_receive_event(create_event(event_trait, timestamp=timestamp)) await hass.async_block_till_done() event_time = timestamp.replace(microsecond=0) assert len(events) == 1 assert event_view(events[0].data) == { "device_id": entry.device_id, - "type": "doorbell_chime", + "type": expected_type, "timestamp": event_time, } -async def test_camera_motion_event(hass): - """Test a pubsub message for a camera motion event.""" - events = async_capture_events(hass, NEST_EVENT) - subscriber = await async_setup_devices( - hass, - "sdm.devices.types.CAMERA", - create_device_traits(["sdm.devices.traits.CameraMotion"]), - ) - registry = er.async_get(hass) - entry = registry.async_get("camera.front") - assert entry is not None - - timestamp = utcnow() - await subscriber.async_receive_event( - create_event("sdm.devices.events.CameraMotion.Motion", timestamp=timestamp) - ) - await hass.async_block_till_done() - - event_time = timestamp.replace(microsecond=0) - assert len(events) == 1 - assert event_view(events[0].data) == { - "device_id": entry.device_id, - "type": "camera_motion", - "timestamp": event_time, - } - - -async def test_camera_sound_event(hass): - """Test a pubsub message for a camera sound event.""" - events = async_capture_events(hass, NEST_EVENT) - subscriber = await async_setup_devices( - hass, - "sdm.devices.types.CAMERA", - create_device_traits(["sdm.devices.traits.CameraSound"]), - ) - registry = er.async_get(hass) - entry = registry.async_get("camera.front") - assert entry is not None - - timestamp = utcnow() - await subscriber.async_receive_event( - create_event("sdm.devices.events.CameraSound.Sound", timestamp=timestamp) - ) - await hass.async_block_till_done() - - event_time = timestamp.replace(microsecond=0) - assert len(events) == 1 - assert event_view(events[0].data) == { - "device_id": entry.device_id, - "type": "camera_sound", - "timestamp": event_time, - } - - -async def test_camera_person_event(hass): +@pytest.mark.parametrize( + "device_traits", + [ + ["sdm.devices.traits.CameraMotion", "sdm.devices.traits.CameraPerson"], + ], +) +async def test_camera_multiple_event(hass, subscriber, setup_platform): """Test a pubsub message for a camera person event.""" events = async_capture_events(hass, NEST_EVENT) - subscriber = await async_setup_devices( - hass, - "sdm.devices.types.DOORBELL", - create_device_traits(["sdm.devices.traits.CameraPerson"]), - ) - registry = er.async_get(hass) - entry = registry.async_get("camera.front") - assert entry is not None - - timestamp = utcnow() - await subscriber.async_receive_event( - create_event("sdm.devices.events.CameraPerson.Person", timestamp=timestamp) - ) - await hass.async_block_till_done() - - event_time = timestamp.replace(microsecond=0) - assert len(events) == 1 - assert event_view(events[0].data) == { - "device_id": entry.device_id, - "type": "camera_person", - "timestamp": event_time, - } - - -async def test_camera_multiple_event(hass): - """Test a pubsub message for a camera person event.""" - events = async_capture_events(hass, NEST_EVENT) - subscriber = await async_setup_devices( - hass, - "sdm.devices.types.DOORBELL", - create_device_traits( - ["sdm.devices.traits.CameraMotion", "sdm.devices.traits.CameraPerson"] - ), - ) + await setup_platform() registry = er.async_get(hass) entry = registry.async_get("camera.front") assert entry is not None @@ -260,28 +225,20 @@ async def test_camera_multiple_event(hass): } -async def test_unknown_event(hass): +async def test_unknown_event(hass, subscriber, setup_platform): """Test a pubsub message for an unknown event type.""" events = async_capture_events(hass, NEST_EVENT) - subscriber = await async_setup_devices( - hass, - "sdm.devices.types.DOORBELL", - create_device_traits(["sdm.devices.traits.DoorbellChime"]), - ) + await setup_platform() await subscriber.async_receive_event(create_event("some-event-id")) await hass.async_block_till_done() assert len(events) == 0 -async def test_unknown_device_id(hass): +async def test_unknown_device_id(hass, subscriber, setup_platform): """Test a pubsub message for an unknown event type.""" events = async_capture_events(hass, NEST_EVENT) - subscriber = await async_setup_devices( - hass, - "sdm.devices.types.DOORBELL", - create_device_traits(["sdm.devices.traits.DoorbellChime"]), - ) + await setup_platform() await subscriber.async_receive_event( create_event("sdm.devices.events.DoorbellChime.Chime", "invalid-device-id") ) @@ -290,14 +247,10 @@ async def test_unknown_device_id(hass): assert len(events) == 0 -async def test_event_message_without_device_event(hass): +async def test_event_message_without_device_event(hass, subscriber, setup_platform): """Test a pubsub message for an unknown event type.""" events = async_capture_events(hass, NEST_EVENT) - subscriber = await async_setup_devices( - hass, - "sdm.devices.types.DOORBELL", - create_device_traits(["sdm.devices.traits.DoorbellChime"]), - ) + await setup_platform() timestamp = utcnow() event = EventMessage( { @@ -312,20 +265,16 @@ async def test_event_message_without_device_event(hass): assert len(events) == 0 -async def test_doorbell_event_thread(hass, auth): +@pytest.mark.parametrize( + "device_traits", + [ + ["sdm.devices.traits.CameraClipPreview", "sdm.devices.traits.CameraPerson"], + ], +) +async def test_doorbell_event_thread(hass, subscriber, setup_platform): """Test a series of pubsub messages in the same thread.""" events = async_capture_events(hass, NEST_EVENT) - subscriber = await async_setup_devices( - hass, - "sdm.devices.types.DOORBELL", - create_device_traits( - [ - "sdm.devices.traits.CameraClipPreview", - "sdm.devices.traits.CameraPerson", - ] - ), - auth, - ) + await setup_platform() registry = er.async_get(hass) entry = registry.async_get("camera.front") assert entry is not None @@ -381,21 +330,20 @@ async def test_doorbell_event_thread(hass, auth): } -async def test_doorbell_event_session_update(hass, auth): +@pytest.mark.parametrize( + "device_traits", + [ + [ + "sdm.devices.traits.CameraClipPreview", + "sdm.devices.traits.CameraPerson", + "sdm.devices.traits.CameraMotion", + ], + ], +) +async def test_doorbell_event_session_update(hass, subscriber, setup_platform): """Test a pubsub message with updates to an existing session.""" events = async_capture_events(hass, NEST_EVENT) - subscriber = await async_setup_devices( - hass, - "sdm.devices.types.DOORBELL", - create_device_traits( - [ - "sdm.devices.traits.CameraClipPreview", - "sdm.devices.traits.CameraPerson", - "sdm.devices.traits.CameraMotion", - ] - ), - auth, - ) + await setup_platform() registry = er.async_get(hass) entry = registry.async_get("camera.front") assert entry is not None @@ -454,14 +402,10 @@ async def test_doorbell_event_session_update(hass, auth): } -async def test_structure_update_event(hass): +async def test_structure_update_event(hass, subscriber, setup_platform): """Test a pubsub message for a new device being added.""" events = async_capture_events(hass, NEST_EVENT) - subscriber = await async_setup_devices( - hass, - "sdm.devices.types.DOORBELL", - create_device_traits(["sdm.devices.traits.DoorbellChime"]), - ) + await setup_platform() # Entity for first device is registered registry = er.async_get(hass) @@ -516,14 +460,16 @@ async def test_structure_update_event(hass): assert not registry.async_get("camera.back") -async def test_event_zones(hass): +@pytest.mark.parametrize( + "device_traits", + [ + ["sdm.devices.traits.CameraMotion"], + ], +) +async def test_event_zones(hass, subscriber, setup_platform): """Test events published with zone information.""" events = async_capture_events(hass, NEST_EVENT) - subscriber = await async_setup_devices( - hass, - "sdm.devices.types.DOORBELL", - create_device_traits(["sdm.devices.traits.CameraMotion"]), - ) + await setup_platform() registry = er.async_get(hass) entry = registry.async_get("camera.front") assert entry is not None diff --git a/tests/components/nest/test_media_source.py b/tests/components/nest/test_media_source.py index 09a3f9f625c..dff740c84f4 100644 --- a/tests/components/nest/test_media_source.py +++ b/tests/components/nest/test_media_source.py @@ -8,11 +8,11 @@ from collections.abc import Generator import datetime from http import HTTPStatus import io +from typing import Any from unittest.mock import patch import aiohttp import av -from google_nest_sdm.device import Device from google_nest_sdm.event import EventMessage import numpy as np import pytest @@ -27,17 +27,11 @@ from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .common import ( - CONFIG, - FakeSubscriber, - async_setup_sdm_platform, - create_config_entry, -) +from .common import DEVICE_ID, CreateDevice, FakeSubscriber from tests.common import async_capture_events DOMAIN = "nest" -DEVICE_ID = "example/api/device/id" DEVICE_NAME = "Front" PLATFORM = "camera" NEST_EVENT = "nest_event" @@ -90,10 +84,42 @@ def frame_image_data(frame_i, total_frames): return img +@pytest.fixture +def platforms() -> list[str]: + """Fixture for platforms to setup.""" + return [PLATFORM] + + @pytest.fixture(autouse=True) -async def setup_media_source(hass) -> None: - """Set up media source.""" - assert await async_setup_component(hass, "media_source", {}) +async def setup_components(hass) -> None: + """Fixture to initialize the integration.""" + await async_setup_component(hass, "media_source", {}) + + +@pytest.fixture +def device_type() -> str: + """Fixture for the type of device under test.""" + return CAMERA_DEVICE_TYPE + + +@pytest.fixture +def device_traits() -> dict[str, Any]: + """Fixture for the present traits of the device under test.""" + return CAMERA_TRAITS + + +@pytest.fixture(autouse=True) +def device( + device_type: str, device_traits: dict[str, Any], create_device: CreateDevice +) -> None: + """Fixture to create a device under test.""" + return create_device.create( + raw_data={ + "name": DEVICE_ID, + "type": device_type, + "traits": device_traits, + } + ) @pytest.fixture @@ -128,22 +154,23 @@ def mp4() -> io.BytesIO: return output -async def async_setup_devices(hass, auth, device_type, traits={}, events=[]): - """Set up the platform and prerequisites.""" - devices = { - DEVICE_ID: Device.MakeDevice( - { - "name": DEVICE_ID, - "type": device_type, - "traits": traits, - }, - auth=auth, - ), - } - subscriber = await async_setup_sdm_platform(hass, PLATFORM, devices=devices) - # Enable feature for fetching media +@pytest.fixture(autouse=True) +def enable_prefetch(subscriber: FakeSubscriber) -> None: + """Fixture to enable media fetching for tests to exercise.""" subscriber.cache_policy.fetch = True - return subscriber + + +@pytest.fixture +def cache_size() -> int: + """Fixture for overrideing cache size.""" + return 100 + + +@pytest.fixture(autouse=True) +def apply_cache_size(cache_size): + """Fixture for patching the cache size.""" + with patch("homeassistant.components.nest.EVENT_MEDIA_CACHE_SIZE", new=cache_size): + yield def create_event( @@ -194,17 +221,20 @@ def create_battery_event_data( } -async def test_no_eligible_devices(hass, auth): +@pytest.mark.parametrize( + "device_type,device_traits", + [ + ( + "sdm.devices.types.THERMOSTAT", + { + "sdm.devices.traits.Temperature": {}, + }, + ) + ], +) +async def test_no_eligible_devices(hass, setup_platform): """Test a media source with no eligible camera devices.""" - await async_setup_devices( - hass, - auth, - "sdm.devices.types.THERMOSTAT", - { - "sdm.devices.traits.Temperature": {}, - }, - ) - + await setup_platform() browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") assert browse.domain == DOMAIN assert browse.identifier == "" @@ -212,10 +242,10 @@ async def test_no_eligible_devices(hass, auth): assert not browse.children -@pytest.mark.parametrize("traits", [CAMERA_TRAITS, BATTERY_CAMERA_TRAITS]) -async def test_supported_device(hass, auth, traits): +@pytest.mark.parametrize("device_traits", [CAMERA_TRAITS, BATTERY_CAMERA_TRAITS]) +async def test_supported_device(hass, setup_platform): """Test a media source with a supported camera.""" - await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, traits) + await setup_platform() assert len(hass.states.async_all()) == 1 camera = hass.states.get("camera.front") @@ -245,14 +275,9 @@ async def test_supported_device(hass, auth, traits): assert len(browse.children) == 0 -async def test_integration_unloaded(hass, auth): +async def test_integration_unloaded(hass, auth, setup_platform): """Test the media player loads, but has no devices, when config unloaded.""" - await async_setup_devices( - hass, - auth, - CAMERA_DEVICE_TYPE, - CAMERA_TRAITS, - ) + await setup_platform() browse = await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}") assert browse.domain == DOMAIN @@ -276,11 +301,9 @@ async def test_integration_unloaded(hass, auth): assert len(browse.children) == 0 -async def test_camera_event(hass, auth, hass_client): +async def test_camera_event(hass, hass_client, subscriber, auth, setup_platform): """Test a media source and image created for an event.""" - subscriber = await async_setup_devices( - hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS - ) + await setup_platform() assert len(hass.states.async_all()) == 1 camera = hass.states.get("camera.front") @@ -380,11 +403,9 @@ async def test_camera_event(hass, auth, hass_client): assert media.mime_type == "image/jpeg" -async def test_event_order(hass, auth): +async def test_event_order(hass, auth, subscriber, setup_platform): """Test multiple events are in descending timestamp order.""" - subscriber = await async_setup_devices( - hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS - ) + await setup_platform() auth.responses = [ aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), @@ -449,14 +470,15 @@ async def test_event_order(hass, auth): assert not browse.children[1].can_play -async def test_multiple_image_events_in_session(hass, auth, hass_client): +async def test_multiple_image_events_in_session( + hass, auth, hass_client, subscriber, setup_platform +): """Test multiple events published within the same event session.""" + await setup_platform() + event_session_id = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." event_timestamp1 = dt_util.now() event_timestamp2 = event_timestamp1 + datetime.timedelta(seconds=5) - subscriber = await async_setup_devices( - hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS - ) assert len(hass.states.async_all()) == 1 camera = hass.states.get("camera.front") @@ -560,13 +582,19 @@ async def test_multiple_image_events_in_session(hass, auth, hass_client): assert contents == IMAGE_BYTES_FROM_EVENT + b"-1" -async def test_multiple_clip_preview_events_in_session(hass, auth, hass_client): +@pytest.mark.parametrize("device_traits", [BATTERY_CAMERA_TRAITS]) +async def test_multiple_clip_preview_events_in_session( + hass, + auth, + hass_client, + subscriber, + setup_platform, +): """Test multiple events published within the same event session.""" + await setup_platform() + event_timestamp1 = dt_util.now() event_timestamp2 = event_timestamp1 + datetime.timedelta(seconds=5) - subscriber = await async_setup_devices( - hass, auth, CAMERA_DEVICE_TYPE, BATTERY_CAMERA_TRAITS - ) assert len(hass.states.async_all()) == 1 camera = hass.states.get("camera.front") @@ -656,9 +684,9 @@ async def test_multiple_clip_preview_events_in_session(hass, auth, hass_client): assert contents == IMAGE_BYTES_FROM_EVENT -async def test_browse_invalid_device_id(hass, auth): +async def test_browse_invalid_device_id(hass, auth, setup_platform): """Test a media source request for an invalid device id.""" - await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) + await setup_platform() device_registry = dr.async_get(hass) device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) @@ -676,9 +704,9 @@ async def test_browse_invalid_device_id(hass, auth): ) -async def test_browse_invalid_event_id(hass, auth): +async def test_browse_invalid_event_id(hass, auth, setup_platform): """Test a media source browsing for an invalid event id.""" - await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) + await setup_platform() device_registry = dr.async_get(hass) device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) @@ -699,9 +727,9 @@ async def test_browse_invalid_event_id(hass, auth): ) -async def test_resolve_missing_event_id(hass, auth): +async def test_resolve_missing_event_id(hass, auth, setup_platform): """Test a media source request missing an event id.""" - await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) + await setup_platform() device_registry = dr.async_get(hass) device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) @@ -716,10 +744,9 @@ async def test_resolve_missing_event_id(hass, auth): ) -async def test_resolve_invalid_device_id(hass, auth): +async def test_resolve_invalid_device_id(hass, auth, setup_platform): """Test resolving media for an invalid event id.""" - await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) - + await setup_platform() with pytest.raises(Unresolvable): await media_source.async_resolve_media( hass, @@ -728,9 +755,9 @@ async def test_resolve_invalid_device_id(hass, auth): ) -async def test_resolve_invalid_event_id(hass, auth): +async def test_resolve_invalid_event_id(hass, auth, setup_platform): """Test resolving media for an invalid event id.""" - await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) + await setup_platform() device_registry = dr.async_get(hass) device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) @@ -750,14 +777,14 @@ async def test_resolve_invalid_event_id(hass, auth): assert media.mime_type == "image/jpeg" -async def test_camera_event_clip_preview(hass, auth, hass_client, mp4): +@pytest.mark.parametrize("device_traits", [BATTERY_CAMERA_TRAITS]) +async def test_camera_event_clip_preview( + hass, auth, hass_client, mp4, subscriber, setup_platform +): """Test an event for a battery camera video clip.""" - subscriber = await async_setup_devices( - hass, auth, CAMERA_DEVICE_TYPE, BATTERY_CAMERA_TRAITS - ) - # Capture any events published received_events = async_capture_events(hass, NEST_EVENT) + await setup_platform() auth.responses = [ aiohttp.web.Response(body=mp4.getvalue()), @@ -857,10 +884,11 @@ async def test_camera_event_clip_preview(hass, auth, hass_client, mp4): await response.read() # Animated gif format not tested -async def test_event_media_render_invalid_device_id(hass, auth, hass_client): +async def test_event_media_render_invalid_device_id( + hass, auth, hass_client, setup_platform +): """Test event media API called with an invalid device id.""" - await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) - + await setup_platform() client = await hass_client() response = await client.get("/api/nest/event_media/invalid-device-id") assert response.status == HTTPStatus.NOT_FOUND, ( @@ -868,10 +896,11 @@ async def test_event_media_render_invalid_device_id(hass, auth, hass_client): ) -async def test_event_media_render_invalid_event_id(hass, auth, hass_client): +async def test_event_media_render_invalid_event_id( + hass, auth, hass_client, setup_platform +): """Test event media API called with an invalid device id.""" - await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) - + await setup_platform() device_registry = dr.async_get(hass) device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) assert device @@ -884,13 +913,11 @@ async def test_event_media_render_invalid_event_id(hass, auth, hass_client): ) -async def test_event_media_failure(hass, auth, hass_client): +async def test_event_media_failure(hass, auth, hass_client, subscriber, setup_platform): """Test event media fetch sees a failure from the server.""" - subscriber = await async_setup_devices( - hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS - ) received_events = async_capture_events(hass, NEST_EVENT) + await setup_platform() # Failure from server when fetching media auth.responses = [ aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR), @@ -937,10 +964,11 @@ async def test_event_media_failure(hass, auth, hass_client): ) -async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin_user): +async def test_media_permission_unauthorized( + hass, auth, hass_client, hass_admin_user, setup_platform +): """Test case where user does not have permissions to view media.""" - await async_setup_devices(hass, auth, CAMERA_DEVICE_TYPE, CAMERA_TRAITS) - + await setup_platform() assert len(hass.states.async_all()) == 1 camera = hass.states.get("camera.front") assert camera is not None @@ -962,33 +990,22 @@ async def test_media_permission_unauthorized(hass, auth, hass_client, hass_admin ) -async def test_multiple_devices(hass, auth, hass_client): +async def test_multiple_devices( + hass, auth, hass_client, create_device, subscriber, setup_platform +): """Test events received for multiple devices.""" - device_id1 = f"{DEVICE_ID}-1" device_id2 = f"{DEVICE_ID}-2" - - devices = { - device_id1: Device.MakeDevice( - { - "name": device_id1, - "type": CAMERA_DEVICE_TYPE, - "traits": CAMERA_TRAITS, - }, - auth=auth, - ), - device_id2: Device.MakeDevice( - { - "name": device_id2, - "type": CAMERA_DEVICE_TYPE, - "traits": CAMERA_TRAITS, - }, - auth=auth, - ), - } - subscriber = await async_setup_sdm_platform(hass, PLATFORM, devices=devices) + create_device.create( + raw_data={ + "name": device_id2, + "type": CAMERA_DEVICE_TYPE, + "traits": CAMERA_TRAITS, + } + ) + await setup_platform() device_registry = dr.async_get(hass) - device1 = device_registry.async_get_device({(DOMAIN, device_id1)}) + device1 = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) assert device1 device2 = device_registry.async_get_device({(DOMAIN, device_id2)}) assert device2 @@ -1018,7 +1035,7 @@ async def test_multiple_devices(hass, auth, hass_client): f"event-session-id-{i}", f"event-id-{i}", PERSON_EVENT, - device_id=device_id1, + device_id=DEVICE_ID, ) ) await hass.async_block_till_done() @@ -1073,34 +1090,18 @@ def event_store() -> Generator[None, None, None]: yield -async def test_media_store_persistence(hass, auth, hass_client, event_store): +@pytest.mark.parametrize("device_traits", [BATTERY_CAMERA_TRAITS]) +async def test_media_store_persistence( + hass, + auth, + hass_client, + event_store, + subscriber, + setup_platform, + config_entry, +): """Test the disk backed media store persistence.""" - nest_device = Device.MakeDevice( - { - "name": DEVICE_ID, - "type": CAMERA_DEVICE_TYPE, - "traits": BATTERY_CAMERA_TRAITS, - }, - auth=auth, - ) - - subscriber = FakeSubscriber() - device_manager = await subscriber.async_get_device_manager() - device_manager.add_device(nest_device) - # Fetch media for events when published - subscriber.cache_policy.fetch = True - - config_entry = create_config_entry() - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" - ), patch("homeassistant.components.nest.PLATFORMS", [PLATFORM]), patch( - "homeassistant.components.nest.api.GoogleNestSubscriber", - return_value=subscriber, - ): - assert await async_setup_component(hass, DOMAIN, CONFIG) - await hass.async_block_till_done() + await setup_platform() device_registry = dr.async_get(hass) device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) @@ -1154,18 +1155,8 @@ async def test_media_store_persistence(hass, auth, hass_client, event_store): # Now rebuild the entire integration and verify that all persisted storage # can be re-loaded from disk. - subscriber = FakeSubscriber() - device_manager = await subscriber.async_get_device_manager() - device_manager.add_device(nest_device) - - with patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" - ), patch("homeassistant.components.nest.PLATFORMS", [PLATFORM]), patch( - "homeassistant.components.nest.api.GoogleNestSubscriber", - return_value=subscriber, - ): - await hass.config_entries.async_reload(config_entry.entry_id) - await hass.async_block_till_done() + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() device_registry = dr.async_get(hass) device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) @@ -1197,11 +1188,12 @@ async def test_media_store_persistence(hass, auth, hass_client, event_store): assert contents == IMAGE_BYTES_FROM_EVENT -async def test_media_store_save_filesystem_error(hass, auth, hass_client): +@pytest.mark.parametrize("device_traits", [BATTERY_CAMERA_TRAITS]) +async def test_media_store_save_filesystem_error( + hass, auth, hass_client, subscriber, setup_platform +): """Test a filesystem error writing event media.""" - subscriber = await async_setup_devices( - hass, auth, CAMERA_DEVICE_TYPE, BATTERY_CAMERA_TRAITS - ) + await setup_platform() auth.responses = [ aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), @@ -1250,11 +1242,11 @@ async def test_media_store_save_filesystem_error(hass, auth, hass_client): ) -async def test_media_store_load_filesystem_error(hass, auth, hass_client): +async def test_media_store_load_filesystem_error( + hass, auth, hass_client, subscriber, setup_platform +): """Test a filesystem error reading event media.""" - subscriber = await async_setup_devices( - hass, auth, CAMERA_DEVICE_TYPE, BATTERY_CAMERA_TRAITS - ) + await setup_platform() assert len(hass.states.async_all()) == 1 camera = hass.states.get("camera.front") @@ -1299,17 +1291,12 @@ async def test_media_store_load_filesystem_error(hass, auth, hass_client): ) -async def test_camera_event_media_eviction(hass, auth, hass_client): +@pytest.mark.parametrize("device_traits,cache_size", [(BATTERY_CAMERA_TRAITS, 5)]) +async def test_camera_event_media_eviction( + hass, auth, hass_client, subscriber, setup_platform +): """Test media files getting evicted from the cache.""" - - # Set small cache size for testing eviction - with patch("homeassistant.components.nest.EVENT_MEDIA_CACHE_SIZE", new=5): - subscriber = await async_setup_devices( - hass, - auth, - CAMERA_DEVICE_TYPE, - BATTERY_CAMERA_TRAITS, - ) + await setup_platform() device_registry = dr.async_get(hass) device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) @@ -1384,23 +1371,9 @@ async def test_camera_event_media_eviction(hass, auth, hass_client): await hass.async_block_till_done() -async def test_camera_image_resize(hass, auth, hass_client): +async def test_camera_image_resize(hass, auth, hass_client, subscriber, setup_platform): """Test scaling a thumbnail for an event image.""" - event_timestamp = dt_util.now() - subscriber = await async_setup_devices( - hass, - auth, - CAMERA_DEVICE_TYPE, - CAMERA_TRAITS, - events=[ - create_event( - EVENT_SESSION_ID, - EVENT_ID, - PERSON_EVENT, - timestamp=event_timestamp, - ), - ], - ) + await setup_platform() device_registry = dr.async_get(hass) device = device_registry.async_get_device({(DOMAIN, DEVICE_ID)}) From f4d339119f1a59659901d08cf1f774caeaee39fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Jun 2022 21:17:37 -1000 Subject: [PATCH 1396/3516] Cache which entities are exposed in emulated_hue (#73093) --- .../components/emulated_hue/config.py | 89 +++++-- .../components/emulated_hue/hue_api.py | 5 +- tests/components/emulated_hue/test_hue_api.py | 241 ++++++++++-------- 3 files changed, 209 insertions(+), 126 deletions(-) diff --git a/homeassistant/components/emulated_hue/config.py b/homeassistant/components/emulated_hue/config.py index c2cf67b43f4..1de6ec98520 100644 --- a/homeassistant/components/emulated_hue/config.py +++ b/homeassistant/components/emulated_hue/config.py @@ -1,14 +1,40 @@ """Support for local control of entities by emulating a Philips Hue bridge.""" from __future__ import annotations -from collections.abc import Iterable +from functools import cache import logging +from homeassistant.components import ( + climate, + cover, + fan, + humidifier, + light, + media_player, + scene, + script, +) from homeassistant.const import CONF_ENTITIES, CONF_TYPE -from homeassistant.core import HomeAssistant, State +from homeassistant.core import Event, HomeAssistant, State, callback, split_entity_id from homeassistant.helpers import storage +from homeassistant.helpers.event import ( + async_track_state_added_domain, + async_track_state_removed_domain, +) from homeassistant.helpers.typing import ConfigType +SUPPORTED_DOMAINS = { + climate.DOMAIN, + cover.DOMAIN, + fan.DOMAIN, + humidifier.DOMAIN, + light.DOMAIN, + media_player.DOMAIN, + scene.DOMAIN, + script.DOMAIN, +} + + TYPE_ALEXA = "alexa" TYPE_GOOGLE = "google_home" @@ -78,7 +104,7 @@ class Config: # Get whether or not UPNP binds to multicast address (239.255.255.250) # or to the unicast address (host_ip_addr) - self.upnp_bind_multicast = conf.get( + self.upnp_bind_multicast: bool = conf.get( CONF_UPNP_BIND_MULTICAST, DEFAULT_UPNP_BIND_MULTICAST ) @@ -93,7 +119,7 @@ class Config: # Get whether or not entities should be exposed by default, or if only # explicitly marked ones will be exposed - self.expose_by_default = conf.get( + self.expose_by_default: bool = conf.get( CONF_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSE_BY_DEFAULT ) @@ -118,18 +144,31 @@ class Config: # Get whether all non-dimmable lights should be reported as dimmable # for compatibility with older installations. - self.lights_all_dimmable = conf.get(CONF_LIGHTS_ALL_DIMMABLE) + self.lights_all_dimmable: bool = conf.get(CONF_LIGHTS_ALL_DIMMABLE) or False + + if self.expose_by_default: + self.track_domains = set(self.exposed_domains) or SUPPORTED_DOMAINS + else: + self.track_domains = { + split_entity_id(entity_id)[0] for entity_id in self.entities + } async def async_setup(self) -> None: - """Set up and migrate to storage.""" - self.store = storage.Store(self.hass, DATA_VERSION, DATA_KEY) # type: ignore[arg-type] + """Set up tracking and migrate to storage.""" + hass = self.hass + self.store = storage.Store(hass, DATA_VERSION, DATA_KEY) # type: ignore[arg-type] + numbers_path = hass.config.path(NUMBERS_FILE) self.numbers = ( - await storage.async_migrator( - self.hass, self.hass.config.path(NUMBERS_FILE), self.store - ) - or {} + await storage.async_migrator(hass, numbers_path, self.store) or {} + ) + async_track_state_added_domain( + hass, self.track_domains, self._clear_exposed_cache + ) + async_track_state_removed_domain( + hass, self.track_domains, self._clear_exposed_cache ) + @cache # pylint: disable=method-cache-max-size-none def entity_id_to_number(self, entity_id: str) -> str: """Get a unique number for the entity id.""" if self.type == TYPE_ALEXA: @@ -166,6 +205,27 @@ class Config: return state.attributes.get(ATTR_EMULATED_HUE_NAME, state.name) + @cache # pylint: disable=method-cache-max-size-none + def get_exposed_states(self) -> list[State]: + """Return a list of exposed states.""" + state_machine = self.hass.states + if self.expose_by_default: + return [ + state + for state in state_machine.async_all() + if self.is_state_exposed(state) + ] + states: list[State] = [] + for entity_id in self.entities: + if (state := state_machine.get(entity_id)) and self.is_state_exposed(state): + states.append(state) + return states + + @callback + def _clear_exposed_cache(self, event: Event) -> None: + """Clear the cache of exposed states.""" + self.get_exposed_states.cache_clear() # pylint: disable=no-member + def is_state_exposed(self, state: State) -> bool: """Cache determine if an entity should be exposed on the emulated bridge.""" if (exposed := self._exposed_cache.get(state.entity_id)) is not None: @@ -174,13 +234,6 @@ class Config: self._exposed_cache[state.entity_id] = exposed return exposed - def filter_exposed_states(self, states: Iterable[State]) -> list[State]: - """Filter a list of all states down to exposed entities.""" - exposed: list[State] = [ - state for state in states if self.is_state_exposed(state) - ] - return exposed - def _is_state_exposed(self, state: State) -> bool: """Determine if an entity state should be exposed on the emulated bridge. diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 2a9022f909d..c5ff9654f90 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -844,10 +844,9 @@ def create_config_model(config: Config, request: web.Request) -> dict[str, Any]: def create_list_of_entities(config: Config, request: web.Request) -> dict[str, Any]: """Create a list of all entities.""" - hass: core.HomeAssistant = request.app["hass"] json_response: dict[str, Any] = { - config.entity_id_to_number(entity.entity_id): state_to_json(config, entity) - for entity in config.filter_exposed_states(hass.states.async_all()) + config.entity_id_to_number(state.entity_id): state_to_json(config, state) + for state in config.get_exposed_states() } return json_response diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 87893f66e1f..e36903983fe 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -49,7 +49,8 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util from tests.common import ( @@ -96,41 +97,58 @@ ENTITY_IDS_BY_NUMBER = { ENTITY_NUMBERS_BY_ID = {v: k for k, v in ENTITY_IDS_BY_NUMBER.items()} -@pytest.fixture -def hass_hue(loop, hass): - """Set up a Home Assistant instance for these tests.""" - # We need to do this to get access to homeassistant/turn_(on,off) - loop.run_until_complete(setup.async_setup_component(hass, "homeassistant", {})) +def patch_upnp(): + """Patch async_create_upnp_datagram_endpoint.""" + return patch( + "homeassistant.components.emulated_hue.async_create_upnp_datagram_endpoint" + ) - loop.run_until_complete( + +async def async_get_lights(client): + """Get lights with the hue client.""" + result = await client.get("/api/username/lights") + assert result.status == HTTPStatus.OK + assert CONTENT_TYPE_JSON in result.headers["content-type"] + return await result.json() + + +async def _async_setup_emulated_hue(hass: HomeAssistant, conf: ConfigType) -> None: + """Set up emulated_hue with a specific config.""" + with patch_upnp(): + await setup.async_setup_component( + hass, + emulated_hue.DOMAIN, + {emulated_hue.DOMAIN: conf}, + ), + await hass.async_block_till_done() + + +@pytest.fixture +async def base_setup(hass): + """Set up homeassistant and http.""" + await asyncio.gather( + setup.async_setup_component(hass, "homeassistant", {}), setup.async_setup_component( hass, http.DOMAIN, {http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}} - ) + ), ) - with patch( - "homeassistant.components.emulated_hue.async_create_upnp_datagram_endpoint" - ): - loop.run_until_complete( - setup.async_setup_component( - hass, - emulated_hue.DOMAIN, - { - emulated_hue.DOMAIN: { - emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT, - emulated_hue.CONF_EXPOSE_BY_DEFAULT: True, - } - }, - ) - ) - loop.run_until_complete( +@pytest.fixture +async def demo_setup(hass): + """Fixture to setup demo platforms.""" + # We need to do this to get access to homeassistant/turn_(on,off) + setups = [ + setup.async_setup_component(hass, "homeassistant", {}), setup.async_setup_component( - hass, light.DOMAIN, {"light": [{"platform": "demo"}]} - ) - ) - - loop.run_until_complete( + hass, http.DOMAIN, {http.DOMAIN: {http.CONF_SERVER_PORT: HTTP_SERVER_PORT}} + ), + *[ + setup.async_setup_component( + hass, comp.DOMAIN, {comp.DOMAIN: [{"platform": "demo"}]} + ) + for comp in (light, climate, humidifier, media_player, fan, cover) + ], setup.async_setup_component( hass, script.DOMAIN, @@ -149,39 +167,7 @@ def hass_hue(loop, hass): } } }, - ) - ) - - loop.run_until_complete( - setup.async_setup_component( - hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]} - ) - ) - - loop.run_until_complete( - setup.async_setup_component( - hass, humidifier.DOMAIN, {"humidifier": [{"platform": "demo"}]} - ) - ) - - loop.run_until_complete( - setup.async_setup_component( - hass, media_player.DOMAIN, {"media_player": [{"platform": "demo"}]} - ) - ) - - loop.run_until_complete( - setup.async_setup_component(hass, fan.DOMAIN, {"fan": [{"platform": "demo"}]}) - ) - - loop.run_until_complete( - setup.async_setup_component( - hass, cover.DOMAIN, {"cover": [{"platform": "demo"}]} - ) - ) - - # setup a dummy scene - loop.run_until_complete( + ), setup.async_setup_component( hass, "scene", @@ -199,21 +185,49 @@ def hass_hue(loop, hass): }, ] }, - ) - ) + ), + ] - # create a lamp without brightness support - hass.states.async_set("light.no_brightness", "on", {}) - - return hass + await asyncio.gather(*setups) @pytest.fixture -def hue_client(loop, hass_hue, hass_client_no_auth): +async def hass_hue(hass, base_setup, demo_setup): + """Set up a Home Assistant instance for these tests.""" + await _async_setup_emulated_hue( + hass, + { + emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT, + emulated_hue.CONF_EXPOSE_BY_DEFAULT: True, + }, + ) + # create a lamp without brightness support + hass.states.async_set("light.no_brightness", "on", {}) + return hass + + +@callback +def _mock_hue_endpoints( + hass: HomeAssistant, conf: ConfigType, entity_numbers: dict[str, str] +) -> None: + """Override the hue config with specific entity numbers.""" + web_app = hass.http.app + config = Config(hass, conf, "127.0.0.1") + config.numbers = entity_numbers + HueUsernameView().register(web_app, web_app.router) + HueAllLightsStateView(config).register(web_app, web_app.router) + HueOneLightStateView(config).register(web_app, web_app.router) + HueOneLightChangeView(config).register(web_app, web_app.router) + HueAllGroupsStateView(config).register(web_app, web_app.router) + HueFullStateView(config).register(web_app, web_app.router) + HueConfigView(config).register(web_app, web_app.router) + + +@pytest.fixture +async def hue_client(hass_hue, hass_client_no_auth): """Create web client for emulated hue api.""" - web_app = hass_hue.http.app - config = Config( - None, + _mock_hue_endpoints( + hass_hue, { emulated_hue.CONF_ENTITIES: { "light.bed_light": {emulated_hue.CONF_ENTITY_HIDDEN: True}, @@ -244,22 +258,12 @@ def hue_client(loop, hass_hue, hass_client_no_auth): "scene.light_off": {emulated_hue.CONF_ENTITY_HIDDEN: False}, }, }, - "127.0.0.1", + ENTITY_IDS_BY_NUMBER, ) - config.numbers = ENTITY_IDS_BY_NUMBER - - HueUsernameView().register(web_app, web_app.router) - HueAllLightsStateView(config).register(web_app, web_app.router) - HueOneLightStateView(config).register(web_app, web_app.router) - HueOneLightChangeView(config).register(web_app, web_app.router) - HueAllGroupsStateView(config).register(web_app, web_app.router) - HueFullStateView(config).register(web_app, web_app.router) - HueConfigView(config).register(web_app, web_app.router) - - return loop.run_until_complete(hass_client_no_auth()) + return await hass_client_no_auth() -async def test_discover_lights(hue_client): +async def test_discover_lights(hass, hue_client): """Test the discovery of lights.""" result = await hue_client.get("/api/username/lights") @@ -292,6 +296,21 @@ async def test_discover_lights(hue_client): assert "00:62:5c:3e:df:58:40:01-43" in devices # scene.light_on assert "00:1c:72:08:ed:09:e7:89-77" in devices # scene.light_off + # Remove the state and ensure it disappears from devices + hass.states.async_remove("light.ceiling_lights") + await hass.async_block_till_done() + + result_json = await async_get_lights(hue_client) + devices = {val["uniqueid"] for val in result_json.values()} + assert "00:2f:d2:31:ce:c5:55:cc-ee" not in devices # light.ceiling_lights + + # Restore the state and ensure it reappears in devices + hass.states.async_set("light.ceiling_lights", STATE_ON) + await hass.async_block_till_done() + result_json = await async_get_lights(hue_client) + devices = {val["uniqueid"] for val in result_json.values()} + assert "00:2f:d2:31:ce:c5:55:cc-ee" in devices # light.ceiling_lights + async def test_light_without_brightness_supported(hass_hue, hue_client): """Test that light without brightness is supported.""" @@ -316,19 +335,8 @@ async def test_lights_all_dimmable(hass, hass_client_no_auth): emulated_hue.CONF_EXPOSE_BY_DEFAULT: True, emulated_hue.CONF_LIGHTS_ALL_DIMMABLE: True, } - with patch( - "homeassistant.components.emulated_hue.async_create_upnp_datagram_endpoint" - ): - await setup.async_setup_component( - hass, - emulated_hue.DOMAIN, - {emulated_hue.DOMAIN: hue_config}, - ) - await hass.async_block_till_done() - config = Config(None, hue_config, "127.0.0.1") - config.numbers = ENTITY_IDS_BY_NUMBER - web_app = hass.http.app - HueOneLightStateView(config).register(web_app, web_app.router) + await _async_setup_emulated_hue(hass, hue_config) + _mock_hue_endpoints(hass, hue_config, ENTITY_IDS_BY_NUMBER) client = await hass_client_no_auth() light_without_brightness_json = await perform_get_light_state( client, "light.no_brightness", HTTPStatus.OK @@ -568,13 +576,7 @@ async def test_get_light_state(hass_hue, hue_client): assert office_json["state"][HUE_API_STATE_SAT] == 217 # Check all lights view - result = await hue_client.get("/api/username/lights") - - assert result.status == HTTPStatus.OK - assert CONTENT_TYPE_JSON in result.headers["content-type"] - - result_json = await result.json() - + result_json = await async_get_lights(hue_client) assert ENTITY_NUMBERS_BY_ID["light.ceiling_lights"] in result_json assert ( result_json[ENTITY_NUMBERS_BY_ID["light.ceiling_lights"]]["state"][ @@ -1616,3 +1618,32 @@ async def test_only_change_hue_or_saturation(hass, hass_hue, hue_client): assert hass_hue.states.get("light.ceiling_lights").attributes[ light.ATTR_HS_COLOR ] == (0, 3) + + +async def test_specificly_exposed_entities(hass, base_setup, hass_client_no_auth): + """Test specific entities with expose by default off.""" + conf = { + emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT, + emulated_hue.CONF_EXPOSE_BY_DEFAULT: False, + emulated_hue.CONF_ENTITIES: { + "light.exposed": {emulated_hue.CONF_ENTITY_HIDDEN: False}, + }, + } + await _async_setup_emulated_hue(hass, conf) + _mock_hue_endpoints(hass, conf, {"1": "light.exposed"}) + hass.states.async_set("light.exposed", STATE_ON) + await hass.async_block_till_done() + client = await hass_client_no_auth() + result_json = await async_get_lights(client) + assert "1" in result_json + + hass.states.async_remove("light.exposed") + await hass.async_block_till_done() + result_json = await async_get_lights(client) + assert "1" not in result_json + + hass.states.async_set("light.exposed", STATE_ON) + await hass.async_block_till_done() + result_json = await async_get_lights(client) + + assert "1" in result_json From 06ebc1fa147cd401d21eff77cf897d36723f61e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 9 Jun 2022 21:53:42 -1000 Subject: [PATCH 1397/3516] Add support for async_remove_config_entry_device to august (#72627) --- homeassistant/components/august/__init__.py | 26 +++++++++--- tests/components/august/mocks.py | 15 +++++-- tests/components/august/test_init.py | 46 +++++++++++++++++++++ 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 570ec5983fe..e8df7e1072d 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -21,6 +21,7 @@ from homeassistant.exceptions import ( ConfigEntryNotReady, HomeAssistantError, ) +from homeassistant.helpers import device_registry as dr from .activity import ActivityStream from .const import DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS @@ -283,12 +284,15 @@ class AugustData(AugustSubscriberMixin): device.device_id, ) - def _get_device_name(self, device_id): + def get_device(self, device_id: str) -> Doorbell | Lock | None: + """Get a device by id.""" + return self._locks_by_id.get(device_id) or self._doorbells_by_id.get(device_id) + + def _get_device_name(self, device_id: str) -> str | None: """Return doorbell or lock name as August has it stored.""" - if device_id in self._locks_by_id: - return self._locks_by_id[device_id].device_name - if device_id in self._doorbells_by_id: - return self._doorbells_by_id[device_id].device_name + if device := self.get_device(device_id): + return device.device_name + return None async def async_lock(self, device_id): """Lock the device.""" @@ -403,3 +407,15 @@ def _restore_live_attrs(lock_detail, attrs): """Restore the non-cache attributes after a cached update.""" for attr, value in attrs.items(): setattr(lock_detail, attr, value) + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove august config entry from a device if its no longer present.""" + data: AugustData = hass.data[DOMAIN][config_entry.entry_id] + return not any( + identifier + for identifier in device_entry.identifiers + if identifier[0] == DOMAIN and data.get_device(identifier[1]) + ) diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index e419488becc..c93e6429f1c 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -1,7 +1,10 @@ """Mocks for the august component.""" +from __future__ import annotations + import json import os import time +from typing import Any, Iterable from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from yalexs.activity import ( @@ -26,7 +29,9 @@ from yalexs.lock import Lock, LockDetail from yalexs.pubnub_async import AugustPubNub from homeassistant.components.august.const import CONF_LOGIN_METHOD, DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -76,9 +81,13 @@ async def _mock_setup_august( async def _create_august_with_devices( - hass, devices, api_call_side_effects=None, activities=None, pubnub=None -): - entry, api_instance = await _create_august_api_with_devices( + hass: HomeAssistant, + devices: Iterable[LockDetail | DoorbellDetail], + api_call_side_effects: dict[str, Any] | None = None, + activities: list[Any] | None = None, + pubnub: AugustPubNub | None = None, +) -> ConfigEntry: + entry, _ = await _create_august_api_with_devices( hass, devices, api_call_side_effects, activities, pubnub ) return entry diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index 320461ca6e9..56113832d23 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -17,6 +17,9 @@ from homeassistant.const import ( STATE_ON, ) from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity_registry import EntityRegistry +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry from tests.components.august.mocks import ( @@ -318,3 +321,46 @@ async def test_load_unload(hass): await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() + + +async def remove_device(ws_client, device_id, config_entry_id): + """Remove config entry from a device.""" + await ws_client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry_id, + "device_id": device_id, + } + ) + response = await ws_client.receive_json() + return response["success"] + + +async def test_device_remove_devices(hass, hass_ws_client): + """Test we can only remove a device that no longer exists.""" + assert await async_setup_component(hass, "config", {}) + august_operative_lock = await _mock_operative_august_lock_detail(hass) + config_entry = await _create_august_with_devices(hass, [august_operative_lock]) + registry: EntityRegistry = er.async_get(hass) + entity = registry.entities["lock.a6697750d607098bae8d6baa11ef8063_name"] + + device_registry = dr.async_get(hass) + device_entry = device_registry.async_get(entity.device_id) + assert ( + await remove_device( + await hass_ws_client(hass), device_entry.id, config_entry.entry_id + ) + is False + ) + + dead_device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, "remove-device-id")}, + ) + assert ( + await remove_device( + await hass_ws_client(hass), dead_device_entry.id, config_entry.entry_id + ) + is True + ) From 3d78240ceecf3263e69eeb58484a86b30ddc23b9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 10 Jun 2022 11:11:40 +0200 Subject: [PATCH 1398/3516] Fix initial tilt value of MQTT cover (#73308) --- homeassistant/components/mqtt/cover.py | 2 -- tests/components/mqtt/test_cover.py | 8 ++------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 325433817c0..8d4df0c301d 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -25,7 +25,6 @@ from homeassistant.const import ( STATE_CLOSING, STATE_OPEN, STATE_OPENING, - STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -470,7 +469,6 @@ class MqttCover(MqttEntity, CoverEntity): } if self._config.get(CONF_TILT_STATUS_TOPIC) is not None: - self._tilt_value = STATE_UNKNOWN topics["tilt_status_topic"] = { "topic": self._config.get(CONF_TILT_STATUS_TOPIC), "msg_callback": tilt_message_received, diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 5796c12f3cf..31e30ebf11a 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -1256,12 +1256,8 @@ async def test_tilt_defaults(hass, mqtt_mock_entry_with_yaml_config): await mqtt_mock_entry_with_yaml_config() state_attributes_dict = hass.states.get("cover.test").attributes - assert ATTR_CURRENT_TILT_POSITION in state_attributes_dict - - current_cover_position = hass.states.get("cover.test").attributes[ - ATTR_CURRENT_TILT_POSITION - ] - assert current_cover_position == STATE_UNKNOWN + # Tilt position is not yet known + assert ATTR_CURRENT_TILT_POSITION not in state_attributes_dict async def test_tilt_via_invocation_defaults(hass, mqtt_mock_entry_with_yaml_config): From b4a6ccfbc8f9107e160cf88d3e8227d4f0324060 Mon Sep 17 00:00:00 2001 From: Matrix Date: Fri, 10 Jun 2022 20:18:46 +0800 Subject: [PATCH 1399/3516] Add yolink thermostat support (#73243) * Add yolink thermostat support * suggest and bugs fix * fix suggest and bugs --- .coveragerc | 1 + homeassistant/components/yolink/__init__.py | 1 + homeassistant/components/yolink/climate.py | 135 ++++++++++++++++++++ homeassistant/components/yolink/const.py | 1 + 4 files changed, 138 insertions(+) create mode 100644 homeassistant/components/yolink/climate.py diff --git a/.coveragerc b/.coveragerc index ea2c41f7e46..ef1a2adb712 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1496,6 +1496,7 @@ omit = homeassistant/components/yolink/__init__.py homeassistant/components/yolink/api.py homeassistant/components/yolink/binary_sensor.py + homeassistant/components/yolink/climate.py homeassistant/components/yolink/const.py homeassistant/components/yolink/coordinator.py homeassistant/components/yolink/entity.py diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 21d36d33a30..92068d1e26e 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -26,6 +26,7 @@ SCAN_INTERVAL = timedelta(minutes=5) PLATFORMS = [ Platform.BINARY_SENSOR, + Platform.CLIMATE, Platform.LOCK, Platform.SENSOR, Platform.SIREN, diff --git a/homeassistant/components/yolink/climate.py b/homeassistant/components/yolink/climate.py new file mode 100644 index 00000000000..1f877571d94 --- /dev/null +++ b/homeassistant/components/yolink/climate.py @@ -0,0 +1,135 @@ +"""YoLink Thermostat.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.climate import ClimateEntity, ClimateEntityFeature +from homeassistant.components.climate.const import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + FAN_AUTO, + FAN_ON, + PRESET_ECO, + PRESET_NONE, + HVACAction, + HVACMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ATTR_COORDINATORS, ATTR_DEVICE_THERMOSTAT, DOMAIN +from .coordinator import YoLinkCoordinator +from .entity import YoLinkEntity + +YOLINK_MODEL_2_HA = { + "cool": HVACMode.COOL, + "heat": HVACMode.HEAT, + "auto": HVACMode.AUTO, + "off": HVACMode.OFF, +} + +HA_MODEL_2_YOLINK = {v: k for k, v in YOLINK_MODEL_2_HA.items()} + +YOLINK_ACTION_2_HA = { + "cool": HVACAction.COOLING, + "heat": HVACAction.HEATING, + "idle": HVACAction.IDLE, +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up YoLink Thermostat from a config entry.""" + device_coordinators = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATORS] + entities = [ + YoLinkClimateEntity(config_entry, device_coordinator) + for device_coordinator in device_coordinators.values() + if device_coordinator.device.device_type == ATTR_DEVICE_THERMOSTAT + ] + async_add_entities(entities) + + +class YoLinkClimateEntity(YoLinkEntity, ClimateEntity): + """YoLink Climate Entity.""" + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: YoLinkCoordinator, + ) -> None: + """Init YoLink Thermostat.""" + super().__init__(config_entry, coordinator) + self._attr_unique_id = f"{coordinator.device.device_id}_climate" + self._attr_name = f"{coordinator.device.device_name} (Thermostat)" + self._attr_temperature_unit = TEMP_CELSIUS + self._attr_fan_modes = [FAN_ON, FAN_AUTO] + self._attr_min_temp = -10 + self._attr_max_temp = 50 + self._attr_hvac_modes = [ + HVACMode.COOL, + HVACMode.HEAT, + HVACMode.AUTO, + HVACMode.OFF, + ] + self._attr_preset_modes = [PRESET_NONE, PRESET_ECO] + self._attr_supported_features = ( + ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + ) + + @callback + def update_entity_state(self, state: dict[str, Any]) -> None: + """Update HA Entity State.""" + normal_state = state.get("state") + if normal_state is not None: + self._attr_current_temperature = normal_state.get("temperature") + self._attr_current_humidity = normal_state.get("humidity") + self._attr_target_temperature_low = normal_state.get("lowTemp") + self._attr_target_temperature_high = normal_state.get("highTemp") + self._attr_fan_mode = normal_state.get("fan") + self._attr_hvac_mode = YOLINK_MODEL_2_HA.get(normal_state.get("mode")) + self._attr_hvac_action = YOLINK_ACTION_2_HA.get(normal_state.get("running")) + eco_setting = state.get("eco") + if eco_setting is not None: + self._attr_preset_mode = ( + PRESET_NONE if eco_setting.get("mode") == "on" else PRESET_ECO + ) + self.async_write_ha_state() + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target hvac mode.""" + if (hvac_mode_id := HA_MODEL_2_YOLINK.get(hvac_mode)) is None: + raise ValueError(f"Received an invalid hvac mode: {hvac_mode}") + await self.call_device_api("setState", {"mode": hvac_mode_id}) + await self.coordinator.async_refresh() + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set fan mode.""" + await self.call_device_api("setState", {"fan": fan_mode}) + self._attr_fan_mode = fan_mode + self.async_write_ha_state() + + async def async_set_temperature(self, **kwargs) -> None: + """Set temperature.""" + target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if target_temp_low is not None: + await self.call_device_api("setState", {"lowTemp": target_temp_low}) + self._attr_target_temperature_low = target_temp_low + if target_temp_high is not None: + await self.call_device_api("setState", {"highTemp": target_temp_high}) + self._attr_target_temperature_high = target_temp_high + await self.coordinator.async_refresh() + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set preset mode.""" + eco_params = "on" if preset_mode == PRESET_ECO else "off" + await self.call_device_api("setECO", {"mode": eco_params}) + self._attr_preset_mode = PRESET_ECO if eco_params == "on" else PRESET_NONE + self.async_write_ha_state() diff --git a/homeassistant/components/yolink/const.py b/homeassistant/components/yolink/const.py index dba0a0ee221..f6add984dc2 100644 --- a/homeassistant/components/yolink/const.py +++ b/homeassistant/components/yolink/const.py @@ -24,3 +24,4 @@ ATTR_DEVICE_LOCK = "Lock" ATTR_DEVICE_MANIPULATOR = "Manipulator" ATTR_DEVICE_CO_SMOKE_SENSOR = "COSmokeSensor" ATTR_DEVICE_SWITCH = "Switch" +ATTR_DEVICE_THERMOSTAT = "Thermostat" From de2fade8c68cac9aa72dd868e79ee67b81934f5f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 10 Jun 2022 14:23:08 +0200 Subject: [PATCH 1400/3516] Improve MQTT reload performance (#73313) * Improve MQTT reload performance * Update homeassistant/components/mqtt/__init__.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/mqtt/mixins.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/mqtt/__init__.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/mqtt/__init__.py | 30 +++++++++++++++++++++-- homeassistant/components/mqtt/const.py | 2 ++ homeassistant/components/mqtt/mixins.py | 21 ++++++---------- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e21885d2585..f9a6ebd025f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -28,7 +28,14 @@ from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.device_registry import DeviceEntry -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.reload import ( + async_integration_yaml_config, + async_setup_reload_service, +) from homeassistant.helpers.typing import ConfigType # Loading the config flow file will register the flow @@ -60,12 +67,14 @@ from .const import ( # noqa: F401 DATA_MQTT, DATA_MQTT_CONFIG, DATA_MQTT_RELOAD_NEEDED, + DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, DEFAULT_QOS, DEFAULT_RETAIN, DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, + MQTT_RELOADED, PLATFORMS, ) from .models import ( # noqa: F401 @@ -227,7 +236,9 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) - await _async_setup_discovery(hass, mqtt_client.conf, entry) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( # noqa: C901 + hass: HomeAssistant, entry: ConfigEntry +) -> bool: """Load a config entry.""" # Merge basic configuration, and add missing defaults for basic options _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) @@ -364,6 +375,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[CONFIG_ENTRY_IS_SETUP] = set() + # Setup reload service. Once support for legacy config is removed in 2022.9, we + # should no longer call async_setup_reload_service but instead implement a custom + # service + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + + async def _async_reload_platforms(_: Event | None) -> None: + """Discover entities for a platform.""" + config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {} + hass.data[DATA_MQTT_UPDATED_CONFIG] = config_yaml.get(DOMAIN, {}) + async_dispatcher_send(hass, MQTT_RELOADED) + async def async_forward_entry_setup(): """Forward the config entry setup to the platforms.""" async with hass.data[DATA_CONFIG_ENTRY_LOCK]: @@ -374,6 +396,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setup( entry, component ) + # Setup reload service after all platforms have loaded + entry.async_on_unload( + hass.bus.async_listen("event_mqtt_reloaded", _async_reload_platforms) + ) hass.async_create_task(async_forward_entry_setup()) diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 2f7e27e7252..b05fd867eeb 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -35,6 +35,7 @@ DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" DATA_MQTT = "mqtt" DATA_MQTT_CONFIG = "mqtt_config" DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" +DATA_MQTT_UPDATED_CONFIG = "mqtt_updated_config" DEFAULT_PREFIX = "homeassistant" DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status" @@ -63,6 +64,7 @@ DOMAIN = "mqtt" MQTT_CONNECTED = "mqtt_connected" MQTT_DISCONNECTED = "mqtt_disconnected" +MQTT_RELOADED = "mqtt_reloaded" PAYLOAD_EMPTY_JSON = "{}" PAYLOAD_NONE = "None" diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index b0f17cc335b..e768c2ff409 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -48,10 +48,6 @@ from homeassistant.helpers.entity import ( async_generate_entity_id, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import ( - async_integration_yaml_config, - async_setup_reload_service, -) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import debug_info, subscription @@ -67,13 +63,14 @@ from .const import ( DATA_MQTT, DATA_MQTT_CONFIG, DATA_MQTT_RELOAD_NEEDED, + DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, DEFAULT_PAYLOAD_AVAILABLE, DEFAULT_PAYLOAD_NOT_AVAILABLE, DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, - PLATFORMS, + MQTT_RELOADED, ) from .debug_info import log_message, log_messages from .discovery import ( @@ -270,14 +267,11 @@ async def async_setup_platform_discovery( ) -> CALLBACK_TYPE: """Set up platform discovery for manual config.""" - async def _async_discover_entities(event: Event | None) -> None: + async def _async_discover_entities() -> None: """Discover entities for a platform.""" - if event: + if DATA_MQTT_UPDATED_CONFIG in hass.data: # The platform has been reloaded - config_yaml = await async_integration_yaml_config(hass, DOMAIN) - if not config_yaml: - return - config_yaml = config_yaml.get(DOMAIN, {}) + config_yaml = hass.data[DATA_MQTT_UPDATED_CONFIG] else: config_yaml = hass.data.get(DATA_MQTT_CONFIG, {}) if not config_yaml: @@ -293,8 +287,8 @@ async def async_setup_platform_discovery( ) ) - unsub = hass.bus.async_listen("event_mqtt_reloaded", _async_discover_entities) - await _async_discover_entities(None) + unsub = async_dispatcher_connect(hass, MQTT_RELOADED, _async_discover_entities) + await _async_discover_entities() return unsub @@ -359,7 +353,6 @@ async def async_setup_platform_helper( async_setup_entities: SetupEntity, ) -> None: """Return true if platform setup should be aborted.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) if not bool(hass.config_entries.async_entries(DOMAIN)): hass.data[DATA_MQTT_RELOAD_NEEDED] = None _LOGGER.warning( From a82a1bfd64708a044af7a716b5e9e057b1656f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Fri, 10 Jun 2022 15:41:42 +0200 Subject: [PATCH 1401/3516] Allow more addon image paths (#73322) --- homeassistant/components/hassio/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 497e246ea77..3f492114545 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -45,7 +45,7 @@ NO_TIMEOUT = re.compile( NO_AUTH_ONBOARDING = re.compile(r"^(?:" r"|supervisor/logs" r"|backups/[^/]+/.+" r")$") NO_AUTH = re.compile( - r"^(?:" r"|app/.*" r"|addons/[^/]+/logo" r"|addons/[^/]+/icon" r")$" + r"^(?:" r"|app/.*" r"|[store\/]*addons/[^/]+/(logo|dark_logo|icon|dark_icon)" r")$" ) NO_STORE = re.compile(r"^(?:" r"|app/entrypoint.js" r")$") From 8ffd4cf0f900a867c3135ec9018c868282d0ce3d Mon Sep 17 00:00:00 2001 From: hesselonline Date: Fri, 10 Jun 2022 20:55:55 +0200 Subject: [PATCH 1402/3516] Fix wallbox sensor rounding (#73310) --- homeassistant/components/wallbox/manifest.json | 4 ---- homeassistant/components/wallbox/sensor.py | 8 ++++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wallbox/manifest.json b/homeassistant/components/wallbox/manifest.json index 914adda980a..5c195b8bfce 100644 --- a/homeassistant/components/wallbox/manifest.json +++ b/homeassistant/components/wallbox/manifest.json @@ -4,10 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wallbox", "requirements": ["wallbox==0.4.9"], - "ssdp": [], - "zeroconf": [], - "homekit": {}, - "dependencies": [], "codeowners": ["@hesselonline"], "iot_class": "cloud_polling", "loggers": ["wallbox"] diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index c8ad3cb3a67..e3598ca7e07 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -167,8 +167,12 @@ class WallboxSensor(WallboxEntity, SensorEntity): @property def native_value(self) -> StateType: - """Return the state of the sensor.""" - if (sensor_round := self.entity_description.precision) is not None: + """Return the state of the sensor. Round the value when it, and the precision property are not None.""" + if ( + sensor_round := self.entity_description.precision + ) is not None and self.coordinator.data[ + self.entity_description.key + ] is not None: return cast( StateType, round(self.coordinator.data[self.entity_description.key], sensor_round), From 2b07082cf6b8521d0c7e2ddc922b4dbd77a1aea4 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Fri, 10 Jun 2022 20:19:02 +0100 Subject: [PATCH 1403/3516] Bump aurorapy version to 0.2.7 (#73327) Co-authored-by: Dave T --- homeassistant/components/aurora_abb_powerone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aurora_abb_powerone/manifest.json b/homeassistant/components/aurora_abb_powerone/manifest.json index 056f2dc98c2..1207932ae1a 100644 --- a/homeassistant/components/aurora_abb_powerone/manifest.json +++ b/homeassistant/components/aurora_abb_powerone/manifest.json @@ -3,7 +3,7 @@ "name": "Aurora ABB PowerOne Solar PV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/aurora_abb_powerone", - "requirements": ["aurorapy==0.2.6"], + "requirements": ["aurorapy==0.2.7"], "codeowners": ["@davet2001"], "iot_class": "local_polling", "loggers": ["aurorapy"] diff --git a/requirements_all.txt b/requirements_all.txt index 5f27d8c524a..b8d114cfe8d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -354,7 +354,7 @@ atenpdu==0.3.2 auroranoaa==0.0.2 # homeassistant.components.aurora_abb_powerone -aurorapy==0.2.6 +aurorapy==0.2.7 # homeassistant.components.generic # homeassistant.components.stream diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 900b8fda255..e247bf42d82 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -290,7 +290,7 @@ asyncsleepiq==1.2.3 auroranoaa==0.0.2 # homeassistant.components.aurora_abb_powerone -aurorapy==0.2.6 +aurorapy==0.2.7 # homeassistant.components.generic # homeassistant.components.stream From 53b3d2ee87c35f08008f035414538e9858a2f6b1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 10 Jun 2022 12:49:58 -0700 Subject: [PATCH 1404/3516] Guard MySQL size calculation returning None (#73331) --- .../recorder/system_health/mysql.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/recorder/system_health/mysql.py b/homeassistant/components/recorder/system_health/mysql.py index 52ea06f61c3..747a806c227 100644 --- a/homeassistant/components/recorder/system_health/mysql.py +++ b/homeassistant/components/recorder/system_health/mysql.py @@ -5,15 +5,18 @@ from sqlalchemy import text from sqlalchemy.orm.session import Session -def db_size_bytes(session: Session, database_name: str) -> float: +def db_size_bytes(session: Session, database_name: str) -> float | None: """Get the mysql database size.""" - return float( - session.execute( - text( - "SELECT ROUND(SUM(DATA_LENGTH + INDEX_LENGTH), 2) " - "FROM information_schema.TABLES WHERE " - "TABLE_SCHEMA=:database_name" - ), - {"database_name": database_name}, - ).first()[0] - ) + size = session.execute( + text( + "SELECT ROUND(SUM(DATA_LENGTH + INDEX_LENGTH), 2) " + "FROM information_schema.TABLES WHERE " + "TABLE_SCHEMA=:database_name" + ), + {"database_name": database_name}, + ).first()[0] + + if size is None: + return None + + return float(size) From 8cf6f501931640cd8c69143f81a2323ab2d60c24 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 8 Jun 2022 10:52:01 +0200 Subject: [PATCH 1405/3516] Ensure netgear devices are tracked with one enabled config entry (#72969) Co-authored-by: Martin Hjelmare --- homeassistant/components/netgear/__init__.py | 7 +++---- homeassistant/components/netgear/router.py | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index 7a4d0e7a8cd..679a93f8da1 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -18,7 +18,6 @@ from .const import ( KEY_COORDINATOR_SPEED, KEY_COORDINATOR_TRAFFIC, KEY_ROUTER, - MODE_ROUTER, PLATFORMS, ) from .errors import CannotLoginException @@ -72,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update_devices() -> bool: """Fetch data from the router.""" - if router.mode == MODE_ROUTER: + if router.track_devices: return await router.async_update_device_trackers() return False @@ -107,7 +106,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval=SPEED_TEST_INTERVAL, ) - if router.mode == MODE_ROUTER: + if router.track_devices: await coordinator.async_config_entry_first_refresh() await coordinator_traffic_meter.async_config_entry_first_refresh() @@ -134,7 +133,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) - if router.mode != MODE_ROUTER: + if not router.track_devices: router_id = None # Remove devices that are no longer tracked device_registry = dr.async_get(hass) diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 301906f22b6..67e573d0e92 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -80,6 +80,7 @@ class NetgearRouter: self.hardware_version = "" self.serial_number = "" + self.track_devices = True self.method_version = 1 consider_home_int = entry.options.get( CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds() @@ -112,11 +113,23 @@ class NetgearRouter: self.serial_number = self._info["SerialNumber"] self.mode = self._info.get("DeviceMode", MODE_ROUTER) + enabled_entries = [ + entry + for entry in self.hass.config_entries.async_entries(DOMAIN) + if entry.disabled_by is None + ] + self.track_devices = self.mode == MODE_ROUTER or len(enabled_entries) == 1 + _LOGGER.debug( + "Netgear track_devices = '%s', device mode '%s'", + self.track_devices, + self.mode, + ) + for model in MODELS_V2: if self.model.startswith(model): self.method_version = 2 - if self.method_version == 2 and self.mode == MODE_ROUTER: + if self.method_version == 2 and self.track_devices: if not self._api.get_attached_devices_2(): _LOGGER.error( "Netgear Model '%s' in MODELS_V2 list, but failed to get attached devices using V2", @@ -133,7 +146,7 @@ class NetgearRouter: return False # set already known devices to away instead of unavailable - if self.mode == MODE_ROUTER: + if self.track_devices: device_registry = dr.async_get(self.hass) devices = dr.async_entries_for_config_entry(device_registry, self.entry_id) for device_entry in devices: From 498da3bba3da12c3d2291583d648c722e51ff865 Mon Sep 17 00:00:00 2001 From: Matrix Date: Wed, 8 Jun 2022 14:11:41 +0800 Subject: [PATCH 1406/3516] Bump yolink-api to 0.0.8 (#73173) * update api libray fix hearbeat message valiation * update yolink-api ignore invalidate message --- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index 7fb78a4974b..d2a02a44c42 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -3,7 +3,7 @@ "name": "YoLink", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yolink", - "requirements": ["yolink-api==0.0.6"], + "requirements": ["yolink-api==0.0.8"], "dependencies": ["auth", "application_credentials"], "codeowners": ["@matrixd2"], "iot_class": "cloud_push" diff --git a/requirements_all.txt b/requirements_all.txt index d0ac6af8c74..63212e94b4c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2486,7 +2486,7 @@ yeelight==0.7.10 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.0.6 +yolink-api==0.0.8 # homeassistant.components.youless youless-api==0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24916a4ac0d..1d4479f9d77 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1638,7 +1638,7 @@ yalexs==1.1.25 yeelight==0.7.10 # homeassistant.components.yolink -yolink-api==0.0.6 +yolink-api==0.0.8 # homeassistant.components.youless youless-api==0.16 From a24463f7ea2414672d47f20b35ca6ca6f1c15cd5 Mon Sep 17 00:00:00 2001 From: d0nni3q84 <62199227+d0nni3q84@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:32:01 -0500 Subject: [PATCH 1407/3516] Fix Feedreader Atom feeds using `updated` date (#73208) * Feedreader: Properly support Atom feeds that use only the `updated` date format and resolve #73207. * Revert "Feedreader: Properly support Atom feeds that use only the `updated` date format and resolve #73207." This reverts commit 4dbd11ee04b4e8f935a22dfb51405b7bdaaba676. * Properly support Atom feeds that use only the `updated` date format and resolve #73207. * Revert "Properly support Atom feeds that use only the `updated` date format and resolve #73207." This reverts commit 14366c6a2491584282b8bb96fe3779fd41849897. * Properly support Atom feeds that use only the `updated` date format and resolve #73207. --- .../components/feedreader/__init__.py | 31 ++++++++++++++---- tests/components/feedreader/test_init.py | 32 ++++++++++++++++++- tests/fixtures/feedreader5.xml | 18 +++++++++++ 3 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 tests/fixtures/feedreader5.xml diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index a4cd546aa16..d06aeec4932 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -70,6 +70,7 @@ class FeedManager: self._last_entry_timestamp = None self._last_update_successful = False self._has_published_parsed = False + self._has_updated_parsed = False self._event_type = EVENT_FEEDREADER self._feed_id = url hass.bus.listen_once(EVENT_HOMEASSISTANT_START, lambda _: self._update()) @@ -122,7 +123,7 @@ class FeedManager: ) self._filter_entries() self._publish_new_entries() - if self._has_published_parsed: + if self._has_published_parsed or self._has_updated_parsed: self._storage.put_timestamp( self._feed_id, self._last_entry_timestamp ) @@ -143,7 +144,7 @@ class FeedManager: def _update_and_fire_entry(self, entry): """Update last_entry_timestamp and fire entry.""" - # Check if the entry has a published date. + # Check if the entry has a published or updated date. if "published_parsed" in entry and entry.published_parsed: # We are lucky, `published_parsed` data available, let's make use of # it to publish only new available entries since the last run @@ -151,9 +152,20 @@ class FeedManager: self._last_entry_timestamp = max( entry.published_parsed, self._last_entry_timestamp ) + elif "updated_parsed" in entry and entry.updated_parsed: + # We are lucky, `updated_parsed` data available, let's make use of + # it to publish only new available entries since the last run + self._has_updated_parsed = True + self._last_entry_timestamp = max( + entry.updated_parsed, self._last_entry_timestamp + ) else: self._has_published_parsed = False - _LOGGER.debug("No published_parsed info available for entry %s", entry) + self._has_updated_parsed = False + _LOGGER.debug( + "No published_parsed or updated_parsed info available for entry %s", + entry, + ) entry.update({"feed_url": self._url}) self._hass.bus.fire(self._event_type, entry) @@ -167,9 +179,16 @@ class FeedManager: # Set last entry timestamp as epoch time if not available self._last_entry_timestamp = datetime.utcfromtimestamp(0).timetuple() for entry in self._feed.entries: - if self._firstrun or ( - "published_parsed" in entry - and entry.published_parsed > self._last_entry_timestamp + if ( + self._firstrun + or ( + "published_parsed" in entry + and entry.published_parsed > self._last_entry_timestamp + ) + or ( + "updated_parsed" in entry + and entry.updated_parsed > self._last_entry_timestamp + ) ): self._update_and_fire_entry(entry) new_entries = True diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index 34f27c36a6c..be5ebb42a6d 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -23,6 +23,7 @@ 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}} +VALID_CONFIG_5 = {feedreader.DOMAIN: {CONF_URLS: [URL], CONF_MAX_ENTRIES: 1}} def load_fixture_bytes(src): @@ -56,6 +57,12 @@ def fixture_feed_three_events(hass): return load_fixture_bytes("feedreader3.xml") +@pytest.fixture(name="feed_atom_event") +def fixture_feed_atom_event(hass): + """Load test feed data for atom event.""" + return load_fixture_bytes("feedreader5.xml") + + @pytest.fixture(name="events") async def fixture_events(hass): """Fixture that catches alexa events.""" @@ -98,7 +105,7 @@ async def test_setup_max_entries(hass): async def test_feed(hass, events, feed_one_event): - """Test simple feed with valid data.""" + """Test simple rss feed with valid data.""" with patch( "feedparser.http.get", return_value=feed_one_event, @@ -120,6 +127,29 @@ async def test_feed(hass, events, feed_one_event): assert events[0].data.published_parsed.tm_min == 10 +async def test_atom_feed(hass, events, feed_atom_event): + """Test simple atom feed with valid data.""" + with patch( + "feedparser.http.get", + return_value=feed_atom_event, + ): + assert await async_setup_component(hass, feedreader.DOMAIN, VALID_CONFIG_5) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].data.title == "Atom-Powered Robots Run Amok" + assert events[0].data.description == "Some text." + assert events[0].data.link == "http://example.org/2003/12/13/atom03" + assert events[0].data.id == "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a" + assert events[0].data.updated_parsed.tm_year == 2003 + assert events[0].data.updated_parsed.tm_mon == 12 + assert events[0].data.updated_parsed.tm_mday == 13 + assert events[0].data.updated_parsed.tm_hour == 18 + assert events[0].data.updated_parsed.tm_min == 30 + + async def test_feed_updates(hass, events, feed_one_event, feed_two_event): """Test feed updates.""" side_effect = [ diff --git a/tests/fixtures/feedreader5.xml b/tests/fixtures/feedreader5.xml new file mode 100644 index 00000000000..d9b1dda1ad2 --- /dev/null +++ b/tests/fixtures/feedreader5.xml @@ -0,0 +1,18 @@ + + + Example Feed + + 2003-12-13T18:30:02Z + + John Doe + + urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6 + + Atom-Powered Robots Run Amok + + urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a + 2003-12-13T18:30:02Z + Some text. + + From 89dfd4b1624d8dd34d0f740d4696cfd4241a3fd6 Mon Sep 17 00:00:00 2001 From: Khole Date: Fri, 10 Jun 2022 04:54:24 +0100 Subject: [PATCH 1408/3516] Hive auth fix for users (#73247) --- homeassistant/components/hive/config_flow.py | 7 +++-- homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hive/test_config_flow.py | 30 -------------------- 5 files changed, 8 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/hive/config_flow.py b/homeassistant/components/hive/config_flow.py index c713a3011f4..90c78aefcbd 100644 --- a/homeassistant/components/hive/config_flow.py +++ b/homeassistant/components/hive/config_flow.py @@ -27,6 +27,7 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.data = {} self.tokens = {} self.entry = None + self.device_registration = False async def async_step_user(self, user_input=None): """Prompt user input. Create or edit entry.""" @@ -88,6 +89,7 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not errors: try: + self.device_registration = True return await self.async_setup_hive_entry() except UnknownHiveError: errors["base"] = "unknown" @@ -102,9 +104,10 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): raise UnknownHiveError # Setup the config entry - await self.hive_auth.device_registration("Home Assistant") + if self.device_registration: + await self.hive_auth.device_registration("Home Assistant") + self.data["device_data"] = await self.hive_auth.getDeviceData() self.data["tokens"] = self.tokens - self.data["device_data"] = await self.hive_auth.getDeviceData() if self.context["source"] == config_entries.SOURCE_REAUTH: self.hass.config_entries.async_update_entry( self.entry, title=self.data["username"], data=self.data diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index d8cd56abe0b..e0faa9e0f20 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,7 +3,7 @@ "name": "Hive", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": ["pyhiveapi==0.5.5"], + "requirements": ["pyhiveapi==0.5.9"], "codeowners": ["@Rendili", "@KJonline"], "iot_class": "cloud_polling", "loggers": ["apyhiveapi"] diff --git a/requirements_all.txt b/requirements_all.txt index 63212e94b4c..7cb70b9e8f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1538,7 +1538,7 @@ pyheos==0.7.2 pyhik==0.3.0 # homeassistant.components.hive -pyhiveapi==0.5.5 +pyhiveapi==0.5.9 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d4479f9d77..af361e4a6b9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1029,7 +1029,7 @@ pyhaversion==22.4.1 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.5.5 +pyhiveapi==0.5.9 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/tests/components/hive/test_config_flow.py b/tests/components/hive/test_config_flow.py index 51ceec43ad2..35e20e8eee3 100644 --- a/tests/components/hive/test_config_flow.py +++ b/tests/components/hive/test_config_flow.py @@ -33,16 +33,6 @@ async def test_import_flow(hass): "AccessToken": "mock-access-token", }, }, - ), patch( - "homeassistant.components.hive.config_flow.Auth.device_registration", - return_value=True, - ), patch( - "homeassistant.components.hive.config_flow.Auth.getDeviceData", - return_value=[ - "mock-device-group-key", - "mock-device-key", - "mock-device-password", - ], ), patch( "homeassistant.components.hive.async_setup", return_value=True ) as mock_setup, patch( @@ -67,11 +57,6 @@ async def test_import_flow(hass): }, "ChallengeName": "SUCCESS", }, - "device_data": [ - "mock-device-group-key", - "mock-device-key", - "mock-device-password", - ], } assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 @@ -96,16 +81,6 @@ async def test_user_flow(hass): "AccessToken": "mock-access-token", }, }, - ), patch( - "homeassistant.components.hive.config_flow.Auth.device_registration", - return_value=True, - ), patch( - "homeassistant.components.hive.config_flow.Auth.getDeviceData", - return_value=[ - "mock-device-group-key", - "mock-device-key", - "mock-device-password", - ], ), patch( "homeassistant.components.hive.async_setup", return_value=True ) as mock_setup, patch( @@ -130,11 +105,6 @@ async def test_user_flow(hass): }, "ChallengeName": "SUCCESS", }, - "device_data": [ - "mock-device-group-key", - "mock-device-key", - "mock-device-password", - ], } assert len(mock_setup.mock_calls) == 1 From db148b65e539bb87aba5d016fcf638f21b187219 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 9 Jun 2022 03:30:13 +0200 Subject: [PATCH 1409/3516] Fix handling of connection error during Synology DSM setup (#73248) * dont reload on conection error during setup * also fetch API errors during update --- .../components/synology_dsm/common.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index e27c7475251..2ca9cbf3ccf 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Callable -from datetime import timedelta import logging from synology_dsm import SynologyDSM @@ -98,7 +97,7 @@ class SynoApi: self._async_setup_api_requests() await self._hass.async_add_executor_job(self._fetch_device_configuration) - await self.async_update() + await self.async_update(first_setup=True) @callback def subscribe(self, api_key: str, unique_id: str) -> Callable[[], None]: @@ -251,7 +250,7 @@ class SynoApi: # ignore API errors during logout pass - async def async_update(self, now: timedelta | None = None) -> None: + async def async_update(self, first_setup: bool = False) -> None: """Update function for updating API information.""" LOGGER.debug("Start data update for '%s'", self._entry.unique_id) self._async_setup_api_requests() @@ -259,14 +258,22 @@ class SynoApi: await self._hass.async_add_executor_job( self.dsm.update, self._with_information ) - except (SynologyDSMLoginFailedException, SynologyDSMRequestException) as err: - LOGGER.warning( - "Connection error during update, fallback by reloading the entry" - ) + except ( + SynologyDSMLoginFailedException, + SynologyDSMRequestException, + SynologyDSMAPIErrorException, + ) as err: LOGGER.debug( "Connection error during update of '%s' with exception: %s", self._entry.unique_id, err, ) + + if first_setup: + raise err + + LOGGER.warning( + "Connection error during update, fallback by reloading the entry" + ) await self._hass.config_entries.async_reload(self._entry.entry_id) return From 44a4f4115f03f4bd4171047659699a1faba99a9d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 8 Jun 2022 16:31:39 -0600 Subject: [PATCH 1410/3516] Bump regenmaschine to 2022.06.1 (#73250) --- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index a61283ea298..e9df60e4697 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.06.0"], + "requirements": ["regenmaschine==2022.06.1"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 7cb70b9e8f6..81c66275128 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2065,7 +2065,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.06.0 +regenmaschine==2022.06.1 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af361e4a6b9..a6a1d9b0f81 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1364,7 +1364,7 @@ rachiopy==1.0.3 radios==0.1.1 # homeassistant.components.rainmachine -regenmaschine==2022.06.0 +regenmaschine==2022.06.1 # homeassistant.components.renault renault-api==0.1.11 From e41cb1e02066e905600611e565d920a072e54a61 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Thu, 9 Jun 2022 13:48:39 +0200 Subject: [PATCH 1411/3516] Improve Netgear logging (#73274) * improve logging * fix black * invert checks --- homeassistant/components/netgear/sensor.py | 44 ++++++++++++++-------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 9dec1ab3390..a1cf134beda 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -5,6 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import date, datetime from decimal import Decimal +import logging from homeassistant.components.sensor import ( RestoreSensor, @@ -34,6 +35,8 @@ from .const import ( ) from .router import NetgearDeviceEntity, NetgearRouter, NetgearRouterEntity +_LOGGER = logging.getLogger(__name__) + SENSOR_TYPES = { "type": SensorEntityDescription( key="type", @@ -114,7 +117,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:upload", index=0, - value=lambda data: data[0] if data is not None else None, + value=lambda data: data[0], ), NetgearSensorEntityDescription( key="NewWeekUpload", @@ -123,7 +126,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:upload", index=1, - value=lambda data: data[1] if data is not None else None, + value=lambda data: data[1], ), NetgearSensorEntityDescription( key="NewWeekDownload", @@ -132,7 +135,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:download", index=0, - value=lambda data: data[0] if data is not None else None, + value=lambda data: data[0], ), NetgearSensorEntityDescription( key="NewWeekDownload", @@ -141,7 +144,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:download", index=1, - value=lambda data: data[1] if data is not None else None, + value=lambda data: data[1], ), NetgearSensorEntityDescription( key="NewMonthUpload", @@ -150,7 +153,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:upload", index=0, - value=lambda data: data[0] if data is not None else None, + value=lambda data: data[0], ), NetgearSensorEntityDescription( key="NewMonthUpload", @@ -159,7 +162,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:upload", index=1, - value=lambda data: data[1] if data is not None else None, + value=lambda data: data[1], ), NetgearSensorEntityDescription( key="NewMonthDownload", @@ -168,7 +171,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:download", index=0, - value=lambda data: data[0] if data is not None else None, + value=lambda data: data[0], ), NetgearSensorEntityDescription( key="NewMonthDownload", @@ -177,7 +180,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:download", index=1, - value=lambda data: data[1] if data is not None else None, + value=lambda data: data[1], ), NetgearSensorEntityDescription( key="NewLastMonthUpload", @@ -186,7 +189,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:upload", index=0, - value=lambda data: data[0] if data is not None else None, + value=lambda data: data[0], ), NetgearSensorEntityDescription( key="NewLastMonthUpload", @@ -195,7 +198,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:upload", index=1, - value=lambda data: data[1] if data is not None else None, + value=lambda data: data[1], ), NetgearSensorEntityDescription( key="NewLastMonthDownload", @@ -204,7 +207,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:download", index=0, - value=lambda data: data[0] if data is not None else None, + value=lambda data: data[0], ), NetgearSensorEntityDescription( key="NewLastMonthDownload", @@ -213,7 +216,7 @@ SENSOR_TRAFFIC_TYPES = [ native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:download", index=1, - value=lambda data: data[1] if data is not None else None, + value=lambda data: data[1], ), ] @@ -372,6 +375,17 @@ class NetgearRouterSensorEntity(NetgearRouterEntity, RestoreSensor): @callback def async_update_device(self) -> None: """Update the Netgear device.""" - if self.coordinator.data is not None: - data = self.coordinator.data.get(self.entity_description.key) - self._value = self.entity_description.value(data) + if self.coordinator.data is None: + return + + data = self.coordinator.data.get(self.entity_description.key) + if data is None: + self._value = None + _LOGGER.debug( + "key '%s' not in Netgear router response '%s'", + self.entity_description.key, + data, + ) + return + + self._value = self.entity_description.value(data) From 6dbe5942ed93d1b2a000154fc2444cd255de9ed3 Mon Sep 17 00:00:00 2001 From: Adam Dullage Date: Fri, 10 Jun 2022 05:37:36 +0100 Subject: [PATCH 1412/3516] Fix polling frequency for Starling integration (#73282) --- homeassistant/components/starlingbank/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/starlingbank/sensor.py b/homeassistant/components/starlingbank/sensor.py index 40f1e0ff3fd..0069fe7a65f 100644 --- a/homeassistant/components/starlingbank/sensor.py +++ b/homeassistant/components/starlingbank/sensor.py @@ -1,6 +1,7 @@ """Support for balance data via the Starling Bank API.""" from __future__ import annotations +from datetime import timedelta import logging import requests @@ -26,6 +27,7 @@ DEFAULT_SANDBOX = False DEFAULT_ACCOUNT_NAME = "Starling" ICON = "mdi:currency-gbp" +SCAN_INTERVAL = timedelta(seconds=180) ACCOUNT_SCHEMA = vol.Schema( { From 1253f7f85d522a6250ed0b408f8bd738310a7e2a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 9 Jun 2022 12:46:13 -0700 Subject: [PATCH 1413/3516] Fix reloading themes crashing if no themes configured (#73287) --- homeassistant/components/frontend/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index c1deb02fc6a..b3907143eb9 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -460,7 +460,7 @@ async def _async_setup_themes( async def reload_themes(_: ServiceCall) -> None: """Reload themes.""" config = await async_hass_config_yaml(hass) - new_themes = config[DOMAIN].get(CONF_THEMES, {}) + new_themes = config.get(DOMAIN, {}).get(CONF_THEMES, {}) hass.data[DATA_THEMES] = new_themes if hass.data[DATA_DEFAULT_THEME] not in new_themes: hass.data[DATA_DEFAULT_THEME] = DEFAULT_THEME From 300d2a08818e571d343360b7ceb14096613dd1e8 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Tue, 7 Jun 2022 14:19:39 -0400 Subject: [PATCH 1414/3516] Bump version of pyunifiprotect to 3.9.0 (#73168) Co-authored-by: J. Nick Koston --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifiprotect/test_binary_sensor.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 898abd73a6f..52f69abea00 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.6.0", "unifi-discovery==1.1.3"], + "requirements": ["pyunifiprotect==3.9.0", "unifi-discovery==1.1.3"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 81c66275128..19bcba89a9e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1993,7 +1993,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.6.0 +pyunifiprotect==3.9.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6a1d9b0f81..d26f67d8267 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1322,7 +1322,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.6.0 +pyunifiprotect==3.9.0 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 60a3d5a8126..88b42d36994 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -168,7 +168,7 @@ async def sensor_fixture( sensor_obj.motion_detected_at = now - timedelta(hours=1) sensor_obj.open_status_changed_at = now - timedelta(hours=1) sensor_obj.alarm_triggered_at = now - timedelta(hours=1) - sensor_obj.tampering_detected_at = now - timedelta(hours=1) + sensor_obj.tampering_detected_at = None mock_entry.api.bootstrap.reset_objects() mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] @@ -204,7 +204,7 @@ async def sensor_none_fixture( sensor_obj.mount_type = MountType.LEAK sensor_obj.battery_status.is_low = False sensor_obj.alarm_settings.is_enabled = False - sensor_obj.tampering_detected_at = now - timedelta(hours=1) + sensor_obj.tampering_detected_at = None mock_entry.api.bootstrap.reset_objects() mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] From 31a4c649ffd73063e35e1e6cd44ac8988da55005 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 8 Jun 2022 19:58:06 -0400 Subject: [PATCH 1415/3516] Bumps version of pyunifiprotect to 3.9.1 (#73252) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 52f69abea00..a27c0125da3 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.9.0", "unifi-discovery==1.1.3"], + "requirements": ["pyunifiprotect==3.9.1", "unifi-discovery==1.1.3"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 19bcba89a9e..9242102e78b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1993,7 +1993,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.9.0 +pyunifiprotect==3.9.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d26f67d8267..af615ce4e7e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1322,7 +1322,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.9.0 +pyunifiprotect==3.9.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 343c2672bbbe367248b133fa6cb7daf7a0fa5c9c Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 9 Jun 2022 23:32:16 -0400 Subject: [PATCH 1416/3516] Bumps version of pyunifiprotect to 3.9.2 to fix compat with protect 2.1.1 (#73299) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index a27c0125da3..199298d76ca 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.9.1", "unifi-discovery==1.1.3"], + "requirements": ["pyunifiprotect==3.9.2", "unifi-discovery==1.1.3"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 9242102e78b..86e7ea1fc44 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1993,7 +1993,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.9.1 +pyunifiprotect==3.9.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af615ce4e7e..d656be540fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1322,7 +1322,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.9.1 +pyunifiprotect==3.9.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From c6b68ed916739da03d217fba986a9d9f33daf180 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 10 Jun 2022 11:11:40 +0200 Subject: [PATCH 1417/3516] Fix initial tilt value of MQTT cover (#73308) --- homeassistant/components/mqtt/cover.py | 2 -- tests/components/mqtt/test_cover.py | 8 ++------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 325433817c0..8d4df0c301d 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -25,7 +25,6 @@ from homeassistant.const import ( STATE_CLOSING, STATE_OPEN, STATE_OPENING, - STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv @@ -470,7 +469,6 @@ class MqttCover(MqttEntity, CoverEntity): } if self._config.get(CONF_TILT_STATUS_TOPIC) is not None: - self._tilt_value = STATE_UNKNOWN topics["tilt_status_topic"] = { "topic": self._config.get(CONF_TILT_STATUS_TOPIC), "msg_callback": tilt_message_received, diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 5796c12f3cf..31e30ebf11a 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -1256,12 +1256,8 @@ async def test_tilt_defaults(hass, mqtt_mock_entry_with_yaml_config): await mqtt_mock_entry_with_yaml_config() state_attributes_dict = hass.states.get("cover.test").attributes - assert ATTR_CURRENT_TILT_POSITION in state_attributes_dict - - current_cover_position = hass.states.get("cover.test").attributes[ - ATTR_CURRENT_TILT_POSITION - ] - assert current_cover_position == STATE_UNKNOWN + # Tilt position is not yet known + assert ATTR_CURRENT_TILT_POSITION not in state_attributes_dict async def test_tilt_via_invocation_defaults(hass, mqtt_mock_entry_with_yaml_config): From f98f7f202261ae60fba528dfe76cc8eb3eda52c0 Mon Sep 17 00:00:00 2001 From: hesselonline Date: Fri, 10 Jun 2022 20:55:55 +0200 Subject: [PATCH 1418/3516] Fix wallbox sensor rounding (#73310) --- homeassistant/components/wallbox/manifest.json | 4 ---- homeassistant/components/wallbox/sensor.py | 8 ++++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wallbox/manifest.json b/homeassistant/components/wallbox/manifest.json index 914adda980a..5c195b8bfce 100644 --- a/homeassistant/components/wallbox/manifest.json +++ b/homeassistant/components/wallbox/manifest.json @@ -4,10 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wallbox", "requirements": ["wallbox==0.4.9"], - "ssdp": [], - "zeroconf": [], - "homekit": {}, - "dependencies": [], "codeowners": ["@hesselonline"], "iot_class": "cloud_polling", "loggers": ["wallbox"] diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index c8ad3cb3a67..e3598ca7e07 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -167,8 +167,12 @@ class WallboxSensor(WallboxEntity, SensorEntity): @property def native_value(self) -> StateType: - """Return the state of the sensor.""" - if (sensor_round := self.entity_description.precision) is not None: + """Return the state of the sensor. Round the value when it, and the precision property are not None.""" + if ( + sensor_round := self.entity_description.precision + ) is not None and self.coordinator.data[ + self.entity_description.key + ] is not None: return cast( StateType, round(self.coordinator.data[self.entity_description.key], sensor_round), From c5adee6821aa4c672b6ec2a0372be40d23b8322f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 10 Jun 2022 14:23:08 +0200 Subject: [PATCH 1419/3516] Improve MQTT reload performance (#73313) * Improve MQTT reload performance * Update homeassistant/components/mqtt/__init__.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/mqtt/mixins.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/mqtt/__init__.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/mqtt/__init__.py | 30 +++++++++++++++++++++-- homeassistant/components/mqtt/const.py | 2 ++ homeassistant/components/mqtt/mixins.py | 21 ++++++---------- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e21885d2585..f9a6ebd025f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -28,7 +28,14 @@ from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.device_registry import DeviceEntry -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.reload import ( + async_integration_yaml_config, + async_setup_reload_service, +) from homeassistant.helpers.typing import ConfigType # Loading the config flow file will register the flow @@ -60,12 +67,14 @@ from .const import ( # noqa: F401 DATA_MQTT, DATA_MQTT_CONFIG, DATA_MQTT_RELOAD_NEEDED, + DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, DEFAULT_QOS, DEFAULT_RETAIN, DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, + MQTT_RELOADED, PLATFORMS, ) from .models import ( # noqa: F401 @@ -227,7 +236,9 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) - await _async_setup_discovery(hass, mqtt_client.conf, entry) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( # noqa: C901 + hass: HomeAssistant, entry: ConfigEntry +) -> bool: """Load a config entry.""" # Merge basic configuration, and add missing defaults for basic options _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) @@ -364,6 +375,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[CONFIG_ENTRY_IS_SETUP] = set() + # Setup reload service. Once support for legacy config is removed in 2022.9, we + # should no longer call async_setup_reload_service but instead implement a custom + # service + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + + async def _async_reload_platforms(_: Event | None) -> None: + """Discover entities for a platform.""" + config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {} + hass.data[DATA_MQTT_UPDATED_CONFIG] = config_yaml.get(DOMAIN, {}) + async_dispatcher_send(hass, MQTT_RELOADED) + async def async_forward_entry_setup(): """Forward the config entry setup to the platforms.""" async with hass.data[DATA_CONFIG_ENTRY_LOCK]: @@ -374,6 +396,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setup( entry, component ) + # Setup reload service after all platforms have loaded + entry.async_on_unload( + hass.bus.async_listen("event_mqtt_reloaded", _async_reload_platforms) + ) hass.async_create_task(async_forward_entry_setup()) diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 2f7e27e7252..b05fd867eeb 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -35,6 +35,7 @@ DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" DATA_MQTT = "mqtt" DATA_MQTT_CONFIG = "mqtt_config" DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" +DATA_MQTT_UPDATED_CONFIG = "mqtt_updated_config" DEFAULT_PREFIX = "homeassistant" DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status" @@ -63,6 +64,7 @@ DOMAIN = "mqtt" MQTT_CONNECTED = "mqtt_connected" MQTT_DISCONNECTED = "mqtt_disconnected" +MQTT_RELOADED = "mqtt_reloaded" PAYLOAD_EMPTY_JSON = "{}" PAYLOAD_NONE = "None" diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index b0f17cc335b..e768c2ff409 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -48,10 +48,6 @@ from homeassistant.helpers.entity import ( async_generate_entity_id, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.reload import ( - async_integration_yaml_config, - async_setup_reload_service, -) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import debug_info, subscription @@ -67,13 +63,14 @@ from .const import ( DATA_MQTT, DATA_MQTT_CONFIG, DATA_MQTT_RELOAD_NEEDED, + DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, DEFAULT_PAYLOAD_AVAILABLE, DEFAULT_PAYLOAD_NOT_AVAILABLE, DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, - PLATFORMS, + MQTT_RELOADED, ) from .debug_info import log_message, log_messages from .discovery import ( @@ -270,14 +267,11 @@ async def async_setup_platform_discovery( ) -> CALLBACK_TYPE: """Set up platform discovery for manual config.""" - async def _async_discover_entities(event: Event | None) -> None: + async def _async_discover_entities() -> None: """Discover entities for a platform.""" - if event: + if DATA_MQTT_UPDATED_CONFIG in hass.data: # The platform has been reloaded - config_yaml = await async_integration_yaml_config(hass, DOMAIN) - if not config_yaml: - return - config_yaml = config_yaml.get(DOMAIN, {}) + config_yaml = hass.data[DATA_MQTT_UPDATED_CONFIG] else: config_yaml = hass.data.get(DATA_MQTT_CONFIG, {}) if not config_yaml: @@ -293,8 +287,8 @@ async def async_setup_platform_discovery( ) ) - unsub = hass.bus.async_listen("event_mqtt_reloaded", _async_discover_entities) - await _async_discover_entities(None) + unsub = async_dispatcher_connect(hass, MQTT_RELOADED, _async_discover_entities) + await _async_discover_entities() return unsub @@ -359,7 +353,6 @@ async def async_setup_platform_helper( async_setup_entities: SetupEntity, ) -> None: """Return true if platform setup should be aborted.""" - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) if not bool(hass.config_entries.async_entries(DOMAIN)): hass.data[DATA_MQTT_RELOAD_NEEDED] = None _LOGGER.warning( From 972aab3c26f41756b9750028479db295bed991ab Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 10 Jun 2022 12:49:58 -0700 Subject: [PATCH 1420/3516] Guard MySQL size calculation returning None (#73331) --- .../recorder/system_health/mysql.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/recorder/system_health/mysql.py b/homeassistant/components/recorder/system_health/mysql.py index 52ea06f61c3..747a806c227 100644 --- a/homeassistant/components/recorder/system_health/mysql.py +++ b/homeassistant/components/recorder/system_health/mysql.py @@ -5,15 +5,18 @@ from sqlalchemy import text from sqlalchemy.orm.session import Session -def db_size_bytes(session: Session, database_name: str) -> float: +def db_size_bytes(session: Session, database_name: str) -> float | None: """Get the mysql database size.""" - return float( - session.execute( - text( - "SELECT ROUND(SUM(DATA_LENGTH + INDEX_LENGTH), 2) " - "FROM information_schema.TABLES WHERE " - "TABLE_SCHEMA=:database_name" - ), - {"database_name": database_name}, - ).first()[0] - ) + size = session.execute( + text( + "SELECT ROUND(SUM(DATA_LENGTH + INDEX_LENGTH), 2) " + "FROM information_schema.TABLES WHERE " + "TABLE_SCHEMA=:database_name" + ), + {"database_name": database_name}, + ).first()[0] + + if size is None: + return None + + return float(size) From 17fd03d8fd691c3f72484fa4d04fc2bce2f21415 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 10 Jun 2022 12:51:48 -0700 Subject: [PATCH 1421/3516] Bumped version to 2022.6.5 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7d887d1a4d7..eeba0f6698c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "4" +PATCH_VERSION: Final = "5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index ed1fdb27e4f..fd13e6f7d09 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.4 +version = 2022.6.5 url = https://www.home-assistant.io/ [options] From e4f354998d8523ead8090f78fce2f9b7b89f949b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Jun 2022 11:04:43 -1000 Subject: [PATCH 1422/3516] Filter out forced updates in live logbook when the state has not changed (#73335) --- homeassistant/components/logbook/helpers.py | 20 +-- .../components/logbook/test_websocket_api.py | 114 ++++++++++++++++++ 2 files changed, 126 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index ef322c44e05..221612e1e97 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -189,9 +189,10 @@ def async_subscribe_events( def _forward_state_events_filtered(event: Event) -> None: if event.data.get("old_state") is None or event.data.get("new_state") is None: return - state: State = event.data["new_state"] - if _is_state_filtered(ent_reg, state) or ( - entities_filter and not entities_filter(state.entity_id) + new_state: State = event.data["new_state"] + old_state: State = event.data["old_state"] + if _is_state_filtered(ent_reg, new_state, old_state) or ( + entities_filter and not entities_filter(new_state.entity_id) ): return target(event) @@ -229,17 +230,20 @@ def is_sensor_continuous(ent_reg: er.EntityRegistry, entity_id: str) -> bool: ) -def _is_state_filtered(ent_reg: er.EntityRegistry, state: State) -> bool: +def _is_state_filtered( + ent_reg: er.EntityRegistry, new_state: State, old_state: State +) -> bool: """Check if the logbook should filter a state. Used when we are in live mode to ensure we only get significant changes (state.last_changed != state.last_updated) """ return bool( - split_entity_id(state.entity_id)[0] in ALWAYS_CONTINUOUS_DOMAINS - or state.last_changed != state.last_updated - or ATTR_UNIT_OF_MEASUREMENT in state.attributes - or is_sensor_continuous(ent_reg, state.entity_id) + new_state.state == old_state.state + or split_entity_id(new_state.entity_id)[0] in ALWAYS_CONTINUOUS_DOMAINS + or new_state.last_changed != new_state.last_updated + or ATTR_UNIT_OF_MEASUREMENT in new_state.attributes + or is_sensor_continuous(ent_reg, new_state.entity_id) ) diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 4df2f456eb6..ac6a31202e7 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -2404,3 +2404,117 @@ async def test_subscribe_entities_some_have_uom_multiple( # Check our listener got unsubscribed assert sum(hass.bus.async_listeners().values()) == init_count + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_logbook_stream_ignores_forced_updates( + hass, recorder_mock, hass_ws_client +): + """Test logbook live stream ignores forced updates.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + + await hass.async_block_till_done() + init_count = sum(hass.bus.async_listeners().values()) + + hass.states.async_set("binary_sensor.is_light", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + state: State = hass.states.get("binary_sensor.is_light") + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + {"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()} + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + { + "entity_id": "binary_sensor.is_light", + "state": "off", + "when": state.last_updated.timestamp(), + } + ] + assert msg["event"]["start_time"] == now.timestamp() + assert msg["event"]["end_time"] > msg["event"]["start_time"] + assert msg["event"]["partial"] is True + + await hass.async_block_till_done() + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [] + + hass.states.async_set("binary_sensor.is_light", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [ + { + "entity_id": "binary_sensor.is_light", + "state": STATE_ON, + "when": ANY, + }, + { + "entity_id": "binary_sensor.is_light", + "state": STATE_OFF, + "when": ANY, + }, + ] + + # Now we force an update to make sure we ignore + # forced updates when the state has not actually changed + + hass.states.async_set("binary_sensor.is_light", STATE_ON) + for _ in range(3): + hass.states.async_set("binary_sensor.is_light", STATE_OFF, force_update=True) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [ + { + "entity_id": "binary_sensor.is_light", + "state": STATE_ON, + "when": ANY, + }, + # We should only get the first one and ignore + # the other forced updates since the state + # has not actually changed + { + "entity_id": "binary_sensor.is_light", + "state": STATE_OFF, + "when": ANY, + }, + ] + + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count From 21cfbe875e9af7a92311374958961195f9fb7748 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Fri, 10 Jun 2022 15:16:47 -0600 Subject: [PATCH 1423/3516] Remove logic to mark litterrobot vacuum entity as unavailable (#73234) --- homeassistant/components/litterrobot/vacuum.py | 7 ------- tests/components/litterrobot/conftest.py | 10 ---------- tests/components/litterrobot/test_vacuum.py | 15 +-------------- 3 files changed, 1 insertion(+), 31 deletions(-) diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index dbe51270857..51be573b14e 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -1,7 +1,6 @@ """Support for Litter-Robot "Vacuum".""" from __future__ import annotations -from datetime import datetime, timedelta, timezone import logging from typing import Any @@ -46,7 +45,6 @@ LITTER_BOX_STATUS_STATE_MAP = { LitterBoxStatus.CAT_SENSOR_INTERRUPTED: STATE_PAUSED, LitterBoxStatus.OFF: STATE_OFF, } -UNAVAILABLE_AFTER = timedelta(minutes=30) async def async_setup_entry( @@ -96,11 +94,6 @@ class LitterRobotCleaner(LitterRobotControlEntity, StateVacuumEntity): | VacuumEntityFeature.TURN_ON ) - @property - def available(self) -> bool: - """Return True if the cleaner has been seen recently.""" - return self.robot.last_seen > datetime.now(timezone.utc) - UNAVAILABLE_AFTER - @property def state(self) -> str: """Return the state of the cleaner.""" diff --git a/tests/components/litterrobot/conftest.py b/tests/components/litterrobot/conftest.py index e8ec5324ae6..0e3d85dc828 100644 --- a/tests/components/litterrobot/conftest.py +++ b/tests/components/litterrobot/conftest.py @@ -1,7 +1,6 @@ """Configure pytest for Litter-Robot tests.""" from __future__ import annotations -from datetime import datetime from typing import Any from unittest.mock import AsyncMock, MagicMock, patch @@ -10,7 +9,6 @@ from pylitterbot.exceptions import InvalidCommandException import pytest from homeassistant.components import litterrobot -from homeassistant.components.litterrobot.vacuum import UNAVAILABLE_AFTER from homeassistant.core import HomeAssistant from .common import CONFIG, ROBOT_DATA @@ -73,14 +71,6 @@ def mock_account_with_sleep_disabled_robot() -> MagicMock: return create_mock_account({"sleepModeActive": "0"}) -@pytest.fixture -def mock_account_with_robot_not_recently_seen() -> MagicMock: - """Mock a Litter-Robot account with a sleeping robot.""" - return create_mock_account( - {"lastSeen": (datetime.now() - UNAVAILABLE_AFTER).isoformat()} - ) - - @pytest.fixture def mock_account_with_error() -> MagicMock: """Mock a Litter-Robot account with error.""" diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index 3adf820d6aa..89f8f077b55 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -24,7 +24,7 @@ from homeassistant.components.vacuum import ( STATE_DOCKED, STATE_ERROR, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow @@ -62,19 +62,6 @@ async def test_vacuum_status_when_sleeping( assert vacuum.attributes.get(ATTR_STATUS) == "Ready (Sleeping)" -async def test_vacuum_state_when_not_recently_seen( - hass: HomeAssistant, mock_account_with_robot_not_recently_seen: MagicMock -) -> None: - """Tests the vacuum state when not seen recently.""" - await setup_integration( - hass, mock_account_with_robot_not_recently_seen, PLATFORM_DOMAIN - ) - - vacuum = hass.states.get(VACUUM_ENTITY_ID) - assert vacuum - assert vacuum.state == STATE_UNAVAILABLE - - async def test_no_robots( hass: HomeAssistant, mock_account_with_no_robots: MagicMock ) -> None: From b1f2e5f897540967ebef2ccf98026d70009b5c4f Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sat, 11 Jun 2022 15:38:43 +1000 Subject: [PATCH 1424/3516] Use create_stream in generic camera config flow (#73237) * Use create_stream in generic camera config flow --- .../components/generic/config_flow.py | 59 ++--- homeassistant/components/generic/strings.json | 10 +- tests/components/generic/conftest.py | 22 +- tests/components/generic/test_camera.py | 218 ++++++++---------- tests/components/generic/test_config_flow.py | 202 +++++++--------- 5 files changed, 220 insertions(+), 291 deletions(-) diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 272c7a2d98e..93b34133c63 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -3,7 +3,6 @@ from __future__ import annotations import contextlib from errno import EHOSTUNREACH, EIO -from functools import partial import io import logging from types import MappingProxyType @@ -11,7 +10,6 @@ from typing import Any import PIL from async_timeout import timeout -import av from httpx import HTTPStatusError, RequestError, TimeoutException import voluptuous as vol import yarl @@ -19,9 +17,10 @@ import yarl from homeassistant.components.stream import ( CONF_RTSP_TRANSPORT, CONF_USE_WALLCLOCK_AS_TIMESTAMPS, + HLS_PROVIDER, RTSP_TRANSPORTS, SOURCE_TIMEOUT, - convert_stream_options, + create_stream, ) from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import ( @@ -198,6 +197,11 @@ async def async_test_stream(hass, info) -> dict[str, str]: """Verify that the stream is valid before we create an entity.""" if not (stream_source := info.get(CONF_STREAM_SOURCE)): return {} + # Import from stream.worker as stream cannot reexport from worker + # without forcing the av dependency on default_config + # pylint: disable=import-outside-toplevel + from homeassistant.components.stream.worker import StreamWorkerError + if not isinstance(stream_source, template_helper.Template): stream_source = template_helper.Template(stream_source, hass) try: @@ -205,42 +209,21 @@ async def async_test_stream(hass, info) -> dict[str, str]: except TemplateError as err: _LOGGER.warning("Problem rendering template %s: %s", stream_source, err) return {CONF_STREAM_SOURCE: "template_error"} + stream_options: dict[str, bool | str] = {} + if rtsp_transport := info.get(CONF_RTSP_TRANSPORT): + stream_options[CONF_RTSP_TRANSPORT] = rtsp_transport + if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): + stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True try: - # For RTSP streams, prefer TCP. This code is duplicated from - # homeassistant.components.stream.__init__.py:create_stream() - # It may be possible & better to call create_stream() directly. - stream_options: dict[str, bool | str] = {} - if rtsp_transport := info.get(CONF_RTSP_TRANSPORT): - stream_options[CONF_RTSP_TRANSPORT] = rtsp_transport - if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): - stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True - pyav_options = convert_stream_options(stream_options) - if isinstance(stream_source, str) and stream_source[:7] == "rtsp://": - pyav_options = { - "rtsp_flags": "prefer_tcp", - "stimeout": "5000000", - **pyav_options, - } - _LOGGER.debug("Attempting to open stream %s", stream_source) - container = await hass.async_add_executor_job( - partial( - av.open, - stream_source, - options=pyav_options, - timeout=SOURCE_TIMEOUT, - ) - ) - _ = container.streams.video[0] - except (av.error.FileNotFoundError): # pylint: disable=c-extension-no-member - return {CONF_STREAM_SOURCE: "stream_file_not_found"} - except (av.error.HTTPNotFoundError): # pylint: disable=c-extension-no-member - return {CONF_STREAM_SOURCE: "stream_http_not_found"} - except (av.error.TimeoutError): # pylint: disable=c-extension-no-member - return {CONF_STREAM_SOURCE: "timeout"} - except av.error.HTTPUnauthorizedError: # pylint: disable=c-extension-no-member - return {CONF_STREAM_SOURCE: "stream_unauthorised"} - except (KeyError, IndexError): - return {CONF_STREAM_SOURCE: "stream_no_video"} + stream = create_stream(hass, stream_source, stream_options, "test_stream") + hls_provider = stream.add_provider(HLS_PROVIDER) + await stream.start() + if not await hls_provider.part_recv(timeout=SOURCE_TIMEOUT): + hass.async_create_task(stream.stop()) + return {CONF_STREAM_SOURCE: "timeout"} + await stream.stop() + except StreamWorkerError as err: + return {CONF_STREAM_SOURCE: str(err)} except PermissionError: return {CONF_STREAM_SOURCE: "stream_not_permitted"} except OSError as err: diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index 6b73c70cf3d..7cb1135fe56 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -12,9 +12,7 @@ "timeout": "Timeout while loading URL", "stream_no_route_to_host": "Could not find host while trying to connect to stream", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", - "stream_unauthorised": "Authorisation failed while trying to connect to stream", - "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", - "stream_no_video": "Stream has no video" + "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?" }, "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", @@ -78,15 +76,11 @@ "unable_still_load": "[%key:component::generic::config::error::unable_still_load%]", "no_still_image_or_stream_url": "[%key:component::generic::config::error::no_still_image_or_stream_url%]", "invalid_still_image": "[%key:component::generic::config::error::invalid_still_image%]", - "stream_file_not_found": "[%key:component::generic::config::error::stream_file_not_found%]", - "stream_http_not_found": "[%key:component::generic::config::error::stream_http_not_found%]", "template_error": "[%key:component::generic::config::error::template_error%]", "timeout": "[%key:component::generic::config::error::timeout%]", "stream_no_route_to_host": "[%key:component::generic::config::error::stream_no_route_to_host%]", "stream_io_error": "[%key:component::generic::config::error::stream_io_error%]", - "stream_unauthorised": "[%key:component::generic::config::error::stream_unauthorised%]", - "stream_not_permitted": "[%key:component::generic::config::error::stream_not_permitted%]", - "stream_no_video": "[%key:component::generic::config::error::stream_no_video%]" + "stream_not_permitted": "[%key:component::generic::config::error::stream_not_permitted%]" } } } diff --git a/tests/components/generic/conftest.py b/tests/components/generic/conftest.py index dc5c545869b..266b848ebe2 100644 --- a/tests/components/generic/conftest.py +++ b/tests/components/generic/conftest.py @@ -1,7 +1,7 @@ """Test fixtures for the generic component.""" from io import BytesIO -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch from PIL import Image import pytest @@ -59,14 +59,20 @@ def fakeimg_gif(fakeimgbytes_gif): @pytest.fixture(scope="package") -def mock_av_open(): - """Fake container object with .streams.video[0] != None.""" - fake = Mock() - fake.streams.video = ["fakevid"] - return patch( - "homeassistant.components.generic.config_flow.av.open", - return_value=fake, +def mock_create_stream(): + """Mock create stream.""" + mock_stream = Mock() + mock_provider = Mock() + mock_provider.part_recv = AsyncMock() + mock_provider.part_recv.return_value = True + mock_stream.add_provider.return_value = mock_provider + mock_stream.start = AsyncMock() + mock_stream.stop = AsyncMock() + fake_create_stream = patch( + "homeassistant.components.generic.config_flow.create_stream", + return_value=mock_stream, ) + return fake_create_stream @pytest.fixture diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 6e8b804f848..ec0d89eb0eb 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -17,26 +17,25 @@ from tests.common import AsyncMock, Mock @respx.mock -async def test_fetching_url(hass, hass_client, fakeimgbytes_png, mock_av_open): +async def test_fetching_url(hass, hass_client, fakeimgbytes_png): """Test that it fetches the given url.""" respx.get("http://example.com").respond(stream=fakeimgbytes_png) - with mock_av_open: - await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "still_image_url": "http://example.com", - "username": "user", - "password": "pass", - "authentication": "basic", - } - }, - ) - await hass.async_block_till_done() + await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "still_image_url": "http://example.com", + "username": "user", + "password": "pass", + "authentication": "basic", + } + }, + ) + await hass.async_block_till_done() client = await hass_client() @@ -179,30 +178,27 @@ async def test_limit_refetch(hass, hass_client, fakeimgbytes_png, fakeimgbytes_j @respx.mock -async def test_stream_source( - hass, hass_client, hass_ws_client, fakeimgbytes_png, mock_av_open -): +async def test_stream_source(hass, hass_client, hass_ws_client, fakeimgbytes_png): """Test that the stream source is rendered.""" respx.get("http://example.com").respond(stream=fakeimgbytes_png) respx.get("http://example.com/0a").respond(stream=fakeimgbytes_png) hass.states.async_set("sensor.temp", "0") - with mock_av_open: - assert await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "still_image_url": "http://example.com", - "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', - "limit_refetch_to_url_change": True, - }, + assert await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "still_image_url": "http://example.com", + "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', + "limit_refetch_to_url_change": True, }, - ) - assert await async_setup_component(hass, "stream", {}) - await hass.async_block_till_done() + }, + ) + assert await async_setup_component(hass, "stream", {}) + await hass.async_block_till_done() hass.states.async_set("sensor.temp", "5") @@ -227,29 +223,26 @@ async def test_stream_source( @respx.mock -async def test_stream_source_error( - hass, hass_client, hass_ws_client, fakeimgbytes_png, mock_av_open -): +async def test_stream_source_error(hass, hass_client, hass_ws_client, fakeimgbytes_png): """Test that the stream source has an error.""" respx.get("http://example.com").respond(stream=fakeimgbytes_png) - with mock_av_open: - assert await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "still_image_url": "http://example.com", - # Does not exist - "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', - "limit_refetch_to_url_change": True, - }, + assert await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "still_image_url": "http://example.com", + # Does not exist + "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', + "limit_refetch_to_url_change": True, }, - ) - assert await async_setup_component(hass, "stream", {}) - await hass.async_block_till_done() + }, + ) + assert await async_setup_component(hass, "stream", {}) + await hass.async_block_till_done() with patch( "homeassistant.components.camera.Stream.endpoint_url", @@ -275,30 +268,27 @@ async def test_stream_source_error( @respx.mock -async def test_setup_alternative_options( - hass, hass_ws_client, fakeimgbytes_png, mock_av_open -): +async def test_setup_alternative_options(hass, hass_ws_client, fakeimgbytes_png): """Test that the stream source is setup with different config options.""" respx.get("https://example.com").respond(stream=fakeimgbytes_png) - with mock_av_open: - assert await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "still_image_url": "https://example.com", - "authentication": "digest", - "username": "user", - "password": "pass", - "stream_source": "rtsp://example.com:554/rtsp/", - "rtsp_transport": "udp", - }, + assert await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "still_image_url": "https://example.com", + "authentication": "digest", + "username": "user", + "password": "pass", + "stream_source": "rtsp://example.com:554/rtsp/", + "rtsp_transport": "udp", }, - ) - await hass.async_block_till_done() + }, + ) + await hass.async_block_till_done() assert hass.states.get("camera.config_test") @@ -346,7 +336,7 @@ async def test_no_stream_source(hass, hass_client, hass_ws_client, fakeimgbytes_ @respx.mock async def test_camera_content_type( - hass, hass_client, fakeimgbytes_svg, fakeimgbytes_jpg, mock_av_open + hass, hass_client, fakeimgbytes_svg, fakeimgbytes_jpg ): """Test generic camera with custom content_type.""" urlsvg = "https://upload.wikimedia.org/wikipedia/commons/0/02/SVG_logo.svg" @@ -372,20 +362,18 @@ async def test_camera_content_type( "verify_ssl": True, } - with mock_av_open: - result1 = await hass.config_entries.flow.async_init( - "generic", - data=cam_config_jpg, - context={"source": SOURCE_IMPORT, "unique_id": 12345}, - ) - await hass.async_block_till_done() - with mock_av_open: - result2 = await hass.config_entries.flow.async_init( - "generic", - data=cam_config_svg, - context={"source": SOURCE_IMPORT, "unique_id": 54321}, - ) - await hass.async_block_till_done() + result1 = await hass.config_entries.flow.async_init( + "generic", + data=cam_config_jpg, + context={"source": SOURCE_IMPORT, "unique_id": 12345}, + ) + await hass.async_block_till_done() + result2 = await hass.config_entries.flow.async_init( + "generic", + data=cam_config_svg, + context={"source": SOURCE_IMPORT, "unique_id": 54321}, + ) + await hass.async_block_till_done() assert result1["type"] == "create_entry" assert result2["type"] == "create_entry" @@ -457,21 +445,20 @@ async def test_timeout_cancelled(hass, hass_client, fakeimgbytes_png, fakeimgbyt assert await resp.read() == fakeimgbytes_png -async def test_no_still_image_url(hass, hass_client, mock_av_open): +async def test_no_still_image_url(hass, hass_client): """Test that the component can grab images from stream with no still_image_url.""" - with mock_av_open: - assert await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "stream_source": "rtsp://example.com:554/rtsp/", - }, + assert await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "stream_source": "rtsp://example.com:554/rtsp/", }, - ) - await hass.async_block_till_done() + }, + ) + await hass.async_block_till_done() client = await hass_client() @@ -503,23 +490,22 @@ async def test_no_still_image_url(hass, hass_client, mock_av_open): assert await resp.read() == b"stream_keyframe_image" -async def test_frame_interval_property(hass, mock_av_open): +async def test_frame_interval_property(hass): """Test that the frame interval is calculated and returned correctly.""" - with mock_av_open: - await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "stream_source": "rtsp://example.com:554/rtsp/", - "framerate": 5, - }, + await async_setup_component( + hass, + "camera", + { + "camera": { + "name": "config_test", + "platform": "generic", + "stream_source": "rtsp://example.com:554/rtsp/", + "framerate": 5, }, - ) - await hass.async_block_till_done() + }, + ) + await hass.async_block_till_done() request = Mock() with patch( diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index a525619d962..ee12056b191 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -2,9 +2,8 @@ import errno import os.path -from unittest.mock import patch +from unittest.mock import AsyncMock, patch -import av import httpx import pytest import respx @@ -23,6 +22,7 @@ from homeassistant.components.stream import ( CONF_RTSP_TRANSPORT, CONF_USE_WALLCLOCK_AS_TIMESTAMPS, ) +from homeassistant.components.stream.worker import StreamWorkerError from homeassistant.const import ( CONF_AUTHENTICATION, CONF_NAME, @@ -57,10 +57,10 @@ TESTDATA_YAML = { @respx.mock -async def test_form(hass, fakeimg_png, mock_av_open, user_flow): +async def test_form(hass, fakeimg_png, user_flow, mock_create_stream): """Test the form with a normal set of settings.""" - with mock_av_open as mock_setup: + with mock_create_stream as mock_setup: result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], TESTDATA, @@ -211,12 +211,12 @@ async def test_still_template( @respx.mock -async def test_form_rtsp_mode(hass, fakeimg_png, mock_av_open, user_flow): +async def test_form_rtsp_mode(hass, fakeimg_png, user_flow, mock_create_stream): """Test we complete ok if the user enters a stream url.""" - with mock_av_open as mock_setup: - data = TESTDATA.copy() - data[CONF_RTSP_TRANSPORT] = "tcp" - data[CONF_STREAM_SOURCE] = "rtsp://127.0.0.1/testurl/2" + data = TESTDATA.copy() + data[CONF_RTSP_TRANSPORT] = "tcp" + data[CONF_STREAM_SOURCE] = "rtsp://127.0.0.1/testurl/2" + with mock_create_stream as mock_setup: result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], data ) @@ -240,7 +240,7 @@ async def test_form_rtsp_mode(hass, fakeimg_png, mock_av_open, user_flow): assert len(mock_setup.mock_calls) == 1 -async def test_form_only_stream(hass, mock_av_open, fakeimgbytes_jpg): +async def test_form_only_stream(hass, fakeimgbytes_jpg, mock_create_stream): """Test we complete ok if the user wants stream only.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -249,7 +249,7 @@ async def test_form_only_stream(hass, mock_av_open, fakeimgbytes_jpg): data = TESTDATA.copy() data.pop(CONF_STILL_IMAGE_URL) data[CONF_STREAM_SOURCE] = "rtsp://user:pass@127.0.0.1/testurl/2" - with mock_av_open as mock_setup: + with mock_create_stream as mock_setup: result3 = await hass.config_entries.flow.async_configure( result["flow_id"], data, @@ -294,13 +294,13 @@ async def test_form_still_and_stream_not_provided(hass, user_flow): @respx.mock -async def test_form_image_timeout(hass, mock_av_open, user_flow): +async def test_form_image_timeout(hass, user_flow, mock_create_stream): """Test we handle invalid image timeout.""" respx.get("http://127.0.0.1/testurl/1").side_effect = [ httpx.TimeoutException, ] - with mock_av_open: + with mock_create_stream: result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], TESTDATA, @@ -312,10 +312,10 @@ async def test_form_image_timeout(hass, mock_av_open, user_flow): @respx.mock -async def test_form_stream_invalidimage(hass, mock_av_open, user_flow): +async def test_form_stream_invalidimage(hass, user_flow, mock_create_stream): """Test we handle invalid image when a stream is specified.""" respx.get("http://127.0.0.1/testurl/1").respond(stream=b"invalid") - with mock_av_open: + with mock_create_stream: result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], TESTDATA, @@ -327,10 +327,10 @@ async def test_form_stream_invalidimage(hass, mock_av_open, user_flow): @respx.mock -async def test_form_stream_invalidimage2(hass, mock_av_open, user_flow): +async def test_form_stream_invalidimage2(hass, user_flow, mock_create_stream): """Test we handle invalid image when a stream is specified.""" respx.get("http://127.0.0.1/testurl/1").respond(content=None) - with mock_av_open: + with mock_create_stream: result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], TESTDATA, @@ -342,10 +342,10 @@ async def test_form_stream_invalidimage2(hass, mock_av_open, user_flow): @respx.mock -async def test_form_stream_invalidimage3(hass, mock_av_open, user_flow): +async def test_form_stream_invalidimage3(hass, user_flow, mock_create_stream): """Test we handle invalid image when a stream is specified.""" respx.get("http://127.0.0.1/testurl/1").respond(content=bytes([0xFF])) - with mock_av_open: + with mock_create_stream: result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], TESTDATA, @@ -356,43 +356,17 @@ async def test_form_stream_invalidimage3(hass, mock_av_open, user_flow): assert result2["errors"] == {"still_image_url": "invalid_still_image"} -@respx.mock -async def test_form_stream_file_not_found(hass, fakeimg_png, user_flow): - """Test we handle file not found.""" - with patch( - "homeassistant.components.generic.config_flow.av.open", - side_effect=av.error.FileNotFoundError(0, 0), - ): - result2 = await hass.config_entries.flow.async_configure( - user_flow["flow_id"], - TESTDATA, - ) - assert result2["type"] == "form" - assert result2["errors"] == {"stream_source": "stream_file_not_found"} - - -@respx.mock -async def test_form_stream_http_not_found(hass, fakeimg_png, user_flow): - """Test we handle invalid auth.""" - with patch( - "homeassistant.components.generic.config_flow.av.open", - side_effect=av.error.HTTPNotFoundError(0, 0), - ): - result2 = await hass.config_entries.flow.async_configure( - user_flow["flow_id"], - TESTDATA, - ) - assert result2["type"] == "form" - assert result2["errors"] == {"stream_source": "stream_http_not_found"} - - @respx.mock async def test_form_stream_timeout(hass, fakeimg_png, user_flow): """Test we handle invalid auth.""" with patch( - "homeassistant.components.generic.config_flow.av.open", - side_effect=av.error.TimeoutError(0, 0), - ): + "homeassistant.components.generic.config_flow.create_stream" + ) as create_stream: + create_stream.return_value.start = AsyncMock() + create_stream.return_value.add_provider.return_value.part_recv = AsyncMock() + create_stream.return_value.add_provider.return_value.part_recv.return_value = ( + False + ) result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], TESTDATA, @@ -402,32 +376,18 @@ async def test_form_stream_timeout(hass, fakeimg_png, user_flow): @respx.mock -async def test_form_stream_unauthorised(hass, fakeimg_png, user_flow): - """Test we handle invalid auth.""" +async def test_form_stream_worker_error(hass, fakeimg_png, user_flow): + """Test we handle a StreamWorkerError and pass the message through.""" with patch( - "homeassistant.components.generic.config_flow.av.open", - side_effect=av.error.HTTPUnauthorizedError(0, 0), + "homeassistant.components.generic.config_flow.create_stream", + side_effect=StreamWorkerError("Some message"), ): result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], TESTDATA, ) assert result2["type"] == "form" - assert result2["errors"] == {"stream_source": "stream_unauthorised"} - - -@respx.mock -async def test_form_stream_novideo(hass, fakeimg_png, user_flow): - """Test we handle invalid stream.""" - with patch( - "homeassistant.components.generic.config_flow.av.open", side_effect=KeyError() - ): - result2 = await hass.config_entries.flow.async_configure( - user_flow["flow_id"], - TESTDATA, - ) - assert result2["type"] == "form" - assert result2["errors"] == {"stream_source": "stream_no_video"} + assert result2["errors"] == {"stream_source": "Some message"} @respx.mock @@ -435,7 +395,7 @@ async def test_form_stream_permission_error(hass, fakeimgbytes_png, user_flow): """Test we handle permission error.""" respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) with patch( - "homeassistant.components.generic.config_flow.av.open", + "homeassistant.components.generic.config_flow.create_stream", side_effect=PermissionError(), ): result2 = await hass.config_entries.flow.async_configure( @@ -450,7 +410,7 @@ async def test_form_stream_permission_error(hass, fakeimgbytes_png, user_flow): async def test_form_no_route_to_host(hass, fakeimg_png, user_flow): """Test we handle no route to host.""" with patch( - "homeassistant.components.generic.config_flow.av.open", + "homeassistant.components.generic.config_flow.create_stream", side_effect=OSError(errno.EHOSTUNREACH, "No route to host"), ): result2 = await hass.config_entries.flow.async_configure( @@ -465,7 +425,7 @@ async def test_form_no_route_to_host(hass, fakeimg_png, user_flow): async def test_form_stream_io_error(hass, fakeimg_png, user_flow): """Test we handle no io error when setting up stream.""" with patch( - "homeassistant.components.generic.config_flow.av.open", + "homeassistant.components.generic.config_flow.create_stream", side_effect=OSError(errno.EIO, "Input/output error"), ): result2 = await hass.config_entries.flow.async_configure( @@ -480,7 +440,7 @@ async def test_form_stream_io_error(hass, fakeimg_png, user_flow): async def test_form_oserror(hass, fakeimg_png, user_flow): """Test we handle OS error when setting up stream.""" with patch( - "homeassistant.components.generic.config_flow.av.open", + "homeassistant.components.generic.config_flow.create_stream", side_effect=OSError("Some other OSError"), ), pytest.raises(OSError): await hass.config_entries.flow.async_configure( @@ -490,7 +450,7 @@ async def test_form_oserror(hass, fakeimg_png, user_flow): @respx.mock -async def test_options_template_error(hass, fakeimgbytes_png, mock_av_open): +async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream): """Test the options flow with a template error.""" respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) respx.get("http://127.0.0.1/testurl/2").respond(stream=fakeimgbytes_png) @@ -503,18 +463,18 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_av_open): options=TESTDATA, ) - with mock_av_open: - mock_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() - result = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" + result = await hass.config_entries.options.async_init(mock_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" - # try updating the still image url - data = TESTDATA.copy() - data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/2" + # try updating the still image url + data = TESTDATA.copy() + data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/2" + with mock_create_stream: result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input=data, @@ -541,12 +501,12 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_av_open): result4["flow_id"], user_input=data, ) - assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM - assert result5["errors"] == {"stream_source": "template_error"} + assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result5["errors"] == {"stream_source": "template_error"} @respx.mock -async def test_options_only_stream(hass, fakeimgbytes_png, mock_av_open): +async def test_options_only_stream(hass, fakeimgbytes_png, mock_create_stream): """Test the options flow without a still_image_url.""" respx.get("http://127.0.0.1/testurl/2").respond(stream=fakeimgbytes_png) data = TESTDATA.copy() @@ -558,36 +518,35 @@ async def test_options_only_stream(hass, fakeimgbytes_png, mock_av_open): data={}, options=data, ) - with mock_av_open: - mock_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() - result = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" + result = await hass.config_entries.options.async_init(mock_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" - # try updating the config options + # try updating the config options + with mock_create_stream: result3 = await hass.config_entries.options.async_configure( result["flow_id"], user_input=data, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result3["data"][CONF_CONTENT_TYPE] == "image/jpeg" + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["data"][CONF_CONTENT_TYPE] == "image/jpeg" # These below can be deleted after deprecation period is finished. @respx.mock -async def test_import(hass, fakeimg_png, mock_av_open): +async def test_import(hass, fakeimg_png): """Test configuration.yaml import used during migration.""" - with mock_av_open: - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML - ) - # duplicate import should be aborted - result2 = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML + ) + # duplicate import should be aborted + result2 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML + ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "Yaml Defined Name" await hass.async_block_till_done() @@ -599,7 +558,7 @@ async def test_import(hass, fakeimg_png, mock_av_open): # These above can be deleted after deprecation period is finished. -async def test_unload_entry(hass, fakeimg_png, mock_av_open): +async def test_unload_entry(hass, fakeimg_png): """Test unloading the generic IP Camera entry.""" mock_entry = MockConfigEntry(domain=DOMAIN, options=TESTDATA) mock_entry.add_to_hass(hass) @@ -669,7 +628,9 @@ async def test_migrate_existing_ids(hass) -> None: @respx.mock -async def test_use_wallclock_as_timestamps_option(hass, fakeimg_png, mock_av_open): +async def test_use_wallclock_as_timestamps_option( + hass, fakeimg_png, mock_create_stream +): """Test the use_wallclock_as_timestamps option flow.""" mock_entry = MockConfigEntry( @@ -679,19 +640,18 @@ async def test_use_wallclock_as_timestamps_option(hass, fakeimg_png, mock_av_ope options=TESTDATA, ) - with mock_av_open: - mock_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init( - mock_entry.entry_id, context={"show_advanced_options": True} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init( + mock_entry.entry_id, context={"show_advanced_options": True} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + with mock_create_stream: result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY From 63b51f566da6a695c234a84f93763a9cac7f90ba Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 11 Jun 2022 02:13:50 -0400 Subject: [PATCH 1425/3516] Fix zwave_js add node schemas (#73343) * Fix zwave_js add node schemas * Code cleanup * Add test --- homeassistant/components/zwave_js/api.py | 32 +++++++++-- tests/components/zwave_js/test_api.py | 70 ++++++++++++++++++++++-- 2 files changed, 90 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 6c186e2f840..6a4aaf0264e 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -14,6 +14,7 @@ from zwave_js_server.const import ( InclusionStrategy, LogLevel, Protocols, + ProvisioningEntryStatus, QRCodeVersion, SecurityClass, ZwaveFeature, @@ -148,6 +149,8 @@ MAX_INCLUSION_REQUEST_INTERVAL = "max_inclusion_request_interval" UUID = "uuid" SUPPORTED_PROTOCOLS = "supported_protocols" ADDITIONAL_PROPERTIES = "additional_properties" +STATUS = "status" +REQUESTED_SECURITY_CLASSES = "requested_security_classes" FEATURE = "feature" UNPROVISION = "unprovision" @@ -160,19 +163,22 @@ def convert_planned_provisioning_entry(info: dict) -> ProvisioningEntry: """Handle provisioning entry dict to ProvisioningEntry.""" return ProvisioningEntry( dsk=info[DSK], - security_classes=[SecurityClass(sec_cls) for sec_cls in info[SECURITY_CLASSES]], + security_classes=info[SECURITY_CLASSES], + status=info[STATUS], + requested_security_classes=info.get(REQUESTED_SECURITY_CLASSES), additional_properties={ - k: v for k, v in info.items() if k not in (DSK, SECURITY_CLASSES) + k: v + for k, v in info.items() + if k not in (DSK, SECURITY_CLASSES, STATUS, REQUESTED_SECURITY_CLASSES) }, ) def convert_qr_provisioning_information(info: dict) -> QRProvisioningInformation: """Convert QR provisioning information dict to QRProvisioningInformation.""" - protocols = [Protocols(proto) for proto in info.get(SUPPORTED_PROTOCOLS, [])] return QRProvisioningInformation( - version=QRCodeVersion(info[VERSION]), - security_classes=[SecurityClass(sec_cls) for sec_cls in info[SECURITY_CLASSES]], + version=info[VERSION], + security_classes=info[SECURITY_CLASSES], dsk=info[DSK], generic_device_class=info[GENERIC_DEVICE_CLASS], specific_device_class=info[SPECIFIC_DEVICE_CLASS], @@ -183,7 +189,9 @@ def convert_qr_provisioning_information(info: dict) -> QRProvisioningInformation application_version=info[APPLICATION_VERSION], max_inclusion_request_interval=info.get(MAX_INCLUSION_REQUEST_INTERVAL), uuid=info.get(UUID), - supported_protocols=protocols if protocols else None, + supported_protocols=info.get(SUPPORTED_PROTOCOLS), + status=info[STATUS], + requested_security_classes=info.get(REQUESTED_SECURITY_CLASSES), additional_properties=info.get(ADDITIONAL_PROPERTIES, {}), ) @@ -197,6 +205,12 @@ PLANNED_PROVISIONING_ENTRY_SCHEMA = vol.All( cv.ensure_list, [vol.Coerce(SecurityClass)], ), + vol.Optional(STATUS, default=ProvisioningEntryStatus.ACTIVE): vol.Coerce( + ProvisioningEntryStatus + ), + vol.Optional(REQUESTED_SECURITY_CLASSES): vol.All( + cv.ensure_list, [vol.Coerce(SecurityClass)] + ), }, # Provisioning entries can have extra keys for SmartStart extra=vol.ALLOW_EXTRA, @@ -226,6 +240,12 @@ QR_PROVISIONING_INFORMATION_SCHEMA = vol.All( cv.ensure_list, [vol.Coerce(Protocols)], ), + vol.Optional(STATUS, default=ProvisioningEntryStatus.ACTIVE): vol.Coerce( + ProvisioningEntryStatus + ), + vol.Optional(REQUESTED_SECURITY_CLASSES): vol.All( + cv.ensure_list, [vol.Coerce(SecurityClass)] + ), vol.Optional(ADDITIONAL_PROPERTIES): dict, } ), diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index da8cfad9624..6c3dd796a7d 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -10,6 +10,7 @@ from zwave_js_server.const import ( InclusionStrategy, LogLevel, Protocols, + ProvisioningEntryStatus, QRCodeVersion, SecurityClass, ZwaveFeature, @@ -63,8 +64,10 @@ from homeassistant.components.zwave_js.api import ( PROPERTY_KEY, QR_CODE_STRING, QR_PROVISIONING_INFORMATION, + REQUESTED_SECURITY_CLASSES, SECURITY_CLASSES, SPECIFIC_DEVICE_CLASS, + STATUS, TYPE, UNPROVISION, VALUE, @@ -619,13 +622,68 @@ async def test_add_node( client.async_send_command.reset_mock() client.async_send_command.return_value = {"success": True} - # Test S2 QR code string + # Test S2 QR provisioning information await ws_client.send_json( { ID: 4, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id, INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value, + QR_PROVISIONING_INFORMATION: { + VERSION: 0, + SECURITY_CLASSES: [0], + DSK: "test", + GENERIC_DEVICE_CLASS: 1, + SPECIFIC_DEVICE_CLASS: 1, + INSTALLER_ICON_TYPE: 1, + MANUFACTURER_ID: 1, + PRODUCT_TYPE: 1, + PRODUCT_ID: 1, + APPLICATION_VERSION: "test", + STATUS: 1, + REQUESTED_SECURITY_CLASSES: [0], + }, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "controller.begin_inclusion", + "options": { + "strategy": InclusionStrategy.SECURITY_S2, + "provisioning": QRProvisioningInformation( + version=QRCodeVersion.S2, + security_classes=[SecurityClass.S2_UNAUTHENTICATED], + dsk="test", + generic_device_class=1, + specific_device_class=1, + installer_icon_type=1, + manufacturer_id=1, + product_type=1, + product_id=1, + application_version="test", + max_inclusion_request_interval=None, + uuid=None, + supported_protocols=None, + status=ProvisioningEntryStatus.INACTIVE, + requested_security_classes=[SecurityClass.S2_UNAUTHENTICATED], + ).to_dict(), + }, + } + + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"success": True} + + # Test S2 QR code string + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/add_node", + ENTRY_ID: entry.entry_id, + INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value, QR_CODE_STRING: "90testtesttesttesttesttesttesttesttesttesttesttesttest", } ) @@ -648,7 +706,7 @@ async def test_add_node( # Test Smart Start QR provisioning information with S2 inclusion strategy fails await ws_client.send_json( { - ID: 5, + ID: 6, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id, INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value, @@ -678,7 +736,7 @@ async def test_add_node( # Test QR provisioning information with S0 inclusion strategy fails await ws_client.send_json( { - ID: 5, + ID: 7, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id, INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S0, @@ -708,7 +766,7 @@ async def test_add_node( # Test ValueError is caught as failure await ws_client.send_json( { - ID: 6, + ID: 8, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id, INCLUSION_STRATEGY: InclusionStrategy.DEFAULT.value, @@ -728,7 +786,7 @@ async def test_add_node( ): await ws_client.send_json( { - ID: 7, + ID: 9, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id, } @@ -744,7 +802,7 @@ async def test_add_node( await hass.async_block_till_done() await ws_client.send_json( - {ID: 8, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id} + {ID: 10, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id} ) msg = await ws_client.receive_json() From dc48791864e62654b6441eeb3bbb5b9dfbd73683 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 11 Jun 2022 02:16:46 -0400 Subject: [PATCH 1426/3516] Add config flow to eight_sleep (#71095) * Add config flow to eight_sleep * simplify tests * Remove extra file * remove unused import * fix redundant code * Update homeassistant/components/eight_sleep/__init__.py Co-authored-by: J. Nick Koston * incorporate feedback * Review comments * remove typing from tests * Fix based on changes * Fix requirements * Remove stale comment * Fix tests * Reverse the flow and force the config entry to reconnect * Review comments * Abort if import flow fails * Split import and user logic * Fix error Co-authored-by: J. Nick Koston --- .coveragerc | 4 +- CODEOWNERS | 1 + .../components/eight_sleep/__init__.py | 181 +++++++++++------- .../components/eight_sleep/binary_sensor.py | 38 ++-- .../components/eight_sleep/config_flow.py | 90 +++++++++ homeassistant/components/eight_sleep/const.py | 7 +- .../components/eight_sleep/manifest.json | 3 +- .../components/eight_sleep/sensor.py | 73 ++++--- .../components/eight_sleep/services.yaml | 12 +- .../components/eight_sleep/strings.json | 19 ++ .../eight_sleep/translations/en.json | 18 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/eight_sleep/__init__.py | 1 + tests/components/eight_sleep/conftest.py | 29 +++ .../eight_sleep/test_config_flow.py | 85 ++++++++ 16 files changed, 432 insertions(+), 133 deletions(-) create mode 100644 homeassistant/components/eight_sleep/config_flow.py create mode 100644 homeassistant/components/eight_sleep/strings.json create mode 100644 homeassistant/components/eight_sleep/translations/en.json create mode 100644 tests/components/eight_sleep/__init__.py create mode 100644 tests/components/eight_sleep/conftest.py create mode 100644 tests/components/eight_sleep/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index ef1a2adb712..8d2cdd32336 100644 --- a/.coveragerc +++ b/.coveragerc @@ -262,7 +262,9 @@ omit = homeassistant/components/eddystone_temperature/sensor.py homeassistant/components/edimax/switch.py homeassistant/components/egardia/* - homeassistant/components/eight_sleep/* + homeassistant/components/eight_sleep/__init__.py + homeassistant/components/eight_sleep/binary_sensor.py + homeassistant/components/eight_sleep/sensor.py homeassistant/components/eliqonline/sensor.py homeassistant/components/elkm1/__init__.py homeassistant/components/elkm1/alarm_control_panel.py diff --git a/CODEOWNERS b/CODEOWNERS index 9d0ba851339..9a57f3e791a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -273,6 +273,7 @@ build.json @home-assistant/supervisor /tests/components/efergy/ @tkdrob /homeassistant/components/egardia/ @jeroenterheerdt /homeassistant/components/eight_sleep/ @mezz64 @raman325 +/tests/components/eight_sleep/ @mezz64 @raman325 /homeassistant/components/elgato/ @frenck /tests/components/elgato/ @frenck /homeassistant/components/elkm1/ @gwww @bdraco diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 1bf22defd74..5cd7bec9244 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -1,34 +1,38 @@ """Support for Eight smart mattress covers and mattresses.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta import logging from pyeight.eight import EightSleep +from pyeight.exceptions import RequestError from pyeight.user import EightUser import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID, CONF_PASSWORD, CONF_USERNAME, Platform -from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers import discovery +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ( + ATTR_HW_VERSION, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_SW_VERSION, + CONF_PASSWORD, + CONF_USERNAME, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError 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.device_registry import async_get +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.typing import UNDEFINED, ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, ) -from .const import ( - ATTR_HEAT_DURATION, - ATTR_TARGET_HEAT, - DATA_API, - DATA_HEAT, - DATA_USER, - DOMAIN, - NAME_MAP, - SERVICE_HEAT_SET, -) +from .const import DOMAIN, NAME_MAP _LOGGER = logging.getLogger(__name__) @@ -37,17 +41,6 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] HEAT_SCAN_INTERVAL = timedelta(seconds=60) USER_SCAN_INTERVAL = timedelta(seconds=300) -VALID_TARGET_HEAT = vol.All(vol.Coerce(int), vol.Clamp(min=-100, max=100)) -VALID_DURATION = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=28800)) - -SERVICE_EIGHT_SCHEMA = vol.Schema( - { - ATTR_ENTITY_ID: cv.entity_ids, - ATTR_TARGET_HEAT: VALID_TARGET_HEAT, - ATTR_HEAT_DURATION: VALID_DURATION, - } -) - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -61,6 +54,15 @@ CONFIG_SCHEMA = vol.Schema( ) +@dataclass +class EightSleepConfigEntryData: + """Data used for all entities for a given config entry.""" + + api: EightSleep + heat_coordinator: DataUpdateCoordinator + user_coordinator: DataUpdateCoordinator + + def _get_device_unique_id(eight: EightSleep, user_obj: EightUser | None = None) -> str: """Get the device's unique ID.""" unique_id = eight.device_id @@ -71,23 +73,36 @@ def _get_device_unique_id(eight: EightSleep, user_obj: EightUser | None = None) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Eight Sleep component.""" + """Old set up method for the Eight Sleep component.""" + if DOMAIN in config: + _LOGGER.warning( + "Your Eight Sleep configuration has been imported into the UI; " + "please remove it from configuration.yaml as support for it " + "will be removed in a future release" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] + ) + ) - if DOMAIN not in config: - return True + return True - conf = config[DOMAIN] - user = conf[CONF_USERNAME] - password = conf[CONF_PASSWORD] +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the Eight Sleep config entry.""" eight = EightSleep( - user, password, hass.config.time_zone, async_get_clientsession(hass) + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + hass.config.time_zone, + async_get_clientsession(hass), ) - hass.data.setdefault(DOMAIN, {}) - # Authenticate, build sensors - success = await eight.start() + try: + success = await eight.start() + except RequestError as err: + raise ConfigEntryNotReady from err if not success: # Authentication failed, cannot continue return False @@ -113,47 +128,60 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # No users, cannot continue return False - hass.data[DOMAIN] = { - DATA_API: eight, - DATA_HEAT: heat_coordinator, - DATA_USER: user_coordinator, + dev_reg = async_get(hass) + assert eight.device_data + device_data = { + ATTR_MANUFACTURER: "Eight Sleep", + ATTR_MODEL: eight.device_data.get("modelString", UNDEFINED), + ATTR_HW_VERSION: eight.device_data.get("sensorInfo", {}).get( + "hwRevision", UNDEFINED + ), + ATTR_SW_VERSION: eight.device_data.get("firmwareVersion", UNDEFINED), } - - for platform in PLATFORMS: - hass.async_create_task( - discovery.async_load_platform(hass, platform, DOMAIN, {}, config) + dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, _get_device_unique_id(eight))}, + name=f"{entry.data[CONF_USERNAME]}'s Eight Sleep", + **device_data, + ) + for user in eight.users.values(): + assert user.user_profile + dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, _get_device_unique_id(eight, user))}, + name=f"{user.user_profile['firstName']}'s Eight Sleep Side", + via_device=(DOMAIN, _get_device_unique_id(eight)), + **device_data, ) - async def async_service_handler(service: ServiceCall) -> None: - """Handle eight sleep service calls.""" - params = service.data.copy() - - sensor = params.pop(ATTR_ENTITY_ID, None) - target = params.pop(ATTR_TARGET_HEAT, None) - duration = params.pop(ATTR_HEAT_DURATION, 0) - - for sens in sensor: - side = sens.split("_")[1] - user_id = eight.fetch_user_id(side) - assert user_id - usr_obj = eight.users[user_id] - await usr_obj.set_heating_level(target, duration) - - await heat_coordinator.async_request_refresh() - - # Register services - hass.services.async_register( - DOMAIN, SERVICE_HEAT_SET, async_service_handler, schema=SERVICE_EIGHT_SCHEMA + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = EightSleepConfigEntryData( + eight, heat_coordinator, user_coordinator ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + # stop the API before unloading everything + config_entry_data: EightSleepConfigEntryData = hass.data[DOMAIN][entry.entry_id] + await config_entry_data.api.stop() + hass.data[DOMAIN].pop(entry.entry_id) + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + + return unload_ok + + class EightSleepBaseEntity(CoordinatorEntity[DataUpdateCoordinator]): """The base Eight Sleep entity class.""" def __init__( self, + entry: ConfigEntry, coordinator: DataUpdateCoordinator, eight: EightSleep, user_id: str | None, @@ -161,6 +189,7 @@ class EightSleepBaseEntity(CoordinatorEntity[DataUpdateCoordinator]): ) -> None: """Initialize the data object.""" super().__init__(coordinator) + self._config_entry = entry self._eight = eight self._user_id = user_id self._sensor = sensor @@ -170,9 +199,25 @@ class EightSleepBaseEntity(CoordinatorEntity[DataUpdateCoordinator]): mapped_name = NAME_MAP.get(sensor, sensor.replace("_", " ").title()) if self._user_obj is not None: - mapped_name = f"{self._user_obj.side.title()} {mapped_name}" + assert self._user_obj.user_profile + name = f"{self._user_obj.user_profile['firstName']}'s {mapped_name}" + self._attr_name = name + else: + self._attr_name = f"Eight Sleep {mapped_name}" + unique_id = f"{_get_device_unique_id(eight, self._user_obj)}.{sensor}" + self._attr_unique_id = unique_id + identifiers = {(DOMAIN, _get_device_unique_id(eight, self._user_obj))} + self._attr_device_info = DeviceInfo(identifiers=identifiers) - self._attr_name = f"Eight {mapped_name}" - self._attr_unique_id = ( - f"{_get_device_unique_id(eight, self._user_obj)}.{sensor}" - ) + async def async_heat_set(self, target: int, duration: int) -> None: + """Handle eight sleep service calls.""" + if self._user_obj is None: + raise HomeAssistantError( + "This entity does not support the heat set service." + ) + + await self._user_obj.set_heating_level(target, duration) + config_entry_data: EightSleepConfigEntryData = self.hass.data[DOMAIN][ + self._config_entry.entry_id + ] + await config_entry_data.heat_coordinator.async_request_refresh() diff --git a/homeassistant/components/eight_sleep/binary_sensor.py b/homeassistant/components/eight_sleep/binary_sensor.py index 94ec423390f..7ad1b882008 100644 --- a/homeassistant/components/eight_sleep/binary_sensor.py +++ b/homeassistant/components/eight_sleep/binary_sensor.py @@ -9,37 +9,30 @@ from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import EightSleepBaseEntity -from .const import DATA_API, DATA_HEAT, DOMAIN +from . import EightSleepBaseEntity, EightSleepConfigEntryData +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +BINARY_SENSORS = ["bed_presence"] -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the eight sleep binary sensor.""" - if discovery_info is None: - return - - eight: EightSleep = hass.data[DOMAIN][DATA_API] - heat_coordinator: DataUpdateCoordinator = hass.data[DOMAIN][DATA_HEAT] - - entities = [] - for user in eight.users.values(): - entities.append( - EightHeatSensor(heat_coordinator, eight, user.user_id, "bed_presence") - ) - - async_add_entities(entities) + config_entry_data: EightSleepConfigEntryData = hass.data[DOMAIN][entry.entry_id] + eight = config_entry_data.api + heat_coordinator = config_entry_data.heat_coordinator + async_add_entities( + EightHeatSensor(entry, heat_coordinator, eight, user.user_id, binary_sensor) + for user in eight.users.values() + for binary_sensor in BINARY_SENSORS + ) class EightHeatSensor(EightSleepBaseEntity, BinarySensorEntity): @@ -49,13 +42,14 @@ class EightHeatSensor(EightSleepBaseEntity, BinarySensorEntity): def __init__( self, + entry: ConfigEntry, coordinator: DataUpdateCoordinator, eight: EightSleep, user_id: str | None, sensor: str, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, eight, user_id, sensor) + super().__init__(entry, coordinator, eight, user_id, sensor) assert self._user_obj _LOGGER.debug( "Presence Sensor: %s, Side: %s, User: %s", diff --git a/homeassistant/components/eight_sleep/config_flow.py b/homeassistant/components/eight_sleep/config_flow.py new file mode 100644 index 00000000000..504fbeb2817 --- /dev/null +++ b/homeassistant/components/eight_sleep/config_flow.py @@ -0,0 +1,90 @@ +"""Config flow for Eight Sleep integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from pyeight.eight import EightSleep +from pyeight.exceptions import RequestError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.selector import ( + TextSelector, + TextSelectorConfig, + TextSelectorType, +) + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): TextSelector( + TextSelectorConfig(type=TextSelectorType.EMAIL) + ), + vol.Required(CONF_PASSWORD): TextSelector( + TextSelectorConfig(type=TextSelectorType.PASSWORD) + ), + } +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Eight Sleep.""" + + VERSION = 1 + + async def _validate_data(self, config: dict[str, str]) -> str | None: + """Validate input data and return any error.""" + await self.async_set_unique_id(config[CONF_USERNAME].lower()) + self._abort_if_unique_id_configured() + + eight = EightSleep( + config[CONF_USERNAME], + config[CONF_PASSWORD], + self.hass.config.time_zone, + client_session=async_get_clientsession(self.hass), + ) + + try: + await eight.fetch_token() + except RequestError as err: + return str(err) + + return 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=STEP_USER_DATA_SCHEMA + ) + + if (err := await self._validate_data(user_input)) is not None: + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_DATA_SCHEMA, + errors={"base": "cannot_connect"}, + description_placeholders={"error": err}, + ) + + return self.async_create_entry(title=user_input[CONF_USERNAME], data=user_input) + + async def async_step_import(self, import_config: dict) -> FlowResult: + """Handle import.""" + if (err := await self._validate_data(import_config)) is not None: + _LOGGER.error("Unable to import configuration.yaml configuration: %s", err) + return self.async_abort( + reason="cannot_connect", description_placeholders={"error": err} + ) + + return self.async_create_entry( + title=import_config[CONF_USERNAME], data=import_config + ) diff --git a/homeassistant/components/eight_sleep/const.py b/homeassistant/components/eight_sleep/const.py index 42a9eea590e..23689066665 100644 --- a/homeassistant/components/eight_sleep/const.py +++ b/homeassistant/components/eight_sleep/const.py @@ -1,7 +1,4 @@ """Eight Sleep constants.""" -DATA_HEAT = "heat" -DATA_USER = "user" -DATA_API = "api" DOMAIN = "eight_sleep" HEAT_ENTITY = "heat" @@ -15,5 +12,5 @@ NAME_MAP = { SERVICE_HEAT_SET = "heat_set" -ATTR_TARGET_HEAT = "target" -ATTR_HEAT_DURATION = "duration" +ATTR_TARGET = "target" +ATTR_DURATION = "duration" diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index e83b2977b77..c1833b222df 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -5,5 +5,6 @@ "requirements": ["pyeight==0.3.0"], "codeowners": ["@mezz64", "@raman325"], "iot_class": "cloud_polling", - "loggers": ["pyeight"] + "loggers": ["pyeight"], + "config_flow": true } diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index b2afa496149..b184cd2496f 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -5,16 +5,17 @@ import logging from typing import Any from pyeight.eight import EightSleep +import voluptuous as vol from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers import entity_platform as ep from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import EightSleepBaseEntity -from .const import DATA_API, DATA_HEAT, DATA_USER, DOMAIN +from . import EightSleepBaseEntity, EightSleepConfigEntryData +from .const import ATTR_DURATION, ATTR_TARGET, DOMAIN, SERVICE_HEAT_SET ATTR_ROOM_TEMP = "Room Temperature" ATTR_AVG_ROOM_TEMP = "Average Room Temperature" @@ -53,37 +54,50 @@ EIGHT_USER_SENSORS = [ EIGHT_HEAT_SENSORS = ["bed_state"] EIGHT_ROOM_SENSORS = ["room_temperature"] +VALID_TARGET_HEAT = vol.All(vol.Coerce(int), vol.Clamp(min=-100, max=100)) +VALID_DURATION = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=28800)) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, +SERVICE_EIGHT_SCHEMA = { + ATTR_TARGET: VALID_TARGET_HEAT, + ATTR_DURATION: VALID_DURATION, +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: ep.AddEntitiesCallback ) -> None: """Set up the eight sleep sensors.""" - if discovery_info is None: - return - - eight: EightSleep = hass.data[DOMAIN][DATA_API] - heat_coordinator: DataUpdateCoordinator = hass.data[DOMAIN][DATA_HEAT] - user_coordinator: DataUpdateCoordinator = hass.data[DOMAIN][DATA_USER] + config_entry_data: EightSleepConfigEntryData = hass.data[DOMAIN][entry.entry_id] + eight = config_entry_data.api + heat_coordinator = config_entry_data.heat_coordinator + user_coordinator = config_entry_data.user_coordinator all_sensors: list[SensorEntity] = [] for obj in eight.users.values(): - for sensor in EIGHT_USER_SENSORS: - all_sensors.append( - EightUserSensor(user_coordinator, eight, obj.user_id, sensor) - ) - for sensor in EIGHT_HEAT_SENSORS: - all_sensors.append( - EightHeatSensor(heat_coordinator, eight, obj.user_id, sensor) - ) - for sensor in EIGHT_ROOM_SENSORS: - all_sensors.append(EightRoomSensor(user_coordinator, eight, sensor)) + all_sensors.extend( + EightUserSensor(entry, user_coordinator, eight, obj.user_id, sensor) + for sensor in EIGHT_USER_SENSORS + ) + all_sensors.extend( + EightHeatSensor(entry, heat_coordinator, eight, obj.user_id, sensor) + for sensor in EIGHT_HEAT_SENSORS + ) + + all_sensors.extend( + EightRoomSensor(entry, user_coordinator, eight, sensor) + for sensor in EIGHT_ROOM_SENSORS + ) async_add_entities(all_sensors) + platform = ep.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_HEAT_SET, + SERVICE_EIGHT_SCHEMA, + "async_heat_set", + ) + class EightHeatSensor(EightSleepBaseEntity, SensorEntity): """Representation of an eight sleep heat-based sensor.""" @@ -92,13 +106,14 @@ class EightHeatSensor(EightSleepBaseEntity, SensorEntity): def __init__( self, + entry: ConfigEntry, coordinator: DataUpdateCoordinator, eight: EightSleep, user_id: str, sensor: str, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, eight, user_id, sensor) + super().__init__(entry, coordinator, eight, user_id, sensor) assert self._user_obj _LOGGER.debug( @@ -147,13 +162,14 @@ class EightUserSensor(EightSleepBaseEntity, SensorEntity): def __init__( self, + entry: ConfigEntry, coordinator: DataUpdateCoordinator, eight: EightSleep, user_id: str, sensor: str, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, eight, user_id, sensor) + super().__init__(entry, coordinator, eight, user_id, sensor) assert self._user_obj if self._sensor == "bed_temperature": @@ -260,12 +276,13 @@ class EightRoomSensor(EightSleepBaseEntity, SensorEntity): def __init__( self, + entry, coordinator: DataUpdateCoordinator, eight: EightSleep, sensor: str, ) -> None: """Initialize the sensor.""" - super().__init__(coordinator, eight, None, sensor) + super().__init__(entry, coordinator, eight, None, sensor) @property def native_value(self) -> int | float | None: diff --git a/homeassistant/components/eight_sleep/services.yaml b/homeassistant/components/eight_sleep/services.yaml index de864afc160..39b960a6f7c 100644 --- a/homeassistant/components/eight_sleep/services.yaml +++ b/homeassistant/components/eight_sleep/services.yaml @@ -1,6 +1,10 @@ heat_set: name: Heat set description: Set heating/cooling level for eight sleep. + target: + entity: + integration: eight_sleep + domain: sensor fields: duration: name: Duration @@ -11,14 +15,6 @@ heat_set: min: 0 max: 28800 unit_of_measurement: seconds - entity_id: - name: Entity - description: Entity id of the bed state to adjust. - required: true - selector: - entity: - integration: eight_sleep - domain: sensor target: name: Target description: Target cooling/heating level from -100 to 100. diff --git a/homeassistant/components/eight_sleep/strings.json b/homeassistant/components/eight_sleep/strings.json new file mode 100644 index 00000000000..21accc53a06 --- /dev/null +++ b/homeassistant/components/eight_sleep/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "Cannot connect to Eight Sleep cloud: {error}" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "Cannot connect to Eight Sleep cloud: {error}" + } + } +} diff --git a/homeassistant/components/eight_sleep/translations/en.json b/homeassistant/components/eight_sleep/translations/en.json new file mode 100644 index 00000000000..dfd604a6c08 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect: {error}" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 1cf8ba743bc..a50fc85a9f0 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -87,6 +87,7 @@ FLOWS = { "ecobee", "econet", "efergy", + "eight_sleep", "elgato", "elkm1", "elmax", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e247bf42d82..08185c36295 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -979,6 +979,9 @@ pyeconet==0.1.15 # homeassistant.components.efergy pyefergy==22.1.1 +# homeassistant.components.eight_sleep +pyeight==0.3.0 + # homeassistant.components.everlights pyeverlights==0.1.0 diff --git a/tests/components/eight_sleep/__init__.py b/tests/components/eight_sleep/__init__.py new file mode 100644 index 00000000000..22348f774be --- /dev/null +++ b/tests/components/eight_sleep/__init__.py @@ -0,0 +1 @@ +"""Tests for the Eight Sleep integration.""" diff --git a/tests/components/eight_sleep/conftest.py b/tests/components/eight_sleep/conftest.py new file mode 100644 index 00000000000..753fe1e30d5 --- /dev/null +++ b/tests/components/eight_sleep/conftest.py @@ -0,0 +1,29 @@ +"""Fixtures for Eight Sleep.""" +from unittest.mock import patch + +from pyeight.exceptions import RequestError +import pytest + + +@pytest.fixture(name="bypass", autouse=True) +def bypass_fixture(): + """Bypasses things that slow te tests down or block them from testing the behavior.""" + with patch( + "homeassistant.components.eight_sleep.config_flow.EightSleep.fetch_token", + ), patch( + "homeassistant.components.eight_sleep.config_flow.EightSleep.at_exit", + ), patch( + "homeassistant.components.eight_sleep.async_setup_entry", + return_value=True, + ): + yield + + +@pytest.fixture(name="token_error") +def token_error_fixture(): + """Simulate error when fetching token.""" + with patch( + "homeassistant.components.eight_sleep.config_flow.EightSleep.fetch_token", + side_effect=RequestError, + ): + yield diff --git a/tests/components/eight_sleep/test_config_flow.py b/tests/components/eight_sleep/test_config_flow.py new file mode 100644 index 00000000000..8015fb6c69d --- /dev/null +++ b/tests/components/eight_sleep/test_config_flow.py @@ -0,0 +1,85 @@ +"""Test the Eight Sleep config flow.""" +from homeassistant import config_entries +from homeassistant.components.eight_sleep.const import DOMAIN +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + + +async def test_form(hass) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "test-username" + assert result2["data"] == { + "username": "test-username", + "password": "test-password", + } + + +async def test_form_invalid_auth(hass, token_error) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "bad-username", + "password": "bad-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_import(hass) -> None: + """Test import works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "username": "test-username", + "password": "test-password", + }, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "test-username" + assert result["data"] == { + "username": "test-username", + "password": "test-password", + } + + +async def test_import_invalid_auth(hass, token_error) -> None: + """Test we handle invalid auth on import.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "username": "bad-username", + "password": "bad-password", + }, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" From b4e9a9b1ed33e790ebec1a42285fb83b36a12311 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 11 Jun 2022 09:38:13 -0400 Subject: [PATCH 1427/3516] Bump zwave-js-server-python to 0.37.2 (#73345) --- 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 1c7eabb4e86..40b74096f0c 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.37.1"], + "requirements": ["zwave-js-server-python==0.37.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 b8d114cfe8d..994c94a1859 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2525,7 +2525,7 @@ zigpy==0.46.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.37.1 +zwave-js-server-python==0.37.2 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 08185c36295..619db24f390 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1671,7 +1671,7 @@ zigpy-znp==0.7.0 zigpy==0.46.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.37.1 +zwave-js-server-python==0.37.2 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 From cb1011156d505d0d5d4371ec662060b011d2322b Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 11 Jun 2022 13:39:43 -0500 Subject: [PATCH 1428/3516] Rely on core config entry error logging in Plex setup (#73368) Rely on core config entry error logging --- homeassistant/components/plex/__init__.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index dbfe55077d7..c8745213f90 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -14,7 +14,7 @@ from plexwebsocket import ( import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN, BrowseError -from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_URL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady @@ -139,12 +139,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, data={**entry.data, PLEX_SERVER_CONFIG: new_server_data} ) except requests.exceptions.ConnectionError as error: - if entry.state is not ConfigEntryState.SETUP_RETRY: - _LOGGER.error( - "Plex server (%s) could not be reached: [%s]", - server_config[CONF_URL], - error, - ) raise ConfigEntryNotReady from error except plexapi.exceptions.Unauthorized as ex: raise ConfigEntryAuthFailed( From 8c968451354fc089c3eb76104f2c4230cae1433b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Jun 2022 09:31:25 -1000 Subject: [PATCH 1429/3516] Add missing exception handlers to radiotherm (#73349) --- homeassistant/components/radiotherm/__init__.py | 4 ++++ homeassistant/components/radiotherm/config_flow.py | 3 ++- homeassistant/components/radiotherm/coordinator.py | 4 ++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/radiotherm/__init__.py b/homeassistant/components/radiotherm/__init__.py index 091d2bb8005..865e75257ec 100644 --- a/homeassistant/components/radiotherm/__init__.py +++ b/homeassistant/components/radiotherm/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Coroutine from socket import timeout from typing import Any, TypeVar +from urllib.error import URLError from radiotherm.validate import RadiothermTstatError @@ -31,6 +32,9 @@ async def _async_call_or_raise_not_ready( except RadiothermTstatError as ex: msg = f"{host} was busy (invalid value returned): {ex}" raise ConfigEntryNotReady(msg) from ex + except (OSError, URLError) as ex: + msg = f"{host} connection error: {ex}" + raise ConfigEntryNotReady(msg) from ex except timeout as ex: msg = f"{host} timed out waiting for a response: {ex}" raise ConfigEntryNotReady(msg) from ex diff --git a/homeassistant/components/radiotherm/config_flow.py b/homeassistant/components/radiotherm/config_flow.py index 030a3e6c022..97ae2c1be0a 100644 --- a/homeassistant/components/radiotherm/config_flow.py +++ b/homeassistant/components/radiotherm/config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging from socket import timeout from typing import Any +from urllib.error import URLError from radiotherm.validate import RadiothermTstatError import voluptuous as vol @@ -29,7 +30,7 @@ async def validate_connection(hass: HomeAssistant, host: str) -> RadioThermInitD """Validate the connection.""" try: return await async_get_init_data(hass, host) - except (timeout, RadiothermTstatError) as ex: + except (timeout, RadiothermTstatError, URLError, OSError) as ex: raise CannotConnect(f"Failed to connect to {host}: {ex}") from ex diff --git a/homeassistant/components/radiotherm/coordinator.py b/homeassistant/components/radiotherm/coordinator.py index 4afa2c0662b..91acdee8710 100644 --- a/homeassistant/components/radiotherm/coordinator.py +++ b/homeassistant/components/radiotherm/coordinator.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging from socket import timeout +from urllib.error import URLError from radiotherm.validate import RadiothermTstatError @@ -38,6 +39,9 @@ class RadioThermUpdateCoordinator(DataUpdateCoordinator[RadioThermUpdate]): except RadiothermTstatError as ex: msg = f"{self._description} was busy (invalid value returned): {ex}" raise UpdateFailed(msg) from ex + except (OSError, URLError) as ex: + msg = f"{self._description} connection error: {ex}" + raise UpdateFailed(msg) from ex except timeout as ex: msg = f"{self._description}) timed out waiting for a response: {ex}" raise UpdateFailed(msg) from ex From 4feb5977eff8f88fdad4711f994f681f942818c2 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 11 Jun 2022 15:34:32 -0400 Subject: [PATCH 1430/3516] Bump aioskybell to 22.6.1 (#73364) --- homeassistant/components/skybell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index 23b29a49247..6884fe07df2 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -3,7 +3,7 @@ "name": "SkyBell", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/skybell", - "requirements": ["aioskybell==22.6.0"], + "requirements": ["aioskybell==22.6.1"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", "loggers": ["aioskybell"] diff --git a/requirements_all.txt b/requirements_all.txt index 994c94a1859..7f4f5dc95f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -241,7 +241,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.skybell -aioskybell==22.6.0 +aioskybell==22.6.1 # homeassistant.components.slimproto aioslimproto==2.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 619db24f390..9f7ac440042 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -210,7 +210,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.skybell -aioskybell==22.6.0 +aioskybell==22.6.1 # homeassistant.components.slimproto aioslimproto==2.0.1 From dd923b2eed0c465559f515269fbef9ab1c87e240 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 11 Jun 2022 21:35:15 +0200 Subject: [PATCH 1431/3516] Minor fix scrape (#73369) --- homeassistant/components/scrape/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 4fc08cba571..1c447439820 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -114,7 +114,7 @@ async def async_setup_entry( if value_template is not None: val_template = Template(value_template, hass) - rest = hass.data.setdefault(DOMAIN, {})[entry.entry_id] + rest = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ From d70ec354683a01dcc6f475539d940dba97f1f91e Mon Sep 17 00:00:00 2001 From: Khole Date: Sat, 11 Jun 2022 20:43:57 +0100 Subject: [PATCH 1432/3516] Hive Bump pyhiveapi to 0.5.10 for credentials fix (#73365) --- homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 341273638bf..45c2b468f23 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -6,7 +6,7 @@ "models": ["HHKBridge*"] }, "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": ["pyhiveapi==0.5.9"], + "requirements": ["pyhiveapi==0.5.10"], "codeowners": ["@Rendili", "@KJonline"], "iot_class": "cloud_polling", "loggers": ["apyhiveapi"] diff --git a/requirements_all.txt b/requirements_all.txt index 7f4f5dc95f6..47ae411433a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1541,7 +1541,7 @@ pyheos==0.7.2 pyhik==0.3.0 # homeassistant.components.hive -pyhiveapi==0.5.9 +pyhiveapi==0.5.10 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f7ac440042..74342f3c1f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1035,7 +1035,7 @@ pyhaversion==22.4.1 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.5.9 +pyhiveapi==0.5.10 # homeassistant.components.homematic pyhomematic==0.1.77 From 297072c1f647277a2efb18cf528c75618ad63550 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 11 Jun 2022 21:25:07 +0100 Subject: [PATCH 1433/3516] Fix initial data load for System Bridge (#73339) * Update package to 3.1.5 * Fix initial loading of data * Remove additional log and make method --- .../components/system_bridge/__init__.py | 16 +++++++++------- .../components/system_bridge/coordinator.py | 10 ++++++++++ .../components/system_bridge/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index 68a017628b8..1bee974a4c4 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -98,19 +98,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - _LOGGER.debug("Data: %s", coordinator.data) - try: # Wait for initial data async with async_timeout.timeout(30): - while coordinator.data is None or all( - getattr(coordinator.data, module) is None for module in MODULES - ): + while not coordinator.is_ready(): _LOGGER.debug( - "Waiting for initial data from %s (%s): %s", + "Waiting for initial data from %s (%s)", entry.title, entry.data[CONF_HOST], - coordinator.data, ) await asyncio.sleep(1) except asyncio.TimeoutError as exception: @@ -118,6 +113,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"Timed out waiting for {entry.title} ({entry.data[CONF_HOST]})." ) from exception + _LOGGER.debug( + "Initial coordinator data for %s (%s):\n%s", + entry.title, + entry.data[CONF_HOST], + coordinator.data.json(), + ) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py index a7343116cde..6088967aa33 100644 --- a/homeassistant/components/system_bridge/coordinator.py +++ b/homeassistant/components/system_bridge/coordinator.py @@ -75,6 +75,16 @@ class SystemBridgeDataUpdateCoordinator( hass, LOGGER, name=DOMAIN, update_interval=timedelta(seconds=30) ) + def is_ready(self) -> bool: + """Return if the data is ready.""" + if self.data is None: + return False + for module in MODULES: + if getattr(self.data, module) is None: + self.logger.debug("%s - Module %s is None", self.title, module) + return False + return True + async def async_get_data( self, modules: list[str], diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 76449e3f3ac..087613413d8 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -3,7 +3,7 @@ "name": "System Bridge", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/system_bridge", - "requirements": ["systembridgeconnector==3.1.3"], + "requirements": ["systembridgeconnector==3.1.5"], "codeowners": ["@timmo001"], "zeroconf": ["_system-bridge._tcp.local."], "after_dependencies": ["zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 47ae411433a..875ea35ad00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2268,7 +2268,7 @@ swisshydrodata==0.1.0 synology-srm==0.2.0 # homeassistant.components.system_bridge -systembridgeconnector==3.1.3 +systembridgeconnector==3.1.5 # homeassistant.components.tailscale tailscale==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 74342f3c1f5..0caab7ebe70 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1504,7 +1504,7 @@ sunwatcher==0.2.1 surepy==0.7.2 # homeassistant.components.system_bridge -systembridgeconnector==3.1.3 +systembridgeconnector==3.1.5 # homeassistant.components.tailscale tailscale==0.2.0 From 51f88d3dad0361214bbb21353ef4c2d6c9bbf0ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 11 Jun 2022 12:05:19 -1000 Subject: [PATCH 1434/3516] Use get_ffmpeg_manager instead of accessing hass.data directly in ring (#73374) Use get_ffmpeg_manager intead of accessing hass.data directly in ring --- homeassistant/components/ring/camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 168df4d62e1..da2e447869a 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -10,7 +10,6 @@ import requests from homeassistant.components import ffmpeg from homeassistant.components.camera import Camera -from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant, callback @@ -33,6 +32,7 @@ async def async_setup_entry( ) -> None: """Set up a Ring Door Bell and StickUp Camera.""" devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] + ffmpeg_manager = ffmpeg.get_ffmpeg_manager(hass) cams = [] for camera in chain( @@ -41,7 +41,7 @@ async def async_setup_entry( if not camera.has_subscription: continue - cams.append(RingCam(config_entry.entry_id, hass.data[DATA_FFMPEG], camera)) + cams.append(RingCam(config_entry.entry_id, ffmpeg_manager, camera)) async_add_entities(cams) From a1637e4fce89f8078a8cff197d71fbe9c5b592e4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 12 Jun 2022 00:25:33 +0000 Subject: [PATCH 1435/3516] [ci skip] Translation update --- .../binary_sensor/translations/sv.json | 3 +- .../eight_sleep/translations/de.json | 19 ++++++++++++ .../eight_sleep/translations/en.json | 5 +-- .../eight_sleep/translations/es.json | 19 ++++++++++++ .../eight_sleep/translations/fr.json | 19 ++++++++++++ .../eight_sleep/translations/hu.json | 19 ++++++++++++ .../eight_sleep/translations/pt-BR.json | 19 ++++++++++++ .../components/google/translations/es.json | 3 ++ .../radiotherm/translations/es.json | 31 +++++++++++++++++++ .../components/scrape/translations/es.json | 15 +++++++++ .../sensibo/translations/sensor.es.json | 8 +++++ .../components/sensor/translations/sv.json | 5 +-- .../components/skybell/translations/es.json | 21 +++++++++++++ 13 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/eight_sleep/translations/de.json create mode 100644 homeassistant/components/eight_sleep/translations/es.json create mode 100644 homeassistant/components/eight_sleep/translations/fr.json create mode 100644 homeassistant/components/eight_sleep/translations/hu.json create mode 100644 homeassistant/components/eight_sleep/translations/pt-BR.json create mode 100644 homeassistant/components/radiotherm/translations/es.json create mode 100644 homeassistant/components/scrape/translations/es.json create mode 100644 homeassistant/components/sensibo/translations/sensor.es.json create mode 100644 homeassistant/components/skybell/translations/es.json diff --git a/homeassistant/components/binary_sensor/translations/sv.json b/homeassistant/components/binary_sensor/translations/sv.json index c6685403e24..eb23c7f12c6 100644 --- a/homeassistant/components/binary_sensor/translations/sv.json +++ b/homeassistant/components/binary_sensor/translations/sv.json @@ -90,7 +90,8 @@ } }, "device_class": { - "motion": "r\u00f6relse" + "motion": "r\u00f6relse", + "power": "effekt" }, "state": { "_": { diff --git a/homeassistant/components/eight_sleep/translations/de.json b/homeassistant/components/eight_sleep/translations/de.json new file mode 100644 index 00000000000..0d2dbadfd51 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Es kann keine Verbindung zur Eight Sleep Cloud hergestellt werden: {error}" + }, + "error": { + "cannot_connect": "Es kann keine Verbindung zur Eight Sleep Cloud hergestellt werden: {error}" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/translations/en.json b/homeassistant/components/eight_sleep/translations/en.json index dfd604a6c08..29926915fbb 100644 --- a/homeassistant/components/eight_sleep/translations/en.json +++ b/homeassistant/components/eight_sleep/translations/en.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "cannot_connect": "Cannot connect to Eight Sleep cloud: {error}" }, "error": { - "cannot_connect": "Failed to connect: {error}" + "cannot_connect": "Cannot connect to Eight Sleep cloud: {error}" }, "step": { "user": { diff --git a/homeassistant/components/eight_sleep/translations/es.json b/homeassistant/components/eight_sleep/translations/es.json new file mode 100644 index 00000000000..5936b1046b7 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se puede conectar a la nube de Eight Sleep: {error}" + }, + "error": { + "cannot_connect": "No se puede conectar a la nube de Eight Sleep: {error}" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/translations/fr.json b/homeassistant/components/eight_sleep/translations/fr.json new file mode 100644 index 00000000000..ae9902a5d7d --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "Impossible de se connecter au cloud Eight Sleep\u00a0: {error}" + }, + "error": { + "cannot_connect": "Impossible de se connecter au cloud Eight Sleep\u00a0: {error}" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/translations/hu.json b/homeassistant/components/eight_sleep/translations/hu.json new file mode 100644 index 00000000000..61c0ce0f92d --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Nem lehet csatlakozni az Eight Sleep felh\u0151h\u00f6z: {error}" + }, + "error": { + "cannot_connect": "Nem lehet csatlakozni az Eight Sleep felh\u0151h\u00f6z: {error}" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/translations/pt-BR.json b/homeassistant/components/eight_sleep/translations/pt-BR.json new file mode 100644 index 00000000000..d7ddac41bbe --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar \u00e0 nuvem Eight Sleep: {error}" + }, + "error": { + "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar \u00e0 nuvem Eight Sleep: {error}" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Nome de usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/es.json b/homeassistant/components/google/translations/es.json index aed1ec5d9ad..9e777e6b377 100644 --- a/homeassistant/components/google/translations/es.json +++ b/homeassistant/components/google/translations/es.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "Sigue las [instrucciones]({more_info_url}) para la [pantalla de consentimiento de OAuth]({oauth_consent_url}) para dar acceso a Home Assistant a tu Google Calendar. Tambi\u00e9n necesita crear credenciales de aplicaci\u00f3n vinculadas a su calendario:\n1. Vaya a [Credenciales]({oauth_creds_url}) y haga clic en **Crear credenciales**.\n1. En la lista desplegable, seleccione **ID de cliente de OAuth**.\n1. Seleccione **TV y dispositivos de entrada limitada** para el tipo de aplicaci\u00f3n." + }, "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", diff --git a/homeassistant/components/radiotherm/translations/es.json b/homeassistant/components/radiotherm/translations/es.json new file mode 100644 index 00000000000..dbb84376ff9 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/es.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Error al conectar", + "unknown": "Error inesperado" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "\u00bfQuieres configurar {name} {model} ({host})?" + }, + "user": { + "data": { + "host": "Host" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "Establezca una retenci\u00f3n permanente al ajustar la temperatura." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/es.json b/homeassistant/components/scrape/translations/es.json new file mode 100644 index 00000000000..660d687344c --- /dev/null +++ b/homeassistant/components/scrape/translations/es.json @@ -0,0 +1,15 @@ +{ + "options": { + "step": { + "init": { + "data_description": { + "resource": "La URL del sitio web que contiene el valor.", + "select": "Define qu\u00e9 etiqueta buscar. Consulte los selectores de CSS de Beautifulsoup para obtener m\u00e1s informaci\u00f3n.", + "state_class": "El state_class del sensor", + "value_template": "Define una plantilla para obtener el estado del sensor", + "verify_ssl": "Habilita/deshabilita la verificaci\u00f3n del certificado SSL/TLS, por ejemplo, si est\u00e1 autofirmado" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.es.json b/homeassistant/components/sensibo/translations/sensor.es.json new file mode 100644 index 00000000000..1b251c70f7d --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.es.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "Normal", + "s": "Sensible" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/sv.json b/homeassistant/components/sensor/translations/sv.json index 720fac84f59..eeec1090a90 100644 --- a/homeassistant/components/sensor/translations/sv.json +++ b/homeassistant/components/sensor/translations/sv.json @@ -4,7 +4,7 @@ "is_battery_level": "Aktuell {entity_name} batteriniv\u00e5", "is_humidity": "Aktuell {entity_name} fuktighet", "is_illuminance": "Aktuell {entity_name} belysning", - "is_power": "Aktuell {entity_name} str\u00f6m", + "is_power": "Aktuell {entity_name} effekt", "is_pressure": "Aktuellt {entity_name} tryck", "is_signal_strength": "Aktuell {entity_name} signalstyrka", "is_temperature": "Aktuell {entity_name} temperatur", @@ -14,7 +14,8 @@ "battery_level": "{entity_name} batteriniv\u00e5 \u00e4ndras", "humidity": "{entity_name} fuktighet \u00e4ndras", "illuminance": "{entity_name} belysning \u00e4ndras", - "power": "{entity_name} str\u00f6mf\u00f6r\u00e4ndringar", + "power": "{entity_name} effektf\u00f6r\u00e4ndringar", + "power_factor": "effektfaktorf\u00f6r\u00e4ndringar", "pressure": "{entity_name} tryckf\u00f6r\u00e4ndringar", "signal_strength": "{entity_name} signalstyrka \u00e4ndras", "temperature": "{entity_name} temperaturf\u00f6r\u00e4ndringar", diff --git a/homeassistant/components/skybell/translations/es.json b/homeassistant/components/skybell/translations/es.json new file mode 100644 index 00000000000..cc93b536c38 --- /dev/null +++ b/homeassistant/components/skybell/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", + "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + }, + "error": { + "cannot_connect": "Error al conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "email": "Correo electronico", + "password": "Contrase\u00f1a" + } + } + } + } +} \ No newline at end of file From 0bcc5d7a292b8176f0f3dc1be0baa03ddf81919e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Jun 2022 13:46:20 -1000 Subject: [PATCH 1436/3516] Add async_remove_config_entry_device support to lookin (#73381) --- homeassistant/components/lookin/__init__.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/homeassistant/components/lookin/__init__.py b/homeassistant/components/lookin/__init__.py index 555b8b551be..9b0a5b05f1f 100644 --- a/homeassistant/components/lookin/__init__.py +++ b/homeassistant/components/lookin/__init__.py @@ -23,6 +23,7 @@ from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_HOST, Platform 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 .const import DOMAIN, PLATFORMS, TYPE_TO_PLATFORM @@ -182,3 +183,19 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: manager: LookinUDPManager = hass.data[DOMAIN][UDP_MANAGER] await manager.async_stop() return unload_ok + + +async def async_remove_config_entry_device( + hass: HomeAssistant, entry: ConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove lookin config entry from a device.""" + data: LookinData = hass.data[DOMAIN][entry.entry_id] + all_identifiers: set[tuple[str, str]] = { + (DOMAIN, data.lookin_device.id), + *((DOMAIN, remote["UUID"]) for remote in data.devices), + } + return not any( + identifier + for identifier in device_entry.identifiers + if identifier in all_identifiers + ) From 02d18a2e1faacc3d7c5fd10f79a44c21fb2c8e65 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 13 Jun 2022 02:16:15 +0200 Subject: [PATCH 1437/3516] Update whois to 0.9.16 (#73408) --- homeassistant/components/whois/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/whois/manifest.json b/homeassistant/components/whois/manifest.json index 8cbb0f6f502..00a2821c8c4 100644 --- a/homeassistant/components/whois/manifest.json +++ b/homeassistant/components/whois/manifest.json @@ -2,7 +2,7 @@ "domain": "whois", "name": "Whois", "documentation": "https://www.home-assistant.io/integrations/whois", - "requirements": ["whois==0.9.13"], + "requirements": ["whois==0.9.16"], "config_flow": true, "codeowners": ["@frenck"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 875ea35ad00..aa93ba61889 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2433,7 +2433,7 @@ webexteamssdk==1.1.1 whirlpool-sixth-sense==0.15.1 # homeassistant.components.whois -whois==0.9.13 +whois==0.9.16 # homeassistant.components.wiffi wiffi==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0caab7ebe70..f2bc79db670 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1606,7 +1606,7 @@ watchdog==2.1.8 whirlpool-sixth-sense==0.15.1 # homeassistant.components.whois -whois==0.9.13 +whois==0.9.16 # homeassistant.components.wiffi wiffi==1.1.0 From 42d39d2c7e527f35f0194ff8cf4aa2d8c7d9daf2 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 13 Jun 2022 00:25:34 +0000 Subject: [PATCH 1438/3516] [ci skip] Translation update --- .../aladdin_connect/translations/pt-BR.json | 6 +++--- .../asuswrt/translations/pt-BR.json | 2 +- .../components/baf/translations/pt-BR.json | 4 ++-- .../eight_sleep/translations/el.json | 19 +++++++++++++++++++ .../eight_sleep/translations/ja.json | 19 +++++++++++++++++++ .../eight_sleep/translations/nl.json | 19 +++++++++++++++++++ .../eight_sleep/translations/pl.json | 19 +++++++++++++++++++ .../eight_sleep/translations/pt-BR.json | 4 ++-- .../components/generic/translations/nl.json | 2 ++ .../geocaching/translations/pt-BR.json | 6 +++--- .../here_travel_time/translations/pt-BR.json | 4 ++-- .../components/iss/translations/pt-BR.json | 2 +- .../laundrify/translations/pt-BR.json | 4 ++-- .../luftdaten/translations/pt-BR.json | 2 +- .../motion_blinds/translations/pt-BR.json | 2 +- .../netatmo/translations/pt-BR.json | 2 +- .../onewire/translations/pt-BR.json | 2 +- .../radiotherm/translations/nl.json | 9 +++++++++ .../radiotherm/translations/pt-BR.json | 6 +++--- .../components/scrape/translations/nl.json | 18 ++++++++++++++++-- .../components/scrape/translations/pt-BR.json | 14 +++++++------- .../skybell/translations/pt-BR.json | 6 +++--- .../components/slack/translations/pt-BR.json | 6 +++--- .../tankerkoenig/translations/pt-BR.json | 2 +- .../totalconnect/translations/nl.json | 4 ++++ .../ukraine_alarm/translations/pt-BR.json | 6 +++--- .../components/ws66i/translations/pt-BR.json | 4 ++-- .../components/yolink/translations/pt-BR.json | 8 ++++---- 28 files changed, 153 insertions(+), 48 deletions(-) create mode 100644 homeassistant/components/eight_sleep/translations/el.json create mode 100644 homeassistant/components/eight_sleep/translations/ja.json create mode 100644 homeassistant/components/eight_sleep/translations/nl.json create mode 100644 homeassistant/components/eight_sleep/translations/pl.json diff --git a/homeassistant/components/aladdin_connect/translations/pt-BR.json b/homeassistant/components/aladdin_connect/translations/pt-BR.json index 2d709bf1125..c1c6d0097cf 100644 --- a/homeassistant/components/aladdin_connect/translations/pt-BR.json +++ b/homeassistant/components/aladdin_connect/translations/pt-BR.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { @@ -19,7 +19,7 @@ "user": { "data": { "password": "Senha", - "username": "Nome de usu\u00e1rio" + "username": "Usu\u00e1rio" } } } diff --git a/homeassistant/components/asuswrt/translations/pt-BR.json b/homeassistant/components/asuswrt/translations/pt-BR.json index a42cab6fe0d..e72feee12d8 100644 --- a/homeassistant/components/asuswrt/translations/pt-BR.json +++ b/homeassistant/components/asuswrt/translations/pt-BR.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_unique_id": "Imposs\u00edvel determinar um ID exclusivo v\u00e1lido para o dispositivo", - "no_unique_id": "[%key:component::asuswrt::config::abort::not_unique_id_exist%]" + "no_unique_id": "Um dispositivo sem um ID exclusivo v\u00e1lido j\u00e1 est\u00e1 configurado. A configura\u00e7\u00e3o de v\u00e1rias inst\u00e2ncias n\u00e3o \u00e9 poss\u00edvel" }, "error": { "cannot_connect": "Falha ao conectar", diff --git a/homeassistant/components/baf/translations/pt-BR.json b/homeassistant/components/baf/translations/pt-BR.json index 72ce0dd06fc..5c55dcd80d8 100644 --- a/homeassistant/components/baf/translations/pt-BR.json +++ b/homeassistant/components/baf/translations/pt-BR.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "ipv6_not_supported": "IPv6 n\u00e3o \u00e9 suportado." }, "error": { - "cannot_connect": "Falhou ao se conectar", + "cannot_connect": "Falha ao conectar", "unknown": "Erro inesperado" }, "flow_title": "{name} - {model} ({ip_address})", diff --git a/homeassistant/components/eight_sleep/translations/el.json b/homeassistant/components/eight_sleep/translations/el.json new file mode 100644 index 00000000000..2bdf1e689ff --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/el.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "cannot_connect": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf \u03c3\u03cd\u03bd\u03bd\u03b5\u03c6\u03bf Eight Sleep: {error}" + }, + "error": { + "cannot_connect": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf \u03c3\u03cd\u03bd\u03bd\u03b5\u03c6\u03bf Eight Sleep: {error}" + }, + "step": { + "user": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/translations/ja.json b/homeassistant/components/eight_sleep/translations/ja.json new file mode 100644 index 00000000000..c91a9c9dd2c --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/ja.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "Eight Sleep\u30af\u30e9\u30a6\u30c9\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093: {error}" + }, + "error": { + "cannot_connect": "Eight Sleep\u30af\u30e9\u30a6\u30c9\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093: {error}" + }, + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/translations/nl.json b/homeassistant/components/eight_sleep/translations/nl.json new file mode 100644 index 00000000000..afd044ded29 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Kan niet verbinden met Eight Sleep cloud: {error}" + }, + "error": { + "cannot_connect": "Kan niet verbinden met Eight Sleep cloud: {error}" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/translations/pl.json b/homeassistant/components/eight_sleep/translations/pl.json new file mode 100644 index 00000000000..a0f0443cabb --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z chmur\u0105 Eight Sleep: {error}" + }, + "error": { + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z chmur\u0105 Eight Sleep: {error}" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/translations/pt-BR.json b/homeassistant/components/eight_sleep/translations/pt-BR.json index d7ddac41bbe..acb63e80352 100644 --- a/homeassistant/components/eight_sleep/translations/pt-BR.json +++ b/homeassistant/components/eight_sleep/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar \u00e0 nuvem Eight Sleep: {error}" }, "error": { @@ -11,7 +11,7 @@ "user": { "data": { "password": "Senha", - "username": "Nome de usu\u00e1rio" + "username": "Usu\u00e1rio" } } } diff --git a/homeassistant/components/generic/translations/nl.json b/homeassistant/components/generic/translations/nl.json index 354f944148f..b7727190810 100644 --- a/homeassistant/components/generic/translations/nl.json +++ b/homeassistant/components/generic/translations/nl.json @@ -15,6 +15,7 @@ "stream_no_video": "Stream heeft geen video", "stream_not_permitted": "Operatie niet toegestaan bij poging om verbinding te maken met stream. Verkeerd RTSP transport protocol?", "stream_unauthorised": "Autorisatie mislukt bij poging om verbinding te maken met stream", + "template_error": "Fout bij het weergeven van sjabloon. Bekijk het logboek voor meer informatie.", "timeout": "Time-out tijdens het laden van URL", "unable_still_load": "Kan geen geldige afbeelding laden van stilstaande afbeelding URL (b.v. ongeldige host, URL of authenticatie fout). Bekijk het log voor meer informatie.", "unknown": "Onverwachte fout" @@ -57,6 +58,7 @@ "stream_no_video": "Stream heeft geen video", "stream_not_permitted": "Operatie niet toegestaan bij poging om verbinding te maken met stream. Verkeerd RTSP transport protocol?", "stream_unauthorised": "Autorisatie mislukt bij poging om verbinding te maken met stream", + "template_error": "Fout bij het weergeven van sjabloon. Bekijk het logboek voor meer informatie.", "timeout": "Time-out tijdens het laden van URL", "unable_still_load": "Kan geen geldige afbeelding laden van stilstaande afbeelding URL (b.v. ongeldige host, URL of authenticatie fout). Bekijk het log voor meer informatie.", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/geocaching/translations/pt-BR.json b/homeassistant/components/geocaching/translations/pt-BR.json index 4a6c20919d0..3767468530c 100644 --- a/homeassistant/components/geocaching/translations/pt-BR.json +++ b/homeassistant/components/geocaching/translations/pt-BR.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "A conta j\u00e1 est\u00e1 configurada", + "already_configured": "A conta j\u00e1 foi configurada", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", - "no_url_available": "Nenhuma URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre este erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})", - "oauth_error": "Dados de token inv\u00e1lidos recebidos.", + "no_url_available": "N\u00e3o h\u00e1 URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})", + "oauth_error": "Dados de token recebidos inv\u00e1lidos.", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "create_entry": { diff --git a/homeassistant/components/here_travel_time/translations/pt-BR.json b/homeassistant/components/here_travel_time/translations/pt-BR.json index 78996561564..34f862f0029 100644 --- a/homeassistant/components/here_travel_time/translations/pt-BR.json +++ b/homeassistant/components/here_travel_time/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", @@ -41,7 +41,7 @@ }, "user": { "data": { - "api_key": "Chave API", + "api_key": "Chave da API", "mode": "Modo de viagem", "name": "Nome" } diff --git a/homeassistant/components/iss/translations/pt-BR.json b/homeassistant/components/iss/translations/pt-BR.json index 6618abe68f4..af3f4d37147 100644 --- a/homeassistant/components/iss/translations/pt-BR.json +++ b/homeassistant/components/iss/translations/pt-BR.json @@ -14,7 +14,7 @@ "step": { "init": { "data": { - "show_on_map": "[%key:component::iss::config::step::user::data::show_on_map%]" + "show_on_map": "Mostrar no mapa" } } } diff --git a/homeassistant/components/laundrify/translations/pt-BR.json b/homeassistant/components/laundrify/translations/pt-BR.json index c053ae34fe0..1e69a1b3204 100644 --- a/homeassistant/components/laundrify/translations/pt-BR.json +++ b/homeassistant/components/laundrify/translations/pt-BR.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "single_instance_allowed": "J\u00e1 est\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { - "cannot_connect": "Falhou ao se conectar", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_format": "Formato Inv\u00e1lido. Especifique como xxx-xxx.", "unknown": "Erro inesperado" diff --git a/homeassistant/components/luftdaten/translations/pt-BR.json b/homeassistant/components/luftdaten/translations/pt-BR.json index 877cc5d133c..d26faf40b3e 100644 --- a/homeassistant/components/luftdaten/translations/pt-BR.json +++ b/homeassistant/components/luftdaten/translations/pt-BR.json @@ -8,7 +8,7 @@ "step": { "user": { "data": { - "show_on_map": "[%key:component::iss::config::step::user::data::show_on_map%]", + "show_on_map": "Mostrar no mapa", "station_id": "ID do Sensor Luftdaten" } } diff --git a/homeassistant/components/motion_blinds/translations/pt-BR.json b/homeassistant/components/motion_blinds/translations/pt-BR.json index 0a3b68357ee..eb0464e24a9 100644 --- a/homeassistant/components/motion_blinds/translations/pt-BR.json +++ b/homeassistant/components/motion_blinds/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "connection_error": "Falha ao conectar" }, diff --git a/homeassistant/components/netatmo/translations/pt-BR.json b/homeassistant/components/netatmo/translations/pt-BR.json index 9f438868d43..b47c0ea3646 100644 --- a/homeassistant/components/netatmo/translations/pt-BR.json +++ b/homeassistant/components/netatmo/translations/pt-BR.json @@ -52,7 +52,7 @@ "lon_ne": "Longitude nordeste", "lon_sw": "Longitude sudoeste", "mode": "C\u00e1lculo", - "show_on_map": "[%key:component::iss::config::step::user::data::show_on_map%]" + "show_on_map": "Mostrar no mapa" }, "description": "Configure um sensor meteorol\u00f3gico p\u00fablico para uma \u00e1rea.", "title": "Sensor meteorol\u00f3gico p\u00fablico Netatmo" diff --git a/homeassistant/components/onewire/translations/pt-BR.json b/homeassistant/components/onewire/translations/pt-BR.json index 303d36f3cb2..307ca800ba2 100644 --- a/homeassistant/components/onewire/translations/pt-BR.json +++ b/homeassistant/components/onewire/translations/pt-BR.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "host": "Host", + "host": "Nome do host", "port": "Porta" }, "title": "Definir detalhes do servidor" diff --git a/homeassistant/components/radiotherm/translations/nl.json b/homeassistant/components/radiotherm/translations/nl.json index ec2d0fc6554..6e23671fe02 100644 --- a/homeassistant/components/radiotherm/translations/nl.json +++ b/homeassistant/components/radiotherm/translations/nl.json @@ -18,5 +18,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "Stel een permanente hold in bij het aanpassen van de temperatuur." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/pt-BR.json b/homeassistant/components/radiotherm/translations/pt-BR.json index 59b4b32d965..da10f6bd457 100644 --- a/homeassistant/components/radiotherm/translations/pt-BR.json +++ b/homeassistant/components/radiotherm/translations/pt-BR.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "unknown": "Erro inesperado" }, "flow_title": "{name} {model} ({host})", @@ -14,7 +14,7 @@ }, "user": { "data": { - "host": "Host" + "host": "Nome do host" } } } diff --git a/homeassistant/components/scrape/translations/nl.json b/homeassistant/components/scrape/translations/nl.json index 81d41d6ff26..90e85d34677 100644 --- a/homeassistant/components/scrape/translations/nl.json +++ b/homeassistant/components/scrape/translations/nl.json @@ -23,8 +23,15 @@ }, "data_description": { "attribute": "Haal de waarde op van een attribuut op de geselecteerde tag", + "authentication": "Type van de HTTP-authenticatie. Ofwel basic of digest", + "device_class": "Het type/klasse van de sensor om het pictogram in de frontend in te stellen", + "headers": "Headers om te gebruiken voor het webverzoek", + "index": "Definieert welke van de door de CSS-selector geretourneerde elementen moeten worden gebruikt", "resource": "De URL naar de website die de waarde bevat", - "state_class": "De state_class van de sensor" + "select": "Definieert naar welke tag moet worden gezocht. Controleer Beautifulsoup CSS-selectors voor details", + "state_class": "De state_class van de sensor", + "value_template": "Definieert een sjabloon om de status van de sensor te krijgen", + "verify_ssl": "Activeert/de-activeert verificatie van SSL/TLS certificaat, als voorbeeld of het is zelf-getekend" } } } @@ -50,8 +57,15 @@ }, "data_description": { "attribute": "Haal de waarde op van een attribuut op de geselecteerde tag", + "authentication": "Type van de HTTP-authenticatie. Ofwel basic of digest", + "device_class": "Het type/klasse van de sensor om het pictogram in de frontend in te stellen", + "headers": "Headers om te gebruiken voor het webverzoek", + "index": "Definieert welke van de door de CSS-selector geretourneerde elementen moeten worden gebruikt", "resource": "De URL naar de website die de waarde bevat", - "state_class": "De state_class van de sensor" + "select": "Definieert naar welke tag moet worden gezocht. Controleer Beautifulsoup CSS-selectors voor details", + "state_class": "De state_class van de sensor", + "value_template": "Definieert een sjabloon om de status van de sensor te krijgen", + "verify_ssl": "Activeert/de-activeert verificatie van SSL/TLS certificaat, als voorbeeld of het is zelf-getekend" } } } diff --git a/homeassistant/components/scrape/translations/pt-BR.json b/homeassistant/components/scrape/translations/pt-BR.json index 24bbfe1fece..9876157182e 100644 --- a/homeassistant/components/scrape/translations/pt-BR.json +++ b/homeassistant/components/scrape/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "A conta j\u00e1 est\u00e1 configurada" + "already_configured": "A conta j\u00e1 foi configurada" }, "step": { "user": { @@ -17,9 +17,9 @@ "select": "Selecionar", "state_class": "Classe de estado", "unit_of_measurement": "Unidade de medida", - "username": "Nome de usu\u00e1rio", + "username": "Usu\u00e1rio", "value_template": "Modelo de valor", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verifique o certificado SSL" }, "data_description": { "attribute": "Obter valor de um atributo na tag selecionada", @@ -51,18 +51,18 @@ "select": "Selecionar", "state_class": "Classe de estado", "unit_of_measurement": "Unidade de medida", - "username": "Nome de usu\u00e1rio", + "username": "Usu\u00e1rio", "value_template": "Modelo de valor", "verify_ssl": "Verificar SSL" }, "data_description": { "attribute": "Obter valor de um atributo na tag selecionada", - "authentication": "Tipo de autentica\u00e7\u00e3o HTTP. Ou b\u00e1sico ou digerido", - "device_class": "O tipo/classe do sensor para definir o \u00edcone no frontend", + "authentication": "Tipo de autentica\u00e7\u00e3o HTTP. b\u00e1sica ou digerida", + "device_class": "O tipo/classe do sensor para definir o \u00edcone na frontend", "headers": "Cabe\u00e7alhos a serem usados para a solicita\u00e7\u00e3o da web", "index": "Define qual dos elementos retornados pelo seletor CSS usar", "resource": "A URL para o site que cont\u00e9m o valor", - "select": "Define qual tag pesquisar. Verifique os seletores CSS do Beautifulsoup para obter detalhes", + "select": "Define qual tag pesquisar. Verifique os seletores CSS da Beautiful Soup para obter detalhes", "state_class": "O classe de estado do sensor", "value_template": "Define um modelo para obter o estado do sensor", "verify_ssl": "Ativa/desativa a verifica\u00e7\u00e3o do certificado SSL/TLS, por exemplo, se for autoassinado" diff --git a/homeassistant/components/skybell/translations/pt-BR.json b/homeassistant/components/skybell/translations/pt-BR.json index 7e7af1a011a..9fed8c0da02 100644 --- a/homeassistant/components/skybell/translations/pt-BR.json +++ b/homeassistant/components/skybell/translations/pt-BR.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "already_configured": "A conta j\u00e1 est\u00e1 configurada", + "already_configured": "A conta j\u00e1 foi configurada", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { "user": { "data": { - "email": "E-mail", + "email": "Email", "password": "Senha" } } diff --git a/homeassistant/components/slack/translations/pt-BR.json b/homeassistant/components/slack/translations/pt-BR.json index a6299848bfc..834dea1bf0a 100644 --- a/homeassistant/components/slack/translations/pt-BR.json +++ b/homeassistant/components/slack/translations/pt-BR.json @@ -4,17 +4,17 @@ "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { "user": { "data": { - "api_key": "Chave de API", + "api_key": "Chave da API", "default_channel": "Canal padr\u00e3o", "icon": "\u00cdcone", - "username": "Nome de usu\u00e1rio" + "username": "Usu\u00e1rio" }, "data_description": { "api_key": "O token da API do Slack a ser usado para enviar mensagens do Slack.", diff --git a/homeassistant/components/tankerkoenig/translations/pt-BR.json b/homeassistant/components/tankerkoenig/translations/pt-BR.json index cad98a4283f..af26b6167b3 100644 --- a/homeassistant/components/tankerkoenig/translations/pt-BR.json +++ b/homeassistant/components/tankerkoenig/translations/pt-BR.json @@ -11,7 +11,7 @@ "step": { "reauth_confirm": { "data": { - "api_key": "Chave API" + "api_key": "Chave da API" } }, "select_station": { diff --git a/homeassistant/components/totalconnect/translations/nl.json b/homeassistant/components/totalconnect/translations/nl.json index aaf06b0b70f..cec03dc035b 100644 --- a/homeassistant/components/totalconnect/translations/nl.json +++ b/homeassistant/components/totalconnect/translations/nl.json @@ -32,6 +32,10 @@ "options": { "step": { "init": { + "data": { + "auto_bypass_low_battery": "Automatische bypass van batterij bijna leeg" + }, + "description": "Automatisch zones omzeilen op het moment dat ze een bijna lege batterij melden.", "title": "TotalConnect-opties" } } diff --git a/homeassistant/components/ukraine_alarm/translations/pt-BR.json b/homeassistant/components/ukraine_alarm/translations/pt-BR.json index 64b371196e3..128bd85492d 100644 --- a/homeassistant/components/ukraine_alarm/translations/pt-BR.json +++ b/homeassistant/components/ukraine_alarm/translations/pt-BR.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "O local j\u00e1 est\u00e1 configurado", - "cannot_connect": "Falhou ao conectar", + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada", + "cannot_connect": "Falha ao conectar", "max_regions": "M\u00e1ximo de 5 regi\u00f5es podem ser configuradas", "rate_limit": "Excesso de pedidos", - "timeout": "Tempo limite estabelecendo conex\u00e3o", + "timeout": "Tempo limite para estabelecer conex\u00e3o atingido", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/ws66i/translations/pt-BR.json b/homeassistant/components/ws66i/translations/pt-BR.json index d440aab3aa4..68bc805d08c 100644 --- a/homeassistant/components/ws66i/translations/pt-BR.json +++ b/homeassistant/components/ws66i/translations/pt-BR.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/yolink/translations/pt-BR.json b/homeassistant/components/yolink/translations/pt-BR.json index 31a69f7ed3f..bbd59ef845f 100644 --- a/homeassistant/components/yolink/translations/pt-BR.json +++ b/homeassistant/components/yolink/translations/pt-BR.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "A conta j\u00e1 est\u00e1 configurada", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "A conta j\u00e1 foi configurada", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", - "no_url_available": "Nenhuma URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre este erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})", - "oauth_error": "Dados de token inv\u00e1lidos recebidos.", + "no_url_available": "N\u00e3o h\u00e1 URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})", + "oauth_error": "Dados de token recebidos inv\u00e1lidos.", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "create_entry": { From 5854dfa84f7d575d0b96640a0b3cbacef7a2b869 Mon Sep 17 00:00:00 2001 From: Marcio Granzotto Rodrigues Date: Sun, 12 Jun 2022 22:27:18 -0300 Subject: [PATCH 1439/3516] Fix smart by bond detection with v3 firmware (#73414) --- homeassistant/components/bond/manifest.json | 2 +- homeassistant/components/bond/utils.py | 5 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bond/common.py | 2 +- tests/components/bond/test_config_flow.py | 24 ++++++------- tests/components/bond/test_diagnostics.py | 2 +- tests/components/bond/test_init.py | 38 ++++++++++++++------- tests/components/bond/test_light.py | 2 +- 9 files changed, 46 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index 52e9dd1763f..a5625d7b642 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-async==0.1.20"], + "requirements": ["bond-async==0.1.22"], "zeroconf": ["_bond._tcp.local."], "codeowners": ["@bdraco", "@prystupa", "@joshs85", "@marciogranzotto"], "quality_scale": "platinum", diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index cba213d9450..c426bf64577 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -5,7 +5,7 @@ import logging from typing import Any, cast from aiohttp import ClientResponseError -from bond_async import Action, Bond +from bond_async import Action, Bond, BondType from homeassistant.util.async_ import gather_with_concurrency @@ -224,4 +224,5 @@ class BondHub: @property def is_bridge(self) -> bool: """Return if the Bond is a Bond Bridge.""" - return bool(self._bridge) + bondid = self._version["bondid"] + return bool(BondType.is_bridge_from_serial(bondid)) diff --git a/requirements_all.txt b/requirements_all.txt index aa93ba61889..843680f5227 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -420,7 +420,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bond -bond-async==0.1.20 +bond-async==0.1.22 # homeassistant.components.bosch_shc boschshcpy==0.2.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f2bc79db670..151a427470b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -321,7 +321,7 @@ blebox_uniapi==1.3.3 blinkpy==0.19.0 # homeassistant.components.bond -bond-async==0.1.20 +bond-async==0.1.22 # homeassistant.components.bosch_shc boschshcpy==0.2.30 diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index c5a649ab30a..f14efcdf172 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -127,7 +127,7 @@ def patch_bond_version( return nullcontext() if return_value is None: - return_value = {"bondid": "test-bond-id"} + return_value = {"bondid": "ZXXX12345"} return patch( "homeassistant.components.bond.Bond.version", diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index 67910af7b6c..4f1e313a34a 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -35,7 +35,7 @@ async def test_user_form(hass: core.HomeAssistant): assert result["errors"] == {} with patch_bond_version( - return_value={"bondid": "test-bond-id"} + return_value={"bondid": "ZXXX12345"} ), patch_bond_device_ids( return_value=["f6776c11", "f6776c12"] ), patch_bond_bridge(), patch_bond_device_properties(), patch_bond_device(), _patch_async_setup_entry() as mock_setup_entry: @@ -64,7 +64,7 @@ async def test_user_form_with_non_bridge(hass: core.HomeAssistant): assert result["errors"] == {} with patch_bond_version( - return_value={"bondid": "test-bond-id"} + return_value={"bondid": "KXXX12345"} ), patch_bond_device_ids( return_value=["f6776c11"] ), patch_bond_device_properties(), patch_bond_device( @@ -96,7 +96,7 @@ async def test_user_form_invalid_auth(hass: core.HomeAssistant): ) with patch_bond_version( - return_value={"bond_id": "test-bond-id"} + return_value={"bond_id": "ZXXX12345"} ), patch_bond_bridge(), patch_bond_device_ids( side_effect=ClientResponseError(Mock(), Mock(), status=401), ): @@ -203,7 +203,7 @@ async def test_zeroconf_form(hass: core.HomeAssistant): host="test-host", addresses=["test-host"], hostname="mock_hostname", - name="test-bond-id.some-other-tail-info", + name="ZXXX12345.some-other-tail-info", port=None, properties={}, type="mock_type", @@ -213,7 +213,7 @@ async def test_zeroconf_form(hass: core.HomeAssistant): assert result["errors"] == {} with patch_bond_version( - return_value={"bondid": "test-bond-id"} + return_value={"bondid": "ZXXX12345"} ), patch_bond_bridge(), patch_bond_device_ids(), _patch_async_setup_entry() as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -241,7 +241,7 @@ async def test_zeroconf_form_token_unavailable(hass: core.HomeAssistant): host="test-host", addresses=["test-host"], hostname="mock_hostname", - name="test-bond-id.some-other-tail-info", + name="ZXXX12345.some-other-tail-info", port=None, properties={}, type="mock_type", @@ -270,7 +270,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.""" - with patch_bond_version(return_value={"bondid": "test-bond-id"}), patch_bond_token( + with patch_bond_version(return_value={"bondid": "ZXXX12345"}), patch_bond_token( return_value={"token": "discovered-token"} ), patch_bond_bridge( return_value={"name": "discovered-name"} @@ -282,7 +282,7 @@ async def test_zeroconf_form_with_token_available(hass: core.HomeAssistant): host="test-host", addresses=["test-host"], hostname="mock_hostname", - name="test-bond-id.some-other-tail-info", + name="ZXXX12345.some-other-tail-info", port=None, properties={}, type="mock_type", @@ -323,7 +323,7 @@ async def test_zeroconf_form_with_token_available_name_unavailable( host="test-host", addresses=["test-host"], hostname="mock_hostname", - name="test-bond-id.some-other-tail-info", + name="ZXXX12345.some-other-tail-info", port=None, properties={}, type="mock_type", @@ -341,7 +341,7 @@ async def test_zeroconf_form_with_token_available_name_unavailable( await hass.async_block_till_done() assert result2["type"] == "create_entry" - assert result2["title"] == "test-bond-id" + assert result2["title"] == "ZXXX12345" assert result2["data"] == { CONF_HOST: "test-host", CONF_ACCESS_TOKEN: "discovered-token", @@ -511,7 +511,7 @@ async def test_zeroconf_form_unexpected_error(hass: core.HomeAssistant): host="test-host", addresses=["test-host"], hostname="mock_hostname", - name="test-bond-id.some-other-tail-info", + name="ZXXX12345.some-other-tail-info", port=None, properties={}, type="mock_type", @@ -536,7 +536,7 @@ async def _help_test_form_unexpected_error( ) with patch_bond_version( - return_value={"bond_id": "test-bond-id"} + return_value={"bond_id": "ZXXX12345"} ), patch_bond_device_ids(side_effect=error): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input diff --git a/tests/components/bond/test_diagnostics.py b/tests/components/bond/test_diagnostics.py index 88d33ff2cc0..b738c72ee8c 100644 --- a/tests/components/bond/test_diagnostics.py +++ b/tests/components/bond/test_diagnostics.py @@ -39,5 +39,5 @@ async def test_diagnostics(hass, hass_client): "data": {"access_token": "**REDACTED**", "host": "some host"}, "title": "Mock Title", }, - "hub": {"version": {"bondid": "test-bond-id"}}, + "hub": {"version": {"bondid": "ZXXX12345"}}, } diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 5db5d8e65bf..d02e2bed4ec 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -9,7 +9,7 @@ import pytest from homeassistant.components.bond.const import DOMAIN from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST +from homeassistant.const import ATTR_ASSUMED_STATE, CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import EntityRegistry @@ -86,7 +86,7 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss with patch_bond_bridge(), patch_bond_version( return_value={ - "bondid": "test-bond-id", + "bondid": "ZXXX12345", "target": "test-model", "fw_ver": "test-version", "mcu_ver": "test-hw-version", @@ -104,11 +104,11 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss assert config_entry.entry_id in hass.data[DOMAIN] assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.unique_id == "test-bond-id" + assert config_entry.unique_id == "ZXXX12345" # verify hub device is registered correctly device_registry = dr.async_get(hass) - hub = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")}) + hub = device_registry.async_get_device(identifiers={(DOMAIN, "ZXXX12345")}) assert hub.name == "bond-name" assert hub.manufacturer == "Olibra" assert hub.model == "test-model" @@ -156,7 +156,7 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant): ) old_identifers = (DOMAIN, "device_id") - new_identifiers = (DOMAIN, "test-bond-id", "device_id") + new_identifiers = (DOMAIN, "ZXXX12345", "device_id") device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -169,7 +169,7 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant): with patch_bond_bridge(), patch_bond_version( return_value={ - "bondid": "test-bond-id", + "bondid": "ZXXX12345", "target": "test-model", "fw_ver": "test-version", } @@ -190,7 +190,7 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant): assert config_entry.entry_id in hass.data[DOMAIN] assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.unique_id == "test-bond-id" + assert config_entry.unique_id == "ZXXX12345" # verify the device info is cleaned up assert device_registry.async_get_device(identifiers={old_identifers}) is None @@ -210,7 +210,7 @@ async def test_smart_by_bond_device_suggested_area(hass: HomeAssistant): side_effect=ClientResponseError(Mock(), Mock(), status=404) ), patch_bond_version( return_value={ - "bondid": "test-bond-id", + "bondid": "KXXX12345", "target": "test-model", "fw_ver": "test-version", } @@ -232,10 +232,10 @@ async def test_smart_by_bond_device_suggested_area(hass: HomeAssistant): assert config_entry.entry_id in hass.data[DOMAIN] assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.unique_id == "test-bond-id" + assert config_entry.unique_id == "KXXX12345" device_registry = dr.async_get(hass) - device = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")}) + device = device_registry.async_get_device(identifiers={(DOMAIN, "KXXX12345")}) assert device is not None assert device.suggested_area == "Den" @@ -256,7 +256,7 @@ async def test_bridge_device_suggested_area(hass: HomeAssistant): } ), patch_bond_version( return_value={ - "bondid": "test-bond-id", + "bondid": "ZXXX12345", "target": "test-model", "fw_ver": "test-version", } @@ -278,10 +278,10 @@ async def test_bridge_device_suggested_area(hass: HomeAssistant): assert config_entry.entry_id in hass.data[DOMAIN] assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.unique_id == "test-bond-id" + assert config_entry.unique_id == "ZXXX12345" device_registry = dr.async_get(hass) - device = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")}) + device = device_registry.async_get_device(identifiers={(DOMAIN, "ZXXX12345")}) assert device is not None assert device.suggested_area == "Office" @@ -343,3 +343,15 @@ async def test_device_remove_devices(hass, hass_ws_client): ) is False ) + + +async def test_smart_by_bond_v3_firmware(hass: HomeAssistant) -> None: + """Test we can detect smart by bond with the v3 firmware.""" + await setup_platform( + hass, + FAN_DOMAIN, + ceiling_fan("name-1"), + bond_version={"bondid": "KXXXX12345", "target": "breck-northstar"}, + bond_device_id="test-device-id", + ) + assert ATTR_ASSUMED_STATE not in hass.states.get("fan.name_1").attributes diff --git a/tests/components/bond/test_light.py b/tests/components/bond/test_light.py index c7d8f195423..7577b1d70ab 100644 --- a/tests/components/bond/test_light.py +++ b/tests/components/bond/test_light.py @@ -249,7 +249,7 @@ async def test_sbb_trust_state(hass: core.HomeAssistant): """Assumed state should be False if device is a Smart by Bond.""" version = { "model": "MR123A", - "bondid": "test-bond-id", + "bondid": "KXXX12345", } await setup_platform( hass, LIGHT_DOMAIN, ceiling_fan("name-1"), bond_version=version, bridge={} From 9ae713f1288f936ebdb86e4aff304e86f91ff59f Mon Sep 17 00:00:00 2001 From: kingy444 Date: Mon, 13 Jun 2022 12:26:38 +1000 Subject: [PATCH 1440/3516] Improve error handling of powerview hub maintenance, remove invalid device classes (#73395) --- .../components/hunterdouglas_powerview/button.py | 8 +------- .../components/hunterdouglas_powerview/coordinator.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/button.py b/homeassistant/components/hunterdouglas_powerview/button.py index 131ef279a20..b13d0217a20 100644 --- a/homeassistant/components/hunterdouglas_powerview/button.py +++ b/homeassistant/components/hunterdouglas_powerview/button.py @@ -7,11 +7,7 @@ from typing import Any, Final from aiopvapi.resources.shade import BaseShade, factory as PvShade -from homeassistant.components.button import ( - ButtonDeviceClass, - ButtonEntity, - ButtonEntityDescription, -) +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory @@ -50,7 +46,6 @@ BUTTONS: Final = [ key="calibrate", name="Calibrate", icon="mdi:swap-vertical-circle-outline", - device_class=ButtonDeviceClass.UPDATE, entity_category=EntityCategory.DIAGNOSTIC, press_action=lambda shade: shade.calibrate(), ), @@ -58,7 +53,6 @@ BUTTONS: Final = [ key="identify", name="Identify", icon="mdi:crosshairs-question", - device_class=ButtonDeviceClass.UPDATE, entity_category=EntityCategory.DIAGNOSTIC, press_action=lambda shade: shade.jog(), ), diff --git a/homeassistant/components/hunterdouglas_powerview/coordinator.py b/homeassistant/components/hunterdouglas_powerview/coordinator.py index bf3d6eb7a54..7c45feba491 100644 --- a/homeassistant/components/hunterdouglas_powerview/coordinator.py +++ b/homeassistant/components/hunterdouglas_powerview/coordinator.py @@ -36,9 +36,19 @@ class PowerviewShadeUpdateCoordinator(DataUpdateCoordinator[PowerviewShadeData]) async def _async_update_data(self) -> PowerviewShadeData: """Fetch data from shade endpoint.""" + async with async_timeout.timeout(10): shade_entries = await self.shades.get_resources() + + if isinstance(shade_entries, bool): + # hub returns boolean on a 204/423 empty response (maintenance) + # continual polling results in inevitable error + raise UpdateFailed("Powerview Hub is undergoing maintenance") + if not shade_entries: raise UpdateFailed("Failed to fetch new shade data") + + # only update if shade_entries is valid self.data.store_group_data(shade_entries[SHADE_DATA]) + return self.data From 9159db4b4ab83736eed6763138684e8caff550cf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Jun 2022 17:04:17 -1000 Subject: [PATCH 1441/3516] Only update unifiprotect ips from discovery when the console is offline (#73411) --- .../components/unifiprotect/config_flow.py | 32 ++++++++++++++- homeassistant/components/unifiprotect/data.py | 10 +++++ .../components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../unifiprotect/test_config_flow.py | 40 ++++++++++++++++++- 6 files changed, 83 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 23e2541e6d8..e183edf4259 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -8,6 +8,7 @@ from typing import Any from aiohttp import CookieJar from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient from pyunifiprotect.data import NVR +from unifi_discovery import async_console_is_alive import voluptuous as vol from homeassistant import config_entries @@ -22,7 +23,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.aiohttp_client import ( + async_create_clientsession, + async_get_clientsession, +) from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.loader import async_get_integration from homeassistant.util.network import is_ip_address @@ -37,11 +41,17 @@ from .const import ( MIN_REQUIRED_PROTECT_V, OUTDATED_LOG_MESSAGE, ) +from .data import async_last_update_was_successful from .discovery import async_start_discovery from .utils import _async_resolve, _async_short_mac, _async_unifi_mac_from_hass _LOGGER = logging.getLogger(__name__) +ENTRY_FAILURE_STATES = ( + config_entries.ConfigEntryState.SETUP_ERROR, + config_entries.ConfigEntryState.SETUP_RETRY, +) + async def async_local_user_documentation_url(hass: HomeAssistant) -> str: """Get the documentation url for creating a local user.""" @@ -54,6 +64,25 @@ def _host_is_direct_connect(host: str) -> bool: return host.endswith(".ui.direct") +async def _async_console_is_offline( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, +) -> bool: + """Check if a console is offline. + + We define offline by the config entry + is in a failure/retry state or the updates + are failing and the console is unreachable + since protect may be updating. + """ + return bool( + entry.state in ENTRY_FAILURE_STATES + or not async_last_update_was_successful(hass, entry) + ) and not await async_console_is_alive( + async_get_clientsession(hass, verify_ssl=False), entry.data[CONF_HOST] + ) + + class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a UniFi Protect config flow.""" @@ -111,6 +140,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): not entry_has_direct_connect and is_ip_address(entry_host) and entry_host != source_ip + and await _async_console_is_offline(self.hass, entry) ): new_host = source_ip if new_host: diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 68c8873c17e..bcc1e561e99 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -26,6 +26,16 @@ from .utils import async_get_adoptable_devices_by_type, async_get_devices _LOGGER = logging.getLogger(__name__) +@callback +def async_last_update_was_successful(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Check if the last update was successful for a config entry.""" + return bool( + DOMAIN in hass.data + and entry.entry_id in hass.data[DOMAIN] + and hass.data[DOMAIN][entry.entry_id].last_update_success + ) + + class ProtectData: """Coordinate updates.""" diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 199298d76ca..2554d12c866 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.9.2", "unifi-discovery==1.1.3"], + "requirements": ["pyunifiprotect==3.9.2", "unifi-discovery==1.1.4"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 843680f5227..f0418e6596f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2355,7 +2355,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.unifiprotect -unifi-discovery==1.1.3 +unifi-discovery==1.1.4 # homeassistant.components.unifiled unifiled==0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 151a427470b..cdb907b731e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1552,7 +1552,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.unifiprotect -unifi-discovery==1.1.3 +unifi-discovery==1.1.4 # homeassistant.components.upb upb_lib==0.4.12 diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 80e845591b1..75f08acb37c 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -402,7 +402,10 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin ) mock_config.add_to_hass(hass) - with _patch_discovery(): + with _patch_discovery(), patch( + "homeassistant.components.unifiprotect.config_flow.async_console_is_alive", + return_value=False, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, @@ -415,6 +418,41 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin assert mock_config.data[CONF_HOST] == "127.0.0.1" +async def test_discovered_by_unifi_discovery_does_not_update_ip_when_console_is_still_online( + hass: HomeAssistant, mock_nvr: NVR +) -> None: + """Test a discovery from unifi-discovery does not update the ip unless the console at the old ip is offline.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.2.2.2", + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect", + "port": 443, + "verify_ssl": False, + }, + version=2, + unique_id=DEVICE_MAC_ADDRESS.replace(":", "").upper(), + ) + mock_config.add_to_hass(hass) + + with _patch_discovery(), patch( + "homeassistant.components.unifiprotect.config_flow.async_console_is_alive", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=UNIFI_DISCOVERY_DICT, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert mock_config.data[CONF_HOST] == "1.2.2.2" + + async def test_discovered_host_not_updated_if_existing_is_a_hostname( hass: HomeAssistant, mock_nvr: NVR ) -> None: From a7f72931ad3d83c8974714e956cb9b729f199293 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Jun 2022 17:12:49 -1000 Subject: [PATCH 1442/3516] Simplify esphome state updates (#73409) --- homeassistant/components/esphome/__init__.py | 19 +++---------------- .../components/esphome/entry_data.py | 17 ++++++----------- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 8aa2dba4d07..2e88a883dc1 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -568,6 +568,7 @@ async def platform_async_setup_entry( @callback def async_list_entities(infos: list[EntityInfo]) -> None: """Update entities of this platform when entities are listed.""" + key_to_component = entry_data.key_to_component old_infos = entry_data.info[component_key] new_infos: dict[int, EntityInfo] = {} add_entities = [] @@ -586,10 +587,12 @@ async def platform_async_setup_entry( entity = entity_type(entry_data, component_key, info.key) add_entities.append(entity) new_infos[info.key] = info + key_to_component[info.key] = component_key # Remove old entities for info in old_infos.values(): entry_data.async_remove_entity(hass, component_key, info.key) + key_to_component.pop(info.key, None) # First copy the now-old info into the backup object entry_data.old_info[component_key] = entry_data.info[component_key] @@ -604,22 +607,6 @@ async def platform_async_setup_entry( async_dispatcher_connect(hass, signal, async_list_entities) ) - @callback - def async_entity_state(state: EntityState) -> None: - """Notify the appropriate entity of an updated state.""" - if not isinstance(state, state_type): - return - # cast back to upper type, otherwise mypy gets confused - state = cast(EntityState, state) - - entry_data.state[component_key][state.key] = state - entry_data.async_update_entity(hass, component_key, state.key) - - signal = f"esphome_{entry.entry_id}_on_state" - entry_data.cleanup_callbacks.append( - async_dispatcher_connect(hass, signal, async_entity_state) - ) - _PropT = TypeVar("_PropT", bound=Callable[..., Any]) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 4c5a94afe0f..7980d1a6a17 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -65,6 +65,7 @@ class RuntimeEntryData: store: Store state: dict[str, dict[int, EntityState]] = field(default_factory=dict) info: dict[str, dict[int, EntityInfo]] = field(default_factory=dict) + key_to_component: dict[int, str] = field(default_factory=dict) # A second list of EntityInfo objects # This is necessary for when an entity is being removed. HA requires @@ -82,14 +83,6 @@ class RuntimeEntryData: platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock) _storage_contents: dict[str, Any] | None = None - @callback - def async_update_entity( - self, hass: HomeAssistant, component_key: str, key: int - ) -> None: - """Schedule the update of an entity.""" - signal = f"esphome_{self.entry_id}_update_{component_key}_{key}" - async_dispatcher_send(hass, signal) - @callback def async_remove_entity( self, hass: HomeAssistant, component_key: str, key: int @@ -131,9 +124,11 @@ class RuntimeEntryData: @callback def async_update_state(self, hass: HomeAssistant, state: EntityState) -> None: - """Distribute an update of state information to all platforms.""" - signal = f"esphome_{self.entry_id}_on_state" - async_dispatcher_send(hass, signal, state) + """Distribute an update of state information to the target.""" + component_key = self.key_to_component[state.key] + self.state[component_key][state.key] = state + signal = f"esphome_{self.entry_id}_update_{component_key}_{state.key}" + async_dispatcher_send(hass, signal) @callback def async_update_device_state(self, hass: HomeAssistant) -> None: From c6a6d7039ef7e46251ce8134589fb32e22cb813a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Jun 2022 17:14:19 -1000 Subject: [PATCH 1443/3516] Add unique ids to lutron_caseta scenes (#73383) --- .coveragerc | 1 + .../components/lutron_caseta/scene.py | 25 ++++++++++++++----- .../components/lutron_caseta/util.py | 7 ++++++ 3 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/lutron_caseta/util.py diff --git a/.coveragerc b/.coveragerc index 8d2cdd32336..455bc7fe8a8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -679,6 +679,7 @@ omit = homeassistant/components/lutron_caseta/light.py homeassistant/components/lutron_caseta/scene.py homeassistant/components/lutron_caseta/switch.py + homeassistant/components/lutron_caseta/util.py homeassistant/components/lw12wifi/light.py homeassistant/components/lyric/__init__.py homeassistant/components/lyric/api.py diff --git a/homeassistant/components/lutron_caseta/scene.py b/homeassistant/components/lutron_caseta/scene.py index d73d8011481..1bbd69615e1 100644 --- a/homeassistant/components/lutron_caseta/scene.py +++ b/homeassistant/components/lutron_caseta/scene.py @@ -1,12 +1,17 @@ """Support for Lutron Caseta scenes.""" from typing import Any +from pylutron_caseta.smartbridge import Smartbridge + 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 BRIDGE_LEAP, DOMAIN as CASETA_DOMAIN +from . import _area_and_name_from_name +from .const import BRIDGE_DEVICE, BRIDGE_LEAP, DOMAIN as CASETA_DOMAIN +from .util import serial_to_unique_id async def async_setup_entry( @@ -20,19 +25,27 @@ async def async_setup_entry( scene entities. """ data = hass.data[CASETA_DOMAIN][config_entry.entry_id] - bridge = data[BRIDGE_LEAP] + bridge: Smartbridge = data[BRIDGE_LEAP] + bridge_device = data[BRIDGE_DEVICE] scenes = bridge.get_scenes() - async_add_entities(LutronCasetaScene(scenes[scene], bridge) for scene in scenes) + async_add_entities( + LutronCasetaScene(scenes[scene], bridge, bridge_device) for scene in scenes + ) class LutronCasetaScene(Scene): """Representation of a Lutron Caseta scene.""" - def __init__(self, scene, bridge): + def __init__(self, scene, bridge, bridge_device): """Initialize the Lutron Caseta scene.""" - self._attr_name = scene["name"] self._scene_id = scene["scene_id"] - self._bridge = bridge + self._bridge: Smartbridge = bridge + bridge_unique_id = serial_to_unique_id(bridge_device["serial"]) + self._attr_device_info = DeviceInfo( + identifiers={(CASETA_DOMAIN, bridge_device["serial"])}, + ) + self._attr_name = _area_and_name_from_name(scene["name"])[1] + self._attr_unique_id = f"scene_{bridge_unique_id}_{self._scene_id}" async def async_activate(self, **kwargs: Any) -> None: """Activate the scene.""" diff --git a/homeassistant/components/lutron_caseta/util.py b/homeassistant/components/lutron_caseta/util.py new file mode 100644 index 00000000000..dfcf7a32228 --- /dev/null +++ b/homeassistant/components/lutron_caseta/util.py @@ -0,0 +1,7 @@ +"""Support for Lutron Caseta.""" +from __future__ import annotations + + +def serial_to_unique_id(serial: int) -> str: + """Convert a lutron serial number to a unique id.""" + return hex(serial)[2:].zfill(8) From 7756ddbe80b5bf6e18c1ca1f858b83e3cdc1e09a Mon Sep 17 00:00:00 2001 From: Corbeno Date: Sun, 12 Jun 2022 22:15:01 -0500 Subject: [PATCH 1444/3516] Bump proxmoxer to 1.3.1 (#73418) bump proxmoxer --- homeassistant/components/proxmoxve/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/proxmoxve/manifest.json b/homeassistant/components/proxmoxve/manifest.json index 4b600abc930..aa76aa60118 100644 --- a/homeassistant/components/proxmoxve/manifest.json +++ b/homeassistant/components/proxmoxve/manifest.json @@ -3,7 +3,7 @@ "name": "Proxmox VE", "documentation": "https://www.home-assistant.io/integrations/proxmoxve", "codeowners": ["@jhollowe", "@Corbeno"], - "requirements": ["proxmoxer==1.1.1"], + "requirements": ["proxmoxer==1.3.1"], "iot_class": "local_polling", "loggers": ["proxmoxer"] } diff --git a/requirements_all.txt b/requirements_all.txt index f0418e6596f..ee66e93b7e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1275,7 +1275,7 @@ proliphix==0.4.1 prometheus_client==0.7.1 # homeassistant.components.proxmoxve -proxmoxer==1.1.1 +proxmoxer==1.3.1 # homeassistant.components.systemmonitor psutil==5.9.0 From 1d5290b03f4d3027b3b502de1022cdc1b6eb02d3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 13 Jun 2022 05:17:51 +0200 Subject: [PATCH 1445/3516] Update watchdog to 2.1.9 (#73407) --- 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 f7562633ba0..64bfbb3df37 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.8"], + "requirements": ["watchdog==2.1.9"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index ee66e93b7e8..8e32ca35d62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2421,7 +2421,7 @@ wallbox==0.4.9 waqiasync==1.0.0 # homeassistant.components.folder_watcher -watchdog==2.1.8 +watchdog==2.1.9 # homeassistant.components.waterfurnace waterfurnace==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cdb907b731e..541e36cf252 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1600,7 +1600,7 @@ wakeonlan==2.0.1 wallbox==0.4.9 # homeassistant.components.folder_watcher -watchdog==2.1.8 +watchdog==2.1.9 # homeassistant.components.whirlpool whirlpool-sixth-sense==0.15.1 From 23e17c5b47ae20ad85c8c15baf2ab3521457b77d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 13 Jun 2022 05:17:58 +0200 Subject: [PATCH 1446/3516] Update coverage to 6.4.1 (#73405) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 7c03d2e51c2..5d366a3c350 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt codecov==2.1.12 -coverage==6.4 +coverage==6.4.1 freezegun==1.2.1 mock-open==1.4.0 mypy==0.961 From f85409b2ea326d5f470d6bfcbaf02a21e92cca27 Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Sun, 12 Jun 2022 23:18:48 -0400 Subject: [PATCH 1447/3516] Remove deprecated services from Mazda integration (#73403) --- homeassistant/components/mazda/__init__.py | 55 +++++----------- homeassistant/components/mazda/const.py | 10 --- homeassistant/components/mazda/services.yaml | 66 -------------------- tests/components/mazda/test_init.py | 48 ++++++++++---- 4 files changed, 51 insertions(+), 128 deletions(-) diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 2af4e46bb1a..85b9700a624 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -33,7 +33,7 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_VEHICLES, DOMAIN, SERVICES +from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_VEHICLES, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -109,34 +109,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if vehicle_id == 0 or api_client is None: raise HomeAssistantError("Vehicle ID not found") - if service_call.service in ( - "start_engine", - "stop_engine", - "turn_on_hazard_lights", - "turn_off_hazard_lights", - ): - _LOGGER.warning( - "The mazda.%s service is deprecated and has been replaced by a button entity; " - "Please use the button entity instead", - service_call.service, - ) - - if service_call.service in ("start_charging", "stop_charging"): - _LOGGER.warning( - "The mazda.%s service is deprecated and has been replaced by a switch entity; " - "Please use the charging switch entity instead", - service_call.service, - ) - api_method = getattr(api_client, service_call.service) try: - if service_call.service == "send_poi": - latitude = service_call.data["latitude"] - longitude = service_call.data["longitude"] - poi_name = service_call.data["poi_name"] - await api_method(vehicle_id, latitude, longitude, poi_name) - else: - await api_method(vehicle_id) + latitude = service_call.data["latitude"] + longitude = service_call.data["longitude"] + poi_name = service_call.data["poi_name"] + await api_method(vehicle_id, latitude, longitude, poi_name) except Exception as ex: raise HomeAssistantError(ex) from ex @@ -157,12 +135,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return device_id - service_schema = vol.Schema( - {vol.Required("device_id"): vol.All(cv.string, validate_mazda_device_id)} - ) - - service_schema_send_poi = service_schema.extend( + service_schema_send_poi = vol.Schema( { + vol.Required("device_id"): vol.All(cv.string, validate_mazda_device_id), vol.Required("latitude"): cv.latitude, vol.Required("longitude"): cv.longitude, vol.Required("poi_name"): cv.string, @@ -220,13 +195,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) # Register services - for service in SERVICES: - hass.services.async_register( - DOMAIN, - service, - async_handle_service_call, - schema=service_schema_send_poi if service == "send_poi" else service_schema, - ) + hass.services.async_register( + DOMAIN, + "send_poi", + async_handle_service_call, + schema=service_schema_send_poi, + ) return True @@ -237,8 +211,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Only remove services if it is the last config entry if len(hass.data[DOMAIN]) == 1: - for service in SERVICES: - hass.services.async_remove(DOMAIN, service) + hass.services.async_remove(DOMAIN, "send_poi") if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/mazda/const.py b/homeassistant/components/mazda/const.py index 5baeef3102d..58ca2183a56 100644 --- a/homeassistant/components/mazda/const.py +++ b/homeassistant/components/mazda/const.py @@ -7,13 +7,3 @@ DATA_COORDINATOR = "coordinator" DATA_VEHICLES = "vehicles" MAZDA_REGIONS = {"MNAO": "North America", "MME": "Europe", "MJO": "Japan"} - -SERVICES = [ - "send_poi", - "start_charging", - "start_engine", - "stop_charging", - "stop_engine", - "turn_off_hazard_lights", - "turn_on_hazard_lights", -] diff --git a/homeassistant/components/mazda/services.yaml b/homeassistant/components/mazda/services.yaml index 80d8c2f64f6..1abf8bd5dea 100644 --- a/homeassistant/components/mazda/services.yaml +++ b/homeassistant/components/mazda/services.yaml @@ -1,47 +1,3 @@ -start_engine: - name: Start engine - description: Start the vehicle engine. - fields: - device_id: - name: Vehicle - description: The vehicle to start - required: true - selector: - device: - integration: mazda -stop_engine: - name: Stop engine - description: Stop the vehicle engine. - fields: - device_id: - name: Vehicle - description: The vehicle to stop - required: true - selector: - device: - integration: mazda -turn_on_hazard_lights: - name: Turn on hazard lights - description: Turn on the vehicle hazard lights. The lights will flash briefly and then turn off. - fields: - device_id: - name: Vehicle - description: The vehicle to turn hazard lights on - required: true - selector: - device: - integration: mazda -turn_off_hazard_lights: - name: Turn off hazard lights - description: Turn off the vehicle hazard lights if they have been manually turned on from inside the vehicle. - fields: - device_id: - name: Vehicle - description: The vehicle to turn hazard lights off - required: true - selector: - device: - integration: mazda send_poi: name: Send POI description: Send a GPS location to the vehicle's navigation system as a POI (Point of Interest). Requires a navigation SD card installed in the vehicle. @@ -82,25 +38,3 @@ send_poi: required: true selector: text: -start_charging: - name: Start charging - description: Start charging the vehicle. For electric vehicles only. - fields: - device_id: - name: Vehicle - description: The vehicle to start charging - required: true - selector: - device: - integration: mazda -stop_charging: - name: Stop charging - description: Stop charging the vehicle. For electric vehicles only. - fields: - device_id: - name: Vehicle - description: The vehicle to stop charging - required: true - selector: - device: - integration: mazda diff --git a/tests/components/mazda/test_init.py b/tests/components/mazda/test_init.py index 9d221bbfe88..bd443bb17f3 100644 --- a/tests/components/mazda/test_init.py +++ b/tests/components/mazda/test_init.py @@ -203,12 +203,6 @@ async def test_device_no_nickname(hass): @pytest.mark.parametrize( "service, service_data, expected_args", [ - ("start_charging", {}, [12345]), - ("start_engine", {}, [12345]), - ("stop_charging", {}, [12345]), - ("stop_engine", {}, [12345]), - ("turn_off_hazard_lights", {}, [12345]), - ("turn_on_hazard_lights", {}, [12345]), ( "send_poi", {"latitude": 1.2345, "longitude": 2.3456, "poi_name": "Work"}, @@ -241,7 +235,15 @@ async def test_service_invalid_device_id(hass): with pytest.raises(vol.error.MultipleInvalid) as err: await hass.services.async_call( - DOMAIN, "start_engine", {"device_id": "invalid"}, blocking=True + DOMAIN, + "send_poi", + { + "device_id": "invalid", + "latitude": 1.2345, + "longitude": 6.7890, + "poi_name": "poi_name", + }, + blocking=True, ) await hass.async_block_till_done() @@ -262,7 +264,15 @@ async def test_service_device_id_not_mazda_vehicle(hass): with pytest.raises(vol.error.MultipleInvalid) as err: await hass.services.async_call( - DOMAIN, "start_engine", {"device_id": other_device.id}, blocking=True + DOMAIN, + "send_poi", + { + "device_id": other_device.id, + "latitude": 1.2345, + "longitude": 6.7890, + "poi_name": "poi_name", + }, + blocking=True, ) await hass.async_block_till_done() @@ -287,7 +297,15 @@ async def test_service_vehicle_id_not_found(hass): with pytest.raises(HomeAssistantError) as err: await hass.services.async_call( - DOMAIN, "start_engine", {"device_id": device_id}, blocking=True + DOMAIN, + "send_poi", + { + "device_id": device_id, + "latitude": 1.2345, + "longitude": 6.7890, + "poi_name": "poi_name", + }, + blocking=True, ) await hass.async_block_till_done() @@ -324,11 +342,19 @@ async def test_service_mazda_api_error(hass): device_id = reg_device.id with patch( - "homeassistant.components.mazda.MazdaAPI.start_engine", + "homeassistant.components.mazda.MazdaAPI.send_poi", side_effect=MazdaException("Test error"), ), pytest.raises(HomeAssistantError) as err: await hass.services.async_call( - DOMAIN, "start_engine", {"device_id": device_id}, blocking=True + DOMAIN, + "send_poi", + { + "device_id": device_id, + "latitude": 1.2345, + "longitude": 6.7890, + "poi_name": "poi_name", + }, + blocking=True, ) await hass.async_block_till_done() From f732c516009994c3ce74665f467eebaa2de85c50 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 12 Jun 2022 23:27:19 -0400 Subject: [PATCH 1448/3516] Add support for playing latest activity video for Skybell (#73373) * Add support for playing latest activity video * ffmpeg * uno mas * uno mas --- homeassistant/components/skybell/camera.py | 39 ++++++++++++++++--- .../components/skybell/manifest.json | 1 + 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/skybell/camera.py b/homeassistant/components/skybell/camera.py index f531e67f2d0..499f1f3bfca 100644 --- a/homeassistant/components/skybell/camera.py +++ b/homeassistant/components/skybell/camera.py @@ -1,6 +1,8 @@ """Camera support for the Skybell HD Doorbell.""" from __future__ import annotations +from aiohttp import web +from haffmpeg.camera import CameraMjpeg import voluptuous as vol from homeassistant.components.camera import ( @@ -8,9 +10,11 @@ from homeassistant.components.camera import ( Camera, CameraEntityDescription, ) +from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -46,11 +50,14 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Skybell switch.""" - async_add_entities( - SkybellCamera(coordinator, description) - for description in CAMERA_TYPES - for coordinator in hass.data[DOMAIN][entry.entry_id] - ) + entities = [] + for description in CAMERA_TYPES: + for coordinator in hass.data[DOMAIN][entry.entry_id]: + if description.key == "avatar": + entities.append(SkybellCamera(coordinator, description)) + else: + entities.append(SkybellActivityCamera(coordinator, description)) + async_add_entities(entities) class SkybellCamera(SkybellEntity, Camera): @@ -70,3 +77,25 @@ class SkybellCamera(SkybellEntity, Camera): ) -> bytes | None: """Get the latest camera image.""" return self._device.images[self.entity_description.key] + + +class SkybellActivityCamera(SkybellCamera): + """A camera implementation for latest Skybell activity.""" + + async def handle_async_mjpeg_stream( + self, request: web.Request + ) -> web.StreamResponse: + """Generate an HTTP MJPEG stream from the latest recorded activity.""" + stream = CameraMjpeg(get_ffmpeg_manager(self.hass).binary) + url = await self.coordinator.device.async_get_activity_video_url() + await stream.open_camera(url, extra_cmd="-r 210") + + try: + return await async_aiohttp_proxy_stream( + self.hass, + request, + await stream.get_reader(), + get_ffmpeg_manager(self.hass).ffmpeg_stream_content_type, + ) + finally: + await stream.close() diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index 6884fe07df2..c0e66aa5462 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -4,6 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/skybell", "requirements": ["aioskybell==22.6.1"], + "dependencies": ["ffmpeg"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", "loggers": ["aioskybell"] From a05c539abed0f0c4bafb4ea7c0eb04fd1e347cdb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Jun 2022 17:28:53 -1000 Subject: [PATCH 1449/3516] Add support for async_remove_config_entry_device to lutron_caseta (#73382) --- .../components/lutron_caseta/__init__.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index dd64ed4ec6f..d915c2a45cb 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import contextlib +from itertools import chain import logging import ssl @@ -48,6 +49,7 @@ from .device_trigger import ( DEVICE_TYPE_SUBTYPE_MAP_TO_LIP, LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP, ) +from .util import serial_to_unique_id _LOGGER = logging.getLogger(__name__) @@ -358,3 +360,41 @@ class LutronCasetaDeviceUpdatableEntity(LutronCasetaDevice): """Update when forcing a refresh of the device.""" self._device = self._smartbridge.get_device_by_id(self.device_id) _LOGGER.debug(self._device) + + +def _id_to_identifier(lutron_id: str) -> None: + """Convert a lutron caseta identifier to a device identifier.""" + return (DOMAIN, lutron_id) + + +async def async_remove_config_entry_device( + hass: HomeAssistant, entry: config_entries.ConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove lutron_caseta config entry from a device.""" + bridge: Smartbridge = hass.data[DOMAIN][entry.entry_id][BRIDGE_LEAP] + devices = bridge.get_devices() + buttons = bridge.buttons + occupancy_groups = bridge.occupancy_groups + bridge_device = devices[BRIDGE_DEVICE_ID] + bridge_unique_id = serial_to_unique_id(bridge_device["serial"]) + all_identifiers: set[tuple[str, str]] = { + # Base bridge + _id_to_identifier(bridge_unique_id), + # Motion sensors and occupancy groups + *( + _id_to_identifier( + f"occupancygroup_{bridge_unique_id}_{device['occupancy_group_id']}" + ) + for device in occupancy_groups.values() + ), + # Button devices such as pico remotes and all other devices + *( + _id_to_identifier(device["serial"]) + for device in chain(devices.values(), buttons.values()) + ), + } + return not any( + identifier + for identifier in device_entry.identifiers + if identifier in all_identifiers + ) From ad9e1fe16629299dc5c0c1131509aaacf8109e91 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Jun 2022 17:29:44 -1000 Subject: [PATCH 1450/3516] Fix reload race in yeelight when updating the ip address (#73390) --- .../components/yeelight/config_flow.py | 5 ++- tests/components/yeelight/test_config_flow.py | 31 ++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index 8a3a5b41320..440b717fd8c 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -96,7 +96,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_update_entry( entry, data={**entry.data, CONF_HOST: self._discovered_ip} ) - reload = True + reload = entry.state in ( + ConfigEntryState.SETUP_RETRY, + ConfigEntryState.LOADED, + ) if reload: self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 80acaa6f10e..1c19a5e7dfd 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -739,7 +739,7 @@ async def test_discovered_zeroconf(hass): async def test_discovery_updates_ip(hass: HomeAssistant): - """Test discovery updtes ip.""" + """Test discovery updates ip.""" config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "1.2.2.3"}, unique_id=ID ) @@ -761,6 +761,35 @@ async def test_discovery_updates_ip(hass: HomeAssistant): assert config_entry.data[CONF_HOST] == IP_ADDRESS +async def test_discovery_updates_ip_no_reload_setup_in_progress(hass: HomeAssistant): + """Test discovery updates ip does not reload if setup is an an error state.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.2.2.3"}, + unique_id=ID, + state=config_entries.ConfigEntryState.SETUP_ERROR, + ) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb() + with patch( + f"{MODULE}.async_setup_entry", return_value=True + ) as mock_setup_entry, _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): + 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"] == "already_configured" + assert config_entry.data[CONF_HOST] == IP_ADDRESS + assert len(mock_setup_entry.mock_calls) == 0 + + async def test_discovery_adds_missing_ip_id_only(hass: HomeAssistant): """Test discovery adds missing ip.""" config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_ID: ID}) From f0a5dbacf88574102fb4f31d2602f9b0ff962369 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 13 Jun 2022 05:48:17 +0200 Subject: [PATCH 1451/3516] Update pytest to 7.1.2 (#73417) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 5d366a3c350..a3986b8a754 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -24,7 +24,7 @@ pytest-test-groups==1.0.3 pytest-sugar==0.9.4 pytest-timeout==2.1.0 pytest-xdist==2.5.0 -pytest==7.1.1 +pytest==7.1.2 requests_mock==1.9.2 respx==0.19.0 stdlib-list==0.7.0 From 11870092804194af6addce17c8404a0734a30f7b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 12 Jun 2022 22:33:45 -0700 Subject: [PATCH 1452/3516] Bump aiohue to 4.4.2 (#73420) --- 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 d3b492f3b9e..b3dbe4df50a 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==4.4.1"], + "requirements": ["aiohue==4.4.2"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 8e32ca35d62..424a15040a8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -169,7 +169,7 @@ aiohomekit==0.7.17 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.4.1 +aiohue==4.4.2 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 541e36cf252..4f1028446c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -153,7 +153,7 @@ aiohomekit==0.7.17 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.4.1 +aiohue==4.4.2 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 7a422774b6c439fbc79c07e459bb3738f04c4baa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Jun 2022 20:05:08 -1000 Subject: [PATCH 1453/3516] Prevent config entries from being reloaded while they are setting up (#73387) --- homeassistant/config_entries.py | 8 ++++++-- tests/test_config_entries.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 14900153ae4..a8b2752d2aa 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -90,7 +90,7 @@ class ConfigEntryState(Enum): """The config entry has not been loaded""" FAILED_UNLOAD = "failed_unload", False """An error occurred when trying to unload the entry""" - SETUP_IN_PROGRESS = "setup_in_progress", True + SETUP_IN_PROGRESS = "setup_in_progress", False """The config entry is setting up.""" _recoverable: bool @@ -104,7 +104,11 @@ class ConfigEntryState(Enum): @property def recoverable(self) -> bool: - """Get if the state is recoverable.""" + """Get if the state is recoverable. + + If the entry is state is recoverable, unloads + and reloads are allowed. + """ return self._recoverable diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index dbbf542d36c..9372a906f71 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3260,3 +3260,15 @@ async def test_unique_id_update_while_setup_in_progress( assert len(async_reload.mock_calls) == 0 await hass.async_block_till_done() assert entry.state is config_entries.ConfigEntryState.LOADED + + +async def test_disallow_entry_reload_with_setup_in_progresss(hass, manager): + """Test we do not allow reload while the config entry is still setting up.""" + entry = MockConfigEntry( + domain="comp", state=config_entries.ConfigEntryState.SETUP_IN_PROGRESS + ) + entry.add_to_hass(hass) + + with pytest.raises(config_entries.OperationNotAllowed): + assert await manager.async_reload(entry.entry_id) + assert entry.state is config_entries.ConfigEntryState.SETUP_IN_PROGRESS From a0974e0c7297537149985f93544dd6f8ed8cfded Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Mon, 13 Jun 2022 16:05:41 +1000 Subject: [PATCH 1454/3516] Refactor LIFX discovery to prevent duplicate discovery response handling (#72213) * Partially revert #70458 and allow duplicate LIFX discoveries Signed-off-by: Avi Miller * Only process one discovery at a time * Revert all LIFX duplicate/inflight discovery checks Also remember LIFX Switches and do as little processing for them as possible. Signed-off-by: Avi Miller * Bump aiolifx version to support the latest LIFX devices LIFX added 22 new product definitions to their public product list at the end of January and those new products are defined in aiolifx v0.8.1, so bump the dependency version. Also switched to testing for relays instead of maintaining a seperate list of switch product IDs. Fixes #72894. Signed-off-by: Avi Miller * Refactor LIFX discovery to better handle duplicate responses Signed-off-by: Avi Miller * Update clear_inflight_discovery with review suggestion Signed-off-by: Avi Miller * Move the existing entity check to before the asyncio lock Signed-off-by: Avi Miller * Bail out of discovery early and if an entity was created Also ensure that the entity always has a unique ID even if the bulb was not successfully discovered. Signed-off-by: Avi Miller Co-authored-by: J. Nick Koston --- homeassistant/components/lifx/light.py | 195 +++++++++++++++++-------- 1 file changed, 137 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index ea9bbeb91a2..28390e5c02a 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass from datetime import timedelta from functools import partial from ipaddress import IPv4Address @@ -9,6 +10,7 @@ import logging import math import aiolifx as aiolifx_module +from aiolifx.aiolifx import LifxDiscovery, Light import aiolifx_effects as aiolifx_effects_module from awesomeversion import AwesomeVersion import voluptuous as vol @@ -49,7 +51,7 @@ from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddEntitiesCallback, EntityPlatform import homeassistant.helpers.entity_registry as er from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -67,9 +69,9 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=10) -DISCOVERY_INTERVAL = 60 +DISCOVERY_INTERVAL = 10 MESSAGE_TIMEOUT = 1 -MESSAGE_RETRIES = 3 +MESSAGE_RETRIES = 8 UNAVAILABLE_GRACE = 90 FIX_MAC_FW = AwesomeVersion("3.70") @@ -252,19 +254,34 @@ def merge_hsbk(base, change): return [b if c is None else c for b, c in zip(base, change)] +@dataclass +class InFlightDiscovery: + """Represent a LIFX device that is being discovered.""" + + device: Light + lock: asyncio.Lock + + class LIFXManager: """Representation of all known LIFX entities.""" - def __init__(self, hass, platform, config_entry, async_add_entities): + def __init__( + self, + hass: HomeAssistant, + platform: EntityPlatform, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: """Initialize the light.""" - self.entities = {} - self.discoveries_inflight = {} + self.entities: dict[str, LIFXLight] = {} + self.switch_devices: list[str] = [] self.hass = hass self.platform = platform self.config_entry = config_entry self.async_add_entities = async_add_entities self.effects_conductor = aiolifx_effects().Conductor(hass.loop) - self.discoveries = [] + self.discoveries: list[LifxDiscovery] = [] + self.discoveries_inflight: dict[str, InFlightDiscovery] = {} self.cleanup_unsub = self.hass.bus.async_listen( EVENT_HOMEASSISTANT_STOP, self.cleanup ) @@ -376,72 +393,128 @@ class LIFXManager: elif service == SERVICE_EFFECT_STOP: await self.effects_conductor.stop(bulbs) - @callback - def register(self, bulb): - """Allow a single in-flight discovery per bulb.""" - if bulb.mac_addr not in self.discoveries_inflight: - self.discoveries_inflight[bulb.mac_addr] = bulb.ip_addr - _LOGGER.debug("Discovered %s (%s)", bulb.ip_addr, bulb.mac_addr) - self.hass.async_create_task(self.register_bulb(bulb)) - else: - _LOGGER.warning("Duplicate LIFX discovery response ignored") + def clear_inflight_discovery(self, inflight: InFlightDiscovery) -> None: + """Clear in-flight discovery.""" + self.discoveries_inflight.pop(inflight.device.mac_addr, None) - async def register_bulb(self, bulb): - """Handle LIFX bulb registration lifecycle.""" + @callback + def register(self, bulb: Light) -> None: + """Allow a single in-flight discovery per bulb.""" + if bulb.mac_addr in self.switch_devices: + _LOGGER.debug( + "Skipping discovered LIFX Switch at %s (%s)", + bulb.ip_addr, + bulb.mac_addr, + ) + return + + # Try to bail out of discovery as early as possible if bulb.mac_addr in self.entities: entity = self.entities[bulb.mac_addr] entity.registered = True _LOGGER.debug("Reconnected to %s", entity.who) - await entity.update_hass() - else: - _LOGGER.debug("Connecting to %s (%s)", bulb.ip_addr, bulb.mac_addr) + return - # Read initial state + if bulb.mac_addr not in self.discoveries_inflight: + inflight = InFlightDiscovery(bulb, asyncio.Lock()) + self.discoveries_inflight[bulb.mac_addr] = inflight + _LOGGER.debug( + "First discovery response received from %s (%s)", + bulb.ip_addr, + bulb.mac_addr, + ) + else: + _LOGGER.debug( + "Duplicate discovery response received from %s (%s)", + bulb.ip_addr, + bulb.mac_addr, + ) + + self.hass.async_create_task( + self._async_handle_discovery(self.discoveries_inflight[bulb.mac_addr]) + ) + + async def _async_handle_discovery(self, inflight: InFlightDiscovery) -> None: + """Handle LIFX bulb registration lifecycle.""" + + # only allow a single discovery process per discovered device + async with inflight.lock: + + # Bail out if an entity was created by a previous discovery while + # this discovery was waiting for the asyncio lock to release. + if inflight.device.mac_addr in self.entities: + self.clear_inflight_discovery(inflight) + entity: LIFXLight = self.entities[inflight.device.mac_addr] + entity.registered = True + _LOGGER.debug("Reconnected to %s", entity.who) + return + + # Determine the product info so that LIFX Switches + # can be skipped. ack = AwaitAioLIFX().wait - # Get the product info first so that LIFX Switches - # can be ignored. - version_resp = await ack(bulb.get_version) - if version_resp and lifx_features(bulb)["relays"]: + if inflight.device.product is None: + if await ack(inflight.device.get_version) is None: + _LOGGER.debug( + "Failed to discover product information for %s (%s)", + inflight.device.ip_addr, + inflight.device.mac_addr, + ) + self.clear_inflight_discovery(inflight) + return + + if lifx_features(inflight.device)["relays"] is True: _LOGGER.debug( - "Not connecting to LIFX Switch %s (%s)", - str(bulb.mac_addr).replace(":", ""), - bulb.ip_addr, + "Skipping discovered LIFX Switch at %s (%s)", + inflight.device.ip_addr, + inflight.device.mac_addr, ) - return False + self.switch_devices.append(inflight.device.mac_addr) + self.clear_inflight_discovery(inflight) + return - color_resp = await ack(bulb.get_color) + await self._async_process_discovery(inflight=inflight) - if color_resp is None or version_resp is None: - _LOGGER.error("Failed to connect to %s", bulb.ip_addr) - bulb.registered = False - if bulb.mac_addr in self.discoveries_inflight: - self.discoveries_inflight.pop(bulb.mac_addr) - else: - bulb.timeout = MESSAGE_TIMEOUT - bulb.retry_count = MESSAGE_RETRIES - bulb.unregister_timeout = UNAVAILABLE_GRACE + async def _async_process_discovery(self, inflight: InFlightDiscovery) -> None: + """Process discovery of a device.""" + bulb = inflight.device + ack = AwaitAioLIFX().wait - if lifx_features(bulb)["multizone"]: - entity = LIFXStrip(bulb, self.effects_conductor) - elif lifx_features(bulb)["color"]: - entity = LIFXColor(bulb, self.effects_conductor) - else: - entity = LIFXWhite(bulb, self.effects_conductor) + bulb.timeout = MESSAGE_TIMEOUT + bulb.retry_count = MESSAGE_RETRIES + bulb.unregister_timeout = UNAVAILABLE_GRACE - _LOGGER.debug("Connected to %s", entity.who) - self.entities[bulb.mac_addr] = entity - self.discoveries_inflight.pop(bulb.mac_addr, None) - self.async_add_entities([entity], True) + # Read initial state + if bulb.color is None: + if await ack(bulb.get_color) is None: + _LOGGER.debug( + "Failed to determine current state of %s (%s)", + bulb.ip_addr, + bulb.mac_addr, + ) + self.clear_inflight_discovery(inflight) + return + + if lifx_features(bulb)["multizone"]: + entity: LIFXLight = LIFXStrip(bulb.mac_addr, bulb, self.effects_conductor) + elif lifx_features(bulb)["color"]: + entity = LIFXColor(bulb.mac_addr, bulb, self.effects_conductor) + else: + entity = LIFXWhite(bulb.mac_addr, bulb, self.effects_conductor) + + self.entities[bulb.mac_addr] = entity + self.async_add_entities([entity], True) + _LOGGER.debug("Entity created for %s", entity.who) + self.clear_inflight_discovery(inflight) @callback - def unregister(self, bulb): - """Disconnect and unregister non-responsive bulbs.""" + def unregister(self, bulb: Light) -> None: + """Mark unresponsive bulbs as unavailable in Home Assistant.""" if bulb.mac_addr in self.entities: entity = self.entities[bulb.mac_addr] - _LOGGER.debug("Disconnected from %s", entity.who) entity.registered = False entity.async_write_ha_state() + _LOGGER.debug("Disconnected from %s", entity.who) @callback def entity_registry_updated(self, event): @@ -506,8 +579,14 @@ class LIFXLight(LightEntity): _attr_supported_features = LightEntityFeature.TRANSITION | LightEntityFeature.EFFECT - def __init__(self, bulb, effects_conductor): + def __init__( + self, + mac_addr: str, + bulb: Light, + effects_conductor: aiolifx_effects_module.Conductor, + ) -> None: """Initialize the light.""" + self.mac_addr = mac_addr self.bulb = bulb self.effects_conductor = effects_conductor self.registered = True @@ -520,10 +599,10 @@ class LIFXLight(LightEntity): self.bulb.host_firmware_version and AwesomeVersion(self.bulb.host_firmware_version) >= FIX_MAC_FW ): - octets = [int(octet, 16) for octet in self.bulb.mac_addr.split(":")] + octets = [int(octet, 16) for octet in self.mac_addr.split(":")] octets[5] = (octets[5] + 1) % 256 return ":".join(f"{octet:02x}" for octet in octets) - return self.bulb.mac_addr + return self.mac_addr @property def device_info(self) -> DeviceInfo: @@ -552,7 +631,7 @@ class LIFXLight(LightEntity): @property def unique_id(self): """Return a unique ID.""" - return self.bulb.mac_addr + return self.mac_addr @property def name(self): @@ -562,7 +641,7 @@ class LIFXLight(LightEntity): @property def who(self): """Return a string identifying the bulb by name and mac.""" - return f"{self.name} ({self.bulb.mac_addr})" + return f"{self.name} ({self.mac_addr})" @property def min_mireds(self): From b261f0fb41f7612c2cd20e688b05ae6f2e047054 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Mon, 13 Jun 2022 08:36:46 +0100 Subject: [PATCH 1455/3516] Use more specific exception and simplify aurora_abb_powerone (#73338) * Use more specific exception for comms timeout * Remove defered uniqueid assigner now yaml has gone Co-authored-by: Dave T --- .../aurora_abb_powerone/__init__.py | 42 +------------------ .../components/aurora_abb_powerone/sensor.py | 20 ++++----- .../aurora_abb_powerone/test_config_flow.py | 4 +- .../aurora_abb_powerone/test_sensor.py | 6 +-- 4 files changed, 14 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/aurora_abb_powerone/__init__.py b/homeassistant/components/aurora_abb_powerone/__init__.py index 585f3720144..c988121b6bd 100644 --- a/homeassistant/components/aurora_abb_powerone/__init__.py +++ b/homeassistant/components/aurora_abb_powerone/__init__.py @@ -10,15 +10,13 @@ import logging -from aurorapy.client import AuroraError, AuroraSerialClient +from aurorapy.client import AuroraSerialClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ADDRESS, CONF_PORT, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady -from .config_flow import validate_and_connect -from .const import ATTR_SERIAL_NUMBER, DOMAIN +from .const import DOMAIN PLATFORMS = [Platform.SENSOR] @@ -31,42 +29,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: comport = entry.data[CONF_PORT] address = entry.data[CONF_ADDRESS] ser_client = AuroraSerialClient(address, comport, parity="N", timeout=1) - # To handle yaml import attempts in darkness, (re)try connecting only if - # unique_id not yet assigned. - if entry.unique_id is None: - try: - res = await hass.async_add_executor_job( - validate_and_connect, hass, entry.data - ) - except AuroraError as error: - if "No response after" in str(error): - raise ConfigEntryNotReady("No response (could be dark)") from error - _LOGGER.error("Failed to connect to inverter: %s", error) - return False - except OSError as error: - if error.errno == 19: # No such device. - _LOGGER.error("Failed to connect to inverter: no such COM port") - return False - _LOGGER.error("Failed to connect to inverter: %s", error) - return False - else: - # If we got here, the device is now communicating (maybe after - # being in darkness). But there's a small risk that the user has - # configured via the UI since we last attempted the yaml setup, - # which means we'd get a duplicate unique ID. - new_id = res[ATTR_SERIAL_NUMBER] - # Check if this unique_id has already been used - for existing_entry in hass.config_entries.async_entries(DOMAIN): - if existing_entry.unique_id == new_id: - _LOGGER.debug( - "Remove already configured config entry for id %s", new_id - ) - hass.async_create_task( - hass.config_entries.async_remove(entry.entry_id) - ) - return False - hass.config_entries.async_update_entry(entry, unique_id=new_id) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ser_client hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index c06cd7bc5a7..188f1c789a2 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -5,7 +5,7 @@ from collections.abc import Mapping import logging from typing import Any -from aurorapy.client import AuroraError, AuroraSerialClient +from aurorapy.client import AuroraError, AuroraSerialClient, AuroraTimeoutError from homeassistant.components.sensor import ( SensorDeviceClass, @@ -102,22 +102,16 @@ class AuroraSensor(AuroraEntity, SensorEntity): self._attr_native_value = round(energy_wh / 1000, 2) self._attr_available = True + except AuroraTimeoutError: + self._attr_state = None + self._attr_native_value = None + self._attr_available = False + _LOGGER.debug("No response from inverter (could be dark)") 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 - # raises an exception. - # aurorapy (gitlab) pull request merged 29/5/2019. When >0.2.6 is - # released, this could be modified to : - # except AuroraTimeoutError as e: - # Workaround: look at the text of the exception - if "No response after" in str(error): - _LOGGER.debug("No response from inverter (could be dark)") - else: - raise error + raise error finally: if self._attr_available != self.available_prev: if self._attr_available: diff --git a/tests/components/aurora_abb_powerone/test_config_flow.py b/tests/components/aurora_abb_powerone/test_config_flow.py index e53dcf5ab06..b30d6dc5eeb 100644 --- a/tests/components/aurora_abb_powerone/test_config_flow.py +++ b/tests/components/aurora_abb_powerone/test_config_flow.py @@ -2,7 +2,7 @@ from logging import INFO from unittest.mock import patch -from aurorapy.client import AuroraError +from aurorapy.client import AuroraError, AuroraTimeoutError from serial.tools import list_ports_common from homeassistant import config_entries, data_entry_flow, setup @@ -127,7 +127,7 @@ async def test_form_invalid_com_ports(hass): with patch( "aurorapy.client.AuroraSerialClient.connect", - side_effect=AuroraError("...No response after..."), + side_effect=AuroraTimeoutError("...No response after..."), return_value=None, ): result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/aurora_abb_powerone/test_sensor.py b/tests/components/aurora_abb_powerone/test_sensor.py index 0a7b7e33302..f41750ba017 100644 --- a/tests/components/aurora_abb_powerone/test_sensor.py +++ b/tests/components/aurora_abb_powerone/test_sensor.py @@ -2,7 +2,7 @@ from datetime import timedelta from unittest.mock import patch -from aurorapy.client import AuroraError +from aurorapy.client import AuroraError, AuroraTimeoutError from homeassistant.components.aurora_abb_powerone.const import ( ATTR_DEVICE_NAME, @@ -126,7 +126,7 @@ async def test_sensor_dark(hass): # sunset with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( "aurorapy.client.AuroraSerialClient.measure", - side_effect=AuroraError("No response after 10 seconds"), + side_effect=AuroraTimeoutError("No response after 10 seconds"), ): async_fire_time_changed(hass, utcnow + timedelta(seconds=60)) await hass.async_block_till_done() @@ -144,7 +144,7 @@ async def test_sensor_dark(hass): # sunset with patch("aurorapy.client.AuroraSerialClient.connect", return_value=None), patch( "aurorapy.client.AuroraSerialClient.measure", - side_effect=AuroraError("No response after 10 seconds"), + side_effect=AuroraTimeoutError("No response after 10 seconds"), ): async_fire_time_changed(hass, utcnow + timedelta(seconds=60)) await hass.async_block_till_done() From d9f3e9a71c98483f9ed4313051c340e6919ab82b Mon Sep 17 00:00:00 2001 From: kingy444 Date: Mon, 13 Jun 2022 18:26:35 +1000 Subject: [PATCH 1456/3516] Add supported_brands to powerview (#73421) --- .../components/hunterdouglas_powerview/manifest.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index c571056be23..8e2206b2778 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -17,5 +17,8 @@ ], "zeroconf": ["_powerview._tcp.local."], "iot_class": "local_polling", - "loggers": ["aiopvapi"] + "loggers": ["aiopvapi"], + "supported_brands": { + "luxaflex": "Luxaflex" + } } From ca0a185b32f7b7ea928041da82060a4cf473c96b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Jun 2022 11:14:30 +0200 Subject: [PATCH 1457/3516] Enforce config-flow type hints to get options flow (#72831) * Enforce config-flow type hints to get options flow * Add checks on return_type * Fix tests * Add tests * Add BinOp to test * Update tests/pylint/test_enforce_type_hints.py Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> * Update pylint/plugins/hass_enforce_type_hints.py Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> * Add TypeHintMatch property * Update pylint/plugins/hass_enforce_type_hints.py Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- pylint/plugins/hass_enforce_type_hints.py | 41 ++++++++++- tests/pylint/test_enforce_type_hints.py | 85 +++++++++++++++++++++++ 2 files changed, 124 insertions(+), 2 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index d8d3c76a028..8194bb72ca5 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -22,6 +22,7 @@ class TypeHintMatch: function_name: str arg_types: dict[int, str] return_type: list[str] | str | None | object + check_return_type_inheritance: bool = False @dataclass @@ -380,6 +381,14 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { ClassTypeHintMatch( base_class="ConfigFlow", matches=[ + TypeHintMatch( + function_name="async_get_options_flow", + arg_types={ + 0: "ConfigEntry", + }, + return_type="OptionsFlow", + check_return_type_inheritance=True, + ), TypeHintMatch( function_name="async_step_dhcp", arg_types={ @@ -504,6 +513,32 @@ def _is_valid_type( return isinstance(node, nodes.Attribute) and node.attrname == expected_type +def _is_valid_return_type(match: TypeHintMatch, node: nodes.NodeNG) -> bool: + if _is_valid_type(match.return_type, node): + return True + + if isinstance(node, nodes.BinOp): + return _is_valid_return_type(match, node.left) and _is_valid_return_type( + match, node.right + ) + + if ( + match.check_return_type_inheritance + and isinstance(match.return_type, str) + and isinstance(node, nodes.Name) + ): + ancestor: nodes.ClassDef + for infer_node in node.infer(): + if isinstance(infer_node, nodes.ClassDef): + if infer_node.name == match.return_type: + return True + for ancestor in infer_node.ancestors(): + if ancestor.name == match.return_type: + return True + + return False + + def _get_all_annotations(node: nodes.FunctionDef) -> list[nodes.NodeNG | None]: args = node.args annotations: list[nodes.NodeNG | None] = ( @@ -619,8 +654,10 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] ) # Check the return type. - if not _is_valid_type(return_type := match.return_type, node.returns): - self.add_message("hass-return-type", node=node, args=return_type or "None") + if not _is_valid_return_type(match, node.returns): + self.add_message( + "hass-return-type", node=node, args=match.return_type or "None" + ) def register(linter: PyLinter) -> None: diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index fa3d32dcb04..86b06a894d0 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -349,3 +349,88 @@ def test_valid_config_flow_step( with assert_no_messages(linter): type_hint_checker.visit_classdef(class_node) + + +def test_invalid_config_flow_async_get_options_flow( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Ensure invalid hints are rejected for ConfigFlow async_get_options_flow.""" + class_node, func_node, arg_node = astroid.extract_node( + """ + class ConfigFlow(): + pass + + class AxisOptionsFlow(): + pass + + class AxisFlowHandler( #@ + ConfigFlow, domain=AXIS_DOMAIN + ): + def async_get_options_flow( #@ + config_entry #@ + ) -> AxisOptionsFlow: + return AxisOptionsFlow(config_entry) + """, + "homeassistant.components.pylint_test.config_flow", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_adds_messages( + linter, + pylint.testutils.MessageTest( + msg_id="hass-argument-type", + node=arg_node, + args=(1, "ConfigEntry"), + line=12, + col_offset=8, + end_line=12, + end_col_offset=20, + ), + pylint.testutils.MessageTest( + msg_id="hass-return-type", + node=func_node, + args="OptionsFlow", + line=11, + col_offset=4, + end_line=11, + end_col_offset=30, + ), + ): + type_hint_checker.visit_classdef(class_node) + + +def test_valid_config_flow_async_get_options_flow( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Ensure valid hints are accepted for ConfigFlow async_get_options_flow.""" + class_node = astroid.extract_node( + """ + class ConfigFlow(): + pass + + class OptionsFlow(): + pass + + class AxisOptionsFlow(OptionsFlow): + pass + + class OtherOptionsFlow(OptionsFlow): + pass + + class AxisFlowHandler( #@ + ConfigFlow, domain=AXIS_DOMAIN + ): + def async_get_options_flow( + config_entry: ConfigEntry + ) -> AxisOptionsFlow | OtherOptionsFlow | OptionsFlow: + if self.use_other: + return OtherOptionsFlow(config_entry) + return AxisOptionsFlow(config_entry) + + """, + "homeassistant.components.pylint_test.config_flow", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_no_messages(linter): + type_hint_checker.visit_classdef(class_node) From b58970065103cbc2d9937305f986833b33ce1d80 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Jun 2022 13:17:59 +0200 Subject: [PATCH 1458/3516] Add async_get_options_flow type hints (a-m) (#73430) --- homeassistant/components/aemet/config_flow.py | 6 +++++- homeassistant/components/alarmdecoder/config_flow.py | 6 +++++- homeassistant/components/apple_tv/config_flow.py | 6 ++++-- homeassistant/components/aurora/config_flow.py | 8 ++++++-- homeassistant/components/azure_event_hub/config_flow.py | 6 ++++-- homeassistant/components/blink/config_flow.py | 8 ++++++-- homeassistant/components/coinbase/config_flow.py | 6 +++++- homeassistant/components/control4/config_flow.py | 6 +++++- homeassistant/components/demo/config_flow.py | 8 ++++++-- homeassistant/components/dexcom/config_flow.py | 6 +++++- homeassistant/components/doorbird/config_flow.py | 6 +++++- homeassistant/components/ezviz/config_flow.py | 8 +++++--- homeassistant/components/forked_daapd/config_flow.py | 6 ++++-- homeassistant/components/glances/config_flow.py | 8 ++++++-- homeassistant/components/harmony/config_flow.py | 6 +++++- homeassistant/components/hive/config_flow.py | 7 +++++-- homeassistant/components/honeywell/config_flow.py | 6 +++++- homeassistant/components/insteon/config_flow.py | 6 ++++-- .../components/islamic_prayer_times/config_flow.py | 8 ++++++-- homeassistant/components/kmtronic/config_flow.py | 8 ++++++-- homeassistant/components/konnected/config_flow.py | 6 +++++- homeassistant/components/litejet/config_flow.py | 6 ++++-- homeassistant/components/meteo_france/config_flow.py | 8 ++++++-- homeassistant/components/mikrotik/config_flow.py | 8 ++++++-- homeassistant/components/monoprice/config_flow.py | 8 ++++++-- 25 files changed, 129 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/aemet/config_flow.py b/homeassistant/components/aemet/config_flow.py index 6c97ca98cb8..1188df5b94f 100644 --- a/homeassistant/components/aemet/config_flow.py +++ b/homeassistant/components/aemet/config_flow.py @@ -1,4 +1,6 @@ """Config flow for AEMET OpenData.""" +from __future__ import annotations + from aemet_opendata import AEMET import voluptuous as vol @@ -50,7 +52,9 @@ class AemetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) diff --git a/homeassistant/components/alarmdecoder/config_flow.py b/homeassistant/components/alarmdecoder/config_flow.py index 37ff5b97994..52d17e407b7 100644 --- a/homeassistant/components/alarmdecoder/config_flow.py +++ b/homeassistant/components/alarmdecoder/config_flow.py @@ -1,4 +1,6 @@ """Config flow for AlarmDecoder.""" +from __future__ import annotations + import logging from adext import AdExt @@ -58,7 +60,9 @@ class AlarmDecoderFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> AlarmDecoderOptionsFlowHandler: """Get the options flow for AlarmDecoder.""" return AlarmDecoderOptionsFlowHandler(config_entry) diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index 8e8e6006895..1f3133d7e16 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -71,7 +71,9 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> AppleTVOptionsFlow: """Get options flow for this handler.""" return AppleTVOptionsFlow(config_entry) @@ -523,7 +525,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class AppleTVOptionsFlow(config_entries.OptionsFlow): """Handle Apple TV options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize Apple TV options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) diff --git a/homeassistant/components/aurora/config_flow.py b/homeassistant/components/aurora/config_flow.py index fd9ebbf424c..a2331c19ec6 100644 --- a/homeassistant/components/aurora/config_flow.py +++ b/homeassistant/components/aurora/config_flow.py @@ -1,4 +1,6 @@ """Config flow for SpaceX Launches and Starman.""" +from __future__ import annotations + import logging from aiohttp import ClientError @@ -22,7 +24,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) @@ -82,7 +86,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle options flow changes.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/azure_event_hub/config_flow.py b/homeassistant/components/azure_event_hub/config_flow.py index a0dded5f487..26980231dc1 100644 --- a/homeassistant/components/azure_event_hub/config_flow.py +++ b/homeassistant/components/azure_event_hub/config_flow.py @@ -79,7 +79,9 @@ class AEHConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> AEHOptionsFlowHandler: """Get the options flow for this handler.""" return AEHOptionsFlowHandler(config_entry) @@ -170,7 +172,7 @@ class AEHConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class AEHOptionsFlowHandler(config_entries.OptionsFlow): """Handle azure event hub options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize AEH options flow.""" self.config_entry = config_entry self.options = deepcopy(dict(config_entry.options)) diff --git a/homeassistant/components/blink/config_flow.py b/homeassistant/components/blink/config_flow.py index a4bee490fb3..b62c7414f46 100644 --- a/homeassistant/components/blink/config_flow.py +++ b/homeassistant/components/blink/config_flow.py @@ -1,4 +1,6 @@ """Config flow to configure Blink.""" +from __future__ import annotations + import logging from blinkpy.auth import Auth, LoginError, TokenRefreshFailed @@ -49,7 +51,9 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> BlinkOptionsFlowHandler: """Get options flow for this handler.""" return BlinkOptionsFlowHandler(config_entry) @@ -129,7 +133,7 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class BlinkOptionsFlowHandler(config_entries.OptionsFlow): """Handle Blink options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize Blink options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) diff --git a/homeassistant/components/coinbase/config_flow.py b/homeassistant/components/coinbase/config_flow.py index 0687bd3f305..6582acc6549 100644 --- a/homeassistant/components/coinbase/config_flow.py +++ b/homeassistant/components/coinbase/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Coinbase integration.""" +from __future__ import annotations + import logging from coinbase.wallet.client import Client @@ -160,7 +162,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) diff --git a/homeassistant/components/control4/config_flow.py b/homeassistant/components/control4/config_flow.py index 2cf1ca845f7..05fd8a2b7c8 100644 --- a/homeassistant/components/control4/config_flow.py +++ b/homeassistant/components/control4/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Control4 integration.""" +from __future__ import annotations + from asyncio import TimeoutError as asyncioTimeoutError import logging @@ -136,7 +138,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) diff --git a/homeassistant/components/demo/config_flow.py b/homeassistant/components/demo/config_flow.py index f99693bfeb2..e389574c658 100644 --- a/homeassistant/components/demo/config_flow.py +++ b/homeassistant/components/demo/config_flow.py @@ -1,4 +1,6 @@ """Config flow to configure demo component.""" +from __future__ import annotations + import voluptuous as vol from homeassistant import config_entries @@ -21,7 +23,9 @@ class DemoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) @@ -33,7 +37,7 @@ class DemoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) diff --git a/homeassistant/components/dexcom/config_flow.py b/homeassistant/components/dexcom/config_flow.py index 063d14549db..6ccb09881af 100644 --- a/homeassistant/components/dexcom/config_flow.py +++ b/homeassistant/components/dexcom/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Dexcom integration.""" +from __future__ import annotations + from pydexcom import AccountError, Dexcom, SessionError import voluptuous as vol @@ -53,7 +55,9 @@ class DexcomConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> DexcomOptionsFlowHandler: """Get the options flow for this handler.""" return DexcomOptionsFlowHandler(config_entry) diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index cc882b0ed50..678340c0259 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -1,4 +1,6 @@ """Config flow for DoorBird integration.""" +from __future__ import annotations + from http import HTTPStatus from ipaddress import ip_address import logging @@ -144,7 +146,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) diff --git a/homeassistant/components/ezviz/config_flow.py b/homeassistant/components/ezviz/config_flow.py index 36cf2ac456e..6c334291ee5 100644 --- a/homeassistant/components/ezviz/config_flow.py +++ b/homeassistant/components/ezviz/config_flow.py @@ -1,4 +1,6 @@ """Config flow for ezviz.""" +from __future__ import annotations + import logging from pyezviz.client import EzvizClient @@ -12,7 +14,7 @@ from pyezviz.exceptions import ( from pyezviz.test_cam_rtsp import TestRTSPAuth import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, OptionsFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import ( CONF_CUSTOMIZE, CONF_IP_ADDRESS, @@ -164,7 +166,7 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow(config_entry: ConfigEntry) -> EzvizOptionsFlowHandler: """Get the options flow for this handler.""" return EzvizOptionsFlowHandler(config_entry) @@ -311,7 +313,7 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN): class EzvizOptionsFlowHandler(OptionsFlow): """Handle Ezviz client options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/forked_daapd/config_flow.py b/homeassistant/components/forked_daapd/config_flow.py index e3cf6fc7c1d..f9282dfc464 100644 --- a/homeassistant/components/forked_daapd/config_flow.py +++ b/homeassistant/components/forked_daapd/config_flow.py @@ -46,7 +46,7 @@ TEST_CONNECTION_ERROR_DICT = { class ForkedDaapdOptionsFlowHandler(config_entries.OptionsFlow): """Handle a forked-daapd options flow.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize.""" self.config_entry = config_entry @@ -110,7 +110,9 @@ class ForkedDaapdFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> ForkedDaapdOptionsFlowHandler: """Return options flow handler.""" return ForkedDaapdOptionsFlowHandler(config_entry) diff --git a/homeassistant/components/glances/config_flow.py b/homeassistant/components/glances/config_flow.py index 72bfa6dd917..a4a345116eb 100644 --- a/homeassistant/components/glances/config_flow.py +++ b/homeassistant/components/glances/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Glances.""" +from __future__ import annotations + import glances_api import voluptuous as vol @@ -59,7 +61,9 @@ class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> GlancesOptionsFlowHandler: """Get the options flow for this handler.""" return GlancesOptionsFlowHandler(config_entry) @@ -86,7 +90,7 @@ class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class GlancesOptionsFlowHandler(config_entries.OptionsFlow): """Handle Glances client options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize Glances options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index 4dca2192c6b..16101f18cff 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Logitech Harmony Hub integration.""" +from __future__ import annotations + import asyncio import logging from urllib.parse import urlparse @@ -137,7 +139,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) diff --git a/homeassistant/components/hive/config_flow.py b/homeassistant/components/hive/config_flow.py index 90c78aefcbd..16d83dc311d 100644 --- a/homeassistant/components/hive/config_flow.py +++ b/homeassistant/components/hive/config_flow.py @@ -1,4 +1,5 @@ """Config Flow for Hive.""" +from __future__ import annotations from apyhiveapi import Auth from apyhiveapi.helper.hive_exceptions import ( @@ -130,7 +131,9 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> HiveOptionsFlowHandler: """Hive options callback.""" return HiveOptionsFlowHandler(config_entry) @@ -138,7 +141,7 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class HiveOptionsFlowHandler(config_entries.OptionsFlow): """Config flow options for Hive.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize Hive options flow.""" self.hive = None self.config_entry = config_entry diff --git a/homeassistant/components/honeywell/config_flow.py b/homeassistant/components/honeywell/config_flow.py index e6fdd9b54bd..7f7d7d7281a 100644 --- a/homeassistant/components/honeywell/config_flow.py +++ b/homeassistant/components/honeywell/config_flow.py @@ -1,4 +1,6 @@ """Config flow to configure the honeywell integration.""" +from __future__ import annotations + import voluptuous as vol from homeassistant import config_entries @@ -52,7 +54,9 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> HoneywellOptionsFlowHandler: """Options callback for Honeywell.""" return HoneywellOptionsFlowHandler(config_entry) diff --git a/homeassistant/components/insteon/config_flow.py b/homeassistant/components/insteon/config_flow.py index be68a66b70a..d9261a65c32 100644 --- a/homeassistant/components/insteon/config_flow.py +++ b/homeassistant/components/insteon/config_flow.py @@ -119,7 +119,9 @@ class InsteonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> InsteonOptionsFlowHandler: """Define the config flow to handle options.""" return InsteonOptionsFlowHandler(config_entry) @@ -234,7 +236,7 @@ class InsteonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class InsteonOptionsFlowHandler(config_entries.OptionsFlow): """Handle an Insteon options flow.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Init the InsteonOptionsFlowHandler class.""" self.config_entry = config_entry diff --git a/homeassistant/components/islamic_prayer_times/config_flow.py b/homeassistant/components/islamic_prayer_times/config_flow.py index 9963423131c..3379af3860f 100644 --- a/homeassistant/components/islamic_prayer_times/config_flow.py +++ b/homeassistant/components/islamic_prayer_times/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Islamic Prayer Times integration.""" +from __future__ import annotations + import voluptuous as vol from homeassistant import config_entries @@ -14,7 +16,9 @@ class IslamicPrayerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> IslamicPrayerOptionsFlowHandler: """Get the options flow for this handler.""" return IslamicPrayerOptionsFlowHandler(config_entry) @@ -36,7 +40,7 @@ class IslamicPrayerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class IslamicPrayerOptionsFlowHandler(config_entries.OptionsFlow): """Handle Islamic Prayer client options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/kmtronic/config_flow.py b/homeassistant/components/kmtronic/config_flow.py index 9c7d48a3de9..8a00a03e673 100644 --- a/homeassistant/components/kmtronic/config_flow.py +++ b/homeassistant/components/kmtronic/config_flow.py @@ -1,4 +1,6 @@ """Config flow for kmtronic integration.""" +from __future__ import annotations + import logging import aiohttp @@ -52,7 +54,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> KMTronicOptionsFlow: """Get the options flow for this handler.""" return KMTronicOptionsFlow(config_entry) @@ -88,7 +92,7 @@ class InvalidAuth(exceptions.HomeAssistantError): class KMTronicOptionsFlow(config_entries.OptionsFlow): """Handle options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index b6f80035dbe..94a58227c56 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -1,4 +1,6 @@ """Config flow for konnected.io integration.""" +from __future__ import annotations + import asyncio import copy import logging @@ -373,7 +375,9 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Return the Options Flow.""" return OptionsFlowHandler(config_entry) diff --git a/homeassistant/components/litejet/config_flow.py b/homeassistant/components/litejet/config_flow.py index df20337a816..e14eda1b745 100644 --- a/homeassistant/components/litejet/config_flow.py +++ b/homeassistant/components/litejet/config_flow.py @@ -19,7 +19,7 @@ from .const import CONF_DEFAULT_TRANSITION, DOMAIN class LiteJetOptionsFlow(config_entries.OptionsFlow): """Handle LiteJet options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize LiteJet options flow.""" self.config_entry = config_entry @@ -85,6 +85,8 @@ class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> LiteJetOptionsFlow: """Get the options flow for this handler.""" return LiteJetOptionsFlow(config_entry) diff --git a/homeassistant/components/meteo_france/config_flow.py b/homeassistant/components/meteo_france/config_flow.py index 26e2ac1bda2..d05c63ef684 100644 --- a/homeassistant/components/meteo_france/config_flow.py +++ b/homeassistant/components/meteo_france/config_flow.py @@ -1,11 +1,13 @@ """Config flow to configure the Meteo-France integration.""" +from __future__ import annotations + import logging from meteofrance_api.client import MeteoFranceClient import voluptuous as vol from homeassistant import config_entries -from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE from homeassistant.core import callback @@ -25,7 +27,9 @@ class MeteoFranceFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> MeteoFranceOptionsFlowHandler: """Get the options flow for this handler.""" return MeteoFranceOptionsFlowHandler(config_entry) diff --git a/homeassistant/components/mikrotik/config_flow.py b/homeassistant/components/mikrotik/config_flow.py index 11117d22842..36b65b6f2ba 100644 --- a/homeassistant/components/mikrotik/config_flow.py +++ b/homeassistant/components/mikrotik/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Mikrotik.""" +from __future__ import annotations + import voluptuous as vol from homeassistant import config_entries @@ -32,7 +34,9 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> MikrotikOptionsFlowHandler: """Get the options flow for this handler.""" return MikrotikOptionsFlowHandler(config_entry) @@ -78,7 +82,7 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class MikrotikOptionsFlowHandler(config_entries.OptionsFlow): """Handle Mikrotik options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize Mikrotik options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/monoprice/config_flow.py b/homeassistant/components/monoprice/config_flow.py index 1261832c371..4065b003ba3 100644 --- a/homeassistant/components/monoprice/config_flow.py +++ b/homeassistant/components/monoprice/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Monoprice 6-Zone Amplifier integration.""" +from __future__ import annotations + import logging from pymonoprice import get_async_monoprice @@ -90,7 +92,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @core.callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> MonopriceOptionsFlowHandler: """Define the config flow to handle options.""" return MonopriceOptionsFlowHandler(config_entry) @@ -110,7 +114,7 @@ def _key_for_source(index, source, previous_sources): class MonopriceOptionsFlowHandler(config_entries.OptionsFlow): """Handle a Monoprice options flow.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize.""" self.config_entry = config_entry From 42ed0fd47bd6f10e0a47eb12e764501d4d2e5706 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Jun 2022 13:30:41 +0200 Subject: [PATCH 1459/3516] Add async_get_options_flow type hints (n-z) (#73431) --- homeassistant/components/omnilogic/config_flow.py | 8 ++++++-- homeassistant/components/onvif/config_flow.py | 6 ++++-- homeassistant/components/opentherm_gw/config_flow.py | 8 ++++++-- homeassistant/components/openweathermap/config_flow.py | 8 ++++++-- homeassistant/components/plaato/config_flow.py | 4 +++- homeassistant/components/plex/config_flow.py | 8 ++++++-- .../components/pvpc_hourly_pricing/config_flow.py | 8 ++++++-- homeassistant/components/rachio/config_flow.py | 6 +++++- homeassistant/components/risco/config_flow.py | 8 ++++++-- homeassistant/components/roomba/config_flow.py | 7 +++++-- homeassistant/components/screenlogic/config_flow.py | 6 +++++- homeassistant/components/sia/config_flow.py | 6 ++++-- homeassistant/components/somfy_mylink/config_flow.py | 6 +++++- homeassistant/components/subaru/config_flow.py | 6 +++++- homeassistant/components/tado/config_flow.py | 6 +++++- homeassistant/components/totalconnect/config_flow.py | 8 ++++++-- homeassistant/components/transmission/config_flow.py | 8 ++++++-- homeassistant/components/wiffi/config_flow.py | 8 ++++++-- homeassistant/components/ws66i/config_flow.py | 8 ++++++-- homeassistant/components/yeelight/config_flow.py | 8 +++++--- 20 files changed, 106 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/omnilogic/config_flow.py b/homeassistant/components/omnilogic/config_flow.py index d5239760fcc..1635eaa7558 100644 --- a/homeassistant/components/omnilogic/config_flow.py +++ b/homeassistant/components/omnilogic/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Omnilogic integration.""" +from __future__ import annotations + import logging from omnilogic import LoginException, OmniLogic, OmniLogicException @@ -21,7 +23,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) @@ -71,7 +75,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle Omnilogic client options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index 1ee0be18467..d7fb46079d9 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -76,7 +76,9 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OnvifOptionsFlowHandler: """Get the options flow for this handler.""" return OnvifOptionsFlowHandler(config_entry) @@ -262,7 +264,7 @@ class OnvifFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class OnvifOptionsFlowHandler(config_entries.OptionsFlow): """Handle ONVIF options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize ONVIF options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 7c3bc8f8f6b..1d66d6e2069 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -1,4 +1,6 @@ """OpenTherm Gateway config flow.""" +from __future__ import annotations + import asyncio import pyotgw @@ -34,7 +36,9 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OpenThermGwOptionsFlow: """Get the options flow for this handler.""" return OpenThermGwOptionsFlow(config_entry) @@ -111,7 +115,7 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OpenThermGwOptionsFlow(config_entries.OptionsFlow): """Handle opentherm_gw options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize the options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/openweathermap/config_flow.py b/homeassistant/components/openweathermap/config_flow.py index 0b7a3a1a25f..612965bdb2f 100644 --- a/homeassistant/components/openweathermap/config_flow.py +++ b/homeassistant/components/openweathermap/config_flow.py @@ -1,4 +1,6 @@ """Config flow for OpenWeatherMap.""" +from __future__ import annotations + from pyowm import OWM from pyowm.commons.exceptions import APIRequestError, UnauthorizedError import voluptuous as vol @@ -33,7 +35,9 @@ class OpenWeatherMapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OpenWeatherMapOptionsFlow: """Get the options flow for this handler.""" return OpenWeatherMapOptionsFlow(config_entry) @@ -89,7 +93,7 @@ class OpenWeatherMapConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OpenWeatherMapOptionsFlow(config_entries.OptionsFlow): """Handle options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py index dc9533c4be2..637122b1d9c 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Plaato.""" +from __future__ import annotations + from pyplaato.plaato import PlaatoDeviceType import voluptuous as vol @@ -161,7 +163,7 @@ class PlaatoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow(config_entry: ConfigEntry) -> PlaatoOptionsFlowHandler: """Get the options flow for this handler.""" return PlaatoOptionsFlowHandler(config_entry) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index 8da67dafc3d..3489e41364e 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Plex.""" +from __future__ import annotations + import copy import logging @@ -85,7 +87,9 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> PlexOptionsFlowHandler: """Get the options flow for this handler.""" return PlexOptionsFlowHandler(config_entry) @@ -334,7 +338,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class PlexOptionsFlowHandler(config_entries.OptionsFlow): """Handle Plex options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize Plex options flow.""" self.options = copy.deepcopy(dict(config_entry.options)) self.server_id = config_entry.data[CONF_SERVER_IDENTIFIER] diff --git a/homeassistant/components/pvpc_hourly_pricing/config_flow.py b/homeassistant/components/pvpc_hourly_pricing/config_flow.py index 76694d570b5..f5aeb951d33 100644 --- a/homeassistant/components/pvpc_hourly_pricing/config_flow.py +++ b/homeassistant/components/pvpc_hourly_pricing/config_flow.py @@ -1,4 +1,6 @@ """Config flow for pvpc_hourly_pricing.""" +from __future__ import annotations + import voluptuous as vol from homeassistant import config_entries @@ -15,7 +17,9 @@ class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> PVPCOptionsFlowHandler: """Get the options flow for this handler.""" return PVPCOptionsFlowHandler(config_entry) @@ -36,7 +40,7 @@ class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class PVPCOptionsFlowHandler(config_entries.OptionsFlow): """Handle PVPC options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index 9e93dba065e..00f31003ba6 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Rachio integration.""" +from __future__ import annotations + from http import HTTPStatus import logging @@ -92,7 +94,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index c20aa2af287..e2e139a19e0 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Risco integration.""" +from __future__ import annotations + import logging from pyrisco import CannotConnectError, RiscoAPI, UnauthorizedError @@ -67,7 +69,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @core.callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> RiscoOptionsFlowHandler: """Define the config flow to handle options.""" return RiscoOptionsFlowHandler(config_entry) @@ -98,7 +102,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class RiscoOptionsFlowHandler(config_entries.OptionsFlow): """Handle a Risco options flow.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize.""" self.config_entry = config_entry self._data = {**DEFAULT_OPTIONS, **config_entry.options} diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index 7aee875308b..0a1c51ca38c 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure roomba component.""" +from __future__ import annotations import asyncio from functools import partial @@ -78,7 +79,9 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) @@ -267,7 +270,7 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/screenlogic/config_flow.py b/homeassistant/components/screenlogic/config_flow.py index 1aeedfb421d..2b845d453df 100644 --- a/homeassistant/components/screenlogic/config_flow.py +++ b/homeassistant/components/screenlogic/config_flow.py @@ -1,4 +1,6 @@ """Config flow for ScreenLogic.""" +from __future__ import annotations + import logging from screenlogicpy import ScreenLogicError, discovery @@ -69,7 +71,9 @@ class ScreenlogicConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> ScreenLogicOptionsFlowHandler: """Get the options flow for ScreenLogic.""" return ScreenLogicOptionsFlowHandler(config_entry) diff --git a/homeassistant/components/sia/config_flow.py b/homeassistant/components/sia/config_flow.py index a2f7bc744e3..df03882e995 100644 --- a/homeassistant/components/sia/config_flow.py +++ b/homeassistant/components/sia/config_flow.py @@ -94,7 +94,9 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> SIAOptionsFlowHandler: """Get the options flow for this handler.""" return SIAOptionsFlowHandler(config_entry) @@ -170,7 +172,7 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class SIAOptionsFlowHandler(config_entries.OptionsFlow): """Handle SIA options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize SIA options flow.""" self.config_entry = config_entry self.options = deepcopy(dict(config_entry.options)) diff --git a/homeassistant/components/somfy_mylink/config_flow.py b/homeassistant/components/somfy_mylink/config_flow.py index 768d12da45b..de38ac271ce 100644 --- a/homeassistant/components/somfy_mylink/config_flow.py +++ b/homeassistant/components/somfy_mylink/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Somfy MyLink integration.""" +from __future__ import annotations + import asyncio from copy import deepcopy import logging @@ -110,7 +112,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) diff --git a/homeassistant/components/subaru/config_flow.py b/homeassistant/components/subaru/config_flow.py index 788b6f04fd5..79c412c8f85 100644 --- a/homeassistant/components/subaru/config_flow.py +++ b/homeassistant/components/subaru/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Subaru integration.""" +from __future__ import annotations + from datetime import datetime import logging @@ -83,7 +85,9 @@ class SubaruConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index a03b370d0a3..ec195573203 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Tado integration.""" +from __future__ import annotations + import logging from PyTado.interface import Tado @@ -112,7 +114,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) diff --git a/homeassistant/components/totalconnect/config_flow.py b/homeassistant/components/totalconnect/config_flow.py index 49e60b5b46e..057328cffb0 100644 --- a/homeassistant/components/totalconnect/config_flow.py +++ b/homeassistant/components/totalconnect/config_flow.py @@ -1,4 +1,6 @@ """Config flow for the Total Connect component.""" +from __future__ import annotations + from total_connect_client.client import TotalConnectClient from total_connect_client.exceptions import AuthenticationError import voluptuous as vol @@ -166,7 +168,9 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> TotalConnectOptionsFlowHandler: """Get options flow.""" return TotalConnectOptionsFlowHandler(config_entry) @@ -174,7 +178,7 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class TotalConnectOptionsFlowHandler(config_entries.OptionsFlow): """TotalConnect options flow handler.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index ce62475b2eb..b57279ebbbb 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Transmission Bittorent Client.""" +from __future__ import annotations + import voluptuous as vol from homeassistant import config_entries @@ -44,7 +46,9 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> TransmissionOptionsFlowHandler: """Get the options flow for this handler.""" return TransmissionOptionsFlowHandler(config_entry) @@ -87,7 +91,7 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class TransmissionOptionsFlowHandler(config_entries.OptionsFlow): """Handle Transmission client options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize Transmission options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/wiffi/config_flow.py b/homeassistant/components/wiffi/config_flow.py index 5087915181e..d6da03c2134 100644 --- a/homeassistant/components/wiffi/config_flow.py +++ b/homeassistant/components/wiffi/config_flow.py @@ -2,6 +2,8 @@ Used by UI to setup a wiffi integration. """ +from __future__ import annotations + import errno import voluptuous as vol @@ -21,7 +23,9 @@ class WiffiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Create Wiffi server setup option flow.""" return OptionsFlowHandler(config_entry) @@ -66,7 +70,7 @@ class WiffiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Wiffi server setup option flow.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry diff --git a/homeassistant/components/ws66i/config_flow.py b/homeassistant/components/ws66i/config_flow.py index b84872da036..a7deb74eb3e 100644 --- a/homeassistant/components/ws66i/config_flow.py +++ b/homeassistant/components/ws66i/config_flow.py @@ -1,4 +1,6 @@ """Config flow for WS66i 6-Zone Amplifier integration.""" +from __future__ import annotations + import logging from typing import Any @@ -114,7 +116,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @core.callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> Ws66iOptionsFlowHandler: """Define the config flow to handle options.""" return Ws66iOptionsFlowHandler(config_entry) @@ -131,7 +135,7 @@ def _key_for_source(index, source, previous_sources): class Ws66iOptionsFlowHandler(config_entries.OptionsFlow): """Handle a WS66i options flow.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize.""" self.config_entry = config_entry diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index 440b717fd8c..5a457b7da95 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Yeelight integration.""" +from __future__ import annotations + import asyncio import logging from urllib.parse import urlparse @@ -10,7 +12,7 @@ from yeelight.main import get_known_models from homeassistant import config_entries, exceptions from homeassistant.components import dhcp, ssdp, zeroconf -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult @@ -46,7 +48,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler: """Return the options flow.""" return OptionsFlowHandler(config_entry) @@ -276,7 +278,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for Yeelight.""" - def __init__(self, config_entry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize the option flow.""" self._config_entry = config_entry From f846cd033fcbd7f4a155d022e24e2f9a14b72b76 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Jun 2022 13:35:50 +0200 Subject: [PATCH 1460/3516] Add async_get_options_flow type hints (mqtt) (#73434) --- homeassistant/components/mqtt/config_flow.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 822ae712573..a8d0957921d 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -1,4 +1,6 @@ """Config flow for MQTT.""" +from __future__ import annotations + from collections import OrderedDict import queue @@ -15,6 +17,7 @@ from homeassistant.const import ( CONF_PROTOCOL, CONF_USERNAME, ) +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from .client import MqttClientSetup @@ -45,7 +48,10 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _hassio_discovery = None @staticmethod - def async_get_options_flow(config_entry): + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> MQTTOptionsFlowHandler: """Get the options flow for this handler.""" return MQTTOptionsFlowHandler(config_entry) @@ -136,10 +142,10 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class MQTTOptionsFlowHandler(config_entries.OptionsFlow): """Handle MQTT options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize MQTT options flow.""" self.config_entry = config_entry - self.broker_config = {} + self.broker_config: dict[str, str | int] = {} self.options = dict(config_entry.options) async def async_step_init(self, user_input=None): From 48e3d68b531dfd49107b850368932e9d0be1241c Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 13 Jun 2022 13:38:53 +0200 Subject: [PATCH 1461/3516] Clean up MQTT platform entry setup at discovery (#72371) * Setup MQTT discovery with entry setup * Wait for entry setup in test * flake --- homeassistant/components/mqtt/__init__.py | 40 +++++++++++++--------- homeassistant/components/mqtt/discovery.py | 24 ------------- tests/components/mqtt/test_init.py | 5 ++- 3 files changed, 25 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index f9a6ebd025f..417d1400758 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -236,9 +236,7 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) - await _async_setup_discovery(hass, mqtt_client.conf, entry) -async def async_setup_entry( # noqa: C901 - hass: HomeAssistant, entry: ConfigEntry -) -> bool: +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load a config entry.""" # Merge basic configuration, and add missing defaults for basic options _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) @@ -386,25 +384,33 @@ async def async_setup_entry( # noqa: C901 hass.data[DATA_MQTT_UPDATED_CONFIG] = config_yaml.get(DOMAIN, {}) async_dispatcher_send(hass, MQTT_RELOADED) - async def async_forward_entry_setup(): - """Forward the config entry setup to the platforms.""" - async with hass.data[DATA_CONFIG_ENTRY_LOCK]: - for component in PLATFORMS: - config_entries_key = f"{component}.mqtt" - if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: - hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) - await hass.config_entries.async_forward_entry_setup( - entry, component - ) + async def async_forward_entry_setup_and_setup_discovery(config_entry): + """Forward the config entry setup to the platforms and set up discovery.""" + # Local import to avoid circular dependencies + # pylint: disable-next=import-outside-toplevel + from . import device_automation, tag + + await asyncio.gather( + *( + [ + device_automation.async_setup_entry(hass, config_entry), + tag.async_setup_entry(hass, config_entry), + ] + + [ + hass.config_entries.async_forward_entry_setup(entry, component) + for component in PLATFORMS + ] + ) + ) + # Setup discovery + if conf.get(CONF_DISCOVERY): + await _async_setup_discovery(hass, conf, entry) # Setup reload service after all platforms have loaded entry.async_on_unload( hass.bus.async_listen("event_mqtt_reloaded", _async_reload_platforms) ) - hass.async_create_task(async_forward_entry_setup()) - - if conf.get(CONF_DISCOVERY): - await _async_setup_discovery(hass, conf, entry) + hass.async_create_task(async_forward_entry_setup_and_setup_discovery(entry)) if DATA_MQTT_RELOAD_NEEDED in hass.data: hass.data.pop(DATA_MQTT_RELOAD_NEEDED) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 3bbf8ef61ad..04dd2d7917f 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -27,8 +27,6 @@ from .const import ( ATTR_DISCOVERY_TOPIC, CONF_AVAILABILITY, CONF_TOPIC, - CONFIG_ENTRY_IS_SETUP, - DATA_CONFIG_ENTRY_LOCK, DOMAIN, ) @@ -227,28 +225,6 @@ async def async_start( # noqa: C901 # Add component _LOGGER.info("Found new component: %s %s", component, discovery_id) hass.data[ALREADY_DISCOVERED][discovery_hash] = None - - config_entries_key = f"{component}.mqtt" - async with hass.data[DATA_CONFIG_ENTRY_LOCK]: - if config_entries_key not in hass.data[CONFIG_ENTRY_IS_SETUP]: - if component == "device_automation": - # Local import to avoid circular dependencies - # pylint: disable-next=import-outside-toplevel - from . import device_automation - - await device_automation.async_setup_entry(hass, config_entry) - elif component == "tag": - # Local import to avoid circular dependencies - # pylint: disable-next=import-outside-toplevel - from . import tag - - await tag.async_setup_entry(hass, config_entry) - else: - await hass.config_entries.async_forward_entry_setup( - config_entry, component - ) - hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) - async_dispatcher_send( hass, MQTT_DISCOVERY_NEW.format(component, "mqtt"), payload ) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 861b50d2f71..8159933a9a4 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1780,13 +1780,12 @@ async def test_setup_entry_with_config_override( # mqtt present in yaml config assert await async_setup_component(hass, mqtt.DOMAIN, {}) await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() # User sets up a config entry entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) entry.add_to_hass(hass) - with patch("homeassistant.components.mqtt.PLATFORMS", []): - assert await hass.config_entries.async_setup(entry.entry_id) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() # Discover a device to verify the entry was setup correctly async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) From 657e7f9a4c3d05bf81ddc873ca7a30c1e711e175 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 13 Jun 2022 13:44:12 +0200 Subject: [PATCH 1462/3516] Simplify MQTT test for setup manual mqtt item from yaml (#72916) simplify test setup manual mqtt item from yaml --- .../mqtt/test_alarm_control_panel.py | 6 ++-- tests/components/mqtt/test_binary_sensor.py | 6 ++-- tests/components/mqtt/test_button.py | 6 ++-- tests/components/mqtt/test_camera.py | 6 ++-- tests/components/mqtt/test_climate.py | 6 ++-- tests/components/mqtt/test_common.py | 8 +---- tests/components/mqtt/test_cover.py | 6 ++-- tests/components/mqtt/test_device_tracker.py | 8 ++--- tests/components/mqtt/test_fan.py | 6 ++-- tests/components/mqtt/test_humidifier.py | 6 ++-- tests/components/mqtt/test_init.py | 30 ++++--------------- tests/components/mqtt/test_legacy_vacuum.py | 6 ++-- tests/components/mqtt/test_light.py | 6 ++-- tests/components/mqtt/test_light_json.py | 6 ++-- tests/components/mqtt/test_light_template.py | 6 ++-- tests/components/mqtt/test_lock.py | 4 +-- tests/components/mqtt/test_number.py | 6 ++-- tests/components/mqtt/test_scene.py | 6 ++-- tests/components/mqtt/test_select.py | 6 ++-- tests/components/mqtt/test_sensor.py | 6 ++-- tests/components/mqtt/test_siren.py | 6 ++-- tests/components/mqtt/test_state_vacuum.py | 6 ++-- tests/components/mqtt/test_switch.py | 6 ++-- 23 files changed, 48 insertions(+), 116 deletions(-) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 2b013ddf8dd..4ea41aa4c64 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -953,13 +953,11 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) -async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): +async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = alarm_control_panel.DOMAIN config = copy.deepcopy(DEFAULT_CONFIG[platform]) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index ebb1d78138f..0561e9fe056 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -1063,13 +1063,11 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( assert "Skip state recovery after reload for binary_sensor.test3" in caplog.text -async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): +async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = binary_sensor.DOMAIN config = copy.deepcopy(DEFAULT_CONFIG[platform]) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 35deccf2bfe..9fa16aa33bb 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -462,13 +462,11 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) -async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): +async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = button.DOMAIN config = copy.deepcopy(DEFAULT_CONFIG[platform]) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 54d829ce9f9..f219178859b 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -330,13 +330,11 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) -async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): +async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = camera.DOMAIN config = copy.deepcopy(DEFAULT_CONFIG[platform]) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 77843cee777..35eec9aa1ff 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -1866,13 +1866,11 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) -async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): +async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = CLIMATE_DOMAIN config = copy.deepcopy(DEFAULT_CONFIG[platform]) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 24482129f3d..92feaa3c109 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -1790,13 +1790,7 @@ async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): assert hass.states.get(f"{domain}.test_new_3") -async def help_test_setup_manual_entity_from_yaml( - hass, - caplog, - tmp_path, - platform, - config, -): +async def help_test_setup_manual_entity_from_yaml(hass, platform, config): """Help to test setup from yaml through configuration entry.""" config_structure = {mqtt.DOMAIN: {platform: config}} diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 31e30ebf11a..c2406aef9b4 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -3348,13 +3348,11 @@ async def test_encoding_subscribable_topics( ) -async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): +async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = cover.DOMAIN config = copy.deepcopy(DEFAULT_CONFIG[platform]) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 34042105af2..c731280f575 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -244,9 +244,7 @@ async def test_legacy_matching_source_type( assert hass.states.get(entity_id).attributes["source_type"] == SOURCE_TYPE_BLUETOOTH -async def test_setup_with_modern_schema( - hass, caplog, tmp_path, mock_device_tracker_conf -): +async def test_setup_with_modern_schema(hass, mock_device_tracker_conf): """Test setup using the modern schema.""" dev_id = "jan" entity_id = f"{DOMAIN}.{dev_id}" @@ -255,8 +253,6 @@ async def test_setup_with_modern_schema( hass.config.components = {"zone"} config = {"name": dev_id, "state_topic": topic} - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, DOMAIN, config - ) + await help_test_setup_manual_entity_from_yaml(hass, DOMAIN, config) assert hass.states.get(entity_id) is not None diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 145edf5ac7d..6a4920ef45a 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -1894,13 +1894,11 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) -async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): +async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = fan.DOMAIN config = copy.deepcopy(DEFAULT_CONFIG[platform]) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index ea9a6edf0e3..cbbf69e2947 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -1267,13 +1267,11 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) -async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): +async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = humidifier.DOMAIN config = copy.deepcopy(DEFAULT_CONFIG[platform]) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 8159933a9a4..474bc03f876 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1362,48 +1362,30 @@ async def test_setup_override_configuration(hass, caplog, tmp_path): assert calls_username_password_set[0][1] == "somepassword" -async def test_setup_manual_mqtt_with_platform_key(hass, caplog, tmp_path): +async def test_setup_manual_mqtt_with_platform_key(hass, caplog): """Test set up a manual MQTT item with a platform key.""" config = {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} - await help_test_setup_manual_entity_from_yaml( - hass, - caplog, - tmp_path, - "light", - config, - ) + await help_test_setup_manual_entity_from_yaml(hass, "light", config) assert ( "Invalid config for [light]: [platform] is an invalid option for [light]. " "Check: light->platform. (See ?, line ?)" in caplog.text ) -async def test_setup_manual_mqtt_with_invalid_config(hass, caplog, tmp_path): +async def test_setup_manual_mqtt_with_invalid_config(hass, caplog): """Test set up a manual MQTT item with an invalid config.""" config = {"name": "test"} - await help_test_setup_manual_entity_from_yaml( - hass, - caplog, - tmp_path, - "light", - config, - ) + await help_test_setup_manual_entity_from_yaml(hass, "light", config) assert ( "Invalid config for [light]: required key not provided @ data['command_topic']." " Got None. (See ?, line ?)" in caplog.text ) -async def test_setup_manual_mqtt_empty_platform(hass, caplog, tmp_path): +async def test_setup_manual_mqtt_empty_platform(hass, caplog): """Test set up a manual MQTT platform without items.""" config = None - await help_test_setup_manual_entity_from_yaml( - hass, - caplog, - tmp_path, - "light", - config, - ) + await help_test_setup_manual_entity_from_yaml(hass, "light", config) assert "voluptuous.error.MultipleInvalid" not in caplog.text diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 43b6e839904..0371153d750 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -966,13 +966,11 @@ async def test_encoding_subscribable_topics( ) -async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): +async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = vacuum.DOMAIN config = deepcopy(DEFAULT_CONFIG) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 08d5432ba27..7ca10683ed7 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -3787,13 +3787,11 @@ async def test_sending_mqtt_effect_command_with_template( assert state.attributes.get("effect") == "colorloop" -async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): +async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = light.DOMAIN config = copy.deepcopy(DEFAULT_CONFIG[platform]) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index c24c5e87937..3e1eb10d717 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -2146,13 +2146,11 @@ async def test_encoding_subscribable_topics( ) -async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): +async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = light.DOMAIN config = copy.deepcopy(DEFAULT_CONFIG[platform]) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 0d4b95e9152..fa2ef113976 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -1250,13 +1250,11 @@ async def test_encoding_subscribable_topics( ) -async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): +async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = light.DOMAIN config = copy.deepcopy(DEFAULT_CONFIG[platform]) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index b48557efc8f..0353702152f 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -738,7 +738,5 @@ async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): config = copy.deepcopy(DEFAULT_CONFIG[platform]) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index a49b6de198d..648499b8751 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -784,13 +784,11 @@ async def test_encoding_subscribable_topics( ) -async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): +async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = number.DOMAIN config = copy.deepcopy(DEFAULT_CONFIG[platform]) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index eb5cb94df2d..305389ba72d 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -222,13 +222,11 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): await help_test_reloadable_late(hass, caplog, tmp_path, domain, config) -async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): +async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = scene.DOMAIN config = copy.deepcopy(DEFAULT_CONFIG[platform]) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index 888dd301018..5b02c4f3a31 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -667,13 +667,11 @@ async def test_encoding_subscribable_topics( ) -async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): +async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = select.DOMAIN config = copy.deepcopy(DEFAULT_CONFIG[platform]) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 7081ae45993..0cc275c9d1f 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -1197,13 +1197,11 @@ async def test_encoding_subscribable_topics( ) -async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): +async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = sensor.DOMAIN config = copy.deepcopy(DEFAULT_CONFIG[platform]) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index 2db2060c133..1dce382421e 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -959,13 +959,11 @@ async def test_encoding_subscribable_topics( ) -async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): +async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = siren.DOMAIN config = copy.deepcopy(DEFAULT_CONFIG[platform]) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index f20a881dda1..64545d2c140 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -692,13 +692,11 @@ async def test_encoding_subscribable_topics( ) -async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): +async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = vacuum.DOMAIN config = deepcopy(DEFAULT_CONFIG) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index b217bf40c22..1ed8db34d8d 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -648,13 +648,11 @@ async def test_encoding_subscribable_topics( ) -async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): +async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = switch.DOMAIN config = copy.deepcopy(DEFAULT_CONFIG[platform]) config["name"] = "test" del config["platform"] - await help_test_setup_manual_entity_from_yaml( - hass, caplog, tmp_path, platform, config - ) + await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None From c195d462cc17db013c577771bc5911664349b4f3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Jun 2022 13:53:47 +0200 Subject: [PATCH 1463/3516] Add async_get_options_flow type hints (hvv) (#73433) --- .../components/hvv_departures/config_flow.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hvv_departures/config_flow.py b/homeassistant/components/hvv_departures/config_flow.py index 488579af12b..d96ab359dda 100644 --- a/homeassistant/components/hvv_departures/config_flow.py +++ b/homeassistant/components/hvv_departures/config_flow.py @@ -1,5 +1,8 @@ """Config flow for HVV integration.""" +from __future__ import annotations + import logging +from typing import Any from pygti.auth import GTI_DEFAULT_HOST from pygti.exceptions import CannotConnect, InvalidAuth @@ -122,7 +125,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: """Get options flow.""" return OptionsFlowHandler(config_entry) @@ -130,12 +135,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): """Options flow handler.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize HVV Departures options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) - self.departure_filters = {} - self.hub = None + self.departure_filters: dict[str, Any] = {} async def async_step_init(self, user_input=None): """Manage the options.""" @@ -143,10 +147,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if not self.departure_filters: departure_list = {} - self.hub = self.hass.data[DOMAIN][self.config_entry.entry_id] + hub: GTIHub = self.hass.data[DOMAIN][self.config_entry.entry_id] try: - departure_list = await self.hub.gti.departureList( + departure_list = await hub.gti.departureList( { "station": self.config_entry.data[CONF_STATION], "time": {"date": "heute", "time": "jetzt"}, From b84e844c7679f48fe09fea68c228f2313eae2724 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 13 Jun 2022 13:55:38 +0200 Subject: [PATCH 1464/3516] Add async_get_options_flow type hints (cast) (#73432) --- homeassistant/components/cast/config_flow.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py index aaf8d5b9c6c..fc657fd2422 100644 --- a/homeassistant/components/cast/config_flow.py +++ b/homeassistant/components/cast/config_flow.py @@ -1,8 +1,13 @@ """Config flow for Cast.""" +from __future__ import annotations + +from typing import Any + import voluptuous as vol from homeassistant import config_entries from homeassistant.components import zeroconf +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv @@ -25,7 +30,10 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._wanted_uuid = set() @staticmethod - def async_get_options_flow(config_entry): + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> CastOptionsFlowHandler: """Get the options flow for this handler.""" return CastOptionsFlowHandler(config_entry) @@ -110,10 +118,10 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): class CastOptionsFlowHandler(config_entries.OptionsFlow): """Handle Google Cast options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize Google Cast options flow.""" self.config_entry = config_entry - self.updated_config = {} + self.updated_config: dict[str, Any] = {} async def async_step_init(self, user_input=None): """Manage the Google Cast options.""" From dca4d3cd61d7f872621ee4021450cc6a0fbd930e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Jun 2022 08:44:46 -1000 Subject: [PATCH 1465/3516] Significantly improve yaml load times when the C loader is available (#73337) --- .github/workflows/ci.yaml | 1 + .github/workflows/wheels.yml | 2 +- Dockerfile.dev | 1 + homeassistant/scripts/check_config.py | 6 +- homeassistant/util/yaml/loader.py | 172 +++++++++++++----- .../fixtures/configuration_invalid.notyaml | 2 +- tests/test_config.py | 18 +- tests/util/yaml/test_init.py | 66 +++++-- 8 files changed, 190 insertions(+), 78 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fb46ab63202..27a761872d9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -28,6 +28,7 @@ env: PIP_CACHE: /tmp/pip-cache SQLALCHEMY_WARN_20: 1 PYTHONASYNCIODEBUG: 1 + HASS_CI: 1 concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 6019e533530..605820efb33 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -158,7 +158,7 @@ jobs: wheels-key: ${{ secrets.WHEELS_KEY }} wheels-user: wheels env-file: true - apk: "build-base;cmake;git;linux-headers;libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;cargo" + apk: "build-base;cmake;git;linux-headers;libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;cargo" pip: "Cython;numpy;scikit-build" skip-binary: aiohttp,grpcio constraints: "homeassistant/package_constraints.txt" diff --git a/Dockerfile.dev b/Dockerfile.dev index 322c63f53dd..0559ebb43cd 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -18,6 +18,7 @@ RUN \ libavfilter-dev \ libpcap-dev \ libturbojpeg0 \ + libyaml-dev \ libxml2 \ git \ cmake \ diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 6182b909f74..221dafa729c 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -191,7 +191,7 @@ def check(config_dir, secrets=False): if secrets: # Ensure !secrets point to the patched function - yaml_loader.SafeLineLoader.add_constructor("!secret", yaml_loader.secret_yaml) + yaml_loader.add_constructor("!secret", yaml_loader.secret_yaml) def secrets_proxy(*args): secrets = Secrets(*args) @@ -219,9 +219,7 @@ def check(config_dir, secrets=False): pat.stop() if secrets: # Ensure !secrets point to the original function - yaml_loader.SafeLineLoader.add_constructor( - "!secret", yaml_loader.secret_yaml - ) + yaml_loader.add_constructor("!secret", yaml_loader.secret_yaml) return res diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 3507ab96286..e3add3a7c44 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections import OrderedDict from collections.abc import Iterator import fnmatch +from io import StringIO import logging import os from pathlib import Path @@ -11,6 +12,14 @@ from typing import Any, TextIO, TypeVar, Union, overload import yaml +try: + from yaml import CSafeLoader as FastestAvailableSafeLoader + + HAS_C_LOADER = True +except ImportError: + HAS_C_LOADER = False + from yaml import SafeLoader as FastestAvailableSafeLoader # type: ignore[misc] + from homeassistant.exceptions import HomeAssistantError from .const import SECRET_YAML @@ -88,6 +97,30 @@ class Secrets: return secrets +class SafeLoader(FastestAvailableSafeLoader): + """The fastest available safe loader.""" + + def __init__(self, stream: Any, secrets: Secrets | None = None) -> None: + """Initialize a safe line loader.""" + self.stream = stream + if isinstance(stream, str): + self.name = "" + elif isinstance(stream, bytes): + self.name = "" + else: + self.name = getattr(stream, "name", "") + super().__init__(stream) + self.secrets = secrets + + def get_name(self) -> str: + """Get the name of the loader.""" + return self.name + + def get_stream_name(self) -> str: + """Get the name of the stream.""" + return self.stream.name or "" + + class SafeLineLoader(yaml.SafeLoader): """Loader class that keeps track of line numbers.""" @@ -103,6 +136,17 @@ class SafeLineLoader(yaml.SafeLoader): node.__line__ = last_line + 1 # type: ignore[attr-defined] return node + def get_name(self) -> str: + """Get the name of the loader.""" + return self.name + + def get_stream_name(self) -> str: + """Get the name of the stream.""" + return self.stream.name or "" + + +LoaderType = Union[SafeLineLoader, SafeLoader] + def load_yaml(fname: str, secrets: Secrets | None = None) -> JSON_TYPE: """Load a YAML file.""" @@ -114,60 +158,90 @@ def load_yaml(fname: str, secrets: Secrets | None = None) -> JSON_TYPE: raise HomeAssistantError(exc) from exc -def parse_yaml(content: str | TextIO, secrets: Secrets | None = None) -> JSON_TYPE: - """Load a YAML file.""" +def parse_yaml( + content: str | TextIO | StringIO, secrets: Secrets | None = None +) -> JSON_TYPE: + """Parse YAML with the fastest available loader.""" + if not HAS_C_LOADER: + return _parse_yaml_pure_python(content, secrets) try: - # If configuration file is empty YAML returns None - # We convert that to an empty dict - return ( - yaml.load(content, Loader=lambda stream: SafeLineLoader(stream, secrets)) - or OrderedDict() - ) + return _parse_yaml(SafeLoader, content, secrets) + except yaml.YAMLError: + # Loading failed, so we now load with the slow line loader + # since the C one will not give us line numbers + if isinstance(content, (StringIO, TextIO)): + # Rewind the stream so we can try again + content.seek(0, 0) + return _parse_yaml_pure_python(content, secrets) + + +def _parse_yaml_pure_python( + content: str | TextIO | StringIO, secrets: Secrets | None = None +) -> JSON_TYPE: + """Parse YAML with the pure python loader (this is very slow).""" + try: + return _parse_yaml(SafeLineLoader, content, secrets) except yaml.YAMLError as exc: _LOGGER.error(str(exc)) raise HomeAssistantError(exc) from exc +def _parse_yaml( + loader: type[SafeLoader] | type[SafeLineLoader], + content: str | TextIO, + secrets: Secrets | None = None, +) -> JSON_TYPE: + """Load a YAML file.""" + # If configuration file is empty YAML returns None + # We convert that to an empty dict + return ( + yaml.load(content, Loader=lambda stream: loader(stream, secrets)) + or OrderedDict() + ) + + @overload def _add_reference( - obj: list | NodeListClass, loader: SafeLineLoader, node: yaml.nodes.Node + obj: list | NodeListClass, + loader: LoaderType, + node: yaml.nodes.Node, ) -> NodeListClass: ... @overload def _add_reference( - obj: str | NodeStrClass, loader: SafeLineLoader, node: yaml.nodes.Node + obj: str | NodeStrClass, + loader: LoaderType, + node: yaml.nodes.Node, ) -> NodeStrClass: ... @overload -def _add_reference( - obj: _DictT, loader: SafeLineLoader, node: yaml.nodes.Node -) -> _DictT: +def _add_reference(obj: _DictT, loader: LoaderType, node: yaml.nodes.Node) -> _DictT: ... -def _add_reference(obj, loader: SafeLineLoader, node: yaml.nodes.Node): # type: ignore[no-untyped-def] +def _add_reference(obj, loader: LoaderType, node: yaml.nodes.Node): # type: ignore[no-untyped-def] """Add file reference information to an object.""" if isinstance(obj, list): obj = NodeListClass(obj) if isinstance(obj, str): obj = NodeStrClass(obj) - setattr(obj, "__config_file__", loader.name) + setattr(obj, "__config_file__", loader.get_name()) setattr(obj, "__line__", node.start_mark.line) return obj -def _include_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE: +def _include_yaml(loader: LoaderType, node: yaml.nodes.Node) -> JSON_TYPE: """Load another YAML file and embeds it using the !include tag. Example: device_tracker: !include device_tracker.yaml """ - fname = os.path.join(os.path.dirname(loader.name), node.value) + fname = os.path.join(os.path.dirname(loader.get_name()), node.value) try: return _add_reference(load_yaml(fname, loader.secrets), loader, node) except FileNotFoundError as exc: @@ -191,12 +265,10 @@ def _find_files(directory: str, pattern: str) -> Iterator[str]: yield filename -def _include_dir_named_yaml( - loader: SafeLineLoader, node: yaml.nodes.Node -) -> OrderedDict: +def _include_dir_named_yaml(loader: LoaderType, node: yaml.nodes.Node) -> OrderedDict: """Load multiple files from directory as a dictionary.""" mapping: OrderedDict = OrderedDict() - loc = os.path.join(os.path.dirname(loader.name), node.value) + loc = os.path.join(os.path.dirname(loader.get_name()), node.value) for fname in _find_files(loc, "*.yaml"): filename = os.path.splitext(os.path.basename(fname))[0] if os.path.basename(fname) == SECRET_YAML: @@ -206,11 +278,11 @@ def _include_dir_named_yaml( def _include_dir_merge_named_yaml( - loader: SafeLineLoader, node: yaml.nodes.Node + loader: LoaderType, node: yaml.nodes.Node ) -> OrderedDict: """Load multiple files from directory as a merged dictionary.""" mapping: OrderedDict = OrderedDict() - loc = os.path.join(os.path.dirname(loader.name), node.value) + loc = os.path.join(os.path.dirname(loader.get_name()), node.value) for fname in _find_files(loc, "*.yaml"): if os.path.basename(fname) == SECRET_YAML: continue @@ -221,10 +293,10 @@ def _include_dir_merge_named_yaml( def _include_dir_list_yaml( - loader: SafeLineLoader, node: yaml.nodes.Node + loader: LoaderType, node: yaml.nodes.Node ) -> list[JSON_TYPE]: """Load multiple files from directory as a list.""" - loc = os.path.join(os.path.dirname(loader.name), node.value) + loc = os.path.join(os.path.dirname(loader.get_name()), node.value) return [ load_yaml(f, loader.secrets) for f in _find_files(loc, "*.yaml") @@ -233,10 +305,10 @@ def _include_dir_list_yaml( def _include_dir_merge_list_yaml( - loader: SafeLineLoader, node: yaml.nodes.Node + loader: LoaderType, node: yaml.nodes.Node ) -> JSON_TYPE: """Load multiple files from directory as a merged list.""" - loc: str = os.path.join(os.path.dirname(loader.name), node.value) + loc: str = os.path.join(os.path.dirname(loader.get_name()), node.value) merged_list: list[JSON_TYPE] = [] for fname in _find_files(loc, "*.yaml"): if os.path.basename(fname) == SECRET_YAML: @@ -247,7 +319,7 @@ def _include_dir_merge_list_yaml( return _add_reference(merged_list, loader, node) -def _ordered_dict(loader: SafeLineLoader, node: yaml.nodes.MappingNode) -> OrderedDict: +def _ordered_dict(loader: LoaderType, node: yaml.nodes.MappingNode) -> OrderedDict: """Load YAML mappings into an ordered dictionary to preserve key order.""" loader.flatten_mapping(node) nodes = loader.construct_pairs(node) @@ -259,14 +331,14 @@ def _ordered_dict(loader: SafeLineLoader, node: yaml.nodes.MappingNode) -> Order try: hash(key) except TypeError as exc: - fname = getattr(loader.stream, "name", "") + fname = loader.get_stream_name() raise yaml.MarkedYAMLError( context=f'invalid key: "{key}"', context_mark=yaml.Mark(fname, 0, line, -1, None, None), # type: ignore[arg-type] ) from exc if key in seen: - fname = getattr(loader.stream, "name", "") + fname = loader.get_stream_name() _LOGGER.warning( 'YAML file %s contains duplicate key "%s". Check lines %d and %d', fname, @@ -279,13 +351,13 @@ def _ordered_dict(loader: SafeLineLoader, node: yaml.nodes.MappingNode) -> Order return _add_reference(OrderedDict(nodes), loader, node) -def _construct_seq(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE: +def _construct_seq(loader: LoaderType, node: yaml.nodes.Node) -> JSON_TYPE: """Add line number and file name to Load YAML sequence.""" (obj,) = loader.construct_yaml_seq(node) return _add_reference(obj, loader, node) -def _env_var_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> str: +def _env_var_yaml(loader: LoaderType, node: yaml.nodes.Node) -> str: """Load environment variables and embed it into the configuration YAML.""" args = node.value.split() @@ -298,27 +370,27 @@ def _env_var_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> str: raise HomeAssistantError(node.value) -def secret_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE: +def secret_yaml(loader: LoaderType, node: yaml.nodes.Node) -> JSON_TYPE: """Load secrets and embed it into the configuration YAML.""" if loader.secrets is None: raise HomeAssistantError("Secrets not supported in this YAML file") - return loader.secrets.get(loader.name, node.value) + return loader.secrets.get(loader.get_name(), node.value) -SafeLineLoader.add_constructor("!include", _include_yaml) -SafeLineLoader.add_constructor( - yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _ordered_dict -) -SafeLineLoader.add_constructor( - yaml.resolver.BaseResolver.DEFAULT_SEQUENCE_TAG, _construct_seq -) -SafeLineLoader.add_constructor("!env_var", _env_var_yaml) -SafeLineLoader.add_constructor("!secret", secret_yaml) -SafeLineLoader.add_constructor("!include_dir_list", _include_dir_list_yaml) -SafeLineLoader.add_constructor("!include_dir_merge_list", _include_dir_merge_list_yaml) -SafeLineLoader.add_constructor("!include_dir_named", _include_dir_named_yaml) -SafeLineLoader.add_constructor( - "!include_dir_merge_named", _include_dir_merge_named_yaml -) -SafeLineLoader.add_constructor("!input", Input.from_node) +def add_constructor(tag: Any, constructor: Any) -> None: + """Add to constructor to all loaders.""" + for yaml_loader in (SafeLoader, SafeLineLoader): + yaml_loader.add_constructor(tag, constructor) + + +add_constructor("!include", _include_yaml) +add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _ordered_dict) +add_constructor(yaml.resolver.BaseResolver.DEFAULT_SEQUENCE_TAG, _construct_seq) +add_constructor("!env_var", _env_var_yaml) +add_constructor("!secret", secret_yaml) +add_constructor("!include_dir_list", _include_dir_list_yaml) +add_constructor("!include_dir_merge_list", _include_dir_merge_list_yaml) +add_constructor("!include_dir_named", _include_dir_named_yaml) +add_constructor("!include_dir_merge_named", _include_dir_merge_named_yaml) +add_constructor("!input", Input.from_node) diff --git a/tests/components/rest/fixtures/configuration_invalid.notyaml b/tests/components/rest/fixtures/configuration_invalid.notyaml index 548d8bcf5a0..4afb3b7ce96 100644 --- a/tests/components/rest/fixtures/configuration_invalid.notyaml +++ b/tests/components/rest/fixtures/configuration_invalid.notyaml @@ -1,2 +1,2 @@ -*!* NOT YAML +-*!*- NOT YAML diff --git a/tests/test_config.py b/tests/test_config.py index 552139fa0ef..9b3f9d8755f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,6 +1,7 @@ """Test config utils.""" # pylint: disable=protected-access from collections import OrderedDict +import contextlib import copy import os from unittest import mock @@ -147,7 +148,7 @@ def test_load_yaml_config_raises_error_if_not_dict(): def test_load_yaml_config_raises_error_if_malformed_yaml(): """Test error raised if invalid YAML.""" with open(YAML_PATH, "w") as fp: - fp.write(":") + fp.write(":-") with pytest.raises(HomeAssistantError): config_util.load_yaml_config_file(YAML_PATH) @@ -156,11 +157,22 @@ def test_load_yaml_config_raises_error_if_malformed_yaml(): def test_load_yaml_config_raises_error_if_unsafe_yaml(): """Test error raised if unsafe YAML.""" with open(YAML_PATH, "w") as fp: - fp.write("hello: !!python/object/apply:os.system") + fp.write("- !!python/object/apply:os.system []") - with pytest.raises(HomeAssistantError): + with patch.object(os, "system") as system_mock, contextlib.suppress( + HomeAssistantError + ): config_util.load_yaml_config_file(YAML_PATH) + assert len(system_mock.mock_calls) == 0 + + # Here we validate that the test above is a good test + # since previously the syntax was not valid + with open(YAML_PATH) as fp, patch.object(os, "system") as system_mock: + list(yaml.unsafe_load_all(fp)) + + assert len(system_mock.mock_calls) == 1 + def test_load_yaml_config_preserves_key_order(): """Test removal of library.""" diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 2b86b3c50e9..1bdadf87a2d 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -1,10 +1,12 @@ """Test Home Assistant yaml loader.""" +import importlib import io import os import unittest from unittest.mock import patch import pytest +import yaml as pyyaml from homeassistant.config import YAML_CONFIG_FILE, load_yaml_config_file from homeassistant.exceptions import HomeAssistantError @@ -14,7 +16,24 @@ from homeassistant.util.yaml import loader as yaml_loader from tests.common import get_test_config_dir, patch_yaml_files -def test_simple_list(): +@pytest.fixture(params=["enable_c_loader", "disable_c_loader"]) +def try_both_loaders(request): + """Disable the yaml c loader.""" + if not request.param == "disable_c_loader": + yield + return + try: + cloader = pyyaml.CSafeLoader + except ImportError: + return + del pyyaml.CSafeLoader + importlib.reload(yaml_loader) + yield + pyyaml.CSafeLoader = cloader + importlib.reload(yaml_loader) + + +def test_simple_list(try_both_loaders): """Test simple list.""" conf = "config:\n - simple\n - list" with io.StringIO(conf) as file: @@ -22,7 +41,7 @@ def test_simple_list(): assert doc["config"] == ["simple", "list"] -def test_simple_dict(): +def test_simple_dict(try_both_loaders): """Test simple dict.""" conf = "key: value" with io.StringIO(conf) as file: @@ -37,14 +56,14 @@ def test_unhashable_key(): load_yaml_config_file(YAML_CONFIG_FILE) -def test_no_key(): +def test_no_key(try_both_loaders): """Test item without a key.""" files = {YAML_CONFIG_FILE: "a: a\nnokeyhere"} with pytest.raises(HomeAssistantError), patch_yaml_files(files): yaml.load_yaml(YAML_CONFIG_FILE) -def test_environment_variable(): +def test_environment_variable(try_both_loaders): """Test config file with environment variable.""" os.environ["PASSWORD"] = "secret_password" conf = "password: !env_var PASSWORD" @@ -54,7 +73,7 @@ def test_environment_variable(): del os.environ["PASSWORD"] -def test_environment_variable_default(): +def test_environment_variable_default(try_both_loaders): """Test config file with default value for environment variable.""" conf = "password: !env_var PASSWORD secret_password" with io.StringIO(conf) as file: @@ -62,14 +81,14 @@ def test_environment_variable_default(): assert doc["password"] == "secret_password" -def test_invalid_environment_variable(): +def test_invalid_environment_variable(try_both_loaders): """Test config file with no environment variable sat.""" conf = "password: !env_var PASSWORD" with pytest.raises(HomeAssistantError), io.StringIO(conf) as file: yaml_loader.yaml.load(file, Loader=yaml_loader.SafeLineLoader) -def test_include_yaml(): +def test_include_yaml(try_both_loaders): """Test include yaml.""" with patch_yaml_files({"test.yaml": "value"}): conf = "key: !include test.yaml" @@ -85,7 +104,7 @@ def test_include_yaml(): @patch("homeassistant.util.yaml.loader.os.walk") -def test_include_dir_list(mock_walk): +def test_include_dir_list(mock_walk, try_both_loaders): """Test include dir list yaml.""" mock_walk.return_value = [["/test", [], ["two.yaml", "one.yaml"]]] @@ -97,7 +116,7 @@ def test_include_dir_list(mock_walk): @patch("homeassistant.util.yaml.loader.os.walk") -def test_include_dir_list_recursive(mock_walk): +def test_include_dir_list_recursive(mock_walk, try_both_loaders): """Test include dir recursive list yaml.""" mock_walk.return_value = [ ["/test", ["tmp2", ".ignore", "ignore"], ["zero.yaml"]], @@ -124,7 +143,7 @@ def test_include_dir_list_recursive(mock_walk): @patch("homeassistant.util.yaml.loader.os.walk") -def test_include_dir_named(mock_walk): +def test_include_dir_named(mock_walk, try_both_loaders): """Test include dir named yaml.""" mock_walk.return_value = [ ["/test", [], ["first.yaml", "second.yaml", "secrets.yaml"]] @@ -139,7 +158,7 @@ def test_include_dir_named(mock_walk): @patch("homeassistant.util.yaml.loader.os.walk") -def test_include_dir_named_recursive(mock_walk): +def test_include_dir_named_recursive(mock_walk, try_both_loaders): """Test include dir named yaml.""" mock_walk.return_value = [ ["/test", ["tmp2", ".ignore", "ignore"], ["first.yaml"]], @@ -167,7 +186,7 @@ def test_include_dir_named_recursive(mock_walk): @patch("homeassistant.util.yaml.loader.os.walk") -def test_include_dir_merge_list(mock_walk): +def test_include_dir_merge_list(mock_walk, try_both_loaders): """Test include dir merge list yaml.""" mock_walk.return_value = [["/test", [], ["first.yaml", "second.yaml"]]] @@ -181,7 +200,7 @@ def test_include_dir_merge_list(mock_walk): @patch("homeassistant.util.yaml.loader.os.walk") -def test_include_dir_merge_list_recursive(mock_walk): +def test_include_dir_merge_list_recursive(mock_walk, try_both_loaders): """Test include dir merge list yaml.""" mock_walk.return_value = [ ["/test", ["tmp2", ".ignore", "ignore"], ["first.yaml"]], @@ -208,7 +227,7 @@ def test_include_dir_merge_list_recursive(mock_walk): @patch("homeassistant.util.yaml.loader.os.walk") -def test_include_dir_merge_named(mock_walk): +def test_include_dir_merge_named(mock_walk, try_both_loaders): """Test include dir merge named yaml.""" mock_walk.return_value = [["/test", [], ["first.yaml", "second.yaml"]]] @@ -225,7 +244,7 @@ def test_include_dir_merge_named(mock_walk): @patch("homeassistant.util.yaml.loader.os.walk") -def test_include_dir_merge_named_recursive(mock_walk): +def test_include_dir_merge_named_recursive(mock_walk, try_both_loaders): """Test include dir merge named yaml.""" mock_walk.return_value = [ ["/test", ["tmp2", ".ignore", "ignore"], ["first.yaml"]], @@ -257,7 +276,7 @@ def test_include_dir_merge_named_recursive(mock_walk): @patch("homeassistant.util.yaml.loader.open", create=True) -def test_load_yaml_encoding_error(mock_open): +def test_load_yaml_encoding_error(mock_open, try_both_loaders): """Test raising a UnicodeDecodeError.""" mock_open.side_effect = UnicodeDecodeError("", b"", 1, 0, "") with pytest.raises(HomeAssistantError): @@ -413,7 +432,7 @@ def test_representing_yaml_loaded_data(): assert yaml.dump(data) == "key:\n- 1\n- '2'\n- 3\n" -def test_duplicate_key(caplog): +def test_duplicate_key(caplog, try_both_loaders): """Test duplicate dict keys.""" files = {YAML_CONFIG_FILE: "key: thing1\nkey: thing2"} with patch_yaml_files(files): @@ -421,7 +440,7 @@ def test_duplicate_key(caplog): assert "contains duplicate key" in caplog.text -def test_no_recursive_secrets(caplog): +def test_no_recursive_secrets(caplog, try_both_loaders): """Test that loading of secrets from the secrets file fails correctly.""" files = {YAML_CONFIG_FILE: "key: !secret a", yaml.SECRET_YAML: "a: 1\nb: !secret a"} with patch_yaml_files(files), pytest.raises(HomeAssistantError) as e: @@ -441,7 +460,16 @@ def test_input_class(): assert len({input, input2}) == 1 -def test_input(): +def test_input(try_both_loaders): """Test loading inputs.""" data = {"hello": yaml.Input("test_name")} assert yaml.parse_yaml(yaml.dump(data)) == data + + +@pytest.mark.skipif( + not os.environ.get("HASS_CI"), + reason="This test validates that the CI has the C loader available", +) +def test_c_loader_is_available_in_ci(): + """Verify we are testing the C loader in the CI.""" + assert yaml.loader.HAS_C_LOADER is True From c660fae8d8dfd190186fc404ca77cc1bc4c0cda3 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 13 Jun 2022 21:17:08 +0200 Subject: [PATCH 1466/3516] Sensibo Add timer (#73072) --- .../components/sensibo/binary_sensor.py | 39 ++- homeassistant/components/sensibo/climate.py | 34 ++- homeassistant/components/sensibo/entity.py | 14 +- homeassistant/components/sensibo/sensor.py | 39 ++- .../components/sensibo/services.yaml | 28 +++ tests/components/sensibo/test_climate.py | 234 +++++++++++++++++- 6 files changed, 373 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/sensibo/binary_sensor.py b/homeassistant/components/sensibo/binary_sensor.py index 3a7deadc405..72003e0a418 100644 --- a/homeassistant/components/sensibo/binary_sensor.py +++ b/homeassistant/components/sensibo/binary_sensor.py @@ -1,9 +1,9 @@ """Binary Sensor platform for Sensibo integration.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Mapping from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from pysensibo.model import MotionSensor, SensiboDevice @@ -36,6 +36,7 @@ class DeviceBaseEntityDescriptionMixin: """Mixin for required Sensibo base description keys.""" value_fn: Callable[[SensiboDevice], bool | None] + extra_fn: Callable[[SensiboDevice], dict[str, str | bool | None] | None] | None @dataclass @@ -77,13 +78,25 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionBinarySensorEntityDescription, ...] = ( ), ) -DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( +MOTION_DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( SensiboDeviceBinarySensorEntityDescription( key="room_occupied", device_class=BinarySensorDeviceClass.MOTION, name="Room Occupied", icon="mdi:motion-sensor", value_fn=lambda data: data.room_occupied, + extra_fn=None, + ), +) + +DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( + SensiboDeviceBinarySensorEntityDescription( + key="timer_on", + device_class=BinarySensorDeviceClass.RUNNING, + name="Timer Running", + icon="mdi:timer", + value_fn=lambda data: data.timer_on, + extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on}, ), ) @@ -94,6 +107,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( name="Pure Boost Enabled", icon="mdi:wind-power-outline", value_fn=lambda data: data.pure_boost_enabled, + extra_fn=None, ), SensiboDeviceBinarySensorEntityDescription( key="pure_ac_integration", @@ -102,6 +116,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( name="Pure Boost linked with AC", icon="mdi:connection", value_fn=lambda data: data.pure_ac_integration, + extra_fn=None, ), SensiboDeviceBinarySensorEntityDescription( key="pure_geo_integration", @@ -110,6 +125,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( name="Pure Boost linked with Presence", icon="mdi:connection", value_fn=lambda data: data.pure_geo_integration, + extra_fn=None, ), SensiboDeviceBinarySensorEntityDescription( key="pure_measure_integration", @@ -118,6 +134,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( name="Pure Boost linked with Indoor Air Quality", icon="mdi:connection", value_fn=lambda data: data.pure_measure_integration, + extra_fn=None, ), SensiboDeviceBinarySensorEntityDescription( key="pure_prime_integration", @@ -126,6 +143,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( name="Pure Boost linked with Outdoor Air Quality", icon="mdi:connection", value_fn=lambda data: data.pure_prime_integration, + extra_fn=None, ), ) @@ -148,11 +166,17 @@ async def async_setup_entry( for sensor_id, sensor_data in device_data.motion_sensors.items() for description in MOTION_SENSOR_TYPES ) + entities.extend( + SensiboDeviceSensor(coordinator, device_id, description) + for description in MOTION_DEVICE_SENSOR_TYPES + for device_id, device_data in coordinator.data.parsed.items() + if device_data.motion_sensors is not None + ) entities.extend( SensiboDeviceSensor(coordinator, device_id, description) for description in DEVICE_SENSOR_TYPES for device_id, device_data in coordinator.data.parsed.items() - if getattr(device_data, description.key) is not None + if device_data.model != "pure" ) entities.extend( SensiboDeviceSensor(coordinator, device_id, description) @@ -223,3 +247,10 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, BinarySensorEntity): def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" return self.entity_description.value_fn(self.device_data) + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return additional attributes.""" + if self.entity_description.extra_fn is not None: + return self.entity_description.extra_fn(self.device_data) + return None diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index c7967d05be0..f0ce7b74c01 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -18,7 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_platform +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.temperature import convert as convert_temperature @@ -27,6 +27,8 @@ from .coordinator import SensiboDataUpdateCoordinator from .entity import SensiboDeviceBaseEntity SERVICE_ASSUME_STATE = "assume_state" +SERVICE_TIMER = "timer" +ATTR_MINUTES = "minutes" PARALLEL_UPDATES = 0 FIELD_TO_FLAG = { @@ -85,6 +87,14 @@ async def async_setup_entry( }, "async_assume_state", ) + platform.async_register_entity_service( + SERVICE_TIMER, + { + vol.Required(ATTR_STATE): vol.In(["on", "off"]), + vol.Optional(ATTR_MINUTES): cv.positive_int, + }, + "async_set_timer", + ) class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): @@ -276,3 +286,25 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): """Sync state with api.""" await self._async_set_ac_state_property("on", state != HVACMode.OFF, True) await self.coordinator.async_refresh() + + async def async_set_timer(self, state: str, minutes: int | None = None) -> None: + """Set or delete timer.""" + if state == "off" and self.device_data.timer_id is None: + raise HomeAssistantError("No timer to delete") + + if state == "on" and minutes is None: + raise ValueError("No value provided for timer") + + if state == "off": + result = await self.async_send_command("del_timer") + else: + new_state = bool(self.device_data.ac_states["on"] is False) + params = { + "minutesFromNow": minutes, + "acState": {**self.device_data.ac_states, "on": new_state}, + } + result = await self.async_send_command("set_timer", params) + + if result["status"] == "success": + return await self.coordinator.async_request_refresh() + raise HomeAssistantError(f"Could not set timer for device {self.name}") diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index c2f4869a4e6..430d7ac61ac 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -57,7 +57,7 @@ class SensiboDeviceBaseEntity(SensiboBaseEntity): ) async def async_send_command( - self, command: str, params: dict[str, Any] + self, command: str, params: dict[str, Any] | None = None ) -> dict[str, Any]: """Send command to Sensibo api.""" try: @@ -72,16 +72,20 @@ class SensiboDeviceBaseEntity(SensiboBaseEntity): return result async def async_send_api_call( - self, command: str, params: dict[str, Any] + self, command: str, params: dict[str, Any] | None = None ) -> dict[str, Any]: """Send api call.""" result: dict[str, Any] = {"status": None} if command == "set_calibration": + if TYPE_CHECKING: + assert params is not None result = await self._client.async_set_calibration( self._device_id, params["data"], ) if command == "set_ac_state": + if TYPE_CHECKING: + assert params is not None result = await self._client.async_set_ac_state_property( self._device_id, params["name"], @@ -89,6 +93,12 @@ class SensiboDeviceBaseEntity(SensiboBaseEntity): params["ac_states"], params["assumed_state"], ) + if command == "set_timer": + if TYPE_CHECKING: + assert params is not None + result = await self._client.async_set_timer(self._device_id, params) + if command == "del_timer": + result = await self._client.async_del_timer(self._device_id) return result diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index f37a054b606..259a24ab876 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -1,9 +1,10 @@ """Sensor platform for Sensibo integration.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Mapping from dataclasses import dataclass -from typing import TYPE_CHECKING +from datetime import datetime +from typing import TYPE_CHECKING, Any from pysensibo.model import MotionSensor, SensiboDevice @@ -44,7 +45,8 @@ class MotionBaseEntityDescriptionMixin: class DeviceBaseEntityDescriptionMixin: """Mixin for required Sensibo base description keys.""" - value_fn: Callable[[SensiboDevice], StateType] + value_fn: Callable[[SensiboDevice], StateType | datetime] + extra_fn: Callable[[SensiboDevice], dict[str, str | bool | None] | None] | None @dataclass @@ -111,13 +113,25 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( name="PM2.5", icon="mdi:air-filter", value_fn=lambda data: data.pm25, + extra_fn=None, ), SensiboDeviceSensorEntityDescription( key="pure_sensitivity", name="Pure Sensitivity", icon="mdi:air-filter", - value_fn=lambda data: str(data.pure_sensitivity).lower(), - device_class="sensibo__sensitivity", + value_fn=lambda data: data.pure_sensitivity, + extra_fn=None, + ), +) + +DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( + SensiboDeviceSensorEntityDescription( + key="timer_time", + device_class=SensorDeviceClass.TIMESTAMP, + name="Timer End Time", + icon="mdi:timer", + value_fn=lambda data: data.timer_time, + extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on}, ), ) @@ -146,6 +160,12 @@ async def async_setup_entry( for description in PURE_SENSOR_TYPES if device_data.model == "pure" ) + entities.extend( + SensiboDeviceSensor(coordinator, device_id, description) + for device_id, device_data in coordinator.data.parsed.items() + for description in DEVICE_SENSOR_TYPES + if device_data.model != "pure" + ) async_add_entities(entities) @@ -205,6 +225,13 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, SensorEntity): self._attr_name = f"{self.device_data.name} {entity_description.name}" @property - def native_value(self) -> StateType: + def native_value(self) -> StateType | datetime: """Return value of sensor.""" return self.entity_description.value_fn(self.device_data) + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return additional attributes.""" + if self.entity_description.extra_fn is not None: + return self.entity_description.extra_fn(self.device_data) + return None diff --git a/homeassistant/components/sensibo/services.yaml b/homeassistant/components/sensibo/services.yaml index bbbdb8611e8..1a64f8703b4 100644 --- a/homeassistant/components/sensibo/services.yaml +++ b/homeassistant/components/sensibo/services.yaml @@ -16,3 +16,31 @@ assume_state: options: - "on" - "off" +timer: + name: Timer + description: Set or delete timer for device. + target: + entity: + integration: sensibo + domain: climate + fields: + state: + name: State + description: Timer on or off. + required: true + example: "on" + selector: + select: + options: + - "on" + - "off" + minutes: + name: Minutes + description: Countdown for timer (for timer state on) + required: false + example: 30 + selector: + number: + min: 0 + step: 1 + mode: box diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index ead4fb02d88..16e83162600 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -1,8 +1,8 @@ """The test for the sensibo binary sensor platform.""" from __future__ import annotations -from datetime import timedelta -from unittest.mock import patch +from datetime import datetime, timedelta +from unittest.mock import AsyncMock, patch from pysensibo.model import SensiboData import pytest @@ -21,7 +21,9 @@ from homeassistant.components.climate.const import ( SERVICE_SET_TEMPERATURE, ) from homeassistant.components.sensibo.climate import ( + ATTR_MINUTES, SERVICE_ASSUME_STATE, + SERVICE_TIMER, _find_valid_target_temp, ) from homeassistant.components.sensibo.const import DOMAIN @@ -32,6 +34,7 @@ from homeassistant.const import ( ATTR_TEMPERATURE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -675,3 +678,230 @@ async def test_climate_no_fan_no_swing( assert state.attributes["swing_mode"] is None assert state.attributes["fan_modes"] is None assert state.attributes["swing_modes"] is None + + +async def test_climate_set_timer( + hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, + load_int: ConfigEntry, + monkeypatch: pytest.MonkeyPatch, + get_data: SensiboData, +) -> None: + """Test the Sensibo climate Set Timer service.""" + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("climate.hallway") + assert hass.states.get("sensor.hallway_timer_end_time").state == STATE_UNKNOWN + assert hass.states.get("binary_sensor.hallway_timer_running").state == "off" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", + return_value={"status": "success", "result": {"id": "SzTGE4oZ4D"}}, + ): + await hass.services.async_call( + DOMAIN, + SERVICE_TIMER, + { + ATTR_ENTITY_ID: state1.entity_id, + ATTR_STATE: "on", + ATTR_MINUTES: 30, + }, + blocking=True, + ) + await hass.async_block_till_done() + + monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_on", True) + monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_id", "SzTGE4oZ4D") + monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_state_on", False) + monkeypatch.setattr( + get_data.parsed["ABC999111"], + "timer_time", + datetime(2022, 6, 6, 12, 00, 00, tzinfo=dt.UTC), + ) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + assert ( + hass.states.get("sensor.hallway_timer_end_time").state + == "2022-06-06T12:00:00+00:00" + ) + assert hass.states.get("binary_sensor.hallway_timer_running").state == "on" + assert hass.states.get("binary_sensor.hallway_timer_running").attributes == { + "device_class": "running", + "friendly_name": "Hallway Timer Running", + "icon": "mdi:timer", + "id": "SzTGE4oZ4D", + "turn_on": False, + } + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_del_timer", + return_value={"status": "success"}, + ): + await hass.services.async_call( + DOMAIN, + SERVICE_TIMER, + { + ATTR_ENTITY_ID: state1.entity_id, + ATTR_STATE: "off", + }, + blocking=True, + ) + await hass.async_block_till_done() + + monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_on", False) + monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_id", None) + monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_state_on", None) + monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_time", None) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.hallway_timer_end_time").state == STATE_UNKNOWN + assert hass.states.get("binary_sensor.hallway_timer_running").state == "off" + + +async def test_climate_set_timer_failures( + hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, + load_int: ConfigEntry, + monkeypatch: pytest.MonkeyPatch, + get_data: SensiboData, +) -> None: + """Test the Sensibo climate Set Timer service failures.""" + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("climate.hallway") + assert hass.states.get("sensor.hallway_timer_end_time").state == STATE_UNKNOWN + assert hass.states.get("binary_sensor.hallway_timer_running").state == "off" + + with pytest.raises(ValueError): + await hass.services.async_call( + DOMAIN, + SERVICE_TIMER, + { + ATTR_ENTITY_ID: state1.entity_id, + ATTR_STATE: "on", + }, + blocking=True, + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", + return_value={"status": "success", "result": {"id": ""}}, + ): + await hass.services.async_call( + DOMAIN, + SERVICE_TIMER, + { + ATTR_ENTITY_ID: state1.entity_id, + ATTR_STATE: "on", + ATTR_MINUTES: 30, + }, + blocking=True, + ) + await hass.async_block_till_done() + + monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_on", True) + monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_id", None) + monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_state_on", False) + monkeypatch.setattr( + get_data.parsed["ABC999111"], + "timer_time", + datetime(2022, 6, 6, 12, 00, 00, tzinfo=dt.UTC), + ) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_TIMER, + { + ATTR_ENTITY_ID: state1.entity_id, + ATTR_STATE: "off", + }, + blocking=True, + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", + return_value={"status": "failure"}, + ): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_TIMER, + { + ATTR_ENTITY_ID: state1.entity_id, + ATTR_STATE: "on", + ATTR_MINUTES: 30, + }, + blocking=True, + ) + await hass.async_block_till_done() From 0ffeb6c3040d9ff226d92f47b6977500d0be5325 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Jun 2022 10:10:05 -1000 Subject: [PATCH 1467/3516] Check if requirements are installed in the executor (#71611) --- homeassistant/requirements.py | 326 ++++++++++++++++++++-------------- tests/test_requirements.py | 45 ++--- 2 files changed, 209 insertions(+), 162 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 7631586d626..9a8cd20983a 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -15,10 +15,7 @@ from .util import package as pkg_util PIP_TIMEOUT = 60 # The default is too low when the internet connection is satellite or high latency MAX_INSTALL_FAILURES = 3 -DATA_PIP_LOCK = "pip_lock" -DATA_PKG_CACHE = "pkg_cache" -DATA_INTEGRATIONS_WITH_REQS = "integrations_with_reqs" -DATA_INSTALL_FAILURE_HISTORY = "install_failure_history" +DATA_REQUIREMENTS_MANAGER = "requirements_manager" CONSTRAINT_FILE = "package_constraints.txt" DISCOVERY_INTEGRATIONS: dict[str, Iterable[str]] = { "dhcp": ("dhcp",), @@ -40,7 +37,7 @@ class RequirementsNotFound(HomeAssistantError): async def async_get_integration_with_requirements( - hass: HomeAssistant, domain: str, done: set[str] | None = None + hass: HomeAssistant, domain: str ) -> Integration: """Get an integration with all requirements installed, including the dependencies. @@ -48,97 +45,8 @@ async def async_get_integration_with_requirements( is invalid, RequirementNotFound if there was some type of failure to install requirements. """ - if done is None: - done = {domain} - else: - done.add(domain) - - integration = await async_get_integration(hass, domain) - - if hass.config.skip_pip: - return integration - - 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( - domain, UNDEFINED - ) - - if isinstance(int_or_evt, asyncio.Event): - await int_or_evt.wait() - - # 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 := cache.get(domain, UNDEFINED)) is UNDEFINED: - raise IntegrationNotFound(domain) - - if int_or_evt is not UNDEFINED: - return cast(Integration, int_or_evt) - - event = cache[domain] = asyncio.Event() - - try: - await _async_process_integration(hass, integration, done) - except Exception: - del cache[domain] - event.set() - raise - - cache[domain] = integration - event.set() - return integration - - -async def _async_process_integration( - hass: HomeAssistant, integration: Integration, done: set[str] -) -> None: - """Process an integration and requirements.""" - if integration.requirements: - await async_process_requirements( - hass, integration.domain, integration.requirements - ) - - deps_to_check = [ - dep - for dep in integration.dependencies + integration.after_dependencies - if dep not in done - ] - - for check_domain, to_check in DISCOVERY_INTEGRATIONS.items(): - if ( - check_domain not in done - and check_domain not in deps_to_check - and any(check in integration.manifest for check in to_check) - ): - deps_to_check.append(check_domain) - - if not deps_to_check: - return - - results = await asyncio.gather( - *( - async_get_integration_with_requirements(hass, dep, done) - for dep in deps_to_check - ), - return_exceptions=True, - ) - for result in results: - if not isinstance(result, BaseException): - continue - if not isinstance(result, IntegrationNotFound) or not ( - not integration.is_built_in - and result.domain in integration.after_dependencies - ): - raise result - - -@callback -def async_clear_install_history(hass: HomeAssistant) -> None: - """Forget the install history.""" - if install_failure_history := hass.data.get(DATA_INSTALL_FAILURE_HISTORY): - install_failure_history.clear() + manager = _async_get_manager(hass) + return await manager.async_get_integration_with_requirements(domain) async def async_process_requirements( @@ -149,49 +57,24 @@ async def async_process_requirements( This method is a coroutine. It will raise RequirementsNotFound if an requirement can't be satisfied. """ - 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: - install_failure_history = hass.data[DATA_INSTALL_FAILURE_HISTORY] = set() - - kwargs = pip_kwargs(hass.config.config_dir) - - async with pip_lock: - for req in requirements: - await _async_process_requirements( - hass, name, req, install_failure_history, kwargs - ) + await _async_get_manager(hass).async_process_requirements(name, requirements) -async def _async_process_requirements( - hass: HomeAssistant, - name: str, - req: str, - install_failure_history: set[str], - kwargs: Any, -) -> None: - """Install a requirement and save failures.""" - if req in install_failure_history: - _LOGGER.info( - "Multiple attempts to install %s failed, install will be retried after next configuration check or restart", - req, - ) - raise RequirementsNotFound(name, [req]) +@callback +def _async_get_manager(hass: HomeAssistant) -> RequirementsManager: + """Get the requirements manager.""" + if DATA_REQUIREMENTS_MANAGER in hass.data: + manager: RequirementsManager = hass.data[DATA_REQUIREMENTS_MANAGER] + return manager - if pkg_util.is_installed(req): - return + manager = hass.data[DATA_REQUIREMENTS_MANAGER] = RequirementsManager(hass) + return manager - def _install(req: str, kwargs: dict[str, Any]) -> bool: - """Install requirement.""" - return pkg_util.install_package(req, **kwargs) - for _ in range(MAX_INSTALL_FAILURES): - if await hass.async_add_executor_job(_install, req, kwargs): - return - - install_failure_history.add(req) - raise RequirementsNotFound(name, [req]) +@callback +def async_clear_install_history(hass: HomeAssistant) -> None: + """Forget the install history.""" + _async_get_manager(hass).install_failure_history.clear() def pip_kwargs(config_dir: str | None) -> dict[str, Any]: @@ -207,3 +90,178 @@ def pip_kwargs(config_dir: str | None) -> dict[str, Any]: if not (config_dir is None or pkg_util.is_virtual_env()) and not is_docker: kwargs["target"] = os.path.join(config_dir, "deps") return kwargs + + +def _install_with_retry(requirement: str, kwargs: dict[str, Any]) -> bool: + """Try to install a package up to MAX_INSTALL_FAILURES times.""" + for _ in range(MAX_INSTALL_FAILURES): + if pkg_util.install_package(requirement, **kwargs): + return True + return False + + +def _install_requirements_if_missing( + requirements: list[str], kwargs: dict[str, Any] +) -> tuple[set[str], set[str]]: + """Install requirements if missing.""" + installed: set[str] = set() + failures: set[str] = set() + for req in requirements: + if pkg_util.is_installed(req) or _install_with_retry(req, kwargs): + installed.add(req) + continue + failures.add(req) + return installed, failures + + +class RequirementsManager: + """Manage requirements.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Init the requirements manager.""" + self.hass = hass + self.pip_lock = asyncio.Lock() + self.integrations_with_reqs: dict[ + str, Integration | asyncio.Event | None | UndefinedType + ] = {} + self.install_failure_history: set[str] = set() + self.is_installed_cache: set[str] = set() + + async def async_get_integration_with_requirements( + self, domain: str, done: set[str] | None = None + ) -> Integration: + """Get an integration with all requirements installed, including the dependencies. + + This can raise IntegrationNotFound if manifest or integration + is invalid, RequirementNotFound if there was some type of + failure to install requirements. + """ + + if done is None: + done = {domain} + else: + done.add(domain) + + integration = await async_get_integration(self.hass, domain) + + if self.hass.config.skip_pip: + return integration + + cache = self.integrations_with_reqs + int_or_evt = cache.get(domain, UNDEFINED) + + if isinstance(int_or_evt, asyncio.Event): + await int_or_evt.wait() + + # 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 := cache.get(domain, UNDEFINED)) is UNDEFINED: + raise IntegrationNotFound(domain) + + if int_or_evt is not UNDEFINED: + return cast(Integration, int_or_evt) + + event = cache[domain] = asyncio.Event() + + try: + await self._async_process_integration(integration, done) + except Exception: + del cache[domain] + event.set() + raise + + cache[domain] = integration + event.set() + return integration + + async def _async_process_integration( + self, integration: Integration, done: set[str] + ) -> None: + """Process an integration and requirements.""" + if integration.requirements: + await self.async_process_requirements( + integration.domain, integration.requirements + ) + + deps_to_check = [ + dep + for dep in integration.dependencies + integration.after_dependencies + if dep not in done + ] + + for check_domain, to_check in DISCOVERY_INTEGRATIONS.items(): + if ( + check_domain not in done + and check_domain not in deps_to_check + and any(check in integration.manifest for check in to_check) + ): + deps_to_check.append(check_domain) + + if not deps_to_check: + return + + results = await asyncio.gather( + *( + self.async_get_integration_with_requirements(dep, done) + for dep in deps_to_check + ), + return_exceptions=True, + ) + for result in results: + if not isinstance(result, BaseException): + continue + if not isinstance(result, IntegrationNotFound) or not ( + not integration.is_built_in + and result.domain in integration.after_dependencies + ): + raise result + + async def async_process_requirements( + self, name: str, requirements: list[str] + ) -> None: + """Install the requirements for a component or platform. + + This method is a coroutine. It will raise RequirementsNotFound + if an requirement can't be satisfied. + """ + if not (missing := self._find_missing_requirements(requirements)): + return + self._raise_for_failed_requirements(name, missing) + + async with self.pip_lock: + # Recaculate missing again now that we have the lock + await self._async_process_requirements( + name, self._find_missing_requirements(requirements) + ) + + def _find_missing_requirements(self, requirements: list[str]) -> list[str]: + """Find requirements that are missing in the cache.""" + return [req for req in requirements if req not in self.is_installed_cache] + + def _raise_for_failed_requirements( + self, integration: str, missing: list[str] + ) -> None: + """Raise RequirementsNotFound so we do not keep trying requirements that have already failed.""" + for req in missing: + if req in self.install_failure_history: + _LOGGER.info( + "Multiple attempts to install %s failed, install will be retried after next configuration check or restart", + req, + ) + raise RequirementsNotFound(integration, [req]) + + async def _async_process_requirements( + self, + name: str, + requirements: list[str], + ) -> None: + """Install a requirement and save failures.""" + kwargs = pip_kwargs(self.hass.config.config_dir) + installed, failures = await self.hass.async_add_executor_job( + _install_requirements_if_missing, requirements, kwargs + ) + self.is_installed_cache |= installed + self.install_failure_history |= failures + if failures: + raise RequirementsNotFound(name, list(failures)) diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 7e4dba42c2f..77499707489 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -213,16 +213,9 @@ async def test_get_integration_with_requirements_pip_install_fails_two_passes(ha assert integration assert integration.domain == "test_component" - assert len(mock_is_installed.mock_calls) == 1 - assert sorted(mock_call[1][0] for mock_call in mock_is_installed.mock_calls) == [ - "test-comp==1.0.0", - ] - + assert len(mock_is_installed.mock_calls) == 0 # On another attempt we remember failures and don't try again - assert len(mock_inst.mock_calls) == 1 - assert sorted(mock_call[1][0] for mock_call in mock_inst.mock_calls) == [ - "test-comp==1.0.0" - ] + assert len(mock_inst.mock_calls) == 0 # Now clear the history and so we try again async_clear_install_history(hass) @@ -239,14 +232,13 @@ async def test_get_integration_with_requirements_pip_install_fails_two_passes(ha assert integration assert integration.domain == "test_component" - assert len(mock_is_installed.mock_calls) == 3 + assert len(mock_is_installed.mock_calls) == 2 assert sorted(mock_call[1][0] for mock_call in mock_is_installed.mock_calls) == [ "test-comp-after-dep==1.0.0", "test-comp-dep==1.0.0", - "test-comp==1.0.0", ] - assert len(mock_inst.mock_calls) == 7 + assert len(mock_inst.mock_calls) == 6 assert sorted(mock_call[1][0] for mock_call in mock_inst.mock_calls) == [ "test-comp-after-dep==1.0.0", "test-comp-after-dep==1.0.0", @@ -254,7 +246,6 @@ async def test_get_integration_with_requirements_pip_install_fails_two_passes(ha "test-comp-dep==1.0.0", "test-comp-dep==1.0.0", "test-comp-dep==1.0.0", - "test-comp==1.0.0", ] # Now clear the history and mock success @@ -272,18 +263,16 @@ async def test_get_integration_with_requirements_pip_install_fails_two_passes(ha assert integration assert integration.domain == "test_component" - assert len(mock_is_installed.mock_calls) == 3 + assert len(mock_is_installed.mock_calls) == 2 assert sorted(mock_call[1][0] for mock_call in mock_is_installed.mock_calls) == [ "test-comp-after-dep==1.0.0", "test-comp-dep==1.0.0", - "test-comp==1.0.0", ] - assert len(mock_inst.mock_calls) == 3 + assert len(mock_inst.mock_calls) == 2 assert sorted(mock_call[1][0] for mock_call in mock_inst.mock_calls) == [ "test-comp-after-dep==1.0.0", "test-comp-dep==1.0.0", - "test-comp==1.0.0", ] @@ -408,12 +397,12 @@ async def test_discovery_requirements_mqtt(hass): hass, MockModule("mqtt_comp", partial_manifest={"mqtt": ["foo/discovery"]}) ) with patch( - "homeassistant.requirements.async_process_requirements", + "homeassistant.requirements.RequirementsManager.async_process_requirements", ) as mock_process: await async_get_integration_with_requirements(hass, "mqtt_comp") assert len(mock_process.mock_calls) == 2 # mqtt also depends on http - assert mock_process.mock_calls[0][1][2] == mqtt.requirements + assert mock_process.mock_calls[0][1][1] == mqtt.requirements async def test_discovery_requirements_ssdp(hass): @@ -425,17 +414,17 @@ async def test_discovery_requirements_ssdp(hass): hass, MockModule("ssdp_comp", partial_manifest={"ssdp": [{"st": "roku:ecp"}]}) ) with patch( - "homeassistant.requirements.async_process_requirements", + "homeassistant.requirements.RequirementsManager.async_process_requirements", ) as mock_process: await async_get_integration_with_requirements(hass, "ssdp_comp") assert len(mock_process.mock_calls) == 4 - assert mock_process.mock_calls[0][1][2] == ssdp.requirements + assert mock_process.mock_calls[0][1][1] == ssdp.requirements # Ensure zeroconf is a dep for ssdp assert { - mock_process.mock_calls[1][1][1], - mock_process.mock_calls[2][1][1], - mock_process.mock_calls[3][1][1], + mock_process.mock_calls[1][1][0], + mock_process.mock_calls[2][1][0], + mock_process.mock_calls[3][1][0], } == {"network", "zeroconf", "http"} @@ -454,12 +443,12 @@ async def test_discovery_requirements_zeroconf(hass, partial_manifest): ) with patch( - "homeassistant.requirements.async_process_requirements", + "homeassistant.requirements.RequirementsManager.async_process_requirements", ) as mock_process: await async_get_integration_with_requirements(hass, "comp") assert len(mock_process.mock_calls) == 3 # zeroconf also depends on http - assert mock_process.mock_calls[0][1][2] == zeroconf.requirements + assert mock_process.mock_calls[0][1][1] == zeroconf.requirements async def test_discovery_requirements_dhcp(hass): @@ -477,9 +466,9 @@ async def test_discovery_requirements_dhcp(hass): ), ) with patch( - "homeassistant.requirements.async_process_requirements", + "homeassistant.requirements.RequirementsManager.async_process_requirements", ) as mock_process: await async_get_integration_with_requirements(hass, "comp") assert len(mock_process.mock_calls) == 1 # dhcp does not depend on http - assert mock_process.mock_calls[0][1][2] == dhcp.requirements + assert mock_process.mock_calls[0][1][1] == dhcp.requirements From 034c0c0593ba850faf624df581bb567fdc670a4f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Jun 2022 10:14:30 -1000 Subject: [PATCH 1468/3516] Improve YAML Dump times with C Dumper (#73424) --- homeassistant/helpers/selector.py | 5 ++-- homeassistant/util/yaml/dumper.py | 26 +++++++++++++++---- .../blueprint/test_websocket_api.py | 14 ++++++++-- tests/util/yaml/test_init.py | 25 +++++++++++++++--- 4 files changed, 56 insertions(+), 14 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 87574949f4e..ccb7ac67dfb 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -5,13 +5,12 @@ from collections.abc import Callable, Sequence from typing import Any, TypedDict, cast import voluptuous as vol -import yaml from homeassistant.backports.enum import StrEnum from homeassistant.const import CONF_MODE, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.util import decorator -from homeassistant.util.yaml.dumper import represent_odict +from homeassistant.util.yaml.dumper import add_representer, represent_odict from . import config_validation as cv @@ -889,7 +888,7 @@ class TimeSelector(Selector): return cast(str, data) -yaml.SafeDumper.add_representer( +add_representer( Selector, lambda dumper, value: represent_odict( dumper, "tag:yaml.org,2002:map", value.serialize() diff --git a/homeassistant/util/yaml/dumper.py b/homeassistant/util/yaml/dumper.py index 3eafc8abdd7..9f69c6c346e 100644 --- a/homeassistant/util/yaml/dumper.py +++ b/homeassistant/util/yaml/dumper.py @@ -1,5 +1,6 @@ """Custom dumper and representers.""" from collections import OrderedDict +from typing import Any import yaml @@ -8,10 +9,20 @@ from .objects import Input, NodeListClass # mypy: allow-untyped-calls, no-warn-return-any +try: + from yaml import CSafeDumper as FastestAvailableSafeDumper +except ImportError: + from yaml import SafeDumper as FastestAvailableSafeDumper # type: ignore[misc] + + def dump(_dict: dict) -> str: """Dump YAML to a string and remove null.""" - return yaml.safe_dump( - _dict, default_flow_style=False, allow_unicode=True, sort_keys=False + return yaml.dump( + _dict, + default_flow_style=False, + allow_unicode=True, + sort_keys=False, + Dumper=FastestAvailableSafeDumper, ).replace(": null\n", ":\n") @@ -51,17 +62,22 @@ def represent_odict( # type: ignore[no-untyped-def] return node -yaml.SafeDumper.add_representer( +def add_representer(klass: Any, representer: Any) -> None: + """Add to representer to the dumper.""" + FastestAvailableSafeDumper.add_representer(klass, representer) + + +add_representer( OrderedDict, lambda dumper, value: represent_odict(dumper, "tag:yaml.org,2002:map", value), ) -yaml.SafeDumper.add_representer( +add_representer( NodeListClass, lambda dumper, value: dumper.represent_sequence("tag:yaml.org,2002:seq", value), ) -yaml.SafeDumper.add_representer( +add_representer( Input, lambda dumper, value: dumper.represent_scalar("!input", value.name), ) diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index 9376710abee..eb2d12f5081 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -5,6 +5,7 @@ from unittest.mock import Mock, patch import pytest from homeassistant.setup import async_setup_component +from homeassistant.util.yaml import parse_yaml @pytest.fixture(autouse=True) @@ -130,9 +131,18 @@ async def test_save_blueprint(hass, aioclient_mock, hass_ws_client): assert msg["id"] == 6 assert msg["success"] assert write_mock.mock_calls - assert write_mock.call_args[0] == ( - "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n selector:\n text: {}\n service_to_call:\n a_number:\n selector:\n number:\n mode: box\n step: 1.0\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n entity_id: light.kitchen\n", + # There are subtle differences in the dumper quoting + # behavior when quoting is not required as both produce + # valid yaml + output_yaml = write_mock.call_args[0][0] + assert output_yaml in ( + # pure python dumper will quote the value after !input + "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n selector:\n text: {}\n service_to_call:\n a_number:\n selector:\n number:\n mode: box\n step: 1.0\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n entity_id: light.kitchen\n" + # c dumper will not quote the value after !input + "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n selector:\n text: {}\n service_to_call:\n a_number:\n selector:\n number:\n mode: box\n step: 1.0\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input trigger_event\naction:\n service: !input service_to_call\n entity_id: light.kitchen\n" ) + # Make sure ita parsable and does not raise + assert len(parse_yaml(output_yaml)) > 1 async def test_save_existing_file(hass, aioclient_mock, hass_ws_client): diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 1bdadf87a2d..11dc40233dc 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -33,6 +33,23 @@ def try_both_loaders(request): importlib.reload(yaml_loader) +@pytest.fixture(params=["enable_c_dumper", "disable_c_dumper"]) +def try_both_dumpers(request): + """Disable the yaml c dumper.""" + if not request.param == "disable_c_dumper": + yield + return + try: + cdumper = pyyaml.CSafeDumper + except ImportError: + return + del pyyaml.CSafeDumper + importlib.reload(yaml_loader) + yield + pyyaml.CSafeDumper = cdumper + importlib.reload(yaml_loader) + + def test_simple_list(try_both_loaders): """Test simple list.""" conf = "config:\n - simple\n - list" @@ -283,12 +300,12 @@ def test_load_yaml_encoding_error(mock_open, try_both_loaders): yaml_loader.load_yaml("test") -def test_dump(): +def test_dump(try_both_dumpers): """The that the dump method returns empty None values.""" assert yaml.dump({"a": None, "b": "b"}) == "a:\nb: b\n" -def test_dump_unicode(): +def test_dump_unicode(try_both_dumpers): """The that the dump method returns empty None values.""" assert yaml.dump({"a": None, "b": "привет"}) == "a:\nb: привет\n" @@ -424,7 +441,7 @@ class TestSecrets(unittest.TestCase): ) -def test_representing_yaml_loaded_data(): +def test_representing_yaml_loaded_data(try_both_dumpers): """Test we can represent YAML loaded data.""" files = {YAML_CONFIG_FILE: 'key: [1, "2", 3]'} with patch_yaml_files(files): @@ -460,7 +477,7 @@ def test_input_class(): assert len({input, input2}) == 1 -def test_input(try_both_loaders): +def test_input(try_both_loaders, try_both_dumpers): """Test loading inputs.""" data = {"hello": yaml.Input("test_name")} assert yaml.parse_yaml(yaml.dump(data)) == data From 51b4d15c8cb83bb715222841aa48e83f77ef38ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Jun 2022 10:17:10 -1000 Subject: [PATCH 1469/3516] Speed up mqtt tests (#73423) Co-authored-by: jbouwh Co-authored-by: Jan Bouwhuis --- .../mqtt/test_alarm_control_panel.py | 10 ++++++++ tests/components/mqtt/test_binary_sensor.py | 8 ++++++ tests/components/mqtt/test_button.py | 14 ++++++++++- tests/components/mqtt/test_camera.py | 8 ++++++ tests/components/mqtt/test_climate.py | 9 ++++++- tests/components/mqtt/test_cover.py | 8 ++++++ tests/components/mqtt/test_device_tracker.py | 11 +++++++- .../mqtt/test_device_tracker_discovery.py | 11 +++++++- tests/components/mqtt/test_device_trigger.py | 12 +++++++++ tests/components/mqtt/test_diagnostics.py | 13 +++++++++- tests/components/mqtt/test_discovery.py | 25 +++++++++++++++++++ tests/components/mqtt/test_fan.py | 8 ++++++ tests/components/mqtt/test_humidifier.py | 8 ++++++ tests/components/mqtt/test_init.py | 14 +++++++++++ tests/components/mqtt/test_legacy_vacuum.py | 9 ++++++- tests/components/mqtt/test_light.py | 8 ++++++ tests/components/mqtt/test_light_json.py | 8 ++++++ tests/components/mqtt/test_light_template.py | 8 ++++++ tests/components/mqtt/test_lock.py | 8 ++++++ tests/components/mqtt/test_number.py | 8 ++++++ tests/components/mqtt/test_scene.py | 9 ++++++- tests/components/mqtt/test_select.py | 14 ++++++++++- tests/components/mqtt/test_sensor.py | 8 ++++++ tests/components/mqtt/test_siren.py | 8 ++++++ tests/components/mqtt/test_state_vacuum.py | 8 ++++++ tests/components/mqtt/test_subscription.py | 11 +++++++- tests/components/mqtt/test_switch.py | 8 ++++++ tests/components/mqtt/test_tag.py | 8 ++++++ tests/components/mqtt/test_trigger.py | 9 ++++++- 29 files changed, 281 insertions(+), 10 deletions(-) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 4ea41aa4c64..f4a72829046 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -30,6 +30,7 @@ from homeassistant.const import ( STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNKNOWN, + Platform, ) from homeassistant.setup import async_setup_component @@ -112,6 +113,15 @@ DEFAULT_CONFIG_REMOTE_CODE_TEXT = { } +@pytest.fixture(autouse=True) +def alarm_control_panel_platform_only(): + """Only setup the alarm_control_panel platform to speed up tests.""" + with patch( + "homeassistant.components.mqtt.PLATFORMS", [Platform.ALARM_CONTROL_PANEL] + ): + yield + + async def test_fail_setup_without_state_topic(hass, mqtt_mock_entry_no_yaml_config): """Test for failing with no state topic.""" with assert_setup_component(0, alarm_control_panel.DOMAIN) as config: diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 0561e9fe056..20037a88d1c 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, + Platform, ) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -62,6 +63,13 @@ DEFAULT_CONFIG = { } +@pytest.fixture(autouse=True) +def binary_sensor_platform_only(): + """Only setup the binary_sensor platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]): + yield + + async def test_setting_sensor_value_expires_availability_topic( hass, mqtt_mock_entry_with_yaml_config, caplog ): diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 9fa16aa33bb..8748ef3be4d 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -5,7 +5,12 @@ from unittest.mock import patch import pytest from homeassistant.components import button -from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + STATE_UNKNOWN, + Platform, +) from homeassistant.setup import async_setup_component from .test_common import ( @@ -41,6 +46,13 @@ DEFAULT_CONFIG = { } +@pytest.fixture(autouse=True) +def button_platform_only(): + """Only setup the button platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BUTTON]): + yield + + @pytest.mark.freeze_time("2021-11-08 13:31:44+00:00") async def test_sending_mqtt_commands(hass, mqtt_mock_entry_with_yaml_config): """Test the sending MQTT commands.""" diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index f219178859b..84bf4181a2c 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -9,6 +9,7 @@ import pytest from homeassistant.components import camera from homeassistant.components.mqtt.camera import MQTT_CAMERA_ATTRIBUTES_BLOCKED +from homeassistant.const import Platform from homeassistant.setup import async_setup_component from .test_common import ( @@ -46,6 +47,13 @@ DEFAULT_CONFIG = { } +@pytest.fixture(autouse=True) +def camera_platform_only(): + """Only setup the camera platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.CAMERA]): + yield + + async def test_run_camera_setup( hass, hass_client_no_auth, mqtt_mock_entry_with_yaml_config ): diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 35eec9aa1ff..c633f267e76 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -26,7 +26,7 @@ from homeassistant.components.climate.const import ( HVACMode, ) from homeassistant.components.mqtt.climate import MQTT_CLIMATE_ATTRIBUTES_BLOCKED -from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.const import ATTR_TEMPERATURE, Platform from homeassistant.setup import async_setup_component from .test_common import ( @@ -106,6 +106,13 @@ DEFAULT_LEGACY_CONFIG = { } +@pytest.fixture(autouse=True) +def climate_platform_only(): + """Only setup the climate platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.CLIMATE]): + yield + + async def test_setup_params(hass, mqtt_mock_entry_with_yaml_config): """Test the initial parameters.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index c2406aef9b4..c0d63cec1b4 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -43,6 +43,7 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, STATE_UNKNOWN, + Platform, ) from homeassistant.setup import async_setup_component @@ -83,6 +84,13 @@ DEFAULT_CONFIG = { } +@pytest.fixture(autouse=True) +def cover_platform_only(): + """Only setup the cover platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.COVER]): + yield + + async def test_state_via_state_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index c731280f575..a9eb9b20825 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -1,8 +1,10 @@ """The tests for the MQTT device tracker platform using configuration.yaml.""" from unittest.mock import patch +import pytest + from homeassistant.components.device_tracker.const import DOMAIN, SOURCE_TYPE_BLUETOOTH -from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME +from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME, Platform from homeassistant.setup import async_setup_component from .test_common import help_test_setup_manual_entity_from_yaml @@ -10,6 +12,13 @@ from .test_common import help_test_setup_manual_entity_from_yaml from tests.common import async_fire_mqtt_message +@pytest.fixture(autouse=True) +def device_tracker_platform_only(): + """Only setup the device_tracker platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.DEVICE_TRACKER]): + yield + + # Deprecated in HA Core 2022.6 async def test_legacy_ensure_device_tracker_platform_validation( hass, mqtt_mock_entry_with_yaml_config diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py index 31853ad1dee..ac4058c9372 100644 --- a/tests/components/mqtt/test_device_tracker_discovery.py +++ b/tests/components/mqtt/test_device_tracker_discovery.py @@ -1,11 +1,13 @@ """The tests for the MQTT device_tracker discovery platform.""" +from unittest.mock import patch + import pytest from homeassistant.components import device_tracker from homeassistant.components.mqtt.const import DOMAIN as MQTT_DOMAIN from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED -from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN +from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN, Platform from homeassistant.setup import async_setup_component from .test_common import help_test_setting_blocked_attribute_via_mqtt_json_message @@ -21,6 +23,13 @@ DEFAULT_CONFIG = { } +@pytest.fixture(autouse=True) +def device_tracker_platform_only(): + """Only setup the device_tracker platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.DEVICE_TRACKER]): + yield + + @pytest.fixture def device_reg(hass): """Return an empty, loaded, registry.""" diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index fe08c85a853..842e1dc4106 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -1,11 +1,13 @@ """The tests for MQTT device triggers.""" import json +from unittest.mock import patch import pytest import homeassistant.components.automation as automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.mqtt import _LOGGER, DOMAIN, debug_info +from homeassistant.const import Platform from homeassistant.helpers import device_registry as dr from homeassistant.helpers.trigger import async_initialize_triggers from homeassistant.setup import async_setup_component @@ -39,6 +41,16 @@ def calls(hass): return async_mock_service(hass, "test", "automation") +@pytest.fixture(autouse=True) +def binary_sensor_and_sensor_only(): + """Only setup the binary_sensor and sensor platform to speed up tests.""" + with patch( + "homeassistant.components.mqtt.PLATFORMS", + [Platform.BINARY_SENSOR, Platform.SENSOR], + ): + yield + + async def test_get_triggers( hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config ): diff --git a/tests/components/mqtt/test_diagnostics.py b/tests/components/mqtt/test_diagnostics.py index 65399a22f70..8cc5d0b1070 100644 --- a/tests/components/mqtt/test_diagnostics.py +++ b/tests/components/mqtt/test_diagnostics.py @@ -1,11 +1,12 @@ """Test MQTT diagnostics.""" import json -from unittest.mock import ANY +from unittest.mock import ANY, patch import pytest from homeassistant.components import mqtt +from homeassistant.const import Platform from tests.common import async_fire_mqtt_message, mock_device_registry from tests.components.diagnostics import ( @@ -31,6 +32,16 @@ default_config = { } +@pytest.fixture(autouse=True) +def device_tracker_sensor_only(): + """Only setup the device_tracker and sensor platforms to speed up tests.""" + with patch( + "homeassistant.components.mqtt.PLATFORMS", + [Platform.DEVICE_TRACKER, Platform.SENSOR], + ): + yield + + @pytest.fixture def device_reg(hass): """Return an empty, loaded, registry.""" diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index df20dc031d0..d185d3334d0 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -18,6 +18,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, + Platform, ) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -65,6 +66,7 @@ async def test_subscribing_config_topic(hass, mqtt_mock_entry_no_yaml_config): assert discovery_topic + "/+/+/+/config" in topics +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) @pytest.mark.parametrize( "topic, log", [ @@ -92,6 +94,7 @@ async def test_invalid_topic(hass, mqtt_mock_entry_no_yaml_config, caplog, topic caplog.clear() +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_invalid_json(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test sending in invalid JSON.""" await mqtt_mock_entry_no_yaml_config() @@ -131,6 +134,7 @@ async def test_only_valid_components(hass, mqtt_mock_entry_no_yaml_config, caplo assert not mock_dispatcher_send.called +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_correct_config_discovery(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test sending in correct JSON.""" await mqtt_mock_entry_no_yaml_config() @@ -148,6 +152,7 @@ async def test_correct_config_discovery(hass, mqtt_mock_entry_no_yaml_config, ca assert ("binary_sensor", "bla") in hass.data[ALREADY_DISCOVERED] +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.FAN]) async def test_discover_fan(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test discovering an MQTT fan.""" await mqtt_mock_entry_no_yaml_config() @@ -165,6 +170,7 @@ async def test_discover_fan(hass, mqtt_mock_entry_no_yaml_config, caplog): assert ("fan", "bla") in hass.data[ALREADY_DISCOVERED] +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.CLIMATE]) async def test_discover_climate(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test discovering an MQTT climate component.""" await mqtt_mock_entry_no_yaml_config() @@ -184,6 +190,7 @@ async def test_discover_climate(hass, mqtt_mock_entry_no_yaml_config, caplog): assert ("climate", "bla") in hass.data[ALREADY_DISCOVERED] +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.ALARM_CONTROL_PANEL]) async def test_discover_alarm_control_panel( hass, mqtt_mock_entry_no_yaml_config, caplog ): @@ -365,6 +372,7 @@ async def test_discovery_with_object_id( assert (domain, "object bla") in hass.data[ALREADY_DISCOVERED] +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_discovery_incl_nodeid(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test sending in correct JSON with optional node_id included.""" await mqtt_mock_entry_no_yaml_config() @@ -382,6 +390,7 @@ async def test_discovery_incl_nodeid(hass, mqtt_mock_entry_no_yaml_config, caplo assert ("binary_sensor", "my_node_id bla") in hass.data[ALREADY_DISCOVERED] +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_non_duplicate_discovery(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test for a non duplicate component.""" await mqtt_mock_entry_no_yaml_config() @@ -406,6 +415,7 @@ async def test_non_duplicate_discovery(hass, mqtt_mock_entry_no_yaml_config, cap assert "Component has already been discovered: binary_sensor bla" in caplog.text +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of component through empty discovery message.""" await mqtt_mock_entry_no_yaml_config() @@ -424,6 +434,7 @@ async def test_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): assert state is None +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_rediscover(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test rediscover of removed component.""" await mqtt_mock_entry_no_yaml_config() @@ -451,6 +462,7 @@ async def test_rediscover(hass, mqtt_mock_entry_no_yaml_config, caplog): assert state is not None +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_rapid_rediscover(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test immediate rediscover of removed component.""" await mqtt_mock_entry_no_yaml_config() @@ -500,6 +512,7 @@ async def test_rapid_rediscover(hass, mqtt_mock_entry_no_yaml_config, caplog): assert events[4].data["old_state"] is None +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_rapid_rediscover_unique(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test immediate rediscover of removed component.""" await mqtt_mock_entry_no_yaml_config() @@ -559,6 +572,7 @@ async def test_rapid_rediscover_unique(hass, mqtt_mock_entry_no_yaml_config, cap assert events[3].data["old_state"] is None +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_rapid_reconfigure(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test immediate reconfigure of added component.""" await mqtt_mock_entry_no_yaml_config() @@ -611,6 +625,7 @@ async def test_rapid_reconfigure(hass, mqtt_mock_entry_no_yaml_config, caplog): assert events[2].data["new_state"].attributes["friendly_name"] == "Wine" +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) async def test_duplicate_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test for a non duplicate component.""" await mqtt_mock_entry_no_yaml_config() @@ -688,6 +703,7 @@ async def test_cleanup_device( ) +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) async def test_cleanup_device_mqtt( hass, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config ): @@ -730,6 +746,7 @@ async def test_cleanup_device_mqtt( mqtt_mock.async_publish.assert_not_called() +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) async def test_cleanup_device_multiple_config_entries( hass, hass_ws_client, device_reg, entity_reg, mqtt_mock_entry_no_yaml_config ): @@ -905,6 +922,7 @@ async def test_cleanup_device_multiple_config_entries_mqtt( mqtt_mock.async_publish.assert_not_called() +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SWITCH]) async def test_discovery_expansion(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test expansion of abbreviated discovery payload.""" await mqtt_mock_entry_no_yaml_config() @@ -963,6 +981,7 @@ async def test_discovery_expansion(hass, mqtt_mock_entry_no_yaml_config, caplog) assert state.state == STATE_UNAVAILABLE +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SWITCH]) async def test_discovery_expansion_2(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test expansion of abbreviated discovery payload.""" await mqtt_mock_entry_no_yaml_config() @@ -1003,6 +1022,7 @@ async def test_discovery_expansion_2(hass, mqtt_mock_entry_no_yaml_config, caplo assert state.state == STATE_UNKNOWN +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SWITCH]) @pytest.mark.no_fail_on_log_exception async def test_discovery_expansion_3(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test expansion of broken discovery payload.""" @@ -1084,6 +1104,7 @@ async def test_discovery_expansion_without_encoding_and_value_template_1( assert state.state == STATE_UNAVAILABLE +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SWITCH]) async def test_discovery_expansion_without_encoding_and_value_template_2( hass, mqtt_mock_entry_no_yaml_config, caplog ): @@ -1190,6 +1211,7 @@ async def test_missing_discover_abbreviations( assert not missing +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SWITCH]) async def test_no_implicit_state_topic_switch( hass, mqtt_mock_entry_no_yaml_config, caplog ): @@ -1214,6 +1236,7 @@ async def test_no_implicit_state_topic_switch( assert state.state == STATE_UNKNOWN +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) @pytest.mark.parametrize( "mqtt_config", [ @@ -1242,6 +1265,7 @@ async def test_complex_discovery_topic_prefix( assert ("binary_sensor", "node1 object1") in hass.data[ALREADY_DISCOVERED] +@patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_mqtt_integration_discovery_subscribe_unsubscribe( hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config ): @@ -1283,6 +1307,7 @@ async def test_mqtt_integration_discovery_subscribe_unsubscribe( assert not mqtt_client_mock.unsubscribe.called +@patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_mqtt_discovery_unsubscribe_once( hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config ): diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 6a4920ef45a..b9ca5e3888d 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -28,6 +28,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, + Platform, ) from homeassistant.setup import async_setup_component @@ -74,6 +75,13 @@ DEFAULT_CONFIG = { } +@pytest.fixture(autouse=True) +def fan_platform_only(): + """Only setup the fan platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.FAN]): + yield + + async def test_fail_setup_if_no_command_topic( hass, caplog, mqtt_mock_entry_no_yaml_config ): diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index cbbf69e2947..e1e757762df 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -29,6 +29,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, + Platform, ) from homeassistant.setup import async_setup_component @@ -75,6 +76,13 @@ DEFAULT_CONFIG = { } +@pytest.fixture(autouse=True) +def humidifer_platform_only(): + """Only setup the humidifer platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.HUMIDIFIER]): + yield + + async def async_turn_on( hass, entity_id=ENTITY_MATCH_ALL, diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 474bc03f876..b75def64834 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -22,6 +22,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, + Platform, ) import homeassistant.core as ha from homeassistant.core import CoreState, HomeAssistant, callback @@ -51,6 +52,16 @@ class RecordCallsPartial(partial): __name__ = "RecordCallPartialTest" +@pytest.fixture(autouse=True) +def sensor_platforms_only(): + """Only setup the sensor platforms to speed up tests.""" + with patch( + "homeassistant.components.mqtt.PLATFORMS", + [Platform.SENSOR, Platform.BINARY_SENSOR], + ): + yield + + @pytest.fixture(autouse=True) def mock_storage(hass_storage): """Autouse hass_storage for the TestCase tests.""" @@ -1362,6 +1373,7 @@ async def test_setup_override_configuration(hass, caplog, tmp_path): assert calls_username_password_set[0][1] == "somepassword" +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) async def test_setup_manual_mqtt_with_platform_key(hass, caplog): """Test set up a manual MQTT item with a platform key.""" config = {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} @@ -1372,6 +1384,7 @@ async def test_setup_manual_mqtt_with_platform_key(hass, caplog): ) +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) async def test_setup_manual_mqtt_with_invalid_config(hass, caplog): """Test set up a manual MQTT item with an invalid config.""" config = {"name": "test"} @@ -2013,6 +2026,7 @@ async def test_mqtt_ws_get_device_debug_info( assert response["result"] == expected_result +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.CAMERA]) async def test_mqtt_ws_get_device_debug_info_binary( hass, device_reg, hass_ws_client, mqtt_mock_entry_no_yaml_config ): diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 0371153d750..224e81781cf 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -29,7 +29,7 @@ from homeassistant.components.vacuum import ( ATTR_STATUS, VacuumEntityFeature, ) -from homeassistant.const import CONF_NAME, CONF_PLATFORM, STATE_OFF, STATE_ON +from homeassistant.const import CONF_NAME, CONF_PLATFORM, STATE_OFF, STATE_ON, Platform from homeassistant.setup import async_setup_component from .test_common import ( @@ -89,6 +89,13 @@ DEFAULT_CONFIG = { DEFAULT_CONFIG_2 = {vacuum.DOMAIN: {"platform": "mqtt", "name": "test"}} +@pytest.fixture(autouse=True) +def vacuum_platform_only(): + """Only setup the vacuum platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.VACUUM]): + yield + + async def test_default_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test that the correct supported features.""" assert await async_setup_component( diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 7ca10683ed7..4d8d8f24a3c 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -209,6 +209,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, + Platform, ) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -251,6 +252,13 @@ DEFAULT_CONFIG = { } +@pytest.fixture(autouse=True) +def light_platform_only(): + """Only setup the light platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]): + yield + + async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_config): """Test if command fails with command topic.""" assert await async_setup_component( diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 3e1eb10d717..64733b4f0f7 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -103,6 +103,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, + Platform, ) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -150,6 +151,13 @@ DEFAULT_CONFIG = { } +@pytest.fixture(autouse=True) +def light_platform_only(): + """Only setup the light platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]): + yield + + class JsonValidator: """Helper to compare JSON.""" diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index fa2ef113976..6e271d08651 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -41,6 +41,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, + Platform, ) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -90,6 +91,13 @@ DEFAULT_CONFIG = { } +@pytest.fixture(autouse=True) +def light_platform_only(): + """Only setup the light platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]): + yield + + @pytest.mark.parametrize( "test_config", [ diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 0353702152f..1bf4183e60f 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -18,6 +18,7 @@ from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + Platform, ) from homeassistant.setup import async_setup_component @@ -58,6 +59,13 @@ DEFAULT_CONFIG = { } +@pytest.fixture(autouse=True) +def lock_platform_only(): + """Only setup the lock platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LOCK]): + yield + + async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 648499b8751..ea79c5cd7aa 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -23,6 +23,7 @@ from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, + Platform, ) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -64,6 +65,13 @@ DEFAULT_CONFIG = { } +@pytest.fixture(autouse=True) +def number_platform_only(): + """Only setup the number platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.NUMBER]): + yield + + async def test_run_number_setup(hass, mqtt_mock_entry_with_yaml_config): """Test that it fetches the given payload.""" topic = "test/number" diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index 305389ba72d..3036565dad5 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -5,7 +5,7 @@ from unittest.mock import patch import pytest from homeassistant.components import scene -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, STATE_UNKNOWN, Platform import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -34,6 +34,13 @@ DEFAULT_CONFIG = { } +@pytest.fixture(autouse=True) +def scene_platform_only(): + """Only setup the scene platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SCENE]): + yield + + async def test_sending_mqtt_commands(hass, mqtt_mock_entry_with_yaml_config): """Test the sending MQTT commands.""" fake_state = ha.State("scene.test", STATE_UNKNOWN) diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index 5b02c4f3a31..c22bd43b86f 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -13,7 +13,12 @@ from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) -from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_ENTITY_ID, + STATE_UNKNOWN, + Platform, +) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -59,6 +64,13 @@ DEFAULT_CONFIG = { } +@pytest.fixture(autouse=True) +def select_platform_only(): + """Only setup the select platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SELECT]): + yield + + async def test_run_select_setup(hass, mqtt_mock_entry_with_yaml_config): """Test that it fetches the given payload.""" topic = "test/select" diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 0cc275c9d1f..f30bcf43392 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -14,6 +14,7 @@ from homeassistant.const import ( STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, + Platform, ) import homeassistant.core as ha from homeassistant.helpers import device_registry as dr @@ -72,6 +73,13 @@ DEFAULT_CONFIG = { } +@pytest.fixture(autouse=True) +def sensor_platform_only(): + """Only setup the sensor platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]): + yield + + async def test_setting_sensor_value_via_mqtt_message( hass, mqtt_mock_entry_with_yaml_config ): diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index 1dce382421e..c3916acb34b 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -15,6 +15,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, + Platform, ) from homeassistant.setup import async_setup_component @@ -55,6 +56,13 @@ DEFAULT_CONFIG = { } +@pytest.fixture(autouse=True) +def siren_platform_only(): + """Only setup the siren platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SIREN]): + yield + + async def async_turn_on(hass, entity_id=ENTITY_MATCH_ALL, parameters={}) -> None: """Turn all or specified siren on.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 64545d2c140..b0b89c28646 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -31,6 +31,7 @@ from homeassistant.const import ( CONF_PLATFORM, ENTITY_MATCH_ALL, STATE_UNKNOWN, + Platform, ) from homeassistant.setup import async_setup_component @@ -87,6 +88,13 @@ DEFAULT_CONFIG_2 = { } +@pytest.fixture(autouse=True) +def vacuum_platform_only(): + """Only setup the vacuum platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.VACUUM]): + yield + + async def test_default_supported_features(hass, mqtt_mock_entry_with_yaml_config): """Test that the correct supported features.""" assert await async_setup_component( diff --git a/tests/components/mqtt/test_subscription.py b/tests/components/mqtt/test_subscription.py index 7c1663b9c09..3be66f0aa90 100644 --- a/tests/components/mqtt/test_subscription.py +++ b/tests/components/mqtt/test_subscription.py @@ -1,5 +1,7 @@ """The tests for the MQTT subscription component.""" -from unittest.mock import ANY +from unittest.mock import ANY, patch + +import pytest from homeassistant.components.mqtt.subscription import ( async_prepare_subscribe_topics, @@ -11,6 +13,13 @@ from homeassistant.core import callback from tests.common import async_fire_mqtt_message +@pytest.fixture(autouse=True) +def no_platforms(): + """Skip platform setup to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", []): + yield + + async def test_subscribe_topics(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test subscription to topics.""" await mqtt_mock_entry_no_yaml_config() diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 1ed8db34d8d..ba23efc859c 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -11,6 +11,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, + Platform, ) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -53,6 +54,13 @@ DEFAULT_CONFIG = { } +@pytest.fixture(autouse=True) +def switch_platform_only(): + """Only setup the switch platform to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SWITCH]): + yield + + async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling state via topic.""" assert await async_setup_component( diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index 09be31011f2..f06dd6f5244 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -7,6 +7,7 @@ import pytest from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.mqtt.const import DOMAIN as MQTT_DOMAIN +from homeassistant.const import Platform from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -42,6 +43,13 @@ DEFAULT_TAG_SCAN_JSON = ( ) +@pytest.fixture(autouse=True) +def binary_sensor_only(): + """Only setup the binary_sensor platform to speed up test.""" + with patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]): + yield + + @pytest.fixture def device_reg(hass): """Return an empty, loaded, registry.""" diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py index a4079558c34..4c0a70707eb 100644 --- a/tests/components/mqtt/test_trigger.py +++ b/tests/components/mqtt/test_trigger.py @@ -1,5 +1,5 @@ """The tests for the MQTT automation.""" -from unittest.mock import ANY +from unittest.mock import ANY, patch import pytest @@ -17,6 +17,13 @@ def calls(hass): return async_mock_service(hass, "test", "automation") +@pytest.fixture(autouse=True) +def no_platforms(): + """Skip platform setup to speed up tests.""" + with patch("homeassistant.components.mqtt.PLATFORMS", []): + yield + + @pytest.fixture(autouse=True) async def setup_comp(hass, mqtt_mock_entry_no_yaml_config): """Initialize components.""" From bb8b51eda3e3605df7db7cd92153d6c03745c802 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Jun 2022 10:56:08 -1000 Subject: [PATCH 1470/3516] Fix typos in ConfigEntryState.recoverable (#73449) --- homeassistant/config_entries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index a8b2752d2aa..2aa5b1b8c62 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -106,7 +106,7 @@ class ConfigEntryState(Enum): def recoverable(self) -> bool: """Get if the state is recoverable. - If the entry is state is recoverable, unloads + If the entry state is recoverable, unloads and reloads are allowed. """ return self._recoverable From 08b55939fbc5333a374846e6ec2bdfa1ac5ca70f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 13 Jun 2022 11:33:29 -1000 Subject: [PATCH 1471/3516] Avoid creating executor job in requirements if another call satisfied the requirement (#73451) --- homeassistant/requirements.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 9a8cd20983a..bd06cf61e4b 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -231,9 +231,9 @@ class RequirementsManager: async with self.pip_lock: # Recaculate missing again now that we have the lock - await self._async_process_requirements( - name, self._find_missing_requirements(requirements) - ) + missing = self._find_missing_requirements(requirements) + if missing: + await self._async_process_requirements(name, missing) def _find_missing_requirements(self, requirements: list[str]) -> list[str]: """Find requirements that are missing in the cache.""" From 4005af99aa8aa5aeb41f63a9c4ec7aa78174ec6e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 14 Jun 2022 00:26:59 +0000 Subject: [PATCH 1472/3516] [ci skip] Translation update --- .../airvisual/translations/sensor.sv.json | 7 ++++ .../components/asuswrt/translations/sv.json | 2 +- .../aurora_abb_powerone/translations/sv.json | 17 ++++++++++ .../azure_devops/translations/sv.json | 11 +++++++ .../binary_sensor/translations/sv.json | 8 +++-- .../cloudflare/translations/bg.json | 5 +++ .../components/cover/translations/sv.json | 4 +-- .../components/daikin/translations/bg.json | 3 +- .../dialogflow/translations/sv.json | 3 ++ .../components/efergy/translations/sv.json | 17 ++++++++++ .../eight_sleep/translations/ca.json | 19 +++++++++++ .../eight_sleep/translations/no.json | 19 +++++++++++ .../eight_sleep/translations/zh-Hant.json | 19 +++++++++++ .../components/elkm1/translations/bg.json | 3 ++ .../components/energy/translations/sv.json | 3 ++ .../enphase_envoy/translations/bg.json | 7 ++++ .../components/flume/translations/bg.json | 3 ++ .../forecast_solar/translations/sv.json | 11 +++++++ .../components/group/translations/bg.json | 5 +++ .../homewizard/translations/sv.json | 9 +++++ .../translations/sv.json | 2 +- .../components/kmtronic/translations/bg.json | 1 + .../components/meater/translations/sv.json | 3 ++ .../met_eireann/translations/bg.json | 11 +++++++ .../components/mill/translations/sv.json | 11 +++++++ .../components/motioneye/translations/bg.json | 11 +++++++ .../components/mysensors/translations/sv.json | 7 ++++ .../components/nest/translations/sv.json | 3 +- .../components/netatmo/translations/bg.json | 9 +++++ .../components/nuheat/translations/bg.json | 7 ++++ .../components/nut/translations/bg.json | 1 + .../components/octoprint/translations/sv.json | 11 +++++++ .../ovo_energy/translations/sv.json | 17 ++++++++++ .../panasonic_viera/translations/bg.json | 1 + .../components/picnic/translations/bg.json | 3 ++ .../components/powerwall/translations/sv.json | 9 +++-- .../components/rfxtrx/translations/bg.json | 3 ++ .../components/rpi_power/translations/sv.json | 12 +++++++ .../components/samsungtv/translations/sv.json | 2 +- .../components/select/translations/sv.json | 7 ++++ .../components/sensor/translations/sv.json | 33 +++++++++++++++---- .../srp_energy/translations/sv.json | 16 +++++++++ .../components/subaru/translations/bg.json | 3 ++ .../synology_dsm/translations/bg.json | 1 + .../components/threshold/translations/sv.json | 16 +++++++++ .../totalconnect/translations/bg.json | 3 +- .../components/vesync/translations/bg.json | 3 ++ .../waze_travel_time/translations/bg.json | 3 +- .../wolflink/translations/sensor.sv.json | 1 + .../xiaomi_miio/translations/sv.json | 11 +++++++ .../components/zwave_js/translations/bg.json | 1 + .../components/zwave_js/translations/sv.json | 7 ++++ 52 files changed, 383 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/airvisual/translations/sensor.sv.json create mode 100644 homeassistant/components/aurora_abb_powerone/translations/sv.json create mode 100644 homeassistant/components/azure_devops/translations/sv.json create mode 100644 homeassistant/components/efergy/translations/sv.json create mode 100644 homeassistant/components/eight_sleep/translations/ca.json create mode 100644 homeassistant/components/eight_sleep/translations/no.json create mode 100644 homeassistant/components/eight_sleep/translations/zh-Hant.json create mode 100644 homeassistant/components/energy/translations/sv.json create mode 100644 homeassistant/components/enphase_envoy/translations/bg.json create mode 100644 homeassistant/components/forecast_solar/translations/sv.json create mode 100644 homeassistant/components/homewizard/translations/sv.json create mode 100644 homeassistant/components/met_eireann/translations/bg.json create mode 100644 homeassistant/components/mill/translations/sv.json create mode 100644 homeassistant/components/motioneye/translations/bg.json create mode 100644 homeassistant/components/mysensors/translations/sv.json create mode 100644 homeassistant/components/nuheat/translations/bg.json create mode 100644 homeassistant/components/octoprint/translations/sv.json create mode 100644 homeassistant/components/ovo_energy/translations/sv.json create mode 100644 homeassistant/components/rpi_power/translations/sv.json create mode 100644 homeassistant/components/select/translations/sv.json create mode 100644 homeassistant/components/srp_energy/translations/sv.json create mode 100644 homeassistant/components/threshold/translations/sv.json create mode 100644 homeassistant/components/xiaomi_miio/translations/sv.json create mode 100644 homeassistant/components/zwave_js/translations/sv.json diff --git a/homeassistant/components/airvisual/translations/sensor.sv.json b/homeassistant/components/airvisual/translations/sensor.sv.json new file mode 100644 index 00000000000..f1fa0bbdcd8 --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.sv.json @@ -0,0 +1,7 @@ +{ + "state": { + "airvisual__pollutant_label": { + "co": "Kolmonoxid" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/sv.json b/homeassistant/components/asuswrt/translations/sv.json index 057a107356a..4e7196b1b21 100644 --- a/homeassistant/components/asuswrt/translations/sv.json +++ b/homeassistant/components/asuswrt/translations/sv.json @@ -31,7 +31,7 @@ "step": { "init": { "data": { - "consider_home": "Sekunder att v\u00e4nta tills attt en enhet anses borta", + "consider_home": "Sekunder att v\u00e4nta tills att en enhet anses borta", "dnsmasq": "Platsen i routern f\u00f6r dnsmasq.leases-filerna", "interface": "Gr\u00e4nssnittet som du vill ha statistik fr\u00e5n (t.ex. eth0, eth1 etc)", "require_ip": "Enheterna m\u00e5ste ha IP (f\u00f6r accesspunktsl\u00e4ge)", diff --git a/homeassistant/components/aurora_abb_powerone/translations/sv.json b/homeassistant/components/aurora_abb_powerone/translations/sv.json new file mode 100644 index 00000000000..361fc8bbbb7 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "no_serial_ports": "Inga com portar funna. M\u00e5ste ha en RS485 enhet f\u00f6r att kommunicera" + }, + "error": { + "cannot_open_serial_port": "Kan inte \u00f6ppna serieporten, kontrollera och f\u00f6rs\u00f6k igen." + }, + "step": { + "user": { + "data": { + "port": "RS485 eller USB-RS485 adapter port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_devops/translations/sv.json b/homeassistant/components/azure_devops/translations/sv.json new file mode 100644 index 00000000000..e87d9570334 --- /dev/null +++ b/homeassistant/components/azure_devops/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "project": "Projekt" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/sv.json b/homeassistant/components/binary_sensor/translations/sv.json index eb23c7f12c6..58f97e77977 100644 --- a/homeassistant/components/binary_sensor/translations/sv.json +++ b/homeassistant/components/binary_sensor/translations/sv.json @@ -36,9 +36,10 @@ "is_on": "{entity_name} \u00e4r p\u00e5", "is_open": "{entity_name} \u00e4r \u00f6ppen", "is_plugged_in": "{entity_name} \u00e4r ansluten", - "is_powered": "{entity_name} \u00e4r str\u00f6mf\u00f6rd", + "is_powered": "{entity_name} \u00e4r p\u00e5slagen", "is_present": "{entity_name} \u00e4r n\u00e4rvarande", "is_problem": "{entity_name} uppt\u00e4cker problem", + "is_running": "{entity_name} k\u00f6rs", "is_smoke": "{entity_name} detekterar r\u00f6k", "is_sound": "{entity_name} uppt\u00e4cker ljud", "is_unsafe": "{entity_name} \u00e4r os\u00e4ker", @@ -72,13 +73,13 @@ "not_occupied": "{entity_name} blev inte upptagen", "not_opened": "{entity_name} st\u00e4ngd", "not_plugged_in": "{entity_name} urkopplad", - "not_powered": "{entity_name} inte str\u00f6mf\u00f6rd", + "not_powered": "{entity_name} inte p\u00e5slagen", "not_present": "{entity_name} inte n\u00e4rvarande", "not_unsafe": "{entity_name} blev s\u00e4ker", "occupied": "{entity_name} blev upptagen", "opened": "{entity_name} \u00f6ppnades", "plugged_in": "{entity_name} ansluten", - "powered": "{entity_name} str\u00f6mf\u00f6rd", + "powered": "{entity_name} p\u00e5slagen", "present": "{entity_name} n\u00e4rvarande", "problem": "{entity_name} b\u00f6rjade uppt\u00e4cka problem", "smoke": "{entity_name} b\u00f6rjade detektera r\u00f6k", @@ -90,6 +91,7 @@ } }, "device_class": { + "heat": "v\u00e4rme", "motion": "r\u00f6relse", "power": "effekt" }, diff --git a/homeassistant/components/cloudflare/translations/bg.json b/homeassistant/components/cloudflare/translations/bg.json index ec50ba10dc8..84593a40a00 100644 --- a/homeassistant/components/cloudflare/translations/bg.json +++ b/homeassistant/components/cloudflare/translations/bg.json @@ -11,6 +11,11 @@ }, "flow_title": "{name}", "step": { + "records": { + "data": { + "records": "\u0417\u0430\u043f\u0438\u0441\u0438" + } + }, "user": { "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Cloudflare" }, diff --git a/homeassistant/components/cover/translations/sv.json b/homeassistant/components/cover/translations/sv.json index a9509740330..624b5102d82 100644 --- a/homeassistant/components/cover/translations/sv.json +++ b/homeassistant/components/cover/translations/sv.json @@ -9,8 +9,8 @@ "is_closing": "{entity_name} st\u00e4ngs", "is_open": "{entity_name} \u00e4r \u00f6ppen", "is_opening": "{entity_name} \u00f6ppnas", - "is_position": "Aktuell position f\u00f6r {entity_name} \u00e4r", - "is_tilt_position": "Aktuell {entity_name} lutningsposition \u00e4r" + "is_position": "Nuvarande position f\u00f6r {entity_name} \u00e4r", + "is_tilt_position": "Nuvarande {entity_name} lutningsposition \u00e4r" }, "trigger_type": { "closed": "{entity_name} st\u00e4ngd", diff --git a/homeassistant/components/daikin/translations/bg.json b/homeassistant/components/daikin/translations/bg.json index b2d1963e4cb..a07f37ab8d5 100644 --- a/homeassistant/components/daikin/translations/bg.json +++ b/homeassistant/components/daikin/translations/bg.json @@ -5,7 +5,8 @@ "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." + "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.", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/translations/sv.json b/homeassistant/components/dialogflow/translations/sv.json index 9642b4b7bec..ebae7e612d0 100644 --- a/homeassistant/components/dialogflow/translations/sv.json +++ b/homeassistant/components/dialogflow/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "webhook_not_internet_accessible": "Din Home Assistant instans m\u00e5ste kunna n\u00e5s fr\u00e5n Internet f\u00f6r att ta emot webhook meddelanden" + }, "create_entry": { "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera [webhook funktionen i Dialogflow]({dialogflow_url}).\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSe [dokumentation]({docs_url}) om hur du konfigurerar detta f\u00f6r mer information." }, diff --git a/homeassistant/components/efergy/translations/sv.json b/homeassistant/components/efergy/translations/sv.json new file mode 100644 index 00000000000..b163c1a520b --- /dev/null +++ b/homeassistant/components/efergy/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, + "step": { + "user": { + "data": { + "api_key": "API nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/translations/ca.json b/homeassistant/components/eight_sleep/translations/ca.json new file mode 100644 index 00000000000..0abb283ce08 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "No es pot connectar amb el n\u00favol d'Eight Sleep: {error}" + }, + "error": { + "cannot_connect": "No es pot connectar amb el n\u00favol d'Eight Sleep: {error}" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/translations/no.json b/homeassistant/components/eight_sleep/translations/no.json new file mode 100644 index 00000000000..715f27e2075 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Kan ikke koble til Eight Sleep-skyen: {error}" + }, + "error": { + "cannot_connect": "Kan ikke koble til Eight Sleep-skyen: {error}" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/translations/zh-Hant.json b/homeassistant/components/eight_sleep/translations/zh-Hant.json new file mode 100644 index 00000000000..cda9624d5f0 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Eight Sleep cloud\uff1a{error}" + }, + "error": { + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Eight Sleep cloud\uff1a{error}" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/bg.json b/homeassistant/components/elkm1/translations/bg.json index 5e83523d419..46a60e96408 100644 --- a/homeassistant/components/elkm1/translations/bg.json +++ b/homeassistant/components/elkm1/translations/bg.json @@ -5,6 +5,9 @@ "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": { + "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": "{mac_address} ({host})", "step": { "discovered_connection": { diff --git a/homeassistant/components/energy/translations/sv.json b/homeassistant/components/energy/translations/sv.json new file mode 100644 index 00000000000..168ae4ae877 --- /dev/null +++ b/homeassistant/components/energy/translations/sv.json @@ -0,0 +1,3 @@ +{ + "title": "Energi" +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/bg.json b/homeassistant/components/enphase_envoy/translations/bg.json new file mode 100644 index 00000000000..c0ccf23f5b5 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/bg.json @@ -0,0 +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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/bg.json b/homeassistant/components/flume/translations/bg.json index 6eca91e8ed2..14aa8f088f3 100644 --- a/homeassistant/components/flume/translations/bg.json +++ b/homeassistant/components/flume/translations/bg.json @@ -9,6 +9,9 @@ "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { + "reauth_confirm": { + "description": "\u041f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0437\u0430 {username} \u0432\u0435\u0447\u0435 \u043d\u0435 \u0435 \u0432\u0430\u043b\u0438\u0434\u043d\u0430." + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/forecast_solar/translations/sv.json b/homeassistant/components/forecast_solar/translations/sv.json new file mode 100644 index 00000000000..fceb441190b --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "modules power": "Total maxeffekt (Watt) p\u00e5 dina solpaneler" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/bg.json b/homeassistant/components/group/translations/bg.json index 6be0657c774..d0982fcfb66 100644 --- a/homeassistant/components/group/translations/bg.json +++ b/homeassistant/components/group/translations/bg.json @@ -8,6 +8,11 @@ }, "title": "\u041d\u043e\u0432\u0430 \u0433\u0440\u0443\u043f\u0430" }, + "fan": { + "data": { + "name": "\u0418\u043c\u0435" + } + }, "lock": { "data": { "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435", diff --git a/homeassistant/components/homewizard/translations/sv.json b/homeassistant/components/homewizard/translations/sv.json new file mode 100644 index 00000000000..c9bc9cd66a9 --- /dev/null +++ b/homeassistant/components/homewizard/translations/sv.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Konfigurera enhet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/sv.json b/homeassistant/components/hunterdouglas_powerview/translations/sv.json index 04371b16514..e572ec2c4a7 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/sv.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/sv.json @@ -9,7 +9,7 @@ }, "step": { "link": { - "description": "Do vill du konfigurera {name} ({host})?", + "description": "Vill du konfigurera {name} ({host})?", "title": "Anslut till PowerView Hub" }, "user": { diff --git a/homeassistant/components/kmtronic/translations/bg.json b/homeassistant/components/kmtronic/translations/bg.json index a84e1c3bfdf..d152ddfcf20 100644 --- a/homeassistant/components/kmtronic/translations/bg.json +++ b/homeassistant/components/kmtronic/translations/bg.json @@ -1,6 +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", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { diff --git a/homeassistant/components/meater/translations/sv.json b/homeassistant/components/meater/translations/sv.json index 383fbbeb5a6..b920ba3abde 100644 --- a/homeassistant/components/meater/translations/sv.json +++ b/homeassistant/components/meater/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "service_unavailable_error": "Programmeringsgr\u00e4nssnittet g\u00e5r inte att komma \u00e5t f\u00f6r n\u00e4rvarande. F\u00f6rs\u00f6k igen senare." + }, "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/met_eireann/translations/bg.json b/homeassistant/components/met_eireann/translations/bg.json new file mode 100644 index 00000000000..35cfa0ad1d7 --- /dev/null +++ b/homeassistant/components/met_eireann/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/mill/translations/sv.json b/homeassistant/components/mill/translations/sv.json new file mode 100644 index 00000000000..cd5effd10d3 --- /dev/null +++ b/homeassistant/components/mill/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "connection_type": "V\u00e4lj anslutningstyp" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/bg.json b/homeassistant/components/motioneye/translations/bg.json new file mode 100644 index 00000000000..02c83a6e916 --- /dev/null +++ b/homeassistant/components/motioneye/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/sv.json b/homeassistant/components/mysensors/translations/sv.json new file mode 100644 index 00000000000..fbbcbdff5e6 --- /dev/null +++ b/homeassistant/components/mysensors/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_serial": "Ogiltig serieport" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/sv.json b/homeassistant/components/nest/translations/sv.json index d929451e504..e0fef47aaec 100644 --- a/homeassistant/components/nest/translations/sv.json +++ b/homeassistant/components/nest/translations/sv.json @@ -27,7 +27,8 @@ }, "device_automation": { "trigger_type": { - "camera_motion": "R\u00f6relse uppt\u00e4ckt" + "camera_motion": "R\u00f6relse uppt\u00e4ckt", + "camera_person": "Person detekterad" } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/bg.json b/homeassistant/components/netatmo/translations/bg.json index 95a038871be..30cbd4c7167 100644 --- a/homeassistant/components/netatmo/translations/bg.json +++ b/homeassistant/components/netatmo/translations/bg.json @@ -16,5 +16,14 @@ "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" } } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "\u0418\u043c\u0435 \u043d\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u0442\u0430" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/bg.json b/homeassistant/components/nuheat/translations/bg.json new file mode 100644 index 00000000000..5d274ec2b73 --- /dev/null +++ b/homeassistant/components/nuheat/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "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/nut/translations/bg.json b/homeassistant/components/nut/translations/bg.json index 4983c9a14b2..09f0ff26e5d 100644 --- a/homeassistant/components/nut/translations/bg.json +++ b/homeassistant/components/nut/translations/bg.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/octoprint/translations/sv.json b/homeassistant/components/octoprint/translations/sv.json new file mode 100644 index 00000000000..e17feb4bbe6 --- /dev/null +++ b/homeassistant/components/octoprint/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ssl": "Anv\u00e4nd SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/sv.json b/homeassistant/components/ovo_energy/translations/sv.json new file mode 100644 index 00000000000..054280346d3 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "reauth": { + "data": { + "password": "L\u00f6senord" + } + }, + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/bg.json b/homeassistant/components/panasonic_viera/translations/bg.json index 9dc5d863d85..6433e60193d 100644 --- a/homeassistant/components/panasonic_viera/translations/bg.json +++ b/homeassistant/components/panasonic_viera/translations/bg.json @@ -2,6 +2,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_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \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": { diff --git a/homeassistant/components/picnic/translations/bg.json b/homeassistant/components/picnic/translations/bg.json index c0ccf23f5b5..ffb593eb287 100644 --- a/homeassistant/components/picnic/translations/bg.json +++ b/homeassistant/components/picnic/translations/bg.json @@ -2,6 +2,9 @@ "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": { + "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/powerwall/translations/sv.json b/homeassistant/components/powerwall/translations/sv.json index 0c6f94cd697..01b1eccd5c0 100644 --- a/homeassistant/components/powerwall/translations/sv.json +++ b/homeassistant/components/powerwall/translations/sv.json @@ -1,14 +1,19 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { - "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "cannot_connect": "Det gick inte att ansluta", + "invalid_auth": "Felaktig autentisering", "unknown": "Ov\u00e4ntat fel", "wrong_version": "Powerwall anv\u00e4nder en programvaruversion som inte st\u00f6ds. T\u00e4nk p\u00e5 att uppgradera eller rapportera det h\u00e4r problemet s\u00e5 att det kan l\u00f6sas." }, "step": { "user": { "data": { - "ip_address": "IP-adress" + "ip_address": "IP-adress", + "password": "L\u00f6senord" } } } diff --git a/homeassistant/components/rfxtrx/translations/bg.json b/homeassistant/components/rfxtrx/translations/bg.json index c03ec99553e..dec08bcfaa5 100644 --- a/homeassistant/components/rfxtrx/translations/bg.json +++ b/homeassistant/components/rfxtrx/translations/bg.json @@ -35,6 +35,9 @@ "data": { "protocols": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0438" } + }, + "set_device_options": { + "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043e\u043f\u0446\u0438\u0438\u0442\u0435 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e" } } } diff --git a/homeassistant/components/rpi_power/translations/sv.json b/homeassistant/components/rpi_power/translations/sv.json new file mode 100644 index 00000000000..0ca2f1f5748 --- /dev/null +++ b/homeassistant/components/rpi_power/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Bara en konfiguration \u00e4r till\u00e5ten." + }, + "step": { + "confirm": { + "description": "Vill du b\u00f6rja med inst\u00e4llning?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/sv.json b/homeassistant/components/samsungtv/translations/sv.json index e9c0803c865..0141800c5c0 100644 --- a/homeassistant/components/samsungtv/translations/sv.json +++ b/homeassistant/components/samsungtv/translations/sv.json @@ -5,7 +5,7 @@ "already_in_progress": "Samsung TV-konfiguration p\u00e5g\u00e5r redan.", "auth_missing": "Home Assistant har inte beh\u00f6righet att ansluta till denna Samsung TV. Kontrollera tv:ns inst\u00e4llningar f\u00f6r att godk\u00e4nna Home Assistant.", "id_missing": "Denna Samsung-enhet har inget serienummer.", - "not_supported": "Denna Samsung TV-enhet st\u00f6ds f\u00f6r n\u00e4rvarande inte.", + "not_supported": "Denna Samsung enhet st\u00f6ds f\u00f6r n\u00e4rvarande inte.", "unknown": "Ov\u00e4ntat fel" }, "flow_title": "{device}", diff --git a/homeassistant/components/select/translations/sv.json b/homeassistant/components/select/translations/sv.json new file mode 100644 index 00000000000..d388cb6c622 --- /dev/null +++ b/homeassistant/components/select/translations/sv.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "condition_type": { + "selected_option": "Nuvarande {entity_name} markerad option" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/sv.json b/homeassistant/components/sensor/translations/sv.json index eeec1090a90..49c49b16c69 100644 --- a/homeassistant/components/sensor/translations/sv.json +++ b/homeassistant/components/sensor/translations/sv.json @@ -1,25 +1,44 @@ { "device_automation": { "condition_type": { - "is_battery_level": "Aktuell {entity_name} batteriniv\u00e5", - "is_humidity": "Aktuell {entity_name} fuktighet", - "is_illuminance": "Aktuell {entity_name} belysning", - "is_power": "Aktuell {entity_name} effekt", + "is_battery_level": "Nuvarande {entity_name} batteriniv\u00e5", + "is_carbon_dioxide": "Nuvarande {entity_name} koncentration av koldioxid", + "is_carbon_monoxide": "Nuvarande {entity_name} koncentration av kolmonoxid", + "is_current": "Nuvarande", + "is_energy": "Nuvarande {entity_name} energi", + "is_frequency": "Nuvarande frekvens", + "is_humidity": "Nuvarande {entity_name} fuktighet", + "is_illuminance": "Nuvarande {entity_name} belysning", + "is_nitrogen_dioxide": "Nuvarande {entity_name} koncentration av kv\u00e4vedioxid", + "is_nitrogen_monoxide": "Nuvarande {entity_name} koncentration av kv\u00e4veoxid", + "is_ozone": "Nuvarande {entity_name} koncentration av ozon", + "is_pm1": "Nuvarande {entity_name} koncentration av PM1 partiklar", + "is_pm10": "Nuvarande {entity_name} koncentration av PM10 partiklar", + "is_pm25": "Nuvarande {entity_name} koncentration av PM2.5 partiklar", + "is_power": "Nuvarande {entity_name} effekt", + "is_power_factor": "Nuvarande {entity_name} effektfaktor", "is_pressure": "Aktuellt {entity_name} tryck", - "is_signal_strength": "Aktuell {entity_name} signalstyrka", + "is_reactive_power": "Nuvarande {entity_name} reaktiv effekt", + "is_signal_strength": "Nuvarande {entity_name} signalstyrka", "is_temperature": "Aktuell {entity_name} temperatur", - "is_value": "Aktuellt {entity_name} v\u00e4rde" + "is_value": "Nuvarande {entity_name} v\u00e4rde", + "is_volatile_organic_compounds": "Nuvarande {entity_name} koncentration av flyktiga organiska \u00e4mnen", + "is_voltage": "Nuvarande {entity_name} sp\u00e4nning" }, "trigger_type": { "battery_level": "{entity_name} batteriniv\u00e5 \u00e4ndras", + "energy": "Energif\u00f6r\u00e4ndringar", "humidity": "{entity_name} fuktighet \u00e4ndras", "illuminance": "{entity_name} belysning \u00e4ndras", "power": "{entity_name} effektf\u00f6r\u00e4ndringar", "power_factor": "effektfaktorf\u00f6r\u00e4ndringar", "pressure": "{entity_name} tryckf\u00f6r\u00e4ndringar", + "reactive_power": "{entity_name} reaktiv effekt\u00e4ndring", "signal_strength": "{entity_name} signalstyrka \u00e4ndras", "temperature": "{entity_name} temperaturf\u00f6r\u00e4ndringar", - "value": "{entity_name} v\u00e4rde \u00e4ndras" + "value": "{entity_name} v\u00e4rde \u00e4ndras", + "volatile_organic_compounds": "{entity_name} koncentrations\u00e4ndringar av flyktiga organiska \u00e4mnen", + "voltage": "{entity_name} sp\u00e4nningsf\u00f6r\u00e4ndringar" } }, "state": { diff --git a/homeassistant/components/srp_energy/translations/sv.json b/homeassistant/components/srp_energy/translations/sv.json new file mode 100644 index 00000000000..880970c74ff --- /dev/null +++ b/homeassistant/components/srp_energy/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan inte ansluta", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/bg.json b/homeassistant/components/subaru/translations/bg.json index 00b879eca0d..a3c6d55e3e9 100644 --- a/homeassistant/components/subaru/translations/bg.json +++ b/homeassistant/components/subaru/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "incorrect_validation_code": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u0435\u043d \u043a\u043e\u0434 \u0437\u0430 \u0432\u0430\u043b\u0438\u0434\u0438\u0440\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/bg.json b/homeassistant/components/synology_dsm/translations/bg.json index a3a107a36e2..dcd0a5ab730 100644 --- a/homeassistant/components/synology_dsm/translations/bg.json +++ b/homeassistant/components/synology_dsm/translations/bg.json @@ -6,6 +6,7 @@ "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": { + "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", "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" }, diff --git a/homeassistant/components/threshold/translations/sv.json b/homeassistant/components/threshold/translations/sv.json new file mode 100644 index 00000000000..613b2c25412 --- /dev/null +++ b/homeassistant/components/threshold/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "need_lower_upper": "Undre och \u00f6vre gr\u00e4ns kan inte vara tomma" + } + }, + "options": { + "step": { + "init": { + "data": { + "lower": "Undre gr\u00e4ns" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/bg.json b/homeassistant/components/totalconnect/translations/bg.json index 1858bd74b7b..e5aed3bb504 100644 --- a/homeassistant/components/totalconnect/translations/bg.json +++ b/homeassistant/components/totalconnect/translations/bg.json @@ -1,7 +1,8 @@ { "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" + "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" }, "step": { "locations": { diff --git a/homeassistant/components/vesync/translations/bg.json b/homeassistant/components/vesync/translations/bg.json index bb496b3422a..c435a669d5a 100644 --- a/homeassistant/components/vesync/translations/bg.json +++ b/homeassistant/components/vesync/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/waze_travel_time/translations/bg.json b/homeassistant/components/waze_travel_time/translations/bg.json index 35cfa0ad1d7..f7d35259c93 100644 --- a/homeassistant/components/waze_travel_time/translations/bg.json +++ b/homeassistant/components/waze_travel_time/translations/bg.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "name": "\u0418\u043c\u0435" + "name": "\u0418\u043c\u0435", + "region": "\u0420\u0435\u0433\u0438\u043e\u043d" } } } diff --git a/homeassistant/components/wolflink/translations/sensor.sv.json b/homeassistant/components/wolflink/translations/sensor.sv.json index 7b55b80227e..ddfd466dce2 100644 --- a/homeassistant/components/wolflink/translations/sensor.sv.json +++ b/homeassistant/components/wolflink/translations/sensor.sv.json @@ -3,6 +3,7 @@ "wolflink__state": { "aktiviert": "Aktiverad", "aus": "Inaktiverad", + "sparen": "Ekonomi", "test": "Test" } } diff --git a/homeassistant/components/xiaomi_miio/translations/sv.json b/homeassistant/components/xiaomi_miio/translations/sv.json new file mode 100644 index 00000000000..17b92cb5058 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "cloud": { + "data": { + "cloud_password": "Molnl\u00f6senord" + } + } + } + } +} \ 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 5d2486acbc1..dd6d70483d3 100644 --- a/homeassistant/components/zwave_js/translations/bg.json +++ b/homeassistant/components/zwave_js/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \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}", diff --git a/homeassistant/components/zwave_js/translations/sv.json b/homeassistant/components/zwave_js/translations/sv.json new file mode 100644 index 00000000000..ecbacaaa3b3 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/sv.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "condition_type": { + "value": "Nuvarande v\u00e4rde f\u00f6r ett Z-Wave v\u00e4rde" + } + } +} \ No newline at end of file From e3b6c7a66fb580838792be72854e79478b704fb9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 14 Jun 2022 08:25:11 +0200 Subject: [PATCH 1473/3516] Add Home Assistant Yellow integration (#73272) Co-authored-by: Martin Hjelmare Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 2 + homeassistant/components/hassio/__init__.py | 1 + .../homeassistant_yellow/__init__.py | 35 ++++++ .../homeassistant_yellow/config_flow.py | 22 ++++ .../components/homeassistant_yellow/const.py | 3 + .../homeassistant_yellow/hardware.py | 34 ++++++ .../homeassistant_yellow/manifest.json | 9 ++ homeassistant/components/zha/config_flow.py | 51 ++++++++- homeassistant/components/zha/manifest.json | 2 +- script/hassfest/manifest.py | 1 + tests/components/hassio/test_init.py | 1 + .../homeassistant_yellow/__init__.py | 1 + .../homeassistant_yellow/conftest.py | 14 +++ .../homeassistant_yellow/test_config_flow.py | 58 ++++++++++ .../homeassistant_yellow/test_hardware.py | 89 +++++++++++++++ .../homeassistant_yellow/test_init.py | 84 ++++++++++++++ tests/components/zha/test_config_flow.py | 104 ++++++++++++++++++ 17 files changed, 509 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/homeassistant_yellow/__init__.py create mode 100644 homeassistant/components/homeassistant_yellow/config_flow.py create mode 100644 homeassistant/components/homeassistant_yellow/const.py create mode 100644 homeassistant/components/homeassistant_yellow/hardware.py create mode 100644 homeassistant/components/homeassistant_yellow/manifest.json create mode 100644 tests/components/homeassistant_yellow/__init__.py create mode 100644 tests/components/homeassistant_yellow/conftest.py create mode 100644 tests/components/homeassistant_yellow/test_config_flow.py create mode 100644 tests/components/homeassistant_yellow/test_hardware.py create mode 100644 tests/components/homeassistant_yellow/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 9a57f3e791a..2d4fb5ceebf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -447,6 +447,8 @@ build.json @home-assistant/supervisor /tests/components/home_plus_control/ @chemaaa /homeassistant/components/homeassistant/ @home-assistant/core /tests/components/homeassistant/ @home-assistant/core +/homeassistant/components/homeassistant_yellow/ @home-assistant/core +/tests/components/homeassistant_yellow/ @home-assistant/core /homeassistant/components/homekit/ @bdraco /tests/components/homekit/ @bdraco /homeassistant/components/homekit_controller/ @Jc2k @bdraco diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 0df29a6153b..cd3c704d4c9 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -215,6 +215,7 @@ HARDWARE_INTEGRATIONS = { "rpi3-64": "raspberry_pi", "rpi4": "raspberry_pi", "rpi4-64": "raspberry_pi", + "yellow": "homeassistant_yellow", } diff --git a/homeassistant/components/homeassistant_yellow/__init__.py b/homeassistant/components/homeassistant_yellow/__init__.py new file mode 100644 index 00000000000..89a73ab769a --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/__init__.py @@ -0,0 +1,35 @@ +"""The Home Assistant Yellow integration.""" +from __future__ import annotations + +from homeassistant.components.hassio import get_os_info +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a Home Assistant Yellow config entry.""" + if (os_info := get_os_info(hass)) is None: + # The hassio integration has not yet fetched data from the supervisor + raise ConfigEntryNotReady + + board: str | None + if (board := os_info.get("board")) is None or not board == "yellow": + # Not running on a Home Assistant Yellow, Home Assistant may have been migrated + hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) + return False + + await hass.config_entries.flow.async_init( + "zha", + context={"source": "hardware"}, + data={ + "radio_type": "efr32", + "port": { + "path": "/dev/ttyAMA1", + "baudrate": 115200, + "flow_control": "hardware", + }, + }, + ) + + return True diff --git a/homeassistant/components/homeassistant_yellow/config_flow.py b/homeassistant/components/homeassistant_yellow/config_flow.py new file mode 100644 index 00000000000..191a28f47a4 --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/config_flow.py @@ -0,0 +1,22 @@ +"""Config flow for the Home Assistant Yellow integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class HomeAssistantYellowConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Home Assistant Yellow.""" + + VERSION = 1 + + async def async_step_system(self, data: dict[str, Any] | None = None) -> FlowResult: + """Handle the initial step.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + return self.async_create_entry(title="Home Assistant Yellow", data={}) diff --git a/homeassistant/components/homeassistant_yellow/const.py b/homeassistant/components/homeassistant_yellow/const.py new file mode 100644 index 00000000000..41eae70b3f2 --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/const.py @@ -0,0 +1,3 @@ +"""Constants for the Home Assistant Yellow integration.""" + +DOMAIN = "homeassistant_yellow" diff --git a/homeassistant/components/homeassistant_yellow/hardware.py b/homeassistant/components/homeassistant_yellow/hardware.py new file mode 100644 index 00000000000..aa1fe4b745b --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/hardware.py @@ -0,0 +1,34 @@ +"""The Home Assistant Yellow hardware platform.""" +from __future__ import annotations + +from homeassistant.components.hardware.models import BoardInfo, HardwareInfo +from homeassistant.components.hassio import get_os_info +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError + +BOARD_NAME = "Home Assistant Yellow" +MANUFACTURER = "homeassistant" +MODEL = "yellow" + + +@callback +def async_info(hass: HomeAssistant) -> HardwareInfo: + """Return board info.""" + if (os_info := get_os_info(hass)) is None: + raise HomeAssistantError + board: str | None + if (board := os_info.get("board")) is None: + raise HomeAssistantError + if not board == "yellow": + raise HomeAssistantError + + return HardwareInfo( + board=BoardInfo( + hassio_board_id=board, + manufacturer=MANUFACTURER, + model=MODEL, + revision=None, + ), + name=BOARD_NAME, + url=None, + ) diff --git a/homeassistant/components/homeassistant_yellow/manifest.json b/homeassistant/components/homeassistant_yellow/manifest.json new file mode 100644 index 00000000000..47e6c8e2cd8 --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "homeassistant_yellow", + "name": "Home Assistant Yellow", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/homeassistant_yellow", + "dependencies": ["hardware", "hassio"], + "codeowners": ["@home-assistant/core"], + "integration_type": "hardware" +} diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index e116954cdcb..1832424587b 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -8,7 +8,7 @@ import voluptuous as vol from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from homeassistant import config_entries -from homeassistant.components import usb, zeroconf +from homeassistant.components import onboarding, usb, zeroconf from homeassistant.const import CONF_NAME from homeassistant.data_entry_flow import FlowResult @@ -36,6 +36,7 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize flow instance.""" self._device_path = None + self._device_settings = None self._radio_type = None self._title = None @@ -242,6 +243,54 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_hardware(self, data=None): + """Handle hardware flow.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + if not data: + return self.async_abort(reason="invalid_hardware_data") + if data.get("radio_type") != "efr32": + return self.async_abort(reason="invalid_hardware_data") + self._radio_type = RadioType.ezsp.name + app_cls = RadioType[self._radio_type].controller + + schema = { + vol.Required( + CONF_DEVICE_PATH, default=self._device_path or vol.UNDEFINED + ): str + } + radio_schema = app_cls.SCHEMA_DEVICE.schema + assert not isinstance(radio_schema, vol.Schema) + + for param, value in radio_schema.items(): + if param in SUPPORTED_PORT_SETTINGS: + schema[param] = value + try: + self._device_settings = vol.Schema(schema)(data.get("port")) + except vol.Invalid: + return self.async_abort(reason="invalid_hardware_data") + + self._title = data["port"]["path"] + + self._set_confirm_only() + return await self.async_step_confirm_hardware() + + async def async_step_confirm_hardware(self, user_input=None): + """Confirm a hardware discovery.""" + if user_input is not None or not onboarding.async_is_onboarded(self.hass): + return self.async_create_entry( + title=self._title, + data={ + CONF_DEVICE: self._device_settings, + CONF_RADIO_TYPE: self._radio_type, + }, + ) + + return self.async_show_form( + step_id="confirm_hardware", + description_placeholders={CONF_NAME: self._title}, + ) + async def detect_radios(dev_path: str) -> dict[str, Any] | None: """Probe all radio types on the device port.""" diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 023af7a8a0e..71131f240a9 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -87,7 +87,7 @@ "name": "*zigate*" } ], - "after_dependencies": ["usb", "zeroconf"], + "after_dependencies": ["onboarding", "usb", "zeroconf"], "iot_class": "local_polling", "loggers": [ "aiosqlite", diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 7f2e8e0d477..0cd20364533 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -56,6 +56,7 @@ NO_IOT_CLASS = [ "hardware", "history", "homeassistant", + "homeassistant_yellow", "image", "input_boolean", "input_button", diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 1569e834562..6ac3debe3d8 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -732,6 +732,7 @@ async def test_coordinator_updates(hass, caplog): ({"board": "rpi3-64"}, "raspberry_pi"), ({"board": "rpi4"}, "raspberry_pi"), ({"board": "rpi4-64"}, "raspberry_pi"), + ({"board": "yellow"}, "homeassistant_yellow"), ], ) async def test_setup_hardware_integration(hass, aioclient_mock, integration): diff --git a/tests/components/homeassistant_yellow/__init__.py b/tests/components/homeassistant_yellow/__init__.py new file mode 100644 index 00000000000..a03eed7b9b2 --- /dev/null +++ b/tests/components/homeassistant_yellow/__init__.py @@ -0,0 +1 @@ +"""Tests for the Home Assistant Yellow integration.""" diff --git a/tests/components/homeassistant_yellow/conftest.py b/tests/components/homeassistant_yellow/conftest.py new file mode 100644 index 00000000000..8700e361dc8 --- /dev/null +++ b/tests/components/homeassistant_yellow/conftest.py @@ -0,0 +1,14 @@ +"""Test fixtures for the Home Assistant Yellow integration.""" +from unittest.mock import patch + +import pytest + + +@pytest.fixture(autouse=True) +def mock_zha(): + """Mock the zha integration.""" + with patch( + "homeassistant.components.zha.async_setup_entry", + return_value=True, + ): + yield diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py new file mode 100644 index 00000000000..2e96b05a919 --- /dev/null +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -0,0 +1,58 @@ +"""Test the Home Assistant Yellow config flow.""" +from unittest.mock import patch + +from homeassistant.components.homeassistant_yellow.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY + +from tests.common import MockConfigEntry, MockModule, mock_integration + + +async def test_config_flow(hass: HomeAssistant) -> None: + """Test the config flow.""" + mock_integration(hass, MockModule("hassio")) + + with patch( + "homeassistant.components.homeassistant_yellow.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "system"} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Home Assistant Yellow" + assert result["data"] == {} + assert result["options"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == {} + assert config_entry.title == "Home Assistant Yellow" + + +async def test_config_flow_single_entry(hass: HomeAssistant) -> None: + """Test only a single entry is allowed.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Yellow", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_yellow.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "system"} + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + mock_setup_entry.assert_not_called() diff --git a/tests/components/homeassistant_yellow/test_hardware.py b/tests/components/homeassistant_yellow/test_hardware.py new file mode 100644 index 00000000000..28403334ec1 --- /dev/null +++ b/tests/components/homeassistant_yellow/test_hardware.py @@ -0,0 +1,89 @@ +"""Test the Home Assistant Yellow hardware platform.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.homeassistant_yellow.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, MockModule, mock_integration + + +async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can get the board info.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Yellow", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value={"board": "yellow"}, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.homeassistant_yellow.hardware.get_os_info", + return_value={"board": "yellow"}, + ): + await client.send_json({"id": 1, "type": "hardware/info"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] == { + "hardware": [ + { + "board": { + "hassio_board_id": "yellow", + "manufacturer": "homeassistant", + "model": "yellow", + "revision": None, + }, + "name": "Home Assistant Yellow", + "url": None, + } + ] + } + + +@pytest.mark.parametrize("os_info", [None, {"board": None}, {"board": "other"}]) +async def test_hardware_info_fail(hass: HomeAssistant, hass_ws_client, os_info) -> None: + """Test async_info raises if os_info is not as expected.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Yellow", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value={"board": "yellow"}, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + with patch( + "homeassistant.components.homeassistant_yellow.hardware.get_os_info", + return_value=os_info, + ): + await client.send_json({"id": 1, "type": "hardware/info"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] == {"hardware": []} diff --git a/tests/components/homeassistant_yellow/test_init.py b/tests/components/homeassistant_yellow/test_init.py new file mode 100644 index 00000000000..308c392ea26 --- /dev/null +++ b/tests/components/homeassistant_yellow/test_init.py @@ -0,0 +1,84 @@ +"""Test the Home Assistant Yellow integration.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.homeassistant_yellow.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, MockModule, mock_integration + + +@pytest.mark.parametrize( + "onboarded, num_entries, num_flows", ((False, 1, 0), (True, 0, 1)) +) +async def test_setup_entry( + hass: HomeAssistant, onboarded, num_entries, num_flows +) -> None: + """Test setup of a config entry, including setup of zha.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Yellow", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value={"board": "yellow"}, + ) as mock_get_os_info, patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=onboarded + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + + assert len(hass.config_entries.async_entries("zha")) == num_entries + assert len(hass.config_entries.flow.async_progress_by_handler("zha")) == num_flows + + +async def test_setup_entry_wrong_board(hass: HomeAssistant) -> None: + """Test setup of a config entry with wrong board type.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Yellow", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value={"board": "generic-x86-64"}, + ) as mock_get_os_info: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + + +async def test_setup_entry_wait_hassio(hass: HomeAssistant) -> None: + """Test setup of a config entry when hassio has not fetched os_info.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Yellow", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value=None, + ) as mock_get_os_info: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + assert config_entry.state == ConfigEntryState.SETUP_RETRY diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index dee04165c1e..df68c21b6c0 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -766,3 +766,107 @@ async def test_migration_ti_cc_to_znp(old_type, new_type, hass, config_entry): assert config_entry.version > 2 assert config_entry.data[CONF_RADIO_TYPE] == new_type + + +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +async def test_hardware_not_onboarded(hass): + """Test hardware flow.""" + data = { + "radio_type": "efr32", + "port": { + "path": "/dev/ttyAMA1", + "baudrate": 115200, + "flow_control": "hardware", + }, + } + with patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ): + result = await hass.config_entries.flow.async_init( + "zha", context={"source": "hardware"}, data=data + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "/dev/ttyAMA1" + assert result["data"] == { + CONF_DEVICE: { + CONF_BAUDRATE: 115200, + CONF_FLOWCONTROL: "hardware", + CONF_DEVICE_PATH: "/dev/ttyAMA1", + }, + CONF_RADIO_TYPE: "ezsp", + } + + +@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) +async def test_hardware_onboarded(hass): + """Test hardware flow.""" + data = { + "radio_type": "efr32", + "port": { + "path": "/dev/ttyAMA1", + "baudrate": 115200, + "flow_control": "hardware", + }, + } + with patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=True + ): + result = await hass.config_entries.flow.async_init( + "zha", context={"source": "hardware"}, data=data + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "confirm_hardware" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "/dev/ttyAMA1" + assert result["data"] == { + CONF_DEVICE: { + CONF_BAUDRATE: 115200, + CONF_FLOWCONTROL: "hardware", + CONF_DEVICE_PATH: "/dev/ttyAMA1", + }, + CONF_RADIO_TYPE: "ezsp", + } + + +async def test_hardware_already_setup(hass): + """Test hardware flow -- already setup.""" + + MockConfigEntry( + domain=DOMAIN, data={CONF_DEVICE: {CONF_DEVICE_PATH: "/dev/ttyUSB1"}} + ).add_to_hass(hass) + + data = { + "radio_type": "efr32", + "port": { + "path": "/dev/ttyAMA1", + "baudrate": 115200, + "flow_control": "hardware", + }, + } + result = await hass.config_entries.flow.async_init( + "zha", context={"source": "hardware"}, data=data + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + + +@pytest.mark.parametrize( + "data", (None, {}, {"radio_type": "best_radio"}, {"radio_type": "efr32"}) +) +async def test_hardware_invalid_data(hass, data): + """Test onboarding flow -- invalid data.""" + + result = await hass.config_entries.flow.async_init( + "zha", context={"source": "hardware"}, data=data + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "invalid_hardware_data" From 3da3503673186458014db71289c940643bcd5222 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 14 Jun 2022 09:40:57 +0200 Subject: [PATCH 1474/3516] Add temperature unit conversion support to NumberEntity (#73233) * Add temperature unit conversion to number * Remove type enforcements * Lint * Fix legacy unit_of_measurement * Address review comments * Fix unit_of_measurement, improve test coverage --- homeassistant/components/number/__init__.py | 248 ++++++++- tests/components/number/test_init.py | 499 +++++++++++++++++- .../custom_components/test/number.py | 54 +- 3 files changed, 779 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 47a80f00561..75f98447865 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -1,16 +1,20 @@ """Component to allow numeric input for platforms.""" from __future__ import annotations +from collections.abc import Callable +from contextlib import suppress from dataclasses import dataclass from datetime import timedelta +import inspect import logging +from math import ceil, floor from typing import Any, final import voluptuous as vol from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_MODE +from homeassistant.const import ATTR_MODE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -19,6 +23,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType +from homeassistant.util import temperature as temperature_util from .const import ( ATTR_MAX, @@ -41,6 +46,13 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) _LOGGER = logging.getLogger(__name__) +class NumberDeviceClass(StrEnum): + """Device class for numbers.""" + + # temperature (C/F) + TEMPERATURE = "temperature" + + class NumberMode(StrEnum): """Modes for number entities.""" @@ -49,6 +61,11 @@ class NumberMode(StrEnum): SLIDER = "slider" +UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = { + NumberDeviceClass.TEMPERATURE: temperature_util.convert, +} + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Number entities.""" component = hass.data[DOMAIN] = EntityComponent( @@ -72,7 +89,15 @@ async def async_set_value(entity: NumberEntity, service_call: ServiceCall) -> No raise ValueError( f"Value {value} for {entity.name} is outside valid range {entity.min_value} - {entity.max_value}" ) - await entity.async_set_value(value) + try: + native_value = entity.convert_to_native_value(value) + # Clamp to the native range + native_value = min( + max(native_value, entity.native_min_value), entity.native_max_value + ) + await entity.async_set_native_value(native_value) + except NotImplementedError: + await entity.async_set_value(value) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -93,8 +118,56 @@ class NumberEntityDescription(EntityDescription): max_value: float | None = None min_value: float | None = None + native_max_value: float | None = None + native_min_value: float | None = None + native_unit_of_measurement: str | None = None + native_step: float | None = None step: float | None = None + def __post_init__(self) -> None: + """Post initialisation processing.""" + if ( + self.max_value is not None + or self.min_value is not None + or self.step is not None + or self.unit_of_measurement is not None + ): + caller = inspect.stack()[2] + module = inspect.getmodule(caller[0]) + if module and module.__file__ and "custom_components" in module.__file__: + report_issue = "report it to the custom component author." + else: + report_issue = ( + "create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" + ) + _LOGGER.warning( + "%s is setting deprecated attributes on an instance of " + "NumberEntityDescription, this is not valid and will be unsupported " + "from Home Assistant 2022.10. Please %s", + module.__name__ if module else self.__class__.__name__, + report_issue, + ) + self.native_unit_of_measurement = self.unit_of_measurement + + +def ceil_decimal(value: float, precision: float = 0) -> float: + """Return the ceiling of f with d decimals. + + This is a simple implementation which ignores floating point inexactness. + """ + factor = 10**precision + return ceil(value * factor) / factor + + +def floor_decimal(value: float, precision: float = 0) -> float: + """Return the floor of f with d decimals. + + This is a simple implementation which ignores floating point inexactness. + """ + factor = 10**precision + return floor(value * factor) / factor + class NumberEntity(Entity): """Representation of a Number entity.""" @@ -106,6 +179,12 @@ class NumberEntity(Entity): _attr_step: float _attr_mode: NumberMode = NumberMode.AUTO _attr_value: float + _attr_native_max_value: float + _attr_native_min_value: float + _attr_native_step: float + _attr_native_value: float + _attr_native_unit_of_measurement: str | None + _deprecated_number_entity_reported = False @property def capability_attributes(self) -> dict[str, Any]: @@ -117,40 +196,84 @@ class NumberEntity(Entity): ATTR_MODE: self.mode, } + @property + def native_min_value(self) -> float: + """Return the minimum value.""" + if hasattr(self, "_attr_native_min_value"): + return self._attr_native_min_value + if ( + hasattr(self, "entity_description") + and self.entity_description.native_min_value is not None + ): + return self.entity_description.native_min_value + return DEFAULT_MIN_VALUE + @property def min_value(self) -> float: """Return the minimum value.""" if hasattr(self, "_attr_min_value"): + self._report_deprecated_number_entity() return self._attr_min_value if ( hasattr(self, "entity_description") and self.entity_description.min_value is not None ): + self._report_deprecated_number_entity() return self.entity_description.min_value - return DEFAULT_MIN_VALUE + return self._convert_to_state_value(self.native_min_value, floor_decimal) + + @property + def native_max_value(self) -> float: + """Return the maximum value.""" + if hasattr(self, "_attr_native_max_value"): + return self._attr_native_max_value + if ( + hasattr(self, "entity_description") + and self.entity_description.native_max_value is not None + ): + return self.entity_description.native_max_value + return DEFAULT_MAX_VALUE @property def max_value(self) -> float: """Return the maximum value.""" if hasattr(self, "_attr_max_value"): + self._report_deprecated_number_entity() return self._attr_max_value if ( hasattr(self, "entity_description") and self.entity_description.max_value is not None ): + self._report_deprecated_number_entity() return self.entity_description.max_value - return DEFAULT_MAX_VALUE + return self._convert_to_state_value(self.native_max_value, ceil_decimal) + + @property + def native_step(self) -> float | None: + """Return the increment/decrement step.""" + if hasattr(self, "_attr_native_step"): + return self._attr_native_step + if ( + hasattr(self, "entity_description") + and self.entity_description.native_step is not None + ): + return self.entity_description.native_step + return None @property def step(self) -> float: """Return the increment/decrement step.""" if hasattr(self, "_attr_step"): + self._report_deprecated_number_entity() return self._attr_step if ( hasattr(self, "entity_description") and self.entity_description.step is not None ): + self._report_deprecated_number_entity() return self.entity_description.step + if (native_step := self.native_step) is not None: + return native_step step = DEFAULT_STEP value_range = abs(self.max_value - self.min_value) if value_range != 0: @@ -169,10 +292,59 @@ class NumberEntity(Entity): """Return the entity state.""" return self.value + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit of measurement of the entity, if any.""" + if hasattr(self, "_attr_native_unit_of_measurement"): + return self._attr_native_unit_of_measurement + if hasattr(self, "entity_description"): + return self.entity_description.native_unit_of_measurement + return None + + @property + def unit_of_measurement(self) -> str | None: + """Return the unit of measurement of the entity, after unit conversion.""" + if hasattr(self, "_attr_unit_of_measurement"): + return self._attr_unit_of_measurement + if ( + hasattr(self, "entity_description") + and self.entity_description.unit_of_measurement is not None + ): + return self.entity_description.unit_of_measurement + + native_unit_of_measurement = self.native_unit_of_measurement + + if ( + self.device_class == NumberDeviceClass.TEMPERATURE + and native_unit_of_measurement in (TEMP_CELSIUS, TEMP_FAHRENHEIT) + ): + return self.hass.config.units.temperature_unit + + return native_unit_of_measurement + + @property + def native_value(self) -> float | None: + """Return the value reported by the sensor.""" + return self._attr_native_value + @property def value(self) -> float | None: """Return the entity value to represent the entity state.""" - return self._attr_value + if hasattr(self, "_attr_value"): + self._report_deprecated_number_entity() + return self._attr_value + + if (native_value := self.native_value) is None: + return native_value + return self._convert_to_state_value(native_value, round) + + def set_native_value(self, value: float) -> None: + """Set new value.""" + raise NotImplementedError() + + async def async_set_native_value(self, value: float) -> None: + """Set new value.""" + await self.hass.async_add_executor_job(self.set_native_value, value) def set_value(self, value: float) -> None: """Set new value.""" @@ -181,3 +353,69 @@ class NumberEntity(Entity): async def async_set_value(self, value: float) -> None: """Set new value.""" await self.hass.async_add_executor_job(self.set_value, value) + + def _convert_to_state_value(self, value: float, method: Callable) -> float: + """Convert a value in the number's native unit to the configured unit.""" + + native_unit_of_measurement = self.native_unit_of_measurement + unit_of_measurement = self.unit_of_measurement + device_class = self.device_class + + if ( + native_unit_of_measurement != unit_of_measurement + and device_class in UNIT_CONVERSIONS + ): + assert native_unit_of_measurement + assert unit_of_measurement + + value_s = str(value) + prec = len(value_s) - value_s.index(".") - 1 if "." in value_s else 0 + + # Suppress ValueError (Could not convert value to float) + with suppress(ValueError): + value_new: float = UNIT_CONVERSIONS[device_class]( + value, + native_unit_of_measurement, + unit_of_measurement, + ) + + # Round to the wanted precision + value = method(value_new, prec) + + return value + + def convert_to_native_value(self, value: float) -> float: + """Convert a value to the number's native unit.""" + + native_unit_of_measurement = self.native_unit_of_measurement + unit_of_measurement = self.unit_of_measurement + device_class = self.device_class + + if ( + value is not None + and native_unit_of_measurement != unit_of_measurement + and device_class in UNIT_CONVERSIONS + ): + assert native_unit_of_measurement + assert unit_of_measurement + + value = UNIT_CONVERSIONS[device_class]( + value, + unit_of_measurement, + native_unit_of_measurement, + ) + + return value + + def _report_deprecated_number_entity(self) -> None: + """Report that the number entity has not been upgraded.""" + if not self._deprecated_number_entity_reported: + self._deprecated_number_entity_reported = True + report_issue = self._suggest_report_issue() + _LOGGER.warning( + "Entity %s (%s) is using deprecated NumberEntity features which will " + "be unsupported from Home Assistant Core 2022.10, please %s", + self.entity_id, + type(self), + report_issue, + ) diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 8fdf03a7d7b..ccc6f0da0c5 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -4,19 +4,138 @@ from unittest.mock import MagicMock import pytest from homeassistant.components.number import ( + ATTR_MAX, + ATTR_MIN, ATTR_STEP, ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE, + NumberDeviceClass, NumberEntity, + NumberEntityDescription, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_UNIT_OF_MEASUREMENT, + CONF_PLATFORM, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) -from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM class MockDefaultNumberEntity(NumberEntity): - """Mock NumberEntity device to use in tests.""" + """Mock NumberEntity device to use in tests. + + This class falls back on defaults for min_value, max_value, step. + """ + + @property + def native_value(self): + """Return the current value.""" + return 0.5 + + +class MockNumberEntity(NumberEntity): + """Mock NumberEntity device to use in tests. + + This class customizes min_value, max_value as overridden methods. + Step is calculated based on the smaller max_value and min_value. + """ + + @property + def native_max_value(self) -> float: + """Return the max value.""" + return 0.5 + + @property + def native_min_value(self) -> float: + """Return the min value.""" + return -0.5 + + @property + def native_unit_of_measurement(self): + """Return the current value.""" + return "native_cats" + + @property + def native_value(self): + """Return the current value.""" + return 0.5 + + +class MockNumberEntityAttr(NumberEntity): + """Mock NumberEntity device to use in tests. + + This class customizes min_value, max_value by setting _attr members. + Step is calculated based on the smaller max_value and min_value. + """ + + _attr_native_max_value = 1000.0 + _attr_native_min_value = -1000.0 + _attr_native_step = 100.0 + _attr_native_unit_of_measurement = "native_dogs" + _attr_native_value = 500.0 + + +class MockNumberEntityDescr(NumberEntity): + """Mock NumberEntity device to use in tests. + + This class customizes min_value, max_value by entity description. + Step is calculated based on the smaller max_value and min_value. + """ + + def __init__(self): + """Initialize the clas instance.""" + self.entity_description = NumberEntityDescription( + "test", + native_max_value=10.0, + native_min_value=-10.0, + native_step=2.0, + native_unit_of_measurement="native_rabbits", + ) + + @property + def native_value(self): + """Return the current value.""" + return None + + +class MockDefaultNumberEntityDeprecated(NumberEntity): + """Mock NumberEntity device to use in tests. + + This class falls back on defaults for min_value, max_value, step. + """ + + @property + def native_value(self): + """Return the current value.""" + return 0.5 + + +class MockNumberEntityDeprecated(NumberEntity): + """Mock NumberEntity device to use in tests. + + This class customizes min_value, max_value as overridden methods. + Step is calculated based on the smaller max_value and min_value. + """ + + @property + def max_value(self) -> float: + """Return the max value.""" + return 0.5 + + @property + def min_value(self) -> float: + """Return the min value.""" + return -0.5 + + @property + def unit_of_measurement(self): + """Return the current value.""" + return "cats" @property def value(self): @@ -24,13 +143,36 @@ class MockDefaultNumberEntity(NumberEntity): return 0.5 -class MockNumberEntity(NumberEntity): - """Mock NumberEntity device to use in tests.""" +class MockNumberEntityAttrDeprecated(NumberEntity): + """Mock NumberEntity device to use in tests. - @property - def max_value(self) -> float: - """Return the max value.""" - return 1.0 + This class customizes min_value, max_value by setting _attr members. + Step is calculated based on the smaller max_value and min_value. + """ + + _attr_max_value = 1000.0 + _attr_min_value = -1000.0 + _attr_step = 100.0 + _attr_unit_of_measurement = "dogs" + _attr_value = 500.0 + + +class MockNumberEntityDescrDeprecated(NumberEntity): + """Mock NumberEntity device to use in tests. + + This class customizes min_value, max_value by entity description. + Step is calculated based on the smaller max_value and min_value. + """ + + def __init__(self): + """Initialize the clas instance.""" + self.entity_description = NumberEntityDescription( + "test", + max_value=10.0, + min_value=-10.0, + step=2.0, + unit_of_measurement="rabbits", + ) @property def value(self): @@ -41,12 +183,97 @@ class MockNumberEntity(NumberEntity): async def test_step(hass: HomeAssistant) -> None: """Test the step calculation.""" number = MockDefaultNumberEntity() + number.hass = hass assert number.step == 1.0 number_2 = MockNumberEntity() + number_2.hass = hass assert number_2.step == 0.1 +async def test_attributes(hass: HomeAssistant) -> None: + """Test the attributes.""" + number = MockDefaultNumberEntity() + number.hass = hass + assert number.max_value == 100.0 + assert number.min_value == 0.0 + assert number.step == 1.0 + assert number.unit_of_measurement is None + assert number.value == 0.5 + + number_2 = MockNumberEntity() + number_2.hass = hass + assert number_2.max_value == 0.5 + assert number_2.min_value == -0.5 + assert number_2.step == 0.1 + assert number_2.unit_of_measurement == "native_cats" + assert number_2.value == 0.5 + + number_3 = MockNumberEntityAttr() + number_3.hass = hass + assert number_3.max_value == 1000.0 + assert number_3.min_value == -1000.0 + assert number_3.step == 100.0 + assert number_3.unit_of_measurement == "native_dogs" + assert number_3.value == 500.0 + + number_4 = MockNumberEntityDescr() + number_4.hass = hass + assert number_4.max_value == 10.0 + assert number_4.min_value == -10.0 + assert number_4.step == 2.0 + assert number_4.unit_of_measurement == "native_rabbits" + assert number_4.value is None + + +async def test_attributes_deprecated(hass: HomeAssistant, caplog) -> None: + """Test overriding the deprecated attributes.""" + number = MockDefaultNumberEntityDeprecated() + number.hass = hass + assert number.max_value == 100.0 + assert number.min_value == 0.0 + assert number.step == 1.0 + assert number.unit_of_measurement is None + assert number.value == 0.5 + + number_2 = MockNumberEntityDeprecated() + number_2.hass = hass + assert number_2.max_value == 0.5 + assert number_2.min_value == -0.5 + assert number_2.step == 0.1 + assert number_2.unit_of_measurement == "cats" + assert number_2.value == 0.5 + + number_3 = MockNumberEntityAttrDeprecated() + number_3.hass = hass + assert number_3.max_value == 1000.0 + assert number_3.min_value == -1000.0 + assert number_3.step == 100.0 + assert number_3.unit_of_measurement == "dogs" + assert number_3.value == 500.0 + + number_4 = MockNumberEntityDescrDeprecated() + number_4.hass = hass + assert number_4.max_value == 10.0 + assert number_4.min_value == -10.0 + assert number_4.step == 2.0 + assert number_4.unit_of_measurement == "rabbits" + assert number_4.value == 0.5 + + assert ( + "Entity None () " + "is using deprecated NumberEntity features" in caplog.text + ) + assert ( + "Entity None () " + "is using deprecated NumberEntity features" in caplog.text + ) + assert ( + "tests.components.number.test_init is setting deprecated attributes on an " + "instance of NumberEntityDescription" in caplog.text + ) + + async def test_sync_set_value(hass: HomeAssistant) -> None: """Test if async set_value calls sync set_value.""" number = MockDefaultNumberEntity() @@ -59,9 +286,7 @@ async def test_sync_set_value(hass: HomeAssistant) -> None: assert number.set_value.call_args[0][0] == 42 -async def test_custom_integration_and_validation( - hass: HomeAssistant, enable_custom_integrations: None -) -> None: +async def test_set_value(hass: HomeAssistant, enable_custom_integrations: None) -> None: """Test we can only set valid values.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -79,9 +304,8 @@ async def test_custom_integration_and_validation( {ATTR_VALUE: 60.0, ATTR_ENTITY_ID: "number.test"}, blocking=True, ) - - hass.states.async_set("number.test", 60.0) await hass.async_block_till_done() + state = hass.states.get("number.test") assert state.state == "60.0" @@ -97,3 +321,252 @@ async def test_custom_integration_and_validation( await hass.async_block_till_done() state = hass.states.get("number.test") assert state.state == "60.0" + + +async def test_deprecated_attributes( + hass: HomeAssistant, enable_custom_integrations: None +) -> None: + """Test entity using deprecated attributes.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init(empty=True) + platform.ENTITIES.append(platform.LegacyMockNumberEntity()) + entity = platform.ENTITIES[0] + entity._attr_name = "Test" + entity._attr_max_value = 25 + entity._attr_min_value = -25 + entity._attr_step = 2.5 + entity._attr_value = 51.0 + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + state = hass.states.get("number.test") + assert state.state == "51.0" + assert state.attributes.get(ATTR_MAX) == 25.0 + assert state.attributes.get(ATTR_MIN) == -25.0 + assert state.attributes.get(ATTR_STEP) == 2.5 + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {ATTR_VALUE: 0.0, ATTR_ENTITY_ID: "number.test"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("number.test") + assert state.state == "0.0" + + # test ValueError trigger + with pytest.raises(ValueError): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {ATTR_VALUE: 110.0, ATTR_ENTITY_ID: "number.test"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("number.test") + assert state.state == "0.0" + + +async def test_deprecated_methods( + hass: HomeAssistant, enable_custom_integrations: None +) -> None: + """Test entity using deprecated methods.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init(empty=True) + platform.ENTITIES.append( + platform.LegacyMockNumberEntity( + name="Test", + max_value=25.0, + min_value=-25.0, + step=2.5, + value=51.0, + ) + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + state = hass.states.get("number.test") + assert state.state == "51.0" + assert state.attributes.get(ATTR_MAX) == 25.0 + assert state.attributes.get(ATTR_MIN) == -25.0 + assert state.attributes.get(ATTR_STEP) == 2.5 + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {ATTR_VALUE: 0.0, ATTR_ENTITY_ID: "number.test"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("number.test") + assert state.state == "0.0" + + # test ValueError trigger + with pytest.raises(ValueError): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {ATTR_VALUE: 110.0, ATTR_ENTITY_ID: "number.test"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("number.test") + assert state.state == "0.0" + + +@pytest.mark.parametrize( + "unit_system, native_unit, state_unit, initial_native_value, initial_state_value, " + "updated_native_value, updated_state_value, native_max_value, state_max_value, " + "native_min_value, state_min_value, native_step, state_step", + [ + ( + IMPERIAL_SYSTEM, + TEMP_FAHRENHEIT, + TEMP_FAHRENHEIT, + 100, + 100, + 50, + 50, + 140, + 140, + -9, + -9, + 3, + 3, + ), + ( + IMPERIAL_SYSTEM, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + 38, + 100, + 10, + 50, + 60, + 140, + -23, + -10, + 3, + 3, + ), + ( + METRIC_SYSTEM, + TEMP_FAHRENHEIT, + TEMP_CELSIUS, + 100, + 38, + 50, + 10, + 140, + 60, + -9, + -23, + 3, + 3, + ), + ( + METRIC_SYSTEM, + TEMP_CELSIUS, + TEMP_CELSIUS, + 38, + 38, + 10, + 10, + 60, + 60, + -23, + -23, + 3, + 3, + ), + ], +) +async def test_temperature_conversion( + hass, + enable_custom_integrations, + unit_system, + native_unit, + state_unit, + initial_native_value, + initial_state_value, + updated_native_value, + updated_state_value, + native_max_value, + state_max_value, + native_min_value, + state_min_value, + native_step, + state_step, +): + """Test temperature conversion.""" + hass.config.units = unit_system + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockNumberEntity( + name="Test", + native_max_value=native_max_value, + native_min_value=native_min_value, + native_step=native_step, + native_unit_of_measurement=native_unit, + native_value=initial_native_value, + device_class=NumberDeviceClass.TEMPERATURE, + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert float(state.state) == pytest.approx(float(initial_state_value)) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit + assert state.attributes[ATTR_MAX] == state_max_value + assert state.attributes[ATTR_MIN] == state_min_value + assert state.attributes[ATTR_STEP] == state_step + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {ATTR_VALUE: updated_state_value, ATTR_ENTITY_ID: entity0.entity_id}, + blocking=True, + ) + + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert float(state.state) == pytest.approx(float(updated_state_value)) + assert entity0._values["native_value"] == updated_native_value + + # Set to the minimum value + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {ATTR_VALUE: state_min_value, ATTR_ENTITY_ID: entity0.entity_id}, + blocking=True, + ) + + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert float(state.state) == pytest.approx(float(state_min_value), rel=0.1) + + # Set to the maximum value + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + {ATTR_VALUE: state_max_value, ATTR_ENTITY_ID: entity0.entity_id}, + blocking=True, + ) + + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert float(state.state) == pytest.approx(float(state_max_value), rel=0.1) diff --git a/tests/testing_config/custom_components/test/number.py b/tests/testing_config/custom_components/test/number.py index 93d7783d684..ac397a4d42b 100644 --- a/tests/testing_config/custom_components/test/number.py +++ b/tests/testing_config/custom_components/test/number.py @@ -13,10 +13,55 @@ ENTITIES = [] class MockNumberEntity(MockEntity, NumberEntity): - """Mock Select class.""" + """Mock number class.""" - _attr_value = 50.0 - _attr_step = 1.0 + @property + def native_max_value(self): + """Return the native native_max_value.""" + return self._handle("native_max_value") + + @property + def native_min_value(self): + """Return the native native_min_value.""" + return self._handle("native_min_value") + + @property + def native_step(self): + """Return the native native_step.""" + return self._handle("native_step") + + @property + def native_unit_of_measurement(self): + """Return the native unit_of_measurement.""" + return self._handle("native_unit_of_measurement") + + @property + def native_value(self): + """Return the native value of this sensor.""" + return self._handle("native_value") + + def set_native_value(self, value: float) -> None: + """Change the selected option.""" + self._values["native_value"] = value + + +class LegacyMockNumberEntity(MockEntity, NumberEntity): + """Mock Number class using deprecated features.""" + + @property + def max_value(self): + """Return the native max_value.""" + return self._handle("max_value") + + @property + def min_value(self): + """Return the native min_value.""" + return self._handle("min_value") + + @property + def step(self): + """Return the native step.""" + return self._handle("step") @property def value(self): @@ -25,7 +70,7 @@ class MockNumberEntity(MockEntity, NumberEntity): def set_value(self, value: float) -> None: """Change the selected option.""" - self._attr_value = value + self._values["value"] = value def init(empty=False): @@ -39,6 +84,7 @@ def init(empty=False): MockNumberEntity( name="test", unique_id=UNIQUE_NUMBER, + native_value=50.0, ), ] ) From 65378f19c88679239d5f9d8251fc66d14e6c44b1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 14 Jun 2022 12:21:02 +0200 Subject: [PATCH 1475/3516] Update caldav to 0.9.1 (#73472) --- homeassistant/components/caldav/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index e6945effca4..dc34542dffa 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -2,7 +2,7 @@ "domain": "caldav", "name": "CalDAV", "documentation": "https://www.home-assistant.io/integrations/caldav", - "requirements": ["caldav==0.9.0"], + "requirements": ["caldav==0.9.1"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["caldav", "vobject"] diff --git a/requirements_all.txt b/requirements_all.txt index 424a15040a8..0be8541de46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -460,7 +460,7 @@ btsmarthub_devicelist==0.2.0 buienradar==1.0.5 # homeassistant.components.caldav -caldav==0.9.0 +caldav==0.9.1 # homeassistant.components.circuit circuit-webhook==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f1028446c4..bebbafa0eaa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -345,7 +345,7 @@ bsblan==0.5.0 buienradar==1.0.5 # homeassistant.components.caldav -caldav==0.9.0 +caldav==0.9.1 # homeassistant.components.co2signal co2signal==0.4.2 From 99db2a5afec7697b5bf33ce541de16592a2008d9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 14 Jun 2022 12:21:32 +0200 Subject: [PATCH 1476/3516] Update requests to 2.28.0 (#73406) * Update requests to 2.28.0 * Fix mypy warning * Fix Facebook messenger tests --- homeassistant/components/facebook/notify.py | 3 +-- homeassistant/components/neato/hub.py | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_test.txt | 2 +- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/facebook/notify.py b/homeassistant/components/facebook/notify.py index ea8848a5af2..e205e1a66cc 100644 --- a/homeassistant/components/facebook/notify.py +++ b/homeassistant/components/facebook/notify.py @@ -3,7 +3,6 @@ from http import HTTPStatus import json import logging -from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol @@ -74,7 +73,7 @@ class FacebookNotificationService(BaseNotificationService): BASE_URL, data=json.dumps(body), params=payload, - headers={CONTENT_TYPE: CONTENT_TYPE_JSON}, + headers={"Content-Type": CONTENT_TYPE_JSON}, timeout=10, ) if resp.status_code != HTTPStatus.OK: diff --git a/homeassistant/components/neato/hub.py b/homeassistant/components/neato/hub.py index cb639de4acb..6ee00b2a8b4 100644 --- a/homeassistant/components/neato/hub.py +++ b/homeassistant/components/neato/hub.py @@ -32,7 +32,7 @@ class NeatoHub: def download_map(self, url: str) -> HTTPResponse: """Download a new map image.""" - map_image_data = self.my_neato.get_map_image(url) + map_image_data: HTTPResponse = self.my_neato.get_map_image(url) return map_image_data async def async_update_entry_unique_id(self, entry: ConfigEntry) -> str: diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bec79680e0d..5939a513d00 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -27,7 +27,7 @@ pyserial==3.5 python-slugify==4.0.1 pyudev==0.22.0 pyyaml==6.0 -requests==2.27.1 +requests==2.28.0 scapy==2.4.5 sqlalchemy==1.4.37 typing-extensions>=3.10.0.2,<5.0 diff --git a/pyproject.toml b/pyproject.toml index cf5ac7a37c2..f17015cc9ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ "pip>=21.0,<22.2", "python-slugify==4.0.1", "pyyaml==6.0", - "requests==2.27.1", + "requests==2.28.0", "typing-extensions>=3.10.0.2,<5.0", "voluptuous==0.13.1", "voluptuous-serialize==2.5.0", diff --git a/requirements.txt b/requirements.txt index fe2bf87ad25..ba7c9e4dd13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ cryptography==36.0.2 pip>=21.0,<22.2 python-slugify==4.0.1 pyyaml==6.0 -requests==2.27.1 +requests==2.28.0 typing-extensions>=3.10.0.2,<5.0 voluptuous==0.13.1 voluptuous-serialize==2.5.0 diff --git a/requirements_test.txt b/requirements_test.txt index a3986b8a754..2ccbd6ab440 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -42,6 +42,6 @@ types-pkg-resources==0.1.3 types-python-slugify==0.1.2 types-pytz==2021.1.2 types-PyYAML==5.4.6 -types-requests==2.25.1 +types-requests==2.27.30 types-toml==0.1.5 types-ujson==0.1.1 From 1ef0102f12dc8894903c6941f2b049b570a79300 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Tue, 14 Jun 2022 13:21:35 +0200 Subject: [PATCH 1477/3516] Add active alarm zones as select entity to Overkiz integration (#68997) * Add active zones as select entity * Clean up for PR --- homeassistant/components/overkiz/select.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/homeassistant/components/overkiz/select.py b/homeassistant/components/overkiz/select.py index 74d3b3ba282..8482932e2e4 100644 --- a/homeassistant/components/overkiz/select.py +++ b/homeassistant/components/overkiz/select.py @@ -50,6 +50,17 @@ def _select_option_memorized_simple_volume( return execute_command(OverkizCommand.SET_MEMORIZED_SIMPLE_VOLUME, option) +def _select_option_active_zone( + option: str, execute_command: Callable[..., Awaitable[None]] +) -> Awaitable[None]: + """Change the selected option for Active Zone(s).""" + # Turn alarm off when empty zone is selected + if option == "": + return execute_command(OverkizCommand.ALARM_OFF) + + return execute_command(OverkizCommand.ALARM_ZONE_ON, option) + + SELECT_DESCRIPTIONS: list[OverkizSelectDescription] = [ OverkizSelectDescription( key=OverkizState.CORE_OPEN_CLOSED_PEDESTRIAN, @@ -83,6 +94,14 @@ SELECT_DESCRIPTIONS: list[OverkizSelectDescription] = [ ), entity_category=EntityCategory.CONFIG, ), + # StatefulAlarmController + OverkizSelectDescription( + key=OverkizState.CORE_ACTIVE_ZONES, + name="Active Zones", + icon="mdi:shield-lock", + options=["", "A", "B", "C", "A,B", "B,C", "A,C", "A,B,C"], + select_option=_select_option_active_zone, + ), ] SUPPORTED_STATES = {description.key: description for description in SELECT_DESCRIPTIONS} From 04c60d218327340a1bb71647f1c6a01a38a2cd97 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Tue, 14 Jun 2022 13:27:58 +0200 Subject: [PATCH 1478/3516] Add support for AtlanticPassAPCZoneControl to Overkiz integration (#72384) * Add support for AtlanticPassAPCZoneControl (overkiz) * Remove unneeded comments * Remove supported features * Fix new standards --- .../overkiz/climate_entities/__init__.py | 2 + .../atlantic_pass_apc_zone_control.py | 40 +++++++++++++++++++ homeassistant/components/overkiz/const.py | 1 + 3 files changed, 43 insertions(+) create mode 100644 homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py diff --git a/homeassistant/components/overkiz/climate_entities/__init__.py b/homeassistant/components/overkiz/climate_entities/__init__.py index 0e98b7c7e21..e38a92b755c 100644 --- a/homeassistant/components/overkiz/climate_entities/__init__.py +++ b/homeassistant/components/overkiz/climate_entities/__init__.py @@ -2,7 +2,9 @@ from pyoverkiz.enums.ui import UIWidget from .atlantic_electrical_heater import AtlanticElectricalHeater +from .atlantic_pass_apc_zone_control import AtlanticPassAPCZoneControl WIDGET_TO_CLIMATE_ENTITY = { UIWidget.ATLANTIC_ELECTRICAL_HEATER: AtlanticElectricalHeater, + UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: AtlanticPassAPCZoneControl, } diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py new file mode 100644 index 00000000000..fc1d909390b --- /dev/null +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control.py @@ -0,0 +1,40 @@ +"""Support for Atlantic Pass APC Zone Control.""" +from typing import cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import HVACMode +from homeassistant.components.overkiz.entity import OverkizEntity +from homeassistant.const import TEMP_CELSIUS + +OVERKIZ_TO_HVAC_MODE: dict[str, str] = { + OverkizCommandParam.HEATING: HVACMode.HEAT, + OverkizCommandParam.DRYING: HVACMode.DRY, + OverkizCommandParam.COOLING: HVACMode.COOL, + OverkizCommandParam.STOP: HVACMode.OFF, +} + +HVAC_MODE_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_HVAC_MODE.items()} + + +class AtlanticPassAPCZoneControl(OverkizEntity, ClimateEntity): + """Representation of Atlantic Pass APC Zone Control.""" + + _attr_hvac_modes = [*HVAC_MODE_TO_OVERKIZ] + _attr_temperature_unit = TEMP_CELSIUS + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode.""" + return OVERKIZ_TO_HVAC_MODE[ + cast( + str, self.executor.select_state(OverkizState.IO_PASS_APC_OPERATING_MODE) + ) + ] + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + await self.executor.async_execute_command( + OverkizCommand.SET_PASS_APC_OPERATING_MODE, HVAC_MODE_TO_OVERKIZ[hvac_mode] + ) diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index 8488103a238..dedf5c6d4a6 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -62,6 +62,7 @@ OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform | None] = { UIClass.WINDOW: Platform.COVER, UIWidget.ALARM_PANEL_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) UIWidget.ATLANTIC_ELECTRICAL_HEATER: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) + UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.DOMESTIC_HOT_WATER_TANK: Platform.SWITCH, # widgetName, uiClass is WaterHeatingSystem (not supported) UIWidget.MY_FOX_ALARM_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) UIWidget.MY_FOX_SECURITY_CAMERA: Platform.SWITCH, # widgetName, uiClass is Camera (not supported) From e08465fe8cc5d44ef5b43e96d27ef76e2f94aee9 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Tue, 14 Jun 2022 15:17:40 +0200 Subject: [PATCH 1479/3516] =?UTF-8?q?Fix=20max=5Fvalue=20access=20for=20nu?= =?UTF-8?q?mber=20platform=20in=E2=80=AFOverkiz=20(#73479)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix wrong property name --- homeassistant/components/overkiz/number.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/overkiz/number.py b/homeassistant/components/overkiz/number.py index 741c666a42a..167065e9015 100644 --- a/homeassistant/components/overkiz/number.py +++ b/homeassistant/components/overkiz/number.py @@ -134,7 +134,7 @@ class OverkizNumber(OverkizDescriptiveEntity, NumberEntity): """Return the entity value to represent the entity state.""" if state := self.device.states.get(self.entity_description.key): if self.entity_description.inverted: - return self._attr_max_value - cast(float, state.value) + return self.max_value - cast(float, state.value) return cast(float, state.value) @@ -143,7 +143,7 @@ class OverkizNumber(OverkizDescriptiveEntity, NumberEntity): async def async_set_value(self, value: float) -> None: """Set new value.""" if self.entity_description.inverted: - value = self._attr_max_value - value + value = self.max_value - value await self.executor.async_execute_command( self.entity_description.command, value From 0b7a030bd4b3f6821684cb4731bd8b77dbdef1bb Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 14 Jun 2022 06:19:22 -0700 Subject: [PATCH 1480/3516] Fix fan support in nest, removing FAN_ONLY which isn't supported (#73422) * Fix fan support in nest, removing FAN_ONLY which isn't supported * Revert change to make supported features dynamic --- homeassistant/components/nest/climate_sdm.py | 37 ++-- tests/components/nest/test_climate_sdm.py | 195 ++++++++++--------- 2 files changed, 119 insertions(+), 113 deletions(-) diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 8a56f78028b..6ee988b714f 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -70,6 +70,7 @@ FAN_MODE_MAP = { "OFF": FAN_OFF, } FAN_INV_MODE_MAP = {v: k for k, v in FAN_MODE_MAP.items()} +FAN_INV_MODES = list(FAN_INV_MODE_MAP) MAX_FAN_DURATION = 43200 # 15 hours is the max in the SDM API MIN_TEMP = 10 @@ -99,7 +100,7 @@ class ThermostatEntity(ClimateEntity): """Initialize ThermostatEntity.""" self._device = device self._device_info = NestDeviceInfo(device) - self._supported_features = 0 + self._attr_supported_features = 0 @property def should_poll(self) -> bool: @@ -124,7 +125,7 @@ class ThermostatEntity(ClimateEntity): async def async_added_to_hass(self) -> None: """Run when entity is added to register update signal handler.""" - self._supported_features = self._get_supported_features() + self._attr_supported_features = self._get_supported_features() self.async_on_remove( self._device.add_update_listener(self.async_write_ha_state) ) @@ -198,8 +199,6 @@ class ThermostatEntity(ClimateEntity): trait = self._device.traits[ThermostatModeTrait.NAME] if trait.mode in THERMOSTAT_MODE_MAP: hvac_mode = THERMOSTAT_MODE_MAP[trait.mode] - if hvac_mode == HVACMode.OFF and self.fan_mode == FAN_ON: - hvac_mode = HVACMode.FAN_ONLY return hvac_mode @property @@ -209,8 +208,6 @@ class ThermostatEntity(ClimateEntity): for mode in self._get_device_hvac_modes: if mode in THERMOSTAT_MODE_MAP: supported_modes.append(THERMOSTAT_MODE_MAP[mode]) - if self.supported_features & ClimateEntityFeature.FAN_MODE: - supported_modes.append(HVACMode.FAN_ONLY) return supported_modes @property @@ -252,7 +249,10 @@ class ThermostatEntity(ClimateEntity): @property def fan_mode(self) -> str: """Return the current fan mode.""" - if FanTrait.NAME in self._device.traits: + if ( + self.supported_features & ClimateEntityFeature.FAN_MODE + and FanTrait.NAME in self._device.traits + ): trait = self._device.traits[FanTrait.NAME] return FAN_MODE_MAP.get(trait.timer_mode, FAN_OFF) return FAN_OFF @@ -260,15 +260,12 @@ class ThermostatEntity(ClimateEntity): @property def fan_modes(self) -> list[str]: """Return the list of available fan modes.""" - modes = [] - if FanTrait.NAME in self._device.traits: - modes = list(FAN_INV_MODE_MAP) - return modes - - @property - def supported_features(self) -> int: - """Bitmap of supported features.""" - return self._supported_features + if ( + self.supported_features & ClimateEntityFeature.FAN_MODE + and FanTrait.NAME in self._device.traits + ): + return FAN_INV_MODES + return [] def _get_supported_features(self) -> int: """Compute the bitmap of supported features from the current state.""" @@ -290,10 +287,6 @@ class ThermostatEntity(ClimateEntity): """Set new target hvac mode.""" if hvac_mode not in self.hvac_modes: raise ValueError(f"Unsupported hvac_mode '{hvac_mode}'") - if hvac_mode == HVACMode.FAN_ONLY: - # Turn the fan on but also turn off the hvac if it is on - await self.async_set_fan_mode(FAN_ON) - hvac_mode = HVACMode.OFF api_mode = THERMOSTAT_INV_MODE_MAP[hvac_mode] trait = self._device.traits[ThermostatModeTrait.NAME] try: @@ -338,6 +331,10 @@ class ThermostatEntity(ClimateEntity): """Set new target fan mode.""" if fan_mode not in self.fan_modes: raise ValueError(f"Unsupported fan_mode '{fan_mode}'") + if fan_mode == FAN_ON and self.hvac_mode == HVACMode.OFF: + raise ValueError( + "Cannot turn on fan, please set an HVAC mode (e.g. heat/cool) first" + ) trait = self._device.traits[FanTrait.NAME] duration = None if fan_mode != FAN_OFF: diff --git a/tests/components/nest/test_climate_sdm.py b/tests/components/nest/test_climate_sdm.py index 123742607ad..c271687a348 100644 --- a/tests/components/nest/test_climate_sdm.py +++ b/tests/components/nest/test_climate_sdm.py @@ -33,15 +33,15 @@ from homeassistant.components.climate.const import ( FAN_ON, HVAC_MODE_COOL, HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_ECO, PRESET_NONE, PRESET_SLEEP, + ClimateEntityFeature, ) -from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.const import ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -794,7 +794,7 @@ async def test_thermostat_fan_off( "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, "sdm.devices.traits.ThermostatMode": { "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], - "mode": "OFF", + "mode": "COOL", }, "sdm.devices.traits.Temperature": { "ambientTemperatureCelsius": 16.2, @@ -806,18 +806,22 @@ async def test_thermostat_fan_off( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.state == HVAC_MODE_COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_OFF assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + ) async def test_thermostat_fan_on( @@ -837,7 +841,7 @@ async def test_thermostat_fan_on( }, "sdm.devices.traits.ThermostatMode": { "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], - "mode": "OFF", + "mode": "COOL", }, "sdm.devices.traits.Temperature": { "ambientTemperatureCelsius": 16.2, @@ -849,18 +853,22 @@ async def test_thermostat_fan_on( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_FAN_ONLY + assert thermostat.state == HVAC_MODE_COOL assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + ) async def test_thermostat_cool_with_fan( @@ -895,11 +903,15 @@ async def test_thermostat_cool_with_fan( HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + ) async def test_thermostat_set_fan( @@ -907,6 +919,68 @@ async def test_thermostat_set_fan( setup_platform: PlatformSetup, auth: FakeAuth, create_device: CreateDevice, +) -> None: + """Test a thermostat enabling the fan.""" + create_device.create( + { + "sdm.devices.traits.Fan": { + "timerMode": "ON", + "timerTimeout": "2019-05-10T03:22:54Z", + }, + "sdm.devices.traits.ThermostatHvac": { + "status": "OFF", + }, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "HEAT", + }, + } + ) + await setup_platform() + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_HEAT + assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON + assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + ) + + # Turn off fan mode + await common.async_set_fan_mode(hass, FAN_OFF) + await hass.async_block_till_done() + + assert auth.method == "post" + assert auth.url == DEVICE_COMMAND + assert auth.json == { + "command": "sdm.devices.commands.Fan.SetTimer", + "params": {"timerMode": "OFF"}, + } + + # Turn on fan mode + await common.async_set_fan_mode(hass, FAN_ON) + await hass.async_block_till_done() + + assert auth.method == "post" + assert auth.url == DEVICE_COMMAND + assert auth.json == { + "command": "sdm.devices.commands.Fan.SetTimer", + "params": { + "duration": "43200s", + "timerMode": "ON", + }, + } + + +async def test_thermostat_set_fan_when_off( + hass: HomeAssistant, + setup_platform: PlatformSetup, + auth: FakeAuth, + create_device: CreateDevice, ) -> None: """Test a thermostat enabling the fan.""" create_device.create( @@ -929,34 +1003,18 @@ async def test_thermostat_set_fan( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_FAN_ONLY + assert thermostat.state == HVAC_MODE_OFF assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + ) - # Turn off fan mode - await common.async_set_fan_mode(hass, FAN_OFF) - await hass.async_block_till_done() - - assert auth.method == "post" - assert auth.url == DEVICE_COMMAND - assert auth.json == { - "command": "sdm.devices.commands.Fan.SetTimer", - "params": {"timerMode": "OFF"}, - } - - # Turn on fan mode - await common.async_set_fan_mode(hass, FAN_ON) - await hass.async_block_till_done() - - assert auth.method == "post" - assert auth.url == DEVICE_COMMAND - assert auth.json == { - "command": "sdm.devices.commands.Fan.SetTimer", - "params": { - "duration": "43200s", - "timerMode": "ON", - }, - } + # Fan cannot be turned on when HVAC is off + with pytest.raises(ValueError): + await common.async_set_fan_mode(hass, FAN_ON, entity_id="climate.my_thermostat") async def test_thermostat_fan_empty( @@ -994,6 +1052,10 @@ async def test_thermostat_fan_empty( } assert ATTR_FAN_MODE not in thermostat.attributes assert ATTR_FAN_MODES not in thermostat.attributes + assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + ) # Ignores set_fan_mode since it is lacking SUPPORT_FAN_MODE await common.async_set_fan_mode(hass, FAN_ON) @@ -1018,7 +1080,7 @@ async def test_thermostat_invalid_fan_mode( "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, "sdm.devices.traits.ThermostatMode": { "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], - "mode": "OFF", + "mode": "COOL", }, "sdm.devices.traits.Temperature": { "ambientTemperatureCelsius": 16.2, @@ -1030,14 +1092,13 @@ async def test_thermostat_invalid_fan_mode( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_FAN_ONLY + assert thermostat.state == HVAC_MODE_COOL assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON @@ -1048,58 +1109,6 @@ async def test_thermostat_invalid_fan_mode( await hass.async_block_till_done() -async def test_thermostat_set_hvac_fan_only( - hass: HomeAssistant, - setup_platform: PlatformSetup, - auth: FakeAuth, - create_device: CreateDevice, -) -> None: - """Test a thermostat enabling the fan via hvac_mode.""" - create_device.create( - { - "sdm.devices.traits.Fan": { - "timerMode": "OFF", - "timerTimeout": "2019-05-10T03:22:54Z", - }, - "sdm.devices.traits.ThermostatHvac": { - "status": "OFF", - }, - "sdm.devices.traits.ThermostatMode": { - "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], - "mode": "OFF", - }, - } - ) - await setup_platform() - - assert len(hass.states.async_all()) == 1 - thermostat = hass.states.get("climate.my_thermostat") - assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF - assert thermostat.attributes[ATTR_FAN_MODE] == FAN_OFF - assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] - - await common.async_set_hvac_mode(hass, HVAC_MODE_FAN_ONLY) - await hass.async_block_till_done() - - assert len(auth.captured_requests) == 2 - - (method, url, json, headers) = auth.captured_requests.pop(0) - assert method == "post" - assert url == DEVICE_COMMAND - assert json == { - "command": "sdm.devices.commands.Fan.SetTimer", - "params": {"duration": "43200s", "timerMode": "ON"}, - } - (method, url, json, headers) = auth.captured_requests.pop(0) - assert method == "post" - assert url == DEVICE_COMMAND - assert json == { - "command": "sdm.devices.commands.ThermostatMode.SetMode", - "params": {"mode": "OFF"}, - } - - async def test_thermostat_target_temp( hass: HomeAssistant, setup_platform: PlatformSetup, @@ -1397,7 +1406,7 @@ async def test_thermostat_hvac_mode_failure( "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, "sdm.devices.traits.ThermostatMode": { "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], - "mode": "OFF", + "mode": "COOL", }, "sdm.devices.traits.Fan": { "timerMode": "OFF", @@ -1416,8 +1425,8 @@ async def test_thermostat_hvac_mode_failure( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.state == HVAC_MODE_COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)] with pytest.raises(HomeAssistantError): From f69ea6017d67e4937ee0570a5241be4fafb2690b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 14 Jun 2022 16:00:07 +0200 Subject: [PATCH 1481/3516] Add device class support to Tuya number entities (#73483) --- homeassistant/components/tuya/number.py | 49 +++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 35efd78871f..a342fe58bd2 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -3,7 +3,11 @@ from __future__ import annotations from tuya_iot import TuyaDevice, TuyaDeviceManager -from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntity, + NumberEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -12,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantTuyaData from .base import IntegerTypeData, TuyaEntity -from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, DPType +from .const import DEVICE_CLASS_UNITS, DOMAIN, TUYA_DISCOVERY_NEW, DPCode, DPType # All descriptions can be found here. Mostly the Integer data types in the # default instructions set of each category end up being a number. @@ -33,24 +37,28 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { NumberEntityDescription( key=DPCode.TEMP_SET, name="Temperature", + device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_SET_F, name="Temperature", + device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_BOILING_C, name="Temperature After Boiling", + device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_BOILING_F, name="Temperature After Boiling", + device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), @@ -108,6 +116,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { NumberEntityDescription( key=DPCode.TEMP_SET, name="Temperature", + device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), @@ -256,6 +265,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { NumberEntityDescription( key=DPCode.TEMP, name="Temperature", + device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer-lines", ), ), @@ -265,11 +275,13 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { NumberEntityDescription( key=DPCode.TEMP_SET, name="Temperature", + device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer-lines", ), NumberEntityDescription( key=DPCode.TEMP_SET_F, name="Temperature", + device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer-lines", ), ), @@ -329,8 +341,37 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity): self._attr_max_value = self._number.max_scaled self._attr_min_value = self._number.min_scaled self._attr_step = self._number.step_scaled - if description.unit_of_measurement is None: - self._attr_unit_of_measurement = self._number.unit + + # 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 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 + # device class cannot be found in the validation mapping. + if ( + self.native_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.native_unit_of_measurement) or uoms.get( + self.native_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 + + # If we still have a device class, we should not use an icon + if self.device_class: + self._attr_icon = None @property def value(self) -> float | None: From 9b157f974d80b4d5e98562b85f846d5f9ea4ea73 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 14 Jun 2022 07:46:00 -1000 Subject: [PATCH 1482/3516] Reduce overhead to refire events from async_track_point_in_utc_time when an asyncio timer fires early (#73295) * Reduce overhead to refire events - asyncio timers can fire early for a varity of reasons including poor clock resolution and performance. To solve this problem we re-arm async_track_point_in_utc_time and try again later when this happens. - On some platforms this means the async_track_point_in_utc_time can end up trying many times to prevent firing the timer early since as soon as it rearms it fires again and this repeats until we reach the appointed time. While there is not much we can do to prevent asyncio from firing the timer callback early, we can reduce the overhead when this happens by using avoiding creating datetime objects * tweak mocking * -vvv * fix time freeze being too broad in litterrobot * adjust --- homeassistant/helpers/event.py | 7 +++---- tests/common.py | 12 +++++++++--- tests/components/litterrobot/test_button.py | 14 +++++++------- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index c9b569c6601..85cd684fca1 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1286,6 +1286,7 @@ def async_track_point_in_utc_time( """Add a listener that fires once after a specific point in UTC time.""" # Ensure point_in_time is UTC utc_point_in_time = dt_util.as_utc(point_in_time) + expected_fire_timestamp = dt_util.utc_to_timestamp(utc_point_in_time) # Since this is called once, we accept a HassJob so we can avoid # having to figure out how to call the action every time its called. @@ -1295,15 +1296,12 @@ def async_track_point_in_utc_time( def run_action(job: HassJob[[datetime], Awaitable[None] | None]) -> None: """Call the action.""" nonlocal cancel_callback - - now = time_tracker_utcnow() - # Depending on the available clock support (including timer hardware # and the OS kernel) it can happen that we fire a little bit too early # as measured by utcnow(). That is bad when callbacks have assumptions # about the current time. Thus, we rearm the timer for the remaining # time. - if (delta := (utc_point_in_time - now).total_seconds()) > 0: + if (delta := (expected_fire_timestamp - time_tracker_timestamp())) > 0: _LOGGER.debug("Called %f seconds too early, rearming", delta) cancel_callback = hass.loop.call_later(delta, run_action, job) @@ -1474,6 +1472,7 @@ track_sunset = threaded_listener_factory(async_track_sunset) # For targeted patching in tests time_tracker_utcnow = dt_util.utcnow +time_tracker_timestamp = time.time @callback diff --git a/tests/common.py b/tests/common.py index bd0b828737b..1a29d0d6dc4 100644 --- a/tests/common.py +++ b/tests/common.py @@ -378,7 +378,10 @@ def async_fire_time_changed( ) -> None: """Fire a time changed event.""" if datetime_ is None: - datetime_ = date_util.utcnow() + utc_datetime = date_util.utcnow() + else: + utc_datetime = date_util.as_utc(datetime_) + timestamp = date_util.utc_to_timestamp(utc_datetime) for task in list(hass.loop._scheduled): if not isinstance(task, asyncio.TimerHandle): @@ -386,13 +389,16 @@ def async_fire_time_changed( if task.cancelled(): continue - mock_seconds_into_future = datetime_.timestamp() - time.time() + mock_seconds_into_future = timestamp - time.time() future_seconds = task.when() - hass.loop.time() if fire_all or mock_seconds_into_future >= future_seconds: with patch( "homeassistant.helpers.event.time_tracker_utcnow", - return_value=date_util.as_utc(datetime_), + return_value=utc_datetime, + ), patch( + "homeassistant.helpers.event.time_tracker_timestamp", + return_value=timestamp, ): task._run() task.cancel() diff --git a/tests/components/litterrobot/test_button.py b/tests/components/litterrobot/test_button.py index 3f802d0e6b2..6291558c832 100644 --- a/tests/components/litterrobot/test_button.py +++ b/tests/components/litterrobot/test_button.py @@ -14,7 +14,6 @@ from .conftest import setup_integration BUTTON_ENTITY = "button.test_reset_waste_drawer" -@freeze_time("2021-11-15 17:37:00", tz_offset=-7) async def test_button(hass: HomeAssistant, mock_account: MagicMock) -> None: """Test the creation and values of the Litter-Robot button.""" await setup_integration(hass, mock_account, BUTTON_DOMAIN) @@ -29,12 +28,13 @@ async def test_button(hass: HomeAssistant, mock_account: MagicMock) -> None: assert entry assert entry.entity_category is EntityCategory.CONFIG - await hass.services.async_call( - BUTTON_DOMAIN, - SERVICE_PRESS, - {ATTR_ENTITY_ID: BUTTON_ENTITY}, - blocking=True, - ) + with freeze_time("2021-11-15 17:37:00", tz_offset=-7): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: BUTTON_ENTITY}, + blocking=True, + ) await hass.async_block_till_done() assert mock_account.robots[0].reset_waste_drawer.call_count == 1 mock_account.robots[0].reset_waste_drawer.assert_called_with() From 61e4b56e1905102e92de9b620751807b45657785 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Jun 2022 10:55:58 -0700 Subject: [PATCH 1483/3516] Guard withings accessing hass.data without it being set (#73454) Co-authored-by: Martin Hjelmare --- homeassistant/components/withings/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 47702090cc0..6e8dee9a774 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -72,11 +72,12 @@ 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 := config.get(DOMAIN, {})): + if not (conf := config.get(DOMAIN)): + # Apply the defaults. + conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] + hass.data[DOMAIN] = {const.CONFIG: conf} return True - # Make the config available to the oauth2 config flow. hass.data[DOMAIN] = {const.CONFIG: conf} # Setup the oauth2 config flow. From 23fa19b75a398d89fe585e8c1d26c175f2024e47 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 14 Jun 2022 19:56:27 +0200 Subject: [PATCH 1484/3516] Support restoring NumberEntity native_value (#73475) --- homeassistant/components/number/__init__.py | 57 ++++++++- tests/components/number/test_init.py | 117 +++++++++++++++++- .../custom_components/test/number.py | 21 +++- 3 files changed, 189 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 75f98447865..5a0bf9947f3 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from contextlib import suppress -from dataclasses import dataclass +import dataclasses from datetime import timedelta import inspect import logging @@ -22,6 +22,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity from homeassistant.helpers.typing import ConfigType from homeassistant.util import temperature as temperature_util @@ -112,7 +113,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) -@dataclass +@dataclasses.dataclass class NumberEntityDescription(EntityDescription): """A class that describes number entities.""" @@ -324,7 +325,7 @@ class NumberEntity(Entity): @property def native_value(self) -> float | None: - """Return the value reported by the sensor.""" + """Return the value reported by the number.""" return self._attr_native_value @property @@ -419,3 +420,53 @@ class NumberEntity(Entity): type(self), report_issue, ) + + +@dataclasses.dataclass +class NumberExtraStoredData(ExtraStoredData): + """Object to hold extra stored data.""" + + native_max_value: float | None + native_min_value: float | None + native_step: float | None + native_unit_of_measurement: str | None + native_value: float | None + + def as_dict(self) -> dict[str, Any]: + """Return a dict representation of the number data.""" + return dataclasses.asdict(self) + + @classmethod + def from_dict(cls, restored: dict[str, Any]) -> NumberExtraStoredData | None: + """Initialize a stored number state from a dict.""" + try: + return cls( + restored["native_max_value"], + restored["native_min_value"], + restored["native_step"], + restored["native_unit_of_measurement"], + restored["native_value"], + ) + except KeyError: + return None + + +class RestoreNumber(NumberEntity, RestoreEntity): + """Mixin class for restoring previous number state.""" + + @property + def extra_restore_state_data(self) -> NumberExtraStoredData: + """Return number specific state data to be restored.""" + return NumberExtraStoredData( + self.native_max_value, + self.native_min_value, + self.native_step, + self.native_unit_of_measurement, + self.native_value, + ) + + async def async_get_last_number_data(self) -> NumberExtraStoredData | None: + """Restore native_*.""" + if (restored_last_extra_data := await self.async_get_last_extra_data()) is None: + return None + return NumberExtraStoredData.from_dict(restored_last_extra_data.as_dict()) diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index ccc6f0da0c5..0df7f79e4a4 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -21,10 +21,13 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, State +from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM +from tests.common import mock_restore_cache_with_extra_data + class MockDefaultNumberEntity(NumberEntity): """Mock NumberEntity device to use in tests. @@ -570,3 +573,115 @@ async def test_temperature_conversion( state = hass.states.get(entity0.entity_id) assert float(state.state) == pytest.approx(float(state_max_value), rel=0.1) + + +RESTORE_DATA = { + "native_max_value": 200.0, + "native_min_value": -10.0, + "native_step": 2.0, + "native_unit_of_measurement": "°F", + "native_value": 123.0, +} + + +async def test_restore_number_save_state( + hass, + hass_storage, + enable_custom_integrations, +): + """Test RestoreNumber.""" + platform = getattr(hass.components, "test.number") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockRestoreNumber( + name="Test", + native_max_value=200.0, + native_min_value=-10.0, + native_step=2.0, + native_unit_of_measurement=TEMP_FAHRENHEIT, + native_value=123.0, + device_class=NumberDeviceClass.TEMPERATURE, + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component(hass, "number", {"number": {"platform": "test"}}) + await hass.async_block_till_done() + + # Trigger saving state + await hass.async_stop() + + assert len(hass_storage[RESTORE_STATE_KEY]["data"]) == 1 + state = hass_storage[RESTORE_STATE_KEY]["data"][0]["state"] + assert state["entity_id"] == entity0.entity_id + extra_data = hass_storage[RESTORE_STATE_KEY]["data"][0]["extra_data"] + assert extra_data == RESTORE_DATA + assert type(extra_data["native_value"]) == float + + +@pytest.mark.parametrize( + "native_max_value, native_min_value, native_step, native_value, native_value_type, extra_data, device_class, uom", + [ + ( + 200.0, + -10.0, + 2.0, + 123.0, + float, + RESTORE_DATA, + NumberDeviceClass.TEMPERATURE, + "°F", + ), + (100.0, 0.0, None, None, type(None), None, None, None), + (100.0, 0.0, None, None, type(None), {}, None, None), + (100.0, 0.0, None, None, type(None), {"beer": 123}, None, None), + ( + 100.0, + 0.0, + None, + None, + type(None), + {"native_unit_of_measurement": "°F", "native_value": {}}, + None, + None, + ), + ], +) +async def test_restore_number_restore_state( + hass, + enable_custom_integrations, + hass_storage, + native_max_value, + native_min_value, + native_step, + native_value, + native_value_type, + extra_data, + device_class, + uom, +): + """Test RestoreNumber.""" + mock_restore_cache_with_extra_data(hass, ((State("number.test", ""), extra_data),)) + + platform = getattr(hass.components, "test.number") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockRestoreNumber( + device_class=device_class, + name="Test", + native_value=None, + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component(hass, "number", {"number": {"platform": "test"}}) + await hass.async_block_till_done() + + assert hass.states.get(entity0.entity_id) + + assert entity0.native_max_value == native_max_value + assert entity0.native_min_value == native_min_value + assert entity0.native_step == native_step + assert entity0.native_value == native_value + assert type(entity0.native_value) == native_value_type + assert entity0.native_unit_of_measurement == uom diff --git a/tests/testing_config/custom_components/test/number.py b/tests/testing_config/custom_components/test/number.py index ac397a4d42b..094698923f4 100644 --- a/tests/testing_config/custom_components/test/number.py +++ b/tests/testing_config/custom_components/test/number.py @@ -3,7 +3,7 @@ Provide a mock number platform. Call init before using it in your tests to ensure clean test data. """ -from homeassistant.components.number import NumberEntity +from homeassistant.components.number import NumberEntity, RestoreNumber from tests.common import MockEntity @@ -37,7 +37,7 @@ class MockNumberEntity(MockEntity, NumberEntity): @property def native_value(self): - """Return the native value of this sensor.""" + """Return the native value of this number.""" return self._handle("native_value") def set_native_value(self, value: float) -> None: @@ -45,6 +45,23 @@ class MockNumberEntity(MockEntity, NumberEntity): self._values["native_value"] = value +class MockRestoreNumber(MockNumberEntity, RestoreNumber): + """Mock RestoreNumber class.""" + + async def async_added_to_hass(self) -> None: + """Restore native_*.""" + await super().async_added_to_hass() + if (last_number_data := await self.async_get_last_number_data()) is None: + return + self._values["native_max_value"] = last_number_data.native_max_value + self._values["native_min_value"] = last_number_data.native_min_value + self._values["native_step"] = last_number_data.native_step + self._values[ + "native_unit_of_measurement" + ] = last_number_data.native_unit_of_measurement + self._values["native_value"] = last_number_data.native_value + + class LegacyMockNumberEntity(MockEntity, NumberEntity): """Mock Number class using deprecated features.""" From 576de9ac4052c90b8737e41110d05f06f41d000e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 14 Jun 2022 20:15:56 +0200 Subject: [PATCH 1485/3516] Migrate NumberEntity u-z to native_value (#73488) --- .../components/unifiprotect/number.py | 10 +-- homeassistant/components/wallbox/number.py | 8 +-- homeassistant/components/wiz/number.py | 16 ++--- homeassistant/components/wled/number.py | 16 ++--- .../components/xiaomi_miio/number.py | 70 +++++++++---------- .../components/yamaha_musiccast/number.py | 10 +-- homeassistant/components/zha/number.py | 14 ++-- homeassistant/components/zwave_js/number.py | 20 +++--- homeassistant/components/zwave_me/number.py | 4 +- 9 files changed, 84 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index 4ebdd17f5c9..3ac5b673ea5 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -198,15 +198,15 @@ class ProtectNumbers(ProtectDeviceEntity, NumberEntity): ) -> None: """Initialize the Number Entities.""" super().__init__(data, device, description) - self._attr_max_value = self.entity_description.ufp_max - self._attr_min_value = self.entity_description.ufp_min - self._attr_step = self.entity_description.ufp_step + self._attr_native_max_value = self.entity_description.ufp_max + self._attr_native_min_value = self.entity_description.ufp_min + self._attr_native_step = self.entity_description.ufp_step @callback def _async_update_device_from_protect(self) -> None: super()._async_update_device_from_protect() - self._attr_value = self.entity_description.get_ufp_value(self.device) + self._attr_native_value = self.entity_description.get_ufp_value(self.device) - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set new value.""" await self.entity_description.ufp_set(self.device, value) diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index 1ad06145ed5..1db791fd389 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -28,7 +28,7 @@ NUMBER_TYPES: dict[str, WallboxNumberEntityDescription] = { CHARGER_MAX_CHARGING_CURRENT_KEY: WallboxNumberEntityDescription( key=CHARGER_MAX_CHARGING_CURRENT_KEY, name="Max. Charging Current", - min_value=6, + native_min_value=6, ), } @@ -74,17 +74,17 @@ class WallboxNumber(WallboxEntity, NumberEntity): self._attr_unique_id = f"{description.key}-{coordinator.data[CHARGER_DATA_KEY][CHARGER_SERIAL_NUMBER_KEY]}" @property - def max_value(self) -> float: + def native_max_value(self) -> float: """Return the maximum available current.""" return cast(float, self._coordinator.data[CHARGER_MAX_AVAILABLE_POWER_KEY]) @property - def value(self) -> float | None: + def native_value(self) -> float | None: """Return the state of the sensor.""" return cast( Optional[float], self._coordinator.data[CHARGER_MAX_CHARGING_CURRENT_KEY] ) - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set the value of the entity.""" await self._coordinator.async_set_charging_current(value) diff --git a/homeassistant/components/wiz/number.py b/homeassistant/components/wiz/number.py index f7d827534b3..d2f68fcf7c3 100644 --- a/homeassistant/components/wiz/number.py +++ b/homeassistant/components/wiz/number.py @@ -48,9 +48,9 @@ async def _async_set_ratio(device: wizlight, ratio: int) -> None: NUMBERS: tuple[WizNumberEntityDescription, ...] = ( WizNumberEntityDescription( key="effect_speed", - min_value=10, - max_value=200, - step=1, + native_min_value=10, + native_max_value=200, + native_step=1, icon="mdi:speedometer", name="Effect Speed", value_fn=lambda device: cast(Optional[int], device.state.get_speed()), @@ -59,9 +59,9 @@ NUMBERS: tuple[WizNumberEntityDescription, ...] = ( ), WizNumberEntityDescription( key="dual_head_ratio", - min_value=0, - max_value=100, - step=1, + native_min_value=0, + native_max_value=100, + native_step=1, icon="mdi:floor-lamp-dual", name="Dual Head Ratio", value_fn=lambda device: cast(Optional[int], device.state.get_ratio()), @@ -113,9 +113,9 @@ class WizSpeedNumber(WizEntity, NumberEntity): def _async_update_attrs(self) -> None: """Handle updating _attr values.""" if (value := self.entity_description.value_fn(self._device)) is not None: - self._attr_value = float(value) + self._attr_native_value = float(value) - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set the speed value.""" await self.entity_description.set_value_fn(self._device, int(value)) await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/wled/number.py b/homeassistant/components/wled/number.py index d551072e452..6c426cc44c5 100644 --- a/homeassistant/components/wled/number.py +++ b/homeassistant/components/wled/number.py @@ -41,17 +41,17 @@ NUMBERS = [ name="Speed", icon="mdi:speedometer", entity_category=EntityCategory.CONFIG, - step=1, - min_value=0, - max_value=255, + native_step=1, + native_min_value=0, + native_max_value=255, ), NumberEntityDescription( key=ATTR_INTENSITY, name="Intensity", entity_category=EntityCategory.CONFIG, - step=1, - min_value=0, - max_value=255, + native_step=1, + native_min_value=0, + native_max_value=255, ), ] @@ -93,7 +93,7 @@ class WLEDNumber(WLEDEntity, NumberEntity): return super().available @property - def value(self) -> float | None: + def native_value(self) -> float | None: """Return the current WLED segment number value.""" return getattr( self.coordinator.data.state.segments[self._segment], @@ -101,7 +101,7 @@ class WLEDNumber(WLEDEntity, NumberEntity): ) @wled_exception_handler - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set the WLED segment value.""" key = self.entity_description.key if key == ATTR_SPEED: diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index f47d80ead17..02855a89c1f 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -108,10 +108,10 @@ NUMBER_TYPES = { key=ATTR_MOTOR_SPEED, name="Motor Speed", icon="mdi:fast-forward-outline", - unit_of_measurement="rpm", - min_value=200, - max_value=2000, - step=10, + native_unit_of_measurement="rpm", + native_min_value=200, + native_max_value=2000, + native_step=10, available_with_device_off=False, method="async_set_motor_speed", entity_category=EntityCategory.CONFIG, @@ -120,9 +120,9 @@ NUMBER_TYPES = { key=ATTR_FAVORITE_LEVEL, name="Favorite Level", icon="mdi:star-cog", - min_value=0, - max_value=17, - step=1, + native_min_value=0, + native_max_value=17, + native_step=1, method="async_set_favorite_level", entity_category=EntityCategory.CONFIG, ), @@ -130,9 +130,9 @@ NUMBER_TYPES = { key=ATTR_FAN_LEVEL, name="Fan Level", icon="mdi:fan", - min_value=1, - max_value=3, - step=1, + native_min_value=1, + native_max_value=3, + native_step=1, method="async_set_fan_level", entity_category=EntityCategory.CONFIG, ), @@ -140,9 +140,9 @@ NUMBER_TYPES = { key=ATTR_VOLUME, name="Volume", icon="mdi:volume-high", - min_value=0, - max_value=100, - step=1, + native_min_value=0, + native_max_value=100, + native_step=1, method="async_set_volume", entity_category=EntityCategory.CONFIG, ), @@ -150,10 +150,10 @@ NUMBER_TYPES = { key=ATTR_OSCILLATION_ANGLE, name="Oscillation Angle", icon="mdi:angle-acute", - unit_of_measurement=DEGREE, - min_value=1, - max_value=120, - step=1, + native_unit_of_measurement=DEGREE, + native_min_value=1, + native_max_value=120, + native_step=1, method="async_set_oscillation_angle", entity_category=EntityCategory.CONFIG, ), @@ -161,10 +161,10 @@ NUMBER_TYPES = { key=ATTR_DELAY_OFF_COUNTDOWN, name="Delay Off Countdown", icon="mdi:fan-off", - unit_of_measurement=TIME_MINUTES, - min_value=0, - max_value=480, - step=1, + native_unit_of_measurement=TIME_MINUTES, + native_min_value=0, + native_max_value=480, + native_step=1, method="async_set_delay_off_countdown", entity_category=EntityCategory.CONFIG, ), @@ -172,9 +172,9 @@ NUMBER_TYPES = { key=ATTR_LED_BRIGHTNESS, name="Led Brightness", icon="mdi:brightness-6", - min_value=0, - max_value=100, - step=1, + native_min_value=0, + native_max_value=100, + native_step=1, method="async_set_led_brightness", entity_category=EntityCategory.CONFIG, ), @@ -182,9 +182,9 @@ NUMBER_TYPES = { key=ATTR_LED_BRIGHTNESS_LEVEL, name="Led Brightness", icon="mdi:brightness-6", - min_value=0, - max_value=8, - step=1, + native_min_value=0, + native_max_value=8, + native_step=1, method="async_set_led_brightness_level", entity_category=EntityCategory.CONFIG, ), @@ -192,10 +192,10 @@ NUMBER_TYPES = { key=ATTR_FAVORITE_RPM, name="Favorite Motor Speed", icon="mdi:star-cog", - unit_of_measurement="rpm", - min_value=300, - max_value=2200, - step=10, + native_unit_of_measurement="rpm", + native_min_value=300, + native_max_value=2200, + native_step=10, method="async_set_favorite_rpm", entity_category=EntityCategory.CONFIG, ), @@ -298,7 +298,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): """Initialize the generic Xiaomi attribute selector.""" super().__init__(name, device, entry, unique_id, coordinator) - self._attr_value = self._extract_value_from_attribute( + self._attr_native_value = self._extract_value_from_attribute( coordinator.data, description.key ) self.entity_description = description @@ -314,18 +314,18 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): return False return super().available - async def async_set_value(self, value): + async def async_set_native_value(self, value): """Set an option of the miio device.""" method = getattr(self, self.entity_description.method) if await method(int(value)): - self._attr_value = value + self._attr_native_value = value self.async_write_ha_state() @callback def _handle_coordinator_update(self): """Fetch state from the device.""" # On state change the device doesn't provide the new state immediately. - self._attr_value = self._extract_value_from_attribute( + self._attr_native_value = self._extract_value_from_attribute( self.coordinator.data, self.entity_description.key ) self.async_write_ha_state() diff --git a/homeassistant/components/yamaha_musiccast/number.py b/homeassistant/components/yamaha_musiccast/number.py index 2648359f768..b05c47ce279 100644 --- a/homeassistant/components/yamaha_musiccast/number.py +++ b/homeassistant/components/yamaha_musiccast/number.py @@ -45,15 +45,15 @@ class NumberCapability(MusicCastCapabilityEntity, NumberEntity): ) -> None: """Initialize the number entity.""" super().__init__(coordinator, capability, zone_id) - self._attr_min_value = capability.value_range.minimum - self._attr_max_value = capability.value_range.maximum - self._attr_step = capability.value_range.step + self._attr_native_min_value = capability.value_range.minimum + self._attr_native_max_value = capability.value_range.maximum + self._attr_native_step = capability.value_range.step @property - def value(self): + def native_value(self): """Return the current value.""" return self.capability.current - async def async_set_value(self, value: float): + async def async_set_native_value(self, value: float): """Set a new value.""" await self.capability.set(value) diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 216b9974df6..2f674df168e 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -287,12 +287,12 @@ class ZhaNumber(ZhaEntity, NumberEntity): ) @property - def value(self): + def native_value(self): """Return the current value.""" return self._analog_output_channel.present_value @property - def min_value(self): + def native_min_value(self): """Return the minimum value.""" min_present_value = self._analog_output_channel.min_present_value if min_present_value is not None: @@ -300,7 +300,7 @@ class ZhaNumber(ZhaEntity, NumberEntity): return 0 @property - def max_value(self): + def native_max_value(self): """Return the maximum value.""" max_present_value = self._analog_output_channel.max_present_value if max_present_value is not None: @@ -308,12 +308,12 @@ class ZhaNumber(ZhaEntity, NumberEntity): return 1023 @property - def step(self): + def native_step(self): """Return the value step.""" resolution = self._analog_output_channel.resolution if resolution is not None: return resolution - return super().step + return super().native_step @property def name(self): @@ -332,7 +332,7 @@ class ZhaNumber(ZhaEntity, NumberEntity): return super().icon @property - def unit_of_measurement(self): + def native_unit_of_measurement(self): """Return the unit the value is expressed in.""" engineering_units = self._analog_output_channel.engineering_units return UNITS.get(engineering_units) @@ -342,7 +342,7 @@ class ZhaNumber(ZhaEntity, NumberEntity): """Handle value update from channel.""" self.async_write_ha_state() - async def async_set_value(self, value): + async def async_set_native_value(self, value): """Update the current value from HA.""" num_value = float(value) if await self._analog_output_channel.async_set_present_value(num_value): diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py index 737b872b7bc..1b17fa35024 100644 --- a/homeassistant/components/zwave_js/number.py +++ b/homeassistant/components/zwave_js/number.py @@ -71,34 +71,34 @@ class ZwaveNumberEntity(ZWaveBaseEntity, NumberEntity): ) @property - def min_value(self) -> float: + def native_min_value(self) -> float: """Return the minimum value.""" if self.info.primary_value.metadata.min is None: return 0 return float(self.info.primary_value.metadata.min) @property - def max_value(self) -> float: + def native_max_value(self) -> float: """Return the maximum value.""" if self.info.primary_value.metadata.max is None: return 255 return float(self.info.primary_value.metadata.max) @property - def value(self) -> float | None: + def native_value(self) -> float | None: """Return the entity value.""" if self.info.primary_value.value is None: return None return float(self.info.primary_value.value) @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" if self.info.primary_value.metadata.unit is None: return None return str(self.info.primary_value.metadata.unit) - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set new value.""" if (target_value := self._target_value) is None: raise HomeAssistantError("Missing target value on device.") @@ -121,19 +121,19 @@ class ZwaveVolumeNumberEntity(ZWaveBaseEntity, NumberEntity): self.correction_factor = 1 # Entity class attributes - self._attr_min_value = 0 - self._attr_max_value = 1 - self._attr_step = 0.01 + self._attr_native_min_value = 0 + self._attr_native_max_value = 1 + self._attr_native_step = 0.01 self._attr_name = self.generate_name(include_value_name=True) @property - def value(self) -> float | None: + def native_value(self) -> float | None: """Return the entity value.""" if self.info.primary_value.value is None: return None return float(self.info.primary_value.value) / self.correction_factor - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set new value.""" await self.info.node.async_set_value( self.info.primary_value, round(value * self.correction_factor) diff --git a/homeassistant/components/zwave_me/number.py b/homeassistant/components/zwave_me/number.py index efa7ceb8603..2fa82514626 100644 --- a/homeassistant/components/zwave_me/number.py +++ b/homeassistant/components/zwave_me/number.py @@ -40,13 +40,13 @@ class ZWaveMeNumber(ZWaveMeEntity, NumberEntity): """Representation of a ZWaveMe Multilevel Switch.""" @property - def value(self): + def native_value(self): """Return the unit of measurement.""" if self.device.level == 99: # Scale max value return 100 return self.device.level - def set_value(self, value: float) -> None: + def set_native_value(self, value: float) -> None: """Update the current value.""" self.controller.zwave_api.send_command( self.device.id, f"exact?level={str(round(value))}" From 1f7340313acf9617fc6035fd44704c64c535d072 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 14 Jun 2022 20:16:36 +0200 Subject: [PATCH 1486/3516] Migrate NumberEntity a-j to native_value (#73486) --- homeassistant/components/baf/number.py | 40 ++++++++--------- homeassistant/components/deconz/number.py | 10 ++--- homeassistant/components/demo/number.py | 24 +++++----- homeassistant/components/esphome/number.py | 12 ++--- .../components/fjaraskupan/number.py | 12 ++--- homeassistant/components/flux_led/number.py | 44 +++++++++---------- homeassistant/components/goodwe/number.py | 22 +++++----- .../components/homekit_controller/number.py | 20 ++++----- homeassistant/components/juicenet/number.py | 22 +++++----- 9 files changed, 103 insertions(+), 103 deletions(-) diff --git a/homeassistant/components/baf/number.py b/homeassistant/components/baf/number.py index 32a3ea5e693..73d60fa6c03 100644 --- a/homeassistant/components/baf/number.py +++ b/homeassistant/components/baf/number.py @@ -40,8 +40,8 @@ AUTO_COMFORT_NUMBER_DESCRIPTIONS = ( BAFNumberDescription( key="comfort_min_speed", name="Auto Comfort Minimum Speed", - min_value=0, - max_value=SPEED_RANGE[1] - 1, + native_min_value=0, + native_max_value=SPEED_RANGE[1] - 1, entity_category=EntityCategory.CONFIG, value_fn=lambda device: cast(Optional[int], device.comfort_min_speed), mode=NumberMode.BOX, @@ -49,8 +49,8 @@ AUTO_COMFORT_NUMBER_DESCRIPTIONS = ( BAFNumberDescription( key="comfort_max_speed", name="Auto Comfort Maximum Speed", - min_value=1, - max_value=SPEED_RANGE[1], + native_min_value=1, + native_max_value=SPEED_RANGE[1], entity_category=EntityCategory.CONFIG, value_fn=lambda device: cast(Optional[int], device.comfort_max_speed), mode=NumberMode.BOX, @@ -58,8 +58,8 @@ AUTO_COMFORT_NUMBER_DESCRIPTIONS = ( BAFNumberDescription( key="comfort_heat_assist_speed", name="Auto Comfort Heat Assist Speed", - min_value=SPEED_RANGE[0], - max_value=SPEED_RANGE[1], + native_min_value=SPEED_RANGE[0], + native_max_value=SPEED_RANGE[1], entity_category=EntityCategory.CONFIG, value_fn=lambda device: cast(Optional[int], device.comfort_heat_assist_speed), mode=NumberMode.BOX, @@ -70,20 +70,20 @@ FAN_NUMBER_DESCRIPTIONS = ( BAFNumberDescription( key="return_to_auto_timeout", name="Return to Auto Timeout", - min_value=ONE_MIN_SECS, - max_value=HALF_DAY_SECS, + native_min_value=ONE_MIN_SECS, + native_max_value=HALF_DAY_SECS, entity_category=EntityCategory.CONFIG, - unit_of_measurement=TIME_SECONDS, + native_unit_of_measurement=TIME_SECONDS, value_fn=lambda device: cast(Optional[int], device.return_to_auto_timeout), mode=NumberMode.SLIDER, ), BAFNumberDescription( key="motion_sense_timeout", name="Motion Sense Timeout", - min_value=ONE_MIN_SECS, - max_value=ONE_DAY_SECS, + native_min_value=ONE_MIN_SECS, + native_max_value=ONE_DAY_SECS, entity_category=EntityCategory.CONFIG, - unit_of_measurement=TIME_SECONDS, + native_unit_of_measurement=TIME_SECONDS, value_fn=lambda device: cast(Optional[int], device.motion_sense_timeout), mode=NumberMode.SLIDER, ), @@ -93,10 +93,10 @@ LIGHT_NUMBER_DESCRIPTIONS = ( BAFNumberDescription( key="light_return_to_auto_timeout", name="Light Return to Auto Timeout", - min_value=ONE_MIN_SECS, - max_value=HALF_DAY_SECS, + native_min_value=ONE_MIN_SECS, + native_max_value=HALF_DAY_SECS, entity_category=EntityCategory.CONFIG, - unit_of_measurement=TIME_SECONDS, + native_unit_of_measurement=TIME_SECONDS, value_fn=lambda device: cast( Optional[int], device.light_return_to_auto_timeout ), @@ -105,10 +105,10 @@ LIGHT_NUMBER_DESCRIPTIONS = ( BAFNumberDescription( key="light_auto_motion_timeout", name="Light Motion Sense Timeout", - min_value=ONE_MIN_SECS, - max_value=ONE_DAY_SECS, + native_min_value=ONE_MIN_SECS, + native_max_value=ONE_DAY_SECS, entity_category=EntityCategory.CONFIG, - unit_of_measurement=TIME_SECONDS, + native_unit_of_measurement=TIME_SECONDS, value_fn=lambda device: cast(Optional[int], device.light_auto_motion_timeout), mode=NumberMode.SLIDER, ), @@ -149,8 +149,8 @@ class BAFNumber(BAFEntity, NumberEntity): def _async_update_attrs(self) -> None: """Update attrs from device.""" if (value := self.entity_description.value_fn(self._device)) is not None: - self._attr_value = float(value) + self._attr_native_value = float(value) - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set the value.""" setattr(self._device, self.entity_description.key, int(value)) diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index a7bb014d76a..81f3e434007 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -43,9 +43,9 @@ ENTITY_DESCRIPTIONS = { value_fn=lambda device: device.delay, suffix="Delay", update_key=PRESENCE_DELAY, - max_value=65535, - min_value=0, - step=1, + native_max_value=65535, + native_min_value=0, + native_step=1, entity_category=EntityCategory.CONFIG, ) ] @@ -107,11 +107,11 @@ class DeconzNumber(DeconzDevice, NumberEntity): super().async_update_callback() @property - def value(self) -> float | None: + def native_value(self) -> float | None: """Return the value of the sensor property.""" return self.entity_description.value_fn(self._device) - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set sensor config.""" data = {self.entity_description.key: int(value)} await self._device.set_config(**data) diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py index 8660604af9e..7a9baf045e5 100644 --- a/homeassistant/components/demo/number.py +++ b/homeassistant/components/demo/number.py @@ -85,9 +85,9 @@ class DemoNumber(NumberEntity): state: float, icon: str, assumed: bool, - min_value: float | None = None, - max_value: float | None = None, - step: float | None = None, + native_min_value: float | None = None, + native_max_value: float | None = None, + native_step: float | None = None, mode: NumberMode = NumberMode.AUTO, ) -> None: """Initialize the Demo Number entity.""" @@ -95,15 +95,15 @@ class DemoNumber(NumberEntity): self._attr_icon = icon self._attr_name = name or DEVICE_DEFAULT_NAME self._attr_unique_id = unique_id - self._attr_value = state + self._attr_native_value = state self._attr_mode = mode - if min_value is not None: - self._attr_min_value = min_value - if max_value is not None: - self._attr_max_value = max_value - if step is not None: - self._attr_step = step + if native_min_value is not None: + self._attr_min_value = native_min_value + if native_max_value is not None: + self._attr_max_value = native_max_value + if native_step is not None: + self._attr_step = native_step self._attr_device_info = DeviceInfo( identifiers={ @@ -113,7 +113,7 @@ class DemoNumber(NumberEntity): name=self.name, ) - async def async_set_value(self, value): + async def async_set_native_value(self, value): """Update the current value.""" - self._attr_value = value + self._attr_native_value = value self.async_write_ha_state() diff --git a/homeassistant/components/esphome/number.py b/homeassistant/components/esphome/number.py index be27779437d..bbca463a908 100644 --- a/homeassistant/components/esphome/number.py +++ b/homeassistant/components/esphome/number.py @@ -52,22 +52,22 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): """A number implementation for esphome.""" @property - def min_value(self) -> float: + def native_min_value(self) -> float: """Return the minimum value.""" return super()._static_info.min_value @property - def max_value(self) -> float: + def native_max_value(self) -> float: """Return the maximum value.""" return super()._static_info.max_value @property - def step(self) -> float: + def native_step(self) -> float: """Return the increment/decrement step.""" return super()._static_info.step @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" return super()._static_info.unit_of_measurement @@ -79,7 +79,7 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): return NumberMode.AUTO @esphome_state_property - def value(self) -> float | None: + def native_value(self) -> float | None: """Return the state of the entity.""" if math.isnan(self._state.state): return None @@ -87,6 +87,6 @@ class EsphomeNumber(EsphomeEntity[NumberInfo, NumberState], NumberEntity): return None return self._state.state - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Update the current value.""" await self._client.number_command(self._static_info.key, value) diff --git a/homeassistant/components/fjaraskupan/number.py b/homeassistant/components/fjaraskupan/number.py index 6314b9c9cc1..511d97cbed8 100644 --- a/homeassistant/components/fjaraskupan/number.py +++ b/homeassistant/components/fjaraskupan/number.py @@ -34,11 +34,11 @@ async def async_setup_entry( class PeriodicVentingTime(CoordinatorEntity[Coordinator], NumberEntity): """Periodic Venting.""" - _attr_max_value: float = 59 - _attr_min_value: float = 0 - _attr_step: float = 1 + _attr_native_max_value: float = 59 + _attr_native_min_value: float = 0 + _attr_native_step: float = 1 _attr_entity_category = EntityCategory.CONFIG - _attr_unit_of_measurement = TIME_MINUTES + _attr_native_unit_of_measurement = TIME_MINUTES def __init__( self, @@ -54,13 +54,13 @@ class PeriodicVentingTime(CoordinatorEntity[Coordinator], NumberEntity): self._attr_name = f"{device_info['name']} Periodic Venting" @property - def value(self) -> float | None: + def native_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: + async def async_set_native_value(self, value: float) -> None: """Set new value.""" await self._device.send_periodic_venting(int(value)) self.coordinator.async_set_updated_data(self._device.state) diff --git a/homeassistant/components/flux_led/number.py b/homeassistant/components/flux_led/number.py index 06f706aee21..65c8a955dcf 100644 --- a/homeassistant/components/flux_led/number.py +++ b/homeassistant/components/flux_led/number.py @@ -97,18 +97,18 @@ class FluxSpeedNumber( ): """Defines a flux_led speed number.""" - _attr_min_value = 1 - _attr_max_value = 100 - _attr_step = 1 + _attr_native_min_value = 1 + _attr_native_max_value = 100 + _attr_native_step = 1 _attr_mode = NumberMode.SLIDER _attr_icon = "mdi:speedometer" @property - def value(self) -> float: + def native_value(self) -> float: """Return the effect speed.""" return cast(float, self._device.speed) - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set the flux speed value.""" current_effect = self._device.effect new_speed = int(value) @@ -130,8 +130,8 @@ class FluxConfigNumber( """Base class for flux config numbers.""" _attr_entity_category = EntityCategory.CONFIG - _attr_min_value = 1 - _attr_step = 1 + _attr_native_min_value = 1 + _attr_native_step = 1 _attr_mode = NumberMode.BOX def __init__( @@ -153,18 +153,18 @@ class FluxConfigNumber( logger=_LOGGER, cooldown=DEBOUNCE_TIME, immediate=False, - function=self._async_set_value, + function=self._async_set_native_value, ) await super().async_added_to_hass() - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set the value.""" self._pending_value = int(value) assert self._debouncer is not None await self._debouncer.async_call() @abstractmethod - async def _async_set_value(self) -> None: + async def _async_set_native_value(self) -> None: """Call on debounce to set the value.""" def _pixels_and_segments_fit_in_music_mode(self) -> bool: @@ -189,19 +189,19 @@ class FluxPixelsPerSegmentNumber(FluxConfigNumber): _attr_icon = "mdi:dots-grid" @property - def max_value(self) -> int: + def native_max_value(self) -> int: """Return the max value.""" return min( PIXELS_PER_SEGMENT_MAX, int(PIXELS_MAX / (self._device.segments or 1)) ) @property - def value(self) -> int: + def native_value(self) -> int: """Return the pixels per segment.""" assert self._device.pixels_per_segment is not None return self._device.pixels_per_segment - async def _async_set_value(self) -> None: + async def _async_set_native_value(self) -> None: """Set the pixels per segment.""" assert self._pending_value is not None await self._device.async_set_device_config( @@ -215,7 +215,7 @@ class FluxSegmentsNumber(FluxConfigNumber): _attr_icon = "mdi:segment" @property - def max_value(self) -> int: + def native_max_value(self) -> int: """Return the max value.""" assert self._device.pixels_per_segment is not None return min( @@ -223,12 +223,12 @@ class FluxSegmentsNumber(FluxConfigNumber): ) @property - def value(self) -> int: + def native_value(self) -> int: """Return the segments.""" assert self._device.segments is not None return self._device.segments - async def _async_set_value(self) -> None: + async def _async_set_native_value(self) -> None: """Set the segments.""" assert self._pending_value is not None await self._device.async_set_device_config(segments=self._pending_value) @@ -249,7 +249,7 @@ class FluxMusicPixelsPerSegmentNumber(FluxMusicNumber): _attr_icon = "mdi:dots-grid" @property - def max_value(self) -> int: + def native_max_value(self) -> int: """Return the max value.""" assert self._device.music_segments is not None return min( @@ -258,12 +258,12 @@ class FluxMusicPixelsPerSegmentNumber(FluxMusicNumber): ) @property - def value(self) -> int: + def native_value(self) -> int: """Return the music pixels per segment.""" assert self._device.music_pixels_per_segment is not None return self._device.music_pixels_per_segment - async def _async_set_value(self) -> None: + async def _async_set_native_value(self) -> None: """Set the music pixels per segment.""" assert self._pending_value is not None await self._device.async_set_device_config( @@ -277,7 +277,7 @@ class FluxMusicSegmentsNumber(FluxMusicNumber): _attr_icon = "mdi:segment" @property - def max_value(self) -> int: + def native_max_value(self) -> int: """Return the max value.""" assert self._device.pixels_per_segment is not None return min( @@ -286,12 +286,12 @@ class FluxMusicSegmentsNumber(FluxMusicNumber): ) @property - def value(self) -> int: + def native_value(self) -> int: """Return the music segments.""" assert self._device.music_segments is not None return self._device.music_segments - async def _async_set_value(self) -> None: + async def _async_set_native_value(self) -> None: """Set the music segments.""" assert self._pending_value is not None await self._device.async_set_device_config(music_segments=self._pending_value) diff --git a/homeassistant/components/goodwe/number.py b/homeassistant/components/goodwe/number.py index 80c7885f26c..00d8d9d0cae 100644 --- a/homeassistant/components/goodwe/number.py +++ b/homeassistant/components/goodwe/number.py @@ -40,24 +40,24 @@ NUMBERS = ( name="Grid export limit", icon="mdi:transmission-tower", entity_category=EntityCategory.CONFIG, - unit_of_measurement=POWER_WATT, + native_unit_of_measurement=POWER_WATT, getter=lambda inv: inv.get_grid_export_limit(), setter=lambda inv, val: inv.set_grid_export_limit(val), - step=100, - min_value=0, - max_value=10000, + native_step=100, + native_min_value=0, + native_max_value=10000, ), GoodweNumberEntityDescription( key="battery_discharge_depth", name="Depth of discharge (on-grid)", icon="mdi:battery-arrow-down", entity_category=EntityCategory.CONFIG, - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, getter=lambda inv: inv.get_ongrid_battery_dod(), setter=lambda inv, val: inv.set_ongrid_battery_dod(val), - step=1, - min_value=0, - max_value=99, + native_step=1, + native_min_value=0, + native_max_value=99, ), ) @@ -105,12 +105,12 @@ class InverterNumberEntity(NumberEntity): self.entity_description = description self._attr_unique_id = f"{DOMAIN}-{description.key}-{inverter.serial_number}" self._attr_device_info = device_info - self._attr_value = float(current_value) + self._attr_native_value = float(current_value) self._inverter: Inverter = inverter - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set new value.""" if self.entity_description.setter: await self.entity_description.setter(self._inverter, int(value)) - self._attr_value = value + self._attr_native_value = value self.async_write_ha_state() diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index b994bc80f4a..4e0f5cfa077 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -104,26 +104,26 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity): return [self._char.type] @property - def min_value(self) -> float: + def native_min_value(self) -> float: """Return the minimum value.""" return self._char.minValue or DEFAULT_MIN_VALUE @property - def max_value(self) -> float: + def native_max_value(self) -> float: """Return the maximum value.""" return self._char.maxValue or DEFAULT_MAX_VALUE @property - def step(self) -> float: + def native_step(self) -> float: """Return the increment/decrement step.""" return self._char.minStep or DEFAULT_STEP @property - def value(self) -> float: + def native_value(self) -> float: """Return the current characteristic value.""" return self._char.value - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set the characteristic to this value.""" await self.async_put_characteristics( { @@ -148,26 +148,26 @@ class HomeKitEcobeeFanModeNumber(CharacteristicEntity, NumberEntity): return f"{prefix} Fan Mode" @property - def min_value(self) -> float: + def native_min_value(self) -> float: """Return the minimum value.""" return self._char.minValue or DEFAULT_MIN_VALUE @property - def max_value(self) -> float: + def native_max_value(self) -> float: """Return the maximum value.""" return self._char.maxValue or DEFAULT_MAX_VALUE @property - def step(self) -> float: + def native_step(self) -> float: """Return the increment/decrement step.""" return self._char.minStep or DEFAULT_STEP @property - def value(self) -> float: + def native_value(self) -> float: """Return the current characteristic value.""" return self._char.value - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set the characteristic to this value.""" # Sending the fan mode request sometimes ends up getting ignored by ecobee diff --git a/homeassistant/components/juicenet/number.py b/homeassistant/components/juicenet/number.py index 24b0ba4f42b..c7f444b83e2 100644 --- a/homeassistant/components/juicenet/number.py +++ b/homeassistant/components/juicenet/number.py @@ -29,16 +29,16 @@ class JuiceNetNumberEntityDescription( ): """An entity description for a JuiceNetNumber.""" - max_value_key: str | None = None + native_max_value_key: str | None = None NUMBER_TYPES: tuple[JuiceNetNumberEntityDescription, ...] = ( JuiceNetNumberEntityDescription( name="Amperage Limit", key="current_charging_amperage_limit", - min_value=6, - max_value_key="max_charging_amperage", - step=1, + native_min_value=6, + native_max_value_key="max_charging_amperage", + native_step=1, setter_key="set_charging_amperage_limit", ), ) @@ -80,19 +80,19 @@ class JuiceNetNumber(JuiceNetDevice, NumberEntity): self._attr_name = f"{self.device.name} {description.name}" @property - def value(self) -> float | None: + def native_value(self) -> float | None: """Return the value of the entity.""" return getattr(self.device, self.entity_description.key, None) @property - def max_value(self) -> float: + def native_max_value(self) -> float: """Return the maximum value.""" - if self.entity_description.max_value_key is not None: - return getattr(self.device, self.entity_description.max_value_key) - if self.entity_description.max_value is not None: - return self.entity_description.max_value + if self.entity_description.native_max_value_key is not None: + return getattr(self.device, self.entity_description.native_max_value_key) + if self.entity_description.native_max_value is not None: + return self.entity_description.native_max_value return DEFAULT_MAX_VALUE - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Update the current value.""" await getattr(self.device, self.entity_description.setter_key)(value) From 8e6fa54e0a297a8d933747d3025647f1006bce0f Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Tue, 14 Jun 2022 15:08:47 -0400 Subject: [PATCH 1487/3516] Improve PECO integration (#73460) --- homeassistant/components/peco/__init__.py | 3 --- homeassistant/components/peco/sensor.py | 4 +--- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/peco/__init__.py b/homeassistant/components/peco/__init__.py index 6f88bf36c50..86f69213a1c 100644 --- a/homeassistant/components/peco/__init__.py +++ b/homeassistant/components/peco/__init__.py @@ -1,7 +1,6 @@ """The PECO Outage Counter integration.""" from __future__ import annotations -import asyncio from dataclasses import dataclass from datetime import timedelta from typing import Final @@ -48,8 +47,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise UpdateFailed(f"Error fetching data: {err}") from err except BadJSONError as err: raise UpdateFailed(f"Error parsing data: {err}") from err - except asyncio.TimeoutError as err: - raise UpdateFailed(f"Timeout fetching data: {err}") from err return data coordinator = DataUpdateCoordinator( diff --git a/homeassistant/components/peco/sensor.py b/homeassistant/components/peco/sensor.py index dfd354f5c03..5afc300bfa8 100644 --- a/homeassistant/components/peco/sensor.py +++ b/homeassistant/components/peco/sensor.py @@ -93,10 +93,8 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( - [PecoSensor(sensor, county, coordinator) for sensor in SENSOR_LIST], - True, + PecoSensor(sensor, county, coordinator) for sensor in SENSOR_LIST ) - return class PecoSensor( From 28bf9db5a298e58bf3ab08148d647e41b0c0df07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 Jun 2022 11:04:43 -1000 Subject: [PATCH 1488/3516] Filter out forced updates in live logbook when the state has not changed (#73335) --- homeassistant/components/logbook/helpers.py | 20 +-- .../components/logbook/test_websocket_api.py | 114 ++++++++++++++++++ 2 files changed, 126 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index ef322c44e05..221612e1e97 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -189,9 +189,10 @@ def async_subscribe_events( def _forward_state_events_filtered(event: Event) -> None: if event.data.get("old_state") is None or event.data.get("new_state") is None: return - state: State = event.data["new_state"] - if _is_state_filtered(ent_reg, state) or ( - entities_filter and not entities_filter(state.entity_id) + new_state: State = event.data["new_state"] + old_state: State = event.data["old_state"] + if _is_state_filtered(ent_reg, new_state, old_state) or ( + entities_filter and not entities_filter(new_state.entity_id) ): return target(event) @@ -229,17 +230,20 @@ def is_sensor_continuous(ent_reg: er.EntityRegistry, entity_id: str) -> bool: ) -def _is_state_filtered(ent_reg: er.EntityRegistry, state: State) -> bool: +def _is_state_filtered( + ent_reg: er.EntityRegistry, new_state: State, old_state: State +) -> bool: """Check if the logbook should filter a state. Used when we are in live mode to ensure we only get significant changes (state.last_changed != state.last_updated) """ return bool( - split_entity_id(state.entity_id)[0] in ALWAYS_CONTINUOUS_DOMAINS - or state.last_changed != state.last_updated - or ATTR_UNIT_OF_MEASUREMENT in state.attributes - or is_sensor_continuous(ent_reg, state.entity_id) + new_state.state == old_state.state + or split_entity_id(new_state.entity_id)[0] in ALWAYS_CONTINUOUS_DOMAINS + or new_state.last_changed != new_state.last_updated + or ATTR_UNIT_OF_MEASUREMENT in new_state.attributes + or is_sensor_continuous(ent_reg, new_state.entity_id) ) diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 4df2f456eb6..ac6a31202e7 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -2404,3 +2404,117 @@ async def test_subscribe_entities_some_have_uom_multiple( # Check our listener got unsubscribed assert sum(hass.bus.async_listeners().values()) == init_count + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_logbook_stream_ignores_forced_updates( + hass, recorder_mock, hass_ws_client +): + """Test logbook live stream ignores forced updates.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + + await hass.async_block_till_done() + init_count = sum(hass.bus.async_listeners().values()) + + hass.states.async_set("binary_sensor.is_light", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + state: State = hass.states.get("binary_sensor.is_light") + await hass.async_block_till_done() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + {"id": 7, "type": "logbook/event_stream", "start_time": now.isoformat()} + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + { + "entity_id": "binary_sensor.is_light", + "state": "off", + "when": state.last_updated.timestamp(), + } + ] + assert msg["event"]["start_time"] == now.timestamp() + assert msg["event"]["end_time"] > msg["event"]["start_time"] + assert msg["event"]["partial"] is True + + await hass.async_block_till_done() + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [] + + hass.states.async_set("binary_sensor.is_light", STATE_ON) + hass.states.async_set("binary_sensor.is_light", STATE_OFF) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [ + { + "entity_id": "binary_sensor.is_light", + "state": STATE_ON, + "when": ANY, + }, + { + "entity_id": "binary_sensor.is_light", + "state": STATE_OFF, + "when": ANY, + }, + ] + + # Now we force an update to make sure we ignore + # forced updates when the state has not actually changed + + hass.states.async_set("binary_sensor.is_light", STATE_ON) + for _ in range(3): + hass.states.async_set("binary_sensor.is_light", STATE_OFF, force_update=True) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert "partial" not in msg["event"]["events"] + assert msg["event"]["events"] == [ + { + "entity_id": "binary_sensor.is_light", + "state": STATE_ON, + "when": ANY, + }, + # We should only get the first one and ignore + # the other forced updates since the state + # has not actually changed + { + "entity_id": "binary_sensor.is_light", + "state": STATE_OFF, + "when": ANY, + }, + ] + + await websocket_client.send_json( + {"id": 8, "type": "unsubscribe_events", "subscription": 7} + ) + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + + assert msg["id"] == 8 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count From 003de09c48e78ebd6d002e97898b79db7c6d1720 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sat, 11 Jun 2022 02:13:50 -0400 Subject: [PATCH 1489/3516] Fix zwave_js add node schemas (#73343) * Fix zwave_js add node schemas * Code cleanup * Add test --- homeassistant/components/zwave_js/api.py | 32 +++++++++-- tests/components/zwave_js/test_api.py | 70 ++++++++++++++++++++++-- 2 files changed, 90 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 5b8b95e951c..27a0c065c0b 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -14,6 +14,7 @@ from zwave_js_server.const import ( InclusionStrategy, LogLevel, Protocols, + ProvisioningEntryStatus, QRCodeVersion, SecurityClass, ZwaveFeature, @@ -148,6 +149,8 @@ MAX_INCLUSION_REQUEST_INTERVAL = "max_inclusion_request_interval" UUID = "uuid" SUPPORTED_PROTOCOLS = "supported_protocols" ADDITIONAL_PROPERTIES = "additional_properties" +STATUS = "status" +REQUESTED_SECURITY_CLASSES = "requested_security_classes" FEATURE = "feature" UNPROVISION = "unprovision" @@ -160,19 +163,22 @@ def convert_planned_provisioning_entry(info: dict) -> ProvisioningEntry: """Handle provisioning entry dict to ProvisioningEntry.""" return ProvisioningEntry( dsk=info[DSK], - security_classes=[SecurityClass(sec_cls) for sec_cls in info[SECURITY_CLASSES]], + security_classes=info[SECURITY_CLASSES], + status=info[STATUS], + requested_security_classes=info.get(REQUESTED_SECURITY_CLASSES), additional_properties={ - k: v for k, v in info.items() if k not in (DSK, SECURITY_CLASSES) + k: v + for k, v in info.items() + if k not in (DSK, SECURITY_CLASSES, STATUS, REQUESTED_SECURITY_CLASSES) }, ) def convert_qr_provisioning_information(info: dict) -> QRProvisioningInformation: """Convert QR provisioning information dict to QRProvisioningInformation.""" - protocols = [Protocols(proto) for proto in info.get(SUPPORTED_PROTOCOLS, [])] return QRProvisioningInformation( - version=QRCodeVersion(info[VERSION]), - security_classes=[SecurityClass(sec_cls) for sec_cls in info[SECURITY_CLASSES]], + version=info[VERSION], + security_classes=info[SECURITY_CLASSES], dsk=info[DSK], generic_device_class=info[GENERIC_DEVICE_CLASS], specific_device_class=info[SPECIFIC_DEVICE_CLASS], @@ -183,7 +189,9 @@ def convert_qr_provisioning_information(info: dict) -> QRProvisioningInformation application_version=info[APPLICATION_VERSION], max_inclusion_request_interval=info.get(MAX_INCLUSION_REQUEST_INTERVAL), uuid=info.get(UUID), - supported_protocols=protocols if protocols else None, + supported_protocols=info.get(SUPPORTED_PROTOCOLS), + status=info[STATUS], + requested_security_classes=info.get(REQUESTED_SECURITY_CLASSES), additional_properties=info.get(ADDITIONAL_PROPERTIES, {}), ) @@ -197,6 +205,12 @@ PLANNED_PROVISIONING_ENTRY_SCHEMA = vol.All( cv.ensure_list, [vol.Coerce(SecurityClass)], ), + vol.Optional(STATUS, default=ProvisioningEntryStatus.ACTIVE): vol.Coerce( + ProvisioningEntryStatus + ), + vol.Optional(REQUESTED_SECURITY_CLASSES): vol.All( + cv.ensure_list, [vol.Coerce(SecurityClass)] + ), }, # Provisioning entries can have extra keys for SmartStart extra=vol.ALLOW_EXTRA, @@ -226,6 +240,12 @@ QR_PROVISIONING_INFORMATION_SCHEMA = vol.All( cv.ensure_list, [vol.Coerce(Protocols)], ), + vol.Optional(STATUS, default=ProvisioningEntryStatus.ACTIVE): vol.Coerce( + ProvisioningEntryStatus + ), + vol.Optional(REQUESTED_SECURITY_CLASSES): vol.All( + cv.ensure_list, [vol.Coerce(SecurityClass)] + ), vol.Optional(ADDITIONAL_PROPERTIES): dict, } ), diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index e59a923ff44..e2ab9cc9503 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -10,6 +10,7 @@ from zwave_js_server.const import ( InclusionStrategy, LogLevel, Protocols, + ProvisioningEntryStatus, QRCodeVersion, SecurityClass, ZwaveFeature, @@ -63,8 +64,10 @@ from homeassistant.components.zwave_js.api import ( PROPERTY_KEY, QR_CODE_STRING, QR_PROVISIONING_INFORMATION, + REQUESTED_SECURITY_CLASSES, SECURITY_CLASSES, SPECIFIC_DEVICE_CLASS, + STATUS, TYPE, UNPROVISION, VALUE, @@ -619,13 +622,68 @@ async def test_add_node( client.async_send_command.reset_mock() client.async_send_command.return_value = {"success": True} - # Test S2 QR code string + # Test S2 QR provisioning information await ws_client.send_json( { ID: 4, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id, INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value, + QR_PROVISIONING_INFORMATION: { + VERSION: 0, + SECURITY_CLASSES: [0], + DSK: "test", + GENERIC_DEVICE_CLASS: 1, + SPECIFIC_DEVICE_CLASS: 1, + INSTALLER_ICON_TYPE: 1, + MANUFACTURER_ID: 1, + PRODUCT_TYPE: 1, + PRODUCT_ID: 1, + APPLICATION_VERSION: "test", + STATUS: 1, + REQUESTED_SECURITY_CLASSES: [0], + }, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command.call_args_list) == 1 + assert client.async_send_command.call_args[0][0] == { + "command": "controller.begin_inclusion", + "options": { + "strategy": InclusionStrategy.SECURITY_S2, + "provisioning": QRProvisioningInformation( + version=QRCodeVersion.S2, + security_classes=[SecurityClass.S2_UNAUTHENTICATED], + dsk="test", + generic_device_class=1, + specific_device_class=1, + installer_icon_type=1, + manufacturer_id=1, + product_type=1, + product_id=1, + application_version="test", + max_inclusion_request_interval=None, + uuid=None, + supported_protocols=None, + status=ProvisioningEntryStatus.INACTIVE, + requested_security_classes=[SecurityClass.S2_UNAUTHENTICATED], + ).to_dict(), + }, + } + + client.async_send_command.reset_mock() + client.async_send_command.return_value = {"success": True} + + # Test S2 QR code string + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/add_node", + ENTRY_ID: entry.entry_id, + INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value, QR_CODE_STRING: "90testtesttesttesttesttesttesttesttesttesttesttesttest", } ) @@ -648,7 +706,7 @@ async def test_add_node( # Test Smart Start QR provisioning information with S2 inclusion strategy fails await ws_client.send_json( { - ID: 5, + ID: 6, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id, INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S2.value, @@ -678,7 +736,7 @@ async def test_add_node( # Test QR provisioning information with S0 inclusion strategy fails await ws_client.send_json( { - ID: 5, + ID: 7, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id, INCLUSION_STRATEGY: InclusionStrategy.SECURITY_S0, @@ -708,7 +766,7 @@ async def test_add_node( # Test ValueError is caught as failure await ws_client.send_json( { - ID: 6, + ID: 8, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id, INCLUSION_STRATEGY: InclusionStrategy.DEFAULT.value, @@ -728,7 +786,7 @@ async def test_add_node( ): await ws_client.send_json( { - ID: 7, + ID: 9, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id, } @@ -744,7 +802,7 @@ async def test_add_node( await hass.async_block_till_done() await ws_client.send_json( - {ID: 8, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id} + {ID: 10, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id} ) msg = await ws_client.receive_json() From 8dc61a8c2e650a6e4cca2c1f26b2b38153636698 Mon Sep 17 00:00:00 2001 From: Khole Date: Sat, 11 Jun 2022 20:43:57 +0100 Subject: [PATCH 1490/3516] Hive Bump pyhiveapi to 0.5.10 for credentials fix (#73365) --- homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index e0faa9e0f20..bc07a251779 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,7 +3,7 @@ "name": "Hive", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": ["pyhiveapi==0.5.9"], + "requirements": ["pyhiveapi==0.5.10"], "codeowners": ["@Rendili", "@KJonline"], "iot_class": "cloud_polling", "loggers": ["apyhiveapi"] diff --git a/requirements_all.txt b/requirements_all.txt index 86e7ea1fc44..828e0882df9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1538,7 +1538,7 @@ pyheos==0.7.2 pyhik==0.3.0 # homeassistant.components.hive -pyhiveapi==0.5.9 +pyhiveapi==0.5.10 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d656be540fb..2b5b0ac2bd2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1029,7 +1029,7 @@ pyhaversion==22.4.1 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.5.9 +pyhiveapi==0.5.10 # homeassistant.components.homematic pyhomematic==0.1.77 From 7b7fc125132557406e057b1ac072344cda7c4cc1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Jun 2022 17:29:44 -1000 Subject: [PATCH 1491/3516] Fix reload race in yeelight when updating the ip address (#73390) --- .../components/yeelight/config_flow.py | 5 ++- tests/components/yeelight/test_config_flow.py | 31 ++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index 8a3a5b41320..440b717fd8c 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -96,7 +96,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_update_entry( entry, data={**entry.data, CONF_HOST: self._discovered_ip} ) - reload = True + reload = entry.state in ( + ConfigEntryState.SETUP_RETRY, + ConfigEntryState.LOADED, + ) if reload: self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 80acaa6f10e..1c19a5e7dfd 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -739,7 +739,7 @@ async def test_discovered_zeroconf(hass): async def test_discovery_updates_ip(hass: HomeAssistant): - """Test discovery updtes ip.""" + """Test discovery updates ip.""" config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "1.2.2.3"}, unique_id=ID ) @@ -761,6 +761,35 @@ async def test_discovery_updates_ip(hass: HomeAssistant): assert config_entry.data[CONF_HOST] == IP_ADDRESS +async def test_discovery_updates_ip_no_reload_setup_in_progress(hass: HomeAssistant): + """Test discovery updates ip does not reload if setup is an an error state.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.2.2.3"}, + unique_id=ID, + state=config_entries.ConfigEntryState.SETUP_ERROR, + ) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb() + with patch( + f"{MODULE}.async_setup_entry", return_value=True + ) as mock_setup_entry, _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ): + 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"] == "already_configured" + assert config_entry.data[CONF_HOST] == IP_ADDRESS + assert len(mock_setup_entry.mock_calls) == 0 + + async def test_discovery_adds_missing_ip_id_only(hass: HomeAssistant): """Test discovery adds missing ip.""" config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_ID: ID}) From c59b03e3dc7ed1abb55c117f52ecf9a9cae23bfe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 12 Jun 2022 17:04:17 -1000 Subject: [PATCH 1492/3516] Only update unifiprotect ips from discovery when the console is offline (#73411) --- .../components/unifiprotect/config_flow.py | 32 ++++++++++++++- homeassistant/components/unifiprotect/data.py | 10 +++++ .../components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../unifiprotect/test_config_flow.py | 40 ++++++++++++++++++- 6 files changed, 83 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index daaae214df9..d9fd3a87cb0 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -7,6 +7,7 @@ from typing import Any from aiohttp import CookieJar from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient from pyunifiprotect.data import NVR +from unifi_discovery import async_console_is_alive import voluptuous as vol from homeassistant import config_entries @@ -21,7 +22,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.aiohttp_client import ( + async_create_clientsession, + async_get_clientsession, +) from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.loader import async_get_integration from homeassistant.util.network import is_ip_address @@ -36,11 +40,17 @@ from .const import ( MIN_REQUIRED_PROTECT_V, OUTDATED_LOG_MESSAGE, ) +from .data import async_last_update_was_successful from .discovery import async_start_discovery from .utils import _async_resolve, _async_short_mac, _async_unifi_mac_from_hass _LOGGER = logging.getLogger(__name__) +ENTRY_FAILURE_STATES = ( + config_entries.ConfigEntryState.SETUP_ERROR, + config_entries.ConfigEntryState.SETUP_RETRY, +) + async def async_local_user_documentation_url(hass: HomeAssistant) -> str: """Get the documentation url for creating a local user.""" @@ -53,6 +63,25 @@ def _host_is_direct_connect(host: str) -> bool: return host.endswith(".ui.direct") +async def _async_console_is_offline( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, +) -> bool: + """Check if a console is offline. + + We define offline by the config entry + is in a failure/retry state or the updates + are failing and the console is unreachable + since protect may be updating. + """ + return bool( + entry.state in ENTRY_FAILURE_STATES + or not async_last_update_was_successful(hass, entry) + ) and not await async_console_is_alive( + async_get_clientsession(hass, verify_ssl=False), entry.data[CONF_HOST] + ) + + class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a UniFi Protect config flow.""" @@ -110,6 +139,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): not entry_has_direct_connect and is_ip_address(entry_host) and entry_host != source_ip + and await _async_console_is_offline(self.hass, entry) ): new_host = source_ip if new_host: diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 371c1c7831b..02cfa6c16ff 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -25,6 +25,16 @@ from .const import CONF_DISABLE_RTSP, DEVICES_THAT_ADOPT, DEVICES_WITH_ENTITIES _LOGGER = logging.getLogger(__name__) +@callback +def async_last_update_was_successful(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Check if the last update was successful for a config entry.""" + return bool( + DOMAIN in hass.data + and entry.entry_id in hass.data[DOMAIN] + and hass.data[DOMAIN][entry.entry_id].last_update_success + ) + + class ProtectData: """Coordinate updates.""" diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 199298d76ca..2554d12c866 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.9.2", "unifi-discovery==1.1.3"], + "requirements": ["pyunifiprotect==3.9.2", "unifi-discovery==1.1.4"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 828e0882df9..c129fb2cb54 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2358,7 +2358,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.unifiprotect -unifi-discovery==1.1.3 +unifi-discovery==1.1.4 # homeassistant.components.unifiled unifiled==0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b5b0ac2bd2..ea6cd6a144a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1546,7 +1546,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.unifiprotect -unifi-discovery==1.1.3 +unifi-discovery==1.1.4 # homeassistant.components.upb upb_lib==0.4.12 diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 80e845591b1..75f08acb37c 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -402,7 +402,10 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin ) mock_config.add_to_hass(hass) - with _patch_discovery(): + with _patch_discovery(), patch( + "homeassistant.components.unifiprotect.config_flow.async_console_is_alive", + return_value=False, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, @@ -415,6 +418,41 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin assert mock_config.data[CONF_HOST] == "127.0.0.1" +async def test_discovered_by_unifi_discovery_does_not_update_ip_when_console_is_still_online( + hass: HomeAssistant, mock_nvr: NVR +) -> None: + """Test a discovery from unifi-discovery does not update the ip unless the console at the old ip is offline.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.2.2.2", + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect", + "port": 443, + "verify_ssl": False, + }, + version=2, + unique_id=DEVICE_MAC_ADDRESS.replace(":", "").upper(), + ) + mock_config.add_to_hass(hass) + + with _patch_discovery(), patch( + "homeassistant.components.unifiprotect.config_flow.async_console_is_alive", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=UNIFI_DISCOVERY_DICT, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert mock_config.data[CONF_HOST] == "1.2.2.2" + + async def test_discovered_host_not_updated_if_existing_is_a_hostname( hass: HomeAssistant, mock_nvr: NVR ) -> None: From 8822feaf854c75605bdf85176e6976c8841e7f87 Mon Sep 17 00:00:00 2001 From: Marcio Granzotto Rodrigues Date: Sun, 12 Jun 2022 22:27:18 -0300 Subject: [PATCH 1493/3516] Fix smart by bond detection with v3 firmware (#73414) --- homeassistant/components/bond/manifest.json | 2 +- homeassistant/components/bond/utils.py | 5 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bond/common.py | 11 +++++- tests/components/bond/test_config_flow.py | 24 ++++++------ tests/components/bond/test_diagnostics.py | 2 +- tests/components/bond/test_init.py | 41 ++++++++++++++------- tests/components/bond/test_light.py | 2 +- 9 files changed, 58 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json index 52e9dd1763f..a5625d7b642 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-async==0.1.20"], + "requirements": ["bond-async==0.1.22"], "zeroconf": ["_bond._tcp.local."], "codeowners": ["@bdraco", "@prystupa", "@joshs85", "@marciogranzotto"], "quality_scale": "platinum", diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index cba213d9450..c426bf64577 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -5,7 +5,7 @@ import logging from typing import Any, cast from aiohttp import ClientResponseError -from bond_async import Action, Bond +from bond_async import Action, Bond, BondType from homeassistant.util.async_ import gather_with_concurrency @@ -224,4 +224,5 @@ class BondHub: @property def is_bridge(self) -> bool: """Return if the Bond is a Bond Bridge.""" - return bool(self._bridge) + bondid = self._version["bondid"] + return bool(BondType.is_bridge_from_serial(bondid)) diff --git a/requirements_all.txt b/requirements_all.txt index c129fb2cb54..a580a474b68 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -417,7 +417,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bond -bond-async==0.1.20 +bond-async==0.1.22 # homeassistant.components.bosch_shc boschshcpy==0.2.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea6cd6a144a..bdbe9ba1e7c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -318,7 +318,7 @@ blebox_uniapi==1.3.3 blinkpy==0.19.0 # homeassistant.components.bond -bond-async==0.1.20 +bond-async==0.1.22 # homeassistant.components.bosch_shc boschshcpy==0.2.30 diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index 4b45a4016c0..909fb35a1e2 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -113,7 +113,7 @@ def patch_bond_version( return nullcontext() if return_value is None: - return_value = {"bondid": "test-bond-id"} + return_value = {"bondid": "ZXXX12345"} return patch( "homeassistant.components.bond.Bond.version", @@ -246,3 +246,12 @@ async def help_test_entity_available( async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) await hass.async_block_till_done() assert hass.states.get(entity_id).state != STATE_UNAVAILABLE + + +def ceiling_fan(name: str): + """Create a ceiling fan with given name.""" + return { + "name": name, + "type": DeviceType.CEILING_FAN, + "actions": ["SetSpeed", "SetDirection"], + } diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index 5d3b357b9f7..519fa9dec9d 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -35,7 +35,7 @@ async def test_user_form(hass: core.HomeAssistant): assert result["errors"] == {} with patch_bond_version( - return_value={"bondid": "test-bond-id"} + return_value={"bondid": "ZXXX12345"} ), patch_bond_device_ids( return_value=["f6776c11", "f6776c12"] ), patch_bond_bridge(), patch_bond_device_properties(), patch_bond_device(), _patch_async_setup_entry() as mock_setup_entry: @@ -64,7 +64,7 @@ async def test_user_form_with_non_bridge(hass: core.HomeAssistant): assert result["errors"] == {} with patch_bond_version( - return_value={"bondid": "test-bond-id"} + return_value={"bondid": "KXXX12345"} ), patch_bond_device_ids( return_value=["f6776c11"] ), patch_bond_device_properties(), patch_bond_device( @@ -96,7 +96,7 @@ async def test_user_form_invalid_auth(hass: core.HomeAssistant): ) with patch_bond_version( - return_value={"bond_id": "test-bond-id"} + return_value={"bond_id": "ZXXX12345"} ), patch_bond_bridge(), patch_bond_device_ids( side_effect=ClientResponseError(Mock(), Mock(), status=401), ): @@ -203,7 +203,7 @@ async def test_zeroconf_form(hass: core.HomeAssistant): host="test-host", addresses=["test-host"], hostname="mock_hostname", - name="test-bond-id.some-other-tail-info", + name="ZXXX12345.some-other-tail-info", port=None, properties={}, type="mock_type", @@ -213,7 +213,7 @@ async def test_zeroconf_form(hass: core.HomeAssistant): assert result["errors"] == {} with patch_bond_version( - return_value={"bondid": "test-bond-id"} + return_value={"bondid": "ZXXX12345"} ), patch_bond_bridge(), patch_bond_device_ids(), _patch_async_setup_entry() as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -241,7 +241,7 @@ async def test_zeroconf_form_token_unavailable(hass: core.HomeAssistant): host="test-host", addresses=["test-host"], hostname="mock_hostname", - name="test-bond-id.some-other-tail-info", + name="ZXXX12345.some-other-tail-info", port=None, properties={}, type="mock_type", @@ -270,7 +270,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.""" - with patch_bond_version(return_value={"bondid": "test-bond-id"}), patch_bond_token( + with patch_bond_version(return_value={"bondid": "ZXXX12345"}), patch_bond_token( return_value={"token": "discovered-token"} ), patch_bond_bridge( return_value={"name": "discovered-name"} @@ -282,7 +282,7 @@ async def test_zeroconf_form_with_token_available(hass: core.HomeAssistant): host="test-host", addresses=["test-host"], hostname="mock_hostname", - name="test-bond-id.some-other-tail-info", + name="ZXXX12345.some-other-tail-info", port=None, properties={}, type="mock_type", @@ -323,7 +323,7 @@ async def test_zeroconf_form_with_token_available_name_unavailable( host="test-host", addresses=["test-host"], hostname="mock_hostname", - name="test-bond-id.some-other-tail-info", + name="ZXXX12345.some-other-tail-info", port=None, properties={}, type="mock_type", @@ -341,7 +341,7 @@ async def test_zeroconf_form_with_token_available_name_unavailable( await hass.async_block_till_done() assert result2["type"] == "create_entry" - assert result2["title"] == "test-bond-id" + assert result2["title"] == "ZXXX12345" assert result2["data"] == { CONF_HOST: "test-host", CONF_ACCESS_TOKEN: "discovered-token", @@ -472,7 +472,7 @@ async def test_zeroconf_form_unexpected_error(hass: core.HomeAssistant): host="test-host", addresses=["test-host"], hostname="mock_hostname", - name="test-bond-id.some-other-tail-info", + name="ZXXX12345.some-other-tail-info", port=None, properties={}, type="mock_type", @@ -497,7 +497,7 @@ async def _help_test_form_unexpected_error( ) with patch_bond_version( - return_value={"bond_id": "test-bond-id"} + return_value={"bond_id": "ZXXX12345"} ), patch_bond_device_ids(side_effect=error): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input diff --git a/tests/components/bond/test_diagnostics.py b/tests/components/bond/test_diagnostics.py index 88d33ff2cc0..b738c72ee8c 100644 --- a/tests/components/bond/test_diagnostics.py +++ b/tests/components/bond/test_diagnostics.py @@ -39,5 +39,5 @@ async def test_diagnostics(hass, hass_client): "data": {"access_token": "**REDACTED**", "host": "some host"}, "title": "Mock Title", }, - "hub": {"version": {"bondid": "test-bond-id"}}, + "hub": {"version": {"bondid": "ZXXX12345"}}, } diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 03eb490b65e..56087d4bf11 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -7,13 +7,15 @@ from bond_async import DeviceType import pytest from homeassistant.components.bond.const import DOMAIN +from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST +from homeassistant.const import ATTR_ASSUMED_STATE, CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from .common import ( + ceiling_fan, patch_bond_bridge, patch_bond_device, patch_bond_device_ids, @@ -23,6 +25,7 @@ from .common import ( patch_setup_entry, patch_start_bpup, setup_bond_entity, + setup_platform, ) from tests.common import MockConfigEntry @@ -81,7 +84,7 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss with patch_bond_bridge(), patch_bond_version( return_value={ - "bondid": "test-bond-id", + "bondid": "ZXXX12345", "target": "test-model", "fw_ver": "test-version", "mcu_ver": "test-hw-version", @@ -99,11 +102,11 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss assert config_entry.entry_id in hass.data[DOMAIN] assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.unique_id == "test-bond-id" + assert config_entry.unique_id == "ZXXX12345" # verify hub device is registered correctly device_registry = dr.async_get(hass) - hub = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")}) + hub = device_registry.async_get_device(identifiers={(DOMAIN, "ZXXX12345")}) assert hub.name == "bond-name" assert hub.manufacturer == "Olibra" assert hub.model == "test-model" @@ -151,7 +154,7 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant): ) old_identifers = (DOMAIN, "device_id") - new_identifiers = (DOMAIN, "test-bond-id", "device_id") + new_identifiers = (DOMAIN, "ZXXX12345", "device_id") device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -164,7 +167,7 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant): with patch_bond_bridge(), patch_bond_version( return_value={ - "bondid": "test-bond-id", + "bondid": "ZXXX12345", "target": "test-model", "fw_ver": "test-version", } @@ -185,7 +188,7 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant): assert config_entry.entry_id in hass.data[DOMAIN] assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.unique_id == "test-bond-id" + assert config_entry.unique_id == "ZXXX12345" # verify the device info is cleaned up assert device_registry.async_get_device(identifiers={old_identifers}) is None @@ -205,7 +208,7 @@ async def test_smart_by_bond_device_suggested_area(hass: HomeAssistant): side_effect=ClientResponseError(Mock(), Mock(), status=404) ), patch_bond_version( return_value={ - "bondid": "test-bond-id", + "bondid": "KXXX12345", "target": "test-model", "fw_ver": "test-version", } @@ -227,10 +230,10 @@ async def test_smart_by_bond_device_suggested_area(hass: HomeAssistant): assert config_entry.entry_id in hass.data[DOMAIN] assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.unique_id == "test-bond-id" + assert config_entry.unique_id == "KXXX12345" device_registry = dr.async_get(hass) - device = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")}) + device = device_registry.async_get_device(identifiers={(DOMAIN, "KXXX12345")}) assert device is not None assert device.suggested_area == "Den" @@ -251,7 +254,7 @@ async def test_bridge_device_suggested_area(hass: HomeAssistant): } ), patch_bond_version( return_value={ - "bondid": "test-bond-id", + "bondid": "ZXXX12345", "target": "test-model", "fw_ver": "test-version", } @@ -273,9 +276,21 @@ async def test_bridge_device_suggested_area(hass: HomeAssistant): assert config_entry.entry_id in hass.data[DOMAIN] assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.unique_id == "test-bond-id" + assert config_entry.unique_id == "ZXXX12345" device_registry = dr.async_get(hass) - device = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")}) + device = device_registry.async_get_device(identifiers={(DOMAIN, "ZXXX12345")}) assert device is not None assert device.suggested_area == "Office" + + +async def test_smart_by_bond_v3_firmware(hass: HomeAssistant) -> None: + """Test we can detect smart by bond with the v3 firmware.""" + await setup_platform( + hass, + FAN_DOMAIN, + ceiling_fan("name-1"), + bond_version={"bondid": "KXXXX12345", "target": "breck-northstar"}, + bond_device_id="test-device-id", + ) + assert ATTR_ASSUMED_STATE not in hass.states.get("fan.name_1").attributes diff --git a/tests/components/bond/test_light.py b/tests/components/bond/test_light.py index c7d8f195423..7577b1d70ab 100644 --- a/tests/components/bond/test_light.py +++ b/tests/components/bond/test_light.py @@ -249,7 +249,7 @@ async def test_sbb_trust_state(hass: core.HomeAssistant): """Assumed state should be False if device is a Smart by Bond.""" version = { "model": "MR123A", - "bondid": "test-bond-id", + "bondid": "KXXX12345", } await setup_platform( hass, LIGHT_DOMAIN, ceiling_fan("name-1"), bond_version=version, bridge={} From 1ab91bcf0f440d0f91c6ded3d631f69812d6bc13 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 12 Jun 2022 22:33:45 -0700 Subject: [PATCH 1494/3516] Bump aiohue to 4.4.2 (#73420) --- 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 d3b492f3b9e..b3dbe4df50a 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==4.4.1"], + "requirements": ["aiohue==4.4.2"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index a580a474b68..c90229fdb76 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -169,7 +169,7 @@ aiohomekit==0.7.17 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.4.1 +aiohue==4.4.2 # homeassistant.components.imap aioimaplib==0.9.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bdbe9ba1e7c..881aa1cef55 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -153,7 +153,7 @@ aiohomekit==0.7.17 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.4.1 +aiohue==4.4.2 # homeassistant.components.apache_kafka aiokafka==0.6.0 From d6bfb86da266b8ac2c167178be4b6d403610b5d3 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 14 Jun 2022 06:19:22 -0700 Subject: [PATCH 1495/3516] Fix fan support in nest, removing FAN_ONLY which isn't supported (#73422) * Fix fan support in nest, removing FAN_ONLY which isn't supported * Revert change to make supported features dynamic --- homeassistant/components/nest/climate_sdm.py | 37 ++-- tests/components/nest/test_climate_sdm.py | 195 ++++++++++--------- 2 files changed, 119 insertions(+), 113 deletions(-) diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 8a56f78028b..6ee988b714f 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -70,6 +70,7 @@ FAN_MODE_MAP = { "OFF": FAN_OFF, } FAN_INV_MODE_MAP = {v: k for k, v in FAN_MODE_MAP.items()} +FAN_INV_MODES = list(FAN_INV_MODE_MAP) MAX_FAN_DURATION = 43200 # 15 hours is the max in the SDM API MIN_TEMP = 10 @@ -99,7 +100,7 @@ class ThermostatEntity(ClimateEntity): """Initialize ThermostatEntity.""" self._device = device self._device_info = NestDeviceInfo(device) - self._supported_features = 0 + self._attr_supported_features = 0 @property def should_poll(self) -> bool: @@ -124,7 +125,7 @@ class ThermostatEntity(ClimateEntity): async def async_added_to_hass(self) -> None: """Run when entity is added to register update signal handler.""" - self._supported_features = self._get_supported_features() + self._attr_supported_features = self._get_supported_features() self.async_on_remove( self._device.add_update_listener(self.async_write_ha_state) ) @@ -198,8 +199,6 @@ class ThermostatEntity(ClimateEntity): trait = self._device.traits[ThermostatModeTrait.NAME] if trait.mode in THERMOSTAT_MODE_MAP: hvac_mode = THERMOSTAT_MODE_MAP[trait.mode] - if hvac_mode == HVACMode.OFF and self.fan_mode == FAN_ON: - hvac_mode = HVACMode.FAN_ONLY return hvac_mode @property @@ -209,8 +208,6 @@ class ThermostatEntity(ClimateEntity): for mode in self._get_device_hvac_modes: if mode in THERMOSTAT_MODE_MAP: supported_modes.append(THERMOSTAT_MODE_MAP[mode]) - if self.supported_features & ClimateEntityFeature.FAN_MODE: - supported_modes.append(HVACMode.FAN_ONLY) return supported_modes @property @@ -252,7 +249,10 @@ class ThermostatEntity(ClimateEntity): @property def fan_mode(self) -> str: """Return the current fan mode.""" - if FanTrait.NAME in self._device.traits: + if ( + self.supported_features & ClimateEntityFeature.FAN_MODE + and FanTrait.NAME in self._device.traits + ): trait = self._device.traits[FanTrait.NAME] return FAN_MODE_MAP.get(trait.timer_mode, FAN_OFF) return FAN_OFF @@ -260,15 +260,12 @@ class ThermostatEntity(ClimateEntity): @property def fan_modes(self) -> list[str]: """Return the list of available fan modes.""" - modes = [] - if FanTrait.NAME in self._device.traits: - modes = list(FAN_INV_MODE_MAP) - return modes - - @property - def supported_features(self) -> int: - """Bitmap of supported features.""" - return self._supported_features + if ( + self.supported_features & ClimateEntityFeature.FAN_MODE + and FanTrait.NAME in self._device.traits + ): + return FAN_INV_MODES + return [] def _get_supported_features(self) -> int: """Compute the bitmap of supported features from the current state.""" @@ -290,10 +287,6 @@ class ThermostatEntity(ClimateEntity): """Set new target hvac mode.""" if hvac_mode not in self.hvac_modes: raise ValueError(f"Unsupported hvac_mode '{hvac_mode}'") - if hvac_mode == HVACMode.FAN_ONLY: - # Turn the fan on but also turn off the hvac if it is on - await self.async_set_fan_mode(FAN_ON) - hvac_mode = HVACMode.OFF api_mode = THERMOSTAT_INV_MODE_MAP[hvac_mode] trait = self._device.traits[ThermostatModeTrait.NAME] try: @@ -338,6 +331,10 @@ class ThermostatEntity(ClimateEntity): """Set new target fan mode.""" if fan_mode not in self.fan_modes: raise ValueError(f"Unsupported fan_mode '{fan_mode}'") + if fan_mode == FAN_ON and self.hvac_mode == HVACMode.OFF: + raise ValueError( + "Cannot turn on fan, please set an HVAC mode (e.g. heat/cool) first" + ) trait = self._device.traits[FanTrait.NAME] duration = None if fan_mode != FAN_OFF: diff --git a/tests/components/nest/test_climate_sdm.py b/tests/components/nest/test_climate_sdm.py index 123742607ad..c271687a348 100644 --- a/tests/components/nest/test_climate_sdm.py +++ b/tests/components/nest/test_climate_sdm.py @@ -33,15 +33,15 @@ from homeassistant.components.climate.const import ( FAN_ON, HVAC_MODE_COOL, HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_ECO, PRESET_NONE, PRESET_SLEEP, + ClimateEntityFeature, ) -from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.const import ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -794,7 +794,7 @@ async def test_thermostat_fan_off( "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, "sdm.devices.traits.ThermostatMode": { "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], - "mode": "OFF", + "mode": "COOL", }, "sdm.devices.traits.Temperature": { "ambientTemperatureCelsius": 16.2, @@ -806,18 +806,22 @@ async def test_thermostat_fan_off( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.state == HVAC_MODE_COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_OFF assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + ) async def test_thermostat_fan_on( @@ -837,7 +841,7 @@ async def test_thermostat_fan_on( }, "sdm.devices.traits.ThermostatMode": { "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], - "mode": "OFF", + "mode": "COOL", }, "sdm.devices.traits.Temperature": { "ambientTemperatureCelsius": 16.2, @@ -849,18 +853,22 @@ async def test_thermostat_fan_on( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_FAN_ONLY + assert thermostat.state == HVAC_MODE_COOL assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + ) async def test_thermostat_cool_with_fan( @@ -895,11 +903,15 @@ async def test_thermostat_cool_with_fan( HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + ) async def test_thermostat_set_fan( @@ -907,6 +919,68 @@ async def test_thermostat_set_fan( setup_platform: PlatformSetup, auth: FakeAuth, create_device: CreateDevice, +) -> None: + """Test a thermostat enabling the fan.""" + create_device.create( + { + "sdm.devices.traits.Fan": { + "timerMode": "ON", + "timerTimeout": "2019-05-10T03:22:54Z", + }, + "sdm.devices.traits.ThermostatHvac": { + "status": "OFF", + }, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "HEAT", + }, + } + ) + await setup_platform() + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_HEAT + assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON + assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + ) + + # Turn off fan mode + await common.async_set_fan_mode(hass, FAN_OFF) + await hass.async_block_till_done() + + assert auth.method == "post" + assert auth.url == DEVICE_COMMAND + assert auth.json == { + "command": "sdm.devices.commands.Fan.SetTimer", + "params": {"timerMode": "OFF"}, + } + + # Turn on fan mode + await common.async_set_fan_mode(hass, FAN_ON) + await hass.async_block_till_done() + + assert auth.method == "post" + assert auth.url == DEVICE_COMMAND + assert auth.json == { + "command": "sdm.devices.commands.Fan.SetTimer", + "params": { + "duration": "43200s", + "timerMode": "ON", + }, + } + + +async def test_thermostat_set_fan_when_off( + hass: HomeAssistant, + setup_platform: PlatformSetup, + auth: FakeAuth, + create_device: CreateDevice, ) -> None: """Test a thermostat enabling the fan.""" create_device.create( @@ -929,34 +1003,18 @@ async def test_thermostat_set_fan( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_FAN_ONLY + assert thermostat.state == HVAC_MODE_OFF assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.FAN_MODE + ) - # Turn off fan mode - await common.async_set_fan_mode(hass, FAN_OFF) - await hass.async_block_till_done() - - assert auth.method == "post" - assert auth.url == DEVICE_COMMAND - assert auth.json == { - "command": "sdm.devices.commands.Fan.SetTimer", - "params": {"timerMode": "OFF"}, - } - - # Turn on fan mode - await common.async_set_fan_mode(hass, FAN_ON) - await hass.async_block_till_done() - - assert auth.method == "post" - assert auth.url == DEVICE_COMMAND - assert auth.json == { - "command": "sdm.devices.commands.Fan.SetTimer", - "params": { - "duration": "43200s", - "timerMode": "ON", - }, - } + # Fan cannot be turned on when HVAC is off + with pytest.raises(ValueError): + await common.async_set_fan_mode(hass, FAN_ON, entity_id="climate.my_thermostat") async def test_thermostat_fan_empty( @@ -994,6 +1052,10 @@ async def test_thermostat_fan_empty( } assert ATTR_FAN_MODE not in thermostat.attributes assert ATTR_FAN_MODES not in thermostat.attributes + assert thermostat.attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + ) # Ignores set_fan_mode since it is lacking SUPPORT_FAN_MODE await common.async_set_fan_mode(hass, FAN_ON) @@ -1018,7 +1080,7 @@ async def test_thermostat_invalid_fan_mode( "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, "sdm.devices.traits.ThermostatMode": { "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], - "mode": "OFF", + "mode": "COOL", }, "sdm.devices.traits.Temperature": { "ambientTemperatureCelsius": 16.2, @@ -1030,14 +1092,13 @@ async def test_thermostat_invalid_fan_mode( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_FAN_ONLY + assert thermostat.state == HVAC_MODE_COOL assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON @@ -1048,58 +1109,6 @@ async def test_thermostat_invalid_fan_mode( await hass.async_block_till_done() -async def test_thermostat_set_hvac_fan_only( - hass: HomeAssistant, - setup_platform: PlatformSetup, - auth: FakeAuth, - create_device: CreateDevice, -) -> None: - """Test a thermostat enabling the fan via hvac_mode.""" - create_device.create( - { - "sdm.devices.traits.Fan": { - "timerMode": "OFF", - "timerTimeout": "2019-05-10T03:22:54Z", - }, - "sdm.devices.traits.ThermostatHvac": { - "status": "OFF", - }, - "sdm.devices.traits.ThermostatMode": { - "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], - "mode": "OFF", - }, - } - ) - await setup_platform() - - assert len(hass.states.async_all()) == 1 - thermostat = hass.states.get("climate.my_thermostat") - assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF - assert thermostat.attributes[ATTR_FAN_MODE] == FAN_OFF - assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] - - await common.async_set_hvac_mode(hass, HVAC_MODE_FAN_ONLY) - await hass.async_block_till_done() - - assert len(auth.captured_requests) == 2 - - (method, url, json, headers) = auth.captured_requests.pop(0) - assert method == "post" - assert url == DEVICE_COMMAND - assert json == { - "command": "sdm.devices.commands.Fan.SetTimer", - "params": {"duration": "43200s", "timerMode": "ON"}, - } - (method, url, json, headers) = auth.captured_requests.pop(0) - assert method == "post" - assert url == DEVICE_COMMAND - assert json == { - "command": "sdm.devices.commands.ThermostatMode.SetMode", - "params": {"mode": "OFF"}, - } - - async def test_thermostat_target_temp( hass: HomeAssistant, setup_platform: PlatformSetup, @@ -1397,7 +1406,7 @@ async def test_thermostat_hvac_mode_failure( "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, "sdm.devices.traits.ThermostatMode": { "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], - "mode": "OFF", + "mode": "COOL", }, "sdm.devices.traits.Fan": { "timerMode": "OFF", @@ -1416,8 +1425,8 @@ async def test_thermostat_hvac_mode_failure( assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF - assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.state == HVAC_MODE_COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE auth.responses = [aiohttp.web.Response(status=HTTPStatus.BAD_REQUEST)] with pytest.raises(HomeAssistantError): From d8f2afb7727ffa66e3a5c10ceca18201f36620d5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Jun 2022 10:55:58 -0700 Subject: [PATCH 1496/3516] Guard withings accessing hass.data without it being set (#73454) Co-authored-by: Martin Hjelmare --- homeassistant/components/withings/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 47702090cc0..6e8dee9a774 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -72,11 +72,12 @@ 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 := config.get(DOMAIN, {})): + if not (conf := config.get(DOMAIN)): + # Apply the defaults. + conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] + hass.data[DOMAIN] = {const.CONFIG: conf} return True - # Make the config available to the oauth2 config flow. hass.data[DOMAIN] = {const.CONFIG: conf} # Setup the oauth2 config flow. From 0b22e47c53b63bd071deaf3a3d24d97ebc92f1a0 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Tue, 14 Jun 2022 15:17:40 +0200 Subject: [PATCH 1497/3516] =?UTF-8?q?Fix=20max=5Fvalue=20access=20for=20nu?= =?UTF-8?q?mber=20platform=20in=E2=80=AFOverkiz=20(#73479)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix wrong property name --- homeassistant/components/overkiz/number.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/overkiz/number.py b/homeassistant/components/overkiz/number.py index 741c666a42a..167065e9015 100644 --- a/homeassistant/components/overkiz/number.py +++ b/homeassistant/components/overkiz/number.py @@ -134,7 +134,7 @@ class OverkizNumber(OverkizDescriptiveEntity, NumberEntity): """Return the entity value to represent the entity state.""" if state := self.device.states.get(self.entity_description.key): if self.entity_description.inverted: - return self._attr_max_value - cast(float, state.value) + return self.max_value - cast(float, state.value) return cast(float, state.value) @@ -143,7 +143,7 @@ class OverkizNumber(OverkizDescriptiveEntity, NumberEntity): async def async_set_value(self, value: float) -> None: """Set new value.""" if self.entity_description.inverted: - value = self._attr_max_value - value + value = self.max_value - value await self.executor.async_execute_command( self.entity_description.command, value From a4a511b6db6f706ce9d42b951da2fd76975b04ec Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Jun 2022 12:31:51 -0700 Subject: [PATCH 1498/3516] Bumped version to 2022.6.6 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index eeba0f6698c..8f8a8008457 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "5" +PATCH_VERSION: Final = "6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index fd13e6f7d09..17e4901b2ff 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.5 +version = 2022.6.6 url = https://www.home-assistant.io/ [options] From a0ed54465f25b4c6a4b78495174f8b18128a43d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 14 Jun 2022 10:02:45 -1000 Subject: [PATCH 1499/3516] Migrate lutron caseta occupancygroup unique ids so they are actually unique (#73378) --- .../components/lutron_caseta/__init__.py | 35 +++++++++++++++++-- .../components/lutron_caseta/binary_sensor.py | 13 +++---- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index d915c2a45cb..27d8ad87861 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -6,6 +6,7 @@ import contextlib from itertools import chain import logging import ssl +from typing import Any import async_timeout from pylutron_caseta import BUTTON_STATUS_PRESSED @@ -16,7 +17,7 @@ from homeassistant import config_entries from homeassistant.const import ATTR_DEVICE_ID, ATTR_SUGGESTED_AREA, CONF_HOST, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.typing import ConfigType @@ -106,6 +107,33 @@ async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool: return True +async def _async_migrate_unique_ids( + hass: HomeAssistant, entry: config_entries.ConfigEntry +) -> None: + """Migrate entities since the occupancygroup were not actually unique.""" + + dev_reg = dr.async_get(hass) + bridge_unique_id = entry.unique_id + + @callback + def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, Any] | None: + if not (unique_id := entity_entry.unique_id): + return None + if not unique_id.startswith("occupancygroup_") or unique_id.startswith( + f"occupancygroup_{bridge_unique_id}" + ): + return None + sensor_id = unique_id.split("_")[1] + new_unique_id = f"occupancygroup_{bridge_unique_id}_{sensor_id}" + if dev_entry := dev_reg.async_get_device({(DOMAIN, unique_id)}): + dev_reg.async_update_device( + dev_entry.id, new_identifiers={(DOMAIN, new_unique_id)} + ) + return {"new_unique_id": f"occupancygroup_{bridge_unique_id}_{sensor_id}"} + + await er.async_migrate_entries(hass, entry.entry_id, _async_migrator) + + async def async_setup_entry( hass: HomeAssistant, config_entry: config_entries.ConfigEntry ) -> bool: @@ -117,6 +145,8 @@ async def async_setup_entry( ca_certs = hass.config.path(config_entry.data[CONF_CA_CERTS]) bridge = None + await _async_migrate_unique_ids(hass, config_entry) + try: bridge = Smartbridge.create_tls( hostname=host, keyfile=keyfile, certfile=certfile, ca_certs=ca_certs @@ -144,7 +174,7 @@ async def async_setup_entry( bridge_device = devices[BRIDGE_DEVICE_ID] if not config_entry.unique_id: hass.config_entries.async_update_entry( - config_entry, unique_id=hex(bridge_device["serial"])[2:].zfill(8) + config_entry, unique_id=serial_to_unique_id(bridge_device["serial"]) ) buttons = bridge.buttons @@ -312,6 +342,7 @@ class LutronCasetaDevice(Entity): self._device = device self._smartbridge = bridge self._bridge_device = bridge_device + self._bridge_unique_id = serial_to_unique_id(bridge_device["serial"]) if "serial" not in self._device: return area, name = _area_and_name_from_name(device["name"]) diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index 6a6e3853280..56a770c0b2e 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -6,14 +6,13 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_SUGGESTED_AREA from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice, _area_and_name_from_name -from .const import BRIDGE_DEVICE, BRIDGE_LEAP, CONFIG_URL, MANUFACTURER, UNASSIGNED_AREA +from .const import BRIDGE_DEVICE, BRIDGE_LEAP, CONFIG_URL, MANUFACTURER async def async_setup_entry( @@ -44,7 +43,9 @@ class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): def __init__(self, device, bridge, bridge_device): """Init an occupancy sensor.""" super().__init__(device, bridge, bridge_device) - info = DeviceInfo( + _, name = _area_and_name_from_name(device["name"]) + self._attr_name = name + self._attr_device_info = DeviceInfo( identifiers={(CASETA_DOMAIN, self.unique_id)}, manufacturer=MANUFACTURER, model="Lutron Occupancy", @@ -53,10 +54,6 @@ class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): configuration_url=CONFIG_URL, entry_type=DeviceEntryType.SERVICE, ) - area, _ = _area_and_name_from_name(device["name"]) - if area != UNASSIGNED_AREA: - info[ATTR_SUGGESTED_AREA] = area - self._attr_device_info = info @property def is_on(self): @@ -77,7 +74,7 @@ class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): @property def unique_id(self): """Return a unique identifier.""" - return f"occupancygroup_{self.device_id}" + return f"occupancygroup_{self._bridge_unique_id}_{self.device_id}" @property def extra_state_attributes(self): From 063e680589ca956b9534c63841fb7fea943aa28f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 14 Jun 2022 13:23:27 -0700 Subject: [PATCH 1500/3516] Fix unifiprotect import --- homeassistant/components/unifiprotect/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 02cfa6c16ff..b902409595e 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -20,7 +20,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.event import async_track_time_interval -from .const import CONF_DISABLE_RTSP, DEVICES_THAT_ADOPT, DEVICES_WITH_ENTITIES +from .const import CONF_DISABLE_RTSP, DEVICES_THAT_ADOPT, DEVICES_WITH_ENTITIES, DOMAIN _LOGGER = logging.getLogger(__name__) From 3bbb4c052c92ee4608fb8b6881ef0c0bbece6132 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 14 Jun 2022 22:49:06 +0200 Subject: [PATCH 1501/3516] Add camera diagnostics to Synology DSM (#73391) --- .../components/synology_dsm/diagnostics.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/synology_dsm/diagnostics.py b/homeassistant/components/synology_dsm/diagnostics.py index 485a44b290a..30af7f94282 100644 --- a/homeassistant/components/synology_dsm/diagnostics.py +++ b/homeassistant/components/synology_dsm/diagnostics.py @@ -1,8 +1,11 @@ """Diagnostics support for Synology DSM.""" from __future__ import annotations +from typing import Any + from synology_dsm.api.surveillance_station.camera import SynoCamera +from homeassistant.components.camera import diagnostics as camera_diagnostics from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -16,13 +19,13 @@ TO_REDACT = {CONF_USERNAME, CONF_PASSWORD, CONF_DEVICE_TOKEN} async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" data: SynologyDSMData = hass.data[DOMAIN][entry.unique_id] syno_api = data.api dsm_info = syno_api.dsm.information - diag_data = { + diag_data: dict[str, Any] = { "entry": async_redact_data(entry.as_dict(), TO_REDACT), "device_info": { "model": dsm_info.model, @@ -33,7 +36,7 @@ async def async_get_config_entry_diagnostics( }, "network": {"interfaces": {}}, "storage": {"disks": {}, "volumes": {}}, - "surveillance_station": {"cameras": {}}, + "surveillance_station": {"cameras": {}, "camera_diagnostics": {}}, "upgrade": {}, "utilisation": {}, "is_system_loaded": True, @@ -45,7 +48,7 @@ async def async_get_config_entry_diagnostics( if syno_api.network is not None: intf: dict for intf in syno_api.network.interfaces: - diag_data["network"]["interfaces"][intf["id"]] = { # type: ignore[index] + diag_data["network"]["interfaces"][intf["id"]] = { "type": intf["type"], "ip": intf["ip"], } @@ -53,7 +56,7 @@ async def async_get_config_entry_diagnostics( if syno_api.storage is not None: disk: dict for disk in syno_api.storage.disks: - diag_data["storage"]["disks"][disk["id"]] = { # type: ignore[index] + diag_data["storage"]["disks"][disk["id"]] = { "name": disk["name"], "vendor": disk["vendor"], "model": disk["model"], @@ -64,7 +67,7 @@ async def async_get_config_entry_diagnostics( volume: dict for volume in syno_api.storage.volumes: - diag_data["storage"]["volumes"][volume["id"]] = { # type: ignore[index] + diag_data["storage"]["volumes"][volume["id"]] = { "name": volume["fs_type"], "size": volume["size"], } @@ -72,13 +75,17 @@ async def async_get_config_entry_diagnostics( if syno_api.surveillance_station is not None: camera: SynoCamera for camera in syno_api.surveillance_station.get_all_cameras(): - diag_data["surveillance_station"]["cameras"][camera.id] = { # type: ignore[index] + diag_data["surveillance_station"]["cameras"][camera.id] = { "name": camera.name, "is_enabled": camera.is_enabled, "is_motion_detection_enabled": camera.is_motion_detection_enabled, "model": camera.model, "resolution": camera.resolution, } + if camera_data := await camera_diagnostics.async_get_config_entry_diagnostics( + hass, entry + ): + diag_data["surveillance_station"]["camera_diagnostics"] = camera_data if syno_api.upgrade is not None: diag_data["upgrade"] = { From 103a6266a24e4e8ac7a9cacdffa83ea031709715 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 14 Jun 2022 23:18:59 +0200 Subject: [PATCH 1502/3516] Fix fetching upgrade data during setup of Synology DSM (#73507) --- .../components/synology_dsm/common.py | 57 +++++++++++-------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 088686660e4..12bad2954dd 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -16,7 +16,6 @@ from synology_dsm.api.surveillance_station import SynoSurveillanceStation from synology_dsm.exceptions import ( SynologyDSMAPIErrorException, SynologyDSMException, - SynologyDSMLoginFailedException, SynologyDSMRequestException, ) @@ -32,7 +31,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback -from .const import CONF_DEVICE_TOKEN +from .const import CONF_DEVICE_TOKEN, SYNOLOGY_CONNECTION_EXCEPTIONS LOGGER = logging.getLogger(__name__) @@ -72,6 +71,10 @@ class SynoApi: async def async_setup(self) -> None: """Start interacting with the NAS.""" + await self._hass.async_add_executor_job(self._setup) + + def _setup(self) -> None: + """Start interacting with the NAS in the executor.""" self.dsm = SynologyDSM( self._entry.data[CONF_HOST], self._entry.data[CONF_PORT], @@ -82,7 +85,7 @@ class SynoApi: timeout=self._entry.options.get(CONF_TIMEOUT), device_token=self._entry.data.get(CONF_DEVICE_TOKEN), ) - await self._hass.async_add_executor_job(self.dsm.login) + self.dsm.login() # check if surveillance station is used self._with_surveillance_station = bool( @@ -94,10 +97,24 @@ class SynoApi: self._with_surveillance_station, ) - self._async_setup_api_requests() + # check if upgrade is available + try: + self.dsm.upgrade.update() + except SynologyDSMAPIErrorException as ex: + self._with_upgrade = False + LOGGER.debug("Disabled fetching upgrade data during setup: %s", ex) - await self._hass.async_add_executor_job(self._fetch_device_configuration) - await self.async_update(first_setup=True) + self._fetch_device_configuration() + + try: + self._update() + except SYNOLOGY_CONNECTION_EXCEPTIONS as err: + LOGGER.debug( + "Connection error during setup of '%s' with exception: %s", + self._entry.unique_id, + err, + ) + raise err @callback def subscribe(self, api_key: str, unique_id: str) -> Callable[[], None]: @@ -117,8 +134,7 @@ class SynoApi: return unsubscribe - @callback - def _async_setup_api_requests(self) -> None: + def _setup_api_requests(self) -> None: """Determine if we should fetch each API, if one entity needs it.""" # Entities not added yet, fetch all if not self._fetching_entities: @@ -243,30 +259,23 @@ class SynoApi: # ignore API errors during logout pass - async def async_update(self, first_setup: bool = False) -> None: + async def async_update(self) -> None: """Update function for updating API information.""" - LOGGER.debug("Start data update for '%s'", self._entry.unique_id) - self._async_setup_api_requests() try: - await self._hass.async_add_executor_job( - self.dsm.update, self._with_information - ) - except ( - SynologyDSMLoginFailedException, - SynologyDSMRequestException, - SynologyDSMAPIErrorException, - ) as err: + await self._hass.async_add_executor_job(self._update) + except SYNOLOGY_CONNECTION_EXCEPTIONS as err: LOGGER.debug( "Connection error during update of '%s' with exception: %s", self._entry.unique_id, err, ) - - if first_setup: - raise err - LOGGER.warning( "Connection error during update, fallback by reloading the entry" ) await self._hass.config_entries.async_reload(self._entry.entry_id) - return + + def _update(self) -> None: + """Update function for updating API information.""" + LOGGER.debug("Start data update for '%s'", self._entry.unique_id) + self._setup_api_requests() + self.dsm.update(self._with_information) From d25a5f3836f72b10381ca74c8413bc86f18a8c79 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 14 Jun 2022 12:16:30 -1000 Subject: [PATCH 1503/3516] Bump zeroconf to 0.38.7 (#73497) --- 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 8cfc0698dc4..8061be2cf8a 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.38.6"], + "requirements": ["zeroconf==0.38.7"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5939a513d00..f423c96dbac 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 yarl==1.7.2 -zeroconf==0.38.6 +zeroconf==0.38.7 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 0be8541de46..8e8ff73462d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2495,7 +2495,7 @@ youtube_dl==2021.12.17 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.38.6 +zeroconf==0.38.7 # homeassistant.components.zha zha-quirks==0.0.75 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bebbafa0eaa..81d7e1a30c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1650,7 +1650,7 @@ yolink-api==0.0.8 youless-api==0.16 # homeassistant.components.zeroconf -zeroconf==0.38.6 +zeroconf==0.38.7 # homeassistant.components.zha zha-quirks==0.0.75 From 32b61e15a19b02906c4fee8a84321cca7c392e03 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 15 Jun 2022 01:35:29 +0200 Subject: [PATCH 1504/3516] Strict typing Trafikverket Ferry (#72459) --- .strict-typing | 1 + .../components/trafikverket_ferry/sensor.py | 14 +++++++------- mypy.ini | 11 +++++++++++ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.strict-typing b/.strict-typing index f81b6249fed..77f7b6f50c2 100644 --- a/.strict-typing +++ b/.strict-typing @@ -226,6 +226,7 @@ homeassistant.components.tplink.* homeassistant.components.tolo.* homeassistant.components.tractive.* homeassistant.components.tradfri.* +homeassistant.components.trafikverket_ferry.* homeassistant.components.trafikverket_train.* homeassistant.components.trafikverket_weatherstation.* homeassistant.components.tts.* diff --git a/homeassistant/components/trafikverket_ferry/sensor.py b/homeassistant/components/trafikverket_ferry/sensor.py index bab73d72210..256341a7132 100644 --- a/homeassistant/components/trafikverket_ferry/sensor.py +++ b/homeassistant/components/trafikverket_ferry/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta -from typing import Any +from typing import Any, cast from homeassistant.components.sensor import ( SensorDeviceClass, @@ -55,21 +55,21 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_time"]), - info_fn=lambda data: data["departure_information"], + info_fn=lambda data: cast(list[str], data["departure_information"]), ), TrafikverketSensorEntityDescription( key="departure_from", name="Departure From", icon="mdi:ferry", - value_fn=lambda data: data["departure_from"], - info_fn=lambda data: data["departure_information"], + value_fn=lambda data: cast(str, data["departure_from"]), + info_fn=lambda data: cast(list[str], data["departure_information"]), ), TrafikverketSensorEntityDescription( key="departure_to", name="Departure To", icon="mdi:ferry", - value_fn=lambda data: data["departure_to"], - info_fn=lambda data: data["departure_information"], + value_fn=lambda data: cast(str, data["departure_to"]), + info_fn=lambda data: cast(list[str], data["departure_information"]), ), TrafikverketSensorEntityDescription( key="departure_modified", @@ -77,7 +77,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_modified"]), - info_fn=lambda data: data["departure_information"], + info_fn=lambda data: cast(list[str], data["departure_information"]), entity_registry_enabled_default=False, ), TrafikverketSensorEntityDescription( diff --git a/mypy.ini b/mypy.ini index 11a2d83ec40..9e27addae89 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2250,6 +2250,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.trafikverket_ferry.*] +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.trafikverket_train.*] check_untyped_defs = true disallow_incomplete_defs = true From 188b1670a30f0f27386943b90cffd178378ba936 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 15 Jun 2022 00:25:37 +0000 Subject: [PATCH 1505/3516] [ci skip] Translation update --- .../eight_sleep/translations/cs.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 homeassistant/components/eight_sleep/translations/cs.json diff --git a/homeassistant/components/eight_sleep/translations/cs.json b/homeassistant/components/eight_sleep/translations/cs.json new file mode 100644 index 00000000000..86766978310 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nemohu se p\u0159ipojit k Eight Sleep cloudu: {error}" + }, + "error": { + "cannot_connect": "Nemohu se p\u0159ipojit k Eight Sleep cloudu: {error}" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file From c64b10878997d0abda032138241ac3bcfdaf2260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roy?= Date: Tue, 14 Jun 2022 20:11:37 -0700 Subject: [PATCH 1506/3516] Bump aiobafi6 to 0.6.0 to fix logging performance (#73517) --- homeassistant/components/baf/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/baf/manifest.json b/homeassistant/components/baf/manifest.json index 8143c35410e..821ad1a21cb 100644 --- a/homeassistant/components/baf/manifest.json +++ b/homeassistant/components/baf/manifest.json @@ -3,7 +3,7 @@ "name": "Big Ass Fans", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/baf", - "requirements": ["aiobafi6==0.5.0"], + "requirements": ["aiobafi6==0.6.0"], "codeowners": ["@bdraco", "@jfroy"], "iot_class": "local_push", "zeroconf": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8e8ff73462d..03359ed2a0e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -122,7 +122,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.5.0 +aiobafi6==0.6.0 # homeassistant.components.aws aiobotocore==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81d7e1a30c9..2d8f44b9da9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -109,7 +109,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.5.0 +aiobafi6==0.6.0 # homeassistant.components.aws aiobotocore==2.1.0 From 1e956bc52f08f5f203b47e380bad4fbae8e92a06 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 14 Jun 2022 20:30:59 -1000 Subject: [PATCH 1507/3516] Reduce bond startup time (#73506) --- homeassistant/components/bond/button.py | 4 ++-- homeassistant/components/bond/cover.py | 11 ++++------- homeassistant/components/bond/entity.py | 8 +++++--- homeassistant/components/bond/fan.py | 20 ++++++++------------ homeassistant/components/bond/light.py | 13 ++++++++----- homeassistant/components/bond/switch.py | 18 +++++++----------- homeassistant/components/bond/utils.py | 21 +++++++++++++++++---- tests/components/bond/test_config_flow.py | 5 +++-- 8 files changed, 54 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/bond/button.py b/homeassistant/components/bond/button.py index 0465e4c51fe..ffdb01b9d88 100644 --- a/homeassistant/components/bond/button.py +++ b/homeassistant/components/bond/button.py @@ -287,10 +287,10 @@ class BondButtonEntity(BondEntity, ButtonEntity): description: BondButtonEntityDescription, ) -> None: """Init Bond button.""" + self.entity_description = description super().__init__( hub, device, bpup_subs, description.name, description.key.lower() ) - self.entity_description = description async def async_press(self, **kwargs: Any) -> None: """Press the button.""" @@ -302,5 +302,5 @@ class BondButtonEntity(BondEntity, ButtonEntity): action = Action(self.entity_description.key) await self._hub.bond.action(self._device.device_id, action) - def _apply_state(self, state: dict) -> None: + def _apply_state(self) -> None: """Apply the state.""" diff --git a/homeassistant/components/bond/cover.py b/homeassistant/components/bond/cover.py index 3938de0d4bd..efe72f947f4 100644 --- a/homeassistant/components/bond/cover.py +++ b/homeassistant/components/bond/cover.py @@ -13,7 +13,6 @@ from homeassistant.components.cover import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import BPUP_SUBS, DOMAIN, HUB @@ -40,14 +39,11 @@ async def async_setup_entry( data = hass.data[DOMAIN][entry.entry_id] hub: BondHub = data[HUB] bpup_subs: BPUPSubscriptions = data[BPUP_SUBS] - - covers: list[Entity] = [ + async_add_entities( BondCover(hub, device, bpup_subs) for device in hub.devices if device.type == DeviceType.MOTORIZED_SHADES - ] - - async_add_entities(covers, True) + ) class BondCover(BondEntity, CoverEntity): @@ -78,7 +74,8 @@ class BondCover(BondEntity, CoverEntity): supported_features |= CoverEntityFeature.STOP_TILT self._attr_supported_features = supported_features - def _apply_state(self, state: dict) -> None: + def _apply_state(self) -> None: + state = self._device.state cover_open = state.get("open") self._attr_is_closed = None if cover_open is None else cover_open == 0 if (bond_position := state.get("position")) is not None: diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index 832e9b5d464..f9f09cfe3cb 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -64,6 +64,8 @@ class BondEntity(Entity): self._attr_name = f"{device.name} {sub_device_name}" else: self._attr_name = device.name + self._attr_assumed_state = self._hub.is_bridge and not self._device.trust_state + self._apply_state() @property def device_info(self) -> DeviceInfo: @@ -137,10 +139,9 @@ class BondEntity(Entity): self._attr_available = False else: self._async_state_callback(state) - self._attr_assumed_state = self._hub.is_bridge and not self._device.trust_state @abstractmethod - def _apply_state(self, state: dict) -> None: + def _apply_state(self) -> None: raise NotImplementedError @callback @@ -153,7 +154,8 @@ class BondEntity(Entity): _LOGGER.debug( "Device state for %s (%s) is:\n%s", self.name, self.entity_id, state ) - self._apply_state(state) + self._device.state = state + self._apply_state() @callback def _async_bpup_callback(self, json_msg: dict) -> None: diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index f2f6b15f923..12eef9c44b0 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -19,7 +19,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform -from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( int_states_in_range, @@ -46,20 +45,17 @@ async def async_setup_entry( hub: BondHub = data[HUB] bpup_subs: BPUPSubscriptions = data[BPUP_SUBS] platform = entity_platform.async_get_current_platform() - - fans: list[Entity] = [ - BondFan(hub, device, bpup_subs) - for device in hub.devices - if DeviceType.is_fan(device.type) - ] - platform.async_register_entity_service( SERVICE_SET_FAN_SPEED_TRACKED_STATE, {vol.Required("speed"): vol.All(vol.Number(scale=0), vol.Range(0, 100))}, "async_set_speed_belief", ) - async_add_entities(fans, True) + async_add_entities( + BondFan(hub, device, bpup_subs) + for device in hub.devices + if DeviceType.is_fan(device.type) + ) class BondFan(BondEntity, FanEntity): @@ -69,15 +65,15 @@ class BondFan(BondEntity, FanEntity): self, hub: BondHub, device: BondDevice, bpup_subs: BPUPSubscriptions ) -> None: """Create HA entity representing Bond fan.""" - super().__init__(hub, device, bpup_subs) - self._power: bool | None = None self._speed: int | None = None self._direction: int | None = None + super().__init__(hub, device, bpup_subs) if self._device.has_action(Action.BREEZE_ON): self._attr_preset_modes = [PRESET_MODE_BREEZE] - def _apply_state(self, state: dict) -> None: + def _apply_state(self) -> None: + state = self._device.state self._power = state.get("power") self._speed = state.get("speed") self._direction = state.get("direction") diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index 55084f37b03..5a76ea6a13e 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -115,7 +115,6 @@ async def async_setup_entry( async_add_entities( fan_lights + fan_up_lights + fan_down_lights + fireplaces + fp_lights + lights, - True, ) @@ -170,7 +169,8 @@ class BondLight(BondBaseLight, BondEntity, LightEntity): self._attr_color_mode = ColorMode.BRIGHTNESS self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} - def _apply_state(self, state: dict) -> None: + def _apply_state(self) -> None: + state = self._device.state self._attr_is_on = state.get("light") == 1 brightness = state.get("brightness") self._attr_brightness = round(brightness * 255 / 100) if brightness else None @@ -227,7 +227,8 @@ class BondLight(BondBaseLight, BondEntity, LightEntity): class BondDownLight(BondBaseLight, BondEntity, LightEntity): """Representation of a Bond light.""" - def _apply_state(self, state: dict) -> None: + def _apply_state(self) -> None: + state = self._device.state self._attr_is_on = bool(state.get("down_light") and state.get("light")) async def async_turn_on(self, **kwargs: Any) -> None: @@ -246,7 +247,8 @@ class BondDownLight(BondBaseLight, BondEntity, LightEntity): class BondUpLight(BondBaseLight, BondEntity, LightEntity): """Representation of a Bond light.""" - def _apply_state(self, state: dict) -> None: + def _apply_state(self) -> None: + state = self._device.state self._attr_is_on = bool(state.get("up_light") and state.get("light")) async def async_turn_on(self, **kwargs: Any) -> None: @@ -268,7 +270,8 @@ class BondFireplace(BondEntity, LightEntity): _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} - def _apply_state(self, state: dict) -> None: + def _apply_state(self) -> None: + state = self._device.state power = state.get("power") flame = state.get("flame") self._attr_is_on = power == 1 diff --git a/homeassistant/components/bond/switch.py b/homeassistant/components/bond/switch.py index da0b19dd9ff..a88be924610 100644 --- a/homeassistant/components/bond/switch.py +++ b/homeassistant/components/bond/switch.py @@ -12,7 +12,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -36,27 +35,24 @@ async def async_setup_entry( hub: BondHub = data[HUB] bpup_subs: BPUPSubscriptions = data[BPUP_SUBS] platform = entity_platform.async_get_current_platform() - - switches: list[Entity] = [ - BondSwitch(hub, device, bpup_subs) - for device in hub.devices - if DeviceType.is_generic(device.type) - ] - platform.async_register_entity_service( SERVICE_SET_POWER_TRACKED_STATE, {vol.Required(ATTR_POWER_STATE): cv.boolean}, "async_set_power_belief", ) - async_add_entities(switches, True) + async_add_entities( + BondSwitch(hub, device, bpup_subs) + for device in hub.devices + if DeviceType.is_generic(device.type) + ) class BondSwitch(BondEntity, SwitchEntity): """Representation of a Bond generic device.""" - def _apply_state(self, state: dict) -> None: - self._attr_is_on = state.get("power") == 1 + def _apply_state(self) -> None: + self._attr_is_on = self._device.state.get("power") == 1 async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py index c426bf64577..3a161a74bc5 100644 --- a/homeassistant/components/bond/utils.py +++ b/homeassistant/components/bond/utils.py @@ -20,11 +20,16 @@ class BondDevice: """Helper device class to hold ID and attributes together.""" def __init__( - self, device_id: str, attrs: dict[str, Any], props: dict[str, Any] + self, + device_id: str, + attrs: dict[str, Any], + props: dict[str, Any], + state: dict[str, Any], ) -> None: """Create a helper device from ID and attributes returned by API.""" self.device_id = device_id self.props = props + self.state = state self._attrs = attrs or {} self._supported_actions: set[str] = set(self._attrs.get("actions", [])) @@ -34,6 +39,7 @@ class BondDevice: "device_id": self.device_id, "props": self.props, "attrs": self._attrs, + "state": self.state, }.__repr__() @property @@ -150,7 +156,11 @@ class BondHub: break setup_device_ids.append(device_id) tasks.extend( - [self.bond.device(device_id), self.bond.device_properties(device_id)] + [ + self.bond.device(device_id), + self.bond.device_properties(device_id), + self.bond.device_state(device_id), + ] ) responses = await gather_with_concurrency(MAX_REQUESTS, *tasks) @@ -158,10 +168,13 @@ class BondHub: for device_id in setup_device_ids: self._devices.append( BondDevice( - device_id, responses[response_idx], responses[response_idx + 1] + device_id, + responses[response_idx], + responses[response_idx + 1], + responses[response_idx + 2], ) ) - response_idx += 2 + response_idx += 3 _LOGGER.debug("Discovered Bond devices: %s", self._devices) try: diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index 4f1e313a34a..15aa643abaf 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -18,6 +18,7 @@ from .common import ( patch_bond_device, patch_bond_device_ids, patch_bond_device_properties, + patch_bond_device_state, patch_bond_token, patch_bond_version, ) @@ -38,7 +39,7 @@ async def test_user_form(hass: core.HomeAssistant): return_value={"bondid": "ZXXX12345"} ), patch_bond_device_ids( return_value=["f6776c11", "f6776c12"] - ), patch_bond_bridge(), patch_bond_device_properties(), patch_bond_device(), _patch_async_setup_entry() as mock_setup_entry: + ), patch_bond_bridge(), patch_bond_device_properties(), patch_bond_device(), patch_bond_device_state(), _patch_async_setup_entry() as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, @@ -73,7 +74,7 @@ async def test_user_form_with_non_bridge(hass: core.HomeAssistant): } ), patch_bond_bridge( return_value={} - ), _patch_async_setup_entry() as mock_setup_entry: + ), patch_bond_device_state(), _patch_async_setup_entry() as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"}, From 77c92b0b77cb246a3c67fabb3c1ff816102a7820 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 15 Jun 2022 01:32:13 -0500 Subject: [PATCH 1508/3516] Mark Sonos speaker as offline when switching to bluetooth (#73519) --- 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 bd217cc9029..f9decf1c27e 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -77,7 +77,7 @@ SUBSCRIPTION_SERVICES = [ "renderingControl", "zoneGroupTopology", ] -SUPPORTED_VANISH_REASONS = ("sleeping", "upgrade") +SUPPORTED_VANISH_REASONS = ("sleeping", "switch to bluetooth", "upgrade") UNUSED_DEVICE_KEYS = ["SPID", "TargetRoomName"] From 16dd70ba994b5f9cb50a7d229e97ea8629740ef9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 14 Jun 2022 20:32:38 -1000 Subject: [PATCH 1509/3516] Switch to a dataclass for lutron_caseta entry data (#73500) --- .../components/lutron_caseta/__init__.py | 20 +++++++----------- .../components/lutron_caseta/binary_sensor.py | 9 ++++---- .../components/lutron_caseta/const.py | 3 --- .../components/lutron_caseta/cover.py | 9 ++++---- .../lutron_caseta/device_trigger.py | 8 +++---- .../components/lutron_caseta/diagnostics.py | 8 +++---- homeassistant/components/lutron_caseta/fan.py | 9 ++++---- .../components/lutron_caseta/light.py | 9 ++++---- .../components/lutron_caseta/models.py | 18 ++++++++++++++++ .../components/lutron_caseta/scene.py | 9 ++++---- .../components/lutron_caseta/switch.py | 9 ++++---- .../lutron_caseta/test_device_trigger.py | 21 ++++++++++++------- 12 files changed, 78 insertions(+), 54 deletions(-) create mode 100644 homeassistant/components/lutron_caseta/models.py diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 27d8ad87861..b4ce82a36c6 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -32,11 +32,8 @@ from .const import ( ATTR_LEAP_BUTTON_NUMBER, ATTR_SERIAL, ATTR_TYPE, - BRIDGE_DEVICE, BRIDGE_DEVICE_ID, - BRIDGE_LEAP, BRIDGE_TIMEOUT, - BUTTON_DEVICES, CONF_CA_CERTS, CONF_CERTFILE, CONF_KEYFILE, @@ -50,6 +47,7 @@ from .device_trigger import ( DEVICE_TYPE_SUBTYPE_MAP_TO_LIP, LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP, ) +from .models import LutronCasetaData from .util import serial_to_unique_id _LOGGER = logging.getLogger(__name__) @@ -186,11 +184,9 @@ async def async_setup_entry( # Store this bridge (keyed by entry_id) so it can be retrieved by the # platforms we're setting up. - hass.data[DOMAIN][entry_id] = { - BRIDGE_LEAP: bridge, - BRIDGE_DEVICE: bridge_device, - BUTTON_DEVICES: button_devices, - } + hass.data[DOMAIN][entry_id] = LutronCasetaData( + bridge, bridge_device, button_devices + ) hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) @@ -319,9 +315,8 @@ async def async_unload_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: """Unload the bridge bridge from a config entry.""" - data = hass.data[DOMAIN][entry.entry_id] - smartbridge: Smartbridge = data[BRIDGE_LEAP] - await smartbridge.close() + data: LutronCasetaData = hass.data[DOMAIN][entry.entry_id] + await data.bridge.close() if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) return unload_ok @@ -402,7 +397,8 @@ async def async_remove_config_entry_device( hass: HomeAssistant, entry: config_entries.ConfigEntry, device_entry: dr.DeviceEntry ) -> bool: """Remove lutron_caseta config entry from a device.""" - bridge: Smartbridge = hass.data[DOMAIN][entry.entry_id][BRIDGE_LEAP] + data: LutronCasetaData = hass.data[DOMAIN][entry.entry_id] + bridge = data.bridge devices = bridge.get_devices() buttons = bridge.buttons occupancy_groups = bridge.occupancy_groups diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index 56a770c0b2e..4b1c53d194b 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -12,7 +12,8 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice, _area_and_name_from_name -from .const import BRIDGE_DEVICE, BRIDGE_LEAP, CONFIG_URL, MANUFACTURER +from .const import CONFIG_URL, MANUFACTURER +from .models import LutronCasetaData async def async_setup_entry( @@ -25,9 +26,9 @@ async def async_setup_entry( Adds occupancy groups from the Caseta bridge associated with the config_entry as binary_sensor entities. """ - data = hass.data[CASETA_DOMAIN][config_entry.entry_id] - bridge = data[BRIDGE_LEAP] - bridge_device = data[BRIDGE_DEVICE] + data: LutronCasetaData = hass.data[CASETA_DOMAIN][config_entry.entry_id] + bridge = data.bridge + bridge_device = data.bridge_device occupancy_groups = bridge.occupancy_groups async_add_entities( LutronOccupancySensor(occupancy_group, bridge, bridge_device) diff --git a/homeassistant/components/lutron_caseta/const.py b/homeassistant/components/lutron_caseta/const.py index 71d686ba2c8..ae8dc0a505a 100644 --- a/homeassistant/components/lutron_caseta/const.py +++ b/homeassistant/components/lutron_caseta/const.py @@ -10,9 +10,6 @@ STEP_IMPORT_FAILED = "import_failed" ERROR_CANNOT_CONNECT = "cannot_connect" ABORT_REASON_CANNOT_CONNECT = "cannot_connect" -BRIDGE_LEAP = "leap" -BRIDGE_DEVICE = "bridge_device" -BUTTON_DEVICES = "button_devices" LUTRON_CASETA_BUTTON_EVENT = "lutron_caseta_button_event" BRIDGE_DEVICE_ID = "1" diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py index afddb2677a7..724dc4258da 100644 --- a/homeassistant/components/lutron_caseta/cover.py +++ b/homeassistant/components/lutron_caseta/cover.py @@ -12,7 +12,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import LutronCasetaDeviceUpdatableEntity -from .const import BRIDGE_DEVICE, BRIDGE_LEAP, DOMAIN as CASETA_DOMAIN +from .const import DOMAIN as CASETA_DOMAIN +from .models import LutronCasetaData async def async_setup_entry( @@ -25,9 +26,9 @@ async def async_setup_entry( Adds shades from the Caseta bridge associated with the config_entry as cover entities. """ - data = hass.data[CASETA_DOMAIN][config_entry.entry_id] - bridge = data[BRIDGE_LEAP] - bridge_device = data[BRIDGE_DEVICE] + data: LutronCasetaData = hass.data[CASETA_DOMAIN][config_entry.entry_id] + bridge = data.bridge + bridge_device = data.bridge_device cover_devices = bridge.get_devices_by_domain(DOMAIN) async_add_entities( LutronCasetaCover(cover_device, bridge, bridge_device) diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index 68394667764..e762e79a8d7 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -29,11 +29,11 @@ from .const import ( ATTR_ACTION, ATTR_BUTTON_NUMBER, ATTR_SERIAL, - BUTTON_DEVICES, CONF_SUBTYPE, DOMAIN, LUTRON_CASETA_BUTTON_EVENT, ) +from .models import LutronCasetaData SUPPORTED_INPUTS_EVENTS_TYPES = [ACTION_PRESS, ACTION_RELEASE] @@ -411,9 +411,9 @@ def get_button_device_by_dr_id(hass: HomeAssistant, device_id: str): if DOMAIN not in hass.data: return None - for config_entry in hass.data[DOMAIN]: - button_devices = hass.data[DOMAIN][config_entry][BUTTON_DEVICES] - if device := button_devices.get(device_id): + for entry_id in hass.data[DOMAIN]: + data: LutronCasetaData = hass.data[DOMAIN][entry_id] + if device := data.button_devices.get(device_id): return device return None diff --git a/homeassistant/components/lutron_caseta/diagnostics.py b/homeassistant/components/lutron_caseta/diagnostics.py index 7ae0b5c40a9..afe69b813f9 100644 --- a/homeassistant/components/lutron_caseta/diagnostics.py +++ b/homeassistant/components/lutron_caseta/diagnostics.py @@ -3,19 +3,19 @@ from __future__ import annotations from typing import Any -from pylutron_caseta.smartbridge import Smartbridge - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import BRIDGE_LEAP, DOMAIN +from .const import DOMAIN +from .models import LutronCasetaData async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - bridge: Smartbridge = hass.data[DOMAIN][entry.entry_id][BRIDGE_LEAP] + data: LutronCasetaData = hass.data[DOMAIN][entry.entry_id] + bridge = data.bridge return { "entry": { "title": entry.title, diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py index cdf00959ed1..e08a5278572 100644 --- a/homeassistant/components/lutron_caseta/fan.py +++ b/homeassistant/components/lutron_caseta/fan.py @@ -13,7 +13,8 @@ from homeassistant.util.percentage import ( ) from . import LutronCasetaDeviceUpdatableEntity -from .const import BRIDGE_DEVICE, BRIDGE_LEAP, DOMAIN as CASETA_DOMAIN +from .const import DOMAIN as CASETA_DOMAIN +from .models import LutronCasetaData DEFAULT_ON_PERCENTAGE = 50 ORDERED_NAMED_FAN_SPEEDS = [FAN_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH] @@ -29,9 +30,9 @@ async def async_setup_entry( Adds fan controllers from the Caseta bridge associated with the config_entry as fan entities. """ - data = hass.data[CASETA_DOMAIN][config_entry.entry_id] - bridge = data[BRIDGE_LEAP] - bridge_device = data[BRIDGE_DEVICE] + data: LutronCasetaData = hass.data[CASETA_DOMAIN][config_entry.entry_id] + bridge = data.bridge + bridge_device = data.bridge_device fan_devices = bridge.get_devices_by_domain(DOMAIN) async_add_entities( LutronCasetaFan(fan_device, bridge, bridge_device) for fan_device in fan_devices diff --git a/homeassistant/components/lutron_caseta/light.py b/homeassistant/components/lutron_caseta/light.py index a58fc21aadf..9fbb80284f5 100644 --- a/homeassistant/components/lutron_caseta/light.py +++ b/homeassistant/components/lutron_caseta/light.py @@ -14,7 +14,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import LutronCasetaDeviceUpdatableEntity -from .const import BRIDGE_DEVICE, BRIDGE_LEAP, DOMAIN as CASETA_DOMAIN +from .const import DOMAIN as CASETA_DOMAIN +from .models import LutronCasetaData def to_lutron_level(level): @@ -37,9 +38,9 @@ async def async_setup_entry( Adds dimmers from the Caseta bridge associated with the config_entry as light entities. """ - data = hass.data[CASETA_DOMAIN][config_entry.entry_id] - bridge = data[BRIDGE_LEAP] - bridge_device = data[BRIDGE_DEVICE] + data: LutronCasetaData = hass.data[CASETA_DOMAIN][config_entry.entry_id] + bridge = data.bridge + bridge_device = data.bridge_device light_devices = bridge.get_devices_by_domain(DOMAIN) async_add_entities( LutronCasetaLight(light_device, bridge, bridge_device) diff --git a/homeassistant/components/lutron_caseta/models.py b/homeassistant/components/lutron_caseta/models.py new file mode 100644 index 00000000000..5845c888a2e --- /dev/null +++ b/homeassistant/components/lutron_caseta/models.py @@ -0,0 +1,18 @@ +"""The lutron_caseta integration models.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from pylutron_caseta.smartbridge import Smartbridge + +from homeassistant.helpers.device_registry import DeviceEntry + + +@dataclass +class LutronCasetaData: + """Data for the lutron_caseta integration.""" + + bridge: Smartbridge + bridge_device: dict[str, Any] + button_devices: dict[str, DeviceEntry] diff --git a/homeassistant/components/lutron_caseta/scene.py b/homeassistant/components/lutron_caseta/scene.py index 1bbd69615e1..2870d6ee96a 100644 --- a/homeassistant/components/lutron_caseta/scene.py +++ b/homeassistant/components/lutron_caseta/scene.py @@ -10,7 +10,8 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import _area_and_name_from_name -from .const import BRIDGE_DEVICE, BRIDGE_LEAP, DOMAIN as CASETA_DOMAIN +from .const import DOMAIN as CASETA_DOMAIN +from .models import LutronCasetaData from .util import serial_to_unique_id @@ -24,9 +25,9 @@ async def async_setup_entry( Adds scenes from the Caseta bridge associated with the config_entry as scene entities. """ - data = hass.data[CASETA_DOMAIN][config_entry.entry_id] - bridge: Smartbridge = data[BRIDGE_LEAP] - bridge_device = data[BRIDGE_DEVICE] + data: LutronCasetaData = hass.data[CASETA_DOMAIN][config_entry.entry_id] + bridge = data.bridge + bridge_device = data.bridge_device scenes = bridge.get_scenes() async_add_entities( LutronCasetaScene(scenes[scene], bridge, bridge_device) for scene in scenes diff --git a/homeassistant/components/lutron_caseta/switch.py b/homeassistant/components/lutron_caseta/switch.py index 7e963352264..062c8891672 100644 --- a/homeassistant/components/lutron_caseta/switch.py +++ b/homeassistant/components/lutron_caseta/switch.py @@ -6,7 +6,8 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import LutronCasetaDeviceUpdatableEntity -from .const import BRIDGE_DEVICE, BRIDGE_LEAP, DOMAIN as CASETA_DOMAIN +from .const import DOMAIN as CASETA_DOMAIN +from .models import LutronCasetaData async def async_setup_entry( @@ -19,9 +20,9 @@ async def async_setup_entry( Adds switches from the Caseta bridge associated with the config_entry as switch entities. """ - data = hass.data[CASETA_DOMAIN][config_entry.entry_id] - bridge = data[BRIDGE_LEAP] - bridge_device = data[BRIDGE_DEVICE] + data: LutronCasetaData = hass.data[CASETA_DOMAIN][config_entry.entry_id] + bridge = data.bridge + bridge_device = data.bridge_device switch_devices = bridge.get_devices_by_domain(DOMAIN) async_add_entities( LutronCasetaLight(switch_device, bridge, bridge_device) diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index cb38df6a381..bdf1e359673 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -1,4 +1,6 @@ """The tests for Lutron Caséta device triggers.""" +from unittest.mock import MagicMock + import pytest from homeassistant.components import automation @@ -15,12 +17,12 @@ from homeassistant.components.lutron_caseta import ( ATTR_TYPE, ) from homeassistant.components.lutron_caseta.const import ( - BUTTON_DEVICES, DOMAIN, LUTRON_CASETA_BUTTON_EVENT, MANUFACTURER, ) from homeassistant.components.lutron_caseta.device_trigger import CONF_SUBTYPE +from homeassistant.components.lutron_caseta.models import LutronCasetaData from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component @@ -83,15 +85,17 @@ async def _async_setup_lutron_with_picos(hass, device_reg): ) dr_button_devices[dr_device.id] = device - hass.data[DOMAIN][config_entry.entry_id] = {BUTTON_DEVICES: dr_button_devices} - + hass.data[DOMAIN][config_entry.entry_id] = LutronCasetaData( + MagicMock(), MagicMock(), dr_button_devices + ) return config_entry.entry_id async def test_get_triggers(hass, device_reg): """Test we get the expected triggers from a lutron pico.""" config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg) - dr_button_devices = hass.data[DOMAIN][config_entry_id][BUTTON_DEVICES] + data: LutronCasetaData = hass.data[DOMAIN][config_entry_id] + dr_button_devices = data.button_devices device_id = list(dr_button_devices)[0] expected_triggers = [ @@ -142,7 +146,8 @@ async def test_if_fires_on_button_event(hass, calls, device_reg): """Test for press trigger firing.""" config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg) - dr_button_devices = hass.data[DOMAIN][config_entry_id][BUTTON_DEVICES] + data: LutronCasetaData = hass.data[DOMAIN][config_entry_id] + dr_button_devices = data.button_devices device_id = list(dr_button_devices)[0] device = dr_button_devices[device_id] assert await async_setup_component( @@ -224,7 +229,8 @@ async def test_validate_trigger_config_unknown_device(hass, calls, device_reg): """Test for no press with an unknown device.""" config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg) - dr_button_devices = hass.data[DOMAIN][config_entry_id][BUTTON_DEVICES] + data: LutronCasetaData = hass.data[DOMAIN][config_entry_id] + dr_button_devices = data.button_devices device_id = list(dr_button_devices)[0] device = dr_button_devices[device_id] device["type"] = "unknown" @@ -267,7 +273,8 @@ 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.""" config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg) - dr_button_devices = hass.data[DOMAIN][config_entry_id][BUTTON_DEVICES] + data: LutronCasetaData = hass.data[DOMAIN][config_entry_id] + dr_button_devices = data.button_devices device_id = list(dr_button_devices)[0] assert await async_setup_component( hass, From a77ea1c39003d2be959c8e73fc8f4e7521a09e84 Mon Sep 17 00:00:00 2001 From: Corbeno Date: Wed, 15 Jun 2022 01:49:55 -0500 Subject: [PATCH 1510/3516] Add device class to proxmoxve binary sensor (#73465) * add device class property to binary sensor * add newline --- homeassistant/components/proxmoxve/binary_sensor.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py index e52b91d4cba..780e7240267 100644 --- a/homeassistant/components/proxmoxve/binary_sensor.py +++ b/homeassistant/components/proxmoxve/binary_sensor.py @@ -1,7 +1,10 @@ """Binary sensor to read Proxmox VE data.""" from __future__ import annotations -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -66,7 +69,7 @@ def create_binary_sensor(coordinator, host_name, node_name, vm_id, name): return ProxmoxBinarySensor( coordinator=coordinator, unique_id=f"proxmox_{node_name}_{vm_id}_running", - name=f"{node_name}_{name}_running", + name=f"{node_name}_{name}", icon="", host_name=host_name, node_name=node_name, @@ -77,6 +80,8 @@ def create_binary_sensor(coordinator, host_name, node_name, vm_id, name): class ProxmoxBinarySensor(ProxmoxEntity, BinarySensorEntity): """A binary sensor for reading Proxmox VE data.""" + _attr_device_class = BinarySensorDeviceClass.RUNNING + def __init__( self, coordinator: DataUpdateCoordinator, From 17eb8c95ddeb40b09bc4082ed1492e72040d6f27 Mon Sep 17 00:00:00 2001 From: Bram Goolaerts Date: Wed, 15 Jun 2022 10:33:53 +0200 Subject: [PATCH 1511/3516] Fix De Lijn 'tzinfo' error (#73502) * Fix De Lijn component tzinfo error This fix should update the issue "Error:'str' object has no attribute 'tzinfo'" (issue #67455) * fix Black and isort errors fixing errors from Black and isort CI validation * Fix black and flake8 issues Fixing black and flake8 issues to pass CI --- homeassistant/components/delijn/sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index e04385dcf3d..ee58a4f21c7 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -1,6 +1,7 @@ """Support for De Lijn (Flemish public transport) information.""" from __future__ import annotations +from datetime import datetime import logging from pydelijn.api import Passages @@ -111,7 +112,9 @@ class DeLijnPublicTransportSensor(SensorEntity): first = self.line.passages[0] if (first_passage := first["due_at_realtime"]) is None: first_passage = first["due_at_schedule"] - self._attr_native_value = first_passage + self._attr_native_value = datetime.strptime( + first_passage, "%Y-%m-%dT%H:%M:%S%z" + ) for key in AUTO_ATTRIBUTES: self._attr_extra_state_attributes[key] = first[key] From 94a8fe0052635299834b8006904609b2e983490e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Jun 2022 10:45:47 +0200 Subject: [PATCH 1512/3516] Remove xiaomi_aqara from mypy ignore list (#73526) --- homeassistant/components/xiaomi_aqara/__init__.py | 9 +++++---- .../components/xiaomi_aqara/binary_sensor.py | 2 +- homeassistant/components/xiaomi_aqara/lock.py | 7 +++++-- homeassistant/components/xiaomi_aqara/sensor.py | 2 +- mypy.ini | 12 ------------ script/hassfest/mypy_config.py | 4 ---- 6 files changed, 12 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 1857b6e83b2..0c9696a42ef 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -82,7 +82,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: def play_ringtone_service(call: ServiceCall) -> None: """Service to play ringtone through Gateway.""" ring_id = call.data.get(ATTR_RINGTONE_ID) - gateway = call.data.get(ATTR_GW_MAC) + gateway: XiaomiGateway = call.data[ATTR_GW_MAC] kwargs = {"mid": ring_id} @@ -93,12 +93,12 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: def stop_ringtone_service(call: ServiceCall) -> None: """Service to stop playing ringtone on Gateway.""" - gateway = call.data.get(ATTR_GW_MAC) + gateway: XiaomiGateway = call.data[ATTR_GW_MAC] gateway.write_to_hub(gateway.sid, mid=10000) def add_device_service(call: ServiceCall) -> None: """Service to add a new sub-device within the next 30 seconds.""" - gateway = call.data.get(ATTR_GW_MAC) + gateway: XiaomiGateway = call.data[ATTR_GW_MAC] gateway.write_to_hub(gateway.sid, join_permission="yes") persistent_notification.async_create( hass, @@ -110,7 +110,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: def remove_device_service(call: ServiceCall) -> None: """Service to remove a sub-device from the gateway.""" device_id = call.data.get(ATTR_DEVICE_ID) - gateway = call.data.get(ATTR_GW_MAC) + gateway: XiaomiGateway = call.data[ATTR_GW_MAC] gateway.write_to_hub(gateway.sid, remove_device=device_id) gateway_only_schema = _add_gateway_to_schema(hass, vol.Schema({})) @@ -181,6 +181,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_HOST], ) + assert entry.unique_id device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, diff --git a/homeassistant/components/xiaomi_aqara/binary_sensor.py b/homeassistant/components/xiaomi_aqara/binary_sensor.py index ef773805849..04e3945e7a2 100644 --- a/homeassistant/components/xiaomi_aqara/binary_sensor.py +++ b/homeassistant/components/xiaomi_aqara/binary_sensor.py @@ -34,7 +34,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Perform the setup for Xiaomi devices.""" - entities = [] + entities: list[XiaomiBinarySensor] = [] gateway = hass.data[DOMAIN][GATEWAYS_KEY][config_entry.entry_id] for entity in gateway.devices["binary_sensor"]: model = entity["model"] diff --git a/homeassistant/components/xiaomi_aqara/lock.py b/homeassistant/components/xiaomi_aqara/lock.py index e21967a9f06..1885c1aef85 100644 --- a/homeassistant/components/xiaomi_aqara/lock.py +++ b/homeassistant/components/xiaomi_aqara/lock.py @@ -1,4 +1,6 @@ """Support for Xiaomi Aqara locks.""" +from __future__ import annotations + from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED @@ -44,13 +46,14 @@ class XiaomiAqaraLock(LockEntity, XiaomiDevice): super().__init__(device, name, xiaomi_hub, config_entry) @property - def is_locked(self) -> bool: + def is_locked(self) -> bool | None: """Return true if lock is locked.""" if self._state is not None: return self._state == STATE_LOCKED + return None @property - def changed_by(self) -> int: + def changed_by(self) -> str: """Last change triggered by.""" return self._changed_by diff --git a/homeassistant/components/xiaomi_aqara/sensor.py b/homeassistant/components/xiaomi_aqara/sensor.py index 3e0d49d628f..5deed77d775 100644 --- a/homeassistant/components/xiaomi_aqara/sensor.py +++ b/homeassistant/components/xiaomi_aqara/sensor.py @@ -88,7 +88,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Perform the setup for Xiaomi devices.""" - entities = [] + entities: list[XiaomiSensor | XiaomiBatterySensor] = [] gateway = hass.data[DOMAIN][GATEWAYS_KEY][config_entry.entry_id] for device in gateway.devices["sensor"]: if device["model"] == "sensor_ht": diff --git a/mypy.ini b/mypy.ini index 9e27addae89..8a9c5b0478a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2976,18 +2976,6 @@ ignore_errors = true [mypy-homeassistant.components.xbox.sensor] ignore_errors = true -[mypy-homeassistant.components.xiaomi_aqara] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_aqara.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_aqara.lock] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_aqara.sensor] -ignore_errors = true - [mypy-homeassistant.components.xiaomi_miio] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 0b705fab983..b7fb778cfee 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -141,10 +141,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.xbox.browse_media", "homeassistant.components.xbox.media_source", "homeassistant.components.xbox.sensor", - "homeassistant.components.xiaomi_aqara", - "homeassistant.components.xiaomi_aqara.binary_sensor", - "homeassistant.components.xiaomi_aqara.lock", - "homeassistant.components.xiaomi_aqara.sensor", "homeassistant.components.xiaomi_miio", "homeassistant.components.xiaomi_miio.air_quality", "homeassistant.components.xiaomi_miio.binary_sensor", From 4ace2c4d3ad92860dab5036f15197d5578d0352e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 15 Jun 2022 10:49:40 +0200 Subject: [PATCH 1513/3516] Migrate overkiz NumberEntity to native_value (#73493) --- homeassistant/components/overkiz/number.py | 45 ++++++++++++++-------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/overkiz/number.py b/homeassistant/components/overkiz/number.py index 167065e9015..8e7d2a93ee9 100644 --- a/homeassistant/components/overkiz/number.py +++ b/homeassistant/components/overkiz/number.py @@ -6,8 +6,13 @@ from typing import cast from pyoverkiz.enums import OverkizCommand, OverkizState -from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntity, + NumberEntityDescription, +) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -38,8 +43,8 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ name="My Position", icon="mdi:content-save-cog", command=OverkizCommand.SET_MEMORIZED_1_POSITION, - min_value=0, - max_value=100, + native_min_value=0, + native_max_value=100, entity_category=EntityCategory.CONFIG, ), # WaterHeater: Expected Number Of Shower (2 - 4) @@ -48,8 +53,8 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ name="Expected Number Of Shower", icon="mdi:shower-head", command=OverkizCommand.SET_EXPECTED_NUMBER_OF_SHOWER, - min_value=2, - max_value=4, + native_min_value=2, + native_max_value=4, entity_category=EntityCategory.CONFIG, ), # SomfyHeatingTemperatureInterface @@ -58,8 +63,10 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ name="Eco Room Temperature", icon="mdi:thermometer", command=OverkizCommand.SET_ECO_TEMPERATURE, - min_value=6, - max_value=29, + device_class=NumberDeviceClass.TEMPERATURE, + native_min_value=6, + native_max_value=29, + native_unit_of_measurement=TEMP_CELSIUS, entity_category=EntityCategory.CONFIG, ), OverkizNumberDescription( @@ -67,8 +74,10 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ name="Comfort Room Temperature", icon="mdi:home-thermometer-outline", command=OverkizCommand.SET_COMFORT_TEMPERATURE, - min_value=7, - max_value=30, + device_class=NumberDeviceClass.TEMPERATURE, + native_min_value=7, + native_max_value=30, + native_unit_of_measurement=TEMP_CELSIUS, entity_category=EntityCategory.CONFIG, ), OverkizNumberDescription( @@ -76,8 +85,10 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ name="Freeze Protection Temperature", icon="mdi:sun-thermometer-outline", command=OverkizCommand.SET_SECURED_POSITION_TEMPERATURE, - min_value=5, - max_value=15, + device_class=NumberDeviceClass.TEMPERATURE, + native_min_value=5, + native_max_value=15, + native_unit_of_measurement=TEMP_CELSIUS, entity_category=EntityCategory.CONFIG, ), # DimmerExteriorHeating (Somfy Terrace Heater) (0 - 100) @@ -86,8 +97,8 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ key=OverkizState.CORE_LEVEL, icon="mdi:patio-heater", command=OverkizCommand.SET_LEVEL, - min_value=0, - max_value=100, + native_min_value=0, + native_max_value=100, inverted=True, ), ] @@ -130,20 +141,20 @@ class OverkizNumber(OverkizDescriptiveEntity, NumberEntity): entity_description: OverkizNumberDescription @property - def value(self) -> float | None: + def native_value(self) -> float | None: """Return the entity value to represent the entity state.""" if state := self.device.states.get(self.entity_description.key): if self.entity_description.inverted: - return self.max_value - cast(float, state.value) + return self.native_max_value - cast(float, state.value) return cast(float, state.value) return None - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set new value.""" if self.entity_description.inverted: - value = self.max_value - value + value = self.native_max_value - value await self.executor.async_execute_command( self.entity_description.command, value From e05e79e53d7ca9aec842d1e6ac85efa773553ea9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 15 Jun 2022 10:56:41 +0200 Subject: [PATCH 1514/3516] Migrate NumberEntity r-t to native_value (#73485) --- .../rituals_perfume_genie/number.py | 8 ++++---- .../components/screenlogic/number.py | 8 ++++---- homeassistant/components/sensibo/number.py | 16 +++++++-------- homeassistant/components/shelly/number.py | 20 +++++++++---------- homeassistant/components/sleepiq/number.py | 16 +++++++-------- homeassistant/components/sonos/number.py | 6 +++--- homeassistant/components/tolo/number.py | 20 +++++++++---------- 7 files changed, 47 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/rituals_perfume_genie/number.py b/homeassistant/components/rituals_perfume_genie/number.py index 1bcadf9aa88..778b70753cb 100644 --- a/homeassistant/components/rituals_perfume_genie/number.py +++ b/homeassistant/components/rituals_perfume_genie/number.py @@ -38,8 +38,8 @@ class DiffuserPerfumeAmount(DiffuserEntity, NumberEntity): """Representation of a diffuser perfume amount number.""" _attr_icon = "mdi:gauge" - _attr_max_value = MAX_PERFUME_AMOUNT - _attr_min_value = MIN_PERFUME_AMOUNT + _attr_native_max_value = MAX_PERFUME_AMOUNT + _attr_native_min_value = MIN_PERFUME_AMOUNT def __init__( self, diffuser: Diffuser, coordinator: RitualsDataUpdateCoordinator @@ -48,11 +48,11 @@ class DiffuserPerfumeAmount(DiffuserEntity, NumberEntity): super().__init__(diffuser, coordinator, PERFUME_AMOUNT_SUFFIX) @property - def value(self) -> int: + def native_value(self) -> int: """Return the current perfume amount.""" return self._diffuser.perfume_amount - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set the perfume amount.""" if not value.is_integer(): raise ValueError( diff --git a/homeassistant/components/screenlogic/number.py b/homeassistant/components/screenlogic/number.py index 253b5c9641e..75dee907cc1 100644 --- a/homeassistant/components/screenlogic/number.py +++ b/homeassistant/components/screenlogic/number.py @@ -46,16 +46,16 @@ class ScreenLogicNumber(ScreenlogicEntity, NumberEntity): """Initialize of the entity.""" super().__init__(coordinator, data_key, enabled) self._body_type = SUPPORTED_SCG_NUMBERS.index(self._data_key) - self._attr_max_value = SCG.LIMIT_FOR_BODY[self._body_type] + self._attr_native_max_value = SCG.LIMIT_FOR_BODY[self._body_type] self._attr_name = f"{self.gateway_name} {self.sensor['name']}" - self._attr_unit_of_measurement = self.sensor["unit"] + self._attr_native_unit_of_measurement = self.sensor["unit"] @property - def value(self) -> float: + def native_value(self) -> float: """Return the current value.""" return self.sensor["value"] - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Update the current value.""" # Need to set both levels at the same time, so we gather # both existing level values and override the one that changed. diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py index 89bd9b270a9..183c4db4b87 100644 --- a/homeassistant/components/sensibo/number.py +++ b/homeassistant/components/sensibo/number.py @@ -39,9 +39,9 @@ DEVICE_NUMBER_TYPES = ( icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, - min_value=-10, - max_value=10, - step=0.1, + native_min_value=-10, + native_max_value=10, + native_step=0.1, ), SensiboNumberEntityDescription( key="calibration_hum", @@ -50,9 +50,9 @@ DEVICE_NUMBER_TYPES = ( icon="mdi:water", entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, - min_value=-10, - max_value=10, - step=0.1, + native_min_value=-10, + native_max_value=10, + native_step=0.1, ), ) @@ -89,12 +89,12 @@ class SensiboNumber(SensiboDeviceBaseEntity, NumberEntity): self._attr_name = f"{self.device_data.name} {entity_description.name}" @property - def value(self) -> float | None: + def native_value(self) -> float | None: """Return the value from coordinator data.""" value: float | None = getattr(self.device_data, self.entity_description.key) return value - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set value for calibration.""" data = {self.entity_description.remote_key: value} result = await self.async_send_command("set_calibration", {"data": data}) diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index dcedc32602d..6658daf674f 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -42,12 +42,12 @@ NUMBERS: Final = { key="device|valvepos", icon="mdi:pipe-valve", name="Valve Position", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, available=lambda block: cast(int, block.valveError) != 1, entity_category=EntityCategory.CONFIG, - min_value=0, - max_value=100, - step=1, + native_min_value=0, + native_max_value=100, + native_step=1, mode=NumberMode("slider"), rest_path="thermostat/0", rest_arg="pos", @@ -62,11 +62,11 @@ def _build_block_description(entry: RegistryEntry) -> BlockNumberDescription: key="", name="", icon=entry.original_icon, - unit_of_measurement=entry.unit_of_measurement, + native_unit_of_measurement=entry.unit_of_measurement, device_class=entry.original_device_class, - min_value=cast(float, entry.capabilities.get("min")), - max_value=cast(float, entry.capabilities.get("max")), - step=cast(float, entry.capabilities.get("step")), + native_min_value=cast(float, entry.capabilities.get("min")), + native_max_value=cast(float, entry.capabilities.get("max")), + native_step=cast(float, entry.capabilities.get("step")), mode=cast(NumberMode, entry.capabilities.get("mode")), ) @@ -97,14 +97,14 @@ class BlockSleepingNumber(ShellySleepingBlockAttributeEntity, NumberEntity): entity_description: BlockNumberDescription @property - def value(self) -> float: + def native_value(self) -> float: """Return value of number.""" if self.block is not None: return cast(float, self.attribute_value) return cast(float, self.last_state) - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set value.""" # Example for Shelly Valve: http://192.168.188.187/thermostat/0?pos=13.0 await self._set_state_full_path( diff --git a/homeassistant/components/sleepiq/number.py b/homeassistant/components/sleepiq/number.py index b9aca69b3f4..a0ade257335 100644 --- a/homeassistant/components/sleepiq/number.py +++ b/homeassistant/components/sleepiq/number.py @@ -70,9 +70,9 @@ def _get_sleeper_unique_id(bed: SleepIQBed, sleeper: SleepIQSleeper) -> str: NUMBER_DESCRIPTIONS: dict[str, SleepIQNumberEntityDescription] = { FIRMNESS: SleepIQNumberEntityDescription( key=FIRMNESS, - min_value=5, - max_value=100, - step=5, + native_min_value=5, + native_max_value=100, + native_step=5, name=ENTITY_TYPES[FIRMNESS], icon=ICON_OCCUPIED, value_fn=lambda sleeper: cast(float, sleeper.sleep_number), @@ -82,9 +82,9 @@ NUMBER_DESCRIPTIONS: dict[str, SleepIQNumberEntityDescription] = { ), ACTUATOR: SleepIQNumberEntityDescription( key=ACTUATOR, - min_value=0, - max_value=100, - step=1, + native_min_value=0, + native_max_value=100, + native_step=1, name=ENTITY_TYPES[ACTUATOR], icon=ICON_OCCUPIED, value_fn=lambda actuator: cast(float, actuator.position), @@ -152,9 +152,9 @@ class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity): @callback def _async_update_attrs(self) -> None: """Update number attributes.""" - self._attr_value = float(self.entity_description.value_fn(self.device)) + self._attr_native_value = float(self.entity_description.value_fn(self.device)) - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set the number value.""" await self.entity_description.set_value_fn(self.device, int(value)) self._attr_value = value diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py index b0a10690ce6..202df1cb6f1 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -73,7 +73,7 @@ class SonosLevelEntity(SonosEntity, NumberEntity): name_suffix = level_type.replace("_", " ").title() self._attr_name = f"{self.speaker.zone_name} {name_suffix}" self.level_type = level_type - self._attr_min_value, self._attr_max_value = valid_range + self._attr_native_min_value, self._attr_native_max_value = valid_range async def _async_fallback_poll(self) -> None: """Poll the value if subscriptions are not working.""" @@ -86,11 +86,11 @@ class SonosLevelEntity(SonosEntity, NumberEntity): setattr(self.speaker, self.level_type, state) @soco_error() - def set_value(self, value: float) -> None: + def set_native_value(self, value: float) -> None: """Set a new value.""" setattr(self.soco, self.level_type, value) @property - def value(self) -> float: + def native_value(self) -> float: """Return the current value.""" return getattr(self.speaker, self.level_type) diff --git a/homeassistant/components/tolo/number.py b/homeassistant/components/tolo/number.py index 85d80756020..a6767a50814 100644 --- a/homeassistant/components/tolo/number.py +++ b/homeassistant/components/tolo/number.py @@ -34,8 +34,8 @@ class ToloNumberEntityDescription( """Class describing TOLO Number entities.""" entity_category = EntityCategory.CONFIG - min_value = 0 - step = 1 + native_min_value = 0 + native_step = 1 NUMBERS = ( @@ -43,8 +43,8 @@ NUMBERS = ( key="power_timer", icon="mdi:power-settings", name="Power Timer", - unit_of_measurement=TIME_MINUTES, - max_value=POWER_TIMER_MAX, + native_unit_of_measurement=TIME_MINUTES, + native_max_value=POWER_TIMER_MAX, getter=lambda settings: settings.power_timer, setter=lambda client, value: client.set_power_timer(value), ), @@ -52,8 +52,8 @@ NUMBERS = ( key="salt_bath_timer", icon="mdi:shaker-outline", name="Salt Bath Timer", - unit_of_measurement=TIME_MINUTES, - max_value=SALT_BATH_TIMER_MAX, + native_unit_of_measurement=TIME_MINUTES, + native_max_value=SALT_BATH_TIMER_MAX, getter=lambda settings: settings.salt_bath_timer, setter=lambda client, value: client.set_salt_bath_timer(value), ), @@ -61,8 +61,8 @@ NUMBERS = ( key="fan_timer", icon="mdi:fan-auto", name="Fan Timer", - unit_of_measurement=TIME_MINUTES, - max_value=FAN_TIMER_MAX, + native_unit_of_measurement=TIME_MINUTES, + native_max_value=FAN_TIMER_MAX, getter=lambda settings: settings.fan_timer, setter=lambda client, value: client.set_fan_timer(value), ), @@ -98,11 +98,11 @@ class ToloNumberEntity(ToloSaunaCoordinatorEntity, NumberEntity): self._attr_unique_id = f"{entry.entry_id}_{entity_description.key}" @property - def value(self) -> float: + def native_value(self) -> float: """Return the value of this TOLO Number entity.""" return self.entity_description.getter(self.coordinator.data.settings) or 0 - def set_value(self, value: float) -> None: + def set_native_value(self, value: float) -> None: """Set the value of this TOLO Number entity.""" int_value = int(value) if int_value == 0: From 05d7d31dfd4d4eb4a8b10a84319d7f2778e65298 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 15 Jun 2022 12:12:07 +0200 Subject: [PATCH 1515/3516] Improve Elgato error handling (#73444) --- homeassistant/components/elgato/button.py | 7 ++-- homeassistant/components/elgato/light.py | 28 ++++++++++------ tests/components/elgato/test_button.py | 21 +++++++----- tests/components/elgato/test_light.py | 41 +++++++++++++---------- 4 files changed, 58 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/elgato/button.py b/homeassistant/components/elgato/button.py index f2cfc2b8673..c846c42c653 100644 --- a/homeassistant/components/elgato/button.py +++ b/homeassistant/components/elgato/button.py @@ -9,6 +9,7 @@ from homeassistant.components.button import ButtonEntity, ButtonEntityDescriptio from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -49,5 +50,7 @@ class ElgatoIdentifyButton(ElgatoEntity, ButtonEntity): """Identify the light, will make it blink.""" try: await self.client.identify() - except ElgatoError: - _LOGGER.exception("An error occurred while identifying the Elgato Light") + except ElgatoError as error: + raise HomeAssistantError( + "An error occurred while identifying the Elgato Light" + ) from error diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index 8b9a7bce7e1..e13119c2887 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -15,6 +15,7 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, async_get_current_platform, @@ -25,7 +26,7 @@ from homeassistant.helpers.update_coordinator import ( ) from . import HomeAssistantElgatoData -from .const import DOMAIN, LOGGER, SERVICE_IDENTIFY +from .const import DOMAIN, SERVICE_IDENTIFY from .entity import ElgatoEntity PARALLEL_UPDATES = 1 @@ -121,9 +122,12 @@ class ElgatoLight( """Turn off the light.""" try: await self.client.light(on=False) - except ElgatoError: - LOGGER.error("An error occurred while updating the Elgato Light") - await self.coordinator.async_refresh() + except ElgatoError as error: + raise HomeAssistantError( + "An error occurred while updating the Elgato Light" + ) from error + finally: + await self.coordinator.async_refresh() async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" @@ -159,14 +163,18 @@ class ElgatoLight( saturation=saturation, temperature=temperature, ) - except ElgatoError: - LOGGER.error("An error occurred while updating the Elgato Light") - await self.coordinator.async_refresh() + except ElgatoError as error: + raise HomeAssistantError( + "An error occurred while updating the Elgato Light" + ) from error + finally: + await self.coordinator.async_refresh() async def async_identify(self) -> None: """Identify the light, will make it blink.""" try: await self.client.identify() - except ElgatoError: - LOGGER.exception("An error occurred while identifying the Elgato Light") - await self.coordinator.async_refresh() + except ElgatoError as error: + raise HomeAssistantError( + "An error occurred while identifying the Elgato Light" + ) from error diff --git a/tests/components/elgato/test_button.py b/tests/components/elgato/test_button.py index 6f182ee191c..2ab3d7ee7c4 100644 --- a/tests/components/elgato/test_button.py +++ b/tests/components/elgato/test_button.py @@ -8,6 +8,7 @@ from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRE from homeassistant.components.elgato.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, ATTR_ICON, STATE_UNKNOWN from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import EntityCategory @@ -68,17 +69,19 @@ async def test_button_identify_error( hass: HomeAssistant, init_integration: MockConfigEntry, mock_elgato: MagicMock, - caplog: pytest.LogCaptureFixture, ) -> None: """Test an error occurs with the Elgato identify button.""" mock_elgato.identify.side_effect = ElgatoError - await hass.services.async_call( - BUTTON_DOMAIN, - SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.identify"}, - blocking=True, - ) - await hass.async_block_till_done() + + with pytest.raises( + HomeAssistantError, match="An error occurred while identifying the Elgato Light" + ): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: "button.identify"}, + blocking=True, + ) + await hass.async_block_till_done() assert len(mock_elgato.identify.mock_calls) == 1 - assert "An error occurred while identifying the Elgato Light" in caplog.text diff --git a/tests/components/elgato/test_light.py b/tests/components/elgato/test_light.py index 743abc1ad49..9cd4f9bd326 100644 --- a/tests/components/elgato/test_light.py +++ b/tests/components/elgato/test_light.py @@ -24,6 +24,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry @@ -183,13 +184,15 @@ async def test_light_unavailable( mock_elgato.state.side_effect = ElgatoError mock_elgato.light.side_effect = ElgatoError - await hass.services.async_call( - LIGHT_DOMAIN, - service, - {ATTR_ENTITY_ID: "light.frenck"}, - blocking=True, - ) - await hass.async_block_till_done() + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + LIGHT_DOMAIN, + service, + {ATTR_ENTITY_ID: "light.frenck"}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.frenck") assert state assert state.state == STATE_UNAVAILABLE @@ -218,18 +221,20 @@ async def test_light_identify_error( hass: HomeAssistant, init_integration: MockConfigEntry, mock_elgato: MagicMock, - caplog: pytest.LogCaptureFixture, ) -> None: """Test error occurred during identifying an Elgato Light.""" mock_elgato.identify.side_effect = ElgatoError - await hass.services.async_call( - DOMAIN, - SERVICE_IDENTIFY, - { - ATTR_ENTITY_ID: "light.frenck", - }, - blocking=True, - ) - await hass.async_block_till_done() + with pytest.raises( + HomeAssistantError, match="An error occurred while identifying the Elgato Light" + ): + await hass.services.async_call( + DOMAIN, + SERVICE_IDENTIFY, + { + ATTR_ENTITY_ID: "light.frenck", + }, + blocking=True, + ) + await hass.async_block_till_done() + assert len(mock_elgato.identify.mock_calls) == 1 - assert "An error occurred while identifying the Elgato Light" in caplog.text From 658ce9d4f23311ff415406c6cc3ef014ce3b635d Mon Sep 17 00:00:00 2001 From: Thibaut Date: Wed, 15 Jun 2022 12:18:15 +0200 Subject: [PATCH 1516/3516] Remove Somfy integration (#73527) * Remove somfy * Remove somfy --- .coveragerc | 6 - CODEOWNERS | 2 - homeassistant/components/somfy/__init__.py | 143 ------------ homeassistant/components/somfy/api.py | 37 ---- homeassistant/components/somfy/climate.py | 177 --------------- homeassistant/components/somfy/config_flow.py | 26 --- homeassistant/components/somfy/const.py | 4 - homeassistant/components/somfy/coordinator.py | 71 ------ homeassistant/components/somfy/cover.py | 205 ------------------ homeassistant/components/somfy/entity.py | 73 ------- homeassistant/components/somfy/manifest.json | 17 -- homeassistant/components/somfy/sensor.py | 54 ----- homeassistant/components/somfy/strings.json | 18 -- homeassistant/components/somfy/switch.py | 55 ----- .../components/somfy/translations/bg.json | 17 -- .../components/somfy/translations/ca.json | 18 -- .../components/somfy/translations/cs.json | 18 -- .../components/somfy/translations/da.json | 16 -- .../components/somfy/translations/de.json | 18 -- .../components/somfy/translations/el.json | 18 -- .../components/somfy/translations/en.json | 18 -- .../components/somfy/translations/en_GB.json | 7 - .../components/somfy/translations/es-419.json | 16 -- .../components/somfy/translations/es.json | 18 -- .../components/somfy/translations/et.json | 18 -- .../components/somfy/translations/fr.json | 18 -- .../components/somfy/translations/he.json | 18 -- .../components/somfy/translations/hr.json | 7 - .../components/somfy/translations/hu.json | 18 -- .../components/somfy/translations/id.json | 18 -- .../components/somfy/translations/it.json | 18 -- .../components/somfy/translations/ja.json | 18 -- .../components/somfy/translations/ko.json | 18 -- .../components/somfy/translations/lb.json | 18 -- .../components/somfy/translations/nl.json | 18 -- .../components/somfy/translations/no.json | 18 -- .../components/somfy/translations/pl.json | 18 -- .../components/somfy/translations/pt-BR.json | 18 -- .../components/somfy/translations/pt.json | 18 -- .../components/somfy/translations/ru.json | 18 -- .../components/somfy/translations/sk.json | 7 - .../components/somfy/translations/sl.json | 16 -- .../components/somfy/translations/sv.json | 16 -- .../components/somfy/translations/tr.json | 18 -- .../components/somfy/translations/uk.json | 18 -- .../somfy/translations/zh-Hant.json | 18 -- homeassistant/generated/config_flows.py | 1 - homeassistant/generated/zeroconf.py | 4 - requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/somfy/__init__.py | 1 - tests/components/somfy/test_config_flow.py | 127 ----------- 52 files changed, 1561 deletions(-) delete mode 100644 homeassistant/components/somfy/__init__.py delete mode 100644 homeassistant/components/somfy/api.py delete mode 100644 homeassistant/components/somfy/climate.py delete mode 100644 homeassistant/components/somfy/config_flow.py delete mode 100644 homeassistant/components/somfy/const.py delete mode 100644 homeassistant/components/somfy/coordinator.py delete mode 100644 homeassistant/components/somfy/cover.py delete mode 100644 homeassistant/components/somfy/entity.py delete mode 100644 homeassistant/components/somfy/manifest.json delete mode 100644 homeassistant/components/somfy/sensor.py delete mode 100644 homeassistant/components/somfy/strings.json delete mode 100644 homeassistant/components/somfy/switch.py delete mode 100644 homeassistant/components/somfy/translations/bg.json delete mode 100644 homeassistant/components/somfy/translations/ca.json delete mode 100644 homeassistant/components/somfy/translations/cs.json delete mode 100644 homeassistant/components/somfy/translations/da.json delete mode 100644 homeassistant/components/somfy/translations/de.json delete mode 100644 homeassistant/components/somfy/translations/el.json delete mode 100644 homeassistant/components/somfy/translations/en.json delete mode 100644 homeassistant/components/somfy/translations/en_GB.json delete mode 100644 homeassistant/components/somfy/translations/es-419.json delete mode 100644 homeassistant/components/somfy/translations/es.json delete mode 100644 homeassistant/components/somfy/translations/et.json delete mode 100644 homeassistant/components/somfy/translations/fr.json delete mode 100644 homeassistant/components/somfy/translations/he.json delete mode 100644 homeassistant/components/somfy/translations/hr.json delete mode 100644 homeassistant/components/somfy/translations/hu.json delete mode 100644 homeassistant/components/somfy/translations/id.json delete mode 100644 homeassistant/components/somfy/translations/it.json delete mode 100644 homeassistant/components/somfy/translations/ja.json delete mode 100644 homeassistant/components/somfy/translations/ko.json delete mode 100644 homeassistant/components/somfy/translations/lb.json delete mode 100644 homeassistant/components/somfy/translations/nl.json delete mode 100644 homeassistant/components/somfy/translations/no.json delete mode 100644 homeassistant/components/somfy/translations/pl.json delete mode 100644 homeassistant/components/somfy/translations/pt-BR.json delete mode 100644 homeassistant/components/somfy/translations/pt.json delete mode 100644 homeassistant/components/somfy/translations/ru.json delete mode 100644 homeassistant/components/somfy/translations/sk.json delete mode 100644 homeassistant/components/somfy/translations/sl.json delete mode 100644 homeassistant/components/somfy/translations/sv.json delete mode 100644 homeassistant/components/somfy/translations/tr.json delete mode 100644 homeassistant/components/somfy/translations/uk.json delete mode 100644 homeassistant/components/somfy/translations/zh-Hant.json delete mode 100644 tests/components/somfy/__init__.py delete mode 100644 tests/components/somfy/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 455bc7fe8a8..fc40f0f35b9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1122,12 +1122,6 @@ omit = homeassistant/components/soma/cover.py homeassistant/components/soma/sensor.py homeassistant/components/soma/utils.py - homeassistant/components/somfy/__init__.py - homeassistant/components/somfy/api.py - homeassistant/components/somfy/climate.py - homeassistant/components/somfy/cover.py - homeassistant/components/somfy/sensor.py - homeassistant/components/somfy/switch.py homeassistant/components/somfy_mylink/__init__.py homeassistant/components/somfy_mylink/cover.py homeassistant/components/sonos/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 2d4fb5ceebf..d94e8866633 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -966,8 +966,6 @@ build.json @home-assistant/supervisor /tests/components/solax/ @squishykid /homeassistant/components/soma/ @ratsept @sebfortier2288 /tests/components/soma/ @ratsept @sebfortier2288 -/homeassistant/components/somfy/ @tetienne -/tests/components/somfy/ @tetienne /homeassistant/components/sonarr/ @ctalkington /tests/components/sonarr/ @ctalkington /homeassistant/components/songpal/ @rytilahti @shenxn diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py deleted file mode 100644 index ed6c58bc0a0..00000000000 --- a/homeassistant/components/somfy/__init__.py +++ /dev/null @@ -1,143 +0,0 @@ -"""Support for Somfy hubs.""" -from datetime import timedelta -import logging - -from pymfy.api.devices.category import Category -import voluptuous as vol - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - CONF_OPTIMISTIC, - Platform, -) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import ( - config_entry_oauth2_flow, - config_validation as cv, - device_registry as dr, -) -from homeassistant.helpers.typing import ConfigType - -from . import api, config_flow -from .const import COORDINATOR, DOMAIN -from .coordinator import SomfyDataUpdateCoordinator - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(minutes=1) -SCAN_INTERVAL_ALL_ASSUMED_STATE = timedelta(minutes=60) - -SOMFY_AUTH_CALLBACK_PATH = "/auth/somfy/callback" -SOMFY_AUTH_START = "/auth/somfy" - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Inclusive(CONF_CLIENT_ID, "oauth"): cv.string, - vol.Inclusive(CONF_CLIENT_SECRET, "oauth"): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - -PLATFORMS = [ - Platform.CLIMATE, - Platform.COVER, - Platform.SENSOR, - Platform.SWITCH, -] - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Somfy component.""" - hass.data[DOMAIN] = {} - domain_config = config.get(DOMAIN, {}) - hass.data[DOMAIN][CONF_OPTIMISTIC] = domain_config.get(CONF_OPTIMISTIC, False) - - if CONF_CLIENT_ID in domain_config: - config_flow.SomfyFlowHandler.async_register_implementation( - hass, - config_entry_oauth2_flow.LocalOAuth2Implementation( - hass, - DOMAIN, - config[DOMAIN][CONF_CLIENT_ID], - config[DOMAIN][CONF_CLIENT_SECRET], - "https://accounts.somfy.com/oauth/oauth/v2/auth", - "https://accounts.somfy.com/oauth/oauth/v2/token", - ), - ) - - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Somfy from a config entry.""" - - _LOGGER.warning( - "The Somfy integration is deprecated and will be removed " - "in Home Assistant Core 2022.7; due to the Somfy Open API deprecation." - "The Somfy Open API will shutdown June 21st 2022, migrate to the " - "Overkiz integration to control your Somfy devices" - ) - - # Backwards compat - if "auth_implementation" not in entry.data: - hass.config_entries.async_update_entry( - entry, data={**entry.data, "auth_implementation": DOMAIN} - ) - - implementation = ( - await config_entry_oauth2_flow.async_get_config_entry_implementation( - hass, entry - ) - ) - - data = hass.data[DOMAIN] - coordinator = SomfyDataUpdateCoordinator( - hass, - _LOGGER, - name="somfy device update", - client=api.ConfigEntrySomfyApi(hass, entry, implementation), - update_interval=SCAN_INTERVAL, - ) - data[COORDINATOR] = coordinator - - await coordinator.async_config_entry_first_refresh() - - if all(not bool(device.states) for device in coordinator.data.values()): - _LOGGER.debug( - "All devices have assumed state. Update interval has been reduced to: %s", - SCAN_INTERVAL_ALL_ASSUMED_STATE, - ) - coordinator.update_interval = SCAN_INTERVAL_ALL_ASSUMED_STATE - - device_registry = dr.async_get(hass) - - hubs = [ - device - for device in coordinator.data.values() - if Category.HUB.value in device.categories - ] - - for hub in hubs: - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - identifiers={(DOMAIN, hub.id)}, - manufacturer="Somfy", - name=hub.name, - model=hub.type, - ) - - hass.config_entries.async_setup_platforms(entry, PLATFORMS) - - return True - - -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/api.py b/homeassistant/components/somfy/api.py deleted file mode 100644 index bfa834cddb2..00000000000 --- a/homeassistant/components/somfy/api.py +++ /dev/null @@ -1,37 +0,0 @@ -"""API for Somfy bound to Home Assistant OAuth.""" -from __future__ import annotations - -from asyncio import run_coroutine_threadsafe - -from pymfy.api import somfy_api - -from homeassistant import config_entries, core -from homeassistant.helpers import config_entry_oauth2_flow - - -class ConfigEntrySomfyApi(somfy_api.SomfyApi): - """Provide a Somfy API tied into an OAuth2 based config entry.""" - - def __init__( - self, - hass: core.HomeAssistant, - config_entry: config_entries.ConfigEntry, - implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, - ) -> None: - """Initialize the Config Entry Somfy API.""" - self.hass = hass - self.config_entry = config_entry - self.session = config_entry_oauth2_flow.OAuth2Session( - hass, config_entry, implementation - ) - super().__init__(None, None, token=self.session.token) - - def refresh_tokens( - self, - ) -> dict[str, str | int]: - """Refresh and return new Somfy tokens using Home Assistant OAuth2 session.""" - run_coroutine_threadsafe( - self.session.async_ensure_token_valid(), self.hass.loop - ).result() - - return self.session.token diff --git a/homeassistant/components/somfy/climate.py b/homeassistant/components/somfy/climate.py deleted file mode 100644 index 1384574c3d3..00000000000 --- a/homeassistant/components/somfy/climate.py +++ /dev/null @@ -1,177 +0,0 @@ -"""Support for Somfy Thermostat.""" -from __future__ import annotations - -from pymfy.api.devices.category import Category -from pymfy.api.devices.thermostat import ( - DurationType, - HvacState, - RegulationState, - TargetMode, - Thermostat, -) - -from homeassistant.components.climate import ClimateEntity -from homeassistant.components.climate.const import ( - PRESET_AWAY, - PRESET_HOME, - PRESET_SLEEP, - ClimateEntityFeature, - HVACMode, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import COORDINATOR, DOMAIN -from .entity import SomfyEntity - -SUPPORTED_CATEGORIES = {Category.HVAC.value} - -PRESET_FROST_GUARD = "Frost Guard" -PRESET_GEOFENCING = "Geofencing" -PRESET_MANUAL = "Manual" - -PRESETS_MAPPING = { - TargetMode.AT_HOME: PRESET_HOME, - TargetMode.AWAY: PRESET_AWAY, - TargetMode.SLEEP: PRESET_SLEEP, - TargetMode.MANUAL: PRESET_MANUAL, - TargetMode.GEOFENCING: PRESET_GEOFENCING, - TargetMode.FROST_PROTECTION: PRESET_FROST_GUARD, -} -REVERSE_PRESET_MAPPING = {v: k for k, v in PRESETS_MAPPING.items()} - -HVAC_MODES_MAPPING = {HvacState.COOL: HVACMode.COOL, HvacState.HEAT: HVACMode.HEAT} - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the Somfy climate platform.""" - domain_data = hass.data[DOMAIN] - coordinator = domain_data[COORDINATOR] - - climates = [ - SomfyClimate(coordinator, device_id) - for device_id, device in coordinator.data.items() - if SUPPORTED_CATEGORIES & set(device.categories) - ] - - async_add_entities(climates) - - -class SomfyClimate(SomfyEntity, ClimateEntity): - """Representation of a Somfy thermostat device.""" - - _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE - ) - - def __init__(self, coordinator, device_id): - """Initialize the Somfy device.""" - super().__init__(coordinator, device_id) - self._climate = None - self._create_device() - - def _create_device(self): - """Update the device with the latest data.""" - self._climate = Thermostat(self.device, self.coordinator.client) - - @property - def temperature_unit(self): - """Return the unit of measurement used by the platform.""" - return TEMP_CELSIUS - - @property - def current_temperature(self): - """Return the current temperature.""" - return self._climate.get_ambient_temperature() - - @property - def target_temperature(self): - """Return the temperature we try to reach.""" - return self._climate.get_target_temperature() - - def set_temperature(self, **kwargs) -> None: - """Set new target temperature.""" - if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: - return - - self._climate.set_target(TargetMode.MANUAL, temperature, DurationType.NEXT_MODE) - - @property - def max_temp(self) -> float: - """Return the maximum temperature.""" - return 26.0 - - @property - def min_temp(self) -> float: - """Return the minimum temperature.""" - return 15.0 - - @property - def current_humidity(self): - """Return the current humidity.""" - return self._climate.get_humidity() - - @property - def hvac_mode(self) -> HVACMode: - """Return hvac operation ie. heat, cool mode.""" - if self._climate.get_regulation_state() == RegulationState.TIMETABLE: - return HVACMode.AUTO - return HVAC_MODES_MAPPING[self._climate.get_hvac_state()] - - @property - def hvac_modes(self) -> list[HVACMode]: - """Return the list of available hvac operation modes. - - HEAT and COOL mode are exclusive. End user has to enable a mode manually within the Somfy application. - So only one mode can be displayed. Auto mode is a scheduler. - """ - hvac_state = HVAC_MODES_MAPPING[self._climate.get_hvac_state()] - return [HVACMode.AUTO, hvac_state] - - def set_hvac_mode(self, hvac_mode: HVACMode) -> None: - """Set new target hvac mode.""" - if hvac_mode == HVACMode.AUTO: - self._climate.cancel_target() - else: - self._climate.set_target( - TargetMode.MANUAL, self.target_temperature, DurationType.FURTHER_NOTICE - ) - - @property - def preset_mode(self) -> str | None: - """Return the current preset mode.""" - mode = self._climate.get_target_mode() - return PRESETS_MAPPING.get(mode) - - @property - def preset_modes(self) -> list[str] | None: - """Return a list of available preset modes.""" - return list(PRESETS_MAPPING.values()) - - def set_preset_mode(self, preset_mode: str) -> None: - """Set new preset mode.""" - if self.preset_mode == preset_mode: - return - - if preset_mode == PRESET_HOME: - temperature = self._climate.get_at_home_temperature() - elif preset_mode == PRESET_AWAY: - temperature = self._climate.get_away_temperature() - elif preset_mode == PRESET_SLEEP: - temperature = self._climate.get_night_temperature() - elif preset_mode == PRESET_FROST_GUARD: - temperature = self._climate.get_frost_protection_temperature() - elif preset_mode in (PRESET_MANUAL, PRESET_GEOFENCING): - temperature = self.target_temperature - else: - raise ValueError(f"Preset mode not supported: {preset_mode}") - - self._climate.set_target( - REVERSE_PRESET_MAPPING[preset_mode], temperature, DurationType.NEXT_MODE - ) diff --git a/homeassistant/components/somfy/config_flow.py b/homeassistant/components/somfy/config_flow.py deleted file mode 100644 index 05d1720cf6d..00000000000 --- a/homeassistant/components/somfy/config_flow.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Config flow for Somfy.""" -import logging - -from homeassistant.helpers import config_entry_oauth2_flow - -from .const import DOMAIN - - -class SomfyFlowHandler( - config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN -): - """Config flow to handle Somfy OAuth2 authentication.""" - - DOMAIN = DOMAIN - - @property - def logger(self) -> logging.Logger: - """Return logger.""" - return logging.getLogger(__name__) - - async def async_step_user(self, user_input=None): - """Handle a flow start.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - return await super().async_step_user(user_input) diff --git a/homeassistant/components/somfy/const.py b/homeassistant/components/somfy/const.py deleted file mode 100644 index 6c7c23e3ab3..00000000000 --- a/homeassistant/components/somfy/const.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Define constants for the Somfy component.""" - -DOMAIN = "somfy" -COORDINATOR = "coordinator" diff --git a/homeassistant/components/somfy/coordinator.py b/homeassistant/components/somfy/coordinator.py deleted file mode 100644 index a22f8185702..00000000000 --- a/homeassistant/components/somfy/coordinator.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Helpers to help coordinate updated.""" -from __future__ import annotations - -from datetime import timedelta -import logging - -from pymfy.api.error import QuotaViolationException, SetupNotFoundException -from pymfy.api.model import Device -from pymfy.api.somfy_api import SomfyApi - -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - - -class SomfyDataUpdateCoordinator(DataUpdateCoordinator): - """Class to manage fetching Somfy data.""" - - def __init__( - self, - hass: HomeAssistant, - logger: logging.Logger, - *, - name: str, - client: SomfyApi, - update_interval: timedelta | None = None, - ) -> None: - """Initialize global data updater.""" - super().__init__( - hass, - logger, - name=name, - update_interval=update_interval, - ) - self.data = {} - self.client = client - self.site_device: dict[str, list] = {} - self.last_site_index = -1 - - async def _async_update_data(self) -> dict[str, Device]: - """Fetch Somfy data. - - Somfy only allow one call per minute to /site. There is one exception: 2 calls are allowed after site retrieval. - """ - if not self.site_device: - sites = await self.hass.async_add_executor_job(self.client.get_sites) - if not sites: - return {} - self.site_device = {site.id: [] for site in sites} - - site_id = self._site_id - try: - devices = await self.hass.async_add_executor_job( - self.client.get_devices, site_id - ) - self.site_device[site_id] = devices - except SetupNotFoundException: - del self.site_device[site_id] - return await self._async_update_data() - except QuotaViolationException: - self.logger.warning("Quota violation") - - return {dev.id: dev for devices in self.site_device.values() for dev in devices} - - @property - def _site_id(self): - """Return the next site id to retrieve. - - This tweak is required as Somfy does not allow to call the /site entrypoint more than once per minute. - """ - self.last_site_index = (self.last_site_index + 1) % len(self.site_device) - return list(self.site_device.keys())[self.last_site_index] diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py deleted file mode 100644 index a2a72d4ce98..00000000000 --- a/homeassistant/components/somfy/cover.py +++ /dev/null @@ -1,205 +0,0 @@ -"""Support for Somfy Covers.""" -from __future__ import annotations - -from typing import cast - -from pymfy.api.devices.blind import Blind -from pymfy.api.devices.category import Category - -from homeassistant.components.cover import ( - ATTR_POSITION, - ATTR_TILT_POSITION, - CoverDeviceClass, - CoverEntity, - CoverEntityFeature, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_OPTIMISTIC, STATE_CLOSED, STATE_OPEN -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.restore_state import RestoreEntity - -from .const import COORDINATOR, DOMAIN -from .coordinator import SomfyDataUpdateCoordinator -from .entity import SomfyEntity - -BLIND_DEVICE_CATEGORIES = {Category.INTERIOR_BLIND.value, Category.EXTERIOR_BLIND.value} -SHUTTER_DEVICE_CATEGORIES = {Category.EXTERIOR_BLIND.value} -SUPPORTED_CATEGORIES = { - Category.ROLLER_SHUTTER.value, - Category.INTERIOR_BLIND.value, - Category.EXTERIOR_BLIND.value, -} - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the Somfy cover platform.""" - domain_data = hass.data[DOMAIN] - coordinator = domain_data[COORDINATOR] - - covers = [ - SomfyCover(coordinator, device_id, domain_data[CONF_OPTIMISTIC]) - for device_id, device in coordinator.data.items() - if SUPPORTED_CATEGORIES & set(device.categories) - ] - - async_add_entities(covers) - - -class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): - """Representation of a Somfy cover device.""" - - def __init__(self, coordinator, device_id, optimistic): - """Initialize the Somfy device.""" - super().__init__(coordinator, device_id) - self.categories = set(self.device.categories) - self.optimistic = optimistic - self._closed = None - self._is_opening = None - self._is_closing = None - self._cover = None - self._create_device() - - def _create_device(self) -> Blind: - """Update the device with the latest data.""" - self._cover = Blind( - self.device, cast(SomfyDataUpdateCoordinator, self.coordinator).client - ) - - @property - def supported_features(self) -> int: - """Flag supported features.""" - supported_features = 0 - if self.has_capability("open"): - supported_features |= CoverEntityFeature.OPEN - if self.has_capability("close"): - supported_features |= CoverEntityFeature.CLOSE - if self.has_capability("stop"): - supported_features |= CoverEntityFeature.STOP - if self.has_capability("position"): - supported_features |= CoverEntityFeature.SET_POSITION - if self.has_capability("rotation"): - supported_features |= ( - CoverEntityFeature.OPEN_TILT - | CoverEntityFeature.CLOSE_TILT - | CoverEntityFeature.STOP_TILT - | CoverEntityFeature.SET_TILT_POSITION - ) - - return supported_features - - async def async_close_cover(self, **kwargs): - """Close the cover.""" - self._is_closing = True - self.async_write_ha_state() - try: - # Blocks until the close command is sent - await self.hass.async_add_executor_job(self._cover.close) - self._closed = True - finally: - self._is_closing = None - self.async_write_ha_state() - - async def async_open_cover(self, **kwargs): - """Open the cover.""" - self._is_opening = True - self.async_write_ha_state() - try: - # Blocks until the open command is sent - await self.hass.async_add_executor_job(self._cover.open) - self._closed = False - finally: - self._is_opening = None - self.async_write_ha_state() - - def stop_cover(self, **kwargs): - """Stop the cover.""" - self._cover.stop() - - def set_cover_position(self, **kwargs): - """Move the cover shutter to a specific position.""" - self._cover.set_position(100 - kwargs[ATTR_POSITION]) - - @property - def device_class(self): - """Return the device class.""" - if self.categories & BLIND_DEVICE_CATEGORIES: - return CoverDeviceClass.BLIND - if self.categories & SHUTTER_DEVICE_CATEGORIES: - return CoverDeviceClass.SHUTTER - return None - - @property - def current_cover_position(self): - """Return the current position of cover shutter.""" - if not self.has_state("position"): - return None - return 100 - self._cover.get_position() - - @property - def is_opening(self): - """Return if the cover is opening.""" - if not self.optimistic: - return None - return self._is_opening - - @property - def is_closing(self): - """Return if the cover is closing.""" - if not self.optimistic: - return None - return self._is_closing - - @property - def is_closed(self) -> bool | None: - """Return if the cover is closed.""" - is_closed = None - if self.has_state("position"): - is_closed = self._cover.is_closed() - elif self.optimistic: - is_closed = self._closed - return is_closed - - @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 not self.has_state("orientation"): - return None - return 100 - self._cover.orientation - - def set_cover_tilt_position(self, **kwargs): - """Move the cover tilt to a specific position.""" - self._cover.orientation = 100 - kwargs[ATTR_TILT_POSITION] - - def open_cover_tilt(self, **kwargs): - """Open the cover tilt.""" - self._cover.orientation = 0 - - def close_cover_tilt(self, **kwargs): - """Close the cover tilt.""" - self._cover.orientation = 100 - - def stop_cover_tilt(self, **kwargs): - """Stop the cover.""" - self._cover.stop() - - async def async_added_to_hass(self): - """Complete the initialization.""" - await super().async_added_to_hass() - if not self.optimistic: - return - # Restore the last state if we use optimistic - last_state = await self.async_get_last_state() - - if last_state is not None and last_state.state in ( - STATE_OPEN, - STATE_CLOSED, - ): - self._closed = last_state.state == STATE_CLOSED diff --git a/homeassistant/components/somfy/entity.py b/homeassistant/components/somfy/entity.py deleted file mode 100644 index 2d92c8a77c0..00000000000 --- a/homeassistant/components/somfy/entity.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Entity representing a Somfy device.""" - -from abc import abstractmethod - -from homeassistant.core import callback -from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import DOMAIN - - -class SomfyEntity(CoordinatorEntity, Entity): - """Representation of a generic Somfy device.""" - - def __init__(self, coordinator, device_id): - """Initialize the Somfy device.""" - super().__init__(coordinator) - self._id = device_id - - @property - def device(self): - """Return data for the device id.""" - return self.coordinator.data[self._id] - - @property - def unique_id(self) -> str: - """Return the unique id base on the id returned by Somfy.""" - return self._id - - @property - def name(self) -> str: - """Return the name of the device.""" - return self.device.name - - @property - def device_info(self) -> DeviceInfo: - """Return device specific attributes. - - Implemented by platform classes. - """ - 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", - ) - - def has_capability(self, capability: str) -> bool: - """Test if device has a capability.""" - capabilities = self.device.capabilities - return bool([c for c in capabilities if c.name == capability]) - - def has_state(self, state: str) -> bool: - """Test if device has a state.""" - states = self.device.states - return bool([c for c in states if c.name == state]) - - @property - def assumed_state(self) -> bool: - """Return if the device has an assumed state.""" - return not bool(self.device.states) - - @callback - def _handle_coordinator_update(self): - """Process an update from the coordinator.""" - self._create_device() - super()._handle_coordinator_update() - - @abstractmethod - def _create_device(self): - """Update the device with the latest data.""" diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json deleted file mode 100644 index 76e3d281228..00000000000 --- a/homeassistant/components/somfy/manifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "domain": "somfy", - "name": "Somfy", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/somfy", - "dependencies": ["auth"], - "codeowners": ["@tetienne"], - "requirements": ["pymfy==0.11.0"], - "zeroconf": [ - { - "type": "_kizbox._tcp.local.", - "name": "gateway*" - } - ], - "iot_class": "cloud_polling", - "loggers": ["pymfy"] -} diff --git a/homeassistant/components/somfy/sensor.py b/homeassistant/components/somfy/sensor.py deleted file mode 100644 index 6dcc45b78a5..00000000000 --- a/homeassistant/components/somfy/sensor.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Support for Somfy Thermostat Battery.""" -from pymfy.api.devices.category import Category -from pymfy.api.devices.thermostat import Thermostat - -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import COORDINATOR, DOMAIN -from .entity import SomfyEntity - -SUPPORTED_CATEGORIES = {Category.HVAC.value} - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the Somfy sensor platform.""" - domain_data = hass.data[DOMAIN] - coordinator = domain_data[COORDINATOR] - - sensors = [ - SomfyThermostatBatterySensor(coordinator, device_id) - for device_id, device in coordinator.data.items() - if SUPPORTED_CATEGORIES & set(device.categories) - ] - - async_add_entities(sensors) - - -class SomfyThermostatBatterySensor(SomfyEntity, SensorEntity): - """Representation of a Somfy thermostat battery.""" - - _attr_device_class = SensorDeviceClass.BATTERY - _attr_native_unit_of_measurement = PERCENTAGE - - def __init__(self, coordinator, device_id): - """Initialize the Somfy device.""" - super().__init__(coordinator, device_id) - self._climate = None - self._create_device() - - def _create_device(self): - """Update the device with the latest data.""" - self._climate = Thermostat(self.device, self.coordinator.client) - - @property - def native_value(self) -> int: - """Return the state of the sensor.""" - return self._climate.get_battery() diff --git a/homeassistant/components/somfy/strings.json b/homeassistant/components/somfy/strings.json deleted file mode 100644 index 85ef981e356..00000000000 --- a/homeassistant/components/somfy/strings.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "step": { - "pick_implementation": { - "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" - } - }, - "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%]" - }, - "create_entry": { - "default": "[%key:common::config_flow::create_entry::authenticated%]" - } - } -} diff --git a/homeassistant/components/somfy/switch.py b/homeassistant/components/somfy/switch.py deleted file mode 100644 index d86fc051a14..00000000000 --- a/homeassistant/components/somfy/switch.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Support for Somfy Camera Shutter.""" -from pymfy.api.devices.camera_protect import CameraProtect -from pymfy.api.devices.category import Category - -from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .const import COORDINATOR, DOMAIN -from .entity import SomfyEntity - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the Somfy switch platform.""" - domain_data = hass.data[DOMAIN] - coordinator = domain_data[COORDINATOR] - - switches = [ - SomfyCameraShutter(coordinator, device_id) - for device_id, device in coordinator.data.items() - if Category.CAMERA.value in device.categories - ] - - async_add_entities(switches) - - -class SomfyCameraShutter(SomfyEntity, SwitchEntity): - """Representation of a Somfy Camera Shutter device.""" - - def __init__(self, coordinator, device_id): - """Initialize the Somfy device.""" - super().__init__(coordinator, device_id) - self._create_device() - - def _create_device(self): - """Update the device with the latest data.""" - self.shutter = CameraProtect(self.device, self.coordinator.client) - - def turn_on(self, **kwargs) -> None: - """Turn the entity on.""" - self.shutter.open_shutter() - - def turn_off(self, **kwargs): - """Turn the entity off.""" - self.shutter.close_shutter() - - @property - def is_on(self) -> bool: - """Return True if entity is on.""" - return self.shutter.get_shutter_position() == "opened" diff --git a/homeassistant/components/somfy/translations/bg.json b/homeassistant/components/somfy/translations/bg.json deleted file mode 100644 index 62905ef389e..00000000000 --- a/homeassistant/components/somfy/translations/bg.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "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.", - "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." - }, - "step": { - "pick_implementation": { - "title": "\u0418\u0437\u0431\u043e\u0440 \u043d\u0430 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/ca.json b/homeassistant/components/somfy/translations/ca.json deleted file mode 100644 index bc34c57c939..00000000000 --- a/homeassistant/components/somfy/translations/ca.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", - "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", - "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", - "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." - }, - "create_entry": { - "default": "Autenticaci\u00f3 exitosa" - }, - "step": { - "pick_implementation": { - "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/cs.json b/homeassistant/components/somfy/translations/cs.json deleted file mode 100644 index acc7d260cad..00000000000 --- a/homeassistant/components/somfy/translations/cs.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", - "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", - "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", - "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." - }, - "create_entry": { - "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" - }, - "step": { - "pick_implementation": { - "title": "Vyberte metodu ov\u011b\u0159en\u00ed" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/da.json b/homeassistant/components/somfy/translations/da.json deleted file mode 100644 index 3b7a79ef008..00000000000 --- a/homeassistant/components/somfy/translations/da.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "Timeout ved generering af autoriseret url.", - "missing_configuration": "Komponenten Somfy er ikke konfigureret. F\u00f8lg venligst dokumentationen." - }, - "create_entry": { - "default": "Godkendt med Somfy." - }, - "step": { - "pick_implementation": { - "title": "V\u00e6lg godkendelsesmetode" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/de.json b/homeassistant/components/somfy/translations/de.json deleted file mode 100644 index 29a959f48ce..00000000000 --- a/homeassistant/components/somfy/translations/de.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", - "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", - "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", - "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." - }, - "create_entry": { - "default": "Erfolgreich authentifiziert" - }, - "step": { - "pick_implementation": { - "title": "W\u00e4hle die Authentifizierungsmethode" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/el.json b/homeassistant/components/somfy/translations/el.json deleted file mode 100644 index 8d1f457ae10..00000000000 --- a/homeassistant/components/somfy/translations/el.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2.", - "missing_configuration": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", - "no_url_available": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL. \u0393\u03b9\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1, [\u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b2\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1\u03c2] ( {docs_url} )", - "single_instance_allowed": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03b4\u03b7. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bc\u03b5\u03c4\u03c1\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." - }, - "create_entry": { - "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" - }, - "step": { - "pick_implementation": { - "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/en.json b/homeassistant/components/somfy/translations/en.json deleted file mode 100644 index e0072d1da4d..00000000000 --- a/homeassistant/components/somfy/translations/en.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "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." - }, - "create_entry": { - "default": "Successfully authenticated" - }, - "step": { - "pick_implementation": { - "title": "Pick Authentication Method" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/en_GB.json b/homeassistant/components/somfy/translations/en_GB.json deleted file mode 100644 index ddf7ee6d5dd..00000000000 --- a/homeassistant/components/somfy/translations/en_GB.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "Timeout generating authorise URL." - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/es-419.json b/homeassistant/components/somfy/translations/es-419.json deleted file mode 100644 index 6acd9bb6bb8..00000000000 --- a/homeassistant/components/somfy/translations/es-419.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente Somfy no est\u00e1 configurado. Por favor, siga la documentaci\u00f3n." - }, - "create_entry": { - "default": "Autenticado con \u00e9xito con Somfy." - }, - "step": { - "pick_implementation": { - "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/es.json b/homeassistant/components/somfy/translations/es.json deleted file mode 100644 index 2f8b35f5af8..00000000000 --- a/homeassistant/components/somfy/translations/es.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", - "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." - }, - "create_entry": { - "default": "Autenticado correctamente" - }, - "step": { - "pick_implementation": { - "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/et.json b/homeassistant/components/somfy/translations/et.json deleted file mode 100644 index 9239f7df0ef..00000000000 --- a/homeassistant/components/somfy/translations/et.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp.", - "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.", - "no_url_available": "URL pole saadaval. Rohkem teavet [check the help section]({docs_url})", - "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." - }, - "create_entry": { - "default": "Edukalt tuvastatud" - }, - "step": { - "pick_implementation": { - "title": "Vali tuvastusmeetod" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/fr.json b/homeassistant/components/somfy/translations/fr.json deleted file mode 100644 index 0c7a25831bc..00000000000 --- a/homeassistant/components/somfy/translations/fr.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", - "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", - "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." - }, - "create_entry": { - "default": "Authentification r\u00e9ussie" - }, - "step": { - "pick_implementation": { - "title": "S\u00e9lectionner une m\u00e9thode d'authentification" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/he.json b/homeassistant/components/somfy/translations/he.json deleted file mode 100644 index c68d7f74d85..00000000000 --- a/homeassistant/components/somfy/translations/he.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "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})", - "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." - }, - "create_entry": { - "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" - }, - "step": { - "pick_implementation": { - "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/hr.json b/homeassistant/components/somfy/translations/hr.json deleted file mode 100644 index a601eb2b9bf..00000000000 --- a/homeassistant/components/somfy/translations/hr.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "create_entry": { - "default": "Uspje\u0161no autentificirano sa Somfy." - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/hu.json b/homeassistant/components/somfy/translations/hu.json deleted file mode 100644 index 96b873b2c42..00000000000 --- a/homeassistant/components/somfy/translations/hu.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "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\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", - "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", - "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." - }, - "create_entry": { - "default": "Sikeres hiteles\u00edt\u00e9s" - }, - "step": { - "pick_implementation": { - "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/id.json b/homeassistant/components/somfy/translations/id.json deleted file mode 100644 index 2d229de00d5..00000000000 --- a/homeassistant/components/somfy/translations/id.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", - "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})", - "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." - }, - "create_entry": { - "default": "Berhasil diautentikasi" - }, - "step": { - "pick_implementation": { - "title": "Pilih Metode Autentikasi" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/it.json b/homeassistant/components/somfy/translations/it.json deleted file mode 100644 index 0201e1e2569..00000000000 --- a/homeassistant/components/somfy/translations/it.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", - "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", - "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", - "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." - }, - "create_entry": { - "default": "Autenticazione riuscita" - }, - "step": { - "pick_implementation": { - "title": "Scegli il metodo di autenticazione" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/ja.json b/homeassistant/components/somfy/translations/ja.json deleted file mode 100644 index 3c25bd7bb8f..00000000000 --- a/homeassistant/components/somfy/translations/ja.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", - "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", - "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" - }, - "create_entry": { - "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" - }, - "step": { - "pick_implementation": { - "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/ko.json b/homeassistant/components/somfy/translations/ko.json deleted file mode 100644 index 568c8d05116..00000000000 --- a/homeassistant/components/somfy/translations/ko.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", - "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." - }, - "create_entry": { - "default": "\uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" - }, - "step": { - "pick_implementation": { - "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/lb.json b/homeassistant/components/somfy/translations/lb.json deleted file mode 100644 index a463473c2e1..00000000000 --- a/homeassistant/components/somfy/translations/lb.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", - "missing_configuration": "Komponent ass nach net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.", - "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})", - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." - }, - "create_entry": { - "default": "Erfollegr\u00e4ich authentifiz\u00e9iert." - }, - "step": { - "pick_implementation": { - "title": "Wiel Authentifikatiouns Method aus" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/nl.json b/homeassistant/components/somfy/translations/nl.json deleted file mode 100644 index efd07952467..00000000000 --- a/homeassistant/components/somfy/translations/nl.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", - "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})", - "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." - }, - "create_entry": { - "default": "Authenticatie geslaagd" - }, - "step": { - "pick_implementation": { - "title": "Kies een authenticatie methode" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/no.json b/homeassistant/components/somfy/translations/no.json deleted file mode 100644 index 57bc6e68436..00000000000 --- a/homeassistant/components/somfy/translations/no.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", - "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", - "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", - "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." - }, - "create_entry": { - "default": "Vellykket godkjenning" - }, - "step": { - "pick_implementation": { - "title": "Velg godkjenningsmetode" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/pl.json b/homeassistant/components/somfy/translations/pl.json deleted file mode 100644 index baeb38e755e..00000000000 --- a/homeassistant/components/somfy/translations/pl.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", - "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", - "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", - "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." - }, - "create_entry": { - "default": "Pomy\u015blnie uwierzytelniono" - }, - "step": { - "pick_implementation": { - "title": "Wybierz metod\u0119 uwierzytelniania" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/pt-BR.json b/homeassistant/components/somfy/translations/pt-BR.json deleted file mode 100644 index 8ad5fac9044..00000000000 --- a/homeassistant/components/somfy/translations/pt-BR.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", - "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", - "no_url_available": "N\u00e3o h\u00e1 URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." - }, - "create_entry": { - "default": "Autenticado com sucesso" - }, - "step": { - "pick_implementation": { - "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/pt.json b/homeassistant/components/somfy/translations/pt.json deleted file mode 100644 index 592ccd85589..00000000000 --- a/homeassistant/components/somfy/translations/pt.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", - "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", - "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." - }, - "create_entry": { - "default": "Autenticado com sucesso" - }, - "step": { - "pick_implementation": { - "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/ru.json b/homeassistant/components/somfy/translations/ru.json deleted file mode 100644 index 38ac0dda412..00000000000 --- a/homeassistant/components/somfy/translations/ru.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", - "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", - "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." - }, - "create_entry": { - "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." - }, - "step": { - "pick_implementation": { - "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/sk.json b/homeassistant/components/somfy/translations/sk.json deleted file mode 100644 index c19b1a0b70c..00000000000 --- a/homeassistant/components/somfy/translations/sk.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "create_entry": { - "default": "\u00daspe\u0161ne overen\u00e9" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/sl.json b/homeassistant/components/somfy/translations/sl.json deleted file mode 100644 index 3b9bc038fe6..00000000000 --- a/homeassistant/components/somfy/translations/sl.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", - "missing_configuration": "Komponenta Somfy ni konfigurirana. Upo\u0161tevajte dokumentacijo." - }, - "create_entry": { - "default": "Uspe\u0161no overjen s Somfy-jem." - }, - "step": { - "pick_implementation": { - "title": "Izberite na\u010din preverjanja pristnosti" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/sv.json b/homeassistant/components/somfy/translations/sv.json deleted file mode 100644 index cf0a8ec75bf..00000000000 --- a/homeassistant/components/somfy/translations/sv.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "Timeout vid skapandet av en auktoriseringsadress.", - "missing_configuration": "Somfy-komponenten \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen." - }, - "create_entry": { - "default": "Lyckad autentisering med Somfy." - }, - "step": { - "pick_implementation": { - "title": "V\u00e4lj autentiseringsmetod" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/tr.json b/homeassistant/components/somfy/translations/tr.json deleted file mode 100644 index b3b645cd52d..00000000000 --- a/homeassistant/components/somfy/translations/tr.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", - "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", - "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})", - "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." - }, - "create_entry": { - "default": "Ba\u015far\u0131yla do\u011fruland\u0131" - }, - "step": { - "pick_implementation": { - "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/uk.json b/homeassistant/components/somfy/translations/uk.json deleted file mode 100644 index 207169ad6b0..00000000000 --- a/homeassistant/components/somfy/translations/uk.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", - "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", - "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", - "single_instance_allowed": "\u0412\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e. \u041c\u043e\u0436\u043b\u0438\u0432\u0430 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f." - }, - "create_entry": { - "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." - }, - "step": { - "pick_implementation": { - "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/zh-Hant.json b/homeassistant/components/somfy/translations/zh-Hant.json deleted file mode 100644 index 8dccd6771cb..00000000000 --- a/homeassistant/components/somfy/translations/zh-Hant.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "abort": { - "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", - "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", - "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})", - "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" - }, - "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49" - }, - "step": { - "pick_implementation": { - "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a50fc85a9f0..6b26b2a99b7 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -327,7 +327,6 @@ FLOWS = { "solarlog", "solax", "soma", - "somfy", "somfy_mylink", "sonarr", "songpal", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 8d4f1148582..21631292f13 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -233,10 +233,6 @@ ZEROCONF = { { "domain": "overkiz", "name": "gateway*" - }, - { - "domain": "somfy", - "name": "gateway*" } ], "_leap._tcp.local.": [ diff --git a/requirements_all.txt b/requirements_all.txt index 03359ed2a0e..d38fc15b273 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1648,9 +1648,6 @@ pymelcloud==2.5.6 # homeassistant.components.meteoclimatic pymeteoclimatic==0.0.6 -# homeassistant.components.somfy -pymfy==0.11.0 - # homeassistant.components.xiaomi_tv pymitv==1.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2d8f44b9da9..2049ae73e25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1115,9 +1115,6 @@ pymelcloud==2.5.6 # homeassistant.components.meteoclimatic pymeteoclimatic==0.0.6 -# homeassistant.components.somfy -pymfy==0.11.0 - # homeassistant.components.mochad pymochad==0.2.0 diff --git a/tests/components/somfy/__init__.py b/tests/components/somfy/__init__.py deleted file mode 100644 index 05f5cbcf4f0..00000000000 --- a/tests/components/somfy/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Somfy component.""" diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py deleted file mode 100644 index 752959802da..00000000000 --- a/tests/components/somfy/test_config_flow.py +++ /dev/null @@ -1,127 +0,0 @@ -"""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 -from homeassistant.components.somfy import DOMAIN -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET -from homeassistant.helpers import config_entry_oauth2_flow - -from tests.common import MockConfigEntry - -CLIENT_ID_VALUE = "1234" -CLIENT_SECRET_VALUE = "5678" - - -async def test_abort_if_no_configuration(hass): - """Check flow abort when no configuration.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "missing_configuration" - - -async def test_abort_if_existing_entry(hass): - """Check flow abort when an entry already exist.""" - MockConfigEntry(domain=DOMAIN).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "single_instance_allowed" - - -async def test_full_flow( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host -): - """Check full flow.""" - assert await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - CONF_CLIENT_ID: CLIENT_ID_VALUE, - CONF_CLIENT_SECRET: CLIENT_SECRET_VALUE, - } - }, - ) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - state = config_entry_oauth2_flow._encode_jwt( - hass, - { - "flow_id": result["flow_id"], - "redirect_uri": "https://example.com/auth/external/callback", - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP - assert result["url"] == ( - "https://accounts.somfy.com/oauth/oauth/v2/auth" - f"?response_type=code&client_id={CLIENT_ID_VALUE}" - "&redirect_uri=https://example.com/auth/external/callback" - f"&state={state}" - ) - - client = await hass_client_no_auth() - resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") - assert resp.status == HTTPStatus.OK - assert resp.headers["content-type"] == "text/html; charset=utf-8" - - aioclient_mock.post( - "https://accounts.somfy.com/oauth/oauth/v2/token", - json={ - "refresh_token": "mock-refresh-token", - "access_token": "mock-access-token", - "type": "Bearer", - "expires_in": 60, - }, - ) - - with patch( - "homeassistant.components.somfy.async_setup_entry", return_value=True - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - - assert result["data"]["auth_implementation"] == DOMAIN - - result["data"]["token"].pop("expires_at") - assert result["data"]["token"] == { - "refresh_token": "mock-refresh-token", - "access_token": "mock-access-token", - "type": "Bearer", - "expires_in": 60, - } - - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_abort_if_authorization_timeout(hass, current_request_with_host): - """Check Somfy authorization timeout.""" - assert await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - CONF_CLIENT_ID: CLIENT_ID_VALUE, - CONF_CLIENT_SECRET: CLIENT_SECRET_VALUE, - } - }, - ) - - with patch( - "homeassistant.components.somfy.config_entry_oauth2_flow." - "LocalOAuth2Implementation.async_generate_authorize_url", - side_effect=asyncio.TimeoutError, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "authorize_url_timeout" From 8007effd4fe6830997f48fe5d941eddae18d4a57 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 15 Jun 2022 13:32:39 +0200 Subject: [PATCH 1517/3516] Update pyupgrade to v2.34.0 (#73530) --- .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 1104ecf07e9..18b34a222aa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.32.1 + rev: v2.34.0 hooks: - id: pyupgrade args: [--py39-plus] diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 047dcbf90ad..0d204771e40 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -12,5 +12,5 @@ mccabe==0.6.1 pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 -pyupgrade==2.32.1 +pyupgrade==2.34.0 yamllint==1.26.3 From 8c0ae545c915e04c1a8e3f478970af0f89b87625 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 15 Jun 2022 14:39:56 +0200 Subject: [PATCH 1518/3516] Migrate knx NumberEntity to native_value (#73536) --- homeassistant/components/knx/number.py | 27 +++++++++--------- tests/components/knx/test_number.py | 38 +++++++++++++++----------- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/knx/number.py b/homeassistant/components/knx/number.py index 9f12aa4ce24..fbf4db3f5b2 100644 --- a/homeassistant/components/knx/number.py +++ b/homeassistant/components/knx/number.py @@ -7,7 +7,7 @@ from xknx import XKNX from xknx.devices import NumericValue from homeassistant import config_entries -from homeassistant.components.number import NumberEntity +from homeassistant.components.number import RestoreNumber from homeassistant.const import ( CONF_ENTITY_CATEGORY, CONF_MODE, @@ -19,7 +19,6 @@ from homeassistant.const import ( ) 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 from .const import ( @@ -57,7 +56,7 @@ def _create_numeric_value(xknx: XKNX, config: ConfigType) -> NumericValue: ) -class KNXNumber(KnxEntity, NumberEntity, RestoreEntity): +class KNXNumber(KnxEntity, RestoreNumber): """Representation of a KNX number.""" _device: NumericValue @@ -65,39 +64,41 @@ class KNXNumber(KnxEntity, NumberEntity, RestoreEntity): def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize a KNX number.""" super().__init__(_create_numeric_value(xknx, config)) - self._attr_max_value = config.get( + self._attr_native_max_value = config.get( NumberSchema.CONF_MAX, self._device.sensor_value.dpt_class.value_max, ) - self._attr_min_value = config.get( + self._attr_native_min_value = config.get( NumberSchema.CONF_MIN, self._device.sensor_value.dpt_class.value_min, ) self._attr_mode = config[CONF_MODE] - self._attr_step = config.get( + self._attr_native_step = config.get( 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) + self._attr_native_unit_of_measurement = self._device.unit_of_measurement() + self._device.sensor_value.value = max(0, self._attr_native_min_value) async def async_added_to_hass(self) -> None: """Restore last state.""" await super().async_added_to_hass() - if not self._device.sensor_value.readable and ( - last_state := await self.async_get_last_state() + if ( + not self._device.sensor_value.readable + and (last_state := await self.async_get_last_state()) + and (last_number_data := await self.async_get_last_number_data()) ): if last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): - self._device.sensor_value.value = float(last_state.state) + self._device.sensor_value.value = last_number_data.native_value @property - def value(self) -> float: + def native_value(self) -> float: """Return the entity value to represent the entity state.""" # self._device.sensor_value.value is set in __init__ so it is never None return cast(float, self._device.resolve_state()) - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set new value.""" await self._device.set(value) diff --git a/tests/components/knx/test_number.py b/tests/components/knx/test_number.py index 668b046df74..837d7624dae 100644 --- a/tests/components/knx/test_number.py +++ b/tests/components/knx/test_number.py @@ -1,6 +1,4 @@ """Test KNX number.""" -from unittest.mock import patch - import pytest from homeassistant.components.knx.const import CONF_RESPOND_TO_READ, KNX_ADDRESS @@ -10,6 +8,8 @@ from homeassistant.core import HomeAssistant, State from .conftest import KNXTestKit +from tests.common import mock_restore_cache_with_extra_data + async def test_number_set_value(hass: HomeAssistant, knx: KNXTestKit): """Test KNX number with passive_address and respond_to_read restoring state.""" @@ -64,22 +64,28 @@ 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: { - CONF_NAME: "test", - KNX_ADDRESS: [test_address, test_passive_address], - CONF_RESPOND_TO_READ: True, - CONF_TYPE: "illuminance", - } + RESTORE_DATA = { + "native_max_value": None, # Ignored by KNX number + "native_min_value": None, # Ignored by KNX number + "native_step": None, # Ignored by KNX number + "native_unit_of_measurement": None, # Ignored by KNX number + "native_value": 160.0, + } + + mock_restore_cache_with_extra_data( + hass, ((State("number.test", "abc"), RESTORE_DATA),) + ) + await knx.setup_integration( + { + NumberSchema.PLATFORM: { + 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" From f8f1bfde2129fb96250fe9d718f6c7a413fce07a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 15 Jun 2022 15:23:36 +0200 Subject: [PATCH 1519/3516] Add lock typing in components (#73539) * Add lock typing in components * Revert freedompro amends --- homeassistant/components/august/lock.py | 5 +++-- homeassistant/components/demo/lock.py | 15 ++++++++------- homeassistant/components/fibaro/lock.py | 8 +++++--- homeassistant/components/homematic/lock.py | 10 ++++++---- homeassistant/components/keba/lock.py | 8 +++++--- homeassistant/components/kiwi/lock.py | 5 +++-- homeassistant/components/mazda/lock.py | 10 +++++++--- homeassistant/components/mqtt/lock.py | 9 +++++---- homeassistant/components/nuki/lock.py | 19 ++++++++++--------- homeassistant/components/smartthings/lock.py | 7 ++++--- homeassistant/components/starline/lock.py | 6 ++++-- homeassistant/components/subaru/lock.py | 5 +++-- homeassistant/components/template/lock.py | 14 ++++++++------ homeassistant/components/volvooncall/lock.py | 6 ++++-- homeassistant/components/zha/lock.py | 5 +++-- 15 files changed, 78 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index c993cf03b89..9269dc52c6a 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -1,5 +1,6 @@ """Support for August lock.""" import logging +from typing import Any from aiohttp import ClientResponseError from yalexs.activity import SOURCE_PUBNUB, ActivityType @@ -44,14 +45,14 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): self._attr_unique_id = f"{self._device_id:s}_lock" self._update_from_data() - async def async_lock(self, **kwargs): + async def async_lock(self, **kwargs: Any) -> None: """Lock the device.""" if self._data.activity_stream.pubnub.connected: await self._data.async_lock_async(self._device_id, self._hyper_bridge) return await self._call_lock_operation(self._data.async_lock) - async def async_unlock(self, **kwargs): + async def async_unlock(self, **kwargs: Any) -> None: """Unlock the device.""" if self._data.activity_stream.pubnub.connected: await self._data.async_unlock_async(self._device_id, self._hyper_bridge) diff --git a/homeassistant/components/demo/lock.py b/homeassistant/components/demo/lock.py index 86188e8b935..c59dfad2fab 100644 --- a/homeassistant/components/demo/lock.py +++ b/homeassistant/components/demo/lock.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from typing import Any from homeassistant.components.lock import LockEntity, LockEntityFeature from homeassistant.config_entries import ConfigEntry @@ -66,26 +67,26 @@ class DemoLock(LockEntity): self._jam_on_operation = jam_on_operation @property - def is_locking(self): + def is_locking(self) -> bool: """Return true if lock is locking.""" return self._state == STATE_LOCKING @property - def is_unlocking(self): + def is_unlocking(self) -> bool: """Return true if lock is unlocking.""" return self._state == STATE_UNLOCKING @property - def is_jammed(self): + def is_jammed(self) -> bool: """Return true if lock is jammed.""" return self._state == STATE_JAMMED @property - def is_locked(self): + def is_locked(self) -> bool: """Return true if lock is locked.""" return self._state == STATE_LOCKED - async def async_lock(self, **kwargs): + async def async_lock(self, **kwargs: Any) -> None: """Lock the device.""" self._state = STATE_LOCKING self.async_write_ha_state() @@ -96,7 +97,7 @@ class DemoLock(LockEntity): self._state = STATE_LOCKED self.async_write_ha_state() - async def async_unlock(self, **kwargs): + async def async_unlock(self, **kwargs: Any) -> None: """Unlock the device.""" self._state = STATE_UNLOCKING self.async_write_ha_state() @@ -104,7 +105,7 @@ class DemoLock(LockEntity): self._state = STATE_UNLOCKED self.async_write_ha_state() - async def async_open(self, **kwargs): + async def async_open(self, **kwargs: Any) -> None: """Open the door latch.""" self._state = STATE_UNLOCKED self.async_write_ha_state() diff --git a/homeassistant/components/fibaro/lock.py b/homeassistant/components/fibaro/lock.py index aed7017ba61..ac4ce658b65 100644 --- a/homeassistant/components/fibaro/lock.py +++ b/homeassistant/components/fibaro/lock.py @@ -1,6 +1,8 @@ """Support for Fibaro locks.""" from __future__ import annotations +from typing import Any + from homeassistant.components.lock import ENTITY_ID_FORMAT, LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -37,18 +39,18 @@ class FibaroLock(FibaroDevice, LockEntity): super().__init__(fibaro_device) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) - def lock(self, **kwargs): + def lock(self, **kwargs: Any) -> None: """Lock the device.""" self.action("secure") self._state = True - def unlock(self, **kwargs): + def unlock(self, **kwargs: Any) -> None: """Unlock the device.""" self.action("unsecure") self._state = False @property - def is_locked(self): + def is_locked(self) -> bool: """Return true if device is locked.""" return self._state diff --git a/homeassistant/components/homematic/lock.py b/homeassistant/components/homematic/lock.py index 32ee4698736..abca46ddf58 100644 --- a/homeassistant/components/homematic/lock.py +++ b/homeassistant/components/homematic/lock.py @@ -1,6 +1,8 @@ """Support for Homematic locks.""" from __future__ import annotations +from typing import Any + from homeassistant.components.lock import LockEntity, LockEntityFeature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -33,19 +35,19 @@ class HMLock(HMDevice, LockEntity): _attr_supported_features = LockEntityFeature.OPEN @property - def is_locked(self): + def is_locked(self) -> bool: """Return true if the lock is locked.""" return not bool(self._hm_get_state()) - def lock(self, **kwargs): + def lock(self, **kwargs: Any) -> None: """Lock the lock.""" self._hmdevice.lock() - def unlock(self, **kwargs): + def unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" self._hmdevice.unlock() - def open(self, **kwargs): + def open(self, **kwargs: Any) -> None: """Open the door latch.""" self._hmdevice.open() diff --git a/homeassistant/components/keba/lock.py b/homeassistant/components/keba/lock.py index b7563ae9b8e..d0316b1e525 100644 --- a/homeassistant/components/keba/lock.py +++ b/homeassistant/components/keba/lock.py @@ -1,6 +1,8 @@ """Support for KEBA charging station switch.""" from __future__ import annotations +from typing import Any + from homeassistant.components.lock import LockEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -51,15 +53,15 @@ class KebaLock(LockEntity): return f"{self._keba.device_name} {self._name}" @property - def is_locked(self): + def is_locked(self) -> bool: """Return true if lock is locked.""" return self._state - async def async_lock(self, **kwargs): + async def async_lock(self, **kwargs: Any) -> None: """Lock wallbox.""" await self._keba.async_stop() - async def async_unlock(self, **kwargs): + async def async_unlock(self, **kwargs: Any) -> None: """Unlock wallbox.""" await self._keba.async_start() diff --git a/homeassistant/components/kiwi/lock.py b/homeassistant/components/kiwi/lock.py index 3884045c9bc..bfd6b430c9f 100644 --- a/homeassistant/components/kiwi/lock.py +++ b/homeassistant/components/kiwi/lock.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from kiwiki import KiwiClient, KiwiException import voluptuous as vol @@ -89,7 +90,7 @@ class KiwiLock(LockEntity): return name or specifier @property - def is_locked(self): + def is_locked(self) -> bool: """Return true if lock is locked.""" return self._state == STATE_LOCKED @@ -104,7 +105,7 @@ class KiwiLock(LockEntity): self._state = STATE_LOCKED self.async_write_ha_state() - def unlock(self, **kwargs): + def unlock(self, **kwargs: Any) -> None: """Unlock the device.""" try: diff --git a/homeassistant/components/mazda/lock.py b/homeassistant/components/mazda/lock.py index 4b51a9eb97e..bcd409d2faf 100644 --- a/homeassistant/components/mazda/lock.py +++ b/homeassistant/components/mazda/lock.py @@ -1,4 +1,8 @@ """Platform for Mazda lock integration.""" +from __future__ import annotations + +from typing import Any + from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -36,17 +40,17 @@ class MazdaLock(MazdaEntity, LockEntity): self._attr_unique_id = self.vin @property - def is_locked(self): + def is_locked(self) -> bool | None: """Return true if lock is locked.""" return self.client.get_assumed_lock_state(self.vehicle_id) - async def async_lock(self, **kwargs): + async def async_lock(self, **kwargs: Any) -> None: """Lock the vehicle doors.""" await self.client.lock_doors(self.vehicle_id) self.async_write_ha_state() - async def async_unlock(self, **kwargs): + async def async_unlock(self, **kwargs: Any) -> None: """Unlock the vehicle doors.""" await self.client.unlock_doors(self.vehicle_id) diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 862e76635f7..0bdb66ab48b 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -2,6 +2,7 @@ from __future__ import annotations import functools +from typing import Any import voluptuous as vol @@ -183,7 +184,7 @@ class MqttLock(MqttEntity, LockEntity): await subscription.async_subscribe_topics(self.hass, self._sub_state) @property - def is_locked(self): + def is_locked(self) -> bool: """Return true if lock is locked.""" return self._state @@ -197,7 +198,7 @@ class MqttLock(MqttEntity, LockEntity): """Flag supported features.""" return LockEntityFeature.OPEN if CONF_PAYLOAD_OPEN in self._config else 0 - async def async_lock(self, **kwargs): + async def async_lock(self, **kwargs: Any) -> None: """Lock the device. This method is a coroutine. @@ -214,7 +215,7 @@ class MqttLock(MqttEntity, LockEntity): self._state = True self.async_write_ha_state() - async def async_unlock(self, **kwargs): + async def async_unlock(self, **kwargs: Any) -> None: """Unlock the device. This method is a coroutine. @@ -231,7 +232,7 @@ class MqttLock(MqttEntity, LockEntity): self._state = False self.async_write_ha_state() - async def async_open(self, **kwargs): + async def async_open(self, **kwargs: Any) -> None: """Open the door latch. This method is a coroutine. diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 6a39bea8cb6..765fc5f711f 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -1,5 +1,6 @@ """Nuki.io lock platform.""" from abc import ABC, abstractmethod +from typing import Any from pynuki.constants import MODE_OPENER_CONTINUOUS import voluptuous as vol @@ -92,15 +93,15 @@ class NukiDeviceEntity(NukiEntity, LockEntity, ABC): return super().available and self._nuki_device.state not in ERROR_STATES @abstractmethod - def lock(self, **kwargs): + def lock(self, **kwargs: Any) -> None: """Lock the device.""" @abstractmethod - def unlock(self, **kwargs): + def unlock(self, **kwargs: Any) -> None: """Unlock the device.""" @abstractmethod - def open(self, **kwargs): + def open(self, **kwargs: Any) -> None: """Open the door latch.""" @@ -112,15 +113,15 @@ class NukiLockEntity(NukiDeviceEntity): """Return true if lock is locked.""" return self._nuki_device.is_locked - def lock(self, **kwargs): + def lock(self, **kwargs: Any) -> None: """Lock the device.""" self._nuki_device.lock() - def unlock(self, **kwargs): + def unlock(self, **kwargs: Any) -> None: """Unlock the device.""" self._nuki_device.unlock() - def open(self, **kwargs): + def open(self, **kwargs: Any) -> None: """Open the door latch.""" self._nuki_device.unlatch() @@ -144,15 +145,15 @@ class NukiOpenerEntity(NukiDeviceEntity): or self._nuki_device.mode == MODE_OPENER_CONTINUOUS ) - def lock(self, **kwargs): + def lock(self, **kwargs: Any) -> None: """Disable ring-to-open.""" self._nuki_device.deactivate_rto() - def unlock(self, **kwargs): + def unlock(self, **kwargs: Any) -> None: """Enable ring-to-open.""" self._nuki_device.activate_rto() - def open(self, **kwargs): + def open(self, **kwargs: Any) -> None: """Buzz open the door.""" self._nuki_device.electric_strike_actuation() diff --git a/homeassistant/components/smartthings/lock.py b/homeassistant/components/smartthings/lock.py index be3fe949061..81866010667 100644 --- a/homeassistant/components/smartthings/lock.py +++ b/homeassistant/components/smartthings/lock.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Sequence +from typing import Any from pysmartthings import Attribute, Capability @@ -50,18 +51,18 @@ def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: class SmartThingsLock(SmartThingsEntity, LockEntity): """Define a SmartThings lock.""" - async def async_lock(self, **kwargs): + async def async_lock(self, **kwargs: Any) -> None: """Lock the device.""" await self._device.lock(set_status=True) self.async_write_ha_state() - async def async_unlock(self, **kwargs): + async def async_unlock(self, **kwargs: Any) -> None: """Unlock the device.""" await self._device.unlock(set_status=True) self.async_write_ha_state() @property - def is_locked(self): + def is_locked(self) -> bool: """Return true if lock is locked.""" return self._device.status.lock == ST_STATE_LOCKED diff --git a/homeassistant/components/starline/lock.py b/homeassistant/components/starline/lock.py index 20e389e48b5..48f91d89809 100644 --- a/homeassistant/components/starline/lock.py +++ b/homeassistant/components/starline/lock.py @@ -1,4 +1,6 @@ """Support for StarLine lock.""" +from typing import Any + from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -68,10 +70,10 @@ class StarlineLock(StarlineEntity, LockEntity): """Return true if lock is locked.""" return self._device.car_state.get("arm") - def lock(self, **kwargs): + def lock(self, **kwargs: Any) -> None: """Lock the car.""" self._account.api.set_car_state(self._device.device_id, "arm", True) - def unlock(self, **kwargs): + def unlock(self, **kwargs: Any) -> None: """Unlock the car.""" self._account.api.set_car_state(self._device.device_id, "arm", False) diff --git a/homeassistant/components/subaru/lock.py b/homeassistant/components/subaru/lock.py index 3c619690e96..bf9d1a8793b 100644 --- a/homeassistant/components/subaru/lock.py +++ b/homeassistant/components/subaru/lock.py @@ -1,5 +1,6 @@ """Support for Subaru door locks.""" import logging +from typing import Any import voluptuous as vol @@ -68,7 +69,7 @@ class SubaruLock(LockEntity): self._attr_unique_id = f"{vin}_door_locks" self._attr_device_info = get_device_info(vehicle_info) - async def async_lock(self, **kwargs): + async def async_lock(self, **kwargs: Any) -> None: """Send the lock command.""" _LOGGER.debug("Locking doors for: %s", self.car_name) await async_call_remote_service( @@ -77,7 +78,7 @@ class SubaruLock(LockEntity): self.vehicle_info, ) - async def async_unlock(self, **kwargs): + async def async_unlock(self, **kwargs: Any) -> None: """Send the unlock command.""" _LOGGER.debug("Unlocking doors for: %s", self.car_name) await async_call_remote_service( diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index f76750124ed..3f83e628f71 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -1,6 +1,8 @@ """Support for locks which integrates with other components.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.components.lock import ( @@ -95,22 +97,22 @@ class TemplateLock(TemplateEntity, LockEntity): return self._optimistic @property - def is_locked(self): + def is_locked(self) -> bool: """Return true if lock is locked.""" return self._state in ("true", STATE_ON, STATE_LOCKED) @property - def is_jammed(self): + def is_jammed(self) -> bool: """Return true if lock is jammed.""" return self._state == STATE_JAMMED @property - def is_unlocking(self): + def is_unlocking(self) -> bool: """Return true if lock is unlocking.""" return self._state == STATE_UNLOCKING @property - def is_locking(self): + def is_locking(self) -> bool: """Return true if lock is locking.""" return self._state == STATE_LOCKING @@ -138,14 +140,14 @@ class TemplateLock(TemplateEntity, LockEntity): ) await super().async_added_to_hass() - async def async_lock(self, **kwargs): + async def async_lock(self, **kwargs: Any) -> None: """Lock the device.""" if self._optimistic: self._state = True self.async_write_ha_state() await self.async_run_script(self._command_lock, context=self._context) - async def async_unlock(self, **kwargs): + async def async_unlock(self, **kwargs: Any) -> None: """Unlock the device.""" if self._optimistic: self._state = False diff --git a/homeassistant/components/volvooncall/lock.py b/homeassistant/components/volvooncall/lock.py index 23f80a4fae5..5023749e622 100644 --- a/homeassistant/components/volvooncall/lock.py +++ b/homeassistant/components/volvooncall/lock.py @@ -1,6 +1,8 @@ """Support for Volvo On Call locks.""" from __future__ import annotations +from typing import Any + from homeassistant.components.lock import LockEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -30,10 +32,10 @@ class VolvoLock(VolvoEntity, LockEntity): """Return true if lock is locked.""" return self.instrument.is_locked - async def async_lock(self, **kwargs): + async def async_lock(self, **kwargs: Any) -> None: """Lock the car.""" await self.instrument.lock() - async def async_unlock(self, **kwargs): + async def async_unlock(self, **kwargs: Any) -> None: """Unlock the car.""" await self.instrument.unlock() diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index 1ebb10cacb6..b26d7087b75 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -1,5 +1,6 @@ """Locks on Zigbee Home Automation networks.""" import functools +from typing import Any import voluptuous as vol from zigpy.zcl.foundation import Status @@ -119,7 +120,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity): """Return state attributes.""" return self.state_attributes - async def async_lock(self, **kwargs): + async def async_lock(self, **kwargs: Any) -> None: """Lock the lock.""" result = await self._doorlock_channel.lock_door() if isinstance(result, Exception) or result[0] is not Status.SUCCESS: @@ -127,7 +128,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity): return self.async_write_ha_state() - async def async_unlock(self, **kwargs): + async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" result = await self._doorlock_channel.unlock_door() if isinstance(result, Exception) or result[0] is not Status.SUCCESS: From b014d558ff6ad20cef7301e6b1c11116f3fa89ea Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 15 Jun 2022 07:15:53 -0700 Subject: [PATCH 1520/3516] Add application credentials platform for nest and deprecate yaml for SDM API (#73050) * Update the nest integration to be useable fully from the config flow * Support discovery in nest config flow * Remove configuration entries * Remove unused import * Remove dead code * Update homeassistant/components/nest/strings.json Co-authored-by: Martin Hjelmare * Remove commented out code * Use config flow for app auth reauthentication path * Improves for re-auth for upgrading existing project and creds * More dead code removal * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Remove outdated code * Update homeassistant/components/nest/config_flow.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/nest/__init__.py | 81 ++- homeassistant/components/nest/api.py | 21 +- .../nest/application_credentials.py | 24 + homeassistant/components/nest/auth.py | 54 -- homeassistant/components/nest/config_flow.py | 341 +++++---- homeassistant/components/nest/const.py | 2 +- homeassistant/components/nest/manifest.json | 2 +- homeassistant/components/nest/strings.json | 38 +- .../components/nest/translations/en.json | 35 +- .../generated/application_credentials.py | 1 + .../helpers/config_entry_oauth2_flow.py | 9 +- tests/components/nest/common.py | 34 +- tests/components/nest/conftest.py | 32 +- tests/components/nest/test_api.py | 5 + .../nest/test_config_flow_legacy.py | 9 - tests/components/nest/test_config_flow_sdm.py | 658 +++++++++++------- tests/components/nest/test_diagnostics.py | 2 - tests/components/nest/test_events.py | 4 +- tests/components/nest/test_init_sdm.py | 21 +- 19 files changed, 856 insertions(+), 517 deletions(-) create mode 100644 homeassistant/components/nest/application_credentials.py delete mode 100644 homeassistant/components/nest/auth.py diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 0920b37e6ef..29c2d817acd 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -22,6 +22,10 @@ from google_nest_sdm.exceptions import ( import voluptuous as vol from homeassistant.auth.permissions.const import POLICY_READ +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.components.camera import Image, img_util from homeassistant.components.http.const import KEY_HASS_USER from homeassistant.components.http.view import HomeAssistantView @@ -54,11 +58,14 @@ from . import api, config_flow from .const import ( CONF_PROJECT_ID, CONF_SUBSCRIBER_ID, + CONF_SUBSCRIBER_ID_IMPORTED, DATA_DEVICE_MANAGER, DATA_NEST_CONFIG, DATA_SDM, DATA_SUBSCRIBER, DOMAIN, + INSTALLED_AUTH_DOMAIN, + WEB_AUTH_DOMAIN, ) from .events import EVENT_NAME_MAP, NEST_EVENT from .legacy import async_setup_legacy, async_setup_legacy_entry @@ -112,20 +119,22 @@ THUMBNAIL_SIZE_PX = 175 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Nest components with dispatch between old/new flows.""" hass.data[DOMAIN] = {} - hass.data[DOMAIN][DATA_NEST_CONFIG] = config.get(DOMAIN) + + hass.http.register_view(NestEventMediaView(hass)) + hass.http.register_view(NestEventMediaThumbnailView(hass)) if DOMAIN not in config: - return True + return True # ConfigMode.SDM_APPLICATION_CREDENTIALS + + # Note that configuration.yaml deprecation warnings are handled in the + # config entry since we don't know what type of credentials we have and + # whether or not they can be imported. + hass.data[DOMAIN][DATA_NEST_CONFIG] = config[DOMAIN] config_mode = config_flow.get_config_mode(hass) if config_mode == config_flow.ConfigMode.LEGACY: return await async_setup_legacy(hass, config) - config_flow.register_flow_implementation_from_config(hass, config) - - hass.http.register_view(NestEventMediaView(hass)) - hass.http.register_view(NestEventMediaThumbnailView(hass)) - return True @@ -171,9 +180,13 @@ class SignalUpdateCallback: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Nest from a config entry with dispatch between old/new flows.""" - if DATA_SDM not in entry.data: + config_mode = config_flow.get_config_mode(hass) + if config_mode == config_flow.ConfigMode.LEGACY: return await async_setup_legacy_entry(hass, entry) + if config_mode == config_flow.ConfigMode.SDM: + await async_import_config(hass, entry) + subscriber = await api.new_subscriber(hass, entry) if not subscriber: return False @@ -223,6 +236,52 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +async def async_import_config(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Attempt to import configuration.yaml settings.""" + config = hass.data[DOMAIN][DATA_NEST_CONFIG] + new_data = { + CONF_PROJECT_ID: config[CONF_PROJECT_ID], + **entry.data, + } + if CONF_SUBSCRIBER_ID not in entry.data: + if CONF_SUBSCRIBER_ID not in config: + raise ValueError("Configuration option 'subscriber_id' missing") + new_data.update( + { + CONF_SUBSCRIBER_ID: config[CONF_SUBSCRIBER_ID], + CONF_SUBSCRIBER_ID_IMPORTED: True, # Don't delete user managed subscriber + } + ) + hass.config_entries.async_update_entry(entry, data=new_data) + + if entry.data["auth_implementation"] == INSTALLED_AUTH_DOMAIN: + # App Auth credentials have been deprecated and must be re-created + # by the user in the config flow + raise ConfigEntryAuthFailed( + "Google has deprecated App Auth credentials, and the integration " + "must be reconfigured in the UI to restore access to Nest Devices." + ) + + if entry.data["auth_implementation"] == WEB_AUTH_DOMAIN: + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential( + config[CONF_CLIENT_ID], + config[CONF_CLIENT_SECRET], + ), + WEB_AUTH_DOMAIN, + ) + + _LOGGER.warning( + "Configuration of Nest integration in YAML is deprecated and " + "will be removed in a future release; Your existing configuration " + "(including OAuth Application Credentials) has been imported into " + "the UI automatically and can be safely removed from your " + "configuration.yaml file" + ) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" if DATA_SDM not in entry.data: @@ -242,7 +301,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle removal of pubsub subscriptions created during config flow.""" - if DATA_SDM not in entry.data or CONF_SUBSCRIBER_ID not in entry.data: + if ( + DATA_SDM not in entry.data + or CONF_SUBSCRIBER_ID not in entry.data + or CONF_SUBSCRIBER_ID_IMPORTED in entry.data + ): return subscriber = await api.new_subscriber(hass, entry) diff --git a/homeassistant/components/nest/api.py b/homeassistant/components/nest/api.py index 830db926d9a..4d92cc30b1a 100644 --- a/homeassistant/components/nest/api.py +++ b/homeassistant/components/nest/api.py @@ -12,7 +12,6 @@ from google_nest_sdm.auth import AbstractAuth from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow @@ -20,8 +19,6 @@ from .const import ( API_URL, CONF_PROJECT_ID, CONF_SUBSCRIBER_ID, - DATA_NEST_CONFIG, - DOMAIN, OAUTH2_TOKEN, SDM_SCOPES, ) @@ -111,21 +108,19 @@ async def new_subscriber( hass, entry ) ) - config = hass.data[DOMAIN][DATA_NEST_CONFIG] - if not ( - subscriber_id := entry.data.get( - CONF_SUBSCRIBER_ID, config.get(CONF_SUBSCRIBER_ID) - ) + if not isinstance( + implementation, config_entry_oauth2_flow.LocalOAuth2Implementation ): - _LOGGER.error("Configuration option 'subscriber_id' required") - return None + raise ValueError(f"Unexpected auth implementation {implementation}") + if not (subscriber_id := entry.data.get(CONF_SUBSCRIBER_ID)): + raise ValueError("Configuration option 'subscriber_id' missing") auth = AsyncConfigEntryAuth( aiohttp_client.async_get_clientsession(hass), config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation), - config[CONF_CLIENT_ID], - config[CONF_CLIENT_SECRET], + implementation.client_id, + implementation.client_secret, ) - return GoogleNestSubscriber(auth, config[CONF_PROJECT_ID], subscriber_id) + return GoogleNestSubscriber(auth, entry.data[CONF_PROJECT_ID], subscriber_id) def new_subscriber_with_token( diff --git a/homeassistant/components/nest/application_credentials.py b/homeassistant/components/nest/application_credentials.py new file mode 100644 index 00000000000..7d88bc37322 --- /dev/null +++ b/homeassistant/components/nest/application_credentials.py @@ -0,0 +1,24 @@ +"""application_credentials platform for nest.""" + +from homeassistant.components.application_credentials import AuthorizationServer +from homeassistant.core import HomeAssistant + +from .const import OAUTH2_TOKEN + + +async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: + """Return authorization server.""" + return AuthorizationServer( + authorize_url="", # Overridden in config flow as needs device access project id + token_url=OAUTH2_TOKEN, + ) + + +async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]: + """Return description placeholders for the credentials dialog.""" + return { + "oauth_consent_url": "https://console.cloud.google.com/apis/credentials/consent", + "more_info_url": "https://www.home-assistant.io/integrations/nest/", + "oauth_creds_url": "https://console.cloud.google.com/apis/credentials", + "redirect_url": "https://my.home-assistant.io/redirect/oauth", + } diff --git a/homeassistant/components/nest/auth.py b/homeassistant/components/nest/auth.py deleted file mode 100644 index 648623b64c7..00000000000 --- a/homeassistant/components/nest/auth.py +++ /dev/null @@ -1,54 +0,0 @@ -"""OAuth implementations.""" -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_entry_oauth2_flow - -from .const import ( - INSTALLED_AUTH_DOMAIN, - OAUTH2_AUTHORIZE, - OAUTH2_TOKEN, - OOB_REDIRECT_URI, - WEB_AUTH_DOMAIN, -) - - -class WebAuth(config_entry_oauth2_flow.LocalOAuth2Implementation): - """OAuth implementation using OAuth for web applications.""" - - name = "OAuth for Web" - - def __init__( - self, hass: HomeAssistant, client_id: str, client_secret: str, project_id: str - ) -> None: - """Initialize WebAuth.""" - super().__init__( - hass, - WEB_AUTH_DOMAIN, - client_id, - client_secret, - OAUTH2_AUTHORIZE.format(project_id=project_id), - OAUTH2_TOKEN, - ) - - -class InstalledAppAuth(config_entry_oauth2_flow.LocalOAuth2Implementation): - """OAuth implementation using OAuth for installed applications.""" - - name = "OAuth for Apps" - - def __init__( - self, hass: HomeAssistant, client_id: str, client_secret: str, project_id: str - ) -> None: - """Initialize InstalledAppAuth.""" - super().__init__( - hass, - INSTALLED_AUTH_DOMAIN, - client_id, - client_secret, - OAUTH2_AUTHORIZE.format(project_id=project_id), - OAUTH2_TOKEN, - ) - - @property - def redirect_uri(self) -> str: - """Return the redirect uri.""" - return OOB_REDIRECT_URI diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index 61a61f6c8e0..bacd61447f5 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -1,27 +1,11 @@ """Config flow to configure Nest. This configuration flow supports the following: - - SDM API with Installed app flow where user enters an auth code manually - SDM API with Web OAuth flow with redirect back to Home Assistant - Legacy Nest API auth flow with where user enters an auth code manually NestFlowHandler is an implementation of AbstractOAuth2FlowHandler with -some overrides to support installed app and old APIs auth flow, reauth, -and other custom steps inserted in the middle of the flow. - -The notable config flow steps are: -- user: To dispatch between API versions -- auth: Inserted to add a hook for the installed app flow to accept a token -- async_oauth_create_entry: Overridden to handle when OAuth is complete. This - does not actually create the entry, but holds on to the OAuth token data - for later -- pubsub: Configure the pubsub subscription. Note that subscriptions created - by the config flow are deleted when removed. -- finish: Handles creating a new configuration entry or updating the existing - configuration entry for reauth. - -The SDM API config flow supports a hybrid of configuration.yaml (used as defaults) -and config flow. +some overrides to custom steps inserted in the middle of the flow. """ from __future__ import annotations @@ -43,16 +27,15 @@ from google_nest_sdm.exceptions import ( from google_nest_sdm.structure import InfoTrait, Structure import voluptuous as vol -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_entry_oauth2_flow -from homeassistant.helpers.typing import ConfigType from homeassistant.util import get_random_string from homeassistant.util.json import load_json -from . import api, auth +from . import api from .const import ( CONF_CLOUD_PROJECT_ID, CONF_PROJECT_ID, @@ -60,14 +43,36 @@ from .const import ( DATA_NEST_CONFIG, DATA_SDM, DOMAIN, - OOB_REDIRECT_URI, + INSTALLED_AUTH_DOMAIN, + OAUTH2_AUTHORIZE, SDM_SCOPES, ) DATA_FLOW_IMPL = "nest_flow_implementation" SUBSCRIPTION_FORMAT = "projects/{cloud_project_id}/subscriptions/home-assistant-{rnd}" SUBSCRIPTION_RAND_LENGTH = 10 + +MORE_INFO_URL = "https://www.home-assistant.io/integrations/nest/#configuration" + +# URLs for Configure Cloud Project step CLOUD_CONSOLE_URL = "https://console.cloud.google.com/home/dashboard" +SDM_API_URL = ( + "https://console.cloud.google.com/apis/library/smartdevicemanagement.googleapis.com" +) +PUBSUB_API_URL = "https://console.cloud.google.com/apis/library/pubsub.googleapis.com" + +# URLs for Configure Device Access Project step +DEVICE_ACCESS_CONSOLE_URL = "https://console.nest.google.com/device-access/" + +# URLs for App Auth deprecation and upgrade +UPGRADE_MORE_INFO_URL = ( + "https://www.home-assistant.io/integrations/nest/#deprecated-app-auth-credentials" +) +DEVICE_ACCESS_CONSOLE_EDIT_URL = ( + "https://console.nest.google.com/device-access/project/{project_id}/information" +) + + _LOGGER = logging.getLogger(__name__) @@ -76,13 +81,15 @@ class ConfigMode(Enum): SDM = 1 # SDM api with configuration.yaml LEGACY = 2 # "Works with Nest" API + SDM_APPLICATION_CREDENTIALS = 3 # Config entry only def get_config_mode(hass: HomeAssistant) -> ConfigMode: """Return the integration configuration mode.""" - if DOMAIN not in hass.data: - return ConfigMode.SDM - config = hass.data[DOMAIN][DATA_NEST_CONFIG] + if DOMAIN not in hass.data or not ( + config := hass.data[DOMAIN].get(DATA_NEST_CONFIG) + ): + return ConfigMode.SDM_APPLICATION_CREDENTIALS if CONF_PROJECT_ID in config: return ConfigMode.SDM return ConfigMode.LEGACY @@ -120,31 +127,6 @@ def register_flow_implementation( } -def register_flow_implementation_from_config( - hass: HomeAssistant, - config: ConfigType, -) -> None: - """Register auth implementations for SDM API from configuration yaml.""" - NestFlowHandler.async_register_implementation( - hass, - auth.InstalledAppAuth( - hass, - config[DOMAIN][CONF_CLIENT_ID], - config[DOMAIN][CONF_CLIENT_SECRET], - config[DOMAIN][CONF_PROJECT_ID], - ), - ) - NestFlowHandler.async_register_implementation( - hass, - auth.WebAuth( - hass, - config[DOMAIN][CONF_CLIENT_ID], - config[DOMAIN][CONF_CLIENT_SECRET], - config[DOMAIN][CONF_PROJECT_ID], - ), - ) - - class NestAuthError(HomeAssistantError): """Base class for Nest auth errors.""" @@ -179,7 +161,7 @@ class NestFlowHandler( def __init__(self) -> None: """Initialize NestFlowHandler.""" super().__init__() - self._reauth = False + self._upgrade = False self._data: dict[str, Any] = {DATA_SDM: {}} # Possible name to use for config entry based on the Google Home name self._structure_config_title: str | None = None @@ -189,6 +171,21 @@ class NestFlowHandler( """Return the configuration type for this flow.""" return get_config_mode(self.hass) + def _async_reauth_entry(self) -> ConfigEntry | None: + """Return existing entry for reauth.""" + if self.source != SOURCE_REAUTH or not ( + entry_id := self.context.get("entry_id") + ): + return None + return next( + ( + entry + for entry in self._async_current_entries() + if entry.entry_id == entry_id + ), + None, + ) + @property def logger(self) -> logging.Logger: """Return logger.""" @@ -204,11 +201,19 @@ class NestFlowHandler( "prompt": "consent", } + async def async_generate_authorize_url(self) -> str: + """Generate a url for the user to authorize based on user input.""" + config = self.hass.data.get(DOMAIN, {}).get(DATA_NEST_CONFIG, {}) + project_id = self._data.get(CONF_PROJECT_ID, config.get(CONF_PROJECT_ID, "")) + query = await super().async_generate_authorize_url() + authorize_url = OAUTH2_AUTHORIZE.format(project_id=project_id) + return f"{authorize_url}{query}" + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Complete OAuth setup and finish pubsub or finish.""" assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" self._data.update(data) - if not self._configure_pubsub(): + if self.source == SOURCE_REAUTH: _LOGGER.debug("Skipping Pub/Sub configuration") return await self.async_step_finish() return await self.async_step_pubsub() @@ -221,8 +226,8 @@ class NestFlowHandler( if user_input is None: _LOGGER.error("Reauth invoked with empty config entry data") return self.async_abort(reason="missing_configuration") - self._reauth = True self._data.update(user_input) + return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( @@ -232,87 +237,178 @@ class NestFlowHandler( assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" if user_input is None: return self.async_show_form(step_id="reauth_confirm") - existing_entries = self._async_current_entries() - if existing_entries: - # Pick an existing auth implementation for Reauth if present. Note - # only one ConfigEntry is allowed so its safe to pick the first. - entry = next(iter(existing_entries)) - if "auth_implementation" in entry.data: - data = {"implementation": entry.data["auth_implementation"]} - return await self.async_step_user(data) + if self._data["auth_implementation"] == INSTALLED_AUTH_DOMAIN: + # The config entry points to an auth mechanism that no longer works and the + # user needs to take action in the google cloud console to resolve. First + # prompt to create app creds, then later ensure they've updated the device + # access console. + self._upgrade = True + implementations = await config_entry_oauth2_flow.async_get_implementations( + self.hass, self.DOMAIN + ) + if not implementations: + return await self.async_step_auth_upgrade() return await self.async_step_user() + async def async_step_auth_upgrade( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Give instructions for upgrade of deprecated app auth.""" + assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" + if user_input is None: + return self.async_show_form( + step_id="auth_upgrade", + description_placeholders={ + "more_info_url": UPGRADE_MORE_INFO_URL, + }, + ) + # Abort this flow and ask the user for application credentials. The frontend + # will restart a new config flow after the user finishes so schedule a new + # re-auth config flow for the same entry so the user may resume. + if reauth_entry := self._async_reauth_entry(): + self.hass.async_add_job(reauth_entry.async_start_reauth, self.hass) + return self.async_abort(reason="missing_credentials") + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initialized by the user.""" - if self.config_mode == ConfigMode.SDM: - # Reauth will update an existing entry - if self._async_current_entries() and not self._reauth: - return self.async_abort(reason="single_instance_allowed") + if self.config_mode == ConfigMode.LEGACY: + return await self.async_step_init(user_input) + self._data[DATA_SDM] = {} + # Reauth will update an existing entry + entries = self._async_current_entries() + if entries and self.source != SOURCE_REAUTH: + return self.async_abort(reason="single_instance_allowed") + if self.source == SOURCE_REAUTH: return await super().async_step_user(user_input) - return await self.async_step_init(user_input) + # Application Credentials setup needs information from the user + # before creating the OAuth URL + return await self.async_step_create_cloud_project() + + async def async_step_create_cloud_project( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle initial step in app credentails flow.""" + implementations = await config_entry_oauth2_flow.async_get_implementations( + self.hass, self.DOMAIN + ) + if implementations: + return await self.async_step_cloud_project() + # This informational step explains to the user how to setup the + # cloud console and other pre-requisites needed before setting up + # an application credential. This extra step also allows discovery + # to start the config flow rather than aborting. The abort step will + # redirect the user to the right panel in the UI then return with a + # valid auth implementation. + if user_input is not None: + return self.async_abort(reason="missing_credentials") + return self.async_show_form( + step_id="create_cloud_project", + description_placeholders={ + "cloud_console_url": CLOUD_CONSOLE_URL, + "sdm_api_url": SDM_API_URL, + "pubsub_api_url": PUBSUB_API_URL, + "more_info_url": MORE_INFO_URL, + }, + ) + + async def async_step_cloud_project( + self, user_input: dict | None = None + ) -> FlowResult: + """Handle cloud project in user input.""" + if user_input is not None: + self._data.update(user_input) + return await self.async_step_device_project() + return self.async_show_form( + step_id="cloud_project", + data_schema=vol.Schema( + { + vol.Required(CONF_CLOUD_PROJECT_ID): str, + } + ), + description_placeholders={ + "cloud_console_url": CLOUD_CONSOLE_URL, + "more_info_url": MORE_INFO_URL, + }, + ) + + async def async_step_device_project( + self, user_input: dict | None = None + ) -> FlowResult: + """Collect device access project from user input.""" + errors = {} + if user_input is not None: + if user_input[CONF_PROJECT_ID] == self._data[CONF_CLOUD_PROJECT_ID]: + _LOGGER.error( + "Device Access Project ID and Cloud Project ID must not be the same, see documentation" + ) + errors[CONF_PROJECT_ID] = "wrong_project_id" + else: + self._data.update(user_input) + return await super().async_step_user() + + return self.async_show_form( + step_id="device_project", + data_schema=vol.Schema( + { + vol.Required(CONF_PROJECT_ID): str, + } + ), + description_placeholders={ + "device_access_console_url": DEVICE_ACCESS_CONSOLE_URL, + "more_info_url": MORE_INFO_URL, + }, + errors=errors, + ) async def async_step_auth( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Create an entry for auth.""" - if self.flow_impl.domain == "nest.installed": - # The default behavior from the parent class is to redirect the - # user with an external step. When using installed app auth, we - # instead prompt the user to sign in and copy/paste and - # authentication code back into this form. - # Note: This is similar to the Legacy API flow below, but it is - # simpler to reuse the OAuth logic in the parent class than to - # reuse SDM code with Legacy API code. - if user_input is not None: - self.external_data = { - "code": user_input["code"], - "state": {"redirect_uri": OOB_REDIRECT_URI}, - } - return await super().async_step_creation(user_input) - - result = await super().async_step_auth() - return self.async_show_form( - step_id="auth", - description_placeholders={"url": result["url"]}, - data_schema=vol.Schema({vol.Required("code"): str}), - ) + """Verify any last pre-requisites before sending user through OAuth flow.""" + if user_input is None and self._upgrade: + # During app auth upgrade we need the user to update their device access project + # before we redirect to the authentication flow. + return await self.async_step_device_project_upgrade() return await super().async_step_auth(user_input) - def _configure_pubsub(self) -> bool: - """Return True if the config flow should configure Pub/Sub.""" - if self._reauth: - # Just refreshing tokens and preserving existing subscriber id - return False - if CONF_SUBSCRIBER_ID in self.hass.data[DOMAIN][DATA_NEST_CONFIG]: - # Hard coded configuration.yaml skips pubsub in config flow - return False - # No existing subscription configured, so create in config flow - return True + async def async_step_device_project_upgrade( + self, user_input: dict | None = None + ) -> FlowResult: + """Update the device access project.""" + if user_input is not None: + # Resume OAuth2 redirects + return await super().async_step_auth() + if not isinstance( + self.flow_impl, config_entry_oauth2_flow.LocalOAuth2Implementation + ): + raise ValueError(f"Unexpected OAuth implementation: {self.flow_impl}") + client_id = self.flow_impl.client_id + return self.async_show_form( + step_id="device_project_upgrade", + description_placeholders={ + "device_access_console_url": DEVICE_ACCESS_CONSOLE_EDIT_URL.format( + project_id=self._data[CONF_PROJECT_ID] + ), + "more_info_url": UPGRADE_MORE_INFO_URL, + "client_id": client_id, + }, + ) async def async_step_pubsub( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Configure and create Pub/Sub subscriber.""" - # Populate data from the previous config entry during reauth, then - # overwrite with the user entered values. - data = {} - if self._reauth: - data.update(self._data) - if user_input: - data.update(user_input) + data = { + **self._data, + **(user_input if user_input is not None else {}), + } cloud_project_id = data.get(CONF_CLOUD_PROJECT_ID, "").strip() + config = self.hass.data.get(DOMAIN, {}).get(DATA_NEST_CONFIG, {}) + project_id = data.get(CONF_PROJECT_ID, config.get(CONF_PROJECT_ID)) - errors = {} - config = self.hass.data[DOMAIN][DATA_NEST_CONFIG] - if cloud_project_id == config[CONF_PROJECT_ID]: - _LOGGER.error( - "Wrong Project ID. Device Access Project ID used, but expected Cloud Project ID" - ) - errors[CONF_CLOUD_PROJECT_ID] = "wrong_project_id" - - if user_input is not None and not errors: + errors: dict[str, str] = {} + if cloud_project_id: # Create the subscriber id and/or verify it already exists. Note that # the existing id is used, and create call below is idempotent if not (subscriber_id := data.get(CONF_SUBSCRIBER_ID, "")): @@ -321,7 +417,7 @@ class NestFlowHandler( subscriber = api.new_subscriber_with_token( self.hass, self._data["token"]["access_token"], - config[CONF_PROJECT_ID], + project_id, subscriber_id, ) try: @@ -373,18 +469,11 @@ class NestFlowHandler( # Update existing config entry when in the reauth flow. This # integration only supports one config entry so remove any prior entries # added before the "single_instance_allowed" check was added - existing_entries = self._async_current_entries() - if existing_entries: - updated = False - for entry in existing_entries: - if updated: - await self.hass.config_entries.async_remove(entry.entry_id) - continue - updated = True - self.hass.config_entries.async_update_entry( - entry, data=self._data, unique_id=DOMAIN - ) - await self.hass.config_entries.async_reload(entry.entry_id) + if entry := self._async_reauth_entry(): + self.hass.config_entries.async_update_entry( + entry, data=self._data, unique_id=DOMAIN + ) + await self.hass.config_entries.async_reload(entry.entry_id) return self.async_abort(reason="reauth_successful") title = self.flow_impl.name if self._structure_config_title: diff --git a/homeassistant/components/nest/const.py b/homeassistant/components/nest/const.py index bd951756eae..64c27c1643b 100644 --- a/homeassistant/components/nest/const.py +++ b/homeassistant/components/nest/const.py @@ -11,6 +11,7 @@ INSTALLED_AUTH_DOMAIN = f"{DOMAIN}.installed" CONF_PROJECT_ID = "project_id" CONF_SUBSCRIBER_ID = "subscriber_id" +CONF_SUBSCRIBER_ID_IMPORTED = "subscriber_id_imported" CONF_CLOUD_PROJECT_ID = "cloud_project_id" SIGNAL_NEST_UPDATE = "nest_update" @@ -25,4 +26,3 @@ SDM_SCOPES = [ "https://www.googleapis.com/auth/pubsub", ] API_URL = "https://smartdevicemanagement.googleapis.com/v1" -OOB_REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob" diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 4f768e08843..d0588d46f06 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -2,7 +2,7 @@ "domain": "nest", "name": "Nest", "config_flow": true, - "dependencies": ["ffmpeg", "http", "auth"], + "dependencies": ["ffmpeg", "http", "application_credentials"], "after_dependencies": ["media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.0.0"], diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 1d3dfda1708..212903179b7 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -1,16 +1,38 @@ { + "application_credentials": { + "description": "Follow the [instructions]({more_info_url}) to configure the Cloud Console:\n\n1. Go to the [OAuth consent screen]({oauth_consent_url}) and configure\n1. Go to [Credentials]({oauth_creds_url}) and click **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **Web Application** for the Application Type.\n1. Add `{redirect_url}` under *Authorized redirect URI*." + }, "config": { "step": { + "auth_upgrade": { + "title": "Nest: App Auth Deprecation", + "description": "App Auth has been deprecated by Google to improve security, and you need to take action by creating new application credentials.\n\nOpen the [documentation]({more_info_url}) to follow along as the next steps will guide you through the steps you need to take to restore access to your Nest devices." + }, + "device_project_upgrade": { + "title": "Nest: Update Device Access Project", + "description": "Update the Nest Device Access Project with your new OAuth Client ID ([more info]({more_info_url}))\n1. Go to the [Device Access Console]({device_access_console_url}).\n1. Click the trash icon next to *OAuth Client ID*.\n1. Click the `...` overflow menu and *Add Client ID*.\n1. Enter your new OAuth Client ID and click **Add**.\n\nYour OAuth Client ID is: `{client_id}`" + }, + "create_cloud_project": { + "title": "Nest: Create and configure Cloud Project", + "description": "The Nest integration allows you to integrate your Nest Thermostats, Cameras, and Doorbells using the Smart Device Management API. The SDM API **requires a US $5** one time setup fee. See documentation for [more info]({more_info_url}).\n\n1. Go to the [Google Cloud Console]({cloud_console_url}).\n1. If this is your first project, click **Create Project** then **New Project**.\n1. Give your Cloud Project a Name and then click **Create**.\n1. Save the Cloud Project ID e.g. *example-project-12345* as you will need it later\n1. Go to API Library for [Smart Device Management API]({sdm_api_url}) and click **Enable**.\n1. Go to API Library for [Cloud Pub/Sub API]({pubsub_api_url}) and click **Enable**.\n\nProceed when your cloud project is set up." + }, + "cloud_project": { + "title": "Nest: Enter Cloud Project ID", + "description": "Enter the Cloud Project ID below e.g. *example-project-12345*. See the [Google Cloud Console]({cloud_console_url}) or the documentation for [more info]({more_info_url}).", + "data": { + "cloud_project_id": "Google Cloud Project ID" + } + }, + "device_project": { + "title": "Nest: Create a Device Access Project", + "description": "Create a Nest Device Access project which **requires a US $5 fee** to set up.\n1. Go to the [Device Access Console]({device_access_console_url}), and through the payment flow.\n1. Click on **Create project**\n1. Give your Device Access project a name and click **Next**.\n1. Enter your OAuth Client ID\n1. Enable events by clicking **Enable** and **Create project**.\n\nEnter your Device Access Project ID below ([more info]({more_info_url})).\n", + "data": { + "project_id": "Device Access Project ID" + } + }, "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" }, - "auth": { - "title": "Link Google Account", - "description": "To link your Google account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided Auth Token code below.", - "data": { - "code": "[%key:common::config_flow::data::access_token%]" - } - }, "pubsub": { "title": "Configure Google Cloud", "description": "Visit the [Cloud Console]({url}) to find your Google Cloud Project ID.", @@ -43,7 +65,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]", "internal_error": "Internal error validating code", "bad_project_id": "Please enter a valid Cloud Project ID (check Cloud Console)", - "wrong_project_id": "Please enter a valid Cloud Project ID (found Device Access Project ID)", + "wrong_project_id": "Please enter a valid Cloud Project ID (was same as Device Access Project ID)", "subscriber_error": "Unknown subscriber error, see logs" }, "abort": { diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index 6376807302b..90f7c244f7b 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "Follow the [instructions]({more_info_url}) to configure the Cloud Console:\n\n1. Go to the [OAuth consent screen]({oauth_consent_url}) and configure\n1. Go to [Credentials]({oauth_creds_url}) and click **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **Web Application** for the Application Type.\n1. Add `{redirect_url}` under *Authorized redirect URI*." + }, "config": { "abort": { "authorize_url_timeout": "Timeout generating authorize URL.", @@ -19,15 +22,41 @@ "subscriber_error": "Unknown subscriber error, see logs", "timeout": "Timeout validating code", "unknown": "Unexpected error", - "wrong_project_id": "Please enter a valid Cloud Project ID (found Device Access Project ID)" + "wrong_project_id": "Please enter a valid Cloud Project ID (was same as Device Access Project ID)" }, "step": { "auth": { "data": { "code": "Access Token" }, - "description": "To link your Google account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided Auth Token code below.", - "title": "Link Google Account" + "description": "To link your Google account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided Auth Token code below ([more info]({more_info_url})).", + "title": "Nest: Link Google Account" + }, + "auth_upgrade": { + "description": "App Auth has been deprecated by Google to improve security, and you need to take action by creating new application credentials.\n\nOpen the [documentation]({more_info_url}) to follow along as the next steps will guide you through the steps you need to take to restore access to your Nest devices.", + "title": "Nest: App Auth Deprecation" + }, + "cloud_project": { + "data": { + "cloud_project_id": "Google Cloud Project ID" + }, + "description": "Enter the Cloud Project ID below e.g. *example-project-12345*. See the [Google Cloud Console]({cloud_console_url}) or the documentation for [more info]({more_info_url}).", + "title": "Nest: Enter Cloud Project ID" + }, + "create_cloud_project": { + "description": "The Nest integration allows you to integrate your Nest Thermostats, Cameras, and Doorbells using the Smart Device Management API. The SDM API **requires a US $5** one time setup fee. See documentation for [more info]({more_info_url}).\n\n1. Go to the [Google Cloud Console]({cloud_console_url}).\n1. If this is your first project, click **Create Project** then **New Project**.\n1. Give your Cloud Project a Name and then click **Create**.\n1. Save the Cloud Project ID e.g. *example-project-12345* as you will need it later\n1. Go to API Library for [Smart Device Management API]({sdm_api_url}) and click **Enable**.\n1. Go to API Library for [Cloud Pub/Sub API]({pubsub_api_url}) and click **Enable**.\n\nProceed when your cloud project is set up.", + "title": "Nest: Create and configure Cloud Project" + }, + "device_project": { + "data": { + "project_id": "Device Access Project ID" + }, + "description": "Create a Nest Device Access project which **requires a US$5 fee** to set up.\n1. Go to the [Device Access Console]({device_access_console_url}), and through the payment flow.\n1. Click on **Create project**\n1. Give your Device Access project a name and click **Next**.\n1. Enter your OAuth Client ID\n1. Enable events by clicking **Enable** and **Create project**.\n\nEnter your Device Access Project ID below ([more info]({more_info_url})).\n", + "title": "Nest: Create a Device Access Project" + }, + "device_project_upgrade": { + "description": "Update the Nest Device Access Project with your new OAuth Client ID ([more info]({more_info_url}))\n1. Go to the [Device Access Console]({device_access_console_url}).\n1. Click the trash icon next to *OAuth Client ID*.\n1. Click the `...` overflow menu and *Add Client ID*.\n1. Enter your new OAuth Client ID and click **Add**.\n\nYour OAuth Client ID is: `{client_id}`", + "title": "Nest: Update Device Access Project" }, "init": { "data": { diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index 6d40b3fdef7..ba9762f58c0 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -11,6 +11,7 @@ APPLICATION_CREDENTIALS = [ "home_connect", "lyric", "neato", + "nest", "netatmo", "senz", "spotify", diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 9322d6e9dc1..0dc3415f7a9 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -233,6 +233,11 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): """Extra data that needs to be appended to the authorize url.""" return {} + async def async_generate_authorize_url(self) -> str: + """Generate a url for the user to authorize.""" + url = await self.flow_impl.async_generate_authorize_url(self.flow_id) + return str(URL(url).update_query(self.extra_authorize_data)) + async def async_step_pick_implementation( self, user_input: dict | None = None ) -> FlowResult: @@ -278,7 +283,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): try: async with async_timeout.timeout(10): - url = await self.flow_impl.async_generate_authorize_url(self.flow_id) + url = await self.async_generate_authorize_url() except asyncio.TimeoutError: return self.async_abort(reason="authorize_url_timeout") except NoURLAvailableError: @@ -289,8 +294,6 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): }, ) - url = str(URL(url).update_query(self.extra_authorize_data)) - return self.async_external_step(step_id="auth", url=url) async def async_step_creation( diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index 988906606ad..765a954b6de 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -1,8 +1,10 @@ """Common libraries for test setup.""" +from __future__ import annotations + from collections.abc import Awaitable, Callable import copy -from dataclasses import dataclass +from dataclasses import dataclass, field import time from typing import Any, Generator, TypeVar @@ -13,6 +15,7 @@ from google_nest_sdm.event import EventMessage from google_nest_sdm.event_media import CachePolicy from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber +from homeassistant.components.application_credentials import ClientCredential from homeassistant.components.nest import DOMAIN from homeassistant.components.nest.const import SDM_SCOPES @@ -73,8 +76,10 @@ def create_config_entry(token_expiration_time=None) -> MockConfigEntry: class NestTestConfig: """Holder for integration configuration.""" - config: dict[str, Any] - config_entry_data: dict[str, Any] + config: dict[str, Any] = field(default_factory=dict) + config_entry_data: dict[str, Any] | None = None + auth_implementation: str = WEB_AUTH_DOMAIN + credential: ClientCredential | None = None # Exercises mode where all configuration is in configuration.yaml @@ -86,7 +91,7 @@ TEST_CONFIG_YAML_ONLY = NestTestConfig( }, ) TEST_CONFIGFLOW_YAML_ONLY = NestTestConfig( - config=TEST_CONFIG_YAML_ONLY.config, config_entry_data=None + config=TEST_CONFIG_YAML_ONLY.config, ) # Exercises mode where subscriber id is created in the config flow, but @@ -106,8 +111,24 @@ TEST_CONFIG_HYBRID = NestTestConfig( "subscriber_id": SUBSCRIBER_ID, }, ) -TEST_CONFIGFLOW_HYBRID = NestTestConfig( - TEST_CONFIG_HYBRID.config, config_entry_data=None +TEST_CONFIGFLOW_HYBRID = NestTestConfig(TEST_CONFIG_HYBRID.config) + +# Exercises mode where all configuration is from the config flow +TEST_CONFIG_APP_CREDS = NestTestConfig( + config_entry_data={ + "sdm": {}, + "token": create_token_entry(), + "project_id": PROJECT_ID, + "cloud_project_id": CLOUD_PROJECT_ID, + "subscriber_id": SUBSCRIBER_ID, + }, + auth_implementation="imported-cred", + credential=ClientCredential(CLIENT_ID, CLIENT_SECRET), +) +TEST_CONFIGFLOW_APP_CREDS = NestTestConfig( + config=TEST_CONFIG_APP_CREDS.config, + auth_implementation="imported-cred", + credential=ClientCredential(CLIENT_ID, CLIENT_SECRET), ) TEST_CONFIG_LEGACY = NestTestConfig( @@ -126,6 +147,7 @@ TEST_CONFIG_LEGACY = NestTestConfig( }, }, }, + credential=None, ) diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index fafd04c3764..bacb3924bcd 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -14,6 +14,9 @@ from google_nest_sdm.auth import AbstractAuth from google_nest_sdm.device_manager import DeviceManager import pytest +from homeassistant.components.application_credentials import ( + async_import_client_credential, +) from homeassistant.components.nest import DOMAIN from homeassistant.components.nest.const import CONF_SUBSCRIBER_ID from homeassistant.core import HomeAssistant @@ -22,9 +25,8 @@ from homeassistant.setup import async_setup_component from .common import ( DEVICE_ID, SUBSCRIBER_ID, - TEST_CONFIG_HYBRID, + TEST_CONFIG_APP_CREDS, TEST_CONFIG_YAML_ONLY, - WEB_AUTH_DOMAIN, CreateDevice, FakeSubscriber, NestTestConfig, @@ -183,14 +185,14 @@ def subscriber_id() -> str: @pytest.fixture -def auth_implementation() -> str | None: +def auth_implementation(nest_test_config: NestTestConfig) -> str | None: """Fixture to let tests override the auth implementation in the config entry.""" - return WEB_AUTH_DOMAIN + return nest_test_config.auth_implementation @pytest.fixture( - params=[TEST_CONFIG_YAML_ONLY, TEST_CONFIG_HYBRID], - ids=["yaml-config-only", "hybrid-config"], + params=[TEST_CONFIG_YAML_ONLY, TEST_CONFIG_APP_CREDS], + ids=["yaml-config-only", "app-creds"], ) def nest_test_config(request) -> NestTestConfig: """Fixture that sets up the configuration used for the test.""" @@ -230,6 +232,20 @@ def config_entry( return MockConfigEntry(domain=DOMAIN, data=data) +@pytest.fixture(autouse=True) +async def credential(hass: HomeAssistant, nest_test_config: NestTestConfig) -> None: + """Fixture that provides the ClientCredential for the test if any.""" + if not nest_test_config.credential: + return + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, + DOMAIN, + nest_test_config.credential, + nest_test_config.auth_implementation, + ) + + @pytest.fixture async def setup_base_platform( hass: HomeAssistant, @@ -240,9 +256,7 @@ async def setup_base_platform( """Fixture to setup the integration platform.""" if config_entry: config_entry.add_to_hass(hass) - with patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" - ), patch("homeassistant.components.nest.PLATFORMS", platforms): + with patch("homeassistant.components.nest.PLATFORMS", platforms): async def _setup_func() -> bool: assert await async_setup_component(hass, DOMAIN, config) diff --git a/tests/components/nest/test_api.py b/tests/components/nest/test_api.py index 894fda09a8f..7d88ba1d329 100644 --- a/tests/components/nest/test_api.py +++ b/tests/components/nest/test_api.py @@ -11,6 +11,8 @@ The tests below exercise both cases during integration setup. import time from unittest.mock import patch +import pytest + from homeassistant.components.nest import DOMAIN from homeassistant.components.nest.const import API_URL, OAUTH2_TOKEN, SDM_SCOPES from homeassistant.setup import async_setup_component @@ -23,6 +25,7 @@ from .common import ( FAKE_REFRESH_TOKEN, FAKE_TOKEN, PROJECT_ID, + TEST_CONFIGFLOW_YAML_ONLY, create_config_entry, ) @@ -35,6 +38,7 @@ async def async_setup_sdm(hass): await hass.async_block_till_done() +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_YAML_ONLY]) async def test_auth(hass, aioclient_mock): """Exercise authentication library creates valid credentials.""" @@ -84,6 +88,7 @@ async def test_auth(hass, aioclient_mock): assert creds.scopes == SDM_SCOPES +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_YAML_ONLY]) async def test_auth_expired_token(hass, aioclient_mock): """Verify behavior of an expired token.""" diff --git a/tests/components/nest/test_config_flow_legacy.py b/tests/components/nest/test_config_flow_legacy.py index 843c9b582ae..f199d2ec7dd 100644 --- a/tests/components/nest/test_config_flow_legacy.py +++ b/tests/components/nest/test_config_flow_legacy.py @@ -13,15 +13,6 @@ from tests.common import MockConfigEntry CONFIG = TEST_CONFIG_LEGACY.config -async def test_abort_if_no_implementation_registered(hass): - """Test we abort if no implementation is registered.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "missing_configuration" - - async def test_abort_if_single_instance_allowed(hass): """Test we abort if Nest is already setup.""" existing_entry = MockConfigEntry(domain=DOMAIN, data={}) diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow_sdm.py index ff55c1f518d..f4299808bf0 100644 --- a/tests/components/nest/test_config_flow_sdm.py +++ b/tests/components/nest/test_config_flow_sdm.py @@ -1,5 +1,8 @@ """Test the Google Nest Device Access config flow.""" +from __future__ import annotations + +from typing import Any from unittest.mock import patch from google_nest_sdm.exceptions import ( @@ -12,23 +15,31 @@ import pytest from homeassistant import config_entries from homeassistant.components import dhcp +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.components.nest.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN from homeassistant.config_entries import ConfigEntry +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from .common import ( APP_AUTH_DOMAIN, CLIENT_ID, + CLIENT_SECRET, CLOUD_PROJECT_ID, FAKE_TOKEN, PROJECT_ID, SUBSCRIBER_ID, + TEST_CONFIG_APP_CREDS, TEST_CONFIG_HYBRID, TEST_CONFIG_YAML_ONLY, - TEST_CONFIGFLOW_HYBRID, + TEST_CONFIGFLOW_APP_CREDS, TEST_CONFIGFLOW_YAML_ONLY, WEB_AUTH_DOMAIN, MockConfigEntry, + NestTestConfig, ) WEB_REDIRECT_URL = "https://example.com/auth/external/callback" @@ -49,17 +60,35 @@ class OAuthFixture: self.hass_client = hass_client_no_auth self.aioclient_mock = aioclient_mock - async def async_pick_flow(self, result: dict, auth_domain: str) -> dict: - """Invoke flow to puth the auth type to use for this flow.""" - assert result["type"] == "form" - assert result["step_id"] == "pick_implementation" + async def async_app_creds_flow( + self, + result: dict, + cloud_project_id: str = CLOUD_PROJECT_ID, + project_id: str = PROJECT_ID, + ) -> None: + """Invoke multiple steps in the app credentials based flow.""" + assert result.get("type") == "form" + assert result.get("step_id") == "cloud_project" - return await self.async_configure(result, {"implementation": auth_domain}) + result = await self.async_configure( + result, {"cloud_project_id": CLOUD_PROJECT_ID} + ) + assert result.get("type") == "form" + assert result.get("step_id") == "device_project" - async def async_oauth_web_flow(self, result: dict) -> None: + result = await self.async_configure(result, {"project_id": PROJECT_ID}) + await self.async_oauth_web_flow(result) + + async def async_oauth_web_flow(self, result: dict, project_id=PROJECT_ID) -> None: """Invoke the oauth flow for Web Auth with fake responses.""" state = self.create_state(result, WEB_REDIRECT_URL) - assert result["url"] == self.authorize_url(state, WEB_REDIRECT_URL) + assert result["type"] == "external" + assert result["url"] == self.authorize_url( + state, + WEB_REDIRECT_URL, + CLIENT_ID, + project_id, + ) # Simulate user redirect back with auth code client = await self.hass_client() @@ -69,38 +98,26 @@ class OAuthFixture: await self.async_mock_refresh(result) - async def async_oauth_app_flow(self, result: dict) -> None: - """Invoke the oauth flow for Installed Auth with fake responses.""" - # Render form with a link to get an auth token - assert result["type"] == "form" - assert result["step_id"] == "auth" - assert "description_placeholders" in result - assert "url" in result["description_placeholders"] - state = self.create_state(result, APP_REDIRECT_URL) - assert result["description_placeholders"]["url"] == self.authorize_url( - state, APP_REDIRECT_URL - ) - # Simulate user entering auth token in form - await self.async_mock_refresh(result, {"code": "abcd"}) - - async def async_reauth(self, old_data: dict) -> dict: + async def async_reauth(self, config_entry: ConfigEntry) -> dict: """Initiate a reuath flow.""" - result = await self.hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=old_data - ) - assert result["type"] == "form" - assert result["step_id"] == "reauth_confirm" + config_entry.async_start_reauth(self.hass) + await self.hass.async_block_till_done() # Advance through the reauth flow - flows = self.hass.config_entries.flow.async_progress() - assert len(flows) == 1 - assert flows[0]["step_id"] == "reauth_confirm" + result = self.async_progress() + assert result["step_id"] == "reauth_confirm" # Advance to the oauth flow return await self.hass.config_entries.flow.async_configure( - flows[0]["flow_id"], {} + result["flow_id"], {} ) + def async_progress(self) -> FlowResult: + """Return the current step of the config flow.""" + flows = self.hass.config_entries.flow.async_progress() + assert len(flows) == 1 + return flows[0] + def create_state(self, result: dict, redirect_url: str) -> str: """Create state object based on redirect url.""" return config_entry_oauth2_flow._encode_jwt( @@ -111,11 +128,13 @@ class OAuthFixture: }, ) - def authorize_url(self, state: str, redirect_url: str) -> str: + def authorize_url( + self, state: str, redirect_url: str, client_id: str, project_id: str + ) -> str: """Generate the expected authorization url.""" - oauth_authorize = OAUTH2_AUTHORIZE.format(project_id=PROJECT_ID) + oauth_authorize = OAUTH2_AUTHORIZE.format(project_id=project_id) return ( - f"{oauth_authorize}?response_type=code&client_id={CLIENT_ID}" + f"{oauth_authorize}?response_type=code&client_id={client_id}" f"&redirect_uri={redirect_url}" f"&state={state}&scope=https://www.googleapis.com/auth/sdm.service" "+https://www.googleapis.com/auth/pubsub" @@ -146,13 +165,16 @@ class OAuthFixture: await self.hass.async_block_till_done() return self.get_config_entry() - async def async_configure(self, result: dict, user_input: dict) -> dict: + async def async_configure( + self, result: dict[str, Any], user_input: dict[str, Any] + ) -> dict: """Advance to the next step in the config flow.""" return await self.hass.config_entries.flow.async_configure( - result["flow_id"], user_input + result["flow_id"], + user_input, ) - async def async_pubsub_flow(self, result: dict, cloud_project_id="") -> ConfigEntry: + async def async_pubsub_flow(self, result: dict, cloud_project_id="") -> None: """Verify the pubsub creation step.""" # Render form with a link to get an auth token assert result["type"] == "form" @@ -164,7 +186,7 @@ class OAuthFixture: def get_config_entry(self) -> ConfigEntry: """Get the config entry.""" entries = self.hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 + assert len(entries) >= 1 return entries[0] @@ -174,42 +196,209 @@ async def oauth(hass, hass_client_no_auth, aioclient_mock, current_request_with_ return OAuthFixture(hass, hass_client_no_auth, aioclient_mock) -@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_YAML_ONLY]) -async def test_web_full_flow(hass, oauth, setup_platform): +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS]) +async def test_app_credentials(hass, oauth, subscriber, setup_platform): """Check full flow.""" await setup_platform() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + await oauth.async_app_creds_flow(result) - result = await oauth.async_pick_flow(result, WEB_AUTH_DOMAIN) - - await oauth.async_oauth_web_flow(result) entry = await oauth.async_finish_setup(result) - assert entry.title == "OAuth for Web" - assert "token" in entry.data - entry.data["token"].pop("expires_at") - assert entry.unique_id == DOMAIN - assert entry.data["token"] == { - "refresh_token": "mock-refresh-token", - "access_token": "mock-access-token", - "type": "Bearer", - "expires_in": 60, + + data = dict(entry.data) + assert "token" in data + data["token"].pop("expires_in") + data["token"].pop("expires_at") + assert "subscriber_id" in data + assert f"projects/{CLOUD_PROJECT_ID}/subscriptions" in data["subscriber_id"] + data.pop("subscriber_id") + assert data == { + "sdm": {}, + "auth_implementation": "imported-cred", + "cloud_project_id": CLOUD_PROJECT_ID, + "project_id": PROJECT_ID, + "token": { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + }, } - # Subscriber from configuration.yaml - assert "subscriber_id" not in entry.data + + +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS]) +async def test_config_flow_restart(hass, oauth, subscriber, setup_platform): + """Check with auth implementation is re-initialized when aborting the flow.""" + await setup_platform() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await oauth.async_app_creds_flow(result) + + # At this point, we should have a valid auth implementation configured. + # Simulate aborting the flow and starting over to ensure we get prompted + # again to configure everything. + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "form" + assert result.get("step_id") == "cloud_project" + + # Change the values to show they are reflected below + result = await oauth.async_configure( + result, {"cloud_project_id": "new-cloud-project-id"} + ) + assert result.get("type") == "form" + assert result.get("step_id") == "device_project" + + result = await oauth.async_configure(result, {"project_id": "new-project-id"}) + await oauth.async_oauth_web_flow(result, "new-project-id") + + entry = await oauth.async_finish_setup(result, {"code": "1234"}) + + data = dict(entry.data) + assert "token" in data + data["token"].pop("expires_in") + data["token"].pop("expires_at") + assert "subscriber_id" in data + assert "projects/new-cloud-project-id/subscriptions" in data["subscriber_id"] + data.pop("subscriber_id") + assert data == { + "sdm": {}, + "auth_implementation": "imported-cred", + "cloud_project_id": "new-cloud-project-id", + "project_id": "new-project-id", + "token": { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + }, + } + + +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS]) +async def test_config_flow_wrong_project_id(hass, oauth, subscriber, setup_platform): + """Check the case where the wrong project ids are entered.""" + await setup_platform() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "form" + assert result.get("step_id") == "cloud_project" + + result = await oauth.async_configure(result, {"cloud_project_id": CLOUD_PROJECT_ID}) + assert result.get("type") == "form" + assert result.get("step_id") == "device_project" + + # Enter the cloud project id instead of device access project id (really we just check + # they are the same value which is never correct) + result = await oauth.async_configure(result, {"project_id": CLOUD_PROJECT_ID}) + assert result["type"] == "form" + assert "errors" in result + assert "project_id" in result["errors"] + assert result["errors"]["project_id"] == "wrong_project_id" + + # Fix with a correct value and complete the rest of the flow + result = await oauth.async_configure(result, {"project_id": PROJECT_ID}) + await oauth.async_oauth_web_flow(result) + await hass.async_block_till_done() + + entry = await oauth.async_finish_setup(result, {"code": "1234"}) + + data = dict(entry.data) + assert "token" in data + data["token"].pop("expires_in") + data["token"].pop("expires_at") + assert "subscriber_id" in data + assert f"projects/{CLOUD_PROJECT_ID}/subscriptions" in data["subscriber_id"] + data.pop("subscriber_id") + assert data == { + "sdm": {}, + "auth_implementation": "imported-cred", + "cloud_project_id": CLOUD_PROJECT_ID, + "project_id": PROJECT_ID, + "token": { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + }, + } + + +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS]) +async def test_config_flow_pubsub_configuration_error( + hass, + oauth, + setup_platform, + mock_subscriber, +): + """Check full flow fails with configuration error.""" + await setup_platform() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await oauth.async_app_creds_flow(result) + + mock_subscriber.create_subscription.side_effect = ConfigurationException + result = await oauth.async_configure(result, {"code": "1234"}) + assert result["type"] == "form" + assert "errors" in result + assert "cloud_project_id" in result["errors"] + assert result["errors"]["cloud_project_id"] == "bad_project_id" + + +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS]) +async def test_config_flow_pubsub_subscriber_error( + hass, oauth, setup_platform, mock_subscriber +): + """Check full flow with a subscriber error.""" + await setup_platform() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await oauth.async_app_creds_flow(result) + + mock_subscriber.create_subscription.side_effect = SubscriberException() + result = await oauth.async_configure(result, {"code": "1234"}) + + assert result["type"] == "form" + assert "errors" in result + assert "cloud_project_id" in result["errors"] + assert result["errors"]["cloud_project_id"] == "subscriber_error" + + +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_YAML_ONLY]) +async def test_config_yaml_ignored(hass, oauth, setup_platform): + """Check full flow.""" + await setup_platform() + + 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"] == "create_cloud_project" + + result = await oauth.async_configure(result, {}) + assert result.get("type") == "abort" + assert result.get("reason") == "missing_credentials" @pytest.mark.parametrize("nest_test_config", [TEST_CONFIG_YAML_ONLY]) async def test_web_reauth(hass, oauth, setup_platform, config_entry): """Test Nest reauthentication.""" - await setup_platform() assert config_entry.data["token"].get("access_token") == FAKE_TOKEN - result = await oauth.async_reauth(config_entry.data) + orig_subscriber_id = config_entry.data.get("subscriber_id") + result = await oauth.async_reauth(config_entry) await oauth.async_oauth_web_flow(result) entry = await oauth.async_finish_setup(result) @@ -223,7 +412,7 @@ async def test_web_reauth(hass, oauth, setup_platform, config_entry): "expires_in": 60, } assert entry.data["auth_implementation"] == WEB_AUTH_DOMAIN - assert "subscriber_id" not in entry.data # not updated + assert entry.data.get("subscriber_id") == orig_subscriber_id # Not updated async def test_single_config_entry(hass, setup_platform): @@ -237,7 +426,9 @@ async def test_single_config_entry(hass, setup_platform): assert result["reason"] == "single_instance_allowed" -async def test_unexpected_existing_config_entries(hass, oauth, setup_platform): +async def test_unexpected_existing_config_entries( + hass, oauth, setup_platform, config_entry +): """Test Nest reauthentication with multiple existing config entries.""" # Note that this case will not happen in the future since only a single # instance is now allowed, but this may have been allowed in the past. @@ -246,23 +437,29 @@ async def test_unexpected_existing_config_entries(hass, oauth, setup_platform): await setup_platform() old_entry = MockConfigEntry( - domain=DOMAIN, data={"auth_implementation": WEB_AUTH_DOMAIN, "sdm": {}} + domain=DOMAIN, + data={ + **config_entry.data, + "extra_data": True, + }, ) old_entry.add_to_hass(hass) entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 2 + orig_subscriber_id = config_entry.data.get("subscriber_id") + # Invoke the reauth flow - result = await oauth.async_reauth(old_entry.data) + result = await oauth.async_reauth(config_entry) await oauth.async_oauth_web_flow(result) await oauth.async_finish_setup(result) - # Only a single entry now exists, and the other was cleaned up + # Only reauth entry was updated, the other entry is preserved entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 + assert len(entries) == 2 entry = entries[0] assert entry.unique_id == DOMAIN entry.data["token"].pop("expires_at") @@ -272,7 +469,14 @@ async def test_unexpected_existing_config_entries(hass, oauth, setup_platform): "type": "Bearer", "expires_in": 60, } - assert "subscriber_id" not in entry.data # not updated + assert entry.data.get("subscriber_id") == orig_subscriber_id # Not updated + assert not entry.data.get("extra_data") + + # Other entry was not refreshed + entry = entries[1] + entry.data["token"].pop("expires_at") + assert entry.data.get("token", {}).get("access_token") == "some-token" + assert entry.data.get("extra_data") async def test_reauth_missing_config_entry(hass, setup_platform): @@ -287,42 +491,51 @@ async def test_reauth_missing_config_entry(hass, setup_platform): assert result["reason"] == "missing_configuration" -@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_YAML_ONLY]) -async def test_app_full_flow(hass, oauth, setup_platform): - """Check full flow.""" - await setup_platform() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) - - await oauth.async_oauth_app_flow(result) - entry = await oauth.async_finish_setup(result, {"code": "1234"}) - assert entry.title == "OAuth for Apps" - assert "token" in entry.data - entry.data["token"].pop("expires_at") - assert entry.unique_id == DOMAIN - assert entry.data["token"] == { - "refresh_token": "mock-refresh-token", - "access_token": "mock-access-token", - "type": "Bearer", - "expires_in": 60, - } - # Subscriber from configuration.yaml - assert "subscriber_id" not in entry.data - - @pytest.mark.parametrize( - "nest_test_config,auth_implementation", [(TEST_CONFIG_YAML_ONLY, APP_AUTH_DOMAIN)] + "nest_test_config,auth_implementation", [(TEST_CONFIG_HYBRID, APP_AUTH_DOMAIN)] ) -async def test_app_reauth(hass, oauth, setup_platform, config_entry): - """Test Nest reauthentication for Installed App Auth.""" +async def test_app_auth_yaml_reauth(hass, oauth, setup_platform, config_entry): + """Test reauth for deprecated app auth credentails upgrade instructions.""" await setup_platform() - result = await oauth.async_reauth(config_entry.data) - await oauth.async_oauth_app_flow(result) + orig_subscriber_id = config_entry.data.get("subscriber_id") + assert config_entry.data["auth_implementation"] == APP_AUTH_DOMAIN + + result = oauth.async_progress() + assert result.get("step_id") == "reauth_confirm" + + result = await oauth.async_configure(result, {}) + assert result.get("type") == "form" + assert result.get("step_id") == "auth_upgrade" + + result = await oauth.async_configure(result, {}) + assert result.get("type") == "abort" + assert result.get("reason") == "missing_credentials" + await hass.async_block_till_done() + # Config flow is aborted, but new one created back in re-auth state waiting for user + # to create application credentials + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + # Emulate user entering credentials (different from configuration.yaml creds) + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential(CLIENT_ID, CLIENT_SECRET), + ) + + # Config flow is placed back into a reuath state + result = oauth.async_progress() + assert result.get("step_id") == "reauth_confirm" + + result = await oauth.async_configure(result, {}) + assert result.get("type") == "form" + assert result.get("step_id") == "device_project_upgrade" + + # Frontend sends user back through the config flow again + result = await oauth.async_configure(result, {}) + await oauth.async_oauth_web_flow(result) # Verify existing tokens are replaced entry = await oauth.async_finish_setup(result, {"code": "1234"}) @@ -334,29 +547,28 @@ async def test_app_reauth(hass, oauth, setup_platform, config_entry): "type": "Bearer", "expires_in": 60, } - assert entry.data["auth_implementation"] == APP_AUTH_DOMAIN - assert "subscriber_id" not in entry.data # not updated + assert entry.data["auth_implementation"] == DOMAIN + assert entry.data.get("subscriber_id") == orig_subscriber_id # Not updated + + # Existing entry is updated + assert config_entry.data["auth_implementation"] == DOMAIN -@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) -async def test_pubsub_subscription(hass, oauth, subscriber, setup_platform): - """Check flow that creates a pub/sub subscription.""" +@pytest.mark.parametrize( + "nest_test_config,auth_implementation", [(TEST_CONFIG_YAML_ONLY, WEB_AUTH_DOMAIN)] +) +async def test_web_auth_yaml_reauth(hass, oauth, setup_platform, config_entry): + """Test Nest reauthentication for Installed App Auth.""" + await setup_platform() - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) - await oauth.async_oauth_app_flow(result) + orig_subscriber_id = config_entry.data.get("subscriber_id") - result = await oauth.async_configure(result, {"code": "1234"}) - await oauth.async_pubsub_flow(result) - entry = await oauth.async_finish_setup( - result, {"cloud_project_id": CLOUD_PROJECT_ID} - ) + result = await oauth.async_reauth(config_entry) + await oauth.async_oauth_web_flow(result) - assert entry.title == "OAuth for Apps" - assert "token" in entry.data + # Verify existing tokens are replaced + entry = await oauth.async_finish_setup(result, {"code": "1234"}) entry.data["token"].pop("expires_at") assert entry.unique_id == DOMAIN assert entry.data["token"] == { @@ -365,11 +577,11 @@ async def test_pubsub_subscription(hass, oauth, subscriber, setup_platform): "type": "Bearer", "expires_in": 60, } - assert "subscriber_id" in entry.data - assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID + assert entry.data["auth_implementation"] == WEB_AUTH_DOMAIN + assert entry.data.get("subscriber_id") == orig_subscriber_id # Not updated -@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS]) async def test_pubsub_subscription_strip_whitespace( hass, oauth, subscriber, setup_platform ): @@ -379,16 +591,12 @@ async def test_pubsub_subscription_strip_whitespace( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) - await oauth.async_oauth_app_flow(result) - - result = await oauth.async_configure(result, {"code": "1234"}) - await oauth.async_pubsub_flow(result) - entry = await oauth.async_finish_setup( - result, {"cloud_project_id": " " + CLOUD_PROJECT_ID + " "} + await oauth.async_app_creds_flow( + result, cloud_project_id=" " + CLOUD_PROJECT_ID + " " ) + entry = await oauth.async_finish_setup(result, {"code": "1234"}) - assert entry.title == "OAuth for Apps" + assert entry.title == "Import from configuration.yaml" assert "token" in entry.data entry.data["token"].pop("expires_at") assert entry.unique_id == DOMAIN @@ -402,7 +610,7 @@ async def test_pubsub_subscription_strip_whitespace( assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID -@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS]) async def test_pubsub_subscription_auth_failure( hass, oauth, setup_platform, mock_subscriber ): @@ -412,102 +620,25 @@ async def test_pubsub_subscription_auth_failure( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) - await oauth.async_oauth_app_flow(result) - result = await oauth.async_configure(result, {"code": "1234"}) mock_subscriber.create_subscription.side_effect = AuthException() - await oauth.async_pubsub_flow(result) - result = await oauth.async_configure(result, {"cloud_project_id": CLOUD_PROJECT_ID}) + await oauth.async_app_creds_flow(result) + result = await oauth.async_configure(result, {"code": "1234"}) assert result["type"] == "abort" assert result["reason"] == "invalid_access_token" -@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) -async def test_pubsub_subscription_failure( - hass, oauth, setup_platform, mock_subscriber -): - """Check flow that creates a pub/sub subscription.""" - await setup_platform() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) - await oauth.async_oauth_app_flow(result) - result = await oauth.async_configure(result, {"code": "1234"}) - await oauth.async_pubsub_flow(result) - - mock_subscriber.create_subscription.side_effect = SubscriberException() - - result = await oauth.async_configure(result, {"cloud_project_id": CLOUD_PROJECT_ID}) - - assert result["type"] == "form" - assert "errors" in result - assert "cloud_project_id" in result["errors"] - assert result["errors"]["cloud_project_id"] == "subscriber_error" - - -@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) -async def test_pubsub_subscription_configuration_failure( - hass, oauth, setup_platform, mock_subscriber -): - """Check flow that creates a pub/sub subscription.""" - await setup_platform() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) - await oauth.async_oauth_app_flow(result) - result = await oauth.async_configure(result, {"code": "1234"}) - await oauth.async_pubsub_flow(result) - - mock_subscriber.create_subscription.side_effect = ConfigurationException() - result = await oauth.async_configure(result, {"cloud_project_id": CLOUD_PROJECT_ID}) - - assert result["type"] == "form" - assert "errors" in result - assert "cloud_project_id" in result["errors"] - assert result["errors"]["cloud_project_id"] == "bad_project_id" - - -@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) -async def test_pubsub_with_wrong_project_id(hass, oauth, setup_platform): - """Test a possible common misconfiguration mixing up project ids.""" - await setup_platform() - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) - await oauth.async_oauth_app_flow(result) - result = await oauth.async_configure(result, {"code": "1234"}) - await oauth.async_pubsub_flow(result) - result = await oauth.async_configure( - result, {"cloud_project_id": PROJECT_ID} # SDM project id - ) - await hass.async_block_till_done() - - assert result["type"] == "form" - assert "errors" in result - assert "cloud_project_id" in result["errors"] - assert result["errors"]["cloud_project_id"] == "wrong_project_id" - - -@pytest.mark.parametrize( - "nest_test_config,auth_implementation", [(TEST_CONFIG_HYBRID, APP_AUTH_DOMAIN)] -) +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIG_APP_CREDS]) async def test_pubsub_subscriber_config_entry_reauth( - hass, oauth, setup_platform, subscriber, config_entry + hass, oauth, setup_platform, subscriber, config_entry, auth_implementation ): """Test the pubsub subscriber id is preserved during reauth.""" await setup_platform() - result = await oauth.async_reauth(config_entry.data) - await oauth.async_oauth_app_flow(result) + result = await oauth.async_reauth(config_entry) + await oauth.async_oauth_web_flow(result) # Entering an updated access token refreshs the config entry. entry = await oauth.async_finish_setup(result, {"code": "1234"}) @@ -519,12 +650,12 @@ async def test_pubsub_subscriber_config_entry_reauth( "type": "Bearer", "expires_in": 60, } - assert entry.data["auth_implementation"] == APP_AUTH_DOMAIN + assert entry.data["auth_implementation"] == auth_implementation assert entry.data["subscriber_id"] == SUBSCRIBER_ID assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID -@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS]) async def test_config_entry_title_from_home(hass, oauth, setup_platform, subscriber): """Test that the Google Home name is used for the config entry title.""" @@ -547,22 +678,16 @@ async def test_config_entry_title_from_home(hass, oauth, setup_platform, subscri result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) - await oauth.async_oauth_app_flow(result) - - result = await oauth.async_configure(result, {"code": "1234"}) - await oauth.async_pubsub_flow(result) - entry = await oauth.async_finish_setup( - result, {"cloud_project_id": CLOUD_PROJECT_ID} - ) + await oauth.async_app_creds_flow(result) + entry = await oauth.async_finish_setup(result, {"code": "1234"}) assert entry.title == "Example Home" assert "token" in entry.data assert "subscriber_id" in entry.data assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID -@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS]) async def test_config_entry_title_multiple_homes( hass, oauth, setup_platform, subscriber ): @@ -599,18 +724,13 @@ async def test_config_entry_title_multiple_homes( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) - await oauth.async_oauth_app_flow(result) + await oauth.async_app_creds_flow(result) - result = await oauth.async_configure(result, {"code": "1234"}) - await oauth.async_pubsub_flow(result) - entry = await oauth.async_finish_setup( - result, {"cloud_project_id": CLOUD_PROJECT_ID} - ) + entry = await oauth.async_finish_setup(result, {"code": "1234"}) assert entry.title == "Example Home #1, Example Home #2" -@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS]) async def test_title_failure_fallback(hass, oauth, setup_platform, mock_subscriber): """Test exception handling when determining the structure names.""" await setup_platform() @@ -618,24 +738,17 @@ async def test_title_failure_fallback(hass, oauth, setup_platform, mock_subscrib result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) - await oauth.async_oauth_app_flow(result) + await oauth.async_app_creds_flow(result) mock_subscriber.async_get_device_manager.side_effect = AuthException() - - result = await oauth.async_configure(result, {"code": "1234"}) - await oauth.async_pubsub_flow(result) - entry = await oauth.async_finish_setup( - result, {"cloud_project_id": CLOUD_PROJECT_ID} - ) - - assert entry.title == "OAuth for Apps" + entry = await oauth.async_finish_setup(result, {"code": "1234"}) + assert entry.title == "Import from configuration.yaml" assert "token" in entry.data assert "subscriber_id" in entry.data assert entry.data["cloud_project_id"] == CLOUD_PROJECT_ID -@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_HYBRID]) +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS]) async def test_structure_missing_trait(hass, oauth, setup_platform, subscriber): """Test handling the case where a structure has no name set.""" @@ -655,34 +768,33 @@ async def test_structure_missing_trait(hass, oauth, setup_platform, subscriber): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - result = await oauth.async_pick_flow(result, APP_AUTH_DOMAIN) - await oauth.async_oauth_app_flow(result) - - result = await oauth.async_configure(result, {"code": "1234"}) - await oauth.async_pubsub_flow(result) - entry = await oauth.async_finish_setup( - result, {"cloud_project_id": CLOUD_PROJECT_ID} - ) + await oauth.async_app_creds_flow(result) + entry = await oauth.async_finish_setup(result, {"code": "1234"}) # Fallback to default name - assert entry.title == "OAuth for Apps" + assert entry.title == "Import from configuration.yaml" -async def test_dhcp_discovery_without_config(hass, oauth): - """Exercise discovery dhcp with no config present (can't run).""" +@pytest.mark.parametrize("nest_test_config", [NestTestConfig()]) +async def test_dhcp_discovery(hass, oauth, subscriber): + """Exercise discovery dhcp starts the config flow and kicks user to frontend creds flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=FAKE_DHCP_DATA, ) await hass.async_block_till_done() - assert result["type"] == "abort" - assert result["reason"] == "missing_configuration" + assert result["type"] == "form" + assert result["step_id"] == "create_cloud_project" + + result = await oauth.async_configure(result, {}) + assert result.get("type") == "abort" + assert result.get("reason") == "missing_credentials" -@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_YAML_ONLY]) -async def test_dhcp_discovery(hass, oauth, setup_platform): - """Discover via dhcp when config is present.""" +@pytest.mark.parametrize("nest_test_config", [TEST_CONFIGFLOW_APP_CREDS]) +async def test_dhcp_discovery_with_creds(hass, oauth, subscriber, setup_platform): + """Exercise discovery dhcp with no config present (can't run).""" await setup_platform() result = await hass.config_entries.flow.async_init( @@ -691,19 +803,33 @@ async def test_dhcp_discovery(hass, oauth, setup_platform): data=FAKE_DHCP_DATA, ) await hass.async_block_till_done() + assert result.get("type") == "form" + assert result.get("step_id") == "cloud_project" - # DHCP discovery invokes the config flow - result = await oauth.async_pick_flow(result, WEB_AUTH_DOMAIN) + result = await oauth.async_configure(result, {"cloud_project_id": CLOUD_PROJECT_ID}) + assert result.get("type") == "form" + assert result.get("step_id") == "device_project" + + result = await oauth.async_configure(result, {"project_id": PROJECT_ID}) await oauth.async_oauth_web_flow(result) - entry = await oauth.async_finish_setup(result) - assert entry.title == "OAuth for Web" - - # Discovery does not run once configured - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, - data=FAKE_DHCP_DATA, - ) + entry = await oauth.async_finish_setup(result, {"code": "1234"}) await hass.async_block_till_done() - assert result["type"] == "abort" - assert result["reason"] == "already_configured" + + data = dict(entry.data) + assert "token" in data + data["token"].pop("expires_in") + data["token"].pop("expires_at") + assert "subscriber_id" in data + assert f"projects/{CLOUD_PROJECT_ID}/subscriptions" in data["subscriber_id"] + data.pop("subscriber_id") + assert data == { + "sdm": {}, + "auth_implementation": "imported-cred", + "cloud_project_id": CLOUD_PROJECT_ID, + "project_id": PROJECT_ID, + "token": { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + }, + } diff --git a/tests/components/nest/test_diagnostics.py b/tests/components/nest/test_diagnostics.py index 8e28222e356..85b63b23301 100644 --- a/tests/components/nest/test_diagnostics.py +++ b/tests/components/nest/test_diagnostics.py @@ -127,8 +127,6 @@ async def test_setup_susbcriber_failure( ): """Test configuration error.""" with patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" - ), patch( "homeassistant.components.nest.api.GoogleNestSubscriber.start_async", side_effect=SubscriberException(), ): diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index 28550bd57b6..83845586764 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -443,9 +443,7 @@ async def test_structure_update_event(hass, subscriber, setup_platform): }, auth=None, ) - with patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" - ), patch("homeassistant.components.nest.PLATFORMS", [PLATFORM]), patch( + with patch("homeassistant.components.nest.PLATFORMS", [PLATFORM]), patch( "homeassistant.components.nest.api.GoogleNestSubscriber", return_value=subscriber, ): diff --git a/tests/components/nest/test_init_sdm.py b/tests/components/nest/test_init_sdm.py index 381252c6f75..1b473ccd62f 100644 --- a/tests/components/nest/test_init_sdm.py +++ b/tests/components/nest/test_init_sdm.py @@ -25,10 +25,13 @@ from homeassistant.components.nest import DOMAIN from homeassistant.config_entries import ConfigEntryState from .common import ( + PROJECT_ID, + SUBSCRIBER_ID, + TEST_CONFIG_APP_CREDS, TEST_CONFIG_HYBRID, TEST_CONFIG_YAML_ONLY, + TEST_CONFIGFLOW_APP_CREDS, FakeSubscriber, - NestTestConfig, YieldFixture, ) @@ -170,7 +173,8 @@ async def test_subscriber_configuration_failure( @pytest.mark.parametrize( - "nest_test_config", [NestTestConfig(config={}, config_entry_data=None)] + "nest_test_config", + [TEST_CONFIGFLOW_APP_CREDS], ) async def test_empty_config(hass, error_caplog, config, setup_platform): """Test setup is a no-op with not config.""" @@ -205,8 +209,12 @@ async def test_unload_entry(hass, setup_platform): TEST_CONFIG_HYBRID, True, ), # Integration created subscriber, garbage collect on remove + ( + TEST_CONFIG_APP_CREDS, + True, + ), # Integration created subscriber, garbage collect on remove ], - ids=["yaml-config-only", "hybrid-config"], + ids=["yaml-config-only", "hybrid-config", "config-entry"], ) async def test_remove_entry(hass, nest_test_config, setup_base_platform, delete_called): """Test successful unload of a ConfigEntry.""" @@ -220,6 +228,9 @@ async def test_remove_entry(hass, nest_test_config, setup_base_platform, delete_ assert len(entries) == 1 entry = entries[0] assert entry.state is ConfigEntryState.LOADED + # Assert entry was imported if from configuration.yaml + assert entry.data.get("subscriber_id") == SUBSCRIBER_ID + assert entry.data.get("project_id") == PROJECT_ID with patch( "homeassistant.components.nest.api.GoogleNestSubscriber.subscriber_id" @@ -234,7 +245,9 @@ async def test_remove_entry(hass, nest_test_config, setup_base_platform, delete_ @pytest.mark.parametrize( - "nest_test_config", [TEST_CONFIG_HYBRID], ids=["hyrbid-config"] + "nest_test_config", + [TEST_CONFIG_HYBRID, TEST_CONFIG_APP_CREDS], + ids=["hyrbid-config", "app-creds"], ) async def test_remove_entry_delete_subscriber_failure( hass, nest_test_config, setup_base_platform From 7a82794ad727e6782638c51e61fa40f26229c5b8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 15 Jun 2022 17:06:44 +0200 Subject: [PATCH 1521/3516] Migrate template NumberEntity to native_value (#73537) --- homeassistant/components/number/__init__.py | 4 +-- homeassistant/components/template/number.py | 38 ++++++++++----------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 5a0bf9947f3..13ba47e87dc 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -252,8 +252,6 @@ class NumberEntity(Entity): @property def native_step(self) -> float | None: """Return the increment/decrement step.""" - if hasattr(self, "_attr_native_step"): - return self._attr_native_step if ( hasattr(self, "entity_description") and self.entity_description.native_step is not None @@ -273,6 +271,8 @@ class NumberEntity(Entity): ): self._report_deprecated_number_entity() return self.entity_description.step + if hasattr(self, "_attr_native_step"): + return self._attr_native_step if (native_step := self.native_step) is not None: return native_step step = DEFAULT_STEP diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index bf3dcbb120b..d41bdee597b 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -119,45 +119,45 @@ class TemplateNumber(TemplateEntity, NumberEntity): self._min_value_template = config[ATTR_MIN] self._max_value_template = config[ATTR_MAX] self._attr_assumed_state = self._optimistic = config[CONF_OPTIMISTIC] - self._attr_value = None - self._attr_step = None - self._attr_min_value = None - self._attr_max_value = None + self._attr_native_value = None + self._attr_native_step = None + self._attr_native_min_value = None + self._attr_native_max_value = None async def async_added_to_hass(self) -> None: """Register callbacks.""" self.add_template_attribute( - "_attr_value", + "_attr_native_value", self._value_template, validator=vol.Coerce(float), none_on_template_error=True, ) self.add_template_attribute( - "_attr_step", + "_attr_native_step", self._step_template, validator=vol.Coerce(float), none_on_template_error=True, ) if self._min_value_template is not None: self.add_template_attribute( - "_attr_min_value", + "_attr_native_min_value", self._min_value_template, validator=vol.Coerce(float), none_on_template_error=True, ) if self._max_value_template is not None: self.add_template_attribute( - "_attr_max_value", + "_attr_native_max_value", self._max_value_template, validator=vol.Coerce(float), none_on_template_error=True, ) await super().async_added_to_hass() - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set value of the number.""" if self._optimistic: - self._attr_value = value + self._attr_native_value = value self.async_write_ha_state() await self.async_run_script( self._command_set_value, @@ -193,35 +193,35 @@ class TriggerNumberEntity(TriggerEntity, NumberEntity): ) @property - def value(self) -> float | None: + def native_value(self) -> float | None: """Return the currently selected option.""" return vol.Any(vol.Coerce(float), None)(self._rendered.get(CONF_STATE)) @property - def min_value(self) -> int: + def native_min_value(self) -> int: """Return the minimum value.""" return vol.Any(vol.Coerce(float), None)( - self._rendered.get(ATTR_MIN, super().min_value) + self._rendered.get(ATTR_MIN, super().native_min_value) ) @property - def max_value(self) -> int: + def native_max_value(self) -> int: """Return the maximum value.""" return vol.Any(vol.Coerce(float), None)( - self._rendered.get(ATTR_MAX, super().max_value) + self._rendered.get(ATTR_MAX, super().native_max_value) ) @property - def step(self) -> int: + def native_step(self) -> int: """Return the increment/decrement step.""" return vol.Any(vol.Coerce(float), None)( - self._rendered.get(ATTR_STEP, super().step) + self._rendered.get(ATTR_STEP, super().native_step) ) - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set value of the number.""" if self._config[CONF_OPTIMISTIC]: - self._attr_value = value + self._attr_native_value = value self.async_write_ha_state() await self._command_set_value.async_run( {ATTR_VALUE: value}, context=self._context From 0ace5af9143e9e9a279419ec8a469123e49eca45 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 15 Jun 2022 23:07:18 +0200 Subject: [PATCH 1522/3516] Correct migration of unifiprotect number (#73553) --- homeassistant/components/unifiprotect/number.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index 3ac5b673ea5..1cfb4477673 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -108,7 +108,7 @@ LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( name="Auto-shutoff Duration", icon="mdi:camera-timer", entity_category=EntityCategory.CONFIG, - unit_of_measurement=TIME_SECONDS, + native_unit_of_measurement=TIME_SECONDS, ufp_min=15, ufp_max=900, ufp_step=15, @@ -139,7 +139,7 @@ DOORLOCK_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( name="Auto-lock Timeout", icon="mdi:walk", entity_category=EntityCategory.CONFIG, - unit_of_measurement=TIME_SECONDS, + native_unit_of_measurement=TIME_SECONDS, ufp_min=0, ufp_max=3600, ufp_step=15, From b4359c7721e0df74263ffa591eeb64cc50a83f45 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 16 Jun 2022 00:22:45 +0000 Subject: [PATCH 1523/3516] [ci skip] Translation update --- .../binary_sensor/translations/ca.json | 2 +- .../eight_sleep/translations/id.json | 19 ++++++++++++ .../components/google/translations/id.json | 3 ++ .../components/nest/translations/ca.json | 9 ++++++ .../components/nest/translations/de.json | 29 +++++++++++++++++ .../components/nest/translations/en.json | 6 ++-- .../components/nest/translations/pt-BR.json | 31 ++++++++++++++++++- .../components/nest/translations/zh-Hant.json | 31 ++++++++++++++++++- 8 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/eight_sleep/translations/id.json diff --git a/homeassistant/components/binary_sensor/translations/ca.json b/homeassistant/components/binary_sensor/translations/ca.json index 10d705e797b..a65633140a0 100644 --- a/homeassistant/components/binary_sensor/translations/ca.json +++ b/homeassistant/components/binary_sensor/translations/ca.json @@ -81,7 +81,7 @@ "not_moist": "{entity_name} es torna sec", "not_moving": "{entity_name} ha parat de moure's", "not_occupied": "{entity_name} es desocupa", - "not_opened": "{entity_name} es tanca", + "not_opened": "{entity_name} tancat/tancada", "not_plugged_in": "{entity_name} desendollat", "not_powered": "{entity_name} no est\u00e0 alimentat", "not_present": "{entity_name} no est\u00e0 present", diff --git a/homeassistant/components/eight_sleep/translations/id.json b/homeassistant/components/eight_sleep/translations/id.json new file mode 100644 index 00000000000..4bc4e32e7ee --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "cannot_connect": "Tidak dapat terhubung ke cloud Eight Sleep: {error}" + }, + "error": { + "cannot_connect": "Tidak dapat terhubung ke cloud Eight Sleep: {error}" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/id.json b/homeassistant/components/google/translations/id.json index 25c3e9fa1e6..085d49e92a3 100644 --- a/homeassistant/components/google/translations/id.json +++ b/homeassistant/components/google/translations/id.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "Ikuti [petunjuk]({more_info_url}) untuk [Layar persetujuan OAuth]({oauth_consent_url}) untuk memberi Home Assistant akses ke Google Kalender Anda. Anda juga perlu membuat Kredensial Aplikasi yang ditautkan ke Kalender Anda:\n1. Buka [Kredensial]({oauth_creds_url}) dan klik **Buat Kredensial**.\n1. Dari daftar pilihan pilih **ID klien OAuth **.\n1. Pilih **TV dan Perangkat Input Terbatas** untuk Jenis Aplikasi.\n\n" + }, "config": { "abort": { "already_configured": "Akun sudah dikonfigurasi", diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index 888b7ed5b44..c8d6238e742 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -29,6 +29,15 @@ "description": "Per enlla\u00e7ar un compte de Google, [autoritza el compte]({url}). \n\nDespr\u00e9s de l'autoritzaci\u00f3, copia i enganxa a continuaci\u00f3 el codi 'token' d'autenticaci\u00f3 proporcionat.", "title": "Vinculaci\u00f3 amb compte de Google" }, + "cloud_project": { + "data": { + "cloud_project_id": "ID de projecte Google Cloud" + }, + "title": "Nest: introdueix l'identificador del projecte Cloud" + }, + "create_cloud_project": { + "title": "Nest: crea i configura el projecte Cloud" + }, "init": { "data": { "flow_impl": "Prove\u00efdor" diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index 47d0505dca5..bee1bb547f2 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "Folgen Sie den [Anweisungen]({more_info_url}), um die Cloud-Konsole zu konfigurieren:\n\n1. Gehen Sie zum [OAuth-Zustimmungsbildschirm]({oauth_consent_url}) und konfigurieren Sie\n1. Gehen Sie zu [Credentials]({oauth_creds_url}) und klicken Sie auf **Create Credentials**.\n1. W\u00e4hlen Sie in der Dropdown-Liste **OAuth-Client-ID**.\n1. W\u00e4hlen Sie **Webanwendung** f\u00fcr den Anwendungstyp.\n1. F\u00fcgen Sie `{redirect_url}` unter *Authorized redirect URI* hinzu." + }, "config": { "abort": { "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", @@ -29,6 +32,32 @@ "description": "Um dein Google-Konto zu verkn\u00fcpfen, w\u00e4hle [Konto autorisieren]({url}).\n\nKopiere nach der Autorisierung den unten angegebenen Authentifizierungstoken-Code.", "title": "Google-Konto verkn\u00fcpfen" }, + "auth_upgrade": { + "description": "App Auth wurde von Google abgeschafft, um die Sicherheit zu verbessern, und Sie m\u00fcssen Ma\u00dfnahmen ergreifen, indem Sie neue Anmeldedaten f\u00fcr die Anwendung erstellen.\n\n\u00d6ffnen Sie die [Dokumentation]({more_info_url}) und folgen Sie den n\u00e4chsten Schritten, die Sie durchf\u00fchren m\u00fcssen, um den Zugriff auf Ihre Nest-Ger\u00e4te wiederherzustellen.", + "title": "Nest: Einstellung der App-Authentifizierung" + }, + "cloud_project": { + "data": { + "cloud_project_id": "Google Cloud Projekt-ID" + }, + "description": "Geben Sie unten die Cloud-Projekt-ID ein, z. B. *example-project-12345*. Siehe die [Google Cloud Console]({cloud_console_url}) oder die Dokumentation f\u00fcr [weitere Informationen]({more_info_url}).", + "title": "Nest: Cloud-Projekt-ID eingeben" + }, + "create_cloud_project": { + "description": "Die Nest-Integration erm\u00f6glicht es Ihnen, Ihre Nest-Thermostate, -Kameras und -T\u00fcrklingeln \u00fcber die Smart Device Management API zu integrieren. Die SDM API **erfordert eine einmalige Einrichtungsgeb\u00fchr von US $5**. Siehe Dokumentation f\u00fcr [weitere Informationen]({more_info_url}).\n\n1. Rufen Sie die [Google Cloud Console]({cloud_console_url}) auf.\n1. Wenn dies Ihr erstes Projekt ist, klicken Sie auf **Projekt erstellen** und dann auf **Neues Projekt**.\n1. Geben Sie Ihrem Cloud-Projekt einen Namen und klicken Sie dann auf **Erstellen**.\n1. Speichern Sie die Cloud Project ID, z. B. *example-project-12345*, da Sie diese sp\u00e4ter ben\u00f6tigen.\n1. Gehen Sie zur API-Bibliothek f\u00fcr [Smart Device Management API]({sdm_api_url}) und klicken Sie auf **Aktivieren**.\n1. Wechseln Sie zur API-Bibliothek f\u00fcr [Cloud Pub/Sub API]({pubsub_api_url}) und klicken Sie auf **Aktivieren**.\n\nFahren Sie fort, wenn Ihr Cloud-Projekt eingerichtet ist.", + "title": "Nest: Cloud-Projekt erstellen und konfigurieren" + }, + "device_project": { + "data": { + "project_id": "Ger\u00e4tezugriffsprojekt ID" + }, + "description": "Erstellen Sie ein Nest Ger\u00e4tezugriffsprojekt, f\u00fcr dessen Einrichtung **eine Geb\u00fchr von 5 US-Dollar** anf\u00e4llt.\n1. Gehen Sie zur [Device Access Console]({device_access_console_url}) und durchlaufen Sie den Zahlungsablauf.\n1. Klicken Sie auf **Projekt erstellen**.\n1. Geben Sie Ihrem Device Access-Projekt einen Namen und klicken Sie auf **Weiter**.\n1. Geben Sie Ihre OAuth-Client-ID ein\n1. Aktivieren Sie Ereignisse, indem Sie auf **Aktivieren** und **Projekt erstellen** klicken.\n\nGeben Sie unten Ihre Ger\u00e4tezugriffsprojekt ID ein ([more info]({more_info_url})).\n", + "title": "Nest: Erstelle ein Ger\u00e4tezugriffsprojekt" + }, + "device_project_upgrade": { + "description": "Aktualisieren Sie das Nest Ger\u00e4tezugriffsprojekt mit Ihrer neuen OAuth Client ID ([more info]({more_info_url}))\n1. Gehen Sie zur [Ger\u00e4tezugriffskonsole]({device_access_console_url}).\n1. Klicken Sie auf das Papierkorbsymbol neben *OAuth Client ID*.\n1. Klicken Sie auf das \u00dcberlaufmen\u00fc und *Client ID hinzuf\u00fcgen*.\n1. Geben Sie Ihre neue OAuth-Client-ID ein und klicken Sie auf **Hinzuf\u00fcgen**.\n\nIhre OAuth-Client-ID lautet: `{client_id}`", + "title": "Nest: Aktualisiere das Ger\u00e4tezugriffsprojekt" + }, "init": { "data": { "flow_impl": "Anbieter" diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index 90f7c244f7b..2f3324ea956 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -29,8 +29,8 @@ "data": { "code": "Access Token" }, - "description": "To link your Google account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided Auth Token code below ([more info]({more_info_url})).", - "title": "Nest: Link Google Account" + "description": "To link your Google account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided Auth Token code below.", + "title": "Link Google Account" }, "auth_upgrade": { "description": "App Auth has been deprecated by Google to improve security, and you need to take action by creating new application credentials.\n\nOpen the [documentation]({more_info_url}) to follow along as the next steps will guide you through the steps you need to take to restore access to your Nest devices.", @@ -51,7 +51,7 @@ "data": { "project_id": "Device Access Project ID" }, - "description": "Create a Nest Device Access project which **requires a US$5 fee** to set up.\n1. Go to the [Device Access Console]({device_access_console_url}), and through the payment flow.\n1. Click on **Create project**\n1. Give your Device Access project a name and click **Next**.\n1. Enter your OAuth Client ID\n1. Enable events by clicking **Enable** and **Create project**.\n\nEnter your Device Access Project ID below ([more info]({more_info_url})).\n", + "description": "Create a Nest Device Access project which **requires a US $5 fee** to set up.\n1. Go to the [Device Access Console]({device_access_console_url}), and through the payment flow.\n1. Click on **Create project**\n1. Give your Device Access project a name and click **Next**.\n1. Enter your OAuth Client ID\n1. Enable events by clicking **Enable** and **Create project**.\n\nEnter your Device Access Project ID below ([more info]({more_info_url})).\n", "title": "Nest: Create a Device Access Project" }, "device_project_upgrade": { diff --git a/homeassistant/components/nest/translations/pt-BR.json b/homeassistant/components/nest/translations/pt-BR.json index 54b558493b8..1a4b2d8fad9 100644 --- a/homeassistant/components/nest/translations/pt-BR.json +++ b/homeassistant/components/nest/translations/pt-BR.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "Siga as [instru\u00e7\u00f5es]( {more_info_url} ) para configurar o Console da nuvem: \n\n 1. V\u00e1 para a [tela de consentimento OAuth]( {oauth_consent_url} ) e configure.\n 1. Acesse [Credentials]( {oauth_creds_url} ) e clique em **Create Credentials**.\n 1. Na lista suspensa, selecione **ID do cliente OAuth**.\n 1. Selecione **Aplicativo Web** para o Tipo de aplicativo.\n 1. Adicione ` {redirect_url} ` em *URI de redirecionamento autorizado*." + }, "config": { "abort": { "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", @@ -19,7 +22,7 @@ "subscriber_error": "Erro de assinante desconhecido, veja os logs", "timeout": "Excedido tempo limite para validar c\u00f3digo", "unknown": "Erro inesperado", - "wrong_project_id": "Insira um ID de projeto do Cloud v\u00e1lido (ID do projeto de acesso ao dispositivo encontrado)" + "wrong_project_id": "Insira um ID de projeto da nuvem v\u00e1lido (\u00e9 o mesmo que o ID do projeto de acesso ao dispositivo)" }, "step": { "auth": { @@ -29,6 +32,32 @@ "description": "Para vincular sua conta do Google, [autorize sua conta]( {url} ). \n\n Ap\u00f3s a autoriza\u00e7\u00e3o, copie e cole o c\u00f3digo de token de autentica\u00e7\u00e3o fornecido abaixo.", "title": "Vincular Conta do Google" }, + "auth_upgrade": { + "description": "A autentica\u00e7\u00e3o de aplicativo foi preterida pelo Google para melhorar a seguran\u00e7a, e voc\u00ea precisa agir criando novas credenciais de aplicativo. \n\n Abra a [documenta\u00e7\u00e3o]( {more_info_url} ) para acompanhar, pois as pr\u00f3ximas etapas o guiar\u00e3o pelas etapas necess\u00e1rias para restaurar o acesso aos seus dispositivos Nest.", + "title": "Nest: suspens\u00e3o de uso da autentica\u00e7\u00e3o do aplicativo" + }, + "cloud_project": { + "data": { + "cloud_project_id": "ID do projeto do Google Cloud" + }, + "description": "Insira o ID do projeto da nuvem abaixo, por exemplo, *example-project-12345*. Consulte o [Google Cloud Console]( {cloud_console_url} ) ou a documenta\u00e7\u00e3o para [mais informa\u00e7\u00f5es]( {more_info_url} ).", + "title": "Nest: Insira o ID do projeto da nuvem" + }, + "create_cloud_project": { + "description": "A integra\u00e7\u00e3o Nest permite que voc\u00ea integre seus termostatos, c\u00e2meras e campainhas Nest usando a API de gerenciamento de dispositivos inteligentes. A API SDM **requer uma taxa de configura\u00e7\u00e3o \u00fanica de US$ 5**. Consulte a documenta\u00e7\u00e3o para [mais informa\u00e7\u00f5es]( {more_info_url} ). \n\n 1. Acesse o [Console do Google Cloud]( {cloud_console_url} ).\n 1. Se este for seu primeiro projeto, clique em **Criar Projeto** e depois em **Novo Projeto**.\n 1. D\u00ea um nome ao seu projeto na nuvem e clique em **Criar**.\n 1. Salve o ID do projeto da nuvem, por exemplo, *example-project-12345*, pois voc\u00ea precisar\u00e1 dele mais tarde.\n 1. Acesse a Biblioteca de API para [Smart Device Management API]( {sdm_api_url} ) e clique em **Ativar**.\n 1. Acesse a API Library for [Cloud Pub/Sub API]( {pubsub_api_url} ) e clique em **Ativar**. \n\n Prossiga quando seu projeto de nuvem estiver configurado.", + "title": "Nest: criar e configurar o projeto de nuvem" + }, + "device_project": { + "data": { + "project_id": "C\u00f3digo do projeto de acesso ao dispositivo" + }, + "description": "Crie um projeto Nest Device Access que **exija uma taxa de US$ 5** para ser configurado.\n 1. V\u00e1 para o [Device Access Console]( {device_access_console_url} ) e atrav\u00e9s do fluxo de pagamento.\n 1. Clique em **Criar projeto**\n 1. D\u00ea um nome ao seu projeto Device Access e clique em **Pr\u00f3ximo**.\n 1. Insira seu ID do cliente OAuth\n 1. Ative os eventos clicando em **Ativar** e **Criar projeto**. \n\n Insira o ID do projeto de acesso ao dispositivo abaixo ([mais informa\u00e7\u00f5es]( {more_info_url} )).", + "title": "Nest: criar um projeto de acesso ao dispositivo" + }, + "device_project_upgrade": { + "description": "Atualize o Nest Device Access Project com seu novo ID do cliente OAuth ([mais informa\u00e7\u00f5es]( {more_info_url} ))\n 1. V\u00e1 para o [Console de acesso ao dispositivo]( {device_access_console_url} ).\n 1. Clique no \u00edcone da lixeira ao lado de *ID do cliente OAuth*.\n 1. Clique no menu flutuante `...` e *Adicionar ID do cliente*.\n 1. Insira seu novo ID do cliente OAuth e clique em **Adicionar**. \n\n Seu ID do cliente OAuth \u00e9: ` {client_id} `", + "title": "Nest: Atualizar projeto de acesso ao dispositivo" + }, "init": { "data": { "flow_impl": "Provedor" diff --git a/homeassistant/components/nest/translations/zh-Hant.json b/homeassistant/components/nest/translations/zh-Hant.json index c52a22e6970..8eb646217b1 100644 --- a/homeassistant/components/nest/translations/zh-Hant.json +++ b/homeassistant/components/nest/translations/zh-Hant.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "\u8ddf\u96a8 [\u8aaa\u660e]({more_info_url}) \u4ee5\u8a2d\u5b9a Cloud \u63a7\u5236\u53f0\uff1a\n\n1. \u700f\u89bd\u81f3 [OAuth \u63a7\u5236\u53f0\u756b\u9762]({oauth_consent_url}) \u4e26\u8a2d\u5b9a\n1. \u700f\u89bd\u81f3 [\u6191\u8b49]({oauth_creds_url}) \u4e26\u9ede\u9078 **\u5efa\u7acb\u6191\u8b49**\u3002\n1. \u7531\u4e0b\u62c9\u9078\u55ae\u4e2d\u9078\u64c7 **OAuth \u7528\u6236\u7aef ID**\u3002\n1. \u61c9\u7528\u7a0b\u5f0f\u985e\u578b\u5247\u9078\u64c7 **Web \u61c9\u7528\u7a0b\u5f0f**\u3002\n1. \u65bc *\u8a8d\u8b49\u91cd\u65b0\u5c0e\u5411 URI* \u4e2d\u65b0\u589e `{redirect_url}`\u3002" + }, "config": { "abort": { "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", @@ -19,7 +22,7 @@ "subscriber_error": "\u672a\u77e5\u8a02\u95b1\u932f\u8aa4\uff0c\u8acb\u53c3\u95b1\u65e5\u8a8c", "timeout": "\u8a8d\u8b49\u78bc\u903e\u6642", "unknown": "\u672a\u9810\u671f\u932f\u8aa4", - "wrong_project_id": "\u8acb\u8f38\u5165\u6709\u6548 Cloud \u5c08\u6848 ID\uff08\u53ef\u65bc Device Access Project ID \u4e2d\u627e\u5230\uff09" + "wrong_project_id": "\u8acb\u8f38\u5165\u6709\u6548 Cloud \u5c08\u6848 ID\uff08\u8207\u88dd\u7f6e\u5b58\u53d6\u5c08\u6848 ID \u76f8\u540c\uff09" }, "step": { "auth": { @@ -29,6 +32,32 @@ "description": "\u6b32\u9023\u7d50 Google \u5e33\u865f\u3001\u8acb\u5148 [\u8a8d\u8b49\u5e33\u865f]({url})\u3002\n\n\u65bc\u8a8d\u8b49\u5f8c\u3001\u65bc\u4e0b\u65b9\u8cbc\u4e0a\u8a8d\u8b49\u6b0a\u6756\u4ee3\u78bc\u3002", "title": "\u9023\u7d50 Google \u5e33\u865f" }, + "auth_upgrade": { + "description": "Google \u5df2\u4e0d\u518d\u63a8\u85a6\u4f7f\u7528 App Auth \u4ee5\u63d0\u9ad8\u5b89\u5168\u6027\u3001\u56e0\u6b64\u60a8\u9700\u8981\u5efa\u7acb\u65b0\u7684\u61c9\u7528\u7a0b\u5f0f\u6191\u8b49\u3002\n\n\u958b\u555f [\u76f8\u95dc\u6587\u4ef6]({more_info_url}) \u4e26\u8ddf\u96a8\u6b65\u9a5f\u6307\u5f15\u3001\u5c07\u5e36\u9818\u60a8\u5b58\u53d6\u6216\u56de\u5fa9\u60a8\u7684 Nest \u88dd\u7f6e\u3002", + "title": "Nest: App Auth \u5df2\u4e0d\u63a8\u85a6\u4f7f\u7528" + }, + "cloud_project": { + "data": { + "cloud_project_id": "Google Cloud \u5c08\u6848 ID" + }, + "description": "\u65bc\u4e0b\u65b9\u8f38\u5165 Cloud \u5c08\u6848 ID\u3002\u4f8b\u5982\uff1a*example-project-12345*\u3002\u8acb\u53c3\u95b1 [Google Cloud Console]({cloud_console_url}) \u6216\u76f8\u95dc\u6587\u4ef6\u4ee5\u7372\u5f97 [\u66f4\u8a73\u7d30\u8cc7\u8a0a]({more_info_url})\u3002", + "title": "Nest\uff1a\u8f38\u5165 Cloud \u5c08\u6848 ID" + }, + "create_cloud_project": { + "description": "Nest \u6574\u5408\u5c07\u5141\u8a31\u4f7f\u7528\u88dd\u7f6e\u7ba1\u7406 API \u4ee5\u6574\u5408 Nest \u6eab\u63a7\u5668\u3001\u651d\u5f71\u6a5f\u53ca\u9580\u9234\u3002SDM API **\u5c07\u5fc5\u9808\u652f\u4ed8 $5 \u7f8e\u91d1** \u4e00\u6b21\u6027\u7684\u8a2d\u5b9a\u8cbb\u7528\u3002\u8acb\u53c3\u95b1\u76f8\u95dc\u6587\u4ef6\u4ee5\u53d6\u5f97 [\u66f4\u591a\u8cc7\u8a0a]({more_info_url})\u3002\n\n1. \u700f\u89bd\u81f3 [Google Cloud \u63a7\u5236\u53f0]({cloud_console_url})\u3002\n1. \u5047\u5982\u9019\u662f\u7b2c\u4e00\u500b\u5c08\u6848\u3001\u9ede\u9078 **\u5efa\u7acb\u5c08\u6848** \u4e26\u9078\u64c7 **\u65b0\u5c08\u6848**\u3002\n1. \u5c0d Cloud \u5c08\u6848\u9032\u884c\u547d\u540d\u4e26\u9ede\u9078 **\u5efa\u7acb**\u3002\n1. \u5132\u5b58 Cloud \u5c08\u6848 ID\u3001\u4f8b\u5982\uff1a*example-project-12345*\u3001\u7a0d\u5f8c\u5c07\u6703\u7528\u4e0a\u3002\n1. \u700f\u89bd\u81f3 [\u667a\u6167\u88dd\u7f6e\u7ba1\u7406 API]({sdm_api_url}) API \u8cc7\u6599\u5eab\u4e26\u9ede\u9078 **\u555f\u7528**\u3002\n1. \u700f\u89bd\u81f3 [Cloud Pub/Sub API]({pubsub_api_url}) API \u8cc7\u6599\u5eab\u4e26\u9ede\u9078 **\u555f\u7528**\u3002\n\nCloud project \u8a2d\u5b9a\u5b8c\u6210\u5f8c\u7e7c\u7e8c\u3002", + "title": "Nest\uff1a\u5efa\u7acb\u4e26\u8a2d\u5b9a Cloud \u5c08\u6848" + }, + "device_project": { + "data": { + "project_id": "\u88dd\u7f6e\u5b58\u53d6\u5c08\u6848 ID" + }, + "description": "\u5efa\u8b70 Nest \u88dd\u7f6e\u5b58\u53d6\u5c08\u6848 **\u5c07\u6703\u9700\u8981\u652f\u4ed8 $5 \u7f8e\u91d1\u8cbb\u7528** \u4ee5\u9032\u884c\u8a2d\u5b9a\u3002\n1. \u9023\u7dda\u81f3 [\u88dd\u7f6e\u5b58\u53d6\u63a7\u5236\u53f0]({device_access_console_url})\u3001\u4e26\u9032\u884c\u4ed8\u6b3e\u7a0b\u5e8f\u3002\n1. \u9ede\u9078 **\u5efa\u7acb\u5c08\u6848**\n1. \u9032\u884c\u88dd\u7f6e\u5b58\u53d6\u5c08\u6848\u547d\u540d\u3001\u4e26\u9ede\u9078 **\u4e0b\u4e00\u6b65**\u3002\n1. \u8f38\u5165 OAuth \u5ba2\u6236\u7aef ID\n1. \u9ede\u9078 **\u555f\u7528** \u4ee5\u555f\u7528\u4e8b\u4ef6\u4e26 **\u5efa\u7acb\u5c08\u6848**\u3002\n\n\u65bc\u4e0b\u65b9 ([\u66f4\u591a\u8cc7\u8a0a]({more_info_url})) \u8f38\u5165\u88dd\u7f6e\u5b58\u53d6\u5c08\u6848 ID\u3002\n", + "title": "Nest\uff1a\u5efa\u7acb\u88dd\u7f6e\u5b58\u53d6\u5c08\u6848" + }, + "device_project_upgrade": { + "description": "\u4f7f\u7528\u65b0\u5efa OAuth \u5ba2\u6236\u7aef ID \u66f4\u65b0 Nest \u88dd\u7f6e\u5b58\u53d6\u5c08\u6848 ([\u66f4\u8a73\u7d30\u8cc7\u8a0a]({more_info_url}))\n1. \u700f\u89bd\u81f3 [\u88dd\u7f6e\u5b58\u53d6\u63a7\u5236\u53f0]({device_access_console_url})\u3002\n1. \u9ede\u9078 *OAuth \u5ba2\u6236\u7aef ID* \u65c1\u7684\u5783\u573e\u6876\u5716\u6848\u3002\n1. \u9ede\u9078 `...` \u9078\u55ae\u4e26\u9078\u64c7 *\u65b0\u589e\u5ba2\u6236\u7aef ID*\u3002\n1. \u8f38\u5165\u65b0\u5efa OAuth \u5ba2\u6236\u7aef ID \u4e26\u9ede\u9078 **\u65b0\u589e**\u3002\n\nOAuth \u5ba2\u6236\u7aef ID \u70ba\uff1a`{client_id}`", + "title": "Nest\uff1a\u66f4\u65b0\u88dd\u7f6e\u5b58\u53d6\u5c08\u6848" + }, "init": { "data": { "flow_impl": "\u8a8d\u8b49\u63d0\u4f9b\u8005" From 90e402eca53d37dd6b8cbf49da1fc18b99b9696a Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 16 Jun 2022 00:21:39 -0500 Subject: [PATCH 1524/3516] Allow removing Sonos devices (#73567) --- homeassistant/components/sonos/__init__.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 1d7acd8d8dc..c26bf269a30 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -21,7 +21,7 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOSTS, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send from homeassistant.helpers.event import async_track_time_interval, call_later from homeassistant.helpers.typing import ConfigType @@ -396,3 +396,17 @@ class SonosDiscoveryManager: AVAILABILITY_CHECK_INTERVAL, ) ) + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove Sonos config entry from a device.""" + known_devices = hass.data[DATA_SONOS].discovered.keys() + for identifier in device_entry.identifiers: + if identifier[0] != DOMAIN: + continue + uid = identifier[1] + if uid not in known_devices: + return True + return False From 90dba36f80a5f724c611ecdb1c394917383f8a08 Mon Sep 17 00:00:00 2001 From: Corbeno Date: Thu, 16 Jun 2022 00:35:58 -0500 Subject: [PATCH 1525/3516] Proxmoxve code cleanup (#73571) code cleanup --- .../components/proxmoxve/binary_sensor.py | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py index 780e7240267..97995431778 100644 --- a/homeassistant/components/proxmoxve/binary_sensor.py +++ b/homeassistant/components/proxmoxve/binary_sensor.py @@ -35,31 +35,18 @@ async def async_setup_platform( for node_config in host_config["nodes"]: node_name = node_config["node"] - for vm_id in node_config["vms"]: - coordinator = host_name_coordinators[node_name][vm_id] + for dev_id in node_config["vms"] + node_config["containers"]: + coordinator = host_name_coordinators[node_name][dev_id] - # unfound vm case + # unfound case if (coordinator_data := coordinator.data) is None: continue - vm_name = coordinator_data["name"] - vm_sensor = create_binary_sensor( - coordinator, host_name, node_name, vm_id, vm_name + name = coordinator_data["name"] + sensor = create_binary_sensor( + coordinator, host_name, node_name, dev_id, name ) - sensors.append(vm_sensor) - - for container_id in node_config["containers"]: - coordinator = host_name_coordinators[node_name][container_id] - - # unfound container case - if (coordinator_data := coordinator.data) is None: - continue - - container_name = coordinator_data["name"] - container_sensor = create_binary_sensor( - coordinator, host_name, node_name, container_id, container_name - ) - sensors.append(container_sensor) + sensors.append(sensor) add_entities(sensors) From af81ec1f5f84869bfbf261e67f9aa5250b49e6ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 15 Jun 2022 19:51:55 -1000 Subject: [PATCH 1526/3516] Handle offline generators in oncue (#73568) Fixes #73565 --- homeassistant/components/oncue/entity.py | 10 +- tests/components/oncue/__init__.py | 277 +++++++++++++++++++++++ tests/components/oncue/test_sensor.py | 22 +- 3 files changed, 304 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/oncue/entity.py b/homeassistant/components/oncue/entity.py index 40ca21edf96..d1942c532e7 100644 --- a/homeassistant/components/oncue/entity.py +++ b/homeassistant/components/oncue/entity.py @@ -3,6 +3,7 @@ from __future__ import annotations from aiooncue import OncueDevice, OncueSensor +from homeassistant.const import ATTR_CONNECTIONS from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from homeassistant.helpers.update_coordinator import ( @@ -30,16 +31,21 @@ class OncueEntity(CoordinatorEntity, Entity): self._device_id = device_id self._attr_unique_id = f"{device_id}_{description.key}" self._attr_name = f"{device.name} {sensor.display_name}" - mac_address_hex = hex(int(device.sensors["MacAddress"].value))[2:] self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, device_id)}, - connections={(dr.CONNECTION_NETWORK_MAC, mac_address_hex)}, name=device.name, hw_version=device.hardware_version, sw_version=device.sensors["FirmwareVersion"].display_value, model=device.sensors["GensetModelNumberSelect"].display_value, manufacturer="Kohler", ) + try: + mac_address_hex = hex(int(device.sensors["MacAddress"].value))[2:] + except ValueError: # MacAddress may be invalid if the gateway is offline + return + self._attr_device_info[ATTR_CONNECTIONS] = { + (dr.CONNECTION_NETWORK_MAC, mac_address_hex) + } @property def _oncue_value(self) -> str: diff --git a/tests/components/oncue/__init__.py b/tests/components/oncue/__init__.py index 48492a19933..32845aa8d26 100644 --- a/tests/components/oncue/__init__.py +++ b/tests/components/oncue/__init__.py @@ -269,6 +269,271 @@ MOCK_ASYNC_FETCH_ALL = { } +MOCK_ASYNC_FETCH_ALL_OFFLINE_DEVICE = { + "456789": OncueDevice( + name="My Generator", + state="Off", + product_name="RDC 2.4", + hardware_version="319", + serial_number="SERIAL", + sensors={ + "Product": OncueSensor( + name="Product", + display_name="Controller Type", + value="RDC 2.4", + display_value="RDC 2.4", + unit=None, + ), + "FirmwareVersion": OncueSensor( + name="FirmwareVersion", + display_name="Current Firmware", + value="2.0.6", + display_value="2.0.6", + unit=None, + ), + "LatestFirmware": OncueSensor( + name="LatestFirmware", + display_name="Latest Firmware", + value="2.0.6", + display_value="2.0.6", + unit=None, + ), + "EngineSpeed": OncueSensor( + name="EngineSpeed", + display_name="Engine Speed", + value="0", + display_value="0 R/min", + unit="R/min", + ), + "EngineTargetSpeed": OncueSensor( + name="EngineTargetSpeed", + display_name="Engine Target Speed", + value="0", + display_value="0 R/min", + unit="R/min", + ), + "EngineOilPressure": OncueSensor( + name="EngineOilPressure", + display_name="Engine Oil Pressure", + value=0, + display_value="0 Psi", + unit="Psi", + ), + "EngineCoolantTemperature": OncueSensor( + name="EngineCoolantTemperature", + display_name="Engine Coolant Temperature", + value=32, + display_value="32 F", + unit="F", + ), + "BatteryVoltage": OncueSensor( + name="BatteryVoltage", + display_name="Battery Voltage", + value="13.4", + display_value="13.4 V", + unit="V", + ), + "LubeOilTemperature": OncueSensor( + name="LubeOilTemperature", + display_name="Lube Oil Temperature", + value=32, + display_value="32 F", + unit="F", + ), + "GensetControllerTemperature": OncueSensor( + name="GensetControllerTemperature", + display_name="Generator Controller Temperature", + value=84.2, + display_value="84.2 F", + unit="F", + ), + "EngineCompartmentTemperature": OncueSensor( + name="EngineCompartmentTemperature", + display_name="Engine Compartment Temperature", + value=62.6, + display_value="62.6 F", + unit="F", + ), + "GeneratorTrueTotalPower": OncueSensor( + name="GeneratorTrueTotalPower", + display_name="Generator True Total Power", + value="0.0", + display_value="0.0 W", + unit="W", + ), + "GeneratorTruePercentOfRatedPower": OncueSensor( + name="GeneratorTruePercentOfRatedPower", + display_name="Generator True Percent Of Rated Power", + value="0", + display_value="0 %", + unit="%", + ), + "GeneratorVoltageAB": OncueSensor( + name="GeneratorVoltageAB", + display_name="Generator Voltage AB", + value="0.0", + display_value="0.0 V", + unit="V", + ), + "GeneratorVoltageAverageLineToLine": OncueSensor( + name="GeneratorVoltageAverageLineToLine", + display_name="Generator Voltage Average Line To Line", + value="0.0", + display_value="0.0 V", + unit="V", + ), + "GeneratorCurrentAverage": OncueSensor( + name="GeneratorCurrentAverage", + display_name="Generator Current Average", + value="0.0", + display_value="0.0 A", + unit="A", + ), + "GeneratorFrequency": OncueSensor( + name="GeneratorFrequency", + display_name="Generator Frequency", + value="0.0", + display_value="0.0 Hz", + unit="Hz", + ), + "GensetSerialNumber": OncueSensor( + name="GensetSerialNumber", + display_name="Generator Serial Number", + value="33FDGMFR0026", + display_value="33FDGMFR0026", + unit=None, + ), + "GensetState": OncueSensor( + name="GensetState", + display_name="Generator State", + value="Off", + display_value="Off", + unit=None, + ), + "GensetControllerSerialNumber": OncueSensor( + name="GensetControllerSerialNumber", + display_name="Generator Controller Serial Number", + value="-1", + display_value="-1", + unit=None, + ), + "GensetModelNumberSelect": OncueSensor( + name="GensetModelNumberSelect", + display_name="Genset Model Number Select", + value="38 RCLB", + display_value="38 RCLB", + unit=None, + ), + "GensetControllerClockTime": OncueSensor( + name="GensetControllerClockTime", + display_name="Generator Controller Clock Time", + value="2022-01-13 18:08:13", + display_value="2022-01-13 18:08:13", + unit=None, + ), + "GensetControllerTotalOperationTime": OncueSensor( + name="GensetControllerTotalOperationTime", + display_name="Generator Controller Total Operation Time", + value="16770.8", + display_value="16770.8 h", + unit="h", + ), + "EngineTotalRunTime": OncueSensor( + name="EngineTotalRunTime", + display_name="Engine Total Run Time", + value="28.1", + display_value="28.1 h", + unit="h", + ), + "EngineTotalRunTimeLoaded": OncueSensor( + name="EngineTotalRunTimeLoaded", + display_name="Engine Total Run Time Loaded", + value="5.5", + display_value="5.5 h", + unit="h", + ), + "EngineTotalNumberOfStarts": OncueSensor( + name="EngineTotalNumberOfStarts", + display_name="Engine Total Number Of Starts", + value="101", + display_value="101", + unit=None, + ), + "GensetTotalEnergy": OncueSensor( + name="GensetTotalEnergy", + display_name="Genset Total Energy", + value="1.2022309E7", + display_value="1.2022309E7 kWh", + unit="kWh", + ), + "AtsContactorPosition": OncueSensor( + name="AtsContactorPosition", + display_name="Ats Contactor Position", + value="Source1", + display_value="Source1", + unit=None, + ), + "AtsSourcesAvailable": OncueSensor( + name="AtsSourcesAvailable", + display_name="Ats Sources Available", + value="Source1", + display_value="Source1", + unit=None, + ), + "Source1VoltageAverageLineToLine": OncueSensor( + name="Source1VoltageAverageLineToLine", + display_name="Source1 Voltage Average Line To Line", + value="253.5", + display_value="253.5 V", + unit="V", + ), + "Source2VoltageAverageLineToLine": OncueSensor( + name="Source2VoltageAverageLineToLine", + display_name="Source2 Voltage Average Line To Line", + value="0.0", + display_value="0.0 V", + unit="V", + ), + "IPAddress": OncueSensor( + name="IPAddress", + display_name="IP Address", + value="1.2.3.4:1026", + display_value="1.2.3.4:1026", + unit=None, + ), + "MacAddress": OncueSensor( + name="MacAddress", + display_name="Mac Address", + value="--", + display_value="--", + unit=None, + ), + "ConnectedServerIPAddress": OncueSensor( + name="ConnectedServerIPAddress", + display_name="Connected Server IP Address", + value="40.117.195.28", + display_value="40.117.195.28", + unit=None, + ), + "NetworkConnectionEstablished": OncueSensor( + name="NetworkConnectionEstablished", + display_name="Network Connection Established", + value="true", + display_value="True", + unit=None, + ), + "SerialNumber": OncueSensor( + name="SerialNumber", + display_name="Serial Number", + value="1073879692", + display_value="1073879692", + unit=None, + ), + }, + ) +} + + def _patch_login_and_data(): @contextmanager def _patcher(): @@ -279,3 +544,15 @@ def _patch_login_and_data(): yield return _patcher() + + +def _patch_login_and_data_offline_device(): + @contextmanager + def _patcher(): + with patch("homeassistant.components.oncue.Oncue.async_login",), patch( + "homeassistant.components.oncue.Oncue.async_fetch_all", + return_value=MOCK_ASYNC_FETCH_ALL_OFFLINE_DEVICE, + ): + yield + + return _patcher() diff --git a/tests/components/oncue/test_sensor.py b/tests/components/oncue/test_sensor.py index 5fe8b807c1b..60c9f68f81b 100644 --- a/tests/components/oncue/test_sensor.py +++ b/tests/components/oncue/test_sensor.py @@ -1,19 +1,29 @@ """Tests for the oncue sensor.""" from __future__ import annotations +import pytest + from homeassistant.components import oncue from homeassistant.components.oncue.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -from . import _patch_login_and_data +from . import _patch_login_and_data, _patch_login_and_data_offline_device from tests.common import MockConfigEntry -async def test_sensors(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "patcher, connections", + [ + [_patch_login_and_data, {("mac", "c9:24:22:6f:14:00")}], + [_patch_login_and_data_offline_device, set()], + ], +) +async def test_sensors(hass: HomeAssistant, patcher, connections) -> None: """Test that the sensors are setup with the expected values.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -21,11 +31,17 @@ async def test_sensors(hass: HomeAssistant) -> None: unique_id="any", ) config_entry.add_to_hass(hass) - with _patch_login_and_data(): + with patcher(): await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) await hass.async_block_till_done() assert config_entry.state == ConfigEntryState.LOADED + entity_registry = er.async_get(hass) + ent = entity_registry.async_get("sensor.my_generator_latest_firmware") + device_registry = dr.async_get(hass) + dev = device_registry.async_get(ent.device_id) + assert dev.connections == connections + assert len(hass.states.async_all("sensor")) == 25 assert hass.states.get("sensor.my_generator_latest_firmware").state == "2.0.6" From 2d07cda4e7d9a4e0c65a32d8702f760efd8b3def Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 Jun 2022 09:05:13 +0200 Subject: [PATCH 1527/3516] Improve number deprecation warnings (#73552) --- homeassistant/components/number/__init__.py | 39 +++++++++++++++++++-- tests/components/number/test_init.py | 8 +++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 13ba47e87dc..f0dc77b7dfb 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -133,8 +133,11 @@ class NumberEntityDescription(EntityDescription): or self.step is not None or self.unit_of_measurement is not None ): - caller = inspect.stack()[2] - module = inspect.getmodule(caller[0]) + if self.__class__.__name__ == "NumberEntityDescription": + caller = inspect.stack()[2] + module = inspect.getmodule(caller[0]) + else: + module = inspect.getmodule(self) if module and module.__file__ and "custom_components" in module.__file__: report_issue = "report it to the custom component author." else: @@ -187,6 +190,38 @@ class NumberEntity(Entity): _attr_native_unit_of_measurement: str | None _deprecated_number_entity_reported = False + def __init_subclass__(cls, **kwargs: Any) -> None: + """Post initialisation processing.""" + super().__init_subclass__(**kwargs) + if any( + method in cls.__dict__ + for method in ( + "async_set_value", + "max_value", + "min_value", + "set_value", + "step", + "unit_of_measurement", + "value", + ) + ): + module = inspect.getmodule(cls) + if module and module.__file__ and "custom_components" in module.__file__: + report_issue = "report it to the custom component author." + else: + report_issue = ( + "create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" + ) + _LOGGER.warning( + "%s::%s is overriding deprecated methods on an instance of " + "NumberEntity, this is not valid and will be unsupported " + "from Home Assistant 2022.10. Please %s", + cls.__module__, + cls.__name__, + report_issue, + ) + @property def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 0df7f79e4a4..9921d2a639e 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -229,8 +229,8 @@ async def test_attributes(hass: HomeAssistant) -> None: assert number_4.value is None -async def test_attributes_deprecated(hass: HomeAssistant, caplog) -> None: - """Test overriding the deprecated attributes.""" +async def test_deprecation_warnings(hass: HomeAssistant, caplog) -> None: + """Test overriding the deprecated attributes is possible and warnings are logged.""" number = MockDefaultNumberEntityDeprecated() number.hass = hass assert number.max_value == 100.0 @@ -263,6 +263,10 @@ async def test_attributes_deprecated(hass: HomeAssistant, caplog) -> None: assert number_4.unit_of_measurement == "rabbits" assert number_4.value == 0.5 + assert ( + "tests.components.number.test_init::MockNumberEntityDeprecated is overriding " + " deprecated methods on an instance of NumberEntity" + ) assert ( "Entity None () " "is using deprecated NumberEntity features" in caplog.text From 7a7729678e57306aa740e15bb1aa3b8e075b02bf Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Thu, 16 Jun 2022 09:15:19 +0100 Subject: [PATCH 1528/3516] Bump growattServer to 1.2.2 (#73561) Fix #71577 - Updating growattServer dependency --- homeassistant/components/growatt_server/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index c8a71d426e7..4127b48ae64 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -3,7 +3,7 @@ "name": "Growatt", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/growatt_server/", - "requirements": ["growattServer==1.1.0"], + "requirements": ["growattServer==1.2.2"], "codeowners": ["@indykoning", "@muppet3000", "@JasperPlant"], "iot_class": "cloud_polling", "loggers": ["growattServer"] diff --git a/requirements_all.txt b/requirements_all.txt index d38fc15b273..6ea4e85475a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -771,7 +771,7 @@ greenwavereality==0.5.1 gridnet==4.0.0 # homeassistant.components.growatt_server -growattServer==1.1.0 +growattServer==1.2.2 # homeassistant.components.gstreamer gstreamer-player==1.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2049ae73e25..e75320d28fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -553,7 +553,7 @@ greeneye_monitor==3.0.3 gridnet==4.0.0 # homeassistant.components.growatt_server -growattServer==1.1.0 +growattServer==1.2.2 # homeassistant.components.profiler guppy3==3.1.2 From 6374fd099241145f0374e76512e420f22d3042c9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Jun 2022 10:19:44 +0200 Subject: [PATCH 1529/3516] Add lock typing in volvooncall (#73548) --- homeassistant/components/volvooncall/lock.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/volvooncall/lock.py b/homeassistant/components/volvooncall/lock.py index 5023749e622..c341627eef4 100644 --- a/homeassistant/components/volvooncall/lock.py +++ b/homeassistant/components/volvooncall/lock.py @@ -3,6 +3,8 @@ from __future__ import annotations from typing import Any +from volvooncall.dashboard import Lock + from homeassistant.components.lock import LockEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -27,8 +29,10 @@ async def async_setup_platform( class VolvoLock(VolvoEntity, LockEntity): """Represents a car lock.""" + instrument: Lock + @property - def is_locked(self): + def is_locked(self) -> bool | None: """Return true if lock is locked.""" return self.instrument.is_locked From 2b5748912d9ec21050dcc3882f1f115563c63804 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Jun 2022 10:20:08 +0200 Subject: [PATCH 1530/3516] Add lock typing in starline (#73546) --- homeassistant/components/starline/lock.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/starline/lock.py b/homeassistant/components/starline/lock.py index 48f91d89809..3a5c45a2ed1 100644 --- a/homeassistant/components/starline/lock.py +++ b/homeassistant/components/starline/lock.py @@ -1,4 +1,6 @@ """Support for StarLine lock.""" +from __future__ import annotations + from typing import Any from homeassistant.components.lock import LockEntity @@ -66,7 +68,7 @@ class StarlineLock(StarlineEntity, LockEntity): ) @property - def is_locked(self): + def is_locked(self) -> bool | None: """Return true if lock is locked.""" return self._device.car_state.get("arm") From 521d52a8b90cd1e6c4f5c6f303a8f0d434b33867 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Jun 2022 10:20:34 +0200 Subject: [PATCH 1531/3516] Add lock typing in nuki (#73545) --- homeassistant/components/nuki/lock.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 765fc5f711f..33d7465e12d 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -2,6 +2,7 @@ from abc import ABC, abstractmethod from typing import Any +from pynuki import NukiLock, NukiOpener from pynuki.constants import MODE_OPENER_CONTINUOUS import voluptuous as vol @@ -73,11 +74,6 @@ class NukiDeviceEntity(NukiEntity, LockEntity, ABC): """Return a unique ID.""" return self._nuki_device.nuki_id - @property - @abstractmethod - def is_locked(self): - """Return true if lock is locked.""" - @property def extra_state_attributes(self): """Return the device specific state attributes.""" @@ -108,8 +104,10 @@ class NukiDeviceEntity(NukiEntity, LockEntity, ABC): class NukiLockEntity(NukiDeviceEntity): """Representation of a Nuki lock.""" + _nuki_device: NukiLock + @property - def is_locked(self): + def is_locked(self) -> bool: """Return true if lock is locked.""" return self._nuki_device.is_locked @@ -137,8 +135,10 @@ class NukiLockEntity(NukiDeviceEntity): class NukiOpenerEntity(NukiDeviceEntity): """Representation of a Nuki opener.""" + _nuki_device: NukiOpener + @property - def is_locked(self): + def is_locked(self) -> bool: """Return true if either ring-to-open or continuous mode is enabled.""" return not ( self._nuki_device.is_rto_activated From 7731cfd978db7045ccf761c020be2c28c307a69b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Jun 2022 10:40:55 +0200 Subject: [PATCH 1532/3516] Add lock typing in freedompro (#73544) --- homeassistant/components/freedompro/lock.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/freedompro/lock.py b/homeassistant/components/freedompro/lock.py index 7dbc625966e..237ad50c053 100644 --- a/homeassistant/components/freedompro/lock.py +++ b/homeassistant/components/freedompro/lock.py @@ -1,5 +1,6 @@ """Support for Freedompro lock.""" import json +from typing import Any from pyfreedompro import put_state @@ -75,10 +76,10 @@ class Device(CoordinatorEntity, LockEntity): await super().async_added_to_hass() self._handle_coordinator_update() - async def async_lock(self, **kwargs): + async def async_lock(self, **kwargs: Any) -> None: """Async function to lock the lock.""" - payload = {"lock": 1} - payload = json.dumps(payload) + payload_dict = {"lock": 1} + payload = json.dumps(payload_dict) await put_state( self._session, self._api_key, @@ -87,10 +88,10 @@ class Device(CoordinatorEntity, LockEntity): ) await self.coordinator.async_request_refresh() - async def async_unlock(self, **kwargs): + async def async_unlock(self, **kwargs: Any) -> None: """Async function to unlock the lock.""" - payload = {"lock": 0} - payload = json.dumps(payload) + payload_dict = {"lock": 0} + payload = json.dumps(payload_dict) await put_state( self._session, self._api_key, From c2b484e38b11458b14c163249ae15a1414e4e435 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 16 Jun 2022 11:43:36 +0200 Subject: [PATCH 1533/3516] Use IP address instead of hostname in Brother integration (#73556) --- .../components/brother/config_flow.py | 5 +-- tests/components/brother/test_config_flow.py | 38 +++++++++++-------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index 24e7d701ed0..bcedc65d7ff 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -83,8 +83,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - # Hostname is format: brother.local. - self.host = discovery_info.hostname.rstrip(".") + self.host = discovery_info.host # Do not probe the device if the host is already configured self._async_abort_entries_match({CONF_HOST: self.host}) @@ -102,7 +101,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # Check if already configured await self.async_set_unique_id(self.brother.serial.lower()) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured({CONF_HOST: self.host}) self.context.update( { diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 6dbaebdfa7b..00493011500 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_HOST, CONF_TYPE from tests.common import MockConfigEntry, load_fixture -CONFIG = {CONF_HOST: "localhost", CONF_TYPE: "laser"} +CONFIG = {CONF_HOST: "127.0.0.1", CONF_TYPE: "laser"} async def test_show_form(hass): @@ -32,13 +32,15 @@ async def test_create_entry_with_hostname(hass): return_value=json.loads(load_fixture("printer_data.json", "brother")), ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: "example.local", CONF_TYPE: "laser"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" - assert result["data"][CONF_HOST] == CONFIG[CONF_HOST] - assert result["data"][CONF_TYPE] == CONFIG[CONF_TYPE] + assert result["data"][CONF_HOST] == "example.local" + assert result["data"][CONF_TYPE] == "laser" async def test_create_entry_with_ipv4_address(hass): @@ -48,9 +50,7 @@ async def test_create_entry_with_ipv4_address(hass): return_value=json.loads(load_fixture("printer_data.json", "brother")), ): result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_HOST: "127.0.0.1", CONF_TYPE: "laser"}, + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -145,7 +145,7 @@ async def test_zeroconf_snmp_error(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", addresses=["mock_host"], hostname="example.local.", name="Brother Printer", @@ -166,7 +166,7 @@ async def test_zeroconf_unsupported_model(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", addresses=["mock_host"], hostname="example.local.", name="Brother Printer", @@ -187,15 +187,18 @@ async def test_zeroconf_device_exists_abort(hass): "brother.Brother._get_data", return_value=json.loads(load_fixture("printer_data.json", "brother")), ): - MockConfigEntry(domain=DOMAIN, unique_id="0123456789", data=CONFIG).add_to_hass( - hass + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={CONF_HOST: "example.local", CONF_TYPE: "laser"}, ) + entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", addresses=["mock_host"], hostname="example.local.", name="Brother Printer", @@ -208,6 +211,9 @@ async def test_zeroconf_device_exists_abort(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + # Test config entry got updated with latest IP + assert entry.data["host"] == "127.0.0.1" + async def test_zeroconf_no_probe_existing_device(hass): """Test we do not probe the device is the host is already configured.""" @@ -218,9 +224,9 @@ async def test_zeroconf_no_probe_existing_device(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", addresses=["mock_host"], - hostname="localhost", + hostname="example.local.", name="Brother Printer", port=None, properties={}, @@ -245,7 +251,7 @@ async def test_zeroconf_confirm_create_entry(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", addresses=["mock_host"], hostname="example.local.", name="Brother Printer", @@ -266,5 +272,5 @@ async def test_zeroconf_confirm_create_entry(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" - assert result["data"][CONF_HOST] == "example.local" + assert result["data"][CONF_HOST] == "127.0.0.1" assert result["data"][CONF_TYPE] == "laser" From 67b035463255921a2450b70367ce702ffbb5a184 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Jun 2022 12:57:41 +0200 Subject: [PATCH 1534/3516] Adjust FlowResult construction in data entry flow (#72884) --- homeassistant/data_entry_flow.py | 140 +++++++++++++++---------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index abc8061c0d5..23b35138df7 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -109,12 +109,12 @@ def _async_flow_handler_to_flow_result( ) -> 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, - } + FlowResult( + 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 ] @@ -446,16 +446,16 @@ class FlowHandler: last_step: bool | None = None, ) -> FlowResult: """Return the definition of a form to gather user input.""" - return { - "type": FlowResultType.FORM, - "flow_id": self.flow_id, - "handler": self.handler, - "step_id": step_id, - "data_schema": data_schema, - "errors": errors, - "description_placeholders": description_placeholders, - "last_step": last_step, # Display next or submit button in frontend - } + return FlowResult( + type=FlowResultType.FORM, + flow_id=self.flow_id, + handler=self.handler, + step_id=step_id, + data_schema=data_schema, + errors=errors, + description_placeholders=description_placeholders, + last_step=last_step, # Display next or submit button in frontend + ) @callback def async_create_entry( @@ -467,16 +467,16 @@ class FlowHandler: description_placeholders: Mapping[str, str] | None = None, ) -> FlowResult: """Finish config flow and create a config entry.""" - return { - "version": self.VERSION, - "type": FlowResultType.CREATE_ENTRY, - "flow_id": self.flow_id, - "handler": self.handler, - "title": title, - "data": data, - "description": description, - "description_placeholders": description_placeholders, - } + return FlowResult( + version=self.VERSION, + type=FlowResultType.CREATE_ENTRY, + flow_id=self.flow_id, + handler=self.handler, + title=title, + data=data, + description=description, + description_placeholders=description_placeholders, + ) @callback def async_abort( @@ -499,24 +499,24 @@ class FlowHandler: description_placeholders: Mapping[str, str] | None = None, ) -> FlowResult: """Return the definition of an external step for the user to take.""" - return { - "type": FlowResultType.EXTERNAL_STEP, - "flow_id": self.flow_id, - "handler": self.handler, - "step_id": step_id, - "url": url, - "description_placeholders": description_placeholders, - } + return FlowResult( + type=FlowResultType.EXTERNAL_STEP, + flow_id=self.flow_id, + handler=self.handler, + step_id=step_id, + url=url, + description_placeholders=description_placeholders, + ) @callback def async_external_step_done(self, *, next_step_id: str) -> FlowResult: """Return the definition of an external step for the user to take.""" - return { - "type": FlowResultType.EXTERNAL_STEP_DONE, - "flow_id": self.flow_id, - "handler": self.handler, - "step_id": next_step_id, - } + return FlowResult( + type=FlowResultType.EXTERNAL_STEP_DONE, + flow_id=self.flow_id, + handler=self.handler, + step_id=next_step_id, + ) @callback def async_show_progress( @@ -527,24 +527,24 @@ class FlowHandler: description_placeholders: Mapping[str, str] | None = None, ) -> FlowResult: """Show a progress message to the user, without user input allowed.""" - return { - "type": FlowResultType.SHOW_PROGRESS, - "flow_id": self.flow_id, - "handler": self.handler, - "step_id": step_id, - "progress_action": progress_action, - "description_placeholders": description_placeholders, - } + return FlowResult( + type=FlowResultType.SHOW_PROGRESS, + flow_id=self.flow_id, + handler=self.handler, + step_id=step_id, + progress_action=progress_action, + description_placeholders=description_placeholders, + ) @callback def async_show_progress_done(self, *, next_step_id: str) -> FlowResult: """Mark the progress done.""" - return { - "type": FlowResultType.SHOW_PROGRESS_DONE, - "flow_id": self.flow_id, - "handler": self.handler, - "step_id": next_step_id, - } + return FlowResult( + type=FlowResultType.SHOW_PROGRESS_DONE, + flow_id=self.flow_id, + handler=self.handler, + step_id=next_step_id, + ) @callback def async_show_menu( @@ -558,15 +558,15 @@ class FlowHandler: Options dict maps step_id => i18n label """ - return { - "type": FlowResultType.MENU, - "flow_id": self.flow_id, - "handler": self.handler, - "step_id": step_id, - "data_schema": vol.Schema({"next_step_id": vol.In(menu_options)}), - "menu_options": menu_options, - "description_placeholders": description_placeholders, - } + return FlowResult( + type=FlowResultType.MENU, + flow_id=self.flow_id, + handler=self.handler, + step_id=step_id, + data_schema=vol.Schema({"next_step_id": vol.In(menu_options)}), + menu_options=menu_options, + description_placeholders=description_placeholders, + ) @callback @@ -577,10 +577,10 @@ def _create_abort_data( description_placeholders: Mapping[str, str] | None = None, ) -> FlowResult: """Return the definition of an external step for the user to take.""" - return { - "type": FlowResultType.ABORT, - "flow_id": flow_id, - "handler": handler, - "reason": reason, - "description_placeholders": description_placeholders, - } + return FlowResult( + type=FlowResultType.ABORT, + flow_id=flow_id, + handler=handler, + reason=reason, + description_placeholders=description_placeholders, + ) From dea80414614c2f201686e840b4d2c1a9411279d8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 Jun 2022 13:34:54 +0200 Subject: [PATCH 1535/3516] Add device_class to MQTT number and migrate to native_value (#73534) --- homeassistant/components/mqtt/number.py | 33 +++-- homeassistant/components/number/__init__.py | 5 +- tests/components/mqtt/test_number.py | 141 ++++++++++++++------ 3 files changed, 122 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 1404dc86a3c..bbc78ae07db 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -11,10 +11,12 @@ from homeassistant.components.number import ( DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE, DEFAULT_STEP, - NumberEntity, + DEVICE_CLASSES_SCHEMA, + RestoreNumber, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + CONF_DEVICE_CLASS, CONF_NAME, CONF_OPTIMISTIC, CONF_UNIT_OF_MEASUREMENT, @@ -23,7 +25,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription @@ -78,6 +79,7 @@ def validate_config(config): _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): vol.Coerce(float), vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): vol.Coerce(float), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -152,7 +154,7 @@ async def _async_setup_entity( async_add_entities([MqttNumber(hass, config, config_entry, discovery_data)]) -class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): +class MqttNumber(MqttEntity, RestoreNumber): """representation of an MQTT number.""" _entity_id_format = number.ENTITY_ID_FORMAT @@ -166,7 +168,7 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): self._current_number = None - NumberEntity.__init__(self) + RestoreNumber.__init__(self) MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod @@ -243,35 +245,37 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): """(Re)Subscribe to topics.""" await subscription.async_subscribe_topics(self.hass, self._sub_state) - if self._optimistic and (last_state := await self.async_get_last_state()): - self._current_number = last_state.state + if self._optimistic and ( + last_number_data := await self.async_get_last_number_data() + ): + self._current_number = last_number_data.native_value @property - def min_value(self) -> float: + def native_min_value(self) -> float: """Return the minimum value.""" return self._config[CONF_MIN] @property - def max_value(self) -> float: + def native_max_value(self) -> float: """Return the maximum value.""" return self._config[CONF_MAX] @property - def step(self) -> float: + def native_step(self) -> float: """Return the increment/decrement step.""" return self._config[CONF_STEP] @property - def unit_of_measurement(self) -> str | None: + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" return self._config.get(CONF_UNIT_OF_MEASUREMENT) @property - def value(self): + def native_value(self): """Return the current value.""" return self._current_number - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Update the current value.""" current_number = value @@ -295,3 +299,8 @@ class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): def assumed_state(self): """Return true if we do optimistic updates.""" return self._optimistic + + @property + def device_class(self) -> str | None: + """Return the device class of the sensor.""" + return self._config.get(CONF_DEVICE_CLASS) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index f0dc77b7dfb..f0095e2aecb 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -8,7 +8,7 @@ from datetime import timedelta import inspect import logging from math import ceil, floor -from typing import Any, final +from typing import Any, Final, final import voluptuous as vol @@ -54,6 +54,9 @@ class NumberDeviceClass(StrEnum): TEMPERATURE = "temperature" +DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(NumberDeviceClass)) + + class NumberMode(StrEnum): """Modes for number entities.""" diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index ea79c5cd7aa..1db7c5e3463 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -18,11 +18,14 @@ from homeassistant.components.number import ( ATTR_VALUE, DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, + NumberDeviceClass, ) from homeassistant.const import ( ATTR_ASSUMED_STATE, + ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, + TEMP_FAHRENHEIT, Platform, ) import homeassistant.core as ha @@ -58,7 +61,7 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import async_fire_mqtt_message +from tests.common import async_fire_mqtt_message, mock_restore_cache_with_extra_data DEFAULT_CONFIG = { number.DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} @@ -84,7 +87,8 @@ async def test_run_number_setup(hass, mqtt_mock_entry_with_yaml_config): "state_topic": topic, "command_topic": topic, "name": "Test Number", - "unit_of_measurement": "my unit", + "device_class": "temperature", + "unit_of_measurement": TEMP_FAHRENHEIT, "payload_reset": "reset!", } }, @@ -97,16 +101,18 @@ async def test_run_number_setup(hass, mqtt_mock_entry_with_yaml_config): await hass.async_block_till_done() state = hass.states.get("number.test_number") - assert state.state == "10" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "my unit" + assert state.state == "-12.0" # 10 °F -> -12 °C + assert state.attributes.get(ATTR_DEVICE_CLASS) == NumberDeviceClass.TEMPERATURE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "°C" async_fire_mqtt_message(hass, topic, "20.5") await hass.async_block_till_done() state = hass.states.get("number.test_number") - assert state.state == "20.5" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "my unit" + assert state.state == "-6.4" # 20.5 °F -> -6.4 °C + assert state.attributes.get(ATTR_DEVICE_CLASS) == NumberDeviceClass.TEMPERATURE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "°C" async_fire_mqtt_message(hass, topic, "reset!") @@ -114,7 +120,8 @@ async def test_run_number_setup(hass, mqtt_mock_entry_with_yaml_config): state = hass.states.get("number.test_number") assert state.state == "unknown" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "my unit" + assert state.attributes.get(ATTR_DEVICE_CLASS) == NumberDeviceClass.TEMPERATURE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "°C" async def test_value_template(hass, mqtt_mock_entry_with_yaml_config): @@ -158,29 +165,70 @@ async def test_value_template(hass, mqtt_mock_entry_with_yaml_config): assert state.state == "unknown" +async def test_restore_native_value(hass, mqtt_mock_entry_with_yaml_config): + """Test that the stored native_value is restored.""" + topic = "test/number" + + RESTORE_DATA = { + "native_max_value": None, # Ignored by MQTT number + "native_min_value": None, # Ignored by MQTT number + "native_step": None, # Ignored by MQTT number + "native_unit_of_measurement": None, # Ignored by MQTT number + "native_value": 100.0, + } + + mock_restore_cache_with_extra_data( + hass, ((ha.State("number.test_number", "abc"), RESTORE_DATA),) + ) + assert await async_setup_component( + hass, + number.DOMAIN, + { + "number": { + "platform": "mqtt", + "command_topic": topic, + "device_class": "temperature", + "unit_of_measurement": TEMP_FAHRENHEIT, + "name": "Test Number", + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get("number.test_number") + assert state.state == "37.8" + assert state.attributes.get(ATTR_ASSUMED_STATE) + + async def test_run_number_service_optimistic(hass, mqtt_mock_entry_with_yaml_config): """Test that set_value service works in optimistic mode.""" topic = "test/number" - fake_state = ha.State("switch.test", "3") + RESTORE_DATA = { + "native_max_value": None, # Ignored by MQTT number + "native_min_value": None, # Ignored by MQTT number + "native_step": None, # Ignored by MQTT number + "native_unit_of_measurement": None, # Ignored by MQTT number + "native_value": 3, + } - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ): - assert await async_setup_component( - hass, - number.DOMAIN, - { - "number": { - "platform": "mqtt", - "command_topic": topic, - "name": "Test Number", - } - }, - ) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + mock_restore_cache_with_extra_data( + hass, ((ha.State("number.test_number", "abc"), RESTORE_DATA),) + ) + assert await async_setup_component( + hass, + number.DOMAIN, + { + "number": { + "platform": "mqtt", + "command_topic": topic, + "name": "Test Number", + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("number.test_number") assert state.state == "3" @@ -232,26 +280,31 @@ async def test_run_number_service_optimistic_with_command_template( """Test that set_value service works in optimistic mode and with a command_template.""" topic = "test/number" - fake_state = ha.State("switch.test", "3") + RESTORE_DATA = { + "native_max_value": None, # Ignored by MQTT number + "native_min_value": None, # Ignored by MQTT number + "native_step": None, # Ignored by MQTT number + "native_unit_of_measurement": None, # Ignored by MQTT number + "native_value": 3, + } - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ): - assert await async_setup_component( - hass, - number.DOMAIN, - { - "number": { - "platform": "mqtt", - "command_topic": topic, - "name": "Test Number", - "command_template": '{"number": {{ value }} }', - } - }, - ) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() + mock_restore_cache_with_extra_data( + hass, ((ha.State("number.test_number", "abc"), RESTORE_DATA),) + ) + assert await async_setup_component( + hass, + number.DOMAIN, + { + "number": { + "platform": "mqtt", + "command_topic": topic, + "name": "Test Number", + "command_template": '{"number": {{ value }} }', + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() state = hass.states.get("number.test_number") assert state.state == "3" From ddca199961aa0ac8943ad0e994c94a963b81a748 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 Jun 2022 13:49:16 +0200 Subject: [PATCH 1536/3516] Migrate tuya NumberEntity to native_value (#73491) --- homeassistant/components/tuya/number.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index a342fe58bd2..bee5242d4ae 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -338,9 +338,9 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity): description.key, dptype=DPType.INTEGER, prefer_function=True ): self._number = int_type - self._attr_max_value = self._number.max_scaled - self._attr_min_value = self._number.min_scaled - self._attr_step = self._number.step_scaled + self._attr_native_max_value = self._number.max_scaled + self._attr_native_min_value = self._number.min_scaled + self._attr_native_step = self._number.step_scaled # Logic to ensure the set device class and API received Unit Of Measurement # match Home Assistants requirements. @@ -373,8 +373,14 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity): if self.device_class: self._attr_icon = None + # 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 value(self) -> float | None: + def native_value(self) -> float | None: """Return the entity value to represent the entity state.""" # Unknown or unsupported data type if self._number is None: @@ -386,7 +392,7 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity): return self._number.scale_value(value) - def set_value(self, value: float) -> None: + def set_native_value(self, value: float) -> None: """Set new value.""" if self._number is None: raise RuntimeError("Cannot set value, device doesn't provide type data") From 8049170e5a9e904f6618580281bbd9fcb8adb64d Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 16 Jun 2022 14:40:41 +0200 Subject: [PATCH 1537/3516] Initialize hass.config_entries for check config (#73575) --- homeassistant/scripts/check_config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 221dafa729c..cff40d2535c 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -13,6 +13,7 @@ from unittest.mock import patch from homeassistant import core from homeassistant.config import get_default_config_dir +from homeassistant.config_entries import ConfigEntries from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import area_registry, device_registry, entity_registry from homeassistant.helpers.check_config import async_check_ha_config_file @@ -228,6 +229,7 @@ async def async_check_config(config_dir): """Check the HA config.""" hass = core.HomeAssistant() hass.config.config_dir = config_dir + hass.config_entries = ConfigEntries(hass, {}) await area_registry.async_load(hass) await device_registry.async_load(hass) await entity_registry.async_load(hass) From e2327622c36316a653d6f38cb7264572ded42ac1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 Jun 2022 15:02:50 +0200 Subject: [PATCH 1538/3516] Migrate SNMP sensor to TemplateEntity (#73324) --- homeassistant/components/snmp/sensor.py | 51 +++++++--------- requirements_test_all.txt | 3 + tests/components/snmp/__init__.py | 1 + tests/components/snmp/test_sensor.py | 79 +++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 30 deletions(-) create mode 100644 tests/components/snmp/__init__.py create mode 100644 tests/components/snmp/test_sensor.py diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index ba111ffc9bc..11f8c7d2f64 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -17,12 +17,11 @@ from pysnmp.hlapi.asyncio import ( ) import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, - CONF_NAME, CONF_PORT, - CONF_UNIT_OF_MEASUREMENT, + CONF_UNIQUE_ID, CONF_USERNAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, @@ -30,6 +29,10 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.template_entity import ( + TEMPLATE_SENSOR_BASE_SCHEMA, + TemplateSensor, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import ( @@ -66,9 +69,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_COMMUNITY, default=DEFAULT_COMMUNITY): cv.string, vol.Optional(CONF_DEFAULT_VALUE): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): vol.In(SNMP_VERSIONS), vol.Optional(CONF_USERNAME): cv.string, @@ -81,7 +82,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( MAP_PRIV_PROTOCOLS ), } -) +).extend(TEMPLATE_SENSOR_BASE_SCHEMA.schema) async def async_setup_platform( @@ -91,12 +92,10 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the SNMP sensor.""" - name = config.get(CONF_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) community = config.get(CONF_COMMUNITY) baseoid = config.get(CONF_BASEOID) - unit = config.get(CONF_UNIT_OF_MEASUREMENT) version = config[CONF_VERSION] username = config.get(CONF_USERNAME) authkey = config.get(CONF_AUTH_KEY) @@ -105,10 +104,7 @@ async def async_setup_platform( privproto = config[CONF_PRIV_PROTOCOL] accept_errors = config.get(CONF_ACCEPT_ERRORS) default_value = config.get(CONF_DEFAULT_VALUE) - value_template = config.get(CONF_VALUE_TEMPLATE) - - if value_template is not None: - value_template.hass = hass + unique_id = config.get(CONF_UNIQUE_ID) if version == "3": @@ -146,35 +142,30 @@ async def async_setup_platform( return data = SnmpData(request_args, baseoid, accept_errors, default_value) - async_add_entities([SnmpSensor(data, name, unit, value_template)], True) + async_add_entities([SnmpSensor(hass, data, config, unique_id)], True) -class SnmpSensor(SensorEntity): +class SnmpSensor(TemplateSensor): """Representation of a SNMP sensor.""" - def __init__(self, data, name, unit_of_measurement, value_template): - """Initialize the sensor.""" - self.data = data - self._name = name - self._state = None - self._unit_of_measurement = unit_of_measurement - self._value_template = value_template + _attr_should_poll = True - @property - def name(self): - """Return the name of the sensor.""" - return self._name + def __init__(self, hass, data, config, unique_id): + """Initialize the sensor.""" + super().__init__( + hass, config=config, unique_id=unique_id, fallback_name=DEFAULT_NAME + ) + self.data = data + self._state = None + self._value_template = config.get(CONF_VALUE_TEMPLATE) + if (value_template := self._value_template) is not None: + value_template.hass = hass @property def native_value(self): """Return the state of the sensor.""" return self._state - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit_of_measurement - async def async_update(self): """Get the latest data and updates the states.""" await self.data.async_update() diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e75320d28fa..b696ce6829e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1241,6 +1241,9 @@ pysmartapp==0.3.3 # homeassistant.components.smartthings pysmartthings==0.7.6 +# homeassistant.components.snmp +pysnmplib==5.0.15 + # homeassistant.components.soma pysoma==0.0.10 diff --git a/tests/components/snmp/__init__.py b/tests/components/snmp/__init__.py new file mode 100644 index 00000000000..e3890bb18fe --- /dev/null +++ b/tests/components/snmp/__init__.py @@ -0,0 +1 @@ +"""Tests for the SNMP integration.""" diff --git a/tests/components/snmp/test_sensor.py b/tests/components/snmp/test_sensor.py new file mode 100644 index 00000000000..9f3a555c2d9 --- /dev/null +++ b/tests/components/snmp/test_sensor.py @@ -0,0 +1,79 @@ +"""SNMP sensor tests.""" + +from unittest.mock import MagicMock, Mock, patch + +import pytest + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + + +@pytest.fixture(autouse=True) +def hlapi_mock(): + """Mock out 3rd party API.""" + mock_data = MagicMock() + mock_data.prettyPrint = Mock(return_value="hello") + with patch( + "homeassistant.components.snmp.sensor.getCmd", + return_value=(None, None, None, [[mock_data]]), + ): + yield + + +async def test_basic_config(hass: HomeAssistant) -> None: + """Test basic entity configuration.""" + + config = { + SENSOR_DOMAIN: { + "platform": "snmp", + "host": "192.168.1.32", + "baseoid": "1.3.6.1.4.1.2021.10.1.3.1", + }, + } + + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.snmp") + assert state.state == "hello" + assert state.attributes == {"friendly_name": "SNMP"} + + +async def test_entity_config(hass: HomeAssistant) -> None: + """Test entity configuration.""" + + config = { + SENSOR_DOMAIN: { + # SNMP configuration + "platform": "snmp", + "host": "192.168.1.32", + "baseoid": "1.3.6.1.4.1.2021.10.1.3.1", + # Entity configuration + "icon": "{{'mdi:one_two_three'}}", + "picture": "{{'blabla.png'}}", + "device_class": "temperature", + "name": "{{'SNMP' + ' ' + 'Sensor'}}", + "state_class": "measurement", + "unique_id": "very_unique", + "unit_of_measurement": "beardsecond", + }, + } + + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + assert entity_registry.async_get("sensor.snmp_sensor").unique_id == "very_unique" + + state = hass.states.get("sensor.snmp_sensor") + assert state.state == "hello" + assert state.attributes == { + "device_class": "temperature", + "entity_picture": "blabla.png", + "friendly_name": "SNMP Sensor", + "icon": "mdi:one_two_three", + "state_class": "measurement", + "unit_of_measurement": "beardsecond", + } From 3e1a4d86a3f7165dd60344348cf21e4a769fe8c1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 16 Jun 2022 16:35:00 +0200 Subject: [PATCH 1539/3516] Fix modification of mutable global in xiaomi_miio number (#73579) --- homeassistant/components/xiaomi_miio/number.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 02855a89c1f..7fd5347f432 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -1,6 +1,7 @@ """Motor speed support for Xiaomi Mi Air Humidifier.""" from __future__ import annotations +import dataclasses from dataclasses import dataclass from homeassistant.components.number import NumberEntity, NumberEntityDescription @@ -273,9 +274,12 @@ async def async_setup_entry( description.key == ATTR_OSCILLATION_ANGLE and model in OSCILLATION_ANGLE_VALUES ): - description.max_value = OSCILLATION_ANGLE_VALUES[model].max_value - description.min_value = OSCILLATION_ANGLE_VALUES[model].min_value - description.step = OSCILLATION_ANGLE_VALUES[model].step + description = dataclasses.replace( + description, + native_max_value=OSCILLATION_ANGLE_VALUES[model].max_value, + native_min_value=OSCILLATION_ANGLE_VALUES[model].min_value, + native_step=OSCILLATION_ANGLE_VALUES[model].step, + ) entities.append( XiaomiNumberEntity( From f7945cdc647ef4cf4de0f642be58e77b46b2456b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 16 Jun 2022 16:43:09 +0200 Subject: [PATCH 1540/3516] Add build musllinux wheel (#73587) * Add build musllinux wheel * cleanup --- .github/workflows/wheels.yml | 118 +++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 605820efb33..602a8a78d8c 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -102,6 +102,54 @@ jobs: requirements-diff: "requirements_diff.txt" requirements: "requirements.txt" + core_musllinux: + name: Build musllinux wheels with musllinux_1_2 / cp310 at ${{ matrix.arch }} for core + if: github.repository_owner == 'home-assistant' + needs: init + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + arch: ${{ fromJson(needs.init.outputs.architectures) }} + steps: + - name: Checkout the repository + uses: actions/checkout@v3.0.2 + + - name: Download env_file + uses: actions/download-artifact@v3 + with: + name: env_file + + - name: Download requirements_diff + uses: actions/download-artifact@v3 + with: + name: requirements_diff + + - name: Adjust ENV / CP310 + run: | + if [ "${{ matrix.arch }}" = "i386" ]; + echo "NPY_DISABLE_SVML=1" >> .env_file + fi + + requirement_files="requirements_all.txt requirements_diff.txt" + for requirement_file in ${requirement_files}; do + sed -i "s|numpy==1.21.6|numpy==1.22.4|g" ${requirement_file} + done + + - name: Build wheels + uses: home-assistant/wheels@2022.06.1 + with: + abi: cp310 + tag: musllinux_1_2 + arch: ${{ matrix.arch }} + wheels-key: ${{ secrets.WHEELS_KEY }} + env-file: true + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev" + skip-binary: aiohttp + constraints: "homeassistant/package_constraints.txt" + requirements-diff: "requirements_diff.txt" + requirements: "requirements.txt" + integrations: name: Build wheels with ${{ matrix.tag }} (${{ matrix.arch }}) for integrations if: github.repository_owner == 'home-assistant' @@ -164,3 +212,73 @@ jobs: constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements_all.txt" + + integrations_musllinux: + name: Build musllinux wheels with musllinux_1_2 / cp310 at ${{ matrix.arch }} for integrations + if: github.repository_owner == 'home-assistant' + needs: init + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + arch: ${{ fromJson(needs.init.outputs.architectures) }} + steps: + - name: Checkout the repository + uses: actions/checkout@v3.0.2 + + - name: Download env_file + uses: actions/download-artifact@v3 + with: + name: env_file + + - name: Download requirements_diff + uses: actions/download-artifact@v3 + with: + name: requirements_diff + + - name: Uncomment packages + run: | + requirement_files="requirements_all.txt requirements_diff.txt" + for requirement_file in ${requirement_files}; do + sed -i "s|# pybluez|pybluez|g" ${requirement_file} + sed -i "s|# bluepy|bluepy|g" ${requirement_file} + sed -i "s|# beacontools|beacontools|g" ${requirement_file} + sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file} + sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file} + sed -i "s|# evdev|evdev|g" ${requirement_file} + sed -i "s|# python-eq3bt|python-eq3bt|g" ${requirement_file} + sed -i "s|# pycups|pycups|g" ${requirement_file} + sed -i "s|# homekit|homekit|g" ${requirement_file} + sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file} + sed -i "s|# decora|decora|g" ${requirement_file} + sed -i "s|# avion|avion|g" ${requirement_file} + sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file} + sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file} + sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} + sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} + done + + - name: Adjust ENV / CP310 + run: | + if [ "${{ matrix.arch }}" = "i386" ]; + echo "NPY_DISABLE_SVML=1" >> .env_file + fi + + requirement_files="requirements_all.txt requirements_diff.txt" + for requirement_file in ${requirement_files}; do + sed -i "s|numpy==1.21.6|numpy==1.22.4|g" ${requirement_file} + done + + - name: Build wheels + uses: home-assistant/wheels@2022.06.1 + with: + abi: cp310 + tag: musllinux_1_2 + arch: ${{ matrix.arch }} + wheels-key: ${{ secrets.WHEELS_KEY }} + env-file: true + apk: "libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev" + skip-binary: aiohttp;grpcio + constraints: "homeassistant/package_constraints.txt" + requirements-diff: "requirements_diff.txt" + requirements: "requirements_all.txt" From 63ff3f87dc61a135daba5e1a2577fcb2e6d93eee Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 16 Jun 2022 17:00:36 +0200 Subject: [PATCH 1541/3516] Fix wheel pipeline (#73594) --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 602a8a78d8c..d9bd606bf85 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -127,7 +127,7 @@ jobs: - name: Adjust ENV / CP310 run: | - if [ "${{ matrix.arch }}" = "i386" ]; + if [ "${{ matrix.arch }}" = "i386" ]; then echo "NPY_DISABLE_SVML=1" >> .env_file fi @@ -260,7 +260,7 @@ jobs: - name: Adjust ENV / CP310 run: | - if [ "${{ matrix.arch }}" = "i386" ]; + if [ "${{ matrix.arch }}" = "i386" ]; then echo "NPY_DISABLE_SVML=1" >> .env_file fi From 9687aab802e1e4377ed0bd3e73c62f47244fb98f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 16 Jun 2022 17:17:30 +0200 Subject: [PATCH 1542/3516] Add yaml-dev core wheel apk (#73597) --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d9bd606bf85..c499f7ee1a4 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -144,7 +144,7 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev" + apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;yaml-dev" skip-binary: aiohttp constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" From 01a4a83babd19e1316c1770b65a2b12803ab1837 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 17 Jun 2022 01:48:52 +1000 Subject: [PATCH 1543/3516] Improve stream playback on high latency cameras (#72547) * Disable LL-HLS for HLS sources * Add extra wait for Nest cameras --- homeassistant/components/camera/__init__.py | 2 +- .../components/generic/config_flow.py | 2 +- homeassistant/components/nest/camera_sdm.py | 2 + homeassistant/components/stream/__init__.py | 72 ++++++++++++------- homeassistant/components/stream/const.py | 1 + homeassistant/components/stream/core.py | 19 ++--- homeassistant/components/stream/hls.py | 28 +++++--- homeassistant/components/stream/recorder.py | 11 ++- homeassistant/components/stream/worker.py | 25 ++++--- tests/components/stream/test_ll_hls.py | 4 +- tests/components/stream/test_worker.py | 48 ++++++++++--- 11 files changed, 145 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 2ed8b58232d..45b77ec1bd6 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -454,7 +454,7 @@ class Camera(Entity): def __init__(self) -> None: """Initialize a camera.""" self.stream: Stream | None = None - self.stream_options: dict[str, str | bool] = {} + self.stream_options: dict[str, str | bool | float] = {} self.content_type: str = DEFAULT_CONTENT_TYPE self.access_tokens: collections.deque = collections.deque([], 2) self._warned_old_signature = False diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 93b34133c63..3cc78fca406 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -209,7 +209,7 @@ async def async_test_stream(hass, info) -> dict[str, str]: except TemplateError as err: _LOGGER.warning("Problem rendering template %s: %s", stream_source, err) return {CONF_STREAM_SOURCE: "template_error"} - stream_options: dict[str, bool | str] = {} + stream_options: dict[str, str | bool | float] = {} if rtsp_transport := info.get(CONF_RTSP_TRANSPORT): stream_options[CONF_RTSP_TRANSPORT] = rtsp_transport if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 61f8ead4ea3..a089163a826 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -20,6 +20,7 @@ from google_nest_sdm.exceptions import ApiException from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.components.camera.const import StreamType +from homeassistant.components.stream import CONF_EXTRA_PART_WAIT_TIME from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -67,6 +68,7 @@ class NestCamera(Camera): self._create_stream_url_lock = asyncio.Lock() self._stream_refresh_unsub: Callable[[], None] | None = None self._attr_is_streaming = CameraLiveStreamTrait.NAME in self._device.traits + self.stream_options[CONF_EXTRA_PART_WAIT_TIME] = 3 @property def should_poll(self) -> bool: diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index c33188fd71c..19ef009a845 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -18,6 +18,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Mapping +import copy import logging import re import secrets @@ -38,6 +39,7 @@ from .const import ( ATTR_ENDPOINTS, ATTR_SETTINGS, ATTR_STREAMS, + CONF_EXTRA_PART_WAIT_TIME, CONF_LL_HLS, CONF_PART_DURATION, CONF_RTSP_TRANSPORT, @@ -62,8 +64,11 @@ from .diagnostics import Diagnostics from .hls import HlsStreamOutput, async_setup_hls __all__ = [ + "ATTR_SETTINGS", + "CONF_EXTRA_PART_WAIT_TIME", "CONF_RTSP_TRANSPORT", "CONF_USE_WALLCLOCK_AS_TIMESTAMPS", + "DOMAIN", "FORMAT_CONTENT_TYPE", "HLS_PROVIDER", "OUTPUT_FORMATS", @@ -91,7 +96,7 @@ def redact_credentials(data: str) -> str: def create_stream( hass: HomeAssistant, stream_source: str, - options: dict[str, str | bool], + options: Mapping[str, str | bool | float], stream_label: str | None = None, ) -> Stream: """Create a stream with the specified identfier based on the source url. @@ -101,11 +106,35 @@ def create_stream( The stream_label is a string used as an additional message in logging. """ + + def convert_stream_options( + hass: HomeAssistant, stream_options: Mapping[str, str | bool | float] + ) -> tuple[dict[str, str], StreamSettings]: + """Convert options from stream options into PyAV options and stream settings.""" + stream_settings = copy.copy(hass.data[DOMAIN][ATTR_SETTINGS]) + pyav_options: dict[str, str] = {} + try: + STREAM_OPTIONS_SCHEMA(stream_options) + except vol.Invalid as exc: + raise HomeAssistantError("Invalid stream options") from exc + + if extra_wait_time := stream_options.get(CONF_EXTRA_PART_WAIT_TIME): + stream_settings.hls_part_timeout += extra_wait_time + if rtsp_transport := stream_options.get(CONF_RTSP_TRANSPORT): + assert isinstance(rtsp_transport, str) + # The PyAV options currently match the stream CONF constants, but this + # will not necessarily always be the case, so they are hard coded here + pyav_options["rtsp_transport"] = rtsp_transport + if stream_options.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): + pyav_options["use_wallclock_as_timestamps"] = "1" + + return pyav_options, stream_settings + if DOMAIN not in hass.config.components: raise HomeAssistantError("Stream integration is not set up.") - # Convert extra stream options into PyAV options - pyav_options = convert_stream_options(options) + # Convert extra stream options into PyAV options and stream settings + pyav_options, stream_settings = convert_stream_options(hass, options) # For RTSP streams, prefer TCP if isinstance(stream_source, str) and stream_source[:7] == "rtsp://": pyav_options = { @@ -115,7 +144,11 @@ def create_stream( } stream = Stream( - hass, stream_source, options=pyav_options, stream_label=stream_label + hass, + stream_source, + pyav_options=pyav_options, + stream_settings=stream_settings, + stream_label=stream_label, ) hass.data[DOMAIN][ATTR_STREAMS].append(stream) return stream @@ -230,13 +263,15 @@ class Stream: self, hass: HomeAssistant, source: str, - options: dict[str, str], + pyav_options: dict[str, str], + stream_settings: StreamSettings, stream_label: str | None = None, ) -> None: """Initialize a stream.""" self.hass = hass self.source = source - self.options = options + self.pyav_options = pyav_options + self._stream_settings = stream_settings self._stream_label = stream_label self.keepalive = False self.access_token: str | None = None @@ -284,7 +319,9 @@ class Stream: self.check_idle() provider = PROVIDERS[fmt]( - self.hass, IdleTimer(self.hass, timeout, idle_callback) + self.hass, + IdleTimer(self.hass, timeout, idle_callback), + self._stream_settings, ) self._outputs[fmt] = provider @@ -368,7 +405,8 @@ class Stream: try: stream_worker( self.source, - self.options, + self.pyav_options, + self._stream_settings, stream_state, self._keyframe_converter, self._thread_quit, @@ -507,22 +545,6 @@ STREAM_OPTIONS_SCHEMA: Final = vol.Schema( { vol.Optional(CONF_RTSP_TRANSPORT): vol.In(RTSP_TRANSPORTS), vol.Optional(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): bool, + vol.Optional(CONF_EXTRA_PART_WAIT_TIME): cv.positive_float, } ) - - -def convert_stream_options(stream_options: dict[str, str | bool]) -> dict[str, str]: - """Convert options from stream options into PyAV options.""" - pyav_options: dict[str, str] = {} - try: - STREAM_OPTIONS_SCHEMA(stream_options) - except vol.Invalid as exc: - raise HomeAssistantError("Invalid stream options") from exc - - if rtsp_transport := stream_options.get(CONF_RTSP_TRANSPORT): - assert isinstance(rtsp_transport, str) - pyav_options["rtsp_transport"] = rtsp_transport - if stream_options.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): - pyav_options["use_wallclock_as_timestamps"] = "1" - - return pyav_options diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py index f8c9ba85d59..35af633435e 100644 --- a/homeassistant/components/stream/const.py +++ b/homeassistant/components/stream/const.py @@ -53,3 +53,4 @@ RTSP_TRANSPORTS = { "http": "HTTP", } CONF_USE_WALLCLOCK_AS_TIMESTAMPS = "use_wallclock_as_timestamps" +CONF_EXTRA_PART_WAIT_TIME = "extra_part_wait_time" diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index da18a5a6a08..c8d831157a8 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -118,6 +118,10 @@ class Segment: if self.hls_playlist_complete: return self.hls_playlist_template[0] if not self.hls_playlist_template: + # Logically EXT-X-DISCONTINUITY makes sense above the parts, but Apple's + # media stream validator seems to only want it before the segment + if last_stream_id != self.stream_id: + self.hls_playlist_template.append("#EXT-X-DISCONTINUITY") # This is a placeholder where the rendered parts will be inserted self.hls_playlist_template.append("{}") if render_parts: @@ -133,22 +137,19 @@ class Segment: # the first element to avoid an extra newline when we don't render any parts. # Append an empty string to create a trailing newline when we do render parts self.hls_playlist_parts.append("") - self.hls_playlist_template = [] - # Logically EXT-X-DISCONTINUITY would make sense above the parts, but Apple's - # media stream validator seems to only want it before the segment - if last_stream_id != self.stream_id: - self.hls_playlist_template.append("#EXT-X-DISCONTINUITY") + self.hls_playlist_template = ( + [] if last_stream_id == self.stream_id else ["#EXT-X-DISCONTINUITY"] + ) # Add the remaining segment metadata + # The placeholder goes on the same line as the next element self.hls_playlist_template.extend( [ - "#EXT-X-PROGRAM-DATE-TIME:" + "{}#EXT-X-PROGRAM-DATE-TIME:" + self.start_time.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", f"#EXTINF:{self.duration:.3f},\n./segment/{self.sequence}.m4s", ] ) - # The placeholder now goes on the same line as the first element - self.hls_playlist_template[0] = "{}" + self.hls_playlist_template[0] # Store intermediate playlist data in member variables for reuse self.hls_playlist_template = ["\n".join(self.hls_playlist_template)] @@ -237,11 +238,13 @@ class StreamOutput: self, hass: HomeAssistant, idle_timer: IdleTimer, + stream_settings: StreamSettings, deque_maxlen: int | None = None, ) -> None: """Initialize a stream output.""" self._hass = hass self.idle_timer = idle_timer + self.stream_settings = stream_settings self._event = asyncio.Event() self._part_event = asyncio.Event() self._segments: deque[Segment] = deque(maxlen=deque_maxlen) diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 8e78093d07a..efecdcbe9dc 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -9,8 +9,6 @@ from aiohttp import web from homeassistant.core import HomeAssistant, callback from .const import ( - ATTR_SETTINGS, - DOMAIN, EXT_X_START_LL_HLS, EXT_X_START_NON_LL_HLS, FORMAT_CONTENT_TYPE, @@ -47,11 +45,15 @@ def async_setup_hls(hass: HomeAssistant) -> str: class HlsStreamOutput(StreamOutput): """Represents HLS Output formats.""" - def __init__(self, hass: HomeAssistant, idle_timer: IdleTimer) -> None: + def __init__( + self, + hass: HomeAssistant, + idle_timer: IdleTimer, + stream_settings: StreamSettings, + ) -> None: """Initialize HLS output.""" - super().__init__(hass, idle_timer, deque_maxlen=MAX_SEGMENTS) - self.stream_settings: StreamSettings = hass.data[DOMAIN][ATTR_SETTINGS] - self._target_duration = self.stream_settings.min_segment_duration + super().__init__(hass, idle_timer, stream_settings, deque_maxlen=MAX_SEGMENTS) + self._target_duration = stream_settings.min_segment_duration @property def name(self) -> str: @@ -78,14 +80,20 @@ class HlsStreamOutput(StreamOutput): ) def discontinuity(self) -> None: - """Remove incomplete segment from deque.""" + """Fix incomplete segment at end of deque.""" self._hass.loop.call_soon_threadsafe(self._async_discontinuity) @callback def _async_discontinuity(self) -> None: - """Remove incomplete segment from deque in event loop.""" - if self._segments and not self._segments[-1].complete: - self._segments.pop() + """Fix incomplete segment at end of deque in event loop.""" + # Fill in the segment duration or delete the segment if empty + if self._segments: + if (last_segment := self._segments[-1]).parts: + last_segment.duration = sum( + part.duration for part in last_segment.parts + ) + else: + self._segments.pop() class HlsMasterPlaylistView(StreamView): diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index ae1c64396c8..4d97c0d683d 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -17,7 +17,7 @@ from .const import ( RECORDER_PROVIDER, SEGMENT_CONTAINER_FORMAT, ) -from .core import PROVIDERS, IdleTimer, Segment, StreamOutput +from .core import PROVIDERS, IdleTimer, Segment, StreamOutput, StreamSettings _LOGGER = logging.getLogger(__name__) @@ -121,9 +121,14 @@ def recorder_save_worker(file_out: str, segments: deque[Segment]) -> None: class RecorderOutput(StreamOutput): """Represents HLS Output formats.""" - def __init__(self, hass: HomeAssistant, idle_timer: IdleTimer) -> None: + def __init__( + self, + hass: HomeAssistant, + idle_timer: IdleTimer, + stream_settings: StreamSettings, + ) -> None: """Initialize recorder output.""" - super().__init__(hass, idle_timer) + super().__init__(hass, idle_timer, stream_settings) self.video_path: str @property diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index f8d12c1cb44..4cfe8864de0 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -16,9 +16,7 @@ from homeassistant.core import HomeAssistant from . import redact_credentials from .const import ( - ATTR_SETTINGS, AUDIO_CODECS, - DOMAIN, HLS_PROVIDER, MAX_MISSING_DTS, MAX_TIMESTAMP_GAP, @@ -87,7 +85,7 @@ class StreamState: # simple to check for discontinuity at output time, and to determine # the discontinuity sequence number. self._stream_id += 1 - # Call discontinuity to remove incomplete segment from the HLS output + # Call discontinuity to fix incomplete segment in HLS output if hls_output := self._outputs_callback().get(HLS_PROVIDER): cast(HlsStreamOutput, hls_output).discontinuity() @@ -111,6 +109,7 @@ class StreamMuxer: video_stream: av.video.VideoStream, audio_stream: av.audio.stream.AudioStream | None, stream_state: StreamState, + stream_settings: StreamSettings, ) -> None: """Initialize StreamMuxer.""" self._hass = hass @@ -126,7 +125,7 @@ class StreamMuxer: self._memory_file_pos: int = cast(int, None) self._part_start_dts: int = cast(int, None) self._part_has_keyframe = False - self._stream_settings: StreamSettings = hass.data[DOMAIN][ATTR_SETTINGS] + self._stream_settings = stream_settings self._stream_state = stream_state self._start_time = datetime.datetime.utcnow() @@ -445,19 +444,20 @@ def unsupported_audio(packets: Iterator[av.Packet], audio_stream: Any) -> bool: def stream_worker( source: str, - options: dict[str, str], + pyav_options: dict[str, str], + stream_settings: StreamSettings, stream_state: StreamState, keyframe_converter: KeyFrameConverter, quit_event: Event, ) -> None: """Handle consuming streams.""" - if av.library_versions["libavformat"][0] >= 59 and "stimeout" in options: + if av.library_versions["libavformat"][0] >= 59 and "stimeout" in pyav_options: # the stimeout option was renamed to timeout as of ffmpeg 5.0 - options["timeout"] = options["stimeout"] - del options["stimeout"] + pyav_options["timeout"] = pyav_options["stimeout"] + del pyav_options["stimeout"] try: - container = av.open(source, options=options, timeout=SOURCE_TIMEOUT) + container = av.open(source, options=pyav_options, timeout=SOURCE_TIMEOUT) except av.AVError as err: raise StreamWorkerError( f"Error opening stream ({err.type}, {err.strerror}) {redact_credentials(str(source))}" @@ -480,6 +480,9 @@ def stream_worker( # Some audio streams do not have a profile and throw errors when remuxing if audio_stream and audio_stream.profile is None: audio_stream = None + # Disable ll-hls for hls inputs + if container.format.name == "hls": + stream_settings.ll_hls = False stream_state.diagnostics.set_value("container_format", container.format.name) stream_state.diagnostics.set_value("video_codec", video_stream.name) if audio_stream: @@ -535,7 +538,9 @@ def stream_worker( "Error demuxing stream while finding first packet: %s" % str(ex) ) from ex - muxer = StreamMuxer(stream_state.hass, video_stream, audio_stream, stream_state) + muxer = StreamMuxer( + stream_state.hass, video_stream, audio_stream, stream_state, stream_settings + ) muxer.reset(start_dts) # Mux the first keyframe, then proceed through the rest of the packets diff --git a/tests/components/stream/test_ll_hls.py b/tests/components/stream/test_ll_hls.py index 4aaec93d646..447b9ff58e9 100644 --- a/tests/components/stream/test_ll_hls.py +++ b/tests/components/stream/test_ll_hls.py @@ -91,12 +91,12 @@ def make_segment_with_parts( ): """Create a playlist response for a segment including part segments.""" response = [] + if discontinuity: + response.append("#EXT-X-DISCONTINUITY") for i in range(num_parts): response.append( f'#EXT-X-PART:DURATION={TEST_PART_DURATION:.3f},URI="./segment/{segment}.{i}.m4s"{",INDEPENDENT=YES" if i%independent_period==0 else ""}' ) - if discontinuity: - response.append("#EXT-X-DISCONTINUITY") response.extend( [ "#EXT-X-PROGRAM-DATE-TIME:" diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index a70f2be81b8..298d7287e69 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -268,17 +268,24 @@ class MockPyAv: return self.container -def run_worker(hass, stream, stream_source): +def run_worker(hass, stream, stream_source, stream_settings=None): """Run the stream worker under test.""" stream_state = StreamState(hass, stream.outputs, stream._diagnostics) stream_worker( - stream_source, {}, stream_state, KeyFrameConverter(hass), threading.Event() + stream_source, + {}, + stream_settings or hass.data[DOMAIN][ATTR_SETTINGS], + stream_state, + KeyFrameConverter(hass), + threading.Event(), ) -async def async_decode_stream(hass, packets, py_av=None): +async def async_decode_stream(hass, packets, py_av=None, stream_settings=None): """Start a stream worker that decodes incoming stream packets into output segments.""" - stream = Stream(hass, STREAM_SOURCE, {}) + stream = Stream( + hass, STREAM_SOURCE, {}, stream_settings or hass.data[DOMAIN][ATTR_SETTINGS] + ) stream.add_provider(HLS_PROVIDER) if not py_av: @@ -290,7 +297,7 @@ async def async_decode_stream(hass, packets, py_av=None): side_effect=py_av.capture_buffer.capture_output_segment, ): try: - run_worker(hass, stream, STREAM_SOURCE) + run_worker(hass, stream, STREAM_SOURCE, stream_settings) except StreamEndedError: # Tests only use a limited number of packets, then the worker exits as expected. In # production, stream ending would be unexpected. @@ -304,7 +311,7 @@ async def async_decode_stream(hass, packets, py_av=None): async def test_stream_open_fails(hass): """Test failure on stream open.""" - stream = Stream(hass, STREAM_SOURCE, {}) + stream = Stream(hass, STREAM_SOURCE, {}, hass.data[DOMAIN][ATTR_SETTINGS]) stream.add_provider(HLS_PROVIDER) with patch("av.open") as av_open, pytest.raises(StreamWorkerError): av_open.side_effect = av.error.InvalidDataError(-2, "error") @@ -637,7 +644,7 @@ async def test_stream_stopped_while_decoding(hass): worker_open = threading.Event() worker_wake = threading.Event() - stream = Stream(hass, STREAM_SOURCE, {}) + stream = Stream(hass, STREAM_SOURCE, {}, hass.data[DOMAIN][ATTR_SETTINGS]) stream.add_provider(HLS_PROVIDER) py_av = MockPyAv() @@ -667,7 +674,7 @@ async def test_update_stream_source(hass): worker_open = threading.Event() worker_wake = threading.Event() - stream = Stream(hass, STREAM_SOURCE, {}) + stream = Stream(hass, STREAM_SOURCE, {}, hass.data[DOMAIN][ATTR_SETTINGS]) stream.add_provider(HLS_PROVIDER) # Note that retries are disabled by default in tests, however the stream is "restarted" when # the stream source is updated. @@ -709,7 +716,9 @@ async def test_update_stream_source(hass): async def test_worker_log(hass, caplog): """Test that the worker logs the url without username and password.""" - stream = Stream(hass, "https://abcd:efgh@foo.bar", {}) + stream = Stream( + hass, "https://abcd:efgh@foo.bar", {}, hass.data[DOMAIN][ATTR_SETTINGS] + ) stream.add_provider(HLS_PROVIDER) with patch("av.open") as av_open, pytest.raises(StreamWorkerError) as err: @@ -906,3 +915,24 @@ async def test_get_image(hass, record_worker_sync): assert await stream.async_get_image() == EMPTY_8_6_JPEG await stream.stop() + + +async def test_worker_disable_ll_hls(hass): + """Test that the worker disables ll-hls for hls inputs.""" + stream_settings = StreamSettings( + ll_hls=True, + min_segment_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS + - SEGMENT_DURATION_ADJUSTER, + part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS, + hls_advance_part_limit=3, + hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS, + ) + py_av = MockPyAv() + py_av.container.format.name = "hls" + await async_decode_stream( + hass, + PacketSequence(TEST_SEQUENCE_LENGTH), + py_av=py_av, + stream_settings=stream_settings, + ) + assert stream_settings.ll_hls is False From 187d56b88ba5a5f5656937d6c6a687e2de96d600 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 16 Jun 2022 20:12:30 +0200 Subject: [PATCH 1544/3516] Add ability to run plugin on unannotated functions (#73520) * Add ability to run plugin on unannotated functions * Use options * Adjust help text * Add test for the option --- pylint/plugins/hass_enforce_type_hints.py | 19 ++++++++++-- tests/pylint/test_enforce_type_hints.py | 37 ++++++++++++++++++++++- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 8194bb72ca5..fc7be8ba8e8 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -588,7 +588,18 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] "Used when method return type is incorrect", ), } - options = () + options = ( + ( + "ignore-missing-annotations", + { + "default": True, + "type": "yn", + "metavar": "", + "help": "Set to ``no`` if you wish to check functions that do not " + "have any type hints.", + }, + ), + ) def __init__(self, linter: PyLinter | None = None) -> None: super().__init__(linter) @@ -641,7 +652,11 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] def _check_function(self, node: nodes.FunctionDef, match: TypeHintMatch) -> None: # Check that at least one argument is annotated. annotations = _get_all_annotations(node) - if node.returns is None and not _has_valid_annotations(annotations): + if ( + self.linter.config.ignore_missing_annotations + and node.returns is None + and not _has_valid_annotations(annotations) + ): return # Check that all arguments are correctly annotated. diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 86b06a894d0..c07014add7f 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -116,7 +116,7 @@ def test_regex_a_or_b( """ ], ) -def test_ignore_not_annotations( +def test_ignore_no_annotations( hass_enforce_type_hints: ModuleType, type_hint_checker: BaseChecker, code: str ) -> None: """Ensure that _is_valid_type is not run if there are no annotations.""" @@ -133,6 +133,41 @@ def test_ignore_not_annotations( is_valid_type.assert_not_called() +@pytest.mark.parametrize( + "code", + [ + """ + async def setup( #@ + arg1, arg2 + ): + pass + """ + ], +) +def test_bypass_ignore_no_annotations( + hass_enforce_type_hints: ModuleType, type_hint_checker: BaseChecker, code: str +) -> None: + """Test `ignore-missing-annotations` option. + + Ensure that `_is_valid_type` is run if there are no annotations + but `ignore-missing-annotations` option is forced to False. + """ + # Set bypass option + type_hint_checker.config.ignore_missing_annotations = False + + func_node = astroid.extract_node( + code, + "homeassistant.components.pylint_test", + ) + type_hint_checker.visit_module(func_node.parent) + + with patch.object( + hass_enforce_type_hints, "_is_valid_type", return_value=True + ) as is_valid_type: + type_hint_checker.visit_asyncfunctiondef(func_node) + is_valid_type.assert_called() + + @pytest.mark.parametrize( "code", [ From ea716307689310322dd42038b065bcba39f34a9d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 16 Jun 2022 22:19:47 +0200 Subject: [PATCH 1545/3516] Musllinux legacy resolver & cargo git (#73614) --- .github/workflows/wheels.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index c499f7ee1a4..ef037ef9fb8 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -47,6 +47,9 @@ jobs: # execinfo-dev when building wheels. The setuptools build setup does not have an option for # adding a single LDFLAG so copy all relevant linux flags here (as of 1.43.0) echo "GRPC_PYTHON_LDFLAGS=-lpthread -Wl,-wrap,memcpy -static-libgcc -lexecinfo" + + # Fix out of memory issues with rust + echo "CARGO_NET_GIT_FETCH_WITH_CLI=true" ) > .env_file - name: Upload env_file @@ -137,7 +140,7 @@ jobs: done - name: Build wheels - uses: home-assistant/wheels@2022.06.1 + uses: home-assistant/wheels@2022.06.2 with: abi: cp310 tag: musllinux_1_2 @@ -270,7 +273,7 @@ jobs: done - name: Build wheels - uses: home-assistant/wheels@2022.06.1 + uses: home-assistant/wheels@2022.06.2 with: abi: cp310 tag: musllinux_1_2 @@ -279,6 +282,7 @@ jobs: env-file: true apk: "libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev" skip-binary: aiohttp;grpcio + legacy: true constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements_all.txt" From 1c6337d548fb54b07245c3832dad3043c97a94bf Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 16 Jun 2022 22:47:02 +0200 Subject: [PATCH 1546/3516] Update wheels builder to 2022.06.3 (#73615) --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index ef037ef9fb8..0c14459b487 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -140,7 +140,7 @@ jobs: done - name: Build wheels - uses: home-assistant/wheels@2022.06.2 + uses: home-assistant/wheels@2022.06.3 with: abi: cp310 tag: musllinux_1_2 @@ -273,7 +273,7 @@ jobs: done - name: Build wheels - uses: home-assistant/wheels@2022.06.2 + uses: home-assistant/wheels@2022.06.3 with: abi: cp310 tag: musllinux_1_2 From d43178db06cbaa84d88c1da183e0e04af38aee97 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 17 Jun 2022 00:20:34 +0000 Subject: [PATCH 1547/3516] [ci skip] Translation update --- .../eight_sleep/translations/it.json | 19 ++++++++++++ .../components/google/translations/it.json | 3 ++ .../components/nest/translations/ca.json | 9 ++++++ .../components/nest/translations/de.json | 2 +- .../components/nest/translations/fr.json | 23 +++++++++++++- .../components/nest/translations/id.json | 9 ++++++ .../components/nest/translations/it.json | 31 ++++++++++++++++++- 7 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/eight_sleep/translations/it.json diff --git a/homeassistant/components/eight_sleep/translations/it.json b/homeassistant/components/eight_sleep/translations/it.json new file mode 100644 index 00000000000..c849a5f3504 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi al cloud Eight Sleep: {error}" + }, + "error": { + "cannot_connect": "Impossibile connettersi al cloud Eight Sleep: {error}" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/it.json b/homeassistant/components/google/translations/it.json index d57998894d9..a0f9d140329 100644 --- a/homeassistant/components/google/translations/it.json +++ b/homeassistant/components/google/translations/it.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "Segui le [istruzioni]({more_info_url}) per la [Schermata di consenso OAuth]({oauth_consent_url}) per consentire ad Home Assistant di accedere al tuo Google Calendar. Devi anche creare le credenziali dell'applicazione collegate al tuo calendario:\n 1. Vai a [Credenziali]({oauth_creds_url}) e fai clic su **Crea credenziali**.\n 2. Dall'elenco a discesa selezionare **ID client OAuth**.\n 3. Selezionare **TV e dispositivi con ingresso limitato** come Tipo di applicazione. \n\n" + }, "config": { "abort": { "already_configured": "L'account \u00e8 gi\u00e0 configurato", diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index c8d6238e742..c7b617f01d6 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -38,6 +38,15 @@ "create_cloud_project": { "title": "Nest: crea i configura el projecte Cloud" }, + "device_project": { + "data": { + "project_id": "ID de projecte Device Access" + }, + "title": "Nest: crea un projecte Device Access" + }, + "device_project_upgrade": { + "title": "Nest: actualitza projecte Device Access" + }, "init": { "data": { "flow_impl": "Prove\u00efdor" diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index bee1bb547f2..659755fc6b4 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -22,7 +22,7 @@ "subscriber_error": "Unbekannter Abonnentenfehler, siehe Protokolle", "timeout": "Ein zeit\u00fcberschreitungs Fehler ist aufgetreten", "unknown": "Unerwarteter Fehler", - "wrong_project_id": "Bitte gib eine g\u00fcltige Cloud-Projekt-ID ein (gefundene Ger\u00e4tezugriffs-Projekt-ID)" + "wrong_project_id": "Gib eine g\u00fcltige Cloud-Projekt-ID ein (identisch mit der Projekt-ID f\u00fcr den Ger\u00e4tezugriff)." }, "step": { "auth": { diff --git a/homeassistant/components/nest/translations/fr.json b/homeassistant/components/nest/translations/fr.json index d639009dff8..30adccca440 100644 --- a/homeassistant/components/nest/translations/fr.json +++ b/homeassistant/components/nest/translations/fr.json @@ -19,7 +19,7 @@ "subscriber_error": "Erreur d'abonn\u00e9 inconnue, voir les journaux", "timeout": "D\u00e9lai de la validation du code expir\u00e9", "unknown": "Erreur inattendue", - "wrong_project_id": "Veuillez saisir un ID de projet Cloud valide (ID de projet d'acc\u00e8s \u00e0 l'appareil trouv\u00e9)" + "wrong_project_id": "Veuillez saisir un ID de projet Cloud valide (\u00e9tait identique \u00e0 l'ID de projet d'acc\u00e8s \u00e0 l'appareil)" }, "step": { "auth": { @@ -29,6 +29,27 @@ "description": "Pour lier votre compte Google, [autorisez votre compte]( {url} ). \n\n Apr\u00e8s autorisation, copiez-collez le code d'authentification fourni ci-dessous.", "title": "Associer un compte Google" }, + "auth_upgrade": { + "title": "Nest\u00a0: abandon de l'authentification d'application" + }, + "cloud_project": { + "data": { + "cloud_project_id": "ID de projet Google\u00a0Cloud" + }, + "title": "Nest\u00a0: saisissez l'ID du projet Cloud" + }, + "create_cloud_project": { + "title": "Nest\u00a0: cr\u00e9er et configurer un projet Cloud" + }, + "device_project": { + "data": { + "project_id": "ID de projet d'acc\u00e8s \u00e0 l'appareil" + }, + "title": "Nest\u00a0: cr\u00e9er un projet d'acc\u00e8s \u00e0 l'appareil" + }, + "device_project_upgrade": { + "title": "Nest\u00a0: mettre \u00e0 jour le projet d'acc\u00e8s \u00e0 l'appareil" + }, "init": { "data": { "flow_impl": "Fournisseur" diff --git a/homeassistant/components/nest/translations/id.json b/homeassistant/components/nest/translations/id.json index cc7cdc600a8..ab7aaa2d459 100644 --- a/homeassistant/components/nest/translations/id.json +++ b/homeassistant/components/nest/translations/id.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "Ikuti [petunjuk]({more_info_url}) untuk mengonfigurasi Konsol Cloud:\n\n1. Buka [Layar persetujuan OAuth]({oauth_consent_url}) dan konfigurasikan\n1. Buka [Kredentsial]({oauth_creds_url}) dan klik **Buat Kredensial**.\n1. Dari daftar pilihan, pilih **ID klien OAuth**.\n1. Pilih **Aplikasi Web** untuk Jenis Aplikasi.\n1. Tambahkan '{redirect_url}' di bawah *URI pengarahan ulang yand diotorisasi*." + }, "config": { "abort": { "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", @@ -29,6 +32,12 @@ "description": "Untuk menautkan akun Google Anda, [otorisasi akun Anda]({url}).\n\nSetelah otorisasi, salin dan tempel Token Auth yang disediakan di bawah ini.", "title": "Tautkan Akun Google" }, + "cloud_project": { + "title": "Nest: Masukkan ID Proyek Cloud" + }, + "device_project_upgrade": { + "title": "Nest: Perbarui Proyek Akses Perangkat" + }, "init": { "data": { "flow_impl": "Penyedia" diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 5942414bcf3..8fd83483290 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "Segui le [istruzioni]({more_info_url}) per configurare la Cloud Console: \n\n 1. Vai alla [schermata di consenso OAuth]({oauth_consent_url}) e configura\n 2. Vai a [Credenziali]({oauth_creds_url}) e fai clic su **Crea credenziali**.\n 3. Dall'elenco a discesa selezionare **ID client OAuth**.\n 4. Selezionare **Applicazione Web** come Tipo di applicazione.\n 5. Aggiungi `{redirect_url}` sotto *URI di reindirizzamento autorizzato*." + }, "config": { "abort": { "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", @@ -19,7 +22,7 @@ "subscriber_error": "Errore di abbonato sconosciuto, vedere i registri", "timeout": "Tempo scaduto per l'inserimento del codice di convalida", "unknown": "Errore imprevisto", - "wrong_project_id": "Inserisci un ID di progetto Cloud valido (trovato ID di progetto di accesso al dispositivo)" + "wrong_project_id": "Inserisci un ID progetto cloud valido (uguale all'ID progetto di accesso al dispositivo)" }, "step": { "auth": { @@ -29,6 +32,32 @@ "description": "Per collegare l'account Google, [authorize your account]({url}).\n\nDopo l'autorizzazione, copia-incolla il codice PIN fornito.", "title": "Connetti l'account Google" }, + "auth_upgrade": { + "description": "App Auth \u00e8 stato ritirato da Google per migliorare la sicurezza e devi agire creando nuove credenziali per l'applicazione. \n\nApri la [documentazione]({more_info_url}) per seguire i passaggi successivi che ti guideranno attraverso i passaggi necessari per ripristinare l'accesso ai tuoi dispositivi Nest.", + "title": "Nest: ritiro dell'autenticazione dell'app" + }, + "cloud_project": { + "data": { + "cloud_project_id": "ID progetto Google Cloud" + }, + "description": "Immetti l'ID progetto cloud di seguito, ad esempio *example-project-12345*. Consulta la [Google Cloud Console]({cloud_console_url}) o la documentazione per [maggiori informazioni]({more_info_url}).", + "title": "Nest: inserisci l'ID del progetto Cloud" + }, + "create_cloud_project": { + "description": "L'integrazione Nest ti consente di integrare i tuoi termostati, videocamere e campanelli Nest utilizzando l'API Smart Device Management. L'API SDM **richiede una tariffa di configurazione una tantum di US $ 5**. Consulta la documentazione per [maggiori informazioni]({more_info_url}). \n\n 1. Vai a [Google Cloud Console]( {cloud_console_url} ).\n 2. Se questo \u00e8 il tuo primo progetto, fai clic su **Crea progetto** e poi su **Nuovo progetto**.\n 3. Assegna un nome al tuo progetto cloud, quindi fai clic su **Crea**.\n 4. Salva l'ID del progetto cloud, ad es. *example-project-12345*, poich\u00e9 ti servir\u00e0 in seguito\n 5. Vai a Libreria API per [API Smart Device Management]({sdm_api_url}) e fai clic su **Abilita**.\n 6. Vai a Libreria API per [Cloud Pub/Sub API]({pubsub_api_url}) e fai clic su **Abilita**. \n\n Procedi quando il tuo progetto cloud \u00e8 impostato.", + "title": "Nest: crea e configura un progetto cloud" + }, + "device_project": { + "data": { + "project_id": "ID progetto di accesso al dispositivo" + }, + "description": "Crea un progetto di accesso al dispositivo Nest la cui configurazione **richiede una commissione di $ 5 USD**.\n 1. Vai alla [Console di accesso al dispositivo]({device_access_console_url}) e attraverso il flusso di pagamento.\n 2. Clicca su **Crea progetto**\n 3. Assegna un nome al progetto di accesso al dispositivo e fai clic su **Avanti**.\n 4. Inserisci il tuo ID cliente OAuth\n 5. Abilita gli eventi facendo clic su **Abilita** e **Crea progetto**. \n\n Inserisci il tuo ID progetto di accesso al dispositivo di seguito ([maggiori informazioni]({more_info_url})).\n", + "title": "Nest: crea un progetto di accesso al dispositivo" + }, + "device_project_upgrade": { + "description": "Aggiorna il progetto di accesso al dispositivo Nest con il nuovo ID client OAuth ([ulteriori informazioni]({more_info_url}))\n1. Vai a [Console di accesso al dispositivo]({device_access_console_url}).\n2. Fai clic sull'icona del cestino accanto a *OAuth Client ID*.\n3. Fai clic sul menu di overflow '...' e *Aggiungi ID client*.\n4. Immettere il nuovo ID client OAuth e fare clic su **Aggiungi**.\n\nIl tuo ID client OAuth \u00e8: '{client_id}'", + "title": "Nest: aggiorna il progetto di accesso al dispositivo" + }, "init": { "data": { "flow_impl": "Provider" From f276523ef32fc4fc03e8d36e3a73d5bebfad30ba Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 17 Jun 2022 11:07:08 +1000 Subject: [PATCH 1548/3516] Ignore in progress segment when adding stream recorder lookback (#73604) --- homeassistant/components/stream/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 19ef009a845..3766d981da5 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -510,7 +510,7 @@ class Stream: num_segments = min(int(lookback // hls.target_duration), MAX_SEGMENTS) # Wait for latest segment, then add the lookback await hls.recv() - recorder.prepend(list(hls.get_segments())[-num_segments:]) + recorder.prepend(list(hls.get_segments())[-num_segments - 1 : -1]) async def async_get_image( self, From cdd5a5f68b3293c295c2230ac25676f51c5aed53 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Fri, 17 Jun 2022 06:07:21 +0100 Subject: [PATCH 1549/3516] Generic ipcam configflow2 followup (#73511) * Address code review comments * Add type hints * Remvoe unused strings * Remove persistent notification setup * Patch async_configre * Fix pylint warning * Address review comments * Clean types * Code review: defer local var assignment Co-authored-by: Dave T Co-authored-by: Martin Hjelmare --- homeassistant/components/generic/camera.py | 26 ++++--- .../components/generic/config_flow.py | 44 +++++++----- homeassistant/components/generic/strings.json | 3 +- .../components/generic/translations/en.json | 7 -- tests/components/generic/conftest.py | 3 +- tests/components/generic/test_config_flow.py | 70 +++++++++++-------- 6 files changed, 89 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index edc51430f0d..5f1f9ba9c2c 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -1,7 +1,9 @@ """Support for IP Cameras.""" from __future__ import annotations +from collections.abc import Mapping import logging +from typing import Any import httpx import voluptuous as vol @@ -115,12 +117,12 @@ async def async_setup_entry( ) -def generate_auth(device_info) -> httpx.Auth | None: +def generate_auth(device_info: Mapping[str, Any]) -> httpx.Auth | None: """Generate httpx.Auth object from credentials.""" - username = device_info.get(CONF_USERNAME) - password = device_info.get(CONF_PASSWORD) + username: str | None = device_info.get(CONF_USERNAME) + password: str | None = device_info.get(CONF_PASSWORD) authentication = device_info.get(CONF_AUTHENTICATION) - if username: + if username and password: if authentication == HTTP_DIGEST_AUTHENTICATION: return httpx.DigestAuth(username=username, password=password) return httpx.BasicAuth(username=username, password=password) @@ -130,7 +132,15 @@ def generate_auth(device_info) -> httpx.Auth | None: class GenericCamera(Camera): """A generic implementation of an IP camera.""" - def __init__(self, hass, device_info, identifier, title): + _last_image: bytes | None + + def __init__( + self, + hass: HomeAssistant, + device_info: Mapping[str, Any], + identifier: str, + title: str, + ) -> None: """Initialize a generic camera.""" super().__init__() self.hass = hass @@ -143,10 +153,10 @@ class GenericCamera(Camera): and self._still_image_url ): self._still_image_url = cv.template(self._still_image_url) - if self._still_image_url not in [None, ""]: + if self._still_image_url: self._still_image_url.hass = hass self._stream_source = device_info.get(CONF_STREAM_SOURCE) - if self._stream_source not in (None, ""): + if self._stream_source: if not isinstance(self._stream_source, template_helper.Template): self._stream_source = cv.template(self._stream_source) self._stream_source.hass = hass @@ -207,7 +217,7 @@ class GenericCamera(Camera): """Return the name of this device.""" return self._name - async def stream_source(self): + async def stream_source(self) -> str | None: """Return the source of the stream.""" if self._stream_source is None: return None diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 3cc78fca406..b6abdc5eec8 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -1,11 +1,11 @@ """Config flow for generic (IP Camera).""" from __future__ import annotations +from collections.abc import Mapping import contextlib from errno import EHOSTUNREACH, EIO import io import logging -from types import MappingProxyType from typing import Any import PIL @@ -32,6 +32,7 @@ from homeassistant.const import ( HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, ) +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template as template_helper @@ -64,7 +65,7 @@ SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml", "webp"} def build_schema( - user_input: dict[str, Any] | MappingProxyType[str, Any], + user_input: Mapping[str, Any], is_options_flow: bool = False, show_advanced_options=False, ): @@ -119,7 +120,7 @@ def build_schema( return vol.Schema(spec) -def get_image_type(image): +def get_image_type(image: bytes) -> str | None: """Get the format of downloaded bytes that could be an image.""" fmt = None imagefile = io.BytesIO(image) @@ -135,7 +136,9 @@ def get_image_type(image): return fmt -async def async_test_still(hass, info) -> tuple[dict[str, str], str | None]: +async def async_test_still( + hass: HomeAssistant, info: Mapping[str, Any] +) -> tuple[dict[str, str], str | None]: """Verify that the still image is valid before we create an entity.""" fmt = None if not (url := info.get(CONF_STILL_IMAGE_URL)): @@ -147,7 +150,7 @@ async def async_test_still(hass, info) -> tuple[dict[str, str], str | None]: except TemplateError as err: _LOGGER.warning("Problem rendering template %s: %s", url, err) return {CONF_STILL_IMAGE_URL: "template_error"}, None - verify_ssl = info.get(CONF_VERIFY_SSL) + verify_ssl = info[CONF_VERIFY_SSL] auth = generate_auth(info) try: async_client = get_async_client(hass, verify_ssl=verify_ssl) @@ -177,7 +180,9 @@ async def async_test_still(hass, info) -> tuple[dict[str, str], str | None]: return {}, f"image/{fmt}" -def slug(hass, template) -> str | None: +def slug( + hass: HomeAssistant, template: str | template_helper.Template | None +) -> str | None: """Convert a camera url into a string suitable for a camera name.""" if not template: return None @@ -193,7 +198,9 @@ def slug(hass, template) -> str | None: return None -async def async_test_stream(hass, info) -> dict[str, str]: +async def async_test_stream( + hass: HomeAssistant, info: Mapping[str, Any] +) -> dict[str, str]: """Verify that the stream is valid before we create an entity.""" if not (stream_source := info.get(CONF_STREAM_SOURCE)): return {} @@ -240,7 +247,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize Generic ConfigFlow.""" self.cached_user_input: dict[str, Any] = {} self.cached_title = "" @@ -252,7 +259,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return GenericOptionsFlowHandler(config_entry) - def check_for_existing(self, options): + def check_for_existing(self, options: dict[str, Any]) -> bool: """Check whether an existing entry is using the same URLs.""" return any( entry.options.get(CONF_STILL_IMAGE_URL) == options.get(CONF_STILL_IMAGE_URL) @@ -273,14 +280,16 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): ): errors["base"] = "no_still_image_or_stream_url" else: - errors, still_format = await async_test_still(self.hass, user_input) - errors = errors | await async_test_stream(self.hass, user_input) - still_url = user_input.get(CONF_STILL_IMAGE_URL) - stream_url = user_input.get(CONF_STREAM_SOURCE) - name = slug(hass, still_url) or slug(hass, stream_url) or DEFAULT_NAME + errors, still_format = await async_test_still(hass, user_input) + errors = errors | await async_test_stream(hass, user_input) if not errors: user_input[CONF_CONTENT_TYPE] = still_format user_input[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False + still_url = user_input.get(CONF_STILL_IMAGE_URL) + stream_url = user_input.get(CONF_STREAM_SOURCE) + name = ( + slug(hass, still_url) or slug(hass, stream_url) or DEFAULT_NAME + ) if still_url is None: # If user didn't specify a still image URL, # The automatically generated still image that stream generates @@ -299,7 +308,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, import_config) -> FlowResult: + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: """Handle config import from yaml.""" # abort if we've already got this one. if self.check_for_existing(import_config): @@ -311,6 +320,7 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN): CONF_NAME, slug(self.hass, still_url) or slug(self.hass, stream_url) or DEFAULT_NAME, ) + if CONF_LIMIT_REFETCH_TO_URL_CHANGE not in import_config: import_config[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False still_format = import_config.get(CONF_CONTENT_TYPE, "image/jpeg") @@ -336,9 +346,9 @@ class GenericOptionsFlowHandler(OptionsFlow): if user_input is not None: errors, still_format = await async_test_still( - self.hass, self.config_entry.options | user_input + hass, self.config_entry.options | user_input ) - errors = errors | await async_test_stream(self.hass, user_input) + errors = errors | await async_test_stream(hass, user_input) still_url = user_input.get(CONF_STILL_IMAGE_URL) stream_url = user_input.get(CONF_STREAM_SOURCE) if not errors: diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index 7cb1135fe56..7d3cab19aa5 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -15,8 +15,7 @@ "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?" }, "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", - "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" }, "step": { "user": { diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json index d01e6e59a4b..4b10ed2d8ac 100644 --- a/homeassistant/components/generic/translations/en.json +++ b/homeassistant/components/generic/translations/en.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "no_devices_found": "No devices found on the network", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { @@ -12,9 +11,7 @@ "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", - "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", - "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", @@ -51,13 +48,9 @@ "already_exists": "A camera with these URL settings already exists.", "invalid_still_image": "URL did not return a valid still image", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", - "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", - "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", - "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", - "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", diff --git a/tests/components/generic/conftest.py b/tests/components/generic/conftest.py index 266b848ebe2..808e858b259 100644 --- a/tests/components/generic/conftest.py +++ b/tests/components/generic/conftest.py @@ -7,7 +7,7 @@ from PIL import Image import pytest import respx -from homeassistant import config_entries, setup +from homeassistant import config_entries from homeassistant.components.generic.const import DOMAIN from tests.common import MockConfigEntry @@ -79,7 +79,6 @@ def mock_create_stream(): async def user_flow(hass): """Initiate a 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} ) diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index ee12056b191..2979513e5c0 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -8,7 +8,7 @@ import httpx import pytest import respx -from homeassistant import config_entries, data_entry_flow, setup +from homeassistant import config_entries, data_entry_flow from homeassistant.components.camera import async_get_image from homeassistant.components.generic.const import ( CONF_CONTENT_TYPE, @@ -60,7 +60,9 @@ TESTDATA_YAML = { async def test_form(hass, fakeimg_png, user_flow, mock_create_stream): """Test the form with a normal set of settings.""" - with mock_create_stream as mock_setup: + with mock_create_stream as mock_setup, patch( + "homeassistant.components.generic.async_setup_entry", return_value=True + ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], TESTDATA, @@ -81,12 +83,12 @@ async def test_form(hass, fakeimg_png, user_flow, mock_create_stream): await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 @respx.mock async def test_form_only_stillimage(hass, fakeimg_png, user_flow): """Test we complete ok if the user wants still images only.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -95,10 +97,12 @@ async def test_form_only_stillimage(hass, fakeimg_png, user_flow): data = TESTDATA.copy() data.pop(CONF_STREAM_SOURCE) - result2 = await hass.config_entries.flow.async_configure( - user_flow["flow_id"], - data, - ) + with patch("homeassistant.components.generic.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + data, + ) + await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { @@ -112,7 +116,6 @@ async def test_form_only_stillimage(hass, fakeimg_png, user_flow): CONF_VERIFY_SSL: False, } - await hass.async_block_till_done() assert respx.calls.call_count == 1 @@ -121,10 +124,12 @@ async def test_form_only_stillimage_gif(hass, fakeimg_gif, user_flow): """Test we complete ok if the user wants a gif.""" data = TESTDATA.copy() data.pop(CONF_STREAM_SOURCE) - result2 = await hass.config_entries.flow.async_configure( - user_flow["flow_id"], - data, - ) + with patch("homeassistant.components.generic.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + data, + ) + await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["options"][CONF_CONTENT_TYPE] == "image/gif" @@ -136,10 +141,12 @@ async def test_form_only_svg_whitespace(hass, fakeimgbytes_svg, user_flow): respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_wspace_svg) data = TESTDATA.copy() data.pop(CONF_STREAM_SOURCE) - result2 = await hass.config_entries.flow.async_configure( - user_flow["flow_id"], - data, - ) + with patch("homeassistant.components.generic.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + data, + ) + await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -161,10 +168,12 @@ async def test_form_only_still_sample(hass, user_flow, image_file): respx.get("http://127.0.0.1/testurl/1").respond(stream=image.read()) data = TESTDATA.copy() data.pop(CONF_STREAM_SOURCE) - result2 = await hass.config_entries.flow.async_configure( - user_flow["flow_id"], - data, - ) + with patch("homeassistant.components.generic.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + data, + ) + await hass.async_block_till_done() assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -203,10 +212,12 @@ async def test_still_template( data = TESTDATA.copy() data.pop(CONF_STREAM_SOURCE) data[CONF_STILL_IMAGE_URL] = template - result2 = await hass.config_entries.flow.async_configure( - user_flow["flow_id"], - data, - ) + with patch("homeassistant.components.generic.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + user_flow["flow_id"], + data, + ) + await hass.async_block_till_done() assert result2["type"] == expected_result @@ -216,7 +227,9 @@ async def test_form_rtsp_mode(hass, fakeimg_png, user_flow, mock_create_stream): data = TESTDATA.copy() data[CONF_RTSP_TRANSPORT] = "tcp" data[CONF_STREAM_SOURCE] = "rtsp://127.0.0.1/testurl/2" - with mock_create_stream as mock_setup: + with mock_create_stream as mock_setup, patch( + "homeassistant.components.generic.async_setup_entry", return_value=True + ): result2 = await hass.config_entries.flow.async_configure( user_flow["flow_id"], data ) @@ -242,7 +255,6 @@ async def test_form_rtsp_mode(hass, fakeimg_png, user_flow, mock_create_stream): async def test_form_only_stream(hass, fakeimgbytes_jpg, mock_create_stream): """Test we complete ok if the user wants stream only.""" - await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -254,6 +266,7 @@ async def test_form_only_stream(hass, fakeimgbytes_jpg, mock_create_stream): result["flow_id"], data, ) + await hass.async_block_till_done() assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result3["title"] == "127_0_0_1" @@ -454,7 +467,6 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream """Test the options flow with a template error.""" respx.get("http://127.0.0.1/testurl/1").respond(stream=fakeimgbytes_png) respx.get("http://127.0.0.1/testurl/2").respond(stream=fakeimgbytes_png) - await setup.async_setup_component(hass, "persistent_notification", {}) mock_entry = MockConfigEntry( title="Test Camera", @@ -649,7 +661,9 @@ async def test_use_wallclock_as_timestamps_option( ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" - with mock_create_stream: + with patch( + "homeassistant.components.generic.async_setup_entry", return_value=True + ), mock_create_stream: result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA}, From e30478457b4ac4eeb5abf54f44caa76985a9fc62 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 17 Jun 2022 07:40:02 +0200 Subject: [PATCH 1550/3516] Fix voltage and current values for Fritz!DECT smart plugs (#73608) fix voltage and current values --- homeassistant/components/fritzbox/sensor.py | 4 +- tests/components/fritzbox/test_switch.py | 65 +++++++++++++++------ 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 2ae7f9dccc8..161dfc196d2 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -96,7 +96,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] - native_value=lambda device: device.voltage / 1000 + native_value=lambda device: device.voltage if getattr(device, "voltage", None) else 0.0, ), @@ -107,7 +107,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] - native_value=lambda device: device.power / device.voltage + native_value=lambda device: device.power / device.voltage / 1000 if device.power and getattr(device, "voltage", None) else 0.0, ), diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index a3135dd61f3..75799a08d48 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -16,6 +16,8 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, POWER_WATT, SERVICE_TURN_OFF, @@ -48,29 +50,54 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): assert state.attributes[ATTR_FRIENDLY_NAME] == CONF_FAKE_NAME assert ATTR_STATE_CLASS not in state.attributes - state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_temperature") - assert state - assert state.state == "1.23" - assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Temperature" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS - assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT - state = hass.states.get(f"{ENTITY_ID}_humidity") assert state is None - state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_power_consumption") - assert state - assert state.state == "5.678" - assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Power Consumption" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT - assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + sensors = ( + [ + f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_temperature", + "1.23", + f"{CONF_FAKE_NAME} Temperature", + TEMP_CELSIUS, + SensorStateClass.MEASUREMENT, + ], + [ + f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_power_consumption", + "5.678", + f"{CONF_FAKE_NAME} Power Consumption", + POWER_WATT, + SensorStateClass.MEASUREMENT, + ], + [ + f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_total_energy", + "1.234", + f"{CONF_FAKE_NAME} Total Energy", + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING, + ], + [ + f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_voltage", + "230", + f"{CONF_FAKE_NAME} Voltage", + ELECTRIC_POTENTIAL_VOLT, + SensorStateClass.MEASUREMENT, + ], + [ + f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_electric_current", + "0.0246869565217391", + f"{CONF_FAKE_NAME} Electric Current", + ELECTRIC_CURRENT_AMPERE, + SensorStateClass.MEASUREMENT, + ], + ) - state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_total_energy") - assert state - assert state.state == "1.234" - assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Total Energy" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_KILO_WATT_HOUR - assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL_INCREASING + for sensor in sensors: + state = hass.states.get(sensor[0]) + assert state + assert state.state == sensor[1] + assert state.attributes[ATTR_FRIENDLY_NAME] == sensor[2] + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == sensor[3] + assert state.attributes[ATTR_STATE_CLASS] == sensor[4] async def test_turn_on(hass: HomeAssistant, fritz: Mock): From ea21a36e52a3a157bf86c75de13c8e78ec006ea2 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 17 Jun 2022 00:04:41 -0700 Subject: [PATCH 1551/3516] Remove default use of google calendars yaml file in tests (#73621) Remove default use of google_calendars.yaml in tests --- tests/components/google/conftest.py | 2 +- tests/components/google/test_calendar.py | 19 ++++++++++++------- tests/components/google/test_init.py | 11 +++-------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 68176493445..27fb6c993ff 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -99,7 +99,7 @@ def calendars_config(calendars_config_entity: dict[str, Any]) -> list[dict[str, ] -@pytest.fixture(autouse=True) +@pytest.fixture def mock_calendars_yaml( hass: HomeAssistant, calendars_config: list[dict[str, Any]], diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 794065ca09a..85711014e72 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -17,13 +17,18 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.template import DATE_STR_FORMAT import homeassistant.util.dt as dt_util -from .conftest import CALENDAR_ID, TEST_YAML_ENTITY, TEST_YAML_ENTITY_NAME +from .conftest import ( + CALENDAR_ID, + TEST_API_ENTITY, + TEST_API_ENTITY_NAME, + TEST_YAML_ENTITY, +) from tests.common import async_fire_time_changed from tests.test_util.aiohttp import AiohttpClientMockResponse -TEST_ENTITY = TEST_YAML_ENTITY -TEST_ENTITY_NAME = TEST_YAML_ENTITY_NAME +TEST_ENTITY = TEST_API_ENTITY +TEST_ENTITY_NAME = TEST_API_ENTITY_NAME TEST_EVENT = { "summary": "Test All Day Event", @@ -58,7 +63,6 @@ TEST_EVENT = { @pytest.fixture(autouse=True) def mock_test_setup( hass, - mock_calendars_yaml, test_api_calendar, mock_calendars_list, config_entry, @@ -87,12 +91,12 @@ def upcoming_date() -> dict[str, Any]: } -def upcoming_event_url() -> str: +def upcoming_event_url(entity: str = TEST_ENTITY) -> str: """Return a calendar API to return events created by upcoming().""" now = dt_util.now() start = (now - datetime.timedelta(minutes=60)).isoformat() end = (now + datetime.timedelta(minutes=60)).isoformat() - return f"/api/calendars/{TEST_ENTITY}?start={urllib.parse.quote(start)}&end={urllib.parse.quote(end)}" + return f"/api/calendars/{entity}?start={urllib.parse.quote(start)}&end={urllib.parse.quote(end)}" async def test_all_day_event( @@ -551,6 +555,7 @@ async def test_http_api_event_paging( async def test_opaque_event( hass, hass_client, + mock_calendars_yaml, mock_events_list_items, component_setup, transparency, @@ -566,7 +571,7 @@ async def test_opaque_event( assert await component_setup() client = await hass_client() - response = await client.get(upcoming_event_url()) + response = await client.get(upcoming_event_url(TEST_YAML_ENTITY)) assert response.status == HTTPStatus.OK events = await response.json() assert (len(events) > 0) == expect_visible_event diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index b2a81b47718..f9391a82b6a 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -74,7 +74,7 @@ def setup_config_entry( ( SERVICE_CREATE_EVENT, {}, - {"entity_id": TEST_YAML_ENTITY}, + {"entity_id": TEST_API_ENTITY}, ), ], ids=("add_event", "create_event"), @@ -245,13 +245,12 @@ async def test_found_calendar_from_api( @pytest.mark.parametrize( - "calendars_config,google_config,config_entry_options", - [([], {}, {CONF_CALENDAR_ACCESS: "read_write"})], + "google_config,config_entry_options", + [({}, {CONF_CALENDAR_ACCESS: "read_write"})], ) async def test_load_application_credentials( hass: HomeAssistant, component_setup: ComponentSetup, - mock_calendars_yaml: None, mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], mock_events_list: ApiResult, @@ -464,7 +463,6 @@ async def test_add_event_invalid_params( component_setup: ComponentSetup, mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], - mock_calendars_yaml: None, mock_events_list: ApiResult, setup_config_entry: MockConfigEntry, add_event_call_service: Callable[dict[str, Any], Awaitable[None]], @@ -504,7 +502,6 @@ async def test_add_event_date_in_x( mock_calendars_list: ApiResult, mock_insert_event: Callable[[..., dict[str, Any]], None], test_api_calendar: dict[str, Any], - mock_calendars_yaml: None, mock_events_list: ApiResult, date_fields: dict[str, Any], start_timedelta: datetime.timedelta, @@ -544,7 +541,6 @@ async def test_add_event_date( mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], mock_insert_event: Callable[[str, dict[str, Any]], None], - mock_calendars_yaml: None, mock_events_list: ApiResult, setup_config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, @@ -586,7 +582,6 @@ async def test_add_event_date_time( mock_calendars_list: ApiResult, mock_insert_event: Callable[[str, dict[str, Any]], None], test_api_calendar: dict[str, Any], - mock_calendars_yaml: None, mock_events_list: ApiResult, setup_config_entry: MockConfigEntry, aioclient_mock: AiohttpClientMocker, From 01ccf721e7a68c9dcbf7ad937960b54f47a17d11 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 17 Jun 2022 10:09:41 +0200 Subject: [PATCH 1552/3516] Update wheels builder to 2022.06.4 (#73628) --- .github/workflows/wheels.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 0c14459b487..473add9781e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -140,7 +140,7 @@ jobs: done - name: Build wheels - uses: home-assistant/wheels@2022.06.3 + uses: home-assistant/wheels@2022.06.4 with: abi: cp310 tag: musllinux_1_2 @@ -273,14 +273,14 @@ jobs: done - name: Build wheels - uses: home-assistant/wheels@2022.06.3 + uses: home-assistant/wheels@2022.06.4 with: abi: cp310 tag: musllinux_1_2 arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev" + apk: "libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran" skip-binary: aiohttp;grpcio legacy: true constraints: "homeassistant/package_constraints.txt" From 546d342604ca742a5fe75457356f50d88b32b7a3 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 17 Jun 2022 11:44:56 +0200 Subject: [PATCH 1553/3516] Update wheels builder to 2022.06.5 (#73633) --- .github/workflows/wheels.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 473add9781e..e86f8235582 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -140,7 +140,7 @@ jobs: done - name: Build wheels - uses: home-assistant/wheels@2022.06.4 + uses: home-assistant/wheels@2022.06.5 with: abi: cp310 tag: musllinux_1_2 @@ -273,7 +273,7 @@ jobs: done - name: Build wheels - uses: home-assistant/wheels@2022.06.4 + uses: home-assistant/wheels@2022.06.5 with: abi: cp310 tag: musllinux_1_2 From e0b362ef3bb0f7550b5a2f912b8084ba0fb11789 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 17 Jun 2022 12:13:16 +0200 Subject: [PATCH 1554/3516] Fix zha log message (#73626) --- homeassistant/components/zha/core/gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 642a5b3ec55..02b2b21c835 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -672,7 +672,7 @@ class ZHAGateway: async def async_remove_zigpy_group(self, group_id: int) -> None: """Remove a Zigbee group from Zigpy.""" if not (group := self.groups.get(group_id)): - _LOGGER.debug("Group: %s:0x%04x could not be found", group.name, group_id) + _LOGGER.debug("Group: 0x%04x could not be found", group_id) return if group.members: tasks = [] From d90f0297723720d72d685eba06daee735f29c32a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 17 Jun 2022 12:14:11 +0200 Subject: [PATCH 1555/3516] Finish migrating demo NumberEntity to native_value (#73581) --- homeassistant/components/demo/number.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py index 7a9baf045e5..02ab1a2a989 100644 --- a/homeassistant/components/demo/number.py +++ b/homeassistant/components/demo/number.py @@ -99,11 +99,11 @@ class DemoNumber(NumberEntity): self._attr_mode = mode if native_min_value is not None: - self._attr_min_value = native_min_value + self._attr_native_min_value = native_min_value if native_max_value is not None: - self._attr_max_value = native_max_value + self._attr_native_max_value = native_max_value if native_step is not None: - self._attr_step = native_step + self._attr_native_step = native_step self._attr_device_info = DeviceInfo( identifiers={ From 2107966fa882a4309d13f898b65146b7f468e7d8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 17 Jun 2022 12:14:35 +0200 Subject: [PATCH 1556/3516] Finish migrating sleepiq NumberEntity to native_value (#73582) --- homeassistant/components/sleepiq/number.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sleepiq/number.py b/homeassistant/components/sleepiq/number.py index a0ade257335..01ba90360ba 100644 --- a/homeassistant/components/sleepiq/number.py +++ b/homeassistant/components/sleepiq/number.py @@ -157,5 +157,5 @@ class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set the number value.""" await self.entity_description.set_value_fn(self.device, int(value)) - self._attr_value = value + self._attr_native_value = value self.async_write_ha_state() From 66feac2257c3dbc73f31de5ccbbc92896f81474e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 17 Jun 2022 12:15:59 +0200 Subject: [PATCH 1557/3516] Finish migrating zha NumberEntity to native_value (#73580) --- homeassistant/components/zha/number.py | 38 +++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 2f674df168e..9103dd2e364 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -363,7 +363,7 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity): """Representation of a ZHA number configuration entity.""" _attr_entity_category = EntityCategory.CONFIG - _attr_step: float = 1.0 + _attr_native_step: float = 1.0 _zcl_attribute: str @classmethod @@ -404,11 +404,11 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity): super().__init__(unique_id, zha_device, channels, **kwargs) @property - def value(self) -> float: + def native_value(self) -> float: """Return the current value.""" return self._channel.cluster.get(self._zcl_attribute) - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Update the current value from HA.""" try: res = await self._channel.cluster.write_attributes( @@ -439,8 +439,8 @@ class AqaraMotionDetectionInterval( ): """Representation of a ZHA on off transition time configuration entity.""" - _attr_min_value: float = 2 - _attr_max_value: float = 65535 + _attr_native_min_value: float = 2 + _attr_native_max_value: float = 65535 _zcl_attribute: str = "detection_interval" @@ -450,8 +450,8 @@ class OnOffTransitionTimeConfigurationEntity( ): """Representation of a ZHA on off transition time configuration entity.""" - _attr_min_value: float = 0x0000 - _attr_max_value: float = 0xFFFF + _attr_native_min_value: float = 0x0000 + _attr_native_max_value: float = 0xFFFF _zcl_attribute: str = "on_off_transition_time" @@ -459,8 +459,8 @@ class OnOffTransitionTimeConfigurationEntity( class OnLevelConfigurationEntity(ZHANumberConfigurationEntity, id_suffix="on_level"): """Representation of a ZHA on level configuration entity.""" - _attr_min_value: float = 0x00 - _attr_max_value: float = 0xFF + _attr_native_min_value: float = 0x00 + _attr_native_max_value: float = 0xFF _zcl_attribute: str = "on_level" @@ -470,8 +470,8 @@ class OnTransitionTimeConfigurationEntity( ): """Representation of a ZHA on transition time configuration entity.""" - _attr_min_value: float = 0x0000 - _attr_max_value: float = 0xFFFE + _attr_native_min_value: float = 0x0000 + _attr_native_max_value: float = 0xFFFE _zcl_attribute: str = "on_transition_time" @@ -481,8 +481,8 @@ class OffTransitionTimeConfigurationEntity( ): """Representation of a ZHA off transition time configuration entity.""" - _attr_min_value: float = 0x0000 - _attr_max_value: float = 0xFFFE + _attr_native_min_value: float = 0x0000 + _attr_native_max_value: float = 0xFFFE _zcl_attribute: str = "off_transition_time" @@ -492,8 +492,8 @@ class DefaultMoveRateConfigurationEntity( ): """Representation of a ZHA default move rate configuration entity.""" - _attr_min_value: float = 0x00 - _attr_max_value: float = 0xFE + _attr_native_min_value: float = 0x00 + _attr_native_max_value: float = 0xFE _zcl_attribute: str = "default_move_rate" @@ -503,8 +503,8 @@ class StartUpCurrentLevelConfigurationEntity( ): """Representation of a ZHA startup current level configuration entity.""" - _attr_min_value: float = 0x00 - _attr_max_value: float = 0xFF + _attr_native_min_value: float = 0x00 + _attr_native_max_value: float = 0xFF _zcl_attribute: str = "start_up_current_level" @@ -519,7 +519,7 @@ class TimerDurationMinutes(ZHANumberConfigurationEntity, id_suffix="timer_durati _attr_entity_category = EntityCategory.CONFIG _attr_icon: str = ICONS[14] - _attr_min_value: float = 0x00 - _attr_max_value: float = 0x257 + _attr_native_min_value: float = 0x00 + _attr_native_max_value: float = 0x257 _attr_unit_of_measurement: str | None = UNITS[72] _zcl_attribute: str = "timer_duration" From baa810aabbee430643229ee4b3e9f4c467d2878e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 17 Jun 2022 12:17:13 +0200 Subject: [PATCH 1558/3516] Improve warnings for datetime and date sensors with invalid states (#73598) --- homeassistant/components/sensor/__init__.py | 10 +++++----- tests/components/sensor/test_init.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 2bbcf3b119d..2c0d75ff471 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -404,10 +404,10 @@ class SensorEntity(Entity): value = value.astimezone(timezone.utc) return value.isoformat(timespec="seconds") - except (AttributeError, TypeError) as err: + except (AttributeError, OverflowError, TypeError) as err: raise ValueError( - f"Invalid datetime: {self.entity_id} has a timestamp device class " - f"but does not provide a datetime state but {type(value)}" + f"Invalid datetime: {self.entity_id} has timestamp device class " + f"but provides state {value}:{type(value)} resulting in '{err}'" ) from err # Received a date value @@ -419,8 +419,8 @@ class SensorEntity(Entity): return value.isoformat() except (AttributeError, TypeError) as err: raise ValueError( - f"Invalid date: {self.entity_id} has a date device class " - f"but does not provide a date state but {type(value)}" + f"Invalid date: {self.entity_id} has date device class " + f"but provides state {value}:{type(value)} resulting in '{err}'" ) from err if ( diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 4a3d0202c91..0bae8235ff9 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -183,8 +183,8 @@ async def test_deprecated_datetime_str( await hass.async_block_till_done() assert ( - f"Invalid {provides}: sensor.test has a {device_class} device class " - f"but does not provide a {provides} state but {type(state_value)}" + f"Invalid {provides}: sensor.test has {device_class} device class " + f"but provides state {state_value}:{type(state_value)}" ) in caplog.text From 2be54de4486112a4b16fe8a85bb5d79bb1d42641 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Fri, 17 Jun 2022 18:26:25 +0200 Subject: [PATCH 1559/3516] Don't verify ssl certificates for ssdp/upnp devices (#73647) --- homeassistant/components/ssdp/__init__.py | 2 +- homeassistant/components/upnp/device.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index e854472f21f..d221cb162f4 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -351,7 +351,7 @@ class Scanner: async def async_start(self) -> None: """Start the scanners.""" - session = async_get_clientsession(self.hass) + session = async_get_clientsession(self.hass, verify_ssl=False) requester = AiohttpSessionRequester(session, True, 10) self._description_cache = DescriptionCache(requester) diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 334d870939f..3a688b8571d 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -48,7 +48,7 @@ async def async_get_mac_address_from_host(hass: HomeAssistant, host: str) -> str async def async_create_device(hass: HomeAssistant, ssdp_location: str) -> Device: """Create UPnP/IGD device.""" - session = async_get_clientsession(hass) + session = async_get_clientsession(hass, verify_ssl=False) requester = AiohttpSessionRequester(session, with_sleep=True, timeout=20) factory = UpnpFactory(requester, disable_state_variable_validation=True) From bf15df75dd46307b558b33c4b261b0e7771a71d3 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Fri, 17 Jun 2022 18:26:45 +0200 Subject: [PATCH 1560/3516] Ignore fake upnp/IGD devices when upnp is discovered (#73645) --- homeassistant/components/upnp/config_flow.py | 13 +++++++++++ tests/components/upnp/test_config_flow.py | 23 ++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 7fa37f589bf..7d4e768e855 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -63,6 +63,12 @@ async def _async_mac_address_from_discovery( return await async_get_mac_address_from_host(hass, host) +def _is_igd_device(discovery_info: ssdp.SsdpServiceInfo) -> bool: + """Test if discovery is a complete IGD device.""" + root_device_info = discovery_info.upnp + return root_device_info.get(ssdp.ATTR_UPNP_DEVICE_TYPE) in {ST_IGD_V1, ST_IGD_V2} + + class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a UPnP/IGD config flow.""" @@ -108,6 +114,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): for discovery in discoveries if ( _is_complete_discovery(discovery) + and _is_igd_device(discovery) and discovery.ssdp_usn not in current_unique_ids ) ] @@ -144,6 +151,12 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.debug("Incomplete discovery, ignoring") return self.async_abort(reason="incomplete_discovery") + # Ensure device is usable. Ideally we would use IgdDevice.is_profile_device, + # but that requires constructing the device completely. + if not _is_igd_device(discovery_info): + LOGGER.debug("Non IGD device, ignoring") + return self.async_abort(reason="non_igd_device") + # Ensure not already configuring/configured. unique_id = discovery_info.ssdp_usn await self.async_set_unique_id(unique_id) diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index 80847ec2737..66d84fe0862 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -14,6 +14,7 @@ from homeassistant.components.upnp.const import ( CONFIG_ENTRY_ST, CONFIG_ENTRY_UDN, DOMAIN, + ST_IGD_V1, ) from homeassistant.core import HomeAssistant @@ -75,6 +76,7 @@ async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistant): ssdp_st=TEST_ST, ssdp_location=TEST_LOCATION, upnp={ + ssdp.ATTR_UPNP_DEVICE_TYPE: ST_IGD_V1, # ssdp.ATTR_UPNP_UDN: TEST_UDN, # Not provided. }, ), @@ -83,6 +85,27 @@ async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistant): assert result["reason"] == "incomplete_discovery" +@pytest.mark.usefixtures("mock_get_source_ip") +async def test_flow_ssdp_non_igd_device(hass: HomeAssistant): + """Test config flow: incomplete discovery through ssdp.""" + # Discovered via step ssdp. + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=ssdp.SsdpServiceInfo( + ssdp_usn=TEST_USN, + ssdp_st=TEST_ST, + ssdp_location=TEST_LOCATION, + upnp={ + ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:WFADevice:1", # Non-IGD + ssdp.ATTR_UPNP_UDN: TEST_UDN, + }, + ), + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "non_igd_device" + + @pytest.mark.usefixtures( "ssdp_instant_discovery", "mock_setup_entry", From 4bc5d7bfed07c20d6f3438ab91c734a620505a33 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Jun 2022 11:41:10 -0500 Subject: [PATCH 1561/3516] Speed up zha tests (#73627) --- .../zha/test_alarm_control_panel.py | 15 ++++++++++++++ tests/components/zha/test_api.py | 16 ++++++++++++++- tests/components/zha/test_binary_sensor.py | 17 ++++++++++++++++ tests/components/zha/test_button.py | 18 +++++++++++++++++ tests/components/zha/test_channels.py | 7 +++++++ tests/components/zha/test_climate.py | 18 +++++++++++++++++ tests/components/zha/test_config_flow.py | 7 +++++++ tests/components/zha/test_cover.py | 15 ++++++++++++++ tests/components/zha/test_device.py | 18 ++++++++++++++++- tests/components/zha/test_device_action.py | 17 ++++++++++++++++ tests/components/zha/test_device_tracker.py | 18 +++++++++++++++++ tests/components/zha/test_device_trigger.py | 9 +++++++++ tests/components/zha/test_diagnostics.py | 12 +++++++++++ tests/components/zha/test_fan.py | 16 +++++++++++++++ tests/components/zha/test_gateway.py | 16 +++++++++++++++ tests/components/zha/test_init.py | 7 +++++++ tests/components/zha/test_light.py | 18 +++++++++++++++++ tests/components/zha/test_lock.py | 14 +++++++++++++ tests/components/zha/test_logbook.py | 11 +++++++++- tests/components/zha/test_number.py | 17 ++++++++++++++++ tests/components/zha/test_select.py | 20 ++++++++++++++++++- tests/components/zha/test_sensor.py | 14 +++++++++++++ tests/components/zha/test_siren.py | 16 +++++++++++++++ tests/components/zha/test_switch.py | 15 ++++++++++++++ 24 files changed, 347 insertions(+), 4 deletions(-) diff --git a/tests/components/zha/test_alarm_control_panel.py b/tests/components/zha/test_alarm_control_panel.py index e3742a3132f..0d3b8ffa9f1 100644 --- a/tests/components/zha/test_alarm_control_panel.py +++ b/tests/components/zha/test_alarm_control_panel.py @@ -22,6 +22,21 @@ from .common import async_enable_traffic, find_entity_id from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE +@pytest.fixture(autouse=True) +def alarm_control_panel_platform_only(): + """Only setup the alarm_control_panel and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", + ( + Platform.ALARM_CONTROL_PANEL, + Platform.DEVICE_TRACKER, + Platform.NUMBER, + Platform.SELECT, + ), + ): + yield + + @pytest.fixture def zigpy_device(zigpy_device_mock): """Device tracker zigpy device.""" diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index dac9855148a..08766bc74ac 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -37,7 +37,7 @@ from homeassistant.components.zha.core.const import ( GROUP_IDS, GROUP_NAME, ) -from homeassistant.const import ATTR_NAME +from homeassistant.const import ATTR_NAME, Platform from homeassistant.core import Context from .conftest import ( @@ -53,6 +53,20 @@ IEEE_SWITCH_DEVICE = "01:2d:6f:00:0a:90:69:e7" IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" +@pytest.fixture(autouse=True) +def required_platform_only(): + """Only setup the required and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", + ( + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, + ), + ): + yield + + @pytest.fixture async def device_switch(hass, zigpy_device_mock, zha_device_joined): """Test zha switch platform.""" diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index bfe2a3ce4f5..c27c8be16d8 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -1,4 +1,6 @@ """Test zha binary sensor.""" +from unittest.mock import patch + import pytest import zigpy.profiles.zha import zigpy.zcl.clusters.measurement as measurement @@ -34,6 +36,21 @@ DEVICE_OCCUPANCY = { } +@pytest.fixture(autouse=True) +def binary_sensor_platform_only(): + """Only setup the binary_sensor and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", + ( + Platform.BINARY_SENSOR, + Platform.DEVICE_TRACKER, + Platform.NUMBER, + Platform.SELECT, + ), + ): + yield + + async def async_test_binary_sensor_on_off(hass, cluster, entity_id): """Test getting on and off messages for binary sensors.""" # binary sensor on diff --git a/tests/components/zha/test_button.py b/tests/components/zha/test_button.py index f692528203f..2fdc263d732 100644 --- a/tests/components/zha/test_button.py +++ b/tests/components/zha/test_button.py @@ -28,6 +28,7 @@ from homeassistant.const import ( ENTITY_CATEGORY_CONFIG, ENTITY_CATEGORY_DIAGNOSTIC, STATE_UNKNOWN, + Platform, ) from homeassistant.helpers import entity_registry as er @@ -37,6 +38,23 @@ from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE from tests.common import mock_coro +@pytest.fixture(autouse=True) +def button_platform_only(): + """Only setup the button and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", + ( + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.DEVICE_TRACKER, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + ), + ): + yield + + @pytest.fixture async def contact_sensor(hass, zigpy_device_mock, zha_device_joined_restored): """Contact sensor fixture.""" diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index e55e10bd7ae..7701992cab4 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -20,6 +20,13 @@ from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE from tests.common import async_capture_events +@pytest.fixture(autouse=True) +def disable_platform_only(): + """Disable platforms to speed up tests.""" + with patch("homeassistant.components.zha.PLATFORMS", []): + yield + + @pytest.fixture def ieee(): """IEEE fixture.""" diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index 7866bba076c..a04b2c116e3 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -171,6 +171,24 @@ ZCL_ATTR_PLUG = { } +@pytest.fixture(autouse=True) +def climate_platform_only(): + """Only setup the climate and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", + ( + Platform.BUTTON, + Platform.CLIMATE, + Platform.BINARY_SENSOR, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, + ), + ): + yield + + @pytest.fixture def device_climate_mock(hass, zigpy_device_mock, zha_device_joined): """Test regular thermostat device.""" diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index df68c21b6c0..285c08d8a3e 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -34,6 +34,13 @@ from homeassistant.data_entry_flow import ( from tests.common import MockConfigEntry +@pytest.fixture(autouse=True) +def disable_platform_only(): + """Disable platforms to speed up tests.""" + with patch("homeassistant.components.zha.PLATFORMS", []): + yield + + def com_port(): """Mock of a serial port.""" port = serial.tools.list_ports_common.ListPortInfo("/dev/ttyUSB1234") diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index d5b07e16685..3dab405151d 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -39,6 +39,21 @@ from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE from tests.common import async_capture_events, mock_coro, mock_restore_cache +@pytest.fixture(autouse=True) +def cover_platform_only(): + """Only setup the cover and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", + ( + Platform.COVER, + Platform.DEVICE_TRACKER, + Platform.NUMBER, + Platform.SELECT, + ), + ): + yield + + @pytest.fixture def zigpy_cover_device(zigpy_device_mock): """Zigpy cover device.""" diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index 0f2caceada5..8b718635b6a 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -12,7 +12,7 @@ from homeassistant.components.zha.core.const import ( CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS, ) -from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE +from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE, Platform import homeassistant.helpers.device_registry as dr import homeassistant.util.dt as dt_util @@ -22,6 +22,22 @@ from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE from tests.common import async_fire_time_changed +@pytest.fixture(autouse=True) +def required_platforms_only(): + """Only setup the required platform and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", + ( + Platform.DEVICE_TRACKER, + Platform.SENSOR, + Platform.SELECT, + Platform.SWITCH, + Platform.BINARY_SENSOR, + ), + ): + yield + + @pytest.fixture def zigpy_device(zigpy_device_mock): """Device tracker zigpy device.""" diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 54cc7e9171d..fffb79fe0f2 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -24,6 +24,23 @@ COMMAND = "command" COMMAND_SINGLE = "single" +@pytest.fixture(autouse=True) +def required_platforms_only(): + """Only setup the required platforms and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", + ( + Platform.BINARY_SENSOR, + Platform.DEVICE_TRACKER, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SIREN, + ), + ): + yield + + @pytest.fixture async def device_ias(hass, zigpy_device_mock, zha_device_joined_restored): """IAS device fixture.""" diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index 06caac91cb2..59c36143a6e 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -1,6 +1,7 @@ """Test ZHA Device Tracker.""" from datetime import timedelta import time +from unittest.mock import patch import pytest import zigpy.profiles.zha @@ -24,6 +25,23 @@ from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE from tests.common import async_fire_time_changed +@pytest.fixture(autouse=True) +def device_tracker_platforms_only(): + """Only setup the device_tracker platforms and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", + ( + Platform.DEVICE_TRACKER, + Platform.BUTTON, + Platform.SELECT, + Platform.NUMBER, + Platform.BINARY_SENSOR, + Platform.SENSOR, + ), + ): + yield + + @pytest.fixture def zigpy_device_dt(zigpy_device_mock): """Device tracker zigpy device.""" diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index ac93cbe0c7f..1f5fa467a93 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -1,6 +1,7 @@ """ZHA device automation trigger tests.""" from datetime import timedelta import time +from unittest.mock import patch import pytest import zigpy.profiles.zha @@ -8,6 +9,7 @@ import zigpy.zcl.clusters.general as general import homeassistant.components.automation as automation from homeassistant.components.device_automation import DeviceAutomationType +from homeassistant.const import Platform from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -36,6 +38,13 @@ LONG_PRESS = "remote_button_long_press" LONG_RELEASE = "remote_button_long_release" +@pytest.fixture(autouse=True) +def sensor_platforms_only(): + """Only setup the sensor platform and required base platforms to speed up tests.""" + with patch("homeassistant.components.zha.PLATFORMS", (Platform.SENSOR,)): + yield + + def _same_lists(list_a, list_b): if len(list_a) != len(list_b): return False diff --git a/tests/components/zha/test_diagnostics.py b/tests/components/zha/test_diagnostics.py index 804b6d73316..d88996c78f1 100644 --- a/tests/components/zha/test_diagnostics.py +++ b/tests/components/zha/test_diagnostics.py @@ -1,6 +1,8 @@ """Tests for the diagnostics data provided by the ESPHome integration.""" +from unittest.mock import patch + import pytest import zigpy.profiles.zha as zha import zigpy.zcl.clusters.security as security @@ -8,6 +10,7 @@ import zigpy.zcl.clusters.security as security from homeassistant.components.diagnostics.const import REDACTED from homeassistant.components.zha.core.device import ZHADevice from homeassistant.components.zha.diagnostics import KEYS_TO_REDACT +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import async_get @@ -26,6 +29,15 @@ CONFIG_ENTRY_DIAGNOSTICS_KEYS = [ ] +@pytest.fixture(autouse=True) +def required_platforms_only(): + """Only setup the required platform and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", (Platform.ALARM_CONTROL_PANEL,) + ): + yield + + @pytest.fixture def zigpy_device(zigpy_device_mock): """Device tracker zigpy device.""" diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 19f5f39a22f..423634db035 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -51,6 +51,22 @@ IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" +@pytest.fixture(autouse=True) +def fan_platform_only(): + """Only setup the fan and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", + ( + Platform.FAN, + Platform.LIGHT, + Platform.DEVICE_TRACKER, + Platform.NUMBER, + Platform.SELECT, + ), + ): + yield + + @pytest.fixture def zigpy_device(zigpy_device_mock): """Device tracker zigpy device.""" diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 67ce6481542..b19c98548ce 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -35,6 +35,22 @@ def zigpy_dev_basic(zigpy_device_mock): ) +@pytest.fixture(autouse=True) +def required_platform_only(): + """Only setup the required and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", + ( + Platform.SENSOR, + Platform.LIGHT, + Platform.DEVICE_TRACKER, + Platform.NUMBER, + Platform.SELECT, + ), + ): + yield + + @pytest.fixture async def zha_dev_basic(hass, zha_device_restored, zigpy_dev_basic): """ZHA device with just a basic cluster.""" diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index 0615eeef623..262a743559f 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -20,6 +20,13 @@ DATA_RADIO_TYPE = "deconz" DATA_PORT_PATH = "/dev/serial/by-id/FTDI_USB__-__Serial_Cable_12345678-if00-port0" +@pytest.fixture(autouse=True) +def disable_platform_only(): + """Disable platforms to speed up tests.""" + with patch("homeassistant.components.zha.PLATFORMS", []): + yield + + @pytest.fixture def config_entry_v1(hass): """Config entry version 1 fixture.""" diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 8cf0e668503..dd6df0dff19 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -80,6 +80,24 @@ LIGHT_COLOR = { } +@pytest.fixture(autouse=True) +def light_platform_only(): + """Only setup the light and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", + ( + Platform.BINARY_SENSOR, + Platform.DEVICE_TRACKER, + Platform.BUTTON, + Platform.LIGHT, + Platform.SENSOR, + Platform.NUMBER, + Platform.SELECT, + ), + ): + yield + + @pytest.fixture async def coordinator(hass, zigpy_device_mock, zha_device_joined): """Test zha light platform.""" diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 0669cebf128..08b720b2ad7 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -27,6 +27,20 @@ CLEAR_PIN_CODE = 7 SET_USER_STATUS = 9 +@pytest.fixture(autouse=True) +def lock_platform_only(): + """Only setup the lock and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", + ( + Platform.DEVICE_TRACKER, + Platform.LOCK, + Platform.SENSOR, + ), + ): + yield + + @pytest.fixture async def lock(hass, zigpy_device_mock, zha_device_joined_restored): """Lock cluster fixture.""" diff --git a/tests/components/zha/test_logbook.py b/tests/components/zha/test_logbook.py index 00e1cc28ea6..33b758fd0a7 100644 --- a/tests/components/zha/test_logbook.py +++ b/tests/components/zha/test_logbook.py @@ -1,11 +1,13 @@ """ZHA logbook describe events tests.""" +from unittest.mock import patch + import pytest import zigpy.profiles.zha import zigpy.zcl.clusters.general as general from homeassistant.components.zha.core.const import ZHA_EVENT -from homeassistant.const import CONF_DEVICE_ID, CONF_UNIQUE_ID +from homeassistant.const import CONF_DEVICE_ID, CONF_UNIQUE_ID, Platform from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -29,6 +31,13 @@ UP = "up" DOWN = "down" +@pytest.fixture(autouse=True) +def sensor_platform_only(): + """Only setup the sensor and required base platforms to speed up tests.""" + with patch("homeassistant.components.zha.PLATFORMS", (Platform.SENSOR,)): + yield + + @pytest.fixture async def mock_devices(hass, zigpy_device_mock, zha_device_joined): """IAS device fixture.""" diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index 01946c05f1a..f6b606ccbbf 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -24,6 +24,23 @@ from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE from tests.common import mock_coro +@pytest.fixture(autouse=True) +def number_platform_only(): + """Only setup the number and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", + ( + Platform.BUTTON, + Platform.DEVICE_TRACKER, + Platform.LIGHT, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + ), + ): + yield + + @pytest.fixture def zigpy_analog_output_device(zigpy_device_mock): """Zigpy analog_output device.""" diff --git a/tests/components/zha/test_select.py b/tests/components/zha/test_select.py index 70b943d5ea2..c883d648e8e 100644 --- a/tests/components/zha/test_select.py +++ b/tests/components/zha/test_select.py @@ -1,6 +1,6 @@ """Test ZHA select entities.""" -from unittest.mock import call +from unittest.mock import call, patch import pytest from zigpy.const import SIG_EP_PROFILE @@ -16,6 +16,24 @@ from .common import find_entity_id from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE +@pytest.fixture(autouse=True) +def select_select_only(): + """Only setup the select and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", + ( + Platform.BUTTON, + Platform.DEVICE_TRACKER, + Platform.SIREN, + Platform.LIGHT, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + ), + ): + yield + + @pytest.fixture async def siren(hass, zigpy_device_mock, zha_device_joined_restored): """Siren fixture.""" diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 0d476fb8bda..c638bdd8c48 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -1,5 +1,6 @@ """Test zha sensor.""" import math +from unittest.mock import patch import pytest import zigpy.profiles.zha @@ -50,6 +51,19 @@ from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE ENTITY_ID_PREFIX = "sensor.fakemanufacturer_fakemodel_e769900a_{}" +@pytest.fixture(autouse=True) +def sensor_platform_only(): + """Only setup the sensor and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", + ( + Platform.DEVICE_TRACKER, + Platform.SENSOR, + ), + ): + yield + + @pytest.fixture async def elec_measurement_zigpy_dev(hass, zigpy_device_mock): """Electric Measurement zigpy device.""" diff --git a/tests/components/zha/test_siren.py b/tests/components/zha/test_siren.py index 285bc1cd585..72a40a8323e 100644 --- a/tests/components/zha/test_siren.py +++ b/tests/components/zha/test_siren.py @@ -28,6 +28,22 @@ from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE from tests.common import async_fire_time_changed, mock_coro +@pytest.fixture(autouse=True) +def siren_platform_only(): + """Only setup the siren and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", + ( + Platform.DEVICE_TRACKER, + Platform.NUMBER, + Platform.SENSOR, + Platform.SELECT, + Platform.SIREN, + ), + ): + yield + + @pytest.fixture async def siren(hass, zigpy_device_mock, zha_device_joined_restored): """Siren fixture.""" diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 99e8a681348..0b8fe658c28 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -41,6 +41,21 @@ IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" +@pytest.fixture(autouse=True) +def switch_platform_only(): + """Only setup the switch and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.zha.PLATFORMS", + ( + Platform.DEVICE_TRACKER, + Platform.SENSOR, + Platform.SELECT, + Platform.SWITCH, + ), + ): + yield + + @pytest.fixture def zigpy_device(zigpy_device_mock): """Device tracker zigpy device.""" From 600d23e0528a616afcf3fe18acdcfdce9bfeab07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Jun 2022 14:03:42 -0500 Subject: [PATCH 1562/3516] Retry on SenseAPIException during sense config entry setup (#73651) --- homeassistant/components/sense/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index 2e0ed4622e8..e938f7132e0 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -5,6 +5,7 @@ import logging from sense_energy import ( ASyncSenseable, + SenseAPIException, SenseAuthenticationException, SenseMFARequiredException, ) @@ -84,6 +85,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady( str(err) or "Timed out during authentication" ) from err + except SenseAPIException as err: + raise ConfigEntryNotReady(str(err)) from err sense_devices_data = SenseDevicesData() try: From 7a3f632c1d1b6f99ccefd637521ca20373a9fd38 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sat, 18 Jun 2022 05:13:07 +1000 Subject: [PATCH 1563/3516] Make stream recorder work concurrently (#73478) --- homeassistant/components/stream/__init__.py | 4 +- homeassistant/components/stream/core.py | 1 - homeassistant/components/stream/hls.py | 5 + homeassistant/components/stream/recorder.py | 219 ++++++++++--------- tests/components/stream/conftest.py | 59 +----- tests/components/stream/test_hls.py | 14 +- tests/components/stream/test_recorder.py | 221 ++++++++++---------- tests/components/stream/test_worker.py | 80 ++++--- 8 files changed, 297 insertions(+), 306 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 3766d981da5..b842eb7fb78 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -502,7 +502,6 @@ class Stream: recorder.video_path = video_path await self.start() - self._logger.debug("Started a stream recording of %s seconds", duration) # Take advantage of lookback hls: HlsStreamOutput = cast(HlsStreamOutput, self.outputs().get(HLS_PROVIDER)) @@ -512,6 +511,9 @@ class Stream: await hls.recv() recorder.prepend(list(hls.get_segments())[-num_segments - 1 : -1]) + self._logger.debug("Started a stream recording of %s seconds", duration) + await recorder.async_record() + async def async_get_image( self, width: int | None = None, diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index c8d831157a8..09d9a9d5031 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -327,7 +327,6 @@ class StreamOutput: """Handle cleanup.""" self._event.set() self.idle_timer.clear() - self._segments = deque(maxlen=self._segments.maxlen) class StreamView(HomeAssistantView): diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index efecdcbe9dc..d3bcbb360a6 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -60,6 +60,11 @@ class HlsStreamOutput(StreamOutput): """Return provider name.""" return HLS_PROVIDER + def cleanup(self) -> None: + """Handle cleanup.""" + super().cleanup() + self._segments.clear() + @property def target_duration(self) -> float: """Return the target duration.""" diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 4d97c0d683d..b33a5fbbf84 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -1,14 +1,11 @@ """Provide functionality to record stream.""" from __future__ import annotations -from collections import deque from io import BytesIO import logging import os -import threading import av -from av.container import OutputContainer from homeassistant.core import HomeAssistant, callback @@ -27,99 +24,9 @@ def async_setup_recorder(hass: HomeAssistant) -> None: """Only here so Provider Registry works.""" -def recorder_save_worker(file_out: str, segments: deque[Segment]) -> None: - """Handle saving stream.""" - - if not segments: - _LOGGER.error("Recording failed to capture anything") - return - - os.makedirs(os.path.dirname(file_out), exist_ok=True) - - pts_adjuster: dict[str, int | None] = {"video": None, "audio": None} - output: OutputContainer | None = None - output_v = None - output_a = None - - last_stream_id = None - # The running duration of processed segments. Note that this is in av.time_base - # units which seem to be defined inversely to how stream time_bases are defined - running_duration = 0 - - last_sequence = float("-inf") - for segment in segments: - # Because the stream_worker is in a different thread from the record service, - # the lookback segments may still have some overlap with the recorder segments - if segment.sequence <= last_sequence: - continue - last_sequence = segment.sequence - - # Open segment - source = av.open( - BytesIO(segment.init + segment.get_data()), - "r", - format=SEGMENT_CONTAINER_FORMAT, - ) - # Skip this segment if it doesn't have data - if source.duration is None: - source.close() - continue - source_v = source.streams.video[0] - source_a = source.streams.audio[0] if len(source.streams.audio) > 0 else None - - # Create output on first segment - if not output: - output = av.open( - file_out, - "w", - format=RECORDER_CONTAINER_FORMAT, - container_options={ - "video_track_timescale": str(int(1 / source_v.time_base)) - }, - ) - - # Add output streams if necessary - if not output_v: - output_v = output.add_stream(template=source_v) - context = output_v.codec_context - context.flags |= "GLOBAL_HEADER" - if source_a and not output_a: - output_a = output.add_stream(template=source_a) - - # Recalculate pts adjustments on first segment and on any discontinuity - # We are assuming time base is the same across all discontinuities - if last_stream_id != segment.stream_id: - last_stream_id = segment.stream_id - pts_adjuster["video"] = int( - (running_duration - source.start_time) - / (av.time_base * source_v.time_base) - ) - if source_a: - pts_adjuster["audio"] = int( - (running_duration - source.start_time) - / (av.time_base * source_a.time_base) - ) - - # Remux video - for packet in source.demux(): - if packet.dts is None: - continue - packet.pts += pts_adjuster[packet.stream.type] - packet.dts += pts_adjuster[packet.stream.type] - packet.stream = output_v if packet.stream.type == "video" else output_a - output.mux(packet) - - running_duration += source.duration - source.start_time - - source.close() - - if output is not None: - output.close() - - @PROVIDERS.register(RECORDER_PROVIDER) class RecorderOutput(StreamOutput): - """Represents HLS Output formats.""" + """Represents the Recorder Output format.""" def __init__( self, @@ -141,13 +48,119 @@ class RecorderOutput(StreamOutput): self._segments.extendleft(reversed(segments)) def cleanup(self) -> None: - """Write recording and clean up.""" - _LOGGER.debug("Starting recorder worker thread") - thread = threading.Thread( - name="recorder_save_worker", - target=recorder_save_worker, - args=(self.video_path, self._segments.copy()), - ) - thread.start() - + """Handle cleanup.""" + self.idle_timer.idle = True super().cleanup() + + async def async_record(self) -> None: + """Handle saving stream.""" + + os.makedirs(os.path.dirname(self.video_path), exist_ok=True) + + pts_adjuster: dict[str, int | None] = {"video": None, "audio": None} + output: av.container.OutputContainer | None = None + output_v = None + output_a = None + + last_stream_id = -1 + # The running duration of processed segments. Note that this is in av.time_base + # units which seem to be defined inversely to how stream time_bases are defined + running_duration = 0 + + last_sequence = float("-inf") + + def write_segment(segment: Segment) -> None: + """Write a segment to output.""" + nonlocal output, output_v, output_a, last_stream_id, running_duration, last_sequence + # Because the stream_worker is in a different thread from the record service, + # the lookback segments may still have some overlap with the recorder segments + if segment.sequence <= last_sequence: + return + last_sequence = segment.sequence + + # Open segment + source = av.open( + BytesIO(segment.init + segment.get_data()), + "r", + format=SEGMENT_CONTAINER_FORMAT, + ) + # Skip this segment if it doesn't have data + if source.duration is None: + source.close() + return + source_v = source.streams.video[0] + source_a = ( + source.streams.audio[0] if len(source.streams.audio) > 0 else None + ) + + # Create output on first segment + if not output: + output = av.open( + self.video_path + ".tmp", + "w", + format=RECORDER_CONTAINER_FORMAT, + container_options={ + "video_track_timescale": str(int(1 / source_v.time_base)) + }, + ) + + # Add output streams if necessary + if not output_v: + output_v = output.add_stream(template=source_v) + context = output_v.codec_context + context.flags |= "GLOBAL_HEADER" + if source_a and not output_a: + output_a = output.add_stream(template=source_a) + + # Recalculate pts adjustments on first segment and on any discontinuity + # We are assuming time base is the same across all discontinuities + if last_stream_id != segment.stream_id: + last_stream_id = segment.stream_id + pts_adjuster["video"] = int( + (running_duration - source.start_time) + / (av.time_base * source_v.time_base) + ) + if source_a: + pts_adjuster["audio"] = int( + (running_duration - source.start_time) + / (av.time_base * source_a.time_base) + ) + + # Remux video + for packet in source.demux(): + if packet.dts is None: + continue + packet.pts += pts_adjuster[packet.stream.type] + packet.dts += pts_adjuster[packet.stream.type] + packet.stream = output_v if packet.stream.type == "video" else output_a + output.mux(packet) + + running_duration += source.duration - source.start_time + + source.close() + + # Write lookback segments + while len(self._segments) > 1: # The last segment is in progress + await self._hass.async_add_executor_job( + write_segment, self._segments.popleft() + ) + # Make sure the first segment has been added + if not self._segments: + await self.recv() + # Write segments as soon as they are completed + while not self.idle: + await self.recv() + await self._hass.async_add_executor_job( + write_segment, self._segments.popleft() + ) + # Write remaining segments + # Should only have 0 or 1 segments, but loop through just in case + while self._segments: + await self._hass.async_add_executor_job( + write_segment, self._segments.popleft() + ) + if output is None: + _LOGGER.error("Recording failed to capture anything") + else: + output.close() + os.rename(self.video_path + ".tmp", self.video_path) diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index a3d2da8bd52..91b4106c1f4 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -12,7 +12,6 @@ so that it can inspect the output. from __future__ import annotations import asyncio -from collections import deque from http import HTTPStatus import logging import threading @@ -20,10 +19,9 @@ from typing import Generator from unittest.mock import Mock, patch from aiohttp import web -import async_timeout import pytest -from homeassistant.components.stream.core import Segment, StreamOutput +from homeassistant.components.stream.core import StreamOutput from homeassistant.components.stream.worker import StreamState from .common import generate_h264_video, stream_teardown @@ -73,61 +71,6 @@ def stream_worker_sync(hass): yield sync -class SaveRecordWorkerSync: - """ - Test fixture to manage RecordOutput thread for recorder_save_worker. - - This is used to assert that the worker is started and stopped cleanly - to avoid thread leaks in tests. - """ - - def __init__(self, hass): - """Initialize SaveRecordWorkerSync.""" - self._hass = hass - self._save_event = None - self._segments = None - self._save_thread = None - self.reset() - - def recorder_save_worker(self, file_out: str, segments: deque[Segment]): - """Mock method for patch.""" - logging.debug("recorder_save_worker thread started") - assert self._save_thread is None - self._segments = segments - self._save_thread = threading.current_thread() - self._hass.loop.call_soon_threadsafe(self._save_event.set) - - async def get_segments(self): - """Return the recorded video segments.""" - async with async_timeout.timeout(TEST_TIMEOUT): - await self._save_event.wait() - return self._segments - - async def join(self): - """Verify save worker was invoked and block on shutdown.""" - async with async_timeout.timeout(TEST_TIMEOUT): - await self._save_event.wait() - self._save_thread.join(timeout=TEST_TIMEOUT) - assert not self._save_thread.is_alive() - - def reset(self): - """Reset callback state for reuse in tests.""" - self._save_thread = None - self._save_event = asyncio.Event() - - -@pytest.fixture() -def record_worker_sync(hass): - """Patch recorder_save_worker for clean thread shutdown for test.""" - sync = SaveRecordWorkerSync(hass) - with patch( - "homeassistant.components.stream.recorder.recorder_save_worker", - side_effect=sync.recorder_save_worker, - autospec=True, - ): - yield sync - - class HLSSync: """Test fixture that intercepts stream worker calls to StreamOutput.""" diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 7343b96ef9a..715e69fb889 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -506,10 +506,12 @@ async def test_remove_incomplete_segment_on_exit( assert len(segments) == 3 assert not segments[-1].complete stream_worker_sync.resume() - stream._thread_quit.set() - stream._thread.join() - stream._thread = None - await hass.async_block_till_done() - assert segments[-1].complete - assert len(segments) == 2 + with patch("homeassistant.components.stream.Stream.remove_provider"): + # Patch remove_provider so the deque is not cleared + stream._thread_quit.set() + stream._thread.join() + stream._thread = None + await hass.async_block_till_done() + assert segments[-1].complete + assert len(segments) == 2 await stream.stop() diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index 9433cbd449d..d7595b47679 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -1,4 +1,5 @@ -"""The tests for hls streams.""" +"""The tests for recording streams.""" +import asyncio from datetime import timedelta from io import BytesIO import os @@ -7,11 +8,14 @@ from unittest.mock import patch import av import pytest -from homeassistant.components.stream import create_stream -from homeassistant.components.stream.const import HLS_PROVIDER, RECORDER_PROVIDER +from homeassistant.components.stream import Stream, create_stream +from homeassistant.components.stream.const import ( + HLS_PROVIDER, + OUTPUT_IDLE_TIMEOUT, + RECORDER_PROVIDER, +) from homeassistant.components.stream.core import Part from homeassistant.components.stream.fmp4utils import find_box -from homeassistant.components.stream.recorder import recorder_save_worker from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -20,40 +24,55 @@ from .common import DefaultSegment as Segment, generate_h264_video, remux_with_a from tests.common import async_fire_time_changed -MAX_ABORT_SEGMENTS = 20 # Abort test to avoid looping forever - -async def test_record_stream(hass, hass_client, record_worker_sync, h264_video): - """ - Test record stream. - - Tests full integration with the stream component, and captures the - stream worker and save worker to allow for clean shutdown of background - threads. The actual save logic is tested in test_recorder_save below. - """ +@pytest.fixture(autouse=True) +async def stream_component(hass): + """Set up the component before each test.""" await async_setup_component(hass, "stream", {"stream": {}}) - # Setup demo track - stream = create_stream(hass, h264_video, {}) + +@pytest.fixture +def filename(tmpdir): + """Use this filename for the tests.""" + return f"{tmpdir}/test.mp4" + + +async def test_record_stream(hass, filename, h264_video): + """Test record stream.""" + + worker_finished = asyncio.Event() + + class MockStream(Stream): + """Mock Stream so we can patch remove_provider.""" + + async def remove_provider(self, provider): + """Add a finished event to Stream.remove_provider.""" + await Stream.remove_provider(self, provider) + worker_finished.set() + + with patch("homeassistant.components.stream.Stream", wraps=MockStream): + stream = create_stream(hass, h264_video, {}) + with patch.object(hass.config, "is_allowed_path", return_value=True): - await stream.async_record("/example/path") + make_recording = hass.async_create_task(stream.async_record(filename)) - # After stream decoding finishes, the record worker thread starts - segments = await record_worker_sync.get_segments() - assert len(segments) >= 1 + # In general usage the recorder will only include what has already been + # processed by the worker. To guarantee we have some output for the test, + # wait until the worker has finished before firing + await worker_finished.wait() - # Verify that the save worker was invoked, then block until its - # thread completes and is shutdown completely to avoid thread leaks. - await record_worker_sync.join() + # Fire the IdleTimer + future = dt_util.utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, future) - await stream.stop() + await make_recording + + # Assert + assert os.path.exists(filename) -async def test_record_lookback( - hass, hass_client, stream_worker_sync, record_worker_sync, h264_video -): +async def test_record_lookback(hass, h264_video): """Exercise record with loopback.""" - await async_setup_component(hass, "stream", {"stream": {}}) stream = create_stream(hass, h264_video, {}) @@ -69,42 +88,8 @@ async def test_record_lookback( await stream.stop() -async def test_recorder_timeout(hass, hass_client, stream_worker_sync, h264_video): - """ - Test recorder timeout. - - Mocks out the cleanup to assert that it is invoked after a timeout. - This test does not start the recorder save thread. - """ - await async_setup_component(hass, "stream", {"stream": {}}) - - stream_worker_sync.pause() - - with patch("homeassistant.components.stream.IdleTimer.fire") as mock_timeout: - # Setup demo track - stream = create_stream(hass, h264_video, {}) - with patch.object(hass.config, "is_allowed_path", return_value=True): - await stream.async_record("/example/path") - recorder = stream.add_provider(RECORDER_PROVIDER) - - await recorder.recv() - - # Wait a minute - future = dt_util.utcnow() + timedelta(minutes=1) - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - assert mock_timeout.called - - stream_worker_sync.resume() - await stream.stop() - await hass.async_block_till_done() - await hass.async_block_till_done() - - -async def test_record_path_not_allowed(hass, hass_client, h264_video): +async def test_record_path_not_allowed(hass, h264_video): """Test where the output path is not allowed by home assistant configuration.""" - await async_setup_component(hass, "stream", {"stream": {}}) stream = create_stream(hass, h264_video, {}) with patch.object( @@ -127,25 +112,8 @@ def add_parts_to_segment(segment, source): ] -async def test_recorder_save(tmpdir, h264_video): - """Test recorder save.""" - # Setup - filename = f"{tmpdir}/test.mp4" - - # Run - segment = Segment(sequence=1) - add_parts_to_segment(segment, h264_video) - segment.duration = 4 - recorder_save_worker(filename, [segment]) - - # Assert - assert os.path.exists(filename) - - -async def test_recorder_discontinuity(tmpdir, h264_video): +async def test_recorder_discontinuity(hass, filename, h264_video): """Test recorder save across a discontinuity.""" - # Setup - filename = f"{tmpdir}/test.mp4" # Run segment_1 = Segment(sequence=1, stream_id=0) @@ -154,18 +122,50 @@ async def test_recorder_discontinuity(tmpdir, h264_video): segment_2 = Segment(sequence=2, stream_id=1) add_parts_to_segment(segment_2, h264_video) segment_2.duration = 4 - recorder_save_worker(filename, [segment_1, segment_2]) + + provider_ready = asyncio.Event() + + class MockStream(Stream): + """Mock Stream so we can patch add_provider.""" + + async def start(self): + """Make Stream.start a noop that gives up async context.""" + await asyncio.sleep(0) + + def add_provider(self, fmt, timeout=OUTPUT_IDLE_TIMEOUT): + """Add a finished event to Stream.add_provider.""" + provider = Stream.add_provider(self, fmt, timeout) + provider_ready.set() + return provider + + with patch.object(hass.config, "is_allowed_path", return_value=True), patch( + "homeassistant.components.stream.Stream", wraps=MockStream + ), patch("homeassistant.components.stream.recorder.RecorderOutput.recv"): + stream = create_stream(hass, "blank", {}) + make_recording = hass.async_create_task(stream.async_record(filename)) + await provider_ready.wait() + + recorder_output = stream.outputs()[RECORDER_PROVIDER] + recorder_output.idle_timer.start() + recorder_output._segments.extend([segment_1, segment_2]) + + # Fire the IdleTimer + future = dt_util.utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, future) + + await make_recording # Assert assert os.path.exists(filename) -async def test_recorder_no_segments(tmpdir): +async def test_recorder_no_segments(hass, filename): """Test recorder behavior with a stream failure which causes no segments.""" - # Setup - filename = f"{tmpdir}/test.mp4" + + stream = create_stream(hass, BytesIO(), {}) # Run - recorder_save_worker("unused-file", []) + with patch.object(hass.config, "is_allowed_path", return_value=True): + await stream.async_record(filename) # Assert assert not os.path.exists(filename) @@ -188,9 +188,7 @@ def h264_mov_video(): ) async def test_record_stream_audio( hass, - hass_client, - stream_worker_sync, - record_worker_sync, + filename, audio_codec, expected_audio_streams, h264_mov_video, @@ -201,28 +199,42 @@ async def test_record_stream_audio( Record stream output should have an audio channel when input has a valid codec and audio packets and no audio channel otherwise. """ - await async_setup_component(hass, "stream", {"stream": {}}) # Remux source video with new audio source = remux_with_audio(h264_mov_video, "mov", audio_codec) # mov can store PCM - record_worker_sync.reset() - stream_worker_sync.pause() + worker_finished = asyncio.Event() + + class MockStream(Stream): + """Mock Stream so we can patch remove_provider.""" + + async def remove_provider(self, provider): + """Add a finished event to Stream.remove_provider.""" + await Stream.remove_provider(self, provider) + worker_finished.set() + + with patch("homeassistant.components.stream.Stream", wraps=MockStream): + stream = create_stream(hass, source, {}) - stream = create_stream(hass, source, {}) with patch.object(hass.config, "is_allowed_path", return_value=True): - await stream.async_record("/example/path") - recorder = stream.add_provider(RECORDER_PROVIDER) + make_recording = hass.async_create_task(stream.async_record(filename)) - while True: - await recorder.recv() - if not (segment := recorder.last_segment): - break - last_segment = segment - stream_worker_sync.resume() + # In general usage the recorder will only include what has already been + # processed by the worker. To guarantee we have some output for the test, + # wait until the worker has finished before firing + await worker_finished.wait() + + # Fire the IdleTimer + future = dt_util.utcnow() + timedelta(seconds=30) + async_fire_time_changed(hass, future) + + await make_recording + + # Assert + assert os.path.exists(filename) result = av.open( - BytesIO(last_segment.init + last_segment.get_data()), + filename, "r", format="mp4", ) @@ -232,14 +244,9 @@ async def test_record_stream_audio( await stream.stop() await hass.async_block_till_done() - # Verify that the save worker was invoked, then block until its - # thread completes and is shutdown completely to avoid thread leaks. - await record_worker_sync.join() - async def test_recorder_log(hass, caplog): """Test starting a stream to record logs the url without username and password.""" - await async_setup_component(hass, "stream", {"stream": {}}) stream = create_stream(hass, "https://abcd:efgh@foo.bar", {}) with patch.object(hass.config, "is_allowed_path", return_value=True): await stream.async_record("/example/path") diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 298d7287e69..863a289c2c5 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -13,6 +13,7 @@ pushed to the output streams. The packet sequence can be used to exercise failure modes or corner cases like how out of order packets are handled. """ +import asyncio import fractions import io import logging @@ -33,6 +34,7 @@ from homeassistant.components.stream.const import ( HLS_PROVIDER, MAX_MISSING_DTS, PACKETS_TO_WAIT_FOR_AUDIO, + RECORDER_PROVIDER, SEGMENT_DURATION_ADJUSTER, TARGET_SEGMENT_DURATION_NON_LL_HLS, ) @@ -732,7 +734,23 @@ async def test_worker_log(hass, caplog): assert "https://abcd:efgh@foo.bar" not in caplog.text -async def test_durations(hass, record_worker_sync): +@pytest.fixture +def worker_finished_stream(): + """Fixture that helps call a stream and wait for the worker to finish.""" + worker_finished = asyncio.Event() + + class MockStream(Stream): + """Mock Stream so we can patch remove_provider.""" + + async def remove_provider(self, provider): + """Add a finished event to Stream.remove_provider.""" + await Stream.remove_provider(self, provider) + worker_finished.set() + + return worker_finished, MockStream + + +async def test_durations(hass, worker_finished_stream): """Test that the duration metadata matches the media.""" # Use a target part duration which has a slight mismatch @@ -751,13 +769,17 @@ async def test_durations(hass, record_worker_sync): ) source = generate_h264_video(duration=SEGMENT_DURATION + 1) - stream = create_stream(hass, source, {}, stream_label="camera") + worker_finished, mock_stream = worker_finished_stream - # use record_worker_sync to grab output segments - with patch.object(hass.config, "is_allowed_path", return_value=True): - await stream.async_record("/example/path") + with patch("homeassistant.components.stream.Stream", wraps=mock_stream): + stream = create_stream(hass, source, {}, stream_label="camera") + + recorder_output = stream.add_provider(RECORDER_PROVIDER, timeout=30) + await stream.start() + await worker_finished.wait() + + complete_segments = list(recorder_output.get_segments())[:-1] - complete_segments = list(await record_worker_sync.get_segments())[:-1] assert len(complete_segments) >= 1 # check that the Part duration metadata matches the durations in the media @@ -803,12 +825,10 @@ async def test_durations(hass, record_worker_sync): abs_tol=1e-6, ) - await record_worker_sync.join() - await stream.stop() -async def test_has_keyframe(hass, record_worker_sync, h264_video): +async def test_has_keyframe(hass, h264_video, worker_finished_stream): """Test that the has_keyframe metadata matches the media.""" await async_setup_component( hass, @@ -824,13 +844,17 @@ async def test_has_keyframe(hass, record_worker_sync, h264_video): }, ) - stream = create_stream(hass, h264_video, {}, stream_label="camera") + worker_finished, mock_stream = worker_finished_stream - # use record_worker_sync to grab output segments - with patch.object(hass.config, "is_allowed_path", return_value=True): - await stream.async_record("/example/path") + with patch("homeassistant.components.stream.Stream", wraps=mock_stream): + stream = create_stream(hass, h264_video, {}, stream_label="camera") + + recorder_output = stream.add_provider(RECORDER_PROVIDER, timeout=30) + await stream.start() + await worker_finished.wait() + + complete_segments = list(recorder_output.get_segments())[:-1] - complete_segments = list(await record_worker_sync.get_segments())[:-1] assert len(complete_segments) >= 1 # check that the Part has_keyframe metadata matches the keyframes in the media @@ -843,12 +867,10 @@ async def test_has_keyframe(hass, record_worker_sync, h264_video): av_part.close() assert part.has_keyframe == media_has_keyframe - await record_worker_sync.join() - await stream.stop() -async def test_h265_video_is_hvc1(hass, record_worker_sync): +async def test_h265_video_is_hvc1(hass, worker_finished_stream): """Test that a h265 video gets muxed as hvc1.""" await async_setup_component( hass, @@ -863,13 +885,16 @@ async def test_h265_video_is_hvc1(hass, record_worker_sync): ) source = generate_h265_video() - stream = create_stream(hass, source, {}, stream_label="camera") - # use record_worker_sync to grab output segments - with patch.object(hass.config, "is_allowed_path", return_value=True): - await stream.async_record("/example/path") + worker_finished, mock_stream = worker_finished_stream + with patch("homeassistant.components.stream.Stream", wraps=mock_stream): + stream = create_stream(hass, source, {}, stream_label="camera") - complete_segments = list(await record_worker_sync.get_segments())[:-1] + recorder_output = stream.add_provider(RECORDER_PROVIDER, timeout=30) + await stream.start() + await worker_finished.wait() + + complete_segments = list(recorder_output.get_segments())[:-1] assert len(complete_segments) >= 1 segment = complete_segments[0] @@ -878,8 +903,6 @@ async def test_h265_video_is_hvc1(hass, record_worker_sync): assert av_part.streams.video[0].codec_tag == "hvc1" av_part.close() - await record_worker_sync.join() - await stream.stop() assert stream.get_diagnostics() == { @@ -891,7 +914,7 @@ async def test_h265_video_is_hvc1(hass, record_worker_sync): } -async def test_get_image(hass, record_worker_sync): +async def test_get_image(hass): """Test that the has_keyframe metadata matches the media.""" await async_setup_component(hass, "stream", {"stream": {}}) @@ -904,14 +927,11 @@ async def test_get_image(hass, record_worker_sync): mock_turbo_jpeg_singleton.instance.return_value = mock_turbo_jpeg() stream = create_stream(hass, source, {}) - # use record_worker_sync to grab output segments with patch.object(hass.config, "is_allowed_path", return_value=True): - await stream.async_record("/example/path") - + make_recording = hass.async_create_task(stream.async_record("/example/path")) + await make_recording assert stream._keyframe_converter._image is None - await record_worker_sync.join() - assert await stream.async_get_image() == EMPTY_8_6_JPEG await stream.stop() From 027f54ca15f50f205eabf39e18ac7eaca63e5817 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 18 Jun 2022 00:24:33 +0000 Subject: [PATCH 1564/3516] [ci skip] Translation update --- .../components/generic/translations/en.json | 7 +++++++ .../components/group/translations/bg.json | 3 ++- .../components/nest/translations/ca.json | 2 +- .../components/nest/translations/ja.json | 21 +++++++++++++++++++ .../components/nest/translations/pl.json | 5 ++++- 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json index 4b10ed2d8ac..d01e6e59a4b 100644 --- a/homeassistant/components/generic/translations/en.json +++ b/homeassistant/components/generic/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "no_devices_found": "No devices found on the network", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { @@ -11,7 +12,9 @@ "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", + "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", @@ -48,9 +51,13 @@ "already_exists": "A camera with these URL settings already exists.", "invalid_still_image": "URL did not return a valid still image", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", + "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", + "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", + "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", diff --git a/homeassistant/components/group/translations/bg.json b/homeassistant/components/group/translations/bg.json index d0982fcfb66..5ecb4d66328 100644 --- a/homeassistant/components/group/translations/bg.json +++ b/homeassistant/components/group/translations/bg.json @@ -17,7 +17,8 @@ "data": { "entities": "\u0427\u043b\u0435\u043d\u043e\u0432\u0435", "name": "\u0418\u043c\u0435" - } + }, + "title": "\u0414\u043e\u0431\u0430\u0432\u044f\u043d\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0430" }, "media_player": { "data": { diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index c7b617f01d6..7b1a9c91bb4 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -19,7 +19,7 @@ "subscriber_error": "Error de subscriptor desconegut, consulta els registres", "timeout": "S'ha acabat el temps d'espera durant la validaci\u00f3 del codi.", "unknown": "Error inesperat", - "wrong_project_id": "Introdueix un ID de projecte Cloud v\u00e0lid (s'ha trobat un ID de projecte d'Acc\u00e9s de Dispositiu)" + "wrong_project_id": "Introdueix un ID de projecte Cloud v\u00e0lid (era el mateix que l'ID de projecte Device Access)" }, "step": { "auth": { diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index c2994de0532..37613407e4d 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -29,6 +29,27 @@ "description": "Google\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30ea\u30f3\u30af\u3059\u308b\u306b\u306f\u3001 [authorize your account]({url}) \u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002\n\n\u8a8d\u8a3c\u5f8c\u3001\u63d0\u4f9b\u3055\u308c\u305f\u8a8d\u8a3c\u30c8\u30fc\u30af\u30f3\u306e\u30b3\u30fc\u30c9\u3092\u4ee5\u4e0b\u306b\u30b3\u30d4\u30fc\u30da\u30fc\u30b9\u30c8\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Google\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30ea\u30f3\u30af\u3059\u308b" }, + "auth_upgrade": { + "title": "\u30cd\u30b9\u30c8: \u30a2\u30d7\u30ea\u8a8d\u8a3c\u306e\u975e\u63a8\u5968" + }, + "cloud_project": { + "data": { + "cloud_project_id": "Google Cloud\u30d7\u30ed\u30b8\u30a7\u30af\u30c8ID" + }, + "title": "\u30cd\u30b9\u30c8: \u30af\u30e9\u30a6\u30c9\u30d7\u30ed\u30b8\u30a7\u30af\u30c8ID\u3092\u5165\u529b" + }, + "create_cloud_project": { + "title": "\u30cd\u30b9\u30c8: \u30af\u30e9\u30a6\u30c9\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u4f5c\u6210\u3068\u8a2d\u5b9a" + }, + "device_project": { + "data": { + "project_id": "\u30c7\u30d0\u30a4\u30b9\u30a2\u30af\u30bb\u30b9\u30d7\u30ed\u30b8\u30a7\u30af\u30c8ID" + }, + "title": "\u30cd\u30b9\u30c8: \u30c7\u30d0\u30a4\u30b9\u30a2\u30af\u30bb\u30b9\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u4f5c\u6210" + }, + "device_project_upgrade": { + "title": "\u30cd\u30b9\u30c8: \u30c7\u30d0\u30a4\u30b9\u30a2\u30af\u30bb\u30b9\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306e\u66f4\u65b0" + }, "init": { "data": { "flow_impl": "\u30d7\u30ed\u30d0\u30a4\u30c0\u30fc" diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json index 5e9b9895db4..4c53185e71f 100644 --- a/homeassistant/components/nest/translations/pl.json +++ b/homeassistant/components/nest/translations/pl.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "Post\u0119puj zgodnie z [instrukcj\u0105]({more_info_url}), aby skonfigurowa\u0107 Cloud Console: \n\n1. Przejd\u017a do [ekranu akceptacji OAuth]( {oauth_consent_url} ) i skonfiguruj\n2. Przejd\u017a do [Po\u015bwiadczenia]({oauth_creds_url}) i kliknij **Utw\u00f3rz po\u015bwiadczenia**.\n3. Z listy rozwijanej wybierz **ID klienta OAuth**.\n4. Wybierz **Aplikacja internetowa** jako Typ aplikacji.\n5. Dodaj `{redirect_url}` pod *Autoryzowany URI przekierowania*." + }, "config": { "abort": { "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", @@ -19,7 +22,7 @@ "subscriber_error": "Nieznany b\u0142\u0105d subskrybenta, zobacz logi", "timeout": "Przekroczono limit czasu sprawdzania poprawno\u015bci kodu", "unknown": "Nieoczekiwany b\u0142\u0105d", - "wrong_project_id": "Podaj prawid\u0142owy Identyfikator projektu chmury (znaleziono identyfikator projektu dost\u0119pu do urz\u0105dzenia)" + "wrong_project_id": "Podaj prawid\u0142owy Identyfikator projektu chmury (taki sam jak identyfikator projektu dost\u0119pu do urz\u0105dzenia)" }, "step": { "auth": { From 7a792b093f5a17491a97288f810b2fde5eb921f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Jun 2022 21:57:44 -0500 Subject: [PATCH 1565/3516] Fix calling permanent off with nexia (#73623) * Fix calling permanent off with nexia Changelog: https://github.com/bdraco/nexia/compare/1.0.1...1.0.2 Fixes #73610 * one more --- homeassistant/components/nexia/climate.py | 4 +++- homeassistant/components/nexia/manifest.json | 2 +- homeassistant/components/nexia/switch.py | 6 +++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 20fcf5c6b85..33ad91e1561 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -391,7 +391,9 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the system mode (Auto, Heat_Cool, Cool, Heat, etc).""" - if hvac_mode == HVACMode.AUTO: + if hvac_mode == HVACMode.OFF: + await self._zone.call_permanent_off() + elif hvac_mode == HVACMode.AUTO: await self._zone.call_return_to_schedule() await self._zone.set_mode(mode=OPERATION_MODE_AUTO) else: diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index f9ca21d9e0b..4bae2d9a15d 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==1.0.1"], + "requirements": ["nexia==1.0.2"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/homeassistant/components/nexia/switch.py b/homeassistant/components/nexia/switch.py index 380fea8c4a0..e242032c947 100644 --- a/homeassistant/components/nexia/switch.py +++ b/homeassistant/components/nexia/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any +from nexia.const import OPERATION_MODE_OFF from nexia.home import NexiaHome from nexia.thermostat import NexiaThermostat from nexia.zone import NexiaThermostatZone @@ -58,7 +59,10 @@ class NexiaHoldSwitch(NexiaThermostatZoneEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Enable permanent hold.""" - await self._zone.call_permanent_hold() + if self._zone.get_current_mode() == OPERATION_MODE_OFF: + await self._zone.call_permanent_off() + else: + await self._zone.call_permanent_hold() self._signal_zone_update() async def async_turn_off(self, **kwargs: Any) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index 6ea4e85475a..79ba4689352 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1083,7 +1083,7 @@ nettigo-air-monitor==1.3.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==1.0.1 +nexia==1.0.2 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b696ce6829e..886c0921e27 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -748,7 +748,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.3.0 # homeassistant.components.nexia -nexia==1.0.1 +nexia==1.0.2 # homeassistant.components.discord nextcord==2.0.0a8 From 0a272113566f8f99f5370cc4ecca63466102fc6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Jun 2022 22:45:20 -0500 Subject: [PATCH 1566/3516] Switch bond data to use a dataclass (#73514) --- homeassistant/components/bond/__init__.py | 15 ++++++--------- homeassistant/components/bond/button.py | 12 +++++------- homeassistant/components/bond/const.py | 3 --- homeassistant/components/bond/cover.py | 10 ++++++---- homeassistant/components/bond/diagnostics.py | 8 ++++---- homeassistant/components/bond/fan.py | 9 +++++---- homeassistant/components/bond/light.py | 9 ++++----- homeassistant/components/bond/models.py | 16 ++++++++++++++++ homeassistant/components/bond/switch.py | 18 ++++++------------ 9 files changed, 52 insertions(+), 48 deletions(-) create mode 100644 homeassistant/components/bond/models.py diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 476423631c3..7dca4db507d 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -20,7 +20,8 @@ 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_SUBS, BRIDGE_MAKE, DOMAIN, HUB +from .const import BRIDGE_MAKE, DOMAIN +from .models import BondData from .utils import BondHub PLATFORMS = [ @@ -69,11 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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, - } + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BondData(hub, bpup_subs) if not entry.unique_id: hass.config_entries.async_update_entry(entry, unique_id=hub.bond_id) @@ -102,8 +99,7 @@ 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) - if unload_ok: + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) return unload_ok @@ -125,7 +121,8 @@ async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry ) -> bool: """Remove bond config entry from a device.""" - hub: BondHub = hass.data[DOMAIN][config_entry.entry_id][HUB] + data: BondData = hass.data[DOMAIN][config_entry.entry_id] + hub = data.hub for identifier in device_entry.identifiers: if identifier[0] != DOMAIN or len(identifier) != 3: continue diff --git a/homeassistant/components/bond/button.py b/homeassistant/components/bond/button.py index ffdb01b9d88..2c6ffc69693 100644 --- a/homeassistant/components/bond/button.py +++ b/homeassistant/components/bond/button.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -import logging from typing import Any from bond_async import Action, BPUPSubscriptions @@ -12,12 +11,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import BPUP_SUBS, DOMAIN, HUB +from .const import DOMAIN from .entity import BondEntity +from .models import BondData from .utils import BondDevice, BondHub -_LOGGER = logging.getLogger(__name__) - # The api requires a step size even though it does not # seem to matter what is is as the underlying device is likely # getting an increase/decrease signal only @@ -246,9 +244,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Bond button devices.""" - data = hass.data[DOMAIN][entry.entry_id] - hub: BondHub = data[HUB] - bpup_subs: BPUPSubscriptions = data[BPUP_SUBS] + data: BondData = hass.data[DOMAIN][entry.entry_id] + hub = data.hub + bpup_subs = data.bpup_subs entities: list[BondButtonEntity] = [] for device in hub.devices: diff --git a/homeassistant/components/bond/const.py b/homeassistant/components/bond/const.py index 778dcbc1a1f..91197763d23 100644 --- a/homeassistant/components/bond/const.py +++ b/homeassistant/components/bond/const.py @@ -7,9 +7,6 @@ DOMAIN = "bond" CONF_BOND_ID: str = "bond_id" -HUB = "hub" -BPUP_SUBS = "bpup_subs" - SERVICE_SET_FAN_SPEED_TRACKED_STATE = "set_fan_speed_tracked_state" SERVICE_SET_POWER_TRACKED_STATE = "set_switch_power_tracked_state" SERVICE_SET_LIGHT_POWER_TRACKED_STATE = "set_light_power_tracked_state" diff --git a/homeassistant/components/bond/cover.py b/homeassistant/components/bond/cover.py index efe72f947f4..0a3e9048451 100644 --- a/homeassistant/components/bond/cover.py +++ b/homeassistant/components/bond/cover.py @@ -15,8 +15,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import BPUP_SUBS, DOMAIN, HUB +from .const import DOMAIN from .entity import BondEntity +from .models import BondData from .utils import BondDevice, BondHub @@ -36,9 +37,10 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Bond cover devices.""" - data = hass.data[DOMAIN][entry.entry_id] - hub: BondHub = data[HUB] - bpup_subs: BPUPSubscriptions = data[BPUP_SUBS] + data: BondData = hass.data[DOMAIN][entry.entry_id] + hub = data.hub + bpup_subs = data.bpup_subs + async_add_entities( BondCover(hub, device, bpup_subs) for device in hub.devices diff --git a/homeassistant/components/bond/diagnostics.py b/homeassistant/components/bond/diagnostics.py index 6af62c3fb24..53e8b5c8225 100644 --- a/homeassistant/components/bond/diagnostics.py +++ b/homeassistant/components/bond/diagnostics.py @@ -7,8 +7,8 @@ from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DOMAIN, HUB -from .utils import BondHub +from .const import DOMAIN +from .models import BondData TO_REDACT = {"access_token"} @@ -17,8 +17,8 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data = hass.data[DOMAIN][entry.entry_id] - hub: BondHub = data[HUB] + data: BondData = hass.data[DOMAIN][entry.entry_id] + hub = data.hub return { "entry": { "title": entry.title, diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py index 12eef9c44b0..d1121e4a3a8 100644 --- a/homeassistant/components/bond/fan.py +++ b/homeassistant/components/bond/fan.py @@ -26,8 +26,9 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from .const import BPUP_SUBS, DOMAIN, HUB, SERVICE_SET_FAN_SPEED_TRACKED_STATE +from .const import DOMAIN, SERVICE_SET_FAN_SPEED_TRACKED_STATE from .entity import BondEntity +from .models import BondData from .utils import BondDevice, BondHub _LOGGER = logging.getLogger(__name__) @@ -41,9 +42,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Bond fan devices.""" - data = hass.data[DOMAIN][entry.entry_id] - hub: BondHub = data[HUB] - bpup_subs: BPUPSubscriptions = data[BPUP_SUBS] + data: BondData = hass.data[DOMAIN][entry.entry_id] + hub = data.hub + bpup_subs = data.bpup_subs platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_FAN_SPEED_TRACKED_STATE, diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py index 5a76ea6a13e..2fcff44ddc1 100644 --- a/homeassistant/components/bond/light.py +++ b/homeassistant/components/bond/light.py @@ -18,13 +18,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ATTR_POWER_STATE, - BPUP_SUBS, DOMAIN, - HUB, SERVICE_SET_LIGHT_BRIGHTNESS_TRACKED_STATE, SERVICE_SET_LIGHT_POWER_TRACKED_STATE, ) from .entity import BondEntity +from .models import BondData from .utils import BondDevice, BondHub _LOGGER = logging.getLogger(__name__) @@ -46,9 +45,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Bond light devices.""" - data = hass.data[DOMAIN][entry.entry_id] - hub: BondHub = data[HUB] - bpup_subs: BPUPSubscriptions = data[BPUP_SUBS] + data: BondData = hass.data[DOMAIN][entry.entry_id] + hub = data.hub + bpup_subs = data.bpup_subs platform = entity_platform.async_get_current_platform() platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/bond/models.py b/homeassistant/components/bond/models.py new file mode 100644 index 00000000000..0caa01af7a0 --- /dev/null +++ b/homeassistant/components/bond/models.py @@ -0,0 +1,16 @@ +"""The bond integration models.""" +from __future__ import annotations + +from dataclasses import dataclass + +from bond_async import BPUPSubscriptions + +from .utils import BondHub + + +@dataclass +class BondData: + """Data for the bond integration.""" + + hub: BondHub + bpup_subs: BPUPSubscriptions diff --git a/homeassistant/components/bond/switch.py b/homeassistant/components/bond/switch.py index a88be924610..afa5e1cee10 100644 --- a/homeassistant/components/bond/switch.py +++ b/homeassistant/components/bond/switch.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from aiohttp.client_exceptions import ClientResponseError -from bond_async import Action, BPUPSubscriptions, DeviceType +from bond_async import Action, DeviceType import voluptuous as vol from homeassistant.components.switch import SwitchEntity @@ -14,15 +14,9 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - ATTR_POWER_STATE, - BPUP_SUBS, - DOMAIN, - HUB, - SERVICE_SET_POWER_TRACKED_STATE, -) +from .const import ATTR_POWER_STATE, DOMAIN, SERVICE_SET_POWER_TRACKED_STATE from .entity import BondEntity -from .utils import BondHub +from .models import BondData async def async_setup_entry( @@ -31,9 +25,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Bond generic devices.""" - data = hass.data[DOMAIN][entry.entry_id] - hub: BondHub = data[HUB] - bpup_subs: BPUPSubscriptions = data[BPUP_SUBS] + data: BondData = hass.data[DOMAIN][entry.entry_id] + hub = data.hub + bpup_subs = data.bpup_subs platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_POWER_TRACKED_STATE, From f4c3bd7e0069e7a7d11a8ff08865b86815a500d3 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 18 Jun 2022 11:41:26 +0200 Subject: [PATCH 1567/3516] Fix issue with pandas wheels (#73669) * Fix issue with pandas wheels * Update builder --- .github/workflows/wheels.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e86f8235582..15aef8b752a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -138,9 +138,10 @@ jobs: for requirement_file in ${requirement_files}; do sed -i "s|numpy==1.21.6|numpy==1.22.4|g" ${requirement_file} done + echo "numpy==1.22.4" >> homeassistant/package_constraints.txt - name: Build wheels - uses: home-assistant/wheels@2022.06.5 + uses: home-assistant/wheels@2022.06.6 with: abi: cp310 tag: musllinux_1_2 @@ -271,9 +272,10 @@ jobs: for requirement_file in ${requirement_files}; do sed -i "s|numpy==1.21.6|numpy==1.22.4|g" ${requirement_file} done + echo "numpy==1.22.4" >> homeassistant/package_constraints.txt - name: Build wheels - uses: home-assistant/wheels@2022.06.5 + uses: home-assistant/wheels@2022.06.6 with: abi: cp310 tag: musllinux_1_2 From 691d49f23ba42c4ee7f053920b10b41322913730 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sat, 18 Jun 2022 13:56:28 -0400 Subject: [PATCH 1568/3516] Refactor migration code for UniFi Protect (#73499) --- .../components/unifiprotect/__init__.py | 60 +------ .../components/unifiprotect/migrate.py | 83 +++++++++ tests/components/unifiprotect/test_init.py | 144 ---------------- tests/components/unifiprotect/test_migrate.py | 158 ++++++++++++++++++ 4 files changed, 244 insertions(+), 201 deletions(-) create mode 100644 homeassistant/components/unifiprotect/migrate.py create mode 100644 tests/components/unifiprotect/test_migrate.py diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 4ec11a899e3..d05f544ada1 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -17,11 +17,10 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, - Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_create_clientsession from .const import ( @@ -37,6 +36,7 @@ from .const import ( ) from .data import ProtectData, async_ufp_instance_for_config_entry_ids from .discovery import async_start_discovery +from .migrate import async_migrate_data from .services import async_cleanup_services, async_setup_services from .utils import _async_unifi_mac_from_hass, async_get_devices @@ -45,60 +45,6 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=DEFAULT_SCAN_INTERVAL) -async def _async_migrate_data( - hass: HomeAssistant, entry: ConfigEntry, protect: ProtectApiClient -) -> None: - - registry = er.async_get(hass) - to_migrate = [] - for entity in er.async_entries_for_config_entry(registry, entry.entry_id): - if entity.domain == Platform.BUTTON and "_" not in entity.unique_id: - _LOGGER.debug("Button %s needs migration", entity.entity_id) - to_migrate.append(entity) - - if len(to_migrate) == 0: - _LOGGER.debug("No entities need migration") - return - - _LOGGER.info("Migrating %s reboot button entities ", len(to_migrate)) - bootstrap = await protect.get_bootstrap() - count = 0 - for button in to_migrate: - device = None - for model in DEVICES_THAT_ADOPT: - attr = f"{model.value}s" - device = getattr(bootstrap, attr).get(button.unique_id) - if device is not None: - break - - if device is None: - continue - - new_unique_id = f"{device.id}_reboot" - _LOGGER.debug( - "Migrating entity %s (old unique_id: %s, new unique_id: %s)", - button.entity_id, - button.unique_id, - new_unique_id, - ) - try: - registry.async_update_entity(button.entity_id, new_unique_id=new_unique_id) - except ValueError: - _LOGGER.warning( - "Could not migrate entity %s (old unique_id: %s, new unique_id: %s)", - button.entity_id, - button.unique_id, - new_unique_id, - ) - else: - count += 1 - - if count < len(to_migrate): - _LOGGER.warning("Failed to migate %s reboot buttons", len(to_migrate) - count) - else: - _LOGGER.info("Migrated %s reboot button entities", count) - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the UniFi Protect config entries.""" @@ -133,7 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False - await _async_migrate_data(hass, entry, protect) + await async_migrate_data(hass, entry, protect) if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=nvr_info.mac) diff --git a/homeassistant/components/unifiprotect/migrate.py b/homeassistant/components/unifiprotect/migrate.py new file mode 100644 index 00000000000..dcba0b504c9 --- /dev/null +++ b/homeassistant/components/unifiprotect/migrate.py @@ -0,0 +1,83 @@ +"""UniFi Protect data migrations.""" +from __future__ import annotations + +import logging + +from pyunifiprotect import ProtectApiClient + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .const import DEVICES_THAT_ADOPT + +_LOGGER = logging.getLogger(__name__) + + +async def async_migrate_data( + hass: HomeAssistant, entry: ConfigEntry, protect: ProtectApiClient +) -> None: + """Run all valid UniFi Protect data migrations.""" + + _LOGGER.debug("Start Migrate: async_migrate_buttons") + await async_migrate_buttons(hass, entry, protect) + _LOGGER.debug("Completed Migrate: async_migrate_buttons") + + +async def async_migrate_buttons( + hass: HomeAssistant, entry: ConfigEntry, protect: ProtectApiClient +) -> None: + """ + Migrate existing Reboot button unique IDs from {device_id} to {deivce_id}_reboot. + + This allows for additional types of buttons that are outside of just a reboot button. + + Added in 2022.6.0. + """ + + registry = er.async_get(hass) + to_migrate = [] + for entity in er.async_entries_for_config_entry(registry, entry.entry_id): + if entity.domain == Platform.BUTTON and "_" not in entity.unique_id: + _LOGGER.debug("Button %s needs migration", entity.entity_id) + to_migrate.append(entity) + + if len(to_migrate) == 0: + _LOGGER.debug("No button entities need migration") + return + + bootstrap = await protect.get_bootstrap() + count = 0 + for button in to_migrate: + device = None + for model in DEVICES_THAT_ADOPT: + attr = f"{model.value}s" + device = getattr(bootstrap, attr).get(button.unique_id) + if device is not None: + break + + if device is None: + continue + + new_unique_id = f"{device.id}_reboot" + _LOGGER.debug( + "Migrating entity %s (old unique_id: %s, new unique_id: %s)", + button.entity_id, + button.unique_id, + new_unique_id, + ) + try: + registry.async_update_entity(button.entity_id, new_unique_id=new_unique_id) + except ValueError: + _LOGGER.warning( + "Could not migrate entity %s (old unique_id: %s, new unique_id: %s)", + button.entity_id, + button.unique_id, + new_unique_id, + ) + else: + count += 1 + + if count < len(to_migrate): + _LOGGER.warning("Failed to migate %s reboot buttons", len(to_migrate) - count) diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index cf899d854fd..68f171b52bf 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -11,7 +11,6 @@ from pyunifiprotect.data import NVR, Light from homeassistant.components.unifiprotect.const import CONF_DISABLE_RTSP, DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component @@ -199,149 +198,6 @@ async def test_setup_starts_discovery( assert len(hass.config_entries.flow.async_progress_by_handler(DOMAIN)) == 1 -async def test_migrate_reboot_button( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light -): - """Test migrating unique ID of reboot button.""" - - light1 = mock_light.copy() - light1._api = mock_entry.api - light1.name = "Test Light 1" - light1.id = "lightid1" - - light2 = mock_light.copy() - light2._api = mock_entry.api - light2.name = "Test Light 2" - light2.id = "lightid2" - mock_entry.api.bootstrap.lights = { - light1.id: light1, - light2.id: light2, - } - mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) - - registry = er.async_get(hass) - registry.async_get_or_create( - Platform.BUTTON, DOMAIN, light1.id, config_entry=mock_entry.entry - ) - registry.async_get_or_create( - Platform.BUTTON, - DOMAIN, - f"{light2.id}_reboot", - config_entry=mock_entry.entry, - ) - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert mock_entry.entry.state == ConfigEntryState.LOADED - assert mock_entry.api.update.called - assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac - - buttons = [] - for entity in er.async_entries_for_config_entry( - registry, mock_entry.entry.entry_id - ): - if entity.domain == Platform.BUTTON.value: - buttons.append(entity) - print(entity.entity_id) - assert len(buttons) == 2 - - assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device") is None - assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device_2") is None - light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_lightid1") - assert light is not None - assert light.unique_id == f"{light1.id}_reboot" - - assert registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device") is None - assert registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device_2") is None - light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_lightid2_reboot") - assert light is not None - assert light.unique_id == f"{light2.id}_reboot" - - -async def test_migrate_reboot_button_no_device( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light -): - """Test migrating unique ID of reboot button if UniFi Protect device ID changed.""" - - light1 = mock_light.copy() - light1._api = mock_entry.api - light1.name = "Test Light 1" - light1.id = "lightid1" - - mock_entry.api.bootstrap.lights = { - light1.id: light1, - } - mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) - - registry = er.async_get(hass) - registry.async_get_or_create( - Platform.BUTTON, DOMAIN, "lightid2", config_entry=mock_entry.entry - ) - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert mock_entry.entry.state == ConfigEntryState.LOADED - assert mock_entry.api.update.called - assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac - - buttons = [] - for entity in er.async_entries_for_config_entry( - registry, mock_entry.entry.entry_id - ): - if entity.domain == Platform.BUTTON.value: - buttons.append(entity) - assert len(buttons) == 2 - - light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_lightid2") - assert light is not None - assert light.unique_id == "lightid2" - - -async def test_migrate_reboot_button_fail( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light -): - """Test migrating unique ID of reboot button.""" - - light1 = mock_light.copy() - light1._api = mock_entry.api - light1.name = "Test Light 1" - light1.id = "lightid1" - - mock_entry.api.bootstrap.lights = { - light1.id: light1, - } - mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) - - registry = er.async_get(hass) - registry.async_get_or_create( - Platform.BUTTON, - DOMAIN, - light1.id, - config_entry=mock_entry.entry, - suggested_object_id=light1.name, - ) - registry.async_get_or_create( - Platform.BUTTON, - DOMAIN, - f"{light1.id}_reboot", - config_entry=mock_entry.entry, - suggested_object_id=light1.name, - ) - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert mock_entry.entry.state == ConfigEntryState.LOADED - assert mock_entry.api.update.called - assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac - - light = registry.async_get(f"{Platform.BUTTON}.test_light_1") - assert light is not None - assert light.unique_id == f"{light1.id}" - - async def test_device_remove_devices( hass: HomeAssistant, mock_entry: MockEntityFixture, diff --git a/tests/components/unifiprotect/test_migrate.py b/tests/components/unifiprotect/test_migrate.py new file mode 100644 index 00000000000..756672bcbca --- /dev/null +++ b/tests/components/unifiprotect/test_migrate.py @@ -0,0 +1,158 @@ +"""Test the UniFi Protect setup flow.""" +# pylint: disable=protected-access +from __future__ import annotations + +from unittest.mock import AsyncMock + +from pyunifiprotect.data import Light + +from homeassistant.components.unifiprotect.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import MockEntityFixture + + +async def test_migrate_reboot_button( + hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light +): + """Test migrating unique ID of reboot button.""" + + light1 = mock_light.copy() + light1._api = mock_entry.api + light1.name = "Test Light 1" + light1.id = "lightid1" + + light2 = mock_light.copy() + light2._api = mock_entry.api + light2.name = "Test Light 2" + light2.id = "lightid2" + mock_entry.api.bootstrap.lights = { + light1.id: light1, + light2.id: light2, + } + mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) + + registry = er.async_get(hass) + registry.async_get_or_create( + Platform.BUTTON, DOMAIN, light1.id, config_entry=mock_entry.entry + ) + registry.async_get_or_create( + Platform.BUTTON, + DOMAIN, + f"{light2.id}_reboot", + config_entry=mock_entry.entry, + ) + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + + assert mock_entry.entry.state == ConfigEntryState.LOADED + assert mock_entry.api.update.called + assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac + + buttons = [] + for entity in er.async_entries_for_config_entry( + registry, mock_entry.entry.entry_id + ): + if entity.domain == Platform.BUTTON.value: + buttons.append(entity) + print(entity.entity_id) + assert len(buttons) == 2 + + assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device") is None + assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device_2") is None + light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_lightid1") + assert light is not None + assert light.unique_id == f"{light1.id}_reboot" + + assert registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device") is None + assert registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device_2") is None + light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_lightid2_reboot") + assert light is not None + assert light.unique_id == f"{light2.id}_reboot" + + +async def test_migrate_reboot_button_no_device( + hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light +): + """Test migrating unique ID of reboot button if UniFi Protect device ID changed.""" + + light1 = mock_light.copy() + light1._api = mock_entry.api + light1.name = "Test Light 1" + light1.id = "lightid1" + + mock_entry.api.bootstrap.lights = { + light1.id: light1, + } + mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) + + registry = er.async_get(hass) + registry.async_get_or_create( + Platform.BUTTON, DOMAIN, "lightid2", config_entry=mock_entry.entry + ) + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + + assert mock_entry.entry.state == ConfigEntryState.LOADED + assert mock_entry.api.update.called + assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac + + buttons = [] + for entity in er.async_entries_for_config_entry( + registry, mock_entry.entry.entry_id + ): + if entity.domain == Platform.BUTTON.value: + buttons.append(entity) + assert len(buttons) == 2 + + light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_lightid2") + assert light is not None + assert light.unique_id == "lightid2" + + +async def test_migrate_reboot_button_fail( + hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light +): + """Test migrating unique ID of reboot button.""" + + light1 = mock_light.copy() + light1._api = mock_entry.api + light1.name = "Test Light 1" + light1.id = "lightid1" + + mock_entry.api.bootstrap.lights = { + light1.id: light1, + } + mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) + + registry = er.async_get(hass) + registry.async_get_or_create( + Platform.BUTTON, + DOMAIN, + light1.id, + config_entry=mock_entry.entry, + suggested_object_id=light1.name, + ) + registry.async_get_or_create( + Platform.BUTTON, + DOMAIN, + f"{light1.id}_reboot", + config_entry=mock_entry.entry, + suggested_object_id=light1.name, + ) + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + + assert mock_entry.entry.state == ConfigEntryState.LOADED + assert mock_entry.api.update.called + assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac + + light = registry.async_get(f"{Platform.BUTTON}.test_light_1") + assert light is not None + assert light.unique_id == f"{light1.id}" From 046d7d2a239e0eab370ff9d11b1885ee9c69dffe Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 18 Jun 2022 19:58:10 +0200 Subject: [PATCH 1569/3516] Add tests for trafikverket_ferry (#71912) --- .coveragerc | 3 - .../components/trafikverket_ferry/__init__.py | 17 +++ .../components/trafikverket_ferry/conftest.py | 80 +++++++++++++ .../trafikverket_ferry/test_coordinator.py | 107 ++++++++++++++++++ .../trafikverket_ferry/test_init.py | 61 ++++++++++ .../trafikverket_ferry/test_sensor.py | 48 ++++++++ 6 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 tests/components/trafikverket_ferry/conftest.py create mode 100644 tests/components/trafikverket_ferry/test_coordinator.py create mode 100644 tests/components/trafikverket_ferry/test_init.py create mode 100644 tests/components/trafikverket_ferry/test_sensor.py diff --git a/.coveragerc b/.coveragerc index fc40f0f35b9..46cbed3a0dc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1301,9 +1301,6 @@ omit = homeassistant/components/tradfri/light.py homeassistant/components/tradfri/sensor.py homeassistant/components/tradfri/switch.py - homeassistant/components/trafikverket_ferry/__init__.py - homeassistant/components/trafikverket_ferry/coordinator.py - homeassistant/components/trafikverket_ferry/sensor.py homeassistant/components/trafikverket_train/__init__.py homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/__init__.py diff --git a/tests/components/trafikverket_ferry/__init__.py b/tests/components/trafikverket_ferry/__init__.py index 4a1491c5bed..97bedb30281 100644 --- a/tests/components/trafikverket_ferry/__init__.py +++ b/tests/components/trafikverket_ferry/__init__.py @@ -1 +1,18 @@ """Tests for the Trafikverket Ferry integration.""" +from __future__ import annotations + +from homeassistant.components.trafikverket_ferry.const import ( + CONF_FROM, + CONF_TIME, + CONF_TO, +) +from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS + +ENTRY_CONFIG = { + CONF_API_KEY: "1234567890", + CONF_FROM: "Harbor 1", + CONF_TO: "Harbor 2", + CONF_TIME: "00:00:00", + CONF_WEEKDAY: WEEKDAYS, + CONF_NAME: "Harbor1", +} diff --git a/tests/components/trafikverket_ferry/conftest.py b/tests/components/trafikverket_ferry/conftest.py new file mode 100644 index 00000000000..452c351ee5d --- /dev/null +++ b/tests/components/trafikverket_ferry/conftest.py @@ -0,0 +1,80 @@ +"""Fixtures for Trafikverket Ferry integration tests.""" +from __future__ import annotations + +from datetime import datetime, timedelta +from unittest.mock import patch + +import pytest +from pytrafikverket.trafikverket_ferry import FerryStop + +from homeassistant.components.trafikverket_ferry.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.util import dt + +from . import ENTRY_CONFIG + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="load_int") +async def load_integration_from_entry( + hass: HomeAssistant, get_ferries: list[FerryStop] +) -> MockConfigEntry: + """Set up the Trafikverket Ferry integration in Home Assistant.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="123", + ) + + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.trafikverket_ferry.coordinator.TrafikverketFerry.async_get_next_ferry_stops", + return_value=get_ferries, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +@pytest.fixture(name="get_ferries") +def fixture_get_ferries() -> list[FerryStop]: + """Construct FerryStop Mock.""" + + depart1 = FerryStop( + "13", + False, + datetime(dt.now().year + 1, 5, 1, 12, 0, tzinfo=dt.UTC), + [""], + "0", + datetime(dt.now().year, 5, 1, 12, 0, tzinfo=dt.UTC), + "Harbor 1", + "Harbor 2", + ) + depart2 = FerryStop( + "14", + False, + datetime(dt.now().year + 1, 5, 1, 12, 0, tzinfo=dt.UTC) + timedelta(minutes=15), + [""], + "0", + datetime(dt.now().year, 5, 1, 12, 0, tzinfo=dt.UTC), + "Harbor 1", + "Harbor 2", + ) + depart3 = FerryStop( + "15", + False, + datetime(dt.now().year + 1, 5, 1, 12, 0, tzinfo=dt.UTC) + timedelta(minutes=30), + [""], + "0", + datetime(dt.now().year, 5, 1, 12, 0, tzinfo=dt.UTC), + "Harbor 1", + "Harbor 2", + ) + + return [depart1, depart2, depart3] diff --git a/tests/components/trafikverket_ferry/test_coordinator.py b/tests/components/trafikverket_ferry/test_coordinator.py new file mode 100644 index 00000000000..7714e0c38f6 --- /dev/null +++ b/tests/components/trafikverket_ferry/test_coordinator.py @@ -0,0 +1,107 @@ +"""The test for the Trafikverket Ferry coordinator.""" +from __future__ import annotations + +from datetime import date, datetime, timedelta +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +import pytest +from pytrafikverket.trafikverket_ferry import FerryStop + +from homeassistant.components.trafikverket_ferry.const import DOMAIN +from homeassistant.components.trafikverket_ferry.coordinator import next_departuredate +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import STATE_UNAVAILABLE, WEEKDAYS +from homeassistant.core import HomeAssistant +from homeassistant.util import dt + +from . import ENTRY_CONFIG + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_coordinator( + hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, + monkeypatch: pytest.MonkeyPatch, + get_ferries: list[FerryStop], +) -> None: + """Test the Trafikverket Ferry coordinator.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="123", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.trafikverket_ferry.coordinator.TrafikverketFerry.async_get_next_ferry_stops", + return_value=get_ferries, + ) as mock_data: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + mock_data.assert_called_once() + state1 = hass.states.get("sensor.harbor1_departure_from") + state2 = hass.states.get("sensor.harbor1_departure_to") + state3 = hass.states.get("sensor.harbor1_departure_time") + assert state1.state == "Harbor 1" + assert state2.state == "Harbor 2" + assert state3.state == str(dt.now().year + 1) + "-05-01T12:00:00+00:00" + mock_data.reset_mock() + + monkeypatch.setattr( + get_ferries[0], + "departure_time", + datetime(dt.now().year + 2, 5, 1, 12, 0, tzinfo=dt.UTC), + ) + + async_fire_time_changed(hass, dt.utcnow() + timedelta(minutes=6)) + await hass.async_block_till_done() + mock_data.assert_called_once() + state1 = hass.states.get("sensor.harbor1_departure_from") + state2 = hass.states.get("sensor.harbor1_departure_to") + state3 = hass.states.get("sensor.harbor1_departure_time") + assert state1.state == "Harbor 1" + assert state2.state == "Harbor 2" + assert state3.state == str(dt.now().year + 2) + "-05-01T12:00:00+00:00" + mock_data.reset_mock() + + mock_data.side_effect = ValueError("info") + async_fire_time_changed(hass, dt.utcnow() + timedelta(minutes=6)) + await hass.async_block_till_done() + mock_data.assert_called_once() + state1 = hass.states.get("sensor.harbor1_departure_from") + assert state1.state == STATE_UNAVAILABLE + mock_data.reset_mock() + + mock_data.return_value = get_ferries + mock_data.side_effect = None + async_fire_time_changed(hass, dt.utcnow() + timedelta(minutes=6)) + await hass.async_block_till_done() + mock_data.assert_called_once() + state1 = hass.states.get("sensor.harbor1_departure_from") + assert state1.state == "Harbor 1" + mock_data.reset_mock() + + +async def test_coordinator_next_departuredate(freezer: FrozenDateTimeFactory) -> None: + """Test the Trafikverket Ferry next_departuredate calculation.""" + freezer.move_to("2022-05-15") + today = date.today() + day_list = ["wed", "thu", "fri", "sat"] + test = next_departuredate(day_list) + assert test == today + timedelta(days=3) + day_list = WEEKDAYS + test = next_departuredate(day_list) + assert test == today + timedelta(days=0) + day_list = ["sun"] + test = next_departuredate(day_list) + assert test == today + timedelta(days=0) + freezer.move_to("2022-05-16") + today = date.today() + day_list = ["wed", "thu", "fri", "sat", "sun"] + test = next_departuredate(day_list) + assert test == today + timedelta(days=2) diff --git a/tests/components/trafikverket_ferry/test_init.py b/tests/components/trafikverket_ferry/test_init.py new file mode 100644 index 00000000000..d5063ab704c --- /dev/null +++ b/tests/components/trafikverket_ferry/test_init.py @@ -0,0 +1,61 @@ +"""Test for Trafikverket Ferry component Init.""" +from __future__ import annotations + +from unittest.mock import patch + +from pytrafikverket.trafikverket_ferry import FerryStop + +from homeassistant import config_entries +from homeassistant.components.trafikverket_ferry.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant + +from . import ENTRY_CONFIG + +from tests.common import MockConfigEntry + + +async def test_setup_entry(hass: HomeAssistant, get_ferries: list[FerryStop]) -> None: + """Test setup entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="123", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.trafikverket_ferry.coordinator.TrafikverketFerry.async_get_next_ferry_stops", + return_value=get_ferries, + ) as mock_tvt_ferry: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is config_entries.ConfigEntryState.LOADED + assert len(mock_tvt_ferry.mock_calls) == 1 + + +async def test_unload_entry(hass: HomeAssistant, get_ferries: list[FerryStop]) -> None: + """Test unload an entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=ENTRY_CONFIG, + entry_id="1", + unique_id="321", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.trafikverket_ferry.coordinator.TrafikverketFerry.async_get_next_ferry_stops", + return_value=get_ferries, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is config_entries.ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED diff --git a/tests/components/trafikverket_ferry/test_sensor.py b/tests/components/trafikverket_ferry/test_sensor.py new file mode 100644 index 00000000000..4353eb5c8ba --- /dev/null +++ b/tests/components/trafikverket_ferry/test_sensor.py @@ -0,0 +1,48 @@ +"""The test for the Trafikverket sensor platform.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +from pytest import MonkeyPatch +from pytrafikverket.trafikverket_ferry import FerryStop + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.util import dt + +from tests.common import async_fire_time_changed + + +async def test_sensor( + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: MonkeyPatch, + get_ferries: list[FerryStop], +) -> None: + """Test the Trafikverket Ferry sensor.""" + state1 = hass.states.get("sensor.harbor1_departure_from") + state2 = hass.states.get("sensor.harbor1_departure_to") + state3 = hass.states.get("sensor.harbor1_departure_time") + assert state1.state == "Harbor 1" + assert state2.state == "Harbor 2" + assert state3.state == str(dt.now().year + 1) + "-05-01T12:00:00+00:00" + assert state1.attributes["icon"] == "mdi:ferry" + assert state1.attributes["other_information"] == [""] + assert state2.attributes["icon"] == "mdi:ferry" + + monkeypatch.setattr(get_ferries[0], "other_information", ["Nothing exiting"]) + + with patch( + "homeassistant.components.trafikverket_ferry.coordinator.TrafikverketFerry.async_get_next_ferry_stops", + return_value=get_ferries, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=6), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("sensor.harbor1_departure_from") + assert state1.state == "Harbor 1" + assert state1.attributes["other_information"] == ["Nothing exiting"] From d5df2b2ee771c8ce607c87a4b522295676f404b9 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 18 Jun 2022 22:15:44 +0200 Subject: [PATCH 1570/3516] Sensibo Add Pure Boost Service (#73114) * Pure Boost Service * Fix tests * Fix mypy * One service to two services * Minor fix test * Fix issues --- homeassistant/components/sensibo/climate.py | 61 +++++++ homeassistant/components/sensibo/entity.py | 7 + .../components/sensibo/services.yaml | 53 ++++++ tests/components/sensibo/test_climate.py | 156 ++++++++++++++++++ 4 files changed, 277 insertions(+) diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index f0ce7b74c01..c146ed350f3 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -29,6 +29,16 @@ from .entity import SensiboDeviceBaseEntity SERVICE_ASSUME_STATE = "assume_state" SERVICE_TIMER = "timer" ATTR_MINUTES = "minutes" +SERVICE_ENABLE_PURE_BOOST = "enable_pure_boost" +SERVICE_DISABLE_PURE_BOOST = "disable_pure_boost" + +ATTR_AC_INTEGRATION = "ac_integration" +ATTR_GEO_INTEGRATION = "geo_integration" +ATTR_INDOOR_INTEGRATION = "indoor_integration" +ATTR_OUTDOOR_INTEGRATION = "outdoor_integration" +ATTR_SENSITIVITY = "sensitivity" +BOOST_INCLUSIVE = "boost_inclusive" + PARALLEL_UPDATES = 0 FIELD_TO_FLAG = { @@ -95,6 +105,24 @@ async def async_setup_entry( }, "async_set_timer", ) + platform.async_register_entity_service( + SERVICE_ENABLE_PURE_BOOST, + { + vol.Inclusive(ATTR_AC_INTEGRATION, "settings"): bool, + vol.Inclusive(ATTR_GEO_INTEGRATION, "settings"): bool, + vol.Inclusive(ATTR_INDOOR_INTEGRATION, "settings"): bool, + vol.Inclusive(ATTR_OUTDOOR_INTEGRATION, "settings"): bool, + vol.Inclusive(ATTR_SENSITIVITY, "settings"): vol.In( + ["Normal", "Sensitive"] + ), + }, + "async_enable_pure_boost", + ) + platform.async_register_entity_service( + SERVICE_DISABLE_PURE_BOOST, + {}, + "async_disable_pure_boost", + ) class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): @@ -308,3 +336,36 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): if result["status"] == "success": return await self.coordinator.async_request_refresh() raise HomeAssistantError(f"Could not set timer for device {self.name}") + + async def async_enable_pure_boost( + self, + ac_integration: bool | None = None, + geo_integration: bool | None = None, + indoor_integration: bool | None = None, + outdoor_integration: bool | None = None, + sensitivity: str | None = None, + ) -> None: + """Enable Pure Boost Configuration.""" + + params: dict[str, str | bool] = { + "enabled": True, + } + if sensitivity is not None: + params["sensitivity"] = sensitivity[0] + if indoor_integration is not None: + params["measurementsIntegration"] = indoor_integration + if ac_integration is not None: + params["acIntegration"] = ac_integration + if geo_integration is not None: + params["geoIntegration"] = geo_integration + if outdoor_integration is not None: + params["primeIntegration"] = outdoor_integration + + await self.async_send_command("set_pure_boost", params) + await self.coordinator.async_refresh() + + async def async_disable_pure_boost(self) -> None: + """Disable Pure Boost Configuration.""" + + await self.async_send_command("set_pure_boost", {"enabled": False}) + await self.coordinator.async_refresh() diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index 430d7ac61ac..bf70f499ec6 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -99,6 +99,13 @@ class SensiboDeviceBaseEntity(SensiboBaseEntity): result = await self._client.async_set_timer(self._device_id, params) if command == "del_timer": result = await self._client.async_del_timer(self._device_id) + if command == "set_pure_boost": + if TYPE_CHECKING: + assert params is not None + result = await self._client.async_set_pureboost( + self._device_id, + params, + ) return result diff --git a/homeassistant/components/sensibo/services.yaml b/homeassistant/components/sensibo/services.yaml index 1a64f8703b4..67006074f6b 100644 --- a/homeassistant/components/sensibo/services.yaml +++ b/homeassistant/components/sensibo/services.yaml @@ -44,3 +44,56 @@ timer: min: 0 step: 1 mode: box +enable_pure_boost: + name: Enable Pure Boost + description: Enable and configure Pure Boost settings. + target: + entity: + integration: sensibo + domain: climate + fields: + ac_integration: + name: AC Integration + description: Integrate with Air Conditioner. + required: false + example: true + selector: + boolean: + geo_integration: + name: Geo Integration + description: Integrate with Presence. + required: false + example: true + selector: + boolean: + indoor_integration: + name: Indoor Air Quality + description: Integrate with checking indoor air quality. + required: false + example: true + selector: + boolean: + outdoor_integration: + name: Outdoor Air Quality + description: Integrate with checking outdoor air quality. + required: false + example: true + selector: + boolean: + sensitivity: + name: Sensitivity + description: Set the sensitivity for Pure Boost. + required: false + example: "Normal" + selector: + select: + options: + - "Normal" + - "Sensitive" +disable_pure_boost: + name: Disable Pure Boost + description: Disable Pure Boost. + target: + entity: + integration: sensibo + domain: climate diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index 16e83162600..30356f2b00d 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -21,8 +21,15 @@ from homeassistant.components.climate.const import ( SERVICE_SET_TEMPERATURE, ) from homeassistant.components.sensibo.climate import ( + ATTR_AC_INTEGRATION, + ATTR_GEO_INTEGRATION, + ATTR_INDOOR_INTEGRATION, ATTR_MINUTES, + ATTR_OUTDOOR_INTEGRATION, + ATTR_SENSITIVITY, SERVICE_ASSUME_STATE, + SERVICE_DISABLE_PURE_BOOST, + SERVICE_ENABLE_PURE_BOOST, SERVICE_TIMER, _find_valid_target_temp, ) @@ -905,3 +912,152 @@ async def test_climate_set_timer_failures( blocking=True, ) await hass.async_block_till_done() + + +async def test_climate_pure_boost( + hass: HomeAssistant, + entity_registry_enabled_by_default: AsyncMock, + load_int: ConfigEntry, + monkeypatch: pytest.MonkeyPatch, + get_data: SensiboData, +) -> None: + """Test the Sensibo climate assumed state service.""" + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state_climate = hass.states.get("climate.kitchen") + state2 = hass.states.get("binary_sensor.kitchen_pure_boost_enabled") + assert state2.state == "off" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_pureboost", + ): + with pytest.raises(MultipleInvalid): + await hass.services.async_call( + DOMAIN, + SERVICE_ENABLE_PURE_BOOST, + { + ATTR_ENTITY_ID: state_climate.entity_id, + ATTR_INDOOR_INTEGRATION: True, + ATTR_OUTDOOR_INTEGRATION: True, + ATTR_SENSITIVITY: "Sensitive", + }, + blocking=True, + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_pureboost", + return_value={ + "status": "success", + "result": { + "enabled": True, + "sensitivity": "S", + "measurements_integration": True, + "ac_integration": False, + "geo_integration": False, + "prime_integration": True, + }, + }, + ): + await hass.services.async_call( + DOMAIN, + SERVICE_ENABLE_PURE_BOOST, + { + ATTR_ENTITY_ID: state_climate.entity_id, + ATTR_AC_INTEGRATION: False, + ATTR_GEO_INTEGRATION: False, + ATTR_INDOOR_INTEGRATION: True, + ATTR_OUTDOOR_INTEGRATION: True, + ATTR_SENSITIVITY: "Sensitive", + }, + blocking=True, + ) + await hass.async_block_till_done() + + monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_boost_enabled", True) + monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_sensitivity", "s") + monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_measure_integration", True) + monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_prime_integration", True) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("binary_sensor.kitchen_pure_boost_enabled") + state2 = hass.states.get( + "binary_sensor.kitchen_pure_boost_linked_with_indoor_air_quality" + ) + state3 = hass.states.get( + "binary_sensor.kitchen_pure_boost_linked_with_outdoor_air_quality" + ) + state4 = hass.states.get("sensor.kitchen_pure_sensitivity") + assert state1.state == "on" + assert state2.state == "on" + assert state3.state == "on" + assert state4.state == "s" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_set_pureboost", + return_value={ + "status": "success", + "result": { + "enabled": False, + "sensitivity": "S", + "measurements_integration": True, + "ac_integration": False, + "geo_integration": False, + "prime_integration": True, + }, + }, + ) as mock_set_pureboost: + await hass.services.async_call( + DOMAIN, + SERVICE_DISABLE_PURE_BOOST, + { + ATTR_ENTITY_ID: state_climate.entity_id, + }, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_pureboost.assert_called_once() + + monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_boost_enabled", False) + monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_sensitivity", "s") + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("binary_sensor.kitchen_pure_boost_enabled") + state4 = hass.states.get("sensor.kitchen_pure_sensitivity") + assert state1.state == "off" + assert state4.state == "s" From dcf6c2d3a41aa45112011277c9ff97c051c3d866 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 19 Jun 2022 00:23:14 +0000 Subject: [PATCH 1571/3516] [ci skip] Translation update --- .../eight_sleep/translations/et.json | 19 ++++++++++++ .../components/google/translations/et.json | 3 ++ .../components/nest/translations/el.json | 29 +++++++++++++++++ .../components/nest/translations/et.json | 29 +++++++++++++++++ .../components/nest/translations/hu.json | 31 ++++++++++++++++++- .../components/nest/translations/id.json | 18 ++++++++++- 6 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/eight_sleep/translations/et.json diff --git a/homeassistant/components/eight_sleep/translations/et.json b/homeassistant/components/eight_sleep/translations/et.json new file mode 100644 index 00000000000..05ba852c7e4 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_connect": "Ei saa \u00fchendust Eight Sleep pilvega: {error}" + }, + "error": { + "cannot_connect": "Ei saa \u00fchendust Eight Sleep pilvega: {error}" + }, + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/et.json b/homeassistant/components/google/translations/et.json index a115378f3a2..11447b1669f 100644 --- a/homeassistant/components/google/translations/et.json +++ b/homeassistant/components/google/translations/et.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "J\u00e4rgi [OAuth n\u00f5usoleku kuva]({oauth_consent_url}) [OAuth n\u00f5usoleku kuva] ) [juhiseid]({more_info_url}), et anda koduabilisele juurdep\u00e4\u00e4s oma Google'i kalendrile. Samuti pead looma kalendriga lingitud rakenduse identimisteabe.\n1. Mine aadressile [Mandaat]({oauth_creds_url}) ja kl\u00f5psake nuppu **Loo mandaat**.\n2. Vali ripploendist **OAuth kliendi ID**.\n3. Vali rakenduse t\u00fc\u00fcbi jaoks **TV ja piiratud sisendiga seadmed**.\n\n" + }, "config": { "abort": { "already_configured": "Kasutaja on juba seadistatud", diff --git a/homeassistant/components/nest/translations/el.json b/homeassistant/components/nest/translations/el.json index 1628a721733..26e4622670e 100644 --- a/homeassistant/components/nest/translations/el.json +++ b/homeassistant/components/nest/translations/el.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "\u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 [\u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2]( {more_info_url} ) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u039a\u03bf\u03bd\u03c3\u03cc\u03bb\u03b1 Cloud: \n\n 1. \u039c\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [\u03bf\u03b8\u03cc\u03bd\u03b7 \u03c3\u03c5\u03bd\u03b1\u03af\u03bd\u03b5\u03c3\u03b7\u03c2 OAuth]( {oauth_consent_url} ) \u03ba\u03b1\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5\n 1. \u039c\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b1 [\u0394\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1] ( {oauth_creds_url} ) \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf **\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03b7\u03c1\u03af\u03c9\u03bd**.\n 1. \u0391\u03c0\u03cc \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03c0\u03c4\u03c5\u03c3\u03c3\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 **OAuth \u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7**.\n 1. \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 **\u0395\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u0399\u03c3\u03c4\u03bf\u03cd** \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03a4\u03cd\u03c0\u03bf \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2.\n 1. \u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03c4\u03bf \" {redirect_url} \" \u03c3\u03c4\u03bf *Authorized redirect URI*." + }, "config": { "abort": { "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2.", @@ -29,6 +32,32 @@ "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Google, [\u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2]({url}).\n\n\u039c\u03b5\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7, \u03b1\u03bd\u03c4\u03b9\u03b3\u03c1\u03ac\u03c8\u03c4\u03b5-\u03b5\u03c0\u03b9\u03ba\u03bf\u03bb\u03bb\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03c0\u03b1\u03c1\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc Auth Token.", "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Google" }, + "auth_upgrade": { + "description": "\u03a4\u03bf App Auth \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd Google \u03b3\u03b9\u03b1 \u03b2\u03b5\u03bb\u03c4\u03af\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b1\u03c3\u03c6\u03ac\u03bb\u03b5\u03b9\u03b1\u03c2 \u03ba\u03b1\u03b9 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03b5 \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b5\u03c2 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ce\u03bd\u03c4\u03b1\u03c2 \u03bd\u03ad\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2. \n\n \u0391\u03bd\u03bf\u03af\u03be\u03c4\u03b5 \u03c4\u03bf [documentation]( {more_info_url} ) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03b5\u03c4\u03b5, \u03ba\u03b1\u03b8\u03ce\u03c2 \u03c4\u03b1 \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b8\u03b1 \u03c3\u03b1\u03c2 \u03ba\u03b1\u03b8\u03bf\u03b4\u03b7\u03b3\u03ae\u03c3\u03bf\u03c5\u03bd \u03c3\u03c4\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03b5\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03ad\u03c1\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 Nest.", + "title": "Nest: \u039a\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2" + }, + "cloud_project": { + "data": { + "cloud_project_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ad\u03c1\u03b3\u03bf\u03c5 Google Cloud" + }, + "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c4\u03bf\u03c5 \u03ad\u03c1\u03b3\u03bf\u03c5 Cloud \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9, \u03c0.\u03c7. *example-project-12345*. \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [Google Cloud Console]({cloud_console_url}) \u03ae \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 [\u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2]({more_info_url}).", + "title": "Nest: \u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ad\u03c1\u03b3\u03bf\u03c5 Cloud" + }, + "create_cloud_project": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Nest \u03c3\u03ac\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03b8\u03b5\u03c1\u03bc\u03bf\u03c3\u03c4\u03ac\u03c4\u03b5\u03c2 Nest, \u03c4\u03b9\u03c2 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b5\u03c2 \u03ba\u03b1\u03b9 \u03c4\u03b1 \u03ba\u03bf\u03c5\u03b4\u03bf\u03cd\u03bd\u03b9\u03b1 \u03c0\u03cc\u03c1\u03c4\u03b1\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf API \u03b4\u03b9\u03b1\u03c7\u03b5\u03af\u03c1\u03b9\u03c3\u03b7\u03c2 \u03ad\u03be\u03c5\u03c0\u03bd\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2. \u03a4\u03bf SDM API **\u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03bc\u03b9\u03b1 \u03b5\u03c6\u03ac\u03c0\u03b1\u03be \u03c7\u03c1\u03ad\u03c9\u03c3\u03b7 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 5 $**. \u0394\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 [\u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2]( {more_info_url} ). \n\n 1. \u039c\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf [Google Cloud Console]( {cloud_console_url} ).\n 1. \u0395\u03ac\u03bd \u03b1\u03c5\u03c4\u03cc \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03bf \u03c0\u03c1\u03ce\u03c4\u03bf \u03c3\u03b1\u03c2 \u03ad\u03c1\u03b3\u03bf, \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf **\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03ad\u03c1\u03b3\u03bf\u03c5** \u03ba\u03b1\u03b9 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1 \u03c3\u03c4\u03bf **\u039d\u03ad\u03bf \u03ad\u03c1\u03b3\u03bf**.\n 1. \u0394\u03ce\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c3\u03c4\u03bf Cloud Project \u03c3\u03b1\u03c2 \u03ba\u03b1\u03b9 \u03bc\u03b5\u03c4\u03ac \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf **\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1**.\n 1. \u0391\u03c0\u03bf\u03b8\u03b7\u03ba\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ad\u03c1\u03b3\u03bf\u03c5 Cloud, \u03c0.\u03c7. *example-project-12345* \u03ba\u03b1\u03b8\u03ce\u03c2 \u03b8\u03b1 \u03c4\u03bf \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af\u03c4\u03b5 \u03b1\u03c1\u03b3\u03cc\u03c4\u03b5\u03c1\u03b1\n 1. \u039c\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u0392\u03b9\u03b2\u03bb\u03b9\u03bf\u03b8\u03ae\u03ba\u03b7 API \u03b3\u03b9\u03b1 \u03c4\u03bf [Smart Device Management API]( {sdm_api_url} ) \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf **\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7**.\n 1. \u039c\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u0392\u03b9\u03b2\u03bb\u03b9\u03bf\u03b8\u03ae\u03ba\u03b7 API \u03b3\u03b9\u03b1 \u03c4\u03bf [Cloud Pub/Sub API]( {pubsub_api_url} ) \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf **\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7**. \n\n \u03a3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b1\u03bd \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03c4\u03bf \u03ad\u03c1\u03b3\u03bf \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf cloud.", + "title": "Nest: \u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b1\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf Cloud Project" + }, + "device_project": { + "data": { + "project_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ad\u03c1\u03b3\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" + }, + "description": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ad\u03c1\u03b3\u03bf \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Nest Device Access, \u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf **\u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03ad\u03bd\u03b1 \u03c4\u03ad\u03bb\u03bf\u03c2 \u03cd\u03c8\u03bf\u03c5\u03c2 5 \u03b4\u03bf\u03bb\u03b1\u03c1\u03af\u03c9\u03bd \u0397\u03a0\u0391** \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c4\u03bf\u03c5.\n1. \u039c\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [Device Access Console]({device_access_console_url}), \u03ba\u03b1\u03b9 \u03bc\u03ad\u03c3\u03c9 \u03c4\u03b7\u03c2 \u03c1\u03bf\u03ae\u03c2 \u03c0\u03bb\u03b7\u03c1\u03c9\u03bc\u03ae\u03c2.\n1. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf **\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03ad\u03c1\u03b3\u03bf\u03c5**.\n1. \u0394\u03ce\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c3\u03c4\u03bf \u03ad\u03c1\u03b3\u03bf Device Access \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf **\u0395\u03c0\u03cc\u03bc\u03b5\u03bd\u03bf**.\n1. \u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 OAuth\n1. \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03ba\u03ac\u03bd\u03bf\u03bd\u03c4\u03b1\u03c2 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae **\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7** \u03ba\u03b1\u03b9 **\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03ad\u03c1\u03b3\u03bf\u03c5**.\n\n\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ad\u03c1\u03b3\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03c3\u03b1\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 ([\u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2]({more_info_url})).\n", + "title": "Nest: \u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ad\u03c1\u03b3\u03bf \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" + }, + "device_project_upgrade": { + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf Nest Device Access Project \u03bc\u03b5 \u03c4\u03bf \u03bd\u03ad\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 OAuth ([\u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2]({more_info_url}))\n1. \u039c\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [Device Access Console]({device_access_console_url}).\n1. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \u03b5\u03b9\u03ba\u03bf\u03bd\u03af\u03b4\u03b9\u03bf \u03c4\u03bf\u03c5 \u03ba\u03ac\u03b4\u03bf\u03c5 \u03b1\u03c0\u03bf\u03c1\u03c1\u03b9\u03bc\u03bc\u03ac\u03c4\u03c9\u03bd \u03b4\u03af\u03c0\u03bb\u03b1 \u03c3\u03c4\u03bf *OAuth Client ID*.\n1. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \u03bc\u03b5\u03bd\u03bf\u03cd \u03c5\u03c0\u03b5\u03c1\u03c7\u03b5\u03af\u03bb\u03b9\u03c3\u03b7\u03c2 `...` \u03ba\u03b1\u03b9 *Add Client ID*.\n1. \u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03bd\u03ad\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 OAuth \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf **\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7**.\n\n\u03a4\u03bf \u03b4\u03b9\u03ba\u03cc \u03c3\u03b1\u03c2 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 OAuth \u03b5\u03af\u03bd\u03b1\u03b9: `{client_id}`", + "title": "Nest: \u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03ad\u03c1\u03b3\u03bf\u03c5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" + }, "init": { "data": { "flow_impl": "\u03a0\u03ac\u03c1\u03bf\u03c7\u03bf\u03c2" diff --git a/homeassistant/components/nest/translations/et.json b/homeassistant/components/nest/translations/et.json index 898c9e9f3f3..3d8cd25f47c 100644 --- a/homeassistant/components/nest/translations/et.json +++ b/homeassistant/components/nest/translations/et.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "J\u00e4rgi pilvekonsooli seadistamiseks [juhiseid]({more_info_url}):\n\n1. Ava [OAuth n\u00f5usoleku kuva]({oauth_consent_url}) ja seadista\n1. Mine aadressile [Mandaat]({oauth_creds_url}) ja kl\u00f5psa nuppu **Loo mandaat**.\n1. Vali ripploendist **OAuth kliendi ID**.\n1. Vali rakenduse t\u00fc\u00fcbi jaoks **Veebirakendus**.\n1. Lisa \"{redirect_url}\" jaotises *Volitatud \u00fcmbersuunamine URI*." + }, "config": { "abort": { "authorize_url_timeout": "Tuvastamise URL-i loomise ajal\u00f5pp.", @@ -29,6 +32,32 @@ "description": "Oma Google'i konto sidumiseks vali [autoriseeri oma konto]({url}).\n\nP\u00e4rast autoriseerimist kopeeri ja aseta allpool esitatud Auth Token'i kood.", "title": "Google'i konto linkimine" }, + "auth_upgrade": { + "description": "Google on app Authi turvalisuse parandamiseks tauninud ja pead tegutsema, luues uusi rakenduse mandaate.\n\nAva [dokumentatsioon]({more_info_url}), mida j\u00e4rgida, kuna j\u00e4rgmised juhised juhendavad teid nest-seadmetele juurdep\u00e4\u00e4su taastamiseks vajalike juhiste kaudu.", + "title": "Nest: App Auth Deprecation" + }, + "cloud_project": { + "data": { + "cloud_project_id": "Google'i pilveprojekti ID" + }, + "description": "Sisesta allpool pilveprojekti ID nt *n\u00e4idisprojekt-12345*. Vaata [Google'i pilvekonsooli]({cloud_console_url}) v\u00f5i dokumentatsiooni [lisateave]({more_info_url}).", + "title": "Nest: sisesta pilveprojekti ID" + }, + "create_cloud_project": { + "description": "Nesti sidumine v\u00f5imaldab integreerida pesa termostaate, kaameraid ja uksekellasid nutiseadme haldamise API abil. SDM API **n\u00f5uab \u00fchekordset h\u00e4\u00e4lestustasu 5 USA dollarit**. Vt dokumentatsiooni teemast [more information]({more_info_url}).\n\n1. Mine [Google'i pilvekonsooli]({cloud_console_url}).\n1. Kui see on esimene projekt, kl\u00f5psa nuppu **Loo projekt** ja seej\u00e4rel **Uus projekt**.\n1. Anna oma pilveprojektile nimi ja seej\u00e4rel kl\u00f5psa nuppu **Loo**.\n1. Salvesta pilveprojekti ID nt *n\u00e4ide-projekt-12345*, kuna vajad seda hiljem\n1. Ava API teek [Smart Device Management API]({sdm_api_url}) jaoks ja kl\u00f5psa nuppu **Luba**.\n1. Mine API teeki [Cloud Pub/Sub API]({pubsub_api_url}) jaoks ja kl\u00f5psa nuppu **Luba**.\n\nJ\u00e4tka kui pilveprojekt on h\u00e4\u00e4lestatud.", + "title": "Nest: Pilveprojekti loomine ja konfigureerimine" + }, + "device_project": { + "data": { + "project_id": "Seadme juurdep\u00e4\u00e4su projekti ID" + }, + "description": "Loo Nest Device Accessi projekt, mille seadistamiseks on vaja 5 USA dollari suurust tasu**.\n1. Ava [Seadme juurdep\u00e4\u00e4sukonsool]({device_access_console_url}) ja maksevoo kaudu.\n1. Vajuta **Loo projekt**\n1. Anna oma seadmele juurdep\u00e4\u00e4su projektile nimi ja kl\u00f5psa nuppu **Next**.\n1. Sisesta oma OAuth Kliendi ID\n1. Luba s\u00fcndmused, kl\u00f5psates nuppu **Luba** ja **Loo projekt**.\n\nSisesta allpool seadme accessi projekti ID ([lisateave]({more_info_url})).\n", + "title": "Nest: seadmele juurdep\u00e4\u00e4su projekti loomine" + }, + "device_project_upgrade": { + "description": "Nest Device Access Projecti v\u00e4rskendamine uue OAuth Client ID-ga ([lisateave]({more_info_url}))\n1. Ava [Seadme juurdep\u00e4\u00e4sukonsool]({device_access_console_url}).\n1. Kl\u00f5psa pr\u00fcgikastiikooni *OAuth Client ID* k\u00f5rval.\n1. Kl\u00f5psa \u00fclet\u00e4itumise men\u00fc\u00fcd \"...\", ja *Lisa kliendi ID*.\n1. Sisesta uus OAuth-kliendi ID ja kl\u00f5psa nuppu **Lisa**.\n\nOAuth-kliendi ID on:{client_id}.", + "title": "Nest: seadmele juurdep\u00e4\u00e4su projekti v\u00e4rskendamine" + }, "init": { "data": { "flow_impl": "Pakkuja" diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json index 1f98e162a7b..a93d06c4f6d 100644 --- a/homeassistant/components/nest/translations/hu.json +++ b/homeassistant/components/nest/translations/hu.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "K\u00f6vesse az [utas\u00edt\u00e1sokat]( {more_info_url} ) a Cloud Console konfigur\u00e1l\u00e1s\u00e1hoz: \n\n 1. Nyissa meg az [OAuth hozz\u00e1j\u00e1rul\u00e1si k\u00e9perny\u0151t]({oauth_consent_url}), \u00e9s \u00e1ll\u00edtsa be\n 1. Nyissa meg a [Hiteles\u00edt\u00e9si adatok]({oauth_creds_url}), majd kattintson a **Hiteles\u00edt\u0151 adatok l\u00e9trehoz\u00e1sa** lehet\u0151s\u00e9gre.\n 1. A leg\u00f6rd\u00fcl\u0151 list\u00e1b\u00f3l v\u00e1lassza az **OAuth-\u00fcgyf\u00e9lazonos\u00edt\u00f3** lehet\u0151s\u00e9get.\n 1. V\u00e1lassza a **Webes alkalmaz\u00e1s** lehet\u0151s\u00e9get az Alkalmaz\u00e1s t\u00edpusak\u00e9nt.\n 1. Adja hozz\u00e1 a `{redirect_url}` \u00e9rt\u00e9ket az *Authorized redirect URI* r\u00e9szhez." + }, "config": { "abort": { "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", @@ -19,7 +22,7 @@ "subscriber_error": "Ismeretlen el\u0151fizet\u0151i hiba, b\u0151vebben a napl\u00f3kban", "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", - "wrong_project_id": "K\u00e9rem, adjon meg egy \u00e9rv\u00e9nyes Cloud Project ID-t (Device Access Project ID tal\u00e1lva)" + "wrong_project_id": "K\u00e9rem, adjon meg egy \u00e9rv\u00e9nyes Cloud Project ID-t (Device Access Project ID-vrl azonos)" }, "step": { "auth": { @@ -29,6 +32,32 @@ "description": "[Enged\u00e9lyezze]({url}) Google-fi\u00f3kj\u00e1t az \u00f6sszekapcsol\u00e1hoz.\n\nAz enged\u00e9lyez\u00e9s ut\u00e1n m\u00e1solja \u00e1t a kapott token k\u00f3dot.", "title": "\u00d6sszekapcsol\u00e1s Google-al" }, + "auth_upgrade": { + "description": "A Google a biztons\u00e1g jav\u00edt\u00e1sa \u00e9rdek\u00e9ben megsz\u00fcntette az App Auth szolg\u00e1ltat\u00e1st, \u00e9s \u00d6nnek \u00faj alkalmaz\u00e1s hiteles\u00edt\u00e9si adatainak l\u00e9trehoz\u00e1s\u00e1val kell tennie valamit. \n\n Nyissa meg a [dokument\u00e1ci\u00f3t]({more_info_url}), hogy k\u00f6vesse, mivel a k\u00f6vetkez\u0151 l\u00e9p\u00e9sek v\u00e9gigvezetik a Nest-eszk\u00f6zeihez val\u00f3 hozz\u00e1f\u00e9r\u00e9s vissza\u00e1ll\u00edt\u00e1s\u00e1hoz sz\u00fcks\u00e9ges l\u00e9p\u00e9seken.", + "title": "Nest: Az alkalmaz\u00e1shiteles\u00edt\u00e9s megsz\u00fcntet\u00e9se" + }, + "cloud_project": { + "data": { + "cloud_project_id": "Google Cloud Project ID" + }, + "description": "Adja meg a Cloud projekt azonos\u00edt\u00f3j\u00e1t, pl. *example-project-12345*. B\u0151vebben: [Google Cloud Console]({cloud_console_url}) vagy [dokument\u00e1ci\u00f3]({more_info_url}).", + "title": "Nest: Adja meg a Cloud Project ID-t" + }, + "create_cloud_project": { + "description": "A Nest-integr\u00e1ci\u00f3 lehet\u0151v\u00e9 teszi Nest termoszt\u00e1tjainak, kamer\u00e1inak \u00e9s ajt\u00f3cseng\u0151inek integr\u00e1l\u00e1s\u00e1t a Smart Device Management API seg\u00edts\u00e9g\u00e9vel. Az SDM API **5 USD** egyszeri be\u00e1ll\u00edt\u00e1si d\u00edjat ig\u00e9nyel. [Tov\u00e1bbi inform\u00e1ci\u00f3]({more_info_url}). \n\n 1. Nyissa meg a [Google Cloud Console]({cloud_console_url}) oldalt.\n 1. Ha ez az els\u0151 projektje, kattintson a **Projekt l\u00e9trehoz\u00e1sa**, majd az **\u00daj projekt** lehet\u0151s\u00e9gre.\n 1. Adjon nevet a Cloud Projectnek, majd kattintson a **L\u00e9trehoz\u00e1s** gombra.\n 1. Mentse el a felh\u0151projekt azonos\u00edt\u00f3j\u00e1t, p\u00e9ld\u00e1ul *example-projekt-12345*, mert k\u00e9s\u0151bb sz\u00fcks\u00e9ge lesz r\u00e1\n 1. Nyissa meg a [Smart Device Management API-t]({sdm_api_url}), \u00e9s kattintson az **Enged\u00e9lyez\u00e9s** lehet\u0151s\u00e9gre.\n 1. Nyissa meg a [Cloud Pub/Sub API-t]({pubsub_api_url}), \u00e9s kattintson az **Enged\u00e9lyez\u00e9s** lehet\u0151s\u00e9gre. \n\nFolytassa a be\u00e1ll\u00edt\u00e1s ut\u00e1n.", + "title": "Nest: Cloud Project l\u00e9trehoz\u00e1sa \u00e9s konfigur\u00e1l\u00e1sa" + }, + "device_project": { + "data": { + "project_id": "Eszk\u00f6z-hozz\u00e1f\u00e9r\u00e9s Projekt azonos\u00edt\u00f3" + }, + "description": "Hozzon l\u00e9tre egy Nest Device Access projektet, amelynek **be\u00e1ll\u00edt\u00e1sa 5 USD d\u00edjat** ig\u00e9nyel.\n1. Menjen a [Device Access Console]({device_access_console_url}) oldalra, \u00e9s a fizet\u00e9si folyamaton kereszt\u00fcl.\n1. Kattintson a **Projekt l\u00e9trehoz\u00e1sa** gombra.\n1. Adjon nevet a Device Access projektnek, \u00e9s kattintson a **K\u00f6vetkez\u0151** gombra.\n1. Adja meg az OAuth \u00fcgyf\u00e9l azonos\u00edt\u00f3j\u00e1t\n1. Enged\u00e9lyezze az esem\u00e9nyeket a **Enable** \u00e9s a **Create project** gombra kattintva.\n\nAdja meg a Device Access projekt azonos\u00edt\u00f3j\u00e1t az al\u00e1bbiakban ([more info]({more_info_url})).\n", + "title": "Nest: Hozzon l\u00e9tre egy eszk\u00f6z-hozz\u00e1f\u00e9r\u00e9si projektet" + }, + "device_project_upgrade": { + "description": "Friss\u00edtse a Nest Device Access projektet az \u00faj OAuth \u00fcgyf\u00e9l azonos\u00edt\u00f3j\u00e1val ([more info]({more_info_url}))\n1. L\u00e9pjen az [Device Access Console]({device_access_console_url}).\n1. Kattintson a *OAuth Client ID* melletti szemetes ikonra.\n1. Kattintson a `...` t\u00falfoly\u00f3 men\u00fcre \u00e9s a *Add Client ID* men\u00fcpontra.\n1. Adja meg az \u00faj OAuth \u00fcgyf\u00e9l azonos\u00edt\u00f3j\u00e1t, \u00e9s kattintson a **Add** gombra.\n\nAz \u00d6n OAuth \u00fcgyf\u00e9l azonos\u00edt\u00f3ja a k\u00f6vetkez\u0151: `{client_id}`", + "title": "Nest: Friss\u00edtse az eszk\u00f6zhozz\u00e1f\u00e9r\u00e9si projektet" + }, "init": { "data": { "flow_impl": "Szolg\u00e1ltat\u00f3" diff --git a/homeassistant/components/nest/translations/id.json b/homeassistant/components/nest/translations/id.json index ab7aaa2d459..e58f891f6c4 100644 --- a/homeassistant/components/nest/translations/id.json +++ b/homeassistant/components/nest/translations/id.json @@ -22,7 +22,7 @@ "subscriber_error": "Kesalahan pelanggan tidak diketahui, lihat log", "timeout": "Tenggang waktu memvalidasi kode telah habis.", "unknown": "Kesalahan yang tidak diharapkan", - "wrong_project_id": "Masukkan Cloud Project ID yang valid (Device Access Project ID yang ditemukan)" + "wrong_project_id": "Masukkan ID Proyek Cloud yang valid (sebelumnya sama dengan ID Proyek Akses Perangkat)" }, "step": { "auth": { @@ -32,9 +32,25 @@ "description": "Untuk menautkan akun Google Anda, [otorisasi akun Anda]({url}).\n\nSetelah otorisasi, salin dan tempel Token Auth yang disediakan di bawah ini.", "title": "Tautkan Akun Google" }, + "auth_upgrade": { + "title": "Nest: Penghentian Autentikasi Aplikasi" + }, "cloud_project": { + "data": { + "cloud_project_id": "ID Proyek Google Cloud" + }, + "description": "Masukkan ID Proyek Cloud di bawah ini, misalnya *contoh-proyek-12345*. Lihat [Konsol Google Cloud]({cloud_console_url}) atau dokumentasi untuk [info selengkapnya]({more_info_url}).", "title": "Nest: Masukkan ID Proyek Cloud" }, + "create_cloud_project": { + "title": "Nest: Buat dan konfigurasikan Proyek Cloud" + }, + "device_project": { + "data": { + "project_id": "ID Proyek Akses Perangkat" + }, + "title": "Nest: Buat Proyek Akses Perangkat" + }, "device_project_upgrade": { "title": "Nest: Perbarui Proyek Akses Perangkat" }, From 45142558ef28ed1ef0224bdfb4207ecb1de6d5e7 Mon Sep 17 00:00:00 2001 From: Rechner Fox <659028+rechner@users.noreply.github.com> Date: Sat, 18 Jun 2022 23:54:10 -0700 Subject: [PATCH 1572/3516] Bump pyenvisalink to 4.5 (#73663) * Bump pyenvisalink to latest version 4.5 * Minor bugfixes: * Prevent reconnects from being scheduled simultaneously * Fix parsing keypad messages containing extra commas * Add pyenvisalink updated dependency --- homeassistant/components/envisalink/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json index 2154cd68772..44a40991a37 100644 --- a/homeassistant/components/envisalink/manifest.json +++ b/homeassistant/components/envisalink/manifest.json @@ -2,7 +2,7 @@ "domain": "envisalink", "name": "Envisalink", "documentation": "https://www.home-assistant.io/integrations/envisalink", - "requirements": ["pyenvisalink==4.4"], + "requirements": ["pyenvisalink==4.5"], "codeowners": ["@ufodone"], "iot_class": "local_push", "loggers": ["pyenvisalink"] diff --git a/requirements_all.txt b/requirements_all.txt index 79ba4689352..ad12a87c152 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1474,7 +1474,7 @@ pyeight==0.3.0 pyemby==1.8 # homeassistant.components.envisalink -pyenvisalink==4.4 +pyenvisalink==4.5 # homeassistant.components.ephember pyephember==0.3.1 From 7714183118780faf8b6d41ceaeb6d34c596164dd Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 19 Jun 2022 10:09:26 -0400 Subject: [PATCH 1573/3516] Add `zwave_js/subscribe_node_status` WS API cmd (#73249) * Add zwave_js/subscribe_node_status WS API cmd * add ready to event --- homeassistant/components/zwave_js/api.py | 18 +++++++++++------ tests/components/zwave_js/test_api.py | 25 +++++++++++++++++++++--- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 6a4aaf0264e..b41c6bf2f92 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -378,6 +378,7 @@ def node_status(node: Node) -> dict[str, Any]: def async_register_api(hass: HomeAssistant) -> None: """Register all of our api endpoints.""" websocket_api.async_register_command(hass, websocket_network_status) + websocket_api.async_register_command(hass, websocket_subscribe_node_status) websocket_api.async_register_command(hass, websocket_node_status) websocket_api.async_register_command(hass, websocket_node_metadata) websocket_api.async_register_command(hass, websocket_node_comments) @@ -422,7 +423,6 @@ def async_register_api(hass: HomeAssistant) -> None: hass, websocket_subscribe_controller_statistics ) websocket_api.async_register_command(hass, websocket_subscribe_node_statistics) - websocket_api.async_register_command(hass, websocket_node_ready) hass.http.register_view(FirmwareUploadView()) @@ -497,25 +497,28 @@ async def websocket_network_status( @websocket_api.websocket_command( { - vol.Required(TYPE): "zwave_js/node_ready", + vol.Required(TYPE): "zwave_js/subscribe_node_status", vol.Required(DEVICE_ID): str, } ) @websocket_api.async_response @async_get_node -async def websocket_node_ready( +async def websocket_subscribe_node_status( hass: HomeAssistant, connection: ActiveConnection, msg: dict, node: Node, ) -> None: - """Subscribe to the node ready event of a Z-Wave JS node.""" + """Subscribe to node status update events of a Z-Wave JS node.""" @callback def forward_event(event: dict) -> None: """Forward the event.""" connection.send_message( - websocket_api.event_message(msg[ID], {"event": event["event"]}) + websocket_api.event_message( + msg[ID], + {"event": event["event"], "status": node.status, "ready": node.ready}, + ) ) @callback @@ -525,7 +528,10 @@ async def websocket_node_ready( unsub() connection.subscriptions[msg["id"]] = async_cleanup - msg[DATA_UNSUBSCRIBE] = unsubs = [node.on("ready", forward_event)] + msg[DATA_UNSUBSCRIBE] = unsubs = [ + node.on(evt, forward_event) + for evt in ("alive", "dead", "sleep", "wake up", "ready") + ] connection.send_result(msg[ID]) diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 6c3dd796a7d..88fea684233 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -198,14 +198,14 @@ async def test_network_status(hass, multisensor_6, integration, hass_ws_client): assert msg["error"]["code"] == ERR_INVALID_FORMAT -async def test_node_ready( +async def test_subscribe_node_status( hass, multisensor_6_state, client, integration, hass_ws_client, ): - """Test the node ready websocket command.""" + """Test the subscribe node status websocket command.""" entry = integration ws_client = await hass_ws_client(hass) node_data = deepcopy(multisensor_6_state) # Copy to allow modification in tests. @@ -222,7 +222,7 @@ async def test_node_ready( await ws_client.send_json( { ID: 3, - TYPE: "zwave_js/node_ready", + TYPE: "zwave_js/subscribe_node_status", DEVICE_ID: device.id, } ) @@ -246,6 +246,25 @@ async def test_node_ready( msg = await ws_client.receive_json() assert msg["event"]["event"] == "ready" + assert msg["event"]["status"] == 1 + assert msg["event"]["ready"] + + event = Event( + "wake up", + { + "source": "node", + "event": "wake up", + "nodeId": node.node_id, + }, + ) + node.receive_event(event) + await hass.async_block_till_done() + + msg = await ws_client.receive_json() + + assert msg["event"]["event"] == "wake up" + assert msg["event"]["status"] == 2 + assert msg["event"]["ready"] async def test_node_status(hass, multisensor_6, integration, hass_ws_client): From b19b6ec6eae7c1e05c38159cfc93f006d244eadb Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sun, 19 Jun 2022 10:22:33 -0400 Subject: [PATCH 1574/3516] Update UniFi Protect to use MAC address for unique ID (#73508) --- .../components/unifiprotect/__init__.py | 2 +- .../components/unifiprotect/camera.py | 4 +- homeassistant/components/unifiprotect/data.py | 8 +- .../components/unifiprotect/entity.py | 13 +-- .../components/unifiprotect/migrate.py | 96 +++++++++++++++++-- .../components/unifiprotect/services.py | 20 ++-- .../components/unifiprotect/utils.py | 51 ++++++---- tests/components/unifiprotect/conftest.py | 35 ++++++- .../unifiprotect/test_binary_sensor.py | 1 - tests/components/unifiprotect/test_button.py | 4 +- tests/components/unifiprotect/test_camera.py | 17 ++-- .../unifiprotect/test_diagnostics.py | 4 +- tests/components/unifiprotect/test_init.py | 5 +- tests/components/unifiprotect/test_light.py | 2 +- tests/components/unifiprotect/test_lock.py | 2 +- .../unifiprotect/test_media_player.py | 2 +- tests/components/unifiprotect/test_migrate.py | 77 ++++++++++++--- .../components/unifiprotect/test_services.py | 8 +- tests/components/unifiprotect/test_switch.py | 6 +- 19 files changed, 266 insertions(+), 91 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index d05f544ada1..a24777f9ecd 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -128,5 +128,5 @@ async def async_remove_config_entry_device( assert api is not None return api.bootstrap.nvr.mac not in unifi_macs and not any( device.mac in unifi_macs - for device in async_get_devices(api, DEVICES_THAT_ADOPT) + for device in async_get_devices(api.bootstrap, DEVICES_THAT_ADOPT) ) diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index 8020d5e8aab..d59ee59b760 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -110,10 +110,10 @@ class ProtectCamera(ProtectDeviceEntity, Camera): super().__init__(data, camera) if self._secure: - self._attr_unique_id = f"{self.device.id}_{self.channel.id}" + self._attr_unique_id = f"{self.device.mac}_{self.channel.id}" self._attr_name = f"{self.device.name} {self.channel.name}" else: - self._attr_unique_id = f"{self.device.id}_{self.channel.id}_insecure" + self._attr_unique_id = f"{self.device.mac}_{self.channel.id}_insecure" self._attr_name = f"{self.device.name} {self.channel.name} Insecure" # only the default (first) channel is enabled by default self._attr_entity_registry_enabled_default = is_default and secure diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index bcc1e561e99..1e9729f7930 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -21,7 +21,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.event import async_track_time_interval from .const import CONF_DISABLE_RTSP, DEVICES_THAT_ADOPT, DEVICES_WITH_ENTITIES, DOMAIN -from .utils import async_get_adoptable_devices_by_type, async_get_devices +from .utils import async_get_devices, async_get_devices_by_type _LOGGER = logging.getLogger(__name__) @@ -70,8 +70,8 @@ class ProtectData: ) -> Generator[ProtectAdoptableDeviceModel, None, None]: """Get all devices matching types.""" for device_type in device_types: - yield from async_get_adoptable_devices_by_type( - self.api, device_type + yield from async_get_devices_by_type( + self.api.bootstrap, device_type ).values() async def async_setup(self) -> None: @@ -153,7 +153,7 @@ class ProtectData: return self.async_signal_device_id_update(self.api.bootstrap.nvr.id) - for device in async_get_devices(self.api, DEVICES_THAT_ADOPT): + for device in async_get_devices(self.api.bootstrap, DEVICES_THAT_ADOPT): self.async_signal_device_id_update(device.id) @callback diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 2911a861535..6de0a4c57cb 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -26,7 +26,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from .const import ATTR_EVENT_SCORE, DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN from .data import ProtectData from .models import ProtectRequiredKeysMixin -from .utils import async_get_adoptable_devices_by_type, get_nested_attr +from .utils import async_device_by_id, get_nested_attr _LOGGER = logging.getLogger(__name__) @@ -117,11 +117,11 @@ class ProtectDeviceEntity(Entity): self.device = device if description is None: - self._attr_unique_id = f"{self.device.id}" + self._attr_unique_id = f"{self.device.mac}" self._attr_name = f"{self.device.name}" else: self.entity_description = description - self._attr_unique_id = f"{self.device.id}_{description.key}" + self._attr_unique_id = f"{self.device.mac}_{description.key}" name = description.name or "" self._attr_name = f"{self.device.name} {name.title()}" @@ -153,10 +153,11 @@ class ProtectDeviceEntity(Entity): """Update Entity object from Protect device.""" if self.data.last_update_success: assert self.device.model - devices = async_get_adoptable_devices_by_type( - self.data.api, self.device.model + device = async_device_by_id( + self.data.api.bootstrap, self.device.id, device_type=self.device.model ) - self.device = devices[self.device.id] + assert device is not None + self.device = device is_connected = ( self.data.last_update_success and self.device.state == StateType.CONNECTED diff --git a/homeassistant/components/unifiprotect/migrate.py b/homeassistant/components/unifiprotect/migrate.py index dcba0b504c9..307020caa5c 100644 --- a/homeassistant/components/unifiprotect/migrate.py +++ b/homeassistant/components/unifiprotect/migrate.py @@ -3,14 +3,18 @@ from __future__ import annotations import logging +from aiohttp.client_exceptions import ServerDisconnectedError from pyunifiprotect import ProtectApiClient +from pyunifiprotect.data import NVR, Bootstrap, ProtectAdoptableDeviceModel +from pyunifiprotect.exceptions import ClientError from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry as er -from .const import DEVICES_THAT_ADOPT +from .utils import async_device_by_id _LOGGER = logging.getLogger(__name__) @@ -24,6 +28,21 @@ async def async_migrate_data( await async_migrate_buttons(hass, entry, protect) _LOGGER.debug("Completed Migrate: async_migrate_buttons") + _LOGGER.debug("Start Migrate: async_migrate_device_ids") + await async_migrate_device_ids(hass, entry, protect) + _LOGGER.debug("Completed Migrate: async_migrate_device_ids") + + +async def async_get_bootstrap(protect: ProtectApiClient) -> Bootstrap: + """Get UniFi Protect bootstrap or raise appropriate HA error.""" + + try: + bootstrap = await protect.get_bootstrap() + except (TimeoutError, ClientError, ServerDisconnectedError) as err: + raise ConfigEntryNotReady from err + + return bootstrap + async def async_migrate_buttons( hass: HomeAssistant, entry: ConfigEntry, protect: ProtectApiClient @@ -47,16 +66,10 @@ async def async_migrate_buttons( _LOGGER.debug("No button entities need migration") return - bootstrap = await protect.get_bootstrap() + bootstrap = await async_get_bootstrap(protect) count = 0 for button in to_migrate: - device = None - for model in DEVICES_THAT_ADOPT: - attr = f"{model.value}s" - device = getattr(bootstrap, attr).get(button.unique_id) - if device is not None: - break - + device = async_device_by_id(bootstrap, button.unique_id) if device is None: continue @@ -81,3 +94,68 @@ async def async_migrate_buttons( if count < len(to_migrate): _LOGGER.warning("Failed to migate %s reboot buttons", len(to_migrate) - count) + + +async def async_migrate_device_ids( + hass: HomeAssistant, entry: ConfigEntry, protect: ProtectApiClient +) -> None: + """ + Migrate unique IDs from {device_id}_{name} format to {mac}_{name} format. + + This makes devices persist better with in HA. Anything a device is unadopted/readopted or + the Protect instance has to rebuild the disk array, the device IDs of Protect devices + can change. This causes a ton of orphaned entities and loss of historical data. MAC + addresses are the one persistent identifier a device has that does not change. + + Added in 2022.7.0. + """ + + registry = er.async_get(hass) + to_migrate = [] + for entity in er.async_entries_for_config_entry(registry, entry.entry_id): + parts = entity.unique_id.split("_") + # device ID = 24 characters, MAC = 12 + if len(parts[0]) == 24: + _LOGGER.debug("Entity %s needs migration", entity.entity_id) + to_migrate.append(entity) + + if len(to_migrate) == 0: + _LOGGER.debug("No entities need migration to MAC address ID") + return + + bootstrap = await async_get_bootstrap(protect) + count = 0 + for entity in to_migrate: + parts = entity.unique_id.split("_") + if parts[0] == bootstrap.nvr.id: + device: NVR | ProtectAdoptableDeviceModel | None = bootstrap.nvr + else: + device = async_device_by_id(bootstrap, parts[0]) + + if device is None: + continue + + new_unique_id = device.mac + if len(parts) > 1: + new_unique_id = f"{device.mac}_{'_'.join(parts[1:])}" + _LOGGER.debug( + "Migrating entity %s (old unique_id: %s, new unique_id: %s)", + entity.entity_id, + entity.unique_id, + new_unique_id, + ) + try: + registry.async_update_entity(entity.entity_id, new_unique_id=new_unique_id) + except ValueError as err: + print(err) + _LOGGER.warning( + "Could not migrate entity %s (old unique_id: %s, new unique_id: %s)", + entity.entity_id, + entity.unique_id, + new_unique_id, + ) + else: + count += 1 + + if count < len(to_migrate): + _LOGGER.warning("Failed to migrate %s entities", len(to_migrate) - count) diff --git a/homeassistant/components/unifiprotect/services.py b/homeassistant/components/unifiprotect/services.py index 828aa9ecfd7..3b7b3db026f 100644 --- a/homeassistant/components/unifiprotect/services.py +++ b/homeassistant/components/unifiprotect/services.py @@ -3,10 +3,11 @@ from __future__ import annotations import asyncio import functools -from typing import Any +from typing import Any, cast from pydantic import ValidationError from pyunifiprotect.api import ProtectApiClient +from pyunifiprotect.data import Chime from pyunifiprotect.exceptions import BadRequest import voluptuous as vol @@ -122,8 +123,8 @@ async def set_default_doorbell_text(hass: HomeAssistant, call: ServiceCall) -> N @callback -def _async_unique_id_to_ufp_device_id(unique_id: str) -> str: - """Extract the UFP device id from the registry entry unique id.""" +def _async_unique_id_to_mac(unique_id: str) -> str: + """Extract the MAC address from the registry entry unique id.""" return unique_id.split("_")[0] @@ -136,10 +137,12 @@ async def set_chime_paired_doorbells(hass: HomeAssistant, call: ServiceCall) -> chime_button = entity_registry.async_get(entity_id) assert chime_button is not None assert chime_button.device_id is not None - chime_ufp_device_id = _async_unique_id_to_ufp_device_id(chime_button.unique_id) + chime_mac = _async_unique_id_to_mac(chime_button.unique_id) instance = _async_get_ufp_instance(hass, chime_button.device_id) - chime = instance.bootstrap.chimes[chime_ufp_device_id] + chime = instance.bootstrap.get_device_from_mac(chime_mac) + chime = cast(Chime, chime) + assert chime is not None call.data = ReadOnlyDict(call.data.get("doorbells") or {}) doorbell_refs = async_extract_referenced_entity_ids(hass, call) @@ -154,10 +157,9 @@ async def set_chime_paired_doorbells(hass: HomeAssistant, call: ServiceCall) -> != BinarySensorDeviceClass.OCCUPANCY ): continue - doorbell_ufp_device_id = _async_unique_id_to_ufp_device_id( - doorbell_sensor.unique_id - ) - camera = instance.bootstrap.cameras[doorbell_ufp_device_id] + doorbell_mac = _async_unique_id_to_mac(doorbell_sensor.unique_id) + camera = instance.bootstrap.get_device_from_mac(doorbell_mac) + assert camera is not None doorbell_ids.add(camera.id) chime.camera_ids = sorted(doorbell_ids) await chime.save_device() diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index fffe987db0f..b57753e15d4 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -7,12 +7,15 @@ from enum import Enum import socket from typing import Any -from pyunifiprotect import ProtectApiClient -from pyunifiprotect.data.base import ProtectAdoptableDeviceModel, ProtectDeviceModel +from pyunifiprotect.data import ( + Bootstrap, + ProtectAdoptableDeviceModel, + ProtectDeviceModel, +) from homeassistant.core import HomeAssistant, callback -from .const import ModelType +from .const import DEVICES_THAT_ADOPT, ModelType def get_nested_attr(obj: Any, attr: str) -> Any: @@ -59,33 +62,45 @@ async def _async_resolve(hass: HomeAssistant, host: str) -> str | None: return None +@callback def async_get_devices_by_type( - api: ProtectApiClient, device_type: ModelType -) -> dict[str, ProtectDeviceModel]: - """Get devices by type.""" - devices: dict[str, ProtectDeviceModel] = getattr( - api.bootstrap, f"{device_type.value}s" - ) - return devices - - -def async_get_adoptable_devices_by_type( - api: ProtectApiClient, device_type: ModelType + bootstrap: Bootstrap, device_type: ModelType ) -> dict[str, ProtectAdoptableDeviceModel]: - """Get adoptable devices by type.""" + """Get devices by type.""" + devices: dict[str, ProtectAdoptableDeviceModel] = getattr( - api.bootstrap, f"{device_type.value}s" + bootstrap, f"{device_type.value}s" ) return devices +@callback +def async_device_by_id( + bootstrap: Bootstrap, + device_id: str, + device_type: ModelType | None = None, +) -> ProtectAdoptableDeviceModel | None: + """Get devices by type.""" + + device_types = DEVICES_THAT_ADOPT + if device_type is not None: + device_types = {device_type} + + device = None + for model in device_types: + device = async_get_devices_by_type(bootstrap, model).get(device_id) + if device is not None: + break + return device + + @callback def async_get_devices( - api: ProtectApiClient, model_type: Iterable[ModelType] + bootstrap: Bootstrap, model_type: Iterable[ModelType] ) -> Generator[ProtectDeviceModel, None, None]: """Return all device by type.""" return ( device for device_type in model_type - for device in async_get_devices_by_type(api, device_type).values() + for device in async_get_devices_by_type(bootstrap, device_type).values() ) diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 3986e4cd5a3..9892bcc3ec6 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -23,6 +23,7 @@ from pyunifiprotect.data import ( Viewer, WSSubscriptionMessage, ) +from pyunifiprotect.test_util.anonymize import random_hex from homeassistant.components.unifiprotect.const import DOMAIN from homeassistant.const import Platform @@ -80,6 +81,26 @@ class MockBootstrap: "chimes": [c.unifi_dict() for c in self.chimes.values()], } + def get_device_from_mac(self, mac: str) -> ProtectAdoptableDeviceModel | None: + """Return device for MAC address.""" + + mac = mac.lower().replace(":", "").replace("-", "").replace("_", "") + + all_devices = ( + self.cameras.values(), + self.lights.values(), + self.sensors.values(), + self.viewers.values(), + self.liveviews.values(), + self.doorlocks.values(), + self.chimes.values(), + ) + for devices in all_devices: + for device in devices: + if device.mac.lower() == mac: + return device + return None + @dataclass class MockEntityFixture: @@ -301,7 +322,19 @@ def ids_from_device_description( description.name.lower().replace(":", "").replace(" ", "_").replace("-", "_") ) - unique_id = f"{device.id}_{description.key}" + unique_id = f"{device.mac}_{description.key}" entity_id = f"{platform.value}.{entity_name}_{description_entity_name}" return unique_id, entity_id + + +def generate_random_ids() -> tuple[str, str]: + """Generate random IDs for device.""" + + return random_hex(24).upper(), random_hex(12).upper() + + +def regenerate_device_ids(device: ProtectAdoptableDeviceModel) -> None: + """Regenerate the IDs on UFP device.""" + + device.id, device.mac = generate_random_ids() diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 834f8634ee1..8fbaf61aca1 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -366,7 +366,6 @@ async def test_binary_sensor_setup_sensor_none( state = hass.states.get(entity_id) assert state - print(entity_id) assert state.state == expected[index] assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION diff --git a/tests/components/unifiprotect/test_button.py b/tests/components/unifiprotect/test_button.py index f3b76cb7abb..5b7122f6227 100644 --- a/tests/components/unifiprotect/test_button.py +++ b/tests/components/unifiprotect/test_button.py @@ -46,7 +46,7 @@ async def test_reboot_button( mock_entry.api.reboot_device = AsyncMock() - unique_id = f"{chime.id}_reboot" + unique_id = f"{chime.mac}_reboot" entity_id = "button.test_chime_reboot_device" entity_registry = er.async_get(hass) @@ -75,7 +75,7 @@ async def test_chime_button( mock_entry.api.play_speaker = AsyncMock() - unique_id = f"{chime.id}_play" + unique_id = f"{chime.mac}_play" entity_id = "button.test_chime_play_chime" entity_registry = er.async_get(hass) diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index 538b3bf7652..2f8d2607da0 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -38,6 +38,7 @@ from .conftest import ( MockEntityFixture, assert_entity_counts, enable_entity, + regenerate_device_ids, time_changed, ) @@ -124,7 +125,7 @@ def validate_default_camera_entity( channel = camera_obj.channels[channel_id] entity_name = f"{camera_obj.name} {channel.name}" - unique_id = f"{camera_obj.id}_{channel.id}" + unique_id = f"{camera_obj.mac}_{channel.id}" entity_id = f"camera.{entity_name.replace(' ', '_').lower()}" entity_registry = er.async_get(hass) @@ -146,7 +147,7 @@ def validate_rtsps_camera_entity( channel = camera_obj.channels[channel_id] entity_name = f"{camera_obj.name} {channel.name}" - unique_id = f"{camera_obj.id}_{channel.id}" + unique_id = f"{camera_obj.mac}_{channel.id}" entity_id = f"camera.{entity_name.replace(' ', '_').lower()}" entity_registry = er.async_get(hass) @@ -168,7 +169,7 @@ def validate_rtsp_camera_entity( channel = camera_obj.channels[channel_id] entity_name = f"{camera_obj.name} {channel.name} Insecure" - unique_id = f"{camera_obj.id}_{channel.id}_insecure" + unique_id = f"{camera_obj.mac}_{channel.id}_insecure" entity_id = f"camera.{entity_name.replace(' ', '_').lower()}" entity_registry = er.async_get(hass) @@ -251,12 +252,12 @@ async def test_basic_setup( camera_high_only.channels[1]._api = mock_entry.api camera_high_only.channels[2]._api = mock_entry.api camera_high_only.name = "Test Camera 1" - camera_high_only.id = "test_high" camera_high_only.channels[0].is_rtsp_enabled = True camera_high_only.channels[0].name = "High" camera_high_only.channels[0].rtsp_alias = "test_high_alias" camera_high_only.channels[1].is_rtsp_enabled = False camera_high_only.channels[2].is_rtsp_enabled = False + regenerate_device_ids(camera_high_only) camera_medium_only = mock_camera.copy(deep=True) camera_medium_only._api = mock_entry.api @@ -264,12 +265,12 @@ async def test_basic_setup( camera_medium_only.channels[1]._api = mock_entry.api camera_medium_only.channels[2]._api = mock_entry.api camera_medium_only.name = "Test Camera 2" - camera_medium_only.id = "test_medium" camera_medium_only.channels[0].is_rtsp_enabled = False camera_medium_only.channels[1].is_rtsp_enabled = True camera_medium_only.channels[1].name = "Medium" camera_medium_only.channels[1].rtsp_alias = "test_medium_alias" camera_medium_only.channels[2].is_rtsp_enabled = False + regenerate_device_ids(camera_medium_only) camera_all_channels = mock_camera.copy(deep=True) camera_all_channels._api = mock_entry.api @@ -277,7 +278,6 @@ async def test_basic_setup( camera_all_channels.channels[1]._api = mock_entry.api camera_all_channels.channels[2]._api = mock_entry.api camera_all_channels.name = "Test Camera 3" - camera_all_channels.id = "test_all" camera_all_channels.channels[0].is_rtsp_enabled = True camera_all_channels.channels[0].name = "High" camera_all_channels.channels[0].rtsp_alias = "test_high_alias" @@ -287,6 +287,7 @@ async def test_basic_setup( camera_all_channels.channels[2].is_rtsp_enabled = True camera_all_channels.channels[2].name = "Low" camera_all_channels.channels[2].rtsp_alias = "test_low_alias" + regenerate_device_ids(camera_all_channels) camera_no_channels = mock_camera.copy(deep=True) camera_no_channels._api = mock_entry.api @@ -294,11 +295,11 @@ async def test_basic_setup( camera_no_channels.channels[1]._api = mock_entry.api camera_no_channels.channels[2]._api = mock_entry.api camera_no_channels.name = "Test Camera 4" - camera_no_channels.id = "test_none" camera_no_channels.channels[0].is_rtsp_enabled = False camera_no_channels.channels[0].name = "High" camera_no_channels.channels[1].is_rtsp_enabled = False camera_no_channels.channels[2].is_rtsp_enabled = False + regenerate_device_ids(camera_no_channels) camera_package = mock_camera.copy(deep=True) camera_package._api = mock_entry.api @@ -306,12 +307,12 @@ async def test_basic_setup( camera_package.channels[1]._api = mock_entry.api camera_package.channels[2]._api = mock_entry.api camera_package.name = "Test Camera 5" - camera_package.id = "test_package" camera_package.channels[0].is_rtsp_enabled = True camera_package.channels[0].name = "High" camera_package.channels[0].rtsp_alias = "test_high_alias" camera_package.channels[1].is_rtsp_enabled = False camera_package.channels[2].is_rtsp_enabled = False + regenerate_device_ids(camera_package) package_channel = camera_package.channels[0].copy(deep=True) package_channel.is_rtsp_enabled = False package_channel.name = "Package Camera" diff --git a/tests/components/unifiprotect/test_diagnostics.py b/tests/components/unifiprotect/test_diagnostics.py index b58e164e913..2e7f8c0e4b4 100644 --- a/tests/components/unifiprotect/test_diagnostics.py +++ b/tests/components/unifiprotect/test_diagnostics.py @@ -4,7 +4,7 @@ from pyunifiprotect.data import NVR, Light from homeassistant.core import HomeAssistant -from .conftest import MockEntityFixture +from .conftest import MockEntityFixture, regenerate_device_ids from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -17,7 +17,7 @@ async def test_diagnostics( light1 = mock_light.copy() light1._api = mock_entry.api light1.name = "Test Light 1" - light1.id = "lightid1" + regenerate_device_ids(light1) mock_entry.api.bootstrap.lights = { light1.id: light1, diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 68f171b52bf..23dfa12fc97 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -16,7 +16,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from . import _patch_discovery -from .conftest import MockBootstrap, MockEntityFixture +from .conftest import MockBootstrap, MockEntityFixture, regenerate_device_ids from tests.common import MockConfigEntry @@ -212,8 +212,7 @@ async def test_device_remove_devices( light1 = mock_light.copy() light1._api = mock_entry.api light1.name = "Test Light 1" - light1.id = "lightid1" - light1.mac = "AABBCCDDEEFF" + regenerate_device_ids(light1) mock_entry.api.bootstrap.lights = { light1.id: light1, diff --git a/tests/components/unifiprotect/test_light.py b/tests/components/unifiprotect/test_light.py index 8f4dc4f8fcf..a3686fdfbd9 100644 --- a/tests/components/unifiprotect/test_light.py +++ b/tests/components/unifiprotect/test_light.py @@ -57,7 +57,7 @@ async def test_light_setup( ): """Test light entity setup.""" - unique_id = light[0].id + unique_id = light[0].mac entity_id = light[1] entity_registry = er.async_get(hass) diff --git a/tests/components/unifiprotect/test_lock.py b/tests/components/unifiprotect/test_lock.py index 0a02fcb22a4..abcea4ec04e 100644 --- a/tests/components/unifiprotect/test_lock.py +++ b/tests/components/unifiprotect/test_lock.py @@ -60,7 +60,7 @@ async def test_lock_setup( ): """Test lock entity setup.""" - unique_id = f"{doorlock[0].id}_lock" + unique_id = f"{doorlock[0].mac}_lock" entity_id = doorlock[1] entity_registry = er.async_get(hass) diff --git a/tests/components/unifiprotect/test_media_player.py b/tests/components/unifiprotect/test_media_player.py index c4586eb7880..d6404ee3fe5 100644 --- a/tests/components/unifiprotect/test_media_player.py +++ b/tests/components/unifiprotect/test_media_player.py @@ -66,7 +66,7 @@ async def test_media_player_setup( ): """Test media_player entity setup.""" - unique_id = f"{camera[0].id}_speaker" + unique_id = f"{camera[0].mac}_speaker" entity_id = camera[1] entity_registry = er.async_get(hass) diff --git a/tests/components/unifiprotect/test_migrate.py b/tests/components/unifiprotect/test_migrate.py index 756672bcbca..b62aa9d7757 100644 --- a/tests/components/unifiprotect/test_migrate.py +++ b/tests/components/unifiprotect/test_migrate.py @@ -12,7 +12,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import MockEntityFixture +from .conftest import MockEntityFixture, generate_random_ids, regenerate_device_ids async def test_migrate_reboot_button( @@ -23,12 +23,13 @@ async def test_migrate_reboot_button( light1 = mock_light.copy() light1._api = mock_entry.api light1.name = "Test Light 1" - light1.id = "lightid1" + regenerate_device_ids(light1) light2 = mock_light.copy() light2._api = mock_entry.api light2.name = "Test Light 2" - light2.id = "lightid2" + regenerate_device_ids(light2) + mock_entry.api.bootstrap.lights = { light1.id: light1, light2.id: light2, @@ -42,7 +43,7 @@ async def test_migrate_reboot_button( registry.async_get_or_create( Platform.BUTTON, DOMAIN, - f"{light2.id}_reboot", + f"{light2.mac}_reboot", config_entry=mock_entry.entry, ) @@ -59,20 +60,21 @@ async def test_migrate_reboot_button( ): if entity.domain == Platform.BUTTON.value: buttons.append(entity) - print(entity.entity_id) assert len(buttons) == 2 assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device") is None assert registry.async_get(f"{Platform.BUTTON}.test_light_1_reboot_device_2") is None - light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_lightid1") + light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_{light1.id.lower()}") assert light is not None - assert light.unique_id == f"{light1.id}_reboot" + assert light.unique_id == f"{light1.mac}_reboot" assert registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device") is None assert registry.async_get(f"{Platform.BUTTON}.test_light_2_reboot_device_2") is None - light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_lightid2_reboot") + light = registry.async_get( + f"{Platform.BUTTON}.unifiprotect_{light2.mac.lower()}_reboot" + ) assert light is not None - assert light.unique_id == f"{light2.id}_reboot" + assert light.unique_id == f"{light2.mac}_reboot" async def test_migrate_reboot_button_no_device( @@ -83,7 +85,9 @@ async def test_migrate_reboot_button_no_device( light1 = mock_light.copy() light1._api = mock_entry.api light1.name = "Test Light 1" - light1.id = "lightid1" + regenerate_device_ids(light1) + + light2_id, _ = generate_random_ids() mock_entry.api.bootstrap.lights = { light1.id: light1, @@ -92,7 +96,7 @@ async def test_migrate_reboot_button_no_device( registry = er.async_get(hass) registry.async_get_or_create( - Platform.BUTTON, DOMAIN, "lightid2", config_entry=mock_entry.entry + Platform.BUTTON, DOMAIN, light2_id, config_entry=mock_entry.entry ) await hass.config_entries.async_setup(mock_entry.entry.entry_id) @@ -110,9 +114,9 @@ async def test_migrate_reboot_button_no_device( buttons.append(entity) assert len(buttons) == 2 - light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_lightid2") + light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_{light2_id.lower()}") assert light is not None - assert light.unique_id == "lightid2" + assert light.unique_id == light2_id async def test_migrate_reboot_button_fail( @@ -123,7 +127,7 @@ async def test_migrate_reboot_button_fail( light1 = mock_light.copy() light1._api = mock_entry.api light1.name = "Test Light 1" - light1.id = "lightid1" + regenerate_device_ids(light1) mock_entry.api.bootstrap.lights = { light1.id: light1, @@ -155,4 +159,47 @@ async def test_migrate_reboot_button_fail( light = registry.async_get(f"{Platform.BUTTON}.test_light_1") assert light is not None - assert light.unique_id == f"{light1.id}" + assert light.unique_id == f"{light1.mac}" + + +async def test_migrate_device_mac_button_fail( + hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light +): + """Test migrating unique ID to MAC format.""" + + light1 = mock_light.copy() + light1._api = mock_entry.api + light1.name = "Test Light 1" + regenerate_device_ids(light1) + + mock_entry.api.bootstrap.lights = { + light1.id: light1, + } + mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) + + registry = er.async_get(hass) + registry.async_get_or_create( + Platform.BUTTON, + DOMAIN, + f"{light1.id}_reboot", + config_entry=mock_entry.entry, + suggested_object_id=light1.name, + ) + registry.async_get_or_create( + Platform.BUTTON, + DOMAIN, + f"{light1.mac}_reboot", + config_entry=mock_entry.entry, + suggested_object_id=light1.name, + ) + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + + assert mock_entry.entry.state == ConfigEntryState.LOADED + assert mock_entry.api.update.called + assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac + + light = registry.async_get(f"{Platform.BUTTON}.test_light_1") + assert light is not None + assert light.unique_id == f"{light1.id}_reboot" diff --git a/tests/components/unifiprotect/test_services.py b/tests/components/unifiprotect/test_services.py index 22f7fdd1a6c..2ad3821cc40 100644 --- a/tests/components/unifiprotect/test_services.py +++ b/tests/components/unifiprotect/test_services.py @@ -21,7 +21,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er -from .conftest import MockEntityFixture +from .conftest import MockEntityFixture, regenerate_device_ids @pytest.fixture(name="device") @@ -165,22 +165,22 @@ async def test_set_chime_paired_doorbells( } camera1 = mock_camera.copy() - camera1.id = "cameraid1" camera1.name = "Test Camera 1" camera1._api = mock_entry.api camera1.channels[0]._api = mock_entry.api camera1.channels[1]._api = mock_entry.api camera1.channels[2]._api = mock_entry.api camera1.feature_flags.has_chime = True + regenerate_device_ids(camera1) camera2 = mock_camera.copy() - camera2.id = "cameraid2" camera2.name = "Test Camera 2" camera2._api = mock_entry.api camera2.channels[0]._api = mock_entry.api camera2.channels[1]._api = mock_entry.api camera2.channels[2]._api = mock_entry.api camera2.feature_flags.has_chime = True + regenerate_device_ids(camera2) mock_entry.api.bootstrap.cameras = { camera1.id: camera1, @@ -210,5 +210,5 @@ async def test_set_chime_paired_doorbells( ) mock_entry.api.update_device.assert_called_once_with( - ModelType.CHIME, mock_chime.id, {"cameraIds": [camera1.id, camera2.id]} + ModelType.CHIME, mock_chime.id, {"cameraIds": sorted([camera1.id, camera2.id])} ) diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index bc0c8387c29..8ca1ef9b533 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -238,7 +238,7 @@ async def test_switch_setup_light( description = LIGHT_SWITCHES[0] - unique_id = f"{light.id}_{description.key}" + unique_id = f"{light.mac}_{description.key}" entity_id = f"switch.test_light_{description.name.lower().replace(' ', '_')}" entity = entity_registry.async_get(entity_id) @@ -282,7 +282,7 @@ async def test_switch_setup_camera_all( description_entity_name = ( description.name.lower().replace(":", "").replace(" ", "_") ) - unique_id = f"{camera.id}_{description.key}" + unique_id = f"{camera.mac}_{description.key}" entity_id = f"switch.test_camera_{description_entity_name}" entity = entity_registry.async_get(entity_id) @@ -329,7 +329,7 @@ async def test_switch_setup_camera_none( description_entity_name = ( description.name.lower().replace(":", "").replace(" ", "_") ) - unique_id = f"{camera_none.id}_{description.key}" + unique_id = f"{camera_none.mac}_{description.key}" entity_id = f"switch.test_camera_{description_entity_name}" entity = entity_registry.async_get(entity_id) From 68135e57af05af38fd9a55992cc9435230999ef0 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 19 Jun 2022 16:28:33 +0200 Subject: [PATCH 1575/3516] Split timer service for Sensibo (#73684) --- .../components/sensibo/binary_sensor.py | 35 +-- homeassistant/components/sensibo/climate.py | 36 ++- homeassistant/components/sensibo/const.py | 1 + .../components/sensibo/services.yaml | 16 +- homeassistant/components/sensibo/switch.py | 146 ++++++++++++ tests/components/sensibo/test_climate.py | 207 ++++-------------- tests/components/sensibo/test_switch.py | 165 ++++++++++++++ 7 files changed, 371 insertions(+), 235 deletions(-) create mode 100644 homeassistant/components/sensibo/switch.py create mode 100644 tests/components/sensibo/test_switch.py diff --git a/homeassistant/components/sensibo/binary_sensor.py b/homeassistant/components/sensibo/binary_sensor.py index 72003e0a418..503717c61e4 100644 --- a/homeassistant/components/sensibo/binary_sensor.py +++ b/homeassistant/components/sensibo/binary_sensor.py @@ -1,9 +1,9 @@ """Binary Sensor platform for Sensibo integration.""" from __future__ import annotations -from collections.abc import Callable, Mapping +from collections.abc import Callable from dataclasses import dataclass -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from pysensibo.model import MotionSensor, SensiboDevice @@ -36,7 +36,6 @@ class DeviceBaseEntityDescriptionMixin: """Mixin for required Sensibo base description keys.""" value_fn: Callable[[SensiboDevice], bool | None] - extra_fn: Callable[[SensiboDevice], dict[str, str | bool | None] | None] | None @dataclass @@ -85,18 +84,6 @@ MOTION_DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, .. name="Room Occupied", icon="mdi:motion-sensor", value_fn=lambda data: data.room_occupied, - extra_fn=None, - ), -) - -DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( - SensiboDeviceBinarySensorEntityDescription( - key="timer_on", - device_class=BinarySensorDeviceClass.RUNNING, - name="Timer Running", - icon="mdi:timer", - value_fn=lambda data: data.timer_on, - extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on}, ), ) @@ -107,7 +94,6 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( name="Pure Boost Enabled", icon="mdi:wind-power-outline", value_fn=lambda data: data.pure_boost_enabled, - extra_fn=None, ), SensiboDeviceBinarySensorEntityDescription( key="pure_ac_integration", @@ -116,7 +102,6 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( name="Pure Boost linked with AC", icon="mdi:connection", value_fn=lambda data: data.pure_ac_integration, - extra_fn=None, ), SensiboDeviceBinarySensorEntityDescription( key="pure_geo_integration", @@ -125,7 +110,6 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( name="Pure Boost linked with Presence", icon="mdi:connection", value_fn=lambda data: data.pure_geo_integration, - extra_fn=None, ), SensiboDeviceBinarySensorEntityDescription( key="pure_measure_integration", @@ -134,7 +118,6 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( name="Pure Boost linked with Indoor Air Quality", icon="mdi:connection", value_fn=lambda data: data.pure_measure_integration, - extra_fn=None, ), SensiboDeviceBinarySensorEntityDescription( key="pure_prime_integration", @@ -143,7 +126,6 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( name="Pure Boost linked with Outdoor Air Quality", icon="mdi:connection", value_fn=lambda data: data.pure_prime_integration, - extra_fn=None, ), ) @@ -172,12 +154,6 @@ async def async_setup_entry( for device_id, device_data in coordinator.data.parsed.items() if device_data.motion_sensors is not None ) - entities.extend( - SensiboDeviceSensor(coordinator, device_id, description) - for description in DEVICE_SENSOR_TYPES - for device_id, device_data in coordinator.data.parsed.items() - if device_data.model != "pure" - ) entities.extend( SensiboDeviceSensor(coordinator, device_id, description) for description in PURE_SENSOR_TYPES @@ -247,10 +223,3 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, BinarySensorEntity): def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" return self.entity_description.value_fn(self.device_data) - - @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: - """Return additional attributes.""" - if self.entity_description.extra_fn is not None: - return self.entity_description.extra_fn(self.device_data) - return None diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index c146ed350f3..6a1084f733c 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -27,7 +27,7 @@ from .coordinator import SensiboDataUpdateCoordinator from .entity import SensiboDeviceBaseEntity SERVICE_ASSUME_STATE = "assume_state" -SERVICE_TIMER = "timer" +SERVICE_ENABLE_TIMER = "enable_timer" ATTR_MINUTES = "minutes" SERVICE_ENABLE_PURE_BOOST = "enable_pure_boost" SERVICE_DISABLE_PURE_BOOST = "disable_pure_boost" @@ -98,12 +98,11 @@ async def async_setup_entry( "async_assume_state", ) platform.async_register_entity_service( - SERVICE_TIMER, + SERVICE_ENABLE_TIMER, { - vol.Required(ATTR_STATE): vol.In(["on", "off"]), - vol.Optional(ATTR_MINUTES): cv.positive_int, + vol.Required(ATTR_MINUTES): cv.positive_int, }, - "async_set_timer", + "async_enable_timer", ) platform.async_register_entity_service( SERVICE_ENABLE_PURE_BOOST, @@ -315,27 +314,18 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): await self._async_set_ac_state_property("on", state != HVACMode.OFF, True) await self.coordinator.async_refresh() - async def async_set_timer(self, state: str, minutes: int | None = None) -> None: - """Set or delete timer.""" - if state == "off" and self.device_data.timer_id is None: - raise HomeAssistantError("No timer to delete") - - if state == "on" and minutes is None: - raise ValueError("No value provided for timer") - - if state == "off": - result = await self.async_send_command("del_timer") - else: - new_state = bool(self.device_data.ac_states["on"] is False) - params = { - "minutesFromNow": minutes, - "acState": {**self.device_data.ac_states, "on": new_state}, - } - result = await self.async_send_command("set_timer", params) + async def async_enable_timer(self, minutes: int) -> None: + """Enable the timer.""" + new_state = bool(self.device_data.ac_states["on"] is False) + params = { + "minutesFromNow": minutes, + "acState": {**self.device_data.ac_states, "on": new_state}, + } + result = await self.async_send_command("set_timer", params) if result["status"] == "success": return await self.coordinator.async_request_refresh() - raise HomeAssistantError(f"Could not set timer for device {self.name}") + raise HomeAssistantError(f"Could not enable timer for device {self.name}") async def async_enable_pure_boost( self, diff --git a/homeassistant/components/sensibo/const.py b/homeassistant/components/sensibo/const.py index 5fce3822bb2..736d663b144 100644 --- a/homeassistant/components/sensibo/const.py +++ b/homeassistant/components/sensibo/const.py @@ -18,6 +18,7 @@ PLATFORMS = [ Platform.NUMBER, Platform.SELECT, Platform.SENSOR, + Platform.SWITCH, Platform.UPDATE, ] DEFAULT_NAME = "Sensibo" diff --git a/homeassistant/components/sensibo/services.yaml b/homeassistant/components/sensibo/services.yaml index 67006074f6b..6eb5c065789 100644 --- a/homeassistant/components/sensibo/services.yaml +++ b/homeassistant/components/sensibo/services.yaml @@ -16,24 +16,14 @@ assume_state: options: - "on" - "off" -timer: - name: Timer - description: Set or delete timer for device. +enable_timer: + name: Enable Timer + description: Enable the timer with custom time. target: entity: integration: sensibo domain: climate fields: - state: - name: State - description: Timer on or off. - required: true - example: "on" - selector: - select: - options: - - "on" - - "off" minutes: name: Minutes description: Countdown for timer (for timer state on) diff --git a/homeassistant/components/sensibo/switch.py b/homeassistant/components/sensibo/switch.py new file mode 100644 index 00000000000..3b9915d0a89 --- /dev/null +++ b/homeassistant/components/sensibo/switch.py @@ -0,0 +1,146 @@ +"""Switch platform for Sensibo integration.""" +from __future__ import annotations + +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from typing import Any + +from pysensibo.model import SensiboDevice + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import SensiboDataUpdateCoordinator +from .entity import SensiboDeviceBaseEntity + +PARALLEL_UPDATES = 0 + + +@dataclass +class DeviceBaseEntityDescriptionMixin: + """Mixin for required Sensibo base description keys.""" + + value_fn: Callable[[SensiboDevice], bool | None] + extra_fn: Callable[[SensiboDevice], dict[str, str | bool | None]] + command_on: str + command_off: str + remote_key: str + + +@dataclass +class SensiboDeviceSwitchEntityDescription( + SwitchEntityDescription, DeviceBaseEntityDescriptionMixin +): + """Describes Sensibo Switch entity.""" + + +DEVICE_SWITCH_TYPES: tuple[SensiboDeviceSwitchEntityDescription, ...] = ( + SensiboDeviceSwitchEntityDescription( + key="timer_on_switch", + device_class=SwitchDeviceClass.SWITCH, + name="Timer", + icon="mdi:timer", + value_fn=lambda data: data.timer_on, + extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on}, + command_on="set_timer", + command_off="del_timer", + remote_key="timer_on", + ), +) + + +def build_params(command: str, device_data: SensiboDevice) -> dict[str, Any] | None: + """Build params for turning on switch.""" + if command == "set_timer": + new_state = bool(device_data.ac_states["on"] is False) + params = { + "minutesFromNow": 60, + "acState": {**device_data.ac_states, "on": new_state}, + } + return params + return None + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Sensibo binary sensor platform.""" + + coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + entities: list[SensiboDeviceSwitch] = [] + + entities.extend( + SensiboDeviceSwitch(coordinator, device_id, description) + for description in DEVICE_SWITCH_TYPES + for device_id, device_data in coordinator.data.parsed.items() + if device_data.model != "pure" + ) + + async_add_entities(entities) + + +class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity): + """Representation of a Sensibo Device Switch.""" + + entity_description: SensiboDeviceSwitchEntityDescription + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + entity_description: SensiboDeviceSwitchEntityDescription, + ) -> None: + """Initiate Sensibo Device Switch.""" + super().__init__( + coordinator, + device_id, + ) + self.entity_description = entity_description + self._attr_unique_id = f"{device_id}-{entity_description.key}" + self._attr_name = f"{self.device_data.name} {entity_description.name}" + + @property + def is_on(self) -> bool | None: + """Return True if entity is on.""" + return self.entity_description.value_fn(self.device_data) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + params = build_params(self.entity_description.command_on, self.device_data) + result = await self.async_send_command( + self.entity_description.command_on, params + ) + + if result["status"] == "success": + setattr(self.device_data, self.entity_description.remote_key, True) + self.async_write_ha_state() + return await self.coordinator.async_request_refresh() + raise HomeAssistantError( + f"Could not execute {self.entity_description.command_on} for device {self.name}" + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + result = await self.async_send_command(self.entity_description.command_off) + + if result["status"] == "success": + setattr(self.device_data, self.entity_description.remote_key, False) + self.async_write_ha_state() + return await self.coordinator.async_request_refresh() + raise HomeAssistantError( + f"Could not execute {self.entity_description.command_off} for device {self.name}" + ) + + @property + def extra_state_attributes(self) -> Mapping[str, Any]: + """Return additional attributes.""" + return self.entity_description.extra_fn(self.device_data) diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index 30356f2b00d..ea7f6eb16fe 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -30,7 +30,7 @@ from homeassistant.components.sensibo.climate import ( SERVICE_ASSUME_STATE, SERVICE_DISABLE_PURE_BOOST, SERVICE_ENABLE_PURE_BOOST, - SERVICE_TIMER, + SERVICE_ENABLE_TIMER, _find_valid_target_temp, ) from homeassistant.components.sensibo.const import DOMAIN @@ -706,9 +706,45 @@ async def test_climate_set_timer( ) await hass.async_block_till_done() - state1 = hass.states.get("climate.hallway") + state_climate = hass.states.get("climate.hallway") assert hass.states.get("sensor.hallway_timer_end_time").state == STATE_UNKNOWN - assert hass.states.get("binary_sensor.hallway_timer_running").state == "off" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", + return_value={"status": "failure"}, + ): + with pytest.raises(MultipleInvalid): + await hass.services.async_call( + DOMAIN, + SERVICE_ENABLE_TIMER, + { + ATTR_ENTITY_ID: state_climate.entity_id, + }, + blocking=True, + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", + return_value={"status": "failure"}, + ): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + SERVICE_ENABLE_TIMER, + { + ATTR_ENTITY_ID: state_climate.entity_id, + ATTR_MINUTES: 30, + }, + blocking=True, + ) + await hass.async_block_till_done() with patch( "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", @@ -719,10 +755,9 @@ async def test_climate_set_timer( ): await hass.services.async_call( DOMAIN, - SERVICE_TIMER, + SERVICE_ENABLE_TIMER, { - ATTR_ENTITY_ID: state1.entity_id, - ATTR_STATE: "on", + ATTR_ENTITY_ID: state_climate.entity_id, ATTR_MINUTES: 30, }, blocking=True, @@ -752,166 +787,6 @@ async def test_climate_set_timer( hass.states.get("sensor.hallway_timer_end_time").state == "2022-06-06T12:00:00+00:00" ) - assert hass.states.get("binary_sensor.hallway_timer_running").state == "on" - assert hass.states.get("binary_sensor.hallway_timer_running").attributes == { - "device_class": "running", - "friendly_name": "Hallway Timer Running", - "icon": "mdi:timer", - "id": "SzTGE4oZ4D", - "turn_on": False, - } - - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_del_timer", - return_value={"status": "success"}, - ): - await hass.services.async_call( - DOMAIN, - SERVICE_TIMER, - { - ATTR_ENTITY_ID: state1.entity_id, - ATTR_STATE: "off", - }, - blocking=True, - ) - await hass.async_block_till_done() - - monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_on", False) - monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_id", None) - monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_state_on", None) - monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_time", None) - - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() - - assert hass.states.get("sensor.hallway_timer_end_time").state == STATE_UNKNOWN - assert hass.states.get("binary_sensor.hallway_timer_running").state == "off" - - -async def test_climate_set_timer_failures( - hass: HomeAssistant, - entity_registry_enabled_by_default: AsyncMock, - load_int: ConfigEntry, - monkeypatch: pytest.MonkeyPatch, - get_data: SensiboData, -) -> None: - """Test the Sensibo climate Set Timer service failures.""" - - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() - - state1 = hass.states.get("climate.hallway") - assert hass.states.get("sensor.hallway_timer_end_time").state == STATE_UNKNOWN - assert hass.states.get("binary_sensor.hallway_timer_running").state == "off" - - with pytest.raises(ValueError): - await hass.services.async_call( - DOMAIN, - SERVICE_TIMER, - { - ATTR_ENTITY_ID: state1.entity_id, - ATTR_STATE: "on", - }, - blocking=True, - ) - await hass.async_block_till_done() - - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", - return_value={"status": "success", "result": {"id": ""}}, - ): - await hass.services.async_call( - DOMAIN, - SERVICE_TIMER, - { - ATTR_ENTITY_ID: state1.entity_id, - ATTR_STATE: "on", - ATTR_MINUTES: 30, - }, - blocking=True, - ) - await hass.async_block_till_done() - - monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_on", True) - monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_id", None) - monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_state_on", False) - monkeypatch.setattr( - get_data.parsed["ABC999111"], - "timer_time", - datetime(2022, 6, 6, 12, 00, 00, tzinfo=dt.UTC), - ) - - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() - - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - DOMAIN, - SERVICE_TIMER, - { - ATTR_ENTITY_ID: state1.entity_id, - ATTR_STATE: "off", - }, - blocking=True, - ) - await hass.async_block_till_done() - - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() - - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", - return_value={"status": "failure"}, - ): - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - DOMAIN, - SERVICE_TIMER, - { - ATTR_ENTITY_ID: state1.entity_id, - ATTR_STATE: "on", - ATTR_MINUTES: 30, - }, - blocking=True, - ) - await hass.async_block_till_done() async def test_climate_pure_boost( diff --git a/tests/components/sensibo/test_switch.py b/tests/components/sensibo/test_switch.py new file mode 100644 index 00000000000..49efca4103e --- /dev/null +++ b/tests/components/sensibo/test_switch.py @@ -0,0 +1,165 @@ +"""The test for the sensibo switch platform.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +from pysensibo.model import SensiboData +import pytest +from pytest import MonkeyPatch + +from homeassistant.components.sensibo.switch import build_params +from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import dt + +from tests.common import async_fire_time_changed + + +async def test_switch( + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: MonkeyPatch, + get_data: SensiboData, +) -> None: + """Test the Sensibo switch.""" + + state1 = hass.states.get("switch.hallway_timer") + assert state1.state == STATE_OFF + assert state1.attributes["id"] is None + assert state1.attributes["turn_on"] is None + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", + return_value={"status": "success", "result": {"id": "SzTGE4oZ4D"}}, + ): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: state1.entity_id, + }, + blocking=True, + ) + await hass.async_block_till_done() + + monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_on", True) + monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_id", "SzTGE4oZ4D") + monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_state_on", False) + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + state1 = hass.states.get("switch.hallway_timer") + assert state1.state == STATE_ON + assert state1.attributes["id"] == "SzTGE4oZ4D" + assert state1.attributes["turn_on"] is False + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_del_timer", + return_value={"status": "success", "result": {"id": "SzTGE4oZ4D"}}, + ): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: state1.entity_id, + }, + blocking=True, + ) + await hass.async_block_till_done() + + monkeypatch.setattr(get_data.parsed["ABC999111"], "timer_on", False) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("switch.hallway_timer") + assert state1.state == STATE_OFF + + +async def test_switch_command_failure( + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: MonkeyPatch, + get_data: SensiboData, +) -> None: + """Test the Sensibo switch fails commands.""" + + state1 = hass.states.get("switch.hallway_timer") + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_timer", + return_value={"status": "failure"}, + ): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: state1.entity_id, + }, + blocking=True, + ) + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_del_timer", + return_value={"status": "failure"}, + ): + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: state1.entity_id, + }, + blocking=True, + ) + + +async def test_build_params( + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: MonkeyPatch, + get_data: SensiboData, +) -> None: + """Test the build params method.""" + + assert build_params("set_timer", get_data.parsed["ABC999111"]) == { + "minutesFromNow": 60, + "acState": {**get_data.parsed["ABC999111"].ac_states, "on": False}, + } + assert build_params("incorrect_command", get_data.parsed["ABC999111"]) is None From 24bf42cfbe674f5cffc3778b0de8ec617c50aa78 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 19 Jun 2022 16:29:57 +0200 Subject: [PATCH 1576/3516] Update pylint to 2.14.3 (#73703) --- homeassistant/components/recorder/pool.py | 4 +--- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py index 52b6b74dfa1..a8579df834c 100644 --- a/homeassistant/components/recorder/pool.py +++ b/homeassistant/components/recorder/pool.py @@ -87,9 +87,7 @@ class RecorderPool(SingletonThreadPool, NullPool): # type: ignore[misc] exclude_integrations={"recorder"}, error_if_core=False, ) - return super( # pylint: disable=bad-super-call - NullPool, self - )._create_connection() + return super(NullPool, self)._create_connection() class MutexPool(StaticPool): # type: ignore[misc] diff --git a/requirements_test.txt b/requirements_test.txt index 2ccbd6ab440..046d8bfb400 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ freezegun==1.2.1 mock-open==1.4.0 mypy==0.961 pre-commit==2.19.0 -pylint==2.14.1 +pylint==2.14.3 pipdeptree==2.2.1 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 From be6c2554dd73232ae4e0f0d9e3d92bcf9e4fba12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sun, 19 Jun 2022 16:43:29 +0200 Subject: [PATCH 1577/3516] Add QNAP QSW DHCP discovery (#73130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * qnap_qsw: add DHCP discovery Signed-off-by: Álvaro Fernández Rojas * qnap_qsw: config_flow: add async_step_dhcp Signed-off-by: Álvaro Fernández Rojas * qnap_qsw: config_flow: lower DHCP logging Signed-off-by: Álvaro Fernández Rojas * tests: qnap_qsw: fix copy & paste Signed-off-by: Álvaro Fernández Rojas * qnap_qsw: dhcp: introduce changes suggested by @bdraco Signed-off-by: Álvaro Fernández Rojas * Update homeassistant/components/qnap_qsw/config_flow.py Co-authored-by: J. Nick Koston * qnap_qsw: async_step_user: disable raising on progress Allows async_step_user to win over a discovery. Signed-off-by: Álvaro Fernández Rojas Co-authored-by: J. Nick Koston --- .../components/qnap_qsw/config_flow.py | 67 ++++++++- .../components/qnap_qsw/manifest.json | 7 +- homeassistant/generated/dhcp.py | 1 + tests/components/qnap_qsw/test_config_flow.py | 137 +++++++++++++++++- 4 files changed, 209 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qnap_qsw/config_flow.py b/homeassistant/components/qnap_qsw/config_flow.py index 891c72c9911..e9d11433021 100644 --- a/homeassistant/components/qnap_qsw/config_flow.py +++ b/homeassistant/components/qnap_qsw/config_flow.py @@ -1,6 +1,7 @@ """Config flow for QNAP QSW.""" from __future__ import annotations +import logging from typing import Any from aioqsw.exceptions import LoginError, QswError @@ -8,6 +9,7 @@ from aioqsw.localapi import ConnectionOptions, QnapQswApi import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import dhcp from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import aiohttp_client @@ -15,10 +17,15 @@ from homeassistant.helpers.device_registry import format_mac from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle config flow for a QNAP QSW device.""" + _discovered_mac: str | None = None + _discovered_url: str | None = None + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -46,7 +53,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if mac is None: raise AbortFlow("invalid_id") - await self.async_set_unique_id(format_mac(mac)) + await self.async_set_unique_id(format_mac(mac), raise_on_progress=False) self._abort_if_unique_id_configured() title = f"QNAP {system_board.get_product()} {mac}" @@ -63,3 +70,61 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ), errors=errors, ) + + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Handle DHCP discovery.""" + self._discovered_url = f"http://{discovery_info.ip}" + self._discovered_mac = discovery_info.macaddress + + _LOGGER.debug("DHCP discovery detected QSW: %s", self._discovered_mac) + + mac = format_mac(self._discovered_mac) + options = ConnectionOptions(self._discovered_url, "", "") + qsw = QnapQswApi(aiohttp_client.async_get_clientsession(self.hass), options) + + try: + await qsw.get_live() + except QswError as err: + raise AbortFlow("cannot_connect") from err + + await self.async_set_unique_id(format_mac(mac)) + self._abort_if_unique_id_configured() + + return await self.async_step_discovered_connection() + + async def async_step_discovered_connection( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + errors = {} + assert self._discovered_url is not None + + if user_input is not None: + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + + qsw = QnapQswApi( + aiohttp_client.async_get_clientsession(self.hass), + ConnectionOptions(self._discovered_url, username, password), + ) + + try: + system_board = await qsw.validate() + except LoginError: + errors[CONF_PASSWORD] = "invalid_auth" + except QswError: + errors[CONF_URL] = "cannot_connect" + else: + title = f"QNAP {system_board.get_product()} {self._discovered_mac}" + return self.async_create_entry(title=title, data=user_input) + + return self.async_show_form( + step_id="discovered_connection", + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/qnap_qsw/manifest.json b/homeassistant/components/qnap_qsw/manifest.json index 0dfd0e4793e..be565f2a07e 100644 --- a/homeassistant/components/qnap_qsw/manifest.json +++ b/homeassistant/components/qnap_qsw/manifest.json @@ -6,5 +6,10 @@ "requirements": ["aioqsw==0.1.0"], "codeowners": ["@Noltari"], "iot_class": "local_polling", - "loggers": ["aioqsw"] + "loggers": ["aioqsw"], + "dhcp": [ + { + "macaddress": "245EBE*" + } + ] } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 43bf7ca2715..9f2438aafa1 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -74,6 +74,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'oncue', 'hostname': 'kohlergen*', 'macaddress': '00146F*'}, {'domain': 'overkiz', 'hostname': 'gateway*', 'macaddress': 'F8811A*'}, {'domain': 'powerwall', 'hostname': '1118431-*'}, + {'domain': 'qnap_qsw', 'macaddress': '245EBE*'}, {'domain': 'rachio', 'hostname': 'rachio-*', 'macaddress': '009D6B*'}, {'domain': 'rachio', 'hostname': 'rachio-*', 'macaddress': 'F0038C*'}, {'domain': 'rachio', 'hostname': 'rachio-*', 'macaddress': '74C63B*'}, diff --git a/tests/components/qnap_qsw/test_config_flow.py b/tests/components/qnap_qsw/test_config_flow.py index e8cc9c56c0a..0b7072dd602 100644 --- a/tests/components/qnap_qsw/test_config_flow.py +++ b/tests/components/qnap_qsw/test_config_flow.py @@ -5,7 +5,8 @@ from unittest.mock import MagicMock, patch from aioqsw.const import API_MAC_ADDR, API_PRODUCT, API_RESULT from aioqsw.exceptions import LoginError, QswError -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow +from homeassistant.components import dhcp from homeassistant.components.qnap_qsw.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME @@ -16,6 +17,16 @@ from .util import CONFIG, LIVE_MOCK, SYSTEM_BOARD_MOCK, USERS_LOGIN_MOCK from tests.common import MockConfigEntry +DHCP_SERVICE_INFO = dhcp.DhcpServiceInfo( + hostname="qsw-m408-4c", + ip="192.168.1.200", + macaddress="245EBE000000", +) + +TEST_PASSWORD = "test-password" +TEST_URL = "test-url" +TEST_USERNAME = "test-username" + async def test_form(hass: HomeAssistant) -> None: """Test that the form is served with valid input.""" @@ -134,3 +145,127 @@ async def test_login_error(hass: HomeAssistant): ) assert result["errors"] == {CONF_PASSWORD: "invalid_auth"} + + +async def test_dhcp_flow(hass: HomeAssistant) -> None: + """Test that DHCP discovery works.""" + with patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_live", + return_value=LIVE_MOCK, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DHCP_SERVICE_INFO, + context={"source": config_entries.SOURCE_DHCP}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "discovered_connection" + + with patch( + "homeassistant.components.qnap_qsw.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_live", + return_value=LIVE_MOCK, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_system_board", + return_value=SYSTEM_BOARD_MOCK, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.post_users_login", + return_value=USERS_LOGIN_MOCK, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + + assert result2["type"] == "create_entry" + assert result2["data"] == { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_dhcp_flow_error(hass: HomeAssistant) -> None: + """Test that DHCP discovery fails.""" + + with patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_live", + side_effect=QswError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DHCP_SERVICE_INFO, + context={"source": config_entries.SOURCE_DHCP}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_dhcp_connection_error(hass: HomeAssistant): + """Test DHCP connection to host error.""" + + with patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_live", + return_value=LIVE_MOCK, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DHCP_SERVICE_INFO, + context={"source": config_entries.SOURCE_DHCP}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "discovered_connection" + + with patch( + "homeassistant.components.qnap_qsw.QnapQswApi.validate", + side_effect=QswError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + + assert result["errors"] == {CONF_URL: "cannot_connect"} + + +async def test_dhcp_login_error(hass: HomeAssistant): + """Test DHCP login error.""" + + with patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_live", + return_value=LIVE_MOCK, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DHCP_SERVICE_INFO, + context={"source": config_entries.SOURCE_DHCP}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "discovered_connection" + + with patch( + "homeassistant.components.qnap_qsw.QnapQswApi.validate", + side_effect=LoginError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + + assert result["errors"] == {CONF_PASSWORD: "invalid_auth"} From 26641fc90df780e9e2e3b00f0efe18e6a4acda1c Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sun, 19 Jun 2022 19:59:37 +0200 Subject: [PATCH 1578/3516] Bump async-upnp-client to 0.31.2 (#73712) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/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 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index cc72f5d4778..7e03d34e900 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.31.1"], + "requirements": ["async-upnp-client==0.31.2"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index 590d1b8370a..a07f33d09dd 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dms", - "requirements": ["async-upnp-client==0.31.1"], + "requirements": ["async-upnp-client==0.31.2"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index ce65af7d8bb..9cd068ef409 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -7,7 +7,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.5.0", "wakeonlan==2.0.1", - "async-upnp-client==0.31.1" + "async-upnp-client==0.31.2" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index f0db05d9015..88e3d0f4286 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.31.1"], + "requirements": ["async-upnp-client==0.31.2"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index dc87e73fdee..a4b913ec4c8 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.31.1", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.31.2", "getmac==0.8.2"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman", "@ehendrix23"], "ssdp": [ diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 57d32f315ba..1032ef0d2e5 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.10", "async-upnp-client==0.31.1"], + "requirements": ["yeelight==0.7.10", "async-upnp-client==0.31.2"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f423c96dbac..8452b4e62fb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodiscover==1.4.11 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.31.1 +async-upnp-client==0.31.2 async_timeout==4.0.2 atomicwrites==1.4.0 attrs==21.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index ad12a87c152..9c37ee79679 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -339,7 +339,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.31.1 +async-upnp-client==0.31.2 # homeassistant.components.supla asyncpysupla==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 886c0921e27..c793c9a564a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -281,7 +281,7 @@ arcam-fmj==0.12.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.31.1 +async-upnp-client==0.31.2 # homeassistant.components.sleepiq asyncsleepiq==1.2.3 From 9b930717205fd273b94cb69e71d9bcac102cd4cb Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 19 Jun 2022 14:12:01 -0400 Subject: [PATCH 1579/3516] Bump zwave-js-server-python to 0.38.0 (#73707) * Bump zwave-js-server-python to 0.38.0 * Fix test --- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_api.py | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 40b74096f0c..f2670be7b80 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.37.2"], + "requirements": ["zwave-js-server-python==0.38.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 9c37ee79679..687abb03abd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2522,7 +2522,7 @@ zigpy==0.46.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.37.2 +zwave-js-server-python==0.38.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c793c9a564a..1f07b1a4a1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1671,7 +1671,7 @@ zigpy-znp==0.7.0 zigpy==0.46.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.37.2 +zwave-js-server-python==0.38.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 88fea684233..f303db6af75 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -3372,7 +3372,7 @@ async def test_abort_firmware_update( ws_client = await hass_ws_client(hass) device = get_device(hass, multisensor_6) - client.async_send_command_no_wait.return_value = {} + client.async_send_command.return_value = {} await ws_client.send_json( { ID: 1, @@ -3383,8 +3383,8 @@ async def test_abort_firmware_update( msg = await ws_client.receive_json() assert msg["success"] - assert len(client.async_send_command_no_wait.call_args_list) == 1 - args = client.async_send_command_no_wait.call_args[0][0] + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] assert args["command"] == "node.abort_firmware_update" assert args["nodeId"] == multisensor_6.node_id From ab95299150ea489d42b6903454b9e064344f98fb Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 19 Jun 2022 11:16:07 -0700 Subject: [PATCH 1580/3516] Bump gcal_sync to 0.10.0 and fix `google` typing (#73710) Bump gcal_sync to 0.10.0 --- homeassistant/components/google/api.py | 4 ++-- homeassistant/components/google/calendar.py | 2 +- homeassistant/components/google/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index a4cda1ff41a..f4a4912a1b9 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -173,7 +173,7 @@ async def async_create_device_flow( return DeviceFlow(hass, oauth_flow, device_flow_info) -class ApiAuthImpl(AbstractAuth): # type: ignore[misc] +class ApiAuthImpl(AbstractAuth): """Authentication implementation for google calendar api library.""" def __init__( @@ -191,7 +191,7 @@ class ApiAuthImpl(AbstractAuth): # type: ignore[misc] return cast(str, self._session.token["access_token"]) -class AccessTokenAuthImpl(AbstractAuth): # type: ignore[misc] +class AccessTokenAuthImpl(AbstractAuth): """Authentication implementation used during config flow, without refresh. This exists to allow the config flow to use the API before it has fully diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 39e3d69e6b9..e27eb0b1336 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -224,7 +224,7 @@ class GoogleCalendarEntity(CalendarEntity): """Return True if the event is visible.""" if self._ignore_availability: return True - return event.transparency == OPAQUE # type: ignore[no-any-return] + return event.transparency == OPAQUE async def async_get_events( self, hass: HomeAssistant, start_date: datetime, end_date: datetime diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 081eae34a95..d39f2093cf0 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", - "requirements": ["gcal-sync==0.9.0", "oauth2client==4.1.3"], + "requirements": ["gcal-sync==0.10.0", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], "iot_class": "cloud_polling", "loggers": ["googleapiclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 687abb03abd..494ad4f27ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -692,7 +692,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==0.9.0 +gcal-sync==0.10.0 # homeassistant.components.geniushub geniushub-client==0.6.30 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f07b1a4a1d..af74cc1329c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -495,7 +495,7 @@ gTTS==2.2.4 garages-amsterdam==3.0.0 # homeassistant.components.google -gcal-sync==0.9.0 +gcal-sync==0.10.0 # homeassistant.components.geocaching geocachingapi==0.2.1 From 801ba6ff8e2d6161d5b6f3fd6622afe9570b9be9 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 19 Jun 2022 14:50:05 -0400 Subject: [PATCH 1581/3516] Add target option to zwave_js firmware upload view (#73690) --- homeassistant/components/zwave_js/api.py | 7 ++++++- tests/components/zwave_js/test_api.py | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index b41c6bf2f92..75180cfa84f 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable import dataclasses from functools import partial, wraps -from typing import Any, Literal +from typing import Any, Literal, cast from aiohttp import web, web_exceptions, web_request import voluptuous as vol @@ -1976,6 +1976,10 @@ class FirmwareUploadView(HomeAssistantView): if "file" not in data or not isinstance(data["file"], web_request.FileField): raise web_exceptions.HTTPBadRequest + target = None + if "target" in data: + target = int(cast(str, data["target"])) + uploaded_file: web_request.FileField = data["file"] try: @@ -1985,6 +1989,7 @@ class FirmwareUploadView(HomeAssistantView): uploaded_file.filename, await hass.async_add_executor_job(uploaded_file.file.read), async_get_clientsession(hass), + target=target, ) except BaseZwaveJSServerError as err: raise web_exceptions.HTTPBadRequest(reason=str(err)) from err diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index f303db6af75..149fe394a6d 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -2847,9 +2847,10 @@ async def test_firmware_upload_view( ) as mock_cmd: resp = await client.post( f"/api/zwave_js/firmware/upload/{device.id}", - data={"file": firmware_file}, + data={"file": firmware_file, "target": "15"}, ) assert mock_cmd.call_args[0][1:4] == (multisensor_6, "file", bytes(10)) + assert mock_cmd.call_args[1] == {"target": 15} assert json.loads(await resp.text()) is None From bb5a6a71046830e5c784ef5f9b792cb84579218d Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 19 Jun 2022 14:50:47 -0400 Subject: [PATCH 1582/3516] Add `zwave_js/get_firmware_update_capabilties` WS command (#73691) * Add zwave_js/get_firmware_update_capabilties WS command * Fix test --- homeassistant/components/zwave_js/api.py | 24 +++++ tests/components/zwave_js/test_api.py | 112 +++++++++++++++++------ 2 files changed, 110 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 75180cfa84f..6b6286b78f4 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -417,6 +417,9 @@ def async_register_api(hass: HomeAssistant) -> None: websocket_api.async_register_command( hass, websocket_subscribe_firmware_update_status ) + websocket_api.async_register_command( + hass, websocket_get_firmware_update_capabilities + ) websocket_api.async_register_command(hass, websocket_check_for_config_updates) websocket_api.async_register_command(hass, websocket_install_config_update) websocket_api.async_register_command( @@ -1944,6 +1947,27 @@ async def websocket_subscribe_firmware_update_status( ) +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/get_firmware_update_capabilities", + vol.Required(DEVICE_ID): str, + } +) +@websocket_api.async_response +@async_handle_failed_command +@async_get_node +async def websocket_get_firmware_update_capabilities( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + node: Node, +) -> None: + """Abort a firmware update.""" + capabilities = await node.async_get_firmware_update_capabilities() + connection.send_result(msg[ID], capabilities.to_dict()) + + class FirmwareUploadView(HomeAssistantView): """View to upload firmware.""" diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 149fe394a6d..337c74e955b 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -3423,19 +3423,10 @@ async def test_abort_firmware_update( assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_LOADED - -async def test_abort_firmware_update_failures( - hass, multisensor_6, client, integration, hass_ws_client -): - """Test failures for the abort_firmware_update websocket command.""" - entry = integration - ws_client = await hass_ws_client(hass) - device = get_device(hass, multisensor_6) - # Test sending command with improper device ID fails await ws_client.send_json( { - ID: 2, + ID: 4, TYPE: "zwave_js/abort_firmware_update", DEVICE_ID: "fake_device", } @@ -3445,22 +3436,6 @@ async def test_abort_firmware_update_failures( assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_FOUND - # Test sending command with not loaded entry fails - await hass.config_entries.async_unload(entry.entry_id) - await hass.async_block_till_done() - - await ws_client.send_json( - { - ID: 3, - TYPE: "zwave_js/abort_firmware_update", - DEVICE_ID: device.id, - } - ) - msg = await ws_client.receive_json() - - assert not msg["success"] - assert msg["error"]["code"] == ERR_NOT_LOADED - async def test_subscribe_firmware_update_status( hass, multisensor_6, integration, client, hass_ws_client @@ -3603,6 +3578,91 @@ async def test_subscribe_firmware_update_status_failures( assert msg["error"]["code"] == ERR_NOT_LOADED +async def test_get_firmware_update_capabilities( + hass, client, multisensor_6, integration, hass_ws_client +): + """Test that the get_firmware_update_capabilities WS API call works.""" + entry = integration + ws_client = await hass_ws_client(hass) + device = get_device(hass, multisensor_6) + + client.async_send_command.return_value = { + "capabilities": { + "firmwareUpgradable": True, + "firmwareTargets": [0], + "continuesToFunction": True, + "supportsActivation": True, + } + } + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/get_firmware_update_capabilities", + DEVICE_ID: device.id, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] == { + "firmware_upgradable": True, + "firmware_targets": [0], + "continues_to_function": True, + "supports_activation": True, + } + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.get_firmware_update_capabilities" + assert args["nodeId"] == multisensor_6.node_id + + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.node.Node.async_get_firmware_update_capabilities", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/get_firmware_update_capabilities", + DEVICE_ID: device.id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/get_firmware_update_capabilities", + DEVICE_ID: device.id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + # Test sending command with improper device ID fails + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/get_firmware_update_capabilities", + DEVICE_ID: "fake_device", + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + async def test_check_for_config_updates(hass, client, integration, hass_ws_client): """Test that the check_for_config_updates WS API call works.""" entry = integration From e53372f5590d8259336488c73b860b679e7bc738 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 19 Jun 2022 15:33:58 -0400 Subject: [PATCH 1583/3516] Add `zwave_js/get_firmware_update_progress` WS command (#73304) Add zwave_js/get_firmware_update_progress WS command --- homeassistant/components/zwave_js/api.py | 21 +++++++++ tests/components/zwave_js/test_api.py | 60 ++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 6b6286b78f4..ca25088a642 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -414,6 +414,7 @@ def async_register_api(hass: HomeAssistant) -> None: ) websocket_api.async_register_command(hass, websocket_data_collection_status) websocket_api.async_register_command(hass, websocket_abort_firmware_update) + websocket_api.async_register_command(hass, websocket_get_firmware_update_progress) websocket_api.async_register_command( hass, websocket_subscribe_firmware_update_status ) @@ -1867,6 +1868,26 @@ async def websocket_abort_firmware_update( connection.send_result(msg[ID]) +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/get_firmware_update_progress", + vol.Required(DEVICE_ID): str, + } +) +@websocket_api.async_response +@async_handle_failed_command +@async_get_node +async def websocket_get_firmware_update_progress( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + node: Node, +) -> None: + """Get whether firmware update is in progress.""" + connection.send_result(msg[ID], await node.async_get_firmware_update_progress()) + + def _get_firmware_update_progress_dict( progress: FirmwareUpdateProgress, ) -> dict[str, int]: diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 337c74e955b..1ed125cc43a 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -3437,6 +3437,66 @@ async def test_abort_firmware_update( assert msg["error"]["code"] == ERR_NOT_FOUND +async def test_get_firmware_update_progress( + hass, client, multisensor_6, integration, hass_ws_client +): + """Test that the get_firmware_update_progress WS API call works.""" + entry = integration + ws_client = await hass_ws_client(hass) + device = get_device(hass, multisensor_6) + + client.async_send_command.return_value = {"progress": True} + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/get_firmware_update_progress", + DEVICE_ID: device.id, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.get_firmware_update_progress" + assert args["nodeId"] == multisensor_6.node_id + + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.node.Node.async_get_firmware_update_progress", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/get_firmware_update_progress", + DEVICE_ID: device.id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/get_firmware_update_progress", + DEVICE_ID: device.id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + async def test_subscribe_firmware_update_status( hass, multisensor_6, integration, client, hass_ws_client ): From e7e9c65e44f5c9a0bceb0f6737952acb8494dccd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 19 Jun 2022 21:38:01 +0200 Subject: [PATCH 1584/3516] Adjust zha routine to get name and original_name (#73646) --- homeassistant/components/zha/api.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index faf8ccc5053..f99255f55a9 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -60,6 +60,7 @@ from .core.const import ( ZHA_CHANNEL_MSG, ZHA_CONFIG_SCHEMAS, ) +from .core.gateway import EntityReference from .core.group import GroupMember from .core.helpers import ( async_cluster_exists, @@ -316,6 +317,22 @@ async def websocket_get_devices( connection.send_result(msg[ID], devices) +@callback +def _get_entity_name( + zha_gateway: ZHAGateway, entity_ref: EntityReference +) -> str | None: + entry = zha_gateway.ha_entity_registry.async_get(entity_ref.reference_id) + return entry.name if entry else None + + +@callback +def _get_entity_original_name( + zha_gateway: ZHAGateway, entity_ref: EntityReference +) -> str | None: + entry = zha_gateway.ha_entity_registry.async_get(entity_ref.reference_id) + return entry.original_name if entry else None + + @websocket_api.require_admin @websocket_api.websocket_command({vol.Required(TYPE): "zha/devices/groupable"}) @websocket_api.async_response @@ -336,12 +353,10 @@ async def websocket_get_groupable_devices( "endpoint_id": ep_id, "entities": [ { - "name": zha_gateway.ha_entity_registry.async_get( - entity_ref.reference_id - ).name, - "original_name": zha_gateway.ha_entity_registry.async_get( - entity_ref.reference_id - ).original_name, + "name": _get_entity_name(zha_gateway, entity_ref), + "original_name": _get_entity_original_name( + zha_gateway, entity_ref + ), } for entity_ref in entity_refs if list(entity_ref.cluster_channels.values())[ From a92105171cc69a5ada661a67a3abfaf8fa55b213 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 19 Jun 2022 21:39:24 +0200 Subject: [PATCH 1585/3516] Remove vizio from mypy ignore list (#73585) * Remove vizio config_flow from mypy ignore list * Fix mypy errors * Adjust media_player * Add space --- homeassistant/components/vizio/config_flow.py | 10 ++++++---- homeassistant/components/vizio/media_player.py | 15 +++++++++------ homeassistant/helpers/config_validation.py | 2 +- mypy.ini | 6 ------ script/hassfest/mypy_config.py | 2 -- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 8acc602dd36..a80105579fe 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -187,11 +187,11 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize config flow.""" self._user_schema = None - self._must_show_form = None + self._must_show_form: bool | None = None self._ch_type = None self._pairing_token = None - self._data = None - self._apps = {} + self._data: dict[str, Any] | None = None + self._apps: dict[str, list] = {} async def _create_entry(self, input_dict: dict[str, Any]) -> FlowResult: """Create vizio config entry.""" @@ -387,10 +387,11 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): Ask user for PIN to complete pairing process. """ - errors = {} + errors: dict[str, str] = {} # Start pairing process if it hasn't already started if not self._ch_type and not self._pairing_token: + assert self._data dev = VizioAsync( DEVICE_ID, self._data[CONF_HOST], @@ -448,6 +449,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _pairing_complete(self, step_id: str) -> FlowResult: """Handle config flow completion.""" + assert self._data if not self._must_show_form: return await self._create_entry(self._data) diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index b2d33551020..ab48f1405a9 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any from pyvizio import VizioAsync from pyvizio.api.apps import find_app_name @@ -103,7 +102,10 @@ async def async_setup_entry( params["data"] = new_data if params: - hass.config_entries.async_update_entry(config_entry, **params) + hass.config_entries.async_update_entry( + config_entry, + **params, # type: ignore[arg-type] + ) device = VizioAsync( DEVICE_ID, @@ -134,7 +136,7 @@ class VizioDevice(MediaPlayerEntity): config_entry: ConfigEntry, device: VizioAsync, name: str, - device_class: str, + device_class: MediaPlayerDeviceClass, apps_coordinator: DataUpdateCoordinator, ) -> None: """Initialize Vizio device.""" @@ -145,8 +147,8 @@ class VizioDevice(MediaPlayerEntity): self._current_input = None self._current_app_config = None self._attr_app_name = None - self._available_inputs = [] - self._available_apps = [] + self._available_inputs: list[str] = [] + self._available_apps: list[str] = [] self._all_apps = apps_coordinator.data if apps_coordinator else None self._conf_apps = config_entry.options.get(CONF_APPS, {}) self._additional_app_configs = config_entry.data.get(CONF_APPS, {}).get( @@ -195,6 +197,7 @@ class VizioDevice(MediaPlayerEntity): self._attr_available = True if not self._attr_device_info: + assert self._attr_unique_id self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._attr_unique_id)}, manufacturer="VIZIO", @@ -276,7 +279,7 @@ class VizioDevice(MediaPlayerEntity): if self._attr_app_name == NO_APP_RUNNING: self._attr_app_name = None - def _get_additional_app_names(self) -> list[dict[str, Any]]: + def _get_additional_app_names(self) -> list[str]: """Return list of additional apps that were included in configuration.yaml.""" return [ additional_app["name"] for additional_app in self._additional_app_configs diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index f459d96040b..2ed4bc7abab 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -758,7 +758,7 @@ def ensure_list_csv(value: Any) -> list: class multi_select: """Multi select validator returning list of selected values.""" - def __init__(self, options: dict) -> None: + def __init__(self, options: dict | list) -> None: """Initialize multi select.""" self.options = options diff --git a/mypy.ini b/mypy.ini index 8a9c5b0478a..f19f5f5dc68 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2940,12 +2940,6 @@ ignore_errors = true [mypy-homeassistant.components.unifi.unifi_entity_base] ignore_errors = true -[mypy-homeassistant.components.vizio.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.vizio.media_player] -ignore_errors = true - [mypy-homeassistant.components.withings] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index b7fb778cfee..09a9820e7b4 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -129,8 +129,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.unifi.device_tracker", "homeassistant.components.unifi.diagnostics", "homeassistant.components.unifi.unifi_entity_base", - "homeassistant.components.vizio.config_flow", - "homeassistant.components.vizio.media_player", "homeassistant.components.withings", "homeassistant.components.withings.binary_sensor", "homeassistant.components.withings.common", From 6f8e0419f01899ddade4b5982f4d333261e25697 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 20 Jun 2022 00:22:34 +0000 Subject: [PATCH 1586/3516] [ci skip] Translation update --- homeassistant/components/nest/translations/ca.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index 7b1a9c91bb4..0767d9c1cf1 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "Segueix les [instruccions]({more_info_url}) per configurar la Cloud Console: \n\n1. V\u00e9s a la [pantalla de consentiment OAuth]({oauth_consent_url}) i configura\n2. V\u00e9s a [Credencials]({oauth_creds_url}) i fes clic a **Crear credencials**.\n3. A la llista desplegable, selecciona **ID de client OAuth**.\n4. Selecciona **Aplicaci\u00f3 web** al tipus d'aplicaci\u00f3.\n5. Afegeix `{redirect_url}` a *URI de redirecci\u00f3 autoritzat*." + }, "config": { "abort": { "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", @@ -29,22 +32,30 @@ "description": "Per enlla\u00e7ar un compte de Google, [autoritza el compte]({url}). \n\nDespr\u00e9s de l'autoritzaci\u00f3, copia i enganxa a continuaci\u00f3 el codi 'token' d'autenticaci\u00f3 proporcionat.", "title": "Vinculaci\u00f3 amb compte de Google" }, + "auth_upgrade": { + "description": "Google ha deixat d'utilitzar l'autenticaci\u00f3 d'aplicacions per millorar la seguretat i has de crear noves credencials d'aplicaci\u00f3. \n\nConsulta la [documentaci\u00f3]({more_info_url}) i segueix els passos que et guiaran per tornar a tenir acc\u00e9s als teus dispositius Nest.", + "title": "Nest: l'autenticaci\u00f3 d'aplicaci\u00f3 s'acaba" + }, "cloud_project": { "data": { "cloud_project_id": "ID de projecte Google Cloud" }, + "description": "Introdueix l'identificador de projecte Cloud a continuaci\u00f3, per exemple, *exemple-projecte-12345*. Consulta [Google Cloud Console]({cloud_console_url}) o la [documentaci\u00f3]({more_info_url}) per a m\u00e9s informaci\u00f3.", "title": "Nest: introdueix l'identificador del projecte Cloud" }, "create_cloud_project": { + "description": "La integraci\u00f3 Nest et permet integrar els termostats, c\u00e0meres i timbres de Nest mitjan\u00e7ant l'API de gesti\u00f3 de dispositius intel\u00b7ligents. L'API SDM **estableix una tarifa de configuraci\u00f3 de 5\u202f$** nom\u00e9s la primera vegada. Consulta la documentaci\u00f3 per a [m\u00e9s informaci\u00f3]({more_info_url}). \n\n1. V\u00e9s a [Google Cloud Console]({cloud_console_url}).\n2. Si aquest \u00e9s el teu primer projecte, fes clic a **Crea projecte** i despr\u00e9s a **Projecte nou**.\n3. D\u00f3na-li un nom al teu projecte Cloud i fes clic a **Crea**.\n4. Desa l'identificador del projecte Cloud, per exemple, *exemple-projecte-12345*, ja que el necessitar\u00e0s m\u00e9s endavant\n5. V\u00e9s a la Biblioteca API de [API de gesti\u00f3 de dispositius intel\u00b7ligents]({sdm_api_url}) i fes clic a **Activar** ('Enable').\n6. V\u00e9s a la Biblioteca API de [API Cloud Pub/Sub]({pubsub_api_url}) i fes clic a **Activar** ('Enable'). \n\nContinua quan el teu projecte Cloud estigui configurat.", "title": "Nest: crea i configura el projecte Cloud" }, "device_project": { "data": { "project_id": "ID de projecte Device Access" }, + "description": "Crea un projecte d'acc\u00e9s a dispositius Nest que **requereix una tarifa de 5 $** per configurar-lo.\n1. V\u00e9s a [Consola d'acc\u00e9s al dispositiu]({device_access_console_url}) i a trav\u00e9s del flux de pagament.\n2. Fes clic a **Crea projecte**.\n3. D\u00f3na-li un nom al projecte d'acc\u00e9s a dispositius i feu clic a **Seg\u00fcent**.\n4. Introdueix el teu ID de client OAuth.\n5. Activa els esdeveniments fent clic a **Activa** i **Crea projecte**. \n\nIntrodueix el teu ID de projecte d'acc\u00e9s a dispositiu a continuaci\u00f3 ([m\u00e9s informaci\u00f3]({more_info_url})).\n", "title": "Nest: crea un projecte Device Access" }, "device_project_upgrade": { + "description": "Actualitza el projecte d'acc\u00e9s al dispositiu Nest amb el teu nou ID de client OAuth ([m\u00e9s informaci\u00f3]({more_info_url}))\n1. V\u00e9s a la [Consola d'acc\u00e9s al dispositiu]({device_access_console_url}).\n2. Fes clic a la icona de la paperera que hi ha al costat de *OAuth Client ID*.\n3. Feu clic al men\u00fa desplegable `...` i *Afegeix un ID de client*.\n4. Introdueix el teu nou ID de client OAuth i feu clic a **Afegeix**.\n\nEl teu ID de client OAuth \u00e9s: `{client_id}`", "title": "Nest: actualitza projecte Device Access" }, "init": { From fcd885954250e9e3589290fafbc429859d177fa5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Jun 2022 21:24:42 -0500 Subject: [PATCH 1587/3516] Remove self from logbook codeowners (#73724) --- CODEOWNERS | 4 ++-- homeassistant/components/logbook/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index d94e8866633..183430eb646 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -582,8 +582,8 @@ build.json @home-assistant/supervisor /tests/components/local_ip/ @issacg /homeassistant/components/lock/ @home-assistant/core /tests/components/lock/ @home-assistant/core -/homeassistant/components/logbook/ @home-assistant/core @bdraco -/tests/components/logbook/ @home-assistant/core @bdraco +/homeassistant/components/logbook/ @home-assistant/core +/tests/components/logbook/ @home-assistant/core /homeassistant/components/logger/ @home-assistant/core /tests/components/logger/ @home-assistant/core /homeassistant/components/logi_circle/ @evanjd diff --git a/homeassistant/components/logbook/manifest.json b/homeassistant/components/logbook/manifest.json index 26a45e74439..66c0348a2ac 100644 --- a/homeassistant/components/logbook/manifest.json +++ b/homeassistant/components/logbook/manifest.json @@ -3,6 +3,6 @@ "name": "Logbook", "documentation": "https://www.home-assistant.io/integrations/logbook", "dependencies": ["frontend", "http", "recorder"], - "codeowners": ["@home-assistant/core", "@bdraco"], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } From 57daeaa17471323806671763e012b7d5a8f53af1 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 20 Jun 2022 08:51:12 +0200 Subject: [PATCH 1588/3516] Fix MQTT config schema to ensure correct validation (#73619) * Ensure config schema validation * Use correct schema for device_tracker * Remove schema validation from the platform setup * Remove loop to build schema --- homeassistant/components/mqtt/__init__.py | 6 +- .../components/mqtt/alarm_control_panel.py | 2 +- .../components/mqtt/binary_sensor.py | 4 +- homeassistant/components/mqtt/button.py | 4 +- homeassistant/components/mqtt/camera.py | 4 +- homeassistant/components/mqtt/climate.py | 4 +- homeassistant/components/mqtt/config.py | 107 +--------- .../components/mqtt/config_integration.py | 193 ++++++++++++++++++ homeassistant/components/mqtt/cover.py | 2 +- .../mqtt/device_tracker/__init__.py | 1 + .../mqtt/device_tracker/schema_discovery.py | 2 +- homeassistant/components/mqtt/fan.py | 4 +- homeassistant/components/mqtt/humidifier.py | 4 +- .../components/mqtt/light/__init__.py | 2 +- homeassistant/components/mqtt/lock.py | 2 +- homeassistant/components/mqtt/mixins.py | 22 +- homeassistant/components/mqtt/number.py | 4 +- homeassistant/components/mqtt/scene.py | 2 +- homeassistant/components/mqtt/select.py | 4 +- homeassistant/components/mqtt/sensor.py | 4 +- homeassistant/components/mqtt/siren.py | 2 +- homeassistant/components/mqtt/switch.py | 4 +- .../components/mqtt/vacuum/__init__.py | 4 +- tests/components/mqtt/test_humidifier.py | 13 ++ tests/components/mqtt/test_init.py | 34 ++- 25 files changed, 258 insertions(+), 176 deletions(-) create mode 100644 homeassistant/components/mqtt/config_integration.py diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 417d1400758..82b66ddc89e 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -47,7 +47,11 @@ from .client import ( # noqa: F401 publish, subscribe, ) -from .config import CONFIG_SCHEMA_BASE, DEFAULT_VALUES, DEPRECATED_CONFIG_KEYS +from .config_integration import ( + CONFIG_SCHEMA_BASE, + DEFAULT_VALUES, + DEPRECATED_CONFIG_KEYS, +) from .const import ( # noqa: F401 ATTR_PAYLOAD, ATTR_QOS, diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index c0c6f9732d7..6bb7d9cd0d1 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -148,7 +148,7 @@ async def async_setup_entry( """Set up MQTT alarm control panel through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery(hass, alarm.DOMAIN, PLATFORM_SCHEMA_MODERN) + await async_setup_platform_discovery(hass, alarm.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index cec065e20f2..39fd87c8b02 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -103,9 +103,7 @@ async def async_setup_entry( """Set up MQTT binary sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, binary_sensor.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, binary_sensor.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index afa9900db35..370243c3579 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -83,9 +83,7 @@ async def async_setup_entry( """Set up MQTT button through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, button.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, button.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 86db828b111..5c8d3bc48b2 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -81,9 +81,7 @@ async def async_setup_entry( """Set up MQTT camera through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, camera.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, camera.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index bdcc82f2c39..a26e9cba8df 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -392,9 +392,7 @@ async def async_setup_entry( """Set up MQTT climate device through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, climate.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, climate.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/config.py b/homeassistant/components/mqtt/config.py index 4f84d911418..8cfc3490f0c 100644 --- a/homeassistant/components/mqtt/config.py +++ b/homeassistant/components/mqtt/config.py @@ -3,126 +3,21 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.const import ( - CONF_CLIENT_ID, - CONF_DISCOVERY, - CONF_PASSWORD, - CONF_PORT, - CONF_PROTOCOL, - CONF_USERNAME, - CONF_VALUE_TEMPLATE, -) +from homeassistant.const import CONF_VALUE_TEMPLATE from homeassistant.helpers import config_validation as cv from .const import ( - ATTR_PAYLOAD, - ATTR_QOS, - ATTR_RETAIN, - ATTR_TOPIC, - CONF_BIRTH_MESSAGE, - CONF_BROKER, - CONF_CERTIFICATE, - CONF_CLIENT_CERT, - CONF_CLIENT_KEY, CONF_COMMAND_TOPIC, - CONF_DISCOVERY_PREFIX, CONF_ENCODING, - CONF_KEEPALIVE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - CONF_TLS_INSECURE, - CONF_TLS_VERSION, - CONF_WILL_MESSAGE, - DEFAULT_BIRTH, - DEFAULT_DISCOVERY, DEFAULT_ENCODING, - DEFAULT_PREFIX, DEFAULT_QOS, DEFAULT_RETAIN, - DEFAULT_WILL, - PLATFORMS, - PROTOCOL_31, - PROTOCOL_311, ) from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic -DEFAULT_PORT = 1883 -DEFAULT_KEEPALIVE = 60 -DEFAULT_PROTOCOL = PROTOCOL_311 -DEFAULT_TLS_PROTOCOL = "auto" - -DEFAULT_VALUES = { - CONF_BIRTH_MESSAGE: DEFAULT_BIRTH, - CONF_DISCOVERY: DEFAULT_DISCOVERY, - CONF_PORT: DEFAULT_PORT, - CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL, - CONF_WILL_MESSAGE: DEFAULT_WILL, -} - -CLIENT_KEY_AUTH_MSG = ( - "client_key and client_cert must both be present in " - "the MQTT broker configuration" -) - -MQTT_WILL_BIRTH_SCHEMA = vol.Schema( - { - vol.Inclusive(ATTR_TOPIC, "topic_payload"): valid_publish_topic, - vol.Inclusive(ATTR_PAYLOAD, "topic_payload"): cv.string, - vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, - vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - }, - required=True, -) - -PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( - {vol.Optional(platform.value): cv.ensure_list for platform in PLATFORMS} -) - -CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend( - { - vol.Optional(CONF_CLIENT_ID): cv.string, - vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( - vol.Coerce(int), vol.Range(min=15) - ), - vol.Optional(CONF_BROKER): cv.string, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), - vol.Inclusive( - CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG - ): cv.isfile, - vol.Inclusive( - CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG - ): cv.isfile, - vol.Optional(CONF_TLS_INSECURE): cv.boolean, - vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"), - vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( - cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) - ), - vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_DISCOVERY): cv.boolean, - # discovery_prefix must be a valid publish topic because if no - # state topic is specified, it will be created with the given prefix. - vol.Optional( - CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX - ): valid_publish_topic, - } -) - -DEPRECATED_CONFIG_KEYS = [ - CONF_BIRTH_MESSAGE, - CONF_BROKER, - CONF_DISCOVERY, - CONF_PASSWORD, - CONF_PORT, - CONF_TLS_VERSION, - CONF_USERNAME, - CONF_WILL_MESSAGE, -] - SCHEMA_BASE = { vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, diff --git a/homeassistant/components/mqtt/config_integration.py b/homeassistant/components/mqtt/config_integration.py new file mode 100644 index 00000000000..ab685a63802 --- /dev/null +++ b/homeassistant/components/mqtt/config_integration.py @@ -0,0 +1,193 @@ +"""Support for MQTT platform config setup.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_DISCOVERY, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, + Platform, +) +from homeassistant.helpers import config_validation as cv + +from . import ( + alarm_control_panel as alarm_control_panel_platform, + binary_sensor as binary_sensor_platform, + button as button_platform, + camera as camera_platform, + climate as climate_platform, + cover as cover_platform, + device_tracker as device_tracker_platform, + fan as fan_platform, + humidifier as humidifier_platform, + light as light_platform, + lock as lock_platform, + number as number_platform, + scene as scene_platform, + select as select_platform, + sensor as sensor_platform, + siren as siren_platform, + switch as switch_platform, + vacuum as vacuum_platform, +) +from .const import ( + ATTR_PAYLOAD, + ATTR_QOS, + ATTR_RETAIN, + ATTR_TOPIC, + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_CERTIFICATE, + CONF_CLIENT_CERT, + CONF_CLIENT_KEY, + CONF_DISCOVERY_PREFIX, + CONF_KEEPALIVE, + CONF_TLS_INSECURE, + CONF_TLS_VERSION, + CONF_WILL_MESSAGE, + DEFAULT_BIRTH, + DEFAULT_DISCOVERY, + DEFAULT_PREFIX, + DEFAULT_QOS, + DEFAULT_RETAIN, + DEFAULT_WILL, + PROTOCOL_31, + PROTOCOL_311, +) +from .util import _VALID_QOS_SCHEMA, valid_publish_topic + +DEFAULT_PORT = 1883 +DEFAULT_KEEPALIVE = 60 +DEFAULT_PROTOCOL = PROTOCOL_311 +DEFAULT_TLS_PROTOCOL = "auto" + +DEFAULT_VALUES = { + CONF_BIRTH_MESSAGE: DEFAULT_BIRTH, + CONF_DISCOVERY: DEFAULT_DISCOVERY, + CONF_PORT: DEFAULT_PORT, + CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL, + CONF_WILL_MESSAGE: DEFAULT_WILL, +} + +PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( + { + Platform.ALARM_CONTROL_PANEL.value: vol.All( + cv.ensure_list, [alarm_control_panel_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.BINARY_SENSOR.value: vol.All( + cv.ensure_list, [binary_sensor_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.BUTTON.value: vol.All( + cv.ensure_list, [button_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.CAMERA.value: vol.All( + cv.ensure_list, [camera_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.CLIMATE.value: vol.All( + cv.ensure_list, [climate_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.COVER.value: vol.All( + cv.ensure_list, [cover_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.DEVICE_TRACKER.value: vol.All( + cv.ensure_list, [device_tracker_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.FAN.value: vol.All( + cv.ensure_list, [fan_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.HUMIDIFIER.value: vol.All( + cv.ensure_list, [humidifier_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.LOCK.value: vol.All( + cv.ensure_list, [lock_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.LIGHT.value: vol.All( + cv.ensure_list, [light_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.NUMBER.value: vol.All( + cv.ensure_list, [number_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.SCENE.value: vol.All( + cv.ensure_list, [scene_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.SELECT.value: vol.All( + cv.ensure_list, [select_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.SENSOR.value: vol.All( + cv.ensure_list, [sensor_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.SIREN.value: vol.All( + cv.ensure_list, [siren_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.SWITCH.value: vol.All( + cv.ensure_list, [switch_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.VACUUM.value: vol.All( + cv.ensure_list, [vacuum_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + } +) + + +CLIENT_KEY_AUTH_MSG = ( + "client_key and client_cert must both be present in " + "the MQTT broker configuration" +) + +MQTT_WILL_BIRTH_SCHEMA = vol.Schema( + { + vol.Inclusive(ATTR_TOPIC, "topic_payload"): valid_publish_topic, + vol.Inclusive(ATTR_PAYLOAD, "topic_payload"): cv.string, + vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + }, + required=True, +) + +CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend( + { + vol.Optional(CONF_CLIENT_ID): cv.string, + vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( + vol.Coerce(int), vol.Range(min=15) + ), + vol.Optional(CONF_BROKER): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), + vol.Inclusive( + CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG + ): cv.isfile, + vol.Inclusive( + CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG + ): cv.isfile, + vol.Optional(CONF_TLS_INSECURE): cv.boolean, + vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"), + vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( + cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) + ), + vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_DISCOVERY): cv.boolean, + # discovery_prefix must be a valid publish topic because if no + # state topic is specified, it will be created with the given prefix. + vol.Optional( + CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX + ): valid_publish_topic, + } +) + +DEPRECATED_CONFIG_KEYS = [ + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_DISCOVERY, + CONF_PASSWORD, + CONF_PORT, + CONF_TLS_VERSION, + CONF_USERNAME, + CONF_WILL_MESSAGE, +] diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 8d4df0c301d..0901a4f63a6 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -241,7 +241,7 @@ async def async_setup_entry( """Set up MQTT cover through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery(hass, cover.DOMAIN, PLATFORM_SCHEMA_MODERN) + await async_setup_platform_discovery(hass, cover.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/device_tracker/__init__.py b/homeassistant/components/mqtt/device_tracker/__init__.py index bcd5bbd4ee1..1b6c2b25ff3 100644 --- a/homeassistant/components/mqtt/device_tracker/__init__.py +++ b/homeassistant/components/mqtt/device_tracker/__init__.py @@ -4,6 +4,7 @@ import voluptuous as vol from homeassistant.components import device_tracker from ..mixins import warn_for_legacy_schema +from .schema_discovery import PLATFORM_SCHEMA_MODERN # noqa: F401 from .schema_discovery import async_setup_entry_from_discovery from .schema_yaml import PLATFORM_SCHEMA_YAML, async_setup_scanner_from_yaml diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 1b48e15b80e..1ba540c8243 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -54,7 +54,7 @@ async def async_setup_entry_from_discovery(hass, config_entry, async_add_entitie *( _async_setup_entity(hass, async_add_entities, config, config_entry) for config in await async_get_platform_config_from_yaml( - hass, device_tracker.DOMAIN, PLATFORM_SCHEMA_MODERN + hass, device_tracker.DOMAIN ) ) ) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index d0b4ff10692..4e1d1465ac2 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -231,9 +231,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, fan.DOMAIN, PLATFORM_SCHEMA_MODERN) - ) + config_entry.async_on_unload(await async_setup_platform_discovery(hass, fan.DOMAIN)) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 1c9ec5dc201..d2856767cf0 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -188,9 +188,7 @@ async def async_setup_entry( """Set up MQTT humidifier through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, humidifier.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, humidifier.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 158ea6ffa0d..d4914cb9506 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -112,7 +112,7 @@ async def async_setup_entry( """Set up MQTT lights configured under the light platform key (deprecated).""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery(hass, light.DOMAIN, PLATFORM_SCHEMA_MODERN) + await async_setup_platform_discovery(hass, light.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 0bdb66ab48b..8cf65485a09 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -104,7 +104,7 @@ async def async_setup_entry( """Set up MQTT lock through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery(hass, lock.DOMAIN, PLATFORM_SCHEMA_MODERN) + await async_setup_platform_discovery(hass, lock.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index e768c2ff409..dcf387eb360 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -10,7 +10,6 @@ from typing import Any, Protocol, cast, final import voluptuous as vol -from homeassistant.config import async_log_exception from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CONFIGURATION_URL, @@ -263,7 +262,7 @@ class SetupEntity(Protocol): async def async_setup_platform_discovery( - hass: HomeAssistant, platform_domain: str, schema: vol.Schema + hass: HomeAssistant, platform_domain: str ) -> CALLBACK_TYPE: """Set up platform discovery for manual config.""" @@ -282,7 +281,7 @@ async def async_setup_platform_discovery( *( discovery.async_load_platform(hass, platform_domain, DOMAIN, config, {}) for config in await async_get_platform_config_from_yaml( - hass, platform_domain, schema, config_yaml + hass, platform_domain, config_yaml ) ) ) @@ -295,32 +294,17 @@ async def async_setup_platform_discovery( async def async_get_platform_config_from_yaml( hass: HomeAssistant, platform_domain: str, - schema: vol.Schema, config_yaml: ConfigType = None, ) -> list[ConfigType]: """Return a list of validated configurations for the domain.""" - def async_validate_config( - hass: HomeAssistant, - config: list[ConfigType], - ) -> list[ConfigType]: - """Validate config.""" - validated_config = [] - for config_item in config: - try: - validated_config.append(schema(config_item)) - except vol.MultipleInvalid as err: - async_log_exception(err, platform_domain, config_item, hass) - - return validated_config - if config_yaml is None: config_yaml = hass.data.get(DATA_MQTT_CONFIG) if not config_yaml: return [] if not (platform_configs := config_yaml.get(platform_domain)): return [] - return async_validate_config(hass, platform_configs) + return platform_configs async def async_setup_entry_helper(hass, domain, async_setup, schema): diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index bbc78ae07db..660ffe987f0 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -136,9 +136,7 @@ async def async_setup_entry( """Set up MQTT number through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, number.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, number.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 9c4a212bd8e..cc911cc3431 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -80,7 +80,7 @@ async def async_setup_entry( """Set up MQTT scene through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery(hass, scene.DOMAIN, PLATFORM_SCHEMA_MODERN) + await async_setup_platform_discovery(hass, scene.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 994c11653b7..0d9f1411fd1 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -95,9 +95,7 @@ async def async_setup_entry( """Set up MQTT select through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, select.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, select.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index f9e0b5151bb..672e22f632f 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -148,9 +148,7 @@ async def async_setup_entry( """Set up MQTT sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, sensor.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, sensor.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index fef2a4fb3dd..e7b91274f4f 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -144,7 +144,7 @@ async def async_setup_entry( """Set up MQTT siren through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery(hass, siren.DOMAIN, PLATFORM_SCHEMA_MODERN) + await async_setup_platform_discovery(hass, siren.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index be7fc655e1e..dadd5f86f20 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -98,9 +98,7 @@ async def async_setup_entry( """Set up MQTT switch through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, switch.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, switch.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 206a15a024a..694e9530939 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -92,9 +92,7 @@ async def async_setup_entry( """Set up MQTT vacuum through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, vacuum.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, vacuum.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index e1e757762df..0301e9e0481 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -13,6 +13,7 @@ from homeassistant.components.humidifier import ( SERVICE_SET_HUMIDITY, SERVICE_SET_MODE, ) +from homeassistant.components.mqtt import CONFIG_SCHEMA from homeassistant.components.mqtt.humidifier import ( CONF_MODE_COMMAND_TOPIC, CONF_MODE_STATE_TOPIC, @@ -1283,3 +1284,15 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_config_schema_validation(hass): + """Test invalid platform options in the config schema do pass the config validation.""" + platform = humidifier.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + CONFIG_SCHEMA({DOMAIN: {platform: config}}) + CONFIG_SCHEMA({DOMAIN: {platform: [config]}}) + with pytest.raises(MultipleInvalid): + CONFIG_SCHEMA({"mqtt": {"humidifier": [{"bla": "bla"}]}}) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index b75def64834..a29f1fd88ef 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -14,7 +14,7 @@ import yaml from homeassistant import config as hass_config from homeassistant.components import mqtt -from homeassistant.components.mqtt import debug_info +from homeassistant.components.mqtt import CONFIG_SCHEMA, debug_info from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA from homeassistant.components.mqtt.models import ReceiveMessage from homeassistant.const import ( @@ -1373,40 +1373,47 @@ async def test_setup_override_configuration(hass, caplog, tmp_path): assert calls_username_password_set[0][1] == "somepassword" -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) +@patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_setup_manual_mqtt_with_platform_key(hass, caplog): """Test set up a manual MQTT item with a platform key.""" config = {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} - await help_test_setup_manual_entity_from_yaml(hass, "light", config) + with pytest.raises(AssertionError): + await help_test_setup_manual_entity_from_yaml(hass, "light", config) assert ( - "Invalid config for [light]: [platform] is an invalid option for [light]. " - "Check: light->platform. (See ?, line ?)" in caplog.text + "Invalid config for [mqtt]: [platform] is an invalid option for [mqtt]" + in caplog.text ) -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) +@patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_setup_manual_mqtt_with_invalid_config(hass, caplog): """Test set up a manual MQTT item with an invalid config.""" config = {"name": "test"} - await help_test_setup_manual_entity_from_yaml(hass, "light", config) + with pytest.raises(AssertionError): + await help_test_setup_manual_entity_from_yaml(hass, "light", config) assert ( - "Invalid config for [light]: required key not provided @ data['command_topic']." + "Invalid config for [mqtt]: required key not provided @ data['mqtt']['light'][0]['command_topic']." " Got None. (See ?, line ?)" in caplog.text ) +@patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_setup_manual_mqtt_empty_platform(hass, caplog): """Test set up a manual MQTT platform without items.""" - config = None + config = [] await help_test_setup_manual_entity_from_yaml(hass, "light", config) assert "voluptuous.error.MultipleInvalid" not in caplog.text +@patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_setup_mqtt_client_protocol(hass): """Test MQTT client protocol setup.""" entry = MockConfigEntry( domain=mqtt.DOMAIN, - data={mqtt.CONF_BROKER: "test-broker", mqtt.config.CONF_PROTOCOL: "3.1"}, + data={ + mqtt.CONF_BROKER: "test-broker", + mqtt.config_integration.CONF_PROTOCOL: "3.1", + }, ) with patch("paho.mqtt.client.Client") as mock_client: mock_client.on_connect(return_value=0) @@ -2612,3 +2619,10 @@ async def test_one_deprecation_warning_per_platform( ): count += 1 assert count == 1 + + +async def test_config_schema_validation(hass): + """Test invalid platform options in the config schema do not pass the config validation.""" + config = {"mqtt": {"sensor": [{"some_illegal_topic": "mystate/topic/path"}]}} + with pytest.raises(vol.MultipleInvalid): + CONFIG_SCHEMA(config) From 2c936addd2470e7e6c67e6d78500fcb3babe0ec8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 Jun 2022 08:52:37 +0200 Subject: [PATCH 1589/3516] Fix handling of illegal dates in onvif sensor (#73600) * Fix handling of illegal dates in onvif sensor * Address review comment * Address review comment --- homeassistant/components/onvif/parsers.py | 38 ++++++++++++++--------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/onvif/parsers.py b/homeassistant/components/onvif/parsers.py index 5141f25cbef..87b901d2c52 100644 --- a/homeassistant/components/onvif/parsers.py +++ b/homeassistant/components/onvif/parsers.py @@ -1,4 +1,6 @@ """ONVIF event parsers.""" +from __future__ import annotations + from collections.abc import Callable, Coroutine import datetime from typing import Any @@ -12,16 +14,16 @@ from .models import Event PARSERS: Registry[str, Callable[[str, Any], Coroutine[Any, Any, Event]]] = Registry() -def datetime_or_zero(value: str) -> datetime: - """Convert strings to datetimes, if invalid, return datetime.min.""" +def local_datetime_or_none(value: str) -> datetime.datetime | None: + """Convert strings to datetimes, if invalid, return None.""" # To handle cameras that return times like '0000-00-00T00:00:00Z' (e.g. hikvision) try: ret = dt_util.parse_datetime(value) except ValueError: - return datetime.datetime.min - if ret is None: - return datetime.datetime.min - return ret + return None + if ret is not None: + return dt_util.as_local(ret) + return None @PARSERS.register("tns1:VideoSource/MotionAlarm") @@ -394,14 +396,16 @@ async def async_parse_last_reboot(uid: str, msg) -> Event: Topic: tns1:Monitoring/OperatingTime/LastReboot """ try: - date_time = datetime_or_zero(msg.Message._value_1.Data.SimpleItem[0].Value) + date_time = local_datetime_or_none( + msg.Message._value_1.Data.SimpleItem[0].Value + ) return Event( f"{uid}_{msg.Topic._value_1}", "Last Reboot", "sensor", "timestamp", None, - dt_util.as_local(date_time), + date_time, EntityCategory.DIAGNOSTIC, ) except (AttributeError, KeyError): @@ -416,14 +420,16 @@ async def async_parse_last_reset(uid: str, msg) -> Event: Topic: tns1:Monitoring/OperatingTime/LastReset """ try: - date_time = datetime_or_zero(msg.Message._value_1.Data.SimpleItem[0].Value) + date_time = local_datetime_or_none( + msg.Message._value_1.Data.SimpleItem[0].Value + ) return Event( f"{uid}_{msg.Topic._value_1}", "Last Reset", "sensor", "timestamp", None, - dt_util.as_local(date_time), + date_time, EntityCategory.DIAGNOSTIC, entity_enabled=False, ) @@ -440,14 +446,16 @@ async def async_parse_backup_last(uid: str, msg) -> Event: """ try: - date_time = datetime_or_zero(msg.Message._value_1.Data.SimpleItem[0].Value) + date_time = local_datetime_or_none( + msg.Message._value_1.Data.SimpleItem[0].Value + ) return Event( f"{uid}_{msg.Topic._value_1}", "Last Backup", "sensor", "timestamp", None, - dt_util.as_local(date_time), + date_time, EntityCategory.DIAGNOSTIC, entity_enabled=False, ) @@ -463,14 +471,16 @@ async def async_parse_last_clock_sync(uid: str, msg) -> Event: Topic: tns1:Monitoring/OperatingTime/LastClockSynchronization """ try: - date_time = datetime_or_zero(msg.Message._value_1.Data.SimpleItem[0].Value) + date_time = local_datetime_or_none( + msg.Message._value_1.Data.SimpleItem[0].Value + ) return Event( f"{uid}_{msg.Topic._value_1}", "Last Clock Synchronization", "sensor", "timestamp", None, - dt_util.as_local(date_time), + date_time, EntityCategory.DIAGNOSTIC, entity_enabled=False, ) From cf000fae1be1ab3b0bf48793106f44c6dba5c84d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 20 Jun 2022 02:04:54 -0500 Subject: [PATCH 1590/3516] Remove self from tplink codeowners (#73723) --- CODEOWNERS | 4 ++-- homeassistant/components/tplink/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 183430eb646..0a1a33794fc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1068,8 +1068,8 @@ build.json @home-assistant/supervisor /tests/components/tomorrowio/ @raman325 @lymanepp /homeassistant/components/totalconnect/ @austinmroczek /tests/components/totalconnect/ @austinmroczek -/homeassistant/components/tplink/ @rytilahti @thegardenmonkey @bdraco -/tests/components/tplink/ @rytilahti @thegardenmonkey @bdraco +/homeassistant/components/tplink/ @rytilahti @thegardenmonkey +/tests/components/tplink/ @rytilahti @thegardenmonkey /homeassistant/components/traccar/ @ludeeus /tests/components/traccar/ @ludeeus /homeassistant/components/trace/ @home-assistant/core diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index 9a419302a18..e9cc687cc02 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tplink", "requirements": ["python-kasa==0.5.0"], - "codeowners": ["@rytilahti", "@thegardenmonkey", "@bdraco"], + "codeowners": ["@rytilahti", "@thegardenmonkey"], "dependencies": ["network"], "quality_scale": "platinum", "iot_class": "local_polling", From 1f4add0119d12d2ada35d7917de5a8ac3fab9c46 Mon Sep 17 00:00:00 2001 From: Max Gashkov Date: Mon, 20 Jun 2022 16:05:28 +0900 Subject: [PATCH 1591/3516] Fix AmbiClimate services definition (#73668) --- homeassistant/components/ambiclimate/services.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ambiclimate/services.yaml b/homeassistant/components/ambiclimate/services.yaml index f75857e4d2e..e5532ae82f9 100644 --- a/homeassistant/components/ambiclimate/services.yaml +++ b/homeassistant/components/ambiclimate/services.yaml @@ -5,7 +5,7 @@ set_comfort_mode: description: > Enable comfort mode on your AC. fields: - Name: + name: description: > String with device name. required: true @@ -18,14 +18,14 @@ send_comfort_feedback: description: > Send feedback for comfort mode. fields: - Name: + name: description: > String with device name. required: true example: Bedroom selector: text: - Value: + value: description: > Send any of the following comfort values: too_hot, too_warm, bit_warm, comfortable, bit_cold, too_cold, freezing required: true @@ -38,14 +38,14 @@ set_temperature_mode: description: > Enable temperature mode on your AC. fields: - Name: + name: description: > String with device name. required: true example: Bedroom selector: text: - Value: + value: description: > Target value in celsius required: true From bd29b91867a24e7ee9fa210bf081050aec1cef50 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Jun 2022 09:08:11 +0200 Subject: [PATCH 1592/3516] Use Mapping for async_step_reauth (a-e) (#72763) * Adjust abode * Adjust airvisual * Adjust aladdin_connect * Adjust ambee * Adjust aussie-broadband * Adjust brunt * Adjust cloudflare * Adjust deconz * Adjust deluge * Adjust devolo_home_control * Adjust efergy * Adjust esphome --- homeassistant/components/abode/config_flow.py | 3 ++- homeassistant/components/airvisual/config_flow.py | 6 ++++-- homeassistant/components/aladdin_connect/config_flow.py | 5 ++--- homeassistant/components/ambee/config_flow.py | 3 ++- homeassistant/components/aussie_broadband/config_flow.py | 5 +++-- homeassistant/components/brunt/config_flow.py | 5 ++--- homeassistant/components/cloudflare/config_flow.py | 3 ++- homeassistant/components/deconz/config_flow.py | 3 ++- homeassistant/components/deluge/config_flow.py | 3 ++- .../components/devolo_home_control/config_flow.py | 7 ++++--- homeassistant/components/efergy/config_flow.py | 3 ++- homeassistant/components/esphome/config_flow.py | 3 ++- 12 files changed, 29 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py index 715bc53e2b2..1cbab2bdbe4 100644 --- a/homeassistant/components/abode/config_flow.py +++ b/homeassistant/components/abode/config_flow.py @@ -1,6 +1,7 @@ """Config flow for the Abode Security System component.""" from __future__ import annotations +from collections.abc import Mapping from http import HTTPStatus from typing import Any, cast @@ -149,7 +150,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self._async_abode_mfa_login() - async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: """Handle reauthorization request from Abode.""" self._username = config[CONF_USERNAME] diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 85ee4bf6ae5..516b8906092 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -2,6 +2,8 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping +from typing import Any from pyairvisual import CloudAPI, NodeSamba from pyairvisual.errors import ( @@ -70,7 +72,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._entry_data_for_reauth: dict[str, str] = {} + self._entry_data_for_reauth: Mapping[str, Any] = {} self._geo_id: str | None = None @property @@ -219,7 +221,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO}, ) - async def async_step_reauth(self, data: dict[str, str]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._entry_data_for_reauth = data self._geo_id = async_get_geography_id(data) diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index 153a63ffb06..f0f622b3ab7 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Aladdin Connect cover integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -44,9 +45,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 entry: config_entries.ConfigEntry | None - async def async_step_reauth( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle re-authentication with Aladdin Connect.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) diff --git a/homeassistant/components/ambee/config_flow.py b/homeassistant/components/ambee/config_flow.py index 0550c541ed0..6c11f01b759 100644 --- a/homeassistant/components/ambee/config_flow.py +++ b/homeassistant/components/ambee/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure the Ambee integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any from ambee import Ambee, AmbeeAuthenticationError, AmbeeError @@ -71,7 +72,7 @@ class AmbeeFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle initiation of re-authentication with Ambee.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/aussie_broadband/config_flow.py b/homeassistant/components/aussie_broadband/config_flow.py index 6e101250386..37de59d4767 100644 --- a/homeassistant/components/aussie_broadband/config_flow.py +++ b/homeassistant/components/aussie_broadband/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Aussie Broadband integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any from aiohttp import ClientError @@ -76,9 +77,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, user_input: dict[str, str]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle reauth on credential failure.""" - self._reauth_username = user_input[CONF_USERNAME] + self._reauth_username = data[CONF_USERNAME] return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/brunt/config_flow.py b/homeassistant/components/brunt/config_flow.py index c81eb2de6ca..bba58deea45 100644 --- a/homeassistant/components/brunt/config_flow.py +++ b/homeassistant/components/brunt/config_flow.py @@ -1,6 +1,7 @@ """Config flow for brunt integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -77,9 +78,7 @@ class BruntConfigFlow(ConfigFlow, domain=DOMAIN): data=user_input, ) - async def async_step_reauth( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/cloudflare/config_flow.py b/homeassistant/components/cloudflare/config_flow.py index 121e0fc9974..af67cbe8ffc 100644 --- a/homeassistant/components/cloudflare/config_flow.py +++ b/homeassistant/components/cloudflare/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Cloudflare integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -97,7 +98,7 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN): self.zones: list[str] | None = None self.records: list[str] | None = None - async def async_step_reauth(self, data: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle initiation of re-authentication with Cloudflare.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 28205a7382d..6e2a286c168 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping from pprint import pformat from typing import Any, cast from urllib.parse import urlparse @@ -204,7 +205,7 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: """Trigger a reauthentication flow.""" self.context["title_placeholders"] = {CONF_HOST: config[CONF_HOST]} diff --git a/homeassistant/components/deluge/config_flow.py b/homeassistant/components/deluge/config_flow.py index 2f38d4d447d..a4bb1893b7e 100644 --- a/homeassistant/components/deluge/config_flow.py +++ b/homeassistant/components/deluge/config_flow.py @@ -1,6 +1,7 @@ """Config flow for the Deluge integration.""" from __future__ import annotations +from collections.abc import Mapping import socket from ssl import SSLError from typing import Any @@ -75,7 +76,7 @@ class DelugeFlowHandler(ConfigFlow, domain=DOMAIN): ) return self.async_show_form(step_id="user", data_schema=schema, errors=errors) - async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: """Handle a reauthorization flow request.""" return await self.async_step_user() diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py index e0e49197f45..fc1689e0742 100644 --- a/homeassistant/components/devolo_home_control/config_flow.py +++ b/homeassistant/components/devolo_home_control/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure the devolo home control integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any import voluptuous as vol @@ -67,14 +68,14 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="zeroconf_confirm", errors={"base": "invalid_auth"} ) - async def async_step_reauth(self, user_input: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle reauthentication.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] ) - self._url = user_input[CONF_MYDEVOLO] + self._url = data[CONF_MYDEVOLO] self.data_schema = { - vol.Required(CONF_USERNAME, default=user_input[CONF_USERNAME]): str, + vol.Required(CONF_USERNAME, default=data[CONF_USERNAME]): str, vol.Required(CONF_PASSWORD): str, } return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/efergy/config_flow.py b/homeassistant/components/efergy/config_flow.py index 5ff6e9ba9f2..0abf99c2504 100644 --- a/homeassistant/components/efergy/config_flow.py +++ b/homeassistant/components/efergy/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Efergy integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any from pyefergy import Efergy, exceptions @@ -52,7 +53,7 @@ class EfergyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: """Handle a reauthorization flow request.""" return await self.async_step_user() diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index b73743ee950..552d7ed420e 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections import OrderedDict +from collections.abc import Mapping from typing import Any from aioesphomeapi import ( @@ -63,7 +64,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a flow initialized by the user.""" return await self._async_step_user_base(user_input=user_input) - async def async_step_reauth(self, data: dict[str, Any] | None = None) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle a flow initialized by a reauth event.""" entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) assert entry is not None From 4e6d753d2fd9de4e5628012c4fcef4b67dd93180 Mon Sep 17 00:00:00 2001 From: Erwin Oldenkamp Date: Mon, 20 Jun 2022 10:10:10 +0200 Subject: [PATCH 1593/3516] Add support for the locked status but car is connected (#73551) --- homeassistant/components/wallbox/__init__.py | 2 +- homeassistant/components/wallbox/const.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index 332a1ee6741..ae003d84a9c 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -70,7 +70,7 @@ CHARGER_STATUS: dict[int, ChargerStatus] = { 195: ChargerStatus.CHARGING, 196: ChargerStatus.DISCHARGING, 209: ChargerStatus.LOCKED, - 210: ChargerStatus.LOCKED, + 210: ChargerStatus.LOCKED_CAR_CONNECTED, } diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index 6152207427b..0e4e1477911 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -41,6 +41,7 @@ class ChargerStatus(StrEnum): ERROR = "Error" READY = "Ready" LOCKED = "Locked" + LOCKED_CAR_CONNECTED = "Locked, car connected" UPDATING = "Updating" WAITING_IN_QUEUE_POWER_SHARING = "Waiting in queue by Power Sharing" WAITING_IN_QUEUE_POWER_BOOST = "Waiting in queue by Power Boost" From 9680a367c828a7d9b0483214f507b161305ba317 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 Jun 2022 10:26:24 +0200 Subject: [PATCH 1594/3516] Prevent using deprecated number features (#73578) --- homeassistant/components/number/__init__.py | 38 +++++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index f0095e2aecb..fe438ea6aea 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -120,13 +120,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class NumberEntityDescription(EntityDescription): """A class that describes number entities.""" - max_value: float | None = None - min_value: float | None = None + max_value: None = None + min_value: None = None native_max_value: float | None = None native_min_value: float | None = None native_unit_of_measurement: str | None = None native_step: float | None = None - step: float | None = None + step: None = None + unit_of_measurement: None = None # Type override, use native_unit_of_measurement def __post_init__(self) -> None: """Post initialisation processing.""" @@ -136,7 +137,7 @@ class NumberEntityDescription(EntityDescription): or self.step is not None or self.unit_of_measurement is not None ): - if self.__class__.__name__ == "NumberEntityDescription": + if self.__class__.__name__ == "NumberEntityDescription": # type: ignore[unreachable] caller = inspect.stack()[2] module = inspect.getmodule(caller[0]) else: @@ -180,12 +181,12 @@ class NumberEntity(Entity): """Representation of a Number entity.""" entity_description: NumberEntityDescription - _attr_max_value: float - _attr_min_value: float + _attr_max_value: None + _attr_min_value: None _attr_state: None = None - _attr_step: float + _attr_step: None _attr_mode: NumberMode = NumberMode.AUTO - _attr_value: float + _attr_value: None _attr_native_max_value: float _attr_native_min_value: float _attr_native_step: float @@ -248,16 +249,17 @@ class NumberEntity(Entity): return DEFAULT_MIN_VALUE @property + @final def min_value(self) -> float: """Return the minimum value.""" if hasattr(self, "_attr_min_value"): self._report_deprecated_number_entity() - return self._attr_min_value + return self._attr_min_value # type: ignore[return-value] if ( hasattr(self, "entity_description") and self.entity_description.min_value is not None ): - self._report_deprecated_number_entity() + self._report_deprecated_number_entity() # type: ignore[unreachable] return self.entity_description.min_value return self._convert_to_state_value(self.native_min_value, floor_decimal) @@ -274,16 +276,17 @@ class NumberEntity(Entity): return DEFAULT_MAX_VALUE @property + @final def max_value(self) -> float: """Return the maximum value.""" if hasattr(self, "_attr_max_value"): self._report_deprecated_number_entity() - return self._attr_max_value + return self._attr_max_value # type: ignore[return-value] if ( hasattr(self, "entity_description") and self.entity_description.max_value is not None ): - self._report_deprecated_number_entity() + self._report_deprecated_number_entity() # type: ignore[unreachable] return self.entity_description.max_value return self._convert_to_state_value(self.native_max_value, ceil_decimal) @@ -298,16 +301,17 @@ class NumberEntity(Entity): return None @property + @final def step(self) -> float: """Return the increment/decrement step.""" if hasattr(self, "_attr_step"): self._report_deprecated_number_entity() - return self._attr_step + return self._attr_step # type: ignore[return-value] if ( hasattr(self, "entity_description") and self.entity_description.step is not None ): - self._report_deprecated_number_entity() + self._report_deprecated_number_entity() # type: ignore[unreachable] return self.entity_description.step if hasattr(self, "_attr_native_step"): return self._attr_native_step @@ -341,6 +345,7 @@ class NumberEntity(Entity): return None @property + @final def unit_of_measurement(self) -> str | None: """Return the unit of measurement of the entity, after unit conversion.""" if hasattr(self, "_attr_unit_of_measurement"): @@ -349,7 +354,7 @@ class NumberEntity(Entity): hasattr(self, "entity_description") and self.entity_description.unit_of_measurement is not None ): - return self.entity_description.unit_of_measurement + return self.entity_description.unit_of_measurement # type: ignore[unreachable] native_unit_of_measurement = self.native_unit_of_measurement @@ -367,6 +372,7 @@ class NumberEntity(Entity): return self._attr_native_value @property + @final def value(self) -> float | None: """Return the entity value to represent the entity state.""" if hasattr(self, "_attr_value"): @@ -385,10 +391,12 @@ class NumberEntity(Entity): """Set new value.""" await self.hass.async_add_executor_job(self.set_native_value, value) + @final def set_value(self, value: float) -> None: """Set new value.""" raise NotImplementedError() + @final async def async_set_value(self, value: float) -> None: """Set new value.""" await self.hass.async_add_executor_job(self.set_value, value) From cd08f1d0c0b79ebe9ed364ed62dcc0d934036403 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 Jun 2022 10:26:50 +0200 Subject: [PATCH 1595/3516] Don't attempt to reload MQTT device tracker (#73577) --- homeassistant/components/mqtt/__init__.py | 3 ++- homeassistant/components/mqtt/const.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 82b66ddc89e..6fd288a86cf 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -80,6 +80,7 @@ from .const import ( # noqa: F401 MQTT_DISCONNECTED, MQTT_RELOADED, PLATFORMS, + RELOADABLE_PLATFORMS, ) from .models import ( # noqa: F401 MqttCommandTemplate, @@ -380,7 +381,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Setup reload service. Once support for legacy config is removed in 2022.9, we # should no longer call async_setup_reload_service but instead implement a custom # service - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + await async_setup_reload_service(hass, DOMAIN, RELOADABLE_PLATFORMS) async def _async_reload_platforms(_: Event | None) -> None: """Discover entities for a platform.""" diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index b05fd867eeb..67a9208faba 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -92,3 +92,23 @@ PLATFORMS = [ Platform.SWITCH, Platform.VACUUM, ] + +RELOADABLE_PLATFORMS = [ + Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.CAMERA, + Platform.CLIMATE, + Platform.COVER, + Platform.FAN, + Platform.HUMIDIFIER, + Platform.LIGHT, + Platform.LOCK, + Platform.NUMBER, + Platform.SELECT, + Platform.SCENE, + Platform.SENSOR, + Platform.SIREN, + Platform.SWITCH, + Platform.VACUUM, +] From 006ea441ade1311bbb4962e5d5d11285e5aae6ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 20 Jun 2022 03:27:38 -0500 Subject: [PATCH 1596/3516] Pickup emulated_hue codeowner (#73725) - I made some changes to this during this cycle so I want to get notifications for the next release. --- CODEOWNERS | 2 ++ homeassistant/components/emulated_hue/manifest.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 0a1a33794fc..b8e6a4ab7a7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -285,6 +285,8 @@ build.json @home-assistant/supervisor /homeassistant/components/emoncms/ @borpin /homeassistant/components/emonitor/ @bdraco /tests/components/emonitor/ @bdraco +/homeassistant/components/emulated_hue/ @bdraco +/tests/components/emulated_hue/ @bdraco /homeassistant/components/emulated_kasa/ @kbickar /tests/components/emulated_kasa/ @kbickar /homeassistant/components/energy/ @home-assistant/core diff --git a/homeassistant/components/emulated_hue/manifest.json b/homeassistant/components/emulated_hue/manifest.json index e5a9072e51d..be5271b78e3 100644 --- a/homeassistant/components/emulated_hue/manifest.json +++ b/homeassistant/components/emulated_hue/manifest.json @@ -5,7 +5,7 @@ "requirements": ["aiohttp_cors==0.7.0"], "dependencies": ["network"], "after_dependencies": ["http"], - "codeowners": [], + "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push" } From db5e94c93ba9fb97722aeb835468696471bfb0b6 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Mon, 20 Jun 2022 10:30:57 +0200 Subject: [PATCH 1597/3516] Fix HomeWizard is not catching RequestError (#73719) * Fix RequestError was not catched * Add test for RequestError --- .../components/homewizard/config_flow.py | 6 +++- .../components/homewizard/coordinator.py | 5 +++- .../components/homewizard/test_config_flow.py | 30 ++++++++++++++++++- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index 7d06f08ce74..6f164637a7c 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -5,7 +5,7 @@ import logging from typing import Any, cast from homewizard_energy import HomeWizardEnergy -from homewizard_energy.errors import DisabledError, UnsupportedError +from homewizard_energy.errors import DisabledError, RequestError, UnsupportedError from voluptuous import Required, Schema from homeassistant import config_entries @@ -187,6 +187,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.error("API version unsuppored") raise AbortFlow("unsupported_api_version") from ex + except RequestError as ex: + _LOGGER.error("Unexpected or no response") + raise AbortFlow("unknown_error") from ex + except Exception as ex: _LOGGER.exception( "Error connecting with Energy Device at %s", diff --git a/homeassistant/components/homewizard/coordinator.py b/homeassistant/components/homewizard/coordinator.py index e12edda63ae..bab7b5d3ba3 100644 --- a/homeassistant/components/homewizard/coordinator.py +++ b/homeassistant/components/homewizard/coordinator.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from homewizard_energy import HomeWizardEnergy -from homewizard_energy.errors import DisabledError +from homewizard_energy.errors import DisabledError, RequestError from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -41,6 +41,9 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry] "state": await self.api.state(), } + except RequestError as ex: + raise UpdateFailed("Device did not respond as expected") from ex + except DisabledError as ex: raise UpdateFailed("API disabled, API must be enabled in the app") from ex diff --git a/tests/components/homewizard/test_config_flow.py b/tests/components/homewizard/test_config_flow.py index d2e7d4c58ae..fca00b71892 100644 --- a/tests/components/homewizard/test_config_flow.py +++ b/tests/components/homewizard/test_config_flow.py @@ -2,7 +2,7 @@ import logging from unittest.mock import patch -from homewizard_energy.errors import DisabledError, UnsupportedError +from homewizard_energy.errors import DisabledError, RequestError, UnsupportedError from homeassistant import config_entries from homeassistant.components import zeroconf @@ -333,3 +333,31 @@ async def test_check_detects_invalid_api(hass, aioclient_mock): assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "unsupported_api_version" + + +async def test_check_requesterror(hass, aioclient_mock): + """Test check detecting device endpoint failed fetching data due to a requesterror.""" + + def mock_initialize(): + raise RequestError + + device = get_mock_device() + device.device.side_effect = mock_initialize + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", + return_value=device, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown_error" From e0dbf10808a4f6a65a215f588996ea9d0d7d8db7 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 20 Jun 2022 10:31:19 +0200 Subject: [PATCH 1598/3516] Fix CSRF token for UniFi (#73716) Bump aiounifi to v32 --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index e779dca22f0..d481f0d0fc4 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==31"], + "requirements": ["aiounifi==32"], "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 494ad4f27ab..778023377ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -259,7 +259,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==31 +aiounifi==32 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af74cc1329c..a6b7ae5023b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -228,7 +228,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==31 +aiounifi==32 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From d733a0547aec55ffc4c8fdafcc1dc270cbea57d7 Mon Sep 17 00:00:00 2001 From: micha91 Date: Mon, 20 Jun 2022 10:36:04 +0200 Subject: [PATCH 1599/3516] Update aiomusiccast (#73694) --- 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 86115e77988..8c0b55def69 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -3,7 +3,7 @@ "name": "MusicCast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast", - "requirements": ["aiomusiccast==0.14.3"], + "requirements": ["aiomusiccast==0.14.4"], "ssdp": [ { "manufacturer": "Yamaha Corporation" diff --git a/requirements_all.txt b/requirements_all.txt index 778023377ba..e15f3a34c59 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -196,7 +196,7 @@ aiolyric==1.0.8 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.14.3 +aiomusiccast==0.14.4 # homeassistant.components.nanoleaf aionanoleaf==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a6b7ae5023b..5298e306f3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -168,7 +168,7 @@ aiolyric==1.0.8 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.14.3 +aiomusiccast==0.14.4 # homeassistant.components.nanoleaf aionanoleaf==0.2.0 From 8e3d9d7435315ec7f3f01e83274d3e24a7325f82 Mon Sep 17 00:00:00 2001 From: Peter Galantha Date: Mon, 20 Jun 2022 01:45:35 -0700 Subject: [PATCH 1600/3516] Specify device_class and state_class on OpenEVSE sensors (#73672) * Specify device_class and state_class * import SensorStateClass --- homeassistant/components/openevse/sensor.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/openevse/sensor.py b/homeassistant/components/openevse/sensor.py index 9f953832674..3dcea4d0126 100644 --- a/homeassistant/components/openevse/sensor.py +++ b/homeassistant/components/openevse/sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( CONF_HOST, @@ -36,34 +37,43 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="charge_time", name="Charge Time Elapsed", native_unit_of_measurement=TIME_MINUTES, + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="ambient_temp", name="Ambient Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="ir_temp", name="IR Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="rtc_temp", name="RTC Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="usage_session", name="Usage this Session", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="usage_total", name="Total Usage", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), ) From 120479acef9a8e9e52fa356f036e55465e441d31 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 20 Jun 2022 04:10:01 -0500 Subject: [PATCH 1601/3516] Enable polling for hardwired powerview devices (#73659) * Enable polling for hardwired powerview devices * Update homeassistant/components/hunterdouglas_powerview/cover.py * Update homeassistant/components/hunterdouglas_powerview/cover.py * docs were wrong * Update homeassistant/components/hunterdouglas_powerview/cover.py * Update homeassistant/components/hunterdouglas_powerview/sensor.py --- .../hunterdouglas_powerview/const.py | 6 +++ .../hunterdouglas_powerview/cover.py | 41 +++++++++++++++++-- .../hunterdouglas_powerview/entity.py | 10 ++--- .../hunterdouglas_powerview/sensor.py | 4 ++ 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/const.py b/homeassistant/components/hunterdouglas_powerview/const.py index 7146d40c737..65c461b6f2f 100644 --- a/homeassistant/components/hunterdouglas_powerview/const.py +++ b/homeassistant/components/hunterdouglas_powerview/const.py @@ -87,3 +87,9 @@ POS_KIND_PRIMARY = 1 POS_KIND_SECONDARY = 2 POS_KIND_VANE = 3 POS_KIND_ERROR = 4 + + +ATTR_BATTERY_KIND = "batteryKind" +BATTERY_KIND_HARDWIRED = 1 +BATTERY_KIND_BATTERY = 2 +BATTERY_KIND_RECHARGABLE = 3 diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index c3061c75301..e0a01f9c381 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Iterable from contextlib import suppress +from datetime import timedelta import logging from typing import Any @@ -74,6 +75,8 @@ RESYNC_DELAY = 60 # implemented for top/down shades, but also works fine with normal shades CLOSED_POSITION = (0.75 / 100) * (MAX_POSITION - MIN_POSITION) +SCAN_INTERVAL = timedelta(minutes=10) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -152,8 +155,6 @@ def hass_position_to_hd(hass_position: int, max_val: int = MAX_POSITION) -> int: class PowerViewShadeBase(ShadeEntity, CoverEntity): """Representation of a powerview shade.""" - # The hub frequently reports stale states - _attr_assumed_state = True _attr_device_class = CoverDeviceClass.SHADE _attr_supported_features = 0 @@ -174,6 +175,26 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): self._attr_supported_features |= CoverEntityFeature.STOP self._forced_resync = None + @property + def assumed_state(self) -> bool: + """If the device is hard wired we are polling state. + + The hub will frequently provide the wrong state + for battery power devices so we set assumed + state in this case. + """ + return not self._is_hard_wired + + @property + def should_poll(self) -> bool: + """Only poll if the device is hard wired. + + We cannot poll battery powered devices + as it would drain their batteries in a matter + of days. + """ + return self._is_hard_wired + @property def extra_state_attributes(self) -> dict[str, str]: """Return the state attributes.""" @@ -336,15 +357,29 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): """Cancel any pending refreshes.""" self._async_cancel_scheduled_transition_update() + @property + def _update_in_progress(self) -> bool: + """Check if an update is already in progress.""" + return bool(self._scheduled_transition_update or self._forced_resync) + @callback def _async_update_shade_from_group(self) -> None: """Update with new data from the coordinator.""" - if self._scheduled_transition_update or self._forced_resync: + if self._update_in_progress: # If a transition is in progress the data will be wrong return self.data.update_from_group_data(self._shade.id) self.async_write_ha_state() + async def async_update(self) -> None: + """Refresh shade position.""" + if self._update_in_progress: + # The update will likely timeout and + # error if are already have one in flight + return + await self._shade.refresh() + self._async_update_shade_data(self._shade.raw_data) + class PowerViewShade(PowerViewShadeBase): """Represent a standard shade.""" diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py index 7814ba9cb12..222324eb55a 100644 --- a/homeassistant/components/hunterdouglas_powerview/entity.py +++ b/homeassistant/components/hunterdouglas_powerview/entity.py @@ -10,6 +10,8 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( + ATTR_BATTERY_KIND, + BATTERY_KIND_HARDWIRED, DEVICE_FIRMWARE, DEVICE_MAC_ADDRESS, DEVICE_MODEL, @@ -83,6 +85,9 @@ class ShadeEntity(HDEntity): super().__init__(coordinator, device_info, room_name, shade.id) self._shade_name = shade_name self._shade = shade + self._is_hard_wired = bool( + shade.raw_data.get(ATTR_BATTERY_KIND) == BATTERY_KIND_HARDWIRED + ) @property def positions(self) -> PowerviewShadePositions: @@ -117,8 +122,3 @@ class ShadeEntity(HDEntity): device_info[ATTR_SW_VERSION] = sw_version return device_info - - async def async_update(self) -> None: - """Refresh shade position.""" - await self._shade.refresh() - self.data.update_shade_positions(self._shade.raw_data) diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py index 8fd492ddb1d..3fc8942eb78 100644 --- a/homeassistant/components/hunterdouglas_powerview/sensor.py +++ b/homeassistant/components/hunterdouglas_powerview/sensor.py @@ -91,3 +91,7 @@ class PowerViewShadeBatterySensor(ShadeEntity, SensorEntity): """Update with new data from the coordinator.""" self._shade.raw_data = self.data.get_raw_data(self._shade.id) self.async_write_ha_state() + + async def async_update(self) -> None: + """Refresh shade battery.""" + await self._shade.refreshBattery() From 06e45893aab613fbd221028af86d9d01a97c0bcf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Jun 2022 11:28:53 +0200 Subject: [PATCH 1602/3516] Remove invalid type definitions in zha (#73596) --- .../components/zha/alarm_control_panel.py | 9 ++++-- homeassistant/components/zha/api.py | 6 ++-- homeassistant/components/zha/button.py | 24 ++++++++------- .../components/zha/core/channels/base.py | 7 +++-- .../components/zha/core/channels/general.py | 7 +++-- .../zha/core/channels/manufacturerspecific.py | 8 ++++- .../components/zha/core/channels/security.py | 6 +++- .../zha/core/channels/smartenergy.py | 6 +++- homeassistant/components/zha/core/device.py | 9 +++--- homeassistant/components/zha/core/helpers.py | 4 +-- .../components/zha/core/registries.py | 22 +++++++------- homeassistant/components/zha/core/store.py | 15 +++++----- homeassistant/components/zha/core/typing.py | 27 +---------------- homeassistant/components/zha/cover.py | 10 +++++-- homeassistant/components/zha/entity.py | 12 +++++--- homeassistant/components/zha/light.py | 8 +++-- homeassistant/components/zha/select.py | 23 +++++++++------ homeassistant/components/zha/sensor.py | 29 ++++++++++--------- homeassistant/components/zha/siren.py | 11 ++++--- mypy.ini | 18 ------------ script/hassfest/mypy_config.py | 6 ---- 21 files changed, 135 insertions(+), 132 deletions(-) diff --git a/homeassistant/components/zha/alarm_control_panel.py b/homeassistant/components/zha/alarm_control_panel.py index ef616a8f894..15d27f95c5c 100644 --- a/homeassistant/components/zha/alarm_control_panel.py +++ b/homeassistant/components/zha/alarm_control_panel.py @@ -1,5 +1,8 @@ """Alarm control panels on Zigbee Home Automation networks.""" +from __future__ import annotations + import functools +from typing import TYPE_CHECKING from zigpy.zcl.clusters.security import IasAce @@ -38,9 +41,11 @@ from .core.const import ( ) from .core.helpers import async_get_zha_config_value from .core.registries import ZHA_ENTITIES -from .core.typing import ZhaDeviceType from .entity import ZhaEntity +if TYPE_CHECKING: + from .core.device import ZHADevice + STRICT_MATCH = functools.partial( ZHA_ENTITIES.strict_match, Platform.ALARM_CONTROL_PANEL ) @@ -83,7 +88,7 @@ class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity): | AlarmControlPanelEntityFeature.TRIGGER ) - def __init__(self, unique_id, zha_device: ZhaDeviceType, channels, **kwargs): + def __init__(self, unique_id, zha_device: ZHADevice, channels, **kwargs): """Initialize the ZHA alarm control device.""" super().__init__(unique_id, zha_device, channels, **kwargs) cfg_entry = zha_device.gateway.config_entry diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index f99255f55a9..737bef5ddff 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -69,11 +69,11 @@ from .core.helpers import ( get_matched_clusters, qr_to_install_code, ) -from .core.typing import ZhaDeviceType if TYPE_CHECKING: from homeassistant.components.websocket_api.connection import ActiveConnection + from .core.device import ZHADevice from .core.gateway import ZHAGateway _LOGGER = logging.getLogger(__name__) @@ -559,7 +559,7 @@ async def websocket_reconfigure_node( """Reconfigure a ZHA nodes entities by its ieee address.""" zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee: EUI64 = msg[ATTR_IEEE] - device: ZhaDeviceType = zha_gateway.get_device(ieee) + device: ZHADevice = zha_gateway.get_device(ieee) async def forward_messages(data): """Forward events to websocket.""" @@ -1084,7 +1084,7 @@ def async_load_api(hass: HomeAssistant) -> None: """Remove a node from the network.""" zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee: EUI64 = service.data[ATTR_IEEE] - zha_device: ZhaDeviceType = zha_gateway.get_device(ieee) + zha_device: ZHADevice = zha_gateway.get_device(ieee) if zha_device is not None and ( zha_device.is_coordinator and zha_device.ieee == zha_gateway.application_controller.ieee diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py index 9f241795267..ed0836042d2 100644 --- a/homeassistant/components/zha/button.py +++ b/homeassistant/components/zha/button.py @@ -4,7 +4,7 @@ from __future__ import annotations import abc import functools import logging -from typing import Any +from typing import TYPE_CHECKING, Any import zigpy.exceptions from zigpy.zcl.foundation import Status @@ -20,9 +20,13 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .core import discovery from .core.const import CHANNEL_IDENTIFY, DATA_ZHA, SIGNAL_ADD_ENTITIES from .core.registries import ZHA_ENTITIES -from .core.typing import ChannelType, ZhaDeviceType from .entity import ZhaEntity +if TYPE_CHECKING: + from .core.channels.base import ZigbeeChannel + from .core.device import ZHADevice + + MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.BUTTON) CONFIG_DIAGNOSTIC_MATCH = functools.partial( ZHA_ENTITIES.config_diagnostic_match, Platform.BUTTON @@ -60,13 +64,13 @@ class ZHAButton(ZhaEntity, ButtonEntity): def __init__( self, unique_id: str, - zha_device: ZhaDeviceType, - channels: list[ChannelType], + zha_device: ZHADevice, + channels: list[ZigbeeChannel], **kwargs, ) -> None: """Init this button.""" super().__init__(unique_id, zha_device, channels, **kwargs) - self._channel: ChannelType = channels[0] + self._channel: ZigbeeChannel = channels[0] @abc.abstractmethod def get_args(self) -> list[Any]: @@ -87,8 +91,8 @@ class ZHAIdentifyButton(ZHAButton): def create_entity( cls, unique_id: str, - zha_device: ZhaDeviceType, - channels: list[ChannelType], + zha_device: ZHADevice, + channels: list[ZigbeeChannel], **kwargs, ) -> ZhaEntity | None: """Entity Factory. @@ -120,13 +124,13 @@ class ZHAAttributeButton(ZhaEntity, ButtonEntity): def __init__( self, unique_id: str, - zha_device: ZhaDeviceType, - channels: list[ChannelType], + zha_device: ZHADevice, + channels: list[ZigbeeChannel], **kwargs, ) -> None: """Init this button.""" super().__init__(unique_id, zha_device, channels, **kwargs) - self._channel: ChannelType = channels[0] + self._channel: ZigbeeChannel = channels[0] async def async_press(self) -> None: """Write attribute with defined value.""" diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index 7ba28a52116..c5df31f0602 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -5,7 +5,7 @@ import asyncio from enum import Enum from functools import partialmethod, wraps import logging -from typing import Any +from typing import TYPE_CHECKING, Any import zigpy.exceptions from zigpy.zcl.foundation import ( @@ -40,6 +40,9 @@ from ..const import ( ) from ..helpers import LogMixin, retryable_req, safe_read +if TYPE_CHECKING: + from . import ChannelPool + _LOGGER = logging.getLogger(__name__) @@ -105,7 +108,7 @@ class ZigbeeChannel(LogMixin): ZCL_INIT_ATTRS: dict[int | str, bool] = {} def __init__( - self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType + self, cluster: zha_typing.ZigpyClusterType, ch_pool: ChannelPool ) -> None: """Initialize ZigbeeChannel.""" self._generic_id = f"channel_0x{cluster.cluster_id:04x}" diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 8b67c81db44..99c5a81688b 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Coroutine -from typing import Any +from typing import TYPE_CHECKING, Any import zigpy.exceptions import zigpy.types as t @@ -28,6 +28,9 @@ from ..const import ( ) from .base import ClientChannel, ZigbeeChannel, parse_and_log_command +if TYPE_CHECKING: + from . import ChannelPool + @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Alarms.cluster_id) class Alarms(ZigbeeChannel): @@ -305,7 +308,7 @@ class OnOffChannel(ZigbeeChannel): } def __init__( - self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType + self, cluster: zha_typing.ZigpyClusterType, ch_pool: ChannelPool ) -> None: """Initialize OnOffChannel.""" super().__init__(cluster, ch_pool) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 0ec9c7c2f4e..a89c74c54e8 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -1,5 +1,8 @@ """Manufacturer specific channels module for Zigbee Home Automation.""" +from __future__ import annotations + import logging +from typing import TYPE_CHECKING from homeassistant.core import callback @@ -16,6 +19,9 @@ from ..const import ( ) from .base import ClientChannel, ZigbeeChannel +if TYPE_CHECKING: + from . import ChannelPool + _LOGGER = logging.getLogger(__name__) @@ -55,7 +61,7 @@ class OppleRemote(ZigbeeChannel): REPORT_CONFIG = [] def __init__( - self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType + self, cluster: zha_typing.ZigpyClusterType, ch_pool: ChannelPool ) -> None: """Initialize Opple channel.""" super().__init__(cluster, ch_pool) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 0e1e0f8e8a3..510237eee60 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -7,6 +7,7 @@ https://home-assistant.io/integrations/zha/ from __future__ import annotations import asyncio +from typing import TYPE_CHECKING from zigpy.exceptions import ZigbeeException from zigpy.zcl.clusters import security @@ -26,6 +27,9 @@ from ..const import ( from ..typing import CALLABLE_T from .base import ChannelStatus, ZigbeeChannel +if TYPE_CHECKING: + from . import ChannelPool + IAS_ACE_ARM = 0x0000 # ("arm", (t.enum8, t.CharacterString, t.uint8_t), False), IAS_ACE_BYPASS = 0x0001 # ("bypass", (t.LVList(t.uint8_t), t.CharacterString), False), IAS_ACE_EMERGENCY = 0x0002 # ("emergency", (), False), @@ -48,7 +52,7 @@ class IasAce(ZigbeeChannel): """IAS Ancillary Control Equipment channel.""" def __init__( - self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType + self, cluster: zha_typing.ZigpyClusterType, ch_pool: ChannelPool ) -> None: """Initialize IAS Ancillary Control Equipment channel.""" super().__init__(cluster, ch_pool) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 927ceb248c5..d8f148c3dc5 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -3,6 +3,7 @@ from __future__ import annotations import enum from functools import partialmethod +from typing import TYPE_CHECKING from zigpy.zcl.clusters import smartenergy @@ -15,6 +16,9 @@ from ..const import ( ) from .base import ZigbeeChannel +if TYPE_CHECKING: + from . import ChannelPool + @registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Calendar.cluster_id) class Calendar(ZigbeeChannel): @@ -115,7 +119,7 @@ class Metering(ZigbeeChannel): SUMMATION = 1 def __init__( - self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType + self, cluster: zha_typing.ZigpyClusterType, ch_pool: ChannelPool ) -> None: """Initialize Metering.""" super().__init__(cluster, ch_pool) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 6e72c17ef42..009b28b10d5 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -78,6 +78,7 @@ from .helpers import LogMixin, async_get_zha_config_value if TYPE_CHECKING: from ..api import ClusterBinding + from .gateway import ZHAGateway _LOGGER = logging.getLogger(__name__) _UPDATE_ALIVE_INTERVAL = (60, 90) @@ -100,7 +101,7 @@ class ZHADevice(LogMixin): self, hass: HomeAssistant, zigpy_device: zha_typing.ZigpyDeviceType, - zha_gateway: zha_typing.ZhaGatewayType, + zha_gateway: ZHAGateway, ) -> None: """Initialize the gateway.""" self.hass = hass @@ -155,12 +156,12 @@ class ZHADevice(LogMixin): return self._zigpy_device @property - def channels(self) -> zha_typing.ChannelsType: + def channels(self) -> channels.Channels: """Return ZHA channels.""" return self._channels @channels.setter - def channels(self, value: zha_typing.ChannelsType) -> None: + def channels(self, value: channels.Channels) -> None: """Channels setter.""" assert isinstance(value, channels.Channels) self._channels = value @@ -332,7 +333,7 @@ class ZHADevice(LogMixin): cls, hass: HomeAssistant, zigpy_dev: zha_typing.ZigpyDeviceType, - gateway: zha_typing.ZhaGatewayType, + gateway: ZHAGateway, restored: bool = False, ): """Create new device.""" diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 33d68822b9f..1c75846ee7e 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -35,7 +35,7 @@ from .const import ( DATA_ZHA_GATEWAY, ) from .registries import BINDABLE_CLUSTERS -from .typing import ZhaDeviceType, ZigpyClusterType +from .typing import ZigpyClusterType if TYPE_CHECKING: from .device import ZHADevice @@ -82,7 +82,7 @@ async def safe_read( async def get_matched_clusters( - source_zha_device: ZhaDeviceType, target_zha_device: ZhaDeviceType + source_zha_device: ZHADevice, target_zha_device: ZHADevice ) -> list[BindingPair]: """Get matched input/output cluster pairs for 2 devices.""" source_clusters = source_zha_device.async_get_std_clusters() diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index fb00e23ac6f..ed6b047566c 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -17,7 +17,7 @@ from homeassistant.const import Platform # importing channels updates registries from . import channels as zha_channels # noqa: F401 pylint: disable=unused-import from .decorators import DictRegistry, SetRegistry -from .typing import CALLABLE_T, ChannelType +from .typing import CALLABLE_T if TYPE_CHECKING: from .channels.base import ClientChannel, ZigbeeChannel @@ -161,7 +161,7 @@ class MatchRule: weight += 1 * len(self.aux_channels) return weight - def claim_channels(self, channel_pool: list[ChannelType]) -> list[ChannelType]: + def claim_channels(self, channel_pool: list[ZigbeeChannel]) -> list[ZigbeeChannel]: """Return a list of channels this rule matches + aux channels.""" claimed = [] if isinstance(self.channel_names, frozenset): @@ -216,7 +216,7 @@ class EntityClassAndChannels: """Container for entity class and corresponding channels.""" entity_class: CALLABLE_T - claimed_channel: list[ChannelType] + claimed_channel: list[ZigbeeChannel] class ZHAEntityRegistry: @@ -247,9 +247,9 @@ class ZHAEntityRegistry: component: str, manufacturer: str, model: str, - channels: list[ChannelType], + channels: list[ZigbeeChannel], default: CALLABLE_T = None, - ) -> tuple[CALLABLE_T, list[ChannelType]]: + ) -> tuple[CALLABLE_T, list[ZigbeeChannel]]: """Match a ZHA Channels to a ZHA Entity class.""" matches = self._strict_registry[component] for match in sorted(matches, key=lambda x: x.weight, reverse=True): @@ -263,11 +263,11 @@ class ZHAEntityRegistry: self, manufacturer: str, model: str, - channels: list[ChannelType], - ) -> tuple[dict[str, list[EntityClassAndChannels]], list[ChannelType]]: + channels: list[ZigbeeChannel], + ) -> tuple[dict[str, list[EntityClassAndChannels]], list[ZigbeeChannel]]: """Match ZHA Channels to potentially multiple ZHA Entity classes.""" result: dict[str, list[EntityClassAndChannels]] = collections.defaultdict(list) - all_claimed: set[ChannelType] = set() + all_claimed: set[ZigbeeChannel] = set() for component, stop_match_groups in self._multi_entity_registry.items(): for stop_match_grp, matches in stop_match_groups.items(): sorted_matches = sorted(matches, key=lambda x: x.weight, reverse=True) @@ -287,11 +287,11 @@ class ZHAEntityRegistry: self, manufacturer: str, model: str, - channels: list[ChannelType], - ) -> tuple[dict[str, list[EntityClassAndChannels]], list[ChannelType]]: + channels: list[ZigbeeChannel], + ) -> tuple[dict[str, list[EntityClassAndChannels]], list[ZigbeeChannel]]: """Match ZHA Channels to potentially multiple ZHA Entity classes.""" result: dict[str, list[EntityClassAndChannels]] = collections.defaultdict(list) - all_claimed: set[ChannelType] = set() + all_claimed: set[ZigbeeChannel] = set() for ( component, stop_match_groups, diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index 28983bdb427..c82f05303a5 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -5,7 +5,7 @@ from collections import OrderedDict from collections.abc import MutableMapping import datetime import time -from typing import cast +from typing import TYPE_CHECKING, cast import attr @@ -13,7 +13,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.storage import Store from homeassistant.loader import bind_hass -from .typing import ZhaDeviceType +if TYPE_CHECKING: + from .device import ZHADevice DATA_REGISTRY = "zha_storage" @@ -42,7 +43,7 @@ class ZhaStorage: self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) @callback - def async_create_device(self, device: ZhaDeviceType) -> ZhaDeviceEntry: + def async_create_device(self, device: ZHADevice) -> ZhaDeviceEntry: """Create a new ZhaDeviceEntry.""" device_entry: ZhaDeviceEntry = ZhaDeviceEntry( name=device.name, ieee=str(device.ieee), last_seen=device.last_seen @@ -52,7 +53,7 @@ class ZhaStorage: return device_entry @callback - def async_get_or_create_device(self, device: ZhaDeviceType) -> ZhaDeviceEntry: + def async_get_or_create_device(self, device: ZHADevice) -> ZhaDeviceEntry: """Create a new ZhaDeviceEntry.""" ieee_str: str = str(device.ieee) if ieee_str in self.devices: @@ -60,14 +61,14 @@ class ZhaStorage: return self.async_create_device(device) @callback - def async_create_or_update_device(self, device: ZhaDeviceType) -> ZhaDeviceEntry: + def async_create_or_update_device(self, device: ZHADevice) -> ZhaDeviceEntry: """Create or update a ZhaDeviceEntry.""" if str(device.ieee) in self.devices: return self.async_update_device(device) return self.async_create_device(device) @callback - def async_delete_device(self, device: ZhaDeviceType) -> None: + def async_delete_device(self, device: ZHADevice) -> None: """Delete ZhaDeviceEntry.""" ieee_str: str = str(device.ieee) if ieee_str in self.devices: @@ -75,7 +76,7 @@ class ZhaStorage: self.async_schedule_save() @callback - def async_update_device(self, device: ZhaDeviceType) -> ZhaDeviceEntry: + def async_update_device(self, device: ZHADevice) -> ZhaDeviceEntry: """Update name of ZhaDeviceEntry.""" ieee_str: str = str(device.ieee) old = self.devices[ieee_str] diff --git a/homeassistant/components/zha/core/typing.py b/homeassistant/components/zha/core/typing.py index 7e5cce8fec5..4c513ea7f21 100644 --- a/homeassistant/components/zha/core/typing.py +++ b/homeassistant/components/zha/core/typing.py @@ -1,6 +1,6 @@ """Typing helpers for ZHA component.""" from collections.abc import Callable -from typing import TYPE_CHECKING, TypeVar +from typing import TypeVar import zigpy.device import zigpy.endpoint @@ -10,33 +10,8 @@ import zigpy.zdo # pylint: disable=invalid-name CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) -ChannelType = "ZigbeeChannel" -ChannelsType = "Channels" -ChannelPoolType = "ChannelPool" -ClientChannelType = "ClientChannel" -ZDOChannelType = "ZDOChannel" -ZhaDeviceType = "ZHADevice" -ZhaEntityType = "ZHAEntity" -ZhaGatewayType = "ZHAGateway" -ZhaGroupType = "ZHAGroupType" ZigpyClusterType = zigpy.zcl.Cluster ZigpyDeviceType = zigpy.device.Device ZigpyEndpointType = zigpy.endpoint.Endpoint ZigpyGroupType = zigpy.group.Group ZigpyZdoType = zigpy.zdo.ZDO - -if TYPE_CHECKING: - import homeassistant.components.zha.entity - - from . import channels, device, gateway, group - from .channels import base as base_channels - - ChannelType = base_channels.ZigbeeChannel - ChannelsType = channels.Channels - ChannelPoolType = channels.ChannelPool - ClientChannelType = base_channels.ClientChannel - ZDOChannelType = base_channels.ZDOChannel - ZhaDeviceType = device.ZHADevice - ZhaEntityType = homeassistant.components.zha.entity.ZhaEntity - ZhaGatewayType = gateway.ZHAGateway - ZhaGroupType = group.ZHAGroup diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 0fdb4daeaa5..2a61c5b4bc8 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import functools import logging +from typing import TYPE_CHECKING from zigpy.zcl.foundation import Status @@ -37,9 +38,12 @@ from .core.const import ( SIGNAL_SET_LEVEL, ) from .core.registries import ZHA_ENTITIES -from .core.typing import ChannelType, ZhaDeviceType from .entity import ZhaEntity +if TYPE_CHECKING: + from .core.channels.base import ZigbeeChannel + from .core.device import ZHADevice + _LOGGER = logging.getLogger(__name__) MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.COVER) @@ -191,8 +195,8 @@ class Shade(ZhaEntity, CoverEntity): def __init__( self, unique_id: str, - zha_device: ZhaDeviceType, - channels: list[ChannelType], + zha_device: ZHADevice, + channels: list[ZigbeeChannel], **kwargs, ) -> None: """Initialize the ZHA light.""" diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index e9ea9ee871a..88dc9454f37 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -29,7 +29,7 @@ from .core.const import ( SIGNAL_REMOVE, ) from .core.helpers import LogMixin -from .core.typing import CALLABLE_T, ChannelType, ZhaDeviceType +from .core.typing import CALLABLE_T if TYPE_CHECKING: from .core.channels.base import ZigbeeChannel @@ -127,7 +127,11 @@ class BaseZhaEntity(LogMixin, entity.Entity): @callback def async_accept_signal( - self, channel: ChannelType, signal: str, func: CALLABLE_T, signal_override=False + self, + channel: ZigbeeChannel, + signal: str, + func: CALLABLE_T, + signal_override=False, ): """Accept a signal from a channel.""" unsub = None @@ -181,8 +185,8 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): def create_entity( cls, unique_id: str, - zha_device: ZhaDeviceType, - channels: list[ChannelType], + zha_device: ZHADevice, + channels: list[ZigbeeChannel], **kwargs, ) -> ZhaEntity | None: """Entity Factory. diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 30ae9688729..520916d469b 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -7,7 +7,7 @@ import functools import itertools import logging import random -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast from zigpy.zcl.clusters.general import Identify, LevelControl, OnOff from zigpy.zcl.clusters.lighting import Color @@ -62,9 +62,11 @@ from .core.const import ( ) from .core.helpers import LogMixin, async_get_zha_config_value from .core.registries import ZHA_ENTITIES -from .core.typing import ZhaDeviceType from .entity import ZhaEntity, ZhaGroupEntity +if TYPE_CHECKING: + from .core.device import ZHADevice + _LOGGER = logging.getLogger(__name__) CAPABILITIES_COLOR_LOOP = 0x4 @@ -341,7 +343,7 @@ class Light(BaseLight, ZhaEntity): _attr_supported_color_modes: set(ColorMode) _REFRESH_INTERVAL = (45, 75) - def __init__(self, unique_id, zha_device: ZhaDeviceType, channels, **kwargs): + def __init__(self, unique_id, zha_device: ZHADevice, channels, **kwargs): """Initialize the ZHA light.""" super().__init__(unique_id, zha_device, channels, **kwargs) self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 8714d804790..231120ba806 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -4,6 +4,7 @@ from __future__ import annotations from enum import Enum import functools import logging +from typing import TYPE_CHECKING from zigpy import types from zigpy.zcl.clusters.general import OnOff @@ -26,9 +27,13 @@ from .core.const import ( Strobe, ) from .core.registries import ZHA_ENTITIES -from .core.typing import ChannelType, ZhaDeviceType from .entity import ZhaEntity +if TYPE_CHECKING: + from .core.channels.base import ZigbeeChannel + from .core.device import ZHADevice + + CONFIG_DIAGNOSTIC_MATCH = functools.partial( ZHA_ENTITIES.config_diagnostic_match, Platform.SELECT ) @@ -64,14 +69,14 @@ class ZHAEnumSelectEntity(ZhaEntity, SelectEntity): def __init__( self, unique_id: str, - zha_device: ZhaDeviceType, - channels: list[ChannelType], + zha_device: ZHADevice, + channels: list[ZigbeeChannel], **kwargs, ) -> None: """Init this select entity.""" self._attr_name = self._enum.__name__ self._attr_options = [entry.name.replace("_", " ") for entry in self._enum] - self._channel: ChannelType = channels[0] + self._channel: ZigbeeChannel = channels[0] super().__init__(unique_id, zha_device, channels, **kwargs) @property @@ -150,8 +155,8 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity): def create_entity( cls, unique_id: str, - zha_device: ZhaDeviceType, - channels: list[ChannelType], + zha_device: ZHADevice, + channels: list[ZigbeeChannel], **kwargs, ) -> ZhaEntity | None: """Entity Factory. @@ -175,13 +180,13 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity): def __init__( self, unique_id: str, - zha_device: ZhaDeviceType, - channels: list[ChannelType], + zha_device: ZHADevice, + channels: list[ZigbeeChannel], **kwargs, ) -> None: """Init this select entity.""" self._attr_options = [entry.name.replace("_", " ") for entry in self._enum] - self._channel: ChannelType = channels[0] + self._channel: ZigbeeChannel = channels[0] super().__init__(unique_id, zha_device, channels, **kwargs) @property diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index cdc37876889..e579967345c 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import functools import numbers -from typing import Any +from typing import TYPE_CHECKING, Any from homeassistant.components.climate.const import HVACAction from homeassistant.components.sensor import ( @@ -63,9 +63,12 @@ from .core.const import ( SIGNAL_ATTR_UPDATED, ) from .core.registries import SMARTTHINGS_HUMIDITY_CLUSTER, ZHA_ENTITIES -from .core.typing import ChannelType, ZhaDeviceType from .entity import ZhaEntity +if TYPE_CHECKING: + from .core.channels.base import ZigbeeChannel + from .core.device import ZHADevice + PARALLEL_UPDATES = 5 BATTERY_SIZES = { @@ -121,20 +124,20 @@ class Sensor(ZhaEntity, SensorEntity): def __init__( self, unique_id: str, - zha_device: ZhaDeviceType, - channels: list[ChannelType], + zha_device: ZHADevice, + channels: list[ZigbeeChannel], **kwargs, ) -> None: """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) - self._channel: ChannelType = channels[0] + self._channel: ZigbeeChannel = channels[0] @classmethod def create_entity( cls, unique_id: str, - zha_device: ZhaDeviceType, - channels: list[ChannelType], + zha_device: ZHADevice, + channels: list[ZigbeeChannel], **kwargs, ) -> ZhaEntity | None: """Entity Factory. @@ -213,8 +216,8 @@ class Battery(Sensor): def create_entity( cls, unique_id: str, - zha_device: ZhaDeviceType, - channels: list[ChannelType], + zha_device: ZHADevice, + channels: list[ZigbeeChannel], **kwargs, ) -> ZhaEntity | None: """Entity Factory. @@ -637,8 +640,8 @@ class ThermostatHVACAction(Sensor, id_suffix="hvac_action"): def create_entity( cls, unique_id: str, - zha_device: ZhaDeviceType, - channels: list[ChannelType], + zha_device: ZHADevice, + channels: list[ZigbeeChannel], **kwargs, ) -> ZhaEntity | None: """Entity Factory. @@ -762,8 +765,8 @@ class RSSISensor(Sensor, id_suffix="rssi"): def create_entity( cls, unique_id: str, - zha_device: ZhaDeviceType, - channels: list[ChannelType], + zha_device: ZHADevice, + channels: list[ZigbeeChannel], **kwargs, ) -> ZhaEntity | None: """Entity Factory. diff --git a/homeassistant/components/zha/siren.py b/homeassistant/components/zha/siren.py index 38b58b8dc54..b509f9585db 100644 --- a/homeassistant/components/zha/siren.py +++ b/homeassistant/components/zha/siren.py @@ -2,7 +2,7 @@ from __future__ import annotations import functools -from typing import Any +from typing import TYPE_CHECKING, Any from zigpy.zcl.clusters.security import IasWd as WD @@ -38,9 +38,12 @@ from .core.const import ( Strobe, ) from .core.registries import ZHA_ENTITIES -from .core.typing import ChannelType, ZhaDeviceType from .entity import ZhaEntity +if TYPE_CHECKING: + from .core.channels.base import ZigbeeChannel + from .core.device import ZHADevice + MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.SIREN) DEFAULT_DURATION = 5 # seconds @@ -72,8 +75,8 @@ class ZHASiren(ZhaEntity, SirenEntity): def __init__( self, unique_id: str, - zha_device: ZhaDeviceType, - channels: list[ChannelType], + zha_device: ZHADevice, + channels: list[ZigbeeChannel], **kwargs, ) -> None: """Init this siren.""" diff --git a/mypy.ini b/mypy.ini index f19f5f5dc68..dcbb2839b57 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3000,9 +3000,6 @@ ignore_errors = true [mypy-homeassistant.components.xiaomi_miio.switch] ignore_errors = true -[mypy-homeassistant.components.zha.alarm_control_panel] -ignore_errors = true - [mypy-homeassistant.components.zha.api] ignore_errors = true @@ -3018,9 +3015,6 @@ ignore_errors = true [mypy-homeassistant.components.zha.config_flow] ignore_errors = true -[mypy-homeassistant.components.zha.core.channels] -ignore_errors = true - [mypy-homeassistant.components.zha.core.channels.base] ignore_errors = true @@ -3039,27 +3033,18 @@ ignore_errors = true [mypy-homeassistant.components.zha.core.channels.lighting] ignore_errors = true -[mypy-homeassistant.components.zha.core.channels.lightlink] -ignore_errors = true - [mypy-homeassistant.components.zha.core.channels.manufacturerspecific] ignore_errors = true [mypy-homeassistant.components.zha.core.channels.measurement] ignore_errors = true -[mypy-homeassistant.components.zha.core.channels.protocol] -ignore_errors = true - [mypy-homeassistant.components.zha.core.channels.security] ignore_errors = true [mypy-homeassistant.components.zha.core.channels.smartenergy] ignore_errors = true -[mypy-homeassistant.components.zha.core.decorators] -ignore_errors = true - [mypy-homeassistant.components.zha.core.device] ignore_errors = true @@ -3081,9 +3066,6 @@ ignore_errors = true [mypy-homeassistant.components.zha.core.store] ignore_errors = true -[mypy-homeassistant.components.zha.core.typing] -ignore_errors = true - [mypy-homeassistant.components.zha.cover] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 09a9820e7b4..9de6e9c6ad8 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -149,26 +149,21 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.xiaomi_miio.light", "homeassistant.components.xiaomi_miio.sensor", "homeassistant.components.xiaomi_miio.switch", - "homeassistant.components.zha.alarm_control_panel", "homeassistant.components.zha.api", "homeassistant.components.zha.binary_sensor", "homeassistant.components.zha.button", "homeassistant.components.zha.climate", "homeassistant.components.zha.config_flow", - "homeassistant.components.zha.core.channels", "homeassistant.components.zha.core.channels.base", "homeassistant.components.zha.core.channels.closures", "homeassistant.components.zha.core.channels.general", "homeassistant.components.zha.core.channels.homeautomation", "homeassistant.components.zha.core.channels.hvac", "homeassistant.components.zha.core.channels.lighting", - "homeassistant.components.zha.core.channels.lightlink", "homeassistant.components.zha.core.channels.manufacturerspecific", "homeassistant.components.zha.core.channels.measurement", - "homeassistant.components.zha.core.channels.protocol", "homeassistant.components.zha.core.channels.security", "homeassistant.components.zha.core.channels.smartenergy", - "homeassistant.components.zha.core.decorators", "homeassistant.components.zha.core.device", "homeassistant.components.zha.core.discovery", "homeassistant.components.zha.core.gateway", @@ -176,7 +171,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.zha.core.helpers", "homeassistant.components.zha.core.registries", "homeassistant.components.zha.core.store", - "homeassistant.components.zha.core.typing", "homeassistant.components.zha.cover", "homeassistant.components.zha.device_action", "homeassistant.components.zha.device_tracker", From edeb5b9286229e46de39c7507f498f0a7a2d415a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 20 Jun 2022 11:46:58 +0200 Subject: [PATCH 1603/3516] Update spotipy to 2.20.0 (#73731) --- homeassistant/components/spotify/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 979f262e54a..2940700d230 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -2,7 +2,7 @@ "domain": "spotify", "name": "Spotify", "documentation": "https://www.home-assistant.io/integrations/spotify", - "requirements": ["spotipy==2.19.0"], + "requirements": ["spotipy==2.20.0"], "zeroconf": ["_spotify-connect._tcp.local."], "dependencies": ["application_credentials"], "codeowners": ["@frenck"], diff --git a/requirements_all.txt b/requirements_all.txt index e15f3a34c59..83227d65d73 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2213,7 +2213,7 @@ speedtest-cli==2.1.3 spiderpy==1.6.1 # homeassistant.components.spotify -spotipy==2.19.0 +spotipy==2.20.0 # homeassistant.components.recorder # homeassistant.components.sql diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5298e306f3c..7d9da343c0b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1467,7 +1467,7 @@ speedtest-cli==2.1.3 spiderpy==1.6.1 # homeassistant.components.spotify -spotipy==2.19.0 +spotipy==2.20.0 # homeassistant.components.recorder # homeassistant.components.sql From 474e0fd6d041bb9e5842562c3034ad5596741b46 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 20 Jun 2022 12:04:12 +0200 Subject: [PATCH 1604/3516] Use pydeconz interface controls for climate platform (#73670) * Use pydeconz interface controls for climate * Bump pydeconz to make use of enums in more places --- homeassistant/components/deconz/climate.py | 107 +++++++++--------- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 56 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 6887b4238d2..880e11f080b 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -5,25 +5,10 @@ from typing import Any from pydeconz.models.event import EventType from pydeconz.models.sensor.thermostat 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, + ThermostatFanMode, + ThermostatMode, + ThermostatPreset, ) from homeassistant.components.climate import DOMAIN, ClimateEntity @@ -53,21 +38,21 @@ from .gateway import DeconzGateway, get_gateway_from_config_entry DECONZ_FAN_SMART = "smart" FAN_MODE_TO_DECONZ = { - 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_FAN_SMART: ThermostatFanMode.SMART, + FAN_AUTO: ThermostatFanMode.AUTO, + FAN_HIGH: ThermostatFanMode.HIGH, + FAN_MEDIUM: ThermostatFanMode.MEDIUM, + FAN_LOW: ThermostatFanMode.LOW, + FAN_ON: ThermostatFanMode.ON, + FAN_OFF: ThermostatFanMode.OFF, } DECONZ_TO_FAN_MODE = {value: key for key, value in FAN_MODE_TO_DECONZ.items()} -HVAC_MODE_TO_DECONZ: dict[HVACMode, str] = { - HVACMode.AUTO: THERMOSTAT_MODE_AUTO, - HVACMode.COOL: THERMOSTAT_MODE_COOL, - HVACMode.HEAT: THERMOSTAT_MODE_HEAT, - HVACMode.OFF: THERMOSTAT_MODE_OFF, +HVAC_MODE_TO_DECONZ = { + HVACMode.AUTO: ThermostatMode.AUTO, + HVACMode.COOL: ThermostatMode.COOL, + HVACMode.HEAT: ThermostatMode.HEAT, + HVACMode.OFF: ThermostatMode.OFF, } DECONZ_PRESET_AUTO = "auto" @@ -76,13 +61,13 @@ DECONZ_PRESET_HOLIDAY = "holiday" DECONZ_PRESET_MANUAL = "manual" PRESET_MODE_TO_DECONZ = { - 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_PRESET_AUTO: ThermostatPreset.AUTO, + PRESET_BOOST: ThermostatPreset.BOOST, + PRESET_COMFORT: ThermostatPreset.COMFORT, + DECONZ_PRESET_COMPLEX: ThermostatPreset.COMPLEX, + PRESET_ECO: ThermostatPreset.ECO, + DECONZ_PRESET_HOLIDAY: ThermostatPreset.HOLIDAY, + DECONZ_PRESET_MANUAL: ThermostatPreset.MANUAL, } DECONZ_TO_PRESET_MODE = {value: key for key, value in PRESET_MODE_TO_DECONZ.items()} @@ -168,23 +153,23 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): """Return fan operation.""" if self._device.fan_mode in DECONZ_TO_FAN_MODE: return DECONZ_TO_FAN_MODE[self._device.fan_mode] - return DECONZ_TO_FAN_MODE[FAN_ON if self._device.state_on else FAN_OFF] + return FAN_ON if self._device.state_on else FAN_OFF async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" if fan_mode not in FAN_MODE_TO_DECONZ: raise ValueError(f"Unsupported fan mode {fan_mode}") - await self._device.set_config(fan_mode=FAN_MODE_TO_DECONZ[fan_mode]) + await self.gateway.api.sensors.thermostat.set_config( + id=self._device.resource_id, + fan_mode=FAN_MODE_TO_DECONZ[fan_mode], + ) # HVAC control @property def hvac_mode(self) -> HVACMode: - """Return hvac operation ie. heat, cool mode. - - Need to be one of HVAC_MODE_*. - """ + """Return hvac operation ie. heat, cool mode.""" if self._device.mode in self._deconz_to_hvac_mode: return self._deconz_to_hvac_mode[self._device.mode] return HVACMode.HEAT if self._device.state_on else HVACMode.OFF @@ -195,9 +180,15 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): raise ValueError(f"Unsupported HVAC mode {hvac_mode}") if len(self._attr_hvac_modes) == 2: # Only allow turn on and off thermostat - await self._device.set_config(on=hvac_mode != HVACMode.OFF) + await self.gateway.api.sensors.thermostat.set_config( + id=self._device.resource_id, + on=hvac_mode != HVACMode.OFF, + ) else: - await self._device.set_config(mode=HVAC_MODE_TO_DECONZ[hvac_mode]) + await self.gateway.api.sensors.thermostat.set_config( + id=self._device.resource_id, + mode=HVAC_MODE_TO_DECONZ[hvac_mode], + ) # Preset control @@ -213,7 +204,10 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): if preset_mode not in PRESET_MODE_TO_DECONZ: raise ValueError(f"Unsupported preset mode {preset_mode}") - await self._device.set_config(preset=PRESET_MODE_TO_DECONZ[preset_mode]) + await self.gateway.api.sensors.thermostat.set_config( + id=self._device.resource_id, + preset=PRESET_MODE_TO_DECONZ[preset_mode], + ) # Temperature control @@ -225,11 +219,11 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): @property def target_temperature(self) -> float | None: """Return the target temperature.""" - if self._device.mode == THERMOSTAT_MODE_COOL and self._device.cooling_setpoint: - return self._device.cooling_setpoint + if self._device.mode == ThermostatMode.COOL and self._device.cooling_setpoint: + return self._device.scaled_cooling_setpoint if self._device.heating_setpoint: - return self._device.heating_setpoint + return self._device.scaled_heating_setpoint return None @@ -238,11 +232,16 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): if ATTR_TEMPERATURE not in kwargs: raise ValueError(f"Expected attribute {ATTR_TEMPERATURE}") - data = {"heating_setpoint": kwargs[ATTR_TEMPERATURE] * 100} - if self._device.mode == "cool": - data = {"cooling_setpoint": kwargs[ATTR_TEMPERATURE] * 100} - - await self._device.set_config(**data) + if self._device.mode == ThermostatMode.COOL: + await self.gateway.api.sensors.thermostat.set_config( + id=self._device.resource_id, + cooling_setpoint=kwargs[ATTR_TEMPERATURE] * 100, + ) + else: + await self.gateway.api.sensors.thermostat.set_config( + id=self._device.resource_id, + heating_setpoint=kwargs[ATTR_TEMPERATURE] * 100, + ) @property def extra_state_attributes(self) -> dict[str, bool | int]: diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 2a4a5ccf253..2306d088c48 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==92"], + "requirements": ["pydeconz==93"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 83227d65d73..07d2983550d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1441,7 +1441,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==92 +pydeconz==93 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d9da343c0b..c12a0584249 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -968,7 +968,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==92 +pydeconz==93 # homeassistant.components.dexcom pydexcom==0.2.3 From b318b9b1967303fb3aef86a8ca2460594ae86cd2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Jun 2022 12:07:33 +0200 Subject: [PATCH 1605/3516] Improve onvif type hints (#73642) * Remove onvif from mypy ignore list * Adjust parsers * Adjust event * Adjust config_flow --- homeassistant/components/onvif/config_flow.py | 3 +- homeassistant/components/onvif/event.py | 4 +- homeassistant/components/onvif/models.py | 18 ++++---- homeassistant/components/onvif/parsers.py | 42 ++++++++++--------- mypy.ini | 15 ------- script/hassfest/mypy_config.py | 5 --- 6 files changed, 35 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index d7fb46079d9..48e5163ced5 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations from pprint import pformat +from typing import Any from urllib.parse import urlparse from onvif.exceptions import ONVIFError @@ -44,7 +45,7 @@ def wsdiscovery() -> list[Service]: return services -async def async_discovery(hass) -> bool: +async def async_discovery(hass) -> list[dict[str, Any]]: """Return if there are devices that can be discovered.""" LOGGER.debug("Starting ONVIF discovery") services = await hass.async_add_executor_job(wsdiscovery) diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 3b4ae981677..3801d8081db 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -114,7 +114,7 @@ class EventManager: await self._subscription.Unsubscribe() self._subscription = None - async def async_restart(self, _now: dt = None) -> None: + async def async_restart(self, _now: dt.datetime | None = None) -> None: """Restart the subscription assuming the camera rebooted.""" if not self.started: return @@ -159,7 +159,7 @@ class EventManager: """Schedule async_pull_messages to run.""" self._unsub_refresh = async_call_later(self.hass, 1, self.async_pull_messages) - async def async_pull_messages(self, _now: dt = None) -> None: + async def async_pull_messages(self, _now: dt.datetime | None = None) -> None: """Pull messages from device.""" if self.hass.state == CoreState.running: try: diff --git a/homeassistant/components/onvif/models.py b/homeassistant/components/onvif/models.py index dea613e3c1c..6cefa6332e2 100644 --- a/homeassistant/components/onvif/models.py +++ b/homeassistant/components/onvif/models.py @@ -11,11 +11,11 @@ from homeassistant.helpers.entity import EntityCategory class DeviceInfo: """Represent device information.""" - manufacturer: str = None - model: str = None - fw_version: str = None - serial_number: str = None - mac: str = None + manufacturer: str | None = None + model: str | None = None + fw_version: str | None = None + serial_number: str | None = None + mac: str | None = None @dataclass @@ -41,7 +41,7 @@ class PTZ: continuous: bool relative: bool absolute: bool - presets: list[str] = None + presets: list[str] | None = None @dataclass @@ -52,7 +52,7 @@ class Profile: token: str name: str video: Video - ptz: PTZ = None + ptz: PTZ | None = None @dataclass @@ -71,8 +71,8 @@ class Event: uid: str name: str platform: str - device_class: str = None - unit_of_measurement: str = None + device_class: str | None = None + unit_of_measurement: str | None = None value: Any = None entity_category: EntityCategory | None = None entity_enabled: bool = True diff --git a/homeassistant/components/onvif/parsers.py b/homeassistant/components/onvif/parsers.py index 87b901d2c52..2c74f873f77 100644 --- a/homeassistant/components/onvif/parsers.py +++ b/homeassistant/components/onvif/parsers.py @@ -11,7 +11,9 @@ from homeassistant.util.decorator import Registry from .models import Event -PARSERS: Registry[str, Callable[[str, Any], Coroutine[Any, Any, Event]]] = Registry() +PARSERS: Registry[ + str, Callable[[str, Any], Coroutine[Any, Any, Event | None]] +] = Registry() def local_datetime_or_none(value: str) -> datetime.datetime | None: @@ -28,7 +30,7 @@ def local_datetime_or_none(value: str) -> datetime.datetime | None: @PARSERS.register("tns1:VideoSource/MotionAlarm") # pylint: disable=protected-access -async def async_parse_motion_alarm(uid: str, msg) -> Event: +async def async_parse_motion_alarm(uid: str, msg) -> Event | None: """Handle parsing event message. Topic: tns1:VideoSource/MotionAlarm @@ -51,7 +53,7 @@ async def async_parse_motion_alarm(uid: str, msg) -> Event: @PARSERS.register("tns1:VideoSource/ImageTooBlurry/ImagingService") @PARSERS.register("tns1:VideoSource/ImageTooBlurry/RecordingService") # pylint: disable=protected-access -async def async_parse_image_too_blurry(uid: str, msg) -> Event: +async def async_parse_image_too_blurry(uid: str, msg) -> Event | None: """Handle parsing event message. Topic: tns1:VideoSource/ImageTooBlurry/* @@ -75,7 +77,7 @@ async def async_parse_image_too_blurry(uid: str, msg) -> Event: @PARSERS.register("tns1:VideoSource/ImageTooDark/ImagingService") @PARSERS.register("tns1:VideoSource/ImageTooDark/RecordingService") # pylint: disable=protected-access -async def async_parse_image_too_dark(uid: str, msg) -> Event: +async def async_parse_image_too_dark(uid: str, msg) -> Event | None: """Handle parsing event message. Topic: tns1:VideoSource/ImageTooDark/* @@ -99,7 +101,7 @@ async def async_parse_image_too_dark(uid: str, msg) -> Event: @PARSERS.register("tns1:VideoSource/ImageTooBright/ImagingService") @PARSERS.register("tns1:VideoSource/ImageTooBright/RecordingService") # pylint: disable=protected-access -async def async_parse_image_too_bright(uid: str, msg) -> Event: +async def async_parse_image_too_bright(uid: str, msg) -> Event | None: """Handle parsing event message. Topic: tns1:VideoSource/ImageTooBright/* @@ -123,7 +125,7 @@ async def async_parse_image_too_bright(uid: str, msg) -> Event: @PARSERS.register("tns1:VideoSource/GlobalSceneChange/ImagingService") @PARSERS.register("tns1:VideoSource/GlobalSceneChange/RecordingService") # pylint: disable=protected-access -async def async_parse_scene_change(uid: str, msg) -> Event: +async def async_parse_scene_change(uid: str, msg) -> Event | None: """Handle parsing event message. Topic: tns1:VideoSource/GlobalSceneChange/* @@ -144,7 +146,7 @@ async def async_parse_scene_change(uid: str, msg) -> Event: @PARSERS.register("tns1:AudioAnalytics/Audio/DetectedSound") # pylint: disable=protected-access -async def async_parse_detected_sound(uid: str, msg) -> Event: +async def async_parse_detected_sound(uid: str, msg) -> Event | None: """Handle parsing event message. Topic: tns1:AudioAnalytics/Audio/DetectedSound @@ -175,7 +177,7 @@ async def async_parse_detected_sound(uid: str, msg) -> Event: @PARSERS.register("tns1:RuleEngine/FieldDetector/ObjectsInside") # pylint: disable=protected-access -async def async_parse_field_detector(uid: str, msg) -> Event: +async def async_parse_field_detector(uid: str, msg) -> Event | None: """Handle parsing event message. Topic: tns1:RuleEngine/FieldDetector/ObjectsInside @@ -207,7 +209,7 @@ async def async_parse_field_detector(uid: str, msg) -> Event: @PARSERS.register("tns1:RuleEngine/CellMotionDetector/Motion") # pylint: disable=protected-access -async def async_parse_cell_motion_detector(uid: str, msg) -> Event: +async def async_parse_cell_motion_detector(uid: str, msg) -> Event | None: """Handle parsing event message. Topic: tns1:RuleEngine/CellMotionDetector/Motion @@ -238,7 +240,7 @@ async def async_parse_cell_motion_detector(uid: str, msg) -> Event: @PARSERS.register("tns1:RuleEngine/MotionRegionDetector/Motion") # pylint: disable=protected-access -async def async_parse_motion_region_detector(uid: str, msg) -> Event: +async def async_parse_motion_region_detector(uid: str, msg) -> Event | None: """Handle parsing event message. Topic: tns1:RuleEngine/MotionRegionDetector/Motion @@ -269,7 +271,7 @@ async def async_parse_motion_region_detector(uid: str, msg) -> Event: @PARSERS.register("tns1:RuleEngine/TamperDetector/Tamper") # pylint: disable=protected-access -async def async_parse_tamper_detector(uid: str, msg) -> Event: +async def async_parse_tamper_detector(uid: str, msg) -> Event | None: """Handle parsing event message. Topic: tns1:RuleEngine/TamperDetector/Tamper @@ -301,7 +303,7 @@ async def async_parse_tamper_detector(uid: str, msg) -> Event: @PARSERS.register("tns1:Device/Trigger/DigitalInput") # pylint: disable=protected-access -async def async_parse_digital_input(uid: str, msg) -> Event: +async def async_parse_digital_input(uid: str, msg) -> Event | None: """Handle parsing event message. Topic: tns1:Device/Trigger/DigitalInput @@ -322,7 +324,7 @@ async def async_parse_digital_input(uid: str, msg) -> Event: @PARSERS.register("tns1:Device/Trigger/Relay") # pylint: disable=protected-access -async def async_parse_relay(uid: str, msg) -> Event: +async def async_parse_relay(uid: str, msg) -> Event | None: """Handle parsing event message. Topic: tns1:Device/Trigger/Relay @@ -343,7 +345,7 @@ async def async_parse_relay(uid: str, msg) -> Event: @PARSERS.register("tns1:Device/HardwareFailure/StorageFailure") # pylint: disable=protected-access -async def async_parse_storage_failure(uid: str, msg) -> Event: +async def async_parse_storage_failure(uid: str, msg) -> Event | None: """Handle parsing event message. Topic: tns1:Device/HardwareFailure/StorageFailure @@ -365,7 +367,7 @@ async def async_parse_storage_failure(uid: str, msg) -> Event: @PARSERS.register("tns1:Monitoring/ProcessorUsage") # pylint: disable=protected-access -async def async_parse_processor_usage(uid: str, msg) -> Event: +async def async_parse_processor_usage(uid: str, msg) -> Event | None: """Handle parsing event message. Topic: tns1:Monitoring/ProcessorUsage @@ -390,7 +392,7 @@ async def async_parse_processor_usage(uid: str, msg) -> Event: @PARSERS.register("tns1:Monitoring/OperatingTime/LastReboot") # pylint: disable=protected-access -async def async_parse_last_reboot(uid: str, msg) -> Event: +async def async_parse_last_reboot(uid: str, msg) -> Event | None: """Handle parsing event message. Topic: tns1:Monitoring/OperatingTime/LastReboot @@ -414,7 +416,7 @@ async def async_parse_last_reboot(uid: str, msg) -> Event: @PARSERS.register("tns1:Monitoring/OperatingTime/LastReset") # pylint: disable=protected-access -async def async_parse_last_reset(uid: str, msg) -> Event: +async def async_parse_last_reset(uid: str, msg) -> Event | None: """Handle parsing event message. Topic: tns1:Monitoring/OperatingTime/LastReset @@ -439,7 +441,7 @@ async def async_parse_last_reset(uid: str, msg) -> Event: @PARSERS.register("tns1:Monitoring/Backup/Last") # pylint: disable=protected-access -async def async_parse_backup_last(uid: str, msg) -> Event: +async def async_parse_backup_last(uid: str, msg) -> Event | None: """Handle parsing event message. Topic: tns1:Monitoring/Backup/Last @@ -465,7 +467,7 @@ async def async_parse_backup_last(uid: str, msg) -> Event: @PARSERS.register("tns1:Monitoring/OperatingTime/LastClockSynchronization") # pylint: disable=protected-access -async def async_parse_last_clock_sync(uid: str, msg) -> Event: +async def async_parse_last_clock_sync(uid: str, msg) -> Event | None: """Handle parsing event message. Topic: tns1:Monitoring/OperatingTime/LastClockSynchronization @@ -490,7 +492,7 @@ async def async_parse_last_clock_sync(uid: str, msg) -> Event: @PARSERS.register("tns1:RecordingConfig/JobState") # pylint: disable=protected-access -async def async_parse_jobstate(uid: str, msg) -> Event: +async def async_parse_jobstate(uid: str, msg) -> Event | None: """Handle parsing event message. Topic: tns1:RecordingConfig/JobState diff --git a/mypy.ini b/mypy.ini index dcbb2839b57..c4561dea5a8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2814,27 +2814,12 @@ ignore_errors = true [mypy-homeassistant.components.onvif.binary_sensor] ignore_errors = true -[mypy-homeassistant.components.onvif.button] -ignore_errors = true - [mypy-homeassistant.components.onvif.camera] ignore_errors = true -[mypy-homeassistant.components.onvif.config_flow] -ignore_errors = true - [mypy-homeassistant.components.onvif.device] ignore_errors = true -[mypy-homeassistant.components.onvif.event] -ignore_errors = true - -[mypy-homeassistant.components.onvif.models] -ignore_errors = true - -[mypy-homeassistant.components.onvif.parsers] -ignore_errors = true - [mypy-homeassistant.components.onvif.sensor] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 9de6e9c6ad8..6b42bcd2cdd 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -87,13 +87,8 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.omnilogic.switch", "homeassistant.components.onvif.base", "homeassistant.components.onvif.binary_sensor", - "homeassistant.components.onvif.button", "homeassistant.components.onvif.camera", - "homeassistant.components.onvif.config_flow", "homeassistant.components.onvif.device", - "homeassistant.components.onvif.event", - "homeassistant.components.onvif.models", - "homeassistant.components.onvif.parsers", "homeassistant.components.onvif.sensor", "homeassistant.components.philips_js", "homeassistant.components.philips_js.config_flow", From 961c33dcc12619f3868a3372a6b28910df4b8794 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 20 Jun 2022 12:27:11 +0200 Subject: [PATCH 1606/3516] Ditch bluepy wheels (#73732) --- .github/workflows/wheels.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 15aef8b752a..54c2f2594ff 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -245,7 +245,6 @@ jobs: requirement_files="requirements_all.txt requirements_diff.txt" for requirement_file in ${requirement_files}; do sed -i "s|# pybluez|pybluez|g" ${requirement_file} - sed -i "s|# bluepy|bluepy|g" ${requirement_file} sed -i "s|# beacontools|beacontools|g" ${requirement_file} sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file} sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file} @@ -254,7 +253,6 @@ jobs: sed -i "s|# pycups|pycups|g" ${requirement_file} sed -i "s|# homekit|homekit|g" ${requirement_file} sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file} - sed -i "s|# decora|decora|g" ${requirement_file} sed -i "s|# avion|avion|g" ${requirement_file} sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file} sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file} From b145aeaf75c89658566c0aeb142d94b4789cc6f3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 Jun 2022 12:27:25 +0200 Subject: [PATCH 1607/3516] Fix flaky recorder test (#73733) --- tests/components/history/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 8c0a80719a8..56ac68d944d 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -994,7 +994,7 @@ async def test_statistics_during_period_in_the_past( assert response["success"] assert response["result"] == {} - past = now - timedelta(days=3) + past = now - timedelta(days=3, hours=1) await client.send_json( { "id": 3, From 2de4b193e3de269bffdb1d65f84859f552195659 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Jun 2022 12:49:40 +0200 Subject: [PATCH 1608/3516] Remove unnecessary type definitions in zha (#73735) * Cleanup ZigpyClusterType * Cleanup ZigpyDeviceType * Cleanup ZigpyEndpointType * Cleanup ZigpyGroupType * Cleanup ZigpyZdoType --- homeassistant/components/zha/core/channels/base.py | 6 ++---- homeassistant/components/zha/core/channels/general.py | 7 +++---- .../zha/core/channels/manufacturerspecific.py | 8 ++++---- .../components/zha/core/channels/security.py | 7 +++---- .../components/zha/core/channels/smartenergy.py | 7 +++---- homeassistant/components/zha/core/device.py | 9 +++++---- homeassistant/components/zha/core/helpers.py | 4 ++-- homeassistant/components/zha/core/typing.py | 11 ----------- 8 files changed, 22 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index c5df31f0602..9b3ad6d9572 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -8,6 +8,7 @@ import logging from typing import TYPE_CHECKING, Any import zigpy.exceptions +import zigpy.zcl from zigpy.zcl.foundation import ( CommandSchema, ConfigureReportingResponseRecord, @@ -19,7 +20,6 @@ from homeassistant.const import ATTR_COMMAND from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from .. import typing as zha_typing from ..const import ( ATTR_ARGS, ATTR_ATTRIBUTE_ID, @@ -107,9 +107,7 @@ class ZigbeeChannel(LogMixin): # attribute read is acceptable. ZCL_INIT_ATTRS: dict[int | str, bool] = {} - def __init__( - self, cluster: zha_typing.ZigpyClusterType, ch_pool: ChannelPool - ) -> None: + def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: """Initialize ZigbeeChannel.""" self._generic_id = f"channel_0x{cluster.cluster_id:04x}" self._ch_pool = ch_pool diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 99c5a81688b..aa292013081 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -7,13 +7,14 @@ from typing import TYPE_CHECKING, Any import zigpy.exceptions import zigpy.types as t +import zigpy.zcl from zigpy.zcl.clusters import general from zigpy.zcl.foundation import Status from homeassistant.core import callback from homeassistant.helpers.event import async_call_later -from .. import registries, typing as zha_typing +from .. import registries from ..const import ( REPORT_CONFIG_ASAP, REPORT_CONFIG_BATTERY_SAVE, @@ -307,9 +308,7 @@ class OnOffChannel(ZigbeeChannel): "start_up_on_off": True, } - def __init__( - self, cluster: zha_typing.ZigpyClusterType, ch_pool: ChannelPool - ) -> None: + def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: """Initialize OnOffChannel.""" super().__init__(cluster, ch_pool) self._off_listener = None diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index a89c74c54e8..144d5736526 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -4,9 +4,11 @@ from __future__ import annotations import logging from typing import TYPE_CHECKING +import zigpy.zcl + from homeassistant.core import callback -from .. import registries, typing as zha_typing +from .. import registries from ..const import ( ATTR_ATTRIBUTE_ID, ATTR_ATTRIBUTE_NAME, @@ -60,9 +62,7 @@ class OppleRemote(ZigbeeChannel): REPORT_CONFIG = [] - def __init__( - self, cluster: zha_typing.ZigpyClusterType, ch_pool: ChannelPool - ) -> None: + def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: """Initialize Opple channel.""" super().__init__(cluster, ch_pool) if self.cluster.endpoint.model == "lumi.motion.ac02": diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 510237eee60..4c0d6bbfd59 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -10,12 +10,13 @@ import asyncio from typing import TYPE_CHECKING from zigpy.exceptions import ZigbeeException +import zigpy.zcl from zigpy.zcl.clusters import security from zigpy.zcl.clusters.security import IasAce as AceCluster from homeassistant.core import callback -from .. import registries, typing as zha_typing +from .. import registries from ..const import ( SIGNAL_ATTR_UPDATED, WARNING_DEVICE_MODE_EMERGENCY, @@ -51,9 +52,7 @@ SIGNAL_ALARM_TRIGGERED = "zha_armed_triggered" class IasAce(ZigbeeChannel): """IAS Ancillary Control Equipment channel.""" - def __init__( - self, cluster: zha_typing.ZigpyClusterType, ch_pool: ChannelPool - ) -> None: + def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: """Initialize IAS Ancillary Control Equipment channel.""" super().__init__(cluster, ch_pool) self.command_map: dict[int, CALLABLE_T] = { diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index d8f148c3dc5..099571aa69e 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -5,9 +5,10 @@ import enum from functools import partialmethod from typing import TYPE_CHECKING +import zigpy.zcl from zigpy.zcl.clusters import smartenergy -from .. import registries, typing as zha_typing +from .. import registries from ..const import ( REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, @@ -118,9 +119,7 @@ class Metering(ZigbeeChannel): DEMAND = 0 SUMMATION = 1 - def __init__( - self, cluster: zha_typing.ZigpyClusterType, ch_pool: ChannelPool - ) -> None: + def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: """Initialize Metering.""" super().__init__(cluster, ch_pool) self._format_spec = None diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 009b28b10d5..588fcac7ca6 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -12,6 +12,7 @@ import time from typing import TYPE_CHECKING, Any from zigpy import types +import zigpy.device import zigpy.exceptions from zigpy.profiles import PROFILES import zigpy.quirks @@ -27,7 +28,7 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.event import async_track_time_interval -from . import channels, typing as zha_typing +from . import channels from .const import ( ATTR_ARGS, ATTR_ATTRIBUTE, @@ -100,7 +101,7 @@ class ZHADevice(LogMixin): def __init__( self, hass: HomeAssistant, - zigpy_device: zha_typing.ZigpyDeviceType, + zigpy_device: zigpy.device.Device, zha_gateway: ZHAGateway, ) -> None: """Initialize the gateway.""" @@ -151,7 +152,7 @@ class ZHADevice(LogMixin): self._ha_device_id = device_id @property - def device(self) -> zha_typing.ZigpyDeviceType: + def device(self) -> zigpy.device.Device: """Return underlying Zigpy device.""" return self._zigpy_device @@ -332,7 +333,7 @@ class ZHADevice(LogMixin): def new( cls, hass: HomeAssistant, - zigpy_dev: zha_typing.ZigpyDeviceType, + zigpy_dev: zigpy.device.Device, gateway: ZHAGateway, restored: bool = False, ): diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 1c75846ee7e..f387cc99bfe 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -21,6 +21,7 @@ import voluptuous as vol import zigpy.exceptions import zigpy.types import zigpy.util +import zigpy.zcl import zigpy.zdo.types as zdo_types from homeassistant.config_entries import ConfigEntry @@ -35,7 +36,6 @@ from .const import ( DATA_ZHA_GATEWAY, ) from .registries import BINDABLE_CLUSTERS -from .typing import ZigpyClusterType if TYPE_CHECKING: from .device import ZHADevice @@ -48,7 +48,7 @@ _T = TypeVar("_T") class BindingPair: """Information for binding.""" - source_cluster: ZigpyClusterType + source_cluster: zigpy.zcl.Cluster target_ieee: zigpy.types.EUI64 target_ep_id: int diff --git a/homeassistant/components/zha/core/typing.py b/homeassistant/components/zha/core/typing.py index 4c513ea7f21..714dc03ef82 100644 --- a/homeassistant/components/zha/core/typing.py +++ b/homeassistant/components/zha/core/typing.py @@ -2,16 +2,5 @@ from collections.abc import Callable from typing import TypeVar -import zigpy.device -import zigpy.endpoint -import zigpy.group -import zigpy.zcl -import zigpy.zdo - # pylint: disable=invalid-name CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) -ZigpyClusterType = zigpy.zcl.Cluster -ZigpyDeviceType = zigpy.device.Device -ZigpyEndpointType = zigpy.endpoint.Endpoint -ZigpyGroupType = zigpy.group.Group -ZigpyZdoType = zigpy.zdo.ZDO From 3571a80c8d9adc2c70379e235000513e4df261ae Mon Sep 17 00:00:00 2001 From: Thibaut Date: Mon, 20 Jun 2022 12:58:08 +0200 Subject: [PATCH 1609/3516] Add support for Somfy Thermostat in Overkiz integration (#67169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Somfy Thermostat * Fix linked device Co-authored-by: Vincent Le Bourlot * Mark Somfy thermostat as supported * Fix wrong usage of cast * Update presets to lowercase * Rename constants * Remove _saved_target_temp * Apply black * Clean code * Fix mypy errors * Use constants from pyoverkiz * Use enum for target temp * Add comment * Use ClimateEntityFeature * Ease code Co-authored-by: Mick Vleeshouwer * Remove unused imports * Use HVACAction * Use HVACMode * Use more Overkiz constants * Don’t copy/paste * Don’t use magic number Co-authored-by: Vincent Le Bourlot Co-authored-by: Mick Vleeshouwer --- .../overkiz/climate_entities/__init__.py | 2 + .../climate_entities/somfy_thermostat.py | 172 ++++++++++++++++++ homeassistant/components/overkiz/const.py | 1 + homeassistant/components/overkiz/executor.py | 4 + .../components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/overkiz/climate_entities/somfy_thermostat.py diff --git a/homeassistant/components/overkiz/climate_entities/__init__.py b/homeassistant/components/overkiz/climate_entities/__init__.py index e38a92b755c..d44aa85d167 100644 --- a/homeassistant/components/overkiz/climate_entities/__init__.py +++ b/homeassistant/components/overkiz/climate_entities/__init__.py @@ -3,8 +3,10 @@ from pyoverkiz.enums.ui import UIWidget from .atlantic_electrical_heater import AtlanticElectricalHeater from .atlantic_pass_apc_zone_control import AtlanticPassAPCZoneControl +from .somfy_thermostat import SomfyThermostat WIDGET_TO_CLIMATE_ENTITY = { UIWidget.ATLANTIC_ELECTRICAL_HEATER: AtlanticElectricalHeater, UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: AtlanticPassAPCZoneControl, + UIWidget.SOMFY_THERMOSTAT: SomfyThermostat, } diff --git a/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py b/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py new file mode 100644 index 00000000000..cfea49881f4 --- /dev/null +++ b/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py @@ -0,0 +1,172 @@ +"""Support for Somfy Smart Thermostat.""" +from __future__ import annotations + +from typing import Any, cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + PRESET_AWAY, + PRESET_HOME, + PRESET_NONE, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS + +from ..coordinator import OverkizDataUpdateCoordinator +from ..entity import OverkizEntity + +PRESET_FREEZE = "freeze" +PRESET_NIGHT = "night" + +STATE_DEROGATION_ACTIVE = "active" +STATE_DEROGATION_INACTIVE = "inactive" + + +OVERKIZ_TO_HVAC_MODES: dict[str, HVACMode] = { + STATE_DEROGATION_ACTIVE: HVACMode.HEAT, + STATE_DEROGATION_INACTIVE: HVACMode.AUTO, +} +HVAC_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_HVAC_MODES.items()} + +OVERKIZ_TO_PRESET_MODES: dict[OverkizCommandParam, str] = { + OverkizCommandParam.AT_HOME_MODE: PRESET_HOME, + OverkizCommandParam.AWAY_MODE: PRESET_AWAY, + OverkizCommandParam.FREEZE_MODE: PRESET_FREEZE, + OverkizCommandParam.MANUAL_MODE: PRESET_NONE, + OverkizCommandParam.SLEEPING_MODE: PRESET_NIGHT, + OverkizCommandParam.SUDDEN_DROP_MODE: PRESET_NONE, +} +PRESET_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_PRESET_MODES.items()} +TARGET_TEMP_TO_OVERKIZ = { + PRESET_HOME: OverkizState.SOMFY_THERMOSTAT_AT_HOME_TARGET_TEMPERATURE, + PRESET_AWAY: OverkizState.SOMFY_THERMOSTAT_AWAY_MODE_TARGET_TEMPERATURE, + PRESET_FREEZE: OverkizState.SOMFY_THERMOSTAT_FREEZE_MODE_TARGET_TEMPERATURE, + PRESET_NIGHT: OverkizState.SOMFY_THERMOSTAT_SLEEPING_MODE_TARGET_TEMPERATURE, +} + +# controllableName is somfythermostat:SomfyThermostatTemperatureSensor +TEMPERATURE_SENSOR_DEVICE_INDEX = 2 + + +class SomfyThermostat(OverkizEntity, ClimateEntity): + """Representation of Somfy Smart Thermostat.""" + + _attr_temperature_unit = TEMP_CELSIUS + _attr_supported_features = ( + ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE + ) + _attr_hvac_modes = [*HVAC_MODES_TO_OVERKIZ] + _attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ] + # Both min and max temp values have been retrieved from the Somfy Application. + _attr_min_temp = 15.0 + _attr_max_temp = 26.0 + + def __init__( + self, device_url: str, coordinator: OverkizDataUpdateCoordinator + ) -> None: + """Init method.""" + super().__init__(device_url, coordinator) + self.temperature_device = self.executor.linked_device( + TEMPERATURE_SENSOR_DEVICE_INDEX + ) + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode.""" + return OVERKIZ_TO_HVAC_MODES[ + cast( + str, self.executor.select_state(OverkizState.CORE_DEROGATION_ACTIVATION) + ) + ] + + @property + def hvac_action(self) -> str: + """Return the current running hvac operation if supported.""" + if not self.current_temperature or not self.target_temperature: + return HVACAction.IDLE + if self.current_temperature < self.target_temperature: + return HVACAction.HEATING + return HVACAction.IDLE + + @property + def preset_mode(self) -> str: + """Return the current preset mode, e.g., home, away, temp.""" + if self.hvac_mode == HVACMode.AUTO: + state_key = OverkizState.SOMFY_THERMOSTAT_HEATING_MODE + else: + state_key = OverkizState.SOMFY_THERMOSTAT_DEROGATION_HEATING_MODE + + state = cast(str, self.executor.select_state(state_key)) + + return OVERKIZ_TO_PRESET_MODES[OverkizCommandParam(state)] + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + if temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE]: + return cast(float, temperature.value) + return None + + @property + def target_temperature(self) -> float | None: + """Return the temperature we try to reach.""" + if self.hvac_mode == HVACMode.AUTO: + if self.preset_mode == PRESET_NONE: + return None + return cast( + float, + self.executor.select_state(TARGET_TEMP_TO_OVERKIZ[self.preset_mode]), + ) + return cast( + float, + self.executor.select_state(OverkizState.CORE_DEROGATED_TARGET_TEMPERATURE), + ) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + temperature = kwargs[ATTR_TEMPERATURE] + + await self.executor.async_execute_command( + OverkizCommand.SET_DEROGATION, + temperature, + OverkizCommandParam.FURTHER_NOTICE, + ) + await self.executor.async_execute_command( + OverkizCommand.SET_MODE_TEMPERATURE, + OverkizCommandParam.MANUAL_MODE, + temperature, + ) + await self.executor.async_execute_command(OverkizCommand.REFRESH_STATE) + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target hvac mode.""" + if hvac_mode == HVACMode.AUTO: + await self.executor.async_execute_command(OverkizCommand.EXIT_DEROGATION) + await self.executor.async_execute_command(OverkizCommand.REFRESH_STATE) + else: + await self.async_set_preset_mode(PRESET_NONE) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + if preset_mode in [PRESET_FREEZE, PRESET_NIGHT, PRESET_AWAY, PRESET_HOME]: + await self.executor.async_execute_command( + OverkizCommand.SET_DEROGATION, + PRESET_MODES_TO_OVERKIZ[preset_mode], + OverkizCommandParam.FURTHER_NOTICE, + ) + elif preset_mode == PRESET_NONE: + await self.executor.async_execute_command( + OverkizCommand.SET_DEROGATION, + self.target_temperature, + OverkizCommandParam.FURTHER_NOTICE, + ) + await self.executor.async_execute_command( + OverkizCommand.SET_MODE_TEMPERATURE, + OverkizCommandParam.MANUAL_MODE, + self.target_temperature, + ) + await self.executor.async_execute_command(OverkizCommand.REFRESH_STATE) diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index dedf5c6d4a6..447ebc5ac42 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -70,6 +70,7 @@ OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform | None] = { UIWidget.RTD_OUTDOOR_SIREN: Platform.SWITCH, # widgetName, uiClass is Siren (not supported) UIWidget.RTS_GENERIC: Platform.COVER, # widgetName, uiClass is Generic (not supported) UIWidget.SIREN_STATUS: None, # widgetName, uiClass is Siren (siren) + UIWidget.SOMFY_THERMOSTAT: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.STATELESS_ALARM_CONTROLLER: Platform.SWITCH, # widgetName, uiClass is Alarm (not supported) UIWidget.STATEFUL_ALARM_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) UIWidget.STATELESS_EXTERIOR_HEATING: Platform.SWITCH, # widgetName, uiClass is ExteriorHeatingSystem (not supported) diff --git a/homeassistant/components/overkiz/executor.py b/homeassistant/components/overkiz/executor.py index 9bf7ef43b02..e82a6e21f63 100644 --- a/homeassistant/components/overkiz/executor.py +++ b/homeassistant/components/overkiz/executor.py @@ -37,6 +37,10 @@ class OverkizExecutor: """Return Overkiz device linked to this entity.""" return self.coordinator.data[self.device_url] + def linked_device(self, index: int) -> Device: + """Return Overkiz device sharing the same base url.""" + return self.coordinator.data[f"{self.base_device_url}#{index}"] + def select_command(self, *commands: str) -> str | None: """Select first existing command in a list of commands.""" existing_commands = self.device.definition.commands diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index c81de1e6139..432988a6dc4 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -3,7 +3,7 @@ "name": "Overkiz (by Somfy)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", - "requirements": ["pyoverkiz==1.4.0"], + "requirements": ["pyoverkiz==1.4.1"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 07d2983550d..7ad94a23ea2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1720,7 +1720,7 @@ pyotgw==1.1b1 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.4.0 +pyoverkiz==1.4.1 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c12a0584249..e4acef811f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1169,7 +1169,7 @@ pyotgw==1.1b1 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.4.0 +pyoverkiz==1.4.1 # homeassistant.components.openweathermap pyowm==3.2.0 From c075760ca0872d047e6a7c0bb7101bc66878c602 Mon Sep 17 00:00:00 2001 From: w-marco <92536586+w-marco@users.noreply.github.com> Date: Mon, 20 Jun 2022 13:03:43 +0200 Subject: [PATCH 1610/3516] Display Windows as TYPE_WINDOW in Google Home (#73533) * Display Windows as TYPE_WINDOW in Google Home * set window type to window in smart_home test --- homeassistant/components/google_assistant/const.py | 3 ++- tests/components/google_assistant/test_smart_home.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index a19707bffbc..339cddae883 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -86,6 +86,7 @@ TYPE_SPEAKER = f"{PREFIX_TYPES}SPEAKER" TYPE_SWITCH = f"{PREFIX_TYPES}SWITCH" TYPE_THERMOSTAT = f"{PREFIX_TYPES}THERMOSTAT" TYPE_TV = f"{PREFIX_TYPES}TV" +TYPE_WINDOW = f"{PREFIX_TYPES}WINDOW" TYPE_VACUUM = f"{PREFIX_TYPES}VACUUM" SERVICE_REQUEST_SYNC = "request_sync" @@ -147,7 +148,7 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = { (binary_sensor.DOMAIN, binary_sensor.BinarySensorDeviceClass.DOOR): TYPE_DOOR, (binary_sensor.DOMAIN, binary_sensor.BinarySensorDeviceClass.LOCK): TYPE_SENSOR, (binary_sensor.DOMAIN, binary_sensor.BinarySensorDeviceClass.OPENING): TYPE_SENSOR, - (binary_sensor.DOMAIN, binary_sensor.BinarySensorDeviceClass.WINDOW): TYPE_SENSOR, + (binary_sensor.DOMAIN, binary_sensor.BinarySensorDeviceClass.WINDOW): TYPE_WINDOW, ( binary_sensor.DOMAIN, binary_sensor.BinarySensorDeviceClass.GARAGE_DOOR, diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 4b11910999a..684a6db2640 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -1026,7 +1026,7 @@ async def test_device_class_switch(hass, device_class, google_type): ("garage_door", "action.devices.types.GARAGE"), ("lock", "action.devices.types.SENSOR"), ("opening", "action.devices.types.SENSOR"), - ("window", "action.devices.types.SENSOR"), + ("window", "action.devices.types.WINDOW"), ], ) async def test_device_class_binary_sensor(hass, device_class, google_type): From b6d3e34ebc2a7348584052073ae40025a4bd7bf9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Jun 2022 14:50:27 +0200 Subject: [PATCH 1611/3516] Drop custom type (CALLABLE_T) from zha (#73736) * Drop CALLABLE_T from zha * Adjust .coveragerc * Apply suggestions from code review Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> * Add TypeVar * Apply suggestions from code review Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> * One more Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> * Flake8 Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- .coveragerc | 1 - .../components/zha/core/channels/security.py | 8 ++-- .../components/zha/core/registries.py | 42 +++++++++++-------- homeassistant/components/zha/core/typing.py | 6 --- homeassistant/components/zha/entity.py | 6 +-- 5 files changed, 31 insertions(+), 32 deletions(-) delete mode 100644 homeassistant/components/zha/core/typing.py diff --git a/.coveragerc b/.coveragerc index 46cbed3a0dc..eba89e5f238 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1516,7 +1516,6 @@ omit = homeassistant/components/zha/core/gateway.py homeassistant/components/zha/core/helpers.py homeassistant/components/zha/core/registries.py - homeassistant/components/zha/core/typing.py homeassistant/components/zha/entity.py homeassistant/components/zha/light.py homeassistant/components/zha/sensor.py diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 4c0d6bbfd59..789e792e149 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -7,7 +7,8 @@ https://home-assistant.io/integrations/zha/ from __future__ import annotations import asyncio -from typing import TYPE_CHECKING +from collections.abc import Callable +from typing import TYPE_CHECKING, Any from zigpy.exceptions import ZigbeeException import zigpy.zcl @@ -25,7 +26,6 @@ from ..const import ( WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_YES, ) -from ..typing import CALLABLE_T from .base import ChannelStatus, ZigbeeChannel if TYPE_CHECKING: @@ -55,7 +55,7 @@ class IasAce(ZigbeeChannel): def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: """Initialize IAS Ancillary Control Equipment channel.""" super().__init__(cluster, ch_pool) - self.command_map: dict[int, CALLABLE_T] = { + self.command_map: dict[int, Callable[..., Any]] = { IAS_ACE_ARM: self.arm, IAS_ACE_BYPASS: self._bypass, IAS_ACE_EMERGENCY: self._emergency, @@ -67,7 +67,7 @@ class IasAce(ZigbeeChannel): IAS_ACE_GET_BYPASSED_ZONE_LIST: self._get_bypassed_zone_list, IAS_ACE_GET_ZONE_STATUS: self._get_zone_status, } - self.arm_map: dict[AceCluster.ArmMode, CALLABLE_T] = { + self.arm_map: dict[AceCluster.ArmMode, Callable[..., Any]] = { AceCluster.ArmMode.Disarm: self._disarm, AceCluster.ArmMode.Arm_All_Zones: self._arm_away, AceCluster.ArmMode.Arm_Day_Home_Only: self._arm_day, diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index ed6b047566c..7e2114b5911 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -4,7 +4,7 @@ from __future__ import annotations import collections from collections.abc import Callable import dataclasses -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypeVar import attr from zigpy import zcl @@ -17,11 +17,15 @@ from homeassistant.const import Platform # importing channels updates registries from . import channels as zha_channels # noqa: F401 pylint: disable=unused-import from .decorators import DictRegistry, SetRegistry -from .typing import CALLABLE_T if TYPE_CHECKING: + from ..entity import ZhaEntity, ZhaGroupEntity from .channels.base import ClientChannel, ZigbeeChannel + +_ZhaEntityT = TypeVar("_ZhaEntityT", bound=type["ZhaEntity"]) +_ZhaGroupEntityT = TypeVar("_ZhaGroupEntityT", bound=type["ZhaGroupEntity"]) + GROUP_ENTITY_DOMAINS = [Platform.LIGHT, Platform.SWITCH, Platform.FAN] PHILLIPS_REMOTE_CLUSTER = 0xFC00 @@ -215,7 +219,7 @@ class MatchRule: class EntityClassAndChannels: """Container for entity class and corresponding channels.""" - entity_class: CALLABLE_T + entity_class: type[ZhaEntity] claimed_channel: list[ZigbeeChannel] @@ -225,19 +229,19 @@ class ZHAEntityRegistry: def __init__(self): """Initialize Registry instance.""" self._strict_registry: dict[ - str, dict[MatchRule, CALLABLE_T] + str, dict[MatchRule, type[ZhaEntity]] ] = collections.defaultdict(dict) self._multi_entity_registry: dict[ - str, dict[int | str | None, dict[MatchRule, list[CALLABLE_T]]] + str, dict[int | str | None, dict[MatchRule, list[type[ZhaEntity]]]] ] = collections.defaultdict( lambda: collections.defaultdict(lambda: collections.defaultdict(list)) ) self._config_diagnostic_entity_registry: dict[ - str, dict[int | str | None, dict[MatchRule, list[CALLABLE_T]]] + str, dict[int | str | None, dict[MatchRule, list[type[ZhaEntity]]]] ] = collections.defaultdict( lambda: collections.defaultdict(lambda: collections.defaultdict(list)) ) - self._group_registry: dict[str, CALLABLE_T] = {} + self._group_registry: dict[str, type[ZhaGroupEntity]] = {} self.single_device_matches: dict[ Platform, dict[EUI64, list[str]] ] = collections.defaultdict(lambda: collections.defaultdict(list)) @@ -248,8 +252,8 @@ class ZHAEntityRegistry: manufacturer: str, model: str, channels: list[ZigbeeChannel], - default: CALLABLE_T = None, - ) -> tuple[CALLABLE_T, list[ZigbeeChannel]]: + default: type[ZhaEntity] | None = None, + ) -> tuple[type[ZhaEntity] | None, list[ZigbeeChannel]]: """Match a ZHA Channels to a ZHA Entity class.""" matches = self._strict_registry[component] for match in sorted(matches, key=lambda x: x.weight, reverse=True): @@ -310,7 +314,7 @@ class ZHAEntityRegistry: return result, list(all_claimed) - def get_group_entity(self, component: str) -> CALLABLE_T: + def get_group_entity(self, component: str) -> type[ZhaGroupEntity] | None: """Match a ZHA group to a ZHA Entity class.""" return self._group_registry.get(component) @@ -322,14 +326,14 @@ class ZHAEntityRegistry: manufacturers: Callable | set[str] | str = None, models: Callable | set[str] | str = None, aux_channels: Callable | set[str] | str = None, - ) -> Callable[[CALLABLE_T], CALLABLE_T]: + ) -> Callable[[_ZhaEntityT], _ZhaEntityT]: """Decorate a strict match rule.""" rule = MatchRule( channel_names, generic_ids, manufacturers, models, aux_channels ) - def decorator(zha_ent: CALLABLE_T) -> CALLABLE_T: + def decorator(zha_ent: _ZhaEntityT) -> _ZhaEntityT: """Register a strict match rule. All non empty fields of a match rule must match. @@ -348,7 +352,7 @@ class ZHAEntityRegistry: models: Callable | set[str] | str = None, aux_channels: Callable | set[str] | str = None, stop_on_match_group: int | str | None = None, - ) -> Callable[[CALLABLE_T], CALLABLE_T]: + ) -> Callable[[_ZhaEntityT], _ZhaEntityT]: """Decorate a loose match rule.""" rule = MatchRule( @@ -359,7 +363,7 @@ class ZHAEntityRegistry: aux_channels, ) - def decorator(zha_entity: CALLABLE_T) -> CALLABLE_T: + def decorator(zha_entity: _ZhaEntityT) -> _ZhaEntityT: """Register a loose match rule. All non empty fields of a match rule must match. @@ -381,7 +385,7 @@ class ZHAEntityRegistry: models: Callable | set[str] | str = None, aux_channels: Callable | set[str] | str = None, stop_on_match_group: int | str | None = None, - ) -> Callable[[CALLABLE_T], CALLABLE_T]: + ) -> Callable[[_ZhaEntityT], _ZhaEntityT]: """Decorate a loose match rule.""" rule = MatchRule( @@ -392,7 +396,7 @@ class ZHAEntityRegistry: aux_channels, ) - def decorator(zha_entity: CALLABLE_T) -> CALLABLE_T: + def decorator(zha_entity: _ZhaEntityT) -> _ZhaEntityT: """Register a loose match rule. All non empty fields of a match rule must match. @@ -405,10 +409,12 @@ class ZHAEntityRegistry: return decorator - def group_match(self, component: str) -> Callable[[CALLABLE_T], CALLABLE_T]: + def group_match( + self, component: str + ) -> Callable[[_ZhaGroupEntityT], _ZhaGroupEntityT]: """Decorate a group match rule.""" - def decorator(zha_ent: CALLABLE_T) -> CALLABLE_T: + def decorator(zha_ent: _ZhaGroupEntityT) -> _ZhaGroupEntityT: """Register a group match rule.""" self._group_registry[component] = zha_ent return zha_ent diff --git a/homeassistant/components/zha/core/typing.py b/homeassistant/components/zha/core/typing.py deleted file mode 100644 index 714dc03ef82..00000000000 --- a/homeassistant/components/zha/core/typing.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Typing helpers for ZHA component.""" -from collections.abc import Callable -from typing import TypeVar - -# pylint: disable=invalid-name -CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 88dc9454f37..fb1a35ff72b 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Callable import functools import logging from typing import TYPE_CHECKING, Any @@ -29,7 +30,6 @@ from .core.const import ( SIGNAL_REMOVE, ) from .core.helpers import LogMixin -from .core.typing import CALLABLE_T if TYPE_CHECKING: from .core.channels.base import ZigbeeChannel @@ -57,7 +57,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): self._state: Any = None self._extra_state_attributes: dict[str, Any] = {} self._zha_device = zha_device - self._unsubs: list[CALLABLE_T] = [] + self._unsubs: list[Callable[[], None]] = [] self.remove_future: asyncio.Future[Any] = asyncio.Future() @property @@ -130,7 +130,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): self, channel: ZigbeeChannel, signal: str, - func: CALLABLE_T, + func: Callable[[], Any], signal_override=False, ): """Accept a signal from a channel.""" From 670bf0641a06676ec90c38fcc90953e8572f6a21 Mon Sep 17 00:00:00 2001 From: Alessandro Ghedini Date: Mon, 20 Jun 2022 14:08:50 +0100 Subject: [PATCH 1612/3516] Update london-tube-status for TfL API breaking change (#73671) * Update london-tube-status for TfL API breaking change The TfL API used by the london_underground component (through the london-tube-status module) introduced breaking changes recently, which in turn broke the component, and require updating the london-tube-status dependency to fix. However the newer module versions also introduced other changes, including switching from requests to aiohttp, which require converting the london_underground component to use async APIs. Fixes #73442 * Update sensor.py Co-authored-by: Erik Montnemery --- .../london_underground/manifest.json | 2 +- .../components/london_underground/sensor.py | 18 +++++++++++------- requirements_all.txt | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/london_underground/manifest.json b/homeassistant/components/london_underground/manifest.json index eed2ec45dd7..e3223eb109f 100644 --- a/homeassistant/components/london_underground/manifest.json +++ b/homeassistant/components/london_underground/manifest.json @@ -2,7 +2,7 @@ "domain": "london_underground", "name": "London Underground", "documentation": "https://www.home-assistant.io/integrations/london_underground", - "requirements": ["london-tube-status==0.2"], + "requirements": ["london-tube-status==0.5"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["london_tube_status"] diff --git a/homeassistant/components/london_underground/sensor.py b/homeassistant/components/london_underground/sensor.py index a73909295b6..a4cc66a8447 100644 --- a/homeassistant/components/london_underground/sensor.py +++ b/homeassistant/components/london_underground/sensor.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -43,21 +44,24 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Tube sensor.""" - data = TubeData() - data.update() + session = async_get_clientsession(hass) + + data = TubeData(session) + await data.update() + sensors = [] for line in config[CONF_LINE]: sensors.append(LondonTubeSensor(line, data)) - add_entities(sensors, True) + async_add_entities(sensors, True) class LondonTubeSensor(SensorEntity): @@ -92,8 +96,8 @@ class LondonTubeSensor(SensorEntity): self.attrs["Description"] = self._description return self.attrs - def update(self): + async def async_update(self): """Update the sensor.""" - self._data.update() + await self._data.update() self._state = self._data.data[self.name]["State"] self._description = self._data.data[self.name]["Description"] diff --git a/requirements_all.txt b/requirements_all.txt index 7ad94a23ea2..1ffbe1a76ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -969,7 +969,7 @@ locationsharinglib==4.1.5 logi_circle==0.2.3 # homeassistant.components.london_underground -london-tube-status==0.2 +london-tube-status==0.5 # homeassistant.components.recorder lru-dict==1.1.7 From 483406dea5c83cf321b7f9298d4db07a773a2884 Mon Sep 17 00:00:00 2001 From: rappenze Date: Mon, 20 Jun 2022 15:46:28 +0200 Subject: [PATCH 1613/3516] Code cleanup fibaro switch and binary sensor (#73386) --- .../components/fibaro/binary_sensor.py | 6 +++-- homeassistant/components/fibaro/switch.py | 22 ++++++++----------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/fibaro/binary_sensor.py b/homeassistant/components/fibaro/binary_sensor.py index 3c423dc0ce8..359869efc25 100644 --- a/homeassistant/components/fibaro/binary_sensor.py +++ b/homeassistant/components/fibaro/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Fibaro binary sensors.""" from __future__ import annotations +from typing import Any + from homeassistant.components.binary_sensor import ( ENTITY_ID_FORMAT, BinarySensorDeviceClass, @@ -49,7 +51,7 @@ async def async_setup_entry( class FibaroBinarySensor(FibaroDevice, BinarySensorEntity): """Representation of a Fibaro Binary Sensor.""" - def __init__(self, fibaro_device): + def __init__(self, fibaro_device: Any) -> None: """Initialize the binary_sensor.""" super().__init__(fibaro_device) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) @@ -62,6 +64,6 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorEntity): self._attr_device_class = SENSOR_TYPES[stype][2] self._attr_icon = SENSOR_TYPES[stype][1] - def update(self): + def update(self) -> None: """Get the latest data and update the state.""" self._attr_is_on = self.current_binary_state diff --git a/homeassistant/components/fibaro/switch.py b/homeassistant/components/fibaro/switch.py index fe2b35866b0..66aad4d673b 100644 --- a/homeassistant/components/fibaro/switch.py +++ b/homeassistant/components/fibaro/switch.py @@ -1,6 +1,8 @@ """Support for Fibaro switches.""" from __future__ import annotations +from typing import Any + from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -31,27 +33,21 @@ async def async_setup_entry( class FibaroSwitch(FibaroDevice, SwitchEntity): """Representation of a Fibaro Switch.""" - def __init__(self, fibaro_device): + def __init__(self, fibaro_device: Any) -> None: """Initialize the Fibaro device.""" - self._state = False super().__init__(fibaro_device) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn device on.""" self.call_turn_on() - self._state = True + self._attr_is_on = True - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn device off.""" self.call_turn_off() - self._state = False + self._attr_is_on = False - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - def update(self): + def update(self) -> None: """Update device state.""" - self._state = self.current_binary_state + self._attr_is_on = self.current_binary_state From 3824703a64cde109a27a5b9b0b684ee309b160d7 Mon Sep 17 00:00:00 2001 From: Joel <34544090+JoelKle@users.noreply.github.com> Date: Mon, 20 Jun 2022 16:08:43 +0200 Subject: [PATCH 1614/3516] Fix homematicip cloud cover tilt position (#73410) * cover slats fixed set tilt position * Update cover.py * Adjust tests Co-authored-by: Erik Montnemery --- homeassistant/components/homematicip_cloud/cover.py | 10 +++++++--- tests/components/homematicip_cloud/test_cover.py | 12 ++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index f4af4d88a8e..b5076fa74ec 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -241,15 +241,19 @@ class HomematicipMultiCoverSlats(HomematicipMultiCoverShutter, CoverEntity): position = kwargs[ATTR_TILT_POSITION] # HmIP slats is closed:1 -> open:0 level = 1 - position / 100.0 - await self._device.set_slats_level(level, self._channel) + await self._device.set_slats_level(slatsLevel=level, channelIndex=self._channel) async def async_open_cover_tilt(self, **kwargs) -> None: """Open the slats.""" - await self._device.set_slats_level(HMIP_SLATS_OPEN, self._channel) + await self._device.set_slats_level( + slatsLevel=HMIP_SLATS_OPEN, channelIndex=self._channel + ) async def async_close_cover_tilt(self, **kwargs) -> None: """Close the slats.""" - await self._device.set_slats_level(HMIP_SLATS_CLOSED, self._channel) + await self._device.set_slats_level( + slatsLevel=HMIP_SLATS_CLOSED, channelIndex=self._channel + ) async def async_stop_cover_tilt(self, **kwargs) -> None: """Stop the device if in motion.""" diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py index a35576ed353..023ba9d5f0e 100644 --- a/tests/components/homematicip_cloud/test_cover.py +++ b/tests/components/homematicip_cloud/test_cover.py @@ -109,7 +109,7 @@ async def test_hmip_cover_slats(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 1 assert hmip_device.mock_calls[-1][0] == "set_slats_level" - assert hmip_device.mock_calls[-1][1] == (0, 1) + assert hmip_device.mock_calls[-1][2] == {"channelIndex": 1, "slatsLevel": 0} await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0) await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0) ha_state = hass.states.get(entity_id) @@ -125,7 +125,7 @@ async def test_hmip_cover_slats(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 4 assert hmip_device.mock_calls[-1][0] == "set_slats_level" - assert hmip_device.mock_calls[-1][1] == (0.5, 1) + assert hmip_device.mock_calls[-1][2] == {"channelIndex": 1, "slatsLevel": 0.5} await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0.5) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OPEN @@ -137,7 +137,7 @@ async def test_hmip_cover_slats(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 6 assert hmip_device.mock_calls[-1][0] == "set_slats_level" - assert hmip_device.mock_calls[-1][1] == (1, 1) + assert hmip_device.mock_calls[-1][2] == {"channelIndex": 1, "slatsLevel": 1} await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 1) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OPEN @@ -187,7 +187,7 @@ async def test_hmip_multi_cover_slats(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 1 assert hmip_device.mock_calls[-1][0] == "set_slats_level" - assert hmip_device.mock_calls[-1][1] == (0, 4) + assert hmip_device.mock_calls[-1][2] == {"channelIndex": 4, "slatsLevel": 0} await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0, channel=4) await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0, channel=4) ha_state = hass.states.get(entity_id) @@ -203,7 +203,7 @@ async def test_hmip_multi_cover_slats(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 4 assert hmip_device.mock_calls[-1][0] == "set_slats_level" - assert hmip_device.mock_calls[-1][1] == (0.5, 4) + assert hmip_device.mock_calls[-1][2] == {"channelIndex": 4, "slatsLevel": 0.5} await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0.5, channel=4) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OPEN @@ -215,7 +215,7 @@ async def test_hmip_multi_cover_slats(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 6 assert hmip_device.mock_calls[-1][0] == "set_slats_level" - assert hmip_device.mock_calls[-1][1] == (1, 4) + assert hmip_device.mock_calls[-1][2] == {"channelIndex": 4, "slatsLevel": 1} await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 1, channel=4) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OPEN From 81e3ed790da1ae7408c87a4b46616d71e2b3ec53 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Mon, 20 Jun 2022 17:09:58 +0300 Subject: [PATCH 1615/3516] Add re-authentication for `transmission` (#73124) * Add reauth flow to transmission * fix async_setup * add strings * fix test coverage --- .../components/transmission/__init__.py | 13 +-- .../components/transmission/config_flow.py | 47 +++++++++ .../components/transmission/strings.json | 10 +- .../transmission/translations/en.json | 10 +- .../transmission/test_config_flow.py | 96 +++++++++++++++++++ tests/components/transmission/test_init.py | 4 +- 6 files changed, 168 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index ac6659b048e..ae5d2dacf61 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -20,7 +20,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval @@ -85,8 +85,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b client = TransmissionClient(hass, config_entry) hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = client - if not await client.async_setup(): - return False + await client.async_setup() return True @@ -152,15 +151,15 @@ class TransmissionClient: """Return the TransmissionData object.""" return self._tm_data - async def async_setup(self): + async def async_setup(self) -> None: """Set up the Transmission client.""" try: self.tm_api = await get_api(self.hass, self.config_entry.data) except CannotConnect as error: raise ConfigEntryNotReady from error - except (AuthenticationError, UnknownError): - return False + except (AuthenticationError, UnknownError) as error: + raise ConfigEntryAuthFailed from error self._tm_data = TransmissionData(self.hass, self.config_entry, self.tm_api) @@ -262,8 +261,6 @@ class TransmissionClient: self.config_entry.add_update_listener(self.async_options_updated) - return True - def add_options(self): """Add options for entry.""" if not self.config_entry.options: diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index b57279ebbbb..c0bc51b0683 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -1,6 +1,9 @@ """Config flow for Transmission Bittorent Client.""" from __future__ import annotations +from collections.abc import Mapping +from typing import Any + import voluptuous as vol from homeassistant import config_entries @@ -13,6 +16,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from . import get_api from .const import ( @@ -43,6 +47,7 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle Tansmission config flow.""" VERSION = 1 + _reauth_entry: config_entries.ConfigEntry | None @staticmethod @callback @@ -87,6 +92,48 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Confirm reauth dialog.""" + errors = {} + assert self._reauth_entry + if user_input is not None: + user_input = {**self._reauth_entry.data, **user_input} + try: + await get_api(self.hass, user_input) + + except AuthenticationError: + errors[CONF_PASSWORD] = "invalid_auth" + except (CannotConnect, UnknownError): + errors["base"] = "cannot_connect" + else: + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=user_input + ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + description_placeholders={ + CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME] + }, + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) + class TransmissionOptionsFlowHandler(config_entries.OptionsFlow): """Handle Transmission client options.""" diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index 81725ad7d16..0194917c416 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -10,6 +10,13 @@ "password": "[%key:common::config_flow::data::password%]", "port": "[%key:common::config_flow::data::port%]" } + }, + "reauth_confirm": { + "description": "The password for {username} is invalid.", + "title": "[%key:common::config_flow::title::reauth%]", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -18,7 +25,8 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "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%]" } }, "options": { diff --git a/homeassistant/components/transmission/translations/en.json b/homeassistant/components/transmission/translations/en.json index e92e307d3bc..3726f6f0a7e 100644 --- a/homeassistant/components/transmission/translations/en.json +++ b/homeassistant/components/transmission/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": { "cannot_connect": "Failed to connect", @@ -9,6 +10,13 @@ "name_exists": "Name already exists" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "The password for {username} is invalid.", + "title": "Reauthenticate Integration" + }, "user": { "data": { "host": "Host", diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 4e3a7c73d6c..7588736e997 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -257,3 +257,99 @@ async def test_error_on_unknown_error(hass, unknown_error): ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "cannot_connect"} + + +async def test_reauth_success(hass, api): + """Test we can reauth.""" + entry = MockConfigEntry( + domain=transmission.DOMAIN, + data=MOCK_ENTRY, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + transmission.DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_ENTRY, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + assert result["description_placeholders"] == {CONF_USERNAME: USERNAME} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + + +async def test_reauth_failed(hass, auth_error): + """Test we can reauth.""" + entry = MockConfigEntry( + domain=transmission.DOMAIN, + data=MOCK_ENTRY, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + transmission.DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_ENTRY, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-wrong-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == { + CONF_PASSWORD: "invalid_auth", + } + + +async def test_reauth_failed_conn_error(hass, conn_error): + """Test we can reauth.""" + entry = MockConfigEntry( + domain=transmission.DOMAIN, + data=MOCK_ENTRY, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + transmission.DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_ENTRY, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-wrong-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/transmission/test_init.py b/tests/components/transmission/test_init.py index 78fddc5be86..c3dc924c54e 100644 --- a/tests/components/transmission/test_init.py +++ b/tests/components/transmission/test_init.py @@ -6,7 +6,7 @@ import pytest from transmissionrpc.error import TransmissionError from homeassistant.components import transmission -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, mock_coro @@ -105,7 +105,7 @@ async def test_setup_failed(hass): with patch( "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized") - ): + ), pytest.raises(ConfigEntryAuthFailed): assert await transmission.async_setup_entry(hass, entry) is False From be2aa44559dcb19fa7211e084cfb9372cbb9e7f5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Jun 2022 16:25:24 +0200 Subject: [PATCH 1616/3516] Fix mypy issues in zha config_flow (#73744) --- homeassistant/components/zha/config_flow.py | 3 +-- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 1832424587b..03be28b9d8c 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -168,11 +168,10 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): radio_type = discovery_info.properties.get("radio_type") or local_name node_name = local_name[: -len(".local")] host = discovery_info.host + port = discovery_info.port if local_name.startswith("tube") or "efr32" in local_name: # This is hard coded to work with legacy devices port = 6638 - else: - port = discovery_info.port device_path = f"socket://{host}:{port}" if current_entry := await self.async_set_unique_id(node_name): diff --git a/mypy.ini b/mypy.ini index c4561dea5a8..d5b5ca39183 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2997,9 +2997,6 @@ ignore_errors = true [mypy-homeassistant.components.zha.climate] ignore_errors = true -[mypy-homeassistant.components.zha.config_flow] -ignore_errors = true - [mypy-homeassistant.components.zha.core.channels.base] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 6b42bcd2cdd..bb46ba39212 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -148,7 +148,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.zha.binary_sensor", "homeassistant.components.zha.button", "homeassistant.components.zha.climate", - "homeassistant.components.zha.config_flow", "homeassistant.components.zha.core.channels.base", "homeassistant.components.zha.core.channels.closures", "homeassistant.components.zha.core.channels.general", From 9a95649a226e5e6279d84745946656bda4f9dbff Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Jun 2022 17:29:45 +0200 Subject: [PATCH 1617/3516] Use a TypedDict for REPORT_CONFIG in zha (#73629) * Introduce ReportConfig TypedDict in zha * Fix hint * Always use Tuple * Replace empty list with empty tuple * Allow float for third config tuple value * ReportConfig -> AttrReportConfig * dict -> AttrReportConfig * Allow int attributes * Add coments --- .../components/zha/core/channels/base.py | 14 ++- .../components/zha/core/channels/closures.py | 10 +- .../components/zha/core/channels/general.py | 54 +++++--- .../zha/core/channels/homeautomation.py | 20 +-- .../components/zha/core/channels/hvac.py | 34 +++-- .../components/zha/core/channels/lighting.py | 8 +- .../zha/core/channels/manufacturerspecific.py | 28 ++--- .../zha/core/channels/measurement.py | 118 ++++++++++-------- .../zha/core/channels/smartenergy.py | 8 +- 9 files changed, 173 insertions(+), 121 deletions(-) diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index 9b3ad6d9572..c9472af5938 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -5,7 +5,7 @@ import asyncio from enum import Enum from functools import partialmethod, wraps import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypedDict import zigpy.exceptions import zigpy.zcl @@ -46,6 +46,16 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) +class AttrReportConfig(TypedDict, total=True): + """Configuration to report for the attributes.""" + + # Could be either an attribute name or attribute id + attr: str | int + # The config for the attribute reporting configuration consists of a tuple for + # (minimum_reported_time_interval_s, maximum_reported_time_interval_s, value_delta) + config: tuple[int, int, int | float] + + def parse_and_log_command(channel, tsn, command_id, args): """Parse and log a zigbee cluster command.""" cmd = channel.cluster.server_commands.get(command_id, [command_id])[0] @@ -99,7 +109,7 @@ class ChannelStatus(Enum): class ZigbeeChannel(LogMixin): """Base channel for a Zigbee cluster.""" - REPORT_CONFIG: tuple[dict[int | str, tuple[int, int, int | float]]] = () + REPORT_CONFIG: tuple[AttrReportConfig] = () BIND: bool = True # Dict of attributes to read on channel initialization. diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index bf50c8fc4ba..de2dcaf38e9 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -5,7 +5,7 @@ from homeassistant.core import callback from .. import registries from ..const import REPORT_CONFIG_IMMEDIATE, SIGNAL_ATTR_UPDATED -from .base import ClientChannel, ZigbeeChannel +from .base import AttrReportConfig, ClientChannel, ZigbeeChannel @registries.ZIGBEE_CHANNEL_REGISTRY.register(closures.DoorLock.cluster_id) @@ -13,7 +13,9 @@ class DoorLockChannel(ZigbeeChannel): """Door lock channel.""" _value_attribute = 0 - REPORT_CONFIG = ({"attr": "lock_state", "config": REPORT_CONFIG_IMMEDIATE},) + REPORT_CONFIG = ( + AttrReportConfig(attr="lock_state", config=REPORT_CONFIG_IMMEDIATE), + ) async def async_update(self): """Retrieve latest state.""" @@ -121,7 +123,9 @@ class WindowCovering(ZigbeeChannel): _value_attribute = 8 REPORT_CONFIG = ( - {"attr": "current_position_lift_percentage", "config": REPORT_CONFIG_IMMEDIATE}, + AttrReportConfig( + attr="current_position_lift_percentage", config=REPORT_CONFIG_IMMEDIATE + ), ) async def async_update(self): diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index aa292013081..8886085bf47 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -27,7 +27,7 @@ from ..const import ( SIGNAL_SET_LEVEL, SIGNAL_UPDATE_DEVICE, ) -from .base import ClientChannel, ZigbeeChannel, parse_and_log_command +from .base import AttrReportConfig, ClientChannel, ZigbeeChannel, parse_and_log_command if TYPE_CHECKING: from . import ChannelPool @@ -42,7 +42,9 @@ class Alarms(ZigbeeChannel): class AnalogInput(ZigbeeChannel): """Analog Input channel.""" - REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + REPORT_CONFIG = ( + AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT), + ) @registries.BINDABLE_CLUSTERS.register(general.AnalogOutput.cluster_id) @@ -50,7 +52,9 @@ class AnalogInput(ZigbeeChannel): class AnalogOutput(ZigbeeChannel): """Analog Output channel.""" - REPORT_CONFIG = ({"attr": "present_value", "config": REPORT_CONFIG_DEFAULT},) + REPORT_CONFIG = ( + AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT), + ) ZCL_INIT_ATTRS = { "min_present_value": True, "max_present_value": True, @@ -119,7 +123,9 @@ class AnalogOutput(ZigbeeChannel): class AnalogValue(ZigbeeChannel): """Analog Value channel.""" - REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + REPORT_CONFIG = ( + AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT), + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.ApplianceControl.cluster_id) @@ -151,21 +157,27 @@ class BasicChannel(ZigbeeChannel): class BinaryInput(ZigbeeChannel): """Binary Input channel.""" - REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + REPORT_CONFIG = ( + AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT), + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryOutput.cluster_id) class BinaryOutput(ZigbeeChannel): """Binary Output channel.""" - REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + REPORT_CONFIG = ( + AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT), + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryValue.cluster_id) class BinaryValue(ZigbeeChannel): """Binary Value channel.""" - REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + REPORT_CONFIG = ( + AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT), + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Commissioning.cluster_id) @@ -177,12 +189,12 @@ class Commissioning(ZigbeeChannel): class DeviceTemperature(ZigbeeChannel): """Device Temperature channel.""" - REPORT_CONFIG = [ + REPORT_CONFIG = ( { "attr": "current_temperature", "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), - } - ] + }, + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.GreenPowerProxy.cluster_id) @@ -225,7 +237,7 @@ class LevelControlChannel(ZigbeeChannel): """Channel for the LevelControl Zigbee cluster.""" CURRENT_LEVEL = 0 - REPORT_CONFIG = ({"attr": "current_level", "config": REPORT_CONFIG_ASAP},) + REPORT_CONFIG = (AttrReportConfig(attr="current_level", config=REPORT_CONFIG_ASAP),) ZCL_INIT_ATTRS = { "on_off_transition_time": True, "on_level": True, @@ -275,21 +287,27 @@ class LevelControlChannel(ZigbeeChannel): class MultistateInput(ZigbeeChannel): """Multistate Input channel.""" - REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + REPORT_CONFIG = ( + AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT), + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateOutput.cluster_id) class MultistateOutput(ZigbeeChannel): """Multistate Output channel.""" - REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + REPORT_CONFIG = ( + AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT), + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateValue.cluster_id) class MultistateValue(ZigbeeChannel): """Multistate Value channel.""" - REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + REPORT_CONFIG = ( + AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT), + ) @registries.CLIENT_CHANNELS_REGISTRY.register(general.OnOff.cluster_id) @@ -303,7 +321,7 @@ class OnOffChannel(ZigbeeChannel): """Channel for the OnOff Zigbee cluster.""" ON_OFF = 0 - REPORT_CONFIG = ({"attr": "on_off", "config": REPORT_CONFIG_IMMEDIATE},) + REPORT_CONFIG = (AttrReportConfig(attr="on_off", config=REPORT_CONFIG_IMMEDIATE),) ZCL_INIT_ATTRS = { "start_up_on_off": True, } @@ -472,8 +490,10 @@ class PowerConfigurationChannel(ZigbeeChannel): """Channel for the zigbee power configuration cluster.""" REPORT_CONFIG = ( - {"attr": "battery_voltage", "config": REPORT_CONFIG_BATTERY_SAVE}, - {"attr": "battery_percentage_remaining", "config": REPORT_CONFIG_BATTERY_SAVE}, + AttrReportConfig(attr="battery_voltage", config=REPORT_CONFIG_BATTERY_SAVE), + AttrReportConfig( + attr="battery_percentage_remaining", config=REPORT_CONFIG_BATTERY_SAVE + ), ) def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine: diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index 60c33c93003..52036706f19 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -12,7 +12,7 @@ from ..const import ( REPORT_CONFIG_OP, SIGNAL_ATTR_UPDATED, ) -from .base import ZigbeeChannel +from .base import AttrReportConfig, ZigbeeChannel @registries.ZIGBEE_CHANNEL_REGISTRY.register( @@ -63,15 +63,15 @@ class ElectricalMeasurementChannel(ZigbeeChannel): POWER_QUALITY_MEASUREMENT = 256 REPORT_CONFIG = ( - {"attr": "active_power", "config": REPORT_CONFIG_OP}, - {"attr": "active_power_max", "config": REPORT_CONFIG_DEFAULT}, - {"attr": "apparent_power", "config": REPORT_CONFIG_OP}, - {"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}, - {"attr": "ac_frequency", "config": REPORT_CONFIG_OP}, - {"attr": "ac_frequency_max", "config": REPORT_CONFIG_DEFAULT}, + AttrReportConfig(attr="active_power", config=REPORT_CONFIG_OP), + AttrReportConfig(attr="active_power_max", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="apparent_power", config=REPORT_CONFIG_OP), + AttrReportConfig(attr="rms_current", config=REPORT_CONFIG_OP), + AttrReportConfig(attr="rms_current_max", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="rms_voltage", config=REPORT_CONFIG_OP), + AttrReportConfig(attr="rms_voltage_max", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="ac_frequency", config=REPORT_CONFIG_OP), + AttrReportConfig(attr="ac_frequency_max", config=REPORT_CONFIG_DEFAULT), ) ZCL_INIT_ATTRS = { "ac_current_divisor": True, diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 5b102d062cb..53f18a0fd0f 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -22,7 +22,7 @@ from ..const import ( REPORT_CONFIG_OP, SIGNAL_ATTR_UPDATED, ) -from .base import ZigbeeChannel +from .base import AttrReportConfig, ZigbeeChannel AttributeUpdateRecord = namedtuple("AttributeUpdateRecord", "attr_id, attr_name, value") REPORT_CONFIG_CLIMATE = (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 25) @@ -41,7 +41,7 @@ class FanChannel(ZigbeeChannel): _value_attribute = 0 - REPORT_CONFIG = ({"attr": "fan_mode", "config": REPORT_CONFIG_OP},) + REPORT_CONFIG = (AttrReportConfig(attr="fan_mode", config=REPORT_CONFIG_OP),) ZCL_INIT_ATTRS = {"fan_mode_sequence": True} @property @@ -90,17 +90,25 @@ class ThermostatChannel(ZigbeeChannel): """Thermostat channel.""" REPORT_CONFIG = ( - {"attr": "local_temperature", "config": REPORT_CONFIG_CLIMATE}, - {"attr": "occupied_cooling_setpoint", "config": REPORT_CONFIG_CLIMATE}, - {"attr": "occupied_heating_setpoint", "config": REPORT_CONFIG_CLIMATE}, - {"attr": "unoccupied_cooling_setpoint", "config": REPORT_CONFIG_CLIMATE}, - {"attr": "unoccupied_heating_setpoint", "config": REPORT_CONFIG_CLIMATE}, - {"attr": "running_mode", "config": REPORT_CONFIG_CLIMATE}, - {"attr": "running_state", "config": REPORT_CONFIG_CLIMATE_DEMAND}, - {"attr": "system_mode", "config": REPORT_CONFIG_CLIMATE}, - {"attr": "occupancy", "config": REPORT_CONFIG_CLIMATE_DISCRETE}, - {"attr": "pi_cooling_demand", "config": REPORT_CONFIG_CLIMATE_DEMAND}, - {"attr": "pi_heating_demand", "config": REPORT_CONFIG_CLIMATE_DEMAND}, + AttrReportConfig(attr="local_temperature", config=REPORT_CONFIG_CLIMATE), + AttrReportConfig( + attr="occupied_cooling_setpoint", config=REPORT_CONFIG_CLIMATE + ), + AttrReportConfig( + attr="occupied_heating_setpoint", config=REPORT_CONFIG_CLIMATE + ), + AttrReportConfig( + attr="unoccupied_cooling_setpoint", config=REPORT_CONFIG_CLIMATE + ), + AttrReportConfig( + attr="unoccupied_heating_setpoint", config=REPORT_CONFIG_CLIMATE + ), + AttrReportConfig(attr="running_mode", config=REPORT_CONFIG_CLIMATE), + AttrReportConfig(attr="running_state", config=REPORT_CONFIG_CLIMATE_DEMAND), + AttrReportConfig(attr="system_mode", config=REPORT_CONFIG_CLIMATE), + AttrReportConfig(attr="occupancy", config=REPORT_CONFIG_CLIMATE_DISCRETE), + AttrReportConfig(attr="pi_cooling_demand", config=REPORT_CONFIG_CLIMATE_DEMAND), + AttrReportConfig(attr="pi_heating_demand", config=REPORT_CONFIG_CLIMATE_DEMAND), ) ZCL_INIT_ATTRS: dict[int | str, bool] = { "abs_min_heat_setpoint_limit": True, diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index 13d5b4c2742..99e6101b0bd 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -7,7 +7,7 @@ from zigpy.zcl.clusters import lighting from .. import registries from ..const import REPORT_CONFIG_DEFAULT -from .base import ClientChannel, ZigbeeChannel +from .base import AttrReportConfig, ClientChannel, ZigbeeChannel @registries.ZIGBEE_CHANNEL_REGISTRY.register(lighting.Ballast.cluster_id) @@ -29,9 +29,9 @@ class ColorChannel(ZigbeeChannel): CAPABILITIES_COLOR_TEMP = 0x10 UNSUPPORTED_ATTRIBUTE = 0x86 REPORT_CONFIG = ( - {"attr": "current_x", "config": REPORT_CONFIG_DEFAULT}, - {"attr": "current_y", "config": REPORT_CONFIG_DEFAULT}, - {"attr": "color_temperature", "config": REPORT_CONFIG_DEFAULT}, + AttrReportConfig(attr="current_x", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="current_y", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="color_temperature", config=REPORT_CONFIG_DEFAULT), ) MAX_MIREDS: int = 500 MIN_MIREDS: int = 153 diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 144d5736526..0c246e28db7 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -19,7 +19,7 @@ from ..const import ( SIGNAL_ATTR_UPDATED, UNKNOWN, ) -from .base import ClientChannel, ZigbeeChannel +from .base import AttrReportConfig, ClientChannel, ZigbeeChannel if TYPE_CHECKING: from . import ChannelPool @@ -31,12 +31,12 @@ _LOGGER = logging.getLogger(__name__) class SmartThingsHumidity(ZigbeeChannel): """Smart Things Humidity channel.""" - REPORT_CONFIG = [ + REPORT_CONFIG = ( { "attr": "measured_value", "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), - } - ] + }, + ) @registries.CHANNEL_ONLY_CLUSTERS.register(0xFD00) @@ -44,7 +44,7 @@ class SmartThingsHumidity(ZigbeeChannel): class OsramButton(ZigbeeChannel): """Osram button channel.""" - REPORT_CONFIG = [] + REPORT_CONFIG = () @registries.CHANNEL_ONLY_CLUSTERS.register(registries.PHILLIPS_REMOTE_CLUSTER) @@ -52,7 +52,7 @@ class OsramButton(ZigbeeChannel): class PhillipsRemote(ZigbeeChannel): """Phillips remote channel.""" - REPORT_CONFIG = [] + REPORT_CONFIG = () @registries.CHANNEL_ONLY_CLUSTERS.register(0xFCC0) @@ -60,7 +60,7 @@ class PhillipsRemote(ZigbeeChannel): class OppleRemote(ZigbeeChannel): """Opple button channel.""" - REPORT_CONFIG = [] + REPORT_CONFIG = () def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: """Initialize Opple channel.""" @@ -87,12 +87,12 @@ class OppleRemote(ZigbeeChannel): class SmartThingsAcceleration(ZigbeeChannel): """Smart Things Acceleration channel.""" - REPORT_CONFIG = [ - {"attr": "acceleration", "config": REPORT_CONFIG_ASAP}, - {"attr": "x_axis", "config": REPORT_CONFIG_ASAP}, - {"attr": "y_axis", "config": REPORT_CONFIG_ASAP}, - {"attr": "z_axis", "config": REPORT_CONFIG_ASAP}, - ] + REPORT_CONFIG = ( + AttrReportConfig(attr="acceleration", config=REPORT_CONFIG_ASAP), + AttrReportConfig(attr="x_axis", config=REPORT_CONFIG_ASAP), + AttrReportConfig(attr="y_axis", config=REPORT_CONFIG_ASAP), + AttrReportConfig(attr="z_axis", config=REPORT_CONFIG_ASAP), + ) @callback def attribute_updated(self, attrid, value): @@ -121,4 +121,4 @@ class SmartThingsAcceleration(ZigbeeChannel): class InovelliCluster(ClientChannel): """Inovelli Button Press Event channel.""" - REPORT_CONFIG = [] + REPORT_CONFIG = () diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py index 7368309cf99..fa6f9c07dee 100644 --- a/homeassistant/components/zha/core/channels/measurement.py +++ b/homeassistant/components/zha/core/channels/measurement.py @@ -8,14 +8,16 @@ from ..const import ( REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MIN_INT, ) -from .base import ZigbeeChannel +from .base import AttrReportConfig, ZigbeeChannel @registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.FlowMeasurement.cluster_id) class FlowMeasurement(ZigbeeChannel): """Flow Measurement channel.""" - REPORT_CONFIG = [{"attr": "measured_value", "config": REPORT_CONFIG_DEFAULT}] + REPORT_CONFIG = ( + AttrReportConfig(attr="measured_value", config=REPORT_CONFIG_DEFAULT), + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register( @@ -24,7 +26,9 @@ class FlowMeasurement(ZigbeeChannel): class IlluminanceLevelSensing(ZigbeeChannel): """Illuminance Level Sensing channel.""" - REPORT_CONFIG = [{"attr": "level_status", "config": REPORT_CONFIG_DEFAULT}] + REPORT_CONFIG = ( + AttrReportConfig(attr="level_status", config=REPORT_CONFIG_DEFAULT), + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register( @@ -33,57 +37,63 @@ class IlluminanceLevelSensing(ZigbeeChannel): class IlluminanceMeasurement(ZigbeeChannel): """Illuminance Measurement channel.""" - REPORT_CONFIG = [{"attr": "measured_value", "config": REPORT_CONFIG_DEFAULT}] + REPORT_CONFIG = ( + AttrReportConfig(attr="measured_value", config=REPORT_CONFIG_DEFAULT), + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.OccupancySensing.cluster_id) class OccupancySensing(ZigbeeChannel): """Occupancy Sensing channel.""" - REPORT_CONFIG = [{"attr": "occupancy", "config": REPORT_CONFIG_IMMEDIATE}] + REPORT_CONFIG = ( + AttrReportConfig(attr="occupancy", config=REPORT_CONFIG_IMMEDIATE), + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.PressureMeasurement.cluster_id) class PressureMeasurement(ZigbeeChannel): """Pressure measurement channel.""" - REPORT_CONFIG = [{"attr": "measured_value", "config": REPORT_CONFIG_DEFAULT}] + REPORT_CONFIG = ( + AttrReportConfig(attr="measured_value", config=REPORT_CONFIG_DEFAULT), + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.RelativeHumidity.cluster_id) class RelativeHumidity(ZigbeeChannel): """Relative Humidity measurement channel.""" - REPORT_CONFIG = [ - { - "attr": "measured_value", - "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 100), - } - ] + REPORT_CONFIG = ( + AttrReportConfig( + attr="measured_value", + config=(REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 100), + ), + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.SoilMoisture.cluster_id) class SoilMoisture(ZigbeeChannel): """Soil Moisture measurement channel.""" - REPORT_CONFIG = [ - { - "attr": "measured_value", - "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 100), - } - ] + REPORT_CONFIG = ( + AttrReportConfig( + attr="measured_value", + config=(REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 100), + ), + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.LeafWetness.cluster_id) class LeafWetness(ZigbeeChannel): """Leaf Wetness measurement channel.""" - REPORT_CONFIG = [ - { - "attr": "measured_value", - "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 100), - } - ] + REPORT_CONFIG = ( + AttrReportConfig( + attr="measured_value", + config=(REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 100), + ), + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register( @@ -92,12 +102,12 @@ class LeafWetness(ZigbeeChannel): class TemperatureMeasurement(ZigbeeChannel): """Temperature measurement channel.""" - REPORT_CONFIG = [ - { - "attr": "measured_value", - "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), - } - ] + REPORT_CONFIG = ( + AttrReportConfig( + attr="measured_value", + config=(REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), + ), + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register( @@ -106,12 +116,12 @@ class TemperatureMeasurement(ZigbeeChannel): class CarbonMonoxideConcentration(ZigbeeChannel): """Carbon Monoxide measurement channel.""" - REPORT_CONFIG = [ - { - "attr": "measured_value", - "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 0.000001), - } - ] + REPORT_CONFIG = ( + AttrReportConfig( + attr="measured_value", + config=(REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 0.000001), + ), + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register( @@ -120,24 +130,24 @@ class CarbonMonoxideConcentration(ZigbeeChannel): class CarbonDioxideConcentration(ZigbeeChannel): """Carbon Dioxide measurement channel.""" - REPORT_CONFIG = [ - { - "attr": "measured_value", - "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 0.000001), - } - ] + REPORT_CONFIG = ( + AttrReportConfig( + attr="measured_value", + config=(REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 0.000001), + ), + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.PM25.cluster_id) class PM25(ZigbeeChannel): """Particulate Matter 2.5 microns or less measurement channel.""" - REPORT_CONFIG = [ - { - "attr": "measured_value", - "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 0.1), - } - ] + REPORT_CONFIG = ( + AttrReportConfig( + attr="measured_value", + config=(REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 0.1), + ), + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register( @@ -146,9 +156,9 @@ class PM25(ZigbeeChannel): class FormaldehydeConcentration(ZigbeeChannel): """Formaldehyde measurement channel.""" - REPORT_CONFIG = [ - { - "attr": "measured_value", - "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 0.000001), - } - ] + REPORT_CONFIG = ( + AttrReportConfig( + attr="measured_value", + config=(REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 0.000001), + ), + ) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 099571aa69e..731ec003011 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -15,7 +15,7 @@ from ..const import ( REPORT_CONFIG_OP, SIGNAL_ATTR_UPDATED, ) -from .base import ZigbeeChannel +from .base import AttrReportConfig, ZigbeeChannel if TYPE_CHECKING: from . import ChannelPool @@ -66,9 +66,9 @@ class Metering(ZigbeeChannel): """Metering channel.""" REPORT_CONFIG = ( - {"attr": "instantaneous_demand", "config": REPORT_CONFIG_OP}, - {"attr": "current_summ_delivered", "config": REPORT_CONFIG_DEFAULT}, - {"attr": "status", "config": REPORT_CONFIG_ASAP}, + AttrReportConfig(attr="instantaneous_demand", config=REPORT_CONFIG_OP), + AttrReportConfig(attr="current_summ_delivered", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="status", config=REPORT_CONFIG_ASAP), ) ZCL_INIT_ATTRS = { "demand_formatting": True, From f43cc18aa305c9014e9892006497fceb6b851241 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 20 Jun 2022 17:31:16 +0200 Subject: [PATCH 1618/3516] Fix type hints in zha platforms (#73745) * Adjust binary_sensor * Adjust device_action * Adjust device_tracker * Adjust fan * Adjust lock * Adjust siren --- homeassistant/components/zha/binary_sensor.py | 6 ++++-- homeassistant/components/zha/device_action.py | 6 ++++-- homeassistant/components/zha/device_tracker.py | 6 +++--- homeassistant/components/zha/fan.py | 2 +- homeassistant/components/zha/lock.py | 8 ++++---- homeassistant/components/zha/siren.py | 7 ++++--- mypy.ini | 18 ------------------ script/hassfest/mypy_config.py | 6 ------ 8 files changed, 20 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 954c60fa895..8130e7d5f98 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -1,4 +1,6 @@ """Binary sensors on Zigbee Home Automation networks.""" +from __future__ import annotations + import functools from homeassistant.components.binary_sensor import ( @@ -60,7 +62,7 @@ async def async_setup_entry( class BinarySensor(ZhaEntity, BinarySensorEntity): """ZHA BinarySensor.""" - SENSOR_ATTR = None + SENSOR_ATTR: str | None = None def __init__(self, unique_id, zha_device, channels, **kwargs): """Initialize the ZHA binary sensor.""" @@ -161,7 +163,7 @@ class IASZone(BinarySensor): SENSOR_ATTR = "zone_status" @property - def device_class(self) -> str: + def device_class(self) -> BinarySensorDeviceClass | None: """Return device class from component DEVICE_CLASSES.""" return CLASS_MAPPING.get(self._channel.cluster.get("zone_type")) diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 049ffbd40f3..3ee8694b09c 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -1,6 +1,8 @@ """Provides device actions for ZHA devices.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE @@ -82,9 +84,9 @@ async def async_get_actions( async def _execute_service_based_action( hass: HomeAssistant, - config: ACTION_SCHEMA, + config: dict[str, Any], variables: TemplateVarsType, - context: Context, + context: Context | None, ) -> None: action_type = config[CONF_TYPE] service_name = SERVICE_NAMES[action_type] diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index c08491ab782..cf4a830f4da 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -107,10 +107,10 @@ class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity): """ return self._battery_level - @property + @property # type: ignore[misc] def device_info( # pylint: disable=overridden-final-method self, - ) -> DeviceInfo | None: + ) -> DeviceInfo: """Return device info.""" # We opt ZHA device tracker back into overriding this method because # it doesn't track IP-based devices. @@ -118,7 +118,7 @@ class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity): return super(ZhaEntity, self).device_info @property - def unique_id(self) -> str | None: + def unique_id(self) -> str: """Return unique ID.""" # Call Super because ScannerEntity overrode it. return super(ZhaEntity, self).unique_id diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 1c2f52c6038..a4b9baa5f0a 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -97,7 +97,7 @@ class BaseFan(FanEntity): """Turn the entity off.""" await self.async_set_percentage(0) - async def async_set_percentage(self, percentage: int | None) -> None: + async def async_set_percentage(self, percentage: int) -> None: """Set the speed percenage of the fan.""" fan_mode = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) await self._async_set_fan_mode(fan_mode) diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index b26d7087b75..449fd1089eb 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -53,7 +53,7 @@ async def async_setup_entry( platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_SET_LOCK_USER_CODE, { vol.Required("code_slot"): vol.Coerce(int), @@ -62,7 +62,7 @@ async def async_setup_entry( "async_set_lock_user_code", ) - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_ENABLE_LOCK_USER_CODE, { vol.Required("code_slot"): vol.Coerce(int), @@ -70,7 +70,7 @@ async def async_setup_entry( "async_enable_lock_user_code", ) - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_DISABLE_LOCK_USER_CODE, { vol.Required("code_slot"): vol.Coerce(int), @@ -78,7 +78,7 @@ async def async_setup_entry( "async_disable_lock_user_code", ) - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_CLEAR_LOCK_USER_CODE, { vol.Required("code_slot"): vol.Coerce(int), diff --git a/homeassistant/components/zha/siren.py b/homeassistant/components/zha/siren.py index b509f9585db..66cd2bf4002 100644 --- a/homeassistant/components/zha/siren.py +++ b/homeassistant/components/zha/siren.py @@ -1,8 +1,9 @@ """Support for ZHA sirens.""" from __future__ import annotations +from collections.abc import Callable import functools -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from zigpy.zcl.clusters.security import IasWd as WD @@ -96,9 +97,9 @@ class ZHASiren(ZhaEntity, SirenEntity): WARNING_DEVICE_MODE_EMERGENCY_PANIC: "Emergency Panic", } super().__init__(unique_id, zha_device, channels, **kwargs) - self._channel: IasWd = channels[0] + self._channel: IasWd = cast(IasWd, channels[0]) self._attr_is_on: bool = False - self._off_listener = None + self._off_listener: Callable[[], None] | None = None async def async_turn_on(self, **kwargs: Any) -> None: """Turn on siren.""" diff --git a/mypy.ini b/mypy.ini index d5b5ca39183..27aa6653357 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2988,9 +2988,6 @@ ignore_errors = true [mypy-homeassistant.components.zha.api] ignore_errors = true -[mypy-homeassistant.components.zha.binary_sensor] -ignore_errors = true - [mypy-homeassistant.components.zha.button] ignore_errors = true @@ -3051,32 +3048,17 @@ ignore_errors = true [mypy-homeassistant.components.zha.cover] ignore_errors = true -[mypy-homeassistant.components.zha.device_action] -ignore_errors = true - -[mypy-homeassistant.components.zha.device_tracker] -ignore_errors = true - [mypy-homeassistant.components.zha.entity] ignore_errors = true -[mypy-homeassistant.components.zha.fan] -ignore_errors = true - [mypy-homeassistant.components.zha.light] ignore_errors = true -[mypy-homeassistant.components.zha.lock] -ignore_errors = true - [mypy-homeassistant.components.zha.select] ignore_errors = true [mypy-homeassistant.components.zha.sensor] ignore_errors = true -[mypy-homeassistant.components.zha.siren] -ignore_errors = true - [mypy-homeassistant.components.zha.switch] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index bb46ba39212..a16faf7f34e 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -145,7 +145,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.xiaomi_miio.sensor", "homeassistant.components.xiaomi_miio.switch", "homeassistant.components.zha.api", - "homeassistant.components.zha.binary_sensor", "homeassistant.components.zha.button", "homeassistant.components.zha.climate", "homeassistant.components.zha.core.channels.base", @@ -166,15 +165,10 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.zha.core.registries", "homeassistant.components.zha.core.store", "homeassistant.components.zha.cover", - "homeassistant.components.zha.device_action", - "homeassistant.components.zha.device_tracker", "homeassistant.components.zha.entity", - "homeassistant.components.zha.fan", "homeassistant.components.zha.light", - "homeassistant.components.zha.lock", "homeassistant.components.zha.select", "homeassistant.components.zha.sensor", - "homeassistant.components.zha.siren", "homeassistant.components.zha.switch", ] From 66b02ecff038672df3aa5b53a408c79e26236bae Mon Sep 17 00:00:00 2001 From: Gordon Allott Date: Mon, 20 Jun 2022 19:27:39 +0100 Subject: [PATCH 1619/3516] Ensure metoffice daily are returned once daily (#72440) * ensure metoffice daily are returned once daily * Fixes metoffice tests for MODE_DAILY --- homeassistant/components/metoffice/helpers.py | 4 ++ tests/components/metoffice/test_weather.py | 45 ++++++++++--------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/metoffice/helpers.py b/homeassistant/components/metoffice/helpers.py index 31ac4c141a9..00d5e73501d 100644 --- a/homeassistant/components/metoffice/helpers.py +++ b/homeassistant/components/metoffice/helpers.py @@ -7,6 +7,7 @@ import datapoint from homeassistant.helpers.update_coordinator import UpdateFailed from homeassistant.util.dt import utcnow +from .const import MODE_3HOURLY from .data import MetOfficeData _LOGGER = logging.getLogger(__name__) @@ -39,6 +40,9 @@ def fetch_data(connection: datapoint.Manager, site, mode) -> MetOfficeData: for day in forecast.days for timestep in day.timesteps if timestep.date > time_now + and ( + mode == MODE_3HOURLY or timestep.date.hour > 6 + ) # ensures only one result per day in MODE_DAILY ], site, ) diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index fb00203661b..bf279ff3cf7 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -163,16 +163,17 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.attributes.get("humidity") == 50 # Also has Forecasts added - again, just pick out 1 entry to check - assert len(weather.attributes.get("forecast")) == 8 + # ensures that daily filters out multiple results per day + assert len(weather.attributes.get("forecast")) == 4 assert ( - weather.attributes.get("forecast")[7]["datetime"] == "2020-04-29T12:00:00+00:00" + weather.attributes.get("forecast")[3]["datetime"] == "2020-04-29T12:00:00+00:00" ) - assert weather.attributes.get("forecast")[7]["condition"] == "rainy" - assert weather.attributes.get("forecast")[7]["precipitation_probability"] == 59 - assert weather.attributes.get("forecast")[7]["temperature"] == 13 - assert weather.attributes.get("forecast")[7]["wind_speed"] == 13 - assert weather.attributes.get("forecast")[7]["wind_bearing"] == "SE" + assert weather.attributes.get("forecast")[3]["condition"] == "rainy" + assert weather.attributes.get("forecast")[3]["precipitation_probability"] == 59 + assert weather.attributes.get("forecast")[3]["temperature"] == 13 + assert weather.attributes.get("forecast")[3]["wind_speed"] == 13 + assert weather.attributes.get("forecast")[3]["wind_bearing"] == "SE" @freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.timezone.utc)) @@ -258,16 +259,17 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("humidity") == 50 # Also has Forecasts added - again, just pick out 1 entry to check - assert len(weather.attributes.get("forecast")) == 8 + # ensures that daily filters out multiple results per day + assert len(weather.attributes.get("forecast")) == 4 assert ( - weather.attributes.get("forecast")[7]["datetime"] == "2020-04-29T12:00:00+00:00" + weather.attributes.get("forecast")[3]["datetime"] == "2020-04-29T12:00:00+00:00" ) - assert weather.attributes.get("forecast")[7]["condition"] == "rainy" - assert weather.attributes.get("forecast")[7]["precipitation_probability"] == 59 - assert weather.attributes.get("forecast")[7]["temperature"] == 13 - assert weather.attributes.get("forecast")[7]["wind_speed"] == 13 - assert weather.attributes.get("forecast")[7]["wind_bearing"] == "SE" + assert weather.attributes.get("forecast")[3]["condition"] == "rainy" + assert weather.attributes.get("forecast")[3]["precipitation_probability"] == 59 + assert weather.attributes.get("forecast")[3]["temperature"] == 13 + assert weather.attributes.get("forecast")[3]["wind_speed"] == 13 + assert weather.attributes.get("forecast")[3]["wind_bearing"] == "SE" # King's Lynn 3-hourly weather platform expected results weather = hass.states.get("weather.met_office_king_s_lynn_3_hourly") @@ -305,13 +307,14 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("humidity") == 75 # All should have Forecast added - again, just picking out 1 entry to check - assert len(weather.attributes.get("forecast")) == 8 + # ensures daily filters out multiple results per day + assert len(weather.attributes.get("forecast")) == 4 assert ( - weather.attributes.get("forecast")[5]["datetime"] == "2020-04-28T12:00:00+00:00" + weather.attributes.get("forecast")[2]["datetime"] == "2020-04-28T12:00:00+00:00" ) - assert weather.attributes.get("forecast")[5]["condition"] == "cloudy" - assert weather.attributes.get("forecast")[5]["precipitation_probability"] == 14 - assert weather.attributes.get("forecast")[5]["temperature"] == 11 - assert weather.attributes.get("forecast")[5]["wind_speed"] == 7 - assert weather.attributes.get("forecast")[5]["wind_bearing"] == "ESE" + assert weather.attributes.get("forecast")[2]["condition"] == "cloudy" + assert weather.attributes.get("forecast")[2]["precipitation_probability"] == 14 + assert weather.attributes.get("forecast")[2]["temperature"] == 11 + assert weather.attributes.get("forecast")[2]["wind_speed"] == 7 + assert weather.attributes.get("forecast")[2]["wind_bearing"] == "ESE" From 16e7593a7b0f37e69b3d8582e241b0994dc2ce48 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 20 Jun 2022 20:29:50 +0200 Subject: [PATCH 1620/3516] Add state class to Flipr sensors (#73747) --- homeassistant/components/flipr/sensor.py | 5 +++++ tests/components/flipr/test_sensor.py | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/homeassistant/components/flipr/sensor.py b/homeassistant/components/flipr/sensor.py index e78031bd5cb..9cf788d7170 100644 --- a/homeassistant/components/flipr/sensor.py +++ b/homeassistant/components/flipr/sensor.py @@ -5,6 +5,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ELECTRIC_POTENTIAL_MILLIVOLT, TEMP_CELSIUS @@ -20,17 +21,20 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Chlorine", native_unit_of_measurement=ELECTRIC_POTENTIAL_MILLIVOLT, icon="mdi:pool", + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="ph", name="pH", icon="mdi:pool", + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="temperature", name="Water Temp", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="date_time", @@ -42,6 +46,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Red OX", native_unit_of_measurement=ELECTRIC_POTENTIAL_MILLIVOLT, icon="mdi:pool", + state_class=SensorStateClass.MEASUREMENT, ), ) diff --git a/tests/components/flipr/test_sensor.py b/tests/components/flipr/test_sensor.py index c5ab3dc1541..30468064dae 100644 --- a/tests/components/flipr/test_sensor.py +++ b/tests/components/flipr/test_sensor.py @@ -5,6 +5,7 @@ from unittest.mock import patch from flipr_api.exceptions import FliprError from homeassistant.components.flipr.const import CONF_FLIPR_ID, DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS, SensorStateClass from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, @@ -62,30 +63,35 @@ async def test_sensors(hass: HomeAssistant) -> None: assert state assert state.attributes.get(ATTR_ICON) == "mdi:pool" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.state == "7.03" state = hass.states.get("sensor.flipr_myfliprid_water_temp") assert state assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is TEMP_CELSIUS + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.state == "10.5" state = hass.states.get("sensor.flipr_myfliprid_last_measured") assert state assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.state == "2021-02-15T09:10:32+00:00" state = hass.states.get("sensor.flipr_myfliprid_red_ox") assert state assert state.attributes.get(ATTR_ICON) == "mdi:pool" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "mV" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.state == "657.58" state = hass.states.get("sensor.flipr_myfliprid_chlorine") assert state assert state.attributes.get(ATTR_ICON) == "mdi:pool" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "mV" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.state == "0.23654886" From c98419b031c93d00b83efad673c83221cba32b24 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 20 Jun 2022 13:59:29 -0500 Subject: [PATCH 1621/3516] Bump soco to 0.28.0 (#73750) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 9144ca559f2..b8506cd2783 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["soco==0.27.1"], + "requirements": ["soco==0.28.0"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "spotify", "zeroconf", "media_source"], "zeroconf": ["_sonos._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index 1ffbe1a76ef..5f512cdd088 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2186,7 +2186,7 @@ smhi-pkg==1.0.15 snapcast==2.1.3 # homeassistant.components.sonos -soco==0.27.1 +soco==0.28.0 # homeassistant.components.solaredge_local solaredge-local==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e4acef811f9..660d41d66eb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1443,7 +1443,7 @@ smart-meter-texas==0.4.7 smhi-pkg==1.0.15 # homeassistant.components.sonos -soco==0.27.1 +soco==0.28.0 # homeassistant.components.solaredge solaredge==0.0.2 From 6cf3c0ede29d0af9bb173738d7b800e25c4097cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jun 2022 21:00:00 +0200 Subject: [PATCH 1622/3516] Bump home-assistant/builder from 2022.03.1 to 2022.06.1 (#73466) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 664f9b3910e..62cbee9321c 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -135,7 +135,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.03.1 + uses: home-assistant/builder@2022.06.1 with: args: | $BUILD_ARGS \ @@ -200,7 +200,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.03.1 + uses: home-assistant/builder@2022.06.1 with: args: | $BUILD_ARGS \ From 55eca2e2b4ebcf11486749035fd3c7e77ea14b8f Mon Sep 17 00:00:00 2001 From: Nate Harris Date: Mon, 20 Jun 2022 13:04:31 -0600 Subject: [PATCH 1623/3516] Bump pycketcasts to 1.0.1 (#73262) * Replace outdated pocketcast dependency * Fix pycketcasts in requirements_all.txt * Fix pycketcasts in requirements_all.txt * Fix pycketcasts in requirements_all.txt --- homeassistant/components/pocketcasts/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/pocketcasts/manifest.json b/homeassistant/components/pocketcasts/manifest.json index f74c77ed3a9..8ee74496447 100644 --- a/homeassistant/components/pocketcasts/manifest.json +++ b/homeassistant/components/pocketcasts/manifest.json @@ -2,7 +2,7 @@ "domain": "pocketcasts", "name": "Pocket Casts", "documentation": "https://www.home-assistant.io/integrations/pocketcasts", - "requirements": ["pycketcasts==1.0.0"], + "requirements": ["pycketcasts==1.0.1"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["pycketcasts"] diff --git a/requirements_all.txt b/requirements_all.txt index 5f512cdd088..013ef770615 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1411,7 +1411,7 @@ pychannels==1.0.0 pychromecast==12.1.3 # homeassistant.components.pocketcasts -pycketcasts==1.0.0 +pycketcasts==1.0.1 # homeassistant.components.climacell pyclimacell==0.18.2 From 4bc13144995f0a68d24376aac375becce5ca92b7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 21 Jun 2022 00:32:32 +0200 Subject: [PATCH 1624/3516] Fix REPORT_CONFIG type hint in zha (#73762) Fix REPORT_CONFIG type hint --- homeassistant/components/zha/core/channels/base.py | 2 +- mypy.ini | 12 ------------ script/hassfest/mypy_config.py | 4 ---- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index c9472af5938..de943ebac16 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -109,7 +109,7 @@ class ChannelStatus(Enum): class ZigbeeChannel(LogMixin): """Base channel for a Zigbee cluster.""" - REPORT_CONFIG: tuple[AttrReportConfig] = () + REPORT_CONFIG: tuple[AttrReportConfig, ...] = () BIND: bool = True # Dict of attributes to read on channel initialization. diff --git a/mypy.ini b/mypy.ini index 27aa6653357..98884d333e7 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2997,9 +2997,6 @@ ignore_errors = true [mypy-homeassistant.components.zha.core.channels.base] ignore_errors = true -[mypy-homeassistant.components.zha.core.channels.closures] -ignore_errors = true - [mypy-homeassistant.components.zha.core.channels.general] ignore_errors = true @@ -3009,15 +3006,6 @@ ignore_errors = true [mypy-homeassistant.components.zha.core.channels.hvac] ignore_errors = true -[mypy-homeassistant.components.zha.core.channels.lighting] -ignore_errors = true - -[mypy-homeassistant.components.zha.core.channels.manufacturerspecific] -ignore_errors = true - -[mypy-homeassistant.components.zha.core.channels.measurement] -ignore_errors = true - [mypy-homeassistant.components.zha.core.channels.security] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index a16faf7f34e..b519e7f2daf 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -148,13 +148,9 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.zha.button", "homeassistant.components.zha.climate", "homeassistant.components.zha.core.channels.base", - "homeassistant.components.zha.core.channels.closures", "homeassistant.components.zha.core.channels.general", "homeassistant.components.zha.core.channels.homeautomation", "homeassistant.components.zha.core.channels.hvac", - "homeassistant.components.zha.core.channels.lighting", - "homeassistant.components.zha.core.channels.manufacturerspecific", - "homeassistant.components.zha.core.channels.measurement", "homeassistant.components.zha.core.channels.security", "homeassistant.components.zha.core.channels.smartenergy", "homeassistant.components.zha.core.device", From b956d125f90531b69392c873244b5a1509d446f7 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Mon, 20 Jun 2022 20:10:59 -0400 Subject: [PATCH 1625/3516] Fix UniFi Protect write rate sensor (#73759) --- homeassistant/components/unifiprotect/sensor.py | 2 +- tests/components/unifiprotect/test_sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index c30cc7fb80f..48337dc416b 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -171,7 +171,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( native_unit_of_measurement=DATA_RATE_BYTES_PER_SECOND, entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, - ufp_value="stats.storage.rate", + ufp_value="stats.storage.rate_per_second", precision=2, ), ProtectSensorEntityDescription( diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index dff746c167f..4f2540b28c4 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -159,7 +159,7 @@ async def camera_fixture( camera_obj.stats.video.recording_start = now camera_obj.stats.storage.used = 100.0 camera_obj.stats.storage.used = 100.0 - camera_obj.stats.storage.rate = 100.0 + camera_obj.stats.storage.rate = 0.1 camera_obj.voltage = 20.0 mock_entry.api.bootstrap.reset_objects() From 109d1844b3a6b870cfb13af78627d49a7faa2aeb Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 21 Jun 2022 00:22:51 +0000 Subject: [PATCH 1626/3516] [ci skip] Translation update --- .../components/abode/translations/sv.json | 5 +++++ .../aladdin_connect/translations/sv.json | 11 ++++++++++ .../components/blink/translations/sv.json | 11 ++++++++++ .../components/brunt/translations/sv.json | 11 ++++++++++ .../components/bsblan/translations/sv.json | 7 +++++++ .../components/canary/translations/sv.json | 11 ++++++++++ .../components/control4/translations/sv.json | 11 ++++++++++ .../components/deluge/translations/sv.json | 11 ++++++++++ .../devolo_home_control/translations/sv.json | 5 +++++ .../components/dexcom/translations/sv.json | 11 ++++++++++ .../eight_sleep/translations/sv.json | 12 +++++++++++ .../components/elkm1/translations/sv.json | 12 +++++++++++ .../components/elmax/translations/sv.json | 11 ++++++++++ .../components/fibaro/translations/sv.json | 11 ++++++++++ .../fireservicerota/translations/sv.json | 11 ++++++++++ .../components/flo/translations/sv.json | 11 ++++++++++ .../components/foscam/translations/sv.json | 11 ++++++++++ .../components/fritz/translations/sv.json | 21 +++++++++++++++++++ .../components/fritzbox/translations/sv.json | 5 +++++ .../fritzbox_callmonitor/translations/sv.json | 11 ++++++++++ .../components/generic/translations/sv.json | 16 ++++++++++++++ .../components/gogogate2/translations/sv.json | 11 ++++++++++ .../growatt_server/translations/sv.json | 11 ++++++++++ .../components/honeywell/translations/sv.json | 11 ++++++++++ .../huisbaasje/translations/sv.json | 7 +++++++ .../hvv_departures/translations/sv.json | 9 ++++++++ .../components/insteon/translations/sv.json | 16 ++++++++++++++ .../intellifire/translations/sv.json | 7 +++++++ .../components/iotawatt/translations/sv.json | 11 ++++++++++ .../components/jellyfin/translations/sv.json | 11 ++++++++++ .../components/kmtronic/translations/sv.json | 11 ++++++++++ .../components/kodi/translations/sv.json | 11 ++++++++++ .../litterrobot/translations/sv.json | 11 ++++++++++ .../components/meater/translations/sv.json | 3 +++ .../components/mill/translations/sv.json | 5 +++++ .../components/mjpeg/translations/sv.json | 20 ++++++++++++++++++ .../components/motioneye/translations/sv.json | 11 ++++++++++ .../components/nam/translations/sv.json | 10 +++++++++ .../components/netgear/translations/sv.json | 11 ++++++++++ .../components/nzbget/translations/sv.json | 11 ++++++++++ .../components/octoprint/translations/sv.json | 3 ++- .../components/omnilogic/translations/sv.json | 11 ++++++++++ .../components/onvif/translations/sv.json | 5 +++++ .../components/overkiz/translations/sv.json | 7 +++++++ .../components/picnic/translations/sv.json | 11 ++++++++++ .../plum_lightpad/translations/sv.json | 11 ++++++++++ .../components/prosegur/translations/sv.json | 16 ++++++++++++++ .../components/qnap_qsw/translations/sv.json | 7 +++++++ .../components/renault/translations/sv.json | 11 ++++++++++ .../components/ridwell/translations/sv.json | 11 ++++++++++ .../components/risco/translations/sv.json | 11 ++++++++++ .../ruckus_unleashed/translations/sv.json | 11 ++++++++++ .../components/scrape/translations/sv.json | 20 ++++++++++++++++++ .../components/sharkiq/translations/sv.json | 5 +++++ .../components/shelly/translations/sv.json | 11 ++++++++++ .../components/slack/translations/sv.json | 11 ++++++++++ .../components/sleepiq/translations/sv.json | 11 ++++++++++ .../smart_meter_texas/translations/sv.json | 11 ++++++++++ .../components/spider/translations/sv.json | 11 ++++++++++ .../squeezebox/translations/sv.json | 11 ++++++++++ .../components/subaru/translations/sv.json | 11 ++++++++++ .../surepetcare/translations/sv.json | 11 ++++++++++ .../synology_dsm/translations/sv.json | 5 +++++ .../components/tile/translations/sv.json | 11 ++++++++++ .../transmission/translations/ca.json | 10 ++++++++- .../transmission/translations/et.json | 10 ++++++++- .../transmission/translations/fr.json | 10 ++++++++- .../transmission/translations/hu.json | 10 ++++++++- .../unifiprotect/translations/sv.json | 11 ++++++++++ .../components/upcloud/translations/sv.json | 11 ++++++++++ .../components/venstar/translations/sv.json | 11 ++++++++++ .../components/vicare/translations/sv.json | 11 ++++++++++ .../components/wallbox/translations/sv.json | 16 ++++++++++++++ .../components/watttime/translations/sv.json | 11 ++++++++++ .../components/whirlpool/translations/sv.json | 11 ++++++++++ .../xiaomi_miio/translations/sv.json | 3 ++- .../yale_smart_alarm/translations/sv.json | 16 ++++++++++++++ .../zoneminder/translations/sv.json | 1 + 78 files changed, 804 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/aladdin_connect/translations/sv.json create mode 100644 homeassistant/components/blink/translations/sv.json create mode 100644 homeassistant/components/brunt/translations/sv.json create mode 100644 homeassistant/components/canary/translations/sv.json create mode 100644 homeassistant/components/control4/translations/sv.json create mode 100644 homeassistant/components/deluge/translations/sv.json create mode 100644 homeassistant/components/dexcom/translations/sv.json create mode 100644 homeassistant/components/eight_sleep/translations/sv.json create mode 100644 homeassistant/components/elmax/translations/sv.json create mode 100644 homeassistant/components/fibaro/translations/sv.json create mode 100644 homeassistant/components/fireservicerota/translations/sv.json create mode 100644 homeassistant/components/flo/translations/sv.json create mode 100644 homeassistant/components/foscam/translations/sv.json create mode 100644 homeassistant/components/fritz/translations/sv.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/sv.json create mode 100644 homeassistant/components/gogogate2/translations/sv.json create mode 100644 homeassistant/components/growatt_server/translations/sv.json create mode 100644 homeassistant/components/honeywell/translations/sv.json create mode 100644 homeassistant/components/iotawatt/translations/sv.json create mode 100644 homeassistant/components/jellyfin/translations/sv.json create mode 100644 homeassistant/components/kmtronic/translations/sv.json create mode 100644 homeassistant/components/kodi/translations/sv.json create mode 100644 homeassistant/components/litterrobot/translations/sv.json create mode 100644 homeassistant/components/mjpeg/translations/sv.json create mode 100644 homeassistant/components/motioneye/translations/sv.json create mode 100644 homeassistant/components/netgear/translations/sv.json create mode 100644 homeassistant/components/nzbget/translations/sv.json create mode 100644 homeassistant/components/omnilogic/translations/sv.json create mode 100644 homeassistant/components/picnic/translations/sv.json create mode 100644 homeassistant/components/plum_lightpad/translations/sv.json create mode 100644 homeassistant/components/prosegur/translations/sv.json create mode 100644 homeassistant/components/renault/translations/sv.json create mode 100644 homeassistant/components/ridwell/translations/sv.json create mode 100644 homeassistant/components/risco/translations/sv.json create mode 100644 homeassistant/components/ruckus_unleashed/translations/sv.json create mode 100644 homeassistant/components/scrape/translations/sv.json create mode 100644 homeassistant/components/shelly/translations/sv.json create mode 100644 homeassistant/components/slack/translations/sv.json create mode 100644 homeassistant/components/sleepiq/translations/sv.json create mode 100644 homeassistant/components/smart_meter_texas/translations/sv.json create mode 100644 homeassistant/components/spider/translations/sv.json create mode 100644 homeassistant/components/squeezebox/translations/sv.json create mode 100644 homeassistant/components/subaru/translations/sv.json create mode 100644 homeassistant/components/surepetcare/translations/sv.json create mode 100644 homeassistant/components/tile/translations/sv.json create mode 100644 homeassistant/components/upcloud/translations/sv.json create mode 100644 homeassistant/components/venstar/translations/sv.json create mode 100644 homeassistant/components/vicare/translations/sv.json create mode 100644 homeassistant/components/wallbox/translations/sv.json create mode 100644 homeassistant/components/watttime/translations/sv.json create mode 100644 homeassistant/components/whirlpool/translations/sv.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/sv.json diff --git a/homeassistant/components/abode/translations/sv.json b/homeassistant/components/abode/translations/sv.json index 9faf392be51..ef61917ad43 100644 --- a/homeassistant/components/abode/translations/sv.json +++ b/homeassistant/components/abode/translations/sv.json @@ -4,6 +4,11 @@ "single_instance_allowed": "Endast en enda konfiguration av Abode \u00e4r till\u00e5ten." }, "step": { + "reauth_confirm": { + "data": { + "username": "E-postadress" + } + }, "user": { "data": { "password": "L\u00f6senord", diff --git a/homeassistant/components/aladdin_connect/translations/sv.json b/homeassistant/components/aladdin_connect/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/sv.json b/homeassistant/components/blink/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/blink/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brunt/translations/sv.json b/homeassistant/components/brunt/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/brunt/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/sv.json b/homeassistant/components/bsblan/translations/sv.json index 46631acc69a..2d2d9662e4b 100644 --- a/homeassistant/components/bsblan/translations/sv.json +++ b/homeassistant/components/bsblan/translations/sv.json @@ -2,6 +2,13 @@ "config": { "abort": { "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/canary/translations/sv.json b/homeassistant/components/canary/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/canary/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/control4/translations/sv.json b/homeassistant/components/control4/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/control4/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/sv.json b/homeassistant/components/deluge/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/deluge/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/sv.json b/homeassistant/components/devolo_home_control/translations/sv.json index 4479e25b250..13e09780b40 100644 --- a/homeassistant/components/devolo_home_control/translations/sv.json +++ b/homeassistant/components/devolo_home_control/translations/sv.json @@ -7,6 +7,11 @@ "password": "L\u00f6senord", "username": "E-postadress / devolo-ID" } + }, + "zeroconf_confirm": { + "data": { + "username": "E-postadress / devolo-id" + } } } } diff --git a/homeassistant/components/dexcom/translations/sv.json b/homeassistant/components/dexcom/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/dexcom/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/translations/sv.json b/homeassistant/components/eight_sleep/translations/sv.json new file mode 100644 index 00000000000..78879942876 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/sv.json b/homeassistant/components/elkm1/translations/sv.json index 19d9bb17e4b..e49782de873 100644 --- a/homeassistant/components/elkm1/translations/sv.json +++ b/homeassistant/components/elkm1/translations/sv.json @@ -4,6 +4,18 @@ "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "discovered_connection": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + }, + "manual_connection": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/sv.json b/homeassistant/components/elmax/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/elmax/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/sv.json b/homeassistant/components/fibaro/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/fibaro/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/sv.json b/homeassistant/components/fireservicerota/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/sv.json b/homeassistant/components/flo/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/flo/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/sv.json b/homeassistant/components/foscam/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/foscam/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/sv.json b/homeassistant/components/fritz/translations/sv.json new file mode 100644 index 00000000000..02cd1e39b0e --- /dev/null +++ b/homeassistant/components/fritz/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + }, + "reauth_confirm": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + }, + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/sv.json b/homeassistant/components/fritzbox/translations/sv.json index 2d5586b6bc6..b611b3a9893 100644 --- a/homeassistant/components/fritzbox/translations/sv.json +++ b/homeassistant/components/fritzbox/translations/sv.json @@ -8,6 +8,11 @@ }, "description": "Do vill du konfigurera {name}?" }, + "reauth_confirm": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + }, "user": { "data": { "host": "V\u00e4rd eller IP-adress", diff --git a/homeassistant/components/fritzbox_callmonitor/translations/sv.json b/homeassistant/components/fritzbox_callmonitor/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/sv.json b/homeassistant/components/generic/translations/sv.json index 78033d17c6e..96aee85c779 100644 --- a/homeassistant/components/generic/translations/sv.json +++ b/homeassistant/components/generic/translations/sv.json @@ -2,6 +2,22 @@ "config": { "abort": { "no_devices_found": "Inga enheter hittades i n\u00e4tverket" + }, + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/sv.json b/homeassistant/components/gogogate2/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/gogogate2/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/sv.json b/homeassistant/components/growatt_server/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/growatt_server/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/sv.json b/homeassistant/components/honeywell/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/honeywell/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/sv.json b/homeassistant/components/huisbaasje/translations/sv.json index d52e8b8362c..4a6100815d6 100644 --- a/homeassistant/components/huisbaasje/translations/sv.json +++ b/homeassistant/components/huisbaasje/translations/sv.json @@ -2,6 +2,13 @@ "config": { "error": { "cannot_connect": "Kunde inte ansluta" + }, + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/sv.json b/homeassistant/components/hvv_departures/translations/sv.json index ff31d1c1484..8d17443df9f 100644 --- a/homeassistant/components/hvv_departures/translations/sv.json +++ b/homeassistant/components/hvv_departures/translations/sv.json @@ -1,4 +1,13 @@ { + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/insteon/translations/sv.json b/homeassistant/components/insteon/translations/sv.json index b8b6834022c..4992e704f9e 100644 --- a/homeassistant/components/insteon/translations/sv.json +++ b/homeassistant/components/insteon/translations/sv.json @@ -2,6 +2,22 @@ "config": { "abort": { "not_insteon_device": "Uppt\u00e4ckt enhet \u00e4r inte en Insteon-enhet" + }, + "step": { + "hubv2": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + }, + "options": { + "step": { + "change_hub_config": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/intellifire/translations/sv.json b/homeassistant/components/intellifire/translations/sv.json index be36fec5fe3..afffc97862b 100644 --- a/homeassistant/components/intellifire/translations/sv.json +++ b/homeassistant/components/intellifire/translations/sv.json @@ -5,6 +5,13 @@ }, "error": { "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "api_config": { + "data": { + "username": "E-postadress" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/iotawatt/translations/sv.json b/homeassistant/components/iotawatt/translations/sv.json new file mode 100644 index 00000000000..d1d69759ba6 --- /dev/null +++ b/homeassistant/components/iotawatt/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "auth": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/sv.json b/homeassistant/components/jellyfin/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/jellyfin/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/sv.json b/homeassistant/components/kmtronic/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/kmtronic/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/sv.json b/homeassistant/components/kodi/translations/sv.json new file mode 100644 index 00000000000..36b53053594 --- /dev/null +++ b/homeassistant/components/kodi/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "credentials": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sv.json b/homeassistant/components/litterrobot/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/sv.json b/homeassistant/components/meater/translations/sv.json index b920ba3abde..44b4ba96acf 100644 --- a/homeassistant/components/meater/translations/sv.json +++ b/homeassistant/components/meater/translations/sv.json @@ -11,6 +11,9 @@ "description": "Bekr\u00e4fta l\u00f6senordet f\u00f6r Meater Cloud-kontot {username}." }, "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + }, "data_description": { "username": "Meater Cloud anv\u00e4ndarnamn, vanligtvis en e-postadress." } diff --git a/homeassistant/components/mill/translations/sv.json b/homeassistant/components/mill/translations/sv.json index cd5effd10d3..8cef92a32b9 100644 --- a/homeassistant/components/mill/translations/sv.json +++ b/homeassistant/components/mill/translations/sv.json @@ -1,6 +1,11 @@ { "config": { "step": { + "cloud": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + }, "user": { "data": { "connection_type": "V\u00e4lj anslutningstyp" diff --git a/homeassistant/components/mjpeg/translations/sv.json b/homeassistant/components/mjpeg/translations/sv.json new file mode 100644 index 00000000000..291fbedbcfb --- /dev/null +++ b/homeassistant/components/mjpeg/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/sv.json b/homeassistant/components/motioneye/translations/sv.json new file mode 100644 index 00000000000..8fd6e00680b --- /dev/null +++ b/homeassistant/components/motioneye/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "admin_username": "Admin Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sv.json b/homeassistant/components/nam/translations/sv.json index 15a583f12a2..9f12ea5a385 100644 --- a/homeassistant/components/nam/translations/sv.json +++ b/homeassistant/components/nam/translations/sv.json @@ -8,6 +8,16 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "credentials": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + }, + "reauth_confirm": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + }, "user": { "data": { "host": "V\u00e4rd" diff --git a/homeassistant/components/netgear/translations/sv.json b/homeassistant/components/netgear/translations/sv.json new file mode 100644 index 00000000000..2672bc03eef --- /dev/null +++ b/homeassistant/components/netgear/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn (frivilligt)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/sv.json b/homeassistant/components/nzbget/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/nzbget/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/sv.json b/homeassistant/components/octoprint/translations/sv.json index e17feb4bbe6..dad4741f152 100644 --- a/homeassistant/components/octoprint/translations/sv.json +++ b/homeassistant/components/octoprint/translations/sv.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "ssl": "Anv\u00e4nd SSL" + "ssl": "Anv\u00e4nd SSL", + "username": "Anv\u00e4ndarnamn" } } } diff --git a/homeassistant/components/omnilogic/translations/sv.json b/homeassistant/components/omnilogic/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/omnilogic/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/sv.json b/homeassistant/components/onvif/translations/sv.json index 626daebc7b2..f2fd2e8429e 100644 --- a/homeassistant/components/onvif/translations/sv.json +++ b/homeassistant/components/onvif/translations/sv.json @@ -1,6 +1,11 @@ { "config": { "step": { + "configure": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + }, "configure_profile": { "title": "Konfigurera Profiler" } diff --git a/homeassistant/components/overkiz/translations/sv.json b/homeassistant/components/overkiz/translations/sv.json index 5ba4512fb35..c825e3cd616 100644 --- a/homeassistant/components/overkiz/translations/sv.json +++ b/homeassistant/components/overkiz/translations/sv.json @@ -3,6 +3,13 @@ "abort": { "reauth_successful": "\u00c5terautentisering lyckades", "reauth_wrong_account": "Du kan bara \u00e5terautentisera denna post med samma Overkiz-konto och hub" + }, + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/sv.json b/homeassistant/components/picnic/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/picnic/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/sv.json b/homeassistant/components/plum_lightpad/translations/sv.json new file mode 100644 index 00000000000..26e9f2d6a49 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "E-postadress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/sv.json b/homeassistant/components/prosegur/translations/sv.json new file mode 100644 index 00000000000..8a60ea1a5dc --- /dev/null +++ b/homeassistant/components/prosegur/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + }, + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/sv.json b/homeassistant/components/qnap_qsw/translations/sv.json index 416ef964cf3..0234fcb9860 100644 --- a/homeassistant/components/qnap_qsw/translations/sv.json +++ b/homeassistant/components/qnap_qsw/translations/sv.json @@ -3,6 +3,13 @@ "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad", "invalid_id": "Enheten returnerade ett ogiltigt unikt ID" + }, + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/renault/translations/sv.json b/homeassistant/components/renault/translations/sv.json new file mode 100644 index 00000000000..26e9f2d6a49 --- /dev/null +++ b/homeassistant/components/renault/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "E-postadress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/sv.json b/homeassistant/components/ridwell/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/ridwell/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/sv.json b/homeassistant/components/risco/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/risco/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruckus_unleashed/translations/sv.json b/homeassistant/components/ruckus_unleashed/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/ruckus_unleashed/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/sv.json b/homeassistant/components/scrape/translations/sv.json new file mode 100644 index 00000000000..291fbedbcfb --- /dev/null +++ b/homeassistant/components/scrape/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/sv.json b/homeassistant/components/sharkiq/translations/sv.json index 75f4175c9af..cae80c6c25f 100644 --- a/homeassistant/components/sharkiq/translations/sv.json +++ b/homeassistant/components/sharkiq/translations/sv.json @@ -9,6 +9,11 @@ "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } + }, + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } } } } diff --git a/homeassistant/components/shelly/translations/sv.json b/homeassistant/components/shelly/translations/sv.json new file mode 100644 index 00000000000..36b53053594 --- /dev/null +++ b/homeassistant/components/shelly/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "credentials": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/sv.json b/homeassistant/components/slack/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/slack/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/sv.json b/homeassistant/components/sleepiq/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/sleepiq/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/sv.json b/homeassistant/components/smart_meter_texas/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spider/translations/sv.json b/homeassistant/components/spider/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/spider/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/sv.json b/homeassistant/components/squeezebox/translations/sv.json new file mode 100644 index 00000000000..8dbe191c902 --- /dev/null +++ b/homeassistant/components/squeezebox/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "edit": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/sv.json b/homeassistant/components/subaru/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/subaru/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/surepetcare/translations/sv.json b/homeassistant/components/surepetcare/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/surepetcare/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/sv.json b/homeassistant/components/synology_dsm/translations/sv.json index 04814596518..012d092de41 100644 --- a/homeassistant/components/synology_dsm/translations/sv.json +++ b/homeassistant/components/synology_dsm/translations/sv.json @@ -12,6 +12,11 @@ }, "description": "Do vill du konfigurera {name} ({host})?" }, + "reauth_confirm": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + }, "user": { "data": { "host": "V\u00e4rd", diff --git a/homeassistant/components/tile/translations/sv.json b/homeassistant/components/tile/translations/sv.json new file mode 100644 index 00000000000..26e9f2d6a49 --- /dev/null +++ b/homeassistant/components/tile/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "E-postadress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/ca.json b/homeassistant/components/transmission/translations/ca.json index 4049cca3840..235e05bb78a 100644 --- a/homeassistant/components/transmission/translations/ca.json +++ b/homeassistant/components/transmission/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": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -9,6 +10,13 @@ "name_exists": "Nom ja existeix" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "La contrasenya de {username} \u00e9s inv\u00e0lida.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "host": "Amfitri\u00f3", diff --git a/homeassistant/components/transmission/translations/et.json b/homeassistant/components/transmission/translations/et.json index 1329444f7cf..745ef1030af 100644 --- a/homeassistant/components/transmission/translations/et.json +++ b/homeassistant/components/transmission/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": { "cannot_connect": "\u00dchendamine nurjus", @@ -9,6 +10,13 @@ "name_exists": "Nimi on juba olemas" }, "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Kasutaja {username} salas\u00f5na on kehtetu", + "title": "Taastuvasta sidumine" + }, "user": { "data": { "host": "", diff --git a/homeassistant/components/transmission/translations/fr.json b/homeassistant/components/transmission/translations/fr.json index f027b6909e8..539764ee64f 100644 --- a/homeassistant/components/transmission/translations/fr.json +++ b/homeassistant/components/transmission/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -9,6 +10,13 @@ "name_exists": "Ce nom est d\u00e9j\u00e0 utilis\u00e9" }, "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "description": "Le mot de passe pour {username} n'est pas valide.", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, "user": { "data": { "host": "H\u00f4te", diff --git a/homeassistant/components/transmission/translations/hu.json b/homeassistant/components/transmission/translations/hu.json index 79a60dc2b5b..1bd7129ed6b 100644 --- a/homeassistant/components/transmission/translations/hu.json +++ b/homeassistant/components/transmission/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": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -9,6 +10,13 @@ "name_exists": "A n\u00e9v m\u00e1r foglalt" }, "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "{username} jelszava \u00e9rv\u00e9nytelen.", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "host": "C\u00edm", diff --git a/homeassistant/components/unifiprotect/translations/sv.json b/homeassistant/components/unifiprotect/translations/sv.json index e2bfaa9118c..702dcbecdb7 100644 --- a/homeassistant/components/unifiprotect/translations/sv.json +++ b/homeassistant/components/unifiprotect/translations/sv.json @@ -2,9 +2,20 @@ "config": { "step": { "discovery_confirm": { + "data": { + "username": "Anv\u00e4ndarnamn" + }, "title": "UniFi Protect uppt\u00e4ckt" }, + "reauth_confirm": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + }, "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + }, "description": "Du beh\u00f6ver en lokal anv\u00e4ndare skapad i din UniFi OS-konsol f\u00f6r att logga in med. Ubiquiti Cloud-anv\u00e4ndare kommer inte att fungera. F\u00f6r mer information: {local_user_documentation_url}" } } diff --git a/homeassistant/components/upcloud/translations/sv.json b/homeassistant/components/upcloud/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/upcloud/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/sv.json b/homeassistant/components/venstar/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/venstar/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/sv.json b/homeassistant/components/vicare/translations/sv.json new file mode 100644 index 00000000000..26e9f2d6a49 --- /dev/null +++ b/homeassistant/components/vicare/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "E-postadress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/sv.json b/homeassistant/components/wallbox/translations/sv.json new file mode 100644 index 00000000000..8a60ea1a5dc --- /dev/null +++ b/homeassistant/components/wallbox/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + }, + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/sv.json b/homeassistant/components/watttime/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/watttime/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/sv.json b/homeassistant/components/whirlpool/translations/sv.json new file mode 100644 index 00000000000..23c825f256f --- /dev/null +++ b/homeassistant/components/whirlpool/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/sv.json b/homeassistant/components/xiaomi_miio/translations/sv.json index 17b92cb5058..20e4d8c6d07 100644 --- a/homeassistant/components/xiaomi_miio/translations/sv.json +++ b/homeassistant/components/xiaomi_miio/translations/sv.json @@ -3,7 +3,8 @@ "step": { "cloud": { "data": { - "cloud_password": "Molnl\u00f6senord" + "cloud_password": "Molnl\u00f6senord", + "cloud_username": "Molnanv\u00e4ndarnamn" } } } diff --git a/homeassistant/components/yale_smart_alarm/translations/sv.json b/homeassistant/components/yale_smart_alarm/translations/sv.json new file mode 100644 index 00000000000..8a60ea1a5dc --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + }, + "user": { + "data": { + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/sv.json b/homeassistant/components/zoneminder/translations/sv.json index 37fd73d32f0..4f0da20207f 100644 --- a/homeassistant/components/zoneminder/translations/sv.json +++ b/homeassistant/components/zoneminder/translations/sv.json @@ -9,6 +9,7 @@ "step": { "user": { "data": { + "username": "Anv\u00e4ndarnamn", "verify_ssl": "Verifiera SSL-certifikat" } } From 3851c7b4b4cafab4ead7aac67b37ce07823ca50c Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Mon, 20 Jun 2022 23:09:13 -0400 Subject: [PATCH 1627/3516] Bumps version of pyunifiprotect to 4.0.4 (#73722) --- .../components/unifiprotect/__init__.py | 5 +- .../components/unifiprotect/config_flow.py | 5 +- homeassistant/components/unifiprotect/data.py | 19 +- .../components/unifiprotect/manifest.json | 2 +- .../components/unifiprotect/services.py | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/unifiprotect/conftest.py | 105 +-- .../fixtures/sample_bootstrap.json | 633 ++++++++++++++++++ .../unifiprotect/test_binary_sensor.py | 21 +- tests/components/unifiprotect/test_button.py | 2 +- tests/components/unifiprotect/test_camera.py | 25 +- tests/components/unifiprotect/test_init.py | 6 +- tests/components/unifiprotect/test_light.py | 2 +- tests/components/unifiprotect/test_lock.py | 2 +- .../unifiprotect/test_media_player.py | 2 +- tests/components/unifiprotect/test_number.py | 21 +- tests/components/unifiprotect/test_select.py | 17 +- tests/components/unifiprotect/test_sensor.py | 17 +- .../components/unifiprotect/test_services.py | 15 +- tests/components/unifiprotect/test_switch.py | 18 +- 21 files changed, 770 insertions(+), 155 deletions(-) create mode 100644 tests/components/unifiprotect/fixtures/sample_bootstrap.json diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index a24777f9ecd..c83221b0ccf 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -7,7 +7,8 @@ import logging from aiohttp import CookieJar from aiohttp.client_exceptions import ServerDisconnectedError -from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient +from pyunifiprotect import ProtectApiClient +from pyunifiprotect.exceptions import ClientError, NotAuthorized from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -68,7 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: nvr_info = await protect.get_nvr() except NotAuthorized as err: raise ConfigEntryAuthFailed(err) from err - except (asyncio.TimeoutError, NvrError, ServerDisconnectedError) as err: + except (asyncio.TimeoutError, ClientError, ServerDisconnectedError) as err: raise ConfigEntryNotReady from err if nvr_info.version < MIN_REQUIRED_PROTECT_V: diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index e183edf4259..9cd15c4e3c2 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -6,8 +6,9 @@ import logging from typing import Any from aiohttp import CookieJar -from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient +from pyunifiprotect import ProtectApiClient from pyunifiprotect.data import NVR +from pyunifiprotect.exceptions import ClientError, NotAuthorized from unifi_discovery import async_console_is_alive import voluptuous as vol @@ -253,7 +254,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except NotAuthorized as ex: _LOGGER.debug(ex) errors[CONF_PASSWORD] = "invalid_auth" - except NvrError as ex: + except ClientError as ex: _LOGGER.debug(ex) errors["base"] = "cannot_connect" else: diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 1e9729f7930..2a30e18d586 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -6,7 +6,7 @@ from datetime import timedelta import logging from typing import Any -from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient +from pyunifiprotect import ProtectApiClient from pyunifiprotect.data import ( Bootstrap, Event, @@ -15,6 +15,7 @@ from pyunifiprotect.data import ( WSSubscriptionMessage, ) from pyunifiprotect.data.base import ProtectAdoptableDeviceModel +from pyunifiprotect.exceptions import ClientError, NotAuthorized from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback @@ -100,23 +101,27 @@ class ProtectData: try: updates = await self.api.update(force=force) - except NvrError: - if self.last_update_success: - _LOGGER.exception("Error while updating") - self.last_update_success = False - # manually trigger update to mark entities unavailable - self._async_process_updates(self.api.bootstrap) except NotAuthorized: await self.async_stop() _LOGGER.exception("Reauthentication required") self._entry.async_start_reauth(self._hass) self.last_update_success = False + except ClientError: + if self.last_update_success: + _LOGGER.exception("Error while updating") + self.last_update_success = False + # manually trigger update to mark entities unavailable + self._async_process_updates(self.api.bootstrap) else: self.last_update_success = True self._async_process_updates(updates) @callback def _async_process_ws_message(self, message: WSSubscriptionMessage) -> None: + # removed packets are not processed yet + if message.new_obj is None: # pragma: no cover + return + if message.new_obj.model in DEVICES_WITH_ENTITIES: self.async_signal_device_id_update(message.new_obj.id) # trigger update for all Cameras with LCD screens when NVR Doorbell settings updates diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 2554d12c866..dfa748835f5 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==3.9.2", "unifi-discovery==1.1.4"], + "requirements": ["pyunifiprotect==4.0.4", "unifi-discovery==1.1.4"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/homeassistant/components/unifiprotect/services.py b/homeassistant/components/unifiprotect/services.py index 3b7b3db026f..915c51b6c0a 100644 --- a/homeassistant/components/unifiprotect/services.py +++ b/homeassistant/components/unifiprotect/services.py @@ -8,7 +8,7 @@ from typing import Any, cast from pydantic import ValidationError from pyunifiprotect.api import ProtectApiClient from pyunifiprotect.data import Chime -from pyunifiprotect.exceptions import BadRequest +from pyunifiprotect.exceptions import ClientError import voluptuous as vol from homeassistant.components.binary_sensor import BinarySensorDeviceClass @@ -100,7 +100,7 @@ async def _async_service_call_nvr( await asyncio.gather( *(getattr(i.bootstrap.nvr, method)(*args, **kwargs) for i in instances) ) - except (BadRequest, ValidationError) as err: + except (ClientError, ValidationError) as err: raise HomeAssistantError(str(err)) from err diff --git a/requirements_all.txt b/requirements_all.txt index 013ef770615..ece350a33c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1990,7 +1990,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.9.2 +pyunifiprotect==4.0.4 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 660d41d66eb..d7c81558ac5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1325,7 +1325,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==3.9.2 +pyunifiprotect==4.0.4 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 9892bcc3ec6..adc69cc8bf9 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -13,6 +13,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest from pyunifiprotect.data import ( NVR, + Bootstrap, Camera, Chime, Doorlock, @@ -39,69 +40,6 @@ from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture MAC_ADDR = "aa:bb:cc:dd:ee:ff" -@dataclass -class MockBootstrap: - """Mock for Bootstrap.""" - - nvr: NVR - cameras: dict[str, Any] - lights: dict[str, Any] - sensors: dict[str, Any] - viewers: dict[str, Any] - liveviews: dict[str, Any] - events: dict[str, Any] - doorlocks: dict[str, Any] - chimes: dict[str, Any] - - def reset_objects(self) -> None: - """Reset all devices on bootstrap for tests.""" - self.cameras = {} - self.lights = {} - self.sensors = {} - self.viewers = {} - self.liveviews = {} - self.events = {} - self.doorlocks = {} - self.chimes = {} - - def process_ws_packet(self, msg: WSSubscriptionMessage) -> None: - """Fake process method for tests.""" - pass - - def unifi_dict(self) -> dict[str, Any]: - """Return UniFi formatted dict representation of the NVR.""" - return { - "nvr": self.nvr.unifi_dict(), - "cameras": [c.unifi_dict() for c in self.cameras.values()], - "lights": [c.unifi_dict() for c in self.lights.values()], - "sensors": [c.unifi_dict() for c in self.sensors.values()], - "viewers": [c.unifi_dict() for c in self.viewers.values()], - "liveviews": [c.unifi_dict() for c in self.liveviews.values()], - "doorlocks": [c.unifi_dict() for c in self.doorlocks.values()], - "chimes": [c.unifi_dict() for c in self.chimes.values()], - } - - def get_device_from_mac(self, mac: str) -> ProtectAdoptableDeviceModel | None: - """Return device for MAC address.""" - - mac = mac.lower().replace(":", "").replace("-", "").replace("_", "") - - all_devices = ( - self.cameras.values(), - self.lights.values(), - self.sensors.values(), - self.viewers.values(), - self.liveviews.values(), - self.doorlocks.values(), - self.chimes.values(), - ) - for devices in all_devices: - for device in devices: - if device.mac.lower() == mac: - return device - return None - - @dataclass class MockEntityFixture: """Mock for NVR.""" @@ -155,27 +93,42 @@ def mock_old_nvr_fixture(): @pytest.fixture(name="mock_bootstrap") def mock_bootstrap_fixture(mock_nvr: NVR): """Mock Bootstrap fixture.""" - return MockBootstrap( - nvr=mock_nvr, - cameras={}, - lights={}, - sensors={}, - viewers={}, - liveviews={}, - events={}, - doorlocks={}, - chimes={}, - ) + data = json.loads(load_fixture("sample_bootstrap.json", integration=DOMAIN)) + data["nvr"] = mock_nvr + data["cameras"] = [] + data["lights"] = [] + data["sensors"] = [] + data["viewers"] = [] + data["liveviews"] = [] + data["events"] = [] + data["doorlocks"] = [] + data["chimes"] = [] + + return Bootstrap.from_unifi_dict(**data) + + +def reset_objects(bootstrap: Bootstrap): + """Reset bootstrap objects.""" + + bootstrap.cameras = {} + bootstrap.lights = {} + bootstrap.sensors = {} + bootstrap.viewers = {} + bootstrap.liveviews = {} + bootstrap.events = {} + bootstrap.doorlocks = {} + bootstrap.chimes = {} @pytest.fixture -def mock_client(mock_bootstrap: MockBootstrap): +def mock_client(mock_bootstrap: Bootstrap): """Mock ProtectApiClient for testing.""" client = Mock() client.bootstrap = mock_bootstrap - nvr = mock_bootstrap.nvr + nvr = client.bootstrap.nvr nvr._api = client + client.bootstrap._api = client client.base_url = "https://127.0.0.1" client.connection_host = IPv4Address("127.0.0.1") diff --git a/tests/components/unifiprotect/fixtures/sample_bootstrap.json b/tests/components/unifiprotect/fixtures/sample_bootstrap.json new file mode 100644 index 00000000000..2b7326831eb --- /dev/null +++ b/tests/components/unifiprotect/fixtures/sample_bootstrap.json @@ -0,0 +1,633 @@ +{ + "authUserId": "4c5f03a8c8bd48ad8e066285", + "accessKey": "8528571101220:340ff666bffb58bc404b859a:8f3f41a7b180b1ff7463fe4f7f13b528ac3d28668f25d0ecaa30c8e7888559e782b38d4335b40861030b75126eb7cea8385f3f9ab59dfa9a993e50757c277053", + "users": [ + { + "permissions": [], + "lastLoginIp": null, + "lastLoginTime": null, + "isOwner": true, + "enableNotifications": false, + "settings": { + "flags": {} + }, + "groups": ["b061186823695fb901973177"], + "alertRules": [], + "notificationsV2": { + "state": "custom", + "motionNotifications": { + "trigger": { + "when": "inherit", + "location": "away", + "schedules": [] + }, + "cameras": [ + { + "inheritFromParent": true, + "motion": [], + "person": [], + "vehicle": [], + "camera": "61b3f5c7033ea703e7000424", + "trigger": { + "when": "always", + "location": "away", + "schedules": [] + } + } + ], + "doorbells": [], + "lights": [], + "doorlocks": [], + "sensors": [] + }, + "systemNotifications": {} + }, + "featureFlags": { + "notificationsV2": true + }, + "id": "fe4c12ae2c1348edb7854e2f", + "hasAcceptedInvite": true, + "allPermissions": [ + "nvr:read:*", + "liveview:create", + "user:read,write,delete:$", + "nvr:write,delete:*", + "group:create,read,write,delete:*", + "user:create,read,write,delete:*", + "schedule:create,read,write,delete:*", + "legacyUFV:read,write,delete:*", + "bridge:create,read,write,delete:*", + "camera:create,read,write,delete,readmedia,deletemedia:*", + "light:create,read,write,delete:*", + "sensor:create,read,write,delete:*", + "doorlock:create,read,write,delete:*", + "viewer:create,read,write,delete:*", + "display:create,read,write,delete:*", + "chime:create,read,write,delete:*" + ], + "cloudAccount": { + "firstName": "Qpvfly", + "lastName": "Ikjzilt", + "email": "QhoFvCv@example.com", + "profileImg": null, + "user": "fe4c12ae2c1348edb7854e2f", + "id": "9efc4511-4539-4402-9581-51cee8b65cf5", + "cloudId": "9efc4511-4539-4402-9581-51cee8b65cf5", + "name": "Qpvfly Ikjzilt", + "modelKey": "cloudIdentity" + }, + "name": "Qpvfly Ikjzilt", + "firstName": "Qpvfly", + "lastName": "Ikjzilt", + "email": "QhoFvCv@example.com", + "localUsername": "QhoFvCv", + "modelKey": "user" + }, + { + "permissions": [], + "lastLoginIp": null, + "lastLoginTime": null, + "isOwner": false, + "enableNotifications": false, + "settings": null, + "groups": ["a7f3b2eb71b4c4e56f1f45ac", "b061186823695fb901973177"], + "alertRules": [], + "notificationsV2": { + "state": "auto", + "motionNotifications": { + "trigger": { + "when": "inherit", + "location": "away", + "schedules": [] + }, + "cameras": [], + "doorbells": [], + "lights": [], + "doorlocks": [], + "sensors": [] + }, + "systemNotifications": {} + }, + "featureFlags": { + "notificationsV2": true + }, + "id": "dcaef9cb8aed05c7db658a46", + "hasAcceptedInvite": false, + "allPermissions": [ + "nvr:read:*", + "liveview:create", + "user:read,write,delete:$", + "bridge:read:*", + "camera:read,readmedia:*", + "doorlock:read:*", + "light:read:*", + "sensor:read:*", + "viewer:read:*", + "display:read:*", + "chime:read:*", + "nvr:read:*", + "liveview:create", + "user:read,write,delete:$", + "nvr:write,delete:*", + "group:create,read,write,delete:*", + "user:create,read,write,delete:*", + "schedule:create,read,write,delete:*", + "legacyUFV:read,write,delete:*", + "bridge:create,read,write,delete:*", + "camera:create,read,write,delete,readmedia,deletemedia:*", + "light:create,read,write,delete:*", + "sensor:create,read,write,delete:*", + "doorlock:create,read,write,delete:*", + "viewer:create,read,write,delete:*", + "display:create,read,write,delete:*", + "chime:create,read,write,delete:*" + ], + "cloudAccount": null, + "name": "Uxqg Wcbz", + "firstName": "Uxqg", + "lastName": "Wcbz", + "email": "epHDEhE@example.com", + "localUsername": "epHDEhE", + "modelKey": "user" + }, + { + "permissions": [ + "liveview:*:d65bb41c14d6aa92bfa4a6d1", + "liveview:*:49bbb5005424a0d35152671a", + "liveview:*:b28c38f1220f6b43f3930dff", + "liveview:*:b9861b533a87ea639fa4d438" + ], + "lastLoginIp": null, + "lastLoginTime": null, + "isOwner": false, + "enableNotifications": false, + "settings": { + "flags": {}, + "web": { + "dewarp": { + "61ddb66b018e2703e7008c19": { + "dewarp": false, + "state": { + "pan": 0, + "tilt": -1.5707963267948966, + "zoom": 1.5707963267948966, + "panning": 0, + "tilting": 0 + } + } + }, + "liveview.includeGlobal": true, + "elements.events_viewmode": "grid", + "elements.viewmode": "list" + } + }, + "groups": ["b061186823695fb901973177"], + "location": { + "isAway": true, + "latitude": null, + "longitude": null + }, + "alertRules": [], + "notificationsV2": { + "state": "custom", + "motionNotifications": { + "trigger": { + "when": "inherit", + "location": "away", + "schedules": [] + }, + "cameras": [ + { + "inheritFromParent": true, + "motion": [], + "camera": "61b3f5c703d2a703e7000427", + "trigger": { + "when": "always", + "location": "away", + "schedules": [] + } + }, + { + "inheritFromParent": true, + "motion": [], + "person": [], + "vehicle": [], + "camera": "61b3f5c7033ea703e7000424", + "trigger": { + "when": "always", + "location": "away", + "schedules": [] + } + } + ], + "doorbells": [], + "lights": [], + "doorlocks": [], + "sensors": [] + }, + "systemNotifications": {} + }, + "featureFlags": { + "notificationsV2": true + }, + "id": "4c5f03a8c8bd48ad8e066285", + "hasAcceptedInvite": false, + "allPermissions": [ + "liveview:*:d65bb41c14d6aa92bfa4a6d1", + "liveview:*:49bbb5005424a0d35152671a", + "liveview:*:b28c38f1220f6b43f3930dff", + "liveview:*:b9861b533a87ea639fa4d438", + "nvr:read:*", + "liveview:create", + "user:read,write,delete:$", + "nvr:write,delete:*", + "group:create,read,write,delete:*", + "user:create,read,write,delete:*", + "schedule:create,read,write,delete:*", + "legacyUFV:read,write,delete:*", + "bridge:create,read,write,delete:*", + "camera:create,read,write,delete,readmedia,deletemedia:*", + "light:create,read,write,delete:*", + "sensor:create,read,write,delete:*", + "doorlock:create,read,write,delete:*", + "viewer:create,read,write,delete:*", + "display:create,read,write,delete:*", + "chime:create,read,write,delete:*" + ], + "cloudAccount": null, + "name": "Ptcmsdo Tfiyoep", + "firstName": "Ptcmsdo", + "lastName": "Tfiyoep", + "email": "EQAoXL@example.com", + "localUsername": "EQAoXL", + "modelKey": "user" + }, + { + "permissions": [], + "lastLoginIp": null, + "lastLoginTime": null, + "isOwner": false, + "enableNotifications": false, + "settings": { + "flags": {}, + "web": { + "dewarp": { + "61c4d1db02c82a03e700429c": { + "dewarp": false, + "state": { + "pan": 0, + "tilt": 0, + "zoom": 1.5707963267948966, + "panning": 0, + "tilting": 0 + } + } + }, + "liveview.includeGlobal": true + } + }, + "groups": ["a7f3b2eb71b4c4e56f1f45ac"], + "alertRules": [], + "notificationsV2": { + "state": "auto", + "motionNotifications": { + "trigger": { + "when": "inherit", + "location": "away", + "schedules": [] + }, + "cameras": [], + "doorbells": [], + "lights": [], + "doorlocks": [], + "sensors": [] + }, + "systemNotifications": {} + }, + "featureFlags": { + "notificationsV2": true + }, + "id": "bc3dd633553907952a6fe20d", + "hasAcceptedInvite": false, + "allPermissions": [ + "nvr:read:*", + "liveview:create", + "user:read,write,delete:$", + "bridge:read:*", + "camera:read,readmedia:*", + "doorlock:read:*", + "light:read:*", + "sensor:read:*", + "viewer:read:*", + "display:read:*", + "chime:read:*" + ], + "cloudAccount": null, + "name": "Evdxou Zgyv", + "firstName": "Evdxou", + "lastName": "Zgyv", + "email": "FMZuD@example.com", + "localUsername": "FMZuD", + "modelKey": "user" + }, + { + "permissions": [], + "lastLoginIp": null, + "lastLoginTime": null, + "isOwner": false, + "enableNotifications": false, + "settings": null, + "groups": ["a7f3b2eb71b4c4e56f1f45ac", "b061186823695fb901973177"], + "alertRules": [], + "notificationsV2": { + "state": "auto", + "motionNotifications": { + "trigger": { + "when": "inherit", + "location": "away", + "schedules": [] + }, + "cameras": [], + "doorbells": [], + "lights": [], + "doorlocks": [], + "sensors": [] + }, + "systemNotifications": {} + }, + "featureFlags": { + "notificationsV2": true + }, + "id": "adec5334b69f56f6a6c47520", + "hasAcceptedInvite": false, + "allPermissions": [ + "nvr:read:*", + "liveview:create", + "user:read,write,delete:$", + "bridge:read:*", + "camera:read,readmedia:*", + "doorlock:read:*", + "light:read:*", + "sensor:read:*", + "viewer:read:*", + "display:read:*", + "chime:read:*", + "nvr:read:*", + "liveview:create", + "user:read,write,delete:$", + "nvr:write,delete:*", + "group:create,read,write,delete:*", + "user:create,read,write,delete:*", + "schedule:create,read,write,delete:*", + "legacyUFV:read,write,delete:*", + "bridge:create,read,write,delete:*", + "camera:create,read,write,delete,readmedia,deletemedia:*", + "light:create,read,write,delete:*", + "sensor:create,read,write,delete:*", + "doorlock:create,read,write,delete:*", + "viewer:create,read,write,delete:*", + "display:create,read,write,delete:*", + "chime:create,read,write,delete:*" + ], + "cloudAccount": null, + "name": "Qpv Elqfgq", + "firstName": "Qpv", + "lastName": "Elqfgq", + "email": "xdr@example.com", + "localUsername": "xdr", + "modelKey": "user" + }, + { + "permissions": [], + "lastLoginIp": null, + "lastLoginTime": null, + "isOwner": false, + "enableNotifications": false, + "settings": null, + "groups": ["a7f3b2eb71b4c4e56f1f45ac", "b061186823695fb901973177"], + "alertRules": [], + "notificationsV2": { + "state": "auto", + "motionNotifications": { + "trigger": { + "when": "inherit", + "location": "away", + "schedules": [] + }, + "cameras": [], + "doorbells": [], + "lights": [], + "doorlocks": [], + "sensors": [] + }, + "systemNotifications": {} + }, + "featureFlags": { + "notificationsV2": true + }, + "id": "8593657a25b7826a4288b6af", + "hasAcceptedInvite": false, + "allPermissions": [ + "nvr:read:*", + "liveview:create", + "user:read,write,delete:$", + "bridge:read:*", + "camera:read,readmedia:*", + "doorlock:read:*", + "light:read:*", + "sensor:read:*", + "viewer:read:*", + "display:read:*", + "chime:read:*", + "nvr:read:*", + "liveview:create", + "user:read,write,delete:$", + "nvr:write,delete:*", + "group:create,read,write,delete:*", + "user:create,read,write,delete:*", + "schedule:create,read,write,delete:*", + "legacyUFV:read,write,delete:*", + "bridge:create,read,write,delete:*", + "camera:create,read,write,delete,readmedia,deletemedia:*", + "light:create,read,write,delete:*", + "sensor:create,read,write,delete:*", + "doorlock:create,read,write,delete:*", + "viewer:create,read,write,delete:*", + "display:create,read,write,delete:*", + "chime:create,read,write,delete:*" + ], + "cloudAccount": null, + "name": "Sgpy Ooevsme", + "firstName": "Sgpy", + "lastName": "Ooevsme", + "email": "WQJNT@example.com", + "localUsername": "WQJNT", + "modelKey": "user" + }, + { + "permissions": [], + "isOwner": false, + "enableNotifications": false, + "groups": ["a7f3b2eb71b4c4e56f1f45ac"], + "alertRules": [], + "notificationsV2": { + "state": "off", + "motionNotifications": { + "trigger": { + "when": "inherit", + "location": "away", + "schedules": [] + }, + "cameras": [], + "doorbells": [], + "lights": [], + "doorlocks": [], + "sensors": [] + }, + "systemNotifications": {} + }, + "featureFlags": { + "notificationsV2": true + }, + "id": "abf647aed3650a781ceba13f", + "hasAcceptedInvite": false, + "allPermissions": [ + "nvr:read:*", + "liveview:create", + "user:read,write,delete:$", + "bridge:read:*", + "camera:read,readmedia:*", + "doorlock:read:*", + "light:read:*", + "sensor:read:*", + "viewer:read:*", + "display:read:*", + "chime:read:*" + ], + "cloudAccount": null, + "name": "Yiiyq Glx", + "firstName": "Yiiyq", + "lastName": "Glx", + "email": "fBjmm@example.com", + "localUsername": "fBjmm", + "modelKey": "user" + } + ], + "groups": [ + { + "name": "Kubw Xnbb", + "permissions": [ + "nvr:read:*", + "liveview:create", + "user:read,write,delete:$", + "nvr:write,delete:*", + "group:create,read,write,delete:*", + "user:create,read,write,delete:*", + "schedule:create,read,write,delete:*", + "legacyUFV:read,write,delete:*", + "bridge:create,read,write,delete:*", + "camera:create,read,write,delete,readmedia,deletemedia:*", + "light:create,read,write,delete:*", + "sensor:create,read,write,delete:*", + "doorlock:create,read,write,delete:*", + "viewer:create,read,write,delete:*", + "display:create,read,write,delete:*", + "chime:create,read,write,delete:*" + ], + "type": "preset", + "isDefault": true, + "id": "b061186823695fb901973177", + "modelKey": "group" + }, + { + "name": "Pmbrvp Wyzqs", + "permissions": [ + "nvr:read:*", + "liveview:create", + "user:read,write,delete:$", + "bridge:read:*", + "camera:read,readmedia:*", + "doorlock:read:*", + "light:read:*", + "sensor:read:*", + "viewer:read:*", + "display:read:*", + "chime:read:*" + ], + "type": "preset", + "isDefault": false, + "id": "a7f3b2eb71b4c4e56f1f45ac", + "modelKey": "group" + } + ], + "schedules": [], + "legacyUFVs": [], + "lastUpdateId": "ebf25bac-d5a1-4f1d-a0ee-74c15981eb70", + "displays": [], + "bridges": [ + { + "mac": "A28D0DB15AE1", + "host": "192.168.231.68", + "connectionHost": "192.168.102.63", + "type": "UFP-UAP-B", + "name": "Sffde Gxcaqe", + "upSince": 1639807977891, + "uptime": 3247782, + "lastSeen": 1643055759891, + "connectedSince": 1642374159304, + "state": "CONNECTED", + "hardwareRevision": 19, + "firmwareVersion": "0.3.1", + "latestFirmwareVersion": null, + "firmwareBuild": null, + "isUpdating": false, + "isAdopting": false, + "isAdopted": true, + "isAdoptedByOther": false, + "isProvisioned": false, + "isRebooting": false, + "isSshEnabled": false, + "canAdopt": false, + "isAttemptingToConnect": false, + "wiredConnectionState": { + "phyRate": null + }, + "id": "1f5a055254fb9169d7536fb9", + "isConnected": true, + "platform": "mt7621", + "modelKey": "bridge" + }, + { + "mac": "C65C557CCA95", + "host": "192.168.87.68", + "connectionHost": "192.168.102.63", + "type": "UFP-UAP-B", + "name": "Axiwj Bbd", + "upSince": 1641257260772, + "uptime": null, + "lastSeen": 1643052750862, + "connectedSince": 1643052754695, + "state": "CONNECTED", + "hardwareRevision": 19, + "firmwareVersion": "0.3.1", + "latestFirmwareVersion": null, + "firmwareBuild": null, + "isUpdating": false, + "isAdopting": false, + "isAdopted": true, + "isAdoptedByOther": false, + "isProvisioned": false, + "isRebooting": false, + "isSshEnabled": false, + "canAdopt": false, + "isAttemptingToConnect": false, + "wiredConnectionState": { + "phyRate": null + }, + "id": "e6901e3665a4c0eab0d9c1a5", + "isConnected": true, + "platform": "mt7621", + "modelKey": "bridge" + } + ] +} diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 8fbaf61aca1..644665cc659 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -36,6 +36,7 @@ from .conftest import ( MockEntityFixture, assert_entity_counts, ids_from_device_description, + reset_objects, ) @@ -51,7 +52,7 @@ async def camera_fixture( # disable pydantic validation so mocking can happen Camera.__config__.validate_assignment = False - camera_obj = mock_camera.copy(deep=True) + camera_obj = mock_camera.copy() camera_obj._api = mock_entry.api camera_obj.channels[0]._api = mock_entry.api camera_obj.channels[1]._api = mock_entry.api @@ -62,7 +63,7 @@ async def camera_fixture( camera_obj.is_dark = False camera_obj.is_motion_detected = False - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] mock_entry.api.bootstrap.cameras = { camera_obj.id: camera_obj, @@ -87,14 +88,14 @@ async def light_fixture( # disable pydantic validation so mocking can happen Light.__config__.validate_assignment = False - light_obj = mock_light.copy(deep=True) + light_obj = mock_light.copy() light_obj._api = mock_entry.api light_obj.name = "Test Light" light_obj.is_dark = False light_obj.is_pir_motion_detected = False light_obj.last_motion = now - timedelta(hours=1) - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] mock_entry.api.bootstrap.lights = { light_obj.id: light_obj, @@ -119,7 +120,7 @@ async def camera_none_fixture( # disable pydantic validation so mocking can happen Camera.__config__.validate_assignment = False - camera_obj = mock_camera.copy(deep=True) + camera_obj = mock_camera.copy() camera_obj._api = mock_entry.api camera_obj.channels[0]._api = mock_entry.api camera_obj.channels[1]._api = mock_entry.api @@ -129,7 +130,7 @@ async def camera_none_fixture( camera_obj.is_dark = False camera_obj.is_motion_detected = False - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] mock_entry.api.bootstrap.cameras = { camera_obj.id: camera_obj, @@ -157,7 +158,7 @@ async def sensor_fixture( # disable pydantic validation so mocking can happen Sensor.__config__.validate_assignment = False - sensor_obj = mock_sensor.copy(deep=True) + sensor_obj = mock_sensor.copy() sensor_obj._api = mock_entry.api sensor_obj.name = "Test Sensor" sensor_obj.mount_type = MountType.DOOR @@ -170,7 +171,7 @@ async def sensor_fixture( sensor_obj.alarm_triggered_at = now - timedelta(hours=1) sensor_obj.tampering_detected_at = None - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] mock_entry.api.bootstrap.sensors = { sensor_obj.id: sensor_obj, @@ -198,7 +199,7 @@ async def sensor_none_fixture( # disable pydantic validation so mocking can happen Sensor.__config__.validate_assignment = False - sensor_obj = mock_sensor.copy(deep=True) + sensor_obj = mock_sensor.copy() sensor_obj._api = mock_entry.api sensor_obj.name = "Test Sensor" sensor_obj.mount_type = MountType.LEAK @@ -206,7 +207,7 @@ async def sensor_none_fixture( sensor_obj.alarm_settings.is_enabled = False sensor_obj.tampering_detected_at = None - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] mock_entry.api.bootstrap.sensors = { sensor_obj.id: sensor_obj, diff --git a/tests/components/unifiprotect/test_button.py b/tests/components/unifiprotect/test_button.py index 5b7122f6227..9a1c7009660 100644 --- a/tests/components/unifiprotect/test_button.py +++ b/tests/components/unifiprotect/test_button.py @@ -21,7 +21,7 @@ async def chime_fixture( ): """Fixture for a single camera for testing the button platform.""" - chime_obj = mock_chime.copy(deep=True) + chime_obj = mock_chime.copy() chime_obj._api = mock_entry.api chime_obj.name = "Test Chime" diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index 2f8d2607da0..03b52c7e52e 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -52,7 +52,7 @@ async def camera_fixture( # disable pydantic validation so mocking can happen ProtectCamera.__config__.validate_assignment = False - camera_obj = mock_camera.copy(deep=True) + camera_obj = mock_camera.copy() camera_obj._api = mock_entry.api camera_obj.channels[0]._api = mock_entry.api camera_obj.channels[1]._api = mock_entry.api @@ -83,7 +83,7 @@ async def camera_package_fixture( ): """Fixture for a single camera for testing the camera platform.""" - camera_obj = mock_camera.copy(deep=True) + camera_obj = mock_camera.copy() camera_obj._api = mock_entry.api camera_obj.channels[0]._api = mock_entry.api camera_obj.channels[1]._api = mock_entry.api @@ -95,7 +95,7 @@ async def camera_package_fixture( camera_obj.channels[0].rtsp_alias = "test_high_alias" camera_obj.channels[1].is_rtsp_enabled = False camera_obj.channels[2].is_rtsp_enabled = False - package_channel = camera_obj.channels[0].copy(deep=True) + package_channel = camera_obj.channels[0].copy() package_channel.is_rtsp_enabled = False package_channel.name = "Package Camera" package_channel.id = 3 @@ -246,8 +246,9 @@ async def test_basic_setup( ): """Test working setup of unifiprotect entry.""" - camera_high_only = mock_camera.copy(deep=True) + camera_high_only = mock_camera.copy() camera_high_only._api = mock_entry.api + camera_high_only.channels = [c.copy() for c in mock_camera.channels] camera_high_only.channels[0]._api = mock_entry.api camera_high_only.channels[1]._api = mock_entry.api camera_high_only.channels[2]._api = mock_entry.api @@ -259,8 +260,9 @@ async def test_basic_setup( camera_high_only.channels[2].is_rtsp_enabled = False regenerate_device_ids(camera_high_only) - camera_medium_only = mock_camera.copy(deep=True) + camera_medium_only = mock_camera.copy() camera_medium_only._api = mock_entry.api + camera_medium_only.channels = [c.copy() for c in mock_camera.channels] camera_medium_only.channels[0]._api = mock_entry.api camera_medium_only.channels[1]._api = mock_entry.api camera_medium_only.channels[2]._api = mock_entry.api @@ -272,8 +274,9 @@ async def test_basic_setup( camera_medium_only.channels[2].is_rtsp_enabled = False regenerate_device_ids(camera_medium_only) - camera_all_channels = mock_camera.copy(deep=True) + camera_all_channels = mock_camera.copy() camera_all_channels._api = mock_entry.api + camera_all_channels.channels = [c.copy() for c in mock_camera.channels] camera_all_channels.channels[0]._api = mock_entry.api camera_all_channels.channels[1]._api = mock_entry.api camera_all_channels.channels[2]._api = mock_entry.api @@ -289,8 +292,9 @@ async def test_basic_setup( camera_all_channels.channels[2].rtsp_alias = "test_low_alias" regenerate_device_ids(camera_all_channels) - camera_no_channels = mock_camera.copy(deep=True) + camera_no_channels = mock_camera.copy() camera_no_channels._api = mock_entry.api + camera_no_channels.channels = [c.copy() for c in camera_no_channels.channels] camera_no_channels.channels[0]._api = mock_entry.api camera_no_channels.channels[1]._api = mock_entry.api camera_no_channels.channels[2]._api = mock_entry.api @@ -301,8 +305,9 @@ async def test_basic_setup( camera_no_channels.channels[2].is_rtsp_enabled = False regenerate_device_ids(camera_no_channels) - camera_package = mock_camera.copy(deep=True) + camera_package = mock_camera.copy() camera_package._api = mock_entry.api + camera_package.channels = [c.copy() for c in mock_camera.channels] camera_package.channels[0]._api = mock_entry.api camera_package.channels[1]._api = mock_entry.api camera_package.channels[2]._api = mock_entry.api @@ -313,7 +318,7 @@ async def test_basic_setup( camera_package.channels[1].is_rtsp_enabled = False camera_package.channels[2].is_rtsp_enabled = False regenerate_device_ids(camera_package) - package_channel = camera_package.channels[0].copy(deep=True) + package_channel = camera_package.channels[0].copy() package_channel.is_rtsp_enabled = False package_channel.name = "Package Camera" package_channel.id = 3 @@ -398,7 +403,7 @@ async def test_missing_channels( ): """Test setting up camera with no camera channels.""" - camera = mock_camera.copy(deep=True) + camera = mock_camera.copy() camera.channels = [] mock_entry.api.bootstrap.cameras = {camera.id: camera} diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 23dfa12fc97..d36183ba135 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, patch import aiohttp from pyunifiprotect import NotAuthorized, NvrError -from pyunifiprotect.data import NVR, Light +from pyunifiprotect.data import NVR, Bootstrap, Light from homeassistant.components.unifiprotect.const import CONF_DISABLE_RTSP, DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState @@ -16,7 +16,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from . import _patch_discovery -from .conftest import MockBootstrap, MockEntityFixture, regenerate_device_ids +from .conftest import MockEntityFixture, regenerate_device_ids from tests.common import MockConfigEntry @@ -52,7 +52,7 @@ async def test_setup_multiple( hass: HomeAssistant, mock_entry: MockEntityFixture, mock_client, - mock_bootstrap: MockBootstrap, + mock_bootstrap: Bootstrap, ): """Test working setup of unifiprotect entry.""" diff --git a/tests/components/unifiprotect/test_light.py b/tests/components/unifiprotect/test_light.py index a3686fdfbd9..c4f324f30fd 100644 --- a/tests/components/unifiprotect/test_light.py +++ b/tests/components/unifiprotect/test_light.py @@ -32,7 +32,7 @@ async def light_fixture( # disable pydantic validation so mocking can happen Light.__config__.validate_assignment = False - light_obj = mock_light.copy(deep=True) + light_obj = mock_light.copy() light_obj._api = mock_entry.api light_obj.name = "Test Light" light_obj.is_light_on = False diff --git a/tests/components/unifiprotect/test_lock.py b/tests/components/unifiprotect/test_lock.py index abcea4ec04e..36b3d140871 100644 --- a/tests/components/unifiprotect/test_lock.py +++ b/tests/components/unifiprotect/test_lock.py @@ -35,7 +35,7 @@ async def doorlock_fixture( # disable pydantic validation so mocking can happen Doorlock.__config__.validate_assignment = False - lock_obj = mock_doorlock.copy(deep=True) + lock_obj = mock_doorlock.copy() lock_obj._api = mock_entry.api lock_obj.name = "Test Lock" lock_obj.lock_status = LockStatusType.OPEN diff --git a/tests/components/unifiprotect/test_media_player.py b/tests/components/unifiprotect/test_media_player.py index d6404ee3fe5..a4cbc9e8d22 100644 --- a/tests/components/unifiprotect/test_media_player.py +++ b/tests/components/unifiprotect/test_media_player.py @@ -38,7 +38,7 @@ async def camera_fixture( # disable pydantic validation so mocking can happen Camera.__config__.validate_assignment = False - camera_obj = mock_camera.copy(deep=True) + camera_obj = mock_camera.copy() camera_obj._api = mock_entry.api camera_obj.channels[0]._api = mock_entry.api camera_obj.channels[1]._api = mock_entry.api diff --git a/tests/components/unifiprotect/test_number.py b/tests/components/unifiprotect/test_number.py index f516ad64a0b..043feae7925 100644 --- a/tests/components/unifiprotect/test_number.py +++ b/tests/components/unifiprotect/test_number.py @@ -23,6 +23,7 @@ from .conftest import ( MockEntityFixture, assert_entity_counts, ids_from_device_description, + reset_objects, ) @@ -35,13 +36,13 @@ async def light_fixture( # disable pydantic validation so mocking can happen Light.__config__.validate_assignment = False - light_obj = mock_light.copy(deep=True) + light_obj = mock_light.copy() light_obj._api = mock_entry.api light_obj.name = "Test Light" light_obj.light_device_settings.pir_sensitivity = 45 light_obj.light_device_settings.pir_duration = timedelta(seconds=45) - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.lights = { light_obj.id: light_obj, } @@ -65,7 +66,7 @@ async def camera_fixture( # disable pydantic validation so mocking can happen Camera.__config__.validate_assignment = False - camera_obj = mock_camera.copy(deep=True) + camera_obj = mock_camera.copy() camera_obj._api = mock_entry.api camera_obj.channels[0]._api = mock_entry.api camera_obj.channels[1]._api = mock_entry.api @@ -79,7 +80,7 @@ async def camera_fixture( camera_obj.mic_volume = 0 camera_obj.isp_settings.zoom_position = 0 - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.cameras = { camera_obj.id: camera_obj, } @@ -103,12 +104,12 @@ async def doorlock_fixture( # disable pydantic validation so mocking can happen Doorlock.__config__.validate_assignment = False - lock_obj = mock_doorlock.copy(deep=True) + lock_obj = mock_doorlock.copy() lock_obj._api = mock_entry.api lock_obj.name = "Test Lock" lock_obj.auto_close_time = timedelta(seconds=45) - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.doorlocks = { lock_obj.id: lock_obj, } @@ -174,7 +175,7 @@ async def test_number_setup_camera_none( ): """Test number entity setup for camera devices (no features).""" - camera_obj = mock_camera.copy(deep=True) + camera_obj = mock_camera.copy() camera_obj._api = mock_entry.api camera_obj.channels[0]._api = mock_entry.api camera_obj.channels[1]._api = mock_entry.api @@ -185,7 +186,7 @@ async def test_number_setup_camera_none( # has_wdr is an the inverse of has HDR camera_obj.feature_flags.has_hdr = True - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.cameras = { camera_obj.id: camera_obj, } @@ -204,7 +205,7 @@ async def test_number_setup_camera_missing_attr( # disable pydantic validation so mocking can happen Camera.__config__.validate_assignment = False - camera_obj = mock_camera.copy(deep=True) + camera_obj = mock_camera.copy() camera_obj._api = mock_entry.api camera_obj.channels[0]._api = mock_entry.api camera_obj.channels[1]._api = mock_entry.api @@ -214,7 +215,7 @@ async def test_number_setup_camera_missing_attr( Camera.__config__.validate_assignment = True - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.cameras = { camera_obj.id: camera_obj, } diff --git a/tests/components/unifiprotect/test_select.py b/tests/components/unifiprotect/test_select.py index fc0abbe29ca..01263a13cd9 100644 --- a/tests/components/unifiprotect/test_select.py +++ b/tests/components/unifiprotect/test_select.py @@ -44,6 +44,7 @@ from .conftest import ( MockEntityFixture, assert_entity_counts, ids_from_device_description, + reset_objects, ) @@ -59,12 +60,12 @@ async def viewer_fixture( # disable pydantic validation so mocking can happen Viewer.__config__.validate_assignment = False - viewer_obj = mock_viewer.copy(deep=True) + viewer_obj = mock_viewer.copy() viewer_obj._api = mock_entry.api viewer_obj.name = "Test Viewer" viewer_obj.liveview_id = mock_liveview.id - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.viewers = { viewer_obj.id: viewer_obj, } @@ -89,7 +90,7 @@ async def camera_fixture( # disable pydantic validation so mocking can happen Camera.__config__.validate_assignment = False - camera_obj = mock_camera.copy(deep=True) + camera_obj = mock_camera.copy() camera_obj._api = mock_entry.api camera_obj.channels[0]._api = mock_entry.api camera_obj.channels[1]._api = mock_entry.api @@ -102,7 +103,7 @@ async def camera_fixture( camera_obj.lcd_message = None camera_obj.chime_duration = 0 - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.cameras = { camera_obj.id: camera_obj, } @@ -129,14 +130,14 @@ async def light_fixture( # disable pydantic validation so mocking can happen Light.__config__.validate_assignment = False - light_obj = mock_light.copy(deep=True) + light_obj = mock_light.copy() light_obj._api = mock_entry.api light_obj.name = "Test Light" light_obj.camera_id = None light_obj.light_mode_settings.mode = LightModeType.MOTION light_obj.light_mode_settings.enable_at = LightModeEnableType.DARK - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.cameras = {camera.id: camera} mock_entry.api.bootstrap.lights = { light_obj.id: light_obj, @@ -161,7 +162,7 @@ async def camera_none_fixture( # disable pydantic validation so mocking can happen Camera.__config__.validate_assignment = False - camera_obj = mock_camera.copy(deep=True) + camera_obj = mock_camera.copy() camera_obj._api = mock_entry.api camera_obj.channels[0]._api = mock_entry.api camera_obj.channels[1]._api = mock_entry.api @@ -172,7 +173,7 @@ async def camera_none_fixture( camera_obj.recording_settings.mode = RecordingMode.ALWAYS camera_obj.isp_settings.ir_led_mode = IRLEDMode.AUTO - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.cameras = { camera_obj.id: camera_obj, } diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index 4f2540b28c4..eb2558aae3d 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -47,6 +47,7 @@ from .conftest import ( assert_entity_counts, enable_entity, ids_from_device_description, + reset_objects, time_changed, ) @@ -63,7 +64,7 @@ async def sensor_fixture( # disable pydantic validation so mocking can happen Sensor.__config__.validate_assignment = False - sensor_obj = mock_sensor.copy(deep=True) + sensor_obj = mock_sensor.copy() sensor_obj._api = mock_entry.api sensor_obj.name = "Test Sensor" sensor_obj.battery_status.percentage = 10.0 @@ -77,7 +78,7 @@ async def sensor_fixture( sensor_obj.up_since = now sensor_obj.bluetooth_connection_state.signal_strength = -50.0 - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.sensors = { sensor_obj.id: sensor_obj, } @@ -102,7 +103,7 @@ async def sensor_none_fixture( # disable pydantic validation so mocking can happen Sensor.__config__.validate_assignment = False - sensor_obj = mock_sensor.copy(deep=True) + sensor_obj = mock_sensor.copy() sensor_obj._api = mock_entry.api sensor_obj.name = "Test Sensor" sensor_obj.battery_status.percentage = 10.0 @@ -113,7 +114,7 @@ async def sensor_none_fixture( sensor_obj.up_since = now sensor_obj.bluetooth_connection_state.signal_strength = -50.0 - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.sensors = { sensor_obj.id: sensor_obj, } @@ -141,7 +142,7 @@ async def camera_fixture( # disable pydantic validation so mocking can happen Camera.__config__.validate_assignment = False - camera_obj = mock_camera.copy(deep=True) + camera_obj = mock_camera.copy() camera_obj._api = mock_entry.api camera_obj.channels[0]._api = mock_entry.api camera_obj.channels[1]._api = mock_entry.api @@ -162,7 +163,7 @@ async def camera_fixture( camera_obj.stats.storage.rate = 0.1 camera_obj.voltage = 20.0 - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] mock_entry.api.bootstrap.cameras = { camera_obj.id: camera_obj, @@ -262,7 +263,7 @@ async def test_sensor_setup_nvr( ): """Test sensor entity setup for NVR device.""" - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) nvr: NVR = mock_entry.api.bootstrap.nvr nvr.up_since = now nvr.system_info.cpu.average_load = 50.0 @@ -339,7 +340,7 @@ async def test_sensor_nvr_missing_values( ): """Test NVR sensor sensors if no data available.""" - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) nvr: NVR = mock_entry.api.bootstrap.nvr nvr.system_info.memory.available = None nvr.system_info.memory.total = None diff --git a/tests/components/unifiprotect/test_services.py b/tests/components/unifiprotect/test_services.py index 2ad3821cc40..d957fe16b4b 100644 --- a/tests/components/unifiprotect/test_services.py +++ b/tests/components/unifiprotect/test_services.py @@ -5,8 +5,8 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock import pytest -from pyunifiprotect.data import Camera, Light, ModelType -from pyunifiprotect.data.devices import Chime +from pyunifiprotect.data import Camera, Chime, Light, ModelType +from pyunifiprotect.data.bootstrap import ProtectDeviceRef from pyunifiprotect.exceptions import BadRequest from homeassistant.components.unifiprotect.const import ATTR_MESSAGE, DOMAIN @@ -163,6 +163,11 @@ async def test_set_chime_paired_doorbells( mock_entry.api.bootstrap.chimes = { mock_chime.id: mock_chime, } + mock_entry.api.bootstrap.mac_lookup = { + mock_chime.mac.lower(): ProtectDeviceRef( + model=mock_chime.model, id=mock_chime.id + ) + } camera1 = mock_camera.copy() camera1.name = "Test Camera 1" @@ -186,6 +191,12 @@ async def test_set_chime_paired_doorbells( camera1.id: camera1, camera2.id: camera2, } + mock_entry.api.bootstrap.mac_lookup[camera1.mac.lower()] = ProtectDeviceRef( + model=camera1.model, id=camera1.id + ) + mock_entry.api.bootstrap.mac_lookup[camera2.mac.lower()] = ProtectDeviceRef( + model=camera2.model, id=camera2.id + ) await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 8ca1ef9b533..1bd6dbeb349 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -28,6 +28,7 @@ from .conftest import ( assert_entity_counts, enable_entity, ids_from_device_description, + reset_objects, ) CAMERA_SWITCHES_BASIC = [ @@ -51,13 +52,13 @@ async def light_fixture( # disable pydantic validation so mocking can happen Light.__config__.validate_assignment = False - light_obj = mock_light.copy(deep=True) + light_obj = mock_light.copy() light_obj._api = mock_entry.api light_obj.name = "Test Light" light_obj.is_ssh_enabled = False light_obj.light_device_settings.is_indicator_enabled = False - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.lights = { light_obj.id: light_obj, } @@ -81,7 +82,7 @@ async def camera_fixture( # disable pydantic validation so mocking can happen Camera.__config__.validate_assignment = False - camera_obj = mock_camera.copy(deep=True) + camera_obj = mock_camera.copy() camera_obj._api = mock_entry.api camera_obj.channels[0]._api = mock_entry.api camera_obj.channels[1]._api = mock_entry.api @@ -110,7 +111,7 @@ async def camera_fixture( camera_obj.osd_settings.is_debug_enabled = False camera_obj.smart_detect_settings.object_types = [] - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.cameras = { camera_obj.id: camera_obj, } @@ -134,7 +135,7 @@ async def camera_none_fixture( # disable pydantic validation so mocking can happen Camera.__config__.validate_assignment = False - camera_obj = mock_camera.copy(deep=True) + camera_obj = mock_camera.copy() camera_obj._api = mock_entry.api camera_obj.channels[0]._api = mock_entry.api camera_obj.channels[1]._api = mock_entry.api @@ -153,7 +154,7 @@ async def camera_none_fixture( camera_obj.osd_settings.is_logo_enabled = False camera_obj.osd_settings.is_debug_enabled = False - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.cameras = { camera_obj.id: camera_obj, } @@ -177,7 +178,8 @@ async def camera_privacy_fixture( # disable pydantic validation so mocking can happen Camera.__config__.validate_assignment = False - camera_obj = mock_camera.copy(deep=True) + # mock_camera._update_lock = None + camera_obj = mock_camera.copy() camera_obj._api = mock_entry.api camera_obj.channels[0]._api = mock_entry.api camera_obj.channels[1]._api = mock_entry.api @@ -197,7 +199,7 @@ async def camera_privacy_fixture( camera_obj.osd_settings.is_logo_enabled = False camera_obj.osd_settings.is_debug_enabled = False - mock_entry.api.bootstrap.reset_objects() + reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.cameras = { camera_obj.id: camera_obj, } From 0007178d63bd58ce2092d49885a35908ef9a0064 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 21 Jun 2022 05:33:47 +0200 Subject: [PATCH 1628/3516] Add filters and service to Sensibo (#73687) --- .../components/sensibo/binary_sensor.py | 9 ++ homeassistant/components/sensibo/button.py | 68 +++++++++++ homeassistant/components/sensibo/const.py | 1 + homeassistant/components/sensibo/entity.py | 2 + homeassistant/components/sensibo/sensor.py | 11 ++ tests/components/sensibo/fixtures/data.json | 2 +- tests/components/sensibo/test_button.py | 110 ++++++++++++++++++ 7 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/sensibo/button.py create mode 100644 tests/components/sensibo/test_button.py diff --git a/homeassistant/components/sensibo/binary_sensor.py b/homeassistant/components/sensibo/binary_sensor.py index 503717c61e4..04e4a0b873d 100644 --- a/homeassistant/components/sensibo/binary_sensor.py +++ b/homeassistant/components/sensibo/binary_sensor.py @@ -52,6 +52,13 @@ class SensiboDeviceBinarySensorEntityDescription( """Describes Sensibo Motion sensor entity.""" +FILTER_CLEAN_REQUIRED_DESCRIPTION = SensiboDeviceBinarySensorEntityDescription( + key="filter_clean", + device_class=BinarySensorDeviceClass.PROBLEM, + name="Filter Clean Required", + value_fn=lambda data: data.filter_clean, +) + MOTION_SENSOR_TYPES: tuple[SensiboMotionBinarySensorEntityDescription, ...] = ( SensiboMotionBinarySensorEntityDescription( key="alive", @@ -85,6 +92,7 @@ MOTION_DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, .. icon="mdi:motion-sensor", value_fn=lambda data: data.room_occupied, ), + FILTER_CLEAN_REQUIRED_DESCRIPTION, ) PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( @@ -127,6 +135,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( icon="mdi:connection", value_fn=lambda data: data.pure_prime_integration, ), + FILTER_CLEAN_REQUIRED_DESCRIPTION, ) diff --git a/homeassistant/components/sensibo/button.py b/homeassistant/components/sensibo/button.py new file mode 100644 index 00000000000..97ae6321f7e --- /dev/null +++ b/homeassistant/components/sensibo/button.py @@ -0,0 +1,68 @@ +"""Button platform for Sensibo integration.""" +from __future__ import annotations + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import SensiboDataUpdateCoordinator +from .entity import SensiboDeviceBaseEntity + +PARALLEL_UPDATES = 0 + +DEVICE_BUTTON_TYPES: ButtonEntityDescription = ButtonEntityDescription( + key="reset_filter", + name="Reset Filter", + icon="mdi:air-filter", + entity_category=EntityCategory.CONFIG, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Sensibo binary sensor platform.""" + + coordinator: SensiboDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + entities: list[SensiboDeviceButton] = [] + + entities.extend( + SensiboDeviceButton(coordinator, device_id, DEVICE_BUTTON_TYPES) + for device_id, device_data in coordinator.data.parsed.items() + ) + + async_add_entities(entities) + + +class SensiboDeviceButton(SensiboDeviceBaseEntity, ButtonEntity): + """Representation of a Sensibo Device Binary Sensor.""" + + entity_description: ButtonEntityDescription + + def __init__( + self, + coordinator: SensiboDataUpdateCoordinator, + device_id: str, + entity_description: ButtonEntityDescription, + ) -> None: + """Initiate Sensibo Device Button.""" + super().__init__( + coordinator, + device_id, + ) + self.entity_description = entity_description + self._attr_unique_id = f"{device_id}-{entity_description.key}" + self._attr_name = f"{self.device_data.name} {entity_description.name}" + + async def async_press(self) -> None: + """Press the button.""" + result = await self.async_send_command("reset_filter") + if result["status"] == "success": + await self.coordinator.async_request_refresh() + return + raise HomeAssistantError(f"Could not set calibration for device {self.name}") diff --git a/homeassistant/components/sensibo/const.py b/homeassistant/components/sensibo/const.py index 736d663b144..d6dbe957def 100644 --- a/homeassistant/components/sensibo/const.py +++ b/homeassistant/components/sensibo/const.py @@ -14,6 +14,7 @@ DEFAULT_SCAN_INTERVAL = 60 DOMAIN = "sensibo" PLATFORMS = [ Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.CLIMATE, Platform.NUMBER, Platform.SELECT, diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index bf70f499ec6..ac2ec24fac1 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -106,6 +106,8 @@ class SensiboDeviceBaseEntity(SensiboBaseEntity): self._device_id, params, ) + if command == "reset_filter": + result = await self._client.async_reset_filter(self._device_id) return result diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 259a24ab876..fad22fdd677 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -63,6 +63,15 @@ class SensiboDeviceSensorEntityDescription( """Describes Sensibo Motion sensor entity.""" +FILTER_LAST_RESET_DESCRIPTION = SensiboDeviceSensorEntityDescription( + key="filter_last_reset", + device_class=SensorDeviceClass.TIMESTAMP, + name="Filter Last Reset", + icon="mdi:timer", + value_fn=lambda data: data.filter_last_reset, + extra_fn=None, +) + MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( SensiboMotionSensorEntityDescription( key="rssi", @@ -122,6 +131,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( value_fn=lambda data: data.pure_sensitivity, extra_fn=None, ), + FILTER_LAST_RESET_DESCRIPTION, ) DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( @@ -133,6 +143,7 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( value_fn=lambda data: data.timer_time, extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on}, ), + FILTER_LAST_RESET_DESCRIPTION, ) diff --git a/tests/components/sensibo/fixtures/data.json b/tests/components/sensibo/fixtures/data.json index 6c44b44821f..5837296d154 100644 --- a/tests/components/sensibo/fixtures/data.json +++ b/tests/components/sensibo/fixtures/data.json @@ -156,7 +156,7 @@ "time": "2022-03-12T15:24:26Z", "secondsAgo": 4219143 }, - "shouldCleanFilters": false + "shouldCleanFilters": true }, "serviceSubscriptions": [], "roomIsOccupied": true, diff --git a/tests/components/sensibo/test_button.py b/tests/components/sensibo/test_button.py new file mode 100644 index 00000000000..66b7a1258b1 --- /dev/null +++ b/tests/components/sensibo/test_button.py @@ -0,0 +1,110 @@ +"""The test for the sensibo button platform.""" +from __future__ import annotations + +from datetime import datetime, timedelta +from unittest.mock import patch + +from freezegun.api import FrozenDateTimeFactory +from pysensibo.model import SensiboData +from pytest import MonkeyPatch, raises + +from homeassistant.components.button import SERVICE_PRESS +from homeassistant.components.button.const import DOMAIN as BUTTON_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import dt + +from tests.common import async_fire_time_changed + + +async def test_button( + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: MonkeyPatch, + get_data: SensiboData, + freezer: FrozenDateTimeFactory, +) -> None: + """Test the Sensibo button.""" + + state_button = hass.states.get("button.hallway_reset_filter") + state_filter_clean = hass.states.get("binary_sensor.hallway_filter_clean_required") + state_filter_last_reset = hass.states.get("sensor.hallway_filter_last_reset") + + assert state_button.state is STATE_UNKNOWN + assert state_filter_clean.state is STATE_ON + assert state_filter_last_reset.state == "2022-03-12T15:24:26+00:00" + + freezer.move_to(datetime(2022, 6, 19, 20, 0, 0)) + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_reset_filter", + return_value={"status": "success"}, + ): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: state_button.entity_id, + }, + blocking=True, + ) + await hass.async_block_till_done() + + monkeypatch.setattr(get_data.parsed["ABC999111"], "filter_clean", False) + monkeypatch.setattr( + get_data.parsed["ABC999111"], + "filter_last_reset", + datetime(2022, 6, 19, 20, 0, 0, tzinfo=dt.UTC), + ) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state_button = hass.states.get("button.hallway_reset_filter") + state_filter_clean = hass.states.get("binary_sensor.hallway_filter_clean_required") + state_filter_last_reset = hass.states.get("sensor.hallway_filter_last_reset") + assert ( + state_button.state == datetime(2022, 6, 19, 20, 0, 0, tzinfo=dt.UTC).isoformat() + ) + assert state_filter_clean.state is STATE_OFF + assert state_filter_last_reset.state == "2022-06-19T20:00:00+00:00" + + +async def test_button_failure( + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: MonkeyPatch, + get_data: SensiboData, +) -> None: + """Test the Sensibo button fails.""" + + state_button = hass.states.get("button.hallway_reset_filter") + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_reset_filter", + return_value={"status": "failure"}, + ): + with raises(HomeAssistantError): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: state_button.entity_id, + }, + blocking=True, + ) From 9d132521424804cfd482229524124f3ab5742123 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 20 Jun 2022 22:52:41 -0500 Subject: [PATCH 1629/3516] Cleanup device callbacks in unifiprotect (#73463) --- .../components/unifiprotect/binary_sensor.py | 18 +++++++---- .../components/unifiprotect/camera.py | 11 +++++-- homeassistant/components/unifiprotect/data.py | 30 ++++++++++--------- .../components/unifiprotect/entity.py | 23 +++++++------- .../components/unifiprotect/light.py | 6 ++-- homeassistant/components/unifiprotect/lock.py | 6 ++-- .../components/unifiprotect/media_player.py | 12 ++++---- .../components/unifiprotect/migrate.py | 4 +-- .../components/unifiprotect/number.py | 6 ++-- .../components/unifiprotect/select.py | 5 ++-- .../components/unifiprotect/sensor.py | 13 ++++---- 11 files changed, 74 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 34c1119eb60..81832de7d44 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -5,7 +5,15 @@ from copy import copy from dataclasses import dataclass import logging -from pyunifiprotect.data import NVR, Camera, Event, Light, MountType, Sensor +from pyunifiprotect.data import ( + NVR, + Camera, + Event, + Light, + MountType, + ProtectModelWithId, + Sensor, +) from pyunifiprotect.data.nvr import UOSDisk from homeassistant.components.binary_sensor import ( @@ -205,8 +213,8 @@ class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity): entity_description: ProtectBinaryEntityDescription @callback - def _async_update_device_from_protect(self) -> None: - super()._async_update_device_from_protect() + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + super()._async_update_device_from_protect(device) self._attr_is_on = self.entity_description.get_ufp_value(self.device) # UP Sense can be any of the 3 contact sensor device classes @@ -240,8 +248,8 @@ class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity): super().__init__(data, device, description) @callback - def _async_update_device_from_protect(self) -> None: - super()._async_update_device_from_protect() + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + super()._async_update_device_from_protect(device) slot = self._disk.slot self._attr_available = False diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index d59ee59b760..c78e8e2f77a 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -5,7 +5,12 @@ from collections.abc import Generator import logging from pyunifiprotect.api import ProtectApiClient -from pyunifiprotect.data import Camera as UFPCamera, CameraChannel, StateType +from pyunifiprotect.data import ( + Camera as UFPCamera, + CameraChannel, + ProtectModelWithId, + StateType, +) from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.config_entries import ConfigEntry @@ -137,8 +142,8 @@ class ProtectCamera(ProtectDeviceEntity, Camera): ) @callback - def _async_update_device_from_protect(self) -> None: - super()._async_update_device_from_protect() + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + super()._async_update_device_from_protect(device) self.channel = self.device.channels[self.channel.id] motion_enabled = self.device.recording_settings.enable_motion_detection self._attr_motion_detection_enabled = ( diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 2a30e18d586..78a3c5ebac8 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -1,7 +1,7 @@ """Base class for protect data.""" from __future__ import annotations -from collections.abc import Generator, Iterable +from collections.abc import Callable, Generator, Iterable from datetime import timedelta import logging from typing import Any @@ -12,9 +12,10 @@ from pyunifiprotect.data import ( Event, Liveview, ModelType, + ProtectAdoptableDeviceModel, + ProtectModelWithId, WSSubscriptionMessage, ) -from pyunifiprotect.data.base import ProtectAdoptableDeviceModel from pyunifiprotect.exceptions import ClientError, NotAuthorized from homeassistant.config_entries import ConfigEntry @@ -54,7 +55,7 @@ class ProtectData: self._entry = entry self._hass = hass self._update_interval = update_interval - self._subscriptions: dict[str, list[CALLBACK_TYPE]] = {} + self._subscriptions: dict[str, list[Callable[[ProtectModelWithId], None]]] = {} self._unsub_interval: CALLBACK_TYPE | None = None self._unsub_websocket: CALLBACK_TYPE | None = None @@ -123,7 +124,7 @@ class ProtectData: return if message.new_obj.model in DEVICES_WITH_ENTITIES: - self.async_signal_device_id_update(message.new_obj.id) + self._async_signal_device_update(message.new_obj) # trigger update for all Cameras with LCD screens when NVR Doorbell settings updates if "doorbell_settings" in message.changed_data: _LOGGER.debug( @@ -132,15 +133,15 @@ class ProtectData: self.api.bootstrap.nvr.update_all_messages() for camera in self.api.bootstrap.cameras.values(): if camera.feature_flags.has_lcd_screen: - self.async_signal_device_id_update(camera.id) + self._async_signal_device_update(camera) # trigger updates for camera that the event references elif isinstance(message.new_obj, Event): if message.new_obj.camera is not None: - self.async_signal_device_id_update(message.new_obj.camera.id) + self._async_signal_device_update(message.new_obj.camera) elif message.new_obj.light is not None: - self.async_signal_device_id_update(message.new_obj.light.id) + self._async_signal_device_update(message.new_obj.light) elif message.new_obj.sensor is not None: - self.async_signal_device_id_update(message.new_obj.sensor.id) + self._async_signal_device_update(message.new_obj.sensor) # alert user viewport needs restart so voice clients can get new options elif len(self.api.bootstrap.viewers) > 0 and isinstance( message.new_obj, Liveview @@ -157,13 +158,13 @@ class ProtectData: if updates is None: return - self.async_signal_device_id_update(self.api.bootstrap.nvr.id) + self._async_signal_device_update(self.api.bootstrap.nvr) for device in async_get_devices(self.api.bootstrap, DEVICES_THAT_ADOPT): - self.async_signal_device_id_update(device.id) + self._async_signal_device_update(device) @callback def async_subscribe_device_id( - self, device_id: str, update_callback: CALLBACK_TYPE + self, device_id: str, update_callback: Callable[[ProtectModelWithId], None] ) -> CALLBACK_TYPE: """Add an callback subscriber.""" if not self._subscriptions: @@ -179,7 +180,7 @@ class ProtectData: @callback def async_unsubscribe_device_id( - self, device_id: str, update_callback: CALLBACK_TYPE + self, device_id: str, update_callback: Callable[[ProtectModelWithId], None] ) -> None: """Remove a callback subscriber.""" self._subscriptions[device_id].remove(update_callback) @@ -190,14 +191,15 @@ class ProtectData: self._unsub_interval = None @callback - def async_signal_device_id_update(self, device_id: str) -> None: + def _async_signal_device_update(self, device: ProtectModelWithId) -> None: """Call the callbacks for a device_id.""" + device_id = device.id if not self._subscriptions.get(device_id): return _LOGGER.debug("Updating device: %s", device_id) for update_callback in self._subscriptions[device_id]: - update_callback() + update_callback(device) @callback diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 6de0a4c57cb..e06d297ef33 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -14,6 +14,7 @@ from pyunifiprotect.data import ( Light, ModelType, ProtectAdoptableDeviceModel, + ProtectModelWithId, Sensor, StateType, Viewer, @@ -26,7 +27,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from .const import ATTR_EVENT_SCORE, DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN from .data import ProtectData from .models import ProtectRequiredKeysMixin -from .utils import async_device_by_id, get_nested_attr +from .utils import get_nested_attr _LOGGER = logging.getLogger(__name__) @@ -127,7 +128,7 @@ class ProtectDeviceEntity(Entity): self._attr_attribution = DEFAULT_ATTRIBUTION self._async_set_device_info() - self._async_update_device_from_protect() + self._async_update_device_from_protect(device) async def async_update(self) -> None: """Update the entity. @@ -149,14 +150,10 @@ class ProtectDeviceEntity(Entity): ) @callback - def _async_update_device_from_protect(self) -> None: + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: """Update Entity object from Protect device.""" if self.data.last_update_success: - assert self.device.model - device = async_device_by_id( - self.data.api.bootstrap, self.device.id, device_type=self.device.model - ) - assert device is not None + assert isinstance(device, ProtectAdoptableDeviceModel) self.device = device is_connected = ( @@ -174,9 +171,9 @@ class ProtectDeviceEntity(Entity): self._attr_available = is_connected @callback - def _async_updated_event(self) -> None: + def _async_updated_event(self, device: ProtectModelWithId) -> None: """Call back for incoming data.""" - self._async_update_device_from_protect() + self._async_update_device_from_protect(device) self.async_write_ha_state() async def async_added_to_hass(self) -> None: @@ -217,7 +214,7 @@ class ProtectNVREntity(ProtectDeviceEntity): ) @callback - def _async_update_device_from_protect(self) -> None: + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: if self.data.last_update_success: self.device = self.data.api.bootstrap.nvr @@ -254,8 +251,8 @@ class EventThumbnailMixin(ProtectDeviceEntity): return attrs @callback - def _async_update_device_from_protect(self) -> None: - super()._async_update_device_from_protect() + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + super()._async_update_device_from_protect(device) self._event = self._async_get_event() attrs = self.extra_state_attributes or {} diff --git a/homeassistant/components/unifiprotect/light.py b/homeassistant/components/unifiprotect/light.py index 8a6f2f5a371..d84c8406acf 100644 --- a/homeassistant/components/unifiprotect/light.py +++ b/homeassistant/components/unifiprotect/light.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from typing import Any -from pyunifiprotect.data import Light +from pyunifiprotect.data import Light, ProtectModelWithId from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry @@ -59,8 +59,8 @@ class ProtectLight(ProtectDeviceEntity, LightEntity): _attr_supported_color_modes = {ColorMode.BRIGHTNESS} @callback - def _async_update_device_from_protect(self) -> None: - super()._async_update_device_from_protect() + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + super()._async_update_device_from_protect(device) self._attr_is_on = self.device.is_light_on self._attr_brightness = unifi_brightness_to_hass( self.device.light_device_settings.led_level diff --git a/homeassistant/components/unifiprotect/lock.py b/homeassistant/components/unifiprotect/lock.py index c4d56dd1e71..9cef3e19e36 100644 --- a/homeassistant/components/unifiprotect/lock.py +++ b/homeassistant/components/unifiprotect/lock.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from typing import Any -from pyunifiprotect.data import Doorlock, LockStatusType +from pyunifiprotect.data import Doorlock, LockStatusType, ProtectModelWithId from homeassistant.components.lock import LockEntity, LockEntityDescription from homeassistant.config_entries import ConfigEntry @@ -56,8 +56,8 @@ class ProtectLock(ProtectDeviceEntity, LockEntity): self._attr_name = f"{self.device.name} Lock" @callback - def _async_update_device_from_protect(self) -> None: - super()._async_update_device_from_protect() + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + super()._async_update_device_from_protect(device) self._attr_is_locked = False self._attr_is_locking = False diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index 1acd14be130..c4fb1dbe15b 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from typing import Any -from pyunifiprotect.data import Camera +from pyunifiprotect.data import Camera, ProtectModelWithId from pyunifiprotect.exceptions import StreamError from homeassistant.components import media_source @@ -83,8 +83,8 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): self._attr_media_content_type = MEDIA_TYPE_MUSIC @callback - def _async_update_device_from_protect(self) -> None: - super()._async_update_device_from_protect() + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + super()._async_update_device_from_protect(device) self._attr_volume_level = float(self.device.speaker_settings.volume / 100) if ( @@ -110,7 +110,7 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): ): _LOGGER.debug("Stopping playback for %s Speaker", self.device.name) await self.device.stop_audio() - self._async_updated_event() + self._async_updated_event(self.device) async def async_play_media( self, media_type: str, media_id: str, **kwargs: Any @@ -134,11 +134,11 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): raise HomeAssistantError(err) from err else: # update state after starting player - self._async_updated_event() + self._async_updated_event(self.device) # wait until player finishes to update state again await self.device.wait_until_audio_completes() - self._async_updated_event() + self._async_updated_event(self.device) async def async_browse_media( self, media_content_type: str | None = None, media_content_id: str | None = None diff --git a/homeassistant/components/unifiprotect/migrate.py b/homeassistant/components/unifiprotect/migrate.py index 307020caa5c..3273bd80408 100644 --- a/homeassistant/components/unifiprotect/migrate.py +++ b/homeassistant/components/unifiprotect/migrate.py @@ -147,12 +147,12 @@ async def async_migrate_device_ids( try: registry.async_update_entity(entity.entity_id, new_unique_id=new_unique_id) except ValueError as err: - print(err) _LOGGER.warning( - "Could not migrate entity %s (old unique_id: %s, new unique_id: %s)", + "Could not migrate entity %s (old unique_id: %s, new unique_id: %s): %s", entity.entity_id, entity.unique_id, new_unique_id, + err, ) else: count += 1 diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index 1cfb4477673..5a3b048e623 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta -from pyunifiprotect.data import Camera, Doorlock, Light +from pyunifiprotect.data import Camera, Doorlock, Light, ProtectModelWithId from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry @@ -203,8 +203,8 @@ class ProtectNumbers(ProtectDeviceEntity, NumberEntity): self._attr_native_step = self.entity_description.ufp_step @callback - def _async_update_device_from_protect(self) -> None: - super()._async_update_device_from_protect() + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + super()._async_update_device_from_protect(device) self._attr_native_value = self.entity_description.get_ufp_value(self.device) async def async_set_native_value(self, value: float) -> None: diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 2c6c5fa4cc6..6f5c2cfd0d7 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -19,6 +19,7 @@ from pyunifiprotect.data import ( LightModeEnableType, LightModeType, MountType, + ProtectModelWithId, RecordingMode, Sensor, Viewer, @@ -355,8 +356,8 @@ class ProtectSelects(ProtectDeviceEntity, SelectEntity): self._async_set_options() @callback - def _async_update_device_from_protect(self) -> None: - super()._async_update_device_from_protect() + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + super()._async_update_device_from_protect(device) # entities with categories are not exposed for voice and safe to update dynamically if ( diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 48337dc416b..7b14a80ed43 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -12,6 +12,7 @@ from pyunifiprotect.data import ( Event, ProtectAdoptableDeviceModel, ProtectDeviceModel, + ProtectModelWithId, Sensor, ) @@ -540,8 +541,8 @@ class ProtectDeviceSensor(ProtectDeviceEntity, SensorEntity): super().__init__(data, device, description) @callback - def _async_update_device_from_protect(self) -> None: - super()._async_update_device_from_protect() + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + super()._async_update_device_from_protect(device) self._attr_native_value = self.entity_description.get_ufp_value(self.device) @@ -560,8 +561,8 @@ class ProtectNVRSensor(ProtectNVREntity, SensorEntity): super().__init__(data, device, description) @callback - def _async_update_device_from_protect(self) -> None: - super()._async_update_device_from_protect() + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + super()._async_update_device_from_protect(device) self._attr_native_value = self.entity_description.get_ufp_value(self.device) @@ -585,9 +586,9 @@ class ProtectEventSensor(ProtectDeviceSensor, EventThumbnailMixin): return event @callback - def _async_update_device_from_protect(self) -> None: + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: # do not call ProtectDeviceSensor method since we want event to get value here - EventThumbnailMixin._async_update_device_from_protect(self) + EventThumbnailMixin._async_update_device_from_protect(self, device) if self._event is None: self._attr_native_value = OBJECT_TYPE_NONE else: From 4813e6e42068b01173b015331aef8da1bcfcc784 Mon Sep 17 00:00:00 2001 From: rappenze Date: Tue, 21 Jun 2022 09:55:08 +0200 Subject: [PATCH 1630/3516] Code cleanup fibaro lock (#73389) * Code cleanup fibaro lock * Adjust typings as suggested in code review --- homeassistant/components/fibaro/lock.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/fibaro/lock.py b/homeassistant/components/fibaro/lock.py index ac4ce658b65..e8fc4ca7180 100644 --- a/homeassistant/components/fibaro/lock.py +++ b/homeassistant/components/fibaro/lock.py @@ -3,6 +3,8 @@ from __future__ import annotations from typing import Any +from fiblary3.client.v4.models import DeviceModel, SceneModel + from homeassistant.components.lock import ENTITY_ID_FORMAT, LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -33,27 +35,21 @@ async def async_setup_entry( class FibaroLock(FibaroDevice, LockEntity): """Representation of a Fibaro Lock.""" - def __init__(self, fibaro_device): + def __init__(self, fibaro_device: DeviceModel | SceneModel) -> None: """Initialize the Fibaro device.""" - self._state = False super().__init__(fibaro_device) self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) def lock(self, **kwargs: Any) -> None: """Lock the device.""" self.action("secure") - self._state = True + self._attr_is_locked = True def unlock(self, **kwargs: Any) -> None: """Unlock the device.""" self.action("unsecure") - self._state = False + self._attr_is_locked = False - @property - def is_locked(self) -> bool: - """Return true if device is locked.""" - return self._state - - def update(self): + def update(self) -> None: """Update device state.""" - self._state = self.current_binary_state + self._attr_is_locked = self.current_binary_state From ad2a41f774186aeae51934b22bf3c6da71c7c652 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 21 Jun 2022 10:07:37 +0200 Subject: [PATCH 1631/3516] Second run for eliminiate bluepy wheels (#73772) --- .github/workflows/wheels.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 54c2f2594ff..9bfcb48e09a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -249,13 +249,9 @@ jobs: sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file} sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file} sed -i "s|# evdev|evdev|g" ${requirement_file} - sed -i "s|# python-eq3bt|python-eq3bt|g" ${requirement_file} sed -i "s|# pycups|pycups|g" ${requirement_file} sed -i "s|# homekit|homekit|g" ${requirement_file} sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file} - sed -i "s|# avion|avion|g" ${requirement_file} - sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file} - sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file} sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} done From c674af3ba1b4a87682427003a86febc95f69c094 Mon Sep 17 00:00:00 2001 From: Thibaut Date: Tue, 21 Jun 2022 10:22:06 +0200 Subject: [PATCH 1632/3516] Remove hvac_action for Somfy Thermostat (#73776) --- .../overkiz/climate_entities/somfy_thermostat.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py b/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py index cfea49881f4..80859d7561b 100644 --- a/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py +++ b/homeassistant/components/overkiz/climate_entities/somfy_thermostat.py @@ -11,7 +11,6 @@ from homeassistant.components.climate.const import ( PRESET_HOME, PRESET_NONE, ClimateEntityFeature, - HVACAction, HVACMode, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS @@ -83,15 +82,6 @@ class SomfyThermostat(OverkizEntity, ClimateEntity): ) ] - @property - def hvac_action(self) -> str: - """Return the current running hvac operation if supported.""" - if not self.current_temperature or not self.target_temperature: - return HVACAction.IDLE - if self.current_temperature < self.target_temperature: - return HVACAction.HEATING - return HVACAction.IDLE - @property def preset_mode(self) -> str: """Return the current preset mode, e.g., home, away, temp.""" From 1b8dd3368a50e4b1d8130cb2e673eaf4c6976df7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 21 Jun 2022 14:36:22 +0200 Subject: [PATCH 1633/3516] Add checks for lock properties in type-hint plugin (#73729) * Add checks for lock properties in type-hint plugin * Adjust comment * Simplify return-type * Only check properties when ignore_missing_annotations is disabled * Adjust tests * Add comment * Adjust docstring --- pylint/plugins/hass_enforce_type_hints.py | 63 ++++++++++++++++++---- tests/pylint/test_enforce_type_hints.py | 66 +++++++++++++++++++++++ 2 files changed, 118 insertions(+), 11 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index fc7be8ba8e8..62dc6feffc6 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -20,8 +20,8 @@ class TypeHintMatch: """Class for pattern matching.""" function_name: str - arg_types: dict[int, str] return_type: list[str] | str | None | object + arg_types: dict[int, str] | None = None check_return_type_inheritance: bool = False @@ -440,7 +440,42 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), ], ), - ] + ], +} +# Properties are normally checked by mypy, and will only be checked +# by pylint when --ignore-missing-annotations is False +_PROPERTY_MATCH: dict[str, list[ClassTypeHintMatch]] = { + "lock": [ + ClassTypeHintMatch( + base_class="LockEntity", + matches=[ + TypeHintMatch( + function_name="changed_by", + return_type=["str", None], + ), + TypeHintMatch( + function_name="code_format", + return_type=["str", None], + ), + TypeHintMatch( + function_name="is_locked", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="is_locking", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="is_unlocking", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="is_jammed", + return_type=["bool", None], + ), + ], + ), + ], } @@ -621,7 +656,12 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] self._function_matchers.extend(function_matches) if class_matches := _CLASS_MATCH.get(module_platform): - self._class_matchers = class_matches + self._class_matchers.extend(class_matches) + + if not self.linter.config.ignore_missing_annotations and ( + property_matches := _PROPERTY_MATCH.get(module_platform) + ): + self._class_matchers.extend(property_matches) def visit_classdef(self, node: nodes.ClassDef) -> None: """Called when a ClassDef node is visited.""" @@ -659,14 +699,15 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] ): return - # Check that all arguments are correctly annotated. - for key, expected_type in match.arg_types.items(): - if not _is_valid_type(expected_type, annotations[key]): - self.add_message( - "hass-argument-type", - node=node.args.args[key], - args=(key + 1, expected_type), - ) + # Check that all positional arguments are correctly annotated. + if match.arg_types: + for key, expected_type in match.arg_types.items(): + if not _is_valid_type(expected_type, annotations[key]): + self.add_message( + "hass-argument-type", + node=node.args.args[key], + args=(key + 1, expected_type), + ) # Check the return type. if not _is_valid_return_type(match, node.returns): diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index c07014add7f..1f601a881a6 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -469,3 +469,69 @@ def test_valid_config_flow_async_get_options_flow( with assert_no_messages(linter): type_hint_checker.visit_classdef(class_node) + + +def test_invalid_entity_properties( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Check missing entity properties when ignore_missing_annotations is False.""" + # Set bypass option + type_hint_checker.config.ignore_missing_annotations = False + + class_node, prop_node = astroid.extract_node( + """ + class LockEntity(): + pass + + class DoorLock( #@ + LockEntity + ): + @property + def changed_by( #@ + self + ): + pass + """, + "homeassistant.components.pylint_test.lock", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_adds_messages( + linter, + pylint.testutils.MessageTest( + msg_id="hass-return-type", + node=prop_node, + args=["str", None], + line=9, + col_offset=4, + end_line=9, + end_col_offset=18, + ), + ): + type_hint_checker.visit_classdef(class_node) + + +def test_ignore_invalid_entity_properties( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Check invalid entity properties are ignored by default.""" + class_node = astroid.extract_node( + """ + class LockEntity(): + pass + + class DoorLock( #@ + LockEntity + ): + @property + def changed_by( + self + ): + pass + """, + "homeassistant.components.pylint_test.lock", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_no_messages(linter): + type_hint_checker.visit_classdef(class_node) From d399815bea6b05ba85f6beb99a8107320078c9da Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 21 Jun 2022 06:42:41 -0700 Subject: [PATCH 1634/3516] Allow multiple google calendar config entries (#73715) * Support multiple config entries at once * Add test coverage for multiple config entries * Add support for multiple config entries to google config flow * Clear hass.data when unloading config entry * Make google config flow defensive against reuse of the same account * Assign existing google config entries a unique id * Migrate entities to new unique id format * Support muliple accounts per oauth client id * Fix mypy typing errors * Hard fail to keep state consistent, removing graceful degredation * Remove invalid entity regsitry entries --- homeassistant/components/google/__init__.py | 19 ++- homeassistant/components/google/calendar.py | 60 ++++++-- .../components/google/config_flow.py | 31 ++--- homeassistant/components/google/strings.json | 1 + tests/components/google/conftest.py | 25 +++- tests/components/google/test_calendar.py | 124 ++++++++++++++++- tests/components/google/test_config_flow.py | 130 +++++++++++++++--- tests/components/google/test_init.py | 128 ++++++++++++++++- 8 files changed, 459 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index e86f1c43ebf..5553350aa23 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -8,6 +8,7 @@ from typing import Any import aiohttp from gcal_sync.api import GoogleCalendarService +from gcal_sync.exceptions import ApiException, AuthException from gcal_sync.model import DateOrDatetime, Event from oauth2client.file import Storage import voluptuous as vol @@ -220,6 +221,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Google from a config entry.""" hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {} + implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( hass, entry @@ -249,7 +252,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: calendar_service = GoogleCalendarService( ApiAuthImpl(async_get_clientsession(hass), session) ) - hass.data[DOMAIN][DATA_SERVICE] = calendar_service + hass.data[DOMAIN][entry.entry_id][DATA_SERVICE] = calendar_service + + if entry.unique_id is None: + try: + primary_calendar = await calendar_service.async_get_calendar("primary") + except AuthException as err: + raise ConfigEntryAuthFailed from err + except ApiException as err: + raise ConfigEntryNotReady from err + else: + hass.config_entries.async_update_entry(entry, unique_id=primary_calendar.id) # Only expose the add event service if we have the correct permissions if get_feature_access(hass, entry) is FeatureAccess.read_write: @@ -271,7 +284,9 @@ def async_entry_has_scopes(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index e27eb0b1336..3c271a2c3c3 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -23,7 +23,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers import ( + config_validation as cv, + entity_platform, + entity_registry as er, +) from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import Throttle @@ -102,15 +106,25 @@ CREATE_EVENT_SCHEMA = vol.All( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the google calendar platform.""" - calendar_service = hass.data[DOMAIN][DATA_SERVICE] + calendar_service = hass.data[DOMAIN][config_entry.entry_id][DATA_SERVICE] try: result = await calendar_service.async_list_calendars() except ApiException as err: raise PlatformNotReady(str(err)) from err + entity_registry = er.async_get(hass) + registry_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + entity_entry_map = { + entity_entry.unique_id: entity_entry for entity_entry in registry_entries + } + # Yaml configuration may override objects from the API calendars = await hass.async_add_executor_job( load_config, hass.config.path(YAML_DEVICES) @@ -126,7 +140,6 @@ async def async_setup_entry( hass, calendar_item.dict(exclude_unset=True) ) new_calendars.append(calendar_info) - # Yaml calendar config may map one calendar to multiple entities with extra options like # offsets or search criteria. num_entities = len(calendar_info[CONF_ENTITIES]) @@ -138,15 +151,44 @@ async def async_setup_entry( "has been imported to the UI, and should now be removed from google_calendars.yaml" ) entity_name = data[CONF_DEVICE_ID] + # The unique id is based on the config entry and calendar id since multiple accounts + # can have a common calendar id (e.g. `en.usa#holiday@group.v.calendar.google.com`). + # When using google_calendars.yaml with multiple entities for a single calendar, we + # have no way to set a unique id. + if num_entities > 1: + unique_id = None + else: + unique_id = f"{config_entry.unique_id}-{calendar_id}" + # Migrate to new unique_id format which supports multiple config entries as of 2022.7 + for old_unique_id in (calendar_id, f"{calendar_id}-{entity_name}"): + if not (entity_entry := entity_entry_map.get(old_unique_id)): + continue + if unique_id: + _LOGGER.debug( + "Migrating unique_id for %s from %s to %s", + entity_entry.entity_id, + old_unique_id, + unique_id, + ) + entity_registry.async_update_entity( + entity_entry.entity_id, new_unique_id=unique_id + ) + else: + _LOGGER.debug( + "Removing entity registry entry for %s from %s", + entity_entry.entity_id, + old_unique_id, + ) + entity_registry.async_remove( + entity_entry.entity_id, + ) entities.append( GoogleCalendarEntity( calendar_service, calendar_id, data, generate_entity_id(ENTITY_ID_FORMAT, entity_name, hass=hass), - # The google_calendars.yaml file lets users add multiple entities for - # the same calendar id and needs additional disambiguation - f"{calendar_id}-{entity_name}" if num_entities > 1 else calendar_id, + unique_id, entity_enabled, ) ) @@ -163,7 +205,7 @@ async def async_setup_entry( await hass.async_add_executor_job(append_calendars_to_config) platform = entity_platform.async_get_current_platform() - if get_feature_access(hass, entry) is FeatureAccess.read_write: + if get_feature_access(hass, config_entry) is FeatureAccess.read_write: platform.async_register_entity_service( SERVICE_CREATE_EVENT, CREATE_EVENT_SCHEMA, @@ -180,7 +222,7 @@ class GoogleCalendarEntity(CalendarEntity): calendar_id: str, data: dict[str, Any], entity_id: str, - unique_id: str, + unique_id: str | None, entity_enabled: bool, ) -> None: """Create the Calendar event device.""" diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index be516230d2b..046840075ff 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -59,14 +59,6 @@ class OAuth2FlowHandler( self.external_data = info return await super().async_step_creation(info) - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle external yaml configuration.""" - if not self._reauth_config_entry and self._async_current_entries(): - return self.async_abort(reason="already_configured") - return await super().async_step_user(user_input) - async def async_step_auth( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -135,14 +127,14 @@ class OAuth2FlowHandler( async def async_oauth_create_entry(self, data: dict) -> FlowResult: """Create an entry for the flow, or update existing entry.""" - existing_entries = self._async_current_entries() - if existing_entries: - assert len(existing_entries) == 1 - entry = existing_entries[0] - self.hass.config_entries.async_update_entry(entry, data=data) - await self.hass.config_entries.async_reload(entry.entry_id) + if self._reauth_config_entry: + self.hass.config_entries.async_update_entry( + self._reauth_config_entry, data=data + ) + await self.hass.config_entries.async_reload( + self._reauth_config_entry.entry_id + ) return self.async_abort(reason="reauth_successful") - calendar_service = GoogleCalendarService( AccessTokenAuthImpl( async_get_clientsession(self.hass), data["token"]["access_token"] @@ -151,11 +143,12 @@ class OAuth2FlowHandler( try: primary_calendar = await calendar_service.async_get_calendar("primary") except ApiException as err: - _LOGGER.debug("Error reading calendar primary calendar: %s", err) - primary_calendar = None - title = primary_calendar.id if primary_calendar else self.flow_impl.name + _LOGGER.error("Error reading primary calendar: %s", err) + return self.async_abort(reason="cannot_connect") + await self.async_set_unique_id(primary_calendar.id) + self._abort_if_unique_id_configured() return self.async_create_entry( - title=title, + title=primary_calendar.id, data=data, options={ CONF_CALENDAR_ACCESS: get_feature_access(self.hass).name, diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json index 6652806cd0f..3ff75047f70 100644 --- a/homeassistant/components/google/strings.json +++ b/homeassistant/components/google/strings.json @@ -15,6 +15,7 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "code_expired": "Authentication code expired or credential setup is invalid, please try again.", diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 27fb6c993ff..4e251b4b006 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable import datetime +import http from typing import Any, Generator, TypeVar from unittest.mock import Mock, mock_open, patch @@ -27,6 +28,7 @@ YieldFixture = Generator[_T, None, None] CALENDAR_ID = "qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com" +EMAIL_ADDRESS = "user@gmail.com" # Entities can either be created based on data directly from the API, or from # the yaml config that overrides the entity name and other settings. A test @@ -53,6 +55,9 @@ TEST_API_CALENDAR = { "defaultReminders": [], } +CLIENT_ID = "client-id" +CLIENT_SECRET = "client-secret" + @pytest.fixture def test_api_calendar(): @@ -148,8 +153,8 @@ def creds( """Fixture that defines creds used in the test.""" return OAuth2Credentials( access_token="ACCESS_TOKEN", - client_id="client-id", - client_secret="client-secret", + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, refresh_token="REFRESH_TOKEN", token_expiry=token_expiry, token_uri="http://example.com", @@ -178,8 +183,15 @@ def config_entry_options() -> dict[str, Any] | None: return None +@pytest.fixture +def config_entry_unique_id() -> str: + """Fixture that returns the default config entry unique id.""" + return EMAIL_ADDRESS + + @pytest.fixture def config_entry( + config_entry_unique_id: str, token_scopes: list[str], config_entry_token_expiry: float, config_entry_options: dict[str, Any] | None, @@ -187,6 +199,7 @@ def config_entry( """Fixture to create a config entry for the integration.""" return MockConfigEntry( domain=DOMAIN, + unique_id=config_entry_unique_id, data={ "auth_implementation": "device_auth", "token": { @@ -271,12 +284,16 @@ def mock_calendar_get( """Fixture for returning a calendar get response.""" def _result( - calendar_id: str, response: dict[str, Any], exc: ClientError | None = None + calendar_id: str, + response: dict[str, Any], + exc: ClientError | None = None, + status: http.HTTPStatus = http.HTTPStatus.OK, ) -> None: aioclient_mock.get( f"{API_BASE_URL}/calendars/{calendar_id}", json=response, exc=exc, + status=status, ) return @@ -315,7 +332,7 @@ def google_config_track_new() -> None: @pytest.fixture def google_config(google_config_track_new: bool | None) -> dict[str, Any]: """Fixture for overriding component config.""" - google_config = {CONF_CLIENT_ID: "client-id", CONF_CLIENT_SECRET: "client-secret"} + google_config = {CONF_CLIENT_ID: CLIENT_ID, CONF_CLIENT_SECRET: CLIENT_SECRET} if google_config_track_new is not None: google_config[CONF_TRACK_NEW] = google_config_track_new return google_config diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 85711014e72..9a0cc2e47fa 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -13,7 +13,9 @@ from aiohttp.client_exceptions import ClientError from gcal_sync.auth import API_BASE_URL import pytest -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.components.google.const import DOMAIN +from homeassistant.const import STATE_OFF, STATE_ON, Platform +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.template import DATE_STR_FORMAT import homeassistant.util.dt as dt_util @@ -665,3 +667,123 @@ async def test_future_event_offset_update_behavior( state = hass.states.get(TEST_ENTITY) assert state.state == STATE_OFF assert state.attributes["offset_reached"] + + +async def test_unique_id( + hass, + mock_events_list_items, + mock_token_read, + component_setup, + config_entry, +): + """Test entity is created with a unique id based on the config entry.""" + mock_events_list_items([]) + assert await component_setup() + + entity_registry = er.async_get(hass) + registry_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + assert {entry.unique_id for entry in registry_entries} == { + f"{config_entry.unique_id}-{CALENDAR_ID}" + } + + +@pytest.mark.parametrize( + "old_unique_id", [CALENDAR_ID, f"{CALENDAR_ID}-we_are_we_are_a_test_calendar"] +) +async def test_unique_id_migration( + hass, + mock_events_list_items, + mock_token_read, + component_setup, + config_entry, + old_unique_id, +): + """Test that old unique id format is migrated to the new format that supports multiple accounts.""" + entity_registry = er.async_get(hass) + + # Create an entity using the old unique id format + entity_registry.async_get_or_create( + DOMAIN, + Platform.CALENDAR, + unique_id=old_unique_id, + config_entry=config_entry, + ) + registry_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + assert {entry.unique_id for entry in registry_entries} == {old_unique_id} + + mock_events_list_items([]) + assert await component_setup() + + registry_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + assert {entry.unique_id for entry in registry_entries} == { + f"{config_entry.unique_id}-{CALENDAR_ID}" + } + + +@pytest.mark.parametrize( + "calendars_config", + [ + [ + { + "cal_id": CALENDAR_ID, + "entities": [ + { + "device_id": "backyard_light", + "name": "Backyard Light", + "search": "#Backyard", + }, + { + "device_id": "front_light", + "name": "Front Light", + "search": "#Front", + }, + ], + } + ], + ], +) +async def test_invalid_unique_id_cleanup( + hass, + mock_events_list_items, + mock_token_read, + component_setup, + config_entry, + mock_calendars_yaml, +): + """Test that old unique id format that is not actually unique is removed.""" + entity_registry = er.async_get(hass) + + # Create an entity using the old unique id format + entity_registry.async_get_or_create( + DOMAIN, + Platform.CALENDAR, + unique_id=f"{CALENDAR_ID}-backyard_light", + config_entry=config_entry, + ) + entity_registry.async_get_or_create( + DOMAIN, + Platform.CALENDAR, + unique_id=f"{CALENDAR_ID}-front_light", + config_entry=config_entry, + ) + registry_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + assert {entry.unique_id for entry in registry_entries} == { + f"{CALENDAR_ID}-backyard_light", + f"{CALENDAR_ID}-front_light", + } + + mock_events_list_items([]) + assert await component_setup() + + registry_entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + assert not registry_entries diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index a346b02e6c2..00f50e129e4 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -27,13 +27,18 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util.dt import utcnow -from .conftest import ComponentSetup, YieldFixture +from .conftest import ( + CLIENT_ID, + CLIENT_SECRET, + EMAIL_ADDRESS, + ComponentSetup, + YieldFixture, +) from tests.common import MockConfigEntry, async_fire_time_changed CODE_CHECK_INTERVAL = 1 CODE_CHECK_ALARM_TIMEDELTA = datetime.timedelta(seconds=CODE_CHECK_INTERVAL * 2) -EMAIL_ADDRESS = "user@gmail.com" @pytest.fixture(autouse=True) @@ -70,6 +75,12 @@ async def mock_exchange(creds: OAuth2Credentials) -> YieldFixture[Mock]: yield mock +@pytest.fixture +async def primary_calendar_email() -> str: + """Fixture to override the google calendar primary email address.""" + return EMAIL_ADDRESS + + @pytest.fixture async def primary_calendar_error() -> ClientError | None: """Fixture for tests to inject an error during calendar lookup.""" @@ -78,12 +89,14 @@ async def primary_calendar_error() -> ClientError | None: @pytest.fixture(autouse=True) async def primary_calendar( - mock_calendar_get: Callable[[...], None], primary_calendar_error: ClientError | None + mock_calendar_get: Callable[[...], None], + primary_calendar_error: ClientError | None, + primary_calendar_email: str, ) -> None: """Fixture to return the primary calendar.""" mock_calendar_get( "primary", - {"id": EMAIL_ADDRESS, "summary": "Personal"}, + {"id": primary_calendar_email, "summary": "Personal"}, exc=primary_calendar_error, ) @@ -165,7 +178,7 @@ async def test_full_flow_application_creds( assert await component_setup() await async_import_client_credential( - hass, DOMAIN, ClientCredential("client-id", "client-secret"), "imported-cred" + hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "imported-cred" ) result = await hass.config_entries.flow.async_init( @@ -327,26 +340,107 @@ async def test_exchange_error( assert len(entries) == 1 -async def test_existing_config_entry( +@pytest.mark.parametrize("google_config", [None]) +async def test_duplicate_config_entries( hass: HomeAssistant, + mock_code_flow: Mock, + mock_exchange: Mock, + config: dict[str, Any], config_entry: MockConfigEntry, component_setup: ComponentSetup, ) -> None: - """Test can't configure when config entry already exists.""" + """Test that the same account cannot be setup twice.""" + assert await component_setup() + await async_import_client_credential( + hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "imported-cred" + ) + + # Load a config entry config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.google.async_setup_entry", return_value=True + ) as mock_setup: + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert await component_setup() - + # Start a new config flow using the same credential result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result.get("type") == "progress" + assert result.get("step_id") == "auth" + assert "description_placeholders" in result + assert "url" in result["description_placeholders"] + + # Run one tick to invoke the credential exchange check + now = utcnow() + await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure(flow_id=result["flow_id"]) assert result.get("type") == "abort" assert result.get("reason") == "already_configured" +@pytest.mark.parametrize( + "google_config,primary_calendar_email", [(None, "another-email@example.com")] +) +async def test_multiple_config_entries( + hass: HomeAssistant, + mock_code_flow: Mock, + mock_exchange: Mock, + config: dict[str, Any], + config_entry: MockConfigEntry, + component_setup: ComponentSetup, +) -> None: + """Test that multiple config entries can be set at once.""" + assert await component_setup() + await async_import_client_credential( + hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "imported-cred" + ) + + # Load a config entry + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.google.async_setup_entry", return_value=True + ) as mock_setup: + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + + # Start a new config flow + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "progress" + assert result.get("step_id") == "auth" + assert "description_placeholders" in result + assert "url" in result["description_placeholders"] + + with patch( + "homeassistant.components.google.async_setup_entry", return_value=True + ) as mock_setup: + # Run one tick to invoke the credential exchange check + now = utcnow() + await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) + await hass.async_block_till_done() + result = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"] + ) + assert result.get("type") == "create_entry" + assert result.get("title") == "another-email@example.com" + assert len(mock_setup.mock_calls) == 1 + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 2 + + async def test_missing_configuration( hass: HomeAssistant, ) -> None: @@ -385,8 +479,8 @@ async def test_wrong_configuration( config_entry_oauth2_flow.LocalOAuth2Implementation( hass, DOMAIN, - "client-id", - "client-secret", + CLIENT_ID, + CLIENT_SECRET, "http://example/authorize", "http://example/token", ), @@ -499,7 +593,7 @@ async def test_reauth_flow( @pytest.mark.parametrize("primary_calendar_error", [ClientError()]) -async def test_title_lookup_failure( +async def test_calendar_lookup_failure( hass: HomeAssistant, mock_code_flow: Mock, mock_exchange: Mock, @@ -516,9 +610,7 @@ async def test_title_lookup_failure( assert "description_placeholders" in result assert "url" in result["description_placeholders"] - with patch( - "homeassistant.components.google.async_setup_entry", return_value=True - ) as mock_setup: + with patch("homeassistant.components.google.async_setup_entry", return_value=True): # Run one tick to invoke the credential exchange check now = utcnow() await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) @@ -527,12 +619,8 @@ async def test_title_lookup_failure( flow_id=result["flow_id"] ) - assert result.get("type") == "create_entry" - assert result.get("title") == "Import from configuration.yaml" - - assert len(mock_setup.mock_calls) == 1 - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 + assert result.get("type") == "abort" + assert result.get("reason") == "cannot_connect" async def test_options_flow_triggers_reauth( diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index f9391a82b6a..d9b9ec8ed03 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -26,6 +26,7 @@ from homeassistant.util.dt import utcnow from .conftest import ( CALENDAR_ID, + EMAIL_ADDRESS, TEST_API_ENTITY, TEST_API_ENTITY_NAME, TEST_YAML_ENTITY, @@ -62,6 +63,7 @@ def setup_config_entry( ) -> MockConfigEntry: """Fixture to initialize the config entry.""" config_entry.add_to_hass(hass) + return config_entry @pytest.fixture( @@ -219,11 +221,9 @@ async def test_calendar_yaml_error( assert hass.states.get(TEST_API_ENTITY) -@pytest.mark.parametrize("calendars_config", [[]]) -async def test_found_calendar_from_api( +async def test_init_calendar( hass: HomeAssistant, component_setup: ComponentSetup, - mock_calendars_yaml: None, mock_calendars_list: ApiResult, test_api_calendar: dict[str, Any], mock_events_list: ApiResult, @@ -275,6 +275,59 @@ async def test_load_application_credentials( assert not hass.states.get(TEST_YAML_ENTITY) +async def test_multiple_config_entries( + hass: HomeAssistant, + component_setup: ComponentSetup, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + mock_events_list: ApiResult, + config_entry: MockConfigEntry, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test finding a calendar from the API.""" + + assert await component_setup() + + config_entry1 = MockConfigEntry( + domain=DOMAIN, data=config_entry.data, unique_id=EMAIL_ADDRESS + ) + calendar1 = { + **test_api_calendar, + "id": "calendar-id1", + "summary": "Example Calendar 1", + } + + mock_calendars_list({"items": [calendar1]}) + mock_events_list({}, calendar_id="calendar-id1") + config_entry1.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry1.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("calendar.example_calendar_1") + assert state + assert state.name == "Example Calendar 1" + assert state.state == STATE_OFF + + config_entry2 = MockConfigEntry( + domain=DOMAIN, data=config_entry.data, unique_id="other-address@example.com" + ) + calendar2 = { + **test_api_calendar, + "id": "calendar-id2", + "summary": "Example Calendar 2", + } + aioclient_mock.clear_requests() + mock_calendars_list({"items": [calendar2]}) + mock_events_list({}, calendar_id="calendar-id2") + config_entry2.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry2.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("calendar.example_calendar_2") + assert state + assert state.name == "Example Calendar 2" + + @pytest.mark.parametrize( "calendars_config_track,expected_state,google_config_track_new", [ @@ -795,3 +848,72 @@ async def test_update_will_reload( ) await hass.async_block_till_done() mock_reload.assert_called_once() + + +@pytest.mark.parametrize("config_entry_unique_id", [None]) +async def test_assign_unique_id( + hass: HomeAssistant, + component_setup: ComponentSetup, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + mock_events_list: ApiResult, + mock_calendar_get: Callable[[...], None], + setup_config_entry: MockConfigEntry, +) -> None: + """Test an existing config is updated to have unique id if it does not exist.""" + + assert setup_config_entry.state is ConfigEntryState.NOT_LOADED + assert setup_config_entry.unique_id is None + + mock_calendar_get( + "primary", + {"id": EMAIL_ADDRESS, "summary": "Personal"}, + ) + + mock_calendars_list({"items": [test_api_calendar]}) + mock_events_list({}) + assert await component_setup() + + assert setup_config_entry.state is ConfigEntryState.LOADED + assert setup_config_entry.unique_id == EMAIL_ADDRESS + + +@pytest.mark.parametrize( + "config_entry_unique_id,request_status,config_entry_status", + [ + (None, http.HTTPStatus.BAD_REQUEST, ConfigEntryState.SETUP_RETRY), + ( + None, + http.HTTPStatus.UNAUTHORIZED, + ConfigEntryState.SETUP_ERROR, + ), + ], +) +async def test_assign_unique_id_failure( + hass: HomeAssistant, + component_setup: ComponentSetup, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + mock_events_list: ApiResult, + mock_calendar_get: Callable[[...], None], + setup_config_entry: MockConfigEntry, + request_status: http.HTTPStatus, + config_entry_status: ConfigEntryState, +) -> None: + """Test lookup failures during unique id assignment are handled gracefully.""" + + assert setup_config_entry.state is ConfigEntryState.NOT_LOADED + assert setup_config_entry.unique_id is None + + mock_calendar_get( + "primary", + {}, + status=request_status, + ) + + mock_calendars_list({"items": [test_api_calendar]}) + mock_events_list({}) + assert await component_setup() + + assert setup_config_entry.state is config_entry_status + assert setup_config_entry.unique_id is None From a96aa64dd1dc9b1f47e14f3f4bb85b39560a37bd Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Tue, 21 Jun 2022 16:35:22 +0200 Subject: [PATCH 1635/3516] Add Somfy to supported brands of Overkiz integration (#73786) Add Somfy to supported brands --- homeassistant/components/overkiz/manifest.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index 432988a6dc4..fa89be5d19e 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -24,6 +24,7 @@ "flexom": "Bouygues Flexom", "hi_kumo": "Hitachi Hi Kumo", "nexity": "Nexity Eugénie", - "rexel": "Rexel Energeasy Connect" + "rexel": "Rexel Energeasy Connect", + "somfy": "Somfy" } } From cf9cab900e3555ea3cee1f09a03b45ed76750105 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 21 Jun 2022 07:36:13 -0700 Subject: [PATCH 1636/3516] Allow multiple configuration entries for nest integration (#73720) * Add multiple config entry support for Nest * Set a config entry unique id based on nest project id * Add missing translations and remove untested committed * Remove unnecessary translation * Remove dead code * Remove old handling to avoid duplicate error logs --- homeassistant/components/nest/__init__.py | 40 ++++++------- homeassistant/components/nest/camera_sdm.py | 4 +- homeassistant/components/nest/climate_sdm.py | 4 +- homeassistant/components/nest/config_flow.py | 17 +++--- homeassistant/components/nest/device_info.py | 30 +++++++++- .../components/nest/device_trigger.py | 46 ++++---------- homeassistant/components/nest/diagnostics.py | 9 ++- homeassistant/components/nest/media_source.py | 27 +++------ homeassistant/components/nest/sensor_sdm.py | 4 +- homeassistant/components/nest/strings.json | 2 +- tests/components/nest/conftest.py | 10 +++- tests/components/nest/test_config_flow_sdm.py | 60 +++++++++++++------ tests/components/nest/test_init_sdm.py | 24 ++++++-- 13 files changed, 161 insertions(+), 116 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 29c2d817acd..0e0128136ad 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -77,9 +77,6 @@ from .media_source import ( _LOGGER = logging.getLogger(__name__) -DATA_NEST_UNAVAILABLE = "nest_unavailable" - -NEST_SETUP_NOTIFICATION = "nest_setup" SENSOR_SCHEMA = vol.Schema( {vol.Optional(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list)} @@ -179,13 +176,16 @@ class SignalUpdateCallback: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Nest from a config entry with dispatch between old/new flows.""" - config_mode = config_flow.get_config_mode(hass) if config_mode == config_flow.ConfigMode.LEGACY: return await async_setup_legacy_entry(hass, entry) if config_mode == config_flow.ConfigMode.SDM: await async_import_config(hass, entry) + elif entry.unique_id != entry.data[CONF_PROJECT_ID]: + hass.config_entries.async_update_entry( + entry, unique_id=entry.data[CONF_PROJECT_ID] + ) subscriber = await api.new_subscriber(hass, entry) if not subscriber: @@ -205,31 +205,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await subscriber.start_async() except AuthException as err: - _LOGGER.debug("Subscriber authentication error: %s", err) - raise ConfigEntryAuthFailed from err + raise ConfigEntryAuthFailed( + f"Subscriber authentication error: {str(err)}" + ) from err except ConfigurationException as err: _LOGGER.error("Configuration error: %s", err) subscriber.stop_async() return False except SubscriberException as err: - if DATA_NEST_UNAVAILABLE not in hass.data[DOMAIN]: - _LOGGER.error("Subscriber error: %s", err) - hass.data[DOMAIN][DATA_NEST_UNAVAILABLE] = True subscriber.stop_async() - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady(f"Subscriber error: {str(err)}") from err try: device_manager = await subscriber.async_get_device_manager() except ApiException as err: - if DATA_NEST_UNAVAILABLE not in hass.data[DOMAIN]: - _LOGGER.error("Device manager error: %s", err) - hass.data[DOMAIN][DATA_NEST_UNAVAILABLE] = True subscriber.stop_async() - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady(f"Device manager error: {str(err)}") from err - hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None) - hass.data[DOMAIN][DATA_SUBSCRIBER] = subscriber - hass.data[DOMAIN][DATA_DEVICE_MANAGER] = device_manager + hass.data[DOMAIN][entry.entry_id] = { + DATA_SUBSCRIBER: subscriber, + DATA_DEVICE_MANAGER: device_manager, + } hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -252,7 +248,9 @@ async def async_import_config(hass: HomeAssistant, entry: ConfigEntry) -> None: CONF_SUBSCRIBER_ID_IMPORTED: True, # Don't delete user managed subscriber } ) - hass.config_entries.async_update_entry(entry, data=new_data) + hass.config_entries.async_update_entry( + entry, data=new_data, unique_id=new_data[CONF_PROJECT_ID] + ) if entry.data["auth_implementation"] == INSTALLED_AUTH_DOMAIN: # App Auth credentials have been deprecated and must be re-created @@ -288,13 +286,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Legacy API return True _LOGGER.debug("Stopping nest subscriber") - subscriber = hass.data[DOMAIN][DATA_SUBSCRIBER] + subscriber = hass.data[DOMAIN][entry.entry_id][DATA_SUBSCRIBER] subscriber.stop_async() unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - hass.data[DOMAIN].pop(DATA_SUBSCRIBER) - hass.data[DOMAIN].pop(DATA_DEVICE_MANAGER) - hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None) + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index a089163a826..4e38338aee8 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -45,7 +45,9 @@ async def async_setup_sdm_entry( ) -> None: """Set up the cameras.""" - device_manager: DeviceManager = hass.data[DOMAIN][DATA_DEVICE_MANAGER] + device_manager: DeviceManager = hass.data[DOMAIN][entry.entry_id][ + DATA_DEVICE_MANAGER + ] entities = [] for device in device_manager.devices.values(): if ( diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 6ee988b714f..452c30073da 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -82,7 +82,9 @@ async def async_setup_sdm_entry( ) -> None: """Set up the client entities.""" - device_manager: DeviceManager = hass.data[DOMAIN][DATA_DEVICE_MANAGER] + device_manager: DeviceManager = hass.data[DOMAIN][entry.entry_id][ + DATA_DEVICE_MANAGER + ] entities = [] for device in device_manager.devices.values(): if ThermostatHvacTrait.NAME in device.traits: diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index bacd61447f5..479a54edbc7 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -276,10 +276,6 @@ class NestFlowHandler( if self.config_mode == ConfigMode.LEGACY: return await self.async_step_init(user_input) self._data[DATA_SDM] = {} - # Reauth will update an existing entry - entries = self._async_current_entries() - if entries and self.source != SOURCE_REAUTH: - return self.async_abort(reason="single_instance_allowed") if self.source == SOURCE_REAUTH: return await super().async_step_user(user_input) # Application Credentials setup needs information from the user @@ -339,13 +335,16 @@ class NestFlowHandler( """Collect device access project from user input.""" errors = {} if user_input is not None: - if user_input[CONF_PROJECT_ID] == self._data[CONF_CLOUD_PROJECT_ID]: + project_id = user_input[CONF_PROJECT_ID] + if project_id == self._data[CONF_CLOUD_PROJECT_ID]: _LOGGER.error( "Device Access Project ID and Cloud Project ID must not be the same, see documentation" ) errors[CONF_PROJECT_ID] = "wrong_project_id" else: self._data.update(user_input) + await self.async_set_unique_id(project_id) + self._abort_if_unique_id_configured() return await super().async_step_user() return self.async_show_form( @@ -465,13 +464,11 @@ class NestFlowHandler( async def async_step_finish(self, data: dict[str, Any] | None = None) -> FlowResult: """Create an entry for the SDM flow.""" assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" - await self.async_set_unique_id(DOMAIN) - # Update existing config entry when in the reauth flow. This - # integration only supports one config entry so remove any prior entries - # added before the "single_instance_allowed" check was added + # Update existing config entry when in the reauth flow. if entry := self._async_reauth_entry(): self.hass.config_entries.async_update_entry( - entry, data=self._data, unique_id=DOMAIN + entry, + data=self._data, ) await self.hass.config_entries.async_reload(entry.entry_id) return self.async_abort(reason="reauth_successful") diff --git a/homeassistant/components/nest/device_info.py b/homeassistant/components/nest/device_info.py index b9aa52aa2c6..2d2b01d3849 100644 --- a/homeassistant/components/nest/device_info.py +++ b/homeassistant/components/nest/device_info.py @@ -2,12 +2,16 @@ from __future__ import annotations +from collections.abc import Mapping + from google_nest_sdm.device import Device from google_nest_sdm.device_traits import InfoTrait +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo -from .const import DOMAIN +from .const import DATA_DEVICE_MANAGER, DOMAIN DEVICE_TYPE_MAP: dict[str, str] = { "sdm.devices.types.CAMERA": "Camera", @@ -66,3 +70,27 @@ class NestDeviceInfo: names = [name for id, name in items] return " ".join(names) return None + + +@callback +def async_nest_devices(hass: HomeAssistant) -> Mapping[str, Device]: + """Return a mapping of all nest devices for all config entries.""" + devices = {} + for entry_id in hass.data[DOMAIN]: + if not (device_manager := hass.data[DOMAIN][entry_id].get(DATA_DEVICE_MANAGER)): + continue + devices.update( + {device.name: device for device in device_manager.devices.values()} + ) + return devices + + +@callback +def async_nest_devices_by_device_id(hass: HomeAssistant) -> Mapping[str, Device]: + """Return a mapping of all nest devices by home assistant device id, for all config entries.""" + device_registry = dr.async_get(hass) + devices = {} + for nest_device_id, device in async_nest_devices(hass).items(): + if device_entry := device_registry.async_get_device({(DOMAIN, nest_device_id)}): + devices[device_entry.id] = device + return devices diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index 05769a407f2..cb546c87ee4 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -1,7 +1,6 @@ """Provides device automations for Nest.""" from __future__ import annotations -from google_nest_sdm.device_manager import DeviceManager import voluptuous as vol from homeassistant.components.automation import ( @@ -14,11 +13,11 @@ from homeassistant.components.device_automation.exceptions import ( ) from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers import device_registry as dr +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers.typing import ConfigType -from .const import DATA_DEVICE_MANAGER, DOMAIN +from .const import DOMAIN +from .device_info import async_nest_devices_by_device_id from .events import DEVICE_TRAIT_TRIGGER_MAP, NEST_EVENT DEVICE = "device" @@ -32,43 +31,18 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( ) -@callback -def async_get_nest_device_id(hass: HomeAssistant, device_id: str) -> str | None: - """Get the nest API device_id from the HomeAssistant device_id.""" - device_registry = dr.async_get(hass) - if device := device_registry.async_get(device_id): - for (domain, unique_id) in device.identifiers: - if domain == DOMAIN: - return unique_id - return None - - -@callback -def async_get_device_trigger_types( - hass: HomeAssistant, nest_device_id: str -) -> list[str]: - """List event triggers supported for a Nest device.""" - device_manager: DeviceManager = hass.data[DOMAIN][DATA_DEVICE_MANAGER] - 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 = [ - trigger_type - for trait in nest_device.traits - if (trigger_type := DEVICE_TRAIT_TRIGGER_MAP.get(trait)) - ] - return trigger_types - - async def async_get_triggers( hass: HomeAssistant, device_id: str ) -> list[dict[str, str]]: """List device triggers for a Nest device.""" - nest_device_id = async_get_nest_device_id(hass, device_id) - if not nest_device_id: + devices = async_nest_devices_by_device_id(hass) + if not (device := devices.get(device_id)): raise InvalidDeviceAutomationConfig(f"Device not found {device_id}") - trigger_types = async_get_device_trigger_types(hass, nest_device_id) + trigger_types = [ + trigger_type + for trait in device.traits + if (trigger_type := DEVICE_TRAIT_TRIGGER_MAP.get(trait)) + ] return [ { CONF_PLATFORM: DEVICE, diff --git a/homeassistant/components/nest/diagnostics.py b/homeassistant/components/nest/diagnostics.py index c21842d5939..d350b719608 100644 --- a/homeassistant/components/nest/diagnostics.py +++ b/homeassistant/components/nest/diagnostics.py @@ -27,10 +27,15 @@ def _async_get_nest_devices( if DATA_SDM not in config_entry.data: return {} - if DATA_DEVICE_MANAGER not in hass.data[DOMAIN]: + if ( + config_entry.entry_id not in hass.data[DOMAIN] + or DATA_DEVICE_MANAGER not in hass.data[DOMAIN][config_entry.entry_id] + ): return {} - device_manager: DeviceManager = hass.data[DOMAIN][DATA_DEVICE_MANAGER] + device_manager: DeviceManager = hass.data[DOMAIN][config_entry.entry_id][ + DATA_DEVICE_MANAGER + ] return device_manager.devices diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index e4e26153b3a..4614d4b1ed4 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -25,7 +25,6 @@ import os from google_nest_sdm.camera_traits import CameraClipPreviewTrait, CameraEventImageTrait from google_nest_sdm.device import Device -from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.event import EventImageType, ImageEventBase from google_nest_sdm.event_media import ( ClipPreviewSession, @@ -57,8 +56,8 @@ from homeassistant.helpers.storage import Store from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.util import dt as dt_util -from .const import DATA_DEVICE_MANAGER, DOMAIN -from .device_info import NestDeviceInfo +from .const import DOMAIN +from .device_info import NestDeviceInfo, async_nest_devices_by_device_id from .events import EVENT_NAME_MAP, MEDIA_SOURCE_EVENT_TITLE_MAP _LOGGER = logging.getLogger(__name__) @@ -271,21 +270,13 @@ async def async_get_media_source(hass: HomeAssistant) -> MediaSource: @callback def async_get_media_source_devices(hass: HomeAssistant) -> Mapping[str, Device]: """Return a mapping of device id to eligible Nest event media devices.""" - if DATA_DEVICE_MANAGER not in hass.data[DOMAIN]: - # Integration unloaded, or is legacy nest integration - return {} - device_manager: DeviceManager = hass.data[DOMAIN][DATA_DEVICE_MANAGER] - device_registry = dr.async_get(hass) - devices = {} - for device in device_manager.devices.values(): - if not ( - CameraEventImageTrait.NAME in device.traits - or CameraClipPreviewTrait.NAME in device.traits - ): - continue - if device_entry := device_registry.async_get_device({(DOMAIN, device.name)}): - devices[device_entry.id] = device - return devices + devices = async_nest_devices_by_device_id(hass) + return { + device_id: device + for device_id, device in devices.items() + if CameraEventImageTrait.NAME in device.traits + or CameraClipPreviewTrait.NAME in device.traits + } @dataclass diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index d33aa3eff8b..c6d1c8b2b30 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -36,7 +36,9 @@ async def async_setup_sdm_entry( ) -> None: """Set up the sensors.""" - device_manager: DeviceManager = hass.data[DOMAIN][DATA_DEVICE_MANAGER] + device_manager: DeviceManager = hass.data[DOMAIN][entry.entry_id][ + DATA_DEVICE_MANAGER + ] entities: list[SensorEntity] = [] for device in device_manager.devices.values(): if TemperatureTrait.NAME in device.traits: diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 212903179b7..0a13de41511 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -69,7 +69,7 @@ "subscriber_error": "Unknown subscriber error, see logs" }, "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "unknown_authorize_url_generation": "[%key:common::config_flow::abort::unknown_authorize_url_generation%]", diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index bacb3924bcd..458685cde70 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -24,6 +24,7 @@ from homeassistant.setup import async_setup_component from .common import ( DEVICE_ID, + PROJECT_ID, SUBSCRIBER_ID, TEST_CONFIG_APP_CREDS, TEST_CONFIG_YAML_ONLY, @@ -213,11 +214,18 @@ def config( return config +@pytest.fixture +def config_entry_unique_id() -> str: + """Fixture to set ConfigEntry unique id.""" + return PROJECT_ID + + @pytest.fixture def config_entry( subscriber_id: str | None, auth_implementation: str | None, nest_test_config: NestTestConfig, + config_entry_unique_id: str, ) -> MockConfigEntry | None: """Fixture that sets up the ConfigEntry for the test.""" if nest_test_config.config_entry_data is None: @@ -229,7 +237,7 @@ def config_entry( else: del data[CONF_SUBSCRIBER_ID] data["auth_implementation"] = auth_implementation - return MockConfigEntry(domain=DOMAIN, data=data) + return MockConfigEntry(domain=DOMAIN, data=data, unique_id=config_entry_unique_id) @pytest.fixture(autouse=True) diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow_sdm.py index f4299808bf0..53a2d9cf2b6 100644 --- a/tests/components/nest/test_config_flow_sdm.py +++ b/tests/components/nest/test_config_flow_sdm.py @@ -76,8 +76,8 @@ class OAuthFixture: assert result.get("type") == "form" assert result.get("step_id") == "device_project" - result = await self.async_configure(result, {"project_id": PROJECT_ID}) - await self.async_oauth_web_flow(result) + result = await self.async_configure(result, {"project_id": project_id}) + await self.async_oauth_web_flow(result, project_id=project_id) async def async_oauth_web_flow(self, result: dict, project_id=PROJECT_ID) -> None: """Invoke the oauth flow for Web Auth with fake responses.""" @@ -404,7 +404,7 @@ async def test_web_reauth(hass, oauth, setup_platform, config_entry): entry = await oauth.async_finish_setup(result) # Verify existing tokens are replaced entry.data["token"].pop("expires_at") - assert entry.unique_id == DOMAIN + assert entry.unique_id == PROJECT_ID assert entry.data["token"] == { "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", @@ -415,25 +415,51 @@ async def test_web_reauth(hass, oauth, setup_platform, config_entry): assert entry.data.get("subscriber_id") == orig_subscriber_id # Not updated -async def test_single_config_entry(hass, setup_platform): - """Test that only a single config entry is allowed.""" +async def test_multiple_config_entries(hass, oauth, setup_platform): + """Verify config flow can be started when existing config entry exists.""" await setup_platform() + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" - assert result["reason"] == "single_instance_allowed" + await oauth.async_app_creds_flow(result, project_id="project-id-2") + entry = await oauth.async_finish_setup(result) + assert entry.title == "Mock Title" + assert "token" in entry.data + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 2 -async def test_unexpected_existing_config_entries( +async def test_duplicate_config_entries(hass, oauth, setup_platform): + """Verify that config entries must be for unique projects.""" + await setup_platform() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "form" + assert result.get("step_id") == "cloud_project" + + result = await oauth.async_configure(result, {"cloud_project_id": CLOUD_PROJECT_ID}) + assert result.get("type") == "form" + assert result.get("step_id") == "device_project" + + result = await oauth.async_configure(result, {"project_id": PROJECT_ID}) + assert result.get("type") == "abort" + assert result.get("reason") == "already_configured" + + +async def test_reauth_multiple_config_entries( hass, oauth, setup_platform, config_entry ): """Test Nest reauthentication with multiple existing config entries.""" - # Note that this case will not happen in the future since only a single - # instance is now allowed, but this may have been allowed in the past. - # On reauth, only one entry is kept and the others are deleted. - await setup_platform() old_entry = MockConfigEntry( @@ -461,7 +487,7 @@ async def test_unexpected_existing_config_entries( entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 2 entry = entries[0] - assert entry.unique_id == DOMAIN + assert entry.unique_id == PROJECT_ID entry.data["token"].pop("expires_at") assert entry.data["token"] == { "refresh_token": "mock-refresh-token", @@ -540,7 +566,7 @@ async def test_app_auth_yaml_reauth(hass, oauth, setup_platform, config_entry): # Verify existing tokens are replaced entry = await oauth.async_finish_setup(result, {"code": "1234"}) entry.data["token"].pop("expires_at") - assert entry.unique_id == DOMAIN + assert entry.unique_id == PROJECT_ID assert entry.data["token"] == { "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", @@ -570,7 +596,7 @@ async def test_web_auth_yaml_reauth(hass, oauth, setup_platform, config_entry): # Verify existing tokens are replaced entry = await oauth.async_finish_setup(result, {"code": "1234"}) entry.data["token"].pop("expires_at") - assert entry.unique_id == DOMAIN + assert entry.unique_id == PROJECT_ID assert entry.data["token"] == { "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", @@ -599,7 +625,7 @@ async def test_pubsub_subscription_strip_whitespace( assert entry.title == "Import from configuration.yaml" assert "token" in entry.data entry.data["token"].pop("expires_at") - assert entry.unique_id == DOMAIN + assert entry.unique_id == PROJECT_ID assert entry.data["token"] == { "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", @@ -643,7 +669,7 @@ async def test_pubsub_subscriber_config_entry_reauth( # Entering an updated access token refreshs the config entry. entry = await oauth.async_finish_setup(result, {"code": "1234"}) entry.data["token"].pop("expires_at") - assert entry.unique_id == DOMAIN + assert entry.unique_id == PROJECT_ID assert entry.data["token"] == { "refresh_token": "mock-refresh-token", "access_token": "mock-access-token", diff --git a/tests/components/nest/test_init_sdm.py b/tests/components/nest/test_init_sdm.py index 1b473ccd62f..d7c82609c60 100644 --- a/tests/components/nest/test_init_sdm.py +++ b/tests/components/nest/test_init_sdm.py @@ -103,18 +103,18 @@ async def test_setup_configuration_failure( @pytest.mark.parametrize("subscriber_side_effect", [SubscriberException()]) async def test_setup_susbcriber_failure( - hass, error_caplog, failing_subscriber, setup_base_platform + hass, warning_caplog, failing_subscriber, setup_base_platform ): """Test configuration error.""" await setup_base_platform() - assert "Subscriber error:" in error_caplog.text + assert "Subscriber error:" in warning_caplog.text entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 assert entries[0].state is ConfigEntryState.SETUP_RETRY -async def test_setup_device_manager_failure(hass, error_caplog, setup_base_platform): +async def test_setup_device_manager_failure(hass, warning_caplog, setup_base_platform): """Test device manager api failure.""" with patch( "homeassistant.components.nest.api.GoogleNestSubscriber.start_async" @@ -124,8 +124,7 @@ async def test_setup_device_manager_failure(hass, error_caplog, setup_base_platf ): await setup_base_platform() - assert len(error_caplog.messages) == 1 - assert "Device manager error:" in error_caplog.text + assert "Device manager error:" in warning_caplog.text entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 @@ -273,3 +272,18 @@ async def test_remove_entry_delete_subscriber_failure( entries = hass.config_entries.async_entries(DOMAIN) assert not entries + + +@pytest.mark.parametrize("config_entry_unique_id", [DOMAIN, None]) +async def test_migrate_unique_id( + hass, error_caplog, setup_platform, config_entry, config_entry_unique_id +): + """Test successful setup.""" + + assert config_entry.state is ConfigEntryState.NOT_LOADED + assert config_entry.unique_id == config_entry_unique_id + + await setup_platform() + + assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.unique_id == PROJECT_ID From 27209574d256d3205cc2bbb63c41d0cb4b116a85 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 21 Jun 2022 16:50:44 +0200 Subject: [PATCH 1637/3516] Use pydeconz interface controls for lock, scene, siren and switch platforms (#73748) --- homeassistant/components/deconz/lock.py | 22 ++++++++++++++++++++-- homeassistant/components/deconz/scene.py | 5 ++++- homeassistant/components/deconz/siren.py | 14 ++++++++++---- homeassistant/components/deconz/switch.py | 10 ++++++++-- 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index 78ccae30441..cf4bd7f14f5 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -62,8 +62,26 @@ class DeconzLock(DeconzDevice, LockEntity): async def async_lock(self, **kwargs: Any) -> None: """Lock the lock.""" - await self._device.lock() + if isinstance(self._device, DoorLock): + await self.gateway.api.sensors.door_lock.set_config( + id=self._device.resource_id, + lock=True, + ) + else: + await self.gateway.api.lights.locks.set_state( + id=self._device.resource_id, + lock=True, + ) async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" - await self._device.unlock() + if isinstance(self._device, DoorLock): + await self.gateway.api.sensors.door_lock.set_config( + id=self._device.resource_id, + lock=False, + ) + else: + await self.gateway.api.lights.locks.set_state( + id=self._device.resource_id, + lock=False, + ) diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index dfbb6ae828b..236389cc100 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -43,4 +43,7 @@ class DeconzScene(DeconzSceneMixin, Scene): async def async_activate(self, **kwargs: Any) -> None: """Activate the scene.""" - await self._device.recall() + await self.gateway.api.scenes.recall( + self._device.group_id, + self._device.id, + ) diff --git a/homeassistant/components/deconz/siren.py b/homeassistant/components/deconz/siren.py index 8427b6ce75d..d44bce01aad 100644 --- a/homeassistant/components/deconz/siren.py +++ b/homeassistant/components/deconz/siren.py @@ -59,11 +59,17 @@ class DeconzSiren(DeconzDevice, SirenEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on siren.""" - data = {} if (duration := kwargs.get(ATTR_DURATION)) is not None: - data["duration"] = duration * 10 - await self._device.turn_on(**data) + duration *= 10 + await self.gateway.api.lights.sirens.set_state( + id=self._device.resource_id, + on=True, + duration=duration, + ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off siren.""" - await self._device.turn_off() + await self.gateway.api.lights.sirens.set_state( + id=self._device.resource_id, + on=False, + ) diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index d54ff1f36ba..b21ec929909 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -56,8 +56,14 @@ class DeconzPowerPlug(DeconzDevice, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" - await self._device.set_state(on=True) + await self.gateway.api.lights.lights.set_state( + id=self._device.resource_id, + on=True, + ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off switch.""" - await self._device.set_state(on=False) + await self.gateway.api.lights.lights.set_state( + id=self._device.resource_id, + on=False, + ) From eac7c5f177bf5702fb42a4eab49de348c9e32f40 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 21 Jun 2022 17:11:20 +0200 Subject: [PATCH 1638/3516] Remove deprecated X-Hassio-Key usage (#73783) * Remove deprecated X-Hassio-Key usage * ... * Update const.py * Update ingress.py * Update test_ingress.py Co-authored-by: Ludeeus --- homeassistant/components/hassio/const.py | 3 +-- homeassistant/components/hassio/handler.py | 6 +++--- homeassistant/components/hassio/http.py | 14 ++++++++------ homeassistant/components/hassio/ingress.py | 4 ++-- tests/components/hassio/test_ingress.py | 16 +++++++++------- tests/components/hassio/test_init.py | 2 +- 6 files changed, 24 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 2d99b1f5605..e4991e5fc03 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -25,8 +25,7 @@ ATTR_METHOD = "method" ATTR_RESULT = "result" ATTR_TIMEOUT = "timeout" - -X_HASSIO = "X-Hassio-Key" +X_AUTH_TOKEN = "X-Supervisor-Token" X_INGRESS_PATH = "X-Ingress-Path" X_HASS_USER_ID = "X-Hass-User-ID" X_HASS_IS_ADMIN = "X-Hass-Is-Admin" diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index ba1b3bfaf35..7b3ed697227 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -13,8 +13,6 @@ from homeassistant.components.http import ( ) from homeassistant.const import SERVER_PORT -from .const import X_HASSIO - _LOGGER = logging.getLogger(__name__) @@ -246,7 +244,9 @@ class HassIO: method, f"http://{self._ip}{command}", json=payload, - headers={X_HASSIO: os.environ.get("SUPERVISOR_TOKEN", "")}, + headers={ + aiohttp.hdrs.AUTHORIZATION: f"Bearer {os.environ.get('SUPERVISOR_TOKEN', '')}" + }, timeout=aiohttp.ClientTimeout(total=timeout), ) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 3f492114545..7d2e79956cc 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -11,6 +11,7 @@ import aiohttp from aiohttp import web from aiohttp.client import ClientTimeout from aiohttp.hdrs import ( + AUTHORIZATION, CACHE_CONTROL, CONTENT_ENCODING, CONTENT_LENGTH, @@ -18,11 +19,12 @@ from aiohttp.hdrs import ( TRANSFER_ENCODING, ) from aiohttp.web_exceptions import HTTPBadGateway +from multidict import istr from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.onboarding import async_is_onboarded -from .const import X_HASS_IS_ADMIN, X_HASS_USER_ID, X_HASSIO +from .const import X_HASS_IS_ADMIN, X_HASS_USER_ID _LOGGER = logging.getLogger(__name__) @@ -89,7 +91,7 @@ class HassIOView(HomeAssistantView): if path == "backups/new/upload": # We need to reuse the full content type that includes the boundary headers[ - "Content-Type" + CONTENT_TYPE ] = request._stored_content_type # pylint: disable=protected-access try: @@ -123,17 +125,17 @@ class HassIOView(HomeAssistantView): raise HTTPBadGateway() -def _init_header(request: web.Request) -> dict[str, str]: +def _init_header(request: web.Request) -> dict[istr, str]: """Create initial header.""" headers = { - X_HASSIO: os.environ.get("SUPERVISOR_TOKEN", ""), + AUTHORIZATION: f"Bearer {os.environ.get('SUPERVISOR_TOKEN', '')}", CONTENT_TYPE: request.content_type, } # Add user data 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)) + headers[istr(X_HASS_USER_ID)] = request["hass_user"].id + headers[istr(X_HASS_IS_ADMIN)] = str(int(request["hass_user"].is_admin)) return headers diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index c8b56e6f1bb..6caa97b788f 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -15,7 +15,7 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import X_HASSIO, X_INGRESS_PATH +from .const import X_AUTH_TOKEN, X_INGRESS_PATH _LOGGER = logging.getLogger(__name__) @@ -183,7 +183,7 @@ def _init_header(request: web.Request, token: str) -> CIMultiDict | dict[str, st headers[name] = value # Inject token / cleanup later on Supervisor - headers[X_HASSIO] = os.environ.get("SUPERVISOR_TOKEN", "") + headers[X_AUTH_TOKEN] = os.environ.get("SUPERVISOR_TOKEN", "") # Ingress information headers[X_INGRESS_PATH] = f"/api/hassio_ingress/{token}" diff --git a/tests/components/hassio/test_ingress.py b/tests/components/hassio/test_ingress.py index 60fea96d4ea..34016fa9052 100644 --- a/tests/components/hassio/test_ingress.py +++ b/tests/components/hassio/test_ingress.py @@ -5,6 +5,8 @@ from unittest.mock import MagicMock, patch from aiohttp.hdrs import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO import pytest +from homeassistant.components.hassio.const import X_AUTH_TOKEN + @pytest.mark.parametrize( "build_type", @@ -35,7 +37,7 @@ async def test_ingress_request_get(hassio_client, build_type, aioclient_mock): # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 - assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" + assert aioclient_mock.mock_calls[-1][3][X_AUTH_TOKEN] == "123456" assert ( aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == f"/api/hassio_ingress/{build_type[0]}" @@ -75,7 +77,7 @@ async def test_ingress_request_post(hassio_client, build_type, aioclient_mock): # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 - assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" + assert aioclient_mock.mock_calls[-1][3][X_AUTH_TOKEN] == "123456" assert ( aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == f"/api/hassio_ingress/{build_type[0]}" @@ -115,7 +117,7 @@ async def test_ingress_request_put(hassio_client, build_type, aioclient_mock): # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 - assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" + assert aioclient_mock.mock_calls[-1][3][X_AUTH_TOKEN] == "123456" assert ( aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == f"/api/hassio_ingress/{build_type[0]}" @@ -155,7 +157,7 @@ async def test_ingress_request_delete(hassio_client, build_type, aioclient_mock) # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 - assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" + assert aioclient_mock.mock_calls[-1][3][X_AUTH_TOKEN] == "123456" assert ( aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == f"/api/hassio_ingress/{build_type[0]}" @@ -195,7 +197,7 @@ async def test_ingress_request_patch(hassio_client, build_type, aioclient_mock): # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 - assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" + assert aioclient_mock.mock_calls[-1][3][X_AUTH_TOKEN] == "123456" assert ( aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == f"/api/hassio_ingress/{build_type[0]}" @@ -235,7 +237,7 @@ async def test_ingress_request_options(hassio_client, build_type, aioclient_mock # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 - assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" + assert aioclient_mock.mock_calls[-1][3][X_AUTH_TOKEN] == "123456" assert ( aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == f"/api/hassio_ingress/{build_type[0]}" @@ -268,7 +270,7 @@ async def test_ingress_websocket(hassio_client, build_type, aioclient_mock): # Check we forwarded command assert len(aioclient_mock.mock_calls) == 1 - assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" + assert aioclient_mock.mock_calls[-1][3][X_AUTH_TOKEN] == "123456" assert ( aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == f"/api/hassio_ingress/{build_type[0]}" diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 6ac3debe3d8..60fec517aa9 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -348,7 +348,7 @@ async def test_setup_hassio_no_additional_data(hass, aioclient_mock): assert result assert aioclient_mock.call_count == 15 - assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" + assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456" async def test_fail_setup_without_environ_var(hass): From 28cc0b9fb2d3a28860bad702c9038a65e39f468b Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Tue, 21 Jun 2022 17:31:26 +0200 Subject: [PATCH 1639/3516] Expose ThreeWayWindowHandle direction as sensor in Overkiz integration (#73784) * Expose ThreeWayWindowHandle direction as sensor * Compile translations --- homeassistant/components/overkiz/entity.py | 1 + homeassistant/components/overkiz/manifest.json | 2 +- homeassistant/components/overkiz/sensor.py | 6 ++++++ homeassistant/components/overkiz/strings.sensor.json | 5 +++++ .../components/overkiz/translations/sensor.en.json | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 20 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/entity.py b/homeassistant/components/overkiz/entity.py index a177766c292..5728349c5d0 100644 --- a/homeassistant/components/overkiz/entity.py +++ b/homeassistant/components/overkiz/entity.py @@ -118,3 +118,4 @@ class OverkizDeviceClass(StrEnum): PRIORITY_LOCK_ORIGINATOR = "overkiz__priority_lock_originator" SENSOR_DEFECT = "overkiz__sensor_defect" SENSOR_ROOM = "overkiz__sensor_room" + THREE_WAY_HANDLE_DIRECTION = "overkiz__three_way_handle_direction" diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index fa89be5d19e..a7595065224 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -3,7 +3,7 @@ "name": "Overkiz (by Somfy)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", - "requirements": ["pyoverkiz==1.4.1"], + "requirements": ["pyoverkiz==1.4.2"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index 10de6f699dd..ac32c76c459 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -366,6 +366,12 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ native_unit_of_measurement=PERCENTAGE, entity_registry_enabled_default=False, ), + # ThreeWayWindowHandle/WindowHandle + OverkizSensorDescription( + key=OverkizState.CORE_THREE_WAY_HANDLE_DIRECTION, + name="Three Way Handle Direction", + device_class=OverkizDeviceClass.THREE_WAY_HANDLE_DIRECTION, + ), ] SUPPORTED_STATES = {description.key: description for description in SENSOR_DESCRIPTIONS} diff --git a/homeassistant/components/overkiz/strings.sensor.json b/homeassistant/components/overkiz/strings.sensor.json index 4df83bcad77..fdeaa5b911b 100644 --- a/homeassistant/components/overkiz/strings.sensor.json +++ b/homeassistant/components/overkiz/strings.sensor.json @@ -36,6 +36,11 @@ "low_battery": "Low battery", "maintenance_required": "Maintenance required", "no_defect": "No defect" + }, + "overkiz__three_way_handle_direction": { + "closed": "Closed", + "open": "Open", + "tilt": "Tilt" } } } diff --git a/homeassistant/components/overkiz/translations/sensor.en.json b/homeassistant/components/overkiz/translations/sensor.en.json index c0eef6b3ef6..13a10f9a072 100644 --- a/homeassistant/components/overkiz/translations/sensor.en.json +++ b/homeassistant/components/overkiz/translations/sensor.en.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "Clean", "dirty": "Dirty" + }, + "overkiz__three_way_handle_direction": { + "closed": "Closed", + "open": "Open", + "tilt": "Tilt" } } } \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index ece350a33c6..2daa37f0808 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1720,7 +1720,7 @@ pyotgw==1.1b1 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.4.1 +pyoverkiz==1.4.2 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d7c81558ac5..fd02dc2ce5c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1169,7 +1169,7 @@ pyotgw==1.1b1 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.4.1 +pyoverkiz==1.4.2 # homeassistant.components.openweathermap pyowm==3.2.0 From efb4b10629c643473f050049c8f04fad8548bae8 Mon Sep 17 00:00:00 2001 From: AdmiralStipe <64564398+AdmiralStipe@users.noreply.github.com> Date: Tue, 21 Jun 2022 18:05:16 +0200 Subject: [PATCH 1640/3516] Change Microsoft TTS default and not configurable audio settings from poor 16kHz/128kbit/s to better quality 24kHz/96kbit/s (#73609) * Update tts.py * Update tts.py * Update tts.py --- homeassistant/components/microsoft/tts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index 59902335d47..840b35c2f85 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -96,7 +96,7 @@ GENDERS = ["Female", "Male"] DEFAULT_LANG = "en-us" DEFAULT_GENDER = "Female" DEFAULT_TYPE = "JennyNeural" -DEFAULT_OUTPUT = "audio-16khz-128kbitrate-mono-mp3" +DEFAULT_OUTPUT = "audio-24khz-96kbitrate-mono-mp3" DEFAULT_RATE = 0 DEFAULT_VOLUME = 0 DEFAULT_PITCH = "default" From f285b6099a7b0b2ed61dc6738c0000537ee2b3c0 Mon Sep 17 00:00:00 2001 From: rappenze Date: Tue, 21 Jun 2022 18:08:47 +0200 Subject: [PATCH 1641/3516] Code cleanup fibaro sensor (#73388) * Code cleanup fibaro sensor * Adjustments based on code review * Changes from code review, use dict instead of tuple * Remove unneeded deafult in dict get * Another variant to create dict --- homeassistant/components/fibaro/sensor.py | 232 ++++++++++------------ 1 file changed, 108 insertions(+), 124 deletions(-) diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py index acaa97ee2a2..88d6113ebb9 100644 --- a/homeassistant/components/fibaro/sensor.py +++ b/homeassistant/components/fibaro/sensor.py @@ -2,11 +2,13 @@ from __future__ import annotations from contextlib import suppress +from typing import Any from homeassistant.components.sensor import ( ENTITY_ID_FORMAT, SensorDeviceClass, SensorEntity, + SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry @@ -27,49 +29,73 @@ from homeassistant.util import convert from . import FIBARO_DEVICES, FibaroDevice from .const import DOMAIN -SENSOR_TYPES = { - "com.fibaro.temperatureSensor": [ - "Temperature", - None, - None, - SensorDeviceClass.TEMPERATURE, - SensorStateClass.MEASUREMENT, - ], - "com.fibaro.smokeSensor": [ - "Smoke", - CONCENTRATION_PARTS_PER_MILLION, - "mdi:fire", - None, - None, - ], - "CO2": [ - "CO2", - CONCENTRATION_PARTS_PER_MILLION, - None, - SensorDeviceClass.CO2, - SensorStateClass.MEASUREMENT, - ], - "com.fibaro.humiditySensor": [ - "Humidity", - PERCENTAGE, - None, - SensorDeviceClass.HUMIDITY, - SensorStateClass.MEASUREMENT, - ], - "com.fibaro.lightSensor": [ - "Light", - LIGHT_LUX, - None, - SensorDeviceClass.ILLUMINANCE, - SensorStateClass.MEASUREMENT, - ], - "com.fibaro.energyMeter": [ - "Energy", - ENERGY_KILO_WATT_HOUR, - None, - SensorDeviceClass.ENERGY, - SensorStateClass.TOTAL_INCREASING, - ], +# List of known sensors which represents a fibaro device +MAIN_SENSOR_TYPES: dict[str, SensorEntityDescription] = { + "com.fibaro.temperatureSensor": SensorEntityDescription( + key="com.fibaro.temperatureSensor", + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + "com.fibaro.smokeSensor": SensorEntityDescription( + key="com.fibaro.smokeSensor", + name="Smoke", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + icon="mdi:fire", + ), + "CO2": SensorEntityDescription( + key="CO2", + name="CO2", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, + ), + "com.fibaro.humiditySensor": SensorEntityDescription( + key="com.fibaro.humiditySensor", + name="Humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + "com.fibaro.lightSensor": SensorEntityDescription( + key="com.fibaro.lightSensor", + name="Light", + native_unit_of_measurement=LIGHT_LUX, + device_class=SensorDeviceClass.ILLUMINANCE, + state_class=SensorStateClass.MEASUREMENT, + ), + "com.fibaro.energyMeter": SensorEntityDescription( + key="com.fibaro.energyMeter", + name="Energy", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), +} + +# List of additional sensors which are created based on a property +# The key is the property name +ADDITIONAL_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="energy", + name="Energy", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="power", + name="Power", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), +) + +FIBARO_TO_HASS_UNIT: dict[str, str] = { + "lux": LIGHT_LUX, + "C": TEMP_CELSIUS, + "F": TEMP_FAHRENHEIT, } @@ -80,14 +106,18 @@ async def async_setup_entry( ) -> None: """Set up the Fibaro controller devices.""" entities: list[SensorEntity] = [] + for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES][Platform.SENSOR]: - entities.append(FibaroSensor(device)) + entity_description = MAIN_SENSOR_TYPES.get(device.type) + + # main sensors are created even if the entity type is not known + entities.append(FibaroSensor(device, entity_description)) + for platform in (Platform.COVER, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH): for device in hass.data[DOMAIN][entry.entry_id][FIBARO_DEVICES][platform]: - if "energy" in device.interfaces: - entities.append(FibaroEnergySensor(device)) - if "power" in device.interfaces: - entities.append(FibaroPowerSensor(device)) + for entity_description in ADDITIONAL_SENSOR_TYPES: + if entity_description.key in device.properties: + entities.append(FibaroAdditionalSensor(device, entity_description)) async_add_entities(entities, True) @@ -95,97 +125,51 @@ async def async_setup_entry( class FibaroSensor(FibaroDevice, SensorEntity): """Representation of a Fibaro Sensor.""" - def __init__(self, fibaro_device): + def __init__( + self, fibaro_device: Any, entity_description: SensorEntityDescription | None + ) -> None: """Initialize the sensor.""" - self.current_value = None - self.last_changed_time = None super().__init__(fibaro_device) + if entity_description is not None: + self.entity_description = entity_description self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id) - if fibaro_device.type in SENSOR_TYPES: - self._unit = SENSOR_TYPES[fibaro_device.type][1] - self._icon = SENSOR_TYPES[fibaro_device.type][2] - self._device_class = SENSOR_TYPES[fibaro_device.type][3] - self._attr_state_class = SENSOR_TYPES[fibaro_device.type][4] - else: - self._unit = None - self._icon = None - self._device_class = None + + # Map unit if it was not defined in the entity description + # or there is no entity description at all with suppress(KeyError, ValueError): - if not self._unit: - if self.fibaro_device.properties.unit == "lux": - self._unit = LIGHT_LUX - elif self.fibaro_device.properties.unit == "C": - self._unit = TEMP_CELSIUS - elif self.fibaro_device.properties.unit == "F": - self._unit = TEMP_FAHRENHEIT - else: - self._unit = self.fibaro_device.properties.unit - - @property - def native_value(self): - """Return the state of the sensor.""" - return self.current_value - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon - - @property - def device_class(self): - """Return the device class of the sensor.""" - return self._device_class + if not self.native_unit_of_measurement: + self._attr_native_unit_of_measurement = FIBARO_TO_HASS_UNIT.get( + fibaro_device.properties.unit, fibaro_device.properties.unit + ) def update(self): """Update the state.""" with suppress(KeyError, ValueError): - self.current_value = float(self.fibaro_device.properties.value) + self._attr_native_value = float(self.fibaro_device.properties.value) -class FibaroEnergySensor(FibaroDevice, SensorEntity): - """Representation of a Fibaro Energy Sensor.""" +class FibaroAdditionalSensor(FibaroDevice, SensorEntity): + """Representation of a Fibaro Additional Sensor.""" - _attr_device_class = SensorDeviceClass.ENERGY - _attr_state_class = SensorStateClass.TOTAL_INCREASING - _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR - - def __init__(self, fibaro_device): + def __init__( + self, fibaro_device: Any, entity_description: SensorEntityDescription + ) -> None: """Initialize the sensor.""" super().__init__(fibaro_device) - self.entity_id = ENTITY_ID_FORMAT.format(f"{self.ha_id}_energy") - self._attr_name = f"{fibaro_device.friendly_name} Energy" - self._attr_unique_id = f"{fibaro_device.unique_id_str}_energy" + self.entity_description = entity_description - def update(self): + # To differentiate additional sensors from main sensors they need + # to get different names and ids + self.entity_id = ENTITY_ID_FORMAT.format( + f"{self.ha_id}_{entity_description.key}" + ) + self._attr_name = f"{fibaro_device.friendly_name} {entity_description.name}" + self._attr_unique_id = f"{fibaro_device.unique_id_str}_{entity_description.key}" + + def update(self) -> None: """Update the state.""" with suppress(KeyError, ValueError): self._attr_native_value = convert( - self.fibaro_device.properties.energy, float - ) - - -class FibaroPowerSensor(FibaroDevice, SensorEntity): - """Representation of a Fibaro Power Sensor.""" - - _attr_device_class = SensorDeviceClass.POWER - _attr_state_class = SensorStateClass.MEASUREMENT - _attr_native_unit_of_measurement = POWER_WATT - - def __init__(self, fibaro_device): - """Initialize the sensor.""" - super().__init__(fibaro_device) - self.entity_id = ENTITY_ID_FORMAT.format(f"{self.ha_id}_power") - self._attr_name = f"{fibaro_device.friendly_name} Power" - self._attr_unique_id = f"{fibaro_device.unique_id_str}_power" - - def update(self): - """Update the state.""" - with suppress(KeyError, ValueError): - self._attr_native_value = convert( - self.fibaro_device.properties.power, float + self.fibaro_device.properties[self.entity_description.key], + float, ) From a816348616aaed030c5c90e5a1de816e53ba16e8 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Wed, 22 Jun 2022 02:12:11 +1000 Subject: [PATCH 1642/3516] Powerview dataclass (#73746) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + .../hunterdouglas_powerview/__init__.py | 56 +++++++------------ .../hunterdouglas_powerview/button.py | 31 +++------- .../hunterdouglas_powerview/config_flow.py | 8 +-- .../hunterdouglas_powerview/const.py | 19 +------ .../hunterdouglas_powerview/cover.py | 39 +++++-------- .../hunterdouglas_powerview/diagnostics.py | 28 ++++------ .../hunterdouglas_powerview/entity.py | 32 ++++------- .../hunterdouglas_powerview/model.py | 33 +++++++++++ .../hunterdouglas_powerview/scene.py | 29 +++------- .../hunterdouglas_powerview/sensor.py | 27 ++++----- 11 files changed, 129 insertions(+), 174 deletions(-) create mode 100644 homeassistant/components/hunterdouglas_powerview/model.py diff --git a/.coveragerc b/.coveragerc index eba89e5f238..0e7a7324064 100644 --- a/.coveragerc +++ b/.coveragerc @@ -512,6 +512,7 @@ omit = homeassistant/components/hunterdouglas_powerview/cover.py homeassistant/components/hunterdouglas_powerview/diagnostics.py homeassistant/components/hunterdouglas_powerview/entity.py + homeassistant/components/hunterdouglas_powerview/model.py homeassistant/components/hunterdouglas_powerview/scene.py homeassistant/components/hunterdouglas_powerview/sensor.py homeassistant/components/hunterdouglas_powerview/shade_data.py diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index fe039964e5d..4a22bc4ed81 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -19,29 +19,14 @@ import homeassistant.helpers.config_validation as cv from .const import ( API_PATH_FWVERSION, - COORDINATOR, DEFAULT_LEGACY_MAINPROCESSOR, - DEVICE_FIRMWARE, - DEVICE_INFO, - DEVICE_MAC_ADDRESS, - DEVICE_MODEL, - DEVICE_NAME, - DEVICE_REVISION, - DEVICE_SERIAL_NUMBER, DOMAIN, FIRMWARE, FIRMWARE_MAINPROCESSOR, FIRMWARE_NAME, - FIRMWARE_REVISION, HUB_EXCEPTIONS, HUB_NAME, MAC_ADDRESS_IN_USERDATA, - PV_API, - PV_HUB_ADDRESS, - PV_ROOM_DATA, - PV_SCENE_DATA, - PV_SHADE_DATA, - PV_SHADES, ROOM_DATA, SCENE_DATA, SERIAL_NUMBER_IN_USERDATA, @@ -49,6 +34,7 @@ from .const import ( USER_DATA, ) from .coordinator import PowerviewShadeUpdateCoordinator +from .model import PowerviewDeviceInfo, PowerviewEntryData from .shade_data import PowerviewShadeData from .util import async_map_data_by_id @@ -72,8 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: async with async_timeout.timeout(10): - device_info = await async_get_device_info(pv_request) - device_info[PV_HUB_ADDRESS] = hub_address + device_info = await async_get_device_info(pv_request, hub_address) async with async_timeout.timeout(10): rooms = Rooms(pv_request) @@ -102,22 +87,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # populate raw shade data into the coordinator for diagnostics coordinator.data.store_group_data(shade_entries[SHADE_DATA]) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - PV_API: pv_request, - PV_ROOM_DATA: room_data, - PV_SCENE_DATA: scene_data, - PV_SHADES: shades, - PV_SHADE_DATA: shade_data, - COORDINATOR: coordinator, - DEVICE_INFO: device_info, - } + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = PowerviewEntryData( + api=pv_request, + room_data=room_data, + scene_data=scene_data, + shade_data=shade_data, + coordinator=coordinator, + device_info=device_info, + ) hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True -async def async_get_device_info(pv_request): +async def async_get_device_info( + pv_request: AioRequest, hub_address: str +) -> PowerviewDeviceInfo: """Determine device info.""" userdata = UserData(pv_request) resources = await userdata.get_resources() @@ -135,14 +121,14 @@ async def async_get_device_info(pv_request): else: main_processor_info = DEFAULT_LEGACY_MAINPROCESSOR - return { - DEVICE_NAME: base64_to_unicode(userdata_data[HUB_NAME]), - DEVICE_MAC_ADDRESS: userdata_data[MAC_ADDRESS_IN_USERDATA], - DEVICE_SERIAL_NUMBER: userdata_data[SERIAL_NUMBER_IN_USERDATA], - DEVICE_REVISION: main_processor_info[FIRMWARE_REVISION], - DEVICE_FIRMWARE: main_processor_info, - DEVICE_MODEL: main_processor_info[FIRMWARE_NAME], - } + return PowerviewDeviceInfo( + name=base64_to_unicode(userdata_data[HUB_NAME]), + mac_address=userdata_data[MAC_ADDRESS_IN_USERDATA], + serial_number=userdata_data[SERIAL_NUMBER_IN_USERDATA], + firmware=main_processor_info, + model=main_processor_info[FIRMWARE_NAME], + hub_address=hub_address, + ) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/hunterdouglas_powerview/button.py b/homeassistant/components/hunterdouglas_powerview/button.py index b13d0217a20..483e2ca2784 100644 --- a/homeassistant/components/hunterdouglas_powerview/button.py +++ b/homeassistant/components/hunterdouglas_powerview/button.py @@ -13,18 +13,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - COORDINATOR, - DEVICE_INFO, - DOMAIN, - PV_API, - PV_ROOM_DATA, - PV_SHADE_DATA, - ROOM_ID_IN_SHADE, - ROOM_NAME_UNICODE, -) +from .const import DOMAIN, ROOM_ID_IN_SHADE, ROOM_NAME_UNICODE from .coordinator import PowerviewShadeUpdateCoordinator from .entity import ShadeEntity +from .model import PowerviewDeviceInfo, PowerviewEntryData @dataclass @@ -66,25 +58,20 @@ async def async_setup_entry( ) -> None: """Set up the hunter douglas advanced feature buttons.""" - pv_data = hass.data[DOMAIN][entry.entry_id] - room_data: dict[str | int, Any] = pv_data[PV_ROOM_DATA] - shade_data = pv_data[PV_SHADE_DATA] - pv_request = pv_data[PV_API] - coordinator: PowerviewShadeUpdateCoordinator = pv_data[COORDINATOR] - device_info: dict[str, Any] = pv_data[DEVICE_INFO] + pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id] entities: list[ButtonEntity] = [] - for raw_shade in shade_data.values(): - shade: BaseShade = PvShade(raw_shade, pv_request) + for raw_shade in pv_entry.shade_data.values(): + shade: BaseShade = PvShade(raw_shade, pv_entry.api) name_before_refresh = shade.name room_id = shade.raw_data.get(ROOM_ID_IN_SHADE) - room_name = room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "") + room_name = pv_entry.room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "") for description in BUTTONS: entities.append( PowerviewButton( - coordinator, - device_info, + pv_entry.coordinator, + pv_entry.device_info, room_name, shade, name_before_refresh, @@ -101,7 +88,7 @@ class PowerviewButton(ShadeEntity, ButtonEntity): def __init__( self, coordinator: PowerviewShadeUpdateCoordinator, - device_info: dict[str, Any], + device_info: PowerviewDeviceInfo, room_name: str, shade: BaseShade, name: str, diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py index e0ebef7abbb..1666db27d86 100644 --- a/homeassistant/components/hunterdouglas_powerview/config_flow.py +++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py @@ -14,7 +14,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import async_get_device_info -from .const import DEVICE_NAME, DEVICE_SERIAL_NUMBER, DOMAIN, HUB_EXCEPTIONS +from .const import DOMAIN, HUB_EXCEPTIONS _LOGGER = logging.getLogger(__name__) @@ -35,14 +35,14 @@ async def validate_input(hass: core.HomeAssistant, hub_address: str) -> dict[str try: async with async_timeout.timeout(10): - device_info = await async_get_device_info(pv_request) + device_info = await async_get_device_info(pv_request, hub_address) except HUB_EXCEPTIONS as err: raise CannotConnect from err # Return info that you want to store in the config entry. return { - "title": device_info[DEVICE_NAME], - "unique_id": device_info[DEVICE_SERIAL_NUMBER], + "title": device_info.name, + "unique_id": device_info.serial_number, } diff --git a/homeassistant/components/hunterdouglas_powerview/const.py b/homeassistant/components/hunterdouglas_powerview/const.py index 65c461b6f2f..9d99710f36d 100644 --- a/homeassistant/components/hunterdouglas_powerview/const.py +++ b/homeassistant/components/hunterdouglas_powerview/const.py @@ -27,13 +27,9 @@ FIRMWARE_REVISION = "revision" FIRMWARE_SUB_REVISION = "subRevision" FIRMWARE_BUILD = "build" -DEVICE_NAME = "device_name" -DEVICE_MAC_ADDRESS = "device_mac_address" -DEVICE_SERIAL_NUMBER = "device_serial_number" -DEVICE_REVISION = "device_revision" -DEVICE_INFO = "device_info" -DEVICE_MODEL = "device_model" -DEVICE_FIRMWARE = "device_firmware" +REDACT_MAC_ADDRESS = "mac_address" +REDACT_SERIAL_NUMBER = "serial_number" +REDACT_HUB_ADDRESS = "hub_address" SCENE_NAME = "name" SCENE_ID = "id" @@ -52,15 +48,6 @@ SHADE_BATTERY_LEVEL_MAX = 200 STATE_ATTRIBUTE_ROOM_NAME = "roomName" -PV_API = "pv_api" -PV_HUB = "pv_hub" -PV_HUB_ADDRESS = "pv_hub_address" -PV_SHADES = "pv_shades" -PV_SCENE_DATA = "pv_scene_data" -PV_SHADE_DATA = "pv_shade_data" -PV_ROOM_DATA = "pv_room_data" -COORDINATOR = "coordinator" - HUB_EXCEPTIONS = ( ServerDisconnectedError, asyncio.TimeoutError, diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index e0a01f9c381..797dded4f76 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -38,23 +38,18 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later from .const import ( - COORDINATOR, - DEVICE_INFO, - DEVICE_MODEL, DOMAIN, LEGACY_DEVICE_MODEL, POS_KIND_PRIMARY, POS_KIND_SECONDARY, POS_KIND_VANE, - PV_API, - PV_ROOM_DATA, - PV_SHADE_DATA, ROOM_ID_IN_SHADE, ROOM_NAME_UNICODE, STATE_ATTRIBUTE_ROOM_NAME, ) from .coordinator import PowerviewShadeUpdateCoordinator from .entity import ShadeEntity +from .model import PowerviewDeviceInfo, PowerviewEntryData from .shade_data import PowerviewShadeMove _LOGGER = logging.getLogger(__name__) @@ -83,18 +78,14 @@ async def async_setup_entry( ) -> None: """Set up the hunter douglas shades.""" - pv_data = hass.data[DOMAIN][entry.entry_id] - room_data: dict[str | int, Any] = pv_data[PV_ROOM_DATA] - shade_data = pv_data[PV_SHADE_DATA] - pv_request = pv_data[PV_API] - coordinator: PowerviewShadeUpdateCoordinator = pv_data[COORDINATOR] - device_info: dict[str, Any] = pv_data[DEVICE_INFO] + pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id] + coordinator: PowerviewShadeUpdateCoordinator = pv_entry.coordinator entities: list[ShadeEntity] = [] - for raw_shade in shade_data.values(): + for raw_shade in pv_entry.shade_data.values(): # The shade may be out of sync with the hub # so we force a refresh when we add it if possible - shade: BaseShade = PvShade(raw_shade, pv_request) + shade: BaseShade = PvShade(raw_shade, pv_entry.api) name_before_refresh = shade.name with suppress(asyncio.TimeoutError): async with async_timeout.timeout(1): @@ -108,10 +99,10 @@ async def async_setup_entry( continue coordinator.data.update_shade_positions(shade.raw_data) room_id = shade.raw_data.get(ROOM_ID_IN_SHADE) - room_name = room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "") + room_name = pv_entry.room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "") entities.extend( create_powerview_shade_entity( - coordinator, device_info, room_name, shade, name_before_refresh + coordinator, pv_entry.device_info, room_name, shade, name_before_refresh ) ) async_add_entities(entities) @@ -119,7 +110,7 @@ async def async_setup_entry( def create_powerview_shade_entity( coordinator: PowerviewShadeUpdateCoordinator, - device_info: dict[str, Any], + device_info: PowerviewDeviceInfo, room_name: str, shade: BaseShade, name_before_refresh: str, @@ -161,7 +152,7 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): def __init__( self, coordinator: PowerviewShadeUpdateCoordinator, - device_info: dict[str, Any], + device_info: PowerviewDeviceInfo, room_name: str, shade: BaseShade, name: str, @@ -171,7 +162,7 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): self._shade: BaseShade = shade self._attr_name = self._shade_name self._scheduled_transition_update: CALLBACK_TYPE | None = None - if self._device_info[DEVICE_MODEL] != LEGACY_DEVICE_MODEL: + if self._device_info.model != LEGACY_DEVICE_MODEL: self._attr_supported_features |= CoverEntityFeature.STOP self._forced_resync = None @@ -387,7 +378,7 @@ class PowerViewShade(PowerViewShadeBase): def __init__( self, coordinator: PowerviewShadeUpdateCoordinator, - device_info: dict[str, Any], + device_info: PowerviewDeviceInfo, room_name: str, shade: BaseShade, name: str, @@ -418,7 +409,7 @@ class PowerViewShadeTDBUBottom(PowerViewShadeTDBU): def __init__( self, coordinator: PowerviewShadeUpdateCoordinator, - device_info: dict[str, Any], + device_info: PowerviewDeviceInfo, room_name: str, shade: BaseShade, name: str, @@ -455,7 +446,7 @@ class PowerViewShadeTDBUTop(PowerViewShadeTDBU): def __init__( self, coordinator: PowerviewShadeUpdateCoordinator, - device_info: dict[str, Any], + device_info: PowerviewDeviceInfo, room_name: str, shade: BaseShade, name: str, @@ -514,7 +505,7 @@ class PowerViewShadeWithTilt(PowerViewShade): def __init__( self, coordinator: PowerviewShadeUpdateCoordinator, - device_info: dict[str, Any], + device_info: PowerviewDeviceInfo, room_name: str, shade: BaseShade, name: str, @@ -526,7 +517,7 @@ class PowerViewShadeWithTilt(PowerViewShade): | CoverEntityFeature.CLOSE_TILT | CoverEntityFeature.SET_TILT_POSITION ) - if self._device_info[DEVICE_MODEL] != LEGACY_DEVICE_MODEL: + if self._device_info.model != LEGACY_DEVICE_MODEL: self._attr_supported_features |= CoverEntityFeature.STOP_TILT @property diff --git a/homeassistant/components/hunterdouglas_powerview/diagnostics.py b/homeassistant/components/hunterdouglas_powerview/diagnostics.py index ca6131b2761..12f424ea501 100644 --- a/homeassistant/components/hunterdouglas_powerview/diagnostics.py +++ b/homeassistant/components/hunterdouglas_powerview/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Powerview Hunter Douglas.""" from __future__ import annotations +from dataclasses import asdict +import logging from typing import Any import attr @@ -12,24 +14,19 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceEntry -from .const import ( - COORDINATOR, - DEVICE_INFO, - DEVICE_MAC_ADDRESS, - DEVICE_SERIAL_NUMBER, - DOMAIN, - PV_HUB_ADDRESS, -) -from .coordinator import PowerviewShadeUpdateCoordinator +from .const import DOMAIN, REDACT_HUB_ADDRESS, REDACT_MAC_ADDRESS, REDACT_SERIAL_NUMBER +from .model import PowerviewEntryData REDACT_CONFIG = { CONF_HOST, - DEVICE_MAC_ADDRESS, - DEVICE_SERIAL_NUMBER, - PV_HUB_ADDRESS, + REDACT_HUB_ADDRESS, + REDACT_MAC_ADDRESS, + REDACT_SERIAL_NUMBER, ATTR_CONFIGURATION_URL, } +_LOGGER = logging.getLogger(__name__) + async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry @@ -70,10 +67,9 @@ def _async_get_diagnostics( entry: ConfigEntry, ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - pv_data = hass.data[DOMAIN][entry.entry_id] - coordinator: PowerviewShadeUpdateCoordinator = pv_data[COORDINATOR] - shade_data = coordinator.data.get_all_raw_data() - hub_info = async_redact_data(pv_data[DEVICE_INFO], REDACT_CONFIG) + pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id] + shade_data = pv_entry.coordinator.data.get_all_raw_data() + hub_info = async_redact_data(asdict(pv_entry.device_info), REDACT_CONFIG) return {"hub_info": hub_info, "shade_data": shade_data} diff --git a/homeassistant/components/hunterdouglas_powerview/entity.py b/homeassistant/components/hunterdouglas_powerview/entity.py index 222324eb55a..a2bbf39fb96 100644 --- a/homeassistant/components/hunterdouglas_powerview/entity.py +++ b/homeassistant/components/hunterdouglas_powerview/entity.py @@ -1,7 +1,5 @@ """The powerview integration base entity.""" -from typing import Any - from aiopvapi.resources.shade import ATTR_TYPE, BaseShade from homeassistant.const import ATTR_MODEL, ATTR_SW_VERSION @@ -12,20 +10,15 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_BATTERY_KIND, BATTERY_KIND_HARDWIRED, - DEVICE_FIRMWARE, - DEVICE_MAC_ADDRESS, - DEVICE_MODEL, - DEVICE_NAME, - DEVICE_SERIAL_NUMBER, DOMAIN, FIRMWARE, FIRMWARE_BUILD, FIRMWARE_REVISION, FIRMWARE_SUB_REVISION, MANUFACTURER, - PV_HUB_ADDRESS, ) from .coordinator import PowerviewShadeUpdateCoordinator +from .model import PowerviewDeviceInfo from .shade_data import PowerviewShadeData, PowerviewShadePositions @@ -35,7 +28,7 @@ class HDEntity(CoordinatorEntity[PowerviewShadeUpdateCoordinator]): def __init__( self, coordinator: PowerviewShadeUpdateCoordinator, - device_info: dict[str, Any], + device_info: PowerviewDeviceInfo, room_name: str, unique_id: str, ) -> None: @@ -43,7 +36,6 @@ class HDEntity(CoordinatorEntity[PowerviewShadeUpdateCoordinator]): super().__init__(coordinator) self._room_name = room_name self._attr_unique_id = unique_id - self._hub_address = device_info[PV_HUB_ADDRESS] self._device_info = device_info @property @@ -54,19 +46,17 @@ class HDEntity(CoordinatorEntity[PowerviewShadeUpdateCoordinator]): @property def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" - firmware = self._device_info[DEVICE_FIRMWARE] + firmware = self._device_info.firmware sw_version = f"{firmware[FIRMWARE_REVISION]}.{firmware[FIRMWARE_SUB_REVISION]}.{firmware[FIRMWARE_BUILD]}" return DeviceInfo( - connections={ - (dr.CONNECTION_NETWORK_MAC, self._device_info[DEVICE_MAC_ADDRESS]) - }, - identifiers={(DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER])}, + connections={(dr.CONNECTION_NETWORK_MAC, self._device_info.mac_address)}, + identifiers={(DOMAIN, self._device_info.serial_number)}, manufacturer=MANUFACTURER, - model=self._device_info[DEVICE_MODEL], - name=self._device_info[DEVICE_NAME], + model=self._device_info.model, + name=self._device_info.name, suggested_area=self._room_name, sw_version=sw_version, - configuration_url=f"http://{self._hub_address}/api/shades", + configuration_url=f"http://{self._device_info.hub_address}/api/shades", ) @@ -76,7 +66,7 @@ class ShadeEntity(HDEntity): def __init__( self, coordinator: PowerviewShadeUpdateCoordinator, - device_info: dict[str, Any], + device_info: PowerviewDeviceInfo, room_name: str, shade: BaseShade, shade_name: str, @@ -104,8 +94,8 @@ class ShadeEntity(HDEntity): suggested_area=self._room_name, manufacturer=MANUFACTURER, model=str(self._shade.raw_data[ATTR_TYPE]), - via_device=(DOMAIN, self._device_info[DEVICE_SERIAL_NUMBER]), - configuration_url=f"http://{self._hub_address}/api/shades/{self._shade.id}", + via_device=(DOMAIN, self._device_info.serial_number), + configuration_url=f"http://{self._device_info.hub_address}/api/shades/{self._shade.id}", ) for shade in self._shade.shade_types: diff --git a/homeassistant/components/hunterdouglas_powerview/model.py b/homeassistant/components/hunterdouglas_powerview/model.py new file mode 100644 index 00000000000..b7ad4a7439c --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/model.py @@ -0,0 +1,33 @@ +"""Define Hunter Douglas data models.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from aiopvapi.helpers.aiorequest import AioRequest + +from .coordinator import PowerviewShadeUpdateCoordinator + + +@dataclass +class PowerviewEntryData: + """Define class for main domain information.""" + + api: AioRequest + room_data: dict[str, Any] + scene_data: dict[str, Any] + shade_data: dict[str, Any] + coordinator: PowerviewShadeUpdateCoordinator + device_info: PowerviewDeviceInfo + + +@dataclass +class PowerviewDeviceInfo: + """Define class for device information.""" + + name: str + mac_address: str + serial_number: str + firmware: dict[str, Any] + model: str + hub_address: str diff --git a/homeassistant/components/hunterdouglas_powerview/scene.py b/homeassistant/components/hunterdouglas_powerview/scene.py index 3476db4949c..ba1221a25ac 100644 --- a/homeassistant/components/hunterdouglas_powerview/scene.py +++ b/homeassistant/components/hunterdouglas_powerview/scene.py @@ -10,17 +10,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - COORDINATOR, - DEVICE_INFO, - DOMAIN, - PV_API, - PV_ROOM_DATA, - PV_SCENE_DATA, - ROOM_NAME_UNICODE, - STATE_ATTRIBUTE_ROOM_NAME, -) +from .const import DOMAIN, ROOM_NAME_UNICODE, STATE_ATTRIBUTE_ROOM_NAME from .entity import HDEntity +from .model import PowerviewEntryData async def async_setup_entry( @@ -28,18 +20,15 @@ async def async_setup_entry( ) -> None: """Set up powerview scene entries.""" - pv_data = hass.data[DOMAIN][entry.entry_id] - room_data = pv_data[PV_ROOM_DATA] - scene_data = pv_data[PV_SCENE_DATA] - pv_request = pv_data[PV_API] - coordinator = pv_data[COORDINATOR] - device_info = pv_data[DEVICE_INFO] + pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id] pvscenes = [] - for raw_scene in scene_data.values(): - scene = PvScene(raw_scene, pv_request) - room_name = room_data.get(scene.room_id, {}).get(ROOM_NAME_UNICODE, "") - pvscenes.append(PowerViewScene(coordinator, device_info, room_name, scene)) + for raw_scene in pv_entry.scene_data.values(): + scene = PvScene(raw_scene, pv_entry.api) + room_name = pv_entry.room_data.get(scene.room_id, {}).get(ROOM_NAME_UNICODE, "") + pvscenes.append( + PowerViewScene(pv_entry.coordinator, pv_entry.device_info, room_name, scene) + ) async_add_entities(pvscenes) diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py index 3fc8942eb78..6328ad63bc2 100644 --- a/homeassistant/components/hunterdouglas_powerview/sensor.py +++ b/homeassistant/components/hunterdouglas_powerview/sensor.py @@ -1,5 +1,5 @@ """Support for hunterdouglass_powerview sensors.""" -from aiopvapi.resources.shade import factory as PvShade +from aiopvapi.resources.shade import BaseShade, factory as PvShade from homeassistant.components.sensor import ( SensorDeviceClass, @@ -13,18 +13,14 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( - COORDINATOR, - DEVICE_INFO, DOMAIN, - PV_API, - PV_ROOM_DATA, - PV_SHADE_DATA, ROOM_ID_IN_SHADE, ROOM_NAME_UNICODE, SHADE_BATTERY_LEVEL, SHADE_BATTERY_LEVEL_MAX, ) from .entity import ShadeEntity +from .model import PowerviewEntryData async def async_setup_entry( @@ -32,24 +28,23 @@ async def async_setup_entry( ) -> None: """Set up the hunter douglas shades sensors.""" - pv_data = hass.data[DOMAIN][entry.entry_id] - room_data = pv_data[PV_ROOM_DATA] - shade_data = pv_data[PV_SHADE_DATA] - pv_request = pv_data[PV_API] - coordinator = pv_data[COORDINATOR] - device_info = pv_data[DEVICE_INFO] + pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id] entities = [] - for raw_shade in shade_data.values(): - shade = PvShade(raw_shade, pv_request) + for raw_shade in pv_entry.shade_data.values(): + shade: BaseShade = PvShade(raw_shade, pv_entry.api) if SHADE_BATTERY_LEVEL not in shade.raw_data: continue name_before_refresh = shade.name room_id = shade.raw_data.get(ROOM_ID_IN_SHADE) - room_name = room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "") + room_name = pv_entry.room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "") entities.append( PowerViewShadeBatterySensor( - coordinator, device_info, room_name, shade, name_before_refresh + pv_entry.coordinator, + pv_entry.device_info, + room_name, + shade, + name_before_refresh, ) ) async_add_entities(entities) From 3823edda32589a083e3fe7d7edb75c7d7ce667a7 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Tue, 21 Jun 2022 12:17:29 -0400 Subject: [PATCH 1643/3516] Add Permission checking for UniFi Protect (#73765) Co-authored-by: J. Nick Koston --- .../components/unifiprotect/button.py | 4 ++- .../components/unifiprotect/entity.py | 7 +++- .../components/unifiprotect/light.py | 11 +++--- .../components/unifiprotect/models.py | 9 +++++ .../components/unifiprotect/number.py | 17 +++++++-- .../components/unifiprotect/select.py | 12 ++++++- .../components/unifiprotect/switch.py | 27 +++++++++++++- tests/components/unifiprotect/test_switch.py | 35 +++++++++++++++++++ 8 files changed, 109 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 9ed5ecc4967..01714868261 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -18,7 +18,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .data import ProtectData from .entity import ProtectDeviceEntity, async_all_device_entities -from .models import ProtectSetableKeysMixin, T +from .models import PermRequired, ProtectSetableKeysMixin, T @dataclass @@ -40,6 +40,7 @@ ALL_DEVICE_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( device_class=ButtonDeviceClass.RESTART, name="Reboot Device", ufp_press="reboot", + ufp_perm=PermRequired.WRITE, ), ) @@ -49,6 +50,7 @@ SENSOR_BUTTONS: tuple[ProtectButtonEntityDescription, ...] = ( name="Clear Tamper", icon="mdi:notification-clear-all", ufp_press="clear_tamper", + ufp_perm=PermRequired.WRITE, ), ) diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index e06d297ef33..155fa49f078 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -26,7 +26,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from .const import ATTR_EVENT_SCORE, DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN from .data import ProtectData -from .models import ProtectRequiredKeysMixin +from .models import PermRequired, ProtectRequiredKeysMixin from .utils import get_nested_attr _LOGGER = logging.getLogger(__name__) @@ -46,6 +46,11 @@ def _async_device_entities( for device in data.get_by_types({model_type}): assert isinstance(device, (Camera, Light, Sensor, Viewer, Doorlock, Chime)) for description in descs: + if description.ufp_perm is not None: + can_write = device.can_write(data.api.bootstrap.auth_user) + if description.ufp_perm == PermRequired.WRITE and not can_write: + continue + if description.ufp_required_field: required_field = get_nested_attr(device, description.ufp_required_field) if not required_field: diff --git a/homeassistant/components/unifiprotect/light.py b/homeassistant/components/unifiprotect/light.py index d84c8406acf..b200fb85e03 100644 --- a/homeassistant/components/unifiprotect/light.py +++ b/homeassistant/components/unifiprotect/light.py @@ -25,13 +25,10 @@ async def async_setup_entry( ) -> None: """Set up lights for UniFi Protect integration.""" data: ProtectData = hass.data[DOMAIN][entry.entry_id] - entities = [ - ProtectLight( - data, - device, - ) - for device in data.api.bootstrap.lights.values() - ] + entities = [] + for device in data.api.bootstrap.lights.values(): + if device.can_write(data.api.bootstrap.auth_user): + entities.append(ProtectLight(data, device)) if not entities: return diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index c28e1757722..81ad8438dd7 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass +from enum import Enum import logging from typing import Any, Generic, TypeVar @@ -17,6 +18,13 @@ _LOGGER = logging.getLogger(__name__) T = TypeVar("T", bound=ProtectDeviceModel) +class PermRequired(int, Enum): + """Type of permission level required for entity.""" + + NO_WRITE = 1 + WRITE = 2 + + @dataclass class ProtectRequiredKeysMixin(EntityDescription, Generic[T]): """Mixin for required keys.""" @@ -25,6 +33,7 @@ class ProtectRequiredKeysMixin(EntityDescription, Generic[T]): ufp_value: str | None = None ufp_value_fn: Callable[[T], Any] | None = None ufp_enabled: str | None = None + ufp_perm: PermRequired | None = None def get_ufp_value(self, obj: T) -> Any: """Return value from UniFi Protect device.""" diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index 5a3b048e623..7bd6ce5b3d8 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -8,7 +8,7 @@ from pyunifiprotect.data import Camera, Doorlock, Light, ProtectModelWithId from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TIME_SECONDS +from homeassistant.const import PERCENTAGE, TIME_SECONDS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .data import ProtectData from .entity import ProtectDeviceEntity, async_all_device_entities -from .models import ProtectSetableKeysMixin, T +from .models import PermRequired, ProtectSetableKeysMixin, T @dataclass @@ -63,30 +63,35 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ufp_required_field="feature_flags.has_wdr", ufp_value="isp_settings.wdr", ufp_set_method="set_wdr_level", + ufp_perm=PermRequired.WRITE, ), ProtectNumberEntityDescription( key="mic_level", name="Microphone Level", icon="mdi:microphone", entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=PERCENTAGE, ufp_min=0, ufp_max=100, ufp_step=1, ufp_required_field="feature_flags.has_mic", ufp_value="mic_volume", ufp_set_method="set_mic_volume", + ufp_perm=PermRequired.WRITE, ), ProtectNumberEntityDescription( key="zoom_position", name="Zoom Level", icon="mdi:magnify-plus-outline", entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=PERCENTAGE, ufp_min=0, ufp_max=100, ufp_step=1, ufp_required_field="feature_flags.can_optical_zoom", ufp_value="isp_settings.zoom_position", ufp_set_method="set_camera_zoom", + ufp_perm=PermRequired.WRITE, ), ) @@ -96,12 +101,14 @@ LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( name="Motion Sensitivity", icon="mdi:walk", entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=PERCENTAGE, ufp_min=0, ufp_max=100, ufp_step=1, ufp_required_field=None, ufp_value="light_device_settings.pir_sensitivity", ufp_set_method="set_sensitivity", + ufp_perm=PermRequired.WRITE, ), ProtectNumberEntityDescription[Light]( key="duration", @@ -115,6 +122,7 @@ LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ufp_required_field=None, ufp_value_fn=_get_pir_duration, ufp_set_method_fn=_set_pir_duration, + ufp_perm=PermRequired.WRITE, ), ) @@ -124,12 +132,14 @@ SENSE_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( name="Motion Sensitivity", icon="mdi:walk", entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=PERCENTAGE, ufp_min=0, ufp_max=100, ufp_step=1, ufp_required_field=None, ufp_value="motion_settings.sensitivity", ufp_set_method="set_motion_sensitivity", + ufp_perm=PermRequired.WRITE, ), ) @@ -146,6 +156,7 @@ DOORLOCK_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ufp_required_field=None, ufp_value_fn=_get_auto_close, ufp_set_method_fn=_set_auto_close, + ufp_perm=PermRequired.WRITE, ), ) @@ -155,11 +166,13 @@ CHIME_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( name="Volume", icon="mdi:speaker", entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=PERCENTAGE, ufp_min=0, ufp_max=100, ufp_step=1, ufp_value="volume", ufp_set_method="set_volume", + ufp_perm=PermRequired.WRITE, ), ) diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 6f5c2cfd0d7..4432e77ac26 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -38,7 +38,7 @@ from homeassistant.util.dt import utcnow from .const import ATTR_DURATION, ATTR_MESSAGE, DOMAIN, TYPE_EMPTY_VALUE from .data import ProtectData from .entity import ProtectDeviceEntity, async_all_device_entities -from .models import ProtectSetableKeysMixin, T +from .models import PermRequired, ProtectSetableKeysMixin, T _LOGGER = logging.getLogger(__name__) _KEY_LIGHT_MOTION = "light_motion" @@ -208,6 +208,7 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ufp_enum_type=RecordingMode, ufp_value="recording_settings.mode", ufp_set_method="set_recording_mode", + ufp_perm=PermRequired.WRITE, ), ProtectSelectEntityDescription( key="infrared", @@ -219,6 +220,7 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ufp_enum_type=IRLEDMode, ufp_value="isp_settings.ir_led_mode", ufp_set_method="set_ir_led_model", + ufp_perm=PermRequired.WRITE, ), ProtectSelectEntityDescription[Camera]( key="doorbell_text", @@ -230,6 +232,7 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ufp_value_fn=_get_doorbell_current, ufp_options_fn=_get_doorbell_options, ufp_set_method_fn=_set_doorbell_message, + ufp_perm=PermRequired.WRITE, ), ProtectSelectEntityDescription( key="chime_type", @@ -241,6 +244,7 @@ CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ufp_enum_type=ChimeType, ufp_value="chime_type", ufp_set_method="set_chime_type", + ufp_perm=PermRequired.WRITE, ), ) @@ -253,6 +257,7 @@ LIGHT_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ufp_options=MOTION_MODE_TO_LIGHT_MODE, ufp_value_fn=_get_light_motion_current, ufp_set_method_fn=_set_light_mode, + ufp_perm=PermRequired.WRITE, ), ProtectSelectEntityDescription[Light]( key="paired_camera", @@ -262,6 +267,7 @@ LIGHT_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ufp_value="camera_id", ufp_options_fn=_get_paired_camera_options, ufp_set_method_fn=_set_paired_camera, + ufp_perm=PermRequired.WRITE, ), ) @@ -275,6 +281,7 @@ SENSE_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ufp_enum_type=MountType, ufp_value="mount_type", ufp_set_method="set_mount_type", + ufp_perm=PermRequired.WRITE, ), ProtectSelectEntityDescription[Sensor]( key="paired_camera", @@ -284,6 +291,7 @@ SENSE_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ufp_value="camera_id", ufp_options_fn=_get_paired_camera_options, ufp_set_method_fn=_set_paired_camera, + ufp_perm=PermRequired.WRITE, ), ) @@ -296,6 +304,7 @@ DOORLOCK_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ufp_value="camera_id", ufp_options_fn=_get_paired_camera_options, ufp_set_method_fn=_set_paired_camera, + ufp_perm=PermRequired.WRITE, ), ) @@ -308,6 +317,7 @@ VIEWER_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( ufp_options_fn=_get_viewer_options, ufp_value_fn=_get_viewer_current, ufp_set_method_fn=_set_liveview, + ufp_perm=PermRequired.WRITE, ), ) diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index d8542da2f7f..06bf9f7251b 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -21,7 +21,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN from .data import ProtectData from .entity import ProtectDeviceEntity, async_all_device_entities -from .models import ProtectSetableKeysMixin, T +from .models import PermRequired, ProtectSetableKeysMixin, T _LOGGER = logging.getLogger(__name__) @@ -56,6 +56,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_value="is_ssh_enabled", ufp_set_method="set_ssh", + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( key="status_light", @@ -65,6 +66,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="feature_flags.has_led_status", ufp_value="led_settings.is_enabled", ufp_set_method="set_status_light", + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( key="hdr_mode", @@ -74,6 +76,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="feature_flags.has_hdr", ufp_value="hdr_mode", ufp_set_method="set_hdr", + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription[Camera]( key="high_fps", @@ -83,6 +86,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="feature_flags.has_highfps", ufp_value_fn=_get_is_highfps, ufp_set_method_fn=_set_highfps, + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( key=_KEY_PRIVACY_MODE, @@ -91,6 +95,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_required_field="feature_flags.has_privacy_mask", ufp_value="is_privacy_on", + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( key="system_sounds", @@ -100,6 +105,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="feature_flags.has_speaker", ufp_value="speaker_settings.are_system_sounds_enabled", ufp_set_method="set_system_sounds", + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( key="osd_name", @@ -108,6 +114,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_value="osd_settings.is_name_enabled", ufp_set_method="set_osd_name", + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( key="osd_date", @@ -116,6 +123,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_value="osd_settings.is_date_enabled", ufp_set_method="set_osd_date", + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( key="osd_logo", @@ -124,6 +132,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_value="osd_settings.is_logo_enabled", ufp_set_method="set_osd_logo", + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( key="osd_bitrate", @@ -132,6 +141,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_value="osd_settings.is_debug_enabled", ufp_set_method="set_osd_bitrate", + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( key="motion", @@ -140,6 +150,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_value="recording_settings.enable_motion_detection", ufp_set_method="set_motion_detection", + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( key="smart_person", @@ -149,6 +160,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="can_detect_person", ufp_value="is_person_detection_on", ufp_set_method="set_person_detection", + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( key="smart_vehicle", @@ -158,6 +170,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="can_detect_vehicle", ufp_value="is_vehicle_detection_on", ufp_set_method="set_vehicle_detection", + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( key="smart_face", @@ -167,6 +180,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="can_detect_face", ufp_value="is_face_detection_on", ufp_set_method="set_face_detection", + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( key="smart_package", @@ -176,6 +190,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_required_field="can_detect_package", ufp_value="is_package_detection_on", ufp_set_method="set_package_detection", + ufp_perm=PermRequired.WRITE, ), ) @@ -187,6 +202,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_value="led_settings.is_enabled", ufp_set_method="set_status_light", + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( key="motion", @@ -195,6 +211,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_value="motion_settings.is_enabled", ufp_set_method="set_motion_status", + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( key="temperature", @@ -203,6 +220,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_value="temperature_settings.is_enabled", ufp_set_method="set_temperature_status", + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( key="humidity", @@ -211,6 +229,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_value="humidity_settings.is_enabled", ufp_set_method="set_humidity_status", + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( key="light", @@ -219,6 +238,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_value="light_settings.is_enabled", ufp_set_method="set_light_status", + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( key="alarm", @@ -226,6 +246,7 @@ SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_value="alarm_settings.is_enabled", ufp_set_method="set_alarm_status", + ufp_perm=PermRequired.WRITE, ), ) @@ -239,6 +260,7 @@ LIGHT_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_value="is_ssh_enabled", ufp_set_method="set_ssh", + ufp_perm=PermRequired.WRITE, ), ProtectSwitchEntityDescription( key="status_light", @@ -247,6 +269,7 @@ LIGHT_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_value="light_device_settings.is_indicator_enabled", ufp_set_method="set_status_light", + ufp_perm=PermRequired.WRITE, ), ) @@ -258,6 +281,7 @@ DOORLOCK_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_value="led_settings.is_enabled", ufp_set_method="set_status_light", + ufp_perm=PermRequired.WRITE, ), ) @@ -270,6 +294,7 @@ VIEWER_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_value="is_ssh_enabled", ufp_set_method="set_ssh", + ufp_perm=PermRequired.WRITE, ), ) diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 1bd6dbeb349..6c9340af5d5 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -8,6 +8,7 @@ import pytest from pyunifiprotect.data import ( Camera, Light, + Permission, RecordingMode, SmartDetectObjectType, VideoMode, @@ -214,6 +215,40 @@ async def camera_privacy_fixture( Camera.__config__.validate_assignment = True +async def test_switch_setup_no_perm( + hass: HomeAssistant, + mock_entry: MockEntityFixture, + mock_light: Light, + mock_camera: Camera, +): + """Test switch entity setup for light devices.""" + + light_obj = mock_light.copy() + light_obj._api = mock_entry.api + + camera_obj = mock_camera.copy() + camera_obj._api = mock_entry.api + camera_obj.channels[0]._api = mock_entry.api + camera_obj.channels[1]._api = mock_entry.api + camera_obj.channels[2]._api = mock_entry.api + + reset_objects(mock_entry.api.bootstrap) + mock_entry.api.bootstrap.lights = { + light_obj.id: light_obj, + } + mock_entry.api.bootstrap.cameras = { + camera_obj.id: camera_obj, + } + mock_entry.api.bootstrap.auth_user.all_permissions = [ + Permission.unifi_dict_to_dict({"rawPermission": "light:read:*"}) + ] + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + + assert_entity_counts(hass, Platform.SWITCH, 0, 0) + + async def test_switch_setup_light( hass: HomeAssistant, mock_entry: MockEntityFixture, From 9fd48da132c33cd998b8d0868128dc677c9fa3c9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 21 Jun 2022 18:53:31 +0200 Subject: [PATCH 1644/3516] Add lock checks to pylint type-hint plugin (#73521) * Add ability to check kwargs type annotation * Add checks for lock * Add tests * Fix components * Fix spelling * Revert "Fix components" This reverts commit 121ff6dc511d28c17b4fc13185155a2402193405. * Adjust comment * Add comments to TypeHintMatch --- pylint/plugins/hass_enforce_type_hints.py | 53 ++++++++++++++++++++--- tests/pylint/test_enforce_type_hints.py | 32 +++++++++++++- 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 62dc6feffc6..a1bf260e968 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -21,7 +21,10 @@ class TypeHintMatch: function_name: str return_type: list[str] | str | None | object + # arg_types is for positional arguments arg_types: dict[int, str] | None = None + # kwarg_types is for the special case `**kwargs` + kwargs_type: str | None = None check_return_type_inheritance: bool = False @@ -442,9 +445,9 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), ], } -# Properties are normally checked by mypy, and will only be checked -# by pylint when --ignore-missing-annotations is False -_PROPERTY_MATCH: dict[str, list[ClassTypeHintMatch]] = { +# Overriding properties and functions are normally checked by mypy, and will only +# be checked by pylint when --ignore-missing-annotations is False +_INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { "lock": [ ClassTypeHintMatch( base_class="LockEntity", @@ -473,6 +476,36 @@ _PROPERTY_MATCH: dict[str, list[ClassTypeHintMatch]] = { function_name="is_jammed", return_type=["bool", None], ), + TypeHintMatch( + function_name="lock", + kwargs_type="Any", + return_type=None, + ), + TypeHintMatch( + function_name="async_lock", + kwargs_type="Any", + return_type=None, + ), + TypeHintMatch( + function_name="unlock", + kwargs_type="Any", + return_type=None, + ), + TypeHintMatch( + function_name="async_unlock", + kwargs_type="Any", + return_type=None, + ), + TypeHintMatch( + function_name="open", + kwargs_type="Any", + return_type=None, + ), + TypeHintMatch( + function_name="async_open", + kwargs_type="Any", + return_type=None, + ), ], ), ], @@ -613,7 +646,7 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] priority = -1 msgs = { "W7431": ( - "Argument %d should be of type %s", + "Argument %s should be of type %s", "hass-argument-type", "Used when method argument type is incorrect", ), @@ -659,7 +692,7 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] self._class_matchers.extend(class_matches) if not self.linter.config.ignore_missing_annotations and ( - property_matches := _PROPERTY_MATCH.get(module_platform) + property_matches := _INHERITANCE_MATCH.get(module_platform) ): self._class_matchers.extend(property_matches) @@ -709,6 +742,16 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] args=(key + 1, expected_type), ) + # Check that kwargs is correctly annotated. + if match.kwargs_type and not _is_valid_type( + match.kwargs_type, node.args.kwargannotation + ): + self.add_message( + "hass-argument-type", + node=node, + args=(node.args.kwarg, match.kwargs_type), + ) + # Check the return type. if not _is_valid_return_type(match, node.returns): self.add_message( diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 1f601a881a6..262ff93afa8 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -478,7 +478,7 @@ def test_invalid_entity_properties( # Set bypass option type_hint_checker.config.ignore_missing_annotations = False - class_node, prop_node = astroid.extract_node( + class_node, prop_node, func_node = astroid.extract_node( """ class LockEntity(): pass @@ -491,6 +491,12 @@ def test_invalid_entity_properties( self ): pass + + async def async_lock( #@ + self, + **kwargs + ) -> bool: + pass """, "homeassistant.components.pylint_test.lock", ) @@ -507,6 +513,24 @@ def test_invalid_entity_properties( end_line=9, end_col_offset=18, ), + pylint.testutils.MessageTest( + msg_id="hass-argument-type", + node=func_node, + args=("kwargs", "Any"), + line=14, + col_offset=4, + end_line=14, + end_col_offset=24, + ), + pylint.testutils.MessageTest( + msg_id="hass-return-type", + node=func_node, + args="None", + line=14, + col_offset=4, + end_line=14, + end_col_offset=24, + ), ): type_hint_checker.visit_classdef(class_node) @@ -528,6 +552,12 @@ def test_ignore_invalid_entity_properties( self ): pass + + async def async_lock( + self, + **kwargs + ) -> bool: + pass """, "homeassistant.components.pylint_test.lock", ) From db9c2427232931b195be039ac006f0b5ff00ce6e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Jun 2022 11:56:32 -0500 Subject: [PATCH 1645/3516] Speed up creating group entities from YAML (#73649) * Speed up creating group entities from YAML - Pass all the entities to async_add_entities in one call to avoid multiple levels of gather * Speed up creating group entities from YAML - Pass all the entities to async_add_entities in one call to avoid multiple levels of gather * Update homeassistant/components/group/__init__.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/group/__init__.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/group/__init__.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/group/__init__.py Co-authored-by: Martin Hjelmare * typing * unbreak Co-authored-by: Martin Hjelmare --- homeassistant/components/group/__init__.py | 123 +++++++++++++-------- 1 file changed, 74 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index e6d7e91f035..1f8fba21e78 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -104,6 +104,12 @@ CONFIG_SCHEMA = vol.Schema( ) +def _async_get_component(hass: HomeAssistant) -> EntityComponent: + if (component := hass.data.get(DOMAIN)) is None: + component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) + return component + + class GroupIntegrationRegistry: """Class to hold a registry of integrations.""" @@ -274,7 +280,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await async_process_integration_platforms(hass, DOMAIN, _process_group_platform) - await _async_process_config(hass, config, component) + await _async_process_config(hass, config) async def reload_service_handler(service: ServiceCall) -> None: """Remove all user-defined groups and load new ones from config.""" @@ -286,7 +292,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if (conf := await component.async_prepare_reload()) is None: return - await _async_process_config(hass, conf, component) + await _async_process_config(hass, conf) await component.async_add_entities(auto) @@ -406,31 +412,33 @@ async def _process_group_platform(hass, domain, platform): platform.async_describe_on_off_states(hass, hass.data[REG_KEY]) -async def _async_process_config(hass, config, component): +async def _async_process_config(hass: HomeAssistant, config: ConfigType) -> None: """Process group configuration.""" hass.data.setdefault(GROUP_ORDER, 0) - tasks = [] + entities = [] + domain_config: dict[str, dict[str, Any]] = config.get(DOMAIN, {}) - for object_id, conf in config.get(DOMAIN, {}).items(): - name = conf.get(CONF_NAME, object_id) - entity_ids = conf.get(CONF_ENTITIES) or [] - icon = conf.get(CONF_ICON) - mode = conf.get(CONF_ALL) + for object_id, conf in domain_config.items(): + name: str = conf.get(CONF_NAME, object_id) + entity_ids: Iterable[str] = conf.get(CONF_ENTITIES) or [] + icon: str | None = conf.get(CONF_ICON) + mode = bool(conf.get(CONF_ALL)) + order: int = hass.data[GROUP_ORDER] # We keep track of the order when we are creating the tasks # in the same way that async_create_group does to make # sure we use the same ordering system. This overcomes # the problem with concurrently creating the groups - tasks.append( - Group.async_create_group( + entities.append( + Group.async_create_group_entity( hass, name, entity_ids, icon=icon, object_id=object_id, mode=mode, - order=hass.data[GROUP_ORDER], + order=order, ) ) @@ -439,7 +447,8 @@ async def _async_process_config(hass, config, component): # we setup a new group hass.data[GROUP_ORDER] += 1 - await asyncio.gather(*tasks) + # If called before the platform async_setup is called (test cases) + await _async_get_component(hass).async_add_entities(entities) class GroupEntity(Entity): @@ -478,14 +487,14 @@ class Group(Entity): def __init__( self, - hass, - name, - order=None, - icon=None, - user_defined=True, - entity_ids=None, - mode=None, - ): + hass: HomeAssistant, + name: str, + order: int | None = None, + icon: str | None = None, + user_defined: bool = True, + entity_ids: Iterable[str] | None = None, + mode: bool | None = None, + ) -> None: """Initialize a group. This Object has factory function for creation. @@ -508,15 +517,15 @@ class Group(Entity): @staticmethod def create_group( - hass, - name, - entity_ids=None, - user_defined=True, - icon=None, - object_id=None, - mode=None, - order=None, - ): + hass: HomeAssistant, + name: str, + entity_ids: Iterable[str] | None = None, + user_defined: bool = True, + icon: str | None = None, + object_id: str | None = None, + mode: bool | None = None, + order: int | None = None, + ) -> Group: """Initialize a group.""" return asyncio.run_coroutine_threadsafe( Group.async_create_group( @@ -526,20 +535,18 @@ class Group(Entity): ).result() @staticmethod - async def async_create_group( - hass, - name, - entity_ids=None, - user_defined=True, - icon=None, - object_id=None, - mode=None, - order=None, - ): - """Initialize a group. - - This method must be run in the event loop. - """ + @callback + def async_create_group_entity( + hass: HomeAssistant, + name: str, + entity_ids: Iterable[str] | None = None, + user_defined: bool = True, + icon: str | None = None, + object_id: str | None = None, + mode: bool | None = None, + order: int | None = None, + ) -> Group: + """Create a group entity.""" if order is None: hass.data.setdefault(GROUP_ORDER, 0) order = hass.data[GROUP_ORDER] @@ -562,12 +569,30 @@ class Group(Entity): ENTITY_ID_FORMAT, object_id or name, hass=hass ) + return group + + @staticmethod + @callback + async def async_create_group( + hass: HomeAssistant, + name: str, + entity_ids: Iterable[str] | None = None, + user_defined: bool = True, + icon: str | None = None, + object_id: str | None = None, + mode: bool | None = None, + order: int | None = None, + ) -> Group: + """Initialize a group. + + This method must be run in the event loop. + """ + group = Group.async_create_group_entity( + hass, name, entity_ids, user_defined, icon, object_id, mode, order + ) + # If called before the platform async_setup is called (test cases) - if (component := hass.data.get(DOMAIN)) is None: - component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass) - - await component.async_add_entities([group]) - + await _async_get_component(hass).async_add_entities([group]) return group @property From 9940a85e28cdddcef9abe1e99b9a2bdb0daadc56 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Tue, 21 Jun 2022 13:01:06 -0400 Subject: [PATCH 1646/3516] Add sensors for read-only devices for UniFi Protect (#73768) --- .../components/unifiprotect/binary_sensor.py | 216 +++++++++++++++++- .../components/unifiprotect/entity.py | 2 + .../components/unifiprotect/select.py | 13 +- .../components/unifiprotect/sensor.py | 128 ++++++++++- .../components/unifiprotect/switch.py | 7 +- .../components/unifiprotect/utils.py | 24 ++ .../unifiprotect/test_binary_sensor.py | 15 +- tests/components/unifiprotect/test_sensor.py | 17 +- 8 files changed, 391 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 81832de7d44..68c395faaf7 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -34,7 +34,8 @@ from .entity import ( ProtectNVREntity, async_all_device_entities, ) -from .models import ProtectRequiredKeysMixin +from .models import PermRequired, ProtectRequiredKeysMixin +from .utils import async_get_is_highfps _LOGGER = logging.getLogger(__name__) _KEY_DOOR = "door" @@ -69,6 +70,126 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( icon="mdi:brightness-6", ufp_value="is_dark", ), + ProtectBinaryEntityDescription( + key="ssh", + name="SSH Enabled", + icon="mdi:lock", + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="is_ssh_enabled", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="status_light", + name="Status Light On", + icon="mdi:led-on", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_required_field="feature_flags.has_led_status", + ufp_value="led_settings.is_enabled", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="hdr_mode", + name="HDR Mode", + icon="mdi:brightness-7", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_required_field="feature_flags.has_hdr", + ufp_value="hdr_mode", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="high_fps", + name="High FPS", + icon="mdi:video-high-definition", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_required_field="feature_flags.has_highfps", + ufp_value_fn=async_get_is_highfps, + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="system_sounds", + name="System Sounds", + icon="mdi:speaker", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_required_field="feature_flags.has_speaker", + ufp_value="speaker_settings.are_system_sounds_enabled", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="osd_name", + name="Overlay: Show Name", + icon="mdi:fullscreen", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="osd_settings.is_name_enabled", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="osd_date", + name="Overlay: Show Date", + icon="mdi:fullscreen", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="osd_settings.is_date_enabled", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="osd_logo", + name="Overlay: Show Logo", + icon="mdi:fullscreen", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="osd_settings.is_logo_enabled", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="osd_bitrate", + name="Overlay: Show Bitrate", + icon="mdi:fullscreen", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="osd_settings.is_debug_enabled", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="motion", + name="Detections: Motion", + icon="mdi:run-fast", + ufp_value="recording_settings.enable_motion_detection", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="smart_person", + name="Detections: Person", + icon="mdi:walk", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_required_field="can_detect_person", + ufp_value="is_person_detection_on", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="smart_vehicle", + name="Detections: Vehicle", + icon="mdi:car", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_required_field="can_detect_vehicle", + ufp_value="is_vehicle_detection_on", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="smart_face", + name="Detections: Face", + icon="mdi:human-greeting", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_required_field="can_detect_face", + ufp_value="is_face_detection_on", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="smart_package", + name="Detections: Package", + icon="mdi:package-variant-closed", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_required_field="can_detect_package", + ufp_value="is_package_detection_on", + ufp_perm=PermRequired.NO_WRITE, + ), ) LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( @@ -84,6 +205,31 @@ LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.MOTION, ufp_value="is_pir_motion_detected", ), + ProtectBinaryEntityDescription( + key="light", + name="Flood Light", + icon="mdi:spotlight-beam", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="is_light_on", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="ssh", + name="SSH Enabled", + icon="mdi:lock", + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="is_ssh_enabled", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="status_light", + name="Status Light On", + icon="mdi:led-on", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="light_device_settings.is_indicator_enabled", + ufp_perm=PermRequired.NO_WRITE, + ), ) SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( @@ -114,6 +260,53 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.TAMPER, ufp_value="is_tampering_detected", ), + ProtectBinaryEntityDescription( + key="status_light", + name="Status Light On", + icon="mdi:led-on", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="led_settings.is_enabled", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="motion", + name="Motion Detection", + icon="mdi:walk", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="motion_settings.is_enabled", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="temperature", + name="Temperature Sensor", + icon="mdi:thermometer", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="temperature_settings.is_enabled", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="humidity", + name="Humidity Sensor", + icon="mdi:water-percent", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="humidity_settings.is_enabled", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="light", + name="Light Sensor", + icon="mdi:brightness-5", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="light_settings.is_enabled", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectBinaryEntityDescription( + key="alarm", + name="Alarm Sound Detection", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="alarm_settings.is_enabled", + ufp_perm=PermRequired.NO_WRITE, + ), ) MOTION_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( @@ -133,6 +326,26 @@ DOORLOCK_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, ufp_value="battery_status.is_low", ), + ProtectBinaryEntityDescription( + key="status_light", + name="Status Light On", + icon="mdi:led-on", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="led_settings.is_enabled", + ufp_perm=PermRequired.NO_WRITE, + ), +) + +VIEWER_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( + ProtectBinaryEntityDescription( + key="ssh", + name="SSH Enabled", + icon="mdi:lock", + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="is_ssh_enabled", + ufp_perm=PermRequired.NO_WRITE, + ), ) @@ -159,6 +372,7 @@ async def async_setup_entry( light_descs=LIGHT_SENSORS, sense_descs=SENSE_SENSORS, lock_descs=DOORLOCK_SENSORS, + viewer_descs=VIEWER_SENSORS, ) entities += _async_motion_entities(data) entities += _async_nvr_entities(data) diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 155fa49f078..9bf3c8de7a0 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -50,6 +50,8 @@ def _async_device_entities( can_write = device.can_write(data.api.bootstrap.auth_user) if description.ufp_perm == PermRequired.WRITE and not can_write: continue + if description.ufp_perm == PermRequired.NO_WRITE and can_write: + continue if description.ufp_required_field: required_field = get_nested_attr(device, description.ufp_required_field) diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 4432e77ac26..15377a37b27 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -39,6 +39,7 @@ from .const import ATTR_DURATION, ATTR_MESSAGE, DOMAIN, TYPE_EMPTY_VALUE from .data import ProtectData from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectSetableKeysMixin, T +from .utils import async_get_light_motion_current _LOGGER = logging.getLogger(__name__) _KEY_LIGHT_MOTION = "light_motion" @@ -151,16 +152,6 @@ def _get_viewer_current(obj: Viewer) -> str: return obj.liveview_id -def _get_light_motion_current(obj: Light) -> str: - # a bit of extra to allow On Motion Always/Dark - if ( - obj.light_mode_settings.mode == LightModeType.MOTION - and obj.light_mode_settings.enable_at == LightModeEnableType.DARK - ): - return f"{LightModeType.MOTION.value}Dark" - return obj.light_mode_settings.mode.value - - def _get_doorbell_current(obj: Camera) -> str | None: if obj.lcd_message is None: return None @@ -255,7 +246,7 @@ LIGHT_SELECTS: tuple[ProtectSelectEntityDescription, ...] = ( icon="mdi:spotlight", entity_category=EntityCategory.CONFIG, ufp_options=MOTION_MODE_TO_LIGHT_MODE, - ufp_value_fn=_get_light_motion_current, + ufp_value_fn=async_get_light_motion_current, ufp_set_method_fn=_set_light_mode, ufp_perm=PermRequired.WRITE, ), diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 7b14a80ed43..57fe0d5aabd 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -10,6 +10,7 @@ from pyunifiprotect.data import ( NVR, Camera, Event, + Light, ProtectAdoptableDeviceModel, ProtectDeviceModel, ProtectModelWithId, @@ -46,7 +47,8 @@ from .entity import ( ProtectNVREntity, async_all_device_entities, ) -from .models import ProtectRequiredKeysMixin, T +from .models import PermRequired, ProtectRequiredKeysMixin, T +from .utils import async_get_light_motion_current _LOGGER = logging.getLogger(__name__) OBJECT_TYPE_NONE = "none" @@ -197,6 +199,51 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ufp_value="last_ring", entity_registry_enabled_default=False, ), + ProtectSensorEntityDescription( + key="mic_level", + name="Microphone Level", + icon="mdi:microphone", + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + ufp_required_field="feature_flags.has_mic", + ufp_value="mic_volume", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectSensorEntityDescription( + key="recording_mode", + name="Recording Mode", + icon="mdi:video-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="recording_settings.mode", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectSensorEntityDescription( + key="infrared", + name="Infrared Mode", + icon="mdi:circle-opacity", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_required_field="feature_flags.has_led_ir", + ufp_value="isp_settings.ir_led_mode", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectSensorEntityDescription( + key="doorbell_text", + name="Doorbell Text", + icon="mdi:card-text", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_required_field="feature_flags.has_lcd_screen", + ufp_value="lcd_message.text", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectSensorEntityDescription( + key="chime_type", + name="Chime Type", + icon="mdi:bell", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ufp_required_field="feature_flags.has_chime", + ufp_value="chime_type", + ), ) CAMERA_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( @@ -284,6 +331,31 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ufp_value="tampering_detected_at", entity_registry_enabled_default=False, ), + ProtectSensorEntityDescription( + key="sensitivity", + name="Motion Sensitivity", + icon="mdi:walk", + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="motion_settings.sensitivity", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectSensorEntityDescription( + key="mount_type", + name="Mount Type", + icon="mdi:screwdriver", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="mount_type", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectSensorEntityDescription( + key="paired_camera", + name="Paired Camera", + icon="mdi:cctv", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="camera.name", + ufp_perm=PermRequired.NO_WRITE, + ), ) DOORLOCK_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( @@ -296,6 +368,14 @@ DOORLOCK_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, ufp_value="battery_status.percentage", ), + ProtectSensorEntityDescription( + key="paired_camera", + name="Paired Camera", + icon="mdi:cctv", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="camera.name", + ufp_perm=PermRequired.NO_WRITE, + ), ) NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( @@ -439,6 +519,31 @@ LIGHT_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( ufp_value="last_motion", entity_registry_enabled_default=False, ), + ProtectSensorEntityDescription( + key="sensitivity", + name="Motion Sensitivity", + icon="mdi:walk", + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="light_device_settings.pir_sensitivity", + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectSensorEntityDescription[Light]( + key="light_motion", + name="Light Mode", + icon="mdi:spotlight", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value_fn=async_get_light_motion_current, + ufp_perm=PermRequired.NO_WRITE, + ), + ProtectSensorEntityDescription( + key="paired_camera", + name="Paired Camera", + icon="mdi:cctv", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="camera.name", + ufp_perm=PermRequired.NO_WRITE, + ), ) MOTION_TRIP_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( @@ -459,6 +564,26 @@ CHIME_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( icon="mdi:bell", ufp_value="last_ring", ), + ProtectSensorEntityDescription( + key="volume", + name="Volume", + icon="mdi:speaker", + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="volume", + ufp_perm=PermRequired.NO_WRITE, + ), +) + +VIEWER_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( + ProtectSensorEntityDescription( + key="viewer", + name="Liveview", + icon="mdi:view-dashboard", + entity_category=EntityCategory.DIAGNOSTIC, + ufp_value="liveview.name", + ufp_perm=PermRequired.NO_WRITE, + ), ) @@ -478,6 +603,7 @@ async def async_setup_entry( light_descs=LIGHT_SENSORS, lock_descs=DOORLOCK_SENSORS, chime_descs=CHIME_SENSORS, + viewer_descs=VIEWER_SENSORS, ) entities += _async_motion_entities(data) entities += _async_nvr_entities(data) diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 06bf9f7251b..efa91b3a6ba 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -22,6 +22,7 @@ from .const import DOMAIN from .data import ProtectData from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectSetableKeysMixin, T +from .utils import async_get_is_highfps _LOGGER = logging.getLogger(__name__) @@ -36,10 +37,6 @@ class ProtectSwitchEntityDescription( _KEY_PRIVACY_MODE = "privacy_mode" -def _get_is_highfps(obj: Camera) -> bool: - return bool(obj.video_mode == VideoMode.HIGH_FPS) - - async def _set_highfps(obj: Camera, value: bool) -> None: if value: await obj.set_video_mode(VideoMode.HIGH_FPS) @@ -84,7 +81,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( icon="mdi:video-high-definition", entity_category=EntityCategory.CONFIG, ufp_required_field="feature_flags.has_highfps", - ufp_value_fn=_get_is_highfps, + ufp_value_fn=async_get_is_highfps, ufp_set_method_fn=_set_highfps, ufp_perm=PermRequired.WRITE, ), diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index b57753e15d4..b2eb8c1ca65 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -9,8 +9,13 @@ from typing import Any from pyunifiprotect.data import ( Bootstrap, + Camera, + Light, + LightModeEnableType, + LightModeType, ProtectAdoptableDeviceModel, ProtectDeviceModel, + VideoMode, ) from homeassistant.core import HomeAssistant, callback @@ -104,3 +109,22 @@ def async_get_devices( for device_type in model_type for device in async_get_devices_by_type(bootstrap, device_type).values() ) + + +@callback +def async_get_is_highfps(obj: Camera) -> bool: + """Return if camera has High FPS mode enabled.""" + + return bool(obj.video_mode == VideoMode.HIGH_FPS) + + +@callback +def async_get_light_motion_current(obj: Light) -> str: + """Get light motion mode for Flood Light.""" + + if ( + obj.light_mode_settings.mode == LightModeType.MOTION + and obj.light_mode_settings.enable_at == LightModeEnableType.DARK + ): + return f"{LightModeType.MOTION.value}Dark" + return obj.light_mode_settings.mode.value diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 644665cc659..8e868b4af21 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -39,6 +39,9 @@ from .conftest import ( reset_objects, ) +LIGHT_SENSOR_WRITE = LIGHT_SENSORS[:2] +SENSE_SENSORS_WRITE = SENSE_SENSORS[:4] + @pytest.fixture(name="camera") async def camera_fixture( @@ -230,7 +233,7 @@ async def test_binary_sensor_setup_light( entity_registry = er.async_get(hass) - for description in LIGHT_SENSORS: + for description in LIGHT_SENSOR_WRITE: unique_id, entity_id = ids_from_device_description( Platform.BINARY_SENSOR, light, description ) @@ -328,7 +331,7 @@ async def test_binary_sensor_setup_sensor( entity_registry = er.async_get(hass) - for description in SENSE_SENSORS: + for description in SENSE_SENSORS_WRITE: unique_id, entity_id = ids_from_device_description( Platform.BINARY_SENSOR, sensor, description ) @@ -356,7 +359,7 @@ async def test_binary_sensor_setup_sensor_none( STATE_UNAVAILABLE, STATE_OFF, ] - for index, description in enumerate(SENSE_SENSORS): + for index, description in enumerate(SENSE_SENSORS_WRITE): unique_id, entity_id = ids_from_device_description( Platform.BINARY_SENSOR, sensor_none, description ) @@ -419,7 +422,7 @@ async def test_binary_sensor_update_light_motion( """Test binary_sensor motion entity.""" _, entity_id = ids_from_device_description( - Platform.BINARY_SENSOR, light, LIGHT_SENSORS[1] + Platform.BINARY_SENSOR, light, LIGHT_SENSOR_WRITE[1] ) event_metadata = EventMetadata(light_id=light.id) @@ -461,7 +464,7 @@ async def test_binary_sensor_update_mount_type_window( """Test binary_sensor motion entity.""" _, entity_id = ids_from_device_description( - Platform.BINARY_SENSOR, sensor, SENSE_SENSORS[0] + Platform.BINARY_SENSOR, sensor, SENSE_SENSORS_WRITE[0] ) state = hass.states.get(entity_id) @@ -492,7 +495,7 @@ async def test_binary_sensor_update_mount_type_garage( """Test binary_sensor motion entity.""" _, entity_id = ids_from_device_description( - Platform.BINARY_SENSOR, sensor, SENSE_SENSORS[0] + Platform.BINARY_SENSOR, sensor, SENSE_SENSORS_WRITE[0] ) state = hass.states.get(entity_id) diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index eb2558aae3d..1a84c4f55ca 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -51,6 +51,9 @@ from .conftest import ( time_changed, ) +CAMERA_SENSORS_WRITE = CAMERA_SENSORS[:5] +SENSE_SENSORS_WRITE = SENSE_SENSORS[:8] + @pytest.fixture(name="sensor") async def sensor_fixture( @@ -193,7 +196,7 @@ async def test_sensor_setup_sensor( "10.0", "none", ) - for index, description in enumerate(SENSE_SENSORS): + for index, description in enumerate(SENSE_SENSORS_WRITE): if not description.entity_registry_enabled_default: continue unique_id, entity_id = ids_from_device_description( @@ -241,7 +244,7 @@ async def test_sensor_setup_sensor_none( STATE_UNAVAILABLE, STATE_UNAVAILABLE, ) - for index, description in enumerate(SENSE_SENSORS): + for index, description in enumerate(SENSE_SENSORS_WRITE): if not description.entity_registry_enabled_default: continue unique_id, entity_id = ids_from_device_description( @@ -411,7 +414,7 @@ async def test_sensor_setup_camera( ): """Test sensor entity setup for camera devices.""" # 3 from all, 7 from camera, 12 NVR - assert_entity_counts(hass, Platform.SENSOR, 24, 13) + assert_entity_counts(hass, Platform.SENSOR, 25, 13) entity_registry = er.async_get(hass) @@ -421,7 +424,7 @@ async def test_sensor_setup_camera( "100.0", "20.0", ) - for index, description in enumerate(CAMERA_SENSORS): + for index, description in enumerate(CAMERA_SENSORS_WRITE): if not description.entity_registry_enabled_default: continue unique_id, entity_id = ids_from_device_description( @@ -536,7 +539,7 @@ async def test_sensor_update_motion( ): """Test sensor motion entity.""" # 3 from all, 7 from camera, 12 NVR - assert_entity_counts(hass, Platform.SENSOR, 24, 13) + assert_entity_counts(hass, Platform.SENSOR, 25, 13) _, entity_id = ids_from_device_description( Platform.SENSOR, camera, MOTION_SENSORS[0] @@ -584,7 +587,7 @@ async def test_sensor_update_alarm( assert_entity_counts(hass, Platform.SENSOR, 22, 14) _, entity_id = ids_from_device_description( - Platform.SENSOR, sensor, SENSE_SENSORS[4] + Platform.SENSOR, sensor, SENSE_SENSORS_WRITE[4] ) event_metadata = EventMetadata(sensor_id=sensor.id, alarm_type="smoke") @@ -632,7 +635,7 @@ async def test_sensor_update_alarm_with_last_trip_time( # Last Trip Time unique_id, entity_id = ids_from_device_description( - Platform.SENSOR, sensor, SENSE_SENSORS[-3] + Platform.SENSOR, sensor, SENSE_SENSORS_WRITE[-3] ) entity_registry = er.async_get(hass) From adf0f629634d5a5d83174bc850b509996acfc490 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Jun 2022 13:09:22 -0500 Subject: [PATCH 1647/3516] Add websocket api to fetch config entries (#73570) * Add websocket api to fetch config entries * add coverage for failure case --- .../components/config/config_entries.py | 118 +++++++---- .../components/config/test_config_entries.py | 192 +++++++++++++++++- 2 files changed, 265 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index b1756b58c3e..ac452666103 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -3,22 +3,29 @@ from __future__ import annotations import asyncio from http import HTTPStatus +from typing import Any from aiohttp import web import aiohttp.web_exceptions import voluptuous as vol -from homeassistant import config_entries, data_entry_flow, loader +from homeassistant import config_entries, data_entry_flow from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView +from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import DependencyError, Unauthorized from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, FlowManagerResourceView, ) -from homeassistant.loader import Integration, async_get_config_flows +from homeassistant.loader import ( + Integration, + IntegrationNotFound, + async_get_config_flows, + async_get_integration, +) async def async_setup(hass): @@ -33,6 +40,7 @@ async def async_setup(hass): hass.http.register_view(OptionManagerFlowIndexView(hass.config_entries.options)) hass.http.register_view(OptionManagerFlowResourceView(hass.config_entries.options)) + websocket_api.async_register_command(hass, config_entries_get) websocket_api.async_register_command(hass, config_entry_disable) websocket_api.async_register_command(hass, config_entry_update) websocket_api.async_register_command(hass, config_entries_progress) @@ -50,49 +58,13 @@ class ConfigManagerEntryIndexView(HomeAssistantView): async def get(self, request): """List available config entries.""" hass: HomeAssistant = request.app["hass"] - - kwargs = {} + domain = None if "domain" in request.query: - kwargs["domain"] = request.query["domain"] - - entries = hass.config_entries.async_entries(**kwargs) - - if "type" not in request.query: - return self.json([entry_json(entry) for entry in entries]) - - integrations = {} - type_filter = request.query["type"] - - async def load_integration( - hass: HomeAssistant, domain: str - ) -> Integration | None: - """Load integration.""" - try: - return await loader.async_get_integration(hass, domain) - except loader.IntegrationNotFound: - return None - - # Fetch all the integrations so we can check their type - for integration in await asyncio.gather( - *( - load_integration(hass, domain) - for domain in {entry.domain for entry in entries} - ) - ): - if integration: - integrations[integration.domain] = integration - - entries = [ - entry - for entry in entries - if (type_filter != "helper" and entry.domain not in integrations) - or ( - entry.domain in integrations - and integrations[entry.domain].integration_type == type_filter - ) - ] - - return self.json([entry_json(entry) for entry in entries]) + domain = request.query["domain"] + type_filter = None + if "type" in request.query: + type_filter = request.query["type"] + return self.json(await async_matching_config_entries(hass, type_filter, domain)) class ConfigManagerEntryResourceView(HomeAssistantView): @@ -415,6 +387,64 @@ async def ignore_config_flow(hass, connection, msg): connection.send_result(msg["id"]) +@websocket_api.websocket_command( + { + vol.Required("type"): "config_entries/get", + vol.Optional("type_filter"): str, + vol.Optional("domain"): str, + } +) +@websocket_api.async_response +async def config_entries_get( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Return matching config entries by type and/or domain.""" + connection.send_result( + msg["id"], + await async_matching_config_entries( + hass, msg.get("type_filter"), msg.get("domain") + ), + ) + + +async def async_matching_config_entries( + hass: HomeAssistant, type_filter: str | None, domain: str | None +) -> list[dict[str, Any]]: + """Return matching config entries by type and/or domain.""" + kwargs = {} + if domain: + kwargs["domain"] = domain + entries = hass.config_entries.async_entries(**kwargs) + + if type_filter is None: + return [entry_json(entry) for entry in entries] + + integrations = {} + # Fetch all the integrations so we can check their type + tasks = ( + async_get_integration(hass, domain) + for domain in {entry.domain for entry in entries} + ) + results = await asyncio.gather(*tasks, return_exceptions=True) + for integration_or_exc in results: + if isinstance(integration_or_exc, Integration): + integrations[integration_or_exc.domain] = integration_or_exc + elif not isinstance(integration_or_exc, IntegrationNotFound): + raise integration_or_exc + + entries = [ + entry + for entry in entries + if (type_filter != "helper" and entry.domain not in integrations) + or ( + entry.domain in integrations + and integrations[entry.domain].integration_type == type_filter + ) + ] + + return [entry_json(entry) for entry in entries] + + @callback def entry_json(entry: config_entries.ConfigEntry) -> dict: """Return JSON value of a config entry.""" diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index bb0d67fc306..611a7f75939 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -2,7 +2,7 @@ from collections import OrderedDict from http import HTTPStatus -from unittest.mock import AsyncMock, patch +from unittest.mock import ANY, AsyncMock, patch import pytest import voluptuous as vol @@ -13,6 +13,7 @@ from homeassistant.config_entries import HANDLERS, ConfigFlow from homeassistant.core import callback from homeassistant.generated import config_flows from homeassistant.helpers import config_entry_flow, config_validation as cv +from homeassistant.loader import IntegrationNotFound from homeassistant.setup import async_setup_component from tests.common import ( @@ -1113,3 +1114,192 @@ async def test_ignore_flow_nonexisting(hass, hass_ws_client): assert not response["success"] assert response["error"]["code"] == "not_found" + + +async def test_get_entries_ws(hass, hass_ws_client, clear_handlers): + """Test get entries with the websocket api.""" + assert await async_setup_component(hass, "config", {}) + mock_integration(hass, MockModule("comp1")) + mock_integration( + hass, MockModule("comp2", partial_manifest={"integration_type": "helper"}) + ) + mock_integration(hass, MockModule("comp3")) + entry = MockConfigEntry( + domain="comp1", + title="Test 1", + source="bla", + ) + entry.add_to_hass(hass) + MockConfigEntry( + domain="comp2", + title="Test 2", + source="bla2", + state=core_ce.ConfigEntryState.SETUP_ERROR, + reason="Unsupported API", + ).add_to_hass(hass) + MockConfigEntry( + domain="comp3", + title="Test 3", + source="bla3", + disabled_by=core_ce.ConfigEntryDisabler.USER, + ).add_to_hass(hass) + + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/get", + } + ) + response = await ws_client.receive_json() + assert response["id"] == 5 + assert response["result"] == [ + { + "disabled_by": None, + "domain": "comp1", + "entry_id": ANY, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "reason": None, + "source": "bla", + "state": "not_loaded", + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "title": "Test 1", + }, + { + "disabled_by": None, + "domain": "comp2", + "entry_id": ANY, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "reason": "Unsupported API", + "source": "bla2", + "state": "setup_error", + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "title": "Test 2", + }, + { + "disabled_by": "user", + "domain": "comp3", + "entry_id": ANY, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "reason": None, + "source": "bla3", + "state": "not_loaded", + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "title": "Test 3", + }, + ] + + await ws_client.send_json( + { + "id": 6, + "type": "config_entries/get", + "domain": "comp1", + "type_filter": "integration", + } + ) + response = await ws_client.receive_json() + assert response["id"] == 6 + assert response["result"] == [ + { + "disabled_by": None, + "domain": "comp1", + "entry_id": ANY, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "reason": None, + "source": "bla", + "state": "not_loaded", + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "title": "Test 1", + } + ] + # Verify we skip broken integrations + + with patch( + "homeassistant.components.config.config_entries.async_get_integration", + side_effect=IntegrationNotFound("any"), + ): + await ws_client.send_json( + { + "id": 7, + "type": "config_entries/get", + "type_filter": "integration", + } + ) + response = await ws_client.receive_json() + + assert response["id"] == 7 + assert response["result"] == [ + { + "disabled_by": None, + "domain": "comp1", + "entry_id": ANY, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "reason": None, + "source": "bla", + "state": "not_loaded", + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "title": "Test 1", + }, + { + "disabled_by": None, + "domain": "comp2", + "entry_id": ANY, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "reason": "Unsupported API", + "source": "bla2", + "state": "setup_error", + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "title": "Test 2", + }, + { + "disabled_by": "user", + "domain": "comp3", + "entry_id": ANY, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "reason": None, + "source": "bla3", + "state": "not_loaded", + "supports_options": False, + "supports_remove_device": False, + "supports_unload": False, + "title": "Test 3", + }, + ] + + # Verify we raise if something really goes wrong + + with patch( + "homeassistant.components.config.config_entries.async_get_integration", + side_effect=Exception, + ): + await ws_client.send_json( + { + "id": 8, + "type": "config_entries/get", + "type_filter": "integration", + } + ) + response = await ws_client.receive_json() + + assert response["id"] == 8 + assert response["success"] is False From 67618311fa0a400daeef10f8e7256c24f21f9dd0 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Tue, 21 Jun 2022 15:21:47 -0400 Subject: [PATCH 1648/3516] Fix auth_sign_path with query params (#73240) Co-authored-by: J. Nick Koston --- homeassistant/components/http/auth.py | 17 ++++-- tests/components/http/test_auth.py | 78 +++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index dab6abede4c..d89a36cdb86 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -7,11 +7,11 @@ from ipaddress import ip_address import logging import secrets from typing import Final -from urllib.parse import unquote from aiohttp import hdrs from aiohttp.web import Application, Request, StreamResponse, middleware import jwt +from yarl import URL from homeassistant.auth.const import GROUP_ID_READ_ONLY from homeassistant.auth.models import User @@ -57,18 +57,24 @@ def async_sign_path( else: refresh_token_id = hass.data[STORAGE_KEY] + url = URL(path) now = dt_util.utcnow() + params = dict(sorted(url.query.items())) encoded = jwt.encode( { "iss": refresh_token_id, - "path": unquote(path), + "path": url.path, + "params": params, "iat": now, "exp": now + expiration, }, secret, algorithm="HS256", ) - return f"{path}?{SIGN_QUERY_PARAM}={encoded}" + + params[SIGN_QUERY_PARAM] = encoded + url = url.with_query(params) + return f"{url.path}?{url.query_string}" @callback @@ -176,6 +182,11 @@ async def async_setup_auth(hass: HomeAssistant, app: Application) -> None: if claims["path"] != request.path: return False + params = dict(sorted(request.query.items())) + del params[SIGN_QUERY_PARAM] + if claims["params"] != params: + return False + refresh_token = await hass.auth.async_get_refresh_token(claims["iss"]) if refresh_token is None: diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 4a2e1e8aed3..d4995383297 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -17,6 +17,7 @@ from homeassistant.components import websocket_api from homeassistant.components.http.auth import ( CONTENT_USER_NAME, DATA_SIGN_SECRET, + SIGN_QUERY_PARAM, STORAGE_KEY, async_setup_auth, async_sign_path, @@ -294,6 +295,83 @@ async def test_auth_access_signed_path_with_refresh_token( assert req.status == HTTPStatus.UNAUTHORIZED +async def test_auth_access_signed_path_with_query_param( + hass, app, aiohttp_client, hass_access_token +): + """Test access with signed url and query params.""" + app.router.add_post("/", mock_handler) + app.router.add_get("/another_path", mock_handler) + await async_setup_auth(hass, app) + client = await aiohttp_client(app) + + refresh_token = await hass.auth.async_validate_access_token(hass_access_token) + + signed_path = async_sign_path( + hass, "/?test=test", timedelta(seconds=5), refresh_token_id=refresh_token.id + ) + + req = await client.get(signed_path) + assert req.status == HTTPStatus.OK + data = await req.json() + assert data["user_id"] == refresh_token.user.id + + +async def test_auth_access_signed_path_with_query_param_order( + hass, app, aiohttp_client, hass_access_token +): + """Test access with signed url and query params different order.""" + app.router.add_post("/", mock_handler) + app.router.add_get("/another_path", mock_handler) + await async_setup_auth(hass, app) + client = await aiohttp_client(app) + + refresh_token = await hass.auth.async_validate_access_token(hass_access_token) + + signed_path = async_sign_path( + hass, + "/?test=test&foo=bar", + timedelta(seconds=5), + refresh_token_id=refresh_token.id, + ) + url = yarl.URL(signed_path) + signed_path = f"{url.path}?{SIGN_QUERY_PARAM}={url.query.get(SIGN_QUERY_PARAM)}&foo=bar&test=test" + + req = await client.get(signed_path) + assert req.status == HTTPStatus.OK + data = await req.json() + assert data["user_id"] == refresh_token.user.id + + +@pytest.mark.parametrize( + "base_url,test_url", + [ + ("/?test=test", "/?test=test&foo=bar"), + ("/", "/?test=test"), + ("/?test=test&foo=bar", "/?test=test&foo=baz"), + ("/?test=test&foo=bar", "/?test=test"), + ], +) +async def test_auth_access_signed_path_with_query_param_tamper( + hass, app, aiohttp_client, hass_access_token, base_url: str, test_url: str +): + """Test access with signed url and query params that have been tampered with.""" + app.router.add_post("/", mock_handler) + app.router.add_get("/another_path", mock_handler) + await async_setup_auth(hass, app) + client = await aiohttp_client(app) + + refresh_token = await hass.auth.async_validate_access_token(hass_access_token) + + signed_path = async_sign_path( + hass, base_url, timedelta(seconds=5), refresh_token_id=refresh_token.id + ) + url = yarl.URL(signed_path) + token = url.query.get(SIGN_QUERY_PARAM) + + req = await client.get(f"{test_url}&{SIGN_QUERY_PARAM}={token}") + assert req.status == HTTPStatus.UNAUTHORIZED + + async def test_auth_access_signed_path_via_websocket( hass, app, hass_ws_client, hass_read_only_access_token ): From 274f585646f93b6f96aae7b693fda1fd1136d07f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 21 Jun 2022 22:21:31 +0200 Subject: [PATCH 1649/3516] Tweak title of zha config entry created by yellow hw (#73797) --- .../homeassistant_yellow/__init__.py | 3 +- homeassistant/components/zha/config_flow.py | 2 +- .../homeassistant_yellow/test_init.py | 35 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homeassistant_yellow/__init__.py b/homeassistant/components/homeassistant_yellow/__init__.py index 89a73ab769a..e6eaa2f7fce 100644 --- a/homeassistant/components/homeassistant_yellow/__init__.py +++ b/homeassistant/components/homeassistant_yellow/__init__.py @@ -23,12 +23,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "zha", context={"source": "hardware"}, data={ - "radio_type": "efr32", + "name": "Yellow", "port": { "path": "/dev/ttyAMA1", "baudrate": 115200, "flow_control": "hardware", }, + "radio_type": "efr32", }, ) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 03be28b9d8c..69da95e8528 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -269,7 +269,7 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except vol.Invalid: return self.async_abort(reason="invalid_hardware_data") - self._title = data["port"]["path"] + self._title = data.get("name", data["port"]["path"]) self._set_confirm_only() return await self.async_step_confirm_hardware() diff --git a/tests/components/homeassistant_yellow/test_init.py b/tests/components/homeassistant_yellow/test_init.py index 308c392ea26..f534c7cd587 100644 --- a/tests/components/homeassistant_yellow/test_init.py +++ b/tests/components/homeassistant_yellow/test_init.py @@ -41,6 +41,41 @@ async def test_setup_entry( assert len(hass.config_entries.flow.async_progress_by_handler("zha")) == num_flows +async def test_setup_zha(hass: HomeAssistant) -> None: + """Test zha gets the right config.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Yellow", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_yellow.get_os_info", + return_value={"board": "yellow"}, + ) as mock_get_os_info, patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_get_os_info.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries("zha")[0] + assert config_entry.data == { + "device": { + "baudrate": 115200, + "flow_control": "hardware", + "path": "/dev/ttyAMA1", + }, + "radio_type": "ezsp", + } + assert config_entry.options == {} + assert config_entry.title == "Yellow" + + async def test_setup_entry_wrong_board(hass: HomeAssistant) -> None: """Test setup of a config entry with wrong board type.""" mock_integration(hass, MockModule("hassio")) From 562ad18fb45860f8d15261fe35b1e4519d62d9f6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 22 Jun 2022 00:45:47 +0200 Subject: [PATCH 1650/3516] Bump pychromecast to 12.1.4 (#73792) --- 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 644a517c666..d7cddaaa293 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==12.1.3"], + "requirements": ["pychromecast==12.1.4"], "after_dependencies": [ "cloud", "http", diff --git a/requirements_all.txt b/requirements_all.txt index 2daa37f0808..859de73843f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1408,7 +1408,7 @@ pycfdns==1.2.2 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==12.1.3 +pychromecast==12.1.4 # homeassistant.components.pocketcasts pycketcasts==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd02dc2ce5c..e576babe4b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -953,7 +953,7 @@ pybotvac==0.0.23 pycfdns==1.2.2 # homeassistant.components.cast -pychromecast==12.1.3 +pychromecast==12.1.4 # homeassistant.components.climacell pyclimacell==0.18.2 From 78dd522ccda18c436a827e036d5a0f1dd8a7673e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 22 Jun 2022 00:26:36 +0000 Subject: [PATCH 1651/3516] [ci skip] Translation update --- .../eight_sleep/translations/tr.json | 19 +++++ .../components/google/translations/ca.json | 1 + .../components/google/translations/en.json | 1 + .../components/google/translations/et.json | 1 + .../components/google/translations/fr.json | 1 + .../components/google/translations/pt-BR.json | 1 + .../components/google/translations/tr.json | 4 + .../google/translations/zh-Hant.json | 1 + .../components/nest/translations/ca.json | 1 + .../components/nest/translations/en.json | 1 + .../components/nest/translations/et.json | 1 + .../components/nest/translations/fr.json | 1 + .../components/nest/translations/pt-BR.json | 1 + .../components/nest/translations/tr.json | 30 ++++++++ .../components/nest/translations/zh-Hant.json | 1 + .../components/plugwise/translations/tr.json | 4 +- .../radiotherm/translations/tr.json | 31 ++++++++ .../components/scrape/translations/tr.json | 73 +++++++++++++++++++ .../sensibo/translations/sensor.tr.json | 8 ++ .../components/skybell/translations/tr.json | 21 ++++++ .../tankerkoenig/translations/tr.json | 11 ++- .../transmission/translations/de.json | 10 ++- .../transmission/translations/pt-BR.json | 10 ++- .../transmission/translations/tr.json | 10 ++- .../transmission/translations/zh-Hant.json | 10 ++- 25 files changed, 246 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/eight_sleep/translations/tr.json create mode 100644 homeassistant/components/radiotherm/translations/tr.json create mode 100644 homeassistant/components/scrape/translations/tr.json create mode 100644 homeassistant/components/sensibo/translations/sensor.tr.json create mode 100644 homeassistant/components/skybell/translations/tr.json diff --git a/homeassistant/components/eight_sleep/translations/tr.json b/homeassistant/components/eight_sleep/translations/tr.json new file mode 100644 index 00000000000..19e958601cb --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Sekiz Uyku bulutuna ba\u011flan\u0131lam\u0131yor: {error}" + }, + "error": { + "cannot_connect": "Sekiz Uyku bulutuna ba\u011flan\u0131lam\u0131yor: {error}" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/ca.json b/homeassistant/components/google/translations/ca.json index 27c9a11f92b..004fcb67f46 100644 --- a/homeassistant/components/google/translations/ca.json +++ b/homeassistant/components/google/translations/ca.json @@ -6,6 +6,7 @@ "abort": { "already_configured": "El compte ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "cannot_connect": "Ha fallat la connexi\u00f3", "code_expired": "El codi d'autenticaci\u00f3 ha caducat o la configuraci\u00f3 de credencials no \u00e9s v\u00e0lida. Torna-ho a provar.", "invalid_access_token": "Token d'acc\u00e9s inv\u00e0lid", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", diff --git a/homeassistant/components/google/translations/en.json b/homeassistant/components/google/translations/en.json index 1bebde4f63a..2ef34ccc84b 100644 --- a/homeassistant/components/google/translations/en.json +++ b/homeassistant/components/google/translations/en.json @@ -6,6 +6,7 @@ "abort": { "already_configured": "Account is already configured", "already_in_progress": "Configuration flow is already in progress", + "cannot_connect": "Failed to connect", "code_expired": "Authentication code expired or credential setup is invalid, please try again.", "invalid_access_token": "Invalid access token", "missing_configuration": "The component is not configured. Please follow the documentation.", diff --git a/homeassistant/components/google/translations/et.json b/homeassistant/components/google/translations/et.json index 11447b1669f..1b5aff5774b 100644 --- a/homeassistant/components/google/translations/et.json +++ b/homeassistant/components/google/translations/et.json @@ -6,6 +6,7 @@ "abort": { "already_configured": "Kasutaja on juba seadistatud", "already_in_progress": "Seadistamine on juba k\u00e4imas", + "cannot_connect": "\u00dchendamine nurjus", "code_expired": "Tuvastuskood on aegunud v\u00f5i mandaadi seadistus on vale, proovi uuesti.", "invalid_access_token": "Vigane juurdep\u00e4\u00e4sut\u00f5end", "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.", diff --git a/homeassistant/components/google/translations/fr.json b/homeassistant/components/google/translations/fr.json index ed60ccb1c1b..7c5e4927787 100644 --- a/homeassistant/components/google/translations/fr.json +++ b/homeassistant/components/google/translations/fr.json @@ -6,6 +6,7 @@ "abort": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "cannot_connect": "\u00c9chec de connexion", "code_expired": "Le code d'authentification a expir\u00e9 ou la configuration des informations d'identification n'est pas valide, veuillez r\u00e9essayer.", "invalid_access_token": "Jeton d'acc\u00e8s non valide", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", diff --git a/homeassistant/components/google/translations/pt-BR.json b/homeassistant/components/google/translations/pt-BR.json index 381976e0284..e934155c9fa 100644 --- a/homeassistant/components/google/translations/pt-BR.json +++ b/homeassistant/components/google/translations/pt-BR.json @@ -6,6 +6,7 @@ "abort": { "already_configured": "A conta j\u00e1 foi configurada", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "cannot_connect": "Falha ao se conectar", "code_expired": "O c\u00f3digo de autentica\u00e7\u00e3o expirou ou a configura\u00e7\u00e3o da credencial \u00e9 inv\u00e1lida. Tente novamente.", "invalid_access_token": "Token de acesso inv\u00e1lido", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", diff --git a/homeassistant/components/google/translations/tr.json b/homeassistant/components/google/translations/tr.json index f7b5b6d79ff..ec265926695 100644 --- a/homeassistant/components/google/translations/tr.json +++ b/homeassistant/components/google/translations/tr.json @@ -1,8 +1,12 @@ { + "application_credentials": { + "description": "Home Assistant'\u0131n Google Takviminize eri\u015fmesine izin vermek i\u00e7in [OAuth izin ekran\u0131]( {oauth_consent_url} ) i\u00e7in [talimatlar\u0131]( {more_info_url} ) uygulay\u0131n. Ayr\u0131ca, Takviminize ba\u011fl\u0131 Uygulama Kimlik Bilgileri olu\u015fturman\u0131z gerekir:\n 1. [Kimlik Bilgileri]( {oauth_creds_url} ) \u00f6\u011fesine gidin ve **Kimlik Bilgileri Olu\u015ftur**'u t\u0131klay\u0131n.\n 1. A\u00e7\u0131l\u0131r listeden **OAuth istemci kimli\u011fi**'ni se\u00e7in.\n 1. Uygulama T\u00fcr\u00fc i\u00e7in **TV ve S\u0131n\u0131rl\u0131 Giri\u015f cihazlar\u0131**'n\u0131 se\u00e7in. \n\n" + }, "config": { "abort": { "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131", "code_expired": "Kimlik do\u011frulama kodunun s\u00fcresi doldu veya kimlik bilgisi kurulumu ge\u00e7ersiz, l\u00fctfen tekrar deneyin.", "invalid_access_token": "Ge\u00e7ersiz eri\u015fim anahtar\u0131", "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", diff --git a/homeassistant/components/google/translations/zh-Hant.json b/homeassistant/components/google/translations/zh-Hant.json index 7b83154db1a..bee271ea8e7 100644 --- a/homeassistant/components/google/translations/zh-Hant.json +++ b/homeassistant/components/google/translations/zh-Hant.json @@ -6,6 +6,7 @@ "abort": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "cannot_connect": "\u9023\u7dda\u5931\u6557", "code_expired": "\u8a8d\u8b49\u78bc\u5df2\u904e\u671f\u6216\u6191\u8b49\u8a2d\u5b9a\u7121\u6548\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index 0767d9c1cf1..791be9975eb 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -4,6 +4,7 @@ }, "config": { "abort": { + "already_configured": "El compte ja est\u00e0 configurat", "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "invalid_access_token": "Token d'acc\u00e9s inv\u00e0lid", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index 2f3324ea956..5f026e55f31 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -4,6 +4,7 @@ }, "config": { "abort": { + "already_configured": "Account is already configured", "authorize_url_timeout": "Timeout generating authorize URL.", "invalid_access_token": "Invalid access token", "missing_configuration": "The component is not configured. Please follow the documentation.", diff --git a/homeassistant/components/nest/translations/et.json b/homeassistant/components/nest/translations/et.json index 3d8cd25f47c..b07845f7dab 100644 --- a/homeassistant/components/nest/translations/et.json +++ b/homeassistant/components/nest/translations/et.json @@ -4,6 +4,7 @@ }, "config": { "abort": { + "already_configured": "Konto on juba h\u00e4\u00e4lestatud", "authorize_url_timeout": "Tuvastamise URL-i loomise ajal\u00f5pp.", "invalid_access_token": "Vigane juurdep\u00e4\u00e4sut\u00f5end", "missing_configuration": "Osis pole seadistatud. Vaata dokumentatsiooni.", diff --git a/homeassistant/components/nest/translations/fr.json b/homeassistant/components/nest/translations/fr.json index 30adccca440..16990b93193 100644 --- a/homeassistant/components/nest/translations/fr.json +++ b/homeassistant/components/nest/translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", "invalid_access_token": "Jeton d'acc\u00e8s non valide", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", diff --git a/homeassistant/components/nest/translations/pt-BR.json b/homeassistant/components/nest/translations/pt-BR.json index 1a4b2d8fad9..d96387aee82 100644 --- a/homeassistant/components/nest/translations/pt-BR.json +++ b/homeassistant/components/nest/translations/pt-BR.json @@ -4,6 +4,7 @@ }, "config": { "abort": { + "already_configured": "Conta j\u00e1 configurada", "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", "invalid_access_token": "Token de acesso inv\u00e1lido", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", diff --git a/homeassistant/components/nest/translations/tr.json b/homeassistant/components/nest/translations/tr.json index f39b0fc935e..1732443184e 100644 --- a/homeassistant/components/nest/translations/tr.json +++ b/homeassistant/components/nest/translations/tr.json @@ -1,6 +1,10 @@ { + "application_credentials": { + "description": "Bulut Konsolunu yap\u0131land\u0131rmak i\u00e7in [talimatlar\u0131]( {more_info_url} ) izleyin: \n\n 1. [OAuth izin ekran\u0131na]( {oauth_consent_url} ) gidin ve yap\u0131land\u0131r\u0131n\n 1. [Kimlik Bilgileri]( {oauth_creds_url} ) \u00f6\u011fesine gidin ve **Kimlik Bilgileri Olu\u015ftur**'u t\u0131klay\u0131n.\n 1. A\u00e7\u0131l\u0131r listeden **OAuth istemci kimli\u011fi**'ni se\u00e7in.\n 1. Uygulama T\u00fcr\u00fc i\u00e7in **Web Uygulamas\u0131**'n\u0131 se\u00e7in.\n 1. *Yetkili y\u00f6nlendirme URI's\u0131* alt\u0131na ` {redirect_url} ` ekleyin." + }, "config": { "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", "invalid_access_token": "Ge\u00e7ersiz eri\u015fim anahtar\u0131", "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", @@ -29,6 +33,32 @@ "description": "Google hesab\u0131n\u0131z\u0131 ba\u011flamak i\u00e7in [hesab\u0131n\u0131z\u0131 yetkilendirin]( {url} ). \n\n Yetkilendirmeden sonra, sa\u011flanan Auth Token kodunu a\u015fa\u011f\u0131ya kopyalay\u0131p yap\u0131\u015ft\u0131r\u0131n.", "title": "Google Hesab\u0131n\u0131 Ba\u011fla" }, + "auth_upgrade": { + "description": "App Auth, g\u00fcenli\u011fi art\u0131rmak i\u00e7in Google taraf\u0131ndan kullan\u0131mdan kald\u0131r\u0131ld\u0131 ve yeni uygulama kimlik bilgileri olu\u015fturarak i\u015flem yapman\u0131z gerekiyor. \n\n Takip etmek i\u00e7in [belgeleri]( {more_info_url} ) a\u00e7\u0131n, \u00e7\u00fcnk\u00fc sonraki ad\u0131mlar Nest cihazlar\u0131n\u0131za eri\u015fimi geri y\u00fcklemek i\u00e7in atman\u0131z gereken ad\u0131mlar konusunda size rehberlik edecektir.", + "title": "Nest: Uygulama Yetkilendirmesinin Kullan\u0131mdan Kald\u0131r\u0131lmas\u0131" + }, + "cloud_project": { + "data": { + "cloud_project_id": "Google Bulut Proje Kimli\u011fi" + }, + "description": "Bulut Projesi Kimli\u011fini a\u015fa\u011f\u0131ya girin, \u00f6rne\u011fin *example-project-12345*. [Google Cloud Console]( {cloud_console_url} ) veya [daha fazla bilgi]( {more_info_url} ) i\u00e7in belgelere bak\u0131n.", + "title": "Nest: Bulut Proje Kimli\u011fini Girin" + }, + "create_cloud_project": { + "description": "Nest entegrasyonu, Ak\u0131ll\u0131 Cihaz Y\u00f6netimi API'sini kullanarak Nest Termostatlar\u0131n\u0131z\u0131, Kameralar\u0131n\u0131z\u0131 ve Kap\u0131 Zillerinizi entegre etmenize olanak tan\u0131r. SDM API **bir kereye mahsus 5 ABD dolar\u0131** tutar\u0131nda kurulum \u00fccreti gerektirir. [daha fazla bilgi]( {more_info_url} ) i\u00e7in belgelere bak\u0131n. \n\n 1. [Google Bulut Konsoluna]( {cloud_console_url} ) gidin.\n 1. Bu ilk projenizse, **Proje Olu\u015ftur**'a ve ard\u0131ndan **Yeni Proje**'ye t\u0131klay\u0131n.\n 1. Bulut Projenize bir Ad verin ve ard\u0131ndan **Olu\u015ftur**'a t\u0131klay\u0131n.\n 1. Bulut Projesi Kimli\u011fini kaydedin, \u00f6rne\u011fin *example-project-12345* daha sonra ihtiya\u00e7 duyaca\u011f\u0131n\u0131z i\u00e7in\n 1. [Smart Device Management API]( {sdm_api_url} ) için API Kitapl\u0131\u011f\u0131na gidin ve **Etkinle\u015ftir**'i t\u0131klay\u0131n.\n 1. [Cloud Pub/Sub API]( {pubsub_api_url} ) için API Kitapl\u0131\u011f\u0131'na gidin ve **Etkinle\u015ftir**'i t\u0131klay\u0131n. \n\n Bulut projeniz kuruldu\u011funda devam edin.", + "title": "Nest: Bulut Projesi olu\u015fturma ve yap\u0131land\u0131rma" + }, + "device_project": { + "data": { + "project_id": "Cihaz Eri\u015fim Projesi Kimli\u011fi" + }, + "description": "Kurmak i\u00e7n **5 ABD dolar\u0131 \u00fccret** gerektiren bir Nest Cihaz Eri\u015fimi projesi olu\u015fturun.\n 1. [Cihaz Eri\u015fim Konsolu]( {device_access_console_url} )'e gidin ve \u00f6deme ak\u0131\u015f\u0131ndan ge\u00e7in.\n 1. **Proje olu\u015ftur**'a t\u0131klay\u0131n\n 1. Cihaz Eri\u015fimi projenize bir ad verin ve **\u0130leri**'ye t\u0131klay\u0131n.\n 1. OAuth M\u00fc\u015fteri Kimli\u011finizi girin\n 1. **Etkinle\u015ftir** ve **Proje olu\u015ftur**'a t\u0131klayarak etkinlikleri etkinle\u015ftirin. \n\n Cihaz Eri\u015fim Projesi Kimli\u011finizi a\u015fa\u011f\u0131ya girin ([daha fazla bilgi]( {more_info_url} )).\n", + "title": "Yuva: Bir Cihaz Eri\u015fim Projesi Olu\u015fturun" + }, + "device_project_upgrade": { + "description": "Nest Device Access Project'i yeni OAuth \u0130stemci Kimli\u011finizle g\u00fcncelleyin ([daha fazla bilgi]( {more_info_url} ))\n 1. [Cihaz Eri\u015fim Konsolu]'na gidin ( {device_access_console_url} ).\n 1. *OAuth \u0130stemci Kimli\u011fi*'nin yan\u0131ndaki \u00e7\u00f6p kutusu simgesini t\u0131klay\u0131n.\n 1. `...` ta\u015fma men\u00fcs\u00fcn\u00fc; ve *M\u00fc\u015fteri Kimli\u011fi Ekle*'yi t\u0131klay\u0131n.\n 1. Yeni OAuth \u0130stemci Kimli\u011finizi girin ve **Ekle**'yi t\u0131klay\u0131n. \n\n OAuth M\u00fc\u015fteri Kimli\u011finiz: ` {client_id} `", + "title": "Nest: Cihaz Eri\u015fim Projesini G\u00fcncelle" + }, "init": { "data": { "flow_impl": "Sa\u011flay\u0131c\u0131" diff --git a/homeassistant/components/nest/translations/zh-Hant.json b/homeassistant/components/nest/translations/zh-Hant.json index 8eb646217b1..11a4823d77a 100644 --- a/homeassistant/components/nest/translations/zh-Hant.json +++ b/homeassistant/components/nest/translations/zh-Hant.json @@ -4,6 +4,7 @@ }, "config": { "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json index b4a07700d6f..b4b49a5e52d 100644 --- a/homeassistant/components/plugwise/translations/tr.json +++ b/homeassistant/components/plugwise/translations/tr.json @@ -15,7 +15,9 @@ "data": { "flow_type": "Ba\u011flant\u0131 t\u00fcr\u00fc", "host": "IP Adresi", - "port": "Port" + "password": "Smile Kimli\u011fi", + "port": "Port", + "username": "Smile Kullan\u0131c\u0131 Ad\u0131" }, "description": "\u00dcr\u00fcn:", "title": "Plugwise tipi" diff --git a/homeassistant/components/radiotherm/translations/tr.json b/homeassistant/components/radiotherm/translations/tr.json new file mode 100644 index 00000000000..f8e6b4f7a6d --- /dev/null +++ b/homeassistant/components/radiotherm/translations/tr.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "{name} {model} ( {host} ) kurulumu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "host": "Sunucu" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "S\u0131cakl\u0131\u011f\u0131 ayarlarken kal\u0131c\u0131 bir bekletme ayarlay\u0131n." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/tr.json b/homeassistant/components/scrape/translations/tr.json new file mode 100644 index 00000000000..954ce1ad052 --- /dev/null +++ b/homeassistant/components/scrape/translations/tr.json @@ -0,0 +1,73 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "attribute": "\u00d6znitelik", + "authentication": "Kimlik do\u011frulama", + "device_class": "Cihaz S\u0131n\u0131f\u0131", + "headers": "Ba\u015fl\u0131klar", + "index": "Dizin", + "name": "Ad", + "password": "Parola", + "resource": "Kaynak", + "select": "Se\u00e7", + "state_class": "Durum S\u0131n\u0131f\u0131", + "unit_of_measurement": "\u00d6l\u00e7\u00fc Birimi", + "username": "Kullan\u0131c\u0131 Ad\u0131", + "value_template": "De\u011fer \u015eablonu", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" + }, + "data_description": { + "attribute": "Se\u00e7ilen etikette bir \u00f6zelli\u011fin de\u011ferini al\u0131n", + "authentication": "HTTP kimlik do\u011frulamas\u0131n\u0131n t\u00fcr\u00fc. Temel veya basit", + "device_class": "\u00d6nu\u00e7taki simgeyi ayarlamak i\u00e7in sens\u00f6r\u00fcn t\u00fcr\u00fc/s\u0131n\u0131f\u0131", + "headers": "Web iste\u011fi i\u00e7in kullan\u0131lacak ba\u015fl\u0131klar", + "index": "CSS se\u00e7ici taraf\u0131ndan d\u00f6nd\u00fcr\u00fclen \u00f6\u011felerden hangisinin kullan\u0131laca\u011f\u0131n\u0131 tan\u0131mlar", + "resource": "De\u011feri i\u00e7eren web sitesinin URL'si", + "select": "Hangi etiketin aranaca\u011f\u0131n\u0131 tan\u0131mlar. Ayr\u0131nt\u0131lar i\u00e7in Beautifulsoup CSS se\u00e7icilerini kontrol edin", + "state_class": "Sens\u00f6r\u00fcn state_class", + "value_template": "Sens\u00f6r\u00fcn durumunu almak i\u00e7in bir \u015fablon tan\u0131mlar", + "verify_ssl": "\u00d6rne\u011fin, kendinden imzal\u0131ysa, SSL/TLS sertifikas\u0131n\u0131n do\u011frulanmas\u0131n\u0131 etkinle\u015ftirir/devre d\u0131\u015f\u0131 b\u0131rak\u0131r" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "attribute": "\u00d6znitelik", + "authentication": "Kimlik do\u011frulama", + "device_class": "Cihaz S\u0131n\u0131f\u0131", + "headers": "Ba\u015fl\u0131klar", + "index": "Dizin", + "name": "Ad", + "password": "Parola", + "resource": "Kaynak", + "select": "Se\u00e7", + "state_class": "Durum S\u0131n\u0131f\u0131", + "unit_of_measurement": "\u00d6l\u00e7\u00fc Birimi", + "username": "Kullan\u0131c\u0131 Ad\u0131", + "value_template": "De\u011fer \u015eablonu", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" + }, + "data_description": { + "attribute": "Se\u00e7ilen etikette bir \u00f6zelli\u011fin de\u011ferini al\u0131n", + "authentication": "HTTP kimlik do\u011frulamas\u0131n\u0131n t\u00fcr\u00fc. Temel veya basit", + "device_class": "\u00d6nu\u00e7taki simgeyi ayarlamak i\u00e7in sens\u00f6r\u00fcn t\u00fcr\u00fc/s\u0131n\u0131f\u0131", + "headers": "Web iste\u011fi i\u00e7in kullan\u0131lacak ba\u015fl\u0131klar", + "index": "CSS se\u00e7ici taraf\u0131ndan d\u00f6nd\u00fcr\u00fclen \u00f6\u011felerden hangisinin kullan\u0131laca\u011f\u0131n\u0131 tan\u0131mlar", + "resource": "De\u011feri i\u00e7eren web sitesinin URL'si", + "select": "Hangi etiketin aranaca\u011f\u0131n\u0131 tan\u0131mlar. Ayr\u0131nt\u0131lar i\u00e7in Beautifulsoup CSS se\u00e7icilerini kontrol edin", + "state_class": "Sens\u00f6r\u00fcn state_class", + "value_template": "Sens\u00f6r\u00fcn durumunu almak i\u00e7in bir \u015fablon tan\u0131mlar", + "verify_ssl": "\u00d6rne\u011fin, kendinden imzal\u0131ysa, SSL/TLS sertifikas\u0131n\u0131n do\u011frulanmas\u0131n\u0131 etkinle\u015ftirir/devre d\u0131\u015f\u0131 b\u0131rak\u0131r" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.tr.json b/homeassistant/components/sensibo/translations/sensor.tr.json new file mode 100644 index 00000000000..3364a75abe2 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.tr.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "Normal", + "s": "Duyarl\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/tr.json b/homeassistant/components/skybell/translations/tr.json new file mode 100644 index 00000000000..68bd9029559 --- /dev/null +++ b/homeassistant/components/skybell/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "email": "E-posta", + "password": "Parola" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/tr.json b/homeassistant/components/tankerkoenig/translations/tr.json index 2d88d2fa670..ca0038b6dbb 100644 --- a/homeassistant/components/tankerkoenig/translations/tr.json +++ b/homeassistant/components/tankerkoenig/translations/tr.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "no_stations": "Menzilde herhangi bir istasyon bulunamad\u0131." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API Anahtar\u0131" + } + }, "select_station": { "data": { "stations": "\u0130stasyonlar" @@ -32,7 +38,8 @@ "init": { "data": { "scan_interval": "G\u00fcncelle\u015ftirme aral\u0131\u011f\u0131", - "show_on_map": "\u0130stasyonlar\u0131 haritada g\u00f6ster" + "show_on_map": "\u0130stasyonlar\u0131 haritada g\u00f6ster", + "stations": "\u0130stasyonlar" }, "title": "Tankerkoenig se\u00e7enekleri" } diff --git a/homeassistant/components/transmission/translations/de.json b/homeassistant/components/transmission/translations/de.json index 2355905d1f7..04274f2c1cb 100644 --- a/homeassistant/components/transmission/translations/de.json +++ b/homeassistant/components/transmission/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": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -9,6 +10,13 @@ "name_exists": "Name existiert bereits" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Das Passwort f\u00fcr {username} ist ung\u00fcltig.", + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/transmission/translations/pt-BR.json b/homeassistant/components/transmission/translations/pt-BR.json index 3353884ef33..781d21e2900 100644 --- a/homeassistant/components/transmission/translations/pt-BR.json +++ b/homeassistant/components/transmission/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "Re-autenticado com sucesso" }, "error": { "cannot_connect": "Falha ao conectar", @@ -9,6 +10,13 @@ "name_exists": "O Nome j\u00e1 existe" }, "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "A senha para o usu\u00e1rio {username} est\u00e1 inv\u00e1lida.", + "title": "Re-autenticar integra\u00e7\u00e3o" + }, "user": { "data": { "host": "Nome do host", diff --git a/homeassistant/components/transmission/translations/tr.json b/homeassistant/components/transmission/translations/tr.json index 72b3410062e..bef2fd47ff7 100644 --- a/homeassistant/components/transmission/translations/tr.json +++ b/homeassistant/components/transmission/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", @@ -9,6 +10,13 @@ "name_exists": "Ad zaten var" }, "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "{username} i\u00e7n \u015fifre ge\u00e7ersiz.", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "host": "Sunucu", diff --git a/homeassistant/components/transmission/translations/zh-Hant.json b/homeassistant/components/transmission/translations/zh-Hant.json index b6769274148..fd3d3a909aa 100644 --- a/homeassistant/components/transmission/translations/zh-Hant.json +++ b/homeassistant/components/transmission/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -9,6 +10,13 @@ "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "{username} \u5bc6\u78bc\u7121\u6548\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", From 21275669d56aab1f0e76a83ab87a5e11b8166e4f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 22 Jun 2022 03:00:58 +0200 Subject: [PATCH 1652/3516] Fix inheritance in zha general channel (#73774) Fix general channel type hints in zha --- homeassistant/components/zha/core/channels/general.py | 5 +++-- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 8886085bf47..b2870d84e15 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -408,9 +408,9 @@ class OnOffConfiguration(ZigbeeChannel): """OnOff Configuration channel.""" -@registries.CLIENT_CHANNELS_REGISTRY.register(general.Ota.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Ota.cluster_id) -class Ota(ZigbeeChannel): +@registries.CLIENT_CHANNELS_REGISTRY.register(general.Ota.cluster_id) +class Ota(ClientChannel): """OTA Channel.""" BIND: bool = False @@ -427,6 +427,7 @@ class Ota(ZigbeeChannel): signal_id = self._ch_pool.unique_id.split("-")[0] if cmd_name == "query_next_image": + assert args self.async_send_signal(SIGNAL_UPDATE_DEVICE.format(signal_id), args[3]) diff --git a/mypy.ini b/mypy.ini index 98884d333e7..b01acbe311f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2997,9 +2997,6 @@ ignore_errors = true [mypy-homeassistant.components.zha.core.channels.base] ignore_errors = true -[mypy-homeassistant.components.zha.core.channels.general] -ignore_errors = true - [mypy-homeassistant.components.zha.core.channels.homeautomation] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index b519e7f2daf..e5bb404b715 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -148,7 +148,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.zha.button", "homeassistant.components.zha.climate", "homeassistant.components.zha.core.channels.base", - "homeassistant.components.zha.core.channels.general", "homeassistant.components.zha.core.channels.homeautomation", "homeassistant.components.zha.core.channels.hvac", "homeassistant.components.zha.core.channels.security", From 6c83ed4c9dbe0a4d272a9435a8156f68133325ec Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 22 Jun 2022 03:02:08 +0200 Subject: [PATCH 1653/3516] Fix api, button and climate type hints in zha (#73771) * Fix zha api type hints * Fix zha button type hints * Fix zha climate type hints --- homeassistant/components/zha/api.py | 14 +++++++++---- homeassistant/components/zha/button.py | 4 ++-- homeassistant/components/zha/climate.py | 26 ++++++++++++------------- mypy.ini | 9 --------- script/hassfest/mypy_config.py | 3 --- 5 files changed, 25 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 737bef5ddff..cc4dd45689e 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -346,7 +346,7 @@ async def websocket_get_groupable_devices( groupable_devices = [] for device in devices: - entity_refs = zha_gateway.device_registry.get(device.ieee) + entity_refs = zha_gateway.device_registry[device.ieee] for ep_id in device.async_get_groupable_endpoints(): groupable_devices.append( { @@ -456,6 +456,7 @@ async def websocket_add_group( group_id: int | None = msg.get(GROUP_ID) members: list[GroupMember] | None = msg.get(ATTR_MEMBERS) group = await zha_gateway.async_create_zigpy_group(group_name, members, group_id) + assert group connection.send_result(msg[ID], group.group_info) @@ -559,7 +560,7 @@ async def websocket_reconfigure_node( """Reconfigure a ZHA nodes entities by its ieee address.""" zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee: EUI64 = msg[ATTR_IEEE] - device: ZHADevice = zha_gateway.get_device(ieee) + device: ZHADevice | None = zha_gateway.get_device(ieee) async def forward_messages(data): """Forward events to websocket.""" @@ -577,6 +578,7 @@ async def websocket_reconfigure_node( connection.subscriptions[msg["id"]] = async_cleanup _LOGGER.debug("Reconfiguring node with ieee_address: %s", ieee) + assert device hass.async_create_task(device.async_configure()) @@ -905,6 +907,7 @@ async def websocket_bind_group( group_id: int = msg[GROUP_ID] bindings: list[ClusterBinding] = msg[BINDINGS] source_device = zha_gateway.get_device(source_ieee) + assert source_device await source_device.async_bind_to_group(group_id, bindings) @@ -927,6 +930,7 @@ async def websocket_unbind_group( group_id: int = msg[GROUP_ID] bindings: list[ClusterBinding] = msg[BINDINGS] source_device = zha_gateway.get_device(source_ieee) + assert source_device await source_device.async_unbind_from_group(group_id, bindings) @@ -941,6 +945,8 @@ async def async_binding_operation( source_device = zha_gateway.get_device(source_ieee) target_device = zha_gateway.get_device(target_ieee) + assert source_device + assert target_device clusters_to_bind = await get_matched_clusters(source_device, target_device) zdo = source_device.device.zdo @@ -997,7 +1003,7 @@ async def websocket_get_configuration( return cv.custom_serializer(schema) - data = {"schemas": {}, "data": {}} + data: dict[str, dict[str, Any]] = {"schemas": {}, "data": {}} for section, schema in ZHA_CONFIG_SCHEMAS.items(): if section == ZHA_ALARM_OPTIONS and not async_cluster_exists( hass, IasAce.cluster_id @@ -1084,7 +1090,7 @@ def async_load_api(hass: HomeAssistant) -> None: """Remove a node from the network.""" zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee: EUI64 = service.data[ATTR_IEEE] - zha_device: ZHADevice = zha_gateway.get_device(ieee) + zha_device: ZHADevice | None = zha_gateway.get_device(ieee) if zha_device is not None and ( zha_device.is_coordinator and zha_device.ieee == zha_gateway.application_controller.ieee diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py index ed0836042d2..29bfb2ca248 100644 --- a/homeassistant/components/zha/button.py +++ b/homeassistant/components/zha/button.py @@ -59,7 +59,7 @@ async def async_setup_entry( class ZHAButton(ZhaEntity, ButtonEntity): """Defines a ZHA button.""" - _command_name: str = None + _command_name: str def __init__( self, @@ -118,7 +118,7 @@ class ZHAIdentifyButton(ZHAButton): class ZHAAttributeButton(ZhaEntity, ButtonEntity): """Defines a ZHA button, which stes value to an attribute.""" - _attribute_name: str = None + _attribute_name: str _attribute_value: Any = None def __init__( diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 291e8413e16..d8b2f0db3af 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -73,16 +73,16 @@ MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.CLIMATE) RUNNING_MODE = {0x00: HVACMode.OFF, 0x03: HVACMode.COOL, 0x04: HVACMode.HEAT} SEQ_OF_OPERATION = { - 0x00: (HVACMode.OFF, HVACMode.COOL), # cooling only - 0x01: (HVACMode.OFF, HVACMode.COOL), # cooling with reheat - 0x02: (HVACMode.OFF, HVACMode.HEAT), # heating only - 0x03: (HVACMode.OFF, HVACMode.HEAT), # heating with reheat + 0x00: [HVACMode.OFF, HVACMode.COOL], # cooling only + 0x01: [HVACMode.OFF, HVACMode.COOL], # cooling with reheat + 0x02: [HVACMode.OFF, HVACMode.HEAT], # heating only + 0x03: [HVACMode.OFF, HVACMode.HEAT], # heating with reheat # cooling and heating 4-pipes - 0x04: (HVACMode.OFF, HVACMode.HEAT_COOL, HVACMode.COOL, HVACMode.HEAT), + 0x04: [HVACMode.OFF, HVACMode.HEAT_COOL, HVACMode.COOL, HVACMode.HEAT], # cooling and heating 4-pipes - 0x05: (HVACMode.OFF, HVACMode.HEAT_COOL, HVACMode.COOL, HVACMode.HEAT), - 0x06: (HVACMode.COOL, HVACMode.HEAT, HVACMode.OFF), # centralite specific - 0x07: (HVACMode.HEAT_COOL, HVACMode.OFF), # centralite specific + 0x05: [HVACMode.OFF, HVACMode.HEAT_COOL, HVACMode.COOL, HVACMode.HEAT], + 0x06: [HVACMode.COOL, HVACMode.HEAT, HVACMode.OFF], # centralite specific + 0x07: [HVACMode.HEAT_COOL, HVACMode.OFF], # centralite specific } HVAC_MODE_2_SYSTEM = { @@ -268,7 +268,7 @@ class Thermostat(ZhaEntity, ClimateEntity): return PRECISION_TENTHS @property - def preset_mode(self) -> str | None: + def preset_mode(self) -> str: """Return current preset mode.""" return self._preset @@ -389,7 +389,7 @@ class Thermostat(ZhaEntity, ClimateEntity): async def async_set_fan_mode(self, fan_mode: str) -> None: """Set fan mode.""" - if fan_mode not in self.fan_modes: + if not self.fan_modes or fan_mode not in self.fan_modes: self.warning("Unsupported '%s' fan mode", fan_mode) return @@ -415,8 +415,8 @@ class Thermostat(ZhaEntity, ClimateEntity): async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if preset_mode not in self.preset_modes: - self.debug("preset mode '%s' is not supported", preset_mode) + if not self.preset_modes or preset_mode not in self.preset_modes: + self.debug("Preset mode '%s' is not supported", preset_mode) return if self.preset_mode not in ( @@ -505,7 +505,7 @@ class SinopeTechnologiesThermostat(Thermostat): self._manufacturer_ch = self.cluster_channels["sinope_manufacturer_specific"] @property - def _rm_rs_action(self) -> str | None: + def _rm_rs_action(self) -> HVACAction: """Return the current HVAC action based on running mode and running state.""" running_mode = self._thrm.running_mode diff --git a/mypy.ini b/mypy.ini index b01acbe311f..1cc40dd3daf 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2985,15 +2985,6 @@ ignore_errors = true [mypy-homeassistant.components.xiaomi_miio.switch] ignore_errors = true -[mypy-homeassistant.components.zha.api] -ignore_errors = true - -[mypy-homeassistant.components.zha.button] -ignore_errors = true - -[mypy-homeassistant.components.zha.climate] -ignore_errors = true - [mypy-homeassistant.components.zha.core.channels.base] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index e5bb404b715..5a53323deb7 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -144,9 +144,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.xiaomi_miio.light", "homeassistant.components.xiaomi_miio.sensor", "homeassistant.components.xiaomi_miio.switch", - "homeassistant.components.zha.api", - "homeassistant.components.zha.button", - "homeassistant.components.zha.climate", "homeassistant.components.zha.core.channels.base", "homeassistant.components.zha.core.channels.homeautomation", "homeassistant.components.zha.core.channels.hvac", From 243905ae3e10f21c9bc8cbde565532e1b7b9112f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 22 Jun 2022 03:04:24 +0200 Subject: [PATCH 1654/3516] Fix cover, light, select, sensor, switch type hints in zha (#73770) * Fix zha sensor type hints * Fix zha entity type hints * Fix switch type hints * Fix light type hints * Fix cover type hints * Fix select type hints --- homeassistant/components/zha/cover.py | 4 ++-- homeassistant/components/zha/entity.py | 8 +++++--- homeassistant/components/zha/light.py | 6 ++++-- homeassistant/components/zha/select.py | 20 ++++++++++---------- homeassistant/components/zha/sensor.py | 5 +++-- homeassistant/components/zha/switch.py | 3 +-- mypy.ini | 18 ------------------ script/hassfest/mypy_config.py | 6 ------ 8 files changed, 25 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 2a61c5b4bc8..413e7e9ae09 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -203,8 +203,8 @@ class Shade(ZhaEntity, CoverEntity): super().__init__(unique_id, zha_device, channels, **kwargs) self._on_off_channel = self.cluster_channels[CHANNEL_ON_OFF] self._level_channel = self.cluster_channels[CHANNEL_LEVEL] - self._position = None - self._is_open = None + self._position: int | None = None + self._is_open: bool | None = None @property def current_cover_position(self): diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index fb1a35ff72b..f70948eb04a 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -128,9 +128,9 @@ class BaseZhaEntity(LogMixin, entity.Entity): @callback def async_accept_signal( self, - channel: ZigbeeChannel, + channel: ZigbeeChannel | None, signal: str, - func: Callable[[], Any], + func: Callable[..., Any], signal_override=False, ): """Accept a signal from a channel.""" @@ -138,6 +138,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): if signal_override: unsub = async_dispatcher_connect(self.hass, signal, func) else: + assert channel unsub = async_dispatcher_connect( self.hass, f"{channel.unique_id}_{signal}", func ) @@ -305,7 +306,7 @@ class ZhaGroupEntity(BaseZhaEntity): if self._change_listener_debouncer is None: self._change_listener_debouncer = Debouncer( self.hass, - self, + _LOGGER, cooldown=UPDATE_GROUP_FROM_CHILD_DELAY, immediate=False, function=functools.partial(self.async_update_ha_state, True), @@ -325,6 +326,7 @@ class ZhaGroupEntity(BaseZhaEntity): def async_state_changed_listener(self, event: Event): """Handle child updates.""" # Delay to ensure that we get updates from all members before updating the group + assert self._change_listener_debouncer self.hass.create_task(self._change_listener_debouncer.async_call()) async def async_will_remove_from_hass(self) -> None: diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 520916d469b..7c9e8d738a4 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -340,13 +340,13 @@ class BaseLight(LogMixin, light.LightEntity): class Light(BaseLight, ZhaEntity): """Representation of a ZHA or ZLL light.""" - _attr_supported_color_modes: set(ColorMode) + _attr_supported_color_modes: set[ColorMode] _REFRESH_INTERVAL = (45, 75) def __init__(self, unique_id, zha_device: ZHADevice, channels, **kwargs): """Initialize the ZHA light.""" super().__init__(unique_id, zha_device, channels, **kwargs) - self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) + self._on_off_channel = self.cluster_channels[CHANNEL_ON_OFF] self._state = bool(self._on_off_channel.on_off) self._level_channel = self.cluster_channels.get(CHANNEL_LEVEL) self._color_channel = self.cluster_channels.get(CHANNEL_COLOR) @@ -391,6 +391,7 @@ class Light(BaseLight, ZhaEntity): if len(self._attr_supported_color_modes) == 1: self._color_mode = next(iter(self._attr_supported_color_modes)) else: # Light supports color_temp + hs, determine which mode the light is in + assert self._color_channel if self._color_channel.color_mode == Color.ColorMode.Color_temperature: self._color_mode = ColorMode.COLOR_TEMP else: @@ -440,6 +441,7 @@ class Light(BaseLight, ZhaEntity): async def async_will_remove_from_hass(self) -> None: """Disconnect entity object when removed.""" + assert self._cancel_refresh_handle self._cancel_refresh_handle() await super().async_will_remove_from_hass() diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 231120ba806..83bbcdca580 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -64,7 +64,7 @@ class ZHAEnumSelectEntity(ZhaEntity, SelectEntity): """Representation of a ZHA select entity.""" _attr_entity_category = EntityCategory.CONFIG - _enum: Enum = None + _enum: type[Enum] def __init__( self, @@ -87,7 +87,7 @@ class ZHAEnumSelectEntity(ZhaEntity, SelectEntity): return None return option.name.replace("_", " ") - async def async_select_option(self, option: str | int) -> None: + async def async_select_option(self, option: str) -> None: """Change the selected option.""" self._channel.data_cache[self._attr_name] = self._enum[option.replace(" ", "_")] self.async_write_ha_state() @@ -116,7 +116,7 @@ class ZHADefaultToneSelectEntity( ): """Representation of a ZHA default siren tone select entity.""" - _enum: Enum = IasWd.Warning.WarningMode + _enum = IasWd.Warning.WarningMode @CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_IAS_WD) @@ -125,7 +125,7 @@ class ZHADefaultSirenLevelSelectEntity( ): """Representation of a ZHA default siren level select entity.""" - _enum: Enum = IasWd.Warning.SirenLevel + _enum = IasWd.Warning.SirenLevel @CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_IAS_WD) @@ -134,14 +134,14 @@ class ZHADefaultStrobeLevelSelectEntity( ): """Representation of a ZHA default siren strobe level select entity.""" - _enum: Enum = IasWd.StrobeLevel + _enum = IasWd.StrobeLevel @CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_IAS_WD) class ZHADefaultStrobeSelectEntity(ZHANonZCLSelectEntity, id_suffix=Strobe.__name__): """Representation of a ZHA default siren strobe select entity.""" - _enum: Enum = Strobe + _enum = Strobe class ZCLEnumSelectEntity(ZhaEntity, SelectEntity): @@ -149,7 +149,7 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity): _select_attr: str _attr_entity_category = EntityCategory.CONFIG - _enum: Enum + _enum: type[Enum] @classmethod def create_entity( @@ -198,7 +198,7 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity): option = self._enum(option) return option.name.replace("_", " ") - async def async_select_option(self, option: str | int) -> None: + async def async_select_option(self, option: str) -> None: """Change the selected option.""" await self._channel.cluster.write_attributes( {self._select_attr: self._enum[option.replace(" ", "_")]} @@ -213,7 +213,7 @@ class ZHAStartupOnOffSelectEntity( """Representation of a ZHA startup onoff select entity.""" _select_attr = "start_up_on_off" - _enum: Enum = OnOff.StartUpOnOff + _enum = OnOff.StartUpOnOff class AqaraMotionSensitivities(types.enum8): @@ -229,4 +229,4 @@ class AqaraMotionSensitivity(ZCLEnumSelectEntity, id_suffix="motion_sensitivity" """Representation of a ZHA on off transition time configuration entity.""" _select_attr = "motion_sensitivity" - _enum: Enum = AqaraMotionSensitivities + _enum = AqaraMotionSensitivities diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index e579967345c..e66f1569b81 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -118,7 +118,7 @@ class Sensor(ZhaEntity, SensorEntity): SENSOR_ATTR: int | str | None = None _decimals: int = 1 _divisor: int = 1 - _multiplier: int = 1 + _multiplier: int | float = 1 _unit: str | None = None def __init__( @@ -455,7 +455,7 @@ class SmartEnergyMetering(Sensor): return self._channel.demand_formatter(value) @property - def native_unit_of_measurement(self) -> str: + def native_unit_of_measurement(self) -> str | None: """Return Unit of measurement.""" return self.unit_of_measure_map.get(self._channel.unit_of_measurement) @@ -760,6 +760,7 @@ class RSSISensor(Sensor, id_suffix="rssi"): _attr_device_class: SensorDeviceClass = SensorDeviceClass.SIGNAL_STRENGTH _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_registry_enabled_default = False + unique_id_suffix: str @classmethod def create_entity( diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 1926b08fc60..fe7526586f9 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -29,7 +29,6 @@ from .entity import ZhaEntity, ZhaGroupEntity if TYPE_CHECKING: from .core.channels.base import ZigbeeChannel - from .core.channels.general import OnOffChannel from .core.device import ZHADevice STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SWITCH) @@ -72,7 +71,7 @@ class Switch(ZhaEntity, SwitchEntity): ) -> None: """Initialize the ZHA switch.""" super().__init__(unique_id, zha_device, channels, **kwargs) - self._on_off_channel: OnOffChannel = self.cluster_channels.get(CHANNEL_ON_OFF) + self._on_off_channel = self.cluster_channels[CHANNEL_ON_OFF] @property def is_on(self) -> bool: diff --git a/mypy.ini b/mypy.ini index 1cc40dd3daf..26314c5bcad 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3020,21 +3020,3 @@ ignore_errors = true [mypy-homeassistant.components.zha.core.store] ignore_errors = true - -[mypy-homeassistant.components.zha.cover] -ignore_errors = true - -[mypy-homeassistant.components.zha.entity] -ignore_errors = true - -[mypy-homeassistant.components.zha.light] -ignore_errors = true - -[mypy-homeassistant.components.zha.select] -ignore_errors = true - -[mypy-homeassistant.components.zha.sensor] -ignore_errors = true - -[mypy-homeassistant.components.zha.switch] -ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 5a53323deb7..6a4ff9d8cdf 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -156,12 +156,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.zha.core.helpers", "homeassistant.components.zha.core.registries", "homeassistant.components.zha.core.store", - "homeassistant.components.zha.cover", - "homeassistant.components.zha.entity", - "homeassistant.components.zha.light", - "homeassistant.components.zha.select", - "homeassistant.components.zha.sensor", - "homeassistant.components.zha.switch", ] # Component modules which should set no_implicit_reexport = true. From 1e0a3246f4892759d2e20dffac8a48554662a0fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 21 Jun 2022 22:45:16 -0500 Subject: [PATCH 1655/3516] Revert "Fix auth_sign_path with query params (#73240)" (#73808) --- homeassistant/components/http/auth.py | 17 ++---- tests/components/http/test_auth.py | 78 --------------------------- 2 files changed, 3 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index d89a36cdb86..dab6abede4c 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -7,11 +7,11 @@ from ipaddress import ip_address import logging import secrets from typing import Final +from urllib.parse import unquote from aiohttp import hdrs from aiohttp.web import Application, Request, StreamResponse, middleware import jwt -from yarl import URL from homeassistant.auth.const import GROUP_ID_READ_ONLY from homeassistant.auth.models import User @@ -57,24 +57,18 @@ def async_sign_path( else: refresh_token_id = hass.data[STORAGE_KEY] - url = URL(path) now = dt_util.utcnow() - params = dict(sorted(url.query.items())) encoded = jwt.encode( { "iss": refresh_token_id, - "path": url.path, - "params": params, + "path": unquote(path), "iat": now, "exp": now + expiration, }, secret, algorithm="HS256", ) - - params[SIGN_QUERY_PARAM] = encoded - url = url.with_query(params) - return f"{url.path}?{url.query_string}" + return f"{path}?{SIGN_QUERY_PARAM}={encoded}" @callback @@ -182,11 +176,6 @@ async def async_setup_auth(hass: HomeAssistant, app: Application) -> None: if claims["path"] != request.path: return False - params = dict(sorted(request.query.items())) - del params[SIGN_QUERY_PARAM] - if claims["params"] != params: - return False - refresh_token = await hass.auth.async_get_refresh_token(claims["iss"]) if refresh_token is None: diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index d4995383297..4a2e1e8aed3 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -17,7 +17,6 @@ from homeassistant.components import websocket_api from homeassistant.components.http.auth import ( CONTENT_USER_NAME, DATA_SIGN_SECRET, - SIGN_QUERY_PARAM, STORAGE_KEY, async_setup_auth, async_sign_path, @@ -295,83 +294,6 @@ async def test_auth_access_signed_path_with_refresh_token( assert req.status == HTTPStatus.UNAUTHORIZED -async def test_auth_access_signed_path_with_query_param( - hass, app, aiohttp_client, hass_access_token -): - """Test access with signed url and query params.""" - app.router.add_post("/", mock_handler) - app.router.add_get("/another_path", mock_handler) - await async_setup_auth(hass, app) - client = await aiohttp_client(app) - - refresh_token = await hass.auth.async_validate_access_token(hass_access_token) - - signed_path = async_sign_path( - hass, "/?test=test", timedelta(seconds=5), refresh_token_id=refresh_token.id - ) - - req = await client.get(signed_path) - assert req.status == HTTPStatus.OK - data = await req.json() - assert data["user_id"] == refresh_token.user.id - - -async def test_auth_access_signed_path_with_query_param_order( - hass, app, aiohttp_client, hass_access_token -): - """Test access with signed url and query params different order.""" - app.router.add_post("/", mock_handler) - app.router.add_get("/another_path", mock_handler) - await async_setup_auth(hass, app) - client = await aiohttp_client(app) - - refresh_token = await hass.auth.async_validate_access_token(hass_access_token) - - signed_path = async_sign_path( - hass, - "/?test=test&foo=bar", - timedelta(seconds=5), - refresh_token_id=refresh_token.id, - ) - url = yarl.URL(signed_path) - signed_path = f"{url.path}?{SIGN_QUERY_PARAM}={url.query.get(SIGN_QUERY_PARAM)}&foo=bar&test=test" - - req = await client.get(signed_path) - assert req.status == HTTPStatus.OK - data = await req.json() - assert data["user_id"] == refresh_token.user.id - - -@pytest.mark.parametrize( - "base_url,test_url", - [ - ("/?test=test", "/?test=test&foo=bar"), - ("/", "/?test=test"), - ("/?test=test&foo=bar", "/?test=test&foo=baz"), - ("/?test=test&foo=bar", "/?test=test"), - ], -) -async def test_auth_access_signed_path_with_query_param_tamper( - hass, app, aiohttp_client, hass_access_token, base_url: str, test_url: str -): - """Test access with signed url and query params that have been tampered with.""" - app.router.add_post("/", mock_handler) - app.router.add_get("/another_path", mock_handler) - await async_setup_auth(hass, app) - client = await aiohttp_client(app) - - refresh_token = await hass.auth.async_validate_access_token(hass_access_token) - - signed_path = async_sign_path( - hass, base_url, timedelta(seconds=5), refresh_token_id=refresh_token.id - ) - url = yarl.URL(signed_path) - token = url.query.get(SIGN_QUERY_PARAM) - - req = await client.get(f"{test_url}&{SIGN_QUERY_PARAM}={token}") - assert req.status == HTTPStatus.UNAUTHORIZED - - async def test_auth_access_signed_path_via_websocket( hass, app, hass_ws_client, hass_read_only_access_token ): From 07a46dee3946e325407d4ae66212f6899733ab95 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 22 Jun 2022 02:08:31 -0500 Subject: [PATCH 1656/3516] Additional surround controls for Sonos (#73805) --- homeassistant/components/sonos/number.py | 2 ++ homeassistant/components/sonos/speaker.py | 13 ++++++++++++- homeassistant/components/sonos/switch.py | 4 ++++ tests/components/sonos/conftest.py | 3 +++ tests/components/sonos/test_number.py | 10 ++++++++++ tests/components/sonos/test_switch.py | 8 ++++++++ 6 files changed, 39 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py index 202df1cb6f1..3b034423471 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -20,6 +20,8 @@ LEVEL_TYPES = { "bass": (-10, 10), "treble": (-10, 10), "sub_gain": (-15, 15), + "surround_level": (-15, 15), + "music_surround_level": (-15, 15), } _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index f9decf1c27e..93d0afbcf9c 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -146,6 +146,9 @@ class SonosSpeaker: self.sub_enabled: bool | None = None self.sub_gain: int | None = None self.surround_enabled: bool | None = None + self.surround_mode: bool | None = None + self.surround_level: int | None = None + self.music_surround_level: int | None = None # Misc features self.buttons_enabled: bool | None = None @@ -515,11 +518,19 @@ class SonosSpeaker: "night_mode", "sub_enabled", "surround_enabled", + "surround_mode", ): if bool_var in variables: setattr(self, bool_var, variables[bool_var] == "1") - for int_var in ("audio_delay", "bass", "treble", "sub_gain"): + for int_var in ( + "audio_delay", + "bass", + "treble", + "sub_gain", + "surround_level", + "music_surround_level", + ): if int_var in variables: setattr(self, int_var, variables[int_var]) diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 29abb097df7..53911d85d3e 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -39,6 +39,7 @@ ATTR_INCLUDE_LINKED_ZONES = "include_linked_zones" ATTR_CROSSFADE = "cross_fade" ATTR_LOUDNESS = "loudness" +ATTR_MUSIC_PLAYBACK_FULL_VOLUME = "surround_mode" ATTR_NIGHT_SOUND = "night_mode" ATTR_SPEECH_ENHANCEMENT = "dialog_level" ATTR_STATUS_LIGHT = "status_light" @@ -50,6 +51,7 @@ ALL_FEATURES = ( ATTR_TOUCH_CONTROLS, ATTR_CROSSFADE, ATTR_LOUDNESS, + ATTR_MUSIC_PLAYBACK_FULL_VOLUME, ATTR_NIGHT_SOUND, ATTR_SPEECH_ENHANCEMENT, ATTR_SUB_ENABLED, @@ -67,6 +69,7 @@ POLL_REQUIRED = ( FRIENDLY_NAMES = { ATTR_CROSSFADE: "Crossfade", ATTR_LOUDNESS: "Loudness", + ATTR_MUSIC_PLAYBACK_FULL_VOLUME: "Surround Music Full Volume", ATTR_NIGHT_SOUND: "Night Sound", ATTR_SPEECH_ENHANCEMENT: "Speech Enhancement", ATTR_STATUS_LIGHT: "Status Light", @@ -77,6 +80,7 @@ FRIENDLY_NAMES = { FEATURE_ICONS = { ATTR_LOUDNESS: "mdi:bullhorn-variant", + ATTR_MUSIC_PLAYBACK_FULL_VOLUME: "mdi:music-note-plus", ATTR_NIGHT_SOUND: "mdi:chat-sleep", ATTR_SPEECH_ENHANCEMENT: "mdi:ear-hearing", ATTR_CROSSFADE: "mdi:swap-horizontal", diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index d493c9d50c9..f776fb62d58 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -117,6 +117,9 @@ def soco_fixture( mock_soco.sub_enabled = False mock_soco.sub_gain = 5 mock_soco.surround_enabled = True + mock_soco.surround_mode = True + mock_soco.surround_level = 3 + mock_soco.music_surround_level = 4 mock_soco.soundbar_audio_input_format = "Dolby 5.1" mock_soco.get_battery_info.return_value = battery_info mock_soco.all_zones = {mock_soco} diff --git a/tests/components/sonos/test_number.py b/tests/components/sonos/test_number.py index 5829a7a6724..83dcdf78ff8 100644 --- a/tests/components/sonos/test_number.py +++ b/tests/components/sonos/test_number.py @@ -22,6 +22,16 @@ async def test_number_entities(hass, async_autosetup_sonos, soco): audio_delay_state = hass.states.get(audio_delay_number.entity_id) assert audio_delay_state.state == "2" + surround_level_number = entity_registry.entities["number.zone_a_surround_level"] + surround_level_state = hass.states.get(surround_level_number.entity_id) + assert surround_level_state.state == "3" + + music_surround_level_number = entity_registry.entities[ + "number.zone_a_music_surround_level" + ] + music_surround_level_state = hass.states.get(music_surround_level_number.entity_id) + assert music_surround_level_state.state == "4" + with patch("soco.SoCo.audio_delay") as mock_audio_delay: await hass.services.async_call( NUMBER_DOMAIN, diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index f224a1e187e..2b794657565 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -52,6 +52,14 @@ async def test_switch_attributes(hass, async_autosetup_sonos, soco): assert alarm_state.attributes.get(ATTR_PLAY_MODE) == "SHUFFLE_NOREPEAT" assert not alarm_state.attributes.get(ATTR_INCLUDE_LINKED_ZONES) + surround_music_full_volume = entity_registry.entities[ + "switch.zone_a_surround_music_full_volume" + ] + surround_music_full_volume_state = hass.states.get( + surround_music_full_volume.entity_id + ) + assert surround_music_full_volume_state.state == STATE_ON + night_sound = entity_registry.entities["switch.zone_a_night_sound"] night_sound_state = hass.states.get(night_sound.entity_id) assert night_sound_state.state == STATE_ON From 39a00ffe09dbaed86943ca540f62aaf29917abb2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Jun 2022 09:49:54 +0200 Subject: [PATCH 1657/3516] Automatically onboard Cast (#73813) --- homeassistant/components/cast/config_flow.py | 4 ++-- tests/components/cast/test_config_flow.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py index fc657fd2422..1c983d6f67a 100644 --- a/homeassistant/components/cast/config_flow.py +++ b/homeassistant/components/cast/config_flow.py @@ -6,7 +6,7 @@ from typing import Any import voluptuous as vol from homeassistant import config_entries -from homeassistant.components import zeroconf +from homeassistant.components import onboarding, zeroconf from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv @@ -102,7 +102,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data = self._get_data() - if user_input is not None: + if user_input is not None or not onboarding.async_is_onboarded(self.hass): return self.async_create_entry(title="Google Cast", data=data) return self.async_show_form(step_id="confirm") diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index 1ad89c7a8e5..d7aa0fdeda9 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -116,6 +116,26 @@ async def test_zeroconf_setup(hass): } +async def test_zeroconf_setup_onboarding(hass): + """Test we automatically finish a config flow through zeroconf during onboarding.""" + with patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ): + result = await hass.config_entries.flow.async_init( + "cast", context={"source": config_entries.SOURCE_ZEROCONF} + ) + + users = await hass.auth.async_get_users() + assert len(users) == 1 + assert result["type"] == "create_entry" + assert result["result"].data == { + "ignore_cec": [], + "known_hosts": [], + "uuid": [], + "user_id": users[0].id, # Home Assistant cast user + } + + def get_suggested(schema, key): """Get suggested value for key in voluptuous schema.""" for k in schema.keys(): From 998e63df61c184d6fc7336e134e372531ec9a826 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Jun 2022 09:54:35 +0200 Subject: [PATCH 1658/3516] Fix Plugwise migration error (#73812) --- homeassistant/components/plugwise/gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index 7eb2b1371d5..afa7451021e 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -113,7 +113,7 @@ def migrate_sensor_entities( # Migrating opentherm_outdoor_temperature to opentherm_outdoor_air_temperature sensor for device_id, device in coordinator.data.devices.items(): - if device["dev_class"] != "heater_central": + if device.get("dev_class") != "heater_central": continue old_unique_id = f"{device_id}-outdoor_temperature" From 504f4a7acf6d3f5f7ba887b72671883a35a1feb8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Jun 2022 09:55:36 +0200 Subject: [PATCH 1659/3516] Update Fibaro config entry on duplicate entry (#73814) --- homeassistant/components/fibaro/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fibaro/config_flow.py b/homeassistant/components/fibaro/config_flow.py index f528fd8a184..b0ea05e49e1 100644 --- a/homeassistant/components/fibaro/config_flow.py +++ b/homeassistant/components/fibaro/config_flow.py @@ -69,7 +69,7 @@ class FibaroConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "invalid_auth" else: await self.async_set_unique_id(info["serial_number"]) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured(updates=user_input) return self.async_create_entry(title=info["name"], data=user_input) return self.async_show_form( From 4bfdc6104566cb12237b19d172e52d3a3644db11 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Jun 2022 03:02:02 -0500 Subject: [PATCH 1660/3516] Fix rachio webhook not being unregistered on unload (#73795) --- homeassistant/components/rachio/__init__.py | 17 ++++--- .../components/rachio/binary_sensor.py | 11 +++-- homeassistant/components/rachio/const.py | 10 +++++ homeassistant/components/rachio/device.py | 45 +++++++++++-------- homeassistant/components/rachio/entity.py | 16 +++---- homeassistant/components/rachio/switch.py | 10 +++-- homeassistant/components/rachio/webhooks.py | 35 +++++++++++---- 7 files changed, 89 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index e75d7117d73..e0ac98b7546 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -17,6 +17,7 @@ from .device import RachioPerson from .webhooks import ( async_get_or_create_registered_webhook_id_and_url, async_register_webhook, + async_unregister_webhook, ) _LOGGER = logging.getLogger(__name__) @@ -28,8 +29,8 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + async_unregister_webhook(hass, entry) hass.data[DOMAIN].pop(entry.entry_id) return unload_ok @@ -59,10 +60,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Get the URL of this server rachio.webhook_auth = secrets.token_hex() try: - ( - webhook_id, - webhook_url, - ) = await async_get_or_create_registered_webhook_id_and_url(hass, entry) + webhook_url = await async_get_or_create_registered_webhook_id_and_url( + hass, entry + ) except cloud.CloudNotConnected as exc: # User has an active cloud subscription, but the connection to the cloud is down raise ConfigEntryNotReady from exc @@ -92,9 +92,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Enable platform - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = person - async_register_webhook(hass, webhook_id, entry.entry_id) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = person + async_register_webhook(hass, entry) hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index 1bfc3cc03ee..2fe99fb442e 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -21,6 +22,7 @@ from .const import ( SIGNAL_RACHIO_RAIN_SENSOR_UPDATE, STATUS_ONLINE, ) +from .device import RachioPerson from .entity import RachioDevice from .webhooks import ( SUBTYPE_COLD_REBOOT, @@ -41,12 +43,13 @@ async def async_setup_entry( """Set up the Rachio binary sensors.""" entities = await hass.async_add_executor_job(_create_entities, hass, config_entry) async_add_entities(entities) - _LOGGER.info("%d Rachio binary sensor(s) added", len(entities)) + _LOGGER.debug("%d Rachio binary sensor(s) added", len(entities)) -def _create_entities(hass, config_entry): - entities = [] - for controller in hass.data[DOMAIN_RACHIO][config_entry.entry_id].controllers: +def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Entity]: + entities: list[Entity] = [] + person: RachioPerson = hass.data[DOMAIN_RACHIO][config_entry.entry_id] + for controller in person.controllers: entities.append(RachioControllerOnlineBinarySensor(controller)) entities.append(RachioRainSensor(controller)) return entities diff --git a/homeassistant/components/rachio/const.py b/homeassistant/components/rachio/const.py index 9dbf14e3907..92a57505a7c 100644 --- a/homeassistant/components/rachio/const.py +++ b/homeassistant/components/rachio/const.py @@ -67,3 +67,13 @@ SIGNAL_RACHIO_SCHEDULE_UPDATE = f"{SIGNAL_RACHIO_UPDATE}_schedule" CONF_WEBHOOK_ID = "webhook_id" CONF_CLOUDHOOK_URL = "cloudhook_url" + +# Webhook callbacks +LISTEN_EVENT_TYPES = [ + "DEVICE_STATUS_EVENT", + "ZONE_STATUS_EVENT", + "RAIN_DELAY_EVENT", + "RAIN_SENSOR_DETECTION_EVENT", + "SCHEDULE_STATUS_EVENT", +] +WEBHOOK_CONST_ID = "homeassistant.rachio:" diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index 911049883d9..5053fa01495 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -3,11 +3,14 @@ from __future__ import annotations from http import HTTPStatus import logging +from typing import Any +from rachiopy import Rachio import voluptuous as vol +from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv @@ -26,12 +29,13 @@ from .const import ( KEY_STATUS, KEY_USERNAME, KEY_ZONES, + LISTEN_EVENT_TYPES, MODEL_GENERATION_1, SERVICE_PAUSE_WATERING, SERVICE_RESUME_WATERING, SERVICE_STOP_WATERING, + WEBHOOK_CONST_ID, ) -from .webhooks import LISTEN_EVENT_TYPES, WEBHOOK_CONST_ID _LOGGER = logging.getLogger(__name__) @@ -54,16 +58,16 @@ STOP_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_DEVICES): cv.string}) class RachioPerson: """Represent a Rachio user.""" - def __init__(self, rachio, config_entry): + def __init__(self, rachio: Rachio, config_entry: ConfigEntry) -> None: """Create an object from the provided API instance.""" # Use API token to get user ID self.rachio = rachio self.config_entry = config_entry self.username = None - self._id = None - self._controllers = [] + self._id: str | None = None + self._controllers: list[RachioIro] = [] - async def async_setup(self, hass): + async def async_setup(self, hass: HomeAssistant) -> None: """Create rachio devices and services.""" await hass.async_add_executor_job(self._setup, hass) can_pause = False @@ -121,7 +125,7 @@ class RachioPerson: schema=RESUME_SERVICE_SCHEMA, ) - def _setup(self, hass): + def _setup(self, hass: HomeAssistant) -> None: """Rachio device setup.""" rachio = self.rachio @@ -139,7 +143,7 @@ class RachioPerson: if int(data[0][KEY_STATUS]) != HTTPStatus.OK: raise ConfigEntryNotReady(f"API Error: {data}") self.username = data[1][KEY_USERNAME] - devices = data[1][KEY_DEVICES] + devices: list[dict[str, Any]] = data[1][KEY_DEVICES] for controller in devices: webhooks = rachio.notification.get_device_webhook(controller[KEY_ID])[1] # The API does not provide a way to tell if a controller is shared @@ -169,12 +173,12 @@ class RachioPerson: _LOGGER.info('Using Rachio API as user "%s"', self.username) @property - def user_id(self) -> str: + def user_id(self) -> str | None: """Get the user ID as defined by the Rachio API.""" return self._id @property - def controllers(self) -> list: + def controllers(self) -> list[RachioIro]: """Get a list of controllers managed by this account.""" return self._controllers @@ -186,7 +190,13 @@ class RachioPerson: class RachioIro: """Represent a Rachio Iro.""" - def __init__(self, hass, rachio, data, webhooks): + def __init__( + self, + hass: HomeAssistant, + rachio: Rachio, + data: dict[str, Any], + webhooks: list[dict[str, Any]], + ) -> None: """Initialize a Rachio device.""" self.hass = hass self.rachio = rachio @@ -199,10 +209,10 @@ class RachioIro: self._schedules = data[KEY_SCHEDULES] self._flex_schedules = data[KEY_FLEX_SCHEDULES] self._init_data = data - self._webhooks = webhooks + self._webhooks: list[dict[str, Any]] = webhooks _LOGGER.debug('%s has ID "%s"', self, self.controller_id) - def setup(self): + def setup(self) -> None: """Rachio Iro setup for webhooks.""" # Listen for all updates self._init_webhooks() @@ -226,7 +236,7 @@ class RachioIro: or webhook[KEY_ID] == current_webhook_id ): self.rachio.notification.delete(webhook[KEY_ID]) - self._webhooks = None + self._webhooks = [] _deinit_webhooks(None) @@ -306,9 +316,6 @@ class RachioIro: _LOGGER.debug("Resuming watering on %s", self) -def is_invalid_auth_code(http_status_code): +def is_invalid_auth_code(http_status_code: int) -> bool: """HTTP status codes that mean invalid auth.""" - if http_status_code in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN): - return True - - return False + return http_status_code in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN) diff --git a/homeassistant/components/rachio/entity.py b/homeassistant/components/rachio/entity.py index a95b6ffb557..1bb971e3e01 100644 --- a/homeassistant/components/rachio/entity.py +++ b/homeassistant/components/rachio/entity.py @@ -4,25 +4,19 @@ from homeassistant.helpers import device_registry from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DEFAULT_NAME, DOMAIN +from .device import RachioIro class RachioDevice(Entity): """Base class for rachio devices.""" - def __init__(self, controller): + _attr_should_poll = False + + def __init__(self, controller: RachioIro) -> None: """Initialize a Rachio device.""" super().__init__() self._controller = controller - - @property - def should_poll(self) -> bool: - """Declare that this entity pushes its state to HA.""" - return False - - @property - def device_info(self) -> DeviceInfo: - """Return the device_info of the device.""" - return DeviceInfo( + self._attr_device_info = DeviceInfo( identifiers={ ( DOMAIN, diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 55427741bf0..227e8beaec3 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -13,6 +13,7 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import as_timestamp, now, parse_datetime, utc_from_timestamp @@ -52,6 +53,7 @@ from .const import ( SLOPE_SLIGHT, SLOPE_STEEP, ) +from .device import RachioPerson from .entity import RachioDevice from .webhooks import ( SUBTYPE_RAIN_DELAY_OFF, @@ -106,7 +108,7 @@ async def async_setup_entry( has_flex_sched = True async_add_entities(entities) - _LOGGER.info("%d Rachio switch(es) added", len(entities)) + _LOGGER.debug("%d Rachio switch(es) added", len(entities)) def start_multiple(service: ServiceCall) -> None: """Service to start multiple zones in sequence.""" @@ -154,9 +156,9 @@ async def async_setup_entry( ) -def _create_entities(hass, config_entry): - entities = [] - person = hass.data[DOMAIN_RACHIO][config_entry.entry_id] +def _create_entities(hass: HomeAssistant, config_entry: ConfigEntry) -> list[Entity]: + entities: list[Entity] = [] + person: RachioPerson = hass.data[DOMAIN_RACHIO][config_entry.entry_id] # Fetch the schedule once at startup # in order to avoid every zone doing it for controller in person.controllers: diff --git a/homeassistant/components/rachio/webhooks.py b/homeassistant/components/rachio/webhooks.py index 6ad396b76a1..5c2fbe5965f 100644 --- a/homeassistant/components/rachio/webhooks.py +++ b/homeassistant/components/rachio/webhooks.py @@ -1,9 +1,12 @@ """Webhooks used by rachio.""" +from __future__ import annotations + from aiohttp import web from homeassistant.components import cloud, webhook +from homeassistant.config_entries import ConfigEntry from homeassistant.const import URL_API -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( @@ -18,6 +21,7 @@ from .const import ( SIGNAL_RACHIO_SCHEDULE_UPDATE, SIGNAL_RACHIO_ZONE_UPDATE, ) +from .device import RachioPerson # Device webhook values TYPE_CONTROLLER_STATUS = "DEVICE_STATUS" @@ -79,16 +83,22 @@ SIGNAL_MAP = { @callback -def async_register_webhook(hass, webhook_id, entry_id): +def async_register_webhook(hass: HomeAssistant, entry: ConfigEntry) -> None: """Register a webhook.""" + webhook_id: str = entry.data[CONF_WEBHOOK_ID] - async def _async_handle_rachio_webhook(hass, webhook_id, request): + async def _async_handle_rachio_webhook( + hass: HomeAssistant, webhook_id: str, request: web.Request + ) -> web.Response: """Handle webhook calls from the server.""" + person: RachioPerson = hass.data[DOMAIN][entry.entry_id] data = await request.json() try: - auth = data.get(KEY_EXTERNAL_ID, "").split(":")[1] - assert auth == hass.data[DOMAIN][entry_id].rachio.webhook_auth + assert ( + data.get(KEY_EXTERNAL_ID, "").split(":")[1] + == person.rachio.webhook_auth + ) except (AssertionError, IndexError): return web.Response(status=web.HTTPForbidden.status_code) @@ -103,8 +113,17 @@ def async_register_webhook(hass, webhook_id, entry_id): ) -async def async_get_or_create_registered_webhook_id_and_url(hass, entry): - """Generate webhook ID.""" +@callback +def async_unregister_webhook(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Unregister a webhook.""" + webhook_id: str = entry.data[CONF_WEBHOOK_ID] + webhook.async_unregister(hass, webhook_id) + + +async def async_get_or_create_registered_webhook_id_and_url( + hass: HomeAssistant, entry: ConfigEntry +) -> str: + """Generate webhook url.""" config = entry.data.copy() updated_config = False @@ -128,4 +147,4 @@ async def async_get_or_create_registered_webhook_id_and_url(hass, entry): if updated_config: hass.config_entries.async_update_entry(entry, data=config) - return webhook_id, webhook_url + return webhook_url From 08b69319ca85e42073e9fb9576db1d3ec896dc13 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Wed, 22 Jun 2022 04:04:11 -0400 Subject: [PATCH 1661/3516] Insteon bug fixes (#73791) --- homeassistant/components/insteon/api/properties.py | 9 ++++----- homeassistant/components/insteon/manifest.json | 4 ++-- homeassistant/components/insteon/utils.py | 5 ++--- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- tests/components/insteon/test_api_properties.py | 4 ++-- 6 files changed, 14 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/insteon/api/properties.py b/homeassistant/components/insteon/api/properties.py index 47def71c1ab..8e697a88459 100644 --- a/homeassistant/components/insteon/api/properties.py +++ b/homeassistant/components/insteon/api/properties.py @@ -99,8 +99,6 @@ def get_properties(device: Device, show_advanced=False): continue prop_schema = get_schema(prop, name, device.groups) - if name == "momentary_delay": - print(prop_schema) if prop_schema is None: continue schema[name] = prop_schema @@ -216,7 +214,7 @@ async def websocket_write_properties( result = await device.async_write_config() await devices.async_save(workdir=hass.config.config_dir) - if result != ResponseStatus.SUCCESS: + if result not in [ResponseStatus.SUCCESS, ResponseStatus.RUN_ON_WAKE]: connection.send_message( websocket_api.error_message( msg[ID], "write_failed", "properties not written to device" @@ -244,9 +242,10 @@ async def websocket_load_properties( notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return - result, _ = await device.async_read_config(read_aldb=False) + result = await device.async_read_config(read_aldb=False) await devices.async_save(workdir=hass.config.config_dir) - if result != ResponseStatus.SUCCESS: + + if result not in [ResponseStatus.SUCCESS, ResponseStatus.RUN_ON_WAKE]: connection.send_message( websocket_api.error_message( msg[ID], "load_failed", "properties not loaded from device" diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index c69f4f2cdf5..1be077a6b38 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -4,8 +4,8 @@ "documentation": "https://www.home-assistant.io/integrations/insteon", "dependencies": ["http", "websocket_api"], "requirements": [ - "pyinsteon==1.1.0", - "insteon-frontend-home-assistant==0.1.0" + "pyinsteon==1.1.1", + "insteon-frontend-home-assistant==0.1.1" ], "codeowners": ["@teharris1"], "dhcp": [ diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py index e8e34ee8e62..09375e7827a 100644 --- a/homeassistant/components/insteon/utils.py +++ b/homeassistant/components/insteon/utils.py @@ -196,9 +196,8 @@ def async_register_services(hass): for address in devices: device = devices[address] if device != devices.modem and device.cat != 0x03: - await device.aldb.async_load( - refresh=reload, callback=async_srv_save_devices - ) + await device.aldb.async_load(refresh=reload) + await async_srv_save_devices() async def async_srv_save_devices(): """Write the Insteon device configuration to file.""" diff --git a/requirements_all.txt b/requirements_all.txt index 859de73843f..9d3d59f3b05 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -888,7 +888,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.insteon -insteon-frontend-home-assistant==0.1.0 +insteon-frontend-home-assistant==0.1.1 # homeassistant.components.intellifire intellifire4py==1.0.2 @@ -1556,7 +1556,7 @@ pyialarm==1.9.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.0 +pyinsteon==1.1.1 # homeassistant.components.intesishome pyintesishome==1.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e576babe4b5..0f9877ff298 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -631,7 +631,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.insteon -insteon-frontend-home-assistant==0.1.0 +insteon-frontend-home-assistant==0.1.1 # homeassistant.components.intellifire intellifire4py==1.0.2 @@ -1047,7 +1047,7 @@ pyialarm==1.9.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.0 +pyinsteon==1.1.1 # homeassistant.components.ipma pyipma==2.0.5 diff --git a/tests/components/insteon/test_api_properties.py b/tests/components/insteon/test_api_properties.py index 7211402e343..9088b23f42a 100644 --- a/tests/components/insteon/test_api_properties.py +++ b/tests/components/insteon/test_api_properties.py @@ -401,7 +401,7 @@ async def test_load_properties(hass, hass_ws_client, kpl_properties_data): ) device = devices["33.33.33"] - device.async_read_config = AsyncMock(return_value=(1, 1)) + device.async_read_config = AsyncMock(return_value=1) with patch.object(insteon.api.properties, "devices", devices): await ws_client.send_json( {ID: 2, TYPE: "insteon/properties/load", DEVICE_ADDRESS: "33.33.33"} @@ -418,7 +418,7 @@ async def test_load_properties_failure(hass, hass_ws_client, kpl_properties_data ) device = devices["33.33.33"] - device.async_read_config = AsyncMock(return_value=(0, 0)) + device.async_read_config = AsyncMock(return_value=0) with patch.object(insteon.api.properties, "devices", devices): await ws_client.send_json( {ID: 2, TYPE: "insteon/properties/load", DEVICE_ADDRESS: "33.33.33"} From 95eb55dd667b456c3615ba19099f392fdaac1e8f Mon Sep 17 00:00:00 2001 From: Jonny Bergdahl Date: Wed, 22 Jun 2022 10:22:09 +0200 Subject: [PATCH 1662/3516] Fix thumbnail issues in Twitch integration (#72564) Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/twitch/sensor.py | 9 ++++++--- tests/components/twitch/test_twitch.py | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index 95006a4cab7..dcc2dc9bbf6 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -108,10 +108,8 @@ class TwitchSensor(SensorEntity): def update(self) -> None: """Update device state.""" - followers = self._client.get_users_follows(to_id=self.unique_id)["total"] channel = self._client.get_users(user_ids=[self.unique_id])["data"][0] - self._attr_extra_state_attributes = { ATTR_FOLLOWING: followers, ATTR_VIEWS: channel["view_count"], @@ -151,8 +149,13 @@ class TwitchSensor(SensorEntity): self._attr_extra_state_attributes[ATTR_GAME] = stream["game_name"] self._attr_extra_state_attributes[ATTR_TITLE] = stream["title"] self._attr_entity_picture = stream["thumbnail_url"] + if self._attr_entity_picture is not None: + self._attr_entity_picture = self._attr_entity_picture.format( + height=24, + width=24, + ) else: self._attr_native_value = STATE_OFFLINE self._attr_extra_state_attributes[ATTR_GAME] = None self._attr_extra_state_attributes[ATTR_TITLE] = None - self._attr_entity_picture = channel["offline_image_url"] + self._attr_entity_picture = channel["profile_image_url"] diff --git a/tests/components/twitch/test_twitch.py b/tests/components/twitch/test_twitch.py index bfffeb4ae7f..0e086fe6f7a 100644 --- a/tests/components/twitch/test_twitch.py +++ b/tests/components/twitch/test_twitch.py @@ -28,6 +28,7 @@ USER_OBJECT = { "id": 123, "display_name": "channel123", "offline_image_url": "logo.png", + "profile_image_url": "logo.png", "view_count": 42, } STREAM_OBJECT_ONLINE = { From 90ad6ca540fd48c409da1558bdf30118e5efbb01 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 22 Jun 2022 04:46:38 -0400 Subject: [PATCH 1663/3516] Bumps version of pyunifiprotect to 4.0.5 (#73798) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index dfa748835f5..00761790474 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.4", "unifi-discovery==1.1.4"], + "requirements": ["pyunifiprotect==4.0.5", "unifi-discovery==1.1.4"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 9d3d59f3b05..8eada81b4e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1990,7 +1990,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.4 +pyunifiprotect==4.0.5 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f9877ff298..12a89392d9e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1325,7 +1325,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.4 +pyunifiprotect==4.0.5 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 574ec5a507fd073553c02a09ec5a96eba27505ef Mon Sep 17 00:00:00 2001 From: Gordon Allott Date: Mon, 20 Jun 2022 19:27:39 +0100 Subject: [PATCH 1664/3516] Ensure metoffice daily are returned once daily (#72440) * ensure metoffice daily are returned once daily * Fixes metoffice tests for MODE_DAILY --- homeassistant/components/metoffice/helpers.py | 4 ++ tests/components/metoffice/test_weather.py | 45 ++++++++++--------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/metoffice/helpers.py b/homeassistant/components/metoffice/helpers.py index 31ac4c141a9..00d5e73501d 100644 --- a/homeassistant/components/metoffice/helpers.py +++ b/homeassistant/components/metoffice/helpers.py @@ -7,6 +7,7 @@ import datapoint from homeassistant.helpers.update_coordinator import UpdateFailed from homeassistant.util.dt import utcnow +from .const import MODE_3HOURLY from .data import MetOfficeData _LOGGER = logging.getLogger(__name__) @@ -39,6 +40,9 @@ def fetch_data(connection: datapoint.Manager, site, mode) -> MetOfficeData: for day in forecast.days for timestep in day.timesteps if timestep.date > time_now + and ( + mode == MODE_3HOURLY or timestep.date.hour > 6 + ) # ensures only one result per day in MODE_DAILY ], site, ) diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index fb00203661b..bf279ff3cf7 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -163,16 +163,17 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.attributes.get("humidity") == 50 # Also has Forecasts added - again, just pick out 1 entry to check - assert len(weather.attributes.get("forecast")) == 8 + # ensures that daily filters out multiple results per day + assert len(weather.attributes.get("forecast")) == 4 assert ( - weather.attributes.get("forecast")[7]["datetime"] == "2020-04-29T12:00:00+00:00" + weather.attributes.get("forecast")[3]["datetime"] == "2020-04-29T12:00:00+00:00" ) - assert weather.attributes.get("forecast")[7]["condition"] == "rainy" - assert weather.attributes.get("forecast")[7]["precipitation_probability"] == 59 - assert weather.attributes.get("forecast")[7]["temperature"] == 13 - assert weather.attributes.get("forecast")[7]["wind_speed"] == 13 - assert weather.attributes.get("forecast")[7]["wind_bearing"] == "SE" + assert weather.attributes.get("forecast")[3]["condition"] == "rainy" + assert weather.attributes.get("forecast")[3]["precipitation_probability"] == 59 + assert weather.attributes.get("forecast")[3]["temperature"] == 13 + assert weather.attributes.get("forecast")[3]["wind_speed"] == 13 + assert weather.attributes.get("forecast")[3]["wind_bearing"] == "SE" @freeze_time(datetime.datetime(2020, 4, 25, 12, tzinfo=datetime.timezone.utc)) @@ -258,16 +259,17 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("humidity") == 50 # Also has Forecasts added - again, just pick out 1 entry to check - assert len(weather.attributes.get("forecast")) == 8 + # ensures that daily filters out multiple results per day + assert len(weather.attributes.get("forecast")) == 4 assert ( - weather.attributes.get("forecast")[7]["datetime"] == "2020-04-29T12:00:00+00:00" + weather.attributes.get("forecast")[3]["datetime"] == "2020-04-29T12:00:00+00:00" ) - assert weather.attributes.get("forecast")[7]["condition"] == "rainy" - assert weather.attributes.get("forecast")[7]["precipitation_probability"] == 59 - assert weather.attributes.get("forecast")[7]["temperature"] == 13 - assert weather.attributes.get("forecast")[7]["wind_speed"] == 13 - assert weather.attributes.get("forecast")[7]["wind_bearing"] == "SE" + assert weather.attributes.get("forecast")[3]["condition"] == "rainy" + assert weather.attributes.get("forecast")[3]["precipitation_probability"] == 59 + assert weather.attributes.get("forecast")[3]["temperature"] == 13 + assert weather.attributes.get("forecast")[3]["wind_speed"] == 13 + assert weather.attributes.get("forecast")[3]["wind_bearing"] == "SE" # King's Lynn 3-hourly weather platform expected results weather = hass.states.get("weather.met_office_king_s_lynn_3_hourly") @@ -305,13 +307,14 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("humidity") == 75 # All should have Forecast added - again, just picking out 1 entry to check - assert len(weather.attributes.get("forecast")) == 8 + # ensures daily filters out multiple results per day + assert len(weather.attributes.get("forecast")) == 4 assert ( - weather.attributes.get("forecast")[5]["datetime"] == "2020-04-28T12:00:00+00:00" + weather.attributes.get("forecast")[2]["datetime"] == "2020-04-28T12:00:00+00:00" ) - assert weather.attributes.get("forecast")[5]["condition"] == "cloudy" - assert weather.attributes.get("forecast")[5]["precipitation_probability"] == 14 - assert weather.attributes.get("forecast")[5]["temperature"] == 11 - assert weather.attributes.get("forecast")[5]["wind_speed"] == 7 - assert weather.attributes.get("forecast")[5]["wind_bearing"] == "ESE" + assert weather.attributes.get("forecast")[2]["condition"] == "cloudy" + assert weather.attributes.get("forecast")[2]["precipitation_probability"] == 14 + assert weather.attributes.get("forecast")[2]["temperature"] == 11 + assert weather.attributes.get("forecast")[2]["wind_speed"] == 7 + assert weather.attributes.get("forecast")[2]["wind_bearing"] == "ESE" From 8dbc6b10855b1317a736c08ece0cb8c3f58ef69b Mon Sep 17 00:00:00 2001 From: Jonny Bergdahl Date: Wed, 22 Jun 2022 10:22:09 +0200 Subject: [PATCH 1665/3516] Fix thumbnail issues in Twitch integration (#72564) Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/twitch/sensor.py | 9 ++++++--- tests/components/twitch/test_twitch.py | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index 95006a4cab7..dcc2dc9bbf6 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -108,10 +108,8 @@ class TwitchSensor(SensorEntity): def update(self) -> None: """Update device state.""" - followers = self._client.get_users_follows(to_id=self.unique_id)["total"] channel = self._client.get_users(user_ids=[self.unique_id])["data"][0] - self._attr_extra_state_attributes = { ATTR_FOLLOWING: followers, ATTR_VIEWS: channel["view_count"], @@ -151,8 +149,13 @@ class TwitchSensor(SensorEntity): self._attr_extra_state_attributes[ATTR_GAME] = stream["game_name"] self._attr_extra_state_attributes[ATTR_TITLE] = stream["title"] self._attr_entity_picture = stream["thumbnail_url"] + if self._attr_entity_picture is not None: + self._attr_entity_picture = self._attr_entity_picture.format( + height=24, + width=24, + ) else: self._attr_native_value = STATE_OFFLINE self._attr_extra_state_attributes[ATTR_GAME] = None self._attr_extra_state_attributes[ATTR_TITLE] = None - self._attr_entity_picture = channel["offline_image_url"] + self._attr_entity_picture = channel["profile_image_url"] diff --git a/tests/components/twitch/test_twitch.py b/tests/components/twitch/test_twitch.py index bfffeb4ae7f..0e086fe6f7a 100644 --- a/tests/components/twitch/test_twitch.py +++ b/tests/components/twitch/test_twitch.py @@ -28,6 +28,7 @@ USER_OBJECT = { "id": 123, "display_name": "channel123", "offline_image_url": "logo.png", + "profile_image_url": "logo.png", "view_count": 42, } STREAM_OBJECT_ONLINE = { From 7b5258bc575767e578218a24f50d7a4f3c5e4ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roy?= Date: Tue, 14 Jun 2022 20:11:37 -0700 Subject: [PATCH 1666/3516] Bump aiobafi6 to 0.6.0 to fix logging performance (#73517) --- homeassistant/components/baf/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/baf/manifest.json b/homeassistant/components/baf/manifest.json index 8143c35410e..821ad1a21cb 100644 --- a/homeassistant/components/baf/manifest.json +++ b/homeassistant/components/baf/manifest.json @@ -3,7 +3,7 @@ "name": "Big Ass Fans", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/baf", - "requirements": ["aiobafi6==0.5.0"], + "requirements": ["aiobafi6==0.6.0"], "codeowners": ["@bdraco", "@jfroy"], "iot_class": "local_push", "zeroconf": [ diff --git a/requirements_all.txt b/requirements_all.txt index c90229fdb76..608d0d5bbdf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -122,7 +122,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.5.0 +aiobafi6==0.6.0 # homeassistant.components.aws aiobotocore==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 881aa1cef55..19a9bdfc684 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -109,7 +109,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.5.0 +aiobafi6==0.6.0 # homeassistant.components.aws aiobotocore==2.1.0 From a310b28170f608a3632b03979e47bb51d42f9531 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 16 Jun 2022 11:43:36 +0200 Subject: [PATCH 1667/3516] Use IP address instead of hostname in Brother integration (#73556) --- .../components/brother/config_flow.py | 5 +-- tests/components/brother/test_config_flow.py | 38 +++++++++++-------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index 39a196aa6cb..a136c98bf91 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -83,8 +83,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - # Hostname is format: brother.local. - self.host = discovery_info.hostname.rstrip(".") + self.host = discovery_info.host # Do not probe the device if the host is already configured self._async_abort_entries_match({CONF_HOST: self.host}) @@ -102,7 +101,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # Check if already configured await self.async_set_unique_id(self.brother.serial.lower()) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured({CONF_HOST: self.host}) self.context.update( { diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 6dbaebdfa7b..00493011500 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_HOST, CONF_TYPE from tests.common import MockConfigEntry, load_fixture -CONFIG = {CONF_HOST: "localhost", CONF_TYPE: "laser"} +CONFIG = {CONF_HOST: "127.0.0.1", CONF_TYPE: "laser"} async def test_show_form(hass): @@ -32,13 +32,15 @@ async def test_create_entry_with_hostname(hass): return_value=json.loads(load_fixture("printer_data.json", "brother")), ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: "example.local", CONF_TYPE: "laser"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" - assert result["data"][CONF_HOST] == CONFIG[CONF_HOST] - assert result["data"][CONF_TYPE] == CONFIG[CONF_TYPE] + assert result["data"][CONF_HOST] == "example.local" + assert result["data"][CONF_TYPE] == "laser" async def test_create_entry_with_ipv4_address(hass): @@ -48,9 +50,7 @@ async def test_create_entry_with_ipv4_address(hass): return_value=json.loads(load_fixture("printer_data.json", "brother")), ): result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_HOST: "127.0.0.1", CONF_TYPE: "laser"}, + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -145,7 +145,7 @@ async def test_zeroconf_snmp_error(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", addresses=["mock_host"], hostname="example.local.", name="Brother Printer", @@ -166,7 +166,7 @@ async def test_zeroconf_unsupported_model(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", addresses=["mock_host"], hostname="example.local.", name="Brother Printer", @@ -187,15 +187,18 @@ async def test_zeroconf_device_exists_abort(hass): "brother.Brother._get_data", return_value=json.loads(load_fixture("printer_data.json", "brother")), ): - MockConfigEntry(domain=DOMAIN, unique_id="0123456789", data=CONFIG).add_to_hass( - hass + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={CONF_HOST: "example.local", CONF_TYPE: "laser"}, ) + entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", addresses=["mock_host"], hostname="example.local.", name="Brother Printer", @@ -208,6 +211,9 @@ async def test_zeroconf_device_exists_abort(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + # Test config entry got updated with latest IP + assert entry.data["host"] == "127.0.0.1" + async def test_zeroconf_no_probe_existing_device(hass): """Test we do not probe the device is the host is already configured.""" @@ -218,9 +224,9 @@ async def test_zeroconf_no_probe_existing_device(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", addresses=["mock_host"], - hostname="localhost", + hostname="example.local.", name="Brother Printer", port=None, properties={}, @@ -245,7 +251,7 @@ async def test_zeroconf_confirm_create_entry(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf.ZeroconfServiceInfo( - host="mock_host", + host="127.0.0.1", addresses=["mock_host"], hostname="example.local.", name="Brother Printer", @@ -266,5 +272,5 @@ async def test_zeroconf_confirm_create_entry(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" - assert result["data"][CONF_HOST] == "example.local" + assert result["data"][CONF_HOST] == "127.0.0.1" assert result["data"][CONF_TYPE] == "laser" From 66bcebbab75155b129cf996b434fe09d003a0f5b Mon Sep 17 00:00:00 2001 From: muppet3000 Date: Thu, 16 Jun 2022 09:15:19 +0100 Subject: [PATCH 1668/3516] Bump growattServer to 1.2.2 (#73561) Fix #71577 - Updating growattServer dependency --- homeassistant/components/growatt_server/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index c8a71d426e7..4127b48ae64 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -3,7 +3,7 @@ "name": "Growatt", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/growatt_server/", - "requirements": ["growattServer==1.1.0"], + "requirements": ["growattServer==1.2.2"], "codeowners": ["@indykoning", "@muppet3000", "@JasperPlant"], "iot_class": "cloud_polling", "loggers": ["growattServer"] diff --git a/requirements_all.txt b/requirements_all.txt index 608d0d5bbdf..20dd9ae64ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -768,7 +768,7 @@ greenwavereality==0.5.1 gridnet==4.0.0 # homeassistant.components.growatt_server -growattServer==1.1.0 +growattServer==1.2.2 # homeassistant.components.gstreamer gstreamer-player==1.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 19a9bdfc684..fe7002d422a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -550,7 +550,7 @@ greeneye_monitor==3.0.3 gridnet==4.0.0 # homeassistant.components.growatt_server -growattServer==1.1.0 +growattServer==1.2.2 # homeassistant.components.profiler guppy3==3.1.2 From cfbc2ed4375d071dd2f9a828633e3b49866936bf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 15 Jun 2022 19:51:55 -1000 Subject: [PATCH 1669/3516] Handle offline generators in oncue (#73568) Fixes #73565 --- homeassistant/components/oncue/entity.py | 10 +- tests/components/oncue/__init__.py | 277 +++++++++++++++++++++++ tests/components/oncue/test_sensor.py | 22 +- 3 files changed, 304 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/oncue/entity.py b/homeassistant/components/oncue/entity.py index 40ca21edf96..d1942c532e7 100644 --- a/homeassistant/components/oncue/entity.py +++ b/homeassistant/components/oncue/entity.py @@ -3,6 +3,7 @@ from __future__ import annotations from aiooncue import OncueDevice, OncueSensor +from homeassistant.const import ATTR_CONNECTIONS from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from homeassistant.helpers.update_coordinator import ( @@ -30,16 +31,21 @@ class OncueEntity(CoordinatorEntity, Entity): self._device_id = device_id self._attr_unique_id = f"{device_id}_{description.key}" self._attr_name = f"{device.name} {sensor.display_name}" - mac_address_hex = hex(int(device.sensors["MacAddress"].value))[2:] self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, device_id)}, - connections={(dr.CONNECTION_NETWORK_MAC, mac_address_hex)}, name=device.name, hw_version=device.hardware_version, sw_version=device.sensors["FirmwareVersion"].display_value, model=device.sensors["GensetModelNumberSelect"].display_value, manufacturer="Kohler", ) + try: + mac_address_hex = hex(int(device.sensors["MacAddress"].value))[2:] + except ValueError: # MacAddress may be invalid if the gateway is offline + return + self._attr_device_info[ATTR_CONNECTIONS] = { + (dr.CONNECTION_NETWORK_MAC, mac_address_hex) + } @property def _oncue_value(self) -> str: diff --git a/tests/components/oncue/__init__.py b/tests/components/oncue/__init__.py index 48492a19933..32845aa8d26 100644 --- a/tests/components/oncue/__init__.py +++ b/tests/components/oncue/__init__.py @@ -269,6 +269,271 @@ MOCK_ASYNC_FETCH_ALL = { } +MOCK_ASYNC_FETCH_ALL_OFFLINE_DEVICE = { + "456789": OncueDevice( + name="My Generator", + state="Off", + product_name="RDC 2.4", + hardware_version="319", + serial_number="SERIAL", + sensors={ + "Product": OncueSensor( + name="Product", + display_name="Controller Type", + value="RDC 2.4", + display_value="RDC 2.4", + unit=None, + ), + "FirmwareVersion": OncueSensor( + name="FirmwareVersion", + display_name="Current Firmware", + value="2.0.6", + display_value="2.0.6", + unit=None, + ), + "LatestFirmware": OncueSensor( + name="LatestFirmware", + display_name="Latest Firmware", + value="2.0.6", + display_value="2.0.6", + unit=None, + ), + "EngineSpeed": OncueSensor( + name="EngineSpeed", + display_name="Engine Speed", + value="0", + display_value="0 R/min", + unit="R/min", + ), + "EngineTargetSpeed": OncueSensor( + name="EngineTargetSpeed", + display_name="Engine Target Speed", + value="0", + display_value="0 R/min", + unit="R/min", + ), + "EngineOilPressure": OncueSensor( + name="EngineOilPressure", + display_name="Engine Oil Pressure", + value=0, + display_value="0 Psi", + unit="Psi", + ), + "EngineCoolantTemperature": OncueSensor( + name="EngineCoolantTemperature", + display_name="Engine Coolant Temperature", + value=32, + display_value="32 F", + unit="F", + ), + "BatteryVoltage": OncueSensor( + name="BatteryVoltage", + display_name="Battery Voltage", + value="13.4", + display_value="13.4 V", + unit="V", + ), + "LubeOilTemperature": OncueSensor( + name="LubeOilTemperature", + display_name="Lube Oil Temperature", + value=32, + display_value="32 F", + unit="F", + ), + "GensetControllerTemperature": OncueSensor( + name="GensetControllerTemperature", + display_name="Generator Controller Temperature", + value=84.2, + display_value="84.2 F", + unit="F", + ), + "EngineCompartmentTemperature": OncueSensor( + name="EngineCompartmentTemperature", + display_name="Engine Compartment Temperature", + value=62.6, + display_value="62.6 F", + unit="F", + ), + "GeneratorTrueTotalPower": OncueSensor( + name="GeneratorTrueTotalPower", + display_name="Generator True Total Power", + value="0.0", + display_value="0.0 W", + unit="W", + ), + "GeneratorTruePercentOfRatedPower": OncueSensor( + name="GeneratorTruePercentOfRatedPower", + display_name="Generator True Percent Of Rated Power", + value="0", + display_value="0 %", + unit="%", + ), + "GeneratorVoltageAB": OncueSensor( + name="GeneratorVoltageAB", + display_name="Generator Voltage AB", + value="0.0", + display_value="0.0 V", + unit="V", + ), + "GeneratorVoltageAverageLineToLine": OncueSensor( + name="GeneratorVoltageAverageLineToLine", + display_name="Generator Voltage Average Line To Line", + value="0.0", + display_value="0.0 V", + unit="V", + ), + "GeneratorCurrentAverage": OncueSensor( + name="GeneratorCurrentAverage", + display_name="Generator Current Average", + value="0.0", + display_value="0.0 A", + unit="A", + ), + "GeneratorFrequency": OncueSensor( + name="GeneratorFrequency", + display_name="Generator Frequency", + value="0.0", + display_value="0.0 Hz", + unit="Hz", + ), + "GensetSerialNumber": OncueSensor( + name="GensetSerialNumber", + display_name="Generator Serial Number", + value="33FDGMFR0026", + display_value="33FDGMFR0026", + unit=None, + ), + "GensetState": OncueSensor( + name="GensetState", + display_name="Generator State", + value="Off", + display_value="Off", + unit=None, + ), + "GensetControllerSerialNumber": OncueSensor( + name="GensetControllerSerialNumber", + display_name="Generator Controller Serial Number", + value="-1", + display_value="-1", + unit=None, + ), + "GensetModelNumberSelect": OncueSensor( + name="GensetModelNumberSelect", + display_name="Genset Model Number Select", + value="38 RCLB", + display_value="38 RCLB", + unit=None, + ), + "GensetControllerClockTime": OncueSensor( + name="GensetControllerClockTime", + display_name="Generator Controller Clock Time", + value="2022-01-13 18:08:13", + display_value="2022-01-13 18:08:13", + unit=None, + ), + "GensetControllerTotalOperationTime": OncueSensor( + name="GensetControllerTotalOperationTime", + display_name="Generator Controller Total Operation Time", + value="16770.8", + display_value="16770.8 h", + unit="h", + ), + "EngineTotalRunTime": OncueSensor( + name="EngineTotalRunTime", + display_name="Engine Total Run Time", + value="28.1", + display_value="28.1 h", + unit="h", + ), + "EngineTotalRunTimeLoaded": OncueSensor( + name="EngineTotalRunTimeLoaded", + display_name="Engine Total Run Time Loaded", + value="5.5", + display_value="5.5 h", + unit="h", + ), + "EngineTotalNumberOfStarts": OncueSensor( + name="EngineTotalNumberOfStarts", + display_name="Engine Total Number Of Starts", + value="101", + display_value="101", + unit=None, + ), + "GensetTotalEnergy": OncueSensor( + name="GensetTotalEnergy", + display_name="Genset Total Energy", + value="1.2022309E7", + display_value="1.2022309E7 kWh", + unit="kWh", + ), + "AtsContactorPosition": OncueSensor( + name="AtsContactorPosition", + display_name="Ats Contactor Position", + value="Source1", + display_value="Source1", + unit=None, + ), + "AtsSourcesAvailable": OncueSensor( + name="AtsSourcesAvailable", + display_name="Ats Sources Available", + value="Source1", + display_value="Source1", + unit=None, + ), + "Source1VoltageAverageLineToLine": OncueSensor( + name="Source1VoltageAverageLineToLine", + display_name="Source1 Voltage Average Line To Line", + value="253.5", + display_value="253.5 V", + unit="V", + ), + "Source2VoltageAverageLineToLine": OncueSensor( + name="Source2VoltageAverageLineToLine", + display_name="Source2 Voltage Average Line To Line", + value="0.0", + display_value="0.0 V", + unit="V", + ), + "IPAddress": OncueSensor( + name="IPAddress", + display_name="IP Address", + value="1.2.3.4:1026", + display_value="1.2.3.4:1026", + unit=None, + ), + "MacAddress": OncueSensor( + name="MacAddress", + display_name="Mac Address", + value="--", + display_value="--", + unit=None, + ), + "ConnectedServerIPAddress": OncueSensor( + name="ConnectedServerIPAddress", + display_name="Connected Server IP Address", + value="40.117.195.28", + display_value="40.117.195.28", + unit=None, + ), + "NetworkConnectionEstablished": OncueSensor( + name="NetworkConnectionEstablished", + display_name="Network Connection Established", + value="true", + display_value="True", + unit=None, + ), + "SerialNumber": OncueSensor( + name="SerialNumber", + display_name="Serial Number", + value="1073879692", + display_value="1073879692", + unit=None, + ), + }, + ) +} + + def _patch_login_and_data(): @contextmanager def _patcher(): @@ -279,3 +544,15 @@ def _patch_login_and_data(): yield return _patcher() + + +def _patch_login_and_data_offline_device(): + @contextmanager + def _patcher(): + with patch("homeassistant.components.oncue.Oncue.async_login",), patch( + "homeassistant.components.oncue.Oncue.async_fetch_all", + return_value=MOCK_ASYNC_FETCH_ALL_OFFLINE_DEVICE, + ): + yield + + return _patcher() diff --git a/tests/components/oncue/test_sensor.py b/tests/components/oncue/test_sensor.py index 5fe8b807c1b..60c9f68f81b 100644 --- a/tests/components/oncue/test_sensor.py +++ b/tests/components/oncue/test_sensor.py @@ -1,19 +1,29 @@ """Tests for the oncue sensor.""" from __future__ import annotations +import pytest + from homeassistant.components import oncue from homeassistant.components.oncue.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -from . import _patch_login_and_data +from . import _patch_login_and_data, _patch_login_and_data_offline_device from tests.common import MockConfigEntry -async def test_sensors(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "patcher, connections", + [ + [_patch_login_and_data, {("mac", "c9:24:22:6f:14:00")}], + [_patch_login_and_data_offline_device, set()], + ], +) +async def test_sensors(hass: HomeAssistant, patcher, connections) -> None: """Test that the sensors are setup with the expected values.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -21,11 +31,17 @@ async def test_sensors(hass: HomeAssistant) -> None: unique_id="any", ) config_entry.add_to_hass(hass) - with _patch_login_and_data(): + with patcher(): await async_setup_component(hass, oncue.DOMAIN, {oncue.DOMAIN: {}}) await hass.async_block_till_done() assert config_entry.state == ConfigEntryState.LOADED + entity_registry = er.async_get(hass) + ent = entity_registry.async_get("sensor.my_generator_latest_firmware") + device_registry = dr.async_get(hass) + dev = device_registry.async_get(ent.device_id) + assert dev.connections == connections + assert len(hass.states.async_all("sensor")) == 25 assert hass.states.get("sensor.my_generator_latest_firmware").state == "2.0.6" From 08382204a348453cec7630fe144e947ecf8f1b26 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 Jun 2022 10:26:50 +0200 Subject: [PATCH 1670/3516] Don't attempt to reload MQTT device tracker (#73577) --- homeassistant/components/mqtt/__init__.py | 3 ++- homeassistant/components/mqtt/const.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index f9a6ebd025f..95cf621f456 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -76,6 +76,7 @@ from .const import ( # noqa: F401 MQTT_DISCONNECTED, MQTT_RELOADED, PLATFORMS, + RELOADABLE_PLATFORMS, ) from .models import ( # noqa: F401 MqttCommandTemplate, @@ -378,7 +379,7 @@ async def async_setup_entry( # noqa: C901 # Setup reload service. Once support for legacy config is removed in 2022.9, we # should no longer call async_setup_reload_service but instead implement a custom # service - await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + await async_setup_reload_service(hass, DOMAIN, RELOADABLE_PLATFORMS) async def _async_reload_platforms(_: Event | None) -> None: """Discover entities for a platform.""" diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index b05fd867eeb..67a9208faba 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -92,3 +92,23 @@ PLATFORMS = [ Platform.SWITCH, Platform.VACUUM, ] + +RELOADABLE_PLATFORMS = [ + Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.CAMERA, + Platform.CLIMATE, + Platform.COVER, + Platform.FAN, + Platform.HUMIDIFIER, + Platform.LIGHT, + Platform.LOCK, + Platform.NUMBER, + Platform.SELECT, + Platform.SCENE, + Platform.SENSOR, + Platform.SIREN, + Platform.SWITCH, + Platform.VACUUM, +] From 8d41f8bbc24ce2eaf35339262795775e9a989d40 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 Jun 2022 08:52:37 +0200 Subject: [PATCH 1671/3516] Fix handling of illegal dates in onvif sensor (#73600) * Fix handling of illegal dates in onvif sensor * Address review comment * Address review comment --- homeassistant/components/onvif/parsers.py | 38 ++++++++++++++--------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/onvif/parsers.py b/homeassistant/components/onvif/parsers.py index 5141f25cbef..87b901d2c52 100644 --- a/homeassistant/components/onvif/parsers.py +++ b/homeassistant/components/onvif/parsers.py @@ -1,4 +1,6 @@ """ONVIF event parsers.""" +from __future__ import annotations + from collections.abc import Callable, Coroutine import datetime from typing import Any @@ -12,16 +14,16 @@ from .models import Event PARSERS: Registry[str, Callable[[str, Any], Coroutine[Any, Any, Event]]] = Registry() -def datetime_or_zero(value: str) -> datetime: - """Convert strings to datetimes, if invalid, return datetime.min.""" +def local_datetime_or_none(value: str) -> datetime.datetime | None: + """Convert strings to datetimes, if invalid, return None.""" # To handle cameras that return times like '0000-00-00T00:00:00Z' (e.g. hikvision) try: ret = dt_util.parse_datetime(value) except ValueError: - return datetime.datetime.min - if ret is None: - return datetime.datetime.min - return ret + return None + if ret is not None: + return dt_util.as_local(ret) + return None @PARSERS.register("tns1:VideoSource/MotionAlarm") @@ -394,14 +396,16 @@ async def async_parse_last_reboot(uid: str, msg) -> Event: Topic: tns1:Monitoring/OperatingTime/LastReboot """ try: - date_time = datetime_or_zero(msg.Message._value_1.Data.SimpleItem[0].Value) + date_time = local_datetime_or_none( + msg.Message._value_1.Data.SimpleItem[0].Value + ) return Event( f"{uid}_{msg.Topic._value_1}", "Last Reboot", "sensor", "timestamp", None, - dt_util.as_local(date_time), + date_time, EntityCategory.DIAGNOSTIC, ) except (AttributeError, KeyError): @@ -416,14 +420,16 @@ async def async_parse_last_reset(uid: str, msg) -> Event: Topic: tns1:Monitoring/OperatingTime/LastReset """ try: - date_time = datetime_or_zero(msg.Message._value_1.Data.SimpleItem[0].Value) + date_time = local_datetime_or_none( + msg.Message._value_1.Data.SimpleItem[0].Value + ) return Event( f"{uid}_{msg.Topic._value_1}", "Last Reset", "sensor", "timestamp", None, - dt_util.as_local(date_time), + date_time, EntityCategory.DIAGNOSTIC, entity_enabled=False, ) @@ -440,14 +446,16 @@ async def async_parse_backup_last(uid: str, msg) -> Event: """ try: - date_time = datetime_or_zero(msg.Message._value_1.Data.SimpleItem[0].Value) + date_time = local_datetime_or_none( + msg.Message._value_1.Data.SimpleItem[0].Value + ) return Event( f"{uid}_{msg.Topic._value_1}", "Last Backup", "sensor", "timestamp", None, - dt_util.as_local(date_time), + date_time, EntityCategory.DIAGNOSTIC, entity_enabled=False, ) @@ -463,14 +471,16 @@ async def async_parse_last_clock_sync(uid: str, msg) -> Event: Topic: tns1:Monitoring/OperatingTime/LastClockSynchronization """ try: - date_time = datetime_or_zero(msg.Message._value_1.Data.SimpleItem[0].Value) + date_time = local_datetime_or_none( + msg.Message._value_1.Data.SimpleItem[0].Value + ) return Event( f"{uid}_{msg.Topic._value_1}", "Last Clock Synchronization", "sensor", "timestamp", None, - dt_util.as_local(date_time), + date_time, EntityCategory.DIAGNOSTIC, entity_enabled=False, ) From 01053418f6d267af7918eb71674198a29a6ea8ec Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Fri, 17 Jun 2022 07:40:02 +0200 Subject: [PATCH 1672/3516] Fix voltage and current values for Fritz!DECT smart plugs (#73608) fix voltage and current values --- homeassistant/components/fritzbox/sensor.py | 4 +- tests/components/fritzbox/test_switch.py | 65 +++++++++++++++------ 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/fritzbox/sensor.py b/homeassistant/components/fritzbox/sensor.py index 2ae7f9dccc8..161dfc196d2 100644 --- a/homeassistant/components/fritzbox/sensor.py +++ b/homeassistant/components/fritzbox/sensor.py @@ -96,7 +96,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] - native_value=lambda device: device.voltage / 1000 + native_value=lambda device: device.voltage if getattr(device, "voltage", None) else 0.0, ), @@ -107,7 +107,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, suitable=lambda device: device.has_powermeter, # type: ignore[no-any-return] - native_value=lambda device: device.power / device.voltage + native_value=lambda device: device.power / device.voltage / 1000 if device.power and getattr(device, "voltage", None) else 0.0, ), diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index a3135dd61f3..75799a08d48 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -16,6 +16,8 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, POWER_WATT, SERVICE_TURN_OFF, @@ -48,29 +50,54 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): assert state.attributes[ATTR_FRIENDLY_NAME] == CONF_FAKE_NAME assert ATTR_STATE_CLASS not in state.attributes - state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_temperature") - assert state - assert state.state == "1.23" - assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Temperature" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS - assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT - state = hass.states.get(f"{ENTITY_ID}_humidity") assert state is None - state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_power_consumption") - assert state - assert state.state == "5.678" - assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Power Consumption" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT - assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT + sensors = ( + [ + f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_temperature", + "1.23", + f"{CONF_FAKE_NAME} Temperature", + TEMP_CELSIUS, + SensorStateClass.MEASUREMENT, + ], + [ + f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_power_consumption", + "5.678", + f"{CONF_FAKE_NAME} Power Consumption", + POWER_WATT, + SensorStateClass.MEASUREMENT, + ], + [ + f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_total_energy", + "1.234", + f"{CONF_FAKE_NAME} Total Energy", + ENERGY_KILO_WATT_HOUR, + SensorStateClass.TOTAL_INCREASING, + ], + [ + f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_voltage", + "230", + f"{CONF_FAKE_NAME} Voltage", + ELECTRIC_POTENTIAL_VOLT, + SensorStateClass.MEASUREMENT, + ], + [ + f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_electric_current", + "0.0246869565217391", + f"{CONF_FAKE_NAME} Electric Current", + ELECTRIC_CURRENT_AMPERE, + SensorStateClass.MEASUREMENT, + ], + ) - state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_total_energy") - assert state - assert state.state == "1.234" - assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Total Energy" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_KILO_WATT_HOUR - assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.TOTAL_INCREASING + for sensor in sensors: + state = hass.states.get(sensor[0]) + assert state + assert state.state == sensor[1] + assert state.attributes[ATTR_FRIENDLY_NAME] == sensor[2] + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == sensor[3] + assert state.attributes[ATTR_STATE_CLASS] == sensor[4] async def test_turn_on(hass: HomeAssistant, fritz: Mock): From 155117758191ff75eef80d09a4a5e6d67dba16ab Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 20 Jun 2022 08:51:12 +0200 Subject: [PATCH 1673/3516] Fix MQTT config schema to ensure correct validation (#73619) * Ensure config schema validation * Use correct schema for device_tracker * Remove schema validation from the platform setup * Remove loop to build schema --- homeassistant/components/mqtt/__init__.py | 6 +- .../components/mqtt/alarm_control_panel.py | 2 +- .../components/mqtt/binary_sensor.py | 4 +- homeassistant/components/mqtt/button.py | 4 +- homeassistant/components/mqtt/camera.py | 4 +- homeassistant/components/mqtt/climate.py | 4 +- homeassistant/components/mqtt/config.py | 107 +--------- .../components/mqtt/config_integration.py | 193 ++++++++++++++++++ homeassistant/components/mqtt/cover.py | 2 +- .../mqtt/device_tracker/__init__.py | 1 + .../mqtt/device_tracker/schema_discovery.py | 2 +- homeassistant/components/mqtt/fan.py | 4 +- homeassistant/components/mqtt/humidifier.py | 4 +- .../components/mqtt/light/__init__.py | 2 +- homeassistant/components/mqtt/lock.py | 2 +- homeassistant/components/mqtt/mixins.py | 22 +- homeassistant/components/mqtt/number.py | 4 +- homeassistant/components/mqtt/scene.py | 2 +- homeassistant/components/mqtt/select.py | 4 +- homeassistant/components/mqtt/sensor.py | 4 +- homeassistant/components/mqtt/siren.py | 2 +- homeassistant/components/mqtt/switch.py | 4 +- .../components/mqtt/vacuum/__init__.py | 4 +- tests/components/mqtt/test_humidifier.py | 13 ++ tests/components/mqtt/test_init.py | 58 +++--- 25 files changed, 262 insertions(+), 196 deletions(-) create mode 100644 homeassistant/components/mqtt/config_integration.py diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 95cf621f456..fc4e15f3237 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -47,7 +47,11 @@ from .client import ( # noqa: F401 publish, subscribe, ) -from .config import CONFIG_SCHEMA_BASE, DEFAULT_VALUES, DEPRECATED_CONFIG_KEYS +from .config_integration import ( + CONFIG_SCHEMA_BASE, + DEFAULT_VALUES, + DEPRECATED_CONFIG_KEYS, +) from .const import ( # noqa: F401 ATTR_PAYLOAD, ATTR_QOS, diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index c0c6f9732d7..6bb7d9cd0d1 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -148,7 +148,7 @@ async def async_setup_entry( """Set up MQTT alarm control panel through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery(hass, alarm.DOMAIN, PLATFORM_SCHEMA_MODERN) + await async_setup_platform_discovery(hass, alarm.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index cec065e20f2..39fd87c8b02 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -103,9 +103,7 @@ async def async_setup_entry( """Set up MQTT binary sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, binary_sensor.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, binary_sensor.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index afa9900db35..370243c3579 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -83,9 +83,7 @@ async def async_setup_entry( """Set up MQTT button through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, button.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, button.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 86db828b111..5c8d3bc48b2 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -81,9 +81,7 @@ async def async_setup_entry( """Set up MQTT camera through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, camera.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, camera.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index bdcc82f2c39..a26e9cba8df 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -392,9 +392,7 @@ async def async_setup_entry( """Set up MQTT climate device through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, climate.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, climate.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/config.py b/homeassistant/components/mqtt/config.py index 4f84d911418..8cfc3490f0c 100644 --- a/homeassistant/components/mqtt/config.py +++ b/homeassistant/components/mqtt/config.py @@ -3,126 +3,21 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.const import ( - CONF_CLIENT_ID, - CONF_DISCOVERY, - CONF_PASSWORD, - CONF_PORT, - CONF_PROTOCOL, - CONF_USERNAME, - CONF_VALUE_TEMPLATE, -) +from homeassistant.const import CONF_VALUE_TEMPLATE from homeassistant.helpers import config_validation as cv from .const import ( - ATTR_PAYLOAD, - ATTR_QOS, - ATTR_RETAIN, - ATTR_TOPIC, - CONF_BIRTH_MESSAGE, - CONF_BROKER, - CONF_CERTIFICATE, - CONF_CLIENT_CERT, - CONF_CLIENT_KEY, CONF_COMMAND_TOPIC, - CONF_DISCOVERY_PREFIX, CONF_ENCODING, - CONF_KEEPALIVE, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, - CONF_TLS_INSECURE, - CONF_TLS_VERSION, - CONF_WILL_MESSAGE, - DEFAULT_BIRTH, - DEFAULT_DISCOVERY, DEFAULT_ENCODING, - DEFAULT_PREFIX, DEFAULT_QOS, DEFAULT_RETAIN, - DEFAULT_WILL, - PLATFORMS, - PROTOCOL_31, - PROTOCOL_311, ) from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic -DEFAULT_PORT = 1883 -DEFAULT_KEEPALIVE = 60 -DEFAULT_PROTOCOL = PROTOCOL_311 -DEFAULT_TLS_PROTOCOL = "auto" - -DEFAULT_VALUES = { - CONF_BIRTH_MESSAGE: DEFAULT_BIRTH, - CONF_DISCOVERY: DEFAULT_DISCOVERY, - CONF_PORT: DEFAULT_PORT, - CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL, - CONF_WILL_MESSAGE: DEFAULT_WILL, -} - -CLIENT_KEY_AUTH_MSG = ( - "client_key and client_cert must both be present in " - "the MQTT broker configuration" -) - -MQTT_WILL_BIRTH_SCHEMA = vol.Schema( - { - vol.Inclusive(ATTR_TOPIC, "topic_payload"): valid_publish_topic, - vol.Inclusive(ATTR_PAYLOAD, "topic_payload"): cv.string, - vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, - vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - }, - required=True, -) - -PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( - {vol.Optional(platform.value): cv.ensure_list for platform in PLATFORMS} -) - -CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend( - { - vol.Optional(CONF_CLIENT_ID): cv.string, - vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( - vol.Coerce(int), vol.Range(min=15) - ), - vol.Optional(CONF_BROKER): cv.string, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), - vol.Inclusive( - CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG - ): cv.isfile, - vol.Inclusive( - CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG - ): cv.isfile, - vol.Optional(CONF_TLS_INSECURE): cv.boolean, - vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"), - vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( - cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) - ), - vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_DISCOVERY): cv.boolean, - # discovery_prefix must be a valid publish topic because if no - # state topic is specified, it will be created with the given prefix. - vol.Optional( - CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX - ): valid_publish_topic, - } -) - -DEPRECATED_CONFIG_KEYS = [ - CONF_BIRTH_MESSAGE, - CONF_BROKER, - CONF_DISCOVERY, - CONF_PASSWORD, - CONF_PORT, - CONF_TLS_VERSION, - CONF_USERNAME, - CONF_WILL_MESSAGE, -] - SCHEMA_BASE = { vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, diff --git a/homeassistant/components/mqtt/config_integration.py b/homeassistant/components/mqtt/config_integration.py new file mode 100644 index 00000000000..ab685a63802 --- /dev/null +++ b/homeassistant/components/mqtt/config_integration.py @@ -0,0 +1,193 @@ +"""Support for MQTT platform config setup.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_DISCOVERY, + CONF_PASSWORD, + CONF_PORT, + CONF_PROTOCOL, + CONF_USERNAME, + Platform, +) +from homeassistant.helpers import config_validation as cv + +from . import ( + alarm_control_panel as alarm_control_panel_platform, + binary_sensor as binary_sensor_platform, + button as button_platform, + camera as camera_platform, + climate as climate_platform, + cover as cover_platform, + device_tracker as device_tracker_platform, + fan as fan_platform, + humidifier as humidifier_platform, + light as light_platform, + lock as lock_platform, + number as number_platform, + scene as scene_platform, + select as select_platform, + sensor as sensor_platform, + siren as siren_platform, + switch as switch_platform, + vacuum as vacuum_platform, +) +from .const import ( + ATTR_PAYLOAD, + ATTR_QOS, + ATTR_RETAIN, + ATTR_TOPIC, + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_CERTIFICATE, + CONF_CLIENT_CERT, + CONF_CLIENT_KEY, + CONF_DISCOVERY_PREFIX, + CONF_KEEPALIVE, + CONF_TLS_INSECURE, + CONF_TLS_VERSION, + CONF_WILL_MESSAGE, + DEFAULT_BIRTH, + DEFAULT_DISCOVERY, + DEFAULT_PREFIX, + DEFAULT_QOS, + DEFAULT_RETAIN, + DEFAULT_WILL, + PROTOCOL_31, + PROTOCOL_311, +) +from .util import _VALID_QOS_SCHEMA, valid_publish_topic + +DEFAULT_PORT = 1883 +DEFAULT_KEEPALIVE = 60 +DEFAULT_PROTOCOL = PROTOCOL_311 +DEFAULT_TLS_PROTOCOL = "auto" + +DEFAULT_VALUES = { + CONF_BIRTH_MESSAGE: DEFAULT_BIRTH, + CONF_DISCOVERY: DEFAULT_DISCOVERY, + CONF_PORT: DEFAULT_PORT, + CONF_TLS_VERSION: DEFAULT_TLS_PROTOCOL, + CONF_WILL_MESSAGE: DEFAULT_WILL, +} + +PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( + { + Platform.ALARM_CONTROL_PANEL.value: vol.All( + cv.ensure_list, [alarm_control_panel_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.BINARY_SENSOR.value: vol.All( + cv.ensure_list, [binary_sensor_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.BUTTON.value: vol.All( + cv.ensure_list, [button_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.CAMERA.value: vol.All( + cv.ensure_list, [camera_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.CLIMATE.value: vol.All( + cv.ensure_list, [climate_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.COVER.value: vol.All( + cv.ensure_list, [cover_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.DEVICE_TRACKER.value: vol.All( + cv.ensure_list, [device_tracker_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.FAN.value: vol.All( + cv.ensure_list, [fan_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.HUMIDIFIER.value: vol.All( + cv.ensure_list, [humidifier_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.LOCK.value: vol.All( + cv.ensure_list, [lock_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.LIGHT.value: vol.All( + cv.ensure_list, [light_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.NUMBER.value: vol.All( + cv.ensure_list, [number_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.SCENE.value: vol.All( + cv.ensure_list, [scene_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.SELECT.value: vol.All( + cv.ensure_list, [select_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.SENSOR.value: vol.All( + cv.ensure_list, [sensor_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.SIREN.value: vol.All( + cv.ensure_list, [siren_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.SWITCH.value: vol.All( + cv.ensure_list, [switch_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + Platform.VACUUM.value: vol.All( + cv.ensure_list, [vacuum_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + ), + } +) + + +CLIENT_KEY_AUTH_MSG = ( + "client_key and client_cert must both be present in " + "the MQTT broker configuration" +) + +MQTT_WILL_BIRTH_SCHEMA = vol.Schema( + { + vol.Inclusive(ATTR_TOPIC, "topic_payload"): valid_publish_topic, + vol.Inclusive(ATTR_PAYLOAD, "topic_payload"): cv.string, + vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, + vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, + }, + required=True, +) + +CONFIG_SCHEMA_BASE = PLATFORM_CONFIG_SCHEMA_BASE.extend( + { + vol.Optional(CONF_CLIENT_ID): cv.string, + vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( + vol.Coerce(int), vol.Range(min=15) + ), + vol.Optional(CONF_BROKER): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CERTIFICATE): vol.Any("auto", cv.isfile), + vol.Inclusive( + CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG + ): cv.isfile, + vol.Inclusive( + CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG + ): cv.isfile, + vol.Optional(CONF_TLS_INSECURE): cv.boolean, + vol.Optional(CONF_TLS_VERSION): vol.Any("auto", "1.0", "1.1", "1.2"), + vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( + cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) + ), + vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional(CONF_DISCOVERY): cv.boolean, + # discovery_prefix must be a valid publish topic because if no + # state topic is specified, it will be created with the given prefix. + vol.Optional( + CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX + ): valid_publish_topic, + } +) + +DEPRECATED_CONFIG_KEYS = [ + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_DISCOVERY, + CONF_PASSWORD, + CONF_PORT, + CONF_TLS_VERSION, + CONF_USERNAME, + CONF_WILL_MESSAGE, +] diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 8d4df0c301d..0901a4f63a6 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -241,7 +241,7 @@ async def async_setup_entry( """Set up MQTT cover through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery(hass, cover.DOMAIN, PLATFORM_SCHEMA_MODERN) + await async_setup_platform_discovery(hass, cover.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/device_tracker/__init__.py b/homeassistant/components/mqtt/device_tracker/__init__.py index bcd5bbd4ee1..1b6c2b25ff3 100644 --- a/homeassistant/components/mqtt/device_tracker/__init__.py +++ b/homeassistant/components/mqtt/device_tracker/__init__.py @@ -4,6 +4,7 @@ import voluptuous as vol from homeassistant.components import device_tracker from ..mixins import warn_for_legacy_schema +from .schema_discovery import PLATFORM_SCHEMA_MODERN # noqa: F401 from .schema_discovery import async_setup_entry_from_discovery from .schema_yaml import PLATFORM_SCHEMA_YAML, async_setup_scanner_from_yaml diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 1b48e15b80e..1ba540c8243 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -54,7 +54,7 @@ async def async_setup_entry_from_discovery(hass, config_entry, async_add_entitie *( _async_setup_entity(hass, async_add_entities, config, config_entry) for config in await async_get_platform_config_from_yaml( - hass, device_tracker.DOMAIN, PLATFORM_SCHEMA_MODERN + hass, device_tracker.DOMAIN ) ) ) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index d0b4ff10692..4e1d1465ac2 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -231,9 +231,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, fan.DOMAIN, PLATFORM_SCHEMA_MODERN) - ) + config_entry.async_on_unload(await async_setup_platform_discovery(hass, fan.DOMAIN)) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 1c9ec5dc201..d2856767cf0 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -188,9 +188,7 @@ async def async_setup_entry( """Set up MQTT humidifier through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, humidifier.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, humidifier.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 158ea6ffa0d..d4914cb9506 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -112,7 +112,7 @@ async def async_setup_entry( """Set up MQTT lights configured under the light platform key (deprecated).""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery(hass, light.DOMAIN, PLATFORM_SCHEMA_MODERN) + await async_setup_platform_discovery(hass, light.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 862e76635f7..ab9eca9aafa 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -103,7 +103,7 @@ async def async_setup_entry( """Set up MQTT lock through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery(hass, lock.DOMAIN, PLATFORM_SCHEMA_MODERN) + await async_setup_platform_discovery(hass, lock.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index e768c2ff409..dcf387eb360 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -10,7 +10,6 @@ from typing import Any, Protocol, cast, final import voluptuous as vol -from homeassistant.config import async_log_exception from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_CONFIGURATION_URL, @@ -263,7 +262,7 @@ class SetupEntity(Protocol): async def async_setup_platform_discovery( - hass: HomeAssistant, platform_domain: str, schema: vol.Schema + hass: HomeAssistant, platform_domain: str ) -> CALLBACK_TYPE: """Set up platform discovery for manual config.""" @@ -282,7 +281,7 @@ async def async_setup_platform_discovery( *( discovery.async_load_platform(hass, platform_domain, DOMAIN, config, {}) for config in await async_get_platform_config_from_yaml( - hass, platform_domain, schema, config_yaml + hass, platform_domain, config_yaml ) ) ) @@ -295,32 +294,17 @@ async def async_setup_platform_discovery( async def async_get_platform_config_from_yaml( hass: HomeAssistant, platform_domain: str, - schema: vol.Schema, config_yaml: ConfigType = None, ) -> list[ConfigType]: """Return a list of validated configurations for the domain.""" - def async_validate_config( - hass: HomeAssistant, - config: list[ConfigType], - ) -> list[ConfigType]: - """Validate config.""" - validated_config = [] - for config_item in config: - try: - validated_config.append(schema(config_item)) - except vol.MultipleInvalid as err: - async_log_exception(err, platform_domain, config_item, hass) - - return validated_config - if config_yaml is None: config_yaml = hass.data.get(DATA_MQTT_CONFIG) if not config_yaml: return [] if not (platform_configs := config_yaml.get(platform_domain)): return [] - return async_validate_config(hass, platform_configs) + return platform_configs async def async_setup_entry_helper(hass, domain, async_setup, schema): diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 1404dc86a3c..7ec07394aa5 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -134,9 +134,7 @@ async def async_setup_entry( """Set up MQTT number through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, number.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, number.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 9c4a212bd8e..cc911cc3431 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -80,7 +80,7 @@ async def async_setup_entry( """Set up MQTT scene through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery(hass, scene.DOMAIN, PLATFORM_SCHEMA_MODERN) + await async_setup_platform_discovery(hass, scene.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 994c11653b7..0d9f1411fd1 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -95,9 +95,7 @@ async def async_setup_entry( """Set up MQTT select through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, select.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, select.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index f9e0b5151bb..672e22f632f 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -148,9 +148,7 @@ async def async_setup_entry( """Set up MQTT sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, sensor.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, sensor.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index fef2a4fb3dd..e7b91274f4f 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -144,7 +144,7 @@ async def async_setup_entry( """Set up MQTT siren through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery(hass, siren.DOMAIN, PLATFORM_SCHEMA_MODERN) + await async_setup_platform_discovery(hass, siren.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index be7fc655e1e..dadd5f86f20 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -98,9 +98,7 @@ async def async_setup_entry( """Set up MQTT switch through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, switch.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, switch.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 206a15a024a..694e9530939 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -92,9 +92,7 @@ async def async_setup_entry( """Set up MQTT vacuum through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml config_entry.async_on_unload( - await async_setup_platform_discovery( - hass, vacuum.DOMAIN, PLATFORM_SCHEMA_MODERN - ) + await async_setup_platform_discovery(hass, vacuum.DOMAIN) ) # setup for discovery setup = functools.partial( diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index ea9a6edf0e3..cea5bfa1787 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -13,6 +13,7 @@ from homeassistant.components.humidifier import ( SERVICE_SET_HUMIDITY, SERVICE_SET_MODE, ) +from homeassistant.components.mqtt import CONFIG_SCHEMA from homeassistant.components.mqtt.humidifier import ( CONF_MODE_COMMAND_TOPIC, CONF_MODE_STATE_TOPIC, @@ -1277,3 +1278,15 @@ async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): hass, caplog, tmp_path, platform, config ) assert hass.states.get(f"{platform}.test") is not None + + +async def test_config_schema_validation(hass): + """Test invalid platform options in the config schema do pass the config validation.""" + platform = humidifier.DOMAIN + config = copy.deepcopy(DEFAULT_CONFIG[platform]) + config["name"] = "test" + del config["platform"] + CONFIG_SCHEMA({DOMAIN: {platform: config}}) + CONFIG_SCHEMA({DOMAIN: {platform: [config]}}) + with pytest.raises(MultipleInvalid): + CONFIG_SCHEMA({"mqtt": {"humidifier": [{"bla": "bla"}]}}) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 861b50d2f71..e9625e0fdb2 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -14,7 +14,7 @@ import yaml from homeassistant import config as hass_config from homeassistant.components import mqtt -from homeassistant.components.mqtt import debug_info +from homeassistant.components.mqtt import CONFIG_SCHEMA, debug_info from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA from homeassistant.components.mqtt.models import ReceiveMessage from homeassistant.const import ( @@ -1362,56 +1362,47 @@ async def test_setup_override_configuration(hass, caplog, tmp_path): assert calls_username_password_set[0][1] == "somepassword" -async def test_setup_manual_mqtt_with_platform_key(hass, caplog, tmp_path): +@patch("homeassistant.components.mqtt.PLATFORMS", []) +async def test_setup_manual_mqtt_with_platform_key(hass, caplog): """Test set up a manual MQTT item with a platform key.""" config = {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} - await help_test_setup_manual_entity_from_yaml( - hass, - caplog, - tmp_path, - "light", - config, - ) + with pytest.raises(AssertionError): + await help_test_setup_manual_entity_from_yaml(hass, "light", config) assert ( - "Invalid config for [light]: [platform] is an invalid option for [light]. " - "Check: light->platform. (See ?, line ?)" in caplog.text + "Invalid config for [mqtt]: [platform] is an invalid option for [mqtt]" + in caplog.text ) -async def test_setup_manual_mqtt_with_invalid_config(hass, caplog, tmp_path): +@patch("homeassistant.components.mqtt.PLATFORMS", []) +async def test_setup_manual_mqtt_with_invalid_config(hass, caplog): """Test set up a manual MQTT item with an invalid config.""" config = {"name": "test"} - await help_test_setup_manual_entity_from_yaml( - hass, - caplog, - tmp_path, - "light", - config, - ) + with pytest.raises(AssertionError): + await help_test_setup_manual_entity_from_yaml(hass, "light", config) assert ( - "Invalid config for [light]: required key not provided @ data['command_topic']." + "Invalid config for [mqtt]: required key not provided @ data['mqtt']['light'][0]['command_topic']." " Got None. (See ?, line ?)" in caplog.text ) -async def test_setup_manual_mqtt_empty_platform(hass, caplog, tmp_path): +@patch("homeassistant.components.mqtt.PLATFORMS", []) +async def test_setup_manual_mqtt_empty_platform(hass, caplog): """Test set up a manual MQTT platform without items.""" - config = None - await help_test_setup_manual_entity_from_yaml( - hass, - caplog, - tmp_path, - "light", - config, - ) + config = [] + await help_test_setup_manual_entity_from_yaml(hass, "light", config) assert "voluptuous.error.MultipleInvalid" not in caplog.text +@patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_setup_mqtt_client_protocol(hass): """Test MQTT client protocol setup.""" entry = MockConfigEntry( domain=mqtt.DOMAIN, - data={mqtt.CONF_BROKER: "test-broker", mqtt.config.CONF_PROTOCOL: "3.1"}, + data={ + mqtt.CONF_BROKER: "test-broker", + mqtt.config_integration.CONF_PROTOCOL: "3.1", + }, ) with patch("paho.mqtt.client.Client") as mock_client: mock_client.on_connect(return_value=0) @@ -2617,3 +2608,10 @@ async def test_one_deprecation_warning_per_platform( ): count += 1 assert count == 1 + + +async def test_config_schema_validation(hass): + """Test invalid platform options in the config schema do not pass the config validation.""" + config = {"mqtt": {"sensor": [{"some_illegal_topic": "mystate/topic/path"}]}} + with pytest.raises(vol.MultipleInvalid): + CONFIG_SCHEMA(config) From 65c1d4812a48e876a8f63fcb3d76e679c419bb27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Jun 2022 21:57:44 -0500 Subject: [PATCH 1674/3516] Fix calling permanent off with nexia (#73623) * Fix calling permanent off with nexia Changelog: https://github.com/bdraco/nexia/compare/1.0.1...1.0.2 Fixes #73610 * one more --- homeassistant/components/nexia/climate.py | 4 +++- homeassistant/components/nexia/manifest.json | 2 +- homeassistant/components/nexia/switch.py | 6 +++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 20fcf5c6b85..33ad91e1561 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -391,7 +391,9 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the system mode (Auto, Heat_Cool, Cool, Heat, etc).""" - if hvac_mode == HVACMode.AUTO: + if hvac_mode == HVACMode.OFF: + await self._zone.call_permanent_off() + elif hvac_mode == HVACMode.AUTO: await self._zone.call_return_to_schedule() await self._zone.set_mode(mode=OPERATION_MODE_AUTO) else: diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index f9ca21d9e0b..4bae2d9a15d 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==1.0.1"], + "requirements": ["nexia==1.0.2"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/homeassistant/components/nexia/switch.py b/homeassistant/components/nexia/switch.py index 380fea8c4a0..e242032c947 100644 --- a/homeassistant/components/nexia/switch.py +++ b/homeassistant/components/nexia/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any +from nexia.const import OPERATION_MODE_OFF from nexia.home import NexiaHome from nexia.thermostat import NexiaThermostat from nexia.zone import NexiaThermostatZone @@ -58,7 +59,10 @@ class NexiaHoldSwitch(NexiaThermostatZoneEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Enable permanent hold.""" - await self._zone.call_permanent_hold() + if self._zone.get_current_mode() == OPERATION_MODE_OFF: + await self._zone.call_permanent_off() + else: + await self._zone.call_permanent_hold() self._signal_zone_update() async def async_turn_off(self, **kwargs: Any) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index 20dd9ae64ff..dbffe63f9fa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1080,7 +1080,7 @@ nettigo-air-monitor==1.2.4 neurio==0.3.1 # homeassistant.components.nexia -nexia==1.0.1 +nexia==1.0.2 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fe7002d422a..73d4d24ca01 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -745,7 +745,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.2.4 # homeassistant.components.nexia -nexia==1.0.1 +nexia==1.0.2 # homeassistant.components.discord nextcord==2.0.0a8 From 9e4ee0d36d11a8b4ad58710cca9d6f3c48eaf1e5 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Fri, 17 Jun 2022 18:26:25 +0200 Subject: [PATCH 1675/3516] Don't verify ssl certificates for ssdp/upnp devices (#73647) --- homeassistant/components/ssdp/__init__.py | 2 +- homeassistant/components/upnp/device.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index e854472f21f..d221cb162f4 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -351,7 +351,7 @@ class Scanner: async def async_start(self) -> None: """Start the scanners.""" - session = async_get_clientsession(self.hass) + session = async_get_clientsession(self.hass, verify_ssl=False) requester = AiohttpSessionRequester(session, True, 10) self._description_cache = DescriptionCache(requester) diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 334d870939f..3a688b8571d 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -48,7 +48,7 @@ async def async_get_mac_address_from_host(hass: HomeAssistant, host: str) -> str async def async_create_device(hass: HomeAssistant, ssdp_location: str) -> Device: """Create UPnP/IGD device.""" - session = async_get_clientsession(hass) + session = async_get_clientsession(hass, verify_ssl=False) requester = AiohttpSessionRequester(session, with_sleep=True, timeout=20) factory = UpnpFactory(requester, disable_state_variable_validation=True) From eda2c8cb8f9f39817b3dceaaca8549b6d5e299d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Jun 2022 14:03:42 -0500 Subject: [PATCH 1676/3516] Retry on SenseAPIException during sense config entry setup (#73651) --- homeassistant/components/sense/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index 2e0ed4622e8..e938f7132e0 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -5,6 +5,7 @@ import logging from sense_energy import ( ASyncSenseable, + SenseAPIException, SenseAuthenticationException, SenseMFARequiredException, ) @@ -84,6 +85,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady( str(err) or "Timed out during authentication" ) from err + except SenseAPIException as err: + raise ConfigEntryNotReady(str(err)) from err sense_devices_data = SenseDevicesData() try: From 14c11cd13a96d310a0fe799dafb22920ae7362b0 Mon Sep 17 00:00:00 2001 From: Max Gashkov Date: Mon, 20 Jun 2022 16:05:28 +0900 Subject: [PATCH 1677/3516] Fix AmbiClimate services definition (#73668) --- homeassistant/components/ambiclimate/services.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ambiclimate/services.yaml b/homeassistant/components/ambiclimate/services.yaml index f75857e4d2e..e5532ae82f9 100644 --- a/homeassistant/components/ambiclimate/services.yaml +++ b/homeassistant/components/ambiclimate/services.yaml @@ -5,7 +5,7 @@ set_comfort_mode: description: > Enable comfort mode on your AC. fields: - Name: + name: description: > String with device name. required: true @@ -18,14 +18,14 @@ send_comfort_feedback: description: > Send feedback for comfort mode. fields: - Name: + name: description: > String with device name. required: true example: Bedroom selector: text: - Value: + value: description: > Send any of the following comfort values: too_hot, too_warm, bit_warm, comfortable, bit_cold, too_cold, freezing required: true @@ -38,14 +38,14 @@ set_temperature_mode: description: > Enable temperature mode on your AC. fields: - Name: + name: description: > String with device name. required: true example: Bedroom selector: text: - Value: + value: description: > Target value in celsius required: true From d211399056ea21c29b9170cfed71fe66e3110dcc Mon Sep 17 00:00:00 2001 From: micha91 Date: Mon, 20 Jun 2022 10:36:04 +0200 Subject: [PATCH 1678/3516] Update aiomusiccast (#73694) --- 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 86115e77988..8c0b55def69 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -3,7 +3,7 @@ "name": "MusicCast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast", - "requirements": ["aiomusiccast==0.14.3"], + "requirements": ["aiomusiccast==0.14.4"], "ssdp": [ { "manufacturer": "Yamaha Corporation" diff --git a/requirements_all.txt b/requirements_all.txt index dbffe63f9fa..47be523361f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -196,7 +196,7 @@ aiolyric==1.0.8 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.14.3 +aiomusiccast==0.14.4 # homeassistant.components.nanoleaf aionanoleaf==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 73d4d24ca01..aa7eaf87853 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -168,7 +168,7 @@ aiolyric==1.0.8 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.14.3 +aiomusiccast==0.14.4 # homeassistant.components.nanoleaf aionanoleaf==0.2.0 From 5c71de8055f0516ac0fc2656f3b72989e230bc3f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 20 Jun 2022 10:31:19 +0200 Subject: [PATCH 1679/3516] Fix CSRF token for UniFi (#73716) Bump aiounifi to v32 --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index e779dca22f0..d481f0d0fc4 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==31"], + "requirements": ["aiounifi==32"], "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 47be523361f..c42bbe6c076 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -256,7 +256,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==31 +aiounifi==32 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa7eaf87853..a1efb0f1941 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -225,7 +225,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==31 +aiounifi==32 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From 29f8493a0654f2279bdd21fe547bebc9a4be569f Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Wed, 22 Jun 2022 04:04:11 -0400 Subject: [PATCH 1680/3516] Insteon bug fixes (#73791) --- homeassistant/components/insteon/api/properties.py | 9 ++++----- homeassistant/components/insteon/manifest.json | 4 ++-- homeassistant/components/insteon/utils.py | 5 ++--- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- tests/components/insteon/test_api_properties.py | 4 ++-- 6 files changed, 14 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/insteon/api/properties.py b/homeassistant/components/insteon/api/properties.py index 47def71c1ab..8e697a88459 100644 --- a/homeassistant/components/insteon/api/properties.py +++ b/homeassistant/components/insteon/api/properties.py @@ -99,8 +99,6 @@ def get_properties(device: Device, show_advanced=False): continue prop_schema = get_schema(prop, name, device.groups) - if name == "momentary_delay": - print(prop_schema) if prop_schema is None: continue schema[name] = prop_schema @@ -216,7 +214,7 @@ async def websocket_write_properties( result = await device.async_write_config() await devices.async_save(workdir=hass.config.config_dir) - if result != ResponseStatus.SUCCESS: + if result not in [ResponseStatus.SUCCESS, ResponseStatus.RUN_ON_WAKE]: connection.send_message( websocket_api.error_message( msg[ID], "write_failed", "properties not written to device" @@ -244,9 +242,10 @@ async def websocket_load_properties( notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) return - result, _ = await device.async_read_config(read_aldb=False) + result = await device.async_read_config(read_aldb=False) await devices.async_save(workdir=hass.config.config_dir) - if result != ResponseStatus.SUCCESS: + + if result not in [ResponseStatus.SUCCESS, ResponseStatus.RUN_ON_WAKE]: connection.send_message( websocket_api.error_message( msg[ID], "load_failed", "properties not loaded from device" diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index c69f4f2cdf5..1be077a6b38 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -4,8 +4,8 @@ "documentation": "https://www.home-assistant.io/integrations/insteon", "dependencies": ["http", "websocket_api"], "requirements": [ - "pyinsteon==1.1.0", - "insteon-frontend-home-assistant==0.1.0" + "pyinsteon==1.1.1", + "insteon-frontend-home-assistant==0.1.1" ], "codeowners": ["@teharris1"], "dhcp": [ diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py index e8e34ee8e62..09375e7827a 100644 --- a/homeassistant/components/insteon/utils.py +++ b/homeassistant/components/insteon/utils.py @@ -196,9 +196,8 @@ def async_register_services(hass): for address in devices: device = devices[address] if device != devices.modem and device.cat != 0x03: - await device.aldb.async_load( - refresh=reload, callback=async_srv_save_devices - ) + await device.aldb.async_load(refresh=reload) + await async_srv_save_devices() async def async_srv_save_devices(): """Write the Insteon device configuration to file.""" diff --git a/requirements_all.txt b/requirements_all.txt index c42bbe6c076..b52f43924d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -885,7 +885,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.insteon -insteon-frontend-home-assistant==0.1.0 +insteon-frontend-home-assistant==0.1.1 # homeassistant.components.intellifire intellifire4py==1.0.2 @@ -1556,7 +1556,7 @@ pyialarmxr-homeassistant==1.0.18 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.0 +pyinsteon==1.1.1 # homeassistant.components.intesishome pyintesishome==1.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a1efb0f1941..1f70484f554 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -628,7 +628,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.insteon -insteon-frontend-home-assistant==0.1.0 +insteon-frontend-home-assistant==0.1.1 # homeassistant.components.intellifire intellifire4py==1.0.2 @@ -1044,7 +1044,7 @@ pyialarmxr-homeassistant==1.0.18 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.0 +pyinsteon==1.1.1 # homeassistant.components.ipma pyipma==2.0.5 diff --git a/tests/components/insteon/test_api_properties.py b/tests/components/insteon/test_api_properties.py index 7211402e343..9088b23f42a 100644 --- a/tests/components/insteon/test_api_properties.py +++ b/tests/components/insteon/test_api_properties.py @@ -401,7 +401,7 @@ async def test_load_properties(hass, hass_ws_client, kpl_properties_data): ) device = devices["33.33.33"] - device.async_read_config = AsyncMock(return_value=(1, 1)) + device.async_read_config = AsyncMock(return_value=1) with patch.object(insteon.api.properties, "devices", devices): await ws_client.send_json( {ID: 2, TYPE: "insteon/properties/load", DEVICE_ADDRESS: "33.33.33"} @@ -418,7 +418,7 @@ async def test_load_properties_failure(hass, hass_ws_client, kpl_properties_data ) device = devices["33.33.33"] - device.async_read_config = AsyncMock(return_value=(0, 0)) + device.async_read_config = AsyncMock(return_value=0) with patch.object(insteon.api.properties, "devices", devices): await ws_client.send_json( {ID: 2, TYPE: "insteon/properties/load", DEVICE_ADDRESS: "33.33.33"} From 30383e0102fb10d744830f767bf8b74faba6bb1b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Jun 2022 09:54:35 +0200 Subject: [PATCH 1681/3516] Fix Plugwise migration error (#73812) --- homeassistant/components/plugwise/gateway.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index 7eb2b1371d5..afa7451021e 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -113,7 +113,7 @@ def migrate_sensor_entities( # Migrating opentherm_outdoor_temperature to opentherm_outdoor_air_temperature sensor for device_id, device in coordinator.data.devices.items(): - if device["dev_class"] != "heater_central": + if device.get("dev_class") != "heater_central": continue old_unique_id = f"{device_id}-outdoor_temperature" From 505c4b0f770f60825ca7b2fce424bda0e37e55db Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Jun 2022 11:50:34 +0200 Subject: [PATCH 1682/3516] Fix MQTT tests for RC --- tests/components/mqtt/test_init.py | 35 +++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index e9625e0fdb2..eb392f8c4f8 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1362,35 +1362,50 @@ async def test_setup_override_configuration(hass, caplog, tmp_path): assert calls_username_password_set[0][1] == "somepassword" -@patch("homeassistant.components.mqtt.PLATFORMS", []) -async def test_setup_manual_mqtt_with_platform_key(hass, caplog): +async def test_setup_manual_mqtt_with_platform_key(hass, caplog, tmp_path): """Test set up a manual MQTT item with a platform key.""" config = {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} with pytest.raises(AssertionError): - await help_test_setup_manual_entity_from_yaml(hass, "light", config) + await help_test_setup_manual_entity_from_yaml( + hass, + caplog, + tmp_path, + "light", + config, + ) assert ( "Invalid config for [mqtt]: [platform] is an invalid option for [mqtt]" in caplog.text ) -@patch("homeassistant.components.mqtt.PLATFORMS", []) -async def test_setup_manual_mqtt_with_invalid_config(hass, caplog): +async def test_setup_manual_mqtt_with_invalid_config(hass, caplog, tmp_path): """Test set up a manual MQTT item with an invalid config.""" config = {"name": "test"} with pytest.raises(AssertionError): - await help_test_setup_manual_entity_from_yaml(hass, "light", config) + await help_test_setup_manual_entity_from_yaml( + hass, + caplog, + tmp_path, + "light", + config, + ) assert ( "Invalid config for [mqtt]: required key not provided @ data['mqtt']['light'][0]['command_topic']." " Got None. (See ?, line ?)" in caplog.text ) -@patch("homeassistant.components.mqtt.PLATFORMS", []) -async def test_setup_manual_mqtt_empty_platform(hass, caplog): +async def test_setup_manual_mqtt_empty_platform(hass, caplog, tmp_path): """Test set up a manual MQTT platform without items.""" - config = [] - await help_test_setup_manual_entity_from_yaml(hass, "light", config) + config = None + await help_test_setup_manual_entity_from_yaml( + hass, + caplog, + tmp_path, + "light", + config, + ) assert "voluptuous.error.MultipleInvalid" not in caplog.text From 08ff99a1e87e444a1dd70bebadb06d2a66d6caec Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Jun 2022 11:50:42 +0200 Subject: [PATCH 1683/3516] Bumped version to 2022.6.7 --- homeassistant/const.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 8f8a8008457..3bcf2a603b4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 6 -PATCH_VERSION: Final = "6" +PATCH_VERSION: Final = "7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/setup.cfg b/setup.cfg index 17e4901b2ff..ab5ab807491 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -version = 2022.6.6 +version = 2022.6.7 url = https://www.home-assistant.io/ [options] From 31af4b709ebea363b41808ac3dad5fb9034dd282 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 22 Jun 2022 12:48:23 +0200 Subject: [PATCH 1684/3516] Add FanEntity type hint checks to pylint plugin (#73801) * Add FanEntity type hint checks to pylint plugin * Add test * Add test * Review comments * Adjust tests * Rename variable * also test keyword_only args * Use docstrings * Fix tests * Better return type --- pylint/plugins/hass_enforce_type_hints.py | 137 +++++++++++++++++++++- tests/pylint/test_enforce_type_hints.py | 70 +++++++++++ 2 files changed, 205 insertions(+), 2 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index a1bf260e968..307510c6621 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -21,10 +21,12 @@ class TypeHintMatch: function_name: str return_type: list[str] | str | None | object - # arg_types is for positional arguments arg_types: dict[int, str] | None = None - # kwarg_types is for the special case `**kwargs` + """arg_types is for positional arguments""" + named_arg_types: dict[str, str] | None = None + """named_arg_types is for named or keyword arguments""" kwargs_type: str | None = None + """kwargs_type is for the special case `**kwargs`""" check_return_type_inheritance: bool = False @@ -448,6 +450,111 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { # Overriding properties and functions are normally checked by mypy, and will only # be checked by pylint when --ignore-missing-annotations is False _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { + "fan": [ + ClassTypeHintMatch( + base_class="FanEntity", + matches=[ + TypeHintMatch( + function_name="is_on", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="percentage", + return_type=["int", None], + ), + TypeHintMatch( + function_name="speed_count", + return_type="int", + ), + TypeHintMatch( + function_name="percentage_step", + return_type="float", + ), + TypeHintMatch( + function_name="current_direction", + return_type=["str", None], + ), + TypeHintMatch( + function_name="oscillating", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="capability_attributes", + return_type="dict[str]", + ), + TypeHintMatch( + function_name="supported_features", + return_type="int", + ), + TypeHintMatch( + function_name="preset_mode", + return_type=["str", None], + ), + TypeHintMatch( + function_name="preset_modes", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="set_percentage", + arg_types={1: "int"}, + return_type=None, + ), + TypeHintMatch( + function_name="async_set_percentage", + arg_types={1: "int"}, + return_type=None, + ), + TypeHintMatch( + function_name="set_preset_mode", + arg_types={1: "str"}, + return_type=None, + ), + TypeHintMatch( + function_name="async_set_preset_mode", + arg_types={1: "str"}, + return_type=None, + ), + TypeHintMatch( + function_name="set_direction", + arg_types={1: "str"}, + return_type=None, + ), + TypeHintMatch( + function_name="async_set_direction", + arg_types={1: "str"}, + return_type=None, + ), + TypeHintMatch( + function_name="turn_on", + named_arg_types={ + "percentage": "int | None", + "preset_mode": "str | None", + }, + kwargs_type="Any", + return_type=None, + ), + TypeHintMatch( + function_name="async_turn_on", + named_arg_types={ + "percentage": "int | None", + "preset_mode": "str | None", + }, + kwargs_type="Any", + return_type=None, + ), + TypeHintMatch( + function_name="oscillate", + arg_types={1: "bool"}, + return_type=None, + ), + TypeHintMatch( + function_name="async_oscillate", + arg_types={1: "bool"}, + return_type=None, + ), + ], + ), + ], "lock": [ ClassTypeHintMatch( base_class="LockEntity", @@ -619,6 +726,21 @@ def _get_all_annotations(node: nodes.FunctionDef) -> list[nodes.NodeNG | None]: return annotations +def _get_named_annotation( + node: nodes.FunctionDef, key: str +) -> tuple[nodes.NodeNG, nodes.NodeNG] | tuple[None, None]: + args = node.args + for index, arg_node in enumerate(args.args): + if key == arg_node.name: + return arg_node, args.annotations[index] + + for index, arg_node in enumerate(args.kwonlyargs): + if key == arg_node.name: + return arg_node, args.kwonlyargs_annotations[index] + + return None, None + + def _has_valid_annotations( annotations: list[nodes.NodeNG | None], ) -> bool: @@ -742,6 +864,17 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] args=(key + 1, expected_type), ) + # Check that all keyword arguments are correctly annotated. + if match.named_arg_types is not None: + for arg_name, expected_type in match.named_arg_types.items(): + arg_node, annotation = _get_named_annotation(node, arg_name) + if arg_node and not _is_valid_type(expected_type, annotation): + self.add_message( + "hass-argument-type", + node=arg_node, + args=(arg_name, expected_type), + ) + # Check that kwargs is correctly annotated. if match.kwargs_type and not _is_valid_type( match.kwargs_type, node.args.kwargannotation diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 262ff93afa8..54c7cf6ec4c 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -565,3 +565,73 @@ def test_ignore_invalid_entity_properties( with assert_no_messages(linter): type_hint_checker.visit_classdef(class_node) + + +def test_named_arguments( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Check missing entity properties when ignore_missing_annotations is False.""" + # Set bypass option + type_hint_checker.config.ignore_missing_annotations = False + + class_node, func_node, percentage_node, preset_mode_node = astroid.extract_node( + """ + class FanEntity(): + pass + + class MyFan( #@ + FanEntity + ): + async def async_turn_on( #@ + self, + percentage, #@ + *, + preset_mode: str, #@ + **kwargs + ) -> bool: + pass + """, + "homeassistant.components.pylint_test.fan", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_adds_messages( + linter, + pylint.testutils.MessageTest( + msg_id="hass-argument-type", + node=percentage_node, + args=("percentage", "int | None"), + line=10, + col_offset=8, + end_line=10, + end_col_offset=18, + ), + pylint.testutils.MessageTest( + msg_id="hass-argument-type", + node=preset_mode_node, + args=("preset_mode", "str | None"), + line=12, + col_offset=8, + end_line=12, + end_col_offset=24, + ), + pylint.testutils.MessageTest( + msg_id="hass-argument-type", + node=func_node, + args=("kwargs", "Any"), + line=8, + col_offset=4, + end_line=8, + end_col_offset=27, + ), + pylint.testutils.MessageTest( + msg_id="hass-return-type", + node=func_node, + args="None", + line=8, + col_offset=4, + end_line=8, + end_col_offset=27, + ), + ): + type_hint_checker.visit_classdef(class_node) From 03246d2649a51598bebb330394ca7e56eb2c45d0 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 22 Jun 2022 21:38:44 +1000 Subject: [PATCH 1685/3516] Use ha-av instead of av and bump to v10.0.0b3 (#73789) * Use ha-av instead of av and bump to v10.0.0b1 * Change generic * Use v10.0.0b2 * Use v10.0.0b3 --- homeassistant/components/generic/manifest.json | 2 +- homeassistant/components/stream/manifest.json | 2 +- requirements_all.txt | 8 ++++---- requirements_test_all.txt | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index c590ddfffcd..3e8e7717a10 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -2,7 +2,7 @@ "domain": "generic", "name": "Generic Camera", "config_flow": true, - "requirements": ["av==9.2.0", "pillow==9.1.1"], + "requirements": ["ha-av==10.0.0b3", "pillow==9.1.1"], "documentation": "https://www.home-assistant.io/integrations/generic", "codeowners": ["@davet2001"], "iot_class": "local_push" diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index f6e00f7c599..eb525700bb0 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -2,7 +2,7 @@ "domain": "stream", "name": "Stream", "documentation": "https://www.home-assistant.io/integrations/stream", - "requirements": ["PyTurboJPEG==1.6.6", "av==9.2.0"], + "requirements": ["PyTurboJPEG==1.6.6", "ha-av==10.0.0b3"], "dependencies": ["http"], "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"], "quality_scale": "internal", diff --git a/requirements_all.txt b/requirements_all.txt index 8eada81b4e9..9a4a1619630 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -356,10 +356,6 @@ auroranoaa==0.0.2 # homeassistant.components.aurora_abb_powerone aurorapy==0.2.7 -# homeassistant.components.generic -# homeassistant.components.stream -av==9.2.0 - # homeassistant.components.avea # avea==1.5.1 @@ -779,6 +775,10 @@ gstreamer-player==1.1.2 # homeassistant.components.profiler guppy3==3.1.2 +# homeassistant.components.generic +# homeassistant.components.stream +ha-av==10.0.0b3 + # homeassistant.components.ffmpeg ha-ffmpeg==3.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12a89392d9e..5c59af04700 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -292,10 +292,6 @@ auroranoaa==0.0.2 # homeassistant.components.aurora_abb_powerone aurorapy==0.2.7 -# homeassistant.components.generic -# homeassistant.components.stream -av==9.2.0 - # homeassistant.components.axis axis==44 @@ -558,6 +554,10 @@ growattServer==1.2.2 # homeassistant.components.profiler guppy3==3.1.2 +# homeassistant.components.generic +# homeassistant.components.stream +ha-av==10.0.0b3 + # homeassistant.components.ffmpeg ha-ffmpeg==3.0.2 From fb2a3ae13522560fcb7ea68ebf3746bbda571284 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Jun 2022 13:39:19 +0200 Subject: [PATCH 1686/3516] Update sentry-sdk to 1.6.0 (#73819) --- 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 b76318f046d..3f01b7bc5f0 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.5.12"], + "requirements": ["sentry-sdk==1.6.0"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 9a4a1619630..aa93b7bae4c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2147,7 +2147,7 @@ sendgrid==6.8.2 sense_energy==0.10.4 # homeassistant.components.sentry -sentry-sdk==1.5.12 +sentry-sdk==1.6.0 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c59af04700..1256de737ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1422,7 +1422,7 @@ securetar==2022.2.0 sense_energy==0.10.4 # homeassistant.components.sentry -sentry-sdk==1.5.12 +sentry-sdk==1.6.0 # homeassistant.components.sharkiq sharkiq==0.0.1 From 33a84838b40b2cb41b1b37401a19bbc1069f5ab4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 22 Jun 2022 13:41:28 +0200 Subject: [PATCH 1687/3516] Fix type hints in zha smartenergy channel (#73775) * Fix type hints in zha smartenergy channel * Adjust unit_of_measurement --- .../zha/core/channels/smartenergy.py | 22 +++++++++++-------- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 731ec003011..66d3e3d6810 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -122,8 +122,8 @@ class Metering(ZigbeeChannel): def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: """Initialize Metering.""" super().__init__(cluster, ch_pool) - self._format_spec = None - self._summa_format = None + self._format_spec: str | None = None + self._summa_format: str | None = None @property def divisor(self) -> int: @@ -131,7 +131,7 @@ class Metering(ZigbeeChannel): return self.cluster.get("divisor") or 1 @property - def device_type(self) -> int | None: + def device_type(self) -> str | int | None: """Return metering device type.""" dev_type = self.cluster.get("metering_device_type") if dev_type is None: @@ -154,7 +154,7 @@ class Metering(ZigbeeChannel): return self.DeviceStatusDefault(status) @property - def unit_of_measurement(self) -> str: + def unit_of_measurement(self) -> int: """Return unit of measurement.""" return self.cluster.get("unit_of_measure") @@ -210,18 +210,22 @@ class Metering(ZigbeeChannel): return f"{{:0{width}.{r_digits}f}}" - def _formatter_function(self, selector: FormatSelector, value: int) -> int | float: + def _formatter_function( + self, selector: FormatSelector, value: int + ) -> int | float | str: """Return formatted value for display.""" - value = value * self.multiplier / self.divisor + value_float = value * self.multiplier / self.divisor if self.unit_of_measurement == 0: # Zigbee spec power unit is kW, but we show the value in W - value_watt = value * 1000 + value_watt = value_float * 1000 if value_watt < 100: return round(value_watt, 1) return round(value_watt) if selector == self.FormatSelector.SUMMATION: - return self._summa_format.format(value).lstrip() - return self._format_spec.format(value).lstrip() + assert self._summa_format + return self._summa_format.format(value_float).lstrip() + assert self._format_spec + return self._format_spec.format(value_float).lstrip() demand_formatter = partialmethod(_formatter_function, FormatSelector.DEMAND) summa_formatter = partialmethod(_formatter_function, FormatSelector.SUMMATION) diff --git a/mypy.ini b/mypy.ini index 26314c5bcad..19241872474 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2997,9 +2997,6 @@ ignore_errors = true [mypy-homeassistant.components.zha.core.channels.security] ignore_errors = true -[mypy-homeassistant.components.zha.core.channels.smartenergy] -ignore_errors = true - [mypy-homeassistant.components.zha.core.device] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 6a4ff9d8cdf..f319ccb5235 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -148,7 +148,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.zha.core.channels.homeautomation", "homeassistant.components.zha.core.channels.hvac", "homeassistant.components.zha.core.channels.security", - "homeassistant.components.zha.core.channels.smartenergy", "homeassistant.components.zha.core.device", "homeassistant.components.zha.core.discovery", "homeassistant.components.zha.core.gateway", From 754fe86dd988b51a20229f8d88dfcdecb60e90d8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 22 Jun 2022 14:15:44 +0200 Subject: [PATCH 1688/3516] Add fan to strict typing (#73820) * Add fan to strict typing * Adjust state_attributes * Adjust capability_attributes * Adjust is_on * Adjust vallox component * Revert "Adjust is_on" This reverts commit 48d207f250f99d8126702342c05a6be6e877e4d5. * Fix is_on property --- .strict-typing | 1 + homeassistant/components/fan/__init__.py | 24 +++++++++++++----------- homeassistant/components/vallox/fan.py | 2 +- mypy.ini | 11 +++++++++++ 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/.strict-typing b/.strict-typing index 77f7b6f50c2..1832a83641a 100644 --- a/.strict-typing +++ b/.strict-typing @@ -87,6 +87,7 @@ homeassistant.components.emulated_hue.* homeassistant.components.esphome.* homeassistant.components.energy.* homeassistant.components.evil_genius_labs.* +homeassistant.components.fan.* homeassistant.components.fastdotcom.* homeassistant.components.filesize.* homeassistant.components.fitbit.* diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 5eb3bedabd5..8f6585f6535 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -7,7 +7,7 @@ from enum import IntEnum import functools as ft import logging import math -from typing import final +from typing import Any, final import voluptuous as vol @@ -80,9 +80,11 @@ class NotValidPresetModeError(ValueError): @bind_hass -def is_on(hass, entity_id: str) -> bool: +def is_on(hass: HomeAssistant, entity_id: str) -> bool: """Return if the fans are on based on the statemachine.""" - return hass.states.get(entity_id).state == STATE_ON + entity = hass.states.get(entity_id) + assert entity + return entity.state == STATE_ON async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -235,7 +237,7 @@ class FanEntity(ToggleEntity): """Set new preset mode.""" await self.hass.async_add_executor_job(self.set_preset_mode, preset_mode) - def _valid_preset_mode_or_raise(self, preset_mode): + def _valid_preset_mode_or_raise(self, preset_mode: str) -> None: """Raise NotValidPresetModeError on invalid preset_mode.""" preset_modes = self.preset_modes if not preset_modes or preset_mode not in preset_modes: @@ -247,7 +249,7 @@ class FanEntity(ToggleEntity): """Set the direction of the fan.""" raise NotImplementedError() - async def async_set_direction(self, direction: str): + async def async_set_direction(self, direction: str) -> None: """Set the direction of the fan.""" await self.hass.async_add_executor_job(self.set_direction, direction) @@ -255,7 +257,7 @@ class FanEntity(ToggleEntity): self, percentage: int | None = None, preset_mode: str | None = None, - **kwargs, + **kwargs: Any, ) -> None: """Turn on the fan.""" raise NotImplementedError() @@ -264,7 +266,7 @@ class FanEntity(ToggleEntity): self, percentage: int | None = None, preset_mode: str | None = None, - **kwargs, + **kwargs: Any, ) -> None: """Turn on the fan.""" await self.hass.async_add_executor_job( @@ -280,12 +282,12 @@ class FanEntity(ToggleEntity): """Oscillate the fan.""" raise NotImplementedError() - async def async_oscillate(self, oscillating: bool): + async def async_oscillate(self, oscillating: bool) -> None: """Oscillate the fan.""" await self.hass.async_add_executor_job(self.oscillate, oscillating) @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if the entity is on.""" return ( self.percentage is not None and self.percentage > 0 @@ -321,7 +323,7 @@ class FanEntity(ToggleEntity): return self._attr_oscillating @property - def capability_attributes(self): + def capability_attributes(self) -> dict[str, list[str] | None]: """Return capability attributes.""" attrs = {} @@ -335,7 +337,7 @@ class FanEntity(ToggleEntity): @final @property - def state_attributes(self) -> dict: + def state_attributes(self) -> dict[str, float | str | None]: """Return optional state attributes.""" data: dict[str, float | str | None] = {} supported_features = self.supported_features diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index 6872acbb5b7..4ba7d2d88fd 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -132,7 +132,7 @@ class ValloxFan(ValloxEntity, FanEntity): Returns true if the mode has been changed, false otherwise. """ try: - self._valid_preset_mode_or_raise(preset_mode) # type: ignore[no-untyped-call] + self._valid_preset_mode_or_raise(preset_mode) except NotValidPresetModeError as err: _LOGGER.error(err) diff --git a/mypy.ini b/mypy.ini index 19241872474..8dca7403eff 100644 --- a/mypy.ini +++ b/mypy.ini @@ -720,6 +720,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.fan.*] +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.fastdotcom.*] check_untyped_defs = true disallow_incomplete_defs = true From 54591b8ca1b928c883beb2aa080361e236d6d704 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Wed, 22 Jun 2022 14:24:16 +0200 Subject: [PATCH 1689/3516] BMW Connected Drive: Handle HTTP 429 issues better (#73675) Co-authored-by: rikroe --- .../bmw_connected_drive/config_flow.py | 21 +++++++++++----- .../bmw_connected_drive/coordinator.py | 22 +++++++++++++---- .../bmw_connected_drive/test_config_flow.py | 24 +++++++++++++++---- 3 files changed, 52 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/config_flow.py b/homeassistant/components/bmw_connected_drive/config_flow.py index c07be4c8849..3994b0732a8 100644 --- a/homeassistant/components/bmw_connected_drive/config_flow.py +++ b/homeassistant/components/bmw_connected_drive/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from bimmer_connected.account import MyBMWAccount +from bimmer_connected.api.authentication import MyBMWAuthentication from bimmer_connected.api.regions import get_region_from_name from httpx import HTTPError import voluptuous as vol @@ -14,7 +14,7 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from . import DOMAIN -from .const import CONF_ALLOWED_REGIONS, CONF_READ_ONLY +from .const import CONF_ALLOWED_REGIONS, CONF_READ_ONLY, CONF_REFRESH_TOKEN DATA_SCHEMA = vol.Schema( { @@ -32,19 +32,22 @@ async def validate_input( Data has the keys from DATA_SCHEMA with values provided by the user. """ - account = MyBMWAccount( + auth = MyBMWAuthentication( data[CONF_USERNAME], data[CONF_PASSWORD], get_region_from_name(data[CONF_REGION]), ) try: - await account.get_vehicles() + await auth.login() except HTTPError as ex: raise CannotConnect from ex # Return info that you want to store in the config entry. - return {"title": f"{data[CONF_USERNAME]}{data.get(CONF_SOURCE, '')}"} + retval = {"title": f"{data[CONF_USERNAME]}{data.get(CONF_SOURCE, '')}"} + if auth.refresh_token: + retval[CONF_REFRESH_TOKEN] = auth.refresh_token + return retval class BMWConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -70,7 +73,13 @@ class BMWConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" if info: - return self.async_create_entry(title=info["title"], data=user_input) + return self.async_create_entry( + title=info["title"], + data={ + **user_input, + CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN), + }, + ) return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors diff --git a/homeassistant/components/bmw_connected_drive/coordinator.py b/homeassistant/components/bmw_connected_drive/coordinator.py index 1443a3e1e29..e5a968b47fd 100644 --- a/homeassistant/components/bmw_connected_drive/coordinator.py +++ b/homeassistant/components/bmw_connected_drive/coordinator.py @@ -7,7 +7,7 @@ import logging from bimmer_connected.account import MyBMWAccount from bimmer_connected.api.regions import get_region_from_name from bimmer_connected.models import GPSPosition -from httpx import HTTPError, TimeoutException +from httpx import HTTPError, HTTPStatusError, TimeoutException from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME @@ -16,7 +16,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN -SCAN_INTERVAL = timedelta(seconds=300) +DEFAULT_SCAN_INTERVAL_SECONDS = 300 +SCAN_INTERVAL = timedelta(seconds=DEFAULT_SCAN_INTERVAL_SECONDS) _LOGGER = logging.getLogger(__name__) @@ -53,8 +54,18 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator): try: await self.account.get_vehicles() - except (HTTPError, TimeoutException) as err: - self._update_config_entry_refresh_token(None) + except (HTTPError, HTTPStatusError, TimeoutException) as err: + if isinstance(err, HTTPStatusError) and err.response.status_code == 429: + # Increase scan interval to not jump to not bring up the issue next time + self.update_interval = timedelta( + seconds=DEFAULT_SCAN_INTERVAL_SECONDS * 3 + ) + if isinstance(err, HTTPStatusError) and err.response.status_code in ( + 401, + 403, + ): + # Clear refresh token only on issues with authorization + self._update_config_entry_refresh_token(None) raise UpdateFailed(f"Error communicating with BMW API: {err}") from err if self.account.refresh_token != old_refresh_token: @@ -65,6 +76,9 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator): self.account.refresh_token, ) + # Reset scan interval after successful update + self.update_interval = timedelta(seconds=DEFAULT_SCAN_INTERVAL_SECONDS) + def _update_config_entry_refresh_token(self, refresh_token: str | None) -> None: """Update or delete the refresh_token in the Config Entry.""" data = { diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index d3c7c64dc99..10178c22de8 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -1,19 +1,32 @@ """Test the for the BMW Connected Drive config flow.""" from unittest.mock import patch +from bimmer_connected.api.authentication import MyBMWAuthentication from httpx import HTTPError from homeassistant import config_entries, data_entry_flow from homeassistant.components.bmw_connected_drive.config_flow import DOMAIN -from homeassistant.components.bmw_connected_drive.const import CONF_READ_ONLY +from homeassistant.components.bmw_connected_drive.const import ( + CONF_READ_ONLY, + CONF_REFRESH_TOKEN, +) from homeassistant.const import CONF_USERNAME from . import FIXTURE_CONFIG_ENTRY, FIXTURE_USER_INPUT from tests.common import MockConfigEntry -FIXTURE_COMPLETE_ENTRY = FIXTURE_USER_INPUT.copy() -FIXTURE_IMPORT_ENTRY = FIXTURE_USER_INPUT.copy() +FIXTURE_REFRESH_TOKEN = "SOME_REFRESH_TOKEN" +FIXTURE_COMPLETE_ENTRY = { + **FIXTURE_USER_INPUT, + CONF_REFRESH_TOKEN: FIXTURE_REFRESH_TOKEN, +} +FIXTURE_IMPORT_ENTRY = {**FIXTURE_USER_INPUT, CONF_REFRESH_TOKEN: None} + + +def login_sideeffect(self: MyBMWAuthentication): + """Mock logging in and setting a refresh token.""" + self.refresh_token = FIXTURE_REFRESH_TOKEN async def test_show_form(hass): @@ -50,8 +63,9 @@ async def test_connection_error(hass): async def test_full_user_flow_implementation(hass): """Test registering an integration and finishing flow works.""" with patch( - "bimmer_connected.account.MyBMWAccount.get_vehicles", - return_value=[], + "bimmer_connected.api.authentication.MyBMWAuthentication.login", + side_effect=login_sideeffect, + autospec=True, ), patch( "homeassistant.components.bmw_connected_drive.async_setup_entry", return_value=True, From 19b2b330377d5afa4368aa4767c598347d5ab14a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Jun 2022 09:03:18 -0500 Subject: [PATCH 1690/3516] Speed up subscribing to mqtt topics on connect (#73685) * Speed up subscribing to mqtt topics * update tests * Remove extra function wrapper * Recover debug logging for subscriptions * Small changes and test * Update homeassistant/components/mqtt/client.py * Update client.py Co-authored-by: jbouwh Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/client.py | 72 ++++++++++++++++++------- tests/components/mqtt/test_init.py | 24 +++++++-- 2 files changed, 74 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 66699372516..d676c128260 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Iterable from functools import lru_cache, partial, wraps import inspect from itertools import groupby @@ -430,7 +430,7 @@ class MQTT: # Only subscribe if currently connected. if self.connected: self._last_subscribe = time.time() - await self._async_perform_subscription(topic, qos) + await self._async_perform_subscriptions(((topic, qos),)) @callback def async_remove() -> None: @@ -464,16 +464,37 @@ class MQTT: _raise_on_error(result) await self._wait_for_mid(mid) - async def _async_perform_subscription(self, topic: str, qos: int) -> None: - """Perform a paho-mqtt subscription.""" + async def _async_perform_subscriptions( + self, subscriptions: Iterable[tuple[str, int]] + ) -> None: + """Perform MQTT client subscriptions.""" + + def _process_client_subscriptions() -> list[tuple[int, int]]: + """Initiate all subscriptions on the MQTT client and return the results.""" + subscribe_result_list = [] + for topic, qos in subscriptions: + result, mid = self._mqttc.subscribe(topic, qos) + subscribe_result_list.append((result, mid)) + _LOGGER.debug("Subscribing to %s, mid: %s", topic, mid) + return subscribe_result_list + async with self._paho_lock: - result: int | None = None - result, mid = await self.hass.async_add_executor_job( - self._mqttc.subscribe, topic, qos + results = await self.hass.async_add_executor_job( + _process_client_subscriptions ) - _LOGGER.debug("Subscribing to %s, mid: %s", topic, mid) - _raise_on_error(result) - await self._wait_for_mid(mid) + + tasks = [] + errors = [] + for result, mid in results: + if result == 0: + tasks.append(self._wait_for_mid(mid)) + else: + errors.append(result) + + if tasks: + await asyncio.gather(*tasks) + if errors: + _raise_on_errors(errors) def _mqtt_on_connect(self, _mqttc, _userdata, _flags, result_code: int) -> None: """On connect callback. @@ -502,10 +523,16 @@ class MQTT: # Group subscriptions to only re-subscribe once for each topic. keyfunc = attrgetter("topic") - for topic, subs in groupby(sorted(self.subscriptions, key=keyfunc), keyfunc): - # Re-subscribe with the highest requested qos - max_qos = max(subscription.qos for subscription in subs) - self.hass.add_job(self._async_perform_subscription, topic, max_qos) + self.hass.add_job( + self._async_perform_subscriptions, + [ + # Re-subscribe with the highest requested qos + (topic, max(subscription.qos for subscription in subs)) + for topic, subs in groupby( + sorted(self.subscriptions, key=keyfunc), keyfunc + ) + ], + ) if ( CONF_BIRTH_MESSAGE in self.conf @@ -638,15 +665,22 @@ class MQTT: ) -def _raise_on_error(result_code: int | None) -> None: +def _raise_on_errors(result_codes: Iterable[int | None]) -> None: """Raise error if error result.""" # pylint: disable-next=import-outside-toplevel import paho.mqtt.client as mqtt - if result_code is not None and result_code != 0: - raise HomeAssistantError( - f"Error talking to MQTT: {mqtt.error_string(result_code)}" - ) + if messages := [ + mqtt.error_string(result_code) + for result_code in result_codes + if result_code != 0 + ]: + raise HomeAssistantError(f"Error talking to MQTT: {', '.join(messages)}") + + +def _raise_on_error(result_code: int | None) -> None: + """Raise error if error result.""" + _raise_on_errors((result_code,)) def _matcher_for_topic(subscription: str) -> Any: diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index a29f1fd88ef..b435798c241 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1312,6 +1312,20 @@ async def test_publish_error(hass, caplog): assert "Failed to connect to MQTT server: Out of memory." in caplog.text +async def test_subscribe_error( + hass, caplog, mqtt_mock_entry_no_yaml_config, mqtt_client_mock +): + """Test publish error.""" + await mqtt_mock_entry_no_yaml_config() + mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 0) + await hass.async_block_till_done() + with pytest.raises(HomeAssistantError): + # simulate client is not connected error before subscribing + mqtt_client_mock.subscribe.side_effect = lambda *args: (4, None) + await mqtt.async_subscribe(hass, "some-topic", lambda *args: 0) + await hass.async_block_till_done() + + async def test_handle_message_callback( hass, caplog, mqtt_mock_entry_no_yaml_config, mqtt_client_mock ): @@ -1424,6 +1438,7 @@ async def test_setup_mqtt_client_protocol(hass): @patch("homeassistant.components.mqtt.client.TIMEOUT_ACK", 0.2) +@patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_handle_mqtt_timeout_on_callback(hass, caplog): """Test publish without receiving an ACK callback.""" mid = 0 @@ -1764,9 +1779,12 @@ async def test_mqtt_subscribes_topics_on_connect( assert mqtt_client_mock.disconnect.call_count == 0 - expected = {"topic/test": 0, "home/sensor": 2, "still/pending": 1} - calls = {call[1][1]: call[1][2] for call in hass.add_job.mock_calls} - assert calls == expected + assert len(hass.add_job.mock_calls) == 1 + assert set(hass.add_job.mock_calls[0][1][1]) == { + ("home/sensor", 2), + ("still/pending", 1), + ("topic/test", 0), + } async def test_setup_entry_with_config_override( From 0461ec156648486416334574054467b19e9739a0 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 22 Jun 2022 11:09:21 -0400 Subject: [PATCH 1691/3516] Fix auth_sign_path with query params (take 2) (#73829) --- homeassistant/components/http/auth.py | 22 +++++- tests/components/http/test_auth.py | 103 ++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index dab6abede4c..09ef6e13e03 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -7,11 +7,11 @@ from ipaddress import ip_address import logging import secrets from typing import Final -from urllib.parse import unquote from aiohttp import hdrs from aiohttp.web import Application, Request, StreamResponse, middleware import jwt +from yarl import URL from homeassistant.auth.const import GROUP_ID_READ_ONLY from homeassistant.auth.models import User @@ -29,6 +29,7 @@ _LOGGER = logging.getLogger(__name__) DATA_API_PASSWORD: Final = "api_password" DATA_SIGN_SECRET: Final = "http.auth.sign_secret" SIGN_QUERY_PARAM: Final = "authSig" +SAFE_QUERY_PARAMS: Final = ["height", "width"] STORAGE_VERSION = 1 STORAGE_KEY = "http.auth" @@ -57,18 +58,26 @@ def async_sign_path( else: refresh_token_id = hass.data[STORAGE_KEY] + url = URL(path) now = dt_util.utcnow() + params = dict(sorted(url.query.items())) + for param in SAFE_QUERY_PARAMS: + params.pop(param, None) encoded = jwt.encode( { "iss": refresh_token_id, - "path": unquote(path), + "path": url.path, + "params": params, "iat": now, "exp": now + expiration, }, secret, algorithm="HS256", ) - return f"{path}?{SIGN_QUERY_PARAM}={encoded}" + + params[SIGN_QUERY_PARAM] = encoded + url = url.with_query(params) + return f"{url.path}?{url.query_string}" @callback @@ -176,6 +185,13 @@ async def async_setup_auth(hass: HomeAssistant, app: Application) -> None: if claims["path"] != request.path: return False + params = dict(sorted(request.query.items())) + del params[SIGN_QUERY_PARAM] + for param in SAFE_QUERY_PARAMS: + params.pop(param, None) + if claims["params"] != params: + return False + refresh_token = await hass.auth.async_get_refresh_token(claims["iss"]) if refresh_token is None: diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 4a2e1e8aed3..a06a696e994 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -17,6 +17,7 @@ from homeassistant.components import websocket_api from homeassistant.components.http.auth import ( CONTENT_USER_NAME, DATA_SIGN_SECRET, + SIGN_QUERY_PARAM, STORAGE_KEY, async_setup_auth, async_sign_path, @@ -294,6 +295,108 @@ async def test_auth_access_signed_path_with_refresh_token( assert req.status == HTTPStatus.UNAUTHORIZED +async def test_auth_access_signed_path_with_query_param( + hass, app, aiohttp_client, hass_access_token +): + """Test access with signed url and query params.""" + app.router.add_post("/", mock_handler) + app.router.add_get("/another_path", mock_handler) + await async_setup_auth(hass, app) + client = await aiohttp_client(app) + + refresh_token = await hass.auth.async_validate_access_token(hass_access_token) + + signed_path = async_sign_path( + hass, "/?test=test", timedelta(seconds=5), refresh_token_id=refresh_token.id + ) + + req = await client.get(signed_path) + assert req.status == HTTPStatus.OK + data = await req.json() + assert data["user_id"] == refresh_token.user.id + + +async def test_auth_access_signed_path_with_query_param_order( + hass, app, aiohttp_client, hass_access_token +): + """Test access with signed url and query params different order.""" + app.router.add_post("/", mock_handler) + app.router.add_get("/another_path", mock_handler) + await async_setup_auth(hass, app) + client = await aiohttp_client(app) + + refresh_token = await hass.auth.async_validate_access_token(hass_access_token) + + signed_path = async_sign_path( + hass, + "/?test=test&foo=bar", + timedelta(seconds=5), + refresh_token_id=refresh_token.id, + ) + url = yarl.URL(signed_path) + signed_path = f"{url.path}?{SIGN_QUERY_PARAM}={url.query.get(SIGN_QUERY_PARAM)}&foo=bar&test=test" + + req = await client.get(signed_path) + assert req.status == HTTPStatus.OK + data = await req.json() + assert data["user_id"] == refresh_token.user.id + + +async def test_auth_access_signed_path_with_query_param_safe_param( + hass, app, aiohttp_client, hass_access_token +): + """Test access with signed url and changing a safe param.""" + app.router.add_post("/", mock_handler) + app.router.add_get("/another_path", mock_handler) + await async_setup_auth(hass, app) + client = await aiohttp_client(app) + + refresh_token = await hass.auth.async_validate_access_token(hass_access_token) + + signed_path = async_sign_path( + hass, + "/?test=test&foo=bar", + timedelta(seconds=5), + refresh_token_id=refresh_token.id, + ) + signed_path = f"{signed_path}&width=100" + + req = await client.get(signed_path) + assert req.status == HTTPStatus.OK + data = await req.json() + assert data["user_id"] == refresh_token.user.id + + +@pytest.mark.parametrize( + "base_url,test_url", + [ + ("/?test=test", "/?test=test&foo=bar"), + ("/", "/?test=test"), + ("/?test=test&foo=bar", "/?test=test&foo=baz"), + ("/?test=test&foo=bar", "/?test=test"), + ], +) +async def test_auth_access_signed_path_with_query_param_tamper( + hass, app, aiohttp_client, hass_access_token, base_url: str, test_url: str +): + """Test access with signed url and query params that have been tampered with.""" + app.router.add_post("/", mock_handler) + app.router.add_get("/another_path", mock_handler) + await async_setup_auth(hass, app) + client = await aiohttp_client(app) + + refresh_token = await hass.auth.async_validate_access_token(hass_access_token) + + signed_path = async_sign_path( + hass, base_url, timedelta(seconds=5), refresh_token_id=refresh_token.id + ) + url = yarl.URL(signed_path) + token = url.query.get(SIGN_QUERY_PARAM) + + req = await client.get(f"{test_url}&{SIGN_QUERY_PARAM}={token}") + assert req.status == HTTPStatus.UNAUTHORIZED + + async def test_auth_access_signed_path_via_websocket( hass, app, hass_ws_client, hass_read_only_access_token ): From 7a407d09dc068e06f3dc21408e47e3efd3df32c6 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 22 Jun 2022 17:13:16 +0200 Subject: [PATCH 1692/3516] Fix filter & room occupied binary sensors (#73827) --- homeassistant/components/sensibo/binary_sensor.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensibo/binary_sensor.py b/homeassistant/components/sensibo/binary_sensor.py index 04e4a0b873d..2e1d449fc1d 100644 --- a/homeassistant/components/sensibo/binary_sensor.py +++ b/homeassistant/components/sensibo/binary_sensor.py @@ -92,6 +92,9 @@ MOTION_DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, .. icon="mdi:motion-sensor", value_fn=lambda data: data.room_occupied, ), +) + +DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( FILTER_CLEAN_REQUIRED_DESCRIPTION, ) @@ -161,7 +164,7 @@ async def async_setup_entry( SensiboDeviceSensor(coordinator, device_id, description) for description in MOTION_DEVICE_SENSOR_TYPES for device_id, device_data in coordinator.data.parsed.items() - if device_data.motion_sensors is not None + if device_data.motion_sensors ) entities.extend( SensiboDeviceSensor(coordinator, device_id, description) @@ -169,6 +172,12 @@ async def async_setup_entry( for device_id, device_data in coordinator.data.parsed.items() if device_data.model == "pure" ) + entities.extend( + SensiboDeviceSensor(coordinator, device_id, description) + for description in DEVICE_SENSOR_TYPES + for device_id, device_data in coordinator.data.parsed.items() + if device_data.model != "pure" + ) async_add_entities(entities) From 143e6a7adc74f6fd8b5d5037b9f772f35150dac3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 22 Jun 2022 17:23:51 +0200 Subject: [PATCH 1693/3516] Add missing type hints in locks (#73831) --- homeassistant/components/sesame/lock.py | 6 ++++-- homeassistant/components/verisure/lock.py | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sesame/lock.py b/homeassistant/components/sesame/lock.py index 99bb2d8a865..b9a230fed63 100644 --- a/homeassistant/components/sesame/lock.py +++ b/homeassistant/components/sesame/lock.py @@ -1,6 +1,8 @@ """Support for Sesame, by CANDY HOUSE.""" from __future__ import annotations +from typing import Any + import pysesame2 import voluptuous as vol @@ -61,11 +63,11 @@ class SesameDevice(LockEntity): """Return True if the device is currently locked, else False.""" return self._is_locked - def lock(self, **kwargs) -> None: + def lock(self, **kwargs: Any) -> None: """Lock the device.""" self._sesame.lock() - def unlock(self, **kwargs) -> None: + def unlock(self, **kwargs: Any) -> None: """Unlock the device.""" self._sesame.unlock() diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 8f9556643f8..f96b99e2a8c 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from typing import Any from verisure import Error as VerisureError @@ -122,7 +123,7 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt """Return the state attributes.""" return {"method": self.changed_method} - async def async_unlock(self, **kwargs) -> None: + async def async_unlock(self, **kwargs: Any) -> None: """Send unlock command.""" code = kwargs.get( ATTR_CODE, self.coordinator.entry.options.get(CONF_LOCK_DEFAULT_CODE) @@ -133,7 +134,7 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt await self.async_set_lock_state(code, STATE_UNLOCKED) - async def async_lock(self, **kwargs) -> None: + async def async_lock(self, **kwargs: Any) -> None: """Send lock command.""" code = kwargs.get( ATTR_CODE, self.coordinator.entry.options.get(CONF_LOCK_DEFAULT_CODE) From 86fde1a644ef5ecde6fc66b90cd0c090feaa847f Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 22 Jun 2022 10:56:17 -0500 Subject: [PATCH 1694/3516] Handle failures during initial Sonos subscription (#73456) --- homeassistant/components/sonos/exception.py | 4 +++ homeassistant/components/sonos/speaker.py | 39 +++++++++++++-------- tests/components/sonos/test_speaker.py | 18 ++++++++++ 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/sonos/exception.py b/homeassistant/components/sonos/exception.py index dd2d30796cc..7ff5dacd293 100644 --- a/homeassistant/components/sonos/exception.py +++ b/homeassistant/components/sonos/exception.py @@ -7,6 +7,10 @@ class UnknownMediaType(BrowseError): """Unknown media type.""" +class SonosSubscriptionsFailed(HomeAssistantError): + """Subscription creation failed.""" + + class SonosUpdateError(HomeAssistantError): """Update failed.""" diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 93d0afbcf9c..d37e3bac2a3 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -57,7 +57,7 @@ from .const import ( SONOS_VANISHED, SUBSCRIPTION_TIMEOUT, ) -from .exception import S1BatteryMissing, SonosUpdateError +from .exception import S1BatteryMissing, SonosSubscriptionsFailed, SonosUpdateError from .favorites import SonosFavorites from .helpers import soco_error from .media import SonosMedia @@ -324,12 +324,29 @@ class SonosSpeaker: async with self._subscription_lock: if self._subscriptions: return - await self._async_subscribe() + try: + await self._async_subscribe() + except SonosSubscriptionsFailed: + _LOGGER.warning("Creating subscriptions failed for %s", self.zone_name) + await self._async_offline() async def _async_subscribe(self) -> None: """Create event subscriptions.""" _LOGGER.debug("Creating subscriptions for %s", self.zone_name) + subscriptions = [ + self._subscribe(getattr(self.soco, service), self.async_dispatch_event) + for service in SUBSCRIPTION_SERVICES + ] + results = await asyncio.gather(*subscriptions, return_exceptions=True) + for result in results: + self.log_subscription_result( + result, "Creating subscription", logging.WARNING + ) + + if any(isinstance(result, Exception) for result in results): + raise SonosSubscriptionsFailed + # Create a polling task in case subscriptions fail or callback events do not arrive if not self._poll_timer: self._poll_timer = async_track_time_interval( @@ -342,16 +359,6 @@ class SonosSpeaker: SCAN_INTERVAL, ) - subscriptions = [ - self._subscribe(getattr(self.soco, service), self.async_dispatch_event) - for service in SUBSCRIPTION_SERVICES - ] - results = await asyncio.gather(*subscriptions, return_exceptions=True) - for result in results: - self.log_subscription_result( - result, "Creating subscription", logging.WARNING - ) - async def _subscribe( self, target: SubscriptionBase, sub_callback: Callable ) -> None: @@ -585,6 +592,11 @@ class SonosSpeaker: await self.async_offline() async def async_offline(self) -> None: + """Handle removal of speaker when unavailable.""" + async with self._subscription_lock: + await self._async_offline() + + async def _async_offline(self) -> None: """Handle removal of speaker when unavailable.""" if not self.available: return @@ -602,8 +614,7 @@ class SonosSpeaker: self._poll_timer() self._poll_timer = None - async with self._subscription_lock: - await self.async_unsubscribe() + await self.async_unsubscribe() self.hass.data[DATA_SONOS].discovery_known.discard(self.soco.uid) diff --git a/tests/components/sonos/test_speaker.py b/tests/components/sonos/test_speaker.py index 96b3d222dc6..e47540a6aab 100644 --- a/tests/components/sonos/test_speaker.py +++ b/tests/components/sonos/test_speaker.py @@ -29,3 +29,21 @@ async def test_fallback_to_polling( assert speaker.subscriptions_failed assert "falling back to polling" in caplog.text assert "Activity on Zone A from SonosSpeaker.update_volume" in caplog.text + + +async def test_subscription_creation_fails(hass: HomeAssistant, async_setup_sonos): + """Test that subscription creation failures are handled.""" + with patch( + "homeassistant.components.sonos.speaker.SonosSpeaker._subscribe", + side_effect=ConnectionError("Took too long"), + ): + await async_setup_sonos() + + speaker = list(hass.data[DATA_SONOS].discovered.values())[0] + assert not speaker._subscriptions + + with patch.object(speaker, "_resub_cooldown_expires_at", None): + speaker.speaker_activity("discovery") + await hass.async_block_till_done() + + assert speaker._subscriptions From 837957d89e80e4f34b88efa346f8030c8daff5ff Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 22 Jun 2022 18:22:01 +0200 Subject: [PATCH 1695/3516] Adjust set_percentage routine in fans (#73837) --- homeassistant/components/esphome/fan.py | 7 +++++-- homeassistant/components/smartthings/fan.py | 7 +++++-- homeassistant/components/wemo/fan.py | 7 +++++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index f2440465e77..41d7e418673 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -67,8 +67,11 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): api_version = self._api_version return api_version.major == 1 and api_version.minor > 3 - async def async_set_percentage(self, percentage: int | None) -> None: + async def async_set_percentage(self, percentage: int) -> None: """Set the speed percentage of the fan.""" + await self._async_set_percentage(percentage) + + async def _async_set_percentage(self, percentage: int | None) -> None: if percentage == 0: await self.async_turn_off() return @@ -95,7 +98,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity): **kwargs: Any, ) -> None: """Turn on the fan.""" - await self.async_set_percentage(percentage) + await self._async_set_percentage(percentage) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the fan.""" diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py index d3f3affa358..36d47533bbb 100644 --- a/homeassistant/components/smartthings/fan.py +++ b/homeassistant/components/smartthings/fan.py @@ -52,8 +52,11 @@ class SmartThingsFan(SmartThingsEntity, FanEntity): _attr_supported_features = FanEntityFeature.SET_SPEED - async def async_set_percentage(self, percentage: int | None) -> None: + async def async_set_percentage(self, percentage: int) -> None: """Set the speed percentage of the fan.""" + await self._async_set_percentage(percentage) + + async def _async_set_percentage(self, percentage: int | None) -> None: if percentage is None: await self._device.switch_on(set_status=True) elif percentage == 0: @@ -72,7 +75,7 @@ class SmartThingsFan(SmartThingsEntity, FanEntity): **kwargs, ) -> None: """Turn the fan on.""" - await self.async_set_percentage(percentage) + await self._async_set_percentage(percentage) async def async_turn_off(self, **kwargs) -> None: """Turn the fan off.""" diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index d24827bee96..81065cf8108 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -136,15 +136,18 @@ class WemoHumidifier(WemoBinaryStateEntity, FanEntity): **kwargs: Any, ) -> None: """Turn the fan on.""" - self.set_percentage(percentage) + self._set_percentage(percentage) def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" with self._wemo_call_wrapper("turn off"): self.wemo.set_state(FanMode.Off) - def set_percentage(self, percentage: int | None) -> None: + def set_percentage(self, percentage: int) -> None: """Set the fan_mode of the Humidifier.""" + self._set_percentage(percentage) + + def _set_percentage(self, percentage: int | None) -> None: if percentage is None: named_speed = self._last_fan_on_mode elif percentage == 0: From 532e25d087b05f2c221af8826567efec4d60be73 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 22 Jun 2022 18:26:25 +0200 Subject: [PATCH 1696/3516] Sensibo use switch for Pure boost (#73833) * Initial commit * Finalize pure boost switch * Fix service required --- .../components/sensibo/binary_sensor.py | 7 -- homeassistant/components/sensibo/climate.py | 23 ++--- .../components/sensibo/services.yaml | 17 ++-- homeassistant/components/sensibo/switch.py | 42 ++++++++- .../components/sensibo/test_binary_sensor.py | 10 +-- tests/components/sensibo/test_climate.py | 51 +---------- tests/components/sensibo/test_switch.py | 87 ++++++++++++++++++- 7 files changed, 140 insertions(+), 97 deletions(-) diff --git a/homeassistant/components/sensibo/binary_sensor.py b/homeassistant/components/sensibo/binary_sensor.py index 2e1d449fc1d..ed280aab4fe 100644 --- a/homeassistant/components/sensibo/binary_sensor.py +++ b/homeassistant/components/sensibo/binary_sensor.py @@ -99,13 +99,6 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( ) PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( - SensiboDeviceBinarySensorEntityDescription( - key="pure_boost_enabled", - device_class=BinarySensorDeviceClass.RUNNING, - name="Pure Boost Enabled", - icon="mdi:wind-power-outline", - value_fn=lambda data: data.pure_boost_enabled, - ), SensiboDeviceBinarySensorEntityDescription( key="pure_ac_integration", entity_category=EntityCategory.DIAGNOSTIC, diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 6a1084f733c..b4af38ab69c 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -107,21 +107,14 @@ async def async_setup_entry( platform.async_register_entity_service( SERVICE_ENABLE_PURE_BOOST, { - vol.Inclusive(ATTR_AC_INTEGRATION, "settings"): bool, - vol.Inclusive(ATTR_GEO_INTEGRATION, "settings"): bool, - vol.Inclusive(ATTR_INDOOR_INTEGRATION, "settings"): bool, - vol.Inclusive(ATTR_OUTDOOR_INTEGRATION, "settings"): bool, - vol.Inclusive(ATTR_SENSITIVITY, "settings"): vol.In( - ["Normal", "Sensitive"] - ), + vol.Required(ATTR_AC_INTEGRATION): bool, + vol.Required(ATTR_GEO_INTEGRATION): bool, + vol.Required(ATTR_INDOOR_INTEGRATION): bool, + vol.Required(ATTR_OUTDOOR_INTEGRATION): bool, + vol.Required(ATTR_SENSITIVITY): vol.In(["Normal", "Sensitive"]), }, "async_enable_pure_boost", ) - platform.async_register_entity_service( - SERVICE_DISABLE_PURE_BOOST, - {}, - "async_disable_pure_boost", - ) class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): @@ -353,9 +346,3 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): await self.async_send_command("set_pure_boost", params) await self.coordinator.async_refresh() - - async def async_disable_pure_boost(self) -> None: - """Disable Pure Boost Configuration.""" - - await self.async_send_command("set_pure_boost", {"enabled": False}) - await self.coordinator.async_refresh() diff --git a/homeassistant/components/sensibo/services.yaml b/homeassistant/components/sensibo/services.yaml index 6eb5c065789..9ce13b70eaa 100644 --- a/homeassistant/components/sensibo/services.yaml +++ b/homeassistant/components/sensibo/services.yaml @@ -45,45 +45,38 @@ enable_pure_boost: ac_integration: name: AC Integration description: Integrate with Air Conditioner. - required: false + required: true example: true selector: boolean: geo_integration: name: Geo Integration description: Integrate with Presence. - required: false + required: true example: true selector: boolean: indoor_integration: name: Indoor Air Quality description: Integrate with checking indoor air quality. - required: false + required: true example: true selector: boolean: outdoor_integration: name: Outdoor Air Quality description: Integrate with checking outdoor air quality. - required: false + required: true example: true selector: boolean: sensitivity: name: Sensitivity description: Set the sensitivity for Pure Boost. - required: false + required: true example: "Normal" selector: select: options: - "Normal" - "Sensitive" -disable_pure_boost: - name: Disable Pure Boost - description: Disable Pure Boost. - target: - entity: - integration: sensibo - domain: climate diff --git a/homeassistant/components/sensibo/switch.py b/homeassistant/components/sensibo/switch.py index 3b9915d0a89..d9cf9417504 100644 --- a/homeassistant/components/sensibo/switch.py +++ b/homeassistant/components/sensibo/switch.py @@ -29,7 +29,7 @@ class DeviceBaseEntityDescriptionMixin: """Mixin for required Sensibo base description keys.""" value_fn: Callable[[SensiboDevice], bool | None] - extra_fn: Callable[[SensiboDevice], dict[str, str | bool | None]] + extra_fn: Callable[[SensiboDevice], dict[str, str | bool | None]] | None command_on: str command_off: str remote_key: str @@ -56,6 +56,19 @@ DEVICE_SWITCH_TYPES: tuple[SensiboDeviceSwitchEntityDescription, ...] = ( ), ) +PURE_SWITCH_TYPES: tuple[SensiboDeviceSwitchEntityDescription, ...] = ( + SensiboDeviceSwitchEntityDescription( + key="pure_boost_switch", + device_class=SwitchDeviceClass.SWITCH, + name="Pure Boost", + value_fn=lambda data: data.pure_boost_enabled, + extra_fn=None, + command_on="set_pure_boost", + command_off="set_pure_boost", + remote_key="pure_boost_enabled", + ), +) + def build_params(command: str, device_data: SensiboDevice) -> dict[str, Any] | None: """Build params for turning on switch.""" @@ -66,6 +79,16 @@ def build_params(command: str, device_data: SensiboDevice) -> dict[str, Any] | N "acState": {**device_data.ac_states, "on": new_state}, } return params + if command == "set_pure_boost": + new_state = bool(device_data.pure_boost_enabled is False) + params = {"enabled": new_state} + if device_data.pure_measure_integration is None: + params["sensitivity"] = "N" + params["measurementsIntegration"] = True + params["acIntegration"] = False + params["geoIntegration"] = False + params["primeIntegration"] = False + return params return None @@ -84,6 +107,12 @@ async def async_setup_entry( for device_id, device_data in coordinator.data.parsed.items() if device_data.model != "pure" ) + entities.extend( + SensiboDeviceSwitch(coordinator, device_id, description) + for description in PURE_SWITCH_TYPES + for device_id, device_data in coordinator.data.parsed.items() + if device_data.model == "pure" + ) async_add_entities(entities) @@ -130,7 +159,10 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - result = await self.async_send_command(self.entity_description.command_off) + params = build_params(self.entity_description.command_on, self.device_data) + result = await self.async_send_command( + self.entity_description.command_off, params + ) if result["status"] == "success": setattr(self.device_data, self.entity_description.remote_key, False) @@ -141,6 +173,8 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity): ) @property - def extra_state_attributes(self) -> Mapping[str, Any]: + def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return additional attributes.""" - return self.entity_description.extra_fn(self.device_data) + if self.entity_description.extra_fn: + return self.entity_description.extra_fn(self.device_data) + return None diff --git a/tests/components/sensibo/test_binary_sensor.py b/tests/components/sensibo/test_binary_sensor.py index efa6c5bdb2a..093cfe7e472 100644 --- a/tests/components/sensibo/test_binary_sensor.py +++ b/tests/components/sensibo/test_binary_sensor.py @@ -27,20 +27,18 @@ async def test_binary_sensor( state2 = hass.states.get("binary_sensor.hallway_motion_sensor_main_sensor") state3 = hass.states.get("binary_sensor.hallway_motion_sensor_motion") state4 = hass.states.get("binary_sensor.hallway_room_occupied") - state5 = hass.states.get("binary_sensor.kitchen_pure_boost_enabled") - state6 = hass.states.get( + state5 = hass.states.get( "binary_sensor.kitchen_pure_boost_linked_with_indoor_air_quality" ) - state7 = hass.states.get( + state6 = hass.states.get( "binary_sensor.kitchen_pure_boost_linked_with_outdoor_air_quality" ) assert state1.state == "on" assert state2.state == "on" assert state3.state == "on" assert state4.state == "on" - assert state5.state == "off" - assert state6.state == "on" - assert state7.state == "off" + assert state5.state == "on" + assert state6.state == "off" monkeypatch.setattr( get_data.parsed["ABC999111"].motion_sensors["AABBCC"], "alive", False diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index ea7f6eb16fe..e7a3c465f76 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -28,7 +28,6 @@ from homeassistant.components.sensibo.climate import ( ATTR_OUTDOOR_INTEGRATION, ATTR_SENSITIVITY, SERVICE_ASSUME_STATE, - SERVICE_DISABLE_PURE_BOOST, SERVICE_ENABLE_PURE_BOOST, SERVICE_ENABLE_TIMER, _find_valid_target_temp, @@ -809,7 +808,7 @@ async def test_climate_pure_boost( await hass.async_block_till_done() state_climate = hass.states.get("climate.kitchen") - state2 = hass.states.get("binary_sensor.kitchen_pure_boost_enabled") + state2 = hass.states.get("switch.kitchen_pure_boost") assert state2.state == "off" with patch( @@ -878,7 +877,7 @@ async def test_climate_pure_boost( ) await hass.async_block_till_done() - state1 = hass.states.get("binary_sensor.kitchen_pure_boost_enabled") + state1 = hass.states.get("switch.kitchen_pure_boost") state2 = hass.states.get( "binary_sensor.kitchen_pure_boost_linked_with_indoor_air_quality" ) @@ -890,49 +889,3 @@ async def test_climate_pure_boost( assert state2.state == "on" assert state3.state == "on" assert state4.state == "s" - - with patch( - "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", - return_value=get_data, - ), patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_set_pureboost", - return_value={ - "status": "success", - "result": { - "enabled": False, - "sensitivity": "S", - "measurements_integration": True, - "ac_integration": False, - "geo_integration": False, - "prime_integration": True, - }, - }, - ) as mock_set_pureboost: - await hass.services.async_call( - DOMAIN, - SERVICE_DISABLE_PURE_BOOST, - { - ATTR_ENTITY_ID: state_climate.entity_id, - }, - blocking=True, - ) - await hass.async_block_till_done() - mock_set_pureboost.assert_called_once() - - monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_boost_enabled", False) - monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_sensitivity", "s") - - with patch( - "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", - return_value=get_data, - ): - async_fire_time_changed( - hass, - dt.utcnow() + timedelta(minutes=5), - ) - await hass.async_block_till_done() - - state1 = hass.states.get("binary_sensor.kitchen_pure_boost_enabled") - state4 = hass.states.get("sensor.kitchen_pure_sensitivity") - assert state1.state == "off" - assert state4.state == "s" diff --git a/tests/components/sensibo/test_switch.py b/tests/components/sensibo/test_switch.py index 49efca4103e..2a24751d70b 100644 --- a/tests/components/sensibo/test_switch.py +++ b/tests/components/sensibo/test_switch.py @@ -25,7 +25,7 @@ from homeassistant.util import dt from tests.common import async_fire_time_changed -async def test_switch( +async def test_switch_timer( hass: HomeAssistant, load_int: ConfigEntry, monkeypatch: MonkeyPatch, @@ -105,6 +105,81 @@ async def test_switch( assert state1.state == STATE_OFF +async def test_switch_pure_boost( + hass: HomeAssistant, + load_int: ConfigEntry, + monkeypatch: MonkeyPatch, + get_data: SensiboData, +) -> None: + """Test the Sensibo switch.""" + + state1 = hass.states.get("switch.kitchen_pure_boost") + assert state1.state == STATE_OFF + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_pureboost", + return_value={"status": "success"}, + ): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: state1.entity_id, + }, + blocking=True, + ) + await hass.async_block_till_done() + + monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_boost_enabled", True) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + state1 = hass.states.get("switch.kitchen_pure_boost") + assert state1.state == STATE_ON + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices_data", + return_value=get_data, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_set_pureboost", + return_value={"status": "success"}, + ): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: state1.entity_id, + }, + blocking=True, + ) + await hass.async_block_till_done() + + monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_boost_enabled", False) + + with patch( + "homeassistant.components.sensibo.coordinator.SensiboClient.async_get_devices_data", + return_value=get_data, + ): + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=5), + ) + await hass.async_block_till_done() + + state1 = hass.states.get("switch.kitchen_pure_boost") + assert state1.state == STATE_OFF + + async def test_switch_command_failure( hass: HomeAssistant, load_int: ConfigEntry, @@ -162,4 +237,14 @@ async def test_build_params( "minutesFromNow": 60, "acState": {**get_data.parsed["ABC999111"].ac_states, "on": False}, } + + monkeypatch.setattr(get_data.parsed["AAZZAAZZ"], "pure_measure_integration", None) + assert build_params("set_pure_boost", get_data.parsed["AAZZAAZZ"]) == { + "enabled": True, + "sensitivity": "N", + "measurementsIntegration": True, + "acIntegration": False, + "geoIntegration": False, + "primeIntegration": False, + } assert build_params("incorrect_command", get_data.parsed["ABC999111"]) is None From 6b6e5fad3c5869f5c5a9b6cc32e6656b01e13884 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 22 Jun 2022 18:43:41 +0200 Subject: [PATCH 1697/3516] Add missing type hints in fans (#73835) --- homeassistant/components/comfoconnect/fan.py | 2 +- homeassistant/components/demo/fan.py | 14 ++++--- homeassistant/components/fjaraskupan/fan.py | 8 ++-- homeassistant/components/freedompro/fan.py | 4 +- homeassistant/components/insteon/fan.py | 7 ++-- homeassistant/components/lutron_caseta/fan.py | 10 +++-- homeassistant/components/modern_forms/fan.py | 2 +- homeassistant/components/mqtt/fan.py | 15 +++---- homeassistant/components/smartthings/fan.py | 3 +- homeassistant/components/smarty/fan.py | 10 ++++- homeassistant/components/switch_as_x/fan.py | 4 +- homeassistant/components/template/fan.py | 16 ++++---- homeassistant/components/tuya/fan.py | 4 +- homeassistant/components/vesync/fan.py | 17 ++++---- homeassistant/components/wilight/fan.py | 14 ++++--- homeassistant/components/xiaomi_miio/fan.py | 41 ++++++++++--------- homeassistant/components/zha/fan.py | 8 +++- homeassistant/components/zwave_me/fan.py | 2 +- 18 files changed, 106 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index 84d82c170e1..f52c065a547 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -118,7 +118,7 @@ class ComfoConnectFan(FanEntity): self, percentage: int | None = None, preset_mode: str | None = None, - **kwargs, + **kwargs: Any, ) -> None: """Turn on the fan.""" if percentage is None: diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index 66440588f17..ef6875cb7c6 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -1,6 +1,8 @@ """Demo fan platform that has a fake fan.""" from __future__ import annotations +from typing import Any + from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -192,9 +194,9 @@ class DemoPercentageFan(BaseDemoFan, FanEntity): def turn_on( self, - percentage: int = None, - preset_mode: str = None, - **kwargs, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, ) -> None: """Turn on the entity.""" if preset_mode: @@ -262,9 +264,9 @@ class AsyncDemoPercentageFan(BaseDemoFan, FanEntity): async def async_turn_on( self, - percentage: int = None, - preset_mode: str = None, - **kwargs, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, ) -> None: """Turn on the entity.""" if preset_mode: diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index fcd95090400..ae5da3d189c 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -1,6 +1,8 @@ """Support for Fjäråskupan fans.""" from __future__ import annotations +from typing import Any + from fjaraskupan import ( COMMAND_AFTERCOOKINGTIMERAUTO, COMMAND_AFTERCOOKINGTIMERMANUAL, @@ -93,9 +95,9 @@ class Fan(CoordinatorEntity[Coordinator], FanEntity): async def async_turn_on( self, - percentage: int = None, - preset_mode: str = None, - **kwargs, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, ) -> None: """Turn on the fan.""" diff --git a/homeassistant/components/freedompro/fan.py b/homeassistant/components/freedompro/fan.py index b7758813865..3513b7672e0 100644 --- a/homeassistant/components/freedompro/fan.py +++ b/homeassistant/components/freedompro/fan.py @@ -60,7 +60,7 @@ class FreedomproFan(CoordinatorEntity, FanEntity): return self._attr_is_on @property - def percentage(self): + def percentage(self) -> int | None: """Return the current speed percentage.""" return self._attr_percentage @@ -111,7 +111,7 @@ class FreedomproFan(CoordinatorEntity, FanEntity): ) await self.coordinator.async_request_refresh() - async def async_set_percentage(self, percentage: int): + async def async_set_percentage(self, percentage: int) -> None: """Set the speed percentage of the fan.""" rotation_speed = {"rotationSpeed": percentage} payload = json.dumps(rotation_speed) diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py index 8639dfb79fe..8fe5fc9346e 100644 --- a/homeassistant/components/insteon/fan.py +++ b/homeassistant/components/insteon/fan.py @@ -2,6 +2,7 @@ from __future__ import annotations import math +from typing import Any from homeassistant.components.fan import ( DOMAIN as FAN_DOMAIN, @@ -62,9 +63,9 @@ class InsteonFanEntity(InsteonEntity, FanEntity): async def async_turn_on( self, - percentage: int = None, - preset_mode: str = None, - **kwargs, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, ) -> None: """Turn on the fan.""" await self.async_set_percentage(percentage or 67) diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py index e08a5278572..44dae8324fa 100644 --- a/homeassistant/components/lutron_caseta/fan.py +++ b/homeassistant/components/lutron_caseta/fan.py @@ -1,6 +1,8 @@ """Support for Lutron Caseta fans.""" from __future__ import annotations +from typing import Any + from pylutron_caseta import FAN_HIGH, FAN_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_OFF from homeassistant.components.fan import DOMAIN, FanEntity, FanEntityFeature @@ -58,10 +60,10 @@ class LutronCasetaFan(LutronCasetaDeviceUpdatableEntity, FanEntity): async def async_turn_on( self, - percentage: int = None, - preset_mode: str = None, - **kwargs, - ): + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: """Turn the fan on.""" if percentage is None: percentage = DEFAULT_ON_PERCENTAGE diff --git a/homeassistant/components/modern_forms/fan.py b/homeassistant/components/modern_forms/fan.py index 318bb8c969d..8bd8665dc3b 100644 --- a/homeassistant/components/modern_forms/fan.py +++ b/homeassistant/components/modern_forms/fan.py @@ -127,7 +127,7 @@ class ModernFormsFanEntity(FanEntity, ModernFormsDeviceEntity): async def async_turn_on( self, percentage: int | None = None, - preset_mode: int | None = None, + preset_mode: str | None = None, **kwargs: Any, ) -> None: """Turn on the fan.""" diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 4e1d1465ac2..4e7e58afb9f 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -4,6 +4,7 @@ from __future__ import annotations import functools import logging import math +from typing import Any import voluptuous as vol @@ -511,17 +512,17 @@ class MqttFan(MqttEntity, FanEntity): return self._state @property - def percentage(self): + def percentage(self) -> int | None: """Return the current percentage.""" return self._percentage @property - def preset_mode(self): + def preset_mode(self) -> str | None: """Return the current preset _mode.""" return self._preset_mode @property - def preset_modes(self) -> list: + def preset_modes(self) -> list[str]: """Get the list of available preset modes.""" return self._preset_modes @@ -536,16 +537,16 @@ class MqttFan(MqttEntity, FanEntity): return self._speed_count @property - def oscillating(self): + def oscillating(self) -> bool | None: """Return the oscillation state.""" return self._oscillation # The speed attribute deprecated in the schema, support will be removed after a quarter (2021.7) async def async_turn_on( self, - percentage: int = None, - preset_mode: str = None, - **kwargs, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, ) -> None: """Turn on the entity. diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py index 36d47533bbb..abcd5d12f75 100644 --- a/homeassistant/components/smartthings/fan.py +++ b/homeassistant/components/smartthings/fan.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Sequence import math +from typing import Any from pysmartthings import Capability @@ -72,7 +73,7 @@ class SmartThingsFan(SmartThingsEntity, FanEntity): self, percentage: int | None = None, preset_mode: str | None = None, - **kwargs, + **kwargs: Any, ) -> None: """Turn the fan on.""" await self._async_set_percentage(percentage) diff --git a/homeassistant/components/smarty/fan.py b/homeassistant/components/smarty/fan.py index 531b96d2558..a51d6783245 100644 --- a/homeassistant/components/smarty/fan.py +++ b/homeassistant/components/smarty/fan.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging import math +from typing import Any from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.core import HomeAssistant, callback @@ -64,7 +65,7 @@ class SmartyFan(FanEntity): return "mdi:air-conditioner" @property - def is_on(self): + def is_on(self) -> bool: """Return state of the fan.""" return bool(self._smarty_fan_speed) @@ -96,7 +97,12 @@ class SmartyFan(FanEntity): self._smarty_fan_speed = fan_speed self.schedule_update_ha_state() - def turn_on(self, percentage=None, preset_mode=None, **kwargs): + def turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: """Turn on the fan.""" _LOGGER.debug("Turning on fan. percentage is %s", percentage) self.set_percentage(percentage or DEFAULT_ON_PERCENTAGE) diff --git a/homeassistant/components/switch_as_x/fan.py b/homeassistant/components/switch_as_x/fan.py index 5ebc8902d06..d4f16a93ef6 100644 --- a/homeassistant/components/switch_as_x/fan.py +++ b/homeassistant/components/switch_as_x/fan.py @@ -1,6 +1,8 @@ """Fan support for switch entities.""" from __future__ import annotations +from typing import Any + from homeassistant.components.fan import FanEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID @@ -53,7 +55,7 @@ class FanSwitch(BaseToggleEntity, FanEntity): self, percentage: int | None = None, preset_mode: str | None = None, - **kwargs, + **kwargs: Any, ) -> None: """Turn on the fan. diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index ca50f6017bd..67cbeb07170 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -215,35 +215,35 @@ class TemplateFan(TemplateEntity, FanEntity): return self._preset_modes @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._state == STATE_ON @property - def preset_mode(self): + def preset_mode(self) -> str | None: """Return the current preset mode.""" return self._preset_mode @property - def percentage(self): + def percentage(self) -> int | None: """Return the current speed percentage.""" return self._percentage @property - def oscillating(self): + def oscillating(self) -> bool | None: """Return the oscillation state.""" return self._oscillating @property - def current_direction(self): + def current_direction(self) -> str | None: """Return the oscillation state.""" return self._direction async def async_turn_on( self, - percentage: int = None, - preset_mode: str = None, - **kwargs, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, ) -> None: """Turn on the fan.""" await self.async_run_script( diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 2d16ed36d40..36ed4c3c58e 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -158,8 +158,8 @@ class TuyaFanEntity(TuyaEntity, FanEntity): def turn_on( self, - percentage: int = None, - preset_mode: str = None, + percentage: int | None = None, + preset_mode: str | None = None, **kwargs: Any, ) -> None: """Turn on the fan.""" diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 44e74209c30..696a6a9ecf9 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -1,6 +1,9 @@ """Support for VeSync fans.""" +from __future__ import annotations + import logging import math +from typing import Any from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry @@ -91,7 +94,7 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): self.smartfan = fan @property - def percentage(self): + def percentage(self) -> int | None: """Return the current speed.""" if ( self.smartfan.mode == "manual" @@ -115,7 +118,7 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): return PRESET_MODES[SKU_TO_BASE_DEVICE.get(self.device.device_type)] @property - def preset_mode(self): + def preset_mode(self) -> str | None: """Get the current preset mode.""" if self.smartfan.mode in (FAN_MODE_AUTO, FAN_MODE_SLEEP): return self.smartfan.mode @@ -148,7 +151,7 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): return attr - def set_percentage(self, percentage): + def set_percentage(self, percentage: int) -> None: """Set the speed of the device.""" if percentage == 0: self.smartfan.turn_off() @@ -167,7 +170,7 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): ) self.schedule_update_ha_state() - def set_preset_mode(self, preset_mode): + def set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of device.""" if preset_mode not in self.preset_modes: raise ValueError( @@ -187,9 +190,9 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): def turn_on( self, - percentage: int = None, - preset_mode: str = None, - **kwargs, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, ) -> None: """Turn the device on.""" if preset_mode: diff --git a/homeassistant/components/wilight/fan.py b/homeassistant/components/wilight/fan.py index b96b4b89c61..7a60fa8ab62 100644 --- a/homeassistant/components/wilight/fan.py +++ b/homeassistant/components/wilight/fan.py @@ -1,6 +1,8 @@ """Support for WiLight Fan.""" from __future__ import annotations +from typing import Any + from pywilight.const import ( FAN_V1, ITEM_FAN, @@ -64,7 +66,7 @@ class WiLightFan(WiLightDevice, FanEntity): return "mdi:fan" @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._status.get("direction", WL_DIRECTION_OFF) != WL_DIRECTION_OFF @@ -98,9 +100,9 @@ class WiLightFan(WiLightDevice, FanEntity): async def async_turn_on( self, - percentage: int = None, - preset_mode: str = None, - **kwargs, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, ) -> None: """Turn on the fan.""" if percentage is None: @@ -108,7 +110,7 @@ class WiLightFan(WiLightDevice, FanEntity): else: await self.async_set_percentage(percentage) - async def async_set_percentage(self, percentage: int): + async def async_set_percentage(self, percentage: int) -> None: """Set the speed of the fan.""" if percentage == 0: await self._client.set_fan_direction(self._index, WL_DIRECTION_OFF) @@ -121,7 +123,7 @@ class WiLightFan(WiLightDevice, FanEntity): wl_speed = percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage) await self._client.set_fan_speed(self._index, wl_speed) - async def async_set_direction(self, direction: str): + async def async_set_direction(self, direction: str) -> None: """Set the direction of the fan.""" wl_direction = WL_DIRECTION_REVERSE if direction == DIRECTION_FORWARD: diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 969093545f0..31908ba373f 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -1,8 +1,11 @@ """Support for Xiaomi Mi Air Purifier and Xiaomi Mi Air Humidifier.""" +from __future__ import annotations + from abc import abstractmethod import asyncio import logging import math +from typing import Any from miio.airfresh import OperationMode as AirfreshOperationMode from miio.airfresh_t2017 import OperationMode as AirfreshOperationModeT2017 @@ -291,12 +294,12 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): """Hold operation mode class.""" @property - def preset_modes(self) -> list: + def preset_modes(self) -> list[str]: """Get the list of available preset modes.""" return self._preset_modes @property - def percentage(self): + def percentage(self) -> None: """Return the percentage based speed of the fan.""" return None @@ -306,15 +309,15 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): return self._state_attrs @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if device is on.""" return self._state async def async_turn_on( self, - percentage: int = None, - preset_mode: str = None, - **kwargs, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, ) -> None: """Turn the device on.""" result = await self._try_command( @@ -352,12 +355,12 @@ class XiaomiGenericAirPurifier(XiaomiGenericDevice): self._speed_count = 100 @property - def speed_count(self): + def speed_count(self) -> int: """Return the number of speeds of the fan supported.""" return self._speed_count @property - def preset_mode(self): + def preset_mode(self) -> str | None: """Get the active preset mode.""" if self._state: preset_mode = self.operation_mode_class(self._mode).name @@ -451,7 +454,7 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): return AirpurifierOperationMode @property - def percentage(self): + def percentage(self) -> int | None: """Return the current percentage based speed.""" if self._state: mode = self.operation_mode_class(self._mode) @@ -528,7 +531,7 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): return AirpurifierMiotOperationMode @property - def percentage(self): + def percentage(self) -> int | None: """Return the current percentage based speed.""" if self._fan_level is None: return None @@ -642,7 +645,7 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): return AirfreshOperationMode @property - def percentage(self): + def percentage(self) -> int | None: """Return the current percentage based speed.""" if self._state: mode = AirfreshOperationMode(self._mode) @@ -733,7 +736,7 @@ class XiaomiAirFreshA1(XiaomiGenericAirPurifier): return AirfreshOperationModeT2017 @property - def percentage(self): + def percentage(self) -> int | None: """Return the current percentage based speed.""" if self._favorite_speed is None: return None @@ -826,17 +829,17 @@ class XiaomiGenericFan(XiaomiGenericDevice): self._percentage = None @property - def preset_mode(self): + def preset_mode(self) -> str | None: """Get the active preset mode.""" return self._preset_mode @property - def preset_modes(self) -> list: + def preset_modes(self) -> list[str]: """Get the list of available preset modes.""" return [mode.name for mode in self.operation_mode_class] @property - def percentage(self): + def percentage(self) -> int | None: """Return the current speed as a percentage.""" if self._state: return self._percentage @@ -844,7 +847,7 @@ class XiaomiGenericFan(XiaomiGenericDevice): return None @property - def oscillating(self): + def oscillating(self) -> bool | None: """Return whether or not the fan is currently oscillating.""" return self._oscillating @@ -890,12 +893,12 @@ class XiaomiFan(XiaomiGenericFan): """Hold operation mode class.""" @property - def preset_mode(self): + def preset_mode(self) -> str: """Get the active preset mode.""" return ATTR_MODE_NATURE if self._nature_mode else ATTR_MODE_NORMAL @property - def preset_modes(self) -> list: + def preset_modes(self) -> list[str]: """Get the list of available preset modes.""" return [ATTR_MODE_NATURE, ATTR_MODE_NORMAL] @@ -1030,7 +1033,7 @@ class XiaomiFanMiot(XiaomiGenericFan): return FanOperationMode @property - def preset_mode(self): + def preset_mode(self) -> str | None: """Get the active preset mode.""" return self._preset_mode diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index a4b9baa5f0a..298f9e47296 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -4,6 +4,7 @@ from __future__ import annotations from abc import abstractmethod import functools import math +from typing import Any from zigpy.exceptions import ZigbeeException from zigpy.zcl.clusters import hvac @@ -87,7 +88,12 @@ class BaseFan(FanEntity): """Return the number of speeds the fan supports.""" return int_states_in_range(SPEED_RANGE) - async def async_turn_on(self, percentage=None, preset_mode=None, **kwargs) -> None: + async def async_turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: """Turn the entity on.""" if percentage is None: percentage = DEFAULT_ON_PERCENTAGE diff --git a/homeassistant/components/zwave_me/fan.py b/homeassistant/components/zwave_me/fan.py index 45d238a6541..c332fb305c5 100644 --- a/homeassistant/components/zwave_me/fan.py +++ b/homeassistant/components/zwave_me/fan.py @@ -66,7 +66,7 @@ class ZWaveMeFan(ZWaveMeEntity, FanEntity): self, percentage: int | None = None, preset_mode: str | None = None, - **kwargs, + **kwargs: Any, ) -> None: """Turn on the fan.""" self.set_percentage(percentage if percentage is not None else 99) From 6cf9b22b5a9f6f8dd6c77ee26fc70840f023c408 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 22 Jun 2022 19:04:39 +0200 Subject: [PATCH 1698/3516] Python 3.10 / Base image 2022.06.01 (#73830) * Python 3.10 / Base image 2022.06.01 * Update requirements * push opencv * we don't need numpy on core for now * Remove unused ignore Co-authored-by: Franck Nijhof --- .github/workflows/wheels.yml | 126 +----------------- Dockerfile | 15 --- build.yaml | 10 +- .../components/compensation/manifest.json | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/iqvia/sensor.py | 2 +- homeassistant/components/opencv/manifest.json | 2 +- .../components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- homeassistant/package_constraints.txt | 3 + machine/khadas-vim3 | 7 +- machine/raspberrypi3 | 4 +- machine/raspberrypi3-64 | 4 +- machine/raspberrypi4 | 4 +- machine/raspberrypi4-64 | 4 +- machine/tinker | 7 +- requirements_all.txt | 4 +- requirements_test_all.txt | 2 +- script/gen_requirements_all.py | 3 + 19 files changed, 43 insertions(+), 162 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 9bfcb48e09a..27307388546 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -65,47 +65,6 @@ jobs: path: ./requirements_diff.txt core: - name: Build wheels with ${{ matrix.tag }} (${{ matrix.arch }}) for core - if: github.repository_owner == 'home-assistant' - needs: init - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - arch: ${{ fromJson(needs.init.outputs.architectures) }} - tag: - - "3.9-alpine3.14" - steps: - - name: Checkout the repository - uses: actions/checkout@v3.0.2 - - - name: Download env_file - uses: actions/download-artifact@v3 - with: - name: env_file - - - name: Download requirements_diff - uses: actions/download-artifact@v3 - with: - name: requirements_diff - - - name: Build wheels - uses: home-assistant/wheels@2022.01.2 - with: - tag: ${{ matrix.tag }} - arch: ${{ matrix.arch }} - wheels-host: wheels.hass.io - wheels-key: ${{ secrets.WHEELS_KEY }} - wheels-user: wheels - env-file: true - apk: "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;cargo" - pip: "Cython;numpy==1.21.6" - skip-binary: aiohttp - constraints: "homeassistant/package_constraints.txt" - requirements-diff: "requirements_diff.txt" - requirements: "requirements.txt" - - core_musllinux: name: Build musllinux wheels with musllinux_1_2 / cp310 at ${{ matrix.arch }} for core if: github.repository_owner == 'home-assistant' needs: init @@ -128,18 +87,6 @@ jobs: with: name: requirements_diff - - name: Adjust ENV / CP310 - run: | - if [ "${{ matrix.arch }}" = "i386" ]; then - echo "NPY_DISABLE_SVML=1" >> .env_file - fi - - requirement_files="requirements_all.txt requirements_diff.txt" - for requirement_file in ${requirement_files}; do - sed -i "s|numpy==1.21.6|numpy==1.22.4|g" ${requirement_file} - done - echo "numpy==1.22.4" >> homeassistant/package_constraints.txt - - name: Build wheels uses: home-assistant/wheels@2022.06.6 with: @@ -148,76 +95,13 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;yaml-dev" + apk: "libffi-dev;openssl-dev;yaml-dev" skip-binary: aiohttp constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements.txt" integrations: - name: Build wheels with ${{ matrix.tag }} (${{ matrix.arch }}) for integrations - if: github.repository_owner == 'home-assistant' - needs: init - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - arch: ${{ fromJson(needs.init.outputs.architectures) }} - tag: - - "3.9-alpine3.14" - steps: - - name: Checkout the repository - uses: actions/checkout@v3.0.2 - - - name: Download env_file - uses: actions/download-artifact@v3 - with: - name: env_file - - - name: Download requirements_diff - uses: actions/download-artifact@v3 - with: - name: requirements_diff - - - name: Uncomment packages - run: | - requirement_files="requirements_all.txt requirements_diff.txt" - for requirement_file in ${requirement_files}; do - sed -i "s|# pybluez|pybluez|g" ${requirement_file} - sed -i "s|# bluepy|bluepy|g" ${requirement_file} - sed -i "s|# beacontools|beacontools|g" ${requirement_file} - sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file} - sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file} - sed -i "s|# evdev|evdev|g" ${requirement_file} - sed -i "s|# python-eq3bt|python-eq3bt|g" ${requirement_file} - sed -i "s|# pycups|pycups|g" ${requirement_file} - sed -i "s|# homekit|homekit|g" ${requirement_file} - sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file} - sed -i "s|# decora|decora|g" ${requirement_file} - sed -i "s|# avion|avion|g" ${requirement_file} - sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file} - sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file} - sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} - sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} - done - - - name: Build wheels - uses: home-assistant/wheels@2022.01.2 - with: - tag: ${{ matrix.tag }} - arch: ${{ matrix.arch }} - wheels-host: wheels.hass.io - wheels-key: ${{ secrets.WHEELS_KEY }} - wheels-user: wheels - env-file: true - apk: "build-base;cmake;git;linux-headers;libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;cargo" - pip: "Cython;numpy;scikit-build" - skip-binary: aiohttp,grpcio - constraints: "homeassistant/package_constraints.txt" - requirements-diff: "requirements_diff.txt" - requirements: "requirements_all.txt" - - integrations_musllinux: name: Build musllinux wheels with musllinux_1_2 / cp310 at ${{ matrix.arch }} for integrations if: github.repository_owner == 'home-assistant' needs: init @@ -256,18 +140,12 @@ jobs: sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} done - - name: Adjust ENV / CP310 + - name: Adjust ENV run: | if [ "${{ matrix.arch }}" = "i386" ]; then echo "NPY_DISABLE_SVML=1" >> .env_file fi - requirement_files="requirements_all.txt requirements_diff.txt" - for requirement_file in ${requirement_files}; do - sed -i "s|numpy==1.21.6|numpy==1.22.4|g" ${requirement_file} - done - echo "numpy==1.22.4" >> homeassistant/package_constraints.txt - - name: Build wheels uses: home-assistant/wheels@2022.06.6 with: diff --git a/Dockerfile b/Dockerfile index 1d6ce675e74..13552d55a3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,21 +25,6 @@ RUN \ -e ./homeassistant --use-deprecated=legacy-resolver \ && python3 -m compileall homeassistant/homeassistant -# Fix Bug with Alpine 3.14 and sqlite 3.35 -# https://gitlab.alpinelinux.org/alpine/aports/-/issues/12524 -ARG BUILD_ARCH -RUN \ - if [ "${BUILD_ARCH}" = "amd64" ]; then \ - export APK_ARCH=x86_64; \ - elif [ "${BUILD_ARCH}" = "i386" ]; then \ - export APK_ARCH=x86; \ - else \ - export APK_ARCH=${BUILD_ARCH}; \ - fi \ - && curl -O http://dl-cdn.alpinelinux.org/alpine/v3.13/main/${APK_ARCH}/sqlite-libs-3.34.1-r0.apk \ - && apk add --no-cache sqlite-libs-3.34.1-r0.apk \ - && rm -f sqlite-libs-3.34.1-r0.apk - # Home Assistant S6-Overlay COPY rootfs / diff --git a/build.yaml b/build.yaml index 196277184a3..23486fb5510 100644 --- a/build.yaml +++ b/build.yaml @@ -1,11 +1,11 @@ image: homeassistant/{arch}-homeassistant shadow_repository: ghcr.io/home-assistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.05.0 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.05.0 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.05.0 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.05.0 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.05.0 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.06.1 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.06.1 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.06.1 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.06.1 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.06.1 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index 213e8888e23..eb3135954b0 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,7 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.21.6"], + "requirements": ["numpy==1.22.4"], "codeowners": ["@Petro31"], "iot_class": "calculated" } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 9bb07157b54..8b0dacd3575 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.21.6", "pyiqvia==2022.04.0"], + "requirements": ["numpy==1.22.4", "pyiqvia==2022.04.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyiqvia"] diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 51f2969e9fe..d8c7ea317c8 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -161,7 +161,7 @@ def calculate_trend(indices: list[float]) -> str: """Calculate the "moving average" of a set of indices.""" index_range = np.arange(0, len(indices)) index_array = np.array(indices) - linear_fit = np.polyfit(index_range, index_array, 1) # type: ignore[no-untyped-call] + linear_fit = np.polyfit(index_range, index_array, 1) slope = round(linear_fit[0], 2) if slope > 0: diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 504b83bdaf9..8cd1604f106 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.21.6", "opencv-python-headless==4.5.2.54"], + "requirements": ["numpy==1.22.4", "opencv-python-headless==4.6.0.66"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index efd4f3d76d0..4168d820fb6 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.5.0", "tf-models-official==2.5.0", "pycocotools==2.0.1", - "numpy==1.21.6", + "numpy==1.22.4", "pillow==9.1.1" ], "codeowners": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index aaae8f7cc54..578aea3bbc6 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.21.6"], + "requirements": ["numpy==1.22.4"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8452b4e62fb..16e508ddcda 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -86,6 +86,9 @@ httpcore==0.15.0 # 5.2.0 fixed a collections abc deprecation hyperframe>=5.2.0 +# Ensure we run compatible with musllinux build env +numpy>=1.22.0 + # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 diff --git a/machine/khadas-vim3 b/machine/khadas-vim3 index be07d6c8aba..5aeaca50780 100644 --- a/machine/khadas-vim3 +++ b/machine/khadas-vim3 @@ -2,4 +2,9 @@ ARG BUILD_VERSION FROM homeassistant/aarch64-homeassistant:$BUILD_VERSION RUN apk --no-cache add \ - usbutils + usbutils \ + && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ + pybluez \ + pygatt[GATTTOOL] \ + -c /usr/src/homeassistant/requirements_all.txt \ + --use-deprecated=legacy-resolver diff --git a/machine/raspberrypi3 b/machine/raspberrypi3 index 9985b4a3b7a..6eed9e94142 100644 --- a/machine/raspberrypi3 +++ b/machine/raspberrypi3 @@ -6,7 +6,9 @@ RUN apk --no-cache add \ raspberrypi-libs \ usbutils \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt \ + pybluez \ + pygatt[GATTTOOL] \ + -c /usr/src/homeassistant/requirements_all.txt \ --use-deprecated=legacy-resolver ## diff --git a/machine/raspberrypi3-64 b/machine/raspberrypi3-64 index 35c6eec77de..1647f91813c 100644 --- a/machine/raspberrypi3-64 +++ b/machine/raspberrypi3-64 @@ -6,7 +6,9 @@ RUN apk --no-cache add \ raspberrypi-libs \ usbutils \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt \ + pybluez \ + pygatt[GATTTOOL] \ + -c /usr/src/homeassistant/requirements_all.txt \ --use-deprecated=legacy-resolver ## diff --git a/machine/raspberrypi4 b/machine/raspberrypi4 index 9985b4a3b7a..6eed9e94142 100644 --- a/machine/raspberrypi4 +++ b/machine/raspberrypi4 @@ -6,7 +6,9 @@ RUN apk --no-cache add \ raspberrypi-libs \ usbutils \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt \ + pybluez \ + pygatt[GATTTOOL] \ + -c /usr/src/homeassistant/requirements_all.txt \ --use-deprecated=legacy-resolver ## diff --git a/machine/raspberrypi4-64 b/machine/raspberrypi4-64 index 35c6eec77de..1647f91813c 100644 --- a/machine/raspberrypi4-64 +++ b/machine/raspberrypi4-64 @@ -6,7 +6,9 @@ RUN apk --no-cache add \ raspberrypi-libs \ usbutils \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - bluepy pybluez -c /usr/src/homeassistant/requirements_all.txt \ + pybluez \ + pygatt[GATTTOOL] \ + -c /usr/src/homeassistant/requirements_all.txt \ --use-deprecated=legacy-resolver ## diff --git a/machine/tinker b/machine/tinker index 9660ca71b9c..5976d533188 100644 --- a/machine/tinker +++ b/machine/tinker @@ -3,8 +3,7 @@ FROM homeassistant/armv7-homeassistant:$BUILD_VERSION RUN apk --no-cache add usbutils \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ - --use-deprecated=legacy-resolver \ - bluepy \ pybluez \ - pygatt[GATTTOOL] + pygatt[GATTTOOL] \ + -c /usr/src/homeassistant/requirements_all.txt \ + --use-deprecated=legacy-resolver diff --git a/requirements_all.txt b/requirements_all.txt index aa93b7bae4c..fbe5d177194 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1123,7 +1123,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.21.6 +numpy==1.22.4 # homeassistant.components.oasa_telematics oasatelematics==0.3 @@ -1156,7 +1156,7 @@ open-garage==0.2.0 open-meteo==0.2.1 # homeassistant.components.opencv -# opencv-python-headless==4.5.2.54 +# opencv-python-headless==4.6.0.66 # homeassistant.components.openerz openerz-api==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1256de737ef..4a627d0d6da 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -773,7 +773,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.21.6 +numpy==1.22.4 # homeassistant.components.google oauth2client==4.1.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 88524ab63de..5db46ffbeba 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -106,6 +106,9 @@ httpcore==0.15.0 # 5.2.0 fixed a collections abc deprecation hyperframe>=5.2.0 +# Ensure we run compatible with musllinux build env +numpy>=1.22.0 + # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 From 54d04d233be98bcd0ebe0363f25ddddcb3d4985a Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 22 Jun 2022 13:13:43 -0400 Subject: [PATCH 1699/3516] Bump version of pyunifiprotect to 4.0.6 (#73843) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 00761790474..2b779c77629 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.5", "unifi-discovery==1.1.4"], + "requirements": ["pyunifiprotect==4.0.6", "unifi-discovery==1.1.4"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index fbe5d177194..de75044f7db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1990,7 +1990,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.5 +pyunifiprotect==4.0.6 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a627d0d6da..7d43748f6fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1325,7 +1325,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.5 +pyunifiprotect==4.0.6 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 8d66623036c324016bf2ab9a23adbb287292ad57 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 22 Jun 2022 12:29:34 -0500 Subject: [PATCH 1700/3516] Add ZoneGroupState statistics to Sonos diagnostics (#73848) --- homeassistant/components/sonos/__init__.py | 8 ++++++++ homeassistant/components/sonos/diagnostics.py | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index c26bf269a30..2e2290dff04 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -190,6 +190,14 @@ class SonosDiscoveryManager: for speaker in self.data.discovered.values(): speaker.activity_stats.log_report() speaker.event_stats.log_report() + if zgs := next( + speaker.soco.zone_group_state for speaker in self.data.discovered.values() + ): + _LOGGER.debug( + "ZoneGroupState stats: (%s/%s) processed", + zgs.processed_count, + zgs.total_requests, + ) await asyncio.gather( *(speaker.async_offline() for speaker in self.data.discovered.values()) ) diff --git a/homeassistant/components/sonos/diagnostics.py b/homeassistant/components/sonos/diagnostics.py index 077ca3a68cd..463884e1ea8 100644 --- a/homeassistant/components/sonos/diagnostics.py +++ b/homeassistant/components/sonos/diagnostics.py @@ -136,4 +136,8 @@ async def async_generate_speaker_info( payload["media"] = await async_generate_media_info(hass, speaker) payload["activity_stats"] = speaker.activity_stats.report() payload["event_stats"] = speaker.event_stats.report() + payload["zone_group_state_stats"] = { + "processed": speaker.soco.zone_group_state.processed_count, + "total_requests": speaker.soco.zone_group_state.total_requests, + } return payload From 9229d14962030f0a5fd11941c2533aa8adb27bac Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Jun 2022 20:17:28 +0200 Subject: [PATCH 1701/3516] Automatically onboard Wiz (#73851) --- homeassistant/components/wiz/config_flow.py | 13 ++++----- tests/components/wiz/test_config_flow.py | 31 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/wiz/config_flow.py b/homeassistant/components/wiz/config_flow.py index b2173ccda97..b1bce3eda0d 100644 --- a/homeassistant/components/wiz/config_flow.py +++ b/homeassistant/components/wiz/config_flow.py @@ -9,7 +9,7 @@ from pywizlight.discovery import DiscoveredBulb from pywizlight.exceptions import WizLightConnectionError, WizLightTimeOutError import voluptuous as vol -from homeassistant.components import dhcp +from homeassistant.components import dhcp, onboarding from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST from homeassistant.data_entry_flow import AbortFlow, FlowResult @@ -29,11 +29,12 @@ class WizConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + _discovered_device: DiscoveredBulb + _name: str + def __init__(self) -> None: """Initialize the config flow.""" - self._discovered_device: DiscoveredBulb | None = None self._discovered_devices: dict[str, DiscoveredBulb] = {} - self._name: str | None = None async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle discovery via dhcp.""" @@ -54,7 +55,6 @@ class WizConfigFlow(ConfigFlow, domain=DOMAIN): async def _async_handle_discovery(self) -> FlowResult: """Handle any discovery.""" device = self._discovered_device - assert device is not None _LOGGER.debug("Discovered device: %s", device) ip_address = device.ip_address mac = device.mac_address @@ -66,7 +66,6 @@ class WizConfigFlow(ConfigFlow, domain=DOMAIN): async def _async_connect_discovered_or_abort(self) -> None: """Connect to the device and verify its responding.""" device = self._discovered_device - assert device is not None bulb = wizlight(device.ip_address) try: bulbtype = await bulb.get_bulbtype() @@ -84,10 +83,8 @@ class WizConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm discovery.""" - assert self._discovered_device is not None - assert self._name is not None ip_address = self._discovered_device.ip_address - if user_input is not None: + if user_input is not None or not onboarding.async_is_onboarded(self.hass): # Make sure the device is still there and # update the name if the firmware has auto # updated since discovery diff --git a/tests/components/wiz/test_config_flow.py b/tests/components/wiz/test_config_flow.py index 58b46bbea9d..f37f2ba21a0 100644 --- a/tests/components/wiz/test_config_flow.py +++ b/tests/components/wiz/test_config_flow.py @@ -506,3 +506,34 @@ async def test_discovery_with_firmware_update(hass): } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "source, data", + [ + (config_entries.SOURCE_DHCP, DHCP_DISCOVERY), + (config_entries.SOURCE_INTEGRATION_DISCOVERY, INTEGRATION_DISCOVERY), + ], +) +async def test_discovered_during_onboarding(hass, source, data): + """Test dhcp or discovery during onboarding creates the config entry.""" + with _patch_wizlight(), patch( + "homeassistant.components.wiz.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.wiz.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=data + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == "WiZ Dimmable White ABCABC" + assert result["data"] == { + CONF_HOST: "1.1.1.1", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 From 1ead6d6762027f8706b58f080149f9688d104dd0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Jun 2022 21:19:34 +0200 Subject: [PATCH 1702/3516] Automatically onboard Yeelight (#73854) --- .../components/yeelight/config_flow.py | 4 +- tests/components/yeelight/test_config_flow.py | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index 5a457b7da95..b4afedd6c51 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -11,7 +11,7 @@ from yeelight.aio import AsyncBulb from yeelight.main import get_known_models from homeassistant import config_entries, exceptions -from homeassistant.components import dhcp, ssdp, zeroconf +from homeassistant.components import dhcp, onboarding, ssdp, zeroconf from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME from homeassistant.core import callback @@ -134,7 +134,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_discovery_confirm(self, user_input=None): """Confirm discovery.""" - if user_input is not None: + if user_input is not None or not onboarding.async_is_onboarded(self.hass): return self.async_create_entry( title=async_format_model_id(self._discovered_model, self.unique_id), data={ diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 1c19a5e7dfd..7d9acc670b5 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -809,3 +809,53 @@ async def test_discovery_adds_missing_ip_id_only(hass: HomeAssistant): assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS + + +@pytest.mark.parametrize( + "source, data", + [ + ( + config_entries.SOURCE_DHCP, + dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress="aa:bb:cc:dd:ee:ff", hostname="mock_hostname" + ), + ), + ( + config_entries.SOURCE_HOMEKIT, + zeroconf.ZeroconfServiceInfo( + host=IP_ADDRESS, + addresses=[IP_ADDRESS], + hostname="mock_hostname", + name="mock_name", + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "aa:bb:cc:dd:ee:ff"}, + type="mock_type", + ), + ), + ], +) +async def test_discovered_during_onboarding(hass, source, data): + """Test we create a config entry when discovered during onboarding.""" + mocked_bulb = _mocked_bulb() + with _patch_discovery(), _patch_discovery_interval(), patch( + f"{MODULE_CONFIG_FLOW}.AsyncBulb", return_value=mocked_bulb + ), 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, patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ) as mock_is_onboarded: + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=data + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["data"] == { + CONF_HOST: IP_ADDRESS, + CONF_ID: "0x000000000015243f", + CONF_MODEL: MODEL, + } + assert mock_async_setup.called + assert mock_async_setup_entry.called + assert mock_is_onboarded.called From 0e674fc59746fb648cc7c3b736e87c2abeb69acd Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 22 Jun 2022 21:35:26 +0200 Subject: [PATCH 1703/3516] Clean up zwave_js logging and hass.data (#73856) --- homeassistant/components/zwave_js/__init__.py | 26 +++++-------------- .../components/zwave_js/binary_sensor.py | 3 --- homeassistant/components/zwave_js/cover.py | 3 --- homeassistant/components/zwave_js/entity.py | 6 +---- homeassistant/components/zwave_js/light.py | 3 --- homeassistant/components/zwave_js/lock.py | 4 +-- homeassistant/components/zwave_js/sensor.py | 4 +-- homeassistant/components/zwave_js/switch.py | 3 --- 8 files changed, 10 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 4f5756361c8..fe616e8bdb9 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -104,8 +104,6 @@ from .services import ZWaveServices CONNECT_TIMEOUT = 10 DATA_CLIENT_LISTEN_TASK = "client_listen_task" DATA_START_PLATFORM_TASK = "start_platform_task" -DATA_CONNECT_FAILED_LOGGED = "connect_failed_logged" -DATA_INVALID_SERVER_VERSION_LOGGED = "invalid_server_version_logged" async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -170,28 +168,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_ensure_addon_running(hass, entry) client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) - entry_hass_data: dict = hass.data[DOMAIN].setdefault(entry.entry_id, {}) # connect and throw error if connection failed try: async with timeout(CONNECT_TIMEOUT): await client.connect() except InvalidServerVersion as err: - if not entry_hass_data.get(DATA_INVALID_SERVER_VERSION_LOGGED): - LOGGER.error("Invalid server version: %s", err) - entry_hass_data[DATA_INVALID_SERVER_VERSION_LOGGED] = True if use_addon: async_ensure_addon_updated(hass) - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady(f"Invalid server version: {err}") from err except (asyncio.TimeoutError, BaseZwaveJSServerError) as err: - if not entry_hass_data.get(DATA_CONNECT_FAILED_LOGGED): - LOGGER.error("Failed to connect: %s", err) - entry_hass_data[DATA_CONNECT_FAILED_LOGGED] = True - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady(f"Failed to connect: {err}") from err else: LOGGER.info("Connected to Zwave JS Server") - entry_hass_data[DATA_CONNECT_FAILED_LOGGED] = False - entry_hass_data[DATA_INVALID_SERVER_VERSION_LOGGED] = False dev_reg = device_registry.async_get(hass) ent_reg = entity_registry.async_get(hass) @@ -202,7 +191,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_register_api(hass) platform_task = hass.async_create_task(start_platforms(hass, entry, client)) - entry_hass_data[DATA_START_PLATFORM_TASK] = platform_task + hass.data[DOMAIN].setdefault(entry.entry_id, {})[ + DATA_START_PLATFORM_TASK + ] = platform_task return True @@ -635,9 +626,7 @@ async def disconnect_client(hass: HomeAssistant, entry: ConfigEntry) -> None: platform_task: asyncio.Task = data[DATA_START_PLATFORM_TASK] listen_task.cancel() platform_task.cancel() - platform_setup_tasks = ( - hass.data[DOMAIN].get(entry.entry_id, {}).get(DATA_PLATFORM_SETUP, {}).values() - ) + platform_setup_tasks = data.get(DATA_PLATFORM_SETUP, {}).values() for task in platform_setup_tasks: task.cancel() @@ -711,8 +700,7 @@ async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) -> try: addon_info = await addon_manager.async_get_addon_info() except AddonError as err: - LOGGER.error(err) - raise ConfigEntryNotReady from err + raise ConfigEntryNotReady(err) from err usb_path: str = entry.data[CONF_USB_PATH] # s0_legacy_key was saved as network_key before s2 was added. diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index fb085cffe62..f6480689910 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -import logging from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import CommandClass @@ -30,8 +29,6 @@ from .entity import ZWaveBaseEntity PARALLEL_UPDATES = 0 -LOGGER = logging.getLogger(__name__) - NOTIFICATION_SMOKE_ALARM = "1" NOTIFICATION_CARBON_MONOOXIDE = "2" diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index ee83db4578c..30364d127eb 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -1,7 +1,6 @@ """Support for Z-Wave cover devices.""" from __future__ import annotations -import logging from typing import Any, cast from zwave_js_server.client import Client as ZwaveClient @@ -39,8 +38,6 @@ from .entity import ZWaveBaseEntity PARALLEL_UPDATES = 0 -LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index a4271ac1c02..79dd1d27a4c 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -1,8 +1,6 @@ """Generic Z-Wave Entity Class.""" from __future__ import annotations -import logging - from zwave_js_server.const import NodeStatus from zwave_js_server.model.driver import Driver from zwave_js_server.model.value import Value as ZwaveValue, get_value_id @@ -12,12 +10,10 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity -from .const import DOMAIN +from .const import DOMAIN, LOGGER from .discovery import ZwaveDiscoveryInfo from .helpers import get_device_id, get_unique_id -LOGGER = logging.getLogger(__name__) - EVENT_VALUE_UPDATED = "value updated" EVENT_VALUE_REMOVED = "value removed" EVENT_DEAD = "dead" diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index 17293e85a21..4c8fe2a3986 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -1,7 +1,6 @@ """Support for Z-Wave lights.""" from __future__ import annotations -import logging from typing import Any, cast from zwave_js_server.client import Client as ZwaveClient @@ -49,8 +48,6 @@ from .entity import ZWaveBaseEntity PARALLEL_UPDATES = 0 -LOGGER = logging.getLogger(__name__) - MULTI_COLOR_MAP = { ColorComponent.WARM_WHITE: COLOR_SWITCH_COMBINED_WARM_WHITE, ColorComponent.COLD_WHITE: COLOR_SWITCH_COMBINED_COLD_WHITE, diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py index ffe99373991..efeadb9b6b3 100644 --- a/homeassistant/components/zwave_js/lock.py +++ b/homeassistant/components/zwave_js/lock.py @@ -1,7 +1,6 @@ """Representation of Z-Wave locks.""" from __future__ import annotations -import logging from typing import Any import voluptuous as vol @@ -27,6 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( DATA_CLIENT, DOMAIN, + LOGGER, SERVICE_CLEAR_LOCK_USERCODE, SERVICE_SET_LOCK_USERCODE, ) @@ -35,8 +35,6 @@ from .entity import ZWaveBaseEntity PARALLEL_UPDATES = 0 -LOGGER = logging.getLogger(__name__) - STATE_TO_ZWAVE_MAP: dict[int, dict[str, int | bool]] = { CommandClass.DOOR_LOCK: { STATE_UNLOCKED: DoorLockMode.UNSECURED, diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index 2b2e2a0de2b..22fbfdab728 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Mapping -import logging from typing import cast import voluptuous as vol @@ -55,6 +54,7 @@ from .const import ( ENTITY_DESC_KEY_TEMPERATURE, ENTITY_DESC_KEY_TOTAL_INCREASING, ENTITY_DESC_KEY_VOLTAGE, + LOGGER, SERVICE_RESET_METER, ) from .discovery import ZwaveDiscoveryInfo @@ -67,8 +67,6 @@ from .helpers import get_device_id, get_valueless_base_unique_id PARALLEL_UPDATES = 0 -LOGGER = logging.getLogger(__name__) - STATUS_ICON: dict[NodeStatus, str] = { NodeStatus.ALIVE: "mdi:heart-pulse", NodeStatus.ASLEEP: "mdi:sleep", diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py index 52b8f813326..154106d56f5 100644 --- a/homeassistant/components/zwave_js/switch.py +++ b/homeassistant/components/zwave_js/switch.py @@ -1,7 +1,6 @@ """Representation of Z-Wave switches.""" from __future__ import annotations -import logging from typing import Any from zwave_js_server.client import Client as ZwaveClient @@ -23,8 +22,6 @@ from .entity import ZWaveBaseEntity PARALLEL_UPDATES = 0 -LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, From 9ac28d20760614e11b79dd42775022e9e6d3daa2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 22 Jun 2022 21:40:22 +0200 Subject: [PATCH 1704/3516] Adjust vesync type hints (#73842) --- homeassistant/components/vesync/fan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 696a6a9ecf9..9932790fa96 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -113,9 +113,9 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): ) @property - def preset_modes(self): + def preset_modes(self) -> list[str]: """Get the list of available preset modes.""" - return PRESET_MODES[SKU_TO_BASE_DEVICE.get(self.device.device_type)] + return PRESET_MODES[SKU_TO_BASE_DEVICE[self.device.device_type]] @property def preset_mode(self) -> str | None: From 8b067e83f73f3aaf4a0a6207baf4547ed42c5bfb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Jun 2022 14:59:51 -0500 Subject: [PATCH 1705/3516] Initial orjson support take 3 (#73849) * Initial orjson support take 2 Still need to work out problem building wheels -- Redux of #72754 / #32153 Now possible since the following is solved: ijl/orjson#220 (comment) This implements orjson where we use our default encoder. This does not implement orjson where `ExtendedJSONEncoder` is used as these areas tend to be called far less frequently. If its desired, this could be done in a followup, but it seemed like a case of diminishing returns (except maybe for large diagnostics files, or traces, but those are not expected to be downloaded frequently). Areas where this makes a perceptible difference: - Anything that subscribes to entities (Initial subscribe_entities payload) - Initial download of registries on first connection / restore - History queries - Saving states to the database - Large logbook queries - Anything that subscribes to events (appdaemon) Cavets: orjson supports serializing dataclasses natively (and much faster) which eliminates the need to implement `as_dict` in many places when the data is already in a dataclass. This works well as long as all the data in the dataclass can also be serialized. I audited all places where we have an `as_dict` for a dataclass and found only backups needs to be adjusted (support for `Path` needed to be added for backups). I was a little bit worried about `SensorExtraStoredData` with `Decimal` but it all seems to work out from since it converts it before it gets to the json encoding cc @dgomes If it turns out to be a problem we can disable this with option |= [orjson.OPT_PASSTHROUGH_DATACLASS](https://github.com/ijl/orjson#opt_passthrough_dataclass) and it will fallback to `as_dict` Its quite impressive for history queries Screen_Shot_2022-05-30_at_23_46_30 * use for views as well * handle UnicodeEncodeError * tweak * DRY * DRY * not needed * fix tests * Update tests/components/http/test_view.py * Update tests/components/http/test_view.py * black * templates --- homeassistant/components/history/__init__.py | 2 +- homeassistant/components/http/view.py | 7 +-- .../components/logbook/websocket_api.py | 2 +- homeassistant/components/recorder/const.py | 10 +--- homeassistant/components/recorder/core.py | 15 +++-- .../components/recorder/db_schema.py | 57 ++++++++++--------- homeassistant/components/recorder/models.py | 4 +- .../components/websocket_api/commands.py | 18 +++--- .../components/websocket_api/connection.py | 3 +- .../components/websocket_api/const.py | 7 --- .../components/websocket_api/messages.py | 7 ++- homeassistant/helpers/aiohttp_client.py | 2 + homeassistant/helpers/json.py | 49 +++++++++++++++- homeassistant/helpers/template.py | 5 +- homeassistant/package_constraints.txt | 1 + homeassistant/scripts/benchmark/__init__.py | 3 +- homeassistant/util/json.py | 9 ++- pyproject.toml | 2 + requirements.txt | 1 + tests/components/energy/test_validate.py | 7 ++- tests/components/http/test_view.py | 12 +++- .../components/websocket_api/test_commands.py | 6 +- 22 files changed, 149 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 27acff54f99..77301532d3d 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -24,10 +24,10 @@ from homeassistant.components.recorder.statistics import ( ) from homeassistant.components.recorder.util import session_scope from homeassistant.components.websocket_api import messages -from homeassistant.components.websocket_api.const import JSON_DUMP from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA +from homeassistant.helpers.json import JSON_DUMP from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 192d2d5d57b..6ab3b2a84a4 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable from http import HTTPStatus -import json import logging from typing import Any @@ -21,7 +20,7 @@ import voluptuous as vol from homeassistant import exceptions from homeassistant.const import CONTENT_TYPE_JSON from homeassistant.core import Context, is_callback -from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.json import JSON_ENCODE_EXCEPTIONS, json_bytes from .const import KEY_AUTHENTICATED, KEY_HASS @@ -53,8 +52,8 @@ class HomeAssistantView: ) -> web.Response: """Return a JSON response.""" try: - msg = json.dumps(result, cls=JSONEncoder, allow_nan=False).encode("UTF-8") - except (ValueError, TypeError) as err: + msg = json_bytes(result) + except JSON_ENCODE_EXCEPTIONS as err: _LOGGER.error("Unable to serialize to JSON: %s\n%s", err, result) raise HTTPInternalServerError from err response = web.Response( diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index a8f9bc50920..04afa82e75b 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -14,10 +14,10 @@ from homeassistant.components import websocket_api from homeassistant.components.recorder import get_instance from homeassistant.components.websocket_api import messages from homeassistant.components.websocket_api.connection import ActiveConnection -from homeassistant.components.websocket_api.const import JSON_DUMP from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers.entityfilter import EntityFilter from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.json import JSON_DUMP import homeassistant.util.dt as dt_util from .const import LOGBOOK_ENTITIES_FILTER diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index e558d19b530..532644c7feb 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -1,12 +1,10 @@ """Recorder constants.""" -from functools import partial -import json -from typing import Final - from homeassistant.backports.enum import StrEnum from homeassistant.const import ATTR_ATTRIBUTION, ATTR_RESTORED, ATTR_SUPPORTED_FEATURES -from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.json import ( # noqa: F401 pylint: disable=unused-import + JSON_DUMP, +) DATA_INSTANCE = "recorder_instance" SQLITE_URL_PREFIX = "sqlite://" @@ -27,8 +25,6 @@ MAX_ROWS_TO_PURGE = 998 DB_WORKER_PREFIX = "DbWorker" -JSON_DUMP: Final = partial(json.dumps, cls=JSONEncoder, separators=(",", ":")) - ALL_DOMAIN_EXCLUDE_ATTRS = {ATTR_ATTRIBUTION, ATTR_RESTORED, ATTR_SUPPORTED_FEATURES} ATTR_KEEP_DAYS = "keep_days" diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index d8260976ccf..9585804690a 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -36,6 +36,7 @@ from homeassistant.helpers.event import ( async_track_time_interval, async_track_utc_time_change, ) +from homeassistant.helpers.json import JSON_ENCODE_EXCEPTIONS from homeassistant.helpers.typing import UNDEFINED, UndefinedType import homeassistant.util.dt as dt_util @@ -754,11 +755,12 @@ class Recorder(threading.Thread): return try: - shared_data = EventData.shared_data_from_event(event) - except (TypeError, ValueError) as ex: + shared_data_bytes = EventData.shared_data_bytes_from_event(event) + except JSON_ENCODE_EXCEPTIONS as ex: _LOGGER.warning("Event is not JSON serializable: %s: %s", event, ex) return + shared_data = shared_data_bytes.decode("utf-8") # Matching attributes found in the pending commit if pending_event_data := self._pending_event_data.get(shared_data): dbevent.event_data_rel = pending_event_data @@ -766,7 +768,7 @@ class Recorder(threading.Thread): elif data_id := self._event_data_ids.get(shared_data): dbevent.data_id = data_id else: - data_hash = EventData.hash_shared_data(shared_data) + data_hash = EventData.hash_shared_data_bytes(shared_data_bytes) # Matching attributes found in the database if data_id := self._find_shared_data_in_db(data_hash, shared_data): self._event_data_ids[shared_data] = dbevent.data_id = data_id @@ -785,10 +787,10 @@ class Recorder(threading.Thread): assert self.event_session is not None try: dbstate = States.from_event(event) - shared_attrs = StateAttributes.shared_attrs_from_event( + shared_attrs_bytes = StateAttributes.shared_attrs_bytes_from_event( event, self._exclude_attributes_by_domain ) - except (TypeError, ValueError) as ex: + except JSON_ENCODE_EXCEPTIONS as ex: _LOGGER.warning( "State is not JSON serializable: %s: %s", event.data.get("new_state"), @@ -796,6 +798,7 @@ class Recorder(threading.Thread): ) return + shared_attrs = shared_attrs_bytes.decode("utf-8") dbstate.attributes = None # Matching attributes found in the pending commit if pending_attributes := self._pending_state_attributes.get(shared_attrs): @@ -804,7 +807,7 @@ class Recorder(threading.Thread): elif attributes_id := self._state_attributes_ids.get(shared_attrs): dbstate.attributes_id = attributes_id else: - attr_hash = StateAttributes.hash_shared_attrs(shared_attrs) + attr_hash = StateAttributes.hash_shared_attrs_bytes(shared_attrs_bytes) # Matching attributes found in the database if attributes_id := self._find_shared_attr_in_db(attr_hash, shared_attrs): dbstate.attributes_id = attributes_id diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 642efe2e969..f300cc0bae7 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -3,12 +3,12 @@ from __future__ import annotations from collections.abc import Callable from datetime import datetime, timedelta -import json import logging from typing import Any, cast import ciso8601 from fnvhash import fnv1a_32 +import orjson from sqlalchemy import ( JSON, BigInteger, @@ -39,9 +39,10 @@ from homeassistant.const import ( MAX_LENGTH_STATE_STATE, ) from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id +from homeassistant.helpers.json import JSON_DUMP, json_bytes import homeassistant.util.dt as dt_util -from .const import ALL_DOMAIN_EXCLUDE_ATTRS, JSON_DUMP +from .const import ALL_DOMAIN_EXCLUDE_ATTRS from .models import StatisticData, StatisticMetaData, process_timestamp # SQLAlchemy Schema @@ -124,7 +125,7 @@ class JSONLiteral(JSON): # type: ignore[misc] def process(value: Any) -> str: """Dump json.""" - return json.dumps(value) + return JSON_DUMP(value) return process @@ -187,7 +188,7 @@ class Events(Base): # type: ignore[misc,valid-type] try: return Event( self.event_type, - json.loads(self.event_data) if self.event_data else {}, + orjson.loads(self.event_data) if self.event_data else {}, EventOrigin(self.origin) if self.origin else EVENT_ORIGIN_ORDER[self.origin_idx], @@ -195,7 +196,7 @@ class Events(Base): # type: ignore[misc,valid-type] context=context, ) except ValueError: - # When json.loads fails + # When orjson.loads fails _LOGGER.exception("Error converting to event: %s", self) return None @@ -223,25 +224,26 @@ class EventData(Base): # type: ignore[misc,valid-type] @staticmethod def from_event(event: Event) -> EventData: """Create object from an event.""" - shared_data = JSON_DUMP(event.data) + shared_data = json_bytes(event.data) return EventData( - shared_data=shared_data, hash=EventData.hash_shared_data(shared_data) + shared_data=shared_data.decode("utf-8"), + hash=EventData.hash_shared_data_bytes(shared_data), ) @staticmethod - def shared_data_from_event(event: Event) -> str: - """Create shared_attrs from an event.""" - return JSON_DUMP(event.data) + def shared_data_bytes_from_event(event: Event) -> bytes: + """Create shared_data from an event.""" + return json_bytes(event.data) @staticmethod - def hash_shared_data(shared_data: str) -> int: + def hash_shared_data_bytes(shared_data_bytes: bytes) -> int: """Return the hash of json encoded shared data.""" - return cast(int, fnv1a_32(shared_data.encode("utf-8"))) + return cast(int, fnv1a_32(shared_data_bytes)) def to_native(self) -> dict[str, Any]: """Convert to an HA state object.""" try: - return cast(dict[str, Any], json.loads(self.shared_data)) + return cast(dict[str, Any], orjson.loads(self.shared_data)) except ValueError: _LOGGER.exception("Error converting row to event data: %s", self) return {} @@ -328,9 +330,9 @@ class States(Base): # type: ignore[misc,valid-type] parent_id=self.context_parent_id, ) try: - attrs = json.loads(self.attributes) if self.attributes else {} + attrs = orjson.loads(self.attributes) if self.attributes else {} except ValueError: - # When json.loads fails + # When orjson.loads fails _LOGGER.exception("Error converting row to state: %s", self) return None if self.last_changed is None or self.last_changed == self.last_updated: @@ -376,40 +378,39 @@ class StateAttributes(Base): # type: ignore[misc,valid-type] """Create object from a state_changed event.""" state: State | None = event.data.get("new_state") # None state means the state was removed from the state machine - dbstate = StateAttributes( - shared_attrs="{}" if state is None else JSON_DUMP(state.attributes) - ) - dbstate.hash = StateAttributes.hash_shared_attrs(dbstate.shared_attrs) + attr_bytes = b"{}" if state is None else json_bytes(state.attributes) + dbstate = StateAttributes(shared_attrs=attr_bytes.decode("utf-8")) + dbstate.hash = StateAttributes.hash_shared_attrs_bytes(attr_bytes) return dbstate @staticmethod - def shared_attrs_from_event( + def shared_attrs_bytes_from_event( event: Event, exclude_attrs_by_domain: dict[str, set[str]] - ) -> str: + ) -> bytes: """Create shared_attrs from a state_changed event.""" state: State | None = event.data.get("new_state") # None state means the state was removed from the state machine if state is None: - return "{}" + return b"{}" domain = split_entity_id(state.entity_id)[0] exclude_attrs = ( exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS ) - return JSON_DUMP( + return json_bytes( {k: v for k, v in state.attributes.items() if k not in exclude_attrs} ) @staticmethod - def hash_shared_attrs(shared_attrs: str) -> int: - """Return the hash of json encoded shared attributes.""" - return cast(int, fnv1a_32(shared_attrs.encode("utf-8"))) + def hash_shared_attrs_bytes(shared_attrs_bytes: bytes) -> int: + """Return the hash of orjson encoded shared attributes.""" + return cast(int, fnv1a_32(shared_attrs_bytes)) def to_native(self) -> dict[str, Any]: """Convert to an HA state object.""" try: - return cast(dict[str, Any], json.loads(self.shared_attrs)) + return cast(dict[str, Any], orjson.loads(self.shared_attrs)) except ValueError: - # When json.loads fails + # When orjson.loads fails _LOGGER.exception("Error converting row to state attributes: %s", self) return {} diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index ef1f76df9fc..64fb44289b0 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -2,10 +2,10 @@ from __future__ import annotations from datetime import datetime -import json import logging from typing import Any, TypedDict, overload +import orjson from sqlalchemy.engine.row import Row from homeassistant.components.websocket_api.const import ( @@ -253,7 +253,7 @@ def decode_attributes_from_row( if not source or source == EMPTY_JSON_OBJECT: return {} try: - attr_cache[source] = attributes = json.loads(source) + attr_cache[source] = attributes = orjson.loads(source) except ValueError: _LOGGER.exception("Error converting row to state attributes: %s", source) attr_cache[source] = attributes = {} diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 61bcb8badf0..bea08722eb0 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -29,7 +29,7 @@ from homeassistant.helpers.event import ( TrackTemplateResult, async_track_template_result, ) -from homeassistant.helpers.json import ExtendedJSONEncoder +from homeassistant.helpers.json import JSON_DUMP, ExtendedJSONEncoder from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import IntegrationNotFound, async_get_integration from homeassistant.setup import DATA_SETUP_TIME, async_get_loaded_integrations @@ -241,13 +241,13 @@ def handle_get_states( # to succeed for the UI to show. response = messages.result_message(msg["id"], states) try: - connection.send_message(const.JSON_DUMP(response)) + connection.send_message(JSON_DUMP(response)) return except (ValueError, TypeError): connection.logger.error( "Unable to serialize to JSON. Bad data found at %s", format_unserializable_data( - find_paths_unserializable_data(response, dump=const.JSON_DUMP) + find_paths_unserializable_data(response, dump=JSON_DUMP) ), ) del response @@ -256,13 +256,13 @@ def handle_get_states( serialized = [] for state in states: try: - serialized.append(const.JSON_DUMP(state)) + serialized.append(JSON_DUMP(state)) except (ValueError, TypeError): # Error is already logged above pass # We now have partially serialized states. Craft some JSON. - response2 = const.JSON_DUMP(messages.result_message(msg["id"], ["TO_REPLACE"])) + response2 = JSON_DUMP(messages.result_message(msg["id"], ["TO_REPLACE"])) response2 = response2.replace('"TO_REPLACE"', ", ".join(serialized)) connection.send_message(response2) @@ -315,13 +315,13 @@ def handle_subscribe_entities( # to succeed for the UI to show. response = messages.event_message(msg["id"], data) try: - connection.send_message(const.JSON_DUMP(response)) + connection.send_message(JSON_DUMP(response)) return except (ValueError, TypeError): connection.logger.error( "Unable to serialize to JSON. Bad data found at %s", format_unserializable_data( - find_paths_unserializable_data(response, dump=const.JSON_DUMP) + find_paths_unserializable_data(response, dump=JSON_DUMP) ), ) del response @@ -330,14 +330,14 @@ def handle_subscribe_entities( cannot_serialize: list[str] = [] for entity_id, state_dict in add_entities.items(): try: - const.JSON_DUMP(state_dict) + JSON_DUMP(state_dict) except (ValueError, TypeError): cannot_serialize.append(entity_id) for entity_id in cannot_serialize: del add_entities[entity_id] - connection.send_message(const.JSON_DUMP(messages.event_message(msg["id"], data))) + connection.send_message(JSON_DUMP(messages.event_message(msg["id"], data))) @decorators.websocket_command({vol.Required("type"): "get_services"}) diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 0280863f83e..26c4c6f8321 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -11,6 +11,7 @@ import voluptuous as vol from homeassistant.auth.models import RefreshToken, User from homeassistant.core import Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized +from homeassistant.helpers.json import JSON_DUMP from . import const, messages @@ -56,7 +57,7 @@ class ActiveConnection: async def send_big_result(self, msg_id: int, result: Any) -> None: """Send a result message that would be expensive to JSON serialize.""" content = await self.hass.async_add_executor_job( - const.JSON_DUMP, messages.result_message(msg_id, result) + JSON_DUMP, messages.result_message(msg_id, result) ) self.send_message(content) diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 107cf6d0270..60a00126092 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -4,12 +4,9 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable from concurrent import futures -from functools import partial -import json from typing import TYPE_CHECKING, Any, Final from homeassistant.core import HomeAssistant -from homeassistant.helpers.json import JSONEncoder if TYPE_CHECKING: from .connection import ActiveConnection # noqa: F401 @@ -53,10 +50,6 @@ SIGNAL_WEBSOCKET_DISCONNECTED: Final = "websocket_disconnected" # Data used to store the current connection list DATA_CONNECTIONS: Final = f"{DOMAIN}.connections" -JSON_DUMP: Final = partial( - json.dumps, cls=JSONEncoder, allow_nan=False, separators=(",", ":") -) - COMPRESSED_STATE_STATE = "s" COMPRESSED_STATE_ATTRIBUTES = "a" COMPRESSED_STATE_CONTEXT = "c" diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index f546ba5eec6..c3e5f6bb5f5 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.core import Event, State from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.json import JSON_DUMP from homeassistant.util.json import ( find_paths_unserializable_data, format_unserializable_data, @@ -193,15 +194,15 @@ def compressed_state_dict_add(state: State) -> dict[str, Any]: def message_to_json(message: dict[str, Any]) -> str: """Serialize a websocket message to json.""" try: - return const.JSON_DUMP(message) + return JSON_DUMP(message) except (ValueError, TypeError): _LOGGER.error( "Unable to serialize to JSON. Bad data found at %s", format_unserializable_data( - find_paths_unserializable_data(message, dump=const.JSON_DUMP) + find_paths_unserializable_data(message, dump=JSON_DUMP) ), ) - return const.JSON_DUMP( + return JSON_DUMP( error_message( message["id"], const.ERR_UNKNOWN_ERROR, "Invalid JSON in response" ) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index eaabb002b0a..2e56698db41 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -14,6 +14,7 @@ from aiohttp import web from aiohttp.hdrs import CONTENT_TYPE, USER_AGENT from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout import async_timeout +import orjson from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ @@ -97,6 +98,7 @@ def _async_create_clientsession( """Create a new ClientSession with kwargs, i.e. for cookies.""" clientsession = aiohttp.ClientSession( connector=_async_get_connector(hass, verify_ssl), + json_serialize=lambda x: orjson.dumps(x).decode("utf-8"), **kwargs, ) # Prevent packages accidentally overriding our default headers diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index c581e5a9361..9248c613b95 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -1,7 +1,12 @@ """Helpers to help with encoding Home Assistant objects in JSON.""" import datetime import json -from typing import Any +from pathlib import Path +from typing import Any, Final + +import orjson + +JSON_ENCODE_EXCEPTIONS = (TypeError, ValueError) class JSONEncoder(json.JSONEncoder): @@ -22,6 +27,20 @@ class JSONEncoder(json.JSONEncoder): return json.JSONEncoder.default(self, o) +def json_encoder_default(obj: Any) -> Any: + """Convert Home Assistant objects. + + Hand other objects to the original method. + """ + if isinstance(obj, set): + return list(obj) + if hasattr(obj, "as_dict"): + return obj.as_dict() + if isinstance(obj, Path): + return obj.as_posix() + raise TypeError + + class ExtendedJSONEncoder(JSONEncoder): """JSONEncoder that supports Home Assistant objects and falls back to repr(o).""" @@ -40,3 +59,31 @@ class ExtendedJSONEncoder(JSONEncoder): return super().default(o) except TypeError: return {"__type": str(type(o)), "repr": repr(o)} + + +def json_bytes(data: Any) -> bytes: + """Dump json bytes.""" + return orjson.dumps( + data, option=orjson.OPT_NON_STR_KEYS, default=json_encoder_default + ) + + +def json_dumps(data: Any) -> str: + """Dump json string. + + orjson supports serializing dataclasses natively which + eliminates the need to implement as_dict in many places + when the data is already in a dataclass. This works + well as long as all the data in the dataclass can also + be serialized. + + If it turns out to be a problem we can disable this + with option |= orjson.OPT_PASSTHROUGH_DATACLASS and it + will fallback to as_dict + """ + return orjson.dumps( + data, option=orjson.OPT_NON_STR_KEYS, default=json_encoder_default + ).decode("utf-8") + + +JSON_DUMP: Final = json_dumps diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 053beab307e..02f95254c86 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -27,6 +27,7 @@ import jinja2 from jinja2 import pass_context, pass_environment from jinja2.sandbox import ImmutableSandboxedEnvironment from jinja2.utils import Namespace +import orjson import voluptuous as vol from homeassistant.const import ( @@ -566,7 +567,7 @@ class Template: variables["value"] = value with suppress(ValueError, TypeError): - variables["value_json"] = json.loads(value) + variables["value_json"] = orjson.loads(value) try: return _render_with_context( @@ -1743,7 +1744,7 @@ def ordinal(value): def from_json(value): """Convert a JSON string to an object.""" - return json.loads(value) + return orjson.loads(value) def to_json(value, ensure_ascii=True): diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 16e508ddcda..e9599bbe3a9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,6 +20,7 @@ httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.7 +orjson==3.7.2 paho-mqtt==1.6.1 pillow==9.1.1 pip>=21.0,<22.2 diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index a681b3e210d..efbfec5e961 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -12,14 +12,13 @@ from timeit import default_timer as timer from typing import TypeVar from homeassistant import core -from homeassistant.components.websocket_api.const import JSON_DUMP from homeassistant.const import EVENT_STATE_CHANGED from homeassistant.helpers.entityfilter import convert_include_exclude_filter from homeassistant.helpers.event import ( async_track_state_change, async_track_state_change_event, ) -from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.json import JSON_DUMP, JSONEncoder # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs # mypy: no-warn-return-any diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index fdee7a7a90f..82ecfd34d6d 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -7,6 +7,8 @@ import json import logging from typing import Any +import orjson + from homeassistant.core import Event, State from homeassistant.exceptions import HomeAssistantError @@ -30,7 +32,7 @@ def load_json(filename: str, default: list | dict | None = None) -> list | dict: """ try: with open(filename, encoding="utf-8") as fdesc: - return json.loads(fdesc.read()) # type: ignore[no-any-return] + return orjson.loads(fdesc.read()) # type: ignore[no-any-return] except FileNotFoundError: # This is not a fatal error _LOGGER.debug("JSON file not found: %s", filename) @@ -56,7 +58,10 @@ def save_json( Returns True on success. """ try: - json_data = json.dumps(data, indent=4, cls=encoder) + if encoder: + json_data = json.dumps(data, indent=2, cls=encoder) + else: + json_data = orjson.dumps(data, option=orjson.OPT_INDENT_2).decode("utf-8") except TypeError as error: msg = f"Failed to serialize to JSON: {filename}. Bad data at {format_unserializable_data(find_paths_unserializable_data(data))}" _LOGGER.error(msg) diff --git a/pyproject.toml b/pyproject.toml index f17015cc9ba..165e781983e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ dependencies = [ "PyJWT==2.4.0", # PyJWT has loose dependency. We want the latest one. "cryptography==36.0.2", + "orjson==3.7.2", "pip>=21.0,<22.2", "python-slugify==4.0.1", "pyyaml==6.0", @@ -119,6 +120,7 @@ extension-pkg-allow-list = [ "av.audio.stream", "av.stream", "ciso8601", + "orjson", "cv2", ] diff --git a/requirements.txt b/requirements.txt index ba7c9e4dd13..0b3791d6eed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,7 @@ ifaddr==0.1.7 jinja2==3.1.2 PyJWT==2.4.0 cryptography==36.0.2 +orjson==3.7.2 pip>=21.0,<22.2 python-slugify==4.0.1 pyyaml==6.0 diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py index 37ebe4147c5..e802688daaf 100644 --- a/tests/components/energy/test_validate.py +++ b/tests/components/energy/test_validate.py @@ -4,6 +4,7 @@ from unittest.mock import patch import pytest from homeassistant.components.energy import async_get_manager, validate +from homeassistant.helpers.json import JSON_DUMP from homeassistant.setup import async_setup_component @@ -408,7 +409,11 @@ async def test_validation_grid( }, ) - assert (await validate.async_validate(hass)).as_dict() == { + result = await validate.async_validate(hass) + # verify its also json serializable + JSON_DUMP(result) + + assert result.as_dict() == { "energy_sources": [ [ { diff --git a/tests/components/http/test_view.py b/tests/components/http/test_view.py index fdd4fbc7808..f6a2ff85d3a 100644 --- a/tests/components/http/test_view.py +++ b/tests/components/http/test_view.py @@ -1,5 +1,6 @@ """Tests for Home Assistant View.""" from http import HTTPStatus +import json from unittest.mock import AsyncMock, Mock from aiohttp.web_exceptions import ( @@ -34,9 +35,16 @@ async def test_invalid_json(caplog): view = HomeAssistantView() with pytest.raises(HTTPInternalServerError): - view.json(float("NaN")) + view.json(rb"\ud800") - assert str(float("NaN")) in caplog.text + assert "Unable to serialize to JSON" in caplog.text + + +async def test_nan_serialized_to_null(caplog): + """Test nan serialized to null JSON.""" + view = HomeAssistantView() + response = view.json(float("NaN")) + assert json.loads(response.body.decode("utf-8")) is None async def test_handling_unauthorized(mock_request): diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 4d3302f7c13..0f4695596fc 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -619,12 +619,15 @@ async def test_states_filters_visible(hass, hass_admin_user, websocket_client): async def test_get_states_not_allows_nan(hass, websocket_client): - """Test get_states command not allows NaN floats.""" + """Test get_states command converts NaN to None.""" hass.states.async_set("greeting.hello", "world") hass.states.async_set("greeting.bad", "data", {"hello": float("NaN")}) hass.states.async_set("greeting.bye", "universe") await websocket_client.send_json({"id": 5, "type": "get_states"}) + bad = dict(hass.states.get("greeting.bad").as_dict()) + bad["attributes"] = dict(bad["attributes"]) + bad["attributes"]["hello"] = None msg = await websocket_client.receive_json() assert msg["id"] == 5 @@ -632,6 +635,7 @@ async def test_get_states_not_allows_nan(hass, websocket_client): assert msg["success"] assert msg["result"] == [ hass.states.get("greeting.hello").as_dict(), + bad, hass.states.get("greeting.bye").as_dict(), ] From 75cfe845e120e06d9aa9f582cddae809de344db5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 22 Jun 2022 22:17:50 +0200 Subject: [PATCH 1706/3516] Adjust freedompro type hints (#73839) --- homeassistant/components/freedompro/fan.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/freedompro/fan.py b/homeassistant/components/freedompro/fan.py index 3513b7672e0..443a1375f24 100644 --- a/homeassistant/components/freedompro/fan.py +++ b/homeassistant/components/freedompro/fan.py @@ -2,6 +2,7 @@ from __future__ import annotations import json +from typing import Any from pyfreedompro import put_state @@ -87,27 +88,30 @@ class FreedomproFan(CoordinatorEntity, FanEntity): await super().async_added_to_hass() self._handle_coordinator_update() - async def async_turn_on(self, percentage=None, preset_mode=None, **kwargs): + async def async_turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: """Async function to turn on the fan.""" payload = {"on": True} - payload = json.dumps(payload) await put_state( self._session, self._api_key, self.unique_id, - payload, + json.dumps(payload), ) await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Async function to turn off the fan.""" payload = {"on": False} - payload = json.dumps(payload) await put_state( self._session, self._api_key, self.unique_id, - payload, + json.dumps(payload), ) await self.coordinator.async_request_refresh() From 73c54b14d0146874c7f542e14ab3db71f267e801 Mon Sep 17 00:00:00 2001 From: Khole Date: Wed, 22 Jun 2022 21:20:47 +0100 Subject: [PATCH 1707/3516] Hive bump pyhiveapi version (#73846) --- homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 45c2b468f23..29477bf7414 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -6,7 +6,7 @@ "models": ["HHKBridge*"] }, "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": ["pyhiveapi==0.5.10"], + "requirements": ["pyhiveapi==0.5.11"], "codeowners": ["@Rendili", "@KJonline"], "iot_class": "cloud_polling", "loggers": ["apyhiveapi"] diff --git a/requirements_all.txt b/requirements_all.txt index de75044f7db..b15f6c885ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1541,7 +1541,7 @@ pyheos==0.7.2 pyhik==0.3.0 # homeassistant.components.hive -pyhiveapi==0.5.10 +pyhiveapi==0.5.11 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d43748f6fd..9c37efee222 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1035,7 +1035,7 @@ pyhaversion==22.4.1 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.5.10 +pyhiveapi==0.5.11 # homeassistant.components.homematic pyhomematic==0.1.77 From ad7da9803fe77656118b3ab956915832c49fe46b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 22 Jun 2022 22:37:01 +0200 Subject: [PATCH 1708/3516] Adjust lutron_caseta type hints (#73840) Co-authored-by: Franck Nijhof --- homeassistant/components/lutron_caseta/fan.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py index 44dae8324fa..46eee1bde6b 100644 --- a/homeassistant/components/lutron_caseta/fan.py +++ b/homeassistant/components/lutron_caseta/fan.py @@ -86,6 +86,6 @@ class LutronCasetaFan(LutronCasetaDeviceUpdatableEntity, FanEntity): await self._smartbridge.set_fan(self.device_id, named_speed) @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" - return self.percentage and self.percentage > 0 + return bool(self.percentage) From 320ef550851862089a859cfb3784093a251a4380 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Jun 2022 22:37:25 +0200 Subject: [PATCH 1709/3516] Automatically onboard Elgato (#73847) --- .../components/elgato/config_flow.py | 5 ++- tests/components/elgato/conftest.py | 10 ++++++ tests/components/elgato/test_config_flow.py | 36 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py index 7abf570ba3e..9e63df0a503 100644 --- a/homeassistant/components/elgato/config_flow.py +++ b/homeassistant/components/elgato/config_flow.py @@ -6,7 +6,7 @@ from typing import Any from elgato import Elgato, ElgatoError import voluptuous as vol -from homeassistant.components import zeroconf +from homeassistant.components import onboarding, zeroconf from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PORT from homeassistant.core import callback @@ -56,6 +56,9 @@ class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): except ElgatoError: return self.async_abort(reason="cannot_connect") + if not onboarding.async_is_onboarded(self.hass): + return self._async_create_entry() + self._set_confirm_only() return self.async_show_form( step_id="zeroconf_confirm", diff --git a/tests/components/elgato/conftest.py b/tests/components/elgato/conftest.py index efae0739c7b..b0d5415110d 100644 --- a/tests/components/elgato/conftest.py +++ b/tests/components/elgato/conftest.py @@ -37,6 +37,16 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]: yield mock_setup +@pytest.fixture +def mock_onboarding() -> Generator[None, MagicMock, None]: + """Mock that Home Assistant is currently onboarding.""" + with patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + yield mock_onboarding + + @pytest.fixture def mock_elgato_config_flow() -> Generator[None, MagicMock, None]: """Return a mocked Elgato client.""" diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py index fdc0ad834d1..0e3916a005e 100644 --- a/tests/components/elgato/test_config_flow.py +++ b/tests/components/elgato/test_config_flow.py @@ -204,3 +204,39 @@ async def test_zeroconf_device_exists_abort( entries = hass.config_entries.async_entries(DOMAIN) assert entries[0].data[CONF_HOST] == "127.0.0.2" + + +async def test_zeroconf_during_onboarding( + hass: HomeAssistant, + mock_elgato_config_flow: MagicMock, + mock_setup_entry: AsyncMock, + mock_onboarding: MagicMock, +) -> None: + """Test the zeroconf creates an entry during onboarding.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="127.0.0.1", + addresses=["127.0.0.1"], + hostname="example.local.", + name="mock_name", + port=9123, + properties={"id": "AA:BB:CC:DD:EE:FF"}, + type="mock_type", + ), + ) + + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("title") == "CN11A1A00001" + assert result.get("data") == { + CONF_HOST: "127.0.0.1", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + CONF_PORT: 9123, + } + assert "result" in result + assert result["result"].unique_id == "CN11A1A00001" + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_elgato_config_flow.info.mock_calls) == 1 + assert len(mock_onboarding.mock_calls) == 1 From ec119ae7186b4cb9e10992a3c4af9be946ebb3e2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Jun 2022 22:37:36 +0200 Subject: [PATCH 1710/3516] Automatically onboard WLED (#73853) --- homeassistant/components/wled/config_flow.py | 4 +-- tests/components/wled/conftest.py | 20 ++++++++--- tests/components/wled/test_config_flow.py | 38 ++++++++++++++++++-- 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index 99630f5781c..1dda368a2b0 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -6,7 +6,7 @@ from typing import Any import voluptuous as vol from wled import WLED, Device, WLEDConnectionError -from homeassistant.components import zeroconf +from homeassistant.components import onboarding, zeroconf from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_HOST, CONF_MAC from homeassistant.core import callback @@ -97,7 +97,7 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initiated by zeroconf.""" - if user_input is not None: + if user_input is not None or not onboarding.async_is_onboarded(self.hass): return self.async_create_entry( title=self.discovered_device.info.name, data={ diff --git a/tests/components/wled/conftest.py b/tests/components/wled/conftest.py index f89d92aaa16..d0b5b24a8fb 100644 --- a/tests/components/wled/conftest.py +++ b/tests/components/wled/conftest.py @@ -1,7 +1,7 @@ """Fixtures for WLED integration tests.""" from collections.abc import Generator import json -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest from wled import Device as WLEDDevice @@ -25,10 +25,22 @@ def mock_config_entry() -> MockConfigEntry: @pytest.fixture -def mock_setup_entry() -> Generator[None, None, None]: +def mock_setup_entry() -> Generator[None, AsyncMock, None]: """Mock setting up a config entry.""" - with patch("homeassistant.components.wled.async_setup_entry", return_value=True): - yield + with patch( + "homeassistant.components.wled.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup + + +@pytest.fixture +def mock_onboarding() -> Generator[None, MagicMock, None]: + """Mock that Home Assistant is currently onboarding.""" + with patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + yield mock_onboarding @pytest.fixture diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index c23f35534b8..e1cf08069da 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -1,5 +1,5 @@ """Tests for the WLED config flow.""" -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, MagicMock from wled import WLEDConnectionError @@ -18,7 +18,7 @@ from tests.common import MockConfigEntry async def test_full_user_flow_implementation( - hass: HomeAssistant, mock_wled_config_flow: MagicMock, mock_setup_entry: None + hass: HomeAssistant, mock_wled_config_flow: MagicMock, mock_setup_entry: AsyncMock ) -> None: """Test the full manual user flow from start to finish.""" result = await hass.config_entries.flow.async_init( @@ -43,7 +43,7 @@ async def test_full_user_flow_implementation( async def test_full_zeroconf_flow_implementation( - hass: HomeAssistant, mock_wled_config_flow: MagicMock, mock_setup_entry: None + hass: HomeAssistant, mock_wled_config_flow: MagicMock, mock_setup_entry: AsyncMock ) -> None: """Test the full manual user flow from start to finish.""" result = await hass.config_entries.flow.async_init( @@ -84,6 +84,38 @@ async def test_full_zeroconf_flow_implementation( assert result2["result"].unique_id == "aabbccddeeff" +async def test_zeroconf_during_onboarding( + hass: HomeAssistant, + mock_wled_config_flow: MagicMock, + mock_setup_entry: AsyncMock, + mock_onboarding: MagicMock, +) -> None: + """Test we create a config entry when discovered during onboarding.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.123", + addresses=["192.168.1.123"], + hostname="example.local.", + name="mock_name", + port=None, + properties={CONF_MAC: "aabbccddeeff"}, + type="mock_type", + ), + ) + + assert result.get("title") == "WLED RGB Light" + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + + assert result.get("data") == {CONF_HOST: "192.168.1.123"} + assert "result" in result + assert result["result"].unique_id == "aabbccddeeff" + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_onboarding.mock_calls) == 1 + + async def test_connection_error( hass: HomeAssistant, mock_wled_config_flow: MagicMock ) -> None: From a8a033681f4fd3d8d1d50e9f7e6a93c032fd0f8c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 22 Jun 2022 22:37:49 +0200 Subject: [PATCH 1711/3516] Automatically onboard DiscoveryFlows (#73841) --- homeassistant/helpers/config_entry_flow.py | 4 ++-- tests/helpers/test_config_entry_flow.py | 24 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index fddc5c82725..1190e947eba 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -6,7 +6,7 @@ import logging from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, cast from homeassistant import config_entries -from homeassistant.components import dhcp, mqtt, ssdp, zeroconf +from homeassistant.components import dhcp, mqtt, onboarding, ssdp, zeroconf from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult @@ -52,7 +52,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm setup.""" - if user_input is None: + if user_input is None and onboarding.async_is_onboarded(self.hass): self._set_confirm_only() return self.async_show_form(step_id="confirm") diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 78b28a8ef10..979aa8bf088 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -139,6 +139,30 @@ async def test_discovery_confirmation(hass, discovery_flow_conf, source): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY +@pytest.mark.parametrize( + "source", + [ + config_entries.SOURCE_DISCOVERY, + config_entries.SOURCE_MQTT, + config_entries.SOURCE_SSDP, + config_entries.SOURCE_ZEROCONF, + config_entries.SOURCE_DHCP, + ], +) +async def test_discovery_during_onboarding(hass, discovery_flow_conf, source): + """Test we create config entry via discovery during onboarding.""" + flow = config_entries.HANDLERS["test"]() + flow.hass = hass + flow.context = {"source": source} + + with patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ): + result = await getattr(flow, f"async_step_{source}")({}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + async def test_multiple_discoveries(hass, discovery_flow_conf): """Test we only create one instance for multiple discoveries.""" mock_entity_platform(hass, "config_flow.test", None) From 5c5fd746fd42f7e33271c3dbb0f12488297b7d0c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 22 Jun 2022 16:38:45 -0400 Subject: [PATCH 1712/3516] Add digital loggers as a Belkin supported brand (#72515) --- homeassistant/components/wemo/manifest.json | 5 ++++- homeassistant/components/wemo/wemo_device.py | 15 +++++++++++++-- tests/components/wemo/conftest.py | 15 +++++++++++++++ tests/components/wemo/test_wemo_device.py | 9 +++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index b324ba060ea..5486a192787 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -14,5 +14,8 @@ }, "codeowners": ["@esev"], "iot_class": "local_push", - "loggers": ["pywemo"] + "loggers": ["pywemo"], + "supported_brands": { + "digital_loggers": "Digital Loggers" + } } diff --git a/homeassistant/components/wemo/wemo_device.py b/homeassistant/components/wemo/wemo_device.py index 1f3e07881c8..826df24a108 100644 --- a/homeassistant/components/wemo/wemo_device.py +++ b/homeassistant/components/wemo/wemo_device.py @@ -9,6 +9,8 @@ from pywemo.subscribe import EVENT_TYPE_LONG_PRESS from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_CONFIGURATION_URL, + ATTR_IDENTIFIERS, CONF_DEVICE_ID, CONF_NAME, CONF_PARAMS, @@ -42,7 +44,7 @@ class DeviceCoordinator(DataUpdateCoordinator): self.hass = hass self.wemo = wemo self.device_id = device_id - self.device_info = _device_info(wemo) + self.device_info = _create_device_info(wemo) self.supports_long_press = wemo.supports_long_press() self.update_lock = asyncio.Lock() @@ -124,6 +126,15 @@ class DeviceCoordinator(DataUpdateCoordinator): raise UpdateFailed("WeMo update failed") from err +def _create_device_info(wemo: WeMoDevice) -> DeviceInfo: + """Create device information. Modify if special device.""" + _dev_info = _device_info(wemo) + if wemo.model_name == "DLI emulated Belkin Socket": + _dev_info[ATTR_CONFIGURATION_URL] = f"http://{wemo.host}" + _dev_info[ATTR_IDENTIFIERS] = {(DOMAIN, wemo.serialnumber[:-1])} + return _dev_info + + def _device_info(wemo: WeMoDevice) -> DeviceInfo: return DeviceInfo( connections={(CONNECTION_UPNP, wemo.udn)}, @@ -144,7 +155,7 @@ async def async_register_device( device_registry = async_get_device_registry(hass) entry = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, **_device_info(wemo) + config_entry_id=config_entry.entry_id, **_create_device_info(wemo) ) device = DeviceCoordinator(hass, wemo, entry.id) diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index 1a5998c1f94..6dc7b1e5d2c 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -97,6 +97,15 @@ def pywemo_device_fixture(pywemo_registry, pywemo_model): yield pywemo_device +@pytest.fixture(name="pywemo_dli_device") +def pywemo_dli_device_fixture(pywemo_registry, pywemo_model): + """Fixture for Digital Loggers emulated instances.""" + with create_pywemo_device(pywemo_registry, pywemo_model) as pywemo_dli_device: + pywemo_dli_device.model_name = "DLI emulated Belkin Socket" + pywemo_dli_device.serialnumber = "1234567891" + yield pywemo_dli_device + + @pytest.fixture(name="wemo_entity_suffix") def wemo_entity_suffix_fixture(): """Fixture to select a specific entity for wemo_entity.""" @@ -129,3 +138,9 @@ async def async_create_wemo_entity(hass, pywemo_device, wemo_entity_suffix): async def async_wemo_entity_fixture(hass, pywemo_device, wemo_entity_suffix): """Fixture for a Wemo entity in hass.""" return await async_create_wemo_entity(hass, pywemo_device, wemo_entity_suffix) + + +@pytest.fixture(name="wemo_dli_entity") +async def async_wemo_dli_entity_fixture(hass, pywemo_dli_device, wemo_entity_suffix): + """Fixture for a Wemo entity in hass.""" + return await async_create_wemo_entity(hass, pywemo_dli_device, wemo_entity_suffix) diff --git a/tests/components/wemo/test_wemo_device.py b/tests/components/wemo/test_wemo_device.py index 9bd3367aeee..a16efb173ae 100644 --- a/tests/components/wemo/test_wemo_device.py +++ b/tests/components/wemo/test_wemo_device.py @@ -168,6 +168,15 @@ async def test_device_info(hass, wemo_entity): assert device_entries[0].sw_version == MOCK_FIRMWARE_VERSION +async def test_dli_device_info(hass, wemo_dli_entity): + """Verify the DeviceInfo data for Digital Loggers emulated wemo device.""" + dr = device_registry.async_get(hass) + device_entries = list(dr.devices.values()) + + assert device_entries[0].configuration_url == "http://127.0.0.1" + assert device_entries[0].identifiers == {(DOMAIN, "123456789")} + + class TestInsight: """Tests specific to the WeMo Insight device.""" From 01a9367281ba71ce7560cd3b51463fdca13475e6 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 22 Jun 2022 16:57:21 -0400 Subject: [PATCH 1713/3516] UniFi Protect cleanup and enable unadopted devices (#73860) --- .../components/unifiprotect/__init__.py | 11 ++- .../components/unifiprotect/binary_sensor.py | 14 ++-- .../components/unifiprotect/button.py | 2 +- .../components/unifiprotect/camera.py | 11 ++- .../components/unifiprotect/config_flow.py | 8 +- homeassistant/components/unifiprotect/data.py | 4 +- .../components/unifiprotect/entity.py | 13 +-- .../components/unifiprotect/light.py | 6 +- homeassistant/components/unifiprotect/lock.py | 21 ++--- .../components/unifiprotect/media_player.py | 26 +++--- .../components/unifiprotect/migrate.py | 6 +- .../components/unifiprotect/models.py | 8 +- .../components/unifiprotect/select.py | 9 +- .../components/unifiprotect/sensor.py | 17 ++-- .../components/unifiprotect/switch.py | 9 +- .../components/unifiprotect/utils.py | 34 +------- tests/components/unifiprotect/conftest.py | 2 +- .../unifiprotect/test_binary_sensor.py | 15 +++- tests/components/unifiprotect/test_camera.py | 15 +++- tests/components/unifiprotect/test_init.py | 24 ++++++ tests/components/unifiprotect/test_light.py | 10 ++- tests/components/unifiprotect/test_lock.py | 10 ++- .../unifiprotect/test_media_player.py | 13 ++- tests/components/unifiprotect/test_migrate.py | 84 +++++++++++++++++++ 24 files changed, 258 insertions(+), 114 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index c83221b0ccf..40214b60766 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -61,6 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: subscribed_models=DEVICES_FOR_SUBSCRIBE, override_connection_host=entry.options.get(CONF_OVERRIDE_CHOST, False), ignore_stats=not entry.options.get(CONF_ALL_UPDATES, False), + ignore_unadopted=False, ) _LOGGER.debug("Connect to UniFi Protect") data_service = ProtectData(hass, protect, SCAN_INTERVAL, entry) @@ -127,7 +128,9 @@ async def async_remove_config_entry_device( } api = async_ufp_instance_for_config_entry_ids(hass, {config_entry.entry_id}) assert api is not None - return api.bootstrap.nvr.mac not in unifi_macs and not any( - device.mac in unifi_macs - for device in async_get_devices(api.bootstrap, DEVICES_THAT_ADOPT) - ) + if api.bootstrap.nvr.mac in unifi_macs: + return False + for device in async_get_devices(api.bootstrap, DEVICES_THAT_ADOPT): + if device.is_adopted_by_us and device.mac in unifi_macs: + return False + return True diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 68c395faaf7..eb4b2024233 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -35,7 +35,6 @@ from .entity import ( async_all_device_entities, ) from .models import PermRequired, ProtectRequiredKeysMixin -from .utils import async_get_is_highfps _LOGGER = logging.getLogger(__name__) _KEY_DOOR = "door" @@ -103,7 +102,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( icon="mdi:video-high-definition", entity_category=EntityCategory.DIAGNOSTIC, ufp_required_field="feature_flags.has_highfps", - ufp_value_fn=async_get_is_highfps, + ufp_value="is_high_fps_enabled", ufp_perm=PermRequired.NO_WRITE, ), ProtectBinaryEntityDescription( @@ -386,12 +385,15 @@ def _async_motion_entities( ) -> list[ProtectDeviceEntity]: entities: list[ProtectDeviceEntity] = [] for device in data.api.bootstrap.cameras.values(): + if not device.is_adopted_by_us: + continue + for description in MOTION_SENSORS: entities.append(ProtectEventBinarySensor(data, device, description)) _LOGGER.debug( "Adding binary sensor entity %s for %s", description.name, - device.name, + device.display_name, ) return entities @@ -468,9 +470,9 @@ class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity): slot = self._disk.slot self._attr_available = False - if self.device.system_info.ustorage is None: - return - + # should not be possible since it would require user to + # _downgrade_ to make ustorage disppear + assert self.device.system_info.ustorage is not None for disk in self.device.system_info.ustorage.disks: if disk.slot == slot: self._disk = disk diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 01714868261..d647cdac64a 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -103,7 +103,7 @@ class ProtectButton(ProtectDeviceEntity, ButtonEntity): ) -> None: """Initialize an UniFi camera.""" super().__init__(data, device, description) - self._attr_name = f"{self.device.name} {self.entity_description.name}" + self._attr_name = f"{self.device.display_name} {self.entity_description.name}" async def async_press(self) -> None: """Press the button.""" diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index c78e8e2f77a..a84346a8384 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -36,9 +36,14 @@ def get_camera_channels( ) -> Generator[tuple[UFPCamera, CameraChannel, bool], None, None]: """Get all the camera channels.""" for camera in protect.bootstrap.cameras.values(): + if not camera.is_adopted_by_us: + continue + if not camera.channels: _LOGGER.warning( - "Camera does not have any channels: %s (id: %s)", camera.name, camera.id + "Camera does not have any channels: %s (id: %s)", + camera.display_name, + camera.id, ) continue @@ -116,10 +121,10 @@ class ProtectCamera(ProtectDeviceEntity, Camera): if self._secure: self._attr_unique_id = f"{self.device.mac}_{self.channel.id}" - self._attr_name = f"{self.device.name} {self.channel.name}" + self._attr_name = f"{self.device.display_name} {self.channel.name}" else: self._attr_unique_id = f"{self.device.mac}_{self.channel.id}_insecure" - self._attr_name = f"{self.device.name} {self.channel.name} Insecure" + self._attr_name = f"{self.device.display_name} {self.channel.name} Insecure" # only the default (first) channel is enabled by default self._attr_entity_registry_enabled_default = is_default and secure diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 9cd15c4e3c2..8e114c4f38b 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -175,9 +175,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_VERIFY_SSL] = False nvr_data, errors = await self._async_get_nvr_data(user_input) if nvr_data and not errors: - return self._async_create_entry( - nvr_data.name or nvr_data.type, user_input - ) + return self._async_create_entry(nvr_data.display_name, user_input) placeholders = { "name": discovery_info["hostname"] @@ -323,9 +321,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(nvr_data.mac) self._abort_if_unique_id_configured() - return self._async_create_entry( - nvr_data.name or nvr_data.type, user_input - ) + return self._async_create_entry(nvr_data.display_name, user_input) user_input = user_input or {} return self.async_show_form( diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 78a3c5ebac8..4a20e816ce2 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -120,7 +120,9 @@ class ProtectData: @callback def _async_process_ws_message(self, message: WSSubscriptionMessage) -> None: # removed packets are not processed yet - if message.new_obj is None: # pragma: no cover + if message.new_obj is None or not getattr( + message.new_obj, "is_adopted_by_us", True + ): return if message.new_obj.model in DEVICES_WITH_ENTITIES: diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 9bf3c8de7a0..65734569de2 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -44,6 +44,9 @@ def _async_device_entities( entities: list[ProtectDeviceEntity] = [] for device in data.get_by_types({model_type}): + if not device.is_adopted_by_us: + continue + assert isinstance(device, (Camera, Light, Sensor, Viewer, Doorlock, Chime)) for description in descs: if description.ufp_perm is not None: @@ -69,7 +72,7 @@ def _async_device_entities( "Adding %s entity %s for %s", klass.__name__, description.name, - device.name, + device.display_name, ) return entities @@ -126,12 +129,12 @@ class ProtectDeviceEntity(Entity): if description is None: self._attr_unique_id = f"{self.device.mac}" - self._attr_name = f"{self.device.name}" + self._attr_name = f"{self.device.display_name}" else: self.entity_description = description self._attr_unique_id = f"{self.device.mac}_{description.key}" name = description.name or "" - self._attr_name = f"{self.device.name} {name.title()}" + self._attr_name = f"{self.device.display_name} {name.title()}" self._attr_attribution = DEFAULT_ATTRIBUTION self._async_set_device_info() @@ -147,7 +150,7 @@ class ProtectDeviceEntity(Entity): @callback def _async_set_device_info(self) -> None: self._attr_device_info = DeviceInfo( - name=self.device.name, + name=self.device.display_name, manufacturer=DEFAULT_BRAND, model=self.device.type, via_device=(DOMAIN, self.data.api.bootstrap.nvr.mac), @@ -214,7 +217,7 @@ class ProtectNVREntity(ProtectDeviceEntity): connections={(dr.CONNECTION_NETWORK_MAC, self.device.mac)}, identifiers={(DOMAIN, self.device.mac)}, manufacturer=DEFAULT_BRAND, - name=self.device.name, + name=self.device.display_name, model=self.device.type, sw_version=str(self.device.version), configuration_url=self.device.api.base_url, diff --git a/homeassistant/components/unifiprotect/light.py b/homeassistant/components/unifiprotect/light.py index b200fb85e03..bd64905a289 100644 --- a/homeassistant/components/unifiprotect/light.py +++ b/homeassistant/components/unifiprotect/light.py @@ -27,12 +27,12 @@ async def async_setup_entry( data: ProtectData = hass.data[DOMAIN][entry.entry_id] entities = [] for device in data.api.bootstrap.lights.values(): + if not device.is_adopted_by_us: + continue + if device.can_write(data.api.bootstrap.auth_user): entities.append(ProtectLight(data, device)) - if not entities: - return - async_add_entities(entities) diff --git a/homeassistant/components/unifiprotect/lock.py b/homeassistant/components/unifiprotect/lock.py index 9cef3e19e36..7258dc5f952 100644 --- a/homeassistant/components/unifiprotect/lock.py +++ b/homeassistant/components/unifiprotect/lock.py @@ -26,13 +26,14 @@ async def async_setup_entry( """Set up locks on a UniFi Protect NVR.""" data: ProtectData = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - ProtectLock( - data, - lock, - ) - for lock in data.api.bootstrap.doorlocks.values() - ) + entities = [] + for device in data.api.bootstrap.doorlocks.values(): + if not device.is_adopted_by_us: + continue + + entities.append(ProtectLock(data, device)) + + async_add_entities(entities) class ProtectLock(ProtectDeviceEntity, LockEntity): @@ -53,7 +54,7 @@ class ProtectLock(ProtectDeviceEntity, LockEntity): LockEntityDescription(key="lock"), ) - self._attr_name = f"{self.device.name} Lock" + self._attr_name = f"{self.device.display_name} Lock" @callback def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: @@ -82,10 +83,10 @@ class ProtectLock(ProtectDeviceEntity, LockEntity): async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" - _LOGGER.debug("Unlocking %s", self.device.name) + _LOGGER.debug("Unlocking %s", self.device.display_name) return await self.device.open_lock() async def async_lock(self, **kwargs: Any) -> None: """Lock the lock.""" - _LOGGER.debug("Locking %s", self.device.name) + _LOGGER.debug("Locking %s", self.device.display_name) return await self.device.close_lock() diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index c4fb1dbe15b..b0391c9d860 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -40,16 +40,14 @@ async def async_setup_entry( """Discover cameras with speakers on a UniFi Protect NVR.""" data: ProtectData = hass.data[DOMAIN][entry.entry_id] - async_add_entities( - [ - ProtectMediaPlayer( - data, - camera, - ) - for camera in data.api.bootstrap.cameras.values() - if camera.feature_flags.has_speaker - ] - ) + entities = [] + for device in data.api.bootstrap.cameras.values(): + if not device.is_adopted_by_us or not device.feature_flags.has_speaker: + continue + + entities.append(ProtectMediaPlayer(data, device)) + + async_add_entities(entities) class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): @@ -79,7 +77,7 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): ), ) - self._attr_name = f"{self.device.name} Speaker" + self._attr_name = f"{self.device.display_name} Speaker" self._attr_media_content_type = MEDIA_TYPE_MUSIC @callback @@ -108,7 +106,7 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): self.device.talkback_stream is not None and self.device.talkback_stream.is_running ): - _LOGGER.debug("Stopping playback for %s Speaker", self.device.name) + _LOGGER.debug("Stopping playback for %s Speaker", self.device.display_name) await self.device.stop_audio() self._async_updated_event(self.device) @@ -126,7 +124,9 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): if media_type != MEDIA_TYPE_MUSIC: raise HomeAssistantError("Only music media type is supported") - _LOGGER.debug("Playing Media %s for %s Speaker", media_id, self.device.name) + _LOGGER.debug( + "Playing Media %s for %s Speaker", media_id, self.device.display_name + ) await self.async_media_stop() try: await self.device.play_audio(media_id, blocking=False) diff --git a/homeassistant/components/unifiprotect/migrate.py b/homeassistant/components/unifiprotect/migrate.py index 3273bd80408..893ca3e458a 100644 --- a/homeassistant/components/unifiprotect/migrate.py +++ b/homeassistant/components/unifiprotect/migrate.py @@ -14,8 +14,6 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import entity_registry as er -from .utils import async_device_by_id - _LOGGER = logging.getLogger(__name__) @@ -69,7 +67,7 @@ async def async_migrate_buttons( bootstrap = await async_get_bootstrap(protect) count = 0 for button in to_migrate: - device = async_device_by_id(bootstrap, button.unique_id) + device = bootstrap.get_device_from_id(button.unique_id) if device is None: continue @@ -130,7 +128,7 @@ async def async_migrate_device_ids( if parts[0] == bootstrap.nvr.id: device: NVR | ProtectAdoptableDeviceModel | None = bootstrap.nvr else: - device = async_device_by_id(bootstrap, parts[0]) + device = bootstrap.get_device_from_id(parts[0]) if device is None: continue diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index 81ad8438dd7..dee2006b429 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -5,9 +5,9 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from enum import Enum import logging -from typing import Any, Generic, TypeVar +from typing import Any, Generic, TypeVar, Union -from pyunifiprotect.data import ProtectDeviceModel +from pyunifiprotect.data import NVR, ProtectAdoptableDeviceModel from homeassistant.helpers.entity import EntityDescription @@ -15,7 +15,7 @@ from .utils import get_nested_attr _LOGGER = logging.getLogger(__name__) -T = TypeVar("T", bound=ProtectDeviceModel) +T = TypeVar("T", bound=Union[ProtectAdoptableDeviceModel, NVR]) class PermRequired(int, Enum): @@ -63,7 +63,7 @@ class ProtectSetableKeysMixin(ProtectRequiredKeysMixin[T]): async def ufp_set(self, obj: T, value: Any) -> None: """Set value for UniFi Protect device.""" - _LOGGER.debug("Setting %s to %s for %s", self.name, value, obj.name) + _LOGGER.debug("Setting %s to %s for %s", self.name, value, obj.display_name) if self.ufp_set_method is not None: await getattr(obj, self.ufp_set_method)(value) elif self.ufp_set_method_fn is not None: diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 15377a37b27..17bdfa390a6 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -143,7 +143,7 @@ def _get_doorbell_options(api: ProtectApiClient) -> list[dict[str, Any]]: def _get_paired_camera_options(api: ProtectApiClient) -> list[dict[str, Any]]: options = [{"id": TYPE_EMPTY_VALUE, "name": "Not Paired"}] for camera in api.bootstrap.cameras.values(): - options.append({"id": camera.id, "name": camera.name or camera.type}) + options.append({"id": camera.id, "name": camera.display_name or camera.type}) return options @@ -353,7 +353,7 @@ class ProtectSelects(ProtectDeviceEntity, SelectEntity): ) -> None: """Initialize the unifi protect select entity.""" super().__init__(data, device, description) - self._attr_name = f"{self.device.name} {self.entity_description.name}" + self._attr_name = f"{self.device.display_name} {self.entity_description.name}" self._async_set_options() @callback @@ -421,7 +421,10 @@ class ProtectSelects(ProtectDeviceEntity, SelectEntity): timeout_msg = f" with timeout of {duration} minute(s)" _LOGGER.debug( - 'Setting message for %s to "%s"%s', self.device.name, message, timeout_msg + 'Setting message for %s to "%s"%s', + self.device.display_name, + message, + timeout_msg, ) await self.device.set_lcd_text( DoorbellMessageType.CUSTOM_MESSAGE, message, reset_at=reset_at diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 57fe0d5aabd..012d52ae215 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -108,7 +108,7 @@ def _get_alarm_sound(obj: Sensor) -> str: ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( - ProtectSensorEntityDescription[ProtectDeviceModel]( + ProtectSensorEntityDescription( key="uptime", name="Uptime", icon="mdi:clock", @@ -353,7 +353,7 @@ SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( name="Paired Camera", icon="mdi:cctv", entity_category=EntityCategory.DIAGNOSTIC, - ufp_value="camera.name", + ufp_value="camera.display_name", ufp_perm=PermRequired.NO_WRITE, ), ) @@ -373,13 +373,13 @@ DOORLOCK_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( name="Paired Camera", icon="mdi:cctv", entity_category=EntityCategory.DIAGNOSTIC, - ufp_value="camera.name", + ufp_value="camera.display_name", ufp_perm=PermRequired.NO_WRITE, ), ) NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( - ProtectSensorEntityDescription[ProtectDeviceModel]( + ProtectSensorEntityDescription( key="uptime", name="Uptime", icon="mdi:clock", @@ -541,7 +541,7 @@ LIGHT_SENSORS: tuple[ProtectSensorEntityDescription, ...] = ( name="Paired Camera", icon="mdi:cctv", entity_category=EntityCategory.DIAGNOSTIC, - ufp_value="camera.name", + ufp_value="camera.display_name", ufp_perm=PermRequired.NO_WRITE, ), ) @@ -618,11 +618,14 @@ def _async_motion_entities( entities: list[ProtectDeviceEntity] = [] for device in data.api.bootstrap.cameras.values(): for description in MOTION_TRIP_SENSORS: + if not device.is_adopted_by_us: + continue + entities.append(ProtectDeviceSensor(data, device, description)) _LOGGER.debug( "Adding trip sensor entity %s for %s", description.name, - device.name, + device.display_name, ) if not device.feature_flags.has_smart_detect: @@ -633,7 +636,7 @@ def _async_motion_entities( _LOGGER.debug( "Adding sensor entity %s for %s", description.name, - device.name, + device.display_name, ) return entities diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index efa91b3a6ba..8b3661ce324 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -22,7 +22,6 @@ from .const import DOMAIN from .data import ProtectData from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectSetableKeysMixin, T -from .utils import async_get_is_highfps _LOGGER = logging.getLogger(__name__) @@ -81,7 +80,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( icon="mdi:video-high-definition", entity_category=EntityCategory.CONFIG, ufp_required_field="feature_flags.has_highfps", - ufp_value_fn=async_get_is_highfps, + ufp_value="is_high_fps_enabled", ufp_set_method_fn=_set_highfps, ufp_perm=PermRequired.WRITE, ), @@ -328,7 +327,7 @@ class ProtectSwitch(ProtectDeviceEntity, SwitchEntity): ) -> None: """Initialize an UniFi Protect Switch.""" super().__init__(data, device, description) - self._attr_name = f"{self.device.name} {self.entity_description.name}" + self._attr_name = f"{self.device.display_name} {self.entity_description.name}" self._switch_type = self.entity_description.key if not isinstance(self.device, Camera): @@ -362,7 +361,9 @@ class ProtectSwitch(ProtectDeviceEntity, SwitchEntity): if self._switch_type == _KEY_PRIVACY_MODE: assert isinstance(self.device, Camera) - _LOGGER.debug("Setting Privacy Mode to false for %s", self.device.name) + _LOGGER.debug( + "Setting Privacy Mode to false for %s", self.device.display_name + ) await self.device.set_privacy( False, self._previous_mic_level, self._previous_record_mode ) diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index b2eb8c1ca65..72baab334f3 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -9,18 +9,15 @@ from typing import Any from pyunifiprotect.data import ( Bootstrap, - Camera, Light, LightModeEnableType, LightModeType, ProtectAdoptableDeviceModel, - ProtectDeviceModel, - VideoMode, ) from homeassistant.core import HomeAssistant, callback -from .const import DEVICES_THAT_ADOPT, ModelType +from .const import ModelType def get_nested_attr(obj: Any, attr: str) -> Any: @@ -79,30 +76,10 @@ def async_get_devices_by_type( return devices -@callback -def async_device_by_id( - bootstrap: Bootstrap, - device_id: str, - device_type: ModelType | None = None, -) -> ProtectAdoptableDeviceModel | None: - """Get devices by type.""" - - device_types = DEVICES_THAT_ADOPT - if device_type is not None: - device_types = {device_type} - - device = None - for model in device_types: - device = async_get_devices_by_type(bootstrap, model).get(device_id) - if device is not None: - break - return device - - @callback def async_get_devices( bootstrap: Bootstrap, model_type: Iterable[ModelType] -) -> Generator[ProtectDeviceModel, None, None]: +) -> Generator[ProtectAdoptableDeviceModel, None, None]: """Return all device by type.""" return ( device @@ -111,13 +88,6 @@ def async_get_devices( ) -@callback -def async_get_is_highfps(obj: Camera) -> bool: - """Return if camera has High FPS mode enabled.""" - - return bool(obj.video_mode == VideoMode.HIGH_FPS) - - @callback def async_get_light_motion_current(obj: Light) -> str: """Get light motion mode for Flood Light.""" diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index adc69cc8bf9..68945ac0988 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -284,7 +284,7 @@ def ids_from_device_description( def generate_random_ids() -> tuple[str, str]: """Generate random IDs for device.""" - return random_hex(24).upper(), random_hex(12).upper() + return random_hex(24).lower(), random_hex(12).upper() def regenerate_device_ids(device: ProtectAdoptableDeviceModel) -> None: diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 8e868b4af21..da9969ad868 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -36,6 +36,7 @@ from .conftest import ( MockEntityFixture, assert_entity_counts, ids_from_device_description, + regenerate_device_ids, reset_objects, ) @@ -65,11 +66,22 @@ async def camera_fixture( camera_obj.last_ring = now - timedelta(hours=1) camera_obj.is_dark = False camera_obj.is_motion_detected = False + regenerate_device_ids(camera_obj) + + no_camera_obj = mock_camera.copy() + no_camera_obj._api = mock_entry.api + no_camera_obj.channels[0]._api = mock_entry.api + no_camera_obj.channels[1]._api = mock_entry.api + no_camera_obj.channels[2]._api = mock_entry.api + no_camera_obj.name = "Unadopted Camera" + no_camera_obj.is_adopted = False + regenerate_device_ids(no_camera_obj) reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] mock_entry.api.bootstrap.cameras = { camera_obj.id: camera_obj, + no_camera_obj.id: no_camera_obj, } await hass.config_entries.async_setup(mock_entry.entry.entry_id) @@ -135,6 +147,7 @@ async def camera_none_fixture( reset_objects(mock_entry.api.bootstrap) mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] + mock_entry.api.bootstrap.nvr.system_info.ustorage = None mock_entry.api.bootstrap.cameras = { camera_obj.id: camera_obj, } @@ -142,7 +155,7 @@ async def camera_none_fixture( await hass.config_entries.async_setup(mock_entry.entry.entry_id) await hass.async_block_till_done() - assert_entity_counts(hass, Platform.BINARY_SENSOR, 8, 8) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 2, 2) yield camera_obj diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index 03b52c7e52e..66da8e8ec04 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -529,13 +529,22 @@ async def test_camera_ws_update( new_camera = camera[0].copy() new_camera.is_recording = True - mock_msg = Mock() - mock_msg.changed_data = {} - mock_msg.new_obj = new_camera + no_camera = camera[0].copy() + no_camera.is_adopted = False new_bootstrap.cameras = {new_camera.id: new_camera} mock_entry.api.bootstrap = new_bootstrap + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = new_camera mock_entry.api.ws_subscription(mock_msg) + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = no_camera + mock_entry.api.ws_subscription(mock_msg) + await hass.async_block_till_done() state = hass.states.get(camera[1]) diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index d36183ba135..5c06eedc4c9 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -242,3 +242,27 @@ async def test_device_remove_devices( await remove_device(await hass_ws_client(hass), dead_device_entry.id, entry_id) is True ) + + +async def test_device_remove_devices_nvr( + hass: HomeAssistant, + mock_entry: MockEntityFixture, + hass_ws_client: Callable[ + [HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse] + ], +) -> None: + """Test we can only remove a NVR device that no longer exists.""" + assert await async_setup_component(hass, "config", {}) + + mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + entry_id = mock_entry.entry.entry_id + + device_registry = dr.async_get(hass) + + live_device_entry = list(device_registry.devices.values())[0] + assert ( + await remove_device(await hass_ws_client(hass), live_device_entry.id, entry_id) + is False + ) diff --git a/tests/components/unifiprotect/test_light.py b/tests/components/unifiprotect/test_light.py index c4f324f30fd..3bcca436911 100644 --- a/tests/components/unifiprotect/test_light.py +++ b/tests/components/unifiprotect/test_light.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import MockEntityFixture, assert_entity_counts +from .conftest import MockEntityFixture, assert_entity_counts, regenerate_device_ids @pytest.fixture(name="light") @@ -36,9 +36,17 @@ async def light_fixture( light_obj._api = mock_entry.api light_obj.name = "Test Light" light_obj.is_light_on = False + regenerate_device_ids(light_obj) + + no_light_obj = mock_light.copy() + no_light_obj._api = mock_entry.api + no_light_obj.name = "Unadopted Light" + no_light_obj.is_adopted = False + regenerate_device_ids(no_light_obj) mock_entry.api.bootstrap.lights = { light_obj.id: light_obj, + no_light_obj.id: no_light_obj, } await hass.config_entries.async_setup(mock_entry.entry.entry_id) diff --git a/tests/components/unifiprotect/test_lock.py b/tests/components/unifiprotect/test_lock.py index 36b3d140871..3ebfd2de22f 100644 --- a/tests/components/unifiprotect/test_lock.py +++ b/tests/components/unifiprotect/test_lock.py @@ -23,7 +23,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import MockEntityFixture, assert_entity_counts +from .conftest import MockEntityFixture, assert_entity_counts, regenerate_device_ids @pytest.fixture(name="doorlock") @@ -39,9 +39,17 @@ async def doorlock_fixture( lock_obj._api = mock_entry.api lock_obj.name = "Test Lock" lock_obj.lock_status = LockStatusType.OPEN + regenerate_device_ids(lock_obj) + + no_lock_obj = mock_doorlock.copy() + no_lock_obj._api = mock_entry.api + no_lock_obj.name = "Unadopted Lock" + no_lock_obj.is_adopted = False + regenerate_device_ids(no_lock_obj) mock_entry.api.bootstrap.doorlocks = { lock_obj.id: lock_obj, + no_lock_obj.id: no_lock_obj, } await hass.config_entries.async_setup(mock_entry.entry.entry_id) diff --git a/tests/components/unifiprotect/test_media_player.py b/tests/components/unifiprotect/test_media_player.py index a4cbc9e8d22..c18a407eadb 100644 --- a/tests/components/unifiprotect/test_media_player.py +++ b/tests/components/unifiprotect/test_media_player.py @@ -26,7 +26,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from .conftest import MockEntityFixture, assert_entity_counts +from .conftest import MockEntityFixture, assert_entity_counts, regenerate_device_ids @pytest.fixture(name="camera") @@ -45,9 +45,20 @@ async def camera_fixture( camera_obj.channels[2]._api = mock_entry.api camera_obj.name = "Test Camera" camera_obj.feature_flags.has_speaker = True + regenerate_device_ids(camera_obj) + + no_camera_obj = mock_camera.copy() + no_camera_obj._api = mock_entry.api + no_camera_obj.channels[0]._api = mock_entry.api + no_camera_obj.channels[1]._api = mock_entry.api + no_camera_obj.channels[2]._api = mock_entry.api + no_camera_obj.name = "Unadopted Camera" + no_camera_obj.is_adopted = False + regenerate_device_ids(no_camera_obj) mock_entry.api.bootstrap.cameras = { camera_obj.id: camera_obj, + no_camera_obj.id: no_camera_obj, } await hass.config_entries.async_setup(mock_entry.entry.entry_id) diff --git a/tests/components/unifiprotect/test_migrate.py b/tests/components/unifiprotect/test_migrate.py index b62aa9d7757..206c85e3654 100644 --- a/tests/components/unifiprotect/test_migrate.py +++ b/tests/components/unifiprotect/test_migrate.py @@ -5,6 +5,8 @@ from __future__ import annotations from unittest.mock import AsyncMock from pyunifiprotect.data import Light +from pyunifiprotect.data.bootstrap import ProtectDeviceRef +from pyunifiprotect.exceptions import NvrError from homeassistant.components.unifiprotect.const import DOMAIN from homeassistant.config_entries import ConfigEntryState @@ -34,6 +36,10 @@ async def test_migrate_reboot_button( light1.id: light1, light2.id: light2, } + mock_entry.api.bootstrap.id_lookup = { + light1.id: ProtectDeviceRef(id=light1.id, model=light1.model), + light2.id: ProtectDeviceRef(id=light2.id, model=light2.model), + } mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) registry = er.async_get(hass) @@ -77,6 +83,41 @@ async def test_migrate_reboot_button( assert light.unique_id == f"{light2.mac}_reboot" +async def test_migrate_nvr_mac( + hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light +): + """Test migrating unique ID of NVR to use MAC address.""" + + mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) + nvr = mock_entry.api.bootstrap.nvr + regenerate_device_ids(nvr) + + registry = er.async_get(hass) + registry.async_get_or_create( + Platform.SENSOR, + DOMAIN, + f"{nvr.id}_storage_utilization", + config_entry=mock_entry.entry, + ) + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + + assert mock_entry.entry.state == ConfigEntryState.LOADED + assert mock_entry.api.update.called + assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac + + assert registry.async_get(f"{Platform.SENSOR}.{DOMAIN}_storage_utilization") is None + assert ( + registry.async_get(f"{Platform.SENSOR}.{DOMAIN}_storage_utilization_2") is None + ) + sensor = registry.async_get( + f"{Platform.SENSOR}.{DOMAIN}_{nvr.id}_storage_utilization" + ) + assert sensor is not None + assert sensor.unique_id == f"{nvr.mac}_storage_utilization" + + async def test_migrate_reboot_button_no_device( hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light ): @@ -132,6 +173,9 @@ async def test_migrate_reboot_button_fail( mock_entry.api.bootstrap.lights = { light1.id: light1, } + mock_entry.api.bootstrap.id_lookup = { + light1.id: ProtectDeviceRef(id=light1.id, model=light1.model), + } mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) registry = er.async_get(hass) @@ -175,6 +219,9 @@ async def test_migrate_device_mac_button_fail( mock_entry.api.bootstrap.lights = { light1.id: light1, } + mock_entry.api.bootstrap.id_lookup = { + light1.id: ProtectDeviceRef(id=light1.id, model=light1.model) + } mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) registry = er.async_get(hass) @@ -203,3 +250,40 @@ async def test_migrate_device_mac_button_fail( light = registry.async_get(f"{Platform.BUTTON}.test_light_1") assert light is not None assert light.unique_id == f"{light1.id}_reboot" + + +async def test_migrate_device_mac_bootstrap_fail( + hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light +): + """Test migrating with a network error.""" + + light1 = mock_light.copy() + light1._api = mock_entry.api + light1.name = "Test Light 1" + regenerate_device_ids(light1) + + mock_entry.api.bootstrap.lights = { + light1.id: light1, + } + mock_entry.api.get_bootstrap = AsyncMock(side_effect=NvrError) + + registry = er.async_get(hass) + registry.async_get_or_create( + Platform.BUTTON, + DOMAIN, + f"{light1.id}_reboot", + config_entry=mock_entry.entry, + suggested_object_id=light1.name, + ) + registry.async_get_or_create( + Platform.BUTTON, + DOMAIN, + f"{light1.mac}_reboot", + config_entry=mock_entry.entry, + suggested_object_id=light1.name, + ) + + await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.async_block_till_done() + + assert mock_entry.entry.state == ConfigEntryState.SETUP_RETRY From aef69f87f4b46545054d7ea023eb1e5324921670 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 22 Jun 2022 23:02:34 +0200 Subject: [PATCH 1714/3516] More enums in deCONZ Alarm Control Panel (#73800) --- .../components/deconz/alarm_control_panel.py | 41 ++++++++----------- .../components/deconz/deconz_event.py | 15 +++---- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../deconz/test_alarm_control_panel.py | 37 +++++++---------- tests/components/deconz/test_deconz_event.py | 29 ++++++------- 7 files changed, 52 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py index 90c34da0f12..bf0f39b75d0 100644 --- a/homeassistant/components/deconz/alarm_control_panel.py +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -1,20 +1,11 @@ """Support for deCONZ alarm control panel devices.""" from __future__ import annotations -from pydeconz.interfaces.alarm_systems import ArmAction +from pydeconz.models.alarm_system import AlarmSystemArmAction from pydeconz.models.event import EventType from pydeconz.models.sensor.ancillary_control import ( - ANCILLARY_CONTROL_ARMED_AWAY, - ANCILLARY_CONTROL_ARMED_NIGHT, - ANCILLARY_CONTROL_ARMED_STAY, - ANCILLARY_CONTROL_ARMING_AWAY, - ANCILLARY_CONTROL_ARMING_NIGHT, - ANCILLARY_CONTROL_ARMING_STAY, - ANCILLARY_CONTROL_DISARMED, - ANCILLARY_CONTROL_ENTRY_DELAY, - ANCILLARY_CONTROL_EXIT_DELAY, - ANCILLARY_CONTROL_IN_ALARM, AncillaryControl, + AncillaryControlPanel, ) from homeassistant.components.alarm_control_panel import ( @@ -40,16 +31,16 @@ from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry DECONZ_TO_ALARM_STATE = { - ANCILLARY_CONTROL_ARMED_AWAY: STATE_ALARM_ARMED_AWAY, - ANCILLARY_CONTROL_ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT, - ANCILLARY_CONTROL_ARMED_STAY: STATE_ALARM_ARMED_HOME, - ANCILLARY_CONTROL_ARMING_AWAY: STATE_ALARM_ARMING, - ANCILLARY_CONTROL_ARMING_NIGHT: STATE_ALARM_ARMING, - ANCILLARY_CONTROL_ARMING_STAY: STATE_ALARM_ARMING, - ANCILLARY_CONTROL_DISARMED: STATE_ALARM_DISARMED, - ANCILLARY_CONTROL_ENTRY_DELAY: STATE_ALARM_PENDING, - ANCILLARY_CONTROL_EXIT_DELAY: STATE_ALARM_PENDING, - ANCILLARY_CONTROL_IN_ALARM: STATE_ALARM_TRIGGERED, + AncillaryControlPanel.ARMED_AWAY: STATE_ALARM_ARMED_AWAY, + AncillaryControlPanel.ARMED_NIGHT: STATE_ALARM_ARMED_NIGHT, + AncillaryControlPanel.ARMED_STAY: STATE_ALARM_ARMED_HOME, + AncillaryControlPanel.ARMING_AWAY: STATE_ALARM_ARMING, + AncillaryControlPanel.ARMING_NIGHT: STATE_ALARM_ARMING, + AncillaryControlPanel.ARMING_STAY: STATE_ALARM_ARMING, + AncillaryControlPanel.DISARMED: STATE_ALARM_DISARMED, + AncillaryControlPanel.ENTRY_DELAY: STATE_ALARM_PENDING, + AncillaryControlPanel.EXIT_DELAY: STATE_ALARM_PENDING, + AncillaryControlPanel.IN_ALARM: STATE_ALARM_TRIGGERED, } @@ -133,26 +124,26 @@ class DeconzAlarmControlPanel(DeconzDevice, AlarmControlPanelEntity): """Send arm away command.""" if code: await self.gateway.api.alarmsystems.arm( - self.alarm_system_id, ArmAction.AWAY, code + self.alarm_system_id, AlarmSystemArmAction.AWAY, code ) async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" if code: await self.gateway.api.alarmsystems.arm( - self.alarm_system_id, ArmAction.STAY, code + self.alarm_system_id, AlarmSystemArmAction.STAY, code ) async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" if code: await self.gateway.api.alarmsystems.arm( - self.alarm_system_id, ArmAction.NIGHT, code + self.alarm_system_id, AlarmSystemArmAction.NIGHT, code ) async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if code: await self.gateway.api.alarmsystems.arm( - self.alarm_system_id, ArmAction.DISARM, code + self.alarm_system_id, AlarmSystemArmAction.DISARM, code ) diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index fa53ef1b5bc..270e66bf91d 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -6,11 +6,8 @@ from typing import Any from pydeconz.models.event import EventType from pydeconz.models.sensor.ancillary_control import ( - ANCILLARY_CONTROL_EMERGENCY, - ANCILLARY_CONTROL_FIRE, - ANCILLARY_CONTROL_INVALID_CODE, - ANCILLARY_CONTROL_PANIC, AncillaryControl, + AncillaryControlAction, ) from pydeconz.models.sensor.switch import Switch @@ -33,10 +30,10 @@ CONF_DECONZ_EVENT = "deconz_event" CONF_DECONZ_ALARM_EVENT = "deconz_alarm_event" SUPPORTED_DECONZ_ALARM_EVENTS = { - ANCILLARY_CONTROL_EMERGENCY, - ANCILLARY_CONTROL_FIRE, - ANCILLARY_CONTROL_INVALID_CODE, - ANCILLARY_CONTROL_PANIC, + AncillaryControlAction.EMERGENCY, + AncillaryControlAction.FIRE, + AncillaryControlAction.INVALID_CODE, + AncillaryControlAction.PANIC, } @@ -183,7 +180,7 @@ class DeconzAlarmEvent(DeconzEventBase): CONF_ID: self.event_id, CONF_UNIQUE_ID: self.serial, CONF_DEVICE_ID: self.device_id, - CONF_EVENT: self._device.action, + CONF_EVENT: self._device.action.value, } self.gateway.hass.bus.async_fire(CONF_DECONZ_ALARM_EVENT, data) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 2306d088c48..ce10845a5b1 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==93"], + "requirements": ["pydeconz==94"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index b15f6c885ef..75dd9dfad8a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1441,7 +1441,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==93 +pydeconz==94 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9c37efee222..8f523f9e9b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -968,7 +968,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==93 +pydeconz==94 # homeassistant.components.dexcom pydexcom==0.2.3 diff --git a/tests/components/deconz/test_alarm_control_panel.py b/tests/components/deconz/test_alarm_control_panel.py index 5c9c192a0aa..213ce3b2e08 100644 --- a/tests/components/deconz/test_alarm_control_panel.py +++ b/tests/components/deconz/test_alarm_control_panel.py @@ -2,19 +2,7 @@ from unittest.mock import patch -from pydeconz.models.sensor.ancillary_control import ( - ANCILLARY_CONTROL_ARMED_AWAY, - ANCILLARY_CONTROL_ARMED_NIGHT, - ANCILLARY_CONTROL_ARMED_STAY, - ANCILLARY_CONTROL_ARMING_AWAY, - ANCILLARY_CONTROL_ARMING_NIGHT, - ANCILLARY_CONTROL_ARMING_STAY, - ANCILLARY_CONTROL_DISARMED, - ANCILLARY_CONTROL_ENTRY_DELAY, - ANCILLARY_CONTROL_EXIT_DELAY, - ANCILLARY_CONTROL_IN_ALARM, - ANCILLARY_CONTROL_NOT_READY, -) +from pydeconz.models.sensor.ancillary_control import AncillaryControlPanel from homeassistant.components.alarm_control_panel import ( DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, @@ -123,7 +111,7 @@ async def test_alarm_control_panel(hass, aioclient_mock, mock_deconz_websocket): "e": "changed", "r": "sensors", "id": "0", - "state": {"panel": ANCILLARY_CONTROL_ARMED_AWAY}, + "state": {"panel": AncillaryControlPanel.ARMED_AWAY}, } await mock_deconz_websocket(data=event_changed_sensor) await hass.async_block_till_done() @@ -137,7 +125,7 @@ async def test_alarm_control_panel(hass, aioclient_mock, mock_deconz_websocket): "e": "changed", "r": "sensors", "id": "0", - "state": {"panel": ANCILLARY_CONTROL_ARMED_NIGHT}, + "state": {"panel": AncillaryControlPanel.ARMED_NIGHT}, } await mock_deconz_websocket(data=event_changed_sensor) await hass.async_block_till_done() @@ -153,7 +141,7 @@ async def test_alarm_control_panel(hass, aioclient_mock, mock_deconz_websocket): "e": "changed", "r": "sensors", "id": "0", - "state": {"panel": ANCILLARY_CONTROL_ARMED_STAY}, + "state": {"panel": AncillaryControlPanel.ARMED_STAY}, } await mock_deconz_websocket(data=event_changed_sensor) await hass.async_block_till_done() @@ -167,7 +155,7 @@ async def test_alarm_control_panel(hass, aioclient_mock, mock_deconz_websocket): "e": "changed", "r": "sensors", "id": "0", - "state": {"panel": ANCILLARY_CONTROL_DISARMED}, + "state": {"panel": AncillaryControlPanel.DISARMED}, } await mock_deconz_websocket(data=event_changed_sensor) await hass.async_block_till_done() @@ -177,9 +165,9 @@ async def test_alarm_control_panel(hass, aioclient_mock, mock_deconz_websocket): # Event signals alarm control panel arming for arming_event in { - ANCILLARY_CONTROL_ARMING_AWAY, - ANCILLARY_CONTROL_ARMING_NIGHT, - ANCILLARY_CONTROL_ARMING_STAY, + AncillaryControlPanel.ARMING_AWAY, + AncillaryControlPanel.ARMING_NIGHT, + AncillaryControlPanel.ARMING_STAY, }: event_changed_sensor = { @@ -196,7 +184,10 @@ async def test_alarm_control_panel(hass, aioclient_mock, mock_deconz_websocket): # Event signals alarm control panel pending - for pending_event in {ANCILLARY_CONTROL_ENTRY_DELAY, ANCILLARY_CONTROL_EXIT_DELAY}: + for pending_event in { + AncillaryControlPanel.ENTRY_DELAY, + AncillaryControlPanel.EXIT_DELAY, + }: event_changed_sensor = { "t": "event", @@ -219,7 +210,7 @@ async def test_alarm_control_panel(hass, aioclient_mock, mock_deconz_websocket): "e": "changed", "r": "sensors", "id": "0", - "state": {"panel": ANCILLARY_CONTROL_IN_ALARM}, + "state": {"panel": AncillaryControlPanel.IN_ALARM}, } await mock_deconz_websocket(data=event_changed_sensor) await hass.async_block_till_done() @@ -233,7 +224,7 @@ async def test_alarm_control_panel(hass, aioclient_mock, mock_deconz_websocket): "e": "changed", "r": "sensors", "id": "0", - "state": {"panel": ANCILLARY_CONTROL_NOT_READY}, + "state": {"panel": AncillaryControlPanel.NOT_READY}, } await mock_deconz_websocket(data=event_changed_sensor) await hass.async_block_till_done() diff --git a/tests/components/deconz/test_deconz_event.py b/tests/components/deconz/test_deconz_event.py index e697edd5a9a..c326892aef2 100644 --- a/tests/components/deconz/test_deconz_event.py +++ b/tests/components/deconz/test_deconz_event.py @@ -3,11 +3,8 @@ from unittest.mock import patch from pydeconz.models.sensor.ancillary_control import ( - ANCILLARY_CONTROL_ARMED_AWAY, - ANCILLARY_CONTROL_EMERGENCY, - ANCILLARY_CONTROL_FIRE, - ANCILLARY_CONTROL_INVALID_CODE, - ANCILLARY_CONTROL_PANIC, + AncillaryControlAction, + AncillaryControlPanel, ) from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN @@ -286,7 +283,7 @@ async def test_deconz_alarm_events(hass, aioclient_mock, mock_deconz_websocket): "e": "changed", "r": "sensors", "id": "1", - "state": {"action": ANCILLARY_CONTROL_EMERGENCY}, + "state": {"action": AncillaryControlAction.EMERGENCY}, } await mock_deconz_websocket(data=event_changed_sensor) await hass.async_block_till_done() @@ -300,7 +297,7 @@ async def test_deconz_alarm_events(hass, aioclient_mock, mock_deconz_websocket): CONF_ID: "keypad", CONF_UNIQUE_ID: "00:00:00:00:00:00:00:01", CONF_DEVICE_ID: device.id, - CONF_EVENT: ANCILLARY_CONTROL_EMERGENCY, + CONF_EVENT: AncillaryControlAction.EMERGENCY.value, } # Fire event @@ -310,7 +307,7 @@ async def test_deconz_alarm_events(hass, aioclient_mock, mock_deconz_websocket): "e": "changed", "r": "sensors", "id": "1", - "state": {"action": ANCILLARY_CONTROL_FIRE}, + "state": {"action": AncillaryControlAction.FIRE}, } await mock_deconz_websocket(data=event_changed_sensor) await hass.async_block_till_done() @@ -324,7 +321,7 @@ async def test_deconz_alarm_events(hass, aioclient_mock, mock_deconz_websocket): CONF_ID: "keypad", CONF_UNIQUE_ID: "00:00:00:00:00:00:00:01", CONF_DEVICE_ID: device.id, - CONF_EVENT: ANCILLARY_CONTROL_FIRE, + CONF_EVENT: AncillaryControlAction.FIRE.value, } # Invalid code event @@ -334,7 +331,7 @@ async def test_deconz_alarm_events(hass, aioclient_mock, mock_deconz_websocket): "e": "changed", "r": "sensors", "id": "1", - "state": {"action": ANCILLARY_CONTROL_INVALID_CODE}, + "state": {"action": AncillaryControlAction.INVALID_CODE}, } await mock_deconz_websocket(data=event_changed_sensor) await hass.async_block_till_done() @@ -348,7 +345,7 @@ async def test_deconz_alarm_events(hass, aioclient_mock, mock_deconz_websocket): CONF_ID: "keypad", CONF_UNIQUE_ID: "00:00:00:00:00:00:00:01", CONF_DEVICE_ID: device.id, - CONF_EVENT: ANCILLARY_CONTROL_INVALID_CODE, + CONF_EVENT: AncillaryControlAction.INVALID_CODE.value, } # Panic event @@ -358,7 +355,7 @@ async def test_deconz_alarm_events(hass, aioclient_mock, mock_deconz_websocket): "e": "changed", "r": "sensors", "id": "1", - "state": {"action": ANCILLARY_CONTROL_PANIC}, + "state": {"action": AncillaryControlAction.PANIC}, } await mock_deconz_websocket(data=event_changed_sensor) await hass.async_block_till_done() @@ -372,7 +369,7 @@ async def test_deconz_alarm_events(hass, aioclient_mock, mock_deconz_websocket): CONF_ID: "keypad", CONF_UNIQUE_ID: "00:00:00:00:00:00:00:01", CONF_DEVICE_ID: device.id, - CONF_EVENT: ANCILLARY_CONTROL_PANIC, + CONF_EVENT: AncillaryControlAction.PANIC.value, } # Only care for changes to specific action events @@ -382,7 +379,7 @@ async def test_deconz_alarm_events(hass, aioclient_mock, mock_deconz_websocket): "e": "changed", "r": "sensors", "id": "1", - "state": {"action": ANCILLARY_CONTROL_ARMED_AWAY}, + "state": {"action": AncillaryControlAction.ARMED_AWAY}, } await mock_deconz_websocket(data=event_changed_sensor) await hass.async_block_till_done() @@ -396,7 +393,7 @@ async def test_deconz_alarm_events(hass, aioclient_mock, mock_deconz_websocket): "e": "changed", "r": "sensors", "id": "1", - "state": {"panel": ANCILLARY_CONTROL_ARMED_AWAY}, + "state": {"panel": AncillaryControlPanel.ARMED_AWAY}, } await mock_deconz_websocket(data=event_changed_sensor) await hass.async_block_till_done() @@ -415,7 +412,7 @@ async def test_deconz_alarm_events(hass, aioclient_mock, mock_deconz_websocket): assert len(hass.states.async_all()) == 0 -async def test_deconz_events_bad_unique_id(hass, aioclient_mock, mock_deconz_websocket): +async def test_deconz_events_bad_unique_id(hass, aioclient_mock): """Verify no devices are created if unique id is bad or missing.""" data = { "sensors": { From b17d4ac65cde9a27ff6032d70b148792e5eba8df Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 22 Jun 2022 23:51:40 +0200 Subject: [PATCH 1715/3516] Remove replicated async definitions in pylint plugin (#73823) Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- pylint/plugins/hass_enforce_type_hints.py | 95 ++++++----------------- 1 file changed, 22 insertions(+), 73 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 307510c6621..a241252c9a9 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -28,6 +28,15 @@ class TypeHintMatch: kwargs_type: str | None = None """kwargs_type is for the special case `**kwargs`""" check_return_type_inheritance: bool = False + has_async_counterpart: bool = False + + def need_to_check_function(self, node: nodes.FunctionDef) -> bool: + """Confirm if function should be checked.""" + return ( + self.function_name == node.name + or self.has_async_counterpart + and node.name == f"async_{self.function_name}" + ) @dataclass @@ -60,14 +69,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "ConfigType", }, return_type="bool", - ), - TypeHintMatch( - function_name="async_setup", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - }, - return_type="bool", + has_async_counterpart=True, ), TypeHintMatch( function_name="async_setup_entry", @@ -121,16 +123,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 3: "DiscoveryInfoType | None", }, return_type=None, - ), - TypeHintMatch( - function_name="async_setup_platform", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - 2: "AddEntitiesCallback", - 3: "DiscoveryInfoType | None", - }, - return_type=None, + has_async_counterpart=True, ), TypeHintMatch( function_name="async_setup_entry", @@ -314,14 +307,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "ConfigType", }, return_type=["DeviceScanner", "DeviceScanner | None"], - ), - TypeHintMatch( - function_name="async_get_scanner", - arg_types={ - 0: "HomeAssistant", - 1: "ConfigType", - }, - return_type=["DeviceScanner", "DeviceScanner | None"], + has_async_counterpart=True, ), ], "device_trigger": [ @@ -498,31 +484,19 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { function_name="set_percentage", arg_types={1: "int"}, return_type=None, - ), - TypeHintMatch( - function_name="async_set_percentage", - arg_types={1: "int"}, - return_type=None, + has_async_counterpart=True, ), TypeHintMatch( function_name="set_preset_mode", arg_types={1: "str"}, return_type=None, - ), - TypeHintMatch( - function_name="async_set_preset_mode", - arg_types={1: "str"}, - return_type=None, + has_async_counterpart=True, ), TypeHintMatch( function_name="set_direction", arg_types={1: "str"}, return_type=None, - ), - TypeHintMatch( - function_name="async_set_direction", - arg_types={1: "str"}, - return_type=None, + has_async_counterpart=True, ), TypeHintMatch( function_name="turn_on", @@ -532,25 +506,13 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { }, kwargs_type="Any", return_type=None, - ), - TypeHintMatch( - function_name="async_turn_on", - named_arg_types={ - "percentage": "int | None", - "preset_mode": "str | None", - }, - kwargs_type="Any", - return_type=None, + has_async_counterpart=True, ), TypeHintMatch( function_name="oscillate", arg_types={1: "bool"}, return_type=None, - ), - TypeHintMatch( - function_name="async_oscillate", - arg_types={1: "bool"}, - return_type=None, + has_async_counterpart=True, ), ], ), @@ -587,31 +549,19 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { function_name="lock", kwargs_type="Any", return_type=None, - ), - TypeHintMatch( - function_name="async_lock", - kwargs_type="Any", - return_type=None, + has_async_counterpart=True, ), TypeHintMatch( function_name="unlock", kwargs_type="Any", return_type=None, - ), - TypeHintMatch( - function_name="async_unlock", - kwargs_type="Any", - return_type=None, + has_async_counterpart=True, ), TypeHintMatch( function_name="open", kwargs_type="Any", return_type=None, - ), - TypeHintMatch( - function_name="async_open", - kwargs_type="Any", - return_type=None, + has_async_counterpart=True, ), ], ), @@ -831,14 +781,13 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] ) -> None: for match in matches: for function_node in node.mymethods(): - function_name: str | None = function_node.name - if match.function_name == function_name: + if match.need_to_check_function(function_node): self._check_function(function_node, match) def visit_functiondef(self, node: nodes.FunctionDef) -> None: """Called when a FunctionDef node is visited.""" for match in self._function_matchers: - if node.name != match.function_name or node.is_method(): + if not match.need_to_check_function(node) or node.is_method(): continue self._check_function(node, match) From b0f4b3030f5200fee343c8b78853defc7acd460c Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 22 Jun 2022 17:58:10 -0500 Subject: [PATCH 1716/3516] Extend timeouts for Spotify and Plex playback on Sonos (#73803) --- .../components/sonos/media_player.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 938a651c34d..0f766f33e6f 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -67,6 +67,7 @@ from .speaker import SonosMedia, SonosSpeaker _LOGGER = logging.getLogger(__name__) +LONG_SERVICE_TIMEOUT = 30.0 VOLUME_INCREMENT = 2 REPEAT_TO_SONOS = { @@ -580,36 +581,44 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): if result.shuffle: self.set_shuffle(True) if enqueue == MediaPlayerEnqueue.ADD: - plex_plugin.add_to_queue(result.media) + plex_plugin.add_to_queue(result.media, timeout=LONG_SERVICE_TIMEOUT) elif enqueue in ( MediaPlayerEnqueue.NEXT, MediaPlayerEnqueue.PLAY, ): pos = (self.media.queue_position or 0) + 1 - new_pos = plex_plugin.add_to_queue(result.media, position=pos) + new_pos = plex_plugin.add_to_queue( + result.media, position=pos, timeout=LONG_SERVICE_TIMEOUT + ) if enqueue == MediaPlayerEnqueue.PLAY: soco.play_from_queue(new_pos - 1) elif enqueue == MediaPlayerEnqueue.REPLACE: soco.clear_queue() - plex_plugin.add_to_queue(result.media) + plex_plugin.add_to_queue(result.media, timeout=LONG_SERVICE_TIMEOUT) soco.play_from_queue(0) return share_link = self.coordinator.share_link if share_link.is_share_link(media_id): if enqueue == MediaPlayerEnqueue.ADD: - share_link.add_share_link_to_queue(media_id) + share_link.add_share_link_to_queue( + media_id, timeout=LONG_SERVICE_TIMEOUT + ) elif enqueue in ( MediaPlayerEnqueue.NEXT, MediaPlayerEnqueue.PLAY, ): pos = (self.media.queue_position or 0) + 1 - new_pos = share_link.add_share_link_to_queue(media_id, position=pos) + new_pos = share_link.add_share_link_to_queue( + media_id, position=pos, timeout=LONG_SERVICE_TIMEOUT + ) if enqueue == MediaPlayerEnqueue.PLAY: soco.play_from_queue(new_pos - 1) elif enqueue == MediaPlayerEnqueue.REPLACE: soco.clear_queue() - share_link.add_share_link_to_queue(media_id) + share_link.add_share_link_to_queue( + media_id, timeout=LONG_SERVICE_TIMEOUT + ) soco.play_from_queue(0) elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK): # If media ID is a relative URL, we serve it from HA. From fe54db6eb9ac57af96d6dde86fa94349c8a6ea14 Mon Sep 17 00:00:00 2001 From: Alex Groleau Date: Wed, 22 Jun 2022 18:58:36 -0400 Subject: [PATCH 1717/3516] Improve Tuya integration fan controller support (#73062) --- homeassistant/components/tuya/const.py | 1 + homeassistant/components/tuya/fan.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index e7340040658..5486e94786d 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -223,6 +223,7 @@ class DPCode(StrEnum): FAN_SPEED = "fan_speed" FAN_SPEED_ENUM = "fan_speed_enum" # Speed mode FAN_SPEED_PERCENT = "fan_speed_percent" # Stepless speed + FAN_SWITCH = "fan_switch" FAN_MODE = "fan_mode" FAN_VERTICAL = "fan_vertical" # Vertical swing flap angle FAR_DETECTION = "far_detection" diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 36ed4c3c58e..021745f4c81 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -74,7 +74,7 @@ class TuyaFanEntity(TuyaEntity, FanEntity): super().__init__(device, device_manager) self._switch = self.find_dpcode( - (DPCode.SWITCH_FAN, DPCode.SWITCH), prefer_function=True + (DPCode.SWITCH_FAN, DPCode.FAN_SWITCH, DPCode.SWITCH), prefer_function=True ) self._attr_preset_modes = [] @@ -177,7 +177,6 @@ class TuyaFanEntity(TuyaEntity, FanEntity): "value": int(self._speed.remap_value_from(percentage, 1, 100)), } ) - return if percentage is not None and self._speeds is not None: commands.append( From e855529f73705d58a5533a268239268cb14e9f74 Mon Sep 17 00:00:00 2001 From: Waldemar Tomme Date: Thu, 23 Jun 2022 01:08:51 +0200 Subject: [PATCH 1718/3516] Fix fints integration (#69041) --- homeassistant/components/fints/manifest.json | 4 +-- homeassistant/components/fints/sensor.py | 30 ++++++++++---------- requirements_all.txt | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/fints/manifest.json b/homeassistant/components/fints/manifest.json index ede1025a6db..11d673a2837 100644 --- a/homeassistant/components/fints/manifest.json +++ b/homeassistant/components/fints/manifest.json @@ -2,8 +2,8 @@ "domain": "fints", "name": "FinTS", "documentation": "https://www.home-assistant.io/integrations/fints", - "requirements": ["fints==1.0.1"], + "requirements": ["fints==3.1.0"], "codeowners": [], - "iot_class": "local_push", + "iot_class": "cloud_polling", "loggers": ["fints", "mt_940", "sepaxml"] } diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index 4b6cb336bde..2e2ccd8e6b6 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -7,7 +7,6 @@ import logging from typing import Any from fints.client import FinTS3PinTanClient -from fints.dialog import FinTSDialogError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -128,6 +127,9 @@ class FinTsClient: As the fints library is stateless, there is not benefit in caching the client objects. If that ever changes, consider caching the client object and also think about potential concurrency problems. + + Note: As of version 2, the fints library is not stateless anymore. + This should be considered when reworking this integration. """ return FinTS3PinTanClient( @@ -140,24 +142,22 @@ class FinTsClient: def detect_accounts(self): """Identify the accounts of the bank.""" + bank = self.client + accounts = bank.get_sepa_accounts() + account_types = { + x["iban"]: x["type"] + for x in bank.get_information()["accounts"] + if x["iban"] is not None + } + balance_accounts = [] holdings_accounts = [] - for account in self.client.get_sepa_accounts(): - try: - self.client.get_balance(account) + for account in accounts: + account_type = account_types[account.iban] + if 1 <= account_type <= 9: # 1-9 is balance account balance_accounts.append(account) - except IndexError: - # account is not a balance account. - pass - except FinTSDialogError: - # account is not a balance account. - pass - try: - self.client.get_holdings(account) + elif 30 <= account_type <= 39: # 30-39 is holdings account holdings_accounts.append(account) - except FinTSDialogError: - # account is not a holdings account. - pass return balance_accounts, holdings_accounts diff --git a/requirements_all.txt b/requirements_all.txt index 75dd9dfad8a..c40ecd7dc6d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -638,7 +638,7 @@ feedparser==6.0.2 fiblary3==0.1.8 # homeassistant.components.fints -fints==1.0.1 +fints==3.1.0 # homeassistant.components.fitbit fitbit==0.3.1 From 3cd18ba38f1891929f6e31b537e3fac13791ae28 Mon Sep 17 00:00:00 2001 From: Liam <101684827+SkiingIsFun123@users.noreply.github.com> Date: Wed, 22 Jun 2022 16:41:22 -0700 Subject: [PATCH 1719/3516] Update CODE_OF_CONDUCT.md (#73468) --- CODE_OF_CONDUCT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index f9b1ea79314..09828047616 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -123,7 +123,7 @@ enforcement ladder][mozilla]. ## Adoption -This Code of Conduct was first adopted January 21st, 2017 and announced in +This Code of Conduct was first adopted on January 21st, 2017, and announced in [this][coc-blog] blog post and has been updated on May 25th, 2020 to version 2.0 of the [Contributor Covenant][homepage] as announced in [this][coc2-blog] blog post. From 3ce5b05aa558171a594e295e7da849d35bd8edd3 Mon Sep 17 00:00:00 2001 From: Christian Rodriguez Date: Wed, 22 Jun 2022 20:59:59 -0300 Subject: [PATCH 1720/3516] Add bypassed custom attribute to NX584ZoneSensor (#71767) --- homeassistant/components/nx584/binary_sensor.py | 5 ++++- tests/components/nx584/test_binary_sensor.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py index cbd1796b768..6fdea44f836 100644 --- a/homeassistant/components/nx584/binary_sensor.py +++ b/homeassistant/components/nx584/binary_sensor.py @@ -116,7 +116,10 @@ class NX584ZoneSensor(BinarySensorEntity): @property def extra_state_attributes(self): """Return the state attributes.""" - return {"zone_number": self._zone["number"]} + return { + "zone_number": self._zone["number"], + "bypassed": self._zone.get("bypassed", False), + } class NX584Watcher(threading.Thread): diff --git a/tests/components/nx584/test_binary_sensor.py b/tests/components/nx584/test_binary_sensor.py index 83f8a49c091..290567345ca 100644 --- a/tests/components/nx584/test_binary_sensor.py +++ b/tests/components/nx584/test_binary_sensor.py @@ -153,11 +153,28 @@ def test_nx584_zone_sensor_normal(): assert not sensor.should_poll assert sensor.is_on assert sensor.extra_state_attributes["zone_number"] == 1 + assert not sensor.extra_state_attributes["bypassed"] zone["state"] = False assert not sensor.is_on +def test_nx584_zone_sensor_bypassed(): + """Test for the NX584 zone sensor.""" + zone = {"number": 1, "name": "foo", "state": True, "bypassed": True} + sensor = nx584.NX584ZoneSensor(zone, "motion") + assert sensor.name == "foo" + assert not sensor.should_poll + assert sensor.is_on + assert sensor.extra_state_attributes["zone_number"] == 1 + assert sensor.extra_state_attributes["bypassed"] + + zone["state"] = False + zone["bypassed"] = False + assert not sensor.is_on + assert not sensor.extra_state_attributes["bypassed"] + + @mock.patch.object(nx584.NX584ZoneSensor, "schedule_update_ha_state") def test_nx584_watcher_process_zone_event(mock_update): """Test the processing of zone events.""" From 33c263d09bdc35de22eb5cb3128dc3176903d7a4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 23 Jun 2022 00:20:13 +0000 Subject: [PATCH 1721/3516] [ci skip] Translation update --- .../accuweather/translations/sv.json | 11 +++++++++ .../components/aemet/translations/sv.json | 11 +++++++++ .../components/airly/translations/sv.json | 1 + .../components/airnow/translations/sv.json | 11 +++++++++ .../components/airvisual/translations/sv.json | 5 ++++ .../aladdin_connect/translations/sv.json | 15 ++++++++++++ .../components/ambee/translations/sv.json | 16 +++++++++++++ .../components/awair/translations/sv.json | 11 +++++++++ .../components/baf/translations/sv.json | 11 +++++++++ .../binary_sensor/translations/sv.json | 4 ++-- .../components/bond/translations/sv.json | 11 +++++++++ .../components/bsblan/translations/sv.json | 1 + .../components/coinbase/translations/sv.json | 9 +++++++ .../components/control4/translations/sv.json | 3 +++ .../components/daikin/translations/sv.json | 1 + .../components/deluge/translations/sv.json | 1 + .../components/doorbird/translations/sv.json | 1 + .../eight_sleep/translations/sv.json | 3 +++ .../components/elkm1/translations/sv.json | 1 + .../components/epson/translations/sv.json | 7 ++++++ .../flick_electric/translations/sv.json | 3 +++ .../components/flo/translations/sv.json | 1 + .../components/foscam/translations/sv.json | 1 + .../freedompro/translations/sv.json | 14 +++++++++++ .../components/generic/translations/sv.json | 2 ++ .../geocaching/translations/sv.json | 21 ++++++++++++++++ .../components/geofency/translations/sv.json | 3 +++ .../geonetnz_volcano/translations/sv.json | 3 +++ .../components/gogogate2/translations/sv.json | 3 +++ .../components/google/translations/de.json | 1 + .../components/google/translations/id.json | 1 + .../components/google/translations/sv.json | 8 +++++++ .../google_travel_time/translations/sv.json | 1 + .../components/group/translations/sv.json | 14 ++++++++++- .../components/habitica/translations/sv.json | 11 +++++++++ .../components/heos/translations/sv.json | 3 +++ .../here_travel_time/translations/sv.json | 18 ++++++++++++++ .../components/hlk_sw16/translations/sv.json | 11 +++++++++ .../humidifier/translations/sv.json | 5 ++++ .../hvv_departures/translations/sv.json | 3 +++ .../components/iaqualink/translations/sv.json | 3 +++ .../components/iqvia/translations/sv.json | 3 +++ .../components/kmtronic/translations/sv.json | 1 + .../components/kodi/translations/sv.json | 5 ++++ .../components/laundrify/translations/sv.json | 12 ++++++++++ .../components/meater/translations/sv.json | 1 + .../components/metoffice/translations/sv.json | 11 +++++++++ .../components/mjpeg/translations/sv.json | 3 +++ .../motion_blinds/translations/sv.json | 14 +++++++++++ .../components/nam/translations/sv.json | 3 ++- .../components/nest/translations/de.json | 1 + .../components/nest/translations/id.json | 5 ++++ .../components/nest/translations/sv.json | 1 + .../components/nuki/translations/sv.json | 11 +++++++++ .../components/octoprint/translations/sv.json | 1 + .../components/onewire/translations/sv.json | 1 + .../components/onvif/translations/sv.json | 1 + .../opengarage/translations/sv.json | 11 +++++++++ .../overkiz/translations/sensor.ca.json | 5 ++++ .../overkiz/translations/sensor.de.json | 5 ++++ .../overkiz/translations/sensor.et.json | 5 ++++ .../overkiz/translations/sensor.fr.json | 5 ++++ .../overkiz/translations/sensor.id.json | 4 ++++ .../overkiz/translations/sensor.pt-BR.json | 5 ++++ .../overkiz/translations/sensor.sv.json | 9 +++++++ .../overkiz/translations/sensor.zh-Hant.json | 5 ++++ .../panasonic_viera/translations/sv.json | 5 ++++ .../components/pi_hole/translations/sv.json | 17 +++++++++++++ .../components/plugwise/translations/sv.json | 5 ++++ .../components/prosegur/translations/sv.json | 1 + .../components/pvoutput/translations/sv.json | 16 +++++++++++++ .../components/qnap_qsw/translations/sv.json | 3 +++ .../radiotherm/translations/sv.json | 18 ++++++++++++++ .../components/rfxtrx/translations/sv.json | 3 +++ .../ruckus_unleashed/translations/sv.json | 1 + .../components/scrape/translations/sv.json | 8 ++++++- .../components/season/translations/sv.json | 7 ++++++ .../sensibo/translations/sensor.sv.json | 7 ++++++ .../components/sensibo/translations/sv.json | 16 +++++++++++++ .../components/sia/translations/sv.json | 7 ++++++ .../components/skybell/translations/sv.json | 21 ++++++++++++++++ .../components/slack/translations/sv.json | 9 +++++++ .../components/solaredge/translations/sv.json | 3 +++ .../squeezebox/translations/sv.json | 5 ++++ .../system_bridge/translations/sv.json | 16 +++++++++++++ .../components/tailscale/translations/sv.json | 16 +++++++++++++ .../tankerkoenig/translations/sv.json | 19 +++++++++++++++ .../components/tautulli/translations/sv.json | 5 ++++ .../components/tibber/translations/sv.json | 11 +++++++++ .../tomorrowio/translations/sv.json | 11 +++++++++ .../trafikverket_ferry/translations/sv.json | 19 +++++++++++++++ .../trafikverket_train/translations/sv.json | 16 +++++++++++++ .../translations/sv.json | 11 +++++++++ .../transmission/translations/id.json | 10 +++++++- .../transmission/translations/sv.json | 9 ++++++- .../tuya/translations/select.sv.json | 17 +++++++++++++ .../ukraine_alarm/translations/sv.json | 8 +++++++ .../uptimerobot/translations/sv.json | 16 +++++++++++++ .../components/vesync/translations/sv.json | 3 +++ .../components/vicare/translations/sv.json | 1 + .../vlc_telnet/translations/sv.json | 11 +++++++++ .../water_heater/translations/sv.json | 3 ++- .../components/whirlpool/translations/sv.json | 3 +++ .../wled/translations/select.sv.json | 7 ++++++ .../components/yolink/translations/sv.json | 24 +++++++++++++++++++ .../components/zwave_js/translations/sv.json | 5 ++++ 106 files changed, 777 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/accuweather/translations/sv.json create mode 100644 homeassistant/components/aemet/translations/sv.json create mode 100644 homeassistant/components/airnow/translations/sv.json create mode 100644 homeassistant/components/ambee/translations/sv.json create mode 100644 homeassistant/components/awair/translations/sv.json create mode 100644 homeassistant/components/baf/translations/sv.json create mode 100644 homeassistant/components/bond/translations/sv.json create mode 100644 homeassistant/components/epson/translations/sv.json create mode 100644 homeassistant/components/freedompro/translations/sv.json create mode 100644 homeassistant/components/geocaching/translations/sv.json create mode 100644 homeassistant/components/google/translations/sv.json create mode 100644 homeassistant/components/habitica/translations/sv.json create mode 100644 homeassistant/components/here_travel_time/translations/sv.json create mode 100644 homeassistant/components/hlk_sw16/translations/sv.json create mode 100644 homeassistant/components/laundrify/translations/sv.json create mode 100644 homeassistant/components/metoffice/translations/sv.json create mode 100644 homeassistant/components/motion_blinds/translations/sv.json create mode 100644 homeassistant/components/nuki/translations/sv.json create mode 100644 homeassistant/components/opengarage/translations/sv.json create mode 100644 homeassistant/components/overkiz/translations/sensor.sv.json create mode 100644 homeassistant/components/pi_hole/translations/sv.json create mode 100644 homeassistant/components/pvoutput/translations/sv.json create mode 100644 homeassistant/components/radiotherm/translations/sv.json create mode 100644 homeassistant/components/season/translations/sv.json create mode 100644 homeassistant/components/sensibo/translations/sensor.sv.json create mode 100644 homeassistant/components/sensibo/translations/sv.json create mode 100644 homeassistant/components/sia/translations/sv.json create mode 100644 homeassistant/components/skybell/translations/sv.json create mode 100644 homeassistant/components/system_bridge/translations/sv.json create mode 100644 homeassistant/components/tailscale/translations/sv.json create mode 100644 homeassistant/components/tankerkoenig/translations/sv.json create mode 100644 homeassistant/components/tibber/translations/sv.json create mode 100644 homeassistant/components/tomorrowio/translations/sv.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/sv.json create mode 100644 homeassistant/components/trafikverket_train/translations/sv.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/sv.json create mode 100644 homeassistant/components/tuya/translations/select.sv.json create mode 100644 homeassistant/components/ukraine_alarm/translations/sv.json create mode 100644 homeassistant/components/uptimerobot/translations/sv.json create mode 100644 homeassistant/components/vlc_telnet/translations/sv.json create mode 100644 homeassistant/components/wled/translations/select.sv.json create mode 100644 homeassistant/components/yolink/translations/sv.json diff --git a/homeassistant/components/accuweather/translations/sv.json b/homeassistant/components/accuweather/translations/sv.json new file mode 100644 index 00000000000..f4a63bb449d --- /dev/null +++ b/homeassistant/components/accuweather/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/sv.json b/homeassistant/components/aemet/translations/sv.json new file mode 100644 index 00000000000..f4a63bb449d --- /dev/null +++ b/homeassistant/components/aemet/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/translations/sv.json b/homeassistant/components/airly/translations/sv.json index d182115230b..ac976224924 100644 --- a/homeassistant/components/airly/translations/sv.json +++ b/homeassistant/components/airly/translations/sv.json @@ -4,6 +4,7 @@ "already_configured": "Airly-integrationen f\u00f6r dessa koordinater \u00e4r redan konfigurerad." }, "error": { + "invalid_api_key": "Ogiltig API-nyckel", "wrong_location": "Inga Airly m\u00e4tstationer i detta omr\u00e5de." }, "step": { diff --git a/homeassistant/components/airnow/translations/sv.json b/homeassistant/components/airnow/translations/sv.json new file mode 100644 index 00000000000..f4a63bb449d --- /dev/null +++ b/homeassistant/components/airnow/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sv.json b/homeassistant/components/airvisual/translations/sv.json index 6a33c0393d9..5273668aa12 100644 --- a/homeassistant/components/airvisual/translations/sv.json +++ b/homeassistant/components/airvisual/translations/sv.json @@ -5,6 +5,11 @@ "invalid_api_key": "Ogiltig API-nyckel" }, "step": { + "geography_by_name": { + "data": { + "api_key": "API-nyckel" + } + }, "node_pro": { "data": { "ip_address": "Enhets IP-adress / v\u00e4rdnamn", diff --git a/homeassistant/components/aladdin_connect/translations/sv.json b/homeassistant/components/aladdin_connect/translations/sv.json index 23c825f256f..867d5d1c5c7 100644 --- a/homeassistant/components/aladdin_connect/translations/sv.json +++ b/homeassistant/components/aladdin_connect/translations/sv.json @@ -1,8 +1,23 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "title": "\u00c5terautenticera integration" + }, "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/ambee/translations/sv.json b/homeassistant/components/ambee/translations/sv.json new file mode 100644 index 00000000000..5ad5b5b6db4 --- /dev/null +++ b/homeassistant/components/ambee/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-nyckel" + } + }, + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/sv.json b/homeassistant/components/awair/translations/sv.json new file mode 100644 index 00000000000..1fda5b91f5a --- /dev/null +++ b/homeassistant/components/awair/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "access_token": "\u00c5tkomstnyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/sv.json b/homeassistant/components/baf/translations/sv.json new file mode 100644 index 00000000000..e3270d4036a --- /dev/null +++ b/homeassistant/components/baf/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/sv.json b/homeassistant/components/binary_sensor/translations/sv.json index 58f97e77977..904ecd8fddc 100644 --- a/homeassistant/components/binary_sensor/translations/sv.json +++ b/homeassistant/components/binary_sensor/translations/sv.json @@ -121,7 +121,7 @@ "on": "\u00d6ppen" }, "gas": { - "off": "Klart", + "off": "Rensa", "on": "Detekterad" }, "heat": { @@ -169,7 +169,7 @@ "on": "Detekterad" }, "vibration": { - "off": "Klart", + "off": "Rensa", "on": "Detekterad" }, "window": { diff --git a/homeassistant/components/bond/translations/sv.json b/homeassistant/components/bond/translations/sv.json new file mode 100644 index 00000000000..1fda5b91f5a --- /dev/null +++ b/homeassistant/components/bond/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "access_token": "\u00c5tkomstnyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/sv.json b/homeassistant/components/bsblan/translations/sv.json index 2d2d9662e4b..6dad0946ee5 100644 --- a/homeassistant/components/bsblan/translations/sv.json +++ b/homeassistant/components/bsblan/translations/sv.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "port": "Port", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/coinbase/translations/sv.json b/homeassistant/components/coinbase/translations/sv.json index 5610f0c79fd..1ab71904a13 100644 --- a/homeassistant/components/coinbase/translations/sv.json +++ b/homeassistant/components/coinbase/translations/sv.json @@ -1,4 +1,13 @@ { + "config": { + "step": { + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + }, "options": { "error": { "currency_unavailable": "En eller flera av de beg\u00e4rda valutasaldona tillhandah\u00e5lls inte av ditt Coinbase API.", diff --git a/homeassistant/components/control4/translations/sv.json b/homeassistant/components/control4/translations/sv.json index 23c825f256f..e1ecf8798c1 100644 --- a/homeassistant/components/control4/translations/sv.json +++ b/homeassistant/components/control4/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/daikin/translations/sv.json b/homeassistant/components/daikin/translations/sv.json index a704822f0b7..03b7358ee1b 100644 --- a/homeassistant/components/daikin/translations/sv.json +++ b/homeassistant/components/daikin/translations/sv.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "api_key": "API-nyckel", "host": "V\u00e4rddatorn", "password": "Enhetsl\u00f6senord (anv\u00e4nds endast av SKYFi-enheter)" }, diff --git a/homeassistant/components/deluge/translations/sv.json b/homeassistant/components/deluge/translations/sv.json index 23c825f256f..1a65fe29a6f 100644 --- a/homeassistant/components/deluge/translations/sv.json +++ b/homeassistant/components/deluge/translations/sv.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "port": "Port", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/doorbird/translations/sv.json b/homeassistant/components/doorbird/translations/sv.json index b2a809a576e..56c44dee6fb 100644 --- a/homeassistant/components/doorbird/translations/sv.json +++ b/homeassistant/components/doorbird/translations/sv.json @@ -8,6 +8,7 @@ "data": { "host": "V\u00e4rd (IP-adress)", "name": "Enhetsnamn", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/eight_sleep/translations/sv.json b/homeassistant/components/eight_sleep/translations/sv.json index 78879942876..af5c7e7fe8d 100644 --- a/homeassistant/components/eight_sleep/translations/sv.json +++ b/homeassistant/components/eight_sleep/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/elkm1/translations/sv.json b/homeassistant/components/elkm1/translations/sv.json index e49782de873..8765d95baf6 100644 --- a/homeassistant/components/elkm1/translations/sv.json +++ b/homeassistant/components/elkm1/translations/sv.json @@ -13,6 +13,7 @@ }, "manual_connection": { "data": { + "protocol": "Protokoll", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/epson/translations/sv.json b/homeassistant/components/epson/translations/sv.json new file mode 100644 index 00000000000..e7ec27624a5 --- /dev/null +++ b/homeassistant/components/epson/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/sv.json b/homeassistant/components/flick_electric/translations/sv.json index 23c825f256f..2957bed953a 100644 --- a/homeassistant/components/flick_electric/translations/sv.json +++ b/homeassistant/components/flick_electric/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/flo/translations/sv.json b/homeassistant/components/flo/translations/sv.json index 23c825f256f..78879942876 100644 --- a/homeassistant/components/flo/translations/sv.json +++ b/homeassistant/components/flo/translations/sv.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/foscam/translations/sv.json b/homeassistant/components/foscam/translations/sv.json index 23c825f256f..78879942876 100644 --- a/homeassistant/components/foscam/translations/sv.json +++ b/homeassistant/components/foscam/translations/sv.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/freedompro/translations/sv.json b/homeassistant/components/freedompro/translations/sv.json new file mode 100644 index 00000000000..9feab4808f7 --- /dev/null +++ b/homeassistant/components/freedompro/translations/sv.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/sv.json b/homeassistant/components/generic/translations/sv.json index 96aee85c779..62b30963a50 100644 --- a/homeassistant/components/generic/translations/sv.json +++ b/homeassistant/components/generic/translations/sv.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "authentication": "Autentiseringen", "username": "Anv\u00e4ndarnamn" } } @@ -15,6 +16,7 @@ "step": { "init": { "data": { + "authentication": "Autentiseringen", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/geocaching/translations/sv.json b/homeassistant/components/geocaching/translations/sv.json new file mode 100644 index 00000000000..d8622ba37fb --- /dev/null +++ b/homeassistant/components/geocaching/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", + "oauth_error": "Mottog ogiltiga tokendata.", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "create_entry": { + "default": "Autentiserats" + }, + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + }, + "reauth_confirm": { + "title": "\u00c5terautenticera integration" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/sv.json b/homeassistant/components/geofency/translations/sv.json index 453b33533ce..8b48a30ce8b 100644 --- a/homeassistant/components/geofency/translations/sv.json +++ b/homeassistant/components/geofency/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud." + }, "create_entry": { "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera webhook funktionen i Geofency.\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n\nSe [dokumentation]({docs_url}) om hur du konfigurerar detta f\u00f6r mer information." }, diff --git a/homeassistant/components/geonetnz_volcano/translations/sv.json b/homeassistant/components/geonetnz_volcano/translations/sv.json index 0ad4f7f0853..65e867f8269 100644 --- a/homeassistant/components/geonetnz_volcano/translations/sv.json +++ b/homeassistant/components/geonetnz_volcano/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Platsen \u00e4r redan konfigurerad" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/gogogate2/translations/sv.json b/homeassistant/components/gogogate2/translations/sv.json index 23c825f256f..f7461922566 100644 --- a/homeassistant/components/gogogate2/translations/sv.json +++ b/homeassistant/components/gogogate2/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/google/translations/de.json b/homeassistant/components/google/translations/de.json index 2111e0c8bab..433324147dd 100644 --- a/homeassistant/components/google/translations/de.json +++ b/homeassistant/components/google/translations/de.json @@ -6,6 +6,7 @@ "abort": { "already_configured": "Konto wurde bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen", "code_expired": "Der Authentifizierungscode ist abgelaufen oder die Anmeldedaten sind ung\u00fcltig, bitte versuche es erneut.", "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", diff --git a/homeassistant/components/google/translations/id.json b/homeassistant/components/google/translations/id.json index 085d49e92a3..ea13f27fce5 100644 --- a/homeassistant/components/google/translations/id.json +++ b/homeassistant/components/google/translations/id.json @@ -6,6 +6,7 @@ "abort": { "already_configured": "Akun sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal terhubung", "code_expired": "Kode autentikasi kedaluwarsa atau penyiapan kredensial tidak valid, coba lagi.", "invalid_access_token": "Token akses tidak valid", "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", diff --git a/homeassistant/components/google/translations/sv.json b/homeassistant/components/google/translations/sv.json new file mode 100644 index 00000000000..7f1f140af90 --- /dev/null +++ b/homeassistant/components/google/translations/sv.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "cannot_connect": "Det gick inte att ansluta." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/sv.json b/homeassistant/components/google_travel_time/translations/sv.json index 18a9d3d507e..1b8cc14bce7 100644 --- a/homeassistant/components/google_travel_time/translations/sv.json +++ b/homeassistant/components/google_travel_time/translations/sv.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "api_key": "API-nyckel", "destination": "Destination", "origin": "Ursprung" } diff --git a/homeassistant/components/group/translations/sv.json b/homeassistant/components/group/translations/sv.json index 77ae43cb1ee..c62d9e18ffc 100644 --- a/homeassistant/components/group/translations/sv.json +++ b/homeassistant/components/group/translations/sv.json @@ -9,8 +9,20 @@ }, "title": "Ny grupp" }, + "light": { + "title": "L\u00e4gg till grupp" + }, "user": { - "title": "Ny grupp" + "title": "L\u00e4gg till grupp" + } + } + }, + "options": { + "step": { + "binary_sensor": { + "data": { + "all": "Alla entiteter" + } } } }, diff --git a/homeassistant/components/habitica/translations/sv.json b/homeassistant/components/habitica/translations/sv.json new file mode 100644 index 00000000000..f4a63bb449d --- /dev/null +++ b/homeassistant/components/habitica/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/translations/sv.json b/homeassistant/components/heos/translations/sv.json index a2cec73f291..100f8fd83a5 100644 --- a/homeassistant/components/heos/translations/sv.json +++ b/homeassistant/components/heos/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/here_travel_time/translations/sv.json b/homeassistant/components/here_travel_time/translations/sv.json new file mode 100644 index 00000000000..0757cc44bf1 --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/sv.json b/homeassistant/components/hlk_sw16/translations/sv.json new file mode 100644 index 00000000000..eba844f6c03 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/sv.json b/homeassistant/components/humidifier/translations/sv.json index 325e9f2e6a0..2818d7b7b04 100644 --- a/homeassistant/components/humidifier/translations/sv.json +++ b/homeassistant/components/humidifier/translations/sv.json @@ -3,5 +3,10 @@ "trigger_type": { "turned_off": "{entity_name} st\u00e4ngdes av" } + }, + "state": { + "_": { + "on": "P\u00e5" + } } } \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/sv.json b/homeassistant/components/hvv_departures/translations/sv.json index 8d17443df9f..3a2983d1035 100644 --- a/homeassistant/components/hvv_departures/translations/sv.json +++ b/homeassistant/components/hvv_departures/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/iaqualink/translations/sv.json b/homeassistant/components/iaqualink/translations/sv.json index 0e086c9c413..e697b5a02b9 100644 --- a/homeassistant/components/iaqualink/translations/sv.json +++ b/homeassistant/components/iaqualink/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/iqvia/translations/sv.json b/homeassistant/components/iqvia/translations/sv.json index 71eb118a858..f6500296b58 100644 --- a/homeassistant/components/iqvia/translations/sv.json +++ b/homeassistant/components/iqvia/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, "error": { "invalid_zip_code": "Ogiltigt postnummer" }, diff --git a/homeassistant/components/kmtronic/translations/sv.json b/homeassistant/components/kmtronic/translations/sv.json index 23c825f256f..a265d988aaa 100644 --- a/homeassistant/components/kmtronic/translations/sv.json +++ b/homeassistant/components/kmtronic/translations/sv.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "host": "V\u00e4rd", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/kodi/translations/sv.json b/homeassistant/components/kodi/translations/sv.json index 36b53053594..9102efc653e 100644 --- a/homeassistant/components/kodi/translations/sv.json +++ b/homeassistant/components/kodi/translations/sv.json @@ -5,6 +5,11 @@ "data": { "username": "Anv\u00e4ndarnamn" } + }, + "user": { + "data": { + "host": "V\u00e4rd" + } } } } diff --git a/homeassistant/components/laundrify/translations/sv.json b/homeassistant/components/laundrify/translations/sv.json new file mode 100644 index 00000000000..dd7447e847e --- /dev/null +++ b/homeassistant/components/laundrify/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/sv.json b/homeassistant/components/meater/translations/sv.json index 44b4ba96acf..47a719743f5 100644 --- a/homeassistant/components/meater/translations/sv.json +++ b/homeassistant/components/meater/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "error": { + "invalid_auth": "Ogiltig autentisering", "service_unavailable_error": "Programmeringsgr\u00e4nssnittet g\u00e5r inte att komma \u00e5t f\u00f6r n\u00e4rvarande. F\u00f6rs\u00f6k igen senare." }, "step": { diff --git a/homeassistant/components/metoffice/translations/sv.json b/homeassistant/components/metoffice/translations/sv.json new file mode 100644 index 00000000000..f4a63bb449d --- /dev/null +++ b/homeassistant/components/metoffice/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/sv.json b/homeassistant/components/mjpeg/translations/sv.json index 291fbedbcfb..dd590fec4ba 100644 --- a/homeassistant/components/mjpeg/translations/sv.json +++ b/homeassistant/components/mjpeg/translations/sv.json @@ -9,6 +9,9 @@ } }, "options": { + "error": { + "invalid_auth": "Ogiltig autentisering" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/motion_blinds/translations/sv.json b/homeassistant/components/motion_blinds/translations/sv.json new file mode 100644 index 00000000000..eee10b4c765 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/sv.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "step": { + "connect": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sv.json b/homeassistant/components/nam/translations/sv.json index 9f12ea5a385..ffa62b8fae2 100644 --- a/homeassistant/components/nam/translations/sv.json +++ b/homeassistant/components/nam/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "device_unsupported": "Enheten st\u00f6ds ej" + "device_unsupported": "Enheten st\u00f6ds ej", + "reauth_unsuccessful": "\u00c5terautentiseringen misslyckades. Ta bort integrationen och konfigurera den igen." }, "error": { "cannot_connect": "Det gick inte att ansluta ", diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index 659755fc6b4..933231fd1e8 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -4,6 +4,7 @@ }, "config": { "abort": { + "already_configured": "Konto wurde bereits konfiguriert", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", diff --git a/homeassistant/components/nest/translations/id.json b/homeassistant/components/nest/translations/id.json index e58f891f6c4..6a45ccaee8c 100644 --- a/homeassistant/components/nest/translations/id.json +++ b/homeassistant/components/nest/translations/id.json @@ -4,6 +4,7 @@ }, "config": { "abort": { + "already_configured": "Akun sudah dikonfigurasi", "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", "invalid_access_token": "Token akses tidak valid", "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", @@ -33,6 +34,7 @@ "title": "Tautkan Akun Google" }, "auth_upgrade": { + "description": "Autentikasi Aplikasi tidak digunakan lagi oleh Google untuk meningkatkan keamanan, dan Anda perlu mengambil tindakan dengan membuat kredensial aplikasi baru. \n\nBuka [dokumentasi]({more_info_url}) untuk panduan langkah selanjutnya yang perlu diambil untuk memulihkan akses ke perangkat Nest Anda.", "title": "Nest: Penghentian Autentikasi Aplikasi" }, "cloud_project": { @@ -43,15 +45,18 @@ "title": "Nest: Masukkan ID Proyek Cloud" }, "create_cloud_project": { + "description": "Integrasi Nest memungkinkan Anda mengintegrasikan Nest Thermostat, Kamera, dan Bel Pintu menggunakan API Smart Device Management. API SDM **memerlukan biaya penyiapan satu kali sebesar USD5**. Lihat dokumentasi untuk [info lebih lanjut]({more_info_url}). \n\n1. Buka [Konsol Google Cloud]({cloud_console_url}).\n1. Jika ini adalah proyek pertama Anda, klik **Buat Proyek** lalu **Proyek Baru**.\n1. Beri Nama Proyek Cloud Anda, lalu klik **Buat**.\n1. Simpan ID Proyek Cloud misalnya *example-project-12345* karena Anda akan membutuhkannya nanti\n1. Buka Perpustakaan API untuk [API Smart Device Management]({sdm_api_url}) dan klik **Aktifkan**.\n1. Buka Perpustakaan API untuk [API Cloud Pub/Sub]({pubsub_api_url}) dan klik **Aktifkan**. \n\n Lanjutkan saat proyek cloud Anda sudah disiapkan.", "title": "Nest: Buat dan konfigurasikan Proyek Cloud" }, "device_project": { "data": { "project_id": "ID Proyek Akses Perangkat" }, + "description": "Buat proyek Akses Perangkat Nest yang **membutuhkan biaya USD5** untuk menyiapkannya.\n1. Buka [Konsol Akses Perangkat]({device_access_console_url}), dan ikuti alur pembayaran.\n1. Klik **Buat proyek**\n1. Beri nama proyek Akses Perangkat Anda dan klik **Berikutnya**.\n1. Masukkan ID Klien OAuth Anda\n1. Aktifkan acara dengan mengklik **Aktifkan** dan **Buat proyek**. \n\n Masukkan ID Proyek Akses Perangkat Anda di bawah ini ([more info]({more_info_url})).\n", "title": "Nest: Buat Proyek Akses Perangkat" }, "device_project_upgrade": { + "description": "Perbarui Proyek Akses Perangkat Nest dengan ID Klien OAuth baru Anda ([info lebih lanjut]({more_info_url}))\n1. Buka [Konsol Akses Perangkat]( {device_access_console_url} ).\n1. Klik ikon tempat sampah di samping *ID Klien OAuth*.\n1. Klik menu luapan `...` dan *Tambah ID Klien*.\n1. Masukkan ID Klien OAuth baru Anda dan klik **Tambah**. \n\n ID Klien OAuth Anda adalah: `{client_id}`", "title": "Nest: Perbarui Proyek Akses Perangkat" }, "init": { diff --git a/homeassistant/components/nest/translations/sv.json b/homeassistant/components/nest/translations/sv.json index e0fef47aaec..9e91f3ddf94 100644 --- a/homeassistant/components/nest/translations/sv.json +++ b/homeassistant/components/nest/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Konto har redan konfigurerats", "authorize_url_timeout": "Timeout vid generering av en autentisieringsadress." }, "error": { diff --git a/homeassistant/components/nuki/translations/sv.json b/homeassistant/components/nuki/translations/sv.json new file mode 100644 index 00000000000..563e2d4a773 --- /dev/null +++ b/homeassistant/components/nuki/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "token": "\u00c5tkomstnyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/sv.json b/homeassistant/components/octoprint/translations/sv.json index dad4741f152..08b58e15cc6 100644 --- a/homeassistant/components/octoprint/translations/sv.json +++ b/homeassistant/components/octoprint/translations/sv.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "host": "V\u00e4rd", "ssl": "Anv\u00e4nd SSL", "username": "Anv\u00e4ndarnamn" } diff --git a/homeassistant/components/onewire/translations/sv.json b/homeassistant/components/onewire/translations/sv.json index 1b100c60d97..9b57beabc8f 100644 --- a/homeassistant/components/onewire/translations/sv.json +++ b/homeassistant/components/onewire/translations/sv.json @@ -3,6 +3,7 @@ "step": { "device_selection": { "data": { + "clear_device_options": "Rensa alla enhetskonfigurationer", "device_selection": "V\u00e4lj enheter att konfigurera" } } diff --git a/homeassistant/components/onvif/translations/sv.json b/homeassistant/components/onvif/translations/sv.json index f2fd2e8429e..2cc40c1e465 100644 --- a/homeassistant/components/onvif/translations/sv.json +++ b/homeassistant/components/onvif/translations/sv.json @@ -3,6 +3,7 @@ "step": { "configure": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } }, diff --git a/homeassistant/components/opengarage/translations/sv.json b/homeassistant/components/opengarage/translations/sv.json new file mode 100644 index 00000000000..eba844f6c03 --- /dev/null +++ b/homeassistant/components/opengarage/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.ca.json b/homeassistant/components/overkiz/translations/sensor.ca.json index 2d94ab16d1a..9e1a40941b0 100644 --- a/homeassistant/components/overkiz/translations/sensor.ca.json +++ b/homeassistant/components/overkiz/translations/sensor.ca.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "Net", "dirty": "Brut" + }, + "overkiz__three_way_handle_direction": { + "closed": "Tancada", + "open": "Oberta", + "tilt": "Inclinada" } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.de.json b/homeassistant/components/overkiz/translations/sensor.de.json index 8f262514e5f..216df8f0cff 100644 --- a/homeassistant/components/overkiz/translations/sensor.de.json +++ b/homeassistant/components/overkiz/translations/sensor.de.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "Sauber", "dirty": "Schmutzig" + }, + "overkiz__three_way_handle_direction": { + "closed": "Geschlossen", + "open": "Offen", + "tilt": "Kippen" } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.et.json b/homeassistant/components/overkiz/translations/sensor.et.json index 974d57b095c..72021d675c8 100644 --- a/homeassistant/components/overkiz/translations/sensor.et.json +++ b/homeassistant/components/overkiz/translations/sensor.et.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "Puhas", "dirty": "R\u00e4pane" + }, + "overkiz__three_way_handle_direction": { + "closed": "Suletud", + "open": "Avatud", + "tilt": "Kallutamine" } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.fr.json b/homeassistant/components/overkiz/translations/sensor.fr.json index af9fd658ab9..23fe8993570 100644 --- a/homeassistant/components/overkiz/translations/sensor.fr.json +++ b/homeassistant/components/overkiz/translations/sensor.fr.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "Propre", "dirty": "Sale" + }, + "overkiz__three_way_handle_direction": { + "closed": "Ferm\u00e9e", + "open": "Ouverte", + "tilt": "Inclin\u00e9e" } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.id.json b/homeassistant/components/overkiz/translations/sensor.id.json index efe4f588a71..bf4703507f8 100644 --- a/homeassistant/components/overkiz/translations/sensor.id.json +++ b/homeassistant/components/overkiz/translations/sensor.id.json @@ -36,6 +36,10 @@ "overkiz__sensor_room": { "clean": "Bersih", "dirty": "Kotor" + }, + "overkiz__three_way_handle_direction": { + "closed": "Tutup", + "open": "Buka" } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.pt-BR.json b/homeassistant/components/overkiz/translations/sensor.pt-BR.json index 3ec82aa83d5..0f391de9027 100644 --- a/homeassistant/components/overkiz/translations/sensor.pt-BR.json +++ b/homeassistant/components/overkiz/translations/sensor.pt-BR.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "Limpo", "dirty": "Sujo" + }, + "overkiz__three_way_handle_direction": { + "closed": "Fechado", + "open": "Aberto", + "tilt": "Inclinar" } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.sv.json b/homeassistant/components/overkiz/translations/sensor.sv.json new file mode 100644 index 00000000000..024e5fe1037 --- /dev/null +++ b/homeassistant/components/overkiz/translations/sensor.sv.json @@ -0,0 +1,9 @@ +{ + "state": { + "overkiz__three_way_handle_direction": { + "closed": "St\u00e4ngd", + "open": "\u00d6ppen", + "tilt": "Vinkla" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.zh-Hant.json b/homeassistant/components/overkiz/translations/sensor.zh-Hant.json index 785c02cbfbc..661dfa5d313 100644 --- a/homeassistant/components/overkiz/translations/sensor.zh-Hant.json +++ b/homeassistant/components/overkiz/translations/sensor.zh-Hant.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "\u826f\u597d", "dirty": "\u4e0d\u4f73" + }, + "overkiz__three_way_handle_direction": { + "closed": "\u95dc\u9589", + "open": "\u958b\u555f", + "tilt": "\u50be\u659c" } } } \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/sv.json b/homeassistant/components/panasonic_viera/translations/sv.json index f70336fae9f..2c863b587ec 100644 --- a/homeassistant/components/panasonic_viera/translations/sv.json +++ b/homeassistant/components/panasonic_viera/translations/sv.json @@ -4,6 +4,11 @@ "unknown": "Ett ov\u00e4ntat fel intr\u00e4ffade. Kontrollera loggarna f\u00f6r mer information." }, "step": { + "pairing": { + "data": { + "pin": "Pin-kod" + } + }, "user": { "data": { "host": "IP-adress", diff --git a/homeassistant/components/pi_hole/translations/sv.json b/homeassistant/components/pi_hole/translations/sv.json new file mode 100644 index 00000000000..0459a7596f3 --- /dev/null +++ b/homeassistant/components/pi_hole/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "api_key": { + "data": { + "api_key": "API-nyckel" + } + }, + "user": { + "data": { + "api_key": "API-nyckel", + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/sv.json b/homeassistant/components/plugwise/translations/sv.json index acbb205a11f..affb73f907e 100644 --- a/homeassistant/components/plugwise/translations/sv.json +++ b/homeassistant/components/plugwise/translations/sv.json @@ -1,6 +1,11 @@ { "config": { "step": { + "user": { + "data": { + "port": "Port" + } + }, "user_gateway": { "data": { "host": "IP address", diff --git a/homeassistant/components/prosegur/translations/sv.json b/homeassistant/components/prosegur/translations/sv.json index 8a60ea1a5dc..4ae0ae62971 100644 --- a/homeassistant/components/prosegur/translations/sv.json +++ b/homeassistant/components/prosegur/translations/sv.json @@ -8,6 +8,7 @@ }, "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/pvoutput/translations/sv.json b/homeassistant/components/pvoutput/translations/sv.json new file mode 100644 index 00000000000..5ad5b5b6db4 --- /dev/null +++ b/homeassistant/components/pvoutput/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-nyckel" + } + }, + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/sv.json b/homeassistant/components/qnap_qsw/translations/sv.json index 0234fcb9860..ec6c8842dca 100644 --- a/homeassistant/components/qnap_qsw/translations/sv.json +++ b/homeassistant/components/qnap_qsw/translations/sv.json @@ -4,6 +4,9 @@ "already_configured": "Enheten \u00e4r redan konfigurerad", "invalid_id": "Enheten returnerade ett ogiltigt unikt ID" }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/radiotherm/translations/sv.json b/homeassistant/components/radiotherm/translations/sv.json new file mode 100644 index 00000000000..f341a6314ee --- /dev/null +++ b/homeassistant/components/radiotherm/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/sv.json b/homeassistant/components/rfxtrx/translations/sv.json index 6cab9bd0b37..28f5c911c3a 100644 --- a/homeassistant/components/rfxtrx/translations/sv.json +++ b/homeassistant/components/rfxtrx/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, "step": { "setup_network": { "data": { diff --git a/homeassistant/components/ruckus_unleashed/translations/sv.json b/homeassistant/components/ruckus_unleashed/translations/sv.json index 23c825f256f..a265d988aaa 100644 --- a/homeassistant/components/ruckus_unleashed/translations/sv.json +++ b/homeassistant/components/ruckus_unleashed/translations/sv.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "host": "V\u00e4rd", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/scrape/translations/sv.json b/homeassistant/components/scrape/translations/sv.json index 291fbedbcfb..c70f08008dc 100644 --- a/homeassistant/components/scrape/translations/sv.json +++ b/homeassistant/components/scrape/translations/sv.json @@ -1,9 +1,14 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, "step": { "user": { "data": { - "username": "Anv\u00e4ndarnamn" + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera SSL-certifikat" } } } @@ -12,6 +17,7 @@ "step": { "init": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/season/translations/sv.json b/homeassistant/components/season/translations/sv.json new file mode 100644 index 00000000000..c0b662beebe --- /dev/null +++ b/homeassistant/components/season/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.sv.json b/homeassistant/components/sensibo/translations/sensor.sv.json new file mode 100644 index 00000000000..ead64d63cd6 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.sv.json @@ -0,0 +1,7 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "Normal" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sv.json b/homeassistant/components/sensibo/translations/sv.json new file mode 100644 index 00000000000..5ad5b5b6db4 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-nyckel" + } + }, + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sia/translations/sv.json b/homeassistant/components/sia/translations/sv.json new file mode 100644 index 00000000000..67fd98a8827 --- /dev/null +++ b/homeassistant/components/sia/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Ov\u00e4ntat fel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/sv.json b/homeassistant/components/skybell/translations/sv.json new file mode 100644 index 00000000000..bb5b9d188a6 --- /dev/null +++ b/homeassistant/components/skybell/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "email": "E-post", + "password": "L\u00f6senord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/sv.json b/homeassistant/components/slack/translations/sv.json index 23c825f256f..34e67b311a2 100644 --- a/homeassistant/components/slack/translations/sv.json +++ b/homeassistant/components/slack/translations/sv.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "api_key": "API-nyckel", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/solaredge/translations/sv.json b/homeassistant/components/solaredge/translations/sv.json index f09320388f2..df7408b43c2 100644 --- a/homeassistant/components/solaredge/translations/sv.json +++ b/homeassistant/components/solaredge/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "invalid_api_key": "Ogiltig API-nyckel" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/squeezebox/translations/sv.json b/homeassistant/components/squeezebox/translations/sv.json index 8dbe191c902..796ddb0da2e 100644 --- a/homeassistant/components/squeezebox/translations/sv.json +++ b/homeassistant/components/squeezebox/translations/sv.json @@ -5,6 +5,11 @@ "data": { "username": "Anv\u00e4ndarnamn" } + }, + "user": { + "data": { + "host": "V\u00e4rd" + } } } } diff --git a/homeassistant/components/system_bridge/translations/sv.json b/homeassistant/components/system_bridge/translations/sv.json new file mode 100644 index 00000000000..5fa635734cb --- /dev/null +++ b/homeassistant/components/system_bridge/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "authenticate": { + "data": { + "api_key": "API-nyckel" + } + }, + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/sv.json b/homeassistant/components/tailscale/translations/sv.json new file mode 100644 index 00000000000..5ad5b5b6db4 --- /dev/null +++ b/homeassistant/components/tailscale/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-nyckel" + } + }, + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/sv.json b/homeassistant/components/tankerkoenig/translations/sv.json new file mode 100644 index 00000000000..4b9b566c7d1 --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-nyckel" + } + }, + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tautulli/translations/sv.json b/homeassistant/components/tautulli/translations/sv.json index 3354c6053dc..0058ba541bf 100644 --- a/homeassistant/components/tautulli/translations/sv.json +++ b/homeassistant/components/tautulli/translations/sv.json @@ -3,6 +3,11 @@ "step": { "reauth_confirm": { "description": "F\u00f6r att hitta din API-nyckel, \u00f6ppna Tautullis webbsida och navigera till Inst\u00e4llningar och sedan till webbgr\u00e4nssnitt. API-nyckeln finns l\u00e4ngst ner p\u00e5 sidan." + }, + "user": { + "data": { + "api_key": "API-nyckel" + } } } } diff --git a/homeassistant/components/tibber/translations/sv.json b/homeassistant/components/tibber/translations/sv.json new file mode 100644 index 00000000000..1fda5b91f5a --- /dev/null +++ b/homeassistant/components/tibber/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "access_token": "\u00c5tkomstnyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sv.json b/homeassistant/components/tomorrowio/translations/sv.json new file mode 100644 index 00000000000..f4a63bb449d --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/sv.json b/homeassistant/components/trafikverket_ferry/translations/sv.json new file mode 100644 index 00000000000..ad82dbe221d --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "invalid_auth": "Ogiltig autentisering" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-nyckel" + } + }, + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/translations/sv.json b/homeassistant/components/trafikverket_train/translations/sv.json new file mode 100644 index 00000000000..5ad5b5b6db4 --- /dev/null +++ b/homeassistant/components/trafikverket_train/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-nyckel" + } + }, + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/sv.json b/homeassistant/components/trafikverket_weatherstation/translations/sv.json new file mode 100644 index 00000000000..f4a63bb449d --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/id.json b/homeassistant/components/transmission/translations/id.json index a96524f5165..7b5fa3a703f 100644 --- a/homeassistant/components/transmission/translations/id.json +++ b/homeassistant/components/transmission/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", @@ -9,6 +10,13 @@ "name_exists": "Nama sudah ada" }, "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Kata sandi untuk {username} tidak valid.", + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/transmission/translations/sv.json b/homeassistant/components/transmission/translations/sv.json index 848fa71de60..0ffbebed9f6 100644 --- a/homeassistant/components/transmission/translations/sv.json +++ b/homeassistant/components/transmission/translations/sv.json @@ -1,13 +1,20 @@ { "config": { "abort": { - "already_configured": "V\u00e4rden \u00e4r redan konfigurerad." + "already_configured": "V\u00e4rden \u00e4r redan konfigurerad.", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det g\u00e5r inte att ansluta till v\u00e4rden", "name_exists": "Namnet finns redan" }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "host": "V\u00e4rd", diff --git a/homeassistant/components/tuya/translations/select.sv.json b/homeassistant/components/tuya/translations/select.sv.json new file mode 100644 index 00000000000..05092fe808c --- /dev/null +++ b/homeassistant/components/tuya/translations/select.sv.json @@ -0,0 +1,17 @@ +{ + "state": { + "tuya__humidifier_spray_mode": { + "humidity": "Luftfuktighet" + }, + "tuya__light_mode": { + "none": "Av" + }, + "tuya__relay_status": { + "on": "P\u00e5", + "power_on": "P\u00e5" + }, + "tuya__vacuum_mode": { + "standby": "Standby" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/sv.json b/homeassistant/components/ukraine_alarm/translations/sv.json new file mode 100644 index 00000000000..4a9945525c8 --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/sv.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sv.json b/homeassistant/components/uptimerobot/translations/sv.json new file mode 100644 index 00000000000..5ad5b5b6db4 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-nyckel" + } + }, + "user": { + "data": { + "api_key": "API-nyckel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/translations/sv.json b/homeassistant/components/vesync/translations/sv.json index b9eedc2f747..4621636cecc 100644 --- a/homeassistant/components/vesync/translations/sv.json +++ b/homeassistant/components/vesync/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vicare/translations/sv.json b/homeassistant/components/vicare/translations/sv.json index 26e9f2d6a49..80588906063 100644 --- a/homeassistant/components/vicare/translations/sv.json +++ b/homeassistant/components/vicare/translations/sv.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "client_id": "API-nyckel", "username": "E-postadress" } } diff --git a/homeassistant/components/vlc_telnet/translations/sv.json b/homeassistant/components/vlc_telnet/translations/sv.json new file mode 100644 index 00000000000..eba844f6c03 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/sv.json b/homeassistant/components/water_heater/translations/sv.json index 37de0012a79..cb4826c461a 100644 --- a/homeassistant/components/water_heater/translations/sv.json +++ b/homeassistant/components/water_heater/translations/sv.json @@ -1,7 +1,8 @@ { "state": { "_": { - "heat_pump": "V\u00e4rmepump" + "heat_pump": "V\u00e4rmepump", + "off": "Av" } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/sv.json b/homeassistant/components/whirlpool/translations/sv.json index 23c825f256f..f7461922566 100644 --- a/homeassistant/components/whirlpool/translations/sv.json +++ b/homeassistant/components/whirlpool/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/wled/translations/select.sv.json b/homeassistant/components/wled/translations/select.sv.json new file mode 100644 index 00000000000..1c3bb4c1ff4 --- /dev/null +++ b/homeassistant/components/wled/translations/select.sv.json @@ -0,0 +1,7 @@ +{ + "state": { + "wled__live_override": { + "1": "P\u00e5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/sv.json b/homeassistant/components/yolink/translations/sv.json new file mode 100644 index 00000000000..3c6db089e0e --- /dev/null +++ b/homeassistant/components/yolink/translations/sv.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", + "oauth_error": "Mottog ogiltiga tokendata.", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "create_entry": { + "default": "Autentiserats" + }, + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + }, + "reauth_confirm": { + "title": "\u00c5terautenticera integration" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/sv.json b/homeassistant/components/zwave_js/translations/sv.json index ecbacaaa3b3..907924e08ac 100644 --- a/homeassistant/components/zwave_js/translations/sv.json +++ b/homeassistant/components/zwave_js/translations/sv.json @@ -3,5 +3,10 @@ "condition_type": { "value": "Nuvarande v\u00e4rde f\u00f6r ett Z-Wave v\u00e4rde" } + }, + "options": { + "error": { + "cannot_connect": "Det gick inte att ansluta." + } } } \ No newline at end of file From ab30d38469db9629525ee331822890dc8ba0eccb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Jun 2022 20:12:48 -0500 Subject: [PATCH 1722/3516] Switch rest to use the json helper (#73867) --- homeassistant/components/rest/sensor.py | 6 +++--- homeassistant/helpers/json.py | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 2965504dc4a..93a96aeb94f 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -1,7 +1,6 @@ """Support for RESTful API sensors.""" from __future__ import annotations -import json import logging from xml.parsers.expat import ExpatError @@ -26,6 +25,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.json import json_dumps, json_loads from homeassistant.helpers.template_entity import TemplateSensor from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -141,7 +141,7 @@ class RestSensor(BaseRestEntity, TemplateSensor): or content_type.startswith("application/rss+xml") ): try: - value = json.dumps(xmltodict.parse(value)) + value = json_dumps(xmltodict.parse(value)) _LOGGER.debug("JSON converted from XML: %s", value) except ExpatError: _LOGGER.warning( @@ -153,7 +153,7 @@ class RestSensor(BaseRestEntity, TemplateSensor): self._attributes = {} if value: try: - json_dict = json.loads(value) + json_dict = json_loads(value) if self._json_attrs_path is not None: json_dict = jsonpath(json_dict, self._json_attrs_path) # jsonpath will always store the result in json_dict[0] diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index 9248c613b95..43e97060f5d 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -86,4 +86,7 @@ def json_dumps(data: Any) -> str: ).decode("utf-8") +json_loads = orjson.loads + + JSON_DUMP: Final = json_dumps From 6c41a101421bce22b403ad341bb72de4c97d9332 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Jun 2022 20:13:02 -0500 Subject: [PATCH 1723/3516] Switch api and event stream to use json helper (#73868) --- homeassistant/components/api/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index ab43632e25c..ec1e13e07fc 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -1,7 +1,6 @@ """Rest API for Home Assistant.""" import asyncio from http import HTTPStatus -import json import logging from aiohttp import web @@ -29,7 +28,7 @@ import homeassistant.core as ha from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceNotFound, TemplateError, Unauthorized from homeassistant.helpers import template -from homeassistant.helpers.json import JSONEncoder +from homeassistant.helpers.json import json_dumps, json_loads from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.helpers.typing import ConfigType @@ -108,7 +107,7 @@ class APIEventStream(HomeAssistantView): if event.event_type == EVENT_HOMEASSISTANT_STOP: data = stop_obj else: - data = json.dumps(event, cls=JSONEncoder) + data = json_dumps(event) await to_write.put(data) @@ -261,7 +260,7 @@ class APIEventView(HomeAssistantView): raise Unauthorized() body = await request.text() try: - event_data = json.loads(body) if body else None + event_data = json_loads(body) if body else None except ValueError: return self.json_message( "Event data should be valid JSON.", HTTPStatus.BAD_REQUEST @@ -314,7 +313,7 @@ class APIDomainServicesView(HomeAssistantView): hass: ha.HomeAssistant = request.app["hass"] body = await request.text() try: - data = json.loads(body) if body else None + data = json_loads(body) if body else None except ValueError: return self.json_message( "Data should be valid JSON.", HTTPStatus.BAD_REQUEST From 168065a9a0cb800130324f359e0971532b0d5afb Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 22 Jun 2022 22:10:41 -0400 Subject: [PATCH 1724/3516] Bump version of pyunifiprotect to 4.0.7 (#73875) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 2b779c77629..e28386d73a8 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.6", "unifi-discovery==1.1.4"], + "requirements": ["pyunifiprotect==4.0.7", "unifi-discovery==1.1.4"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index c40ecd7dc6d..962a246c2d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1990,7 +1990,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.6 +pyunifiprotect==4.0.7 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8f523f9e9b7..4a48872ddee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1325,7 +1325,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.6 +pyunifiprotect==4.0.7 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From b5f6f785d5c5e0c85da65cea60ac4fb7b37a9191 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Jun 2022 21:32:48 -0500 Subject: [PATCH 1725/3516] Switch mobile_app to use the json helper (#73870) --- homeassistant/components/mobile_app/helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 545c3511fc9..e1c0841984f 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -13,7 +13,7 @@ from nacl.secret import SecretBox 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 +from homeassistant.helpers.json import JSONEncoder, json_loads from .const import ( ATTR_APP_DATA, @@ -85,7 +85,7 @@ def _decrypt_payload_helper( key_bytes = get_key_bytes(key, keylen) msg_bytes = decrypt(ciphertext, key_bytes) - message = json.loads(msg_bytes.decode("utf-8")) + message = json_loads(msg_bytes) _LOGGER.debug("Successfully decrypted mobile_app payload") return message From 164eba7e5d3b76844821f19164ca4330c9d44b0a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Jun 2022 21:57:38 -0500 Subject: [PATCH 1726/3516] Switch loader to use json helper (#73872) --- homeassistant/helpers/json.py | 1 + homeassistant/loader.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index 43e97060f5d..fd1153711ad 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -7,6 +7,7 @@ from typing import Any, Final import orjson JSON_ENCODE_EXCEPTIONS = (TypeError, ValueError) +JSON_DECODE_EXCEPTIONS = (orjson.JSONDecodeError,) class JSONEncoder(json.JSONEncoder): diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 589f316532b..ab681d7c42d 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -11,7 +11,6 @@ from collections.abc import Callable from contextlib import suppress import functools as ft import importlib -import json import logging import pathlib import sys @@ -30,6 +29,7 @@ from .generated.mqtt import MQTT from .generated.ssdp import SSDP from .generated.usb import USB from .generated.zeroconf import HOMEKIT, ZEROCONF +from .helpers.json import JSON_DECODE_EXCEPTIONS, json_loads from .util.async_ import gather_with_concurrency # Typing imports that create a circular dependency @@ -366,8 +366,8 @@ class Integration: continue try: - manifest = json.loads(manifest_path.read_text()) - except ValueError as err: + manifest = json_loads(manifest_path.read_text()) + except JSON_DECODE_EXCEPTIONS as err: _LOGGER.error( "Error parsing manifest.json file at %s: %s", manifest_path, err ) From 303ce715edb6d3a338547382302d7e5d2c697505 Mon Sep 17 00:00:00 2001 From: gigatexel <65073191+gigatexel@users.noreply.github.com> Date: Thu, 23 Jun 2022 09:15:16 +0200 Subject: [PATCH 1727/3516] Adapt DSMR integration to changes in dsmr_parser for Belgian/Dutch meters (#73817) --- homeassistant/components/dsmr/const.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index 43c0e66e945..9f08e812e04 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -245,10 +245,28 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), + DSMRSensorEntityDescription( + key=obis_references.BELGIUM_MAX_POWER_PER_PHASE, + name="Max power per phase", + dsmr_versions={"5B"}, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.BELGIUM_MAX_CURRENT_PER_PHASE, + name="Max current per phase", + dsmr_versions={"5B"}, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), DSMRSensorEntityDescription( key=obis_references.ELECTRICITY_IMPORTED_TOTAL, name="Energy Consumption (total)", - dsmr_versions={"5", "5B", "5L", "5S", "Q3D"}, + dsmr_versions={"5L", "5S", "Q3D"}, force_update=True, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, From bccec77e1939baf61bd4a11a361f691eafb3d1f9 Mon Sep 17 00:00:00 2001 From: henryptung Date: Thu, 23 Jun 2022 00:38:39 -0700 Subject: [PATCH 1728/3516] Fix Broadlink discovery for new RM Mini3 (#73822) --- homeassistant/components/broadlink/manifest.json | 3 +++ homeassistant/generated/dhcp.py | 1 + 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index 949f8add20b..4ae0e39cf04 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -18,6 +18,9 @@ }, { "macaddress": "B4430D*" + }, + { + "macaddress": "C8F742*" } ], "iot_class": "local_polling", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 9f2438aafa1..91398ed00ef 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -23,6 +23,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'broadlink', 'macaddress': '24DFA7*'}, {'domain': 'broadlink', 'macaddress': 'A043B0*'}, {'domain': 'broadlink', 'macaddress': 'B4430D*'}, + {'domain': 'broadlink', 'macaddress': 'C8F742*'}, {'domain': 'elkm1', 'registered_devices': True}, {'domain': 'elkm1', 'macaddress': '00409D*'}, {'domain': 'emonitor', 'hostname': 'emonitor*', 'macaddress': '0090C2*'}, From 90e1fb6ce2faadb9a35fdbe1774fce7b4456364f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 23 Jun 2022 10:48:30 +0200 Subject: [PATCH 1729/3516] Weather unit conversion (#73441) Co-authored-by: Erik --- homeassistant/components/demo/weather.py | 51 +- homeassistant/components/weather/__init__.py | 711 ++++++++++++++-- tests/components/accuweather/test_weather.py | 6 +- tests/components/aemet/test_weather.py | 6 +- tests/components/climacell/test_weather.py | 20 +- tests/components/demo/test_weather.py | 19 +- tests/components/ipma/test_weather.py | 4 +- tests/components/knx/test_weather.py | 4 +- tests/components/tomorrowio/test_weather.py | 6 +- tests/components/weather/test_init.py | 797 ++++++++++++++++-- .../custom_components/test/weather.py | 101 ++- 11 files changed, 1531 insertions(+), 194 deletions(-) diff --git a/homeassistant/components/demo/weather.py b/homeassistant/components/demo/weather.py index 916083c5ad1..eed3e970b12 100644 --- a/homeassistant/components/demo/weather.py +++ b/homeassistant/components/demo/weather.py @@ -27,7 +27,14 @@ from homeassistant.components.weather import ( WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import ( + PRESSURE_HPA, + PRESSURE_INHG, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -77,6 +84,8 @@ def setup_platform( 1099, 0.5, TEMP_CELSIUS, + PRESSURE_HPA, + SPEED_METERS_PER_SECOND, [ [ATTR_CONDITION_RAINY, 1, 22, 15, 60], [ATTR_CONDITION_RAINY, 5, 19, 8, 30], @@ -95,6 +104,8 @@ def setup_platform( 987, 4.8, TEMP_FAHRENHEIT, + PRESSURE_INHG, + SPEED_MILES_PER_HOUR, [ [ATTR_CONDITION_SNOWY, 2, -10, -15, 60], [ATTR_CONDITION_PARTLYCLOUDY, 1, -13, -14, 25], @@ -121,16 +132,20 @@ class DemoWeather(WeatherEntity): pressure, wind_speed, temperature_unit, + pressure_unit, + wind_speed_unit, forecast, ): """Initialize the Demo weather.""" self._name = name self._condition = condition - self._temperature = temperature - self._temperature_unit = temperature_unit + self._native_temperature = temperature + self._native_temperature_unit = temperature_unit self._humidity = humidity - self._pressure = pressure - self._wind_speed = wind_speed + self._native_pressure = pressure + self._native_pressure_unit = pressure_unit + self._native_wind_speed = wind_speed + self._native_wind_speed_unit = wind_speed_unit self._forecast = forecast @property @@ -144,14 +159,14 @@ class DemoWeather(WeatherEntity): return False @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" - return self._temperature + return self._native_temperature @property - def temperature_unit(self): + def native_temperature_unit(self): """Return the unit of measurement.""" - return self._temperature_unit + return self._native_temperature_unit @property def humidity(self): @@ -159,14 +174,24 @@ class DemoWeather(WeatherEntity): return self._humidity @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" - return self._wind_speed + return self._native_wind_speed @property - def pressure(self): + def native_wind_speed_unit(self): + """Return the wind speed.""" + return self._native_wind_speed_unit + + @property + def native_pressure(self): """Return the pressure.""" - return self._pressure + return self._native_pressure + + @property + def native_pressure_unit(self): + """Return the pressure.""" + return self._native_pressure_unit @property def condition(self): diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 2e0f8912867..3e72c7ad931 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -1,22 +1,47 @@ """Weather component that handles meteorological data for your location.""" from __future__ import annotations +from collections.abc import Callable +from contextlib import suppress from dataclasses import dataclass from datetime import timedelta +import inspect import logging -from typing import Final, TypedDict, final +from typing import Any, Final, TypedDict, final from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_MILES, + LENGTH_MILLIMETERS, + PRECISION_HALVES, + PRECISION_TENTHS, + PRECISION_WHOLE, + PRESSURE_HPA, + PRESSURE_INHG, + PRESSURE_MBAR, + PRESSURE_MMHG, + SPEED_KILOMETERS_PER_HOUR, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.typing import ConfigType +from homeassistant.util import ( + distance as distance_util, + pressure as pressure_util, + speed as speed_util, + temperature as temperature_util, +) # mypy: allow-untyped-defs, no-check-untyped-defs @@ -40,21 +65,31 @@ ATTR_CONDITION_WINDY = "windy" ATTR_CONDITION_WINDY_VARIANT = "windy-variant" ATTR_FORECAST = "forecast" ATTR_FORECAST_CONDITION: Final = "condition" +ATTR_FORECAST_NATIVE_PRECIPITATION: Final = "native_precipitation" ATTR_FORECAST_PRECIPITATION: Final = "precipitation" ATTR_FORECAST_PRECIPITATION_PROBABILITY: Final = "precipitation_probability" +ATTR_FORECAST_NATIVE_PRESSURE: Final = "native_pressure" ATTR_FORECAST_PRESSURE: Final = "pressure" +ATTR_FORECAST_NATIVE_TEMP: Final = "native_temperature" ATTR_FORECAST_TEMP: Final = "temperature" +ATTR_FORECAST_NATIVE_TEMP_LOW: Final = "native_templow" ATTR_FORECAST_TEMP_LOW: Final = "templow" ATTR_FORECAST_TIME: Final = "datetime" ATTR_FORECAST_WIND_BEARING: Final = "wind_bearing" +ATTR_FORECAST_NATIVE_WIND_SPEED: Final = "native_wind_speed" ATTR_FORECAST_WIND_SPEED: Final = "wind_speed" ATTR_WEATHER_HUMIDITY = "humidity" ATTR_WEATHER_OZONE = "ozone" ATTR_WEATHER_PRESSURE = "pressure" +ATTR_WEATHER_PRESSURE_UNIT = "pressure_unit" ATTR_WEATHER_TEMPERATURE = "temperature" +ATTR_WEATHER_TEMPERATURE_UNIT = "temperature_unit" ATTR_WEATHER_VISIBILITY = "visibility" +ATTR_WEATHER_VISIBILITY_UNIT = "visibility_unit" ATTR_WEATHER_WIND_BEARING = "wind_bearing" ATTR_WEATHER_WIND_SPEED = "wind_speed" +ATTR_WEATHER_WIND_SPEED_UNIT = "wind_speed_unit" +ATTR_WEATHER_PRECIPITATION_UNIT = "precipitation_unit" DOMAIN = "weather" @@ -64,18 +99,83 @@ SCAN_INTERVAL = timedelta(seconds=30) ROUNDING_PRECISION = 2 +VALID_UNITS_PRESSURE: tuple[str, ...] = ( + PRESSURE_HPA, + PRESSURE_MBAR, + PRESSURE_INHG, + PRESSURE_MMHG, +) +VALID_UNITS_TEMPERATURE: tuple[str, ...] = ( + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +VALID_UNITS_PRECIPITATION: tuple[str, ...] = ( + LENGTH_MILLIMETERS, + LENGTH_INCHES, +) +VALID_UNITS_VISIBILITY: tuple[str, ...] = ( + LENGTH_KILOMETERS, + LENGTH_MILES, +) +VALID_UNITS_WIND_SPEED: tuple[str, ...] = ( + SPEED_METERS_PER_SECOND, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, +) + +UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = { + ATTR_WEATHER_PRESSURE_UNIT: pressure_util.convert, + ATTR_WEATHER_TEMPERATURE_UNIT: temperature_util.convert, + ATTR_WEATHER_VISIBILITY_UNIT: distance_util.convert, + ATTR_WEATHER_PRECIPITATION_UNIT: distance_util.convert, + ATTR_WEATHER_WIND_SPEED_UNIT: speed_util.convert, +} + +VALID_UNITS: dict[str, tuple[str, ...]] = { + ATTR_WEATHER_PRESSURE_UNIT: VALID_UNITS_PRESSURE, + ATTR_WEATHER_TEMPERATURE_UNIT: VALID_UNITS_TEMPERATURE, + ATTR_WEATHER_VISIBILITY_UNIT: VALID_UNITS_VISIBILITY, + ATTR_WEATHER_PRECIPITATION_UNIT: VALID_UNITS_PRECIPITATION, + ATTR_WEATHER_WIND_SPEED_UNIT: VALID_UNITS_WIND_SPEED, +} + + +def round_temperature(temperature: float | None, precision: float) -> float | None: + """Convert temperature into preferred precision for display.""" + if temperature is None: + return None + + # Round in the units appropriate + if precision == PRECISION_HALVES: + temperature = round(temperature * 2) / 2.0 + elif precision == PRECISION_TENTHS: + temperature = round(temperature, 1) + # Integer as a fall back (PRECISION_WHOLE) + else: + temperature = round(temperature) + + return temperature + class Forecast(TypedDict, total=False): - """Typed weather forecast dict.""" + """Typed weather forecast dict. + + All attributes are in native units and old attributes kept for backwards compatibility. + """ condition: str | None datetime: str precipitation_probability: int | None + native_precipitation: float | None precipitation: float | None + native_pressure: float | None pressure: float | None + native_temperature: float | None temperature: float | None + native_templow: float | None templow: float | None wind_bearing: float | str | None + native_wind_speed: float | None wind_speed: float | None @@ -114,38 +214,219 @@ class WeatherEntity(Entity): _attr_humidity: float | None = None _attr_ozone: float | None = None _attr_precision: float - _attr_pressure: float | None = None - _attr_pressure_unit: str | None = None + _attr_pressure: float | None = ( + None # Provide backwards compatibility. Use _attr_native_pressure + ) + _attr_pressure_unit: str | None = ( + None # Provide backwards compatibility. Use _attr_native_pressure_unit + ) _attr_state: None = None - _attr_temperature_unit: str - _attr_temperature: float | None - _attr_visibility: float | None = None - _attr_visibility_unit: str | None = None - _attr_precipitation_unit: str | None = None + _attr_temperature: float | None = ( + None # Provide backwards compatibility. Use _attr_native_temperature + ) + _attr_temperature_unit: str | None = ( + None # Provide backwards compatibility. Use _attr_native_temperature_unit + ) + _attr_visibility: float | None = ( + None # Provide backwards compatibility. Use _attr_native_visibility + ) + _attr_visibility_unit: str | None = ( + None # Provide backwards compatibility. Use _attr_native_visibility_unit + ) + _attr_precipitation_unit: str | None = ( + None # Provide backwards compatibility. Use _attr_native_precipitation_unit + ) _attr_wind_bearing: float | str | None = None - _attr_wind_speed: float | None = None - _attr_wind_speed_unit: str | None = None + _attr_wind_speed: float | None = ( + None # Provide backwards compatibility. Use _attr_native_wind_speed + ) + _attr_wind_speed_unit: str | None = ( + None # Provide backwards compatibility. Use _attr_native_wind_speed_unit + ) + + _attr_native_pressure: float | None = None + _attr_native_pressure_unit: str | None = None + _attr_native_temperature: float | None = None + _attr_native_temperature_unit: str | None = None + _attr_native_visibility: float | None = None + _attr_native_visibility_unit: str | None = None + _attr_native_precipitation_unit: str | None = None + _attr_native_wind_speed: float | None = None + _attr_native_wind_speed_unit: str | None = None + + _weather_option_temperature_unit: str | None = None + _weather_option_pressure_unit: str | None = None + _weather_option_visibility_unit: str | None = None + _weather_option_precipitation_unit: str | None = None + _weather_option_wind_speed_unit: str | None = None + + def __init_subclass__(cls, **kwargs: Any) -> None: + """Post initialisation processing.""" + super().__init_subclass__(**kwargs) + _reported = False + if any( + method in cls.__dict__ + for method in ( + "_attr_temperature", + "temperature", + "_attr_temperature_unit", + "temperature_unit", + "_attr_pressure", + "pressure", + "_attr_pressure_unit", + "pressure_unit", + "_attr_wind_speed", + "wind_speed", + "_attr_wind_speed_unit", + "wind_speed_unit", + "_attr_visibility", + "visibility", + "_attr_visibility_unit", + "visibility_unit", + "_attr_precipitation_unit", + "precipitation_unit", + ) + ): + if _reported is False: + module = inspect.getmodule(cls) + _reported = True + if ( + module + and module.__file__ + and "custom_components" in module.__file__ + ): + report_issue = "report it to the custom component author." + else: + report_issue = ( + "create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" + ) + _LOGGER.warning( + "%s::%s is overriding deprecated methods on an instance of " + "WeatherEntity, this is not valid and will be unsupported " + "from Home Assistant 2022.10. Please %s", + cls.__module__, + cls.__name__, + report_issue, + ) + + async def async_internal_added_to_hass(self) -> None: + """Call when the sensor entity is added to hass.""" + await super().async_internal_added_to_hass() + if not self.registry_entry: + return + self.async_registry_entry_updated() @property def temperature(self) -> float | None: - """Return the platform temperature in native units (i.e. not converted).""" + """Return the temperature for backward compatibility. + + Should not be set by integrations. + """ return self._attr_temperature @property - def temperature_unit(self) -> str: + def native_temperature(self) -> float | None: + """Return the temperature in native units.""" + if (temperature := self.temperature) is not None: + return temperature + + return self._attr_native_temperature + + @property + def native_temperature_unit(self) -> str | None: """Return the native unit of measurement for temperature.""" + if (temperature_unit := self.temperature_unit) is not None: + return temperature_unit + + return self._attr_native_temperature_unit + + @property + def temperature_unit(self) -> str | None: + """Return the temperature unit for backward compatibility. + + Should not be set by integrations. + """ return self._attr_temperature_unit + @final + @property + def _default_temperature_unit(self) -> str: + """Return the default unit of measurement for temperature. + + Should not be set by integrations. + """ + return self.hass.config.units.temperature_unit + + @final + @property + def _temperature_unit(self) -> str: + """Return the converted unit of measurement for temperature. + + Should not be set by integrations. + """ + if ( + weather_option_temperature_unit := self._weather_option_temperature_unit + ) is not None: + return weather_option_temperature_unit + + return self._default_temperature_unit + @property def pressure(self) -> float | None: - """Return the pressure in native units.""" + """Return the pressure for backward compatibility. + + Should not be set by integrations. + """ return self._attr_pressure @property - def pressure_unit(self) -> str | None: + def native_pressure(self) -> float | None: + """Return the pressure in native units.""" + if (pressure := self.pressure) is not None: + return pressure + + return self._attr_native_pressure + + @property + def native_pressure_unit(self) -> str | None: """Return the native unit of measurement for pressure.""" + if (pressure_unit := self.pressure_unit) is not None: + return pressure_unit + + return self._attr_native_pressure_unit + + @property + def pressure_unit(self) -> str | None: + """Return the pressure unit for backward compatibility. + + Should not be set by integrations. + """ return self._attr_pressure_unit + @final + @property + def _default_pressure_unit(self) -> str: + """Return the default unit of measurement for pressure. + + Should not be set by integrations. + """ + return PRESSURE_HPA if self.hass.config.units.is_metric else PRESSURE_INHG + + @final + @property + def _pressure_unit(self) -> str: + """Return the converted unit of measurement for pressure. + + Should not be set by integrations. + """ + if ( + weather_option_pressure_unit := self._weather_option_pressure_unit + ) is not None: + return weather_option_pressure_unit + + return self._default_pressure_unit + @property def humidity(self) -> float | None: """Return the humidity in native units.""" @@ -153,14 +434,63 @@ class WeatherEntity(Entity): @property def wind_speed(self) -> float | None: - """Return the wind speed in native units.""" + """Return the wind_speed for backward compatibility. + + Should not be set by integrations. + """ return self._attr_wind_speed @property - def wind_speed_unit(self) -> str | None: + def native_wind_speed(self) -> float | None: + """Return the wind speed in native units.""" + if (wind_speed := self.wind_speed) is not None: + return wind_speed + + return self._attr_native_wind_speed + + @property + def native_wind_speed_unit(self) -> str | None: """Return the native unit of measurement for wind speed.""" + if (wind_speed_unit := self.wind_speed_unit) is not None: + return wind_speed_unit + + return self._attr_native_wind_speed_unit + + @property + def wind_speed_unit(self) -> str | None: + """Return the wind_speed unit for backward compatibility. + + Should not be set by integrations. + """ return self._attr_wind_speed_unit + @final + @property + def _default_wind_speed_unit(self) -> str: + """Return the default unit of measurement for wind speed. + + Should not be set by integrations. + """ + return ( + SPEED_KILOMETERS_PER_HOUR + if self.hass.config.units.is_metric + else SPEED_MILES_PER_HOUR + ) + + @final + @property + def _wind_speed_unit(self) -> str: + """Return the converted unit of measurement for wind speed. + + Should not be set by integrations. + """ + if ( + weather_option_wind_speed_unit := self._weather_option_wind_speed_unit + ) is not None: + return weather_option_wind_speed_unit + + return self._default_wind_speed_unit + @property def wind_bearing(self) -> float | str | None: """Return the wind bearing.""" @@ -173,24 +503,103 @@ class WeatherEntity(Entity): @property def visibility(self) -> float | None: - """Return the visibility in native units.""" + """Return the visibility for backward compatibility. + + Should not be set by integrations. + """ return self._attr_visibility @property - def visibility_unit(self) -> str | None: + def native_visibility(self) -> float | None: + """Return the visibility in native units.""" + if (visibility := self.visibility) is not None: + return visibility + + return self._attr_native_visibility + + @property + def native_visibility_unit(self) -> str | None: """Return the native unit of measurement for visibility.""" + if (visibility_unit := self.visibility_unit) is not None: + return visibility_unit + + return self._attr_native_visibility_unit + + @property + def visibility_unit(self) -> str | None: + """Return the visibility unit for backward compatibility. + + Should not be set by integrations. + """ return self._attr_visibility_unit + @final + @property + def _default_visibility_unit(self) -> str: + """Return the default unit of measurement for visibility. + + Should not be set by integrations. + """ + return self.hass.config.units.length_unit + + @final + @property + def _visibility_unit(self) -> str: + """Return the converted unit of measurement for visibility. + + Should not be set by integrations. + """ + if ( + weather_option_visibility_unit := self._weather_option_visibility_unit + ) is not None: + return weather_option_visibility_unit + + return self._default_visibility_unit + @property def forecast(self) -> list[Forecast] | None: """Return the forecast in native units.""" return self._attr_forecast @property - def precipitation_unit(self) -> str | None: + def native_precipitation_unit(self) -> str | None: """Return the native unit of measurement for accumulated precipitation.""" + if (precipitation_unit := self.precipitation_unit) is not None: + return precipitation_unit + + return self._attr_native_precipitation_unit + + @property + def precipitation_unit(self) -> str | None: + """Return the precipitation unit for backward compatibility. + + Should not be set by integrations. + """ return self._attr_precipitation_unit + @final + @property + def _default_precipitation_unit(self) -> str: + """Return the default unit of measurement for precipitation. + + Should not be set by integrations. + """ + return self.hass.config.units.accumulated_precipitation_unit + + @final + @property + def _precipitation_unit(self) -> str: + """Return the converted unit of measurement for precipitation. + + Should not be set by integrations. + """ + if ( + weather_option_precipitation_unit := self._weather_option_precipitation_unit + ) is not None: + return weather_option_precipitation_unit + + return self._default_precipitation_unit + @property def precision(self) -> float: """Return the precision of the temperature value, after unit conversion.""" @@ -198,7 +607,7 @@ class WeatherEntity(Entity): return self._attr_precision return ( PRECISION_TENTHS - if self.hass.config.units.temperature_unit == TEMP_CELSIUS + if self._temperature_unit == TEMP_CELSIUS else PRECISION_WHOLE ) @@ -207,13 +616,24 @@ class WeatherEntity(Entity): def state_attributes(self): """Return the state attributes, converted from native units to user-configured units.""" data = {} - if self.temperature is not None: - data[ATTR_WEATHER_TEMPERATURE] = show_temp( - self.hass, - self.temperature, - self.temperature_unit, - self.precision, - ) + + precision = self.precision + + if (temperature := self.native_temperature) is not None: + from_unit = self.native_temperature_unit or self._default_temperature_unit + to_unit = self._temperature_unit + try: + temperature_f = float(temperature) + value_temp = UNIT_CONVERSIONS[ATTR_WEATHER_TEMPERATURE_UNIT]( + temperature_f, from_unit, to_unit + ) + data[ATTR_WEATHER_TEMPERATURE] = round_temperature( + value_temp, precision + ) + except (TypeError, ValueError): + data[ATTR_WEATHER_TEMPERATURE] = temperature + + data[ATTR_WEATHER_TEMPERATURE_UNIT] = self._temperature_unit if (humidity := self.humidity) is not None: data[ATTR_WEATHER_HUMIDITY] = round(humidity) @@ -221,77 +641,159 @@ class WeatherEntity(Entity): if (ozone := self.ozone) is not None: data[ATTR_WEATHER_OZONE] = ozone - if (pressure := self.pressure) is not None: - if (unit := self.pressure_unit) is not None: - pressure = round( - self.hass.config.units.pressure(pressure, unit), ROUNDING_PRECISION + if (pressure := self.native_pressure) is not None: + from_unit = self.native_pressure_unit or self._default_pressure_unit + to_unit = self._pressure_unit + try: + pressure_f = float(pressure) + value_pressure = UNIT_CONVERSIONS[ATTR_WEATHER_PRESSURE_UNIT]( + pressure_f, from_unit, to_unit ) - data[ATTR_WEATHER_PRESSURE] = pressure + data[ATTR_WEATHER_PRESSURE] = round(value_pressure, ROUNDING_PRECISION) + except (TypeError, ValueError): + data[ATTR_WEATHER_PRESSURE] = pressure + + data[ATTR_WEATHER_PRESSURE_UNIT] = self._pressure_unit if (wind_bearing := self.wind_bearing) is not None: data[ATTR_WEATHER_WIND_BEARING] = wind_bearing - if (wind_speed := self.wind_speed) is not None: - if (unit := self.wind_speed_unit) is not None: - wind_speed = round( - self.hass.config.units.wind_speed(wind_speed, unit), - ROUNDING_PRECISION, + if (wind_speed := self.native_wind_speed) is not None: + from_unit = self.native_wind_speed_unit or self._default_wind_speed_unit + to_unit = self._wind_speed_unit + try: + wind_speed_f = float(wind_speed) + value_wind_speed = UNIT_CONVERSIONS[ATTR_WEATHER_WIND_SPEED_UNIT]( + wind_speed_f, from_unit, to_unit ) - data[ATTR_WEATHER_WIND_SPEED] = wind_speed + data[ATTR_WEATHER_WIND_SPEED] = round( + value_wind_speed, ROUNDING_PRECISION + ) + except (TypeError, ValueError): + data[ATTR_WEATHER_WIND_SPEED] = wind_speed - if (visibility := self.visibility) is not None: - if (unit := self.visibility_unit) is not None: - visibility = round( - self.hass.config.units.length(visibility, unit), ROUNDING_PRECISION + data[ATTR_WEATHER_WIND_SPEED_UNIT] = self._wind_speed_unit + + if (visibility := self.native_visibility) is not None: + from_unit = self.native_visibility_unit or self._default_visibility_unit + to_unit = self._visibility_unit + try: + visibility_f = float(visibility) + value_visibility = UNIT_CONVERSIONS[ATTR_WEATHER_VISIBILITY_UNIT]( + visibility_f, from_unit, to_unit ) - data[ATTR_WEATHER_VISIBILITY] = visibility + data[ATTR_WEATHER_VISIBILITY] = round( + value_visibility, ROUNDING_PRECISION + ) + except (TypeError, ValueError): + data[ATTR_WEATHER_VISIBILITY] = visibility + + data[ATTR_WEATHER_VISIBILITY_UNIT] = self._visibility_unit + data[ATTR_WEATHER_PRECIPITATION_UNIT] = self._precipitation_unit if self.forecast is not None: forecast = [] for forecast_entry in self.forecast: forecast_entry = dict(forecast_entry) - forecast_entry[ATTR_FORECAST_TEMP] = show_temp( - self.hass, - forecast_entry[ATTR_FORECAST_TEMP], - self.temperature_unit, - self.precision, + + temperature = forecast_entry.pop( + ATTR_FORECAST_NATIVE_TEMP, forecast_entry.get(ATTR_FORECAST_TEMP) ) - if ATTR_FORECAST_TEMP_LOW in forecast_entry: - forecast_entry[ATTR_FORECAST_TEMP_LOW] = show_temp( - self.hass, - forecast_entry[ATTR_FORECAST_TEMP_LOW], - self.temperature_unit, - self.precision, + + from_temp_unit = ( + self.native_temperature_unit or self._default_temperature_unit + ) + to_temp_unit = self._temperature_unit + + if temperature is None: + forecast_entry[ATTR_FORECAST_TEMP] = None + else: + with suppress(TypeError, ValueError): + temperature_f = float(temperature) + value_temp = UNIT_CONVERSIONS[ATTR_WEATHER_TEMPERATURE_UNIT]( + temperature_f, + from_temp_unit, + to_temp_unit, + ) + forecast_entry[ATTR_FORECAST_TEMP] = round_temperature( + value_temp, precision + ) + + if forecast_temp_low := forecast_entry.pop( + ATTR_FORECAST_NATIVE_TEMP_LOW, + forecast_entry.get(ATTR_FORECAST_TEMP_LOW), + ): + with suppress(TypeError, ValueError): + forecast_temp_low_f = float(forecast_temp_low) + value_temp_low = UNIT_CONVERSIONS[ + ATTR_WEATHER_TEMPERATURE_UNIT + ]( + forecast_temp_low_f, + from_temp_unit, + to_temp_unit, + ) + + forecast_entry[ATTR_FORECAST_TEMP_LOW] = round_temperature( + value_temp_low, precision + ) + + if forecast_pressure := forecast_entry.pop( + ATTR_FORECAST_NATIVE_PRESSURE, + forecast_entry.get(ATTR_FORECAST_PRESSURE), + ): + from_pressure_unit = ( + self.native_pressure_unit or self._default_pressure_unit ) - if ( - native_pressure := forecast_entry.get(ATTR_FORECAST_PRESSURE) - ) is not None: - if (unit := self.pressure_unit) is not None: - pressure = round( - self.hass.config.units.pressure(native_pressure, unit), - ROUNDING_PRECISION, - ) - forecast_entry[ATTR_FORECAST_PRESSURE] = pressure - if ( - native_wind_speed := forecast_entry.get(ATTR_FORECAST_WIND_SPEED) - ) is not None: - if (unit := self.wind_speed_unit) is not None: - wind_speed = round( - self.hass.config.units.wind_speed(native_wind_speed, unit), - ROUNDING_PRECISION, - ) - forecast_entry[ATTR_FORECAST_WIND_SPEED] = wind_speed - if ( - native_precip := forecast_entry.get(ATTR_FORECAST_PRECIPITATION) - ) is not None: - if (unit := self.precipitation_unit) is not None: - precipitation = round( - self.hass.config.units.accumulated_precipitation( - native_precip, unit + to_pressure_unit = self._pressure_unit + with suppress(TypeError, ValueError): + forecast_pressure_f = float(forecast_pressure) + forecast_entry[ATTR_FORECAST_PRESSURE] = round( + UNIT_CONVERSIONS[ATTR_WEATHER_PRESSURE_UNIT]( + forecast_pressure_f, + from_pressure_unit, + to_pressure_unit, + ), + ROUNDING_PRECISION, + ) + + if forecast_wind_speed := forecast_entry.pop( + ATTR_FORECAST_NATIVE_WIND_SPEED, + forecast_entry.get(ATTR_FORECAST_WIND_SPEED), + ): + from_wind_speed_unit = ( + self.native_wind_speed_unit or self._default_wind_speed_unit + ) + to_wind_speed_unit = self._wind_speed_unit + with suppress(TypeError, ValueError): + forecast_wind_speed_f = float(forecast_wind_speed) + forecast_entry[ATTR_FORECAST_WIND_SPEED] = round( + UNIT_CONVERSIONS[ATTR_WEATHER_WIND_SPEED_UNIT]( + forecast_wind_speed_f, + from_wind_speed_unit, + to_wind_speed_unit, + ), + ROUNDING_PRECISION, + ) + + if forecast_precipitation := forecast_entry.pop( + ATTR_FORECAST_NATIVE_PRECIPITATION, + forecast_entry.get(ATTR_FORECAST_PRECIPITATION), + ): + from_precipitation_unit = ( + self.native_precipitation_unit + or self._default_precipitation_unit + ) + to_precipitation_unit = self._precipitation_unit + with suppress(TypeError, ValueError): + forecast_precipitation_f = float(forecast_precipitation) + forecast_entry[ATTR_FORECAST_PRECIPITATION] = round( + UNIT_CONVERSIONS[ATTR_WEATHER_PRECIPITATION_UNIT]( + forecast_precipitation_f, + from_precipitation_unit, + to_precipitation_unit, ), ROUNDING_PRECISION, ) - forecast_entry[ATTR_FORECAST_PRECIPITATION] = precipitation forecast.append(forecast_entry) @@ -309,3 +811,44 @@ class WeatherEntity(Entity): def condition(self) -> str | None: """Return the current condition.""" return self._attr_condition + + @callback + def async_registry_entry_updated(self) -> None: + """Run when the entity registry entry has been updated.""" + assert self.registry_entry + self._weather_option_temperature_unit = None + self._weather_option_pressure_unit = None + self._weather_option_precipitation_unit = None + self._weather_option_wind_speed_unit = None + self._weather_option_visibility_unit = None + if weather_options := self.registry_entry.options.get(DOMAIN): + if ( + custom_unit_temperature := weather_options.get( + ATTR_WEATHER_TEMPERATURE_UNIT + ) + ) and custom_unit_temperature in VALID_UNITS[ATTR_WEATHER_TEMPERATURE_UNIT]: + self._weather_option_temperature_unit = custom_unit_temperature + if ( + custom_unit_pressure := weather_options.get(ATTR_WEATHER_PRESSURE_UNIT) + ) and custom_unit_pressure in VALID_UNITS[ATTR_WEATHER_PRESSURE_UNIT]: + self._weather_option_pressure_unit = custom_unit_pressure + if ( + custom_unit_precipitation := weather_options.get( + ATTR_WEATHER_PRECIPITATION_UNIT + ) + ) and custom_unit_precipitation in VALID_UNITS[ + ATTR_WEATHER_PRECIPITATION_UNIT + ]: + self._weather_option_precipitation_unit = custom_unit_precipitation + if ( + custom_unit_wind_speed := weather_options.get( + ATTR_WEATHER_WIND_SPEED_UNIT + ) + ) and custom_unit_wind_speed in VALID_UNITS[ATTR_WEATHER_WIND_SPEED_UNIT]: + self._weather_option_wind_speed_unit = custom_unit_wind_speed + if ( + custom_unit_visibility := weather_options.get( + ATTR_WEATHER_VISIBILITY_UNIT + ) + ) and custom_unit_visibility in VALID_UNITS[ATTR_WEATHER_VISIBILITY_UNIT]: + self._weather_option_visibility_unit = custom_unit_visibility diff --git a/tests/components/accuweather/test_weather.py b/tests/components/accuweather/test_weather.py index 02ace5d3f1d..97f588cb477 100644 --- a/tests/components/accuweather/test_weather.py +++ b/tests/components/accuweather/test_weather.py @@ -46,7 +46,7 @@ async def test_weather_without_forecast(hass): assert state.attributes.get(ATTR_WEATHER_TEMPERATURE) == 22.6 assert state.attributes.get(ATTR_WEATHER_VISIBILITY) == 16.1 assert state.attributes.get(ATTR_WEATHER_WIND_BEARING) == 180 - assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 4.03 + assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 14.5 # 4.03 m/s -> km/h assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION entry = registry.async_get("weather.home") @@ -68,7 +68,7 @@ async def test_weather_with_forecast(hass): assert state.attributes.get(ATTR_WEATHER_TEMPERATURE) == 22.6 assert state.attributes.get(ATTR_WEATHER_VISIBILITY) == 16.1 assert state.attributes.get(ATTR_WEATHER_WIND_BEARING) == 180 - assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 4.03 + assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 14.5 # 4.03 m/s -> km/h assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION forecast = state.attributes.get(ATTR_FORECAST)[0] assert forecast.get(ATTR_FORECAST_CONDITION) == "lightning-rainy" @@ -78,7 +78,7 @@ async def test_weather_with_forecast(hass): assert forecast.get(ATTR_FORECAST_TEMP_LOW) == 15.4 assert forecast.get(ATTR_FORECAST_TIME) == "2020-07-26T05:00:00+00:00" assert forecast.get(ATTR_FORECAST_WIND_BEARING) == 166 - assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 3.61 + assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 13.0 # 3.61 m/s -> km/h entry = registry.async_get("weather.home") assert entry diff --git a/tests/components/aemet/test_weather.py b/tests/components/aemet/test_weather.py index 809b61e0bda..ee021cc7f6d 100644 --- a/tests/components/aemet/test_weather.py +++ b/tests/components/aemet/test_weather.py @@ -42,10 +42,10 @@ async def test_aemet_weather(hass): assert state.state == ATTR_CONDITION_SNOWY assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_WEATHER_HUMIDITY) == 99.0 - assert state.attributes.get(ATTR_WEATHER_PRESSURE) == 100440.0 + assert state.attributes.get(ATTR_WEATHER_PRESSURE) == 1004.4 # 100440.0 Pa -> hPa assert state.attributes.get(ATTR_WEATHER_TEMPERATURE) == -0.7 assert state.attributes.get(ATTR_WEATHER_WIND_BEARING) == 90.0 - assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 4.17 + assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 15.0 # 4.17 m/s -> km/h forecast = state.attributes.get(ATTR_FORECAST)[0] assert forecast.get(ATTR_FORECAST_CONDITION) == ATTR_CONDITION_PARTLYCLOUDY assert forecast.get(ATTR_FORECAST_PRECIPITATION) is None @@ -57,7 +57,7 @@ async def test_aemet_weather(hass): == dt_util.parse_datetime("2021-01-10 00:00:00+00:00").isoformat() ) assert forecast.get(ATTR_FORECAST_WIND_BEARING) == 45.0 - assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 5.56 + assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 20.0 # 5.56 m/s -> km/h state = hass.states.get("weather.aemet_hourly") assert state is None diff --git a/tests/components/climacell/test_weather.py b/tests/components/climacell/test_weather.py index 3c02f6b9b1f..593caa7755f 100644 --- a/tests/components/climacell/test_weather.py +++ b/tests/components/climacell/test_weather.py @@ -132,7 +132,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-12T00:00:00-08:00", - ATTR_FORECAST_PRECIPITATION: 0.0457, + ATTR_FORECAST_PRECIPITATION: 0.05, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, ATTR_FORECAST_TEMP: 19.9, ATTR_FORECAST_TEMP_LOW: 12.1, @@ -148,7 +148,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, ATTR_FORECAST_TIME: "2021-03-14T00:00:00-08:00", - ATTR_FORECAST_PRECIPITATION: 1.0744, + ATTR_FORECAST_PRECIPITATION: 1.07, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 75, ATTR_FORECAST_TEMP: 6.4, ATTR_FORECAST_TEMP_LOW: 3.2, @@ -156,7 +156,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY, ATTR_FORECAST_TIME: "2021-03-15T00:00:00-07:00", # DST starts - ATTR_FORECAST_PRECIPITATION: 7.3050, + ATTR_FORECAST_PRECIPITATION: 7.3, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95, ATTR_FORECAST_TEMP: 1.2, ATTR_FORECAST_TEMP_LOW: 0.2, @@ -164,7 +164,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-16T00:00:00-07:00", - ATTR_FORECAST_PRECIPITATION: 0.0051, + ATTR_FORECAST_PRECIPITATION: 0.01, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5, ATTR_FORECAST_TEMP: 6.1, ATTR_FORECAST_TEMP_LOW: -1.6, @@ -188,7 +188,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-19T00:00:00-07:00", - ATTR_FORECAST_PRECIPITATION: 0.1778, + ATTR_FORECAST_PRECIPITATION: 0.18, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 45, ATTR_FORECAST_TEMP: 9.4, ATTR_FORECAST_TEMP_LOW: 4.7, @@ -196,7 +196,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, ATTR_FORECAST_TIME: "2021-03-20T00:00:00-07:00", - ATTR_FORECAST_PRECIPITATION: 1.2319, + ATTR_FORECAST_PRECIPITATION: 1.23, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, ATTR_FORECAST_TEMP: 5.0, ATTR_FORECAST_TEMP_LOW: 3.1, @@ -204,7 +204,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-21T00:00:00-07:00", - ATTR_FORECAST_PRECIPITATION: 0.0432, + ATTR_FORECAST_PRECIPITATION: 0.04, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 20, ATTR_FORECAST_TEMP: 6.8, ATTR_FORECAST_TEMP_LOW: 0.9, @@ -213,11 +213,11 @@ async def test_v3_weather( assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "ClimaCell - Daily" assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 24 assert weather_state.attributes[ATTR_WEATHER_OZONE] == 52.625 - assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1028.1246 + assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1028.12 assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 6.6 - assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 9.9940 + assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 9.99 assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 320.31 - assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 14.6289 + assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 14.63 assert weather_state.attributes[ATTR_CLOUD_COVER] == 100 assert weather_state.attributes[ATTR_WIND_GUST] == 24.0758 assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain" diff --git a/tests/components/demo/test_weather.py b/tests/components/demo/test_weather.py index db3f3441df1..8c93219f8e6 100644 --- a/tests/components/demo/test_weather.py +++ b/tests/components/demo/test_weather.py @@ -36,7 +36,7 @@ async def test_attributes(hass): assert data.get(ATTR_WEATHER_TEMPERATURE) == 21.6 assert data.get(ATTR_WEATHER_HUMIDITY) == 92 assert data.get(ATTR_WEATHER_PRESSURE) == 1099 - assert data.get(ATTR_WEATHER_WIND_SPEED) == 0.5 + assert data.get(ATTR_WEATHER_WIND_SPEED) == 1.8 # 0.5 m/s -> km/h assert data.get(ATTR_WEATHER_WIND_BEARING) is None assert data.get(ATTR_WEATHER_OZONE) is None assert data.get(ATTR_ATTRIBUTION) == "Powered by Home Assistant" @@ -53,20 +53,3 @@ async def test_attributes(hass): data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 100 ) assert len(data.get(ATTR_FORECAST)) == 7 - - -async def test_temperature_convert(hass): - """Test temperature conversion.""" - assert await async_setup_component( - hass, weather.DOMAIN, {"weather": {"platform": "demo"}} - ) - hass.config.units = METRIC_SYSTEM - await hass.async_block_till_done() - - state = hass.states.get("weather.demo_weather_north") - assert state is not None - - assert state.state == "rainy" - - data = state.attributes - assert data.get(ATTR_WEATHER_TEMPERATURE) == -24.4 diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index 7ed1c4d3723..e6469043474 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -198,7 +198,7 @@ async def test_daily_forecast(hass): assert forecast.get(ATTR_FORECAST_TEMP) == 16.2 assert forecast.get(ATTR_FORECAST_TEMP_LOW) == 10.6 assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == "100.0" - assert forecast.get(ATTR_FORECAST_WIND_SPEED) == "10" + assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 10.0 assert forecast.get(ATTR_FORECAST_WIND_BEARING) == "S" @@ -222,5 +222,5 @@ async def test_hourly_forecast(hass): assert forecast.get(ATTR_FORECAST_CONDITION) == "rainy" assert forecast.get(ATTR_FORECAST_TEMP) == 7.7 assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 80.0 - assert forecast.get(ATTR_FORECAST_WIND_SPEED) == "32.7" + assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 32.7 assert forecast.get(ATTR_FORECAST_WIND_BEARING) == "S" diff --git a/tests/components/knx/test_weather.py b/tests/components/knx/test_weather.py index 21d80248b97..c4a7c5de7a4 100644 --- a/tests/components/knx/test_weather.py +++ b/tests/components/knx/test_weather.py @@ -85,8 +85,8 @@ async def test_weather(hass: HomeAssistant, knx: KNXTestKit): state = hass.states.get("weather.test") assert state.attributes["temperature"] == 0.4 assert state.attributes["wind_bearing"] == 270 - assert state.attributes["wind_speed"] == 1.4400000000000002 - assert state.attributes["pressure"] == 980.5824 + assert state.attributes["wind_speed"] == 1.44 + assert state.attributes["pressure"] == 980.58 assert state.state is ATTR_CONDITION_SUNNY # update from KNX - set rain alarm diff --git a/tests/components/tomorrowio/test_weather.py b/tests/components/tomorrowio/test_weather.py index f9c7e00b7cd..52c29161452 100644 --- a/tests/components/tomorrowio/test_weather.py +++ b/tests/components/tomorrowio/test_weather.py @@ -99,13 +99,13 @@ async def test_v4_weather(hass: HomeAssistant) -> None: ATTR_FORECAST_TEMP: 45.9, ATTR_FORECAST_TEMP_LOW: 26.1, ATTR_FORECAST_WIND_BEARING: 239.6, - ATTR_FORECAST_WIND_SPEED: 9.49, + ATTR_FORECAST_WIND_SPEED: 34.16, # 9.49 m/s -> km/h } assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "Tomorrow.io - Daily" assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 23 assert weather_state.attributes[ATTR_WEATHER_OZONE] == 46.53 - assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 3035.0 + assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 30.35 assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 44.1 assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 8.15 assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14 - assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 9.33 + assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 33.59 # 9.33 m/s ->km/h diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index 9849a6abe18..814d3b7857c 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -1,4 +1,6 @@ """The test for weather entity.""" +from datetime import datetime + import pytest from pytest import approx @@ -9,19 +11,44 @@ from homeassistant.components.weather import ( ATTR_FORECAST_PRESSURE, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, + ATTR_WEATHER_OZONE, + ATTR_WEATHER_PRECIPITATION_UNIT, ATTR_WEATHER_PRESSURE, + ATTR_WEATHER_PRESSURE_UNIT, ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_TEMPERATURE_UNIT, ATTR_WEATHER_VISIBILITY, + ATTR_WEATHER_VISIBILITY_UNIT, + ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, + ATTR_WEATHER_WIND_SPEED_UNIT, + ROUNDING_PRECISION, + Forecast, + WeatherEntity, + round_temperature, ) from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + LENGTH_INCHES, + LENGTH_KILOMETERS, LENGTH_MILES, LENGTH_MILLIMETERS, + PRECISION_HALVES, + PRECISION_TENTHS, + PRECISION_WHOLE, + PRESSURE_HPA, PRESSURE_INHG, + PRESSURE_PA, + SPEED_KILOMETERS_PER_HOUR, SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure @@ -29,11 +56,75 @@ from homeassistant.util.speed import convert as convert_speed from homeassistant.util.temperature import convert as convert_temperature from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM +from tests.testing_config.custom_components.test import weather as WeatherPlatform -async def create_entity(hass, **kwargs): + +class MockWeatherEntity(WeatherEntity): + """Mock a Weather Entity.""" + + def __init__(self) -> None: + """Initiate Entity.""" + super().__init__() + self._attr_condition = ATTR_CONDITION_SUNNY + self._attr_native_precipitation_unit = LENGTH_MILLIMETERS + self._attr_native_pressure = 10 + self._attr_native_pressure_unit = PRESSURE_HPA + self._attr_native_temperature = 20 + self._attr_native_temperature_unit = TEMP_CELSIUS + self._attr_native_visibility = 30 + self._attr_native_visibility_unit = LENGTH_KILOMETERS + self._attr_native_wind_speed = 3 + self._attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + self._attr_forecast = [ + Forecast( + datetime=datetime(2022, 6, 20, 20, 00, 00), + native_precipitation=1, + native_temperature=20, + ) + ] + + +class MockWeatherEntityPrecision(WeatherEntity): + """Mock a Weather Entity with precision.""" + + def __init__(self) -> None: + """Initiate Entity.""" + super().__init__() + self._attr_condition = ATTR_CONDITION_SUNNY + self._attr_native_temperature = 20.3 + self._attr_native_temperature_unit = TEMP_CELSIUS + self._attr_precision = PRECISION_HALVES + + +class MockWeatherEntityCompat(WeatherEntity): + """Mock a Weather Entity using old attributes.""" + + def __init__(self) -> None: + """Initiate Entity.""" + super().__init__() + self._attr_condition = ATTR_CONDITION_SUNNY + self._attr_precipitation_unit = LENGTH_MILLIMETERS + self._attr_pressure = 10 + self._attr_pressure_unit = PRESSURE_HPA + self._attr_temperature = 20 + self._attr_temperature_unit = TEMP_CELSIUS + self._attr_visibility = 30 + self._attr_visibility_unit = LENGTH_KILOMETERS + self._attr_wind_speed = 3 + self._attr_wind_speed_unit = SPEED_METERS_PER_SECOND + self._attr_forecast = [ + Forecast( + datetime=datetime(2022, 6, 20, 20, 00, 00), + precipitation=1, + temperature=20, + ) + ] + + +async def create_entity(hass: HomeAssistant, **kwargs): """Create the weather entity to run tests on.""" - kwargs = {"temperature": None, "temperature_unit": None, **kwargs} - platform = getattr(hass.components, "test.weather") + kwargs = {"native_temperature": None, "native_temperature_unit": None, **kwargs} + platform: WeatherPlatform = getattr(hass.components, "test.weather") platform.init(empty=True) platform.ENTITIES.append( platform.MockWeatherMockForecast( @@ -49,145 +140,741 @@ async def create_entity(hass, **kwargs): return entity0 -@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) -async def test_temperature_conversion( - hass, +@pytest.mark.parametrize("native_unit", (TEMP_FAHRENHEIT, TEMP_CELSIUS)) +@pytest.mark.parametrize( + "state_unit, unit_system", + ((TEMP_CELSIUS, METRIC_SYSTEM), (TEMP_FAHRENHEIT, IMPERIAL_SYSTEM)), +) +async def test_temperature( + hass: HomeAssistant, enable_custom_integrations, + native_unit: str, + state_unit: str, unit_system, ): - """Test temperature conversion.""" + """Test temperature.""" hass.config.units = unit_system native_value = 38 - native_unit = TEMP_FAHRENHEIT + state_value = convert_temperature(native_value, native_unit, state_unit) entity0 = await create_entity( - hass, temperature=native_value, temperature_unit=native_unit + hass, native_temperature=native_value, native_temperature_unit=native_unit ) state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] - expected = convert_temperature( - native_value, native_unit, unit_system.temperature_unit - ) + expected = state_value assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( expected, rel=0.1 ) + assert state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == state_unit assert float(forecast[ATTR_FORECAST_TEMP]) == approx(expected, rel=0.1) assert float(forecast[ATTR_FORECAST_TEMP_LOW]) == approx(expected, rel=0.1) -@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) -async def test_pressure_conversion( - hass, +@pytest.mark.parametrize("native_unit", (None,)) +@pytest.mark.parametrize( + "state_unit, unit_system", + ((TEMP_CELSIUS, METRIC_SYSTEM), (TEMP_FAHRENHEIT, IMPERIAL_SYSTEM)), +) +async def test_temperature_no_unit( + hass: HomeAssistant, enable_custom_integrations, + native_unit: str, + state_unit: str, unit_system, ): - """Test pressure conversion.""" + """Test temperature when the entity does not declare a native unit.""" hass.config.units = unit_system - native_value = 30 - native_unit = PRESSURE_INHG + native_value = 38 + state_value = native_value entity0 = await create_entity( - hass, pressure=native_value, pressure_unit=native_unit + hass, native_temperature=native_value, native_temperature_unit=native_unit + ) + + state = hass.states.get(entity0.entity_id) + forecast = state.attributes[ATTR_FORECAST][0] + + expected = state_value + assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( + expected, rel=0.1 + ) + assert state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == state_unit + assert float(forecast[ATTR_FORECAST_TEMP]) == approx(expected, rel=0.1) + assert float(forecast[ATTR_FORECAST_TEMP_LOW]) == approx(expected, rel=0.1) + + +@pytest.mark.parametrize("native_unit", (PRESSURE_INHG, PRESSURE_INHG)) +@pytest.mark.parametrize( + "state_unit, unit_system", + ((PRESSURE_HPA, METRIC_SYSTEM), (PRESSURE_INHG, IMPERIAL_SYSTEM)), +) +async def test_pressure( + hass: HomeAssistant, + enable_custom_integrations, + native_unit: str, + state_unit: str, + unit_system, +): + """Test pressure.""" + hass.config.units = unit_system + native_value = 30 + state_value = convert_pressure(native_value, native_unit, state_unit) + + entity0 = await create_entity( + hass, native_pressure=native_value, native_pressure_unit=native_unit ) state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] - expected = convert_pressure(native_value, native_unit, unit_system.pressure_unit) + expected = state_value assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == approx(expected, rel=1e-2) assert float(forecast[ATTR_FORECAST_PRESSURE]) == approx(expected, rel=1e-2) -@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) -async def test_wind_speed_conversion( - hass, +@pytest.mark.parametrize("native_unit", (None,)) +@pytest.mark.parametrize( + "state_unit, unit_system", + ((PRESSURE_HPA, METRIC_SYSTEM), (PRESSURE_INHG, IMPERIAL_SYSTEM)), +) +async def test_pressure_no_unit( + hass: HomeAssistant, enable_custom_integrations, + native_unit: str, + state_unit: str, unit_system, ): - """Test wind speed conversion.""" + """Test pressure when the entity does not declare a native unit.""" hass.config.units = unit_system - native_value = 10 - native_unit = SPEED_METERS_PER_SECOND + native_value = 30 + state_value = native_value entity0 = await create_entity( - hass, wind_speed=native_value, wind_speed_unit=native_unit + hass, native_pressure=native_value, native_pressure_unit=native_unit + ) + state = hass.states.get(entity0.entity_id) + forecast = state.attributes[ATTR_FORECAST][0] + + expected = state_value + assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == approx(expected, rel=1e-2) + assert float(forecast[ATTR_FORECAST_PRESSURE]) == approx(expected, rel=1e-2) + + +@pytest.mark.parametrize( + "native_unit", + (SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR, SPEED_METERS_PER_SECOND), +) +@pytest.mark.parametrize( + "state_unit, unit_system", + ( + (SPEED_KILOMETERS_PER_HOUR, METRIC_SYSTEM), + (SPEED_MILES_PER_HOUR, IMPERIAL_SYSTEM), + ), +) +async def test_wind_speed( + hass: HomeAssistant, + enable_custom_integrations, + native_unit: str, + state_unit: str, + unit_system, +): + """Test wind speed.""" + hass.config.units = unit_system + native_value = 10 + state_value = convert_speed(native_value, native_unit, state_unit) + + entity0 = await create_entity( + hass, native_wind_speed=native_value, native_wind_speed_unit=native_unit ) state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] - expected = convert_speed(native_value, native_unit, unit_system.wind_speed_unit) + expected = state_value assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == approx( expected, rel=1e-2 ) assert float(forecast[ATTR_FORECAST_WIND_SPEED]) == approx(expected, rel=1e-2) -@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) -async def test_visibility_conversion( - hass, +@pytest.mark.parametrize("native_unit", (None,)) +@pytest.mark.parametrize( + "state_unit, unit_system", + ( + (SPEED_KILOMETERS_PER_HOUR, METRIC_SYSTEM), + (SPEED_MILES_PER_HOUR, IMPERIAL_SYSTEM), + ), +) +async def test_wind_speed_no_unit( + hass: HomeAssistant, enable_custom_integrations, + native_unit: str, + state_unit: str, unit_system, ): - """Test visibility conversion.""" + """Test wind speed when the entity does not declare a native unit.""" hass.config.units = unit_system native_value = 10 - native_unit = LENGTH_MILES + state_value = native_value entity0 = await create_entity( - hass, visibility=native_value, visibility_unit=native_unit + hass, native_wind_speed=native_value, native_wind_speed_unit=native_unit ) state = hass.states.get(entity0.entity_id) - expected = convert_distance(native_value, native_unit, unit_system.length_unit) + forecast = state.attributes[ATTR_FORECAST][0] + + expected = state_value + assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == approx( + expected, rel=1e-2 + ) + assert float(forecast[ATTR_FORECAST_WIND_SPEED]) == approx(expected, rel=1e-2) + + +@pytest.mark.parametrize("native_unit", (LENGTH_MILES, LENGTH_KILOMETERS)) +@pytest.mark.parametrize( + "state_unit, unit_system", + ( + (LENGTH_KILOMETERS, METRIC_SYSTEM), + (LENGTH_MILES, IMPERIAL_SYSTEM), + ), +) +async def test_visibility( + hass: HomeAssistant, + enable_custom_integrations, + native_unit: str, + state_unit: str, + unit_system, +): + """Test visibility.""" + hass.config.units = unit_system + native_value = 10 + state_value = convert_distance(native_value, native_unit, state_unit) + + entity0 = await create_entity( + hass, native_visibility=native_value, native_visibility_unit=native_unit + ) + + state = hass.states.get(entity0.entity_id) + expected = state_value assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == approx( expected, rel=1e-2 ) -@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) -async def test_precipitation_conversion( - hass, +@pytest.mark.parametrize("native_unit", (None,)) +@pytest.mark.parametrize( + "state_unit, unit_system", + ( + (LENGTH_KILOMETERS, METRIC_SYSTEM), + (LENGTH_MILES, IMPERIAL_SYSTEM), + ), +) +async def test_visibility_no_unit( + hass: HomeAssistant, enable_custom_integrations, + native_unit: str, + state_unit: str, unit_system, ): - """Test precipitation conversion.""" + """Test visibility when the entity does not declare a native unit.""" hass.config.units = unit_system - native_value = 30 - native_unit = LENGTH_MILLIMETERS + native_value = 10 + state_value = native_value entity0 = await create_entity( - hass, precipitation=native_value, precipitation_unit=native_unit + hass, native_visibility=native_value, native_visibility_unit=native_unit + ) + + state = hass.states.get(entity0.entity_id) + expected = state_value + assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == approx( + expected, rel=1e-2 + ) + + +@pytest.mark.parametrize("native_unit", (LENGTH_INCHES, LENGTH_MILLIMETERS)) +@pytest.mark.parametrize( + "state_unit, unit_system", + ( + (LENGTH_MILLIMETERS, METRIC_SYSTEM), + (LENGTH_INCHES, IMPERIAL_SYSTEM), + ), +) +async def test_precipitation( + hass: HomeAssistant, + enable_custom_integrations, + native_unit: str, + state_unit: str, + unit_system, +): + """Test precipitation.""" + hass.config.units = unit_system + native_value = 30 + state_value = convert_distance(native_value, native_unit, state_unit) + + entity0 = await create_entity( + hass, native_precipitation=native_value, native_precipitation_unit=native_unit ) state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] - expected = convert_distance( - native_value, native_unit, unit_system.accumulated_precipitation_unit - ) + expected = state_value assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx(expected, rel=1e-2) +@pytest.mark.parametrize("native_unit", (None,)) +@pytest.mark.parametrize( + "state_unit, unit_system", + ( + (LENGTH_MILLIMETERS, METRIC_SYSTEM), + (LENGTH_INCHES, IMPERIAL_SYSTEM), + ), +) +async def test_precipitation_no_unit( + hass: HomeAssistant, + enable_custom_integrations, + native_unit: str, + state_unit: str, + unit_system, +): + """Test precipitation when the entity does not declare a native unit.""" + hass.config.units = unit_system + native_value = 30 + state_value = native_value + + entity0 = await create_entity( + hass, native_precipitation=native_value, native_precipitation_unit=native_unit + ) + + state = hass.states.get(entity0.entity_id) + forecast = state.attributes[ATTR_FORECAST][0] + + expected = state_value + assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx(expected, rel=1e-2) + + +async def test_wind_bearing_and_ozone( + hass: HomeAssistant, + enable_custom_integrations, +): + """Test wind bearing.""" + wind_bearing_value = 180 + ozone_value = 10 + + entity0 = await create_entity( + hass, wind_bearing=wind_bearing_value, ozone=ozone_value + ) + + state = hass.states.get(entity0.entity_id) + assert float(state.attributes[ATTR_WEATHER_WIND_BEARING]) == 180 + assert float(state.attributes[ATTR_WEATHER_OZONE]) == 10 + + async def test_none_forecast( - hass, + hass: HomeAssistant, enable_custom_integrations, ): """Test that conversion with None values succeeds.""" entity0 = await create_entity( hass, - pressure=None, - pressure_unit=PRESSURE_INHG, - wind_speed=None, - wind_speed_unit=SPEED_METERS_PER_SECOND, - precipitation=None, - precipitation_unit=LENGTH_MILLIMETERS, + native_pressure=None, + native_pressure_unit=PRESSURE_INHG, + native_wind_speed=None, + native_wind_speed_unit=SPEED_METERS_PER_SECOND, + native_precipitation=None, + native_precipitation_unit=LENGTH_MILLIMETERS, ) state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] - assert forecast[ATTR_FORECAST_PRESSURE] is None - assert forecast[ATTR_FORECAST_WIND_SPEED] is None - assert forecast[ATTR_FORECAST_PRECIPITATION] is None + assert forecast.get(ATTR_FORECAST_PRESSURE) is None + assert forecast.get(ATTR_FORECAST_WIND_SPEED) is None + assert forecast.get(ATTR_FORECAST_PRECIPITATION) is None + + +async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> None: + """Test custom unit.""" + wind_speed_value = 5 + wind_speed_unit = SPEED_METERS_PER_SECOND + pressure_value = 110 + pressure_unit = PRESSURE_HPA + temperature_value = 20 + temperature_unit = TEMP_CELSIUS + visibility_value = 11 + visibility_unit = LENGTH_KILOMETERS + precipitation_value = 1.1 + precipitation_unit = LENGTH_MILLIMETERS + + set_options = { + "wind_speed_unit": SPEED_MILES_PER_HOUR, + "precipitation_unit": LENGTH_INCHES, + "pressure_unit": PRESSURE_INHG, + "temperature_unit": TEMP_FAHRENHEIT, + "visibility_unit": LENGTH_MILES, + } + + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get_or_create("weather", "test", "very_unique") + entity_registry.async_update_entity_options(entry.entity_id, "weather", set_options) + await hass.async_block_till_done() + + platform: WeatherPlatform = getattr(hass.components, "test.weather") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockWeatherMockForecast( + name="Test", + condition=ATTR_CONDITION_SUNNY, + native_temperature=temperature_value, + native_temperature_unit=temperature_unit, + native_wind_speed=wind_speed_value, + native_wind_speed_unit=wind_speed_unit, + native_pressure=pressure_value, + native_pressure_unit=pressure_unit, + native_visibility=visibility_value, + native_visibility_unit=visibility_unit, + native_precipitation=precipitation_value, + native_precipitation_unit=precipitation_unit, + unique_id="very_unique", + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component( + hass, "weather", {"weather": {"platform": "test"}} + ) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + forecast = state.attributes[ATTR_FORECAST][0] + + expected_wind_speed = round( + convert_speed(wind_speed_value, wind_speed_unit, SPEED_MILES_PER_HOUR), + ROUNDING_PRECISION, + ) + expected_temperature = convert_temperature( + temperature_value, temperature_unit, TEMP_FAHRENHEIT + ) + expected_pressure = round( + convert_pressure(pressure_value, pressure_unit, PRESSURE_INHG), + ROUNDING_PRECISION, + ) + expected_visibility = round( + convert_distance(visibility_value, visibility_unit, LENGTH_MILES), + ROUNDING_PRECISION, + ) + expected_precipitation = round( + convert_distance(precipitation_value, precipitation_unit, LENGTH_INCHES), + ROUNDING_PRECISION, + ) + + assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == approx( + expected_wind_speed + ) + assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( + expected_temperature, rel=0.1 + ) + assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == approx(expected_pressure) + assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == approx( + expected_visibility + ) + assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx( + expected_precipitation, rel=1e-2 + ) + + assert ( + state.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] + == set_options["precipitation_unit"] + ) + assert state.attributes[ATTR_WEATHER_PRESSURE_UNIT] == set_options["pressure_unit"] + assert ( + state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] + == set_options["temperature_unit"] + ) + assert ( + state.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == set_options["visibility_unit"] + ) + assert ( + state.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] == set_options["wind_speed_unit"] + ) + + +async def test_backwards_compatibility( + hass: HomeAssistant, enable_custom_integrations +) -> None: + """Test backwards compatibility.""" + wind_speed_value = 5 + wind_speed_unit = SPEED_METERS_PER_SECOND + pressure_value = 110000 + pressure_unit = PRESSURE_PA + temperature_value = 20 + temperature_unit = TEMP_CELSIUS + visibility_value = 11 + visibility_unit = LENGTH_KILOMETERS + precipitation_value = 1 + precipitation_unit = LENGTH_MILLIMETERS + + hass.config.units = METRIC_SYSTEM + + platform: WeatherPlatform = getattr(hass.components, "test.weather") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockWeatherMockForecastCompat( + name="Test", + condition=ATTR_CONDITION_SUNNY, + temperature=temperature_value, + temperature_unit=temperature_unit, + wind_speed=wind_speed_value, + wind_speed_unit=wind_speed_unit, + pressure=pressure_value, + pressure_unit=pressure_unit, + visibility=visibility_value, + visibility_unit=visibility_unit, + precipitation=precipitation_value, + precipitation_unit=precipitation_unit, + unique_id="very_unique", + ) + ) + platform.ENTITIES.append( + platform.MockWeatherMockForecastCompat( + name="Test2", + condition=ATTR_CONDITION_SUNNY, + temperature=temperature_value, + temperature_unit=temperature_unit, + wind_speed=wind_speed_value, + pressure=pressure_value, + visibility=visibility_value, + precipitation=precipitation_value, + unique_id="very_unique2", + ) + ) + + entity0 = platform.ENTITIES[0] + entity1 = platform.ENTITIES[1] + assert await async_setup_component( + hass, "weather", {"weather": {"platform": "test"}} + ) + assert await async_setup_component( + hass, "weather", {"weather": {"platform": "test2"}} + ) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + forecast = state.attributes[ATTR_FORECAST][0] + state1 = hass.states.get(entity1.entity_id) + forecast1 = state1.attributes[ATTR_FORECAST][0] + + assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == approx( + wind_speed_value * 3.6 + ) + assert state.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] == SPEED_KILOMETERS_PER_HOUR + assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( + temperature_value, rel=0.1 + ) + assert state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == TEMP_CELSIUS + assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == approx( + pressure_value / 100 + ) + assert state.attributes[ATTR_WEATHER_PRESSURE_UNIT] == PRESSURE_HPA + assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == approx(visibility_value) + assert state.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == LENGTH_KILOMETERS + assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx( + precipitation_value, rel=1e-2 + ) + assert state.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == LENGTH_MILLIMETERS + + assert float(state1.attributes[ATTR_WEATHER_WIND_SPEED]) == approx(wind_speed_value) + assert state1.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] == SPEED_KILOMETERS_PER_HOUR + assert float(state1.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( + temperature_value, rel=0.1 + ) + assert state1.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == TEMP_CELSIUS + assert float(state1.attributes[ATTR_WEATHER_PRESSURE]) == approx(pressure_value) + assert state1.attributes[ATTR_WEATHER_PRESSURE_UNIT] == PRESSURE_HPA + assert float(state1.attributes[ATTR_WEATHER_VISIBILITY]) == approx(visibility_value) + assert state1.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == LENGTH_KILOMETERS + assert float(forecast1[ATTR_FORECAST_PRECIPITATION]) == approx( + precipitation_value, rel=1e-2 + ) + assert state1.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == LENGTH_MILLIMETERS + + +async def test_backwards_compatibility_convert_values( + hass: HomeAssistant, enable_custom_integrations +) -> None: + """Test backward compatibility for converting values.""" + wind_speed_value = 5 + wind_speed_unit = SPEED_METERS_PER_SECOND + pressure_value = 110000 + pressure_unit = PRESSURE_PA + temperature_value = 20 + temperature_unit = TEMP_CELSIUS + visibility_value = 11 + visibility_unit = LENGTH_KILOMETERS + precipitation_value = 1 + precipitation_unit = LENGTH_MILLIMETERS + + hass.config.units = IMPERIAL_SYSTEM + + platform: WeatherPlatform = getattr(hass.components, "test.weather") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockWeatherMockForecastCompat( + name="Test", + condition=ATTR_CONDITION_SUNNY, + temperature=temperature_value, + temperature_unit=temperature_unit, + wind_speed=wind_speed_value, + wind_speed_unit=wind_speed_unit, + pressure=pressure_value, + pressure_unit=pressure_unit, + visibility=visibility_value, + visibility_unit=visibility_unit, + precipitation=precipitation_value, + precipitation_unit=precipitation_unit, + unique_id="very_unique", + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component( + hass, "weather", {"weather": {"platform": "test"}} + ) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + + expected_wind_speed = round( + convert_speed(wind_speed_value, wind_speed_unit, SPEED_MILES_PER_HOUR), + ROUNDING_PRECISION, + ) + expected_temperature = convert_temperature( + temperature_value, temperature_unit, TEMP_FAHRENHEIT + ) + expected_pressure = round( + convert_pressure(pressure_value, pressure_unit, PRESSURE_INHG), + ROUNDING_PRECISION, + ) + expected_visibility = round( + convert_distance(visibility_value, visibility_unit, LENGTH_MILES), + ROUNDING_PRECISION, + ) + expected_precipitation = round( + convert_distance(precipitation_value, precipitation_unit, LENGTH_INCHES), + ROUNDING_PRECISION, + ) + + assert state.attributes == { + ATTR_FORECAST: [ + { + ATTR_FORECAST_PRECIPITATION: approx(expected_precipitation, rel=0.1), + ATTR_FORECAST_PRESSURE: approx(expected_pressure, rel=0.1), + ATTR_FORECAST_TEMP: approx(expected_temperature, rel=0.1), + ATTR_FORECAST_TEMP_LOW: approx(expected_temperature, rel=0.1), + ATTR_FORECAST_WIND_BEARING: None, + ATTR_FORECAST_WIND_SPEED: approx(expected_wind_speed, rel=0.1), + } + ], + ATTR_FRIENDLY_NAME: "Test", + ATTR_WEATHER_PRECIPITATION_UNIT: LENGTH_INCHES, + ATTR_WEATHER_PRESSURE: approx(expected_pressure, rel=0.1), + ATTR_WEATHER_PRESSURE_UNIT: PRESSURE_INHG, + ATTR_WEATHER_TEMPERATURE: approx(expected_temperature, rel=0.1), + ATTR_WEATHER_TEMPERATURE_UNIT: TEMP_FAHRENHEIT, + ATTR_WEATHER_VISIBILITY: approx(expected_visibility, rel=0.1), + ATTR_WEATHER_VISIBILITY_UNIT: LENGTH_MILES, + ATTR_WEATHER_WIND_SPEED: approx(expected_wind_speed, rel=0.1), + ATTR_WEATHER_WIND_SPEED_UNIT: SPEED_MILES_PER_HOUR, + } + + +async def test_backwards_compatibility_round_temperature(hass: HomeAssistant) -> None: + """Test backward compatibility for rounding temperature.""" + + assert round_temperature(20.3, PRECISION_HALVES) == 20.5 + assert round_temperature(20.3, PRECISION_TENTHS) == 20.3 + assert round_temperature(20.3, PRECISION_WHOLE) == 20 + assert round_temperature(None, PRECISION_WHOLE) is None + + +async def test_attr(hass: HomeAssistant) -> None: + """Test the _attr attributes.""" + + weather = MockWeatherEntity() + weather.hass = hass + + assert weather.condition == ATTR_CONDITION_SUNNY + assert weather.native_precipitation_unit == LENGTH_MILLIMETERS + assert weather._precipitation_unit == LENGTH_MILLIMETERS + assert weather.native_pressure == 10 + assert weather.native_pressure_unit == PRESSURE_HPA + assert weather._pressure_unit == PRESSURE_HPA + assert weather.native_temperature == 20 + assert weather.native_temperature_unit == TEMP_CELSIUS + assert weather._temperature_unit == TEMP_CELSIUS + assert weather.native_visibility == 30 + assert weather.native_visibility_unit == LENGTH_KILOMETERS + assert weather._visibility_unit == LENGTH_KILOMETERS + assert weather.native_wind_speed == 3 + assert weather.native_wind_speed_unit == SPEED_METERS_PER_SECOND + assert weather._wind_speed_unit == SPEED_KILOMETERS_PER_HOUR + + +async def test_attr_compatibility(hass: HomeAssistant) -> None: + """Test the _attr attributes in compatibility mode.""" + + weather = MockWeatherEntityCompat() + weather.hass = hass + + assert weather.condition == ATTR_CONDITION_SUNNY + assert weather._precipitation_unit == LENGTH_MILLIMETERS + assert weather.pressure == 10 + assert weather._pressure_unit == PRESSURE_HPA + assert weather.temperature == 20 + assert weather._temperature_unit == TEMP_CELSIUS + assert weather.visibility == 30 + assert weather.visibility_unit == LENGTH_KILOMETERS + assert weather.wind_speed == 3 + assert weather._wind_speed_unit == SPEED_KILOMETERS_PER_HOUR + + forecast_entry = [ + Forecast( + datetime=datetime(2022, 6, 20, 20, 00, 00), + precipitation=1, + temperature=20, + ) + ] + + assert weather.forecast == forecast_entry + + assert weather.state_attributes == { + ATTR_FORECAST: forecast_entry, + ATTR_WEATHER_PRESSURE: 10.0, + ATTR_WEATHER_PRESSURE_UNIT: PRESSURE_HPA, + ATTR_WEATHER_TEMPERATURE: 20.0, + ATTR_WEATHER_TEMPERATURE_UNIT: TEMP_CELSIUS, + ATTR_WEATHER_VISIBILITY: 30.0, + ATTR_WEATHER_VISIBILITY_UNIT: LENGTH_KILOMETERS, + ATTR_WEATHER_WIND_SPEED: 3.0 * 3.6, + ATTR_WEATHER_WIND_SPEED_UNIT: SPEED_KILOMETERS_PER_HOUR, + ATTR_WEATHER_PRECIPITATION_UNIT: LENGTH_MILLIMETERS, + } + + +async def test_precision_for_temperature(hass: HomeAssistant) -> None: + """Test the precision for temperature.""" + + weather = MockWeatherEntityPrecision() + weather.hass = hass + + assert weather.condition == ATTR_CONDITION_SUNNY + assert weather.native_temperature == 20.3 + assert weather._temperature_unit == TEMP_CELSIUS + assert weather.precision == PRECISION_HALVES + + assert weather.state_attributes[ATTR_WEATHER_TEMPERATURE] == 20.5 diff --git a/tests/testing_config/custom_components/test/weather.py b/tests/testing_config/custom_components/test/weather.py index 224d6495548..23a9569c785 100644 --- a/tests/testing_config/custom_components/test/weather.py +++ b/tests/testing_config/custom_components/test/weather.py @@ -6,6 +6,11 @@ Call init before using it in your tests to ensure clean test data. from __future__ import annotations from homeassistant.components.weather import ( + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRESSURE, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRESSURE, ATTR_FORECAST_TEMP, @@ -37,6 +42,80 @@ async def async_setup_platform( class MockWeather(MockEntity, WeatherEntity): """Mock weather class.""" + @property + def native_temperature(self) -> float | None: + """Return the platform temperature.""" + return self._handle("native_temperature") + + @property + def native_temperature_unit(self) -> str | None: + """Return the unit of measurement for temperature.""" + return self._handle("native_temperature_unit") + + @property + def native_pressure(self) -> float | None: + """Return the pressure.""" + return self._handle("native_pressure") + + @property + def native_pressure_unit(self) -> str | None: + """Return the unit of measurement for pressure.""" + return self._handle("native_pressure_unit") + + @property + def humidity(self) -> float | None: + """Return the humidity.""" + return self._handle("humidity") + + @property + def native_wind_speed(self) -> float | None: + """Return the wind speed.""" + return self._handle("native_wind_speed") + + @property + def native_wind_speed_unit(self) -> str | None: + """Return the unit of measurement for wind speed.""" + return self._handle("native_wind_speed_unit") + + @property + def wind_bearing(self) -> float | str | None: + """Return the wind bearing.""" + return self._handle("wind_bearing") + + @property + def ozone(self) -> float | None: + """Return the ozone level.""" + return self._handle("ozone") + + @property + def native_visibility(self) -> float | None: + """Return the visibility.""" + return self._handle("native_visibility") + + @property + def native_visibility_unit(self) -> str | None: + """Return the unit of measurement for visibility.""" + return self._handle("native_visibility_unit") + + @property + def forecast(self) -> list[Forecast] | None: + """Return the forecast.""" + return self._handle("forecast") + + @property + def native_precipitation_unit(self) -> str | None: + """Return the native unit of measurement for accumulated precipitation.""" + return self._handle("native_precipitation_unit") + + @property + def condition(self) -> str | None: + """Return the current condition.""" + return self._handle("condition") + + +class MockWeatherCompat(MockEntity, WeatherEntity): + """Mock weather class for backwards compatibility check.""" + @property def temperature(self) -> float | None: """Return the platform temperature.""" @@ -99,7 +178,7 @@ class MockWeather(MockEntity, WeatherEntity): @property def precipitation_unit(self) -> str | None: - """Return the native unit of measurement for accumulated precipitation.""" + """Return the unit of measurement for accumulated precipitation.""" return self._handle("precipitation_unit") @property @@ -111,6 +190,26 @@ class MockWeather(MockEntity, WeatherEntity): class MockWeatherMockForecast(MockWeather): """Mock weather class with mocked forecast.""" + @property + def forecast(self) -> list[Forecast] | None: + """Return the forecast.""" + return [ + { + ATTR_FORECAST_NATIVE_TEMP: self.native_temperature, + ATTR_FORECAST_NATIVE_TEMP_LOW: self.native_temperature, + ATTR_FORECAST_NATIVE_PRESSURE: self.native_pressure, + ATTR_FORECAST_NATIVE_WIND_SPEED: self.native_wind_speed, + ATTR_FORECAST_WIND_BEARING: self.wind_bearing, + ATTR_FORECAST_NATIVE_PRECIPITATION: self._values.get( + "native_precipitation" + ), + } + ] + + +class MockWeatherMockForecastCompat(MockWeatherCompat): + """Mock weather class with mocked forecast for compatibility check.""" + @property def forecast(self) -> list[Forecast] | None: """Return the forecast.""" From f91a2220348395c71986be1b879cae4d29c6b2ea Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 23 Jun 2022 11:12:22 +0200 Subject: [PATCH 1730/3516] Fix compensation (numpy) tests (#73890) --- tests/components/compensation/test_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/compensation/test_sensor.py b/tests/components/compensation/test_sensor.py index 65741fd86ba..6d504d03b6a 100644 --- a/tests/components/compensation/test_sensor.py +++ b/tests/components/compensation/test_sensor.py @@ -163,7 +163,7 @@ async def test_numpy_errors(hass, caplog): await hass.async_start() await hass.async_block_till_done() - assert "invalid value encountered in true_divide" in caplog.text + assert "invalid value encountered in divide" in caplog.text async def test_datapoints_greater_than_degree(hass, caplog): From 4ee92f3953d5caa66131a1c2586289221a633793 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Thu, 23 Jun 2022 11:34:34 +0200 Subject: [PATCH 1731/3516] Improve hvac_mode compatibility of vicare (#66454) --- homeassistant/components/vicare/climate.py | 48 ++++++++++++++-------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 4773101f1b9..fb8e60c3318 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -71,19 +71,13 @@ VICARE_TEMP_HEATING_MIN = 3 VICARE_TEMP_HEATING_MAX = 37 VICARE_TO_HA_HVAC_HEATING = { - VICARE_MODE_DHW: HVACMode.OFF, - VICARE_MODE_HEATING: HVACMode.HEAT, - VICARE_MODE_DHWANDHEATING: HVACMode.AUTO, - VICARE_MODE_DHWANDHEATINGCOOLING: HVACMode.AUTO, VICARE_MODE_FORCEDREDUCED: HVACMode.OFF, - VICARE_MODE_FORCEDNORMAL: HVACMode.HEAT, VICARE_MODE_OFF: HVACMode.OFF, -} - -HA_TO_VICARE_HVAC_HEATING = { - HVACMode.HEAT: VICARE_MODE_FORCEDNORMAL, - HVACMode.OFF: VICARE_MODE_FORCEDREDUCED, - HVACMode.AUTO: VICARE_MODE_DHWANDHEATING, + VICARE_MODE_DHW: HVACMode.OFF, + VICARE_MODE_DHWANDHEATINGCOOLING: HVACMode.AUTO, + VICARE_MODE_DHWANDHEATING: HVACMode.AUTO, + VICARE_MODE_HEATING: HVACMode.AUTO, + VICARE_MODE_FORCEDNORMAL: HVACMode.HEAT, } VICARE_TO_HA_PRESET_HEATING = { @@ -276,19 +270,41 @@ class ViCareClimate(ClimateEntity): def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set a new hvac mode on the ViCare API.""" - vicare_mode = HA_TO_VICARE_HVAC_HEATING.get(hvac_mode) + if "vicare_modes" not in self._attributes: + raise ValueError("Cannot set hvac mode when vicare_modes are not known") + + vicare_mode = self.vicare_mode_from_hvac_mode(hvac_mode) if vicare_mode is None: - raise ValueError( - f"Cannot set invalid vicare mode: {hvac_mode} / {vicare_mode}" - ) + raise ValueError(f"Cannot set invalid hvac mode: {hvac_mode}") _LOGGER.debug("Setting hvac mode to %s / %s", hvac_mode, vicare_mode) self._circuit.setMode(vicare_mode) + def vicare_mode_from_hvac_mode(self, hvac_mode): + """Return the corresponding vicare mode for an hvac_mode.""" + if "vicare_modes" not in self._attributes: + return None + + supported_modes = self._attributes["vicare_modes"] + for key, value in VICARE_TO_HA_HVAC_HEATING.items(): + if key in supported_modes and value == hvac_mode: + return key + return None + @property def hvac_modes(self) -> list[HVACMode]: """Return the list of available hvac modes.""" - return list(HA_TO_VICARE_HVAC_HEATING) + if "vicare_modes" not in self._attributes: + return [] + + supported_modes = self._attributes["vicare_modes"] + hvac_modes = [] + for key, value in VICARE_TO_HA_HVAC_HEATING.items(): + if value in hvac_modes: + continue + if key in supported_modes: + hvac_modes.append(value) + return hvac_modes @property def hvac_action(self) -> HVACAction: From 0dd181f9221155f8004a0bb6203bc2ab5bf3525c Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 23 Jun 2022 12:35:47 +0300 Subject: [PATCH 1732/3516] Remove deprecated YAML for Islamic prayer times (#72483) --- .../islamic_prayer_times/__init__.py | 41 ++----------------- .../islamic_prayer_times/config_flow.py | 4 -- .../islamic_prayer_times/test_config_flow.py | 13 ------ .../islamic_prayer_times/test_init.py | 17 -------- 4 files changed, 4 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index c88e26e1c90..406eaf23670 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -4,63 +4,30 @@ import logging from prayer_times_calculator import PrayerTimesCalculator, exceptions from requests.exceptions import ConnectionError as ConnError -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later, async_track_point_in_time -from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util -from .const import ( - CALC_METHODS, - CONF_CALC_METHOD, - DATA_UPDATED, - DEFAULT_CALC_METHOD, - DOMAIN, -) +from .const import CONF_CALC_METHOD, DATA_UPDATED, DEFAULT_CALC_METHOD, DOMAIN _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.SENSOR] -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: { - vol.Optional(CONF_CALC_METHOD, default=DEFAULT_CALC_METHOD): vol.In( - CALC_METHODS - ), - } - }, - ), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Import the Islamic Prayer component from config.""" - if DOMAIN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] - ) - ) - - return True +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up the Islamic Prayer Component.""" client = IslamicPrayerClient(hass, config_entry) - if not await client.async_setup(): - return False + await client.async_setup() hass.data.setdefault(DOMAIN, client) return True diff --git a/homeassistant/components/islamic_prayer_times/config_flow.py b/homeassistant/components/islamic_prayer_times/config_flow.py index 3379af3860f..5278750d36e 100644 --- a/homeassistant/components/islamic_prayer_times/config_flow.py +++ b/homeassistant/components/islamic_prayer_times/config_flow.py @@ -32,10 +32,6 @@ class IslamicPrayerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=NAME, data=user_input) - async def async_step_import(self, import_config): - """Import from config.""" - return await self.async_step_user(user_input=import_config) - class IslamicPrayerOptionsFlowHandler(config_entries.OptionsFlow): """Handle Islamic Prayer client options.""" diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index 18d64842c65..730c5634770 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -59,19 +59,6 @@ async def test_options(hass): assert result["data"][CONF_CALC_METHOD] == "makkah" -async def test_import(hass): - """Test import step.""" - result = await hass.config_entries.flow.async_init( - islamic_prayer_times.DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_CALC_METHOD: "makkah"}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "Islamic Prayer Times" - assert result["data"][CONF_CALC_METHOD] == "makkah" - - async def test_integration_already_configured(hass): """Test integration is already configured.""" entry = MockConfigEntry( diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index e40af8c89ff..5a092373eef 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -9,7 +9,6 @@ import pytest from homeassistant import config_entries from homeassistant.components import islamic_prayer_times -from homeassistant.setup import async_setup_component from . import ( NEW_PRAYER_TIMES, @@ -28,22 +27,6 @@ def set_utc(hass): hass.config.set_time_zone("UTC") -async def test_setup_with_config(hass): - """Test that we import the config and setup the client.""" - config = { - islamic_prayer_times.DOMAIN: {islamic_prayer_times.CONF_CALC_METHOD: "isna"} - } - with patch( - "prayer_times_calculator.PrayerTimesCalculator.fetch_prayer_times", - return_value=PRAYER_TIMES, - ): - assert ( - await async_setup_component(hass, islamic_prayer_times.DOMAIN, config) - is True - ) - await hass.async_block_till_done() - - async def test_successful_config_entry(hass): """Test that Islamic Prayer Times is configured successfully.""" From 10b083bbf59bf24e8099fc3002a8fbc6460bc2f8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 23 Jun 2022 05:41:34 -0400 Subject: [PATCH 1733/3516] Sync empty entities when Google is disabled in cloud (#72806) --- homeassistant/components/cloud/client.py | 6 +++--- .../components/cloud/google_config.py | 1 + .../components/google_assistant/helpers.py | 8 ++++--- .../components/google_assistant/smart_home.py | 21 ++++++++++++++++--- tests/components/cloud/test_client.py | 21 +++++++++++++++---- 5 files changed, 44 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index c47544f9d99..6011e9bf551 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -210,11 +210,11 @@ class CloudClient(Interface): async def async_google_message(self, payload: dict[Any, Any]) -> dict[Any, Any]: """Process cloud google message to client.""" - if not self._prefs.google_enabled: - return ga.turned_off_response(payload) - gconf = await self.get_google_config() + if not self._prefs.google_enabled: + return ga.api_disabled_response(payload, gconf.agent_user_id) + return await ga.async_handle_message( self._hass, gconf, gconf.cloud_user, payload, gc.SOURCE_CLOUD ) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 81f00b69b23..9bb2e405dca 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -219,6 +219,7 @@ class CloudGoogleConfig(AbstractConfig): sync_entities = True elif not self.enabled and self.is_local_sdk_active: self.async_disable_local_sdk() + sync_entities = True self._cur_entity_prefs = prefs.google_entity_configs self._cur_default_expose = prefs.google_default_expose diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 15a8d832403..2ed91b42ec6 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -356,9 +356,6 @@ class AbstractConfig(ABC): pprint.pformat(payload), ) - if not self.enabled: - return json_response(smart_home.turned_off_response(payload)) - if (agent_user_id := self.get_local_agent_user_id(webhook_id)) is None: # No agent user linked to this webhook, means that the user has somehow unregistered # removing webhook and stopping processing of this request. @@ -370,6 +367,11 @@ class AbstractConfig(ABC): webhook.async_unregister(self.hass, webhook_id) return None + if not self.enabled: + return json_response( + smart_home.api_disabled_response(payload, agent_user_id) + ) + result = await smart_home.async_handle_message( self.hass, self, diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 805c9100d9f..227b033bcaa 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -99,7 +99,7 @@ async def async_devices_sync(hass, data, payload): except Exception: # pylint: disable=broad-except _LOGGER.exception("Error serializing %s", entity.entity_id) - response = {"agentUserId": agent_user_id, "devices": devices} + response = create_sync_response(agent_user_id, devices) _LOGGER.debug("Syncing entities response: %s", response) @@ -300,9 +300,24 @@ async def async_devices_proxy_selected(hass, data: RequestData, payload): return {} -def turned_off_response(message): +def create_sync_response(agent_user_id: str, devices: list): + """Return an empty sync response.""" + return { + "agentUserId": agent_user_id, + "devices": devices, + } + + +def api_disabled_response(message, agent_user_id): """Return a device turned off response.""" + inputs: list = message.get("inputs") + + if inputs and inputs[0].get("intent") == "action.devices.SYNC": + payload = create_sync_response(agent_user_id, []) + else: + payload = {"errorCode": "deviceTurnedOff"} + return { "requestId": message.get("requestId"), - "payload": {"errorCode": "deviceTurnedOff"}, + "payload": payload, } diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index f56a1c86d4d..c125f5c252a 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -134,7 +134,16 @@ async def test_handler_google_actions(hass): assert device["roomHint"] == "living room" -async def test_handler_google_actions_disabled(hass, mock_cloud_fixture): +@pytest.mark.parametrize( + "intent,response_payload", + [ + ("action.devices.SYNC", {"agentUserId": "myUserName", "devices": []}), + ("action.devices.QUERY", {"errorCode": "deviceTurnedOff"}), + ], +) +async def test_handler_google_actions_disabled( + hass, mock_cloud_fixture, intent, response_payload +): """Test handler Google Actions when user has disabled it.""" mock_cloud_fixture._prefs[PREF_ENABLE_GOOGLE] = False @@ -142,13 +151,17 @@ async def test_handler_google_actions_disabled(hass, mock_cloud_fixture): assert await async_setup_component(hass, "cloud", {}) reqid = "5711642932632160983" - data = {"requestId": reqid, "inputs": [{"intent": "action.devices.SYNC"}]} + data = {"requestId": reqid, "inputs": [{"intent": intent}]} cloud = hass.data["cloud"] - resp = await cloud.client.async_google_message(data) + with patch( + "hass_nabucasa.Cloud._decode_claims", + return_value={"cognito:username": "myUserName"}, + ): + resp = await cloud.client.async_google_message(data) assert resp["requestId"] == reqid - assert resp["payload"]["errorCode"] == "deviceTurnedOff" + assert resp["payload"] == response_payload async def test_webhook_msg(hass, caplog): From a3ce80baed4ced985a39e7cfc7d5bf6cb52eb3cc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 23 Jun 2022 11:44:25 +0200 Subject: [PATCH 1734/3516] Improve nuki type hints (#73891) --- homeassistant/components/nuki/__init__.py | 11 +++++++---- homeassistant/components/nuki/lock.py | 17 +++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 6976c2dc682..e9cef7aa6cd 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -3,8 +3,9 @@ from datetime import timedelta import logging import async_timeout -from pynuki import NukiBridge +from pynuki import NukiBridge, NukiLock, NukiOpener from pynuki.bridge import InvalidCredentialsException +from pynuki.device import NukiDevice from requests.exceptions import RequestException from homeassistant import exceptions @@ -34,11 +35,11 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.LOCK] UPDATE_INTERVAL = timedelta(seconds=30) -def _get_bridge_devices(bridge): +def _get_bridge_devices(bridge: NukiBridge) -> tuple[list[NukiLock], list[NukiOpener]]: return bridge.locks, bridge.openers -def _update_devices(devices): +def _update_devices(devices: list[NukiDevice]) -> None: for device in devices: for level in (False, True): try: @@ -136,7 +137,9 @@ class NukiEntity(CoordinatorEntity): """ - def __init__(self, coordinator, nuki_device): + def __init__( + self, coordinator: DataUpdateCoordinator[None], nuki_device: NukiDevice + ) -> None: """Pass coordinator to CoordinatorEntity.""" super().__init__(coordinator) self._nuki_device = nuki_device diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 33d7465e12d..8b6c843f48a 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -1,4 +1,6 @@ """Nuki.io lock platform.""" +from __future__ import annotations + from abc import ABC, abstractmethod from typing import Any @@ -65,23 +67,22 @@ class NukiDeviceEntity(NukiEntity, LockEntity, ABC): _attr_supported_features = LockEntityFeature.OPEN @property - def name(self): + def name(self) -> str | None: """Return the name of the lock.""" return self._nuki_device.name @property - def unique_id(self) -> str: + def unique_id(self) -> str | None: """Return a unique ID.""" return self._nuki_device.nuki_id @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device specific state attributes.""" - data = { + return { ATTR_BATTERY_CRITICAL: self._nuki_device.battery_critical, ATTR_NUKI_ID: self._nuki_device.nuki_id, } - return data @property def available(self) -> bool: @@ -123,7 +124,7 @@ class NukiLockEntity(NukiDeviceEntity): """Open the door latch.""" self._nuki_device.unlatch() - def lock_n_go(self, unlatch): + def lock_n_go(self, unlatch: bool) -> None: """Lock and go. This will first unlock the door, then wait for 20 seconds (or another @@ -157,10 +158,10 @@ class NukiOpenerEntity(NukiDeviceEntity): """Buzz open the door.""" self._nuki_device.electric_strike_actuation() - def lock_n_go(self, unlatch): + def lock_n_go(self, unlatch: bool) -> None: """Stub service.""" - def set_continuous_mode(self, enable): + def set_continuous_mode(self, enable: bool) -> None: """Continuous Mode. This feature will cause the door to automatically open when anyone From 48bd7cf5e1679bda3fa0fb266d25b5903fbe806f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 23 Jun 2022 12:01:05 +0200 Subject: [PATCH 1735/3516] Add missing ToggleEntity type hints in fans (#73887) --- homeassistant/components/demo/fan.py | 4 ++-- homeassistant/components/fjaraskupan/fan.py | 2 +- homeassistant/components/insteon/fan.py | 2 +- homeassistant/components/lutron_caseta/fan.py | 2 +- homeassistant/components/mqtt/fan.py | 2 +- homeassistant/components/smartthings/fan.py | 2 +- homeassistant/components/smarty/fan.py | 2 +- homeassistant/components/wilight/fan.py | 2 +- homeassistant/components/xiaomi_miio/fan.py | 2 +- homeassistant/components/zha/fan.py | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index ef6875cb7c6..a36e7a35cff 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -208,7 +208,7 @@ class DemoPercentageFan(BaseDemoFan, FanEntity): self.set_percentage(percentage) - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn off the entity.""" self.set_percentage(0) @@ -278,7 +278,7 @@ class AsyncDemoPercentageFan(BaseDemoFan, FanEntity): await self.async_set_percentage(percentage) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the entity.""" await self.async_oscillate(False) await self.async_set_percentage(0) diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index ae5da3d189c..e372d540f54 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -135,7 +135,7 @@ class Fan(CoordinatorEntity[Coordinator], FanEntity): else: raise UnsupportedPreset(f"The preset {preset_mode} is unsupported") - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" await self._device.send_command(COMMAND_STOP_FAN) self.coordinator.async_set_updated_data(self._device.state) diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py index 8fe5fc9346e..c7512ba0278 100644 --- a/homeassistant/components/insteon/fan.py +++ b/homeassistant/components/insteon/fan.py @@ -70,7 +70,7 @@ class InsteonFanEntity(InsteonEntity, FanEntity): """Turn on the fan.""" await self.async_set_percentage(percentage or 67) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the fan.""" await self._insteon_device.async_fan_off() diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py index 46eee1bde6b..bf2328565d4 100644 --- a/homeassistant/components/lutron_caseta/fan.py +++ b/homeassistant/components/lutron_caseta/fan.py @@ -70,7 +70,7 @@ class LutronCasetaFan(LutronCasetaDeviceUpdatableEntity, FanEntity): await self.async_set_percentage(percentage) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the fan off.""" await self.async_set_percentage(0) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 4e7e58afb9f..f1a1dbed657 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -568,7 +568,7 @@ class MqttFan(MqttEntity, FanEntity): self._state = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the entity. This method is a coroutine. diff --git a/homeassistant/components/smartthings/fan.py b/homeassistant/components/smartthings/fan.py index abcd5d12f75..7278f350dc1 100644 --- a/homeassistant/components/smartthings/fan.py +++ b/homeassistant/components/smartthings/fan.py @@ -78,7 +78,7 @@ class SmartThingsFan(SmartThingsEntity, FanEntity): """Turn the fan on.""" await self._async_set_percentage(percentage) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the fan off.""" await self._device.switch_off(set_status=True) # State is set optimistically in the command above, therefore update diff --git a/homeassistant/components/smarty/fan.py b/homeassistant/components/smarty/fan.py index a51d6783245..ceb3b17e030 100644 --- a/homeassistant/components/smarty/fan.py +++ b/homeassistant/components/smarty/fan.py @@ -107,7 +107,7 @@ class SmartyFan(FanEntity): _LOGGER.debug("Turning on fan. percentage is %s", percentage) self.set_percentage(percentage or DEFAULT_ON_PERCENTAGE) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn off the fan.""" _LOGGER.debug("Turning off fan") if not self._smarty.turn_off(): diff --git a/homeassistant/components/wilight/fan.py b/homeassistant/components/wilight/fan.py index 7a60fa8ab62..a93e0eb9447 100644 --- a/homeassistant/components/wilight/fan.py +++ b/homeassistant/components/wilight/fan.py @@ -130,6 +130,6 @@ class WiLightFan(WiLightDevice, FanEntity): wl_direction = WL_DIRECTION_FORWARD await self._client.set_fan_direction(self._index, wl_direction) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the fan off.""" await self._client.set_fan_direction(self._index, WL_DIRECTION_OFF) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 31908ba373f..b70ddc945ea 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -334,7 +334,7 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): self._state = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" result = await self._try_command( "Turning the miio device off failed.", self._device.off diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 298f9e47296..1873c906a6f 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -99,7 +99,7 @@ class BaseFan(FanEntity): percentage = DEFAULT_ON_PERCENTAGE await self.async_set_percentage(percentage) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" await self.async_set_percentage(0) From b4cc9367cf9db2209b0e657927c2f2579d79dd42 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Thu, 23 Jun 2022 12:18:01 +0200 Subject: [PATCH 1736/3516] Bump bimmer_connected to 0.9.5 (#73888) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index cd6daa83705..40af0b9e210 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.9.4"], + "requirements": ["bimmer_connected==0.9.5"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 962a246c2d2..10e4ce115da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -393,7 +393,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.4 +bimmer_connected==0.9.5 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a48872ddee..00f04afa482 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -308,7 +308,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.4 +bimmer_connected==0.9.5 # homeassistant.components.blebox blebox_uniapi==1.3.3 From 0787ee134504f8563d8600430632ee900d8e843f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 23 Jun 2022 12:42:05 +0200 Subject: [PATCH 1737/3516] Use attributes in comfoconnect fan (#73892) --- homeassistant/components/comfoconnect/fan.py | 26 ++++---------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index f52c065a547..dd2c9632d7c 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -53,12 +53,16 @@ def setup_platform( class ComfoConnectFan(FanEntity): """Representation of the ComfoConnect fan platform.""" + _attr_icon = "mdi:air-conditioner" + _attr_should_poll = False _attr_supported_features = FanEntityFeature.SET_SPEED current_speed = None def __init__(self, ccb: ComfoConnectBridge) -> None: """Initialize the ComfoConnect fan.""" self._ccb = ccb + self._attr_name = ccb.name + self._attr_unique_id = ccb.unique_id async def async_added_to_hass(self) -> None: """Register for sensor updates.""" @@ -74,7 +78,7 @@ class ComfoConnectFan(FanEntity): self._ccb.comfoconnect.register_sensor, SENSOR_FAN_SPEED_MODE ) - def _handle_update(self, value): + def _handle_update(self, value: float) -> None: """Handle update callbacks.""" _LOGGER.debug( "Handle update for fan speed (%d): %s", SENSOR_FAN_SPEED_MODE, value @@ -82,26 +86,6 @@ class ComfoConnectFan(FanEntity): self.current_speed = value self.schedule_update_ha_state() - @property - def should_poll(self) -> bool: - """Do not poll.""" - return False - - @property - def unique_id(self): - """Return a unique_id for this entity.""" - return self._ccb.unique_id - - @property - def name(self): - """Return the name of the fan.""" - return self._ccb.name - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:air-conditioner" - @property def percentage(self) -> int | None: """Return the current speed percentage.""" From 95eeb8eff3e80a7e3d94f7ffc28ca93ffcd52dda Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 23 Jun 2022 13:58:24 +0200 Subject: [PATCH 1738/3516] Update Builder & Wheels + support yellow (#73896) --- .github/workflows/builder.yml | 5 +++-- .github/workflows/wheels.yml | 4 ++-- machine/yellow | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 machine/yellow diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 62cbee9321c..6eea7cea953 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -135,7 +135,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.06.1 + uses: home-assistant/builder@2022.06.2 with: args: | $BUILD_ARGS \ @@ -171,6 +171,7 @@ jobs: - raspberrypi4 - raspberrypi4-64 - tinker + - yellow steps: - name: Checkout the repository uses: actions/checkout@v3.0.2 @@ -200,7 +201,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.06.1 + uses: home-assistant/builder@2022.06.2 with: args: | $BUILD_ARGS \ diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 27307388546..e65b0e7091a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -88,7 +88,7 @@ jobs: name: requirements_diff - name: Build wheels - uses: home-assistant/wheels@2022.06.6 + uses: home-assistant/wheels@2022.06.7 with: abi: cp310 tag: musllinux_1_2 @@ -147,7 +147,7 @@ jobs: fi - name: Build wheels - uses: home-assistant/wheels@2022.06.6 + uses: home-assistant/wheels@2022.06.7 with: abi: cp310 tag: musllinux_1_2 diff --git a/machine/yellow b/machine/yellow new file mode 100644 index 00000000000..d8e7421f9b1 --- /dev/null +++ b/machine/yellow @@ -0,0 +1,14 @@ +ARG BUILD_VERSION +FROM homeassistant/aarch64-homeassistant:$BUILD_VERSION + +RUN apk --no-cache add \ + raspberrypi \ + raspberrypi-libs \ + usbutils + +## +# Set symlinks for raspberry pi binaries. +RUN ln -sv /opt/vc/bin/raspistill /usr/local/bin/raspistill \ + && ln -sv /opt/vc/bin/raspivid /usr/local/bin/raspivid \ + && ln -sv /opt/vc/bin/raspividyuv /usr/local/bin/raspividyuv \ + && ln -sv /opt/vc/bin/raspiyuv /usr/local/bin/raspiyuv From 2742bf86e3724c64c3eefa5c3a5b0a13be840f30 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Jun 2022 07:29:09 -0500 Subject: [PATCH 1739/3516] Switch mqtt to use json helper (#73871) * Switch mqtt to use json helper * whitespace --- homeassistant/components/mqtt/cover.py | 4 ++-- homeassistant/components/mqtt/discovery.py | 4 ++-- .../components/mqtt/light/schema_json.py | 8 ++++---- homeassistant/components/mqtt/mixins.py | 4 ++-- homeassistant/components/mqtt/siren.py | 8 ++++---- homeassistant/components/mqtt/trigger.py | 4 ++-- .../components/mqtt/vacuum/schema_legacy.py | 5 ++--- .../components/mqtt/vacuum/schema_state.py | 7 +++---- tests/components/mqtt/test_light_json.py | 16 ++++++++-------- tests/components/mqtt/test_siren.py | 10 +++++----- 10 files changed, 34 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 0901a4f63a6..754fcb7ec44 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -2,7 +2,6 @@ from __future__ import annotations import functools -from json import JSONDecodeError, loads as json_loads import logging import voluptuous as vol @@ -29,6 +28,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_loads from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription @@ -418,7 +418,7 @@ class MqttCover(MqttEntity, CoverEntity): try: payload = json_loads(payload) - except JSONDecodeError: + except JSON_DECODE_EXCEPTIONS: pass if isinstance(payload, dict): diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 04dd2d7917f..5b39e8fa1b5 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio from collections import deque import functools -import json import logging import re import time @@ -17,6 +16,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) +from homeassistant.helpers.json import json_loads from homeassistant.loader import async_get_mqtt from .. import mqtt @@ -117,7 +117,7 @@ async def async_start( # noqa: C901 if payload: try: - payload = json.loads(payload) + payload = json_loads(payload) except ValueError: _LOGGER.warning("Unable to parse JSON %s: '%s'", object_id, payload) return diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index be49f1ad2e3..716366cbe22 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -1,6 +1,5 @@ """Support for MQTT JSON lights.""" from contextlib import suppress -import json import logging import voluptuous as vol @@ -46,6 +45,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.json import json_dumps, json_loads from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util @@ -317,7 +317,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): @log_messages(self.hass, self.entity_id) def state_received(msg): """Handle new MQTT messages.""" - values = json.loads(msg.payload) + values = json_loads(msg.payload) if values["state"] == "ON": self._state = True @@ -644,7 +644,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): await self.async_publish( self._topic[CONF_COMMAND_TOPIC], - json.dumps(message), + json_dumps(message), self._config[CONF_QOS], self._config[CONF_RETAIN], self._config[CONF_ENCODING], @@ -669,7 +669,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): await self.async_publish( self._topic[CONF_COMMAND_TOPIC], - json.dumps(message), + json_dumps(message), self._config[CONF_QOS], self._config[CONF_RETAIN], self._config[CONF_ENCODING], diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index dcf387eb360..10fe6cb6cc5 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -4,7 +4,6 @@ from __future__ import annotations from abc import abstractmethod import asyncio from collections.abc import Callable -import json import logging from typing import Any, Protocol, cast, final @@ -47,6 +46,7 @@ from homeassistant.helpers.entity import ( async_generate_entity_id, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.json import json_loads from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import debug_info, subscription @@ -393,7 +393,7 @@ class MqttAttributes(Entity): def attributes_message_received(msg: ReceiveMessage) -> None: try: payload = attr_tpl(msg.payload) - json_dict = json.loads(payload) if isinstance(payload, str) else None + json_dict = json_loads(payload) if isinstance(payload, str) else None if isinstance(json_dict, dict): filtered_dict = { k: v diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index e7b91274f4f..a6cff4cf91d 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -3,7 +3,6 @@ from __future__ import annotations import copy import functools -import json import logging from typing import Any @@ -32,6 +31,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_dumps, json_loads from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription @@ -253,13 +253,13 @@ class MqttSiren(MqttEntity, SirenEntity): json_payload = {STATE: payload} else: try: - json_payload = json.loads(payload) + json_payload = json_loads(payload) _LOGGER.debug( "JSON payload detected after processing payload '%s' on topic %s", json_payload, msg.topic, ) - except json.decoder.JSONDecodeError: + except JSON_DECODE_EXCEPTIONS: _LOGGER.warning( "No valid (JSON) payload detected after processing payload '%s' on topic %s", json_payload, @@ -344,7 +344,7 @@ class MqttSiren(MqttEntity, SirenEntity): payload = ( self._command_templates[template](value, template_variables) if self._command_templates[template] - else json.dumps(template_variables) + else json_dumps(template_variables) ) if payload and payload not in PAYLOAD_NONE: await self.async_publish( diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index 7d1f93d30eb..aca4cf0a480 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -1,6 +1,5 @@ """Offer MQTT listening automation rules.""" from contextlib import suppress -import json import logging import voluptuous as vol @@ -12,6 +11,7 @@ from homeassistant.components.automation import ( from homeassistant.const import CONF_PAYLOAD, CONF_PLATFORM, CONF_VALUE_TEMPLATE from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers.json import json_loads from homeassistant.helpers.typing import ConfigType from .. import mqtt @@ -89,7 +89,7 @@ async def async_attach_trigger( } with suppress(ValueError): - data["payload_json"] = json.loads(mqttmsg.payload) + data["payload_json"] = json_loads(mqttmsg.payload) hass.async_run_hass_job(job, {"trigger": data}) diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index f25131c43b7..6b957aded5c 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -1,6 +1,4 @@ """Support for Legacy MQTT vacuum.""" -import json - import voluptuous as vol from homeassistant.components.vacuum import ( @@ -14,6 +12,7 @@ from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.icon import icon_for_battery_level +from homeassistant.helpers.json import json_dumps from .. import subscription from ..config import MQTT_BASE_SCHEMA @@ -511,7 +510,7 @@ class MqttVacuum(MqttEntity, VacuumEntity): if params: message = {"command": command} message.update(params) - message = json.dumps(message) + message = json_dumps(message) else: message = command await self.async_publish( diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 3d670780994..af6c8d289d8 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -1,6 +1,4 @@ """Support for a State MQTT vacuum.""" -import json - import voluptuous as vol from homeassistant.components.vacuum import ( @@ -21,6 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.json import json_dumps, json_loads from .. import subscription from ..config import MQTT_BASE_SCHEMA @@ -203,7 +202,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): @log_messages(self.hass, self.entity_id) def state_message_received(msg): """Handle state MQTT message.""" - payload = json.loads(msg.payload) + payload = json_loads(msg.payload) if STATE in payload and ( payload[STATE] in POSSIBLE_STATES or payload[STATE] is None ): @@ -347,7 +346,7 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity): if params: message = {"command": command} message.update(params) - message = json.dumps(message) + message = json_dumps(message) else: message = command await self.async_publish( diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 64733b4f0f7..b930de9b6c3 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -688,7 +688,7 @@ async def test_sending_mqtt_commands_and_optimistic( await common.async_turn_on(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", '{"state": "ON"}', 2, False + "test_light_rgb/set", '{"state":"ON"}', 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") @@ -709,7 +709,7 @@ async def test_sending_mqtt_commands_and_optimistic( await common.async_turn_off(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", '{"state": "OFF"}', 2, False + "test_light_rgb/set", '{"state":"OFF"}', 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") @@ -838,7 +838,7 @@ async def test_sending_mqtt_commands_and_optimistic2( # Turn the light on await common.async_turn_on(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", '{"state": "ON"}', 2, False + "test_light_rgb/set", '{"state":"ON"}', 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") @@ -848,7 +848,7 @@ async def test_sending_mqtt_commands_and_optimistic2( await common.async_turn_on(hass, "light.test", color_temp=90) mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", - JsonValidator('{"state": "ON", "color_temp": 90}'), + JsonValidator('{"state":"ON","color_temp":90}'), 2, False, ) @@ -859,7 +859,7 @@ async def test_sending_mqtt_commands_and_optimistic2( # Turn the light off await common.async_turn_off(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", '{"state": "OFF"}', 2, False + "test_light_rgb/set", '{"state":"OFF"}', 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") @@ -2004,7 +2004,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): light.DOMAIN, DEFAULT_CONFIG, light.SERVICE_TURN_ON, - command_payload='{"state": "ON"}', + command_payload='{"state":"ON"}', state_payload='{"state":"ON"}', ) @@ -2038,7 +2038,7 @@ async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): light.SERVICE_TURN_ON, "command_topic", None, - '{"state": "ON"}', + '{"state":"ON"}', None, None, None, @@ -2047,7 +2047,7 @@ async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): light.SERVICE_TURN_OFF, "command_topic", None, - '{"state": "OFF"}', + '{"state":"OFF"}', None, None, None, diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index c3916acb34b..6da9682c1c7 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -140,7 +140,7 @@ async def test_sending_mqtt_commands_and_optimistic( await async_turn_on(hass, entity_id="siren.test") mqtt_mock.async_publish.assert_called_once_with( - "command-topic", '{"state": "beer on"}', 2, False + "command-topic", '{"state":"beer on"}', 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("siren.test") @@ -149,7 +149,7 @@ async def test_sending_mqtt_commands_and_optimistic( await async_turn_off(hass, entity_id="siren.test") mqtt_mock.async_publish.assert_called_once_with( - "command-topic", '{"state": "beer off"}', 2, False + "command-topic", '{"state":"beer off"}', 2, False ) state = hass.states.get("siren.test") assert state.state == STATE_OFF @@ -870,7 +870,7 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): siren.DOMAIN, DEFAULT_CONFIG, siren.SERVICE_TURN_ON, - command_payload='{"state": "ON"}', + command_payload='{"state":"ON"}', ) @@ -881,14 +881,14 @@ async def test_entity_debug_info_message(hass, mqtt_mock_entry_no_yaml_config): siren.SERVICE_TURN_ON, "command_topic", None, - '{"state": "ON"}', + '{"state":"ON"}', None, ), ( siren.SERVICE_TURN_OFF, "command_topic", None, - '{"state": "OFF"}', + '{"state":"OFF"}', None, ), ], From 8015bb98a909c95d044579a77e6a22abf108e6f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Jun 2022 07:32:26 -0500 Subject: [PATCH 1740/3516] Switch recorder and templates to use json helper (#73876) - These were using orjson directly, its a bit cleaner to use the helper so everything is easier to adjust in the future if we need to change anything about the loading --- .../components/recorder/db_schema.py | 32 +++++++++++-------- homeassistant/components/recorder/models.py | 4 +-- homeassistant/helpers/template.py | 8 ++--- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index f300cc0bae7..36487353e25 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -8,7 +8,6 @@ from typing import Any, cast import ciso8601 from fnvhash import fnv1a_32 -import orjson from sqlalchemy import ( JSON, BigInteger, @@ -39,7 +38,12 @@ from homeassistant.const import ( MAX_LENGTH_STATE_STATE, ) from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id -from homeassistant.helpers.json import JSON_DUMP, json_bytes +from homeassistant.helpers.json import ( + JSON_DECODE_EXCEPTIONS, + JSON_DUMP, + json_bytes, + json_loads, +) import homeassistant.util.dt as dt_util from .const import ALL_DOMAIN_EXCLUDE_ATTRS @@ -188,15 +192,15 @@ class Events(Base): # type: ignore[misc,valid-type] try: return Event( self.event_type, - orjson.loads(self.event_data) if self.event_data else {}, + json_loads(self.event_data) if self.event_data else {}, EventOrigin(self.origin) if self.origin else EVENT_ORIGIN_ORDER[self.origin_idx], process_timestamp(self.time_fired), context=context, ) - except ValueError: - # When orjson.loads fails + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails _LOGGER.exception("Error converting to event: %s", self) return None @@ -243,8 +247,8 @@ class EventData(Base): # type: ignore[misc,valid-type] def to_native(self) -> dict[str, Any]: """Convert to an HA state object.""" try: - return cast(dict[str, Any], orjson.loads(self.shared_data)) - except ValueError: + return cast(dict[str, Any], json_loads(self.shared_data)) + except JSON_DECODE_EXCEPTIONS: _LOGGER.exception("Error converting row to event data: %s", self) return {} @@ -330,9 +334,9 @@ class States(Base): # type: ignore[misc,valid-type] parent_id=self.context_parent_id, ) try: - attrs = orjson.loads(self.attributes) if self.attributes else {} - except ValueError: - # When orjson.loads fails + attrs = json_loads(self.attributes) if self.attributes else {} + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails _LOGGER.exception("Error converting row to state: %s", self) return None if self.last_changed is None or self.last_changed == self.last_updated: @@ -402,15 +406,15 @@ class StateAttributes(Base): # type: ignore[misc,valid-type] @staticmethod def hash_shared_attrs_bytes(shared_attrs_bytes: bytes) -> int: - """Return the hash of orjson encoded shared attributes.""" + """Return the hash of json encoded shared attributes.""" return cast(int, fnv1a_32(shared_attrs_bytes)) def to_native(self) -> dict[str, Any]: """Convert to an HA state object.""" try: - return cast(dict[str, Any], orjson.loads(self.shared_attrs)) - except ValueError: - # When orjson.loads fails + return cast(dict[str, Any], json_loads(self.shared_attrs)) + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails _LOGGER.exception("Error converting row to state attributes: %s", self) return {} diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 64fb44289b0..ff53d9be3d1 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -5,7 +5,6 @@ from datetime import datetime import logging from typing import Any, TypedDict, overload -import orjson from sqlalchemy.engine.row import Row from homeassistant.components.websocket_api.const import ( @@ -15,6 +14,7 @@ from homeassistant.components.websocket_api.const import ( COMPRESSED_STATE_STATE, ) from homeassistant.core import Context, State +from homeassistant.helpers.json import json_loads import homeassistant.util.dt as dt_util # pylint: disable=invalid-name @@ -253,7 +253,7 @@ def decode_attributes_from_row( if not source or source == EMPTY_JSON_OBJECT: return {} try: - attr_cache[source] = attributes = orjson.loads(source) + attr_cache[source] = attributes = json_loads(source) except ValueError: _LOGGER.exception("Error converting row to state attributes: %s", source) attr_cache[source] = attributes = {} diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 02f95254c86..ac5b4c8119f 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -27,7 +27,6 @@ import jinja2 from jinja2 import pass_context, pass_environment from jinja2.sandbox import ImmutableSandboxedEnvironment from jinja2.utils import Namespace -import orjson import voluptuous as vol from homeassistant.const import ( @@ -58,6 +57,7 @@ from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.thread import ThreadWithException from . import area_registry, device_registry, entity_registry, location as loc_helper +from .json import JSON_DECODE_EXCEPTIONS, json_loads from .typing import TemplateVarsType # mypy: allow-untyped-defs, no-check-untyped-defs @@ -566,8 +566,8 @@ class Template: variables = dict(variables or {}) variables["value"] = value - with suppress(ValueError, TypeError): - variables["value_json"] = orjson.loads(value) + with suppress(*JSON_DECODE_EXCEPTIONS): + variables["value_json"] = json_loads(value) try: return _render_with_context( @@ -1744,7 +1744,7 @@ def ordinal(value): def from_json(value): """Convert a JSON string to an object.""" - return orjson.loads(value) + return json_loads(value) def to_json(value, ensure_ascii=True): From 95abfb5748ce24788146c6efdb9098ddfe060211 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Thu, 23 Jun 2022 23:37:28 +1000 Subject: [PATCH 1741/3516] Powerview polling tdbu (#73899) --- .../components/hunterdouglas_powerview/cover.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 797dded4f76..fe26a100569 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -464,6 +464,15 @@ class PowerViewShadeTDBUTop(PowerViewShadeTDBU): ATTR_POSKIND2: POS_KIND_SECONDARY, } + @property + def should_poll(self) -> bool: + """Certain shades create multiple entities. + + Do not poll shade multiple times. One shade will return data + for both and multiple polling will cause timeouts. + """ + return False + @property def is_closed(self): """Return if the cover is closed.""" From ff7d840a6c556436e01f7d86a922e13275eb1713 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 23 Jun 2022 10:13:36 -0400 Subject: [PATCH 1742/3516] Bump zwave-js-server-python to 0.39.0 (#73904) --- 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 f2670be7b80..d1097a6cd65 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.38.0"], + "requirements": ["zwave-js-server-python==0.39.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 10e4ce115da..2ffb5aad412 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2522,7 +2522,7 @@ zigpy==0.46.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.38.0 +zwave-js-server-python==0.39.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00f04afa482..cce7c56108d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1671,7 +1671,7 @@ zigpy-znp==0.7.0 zigpy==0.46.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.38.0 +zwave-js-server-python==0.39.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 From 3c82c718cb37fca96002558019e3a0fba7ccde66 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 23 Jun 2022 16:34:40 +0200 Subject: [PATCH 1743/3516] Improve typing in fans and locks (#73901) --- homeassistant/components/august/lock.py | 2 +- homeassistant/components/demo/fan.py | 4 ++-- homeassistant/components/demo/lock.py | 2 +- homeassistant/components/kiwi/lock.py | 4 ++-- homeassistant/components/mqtt/fan.py | 2 +- homeassistant/components/mqtt/lock.py | 4 ++-- homeassistant/components/sesame/lock.py | 2 +- homeassistant/components/smartthings/lock.py | 2 +- homeassistant/components/starline/lock.py | 6 +++--- homeassistant/components/template/fan.py | 2 +- homeassistant/components/template/lock.py | 6 +++--- homeassistant/components/verisure/lock.py | 2 +- homeassistant/components/vesync/fan.py | 2 +- homeassistant/components/xiaomi_aqara/lock.py | 2 +- homeassistant/components/xiaomi_miio/fan.py | 2 +- homeassistant/components/zha/fan.py | 2 +- homeassistant/components/zha/lock.py | 5 +++-- homeassistant/components/zwave_js/fan.py | 2 +- 18 files changed, 27 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 9269dc52c6a..d77a61a0659 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -127,7 +127,7 @@ class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity): "keypad_battery_level" ] = self._detail.keypad.battery_level - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore ATTR_CHANGED_BY on startup since it is likely no longer in the activity log.""" await super().async_added_to_hass() diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index a36e7a35cff..02623b7b644 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -124,7 +124,7 @@ class BaseDemoFan(FanEntity): self._direction = "forward" @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique id.""" return self._unique_id @@ -134,7 +134,7 @@ class BaseDemoFan(FanEntity): return self._name @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed for a demo fan.""" return False diff --git a/homeassistant/components/demo/lock.py b/homeassistant/components/demo/lock.py index c59dfad2fab..d21d89f238b 100644 --- a/homeassistant/components/demo/lock.py +++ b/homeassistant/components/demo/lock.py @@ -111,7 +111,7 @@ class DemoLock(LockEntity): self.async_write_ha_state() @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" if self._openable: return LockEntityFeature.OPEN diff --git a/homeassistant/components/kiwi/lock.py b/homeassistant/components/kiwi/lock.py index bfd6b430c9f..44dc2bb2521 100644 --- a/homeassistant/components/kiwi/lock.py +++ b/homeassistant/components/kiwi/lock.py @@ -83,7 +83,7 @@ class KiwiLock(LockEntity): } @property - def name(self): + def name(self) -> str | None: """Return the name of the lock.""" name = self._sensor.get("name") specifier = self._sensor["address"].get("specifier") @@ -95,7 +95,7 @@ class KiwiLock(LockEntity): return self._state == STATE_LOCKED @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device specific state attributes.""" return self._device_attrs diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index f1a1dbed657..721fa93f244 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -502,7 +502,7 @@ class MqttFan(MqttEntity, FanEntity): await subscription.async_subscribe_topics(self.hass, self._sub_state) @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 8cf65485a09..1d6a40c2331 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -189,12 +189,12 @@ class MqttLock(MqttEntity, LockEntity): return self._state @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return LockEntityFeature.OPEN if CONF_PAYLOAD_OPEN in self._config else 0 diff --git a/homeassistant/components/sesame/lock.py b/homeassistant/components/sesame/lock.py index b9a230fed63..c539e7507eb 100644 --- a/homeassistant/components/sesame/lock.py +++ b/homeassistant/components/sesame/lock.py @@ -82,7 +82,7 @@ class SesameDevice(LockEntity): self._responsive = status["responsive"] @property - def extra_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return { ATTR_DEVICE_ID: self._device_id, diff --git a/homeassistant/components/smartthings/lock.py b/homeassistant/components/smartthings/lock.py index 81866010667..c0fbc32fa19 100644 --- a/homeassistant/components/smartthings/lock.py +++ b/homeassistant/components/smartthings/lock.py @@ -67,7 +67,7 @@ class SmartThingsLock(SmartThingsEntity, LockEntity): return self._device.status.lock == ST_STATE_LOCKED @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" state_attrs = {} status = self._device.status.attributes[Attribute.lock] diff --git a/homeassistant/components/starline/lock.py b/homeassistant/components/starline/lock.py index 3a5c45a2ed1..4fb8457a779 100644 --- a/homeassistant/components/starline/lock.py +++ b/homeassistant/components/starline/lock.py @@ -35,12 +35,12 @@ class StarlineLock(StarlineEntity, LockEntity): super().__init__(account, device, "lock", "Security") @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return super().available and self._device.online @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, bool]: """Return the state attributes of the lock. Possible dictionary keys: @@ -61,7 +61,7 @@ class StarlineLock(StarlineEntity, LockEntity): return self._device.alarm_state @property - def icon(self): + def icon(self) -> str: """Icon to use in the frontend, if any.""" return ( "mdi:shield-check-outline" if self.is_locked else "mdi:shield-alert-outline" diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 67cbeb07170..b60e7f53364 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -354,7 +354,7 @@ class TemplateFan(TemplateEntity, FanEntity): ) self._state = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.add_template_attribute("_state", self._template, None, self._update_state) if self._preset_mode_template is not None: diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 3f83e628f71..da8be80d8a4 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -92,9 +92,9 @@ class TemplateLock(TemplateEntity, LockEntity): self._optimistic = config.get(CONF_OPTIMISTIC) @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" - return self._optimistic + return bool(self._optimistic) @property def is_locked(self) -> bool: @@ -133,7 +133,7 @@ class TemplateLock(TemplateEntity, LockEntity): self._state = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.add_template_attribute( "_state", self._state_template, None, self._update_state diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index f96b99e2a8c..8074cf28f32 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -119,7 +119,7 @@ class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEnt ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, str]: """Return the state attributes.""" return {"method": self.changed_method} diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 9932790fa96..f89224aaba8 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -130,7 +130,7 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): return self.smartfan.uuid @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the fan.""" attr = {} diff --git a/homeassistant/components/xiaomi_aqara/lock.py b/homeassistant/components/xiaomi_aqara/lock.py index 1885c1aef85..fea729b2b47 100644 --- a/homeassistant/components/xiaomi_aqara/lock.py +++ b/homeassistant/components/xiaomi_aqara/lock.py @@ -58,7 +58,7 @@ class XiaomiAqaraLock(LockEntity, XiaomiDevice): return self._changed_by @property - def extra_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict[str, int]: """Return the state attributes.""" attributes = {ATTR_VERIFIED_WRONG_TIMES: self._verified_wrong_times} return attributes diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index b70ddc945ea..ac85955b347 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -304,7 +304,7 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): return None @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the device.""" return self._state_attrs diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 1873c906a6f..3b9793c5137 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -134,7 +134,7 @@ class ZhaFan(BaseFan, ZhaEntity): super().__init__(unique_id, zha_device, channels, **kwargs) self._fan_channel = self.cluster_channels.get(CHANNEL_FAN) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() self.async_accept_signal( diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index 449fd1089eb..6615141f4d1 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -11,6 +11,7 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import StateType from .core import discovery from .core.const import ( @@ -96,7 +97,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity): super().__init__(unique_id, zha_device, channels, **kwargs) self._doorlock_channel = self.cluster_channels.get(CHANNEL_DOORLOCK) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() self.async_accept_signal( @@ -116,7 +117,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity): return self._state == STATE_LOCKED @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, StateType]: """Return state attributes.""" return self.state_attributes diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index ae9df47b420..27f73353ca8 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -404,7 +404,7 @@ class ZwaveThermostatFan(ZWaveBaseEntity, FanEntity): return cast(str, self._fan_state.metadata.states[str(value)]) @property - def extra_state_attributes(self) -> dict[str, str] | None: + def extra_state_attributes(self) -> dict[str, str]: """Return the optional state attributes.""" attrs = {} From e874ba2a42d95b9312db9680023aa615f20bf542 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 23 Jun 2022 16:48:49 +0200 Subject: [PATCH 1744/3516] Improve CoverEntity typing (#73903) --- homeassistant/components/cover/__init__.py | 45 ++++++++++++---------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index aecca5a4029..b66398b3491 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -1,13 +1,15 @@ """Support for Cover devices.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta from enum import IntEnum import functools as ft import logging -from typing import Any, final +from typing import Any, TypeVar, final +from typing_extensions import ParamSpec import voluptuous as vol from homeassistant.backports.enum import StrEnum @@ -38,8 +40,6 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) DOMAIN = "cover" @@ -47,6 +47,9 @@ SCAN_INTERVAL = timedelta(seconds=15) ENTITY_ID_FORMAT = DOMAIN + ".{}" +_P = ParamSpec("_P") +_R = TypeVar("_R") + class CoverDeviceClass(StrEnum): """Device class for cover.""" @@ -112,7 +115,7 @@ ATTR_TILT_POSITION = "tilt_position" @bind_hass -def is_closed(hass, entity_id): +def is_closed(hass: HomeAssistant, entity_id: str) -> bool: """Return if the cover is closed based on the statemachine.""" return hass.states.is_state(entity_id, STATE_CLOSED) @@ -273,7 +276,7 @@ class CoverEntity(Entity): @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" data = {} @@ -327,7 +330,7 @@ class CoverEntity(Entity): """Open the cover.""" raise NotImplementedError() - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self.hass.async_add_executor_job(ft.partial(self.open_cover, **kwargs)) @@ -335,7 +338,7 @@ class CoverEntity(Entity): """Close cover.""" raise NotImplementedError() - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" await self.hass.async_add_executor_job(ft.partial(self.close_cover, **kwargs)) @@ -349,7 +352,7 @@ class CoverEntity(Entity): function = self._get_toggle_function(fns) function(**kwargs) - async def async_toggle(self, **kwargs): + async def async_toggle(self, **kwargs: Any) -> None: """Toggle the entity.""" fns = { "open": self.async_open_cover, @@ -359,26 +362,26 @@ class CoverEntity(Entity): function = self._get_toggle_function(fns) await function(**kwargs) - def set_cover_position(self, **kwargs): + def set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" await self.hass.async_add_executor_job( ft.partial(self.set_cover_position, **kwargs) ) - def stop_cover(self, **kwargs): + def stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self.hass.async_add_executor_job(ft.partial(self.stop_cover, **kwargs)) def open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" - async def async_open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" await self.hass.async_add_executor_job( ft.partial(self.open_cover_tilt, **kwargs) @@ -387,25 +390,25 @@ class CoverEntity(Entity): def close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" - async def async_close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" await self.hass.async_add_executor_job( ft.partial(self.close_cover_tilt, **kwargs) ) - def set_cover_tilt_position(self, **kwargs): + def set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" - async def async_set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" await self.hass.async_add_executor_job( ft.partial(self.set_cover_tilt_position, **kwargs) ) - def stop_cover_tilt(self, **kwargs): + def stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the cover.""" - async def async_stop_cover_tilt(self, **kwargs): + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the cover.""" await self.hass.async_add_executor_job( ft.partial(self.stop_cover_tilt, **kwargs) @@ -418,14 +421,16 @@ class CoverEntity(Entity): else: self.close_cover_tilt(**kwargs) - async def async_toggle_tilt(self, **kwargs): + async def async_toggle_tilt(self, **kwargs: Any) -> None: """Toggle the entity.""" if self.current_cover_tilt_position == 0: await self.async_open_cover_tilt(**kwargs) else: await self.async_close_cover_tilt(**kwargs) - def _get_toggle_function(self, fns): + def _get_toggle_function( + self, fns: dict[str, Callable[_P, _R]] + ) -> Callable[_P, _R]: if CoverEntityFeature.STOP | self.supported_features and ( self.is_closing or self.is_opening ): From 17d839df79e4c0f20a964170baa106c999ea351e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 23 Jun 2022 19:50:46 +0200 Subject: [PATCH 1745/3516] Set codeowner of weather to @home-assistant/core (#73915) --- CODEOWNERS | 4 ++-- homeassistant/components/weather/manifest.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b8e6a4ab7a7..9de118552aa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1162,8 +1162,8 @@ build.json @home-assistant/supervisor /tests/components/watttime/ @bachya /homeassistant/components/waze_travel_time/ @eifinger /tests/components/waze_travel_time/ @eifinger -/homeassistant/components/weather/ @fabaff -/tests/components/weather/ @fabaff +/homeassistant/components/weather/ @home-assistant/core +/tests/components/weather/ @home-assistant/core /homeassistant/components/webhook/ @home-assistant/core /tests/components/webhook/ @home-assistant/core /homeassistant/components/webostv/ @bendavid @thecode diff --git a/homeassistant/components/weather/manifest.json b/homeassistant/components/weather/manifest.json index c77e8408c83..cbf04af989d 100644 --- a/homeassistant/components/weather/manifest.json +++ b/homeassistant/components/weather/manifest.json @@ -2,6 +2,6 @@ "domain": "weather", "name": "Weather", "documentation": "https://www.home-assistant.io/integrations/weather", - "codeowners": ["@fabaff"], + "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } From 3d59088a62d6affb82d42ac6bb494a2919ac9663 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Jun 2022 13:13:42 -0500 Subject: [PATCH 1746/3516] Bump sqlalchemy to 1.4.38 (#73916) Changes: https://docs.sqlalchemy.org/en/14/changelog/changelog_14.html#change-1.4.38 --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 38897c42e1a..3d22781906a 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.37", "fnvhash==0.1.0", "lru-dict==1.1.7"], + "requirements": ["sqlalchemy==1.4.38", "fnvhash==0.1.0", "lru-dict==1.1.7"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 4562b945008..0e6c68071cc 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,7 +2,7 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.4.37"], + "requirements": ["sqlalchemy==1.4.38"], "codeowners": ["@dgomes", "@gjohansson-ST"], "config_flow": true, "iot_class": "local_polling" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e9599bbe3a9..c9d011f3471 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ pyudev==0.22.0 pyyaml==6.0 requests==2.28.0 scapy==2.4.5 -sqlalchemy==1.4.37 +sqlalchemy==1.4.38 typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index 2ffb5aad412..8935161ae1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2217,7 +2217,7 @@ spotipy==2.20.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.37 +sqlalchemy==1.4.38 # homeassistant.components.srp_energy srpenergy==1.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cce7c56108d..d4f43b284c0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1471,7 +1471,7 @@ spotipy==2.20.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.37 +sqlalchemy==1.4.38 # homeassistant.components.srp_energy srpenergy==1.3.6 From edb386c73660ffc908c951a3d3237cb40cc8c6c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Jun 2022 13:19:13 -0500 Subject: [PATCH 1747/3516] Switch frontend to use json helper (#73874) --- homeassistant/components/frontend/__init__.py | 4 ++-- homeassistant/helpers/json.py | 9 +++++++++ tests/helpers/test_json.py | 15 ++++++++++++++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index b3907143eb9..188ecb8ff98 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Iterator from functools import lru_cache -import json import logging import os import pathlib @@ -22,6 +21,7 @@ from homeassistant.const import CONF_MODE, CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import service import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.json import json_dumps_sorted from homeassistant.helpers.storage import Store from homeassistant.helpers.translation import async_get_translations from homeassistant.helpers.typing import ConfigType @@ -135,7 +135,7 @@ class Manifest: return self._serialized def _serialize(self) -> None: - self._serialized = json.dumps(self.manifest, sort_keys=True) + self._serialized = json_dumps_sorted(self.manifest) def update_key(self, key: str, val: str) -> None: """Add a keyval to the manifest.json.""" diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index fd1153711ad..dbe3163da08 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -87,6 +87,15 @@ def json_dumps(data: Any) -> str: ).decode("utf-8") +def json_dumps_sorted(data: Any) -> str: + """Dump json string with keys sorted.""" + return orjson.dumps( + data, + option=orjson.OPT_NON_STR_KEYS | orjson.OPT_SORT_KEYS, + default=json_encoder_default, + ).decode("utf-8") + + json_loads = orjson.loads diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py index 4968c872946..17066b682af 100644 --- a/tests/helpers/test_json.py +++ b/tests/helpers/test_json.py @@ -1,10 +1,15 @@ """Test Home Assistant remote methods and classes.""" import datetime +import json import pytest from homeassistant import core -from homeassistant.helpers.json import ExtendedJSONEncoder, JSONEncoder +from homeassistant.helpers.json import ( + ExtendedJSONEncoder, + JSONEncoder, + json_dumps_sorted, +) from homeassistant.util import dt as dt_util @@ -64,3 +69,11 @@ def test_extended_json_encoder(hass): # Default method falls back to repr(o) o = object() assert ha_json_enc.default(o) == {"__type": str(type(o)), "repr": repr(o)} + + +def test_json_dumps_sorted(): + """Test the json dumps sorted function.""" + data = {"c": 3, "a": 1, "b": 2} + assert json_dumps_sorted(data) == json.dumps( + data, sort_keys=True, separators=(",", ":") + ) From b3b470757980db546e2c1c131a53bd46039ed81b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 23 Jun 2022 20:26:51 +0200 Subject: [PATCH 1748/3516] Fix deCONZ group state regression (#73907) --- homeassistant/components/deconz/gateway.py | 1 - homeassistant/components/deconz/light.py | 25 ++++++++++++++++++- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 5890e372e66..c94b2c6d86d 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -287,7 +287,6 @@ async def get_deconz_session( config[CONF_HOST], config[CONF_PORT], config[CONF_API_KEY], - legacy_add_device=False, ) try: async with async_timeout.timeout(10): diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 53773369176..0cca007742a 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -105,7 +105,10 @@ async def async_setup_entry( @callback def async_add_group(_: EventType, group_id: str) -> None: - """Add group from deCONZ.""" + """Add group from deCONZ. + + Update group states based on its sum of related lights. + """ if ( not gateway.option_allow_deconz_groups or (group := gateway.api.groups[group_id]) @@ -113,6 +116,16 @@ async def async_setup_entry( ): return + first = True + for light_id in group.lights: + if ( + (light := gateway.api.lights.lights.get(light_id)) + and light.ZHATYPE == Light.ZHATYPE + and light.reachable + ): + group.update_color_state(light, update_all_attributes=first) + first = False + async_add_entities([DeconzGroup(group, gateway)]) config_entry.async_on_unload( @@ -289,6 +302,16 @@ class DeconzLight(DeconzBaseLight[Light]): """Return the coldest color_temp that this light supports.""" return self._device.min_color_temp or super().min_mireds + @callback + def async_update_callback(self) -> None: + """Light state will also reflect in relevant groups.""" + super().async_update_callback() + + if self._device.reachable and "attr" not in self._device.changed_keys: + for group in self.gateway.api.groups.values(): + if self._device.resource_id in group.lights: + group.update_color_state(self._device) + class DeconzGroup(DeconzBaseLight[Group]): """Representation of a deCONZ group.""" diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index ce10845a5b1..09dcc190a4f 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==94"], + "requirements": ["pydeconz==95"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 8935161ae1e..705d5341cb1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1441,7 +1441,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==94 +pydeconz==95 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d4f43b284c0..b22bce40639 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -968,7 +968,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==94 +pydeconz==95 # homeassistant.components.dexcom pydexcom==0.2.3 From fd9fdc628367be9d0c3daa91e3a63e3ce6199924 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Jun 2022 13:32:45 -0500 Subject: [PATCH 1749/3516] Fix error reporting with unserializable json (#73908) --- homeassistant/util/json.py | 4 +++- tests/util/test_json.py | 14 +++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 82ecfd34d6d..8a9663bb95d 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -57,13 +57,15 @@ def save_json( Returns True on success. """ + dump: Callable[[Any], Any] = json.dumps try: if encoder: json_data = json.dumps(data, indent=2, cls=encoder) else: + dump = orjson.dumps json_data = orjson.dumps(data, option=orjson.OPT_INDENT_2).decode("utf-8") except TypeError as error: - msg = f"Failed to serialize to JSON: {filename}. Bad data at {format_unserializable_data(find_paths_unserializable_data(data))}" + msg = f"Failed to serialize to JSON: {filename}. Bad data at {format_unserializable_data(find_paths_unserializable_data(data, dump=dump))}" _LOGGER.error(msg) raise SerializationError(msg) from error diff --git a/tests/util/test_json.py b/tests/util/test_json.py index 461d94d0c67..abf47b0bc53 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -11,6 +11,7 @@ import pytest from homeassistant.core import Event, State from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.template import TupleWrapper from homeassistant.util.json import ( SerializationError, find_paths_unserializable_data, @@ -72,7 +73,7 @@ def test_overwrite_and_reload(atomic_writes): def test_save_bad_data(): - """Test error from trying to save unserialisable data.""" + """Test error from trying to save unserializable data.""" with pytest.raises(SerializationError) as excinfo: save_json("test4", {"hello": set()}) @@ -82,6 +83,17 @@ def test_save_bad_data(): ) +def test_save_bad_data_tuple_wrapper(): + """Test error from trying to save unserializable data.""" + with pytest.raises(SerializationError) as excinfo: + save_json("test4", {"hello": TupleWrapper(("4", "5"))}) + + assert ( + "Failed to serialize to JSON: test4. Bad data at $.hello=('4', '5')(" + in str(excinfo.value) + ) + + def test_load_bad_data(): """Test error from trying to load unserialisable data.""" fname = _path_for("test5") From 01606c34aa29e7f681415e3a5ac5be6d8678b377 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 23 Jun 2022 20:34:16 +0200 Subject: [PATCH 1750/3516] Correct handling of weather forecast (#73909) --- homeassistant/components/weather/__init__.py | 40 ++++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 3e72c7ad931..f09e76b0073 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -719,10 +719,12 @@ class WeatherEntity(Entity): value_temp, precision ) - if forecast_temp_low := forecast_entry.pop( - ATTR_FORECAST_NATIVE_TEMP_LOW, - forecast_entry.get(ATTR_FORECAST_TEMP_LOW), - ): + if ( + forecast_temp_low := forecast_entry.pop( + ATTR_FORECAST_NATIVE_TEMP_LOW, + forecast_entry.get(ATTR_FORECAST_TEMP_LOW), + ) + ) is not None: with suppress(TypeError, ValueError): forecast_temp_low_f = float(forecast_temp_low) value_temp_low = UNIT_CONVERSIONS[ @@ -737,10 +739,12 @@ class WeatherEntity(Entity): value_temp_low, precision ) - if forecast_pressure := forecast_entry.pop( - ATTR_FORECAST_NATIVE_PRESSURE, - forecast_entry.get(ATTR_FORECAST_PRESSURE), - ): + if ( + forecast_pressure := forecast_entry.pop( + ATTR_FORECAST_NATIVE_PRESSURE, + forecast_entry.get(ATTR_FORECAST_PRESSURE), + ) + ) is not None: from_pressure_unit = ( self.native_pressure_unit or self._default_pressure_unit ) @@ -756,10 +760,12 @@ class WeatherEntity(Entity): ROUNDING_PRECISION, ) - if forecast_wind_speed := forecast_entry.pop( - ATTR_FORECAST_NATIVE_WIND_SPEED, - forecast_entry.get(ATTR_FORECAST_WIND_SPEED), - ): + if ( + forecast_wind_speed := forecast_entry.pop( + ATTR_FORECAST_NATIVE_WIND_SPEED, + forecast_entry.get(ATTR_FORECAST_WIND_SPEED), + ) + ) is not None: from_wind_speed_unit = ( self.native_wind_speed_unit or self._default_wind_speed_unit ) @@ -775,10 +781,12 @@ class WeatherEntity(Entity): ROUNDING_PRECISION, ) - if forecast_precipitation := forecast_entry.pop( - ATTR_FORECAST_NATIVE_PRECIPITATION, - forecast_entry.get(ATTR_FORECAST_PRECIPITATION), - ): + if ( + forecast_precipitation := forecast_entry.pop( + ATTR_FORECAST_NATIVE_PRECIPITATION, + forecast_entry.get(ATTR_FORECAST_PRECIPITATION), + ) + ) is not None: from_precipitation_unit = ( self.native_precipitation_unit or self._default_precipitation_unit From 5c193323b240aaa80c1e57da12b2444b03ac2af8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Jun 2022 13:43:56 -0500 Subject: [PATCH 1751/3516] Bump aiohomekit to 0.7.18 (#73919) Changelog: https://github.com/Jc2k/aiohomekit/compare/0.7.17...0.7.18 --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index ae9b1261bc8..3b3c5e51cf8 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.7.17"], + "requirements": ["aiohomekit==0.7.18"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 705d5341cb1..ba18173bcac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -162,7 +162,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.17 +aiohomekit==0.7.18 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b22bce40639..8545067551e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.17 +aiohomekit==0.7.18 # homeassistant.components.emulated_hue # homeassistant.components.http From 00a79635c14618e3d2ab798fc228d7c5e1abf208 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Jun 2022 13:59:55 -0500 Subject: [PATCH 1752/3516] Revert "Remove sqlalchemy lambda_stmt usage from history, logbook, and statistics (#73191)" (#73917) --- .../components/logbook/queries/__init__.py | 26 ++-- .../components/logbook/queries/all.py | 20 +-- .../components/logbook/queries/devices.py | 42 +++--- .../components/logbook/queries/entities.py | 47 ++++--- .../logbook/queries/entities_and_devices.py | 59 ++++---- homeassistant/components/recorder/history.py | 130 ++++++++++-------- .../components/recorder/statistics.py | 99 +++++++------ homeassistant/components/recorder/util.py | 10 +- tests/components/recorder/test_util.py | 23 ++-- 9 files changed, 253 insertions(+), 203 deletions(-) diff --git a/homeassistant/components/logbook/queries/__init__.py b/homeassistant/components/logbook/queries/__init__.py index a59ebc94b87..3c027823612 100644 --- a/homeassistant/components/logbook/queries/__init__.py +++ b/homeassistant/components/logbook/queries/__init__.py @@ -2,9 +2,8 @@ from __future__ import annotations from datetime import datetime as dt -import json -from sqlalchemy.sql.selectable import Select +from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components.recorder.filters import Filters @@ -22,7 +21,7 @@ def statement_for_request( device_ids: list[str] | None = None, filters: Filters | None = None, context_id: str | None = None, -) -> Select: +) -> StatementLambdaElement: """Generate the logbook statement for a logbook request.""" # No entities: logbook sends everything for the timeframe @@ -39,36 +38,41 @@ def statement_for_request( context_id, ) + # sqlalchemy caches object quoting, the + # json quotable ones must be a different + # object from the non-json ones to prevent + # sqlalchemy from quoting them incorrectly + # entities and devices: logbook sends everything for the timeframe for the entities and devices if entity_ids and device_ids: - json_quoted_entity_ids = [json.dumps(entity_id) for entity_id in entity_ids] - json_quoted_device_ids = [json.dumps(device_id) for device_id in device_ids] + json_quotable_entity_ids = list(entity_ids) + json_quotable_device_ids = list(device_ids) return entities_devices_stmt( start_day, end_day, event_types, entity_ids, - json_quoted_entity_ids, - json_quoted_device_ids, + json_quotable_entity_ids, + json_quotable_device_ids, ) # entities: logbook sends everything for the timeframe for the entities if entity_ids: - json_quoted_entity_ids = [json.dumps(entity_id) for entity_id in entity_ids] + json_quotable_entity_ids = list(entity_ids) return entities_stmt( start_day, end_day, event_types, entity_ids, - json_quoted_entity_ids, + json_quotable_entity_ids, ) # devices: logbook sends everything for the timeframe for the devices assert device_ids is not None - json_quoted_device_ids = [json.dumps(device_id) for device_id in device_ids] + json_quotable_device_ids = list(device_ids) return devices_stmt( start_day, end_day, event_types, - json_quoted_device_ids, + json_quotable_device_ids, ) diff --git a/homeassistant/components/logbook/queries/all.py b/homeassistant/components/logbook/queries/all.py index e0a651c7972..da05aa02fff 100644 --- a/homeassistant/components/logbook/queries/all.py +++ b/homeassistant/components/logbook/queries/all.py @@ -3,9 +3,10 @@ from __future__ import annotations from datetime import datetime as dt +from sqlalchemy import lambda_stmt from sqlalchemy.orm import Query from sqlalchemy.sql.elements import ClauseList -from sqlalchemy.sql.selectable import Select +from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components.recorder.db_schema import ( LAST_UPDATED_INDEX, @@ -28,29 +29,32 @@ def all_stmt( states_entity_filter: ClauseList | None = None, events_entity_filter: ClauseList | None = None, context_id: str | None = None, -) -> Select: +) -> StatementLambdaElement: """Generate a logbook query for all entities.""" - stmt = select_events_without_states(start_day, end_day, event_types) + stmt = lambda_stmt( + lambda: select_events_without_states(start_day, end_day, event_types) + ) if context_id is not None: # Once all the old `state_changed` events # are gone from the database remove the # _legacy_select_events_context_id() - stmt = stmt.where(Events.context_id == context_id).union_all( + stmt += lambda s: s.where(Events.context_id == context_id).union_all( _states_query_for_context_id(start_day, end_day, context_id), legacy_select_events_context_id(start_day, end_day, context_id), ) else: if events_entity_filter is not None: - stmt = stmt.where(events_entity_filter) + stmt += lambda s: s.where(events_entity_filter) if states_entity_filter is not None: - stmt = stmt.union_all( + stmt += lambda s: s.union_all( _states_query_for_all(start_day, end_day).where(states_entity_filter) ) else: - stmt = stmt.union_all(_states_query_for_all(start_day, end_day)) + stmt += lambda s: s.union_all(_states_query_for_all(start_day, end_day)) - return stmt.order_by(Events.time_fired) + stmt += lambda s: s.order_by(Events.time_fired) + return stmt def _states_query_for_all(start_day: dt, end_day: dt) -> Query: diff --git a/homeassistant/components/logbook/queries/devices.py b/homeassistant/components/logbook/queries/devices.py index cbe766fb02c..f750c552bc4 100644 --- a/homeassistant/components/logbook/queries/devices.py +++ b/homeassistant/components/logbook/queries/devices.py @@ -4,10 +4,11 @@ from __future__ import annotations from collections.abc import Iterable from datetime import datetime as dt -from sqlalchemy import select +from sqlalchemy import lambda_stmt, select from sqlalchemy.orm import Query from sqlalchemy.sql.elements import ClauseList -from sqlalchemy.sql.selectable import CTE, CompoundSelect, Select +from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import CTE, CompoundSelect from homeassistant.components.recorder.db_schema import ( DEVICE_ID_IN_EVENT, @@ -30,11 +31,11 @@ def _select_device_id_context_ids_sub_query( start_day: dt, end_day: dt, event_types: tuple[str, ...], - json_quoted_device_ids: list[str], + json_quotable_device_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple devices.""" inner = select_events_context_id_subquery(start_day, end_day, event_types).where( - apply_event_device_id_matchers(json_quoted_device_ids) + apply_event_device_id_matchers(json_quotable_device_ids) ) return select(inner.c.context_id).group_by(inner.c.context_id) @@ -44,14 +45,14 @@ def _apply_devices_context_union( start_day: dt, end_day: dt, event_types: tuple[str, ...], - json_quoted_device_ids: list[str], + json_quotable_device_ids: list[str], ) -> CompoundSelect: """Generate a CTE to find the device context ids and a query to find linked row.""" devices_cte: CTE = _select_device_id_context_ids_sub_query( start_day, end_day, event_types, - json_quoted_device_ids, + json_quotable_device_ids, ).cte() return query.union_all( apply_events_context_hints( @@ -71,22 +72,25 @@ def devices_stmt( start_day: dt, end_day: dt, event_types: tuple[str, ...], - json_quoted_device_ids: list[str], -) -> Select: + json_quotable_device_ids: list[str], +) -> StatementLambdaElement: """Generate a logbook query for multiple devices.""" - return _apply_devices_context_union( - select_events_without_states(start_day, end_day, event_types).where( - apply_event_device_id_matchers(json_quoted_device_ids) - ), - start_day, - end_day, - event_types, - json_quoted_device_ids, - ).order_by(Events.time_fired) + stmt = lambda_stmt( + lambda: _apply_devices_context_union( + select_events_without_states(start_day, end_day, event_types).where( + apply_event_device_id_matchers(json_quotable_device_ids) + ), + start_day, + end_day, + event_types, + json_quotable_device_ids, + ).order_by(Events.time_fired) + ) + return stmt def apply_event_device_id_matchers( - json_quoted_device_ids: Iterable[str], + json_quotable_device_ids: Iterable[str], ) -> ClauseList: """Create matchers for the device_ids in the event_data.""" - return DEVICE_ID_IN_EVENT.in_(json_quoted_device_ids) + return DEVICE_ID_IN_EVENT.in_(json_quotable_device_ids) diff --git a/homeassistant/components/logbook/queries/entities.py b/homeassistant/components/logbook/queries/entities.py index 4d250fbb0f1..4ef96c100d7 100644 --- a/homeassistant/components/logbook/queries/entities.py +++ b/homeassistant/components/logbook/queries/entities.py @@ -5,9 +5,10 @@ from collections.abc import Iterable from datetime import datetime as dt import sqlalchemy -from sqlalchemy import select, union_all +from sqlalchemy import lambda_stmt, select, union_all from sqlalchemy.orm import Query -from sqlalchemy.sql.selectable import CTE, CompoundSelect, Select +from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import CTE, CompoundSelect from homeassistant.components.recorder.db_schema import ( ENTITY_ID_IN_EVENT, @@ -35,12 +36,12 @@ def _select_entities_context_ids_sub_query( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quoted_entity_ids: list[str], + json_quotable_entity_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple entities.""" union = union_all( select_events_context_id_subquery(start_day, end_day, event_types).where( - apply_event_entity_id_matchers(json_quoted_entity_ids) + apply_event_entity_id_matchers(json_quotable_entity_ids) ), apply_entities_hints(select(States.context_id)) .filter((States.last_updated > start_day) & (States.last_updated < end_day)) @@ -55,7 +56,7 @@ def _apply_entities_context_union( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quoted_entity_ids: list[str], + json_quotable_entity_ids: list[str], ) -> CompoundSelect: """Generate a CTE to find the entity and device context ids and a query to find linked row.""" entities_cte: CTE = _select_entities_context_ids_sub_query( @@ -63,7 +64,7 @@ def _apply_entities_context_union( end_day, event_types, entity_ids, - json_quoted_entity_ids, + json_quotable_entity_ids, ).cte() # We used to optimize this to exclude rows we already in the union with # a States.entity_id.not_in(entity_ids) but that made the @@ -90,19 +91,21 @@ def entities_stmt( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quoted_entity_ids: list[str], -) -> Select: + json_quotable_entity_ids: list[str], +) -> StatementLambdaElement: """Generate a logbook query for multiple entities.""" - return _apply_entities_context_union( - select_events_without_states(start_day, end_day, event_types).where( - apply_event_entity_id_matchers(json_quoted_entity_ids) - ), - start_day, - end_day, - event_types, - entity_ids, - json_quoted_entity_ids, - ).order_by(Events.time_fired) + return lambda_stmt( + lambda: _apply_entities_context_union( + select_events_without_states(start_day, end_day, event_types).where( + apply_event_entity_id_matchers(json_quotable_entity_ids) + ), + start_day, + end_day, + event_types, + entity_ids, + json_quotable_entity_ids, + ).order_by(Events.time_fired) + ) def states_query_for_entity_ids( @@ -115,12 +118,12 @@ def states_query_for_entity_ids( def apply_event_entity_id_matchers( - json_quoted_entity_ids: Iterable[str], + json_quotable_entity_ids: Iterable[str], ) -> sqlalchemy.or_: """Create matchers for the entity_id in the event_data.""" - return ENTITY_ID_IN_EVENT.in_(json_quoted_entity_ids) | OLD_ENTITY_ID_IN_EVENT.in_( - json_quoted_entity_ids - ) + return ENTITY_ID_IN_EVENT.in_( + json_quotable_entity_ids + ) | OLD_ENTITY_ID_IN_EVENT.in_(json_quotable_entity_ids) def apply_entities_hints(query: Query) -> Query: diff --git a/homeassistant/components/logbook/queries/entities_and_devices.py b/homeassistant/components/logbook/queries/entities_and_devices.py index 8b8051e2966..591918dd653 100644 --- a/homeassistant/components/logbook/queries/entities_and_devices.py +++ b/homeassistant/components/logbook/queries/entities_and_devices.py @@ -5,9 +5,10 @@ from collections.abc import Iterable from datetime import datetime as dt import sqlalchemy -from sqlalchemy import select, union_all +from sqlalchemy import lambda_stmt, select, union_all from sqlalchemy.orm import Query -from sqlalchemy.sql.selectable import CTE, CompoundSelect, Select +from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import CTE, CompoundSelect from homeassistant.components.recorder.db_schema import EventData, Events, States @@ -32,14 +33,14 @@ def _select_entities_device_id_context_ids_sub_query( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quoted_entity_ids: list[str], - json_quoted_device_ids: list[str], + json_quotable_entity_ids: list[str], + json_quotable_device_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple entities and multiple devices.""" union = union_all( select_events_context_id_subquery(start_day, end_day, event_types).where( _apply_event_entity_id_device_id_matchers( - json_quoted_entity_ids, json_quoted_device_ids + json_quotable_entity_ids, json_quotable_device_ids ) ), apply_entities_hints(select(States.context_id)) @@ -55,16 +56,16 @@ def _apply_entities_devices_context_union( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quoted_entity_ids: list[str], - json_quoted_device_ids: list[str], + json_quotable_entity_ids: list[str], + json_quotable_device_ids: list[str], ) -> CompoundSelect: devices_entities_cte: CTE = _select_entities_device_id_context_ids_sub_query( start_day, end_day, event_types, entity_ids, - json_quoted_entity_ids, - json_quoted_device_ids, + json_quotable_entity_ids, + json_quotable_device_ids, ).cte() # We used to optimize this to exclude rows we already in the union with # a States.entity_id.not_in(entity_ids) but that made the @@ -91,30 +92,32 @@ def entities_devices_stmt( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quoted_entity_ids: list[str], - json_quoted_device_ids: list[str], -) -> Select: + json_quotable_entity_ids: list[str], + json_quotable_device_ids: list[str], +) -> StatementLambdaElement: """Generate a logbook query for multiple entities.""" - stmt = _apply_entities_devices_context_union( - select_events_without_states(start_day, end_day, event_types).where( - _apply_event_entity_id_device_id_matchers( - json_quoted_entity_ids, json_quoted_device_ids - ) - ), - start_day, - end_day, - event_types, - entity_ids, - json_quoted_entity_ids, - json_quoted_device_ids, - ).order_by(Events.time_fired) + stmt = lambda_stmt( + lambda: _apply_entities_devices_context_union( + select_events_without_states(start_day, end_day, event_types).where( + _apply_event_entity_id_device_id_matchers( + json_quotable_entity_ids, json_quotable_device_ids + ) + ), + start_day, + end_day, + event_types, + entity_ids, + json_quotable_entity_ids, + json_quotable_device_ids, + ).order_by(Events.time_fired) + ) return stmt def _apply_event_entity_id_device_id_matchers( - json_quoted_entity_ids: Iterable[str], json_quoted_device_ids: Iterable[str] + json_quotable_entity_ids: Iterable[str], json_quotable_device_ids: Iterable[str] ) -> sqlalchemy.or_: """Create matchers for the device_id and entity_id in the event_data.""" return apply_event_entity_id_matchers( - json_quoted_entity_ids - ) | apply_event_device_id_matchers(json_quoted_device_ids) + json_quotable_entity_ids + ) | apply_event_device_id_matchers(json_quotable_device_ids) diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 1238b63f3c9..e1eca282a3a 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -9,11 +9,13 @@ import logging import time from typing import Any, cast -from sqlalchemy import Column, Text, and_, func, or_, select +from sqlalchemy import Column, Text, and_, func, lambda_stmt, or_, select from sqlalchemy.engine.row import Row +from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal -from sqlalchemy.sql.selectable import Select, Subquery +from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Subquery from homeassistant.components import recorder from homeassistant.components.websocket_api.const import ( @@ -32,7 +34,7 @@ from .models import ( process_timestamp_to_utc_isoformat, row_to_compressed_state, ) -from .util import execute_stmt, session_scope +from .util import execute_stmt_lambda_element, session_scope # mypy: allow-untyped-defs, no-check-untyped-defs @@ -112,18 +114,22 @@ def _schema_version(hass: HomeAssistant) -> int: return recorder.get_instance(hass).schema_version -def stmt_and_join_attributes( +def lambda_stmt_and_join_attributes( schema_version: int, no_attributes: bool, include_last_changed: bool = True -) -> tuple[Select, bool]: - """Return the stmt and if StateAttributes should be joined.""" +) -> tuple[StatementLambdaElement, bool]: + """Return the lambda_stmt and if StateAttributes should be joined. + + Because these are lambda_stmt the values inside the lambdas need + to be explicitly written out to avoid caching the wrong values. + """ # If no_attributes was requested we do the query # without the attributes fields and do not join the # state_attributes table if no_attributes: if include_last_changed: - return select(*QUERY_STATE_NO_ATTR), False + return lambda_stmt(lambda: select(*QUERY_STATE_NO_ATTR)), False return ( - select(*QUERY_STATE_NO_ATTR_NO_LAST_CHANGED), + lambda_stmt(lambda: select(*QUERY_STATE_NO_ATTR_NO_LAST_CHANGED)), False, ) # If we in the process of migrating schema we do @@ -132,19 +138,19 @@ def stmt_and_join_attributes( if schema_version < 25: if include_last_changed: return ( - select(*QUERY_STATES_PRE_SCHEMA_25), + lambda_stmt(lambda: select(*QUERY_STATES_PRE_SCHEMA_25)), False, ) return ( - select(*QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED), + lambda_stmt(lambda: select(*QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED)), False, ) # Finally if no migration is in progress and no_attributes # was not requested, we query both attributes columns and # join state_attributes if include_last_changed: - return select(*QUERY_STATES), True - return select(*QUERY_STATES_NO_LAST_CHANGED), True + return lambda_stmt(lambda: select(*QUERY_STATES)), True + return lambda_stmt(lambda: select(*QUERY_STATES_NO_LAST_CHANGED)), True def get_significant_states( @@ -176,7 +182,7 @@ def get_significant_states( ) -def _ignore_domains_filter(query: Select) -> Select: +def _ignore_domains_filter(query: Query) -> Query: """Add a filter to ignore domains we do not fetch history for.""" return query.filter( and_( @@ -196,9 +202,9 @@ def _significant_states_stmt( filters: Filters | None, significant_changes_only: bool, no_attributes: bool, -) -> Select: +) -> StatementLambdaElement: """Query the database for significant state changes.""" - stmt, join_attributes = stmt_and_join_attributes( + stmt, join_attributes = lambda_stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=not significant_changes_only ) if ( @@ -207,11 +213,11 @@ def _significant_states_stmt( and significant_changes_only and split_entity_id(entity_ids[0])[0] not in SIGNIFICANT_DOMAINS ): - stmt = stmt.filter( + stmt += lambda q: q.filter( (States.last_changed == States.last_updated) | States.last_changed.is_(None) ) elif significant_changes_only: - stmt = stmt.filter( + stmt += lambda q: q.filter( or_( *[ States.entity_id.like(entity_domain) @@ -225,22 +231,25 @@ def _significant_states_stmt( ) if entity_ids: - stmt = stmt.filter(States.entity_id.in_(entity_ids)) + stmt += lambda q: q.filter(States.entity_id.in_(entity_ids)) else: - stmt = _ignore_domains_filter(stmt) + stmt += _ignore_domains_filter if filters and filters.has_config: entity_filter = filters.states_entity_filter() - stmt = stmt.filter(entity_filter) + stmt = stmt.add_criteria( + lambda q: q.filter(entity_filter), track_on=[filters] + ) - stmt = stmt.filter(States.last_updated > start_time) + stmt += lambda q: q.filter(States.last_updated > start_time) if end_time: - stmt = stmt.filter(States.last_updated < end_time) + stmt += lambda q: q.filter(States.last_updated < end_time) if join_attributes: - stmt = stmt.outerjoin( + stmt += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) - return stmt.order_by(States.entity_id, States.last_updated) + stmt += lambda q: q.order_by(States.entity_id, States.last_updated) + return stmt def get_significant_states_with_session( @@ -277,7 +286,9 @@ def get_significant_states_with_session( significant_changes_only, no_attributes, ) - states = execute_stmt(session, stmt, None if entity_ids else start_time, end_time) + states = execute_stmt_lambda_element( + session, stmt, None if entity_ids else start_time, end_time + ) return _sorted_states_to_dict( hass, session, @@ -329,28 +340,28 @@ def _state_changed_during_period_stmt( no_attributes: bool, descending: bool, limit: int | None, -) -> Select: - stmt, join_attributes = stmt_and_join_attributes( +) -> StatementLambdaElement: + stmt, join_attributes = lambda_stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=False ) - stmt = stmt.filter( + stmt += lambda q: q.filter( ((States.last_changed == States.last_updated) | States.last_changed.is_(None)) & (States.last_updated > start_time) ) if end_time: - stmt = stmt.filter(States.last_updated < end_time) + stmt += lambda q: q.filter(States.last_updated < end_time) if entity_id: - stmt = stmt.filter(States.entity_id == entity_id) + stmt += lambda q: q.filter(States.entity_id == entity_id) if join_attributes: - stmt = stmt.outerjoin( + stmt += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) if descending: - stmt = stmt.order_by(States.entity_id, States.last_updated.desc()) + stmt += lambda q: q.order_by(States.entity_id, States.last_updated.desc()) else: - stmt = stmt.order_by(States.entity_id, States.last_updated) + stmt += lambda q: q.order_by(States.entity_id, States.last_updated) if limit: - stmt = stmt.limit(limit) + stmt += lambda q: q.limit(limit) return stmt @@ -378,7 +389,7 @@ def state_changes_during_period( descending, limit, ) - states = execute_stmt( + states = execute_stmt_lambda_element( session, stmt, None if entity_id else start_time, end_time ) return cast( @@ -396,22 +407,23 @@ def state_changes_during_period( def _get_last_state_changes_stmt( schema_version: int, number_of_states: int, entity_id: str | None -) -> Select: - stmt, join_attributes = stmt_and_join_attributes( +) -> StatementLambdaElement: + stmt, join_attributes = lambda_stmt_and_join_attributes( schema_version, False, include_last_changed=False ) - stmt = stmt.filter( + stmt += lambda q: q.filter( (States.last_changed == States.last_updated) | States.last_changed.is_(None) ) if entity_id: - stmt = stmt.filter(States.entity_id == entity_id) + stmt += lambda q: q.filter(States.entity_id == entity_id) if join_attributes: - stmt = stmt.outerjoin( + stmt += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) - return stmt.order_by(States.entity_id, States.last_updated.desc()).limit( + stmt += lambda q: q.order_by(States.entity_id, States.last_updated.desc()).limit( number_of_states ) + return stmt def get_last_state_changes( @@ -426,7 +438,7 @@ def get_last_state_changes( stmt = _get_last_state_changes_stmt( _schema_version(hass), number_of_states, entity_id ) - states = list(execute_stmt(session, stmt)) + states = list(execute_stmt_lambda_element(session, stmt)) return cast( MutableMapping[str, list[State]], _sorted_states_to_dict( @@ -446,14 +458,14 @@ def _get_states_for_entites_stmt( utc_point_in_time: datetime, entity_ids: list[str], no_attributes: bool, -) -> Select: +) -> StatementLambdaElement: """Baked query to get states for specific entities.""" - stmt, join_attributes = stmt_and_join_attributes( + stmt, join_attributes = lambda_stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=True ) # We got an include-list of entities, accelerate the query by filtering already # in the inner query. - stmt = stmt.where( + stmt += lambda q: q.where( States.state_id == ( select(func.max(States.state_id).label("max_state_id")) @@ -467,7 +479,7 @@ def _get_states_for_entites_stmt( ).c.max_state_id ) if join_attributes: - stmt = stmt.outerjoin( + stmt += lambda q: q.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) return stmt @@ -498,9 +510,9 @@ def _get_states_for_all_stmt( utc_point_in_time: datetime, filters: Filters | None, no_attributes: bool, -) -> Select: +) -> StatementLambdaElement: """Baked query to get states for all entities.""" - stmt, join_attributes = stmt_and_join_attributes( + stmt, join_attributes = lambda_stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=True ) # We did not get an include-list of entities, query all states in the inner @@ -510,7 +522,7 @@ def _get_states_for_all_stmt( most_recent_states_by_date = _generate_most_recent_states_by_date( run_start, utc_point_in_time ) - stmt = stmt.where( + stmt += lambda q: q.where( States.state_id == ( select(func.max(States.state_id).label("max_state_id")) @@ -526,12 +538,12 @@ def _get_states_for_all_stmt( .subquery() ).c.max_state_id, ) - stmt = _ignore_domains_filter(stmt) + stmt += _ignore_domains_filter if filters and filters.has_config: entity_filter = filters.states_entity_filter() - stmt = stmt.filter(entity_filter) + stmt = stmt.add_criteria(lambda q: q.filter(entity_filter), track_on=[filters]) if join_attributes: - stmt = stmt.outerjoin( + stmt += lambda q: q.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) return stmt @@ -549,7 +561,7 @@ def _get_rows_with_session( """Return the states at a specific point in time.""" schema_version = _schema_version(hass) if entity_ids and len(entity_ids) == 1: - return execute_stmt( + return execute_stmt_lambda_element( session, _get_single_entity_states_stmt( schema_version, utc_point_in_time, entity_ids[0], no_attributes @@ -574,7 +586,7 @@ def _get_rows_with_session( schema_version, run.start, utc_point_in_time, filters, no_attributes ) - return execute_stmt(session, stmt) + return execute_stmt_lambda_element(session, stmt) def _get_single_entity_states_stmt( @@ -582,14 +594,14 @@ def _get_single_entity_states_stmt( utc_point_in_time: datetime, entity_id: str, no_attributes: bool = False, -) -> Select: +) -> StatementLambdaElement: # Use an entirely different (and extremely fast) query if we only # have a single entity id - stmt, join_attributes = stmt_and_join_attributes( + stmt, join_attributes = lambda_stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=True ) - stmt = ( - stmt.filter( + stmt += ( + lambda q: q.filter( States.last_updated < utc_point_in_time, States.entity_id == entity_id, ) @@ -597,7 +609,7 @@ def _get_single_entity_states_stmt( .limit(1) ) if join_attributes: - stmt = stmt.outerjoin( + stmt += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) return stmt diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 8d314830ec4..26221aa199b 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -14,12 +14,13 @@ import re from statistics import mean from typing import TYPE_CHECKING, Any, Literal, overload -from sqlalchemy import bindparam, func, select +from sqlalchemy import bindparam, func, lambda_stmt, select from sqlalchemy.engine.row import Row from sqlalchemy.exc import SQLAlchemyError, StatementError from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import literal_column, true -from sqlalchemy.sql.selectable import Select, Subquery +from sqlalchemy.sql.lambdas import StatementLambdaElement +from sqlalchemy.sql.selectable import Subquery import voluptuous as vol from homeassistant.const import ( @@ -49,7 +50,12 @@ from .models import ( process_timestamp, process_timestamp_to_utc_isoformat, ) -from .util import execute, execute_stmt, retryable_database_job, session_scope +from .util import ( + execute, + execute_stmt_lambda_element, + retryable_database_job, + session_scope, +) if TYPE_CHECKING: from . import Recorder @@ -474,10 +480,10 @@ def delete_statistics_meta_duplicates(session: Session) -> None: def _compile_hourly_statistics_summary_mean_stmt( start_time: datetime, end_time: datetime -) -> Select: +) -> StatementLambdaElement: """Generate the summary mean statement for hourly statistics.""" - return ( - select(*QUERY_STATISTICS_SUMMARY_MEAN) + return lambda_stmt( + lambda: select(*QUERY_STATISTICS_SUMMARY_MEAN) .filter(StatisticsShortTerm.start >= start_time) .filter(StatisticsShortTerm.start < end_time) .group_by(StatisticsShortTerm.metadata_id) @@ -500,7 +506,7 @@ def compile_hourly_statistics( # Compute last hour's average, min, max summary: dict[str, StatisticData] = {} stmt = _compile_hourly_statistics_summary_mean_stmt(start_time, end_time) - stats = execute_stmt(session, stmt) + stats = execute_stmt_lambda_element(session, stmt) if stats: for stat in stats: @@ -682,17 +688,17 @@ def _generate_get_metadata_stmt( statistic_ids: list[str] | tuple[str] | None = None, statistic_type: Literal["mean"] | Literal["sum"] | None = None, statistic_source: str | None = None, -) -> Select: +) -> StatementLambdaElement: """Generate a statement to fetch metadata.""" - stmt = select(*QUERY_STATISTIC_META) + stmt = lambda_stmt(lambda: select(*QUERY_STATISTIC_META)) if statistic_ids is not None: - stmt = stmt.where(StatisticsMeta.statistic_id.in_(statistic_ids)) + stmt += lambda q: q.where(StatisticsMeta.statistic_id.in_(statistic_ids)) if statistic_source is not None: - stmt = stmt.where(StatisticsMeta.source == statistic_source) + stmt += lambda q: q.where(StatisticsMeta.source == statistic_source) if statistic_type == "mean": - stmt = stmt.where(StatisticsMeta.has_mean == true()) + stmt += lambda q: q.where(StatisticsMeta.has_mean == true()) elif statistic_type == "sum": - stmt = stmt.where(StatisticsMeta.has_sum == true()) + stmt += lambda q: q.where(StatisticsMeta.has_sum == true()) return stmt @@ -714,7 +720,7 @@ def get_metadata_with_session( # Fetch metatadata from the database stmt = _generate_get_metadata_stmt(statistic_ids, statistic_type, statistic_source) - result = execute_stmt(session, stmt) + result = execute_stmt_lambda_element(session, stmt) if not result: return {} @@ -976,30 +982,44 @@ def _statistics_during_period_stmt( start_time: datetime, end_time: datetime | None, metadata_ids: list[int] | None, -) -> Select: - """Prepare a database query for statistics during a given period.""" - stmt = select(*QUERY_STATISTICS).filter(Statistics.start >= start_time) +) -> StatementLambdaElement: + """Prepare a database query for statistics during a given period. + + This prepares a lambda_stmt query, so we don't insert the parameters yet. + """ + stmt = lambda_stmt( + lambda: select(*QUERY_STATISTICS).filter(Statistics.start >= start_time) + ) if end_time is not None: - stmt = stmt.filter(Statistics.start < end_time) + stmt += lambda q: q.filter(Statistics.start < end_time) if metadata_ids: - stmt = stmt.filter(Statistics.metadata_id.in_(metadata_ids)) - return stmt.order_by(Statistics.metadata_id, Statistics.start) + stmt += lambda q: q.filter(Statistics.metadata_id.in_(metadata_ids)) + stmt += lambda q: q.order_by(Statistics.metadata_id, Statistics.start) + return stmt def _statistics_during_period_stmt_short_term( start_time: datetime, end_time: datetime | None, metadata_ids: list[int] | None, -) -> Select: - """Prepare a database query for short term statistics during a given period.""" - stmt = select(*QUERY_STATISTICS_SHORT_TERM).filter( - StatisticsShortTerm.start >= start_time +) -> StatementLambdaElement: + """Prepare a database query for short term statistics during a given period. + + This prepares a lambda_stmt query, so we don't insert the parameters yet. + """ + stmt = lambda_stmt( + lambda: select(*QUERY_STATISTICS_SHORT_TERM).filter( + StatisticsShortTerm.start >= start_time + ) ) if end_time is not None: - stmt = stmt.filter(StatisticsShortTerm.start < end_time) + stmt += lambda q: q.filter(StatisticsShortTerm.start < end_time) if metadata_ids: - stmt = stmt.filter(StatisticsShortTerm.metadata_id.in_(metadata_ids)) - return stmt.order_by(StatisticsShortTerm.metadata_id, StatisticsShortTerm.start) + stmt += lambda q: q.filter(StatisticsShortTerm.metadata_id.in_(metadata_ids)) + stmt += lambda q: q.order_by( + StatisticsShortTerm.metadata_id, StatisticsShortTerm.start + ) + return stmt def statistics_during_period( @@ -1034,7 +1054,7 @@ def statistics_during_period( else: table = Statistics stmt = _statistics_during_period_stmt(start_time, end_time, metadata_ids) - stats = execute_stmt(session, stmt) + stats = execute_stmt_lambda_element(session, stmt) if not stats: return {} @@ -1065,10 +1085,10 @@ def statistics_during_period( def _get_last_statistics_stmt( metadata_id: int, number_of_stats: int, -) -> Select: +) -> StatementLambdaElement: """Generate a statement for number_of_stats statistics for a given statistic_id.""" - return ( - select(*QUERY_STATISTICS) + return lambda_stmt( + lambda: select(*QUERY_STATISTICS) .filter_by(metadata_id=metadata_id) .order_by(Statistics.metadata_id, Statistics.start.desc()) .limit(number_of_stats) @@ -1078,10 +1098,10 @@ def _get_last_statistics_stmt( def _get_last_statistics_short_term_stmt( metadata_id: int, number_of_stats: int, -) -> Select: +) -> StatementLambdaElement: """Generate a statement for number_of_stats short term statistics for a given statistic_id.""" - return ( - select(*QUERY_STATISTICS_SHORT_TERM) + return lambda_stmt( + lambda: select(*QUERY_STATISTICS_SHORT_TERM) .filter_by(metadata_id=metadata_id) .order_by(StatisticsShortTerm.metadata_id, StatisticsShortTerm.start.desc()) .limit(number_of_stats) @@ -1107,7 +1127,7 @@ def _get_last_statistics( stmt = _get_last_statistics_stmt(metadata_id, number_of_stats) else: stmt = _get_last_statistics_short_term_stmt(metadata_id, number_of_stats) - stats = execute_stmt(session, stmt) + stats = execute_stmt_lambda_element(session, stmt) if not stats: return {} @@ -1157,11 +1177,11 @@ def _generate_most_recent_statistic_row(metadata_ids: list[int]) -> Subquery: def _latest_short_term_statistics_stmt( metadata_ids: list[int], -) -> Select: +) -> StatementLambdaElement: """Create the statement for finding the latest short term stat rows.""" - stmt = select(*QUERY_STATISTICS_SHORT_TERM) + stmt = lambda_stmt(lambda: select(*QUERY_STATISTICS_SHORT_TERM)) most_recent_statistic_row = _generate_most_recent_statistic_row(metadata_ids) - return stmt.join( + stmt += lambda s: s.join( most_recent_statistic_row, ( StatisticsShortTerm.metadata_id # pylint: disable=comparison-with-callable @@ -1169,6 +1189,7 @@ def _latest_short_term_statistics_stmt( ) & (StatisticsShortTerm.start == most_recent_statistic_row.c.start_max), ) + return stmt def get_latest_short_term_statistics( @@ -1191,7 +1212,7 @@ def get_latest_short_term_statistics( if statistic_id in metadata ] stmt = _latest_short_term_statistics_stmt(metadata_ids) - stats = execute_stmt(session, stmt) + stats = execute_stmt_lambda_element(session, stmt) if not stats: return {} diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 7e183f7f64f..c1fbc831987 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -22,6 +22,7 @@ from sqlalchemy.engine.row import Row from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session +from sqlalchemy.sql.lambdas import StatementLambdaElement from typing_extensions import Concatenate, ParamSpec from homeassistant.core import HomeAssistant @@ -165,9 +166,9 @@ def execute( assert False # unreachable # pragma: no cover -def execute_stmt( +def execute_stmt_lambda_element( session: Session, - query: Query, + stmt: StatementLambdaElement, start_time: datetime | None = None, end_time: datetime | None = None, yield_per: int | None = DEFAULT_YIELD_STATES_ROWS, @@ -183,12 +184,11 @@ def execute_stmt( specific entities) since they are usually faster with .all(). """ + executed = session.execute(stmt) use_all = not start_time or ((end_time or dt_util.utcnow()) - start_time).days <= 1 for tryno in range(0, RETRIES): try: - if use_all: - return session.execute(query).all() # type: ignore[no-any-return] - return session.execute(query).yield_per(yield_per) # type: ignore[no-any-return] + return executed.all() if use_all else executed.yield_per(yield_per) # type: ignore[no-any-return] except SQLAlchemyError as err: _LOGGER.error("Error executing query: %s", err) if tryno == RETRIES - 1: diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 97cf4a58b5c..8624719f951 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -9,6 +9,7 @@ from sqlalchemy import text from sqlalchemy.engine.result import ChunkedIteratorResult from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.sql.elements import TextClause +from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components import recorder from homeassistant.components.recorder import history, util @@ -712,8 +713,8 @@ def test_build_mysqldb_conv(): @patch("homeassistant.components.recorder.util.QUERY_RETRY_WAIT", 0) -def test_execute_stmt(hass_recorder): - """Test executing with execute_stmt.""" +def test_execute_stmt_lambda_element(hass_recorder): + """Test executing with execute_stmt_lambda_element.""" hass = hass_recorder() instance = recorder.get_instance(hass) hass.states.set("sensor.on", "on") @@ -724,15 +725,13 @@ def test_execute_stmt(hass_recorder): one_week_from_now = now + timedelta(days=7) class MockExecutor: - - _calls = 0 - def __init__(self, stmt): - """Init the mock.""" + assert isinstance(stmt, StatementLambdaElement) + self.calls = 0 def all(self): - MockExecutor._calls += 1 - if MockExecutor._calls == 2: + self.calls += 1 + if self.calls == 2: return ["mock_row"] raise SQLAlchemyError @@ -741,24 +740,24 @@ def test_execute_stmt(hass_recorder): stmt = history._get_single_entity_states_stmt( instance.schema_version, dt_util.utcnow(), "sensor.on", False ) - rows = util.execute_stmt(session, stmt) + rows = util.execute_stmt_lambda_element(session, stmt) assert isinstance(rows, list) assert rows[0].state == new_state.state assert rows[0].entity_id == new_state.entity_id # Time window >= 2 days, we get a ChunkedIteratorResult - rows = util.execute_stmt(session, stmt, now, one_week_from_now) + rows = util.execute_stmt_lambda_element(session, stmt, now, one_week_from_now) assert isinstance(rows, ChunkedIteratorResult) row = next(rows) assert row.state == new_state.state assert row.entity_id == new_state.entity_id # Time window < 2 days, we get a list - rows = util.execute_stmt(session, stmt, now, tomorrow) + rows = util.execute_stmt_lambda_element(session, stmt, now, tomorrow) assert isinstance(rows, list) assert rows[0].state == new_state.state assert rows[0].entity_id == new_state.entity_id with patch.object(session, "execute", MockExecutor): - rows = util.execute_stmt(session, stmt, now, tomorrow) + rows = util.execute_stmt_lambda_element(session, stmt, now, tomorrow) assert rows == ["mock_row"] From e57f34f0f2249aa97d3f93339bfe54e88ab4b2bf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 23 Jun 2022 21:01:08 +0200 Subject: [PATCH 1753/3516] Migrate openweathermap to native_* (#73913) --- .../components/openweathermap/const.py | 16 +++++----- .../components/openweathermap/weather.py | 32 +++++++++---------- .../weather_update_coordinator.py | 28 +++++++++------- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 8d673507929..027c08fd84b 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -22,11 +22,11 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRESSURE, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_PRESSURE, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ) from homeassistant.const import ( @@ -266,7 +266,7 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION, + key=ATTR_FORECAST_NATIVE_PRECIPITATION, name="Precipitation", native_unit_of_measurement=LENGTH_MILLIMETERS, ), @@ -276,19 +276,19 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_PRESSURE, + key=ATTR_FORECAST_NATIVE_PRESSURE, name="Pressure", native_unit_of_measurement=PRESSURE_HPA, device_class=SensorDeviceClass.PRESSURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP, + key=ATTR_FORECAST_NATIVE_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP_LOW, + key=ATTR_FORECAST_NATIVE_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index d4ab99bc30b..fce6efdf3c5 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -3,12 +3,16 @@ from __future__ import annotations from homeassistant.components.weather import Forecast, WeatherEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRESSURE_HPA, PRESSURE_INHG, TEMP_CELSIUS +from homeassistant.const import ( + LENGTH_MILLIMETERS, + PRESSURE_HPA, + SPEED_METERS_PER_SECOND, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util.pressure import convert as pressure_convert from .const import ( ATTR_API_CONDITION, @@ -49,7 +53,11 @@ class OpenWeatherMapWeather(WeatherEntity): _attr_attribution = ATTRIBUTION _attr_should_poll = False - _attr_temperature_unit = TEMP_CELSIUS + + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND def __init__( self, @@ -74,19 +82,14 @@ class OpenWeatherMapWeather(WeatherEntity): return self._weather_coordinator.data[ATTR_API_CONDITION] @property - def temperature(self) -> float | None: + def native_temperature(self) -> float | None: """Return the temperature.""" return self._weather_coordinator.data[ATTR_API_TEMPERATURE] @property - def pressure(self) -> float | None: + def native_pressure(self) -> float | None: """Return the pressure.""" - pressure = self._weather_coordinator.data[ATTR_API_PRESSURE] - # OpenWeatherMap returns pressure in hPA, so convert to - # inHg if we aren't using metric. - if not self.hass.config.units.is_metric and pressure: - return pressure_convert(pressure, PRESSURE_HPA, PRESSURE_INHG) - return pressure + return self._weather_coordinator.data[ATTR_API_PRESSURE] @property def humidity(self) -> float | None: @@ -94,12 +97,9 @@ class OpenWeatherMapWeather(WeatherEntity): return self._weather_coordinator.data[ATTR_API_HUMIDITY] @property - def wind_speed(self) -> float | None: + def native_wind_speed(self) -> float | None: """Return the wind speed.""" - wind_speed = self._weather_coordinator.data[ATTR_API_WIND_SPEED] - if self.hass.config.units.name == "imperial": - return round(wind_speed * 2.24, 2) - return round(wind_speed * 3.6, 2) + return self._weather_coordinator.data[ATTR_API_WIND_SPEED] @property def wind_bearing(self) -> float | str | None: diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index 26341621051..36511424737 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -9,14 +9,14 @@ from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRESSURE, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, 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, ) from homeassistant.helpers import sun from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -161,14 +161,14 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ATTR_FORECAST_TIME: dt.utc_from_timestamp( entry.reference_time("unix") ).isoformat(), - ATTR_FORECAST_PRECIPITATION: self._calc_precipitation( + ATTR_FORECAST_NATIVE_PRECIPITATION: self._calc_precipitation( entry.rain, entry.snow ), ATTR_FORECAST_PRECIPITATION_PROBABILITY: ( round(entry.precipitation_probability * 100) ), - ATTR_FORECAST_PRESSURE: entry.pressure.get("press"), - ATTR_FORECAST_WIND_SPEED: entry.wind().get("speed"), + ATTR_FORECAST_NATIVE_PRESSURE: entry.pressure.get("press"), + ATTR_FORECAST_NATIVE_WIND_SPEED: entry.wind().get("speed"), ATTR_FORECAST_WIND_BEARING: entry.wind().get("deg"), ATTR_FORECAST_CONDITION: self._get_condition( entry.weather_code, entry.reference_time("unix") @@ -178,10 +178,16 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): temperature_dict = entry.temperature("celsius") if "max" in temperature_dict and "min" in temperature_dict: - forecast[ATTR_FORECAST_TEMP] = entry.temperature("celsius").get("max") - forecast[ATTR_FORECAST_TEMP_LOW] = entry.temperature("celsius").get("min") + forecast[ATTR_FORECAST_NATIVE_TEMP] = entry.temperature("celsius").get( + "max" + ) + forecast[ATTR_FORECAST_NATIVE_TEMP_LOW] = entry.temperature("celsius").get( + "min" + ) else: - forecast[ATTR_FORECAST_TEMP] = entry.temperature("celsius").get("temp") + forecast[ATTR_FORECAST_NATIVE_TEMP] = entry.temperature("celsius").get( + "temp" + ) return forecast From 03f0916e7cc9b1f0eded498c8d6f68f4cc26e5a8 Mon Sep 17 00:00:00 2001 From: tbertonatti Date: Thu, 23 Jun 2022 16:02:48 -0300 Subject: [PATCH 1754/3516] Add embed image parameter for Discord notify (#73474) --- homeassistant/components/discord/notify.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index 299919472cf..d97ce7042bc 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -27,6 +27,7 @@ ATTR_EMBED_FIELDS = "fields" ATTR_EMBED_FOOTER = "footer" ATTR_EMBED_TITLE = "title" ATTR_EMBED_THUMBNAIL = "thumbnail" +ATTR_EMBED_IMAGE = "image" ATTR_EMBED_URL = "url" ATTR_IMAGES = "images" @@ -94,6 +95,8 @@ class DiscordNotificationService(BaseNotificationService): embed.set_author(**embedding[ATTR_EMBED_AUTHOR]) if ATTR_EMBED_THUMBNAIL in embedding: embed.set_thumbnail(**embedding[ATTR_EMBED_THUMBNAIL]) + if ATTR_EMBED_IMAGE in embedding: + embed.set_image(**embedding[ATTR_EMBED_IMAGE]) embeds.append(embed) if ATTR_IMAGES in data: From 0df0533cd4b1c6743cd48a77354ecd656c187f0e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 23 Jun 2022 21:20:42 +0200 Subject: [PATCH 1755/3516] Use attributes in smarty fan (#73895) --- .../components/smarty/binary_sensor.py | 23 +++++++++----- homeassistant/components/smarty/fan.py | 29 ++++++------------ homeassistant/components/smarty/sensor.py | 30 +++++++++++-------- 3 files changed, 42 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/smarty/binary_sensor.py b/homeassistant/components/smarty/binary_sensor.py index f3e2c8bab2a..d9d757a71b5 100644 --- a/homeassistant/components/smarty/binary_sensor.py +++ b/homeassistant/components/smarty/binary_sensor.py @@ -3,6 +3,8 @@ from __future__ import annotations import logging +from pysmarty import Smarty + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -24,8 +26,8 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Smarty Binary Sensor Platform.""" - smarty = hass.data[DOMAIN]["api"] - name = hass.data[DOMAIN]["name"] + smarty: Smarty = hass.data[DOMAIN]["api"] + name: str = hass.data[DOMAIN]["name"] sensors = [ AlarmSensor(name, smarty), @@ -41,18 +43,23 @@ class SmartyBinarySensor(BinarySensorEntity): _attr_should_poll = False - def __init__(self, name, device_class, smarty): + def __init__( + self, + name: str, + device_class: BinarySensorDeviceClass | None, + smarty: Smarty, + ) -> None: """Initialize the entity.""" self._attr_name = name self._attr_device_class = device_class self._smarty = smarty - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call to update.""" async_dispatcher_connect(self.hass, SIGNAL_UPDATE_SMARTY, self._update_callback) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) @@ -60,7 +67,7 @@ class SmartyBinarySensor(BinarySensorEntity): class BoostSensor(SmartyBinarySensor): """Boost State Binary Sensor.""" - def __init__(self, name, smarty): + def __init__(self, name: str, smarty: Smarty) -> None: """Alarm Sensor Init.""" super().__init__(name=f"{name} Boost State", device_class=None, smarty=smarty) @@ -73,7 +80,7 @@ class BoostSensor(SmartyBinarySensor): class AlarmSensor(SmartyBinarySensor): """Alarm Binary Sensor.""" - def __init__(self, name, smarty): + def __init__(self, name: str, smarty: Smarty) -> None: """Alarm Sensor Init.""" super().__init__( name=f"{name} Alarm", @@ -90,7 +97,7 @@ class AlarmSensor(SmartyBinarySensor): class WarningSensor(SmartyBinarySensor): """Warning Sensor.""" - def __init__(self, name, smarty): + def __init__(self, name: str, smarty: Smarty) -> None: """Warning Sensor Init.""" super().__init__( name=f"{name} Warning", diff --git a/homeassistant/components/smarty/fan.py b/homeassistant/components/smarty/fan.py index ceb3b17e030..cf4b49e6105 100644 --- a/homeassistant/components/smarty/fan.py +++ b/homeassistant/components/smarty/fan.py @@ -5,6 +5,8 @@ import logging import math from typing import Any +from pysmarty import Smarty + from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -32,8 +34,8 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Smarty Fan Platform.""" - smarty = hass.data[DOMAIN]["api"] - name = hass.data[DOMAIN]["name"] + smarty: Smarty = hass.data[DOMAIN]["api"] + name: str = hass.data[DOMAIN]["name"] async_add_entities([SmartyFan(name, smarty)], True) @@ -41,29 +43,16 @@ async def async_setup_platform( class SmartyFan(FanEntity): """Representation of a Smarty Fan.""" + _attr_icon = "mdi:air-conditioner" + _attr_should_poll = False _attr_supported_features = FanEntityFeature.SET_SPEED def __init__(self, name, smarty): """Initialize the entity.""" - self._name = name + self._attr_name = name self._smarty_fan_speed = 0 self._smarty = smarty - @property - def should_poll(self): - """Do not poll.""" - return False - - @property - def name(self): - """Return the name of the fan.""" - return self._name - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:air-conditioner" - @property def is_on(self) -> bool: """Return state of the fan.""" @@ -116,7 +105,7 @@ class SmartyFan(FanEntity): self._smarty_fan_speed = 0 self.schedule_update_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call to update fan.""" self.async_on_remove( async_dispatcher_connect( @@ -125,7 +114,7 @@ class SmartyFan(FanEntity): ) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" _LOGGER.debug("Updating state") self._smarty_fan_speed = self._smarty.fan_speed diff --git a/homeassistant/components/smarty/sensor.py b/homeassistant/components/smarty/sensor.py index 2ba5e81f286..1c76fe3bfb9 100644 --- a/homeassistant/components/smarty/sensor.py +++ b/homeassistant/components/smarty/sensor.py @@ -4,6 +4,8 @@ from __future__ import annotations import datetime as dt import logging +from pysmarty import Smarty + from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback @@ -24,8 +26,8 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Smarty Sensor Platform.""" - smarty = hass.data[DOMAIN]["api"] - name = hass.data[DOMAIN]["name"] + smarty: Smarty = hass.data[DOMAIN]["api"] + name: str = hass.data[DOMAIN]["name"] sensors = [ SupplyAirTemperatureSensor(name, smarty), @@ -45,8 +47,12 @@ class SmartySensor(SensorEntity): _attr_should_poll = False def __init__( - self, name: str, device_class: str, smarty, unit_of_measurement: str = "" - ): + self, + name: str, + device_class: SensorDeviceClass | None, + smarty: Smarty, + unit_of_measurement: str | None, + ) -> None: """Initialize the entity.""" self._attr_name = name self._attr_native_value = None @@ -54,12 +60,12 @@ class SmartySensor(SensorEntity): self._attr_native_unit_of_measurement = unit_of_measurement self._smarty = smarty - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call to update.""" async_dispatcher_connect(self.hass, SIGNAL_UPDATE_SMARTY, self._update_callback) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) @@ -67,7 +73,7 @@ class SmartySensor(SensorEntity): class SupplyAirTemperatureSensor(SmartySensor): """Supply Air Temperature Sensor.""" - def __init__(self, name, smarty): + def __init__(self, name: str, smarty: Smarty) -> None: """Supply Air Temperature Init.""" super().__init__( name=f"{name} Supply Air Temperature", @@ -85,7 +91,7 @@ class SupplyAirTemperatureSensor(SmartySensor): class ExtractAirTemperatureSensor(SmartySensor): """Extract Air Temperature Sensor.""" - def __init__(self, name, smarty): + def __init__(self, name: str, smarty: Smarty) -> None: """Supply Air Temperature Init.""" super().__init__( name=f"{name} Extract Air Temperature", @@ -103,7 +109,7 @@ class ExtractAirTemperatureSensor(SmartySensor): class OutdoorAirTemperatureSensor(SmartySensor): """Extract Air Temperature Sensor.""" - def __init__(self, name, smarty): + def __init__(self, name: str, smarty: Smarty) -> None: """Outdoor Air Temperature Init.""" super().__init__( name=f"{name} Outdoor Air Temperature", @@ -121,7 +127,7 @@ class OutdoorAirTemperatureSensor(SmartySensor): class SupplyFanSpeedSensor(SmartySensor): """Supply Fan Speed RPM.""" - def __init__(self, name, smarty): + def __init__(self, name: str, smarty: Smarty) -> None: """Supply Fan Speed RPM Init.""" super().__init__( name=f"{name} Supply Fan Speed", @@ -139,7 +145,7 @@ class SupplyFanSpeedSensor(SmartySensor): class ExtractFanSpeedSensor(SmartySensor): """Extract Fan Speed RPM.""" - def __init__(self, name, smarty): + def __init__(self, name: str, smarty: Smarty) -> None: """Extract Fan Speed RPM Init.""" super().__init__( name=f"{name} Extract Fan Speed", @@ -157,7 +163,7 @@ class ExtractFanSpeedSensor(SmartySensor): class FilterDaysLeftSensor(SmartySensor): """Filter Days Left.""" - def __init__(self, name, smarty): + def __init__(self, name: str, smarty: Smarty) -> None: """Filter Days Left Init.""" super().__init__( name=f"{name} Filter Days Left", From d19fc0622b6f99fe19099cb668de15e217314369 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 23 Jun 2022 21:31:30 +0200 Subject: [PATCH 1756/3516] Add ToggleEntity to pylint fan checks (#73886) --- pylint/plugins/hass_enforce_type_hints.py | 32 ++++++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index a241252c9a9..9ec2fa83806 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -435,15 +435,39 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { } # Overriding properties and functions are normally checked by mypy, and will only # be checked by pylint when --ignore-missing-annotations is False +_TOGGLE_ENTITY_MATCH: list[TypeHintMatch] = [ + TypeHintMatch( + function_name="is_on", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="turn_on", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_off", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="toggle", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), +] _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { "fan": [ + ClassTypeHintMatch( + base_class="ToggleEntity", + matches=_TOGGLE_ENTITY_MATCH, + ), ClassTypeHintMatch( base_class="FanEntity", matches=[ - TypeHintMatch( - function_name="is_on", - return_type=["bool", None], - ), TypeHintMatch( function_name="percentage", return_type=["int", None], From 8865a58f744d149a973790545a0785775c1a176e Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Thu, 23 Jun 2022 20:34:08 +0100 Subject: [PATCH 1757/3516] Improve Glances entity descriptions, add long term statistics (#73049) --- homeassistant/components/glances/const.py | 37 ++++++++++++++++++----- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index e5a8f1424c2..3fd26165283 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -4,7 +4,11 @@ from __future__ import annotations from dataclasses import dataclass import sys -from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.const import DATA_GIBIBYTES, DATA_MEBIBYTES, PERCENTAGE, TEMP_CELSIUS DOMAIN = "glances" @@ -40,6 +44,7 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( name_suffix="used percent", native_unit_of_measurement=PERCENTAGE, icon="mdi:harddisk", + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="disk_use", @@ -47,6 +52,7 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( name_suffix="used", native_unit_of_measurement=DATA_GIBIBYTES, icon="mdi:harddisk", + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="disk_free", @@ -54,6 +60,7 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( name_suffix="free", native_unit_of_measurement=DATA_GIBIBYTES, icon="mdi:harddisk", + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="memory_use_percent", @@ -61,6 +68,7 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( name_suffix="RAM used percent", native_unit_of_measurement=PERCENTAGE, icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="memory_use", @@ -68,6 +76,7 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( name_suffix="RAM used", native_unit_of_measurement=DATA_MEBIBYTES, icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="memory_free", @@ -75,6 +84,7 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( name_suffix="RAM free", native_unit_of_measurement=DATA_MEBIBYTES, icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="swap_use_percent", @@ -82,6 +92,7 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( name_suffix="Swap used percent", native_unit_of_measurement=PERCENTAGE, icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="swap_use", @@ -89,6 +100,7 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( name_suffix="Swap used", native_unit_of_measurement=DATA_GIBIBYTES, icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="swap_free", @@ -96,41 +108,43 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( name_suffix="Swap free", native_unit_of_measurement=DATA_GIBIBYTES, icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="processor_load", type="load", name_suffix="CPU load", - native_unit_of_measurement="15 min", icon=CPU_ICON, + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="process_running", type="processcount", name_suffix="Running", - native_unit_of_measurement="Count", icon=CPU_ICON, + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="process_total", type="processcount", name_suffix="Total", - native_unit_of_measurement="Count", icon=CPU_ICON, + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="process_thread", type="processcount", name_suffix="Thread", - native_unit_of_measurement="Count", icon=CPU_ICON, + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="process_sleeping", type="processcount", name_suffix="Sleeping", - native_unit_of_measurement="Count", + native_unit_of_measurement="", icon=CPU_ICON, + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="cpu_use_percent", @@ -138,6 +152,7 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( name_suffix="CPU used", native_unit_of_measurement=PERCENTAGE, icon=CPU_ICON, + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="temperature_core", @@ -145,6 +160,7 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( name_suffix="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="temperature_hdd", @@ -152,6 +168,7 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( name_suffix="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="fan_speed", @@ -159,6 +176,7 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( name_suffix="Fan speed", native_unit_of_measurement="RPM", icon="mdi:fan", + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="battery", @@ -166,13 +184,14 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( name_suffix="Charge", native_unit_of_measurement=PERCENTAGE, icon="mdi:battery", + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="docker_active", type="docker", name_suffix="Containers active", - native_unit_of_measurement="", icon="mdi:docker", + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="docker_cpu_use", @@ -180,6 +199,7 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( name_suffix="Containers CPU used", native_unit_of_measurement=PERCENTAGE, icon="mdi:docker", + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="docker_memory_use", @@ -187,17 +207,20 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( name_suffix="Containers RAM used", native_unit_of_measurement=DATA_MEBIBYTES, icon="mdi:docker", + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="used", type="raid", name_suffix="Raid used", icon="mdi:harddisk", + state_class=SensorStateClass.MEASUREMENT, ), GlancesSensorEntityDescription( key="available", type="raid", name_suffix="Raid available", icon="mdi:harddisk", + state_class=SensorStateClass.MEASUREMENT, ), ) From 186141ee4df74de2d0b6cb744652360e8b4cb558 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 23 Jun 2022 21:35:05 +0200 Subject: [PATCH 1758/3516] Use attributes in keba locks and binary sensors (#73894) Co-authored-by: Franck Nijhof --- .../components/keba/binary_sensor.py | 70 ++++++++----------- homeassistant/components/keba/lock.py | 46 ++++-------- homeassistant/components/keba/sensor.py | 8 +-- 3 files changed, 46 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/keba/binary_sensor.py b/homeassistant/components/keba/binary_sensor.py index 19f3bd428ec..7997130c90a 100644 --- a/homeassistant/components/keba/binary_sensor.py +++ b/homeassistant/components/keba/binary_sensor.py @@ -1,6 +1,8 @@ """Support for KEBA charging station binary sensors.""" from __future__ import annotations +from typing import Any + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -9,7 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN +from . import DOMAIN, KebaHandler async def async_setup_platform( @@ -22,7 +24,7 @@ async def async_setup_platform( if discovery_info is None: return - keba = hass.data[DOMAIN] + keba: KebaHandler = hass.data[DOMAIN] sensors = [ KebaBinarySensor( @@ -60,53 +62,37 @@ async def async_setup_platform( class KebaBinarySensor(BinarySensorEntity): """Representation of a binary sensor of a KEBA charging station.""" - def __init__(self, keba, key, name, entity_type, device_class): + _attr_should_poll = False + + def __init__( + self, + keba: KebaHandler, + key: str, + name: str, + entity_type: str, + device_class: BinarySensorDeviceClass, + ) -> None: """Initialize the KEBA Sensor.""" self._key = key self._keba = keba - self._name = name - self._entity_type = entity_type - self._device_class = device_class - self._is_on = None - self._attributes = {} + self._attributes: dict[str, Any] = {} + + self._attr_device_class = device_class + self._attr_name = f"{keba.device_name} {name}" + self._attr_unique_id = f"{keba.device_id}_{entity_type}" @property - def should_poll(self): - """Deactivate polling. Data updated by KebaHandler.""" - return False - - @property - def unique_id(self): - """Return the unique ID of the binary sensor.""" - return f"{self._keba.device_id}_{self._entity_type}" - - @property - def name(self): - """Return the name of the device.""" - return f"{self._keba.device_name} {self._name}" - - @property - def device_class(self): - """Return the class of this sensor.""" - return self._device_class - - @property - def is_on(self): - """Return true if sensor is on.""" - return self._is_on - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the binary sensor.""" return self._attributes - async def async_update(self): + async def async_update(self) -> None: """Get latest cached states from the device.""" if self._key == "Online": - self._is_on = self._keba.get_value(self._key) + self._attr_is_on = self._keba.get_value(self._key) elif self._key == "Plug": - self._is_on = self._keba.get_value("Plug_plugged") + self._attr_is_on = self._keba.get_value("Plug_plugged") self._attributes["plugged_on_wallbox"] = self._keba.get_value( "Plug_wallbox" ) @@ -114,23 +100,23 @@ class KebaBinarySensor(BinarySensorEntity): self._attributes["plugged_on_EV"] = self._keba.get_value("Plug_EV") elif self._key == "State": - self._is_on = self._keba.get_value("State_on") + self._attr_is_on = self._keba.get_value("State_on") self._attributes["status"] = self._keba.get_value("State_details") self._attributes["max_charging_rate"] = str( self._keba.get_value("Max curr") ) elif self._key == "Tmo FS": - self._is_on = not self._keba.get_value("FS_on") + self._attr_is_on = not self._keba.get_value("FS_on") self._attributes["failsafe_timeout"] = str(self._keba.get_value("Tmo FS")) self._attributes["fallback_current"] = str(self._keba.get_value("Curr FS")) elif self._key == "Authreq": - self._is_on = self._keba.get_value(self._key) == 0 + self._attr_is_on = self._keba.get_value(self._key) == 0 - def update_callback(self): + def update_callback(self) -> None: """Schedule a state update.""" self.async_schedule_update_ha_state(True) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Add update callback after being added to hass.""" self._keba.add_update_listener(self.update_callback) diff --git a/homeassistant/components/keba/lock.py b/homeassistant/components/keba/lock.py index d0316b1e525..de8d28d7739 100644 --- a/homeassistant/components/keba/lock.py +++ b/homeassistant/components/keba/lock.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN +from . import DOMAIN, KebaHandler async def async_setup_platform( @@ -21,41 +21,23 @@ async def async_setup_platform( if discovery_info is None: return - keba = hass.data[DOMAIN] + keba: KebaHandler = hass.data[DOMAIN] - sensors = [KebaLock(keba, "Authentication", "authentication")] - async_add_entities(sensors) + locks = [KebaLock(keba, "Authentication", "authentication")] + async_add_entities(locks) class KebaLock(LockEntity): """The entity class for KEBA charging stations switch.""" - def __init__(self, keba, name, entity_type): + _attr_should_poll = False + + def __init__(self, keba: KebaHandler, name: str, entity_type: str) -> None: """Initialize the KEBA switch.""" self._keba = keba - self._name = name - self._entity_type = entity_type - self._state = True - - @property - def should_poll(self): - """Deactivate polling. Data updated by KebaHandler.""" - return False - - @property - def unique_id(self): - """Return the unique ID of the lock.""" - return f"{self._keba.device_id}_{self._entity_type}" - - @property - def name(self): - """Return the name of the device.""" - return f"{self._keba.device_name} {self._name}" - - @property - def is_locked(self) -> bool: - """Return true if lock is locked.""" - return self._state + self._attr_is_locked = True + self._attr_name = f"{keba.device_name} {name}" + self._attr_unique_id = f"{keba.device_id}_{entity_type}" async def async_lock(self, **kwargs: Any) -> None: """Lock wallbox.""" @@ -65,14 +47,14 @@ class KebaLock(LockEntity): """Unlock wallbox.""" await self._keba.async_start() - async def async_update(self): + async def async_update(self) -> None: """Attempt to retrieve on off state from the switch.""" - self._state = self._keba.get_value("Authreq") == 1 + self._attr_is_locked = self._keba.get_value("Authreq") == 1 - def update_callback(self): + def update_callback(self) -> None: """Schedule a state update.""" self.async_schedule_update_ha_state(True) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Add update callback after being added to hass.""" self._keba.add_update_listener(self.update_callback) diff --git a/homeassistant/components/keba/sensor.py b/homeassistant/components/keba/sensor.py index 6309f0b79fb..d35c22905f1 100644 --- a/homeassistant/components/keba/sensor.py +++ b/homeassistant/components/keba/sensor.py @@ -109,11 +109,11 @@ class KebaSensor(SensorEntity): self._attributes: dict[str, str] = {} @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, str]: """Return the state attributes of the binary sensor.""" return self._attributes - async def async_update(self): + async def async_update(self) -> None: """Get latest cached states from the device.""" self._attr_native_value = self._keba.get_value(self.entity_description.key) @@ -128,10 +128,10 @@ class KebaSensor(SensorEntity): elif self.entity_description.key == "Curr user": self._attributes["max_current_hardware"] = self._keba.get_value("Curr HW") - def update_callback(self): + def update_callback(self) -> None: """Schedule a state update.""" self.async_schedule_update_ha_state(True) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Add update callback after being added to hass.""" self._keba.add_update_listener(self.update_callback) From 9b8c3e37bbee3dbaa949705c7ae7b29f521988e7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 23 Jun 2022 21:38:17 +0200 Subject: [PATCH 1759/3516] Improve group tests (#73630) --- .../components/group/binary_sensor.py | 2 +- tests/components/group/test_binary_sensor.py | 169 ++++++++++----- tests/components/group/test_cover.py | 192 +++++++++++------- tests/components/group/test_fan.py | 96 ++++++--- tests/components/group/test_light.py | 132 ++++++++++-- tests/components/group/test_lock.py | 122 ++++++++--- tests/components/group/test_media_player.py | 77 +++++-- tests/components/group/test_switch.py | 130 ++++++++++-- 8 files changed, 685 insertions(+), 235 deletions(-) diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index 54a98a68e43..ff0e58badfb 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -132,7 +132,7 @@ class BinarySensorGroup(GroupEntity, BinarySensorEntity): # filtered_states are members currently in the state machine filtered_states: list[str] = [x.state for x in all_states if x is not None] - # Set group as unavailable if all members are unavailable + # Set group as unavailable if all members are unavailable or missing self._attr_available = any( state != STATE_UNAVAILABLE for state in filtered_states ) diff --git a/tests/components/group/test_binary_sensor.py b/tests/components/group/test_binary_sensor.py index a0872b11f16..fbc19904faa 100644 --- a/tests/components/group/test_binary_sensor.py +++ b/tests/components/group/test_binary_sensor.py @@ -50,7 +50,13 @@ async def test_default_state(hass): async def test_state_reporting_all(hass): - """Test the state reporting.""" + """Test the state reporting in 'all' mode. + + The group state is unavailable if all group members are unavailable. + Otherwise, the group state is unknown if at least one group member is unknown or unavailable. + Otherwise, the group state is off if at least one group member is off. + Otherwise, the group state is on. + """ await async_setup_component( hass, BINARY_SENSOR_DOMAIN, @@ -68,26 +74,12 @@ async def test_state_reporting_all(hass): await hass.async_start() await hass.async_block_till_done() - hass.states.async_set("binary_sensor.test1", STATE_ON) - hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) - await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN - - hass.states.async_set("binary_sensor.test1", STATE_ON) - hass.states.async_set("binary_sensor.test2", STATE_OFF) - await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF - - hass.states.async_set("binary_sensor.test1", STATE_OFF) - hass.states.async_set("binary_sensor.test2", STATE_OFF) - await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF - - hass.states.async_set("binary_sensor.test1", STATE_ON) - hass.states.async_set("binary_sensor.test2", STATE_ON) - await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON + # Initial state with no group member in the state machine -> unavailable + assert ( + hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNAVAILABLE + ) + # All group members unavailable -> unavailable hass.states.async_set("binary_sensor.test1", STATE_UNAVAILABLE) hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() @@ -95,6 +87,12 @@ async def test_state_reporting_all(hass): hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNAVAILABLE ) + # At least one member unknown or unavailable -> group unknown + hass.states.async_set("binary_sensor.test1", STATE_ON) + hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + hass.states.async_set("binary_sensor.test1", STATE_ON) hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) await hass.async_block_till_done() @@ -105,9 +103,55 @@ async def test_state_reporting_all(hass): await hass.async_block_till_done() assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + hass.states.async_set("binary_sensor.test1", STATE_OFF) + hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + + hass.states.async_set("binary_sensor.test1", STATE_OFF) + hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + + hass.states.async_set("binary_sensor.test1", STATE_UNKNOWN) + hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + + # At least one member off -> group off + hass.states.async_set("binary_sensor.test1", STATE_ON) + hass.states.async_set("binary_sensor.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF + + hass.states.async_set("binary_sensor.test1", STATE_OFF) + hass.states.async_set("binary_sensor.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF + + # Otherwise -> on + hass.states.async_set("binary_sensor.test1", STATE_ON) + hass.states.async_set("binary_sensor.test2", STATE_ON) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON + + # All group members removed from the state machine -> unavailable + hass.states.async_remove("binary_sensor.test1") + hass.states.async_remove("binary_sensor.test2") + await hass.async_block_till_done() + assert ( + hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNAVAILABLE + ) + async def test_state_reporting_any(hass): - """Test the state reporting.""" + """Test the state reporting in 'any' mode. + + The group state is unavailable if all group members are unavailable. + Otherwise, the group state is unknown if all group members are unknown. + Otherwise, the group state is on if at least one group member is on. + Otherwise, the group state is off. + """ await async_setup_component( hass, BINARY_SENSOR_DOMAIN, @@ -126,26 +170,17 @@ async def test_state_reporting_any(hass): await hass.async_start() await hass.async_block_till_done() - hass.states.async_set("binary_sensor.test1", STATE_ON) - hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) - await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("binary_sensor.binary_sensor_group") + assert entry + assert entry.unique_id == "unique_identifier" - hass.states.async_set("binary_sensor.test1", STATE_ON) - hass.states.async_set("binary_sensor.test2", STATE_OFF) - await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON - - hass.states.async_set("binary_sensor.test1", STATE_OFF) - hass.states.async_set("binary_sensor.test2", STATE_OFF) - await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF - - hass.states.async_set("binary_sensor.test1", STATE_ON) - hass.states.async_set("binary_sensor.test2", STATE_ON) - await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON + # Initial state with no group member in the state machine -> unavailable + assert ( + hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNAVAILABLE + ) + # All group members unavailable -> unavailable hass.states.async_set("binary_sensor.test1", STATE_UNAVAILABLE) hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() @@ -153,17 +188,59 @@ async def test_state_reporting_any(hass): hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNAVAILABLE ) - entity_registry = er.async_get(hass) - entry = entity_registry.async_get("binary_sensor.binary_sensor_group") - assert entry - assert entry.unique_id == "unique_identifier" + # All group members unknown -> unknown + hass.states.async_set("binary_sensor.test1", STATE_UNKNOWN) + hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + + # Group members unknown or unavailable -> unknown + hass.states.async_set("binary_sensor.test1", STATE_UNKNOWN) + hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + + # At least one member on -> group on + hass.states.async_set("binary_sensor.test1", STATE_ON) + hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON + + hass.states.async_set("binary_sensor.test1", STATE_ON) + hass.states.async_set("binary_sensor.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON + + hass.states.async_set("binary_sensor.test1", STATE_ON) + hass.states.async_set("binary_sensor.test2", STATE_ON) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON hass.states.async_set("binary_sensor.test1", STATE_ON) hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) await hass.async_block_till_done() assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_ON - hass.states.async_set("binary_sensor.test1", STATE_UNKNOWN) - hass.states.async_set("binary_sensor.test2", STATE_UNKNOWN) + # Otherwise -> off + hass.states.async_set("binary_sensor.test1", STATE_OFF) + hass.states.async_set("binary_sensor.test2", STATE_OFF) await hass.async_block_till_done() - assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNKNOWN + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF + + hass.states.async_set("binary_sensor.test1", STATE_UNKNOWN) + hass.states.async_set("binary_sensor.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF + + hass.states.async_set("binary_sensor.test1", STATE_UNAVAILABLE) + hass.states.async_set("binary_sensor.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("binary_sensor.binary_sensor_group").state == STATE_OFF + + # All group members removed from the state machine -> unavailable + hass.states.async_remove("binary_sensor.test1") + hass.states.async_remove("binary_sensor.test2") + await hass.async_block_till_done() + assert ( + hass.states.get("binary_sensor.binary_sensor_group").state == STATE_UNAVAILABLE + ) diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py index d090141a9d2..83c85a70b63 100644 --- a/tests/components/group/test_cover.py +++ b/tests/components/group/test_cover.py @@ -33,6 +33,7 @@ from homeassistant.const import ( STATE_CLOSING, STATE_OPEN, STATE_OPENING, + STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.helpers import entity_registry as er @@ -99,7 +100,14 @@ async def setup_comp(hass, config_count): @pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) async def test_state(hass, setup_comp): - """Test handling of state.""" + """Test handling of state. + + The group state is unknown if all group members are unknown or unavailable. + Otherwise, the group state is opening if at least one group member is opening. + Otherwise, the group state is closing if at least one group member is closing. + Otherwise, the group state is open if at least one group member is open. + Otherwise, the group state is closed. + """ state = hass.states.get(COVER_GROUP) # No entity has a valid state -> group state unknown assert state.state == STATE_UNKNOWN @@ -115,87 +123,125 @@ async def test_state(hass, setup_comp): 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 + # The group state is unknown if all group members are unknown or unavailable. + for state_1 in (STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_2 in (STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_3 in (STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set(DEMO_COVER, state_1, {}) + hass.states.async_set(DEMO_COVER_POS, state_2, {}) + hass.states.async_set(DEMO_COVER_TILT, state_3, {}) + hass.states.async_set(DEMO_TILT, STATE_UNAVAILABLE, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_UNKNOWN - # 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 + for state_1 in (STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_2 in (STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_3 in (STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set(DEMO_COVER, state_1, {}) + hass.states.async_set(DEMO_COVER_POS, state_2, {}) + hass.states.async_set(DEMO_COVER_TILT, state_3, {}) + 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 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 + # At least one member opening -> group opening + for state_1 in ( + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + for state_2 in ( + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + for state_3 in ( + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + hass.states.async_set(DEMO_COVER, state_1, {}) + hass.states.async_set(DEMO_COVER_POS, state_2, {}) + hass.states.async_set(DEMO_COVER_TILT, state_3, {}) + hass.states.async_set(DEMO_TILT, STATE_OPENING, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_OPENING - # 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 + # At least one member closing -> group closing + for state_1 in ( + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + for state_2 in ( + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + for state_3 in ( + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + hass.states.async_set(DEMO_COVER, state_1, {}) + hass.states.async_set(DEMO_COVER_POS, state_2, {}) + hass.states.async_set(DEMO_COVER_TILT, state_3, {}) + hass.states.async_set(DEMO_TILT, STATE_CLOSING, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_CLOSING - # 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 + # At least one member open -> group open + for state_1 in (STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_2 in (STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_3 in (STATE_CLOSED, STATE_OPEN, STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set(DEMO_COVER, state_1, {}) + hass.states.async_set(DEMO_COVER_POS, state_2, {}) + hass.states.async_set(DEMO_COVER_TILT, state_3, {}) + 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 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, {}) + # At least one member closed -> group closed + for state_1 in (STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_2 in (STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_3 in (STATE_CLOSED, STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set(DEMO_COVER, state_1, {}) + hass.states.async_set(DEMO_COVER_POS, state_2, {}) + hass.states.async_set(DEMO_COVER_TILT, state_3, {}) + 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 + + # All group members removed from the state machine -> unknown + hass.states.async_remove(DEMO_COVER) + hass.states.async_remove(DEMO_COVER_POS) + hass.states.async_remove(DEMO_COVER_TILT) + hass.states.async_remove(DEMO_TILT) 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): diff --git a/tests/components/group/test_fan.py b/tests/components/group/test_fan.py index 19b4fe4670a..8aefd12c93a 100644 --- a/tests/components/group/test_fan.py +++ b/tests/components/group/test_fan.py @@ -33,6 +33,8 @@ from homeassistant.const import ( CONF_UNIQUE_ID, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import CoreState from homeassistant.helpers import entity_registry as er @@ -111,7 +113,11 @@ async def setup_comp(hass, config_count): @pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) async def test_state(hass, setup_comp): - """Test handling of state.""" + """Test handling of state. + + The group state is on if at least one group member is on. + Otherwise, the group state is off. + """ state = hass.states.get(FAN_GROUP) # No entity has a valid state -> group state off assert state.state == STATE_OFF @@ -123,41 +129,55 @@ async def test_state(hass, setup_comp): 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 + # The group state is off if all group members are off, unknown or unavailable. + for state_1 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_2 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_3 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set(CEILING_FAN_ENTITY_ID, state_1, {}) + hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, state_2, {}) + hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, state_3, {}) + 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 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 + for state_1 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_2 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_3 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set(CEILING_FAN_ENTITY_ID, state_1, {}) + hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, state_2, {}) + hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, state_3, {}) + hass.states.async_set( + PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_UNAVAILABLE, {} + ) + 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 + for state_1 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_2 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_3 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set(CEILING_FAN_ENTITY_ID, state_1, {}) + hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, state_2, {}) + hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, state_3, {}) + hass.states.async_set( + PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_UNKNOWN, {} + ) + await hass.async_block_till_done() + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_OFF - # 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 + # At least one member on -> group on + for state_1 in (STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_2 in (STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_3 in (STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set(CEILING_FAN_ENTITY_ID, state_1, {}) + hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, state_2, {}) + hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, state_3, {}) + 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) @@ -167,6 +187,16 @@ async def test_state(hass, setup_comp): assert ATTR_ASSUMED_STATE not in state.attributes assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + # now remove all entities + hass.states.async_remove(CEILING_FAN_ENTITY_ID) + hass.states.async_remove(LIVING_ROOM_FAN_ENTITY_ID) + hass.states.async_remove(PERCENTAGE_FULL_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) diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index d5f7abedb44..f3083812553 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -88,8 +88,14 @@ async def test_default_state(hass): assert entry.unique_id == "unique_identifier" -async def test_state_reporting(hass): - """Test the state reporting.""" +async def test_state_reporting_any(hass): + """Test the state reporting in 'any' mode. + + The group state is unavailable if all group members are unavailable. + Otherwise, the group state is unknown if all group members are unknown. + Otherwise, the group state is on if at least one group member is on. + Otherwise, the group state is off. + """ await async_setup_component( hass, LIGHT_DOMAIN, @@ -105,29 +111,79 @@ async def test_state_reporting(hass): await hass.async_start() await hass.async_block_till_done() - hass.states.async_set("light.test1", STATE_ON) - hass.states.async_set("light.test2", STATE_UNAVAILABLE) - await hass.async_block_till_done() - assert hass.states.get("light.light_group").state == STATE_ON - - hass.states.async_set("light.test1", STATE_ON) - hass.states.async_set("light.test2", STATE_OFF) - await hass.async_block_till_done() - assert hass.states.get("light.light_group").state == STATE_ON - - hass.states.async_set("light.test1", STATE_OFF) - hass.states.async_set("light.test2", STATE_OFF) - await hass.async_block_till_done() - assert hass.states.get("light.light_group").state == STATE_OFF + # Initial state with no group member in the state machine -> unavailable + assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE + # All group members unavailable -> unavailable hass.states.async_set("light.test1", STATE_UNAVAILABLE) hass.states.async_set("light.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE + # All group members unknown -> unknown + hass.states.async_set("light.test1", STATE_UNKNOWN) + hass.states.async_set("light.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNKNOWN + + # Group members unknown or unavailable -> unknown + hass.states.async_set("light.test1", STATE_UNKNOWN) + hass.states.async_set("light.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNKNOWN + + # At least one member on -> group on + hass.states.async_set("light.test1", STATE_ON) + hass.states.async_set("light.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_ON + + hass.states.async_set("light.test1", STATE_ON) + hass.states.async_set("light.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_ON + + hass.states.async_set("light.test1", STATE_ON) + hass.states.async_set("light.test2", STATE_ON) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_ON + + hass.states.async_set("light.test1", STATE_ON) + hass.states.async_set("light.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_ON + + # Otherwise -> off + hass.states.async_set("light.test1", STATE_OFF) + hass.states.async_set("light.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_OFF + + hass.states.async_set("light.test1", STATE_UNKNOWN) + hass.states.async_set("light.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_OFF + + hass.states.async_set("light.test1", STATE_UNAVAILABLE) + hass.states.async_set("light.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_OFF + + # All group members removed from the state machine -> unavailable + hass.states.async_remove("light.test1") + hass.states.async_remove("light.test2") + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE + async def test_state_reporting_all(hass): - """Test the state reporting.""" + """Test the state reporting in 'all' mode. + + The group state is unavailable if all group members are unavailable. + Otherwise, the group state is unknown if at least one group member is unknown or unavailable. + Otherwise, the group state is off if at least one group member is off. + Otherwise, the group state is on. + """ await async_setup_component( hass, LIGHT_DOMAIN, @@ -143,11 +199,47 @@ async def test_state_reporting_all(hass): await hass.async_start() await hass.async_block_till_done() + # Initial state with no group member in the state machine -> unavailable + assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE + + # All group members unavailable -> unavailable + hass.states.async_set("light.test1", STATE_UNAVAILABLE) + hass.states.async_set("light.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE + + # At least one member unknown or unavailable -> group unknown hass.states.async_set("light.test1", STATE_ON) hass.states.async_set("light.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() assert hass.states.get("light.light_group").state == STATE_UNKNOWN + hass.states.async_set("light.test1", STATE_ON) + hass.states.async_set("light.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNKNOWN + + hass.states.async_set("light.test1", STATE_UNKNOWN) + hass.states.async_set("light.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNKNOWN + + hass.states.async_set("light.test1", STATE_OFF) + hass.states.async_set("light.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNKNOWN + + hass.states.async_set("light.test1", STATE_OFF) + hass.states.async_set("light.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNKNOWN + + hass.states.async_set("binary_sensor.test1", STATE_UNKNOWN) + hass.states.async_set("binary_sensor.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("light.light_group").state == STATE_UNKNOWN + + # At least one member off -> group off hass.states.async_set("light.test1", STATE_ON) hass.states.async_set("light.test2", STATE_OFF) await hass.async_block_till_done() @@ -158,13 +250,15 @@ async def test_state_reporting_all(hass): await hass.async_block_till_done() assert hass.states.get("light.light_group").state == STATE_OFF + # Otherwise -> on hass.states.async_set("light.test1", STATE_ON) hass.states.async_set("light.test2", STATE_ON) await hass.async_block_till_done() assert hass.states.get("light.light_group").state == STATE_ON - hass.states.async_set("light.test1", STATE_UNAVAILABLE) - hass.states.async_set("light.test2", STATE_UNAVAILABLE) + # All group members removed from the state machine -> unavailable + hass.states.async_remove("light.test1") + hass.states.async_remove("light.test2") await hass.async_block_till_done() assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE diff --git a/tests/components/group/test_lock.py b/tests/components/group/test_lock.py index 8db28fab18e..e76e47577c6 100644 --- a/tests/components/group/test_lock.py +++ b/tests/components/group/test_lock.py @@ -57,7 +57,16 @@ async def test_default_state(hass): async def test_state_reporting(hass): - """Test the state reporting.""" + """Test the state reporting. + + The group state is unavailable if all group members are unavailable. + Otherwise, the group state is unknown if at least one group member is unknown or unavailable. + Otherwise, the group state is jammed if at least one group member is jammed. + Otherwise, the group state is locking if at least one group member is locking. + Otherwise, the group state is unlocking if at least one group member is unlocking. + Otherwise, the group state is unlocked if at least one group member is unlocked. + Otherwise, the group state is locked. + """ await async_setup_component( hass, LOCK_DOMAIN, @@ -72,43 +81,98 @@ async def test_state_reporting(hass): await hass.async_start() await hass.async_block_till_done() - hass.states.async_set("lock.test1", STATE_LOCKED) + # Initial state with no group member in the state machine -> unavailable + assert hass.states.get("lock.lock_group").state == STATE_UNAVAILABLE + + # All group members unavailable -> unavailable + hass.states.async_set("lock.test1", STATE_UNAVAILABLE) hass.states.async_set("lock.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() - assert hass.states.get("lock.lock_group").state == STATE_UNKNOWN + assert hass.states.get("lock.lock_group").state == STATE_UNAVAILABLE - hass.states.async_set("lock.test1", STATE_LOCKED) - hass.states.async_set("lock.test2", STATE_UNLOCKED) - await hass.async_block_till_done() - assert hass.states.get("lock.lock_group").state == STATE_UNLOCKED + # At least one member unknown or unavailable -> group unknown + for state_1 in ( + STATE_JAMMED, + STATE_LOCKED, + STATE_LOCKING, + STATE_UNKNOWN, + STATE_UNLOCKED, + STATE_UNLOCKING, + ): + hass.states.async_set("lock.test1", state_1) + hass.states.async_set("lock.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_UNKNOWN + for state_1 in ( + STATE_JAMMED, + STATE_LOCKED, + STATE_LOCKING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + STATE_UNLOCKED, + STATE_UNLOCKING, + ): + hass.states.async_set("lock.test1", state_1) + hass.states.async_set("lock.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_UNKNOWN + + # At least one member jammed -> group jammed + for state_1 in ( + STATE_JAMMED, + STATE_LOCKED, + STATE_LOCKING, + STATE_UNLOCKED, + STATE_UNLOCKING, + ): + hass.states.async_set("lock.test1", state_1) + hass.states.async_set("lock.test2", STATE_JAMMED) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_JAMMED + + # At least one member locking -> group unlocking + for state_1 in ( + STATE_LOCKED, + STATE_LOCKING, + STATE_UNLOCKED, + STATE_UNLOCKING, + ): + hass.states.async_set("lock.test1", state_1) + hass.states.async_set("lock.test2", STATE_LOCKING) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_LOCKING + + # At least one member unlocking -> group unlocking + for state_1 in ( + STATE_LOCKED, + STATE_UNLOCKED, + STATE_UNLOCKING, + ): + hass.states.async_set("lock.test1", state_1) + hass.states.async_set("lock.test2", STATE_UNLOCKING) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_UNLOCKING + + # At least one member unlocked -> group unlocked + for state_1 in ( + STATE_LOCKED, + STATE_UNLOCKED, + ): + hass.states.async_set("lock.test1", state_1) + hass.states.async_set("lock.test2", STATE_UNLOCKED) + await hass.async_block_till_done() + assert hass.states.get("lock.lock_group").state == STATE_UNLOCKED + + # Otherwise -> locked hass.states.async_set("lock.test1", STATE_LOCKED) hass.states.async_set("lock.test2", STATE_LOCKED) await hass.async_block_till_done() assert hass.states.get("lock.lock_group").state == STATE_LOCKED - hass.states.async_set("lock.test1", STATE_UNLOCKED) - hass.states.async_set("lock.test2", STATE_UNLOCKED) - await hass.async_block_till_done() - assert hass.states.get("lock.lock_group").state == STATE_UNLOCKED - - hass.states.async_set("lock.test1", STATE_UNLOCKED) - hass.states.async_set("lock.test2", STATE_JAMMED) - await hass.async_block_till_done() - assert hass.states.get("lock.lock_group").state == STATE_JAMMED - - hass.states.async_set("lock.test1", STATE_LOCKED) - hass.states.async_set("lock.test2", STATE_UNLOCKING) - await hass.async_block_till_done() - assert hass.states.get("lock.lock_group").state == STATE_UNLOCKING - - hass.states.async_set("lock.test1", STATE_UNLOCKED) - hass.states.async_set("lock.test2", STATE_LOCKING) - await hass.async_block_till_done() - assert hass.states.get("lock.lock_group").state == STATE_LOCKING - - hass.states.async_set("lock.test1", STATE_UNAVAILABLE) - hass.states.async_set("lock.test2", STATE_UNAVAILABLE) + # All group members removed from the state machine -> unavailable + hass.states.async_remove("lock.test1") + hass.states.async_remove("lock.test2") await hass.async_block_till_done() assert hass.states.get("lock.lock_group").state == STATE_UNAVAILABLE diff --git a/tests/components/group/test_media_player.py b/tests/components/group/test_media_player.py index f741e2d1a84..85e75ffcba6 100644 --- a/tests/components/group/test_media_player.py +++ b/tests/components/group/test_media_player.py @@ -43,6 +43,8 @@ from homeassistant.const import ( SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, SERVICE_VOLUME_UP, + STATE_BUFFERING, + STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, @@ -99,7 +101,17 @@ async def test_default_state(hass): async def test_state_reporting(hass): - """Test the state reporting.""" + """Test the state reporting. + + The group state is unavailable if all group members are unavailable. + Otherwise, the group state is unknown if all group members are unknown. + Otherwise, the group state is buffering if all group members are buffering. + Otherwise, the group state is idle if all group members are idle. + Otherwise, the group state is paused if all group members are paused. + Otherwise, the group state is playing if all group members are playing. + Otherwise, the group state is on if at least one group member is not off, unavailable or unknown. + Otherwise, the group state is off. + """ await async_setup_component( hass, MEDIA_DOMAIN, @@ -114,27 +126,60 @@ async def test_state_reporting(hass): await hass.async_start() await hass.async_block_till_done() + # Initial state with no group member in the state machine -> unknown assert hass.states.get("media_player.media_group").state == STATE_UNKNOWN - hass.states.async_set("media_player.player_1", STATE_ON) - hass.states.async_set("media_player.player_2", STATE_UNAVAILABLE) - await hass.async_block_till_done() - assert hass.states.get("media_player.media_group").state == STATE_ON + # All group members buffering -> buffering + # All group members idle -> idle + # All group members paused -> paused + # All group members playing -> playing + # All group members unavailable -> unavailable + # All group members unknown -> unknown + for state in ( + STATE_BUFFERING, + STATE_IDLE, + STATE_PAUSED, + STATE_PLAYING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + hass.states.async_set("media_player.player_1", state) + hass.states.async_set("media_player.player_2", state) + await hass.async_block_till_done() + assert hass.states.get("media_player.media_group").state == state - hass.states.async_set("media_player.player_1", STATE_ON) - hass.states.async_set("media_player.player_2", STATE_OFF) - await hass.async_block_till_done() - assert hass.states.get("media_player.media_group").state == STATE_ON + # At least one member not off, unavailable or unknown -> on + for state_1 in (STATE_BUFFERING, STATE_IDLE, STATE_ON, STATE_PAUSED, STATE_PLAYING): + for state_2 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set("media_player.player_1", state_1) + hass.states.async_set("media_player.player_2", state_2) + await hass.async_block_till_done() + assert hass.states.get("media_player.media_group").state == STATE_ON - hass.states.async_set("media_player.player_1", STATE_OFF) - hass.states.async_set("media_player.player_2", STATE_UNAVAILABLE) - await hass.async_block_till_done() - assert hass.states.get("media_player.media_group").state == STATE_OFF + for state_1 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): + hass.states.async_set("media_player.player_1", state_1) + hass.states.async_set("media_player.player_2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("media_player.media_group").state == STATE_OFF - hass.states.async_set("media_player.player_1", STATE_UNAVAILABLE) - hass.states.async_set("media_player.player_2", STATE_UNAVAILABLE) + # Otherwise off + for state_1 in (STATE_OFF, STATE_UNKNOWN): + hass.states.async_set("media_player.player_1", state_1) + hass.states.async_set("media_player.player_2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("media_player.media_group").state == STATE_OFF + + for state_1 in (STATE_OFF, STATE_UNAVAILABLE): + hass.states.async_set("media_player.player_1", state_1) + hass.states.async_set("media_player.player_2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("media_player.media_group").state == STATE_OFF + + # All group members removed from the state machine -> unknown + hass.states.async_remove("media_player.player_1") + hass.states.async_remove("media_player.player_2") await hass.async_block_till_done() - assert hass.states.get("media_player.media_group").state == STATE_UNAVAILABLE + assert hass.states.get("media_player.media_group").state == STATE_UNKNOWN async def test_supported_features(hass): diff --git a/tests/components/group/test_switch.py b/tests/components/group/test_switch.py index 5df2542d101..9a8da274a0a 100644 --- a/tests/components/group/test_switch.py +++ b/tests/components/group/test_switch.py @@ -56,7 +56,13 @@ async def test_default_state(hass): async def test_state_reporting(hass): - """Test the state reporting.""" + """Test the state reporting in 'any' mode. + + The group state is unavailable if all group members are unavailable. + Otherwise, the group state is unknown if all group members are unknown. + Otherwise, the group state is on if at least one group member is on. + Otherwise, the group state is off. + """ await async_setup_component( hass, SWITCH_DOMAIN, @@ -72,29 +78,79 @@ async def test_state_reporting(hass): await hass.async_start() await hass.async_block_till_done() - hass.states.async_set("switch.test1", STATE_ON) - hass.states.async_set("switch.test2", STATE_UNAVAILABLE) - await hass.async_block_till_done() - assert hass.states.get("switch.switch_group").state == STATE_ON - - hass.states.async_set("switch.test1", STATE_ON) - hass.states.async_set("switch.test2", STATE_OFF) - await hass.async_block_till_done() - assert hass.states.get("switch.switch_group").state == STATE_ON - - hass.states.async_set("switch.test1", STATE_OFF) - hass.states.async_set("switch.test2", STATE_OFF) - await hass.async_block_till_done() - assert hass.states.get("switch.switch_group").state == STATE_OFF + # Initial state with no group member in the state machine -> unavailable + assert hass.states.get("switch.switch_group").state == STATE_UNAVAILABLE + # All group members unavailable -> unavailable hass.states.async_set("switch.test1", STATE_UNAVAILABLE) hass.states.async_set("switch.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() assert hass.states.get("switch.switch_group").state == STATE_UNAVAILABLE + # All group members unknown -> unknown + hass.states.async_set("switch.test1", STATE_UNKNOWN) + hass.states.async_set("switch.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNKNOWN + + # Group members unknown or unavailable -> unknown + hass.states.async_set("switch.test1", STATE_UNKNOWN) + hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNKNOWN + + # At least one member on -> group on + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_ON + + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_ON + + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_ON) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_ON + + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_ON + + # Otherwise -> off + hass.states.async_set("switch.test1", STATE_OFF) + hass.states.async_set("switch.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_OFF + + hass.states.async_set("switch.test1", STATE_UNKNOWN) + hass.states.async_set("switch.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_OFF + + hass.states.async_set("switch.test1", STATE_UNAVAILABLE) + hass.states.async_set("switch.test2", STATE_OFF) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_OFF + + # All group members removed from the state machine -> unavailable + hass.states.async_remove("switch.test1") + hass.states.async_remove("switch.test2") + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNAVAILABLE + async def test_state_reporting_all(hass): - """Test the state reporting.""" + """Test the state reporting in 'all' mode. + + The group state is unavailable if all group members are unavailable. + Otherwise, the group state is unknown if at least one group member is unknown or unavailable. + Otherwise, the group state is off if at least one group member is off. + Otherwise, the group state is on. + """ await async_setup_component( hass, SWITCH_DOMAIN, @@ -110,11 +166,47 @@ async def test_state_reporting_all(hass): await hass.async_start() await hass.async_block_till_done() + # Initial state with no group member in the state machine -> unavailable + assert hass.states.get("switch.switch_group").state == STATE_UNAVAILABLE + + # All group members unavailable -> unavailable + hass.states.async_set("switch.test1", STATE_UNAVAILABLE) + hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNAVAILABLE + + # At least one member unknown or unavailable -> group unknown hass.states.async_set("switch.test1", STATE_ON) hass.states.async_set("switch.test2", STATE_UNAVAILABLE) await hass.async_block_till_done() assert hass.states.get("switch.switch_group").state == STATE_UNKNOWN + hass.states.async_set("switch.test1", STATE_ON) + hass.states.async_set("switch.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNKNOWN + + hass.states.async_set("switch.test1", STATE_UNKNOWN) + hass.states.async_set("switch.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNKNOWN + + hass.states.async_set("switch.test1", STATE_OFF) + hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNKNOWN + + hass.states.async_set("switch.test1", STATE_OFF) + hass.states.async_set("switch.test2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNKNOWN + + hass.states.async_set("switch.test1", STATE_UNKNOWN) + hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("switch.switch_group").state == STATE_UNKNOWN + + # At least one member off -> group off hass.states.async_set("switch.test1", STATE_ON) hass.states.async_set("switch.test2", STATE_OFF) await hass.async_block_till_done() @@ -125,13 +217,15 @@ async def test_state_reporting_all(hass): await hass.async_block_till_done() assert hass.states.get("switch.switch_group").state == STATE_OFF + # Otherwise -> on hass.states.async_set("switch.test1", STATE_ON) hass.states.async_set("switch.test2", STATE_ON) await hass.async_block_till_done() assert hass.states.get("switch.switch_group").state == STATE_ON - hass.states.async_set("switch.test1", STATE_UNAVAILABLE) - hass.states.async_set("switch.test2", STATE_UNAVAILABLE) + # All group members removed from the state machine -> unavailable + hass.states.async_remove("switch.test1") + hass.states.async_remove("switch.test2") await hass.async_block_till_done() assert hass.states.get("switch.switch_group").state == STATE_UNAVAILABLE From 3058a432a59579a5a9fbd71c8de127981d06d768 Mon Sep 17 00:00:00 2001 From: 0bmay <57501269+0bmay@users.noreply.github.com> Date: Thu, 23 Jun 2022 14:33:03 -0700 Subject: [PATCH 1760/3516] Bump py-canary to 0.5.3 (#73922) --- homeassistant/components/canary/alarm_control_panel.py | 8 ++------ homeassistant/components/canary/camera.py | 2 +- homeassistant/components/canary/coordinator.py | 3 ++- homeassistant/components/canary/manifest.json | 2 +- homeassistant/components/canary/model.py | 2 +- homeassistant/components/canary/sensor.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/canary/__init__.py | 2 +- tests/components/canary/test_alarm_control_panel.py | 2 +- 10 files changed, 12 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index d33e98008d6..f668da25e2e 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -3,12 +3,8 @@ from __future__ import annotations from typing import Any -from canary.api import ( - LOCATION_MODE_AWAY, - LOCATION_MODE_HOME, - LOCATION_MODE_NIGHT, - Location, -) +from canary.const import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT +from canary.model import Location from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntity, diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 5caae6f5cc5..44bac9e3bde 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -5,8 +5,8 @@ from datetime import timedelta from typing import Final from aiohttp.web import Request, StreamResponse -from canary.api import Device, Location from canary.live_stream_api import LiveStreamSession +from canary.model import Device, Location from haffmpeg.camera import CameraMjpeg import voluptuous as vol diff --git a/homeassistant/components/canary/coordinator.py b/homeassistant/components/canary/coordinator.py index 4c6c9ce5777..b2a8ef4daaa 100644 --- a/homeassistant/components/canary/coordinator.py +++ b/homeassistant/components/canary/coordinator.py @@ -6,7 +6,8 @@ from datetime import timedelta import logging from async_timeout import timeout -from canary.api import Api, Location +from canary.api import Api +from canary.model import Location from requests.exceptions import ConnectTimeout, HTTPError from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/canary/manifest.json b/homeassistant/components/canary/manifest.json index fdae5c83d7b..bf7ceaec273 100644 --- a/homeassistant/components/canary/manifest.json +++ b/homeassistant/components/canary/manifest.json @@ -2,7 +2,7 @@ "domain": "canary", "name": "Canary", "documentation": "https://www.home-assistant.io/integrations/canary", - "requirements": ["py-canary==0.5.2"], + "requirements": ["py-canary==0.5.3"], "dependencies": ["ffmpeg"], "codeowners": [], "config_flow": true, diff --git a/homeassistant/components/canary/model.py b/homeassistant/components/canary/model.py index 848278d9aec..12fb8209108 100644 --- a/homeassistant/components/canary/model.py +++ b/homeassistant/components/canary/model.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import ValuesView from typing import Optional, TypedDict -from canary.api import Location +from canary.model import Location class CanaryData(TypedDict): diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 3de088016a9..c80c178fbb5 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Final -from canary.api import Device, Location, SensorType +from canary.model import Device, Location, SensorType from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry diff --git a/requirements_all.txt b/requirements_all.txt index ba18173bcac..fc14180138f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1296,7 +1296,7 @@ pushover_complete==1.1.1 pvo==0.2.2 # homeassistant.components.canary -py-canary==0.5.2 +py-canary==0.5.3 # homeassistant.components.cpuspeed py-cpuinfo==8.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8545067551e..0f3fc63c76f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -880,7 +880,7 @@ pushbullet.py==0.11.0 pvo==0.2.2 # homeassistant.components.canary -py-canary==0.5.2 +py-canary==0.5.3 # homeassistant.components.cpuspeed py-cpuinfo==8.0.0 diff --git a/tests/components/canary/__init__.py b/tests/components/canary/__init__.py index b327fb0ebcb..46737929dc5 100644 --- a/tests/components/canary/__init__.py +++ b/tests/components/canary/__init__.py @@ -1,7 +1,7 @@ """Tests for the Canary integration.""" from unittest.mock import MagicMock, PropertyMock, patch -from canary.api import SensorType +from canary.model import SensorType from homeassistant.components.canary.const import ( CONF_FFMPEG_ARGUMENTS, diff --git a/tests/components/canary/test_alarm_control_panel.py b/tests/components/canary/test_alarm_control_panel.py index 5034792d389..f6eed94e267 100644 --- a/tests/components/canary/test_alarm_control_panel.py +++ b/tests/components/canary/test_alarm_control_panel.py @@ -1,7 +1,7 @@ """The tests for the Canary alarm_control_panel platform.""" from unittest.mock import PropertyMock, patch -from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT +from canary.const import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN from homeassistant.components.canary import DOMAIN From 28dd92d92888e672917556ae77a7174992a055b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Jun 2022 16:35:10 -0500 Subject: [PATCH 1761/3516] Fix logbook state query with postgresql (#73924) --- homeassistant/components/logbook/processor.py | 4 ++-- homeassistant/components/logbook/queries/common.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index 82225df8364..6d491ec2892 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -377,9 +377,9 @@ def _rows_match(row: Row | EventAsRow, other_row: Row | EventAsRow) -> bool: """Check of rows match by using the same method as Events __hash__.""" if ( row is other_row - or (state_id := row.state_id) is not None + or (state_id := row.state_id) and state_id == other_row.state_id - or (event_id := row.event_id) is not None + or (event_id := row.event_id) and event_id == other_row.event_id ): return True diff --git a/homeassistant/components/logbook/queries/common.py b/homeassistant/components/logbook/queries/common.py index 5b79f6e0d32..466df668da8 100644 --- a/homeassistant/components/logbook/queries/common.py +++ b/homeassistant/components/logbook/queries/common.py @@ -87,7 +87,7 @@ EVENT_COLUMNS_FOR_STATE_SELECT = [ ] EMPTY_STATE_COLUMNS = ( - literal(value=None, type_=sqlalchemy.String).label("state_id"), + literal(value=0, type_=sqlalchemy.Integer).label("state_id"), literal(value=None, type_=sqlalchemy.String).label("state"), literal(value=None, type_=sqlalchemy.String).label("entity_id"), literal(value=None, type_=sqlalchemy.String).label("icon"), From dc0ea6fd559d5ad12482fcc291cfac97550ce489 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 24 Jun 2022 01:57:12 +0200 Subject: [PATCH 1762/3516] Flush CI caches (#73926) --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 27a761872d9..49f62f0943d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,8 +20,8 @@ on: type: boolean env: - CACHE_VERSION: 9 - PIP_CACHE_VERSION: 3 + CACHE_VERSION: 10 + PIP_CACHE_VERSION: 4 HA_SHORT_VERSION: 2022.7 DEFAULT_PYTHON: 3.9 PRE_COMMIT_CACHE: ~/.cache/pre-commit From 768e53ac2d0509ff2f2c7af17820b4ef4772f019 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 23 Jun 2022 20:13:37 -0400 Subject: [PATCH 1763/3516] Add zwave_js/get_any_firmware_update_progress WS cmd (#73905) --- homeassistant/components/zwave_js/api.py | 27 +++++++++ tests/components/zwave_js/test_api.py | 71 ++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index ca25088a642..8b98c61c4b2 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -421,6 +421,9 @@ def async_register_api(hass: HomeAssistant) -> None: websocket_api.async_register_command( hass, websocket_get_firmware_update_capabilities ) + websocket_api.async_register_command( + hass, websocket_get_any_firmware_update_progress + ) websocket_api.async_register_command(hass, websocket_check_for_config_updates) websocket_api.async_register_command(hass, websocket_install_config_update) websocket_api.async_register_command( @@ -1989,6 +1992,30 @@ async def websocket_get_firmware_update_capabilities( connection.send_result(msg[ID], capabilities.to_dict()) +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/get_any_firmware_update_progress", + vol.Required(ENTRY_ID): str, + } +) +@websocket_api.async_response +@async_handle_failed_command +@async_get_entry +async def websocket_get_any_firmware_update_progress( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, + driver: Driver, +) -> None: + """Get whether any firmware updates are in progress.""" + connection.send_result( + msg[ID], await driver.controller.async_get_any_firmware_update_progress() + ) + + class FirmwareUploadView(HomeAssistantView): """View to upload firmware.""" diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 1ed125cc43a..3a2bff54e88 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -3723,6 +3723,77 @@ async def test_get_firmware_update_capabilities( assert msg["error"]["code"] == ERR_NOT_FOUND +async def test_get_any_firmware_update_progress( + hass, client, integration, hass_ws_client +): + """Test that the get_any_firmware_update_progress WS API call works.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"progress": True} + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/get_any_firmware_update_progress", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "controller.get_any_firmware_update_progress" + + # Test FailedZWaveCommand is caught + with patch( + "zwave_js_server.model.controller.Controller.async_get_any_firmware_update_progress", + side_effect=FailedZWaveCommand("failed_command", 1, "error message"), + ): + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/get_any_firmware_update_progress", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_error" + assert msg["error"]["message"] == "Z-Wave error 1: error message" + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/get_any_firmware_update_progress", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + # Test sending command with improper device ID fails + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/get_any_firmware_update_progress", + ENTRY_ID: "invalid_entry", + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + async def test_check_for_config_updates(hass, client, integration, hass_ws_client): """Test that the check_for_config_updates WS API call works.""" entry = integration From c607994fbeb1a746ebd2319754a4c3bb43c3cee7 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 24 Jun 2022 00:23:27 +0000 Subject: [PATCH 1764/3516] [ci skip] Translation update --- .../components/google/translations/it.json | 1 + .../components/google/translations/ja.json | 1 + .../components/google/translations/no.json | 1 + .../components/google/translations/pl.json | 1 + .../components/hue/translations/no.json | 2 +- .../components/nest/translations/it.json | 9 +++--- .../components/nest/translations/ja.json | 1 + .../components/nest/translations/no.json | 32 ++++++++++++++++++- .../components/nest/translations/pl.json | 29 ++++++++++++++++- .../overkiz/translations/sensor.it.json | 5 +++ .../overkiz/translations/sensor.ja.json | 5 +++ .../overkiz/translations/sensor.no.json | 5 +++ .../overkiz/translations/sensor.pl.json | 5 +++ .../components/scrape/translations/it.json | 4 +-- .../transmission/translations/it.json | 10 +++++- .../transmission/translations/ja.json | 10 +++++- .../transmission/translations/no.json | 10 +++++- .../transmission/translations/pl.json | 10 +++++- 18 files changed, 128 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/google/translations/it.json b/homeassistant/components/google/translations/it.json index a0f9d140329..ef5ec01202d 100644 --- a/homeassistant/components/google/translations/it.json +++ b/homeassistant/components/google/translations/it.json @@ -6,6 +6,7 @@ "abort": { "already_configured": "L'account \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "cannot_connect": "Impossibile connettersi", "code_expired": "Il codice di autenticazione \u00e8 scaduto o la configurazione delle credenziali non \u00e8 valida, riprova.", "invalid_access_token": "Token di accesso non valido", "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", diff --git a/homeassistant/components/google/translations/ja.json b/homeassistant/components/google/translations/ja.json index d5b52c197a8..4cb05958a76 100644 --- a/homeassistant/components/google/translations/ja.json +++ b/homeassistant/components/google/translations/ja.json @@ -6,6 +6,7 @@ "abort": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "code_expired": "\u8a8d\u8a3c\u30b3\u30fc\u30c9\u306e\u6709\u52b9\u671f\u9650\u304c\u5207\u308c\u3066\u3044\u308b\u304b\u3001\u8cc7\u683c\u60c5\u5831\u306e\u8a2d\u5b9a\u304c\u7121\u52b9\u3067\u3059\u3002\u3082\u3046\u4e00\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", "invalid_access_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", diff --git a/homeassistant/components/google/translations/no.json b/homeassistant/components/google/translations/no.json index 4fcbeb1dae8..9842da0362c 100644 --- a/homeassistant/components/google/translations/no.json +++ b/homeassistant/components/google/translations/no.json @@ -6,6 +6,7 @@ "abort": { "already_configured": "Kontoen er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "cannot_connect": "Tilkobling mislyktes", "code_expired": "Autentiseringskoden er utl\u00f8pt eller p\u00e5loggingsoppsettet er ugyldig. Pr\u00f8v p\u00e5 nytt.", "invalid_access_token": "Ugyldig tilgangstoken", "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", diff --git a/homeassistant/components/google/translations/pl.json b/homeassistant/components/google/translations/pl.json index 8013e775e62..3f78e3e9d6f 100644 --- a/homeassistant/components/google/translations/pl.json +++ b/homeassistant/components/google/translations/pl.json @@ -6,6 +6,7 @@ "abort": { "already_configured": "Konto jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "code_expired": "Kod uwierzytelniaj\u0105cy wygas\u0142 lub konfiguracja po\u015bwiadcze\u0144 jest nieprawid\u0142owa, spr\u00f3buj ponownie.", "invalid_access_token": "Niepoprawny token dost\u0119pu", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", diff --git a/homeassistant/components/hue/translations/no.json b/homeassistant/components/hue/translations/no.json index 9c18003aca0..d34899db978 100644 --- a/homeassistant/components/hue/translations/no.json +++ b/homeassistant/components/hue/translations/no.json @@ -5,7 +5,7 @@ "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "cannot_connect": "Tilkobling mislyktes", - "discover_timeout": "Kunne ikke oppdage Hue Bridger", + "discover_timeout": "Fant ingen Hue Bridger", "invalid_host": "Ugyldig vert", "no_bridges": "Ingen Philips Hue Bridger oppdaget", "not_hue_bridge": "Ikke en Hue bro", diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 8fd83483290..8979631dec0 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -4,6 +4,7 @@ }, "config": { "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "invalid_access_token": "Token di accesso non valido", "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", @@ -33,7 +34,7 @@ "title": "Connetti l'account Google" }, "auth_upgrade": { - "description": "App Auth \u00e8 stato ritirato da Google per migliorare la sicurezza e devi agire creando nuove credenziali per l'applicazione. \n\nApri la [documentazione]({more_info_url}) per seguire i passaggi successivi che ti guideranno attraverso i passaggi necessari per ripristinare l'accesso ai tuoi dispositivi Nest.", + "description": "App Auth \u00e8 stato ritirato da Google per migliorare la sicurezza ed \u00e8 necessario intervenire creando nuove credenziali per l'applicazione. \n\nApri la [documentazione]({more_info_url}) per seguire i passaggi successivi che ti guideranno attraverso gli stadi necessari per ripristinare l'accesso ai tuoi dispositivi Nest.", "title": "Nest: ritiro dell'autenticazione dell'app" }, "cloud_project": { @@ -44,18 +45,18 @@ "title": "Nest: inserisci l'ID del progetto Cloud" }, "create_cloud_project": { - "description": "L'integrazione Nest ti consente di integrare i tuoi termostati, videocamere e campanelli Nest utilizzando l'API Smart Device Management. L'API SDM **richiede una tariffa di configurazione una tantum di US $ 5**. Consulta la documentazione per [maggiori informazioni]({more_info_url}). \n\n 1. Vai a [Google Cloud Console]( {cloud_console_url} ).\n 2. Se questo \u00e8 il tuo primo progetto, fai clic su **Crea progetto** e poi su **Nuovo progetto**.\n 3. Assegna un nome al tuo progetto cloud, quindi fai clic su **Crea**.\n 4. Salva l'ID del progetto cloud, ad es. *example-project-12345*, poich\u00e9 ti servir\u00e0 in seguito\n 5. Vai a Libreria API per [API Smart Device Management]({sdm_api_url}) e fai clic su **Abilita**.\n 6. Vai a Libreria API per [Cloud Pub/Sub API]({pubsub_api_url}) e fai clic su **Abilita**. \n\n Procedi quando il tuo progetto cloud \u00e8 impostato.", + "description": "L'integrazione Nest ti consente di integrare i tuoi termostati, videocamere e campanelli Nest utilizzando l'API Smart Device Management. L'API SDM **richiede una tariffa di configurazione una tantum di 5 $ USD**. Consulta la documentazione per [maggiori informazioni]({more_info_url}). \n\n 1. Vai su [Google Cloud Console]({cloud_console_url}).\n 2. Se questo \u00e8 il tuo primo progetto, fai clic su **Crea progetto** e poi su **Nuovo progetto**.\n 3. Assegna un nome al tuo progetto cloud, quindi fai clic su **Crea**.\n 4. Salva l'ID del progetto cloud, ad es. *example-project-12345*, poich\u00e9 ti servir\u00e0 in seguito.\n 5. Vai su Libreria API per [API Smart Device Management]({sdm_api_url}) e fai clic su **Abilita**.\n 6. Vai su Libreria API per [Cloud Pub/Sub API]({pubsub_api_url}) e fai clic su **Abilita**. \n\nProcedi quando il tuo progetto cloud \u00e8 impostato.", "title": "Nest: crea e configura un progetto cloud" }, "device_project": { "data": { "project_id": "ID progetto di accesso al dispositivo" }, - "description": "Crea un progetto di accesso al dispositivo Nest la cui configurazione **richiede una commissione di $ 5 USD**.\n 1. Vai alla [Console di accesso al dispositivo]({device_access_console_url}) e attraverso il flusso di pagamento.\n 2. Clicca su **Crea progetto**\n 3. Assegna un nome al progetto di accesso al dispositivo e fai clic su **Avanti**.\n 4. Inserisci il tuo ID cliente OAuth\n 5. Abilita gli eventi facendo clic su **Abilita** e **Crea progetto**. \n\n Inserisci il tuo ID progetto di accesso al dispositivo di seguito ([maggiori informazioni]({more_info_url})).\n", + "description": "Crea un progetto di accesso al dispositivo Nest la cui configurazione **richiede una commissione di 5 $ USD**.\n 1. Vai alla [Console di accesso al dispositivo]({device_access_console_url}) e attraverso il flusso di pagamento.\n 2. Clicca su **Crea progetto**\n 3. Assegna un nome al progetto di accesso al dispositivo e fai clic su **Avanti**.\n 4. Inserisci il tuo ID Client OAuth\n 5. Abilita gli eventi facendo clic su **Abilita** e **Crea progetto**. \n\nInserisci il tuo ID progetto di accesso al dispositivo di seguito ([maggiori informazioni]({more_info_url})).\n", "title": "Nest: crea un progetto di accesso al dispositivo" }, "device_project_upgrade": { - "description": "Aggiorna il progetto di accesso al dispositivo Nest con il nuovo ID client OAuth ([ulteriori informazioni]({more_info_url}))\n1. Vai a [Console di accesso al dispositivo]({device_access_console_url}).\n2. Fai clic sull'icona del cestino accanto a *OAuth Client ID*.\n3. Fai clic sul menu di overflow '...' e *Aggiungi ID client*.\n4. Immettere il nuovo ID client OAuth e fare clic su **Aggiungi**.\n\nIl tuo ID client OAuth \u00e8: '{client_id}'", + "description": "Aggiorna il progetto di accesso al dispositivo Nest con il nuovo ID client OAuth ([ulteriori informazioni]({more_info_url}))\n1. Vai a [Console di accesso al dispositivo]({device_access_console_url}).\n2. Fai clic sull'icona del cestino accanto a *ID Client OAuth*.\n3. Fai clic sul menu a comparsa '...' e *Aggiungi ID Client*.\n4. Immettere il nuovo ID Client OAuth e fare clic su **Aggiungi**.\n\nIl tuo ID Client OAuth \u00e8: `{client_id}`", "title": "Nest: aggiorna il progetto di accesso al dispositivo" }, "init": { diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index 37613407e4d..ee9fcbba98d 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "invalid_access_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json index fcc6aeadddf..ce6663ec22e 100644 --- a/homeassistant/components/nest/translations/no.json +++ b/homeassistant/components/nest/translations/no.json @@ -1,6 +1,10 @@ { + "application_credentials": { + "description": "F\u00f8lg [instruksjonene]( {more_info_url} ) for \u00e5 konfigurere Cloud Console: \n\n 1. G\u00e5 til [OAuth-samtykkeskjermen]( {oauth_consent_url} ) og konfigurer\n 1. G\u00e5 til [Credentials]( {oauth_creds_url} ) og klikk p\u00e5 **Create Credentials**.\n 1. Velg **OAuth-klient-ID** fra rullegardinlisten.\n 1. Velg **Nettapplikasjon** for applikasjonstype.\n 1. Legg til ` {redirect_url} ` under *Autorisert omdirigerings-URI*." + }, "config": { "abort": { + "already_configured": "Kontoen er allerede konfigurert", "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "invalid_access_token": "Ugyldig tilgangstoken", "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", @@ -19,7 +23,7 @@ "subscriber_error": "Ukjent abonnentfeil, se logger", "timeout": "Tidsavbrudd ved validering av kode", "unknown": "Uventet feil", - "wrong_project_id": "Angi en gyldig Cloud Project ID (funnet Device Access Project ID)" + "wrong_project_id": "Angi en gyldig Cloud Project ID (var den samme som Device Access Project ID)" }, "step": { "auth": { @@ -29,6 +33,32 @@ "description": "For \u00e5 koble til Google-kontoen din, [autoriser kontoen din]( {url} ). \n\n Etter autorisasjon, kopier og lim inn den oppgitte Auth Token-koden nedenfor.", "title": "Koble til Google-kontoen" }, + "auth_upgrade": { + "description": "App Auth har blitt avviklet av Google for \u00e5 forbedre sikkerheten, og du m\u00e5 iverksette tiltak ved \u00e5 opprette ny applikasjonslegitimasjon. \n\n \u00c5pne [dokumentasjonen]( {more_info_url} ) for \u00e5 f\u00f8lge med, da de neste trinnene vil lede deg gjennom trinnene du m\u00e5 ta for \u00e5 gjenopprette tilgangen til Nest-enhetene dine.", + "title": "Nest: Appautentisering avvikelse" + }, + "cloud_project": { + "data": { + "cloud_project_id": "Google Cloud Project ID" + }, + "description": "Skriv inn Cloud Project ID nedenfor, f.eks. *example-project-12345*. Se [Google Cloud Console]( {cloud_console_url} ) eller dokumentasjonen for [mer info]( {more_info_url} ).", + "title": "Nest: Angi Cloud Project ID" + }, + "create_cloud_project": { + "description": "Nest-integreringen lar deg integrere Nest-termostatene, kameraene og d\u00f8rklokkene dine ved hjelp av Smart Device Management API. SDM API **krever en engangsavgift p\u00e5 5 USD**. Se dokumentasjonen for [mer info]( {more_info_url} ). \n\n 1. G\u00e5 til [Google Cloud Console]( {cloud_console_url} ).\n 1. Hvis dette er ditt f\u00f8rste prosjekt, klikker du **Opprett prosjekt** og deretter **Nytt prosjekt**.\n 1. Gi skyprosjektet ditt et navn, og klikk deretter p\u00e5 **Opprett**.\n 1. Lagre Cloud Project ID, f.eks. *example-project-12345*, slik du trenger den senere\n 1. G\u00e5 til API Library for [Smart Device Management API]( {sdm_api_url} ) og klikk p\u00e5 **Aktiver**.\n 1. G\u00e5 til API-biblioteket for [Cloud Pub/Sub API]( {pubsub_api_url} ) og klikk p\u00e5 **Aktiver**. \n\n Fortsett n\u00e5r skyprosjektet ditt er satt opp.", + "title": "Nest: Opprett og konfigurer Cloud Project" + }, + "device_project": { + "data": { + "project_id": "Prosjekt-ID for enhetstilgang" + }, + "description": "Opprett et Nest Device Access-prosjekt som **krever en avgift p\u00e5 USD 5** for \u00e5 konfigurere.\n 1. G\u00e5 til [Device Access Console]( {device_access_console_url} ), og gjennom betalingsflyten.\n 1. Klikk p\u00e5 **Opprett prosjekt**\n 1. Gi Device Access-prosjektet ditt et navn og klikk p\u00e5 **Neste**.\n 1. Skriv inn din OAuth-klient-ID\n 1. Aktiver hendelser ved \u00e5 klikke **Aktiver** og **Opprett prosjekt**. \n\n Skriv inn Device Access Project ID nedenfor ([mer info]( {more_info_url} )).\n", + "title": "Nest: Opprett et Device Access Project" + }, + "device_project_upgrade": { + "description": "Oppdater Nest Device Access Project med din nye OAuth-klient-ID ([mer info]( {more_info_url} ))\n 1. G\u00e5 til [Device Access Console]( {device_access_console_url} ).\n 1. Klikk p\u00e5 s\u00f8ppelikonet ved siden av *OAuth Client ID*.\n 1. Klikk p\u00e5 \"...\" overl\u00f8psmenyen og *Legg til klient-ID*.\n 1. Skriv inn din nye OAuth-klient-ID og klikk p\u00e5 **Legg til**. \n\n Din OAuth-klient-ID er: ` {client_id} `", + "title": "Nest: Oppdater Device Access Project" + }, "init": { "data": { "flow_impl": "Tilbyder" diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json index 4c53185e71f..c7a5c88b120 100644 --- a/homeassistant/components/nest/translations/pl.json +++ b/homeassistant/components/nest/translations/pl.json @@ -1,9 +1,10 @@ { "application_credentials": { - "description": "Post\u0119puj zgodnie z [instrukcj\u0105]({more_info_url}), aby skonfigurowa\u0107 Cloud Console: \n\n1. Przejd\u017a do [ekranu akceptacji OAuth]( {oauth_consent_url} ) i skonfiguruj\n2. Przejd\u017a do [Po\u015bwiadczenia]({oauth_creds_url}) i kliknij **Utw\u00f3rz po\u015bwiadczenia**.\n3. Z listy rozwijanej wybierz **ID klienta OAuth**.\n4. Wybierz **Aplikacja internetowa** jako Typ aplikacji.\n5. Dodaj `{redirect_url}` pod *Autoryzowany URI przekierowania*." + "description": "Post\u0119puj zgodnie z [instrukcj\u0105]({more_info_url}), aby skonfigurowa\u0107 Cloud Console: \n\n1. Przejd\u017a do [ekranu akceptacji OAuth]({oauth_consent_url}) i skonfiguruj.\n2. Przejd\u017a do [Po\u015bwiadczenia]({oauth_creds_url}) i kliknij **Utw\u00f3rz po\u015bwiadczenia**.\n3. Z listy rozwijanej wybierz **Identyfikator klienta OAuth**.\n4. Wybierz **Aplikacja internetowa** jako Typ aplikacji.\n5. Dodaj `{redirect_url}` pod *Autoryzowany URI przekierowania*." }, "config": { "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", "invalid_access_token": "Niepoprawny token dost\u0119pu", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", @@ -32,6 +33,32 @@ "description": "Aby po\u0142\u0105czy\u0107 swoje konto Google, [authorize your account]({url}). \n\nPo autoryzacji skopiuj i wklej podany poni\u017cej token uwierzytelniaj\u0105cy.", "title": "Po\u0142\u0105czenie z kontem Google" }, + "auth_upgrade": { + "description": "App Auth zosta\u0142o wycofane przez Google w celu poprawy bezpiecze\u0144stwa i musisz podj\u0105\u0107 dzia\u0142ania, tworz\u0105c nowe dane logowania do aplikacji. \n\nOtw\u00f3rz [dokumentacj\u0119]({more_info_url}), aby przej\u015b\u0107 dalej. Kolejne kroki poprowadz\u0105 Ci\u0119 przez instrukcje, kt\u00f3re musisz wykona\u0107, aby przywr\u00f3ci\u0107 dost\u0119p do urz\u0105dze\u0144 Nest.", + "title": "Nest: wycofanie App Auth" + }, + "cloud_project": { + "data": { + "cloud_project_id": "Identyfikator projektu chmury Google" + }, + "description": "Wpisz poni\u017cej identyfikator projektu chmury, np. *przykladowy-projekt-12345*. Zajrzyj do [Konsoli Google Cloud]({cloud_console_url}) lub dokumentacji po [wi\u0119cej informacji]({more_info_url}).", + "title": "Nest: Wprowad\u017a identyfikator projektu chmury" + }, + "create_cloud_project": { + "description": "Integracja Nest umo\u017cliwia integracj\u0119 termostat\u00f3w, kamer i dzwonk\u00f3w Nest za pomoc\u0105 Smart Device Management API. Interfejs API SDM **wymaga jednorazowej op\u0142aty instalacyjnej w wysoko\u015bci 5 dolar\u00f3w**. Zajrzyj do dokumentacji po [wi\u0119cej informacji]({more_info_url}). \n\n1. Przejd\u017a do [Konsoli Google Cloud]({cloud_console_url}).\n2. Je\u015bli to Tw\u00f3j pierwszy projekt, kliknij **Utw\u00f3rz projekt**, a nast\u0119pnie **Nowy projekt**.\n3. Nadaj nazw\u0119 swojemu projektowi w chmurze, a nast\u0119pnie kliknij **Utw\u00f3rz**.\n4. Zapisz identyfikator projektu chmury, np. *przykladowy-projekt-12345*, poniewa\u017c b\u0119dziesz go potrzebowa\u0107 p\u00f3\u017aniej.\n5. Przejd\u017a do biblioteki API dla [Smart Device Management API]({sdm_api_url}) i kliknij **W\u0142\u0105cz**.\n6. Przejd\u017a do biblioteki API dla [Cloud Pub/Sub API]({pubsub_api_url}) i kliknij **W\u0142\u0105cz**. \n\nKontynuuj po skonfigurowaniu projektu w chmurze.", + "title": "Nest: Utw\u00f3rz i skonfiguruj projekt w chmurze" + }, + "device_project": { + "data": { + "project_id": "Identyfikator projektu dost\u0119pu do urz\u0105dzenia" + }, + "description": "Utw\u00f3rz projekt dost\u0119pu do urz\u0105dzenia Nest, kt\u00f3rego konfiguracja **wymaga op\u0142aty w wysoko\u015bci 5 dolar\u00f3w**.\n1. Przejd\u017a do [Konsoli dost\u0119pu do urz\u0105dzenia]({device_access_console_url}) i przejd\u017a przez proces p\u0142atno\u015bci.\n2. Kliknij **Utw\u00f3rz projekt**.\n3. Nadaj projektowi dost\u0119pu do urz\u0105dzenia nazw\u0119 i kliknij **Dalej**.\n4. Wprowad\u017a sw\u00f3j identyfikator klienta OAuth\n5. W\u0142\u0105cz wydarzenia, klikaj\u0105c **W\u0142\u0105cz** i **Utw\u00f3rz projekt**. \n\nPod ([wi\u0119cej informacji]({more_info_url})) wpisz sw\u00f3j identyfikator projektu dost\u0119pu do urz\u0105dzenia.\n", + "title": "Nest: Utw\u00f3rz projekt dost\u0119pu do urz\u0105dzenia" + }, + "device_project_upgrade": { + "description": "Zaktualizuj projekt dost\u0119pu do urz\u0105dzenia Nest przy u\u017cyciu nowego identyfikatora klienta OAuth ([wi\u0119cej informacji]({more_info_url}))\n1. Przejd\u017a do [Konsoli dost\u0119pu do urz\u0105dzenia]({device_access_console_url}).\n2. Kliknij ikon\u0119 kosza obok *Identyfikator klienta OAuth*.\n3. Kliknij menu rozszerzone `...` i *Dodaj identyfikator klienta*.\n4. Wprowad\u017a nowy identyfikator klienta OAuth i kliknij **Dodaj**. \n\nTw\u00f3j identyfikator klienta OAuth to: `{client_id}`", + "title": "Nest: Zaktualizuj projekt dost\u0119pu do urz\u0105dzenia" + }, "init": { "data": { "flow_impl": "Dostawca" diff --git a/homeassistant/components/overkiz/translations/sensor.it.json b/homeassistant/components/overkiz/translations/sensor.it.json index cb63ec30408..0799ae8d7c8 100644 --- a/homeassistant/components/overkiz/translations/sensor.it.json +++ b/homeassistant/components/overkiz/translations/sensor.it.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "Pulito", "dirty": "Sporco" + }, + "overkiz__three_way_handle_direction": { + "closed": "Chiuso", + "open": "Aperto", + "tilt": "Inclinazione" } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.ja.json b/homeassistant/components/overkiz/translations/sensor.ja.json index ca1f1833a0a..e1c44a8e3ff 100644 --- a/homeassistant/components/overkiz/translations/sensor.ja.json +++ b/homeassistant/components/overkiz/translations/sensor.ja.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "\u30af\u30ea\u30fc\u30f3", "dirty": "\u30c0\u30fc\u30c6\u30a3" + }, + "overkiz__three_way_handle_direction": { + "closed": "\u30af\u30ed\u30fc\u30ba\u30c9", + "open": "\u30aa\u30fc\u30d7\u30f3", + "tilt": "\u50be\u659c(Tilt)" } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.no.json b/homeassistant/components/overkiz/translations/sensor.no.json index 23a94df54c7..65f3cbeed9f 100644 --- a/homeassistant/components/overkiz/translations/sensor.no.json +++ b/homeassistant/components/overkiz/translations/sensor.no.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "Ren", "dirty": "Skitten" + }, + "overkiz__three_way_handle_direction": { + "closed": "Lukket", + "open": "\u00c5pen", + "tilt": "Vippe" } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.pl.json b/homeassistant/components/overkiz/translations/sensor.pl.json index 0633c0d8424..440cf4998f8 100644 --- a/homeassistant/components/overkiz/translations/sensor.pl.json +++ b/homeassistant/components/overkiz/translations/sensor.pl.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "czysto", "dirty": "brudno" + }, + "overkiz__three_way_handle_direction": { + "closed": "zamkni\u0119ta", + "open": "otwarta", + "tilt": "uchylona" } } } \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/it.json b/homeassistant/components/scrape/translations/it.json index 5fd8428335f..e64ee3022d8 100644 --- a/homeassistant/components/scrape/translations/it.json +++ b/homeassistant/components/scrape/translations/it.json @@ -23,7 +23,7 @@ }, "data_description": { "attribute": "Ottieni il valore di un attributo sull'etichetta selezionata", - "authentication": "Tipo di autenticazione HTTP. Base o digest", + "authentication": "Tipo di autenticazione HTTP. basic o digest", "device_class": "Il tipo/classe del sensore per impostare l'icona nel frontend", "headers": "Intestazioni da utilizzare per la richiesta web", "index": "Definisce quale degli elementi restituiti dal selettore CSS utilizzare", @@ -57,7 +57,7 @@ }, "data_description": { "attribute": "Ottieni il valore di un attributo sull'etichetta selezionata", - "authentication": "Tipo di autenticazione HTTP. Base o digest", + "authentication": "Tipo di autenticazione HTTP. basic o digest", "device_class": "Il tipo/classe del sensore per impostare l'icona nel frontend", "headers": "Intestazioni da utilizzare per la richiesta web", "index": "Definisce quale degli elementi restituiti dal selettore CSS utilizzare", diff --git a/homeassistant/components/transmission/translations/it.json b/homeassistant/components/transmission/translations/it.json index 18edfb17351..2cefcdfb290 100644 --- a/homeassistant/components/transmission/translations/it.json +++ b/homeassistant/components/transmission/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": { "cannot_connect": "Impossibile connettersi", @@ -9,6 +10,13 @@ "name_exists": "Nome gi\u00e0 esistente" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "La password per {username} non \u00e8 valida.", + "title": "Autentica nuovamente l'integrazione" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/transmission/translations/ja.json b/homeassistant/components/transmission/translations/ja.json index 5bd5b98a8b3..6f3b3191c83 100644 --- a/homeassistant/components/transmission/translations/ja.json +++ b/homeassistant/components/transmission/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", @@ -9,6 +10,13 @@ "name_exists": "\u540d\u524d\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u7121\u52b9\u3067\u3059\u3002", + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + }, "user": { "data": { "host": "\u30db\u30b9\u30c8", diff --git a/homeassistant/components/transmission/translations/no.json b/homeassistant/components/transmission/translations/no.json index cab8fd22659..fe15e4adc43 100644 --- a/homeassistant/components/transmission/translations/no.json +++ b/homeassistant/components/transmission/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -9,6 +10,13 @@ "name_exists": "Navn eksisterer allerede" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Passordet for {username} er ugyldig.", + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "host": "Vert", diff --git a/homeassistant/components/transmission/translations/pl.json b/homeassistant/components/transmission/translations/pl.json index dd2b28ad65f..994744a3547 100644 --- a/homeassistant/components/transmission/translations/pl.json +++ b/homeassistant/components/transmission/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -9,6 +10,13 @@ "name_exists": "Nazwa ju\u017c istnieje" }, "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Has\u0142o dla u\u017cytkownika {username} jest nieprawid\u0142owe.", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "host": "Nazwa hosta lub adres IP", From e5c40d58ffc606c40b0400aa4a9ece472004393d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Jun 2022 21:13:43 -0500 Subject: [PATCH 1765/3516] Add roku 3820X model to discovery (#73933) --- homeassistant/components/roku/manifest.json | 2 +- homeassistant/generated/zeroconf.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 3f63a7039c1..05fe0e1b260 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/roku", "requirements": ["rokuecp==0.16.0"], "homekit": { - "models": ["3810X", "4660X", "7820X", "C105X", "C135X"] + "models": ["3820X", "3810X", "4660X", "7820X", "C105X", "C135X"] }, "ssdp": [ { diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 21631292f13..faca1c17854 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -406,6 +406,7 @@ ZEROCONF = { HOMEKIT = { "3810X": "roku", + "3820X": "roku", "4660X": "roku", "7820X": "roku", "819LMB": "myq", From e4a770984d82f3a3f9ff88a9a47b612caf2b90e1 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 23 Jun 2022 22:50:39 -0400 Subject: [PATCH 1766/3516] Bump version of pyunifiprotect to 4.0.8 (#73934) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index e28386d73a8..da82871d313 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.7", "unifi-discovery==1.1.4"], + "requirements": ["pyunifiprotect==4.0.8", "unifi-discovery==1.1.4"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index fc14180138f..0e937316fc2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1990,7 +1990,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.7 +pyunifiprotect==4.0.8 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f3fc63c76f..ea092b5744e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1325,7 +1325,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.7 +pyunifiprotect==4.0.8 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 307666da7f54cede3a73cb52ef13aab1e13d4954 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 23 Jun 2022 22:51:31 -0500 Subject: [PATCH 1767/3516] Bump Frontend to 20220624.0 (#73938) --- 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 7d07bbd543c..b7c1c0ddeff 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220601.0"], + "requirements": ["home-assistant-frontend==20220624.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c9d011f3471..55cc067829b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220601.0 +home-assistant-frontend==20220624.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 0e937316fc2..542fcdbb540 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -825,7 +825,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220601.0 +home-assistant-frontend==20220624.0 # homeassistant.components.home_connect homeconnect==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea092b5744e..b41bf1b223d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -592,7 +592,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220601.0 +home-assistant-frontend==20220624.0 # homeassistant.components.home_connect homeconnect==0.7.0 From a92ab7a6693bb8a1ae42e5674f5abfa9ecc04d16 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 24 Jun 2022 06:40:26 +0200 Subject: [PATCH 1768/3516] Adjust CoverEntity function type hints in components (#73912) Adjust CoverEntity functions in components --- homeassistant/components/acmeda/cover.py | 16 +++--- homeassistant/components/ads/cover.py | 12 ++-- .../components/advantage_air/cover.py | 8 ++- homeassistant/components/blebox/cover.py | 10 ++-- homeassistant/components/bosch_shc/cover.py | 10 ++-- .../components/command_line/cover.py | 6 +- homeassistant/components/demo/cover.py | 14 +++-- homeassistant/components/dynalite/cover.py | 18 +++--- homeassistant/components/fibaro/cover.py | 16 +++--- homeassistant/components/freedompro/cover.py | 5 +- homeassistant/components/garadget/cover.py | 7 ++- homeassistant/components/gogogate2/cover.py | 6 +- homeassistant/components/homematic/cover.py | 18 +++--- .../components/homematicip_cloud/cover.py | 56 ++++++++++--------- homeassistant/components/insteon/cover.py | 7 ++- homeassistant/components/lutron/cover.py | 7 ++- .../components/lutron_caseta/cover.py | 10 ++-- .../components/motion_blinds/cover.py | 27 ++++----- homeassistant/components/mqtt/cover.py | 17 +++--- homeassistant/components/myq/cover.py | 6 +- homeassistant/components/opengarage/cover.py | 5 +- homeassistant/components/rflink/cover.py | 7 ++- homeassistant/components/scsgate/cover.py | 7 ++- homeassistant/components/slide/cover.py | 9 +-- homeassistant/components/smartthings/cover.py | 5 +- homeassistant/components/soma/cover.py | 18 +++--- .../components/somfy_mylink/cover.py | 9 +-- homeassistant/components/supla/cover.py | 17 +++--- homeassistant/components/tellduslive/cover.py | 8 ++- homeassistant/components/tellstick/cover.py | 8 ++- homeassistant/components/template/cover.py | 17 +++--- homeassistant/components/tuya/cover.py | 2 +- homeassistant/components/velux/cover.py | 18 +++--- homeassistant/components/vera/cover.py | 2 +- homeassistant/components/wilight/cover.py | 10 ++-- .../components/xiaomi_aqara/cover.py | 10 ++-- homeassistant/components/zha/cover.py | 24 ++++---- homeassistant/components/zwave_me/cover.py | 4 +- 38 files changed, 254 insertions(+), 202 deletions(-) diff --git a/homeassistant/components/acmeda/cover.py b/homeassistant/components/acmeda/cover.py index 2fbd2de6c42..d772d8ab01f 100644 --- a/homeassistant/components/acmeda/cover.py +++ b/homeassistant/components/acmeda/cover.py @@ -1,6 +1,8 @@ """Support for Acmeda Roller Blinds.""" from __future__ import annotations +from typing import Any + from homeassistant.components.cover import ( ATTR_POSITION, CoverEntity, @@ -92,31 +94,31 @@ class AcmedaCover(AcmedaBase, CoverEntity): """Return if the cover is closed.""" return self.roller.closed_percent == 100 - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close the roller.""" await self.roller.move_down() - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the roller.""" await self.roller.move_up() - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the roller.""" await self.roller.move_stop() - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the roller shutter to a specific position.""" await self.roller.move_to(100 - kwargs[ATTR_POSITION]) - async def async_close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close the roller.""" await self.roller.move_down() - async def async_open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open the roller.""" await self.roller.move_up() - async def async_stop_cover_tilt(self, **kwargs): + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the roller.""" await self.roller.move_stop() diff --git a/homeassistant/components/ads/cover.py b/homeassistant/components/ads/cover.py index c1f057b588e..a976ce15877 100644 --- a/homeassistant/components/ads/cover.py +++ b/homeassistant/components/ads/cover.py @@ -1,6 +1,8 @@ """Support for ADS covers.""" from __future__ import annotations +from typing import Any + import pyads import voluptuous as vol @@ -122,7 +124,7 @@ class AdsCover(AdsEntity, CoverEntity): if ads_var_pos_set is not None: self._attr_supported_features |= CoverEntityFeature.SET_POSITION - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register device notification.""" if self._ads_var is not None: await self.async_initialize_device(self._ads_var, pyads.PLCTYPE_BOOL) @@ -146,12 +148,12 @@ class AdsCover(AdsEntity, CoverEntity): """Return current position of cover.""" return self._state_dict[STATE_KEY_POSITION] - def stop_cover(self, **kwargs): + def stop_cover(self, **kwargs: Any) -> None: """Fire the stop action.""" if self._ads_var_stop: self._ads_hub.write_by_name(self._ads_var_stop, True, pyads.PLCTYPE_BOOL) - def set_cover_position(self, **kwargs): + def set_cover_position(self, **kwargs: Any) -> None: """Set cover position.""" position = kwargs[ATTR_POSITION] if self._ads_var_pos_set is not None: @@ -159,14 +161,14 @@ class AdsCover(AdsEntity, CoverEntity): self._ads_var_pos_set, position, pyads.PLCTYPE_BYTE ) - def open_cover(self, **kwargs): + def open_cover(self, **kwargs: Any) -> None: """Move the cover up.""" if self._ads_var_open is not None: self._ads_hub.write_by_name(self._ads_var_open, True, pyads.PLCTYPE_BOOL) elif self._ads_var_pos_set is not None: self.set_cover_position(position=100) - def close_cover(self, **kwargs): + def close_cover(self, **kwargs: Any) -> None: """Move the cover down.""" if self._ads_var_close is not None: self._ads_hub.write_by_name(self._ads_var_close, True, pyads.PLCTYPE_BOOL) diff --git a/homeassistant/components/advantage_air/cover.py b/homeassistant/components/advantage_air/cover.py index 847ca41c42c..bbe17835d71 100644 --- a/homeassistant/components/advantage_air/cover.py +++ b/homeassistant/components/advantage_air/cover.py @@ -1,4 +1,6 @@ """Cover platform for Advantage Air integration.""" +from typing import Any + from homeassistant.components.cover import ( ATTR_POSITION, CoverDeviceClass, @@ -67,7 +69,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): return self._zone["value"] return 0 - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Fully open zone vent.""" await self.async_change( { @@ -79,7 +81,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): } ) - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Fully close zone vent.""" await self.async_change( { @@ -89,7 +91,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): } ) - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Change vent position.""" position = round(kwargs[ATTR_POSITION] / 5) * 5 if position == 0: diff --git a/homeassistant/components/blebox/cover.py b/homeassistant/components/blebox/cover.py index 09d28a4f87a..a7f531fe519 100644 --- a/homeassistant/components/blebox/cover.py +++ b/homeassistant/components/blebox/cover.py @@ -1,4 +1,6 @@ """BleBox cover entity.""" +from typing import Any + from homeassistant.components.cover import ( ATTR_POSITION, CoverEntity, @@ -62,21 +64,21 @@ class BleBoxCoverEntity(BleBoxEntity, CoverEntity): """Return whether cover is closed.""" return self._is_state(STATE_CLOSED) - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover position.""" await self._feature.async_open() - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover position.""" await self._feature.async_close() - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Set the cover position.""" position = kwargs[ATTR_POSITION] await self._feature.async_set_position(100 - position) - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self._feature.async_stop() diff --git a/homeassistant/components/bosch_shc/cover.py b/homeassistant/components/bosch_shc/cover.py index fd191d59bc3..cdbe884dc45 100644 --- a/homeassistant/components/bosch_shc/cover.py +++ b/homeassistant/components/bosch_shc/cover.py @@ -1,4 +1,6 @@ """Platform for cover integration.""" +from typing import Any + from boschshcpy import SHCSession, SHCShutterControl from homeassistant.components.cover import ( @@ -54,7 +56,7 @@ class ShutterControlCover(SHCEntity, CoverEntity): """Return the current cover position.""" return round(self._device.level * 100.0) - def stop_cover(self, **kwargs): + def stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" self._device.stop() @@ -79,15 +81,15 @@ class ShutterControlCover(SHCEntity, CoverEntity): == SHCShutterControl.ShutterControlService.State.CLOSING ) - def open_cover(self, **kwargs): + def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" self._device.level = 1.0 - def close_cover(self, **kwargs): + def close_cover(self, **kwargs: Any) -> None: """Close cover.""" self._device.level = 0.0 - def set_cover_position(self, **kwargs): + def set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] self._device.level = position / 100.0 diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 321b18437d9..609166f2d16 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -153,14 +153,14 @@ class CommandCover(CoverEntity): payload = self._value_template.render_with_possible_json_value(payload) self._state = int(payload) - def open_cover(self, **kwargs) -> None: + def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" self._move_cover(self._command_open) - def close_cover(self, **kwargs) -> None: + def close_cover(self, **kwargs: Any) -> None: """Close the cover.""" self._move_cover(self._command_close) - def stop_cover(self, **kwargs) -> None: + def stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" self._move_cover(self._command_stop) diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index 9a1ea6239ee..79a51406857 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -1,6 +1,8 @@ """Demo platform for the cover component.""" from __future__ import annotations +from typing import Any + from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, @@ -159,7 +161,7 @@ class DemoCover(CoverEntity): return self._supported_features return super().supported_features - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" if self._position == 0: return @@ -173,7 +175,7 @@ class DemoCover(CoverEntity): self._requested_closing = True self.async_write_ha_state() - async def async_close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" if self._tilt_position in (0, None): return @@ -181,7 +183,7 @@ class DemoCover(CoverEntity): self._listen_cover_tilt() self._requested_closing_tilt = True - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" if self._position == 100: return @@ -195,7 +197,7 @@ class DemoCover(CoverEntity): self._requested_closing = False self.async_write_ha_state() - async def async_open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" if self._tilt_position in (100, None): return @@ -223,7 +225,7 @@ class DemoCover(CoverEntity): self._listen_cover_tilt() self._requested_closing_tilt = tilt_position < self._tilt_position - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" self._is_closing = False self._is_opening = False @@ -234,7 +236,7 @@ class DemoCover(CoverEntity): self._unsub_listener_cover = None self._set_position = None - async def async_stop_cover_tilt(self, **kwargs): + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the cover tilt.""" if self._tilt_position is None: return diff --git a/homeassistant/components/dynalite/cover.py b/homeassistant/components/dynalite/cover.py index 930ced4ff54..e5c38996a89 100644 --- a/homeassistant/components/dynalite/cover.py +++ b/homeassistant/components/dynalite/cover.py @@ -1,5 +1,7 @@ """Support for the Dynalite channels as covers.""" +from typing import Any + from homeassistant.components.cover import DEVICE_CLASSES, CoverDeviceClass, CoverEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -60,19 +62,19 @@ class DynaliteCover(DynaliteBase, CoverEntity): """Return true if cover is closed.""" return self._device.is_closed - async def async_open_cover(self, **kwargs) -> None: + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self._device.async_open_cover(**kwargs) - async def async_close_cover(self, **kwargs) -> None: + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self._device.async_close_cover(**kwargs) - async def async_set_cover_position(self, **kwargs) -> None: + async def async_set_cover_position(self, **kwargs: Any) -> None: """Set the cover position.""" await self._device.async_set_cover_position(**kwargs) - async def async_stop_cover(self, **kwargs) -> None: + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self._device.async_stop_cover(**kwargs) @@ -85,18 +87,18 @@ class DynaliteCoverWithTilt(DynaliteCover): """Return the current tilt position.""" return self._device.current_cover_tilt_position - async def async_open_cover_tilt(self, **kwargs) -> None: + async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open cover tilt.""" await self._device.async_open_cover_tilt(**kwargs) - async def async_close_cover_tilt(self, **kwargs) -> None: + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close cover tilt.""" await self._device.async_close_cover_tilt(**kwargs) - async def async_set_cover_tilt_position(self, **kwargs) -> None: + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Set the cover tilt position.""" await self._device.async_set_cover_tilt_position(**kwargs) - async def async_stop_cover_tilt(self, **kwargs) -> None: + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the cover tilt.""" await self._device.async_stop_cover_tilt(**kwargs) diff --git a/homeassistant/components/fibaro/cover.py b/homeassistant/components/fibaro/cover.py index e898cd7ead9..1e5583f20d6 100644 --- a/homeassistant/components/fibaro/cover.py +++ b/homeassistant/components/fibaro/cover.py @@ -1,6 +1,8 @@ """Support for Fibaro cover - curtains, rollershutters etc.""" from __future__ import annotations +from typing import Any + from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, @@ -79,11 +81,11 @@ class FibaroCover(FibaroDevice, CoverEntity): """Return the current tilt position for venetian blinds.""" return self.bound(self.level2) - def set_cover_position(self, **kwargs): + def set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" self.set_level(kwargs.get(ATTR_POSITION)) - def set_cover_tilt_position(self, **kwargs): + def set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" self.set_level2(kwargs.get(ATTR_TILT_POSITION)) @@ -97,22 +99,22 @@ class FibaroCover(FibaroDevice, CoverEntity): return None return self.current_cover_position == 0 - def open_cover(self, **kwargs): + def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" self.action("open") - def close_cover(self, **kwargs): + def close_cover(self, **kwargs: Any) -> None: """Close the cover.""" self.action("close") - def open_cover_tilt(self, **kwargs): + def open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" self.set_level2(100) - def close_cover_tilt(self, **kwargs): + def close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover.""" self.set_level2(0) - def stop_cover(self, **kwargs): + def stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" self.action("stop") diff --git a/homeassistant/components/freedompro/cover.py b/homeassistant/components/freedompro/cover.py index ab3914519dd..e4483f0005b 100644 --- a/homeassistant/components/freedompro/cover.py +++ b/homeassistant/components/freedompro/cover.py @@ -1,5 +1,6 @@ """Support for Freedompro cover.""" import json +from typing import Any from pyfreedompro import put_state @@ -96,11 +97,11 @@ class Device(CoordinatorEntity, CoverEntity): await super().async_added_to_hass() self._handle_coordinator_update() - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self.async_set_cover_position(position=100) - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self.async_set_cover_position(position=0) diff --git a/homeassistant/components/garadget/cover.py b/homeassistant/components/garadget/cover.py index 75198fa2f60..e0372db0c6c 100644 --- a/homeassistant/components/garadget/cover.py +++ b/homeassistant/components/garadget/cover.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import requests import voluptuous as vol @@ -207,21 +208,21 @@ class GaradgetCover(CoverEntity): """Check the state of the service during an operation.""" self.schedule_update_ha_state(True) - def close_cover(self, **kwargs): + def close_cover(self, **kwargs: Any) -> None: """Close the cover.""" if self._state not in ["close", "closing"]: ret = self._put_command("setState", "close") self._start_watcher("close") return ret.get("return_value") == 1 - def open_cover(self, **kwargs): + def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" if self._state not in ["open", "opening"]: ret = self._put_command("setState", "open") self._start_watcher("open") return ret.get("return_value") == 1 - def stop_cover(self, **kwargs): + def stop_cover(self, **kwargs: Any) -> None: """Stop the door where it is.""" if self._state not in ["stopped"]: ret = self._put_command("setState", "stop") diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index af3bd1c7530..b7434952fc1 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -1,6 +1,8 @@ """Support for Gogogate2 garage Doors.""" from __future__ import annotations +from typing import Any + from ismartgate.common import ( AbstractDoor, DoorStatus, @@ -84,12 +86,12 @@ class DeviceCover(GoGoGate2Entity, CoverEntity): """Return if the cover is opening or not.""" return self.door_status == TransitionDoorStatus.OPENING - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the door.""" await self._api.async_open_door(self._door_id) await self.coordinator.async_refresh() - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close the door.""" await self._api.async_close_door(self._door_id) await self.coordinator.async_refresh() diff --git a/homeassistant/components/homematic/cover.py b/homeassistant/components/homematic/cover.py index 2aa47ad863a..d138e172784 100644 --- a/homeassistant/components/homematic/cover.py +++ b/homeassistant/components/homematic/cover.py @@ -1,6 +1,8 @@ """Support for HomeMatic covers.""" from __future__ import annotations +from typing import Any + from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, @@ -49,7 +51,7 @@ class HMCover(HMDevice, CoverEntity): """ return int(self._hm_get_state() * 100) - def set_cover_position(self, **kwargs): + def set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" if ATTR_POSITION in kwargs: position = float(kwargs[ATTR_POSITION]) @@ -64,15 +66,15 @@ class HMCover(HMDevice, CoverEntity): return self.current_cover_position == 0 return None - def open_cover(self, **kwargs): + def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" self._hmdevice.move_up(self._channel) - def close_cover(self, **kwargs): + def close_cover(self, **kwargs: Any) -> None: """Close the cover.""" self._hmdevice.move_down(self._channel) - def stop_cover(self, **kwargs): + def stop_cover(self, **kwargs: Any) -> None: """Stop the device if in motion.""" self._hmdevice.stop(self._channel) @@ -93,7 +95,7 @@ class HMCover(HMDevice, CoverEntity): return None return int(position * 100) - def set_cover_tilt_position(self, **kwargs): + def set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" if "LEVEL_2" in self._data and ATTR_TILT_POSITION in kwargs: position = float(kwargs[ATTR_TILT_POSITION]) @@ -101,17 +103,17 @@ class HMCover(HMDevice, CoverEntity): level = position / 100.0 self._hmdevice.set_cover_tilt_position(level, self._channel) - def open_cover_tilt(self, **kwargs): + def open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" if "LEVEL_2" in self._data: self._hmdevice.open_slats() - def close_cover_tilt(self, **kwargs): + def close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" if "LEVEL_2" in self._data: self._hmdevice.close_slats() - def stop_cover_tilt(self, **kwargs): + def stop_cover_tilt(self, **kwargs: Any) -> None: """Stop cover tilt.""" if "LEVEL_2" in self._data: self.stop_cover(**kwargs) diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index b5076fa74ec..31faea875a4 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -1,6 +1,8 @@ """Support for HomematicIP Cloud cover devices.""" from __future__ import annotations +from typing import Any + from homematicip.aio.device import ( AsyncBlindModule, AsyncDinRailBlind4, @@ -86,14 +88,14 @@ class HomematicipBlindModule(HomematicipGenericEntity, CoverEntity): return int((1 - self._device.secondaryShadingLevel) * 100) return None - async def async_set_cover_position(self, **kwargs) -> None: + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] # HmIP cover is closed:1 -> open:0 level = 1 - position / 100.0 await self._device.set_primary_shading_level(primaryShadingLevel=level) - async def async_set_cover_tilt_position(self, **kwargs) -> None: + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover to a specific tilt position.""" position = kwargs[ATTR_TILT_POSITION] # HmIP slats is closed:1 -> open:0 @@ -110,37 +112,37 @@ class HomematicipBlindModule(HomematicipGenericEntity, CoverEntity): return self._device.primaryShadingLevel == HMIP_COVER_CLOSED return None - async def async_open_cover(self, **kwargs) -> None: + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self._device.set_primary_shading_level( primaryShadingLevel=HMIP_COVER_OPEN ) - async def async_close_cover(self, **kwargs) -> None: + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self._device.set_primary_shading_level( primaryShadingLevel=HMIP_COVER_CLOSED ) - async def async_stop_cover(self, **kwargs) -> None: + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the device if in motion.""" await self._device.stop() - async def async_open_cover_tilt(self, **kwargs) -> None: + async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open the slats.""" await self._device.set_secondary_shading_level( primaryShadingLevel=self._device.primaryShadingLevel, secondaryShadingLevel=HMIP_SLATS_OPEN, ) - async def async_close_cover_tilt(self, **kwargs) -> None: + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close the slats.""" await self._device.set_secondary_shading_level( primaryShadingLevel=self._device.primaryShadingLevel, secondaryShadingLevel=HMIP_SLATS_CLOSED, ) - async def async_stop_cover_tilt(self, **kwargs) -> None: + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the device if in motion.""" await self._device.stop() @@ -174,7 +176,7 @@ class HomematicipMultiCoverShutter(HomematicipGenericEntity, CoverEntity): ) return None - async def async_set_cover_position(self, **kwargs) -> None: + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] # HmIP cover is closed:1 -> open:0 @@ -191,15 +193,15 @@ class HomematicipMultiCoverShutter(HomematicipGenericEntity, CoverEntity): ) return None - async def async_open_cover(self, **kwargs) -> None: + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self._device.set_shutter_level(HMIP_COVER_OPEN, self._channel) - async def async_close_cover(self, **kwargs) -> None: + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self._device.set_shutter_level(HMIP_COVER_CLOSED, self._channel) - async def async_stop_cover(self, **kwargs) -> None: + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the device if in motion.""" await self._device.set_shutter_stop(self._channel) @@ -236,26 +238,26 @@ class HomematicipMultiCoverSlats(HomematicipMultiCoverShutter, CoverEntity): ) return None - async def async_set_cover_tilt_position(self, **kwargs) -> None: + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover to a specific tilt position.""" position = kwargs[ATTR_TILT_POSITION] # HmIP slats is closed:1 -> open:0 level = 1 - position / 100.0 await self._device.set_slats_level(slatsLevel=level, channelIndex=self._channel) - async def async_open_cover_tilt(self, **kwargs) -> None: + async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open the slats.""" await self._device.set_slats_level( slatsLevel=HMIP_SLATS_OPEN, channelIndex=self._channel ) - async def async_close_cover_tilt(self, **kwargs) -> None: + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close the slats.""" await self._device.set_slats_level( slatsLevel=HMIP_SLATS_CLOSED, channelIndex=self._channel ) - async def async_stop_cover_tilt(self, **kwargs) -> None: + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the device if in motion.""" await self._device.set_shutter_stop(self._channel) @@ -292,15 +294,15 @@ class HomematicipGarageDoorModule(HomematicipGenericEntity, CoverEntity): """Return if the cover is closed.""" return self._device.doorState == DoorState.CLOSED - async def async_open_cover(self, **kwargs) -> None: + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self._device.send_door_command(DoorCommand.OPEN) - async def async_close_cover(self, **kwargs) -> None: + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self._device.send_door_command(DoorCommand.CLOSE) - async def async_stop_cover(self, **kwargs) -> None: + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self._device.send_door_command(DoorCommand.STOP) @@ -339,40 +341,40 @@ class HomematicipCoverShutterGroup(HomematicipGenericEntity, CoverEntity): return self._device.shutterLevel == HMIP_COVER_CLOSED return None - async def async_set_cover_position(self, **kwargs) -> None: + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] # HmIP cover is closed:1 -> open:0 level = 1 - position / 100.0 await self._device.set_shutter_level(level) - async def async_set_cover_tilt_position(self, **kwargs) -> None: + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover to a specific tilt position.""" position = kwargs[ATTR_TILT_POSITION] # HmIP slats is closed:1 -> open:0 level = 1 - position / 100.0 await self._device.set_slats_level(level) - async def async_open_cover(self, **kwargs) -> None: + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self._device.set_shutter_level(HMIP_COVER_OPEN) - async def async_close_cover(self, **kwargs) -> None: + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self._device.set_shutter_level(HMIP_COVER_CLOSED) - async def async_stop_cover(self, **kwargs) -> None: + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the group if in motion.""" await self._device.set_shutter_stop() - async def async_open_cover_tilt(self, **kwargs) -> None: + async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open the slats.""" await self._device.set_slats_level(HMIP_SLATS_OPEN) - async def async_close_cover_tilt(self, **kwargs) -> None: + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close the slats.""" await self._device.set_slats_level(HMIP_SLATS_CLOSED) - async def async_stop_cover_tilt(self, **kwargs) -> None: + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the group if in motion.""" await self._device.set_shutter_stop() diff --git a/homeassistant/components/insteon/cover.py b/homeassistant/components/insteon/cover.py index defa1acaa38..68f7f6156e3 100644 --- a/homeassistant/components/insteon/cover.py +++ b/homeassistant/components/insteon/cover.py @@ -1,5 +1,6 @@ """Support for Insteon covers via PowerLinc Modem.""" import math +from typing import Any from homeassistant.components.cover import ( ATTR_POSITION, @@ -59,15 +60,15 @@ class InsteonCoverEntity(InsteonEntity, CoverEntity): """Return the boolean response if the node is on.""" return bool(self.current_cover_position) - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open cover.""" await self._insteon_device.async_open() - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" await self._insteon_device.async_close() - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Set the cover position.""" position = int(kwargs[ATTR_POSITION] * 255 / 100) if position == 0: diff --git a/homeassistant/components/lutron/cover.py b/homeassistant/components/lutron/cover.py index 45b7751aa7c..67a0e093337 100644 --- a/homeassistant/components/lutron/cover.py +++ b/homeassistant/components/lutron/cover.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.cover import ( ATTR_POSITION, @@ -51,15 +52,15 @@ class LutronCover(LutronDevice, CoverEntity): """Return the current position of cover.""" return self._lutron_device.last_level() - def close_cover(self, **kwargs): + def close_cover(self, **kwargs: Any) -> None: """Close the cover.""" self._lutron_device.level = 0 - def open_cover(self, **kwargs): + def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" self._lutron_device.level = 100 - def set_cover_position(self, **kwargs): + def set_cover_position(self, **kwargs: Any) -> None: """Move the shade to a specific position.""" if ATTR_POSITION in kwargs: position = kwargs[ATTR_POSITION] diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py index 724dc4258da..68932a2a011 100644 --- a/homeassistant/components/lutron_caseta/cover.py +++ b/homeassistant/components/lutron_caseta/cover.py @@ -1,5 +1,7 @@ """Support for Lutron Caseta shades.""" +from typing import Any + from homeassistant.components.cover import ( ATTR_POSITION, DOMAIN, @@ -57,23 +59,23 @@ class LutronCasetaCover(LutronCasetaDeviceUpdatableEntity, CoverEntity): """Return the current position of cover.""" return self._device["current_state"] - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Top the cover.""" await self._smartbridge.stop_cover(self.device_id) - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self._smartbridge.lower_cover(self.device_id) self.async_update() self.async_write_ha_state() - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self._smartbridge.raise_cover(self.device_id) self.async_update() self.async_write_ha_state() - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the shade to a specific position.""" if ATTR_POSITION in kwargs: position = kwargs[ATTR_POSITION] diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 23e7e3d834a..301d2c6fbc7 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -1,5 +1,6 @@ """Support for Motion Blinds using their WLAN API.""" import logging +from typing import Any from motionblinds import DEVICE_TYPES_WIFI, BlindType import voluptuous as vol @@ -243,7 +244,7 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): return None return self._blind.position == 100 - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to multicast pushes and register signal handler.""" self._blind.Register_callback(self.unique_id, self.schedule_update_ha_state) await super().async_added_to_hass() @@ -288,19 +289,19 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): self.hass, UPDATE_INTERVAL_MOVING, self.async_scheduled_update_request ) - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Open) await self.async_request_position_till_stop() - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Close) await self.async_request_position_till_stop() - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] async with self._api_lock: @@ -327,7 +328,7 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): ) await self.async_request_position_till_stop() - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Stop) @@ -349,23 +350,23 @@ class MotionTiltDevice(MotionPositionDevice): return None return self._blind.angle * 100 / 180 - async def async_open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Set_angle, 180) - async def async_close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Set_angle, 0) - async def async_set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" angle = kwargs[ATTR_TILT_POSITION] * 180 / 100 async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Set_angle, angle) - async def async_stop_cover_tilt(self, **kwargs): + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the cover.""" async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Stop) @@ -463,19 +464,19 @@ class MotionTDBUDevice(MotionPositionDevice): attributes[ATTR_WIDTH] = self._blind.width return attributes - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Open, self._motor_key) await self.async_request_position_till_stop() - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Close, self._motor_key) await self.async_request_position_till_stop() - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific scaled position.""" position = kwargs[ATTR_POSITION] async with self._api_lock: @@ -496,7 +497,7 @@ class MotionTDBUDevice(MotionPositionDevice): await self.async_request_position_till_stop() - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" async with self._api_lock: await self.hass.async_add_executor_job(self._blind.Stop, self._motor_key) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 754fcb7ec44..8a32471ecec 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -3,6 +3,7 @@ from __future__ import annotations import functools import logging +from typing import Any import voluptuous as vol @@ -545,7 +546,7 @@ class MqttCover(MqttEntity, CoverEntity): return supported_features - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Move the cover up. This method is a coroutine. @@ -566,7 +567,7 @@ class MqttCover(MqttEntity, CoverEntity): ) self.async_write_ha_state() - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Move the cover down. This method is a coroutine. @@ -587,7 +588,7 @@ class MqttCover(MqttEntity, CoverEntity): ) self.async_write_ha_state() - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the device. This method is a coroutine. @@ -600,7 +601,7 @@ class MqttCover(MqttEntity, CoverEntity): self._config[CONF_ENCODING], ) - async def async_open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Tilt the cover open.""" tilt_open_position = self._config[CONF_TILT_OPEN_POSITION] variables = { @@ -625,7 +626,7 @@ class MqttCover(MqttEntity, CoverEntity): ) self.async_write_ha_state() - async def async_close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Tilt the cover closed.""" tilt_closed_position = self._config[CONF_TILT_CLOSED_POSITION] variables = { @@ -652,7 +653,7 @@ class MqttCover(MqttEntity, CoverEntity): ) self.async_write_ha_state() - async def async_set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" tilt = kwargs[ATTR_TILT_POSITION] percentage_tilt = tilt @@ -680,7 +681,7 @@ class MqttCover(MqttEntity, CoverEntity): self._tilt_value = percentage_tilt self.async_write_ha_state() - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] percentage_position = position @@ -711,7 +712,7 @@ class MqttCover(MqttEntity, CoverEntity): self._position = percentage_position self.async_write_ha_state() - async def async_toggle_tilt(self, **kwargs): + async def async_toggle_tilt(self, **kwargs: Any) -> None: """Toggle the entity.""" if self.is_tilt_closed(): await self.async_open_cover_tilt(**kwargs) diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index 861ae379007..fe8ef16bc89 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -1,4 +1,6 @@ """Support for MyQ-Enabled Garage Doors.""" +from typing import Any + from pymyq.const import DEVICE_TYPE_GATE as MYQ_DEVICE_TYPE_GATE from pymyq.errors import MyQError @@ -67,7 +69,7 @@ class MyQCover(MyQEntity, CoverEntity): """Return if the cover is opening or not.""" return MYQ_TO_HASS.get(self._device.state) == STATE_OPENING - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Issue close command to cover.""" if self.is_closing or self.is_closed: return @@ -90,7 +92,7 @@ class MyQCover(MyQEntity, CoverEntity): if not result: raise HomeAssistantError(f"Closing of cover {self._device.name} failed") - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Issue open command to cover.""" if self.is_opening or self.is_open: return diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index 9d5bd6e5cb6..ac0af64737a 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -1,5 +1,6 @@ """Platform for the opengarage.io cover component.""" import logging +from typing import Any from homeassistant.components.cover import ( CoverDeviceClass, @@ -62,7 +63,7 @@ class OpenGarageCover(OpenGarageEntity, CoverEntity): return None return self._state == STATE_OPENING - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" if self._state in [STATE_CLOSED, STATE_CLOSING]: return @@ -70,7 +71,7 @@ class OpenGarageCover(OpenGarageEntity, CoverEntity): self._state = STATE_CLOSING await self._push_button() - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" if self._state in [STATE_OPEN, STATE_OPENING]: return diff --git a/homeassistant/components/rflink/cover.py b/homeassistant/components/rflink/cover.py index cd109821582..91e68fa0fb8 100644 --- a/homeassistant/components/rflink/cover.py +++ b/homeassistant/components/rflink/cover.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -155,15 +156,15 @@ class RflinkCover(RflinkCommand, CoverEntity, RestoreEntity): """Return True because covers can be stopped midway.""" return True - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Turn the device close.""" await self._async_handle_command("close_cover") - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Turn the device open.""" await self._async_handle_command("open_cover") - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Turn the device stop.""" await self._async_handle_command("stop_cover") diff --git a/homeassistant/components/scsgate/cover.py b/homeassistant/components/scsgate/cover.py index ddfb59c6fba..8d94f4214af 100644 --- a/homeassistant/components/scsgate/cover.py +++ b/homeassistant/components/scsgate/cover.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from scsgate.tasks import ( HaltRollerShutterTask, @@ -85,15 +86,15 @@ class SCSGateCover(CoverEntity): """Return if the cover is closed.""" return None - def open_cover(self, **kwargs): + def open_cover(self, **kwargs: Any) -> None: """Move the cover.""" self._scsgate.append_task(RaiseRollerShutterTask(target=self._scs_id)) - def close_cover(self, **kwargs): + def close_cover(self, **kwargs: Any) -> None: """Move the cover down.""" self._scsgate.append_task(LowerRollerShutterTask(target=self._scs_id)) - def stop_cover(self, **kwargs): + def stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" self._scsgate.append_task(HaltRollerShutterTask(target=self._scs_id)) diff --git a/homeassistant/components/slide/cover.py b/homeassistant/components/slide/cover.py index 7841be50977..52fcc3c8da4 100644 --- a/homeassistant/components/slide/cover.py +++ b/homeassistant/components/slide/cover.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.cover import ATTR_POSITION, CoverDeviceClass, CoverEntity from homeassistant.const import ATTR_ID, STATE_CLOSED, STATE_CLOSING, STATE_OPENING @@ -83,21 +84,21 @@ class SlideCover(CoverEntity): pos = int(pos * 100) return pos - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" self._slide["state"] = STATE_OPENING await self._api.slide_open(self._id) - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" self._slide["state"] = STATE_CLOSING await self._api.slide_close(self._id) - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self._api.slide_stop(self._id) - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" position = kwargs[ATTR_POSITION] / 100 if not self._invert: diff --git a/homeassistant/components/smartthings/cover.py b/homeassistant/components/smartthings/cover.py index 578b13879e8..7e2de17cad3 100644 --- a/homeassistant/components/smartthings/cover.py +++ b/homeassistant/components/smartthings/cover.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Sequence +from typing import Any from pysmartthings import Attribute, Capability @@ -81,7 +82,7 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): if Capability.switch_level in device.capabilities: self._attr_supported_features |= CoverEntityFeature.SET_POSITION - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" # Same command for all 3 supported capabilities await self._device.close(set_status=True) @@ -89,7 +90,7 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): # the entity state ahead of receiving the confirming push updates self.async_schedule_update_ha_state(True) - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" # Same for all capability types await self._device.open(set_status=True) diff --git a/homeassistant/components/soma/cover.py b/homeassistant/components/soma/cover.py index abc6d828acc..5777e904597 100644 --- a/homeassistant/components/soma/cover.py +++ b/homeassistant/components/soma/cover.py @@ -1,6 +1,8 @@ """Support for Soma Covers.""" from __future__ import annotations +from typing import Any + from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, @@ -59,7 +61,7 @@ class SomaTilt(SomaEntity, CoverEntity): """Return if the cover tilt is closed.""" return self.current_position == 0 - def close_cover_tilt(self, **kwargs): + def close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" response = self.api.set_shade_position(self.device["mac"], 100) if not is_api_response_success(response): @@ -68,7 +70,7 @@ class SomaTilt(SomaEntity, CoverEntity): ) self.set_position(0) - def open_cover_tilt(self, **kwargs): + def open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" response = self.api.set_shade_position(self.device["mac"], -100) if not is_api_response_success(response): @@ -77,7 +79,7 @@ class SomaTilt(SomaEntity, CoverEntity): ) self.set_position(100) - def stop_cover_tilt(self, **kwargs): + def stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the cover tilt.""" response = self.api.stop_shade(self.device["mac"]) if not is_api_response_success(response): @@ -87,7 +89,7 @@ class SomaTilt(SomaEntity, CoverEntity): # Set cover position to some value where up/down are both enabled self.set_position(50) - def set_cover_tilt_position(self, **kwargs): + def set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" # 0 -> Closed down (api: 100) # 50 -> Fully open (api: 0) @@ -133,7 +135,7 @@ class SomaShade(SomaEntity, CoverEntity): """Return if the cover is closed.""" return self.current_position == 0 - def close_cover(self, **kwargs): + def close_cover(self, **kwargs: Any) -> None: """Close the cover.""" response = self.api.set_shade_position(self.device["mac"], 100) if not is_api_response_success(response): @@ -141,7 +143,7 @@ class SomaShade(SomaEntity, CoverEntity): f'Error while closing the cover ({self.name}): {response["msg"]}' ) - def open_cover(self, **kwargs): + def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" response = self.api.set_shade_position(self.device["mac"], 0) if not is_api_response_success(response): @@ -149,7 +151,7 @@ class SomaShade(SomaEntity, CoverEntity): f'Error while opening the cover ({self.name}): {response["msg"]}' ) - def stop_cover(self, **kwargs): + def stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" response = self.api.stop_shade(self.device["mac"]) if not is_api_response_success(response): @@ -159,7 +161,7 @@ class SomaShade(SomaEntity, CoverEntity): # Set cover position to some value where up/down are both enabled self.set_position(50) - def set_cover_position(self, **kwargs): + def set_cover_position(self, **kwargs: Any) -> None: """Move the cover shutter to a specific position.""" self.current_position = kwargs[ATTR_POSITION] response = self.api.set_shade_position( diff --git a/homeassistant/components/somfy_mylink/cover.py b/homeassistant/components/somfy_mylink/cover.py index d1b09175deb..43c9ca63bb5 100644 --- a/homeassistant/components/somfy_mylink/cover.py +++ b/homeassistant/components/somfy_mylink/cover.py @@ -1,5 +1,6 @@ """Cover Platform for the Somfy MyLink component.""" import logging +from typing import Any from homeassistant.components.cover import CoverDeviceClass, CoverEntity from homeassistant.config_entries import ConfigEntry @@ -87,7 +88,7 @@ class SomfyShade(RestoreEntity, CoverEntity): name=name, ) - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" self._attr_is_closing = True self.async_write_ha_state() @@ -102,7 +103,7 @@ class SomfyShade(RestoreEntity, CoverEntity): self._attr_is_closing = None self.async_write_ha_state() - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" self._attr_is_opening = True self.async_write_ha_state() @@ -117,11 +118,11 @@ class SomfyShade(RestoreEntity, CoverEntity): self._attr_is_opening = None self.async_write_ha_state() - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self.somfy_mylink.move_stop(self._target_id) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Complete the initialization.""" await super().async_added_to_hass() # Restore the last state diff --git a/homeassistant/components/supla/cover.py b/homeassistant/components/supla/cover.py index 172f1cadf43..b1cc0951259 100644 --- a/homeassistant/components/supla/cover.py +++ b/homeassistant/components/supla/cover.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging from pprint import pformat +from typing import Any from homeassistant.components.cover import ATTR_POSITION, CoverDeviceClass, CoverEntity from homeassistant.core import HomeAssistant @@ -65,7 +66,7 @@ class SuplaCover(SuplaChannel, CoverEntity): return 100 - state["shut"] return None - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" await self.async_action("REVEAL", percentage=kwargs.get(ATTR_POSITION)) @@ -76,15 +77,15 @@ class SuplaCover(SuplaChannel, CoverEntity): return None return self.current_cover_position == 0 - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self.async_action("REVEAL") - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self.async_action("SHUT") - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self.async_action("STOP") @@ -100,21 +101,21 @@ class SuplaGateDoor(SuplaChannel, CoverEntity): return state.get("hi") return None - async def async_open_cover(self, **kwargs) -> None: + async def async_open_cover(self, **kwargs: Any) -> None: """Open the gate.""" if self.is_closed: await self.async_action("OPEN_CLOSE") - async def async_close_cover(self, **kwargs) -> None: + async def async_close_cover(self, **kwargs: Any) -> None: """Close the gate.""" if not self.is_closed: await self.async_action("OPEN_CLOSE") - async def async_stop_cover(self, **kwargs) -> None: + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the gate.""" await self.async_action("OPEN_CLOSE") - async def async_toggle(self, **kwargs) -> None: + async def async_toggle(self, **kwargs: Any) -> None: """Toggle the gate.""" await self.async_action("OPEN_CLOSE") diff --git a/homeassistant/components/tellduslive/cover.py b/homeassistant/components/tellduslive/cover.py index b64c3f887a0..49c35ac3114 100644 --- a/homeassistant/components/tellduslive/cover.py +++ b/homeassistant/components/tellduslive/cover.py @@ -1,4 +1,6 @@ """Support for Tellstick covers using Tellstick Net.""" +from typing import Any + from homeassistant.components import cover, tellduslive from homeassistant.components.cover import CoverEntity from homeassistant.config_entries import ConfigEntry @@ -36,17 +38,17 @@ class TelldusLiveCover(TelldusLiveEntity, CoverEntity): """Return the current position of the cover.""" return self.device.is_down - def close_cover(self, **kwargs): + def close_cover(self, **kwargs: Any) -> None: """Close the cover.""" self.device.down() self._update_callback() - def open_cover(self, **kwargs): + def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" self.device.up() self._update_callback() - def stop_cover(self, **kwargs): + def stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" self.device.stop() self._update_callback() diff --git a/homeassistant/components/tellstick/cover.py b/homeassistant/components/tellstick/cover.py index 65f11bdbae6..7c38741960b 100644 --- a/homeassistant/components/tellstick/cover.py +++ b/homeassistant/components/tellstick/cover.py @@ -1,6 +1,8 @@ """Support for Tellstick covers.""" from __future__ import annotations +from typing import Any + from homeassistant.components.cover import CoverEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -51,15 +53,15 @@ class TellstickCover(TellstickDevice, CoverEntity): """Return True if unable to access real state of the entity.""" return True - def close_cover(self, **kwargs): + def close_cover(self, **kwargs: Any) -> None: """Close the cover.""" self._tellcore_device.down() - def open_cover(self, **kwargs): + def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" self._tellcore_device.up() - def stop_cover(self, **kwargs): + def stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" self._tellcore_device.stop() diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 53b829cac9e..0c86b1d5d5a 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -180,7 +181,7 @@ class CoverTemplate(TemplateEntity, CoverEntity): self._is_closing = False self._tilt_value = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" if self._template: self.add_template_attribute( @@ -322,7 +323,7 @@ class CoverTemplate(TemplateEntity, CoverEntity): return supported_features - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Move the cover up.""" if self._open_script: await self.async_run_script(self._open_script, context=self._context) @@ -336,7 +337,7 @@ class CoverTemplate(TemplateEntity, CoverEntity): self._position = 100 self.async_write_ha_state() - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Move the cover down.""" if self._close_script: await self.async_run_script(self._close_script, context=self._context) @@ -350,12 +351,12 @@ class CoverTemplate(TemplateEntity, CoverEntity): self._position = 0 self.async_write_ha_state() - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Fire the stop action.""" if self._stop_script: await self.async_run_script(self._stop_script, context=self._context) - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Set cover position.""" self._position = kwargs[ATTR_POSITION] await self.async_run_script( @@ -366,7 +367,7 @@ class CoverTemplate(TemplateEntity, CoverEntity): if self._optimistic: self.async_write_ha_state() - async def async_open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Tilt the cover open.""" self._tilt_value = 100 await self.async_run_script( @@ -377,7 +378,7 @@ class CoverTemplate(TemplateEntity, CoverEntity): if self._tilt_optimistic: self.async_write_ha_state() - async def async_close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Tilt the cover closed.""" self._tilt_value = 0 await self.async_run_script( @@ -388,7 +389,7 @@ class CoverTemplate(TemplateEntity, CoverEntity): if self._tilt_optimistic: self.async_write_ha_state() - async def async_set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" self._tilt_value = kwargs[ATTR_TILT_POSITION] await self.async_run_script( diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index e75b07b988d..d8b0a97480e 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -360,7 +360,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity): ] ) - def set_cover_tilt_position(self, **kwargs): + def set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" if self._tilt is None: raise RuntimeError( diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index 26cccfce6ce..1c8a4afcc6f 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -1,6 +1,8 @@ """Support for Velux covers.""" from __future__ import annotations +from typing import Any + from pyvlx import OpeningDevice, Position from pyvlx.opening_device import Awning, Blind, GarageDoor, Gate, RollerShutter, Window @@ -89,15 +91,15 @@ class VeluxCover(VeluxEntity, CoverEntity): """Return if the cover is closed.""" return self.node.position.closed - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self.node.close(wait_for_completion=False) - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self.node.open(wait_for_completion=False) - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" position_percent = 100 - kwargs[ATTR_POSITION] @@ -105,23 +107,23 @@ class VeluxCover(VeluxEntity, CoverEntity): Position(position_percent=position_percent), wait_for_completion=False ) - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self.node.stop(wait_for_completion=False) - async def async_close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close cover tilt.""" await self.node.close_orientation(wait_for_completion=False) - async def async_open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open cover tilt.""" await self.node.open_orientation(wait_for_completion=False) - async def async_stop_cover_tilt(self, **kwargs): + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop cover tilt.""" await self.node.stop_orientation(wait_for_completion=False) - async def async_set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move cover tilt to a specific position.""" position_percent = 100 - kwargs[ATTR_TILT_POSITION] orientation = Position(position_percent=position_percent) diff --git a/homeassistant/components/vera/cover.py b/homeassistant/components/vera/cover.py index 2f1a602ca19..5baf495b5fd 100644 --- a/homeassistant/components/vera/cover.py +++ b/homeassistant/components/vera/cover.py @@ -55,7 +55,7 @@ class VeraCover(VeraDevice[veraApi.VeraCurtain], CoverEntity): return 100 return position - def set_cover_position(self, **kwargs) -> None: + def set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" self.vera_device.set_level(kwargs.get(ATTR_POSITION)) self.schedule_update_ha_state() diff --git a/homeassistant/components/wilight/cover.py b/homeassistant/components/wilight/cover.py index ad94c224518..6ee4a857d36 100644 --- a/homeassistant/components/wilight/cover.py +++ b/homeassistant/components/wilight/cover.py @@ -1,4 +1,6 @@ """Support for WiLight Cover.""" +from typing import Any + from pywilight.const import ( COVER_V1, ITEM_COVER, @@ -86,19 +88,19 @@ class WiLightCover(WiLightDevice, CoverEntity): and wilight_to_hass_position(self._status["position_current"]) == 0 ) - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self._client.cover_command(self._index, WL_OPEN) - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" await self._client.cover_command(self._index, WL_CLOSE) - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" position = hass_to_wilight_position(kwargs[ATTR_POSITION]) await self._client.set_cover_position(self._index, position) - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self._client.cover_command(self._index, WL_STOP) diff --git a/homeassistant/components/xiaomi_aqara/cover.py b/homeassistant/components/xiaomi_aqara/cover.py index 422d9b21e0d..e9946e37815 100644 --- a/homeassistant/components/xiaomi_aqara/cover.py +++ b/homeassistant/components/xiaomi_aqara/cover.py @@ -1,4 +1,6 @@ """Support for Xiaomi curtain.""" +from typing import Any + from homeassistant.components.cover import ATTR_POSITION, CoverEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -53,19 +55,19 @@ class XiaomiGenericCover(XiaomiDevice, CoverEntity): """Return if the cover is closed.""" return self.current_cover_position <= 0 - def close_cover(self, **kwargs): + def close_cover(self, **kwargs: Any) -> None: """Close the cover.""" self._write_to_hub(self._sid, **{self._data_key: "close"}) - def open_cover(self, **kwargs): + def open_cover(self, **kwargs: Any) -> None: """Open the cover.""" self._write_to_hub(self._sid, **{self._data_key: "open"}) - def stop_cover(self, **kwargs): + def stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" self._write_to_hub(self._sid, **{self._data_key: "stop"}) - def set_cover_position(self, **kwargs): + def set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" position = kwargs.get(ATTR_POSITION) if self._data_key == DATA_KEY_PROTO_V2: diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 413e7e9ae09..39f76b6b77f 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio import functools import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from zigpy.zcl.foundation import Status @@ -77,7 +77,7 @@ class ZhaCover(ZhaEntity, CoverEntity): self._cover_channel = self.cluster_channels.get(CHANNEL_COVER) self._current_position = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() self.async_accept_signal( @@ -134,19 +134,19 @@ class ZhaCover(ZhaEntity, CoverEntity): self._state = state self.async_write_ha_state() - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the window cover.""" res = await self._cover_channel.up_open() if not isinstance(res, Exception) and res[1] is Status.SUCCESS: self.async_update_state(STATE_OPENING) - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close the window cover.""" res = await self._cover_channel.down_close() if not isinstance(res, Exception) and res[1] is Status.SUCCESS: self.async_update_state(STATE_CLOSING) - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the roller shutter to a specific position.""" new_pos = kwargs[ATTR_POSITION] res = await self._cover_channel.go_to_lift_percentage(100 - new_pos) @@ -155,7 +155,7 @@ class ZhaCover(ZhaEntity, CoverEntity): STATE_CLOSING if new_pos < self._current_position else STATE_OPENING ) - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the window cover.""" res = await self._cover_channel.stop() if not isinstance(res, Exception) and res[1] is Status.SUCCESS: @@ -221,7 +221,7 @@ class Shade(ZhaEntity, CoverEntity): return None return not self._is_open - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() self.async_accept_signal( @@ -251,7 +251,7 @@ class Shade(ZhaEntity, CoverEntity): self._position = int(value * 100 / 255) self.async_write_ha_state() - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the window cover.""" res = await self._on_off_channel.on() if isinstance(res, Exception) or res[1] != Status.SUCCESS: @@ -261,7 +261,7 @@ class Shade(ZhaEntity, CoverEntity): self._is_open = True self.async_write_ha_state() - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Close the window cover.""" res = await self._on_off_channel.off() if isinstance(res, Exception) or res[1] != Status.SUCCESS: @@ -271,7 +271,7 @@ class Shade(ZhaEntity, CoverEntity): self._is_open = False self.async_write_ha_state() - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the roller shutter to a specific position.""" new_pos = kwargs[ATTR_POSITION] res = await self._level_channel.move_to_level_with_on_off( @@ -285,7 +285,7 @@ class Shade(ZhaEntity, CoverEntity): self._position = new_pos self.async_write_ha_state() - async def async_stop_cover(self, **kwargs) -> None: + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" res = await self._level_channel.stop() if isinstance(res, Exception) or res[1] != Status.SUCCESS: @@ -301,7 +301,7 @@ class KeenVent(Shade): _attr_device_class = CoverDeviceClass.DAMPER - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" position = self._position or 100 tasks = [ diff --git a/homeassistant/components/zwave_me/cover.py b/homeassistant/components/zwave_me/cover.py index 7857306ef1f..5e2fdba8608 100644 --- a/homeassistant/components/zwave_me/cover.py +++ b/homeassistant/components/zwave_me/cover.py @@ -53,11 +53,11 @@ class ZWaveMeCover(ZWaveMeEntity, CoverEntity): | CoverEntityFeature.SET_POSITION ) - def close_cover(self, **kwargs): + def close_cover(self, **kwargs: Any) -> None: """Close cover.""" self.controller.zwave_api.send_command(self.device.id, "exact?level=0") - def open_cover(self, **kwargs): + def open_cover(self, **kwargs: Any) -> None: """Open cover.""" self.controller.zwave_api.send_command(self.device.id, "exact?level=99") From d1708861db137a3da1b3d65020144a8bb953fbdf Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Fri, 24 Jun 2022 09:39:48 +0300 Subject: [PATCH 1769/3516] Add config flow for `simplepush` (#73471) * Add config flow for `simplepush` * fix warning message * fix typos * Add importing yaml config * patch integration setup * Add check for errrors raised by the library * fix coverage * Adjust comment and logging message Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + CODEOWNERS | 2 + .../components/simplepush/__init__.py | 38 +++++ .../components/simplepush/config_flow.py | 88 +++++++++++ homeassistant/components/simplepush/const.py | 13 ++ .../components/simplepush/manifest.json | 3 +- homeassistant/components/simplepush/notify.py | 83 +++++++--- .../components/simplepush/strings.json | 21 +++ .../simplepush/translations/en.json | 21 +++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/simplepush/__init__.py | 1 + .../components/simplepush/test_config_flow.py | 144 ++++++++++++++++++ 13 files changed, 394 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/simplepush/config_flow.py create mode 100644 homeassistant/components/simplepush/const.py create mode 100644 homeassistant/components/simplepush/strings.json create mode 100644 homeassistant/components/simplepush/translations/en.json create mode 100644 tests/components/simplepush/__init__.py create mode 100644 tests/components/simplepush/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 0e7a7324064..928f4d7789e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1065,6 +1065,7 @@ omit = homeassistant/components/shelly/sensor.py homeassistant/components/shelly/utils.py homeassistant/components/sigfox/sensor.py + homeassistant/components/simplepush/__init__.py homeassistant/components/simplepush/notify.py homeassistant/components/simplisafe/__init__.py homeassistant/components/simplisafe/alarm_control_panel.py diff --git a/CODEOWNERS b/CODEOWNERS index 9de118552aa..e2d0cbdaa3f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -929,6 +929,8 @@ build.json @home-assistant/supervisor /tests/components/sighthound/ @robmarkcole /homeassistant/components/signal_messenger/ @bbernhard /tests/components/signal_messenger/ @bbernhard +/homeassistant/components/simplepush/ @engrbm87 +/tests/components/simplepush/ @engrbm87 /homeassistant/components/simplisafe/ @bachya /tests/components/simplisafe/ @bachya /homeassistant/components/sinch/ @bendikrb diff --git a/homeassistant/components/simplepush/__init__.py b/homeassistant/components/simplepush/__init__.py index 8253cfad8b4..c5782258cb7 100644 --- a/homeassistant/components/simplepush/__init__.py +++ b/homeassistant/components/simplepush/__init__.py @@ -1 +1,39 @@ """The simplepush component.""" + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import discovery +from homeassistant.helpers.typing import ConfigType + +from .const import DATA_HASS_CONFIG, DOMAIN + +PLATFORMS = [Platform.NOTIFY] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the simplepush component.""" + + hass.data[DATA_HASS_CONFIG] = config + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up simplepush from a config entry.""" + + hass.async_create_task( + discovery.async_load_platform( + hass, + Platform.NOTIFY, + DOMAIN, + dict(entry.data), + hass.data[DATA_HASS_CONFIG], + ) + ) + + return True + + +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/simplepush/config_flow.py b/homeassistant/components/simplepush/config_flow.py new file mode 100644 index 00000000000..cf08a341114 --- /dev/null +++ b/homeassistant/components/simplepush/config_flow.py @@ -0,0 +1,88 @@ +"""Config flow for simplepush integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from simplepush import UnknownError, send, send_encrypted +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_NAME, CONF_PASSWORD +from homeassistant.data_entry_flow import FlowResult + +from .const import ATTR_ENCRYPTED, CONF_DEVICE_KEY, CONF_SALT, DEFAULT_NAME, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +def validate_input(entry: dict[str, str]) -> dict[str, str] | None: + """Validate user input.""" + try: + if CONF_PASSWORD in entry: + send_encrypted( + entry[CONF_DEVICE_KEY], + entry[CONF_PASSWORD], + entry[CONF_PASSWORD], + "HA test", + "Message delivered successfully", + ) + else: + send(entry[CONF_DEVICE_KEY], "HA test", "Message delivered successfully") + except UnknownError: + return {"base": "cannot_connect"} + + return None + + +class SimplePushFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for simplepush.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by the user.""" + errors: dict[str, str] | None = None + if user_input is not None: + + await self.async_set_unique_id(user_input[CONF_DEVICE_KEY]) + self._abort_if_unique_id_configured() + + self._async_abort_entries_match( + { + CONF_NAME: user_input[CONF_NAME], + } + ) + + if not ( + errors := await self.hass.async_add_executor_job( + validate_input, user_input + ) + ): + return self.async_create_entry( + title=user_input[CONF_NAME], + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_DEVICE_KEY): str, + vol.Required(CONF_NAME, default=DEFAULT_NAME): str, + vol.Inclusive(CONF_PASSWORD, ATTR_ENCRYPTED): str, + vol.Inclusive(CONF_SALT, ATTR_ENCRYPTED): str, + } + ), + errors=errors, + ) + + async def async_step_import(self, import_config: dict[str, str]) -> FlowResult: + """Import a config entry from configuration.yaml.""" + _LOGGER.warning( + "Configuration of the simplepush integration in YAML is deprecated and " + "will be removed in a future release; Your existing configuration " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file" + ) + return await self.async_step_user(import_config) diff --git a/homeassistant/components/simplepush/const.py b/homeassistant/components/simplepush/const.py new file mode 100644 index 00000000000..6195a5fd1d9 --- /dev/null +++ b/homeassistant/components/simplepush/const.py @@ -0,0 +1,13 @@ +"""Constants for the simplepush integration.""" + +from typing import Final + +DOMAIN: Final = "simplepush" +DEFAULT_NAME: Final = "simplepush" +DATA_HASS_CONFIG: Final = "simplepush_hass_config" + +ATTR_ENCRYPTED: Final = "encrypted" +ATTR_EVENT: Final = "event" + +CONF_DEVICE_KEY: Final = "device_key" +CONF_SALT: Final = "salt" diff --git a/homeassistant/components/simplepush/manifest.json b/homeassistant/components/simplepush/manifest.json index 26321d17aef..7c37546485a 100644 --- a/homeassistant/components/simplepush/manifest.json +++ b/homeassistant/components/simplepush/manifest.json @@ -3,7 +3,8 @@ "name": "Simplepush", "documentation": "https://www.home-assistant.io/integrations/simplepush", "requirements": ["simplepush==1.1.4"], - "codeowners": [], + "codeowners": ["@engrbm87"], + "config_flow": true, "iot_class": "cloud_polling", "loggers": ["simplepush"] } diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index 5a83dec69f0..e9cd9813175 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -1,5 +1,10 @@ """Simplepush notification service.""" -from simplepush import send, send_encrypted +from __future__ import annotations + +import logging +from typing import Any + +from simplepush import BadRequest, UnknownError, send, send_encrypted import voluptuous as vol from homeassistant.components.notify import ( @@ -8,14 +13,16 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.components.notify.const import ATTR_DATA +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_EVENT, CONF_PASSWORD +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -ATTR_ENCRYPTED = "encrypted" - -CONF_DEVICE_KEY = "device_key" -CONF_SALT = "salt" +from .const import ATTR_ENCRYPTED, ATTR_EVENT, CONF_DEVICE_KEY, CONF_SALT, DOMAIN +# Configuring simplepush under the notify platform will be removed in 2022.9.0 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_DEVICE_KEY): cv.string, @@ -25,34 +32,62 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +_LOGGER = logging.getLogger(__name__) -def get_service(hass, config, discovery_info=None): + +async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> SimplePushNotificationService | None: """Get the Simplepush notification service.""" - return SimplePushNotificationService(config) + if discovery_info is None: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) + return None + + return SimplePushNotificationService(discovery_info) class SimplePushNotificationService(BaseNotificationService): """Implementation of the notification service for Simplepush.""" - def __init__(self, config): + def __init__(self, config: dict[str, Any]) -> None: """Initialize the Simplepush notification service.""" - self._device_key = config.get(CONF_DEVICE_KEY) - self._event = config.get(CONF_EVENT) - self._password = config.get(CONF_PASSWORD) - self._salt = config.get(CONF_SALT) + self._device_key: str = config[CONF_DEVICE_KEY] + self._event: str | None = config.get(CONF_EVENT) + self._password: str | None = config.get(CONF_PASSWORD) + self._salt: str | None = config.get(CONF_SALT) - def send_message(self, message="", **kwargs): + def send_message(self, message: str, **kwargs: Any) -> None: """Send a message to a Simplepush user.""" title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - if self._password: - send_encrypted( - self._device_key, - self._password, - self._salt, - title, - message, - event=self._event, - ) - else: - send(self._device_key, title, message, event=self._event) + # event can now be passed in the service data + event = None + if data := kwargs.get(ATTR_DATA): + event = data.get(ATTR_EVENT) + + # use event from config until YAML config is removed + event = event or self._event + + try: + if self._password: + send_encrypted( + self._device_key, + self._password, + self._salt, + title, + message, + event=event, + ) + else: + send(self._device_key, title, message, event=event) + + except BadRequest: + _LOGGER.error("Bad request. Title or message are too long") + except UnknownError: + _LOGGER.error("Failed to send the notification") diff --git a/homeassistant/components/simplepush/strings.json b/homeassistant/components/simplepush/strings.json new file mode 100644 index 00000000000..0031dc32340 --- /dev/null +++ b/homeassistant/components/simplepush/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "device_key": "The device key of your device", + "event": "The event for the events.", + "password": "The password of the encryption used by your device", + "salt": "The salt used by your device." + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/simplepush/translations/en.json b/homeassistant/components/simplepush/translations/en.json new file mode 100644 index 00000000000..a36a3b2b273 --- /dev/null +++ b/homeassistant/components/simplepush/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "device_key": "The device key of your device", + "event": "The event for the events.", + "name": "Name", + "password": "The password of the encryption used by your device", + "salt": "The salt used by your device." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6b26b2a99b7..3c6ad94a21f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -311,6 +311,7 @@ FLOWS = { "shelly", "shopping_list", "sia", + "simplepush", "simplisafe", "skybell", "slack", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b41bf1b223d..a8fd3b39dbf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1430,6 +1430,9 @@ sharkiq==0.0.1 # homeassistant.components.sighthound simplehound==0.3 +# homeassistant.components.simplepush +simplepush==1.1.4 + # homeassistant.components.simplisafe simplisafe-python==2022.06.0 diff --git a/tests/components/simplepush/__init__.py b/tests/components/simplepush/__init__.py new file mode 100644 index 00000000000..fd40577f8fa --- /dev/null +++ b/tests/components/simplepush/__init__.py @@ -0,0 +1 @@ +"""Tests for the simeplush integration.""" diff --git a/tests/components/simplepush/test_config_flow.py b/tests/components/simplepush/test_config_flow.py new file mode 100644 index 00000000000..4636df6b28f --- /dev/null +++ b/tests/components/simplepush/test_config_flow.py @@ -0,0 +1,144 @@ +"""Test Simplepush config flow.""" +from unittest.mock import patch + +import pytest +from simplepush import UnknownError + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.simplepush.const import CONF_DEVICE_KEY, CONF_SALT, DOMAIN +from homeassistant.const import CONF_NAME, CONF_PASSWORD +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +MOCK_CONFIG = { + CONF_DEVICE_KEY: "abc", + CONF_NAME: "simplepush", +} + + +@pytest.fixture(autouse=True) +def simplepush_setup_fixture(): + """Patch simplepush setup entry.""" + with patch( + "homeassistant.components.simplepush.async_setup_entry", return_value=True + ): + yield + + +@pytest.fixture(autouse=True) +def mock_api_request(): + """Patch simplepush api request.""" + with patch("homeassistant.components.simplepush.config_flow.send"), patch( + "homeassistant.components.simplepush.config_flow.send_encrypted" + ): + yield + + +async def test_flow_successful(hass: HomeAssistant) -> None: + """Test user initialized flow with minimum config.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "simplepush" + assert result["data"] == MOCK_CONFIG + + +async def test_flow_with_password(hass: HomeAssistant) -> None: + """Test user initialized flow with password and salt.""" + mock_config_pass = {**MOCK_CONFIG, CONF_PASSWORD: "password", CONF_SALT: "salt"} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=mock_config_pass, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "simplepush" + assert result["data"] == mock_config_pass + + +async def test_flow_user_device_key_already_configured(hass: HomeAssistant) -> None: + """Test user initialized flow with duplicate device key.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + unique_id="abc", + ) + + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_user_name_already_configured(hass: HomeAssistant) -> None: + """Test user initialized flow with duplicate name.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + unique_id="abc", + ) + + entry.add_to_hass(hass) + + new_entry = MOCK_CONFIG.copy() + new_entry[CONF_DEVICE_KEY] = "abc1" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_error_on_connection_failure(hass: HomeAssistant) -> None: + """Test when connection to api fails.""" + with patch( + "homeassistant.components.simplepush.config_flow.send", + side_effect=UnknownError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_flow_import(hass: HomeAssistant) -> None: + """Test an import flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_CONFIG, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "simplepush" + assert result["data"] == MOCK_CONFIG From 2f78faa7181f8832a68fb102e95a92feb9c56e59 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Jun 2022 01:44:35 -0500 Subject: [PATCH 1770/3516] Make aiohttp mockers aware of the json loads kwarg (#73939) --- homeassistant/util/aiohttp.py | 8 +++++--- tests/test_util/aiohttp.py | 8 ++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/util/aiohttp.py b/homeassistant/util/aiohttp.py index aa1aea1abc3..05ade335a53 100644 --- a/homeassistant/util/aiohttp.py +++ b/homeassistant/util/aiohttp.py @@ -3,13 +3,15 @@ from __future__ import annotations from http import HTTPStatus import io -import json from typing import Any from urllib.parse import parse_qsl from aiohttp import payload, web +from aiohttp.typedefs import JSONDecoder from multidict import CIMultiDict, MultiDict +from homeassistant.helpers.json import json_loads + class MockStreamReader: """Small mock to imitate stream reader.""" @@ -64,9 +66,9 @@ class MockRequest: """Return the body as text.""" return MockStreamReader(self._content) - async def json(self) -> Any: + async def json(self, loads: JSONDecoder = json_loads) -> Any: """Return the body as JSON.""" - return json.loads(self._text) + return loads(self._text) async def post(self) -> MultiDict[str]: """Return POST parameters.""" diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index d69c6d7c290..9ed47109210 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -2,7 +2,6 @@ import asyncio from contextlib import contextmanager from http import HTTPStatus -import json as _json import re from unittest import mock from urllib.parse import parse_qs @@ -14,6 +13,7 @@ from multidict import CIMultiDict from yarl import URL from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE +from homeassistant.helpers.json import json_dumps, json_loads RETYPE = type(re.compile("")) @@ -169,7 +169,7 @@ class AiohttpClientMockResponse: ): """Initialize a fake response.""" if json is not None: - text = _json.dumps(json) + text = json_dumps(json) if text is not None: response = text.encode("utf-8") if response is None: @@ -252,9 +252,9 @@ class AiohttpClientMockResponse: """Return mock response as a string.""" return self.response.decode(encoding, errors=errors) - async def json(self, encoding="utf-8", content_type=None): + async def json(self, encoding="utf-8", content_type=None, loads=json_loads): """Return mock response as a json.""" - return _json.loads(self.response.decode(encoding)) + return loads(self.response.decode(encoding)) def release(self): """Mock release.""" From 6cafcb016fc01713a6717245677c35d8cc7b929e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 24 Jun 2022 10:22:40 +0200 Subject: [PATCH 1771/3516] Adjust rfxtrx cover type hints (#73947) --- homeassistant/components/rfxtrx/cover.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index ceb34520b07..6bf49beb89a 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import RFXtrx as rfxtrxmod @@ -65,13 +66,13 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): device: rfxtrxmod.RFXtrxDevice, device_id: DeviceTuple, event: rfxtrxmod.RFXtrxEvent = None, - venetian_blind_mode: bool | None = None, + venetian_blind_mode: str | None = None, ) -> None: """Initialize the RFXtrx cover device.""" super().__init__(device, device_id, event) self._venetian_blind_mode = venetian_blind_mode - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore device state.""" await super().async_added_to_hass() @@ -81,7 +82,7 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): self._state = old_state.state == STATE_OPEN @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" supported_features = ( CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP @@ -100,11 +101,11 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): return supported_features @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the cover is closed.""" return not self._state - async def async_open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs: Any) -> None: """Move the cover up.""" if self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_US: await self._async_send(self._device.send_up05sec) @@ -115,7 +116,7 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): self._state = True self.async_write_ha_state() - async def async_close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs: Any) -> None: """Move the cover down.""" if self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_US: await self._async_send(self._device.send_down05sec) @@ -126,27 +127,27 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): self._state = False self.async_write_ha_state() - async def async_stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self._async_send(self._device.send_stop) self._state = True self.async_write_ha_state() - async def async_open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Tilt the cover up.""" if self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_US: await self._async_send(self._device.send_up2sec) elif self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_EU: await self._async_send(self._device.send_up05sec) - async def async_close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Tilt the cover down.""" if self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_US: await self._async_send(self._device.send_down2sec) elif self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_EU: await self._async_send(self._device.send_down05sec) - async def async_stop_cover_tilt(self, **kwargs): + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the cover tilt.""" await self._async_send(self._device.send_stop) self._state = True From f29cc33fa0652b3d1c1c71081344d7b5b80e174a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Jun 2022 08:43:35 -0500 Subject: [PATCH 1772/3516] Fix selecting entity_ids and device_ids in events with MySQL and PostgreSQL with logbook (#73918) * Fix selecting entity_ids and device_ids in events with MySQL and PostgreSQL Fixes #73818 * add cover --- .../components/logbook/queries/__init__.py | 17 ++--- .../components/logbook/queries/devices.py | 5 +- .../components/logbook/queries/entities.py | 29 +++++--- .../logbook/queries/entities_and_devices.py | 30 ++++---- .../components/logbook/test_websocket_api.py | 72 +++++++++++++++---- 5 files changed, 105 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/logbook/queries/__init__.py b/homeassistant/components/logbook/queries/__init__.py index 3c027823612..0c3a63f990e 100644 --- a/homeassistant/components/logbook/queries/__init__.py +++ b/homeassistant/components/logbook/queries/__init__.py @@ -6,6 +6,7 @@ from datetime import datetime as dt from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components.recorder.filters import Filters +from homeassistant.helpers.json import json_dumps from .all import all_stmt from .devices import devices_stmt @@ -45,34 +46,34 @@ def statement_for_request( # entities and devices: logbook sends everything for the timeframe for the entities and devices if entity_ids and device_ids: - json_quotable_entity_ids = list(entity_ids) - json_quotable_device_ids = list(device_ids) + json_quoted_entity_ids = [json_dumps(entity_id) for entity_id in entity_ids] + json_quoted_device_ids = [json_dumps(device_id) for device_id in device_ids] return entities_devices_stmt( start_day, end_day, event_types, entity_ids, - json_quotable_entity_ids, - json_quotable_device_ids, + json_quoted_entity_ids, + json_quoted_device_ids, ) # entities: logbook sends everything for the timeframe for the entities if entity_ids: - json_quotable_entity_ids = list(entity_ids) + json_quoted_entity_ids = [json_dumps(entity_id) for entity_id in entity_ids] return entities_stmt( start_day, end_day, event_types, entity_ids, - json_quotable_entity_ids, + json_quoted_entity_ids, ) # devices: logbook sends everything for the timeframe for the devices assert device_ids is not None - json_quotable_device_ids = list(device_ids) + json_quoted_device_ids = [json_dumps(device_id) for device_id in device_ids] return devices_stmt( start_day, end_day, event_types, - json_quotable_device_ids, + json_quoted_device_ids, ) diff --git a/homeassistant/components/logbook/queries/devices.py b/homeassistant/components/logbook/queries/devices.py index f750c552bc4..e268c2d3ac3 100644 --- a/homeassistant/components/logbook/queries/devices.py +++ b/homeassistant/components/logbook/queries/devices.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Iterable from datetime import datetime as dt +import sqlalchemy from sqlalchemy import lambda_stmt, select from sqlalchemy.orm import Query from sqlalchemy.sql.elements import ClauseList @@ -93,4 +94,6 @@ def apply_event_device_id_matchers( json_quotable_device_ids: Iterable[str], ) -> ClauseList: """Create matchers for the device_ids in the event_data.""" - return DEVICE_ID_IN_EVENT.in_(json_quotable_device_ids) + return DEVICE_ID_IN_EVENT.is_not(None) & sqlalchemy.cast( + DEVICE_ID_IN_EVENT, sqlalchemy.Text() + ).in_(json_quotable_device_ids) diff --git a/homeassistant/components/logbook/queries/entities.py b/homeassistant/components/logbook/queries/entities.py index 4ef96c100d7..3803da6f4e8 100644 --- a/homeassistant/components/logbook/queries/entities.py +++ b/homeassistant/components/logbook/queries/entities.py @@ -36,12 +36,12 @@ def _select_entities_context_ids_sub_query( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], + json_quoted_entity_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple entities.""" union = union_all( select_events_context_id_subquery(start_day, end_day, event_types).where( - apply_event_entity_id_matchers(json_quotable_entity_ids) + apply_event_entity_id_matchers(json_quoted_entity_ids) ), apply_entities_hints(select(States.context_id)) .filter((States.last_updated > start_day) & (States.last_updated < end_day)) @@ -56,7 +56,7 @@ def _apply_entities_context_union( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], + json_quoted_entity_ids: list[str], ) -> CompoundSelect: """Generate a CTE to find the entity and device context ids and a query to find linked row.""" entities_cte: CTE = _select_entities_context_ids_sub_query( @@ -64,7 +64,7 @@ def _apply_entities_context_union( end_day, event_types, entity_ids, - json_quotable_entity_ids, + json_quoted_entity_ids, ).cte() # We used to optimize this to exclude rows we already in the union with # a States.entity_id.not_in(entity_ids) but that made the @@ -91,19 +91,19 @@ def entities_stmt( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], + json_quoted_entity_ids: list[str], ) -> StatementLambdaElement: """Generate a logbook query for multiple entities.""" return lambda_stmt( lambda: _apply_entities_context_union( select_events_without_states(start_day, end_day, event_types).where( - apply_event_entity_id_matchers(json_quotable_entity_ids) + apply_event_entity_id_matchers(json_quoted_entity_ids) ), start_day, end_day, event_types, entity_ids, - json_quotable_entity_ids, + json_quoted_entity_ids, ).order_by(Events.time_fired) ) @@ -118,12 +118,19 @@ def states_query_for_entity_ids( def apply_event_entity_id_matchers( - json_quotable_entity_ids: Iterable[str], + json_quoted_entity_ids: Iterable[str], ) -> sqlalchemy.or_: """Create matchers for the entity_id in the event_data.""" - return ENTITY_ID_IN_EVENT.in_( - json_quotable_entity_ids - ) | OLD_ENTITY_ID_IN_EVENT.in_(json_quotable_entity_ids) + return sqlalchemy.or_( + ENTITY_ID_IN_EVENT.is_not(None) + & sqlalchemy.cast(ENTITY_ID_IN_EVENT, sqlalchemy.Text()).in_( + json_quoted_entity_ids + ), + OLD_ENTITY_ID_IN_EVENT.is_not(None) + & sqlalchemy.cast(OLD_ENTITY_ID_IN_EVENT, sqlalchemy.Text()).in_( + json_quoted_entity_ids + ), + ) def apply_entities_hints(query: Query) -> Query: diff --git a/homeassistant/components/logbook/queries/entities_and_devices.py b/homeassistant/components/logbook/queries/entities_and_devices.py index 591918dd653..f22a8392e19 100644 --- a/homeassistant/components/logbook/queries/entities_and_devices.py +++ b/homeassistant/components/logbook/queries/entities_and_devices.py @@ -33,14 +33,14 @@ def _select_entities_device_id_context_ids_sub_query( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], - json_quotable_device_ids: list[str], + json_quoted_entity_ids: list[str], + json_quoted_device_ids: list[str], ) -> CompoundSelect: """Generate a subquery to find context ids for multiple entities and multiple devices.""" union = union_all( select_events_context_id_subquery(start_day, end_day, event_types).where( _apply_event_entity_id_device_id_matchers( - json_quotable_entity_ids, json_quotable_device_ids + json_quoted_entity_ids, json_quoted_device_ids ) ), apply_entities_hints(select(States.context_id)) @@ -56,16 +56,16 @@ def _apply_entities_devices_context_union( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], - json_quotable_device_ids: list[str], + json_quoted_entity_ids: list[str], + json_quoted_device_ids: list[str], ) -> CompoundSelect: devices_entities_cte: CTE = _select_entities_device_id_context_ids_sub_query( start_day, end_day, event_types, entity_ids, - json_quotable_entity_ids, - json_quotable_device_ids, + json_quoted_entity_ids, + json_quoted_device_ids, ).cte() # We used to optimize this to exclude rows we already in the union with # a States.entity_id.not_in(entity_ids) but that made the @@ -92,32 +92,32 @@ def entities_devices_stmt( end_day: dt, event_types: tuple[str, ...], entity_ids: list[str], - json_quotable_entity_ids: list[str], - json_quotable_device_ids: list[str], + json_quoted_entity_ids: list[str], + json_quoted_device_ids: list[str], ) -> StatementLambdaElement: """Generate a logbook query for multiple entities.""" stmt = lambda_stmt( lambda: _apply_entities_devices_context_union( select_events_without_states(start_day, end_day, event_types).where( _apply_event_entity_id_device_id_matchers( - json_quotable_entity_ids, json_quotable_device_ids + json_quoted_entity_ids, json_quoted_device_ids ) ), start_day, end_day, event_types, entity_ids, - json_quotable_entity_ids, - json_quotable_device_ids, + json_quoted_entity_ids, + json_quoted_device_ids, ).order_by(Events.time_fired) ) return stmt def _apply_event_entity_id_device_id_matchers( - json_quotable_entity_ids: Iterable[str], json_quotable_device_ids: Iterable[str] + json_quoted_entity_ids: Iterable[str], json_quoted_device_ids: Iterable[str] ) -> sqlalchemy.or_: """Create matchers for the device_id and entity_id in the event_data.""" return apply_event_entity_id_matchers( - json_quotable_entity_ids - ) | apply_event_device_id_matchers(json_quotable_device_ids) + json_quoted_entity_ids + ) | apply_event_device_id_matchers(json_quoted_device_ids) diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index ac6a31202e7..66fbc9b0bca 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -94,7 +94,7 @@ async def _async_mock_entity_with_logbook_platform(hass): return entry -async def _async_mock_device_with_logbook_platform(hass): +async def _async_mock_devices_with_logbook_platform(hass): """Mock an integration that provides a device that are described by the logbook.""" entry = MockConfigEntry(domain="test", data={"first": True}, options=None) entry.add_to_hass(hass) @@ -109,8 +109,18 @@ async def _async_mock_device_with_logbook_platform(hass): model="model", suggested_area="Game Room", ) + device2 = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:CC")}, + identifiers={("bridgeid", "4567")}, + sw_version="sw-version", + name="device name", + manufacturer="manufacturer", + model="model", + suggested_area="Living Room", + ) await _async_mock_logbook_platform(hass) - return device + return [device, device2] async def test_get_events(hass, hass_ws_client, recorder_mock): @@ -392,10 +402,13 @@ async def test_get_events_with_device_ids(hass, hass_ws_client, recorder_mock): ] ) - device = await _async_mock_device_with_logbook_platform(hass) + devices = await _async_mock_devices_with_logbook_platform(hass) + device = devices[0] + device2 = devices[1] hass.bus.async_fire(EVENT_HOMEASSISTANT_START) hass.bus.async_fire("mock_event", {"device_id": device.id}) + hass.bus.async_fire("mock_event", {"device_id": device2.id}) hass.states.async_set("light.kitchen", STATE_OFF) await hass.async_block_till_done() @@ -423,7 +436,7 @@ async def test_get_events_with_device_ids(hass, hass_ws_client, recorder_mock): "id": 1, "type": "logbook/get_events", "start_time": now.isoformat(), - "device_ids": [device.id], + "device_ids": [device.id, device2.id], } ) response = await client.receive_json() @@ -431,10 +444,13 @@ async def test_get_events_with_device_ids(hass, hass_ws_client, recorder_mock): assert response["id"] == 1 results = response["result"] - assert len(results) == 1 + assert len(results) == 2 assert results[0]["name"] == "device name" assert results[0]["message"] == "is on fire" assert isinstance(results[0]["when"], float) + assert results[1]["name"] == "device name" + assert results[1]["message"] == "is on fire" + assert isinstance(results[1]["when"], float) await client.send_json( { @@ -470,17 +486,20 @@ async def test_get_events_with_device_ids(hass, hass_ws_client, recorder_mock): assert response["id"] == 3 results = response["result"] - assert len(results) == 4 + assert len(results) == 5 assert results[0]["message"] == "started" assert results[1]["name"] == "device name" assert results[1]["message"] == "is on fire" assert isinstance(results[1]["when"], float) - assert results[2]["entity_id"] == "light.kitchen" - assert results[2]["state"] == "on" + assert results[2]["name"] == "device name" + assert results[2]["message"] == "is on fire" assert isinstance(results[2]["when"], float) assert results[3]["entity_id"] == "light.kitchen" - assert results[3]["state"] == "off" + assert results[3]["state"] == "on" assert isinstance(results[3]["when"], float) + assert results[4]["entity_id"] == "light.kitchen" + assert results[4]["state"] == "off" + assert isinstance(results[4]["when"], float) @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) @@ -1731,7 +1750,9 @@ async def test_subscribe_unsubscribe_logbook_stream_device( for comp in ("homeassistant", "logbook", "automation", "script") ] ) - device = await _async_mock_device_with_logbook_platform(hass) + devices = await _async_mock_devices_with_logbook_platform(hass) + device = devices[0] + device2 = devices[1] await hass.async_block_till_done() init_count = sum(hass.bus.async_listeners().values()) @@ -1743,7 +1764,7 @@ async def test_subscribe_unsubscribe_logbook_stream_device( "id": 7, "type": "logbook/event_stream", "start_time": now.isoformat(), - "device_ids": [device.id], + "device_ids": [device.id, device2.id], } ) @@ -1775,6 +1796,29 @@ async def test_subscribe_unsubscribe_logbook_stream_device( {"domain": "test", "message": "is on fire", "name": "device name", "when": ANY} ] + for _ in range(3): + hass.bus.async_fire("mock_event", {"device_id": device.id}) + hass.bus.async_fire("mock_event", {"device_id": device2.id}) + await hass.async_block_till_done() + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + { + "domain": "test", + "message": "is on fire", + "name": "device name", + "when": ANY, + }, + { + "domain": "test", + "message": "is on fire", + "name": "device name", + "when": ANY, + }, + ] + await websocket_client.send_json( {"id": 8, "type": "unsubscribe_events", "subscription": 7} ) @@ -1950,7 +1994,8 @@ async def test_live_stream_with_one_second_commit_interval( for comp in ("homeassistant", "logbook", "automation", "script") ] ) - device = await _async_mock_device_with_logbook_platform(hass) + devices = await _async_mock_devices_with_logbook_platform(hass) + device = devices[0] await hass.async_block_till_done() init_count = sum(hass.bus.async_listeners().values()) @@ -2143,7 +2188,8 @@ async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplo ] ) await async_wait_recording_done(hass) - device = await _async_mock_device_with_logbook_platform(hass) + devices = await _async_mock_devices_with_logbook_platform(hass) + device = devices[0] await async_wait_recording_done(hass) # Block the recorder queue From b880a05e4533a63222e19b7bc9520e9b1620f4b2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 24 Jun 2022 16:35:38 +0200 Subject: [PATCH 1773/3516] Fix type hints in zha remaining channels (#73778) * Fix hvac channel type hints * Fix security channel type hints * Fix homeautomation channel type hints * Fix type hints in zha base channel * Adjust select entity * Remove unused arg --- .../components/zha/core/channels/__init__.py | 2 +- .../components/zha/core/channels/base.py | 9 +++++---- .../zha/core/channels/homeautomation.py | 6 +++++- .../components/zha/core/channels/hvac.py | 1 + .../components/zha/core/channels/security.py | 17 ++++------------- homeassistant/components/zha/select.py | 1 + mypy.ini | 12 ------------ script/hassfest/mypy_config.py | 4 ---- 8 files changed, 17 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 33143821f9c..2da7462f3eb 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -370,7 +370,7 @@ class ChannelPool: return [self.all_channels[chan_id] for chan_id in (available - claimed)] @callback - def zha_send_event(self, event_data: dict[str, str | int]) -> None: + def zha_send_event(self, event_data: dict[str, Any]) -> None: """Relay events to hass.""" self._channels.zha_send_event( { diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index de943ebac16..ae5980cd630 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -137,7 +137,7 @@ class ZigbeeChannel(LogMixin): self.value_attribute = attr self._status = ChannelStatus.CREATED self._cluster.add_listener(self) - self.data_cache = {} + self.data_cache: dict[str, Enum] = {} @property def id(self) -> str: @@ -278,7 +278,7 @@ class ZigbeeChannel(LogMixin): ) def _configure_reporting_status( - self, attrs: dict[int | str, tuple], res: list | tuple + self, attrs: dict[int | str, tuple[int, int, float | int]], res: list | tuple ) -> None: """Parse configure reporting result.""" if isinstance(res, (Exception, ConfigureReportingResponseRecord)): @@ -304,10 +304,10 @@ class ZigbeeChannel(LogMixin): for r in res if r.status != Status.SUCCESS ] - attrs = {self.cluster.attributes.get(r, [r])[0] for r in attrs} + attributes = {self.cluster.attributes.get(r, [r])[0] for r in attrs} self.debug( "Successfully configured reporting for '%s' on '%s' cluster", - attrs - set(failed), + attributes - set(failed), self.name, ) self.debug( @@ -393,6 +393,7 @@ class ZigbeeChannel(LogMixin): def zha_send_event(self, command: str, arg: list | dict | CommandSchema) -> None: """Relay events to hass.""" + args: list | dict if isinstance(arg, CommandSchema): args = [a for a in arg if a is not None] params = arg.as_dict() diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index 52036706f19..69295ef6f81 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -158,7 +158,11 @@ class ElectricalMeasurementChannel(ZigbeeChannel): return None meas_type = self.MeasurementType(meas_type) - return ", ".join(m.name for m in self.MeasurementType if m in meas_type) + return ", ".join( + m.name + for m in self.MeasurementType + if m in meas_type and m.name is not None + ) @registries.ZIGBEE_CHANNEL_REGISTRY.register( diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 53f18a0fd0f..4b4909299b1 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -293,6 +293,7 @@ class ThermostatChannel(ZigbeeChannel): return bool(self.occupancy) except ZigbeeException as ex: self.debug("Couldn't read 'occupancy' attribute: %s", ex) + return None async def write_attributes(self, data, **kwargs): """Write attributes helper.""" diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 789e792e149..41e65019415 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -199,26 +199,17 @@ class IasAce(ZigbeeChannel): def _emergency(self) -> None: """Handle the IAS ACE emergency command.""" - self._set_alarm( - AceCluster.AlarmStatus.Emergency, - IAS_ACE_EMERGENCY, - ) + self._set_alarm(AceCluster.AlarmStatus.Emergency) def _fire(self) -> None: """Handle the IAS ACE fire command.""" - self._set_alarm( - AceCluster.AlarmStatus.Fire, - IAS_ACE_FIRE, - ) + self._set_alarm(AceCluster.AlarmStatus.Fire) def _panic(self) -> None: """Handle the IAS ACE panic command.""" - self._set_alarm( - AceCluster.AlarmStatus.Emergency_Panic, - IAS_ACE_PANIC, - ) + self._set_alarm(AceCluster.AlarmStatus.Emergency_Panic) - def _set_alarm(self, status: AceCluster.PanelStatus, event: str) -> None: + def _set_alarm(self, status: AceCluster.AlarmStatus) -> None: """Set the specified alarm status.""" self.alarm_status = status self.armed_state = AceCluster.PanelStatus.In_Alarm diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 83bbcdca580..6d202ccceb2 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -64,6 +64,7 @@ class ZHAEnumSelectEntity(ZhaEntity, SelectEntity): """Representation of a ZHA select entity.""" _attr_entity_category = EntityCategory.CONFIG + _attr_name: str _enum: type[Enum] def __init__( diff --git a/mypy.ini b/mypy.ini index 8dca7403eff..e2e91ef921c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2996,18 +2996,6 @@ ignore_errors = true [mypy-homeassistant.components.xiaomi_miio.switch] ignore_errors = true -[mypy-homeassistant.components.zha.core.channels.base] -ignore_errors = true - -[mypy-homeassistant.components.zha.core.channels.homeautomation] -ignore_errors = true - -[mypy-homeassistant.components.zha.core.channels.hvac] -ignore_errors = true - -[mypy-homeassistant.components.zha.core.channels.security] -ignore_errors = true - [mypy-homeassistant.components.zha.core.device] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index f319ccb5235..49cf3b7b65a 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -144,10 +144,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.xiaomi_miio.light", "homeassistant.components.xiaomi_miio.sensor", "homeassistant.components.xiaomi_miio.switch", - "homeassistant.components.zha.core.channels.base", - "homeassistant.components.zha.core.channels.homeautomation", - "homeassistant.components.zha.core.channels.hvac", - "homeassistant.components.zha.core.channels.security", "homeassistant.components.zha.core.device", "homeassistant.components.zha.core.discovery", "homeassistant.components.zha.core.gateway", From 1866a1e92561dba2c74fd1b8db236ace83c5cbb4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Jun 2022 09:59:01 -0500 Subject: [PATCH 1774/3516] Handle non-str keys when storing json data (#73958) --- homeassistant/util/json.py | 11 +++++++++-- tests/util/test_json.py | 8 ++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 8a9663bb95d..d69a4106728 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -45,6 +45,13 @@ def load_json(filename: str, default: list | dict | None = None) -> list | dict: return {} if default is None else default +def _orjson_encoder(data: Any) -> str: + """JSON encoder that uses orjson.""" + return orjson.dumps( + data, option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS + ).decode("utf-8") + + def save_json( filename: str, data: list | dict, @@ -62,8 +69,8 @@ def save_json( if encoder: json_data = json.dumps(data, indent=2, cls=encoder) else: - dump = orjson.dumps - json_data = orjson.dumps(data, option=orjson.OPT_INDENT_2).decode("utf-8") + dump = _orjson_encoder + json_data = _orjson_encoder(data) except TypeError as error: msg = f"Failed to serialize to JSON: {filename}. Bad data at {format_unserializable_data(find_paths_unserializable_data(data, dump=dump))}" _LOGGER.error(msg) diff --git a/tests/util/test_json.py b/tests/util/test_json.py index abf47b0bc53..9974cbb9628 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -52,6 +52,14 @@ def test_save_and_load(): assert data == TEST_JSON_A +def test_save_and_load_int_keys(): + """Test saving and loading back stringifies the keys.""" + fname = _path_for("test1") + save_json(fname, {1: "a", 2: "b"}) + data = load_json(fname) + assert data == {"1": "a", "2": "b"} + + def test_save_and_load_private(): """Test we can load private files and that they are protected.""" fname = _path_for("test2") From 44da543ca06eaf26c4fe73cf68f34990c60432ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Jun 2022 09:59:41 -0500 Subject: [PATCH 1775/3516] Bump nexia to 2.0.0 (#73935) --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 4bae2d9a15d..1cb410ad1a8 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==1.0.2"], + "requirements": ["nexia==2.0.0"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 542fcdbb540..339c0a8da88 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1083,7 +1083,7 @@ nettigo-air-monitor==1.3.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==1.0.2 +nexia==2.0.0 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a8fd3b39dbf..33763e0b892 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -748,7 +748,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.3.0 # homeassistant.components.nexia -nexia==1.0.2 +nexia==2.0.0 # homeassistant.components.discord nextcord==2.0.0a8 From 57efa9569c2bb744f167c4e07b0f7600702fc091 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 24 Jun 2022 17:05:36 -0400 Subject: [PATCH 1776/3516] Cache is_supported for Google entities (#73936) --- .../components/google_assistant/helpers.py | 13 ++++++- .../google_assistant/test_helpers.py | 35 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 2ed91b42ec6..932611390eb 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -90,6 +90,7 @@ class AbstractConfig(ABC): self._local_sdk_active = False self._local_last_active: datetime | None = None self._local_sdk_version_warn = False + self.is_supported_cache: dict[str, tuple[int | None, bool]] = {} async def async_initialize(self): """Perform async initialization of config.""" @@ -541,7 +542,17 @@ class GoogleEntity: @callback def is_supported(self) -> bool: """Return if the entity is supported by Google.""" - return bool(self.traits()) + features: int | None = self.state.attributes.get(ATTR_SUPPORTED_FEATURES) + + result = self.config.is_supported_cache.get(self.entity_id) + + if result is None or result[0] != features: + result = self.config.is_supported_cache[self.entity_id] = ( + features, + bool(self.traits()), + ) + + return result[1] @callback def might_2fa(self) -> bool: diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index 1ab573baf2a..8898fc7ef76 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -327,7 +327,9 @@ async def test_sync_entities_all(agents, result): def test_supported_features_string(caplog): """Test bad supported features.""" entity = helpers.GoogleEntity( - None, None, State("test.entity_id", "on", {"supported_features": "invalid"}) + None, + MockConfig(), + State("test.entity_id", "on", {"supported_features": "invalid"}), ) assert entity.is_supported() is False assert "Entity test.entity_id contains invalid supported_features value invalid" @@ -427,3 +429,34 @@ async def test_config_local_sdk_warn_version(hass, hass_client, caplog, version) f"Local SDK version is too old ({version}), check documentation on how " "to update to the latest version" ) in caplog.text + + +def test_is_supported_cached(): + """Test is_supported is cached.""" + config = MockConfig() + + def entity(features: int): + return helpers.GoogleEntity( + None, + config, + State("test.entity_id", "on", {"supported_features": features}), + ) + + with patch( + "homeassistant.components.google_assistant.helpers.GoogleEntity.traits", + return_value=[1], + ) as mock_traits: + assert entity(1).is_supported() is True + assert len(mock_traits.mock_calls) == 1 + + # Supported feature changes, so we calculate again + assert entity(2).is_supported() is True + assert len(mock_traits.mock_calls) == 2 + + mock_traits.reset_mock() + + # Supported feature is same, so we do not calculate again + mock_traits.side_effect = ValueError + + assert entity(2).is_supported() is True + assert len(mock_traits.mock_calls) == 0 From 32e0d9f47c47c1caea81cfe56150531beeafb3f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 24 Jun 2022 16:28:26 -0500 Subject: [PATCH 1777/3516] Speed up generation of template states (#73728) * Speed up generation of template states * tweak * cache * cache hash * weaken * Revert "weaken" This reverts commit 4856f500807c21aa1c9333d44fd53555bae7bb82. * lower cache size as it tends to be the same ones over and over * lower cache size as it tends to be the same ones over and over * lower cache size as it tends to be the same ones over and over * cover * Update homeassistant/helpers/template.py Co-authored-by: Paulus Schoutsen * id reuse is possible * account for iterting all sensors Co-authored-by: Paulus Schoutsen --- homeassistant/core.py | 7 +++++ homeassistant/helpers/template.py | 52 +++++++++++++++++++++++++++---- tests/helpers/test_template.py | 10 ++++++ 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index b8f509abef3..b568ee72689 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1110,6 +1110,13 @@ class State: self.domain, self.object_id = split_entity_id(self.entity_id) self._as_dict: ReadOnlyDict[str, Collection[Any]] | None = None + def __hash__(self) -> int: + """Make the state hashable. + + State objects are effectively immutable. + """ + return hash((id(self), self.last_updated)) + @property def name(self) -> str: """Name of this state.""" diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index ac5b4c8119f..eca76a8c7bc 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -9,7 +9,7 @@ from collections.abc import Callable, Generator, Iterable from contextlib import contextmanager, suppress from contextvars import ContextVar from datetime import datetime, timedelta -from functools import partial, wraps +from functools import cache, lru_cache, partial, wraps import json import logging import math @@ -98,6 +98,9 @@ template_cv: ContextVar[tuple[str, str] | None] = ContextVar( "template_cv", default=None ) +CACHED_TEMPLATE_STATES = 512 +EVAL_CACHE_SIZE = 512 + @bind_hass def attach(hass: HomeAssistant, obj: Any) -> None: @@ -222,6 +225,9 @@ def _false(arg: str) -> bool: return False +_cached_literal_eval = lru_cache(maxsize=EVAL_CACHE_SIZE)(literal_eval) + + class RenderInfo: """Holds information about a template render.""" @@ -318,6 +324,7 @@ class Template: "_exc_info", "_limited", "_strict", + "_hash_cache", ) def __init__(self, template, hass=None): @@ -333,6 +340,7 @@ class Template: self._exc_info = None self._limited = None self._strict = None + self._hash_cache: int = hash(self.template) @property def _env(self) -> TemplateEnvironment: @@ -421,7 +429,7 @@ class Template: def _parse_result(self, render_result: str) -> Any: """Parse the result.""" try: - result = literal_eval(render_result) + result = _cached_literal_eval(render_result) if type(result) in RESULT_WRAPPERS: result = RESULT_WRAPPERS[type(result)]( @@ -618,16 +626,30 @@ class Template: def __hash__(self) -> int: """Hash code for template.""" - return hash(self.template) + return self._hash_cache def __repr__(self) -> str: """Representation of Template.""" return 'Template("' + self.template + '")' +@cache +def _domain_states(hass: HomeAssistant, name: str) -> DomainStates: + return DomainStates(hass, name) + + +def _readonly(*args: Any, **kwargs: Any) -> Any: + """Raise an exception when a states object is modified.""" + raise RuntimeError(f"Cannot modify template States object: {args} {kwargs}") + + class AllStates: """Class to expose all HA states as attributes.""" + __setitem__ = _readonly + __delitem__ = _readonly + __slots__ = ("_hass",) + def __init__(self, hass: HomeAssistant) -> None: """Initialize all states.""" self._hass = hass @@ -643,7 +665,7 @@ class AllStates: if not valid_entity_id(f"{name}.entity"): raise TemplateError(f"Invalid domain name '{name}'") - return DomainStates(self._hass, name) + return _domain_states(self._hass, name) # Jinja will try __getitem__ first and it avoids the need # to call is_safe_attribute @@ -682,6 +704,11 @@ class AllStates: class DomainStates: """Class to expose a specific HA domain as attributes.""" + __slots__ = ("_hass", "_domain") + + __setitem__ = _readonly + __delitem__ = _readonly + def __init__(self, hass: HomeAssistant, domain: str) -> None: """Initialize the domain states.""" self._hass = hass @@ -727,6 +754,9 @@ class TemplateStateBase(State): _state: State + __setitem__ = _readonly + __delitem__ = _readonly + # Inheritance is done so functions that check against State keep working # pylint: disable=super-init-not-called def __init__(self, hass: HomeAssistant, collect: bool, entity_id: str) -> None: @@ -865,10 +895,15 @@ def _collect_state(hass: HomeAssistant, entity_id: str) -> None: entity_collect.entities.add(entity_id) +@lru_cache(maxsize=CACHED_TEMPLATE_STATES) +def _template_state_no_collect(hass: HomeAssistant, state: State) -> TemplateState: + return TemplateState(hass, state, collect=False) + + def _state_generator(hass: HomeAssistant, domain: str | None) -> Generator: """State generator for a domain or all states.""" for state in sorted(hass.states.async_all(domain), key=attrgetter("entity_id")): - yield TemplateState(hass, state, collect=False) + yield _template_state_no_collect(hass, state) def _get_state_if_valid(hass: HomeAssistant, entity_id: str) -> TemplateState | None: @@ -882,6 +917,11 @@ def _get_state(hass: HomeAssistant, entity_id: str) -> TemplateState | None: return _get_template_state_from_state(hass, entity_id, hass.states.get(entity_id)) +@lru_cache(maxsize=CACHED_TEMPLATE_STATES) +def _template_state(hass: HomeAssistant, state: State) -> TemplateState: + return TemplateState(hass, state) + + def _get_template_state_from_state( hass: HomeAssistant, entity_id: str, state: State | None ) -> TemplateState | None: @@ -890,7 +930,7 @@ def _get_template_state_from_state( # access to the state properties in the state wrapper. _collect_state(hass, entity_id) return None - return TemplateState(hass, state) + return _template_state(hass, state) def _resolve_state( diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index a1fd3e73f59..69a3af22759 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -18,6 +18,7 @@ from homeassistant.const import ( MASS_GRAMS, PRESSURE_PA, SPEED_KILOMETERS_PER_HOUR, + STATE_ON, TEMP_CELSIUS, VOLUME_LITERS, ) @@ -3831,3 +3832,12 @@ 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_template_states_blocks_setitem(hass): + """Test we cannot setitem on TemplateStates.""" + hass.states.async_set("light.new", STATE_ON) + state = hass.states.get("light.new") + template_state = template.TemplateState(hass, state, True) + with pytest.raises(RuntimeError): + template_state["any"] = "any" From 0461eda83b39d07308a240b416066edd8704ac9f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 25 Jun 2022 00:34:49 +0200 Subject: [PATCH 1778/3516] Adjust demo cover position methods (#73944) --- homeassistant/components/demo/cover.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index 79a51406857..5c908dfa33a 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -205,9 +205,9 @@ class DemoCover(CoverEntity): self._listen_cover_tilt() self._requested_closing_tilt = False - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" - position = kwargs.get(ATTR_POSITION) + position: int = kwargs[ATTR_POSITION] self._set_position = round(position, -1) if self._position == position: return @@ -215,9 +215,9 @@ class DemoCover(CoverEntity): self._listen_cover() self._requested_closing = position < self._position - async def async_set_cover_tilt_position(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover til to a specific position.""" - tilt_position = kwargs.get(ATTR_TILT_POSITION) + tilt_position: int = kwargs[ATTR_TILT_POSITION] self._set_tilt_position = round(tilt_position, -1) if self._tilt_position == tilt_position: return From 15b756417145bbafc0a462096acef992e580e6ae Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 25 Jun 2022 00:48:27 +0200 Subject: [PATCH 1779/3516] Fix coverage issue in CI (#73959) * Fix coverage issue in CI * Adjust to latest findings Co-authored-by: Franck Nijhof --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 49f62f0943d..fcd879d7512 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -796,7 +796,7 @@ jobs: --dist=loadfile \ --test-group-count ${{ needs.changes.outputs.test_group_count }} \ --test-group=${{ matrix.group }} \ - --cov homeassistant \ + --cov="homeassistant" \ --cov-report=xml \ -o console_output_style=count \ -p no:sugar \ @@ -818,8 +818,8 @@ jobs: -qq \ --timeout=9 \ --durations=10 \ - -n auto \ - --cov homeassistant.components.${{ matrix.group }} \ + -n 0 \ + --cov="homeassistant.components.${{ matrix.group }}" \ --cov-report=xml \ --cov-report=term-missing \ -o console_output_style=count \ From 9b88b77b6619a89c8aa641bf59331666072bf900 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 25 Jun 2022 00:55:01 +0200 Subject: [PATCH 1780/3516] Use attributes in wilight (#73898) Co-authored-by: Franck Nijhof --- homeassistant/components/wilight/__init__.py | 52 ++++++---------- homeassistant/components/wilight/cover.py | 21 ++++--- homeassistant/components/wilight/fan.py | 22 +++---- homeassistant/components/wilight/light.py | 60 ++++++++++--------- .../components/wilight/parent_device.py | 32 +++++----- 5 files changed, 88 insertions(+), 99 deletions(-) diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index 932ce1538bf..2cdcf20c1ea 100644 --- a/homeassistant/components/wilight/__init__.py +++ b/homeassistant/components/wilight/__init__.py @@ -1,5 +1,9 @@ """The WiLight integration.""" +from typing import Any + +from pywilight.wilight_device import Device as PyWiLightDevice + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback @@ -51,61 +55,43 @@ class WiLightDevice(Entity): Contains the common logic for WiLight entities. """ - def __init__(self, api_device, index, item_name): + _attr_should_poll = False + + def __init__(self, api_device: PyWiLightDevice, index: str, item_name: str) -> None: """Initialize the device.""" # WiLight specific attributes for every component type self._device_id = api_device.device_id - self._sw_version = api_device.swversion self._client = api_device.client - self._model = api_device.model - self._name = item_name self._index = index - self._unique_id = f"{self._device_id}_{self._index}" - self._status = {} + self._status: dict[str, Any] = {} - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return a name for this WiLight item.""" - return self._name - - @property - def unique_id(self): - """Return the unique ID for this WiLight item.""" - return self._unique_id - - @property - def device_info(self) -> DeviceInfo: - """Return the device info.""" - return DeviceInfo( - name=self._name, - identifiers={(DOMAIN, self._unique_id)}, - model=self._model, + self._attr_name = item_name + self._attr_unique_id = f"{self._device_id}_{index}" + self._attr_device_info = DeviceInfo( + name=item_name, + identifiers={(DOMAIN, self._attr_unique_id)}, + model=api_device.model, manufacturer="WiLight", - sw_version=self._sw_version, + sw_version=api_device.swversion, via_device=(DOMAIN, self._device_id), ) @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return bool(self._client.is_connected) @callback - def handle_event_callback(self, states): + def handle_event_callback(self, states: dict[str, Any]) -> None: """Propagate changes through ha.""" self._status = states self.async_write_ha_state() - async def async_update(self): + async def async_update(self) -> None: """Synchronize state with api_device.""" await self._client.status(self._index) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register update callback.""" self._client.register_status_callback(self.handle_event_callback, self._index) await self._client.status(self._index) diff --git a/homeassistant/components/wilight/cover.py b/homeassistant/components/wilight/cover.py index 6ee4a857d36..cd0a3cc21ac 100644 --- a/homeassistant/components/wilight/cover.py +++ b/homeassistant/components/wilight/cover.py @@ -1,4 +1,6 @@ """Support for WiLight Cover.""" +from __future__ import annotations + from typing import Any from pywilight.const import ( @@ -18,16 +20,18 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN, WiLightDevice +from .parent_device import WiLightParent async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up WiLight covers from a config entry.""" - parent = hass.data[DOMAIN][entry.entry_id] + parent: WiLightParent = hass.data[DOMAIN][entry.entry_id] # Handle a discovered WiLight device. entities = [] + assert parent.api for item in parent.api.items: if item["type"] != ITEM_COVER: continue @@ -35,18 +39,17 @@ async def async_setup_entry( item_name = item["name"] if item["sub_type"] != COVER_V1: continue - entity = WiLightCover(parent.api, index, item_name) - entities.append(entity) + entities.append(WiLightCover(parent.api, index, item_name)) async_add_entities(entities) -def wilight_to_hass_position(value): +def wilight_to_hass_position(value: int) -> int: """Convert wilight position 1..255 to hass format 0..100.""" return min(100, round((value * 100) / 255)) -def hass_to_wilight_position(value): +def hass_to_wilight_position(value: int) -> int: """Convert hass position 0..100 to wilight 1..255 scale.""" return min(255, round((value * 255) / 100)) @@ -55,7 +58,7 @@ class WiLightCover(WiLightDevice, CoverEntity): """Representation of a WiLights cover.""" @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """Return current position of cover. None is unknown, 0 is closed, 100 is fully open. @@ -65,21 +68,21 @@ class WiLightCover(WiLightDevice, CoverEntity): return None @property - def is_opening(self): + def is_opening(self) -> bool | None: """Return if the cover is opening or not.""" if "motor_state" not in self._status: return None return self._status["motor_state"] == WL_OPENING @property - def is_closing(self): + def is_closing(self) -> bool | None: """Return if the cover is closing or not.""" if "motor_state" not in self._status: return None return self._status["motor_state"] == WL_CLOSING @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" if "motor_state" not in self._status or "position_current" not in self._status: return None diff --git a/homeassistant/components/wilight/fan.py b/homeassistant/components/wilight/fan.py index a93e0eb9447..c598e6db397 100644 --- a/homeassistant/components/wilight/fan.py +++ b/homeassistant/components/wilight/fan.py @@ -13,6 +13,7 @@ from pywilight.const import ( WL_SPEED_LOW, WL_SPEED_MEDIUM, ) +from pywilight.wilight_device import Device as PyWiLightDevice from homeassistant.components.fan import DIRECTION_FORWARD, FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry @@ -24,6 +25,7 @@ from homeassistant.util.percentage import ( ) from . import DOMAIN, WiLightDevice +from .parent_device import WiLightParent ORDERED_NAMED_FAN_SPEEDS = [WL_SPEED_LOW, WL_SPEED_MEDIUM, WL_SPEED_HIGH] @@ -32,10 +34,11 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up WiLight lights from a config entry.""" - parent = hass.data[DOMAIN][entry.entry_id] + parent: WiLightParent = hass.data[DOMAIN][entry.entry_id] # Handle a discovered WiLight device. entities = [] + assert parent.api for item in parent.api.items: if item["type"] != ITEM_FAN: continue @@ -43,8 +46,7 @@ async def async_setup_entry( item_name = item["name"] if item["sub_type"] != FAN_V1: continue - entity = WiLightFan(parent.api, index, item_name) - entities.append(entity) + entities.append(WiLightFan(parent.api, index, item_name)) async_add_entities(entities) @@ -52,19 +54,16 @@ async def async_setup_entry( class WiLightFan(WiLightDevice, FanEntity): """Representation of a WiLights fan.""" + _attr_icon = "mdi:fan" + _attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS) _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.DIRECTION - def __init__(self, api_device, index, item_name): + def __init__(self, api_device: PyWiLightDevice, index: str, item_name: str) -> None: """Initialize the device.""" super().__init__(api_device, index, item_name) # Initialize the WiLights fan. self._direction = WL_DIRECTION_FORWARD - @property - def icon(self): - """Return the icon of device based on its type.""" - return "mdi:fan" - @property def is_on(self) -> bool: """Return true if device is on.""" @@ -83,11 +82,6 @@ class WiLightFan(WiLightDevice, FanEntity): return None return ordered_list_item_to_percentage(ORDERED_NAMED_FAN_SPEEDS, wl_speed) - @property - def speed_count(self) -> int: - """Return the number of speeds the fan supports.""" - return len(ORDERED_NAMED_FAN_SPEEDS) - @property def current_direction(self) -> str: """Return the current direction of the fan.""" diff --git a/homeassistant/components/wilight/light.py b/homeassistant/components/wilight/light.py index 10ff79fe60d..ea9e19dcb30 100644 --- a/homeassistant/components/wilight/light.py +++ b/homeassistant/components/wilight/light.py @@ -1,5 +1,10 @@ """Support for WiLight lights.""" +from __future__ import annotations + +from typing import Any + from pywilight.const import ITEM_LIGHT, LIGHT_COLOR, LIGHT_DIMMER, LIGHT_ON_OFF +from pywilight.wilight_device import Device as PyWiLightDevice from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -12,25 +17,23 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN, WiLightDevice +from .parent_device import WiLightParent -def entities_from_discovered_wilight(hass, api_device): +def entities_from_discovered_wilight(api_device: PyWiLightDevice) -> list[LightEntity]: """Parse configuration and add WiLight light entities.""" - entities = [] + entities: list[LightEntity] = [] for item in api_device.items: if item["type"] != ITEM_LIGHT: continue index = item["index"] item_name = item["name"] if item["sub_type"] == LIGHT_ON_OFF: - entity = WiLightLightOnOff(api_device, index, item_name) + entities.append(WiLightLightOnOff(api_device, index, item_name)) elif item["sub_type"] == LIGHT_DIMMER: - entity = WiLightLightDimmer(api_device, index, item_name) + entities.append(WiLightLightDimmer(api_device, index, item_name)) elif item["sub_type"] == LIGHT_COLOR: - entity = WiLightLightColor(api_device, index, item_name) - else: - continue - entities.append(entity) + entities.append(WiLightLightColor(api_device, index, item_name)) return entities @@ -39,10 +42,11 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up WiLight lights from a config entry.""" - parent = hass.data[DOMAIN][entry.entry_id] + parent: WiLightParent = hass.data[DOMAIN][entry.entry_id] # Handle a discovered WiLight device. - entities = entities_from_discovered_wilight(hass, parent.api) + assert parent.api + entities = entities_from_discovered_wilight(parent.api) async_add_entities(entities) @@ -53,15 +57,15 @@ class WiLightLightOnOff(WiLightDevice, LightEntity): _attr_supported_color_modes = {ColorMode.ONOFF} @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if device is on.""" return self._status.get("on") - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self._client.turn_on(self._index) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self._client.turn_off(self._index) @@ -73,16 +77,16 @@ class WiLightLightDimmer(WiLightDevice, LightEntity): _attr_supported_color_modes = {ColorMode.BRIGHTNESS} @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of this light between 0..255.""" return int(self._status.get("brightness", 0)) @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if device is on.""" return self._status.get("on") - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on,set brightness if needed.""" # Dimmer switches use a range of [0, 255] to control # brightness. Level 255 might mean to set it to previous value @@ -92,27 +96,27 @@ class WiLightLightDimmer(WiLightDevice, LightEntity): else: await self._client.turn_on(self._index) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self._client.turn_off(self._index) -def wilight_to_hass_hue(value): +def wilight_to_hass_hue(value: int) -> float: """Convert wilight hue 1..255 to hass 0..360 scale.""" return min(360, round((value * 360) / 255, 3)) -def hass_to_wilight_hue(value): +def hass_to_wilight_hue(value: float) -> int: """Convert hass hue 0..360 to wilight 1..255 scale.""" return min(255, round((value * 255) / 360)) -def wilight_to_hass_saturation(value): +def wilight_to_hass_saturation(value: int) -> float: """Convert wilight saturation 1..255 to hass 0..100 scale.""" return min(100, round((value * 100) / 255, 3)) -def hass_to_wilight_saturation(value): +def hass_to_wilight_saturation(value: float) -> int: """Convert hass saturation 0..100 to wilight 1..255 scale.""" return min(255, round((value * 255) / 100)) @@ -124,24 +128,24 @@ class WiLightLightColor(WiLightDevice, LightEntity): _attr_supported_color_modes = {ColorMode.HS} @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of this light between 0..255.""" return int(self._status.get("brightness", 0)) @property - def hs_color(self): + def hs_color(self) -> tuple[float, float]: """Return the hue and saturation color value [float, float].""" - return [ + return ( wilight_to_hass_hue(int(self._status.get("hue", 0))), wilight_to_hass_saturation(int(self._status.get("saturation", 0))), - ] + ) @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if device is on.""" return self._status.get("on") - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on,set brightness if needed.""" # Brightness use a range of [0, 255] to control # Hue use a range of [0, 360] to control @@ -161,6 +165,6 @@ class WiLightLightColor(WiLightDevice, LightEntity): else: await self._client.turn_on(self._index) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self._client.turn_off(self._index) diff --git a/homeassistant/components/wilight/parent_device.py b/homeassistant/components/wilight/parent_device.py index faf71b74f72..17a33fef633 100644 --- a/homeassistant/components/wilight/parent_device.py +++ b/homeassistant/components/wilight/parent_device.py @@ -1,12 +1,16 @@ """The WiLight Device integration.""" +from __future__ import annotations + import asyncio import logging import pywilight +from pywilight.wilight_device import Device as PyWiLightDevice import requests +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send _LOGGER = logging.getLogger(__name__) @@ -15,23 +19,23 @@ _LOGGER = logging.getLogger(__name__) class WiLightParent: """Manages a single WiLight Parent Device.""" - def __init__(self, hass, config_entry): + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize the system.""" - self._host = config_entry.data[CONF_HOST] + self._host: str = config_entry.data[CONF_HOST] self._hass = hass - self._api = None + self._api: PyWiLightDevice | None = None @property - def host(self): + def host(self) -> str: """Return the host of this parent.""" return self._host @property - def api(self): + def api(self) -> PyWiLightDevice | None: """Return the api of this parent.""" return self._api - async def async_setup(self): + async def async_setup(self) -> bool: """Set up a WiLight Parent Device based on host parameter.""" host = self._host hass = self._hass @@ -42,7 +46,7 @@ class WiLightParent: return False @callback - def disconnected(): + def disconnected() -> None: # Schedule reconnect after connection has been lost. _LOGGER.warning("WiLight %s disconnected", api_device.device_id) async_dispatcher_send( @@ -50,14 +54,14 @@ class WiLightParent: ) @callback - def reconnected(): + def reconnected() -> None: # Schedule reconnect after connection has been lost. _LOGGER.warning("WiLight %s reconnect", api_device.device_id) async_dispatcher_send( hass, f"wilight_device_available_{api_device.device_id}", True ) - async def connect(api_device): + async def connect(api_device: PyWiLightDevice) -> None: # Set up connection and hook it into HA for reconnect/shutdown. _LOGGER.debug("Initiating connection to %s", api_device.device_id) @@ -81,7 +85,7 @@ class WiLightParent: return True - async def async_reset(self): + async def async_reset(self) -> None: """Reset api.""" # If the initialization was not wrong. @@ -89,15 +93,13 @@ class WiLightParent: self._api.client.stop() -def create_api_device(host): +def create_api_device(host: str) -> PyWiLightDevice: """Create an API Device.""" try: - device = pywilight.device_from_host(host) + return pywilight.device_from_host(host) except ( requests.exceptions.ConnectionError, requests.exceptions.Timeout, ) as err: _LOGGER.error("Unable to access WiLight at %s (%s)", host, err) return None - - return device From a267045a31a2bfe0ceff83cd5c46ee87ec9b361c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 25 Jun 2022 01:05:31 +0200 Subject: [PATCH 1781/3516] Migrate open_meteo to native_* (#73910) --- .../components/open_meteo/weather.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py index 40b52248a52..d1f0adf2e87 100644 --- a/homeassistant/components/open_meteo/weather.py +++ b/homeassistant/components/open_meteo/weather.py @@ -5,7 +5,11 @@ from open_meteo import Forecast as OpenMeteoForecast from homeassistant.components.weather import Forecast, WeatherEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import ( + LENGTH_MILLIMETERS, + SPEED_KILOMETERS_PER_HOUR, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -33,7 +37,9 @@ class OpenMeteoWeatherEntity( ): """Defines an Open-Meteo weather entity.""" - _attr_temperature_unit = TEMP_CELSIUS + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR def __init__( self, @@ -63,14 +69,14 @@ class OpenMeteoWeatherEntity( ) @property - def temperature(self) -> float | None: + def native_temperature(self) -> float | None: """Return the platform temperature.""" if not self.coordinator.data.current_weather: return None return self.coordinator.data.current_weather.temperature @property - def wind_speed(self) -> float | None: + def native_wind_speed(self) -> float | None: """Return the wind speed.""" if not self.coordinator.data.current_weather: return None @@ -103,19 +109,19 @@ class OpenMeteoWeatherEntity( ) if daily.precipitation_sum is not None: - forecast["precipitation"] = daily.precipitation_sum[index] + forecast["native_precipitation"] = daily.precipitation_sum[index] if daily.temperature_2m_max is not None: - forecast["temperature"] = daily.temperature_2m_max[index] + forecast["native_temperature"] = daily.temperature_2m_max[index] if daily.temperature_2m_min is not None: - forecast["templow"] = daily.temperature_2m_min[index] + forecast["native_templow"] = daily.temperature_2m_min[index] if daily.wind_direction_10m_dominant is not None: forecast["wind_bearing"] = daily.wind_direction_10m_dominant[index] if daily.wind_speed_10m_max is not None: - forecast["wind_speed"] = daily.wind_speed_10m_max[index] + forecast["native_wind_speed"] = daily.wind_speed_10m_max[index] forecasts.append(forecast) From ad3bd6773c5da0ab9827ee0f73d6c1d141fc4c78 Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Sat, 25 Jun 2022 00:23:26 +0100 Subject: [PATCH 1782/3516] Add device_info to Glances entities (#73047) --- homeassistant/components/glances/sensor.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index a907dd1695a..0d60747ecaa 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -4,6 +4,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_UPDATED, DOMAIN, SENSOR_TYPES, GlancesSensorEntityDescription @@ -30,6 +31,7 @@ async def async_setup_entry( name, disk["mnt_point"], description, + config_entry.entry_id, ) ) elif description.type == "sensors": @@ -42,11 +44,16 @@ async def async_setup_entry( name, sensor["label"], description, + config_entry.entry_id, ) ) elif description.type == "raid": for raid_device in client.api.data[description.type]: - dev.append(GlancesSensor(client, name, raid_device, description)) + dev.append( + GlancesSensor( + client, name, raid_device, description, config_entry.entry_id + ) + ) elif client.api.data[description.type]: dev.append( GlancesSensor( @@ -54,6 +61,7 @@ async def async_setup_entry( name, "", description, + config_entry.entry_id, ) ) @@ -71,6 +79,7 @@ class GlancesSensor(SensorEntity): name, sensor_name_prefix, description: GlancesSensorEntityDescription, + config_entry_id: str, ): """Initialize the sensor.""" self.glances_data = glances_data @@ -80,6 +89,11 @@ class GlancesSensor(SensorEntity): self.entity_description = description self._attr_name = f"{name} {sensor_name_prefix} {description.name_suffix}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, config_entry_id)}, + manufacturer="Glances", + name=name, + ) @property def unique_id(self): From 0166816200c364d3a906d2071f8c9f72c7704764 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 25 Jun 2022 00:24:25 +0000 Subject: [PATCH 1783/3516] [ci skip] Translation update --- .../eight_sleep/translations/uk.json | 19 ++++++++++++++++ .../components/google/translations/uk.json | 7 ++++++ .../components/nest/translations/uk.json | 20 +++++++++++++++++ .../overkiz/translations/sensor.uk.json | 9 ++++++++ .../radiotherm/translations/it.json | 2 +- .../radiotherm/translations/uk.json | 22 +++++++++++++++++++ .../sensibo/translations/sensor.uk.json | 8 +++++++ .../simplepush/translations/et.json | 21 ++++++++++++++++++ .../simplepush/translations/fr.json | 21 ++++++++++++++++++ .../simplepush/translations/no.json | 21 ++++++++++++++++++ .../simplepush/translations/pt-BR.json | 21 ++++++++++++++++++ .../simplepush/translations/zh-Hant.json | 21 ++++++++++++++++++ .../components/skybell/translations/uk.json | 12 ++++++++++ .../transmission/translations/uk.json | 10 ++++++++- .../components/vacuum/translations/nn.json | 8 +++++++ 15 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/eight_sleep/translations/uk.json create mode 100644 homeassistant/components/google/translations/uk.json create mode 100644 homeassistant/components/overkiz/translations/sensor.uk.json create mode 100644 homeassistant/components/radiotherm/translations/uk.json create mode 100644 homeassistant/components/sensibo/translations/sensor.uk.json create mode 100644 homeassistant/components/simplepush/translations/et.json create mode 100644 homeassistant/components/simplepush/translations/fr.json create mode 100644 homeassistant/components/simplepush/translations/no.json create mode 100644 homeassistant/components/simplepush/translations/pt-BR.json create mode 100644 homeassistant/components/simplepush/translations/zh-Hant.json create mode 100644 homeassistant/components/skybell/translations/uk.json diff --git a/homeassistant/components/eight_sleep/translations/uk.json b/homeassistant/components/eight_sleep/translations/uk.json new file mode 100644 index 00000000000..4dea8ca0857 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u0454\u0442\u044c\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0445\u043c\u0430\u0440\u0438 Eight Sleep: {error}" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u0454\u0442\u044c\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0445\u043c\u0430\u0440\u0438 Eight Sleep: {error}" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/uk.json b/homeassistant/components/google/translations/uk.json new file mode 100644 index 00000000000..d0beb9cab9f --- /dev/null +++ b/homeassistant/components/google/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/uk.json b/homeassistant/components/nest/translations/uk.json index 9ab8349670e..cfdb2c91ee2 100644 --- a/homeassistant/components/nest/translations/uk.json +++ b/homeassistant/components/nest/translations/uk.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u041e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e", "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", @@ -18,6 +19,25 @@ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { + "auth_upgrade": { + "title": "Nest: \u0437\u0430\u0431\u043e\u0440\u043e\u043d\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457 \u0434\u043e\u0434\u0430\u0442\u043a\u0456\u0432" + }, + "cloud_project": { + "data": { + "cloud_project_id": "ID \u043f\u0440\u043e\u0435\u043a\u0442\u0443 Google Cloud" + } + }, + "create_cloud_project": { + "title": "Nest: \u0441\u0442\u0432\u043e\u0440\u0456\u0442\u044c \u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Cloud Project" + }, + "device_project": { + "data": { + "project_id": "ID \u043f\u0440\u043e\u0435\u043a\u0442\u0443 \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + }, + "device_project_upgrade": { + "title": "Nest: \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u043e\u0435\u043a\u0442\u0443 \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, "init": { "data": { "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440" diff --git a/homeassistant/components/overkiz/translations/sensor.uk.json b/homeassistant/components/overkiz/translations/sensor.uk.json new file mode 100644 index 00000000000..cf84368e911 --- /dev/null +++ b/homeassistant/components/overkiz/translations/sensor.uk.json @@ -0,0 +1,9 @@ +{ + "state": { + "overkiz__three_way_handle_direction": { + "closed": "\u0417\u0430\u043a\u0440\u0438\u0442\u043e", + "open": "\u0412\u0456\u0434\u043a\u0440\u0438\u0442\u043e", + "tilt": "\u041d\u0430\u0445\u0438\u043b" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/it.json b/homeassistant/components/radiotherm/translations/it.json index 1fd9c7152ff..653dd56321b 100644 --- a/homeassistant/components/radiotherm/translations/it.json +++ b/homeassistant/components/radiotherm/translations/it.json @@ -23,7 +23,7 @@ "step": { "init": { "data": { - "hold_temp": "Impostare una sospensione permanente durante la regolazione della temperatura." + "hold_temp": "Imposta un blocco permanente quando si regola la temperatura." } } } diff --git a/homeassistant/components/radiotherm/translations/uk.json b/homeassistant/components/radiotherm/translations/uk.json new file mode 100644 index 00000000000..51459878ddb --- /dev/null +++ b/homeassistant/components/radiotherm/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0457\u0442\u0438 {name} {model} ({host})?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.uk.json b/homeassistant/components/sensibo/translations/sensor.uk.json new file mode 100644 index 00000000000..d93a147307e --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.uk.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "\u041d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0438\u0439", + "s": "\u0427\u0443\u0442\u043b\u0438\u0432\u0438\u0439" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/et.json b/homeassistant/components/simplepush/translations/et.json new file mode 100644 index 00000000000..2501d992c83 --- /dev/null +++ b/homeassistant/components/simplepush/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "device_key": "Seadme seadmev\u00f5ti", + "event": "S\u00fcndmuste jaoks m\u00f5eldud s\u00fcndmus.", + "name": "Nimi", + "password": "Seadmes kasutatava kr\u00fcptimise parool", + "salt": "Sadmes kasutatav sool." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/fr.json b/homeassistant/components/simplepush/translations/fr.json new file mode 100644 index 00000000000..546d03bb131 --- /dev/null +++ b/homeassistant/components/simplepush/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "device_key": "La cl\u00e9 d'appareil de votre appareil", + "event": "L'\u00e9v\u00e9nement pour les \u00e9v\u00e9nements.", + "name": "Nom", + "password": "Le mot de passe du chiffrement utilis\u00e9 par votre appareil", + "salt": "Le salage utilis\u00e9 par votre appareil." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/no.json b/homeassistant/components/simplepush/translations/no.json new file mode 100644 index 00000000000..78cf864a33d --- /dev/null +++ b/homeassistant/components/simplepush/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "device_key": "Enhetsn\u00f8kkelen til enheten din", + "event": "Arrangementet for arrangementene.", + "name": "Navn", + "password": "Passordet til krypteringen som brukes av enheten din", + "salt": "Saltet som brukes av enheten." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/pt-BR.json b/homeassistant/components/simplepush/translations/pt-BR.json new file mode 100644 index 00000000000..bf933fe94da --- /dev/null +++ b/homeassistant/components/simplepush/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar" + }, + "step": { + "user": { + "data": { + "device_key": "A chave do dispositivo do seu dispositivo", + "event": "O evento para os eventos.", + "name": "Nome", + "password": "A senha da criptografia usada pelo seu dispositivo", + "salt": "O salto utilizado pelo seu dispositivo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/zh-Hant.json b/homeassistant/components/simplepush/translations/zh-Hant.json new file mode 100644 index 00000000000..891f2242467 --- /dev/null +++ b/homeassistant/components/simplepush/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "device_key": "\u88dd\u7f6e\u4e4b\u88dd\u7f6e\u5bc6\u9470", + "event": "\u4e8b\u4ef6\u7684\u4e8b\u4ef6\u3002", + "name": "\u540d\u7a31", + "password": "\u88dd\u7f6e\u6240\u4f7f\u7528\u4e4b\u52a0\u5bc6\u5bc6\u78bc", + "salt": "\u88dd\u7f6e\u6240\u4f7f\u7528\u4e4b Salt" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/uk.json b/homeassistant/components/skybell/translations/uk.json new file mode 100644 index 00000000000..19744315085 --- /dev/null +++ b/homeassistant/components/skybell/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "Email", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/uk.json b/homeassistant/components/transmission/translations/uk.json index 5bc74f7da2a..9fbe0848657 100644 --- a/homeassistant/components/transmission/translations/uk.json +++ b/homeassistant/components/transmission/translations/uk.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", @@ -9,6 +10,13 @@ "name_exists": "\u0426\u044f \u043d\u0430\u0437\u0432\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username} \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/vacuum/translations/nn.json b/homeassistant/components/vacuum/translations/nn.json index e06ae761458..12d981555ad 100644 --- a/homeassistant/components/vacuum/translations/nn.json +++ b/homeassistant/components/vacuum/translations/nn.json @@ -1,4 +1,12 @@ { + "device_automation": { + "condition_type": { + "is_cleaning": "k\u00f8yrer" + }, + "trigger_type": { + "cleaning": "starta reingjering" + } + }, "state": { "_": { "cleaning": "Reingjer", From 55b5ade5861582cf0750b4dba9385486bbf71c35 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Jun 2022 03:31:44 -0500 Subject: [PATCH 1784/3516] Prime platform.uname cache at startup to fix blocking subprocess in the event loop (#73975) Prime platform.uname cache at startup to fix blocking subprocess - Multiple modules check platform.uname()[0] at startup which does a blocking subprocess call. We can avoid this happening in the eventloop and distrupting startup stability by priming the cache ahead of time in the executor --- homeassistant/bootstrap.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index eabbbb49362..81d0fa72134 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -7,6 +7,7 @@ from datetime import datetime, timedelta import logging import logging.handlers import os +import platform import sys import threading from time import monotonic @@ -540,11 +541,22 @@ async def _async_set_up_integrations( stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains + def _cache_uname_processor() -> None: + """Cache the result of platform.uname().processor in the executor. + + Multiple modules call this function at startup which + executes a blocking subprocess call. This is a problem for the + asyncio event loop. By primeing the cache of uname we can + avoid the blocking call in the event loop. + """ + platform.uname().processor # pylint: disable=expression-not-assigned + # Load the registries await asyncio.gather( device_registry.async_load(hass), entity_registry.async_load(hass), area_registry.async_load(hass), + hass.async_add_executor_job(_cache_uname_processor), ) # Start setup From 10dc38e0ec27f7bef990ee431459342f9c3c52b4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 25 Jun 2022 11:59:56 +0200 Subject: [PATCH 1785/3516] Adjust CoverEntity property type hints in components (#73943) * Adjust CoverEntity property type hints in components * Revert changes to rflink * Revert changes to wilight --- homeassistant/components/acmeda/cover.py | 8 +++---- homeassistant/components/ads/cover.py | 4 ++-- .../components/advantage_air/cover.py | 4 ++-- homeassistant/components/blebox/cover.py | 12 ++++++---- homeassistant/components/bosch_shc/cover.py | 8 +++---- homeassistant/components/brunt/cover.py | 3 +-- homeassistant/components/demo/cover.py | 18 +++++++------- homeassistant/components/esphome/cover.py | 2 +- homeassistant/components/fibaro/cover.py | 6 ++--- homeassistant/components/garadget/cover.py | 18 ++++++++------ homeassistant/components/gogogate2/cover.py | 8 +++---- homeassistant/components/homematic/cover.py | 10 ++++---- .../hunterdouglas_powerview/cover.py | 4 ++-- homeassistant/components/insteon/cover.py | 4 ++-- homeassistant/components/lutron/cover.py | 6 ++--- .../components/lutron_caseta/cover.py | 4 ++-- .../components/motion_blinds/cover.py | 24 ++++++++++--------- homeassistant/components/mqtt/cover.py | 19 ++++++++------- homeassistant/components/myq/cover.py | 8 +++---- homeassistant/components/opengarage/cover.py | 8 ++++--- homeassistant/components/scsgate/cover.py | 6 ++--- homeassistant/components/slide/cover.py | 10 ++++---- homeassistant/components/smartthings/cover.py | 10 ++++---- homeassistant/components/soma/cover.py | 8 +++---- homeassistant/components/supla/cover.py | 8 +++---- homeassistant/components/tellduslive/cover.py | 5 ++-- homeassistant/components/tellstick/cover.py | 4 ++-- homeassistant/components/template/cover.py | 17 ++++++------- homeassistant/components/velux/cover.py | 10 ++++---- .../components/xiaomi_aqara/cover.py | 4 ++-- homeassistant/components/zha/cover.py | 10 ++++---- 31 files changed, 141 insertions(+), 129 deletions(-) diff --git a/homeassistant/components/acmeda/cover.py b/homeassistant/components/acmeda/cover.py index d772d8ab01f..887e26cd7fc 100644 --- a/homeassistant/components/acmeda/cover.py +++ b/homeassistant/components/acmeda/cover.py @@ -47,7 +47,7 @@ class AcmedaCover(AcmedaBase, CoverEntity): """Representation of a Acmeda cover device.""" @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """Return the current position of the roller blind. None is unknown, 0 is closed, 100 is fully open. @@ -58,7 +58,7 @@ class AcmedaCover(AcmedaBase, CoverEntity): return position @property - def current_cover_tilt_position(self): + def current_cover_tilt_position(self) -> int | None: """Return the current tilt of the roller blind. None is unknown, 0 is closed, 100 is fully open. @@ -69,7 +69,7 @@ class AcmedaCover(AcmedaBase, CoverEntity): return position @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" supported_features = 0 if self.current_cover_position is not None: @@ -90,7 +90,7 @@ class AcmedaCover(AcmedaBase, CoverEntity): return supported_features @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the cover is closed.""" return self.roller.closed_percent == 100 diff --git a/homeassistant/components/ads/cover.py b/homeassistant/components/ads/cover.py index a976ce15877..a2fb1888cd3 100644 --- a/homeassistant/components/ads/cover.py +++ b/homeassistant/components/ads/cover.py @@ -135,7 +135,7 @@ class AdsCover(AdsEntity, CoverEntity): ) @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return if the cover is closed.""" if self._ads_var is not None: return self._state_dict[STATE_KEY_STATE] @@ -144,7 +144,7 @@ class AdsCover(AdsEntity, CoverEntity): return None @property - def current_cover_position(self): + def current_cover_position(self) -> int: """Return current position of cover.""" return self._state_dict[STATE_KEY_POSITION] diff --git a/homeassistant/components/advantage_air/cover.py b/homeassistant/components/advantage_air/cover.py index bbe17835d71..36ae2c7fff0 100644 --- a/homeassistant/components/advantage_air/cover.py +++ b/homeassistant/components/advantage_air/cover.py @@ -58,12 +58,12 @@ class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): ) @property - def is_closed(self): + def is_closed(self) -> bool: """Return if vent is fully closed.""" return self._zone["state"] == ADVANTAGE_AIR_STATE_CLOSE @property - def current_cover_position(self): + def current_cover_position(self) -> int: """Return vents current position as a percentage.""" if self._zone["state"] == ADVANTAGE_AIR_STATE_OPEN: return self._zone["value"] diff --git a/homeassistant/components/blebox/cover.py b/homeassistant/components/blebox/cover.py index a7f531fe519..368443988a4 100644 --- a/homeassistant/components/blebox/cover.py +++ b/homeassistant/components/blebox/cover.py @@ -1,4 +1,6 @@ """BleBox cover entity.""" +from __future__ import annotations + from typing import Any from homeassistant.components.cover import ( @@ -41,7 +43,7 @@ class BleBoxCoverEntity(BleBoxEntity, CoverEntity): ) @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """Return the current cover position.""" position = self._feature.current if position == -1: # possible for shutterBox @@ -50,17 +52,17 @@ class BleBoxCoverEntity(BleBoxEntity, CoverEntity): return None if position is None else 100 - position @property - def is_opening(self): + def is_opening(self) -> bool | None: """Return whether cover is opening.""" return self._is_state(STATE_OPENING) @property - def is_closing(self): + def is_closing(self) -> bool | None: """Return whether cover is closing.""" return self._is_state(STATE_CLOSING) @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return whether cover is closed.""" return self._is_state(STATE_CLOSED) @@ -82,6 +84,6 @@ class BleBoxCoverEntity(BleBoxEntity, CoverEntity): """Stop the cover.""" await self._feature.async_stop() - def _is_state(self, state_name): + def _is_state(self, state_name) -> bool | None: value = BLEBOX_TO_HASS_COVER_STATES[self._feature.state] return None if value is None else value == state_name diff --git a/homeassistant/components/bosch_shc/cover.py b/homeassistant/components/bosch_shc/cover.py index cdbe884dc45..91dc361a23d 100644 --- a/homeassistant/components/bosch_shc/cover.py +++ b/homeassistant/components/bosch_shc/cover.py @@ -52,7 +52,7 @@ class ShutterControlCover(SHCEntity, CoverEntity): ) @property - def current_cover_position(self): + def current_cover_position(self) -> int: """Return the current cover position.""" return round(self._device.level * 100.0) @@ -61,12 +61,12 @@ class ShutterControlCover(SHCEntity, CoverEntity): self._device.stop() @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the cover is closed or not.""" return self.current_cover_position == 0 @property - def is_opening(self): + def is_opening(self) -> bool: """Return if the cover is opening or not.""" return ( self._device.operation_state @@ -74,7 +74,7 @@ class ShutterControlCover(SHCEntity, CoverEntity): ) @property - def is_closing(self): + def is_closing(self) -> bool: """Return if the cover is closing or not.""" return ( self._device.operation_state diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index c38dfa0eb78..489229622b2 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -1,7 +1,6 @@ """Support for Brunt Blind Engine covers.""" from __future__ import annotations -from collections.abc import MutableMapping from typing import Any from aiohttp.client_exceptions import ClientResponseError @@ -140,7 +139,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): return self.move_state == 2 @property - def extra_state_attributes(self) -> MutableMapping[str, Any]: + def extra_state_attributes(self) -> dict[str, Any]: """Return the detailed device state attributes.""" return { ATTR_REQUEST_POSITION: self.request_cover_position, diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index 5c908dfa33a..f867ed3faa4 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -110,42 +110,42 @@ class DemoCover(CoverEntity): ) @property - def unique_id(self): + def unique_id(self) -> str: """Return unique ID for cover.""" return self._unique_id @property - def name(self): + def name(self) -> str: """Return the name of the cover.""" return self._name @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed for a demo cover.""" return False @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """Return the current position of the cover.""" return self._position @property - def current_cover_tilt_position(self): + def current_cover_tilt_position(self) -> int | None: """Return the current tilt position of the cover.""" return self._tilt_position @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the cover is closed.""" return self._closed @property - def is_closing(self): + def is_closing(self) -> bool: """Return if the cover is closing.""" return self._is_closing @property - def is_opening(self): + def is_opening(self) -> bool: """Return if the cover is opening.""" return self._is_opening @@ -155,7 +155,7 @@ class DemoCover(CoverEntity): return self._device_class @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" if self._supported_features is not None: return self._supported_features diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 4296c899253..ab8b7af2185 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -111,7 +111,7 @@ class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity): """Stop the cover.""" await self._client.cover_command(key=self._static_info.key, stop=True) - async def async_set_cover_position(self, **kwargs: int) -> None: + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" await self._client.cover_command( key=self._static_info.key, position=kwargs[ATTR_POSITION] / 100 diff --git a/homeassistant/components/fibaro/cover.py b/homeassistant/components/fibaro/cover.py index 1e5583f20d6..c0749f9c100 100644 --- a/homeassistant/components/fibaro/cover.py +++ b/homeassistant/components/fibaro/cover.py @@ -72,12 +72,12 @@ class FibaroCover(FibaroDevice, CoverEntity): return False @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """Return current position of cover. 0 is closed, 100 is open.""" return self.bound(self.level) @property - def current_cover_tilt_position(self): + def current_cover_tilt_position(self) -> int | None: """Return the current tilt position for venetian blinds.""" return self.bound(self.level2) @@ -90,7 +90,7 @@ class FibaroCover(FibaroDevice, CoverEntity): self.set_level2(kwargs.get(ATTR_TILT_POSITION)) @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return if the cover is closed.""" if self._is_open_close_only(): return self.fibaro_device.properties.state.lower() == "closed" diff --git a/homeassistant/components/garadget/cover.py b/homeassistant/components/garadget/cover.py index e0372db0c6c..96ebe698605 100644 --- a/homeassistant/components/garadget/cover.py +++ b/homeassistant/components/garadget/cover.py @@ -7,7 +7,11 @@ from typing import Any import requests import voluptuous as vol -from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity +from homeassistant.components.cover import ( + PLATFORM_SCHEMA, + CoverDeviceClass, + CoverEntity, +) from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_COVERS, @@ -135,17 +139,17 @@ class GaradgetCover(CoverEntity): self.remove_token() @property - def name(self): + def name(self) -> str: """Return the name of the cover.""" return self._name @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return self._available @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" data = {} @@ -164,16 +168,16 @@ class GaradgetCover(CoverEntity): return data @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return if the cover is closed.""" if self._state is None: return None return self._state == STATE_CLOSED @property - def device_class(self): + def device_class(self) -> CoverDeviceClass: """Return the class of this device, from component DEVICE_CLASSES.""" - return "garage" + return CoverDeviceClass.GARAGE def get_token(self): """Get new token for usage during this session.""" diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index b7434952fc1..f0039d85295 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -62,12 +62,12 @@ class DeviceCover(GoGoGate2Entity, CoverEntity): ) @property - def name(self): + def name(self) -> str | None: """Return the name of the door.""" return self.door.name @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return true if cover is closed, else False.""" door_status = self.door_status if door_status == DoorStatus.OPENED: @@ -77,12 +77,12 @@ class DeviceCover(GoGoGate2Entity, CoverEntity): return None @property - def is_closing(self): + def is_closing(self) -> bool: """Return if the cover is closing or not.""" return self.door_status == TransitionDoorStatus.CLOSING @property - def is_opening(self): + def is_opening(self) -> bool: """Return if the cover is opening or not.""" return self.door_status == TransitionDoorStatus.OPENING diff --git a/homeassistant/components/homematic/cover.py b/homeassistant/components/homematic/cover.py index d138e172784..d5f1802e774 100644 --- a/homeassistant/components/homematic/cover.py +++ b/homeassistant/components/homematic/cover.py @@ -43,7 +43,7 @@ class HMCover(HMDevice, CoverEntity): """Representation a HomeMatic Cover.""" @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """ Return current position of cover. @@ -60,7 +60,7 @@ class HMCover(HMDevice, CoverEntity): self._hmdevice.set_level(level, self._channel) @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return whether the cover is closed.""" if self.current_cover_position is not None: return self.current_cover_position == 0 @@ -86,7 +86,7 @@ class HMCover(HMDevice, CoverEntity): self._data.update({"LEVEL_2": None}) @property - def current_cover_tilt_position(self): + def current_cover_tilt_position(self) -> int | None: """Return current position of cover tilt. None is unknown, 0 is closed, 100 is fully open. @@ -125,7 +125,7 @@ class HMGarage(HMCover): _attr_device_class = CoverDeviceClass.GARAGE @property - def current_cover_position(self): + def current_cover_position(self) -> None: """ Return current position of cover. @@ -135,7 +135,7 @@ class HMGarage(HMCover): return None @property - def is_closed(self): + def is_closed(self) -> bool: """Return whether the cover is closed.""" return self._hmdevice.is_closed(self._hm_get_state()) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index fe26a100569..3f7d04f5b87 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -192,7 +192,7 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): return {STATE_ATTRIBUTE_ROOM_NAME: self._room_name} @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the cover is closed.""" return self.positions.primary <= CLOSED_POSITION @@ -474,7 +474,7 @@ class PowerViewShadeTDBUTop(PowerViewShadeTDBU): return False @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the cover is closed.""" # top shade needs to check other motor return self.positions.secondary <= CLOSED_POSITION diff --git a/homeassistant/components/insteon/cover.py b/homeassistant/components/insteon/cover.py index 68f7f6156e3..645450166b9 100644 --- a/homeassistant/components/insteon/cover.py +++ b/homeassistant/components/insteon/cover.py @@ -47,7 +47,7 @@ class InsteonCoverEntity(InsteonEntity, CoverEntity): ) @property - def current_cover_position(self): + def current_cover_position(self) -> int: """Return the current cover position.""" if self._insteon_device_group.value is not None: pos = self._insteon_device_group.value @@ -56,7 +56,7 @@ class InsteonCoverEntity(InsteonEntity, CoverEntity): return int(math.ceil(pos * 100 / 255)) @property - def is_closed(self): + def is_closed(self) -> bool: """Return the boolean response if the node is on.""" return bool(self.current_cover_position) diff --git a/homeassistant/components/lutron/cover.py b/homeassistant/components/lutron/cover.py index 67a0e093337..fa62ef3745a 100644 --- a/homeassistant/components/lutron/cover.py +++ b/homeassistant/components/lutron/cover.py @@ -43,12 +43,12 @@ class LutronCover(LutronDevice, CoverEntity): ) @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the cover is closed.""" return self._lutron_device.last_level() < 1 @property - def current_cover_position(self): + def current_cover_position(self) -> int: """Return the current position of cover.""" return self._lutron_device.last_level() @@ -73,6 +73,6 @@ class LutronCover(LutronDevice, CoverEntity): _LOGGER.debug("Lutron ID: %d updated to %f", self._lutron_device.id, level) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return {"lutron_integration_id": self._lutron_device.id} diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py index 68932a2a011..b74642a8589 100644 --- a/homeassistant/components/lutron_caseta/cover.py +++ b/homeassistant/components/lutron_caseta/cover.py @@ -50,12 +50,12 @@ class LutronCasetaCover(LutronCasetaDeviceUpdatableEntity, CoverEntity): _attr_device_class = CoverDeviceClass.SHADE @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the cover is closed.""" return self._device["current_state"] < 1 @property - def current_cover_position(self): + def current_cover_position(self) -> int: """Return the current position of cover.""" return self._device["current_state"] diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 301d2c6fbc7..a73166912f4 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -1,4 +1,6 @@ """Support for Motion Blinds using their WLAN API.""" +from __future__ import annotations + import logging from typing import Any @@ -216,7 +218,7 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): ) @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" if self.coordinator.data is None: return False @@ -227,7 +229,7 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): return self.coordinator.data[self._blind.mac][ATTR_AVAILABLE] @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """ Return current position of cover. @@ -238,7 +240,7 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): return 100 - self._blind.position @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" if self._blind.position is None: return None @@ -249,7 +251,7 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): self._blind.Register_callback(self.unique_id, self.schedule_update_ha_state) await super().async_added_to_hass() - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Unsubscribe when removed.""" self._blind.Remove_callback(self.unique_id) await super().async_will_remove_from_hass() @@ -340,7 +342,7 @@ class MotionTiltDevice(MotionPositionDevice): _restore_tilt = True @property - def current_cover_tilt_position(self): + def current_cover_tilt_position(self) -> int | None: """ Return current angle of cover. @@ -378,7 +380,7 @@ class MotionTiltOnlyDevice(MotionTiltDevice): _restore_tilt = False @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" supported_features = ( CoverEntityFeature.OPEN_TILT @@ -392,12 +394,12 @@ class MotionTiltOnlyDevice(MotionTiltDevice): return supported_features @property - def current_cover_position(self): + def current_cover_position(self) -> None: """Return current position of cover.""" return None @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" if self._blind.angle is None: return None @@ -430,7 +432,7 @@ class MotionTDBUDevice(MotionPositionDevice): _LOGGER.error("Unknown motor '%s'", self._motor) @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """ Return current position of cover. @@ -442,7 +444,7 @@ class MotionTDBUDevice(MotionPositionDevice): return 100 - self._blind.scaled_position[self._motor_key] @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" if self._blind.position is None: return None @@ -453,7 +455,7 @@ class MotionTDBUDevice(MotionPositionDevice): return self._blind.position[self._motor_key] == 100 @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device specific state attributes.""" attributes = {} if self._blind.position is not None: diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 8a32471ecec..54ed4f2b0a0 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -12,6 +12,7 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, DEVICE_CLASSES_SCHEMA, + CoverDeviceClass, CoverEntity, CoverEntityFeature, ) @@ -486,12 +487,12 @@ class MqttCover(MqttEntity, CoverEntity): await subscription.async_subscribe_topics(self.hass, self._sub_state) @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" - return self._optimistic + return bool(self._optimistic) @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return true if the cover is closed or None if the status is unknown.""" if self._state is None: return None @@ -499,17 +500,17 @@ class MqttCover(MqttEntity, CoverEntity): return self._state == STATE_CLOSED @property - def is_opening(self): + def is_opening(self) -> bool: """Return true if the cover is actively opening.""" return self._state == STATE_OPENING @property - def is_closing(self): + def is_closing(self) -> bool: """Return true if the cover is actively closing.""" return self._state == STATE_CLOSING @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """Return current position of cover. None is unknown, 0 is closed, 100 is fully open. @@ -517,17 +518,17 @@ class MqttCover(MqttEntity, CoverEntity): return self._position @property - def current_cover_tilt_position(self): + def current_cover_tilt_position(self) -> int | None: """Return current position of cover tilt.""" return self._tilt_value @property - def device_class(self): + def device_class(self) -> CoverDeviceClass | None: """Return the class of this sensor.""" return self._config.get(CONF_DEVICE_CLASS) @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" supported_features = 0 if self._config.get(CONF_COMMAND_TOPIC) is not None: diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index fe8ef16bc89..51d0b3290a6 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -50,22 +50,22 @@ class MyQCover(MyQEntity, CoverEntity): self._attr_unique_id = device.device_id @property - def is_closed(self): + def is_closed(self) -> bool: """Return true if cover is closed, else False.""" return MYQ_TO_HASS.get(self._device.state) == STATE_CLOSED @property - def is_closing(self): + def is_closing(self) -> bool: """Return if the cover is closing or not.""" return MYQ_TO_HASS.get(self._device.state) == STATE_CLOSING @property - def is_open(self): + def is_open(self) -> bool: """Return if the cover is opening or not.""" return MYQ_TO_HASS.get(self._device.state) == STATE_OPEN @property - def is_opening(self): + def is_opening(self) -> bool: """Return if the cover is opening or not.""" return MYQ_TO_HASS.get(self._device.state) == STATE_OPENING diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index ac0af64737a..aff913cf205 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -1,4 +1,6 @@ """Platform for the opengarage.io cover component.""" +from __future__ import annotations + import logging from typing import Any @@ -43,21 +45,21 @@ class OpenGarageCover(OpenGarageEntity, CoverEntity): super().__init__(open_garage_data_coordinator, device_id) @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return if the cover is closed.""" if self._state is None: return None return self._state == STATE_CLOSED @property - def is_closing(self): + def is_closing(self) -> bool | None: """Return if the cover is closing.""" if self._state is None: return None return self._state == STATE_CLOSING @property - def is_opening(self): + def is_opening(self) -> bool | None: """Return if the cover is opening.""" if self._state is None: return None diff --git a/homeassistant/components/scsgate/cover.py b/homeassistant/components/scsgate/cover.py index 8d94f4214af..4aa08cae3bd 100644 --- a/homeassistant/components/scsgate/cover.py +++ b/homeassistant/components/scsgate/cover.py @@ -72,17 +72,17 @@ class SCSGateCover(CoverEntity): return self._scs_id @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed.""" return False @property - def name(self): + def name(self) -> str: """Return the name of the cover.""" return self._name @property - def is_closed(self): + def is_closed(self) -> None: """Return if the cover is closed.""" return None diff --git a/homeassistant/components/slide/cover.py b/homeassistant/components/slide/cover.py index 52fcc3c8da4..866d3d40307 100644 --- a/homeassistant/components/slide/cover.py +++ b/homeassistant/components/slide/cover.py @@ -52,29 +52,29 @@ class SlideCover(CoverEntity): self._invert = slide["invert"] @property - def is_opening(self): + def is_opening(self) -> bool: """Return if the cover is opening or not.""" return self._slide["state"] == STATE_OPENING @property - def is_closing(self): + def is_closing(self) -> bool: """Return if the cover is closing or not.""" return self._slide["state"] == STATE_CLOSING @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return None if status is unknown, True if closed, else False.""" if self._slide["state"] is None: return None return self._slide["state"] == STATE_CLOSED @property - def available(self): + def available(self) -> bool: """Return False if state is not available.""" return self._slide["online"] @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """Return the current position of cover shutter.""" if (pos := self._slide["pos"]) is not None: if (1 - pos) <= DEFAULT_OFFSET or pos <= DEFAULT_OFFSET: diff --git a/homeassistant/components/smartthings/cover.py b/homeassistant/components/smartthings/cover.py index 7e2de17cad3..80a30131301 100644 --- a/homeassistant/components/smartthings/cover.py +++ b/homeassistant/components/smartthings/cover.py @@ -126,17 +126,17 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): self._state_attrs[ATTR_BATTERY_LEVEL] = battery @property - def is_opening(self): + def is_opening(self) -> bool: """Return if the cover is opening or not.""" return self._state == STATE_OPENING @property - def is_closing(self): + def is_closing(self) -> bool: """Return if the cover is closing or not.""" return self._state == STATE_CLOSING @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return if the cover is closed or not.""" if self._state == STATE_CLOSED: return True @@ -150,11 +150,11 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): return self._device.status.level @property - def device_class(self): + def device_class(self) -> CoverDeviceClass | None: """Define this cover as a garage door.""" return self._device_class @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Get additional state attributes.""" return self._state_attrs diff --git a/homeassistant/components/soma/cover.py b/homeassistant/components/soma/cover.py index 5777e904597..0130b0ca7b1 100644 --- a/homeassistant/components/soma/cover.py +++ b/homeassistant/components/soma/cover.py @@ -52,12 +52,12 @@ class SomaTilt(SomaEntity, CoverEntity): ) @property - def current_cover_tilt_position(self): + def current_cover_tilt_position(self) -> int: """Return the current cover tilt position.""" return self.current_position @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the cover tilt is closed.""" return self.current_position == 0 @@ -126,12 +126,12 @@ class SomaShade(SomaEntity, CoverEntity): ) @property - def current_cover_position(self): + def current_cover_position(self) -> int: """Return the current cover position.""" return self.current_position @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the cover is closed.""" return self.current_position == 0 diff --git a/homeassistant/components/supla/cover.py b/homeassistant/components/supla/cover.py index b1cc0951259..c6c1d9c07db 100644 --- a/homeassistant/components/supla/cover.py +++ b/homeassistant/components/supla/cover.py @@ -60,7 +60,7 @@ class SuplaCover(SuplaChannel, CoverEntity): """Representation of a Supla Cover.""" @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """Return current position of cover. 0 is closed, 100 is open.""" if state := self.channel_data.get("state"): return 100 - state["shut"] @@ -71,7 +71,7 @@ class SuplaCover(SuplaChannel, CoverEntity): await self.async_action("REVEAL", percentage=kwargs.get(ATTR_POSITION)) @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return if the cover is closed.""" if self.current_cover_position is None: return None @@ -94,7 +94,7 @@ class SuplaGateDoor(SuplaChannel, CoverEntity): """Representation of a Supla gate door.""" @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return if the gate is closed or not.""" state = self.channel_data.get("state") if state and "hi" in state: @@ -120,6 +120,6 @@ class SuplaGateDoor(SuplaChannel, CoverEntity): await self.async_action("OPEN_CLOSE") @property - def device_class(self): + def device_class(self) -> CoverDeviceClass: """Return the class of this device, from component DEVICE_CLASSES.""" return CoverDeviceClass.GARAGE diff --git a/homeassistant/components/tellduslive/cover.py b/homeassistant/components/tellduslive/cover.py index 49c35ac3114..829478fc990 100644 --- a/homeassistant/components/tellduslive/cover.py +++ b/homeassistant/components/tellduslive/cover.py @@ -8,6 +8,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import TelldusLiveClient from .entry import TelldusLiveEntity @@ -20,7 +21,7 @@ async def async_setup_entry( async def async_discover_cover(device_id): """Discover and add a discovered sensor.""" - client = hass.data[tellduslive.DOMAIN] + client: TelldusLiveClient = hass.data[tellduslive.DOMAIN] async_add_entities([TelldusLiveCover(client, device_id)]) async_dispatcher_connect( @@ -34,7 +35,7 @@ class TelldusLiveCover(TelldusLiveEntity, CoverEntity): """Representation of a cover.""" @property - def is_closed(self): + def is_closed(self) -> bool: """Return the current position of the cover.""" return self.device.is_down diff --git a/homeassistant/components/tellstick/cover.py b/homeassistant/components/tellstick/cover.py index 7c38741960b..17da5684670 100644 --- a/homeassistant/components/tellstick/cover.py +++ b/homeassistant/components/tellstick/cover.py @@ -44,12 +44,12 @@ class TellstickCover(TellstickDevice, CoverEntity): """Representation of a Tellstick cover.""" @property - def is_closed(self): + def is_closed(self) -> None: """Return the current position of the cover is not possible.""" return None @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return True if unable to access real state of the entity.""" return True diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 0c86b1d5d5a..aad0270e434 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -12,6 +12,7 @@ from homeassistant.components.cover import ( DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, + CoverDeviceClass, CoverEntity, CoverEntityFeature, ) @@ -154,7 +155,7 @@ class CoverTemplate(TemplateEntity, CoverEntity): self._template = config.get(CONF_VALUE_TEMPLATE) self._position_template = config.get(CONF_POSITION_TEMPLATE) self._tilt_template = config.get(CONF_TILT_TEMPLATE) - self._device_class = config.get(CONF_DEVICE_CLASS) + self._device_class: CoverDeviceClass | None = config.get(CONF_DEVICE_CLASS) self._open_script = None if (open_action := config.get(OPEN_ACTION)) is not None: self._open_script = Script(hass, open_action, friendly_name, DOMAIN) @@ -270,22 +271,22 @@ class CoverTemplate(TemplateEntity, CoverEntity): self._tilt_value = state @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the cover is closed.""" return self._position == 0 @property - def is_opening(self): + def is_opening(self) -> bool: """Return if the cover is currently opening.""" return self._is_opening @property - def is_closing(self): + def is_closing(self) -> bool: """Return if the cover is currently closing.""" return self._is_closing @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """Return current position of cover. None is unknown, 0 is closed, 100 is fully open. @@ -295,7 +296,7 @@ class CoverTemplate(TemplateEntity, CoverEntity): return None @property - def current_cover_tilt_position(self): + def current_cover_tilt_position(self) -> int | None: """Return current position of cover tilt. None is unknown, 0 is closed, 100 is fully open. @@ -303,12 +304,12 @@ class CoverTemplate(TemplateEntity, CoverEntity): return self._tilt_value @property - def device_class(self): + def device_class(self) -> CoverDeviceClass | None: """Return the device class of the cover.""" return self._device_class @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index 1c8a4afcc6f..f721a628ef8 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -40,7 +40,7 @@ class VeluxCover(VeluxEntity, CoverEntity): """Representation of a Velux cover.""" @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" supported_features = ( CoverEntityFeature.OPEN @@ -58,19 +58,19 @@ class VeluxCover(VeluxEntity, CoverEntity): return supported_features @property - def current_cover_position(self): + def current_cover_position(self) -> int: """Return the current position of the cover.""" return 100 - self.node.position.position_percent @property - def current_cover_tilt_position(self): + def current_cover_tilt_position(self) -> int | None: """Return the current position of the cover.""" if isinstance(self.node, Blind): return 100 - self.node.orientation.position_percent return None @property - def device_class(self): + def device_class(self) -> CoverDeviceClass: """Define this cover as either awning, blind, garage, gate, shutter or window.""" if isinstance(self.node, Awning): return CoverDeviceClass.AWNING @@ -87,7 +87,7 @@ class VeluxCover(VeluxEntity, CoverEntity): return CoverDeviceClass.WINDOW @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the cover is closed.""" return self.node.position.closed diff --git a/homeassistant/components/xiaomi_aqara/cover.py b/homeassistant/components/xiaomi_aqara/cover.py index e9946e37815..b6de7189d83 100644 --- a/homeassistant/components/xiaomi_aqara/cover.py +++ b/homeassistant/components/xiaomi_aqara/cover.py @@ -46,12 +46,12 @@ class XiaomiGenericCover(XiaomiDevice, CoverEntity): super().__init__(device, name, xiaomi_hub, config_entry) @property - def current_cover_position(self): + def current_cover_position(self) -> int: """Return the current position of the cover.""" return self._pos @property - def is_closed(self): + def is_closed(self) -> bool: """Return if the cover is closed.""" return self.current_cover_position <= 0 diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 39f76b6b77f..6ade62343b1 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -92,24 +92,24 @@ class ZhaCover(ZhaEntity, CoverEntity): self._current_position = last_state.attributes["current_position"] @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return if the cover is closed.""" if self.current_cover_position is None: return None return self.current_cover_position == 0 @property - def is_opening(self): + def is_opening(self) -> bool: """Return if the cover is opening or not.""" return self._state == STATE_OPENING @property - def is_closing(self): + def is_closing(self) -> bool: """Return if the cover is closing or not.""" return self._state == STATE_CLOSING @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """Return the current position of ZHA cover. None is unknown, 0 is closed, 100 is fully open. @@ -207,7 +207,7 @@ class Shade(ZhaEntity, CoverEntity): self._is_open: bool | None = None @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """Return current position of cover. None is unknown, 0 is closed, 100 is fully open. From 3743d42ade80528325d36357ca6f9629d4970eaa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 25 Jun 2022 12:31:53 +0200 Subject: [PATCH 1786/3516] Adjust smartthings cover type hints (#73948) --- homeassistant/components/smartthings/cover.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/smartthings/cover.py b/homeassistant/components/smartthings/cover.py index 80a30131301..59f6e09df19 100644 --- a/homeassistant/components/smartthings/cover.py +++ b/homeassistant/components/smartthings/cover.py @@ -70,6 +70,8 @@ def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: class SmartThingsCover(SmartThingsEntity, CoverEntity): """Define a SmartThings cover.""" + _attr_supported_features: int + def __init__(self, device): """Initialize the cover class.""" super().__init__(device) @@ -98,7 +100,7 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): # the entity state ahead of receiving the confirming push updates self.async_schedule_update_ha_state(True) - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" if not self._attr_supported_features & CoverEntityFeature.SET_POSITION: return @@ -143,7 +145,7 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): return None if self._state is None else False @property - def current_cover_position(self): + def current_cover_position(self) -> int | None: """Return current position of cover.""" if not self._attr_supported_features & CoverEntityFeature.SET_POSITION: return None From 9eed8b2ef4dc3ffd271368af545b28ceb6e4e93c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 25 Jun 2022 12:32:55 +0200 Subject: [PATCH 1787/3516] Adjust freedompro cover position method (#73945) --- homeassistant/components/freedompro/cover.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/freedompro/cover.py b/homeassistant/components/freedompro/cover.py index e4483f0005b..ebb8a98b4b1 100644 --- a/homeassistant/components/freedompro/cover.py +++ b/homeassistant/components/freedompro/cover.py @@ -105,15 +105,13 @@ class Device(CoordinatorEntity, CoverEntity): """Close the cover.""" await self.async_set_cover_position(position=0) - async def async_set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs: Any) -> None: """Async function to set position to cover.""" - payload = {} - payload["position"] = kwargs[ATTR_POSITION] - payload = json.dumps(payload) + payload = {"position": kwargs[ATTR_POSITION]} await put_state( self._session, self._api_key, self.unique_id, - payload, + json.dumps(payload), ) await self.coordinator.async_request_refresh() From eb6afd27b312ed2910a1ad32aa20a3f4ebd54cf8 Mon Sep 17 00:00:00 2001 From: rappenze Date: Sat, 25 Jun 2022 12:34:30 +0200 Subject: [PATCH 1788/3516] Fix fibaro cover state (#73921) --- homeassistant/components/fibaro/cover.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/fibaro/cover.py b/homeassistant/components/fibaro/cover.py index c0749f9c100..364cbcf39cb 100644 --- a/homeassistant/components/fibaro/cover.py +++ b/homeassistant/components/fibaro/cover.py @@ -93,6 +93,11 @@ class FibaroCover(FibaroDevice, CoverEntity): def is_closed(self) -> bool | None: """Return if the cover is closed.""" if self._is_open_close_only(): + if ( + "state" not in self.fibaro_device.properties + or self.fibaro_device.properties.state.lower() == "unknown" + ): + return None return self.fibaro_device.properties.state.lower() == "closed" if self.current_cover_position is None: From 85fdc562400211bbaab07233bd5e9a7c496ddb44 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Jun 2022 09:41:47 -0500 Subject: [PATCH 1789/3516] Bump aiosteamist to 0.3.2 (#73976) Changelog: https://github.com/bdraco/aiosteamist/compare/0.3.1...0.3.2 --- homeassistant/components/steamist/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/steamist/manifest.json b/homeassistant/components/steamist/manifest.json index 057645a1d4c..4ea50e5c7de 100644 --- a/homeassistant/components/steamist/manifest.json +++ b/homeassistant/components/steamist/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/steamist", - "requirements": ["aiosteamist==0.3.1", "discovery30303==0.2.1"], + "requirements": ["aiosteamist==0.3.2", "discovery30303==0.2.1"], "codeowners": ["@bdraco"], "iot_class": "local_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 339c0a8da88..c76bc5c26f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -247,7 +247,7 @@ aioskybell==22.6.1 aioslimproto==2.0.1 # homeassistant.components.steamist -aiosteamist==0.3.1 +aiosteamist==0.3.2 # homeassistant.components.switcher_kis aioswitcher==2.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33763e0b892..7c50b7ee62b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -216,7 +216,7 @@ aioskybell==22.6.1 aioslimproto==2.0.1 # homeassistant.components.steamist -aiosteamist==0.3.1 +aiosteamist==0.3.2 # homeassistant.components.switcher_kis aioswitcher==2.0.6 From e67f8720e81e8618370aaf3e6a36b8b1553b9e9a Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sat, 25 Jun 2022 11:15:38 -0400 Subject: [PATCH 1790/3516] Refactor UniFi Protect tests (#73971) Co-authored-by: J. Nick Koston --- tests/components/unifiprotect/conftest.py | 327 +++++++------ .../unifiprotect/fixtures/sample_camera.json | 54 +-- .../unifiprotect/fixtures/sample_chime.json | 2 +- .../fixtures/sample_doorlock.json | 6 +- .../unifiprotect/fixtures/sample_light.json | 14 +- .../unifiprotect/fixtures/sample_sensor.json | 24 +- .../unifiprotect/test_binary_sensor.py | 320 +++---------- tests/components/unifiprotect/test_button.py | 45 +- tests/components/unifiprotect/test_camera.py | 408 ++++++----------- .../unifiprotect/test_config_flow.py | 54 ++- .../unifiprotect/test_diagnostics.py | 47 +- tests/components/unifiprotect/test_init.py | 165 +++---- tests/components/unifiprotect/test_light.py | 98 ++-- tests/components/unifiprotect/test_lock.py | 174 ++++--- .../unifiprotect/test_media_player.py | 197 ++++---- tests/components/unifiprotect/test_migrate.py | 222 ++++----- tests/components/unifiprotect/test_number.py | 192 ++------ tests/components/unifiprotect/test_select.py | 429 +++++++----------- tests/components/unifiprotect/test_sensor.py | 371 ++++++--------- .../components/unifiprotect/test_services.py | 96 +--- tests/components/unifiprotect/test_switch.py | 416 ++++++----------- tests/components/unifiprotect/utils.py | 168 +++++++ 22 files changed, 1535 insertions(+), 2294 deletions(-) create mode 100644 tests/components/unifiprotect/utils.py diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 68945ac0988..51cef190e2f 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -3,14 +3,14 @@ from __future__ import annotations from collections.abc import Callable -from dataclasses import dataclass -from datetime import timedelta +from datetime import datetime, timedelta from ipaddress import IPv4Address import json from typing import Any from unittest.mock import AsyncMock, Mock, patch import pytest +from pyunifiprotect import ProtectApiClient from pyunifiprotect.data import ( NVR, Bootstrap, @@ -19,37 +19,27 @@ from pyunifiprotect.data import ( Doorlock, Light, Liveview, - ProtectAdoptableDeviceModel, Sensor, + SmartDetectObjectType, + VideoMode, Viewer, WSSubscriptionMessage, ) -from pyunifiprotect.test_util.anonymize import random_hex from homeassistant.components.unifiprotect.const import DOMAIN -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant, split_entity_id -from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity import EntityDescription +from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util from . import _patch_discovery +from .utils import MockUFPFixture -from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture +from tests.common import MockConfigEntry, load_fixture MAC_ADDR = "aa:bb:cc:dd:ee:ff" -@dataclass -class MockEntityFixture: - """Mock for NVR.""" - - entry: MockConfigEntry - api: Mock - - -@pytest.fixture(name="mock_nvr") -def mock_nvr_fixture(): +@pytest.fixture(name="nvr") +def mock_nvr(): """Mock UniFi Protect Camera device.""" data = json.loads(load_fixture("sample_nvr.json", integration=DOMAIN)) @@ -63,7 +53,7 @@ def mock_nvr_fixture(): NVR.__config__.validate_assignment = True -@pytest.fixture(name="mock_ufp_config_entry") +@pytest.fixture(name="ufp_config_entry") def mock_ufp_config_entry(): """Mock the unifiprotect config entry.""" @@ -81,8 +71,8 @@ def mock_ufp_config_entry(): ) -@pytest.fixture(name="mock_old_nvr") -def mock_old_nvr_fixture(): +@pytest.fixture(name="old_nvr") +def old_nvr(): """Mock UniFi Protect Camera device.""" data = json.loads(load_fixture("sample_nvr.json", integration=DOMAIN)) @@ -90,11 +80,11 @@ def mock_old_nvr_fixture(): return NVR.from_unifi_dict(**data) -@pytest.fixture(name="mock_bootstrap") -def mock_bootstrap_fixture(mock_nvr: NVR): +@pytest.fixture(name="bootstrap") +def bootstrap_fixture(nvr: NVR): """Mock Bootstrap fixture.""" data = json.loads(load_fixture("sample_bootstrap.json", integration=DOMAIN)) - data["nvr"] = mock_nvr + data["nvr"] = nvr data["cameras"] = [] data["lights"] = [] data["sensors"] = [] @@ -107,24 +97,11 @@ def mock_bootstrap_fixture(mock_nvr: NVR): return Bootstrap.from_unifi_dict(**data) -def reset_objects(bootstrap: Bootstrap): - """Reset bootstrap objects.""" - - bootstrap.cameras = {} - bootstrap.lights = {} - bootstrap.sensors = {} - bootstrap.viewers = {} - bootstrap.liveviews = {} - bootstrap.events = {} - bootstrap.doorlocks = {} - bootstrap.chimes = {} - - -@pytest.fixture -def mock_client(mock_bootstrap: Bootstrap): +@pytest.fixture(name="ufp_client") +def mock_ufp_client(bootstrap: Bootstrap): """Mock ProtectApiClient for testing.""" client = Mock() - client.bootstrap = mock_bootstrap + client.bootstrap = bootstrap nvr = client.bootstrap.nvr nvr._api = client @@ -133,161 +110,227 @@ def mock_client(mock_bootstrap: Bootstrap): client.base_url = "https://127.0.0.1" client.connection_host = IPv4Address("127.0.0.1") client.get_nvr = AsyncMock(return_value=nvr) - client.update = AsyncMock(return_value=mock_bootstrap) + client.update = AsyncMock(return_value=bootstrap) client.async_disconnect_ws = AsyncMock() - - def subscribe(ws_callback: Callable[[WSSubscriptionMessage], None]) -> Any: - client.ws_subscription = ws_callback - - return Mock() - - client.subscribe_websocket = subscribe return client -@pytest.fixture +@pytest.fixture(name="ufp") def mock_entry( - hass: HomeAssistant, - mock_ufp_config_entry: MockConfigEntry, - mock_client, # pylint: disable=redefined-outer-name + hass: HomeAssistant, ufp_config_entry: MockConfigEntry, ufp_client: ProtectApiClient ): """Mock ProtectApiClient for testing.""" with _patch_discovery(no_device=True), patch( "homeassistant.components.unifiprotect.ProtectApiClient" ) as mock_api: - mock_ufp_config_entry.add_to_hass(hass) + ufp_config_entry.add_to_hass(hass) - mock_api.return_value = mock_client + mock_api.return_value = ufp_client - yield MockEntityFixture(mock_ufp_config_entry, mock_client) + ufp = MockUFPFixture(ufp_config_entry, ufp_client) + + def subscribe(ws_callback: Callable[[WSSubscriptionMessage], None]) -> Any: + ufp.ws_subscription = ws_callback + return Mock() + + ufp_client.subscribe_websocket = subscribe + yield ufp @pytest.fixture -def mock_liveview(): +def liveview(): """Mock UniFi Protect Liveview.""" data = json.loads(load_fixture("sample_liveview.json", integration=DOMAIN)) return Liveview.from_unifi_dict(**data) -@pytest.fixture -def mock_camera(): +@pytest.fixture(name="camera") +def camera_fixture(fixed_now: datetime): """Mock UniFi Protect Camera device.""" + # disable pydantic validation so mocking can happen + Camera.__config__.validate_assignment = False + data = json.loads(load_fixture("sample_camera.json", integration=DOMAIN)) - return Camera.from_unifi_dict(**data) + camera = Camera.from_unifi_dict(**data) + camera.last_motion = fixed_now - timedelta(hours=1) + + yield camera + + Camera.__config__.validate_assignment = True + + +@pytest.fixture(name="camera_all") +def camera_all_fixture(camera: Camera): + """Mock UniFi Protect Camera device.""" + + all_camera = camera.copy() + all_camera.channels = [all_camera.channels[0].copy()] + + medium_channel = all_camera.channels[0].copy() + medium_channel.name = "Medium" + medium_channel.id = 1 + medium_channel.rtsp_alias = "test_medium_alias" + all_camera.channels.append(medium_channel) + + low_channel = all_camera.channels[0].copy() + low_channel.name = "Low" + low_channel.id = 2 + low_channel.rtsp_alias = "test_medium_alias" + all_camera.channels.append(low_channel) + + return all_camera + + +@pytest.fixture(name="doorbell") +def doorbell_fixture(camera: Camera, fixed_now: datetime): + """Mock UniFi Protect Camera device (with chime).""" + + doorbell = camera.copy() + doorbell.channels = [c.copy() for c in doorbell.channels] + + package_channel = doorbell.channels[0].copy() + package_channel.name = "Package Camera" + package_channel.id = 3 + package_channel.fps = 2 + package_channel.rtsp_alias = "test_package_alias" + + doorbell.channels.append(package_channel) + doorbell.feature_flags.video_modes = [VideoMode.DEFAULT, VideoMode.HIGH_FPS] + doorbell.feature_flags.smart_detect_types = [ + SmartDetectObjectType.PERSON, + SmartDetectObjectType.VEHICLE, + ] + doorbell.feature_flags.has_hdr = True + doorbell.feature_flags.has_lcd_screen = True + doorbell.feature_flags.has_speaker = True + doorbell.feature_flags.has_privacy_mask = True + doorbell.feature_flags.has_chime = True + doorbell.feature_flags.has_smart_detect = True + doorbell.feature_flags.has_package_camera = True + doorbell.feature_flags.has_led_status = True + doorbell.last_ring = fixed_now - timedelta(hours=1) + return doorbell @pytest.fixture -def mock_light(): +def unadopted_camera(camera: Camera): + """Mock UniFi Protect Camera device (unadopted).""" + + no_camera = camera.copy() + no_camera.channels = [c.copy() for c in no_camera.channels] + no_camera.name = "Unadopted Camera" + no_camera.is_adopted = False + return no_camera + + +@pytest.fixture(name="light") +def light_fixture(): """Mock UniFi Protect Light device.""" + # disable pydantic validation so mocking can happen + Light.__config__.validate_assignment = False + data = json.loads(load_fixture("sample_light.json", integration=DOMAIN)) - return Light.from_unifi_dict(**data) + yield Light.from_unifi_dict(**data) + + Light.__config__.validate_assignment = True @pytest.fixture -def mock_viewer(): +def unadopted_light(light: Light): + """Mock UniFi Protect Light device (unadopted).""" + + no_light = light.copy() + no_light.name = "Unadopted Light" + no_light.is_adopted = False + return no_light + + +@pytest.fixture +def viewer(): """Mock UniFi Protect Viewport device.""" + # disable pydantic validation so mocking can happen + Viewer.__config__.validate_assignment = False + data = json.loads(load_fixture("sample_viewport.json", integration=DOMAIN)) - return Viewer.from_unifi_dict(**data) + yield Viewer.from_unifi_dict(**data) + + Viewer.__config__.validate_assignment = True -@pytest.fixture -def mock_sensor(): +@pytest.fixture(name="sensor") +def sensor_fixture(fixed_now: datetime): """Mock UniFi Protect Sensor device.""" + # disable pydantic validation so mocking can happen + Sensor.__config__.validate_assignment = False + data = json.loads(load_fixture("sample_sensor.json", integration=DOMAIN)) - return Sensor.from_unifi_dict(**data) + sensor: Sensor = Sensor.from_unifi_dict(**data) + sensor.motion_detected_at = fixed_now - timedelta(hours=1) + sensor.open_status_changed_at = fixed_now - timedelta(hours=1) + sensor.alarm_triggered_at = fixed_now - timedelta(hours=1) + yield sensor + + Sensor.__config__.validate_assignment = True -@pytest.fixture -def mock_doorlock(): +@pytest.fixture(name="sensor_all") +def csensor_all_fixture(sensor: Sensor): + """Mock UniFi Protect Sensor device.""" + + all_sensor = sensor.copy() + all_sensor.light_settings.is_enabled = True + all_sensor.humidity_settings.is_enabled = True + all_sensor.temperature_settings.is_enabled = True + all_sensor.alarm_settings.is_enabled = True + all_sensor.led_settings.is_enabled = True + all_sensor.motion_settings.is_enabled = True + + return all_sensor + + +@pytest.fixture(name="doorlock") +def doorlock_fixture(): """Mock UniFi Protect Doorlock device.""" + # disable pydantic validation so mocking can happen + Doorlock.__config__.validate_assignment = False + data = json.loads(load_fixture("sample_doorlock.json", integration=DOMAIN)) - return Doorlock.from_unifi_dict(**data) + yield Doorlock.from_unifi_dict(**data) + + Doorlock.__config__.validate_assignment = True @pytest.fixture -def mock_chime(): +def unadopted_doorlock(doorlock: Doorlock): + """Mock UniFi Protect Light device (unadopted).""" + + no_doorlock = doorlock.copy() + no_doorlock.name = "Unadopted Lock" + no_doorlock.is_adopted = False + return no_doorlock + + +@pytest.fixture +def chime(): """Mock UniFi Protect Chime device.""" + # disable pydantic validation so mocking can happen + Chime.__config__.validate_assignment = False + data = json.loads(load_fixture("sample_chime.json", integration=DOMAIN)) - return Chime.from_unifi_dict(**data) + yield Chime.from_unifi_dict(**data) + + Chime.__config__.validate_assignment = True -@pytest.fixture -def now(): +@pytest.fixture(name="fixed_now") +def fixed_now_fixture(): """Return datetime object that will be consistent throughout test.""" return dt_util.utcnow() - - -async def time_changed(hass: HomeAssistant, seconds: int) -> None: - """Trigger time changed.""" - next_update = dt_util.utcnow() + timedelta(seconds) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - - -async def enable_entity( - hass: HomeAssistant, entry_id: str, entity_id: str -) -> er.RegistryEntry: - """Enable a disabled entity.""" - entity_registry = er.async_get(hass) - - updated_entity = entity_registry.async_update_entity(entity_id, disabled_by=None) - assert not updated_entity.disabled - await hass.config_entries.async_reload(entry_id) - await hass.async_block_till_done() - - return updated_entity - - -def assert_entity_counts( - hass: HomeAssistant, platform: Platform, total: int, enabled: int -) -> None: - """Assert entity counts for a given platform.""" - - entity_registry = er.async_get(hass) - - entities = [ - e for e in entity_registry.entities if split_entity_id(e)[0] == platform.value - ] - - assert len(entities) == total - assert len(hass.states.async_all(platform.value)) == enabled - - -def ids_from_device_description( - platform: Platform, - device: ProtectAdoptableDeviceModel, - description: EntityDescription, -) -> tuple[str, str]: - """Return expected unique_id and entity_id for a give platform/device/description combination.""" - - entity_name = ( - device.name.lower().replace(":", "").replace(" ", "_").replace("-", "_") - ) - description_entity_name = ( - description.name.lower().replace(":", "").replace(" ", "_").replace("-", "_") - ) - - unique_id = f"{device.mac}_{description.key}" - entity_id = f"{platform.value}.{entity_name}_{description_entity_name}" - - return unique_id, entity_id - - -def generate_random_ids() -> tuple[str, str]: - """Generate random IDs for device.""" - - return random_hex(24).lower(), random_hex(12).upper() - - -def regenerate_device_ids(device: ProtectAdoptableDeviceModel) -> None: - """Regenerate the IDs on UFP device.""" - - device.id, device.mac = generate_random_ids() diff --git a/tests/components/unifiprotect/fixtures/sample_camera.json b/tests/components/unifiprotect/fixtures/sample_camera.json index eb07c6df63b..e7ffbd0abcc 100644 --- a/tests/components/unifiprotect/fixtures/sample_camera.json +++ b/tests/components/unifiprotect/fixtures/sample_camera.json @@ -4,7 +4,7 @@ "host": "192.168.6.90", "connectionHost": "192.168.178.217", "type": "UVC G4 Instant", - "name": "Fufail Qqjx", + "name": "Test Camera", "upSince": 1640020678036, "uptime": 3203, "lastSeen": 1640023881036, @@ -20,18 +20,18 @@ "isAdoptedByOther": false, "isProvisioned": true, "isRebooting": false, - "isSshEnabled": true, + "isSshEnabled": false, "canAdopt": false, "isAttemptingToConnect": false, "lastMotion": 1640021213927, - "micVolume": 100, + "micVolume": 0, "isMicEnabled": true, "isRecording": false, "isWirelessUplinkEnabled": true, "isMotionDetected": false, "isSmartDetected": false, "phyRate": 72, - "hdrMode": true, + "hdrMode": false, "videoMode": "default", "isProbingForWifi": false, "apMac": null, @@ -57,18 +57,18 @@ } }, "videoReconfigurationInProgress": false, - "voltage": null, + "voltage": 20.0, "wiredConnectionState": { - "phyRate": null + "phyRate": 1000 }, "channels": [ { "id": 0, "videoId": "video1", - "name": "Jzi Bftu", + "name": "High", "enabled": true, "isRtspEnabled": true, - "rtspAlias": "ANOAPfoKMW7VixG1", + "rtspAlias": "test_high_alias", "width": 2688, "height": 1512, "fps": 30, @@ -83,10 +83,10 @@ { "id": 1, "videoId": "video2", - "name": "Rgcpxsf Xfwt", + "name": "Medium", "enabled": true, - "isRtspEnabled": true, - "rtspAlias": "XHXAdHVKGVEzMNTP", + "isRtspEnabled": false, + "rtspAlias": null, "width": 1280, "height": 720, "fps": 30, @@ -101,7 +101,7 @@ { "id": 2, "videoId": "video3", - "name": "Umefvk Fug", + "name": "Low", "enabled": true, "isRtspEnabled": false, "rtspAlias": null, @@ -121,7 +121,7 @@ "aeMode": "auto", "irLedMode": "auto", "irLedLevel": 255, - "wdr": 1, + "wdr": 0, "icrSensitivity": 0, "brightness": 50, "contrast": 50, @@ -161,8 +161,8 @@ "quality": 100 }, "osdSettings": { - "isNameEnabled": true, - "isDateEnabled": true, + "isNameEnabled": false, + "isDateEnabled": false, "isLogoEnabled": false, "isDebugEnabled": false }, @@ -181,7 +181,7 @@ "minMotionEventTrigger": 1000, "endMotionEventDelay": 3000, "suppressIlluminationSurge": false, - "mode": "detections", + "mode": "always", "geofencing": "off", "motionAlgorithm": "enhanced", "enablePirTimelapse": false, @@ -223,8 +223,8 @@ ], "smartDetectLines": [], "stats": { - "rxBytes": 33684237, - "txBytes": 1208318620, + "rxBytes": 100, + "txBytes": 100, "wifi": { "channel": 6, "frequency": 2437, @@ -248,8 +248,8 @@ "timelapseEndLQ": 1640021765237 }, "storage": { - "used": 20401094656, - "rate": 693.424269097809 + "used": 100, + "rate": 0.1 }, "wifiQuality": 100, "wifiStrength": -35 @@ -257,7 +257,7 @@ "featureFlags": { "canAdjustIrLedLevel": false, "canMagicZoom": false, - "canOpticalZoom": false, + "canOpticalZoom": true, "canTouchFocus": false, "hasAccelerometer": true, "hasAec": true, @@ -268,15 +268,15 @@ "hasIcrSensitivity": true, "hasLdc": false, "hasLedIr": true, - "hasLedStatus": true, + "hasLedStatus": false, "hasLineIn": false, "hasMic": true, - "hasPrivacyMask": true, + "hasPrivacyMask": false, "hasRtc": false, "hasSdCard": false, - "hasSpeaker": true, + "hasSpeaker": false, "hasWifi": true, - "hasHdr": true, + "hasHdr": false, "hasAutoICROnly": true, "videoModes": ["default"], "videoModeMaxFps": [], @@ -353,14 +353,14 @@ "frequency": 2437, "phyRate": 72, "signalQuality": 100, - "signalStrength": -35, + "signalStrength": -50, "ssid": "Mortis Camera" }, "lenses": [], "id": "0de062b4f6922d489d3b312d", "isConnected": true, "platform": "sav530q", - "hasSpeaker": true, + "hasSpeaker": false, "hasWifi": true, "audioBitrate": 64000, "canManage": false, diff --git a/tests/components/unifiprotect/fixtures/sample_chime.json b/tests/components/unifiprotect/fixtures/sample_chime.json index 975cfcebaea..4a2637fc700 100644 --- a/tests/components/unifiprotect/fixtures/sample_chime.json +++ b/tests/components/unifiprotect/fixtures/sample_chime.json @@ -3,7 +3,7 @@ "host": "192.168.144.146", "connectionHost": "192.168.234.27", "type": "UP Chime", - "name": "Xaorvu Tvsv", + "name": "Test Chime", "upSince": 1651882870009, "uptime": 567870, "lastSeen": 1652450740009, diff --git a/tests/components/unifiprotect/fixtures/sample_doorlock.json b/tests/components/unifiprotect/fixtures/sample_doorlock.json index 12cd7858e9d..a2e2ba0ab89 100644 --- a/tests/components/unifiprotect/fixtures/sample_doorlock.json +++ b/tests/components/unifiprotect/fixtures/sample_doorlock.json @@ -3,7 +3,7 @@ "host": null, "connectionHost": "192.168.102.63", "type": "UFP-LOCK-R", - "name": "Wkltg Qcjxv", + "name": "Test Lock", "upSince": 1643050461849, "uptime": null, "lastSeen": 1643052750858, @@ -23,9 +23,9 @@ "canAdopt": false, "isAttemptingToConnect": false, "credentials": "955756200c7f43936df9d5f7865f058e1528945aac0f0cb27cef960eb58f17db", - "lockStatus": "CLOSING", + "lockStatus": "OPEN", "enableHomekit": false, - "autoCloseTimeMs": 15000, + "autoCloseTimeMs": 45000, "wiredConnectionState": { "phyRate": null }, diff --git a/tests/components/unifiprotect/fixtures/sample_light.json b/tests/components/unifiprotect/fixtures/sample_light.json index ed0f89f3a11..ce7de9e852c 100644 --- a/tests/components/unifiprotect/fixtures/sample_light.json +++ b/tests/components/unifiprotect/fixtures/sample_light.json @@ -3,7 +3,7 @@ "host": "192.168.10.86", "connectionHost": "192.168.178.217", "type": "UP FloodLight", - "name": "Byyfbpe Ufoka", + "name": "Test Light", "upSince": 1638128991022, "uptime": 1894890, "lastSeen": 1640023881022, @@ -19,7 +19,7 @@ "isAdoptedByOther": false, "isProvisioned": false, "isRebooting": false, - "isSshEnabled": true, + "isSshEnabled": false, "canAdopt": false, "isAttemptingToConnect": false, "isPirMotionDetected": false, @@ -31,20 +31,20 @@ "phyRate": 100 }, "lightDeviceSettings": { - "isIndicatorEnabled": true, + "isIndicatorEnabled": false, "ledLevel": 6, "luxSensitivity": "medium", - "pirDuration": 120000, - "pirSensitivity": 46 + "pirDuration": 45000, + "pirSensitivity": 45 }, "lightOnSettings": { "isLedForceOn": false }, "lightModeSettings": { - "mode": "off", + "mode": "motion", "enableAt": "fulltime" }, - "camera": "193be66559c03ec5629f54cd", + "camera": null, "id": "37dd610720816cfb5c547967", "isConnected": true, "isCameraPaired": true, diff --git a/tests/components/unifiprotect/fixtures/sample_sensor.json b/tests/components/unifiprotect/fixtures/sample_sensor.json index 08ce9a17be2..cbba1f7583e 100644 --- a/tests/components/unifiprotect/fixtures/sample_sensor.json +++ b/tests/components/unifiprotect/fixtures/sample_sensor.json @@ -2,7 +2,7 @@ "mac": "26DBAFF133A4", "connectionHost": "192.168.216.198", "type": "UFP-SENSE", - "name": "Egdczv Urg", + "name": "Test Sensor", "upSince": 1641256963255, "uptime": null, "lastSeen": 1641259127934, @@ -25,7 +25,7 @@ "mountType": "door", "leakDetectedAt": null, "tamperingDetectedAt": null, - "isOpened": true, + "isOpened": false, "openStatusChangedAt": 1641269036582, "alarmTriggeredAt": null, "motionDetectedAt": 1641269044824, @@ -34,53 +34,53 @@ }, "stats": { "light": { - "value": 0, + "value": 10.0, "status": "neutral" }, "humidity": { - "value": 35, + "value": 10.0, "status": "neutral" }, "temperature": { - "value": 17.23, + "value": 10.0, "status": "neutral" } }, "bluetoothConnectionState": { "signalQuality": 15, - "signalStrength": -84 + "signalStrength": -50 }, "batteryStatus": { - "percentage": 100, + "percentage": 10, "isLow": false }, "alarmSettings": { "isEnabled": false }, "lightSettings": { - "isEnabled": true, + "isEnabled": false, "lowThreshold": null, "highThreshold": null, "margin": 10 }, "motionSettings": { - "isEnabled": true, + "isEnabled": false, "sensitivity": 100 }, "temperatureSettings": { - "isEnabled": true, + "isEnabled": false, "lowThreshold": null, "highThreshold": null, "margin": 0.1 }, "humiditySettings": { - "isEnabled": true, + "isEnabled": false, "lowThreshold": null, "highThreshold": null, "margin": 1 }, "ledSettings": { - "isEnabled": true + "isEnabled": false }, "bridge": "61b3f5c90050a703e700042a", "camera": "2f9beb2e6f79af3c32c22d49", diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index da9969ad868..856c034905f 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -2,11 +2,9 @@ # pylint: disable=protected-access from __future__ import annotations -from copy import copy from datetime import datetime, timedelta from unittest.mock import Mock -import pytest from pyunifiprotect.data import Camera, Event, EventType, Light, MountType, Sensor from pyunifiprotect.data.nvr import EventMetadata @@ -32,218 +30,25 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import ( - MockEntityFixture, +from .utils import ( + MockUFPFixture, assert_entity_counts, ids_from_device_description, - regenerate_device_ids, - reset_objects, + init_entry, ) LIGHT_SENSOR_WRITE = LIGHT_SENSORS[:2] SENSE_SENSORS_WRITE = SENSE_SENSORS[:4] -@pytest.fixture(name="camera") -async def camera_fixture( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - mock_camera: Camera, - now: datetime, -): - """Fixture for a single camera for testing the binary_sensor platform.""" - - # disable pydantic validation so mocking can happen - Camera.__config__.validate_assignment = False - - camera_obj = mock_camera.copy() - camera_obj._api = mock_entry.api - camera_obj.channels[0]._api = mock_entry.api - camera_obj.channels[1]._api = mock_entry.api - camera_obj.channels[2]._api = mock_entry.api - camera_obj.name = "Test Camera" - camera_obj.feature_flags.has_chime = True - camera_obj.last_ring = now - timedelta(hours=1) - camera_obj.is_dark = False - camera_obj.is_motion_detected = False - regenerate_device_ids(camera_obj) - - no_camera_obj = mock_camera.copy() - no_camera_obj._api = mock_entry.api - no_camera_obj.channels[0]._api = mock_entry.api - no_camera_obj.channels[1]._api = mock_entry.api - no_camera_obj.channels[2]._api = mock_entry.api - no_camera_obj.name = "Unadopted Camera" - no_camera_obj.is_adopted = False - regenerate_device_ids(no_camera_obj) - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] - mock_entry.api.bootstrap.cameras = { - camera_obj.id: camera_obj, - no_camera_obj.id: no_camera_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.BINARY_SENSOR, 9, 9) - - yield camera_obj - - Camera.__config__.validate_assignment = True - - -@pytest.fixture(name="light") -async def light_fixture( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light, now: datetime -): - """Fixture for a single light for testing the binary_sensor platform.""" - - # disable pydantic validation so mocking can happen - Light.__config__.validate_assignment = False - - light_obj = mock_light.copy() - light_obj._api = mock_entry.api - light_obj.name = "Test Light" - light_obj.is_dark = False - light_obj.is_pir_motion_detected = False - light_obj.last_motion = now - timedelta(hours=1) - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] - mock_entry.api.bootstrap.lights = { - light_obj.id: light_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.BINARY_SENSOR, 8, 8) - - yield light_obj - - Light.__config__.validate_assignment = True - - -@pytest.fixture(name="camera_none") -async def camera_none_fixture( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera -): - """Fixture for a single camera for testing the binary_sensor platform.""" - - # disable pydantic validation so mocking can happen - Camera.__config__.validate_assignment = False - - camera_obj = mock_camera.copy() - camera_obj._api = mock_entry.api - camera_obj.channels[0]._api = mock_entry.api - camera_obj.channels[1]._api = mock_entry.api - camera_obj.channels[2]._api = mock_entry.api - camera_obj.name = "Test Camera" - camera_obj.feature_flags.has_chime = False - camera_obj.is_dark = False - camera_obj.is_motion_detected = False - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] - mock_entry.api.bootstrap.nvr.system_info.ustorage = None - mock_entry.api.bootstrap.cameras = { - camera_obj.id: camera_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.BINARY_SENSOR, 2, 2) - - yield camera_obj - - Camera.__config__.validate_assignment = True - - -@pytest.fixture(name="sensor") -async def sensor_fixture( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - mock_sensor: Sensor, - now: datetime, -): - """Fixture for a single sensor for testing the binary_sensor platform.""" - - # disable pydantic validation so mocking can happen - Sensor.__config__.validate_assignment = False - - sensor_obj = mock_sensor.copy() - sensor_obj._api = mock_entry.api - sensor_obj.name = "Test Sensor" - sensor_obj.mount_type = MountType.DOOR - sensor_obj.is_opened = False - sensor_obj.battery_status.is_low = False - sensor_obj.is_motion_detected = False - sensor_obj.alarm_settings.is_enabled = True - sensor_obj.motion_detected_at = now - timedelta(hours=1) - sensor_obj.open_status_changed_at = now - timedelta(hours=1) - sensor_obj.alarm_triggered_at = now - timedelta(hours=1) - sensor_obj.tampering_detected_at = None - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] - mock_entry.api.bootstrap.sensors = { - sensor_obj.id: sensor_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.BINARY_SENSOR, 10, 10) - - yield sensor_obj - - Sensor.__config__.validate_assignment = True - - -@pytest.fixture(name="sensor_none") -async def sensor_none_fixture( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - mock_sensor: Sensor, - now: datetime, -): - """Fixture for a single sensor for testing the binary_sensor platform.""" - - # disable pydantic validation so mocking can happen - Sensor.__config__.validate_assignment = False - - sensor_obj = mock_sensor.copy() - sensor_obj._api = mock_entry.api - sensor_obj.name = "Test Sensor" - sensor_obj.mount_type = MountType.LEAK - sensor_obj.battery_status.is_low = False - sensor_obj.alarm_settings.is_enabled = False - sensor_obj.tampering_detected_at = None - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] - mock_entry.api.bootstrap.sensors = { - sensor_obj.id: sensor_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.BINARY_SENSOR, 10, 10) - - yield sensor_obj - - Sensor.__config__.validate_assignment = True - - async def test_binary_sensor_setup_light( - hass: HomeAssistant, light: Light, now: datetime + hass: HomeAssistant, ufp: MockUFPFixture, light: Light ): """Test binary_sensor entity setup for light devices.""" + await init_entry(hass, ufp, [light]) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 8, 8) + entity_registry = er.async_get(hass) for description in LIGHT_SENSOR_WRITE: @@ -262,15 +67,22 @@ async def test_binary_sensor_setup_light( async def test_binary_sensor_setup_camera_all( - hass: HomeAssistant, camera: Camera, now: datetime + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + unadopted_camera: Camera, ): """Test binary_sensor entity setup for camera devices (all features).""" + ufp.api.bootstrap.nvr.system_info.ustorage = None + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 3, 3) + entity_registry = er.async_get(hass) description = CAMERA_SENSORS[0] unique_id, entity_id = ids_from_device_description( - Platform.BINARY_SENSOR, camera, description + Platform.BINARY_SENSOR, doorbell, description ) entity = entity_registry.async_get(entity_id) @@ -285,7 +97,7 @@ async def test_binary_sensor_setup_camera_all( # Is Dark description = CAMERA_SENSORS[1] unique_id, entity_id = ids_from_device_description( - Platform.BINARY_SENSOR, camera, description + Platform.BINARY_SENSOR, doorbell, description ) entity = entity_registry.async_get(entity_id) @@ -300,7 +112,7 @@ async def test_binary_sensor_setup_camera_all( # Motion description = MOTION_SENSORS[0] unique_id, entity_id = ids_from_device_description( - Platform.BINARY_SENSOR, camera, description + Platform.BINARY_SENSOR, doorbell, description ) entity = entity_registry.async_get(entity_id) @@ -315,16 +127,19 @@ async def test_binary_sensor_setup_camera_all( async def test_binary_sensor_setup_camera_none( - hass: HomeAssistant, - camera_none: Camera, + hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera ): """Test binary_sensor entity setup for camera devices (no features).""" + ufp.api.bootstrap.nvr.system_info.ustorage = None + await init_entry(hass, ufp, [camera]) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 2, 2) + entity_registry = er.async_get(hass) description = CAMERA_SENSORS[1] unique_id, entity_id = ids_from_device_description( - Platform.BINARY_SENSOR, camera_none, description + Platform.BINARY_SENSOR, camera, description ) entity = entity_registry.async_get(entity_id) @@ -338,15 +153,18 @@ async def test_binary_sensor_setup_camera_none( async def test_binary_sensor_setup_sensor( - hass: HomeAssistant, sensor: Sensor, now: datetime + hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor ): """Test binary_sensor entity setup for sensor devices.""" + await init_entry(hass, ufp, [sensor_all]) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 10, 10) + entity_registry = er.async_get(hass) for description in SENSE_SENSORS_WRITE: unique_id, entity_id = ids_from_device_description( - Platform.BINARY_SENSOR, sensor, description + Platform.BINARY_SENSOR, sensor_all, description ) entity = entity_registry.async_get(entity_id) @@ -360,10 +178,14 @@ async def test_binary_sensor_setup_sensor( async def test_binary_sensor_setup_sensor_none( - hass: HomeAssistant, sensor_none: Sensor + hass: HomeAssistant, ufp: MockUFPFixture, sensor: Sensor ): """Test binary_sensor entity setup for sensor with most sensors disabled.""" + sensor.mount_type = MountType.LEAK + await init_entry(hass, ufp, [sensor]) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 10, 10) + entity_registry = er.async_get(hass) expected = [ @@ -374,7 +196,7 @@ async def test_binary_sensor_setup_sensor_none( ] for index, description in enumerate(SENSE_SENSORS_WRITE): unique_id, entity_id = ids_from_device_description( - Platform.BINARY_SENSOR, sensor_none, description + Platform.BINARY_SENSOR, sensor, description ) entity = entity_registry.async_get(entity_id) @@ -388,27 +210,33 @@ async def test_binary_sensor_setup_sensor_none( async def test_binary_sensor_update_motion( - hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera, now: datetime + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + unadopted_camera: Camera, + fixed_now: datetime, ): """Test binary_sensor motion entity.""" + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 9, 9) + _, entity_id = ids_from_device_description( - Platform.BINARY_SENSOR, camera, MOTION_SENSORS[0] + Platform.BINARY_SENSOR, doorbell, MOTION_SENSORS[0] ) event = Event( id="test_event_id", type=EventType.MOTION, - start=now - timedelta(seconds=1), + start=fixed_now - timedelta(seconds=1), end=None, score=100, smart_detect_types=[], smart_detect_event_ids=[], - camera_id=camera.id, + camera_id=doorbell.id, ) - new_bootstrap = copy(mock_entry.api.bootstrap) - new_camera = camera.copy() + new_camera = doorbell.copy() new_camera.is_motion_detected = True new_camera.last_motion_event_id = event.id @@ -416,10 +244,9 @@ async def test_binary_sensor_update_motion( mock_msg.changed_data = {} mock_msg.new_obj = new_camera - new_bootstrap.cameras = {new_camera.id: new_camera} - new_bootstrap.events = {event.id: event} - mock_entry.api.bootstrap = new_bootstrap - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.api.bootstrap.events = {event.id: event} + ufp.ws_msg(mock_msg) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -430,10 +257,13 @@ async def test_binary_sensor_update_motion( async def test_binary_sensor_update_light_motion( - hass: HomeAssistant, mock_entry: MockEntityFixture, light: Light, now: datetime + hass: HomeAssistant, ufp: MockUFPFixture, light: Light, fixed_now: datetime ): """Test binary_sensor motion entity.""" + await init_entry(hass, ufp, [light]) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 8, 8) + _, entity_id = ids_from_device_description( Platform.BINARY_SENSOR, light, LIGHT_SENSOR_WRITE[1] ) @@ -442,16 +272,15 @@ async def test_binary_sensor_update_light_motion( event = Event( id="test_event_id", type=EventType.MOTION_LIGHT, - start=now - timedelta(seconds=1), + start=fixed_now - timedelta(seconds=1), end=None, score=100, smart_detect_types=[], smart_detect_event_ids=[], metadata=event_metadata, - api=mock_entry.api, + api=ufp.api, ) - new_bootstrap = copy(mock_entry.api.bootstrap) new_light = light.copy() new_light.is_pir_motion_detected = True new_light.last_motion_event_id = event.id @@ -460,10 +289,9 @@ async def test_binary_sensor_update_light_motion( mock_msg.changed_data = {} mock_msg.new_obj = event - new_bootstrap.lights = {new_light.id: new_light} - new_bootstrap.events = {event.id: event} - mock_entry.api.bootstrap = new_bootstrap - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.lights = {new_light.id: new_light} + ufp.api.bootstrap.events = {event.id: event} + ufp.ws_msg(mock_msg) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -472,29 +300,30 @@ async def test_binary_sensor_update_light_motion( async def test_binary_sensor_update_mount_type_window( - hass: HomeAssistant, mock_entry: MockEntityFixture, sensor: Sensor + hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor ): """Test binary_sensor motion entity.""" + await init_entry(hass, ufp, [sensor_all]) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 10, 10) + _, entity_id = ids_from_device_description( - Platform.BINARY_SENSOR, sensor, SENSE_SENSORS_WRITE[0] + Platform.BINARY_SENSOR, sensor_all, SENSE_SENSORS_WRITE[0] ) state = hass.states.get(entity_id) assert state assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.DOOR.value - new_bootstrap = copy(mock_entry.api.bootstrap) - new_sensor = sensor.copy() + new_sensor = sensor_all.copy() new_sensor.mount_type = MountType.WINDOW mock_msg = Mock() mock_msg.changed_data = {} mock_msg.new_obj = new_sensor - new_bootstrap.sensors = {new_sensor.id: new_sensor} - mock_entry.api.bootstrap = new_bootstrap - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.sensors = {new_sensor.id: new_sensor} + ufp.ws_msg(mock_msg) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -503,29 +332,30 @@ async def test_binary_sensor_update_mount_type_window( async def test_binary_sensor_update_mount_type_garage( - hass: HomeAssistant, mock_entry: MockEntityFixture, sensor: Sensor + hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor ): """Test binary_sensor motion entity.""" + await init_entry(hass, ufp, [sensor_all]) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 10, 10) + _, entity_id = ids_from_device_description( - Platform.BINARY_SENSOR, sensor, SENSE_SENSORS_WRITE[0] + Platform.BINARY_SENSOR, sensor_all, SENSE_SENSORS_WRITE[0] ) state = hass.states.get(entity_id) assert state assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.DOOR.value - new_bootstrap = copy(mock_entry.api.bootstrap) - new_sensor = sensor.copy() + new_sensor = sensor_all.copy() new_sensor.mount_type = MountType.GARAGE mock_msg = Mock() mock_msg.changed_data = {} mock_msg.new_obj = new_sensor - new_bootstrap.sensors = {new_sensor.id: new_sensor} - mock_entry.api.bootstrap = new_bootstrap - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.sensors = {new_sensor.id: new_sensor} + ufp.ws_msg(mock_msg) await hass.async_block_till_done() state = hass.states.get(entity_id) diff --git a/tests/components/unifiprotect/test_button.py b/tests/components/unifiprotect/test_button.py index 9a1c7009660..a846214a7aa 100644 --- a/tests/components/unifiprotect/test_button.py +++ b/tests/components/unifiprotect/test_button.py @@ -4,7 +4,6 @@ from __future__ import annotations from unittest.mock import AsyncMock -import pytest from pyunifiprotect.data.devices import Chime from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION @@ -12,39 +11,20 @@ from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import MockEntityFixture, assert_entity_counts, enable_entity - - -@pytest.fixture(name="chime") -async def chime_fixture( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_chime: Chime -): - """Fixture for a single camera for testing the button platform.""" - - chime_obj = mock_chime.copy() - chime_obj._api = mock_entry.api - chime_obj.name = "Test Chime" - - mock_entry.api.bootstrap.chimes = { - chime_obj.id: chime_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.BUTTON, 3, 2) - - return chime_obj +from .utils import MockUFPFixture, assert_entity_counts, enable_entity, init_entry async def test_reboot_button( hass: HomeAssistant, - mock_entry: MockEntityFixture, + ufp: MockUFPFixture, chime: Chime, ): """Test button entity.""" - mock_entry.api.reboot_device = AsyncMock() + await init_entry(hass, ufp, [chime]) + assert_entity_counts(hass, Platform.BUTTON, 3, 2) + + ufp.api.reboot_device = AsyncMock() unique_id = f"{chime.mac}_reboot" entity_id = "button.test_chime_reboot_device" @@ -55,7 +35,7 @@ async def test_reboot_button( assert entity.disabled assert entity.unique_id == unique_id - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await enable_entity(hass, ufp.entry.entry_id, entity_id) state = hass.states.get(entity_id) assert state assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION @@ -63,17 +43,20 @@ async def test_reboot_button( await hass.services.async_call( "button", "press", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - mock_entry.api.reboot_device.assert_called_once() + ufp.api.reboot_device.assert_called_once() async def test_chime_button( hass: HomeAssistant, - mock_entry: MockEntityFixture, + ufp: MockUFPFixture, chime: Chime, ): """Test button entity.""" - mock_entry.api.play_speaker = AsyncMock() + await init_entry(hass, ufp, [chime]) + assert_entity_counts(hass, Platform.BUTTON, 3, 2) + + ufp.api.play_speaker = AsyncMock() unique_id = f"{chime.mac}_play" entity_id = "button.test_chime_play_chime" @@ -91,4 +74,4 @@ async def test_chime_button( await hass.services.async_call( "button", "press", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - mock_entry.api.play_speaker.assert_called_once() + ufp.api.play_speaker.assert_called_once() diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index 66da8e8ec04..6fad7cb899e 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -2,16 +2,13 @@ # pylint: disable=protected-access from __future__ import annotations -from copy import copy from unittest.mock import AsyncMock, Mock -import pytest from pyunifiprotect.data import Camera as ProtectCamera, CameraChannel, StateType from pyunifiprotect.exceptions import NvrError from homeassistant.components.camera import ( SUPPORT_STREAM, - Camera, async_get_image, async_get_stream_source, ) @@ -34,87 +31,15 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from .conftest import ( - MockEntityFixture, +from .utils import ( + MockUFPFixture, assert_entity_counts, enable_entity, - regenerate_device_ids, + init_entry, time_changed, ) -@pytest.fixture(name="camera") -async def camera_fixture( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera -): - """Fixture for a single camera for testing the camera platform.""" - - # disable pydantic validation so mocking can happen - ProtectCamera.__config__.validate_assignment = False - - camera_obj = mock_camera.copy() - camera_obj._api = mock_entry.api - camera_obj.channels[0]._api = mock_entry.api - camera_obj.channels[1]._api = mock_entry.api - camera_obj.channels[2]._api = mock_entry.api - camera_obj.name = "Test Camera" - camera_obj.channels[0].is_rtsp_enabled = True - camera_obj.channels[0].name = "High" - camera_obj.channels[1].is_rtsp_enabled = False - camera_obj.channels[2].is_rtsp_enabled = False - - mock_entry.api.bootstrap.cameras = { - camera_obj.id: camera_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.CAMERA, 2, 1) - - yield (camera_obj, "camera.test_camera_high") - - ProtectCamera.__config__.validate_assignment = True - - -@pytest.fixture(name="camera_package") -async def camera_package_fixture( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera -): - """Fixture for a single camera for testing the camera platform.""" - - camera_obj = mock_camera.copy() - camera_obj._api = mock_entry.api - camera_obj.channels[0]._api = mock_entry.api - camera_obj.channels[1]._api = mock_entry.api - camera_obj.channels[2]._api = mock_entry.api - camera_obj.name = "Test Camera" - camera_obj.feature_flags.has_package_camera = True - camera_obj.channels[0].is_rtsp_enabled = True - camera_obj.channels[0].name = "High" - camera_obj.channels[0].rtsp_alias = "test_high_alias" - camera_obj.channels[1].is_rtsp_enabled = False - camera_obj.channels[2].is_rtsp_enabled = False - package_channel = camera_obj.channels[0].copy() - package_channel.is_rtsp_enabled = False - package_channel.name = "Package Camera" - package_channel.id = 3 - package_channel.fps = 2 - package_channel.rtsp_alias = "test_package_alias" - camera_obj.channels.append(package_channel) - - mock_entry.api.bootstrap.cameras = { - camera_obj.id: camera_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.CAMERA, 3, 2) - - return (camera_obj, "camera.test_camera_package_camera") - - def validate_default_camera_entity( hass: HomeAssistant, camera_obj: ProtectCamera, @@ -242,99 +167,46 @@ async def validate_no_stream_camera_state( async def test_basic_setup( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: ProtectCamera + hass: HomeAssistant, + ufp: MockUFPFixture, + camera_all: ProtectCamera, + doorbell: ProtectCamera, ): """Test working setup of unifiprotect entry.""" - camera_high_only = mock_camera.copy() - camera_high_only._api = mock_entry.api - camera_high_only.channels = [c.copy() for c in mock_camera.channels] - camera_high_only.channels[0]._api = mock_entry.api - camera_high_only.channels[1]._api = mock_entry.api - camera_high_only.channels[2]._api = mock_entry.api + camera_high_only = camera_all.copy() + camera_high_only.channels = [c.copy() for c in camera_all.channels] camera_high_only.name = "Test Camera 1" camera_high_only.channels[0].is_rtsp_enabled = True - camera_high_only.channels[0].name = "High" - camera_high_only.channels[0].rtsp_alias = "test_high_alias" camera_high_only.channels[1].is_rtsp_enabled = False camera_high_only.channels[2].is_rtsp_enabled = False - regenerate_device_ids(camera_high_only) - camera_medium_only = mock_camera.copy() - camera_medium_only._api = mock_entry.api - camera_medium_only.channels = [c.copy() for c in mock_camera.channels] - camera_medium_only.channels[0]._api = mock_entry.api - camera_medium_only.channels[1]._api = mock_entry.api - camera_medium_only.channels[2]._api = mock_entry.api + camera_medium_only = camera_all.copy() + camera_medium_only.channels = [c.copy() for c in camera_all.channels] camera_medium_only.name = "Test Camera 2" camera_medium_only.channels[0].is_rtsp_enabled = False camera_medium_only.channels[1].is_rtsp_enabled = True - camera_medium_only.channels[1].name = "Medium" - camera_medium_only.channels[1].rtsp_alias = "test_medium_alias" camera_medium_only.channels[2].is_rtsp_enabled = False - regenerate_device_ids(camera_medium_only) - camera_all_channels = mock_camera.copy() - camera_all_channels._api = mock_entry.api - camera_all_channels.channels = [c.copy() for c in mock_camera.channels] - camera_all_channels.channels[0]._api = mock_entry.api - camera_all_channels.channels[1]._api = mock_entry.api - camera_all_channels.channels[2]._api = mock_entry.api - camera_all_channels.name = "Test Camera 3" - camera_all_channels.channels[0].is_rtsp_enabled = True - camera_all_channels.channels[0].name = "High" - camera_all_channels.channels[0].rtsp_alias = "test_high_alias" - camera_all_channels.channels[1].is_rtsp_enabled = True - camera_all_channels.channels[1].name = "Medium" - camera_all_channels.channels[1].rtsp_alias = "test_medium_alias" - camera_all_channels.channels[2].is_rtsp_enabled = True - camera_all_channels.channels[2].name = "Low" - camera_all_channels.channels[2].rtsp_alias = "test_low_alias" - regenerate_device_ids(camera_all_channels) + camera_all.name = "Test Camera 3" - camera_no_channels = mock_camera.copy() - camera_no_channels._api = mock_entry.api - camera_no_channels.channels = [c.copy() for c in camera_no_channels.channels] - camera_no_channels.channels[0]._api = mock_entry.api - camera_no_channels.channels[1]._api = mock_entry.api - camera_no_channels.channels[2]._api = mock_entry.api + camera_no_channels = camera_all.copy() + camera_no_channels.channels = [c.copy() for c in camera_all.channels] camera_no_channels.name = "Test Camera 4" camera_no_channels.channels[0].is_rtsp_enabled = False - camera_no_channels.channels[0].name = "High" camera_no_channels.channels[1].is_rtsp_enabled = False camera_no_channels.channels[2].is_rtsp_enabled = False - regenerate_device_ids(camera_no_channels) - camera_package = mock_camera.copy() - camera_package._api = mock_entry.api - camera_package.channels = [c.copy() for c in mock_camera.channels] - camera_package.channels[0]._api = mock_entry.api - camera_package.channels[1]._api = mock_entry.api - camera_package.channels[2]._api = mock_entry.api - camera_package.name = "Test Camera 5" - camera_package.channels[0].is_rtsp_enabled = True - camera_package.channels[0].name = "High" - camera_package.channels[0].rtsp_alias = "test_high_alias" - camera_package.channels[1].is_rtsp_enabled = False - camera_package.channels[2].is_rtsp_enabled = False - regenerate_device_ids(camera_package) - package_channel = camera_package.channels[0].copy() - package_channel.is_rtsp_enabled = False - package_channel.name = "Package Camera" - package_channel.id = 3 - package_channel.fps = 2 - package_channel.rtsp_alias = "test_package_alias" - camera_package.channels.append(package_channel) + doorbell.name = "Test Camera 5" - mock_entry.api.bootstrap.cameras = { - camera_high_only.id: camera_high_only, - camera_medium_only.id: camera_medium_only, - camera_all_channels.id: camera_all_channels, - camera_no_channels.id: camera_no_channels, - camera_package.id: camera_package, - } - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() + devices = [ + camera_high_only, + camera_medium_only, + camera_all, + camera_no_channels, + doorbell, + ] + await init_entry(hass, ufp, devices) assert_entity_counts(hass, Platform.CAMERA, 14, 6) @@ -343,7 +215,7 @@ async def test_basic_setup( await validate_rtsps_camera_state(hass, camera_high_only, 0, entity_id) entity_id = validate_rtsp_camera_entity(hass, camera_high_only, 0) - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await enable_entity(hass, ufp.entry.entry_id, entity_id) await validate_rtsp_camera_state(hass, camera_high_only, 0, entity_id) # test camera 2 @@ -351,32 +223,32 @@ async def test_basic_setup( await validate_rtsps_camera_state(hass, camera_medium_only, 1, entity_id) entity_id = validate_rtsp_camera_entity(hass, camera_medium_only, 1) - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await enable_entity(hass, ufp.entry.entry_id, entity_id) await validate_rtsp_camera_state(hass, camera_medium_only, 1, entity_id) # test camera 3 - entity_id = validate_default_camera_entity(hass, camera_all_channels, 0) - await validate_rtsps_camera_state(hass, camera_all_channels, 0, entity_id) + entity_id = validate_default_camera_entity(hass, camera_all, 0) + await validate_rtsps_camera_state(hass, camera_all, 0, entity_id) - entity_id = validate_rtsp_camera_entity(hass, camera_all_channels, 0) - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) - await validate_rtsp_camera_state(hass, camera_all_channels, 0, entity_id) + entity_id = validate_rtsp_camera_entity(hass, camera_all, 0) + await enable_entity(hass, ufp.entry.entry_id, entity_id) + await validate_rtsp_camera_state(hass, camera_all, 0, entity_id) - entity_id = validate_rtsps_camera_entity(hass, camera_all_channels, 1) - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) - await validate_rtsps_camera_state(hass, camera_all_channels, 1, entity_id) + entity_id = validate_rtsps_camera_entity(hass, camera_all, 1) + await enable_entity(hass, ufp.entry.entry_id, entity_id) + await validate_rtsps_camera_state(hass, camera_all, 1, entity_id) - entity_id = validate_rtsp_camera_entity(hass, camera_all_channels, 1) - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) - await validate_rtsp_camera_state(hass, camera_all_channels, 1, entity_id) + entity_id = validate_rtsp_camera_entity(hass, camera_all, 1) + await enable_entity(hass, ufp.entry.entry_id, entity_id) + await validate_rtsp_camera_state(hass, camera_all, 1, entity_id) - entity_id = validate_rtsps_camera_entity(hass, camera_all_channels, 2) - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) - await validate_rtsps_camera_state(hass, camera_all_channels, 2, entity_id) + entity_id = validate_rtsps_camera_entity(hass, camera_all, 2) + await enable_entity(hass, ufp.entry.entry_id, entity_id) + await validate_rtsps_camera_state(hass, camera_all, 2, entity_id) - entity_id = validate_rtsp_camera_entity(hass, camera_all_channels, 2) - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) - await validate_rtsp_camera_state(hass, camera_all_channels, 2, entity_id) + entity_id = validate_rtsp_camera_entity(hass, camera_all, 2) + await enable_entity(hass, ufp.entry.entry_id, entity_id) + await validate_rtsp_camera_state(hass, camera_all, 2, entity_id) # test camera 4 entity_id = validate_default_camera_entity(hass, camera_no_channels, 0) @@ -385,197 +257,194 @@ async def test_basic_setup( ) # test camera 5 - entity_id = validate_default_camera_entity(hass, camera_package, 0) - await validate_rtsps_camera_state(hass, camera_package, 0, entity_id) + entity_id = validate_default_camera_entity(hass, doorbell, 0) + await validate_rtsps_camera_state(hass, doorbell, 0, entity_id) - entity_id = validate_rtsp_camera_entity(hass, camera_package, 0) - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) - await validate_rtsp_camera_state(hass, camera_package, 0, entity_id) + entity_id = validate_rtsp_camera_entity(hass, doorbell, 0) + await enable_entity(hass, ufp.entry.entry_id, entity_id) + await validate_rtsp_camera_state(hass, doorbell, 0, entity_id) - entity_id = validate_default_camera_entity(hass, camera_package, 3) - await validate_no_stream_camera_state( - hass, camera_package, 3, entity_id, features=0 - ) + entity_id = validate_default_camera_entity(hass, doorbell, 3) + await validate_no_stream_camera_state(hass, doorbell, 3, entity_id, features=0) async def test_missing_channels( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: ProtectCamera + hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera ): """Test setting up camera with no camera channels.""" - camera = mock_camera.copy() - camera.channels = [] + camera1 = camera.copy() + camera1.channels = [] - mock_entry.api.bootstrap.cameras = {camera.id: camera} + await init_entry(hass, ufp, [camera1]) - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - entity_registry = er.async_get(hass) - - assert len(hass.states.async_all()) == 0 - assert len(entity_registry.entities) == 0 + assert_entity_counts(hass, Platform.CAMERA, 0, 0) async def test_camera_image( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - camera: tuple[Camera, str], + hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera ): """Test retrieving camera image.""" - mock_entry.api.get_camera_snapshot = AsyncMock() + await init_entry(hass, ufp, [camera]) + assert_entity_counts(hass, Platform.CAMERA, 2, 1) - await async_get_image(hass, camera[1]) - mock_entry.api.get_camera_snapshot.assert_called_once() + ufp.api.get_camera_snapshot = AsyncMock() + + await async_get_image(hass, "camera.test_camera_high") + ufp.api.get_camera_snapshot.assert_called_once() async def test_package_camera_image( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - camera_package: tuple[Camera, str], + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: ProtectCamera ): """Test retrieving package camera image.""" - mock_entry.api.get_package_camera_snapshot = AsyncMock() + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.CAMERA, 3, 2) - await async_get_image(hass, camera_package[1]) - mock_entry.api.get_package_camera_snapshot.assert_called_once() + ufp.api.get_package_camera_snapshot = AsyncMock() + + await async_get_image(hass, "camera.test_camera_package_camera") + ufp.api.get_package_camera_snapshot.assert_called_once() async def test_camera_generic_update( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - camera: tuple[ProtectCamera, str], + hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera ): """Tests generic entity update service.""" + await init_entry(hass, ufp, [camera]) + assert_entity_counts(hass, Platform.CAMERA, 2, 1) + entity_id = "camera.test_camera_high" + assert await async_setup_component(hass, "homeassistant", {}) - state = hass.states.get(camera[1]) + state = hass.states.get(entity_id) assert state and state.state == "idle" - mock_entry.api.update = AsyncMock(return_value=None) + ufp.api.update = AsyncMock(return_value=None) await hass.services.async_call( "homeassistant", "update_entity", - {ATTR_ENTITY_ID: camera[1]}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) - state = hass.states.get(camera[1]) + state = hass.states.get(entity_id) assert state and state.state == "idle" async def test_camera_interval_update( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - camera: tuple[ProtectCamera, str], + hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera ): """Interval updates updates camera entity.""" - state = hass.states.get(camera[1]) + await init_entry(hass, ufp, [camera]) + assert_entity_counts(hass, Platform.CAMERA, 2, 1) + entity_id = "camera.test_camera_high" + + state = hass.states.get(entity_id) assert state and state.state == "idle" - new_bootstrap = copy(mock_entry.api.bootstrap) - new_camera = camera[0].copy() + new_camera = camera.copy() new_camera.is_recording = True - new_bootstrap.cameras = {new_camera.id: new_camera} - mock_entry.api.update = AsyncMock(return_value=new_bootstrap) - mock_entry.api.bootstrap = new_bootstrap + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.api.update = AsyncMock(return_value=ufp.api.bootstrap) await time_changed(hass, DEFAULT_SCAN_INTERVAL) - state = hass.states.get(camera[1]) + state = hass.states.get(entity_id) assert state and state.state == "recording" async def test_camera_bad_interval_update( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - camera: tuple[Camera, str], + hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera ): """Interval updates marks camera unavailable.""" - state = hass.states.get(camera[1]) + await init_entry(hass, ufp, [camera]) + assert_entity_counts(hass, Platform.CAMERA, 2, 1) + entity_id = "camera.test_camera_high" + + state = hass.states.get(entity_id) assert state and state.state == "idle" # update fails - mock_entry.api.update = AsyncMock(side_effect=NvrError) + ufp.api.update = AsyncMock(side_effect=NvrError) await time_changed(hass, DEFAULT_SCAN_INTERVAL) - state = hass.states.get(camera[1]) + state = hass.states.get(entity_id) assert state and state.state == "unavailable" # next update succeeds - mock_entry.api.update = AsyncMock(return_value=mock_entry.api.bootstrap) + ufp.api.update = AsyncMock(return_value=ufp.api.bootstrap) await time_changed(hass, DEFAULT_SCAN_INTERVAL) - state = hass.states.get(camera[1]) + state = hass.states.get(entity_id) assert state and state.state == "idle" async def test_camera_ws_update( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - camera: tuple[ProtectCamera, str], + hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera ): """WS update updates camera entity.""" - state = hass.states.get(camera[1]) + await init_entry(hass, ufp, [camera]) + assert_entity_counts(hass, Platform.CAMERA, 2, 1) + entity_id = "camera.test_camera_high" + + state = hass.states.get(entity_id) assert state and state.state == "idle" - new_bootstrap = copy(mock_entry.api.bootstrap) - new_camera = camera[0].copy() + new_camera = camera.copy() new_camera.is_recording = True - no_camera = camera[0].copy() + no_camera = camera.copy() no_camera.is_adopted = False - new_bootstrap.cameras = {new_camera.id: new_camera} - mock_entry.api.bootstrap = new_bootstrap - + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} mock_msg = Mock() mock_msg.changed_data = {} mock_msg.new_obj = new_camera - mock_entry.api.ws_subscription(mock_msg) + ufp.ws_msg(mock_msg) mock_msg = Mock() mock_msg.changed_data = {} mock_msg.new_obj = no_camera - mock_entry.api.ws_subscription(mock_msg) + ufp.ws_msg(mock_msg) await hass.async_block_till_done() - state = hass.states.get(camera[1]) + state = hass.states.get(entity_id) assert state and state.state == "recording" async def test_camera_ws_update_offline( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - camera: tuple[ProtectCamera, str], + hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera ): """WS updates marks camera unavailable.""" - state = hass.states.get(camera[1]) + await init_entry(hass, ufp, [camera]) + assert_entity_counts(hass, Platform.CAMERA, 2, 1) + entity_id = "camera.test_camera_high" + + state = hass.states.get(entity_id) assert state and state.state == "idle" # camera goes offline - new_bootstrap = copy(mock_entry.api.bootstrap) - new_camera = camera[0].copy() + new_camera = camera.copy() new_camera.state = StateType.DISCONNECTED mock_msg = Mock() mock_msg.changed_data = {} mock_msg.new_obj = new_camera - new_bootstrap.cameras = {new_camera.id: new_camera} - mock_entry.api.bootstrap = new_bootstrap - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.ws_msg(mock_msg) await hass.async_block_till_done() - state = hass.states.get(camera[1]) + state = hass.states.get(entity_id) assert state and state.state == "unavailable" # camera comes back online @@ -585,50 +454,53 @@ async def test_camera_ws_update_offline( mock_msg.changed_data = {} mock_msg.new_obj = new_camera - new_bootstrap.cameras = {new_camera.id: new_camera} - mock_entry.api.bootstrap = new_bootstrap - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.ws_msg(mock_msg) await hass.async_block_till_done() - state = hass.states.get(camera[1]) + state = hass.states.get(entity_id) assert state and state.state == "idle" async def test_camera_enable_motion( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - camera: tuple[ProtectCamera, str], + hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera ): """Tests generic entity update service.""" - camera[0].__fields__["set_motion_detection"] = Mock() - camera[0].set_motion_detection = AsyncMock() + await init_entry(hass, ufp, [camera]) + assert_entity_counts(hass, Platform.CAMERA, 2, 1) + entity_id = "camera.test_camera_high" + + camera.__fields__["set_motion_detection"] = Mock() + camera.set_motion_detection = AsyncMock() await hass.services.async_call( "camera", "enable_motion_detection", - {ATTR_ENTITY_ID: camera[1]}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) - camera[0].set_motion_detection.assert_called_once_with(True) + camera.set_motion_detection.assert_called_once_with(True) async def test_camera_disable_motion( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - camera: tuple[ProtectCamera, str], + hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera ): """Tests generic entity update service.""" - camera[0].__fields__["set_motion_detection"] = Mock() - camera[0].set_motion_detection = AsyncMock() + await init_entry(hass, ufp, [camera]) + assert_entity_counts(hass, Platform.CAMERA, 2, 1) + entity_id = "camera.test_camera_high" + + camera.__fields__["set_motion_detection"] = Mock() + camera.set_motion_detection = AsyncMock() await hass.services.async_call( "camera", "disable_motion_detection", - {ATTR_ENTITY_ID: camera[1]}, + {ATTR_ENTITY_ID: entity_id}, blocking=True, ) - camera[0].set_motion_detection.assert_called_once_with(False) + camera.set_motion_detection.assert_called_once_with(False) diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 75f08acb37c..3d561f2d781 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -6,7 +6,7 @@ import socket from unittest.mock import patch import pytest -from pyunifiprotect import NotAuthorized, NvrError +from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient from pyunifiprotect.data import NVR from homeassistant import config_entries @@ -61,7 +61,7 @@ UNIFI_DISCOVERY_DICT = asdict(UNIFI_DISCOVERY) UNIFI_DISCOVERY_DICT_PARTIAL = asdict(UNIFI_DISCOVERY_PARTIAL) -async def test_form(hass: HomeAssistant, mock_nvr: NVR) -> None: +async def test_form(hass: HomeAssistant, nvr: NVR) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -71,7 +71,7 @@ async def test_form(hass: HomeAssistant, mock_nvr: NVR) -> None: with patch( "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", - return_value=mock_nvr, + return_value=nvr, ), patch( "homeassistant.components.unifiprotect.async_setup_entry", return_value=True, @@ -99,7 +99,7 @@ async def test_form(hass: HomeAssistant, mock_nvr: NVR) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_version_too_old(hass: HomeAssistant, mock_old_nvr: NVR) -> None: +async def test_form_version_too_old(hass: HomeAssistant, old_nvr: NVR) -> None: """Test we handle the version being too old.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -107,7 +107,7 @@ async def test_form_version_too_old(hass: HomeAssistant, mock_old_nvr: NVR) -> N with patch( "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", - return_value=mock_old_nvr, + return_value=old_nvr, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -168,7 +168,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "cannot_connect"} -async def test_form_reauth_auth(hass: HomeAssistant, mock_nvr: NVR) -> None: +async def test_form_reauth_auth(hass: HomeAssistant, nvr: NVR) -> None: """Test we handle reauth auth.""" mock_config = MockConfigEntry( domain=DOMAIN, @@ -217,7 +217,7 @@ async def test_form_reauth_auth(hass: HomeAssistant, mock_nvr: NVR) -> None: with patch( "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", - return_value=mock_nvr, + return_value=nvr, ): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], @@ -231,7 +231,7 @@ async def test_form_reauth_auth(hass: HomeAssistant, mock_nvr: NVR) -> None: assert result3["reason"] == "reauth_successful" -async def test_form_options(hass: HomeAssistant, mock_client) -> None: +async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) -> None: """Test we handle options flows.""" mock_config = MockConfigEntry( domain=DOMAIN, @@ -251,7 +251,7 @@ async def test_form_options(hass: HomeAssistant, mock_client) -> None: with _patch_discovery(), patch( "homeassistant.components.unifiprotect.ProtectApiClient" ) as mock_api: - mock_api.return_value = mock_client + mock_api.return_value = ufp_client await hass.config_entries.async_setup(mock_config.entry_id) await hass.async_block_till_done() @@ -300,7 +300,7 @@ async def test_discovered_by_ssdp_or_dhcp( async def test_discovered_by_unifi_discovery_direct_connect( - hass: HomeAssistant, mock_nvr: NVR + hass: HomeAssistant, nvr: NVR ) -> None: """Test a discovery from unifi-discovery.""" @@ -324,7 +324,7 @@ async def test_discovered_by_unifi_discovery_direct_connect( with patch( "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", - return_value=mock_nvr, + return_value=nvr, ), patch( "homeassistant.components.unifiprotect.async_setup_entry", return_value=True, @@ -352,7 +352,7 @@ async def test_discovered_by_unifi_discovery_direct_connect( async def test_discovered_by_unifi_discovery_direct_connect_updated( - hass: HomeAssistant, mock_nvr: NVR + hass: HomeAssistant, ) -> None: """Test a discovery from unifi-discovery updates the direct connect host.""" mock_config = MockConfigEntry( @@ -384,7 +384,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated( async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_using_direct_connect( - hass: HomeAssistant, mock_nvr: NVR + hass: HomeAssistant, ) -> None: """Test a discovery from unifi-discovery updates the host but not direct connect if its not in use.""" mock_config = MockConfigEntry( @@ -419,7 +419,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin async def test_discovered_by_unifi_discovery_does_not_update_ip_when_console_is_still_online( - hass: HomeAssistant, mock_nvr: NVR + hass: HomeAssistant, ) -> None: """Test a discovery from unifi-discovery does not update the ip unless the console at the old ip is offline.""" mock_config = MockConfigEntry( @@ -454,7 +454,7 @@ async def test_discovered_by_unifi_discovery_does_not_update_ip_when_console_is_ async def test_discovered_host_not_updated_if_existing_is_a_hostname( - hass: HomeAssistant, mock_nvr: NVR + hass: HomeAssistant, ) -> None: """Test we only update the host if its an ip address from discovery.""" mock_config = MockConfigEntry( @@ -484,9 +484,7 @@ async def test_discovered_host_not_updated_if_existing_is_a_hostname( assert mock_config.data[CONF_HOST] == "a.hostname" -async def test_discovered_by_unifi_discovery( - hass: HomeAssistant, mock_nvr: NVR -) -> None: +async def test_discovered_by_unifi_discovery(hass: HomeAssistant, nvr: NVR) -> None: """Test a discovery from unifi-discovery.""" with _patch_discovery(): @@ -509,7 +507,7 @@ async def test_discovered_by_unifi_discovery( with patch( "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", - side_effect=[NotAuthorized, mock_nvr], + side_effect=[NotAuthorized, nvr], ), patch( "homeassistant.components.unifiprotect.async_setup_entry", return_value=True, @@ -537,7 +535,7 @@ async def test_discovered_by_unifi_discovery( async def test_discovered_by_unifi_discovery_partial( - hass: HomeAssistant, mock_nvr: NVR + hass: HomeAssistant, nvr: NVR ) -> None: """Test a discovery from unifi-discovery partial.""" @@ -561,7 +559,7 @@ async def test_discovered_by_unifi_discovery_partial( with patch( "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", - return_value=mock_nvr, + return_value=nvr, ), patch( "homeassistant.components.unifiprotect.async_setup_entry", return_value=True, @@ -589,7 +587,7 @@ async def test_discovered_by_unifi_discovery_partial( async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface( - hass: HomeAssistant, mock_nvr: NVR + hass: HomeAssistant, ) -> None: """Test a discovery from unifi-discovery from an alternate interface.""" mock_config = MockConfigEntry( @@ -619,7 +617,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface_ip_matches( - hass: HomeAssistant, mock_nvr: NVR + hass: HomeAssistant, ) -> None: """Test a discovery from unifi-discovery from an alternate interface when the ip matches.""" mock_config = MockConfigEntry( @@ -649,7 +647,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface_resolver( - hass: HomeAssistant, mock_nvr: NVR + hass: HomeAssistant, ) -> None: """Test a discovery from unifi-discovery from an alternate interface when direct connect domain resolves to host ip.""" mock_config = MockConfigEntry( @@ -687,7 +685,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface_resolver_fails( - hass: HomeAssistant, mock_nvr: NVR + hass: HomeAssistant, nvr: NVR ) -> None: """Test we can still configure if the resolver fails.""" mock_config = MockConfigEntry( @@ -730,7 +728,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa with patch( "homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_nvr", - return_value=mock_nvr, + return_value=nvr, ), patch( "homeassistant.components.unifiprotect.async_setup_entry", return_value=True, @@ -758,7 +756,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa async def test_discovered_by_unifi_discovery_direct_connect_on_different_interface_resolver_no_result( - hass: HomeAssistant, mock_nvr: NVR + hass: HomeAssistant, ) -> None: """Test a discovery from unifi-discovery from an alternate interface when direct connect domain resolve has no result.""" mock_config = MockConfigEntry( @@ -791,7 +789,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa assert result["reason"] == "already_configured" -async def test_discovery_can_be_ignored(hass: HomeAssistant, mock_nvr: NVR) -> None: +async def test_discovery_can_be_ignored(hass: HomeAssistant) -> None: """Test a discovery can be ignored.""" mock_config = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/unifiprotect/test_diagnostics.py b/tests/components/unifiprotect/test_diagnostics.py index 2e7f8c0e4b4..a0ed8f0d882 100644 --- a/tests/components/unifiprotect/test_diagnostics.py +++ b/tests/components/unifiprotect/test_diagnostics.py @@ -4,53 +4,44 @@ from pyunifiprotect.data import NVR, Light from homeassistant.core import HomeAssistant -from .conftest import MockEntityFixture, regenerate_device_ids +from .utils import MockUFPFixture, init_entry from tests.components.diagnostics import get_diagnostics_for_config_entry async def test_diagnostics( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light, hass_client + hass: HomeAssistant, ufp: MockUFPFixture, light: Light, hass_client ): """Test generating diagnostics for a config entry.""" - light1 = mock_light.copy() - light1._api = mock_entry.api - light1.name = "Test Light 1" - regenerate_device_ids(light1) + await init_entry(hass, ufp, [light]) - mock_entry.api.bootstrap.lights = { - light1.id: light1, - } - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() + diag = await get_diagnostics_for_config_entry(hass, hass_client, ufp.entry) - diag = await get_diagnostics_for_config_entry(hass, hass_client, mock_entry.entry) - - nvr_obj: NVR = mock_entry.api.bootstrap.nvr + nvr: NVR = ufp.api.bootstrap.nvr # validate some of the data assert "nvr" in diag and isinstance(diag["nvr"], dict) - nvr = diag["nvr"] + nvr_dict = diag["nvr"] # should have been anonymized - assert nvr["id"] != nvr_obj.id - assert nvr["mac"] != nvr_obj.mac - assert nvr["host"] != str(nvr_obj.host) + assert nvr_dict["id"] != nvr.id + assert nvr_dict["mac"] != nvr.mac + assert nvr_dict["host"] != str(nvr.host) # should have been kept - assert nvr["firmwareVersion"] == nvr_obj.firmware_version - assert nvr["version"] == str(nvr_obj.version) - assert nvr["type"] == nvr_obj.type + assert nvr_dict["firmwareVersion"] == nvr.firmware_version + assert nvr_dict["version"] == str(nvr.version) + assert nvr_dict["type"] == nvr.type assert ( "lights" in diag and isinstance(diag["lights"], list) and len(diag["lights"]) == 1 ) - light = diag["lights"][0] + light_dict = diag["lights"][0] # should have been anonymized - assert light["id"] != light1.id - assert light["name"] != light1.mac - assert light["mac"] != light1.mac - assert light["host"] != str(light1.host) + assert light_dict["id"] != light.id + assert light_dict["name"] != light.mac + assert light_dict["mac"] != light.mac + assert light_dict["host"] != str(light.host) # should have been kept - assert light["firmwareVersion"] == light1.firmware_version - assert light["type"] == light1.type + assert light_dict["firmwareVersion"] == light.firmware_version + assert light_dict["type"] == light.type diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 5c06eedc4c9..c0ad30ad115 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable from unittest.mock import AsyncMock, patch import aiohttp -from pyunifiprotect import NotAuthorized, NvrError +from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient from pyunifiprotect.data import NVR, Bootstrap, Light from homeassistant.components.unifiprotect.const import CONF_DISABLE_RTSP, DOMAIN @@ -16,7 +16,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from . import _patch_discovery -from .conftest import MockEntityFixture, regenerate_device_ids +from .utils import MockUFPFixture, init_entry from tests.common import MockConfigEntry @@ -37,37 +37,36 @@ async def remove_device( return response["success"] -async def test_setup(hass: HomeAssistant, mock_entry: MockEntityFixture): +async def test_setup(hass: HomeAssistant, ufp: MockUFPFixture): """Test working setup of unifiprotect entry.""" - await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() - assert mock_entry.entry.state == ConfigEntryState.LOADED - assert mock_entry.api.update.called - assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac + assert ufp.entry.state == ConfigEntryState.LOADED + assert ufp.api.update.called + assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac async def test_setup_multiple( hass: HomeAssistant, - mock_entry: MockEntityFixture, - mock_client, - mock_bootstrap: Bootstrap, + ufp: MockUFPFixture, + bootstrap: Bootstrap, ): """Test working setup of unifiprotect entry.""" - await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() - assert mock_entry.entry.state == ConfigEntryState.LOADED - assert mock_entry.api.update.called - assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac + assert ufp.entry.state == ConfigEntryState.LOADED + assert ufp.api.update.called + assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac - nvr = mock_bootstrap.nvr - nvr._api = mock_client + nvr = bootstrap.nvr + nvr._api = ufp.api nvr.mac = "A1E00C826983" nvr.id - mock_client.get_nvr = AsyncMock(return_value=nvr) + ufp.api.get_nvr = AsyncMock(return_value=nvr) with patch("homeassistant.components.unifiprotect.ProtectApiClient") as mock_api: mock_config = MockConfigEntry( @@ -84,148 +83,134 @@ async def test_setup_multiple( ) mock_config.add_to_hass(hass) - mock_api.return_value = mock_client + mock_api.return_value = ufp.api await hass.config_entries.async_setup(mock_config.entry_id) await hass.async_block_till_done() assert mock_config.state == ConfigEntryState.LOADED - assert mock_client.update.called - assert mock_config.unique_id == mock_client.bootstrap.nvr.mac + assert ufp.api.update.called + assert mock_config.unique_id == ufp.api.bootstrap.nvr.mac -async def test_reload(hass: HomeAssistant, mock_entry: MockEntityFixture): +async def test_reload(hass: HomeAssistant, ufp: MockUFPFixture): """Test updating entry reload entry.""" - await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() - assert mock_entry.entry.state == ConfigEntryState.LOADED + assert ufp.entry.state == ConfigEntryState.LOADED - options = dict(mock_entry.entry.options) + options = dict(ufp.entry.options) options[CONF_DISABLE_RTSP] = True - hass.config_entries.async_update_entry(mock_entry.entry, options=options) + hass.config_entries.async_update_entry(ufp.entry, options=options) await hass.async_block_till_done() - assert mock_entry.entry.state == ConfigEntryState.LOADED - assert mock_entry.api.async_disconnect_ws.called + assert ufp.entry.state == ConfigEntryState.LOADED + assert ufp.api.async_disconnect_ws.called -async def test_unload(hass: HomeAssistant, mock_entry: MockEntityFixture): +async def test_unload(hass: HomeAssistant, ufp: MockUFPFixture): """Test unloading of unifiprotect entry.""" - await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() - assert mock_entry.entry.state == ConfigEntryState.LOADED + assert ufp.entry.state == ConfigEntryState.LOADED - await hass.config_entries.async_unload(mock_entry.entry.entry_id) - assert mock_entry.entry.state == ConfigEntryState.NOT_LOADED - assert mock_entry.api.async_disconnect_ws.called + await hass.config_entries.async_unload(ufp.entry.entry_id) + assert ufp.entry.state == ConfigEntryState.NOT_LOADED + assert ufp.api.async_disconnect_ws.called -async def test_setup_too_old( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_old_nvr: NVR -): +async def test_setup_too_old(hass: HomeAssistant, ufp: MockUFPFixture, old_nvr: NVR): """Test setup of unifiprotect entry with too old of version of UniFi Protect.""" - mock_entry.api.get_nvr.return_value = mock_old_nvr + ufp.api.get_nvr.return_value = old_nvr - await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() - assert mock_entry.entry.state == ConfigEntryState.SETUP_ERROR - assert not mock_entry.api.update.called + assert ufp.entry.state == ConfigEntryState.SETUP_ERROR + assert not ufp.api.update.called -async def test_setup_failed_update(hass: HomeAssistant, mock_entry: MockEntityFixture): +async def test_setup_failed_update(hass: HomeAssistant, ufp: MockUFPFixture): """Test setup of unifiprotect entry with failed update.""" - mock_entry.api.update = AsyncMock(side_effect=NvrError) + ufp.api.update = AsyncMock(side_effect=NvrError) - await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() - assert mock_entry.entry.state == ConfigEntryState.SETUP_RETRY - assert mock_entry.api.update.called + assert ufp.entry.state == ConfigEntryState.SETUP_RETRY + assert ufp.api.update.called -async def test_setup_failed_update_reauth( - hass: HomeAssistant, mock_entry: MockEntityFixture -): +async def test_setup_failed_update_reauth(hass: HomeAssistant, ufp: MockUFPFixture): """Test setup of unifiprotect entry with update that gives unauthroized error.""" - mock_entry.api.update = AsyncMock(side_effect=NotAuthorized) + ufp.api.update = AsyncMock(side_effect=NotAuthorized) - await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() - assert mock_entry.entry.state == ConfigEntryState.SETUP_RETRY - assert mock_entry.api.update.called + assert ufp.entry.state == ConfigEntryState.SETUP_RETRY + assert ufp.api.update.called -async def test_setup_failed_error(hass: HomeAssistant, mock_entry: MockEntityFixture): +async def test_setup_failed_error(hass: HomeAssistant, ufp: MockUFPFixture): """Test setup of unifiprotect entry with generic error.""" - mock_entry.api.get_nvr = AsyncMock(side_effect=NvrError) + ufp.api.get_nvr = AsyncMock(side_effect=NvrError) - await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() - assert mock_entry.entry.state == ConfigEntryState.SETUP_RETRY - assert not mock_entry.api.update.called + assert ufp.entry.state == ConfigEntryState.SETUP_RETRY + assert not ufp.api.update.called -async def test_setup_failed_auth(hass: HomeAssistant, mock_entry: MockEntityFixture): +async def test_setup_failed_auth(hass: HomeAssistant, ufp: MockUFPFixture): """Test setup of unifiprotect entry with unauthorized error.""" - mock_entry.api.get_nvr = AsyncMock(side_effect=NotAuthorized) + ufp.api.get_nvr = AsyncMock(side_effect=NotAuthorized) - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - assert mock_entry.entry.state == ConfigEntryState.SETUP_ERROR - assert not mock_entry.api.update.called + await hass.config_entries.async_setup(ufp.entry.entry_id) + assert ufp.entry.state == ConfigEntryState.SETUP_ERROR + assert not ufp.api.update.called async def test_setup_starts_discovery( - hass: HomeAssistant, mock_ufp_config_entry: ConfigEntry, mock_client + hass: HomeAssistant, ufp_config_entry: ConfigEntry, ufp_client: ProtectApiClient ): """Test setting up will start discovery.""" with _patch_discovery(), patch( "homeassistant.components.unifiprotect.ProtectApiClient" ) as mock_api: - mock_ufp_config_entry.add_to_hass(hass) - mock_api.return_value = mock_client - mock_entry = MockEntityFixture(mock_ufp_config_entry, mock_client) + ufp_config_entry.add_to_hass(hass) + mock_api.return_value = ufp_client + ufp = MockUFPFixture(ufp_config_entry, ufp_client) - await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() - assert mock_entry.entry.state == ConfigEntryState.LOADED + assert ufp.entry.state == ConfigEntryState.LOADED await hass.async_block_till_done() assert len(hass.config_entries.flow.async_progress_by_handler(DOMAIN)) == 1 async def test_device_remove_devices( hass: HomeAssistant, - mock_entry: MockEntityFixture, - mock_light: Light, + ufp: MockUFPFixture, + light: Light, hass_ws_client: Callable[ [HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse] ], ) -> None: """Test we can only remove a device that no longer exists.""" + + await init_entry(hass, ufp, [light]) assert await async_setup_component(hass, "config", {}) - - light1 = mock_light.copy() - light1._api = mock_entry.api - light1.name = "Test Light 1" - regenerate_device_ids(light1) - - mock_entry.api.bootstrap.lights = { - light1.id: light1, - } - - mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) - light_entity_id = "light.test_light_1" - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - entry_id = mock_entry.entry.entry_id + entity_id = "light.test_light" + entry_id = ufp.entry.entry_id registry: er.EntityRegistry = er.async_get(hass) - entity = registry.entities[light_entity_id] + entity = registry.async_get(entity_id) + assert entity is not None device_registry = dr.async_get(hass) live_device_entry = device_registry.async_get(entity.device_id) @@ -246,7 +231,7 @@ async def test_device_remove_devices( async def test_device_remove_devices_nvr( hass: HomeAssistant, - mock_entry: MockEntityFixture, + ufp: MockUFPFixture, hass_ws_client: Callable[ [HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse] ], @@ -254,10 +239,10 @@ async def test_device_remove_devices_nvr( """Test we can only remove a NVR device that no longer exists.""" assert await async_setup_component(hass, "config", {}) - mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) - await hass.config_entries.async_setup(mock_entry.entry.entry_id) + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() - entry_id = mock_entry.entry.entry_id + entry_id = ufp.entry.entry_id device_registry = dr.async_get(hass) diff --git a/tests/components/unifiprotect/test_light.py b/tests/components/unifiprotect/test_light.py index 3bcca436911..3c575de8d00 100644 --- a/tests/components/unifiprotect/test_light.py +++ b/tests/components/unifiprotect/test_light.py @@ -2,11 +2,10 @@ # pylint: disable=protected-access from __future__ import annotations -from copy import copy from unittest.mock import AsyncMock, Mock -import pytest from pyunifiprotect.data import Light +from pyunifiprotect.data.types import LEDLevel from homeassistant.components.light import ATTR_BRIGHTNESS from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION @@ -20,53 +19,19 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import MockEntityFixture, assert_entity_counts, regenerate_device_ids - - -@pytest.fixture(name="light") -async def light_fixture( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light -): - """Fixture for a single light for testing the light platform.""" - - # disable pydantic validation so mocking can happen - Light.__config__.validate_assignment = False - - light_obj = mock_light.copy() - light_obj._api = mock_entry.api - light_obj.name = "Test Light" - light_obj.is_light_on = False - regenerate_device_ids(light_obj) - - no_light_obj = mock_light.copy() - no_light_obj._api = mock_entry.api - no_light_obj.name = "Unadopted Light" - no_light_obj.is_adopted = False - regenerate_device_ids(no_light_obj) - - mock_entry.api.bootstrap.lights = { - light_obj.id: light_obj, - no_light_obj.id: no_light_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.LIGHT, 1, 1) - - yield (light_obj, "light.test_light") - - Light.__config__.validate_assignment = True +from .utils import MockUFPFixture, assert_entity_counts, init_entry async def test_light_setup( - hass: HomeAssistant, - light: tuple[Light, str], + hass: HomeAssistant, ufp: MockUFPFixture, light: Light, unadopted_light: Light ): """Test light entity setup.""" - unique_id = light[0].mac - entity_id = light[1] + await init_entry(hass, ufp, [light, unadopted_light]) + assert_entity_counts(hass, Platform.LIGHT, 1, 1) + + unique_id = light.mac + entity_id = "light.test_light" entity_registry = er.async_get(hass) entity = entity_registry.async_get(entity_id) @@ -80,41 +45,42 @@ async def test_light_setup( async def test_light_update( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - light: tuple[Light, str], + hass: HomeAssistant, ufp: MockUFPFixture, light: Light, unadopted_light: Light ): """Test light entity update.""" - new_bootstrap = copy(mock_entry.api.bootstrap) - new_light = light[0].copy() + await init_entry(hass, ufp, [light, unadopted_light]) + assert_entity_counts(hass, Platform.LIGHT, 1, 1) + + new_light = light.copy() new_light.is_light_on = True - new_light.light_device_settings.led_level = 3 + new_light.light_device_settings.led_level = LEDLevel(3) mock_msg = Mock() mock_msg.changed_data = {} mock_msg.new_obj = new_light - new_bootstrap.lights = {new_light.id: new_light} - mock_entry.api.bootstrap = new_bootstrap - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.lights = {new_light.id: new_light} + ufp.ws_msg(mock_msg) await hass.async_block_till_done() - state = hass.states.get(light[1]) + state = hass.states.get("light.test_light") assert state assert state.state == STATE_ON assert state.attributes[ATTR_BRIGHTNESS] == 128 async def test_light_turn_on( - hass: HomeAssistant, - light: tuple[Light, str], + hass: HomeAssistant, ufp: MockUFPFixture, light: Light, unadopted_light: Light ): """Test light entity turn off.""" - entity_id = light[1] - light[0].__fields__["set_light"] = Mock() - light[0].set_light = AsyncMock() + await init_entry(hass, ufp, [light, unadopted_light]) + assert_entity_counts(hass, Platform.LIGHT, 1, 1) + + entity_id = "light.test_light" + light.__fields__["set_light"] = Mock() + light.set_light = AsyncMock() await hass.services.async_call( "light", @@ -123,18 +89,20 @@ async def test_light_turn_on( blocking=True, ) - light[0].set_light.assert_called_once_with(True, 3) + light.set_light.assert_called_once_with(True, 3) async def test_light_turn_off( - hass: HomeAssistant, - light: tuple[Light, str], + hass: HomeAssistant, ufp: MockUFPFixture, light: Light, unadopted_light: Light ): """Test light entity turn on.""" - entity_id = light[1] - light[0].__fields__["set_light"] = Mock() - light[0].set_light = AsyncMock() + await init_entry(hass, ufp, [light, unadopted_light]) + assert_entity_counts(hass, Platform.LIGHT, 1, 1) + + entity_id = "light.test_light" + light.__fields__["set_light"] = Mock() + light.set_light = AsyncMock() await hass.services.async_call( "light", @@ -143,4 +111,4 @@ async def test_light_turn_off( blocking=True, ) - light[0].set_light.assert_called_once_with(False) + light.set_light.assert_called_once_with(False) diff --git a/tests/components/unifiprotect/test_lock.py b/tests/components/unifiprotect/test_lock.py index 3ebfd2de22f..21b3c77deb5 100644 --- a/tests/components/unifiprotect/test_lock.py +++ b/tests/components/unifiprotect/test_lock.py @@ -2,10 +2,8 @@ # pylint: disable=protected-access from __future__ import annotations -from copy import copy from unittest.mock import AsyncMock, Mock -import pytest from pyunifiprotect.data import Doorlock, LockStatusType from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION @@ -23,53 +21,22 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import MockEntityFixture, assert_entity_counts, regenerate_device_ids - - -@pytest.fixture(name="doorlock") -async def doorlock_fixture( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_doorlock: Doorlock -): - """Fixture for a single doorlock for testing the lock platform.""" - - # disable pydantic validation so mocking can happen - Doorlock.__config__.validate_assignment = False - - lock_obj = mock_doorlock.copy() - lock_obj._api = mock_entry.api - lock_obj.name = "Test Lock" - lock_obj.lock_status = LockStatusType.OPEN - regenerate_device_ids(lock_obj) - - no_lock_obj = mock_doorlock.copy() - no_lock_obj._api = mock_entry.api - no_lock_obj.name = "Unadopted Lock" - no_lock_obj.is_adopted = False - regenerate_device_ids(no_lock_obj) - - mock_entry.api.bootstrap.doorlocks = { - lock_obj.id: lock_obj, - no_lock_obj.id: no_lock_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.LOCK, 1, 1) - - yield (lock_obj, "lock.test_lock_lock") - - Doorlock.__config__.validate_assignment = True +from .utils import MockUFPFixture, assert_entity_counts, init_entry async def test_lock_setup( hass: HomeAssistant, - doorlock: tuple[Doorlock, str], + ufp: MockUFPFixture, + doorlock: Doorlock, + unadopted_doorlock: Doorlock, ): """Test lock entity setup.""" - unique_id = f"{doorlock[0].mac}_lock" - entity_id = doorlock[1] + await init_entry(hass, ufp, [doorlock, unadopted_doorlock]) + assert_entity_counts(hass, Platform.LOCK, 1, 1) + + unique_id = f"{doorlock.mac}_lock" + entity_id = "lock.test_lock_lock" entity_registry = er.async_get(hass) entity = entity_registry.async_get(entity_id) @@ -84,166 +51,183 @@ async def test_lock_setup( async def test_lock_locked( hass: HomeAssistant, - mock_entry: MockEntityFixture, - doorlock: tuple[Doorlock, str], + ufp: MockUFPFixture, + doorlock: Doorlock, + unadopted_doorlock: Doorlock, ): """Test lock entity locked.""" - new_bootstrap = copy(mock_entry.api.bootstrap) - new_lock = doorlock[0].copy() + await init_entry(hass, ufp, [doorlock, unadopted_doorlock]) + assert_entity_counts(hass, Platform.LOCK, 1, 1) + + new_lock = doorlock.copy() new_lock.lock_status = LockStatusType.CLOSED mock_msg = Mock() mock_msg.changed_data = {} mock_msg.new_obj = new_lock - new_bootstrap.doorlocks = {new_lock.id: new_lock} - mock_entry.api.bootstrap = new_bootstrap - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.doorlocks = {new_lock.id: new_lock} + ufp.ws_msg(mock_msg) await hass.async_block_till_done() - state = hass.states.get(doorlock[1]) + state = hass.states.get("lock.test_lock_lock") assert state assert state.state == STATE_LOCKED async def test_lock_unlocking( hass: HomeAssistant, - mock_entry: MockEntityFixture, - doorlock: tuple[Doorlock, str], + ufp: MockUFPFixture, + doorlock: Doorlock, + unadopted_doorlock: Doorlock, ): """Test lock entity unlocking.""" - new_bootstrap = copy(mock_entry.api.bootstrap) - new_lock = doorlock[0].copy() + await init_entry(hass, ufp, [doorlock, unadopted_doorlock]) + assert_entity_counts(hass, Platform.LOCK, 1, 1) + + new_lock = doorlock.copy() new_lock.lock_status = LockStatusType.OPENING mock_msg = Mock() mock_msg.changed_data = {} mock_msg.new_obj = new_lock - new_bootstrap.doorlocks = {new_lock.id: new_lock} - mock_entry.api.bootstrap = new_bootstrap - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.doorlocks = {new_lock.id: new_lock} + ufp.ws_msg(mock_msg) await hass.async_block_till_done() - state = hass.states.get(doorlock[1]) + state = hass.states.get("lock.test_lock_lock") assert state assert state.state == STATE_UNLOCKING async def test_lock_locking( hass: HomeAssistant, - mock_entry: MockEntityFixture, - doorlock: tuple[Doorlock, str], + ufp: MockUFPFixture, + doorlock: Doorlock, + unadopted_doorlock: Doorlock, ): """Test lock entity locking.""" - new_bootstrap = copy(mock_entry.api.bootstrap) - new_lock = doorlock[0].copy() + await init_entry(hass, ufp, [doorlock, unadopted_doorlock]) + assert_entity_counts(hass, Platform.LOCK, 1, 1) + + new_lock = doorlock.copy() new_lock.lock_status = LockStatusType.CLOSING mock_msg = Mock() mock_msg.changed_data = {} mock_msg.new_obj = new_lock - new_bootstrap.doorlocks = {new_lock.id: new_lock} - mock_entry.api.bootstrap = new_bootstrap - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.doorlocks = {new_lock.id: new_lock} + ufp.ws_msg(mock_msg) await hass.async_block_till_done() - state = hass.states.get(doorlock[1]) + state = hass.states.get("lock.test_lock_lock") assert state assert state.state == STATE_LOCKING async def test_lock_jammed( hass: HomeAssistant, - mock_entry: MockEntityFixture, - doorlock: tuple[Doorlock, str], + ufp: MockUFPFixture, + doorlock: Doorlock, + unadopted_doorlock: Doorlock, ): """Test lock entity jammed.""" - new_bootstrap = copy(mock_entry.api.bootstrap) - new_lock = doorlock[0].copy() + await init_entry(hass, ufp, [doorlock, unadopted_doorlock]) + assert_entity_counts(hass, Platform.LOCK, 1, 1) + + new_lock = doorlock.copy() new_lock.lock_status = LockStatusType.JAMMED_WHILE_CLOSING mock_msg = Mock() mock_msg.changed_data = {} mock_msg.new_obj = new_lock - new_bootstrap.doorlocks = {new_lock.id: new_lock} - mock_entry.api.bootstrap = new_bootstrap - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.doorlocks = {new_lock.id: new_lock} + ufp.ws_msg(mock_msg) await hass.async_block_till_done() - state = hass.states.get(doorlock[1]) + state = hass.states.get("lock.test_lock_lock") assert state assert state.state == STATE_JAMMED async def test_lock_unavailable( hass: HomeAssistant, - mock_entry: MockEntityFixture, - doorlock: tuple[Doorlock, str], + ufp: MockUFPFixture, + doorlock: Doorlock, + unadopted_doorlock: Doorlock, ): """Test lock entity unavailable.""" - new_bootstrap = copy(mock_entry.api.bootstrap) - new_lock = doorlock[0].copy() + await init_entry(hass, ufp, [doorlock, unadopted_doorlock]) + assert_entity_counts(hass, Platform.LOCK, 1, 1) + + new_lock = doorlock.copy() new_lock.lock_status = LockStatusType.NOT_CALIBRATED mock_msg = Mock() mock_msg.changed_data = {} mock_msg.new_obj = new_lock - new_bootstrap.doorlocks = {new_lock.id: new_lock} - mock_entry.api.bootstrap = new_bootstrap - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.doorlocks = {new_lock.id: new_lock} + ufp.ws_msg(mock_msg) await hass.async_block_till_done() - state = hass.states.get(doorlock[1]) + state = hass.states.get("lock.test_lock_lock") assert state assert state.state == STATE_UNAVAILABLE async def test_lock_do_lock( hass: HomeAssistant, - doorlock: tuple[Doorlock, str], + ufp: MockUFPFixture, + doorlock: Doorlock, + unadopted_doorlock: Doorlock, ): """Test lock entity lock service.""" - doorlock[0].__fields__["close_lock"] = Mock() - doorlock[0].close_lock = AsyncMock() + await init_entry(hass, ufp, [doorlock, unadopted_doorlock]) + assert_entity_counts(hass, Platform.LOCK, 1, 1) + + doorlock.__fields__["close_lock"] = Mock() + doorlock.close_lock = AsyncMock() await hass.services.async_call( "lock", "lock", - {ATTR_ENTITY_ID: doorlock[1]}, + {ATTR_ENTITY_ID: "lock.test_lock_lock"}, blocking=True, ) - doorlock[0].close_lock.assert_called_once() + doorlock.close_lock.assert_called_once() async def test_lock_do_unlock( hass: HomeAssistant, - mock_entry: MockEntityFixture, - doorlock: tuple[Doorlock, str], + ufp: MockUFPFixture, + doorlock: Doorlock, + unadopted_doorlock: Doorlock, ): """Test lock entity unlock service.""" - new_bootstrap = copy(mock_entry.api.bootstrap) - new_lock = doorlock[0].copy() + await init_entry(hass, ufp, [doorlock, unadopted_doorlock]) + assert_entity_counts(hass, Platform.LOCK, 1, 1) + + new_lock = doorlock.copy() new_lock.lock_status = LockStatusType.CLOSED mock_msg = Mock() mock_msg.changed_data = {} mock_msg.new_obj = new_lock - new_bootstrap.doorlocks = {new_lock.id: new_lock} - mock_entry.api.bootstrap = new_bootstrap - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.doorlocks = {new_lock.id: new_lock} + ufp.ws_msg(mock_msg) await hass.async_block_till_done() new_lock.__fields__["open_lock"] = Mock() @@ -252,7 +236,7 @@ async def test_lock_do_unlock( await hass.services.async_call( "lock", "unlock", - {ATTR_ENTITY_ID: doorlock[1]}, + {ATTR_ENTITY_ID: "lock.test_lock_lock"}, blocking=True, ) diff --git a/tests/components/unifiprotect/test_media_player.py b/tests/components/unifiprotect/test_media_player.py index c18a407eadb..678fa0c9be4 100644 --- a/tests/components/unifiprotect/test_media_player.py +++ b/tests/components/unifiprotect/test_media_player.py @@ -2,7 +2,6 @@ # pylint: disable=protected-access from __future__ import annotations -from copy import copy from unittest.mock import AsyncMock, Mock, patch import pytest @@ -26,66 +25,29 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from .conftest import MockEntityFixture, assert_entity_counts, regenerate_device_ids - - -@pytest.fixture(name="camera") -async def camera_fixture( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera -): - """Fixture for a single camera for testing the media_player platform.""" - - # disable pydantic validation so mocking can happen - Camera.__config__.validate_assignment = False - - camera_obj = mock_camera.copy() - camera_obj._api = mock_entry.api - camera_obj.channels[0]._api = mock_entry.api - camera_obj.channels[1]._api = mock_entry.api - camera_obj.channels[2]._api = mock_entry.api - camera_obj.name = "Test Camera" - camera_obj.feature_flags.has_speaker = True - regenerate_device_ids(camera_obj) - - no_camera_obj = mock_camera.copy() - no_camera_obj._api = mock_entry.api - no_camera_obj.channels[0]._api = mock_entry.api - no_camera_obj.channels[1]._api = mock_entry.api - no_camera_obj.channels[2]._api = mock_entry.api - no_camera_obj.name = "Unadopted Camera" - no_camera_obj.is_adopted = False - regenerate_device_ids(no_camera_obj) - - mock_entry.api.bootstrap.cameras = { - camera_obj.id: camera_obj, - no_camera_obj.id: no_camera_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1) - - yield (camera_obj, "media_player.test_camera_speaker") - - Camera.__config__.validate_assignment = True +from .utils import MockUFPFixture, assert_entity_counts, init_entry async def test_media_player_setup( hass: HomeAssistant, - camera: tuple[Camera, str], + ufp: MockUFPFixture, + doorbell: Camera, + unadopted_camera: Camera, ): """Test media_player entity setup.""" - unique_id = f"{camera[0].mac}_speaker" - entity_id = camera[1] + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1) + + unique_id = f"{doorbell.mac}_speaker" + entity_id = "media_player.test_camera_speaker" entity_registry = er.async_get(hass) entity = entity_registry.async_get(entity_id) assert entity assert entity.unique_id == unique_id - expected_volume = float(camera[0].speaker_settings.volume / 100) + expected_volume = float(doorbell.speaker_settings.volume / 100) state = hass.states.get(entity_id) assert state @@ -98,13 +60,16 @@ async def test_media_player_setup( async def test_media_player_update( hass: HomeAssistant, - mock_entry: MockEntityFixture, - camera: tuple[Camera, str], + ufp: MockUFPFixture, + doorbell: Camera, + unadopted_camera: Camera, ): """Test media_player entity update.""" - new_bootstrap = copy(mock_entry.api.bootstrap) - new_camera = camera[0].copy() + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1) + + new_camera = doorbell.copy() new_camera.talkback_stream = Mock() new_camera.talkback_stream.is_running = True @@ -112,44 +77,51 @@ async def test_media_player_update( mock_msg.changed_data = {} mock_msg.new_obj = new_camera - new_bootstrap.cameras = {new_camera.id: new_camera} - mock_entry.api.bootstrap = new_bootstrap - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.ws_msg(mock_msg) await hass.async_block_till_done() - state = hass.states.get(camera[1]) + state = hass.states.get("media_player.test_camera_speaker") assert state assert state.state == STATE_PLAYING async def test_media_player_set_volume( hass: HomeAssistant, - camera: tuple[Camera, str], + ufp: MockUFPFixture, + doorbell: Camera, + unadopted_camera: Camera, ): """Test media_player entity test set_volume_level.""" - camera[0].__fields__["set_speaker_volume"] = Mock() - camera[0].set_speaker_volume = AsyncMock() + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1) + + doorbell.__fields__["set_speaker_volume"] = Mock() + doorbell.set_speaker_volume = AsyncMock() await hass.services.async_call( "media_player", "volume_set", - {ATTR_ENTITY_ID: camera[1], "volume_level": 0.5}, + {ATTR_ENTITY_ID: "media_player.test_camera_speaker", "volume_level": 0.5}, blocking=True, ) - camera[0].set_speaker_volume.assert_called_once_with(50) + doorbell.set_speaker_volume.assert_called_once_with(50) async def test_media_player_stop( hass: HomeAssistant, - mock_entry: MockEntityFixture, - camera: tuple[Camera, str], + ufp: MockUFPFixture, + doorbell: Camera, + unadopted_camera: Camera, ): """Test media_player entity test media_stop.""" - new_bootstrap = copy(mock_entry.api.bootstrap) - new_camera = camera[0].copy() + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1) + + new_camera = doorbell.copy() new_camera.talkback_stream = AsyncMock() new_camera.talkback_stream.is_running = True @@ -157,15 +129,14 @@ async def test_media_player_stop( mock_msg.changed_data = {} mock_msg.new_obj = new_camera - new_bootstrap.cameras = {new_camera.id: new_camera} - mock_entry.api.bootstrap = new_bootstrap - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.ws_msg(mock_msg) await hass.async_block_till_done() await hass.services.async_call( "media_player", "media_stop", - {ATTR_ENTITY_ID: camera[1]}, + {ATTR_ENTITY_ID: "media_player.test_camera_speaker"}, blocking=True, ) @@ -174,44 +145,56 @@ async def test_media_player_stop( async def test_media_player_play( hass: HomeAssistant, - camera: tuple[Camera, str], + ufp: MockUFPFixture, + doorbell: Camera, + unadopted_camera: Camera, ): """Test media_player entity test play_media.""" - camera[0].__fields__["stop_audio"] = Mock() - camera[0].__fields__["play_audio"] = Mock() - camera[0].__fields__["wait_until_audio_completes"] = Mock() - camera[0].stop_audio = AsyncMock() - camera[0].play_audio = AsyncMock() - camera[0].wait_until_audio_completes = AsyncMock() + + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1) + + doorbell.__fields__["stop_audio"] = Mock() + doorbell.__fields__["play_audio"] = Mock() + doorbell.__fields__["wait_until_audio_completes"] = Mock() + doorbell.stop_audio = AsyncMock() + doorbell.play_audio = AsyncMock() + doorbell.wait_until_audio_completes = AsyncMock() await hass.services.async_call( "media_player", "play_media", { - ATTR_ENTITY_ID: camera[1], + ATTR_ENTITY_ID: "media_player.test_camera_speaker", "media_content_id": "http://example.com/test.mp3", "media_content_type": "music", }, blocking=True, ) - camera[0].play_audio.assert_called_once_with( + doorbell.play_audio.assert_called_once_with( "http://example.com/test.mp3", blocking=False ) - camera[0].wait_until_audio_completes.assert_called_once() + doorbell.wait_until_audio_completes.assert_called_once() async def test_media_player_play_media_source( hass: HomeAssistant, - camera: tuple[Camera, str], + ufp: MockUFPFixture, + doorbell: Camera, + unadopted_camera: Camera, ): """Test media_player entity test play_media.""" - camera[0].__fields__["stop_audio"] = Mock() - camera[0].__fields__["play_audio"] = Mock() - camera[0].__fields__["wait_until_audio_completes"] = Mock() - camera[0].stop_audio = AsyncMock() - camera[0].play_audio = AsyncMock() - camera[0].wait_until_audio_completes = AsyncMock() + + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1) + + doorbell.__fields__["stop_audio"] = Mock() + doorbell.__fields__["play_audio"] = Mock() + doorbell.__fields__["wait_until_audio_completes"] = Mock() + doorbell.stop_audio = AsyncMock() + doorbell.play_audio = AsyncMock() + doorbell.wait_until_audio_completes = AsyncMock() with patch( "homeassistant.components.media_source.async_resolve_media", @@ -221,65 +204,75 @@ async def test_media_player_play_media_source( "media_player", "play_media", { - ATTR_ENTITY_ID: camera[1], + ATTR_ENTITY_ID: "media_player.test_camera_speaker", "media_content_id": "media-source://some_source/some_id", "media_content_type": "audio/mpeg", }, blocking=True, ) - camera[0].play_audio.assert_called_once_with( + doorbell.play_audio.assert_called_once_with( "http://example.com/test.mp3", blocking=False ) - camera[0].wait_until_audio_completes.assert_called_once() + doorbell.wait_until_audio_completes.assert_called_once() async def test_media_player_play_invalid( hass: HomeAssistant, - camera: tuple[Camera, str], + ufp: MockUFPFixture, + doorbell: Camera, + unadopted_camera: Camera, ): """Test media_player entity test play_media, not music.""" - camera[0].__fields__["play_audio"] = Mock() - camera[0].play_audio = AsyncMock() + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1) + + doorbell.__fields__["play_audio"] = Mock() + doorbell.play_audio = AsyncMock() with pytest.raises(HomeAssistantError): await hass.services.async_call( "media_player", "play_media", { - ATTR_ENTITY_ID: camera[1], + ATTR_ENTITY_ID: "media_player.test_camera_speaker", "media_content_id": "/test.png", "media_content_type": "image", }, blocking=True, ) - assert not camera[0].play_audio.called + assert not doorbell.play_audio.called async def test_media_player_play_error( hass: HomeAssistant, - camera: tuple[Camera, str], + ufp: MockUFPFixture, + doorbell: Camera, + unadopted_camera: Camera, ): """Test media_player entity test play_media, not music.""" - camera[0].__fields__["play_audio"] = Mock() - camera[0].__fields__["wait_until_audio_completes"] = Mock() - camera[0].play_audio = AsyncMock(side_effect=StreamError) - camera[0].wait_until_audio_completes = AsyncMock() + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1) + + doorbell.__fields__["play_audio"] = Mock() + doorbell.__fields__["wait_until_audio_completes"] = Mock() + doorbell.play_audio = AsyncMock(side_effect=StreamError) + doorbell.wait_until_audio_completes = AsyncMock() with pytest.raises(HomeAssistantError): await hass.services.async_call( "media_player", "play_media", { - ATTR_ENTITY_ID: camera[1], + ATTR_ENTITY_ID: "media_player.test_camera_speaker", "media_content_id": "/test.mp3", "media_content_type": "music", }, blocking=True, ) - assert camera[0].play_audio.called - assert not camera[0].wait_until_audio_completes.called + assert doorbell.play_audio.called + assert not doorbell.wait_until_audio_completes.called diff --git a/tests/components/unifiprotect/test_migrate.py b/tests/components/unifiprotect/test_migrate.py index 206c85e3654..64c8384d400 100644 --- a/tests/components/unifiprotect/test_migrate.py +++ b/tests/components/unifiprotect/test_migrate.py @@ -5,7 +5,6 @@ from __future__ import annotations from unittest.mock import AsyncMock from pyunifiprotect.data import Light -from pyunifiprotect.data.bootstrap import ProtectDeviceRef from pyunifiprotect.exceptions import NvrError from homeassistant.components.unifiprotect.const import DOMAIN @@ -14,56 +13,47 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import MockEntityFixture, generate_random_ids, regenerate_device_ids +from .utils import ( + MockUFPFixture, + generate_random_ids, + init_entry, + regenerate_device_ids, +) async def test_migrate_reboot_button( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light + hass: HomeAssistant, ufp: MockUFPFixture, light: Light ): """Test migrating unique ID of reboot button.""" - light1 = mock_light.copy() - light1._api = mock_entry.api + light1 = light.copy() light1.name = "Test Light 1" regenerate_device_ids(light1) - light2 = mock_light.copy() - light2._api = mock_entry.api + light2 = light.copy() light2.name = "Test Light 2" regenerate_device_ids(light2) - mock_entry.api.bootstrap.lights = { - light1.id: light1, - light2.id: light2, - } - mock_entry.api.bootstrap.id_lookup = { - light1.id: ProtectDeviceRef(id=light1.id, model=light1.model), - light2.id: ProtectDeviceRef(id=light2.id, model=light2.model), - } - mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) - registry = er.async_get(hass) registry.async_get_or_create( - Platform.BUTTON, DOMAIN, light1.id, config_entry=mock_entry.entry + Platform.BUTTON, DOMAIN, light1.id, config_entry=ufp.entry ) registry.async_get_or_create( Platform.BUTTON, DOMAIN, f"{light2.mac}_reboot", - config_entry=mock_entry.entry, + config_entry=ufp.entry, ) - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [light1, light2], regenerate_ids=False) - assert mock_entry.entry.state == ConfigEntryState.LOADED - assert mock_entry.api.update.called - assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac + assert ufp.entry.state == ConfigEntryState.LOADED + assert ufp.api.update.called + assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac buttons = [] - for entity in er.async_entries_for_config_entry( - registry, mock_entry.entry.entry_id - ): + for entity in er.async_entries_for_config_entry(registry, ufp.entry.entry_id): if entity.domain == Platform.BUTTON.value: buttons.append(entity) assert len(buttons) == 2 @@ -83,29 +73,33 @@ async def test_migrate_reboot_button( assert light.unique_id == f"{light2.mac}_reboot" -async def test_migrate_nvr_mac( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light -): +async def test_migrate_nvr_mac(hass: HomeAssistant, ufp: MockUFPFixture, light: Light): """Test migrating unique ID of NVR to use MAC address.""" - mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) - nvr = mock_entry.api.bootstrap.nvr - regenerate_device_ids(nvr) + light1 = light.copy() + light1.name = "Test Light 1" + regenerate_device_ids(light1) + light2 = light.copy() + light2.name = "Test Light 2" + regenerate_device_ids(light2) + + nvr = ufp.api.bootstrap.nvr + regenerate_device_ids(nvr) registry = er.async_get(hass) registry.async_get_or_create( Platform.SENSOR, DOMAIN, f"{nvr.id}_storage_utilization", - config_entry=mock_entry.entry, + config_entry=ufp.entry, ) - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [light1, light2], regenerate_ids=False) - assert mock_entry.entry.state == ConfigEntryState.LOADED - assert mock_entry.api.update.called - assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac + assert ufp.entry.state == ConfigEntryState.LOADED + assert ufp.api.update.called + assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac assert registry.async_get(f"{Platform.SENSOR}.{DOMAIN}_storage_utilization") is None assert ( @@ -119,171 +113,123 @@ async def test_migrate_nvr_mac( async def test_migrate_reboot_button_no_device( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light + hass: HomeAssistant, ufp: MockUFPFixture, light: Light ): """Test migrating unique ID of reboot button if UniFi Protect device ID changed.""" - light1 = mock_light.copy() - light1._api = mock_entry.api - light1.name = "Test Light 1" - regenerate_device_ids(light1) - light2_id, _ = generate_random_ids() - mock_entry.api.bootstrap.lights = { - light1.id: light1, - } - mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) - registry = er.async_get(hass) registry.async_get_or_create( - Platform.BUTTON, DOMAIN, light2_id, config_entry=mock_entry.entry + Platform.BUTTON, DOMAIN, light2_id, config_entry=ufp.entry ) - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [light], regenerate_ids=False) - assert mock_entry.entry.state == ConfigEntryState.LOADED - assert mock_entry.api.update.called - assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac + assert ufp.entry.state == ConfigEntryState.LOADED + assert ufp.api.update.called + assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac buttons = [] - for entity in er.async_entries_for_config_entry( - registry, mock_entry.entry.entry_id - ): + for entity in er.async_entries_for_config_entry(registry, ufp.entry.entry_id): if entity.domain == Platform.BUTTON.value: buttons.append(entity) assert len(buttons) == 2 - light = registry.async_get(f"{Platform.BUTTON}.unifiprotect_{light2_id.lower()}") - assert light is not None - assert light.unique_id == light2_id + entity = registry.async_get(f"{Platform.BUTTON}.unifiprotect_{light2_id.lower()}") + assert entity is not None + assert entity.unique_id == light2_id async def test_migrate_reboot_button_fail( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light + hass: HomeAssistant, ufp: MockUFPFixture, light: Light ): """Test migrating unique ID of reboot button.""" - light1 = mock_light.copy() - light1._api = mock_entry.api - light1.name = "Test Light 1" - regenerate_device_ids(light1) - - mock_entry.api.bootstrap.lights = { - light1.id: light1, - } - mock_entry.api.bootstrap.id_lookup = { - light1.id: ProtectDeviceRef(id=light1.id, model=light1.model), - } - mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) - registry = er.async_get(hass) registry.async_get_or_create( Platform.BUTTON, DOMAIN, - light1.id, - config_entry=mock_entry.entry, - suggested_object_id=light1.name, + light.id, + config_entry=ufp.entry, + suggested_object_id=light.display_name, ) registry.async_get_or_create( Platform.BUTTON, DOMAIN, - f"{light1.id}_reboot", - config_entry=mock_entry.entry, - suggested_object_id=light1.name, + f"{light.id}_reboot", + config_entry=ufp.entry, + suggested_object_id=light.display_name, ) - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [light], regenerate_ids=False) - assert mock_entry.entry.state == ConfigEntryState.LOADED - assert mock_entry.api.update.called - assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac + assert ufp.entry.state == ConfigEntryState.LOADED + assert ufp.api.update.called + assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac - light = registry.async_get(f"{Platform.BUTTON}.test_light_1") - assert light is not None - assert light.unique_id == f"{light1.mac}" + entity = registry.async_get(f"{Platform.BUTTON}.test_light") + assert entity is not None + assert entity.unique_id == f"{light.mac}" async def test_migrate_device_mac_button_fail( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light + hass: HomeAssistant, ufp: MockUFPFixture, light: Light ): """Test migrating unique ID to MAC format.""" - light1 = mock_light.copy() - light1._api = mock_entry.api - light1.name = "Test Light 1" - regenerate_device_ids(light1) - - mock_entry.api.bootstrap.lights = { - light1.id: light1, - } - mock_entry.api.bootstrap.id_lookup = { - light1.id: ProtectDeviceRef(id=light1.id, model=light1.model) - } - mock_entry.api.get_bootstrap = AsyncMock(return_value=mock_entry.api.bootstrap) - registry = er.async_get(hass) registry.async_get_or_create( Platform.BUTTON, DOMAIN, - f"{light1.id}_reboot", - config_entry=mock_entry.entry, - suggested_object_id=light1.name, + f"{light.id}_reboot", + config_entry=ufp.entry, + suggested_object_id=light.display_name, ) registry.async_get_or_create( Platform.BUTTON, DOMAIN, - f"{light1.mac}_reboot", - config_entry=mock_entry.entry, - suggested_object_id=light1.name, + f"{light.mac}_reboot", + config_entry=ufp.entry, + suggested_object_id=light.display_name, ) - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [light], regenerate_ids=False) - assert mock_entry.entry.state == ConfigEntryState.LOADED - assert mock_entry.api.update.called - assert mock_entry.entry.unique_id == mock_entry.api.bootstrap.nvr.mac + assert ufp.entry.state == ConfigEntryState.LOADED + assert ufp.api.update.called + assert ufp.entry.unique_id == ufp.api.bootstrap.nvr.mac - light = registry.async_get(f"{Platform.BUTTON}.test_light_1") - assert light is not None - assert light.unique_id == f"{light1.id}_reboot" + entity = registry.async_get(f"{Platform.BUTTON}.test_light") + assert entity is not None + assert entity.unique_id == f"{light.id}_reboot" async def test_migrate_device_mac_bootstrap_fail( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light + hass: HomeAssistant, ufp: MockUFPFixture, light: Light ): """Test migrating with a network error.""" - light1 = mock_light.copy() - light1._api = mock_entry.api - light1.name = "Test Light 1" - regenerate_device_ids(light1) - - mock_entry.api.bootstrap.lights = { - light1.id: light1, - } - mock_entry.api.get_bootstrap = AsyncMock(side_effect=NvrError) - registry = er.async_get(hass) registry.async_get_or_create( Platform.BUTTON, DOMAIN, - f"{light1.id}_reboot", - config_entry=mock_entry.entry, - suggested_object_id=light1.name, + f"{light.id}_reboot", + config_entry=ufp.entry, + suggested_object_id=light.name, ) registry.async_get_or_create( Platform.BUTTON, DOMAIN, - f"{light1.mac}_reboot", - config_entry=mock_entry.entry, - suggested_object_id=light1.name, + f"{light.mac}_reboot", + config_entry=ufp.entry, + suggested_object_id=light.name, ) - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() + ufp.api.get_bootstrap = AsyncMock(side_effect=NvrError) + await init_entry(hass, ufp, [light], regenerate_ids=False) - assert mock_entry.entry.state == ConfigEntryState.SETUP_RETRY + assert ufp.entry.state == ConfigEntryState.SETUP_RETRY diff --git a/tests/components/unifiprotect/test_number.py b/tests/components/unifiprotect/test_number.py index 043feae7925..656f7d08ba5 100644 --- a/tests/components/unifiprotect/test_number.py +++ b/tests/components/unifiprotect/test_number.py @@ -19,119 +19,23 @@ from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import ( - MockEntityFixture, +from .utils import ( + MockUFPFixture, assert_entity_counts, ids_from_device_description, - reset_objects, + init_entry, ) -@pytest.fixture(name="light") -async def light_fixture( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light -): - """Fixture for a single light for testing the number platform.""" - - # disable pydantic validation so mocking can happen - Light.__config__.validate_assignment = False - - light_obj = mock_light.copy() - light_obj._api = mock_entry.api - light_obj.name = "Test Light" - light_obj.light_device_settings.pir_sensitivity = 45 - light_obj.light_device_settings.pir_duration = timedelta(seconds=45) - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.lights = { - light_obj.id: light_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.NUMBER, 2, 2) - - yield light_obj - - Light.__config__.validate_assignment = True - - -@pytest.fixture(name="camera") -async def camera_fixture( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera -): - """Fixture for a single camera for testing the number platform.""" - - # disable pydantic validation so mocking can happen - Camera.__config__.validate_assignment = False - - camera_obj = mock_camera.copy() - camera_obj._api = mock_entry.api - camera_obj.channels[0]._api = mock_entry.api - camera_obj.channels[1]._api = mock_entry.api - camera_obj.channels[2]._api = mock_entry.api - camera_obj.name = "Test Camera" - camera_obj.feature_flags.can_optical_zoom = True - camera_obj.feature_flags.has_mic = True - # has_wdr is an the inverse of has HDR - camera_obj.feature_flags.has_hdr = False - camera_obj.isp_settings.wdr = 0 - camera_obj.mic_volume = 0 - camera_obj.isp_settings.zoom_position = 0 - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.cameras = { - camera_obj.id: camera_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.NUMBER, 3, 3) - - yield camera_obj - - Camera.__config__.validate_assignment = True - - -@pytest.fixture(name="doorlock") -async def doorlock_fixture( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_doorlock: Doorlock -): - """Fixture for a single doorlock for testing the number platform.""" - - # disable pydantic validation so mocking can happen - Doorlock.__config__.validate_assignment = False - - lock_obj = mock_doorlock.copy() - lock_obj._api = mock_entry.api - lock_obj.name = "Test Lock" - lock_obj.auto_close_time = timedelta(seconds=45) - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.doorlocks = { - lock_obj.id: lock_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.NUMBER, 1, 1) - - yield lock_obj - - Doorlock.__config__.validate_assignment = True - - async def test_number_setup_light( - hass: HomeAssistant, - light: Light, + hass: HomeAssistant, ufp: MockUFPFixture, light: Light ): """Test number entity setup for light devices.""" - entity_registry = er.async_get(hass) + await init_entry(hass, ufp, [light]) + assert_entity_counts(hass, Platform.NUMBER, 2, 2) + entity_registry = er.async_get(hass) for description in LIGHT_NUMBERS: unique_id, entity_id = ids_from_device_description( Platform.NUMBER, light, description @@ -148,11 +52,13 @@ async def test_number_setup_light( async def test_number_setup_camera_all( - hass: HomeAssistant, - camera: Camera, + hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera ): """Test number entity setup for camera devices (all features).""" + await init_entry(hass, ufp, [camera]) + assert_entity_counts(hass, Platform.NUMBER, 3, 3) + entity_registry = er.async_get(hass) for description in CAMERA_NUMBERS: @@ -171,64 +77,38 @@ async def test_number_setup_camera_all( async def test_number_setup_camera_none( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera + hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera ): """Test number entity setup for camera devices (no features).""" - camera_obj = mock_camera.copy() - camera_obj._api = mock_entry.api - camera_obj.channels[0]._api = mock_entry.api - camera_obj.channels[1]._api = mock_entry.api - camera_obj.channels[2]._api = mock_entry.api - camera_obj.name = "Test Camera" - camera_obj.feature_flags.can_optical_zoom = False - camera_obj.feature_flags.has_mic = False + camera.feature_flags.can_optical_zoom = False + camera.feature_flags.has_mic = False # has_wdr is an the inverse of has HDR - camera_obj.feature_flags.has_hdr = True - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.cameras = { - camera_obj.id: camera_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() + camera.feature_flags.has_hdr = True + await init_entry(hass, ufp, [camera]) assert_entity_counts(hass, Platform.NUMBER, 0, 0) async def test_number_setup_camera_missing_attr( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera + hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera ): """Test number entity setup for camera devices (no features, bad attrs).""" - # disable pydantic validation so mocking can happen - Camera.__config__.validate_assignment = False - - camera_obj = mock_camera.copy() - camera_obj._api = mock_entry.api - camera_obj.channels[0]._api = mock_entry.api - camera_obj.channels[1]._api = mock_entry.api - camera_obj.channels[2]._api = mock_entry.api - camera_obj.name = "Test Camera" - camera_obj.feature_flags = None - - Camera.__config__.validate_assignment = True - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.cameras = { - camera_obj.id: camera_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() + camera.feature_flags = None + await init_entry(hass, ufp, [camera]) assert_entity_counts(hass, Platform.NUMBER, 0, 0) -async def test_number_light_sensitivity(hass: HomeAssistant, light: Light): +async def test_number_light_sensitivity( + hass: HomeAssistant, ufp: MockUFPFixture, light: Light +): """Test sensitivity number entity for lights.""" + await init_entry(hass, ufp, [light]) + assert_entity_counts(hass, Platform.NUMBER, 2, 2) + description = LIGHT_NUMBERS[0] assert description.ufp_set_method is not None @@ -244,9 +124,14 @@ async def test_number_light_sensitivity(hass: HomeAssistant, light: Light): light.set_sensitivity.assert_called_once_with(15.0) -async def test_number_light_duration(hass: HomeAssistant, light: Light): +async def test_number_light_duration( + hass: HomeAssistant, ufp: MockUFPFixture, light: Light +): """Test auto-shutoff duration number entity for lights.""" + await init_entry(hass, ufp, [light]) + assert_entity_counts(hass, Platform.NUMBER, 2, 2) + description = LIGHT_NUMBERS[1] light.__fields__["set_duration"] = Mock() @@ -263,10 +148,16 @@ async def test_number_light_duration(hass: HomeAssistant, light: Light): @pytest.mark.parametrize("description", CAMERA_NUMBERS) async def test_number_camera_simple( - hass: HomeAssistant, camera: Camera, description: ProtectNumberEntityDescription + hass: HomeAssistant, + ufp: MockUFPFixture, + camera: Camera, + description: ProtectNumberEntityDescription, ): """Tests all simple numbers for cameras.""" + await init_entry(hass, ufp, [camera]) + assert_entity_counts(hass, Platform.NUMBER, 3, 3) + assert description.ufp_set_method is not None camera.__fields__[description.ufp_set_method] = Mock() @@ -282,9 +173,14 @@ async def test_number_camera_simple( set_method.assert_called_once_with(1.0) -async def test_number_lock_auto_close(hass: HomeAssistant, doorlock: Doorlock): +async def test_number_lock_auto_close( + hass: HomeAssistant, ufp: MockUFPFixture, doorlock: Doorlock +): """Test auto-lock timeout for locks.""" + await init_entry(hass, ufp, [doorlock]) + assert_entity_counts(hass, Platform.NUMBER, 1, 1) + description = DOORLOCK_NUMBERS[0] doorlock.__fields__["set_auto_close_time"] = Mock() diff --git a/tests/components/unifiprotect/test_select.py b/tests/components/unifiprotect/test_select.py index 01263a13cd9..637a0d4ad5d 100644 --- a/tests/components/unifiprotect/test_select.py +++ b/tests/components/unifiprotect/test_select.py @@ -3,7 +3,7 @@ from __future__ import annotations from copy import copy -from datetime import timedelta +from datetime import datetime, timedelta from unittest.mock import AsyncMock, Mock, patch import pytest @@ -38,162 +38,24 @@ from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, ATTR_OPTION, P from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from homeassistant.util.dt import utcnow -from .conftest import ( - MockEntityFixture, +from .utils import ( + MockUFPFixture, assert_entity_counts, ids_from_device_description, - reset_objects, + init_entry, ) -@pytest.fixture(name="viewer") -async def viewer_fixture( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - mock_viewer: Viewer, - mock_liveview: Liveview, -): - """Fixture for a single viewport for testing the select platform.""" - - # disable pydantic validation so mocking can happen - Viewer.__config__.validate_assignment = False - - viewer_obj = mock_viewer.copy() - viewer_obj._api = mock_entry.api - viewer_obj.name = "Test Viewer" - viewer_obj.liveview_id = mock_liveview.id - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.viewers = { - viewer_obj.id: viewer_obj, - } - mock_entry.api.bootstrap.liveviews = {mock_liveview.id: mock_liveview} - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.SELECT, 1, 1) - - yield viewer_obj - - Viewer.__config__.validate_assignment = True - - -@pytest.fixture(name="camera") -async def camera_fixture( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera -): - """Fixture for a single camera for testing the select platform.""" - - # disable pydantic validation so mocking can happen - Camera.__config__.validate_assignment = False - - camera_obj = mock_camera.copy() - camera_obj._api = mock_entry.api - camera_obj.channels[0]._api = mock_entry.api - camera_obj.channels[1]._api = mock_entry.api - camera_obj.channels[2]._api = mock_entry.api - camera_obj.name = "Test Camera" - camera_obj.feature_flags.has_lcd_screen = True - camera_obj.feature_flags.has_chime = True - camera_obj.recording_settings.mode = RecordingMode.ALWAYS - camera_obj.isp_settings.ir_led_mode = IRLEDMode.AUTO - camera_obj.lcd_message = None - camera_obj.chime_duration = 0 - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.cameras = { - camera_obj.id: camera_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.SELECT, 4, 4) - - yield camera_obj - - Camera.__config__.validate_assignment = True - - -@pytest.fixture(name="light") -async def light_fixture( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - mock_light: Light, - camera: Camera, -): - """Fixture for a single light for testing the select platform.""" - - # disable pydantic validation so mocking can happen - Light.__config__.validate_assignment = False - - light_obj = mock_light.copy() - light_obj._api = mock_entry.api - light_obj.name = "Test Light" - light_obj.camera_id = None - light_obj.light_mode_settings.mode = LightModeType.MOTION - light_obj.light_mode_settings.enable_at = LightModeEnableType.DARK - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.cameras = {camera.id: camera} - mock_entry.api.bootstrap.lights = { - light_obj.id: light_obj, - } - - await hass.config_entries.async_reload(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.SELECT, 6, 6) - - yield light_obj - - Light.__config__.validate_assignment = True - - -@pytest.fixture(name="camera_none") -async def camera_none_fixture( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera -): - """Fixture for a single camera for testing the select platform.""" - - # disable pydantic validation so mocking can happen - Camera.__config__.validate_assignment = False - - camera_obj = mock_camera.copy() - camera_obj._api = mock_entry.api - camera_obj.channels[0]._api = mock_entry.api - camera_obj.channels[1]._api = mock_entry.api - camera_obj.channels[2]._api = mock_entry.api - camera_obj.name = "Test Camera" - camera_obj.feature_flags.has_lcd_screen = False - camera_obj.feature_flags.has_chime = False - camera_obj.recording_settings.mode = RecordingMode.ALWAYS - camera_obj.isp_settings.ir_led_mode = IRLEDMode.AUTO - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.cameras = { - camera_obj.id: camera_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.SELECT, 2, 2) - - yield camera_obj - - Camera.__config__.validate_assignment = True - - async def test_select_setup_light( - hass: HomeAssistant, - light: Light, + hass: HomeAssistant, ufp: MockUFPFixture, light: Light ): """Test select entity setup for light devices.""" + light.light_mode_settings.enable_at = LightModeEnableType.DARK + await init_entry(hass, ufp, [light]) + assert_entity_counts(hass, Platform.SELECT, 2, 2) + entity_registry = er.async_get(hass) expected_values = ("On Motion - When Dark", "Not Paired") @@ -213,11 +75,14 @@ async def test_select_setup_light( async def test_select_setup_viewer( - hass: HomeAssistant, - viewer: Viewer, + hass: HomeAssistant, ufp: MockUFPFixture, viewer: Viewer, liveview: Liveview ): """Test select entity setup for light devices.""" + ufp.api.bootstrap.liveviews = {liveview.id: liveview} + await init_entry(hass, ufp, [viewer]) + assert_entity_counts(hass, Platform.SELECT, 1, 1) + entity_registry = er.async_get(hass) description = VIEWER_SELECTS[0] @@ -236,15 +101,46 @@ async def test_select_setup_viewer( async def test_select_setup_camera_all( - hass: HomeAssistant, - camera: Camera, + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ): """Test select entity setup for camera devices (all features).""" + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.SELECT, 4, 4) + entity_registry = er.async_get(hass) expected_values = ("Always", "Auto", "Default Message (Welcome)", "None") for index, description in enumerate(CAMERA_SELECTS): + unique_id, entity_id = ids_from_device_description( + Platform.SELECT, doorbell, description + ) + + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == unique_id + + state = hass.states.get(entity_id) + assert state + assert state.state == expected_values[index] + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + + +async def test_select_setup_camera_none( + hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera +): + """Test select entity setup for camera devices (no features).""" + + await init_entry(hass, ufp, [camera]) + assert_entity_counts(hass, Platform.SELECT, 2, 2) + + entity_registry = er.async_get(hass) + expected_values = ("Always", "Auto", "Default Message (Welcome)") + + for index, description in enumerate(CAMERA_SELECTS): + if index == 2: + return + unique_id, entity_id = ids_from_device_description( Platform.SELECT, camera, description ) @@ -259,41 +155,15 @@ async def test_select_setup_camera_all( assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION -async def test_select_setup_camera_none( - hass: HomeAssistant, - camera_none: Camera, -): - """Test select entity setup for camera devices (no features).""" - - entity_registry = er.async_get(hass) - expected_values = ("Always", "Auto", "Default Message (Welcome)") - - for index, description in enumerate(CAMERA_SELECTS): - if index == 2: - return - - unique_id, entity_id = ids_from_device_description( - Platform.SELECT, camera_none, description - ) - - entity = entity_registry.async_get(entity_id) - assert entity - assert entity.unique_id == unique_id - - state = hass.states.get(entity_id) - assert state - assert state.state == expected_values[index] - assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION - - async def test_select_update_liveview( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - viewer: Viewer, - mock_liveview: Liveview, + hass: HomeAssistant, ufp: MockUFPFixture, viewer: Viewer, liveview: Liveview ): """Test select entity update (new Liveview).""" + ufp.api.bootstrap.liveviews = {liveview.id: liveview} + await init_entry(hass, ufp, [viewer]) + assert_entity_counts(hass, Platform.SELECT, 1, 1) + _, entity_id = ids_from_device_description( Platform.SELECT, viewer, VIEWER_SELECTS[0] ) @@ -302,17 +172,18 @@ async def test_select_update_liveview( assert state expected_options = state.attributes[ATTR_OPTIONS] - new_bootstrap = copy(mock_entry.api.bootstrap) - new_liveview = copy(mock_liveview) + new_liveview = copy(liveview) new_liveview.id = "test_id" mock_msg = Mock() mock_msg.changed_data = {} mock_msg.new_obj = new_liveview - new_bootstrap.liveviews = {**new_bootstrap.liveviews, new_liveview.id: new_liveview} - mock_entry.api.bootstrap = new_bootstrap - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.liveviews = { + **ufp.api.bootstrap.liveviews, + new_liveview.id: new_liveview, + } + ufp.ws_msg(mock_msg) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -321,16 +192,17 @@ async def test_select_update_liveview( async def test_select_update_doorbell_settings( - hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ): """Test select entity update (new Doorbell Message).""" - expected_length = ( - len(mock_entry.api.bootstrap.nvr.doorbell_settings.all_messages) + 1 - ) + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.SELECT, 4, 4) + + expected_length = len(ufp.api.bootstrap.nvr.doorbell_settings.all_messages) + 1 _, entity_id = ids_from_device_description( - Platform.SELECT, camera, CAMERA_SELECTS[2] + Platform.SELECT, doorbell, CAMERA_SELECTS[2] ) state = hass.states.get(entity_id) @@ -338,7 +210,7 @@ async def test_select_update_doorbell_settings( assert len(state.attributes[ATTR_OPTIONS]) == expected_length expected_length += 1 - new_nvr = copy(mock_entry.api.bootstrap.nvr) + new_nvr = copy(ufp.api.bootstrap.nvr) new_nvr.__fields__["update_all_messages"] = Mock() new_nvr.update_all_messages = Mock() @@ -354,8 +226,8 @@ async def test_select_update_doorbell_settings( mock_msg.changed_data = {"doorbell_settings": {}} mock_msg.new_obj = new_nvr - mock_entry.api.bootstrap.nvr = new_nvr - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.nvr = new_nvr + ufp.ws_msg(mock_msg) await hass.async_block_till_done() new_nvr.update_all_messages.assert_called_once() @@ -366,22 +238,22 @@ async def test_select_update_doorbell_settings( async def test_select_update_doorbell_message( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - camera: Camera, + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ): """Test select entity update (change doorbell message).""" + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.SELECT, 4, 4) + _, entity_id = ids_from_device_description( - Platform.SELECT, camera, CAMERA_SELECTS[2] + Platform.SELECT, doorbell, CAMERA_SELECTS[2] ) state = hass.states.get(entity_id) assert state assert state.state == "Default Message (Welcome)" - new_bootstrap = copy(mock_entry.api.bootstrap) - new_camera = camera.copy() + new_camera = doorbell.copy() new_camera.lcd_message = LCDMessage( type=DoorbellMessageType.CUSTOM_MESSAGE, text="Test" ) @@ -390,9 +262,8 @@ async def test_select_update_doorbell_message( mock_msg.changed_data = {} mock_msg.new_obj = new_camera - new_bootstrap.cameras = {new_camera.id: new_camera} - mock_entry.api.bootstrap = new_bootstrap - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.ws_msg(mock_msg) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -401,10 +272,13 @@ async def test_select_update_doorbell_message( async def test_select_set_option_light_motion( - hass: HomeAssistant, - light: Light, + hass: HomeAssistant, ufp: MockUFPFixture, light: Light ): """Test Light Mode select.""" + + await init_entry(hass, ufp, [light]) + assert_entity_counts(hass, Platform.SELECT, 2, 2) + _, entity_id = ids_from_device_description(Platform.SELECT, light, LIGHT_SELECTS[0]) light.__fields__["set_light_settings"] = Mock() @@ -423,10 +297,13 @@ async def test_select_set_option_light_motion( async def test_select_set_option_light_camera( - hass: HomeAssistant, - light: Light, + hass: HomeAssistant, ufp: MockUFPFixture, light: Light, camera: Camera ): """Test Paired Camera select.""" + + await init_entry(hass, ufp, [light, camera]) + assert_entity_counts(hass, Platform.SELECT, 4, 4) + _, entity_id = ids_from_device_description(Platform.SELECT, light, LIGHT_SELECTS[1]) light.__fields__["set_paired_camera"] = Mock() @@ -454,16 +331,19 @@ async def test_select_set_option_light_camera( async def test_select_set_option_camera_recording( - hass: HomeAssistant, - camera: Camera, + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ): """Test Recording Mode select.""" + + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.SELECT, 4, 4) + _, entity_id = ids_from_device_description( - Platform.SELECT, camera, CAMERA_SELECTS[0] + Platform.SELECT, doorbell, CAMERA_SELECTS[0] ) - camera.__fields__["set_recording_mode"] = Mock() - camera.set_recording_mode = AsyncMock() + doorbell.__fields__["set_recording_mode"] = Mock() + doorbell.set_recording_mode = AsyncMock() await hass.services.async_call( "select", @@ -472,20 +352,23 @@ async def test_select_set_option_camera_recording( blocking=True, ) - camera.set_recording_mode.assert_called_once_with(RecordingMode.NEVER) + doorbell.set_recording_mode.assert_called_once_with(RecordingMode.NEVER) async def test_select_set_option_camera_ir( - hass: HomeAssistant, - camera: Camera, + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ): """Test Infrared Mode select.""" + + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.SELECT, 4, 4) + _, entity_id = ids_from_device_description( - Platform.SELECT, camera, CAMERA_SELECTS[1] + Platform.SELECT, doorbell, CAMERA_SELECTS[1] ) - camera.__fields__["set_ir_led_model"] = Mock() - camera.set_ir_led_model = AsyncMock() + doorbell.__fields__["set_ir_led_model"] = Mock() + doorbell.set_ir_led_model = AsyncMock() await hass.services.async_call( "select", @@ -494,20 +377,23 @@ async def test_select_set_option_camera_ir( blocking=True, ) - camera.set_ir_led_model.assert_called_once_with(IRLEDMode.ON) + doorbell.set_ir_led_model.assert_called_once_with(IRLEDMode.ON) async def test_select_set_option_camera_doorbell_custom( - hass: HomeAssistant, - camera: Camera, + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ): """Test Doorbell Text select (user defined message).""" + + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.SELECT, 4, 4) + _, entity_id = ids_from_device_description( - Platform.SELECT, camera, CAMERA_SELECTS[2] + Platform.SELECT, doorbell, CAMERA_SELECTS[2] ) - camera.__fields__["set_lcd_text"] = Mock() - camera.set_lcd_text = AsyncMock() + doorbell.__fields__["set_lcd_text"] = Mock() + doorbell.set_lcd_text = AsyncMock() await hass.services.async_call( "select", @@ -516,22 +402,25 @@ async def test_select_set_option_camera_doorbell_custom( blocking=True, ) - camera.set_lcd_text.assert_called_once_with( + doorbell.set_lcd_text.assert_called_once_with( DoorbellMessageType.CUSTOM_MESSAGE, text="Test" ) async def test_select_set_option_camera_doorbell_unifi( - hass: HomeAssistant, - camera: Camera, + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ): """Test Doorbell Text select (unifi message).""" + + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.SELECT, 4, 4) + _, entity_id = ids_from_device_description( - Platform.SELECT, camera, CAMERA_SELECTS[2] + Platform.SELECT, doorbell, CAMERA_SELECTS[2] ) - camera.__fields__["set_lcd_text"] = Mock() - camera.set_lcd_text = AsyncMock() + doorbell.__fields__["set_lcd_text"] = Mock() + doorbell.set_lcd_text = AsyncMock() await hass.services.async_call( "select", @@ -543,7 +432,7 @@ async def test_select_set_option_camera_doorbell_unifi( blocking=True, ) - camera.set_lcd_text.assert_called_once_with( + doorbell.set_lcd_text.assert_called_once_with( DoorbellMessageType.LEAVE_PACKAGE_AT_DOOR ) @@ -557,20 +446,23 @@ async def test_select_set_option_camera_doorbell_unifi( blocking=True, ) - camera.set_lcd_text.assert_called_with(None) + doorbell.set_lcd_text.assert_called_with(None) async def test_select_set_option_camera_doorbell_default( - hass: HomeAssistant, - camera: Camera, + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ): """Test Doorbell Text select (default message).""" + + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.SELECT, 4, 4) + _, entity_id = ids_from_device_description( - Platform.SELECT, camera, CAMERA_SELECTS[2] + Platform.SELECT, doorbell, CAMERA_SELECTS[2] ) - camera.__fields__["set_lcd_text"] = Mock() - camera.set_lcd_text = AsyncMock() + doorbell.__fields__["set_lcd_text"] = Mock() + doorbell.set_lcd_text = AsyncMock() await hass.services.async_call( "select", @@ -582,14 +474,18 @@ async def test_select_set_option_camera_doorbell_default( blocking=True, ) - camera.set_lcd_text.assert_called_once_with(None) + doorbell.set_lcd_text.assert_called_once_with(None) async def test_select_set_option_viewer( - hass: HomeAssistant, - viewer: Viewer, + hass: HomeAssistant, ufp: MockUFPFixture, viewer: Viewer, liveview: Liveview ): """Test Liveview select.""" + + ufp.api.bootstrap.liveviews = {liveview.id: liveview} + await init_entry(hass, ufp, [viewer]) + assert_entity_counts(hass, Platform.SELECT, 1, 1) + _, entity_id = ids_from_device_description( Platform.SELECT, viewer, VIEWER_SELECTS[0] ) @@ -610,16 +506,19 @@ async def test_select_set_option_viewer( async def test_select_service_doorbell_invalid( - hass: HomeAssistant, - camera: Camera, + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ): """Test Doorbell Text service (invalid).""" + + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.SELECT, 4, 4) + _, entity_id = ids_from_device_description( - Platform.SELECT, camera, CAMERA_SELECTS[1] + Platform.SELECT, doorbell, CAMERA_SELECTS[1] ) - camera.__fields__["set_lcd_text"] = Mock() - camera.set_lcd_text = AsyncMock() + doorbell.__fields__["set_lcd_text"] = Mock() + doorbell.set_lcd_text = AsyncMock() with pytest.raises(HomeAssistantError): await hass.services.async_call( @@ -629,20 +528,23 @@ async def test_select_service_doorbell_invalid( blocking=True, ) - assert not camera.set_lcd_text.called + assert not doorbell.set_lcd_text.called async def test_select_service_doorbell_success( - hass: HomeAssistant, - camera: Camera, + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ): """Test Doorbell Text service (success).""" + + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.SELECT, 4, 4) + _, entity_id = ids_from_device_description( - Platform.SELECT, camera, CAMERA_SELECTS[2] + Platform.SELECT, doorbell, CAMERA_SELECTS[2] ) - camera.__fields__["set_lcd_text"] = Mock() - camera.set_lcd_text = AsyncMock() + doorbell.__fields__["set_lcd_text"] = Mock() + doorbell.set_lcd_text = AsyncMock() await hass.services.async_call( "unifiprotect", @@ -654,7 +556,7 @@ async def test_select_service_doorbell_success( blocking=True, ) - camera.set_lcd_text.assert_called_once_with( + doorbell.set_lcd_text.assert_called_once_with( DoorbellMessageType.CUSTOM_MESSAGE, "Test", reset_at=None ) @@ -663,18 +565,23 @@ async def test_select_service_doorbell_success( async def test_select_service_doorbell_with_reset( mock_now, hass: HomeAssistant, - camera: Camera, + ufp: MockUFPFixture, + doorbell: Camera, + fixed_now: datetime, ): """Test Doorbell Text service (success with reset time).""" - now = utcnow() - mock_now.return_value = now + + mock_now.return_value = fixed_now _, entity_id = ids_from_device_description( - Platform.SELECT, camera, CAMERA_SELECTS[2] + Platform.SELECT, doorbell, CAMERA_SELECTS[2] ) - camera.__fields__["set_lcd_text"] = Mock() - camera.set_lcd_text = AsyncMock() + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.SELECT, 4, 4) + + doorbell.__fields__["set_lcd_text"] = Mock() + doorbell.set_lcd_text = AsyncMock() await hass.services.async_call( "unifiprotect", @@ -687,8 +594,8 @@ async def test_select_service_doorbell_with_reset( blocking=True, ) - camera.set_lcd_text.assert_called_once_with( + doorbell.set_lcd_text.assert_called_once_with( DoorbellMessageType.CUSTOM_MESSAGE, "Test", - reset_at=now + timedelta(minutes=60), + reset_at=fixed_now + timedelta(minutes=60), ) diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index 1a84c4f55ca..e204b09b1b0 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -2,11 +2,9 @@ # pylint: disable=protected-access from __future__ import annotations -from copy import copy from datetime import datetime, timedelta from unittest.mock import AsyncMock, Mock -import pytest from pyunifiprotect.data import ( NVR, Camera, @@ -15,7 +13,6 @@ from pyunifiprotect.data import ( Sensor, SmartDetectObjectType, ) -from pyunifiprotect.data.base import WifiConnectionState, WiredConnectionState from pyunifiprotect.data.nvr import EventMetadata from homeassistant.components.unifiprotect.const import ( @@ -42,11 +39,12 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import ( - MockEntityFixture, +from .utils import ( + MockUFPFixture, assert_entity_counts, enable_entity, ids_from_device_description, + init_entry, reset_objects, time_changed, ) @@ -55,136 +53,12 @@ CAMERA_SENSORS_WRITE = CAMERA_SENSORS[:5] SENSE_SENSORS_WRITE = SENSE_SENSORS[:8] -@pytest.fixture(name="sensor") -async def sensor_fixture( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - mock_sensor: Sensor, - now: datetime, -): - """Fixture for a single sensor for testing the sensor platform.""" - - # disable pydantic validation so mocking can happen - Sensor.__config__.validate_assignment = False - - sensor_obj = mock_sensor.copy() - sensor_obj._api = mock_entry.api - sensor_obj.name = "Test Sensor" - sensor_obj.battery_status.percentage = 10.0 - sensor_obj.light_settings.is_enabled = True - sensor_obj.humidity_settings.is_enabled = True - sensor_obj.temperature_settings.is_enabled = True - sensor_obj.alarm_settings.is_enabled = True - sensor_obj.stats.light.value = 10.0 - sensor_obj.stats.humidity.value = 10.0 - sensor_obj.stats.temperature.value = 10.0 - sensor_obj.up_since = now - sensor_obj.bluetooth_connection_state.signal_strength = -50.0 - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.sensors = { - sensor_obj.id: sensor_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - yield sensor_obj - - Sensor.__config__.validate_assignment = True - - -@pytest.fixture(name="sensor_none") -async def sensor_none_fixture( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - mock_sensor: Sensor, - now: datetime, -): - """Fixture for a single sensor for testing the sensor platform.""" - - # disable pydantic validation so mocking can happen - Sensor.__config__.validate_assignment = False - - sensor_obj = mock_sensor.copy() - sensor_obj._api = mock_entry.api - sensor_obj.name = "Test Sensor" - sensor_obj.battery_status.percentage = 10.0 - sensor_obj.light_settings.is_enabled = False - sensor_obj.humidity_settings.is_enabled = False - sensor_obj.temperature_settings.is_enabled = False - sensor_obj.alarm_settings.is_enabled = False - sensor_obj.up_since = now - sensor_obj.bluetooth_connection_state.signal_strength = -50.0 - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.sensors = { - sensor_obj.id: sensor_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - # 4 from all, 5 from sense, 12 NVR - assert_entity_counts(hass, Platform.SENSOR, 22, 14) - - yield sensor_obj - - Sensor.__config__.validate_assignment = True - - -@pytest.fixture(name="camera") -async def camera_fixture( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - mock_camera: Camera, - now: datetime, -): - """Fixture for a single camera for testing the sensor platform.""" - - # disable pydantic validation so mocking can happen - Camera.__config__.validate_assignment = False - - camera_obj = mock_camera.copy() - camera_obj._api = mock_entry.api - camera_obj.channels[0]._api = mock_entry.api - camera_obj.channels[1]._api = mock_entry.api - camera_obj.channels[2]._api = mock_entry.api - camera_obj.name = "Test Camera" - camera_obj.feature_flags.has_smart_detect = True - camera_obj.feature_flags.has_chime = True - camera_obj.is_smart_detected = False - camera_obj.wired_connection_state = WiredConnectionState(phy_rate=1000) - camera_obj.wifi_connection_state = WifiConnectionState( - signal_quality=100, signal_strength=-50 - ) - camera_obj.stats.rx_bytes = 100.0 - camera_obj.stats.tx_bytes = 100.0 - camera_obj.stats.video.recording_start = now - camera_obj.stats.storage.used = 100.0 - camera_obj.stats.storage.used = 100.0 - camera_obj.stats.storage.rate = 0.1 - camera_obj.voltage = 20.0 - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.nvr.system_info.storage.devices = [] - mock_entry.api.bootstrap.cameras = { - camera_obj.id: camera_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - yield camera_obj - - Camera.__config__.validate_assignment = True - - async def test_sensor_setup_sensor( - hass: HomeAssistant, mock_entry: MockEntityFixture, sensor: Sensor + hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor ): """Test sensor entity setup for sensor devices.""" - # 5 from all, 5 from sense, 12 NVR + + await init_entry(hass, ufp, [sensor_all]) assert_entity_counts(hass, Platform.SENSOR, 22, 14) entity_registry = er.async_get(hass) @@ -196,6 +70,57 @@ async def test_sensor_setup_sensor( "10.0", "none", ) + for index, description in enumerate(SENSE_SENSORS_WRITE): + if not description.entity_registry_enabled_default: + continue + unique_id, entity_id = ids_from_device_description( + Platform.SENSOR, sensor_all, description + ) + + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == unique_id + + state = hass.states.get(entity_id) + assert state + assert state.state == expected_values[index] + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + + # BLE signal + unique_id, entity_id = ids_from_device_description( + Platform.SENSOR, sensor_all, ALL_DEVICES_SENSORS[1] + ) + + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.disabled is True + assert entity.unique_id == unique_id + + await enable_entity(hass, ufp.entry.entry_id, entity_id) + + state = hass.states.get(entity_id) + assert state + assert state.state == "-50" + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + + +async def test_sensor_setup_sensor_none( + hass: HomeAssistant, ufp: MockUFPFixture, sensor: Sensor +): + """Test sensor entity setup for sensor devices with no sensors enabled.""" + + await init_entry(hass, ufp, [sensor]) + assert_entity_counts(hass, Platform.SENSOR, 22, 14) + + entity_registry = er.async_get(hass) + + expected_values = ( + "10", + STATE_UNAVAILABLE, + STATE_UNAVAILABLE, + STATE_UNAVAILABLE, + STATE_UNAVAILABLE, + ) for index, description in enumerate(SENSE_SENSORS_WRITE): if not description.entity_registry_enabled_default: continue @@ -212,63 +137,15 @@ async def test_sensor_setup_sensor( assert state.state == expected_values[index] assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION - # BLE signal - unique_id, entity_id = ids_from_device_description( - Platform.SENSOR, sensor, ALL_DEVICES_SENSORS[1] - ) - - entity = entity_registry.async_get(entity_id) - assert entity - assert entity.disabled is True - assert entity.unique_id == unique_id - - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) - - state = hass.states.get(entity_id) - assert state - assert state.state == "-50" - assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION - - -async def test_sensor_setup_sensor_none( - hass: HomeAssistant, mock_entry: MockEntityFixture, sensor_none: Sensor -): - """Test sensor entity setup for sensor devices with no sensors enabled.""" - - entity_registry = er.async_get(hass) - - expected_values = ( - "10", - STATE_UNAVAILABLE, - STATE_UNAVAILABLE, - STATE_UNAVAILABLE, - STATE_UNAVAILABLE, - ) - for index, description in enumerate(SENSE_SENSORS_WRITE): - if not description.entity_registry_enabled_default: - continue - unique_id, entity_id = ids_from_device_description( - Platform.SENSOR, sensor_none, description - ) - - entity = entity_registry.async_get(entity_id) - assert entity - assert entity.unique_id == unique_id - - state = hass.states.get(entity_id) - assert state - assert state.state == expected_values[index] - assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION - async def test_sensor_setup_nvr( - hass: HomeAssistant, mock_entry: MockEntityFixture, now: datetime + hass: HomeAssistant, ufp: MockUFPFixture, fixed_now: datetime ): """Test sensor entity setup for NVR device.""" - reset_objects(mock_entry.api.bootstrap) - nvr: NVR = mock_entry.api.bootstrap.nvr - nvr.up_since = now + reset_objects(ufp.api.bootstrap) + nvr: NVR = ufp.api.bootstrap.nvr + nvr.up_since = fixed_now nvr.system_info.cpu.average_load = 50.0 nvr.system_info.cpu.temperature = 50.0 nvr.storage_stats.utilization = 50.0 @@ -282,16 +159,15 @@ async def test_sensor_setup_nvr( nvr.storage_stats.storage_distribution.free.percentage = 50.0 nvr.storage_stats.capacity = 50.0 - await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() - # 2 from all, 4 from sense, 12 NVR assert_entity_counts(hass, Platform.SENSOR, 12, 9) entity_registry = er.async_get(hass) expected_values = ( - now.replace(second=0, microsecond=0).isoformat(), + fixed_now.replace(second=0, microsecond=0).isoformat(), "50.0", "50.0", "50.0", @@ -312,7 +188,7 @@ async def test_sensor_setup_nvr( assert entity.unique_id == unique_id if not description.entity_registry_enabled_default: - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await enable_entity(hass, ufp.entry.entry_id, entity_id) state = hass.states.get(entity_id) assert state @@ -330,7 +206,7 @@ async def test_sensor_setup_nvr( assert entity.disabled is not description.entity_registry_enabled_default assert entity.unique_id == unique_id - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await enable_entity(hass, ufp.entry.entry_id, entity_id) state = hass.states.get(entity_id) assert state @@ -338,22 +214,19 @@ async def test_sensor_setup_nvr( assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION -async def test_sensor_nvr_missing_values( - hass: HomeAssistant, mock_entry: MockEntityFixture, now: datetime -): +async def test_sensor_nvr_missing_values(hass: HomeAssistant, ufp: MockUFPFixture): """Test NVR sensor sensors if no data available.""" - reset_objects(mock_entry.api.bootstrap) - nvr: NVR = mock_entry.api.bootstrap.nvr + reset_objects(ufp.api.bootstrap) + nvr: NVR = ufp.api.bootstrap.nvr nvr.system_info.memory.available = None nvr.system_info.memory.total = None nvr.up_since = None nvr.storage_stats.capacity = None - await hass.config_entries.async_setup(mock_entry.entry.entry_id) + await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() - # 2 from all, 4 from sense, 12 NVR assert_entity_counts(hass, Platform.SENSOR, 12, 9) entity_registry = er.async_get(hass) @@ -368,7 +241,7 @@ async def test_sensor_nvr_missing_values( assert entity assert entity.unique_id == unique_id - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await enable_entity(hass, ufp.entry.entry_id, entity_id) state = hass.states.get(entity_id) assert state @@ -401,7 +274,7 @@ async def test_sensor_nvr_missing_values( assert entity.disabled is True assert entity.unique_id == unique_id - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await enable_entity(hass, ufp.entry.entry_id, entity_id) state = hass.states.get(entity_id) assert state @@ -410,16 +283,17 @@ async def test_sensor_nvr_missing_values( async def test_sensor_setup_camera( - hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera, now: datetime + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime ): """Test sensor entity setup for camera devices.""" - # 3 from all, 7 from camera, 12 NVR + + await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SENSOR, 25, 13) entity_registry = er.async_get(hass) expected_values = ( - now.replace(microsecond=0).isoformat(), + fixed_now.replace(microsecond=0).isoformat(), "100", "100.0", "20.0", @@ -428,7 +302,7 @@ async def test_sensor_setup_camera( if not description.entity_registry_enabled_default: continue unique_id, entity_id = ids_from_device_description( - Platform.SENSOR, camera, description + Platform.SENSOR, doorbell, description ) entity = entity_registry.async_get(entity_id) @@ -444,7 +318,7 @@ async def test_sensor_setup_camera( expected_values = ("100", "100") for index, description in enumerate(CAMERA_DISABLED_SENSORS): unique_id, entity_id = ids_from_device_description( - Platform.SENSOR, camera, description + Platform.SENSOR, doorbell, description ) entity = entity_registry.async_get(entity_id) @@ -452,7 +326,7 @@ async def test_sensor_setup_camera( assert entity.disabled is not description.entity_registry_enabled_default assert entity.unique_id == unique_id - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await enable_entity(hass, ufp.entry.entry_id, entity_id) state = hass.states.get(entity_id) assert state @@ -461,7 +335,7 @@ async def test_sensor_setup_camera( # Wired signal unique_id, entity_id = ids_from_device_description( - Platform.SENSOR, camera, ALL_DEVICES_SENSORS[2] + Platform.SENSOR, doorbell, ALL_DEVICES_SENSORS[2] ) entity = entity_registry.async_get(entity_id) @@ -469,7 +343,7 @@ async def test_sensor_setup_camera( assert entity.disabled is True assert entity.unique_id == unique_id - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await enable_entity(hass, ufp.entry.entry_id, entity_id) state = hass.states.get(entity_id) assert state @@ -478,7 +352,7 @@ async def test_sensor_setup_camera( # WiFi signal unique_id, entity_id = ids_from_device_description( - Platform.SENSOR, camera, ALL_DEVICES_SENSORS[3] + Platform.SENSOR, doorbell, ALL_DEVICES_SENSORS[3] ) entity = entity_registry.async_get(entity_id) @@ -486,7 +360,7 @@ async def test_sensor_setup_camera( assert entity.disabled is True assert entity.unique_id == unique_id - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await enable_entity(hass, ufp.entry.entry_id, entity_id) state = hass.states.get(entity_id) assert state @@ -495,7 +369,7 @@ async def test_sensor_setup_camera( # Detected Object unique_id, entity_id = ids_from_device_description( - Platform.SENSOR, camera, MOTION_SENSORS[0] + Platform.SENSOR, doorbell, MOTION_SENSORS[0] ) entity = entity_registry.async_get(entity_id) @@ -512,16 +386,20 @@ async def test_sensor_setup_camera( async def test_sensor_setup_camera_with_last_trip_time( hass: HomeAssistant, entity_registry_enabled_by_default: AsyncMock, - mock_entry: MockEntityFixture, - camera: Camera, - now: datetime, + ufp: MockUFPFixture, + doorbell: Camera, + fixed_now: datetime, ): """Test sensor entity setup for camera devices with last trip time.""" + + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.SENSOR, 25, 25) + entity_registry = er.async_get(hass) # Last Trip Time unique_id, entity_id = ids_from_device_description( - Platform.SENSOR, camera, MOTION_TRIP_SENSORS[0] + Platform.SENSOR, doorbell, MOTION_TRIP_SENSORS[0] ) entity = entity_registry.async_get(entity_id) @@ -530,35 +408,38 @@ async def test_sensor_setup_camera_with_last_trip_time( state = hass.states.get(entity_id) assert state - assert state.state == "2021-12-20T17:26:53+00:00" + assert ( + state.state + == (fixed_now - timedelta(hours=1)).replace(microsecond=0).isoformat() + ) assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION async def test_sensor_update_motion( - hass: HomeAssistant, mock_entry: MockEntityFixture, camera: Camera, now: datetime + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime ): """Test sensor motion entity.""" - # 3 from all, 7 from camera, 12 NVR + + await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SENSOR, 25, 13) _, entity_id = ids_from_device_description( - Platform.SENSOR, camera, MOTION_SENSORS[0] + Platform.SENSOR, doorbell, MOTION_SENSORS[0] ) event = Event( id="test_event_id", type=EventType.SMART_DETECT, - start=now - timedelta(seconds=1), + start=fixed_now - timedelta(seconds=1), end=None, score=100, smart_detect_types=[SmartDetectObjectType.PERSON], smart_detect_event_ids=[], - camera_id=camera.id, - api=mock_entry.api, + camera_id=doorbell.id, + api=ufp.api, ) - new_bootstrap = copy(mock_entry.api.bootstrap) - new_camera = camera.copy() + new_camera = doorbell.copy() new_camera.is_smart_detected = True new_camera.last_smart_detect_event_id = event.id @@ -566,10 +447,9 @@ async def test_sensor_update_motion( mock_msg.changed_data = {} mock_msg.new_obj = event - new_bootstrap.cameras = {new_camera.id: new_camera} - new_bootstrap.events = {event.id: event} - mock_entry.api.bootstrap = new_bootstrap - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.cameras = {new_camera.id: new_camera} + ufp.api.bootstrap.events = {event.id: event} + ufp.ws_msg(mock_msg) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -580,31 +460,31 @@ async def test_sensor_update_motion( async def test_sensor_update_alarm( - hass: HomeAssistant, mock_entry: MockEntityFixture, sensor: Sensor, now: datetime + hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor, fixed_now: datetime ): """Test sensor motion entity.""" - # 5 from all, 5 from sense, 12 NVR + + await init_entry(hass, ufp, [sensor_all]) assert_entity_counts(hass, Platform.SENSOR, 22, 14) _, entity_id = ids_from_device_description( - Platform.SENSOR, sensor, SENSE_SENSORS_WRITE[4] + Platform.SENSOR, sensor_all, SENSE_SENSORS_WRITE[4] ) - event_metadata = EventMetadata(sensor_id=sensor.id, alarm_type="smoke") + event_metadata = EventMetadata(sensor_id=sensor_all.id, alarm_type="smoke") event = Event( id="test_event_id", type=EventType.SENSOR_ALARM, - start=now - timedelta(seconds=1), + start=fixed_now - timedelta(seconds=1), end=None, score=100, smart_detect_types=[], smart_detect_event_ids=[], metadata=event_metadata, - api=mock_entry.api, + api=ufp.api, ) - new_bootstrap = copy(mock_entry.api.bootstrap) - new_sensor = sensor.copy() + new_sensor = sensor_all.copy() new_sensor.set_alarm_timeout() new_sensor.last_alarm_event_id = event.id @@ -612,10 +492,9 @@ async def test_sensor_update_alarm( mock_msg.changed_data = {} mock_msg.new_obj = event - new_bootstrap.sensors = {new_sensor.id: new_sensor} - new_bootstrap.events = {event.id: event} - mock_entry.api.bootstrap = new_bootstrap - mock_entry.api.ws_subscription(mock_msg) + ufp.api.bootstrap.sensors = {new_sensor.id: new_sensor} + ufp.api.bootstrap.events = {event.id: event} + ufp.ws_msg(mock_msg) await hass.async_block_till_done() state = hass.states.get(entity_id) @@ -627,15 +506,18 @@ async def test_sensor_update_alarm( async def test_sensor_update_alarm_with_last_trip_time( hass: HomeAssistant, entity_registry_enabled_by_default: AsyncMock, - mock_entry: MockEntityFixture, - sensor: Sensor, - now: datetime, + ufp: MockUFPFixture, + sensor_all: Sensor, + fixed_now: datetime, ): """Test sensor motion entity with last trip time.""" + await init_entry(hass, ufp, [sensor_all]) + assert_entity_counts(hass, Platform.SENSOR, 22, 22) + # Last Trip Time unique_id, entity_id = ids_from_device_description( - Platform.SENSOR, sensor, SENSE_SENSORS_WRITE[-3] + Platform.SENSOR, sensor_all, SENSE_SENSORS_WRITE[-3] ) entity_registry = er.async_get(hass) @@ -645,5 +527,8 @@ async def test_sensor_update_alarm_with_last_trip_time( state = hass.states.get(entity_id) assert state - assert state.state == "2022-01-04T04:03:56+00:00" + assert ( + state.state + == (fixed_now - timedelta(hours=1)).replace(microsecond=0).isoformat() + ) assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION diff --git a/tests/components/unifiprotect/test_services.py b/tests/components/unifiprotect/test_services.py index d957fe16b4b..460ba488cb2 100644 --- a/tests/components/unifiprotect/test_services.py +++ b/tests/components/unifiprotect/test_services.py @@ -6,7 +6,6 @@ from unittest.mock import AsyncMock, Mock import pytest from pyunifiprotect.data import Camera, Chime, Light, ModelType -from pyunifiprotect.data.bootstrap import ProtectDeviceRef from pyunifiprotect.exceptions import BadRequest from homeassistant.components.unifiprotect.const import ATTR_MESSAGE, DOMAIN @@ -21,15 +20,14 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er -from .conftest import MockEntityFixture, regenerate_device_ids +from .utils import MockUFPFixture, init_entry @pytest.fixture(name="device") -async def device_fixture(hass: HomeAssistant, mock_entry: MockEntityFixture): +async def device_fixture(hass: HomeAssistant, ufp: MockUFPFixture): """Fixture with entry setup to call services with.""" - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() + await init_entry(hass, ufp, []) device_registry = dr.async_get(hass) @@ -37,30 +35,20 @@ async def device_fixture(hass: HomeAssistant, mock_entry: MockEntityFixture): @pytest.fixture(name="subdevice") -async def subdevice_fixture( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light -): +async def subdevice_fixture(hass: HomeAssistant, ufp: MockUFPFixture, light: Light): """Fixture with entry setup to call services with.""" - mock_light._api = mock_entry.api - mock_entry.api.bootstrap.lights = { - mock_light.id: mock_light, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() + await init_entry(hass, ufp, [light]) device_registry = dr.async_get(hass) return [d for d in device_registry.devices.values() if d.name != "UnifiProtect"][0] -async def test_global_service_bad_device( - hass: HomeAssistant, device: dr.DeviceEntry, mock_entry: MockEntityFixture -): +async def test_global_service_bad_device(hass: HomeAssistant, ufp: MockUFPFixture): """Test global service, invalid device ID.""" - nvr = mock_entry.api.bootstrap.nvr + nvr = ufp.api.bootstrap.nvr nvr.__fields__["add_custom_doorbell_message"] = Mock() nvr.add_custom_doorbell_message = AsyncMock() @@ -75,11 +63,11 @@ async def test_global_service_bad_device( async def test_global_service_exception( - hass: HomeAssistant, device: dr.DeviceEntry, mock_entry: MockEntityFixture + hass: HomeAssistant, device: dr.DeviceEntry, ufp: MockUFPFixture ): """Test global service, unexpected error.""" - nvr = mock_entry.api.bootstrap.nvr + nvr = ufp.api.bootstrap.nvr nvr.__fields__["add_custom_doorbell_message"] = Mock() nvr.add_custom_doorbell_message = AsyncMock(side_effect=BadRequest) @@ -94,11 +82,11 @@ async def test_global_service_exception( async def test_add_doorbell_text( - hass: HomeAssistant, device: dr.DeviceEntry, mock_entry: MockEntityFixture + hass: HomeAssistant, device: dr.DeviceEntry, ufp: MockUFPFixture ): """Test add_doorbell_text service.""" - nvr = mock_entry.api.bootstrap.nvr + nvr = ufp.api.bootstrap.nvr nvr.__fields__["add_custom_doorbell_message"] = Mock() nvr.add_custom_doorbell_message = AsyncMock() @@ -112,11 +100,11 @@ async def test_add_doorbell_text( async def test_remove_doorbell_text( - hass: HomeAssistant, subdevice: dr.DeviceEntry, mock_entry: MockEntityFixture + hass: HomeAssistant, subdevice: dr.DeviceEntry, ufp: MockUFPFixture ): """Test remove_doorbell_text service.""" - nvr = mock_entry.api.bootstrap.nvr + nvr = ufp.api.bootstrap.nvr nvr.__fields__["remove_custom_doorbell_message"] = Mock() nvr.remove_custom_doorbell_message = AsyncMock() @@ -130,11 +118,11 @@ async def test_remove_doorbell_text( async def test_set_default_doorbell_text( - hass: HomeAssistant, device: dr.DeviceEntry, mock_entry: MockEntityFixture + hass: HomeAssistant, device: dr.DeviceEntry, ufp: MockUFPFixture ): """Test set_default_doorbell_text service.""" - nvr = mock_entry.api.bootstrap.nvr + nvr = ufp.api.bootstrap.nvr nvr.__fields__["set_default_doorbell_message"] = Mock() nvr.set_default_doorbell_message = AsyncMock() @@ -149,57 +137,21 @@ async def test_set_default_doorbell_text( async def test_set_chime_paired_doorbells( hass: HomeAssistant, - mock_entry: MockEntityFixture, - mock_chime: Chime, - mock_camera: Camera, + ufp: MockUFPFixture, + chime: Chime, + doorbell: Camera, ): """Test set_chime_paired_doorbells.""" - mock_entry.api.update_device = AsyncMock() + ufp.api.update_device = AsyncMock() - mock_chime._api = mock_entry.api - mock_chime.name = "Test Chime" - mock_chime._initial_data = mock_chime.dict() - mock_entry.api.bootstrap.chimes = { - mock_chime.id: mock_chime, - } - mock_entry.api.bootstrap.mac_lookup = { - mock_chime.mac.lower(): ProtectDeviceRef( - model=mock_chime.model, id=mock_chime.id - ) - } - - camera1 = mock_camera.copy() + camera1 = doorbell.copy() camera1.name = "Test Camera 1" - camera1._api = mock_entry.api - camera1.channels[0]._api = mock_entry.api - camera1.channels[1]._api = mock_entry.api - camera1.channels[2]._api = mock_entry.api - camera1.feature_flags.has_chime = True - regenerate_device_ids(camera1) - camera2 = mock_camera.copy() + camera2 = doorbell.copy() camera2.name = "Test Camera 2" - camera2._api = mock_entry.api - camera2.channels[0]._api = mock_entry.api - camera2.channels[1]._api = mock_entry.api - camera2.channels[2]._api = mock_entry.api - camera2.feature_flags.has_chime = True - regenerate_device_ids(camera2) - mock_entry.api.bootstrap.cameras = { - camera1.id: camera1, - camera2.id: camera2, - } - mock_entry.api.bootstrap.mac_lookup[camera1.mac.lower()] = ProtectDeviceRef( - model=camera1.model, id=camera1.id - ) - mock_entry.api.bootstrap.mac_lookup[camera2.mac.lower()] = ProtectDeviceRef( - model=camera2.model, id=camera2.id - ) - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() + await init_entry(hass, ufp, [camera1, camera2, chime]) registry = er.async_get(hass) chime_entry = registry.async_get("button.test_chime_play_chime") @@ -220,6 +172,6 @@ async def test_set_chime_paired_doorbells( blocking=True, ) - mock_entry.api.update_device.assert_called_once_with( - ModelType.CHIME, mock_chime.id, {"cameraIds": sorted([camera1.id, camera2.id])} + ufp.api.update_device.assert_called_once_with( + ModelType.CHIME, chime.id, {"cameraIds": sorted([camera1.id, camera2.id])} ) diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 6c9340af5d5..0c45ec28b7b 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -5,14 +5,7 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock import pytest -from pyunifiprotect.data import ( - Camera, - Light, - Permission, - RecordingMode, - SmartDetectObjectType, - VideoMode, -) +from pyunifiprotect.data import Camera, Light, Permission, RecordingMode, VideoMode from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.switch import ( @@ -24,12 +17,12 @@ from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, STATE_OFF, Pla from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .conftest import ( - MockEntityFixture, +from .utils import ( + MockUFPFixture, assert_entity_counts, enable_entity, ids_from_device_description, - reset_objects, + init_entry, ) CAMERA_SWITCHES_BASIC = [ @@ -44,218 +37,33 @@ CAMERA_SWITCHES_NO_EXTRA = [ ] -@pytest.fixture(name="light") -async def light_fixture( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_light: Light -): - """Fixture for a single light for testing the switch platform.""" - - # disable pydantic validation so mocking can happen - Light.__config__.validate_assignment = False - - light_obj = mock_light.copy() - light_obj._api = mock_entry.api - light_obj.name = "Test Light" - light_obj.is_ssh_enabled = False - light_obj.light_device_settings.is_indicator_enabled = False - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.lights = { - light_obj.id: light_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.SWITCH, 2, 1) - - yield light_obj - - Light.__config__.validate_assignment = True - - -@pytest.fixture(name="camera") -async def camera_fixture( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera -): - """Fixture for a single camera for testing the switch platform.""" - - # disable pydantic validation so mocking can happen - Camera.__config__.validate_assignment = False - - camera_obj = mock_camera.copy() - camera_obj._api = mock_entry.api - camera_obj.channels[0]._api = mock_entry.api - camera_obj.channels[1]._api = mock_entry.api - camera_obj.channels[2]._api = mock_entry.api - camera_obj.name = "Test Camera" - camera_obj.recording_settings.mode = RecordingMode.DETECTIONS - camera_obj.feature_flags.has_led_status = True - camera_obj.feature_flags.has_hdr = True - camera_obj.feature_flags.video_modes = [VideoMode.DEFAULT, VideoMode.HIGH_FPS] - camera_obj.feature_flags.has_privacy_mask = True - camera_obj.feature_flags.has_speaker = True - camera_obj.feature_flags.has_smart_detect = True - camera_obj.feature_flags.smart_detect_types = [ - SmartDetectObjectType.PERSON, - SmartDetectObjectType.VEHICLE, - ] - camera_obj.is_ssh_enabled = False - camera_obj.led_settings.is_enabled = False - camera_obj.hdr_mode = False - camera_obj.video_mode = VideoMode.DEFAULT - camera_obj.remove_privacy_zone() - camera_obj.speaker_settings.are_system_sounds_enabled = False - camera_obj.osd_settings.is_name_enabled = False - camera_obj.osd_settings.is_date_enabled = False - camera_obj.osd_settings.is_logo_enabled = False - camera_obj.osd_settings.is_debug_enabled = False - camera_obj.smart_detect_settings.object_types = [] - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.cameras = { - camera_obj.id: camera_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.SWITCH, 13, 12) - - yield camera_obj - - Camera.__config__.validate_assignment = True - - -@pytest.fixture(name="camera_none") -async def camera_none_fixture( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera -): - """Fixture for a single camera for testing the switch platform.""" - - # disable pydantic validation so mocking can happen - Camera.__config__.validate_assignment = False - - camera_obj = mock_camera.copy() - camera_obj._api = mock_entry.api - camera_obj.channels[0]._api = mock_entry.api - camera_obj.channels[1]._api = mock_entry.api - camera_obj.channels[2]._api = mock_entry.api - camera_obj.name = "Test Camera" - camera_obj.recording_settings.mode = RecordingMode.DETECTIONS - camera_obj.feature_flags.has_led_status = False - camera_obj.feature_flags.has_hdr = False - camera_obj.feature_flags.video_modes = [VideoMode.DEFAULT] - camera_obj.feature_flags.has_privacy_mask = False - camera_obj.feature_flags.has_speaker = False - camera_obj.feature_flags.has_smart_detect = False - camera_obj.is_ssh_enabled = False - camera_obj.osd_settings.is_name_enabled = False - camera_obj.osd_settings.is_date_enabled = False - camera_obj.osd_settings.is_logo_enabled = False - camera_obj.osd_settings.is_debug_enabled = False - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.cameras = { - camera_obj.id: camera_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.SWITCH, 6, 5) - - yield camera_obj - - Camera.__config__.validate_assignment = True - - -@pytest.fixture(name="camera_privacy") -async def camera_privacy_fixture( - hass: HomeAssistant, mock_entry: MockEntityFixture, mock_camera: Camera -): - """Fixture for a single camera for testing the switch platform.""" - - # disable pydantic validation so mocking can happen - Camera.__config__.validate_assignment = False - - # mock_camera._update_lock = None - camera_obj = mock_camera.copy() - camera_obj._api = mock_entry.api - camera_obj.channels[0]._api = mock_entry.api - camera_obj.channels[1]._api = mock_entry.api - camera_obj.channels[2]._api = mock_entry.api - camera_obj.name = "Test Camera" - camera_obj.recording_settings.mode = RecordingMode.NEVER - camera_obj.feature_flags.has_led_status = False - camera_obj.feature_flags.has_hdr = False - camera_obj.feature_flags.video_modes = [VideoMode.DEFAULT] - camera_obj.feature_flags.has_privacy_mask = True - camera_obj.feature_flags.has_speaker = False - camera_obj.feature_flags.has_smart_detect = False - camera_obj.add_privacy_zone() - camera_obj.is_ssh_enabled = False - camera_obj.osd_settings.is_name_enabled = False - camera_obj.osd_settings.is_date_enabled = False - camera_obj.osd_settings.is_logo_enabled = False - camera_obj.osd_settings.is_debug_enabled = False - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.cameras = { - camera_obj.id: camera_obj, - } - - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() - - assert_entity_counts(hass, Platform.SWITCH, 7, 6) - - yield camera_obj - - Camera.__config__.validate_assignment = True - - async def test_switch_setup_no_perm( hass: HomeAssistant, - mock_entry: MockEntityFixture, - mock_light: Light, - mock_camera: Camera, + ufp: MockUFPFixture, + light: Light, + doorbell: Camera, ): """Test switch entity setup for light devices.""" - light_obj = mock_light.copy() - light_obj._api = mock_entry.api - - camera_obj = mock_camera.copy() - camera_obj._api = mock_entry.api - camera_obj.channels[0]._api = mock_entry.api - camera_obj.channels[1]._api = mock_entry.api - camera_obj.channels[2]._api = mock_entry.api - - reset_objects(mock_entry.api.bootstrap) - mock_entry.api.bootstrap.lights = { - light_obj.id: light_obj, - } - mock_entry.api.bootstrap.cameras = { - camera_obj.id: camera_obj, - } - mock_entry.api.bootstrap.auth_user.all_permissions = [ + ufp.api.bootstrap.auth_user.all_permissions = [ Permission.unifi_dict_to_dict({"rawPermission": "light:read:*"}) ] - await hass.config_entries.async_setup(mock_entry.entry.entry_id) - await hass.async_block_till_done() + await init_entry(hass, ufp, [light, doorbell]) assert_entity_counts(hass, Platform.SWITCH, 0, 0) async def test_switch_setup_light( hass: HomeAssistant, - mock_entry: MockEntityFixture, + ufp: MockUFPFixture, light: Light, ): """Test switch entity setup for light devices.""" + await init_entry(hass, ufp, [light]) + assert_entity_counts(hass, Platform.SWITCH, 2, 1) + entity_registry = er.async_get(hass) description = LIGHT_SWITCHES[1] @@ -283,7 +91,7 @@ async def test_switch_setup_light( assert entity.disabled is True assert entity.unique_id == unique_id - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await enable_entity(hass, ufp.entry.entry_id, entity_id) state = hass.states.get(entity_id) assert state @@ -293,14 +101,67 @@ async def test_switch_setup_light( async def test_switch_setup_camera_all( hass: HomeAssistant, - mock_entry: MockEntityFixture, - camera: Camera, + ufp: MockUFPFixture, + doorbell: Camera, ): """Test switch entity setup for camera devices (all enabled feature flags).""" + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.SWITCH, 13, 12) + entity_registry = er.async_get(hass) for description in CAMERA_SWITCHES_BASIC: + unique_id, entity_id = ids_from_device_description( + Platform.SWITCH, doorbell, description + ) + + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.unique_id == unique_id + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + + description = CAMERA_SWITCHES[0] + + description_entity_name = ( + description.name.lower().replace(":", "").replace(" ", "_") + ) + unique_id = f"{doorbell.mac}_{description.key}" + entity_id = f"switch.test_camera_{description_entity_name}" + + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.disabled is True + assert entity.unique_id == unique_id + + await enable_entity(hass, ufp.entry.entry_id, entity_id) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION + + +async def test_switch_setup_camera_none( + hass: HomeAssistant, + ufp: MockUFPFixture, + camera: Camera, +): + """Test switch entity setup for camera devices (no enabled feature flags).""" + + await init_entry(hass, ufp, [camera]) + assert_entity_counts(hass, Platform.SWITCH, 6, 5) + + entity_registry = er.async_get(hass) + + for description in CAMERA_SWITCHES_BASIC: + if description.ufp_required_field is not None: + continue + unique_id, entity_id = ids_from_device_description( Platform.SWITCH, camera, description ) @@ -327,7 +188,7 @@ async def test_switch_setup_camera_all( assert entity.disabled is True assert entity.unique_id == unique_id - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + await enable_entity(hass, ufp.entry.entry_id, entity_id) state = hass.states.get(entity_id) assert state @@ -335,56 +196,14 @@ async def test_switch_setup_camera_all( assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION -async def test_switch_setup_camera_none( - hass: HomeAssistant, - mock_entry: MockEntityFixture, - camera_none: Camera, +async def test_switch_light_status( + hass: HomeAssistant, ufp: MockUFPFixture, light: Light ): - """Test switch entity setup for camera devices (no enabled feature flags).""" - - entity_registry = er.async_get(hass) - - for description in CAMERA_SWITCHES_BASIC: - if description.ufp_required_field is not None: - continue - - unique_id, entity_id = ids_from_device_description( - Platform.SWITCH, camera_none, description - ) - - entity = entity_registry.async_get(entity_id) - assert entity - assert entity.unique_id == unique_id - - state = hass.states.get(entity_id) - assert state - assert state.state == STATE_OFF - assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION - - description = CAMERA_SWITCHES[0] - - description_entity_name = ( - description.name.lower().replace(":", "").replace(" ", "_") - ) - unique_id = f"{camera_none.mac}_{description.key}" - entity_id = f"switch.test_camera_{description_entity_name}" - - entity = entity_registry.async_get(entity_id) - assert entity - assert entity.disabled is True - assert entity.unique_id == unique_id - - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) - - state = hass.states.get(entity_id) - assert state - assert state.state == STATE_OFF - assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION - - -async def test_switch_light_status(hass: HomeAssistant, light: Light): """Tests status light switch for lights.""" + await init_entry(hass, ufp, [light]) + assert_entity_counts(hass, Platform.SWITCH, 2, 1) + description = LIGHT_SWITCHES[1] light.__fields__["set_status_light"] = Mock() @@ -406,44 +225,53 @@ async def test_switch_light_status(hass: HomeAssistant, light: Light): async def test_switch_camera_ssh( - hass: HomeAssistant, camera: Camera, mock_entry: MockEntityFixture + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ): """Tests SSH switch for cameras.""" + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.SWITCH, 13, 12) + description = CAMERA_SWITCHES[0] - camera.__fields__["set_ssh"] = Mock() - camera.set_ssh = AsyncMock() + doorbell.__fields__["set_ssh"] = Mock() + doorbell.set_ssh = AsyncMock() - _, entity_id = ids_from_device_description(Platform.SWITCH, camera, description) - await enable_entity(hass, mock_entry.entry.entry_id, entity_id) + _, entity_id = ids_from_device_description(Platform.SWITCH, doorbell, description) + await enable_entity(hass, ufp.entry.entry_id, entity_id) await hass.services.async_call( "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - camera.set_ssh.assert_called_once_with(True) + doorbell.set_ssh.assert_called_once_with(True) await hass.services.async_call( "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - camera.set_ssh.assert_called_with(False) + doorbell.set_ssh.assert_called_with(False) @pytest.mark.parametrize("description", CAMERA_SWITCHES_NO_EXTRA) async def test_switch_camera_simple( - hass: HomeAssistant, camera: Camera, description: ProtectSwitchEntityDescription + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + description: ProtectSwitchEntityDescription, ): """Tests all simple switches for cameras.""" + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.SWITCH, 13, 12) + assert description.ufp_set_method is not None - camera.__fields__[description.ufp_set_method] = Mock() - setattr(camera, description.ufp_set_method, AsyncMock()) - set_method = getattr(camera, description.ufp_set_method) + doorbell.__fields__[description.ufp_set_method] = Mock() + setattr(doorbell, description.ufp_set_method, AsyncMock()) + set_method = getattr(doorbell, description.ufp_set_method) - _, entity_id = ids_from_device_description(Platform.SWITCH, camera, description) + _, entity_id = ids_from_device_description(Platform.SWITCH, doorbell, description) await hass.services.async_call( "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True @@ -458,70 +286,82 @@ async def test_switch_camera_simple( set_method.assert_called_with(False) -async def test_switch_camera_highfps(hass: HomeAssistant, camera: Camera): +async def test_switch_camera_highfps( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera +): """Tests High FPS switch for cameras.""" + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.SWITCH, 13, 12) + description = CAMERA_SWITCHES[3] - camera.__fields__["set_video_mode"] = Mock() - camera.set_video_mode = AsyncMock() + doorbell.__fields__["set_video_mode"] = Mock() + doorbell.set_video_mode = AsyncMock() - _, entity_id = ids_from_device_description(Platform.SWITCH, camera, description) + _, entity_id = ids_from_device_description(Platform.SWITCH, doorbell, description) await hass.services.async_call( "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - camera.set_video_mode.assert_called_once_with(VideoMode.HIGH_FPS) + doorbell.set_video_mode.assert_called_once_with(VideoMode.HIGH_FPS) await hass.services.async_call( "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - camera.set_video_mode.assert_called_with(VideoMode.DEFAULT) + doorbell.set_video_mode.assert_called_with(VideoMode.DEFAULT) -async def test_switch_camera_privacy(hass: HomeAssistant, camera: Camera): +async def test_switch_camera_privacy( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera +): """Tests Privacy Mode switch for cameras.""" + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.SWITCH, 13, 12) + description = CAMERA_SWITCHES[4] - camera.__fields__["set_privacy"] = Mock() - camera.set_privacy = AsyncMock() + doorbell.__fields__["set_privacy"] = Mock() + doorbell.set_privacy = AsyncMock() - _, entity_id = ids_from_device_description(Platform.SWITCH, camera, description) + _, entity_id = ids_from_device_description(Platform.SWITCH, doorbell, description) await hass.services.async_call( "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - camera.set_privacy.assert_called_once_with(True, 0, RecordingMode.NEVER) + doorbell.set_privacy.assert_called_once_with(True, 0, RecordingMode.NEVER) await hass.services.async_call( "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - camera.set_privacy.assert_called_with( - False, camera.mic_volume, camera.recording_settings.mode + doorbell.set_privacy.assert_called_with( + False, doorbell.mic_volume, doorbell.recording_settings.mode ) async def test_switch_camera_privacy_already_on( - hass: HomeAssistant, camera_privacy: Camera + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ): """Tests Privacy Mode switch for cameras with privacy mode defaulted on.""" + doorbell.add_privacy_zone() + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.SWITCH, 13, 12) + description = CAMERA_SWITCHES[4] - camera_privacy.__fields__["set_privacy"] = Mock() - camera_privacy.set_privacy = AsyncMock() + doorbell.__fields__["set_privacy"] = Mock() + doorbell.set_privacy = AsyncMock() - _, entity_id = ids_from_device_description( - Platform.SWITCH, camera_privacy, description - ) + _, entity_id = ids_from_device_description(Platform.SWITCH, doorbell, description) await hass.services.async_call( "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - camera_privacy.set_privacy.assert_called_once_with(False, 100, RecordingMode.ALWAYS) + doorbell.set_privacy.assert_called_once_with(False, 100, RecordingMode.ALWAYS) diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py new file mode 100644 index 00000000000..517da9e73c6 --- /dev/null +++ b/tests/components/unifiprotect/utils.py @@ -0,0 +1,168 @@ +"""Test helpers for UniFi Protect.""" +# pylint: disable=protected-access +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta +from typing import Any, Callable, Sequence + +from pyunifiprotect import ProtectApiClient +from pyunifiprotect.data import ( + Bootstrap, + Camera, + ProtectAdoptableDeviceModel, + WSSubscriptionMessage, +) +from pyunifiprotect.data.bootstrap import ProtectDeviceRef +from pyunifiprotect.test_util.anonymize import random_hex + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, split_entity_id +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import EntityDescription +import homeassistant.util.dt as dt_util + +from tests.common import MockConfigEntry, async_fire_time_changed + + +@dataclass +class MockUFPFixture: + """Mock for NVR.""" + + entry: MockConfigEntry + api: ProtectApiClient + ws_subscription: Callable[[WSSubscriptionMessage], None] | None = None + + def ws_msg(self, msg: WSSubscriptionMessage) -> Any: + """Emit WS message for testing.""" + + if self.ws_subscription is not None: + return self.ws_subscription(msg) + + +def reset_objects(bootstrap: Bootstrap): + """Reset bootstrap objects.""" + + bootstrap.cameras = {} + bootstrap.lights = {} + bootstrap.sensors = {} + bootstrap.viewers = {} + bootstrap.events = {} + bootstrap.doorlocks = {} + bootstrap.chimes = {} + + +async def time_changed(hass: HomeAssistant, seconds: int) -> None: + """Trigger time changed.""" + next_update = dt_util.utcnow() + timedelta(seconds) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + +async def enable_entity( + hass: HomeAssistant, entry_id: str, entity_id: str +) -> er.RegistryEntry: + """Enable a disabled entity.""" + entity_registry = er.async_get(hass) + + updated_entity = entity_registry.async_update_entity(entity_id, disabled_by=None) + assert not updated_entity.disabled + await hass.config_entries.async_reload(entry_id) + await hass.async_block_till_done() + + return updated_entity + + +def assert_entity_counts( + hass: HomeAssistant, platform: Platform, total: int, enabled: int +) -> None: + """Assert entity counts for a given platform.""" + + entity_registry = er.async_get(hass) + + entities = [ + e for e in entity_registry.entities if split_entity_id(e)[0] == platform.value + ] + + assert len(entities) == total + assert len(hass.states.async_all(platform.value)) == enabled + + +def normalize_name(name: str) -> str: + """Normalize name.""" + + return name.lower().replace(":", "").replace(" ", "_").replace("-", "_") + + +def ids_from_device_description( + platform: Platform, + device: ProtectAdoptableDeviceModel, + description: EntityDescription, +) -> tuple[str, str]: + """Return expected unique_id and entity_id for a give platform/device/description combination.""" + + entity_name = normalize_name(device.display_name) + description_entity_name = normalize_name(str(description.name)) + + unique_id = f"{device.mac}_{description.key}" + entity_id = f"{platform.value}.{entity_name}_{description_entity_name}" + + return unique_id, entity_id + + +def generate_random_ids() -> tuple[str, str]: + """Generate random IDs for device.""" + + return random_hex(24).lower(), random_hex(12).upper() + + +def regenerate_device_ids(device: ProtectAdoptableDeviceModel) -> None: + """Regenerate the IDs on UFP device.""" + + device.id, device.mac = generate_random_ids() + + +def add_device_ref(bootstrap: Bootstrap, device: ProtectAdoptableDeviceModel) -> None: + """Manually add device ref to bootstrap for lookup.""" + + ref = ProtectDeviceRef(id=device.id, model=device.model) + bootstrap.id_lookup[device.id] = ref + bootstrap.mac_lookup[device.mac.lower()] = ref + + +def add_device( + bootstrap: Bootstrap, device: ProtectAdoptableDeviceModel, regenerate_ids: bool +) -> None: + """Add test device to bootstrap.""" + + if device.model is None: + return + + device._api = bootstrap.api + if isinstance(device, Camera): + for channel in device.channels: + channel._api = bootstrap.api + + if regenerate_ids: + regenerate_device_ids(device) + device._initial_data = device.dict() + + devices = getattr(bootstrap, f"{device.model.value}s") + devices[device.id] = device + add_device_ref(bootstrap, device) + + +async def init_entry( + hass: HomeAssistant, + ufp: MockUFPFixture, + devices: Sequence[ProtectAdoptableDeviceModel], + regenerate_ids: bool = True, +) -> None: + """Initialize Protect entry with given devices.""" + + reset_objects(ufp.api.bootstrap) + for device in devices: + add_device(ufp.api.bootstrap, device, regenerate_ids) + + await hass.config_entries.async_setup(ufp.entry.entry_id) + await hass.async_block_till_done() From 15ed329108af095e21f1d8eb987430a7a6e707d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Jun 2022 11:02:38 -0500 Subject: [PATCH 1791/3516] Add async_remove_config_entry_device support to nexia (#73966) Co-authored-by: Paulus Schoutsen --- homeassistant/components/nexia/__init__.py | 23 +++++++-- tests/components/nexia/test_init.py | 60 ++++++++++++++++++++++ 2 files changed, 80 insertions(+), 3 deletions(-) create mode 100644 tests/components/nexia/test_init.py diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index ab491c0a271..355c17a2ed1 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -5,11 +5,13 @@ import logging import aiohttp from nexia.const import BRAND_NEXIA from nexia.home import NexiaHome +from nexia.thermostat import NexiaThermostat from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -66,8 +68,23 @@ 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) - if unload_ok: + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove a nexia config entry from a device.""" + coordinator: NexiaDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + nexia_home: NexiaHome = coordinator.nexia_home + dev_ids = {dev_id[1] for dev_id in device_entry.identifiers if dev_id[0] == DOMAIN} + for thermostat_id in nexia_home.get_thermostat_ids(): + if thermostat_id in dev_ids: + return False + thermostat: NexiaThermostat = nexia_home.get_thermostat_by_id(thermostat_id) + for zone_id in thermostat.get_zone_ids(): + if zone_id in dev_ids: + return False + return True diff --git a/tests/components/nexia/test_init.py b/tests/components/nexia/test_init.py new file mode 100644 index 00000000000..667c03a23cf --- /dev/null +++ b/tests/components/nexia/test_init.py @@ -0,0 +1,60 @@ +"""The init tests for the nexia platform.""" + + +from homeassistant.components.nexia.const import DOMAIN +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity_registry import EntityRegistry +from homeassistant.setup import async_setup_component + +from .util import async_init_integration + + +async def remove_device(ws_client, device_id, config_entry_id): + """Remove config entry from a device.""" + await ws_client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry_id, + "device_id": device_id, + } + ) + response = await ws_client.receive_json() + return response["success"] + + +async def test_device_remove_devices(hass, hass_ws_client): + """Test we can only remove a device that no longer exists.""" + await async_setup_component(hass, "config", {}) + config_entry = await async_init_integration(hass) + entry_id = config_entry.entry_id + device_registry = dr.async_get(hass) + + registry: EntityRegistry = er.async_get(hass) + entity = registry.entities["sensor.nick_office_temperature"] + + live_zone_device_entry = device_registry.async_get(entity.device_id) + assert ( + await remove_device( + await hass_ws_client(hass), live_zone_device_entry.id, entry_id + ) + is False + ) + + entity = registry.entities["sensor.master_suite_relative_humidity"] + live_thermostat_device_entry = device_registry.async_get(entity.device_id) + assert ( + await remove_device( + await hass_ws_client(hass), live_thermostat_device_entry.id, entry_id + ) + is False + ) + + dead_device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, "unused")}, + ) + assert ( + await remove_device(await hass_ws_client(hass), dead_device_entry.id, entry_id) + is True + ) From 949922ef2cb7a7c6e4a83cb91856c70728acd806 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Jun 2022 11:19:11 -0500 Subject: [PATCH 1792/3516] Fix exception when as_dict is called on a TemplateState (#73984) --- homeassistant/helpers/template.py | 4 +++- tests/helpers/test_template.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index eca76a8c7bc..53e2eb122f6 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -5,7 +5,7 @@ from ast import literal_eval import asyncio import base64 import collections.abc -from collections.abc import Callable, Generator, Iterable +from collections.abc import Callable, Collection, Generator, Iterable from contextlib import contextmanager, suppress from contextvars import ContextVar from datetime import datetime, timedelta @@ -54,6 +54,7 @@ from homeassistant.util import ( slugify as slugify_util, ) from homeassistant.util.async_ import run_callback_threadsafe +from homeassistant.util.read_only_dict import ReadOnlyDict from homeassistant.util.thread import ThreadWithException from . import area_registry, device_registry, entity_registry, location as loc_helper @@ -764,6 +765,7 @@ class TemplateStateBase(State): self._hass = hass self._collect = collect self._entity_id = entity_id + self._as_dict: ReadOnlyDict[str, Collection[Any]] | None = None def _collect_state(self) -> None: if self._collect and _RENDER_INFO in self._hass.data: diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 69a3af22759..59b653bc23e 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -26,6 +26,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError from homeassistant.helpers import device_registry as dr, entity, template from homeassistant.helpers.entity_platform import EntityPlatform +from homeassistant.helpers.json import json_dumps from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import UnitSystem @@ -3841,3 +3842,12 @@ async def test_template_states_blocks_setitem(hass): template_state = template.TemplateState(hass, state, True) with pytest.raises(RuntimeError): template_state["any"] = "any" + + +async def test_template_states_can_serialize(hass): + """Test TemplateState is serializable.""" + hass.states.async_set("light.new", STATE_ON) + state = hass.states.get("light.new") + template_state = template.TemplateState(hass, state, True) + assert template_state.as_dict() is template_state.as_dict() + assert json_dumps(template_state) == json_dumps(template_state) From f78d209f9387386f0888fc791b822b6de90f12b4 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Sat, 25 Jun 2022 19:26:57 +0200 Subject: [PATCH 1793/3516] Bump bimmer_connected to 0.9.6 (#73977) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 40af0b9e210..b10d4842163 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.9.5"], + "requirements": ["bimmer_connected==0.9.6"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index c76bc5c26f3..656cd4d5bcd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -393,7 +393,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.5 +bimmer_connected==0.9.6 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c50b7ee62b..4728716b779 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -308,7 +308,7 @@ beautifulsoup4==4.11.1 bellows==0.30.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.5 +bimmer_connected==0.9.6 # homeassistant.components.blebox blebox_uniapi==1.3.3 From ce144bf63145813c76fbbe4f9423341764695057 Mon Sep 17 00:00:00 2001 From: Khole Date: Sat, 25 Jun 2022 23:13:30 +0100 Subject: [PATCH 1794/3516] Add Hive device configuration to config flow (#73955) Co-authored-by: Martin Hjelmare --- homeassistant/components/hive/config_flow.py | 37 +++- homeassistant/components/hive/const.py | 1 + homeassistant/components/hive/manifest.json | 2 +- homeassistant/components/hive/strings.json | 9 +- .../components/hive/translations/en.json | 9 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/hive/test_config_flow.py | 162 +++++++++++++++--- 8 files changed, 190 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/hive/config_flow.py b/homeassistant/components/hive/config_flow.py index 16d83dc311d..5368aa22c3f 100644 --- a/homeassistant/components/hive/config_flow.py +++ b/homeassistant/components/hive/config_flow.py @@ -14,7 +14,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.core import callback -from .const import CONF_CODE, CONFIG_ENTRY_VERSION, DOMAIN +from .const import CONF_CODE, CONF_DEVICE_NAME, CONFIG_ENTRY_VERSION, DOMAIN class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -29,6 +29,7 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.tokens = {} self.entry = None self.device_registration = False + self.device_name = "Home Assistant" async def async_step_user(self, user_input=None): """Prompt user input. Create or edit entry.""" @@ -60,7 +61,7 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_2fa() if not errors: - # Complete the entry setup. + # Complete the entry. try: return await self.async_setup_hive_entry() except UnknownHiveError: @@ -89,15 +90,36 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "no_internet_available" if not errors: - try: - self.device_registration = True + if self.context["source"] == config_entries.SOURCE_REAUTH: return await self.async_setup_hive_entry() - except UnknownHiveError: - errors["base"] = "unknown" + self.device_registration = True + return await self.async_step_configuration() schema = vol.Schema({vol.Required(CONF_CODE): str}) return self.async_show_form(step_id="2fa", data_schema=schema, errors=errors) + async def async_step_configuration(self, user_input=None): + """Handle hive configuration step.""" + errors = {} + + if user_input: + if self.device_registration: + self.device_name = user_input["device_name"] + await self.hive_auth.device_registration(user_input["device_name"]) + self.data["device_data"] = await self.hive_auth.get_device_data() + + try: + return await self.async_setup_hive_entry() + except UnknownHiveError: + errors["base"] = "unknown" + + schema = vol.Schema( + {vol.Optional(CONF_DEVICE_NAME, default=self.device_name): str} + ) + return self.async_show_form( + step_id="configuration", data_schema=schema, errors=errors + ) + async def async_setup_hive_entry(self): """Finish setup and create the config entry.""" @@ -105,9 +127,6 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): raise UnknownHiveError # Setup the config entry - if self.device_registration: - await self.hive_auth.device_registration("Home Assistant") - self.data["device_data"] = await self.hive_auth.getDeviceData() self.data["tokens"] = self.tokens if self.context["source"] == config_entries.SOURCE_REAUTH: self.hass.config_entries.async_update_entry( diff --git a/homeassistant/components/hive/const.py b/homeassistant/components/hive/const.py index 82c07761eef..b7a2be6910f 100644 --- a/homeassistant/components/hive/const.py +++ b/homeassistant/components/hive/const.py @@ -5,6 +5,7 @@ ATTR_MODE = "mode" ATTR_TIME_PERIOD = "time_period" ATTR_ONOFF = "on_off" CONF_CODE = "2fa" +CONF_DEVICE_NAME = "device_name" CONFIG_ENTRY_VERSION = 1 DEFAULT_NAME = "Hive" DOMAIN = "hive" diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 29477bf7414..406b32d86f8 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -6,7 +6,7 @@ "models": ["HHKBridge*"] }, "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": ["pyhiveapi==0.5.11"], + "requirements": ["pyhiveapi==0.5.13"], "codeowners": ["@Rendili", "@KJonline"], "iot_class": "cloud_polling", "loggers": ["apyhiveapi"] diff --git a/homeassistant/components/hive/strings.json b/homeassistant/components/hive/strings.json index 7628abc5b06..3435517aec7 100644 --- a/homeassistant/components/hive/strings.json +++ b/homeassistant/components/hive/strings.json @@ -3,7 +3,7 @@ "step": { "user": { "title": "Hive Login", - "description": "Enter your Hive login information and configuration.", + "description": "Enter your Hive login information.", "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", @@ -17,6 +17,13 @@ "2fa": "Two-factor code" } }, + "configuration": { + "data": { + "device_name": "Device Name" + }, + "description": "Enter your Hive configuration ", + "title": "Hive Configuration." + }, "reauth": { "title": "Hive Login", "description": "Re-enter your Hive login information.", diff --git a/homeassistant/components/hive/translations/en.json b/homeassistant/components/hive/translations/en.json index 32453da0a0c..3ef7b3d0f43 100644 --- a/homeassistant/components/hive/translations/en.json +++ b/homeassistant/components/hive/translations/en.json @@ -20,6 +20,13 @@ "description": "Enter your Hive authentication code. \n \n Please enter code 0000 to request another code.", "title": "Hive Two-factor Authentication." }, + "configuration": { + "data": { + "device_name": "Device Name" + }, + "description": "Enter your Hive configuration ", + "title": "Hive Configuration." + }, "reauth": { "data": { "password": "Password", @@ -34,7 +41,7 @@ "scan_interval": "Scan Interval (seconds)", "username": "Username" }, - "description": "Enter your Hive login information and configuration.", + "description": "Enter your Hive login information.", "title": "Hive Login" } } diff --git a/requirements_all.txt b/requirements_all.txt index 656cd4d5bcd..dca486c0d3f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1541,7 +1541,7 @@ pyheos==0.7.2 pyhik==0.3.0 # homeassistant.components.hive -pyhiveapi==0.5.11 +pyhiveapi==0.5.13 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4728716b779..73f98c5eafc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1035,7 +1035,7 @@ pyhaversion==22.4.1 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.5.11 +pyhiveapi==0.5.13 # homeassistant.components.homematic pyhomematic==0.1.77 diff --git a/tests/components/hive/test_config_flow.py b/tests/components/hive/test_config_flow.py index 35e20e8eee3..e6e2a06501a 100644 --- a/tests/components/hive/test_config_flow.py +++ b/tests/components/hive/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from apyhiveapi.helper import hive_exceptions from homeassistant import config_entries, data_entry_flow -from homeassistant.components.hive.const import CONF_CODE, DOMAIN +from homeassistant.components.hive.const import CONF_CODE, CONF_DEVICE_NAME, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from tests.common import MockConfigEntry @@ -16,6 +16,7 @@ UPDATED_PASSWORD = "updated-password" INCORRECT_PASSWORD = "incorrect-password" SCAN_INTERVAL = 120 UPDATED_SCAN_INTERVAL = 60 +DEVICE_NAME = "Test Home Assistant" MFA_CODE = "1234" MFA_RESEND_CODE = "0000" MFA_INVALID_CODE = "HIVE" @@ -148,11 +149,23 @@ async def test_user_flow_2fa(hass): "AccessToken": "mock-access-token", }, }, - ), patch( + ): + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_CODE: MFA_CODE, + }, + ) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["step_id"] == "configuration" + assert result3["errors"] == {} + + with patch( "homeassistant.components.hive.config_flow.Auth.device_registration", return_value=True, ), patch( - "homeassistant.components.hive.config_flow.Auth.getDeviceData", + "homeassistant.components.hive.config_flow.Auth.get_device_data", return_value=[ "mock-device-group-key", "mock-device-key", @@ -164,14 +177,17 @@ async def test_user_flow_2fa(hass): "homeassistant.components.hive.async_setup_entry", return_value=True, ) as mock_setup_entry: - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], {CONF_CODE: MFA_CODE} + result4 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_DEVICE_NAME: DEVICE_NAME, + }, ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result3["title"] == USERNAME - assert result3["data"] == { + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["title"] == USERNAME + assert result4["data"] == { CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, "tokens": { @@ -235,9 +251,6 @@ async def test_reauth_flow(hass): "AccessToken": "mock-access-token", }, }, - ), patch( - "homeassistant.components.hive.config_flow.Auth.device_registration", - return_value=True, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -255,6 +268,82 @@ async def test_reauth_flow(hass): assert len(hass.config_entries.async_entries(DOMAIN)) == 1 +async def test_reauth_2fa_flow(hass): + """Test the reauth flow.""" + + mock_config = MockConfigEntry( + domain=DOMAIN, + unique_id=USERNAME, + data={ + CONF_USERNAME: USERNAME, + CONF_PASSWORD: INCORRECT_PASSWORD, + "tokens": { + "AccessToken": "mock-access-token", + "RefreshToken": "mock-refresh-token", + }, + }, + ) + mock_config.add_to_hass(hass) + + with patch( + "homeassistant.components.hive.config_flow.Auth.login", + side_effect=hive_exceptions.HiveInvalidPassword(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": mock_config.unique_id, + }, + data=mock_config.data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_password"} + + with patch( + "homeassistant.components.hive.config_flow.Auth.login", + return_value={ + "ChallengeName": "SMS_MFA", + }, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: UPDATED_PASSWORD, + }, + ) + + with patch( + "homeassistant.components.hive.config_flow.Auth.sms_2fa", + return_value={ + "ChallengeName": "SUCCESS", + "AuthenticationResult": { + "RefreshToken": "mock-refresh-token", + "AccessToken": "mock-access-token", + }, + }, + ), patch( + "homeassistant.components.hive.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_CODE: MFA_CODE, + }, + ) + await hass.async_block_till_done() + + assert mock_config.data.get("username") == USERNAME + assert mock_config.data.get("password") == UPDATED_PASSWORD + assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["reason"] == "reauth_successful" + assert len(mock_setup_entry.mock_calls) == 1 + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + async def test_option_flow(hass): """Test config flow options.""" @@ -343,11 +432,23 @@ async def test_user_flow_2fa_send_new_code(hass): "AccessToken": "mock-access-token", }, }, - ), patch( + ): + result4 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_CODE: MFA_CODE, + }, + ) + + assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["step_id"] == "configuration" + assert result4["errors"] == {} + + with patch( "homeassistant.components.hive.config_flow.Auth.device_registration", return_value=True, ), patch( - "homeassistant.components.hive.config_flow.Auth.getDeviceData", + "homeassistant.components.hive.config_flow.Auth.get_device_data", return_value=[ "mock-device-group-key", "mock-device-key", @@ -359,14 +460,14 @@ async def test_user_flow_2fa_send_new_code(hass): "homeassistant.components.hive.async_setup_entry", return_value=True, ) as mock_setup_entry: - result4 = await hass.config_entries.flow.async_configure( - result3["flow_id"], {CONF_CODE: MFA_CODE} + result5 = await hass.config_entries.flow.async_configure( + result4["flow_id"], {CONF_DEVICE_NAME: DEVICE_NAME} ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result4["title"] == USERNAME - assert result4["data"] == { + assert result5["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result5["title"] == USERNAME + assert result5["data"] == { CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, "tokens": { @@ -610,7 +711,28 @@ async def test_user_flow_2fa_unknown_error(hass): result2["flow_id"], {CONF_CODE: MFA_CODE}, ) - await hass.async_block_till_done() assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result3["errors"] == {"base": "unknown"} + assert result3["step_id"] == "configuration" + assert result3["errors"] == {} + + with patch( + "homeassistant.components.hive.config_flow.Auth.device_registration", + return_value=True, + ), patch( + "homeassistant.components.hive.config_flow.Auth.get_device_data", + return_value=[ + "mock-device-group-key", + "mock-device-key", + "mock-device-password", + ], + ): + result4 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_DEVICE_NAME: DEVICE_NAME}, + ) + await hass.async_block_till_done() + + assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["step_id"] == "configuration" + assert result4["errors"] == {"base": "unknown"} From ba64d9db64a9ceeea0a0a8a6bb6c32014d46d4b3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 26 Jun 2022 00:28:21 +0000 Subject: [PATCH 1795/3516] [ci skip] Translation update --- .../components/cast/translations/he.json | 5 ++++ .../components/dnsip/translations/he.json | 29 +++++++++++++++++++ .../components/generic/translations/he.json | 4 +-- .../components/google/translations/he.json | 3 ++ .../components/google/translations/hu.json | 1 + .../components/konnected/translations/he.json | 4 +-- .../components/nest/translations/hu.json | 1 + .../overkiz/translations/sensor.hu.json | 5 ++++ .../radiotherm/translations/he.json | 8 +++++ .../components/rfxtrx/translations/he.json | 7 +++++ .../components/scrape/translations/he.json | 12 ++++++++ .../simplepush/translations/de.json | 21 ++++++++++++++ .../simplepush/translations/hu.json | 21 ++++++++++++++ .../components/skybell/translations/he.json | 14 +++++++++ .../tomorrowio/translations/sensor.he.json | 7 +++++ 15 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/dnsip/translations/he.json create mode 100644 homeassistant/components/radiotherm/translations/he.json create mode 100644 homeassistant/components/scrape/translations/he.json create mode 100644 homeassistant/components/simplepush/translations/de.json create mode 100644 homeassistant/components/simplepush/translations/hu.json create mode 100644 homeassistant/components/skybell/translations/he.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.he.json diff --git a/homeassistant/components/cast/translations/he.json b/homeassistant/components/cast/translations/he.json index d50e5b20684..2d225eb1fed 100644 --- a/homeassistant/components/cast/translations/he.json +++ b/homeassistant/components/cast/translations/he.json @@ -25,6 +25,11 @@ }, "step": { "advanced_options": { + "data": { + "ignore_cec": "\u05dc\u05d4\u05ea\u05e2\u05dc\u05dd \u05de-CEC", + "uuid": "UUIDs \u05de\u05d5\u05ea\u05e8\u05d9\u05dd" + }, + "description": "UUIDs \u05de\u05d5\u05ea\u05e8\u05d9\u05dd - \u05e8\u05e9\u05d9\u05de\u05d4 \u05de\u05d5\u05e4\u05e8\u05d3\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7\u05d9\u05dd \u05e9\u05dc UUIDs \u05e9\u05dc \u05d4\u05ea\u05e7\u05e0\u05d9 Cast \u05dc\u05d4\u05d5\u05e1\u05e4\u05d4 \u05d0\u05dc Home Assistant. \u05d9\u05e9 \u05dc\u05d4\u05e9\u05ea\u05de\u05e9 \u05e8\u05e7 \u05d0\u05dd \u05d0\u05d9\u05df \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d5\u05e1\u05d9\u05e3 \u05d0\u05ea \u05db\u05dc \u05d4\u05ea\u05e7\u05e0\u05d9 cast \u05d4\u05d6\u05de\u05d9\u05e0\u05d9\u05dd.\n\u05dc\u05d4\u05ea\u05e2\u05dc\u05dd \u05de-CEC - \u05e8\u05e9\u05d9\u05de\u05d4 \u05de\u05d5\u05e4\u05e8\u05d3\u05ea \u05d1\u05e4\u05e1\u05d9\u05e7\u05d9\u05dd \u05e9\u05dc Chromecasts \u05e9\u05d0\u05de\u05d5\u05e8\u05d4 \u05dc\u05d4\u05ea\u05e2\u05dc\u05dd \u05de\u05e0\u05ea\u05d5\u05e0\u05d9 CEC \u05dc\u05e7\u05d1\u05d9\u05e2\u05ea \u05d4\u05e7\u05dc\u05d8 \u05d4\u05e4\u05e2\u05d9\u05dc. \u05d6\u05d4 \u05d9\u05d5\u05e2\u05d1\u05e8 \u05dc- pychromecast.IGNORE_CEC.", "title": "\u05ea\u05e6\u05d5\u05e8\u05d4 \u05de\u05ea\u05e7\u05d3\u05de\u05ea \u05e9\u05dc Google Cast" }, "basic_options": { diff --git a/homeassistant/components/dnsip/translations/he.json b/homeassistant/components/dnsip/translations/he.json new file mode 100644 index 00000000000..13f865d61a2 --- /dev/null +++ b/homeassistant/components/dnsip/translations/he.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "invalid_hostname": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05ea" + }, + "step": { + "user": { + "data": { + "hostname": "\u05e9\u05dd \u05d4\u05de\u05d0\u05e8\u05d7 \u05e9\u05e2\u05d1\u05d5\u05e8\u05d5 \u05e0\u05d9\u05ea\u05df \u05dc\u05d1\u05e6\u05e2 \u05e9\u05d0\u05d9\u05dc\u05ea\u05ea DNS", + "resolver": "\u05de\u05e4\u05e2\u05e0\u05d7 \u05e2\u05d1\u05d5\u05e8 \u05d1\u05d3\u05d9\u05e7\u05ea IPV4", + "resolver_ipv6": "\u05de\u05e4\u05e2\u05e0\u05d7 \u05e2\u05d1\u05d5\u05e8 \u05d1\u05d3\u05d9\u05e7\u05ea IPV6" + } + } + } + }, + "options": { + "error": { + "invalid_resolver": "\u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05ea \u05dc\u05de\u05e4\u05e2\u05e0\u05d7" + }, + "step": { + "init": { + "data": { + "resolver": "\u05de\u05e4\u05e2\u05e0\u05d7 \u05e2\u05d1\u05d5\u05e8 \u05d1\u05d3\u05d9\u05e7\u05ea IPV4", + "resolver_ipv6": "\u05de\u05e4\u05e2\u05e0\u05d7 \u05e2\u05d1\u05d5\u05e8 \u05d1\u05d3\u05d9\u05e7\u05ea IPV6" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/he.json b/homeassistant/components/generic/translations/he.json index f39f78074f5..3d2f8cdca4e 100644 --- a/homeassistant/components/generic/translations/he.json +++ b/homeassistant/components/generic/translations/he.json @@ -36,7 +36,7 @@ "limit_refetch_to_url_change": "\u05d4\u05d2\u05d1\u05dc\u05d4 \u05e9\u05dc \u05d0\u05d7\u05e1\u05d5\u05df \u05d7\u05d5\u05d6\u05e8 \u05dc\u05e9\u05d9\u05e0\u05d5\u05d9 \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "rtsp_transport": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 RTSP", - "still_image_url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, http://...)", + "still_image_url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, https://...)", "stream_source": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05de\u05e7\u05d5\u05e8 \u05d6\u05e8\u05dd (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, rtsp://...)", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" @@ -75,7 +75,7 @@ "limit_refetch_to_url_change": "\u05d4\u05d2\u05d1\u05dc\u05d4 \u05e9\u05dc \u05d0\u05d7\u05e1\u05d5\u05df \u05d7\u05d5\u05d6\u05e8 \u05dc\u05e9\u05d9\u05e0\u05d5\u05d9 \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "rtsp_transport": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 RTSP", - "still_image_url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, http://...)", + "still_image_url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, https://...)", "stream_source": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05de\u05e7\u05d5\u05e8 \u05d6\u05e8\u05dd (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, rtsp://...)", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" diff --git a/homeassistant/components/google/translations/he.json b/homeassistant/components/google/translations/he.json index df5ec28163e..191450ed8eb 100644 --- a/homeassistant/components/google/translations/he.json +++ b/homeassistant/components/google/translations/he.json @@ -11,6 +11,9 @@ "create_entry": { "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" }, + "progress": { + "exchange": "\u05db\u05d3\u05d9 \u05dc\u05e7\u05e9\u05e8 \u05d0\u05ea \u05d7\u05e9\u05d1\u05d5\u05df \u05d4\u05d2\u05d5\u05d2\u05dc \u05e9\u05dc\u05da, \u05d9\u05e9 \u05dc\u05d1\u05e7\u05e8 \u05d1\u05db\u05ea\u05d5\u05d1\u05ea [{url}]({url}) \u05d5\u05dc\u05d4\u05d6\u05d9\u05df \u05e7\u05d5\u05d3:\n\n{user_code}" + }, "step": { "auth": { "title": "\u05e7\u05d9\u05e9\u05d5\u05e8 \u05d7\u05e9\u05d1\u05d5\u05df \u05d2\u05d5\u05d2\u05dc" diff --git a/homeassistant/components/google/translations/hu.json b/homeassistant/components/google/translations/hu.json index 6a5c7d2d68c..0ff516dcfed 100644 --- a/homeassistant/components/google/translations/hu.json +++ b/homeassistant/components/google/translations/hu.json @@ -6,6 +6,7 @@ "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "code_expired": "A hiteles\u00edt\u00e9si k\u00f3d lej\u00e1rt vagy a hiteles\u00edt\u0151 adatok be\u00e1ll\u00edt\u00e1sa \u00e9rv\u00e9nytelen, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra.", "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", diff --git a/homeassistant/components/konnected/translations/he.json b/homeassistant/components/konnected/translations/he.json index f07caab4ee1..622c73e2e61 100644 --- a/homeassistant/components/konnected/translations/he.json +++ b/homeassistant/components/konnected/translations/he.json @@ -26,7 +26,7 @@ "step": { "options_binary": { "data": { - "name": "\u05e9\u05dd (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)" + "name": "\u05e9\u05dd" } }, "options_digital": { @@ -39,7 +39,7 @@ }, "options_switch": { "data": { - "name": "\u05e9\u05dd (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)" + "name": "\u05e9\u05dd" } } } diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json index a93d06c4f6d..228f4f5e745 100644 --- a/homeassistant/components/nest/translations/hu.json +++ b/homeassistant/components/nest/translations/hu.json @@ -4,6 +4,7 @@ }, "config": { "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", diff --git a/homeassistant/components/overkiz/translations/sensor.hu.json b/homeassistant/components/overkiz/translations/sensor.hu.json index a361d0ebde2..5646f6bdd7a 100644 --- a/homeassistant/components/overkiz/translations/sensor.hu.json +++ b/homeassistant/components/overkiz/translations/sensor.hu.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "Tiszta", "dirty": "Piszkos" + }, + "overkiz__three_way_handle_direction": { + "closed": "Z\u00e1rva", + "open": "Nyitva", + "tilt": "D\u00f6nt\u00e9s" } } } \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/he.json b/homeassistant/components/radiotherm/translations/he.json new file mode 100644 index 00000000000..77232a68dd2 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/he.json @@ -0,0 +1,8 @@ +{ + "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} {model} ({host})" + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/he.json b/homeassistant/components/rfxtrx/translations/he.json index cabe3734e11..2c2ecd6ccae 100644 --- a/homeassistant/components/rfxtrx/translations/he.json +++ b/homeassistant/components/rfxtrx/translations/he.json @@ -32,6 +32,13 @@ "error": { "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" + }, + "step": { + "prompt_options": { + "data": { + "protocols": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc\u05d9\u05dd" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/he.json b/homeassistant/components/scrape/translations/he.json new file mode 100644 index 00000000000..463ce9035f4 --- /dev/null +++ b/homeassistant/components/scrape/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u05e9\u05dd", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/de.json b/homeassistant/components/simplepush/translations/de.json new file mode 100644 index 00000000000..c7f633d312d --- /dev/null +++ b/homeassistant/components/simplepush/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "device_key": "Der Ger\u00e4teschl\u00fcssel deines Ger\u00e4ts", + "event": "Das Ereignis f\u00fcr die Ereignisse.", + "name": "Name", + "password": "Das Passwort der von deinem Ger\u00e4t verwendeten Verschl\u00fcsselung", + "salt": "Das von deinem Ger\u00e4t verwendete Salt." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/hu.json b/homeassistant/components/simplepush/translations/hu.json new file mode 100644 index 00000000000..09195f343aa --- /dev/null +++ b/homeassistant/components/simplepush/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "device_key": "Az eszk\u00f6z kulcsa", + "event": "Az esem\u00e9ny", + "name": "Elnevez\u00e9s", + "password": "A k\u00e9sz\u00fcl\u00e9k \u00e1ltal haszn\u00e1lt titkos\u00edt\u00e1s jelszava", + "salt": "A k\u00e9sz\u00fcl\u00e9k \u00e1ltal haszn\u00e1lt salt." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/he.json b/homeassistant/components/skybell/translations/he.json new file mode 100644 index 00000000000..28e8ddd34c9 --- /dev/null +++ b/homeassistant/components/skybell/translations/he.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "step": { + "user": { + "data": { + "email": "\u05d3\u05d5\u05d0\"\u05dc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.he.json b/homeassistant/components/tomorrowio/translations/sensor.he.json new file mode 100644 index 00000000000..a91a9b3255b --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.he.json @@ -0,0 +1,7 @@ +{ + "state": { + "tomorrowio__precipitation_type": { + "none": "\u05dc\u05dc\u05d0" + } + } +} \ No newline at end of file From 6ec6f0a835b9f24ae24b73e966d0036d7975ee39 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Jun 2022 21:02:50 -0500 Subject: [PATCH 1796/3516] Fix file sensor reading the whole file to get the last line (#73986) --- homeassistant/components/file/manifest.json | 3 +- homeassistant/components/file/sensor.py | 4 +- requirements_all.txt | 3 ++ requirements_test_all.txt | 3 ++ tests/components/file/fixtures/file_empty.txt | 0 tests/components/file/fixtures/file_value.txt | 3 ++ .../file/fixtures/file_value_template.txt | 2 + tests/components/file/test_sensor.py | 50 +++++++++---------- 8 files changed, 40 insertions(+), 28 deletions(-) create mode 100644 tests/components/file/fixtures/file_empty.txt create mode 100644 tests/components/file/fixtures/file_value.txt create mode 100644 tests/components/file/fixtures/file_value_template.txt diff --git a/homeassistant/components/file/manifest.json b/homeassistant/components/file/manifest.json index 8688ed7939c..2283e74a5e7 100644 --- a/homeassistant/components/file/manifest.json +++ b/homeassistant/components/file/manifest.json @@ -3,5 +3,6 @@ "name": "File", "documentation": "https://www.home-assistant.io/integrations/file", "codeowners": ["@fabaff"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "requirements": ["file-read-backwards==2.0.0"] } diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py index e69a7701eb9..8c0966f30bd 100644 --- a/homeassistant/components/file/sensor.py +++ b/homeassistant/components/file/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging import os +from file_read_backwards import FileReadBackwards import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity @@ -77,9 +78,10 @@ class FileSensor(SensorEntity): def update(self): """Get the latest entry from a file and updates the state.""" try: - with open(self._file_path, encoding="utf-8") as file_data: + with FileReadBackwards(self._file_path, encoding="utf-8") as file_data: for line in file_data: data = line + break data = data.strip() except (IndexError, FileNotFoundError, IsADirectoryError, UnboundLocalError): _LOGGER.warning( diff --git a/requirements_all.txt b/requirements_all.txt index dca486c0d3f..7af4684e169 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -637,6 +637,9 @@ feedparser==6.0.2 # homeassistant.components.fibaro fiblary3==0.1.8 +# homeassistant.components.file +file-read-backwards==2.0.0 + # homeassistant.components.fints fints==3.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 73f98c5eafc..04c6f2d2708 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -455,6 +455,9 @@ feedparser==6.0.2 # homeassistant.components.fibaro fiblary3==0.1.8 +# homeassistant.components.file +file-read-backwards==2.0.0 + # homeassistant.components.fivem fivem-api==0.1.2 diff --git a/tests/components/file/fixtures/file_empty.txt b/tests/components/file/fixtures/file_empty.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/file/fixtures/file_value.txt b/tests/components/file/fixtures/file_value.txt new file mode 100644 index 00000000000..acfd8fbf1a9 --- /dev/null +++ b/tests/components/file/fixtures/file_value.txt @@ -0,0 +1,3 @@ +43 +45 +21 diff --git a/tests/components/file/fixtures/file_value_template.txt b/tests/components/file/fixtures/file_value_template.txt new file mode 100644 index 00000000000..30a1b0ea8ba --- /dev/null +++ b/tests/components/file/fixtures/file_value_template.txt @@ -0,0 +1,2 @@ +{"temperature": 29, "humidity": 31} +{"temperature": 26, "humidity": 36} diff --git a/tests/components/file/test_sensor.py b/tests/components/file/test_sensor.py index 97fe6250d02..725ccb527f8 100644 --- a/tests/components/file/test_sensor.py +++ b/tests/components/file/test_sensor.py @@ -1,5 +1,5 @@ """The tests for local file sensor platform.""" -from unittest.mock import Mock, mock_open, patch +from unittest.mock import Mock, patch import pytest @@ -7,7 +7,7 @@ from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import mock_registry +from tests.common import get_fixture_path, mock_registry @pytest.fixture @@ -21,13 +21,14 @@ def entity_reg(hass): async def test_file_value(hass: HomeAssistant) -> None: """Test the File sensor.""" config = { - "sensor": {"platform": "file", "name": "file1", "file_path": "mock.file1"} + "sensor": { + "platform": "file", + "name": "file1", + "file_path": get_fixture_path("file_value.txt", "file"), + } } - m_open = mock_open(read_data="43\n45\n21") - with patch( - "homeassistant.components.file.sensor.open", m_open, create=True - ), patch.object(hass.config, "is_allowed_path", return_value=True): + with patch.object(hass.config, "is_allowed_path", return_value=True): assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() @@ -43,19 +44,12 @@ async def test_file_value_template(hass: HomeAssistant) -> None: "sensor": { "platform": "file", "name": "file2", - "file_path": "mock.file2", + "file_path": get_fixture_path("file_value_template.txt", "file"), "value_template": "{{ value_json.temperature }}", } } - data = ( - '{"temperature": 29, "humidity": 31}\n' + '{"temperature": 26, "humidity": 36}' - ) - - m_open = mock_open(read_data=data) - with patch( - "homeassistant.components.file.sensor.open", m_open, create=True - ), patch.object(hass.config, "is_allowed_path", return_value=True): + with patch.object(hass.config, "is_allowed_path", return_value=True): assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() @@ -67,12 +61,15 @@ async def test_file_value_template(hass: HomeAssistant) -> None: @patch("os.access", Mock(return_value=True)) async def test_file_empty(hass: HomeAssistant) -> None: """Test the File sensor with an empty file.""" - config = {"sensor": {"platform": "file", "name": "file3", "file_path": "mock.file"}} + config = { + "sensor": { + "platform": "file", + "name": "file3", + "file_path": get_fixture_path("file_empty.txt", "file"), + } + } - m_open = mock_open(read_data="") - with patch( - "homeassistant.components.file.sensor.open", m_open, create=True - ), patch.object(hass.config, "is_allowed_path", return_value=True): + with patch.object(hass.config, "is_allowed_path", return_value=True): assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() @@ -85,13 +82,14 @@ async def test_file_empty(hass: HomeAssistant) -> None: async def test_file_path_invalid(hass: HomeAssistant) -> None: """Test the File sensor with invalid path.""" config = { - "sensor": {"platform": "file", "name": "file4", "file_path": "mock.file4"} + "sensor": { + "platform": "file", + "name": "file4", + "file_path": get_fixture_path("file_value.txt", "file"), + } } - m_open = mock_open(read_data="43\n45\n21") - with patch( - "homeassistant.components.file.sensor.open", m_open, create=True - ), patch.object(hass.config, "is_allowed_path", return_value=False): + with patch.object(hass.config, "is_allowed_path", return_value=False): assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() From fb5e6aaa2965dda7c09b1cb49cde3da70d036d1a Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Sun, 26 Jun 2022 11:33:11 +0100 Subject: [PATCH 1797/3516] Clean up Glances sensors a bit (#73998) --- homeassistant/components/glances/sensor.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 0d60747ecaa..7dfd0c503ef 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -7,6 +7,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import GlancesData from .const import DATA_UPDATED, DOMAIN, SENSOR_TYPES, GlancesSensorEntityDescription @@ -31,7 +32,6 @@ async def async_setup_entry( name, disk["mnt_point"], description, - config_entry.entry_id, ) ) elif description.type == "sensors": @@ -44,16 +44,11 @@ async def async_setup_entry( name, sensor["label"], description, - config_entry.entry_id, ) ) elif description.type == "raid": for raid_device in client.api.data[description.type]: - dev.append( - GlancesSensor( - client, name, raid_device, description, config_entry.entry_id - ) - ) + dev.append(GlancesSensor(client, name, raid_device, description)) elif client.api.data[description.type]: dev.append( GlancesSensor( @@ -61,7 +56,6 @@ async def async_setup_entry( name, "", description, - config_entry.entry_id, ) ) @@ -75,12 +69,11 @@ class GlancesSensor(SensorEntity): def __init__( self, - glances_data, - name, - sensor_name_prefix, + glances_data: GlancesData, + name: str, + sensor_name_prefix: str, description: GlancesSensorEntityDescription, - config_entry_id: str, - ): + ) -> None: """Initialize the sensor.""" self.glances_data = glances_data self._sensor_name_prefix = sensor_name_prefix @@ -90,7 +83,7 @@ class GlancesSensor(SensorEntity): self.entity_description = description self._attr_name = f"{name} {sensor_name_prefix} {description.name_suffix}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, config_entry_id)}, + identifiers={(DOMAIN, glances_data.config_entry.entry_id)}, manufacturer="Glances", name=name, ) From 9a0b3796d3fcf45b384604f10cb4367f27b0facb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Jun 2022 03:38:17 -0700 Subject: [PATCH 1798/3516] Bump xmltodict to 0.13.0 (#73974) Changelog: https://github.com/martinblech/xmltodict/blob/v0.13.0/CHANGELOG.md --- homeassistant/components/bluesound/manifest.json | 2 +- homeassistant/components/fritz/manifest.json | 2 +- homeassistant/components/rest/manifest.json | 2 +- homeassistant/components/startca/manifest.json | 2 +- homeassistant/components/ted5000/manifest.json | 2 +- homeassistant/components/zestimate/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bluesound/manifest.json b/homeassistant/components/bluesound/manifest.json index bfefff36601..d1b86f80326 100644 --- a/homeassistant/components/bluesound/manifest.json +++ b/homeassistant/components/bluesound/manifest.json @@ -2,7 +2,7 @@ "domain": "bluesound", "name": "Bluesound", "documentation": "https://www.home-assistant.io/integrations/bluesound", - "requirements": ["xmltodict==0.12.0"], + "requirements": ["xmltodict==0.13.0"], "codeowners": ["@thrawnarn"], "iot_class": "local_polling" } diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index 5eb210d2091..e5828ba76cf 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -2,7 +2,7 @@ "domain": "fritz", "name": "AVM FRITZ!Box Tools", "documentation": "https://www.home-assistant.io/integrations/fritz", - "requirements": ["fritzconnection==1.8.0", "xmltodict==0.12.0"], + "requirements": ["fritzconnection==1.8.0", "xmltodict==0.13.0"], "dependencies": ["network"], "codeowners": ["@mammuth", "@AaronDavidSchneider", "@chemelli74", "@mib1185"], "config_flow": true, diff --git a/homeassistant/components/rest/manifest.json b/homeassistant/components/rest/manifest.json index c81656d82b4..f6e7631e623 100644 --- a/homeassistant/components/rest/manifest.json +++ b/homeassistant/components/rest/manifest.json @@ -2,7 +2,7 @@ "domain": "rest", "name": "RESTful", "documentation": "https://www.home-assistant.io/integrations/rest", - "requirements": ["jsonpath==0.82", "xmltodict==0.12.0"], + "requirements": ["jsonpath==0.82", "xmltodict==0.13.0"], "codeowners": [], "iot_class": "local_polling" } diff --git a/homeassistant/components/startca/manifest.json b/homeassistant/components/startca/manifest.json index d08f276e770..68786ecf341 100644 --- a/homeassistant/components/startca/manifest.json +++ b/homeassistant/components/startca/manifest.json @@ -2,7 +2,7 @@ "domain": "startca", "name": "Start.ca", "documentation": "https://www.home-assistant.io/integrations/startca", - "requirements": ["xmltodict==0.12.0"], + "requirements": ["xmltodict==0.13.0"], "codeowners": [], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/ted5000/manifest.json b/homeassistant/components/ted5000/manifest.json index 1ab57418af5..8852c0184b2 100644 --- a/homeassistant/components/ted5000/manifest.json +++ b/homeassistant/components/ted5000/manifest.json @@ -2,7 +2,7 @@ "domain": "ted5000", "name": "The Energy Detective TED5000", "documentation": "https://www.home-assistant.io/integrations/ted5000", - "requirements": ["xmltodict==0.12.0"], + "requirements": ["xmltodict==0.13.0"], "codeowners": [], "iot_class": "local_polling" } diff --git a/homeassistant/components/zestimate/manifest.json b/homeassistant/components/zestimate/manifest.json index 4fee44ffcac..d382fc26ab0 100644 --- a/homeassistant/components/zestimate/manifest.json +++ b/homeassistant/components/zestimate/manifest.json @@ -2,7 +2,7 @@ "domain": "zestimate", "name": "Zestimate", "documentation": "https://www.home-assistant.io/integrations/zestimate", - "requirements": ["xmltodict==0.12.0"], + "requirements": ["xmltodict==0.13.0"], "codeowners": [], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 7af4684e169..95cbdd5b0bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2465,7 +2465,7 @@ xknx==0.21.3 # homeassistant.components.startca # homeassistant.components.ted5000 # homeassistant.components.zestimate -xmltodict==0.12.0 +xmltodict==0.13.0 # homeassistant.components.xs1 xs1-api-client==3.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04c6f2d2708..f9f095161ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1638,7 +1638,7 @@ xknx==0.21.3 # homeassistant.components.startca # homeassistant.components.ted5000 # homeassistant.components.zestimate -xmltodict==0.12.0 +xmltodict==0.13.0 # homeassistant.components.yale_smart_alarm yalesmartalarmclient==0.3.8 From 11ec8b9186219e8e9a9b9b620b1ba18df8a0904c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 27 Jun 2022 00:25:49 +0000 Subject: [PATCH 1799/3516] [ci skip] Translation update --- .../components/google/translations/el.json | 1 + .../components/hive/translations/ca.json | 9 +++++++- .../components/hive/translations/de.json | 9 +++++++- .../components/hive/translations/el.json | 7 +++++++ .../components/hive/translations/fr.json | 7 +++++++ .../components/hive/translations/hu.json | 7 +++++++ .../components/hive/translations/id.json | 9 +++++++- .../components/hive/translations/it.json | 9 +++++++- .../components/hive/translations/ja.json | 7 +++++++ .../components/hive/translations/pt-BR.json | 9 +++++++- .../components/hive/translations/zh-Hant.json | 9 +++++++- .../components/nest/translations/el.json | 1 + .../overkiz/translations/sensor.el.json | 5 +++++ .../simplepush/translations/ca.json | 21 +++++++++++++++++++ .../simplepush/translations/el.json | 21 +++++++++++++++++++ .../simplepush/translations/hu.json | 2 +- .../simplepush/translations/id.json | 21 +++++++++++++++++++ .../simplepush/translations/it.json | 21 +++++++++++++++++++ .../simplepush/translations/ja.json | 21 +++++++++++++++++++ .../transmission/translations/el.json | 10 ++++++++- 20 files changed, 198 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/simplepush/translations/ca.json create mode 100644 homeassistant/components/simplepush/translations/el.json create mode 100644 homeassistant/components/simplepush/translations/id.json create mode 100644 homeassistant/components/simplepush/translations/it.json create mode 100644 homeassistant/components/simplepush/translations/ja.json diff --git a/homeassistant/components/google/translations/el.json b/homeassistant/components/google/translations/el.json index fdcf17d26ce..65cc5a0038d 100644 --- a/homeassistant/components/google/translations/el.json +++ b/homeassistant/components/google/translations/el.json @@ -6,6 +6,7 @@ "abort": { "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "code_expired": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ad\u03bb\u03b7\u03be\u03b5 \u03ae \u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03b7\u03c1\u03af\u03c9\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ac\u03ba\u03c5\u03c1\u03b7, \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", "invalid_access_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "missing_configuration": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", diff --git a/homeassistant/components/hive/translations/ca.json b/homeassistant/components/hive/translations/ca.json index edebafba579..50a06169547 100644 --- a/homeassistant/components/hive/translations/ca.json +++ b/homeassistant/components/hive/translations/ca.json @@ -20,6 +20,13 @@ "description": "Introdueix codi d'autenticaci\u00f3 Hive. \n\n Introdueix el codi 0000 per demanar un altre codi.", "title": "Verificaci\u00f3 en dos passos de Hive." }, + "configuration": { + "data": { + "device_name": "Nom del dispositiu" + }, + "description": "Introdueix la teva configuraci\u00f3 de Hive ", + "title": "Configuraci\u00f3 de Hive." + }, "reauth": { "data": { "password": "Contrasenya", @@ -34,7 +41,7 @@ "scan_interval": "Interval d'escaneig (segons)", "username": "Nom d'usuari" }, - "description": "Actualitza la informaci\u00f3 i configuraci\u00f3 d'inici de sessi\u00f3.", + "description": "Introdueix la informaci\u00f3 d'inici de sessi\u00f3 de Hive.", "title": "Inici de sessi\u00f3 Hive" } } diff --git a/homeassistant/components/hive/translations/de.json b/homeassistant/components/hive/translations/de.json index bd5876bb023..e40fd0f499f 100644 --- a/homeassistant/components/hive/translations/de.json +++ b/homeassistant/components/hive/translations/de.json @@ -20,6 +20,13 @@ "description": "Gib deinen Hive-Authentifizierungscode ein. \n \nBitte gib den Code 0000 ein, um einen anderen Code anzufordern.", "title": "Hive Zwei-Faktor-Authentifizierung." }, + "configuration": { + "data": { + "device_name": "Ger\u00e4tename" + }, + "description": "Gib deine Hive-Konfiguration ein ", + "title": "Hive-Konfiguration." + }, "reauth": { "data": { "password": "Passwort", @@ -34,7 +41,7 @@ "scan_interval": "Scanintervall (Sekunden)", "username": "Benutzername" }, - "description": "Gebe deine Anmeldeinformationen und -konfiguration f\u00fcr Hive ein", + "description": "Gib deine Hive-Anmeldeinformationen ein.", "title": "Hive Anmeldung" } } diff --git a/homeassistant/components/hive/translations/el.json b/homeassistant/components/hive/translations/el.json index 986dd52ef19..3c6be9f4eba 100644 --- a/homeassistant/components/hive/translations/el.json +++ b/homeassistant/components/hive/translations/el.json @@ -20,6 +20,13 @@ "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 Hive. \n\n \u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc 0000 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b6\u03b7\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ac\u03bb\u03bb\u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc.", "title": "\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd \u03c4\u03bf\u03c5 Hive." }, + "configuration": { + "data": { + "device_name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Hive \u03c3\u03b1\u03c2", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Hive." + }, "reauth": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/hive/translations/fr.json b/homeassistant/components/hive/translations/fr.json index 5868a2bf175..118a0ff9b75 100644 --- a/homeassistant/components/hive/translations/fr.json +++ b/homeassistant/components/hive/translations/fr.json @@ -20,6 +20,13 @@ "description": "Entrez votre code d\u2019authentification Hive. \n \nVeuillez entrer le code 0000 pour demander un autre code.", "title": "Authentification \u00e0 deux facteurs Hive." }, + "configuration": { + "data": { + "device_name": "Nom de l'appareil" + }, + "description": "Saisissez votre configuration Hive ", + "title": "Configuration Hive." + }, "reauth": { "data": { "password": "Mot de passe", diff --git a/homeassistant/components/hive/translations/hu.json b/homeassistant/components/hive/translations/hu.json index 9b0d3c21590..8a265ff63c0 100644 --- a/homeassistant/components/hive/translations/hu.json +++ b/homeassistant/components/hive/translations/hu.json @@ -20,6 +20,13 @@ "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." }, + "configuration": { + "data": { + "device_name": "Eszk\u00f6zn\u00e9v" + }, + "description": "Adja meg Hive konfigur\u00e1ci\u00f3j\u00e1t", + "title": "Hive konfigur\u00e1ci\u00f3." + }, "reauth": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/hive/translations/id.json b/homeassistant/components/hive/translations/id.json index e092515e91e..9c008d06802 100644 --- a/homeassistant/components/hive/translations/id.json +++ b/homeassistant/components/hive/translations/id.json @@ -20,6 +20,13 @@ "description": "Masukkan kode autentikasi Hive Anda. \n \nMasukkan kode 0000 untuk meminta kode lain.", "title": "Autentikasi Dua Faktor Hive." }, + "configuration": { + "data": { + "device_name": "Nama Perangkat" + }, + "description": "Masukkan konfigurasi Hive Anda", + "title": "Konfigurasi Hive" + }, "reauth": { "data": { "password": "Kata Sandi", @@ -34,7 +41,7 @@ "scan_interval": "Interval Pindai (detik)", "username": "Nama Pengguna" }, - "description": "Masukkan informasi masuk dan konfigurasi Hive Anda.", + "description": "Masukkan informasi masuk Hive Anda.", "title": "Info Masuk Hive" } } diff --git a/homeassistant/components/hive/translations/it.json b/homeassistant/components/hive/translations/it.json index 38edac70cb1..fcd841cd1d2 100644 --- a/homeassistant/components/hive/translations/it.json +++ b/homeassistant/components/hive/translations/it.json @@ -20,6 +20,13 @@ "description": "Inserisci il tuo codice di autenticazione Hive. \n\n Inserisci il codice 0000 per richiedere un altro codice.", "title": "Autenticazione a due fattori di Hive." }, + "configuration": { + "data": { + "device_name": "Nome del dispositivo" + }, + "description": "Inserisci la tua configurazione Hive ", + "title": "Configurazione Hive." + }, "reauth": { "data": { "password": "Password", @@ -34,7 +41,7 @@ "scan_interval": "Intervallo di scansione (secondi)", "username": "Nome utente" }, - "description": "Inserisci le informazioni di accesso e la configurazione di Hive.", + "description": "Inserisci le tue informazioni di accesso Hive.", "title": "Accesso Hive" } } diff --git a/homeassistant/components/hive/translations/ja.json b/homeassistant/components/hive/translations/ja.json index 55b18b13427..ed11bbd8b7e 100644 --- a/homeassistant/components/hive/translations/ja.json +++ b/homeassistant/components/hive/translations/ja.json @@ -20,6 +20,13 @@ "description": "Hive\u8a8d\u8a3c\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u307e\u3059\u3002 \n\n\u5225\u306e\u30b3\u30fc\u30c9\u3092\u30ea\u30af\u30a8\u30b9\u30c8\u3059\u308b\u306b\u306f\u3001\u30b3\u30fc\u30c9 0000 \u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Hive 2\u8981\u7d20\u8a8d\u8a3c\u3002" }, + "configuration": { + "data": { + "device_name": "\u30c7\u30d0\u30a4\u30b9\u540d" + }, + "description": "Hive\u306e\u8a2d\u5b9a\u3092\u5165\u529b\u3057\u307e\u3059 ", + "title": "Hive\u306e\u8a2d\u5b9a\u3002" + }, "reauth": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", diff --git a/homeassistant/components/hive/translations/pt-BR.json b/homeassistant/components/hive/translations/pt-BR.json index 5f5f7e857c2..3a0cf19884e 100644 --- a/homeassistant/components/hive/translations/pt-BR.json +++ b/homeassistant/components/hive/translations/pt-BR.json @@ -20,6 +20,13 @@ "description": "Digite seu c\u00f3digo de autentica\u00e7\u00e3o Hive. \n\n Insira o c\u00f3digo 0000 para solicitar outro c\u00f3digo.", "title": "Autentica\u00e7\u00e3o de dois fatores do Hive." }, + "configuration": { + "data": { + "device_name": "Nome do dispositivo" + }, + "description": "Digite sua configura\u00e7\u00e3o de Hive", + "title": "Configura\u00e7\u00e3o Hive" + }, "reauth": { "data": { "password": "Senha", @@ -34,7 +41,7 @@ "scan_interval": "Intervalo de escaneamento (segundos)", "username": "Usu\u00e1rio" }, - "description": "Insira suas informa\u00e7\u00f5es de login e configura\u00e7\u00e3o do Hive.", + "description": "Insira suas informa\u00e7\u00f5es de login de Hive.", "title": "Login do Hive" } } diff --git a/homeassistant/components/hive/translations/zh-Hant.json b/homeassistant/components/hive/translations/zh-Hant.json index 0af7e218f6e..6f23d8299a2 100644 --- a/homeassistant/components/hive/translations/zh-Hant.json +++ b/homeassistant/components/hive/translations/zh-Hant.json @@ -20,6 +20,13 @@ "description": "\u8f38\u5165 Hive \u8a8d\u8b49\u78bc\u3002\n \n \u8acb\u8f38\u5165 0000 \u4ee5\u7372\u53d6\u5176\u4ed6\u8a8d\u8b49\u78bc\u3002", "title": "\u96d9\u91cd\u8a8d\u8b49" }, + "configuration": { + "data": { + "device_name": "\u88dd\u7f6e\u540d\u7a31" + }, + "description": "\u8f38\u5165 Hive \u8a2d\u5b9a", + "title": "Hive \u8a2d\u5b9a\u3002" + }, "reauth": { "data": { "password": "\u5bc6\u78bc", @@ -34,7 +41,7 @@ "scan_interval": "\u6383\u63cf\u9593\u8ddd\uff08\u79d2\uff09", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8f38\u5165 Hive \u767b\u5165\u8cc7\u8a0a\u8207\u8a2d\u5b9a\u3002", + "description": "\u8f38\u5165 Hive \u767b\u5165\u8cc7\u8a0a\u3002", "title": "Hive \u767b\u5165\u8cc7\u8a0a" } } diff --git a/homeassistant/components/nest/translations/el.json b/homeassistant/components/nest/translations/el.json index 26e4622670e..b94ec0ee9df 100644 --- a/homeassistant/components/nest/translations/el.json +++ b/homeassistant/components/nest/translations/el.json @@ -4,6 +4,7 @@ }, "config": { "abort": { + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2.", "invalid_access_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "missing_configuration": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", diff --git a/homeassistant/components/overkiz/translations/sensor.el.json b/homeassistant/components/overkiz/translations/sensor.el.json index 445a457195c..5335b9d8049 100644 --- a/homeassistant/components/overkiz/translations/sensor.el.json +++ b/homeassistant/components/overkiz/translations/sensor.el.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "\u039a\u03b1\u03b8\u03b1\u03c1\u03cc\u03c2", "dirty": "\u0392\u03c1\u03ce\u03bc\u03b9\u03ba\u03bf\u03c2" + }, + "overkiz__three_way_handle_direction": { + "closed": "\u039a\u03bb\u03b5\u03b9\u03c3\u03c4\u03cc", + "open": "\u0386\u03bd\u03bf\u03b9\u03b3\u03bc\u03b1", + "tilt": "\u039a\u03bb\u03af\u03c3\u03b7" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/ca.json b/homeassistant/components/simplepush/translations/ca.json new file mode 100644 index 00000000000..161e1a3c36c --- /dev/null +++ b/homeassistant/components/simplepush/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "device_key": "Clau del teu dispositiu", + "event": "Esdeveniment per als esdeveniments.", + "name": "Nom", + "password": "Contrasenya del xifrat que utilitza el teu dispositiu", + "salt": "La sal ('salt') que utilitza el teu dispositiu." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/el.json b/homeassistant/components/simplepush/translations/el.json new file mode 100644 index 00000000000..bdcc7239acc --- /dev/null +++ b/homeassistant/components/simplepush/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "user": { + "data": { + "device_key": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03c3\u03b1\u03c2", + "event": "\u03a4\u03bf \u03b3\u03b5\u03b3\u03bf\u03bd\u03cc\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b1 \u03b3\u03b5\u03b3\u03bf\u03bd\u03cc\u03c4\u03b1.", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "password": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2", + "salt": "\u03a4\u03bf \u03b1\u03bb\u03ac\u03c4\u03b9 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/hu.json b/homeassistant/components/simplepush/translations/hu.json index 09195f343aa..e5deb2bf2fc 100644 --- a/homeassistant/components/simplepush/translations/hu.json +++ b/homeassistant/components/simplepush/translations/hu.json @@ -13,7 +13,7 @@ "event": "Az esem\u00e9ny", "name": "Elnevez\u00e9s", "password": "A k\u00e9sz\u00fcl\u00e9k \u00e1ltal haszn\u00e1lt titkos\u00edt\u00e1s jelszava", - "salt": "A k\u00e9sz\u00fcl\u00e9k \u00e1ltal haszn\u00e1lt salt." + "salt": "A k\u00e9sz\u00fcl\u00e9k \u00e1ltal haszn\u00e1lt 'salt'." } } } diff --git a/homeassistant/components/simplepush/translations/id.json b/homeassistant/components/simplepush/translations/id.json new file mode 100644 index 00000000000..35984af5d5f --- /dev/null +++ b/homeassistant/components/simplepush/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "device_key": "Kunci perangkat untuk perangkat Anda", + "event": "Event untuk daftar event", + "name": "Nama", + "password": "Kata sandi enkripsi yang digunakan oleh perangkat Anda", + "salt": "Salt yang digunakan oleh perangkat Anda." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/it.json b/homeassistant/components/simplepush/translations/it.json new file mode 100644 index 00000000000..2ba1bea5d96 --- /dev/null +++ b/homeassistant/components/simplepush/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "device_key": "La chiave del dispositivo", + "event": "L'evento per gli eventi.", + "name": "Nome", + "password": "La password della crittografia utilizzata dal tuo dispositivo", + "salt": "Il salt utilizzato dal tuo dispositivo." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/ja.json b/homeassistant/components/simplepush/translations/ja.json new file mode 100644 index 00000000000..8e3023e602e --- /dev/null +++ b/homeassistant/components/simplepush/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "step": { + "user": { + "data": { + "device_key": "\u30c7\u30d0\u30a4\u30b9\u306e\u30c7\u30d0\u30a4\u30b9\u30ad\u30fc", + "event": "\u30a4\u30d9\u30f3\u30c8\u306e\u305f\u3081\u306e\u30a4\u30d9\u30f3\u30c8\u3067\u3059\u3002", + "name": "\u540d\u524d", + "password": "\u30c7\u30d0\u30a4\u30b9\u3067\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u6697\u53f7\u5316\u306e\u30d1\u30b9\u30ef\u30fc\u30c9", + "salt": "\u30c7\u30d0\u30a4\u30b9\u3067\u4f7f\u7528\u3055\u308c\u3066\u3044\u308bsalt\u3067\u3059\u3002" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/el.json b/homeassistant/components/transmission/translations/el.json index 4a0701cdaf9..6790e6e351d 100644 --- a/homeassistant/components/transmission/translations/el.json +++ b/homeassistant/components/transmission/translations/el.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", @@ -9,6 +10,13 @@ "name_exists": "\u03a4\u03bf \u038c\u03bd\u03bf\u03bc\u03b1 \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "description": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {username} \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2.", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, "user": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", From aa314a090146b44d32ab2a2754321ae03bf248de Mon Sep 17 00:00:00 2001 From: akloeckner Date: Mon, 27 Jun 2022 08:59:29 +0200 Subject: [PATCH 1800/3516] Add this variable to trigger-based templates (#72437) add this variables to trigger-based templates follow-up for https://github.com/home-assistant/core/issues/70359 --- .../components/template/trigger_entity.py | 13 ++++++-- tests/components/template/test_sensor.py | 32 +++++++++++++------ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/template/trigger_entity.py b/homeassistant/components/template/trigger_entity.py index 6780d12c507..b5696003c94 100644 --- a/homeassistant/components/template/trigger_entity.py +++ b/homeassistant/components/template/trigger_entity.py @@ -147,25 +147,32 @@ class TriggerEntity(CoordinatorEntity[TriggerUpdateCoordinator]): @callback def _process_data(self) -> None: """Process new data.""" + + this = None + if state := self.hass.states.get(self.entity_id): + this = state.as_dict() + run_variables = self.coordinator.data["run_variables"] + variables = {"this": this, **(run_variables or {})} + try: rendered = dict(self._static_rendered) for key in self._to_render_simple: rendered[key] = self._config[key].async_render( - self.coordinator.data["run_variables"], + variables, parse_result=key in self._parse_result, ) for key in self._to_render_complex: rendered[key] = template.render_complex( self._config[key], - self.coordinator.data["run_variables"], + variables, ) if CONF_ATTRIBUTES in self._config: rendered[CONF_ATTRIBUTES] = template.render_complex( self._config[CONF_ATTRIBUTES], - self.coordinator.data["run_variables"], + variables, ) self._rendered = rendered diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index ddf13c2015b..44fa96cfc6a 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -624,20 +624,32 @@ async def test_sun_renders_once_per_sensor(hass, start_ha): } -@pytest.mark.parametrize("count,domain", [(1, sensor.DOMAIN)]) +@pytest.mark.parametrize("count,domain", [(1, "template")]) @pytest.mark.parametrize( "config", [ { - "sensor": { - "platform": "template", - "sensors": { - "test_template_sensor": { - "value_template": "{{ this.attributes.test }}: {{ this.entity_id }}", - "attribute_templates": { - "test": "It {{ states.sensor.test_state.state }}" - }, - } + "template": { + "sensor": { + "name": "test_template_sensor", + "state": "{{ this.attributes.test }}: {{ this.entity_id }}", + "attributes": {"test": "It {{ states.sensor.test_state.state }}"}, + }, + }, + }, + { + "template": { + "trigger": { + "platform": "state", + "entity_id": [ + "sensor.test_state", + "sensor.test_template_sensor", + ], + }, + "sensor": { + "name": "test_template_sensor", + "state": "{{ this.attributes.test }}: {{ this.entity_id }}", + "attributes": {"test": "It {{ states.sensor.test_state.state }}"}, }, }, }, From a94579107c189bb1814b5a8456091f79fc6b1a70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 27 Jun 2022 11:38:40 +0200 Subject: [PATCH 1801/3516] Bump awesomeversion from 22.5.2 to 22.6.0 (#74030) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 55cc067829b..6fb51cf0d09 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ async-upnp-client==0.31.2 async_timeout==4.0.2 atomicwrites==1.4.0 attrs==21.2.0 -awesomeversion==22.5.2 +awesomeversion==22.6.0 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/pyproject.toml b/pyproject.toml index 165e781983e..fc07110dbad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "async_timeout==4.0.2", "attrs==21.2.0", "atomicwrites==1.4.0", - "awesomeversion==22.5.2", + "awesomeversion==22.6.0", "bcrypt==3.1.7", "certifi>=2021.5.30", "ciso8601==2.2.0", diff --git a/requirements.txt b/requirements.txt index 0b3791d6eed..21e11def1b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ astral==2.2 async_timeout==4.0.2 attrs==21.2.0 atomicwrites==1.4.0 -awesomeversion==22.5.2 +awesomeversion==22.6.0 bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 From e32c7dbf9219bc762085e7731a4a6cc68c7ca60c Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Mon, 27 Jun 2022 05:39:02 -0400 Subject: [PATCH 1802/3516] Use built in unit handling for nws weather (#73981) use built in unit handling for nws --- homeassistant/components/nws/weather.py | 114 ++++++++++-------------- tests/components/nws/const.py | 6 +- 2 files changed, 49 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 44e1ed2d2c4..5bb1bb0bf6c 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -3,22 +3,18 @@ from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, - LENGTH_KILOMETERS, LENGTH_METERS, - LENGTH_MILES, - PRESSURE_HPA, - PRESSURE_INHG, PRESSURE_PA, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR, @@ -28,9 +24,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -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.speed import convert as convert_speed from homeassistant.util.temperature import convert as convert_temperature @@ -155,68 +149,54 @@ class NWSWeather(WeatherEntity): return f"{self.station} {self.mode.title()}" @property - def temperature(self): + def native_temperature(self): """Return the current temperature.""" - temp_c = None if self.observation: - temp_c = self.observation.get("temperature") - if temp_c is not None: - return convert_temperature(temp_c, TEMP_CELSIUS, TEMP_FAHRENHEIT) + return self.observation.get("temperature") return None @property - def pressure(self): + def native_temperature_unit(self): + """Return the current temperature unit.""" + return TEMP_CELSIUS + + @property + def native_pressure(self): """Return the current pressure.""" - pressure_pa = None if self.observation: - pressure_pa = self.observation.get("seaLevelPressure") - if pressure_pa is None: - return None - if self.is_metric: - pressure = convert_pressure(pressure_pa, PRESSURE_PA, PRESSURE_HPA) - pressure = round(pressure) - else: - pressure = convert_pressure(pressure_pa, PRESSURE_PA, PRESSURE_INHG) - pressure = round(pressure, 2) - return pressure + return self.observation.get("seaLevelPressure") + return None + + @property + def native_pressure_unit(self): + """Return the current pressure unit.""" + return PRESSURE_PA @property def humidity(self): """Return the name of the sensor.""" - humidity = None if self.observation: - humidity = self.observation.get("relativeHumidity") - return humidity + return self.observation.get("relativeHumidity") + return None @property - def wind_speed(self): + def native_wind_speed(self): """Return the current windspeed.""" - wind_km_hr = None if self.observation: - wind_km_hr = self.observation.get("windSpeed") - if wind_km_hr is None: - return None + return self.observation.get("windSpeed") + return None - if self.is_metric: - wind = wind_km_hr - else: - wind = convert_speed( - wind_km_hr, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR - ) - return round(wind) + @property + def native_wind_speed_unit(self): + """Return the current windspeed.""" + return SPEED_KILOMETERS_PER_HOUR @property def wind_bearing(self): """Return the current wind bearing (degrees).""" - wind_bearing = None if self.observation: - wind_bearing = self.observation.get("windDirection") - return wind_bearing - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_FAHRENHEIT + return self.observation.get("windDirection") + return None @property def condition(self): @@ -232,19 +212,16 @@ class NWSWeather(WeatherEntity): return None @property - def visibility(self): + def native_visibility(self): """Return visibility.""" - vis_m = None if self.observation: - vis_m = self.observation.get("visibility") - if vis_m is None: - return None + return self.observation.get("visibility") + return None - if self.is_metric: - vis = convert_distance(vis_m, LENGTH_METERS, LENGTH_KILOMETERS) - else: - vis = convert_distance(vis_m, LENGTH_METERS, LENGTH_MILES) - return round(vis, 0) + @property + def native_visibility_unit(self): + """Return visibility unit.""" + return LENGTH_METERS @property def forecast(self): @@ -257,10 +234,16 @@ class NWSWeather(WeatherEntity): ATTR_FORECAST_DETAILED_DESCRIPTION: forecast_entry.get( "detailedForecast" ), - ATTR_FORECAST_TEMP: forecast_entry.get("temperature"), ATTR_FORECAST_TIME: forecast_entry.get("startTime"), } + if (temp := forecast_entry.get("temperature")) is not None: + data[ATTR_FORECAST_NATIVE_TEMP] = convert_temperature( + temp, TEMP_FAHRENHEIT, TEMP_CELSIUS + ) + else: + data[ATTR_FORECAST_NATIVE_TEMP] = None + if self.mode == DAYNIGHT: data[ATTR_FORECAST_DAYTIME] = forecast_entry.get("isDaytime") time = forecast_entry.get("iconTime") @@ -275,16 +258,11 @@ class NWSWeather(WeatherEntity): data[ATTR_FORECAST_WIND_BEARING] = forecast_entry.get("windBearing") wind_speed = forecast_entry.get("windSpeedAvg") if wind_speed is not None: - if self.is_metric: - data[ATTR_FORECAST_WIND_SPEED] = round( - convert_speed( - wind_speed, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR - ) - ) - else: - data[ATTR_FORECAST_WIND_SPEED] = round(wind_speed) + data[ATTR_FORECAST_NATIVE_WIND_SPEED] = convert_speed( + wind_speed, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR + ) else: - data[ATTR_FORECAST_WIND_SPEED] = None + data[ATTR_FORECAST_NATIVE_WIND_SPEED] = None forecast.append(data) return forecast diff --git a/tests/components/nws/const.py b/tests/components/nws/const.py index dcf591b83ae..850d330c9ae 100644 --- a/tests/components/nws/const.py +++ b/tests/components/nws/const.py @@ -105,13 +105,13 @@ WEATHER_EXPECTED_OBSERVATION_IMPERIAL = { ), ATTR_WEATHER_WIND_BEARING: 180, ATTR_WEATHER_WIND_SPEED: round( - convert_speed(10, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR) + convert_speed(10, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR), 2 ), ATTR_WEATHER_PRESSURE: round( convert_pressure(100000, PRESSURE_PA, PRESSURE_INHG), 2 ), ATTR_WEATHER_VISIBILITY: round( - convert_distance(10000, LENGTH_METERS, LENGTH_MILES) + convert_distance(10000, LENGTH_METERS, LENGTH_MILES), 2 ), ATTR_WEATHER_HUMIDITY: 10, } @@ -161,7 +161,7 @@ EXPECTED_FORECAST_METRIC = { convert_temperature(10, TEMP_FAHRENHEIT, TEMP_CELSIUS), 1 ), ATTR_FORECAST_WIND_SPEED: round( - convert_speed(10, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR) + convert_speed(10, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR), 2 ), ATTR_FORECAST_WIND_BEARING: 180, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 90, From b185de0ac00bb60d123191df0a231d058dfe6dcf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 27 Jun 2022 12:10:31 +0200 Subject: [PATCH 1803/3516] Add base Entity to pylint checks (#73902) * Add base entity properties * Add special case of Mapping[xxx, Any] * Add Mapping tests * Add entity functions * Adjust docstring * Add update/async_update --- pylint/plugins/hass_enforce_type_hints.py | 147 ++++++++++++++++++++-- tests/pylint/test_enforce_type_hints.py | 102 +++++++++++++++ 2 files changed, 238 insertions(+), 11 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 9ec2fa83806..6555510ff58 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -435,6 +435,117 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { } # Overriding properties and functions are normally checked by mypy, and will only # be checked by pylint when --ignore-missing-annotations is False +_ENTITY_MATCH: list[TypeHintMatch] = [ + TypeHintMatch( + function_name="should_poll", + return_type="bool", + ), + TypeHintMatch( + function_name="unique_id", + return_type=["str", None], + ), + TypeHintMatch( + function_name="name", + return_type=["str", None], + ), + TypeHintMatch( + function_name="state", + return_type=["StateType", None, "str", "int", "float"], + ), + TypeHintMatch( + function_name="capability_attributes", + return_type=["Mapping[str, Any]", None], + ), + TypeHintMatch( + function_name="state_attributes", + return_type=["dict[str, Any]", None], + ), + TypeHintMatch( + function_name="device_state_attributes", + return_type=["Mapping[str, Any]", None], + ), + TypeHintMatch( + function_name="extra_state_attributes", + return_type=["Mapping[str, Any]", None], + ), + TypeHintMatch( + function_name="device_info", + return_type=["DeviceInfo", None], + ), + TypeHintMatch( + function_name="device_class", + return_type=["str", None], + ), + TypeHintMatch( + function_name="unit_of_measurement", + return_type=["str", None], + ), + TypeHintMatch( + function_name="icon", + return_type=["str", None], + ), + TypeHintMatch( + function_name="entity_picture", + return_type=["str", None], + ), + TypeHintMatch( + function_name="available", + return_type="bool", + ), + TypeHintMatch( + function_name="assumed_state", + return_type="bool", + ), + TypeHintMatch( + function_name="force_update", + return_type="bool", + ), + TypeHintMatch( + function_name="supported_features", + return_type=["int", None], + ), + TypeHintMatch( + function_name="context_recent_time", + return_type="timedelta", + ), + TypeHintMatch( + function_name="entity_registry_enabled_default", + return_type="bool", + ), + TypeHintMatch( + function_name="entity_registry_visible_default", + return_type="bool", + ), + TypeHintMatch( + function_name="attribution", + return_type=["str", None], + ), + TypeHintMatch( + function_name="entity_category", + return_type=["EntityCategory", None], + ), + TypeHintMatch( + function_name="async_removed_from_registry", + return_type=None, + ), + TypeHintMatch( + function_name="async_added_to_hass", + return_type=None, + ), + TypeHintMatch( + function_name="async_will_remove_from_hass", + return_type=None, + ), + TypeHintMatch( + function_name="async_registry_entry_updated", + return_type=None, + ), + TypeHintMatch( + function_name="update", + return_type=None, + has_async_counterpart=True, + ), +] _TOGGLE_ENTITY_MATCH: list[TypeHintMatch] = [ TypeHintMatch( function_name="is_on", @@ -461,6 +572,10 @@ _TOGGLE_ENTITY_MATCH: list[TypeHintMatch] = [ ] _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { "fan": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), ClassTypeHintMatch( base_class="ToggleEntity", matches=_TOGGLE_ENTITY_MATCH, @@ -488,14 +603,6 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { function_name="oscillating", return_type=["bool", None], ), - TypeHintMatch( - function_name="capability_attributes", - return_type="dict[str]", - ), - TypeHintMatch( - function_name="supported_features", - return_type="int", - ), TypeHintMatch( function_name="preset_mode", return_type=["str", None], @@ -542,6 +649,10 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), ], "lock": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), ClassTypeHintMatch( base_class="LockEntity", matches=[ @@ -594,7 +705,9 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { def _is_valid_type( - expected_type: list[str] | str | None | object, node: nodes.NodeNG + expected_type: list[str] | str | None | object, + node: nodes.NodeNG, + in_return: bool = False, ) -> bool: """Check the argument node against the expected type.""" if expected_type is UNDEFINED: @@ -602,7 +715,7 @@ def _is_valid_type( if isinstance(expected_type, list): for expected_type_item in expected_type: - if _is_valid_type(expected_type_item, node): + if _is_valid_type(expected_type_item, node, in_return): return True return False @@ -638,6 +751,18 @@ def _is_valid_type( # Special case for xxx[yyy, zzz]` if match := _TYPE_HINT_MATCHERS["x_of_y_comma_z"].match(expected_type): + # Handle special case of Mapping[xxx, Any] + if in_return and match.group(1) == "Mapping" and match.group(3) == "Any": + return ( + isinstance(node, nodes.Subscript) + and isinstance(node.value, nodes.Name) + # We accept dict when Mapping is needed + and node.value.name in ("Mapping", "dict") + and isinstance(node.slice, nodes.Tuple) + and _is_valid_type(match.group(2), node.slice.elts[0]) + # Ignore second item + # and _is_valid_type(match.group(3), node.slice.elts[1]) + ) return ( isinstance(node, nodes.Subscript) and _is_valid_type(match.group(1), node.value) @@ -663,7 +788,7 @@ def _is_valid_type( def _is_valid_return_type(match: TypeHintMatch, node: nodes.NodeNG) -> bool: - if _is_valid_type(match.return_type, node): + if _is_valid_type(match.return_type, node, True): return True if isinstance(node, nodes.BinOp): diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 54c7cf6ec4c..8b4b8d4d058 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -635,3 +635,105 @@ def test_named_arguments( ), ): type_hint_checker.visit_classdef(class_node) + + +@pytest.mark.parametrize( + "return_hint", + [ + "", + "-> Mapping[int, int]", + "-> dict[int, Any]", + ], +) +def test_invalid_mapping_return_type( + linter: UnittestLinter, + type_hint_checker: BaseChecker, + return_hint: str, +) -> None: + """Check that Mapping[xxx, Any] doesn't accept invalid Mapping or dict.""" + # Set bypass option + type_hint_checker.config.ignore_missing_annotations = False + + class_node, property_node = astroid.extract_node( + f""" + class Entity(): + pass + + class ToggleEntity(Entity): + pass + + class FanEntity(ToggleEntity): + pass + + class MyFanA( #@ + FanEntity + ): + @property + def capability_attributes( #@ + self + ){return_hint}: + pass + """, + "homeassistant.components.pylint_test.fan", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_adds_messages( + linter, + pylint.testutils.MessageTest( + msg_id="hass-return-type", + node=property_node, + args=["Mapping[str, Any]", None], + line=15, + col_offset=4, + end_line=15, + end_col_offset=29, + ), + ): + type_hint_checker.visit_classdef(class_node) + + +@pytest.mark.parametrize( + "return_hint", + [ + "-> Mapping[str, Any]", + "-> Mapping[str, bool | int]", + "-> dict[str, Any]", + "-> dict[str, str]", + ], +) +def test_valid_mapping_return_type( + linter: UnittestLinter, + type_hint_checker: BaseChecker, + return_hint: str, +) -> None: + """Check that Mapping[xxx, Any] accepts both Mapping and dict.""" + # Set bypass option + type_hint_checker.config.ignore_missing_annotations = False + + class_node = astroid.extract_node( + f""" + class Entity(): + pass + + class ToggleEntity(Entity): + pass + + class FanEntity(ToggleEntity): + pass + + class MyFanA( #@ + FanEntity + ): + @property + def capability_attributes( + self + ){return_hint}: + pass + """, + "homeassistant.components.pylint_test.fan", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_no_messages(linter): + type_hint_checker.visit_classdef(class_node) From 10ea88e0ea839829551a6a6018cc1b3b81e23413 Mon Sep 17 00:00:00 2001 From: RenierM26 <66512715+RenierM26@users.noreply.github.com> Date: Mon, 27 Jun 2022 13:56:51 +0200 Subject: [PATCH 1804/3516] Switchbot bump Dependency 0.14.0 (#74001) * Bump requirement. * Switchbot depenacy update, full async. * Update tests, remove redundant config entry check. * Update requirements_test_all.txt * Update requirements_all.txt * Remove asyncio lock. Not required anymore with bleak. * Update requirements_all.txt * Update requirements_test_all.txt * pyswitchbot no longer uses bluepy --- .../components/switchbot/__init__.py | 15 +------- .../components/switchbot/binary_sensor.py | 5 +-- .../components/switchbot/config_flow.py | 15 +++----- homeassistant/components/switchbot/const.py | 1 - .../components/switchbot/coordinator.py | 18 +++------- homeassistant/components/switchbot/cover.py | 36 +++++++------------ .../components/switchbot/manifest.json | 2 +- homeassistant/components/switchbot/sensor.py | 5 +-- homeassistant/components/switchbot/switch.py | 28 +++++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/gen_requirements_all.py | 1 - tests/components/switchbot/conftest.py | 17 +++++++-- 13 files changed, 59 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 0059d655767..68fbd4bd584 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -1,17 +1,14 @@ """Support for Switchbot devices.""" -from asyncio import Lock -import switchbot # pylint: disable=import-error +import switchbot from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_SENSOR_TYPE, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from .const import ( ATTR_BOT, ATTR_CURTAIN, - BTLE_LOCK, COMMON_OPTIONS, CONF_RETRY_COUNT, CONF_RETRY_TIMEOUT, @@ -50,12 +47,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Uses BTLE advertisement data, all Switchbot devices in range is stored here. if DATA_COORDINATOR not in hass.data[DOMAIN]: - # Check if asyncio.lock is stored in hass data. - # BTLE has issues with multiple connections, - # so we use a lock to ensure that only one API request is reaching it at a time: - if BTLE_LOCK not in hass.data[DOMAIN]: - hass.data[DOMAIN][BTLE_LOCK] = Lock() - if COMMON_OPTIONS not in hass.data[DOMAIN]: hass.data[DOMAIN][COMMON_OPTIONS] = {**entry.options} @@ -72,7 +63,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api=switchbot, retry_count=hass.data[DOMAIN][COMMON_OPTIONS][CONF_RETRY_COUNT], scan_timeout=hass.data[DOMAIN][COMMON_OPTIONS][CONF_SCAN_TIMEOUT], - api_lock=hass.data[DOMAIN][BTLE_LOCK], ) hass.data[DOMAIN][DATA_COORDINATOR] = coordinator @@ -82,9 +72,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - if not coordinator.last_update_success: - raise ConfigEntryNotReady - entry.async_on_unload(entry.add_update_listener(_async_update_listener)) hass.data[DOMAIN][entry.entry_id] = {DATA_COORDINATOR: coordinator} diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index c3f88e924ea..e2a5a951d1d 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -8,6 +8,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -33,8 +34,8 @@ async def async_setup_entry( DATA_COORDINATOR ] - if not coordinator.data[entry.unique_id].get("data"): - return + if not coordinator.data.get(entry.unique_id): + raise PlatformNotReady async_add_entities( [ diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 70e032414a7..362f3b01ae7 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -1,11 +1,10 @@ """Config flow for Switchbot.""" from __future__ import annotations -from asyncio import Lock import logging from typing import Any -from switchbot import GetSwitchbotDevices # pylint: disable=import-error +from switchbot import GetSwitchbotDevices import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow @@ -14,7 +13,6 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from .const import ( - BTLE_LOCK, CONF_RETRY_COUNT, CONF_RETRY_TIMEOUT, CONF_SCAN_TIMEOUT, @@ -30,10 +28,10 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def _btle_connect() -> dict: +async def _btle_connect() -> dict: """Scan for BTLE advertisement data.""" - switchbot_devices = GetSwitchbotDevices().discover() + switchbot_devices = await GetSwitchbotDevices().discover() if not switchbot_devices: raise NotConnectedError("Failed to discover switchbot") @@ -52,14 +50,9 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): # store asyncio.lock in hass data if not present. if DOMAIN not in self.hass.data: self.hass.data.setdefault(DOMAIN, {}) - if BTLE_LOCK not in self.hass.data[DOMAIN]: - self.hass.data[DOMAIN][BTLE_LOCK] = Lock() - - connect_lock = self.hass.data[DOMAIN][BTLE_LOCK] # Discover switchbots nearby. - async with connect_lock: - _btle_adv_data = await self.hass.async_add_executor_job(_btle_connect) + _btle_adv_data = await _btle_connect() return _btle_adv_data diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index 8ca7fadf41c..b1587e97c10 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -22,5 +22,4 @@ CONF_SCAN_TIMEOUT = "scan_timeout" # Data DATA_COORDINATOR = "coordinator" -BTLE_LOCK = "btle_lock" COMMON_OPTIONS = "common_options" diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index e901cc539ea..e8e2e240dc6 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -1,11 +1,10 @@ """Provides the switchbot DataUpdateCoordinator.""" from __future__ import annotations -from asyncio import Lock from datetime import timedelta import logging -import switchbot # pylint: disable=import-error +import switchbot from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -26,7 +25,6 @@ class SwitchbotDataUpdateCoordinator(DataUpdateCoordinator): api: switchbot, retry_count: int, scan_timeout: int, - api_lock: Lock, ) -> None: """Initialize global switchbot data updater.""" self.switchbot_api = api @@ -39,20 +37,12 @@ class SwitchbotDataUpdateCoordinator(DataUpdateCoordinator): hass, _LOGGER, name=DOMAIN, update_interval=self.update_interval ) - self.api_lock = api_lock - - def _update_data(self) -> dict | None: - """Fetch device states from switchbot api.""" - - return self.switchbot_data.discover( - retry=self.retry_count, scan_timeout=self.scan_timeout - ) - async def _async_update_data(self) -> dict | None: """Fetch data from switchbot.""" - async with self.api_lock: - switchbot_data = await self.hass.async_add_executor_job(self._update_data) + switchbot_data = await self.switchbot_data.discover( + retry=self.retry_count, scan_timeout=self.scan_timeout + ) if not switchbot_data: raise UpdateFailed("Unable to fetch switchbot services data") diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index 9f265d696ad..9223217c173 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from typing import Any -from switchbot import SwitchbotCurtain # pylint: disable=import-error +from switchbot import SwitchbotCurtain from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, @@ -16,6 +16,7 @@ from homeassistant.components.cover import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -36,6 +37,9 @@ async def async_setup_entry( DATA_COORDINATOR ] + if not coordinator.data.get(entry.unique_id): + raise PlatformNotReady + async_add_entities( [ SwitchBotCurtainEntity( @@ -94,44 +98,30 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): """Open the curtain.""" _LOGGER.debug("Switchbot to open curtain %s", self._mac) - - async with self.coordinator.api_lock: - self._last_run_success = bool( - await self.hass.async_add_executor_job(self._device.open) - ) + self._last_run_success = bool(await self._device.open()) + self.async_write_ha_state() async def async_close_cover(self, **kwargs: Any) -> None: """Close the curtain.""" _LOGGER.debug("Switchbot to close the curtain %s", self._mac) - - async with self.coordinator.api_lock: - self._last_run_success = bool( - await self.hass.async_add_executor_job(self._device.close) - ) + self._last_run_success = bool(await self._device.close()) + self.async_write_ha_state() async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the moving of this device.""" _LOGGER.debug("Switchbot to stop %s", self._mac) - - async with self.coordinator.api_lock: - self._last_run_success = bool( - await self.hass.async_add_executor_job(self._device.stop) - ) + self._last_run_success = bool(await self._device.stop()) + self.async_write_ha_state() async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover shutter to a specific position.""" position = kwargs.get(ATTR_POSITION) _LOGGER.debug("Switchbot to move at %d %s", position, self._mac) - - async with self.coordinator.api_lock: - self._last_run_success = bool( - await self.hass.async_add_executor_job( - self._device.set_position, position - ) - ) + self._last_run_success = bool(await self._device.set_position(position)) + self.async_write_ha_state() @callback def _handle_coordinator_update(self) -> None: diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 7a16225dcbb..cb485ffd8a5 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.13.3"], + "requirements": ["PySwitchbot==0.14.0"], "config_flow": true, "codeowners": ["@danielhiversen", "@RenierM26"], "iot_class": "local_polling", diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index 1ee0276b7ee..759a504d19a 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -14,6 +14,7 @@ from homeassistant.const import ( SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -53,8 +54,8 @@ async def async_setup_entry( DATA_COORDINATOR ] - if not coordinator.data[entry.unique_id].get("data"): - return + if not coordinator.data.get(entry.unique_id): + raise PlatformNotReady async_add_entities( [ diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index b5507594521..404a92eda82 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -4,12 +4,13 @@ from __future__ import annotations import logging from typing import Any -from switchbot import Switchbot # pylint: disable=import-error +from switchbot import Switchbot from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, STATE_ON from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import entity_platform from homeassistant.helpers.restore_state import RestoreEntity @@ -32,6 +33,9 @@ async def async_setup_entry( DATA_COORDINATOR ] + if not coordinator.data.get(entry.unique_id): + raise PlatformNotReady + async_add_entities( [ SwitchBotBotEntity( @@ -80,25 +84,19 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): """Turn device on.""" _LOGGER.info("Turn Switchbot bot on %s", self._mac) - async with self.coordinator.api_lock: - self._last_run_success = bool( - await self.hass.async_add_executor_job(self._device.turn_on) - ) - if self._last_run_success: - self._attr_is_on = True - self.async_write_ha_state() + self._last_run_success = bool(await self._device.turn_on()) + if self._last_run_success: + self._attr_is_on = True + self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: """Turn device off.""" _LOGGER.info("Turn Switchbot bot off %s", self._mac) - async with self.coordinator.api_lock: - self._last_run_success = bool( - await self.hass.async_add_executor_job(self._device.turn_off) - ) - if self._last_run_success: - self._attr_is_on = False - self.async_write_ha_state() + self._last_run_success = bool(await self._device.turn_off()) + if self._last_run_success: + self._attr_is_on = False + self.async_write_ha_state() @property def assumed_state(self) -> bool: diff --git a/requirements_all.txt b/requirements_all.txt index 95cbdd5b0bf..3173a74468b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -34,7 +34,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -# PySwitchbot==0.13.3 +PySwitchbot==0.14.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f9f095161ea..ffab3df0314 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -30,7 +30,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -# PySwitchbot==0.13.3 +PySwitchbot==0.14.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 5db46ffbeba..a2a0eab897a 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -30,7 +30,6 @@ COMMENT_REQUIREMENTS = ( "opencv-python-headless", "pybluez", "pycups", - "PySwitchbot", "pySwitchmate", "python-eq3bt", "python-gammu", diff --git a/tests/components/switchbot/conftest.py b/tests/components/switchbot/conftest.py index 2fdea69de16..550aeb08082 100644 --- a/tests/components/switchbot/conftest.py +++ b/tests/components/switchbot/conftest.py @@ -45,6 +45,19 @@ class MocGetSwitchbotDevices: "model": "m", "rawAdvData": "000d6d00", }, + "c0ceb0d426be": { + "mac_address": "c0:ce:b0:d4:26:be", + "isEncrypted": False, + "data": { + "temp": {"c": 21.6, "f": 70.88}, + "fahrenheit": False, + "humidity": 73, + "battery": 100, + "rssi": -58, + }, + "model": "T", + "modelName": "WoSensorTH", + }, } self._curtain_all_services_data = { "mac_address": "e7:89:43:90:90:90", @@ -72,11 +85,11 @@ class MocGetSwitchbotDevices: "modelName": "WoOther", } - def discover(self, retry=0, scan_timeout=0): + async def discover(self, retry=0, scan_timeout=0): """Mock discover.""" return self._all_services_data - def get_device_data(self, mac=None): + async def get_device_data(self, mac=None): """Return data for specific device.""" if mac == "e7:89:43:99:99:99": return self._all_services_data From f9c83dd99193f79fbf34ed45ae4d3148519d2f77 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 27 Jun 2022 14:58:23 +0200 Subject: [PATCH 1805/3516] Add CoverEntity to pylint checks (#74036) * Add CoverEntity to pylint checks * Avoid false positivies on device_class * Adjust device_class handling * Adjust device_class again using a singleton * Adjust device_class (again) * Simplify DEVICE_CLASS check * Keep device_class in base class --- pylint/plugins/hass_enforce_type_hints.py | 115 +++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 6555510ff58..711f40b26ef 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -10,6 +10,7 @@ from pylint.lint import PyLinter from homeassistant.const import Platform +DEVICE_CLASS = object() UNDEFINED = object() _PLATFORMS: set[str] = {platform.value for platform in Platform} @@ -474,7 +475,7 @@ _ENTITY_MATCH: list[TypeHintMatch] = [ ), TypeHintMatch( function_name="device_class", - return_type=["str", None], + return_type=[DEVICE_CLASS, "str", None], ), TypeHintMatch( function_name="unit_of_measurement", @@ -571,6 +572,101 @@ _TOGGLE_ENTITY_MATCH: list[TypeHintMatch] = [ ), ] _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { + "cover": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="CoverEntity", + matches=[ + TypeHintMatch( + function_name="device_class", + return_type=["CoverDeviceClass", "str", None], + ), + TypeHintMatch( + function_name="current_cover_position", + return_type=["int", None], + ), + TypeHintMatch( + function_name="current_cover_tilt_position", + return_type=["int", None], + ), + TypeHintMatch( + function_name="is_opening", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="is_closing", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="is_closed", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="open_cover", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="close_cover", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="toggle", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_cover_position", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="stop_cover", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="open_cover_tilt", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="close_cover_tilt", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_cover_tilt_position", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="stop_cover_tilt", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="toggle_tilt", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ], "fan": [ ClassTypeHintMatch( base_class="Entity", @@ -583,6 +679,10 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ClassTypeHintMatch( base_class="FanEntity", matches=[ + TypeHintMatch( + function_name="device_class", + return_type=["str", None], + ), TypeHintMatch( function_name="percentage", return_type=["int", None], @@ -656,6 +756,10 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ClassTypeHintMatch( base_class="LockEntity", matches=[ + TypeHintMatch( + function_name="device_class", + return_type=["str", None], + ), TypeHintMatch( function_name="changed_by", return_type=["str", None], @@ -713,6 +817,15 @@ def _is_valid_type( if expected_type is UNDEFINED: return True + # Special case for device_class + if expected_type == DEVICE_CLASS and in_return: + return ( + isinstance(node, nodes.Name) + and node.name.endswith("DeviceClass") + or isinstance(node, nodes.Attribute) + and node.attrname.endswith("DeviceClass") + ) + if isinstance(expected_type, list): for expected_type_item in expected_type: if _is_valid_type(expected_type_item, node, in_return): From 320fa25a99c08b65334d156e1853699993b7c902 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 27 Jun 2022 19:50:56 +0200 Subject: [PATCH 1806/3516] Fix re-login logic when UniFi integration receives a 401 (#74013) --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index d481f0d0fc4..510659c56d3 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==32"], + "requirements": ["aiounifi==33"], "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 3173a74468b..95e4f539a27 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -259,7 +259,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==32 +aiounifi==33 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ffab3df0314..4bd6c87f04f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -228,7 +228,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==32 +aiounifi==33 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From 992ceb1a09af00e2a5473729e6d02b287cffbba2 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 27 Jun 2022 20:24:15 +0200 Subject: [PATCH 1807/3516] Google Assistant diagnostics and synchronization (#73574) * Add config flow import for local google assistant * Add diagnostic with sync response * Add button for device sync --- .../components/google_assistant/__init__.py | 49 +++++++- .../components/google_assistant/button.py | 53 +++++++++ .../google_assistant/config_flow.py | 19 +++ .../components/google_assistant/const.py | 2 + .../google_assistant/diagnostics.py | 41 +++++++ .../components/google_assistant/helpers.py | 4 +- .../components/google_assistant/smart_home.py | 32 ++--- .../google_assistant/test_button.py | 55 +++++++++ .../google_assistant/test_diagnostics.py | 109 ++++++++++++++++++ .../components/google_assistant/test_init.py | 38 +++++- 10 files changed, 384 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/google_assistant/button.py create mode 100644 homeassistant/components/google_assistant/config_flow.py create mode 100644 homeassistant/components/google_assistant/diagnostics.py create mode 100644 tests/components/google_assistant/test_button.py create mode 100644 tests/components/google_assistant/test_diagnostics.py diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index dca436d8e2a..638ccfd9133 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -5,9 +5,10 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.typing import ConfigType from .const import ( @@ -23,6 +24,7 @@ from .const import ( CONF_ROOM_HINT, CONF_SECURE_DEVICES_PIN, CONF_SERVICE_ACCOUNT, + DATA_CONFIG, DEFAULT_EXPOSE_BY_DEFAULT, DEFAULT_EXPOSED_DOMAINS, DOMAIN, @@ -37,6 +39,8 @@ _LOGGER = logging.getLogger(__name__) CONF_ALLOW_UNLOCK = "allow_unlock" +PLATFORMS = [Platform.BUTTON] + ENTITY_SCHEMA = vol.Schema( { vol.Optional(CONF_NAME): cv.string, @@ -95,11 +99,48 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: if DOMAIN not in yaml_config: return True - config = yaml_config[DOMAIN] + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CONFIG] = yaml_config[DOMAIN] + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_PROJECT_ID: yaml_config[DOMAIN][CONF_PROJECT_ID]}, + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up from a config entry.""" + + config: ConfigType = {**hass.data[DOMAIN][DATA_CONFIG]} + + if entry.source == SOURCE_IMPORT: + # if project was changed, remove entry a new will be setup + if config[CONF_PROJECT_ID] != entry.data[CONF_PROJECT_ID]: + hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) + return False + + config.update(entry.data) + + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, config[CONF_PROJECT_ID])}, + manufacturer="Google", + model="Google Assistant", + name=config[CONF_PROJECT_ID], + entry_type=dr.DeviceEntryType.SERVICE, + ) google_config = GoogleConfig(hass, config) await google_config.async_initialize() + hass.data[DOMAIN][entry.entry_id] = google_config + hass.http.register_view(GoogleAssistantView(google_config)) if google_config.should_report_state: @@ -123,4 +164,6 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True diff --git a/homeassistant/components/google_assistant/button.py b/homeassistant/components/google_assistant/button.py new file mode 100644 index 00000000000..322a021053a --- /dev/null +++ b/homeassistant/components/google_assistant/button.py @@ -0,0 +1,53 @@ +"""Support for buttons.""" +from __future__ import annotations + +from homeassistant import config_entries +from homeassistant.components.button import ButtonEntity +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType + +from .const import CONF_PROJECT_ID, CONF_SERVICE_ACCOUNT, DATA_CONFIG, DOMAIN +from .http import GoogleConfig + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the platform.""" + yaml_config: ConfigType = hass.data[DOMAIN][DATA_CONFIG] + google_config: GoogleConfig = hass.data[DOMAIN][config_entry.entry_id] + + entities = [] + + if CONF_SERVICE_ACCOUNT in yaml_config: + entities.append(SyncButton(config_entry.data[CONF_PROJECT_ID], google_config)) + + async_add_entities(entities) + + +class SyncButton(ButtonEntity): + """Representation of a synchronization button.""" + + def __init__(self, project_id: str, google_config: GoogleConfig) -> None: + """Initialize button.""" + super().__init__() + self._google_config = google_config + self._attr_entity_category = EntityCategory.DIAGNOSTIC + self._attr_unique_id = f"{project_id}_sync" + self._attr_name = "Synchronize Devices" + self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, project_id)}) + + async def async_press(self) -> None: + """Press the button.""" + assert self._context + agent_user_id = self._google_config.get_agent_user_id(self._context) + result = await self._google_config.async_sync_entities(agent_user_id) + if result != 200: + raise HomeAssistantError( + f"Unable to sync devices with result code: {result}, check log for more info." + ) diff --git a/homeassistant/components/google_assistant/config_flow.py b/homeassistant/components/google_assistant/config_flow.py new file mode 100644 index 00000000000..e8e0d9962f9 --- /dev/null +++ b/homeassistant/components/google_assistant/config_flow.py @@ -0,0 +1,19 @@ +"""Config flow for google assistant component.""" + +from homeassistant import config_entries + +from .const import CONF_PROJECT_ID, DOMAIN + + +class GoogleAssistantHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + + async def async_step_import(self, user_input): + """Import a config entry.""" + await self.async_set_unique_id(unique_id=user_input[CONF_PROJECT_ID]) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input[CONF_PROJECT_ID], data=user_input + ) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 339cddae883..dbcf60ac098 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -40,6 +40,8 @@ CONF_ROOM_HINT = "room" CONF_SECURE_DEVICES_PIN = "secure_devices_pin" CONF_SERVICE_ACCOUNT = "service_account" +DATA_CONFIG = "config" + DEFAULT_EXPOSE_BY_DEFAULT = True DEFAULT_EXPOSED_DOMAINS = [ "alarm_control_panel", diff --git a/homeassistant/components/google_assistant/diagnostics.py b/homeassistant/components/google_assistant/diagnostics.py new file mode 100644 index 00000000000..01e17e0bcf8 --- /dev/null +++ b/homeassistant/components/google_assistant/diagnostics.py @@ -0,0 +1,41 @@ +"""Diagnostics support for Hue.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.components.diagnostics.const import REDACTED +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + +from .const import CONF_SECURE_DEVICES_PIN, CONF_SERVICE_ACCOUNT, DATA_CONFIG, DOMAIN +from .http import GoogleConfig +from .smart_home import async_devices_sync_response, create_sync_response + +TO_REDACT = [ + "uuid", + "baseUrl", + "webhookId", + CONF_SERVICE_ACCOUNT, + CONF_SECURE_DEVICES_PIN, + CONF_API_KEY, +] + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostic information.""" + data = hass.data[DOMAIN] + config: GoogleConfig = data[entry.entry_id] + yaml_config: ConfigType = data[DATA_CONFIG] + devices = await async_devices_sync_response(hass, config, REDACTED) + sync = create_sync_response(REDACTED, devices) + + return { + "config_entry": async_redact_data(entry.as_dict(), TO_REDACT), + "yaml_config": async_redact_data(yaml_config, TO_REDACT), + "sync": async_redact_data(sync, TO_REDACT), + } diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 932611390eb..6f81ddebdb4 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -160,7 +160,9 @@ class AbstractConfig(ABC): def get_local_webhook_id(self, agent_user_id): """Return the webhook ID to be used for actions for a given agent user id via the local SDK.""" - return self._store.agent_user_ids[agent_user_id][STORE_GOOGLE_LOCAL_WEBHOOK_ID] + if data := self._store.agent_user_ids.get(agent_user_id): + return data[STORE_GOOGLE_LOCAL_WEBHOOK_ID] + return None @abstractmethod def get_agent_user_id(self, context): diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 227b033bcaa..75a3fd76b9b 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -71,6 +71,24 @@ async def _process(hass, data, message): return {"requestId": data.request_id, "payload": result} +async def async_devices_sync_response(hass, config, agent_user_id): + """Generate the device serialization.""" + entities = async_get_entities(hass, config) + instance_uuid = await instance_id.async_get(hass) + devices = [] + + for entity in entities: + if not entity.should_expose(): + continue + + try: + devices.append(entity.sync_serialize(agent_user_id, instance_uuid)) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error serializing %s", entity.entity_id) + + return devices + + @HANDLERS.register("action.devices.SYNC") async def async_devices_sync(hass, data, payload): """Handle action.devices.SYNC request. @@ -86,19 +104,7 @@ async def async_devices_sync(hass, data, payload): agent_user_id = data.config.get_agent_user_id(data.context) await data.config.async_connect_agent_user(agent_user_id) - entities = async_get_entities(hass, data.config) - instance_uuid = await instance_id.async_get(hass) - devices = [] - - for entity in entities: - if not entity.should_expose(): - continue - - try: - devices.append(entity.sync_serialize(agent_user_id, instance_uuid)) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error serializing %s", entity.entity_id) - + devices = await async_devices_sync_response(hass, data.config, agent_user_id) response = create_sync_response(agent_user_id, devices) _LOGGER.debug("Syncing entities response: %s", response) diff --git a/tests/components/google_assistant/test_button.py b/tests/components/google_assistant/test_button.py new file mode 100644 index 00000000000..0783b70dff3 --- /dev/null +++ b/tests/components/google_assistant/test_button.py @@ -0,0 +1,55 @@ +"""Test buttons.""" + +from unittest.mock import patch + +from pytest import raises + +from homeassistant.components import google_assistant as ga +from homeassistant.core import Context, HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component + +from .test_http import DUMMY_CONFIG + +from tests.common import MockUser + + +async def test_sync_button(hass: HomeAssistant, hass_owner_user: MockUser): + """Test sync button.""" + + await async_setup_component( + hass, + ga.DOMAIN, + {"google_assistant": DUMMY_CONFIG}, + ) + + await hass.async_block_till_done() + + state = hass.states.get("button.synchronize_devices") + assert state + + config_entry = hass.config_entries.async_entries("google_assistant")[0] + google_config: ga.GoogleConfig = hass.data[ga.DOMAIN][config_entry.entry_id] + + with patch.object(google_config, "async_sync_entities") as mock_sync_entities: + mock_sync_entities.return_value = 200 + context = Context(user_id=hass_owner_user.id) + await hass.services.async_call( + "button", + "press", + {"entity_id": "button.synchronize_devices"}, + blocking=True, + context=context, + ) + mock_sync_entities.assert_called_once_with(hass_owner_user.id) + + with raises(HomeAssistantError): + mock_sync_entities.return_value = 400 + + await hass.services.async_call( + "button", + "press", + {"entity_id": "button.synchronize_devices"}, + blocking=True, + context=context, + ) diff --git a/tests/components/google_assistant/test_diagnostics.py b/tests/components/google_assistant/test_diagnostics.py new file mode 100644 index 00000000000..13721c17f88 --- /dev/null +++ b/tests/components/google_assistant/test_diagnostics.py @@ -0,0 +1,109 @@ +"""Test diagnostics.""" + +from typing import Any +from unittest.mock import ANY + +from homeassistant import core, setup +from homeassistant.components import google_assistant as ga, switch +from homeassistant.setup import async_setup_component + +from .test_http import DUMMY_CONFIG + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics(hass: core.HomeAssistant, hass_client: Any): + """Test diagnostics v1.""" + + await setup.async_setup_component( + hass, switch.DOMAIN, {"switch": [{"platform": "demo"}]} + ) + + await async_setup_component( + hass, + ga.DOMAIN, + {"google_assistant": DUMMY_CONFIG}, + ) + + config_entry = hass.config_entries.async_entries("google_assistant")[0] + result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + assert result == { + "config_entry": { + "data": {"project_id": "1234"}, + "disabled_by": None, + "domain": "google_assistant", + "entry_id": ANY, + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "import", + "title": "1234", + "unique_id": "1234", + "version": 1, + }, + "sync": { + "agentUserId": "**REDACTED**", + "devices": [ + { + "attributes": {"commandOnlyOnOff": True}, + "id": "switch.decorative_lights", + "otherDeviceIds": [{"deviceId": "switch.decorative_lights"}], + "name": {"name": "Decorative Lights"}, + "traits": ["action.devices.traits.OnOff"], + "type": "action.devices.types.SWITCH", + "willReportState": False, + "customData": { + "baseUrl": "**REDACTED**", + "httpPort": 8123, + "httpSSL": False, + "proxyDeviceId": "**REDACTED**", + "uuid": "**REDACTED**", + "webhookId": None, + }, + }, + { + "attributes": {}, + "id": "switch.ac", + "otherDeviceIds": [{"deviceId": "switch.ac"}], + "name": {"name": "AC"}, + "traits": ["action.devices.traits.OnOff"], + "type": "action.devices.types.OUTLET", + "willReportState": False, + "customData": { + "baseUrl": "**REDACTED**", + "httpPort": 8123, + "httpSSL": False, + "proxyDeviceId": "**REDACTED**", + "uuid": "**REDACTED**", + "webhookId": None, + }, + }, + ], + }, + "yaml_config": { + "expose_by_default": True, + "exposed_domains": [ + "alarm_control_panel", + "binary_sensor", + "climate", + "cover", + "fan", + "group", + "humidifier", + "input_boolean", + "input_select", + "light", + "lock", + "media_player", + "scene", + "script", + "select", + "sensor", + "switch", + "vacuum", + ], + "project_id": "1234", + "report_state": False, + "service_account": "**REDACTED**", + }, + } diff --git a/tests/components/google_assistant/test_init.py b/tests/components/google_assistant/test_init.py index 69198b99aaa..bdd6932c91d 100644 --- a/tests/components/google_assistant/test_init.py +++ b/tests/components/google_assistant/test_init.py @@ -2,11 +2,47 @@ from http import HTTPStatus from homeassistant.components import google_assistant as ga -from homeassistant.core import Context +from homeassistant.core import Context, HomeAssistant from homeassistant.setup import async_setup_component from .test_http import DUMMY_CONFIG +from tests.common import MockConfigEntry + + +async def test_import(hass: HomeAssistant): + """Test import.""" + + await async_setup_component( + hass, + ga.DOMAIN, + {"google_assistant": DUMMY_CONFIG}, + ) + + entries = hass.config_entries.async_entries("google_assistant") + assert len(entries) == 1 + assert entries[0].data[ga.const.CONF_PROJECT_ID] == "1234" + + +async def test_import_changed(hass: HomeAssistant): + """Test import with changed project id.""" + + old_entry = MockConfigEntry( + domain=ga.DOMAIN, data={ga.const.CONF_PROJECT_ID: "4321"}, source="import" + ) + old_entry.add_to_hass(hass) + + await async_setup_component( + hass, + ga.DOMAIN, + {"google_assistant": DUMMY_CONFIG}, + ) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries("google_assistant") + assert len(entries) == 1 + assert entries[0].data[ga.const.CONF_PROJECT_ID] == "1234" + async def test_request_sync_service(aioclient_mock, hass): """Test that it posts to the request_sync url.""" From 5f06404db5cbff7abf8b0436d10b6b7b03e6faf2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 27 Jun 2022 20:25:36 +0200 Subject: [PATCH 1808/3516] Migrate tomorrowio to native_* (#74050) --- .../components/tomorrowio/weather.py | 34 +++++++++---------- tests/components/tomorrowio/test_weather.py | 10 ++++++ 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/tomorrowio/weather.py b/homeassistant/components/tomorrowio/weather.py index 346f673362e..07ea079b1ce 100644 --- a/homeassistant/components/tomorrowio/weather.py +++ b/homeassistant/components/tomorrowio/weather.py @@ -8,13 +8,13 @@ from pytomorrowio.const import DAILY, FORECASTS, HOURLY, NOWCAST, WeatherCode from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry @@ -74,11 +74,11 @@ async def async_setup_entry( class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): """Entity that talks to Tomorrow.io v4 API to retrieve weather data.""" - _attr_temperature_unit = TEMP_CELSIUS - _attr_pressure_unit = PRESSURE_HPA - _attr_wind_speed_unit = SPEED_METERS_PER_SECOND - _attr_visibility_unit = LENGTH_KILOMETERS - _attr_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_visibility_unit = LENGTH_KILOMETERS + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND def __init__( self, @@ -119,12 +119,12 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): data = { ATTR_FORECAST_TIME: forecast_dt.isoformat(), ATTR_FORECAST_CONDITION: translated_condition, - ATTR_FORECAST_PRECIPITATION: precipitation, + ATTR_FORECAST_NATIVE_PRECIPITATION: precipitation, ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability, - ATTR_FORECAST_TEMP: temp, - ATTR_FORECAST_TEMP_LOW: temp_low, + ATTR_FORECAST_NATIVE_TEMP: temp, + ATTR_FORECAST_NATIVE_TEMP_LOW: temp_low, ATTR_FORECAST_WIND_BEARING: wind_direction, - ATTR_FORECAST_WIND_SPEED: wind_speed, + ATTR_FORECAST_NATIVE_WIND_SPEED: wind_speed, } return {k: v for k, v in data.items() if v is not None} @@ -145,12 +145,12 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): return CONDITIONS[condition] @property - def temperature(self): + def native_temperature(self): """Return the platform temperature.""" return self._get_current_property(TMRW_ATTR_TEMPERATURE) @property - def pressure(self): + def native_pressure(self): """Return the raw pressure.""" return self._get_current_property(TMRW_ATTR_PRESSURE) @@ -160,7 +160,7 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): return self._get_current_property(TMRW_ATTR_HUMIDITY) @property - def wind_speed(self): + def native_wind_speed(self): """Return the raw wind speed.""" return self._get_current_property(TMRW_ATTR_WIND_SPEED) @@ -183,7 +183,7 @@ class TomorrowioWeatherEntity(TomorrowioEntity, WeatherEntity): ) @property - def visibility(self): + def native_visibility(self): """Return the raw visibility.""" return self._get_current_property(TMRW_ATTR_VISIBILITY) diff --git a/tests/components/tomorrowio/test_weather.py b/tests/components/tomorrowio/test_weather.py index 52c29161452..3d2b0669f5b 100644 --- a/tests/components/tomorrowio/test_weather.py +++ b/tests/components/tomorrowio/test_weather.py @@ -29,11 +29,16 @@ from homeassistant.components.weather import ( ATTR_FORECAST_WIND_SPEED, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_OZONE, + ATTR_WEATHER_PRECIPITATION_UNIT, ATTR_WEATHER_PRESSURE, + ATTR_WEATHER_PRESSURE_UNIT, ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_TEMPERATURE_UNIT, ATTR_WEATHER_VISIBILITY, + ATTR_WEATHER_VISIBILITY_UNIT, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, + ATTR_WEATHER_WIND_SPEED_UNIT, DOMAIN as WEATHER_DOMAIN, ) from homeassistant.config_entries import SOURCE_USER @@ -104,8 +109,13 @@ async def test_v4_weather(hass: HomeAssistant) -> None: assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "Tomorrow.io - Daily" assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 23 assert weather_state.attributes[ATTR_WEATHER_OZONE] == 46.53 + assert weather_state.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == "mm" assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 30.35 + assert weather_state.attributes[ATTR_WEATHER_PRESSURE_UNIT] == "hPa" assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 44.1 + assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == "°C" assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 8.15 + assert weather_state.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == "km" assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14 assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 33.59 # 9.33 m/s ->km/h + assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] == "km/h" From 84ea8a3c438a4e2e95bb96c16b99e5ed6a86d7d7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 27 Jun 2022 20:26:04 +0200 Subject: [PATCH 1809/3516] Fix misleading comments in tomorrowio (#74049) * Fix misleading comments in tomorrowio * Add test --- homeassistant/components/tomorrowio/sensor.py | 26 +++++++-------- tests/components/tomorrowio/test_sensor.py | 32 +++++++++++++++++++ 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/tomorrowio/sensor.py b/homeassistant/components/tomorrowio/sensor.py index ed4ae915c1c..ea114af0544 100644 --- a/homeassistant/components/tomorrowio/sensor.py +++ b/homeassistant/components/tomorrowio/sensor.py @@ -28,7 +28,6 @@ from homeassistant.const import ( IRRADIATION_BTUS_PER_HOUR_SQUARE_FOOT, IRRADIATION_WATTS_PER_SQUARE_METER, LENGTH_KILOMETERS, - LENGTH_METERS, LENGTH_MILES, PERCENTAGE, PRESSURE_HPA, @@ -40,6 +39,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify from homeassistant.util.distance import convert as distance_convert +from homeassistant.util.speed import convert as speed_convert from . import TomorrowioDataUpdateCoordinator, TomorrowioEntity from .const import ( @@ -113,14 +113,14 @@ SENSOR_TYPES = ( native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), - # Data comes in as inHg + # Data comes in as hPa TomorrowioSensorEntityDescription( key=TMRW_ATTR_PRESSURE_SURFACE_LEVEL, name="Pressure (Surface Level)", native_unit_of_measurement=PRESSURE_HPA, device_class=SensorDeviceClass.PRESSURE, ), - # Data comes in as BTUs/(hr * ft^2) + # Data comes in as W/m^2, convert to BTUs/(hr * ft^2) for imperial # https://www.theunitconverter.com/watt-square-meter-to-btu-hour-square-foot-conversion/ TomorrowioSensorEntityDescription( key=TMRW_ATTR_SOLAR_GHI, @@ -129,7 +129,7 @@ SENSOR_TYPES = ( unit_metric=IRRADIATION_WATTS_PER_SQUARE_METER, imperial_conversion=(1 / 3.15459), ), - # Data comes in as miles + # Data comes in as km, convert to miles for imperial TomorrowioSensorEntityDescription( key=TMRW_ATTR_CLOUD_BASE, name="Cloud Base", @@ -139,7 +139,7 @@ SENSOR_TYPES = ( val, LENGTH_KILOMETERS, LENGTH_MILES ), ), - # Data comes in as miles + # Data comes in as km, convert to miles for imperial TomorrowioSensorEntityDescription( key=TMRW_ATTR_CLOUD_CEILING, name="Cloud Ceiling", @@ -154,16 +154,15 @@ SENSOR_TYPES = ( name="Cloud Cover", native_unit_of_measurement=PERCENTAGE, ), - # Data comes in as MPH + # Data comes in as m/s, convert to mi/h for imperial TomorrowioSensorEntityDescription( key=TMRW_ATTR_WIND_GUST, name="Wind Gust", unit_imperial=SPEED_MILES_PER_HOUR, unit_metric=SPEED_METERS_PER_SECOND, - imperial_conversion=lambda val: distance_convert( - val, LENGTH_METERS, LENGTH_MILES - ) - * 3600, + imperial_conversion=lambda val: speed_convert( + val, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR + ), ), TomorrowioSensorEntityDescription( key=TMRW_ATTR_PRECIPITATION_TYPE, @@ -172,7 +171,7 @@ SENSOR_TYPES = ( device_class="tomorrowio__precipitation_type", icon="mdi:weather-snowy-rainy", ), - # Data comes in as ppb + # Data comes in as ppb, convert to µg/m^3 # Molecular weight of Ozone is 48 TomorrowioSensorEntityDescription( key=TMRW_ATTR_OZONE, @@ -193,7 +192,7 @@ SENSOR_TYPES = ( native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM10, ), - # Data comes in as ppb + # Data comes in as ppb, convert to µg/m^3 # Molecular weight of Nitrogen Dioxide is 46.01 TomorrowioSensorEntityDescription( key=TMRW_ATTR_NITROGEN_DIOXIDE, @@ -202,7 +201,7 @@ SENSOR_TYPES = ( multiplication_factor=convert_ppb_to_ugm3(46.01), device_class=SensorDeviceClass.NITROGEN_DIOXIDE, ), - # Data comes in as ppb + # Data comes in as ppb, convert to ppm TomorrowioSensorEntityDescription( key=TMRW_ATTR_CARBON_MONOXIDE, name="Carbon Monoxide", @@ -210,6 +209,7 @@ SENSOR_TYPES = ( multiplication_factor=1 / 1000, device_class=SensorDeviceClass.CO, ), + # Data comes in as ppb, convert to µg/m^3 # Molecular weight of Sulphur Dioxide is 64.07 TomorrowioSensorEntityDescription( key=TMRW_ATTR_SULPHUR_DIOXIDE, diff --git a/tests/components/tomorrowio/test_sensor.py b/tests/components/tomorrowio/test_sensor.py index ef025204ea6..51b3db00c6e 100644 --- a/tests/components/tomorrowio/test_sensor.py +++ b/tests/components/tomorrowio/test_sensor.py @@ -25,6 +25,7 @@ from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.entity_registry import async_get from homeassistant.util import dt as dt_util +from homeassistant.util.unit_system import IMPERIAL_SYSTEM from .const import API_V4_ENTRY_DATA @@ -169,6 +170,37 @@ async def test_v4_sensor(hass: HomeAssistant) -> None: check_sensor_state(hass, PRECIPITATION_TYPE, "rain") +async def test_v4_sensor_imperial(hass: HomeAssistant) -> None: + """Test v4 sensor data.""" + hass.config.units = IMPERIAL_SYSTEM + await _setup(hass, V4_FIELDS, API_V4_ENTRY_DATA) + check_sensor_state(hass, O3, "91.35") + check_sensor_state(hass, CO, "0.0") + check_sensor_state(hass, NO2, "20.08") + check_sensor_state(hass, SO2, "4.32") + check_sensor_state(hass, PM25, "0.15") + check_sensor_state(hass, PM10, "0.57") + check_sensor_state(hass, MEP_AQI, "23") + check_sensor_state(hass, MEP_HEALTH_CONCERN, "good") + check_sensor_state(hass, MEP_PRIMARY_POLLUTANT, "pm10") + check_sensor_state(hass, EPA_AQI, "24") + check_sensor_state(hass, EPA_HEALTH_CONCERN, "good") + check_sensor_state(hass, EPA_PRIMARY_POLLUTANT, "pm25") + check_sensor_state(hass, FIRE_INDEX, "10") + check_sensor_state(hass, GRASS_POLLEN, "none") + check_sensor_state(hass, WEED_POLLEN, "none") + check_sensor_state(hass, TREE_POLLEN, "none") + check_sensor_state(hass, FEELS_LIKE, "214.3") + check_sensor_state(hass, DEW_POINT, "163.08") + check_sensor_state(hass, PRESSURE_SURFACE_LEVEL, "29.47") + check_sensor_state(hass, GHI, "0.0") + check_sensor_state(hass, CLOUD_BASE, "0.46") + check_sensor_state(hass, CLOUD_COVER, "100") + check_sensor_state(hass, CLOUD_CEILING, "0.46") + check_sensor_state(hass, WIND_GUST, "28.27") + check_sensor_state(hass, PRECIPITATION_TYPE, "rain") + + async def test_entity_description() -> None: """Test improper entity description raises.""" with pytest.raises(ValueError): From 33f5b225fb21dfda80183470cba1fb716404a3f0 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 27 Jun 2022 21:29:19 +0200 Subject: [PATCH 1810/3516] Use aiounifi v34 to utilise orjson for better performance (#74065) Bump aiounifi to v34 --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 510659c56d3..36186b6fed8 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==33"], + "requirements": ["aiounifi==34"], "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 95e4f539a27..30af6682715 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -259,7 +259,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==33 +aiounifi==34 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4bd6c87f04f..c9f7d815469 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -228,7 +228,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.4 # homeassistant.components.unifi -aiounifi==33 +aiounifi==34 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From b9c636ba4e240a03fbb65df6dab4d28cdfcaf78a Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Mon, 27 Jun 2022 17:03:25 -0400 Subject: [PATCH 1811/3516] Automatically add newly added devices for UniFi Protect (#73879) --- .../components/unifiprotect/binary_sensor.py | 31 +++++++- .../components/unifiprotect/button.py | 18 ++++- .../components/unifiprotect/camera.py | 65 ++++++++++++----- .../components/unifiprotect/const.py | 3 + homeassistant/components/unifiprotect/data.py | 73 +++++++++++++++---- .../components/unifiprotect/entity.py | 42 ++++++++--- .../components/unifiprotect/light.py | 23 +++++- homeassistant/components/unifiprotect/lock.py | 20 ++++- .../components/unifiprotect/media_player.py | 21 ++++-- .../components/unifiprotect/number.py | 28 ++++++- .../components/unifiprotect/select.py | 22 +++++- .../components/unifiprotect/sensor.py | 37 ++++++++-- .../components/unifiprotect/switch.py | 20 ++++- .../components/unifiprotect/utils.py | 10 ++- .../unifiprotect/test_binary_sensor.py | 44 +++++++++++ tests/components/unifiprotect/test_button.py | 22 +++++- tests/components/unifiprotect/test_camera.py | 29 +++++++- tests/components/unifiprotect/test_light.py | 19 ++++- tests/components/unifiprotect/test_lock.py | 21 +++++- .../unifiprotect/test_media_player.py | 21 +++++- tests/components/unifiprotect/test_number.py | 41 +++++++++++ tests/components/unifiprotect/test_select.py | 44 +++++++++++ tests/components/unifiprotect/test_sensor.py | 30 ++++++++ tests/components/unifiprotect/test_switch.py | 30 ++++++++ tests/components/unifiprotect/utils.py | 58 ++++++++++++++- 25 files changed, 696 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index eb4b2024233..598e0632fbb 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -11,6 +11,7 @@ from pyunifiprotect.data import ( Event, Light, MountType, + ProtectAdoptableDeviceModel, ProtectModelWithId, Sensor, ) @@ -23,10 +24,11 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DISPATCH_ADOPT, DOMAIN from .data import ProtectData from .entity import ( EventThumbnailMixin, @@ -35,6 +37,7 @@ from .entity import ( async_all_device_entities, ) from .models import PermRequired, ProtectRequiredKeysMixin +from .utils import async_dispatch_id as _ufpd _LOGGER = logging.getLogger(__name__) _KEY_DOOR = "door" @@ -364,6 +367,24 @@ async def async_setup_entry( ) -> None: """Set up binary sensors for UniFi Protect integration.""" data: ProtectData = hass.data[DOMAIN][entry.entry_id] + + async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: + entities: list[ProtectDeviceEntity] = async_all_device_entities( + data, + ProtectDeviceBinarySensor, + camera_descs=CAMERA_SENSORS, + light_descs=LIGHT_SENSORS, + sense_descs=SENSE_SENSORS, + lock_descs=DOORLOCK_SENSORS, + viewer_descs=VIEWER_SENSORS, + ufp_device=device, + ) + if device.is_adopted and isinstance(device, Camera): + entities += _async_motion_entities(data, ufp_device=device) + async_add_entities(entities) + + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + entities: list[ProtectDeviceEntity] = async_all_device_entities( data, ProtectDeviceBinarySensor, @@ -382,10 +403,14 @@ async def async_setup_entry( @callback def _async_motion_entities( data: ProtectData, + ufp_device: ProtectAdoptableDeviceModel | None = None, ) -> list[ProtectDeviceEntity]: entities: list[ProtectDeviceEntity] = [] - for device in data.api.bootstrap.cameras.values(): - if not device.is_adopted_by_us: + devices = ( + data.api.bootstrap.cameras.values() if ufp_device is None else [ufp_device] + ) + for device in devices: + if not device.is_adopted: continue for description in MOTION_SENSORS: diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index d647cdac64a..901139109d3 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -13,12 +13,14 @@ from homeassistant.components.button import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DISPATCH_ADOPT, DOMAIN from .data import ProtectData from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectSetableKeysMixin, T +from .utils import async_dispatch_id as _ufpd @dataclass @@ -79,6 +81,19 @@ async def async_setup_entry( """Discover devices on a UniFi Protect NVR.""" data: ProtectData = hass.data[DOMAIN][entry.entry_id] + async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: + entities = async_all_device_entities( + data, + ProtectButton, + all_descs=ALL_DEVICE_BUTTONS, + chime_descs=CHIME_BUTTONS, + sense_descs=SENSOR_BUTTONS, + ufp_device=device, + ) + async_add_entities(entities) + + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + entities: list[ProtectDeviceEntity] = async_all_device_entities( data, ProtectButton, @@ -86,7 +101,6 @@ async def async_setup_entry( chime_descs=CHIME_BUTTONS, sense_descs=SENSOR_BUTTONS, ) - async_add_entities(entities) diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index a84346a8384..336a5ae9187 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -4,10 +4,10 @@ from __future__ import annotations from collections.abc import Generator import logging -from pyunifiprotect.api import ProtectApiClient from pyunifiprotect.data import ( Camera as UFPCamera, CameraChannel, + ProtectAdoptableDeviceModel, ProtectModelWithId, StateType, ) @@ -15,6 +15,7 @@ from pyunifiprotect.data import ( from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -23,28 +24,39 @@ from .const import ( ATTR_FPS, ATTR_HEIGHT, ATTR_WIDTH, + DISPATCH_ADOPT, + DISPATCH_CHANNELS, DOMAIN, ) from .data import ProtectData from .entity import ProtectDeviceEntity +from .utils import async_dispatch_id as _ufpd _LOGGER = logging.getLogger(__name__) def get_camera_channels( - protect: ProtectApiClient, + data: ProtectData, + ufp_device: UFPCamera | None = None, ) -> Generator[tuple[UFPCamera, CameraChannel, bool], None, None]: """Get all the camera channels.""" - for camera in protect.bootstrap.cameras.values(): + + devices = ( + data.api.bootstrap.cameras.values() if ufp_device is None else [ufp_device] + ) + for camera in devices: if not camera.is_adopted_by_us: continue if not camera.channels: - _LOGGER.warning( - "Camera does not have any channels: %s (id: %s)", - camera.display_name, - camera.id, - ) + if ufp_device is None: + # only warn on startup + _LOGGER.warning( + "Camera does not have any channels: %s (id: %s)", + camera.display_name, + camera.id, + ) + data.async_add_pending_camera_id(camera.id) continue is_default = True @@ -60,17 +72,12 @@ def get_camera_channels( yield camera, camera.channels[0], True -async def async_setup_entry( - hass: HomeAssistant, - entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Discover cameras on a UniFi Protect NVR.""" - data: ProtectData = hass.data[DOMAIN][entry.entry_id] +def _async_camera_entities( + data: ProtectData, ufp_device: UFPCamera | None = None +) -> list[ProtectDeviceEntity]: disable_stream = data.disable_stream - - entities = [] - for camera, channel, is_default in get_camera_channels(data.api): + entities: list[ProtectDeviceEntity] = [] + for camera, channel, is_default in get_camera_channels(data, ufp_device): # do not enable streaming for package camera # 2 FPS causes a lot of buferring entities.append( @@ -95,6 +102,28 @@ async def async_setup_entry( disable_stream, ) ) + return entities + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Discover cameras on a UniFi Protect NVR.""" + data: ProtectData = hass.data[DOMAIN][entry.entry_id] + + async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: + if not isinstance(device, UFPCamera): + return + + entities = _async_camera_entities(data, ufp_device=device) + async_add_entities(entities) + + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_CHANNELS), _add_new_device) + + entities = _async_camera_entities(data) async_add_entities(entities) diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 3ba22e6b85b..3c29d0c9972 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -60,3 +60,6 @@ PLATFORMS = [ Platform.SENSOR, Platform.SWITCH, ] + +DISPATCH_ADOPT = "adopt_device" +DISPATCH_CHANNELS = "new_camera_channels" diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 4a20e816ce2..30887f04235 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -10,6 +10,7 @@ from pyunifiprotect import ProtectApiClient from pyunifiprotect.data import ( Bootstrap, Event, + EventType, Liveview, ModelType, ProtectAdoptableDeviceModel, @@ -20,10 +21,22 @@ from pyunifiprotect.exceptions import ClientError, NotAuthorized from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval -from .const import CONF_DISABLE_RTSP, DEVICES_THAT_ADOPT, DEVICES_WITH_ENTITIES, DOMAIN -from .utils import async_get_devices, async_get_devices_by_type +from .const import ( + CONF_DISABLE_RTSP, + DEVICES_THAT_ADOPT, + DEVICES_WITH_ENTITIES, + DISPATCH_ADOPT, + DISPATCH_CHANNELS, + DOMAIN, +) +from .utils import ( + async_dispatch_id as _ufpd, + async_get_devices, + async_get_devices_by_type, +) _LOGGER = logging.getLogger(__name__) @@ -56,6 +69,7 @@ class ProtectData: self._hass = hass self._update_interval = update_interval self._subscriptions: dict[str, list[Callable[[ProtectModelWithId], None]]] = {} + self._pending_camera_ids: set[str] = set() self._unsub_interval: CALLBACK_TYPE | None = None self._unsub_websocket: CALLBACK_TYPE | None = None @@ -117,6 +131,18 @@ class ProtectData: self.last_update_success = True self._async_process_updates(updates) + @callback + def async_add_pending_camera_id(self, camera_id: str) -> None: + """ + Add pending camera. + + A "pending camera" is one that has been adopted by not had its camera channels + initialized yet. Will cause Websocket code to check for channels to be + initialized for the camera and issue a dispatch once they do. + """ + + self._pending_camera_ids.add(camera_id) + @callback def _async_process_ws_message(self, message: WSSubscriptionMessage) -> None: # removed packets are not processed yet @@ -125,8 +151,19 @@ class ProtectData: ): return - if message.new_obj.model in DEVICES_WITH_ENTITIES: - self._async_signal_device_update(message.new_obj) + obj = message.new_obj + if obj.model in DEVICES_WITH_ENTITIES: + self._async_signal_device_update(obj) + if ( + obj.model == ModelType.CAMERA + and obj.id in self._pending_camera_ids + and "channels" in message.changed_data + ): + self._pending_camera_ids.remove(obj.id) + async_dispatcher_send( + self._hass, _ufpd(self._entry, DISPATCH_CHANNELS), obj + ) + # trigger update for all Cameras with LCD screens when NVR Doorbell settings updates if "doorbell_settings" in message.changed_data: _LOGGER.debug( @@ -137,17 +174,25 @@ class ProtectData: if camera.feature_flags.has_lcd_screen: self._async_signal_device_update(camera) # trigger updates for camera that the event references - elif isinstance(message.new_obj, Event): - if message.new_obj.camera is not None: - self._async_signal_device_update(message.new_obj.camera) - elif message.new_obj.light is not None: - self._async_signal_device_update(message.new_obj.light) - elif message.new_obj.sensor is not None: - self._async_signal_device_update(message.new_obj.sensor) + elif isinstance(obj, Event): + if obj.type == EventType.DEVICE_ADOPTED: + if obj.metadata is not None and obj.metadata.device_id is not None: + device = self.api.bootstrap.get_device_from_id( + obj.metadata.device_id + ) + if device is not None: + _LOGGER.debug("New device detected: %s", device.id) + async_dispatcher_send( + self._hass, _ufpd(self._entry, DISPATCH_ADOPT), device + ) + elif obj.camera is not None: + self._async_signal_device_update(obj.camera) + elif obj.light is not None: + self._async_signal_device_update(obj.light) + elif obj.sensor is not None: + self._async_signal_device_update(obj.sensor) # alert user viewport needs restart so voice clients can get new options - elif len(self.api.bootstrap.viewers) > 0 and isinstance( - message.new_obj, Liveview - ): + elif len(self.api.bootstrap.viewers) > 0 and isinstance(obj, Liveview): _LOGGER.warning( "Liveviews updated. Restart Home Assistant to update Viewport select options" ) diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index 65734569de2..b7419d0a41e 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -38,12 +38,16 @@ def _async_device_entities( klass: type[ProtectDeviceEntity], model_type: ModelType, descs: Sequence[ProtectRequiredKeysMixin], + ufp_device: ProtectAdoptableDeviceModel | None = None, ) -> list[ProtectDeviceEntity]: if len(descs) == 0: return [] entities: list[ProtectDeviceEntity] = [] - for device in data.get_by_types({model_type}): + devices = ( + [ufp_device] if ufp_device is not None else data.get_by_types({model_type}) + ) + for device in devices: if not device.is_adopted_by_us: continue @@ -89,6 +93,7 @@ def async_all_device_entities( lock_descs: Sequence[ProtectRequiredKeysMixin] | None = None, chime_descs: Sequence[ProtectRequiredKeysMixin] | None = None, all_descs: Sequence[ProtectRequiredKeysMixin] | None = None, + ufp_device: ProtectAdoptableDeviceModel | None = None, ) -> list[ProtectDeviceEntity]: """Generate a list of all the device entities.""" all_descs = list(all_descs or []) @@ -99,14 +104,33 @@ def async_all_device_entities( lock_descs = list(lock_descs or []) + all_descs chime_descs = list(chime_descs or []) + all_descs - return ( - _async_device_entities(data, klass, ModelType.CAMERA, camera_descs) - + _async_device_entities(data, klass, ModelType.LIGHT, light_descs) - + _async_device_entities(data, klass, ModelType.SENSOR, sense_descs) - + _async_device_entities(data, klass, ModelType.VIEWPORT, viewer_descs) - + _async_device_entities(data, klass, ModelType.DOORLOCK, lock_descs) - + _async_device_entities(data, klass, ModelType.CHIME, chime_descs) - ) + if ufp_device is None: + return ( + _async_device_entities(data, klass, ModelType.CAMERA, camera_descs) + + _async_device_entities(data, klass, ModelType.LIGHT, light_descs) + + _async_device_entities(data, klass, ModelType.SENSOR, sense_descs) + + _async_device_entities(data, klass, ModelType.VIEWPORT, viewer_descs) + + _async_device_entities(data, klass, ModelType.DOORLOCK, lock_descs) + + _async_device_entities(data, klass, ModelType.CHIME, chime_descs) + ) + + descs = [] + if ufp_device.model == ModelType.CAMERA: + descs = camera_descs + elif ufp_device.model == ModelType.LIGHT: + descs = light_descs + elif ufp_device.model == ModelType.SENSOR: + descs = sense_descs + elif ufp_device.model == ModelType.VIEWPORT: + descs = viewer_descs + elif ufp_device.model == ModelType.DOORLOCK: + descs = lock_descs + elif ufp_device.model == ModelType.CHIME: + descs = chime_descs + + if len(descs) == 0 or ufp_device.model is None: + return [] + return _async_device_entities(data, klass, ufp_device.model, descs, ufp_device) class ProtectDeviceEntity(Entity): diff --git a/homeassistant/components/unifiprotect/light.py b/homeassistant/components/unifiprotect/light.py index bd64905a289..fdfe41bca3c 100644 --- a/homeassistant/components/unifiprotect/light.py +++ b/homeassistant/components/unifiprotect/light.py @@ -4,16 +4,23 @@ from __future__ import annotations import logging from typing import Any -from pyunifiprotect.data import Light, ProtectModelWithId +from pyunifiprotect.data import ( + Light, + ModelType, + ProtectAdoptableDeviceModel, + ProtectModelWithId, +) from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DISPATCH_ADOPT, DOMAIN from .data import ProtectData from .entity import ProtectDeviceEntity +from .utils import async_dispatch_id as _ufpd _LOGGER = logging.getLogger(__name__) @@ -25,6 +32,18 @@ async def async_setup_entry( ) -> None: """Set up lights for UniFi Protect integration.""" data: ProtectData = hass.data[DOMAIN][entry.entry_id] + + async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: + if not device.is_adopted_by_us: + return + + if device.model == ModelType.LIGHT and device.can_write( + data.api.bootstrap.auth_user + ): + async_add_entities([ProtectLight(data, device)]) + + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + entities = [] for device in data.api.bootstrap.lights.values(): if not device.is_adopted_by_us: diff --git a/homeassistant/components/unifiprotect/lock.py b/homeassistant/components/unifiprotect/lock.py index 7258dc5f952..400d463050e 100644 --- a/homeassistant/components/unifiprotect/lock.py +++ b/homeassistant/components/unifiprotect/lock.py @@ -4,16 +4,23 @@ from __future__ import annotations import logging from typing import Any -from pyunifiprotect.data import Doorlock, LockStatusType, ProtectModelWithId +from pyunifiprotect.data import ( + Doorlock, + LockStatusType, + ProtectAdoptableDeviceModel, + ProtectModelWithId, +) from homeassistant.components.lock import LockEntity, LockEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DISPATCH_ADOPT, DOMAIN from .data import ProtectData from .entity import ProtectDeviceEntity +from .utils import async_dispatch_id as _ufpd _LOGGER = logging.getLogger(__name__) @@ -26,6 +33,15 @@ async def async_setup_entry( """Set up locks on a UniFi Protect NVR.""" data: ProtectData = hass.data[DOMAIN][entry.entry_id] + async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: + if not device.is_adopted_by_us: + return + + if isinstance(device, Doorlock): + async_add_entities([ProtectLock(data, device)]) + + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + entities = [] for device in data.api.bootstrap.doorlocks.values(): if not device.is_adopted_by_us: diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index b0391c9d860..41109c053f6 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from typing import Any -from pyunifiprotect.data import Camera, ProtectModelWithId +from pyunifiprotect.data import Camera, ProtectAdoptableDeviceModel, ProtectModelWithId from pyunifiprotect.exceptions import StreamError from homeassistant.components import media_source @@ -23,11 +23,13 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_IDLE, STATE_PLAYING from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DISPATCH_ADOPT, DOMAIN from .data import ProtectData from .entity import ProtectDeviceEntity +from .utils import async_dispatch_id as _ufpd _LOGGER = logging.getLogger(__name__) @@ -40,12 +42,21 @@ async def async_setup_entry( """Discover cameras with speakers on a UniFi Protect NVR.""" data: ProtectData = hass.data[DOMAIN][entry.entry_id] + async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: + if not device.is_adopted_by_us: + return + + if isinstance(device, Camera) and device.feature_flags.has_speaker: + async_add_entities([ProtectMediaPlayer(data, device)]) + + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + entities = [] for device in data.api.bootstrap.cameras.values(): - if not device.is_adopted_by_us or not device.feature_flags.has_speaker: + if not device.is_adopted_by_us: continue - - entities.append(ProtectMediaPlayer(data, device)) + if device.feature_flags.has_speaker: + entities.append(ProtectMediaPlayer(data, device)) async_add_entities(entities) diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index 7bd6ce5b3d8..a017d2330b6 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -4,19 +4,27 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta -from pyunifiprotect.data import Camera, Doorlock, Light, ProtectModelWithId +from pyunifiprotect.data import ( + Camera, + Doorlock, + Light, + ProtectAdoptableDeviceModel, + ProtectModelWithId, +) from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, TIME_SECONDS from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DISPATCH_ADOPT, DOMAIN from .data import ProtectData from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectSetableKeysMixin, T +from .utils import async_dispatch_id as _ufpd @dataclass @@ -184,6 +192,22 @@ async def async_setup_entry( ) -> None: """Set up number entities for UniFi Protect integration.""" data: ProtectData = hass.data[DOMAIN][entry.entry_id] + + async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: + entities = async_all_device_entities( + data, + ProtectNumbers, + camera_descs=CAMERA_NUMBERS, + light_descs=LIGHT_NUMBERS, + sense_descs=SENSE_NUMBERS, + lock_descs=DOORLOCK_NUMBERS, + chime_descs=CHIME_NUMBERS, + ufp_device=device, + ) + async_add_entities(entities) + + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + entities: list[ProtectDeviceEntity] = async_all_device_entities( data, ProtectNumbers, diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 17bdfa390a6..5ea956ca603 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -19,6 +19,7 @@ from pyunifiprotect.data import ( LightModeEnableType, LightModeType, MountType, + ProtectAdoptableDeviceModel, ProtectModelWithId, RecordingMode, Sensor, @@ -32,14 +33,15 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.util.dt import utcnow -from .const import ATTR_DURATION, ATTR_MESSAGE, DOMAIN, TYPE_EMPTY_VALUE +from .const import ATTR_DURATION, ATTR_MESSAGE, DISPATCH_ADOPT, DOMAIN, TYPE_EMPTY_VALUE from .data import ProtectData from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectSetableKeysMixin, T -from .utils import async_get_light_motion_current +from .utils import async_dispatch_id as _ufpd, async_get_light_motion_current _LOGGER = logging.getLogger(__name__) _KEY_LIGHT_MOTION = "light_motion" @@ -320,6 +322,22 @@ async def async_setup_entry( ) -> None: """Set up number entities for UniFi Protect integration.""" data: ProtectData = hass.data[DOMAIN][entry.entry_id] + + async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: + entities = async_all_device_entities( + data, + ProtectSelects, + camera_descs=CAMERA_SELECTS, + light_descs=LIGHT_SELECTS, + sense_descs=SENSE_SELECTS, + viewer_descs=VIEWER_SELECTS, + lock_descs=DOORLOCK_SELECTS, + ufp_device=device, + ) + async_add_entities(entities) + + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + entities: list[ProtectDeviceEntity] = async_all_device_entities( data, ProtectSelects, diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index 012d52ae215..a46e4c790b7 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -36,10 +36,11 @@ from homeassistant.const import ( TIME_SECONDS, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DISPATCH_ADOPT, DOMAIN from .data import ProtectData from .entity import ( EventThumbnailMixin, @@ -48,7 +49,7 @@ from .entity import ( async_all_device_entities, ) from .models import PermRequired, ProtectRequiredKeysMixin, T -from .utils import async_get_light_motion_current +from .utils import async_dispatch_id as _ufpd, async_get_light_motion_current _LOGGER = logging.getLogger(__name__) OBJECT_TYPE_NONE = "none" @@ -594,6 +595,26 @@ async def async_setup_entry( ) -> None: """Set up sensors for UniFi Protect integration.""" data: ProtectData = hass.data[DOMAIN][entry.entry_id] + + async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: + entities = async_all_device_entities( + data, + ProtectDeviceSensor, + all_descs=ALL_DEVICES_SENSORS, + camera_descs=CAMERA_SENSORS + CAMERA_DISABLED_SENSORS, + sense_descs=SENSE_SENSORS, + light_descs=LIGHT_SENSORS, + lock_descs=DOORLOCK_SENSORS, + chime_descs=CHIME_SENSORS, + viewer_descs=VIEWER_SENSORS, + ufp_device=device, + ) + if device.is_adopted_by_us and isinstance(device, Camera): + entities += _async_motion_entities(data, ufp_device=device) + async_add_entities(entities) + + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + entities: list[ProtectDeviceEntity] = async_all_device_entities( data, ProtectDeviceSensor, @@ -614,13 +635,17 @@ async def async_setup_entry( @callback def _async_motion_entities( data: ProtectData, + ufp_device: Camera | None = None, ) -> list[ProtectDeviceEntity]: entities: list[ProtectDeviceEntity] = [] - for device in data.api.bootstrap.cameras.values(): - for description in MOTION_TRIP_SENSORS: - if not device.is_adopted_by_us: - continue + devices = ( + data.api.bootstrap.cameras.values() if ufp_device is None else [ufp_device] + ) + for device in devices: + if not device.is_adopted_by_us: + continue + for description in MOTION_TRIP_SENSORS: entities.append(ProtectDeviceSensor(data, device, description)) _LOGGER.debug( "Adding trip sensor entity %s for %s", diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 8b3661ce324..71812459b95 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -15,13 +15,15 @@ from pyunifiprotect.data import ( from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DISPATCH_ADOPT, DOMAIN from .data import ProtectData from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectSetableKeysMixin, T +from .utils import async_dispatch_id as _ufpd _LOGGER = logging.getLogger(__name__) @@ -302,6 +304,22 @@ async def async_setup_entry( ) -> None: """Set up sensors for UniFi Protect integration.""" data: ProtectData = hass.data[DOMAIN][entry.entry_id] + + async def _add_new_device(device: ProtectAdoptableDeviceModel) -> None: + entities = async_all_device_entities( + data, + ProtectSwitch, + camera_descs=CAMERA_SWITCHES, + light_descs=LIGHT_SWITCHES, + sense_descs=SENSE_SWITCHES, + lock_descs=DOORLOCK_SWITCHES, + viewer_descs=VIEWER_SWITCHES, + ufp_device=device, + ) + async_add_entities(entities) + + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + entities: list[ProtectDeviceEntity] = async_all_device_entities( data, ProtectSwitch, diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index 72baab334f3..808117aac9e 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -15,9 +15,10 @@ from pyunifiprotect.data import ( ProtectAdoptableDeviceModel, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from .const import ModelType +from .const import DOMAIN, ModelType def get_nested_attr(obj: Any, attr: str) -> Any: @@ -98,3 +99,10 @@ def async_get_light_motion_current(obj: Light) -> str: ): return f"{LightModeType.MOTION.value}Dark" return obj.light_mode_settings.mode.value + + +@callback +def async_dispatch_id(entry: ConfigEntry, dispatch: str) -> str: + """Generate entry specific dispatch ID.""" + + return f"{DOMAIN}.{entry.entry_id}.{dispatch}" diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 856c034905f..640bf81ec49 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -32,15 +32,59 @@ from homeassistant.helpers import entity_registry as er from .utils import ( MockUFPFixture, + adopt_devices, assert_entity_counts, ids_from_device_description, init_entry, + remove_entities, ) LIGHT_SENSOR_WRITE = LIGHT_SENSORS[:2] SENSE_SENSORS_WRITE = SENSE_SENSORS[:4] +async def test_binary_sensor_camera_remove( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, unadopted_camera: Camera +): + """Test removing and re-adding a camera device.""" + + ufp.api.bootstrap.nvr.system_info.ustorage = None + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 3, 3) + await remove_entities(hass, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 0, 0) + await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 3, 3) + + +async def test_binary_sensor_light_remove( + hass: HomeAssistant, ufp: MockUFPFixture, light: Light +): + """Test removing and re-adding a light device.""" + + ufp.api.bootstrap.nvr.system_info.ustorage = None + await init_entry(hass, ufp, [light]) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 2, 2) + await remove_entities(hass, [light]) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 0, 0) + await adopt_devices(hass, ufp, [light]) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 2, 2) + + +async def test_binary_sensor_sensor_remove( + hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor +): + """Test removing and re-adding a light device.""" + + ufp.api.bootstrap.nvr.system_info.ustorage = None + await init_entry(hass, ufp, [sensor_all]) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 4, 4) + await remove_entities(hass, [sensor_all]) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 0, 0) + await adopt_devices(hass, ufp, [sensor_all]) + assert_entity_counts(hass, Platform.BINARY_SENSOR, 4, 4) + + async def test_binary_sensor_setup_light( hass: HomeAssistant, ufp: MockUFPFixture, light: Light ): diff --git a/tests/components/unifiprotect/test_button.py b/tests/components/unifiprotect/test_button.py index a846214a7aa..a46d74e0b8e 100644 --- a/tests/components/unifiprotect/test_button.py +++ b/tests/components/unifiprotect/test_button.py @@ -11,7 +11,27 @@ from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .utils import MockUFPFixture, assert_entity_counts, enable_entity, init_entry +from .utils import ( + MockUFPFixture, + adopt_devices, + assert_entity_counts, + enable_entity, + init_entry, + remove_entities, +) + + +async def test_button_chime_remove( + hass: HomeAssistant, ufp: MockUFPFixture, chime: Chime +): + """Test removing and re-adding a light device.""" + + await init_entry(hass, ufp, [chime]) + assert_entity_counts(hass, Platform.BUTTON, 3, 2) + await remove_entities(hass, [chime]) + assert_entity_counts(hass, Platform.BUTTON, 0, 0) + await adopt_devices(hass, ufp, [chime]) + assert_entity_counts(hass, Platform.BUTTON, 3, 2) async def test_reboot_button( diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index 6fad7cb899e..2b103e8d714 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -33,9 +33,11 @@ from homeassistant.setup import async_setup_component from .utils import ( MockUFPFixture, + adopt_devices, assert_entity_counts, enable_entity, init_entry, + remove_entities, time_changed, ) @@ -268,18 +270,37 @@ async def test_basic_setup( await validate_no_stream_camera_state(hass, doorbell, 3, entity_id, features=0) -async def test_missing_channels( - hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera -): +async def test_adopt(hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera): """Test setting up camera with no camera channels.""" camera1 = camera.copy() camera1.channels = [] await init_entry(hass, ufp, [camera1]) - assert_entity_counts(hass, Platform.CAMERA, 0, 0) + await remove_entities(hass, [camera1]) + assert_entity_counts(hass, Platform.CAMERA, 0, 0) + camera1.channels = [] + await adopt_devices(hass, ufp, [camera1]) + assert_entity_counts(hass, Platform.CAMERA, 0, 0) + + camera1.channels = camera.channels + for channel in camera1.channels: + channel._api = ufp.api + + mock_msg = Mock() + mock_msg.changed_data = {"channels": camera.channels} + mock_msg.new_obj = camera1 + ufp.ws_msg(mock_msg) + await hass.async_block_till_done() + assert_entity_counts(hass, Platform.CAMERA, 2, 1) + + await remove_entities(hass, [camera1]) + assert_entity_counts(hass, Platform.CAMERA, 0, 0) + await adopt_devices(hass, ufp, [camera1]) + assert_entity_counts(hass, Platform.CAMERA, 2, 1) + async def test_camera_image( hass: HomeAssistant, ufp: MockUFPFixture, camera: ProtectCamera diff --git a/tests/components/unifiprotect/test_light.py b/tests/components/unifiprotect/test_light.py index 3c575de8d00..40f2191828e 100644 --- a/tests/components/unifiprotect/test_light.py +++ b/tests/components/unifiprotect/test_light.py @@ -19,7 +19,24 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .utils import MockUFPFixture, assert_entity_counts, init_entry +from .utils import ( + MockUFPFixture, + adopt_devices, + assert_entity_counts, + init_entry, + remove_entities, +) + + +async def test_light_remove(hass: HomeAssistant, ufp: MockUFPFixture, light: Light): + """Test removing and re-adding a light device.""" + + await init_entry(hass, ufp, [light]) + assert_entity_counts(hass, Platform.LIGHT, 1, 1) + await remove_entities(hass, [light]) + assert_entity_counts(hass, Platform.LIGHT, 0, 0) + await adopt_devices(hass, ufp, [light]) + assert_entity_counts(hass, Platform.LIGHT, 1, 1) async def test_light_setup( diff --git a/tests/components/unifiprotect/test_lock.py b/tests/components/unifiprotect/test_lock.py index 21b3c77deb5..d6534e93845 100644 --- a/tests/components/unifiprotect/test_lock.py +++ b/tests/components/unifiprotect/test_lock.py @@ -21,7 +21,26 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from .utils import MockUFPFixture, assert_entity_counts, init_entry +from .utils import ( + MockUFPFixture, + adopt_devices, + assert_entity_counts, + init_entry, + remove_entities, +) + + +async def test_lock_remove( + hass: HomeAssistant, ufp: MockUFPFixture, doorlock: Doorlock +): + """Test removing and re-adding a lock device.""" + + await init_entry(hass, ufp, [doorlock]) + assert_entity_counts(hass, Platform.LOCK, 1, 1) + await remove_entities(hass, [doorlock]) + assert_entity_counts(hass, Platform.LOCK, 0, 0) + await adopt_devices(hass, ufp, [doorlock]) + assert_entity_counts(hass, Platform.LOCK, 1, 1) async def test_lock_setup( diff --git a/tests/components/unifiprotect/test_media_player.py b/tests/components/unifiprotect/test_media_player.py index 678fa0c9be4..ade84e2d51c 100644 --- a/tests/components/unifiprotect/test_media_player.py +++ b/tests/components/unifiprotect/test_media_player.py @@ -25,7 +25,26 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from .utils import MockUFPFixture, assert_entity_counts, init_entry +from .utils import ( + MockUFPFixture, + adopt_devices, + assert_entity_counts, + init_entry, + remove_entities, +) + + +async def test_media_player_camera_remove( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera +): + """Test removing and re-adding a light device.""" + + await init_entry(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1) + await remove_entities(hass, [doorbell]) + assert_entity_counts(hass, Platform.MEDIA_PLAYER, 0, 0) + await adopt_devices(hass, ufp, [doorbell]) + assert_entity_counts(hass, Platform.MEDIA_PLAYER, 1, 1) async def test_media_player_setup( diff --git a/tests/components/unifiprotect/test_number.py b/tests/components/unifiprotect/test_number.py index 656f7d08ba5..51e9dfc85a2 100644 --- a/tests/components/unifiprotect/test_number.py +++ b/tests/components/unifiprotect/test_number.py @@ -21,12 +21,53 @@ from homeassistant.helpers import entity_registry as er from .utils import ( MockUFPFixture, + adopt_devices, assert_entity_counts, ids_from_device_description, init_entry, + remove_entities, ) +async def test_number_sensor_camera_remove( + hass: HomeAssistant, ufp: MockUFPFixture, camera: Camera, unadopted_camera: Camera +): + """Test removing and re-adding a camera device.""" + + await init_entry(hass, ufp, [camera, unadopted_camera]) + assert_entity_counts(hass, Platform.NUMBER, 3, 3) + await remove_entities(hass, [camera, unadopted_camera]) + assert_entity_counts(hass, Platform.NUMBER, 0, 0) + await adopt_devices(hass, ufp, [camera, unadopted_camera]) + assert_entity_counts(hass, Platform.NUMBER, 3, 3) + + +async def test_number_sensor_light_remove( + hass: HomeAssistant, ufp: MockUFPFixture, light: Light +): + """Test removing and re-adding a light device.""" + + await init_entry(hass, ufp, [light]) + assert_entity_counts(hass, Platform.NUMBER, 2, 2) + await remove_entities(hass, [light]) + assert_entity_counts(hass, Platform.NUMBER, 0, 0) + await adopt_devices(hass, ufp, [light]) + assert_entity_counts(hass, Platform.NUMBER, 2, 2) + + +async def test_number_lock_remove( + hass: HomeAssistant, ufp: MockUFPFixture, doorlock: Doorlock +): + """Test removing and re-adding a light device.""" + + await init_entry(hass, ufp, [doorlock]) + assert_entity_counts(hass, Platform.NUMBER, 1, 1) + await remove_entities(hass, [doorlock]) + assert_entity_counts(hass, Platform.NUMBER, 0, 0) + await adopt_devices(hass, ufp, [doorlock]) + assert_entity_counts(hass, Platform.NUMBER, 1, 1) + + async def test_number_setup_light( hass: HomeAssistant, ufp: MockUFPFixture, light: Light ): diff --git a/tests/components/unifiprotect/test_select.py b/tests/components/unifiprotect/test_select.py index 637a0d4ad5d..46bc70f61f6 100644 --- a/tests/components/unifiprotect/test_select.py +++ b/tests/components/unifiprotect/test_select.py @@ -41,12 +41,56 @@ from homeassistant.helpers import entity_registry as er from .utils import ( MockUFPFixture, + adopt_devices, assert_entity_counts, ids_from_device_description, init_entry, + remove_entities, ) +async def test_select_camera_remove( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, unadopted_camera: Camera +): + """Test removing and re-adding a camera device.""" + + ufp.api.bootstrap.nvr.system_info.ustorage = None + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.SELECT, 4, 4) + await remove_entities(hass, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.SELECT, 0, 0) + await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.SELECT, 4, 4) + + +async def test_select_light_remove( + hass: HomeAssistant, ufp: MockUFPFixture, light: Light +): + """Test removing and re-adding a light device.""" + + ufp.api.bootstrap.nvr.system_info.ustorage = None + await init_entry(hass, ufp, [light]) + assert_entity_counts(hass, Platform.SELECT, 2, 2) + await remove_entities(hass, [light]) + assert_entity_counts(hass, Platform.SELECT, 0, 0) + await adopt_devices(hass, ufp, [light]) + assert_entity_counts(hass, Platform.SELECT, 2, 2) + + +async def test_select_viewer_remove( + hass: HomeAssistant, ufp: MockUFPFixture, viewer: Viewer +): + """Test removing and re-adding a light device.""" + + ufp.api.bootstrap.nvr.system_info.ustorage = None + await init_entry(hass, ufp, [viewer]) + assert_entity_counts(hass, Platform.SELECT, 1, 1) + await remove_entities(hass, [viewer]) + assert_entity_counts(hass, Platform.SELECT, 0, 0) + await adopt_devices(hass, ufp, [viewer]) + assert_entity_counts(hass, Platform.SELECT, 1, 1) + + async def test_select_setup_light( hass: HomeAssistant, ufp: MockUFPFixture, light: Light ): diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index e204b09b1b0..fcad6ce2725 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -41,10 +41,12 @@ from homeassistant.helpers import entity_registry as er from .utils import ( MockUFPFixture, + adopt_devices, assert_entity_counts, enable_entity, ids_from_device_description, init_entry, + remove_entities, reset_objects, time_changed, ) @@ -53,6 +55,34 @@ CAMERA_SENSORS_WRITE = CAMERA_SENSORS[:5] SENSE_SENSORS_WRITE = SENSE_SENSORS[:8] +async def test_sensor_camera_remove( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, unadopted_camera: Camera +): + """Test removing and re-adding a camera device.""" + + ufp.api.bootstrap.nvr.system_info.ustorage = None + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.SENSOR, 25, 13) + await remove_entities(hass, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.SENSOR, 12, 9) + await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.SENSOR, 25, 13) + + +async def test_sensor_sensor_remove( + hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor +): + """Test removing and re-adding a light device.""" + + ufp.api.bootstrap.nvr.system_info.ustorage = None + await init_entry(hass, ufp, [sensor_all]) + assert_entity_counts(hass, Platform.SENSOR, 22, 14) + await remove_entities(hass, [sensor_all]) + assert_entity_counts(hass, Platform.SENSOR, 12, 9) + await adopt_devices(hass, ufp, [sensor_all]) + assert_entity_counts(hass, Platform.SENSOR, 22, 14) + + async def test_sensor_setup_sensor( hass: HomeAssistant, ufp: MockUFPFixture, sensor_all: Sensor ): diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 0c45ec28b7b..684e3b8e441 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -19,10 +19,12 @@ from homeassistant.helpers import entity_registry as er from .utils import ( MockUFPFixture, + adopt_devices, assert_entity_counts, enable_entity, ids_from_device_description, init_entry, + remove_entities, ) CAMERA_SWITCHES_BASIC = [ @@ -37,6 +39,34 @@ CAMERA_SWITCHES_NO_EXTRA = [ ] +async def test_switch_camera_remove( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, unadopted_camera: Camera +): + """Test removing and re-adding a camera device.""" + + ufp.api.bootstrap.nvr.system_info.ustorage = None + await init_entry(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.SWITCH, 13, 12) + await remove_entities(hass, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.SWITCH, 0, 0) + await adopt_devices(hass, ufp, [doorbell, unadopted_camera]) + assert_entity_counts(hass, Platform.SWITCH, 13, 12) + + +async def test_switch_light_remove( + hass: HomeAssistant, ufp: MockUFPFixture, light: Light +): + """Test removing and re-adding a light device.""" + + ufp.api.bootstrap.nvr.system_info.ustorage = None + await init_entry(hass, ufp, [light]) + assert_entity_counts(hass, Platform.SWITCH, 2, 1) + await remove_entities(hass, [light]) + assert_entity_counts(hass, Platform.SWITCH, 0, 0) + await adopt_devices(hass, ufp, [light]) + assert_entity_counts(hass, Platform.SWITCH, 2, 1) + + async def test_switch_setup_no_perm( hass: HomeAssistant, ufp: MockUFPFixture, diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index 517da9e73c6..260c6996128 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -5,11 +5,15 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta from typing import Any, Callable, Sequence +from unittest.mock import Mock from pyunifiprotect import ProtectApiClient from pyunifiprotect.data import ( Bootstrap, Camera, + Event, + EventType, + ModelType, ProtectAdoptableDeviceModel, WSSubscriptionMessage, ) @@ -18,7 +22,7 @@ from pyunifiprotect.test_util.anonymize import random_hex from homeassistant.const import Platform from homeassistant.core import HomeAssistant, split_entity_id -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import EntityDescription import homeassistant.util.dt as dt_util @@ -166,3 +170,55 @@ async def init_entry( await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() + + +async def remove_entities( + hass: HomeAssistant, + ufp_devices: list[ProtectAdoptableDeviceModel], +) -> None: + """Remove all entities for given Protect devices.""" + + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + for ufp_device in ufp_devices: + if not ufp_device.is_adopted_by_us: + continue + + name = ufp_device.display_name.replace(" ", "_").lower() + entity = entity_registry.async_get(f"{Platform.SENSOR}.{name}_uptime") + assert entity is not None + + device_id = entity.device_id + for reg in list(entity_registry.entities.values()): + if reg.device_id == device_id: + entity_registry.async_remove(reg.entity_id) + device_registry.async_remove_device(device_id) + + await hass.async_block_till_done() + + +async def adopt_devices( + hass: HomeAssistant, + ufp: MockUFPFixture, + ufp_devices: list[ProtectAdoptableDeviceModel], +): + """Emit WS to re-adopt give Protect devices.""" + + for ufp_device in ufp_devices: + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = Event( + api=ufp_device.api, + id=random_hex(24), + smart_detect_types=[], + smart_detect_event_ids=[], + type=EventType.DEVICE_ADOPTED, + start=dt_util.utcnow(), + score=100, + metadata={"device_id": ufp_device.id}, + model=ModelType.EVENT, + ) + ufp.ws_msg(mock_msg) + + await hass.async_block_till_done() From 08c5c6ca1c2cf4ece207697ba1e8ca10b843cddd Mon Sep 17 00:00:00 2001 From: shbatm Date: Mon, 27 Jun 2022 16:24:25 -0500 Subject: [PATCH 1812/3516] ISY994: Bump pyisy to 3.0.7 (#74071) --- homeassistant/components/isy994/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index d131a150fb3..f3620ec9663 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -2,7 +2,7 @@ "domain": "isy994", "name": "Universal Devices ISY994", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.0.6"], + "requirements": ["pyisy==3.0.7"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 30af6682715..105f8f08f20 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1580,7 +1580,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.6 +pyisy==3.0.7 # homeassistant.components.itach pyitachip2ir==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9f7d815469..43fb970d99a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1065,7 +1065,7 @@ pyiqvia==2022.04.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.6 +pyisy==3.0.7 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 From 21b842cf9cf49fdb9449fb24b46a55974a108186 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 28 Jun 2022 00:48:18 +0200 Subject: [PATCH 1813/3516] Partially revert "Switch loader to use json helper (#73872)" (#74077) --- homeassistant/loader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index ab681d7c42d..589f316532b 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -11,6 +11,7 @@ from collections.abc import Callable from contextlib import suppress import functools as ft import importlib +import json import logging import pathlib import sys @@ -29,7 +30,6 @@ from .generated.mqtt import MQTT from .generated.ssdp import SSDP from .generated.usb import USB from .generated.zeroconf import HOMEKIT, ZEROCONF -from .helpers.json import JSON_DECODE_EXCEPTIONS, json_loads from .util.async_ import gather_with_concurrency # Typing imports that create a circular dependency @@ -366,8 +366,8 @@ class Integration: continue try: - manifest = json_loads(manifest_path.read_text()) - except JSON_DECODE_EXCEPTIONS as err: + manifest = json.loads(manifest_path.read_text()) + except ValueError as err: _LOGGER.error( "Error parsing manifest.json file at %s: %s", manifest_path, err ) From e8917af823c5fcc919169fc9f1b575b17335f3b0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 00:48:56 +0200 Subject: [PATCH 1814/3516] Cleanup update/async_update typing in Entities (#74035) --- homeassistant/components/garadget/cover.py | 2 +- homeassistant/components/lutron/cover.py | 2 +- homeassistant/components/soma/cover.py | 4 ++-- homeassistant/components/zha/cover.py | 2 +- homeassistant/components/zha/fan.py | 2 +- homeassistant/components/zha/lock.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/garadget/cover.py b/homeassistant/components/garadget/cover.py index 96ebe698605..826f21e9f88 100644 --- a/homeassistant/components/garadget/cover.py +++ b/homeassistant/components/garadget/cover.py @@ -233,7 +233,7 @@ class GaradgetCover(CoverEntity): self._start_watcher("stop") return ret["return_value"] == 1 - def update(self): + def update(self) -> None: """Get updated status from API.""" try: status = self._get_variable("doorStatus") diff --git a/homeassistant/components/lutron/cover.py b/homeassistant/components/lutron/cover.py index fa62ef3745a..65a1c737d55 100644 --- a/homeassistant/components/lutron/cover.py +++ b/homeassistant/components/lutron/cover.py @@ -66,7 +66,7 @@ class LutronCover(LutronDevice, CoverEntity): position = kwargs[ATTR_POSITION] self._lutron_device.level = position - def update(self): + def update(self) -> None: """Call when forcing a refresh of the device.""" # Reading the property (rather than last_level()) fetches value level = self._lutron_device.level diff --git a/homeassistant/components/soma/cover.py b/homeassistant/components/soma/cover.py index 0130b0ca7b1..116f88aa20e 100644 --- a/homeassistant/components/soma/cover.py +++ b/homeassistant/components/soma/cover.py @@ -102,7 +102,7 @@ class SomaTilt(SomaEntity, CoverEntity): ) self.set_position(kwargs[ATTR_TILT_POSITION]) - async def async_update(self): + async def async_update(self) -> None: """Update the entity with the latest data.""" response = await self.get_shade_state_from_api() @@ -172,7 +172,7 @@ class SomaShade(SomaEntity, CoverEntity): f'Error while setting the cover position ({self.name}): {response["msg"]}' ) - async def async_update(self): + async def async_update(self) -> None: """Update the cover with the latest data.""" response = await self.get_shade_state_from_api() diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 6ade62343b1..f6c67e6981d 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -162,7 +162,7 @@ class ZhaCover(ZhaEntity, CoverEntity): self._state = STATE_OPEN if self._current_position > 0 else STATE_CLOSED self.async_write_ha_state() - async def async_update(self): + async def async_update(self) -> None: """Attempt to retrieve the open/close state of the cover.""" await super().async_update() await self.async_get_state() diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 3b9793c5137..8e24427b679 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -202,7 +202,7 @@ class FanGroup(BaseFan, ZhaGroupEntity): self.error("Could not set fan mode: %s", ex) self.async_set_state(0, "fan_mode", fan_mode) - async def async_update(self): + async def async_update(self) -> None: """Attempt to retrieve on off state from the fan.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] states: list[State] = list(filter(None, all_states)) diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index 6615141f4d1..a2ec5e068cb 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -137,7 +137,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity): return self.async_write_ha_state() - async def async_update(self): + async def async_update(self) -> None: """Attempt to retrieve state from the lock.""" await super().async_update() await self.async_get_state() From 09dca3cd9478d0fbc12ee143b8e858e31e69e559 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 28 Jun 2022 01:46:58 +0200 Subject: [PATCH 1815/3516] Remove invalid unit of measurement from Glances (#73983) --- homeassistant/components/glances/const.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index 3fd26165283..d28c7395a43 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -142,7 +142,6 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( key="process_sleeping", type="processcount", name_suffix="Sleeping", - native_unit_of_measurement="", icon=CPU_ICON, state_class=SensorStateClass.MEASUREMENT, ), From c62bfcaa4cc70571acca65875ffc8da8a54649f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Tue, 28 Jun 2022 01:47:55 +0200 Subject: [PATCH 1816/3516] Nuki opener event on ring (#72793) * feat(nuki): add ring action timestamp attribute * feat(nuki): add ring action state attribute * Emit event on Nuki Opener ring * Removed event attributes * Use entity registry to get entity id * Move event firing to the async update method * Move events code outside try-except * Black autoformat * Added missing period to doc * Import order Co-authored-by: Franck Nijhof --- homeassistant/components/nuki/__init__.py | 41 +++++++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index e9cef7aa6cd..a59b0a62f70 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -1,4 +1,5 @@ """The nuki component.""" +from collections import defaultdict from datetime import timedelta import logging @@ -12,6 +13,7 @@ from homeassistant import exceptions from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -39,17 +41,36 @@ def _get_bridge_devices(bridge: NukiBridge) -> tuple[list[NukiLock], list[NukiOp return bridge.locks, bridge.openers -def _update_devices(devices: list[NukiDevice]) -> None: +def _update_devices(devices: list[NukiDevice]) -> dict[str, set[str]]: + """ + Update the Nuki devices. + + Returns: + A dict with the events to be fired. The event type is the key and the device ids are the value + """ + + events: dict[str, set[str]] = defaultdict(set) + for device in devices: for level in (False, True): try: - device.update(level) + if isinstance(device, NukiOpener): + last_ring_action_state = device.ring_action_state + + device.update(level) + + if not last_ring_action_state and device.ring_action_state: + events["ring"].add(device.nuki_id) + else: + device.update(level) except RequestException: continue if device.state not in ERROR_STATES: break + return events + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Nuki entry.""" @@ -86,12 +107,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Note: asyncio.TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. async with async_timeout.timeout(10): - await hass.async_add_executor_job(_update_devices, locks + openers) + events = await hass.async_add_executor_job( + _update_devices, locks + openers + ) except InvalidCredentialsException as err: raise UpdateFailed(f"Invalid credentials for Bridge: {err}") from err except RequestException as err: raise UpdateFailed(f"Error communicating with Bridge: {err}") from err + ent_reg = er.async_get(hass) + for event, device_ids in events.items(): + for device_id in device_ids: + entity_id = ent_reg.async_get_entity_id( + Platform.LOCK, DOMAIN, device_id + ) + event_data = { + "entity_id": entity_id, + "type": event, + } + hass.bus.async_fire("nuki_event", event_data) + coordinator = DataUpdateCoordinator( hass, _LOGGER, From b2c84a4c4ae70eeb7ca39a92b8e77d28683b4f44 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 01:49:10 +0200 Subject: [PATCH 1817/3516] Adjust reauth in awair config flow (#72386) * Adjust config-flow type hints in awair * Improve typing of dict arguments * Use mapping for async_step_reauth * Add async_step_reauth_confirm * Don't try old token * Adjust translations * Adjust tests * Update tests/components/awair/test_config_flow.py Co-authored-by: Martin Hjelmare * Update tests/components/awair/test_config_flow.py Co-authored-by: Martin Hjelmare * Update tests/components/awair/test_config_flow.py Co-authored-by: Martin Hjelmare * Update tests/components/awair/test_config_flow.py Co-authored-by: Martin Hjelmare * Update tests/components/awair/test_config_flow.py Co-authored-by: Martin Hjelmare * Update tests/components/awair/test_config_flow.py Co-authored-by: Martin Hjelmare * Update tests/components/awair/test_config_flow.py Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/awair/config_flow.py | 18 +++- homeassistant/components/awair/strings.json | 2 +- .../components/awair/translations/en.json | 2 +- tests/components/awair/test_config_flow.py | 82 ++++++++++++------- 4 files changed, 69 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index 1eff98dd78d..fc7fd1e79a4 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -1,12 +1,16 @@ """Config flow for Awair.""" from __future__ import annotations +from collections.abc import Mapping +from typing import Any + from python_awair import Awair from python_awair.exceptions import AuthError, AwairError import voluptuous as vol from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN, LOGGER @@ -17,7 +21,9 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input: dict | None = None): + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -42,8 +48,14 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, user_input: dict | None = None): + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Handle re-auth if token invalid.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm reauth dialog.""" errors = {} if user_input is not None: @@ -62,7 +74,7 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): errors = {CONF_ACCESS_TOKEN: error} return self.async_show_form( - step_id="reauth", + step_id="reauth_confirm", data_schema=vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}), errors=errors, ) diff --git a/homeassistant/components/awair/strings.json b/homeassistant/components/awair/strings.json index f9b1f40e047..5ed7c0e715e 100644 --- a/homeassistant/components/awair/strings.json +++ b/homeassistant/components/awair/strings.json @@ -8,7 +8,7 @@ "email": "[%key:common::config_flow::data::email%]" } }, - "reauth": { + "reauth_confirm": { "description": "Please re-enter your Awair developer access token.", "data": { "access_token": "[%key:common::config_flow::data::access_token%]", diff --git a/homeassistant/components/awair/translations/en.json b/homeassistant/components/awair/translations/en.json index 0e5a1e62bb5..52ffd13ec79 100644 --- a/homeassistant/components/awair/translations/en.json +++ b/homeassistant/components/awair/translations/en.json @@ -10,7 +10,7 @@ "unknown": "Unexpected error" }, "step": { - "reauth": { + "reauth_confirm": { "data": { "access_token": "Access Token", "email": "Email" diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index 8afe9a1c701..47e58fea421 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -8,6 +8,7 @@ from homeassistant import data_entry_flow from homeassistant.components.awair.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant from .const import CONFIG, DEVICES_FIXTURE, NO_DEVICES_FIXTURE, UNIQUE_ID, USER_FIXTURE @@ -82,46 +83,67 @@ async def test_no_devices_error(hass): assert result["reason"] == "no_devices_found" -async def test_reauth(hass): +async def test_reauth(hass: HomeAssistant) -> None: """Test reauth flow.""" - with patch( - "python_awair.AwairClient.query", side_effect=[USER_FIXTURE, DEVICES_FIXTURE] - ), patch( - "homeassistant.components.awair.sensor.async_setup_entry", - return_value=True, - ): - mock_config = MockConfigEntry(domain=DOMAIN, unique_id=UNIQUE_ID, data=CONFIG) - mock_config.add_to_hass(hass) - hass.config_entries.async_update_entry( - mock_config, data={**CONFIG, CONF_ACCESS_TOKEN: "blah"} - ) + mock_config = MockConfigEntry( + domain=DOMAIN, unique_id=UNIQUE_ID, data={**CONFIG, CONF_ACCESS_TOKEN: "blah"} + ) + mock_config.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH, "unique_id": UNIQUE_ID}, - data=CONFIG, - ) - - assert result["type"] == "abort" - assert result["reason"] == "reauth_successful" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "unique_id": UNIQUE_ID}, + data={**CONFIG, CONF_ACCESS_TOKEN: "blah"}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {} with patch("python_awair.AwairClient.query", side_effect=AuthError()): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH, "unique_id": UNIQUE_ID}, - data=CONFIG, + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONFIG, ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" assert result["errors"] == {CONF_ACCESS_TOKEN: "invalid_access_token"} - with patch("python_awair.AwairClient.query", side_effect=AwairError()): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH, "unique_id": UNIQUE_ID}, - data=CONFIG, + with patch( + "python_awair.AwairClient.query", side_effect=[USER_FIXTURE, DEVICES_FIXTURE] + ), patch("homeassistant.components.awair.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONFIG, ) - assert result["type"] == "abort" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + + +async def test_reauth_error(hass: HomeAssistant) -> None: + """Test reauth flow.""" + mock_config = MockConfigEntry( + domain=DOMAIN, unique_id=UNIQUE_ID, data={**CONFIG, CONF_ACCESS_TOKEN: "blah"} + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "unique_id": UNIQUE_ID}, + data={**CONFIG, CONF_ACCESS_TOKEN: "blah"}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {} + + with patch("python_awair.AwairClient.query", side_effect=AwairError()): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONFIG, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "unknown" From 2f0fe0df821ae0805a8b18b5770c455647dbe15a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 28 Jun 2022 01:50:06 +0200 Subject: [PATCH 1818/3516] Fix wind speed SMHI (#72999) --- homeassistant/components/smhi/weather.py | 67 ++++++++++++++++-------- tests/components/smhi/test_weather.py | 47 +++++++++++++++-- 2 files changed, 88 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index b63d32dc538..cbe2ad37fe9 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping from datetime import datetime, timedelta import logging -from typing import Final +from typing import Any, Final import aiohttp import async_timeout @@ -27,10 +28,14 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRESSURE, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ROUNDING_PRECISION, Forecast, WeatherEntity, ) @@ -41,6 +46,8 @@ from homeassistant.const import ( CONF_NAME, LENGTH_KILOMETERS, LENGTH_MILLIMETERS, + PRESSURE_HPA, + SPEED_METERS_PER_SECOND, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -49,7 +56,7 @@ from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later -from homeassistant.util import Throttle, slugify +from homeassistant.util import Throttle, slugify, speed as speed_util from .const import ( ATTR_SMHI_CLOUDINESS, @@ -112,9 +119,11 @@ class SmhiWeather(WeatherEntity): """Representation of a weather entity.""" _attr_attribution = "Swedish weather institute (SMHI)" - _attr_temperature_unit = TEMP_CELSIUS - _attr_visibility_unit = LENGTH_KILOMETERS - _attr_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_visibility_unit = LENGTH_KILOMETERS + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + _attr_native_pressure_unit = PRESSURE_HPA def __init__( self, @@ -139,7 +148,23 @@ class SmhiWeather(WeatherEntity): configuration_url="http://opendata.smhi.se/apidocs/metfcst/parameters.html", ) self._attr_condition = None - self._attr_temperature = None + self._attr_native_temperature = None + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return additional attributes.""" + if self._forecasts: + wind_gust = speed_util.convert( + self._forecasts[0].wind_gust, + SPEED_METERS_PER_SECOND, + self._wind_speed_unit, + ) + return { + ATTR_SMHI_CLOUDINESS: self._forecasts[0].cloudiness, + ATTR_SMHI_WIND_GUST_SPEED: round(wind_gust, ROUNDING_PRECISION), + ATTR_SMHI_THUNDER_PROBABILITY: self._forecasts[0].thunder, + } + return None @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self) -> None: @@ -156,13 +181,12 @@ class SmhiWeather(WeatherEntity): return if self._forecasts: - self._attr_temperature = self._forecasts[0].temperature + self._attr_native_temperature = self._forecasts[0].temperature self._attr_humidity = self._forecasts[0].humidity - # Convert from m/s to km/h - self._attr_wind_speed = round(self._forecasts[0].wind_speed * 18 / 5) + self._attr_native_wind_speed = self._forecasts[0].wind_speed self._attr_wind_bearing = self._forecasts[0].wind_direction - self._attr_visibility = self._forecasts[0].horizontal_visibility - self._attr_pressure = self._forecasts[0].pressure + self._attr_native_visibility = self._forecasts[0].horizontal_visibility + self._attr_native_pressure = self._forecasts[0].pressure self._attr_condition = next( ( k @@ -171,12 +195,6 @@ class SmhiWeather(WeatherEntity): ), None, ) - self._attr_extra_state_attributes = { - ATTR_SMHI_CLOUDINESS: self._forecasts[0].cloudiness, - # Convert from m/s to km/h - ATTR_SMHI_WIND_GUST_SPEED: round(self._forecasts[0].wind_gust * 18 / 5), - ATTR_SMHI_THUNDER_PROBABILITY: self._forecasts[0].thunder, - } async def retry_update(self, _: datetime) -> None: """Retry refresh weather forecast.""" @@ -200,10 +218,13 @@ class SmhiWeather(WeatherEntity): data.append( { ATTR_FORECAST_TIME: forecast.valid_time.isoformat(), - ATTR_FORECAST_TEMP: forecast.temperature_max, - ATTR_FORECAST_TEMP_LOW: forecast.temperature_min, - ATTR_FORECAST_PRECIPITATION: round(forecast.total_precipitation, 1), + ATTR_FORECAST_NATIVE_TEMP: forecast.temperature_max, + ATTR_FORECAST_NATIVE_TEMP_LOW: forecast.temperature_min, + ATTR_FORECAST_NATIVE_PRECIPITATION: forecast.total_precipitation, ATTR_FORECAST_CONDITION: condition, + ATTR_FORECAST_NATIVE_PRESSURE: forecast.pressure, + ATTR_FORECAST_WIND_BEARING: forecast.wind_direction, + ATTR_FORECAST_NATIVE_WIND_SPEED: forecast.wind_speed, } ) diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index c890ad62216..0097a7a5c5a 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -16,18 +16,24 @@ from homeassistant.components.weather import ( ATTR_FORECAST, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRESSURE, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_VISIBILITY, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, + ATTR_WEATHER_WIND_SPEED_UNIT, + DOMAIN as WEATHER_DOMAIN, ) -from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNKNOWN +from homeassistant.const import ATTR_ATTRIBUTION, SPEED_METERS_PER_SECOND, STATE_UNKNOWN from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow from . import ENTITY_ID, TEST_CONFIG @@ -58,13 +64,13 @@ async def test_setup_hass( assert state.state == "sunny" 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_SMHI_WIND_GUST_SPEED] == 16.92 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 assert state.attributes[ATTR_WEATHER_VISIBILITY] == 50 - assert state.attributes[ATTR_WEATHER_WIND_SPEED] == 7 + assert state.attributes[ATTR_WEATHER_WIND_SPEED] == 6.84 assert state.attributes[ATTR_WEATHER_WIND_BEARING] == 134 assert len(state.attributes["forecast"]) == 4 @@ -74,6 +80,9 @@ async def test_setup_hass( assert forecast[ATTR_FORECAST_TEMP_LOW] == 6 assert forecast[ATTR_FORECAST_PRECIPITATION] == 0 assert forecast[ATTR_FORECAST_CONDITION] == "partlycloudy" + assert forecast[ATTR_FORECAST_PRESSURE] == 1026 + assert forecast[ATTR_FORECAST_WIND_BEARING] == 203 + assert forecast[ATTR_FORECAST_WIND_SPEED] == 6.12 async def test_properties_no_data(hass: HomeAssistant) -> None: @@ -305,3 +314,35 @@ def test_condition_class(): assert get_condition(23) == "snowy-rainy" # 24. Heavy sleet assert get_condition(24) == "snowy-rainy" + + +async def test_custom_speed_unit( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str +) -> None: + """Test Wind Gust speed with custom unit.""" + uri = APIURL_TEMPLATE.format(TEST_CONFIG["longitude"], TEST_CONFIG["latitude"]) + aioclient_mock.get(uri, text=api_response) + + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + + assert state + assert state.name == "test" + assert state.attributes[ATTR_SMHI_WIND_GUST_SPEED] == 16.92 + + entity_reg = er.async_get(hass) + entity_reg.async_update_entity_options( + state.entity_id, + WEATHER_DOMAIN, + {ATTR_WEATHER_WIND_SPEED_UNIT: SPEED_METERS_PER_SECOND}, + ) + + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_SMHI_WIND_GUST_SPEED] == 4.7 From e706c6a15f639184b854e27962e4e33f02d2bc99 Mon Sep 17 00:00:00 2001 From: leroyloren <57643470+leroyloren@users.noreply.github.com> Date: Tue, 28 Jun 2022 01:53:57 +0200 Subject: [PATCH 1819/3516] Visiblity fix unit km to m (#74008) --- homeassistant/components/openweathermap/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 027c08fd84b..f180f2a9bbf 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -31,7 +31,7 @@ from homeassistant.components.weather import ( ) from homeassistant.const import ( DEGREE, - LENGTH_KILOMETERS, + LENGTH_METERS, LENGTH_MILLIMETERS, PERCENTAGE, PRESSURE_HPA, @@ -248,7 +248,7 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=ATTR_API_VISIBILITY_DISTANCE, name="Visibility", - native_unit_of_measurement=LENGTH_KILOMETERS, + native_unit_of_measurement=LENGTH_METERS, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( From fef21c02eedc757eb41114dd655c08c0c8952761 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 28 Jun 2022 07:56:10 +0800 Subject: [PATCH 1820/3516] Clean up disabling audio in stream (#74038) --- homeassistant/components/stream/worker.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 4cfe8864de0..1d29bd17c33 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -473,10 +473,6 @@ def stream_worker( audio_stream = None if audio_stream and audio_stream.name not in AUDIO_CODECS: audio_stream = None - # These formats need aac_adtstoasc bitstream filter, but auto_bsf not - # compatible with empty_moov and manual bitstream filters not in PyAV - if container.format.name in {"hls", "mpegts"}: - audio_stream = None # Some audio streams do not have a profile and throw errors when remuxing if audio_stream and audio_stream.profile is None: audio_stream = None From 192986ba8a2e8f0146776381def52d22eaeabb2e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 28 Jun 2022 01:57:46 +0200 Subject: [PATCH 1821/3516] Migrate buienradar to native_* (#74059) --- .../components/buienradar/weather.py | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index aa336d3929c..6fdf5c166ee 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -28,16 +28,25 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + LENGTH_METERS, + LENGTH_MILLIMETERS, + PRESSURE_HPA, + SPEED_METERS_PER_SECOND, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -111,7 +120,11 @@ async def async_setup_entry( class BrWeather(WeatherEntity): """Representation of a weather condition.""" - _attr_temperature_unit = TEMP_CELSIUS + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_visibility_unit = LENGTH_METERS + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND def __init__(self, data, config, coordinates): """Initialize the platform with a data instance and station name.""" @@ -142,12 +155,12 @@ class BrWeather(WeatherEntity): return conditions.get(ccode) @property - def temperature(self): + def native_temperature(self): """Return the current temperature.""" return self._data.temperature @property - def pressure(self): + def native_pressure(self): """Return the current pressure.""" return self._data.pressure @@ -157,18 +170,14 @@ class BrWeather(WeatherEntity): return self._data.humidity @property - def visibility(self): - """Return the current visibility in km.""" - if self._data.visibility is None: - return None - return round(self._data.visibility / 1000, 1) + def native_visibility(self): + """Return the current visibility in m.""" + return self._data.visibility @property - def wind_speed(self): - """Return the current windspeed in km/h.""" - if self._data.wind_speed is None: - return None - return round(self._data.wind_speed * 3.6, 1) + def native_wind_speed(self): + """Return the current windspeed in m/s.""" + return self._data.wind_speed @property def wind_bearing(self): @@ -191,11 +200,11 @@ class BrWeather(WeatherEntity): data_out = { ATTR_FORECAST_TIME: data_in.get(DATETIME).isoformat(), ATTR_FORECAST_CONDITION: cond[condcode], - ATTR_FORECAST_TEMP_LOW: data_in.get(MIN_TEMP), - ATTR_FORECAST_TEMP: data_in.get(MAX_TEMP), - ATTR_FORECAST_PRECIPITATION: data_in.get(RAIN), + ATTR_FORECAST_NATIVE_TEMP_LOW: data_in.get(MIN_TEMP), + ATTR_FORECAST_NATIVE_TEMP: data_in.get(MAX_TEMP), + ATTR_FORECAST_NATIVE_PRECIPITATION: data_in.get(RAIN), ATTR_FORECAST_WIND_BEARING: data_in.get(WINDAZIMUTH), - ATTR_FORECAST_WIND_SPEED: round(data_in.get(WINDSPEED) * 3.6, 1), + ATTR_FORECAST_NATIVE_WIND_SPEED: data_in.get(WINDSPEED), } fcdata_out.append(data_out) From 7e341aaef218dbeb4c48102ed1aa752769bc5f82 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 28 Jun 2022 00:26:39 +0000 Subject: [PATCH 1822/3516] [ci skip] Translation update --- homeassistant/components/awair/translations/en.json | 7 +++++++ homeassistant/components/hive/translations/et.json | 9 ++++++++- homeassistant/components/hive/translations/no.json | 9 ++++++++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/awair/translations/en.json b/homeassistant/components/awair/translations/en.json index 52ffd13ec79..caec592c527 100644 --- a/homeassistant/components/awair/translations/en.json +++ b/homeassistant/components/awair/translations/en.json @@ -10,6 +10,13 @@ "unknown": "Unexpected error" }, "step": { + "reauth": { + "data": { + "access_token": "Access Token", + "email": "Email" + }, + "description": "Please re-enter your Awair developer access token." + }, "reauth_confirm": { "data": { "access_token": "Access Token", diff --git a/homeassistant/components/hive/translations/et.json b/homeassistant/components/hive/translations/et.json index 5cffcb036d3..d5916257589 100644 --- a/homeassistant/components/hive/translations/et.json +++ b/homeassistant/components/hive/translations/et.json @@ -20,6 +20,13 @@ "description": "Sisesta oma Hive autentimiskood. \n\n Uue koodi taotlemiseks sisesta kood 0000.", "title": "Hive kaheastmeline autentimine." }, + "configuration": { + "data": { + "device_name": "Seadme nimi" + }, + "description": "Sisesta oma Hive andmed", + "title": "Hive s\u00e4tted" + }, "reauth": { "data": { "password": "Salas\u00f5na", @@ -34,7 +41,7 @@ "scan_interval": "P\u00e4ringute intervall (sekundites)", "username": "Kasutajanimi" }, - "description": "Sisesta oma Hive sisselogimisteave ja s\u00e4tted.", + "description": "Sisesta oma Hive sisselogimisteave.", "title": "Hive sisselogimine" } } diff --git a/homeassistant/components/hive/translations/no.json b/homeassistant/components/hive/translations/no.json index c5213aafeee..17241b940c4 100644 --- a/homeassistant/components/hive/translations/no.json +++ b/homeassistant/components/hive/translations/no.json @@ -20,6 +20,13 @@ "description": "Skriv inn din Hive-godkjenningskode. \n\n Vennligst skriv inn kode 0000 for \u00e5 be om en annen kode.", "title": "Hive Totrinnsbekreftelse autentisering." }, + "configuration": { + "data": { + "device_name": "Enhetsnavn" + }, + "description": "Skriv inn Hive-konfigurasjonen din", + "title": "Hive-konfigurasjon." + }, "reauth": { "data": { "password": "Passord", @@ -34,7 +41,7 @@ "scan_interval": "Skanneintervall (sekunder)", "username": "Brukernavn" }, - "description": "Skriv inn inn innloggingsinformasjonen og konfigurasjonen for Hive.", + "description": "Skriv inn Hive-p\u00e5loggingsinformasjonen din.", "title": "Hive-p\u00e5logging" } } From cdaa6c0d429bc70078537291bb35d1bba2a9f24d Mon Sep 17 00:00:00 2001 From: maikukun <107741047+maikukun@users.noreply.github.com> Date: Mon, 27 Jun 2022 19:31:30 -0700 Subject: [PATCH 1823/3516] Update tesla_powerwall to 0.3.18 (#74026) --- 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 be5d4678e27..3d1eb07f3fa 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.17"], + "requirements": ["tesla-powerwall==0.3.18"], "codeowners": ["@bdraco", "@jrester"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 105f8f08f20..5f5fc9fe4d6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2298,7 +2298,7 @@ temperusb==1.5.3 # tensorflow==2.5.0 # homeassistant.components.powerwall -tesla-powerwall==0.3.17 +tesla-powerwall==0.3.18 # homeassistant.components.tesla_wall_connector tesla-wall-connector==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 43fb970d99a..24903c43b30 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1519,7 +1519,7 @@ tailscale==0.2.0 tellduslive==0.10.11 # homeassistant.components.powerwall -tesla-powerwall==0.3.17 +tesla-powerwall==0.3.18 # homeassistant.components.tesla_wall_connector tesla-wall-connector==1.0.1 From 1e0788aeea2c331b30a6f56ec57600bc4f045b14 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 28 Jun 2022 04:51:02 +0200 Subject: [PATCH 1824/3516] Allow partial tests to take 20 minutes, use all cores (#74079) --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fcd879d7512..bf690740c6d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -803,7 +803,7 @@ jobs: tests - name: Run pytest (partially) if: needs.changes.outputs.test_full_suite == 'false' - timeout-minutes: 10 + timeout-minutes: 20 shell: bash run: | . venv/bin/activate @@ -818,7 +818,7 @@ jobs: -qq \ --timeout=9 \ --durations=10 \ - -n 0 \ + -n auto \ --cov="homeassistant.components.${{ matrix.group }}" \ --cov-report=xml \ --cov-report=term-missing \ From df357962b3f25004eb73cbda38e372ad9bd10a8d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Jun 2022 21:59:08 -0700 Subject: [PATCH 1825/3516] Bump orjson to 3.7.5 (#74083) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6fb51cf0d09..dfbf056936f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,7 +20,7 @@ httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.7 -orjson==3.7.2 +orjson==3.7.5 paho-mqtt==1.6.1 pillow==9.1.1 pip>=21.0,<22.2 diff --git a/pyproject.toml b/pyproject.toml index fc07110dbad..59df3967b6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ dependencies = [ "PyJWT==2.4.0", # PyJWT has loose dependency. We want the latest one. "cryptography==36.0.2", - "orjson==3.7.2", + "orjson==3.7.5", "pip>=21.0,<22.2", "python-slugify==4.0.1", "pyyaml==6.0", diff --git a/requirements.txt b/requirements.txt index 21e11def1b5..7506201eae1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ ifaddr==0.1.7 jinja2==3.1.2 PyJWT==2.4.0 cryptography==36.0.2 -orjson==3.7.2 +orjson==3.7.5 pip>=21.0,<22.2 python-slugify==4.0.1 pyyaml==6.0 From 91a119917d93527c73778dcfe78feb17537c2ed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=5Bp=CA=B2=C9=B5s=5D?= Date: Tue, 28 Jun 2022 07:00:44 +0200 Subject: [PATCH 1826/3516] List more private and link-local IP networks (#74064) List more private and link-local IP networks The IPv6 link-local network is especially important as without it local accounts don't work on IPv6-capable networks with no IPv6 DHCP server. --- homeassistant/util/network.py | 21 ++++++++++++++------- tests/util/test_network.py | 8 ++++++++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/homeassistant/util/network.py b/homeassistant/util/network.py index 87077a0eb0a..7d0d6e99639 100644 --- a/homeassistant/util/network.py +++ b/homeassistant/util/network.py @@ -14,14 +14,21 @@ LOOPBACK_NETWORKS = ( # RFC6890 - Address allocation for Private Internets PRIVATE_NETWORKS = ( - ip_network("fd00::/8"), ip_network("10.0.0.0/8"), ip_network("172.16.0.0/12"), ip_network("192.168.0.0/16"), + ip_network("fd00::/8"), + ip_network("::ffff:10.0.0.0/104"), + ip_network("::ffff:172.16.0.0/108"), + ip_network("::ffff:192.168.0.0/112"), ) # RFC6890 - Link local ranges -LINK_LOCAL_NETWORK = ip_network("169.254.0.0/16") +LINK_LOCAL_NETWORKS = ( + ip_network("169.254.0.0/16"), + ip_network("fe80::/10"), + ip_network("::ffff:169.254.0.0/112"), +) def is_loopback(address: IPv4Address | IPv6Address) -> bool: @@ -30,18 +37,18 @@ def is_loopback(address: IPv4Address | IPv6Address) -> bool: def is_private(address: IPv4Address | IPv6Address) -> bool: - """Check if an address is a private address.""" + """Check if an address is a unique local non-loopback address.""" return any(address in network for network in PRIVATE_NETWORKS) def is_link_local(address: IPv4Address | IPv6Address) -> bool: - """Check if an address is link local.""" - return address in LINK_LOCAL_NETWORK + """Check if an address is link-local (local but not necessarily unique).""" + return any(address in network for network in LINK_LOCAL_NETWORKS) def is_local(address: IPv4Address | IPv6Address) -> bool: - """Check if an address is loopback or private.""" - return is_loopback(address) or is_private(address) + """Check if an address is on a local network.""" + return is_loopback(address) or is_private(address) or is_link_local(address) def is_invalid(address: IPv4Address | IPv6Address) -> bool: diff --git a/tests/util/test_network.py b/tests/util/test_network.py index 4f372e5e1a7..7339b6dc51d 100644 --- a/tests/util/test_network.py +++ b/tests/util/test_network.py @@ -30,7 +30,9 @@ def test_is_private(): def test_is_link_local(): """Test link local addresses.""" assert network_util.is_link_local(ip_address("169.254.12.3")) + assert network_util.is_link_local(ip_address("fe80::1234:5678:abcd")) assert not network_util.is_link_local(ip_address("127.0.0.1")) + assert not network_util.is_link_local(ip_address("::1")) def test_is_invalid(): @@ -43,7 +45,13 @@ def test_is_local(): """Test local addresses.""" assert network_util.is_local(ip_address("192.168.0.1")) assert network_util.is_local(ip_address("127.0.0.1")) + assert network_util.is_local(ip_address("fd12:3456:789a:1::1")) + assert network_util.is_local(ip_address("fe80::1234:5678:abcd")) + assert network_util.is_local(ip_address("::ffff:192.168.0.1")) assert not network_util.is_local(ip_address("208.5.4.2")) + assert not network_util.is_local(ip_address("198.51.100.1")) + assert not network_util.is_local(ip_address("2001:DB8:FA1::1")) + assert not network_util.is_local(ip_address("::ffff:208.5.4.2")) def test_is_ip_address(): From 1804f70a5b4f8bd3a178ea7969134f9d064cda3f Mon Sep 17 00:00:00 2001 From: Matrix Date: Tue, 28 Jun 2022 14:39:12 +0800 Subject: [PATCH 1827/3516] Fix missing leak sensor battery expose (#74084) --- homeassistant/components/yolink/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 7c578fbaa73..4679c3e670b 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -22,6 +22,7 @@ from .const import ( ATTR_COORDINATORS, ATTR_DEVICE_CO_SMOKE_SENSOR, ATTR_DEVICE_DOOR_SENSOR, + ATTR_DEVICE_LEAK_SENSOR, ATTR_DEVICE_LOCK, ATTR_DEVICE_MANIPULATOR, ATTR_DEVICE_MOTION_SENSOR, @@ -61,6 +62,7 @@ SENSOR_DEVICE_TYPE = [ BATTERY_POWER_SENSOR = [ ATTR_DEVICE_DOOR_SENSOR, + ATTR_DEVICE_LEAK_SENSOR, ATTR_DEVICE_MOTION_SENSOR, ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_VIBRATION_SENSOR, From 530e1f908020631932c6d2fb5b80626f19325e22 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 08:40:46 +0200 Subject: [PATCH 1828/3516] Fix reauth step in geocaching (#74089) --- homeassistant/components/geocaching/config_flow.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/geocaching/config_flow.py b/homeassistant/components/geocaching/config_flow.py index 83c9ed17586..9ce3cb76775 100644 --- a/homeassistant/components/geocaching/config_flow.py +++ b/homeassistant/components/geocaching/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Geocaching.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -24,9 +25,9 @@ class GeocachingFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): """Return logger.""" return logging.getLogger(__name__) - async def async_step_reauth(self, user_input: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" - return await self.async_step_reauth_confirm(user_input=user_input) + return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None From b6676df1cbc6430339c5b8f9b40baa4009d95067 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 08:43:06 +0200 Subject: [PATCH 1829/3516] Adjust config-flow reauth type hints in components (#74088) --- homeassistant/components/google/config_flow.py | 5 ++--- homeassistant/components/neato/config_flow.py | 4 ++-- homeassistant/components/netatmo/config_flow.py | 4 +++- homeassistant/components/spotify/config_flow.py | 3 ++- homeassistant/components/yolink/config_flow.py | 3 ++- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index 046840075ff..a5951edec22 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Google integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -155,9 +156,7 @@ class OAuth2FlowHandler( }, ) - async def async_step_reauth( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" self._reauth_config_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py index 15544371b2e..1c180fe1dbd 100644 --- a/homeassistant/components/neato/config_flow.py +++ b/homeassistant/components/neato/config_flow.py @@ -1,8 +1,8 @@ """Config flow for Neato Botvac.""" from __future__ import annotations +from collections.abc import Mapping import logging -from types import MappingProxyType from typing import Any from homeassistant.config_entries import SOURCE_REAUTH @@ -35,7 +35,7 @@ class OAuth2FlowHandler( return await super().async_step_user(user_input=user_input) - async def async_step_reauth(self, data: MappingProxyType[str, Any]) -> FlowResult: + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon migration of old entries.""" return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index bbd28e8398f..125e6ee38e5 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -1,7 +1,9 @@ """Config flow for Netatmo.""" from __future__ import annotations +from collections.abc import Mapping import logging +from typing import Any import uuid import voluptuous as vol @@ -66,7 +68,7 @@ class NetatmoFlowHandler( return await super().async_step_user(user_input) - async def async_step_reauth(self, user_input: dict | None = None) -> FlowResult: + async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/spotify/config_flow.py b/homeassistant/components/spotify/config_flow.py index e1780ff9d40..30ad74f8e26 100644 --- a/homeassistant/components/spotify/config_flow.py +++ b/homeassistant/components/spotify/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Spotify.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -56,7 +57,7 @@ class SpotifyFlowHandler( return self.async_create_entry(title=name, data=data) - async def async_step_reauth(self, entry: dict[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry: Mapping[str, Any]) -> FlowResult: """Perform reauth upon migration of old entries.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/yolink/config_flow.py b/homeassistant/components/yolink/config_flow.py index 35a4c4ebea8..68eabdfa183 100644 --- a/homeassistant/components/yolink/config_flow.py +++ b/homeassistant/components/yolink/config_flow.py @@ -1,6 +1,7 @@ """Config flow for yolink.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -30,7 +31,7 @@ class OAuth2FlowHandler( scopes = ["create"] return {"scope": " ".join(scopes)} - async def async_step_reauth(self, user_input=None) -> FlowResult: + async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] From 720768560d9145857e3bcbcfc9b6c4f0467d140d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Jun 2022 23:44:06 -0700 Subject: [PATCH 1830/3516] Fix devices missing in logbook when all requested entities are filtered (#74073) --- .../components/logbook/websocket_api.py | 2 +- .../components/logbook/test_websocket_api.py | 93 +++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index 04afa82e75b..3af87b26caa 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -301,7 +301,7 @@ async def ws_event_stream( entity_ids = msg.get("entity_ids") if entity_ids: entity_ids = async_filter_entities(hass, entity_ids) - if not entity_ids: + if not entity_ids and not device_ids: _async_send_empty_response(connection, msg_id, start_time, end_time) return diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 66fbc9b0bca..6c4908a2ad5 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -2564,3 +2564,96 @@ async def test_logbook_stream_ignores_forced_updates( # Check our listener got unsubscribed assert sum(hass.bus.async_listeners().values()) == init_count + + +@patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) +async def test_subscribe_all_entities_are_continuous_with_device( + hass, recorder_mock, hass_ws_client +): + """Test subscribe/unsubscribe logbook stream with entities that are always filtered and a device.""" + now = dt_util.utcnow() + await asyncio.gather( + *[ + async_setup_component(hass, comp, {}) + for comp in ("homeassistant", "logbook", "automation", "script") + ] + ) + await async_wait_recording_done(hass) + devices = await _async_mock_devices_with_logbook_platform(hass) + device = devices[0] + device2 = devices[1] + + entity_ids = ("sensor.uom", "sensor.uom_two") + + def _create_events(): + for entity_id in entity_ids: + for state in ("1", "2", "3"): + hass.states.async_set( + entity_id, state, {ATTR_UNIT_OF_MEASUREMENT: "any"} + ) + hass.states.async_set("counter.any", state) + hass.states.async_set("proximity.any", state) + hass.bus.async_fire("mock_event", {"device_id": device.id}) + hass.bus.async_fire("mock_event", {"device_id": device2.id}) + + init_count = sum(hass.bus.async_listeners().values()) + _create_events() + + await async_wait_recording_done(hass) + websocket_client = await hass_ws_client() + await websocket_client.send_json( + { + "id": 7, + "type": "logbook/event_stream", + "start_time": now.isoformat(), + "entity_ids": ["sensor.uom", "counter.any", "proximity.any"], + "device_ids": [device.id, device2.id], + } + ) + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + {"domain": "test", "message": "is on fire", "name": "device name", "when": ANY}, + {"domain": "test", "message": "is on fire", "name": "device name", "when": ANY}, + ] + assert msg["event"]["partial"] is True + + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [] + assert "partial" not in msg["event"] + + for _ in range(2): + _create_events() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [ + { + "domain": "test", + "message": "is on fire", + "name": "device name", + "when": ANY, + }, + { + "domain": "test", + "message": "is on fire", + "name": "device name", + "when": ANY, + }, + ] + assert "partial" not in msg["event"] + + await websocket_client.close() + await hass.async_block_till_done() + + # Check our listener got unsubscribed + assert sum(hass.bus.async_listeners().values()) == init_count From fb108533580d5f4c326ca970d8e6fd4998cc5593 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 08:50:16 +0200 Subject: [PATCH 1831/3516] Fix mypy issues in zha core modules (#74028) * Fix mypy issues in zha gateway, group and helpers * Cleanup device * Apply suggestion * Raise ValueError * Use hass.config.path --- homeassistant/components/zha/core/device.py | 4 ++-- homeassistant/components/zha/core/gateway.py | 6 +++--- homeassistant/components/zha/core/group.py | 6 ++---- homeassistant/components/zha/core/helpers.py | 2 ++ mypy.ini | 12 ------------ script/hassfest/mypy_config.py | 4 ---- 6 files changed, 9 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 588fcac7ca6..e83c0afbceb 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -276,7 +276,7 @@ class ZHADevice(LogMixin): @property def skip_configuration(self) -> bool: """Return true if the device should not issue configuration related commands.""" - return self._zigpy_device.skip_configuration or self.is_coordinator + return self._zigpy_device.skip_configuration or bool(self.is_coordinator) @property def gateway(self): @@ -819,7 +819,7 @@ class ZHADevice(LogMixin): fmt = f"{log_msg[1]} completed: %s" zdo.debug(fmt, *(log_msg[2] + (outcome,))) - def log(self, level: int, msg: str, *args: Any, **kwargs: dict) -> None: + def log(self, level: int, msg: str, *args: Any, **kwargs: Any) -> None: """Log a message.""" msg = f"[%s](%s): {msg}" args = (self.nwk, self.model) + args diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 02b2b21c835..b9465d5e2aa 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -8,7 +8,6 @@ from datetime import timedelta from enum import Enum import itertools import logging -import os import time import traceback from typing import TYPE_CHECKING, Any, NamedTuple, Union @@ -163,7 +162,7 @@ class ZHAGateway: app_config = self._config.get(CONF_ZIGPY, {}) database = self._config.get( CONF_DATABASE, - os.path.join(self._hass.config.config_dir, DEFAULT_DATABASE_NAME), + self._hass.config.path(DEFAULT_DATABASE_NAME), ) app_config[CONF_DATABASE] = database app_config[CONF_DEVICE] = self.config_entry.data[CONF_DEVICE] @@ -333,7 +332,7 @@ class ZHAGateway: def group_removed(self, zigpy_group: zigpy.group.Group) -> None: """Handle zigpy group removed event.""" self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_REMOVED) - zha_group = self._groups.pop(zigpy_group.group_id, None) + zha_group = self._groups.pop(zigpy_group.group_id) zha_group.info("group_removed") self._cleanup_group_entity_registry_entries(zigpy_group) @@ -428,6 +427,7 @@ class ZHAGateway: ] # then we get all group entity entries tied to the coordinator + assert self.coordinator_zha_device all_group_entity_entries = er.async_entries_for_device( self.ha_entity_registry, self.coordinator_zha_device.device_id, diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index 1392041c4d4..7f1c9f09998 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -78,7 +78,7 @@ class ZHAGroupMember(LogMixin): return member_info @property - def associated_entities(self) -> list[GroupEntityReference]: + def associated_entities(self) -> list[dict[str, Any]]: """Return the list of entities that were derived from this endpoint.""" ha_entity_registry = self.device.gateway.ha_entity_registry zha_device_registry = self.device.gateway.device_registry @@ -150,9 +150,7 @@ class ZHAGroup(LogMixin): def members(self) -> list[ZHAGroupMember]: """Return the ZHA devices that are members of this group.""" return [ - ZHAGroupMember( - self, self._zha_gateway.devices.get(member_ieee), endpoint_id - ) + ZHAGroupMember(self, self._zha_gateway.devices[member_ieee], endpoint_id) for (member_ieee, endpoint_id) in self._zigpy_group.members.keys() if member_ieee in self._zha_gateway.devices ] diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index f387cc99bfe..390ef290dc2 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -169,6 +169,8 @@ def async_get_zha_device(hass: HomeAssistant, device_id: str) -> ZHADevice: """Get a ZHA device for the given device registry id.""" device_registry = dr.async_get(hass) registry_device = device_registry.async_get(device_id) + if not registry_device: + raise ValueError(f"Device id `{device_id}` not found in registry.") zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee_address = list(list(registry_device.identifiers)[0])[1] ieee = zigpy.types.EUI64.convert(ieee_address) diff --git a/mypy.ini b/mypy.ini index e2e91ef921c..b5c56d71ff9 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2996,21 +2996,9 @@ ignore_errors = true [mypy-homeassistant.components.xiaomi_miio.switch] ignore_errors = true -[mypy-homeassistant.components.zha.core.device] -ignore_errors = true - [mypy-homeassistant.components.zha.core.discovery] ignore_errors = true -[mypy-homeassistant.components.zha.core.gateway] -ignore_errors = true - -[mypy-homeassistant.components.zha.core.group] -ignore_errors = true - -[mypy-homeassistant.components.zha.core.helpers] -ignore_errors = true - [mypy-homeassistant.components.zha.core.registries] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 49cf3b7b65a..453eade7b2d 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -144,11 +144,7 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.xiaomi_miio.light", "homeassistant.components.xiaomi_miio.sensor", "homeassistant.components.xiaomi_miio.switch", - "homeassistant.components.zha.core.device", "homeassistant.components.zha.core.discovery", - "homeassistant.components.zha.core.gateway", - "homeassistant.components.zha.core.group", - "homeassistant.components.zha.core.helpers", "homeassistant.components.zha.core.registries", "homeassistant.components.zha.core.store", ] From cc8170fcfec8ed9070ebdc903c19246b228d8e60 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 28 Jun 2022 08:50:31 +0200 Subject: [PATCH 1832/3516] Align code between group platforms (#74057) --- .../components/group/binary_sensor.py | 21 +++++++++---------- homeassistant/components/group/light.py | 18 +++++++--------- homeassistant/components/group/switch.py | 1 + 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/group/binary_sensor.py b/homeassistant/components/group/binary_sensor.py index ff0e58badfb..473a5a5e885 100644 --- a/homeassistant/components/group/binary_sensor.py +++ b/homeassistant/components/group/binary_sensor.py @@ -88,6 +88,8 @@ async def async_setup_entry( class BinarySensorGroup(GroupEntity, BinarySensorEntity): """Representation of a BinarySensorGroup.""" + _attr_available: bool = False + def __init__( self, unique_id: str | None, @@ -127,27 +129,24 @@ class BinarySensorGroup(GroupEntity, BinarySensorEntity): @callback def async_update_group_state(self) -> None: """Query all members and determine the binary sensor group state.""" - all_states = [self.hass.states.get(x) for x in self._entity_ids] - - # filtered_states are members currently in the state machine - filtered_states: list[str] = [x.state for x in all_states if x is not None] + states = [ + state.state + for entity_id in self._entity_ids + if (state := self.hass.states.get(entity_id)) is not None + ] # Set group as unavailable if all members are unavailable or missing - self._attr_available = any( - state != STATE_UNAVAILABLE for state in filtered_states - ) + self._attr_available = any(state != STATE_UNAVAILABLE for state in states) valid_state = self.mode( - state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in filtered_states + state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states ) if not valid_state: # Set as unknown if any / all member is not unknown or unavailable self._attr_is_on = None else: # Set as ON if any / all member is ON - states = list(map(lambda x: x == STATE_ON, filtered_states)) - state = self.mode(states) - self._attr_is_on = state + self._attr_is_on = self.mode(state == STATE_ON for state in states) @property def device_class(self) -> str | None: diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index b9741085c2d..e0645da6141 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -46,7 +46,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event @@ -214,15 +214,15 @@ class LightGroup(GroupEntity, LightEntity): @callback def async_update_group_state(self) -> None: """Query all members and determine the light group state.""" - all_states = [self.hass.states.get(x) for x in self._entity_ids] - states: list[State] = list(filter(None, all_states)) + states = [ + state + for entity_id in self._entity_ids + if (state := self.hass.states.get(entity_id)) is not None + ] on_states = [state for state in states if state.state == STATE_ON] - # filtered_states are members currently in the state machine - filtered_states: list[str] = [x.state for x in all_states if x is not None] - valid_state = self.mode( - state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in filtered_states + state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states ) if not valid_state: @@ -230,9 +230,7 @@ class LightGroup(GroupEntity, LightEntity): self._attr_is_on = None else: # Set as ON if any / all member is ON - self._attr_is_on = self.mode( - list(map(lambda x: x == STATE_ON, filtered_states)) - ) + self._attr_is_on = self.mode(state.state == STATE_ON for state in states) self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states) self._attr_brightness = reduce_attribute(on_states, ATTR_BRIGHTNESS) diff --git a/homeassistant/components/group/switch.py b/homeassistant/components/group/switch.py index 6b879e55cea..8b60e1f1402 100644 --- a/homeassistant/components/group/switch.py +++ b/homeassistant/components/group/switch.py @@ -170,4 +170,5 @@ class SwitchGroup(GroupEntity, SwitchEntity): # Set as ON if any / all member is ON self._attr_is_on = self.mode(state == STATE_ON for state in states) + # Set group as unavailable if all members are unavailable or missing self._attr_available = any(state != STATE_UNAVAILABLE for state in states) From 87b46a699a21fc06672bc780193f3c264dcf8424 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 08:52:20 +0200 Subject: [PATCH 1833/3516] Fix mypy issues in zha store (#74032) --- homeassistant/components/zha/core/store.py | 13 +++++++------ mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index c82f05303a5..e58dcd46dba 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -5,7 +5,7 @@ from collections import OrderedDict from collections.abc import MutableMapping import datetime import time -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Any, cast import attr @@ -45,10 +45,11 @@ class ZhaStorage: @callback def async_create_device(self, device: ZHADevice) -> ZhaDeviceEntry: """Create a new ZhaDeviceEntry.""" + ieee_str: str = str(device.ieee) device_entry: ZhaDeviceEntry = ZhaDeviceEntry( - name=device.name, ieee=str(device.ieee), last_seen=device.last_seen + name=device.name, ieee=ieee_str, last_seen=device.last_seen ) - self.devices[device_entry.ieee] = device_entry + self.devices[ieee_str] = device_entry self.async_schedule_save() return device_entry @@ -81,8 +82,8 @@ class ZhaStorage: ieee_str: str = str(device.ieee) old = self.devices[ieee_str] - if old is not None and device.last_seen is None: - return + if device.last_seen is None: + return old changes = {} changes["last_seen"] = device.last_seen @@ -93,7 +94,7 @@ class ZhaStorage: async def async_load(self) -> None: """Load the registry of zha device entries.""" - data = await self._store.async_load() + data = cast(dict[str, Any], await self._store.async_load()) devices: OrderedDict[str, ZhaDeviceEntry] = OrderedDict() diff --git a/mypy.ini b/mypy.ini index b5c56d71ff9..0f8c8fc0b61 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3001,6 +3001,3 @@ ignore_errors = true [mypy-homeassistant.components.zha.core.registries] ignore_errors = true - -[mypy-homeassistant.components.zha.core.store] -ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 453eade7b2d..0fd62d49ae2 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -146,7 +146,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.xiaomi_miio.switch", "homeassistant.components.zha.core.discovery", "homeassistant.components.zha.core.registries", - "homeassistant.components.zha.core.store", ] # Component modules which should set no_implicit_reexport = true. From 567df9cc4db44d42431b25300f1781713bf44581 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Jun 2022 23:53:17 -0700 Subject: [PATCH 1834/3516] Add async_remove_config_entry_device to enphase_envoy (#74012) --- .../components/enphase_envoy/__init__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 696baa31775..61bf9b64bcf 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -12,6 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -96,3 +97,20 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry +) -> bool: + """Remove a enphase_envoy config entry from a device.""" + dev_ids = {dev_id[1] for dev_id in device_entry.identifiers if dev_id[0] == DOMAIN} + data: dict = hass.data[DOMAIN][config_entry.entry_id] + coordinator: DataUpdateCoordinator = data[COORDINATOR] + envoy_data: dict = coordinator.data + envoy_serial_num = config_entry.unique_id + if envoy_serial_num in dev_ids: + return False + for inverter in envoy_data.get("inverters_production", []): + if str(inverter) in dev_ids: + return False + return True From cb46bb5bfad279ce21e99f37b4a75f9cb2661c58 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Jun 2022 00:34:56 -0700 Subject: [PATCH 1835/3516] Revert "Partially revert "Switch loader to use json helper (#73872)" (#74077)" (#74087) --- homeassistant/loader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 589f316532b..ab681d7c42d 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -11,7 +11,6 @@ from collections.abc import Callable from contextlib import suppress import functools as ft import importlib -import json import logging import pathlib import sys @@ -30,6 +29,7 @@ from .generated.mqtt import MQTT from .generated.ssdp import SSDP from .generated.usb import USB from .generated.zeroconf import HOMEKIT, ZEROCONF +from .helpers.json import JSON_DECODE_EXCEPTIONS, json_loads from .util.async_ import gather_with_concurrency # Typing imports that create a circular dependency @@ -366,8 +366,8 @@ class Integration: continue try: - manifest = json.loads(manifest_path.read_text()) - except ValueError as err: + manifest = json_loads(manifest_path.read_text()) + except JSON_DECODE_EXCEPTIONS as err: _LOGGER.error( "Error parsing manifest.json file at %s: %s", manifest_path, err ) From 800bae68a8c45433b132cb50bcb700f2c758940c Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 28 Jun 2022 02:51:33 -0500 Subject: [PATCH 1836/3516] Fix clearing of Sonos library cache during regrouping (#74085) Fix clearing of ZoneGroupState attribute cache --- homeassistant/components/sonos/speaker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index d37e3bac2a3..729d1a1457f 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -1084,8 +1084,8 @@ class SonosSpeaker: except asyncio.TimeoutError: _LOGGER.warning("Timeout waiting for target groups %s", groups) - for speaker in hass.data[DATA_SONOS].discovered.values(): - speaker.soco._zgs_cache.clear() # pylint: disable=protected-access + any_speaker = next(iter(hass.data[DATA_SONOS].discovered.values())) + any_speaker.soco.zone_group_state.clear_cache() # # Media and playback state handlers From 8e1ec07f3d3ab5e95a4bb7c44dfff048dd9a4b74 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 10:00:23 +0200 Subject: [PATCH 1837/3516] Adjust type hints in component alarm methods (#74092) * Adjust type hints in component alarm methods * Undo related change * Undo related change --- .../components/agent_dvr/alarm_control_panel.py | 12 +++++++----- .../alarmdecoder/alarm_control_panel.py | 12 +++++++----- .../components/blink/alarm_control_panel.py | 8 +++++--- .../components/concord232/alarm_control_panel.py | 8 ++++---- .../components/egardia/alarm_control_panel.py | 10 +++++----- .../components/envisalink/alarm_control_panel.py | 12 ++++++------ .../components/hive/alarm_control_panel.py | 10 ++++++---- .../homematicip_cloud/alarm_control_panel.py | 6 +++--- .../components/ialarm/alarm_control_panel.py | 8 +++++--- .../components/ifttt/alarm_control_panel.py | 8 ++++---- .../components/lupusec/alarm_control_panel.py | 6 +++--- .../components/manual/alarm_control_panel.py | 16 ++++++++-------- .../manual_mqtt/alarm_control_panel.py | 12 ++++++------ .../components/mqtt/alarm_control_panel.py | 14 +++++++------- .../components/ness_alarm/alarm_control_panel.py | 10 +++++----- .../components/nx584/alarm_control_panel.py | 8 ++++---- .../components/point/alarm_control_panel.py | 10 ++++++---- .../components/risco/alarm_control_panel.py | 12 +++++++----- .../satel_integra/alarm_control_panel.py | 8 ++++---- .../components/spc/alarm_control_panel.py | 10 +++++----- .../components/template/alarm_control_panel.py | 10 +++++----- .../totalconnect/alarm_control_panel.py | 14 ++++++++------ .../xiaomi_miio/alarm_control_panel.py | 8 +++++--- 23 files changed, 125 insertions(+), 107 deletions(-) diff --git a/homeassistant/components/agent_dvr/alarm_control_panel.py b/homeassistant/components/agent_dvr/alarm_control_panel.py index 8978be97c1d..632b2e29d57 100644 --- a/homeassistant/components/agent_dvr/alarm_control_panel.py +++ b/homeassistant/components/agent_dvr/alarm_control_panel.py @@ -1,4 +1,6 @@ """Support for Agent DVR Alarm Control Panels.""" +from __future__ import annotations + from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntity, AlarmControlPanelEntityFeature, @@ -58,7 +60,7 @@ class AgentBaseStation(AlarmControlPanelEntity): sw_version=client.version, ) - async def async_update(self): + async def async_update(self) -> None: """Update the state of the device.""" await self._client.update() self._attr_available = self._client.is_available @@ -76,24 +78,24 @@ class AgentBaseStation(AlarmControlPanelEntity): else: self._attr_state = STATE_ALARM_DISARMED - async def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" await self._client.disarm() self._attr_state = STATE_ALARM_DISARMED - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command. Uses custom mode.""" await self._client.arm() await self._client.set_active_profile(CONF_AWAY_MODE_NAME) self._attr_state = STATE_ALARM_ARMED_AWAY - async def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command. Uses custom mode.""" await self._client.arm() await self._client.set_active_profile(CONF_HOME_MODE_NAME) self._attr_state = STATE_ALARM_ARMED_HOME - async def async_alarm_arm_night(self, code=None): + async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command. Uses custom mode.""" await self._client.arm() await self._client.set_active_profile(CONF_NIGHT_MODE_NAME) diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 991d588eccf..ca11b9d6894 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -1,4 +1,6 @@ """Support for AlarmDecoder-based alarm control panels (Honeywell/DSC).""" +from __future__ import annotations + import voluptuous as vol from homeassistant.components.alarm_control_panel import ( @@ -91,7 +93,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): self._attr_code_arm_required = code_arm_required self._alt_night_mode = alt_night_mode - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( @@ -126,12 +128,12 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): } self.schedule_update_ha_state() - def alarm_disarm(self, code=None): + def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if code: self._client.send(f"{code!s}1") - def alarm_arm_away(self, code=None): + def alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" self._client.arm_away( code=code, @@ -139,7 +141,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): auto_bypass=self._auto_bypass, ) - def alarm_arm_home(self, code=None): + def alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" self._client.arm_home( code=code, @@ -147,7 +149,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanelEntity): auto_bypass=self._auto_bypass, ) - def alarm_arm_night(self, code=None): + def alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" self._client.arm_night( code=code, diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py index dbea1371af2..22a142ff44c 100644 --- a/homeassistant/components/blink/alarm_control_panel.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -1,4 +1,6 @@ """Support for Blink Alarm Control Panel.""" +from __future__ import annotations + import logging from homeassistant.components.alarm_control_panel import ( @@ -51,7 +53,7 @@ class BlinkSyncModule(AlarmControlPanelEntity): identifiers={(DOMAIN, sync.serial)}, name=name, manufacturer=DEFAULT_BRAND ) - def update(self): + def update(self) -> None: """Update the state of the device.""" _LOGGER.debug("Updating Blink Alarm Control Panel %s", self._name) self.data.refresh() @@ -63,12 +65,12 @@ class BlinkSyncModule(AlarmControlPanelEntity): self.sync.attributes[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION self._attr_extra_state_attributes = self.sync.attributes - def alarm_disarm(self, code=None): + def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" self.sync.arm = False self.sync.refresh() - def alarm_arm_away(self, code=None): + def alarm_arm_away(self, code: str | None = None) -> None: """Send arm command.""" self.sync.arm = True self.sync.refresh() diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index e8f21c46278..4b46d1bf98c 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -103,7 +103,7 @@ class Concord232Alarm(alarm.AlarmControlPanelEntity): """Return the state of the device.""" return self._state - def update(self): + def update(self) -> None: """Update values from API.""" try: part = self._alarm.list_partitions()[0] @@ -124,13 +124,13 @@ class Concord232Alarm(alarm.AlarmControlPanelEntity): else: self._state = STATE_ALARM_ARMED_AWAY - def alarm_disarm(self, code=None): + def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if not self._validate_code(code, STATE_ALARM_DISARMED): return self._alarm.disarm(code) - def alarm_arm_home(self, code=None): + def alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" if not self._validate_code(code, STATE_ALARM_ARMED_HOME): return @@ -139,7 +139,7 @@ class Concord232Alarm(alarm.AlarmControlPanelEntity): else: self._alarm.arm("stay") - def alarm_arm_away(self, code=None): + def alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" if not self._validate_code(code, STATE_ALARM_ARMED_AWAY): return diff --git a/homeassistant/components/egardia/alarm_control_panel.py b/homeassistant/components/egardia/alarm_control_panel.py index 2e2abe1fc87..f35d248c968 100644 --- a/homeassistant/components/egardia/alarm_control_panel.py +++ b/homeassistant/components/egardia/alarm_control_panel.py @@ -79,7 +79,7 @@ class EgardiaAlarm(alarm.AlarmControlPanelEntity): self._rs_codes = rs_codes self._rs_port = rs_port - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Add Egardiaserver callback if enabled.""" if self._rs_enabled: _LOGGER.debug("Registering callback to Egardiaserver") @@ -134,12 +134,12 @@ class EgardiaAlarm(alarm.AlarmControlPanelEntity): else: _LOGGER.error("Ignoring status") - def update(self): + def update(self) -> None: """Update the alarm status.""" status = self._egardiasystem.getstate() self.parsestatus(status) - def alarm_disarm(self, code=None): + def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" try: self._egardiasystem.alarm_disarm() @@ -149,7 +149,7 @@ class EgardiaAlarm(alarm.AlarmControlPanelEntity): err, ) - def alarm_arm_home(self, code=None): + def alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" try: self._egardiasystem.alarm_arm_home() @@ -160,7 +160,7 @@ class EgardiaAlarm(alarm.AlarmControlPanelEntity): err, ) - def alarm_arm_away(self, code=None): + def alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" try: self._egardiasystem.alarm_arm_away() diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index 3dfa1e0762d..35cfe8558a8 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -121,7 +121,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity): _LOGGER.debug("Setting up alarm: %s", alarm_name) super().__init__(alarm_name, info, controller) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( @@ -168,7 +168,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity): state = STATE_ALARM_DISARMED return state - async def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if code: self.hass.data[DATA_EVL].disarm_partition(str(code), self._partition_number) @@ -177,7 +177,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity): str(self._code), self._partition_number ) - async def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" if code: self.hass.data[DATA_EVL].arm_stay_partition( @@ -188,7 +188,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity): str(self._code), self._partition_number ) - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" if code: self.hass.data[DATA_EVL].arm_away_partition( @@ -199,11 +199,11 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity): str(self._code), self._partition_number ) - async def async_alarm_trigger(self, code=None): + async def async_alarm_trigger(self, code: str | None = None) -> None: """Alarm trigger command. Will be used to trigger a panic alarm.""" self.hass.data[DATA_EVL].panic_alarm(self._panic_type) - async def async_alarm_arm_night(self, code=None): + async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" self.hass.data[DATA_EVL].arm_night_partition( str(code) if code else str(self._code), self._partition_number diff --git a/homeassistant/components/hive/alarm_control_panel.py b/homeassistant/components/hive/alarm_control_panel.py index f8f35e20ffa..48b59e351be 100644 --- a/homeassistant/components/hive/alarm_control_panel.py +++ b/homeassistant/components/hive/alarm_control_panel.py @@ -1,4 +1,6 @@ """Support for the Hive alarm.""" +from __future__ import annotations + from datetime import timedelta from homeassistant.components.alarm_control_panel import ( @@ -49,19 +51,19 @@ class HiveAlarmControlPanelEntity(HiveEntity, AlarmControlPanelEntity): | AlarmControlPanelEntityFeature.ARM_AWAY ) - async def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" await self.hive.alarm.setMode(self.device, "home") - async def async_alarm_arm_night(self, code=None): + async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" await self.hive.alarm.setMode(self.device, "asleep") - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" await self.hive.alarm.setMode(self.device, "away") - async def async_update(self): + async def async_update(self) -> None: """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) self.device = await self.hive.alarm.getAlarm(self.device) diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 86e187410b3..3b6ae684d07 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -83,15 +83,15 @@ class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity): def _security_and_alarm(self) -> SecurityAndAlarmHome: return self._home.get_functionalHome(SecurityAndAlarmHome) - async def async_alarm_disarm(self, code=None) -> None: + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" await self._home.set_security_zones_activation(False, False) - async def async_alarm_arm_home(self, code=None) -> None: + async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" await self._home.set_security_zones_activation(False, True) - async def async_alarm_arm_away(self, code=None) -> None: + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" await self._home.set_security_zones_activation(True, True) diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py index be53eb99525..74310d940e7 100644 --- a/homeassistant/components/ialarm/alarm_control_panel.py +++ b/homeassistant/components/ialarm/alarm_control_panel.py @@ -1,4 +1,6 @@ """Interfaces with iAlarm control panels.""" +from __future__ import annotations + from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntity, AlarmControlPanelEntityFeature, @@ -52,14 +54,14 @@ class IAlarmPanel(CoordinatorEntity, AlarmControlPanelEntity): """Return the state of the device.""" return self.coordinator.state - def alarm_disarm(self, code=None): + def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" self.coordinator.ialarm.disarm() - def alarm_arm_home(self, code=None): + def alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" self.coordinator.ialarm.arm_stay() - def alarm_arm_away(self, code=None): + def alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" self.coordinator.ialarm.arm_away() diff --git a/homeassistant/components/ifttt/alarm_control_panel.py b/homeassistant/components/ifttt/alarm_control_panel.py index ebab2592403..840dd2fed62 100644 --- a/homeassistant/components/ifttt/alarm_control_panel.py +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -179,25 +179,25 @@ class IFTTTAlarmPanel(AlarmControlPanelEntity): return CodeFormat.NUMBER return CodeFormat.TEXT - def alarm_disarm(self, code=None): + def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if not self._check_code(code): return self.set_alarm_state(self._event_disarm, STATE_ALARM_DISARMED) - def alarm_arm_away(self, code=None): + def alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" if self._code_arm_required and not self._check_code(code): return self.set_alarm_state(self._event_away, STATE_ALARM_ARMED_AWAY) - def alarm_arm_home(self, code=None): + def alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" if self._code_arm_required and not self._check_code(code): return self.set_alarm_state(self._event_home, STATE_ALARM_ARMED_HOME) - def alarm_arm_night(self, code=None): + def alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" if self._code_arm_required and not self._check_code(code): return diff --git a/homeassistant/components/lupusec/alarm_control_panel.py b/homeassistant/components/lupusec/alarm_control_panel.py index 812225ea407..425b813d18a 100644 --- a/homeassistant/components/lupusec/alarm_control_panel.py +++ b/homeassistant/components/lupusec/alarm_control_panel.py @@ -69,14 +69,14 @@ class LupusecAlarm(LupusecDevice, AlarmControlPanelEntity): state = None return state - def alarm_arm_away(self, code=None): + def alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" self._device.set_away() - def alarm_disarm(self, code=None): + def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" self._device.set_standby() - def alarm_arm_home(self, code=None): + def alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" self._device.set_home() diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py index dd347336d9e..cfa81a816c3 100644 --- a/homeassistant/components/manual/alarm_control_panel.py +++ b/homeassistant/components/manual/alarm_control_panel.py @@ -302,7 +302,7 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): """Whether the code is required for arm actions.""" return self._code_arm_required - def alarm_disarm(self, code=None): + def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if not self._validate_code(code, STATE_ALARM_DISARMED): return @@ -311,7 +311,7 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): self._state_ts = dt_util.utcnow() self.schedule_update_ha_state() - def alarm_arm_home(self, code=None): + def alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" if self._code_arm_required and not self._validate_code( code, STATE_ALARM_ARMED_HOME @@ -320,7 +320,7 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): self._update_state(STATE_ALARM_ARMED_HOME) - def alarm_arm_away(self, code=None): + def alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" if self._code_arm_required and not self._validate_code( code, STATE_ALARM_ARMED_AWAY @@ -329,7 +329,7 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): self._update_state(STATE_ALARM_ARMED_AWAY) - def alarm_arm_night(self, code=None): + def alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" if self._code_arm_required and not self._validate_code( code, STATE_ALARM_ARMED_NIGHT @@ -338,7 +338,7 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): self._update_state(STATE_ALARM_ARMED_NIGHT) - def alarm_arm_vacation(self, code=None): + def alarm_arm_vacation(self, code: str | None = None) -> None: """Send arm vacation command.""" if self._code_arm_required and not self._validate_code( code, STATE_ALARM_ARMED_VACATION @@ -347,7 +347,7 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): self._update_state(STATE_ALARM_ARMED_VACATION) - def alarm_arm_custom_bypass(self, code=None): + def alarm_arm_custom_bypass(self, code: str | None = None) -> None: """Send arm custom bypass command.""" if self._code_arm_required and not self._validate_code( code, STATE_ALARM_ARMED_CUSTOM_BYPASS @@ -356,7 +356,7 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): self._update_state(STATE_ALARM_ARMED_CUSTOM_BYPASS) - def alarm_trigger(self, code=None): + def alarm_trigger(self, code: str | None = None) -> None: """ Send alarm trigger command. @@ -428,7 +428,7 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): """Update state at a scheduled point in time.""" self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" await super().async_added_to_hass() if state := await self.async_get_last_state(): diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index 67675a44e22..719e85bf16c 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -327,7 +327,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): """Whether the code is required for arm actions.""" return self._code_arm_required - def alarm_disarm(self, code=None): + def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if not self._validate_code(code, STATE_ALARM_DISARMED): return @@ -336,7 +336,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): self._state_ts = dt_util.utcnow() self.schedule_update_ha_state() - def alarm_arm_home(self, code=None): + def alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" if self._code_arm_required and not self._validate_code( code, STATE_ALARM_ARMED_HOME @@ -345,7 +345,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): self._update_state(STATE_ALARM_ARMED_HOME) - def alarm_arm_away(self, code=None): + def alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" if self._code_arm_required and not self._validate_code( code, STATE_ALARM_ARMED_AWAY @@ -354,7 +354,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): self._update_state(STATE_ALARM_ARMED_AWAY) - def alarm_arm_night(self, code=None): + def alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" if self._code_arm_required and not self._validate_code( code, STATE_ALARM_ARMED_NIGHT @@ -363,7 +363,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): self._update_state(STATE_ALARM_ARMED_NIGHT) - def alarm_trigger(self, code=None): + def alarm_trigger(self, code: str | None = None) -> None: """ Send alarm trigger command. @@ -426,7 +426,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): ATTR_POST_PENDING_STATE: self._state, } - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to MQTT events.""" async_track_state_change_event( self.hass, [self.entity_id], self._async_state_changed_listener diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 6bb7d9cd0d1..bd2495fd5d1 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -264,7 +264,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): code_required = self._config.get(CONF_CODE_ARM_REQUIRED) return code_required - async def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command. This method is a coroutine. @@ -275,7 +275,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): payload = self._config[CONF_PAYLOAD_DISARM] await self._publish(code, payload) - async def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command. This method is a coroutine. @@ -286,7 +286,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): action = self._config[CONF_PAYLOAD_ARM_HOME] await self._publish(code, action) - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command. This method is a coroutine. @@ -297,7 +297,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): action = self._config[CONF_PAYLOAD_ARM_AWAY] await self._publish(code, action) - async def async_alarm_arm_night(self, code=None): + async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command. This method is a coroutine. @@ -308,7 +308,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): action = self._config[CONF_PAYLOAD_ARM_NIGHT] await self._publish(code, action) - async def async_alarm_arm_vacation(self, code=None): + async def async_alarm_arm_vacation(self, code: str | None = None) -> None: """Send arm vacation command. This method is a coroutine. @@ -319,7 +319,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): action = self._config[CONF_PAYLOAD_ARM_VACATION] await self._publish(code, action) - async def async_alarm_arm_custom_bypass(self, code=None): + async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None: """Send arm custom bypass command. This method is a coroutine. @@ -330,7 +330,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): action = self._config[CONF_PAYLOAD_ARM_CUSTOM_BYPASS] await self._publish(code, action) - async def async_alarm_trigger(self, code=None): + async def async_alarm_trigger(self, code: str | None = None) -> None: """Send trigger command. This method is a coroutine. diff --git a/homeassistant/components/ness_alarm/alarm_control_panel.py b/homeassistant/components/ness_alarm/alarm_control_panel.py index 9fccee6f64f..0e80ac57e01 100644 --- a/homeassistant/components/ness_alarm/alarm_control_panel.py +++ b/homeassistant/components/ness_alarm/alarm_control_panel.py @@ -53,7 +53,7 @@ class NessAlarmPanel(alarm.AlarmControlPanelEntity): self._name = name self._state = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( @@ -81,19 +81,19 @@ class NessAlarmPanel(alarm.AlarmControlPanelEntity): """Return the state of the device.""" return self._state - async def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" await self._client.disarm(code) - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" await self._client.arm_away(code) - async def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" await self._client.arm_home(code) - async def async_alarm_trigger(self, code=None): + async def async_alarm_trigger(self, code: str | None = None) -> None: """Send trigger/panic command.""" await self._client.panic(code) diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index 2ef664cb6d4..735c8104ef5 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -119,7 +119,7 @@ class NX584Alarm(alarm.AlarmControlPanelEntity): """Return the state of the device.""" return self._state - def update(self): + def update(self) -> None: """Process new events from panel.""" try: part = self._alarm.list_partitions()[0] @@ -157,15 +157,15 @@ class NX584Alarm(alarm.AlarmControlPanelEntity): if flag == "Siren on": self._state = STATE_ALARM_TRIGGERED - def alarm_disarm(self, code=None): + def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" self._alarm.disarm(code) - def alarm_arm_home(self, code=None): + def alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" self._alarm.arm("stay") - def alarm_arm_away(self, code=None): + def alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" self._alarm.arm("exit") diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index 46ea95ba927..bd3deb6e2c9 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -1,4 +1,6 @@ """Support for Minut Point.""" +from __future__ import annotations + import logging from homeassistant.components.alarm_control_panel import ( @@ -58,14 +60,14 @@ class MinutPointAlarmControl(AlarmControlPanelEntity): self._async_unsub_hook_dispatcher_connect = None self._changed_by = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to HOme Assistant.""" await super().async_added_to_hass() self._async_unsub_hook_dispatcher_connect = async_dispatcher_connect( self.hass, SIGNAL_WEBHOOK, self._webhook_event ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Disconnect dispatcher listener when removed.""" await super().async_will_remove_from_hass() if self._async_unsub_hook_dispatcher_connect: @@ -106,13 +108,13 @@ class MinutPointAlarmControl(AlarmControlPanelEntity): """Return the user the last change was triggered by.""" return self._changed_by - async def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" status = await self._client.async_alarm_disarm(self._home_id) if status: self._home["alarm_status"] = "off" - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" status = await self._client.async_alarm_arm(self._home_id) if status: diff --git a/homeassistant/components/risco/alarm_control_panel.py b/homeassistant/components/risco/alarm_control_panel.py index f3578151acc..f814be0f2bd 100644 --- a/homeassistant/components/risco/alarm_control_panel.py +++ b/homeassistant/components/risco/alarm_control_panel.py @@ -1,4 +1,6 @@ """Support for Risco alarms.""" +from __future__ import annotations + import logging from homeassistant.components.alarm_control_panel import ( @@ -138,26 +140,26 @@ class RiscoAlarm(AlarmControlPanelEntity, RiscoEntity): """Validate given code.""" return code == self._code - async def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if self._code_disarm_required and not self._validate_code(code): _LOGGER.warning("Wrong code entered for disarming") return await self._call_alarm_method("disarm") - async def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" await self._arm(STATE_ALARM_ARMED_HOME, code) - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" await self._arm(STATE_ALARM_ARMED_AWAY, code) - async def async_alarm_arm_night(self, code=None): + async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" await self._arm(STATE_ALARM_ARMED_NIGHT, code) - async def async_alarm_arm_custom_bypass(self, code=None): + async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None: """Send arm custom bypass command.""" await self._arm(STATE_ALARM_ARMED_CUSTOM_BYPASS, code) diff --git a/homeassistant/components/satel_integra/alarm_control_panel.py b/homeassistant/components/satel_integra/alarm_control_panel.py index 4c036b4be85..054909cd3c2 100644 --- a/homeassistant/components/satel_integra/alarm_control_panel.py +++ b/homeassistant/components/satel_integra/alarm_control_panel.py @@ -74,7 +74,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanelEntity): self._partition_id = partition_id self._satel = controller - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Update alarm status and register callbacks for future updates.""" _LOGGER.debug("Starts listening for panel messages") self._update_alarm_status() @@ -149,7 +149,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanelEntity): """Return the state of the device.""" return self._state - async def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if not code: _LOGGER.debug("Code was empty or None") @@ -166,14 +166,14 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanelEntity): await asyncio.sleep(1) await self._satel.clear_alarm(code, [self._partition_id]) - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" _LOGGER.debug("Arming away") if code: await self._satel.arm(code, [self._partition_id]) - async def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" _LOGGER.debug("Arming home") diff --git a/homeassistant/components/spc/alarm_control_panel.py b/homeassistant/components/spc/alarm_control_panel.py index d519e6b7f2b..a1b4e2b2392 100644 --- a/homeassistant/components/spc/alarm_control_panel.py +++ b/homeassistant/components/spc/alarm_control_panel.py @@ -62,7 +62,7 @@ class SpcAlarm(alarm.AlarmControlPanelEntity): self._area = area self._api = api - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call for adding new entities.""" self.async_on_remove( async_dispatcher_connect( @@ -97,22 +97,22 @@ class SpcAlarm(alarm.AlarmControlPanelEntity): """Return the state of the device.""" return _get_alarm_state(self._area) - async def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" await self._api.change_mode(area=self._area, new_mode=AreaMode.UNSET) - async def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" await self._api.change_mode(area=self._area, new_mode=AreaMode.PART_SET_A) - async def async_alarm_arm_night(self, code=None): + async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm home command.""" await self._api.change_mode(area=self._area, new_mode=AreaMode.PART_SET_B) - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" await self._api.change_mode(area=self._area, new_mode=AreaMode.FULL_SET) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 96cc5a8330e..74d794d703a 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -216,7 +216,7 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): ) self._state = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" if self._template: self.add_template_attribute( @@ -239,25 +239,25 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): if optimistic_set: self.async_write_ha_state() - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Arm the panel to Away.""" await self._async_alarm_arm( STATE_ALARM_ARMED_AWAY, script=self._arm_away_script, code=code ) - async def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code: str | None = None) -> None: """Arm the panel to Home.""" await self._async_alarm_arm( STATE_ALARM_ARMED_HOME, script=self._arm_home_script, code=code ) - async def async_alarm_arm_night(self, code=None): + async def async_alarm_arm_night(self, code: str | None = None) -> None: """Arm the panel to Night.""" await self._async_alarm_arm( STATE_ALARM_ARMED_NIGHT, script=self._arm_night_script, code=code ) - async def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code: str | None = None) -> None: """Disarm the panel.""" await self._async_alarm_arm( STATE_ALARM_DISARMED, script=self._disarm_script, code=code diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index bf7a1ae410b..6ed29dbcad3 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -1,4 +1,6 @@ """Interfaces with TotalConnect alarm control panels.""" +from __future__ import annotations + from total_connect_client import ArmingHelper from total_connect_client.exceptions import BadResultCodeError, UsercodeInvalid @@ -163,7 +165,7 @@ class TotalConnectAlarm(CoordinatorEntity, alarm.AlarmControlPanelEntity): """Return the state attributes of the device.""" return self._extra_state_attributes - async def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" try: await self.hass.async_add_executor_job(self._disarm) @@ -182,7 +184,7 @@ class TotalConnectAlarm(CoordinatorEntity, alarm.AlarmControlPanelEntity): """Disarm synchronous.""" ArmingHelper(self._partition).disarm() - async def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" try: await self.hass.async_add_executor_job(self._arm_home) @@ -201,7 +203,7 @@ class TotalConnectAlarm(CoordinatorEntity, alarm.AlarmControlPanelEntity): """Arm home synchronous.""" ArmingHelper(self._partition).arm_stay() - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" try: await self.hass.async_add_executor_job(self._arm_away) @@ -220,7 +222,7 @@ class TotalConnectAlarm(CoordinatorEntity, alarm.AlarmControlPanelEntity): """Arm away synchronous.""" ArmingHelper(self._partition).arm_away() - async def async_alarm_arm_night(self, code=None): + async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" try: await self.hass.async_add_executor_job(self._arm_night) @@ -239,7 +241,7 @@ class TotalConnectAlarm(CoordinatorEntity, alarm.AlarmControlPanelEntity): """Arm night synchronous.""" ArmingHelper(self._partition).arm_stay_night() - async def async_alarm_arm_home_instant(self, code=None): + async def async_alarm_arm_home_instant(self, code: str | None = None) -> None: """Send arm home instant command.""" try: await self.hass.async_add_executor_job(self._arm_home_instant) @@ -258,7 +260,7 @@ class TotalConnectAlarm(CoordinatorEntity, alarm.AlarmControlPanelEntity): """Arm home instant synchronous.""" ArmingHelper(self._partition).arm_stay_instant() - async def async_alarm_arm_away_instant(self, code=None): + async def async_alarm_arm_away_instant(self, code: str | None = None) -> None: """Send arm away instant command.""" try: await self.hass.async_add_executor_job(self._arm_away_instant) diff --git a/homeassistant/components/xiaomi_miio/alarm_control_panel.py b/homeassistant/components/xiaomi_miio/alarm_control_panel.py index 25c995b2b24..be7daf5e077 100644 --- a/homeassistant/components/xiaomi_miio/alarm_control_panel.py +++ b/homeassistant/components/xiaomi_miio/alarm_control_panel.py @@ -1,4 +1,6 @@ """Support for Xiomi Gateway alarm control panels.""" +from __future__ import annotations + from functools import partial import logging @@ -110,19 +112,19 @@ class XiaomiGatewayAlarm(AlarmControlPanelEntity): except DeviceException as exc: _LOGGER.error(mask_error, exc) - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Turn on.""" await self._try_command( "Turning the alarm on failed: %s", self._gateway.alarm.on ) - async def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code: str | None = None) -> None: """Turn off.""" await self._try_command( "Turning the alarm off failed: %s", self._gateway.alarm.off ) - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" try: state = await self.hass.async_add_executor_job(self._gateway.alarm.status) From f66acf293f080017a784c98523e59c3a5f10d190 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 10:02:16 +0200 Subject: [PATCH 1838/3516] Adjust type hints in prosegur alarm (#74093) * Adjust type hints in prosegur alarm * Adjust hint --- .../components/prosegur/alarm_control_panel.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/prosegur/alarm_control_panel.py b/homeassistant/components/prosegur/alarm_control_panel.py index e7176b241e5..133c182e2cc 100644 --- a/homeassistant/components/prosegur/alarm_control_panel.py +++ b/homeassistant/components/prosegur/alarm_control_panel.py @@ -1,4 +1,6 @@ """Support for Prosegur alarm control panels.""" +from __future__ import annotations + import logging from pyprosegur.auth import Auth @@ -44,12 +46,12 @@ class ProsegurAlarm(alarm.AlarmControlPanelEntity): AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_HOME ) + _installation: Installation def __init__(self, contract: str, auth: Auth) -> None: """Initialize the Prosegur alarm panel.""" self._changed_by = None - self._installation = None self.contract = contract self._auth = auth @@ -57,7 +59,7 @@ class ProsegurAlarm(alarm.AlarmControlPanelEntity): self._attr_name = f"contract {self.contract}" self._attr_unique_id = self.contract - async def async_update(self): + async def async_update(self) -> None: """Update alarm status.""" try: @@ -70,14 +72,14 @@ class ProsegurAlarm(alarm.AlarmControlPanelEntity): self._attr_state = STATE_MAPPING.get(self._installation.status) self._attr_available = True - async def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" await self._installation.disarm(self._auth) - async def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm away command.""" await self._installation.arm_partially(self._auth) - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" await self._installation.arm(self._auth) From 319ef38d346ed5ce87b7f8b11054a8c484597e7f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 10:02:57 +0200 Subject: [PATCH 1839/3516] Add AlarmControlPanelEntity to pylint checks (#74091) --- pylint/plugins/hass_enforce_type_hints.py | 87 +++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 711f40b26ef..5c1a35c757a 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -572,6 +572,93 @@ _TOGGLE_ENTITY_MATCH: list[TypeHintMatch] = [ ), ] _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { + "alarm_control_panel": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="AlarmControlPanelEntity", + matches=[ + TypeHintMatch( + function_name="device_class", + return_type=["str", None], + ), + TypeHintMatch( + function_name="code_format", + return_type=["CodeFormat", None], + ), + TypeHintMatch( + function_name="changed_by", + return_type=["str", None], + ), + TypeHintMatch( + function_name="code_arm_required", + return_type="bool", + ), + TypeHintMatch( + function_name="supported_features", + return_type="int", + ), + TypeHintMatch( + function_name="alarm_disarm", + named_arg_types={ + "code": "str | None", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="alarm_arm_home", + named_arg_types={ + "code": "str | None", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="alarm_arm_away", + named_arg_types={ + "code": "str | None", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="alarm_arm_night", + named_arg_types={ + "code": "str | None", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="alarm_arm_vacation", + named_arg_types={ + "code": "str | None", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="alarm_trigger", + named_arg_types={ + "code": "str | None", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="alarm_arm_custom_bypass", + named_arg_types={ + "code": "str | None", + }, + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ], "cover": [ ClassTypeHintMatch( base_class="Entity", From 35df012b6e59f5cb36309ae5485d952c0d9ba01c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 10:06:05 +0200 Subject: [PATCH 1840/3516] Fix reauth step in nest (#74090) --- homeassistant/components/nest/config_flow.py | 9 ++------- tests/components/nest/test_config_flow_sdm.py | 12 ------------ 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index 479a54edbc7..2e89f7970fa 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -11,7 +11,7 @@ from __future__ import annotations import asyncio from collections import OrderedDict -from collections.abc import Iterable +from collections.abc import Iterable, Mapping from enum import Enum import logging import os @@ -218,14 +218,9 @@ class NestFlowHandler( return await self.async_step_finish() return await self.async_step_pubsub() - async def async_step_reauth( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" - if user_input is None: - _LOGGER.error("Reauth invoked with empty config entry data") - return self.async_abort(reason="missing_configuration") self._data.update(user_input) return await self.async_step_reauth_confirm() diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow_sdm.py index 53a2d9cf2b6..b2ef95b138e 100644 --- a/tests/components/nest/test_config_flow_sdm.py +++ b/tests/components/nest/test_config_flow_sdm.py @@ -505,18 +505,6 @@ async def test_reauth_multiple_config_entries( assert entry.data.get("extra_data") -async def test_reauth_missing_config_entry(hass, setup_platform): - """Test the reauth flow invoked missing existing data.""" - await setup_platform() - - # Invoke the reauth flow with no existing data - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=None - ) - assert result["type"] == "abort" - assert result["reason"] == "missing_configuration" - - @pytest.mark.parametrize( "nest_test_config,auth_implementation", [(TEST_CONFIG_HYBRID, APP_AUTH_DOMAIN)] ) From c19a8ef8e00f4a572042c51bbab11610b0120ea5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 10:08:36 +0200 Subject: [PATCH 1841/3516] Enforce flow-handler result type hint for step_* (#72834) --- pylint/plugins/hass_enforce_type_hints.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 5c1a35c757a..e1517ede1ff 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -37,6 +37,8 @@ class TypeHintMatch: self.function_name == node.name or self.has_async_counterpart and node.name == f"async_{self.function_name}" + or self.function_name.endswith("*") + and node.name.startswith(self.function_name[:-1]) ) @@ -370,6 +372,16 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { "config_flow": [ + ClassTypeHintMatch( + base_class="FlowHandler", + matches=[ + TypeHintMatch( + function_name="async_step_*", + arg_types={}, + return_type="FlowResult", + ), + ], + ), ClassTypeHintMatch( base_class="ConfigFlow", matches=[ From 9a613aeb96c9887e5394345775816fab184b4cfc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 28 Jun 2022 10:22:46 +0200 Subject: [PATCH 1842/3516] Modify behavior of media_player groups (#74056) --- .../components/group/media_player.py | 30 ++++++++++----- tests/components/group/test_media_player.py | 38 ++++++++++--------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index 46c019dbc7c..e0cbf84a693 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -103,6 +103,8 @@ async def async_setup_entry( class MediaPlayerGroup(MediaPlayerEntity): """Representation of a Media Group.""" + _attr_available: bool = False + def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None: """Initialize a Media Group entity.""" self._name = name @@ -390,19 +392,29 @@ class MediaPlayerGroup(MediaPlayerEntity): @callback def async_update_state(self) -> None: """Query all members and determine the media group state.""" - states = [self.hass.states.get(entity) for entity in self._entities] - states_values = [state.state for state in states if state is not None] - off_values = STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN + states = [ + state.state + for entity_id in self._entities + if (state := self.hass.states.get(entity_id)) is not None + ] - if states_values: - if states_values.count(states_values[0]) == len(states_values): - self._state = states_values[0] - elif any(state for state in states_values if state not in off_values): + # Set group as unavailable if all members are unavailable or missing + self._attr_available = any(state != STATE_UNAVAILABLE for state in states) + + valid_state = any( + state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states + ) + if not valid_state: + # Set as unknown if all members are unknown or unavailable + self._state = None + else: + off_values = (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN) + if states.count(states[0]) == len(states): + self._state = states[0] + elif any(state for state in states if state not in off_values): self._state = STATE_ON else: self._state = STATE_OFF - else: - self._state = None supported_features = 0 if self._features[KEY_CLEAR_PLAYLIST]: diff --git a/tests/components/group/test_media_player.py b/tests/components/group/test_media_player.py index 85e75ffcba6..5d0d52ae3ac 100644 --- a/tests/components/group/test_media_player.py +++ b/tests/components/group/test_media_player.py @@ -126,8 +126,24 @@ async def test_state_reporting(hass): await hass.async_start() await hass.async_block_till_done() - # Initial state with no group member in the state machine -> unknown - assert hass.states.get("media_player.media_group").state == STATE_UNKNOWN + # Initial state with no group member in the state machine -> unavailable + assert hass.states.get("media_player.media_group").state == STATE_UNAVAILABLE + + # All group members unavailable -> unavailable + hass.states.async_set("media_player.player_1", STATE_UNAVAILABLE) + hass.states.async_set("media_player.player_2", STATE_UNAVAILABLE) + await hass.async_block_till_done() + assert hass.states.get("media_player.media_group").state == STATE_UNAVAILABLE + + # The group state is unknown if all group members are unknown or unavailable. + for state_1 in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + hass.states.async_set("media_player.player_1", state_1) + hass.states.async_set("media_player.player_2", STATE_UNKNOWN) + await hass.async_block_till_done() + assert hass.states.get("media_player.media_group").state == STATE_UNKNOWN # All group members buffering -> buffering # All group members idle -> idle @@ -156,30 +172,18 @@ async def test_state_reporting(hass): await hass.async_block_till_done() assert hass.states.get("media_player.media_group").state == STATE_ON + # Otherwise off for state_1 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): hass.states.async_set("media_player.player_1", state_1) hass.states.async_set("media_player.player_2", STATE_OFF) await hass.async_block_till_done() assert hass.states.get("media_player.media_group").state == STATE_OFF - # Otherwise off - for state_1 in (STATE_OFF, STATE_UNKNOWN): - hass.states.async_set("media_player.player_1", state_1) - hass.states.async_set("media_player.player_2", STATE_UNAVAILABLE) - await hass.async_block_till_done() - assert hass.states.get("media_player.media_group").state == STATE_OFF - - for state_1 in (STATE_OFF, STATE_UNAVAILABLE): - hass.states.async_set("media_player.player_1", state_1) - hass.states.async_set("media_player.player_2", STATE_UNKNOWN) - await hass.async_block_till_done() - assert hass.states.get("media_player.media_group").state == STATE_OFF - - # All group members removed from the state machine -> unknown + # All group members removed from the state machine -> unavailable hass.states.async_remove("media_player.player_1") hass.states.async_remove("media_player.player_2") await hass.async_block_till_done() - assert hass.states.get("media_player.media_group").state == STATE_UNKNOWN + assert hass.states.get("media_player.media_group").state == STATE_UNAVAILABLE async def test_supported_features(hass): From 8328f9b623753733b8309c09a95580acf375dddf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 10:26:36 +0200 Subject: [PATCH 1843/3516] Cleanup async_update in smartthings cover (#74040) --- homeassistant/components/smartthings/cover.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/smartthings/cover.py b/homeassistant/components/smartthings/cover.py index 59f6e09df19..0ff3a82d788 100644 --- a/homeassistant/components/smartthings/cover.py +++ b/homeassistant/components/smartthings/cover.py @@ -107,20 +107,17 @@ class SmartThingsCover(SmartThingsEntity, CoverEntity): # Do not set_status=True as device will report progress. await self._device.set_level(kwargs[ATTR_POSITION], 0) - async def async_update(self): + async def async_update(self) -> None: """Update the attrs of the cover.""" - value = None if Capability.door_control in self._device.capabilities: self._device_class = CoverDeviceClass.DOOR - value = self._device.status.door + self._state = VALUE_TO_STATE.get(self._device.status.door) elif Capability.window_shade in self._device.capabilities: self._device_class = CoverDeviceClass.SHADE - value = self._device.status.window_shade + self._state = VALUE_TO_STATE.get(self._device.status.window_shade) elif Capability.garage_door_control in self._device.capabilities: self._device_class = CoverDeviceClass.GARAGE - value = self._device.status.door - - self._state = VALUE_TO_STATE.get(value) + self._state = VALUE_TO_STATE.get(self._device.status.door) self._state_attrs = {} battery = self._device.status.attributes[Attribute.battery].value From 824de2ef4cdcdd59e8827ebffdaf26c0ffb58904 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 28 Jun 2022 10:29:56 +0200 Subject: [PATCH 1844/3516] Modify behavior of lock groups (#74055) --- homeassistant/components/group/lock.py | 2 +- tests/components/group/test_lock.py | 28 +++++++++----------------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/group/lock.py b/homeassistant/components/group/lock.py index fe9503137c6..610e15f3ecc 100644 --- a/homeassistant/components/group/lock.py +++ b/homeassistant/components/group/lock.py @@ -166,7 +166,7 @@ class LockGroup(GroupEntity, LockEntity): if (state := self.hass.states.get(entity_id)) is not None ] - valid_state = all( + valid_state = any( state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states ) diff --git a/tests/components/group/test_lock.py b/tests/components/group/test_lock.py index e76e47577c6..4b12bcfbd7c 100644 --- a/tests/components/group/test_lock.py +++ b/tests/components/group/test_lock.py @@ -90,28 +90,10 @@ async def test_state_reporting(hass): await hass.async_block_till_done() assert hass.states.get("lock.lock_group").state == STATE_UNAVAILABLE - # At least one member unknown or unavailable -> group unknown + # The group state is unknown if all group members are unknown or unavailable. for state_1 in ( - STATE_JAMMED, - STATE_LOCKED, - STATE_LOCKING, - STATE_UNKNOWN, - STATE_UNLOCKED, - STATE_UNLOCKING, - ): - hass.states.async_set("lock.test1", state_1) - hass.states.async_set("lock.test2", STATE_UNAVAILABLE) - await hass.async_block_till_done() - assert hass.states.get("lock.lock_group").state == STATE_UNKNOWN - - for state_1 in ( - STATE_JAMMED, - STATE_LOCKED, - STATE_LOCKING, STATE_UNAVAILABLE, STATE_UNKNOWN, - STATE_UNLOCKED, - STATE_UNLOCKING, ): hass.states.async_set("lock.test1", state_1) hass.states.async_set("lock.test2", STATE_UNKNOWN) @@ -123,6 +105,8 @@ async def test_state_reporting(hass): STATE_JAMMED, STATE_LOCKED, STATE_LOCKING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, STATE_UNLOCKED, STATE_UNLOCKING, ): @@ -135,6 +119,8 @@ async def test_state_reporting(hass): for state_1 in ( STATE_LOCKED, STATE_LOCKING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, STATE_UNLOCKED, STATE_UNLOCKING, ): @@ -146,6 +132,8 @@ async def test_state_reporting(hass): # At least one member unlocking -> group unlocking for state_1 in ( STATE_LOCKED, + STATE_UNAVAILABLE, + STATE_UNKNOWN, STATE_UNLOCKED, STATE_UNLOCKING, ): @@ -157,6 +145,8 @@ async def test_state_reporting(hass): # At least one member unlocked -> group unlocked for state_1 in ( STATE_LOCKED, + STATE_UNAVAILABLE, + STATE_UNKNOWN, STATE_UNLOCKED, ): hass.states.async_set("lock.test1", state_1) From 6eeb1855ff531cd7c2aff5ad16e90624d38266da Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 28 Jun 2022 04:32:50 -0400 Subject: [PATCH 1845/3516] Remove entities from Alexa when disabling Alexa (#73999) Co-authored-by: Martin Hjelmare --- .../components/cloud/alexa_config.py | 69 +++++++++++++------ homeassistant/components/cloud/prefs.py | 4 ++ tests/components/cloud/test_alexa_config.py | 36 +++++++--- 3 files changed, 78 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index cf52b458a28..1e59c9a6512 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -1,5 +1,8 @@ """Alexa configuration for Home Assistant Cloud.""" +from __future__ import annotations + import asyncio +from collections.abc import Callable from contextlib import suppress from datetime import timedelta from http import HTTPStatus @@ -24,7 +27,15 @@ from homeassistant.helpers.event import async_call_later from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from .const import CONF_ENTITY_CONFIG, CONF_FILTER, PREF_SHOULD_EXPOSE +from .const import ( + CONF_ENTITY_CONFIG, + CONF_FILTER, + PREF_ALEXA_DEFAULT_EXPOSE, + PREF_ALEXA_ENTITY_CONFIGS, + PREF_ALEXA_REPORT_STATE, + PREF_ENABLE_ALEXA, + PREF_SHOULD_EXPOSE, +) from .prefs import CloudPreferences _LOGGER = logging.getLogger(__name__) @@ -54,8 +65,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): self._token = None self._token_valid = None self._cur_entity_prefs = prefs.alexa_entity_configs - self._cur_default_expose = prefs.alexa_default_expose - self._alexa_sync_unsub = None + self._alexa_sync_unsub: Callable[[], None] | None = None self._endpoint = None @property @@ -75,7 +85,11 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): @property def should_report_state(self): """Return if states should be proactively reported.""" - return self._prefs.alexa_report_state and self.authorized + return ( + self._prefs.alexa_enabled + and self._prefs.alexa_report_state + and self.authorized + ) @property def endpoint(self): @@ -179,7 +193,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): self._token_valid = utcnow() + timedelta(seconds=body["expires_in"]) return self._token - async def _async_prefs_updated(self, prefs): + async def _async_prefs_updated(self, prefs: CloudPreferences) -> None: """Handle updated preferences.""" if not self._cloud.is_logged_in: if self.is_reporting_states: @@ -190,6 +204,8 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): self._alexa_sync_unsub = None return + updated_prefs = prefs.last_updated + if ( ALEXA_DOMAIN not in self.hass.config.components and self.enabled @@ -211,28 +227,30 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): await self.async_sync_entities() return - # If user has filter in config.yaml, don't sync. - if not self._config[CONF_FILTER].empty_filter: - return - - # If entity prefs are the same, don't sync. - if ( - self._cur_entity_prefs is prefs.alexa_entity_configs - and self._cur_default_expose is prefs.alexa_default_expose + # Nothing to do if no Alexa related things have changed + if not any( + key in updated_prefs + for key in ( + PREF_ALEXA_DEFAULT_EXPOSE, + PREF_ALEXA_ENTITY_CONFIGS, + PREF_ALEXA_REPORT_STATE, + PREF_ENABLE_ALEXA, + ) ): return - if self._alexa_sync_unsub: - self._alexa_sync_unsub() - self._alexa_sync_unsub = None + # If we update just entity preferences, delay updating + # as we might update more + if updated_prefs == {PREF_ALEXA_ENTITY_CONFIGS}: + if self._alexa_sync_unsub: + self._alexa_sync_unsub() - if self._cur_default_expose is not prefs.alexa_default_expose: - await self.async_sync_entities() + self._alexa_sync_unsub = async_call_later( + self.hass, SYNC_DELAY, self._sync_prefs + ) return - self._alexa_sync_unsub = async_call_later( - self.hass, SYNC_DELAY, self._sync_prefs - ) + await self.async_sync_entities() async def _sync_prefs(self, _now): """Sync the updated preferences to Alexa.""" @@ -243,9 +261,14 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): seen = set() to_update = [] to_remove = [] + is_enabled = self.enabled for entity_id, info in old_prefs.items(): seen.add(entity_id) + + if not is_enabled: + to_remove.append(entity_id) + old_expose = info.get(PREF_SHOULD_EXPOSE) if entity_id in new_prefs: @@ -291,8 +314,10 @@ class CloudAlexaConfig(alexa_config.AbstractConfig): to_update = [] to_remove = [] + is_enabled = self.enabled + for entity in alexa_entities.async_get_entities(self.hass, self): - if self.should_expose(entity.entity_id): + if is_enabled and self.should_expose(entity.entity_id): to_update.append(entity.entity_id) else: to_remove.append(entity.entity_id) diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 275c2a56326..17ec00026bc 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -50,6 +50,7 @@ class CloudPreferences: self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) self._prefs = None self._listeners = [] + self.last_updated: set[str] = set() async def async_initialize(self): """Finish initializing the preferences.""" @@ -308,6 +309,9 @@ class CloudPreferences: async def _save_prefs(self, prefs): """Save preferences to disk.""" + self.last_updated = { + key for key, value in prefs.items() if value != self._prefs.get(key) + } self._prefs = prefs await self._store.async_save(self._prefs) diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 465ff7dd3d4..4e0df3c8ee3 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -9,7 +9,6 @@ from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import EntityCategory -from homeassistant.util.dt import utcnow from tests.common import async_fire_time_changed, mock_registry @@ -270,10 +269,7 @@ async def test_alexa_config_fail_refresh_token( @contextlib.contextmanager def patch_sync_helper(): - """Patch sync helper. - - In Py3.7 this would have been an async context manager. - """ + """Patch sync helper.""" to_update = [] to_remove = [] @@ -291,21 +287,32 @@ def patch_sync_helper(): async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs, cloud_stub): """Test Alexa config responds to updating exposed entities.""" + hass.states.async_set("binary_sensor.door", "on") + hass.states.async_set( + "sensor.temp", + "23", + {"device_class": "temperature", "unit_of_measurement": "°C"}, + ) + hass.states.async_set("light.kitchen", "off") + await cloud_prefs.async_update( + alexa_enabled=True, alexa_report_state=False, ) - await alexa_config.CloudAlexaConfig( + conf = alexa_config.CloudAlexaConfig( hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, cloud_stub - ).async_initialize() + ) + await conf.async_initialize() with patch_sync_helper() as (to_update, to_remove): await cloud_prefs.async_update_alexa_entity_config( entity_id="light.kitchen", should_expose=True ) await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow()) + async_fire_time_changed(hass, fire_all=True) await hass.async_block_till_done() + assert conf._alexa_sync_unsub is None assert to_update == ["light.kitchen"] assert to_remove == [] @@ -320,12 +327,23 @@ async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs, cloud_stub): entity_id="sensor.temp", should_expose=True ) await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow()) + async_fire_time_changed(hass, fire_all=True) await hass.async_block_till_done() + assert conf._alexa_sync_unsub is None assert sorted(to_update) == ["binary_sensor.door", "sensor.temp"] assert to_remove == ["light.kitchen"] + with patch_sync_helper() as (to_update, to_remove): + await cloud_prefs.async_update( + alexa_enabled=False, + ) + await hass.async_block_till_done() + + assert conf._alexa_sync_unsub is None + assert to_update == [] + assert to_remove == ["binary_sensor.door", "sensor.temp", "light.kitchen"] + async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): """Test Alexa config responds to entity registry.""" From 28c1a5c09f7499c7b6b75d28831edde82d3b5a97 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 10:52:41 +0200 Subject: [PATCH 1846/3516] Enforce config-flow type hints for reauth step (#72830) --- pylint/plugins/hass_enforce_type_hints.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index e1517ede1ff..f3c7a01a10f 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -421,6 +421,13 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { }, return_type="FlowResult", ), + TypeHintMatch( + function_name="async_step_reauth", + arg_types={ + 1: "Mapping[str, Any]", + }, + return_type="FlowResult", + ), TypeHintMatch( function_name="async_step_ssdp", arg_types={ From 7d709c074d6564376193e307af2eeb59984ba472 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 28 Jun 2022 10:58:52 +0200 Subject: [PATCH 1847/3516] Add support for unavailable and unknown to fan groups (#74054) --- homeassistant/components/group/fan.py | 31 ++++++++--- tests/components/group/test_fan.py | 79 ++++++++++++++------------- 2 files changed, 66 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/group/fan.py b/homeassistant/components/group/fan.py index 4badbe6df51..7d09c9573b5 100644 --- a/homeassistant/components/group/fan.py +++ b/homeassistant/components/group/fan.py @@ -32,6 +32,8 @@ from homeassistant.const import ( CONF_NAME, CONF_UNIQUE_ID, STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er @@ -98,6 +100,7 @@ async def async_setup_entry( class FanGroup(GroupEntity, FanEntity): """Representation of a FanGroup.""" + _attr_available: bool = False _attr_assumed_state: bool = True def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None: @@ -109,7 +112,7 @@ class FanGroup(GroupEntity, FanEntity): self._direction = None self._supported_features = 0 self._speed_count = 100 - self._is_on = False + self._is_on: bool | None = False self._attr_name = name self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entities} self._attr_unique_id = unique_id @@ -125,7 +128,7 @@ class FanGroup(GroupEntity, FanEntity): return self._speed_count @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if the entity is on.""" return self._is_on @@ -270,11 +273,25 @@ class FanGroup(GroupEntity, FanEntity): """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]) + states = [ + state + for entity_id in self._entities + if (state := self.hass.states.get(entity_id)) is not None + ] + self._attr_assumed_state |= not states_equal(states) + + # Set group as unavailable if all members are unavailable or missing + self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states) + + valid_state = any( + state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states ) - self._is_on = any(state.state == STATE_ON for state in on_states) - self._attr_assumed_state |= not states_equal(on_states) + if not valid_state: + # Set as unknown if all members are unknown or unavailable + self._is_on = None + else: + # Set as ON if any member is ON + self._is_on = any(state.state == STATE_ON for state in states) percentage_states = self._async_states_by_support_flag( FanEntityFeature.SET_SPEED @@ -306,5 +323,5 @@ class FanGroup(GroupEntity, FanEntity): 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 + state.attributes.get(ATTR_ASSUMED_STATE) for state in states ) diff --git a/tests/components/group/test_fan.py b/tests/components/group/test_fan.py index 8aefd12c93a..bb2cf311191 100644 --- a/tests/components/group/test_fan.py +++ b/tests/components/group/test_fan.py @@ -119,15 +119,45 @@ async def test_state(hass, setup_comp): Otherwise, the group state is off. """ state = hass.states.get(FAN_GROUP) - # No entity has a valid state -> group state off - assert state.state == STATE_OFF + # No entity has a valid state -> group state unavailable + assert state.state == STATE_UNAVAILABLE assert state.attributes[ATTR_FRIENDLY_NAME] == DEFAULT_NAME + assert ATTR_ENTITY_ID not in state.attributes + assert ATTR_ASSUMED_STATE not in state.attributes + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + # Test group members exposed as attribute + hass.states.async_set(CEILING_FAN_ENTITY_ID, STATE_UNKNOWN, {}) + await hass.async_block_till_done() + state = hass.states.get(FAN_GROUP) 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 + + # All group members unavailable -> unavailable + hass.states.async_set(CEILING_FAN_ENTITY_ID, STATE_UNAVAILABLE) + hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, STATE_UNAVAILABLE) + hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, STATE_UNAVAILABLE) + hass.states.async_set(PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_UNAVAILABLE) + await hass.async_block_till_done() + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_UNAVAILABLE + + # The group state is unknown if all group members are unknown or unavailable. + for state_1 in (STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_2 in (STATE_UNAVAILABLE, STATE_UNKNOWN): + for state_3 in (STATE_UNAVAILABLE, STATE_UNKNOWN): + print("meh") + hass.states.async_set(CEILING_FAN_ENTITY_ID, state_1, {}) + hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, state_2, {}) + hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, state_3, {}) + hass.states.async_set( + PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_UNKNOWN, {} + ) + await hass.async_block_till_done() + state = hass.states.get(FAN_GROUP) + assert state.state == STATE_UNKNOWN # The group state is off if all group members are off, unknown or unavailable. for state_1 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): @@ -141,32 +171,6 @@ async def test_state(hass, setup_comp): state = hass.states.get(FAN_GROUP) assert state.state == STATE_OFF - for state_1 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): - for state_2 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): - for state_3 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): - hass.states.async_set(CEILING_FAN_ENTITY_ID, state_1, {}) - hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, state_2, {}) - hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, state_3, {}) - hass.states.async_set( - PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_UNAVAILABLE, {} - ) - await hass.async_block_till_done() - state = hass.states.get(FAN_GROUP) - assert state.state == STATE_OFF - - for state_1 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): - for state_2 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): - for state_3 in (STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN): - hass.states.async_set(CEILING_FAN_ENTITY_ID, state_1, {}) - hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, state_2, {}) - hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, state_3, {}) - hass.states.async_set( - PERCENTAGE_LIMITED_FAN_ENTITY_ID, STATE_UNKNOWN, {} - ) - await hass.async_block_till_done() - state = hass.states.get(FAN_GROUP) - assert state.state == STATE_OFF - # At least one member on -> group on for state_1 in (STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN): for state_2 in (STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN): @@ -183,7 +187,7 @@ async def test_state(hass, setup_comp): 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 state.state == STATE_UNKNOWN assert ATTR_ASSUMED_STATE not in state.attributes assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 @@ -193,7 +197,7 @@ async def test_state(hass, setup_comp): hass.states.async_remove(PERCENTAGE_FULL_FAN_ENTITY_ID) await hass.async_block_till_done() state = hass.states.get(FAN_GROUP) - assert state.state == STATE_OFF + assert state.state == STATE_UNAVAILABLE assert ATTR_ASSUMED_STATE not in state.attributes assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 @@ -208,12 +212,9 @@ async def test_state(hass, setup_comp): 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.state == STATE_UNAVAILABLE assert state.attributes[ATTR_FRIENDLY_NAME] == DEFAULT_NAME - assert state.attributes[ATTR_ENTITY_ID] == [ - *FULL_FAN_ENTITY_IDS, - *LIMITED_FAN_ENTITY_IDS, - ] + assert ATTR_ENTITY_ID not in state.attributes 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, {}) @@ -223,6 +224,10 @@ async def test_attributes(hass, setup_comp): await hass.async_block_till_done() state = hass.states.get(FAN_GROUP) assert state.state == STATE_ON + assert state.attributes[ATTR_ENTITY_ID] == [ + *FULL_FAN_ENTITY_IDS, + *LIMITED_FAN_ENTITY_IDS, + ] # Add Entity that supports speed hass.states.async_set( From f66fc65d0b349ea52e495976800154450b954bca Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 28 Jun 2022 11:01:14 +0200 Subject: [PATCH 1848/3516] Migrate environment_canada to native_* (#74048) --- .../components/environment_canada/weather.py | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index b79323b0462..40706ffb6c1 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -17,14 +17,19 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_CONDITION_WINDY, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import ( + LENGTH_KILOMETERS, + PRESSURE_KPA, + SPEED_KILOMETERS_PER_HOUR, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -63,6 +68,11 @@ async def async_setup_entry( class ECWeather(CoordinatorEntity, WeatherEntity): """Representation of a weather condition.""" + _attr_native_pressure_unit = PRESSURE_KPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_visibility_unit = LENGTH_KILOMETERS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__(self, coordinator, hourly): """Initialize Environment Canada weather.""" super().__init__(coordinator) @@ -78,7 +88,7 @@ class ECWeather(CoordinatorEntity, WeatherEntity): self._hourly = hourly @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" if ( temperature := self.ec_data.conditions.get("temperature", {}).get("value") @@ -92,11 +102,6 @@ class ECWeather(CoordinatorEntity, WeatherEntity): return float(temperature) return None - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def humidity(self): """Return the humidity.""" @@ -105,7 +110,7 @@ class ECWeather(CoordinatorEntity, WeatherEntity): return None @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" if self.ec_data.conditions.get("wind_speed", {}).get("value"): return float(self.ec_data.conditions["wind_speed"]["value"]) @@ -119,14 +124,14 @@ class ECWeather(CoordinatorEntity, WeatherEntity): return None @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" if self.ec_data.conditions.get("pressure", {}).get("value"): - return 10 * float(self.ec_data.conditions["pressure"]["value"]) + return float(self.ec_data.conditions["pressure"]["value"]) return None @property - def visibility(self): + def native_visibility(self): """Return the visibility.""" if self.ec_data.conditions.get("visibility", {}).get("value"): return float(self.ec_data.conditions["visibility"]["value"]) @@ -175,16 +180,16 @@ def get_forecast(ec_data, hourly): if half_days[0]["temperature_class"] == "high": today.update( { - ATTR_FORECAST_TEMP: int(half_days[0]["temperature"]), - ATTR_FORECAST_TEMP_LOW: int(half_days[1]["temperature"]), + ATTR_FORECAST_NATIVE_TEMP: int(half_days[0]["temperature"]), + ATTR_FORECAST_NATIVE_TEMP_LOW: int(half_days[1]["temperature"]), } ) half_days = half_days[2:] else: today.update( { - ATTR_FORECAST_TEMP: None, - ATTR_FORECAST_TEMP_LOW: int(half_days[0]["temperature"]), + ATTR_FORECAST_NATIVE_TEMP: None, + ATTR_FORECAST_NATIVE_TEMP_LOW: int(half_days[0]["temperature"]), } ) half_days = half_days[1:] @@ -197,8 +202,8 @@ def get_forecast(ec_data, hourly): ATTR_FORECAST_TIME: ( dt.now() + datetime.timedelta(days=day) ).isoformat(), - ATTR_FORECAST_TEMP: int(half_days[high]["temperature"]), - ATTR_FORECAST_TEMP_LOW: int(half_days[low]["temperature"]), + ATTR_FORECAST_NATIVE_TEMP: int(half_days[high]["temperature"]), + ATTR_FORECAST_NATIVE_TEMP_LOW: int(half_days[low]["temperature"]), ATTR_FORECAST_CONDITION: icon_code_to_condition( int(half_days[high]["icon_code"]) ), @@ -213,7 +218,7 @@ def get_forecast(ec_data, hourly): forecast_array.append( { ATTR_FORECAST_TIME: hour["period"].isoformat(), - ATTR_FORECAST_TEMP: int(hour["temperature"]), + ATTR_FORECAST_NATIVE_TEMP: int(hour["temperature"]), ATTR_FORECAST_CONDITION: icon_code_to_condition( int(hour["icon_code"]) ), From 37e8f113d41c89e6287c8b2b51602a8f473a9221 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 28 Jun 2022 11:02:13 +0200 Subject: [PATCH 1849/3516] Migrate zamg to native_* (#74034) --- homeassistant/components/zamg/weather.py | 26 ++++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zamg/weather.py b/homeassistant/components/zamg/weather.py index 6a5d7ccdf81..2bf4a5b39f6 100644 --- a/homeassistant/components/zamg/weather.py +++ b/homeassistant/components/zamg/weather.py @@ -14,7 +14,14 @@ from homeassistant.components.weather import ( PLATFORM_SCHEMA, WeatherEntity, ) -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + LENGTH_MILLIMETERS, + SPEED_KILOMETERS_PER_HOUR, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -80,6 +87,12 @@ def setup_platform( class ZamgWeather(WeatherEntity): """Representation of a weather condition.""" + _attr_native_pressure_unit = ( + LENGTH_MILLIMETERS # API reports l/m², equivalent to mm + ) + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__(self, zamg_data, stationname=None): """Initialise the platform with a data instance and station name.""" self.zamg_data = zamg_data @@ -104,17 +117,12 @@ class ZamgWeather(WeatherEntity): return ATTRIBUTION @property - def temperature(self): + def native_temperature(self): """Return the platform temperature.""" return self.zamg_data.get_data(ATTR_WEATHER_TEMPERATURE) @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" return self.zamg_data.get_data(ATTR_WEATHER_PRESSURE) @@ -124,7 +132,7 @@ class ZamgWeather(WeatherEntity): return self.zamg_data.get_data(ATTR_WEATHER_HUMIDITY) @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" return self.zamg_data.get_data(ATTR_WEATHER_WIND_SPEED) From 734b99e6ac278d0404e40d172bb21d67330e9769 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 11:07:46 +0200 Subject: [PATCH 1850/3516] Improve type hints in zha alarm (#74094) * Improve type hints in zha alarm * Allow None code --- .../components/zha/alarm_control_panel.py | 27 +++++++------------ .../components/zha/core/channels/security.py | 2 +- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/zha/alarm_control_panel.py b/homeassistant/components/zha/alarm_control_panel.py index 15d27f95c5c..ee37a345e17 100644 --- a/homeassistant/components/zha/alarm_control_panel.py +++ b/homeassistant/components/zha/alarm_control_panel.py @@ -81,6 +81,7 @@ async def async_setup_entry( class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity): """Entity for ZHA alarm control devices.""" + _attr_code_format = CodeFormat.TEXT _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY @@ -103,7 +104,7 @@ class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity): cfg_entry, ZHA_ALARM_OPTIONS, CONF_ALARM_FAILED_TRIES, 3 ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() self.async_accept_signal( @@ -119,45 +120,35 @@ class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity): self.async_write_ha_state() @property - def code_format(self): - """Regex for code format or None if no code is required.""" - return CodeFormat.TEXT - - @property - def changed_by(self): - """Last change triggered by.""" - return None - - @property - def code_arm_required(self): + def code_arm_required(self) -> bool: """Whether the code is required for arm actions.""" return self._channel.code_required_arm_actions - async def async_alarm_disarm(self, code=None): + async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" self._channel.arm(IasAce.ArmMode.Disarm, code, 0) self.async_write_ha_state() - async def async_alarm_arm_home(self, code=None): + async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" self._channel.arm(IasAce.ArmMode.Arm_Day_Home_Only, code, 0) self.async_write_ha_state() - async def async_alarm_arm_away(self, code=None): + async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" self._channel.arm(IasAce.ArmMode.Arm_All_Zones, code, 0) self.async_write_ha_state() - async def async_alarm_arm_night(self, code=None): + async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" self._channel.arm(IasAce.ArmMode.Arm_Night_Sleep_Only, code, 0) self.async_write_ha_state() - async def async_alarm_trigger(self, code=None): + async def async_alarm_trigger(self, code: str | None = None) -> None: """Send alarm trigger command.""" self.async_write_ha_state() @property - def state(self): + def state(self) -> str | None: """Return the state of the entity.""" return IAS_ACE_STATE_MAP.get(self._channel.armed_state) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 41e65019415..9463a0351c1 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -92,7 +92,7 @@ class IasAce(ZigbeeChannel): ) self.command_map[command_id](*args) - def arm(self, arm_mode: int, code: str, zone_id: int): + def arm(self, arm_mode: int, code: str | None, zone_id: int) -> None: """Handle the IAS ACE arm command.""" mode = AceCluster.ArmMode(arm_mode) From 38759bb98bff4bce5a6e52c694496043a251b5d7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 11:08:31 +0200 Subject: [PATCH 1851/3516] Adjust tilt_position method in esphome cover (#74041) --- homeassistant/components/esphome/cover.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index ab8b7af2185..97ae22dcccc 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -125,8 +125,9 @@ class EsphomeCover(EsphomeEntity[CoverInfo, CoverState], CoverEntity): """Close the cover tilt.""" await self._client.cover_command(key=self._static_info.key, tilt=0.0) - async def async_set_cover_tilt_position(self, **kwargs: int) -> None: + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" + tilt_position: int = kwargs[ATTR_TILT_POSITION] await self._client.cover_command( - key=self._static_info.key, tilt=kwargs[ATTR_TILT_POSITION] / 100 + key=self._static_info.key, tilt=tilt_position / 100 ) From af71c250d559d00f10f9be5f42fea50b83e00535 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 11:10:31 +0200 Subject: [PATCH 1852/3516] Use attributes in concord232 alarm (#74097) --- .../concord232/alarm_control_panel.py | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index 4b46d1bf98c..de5d4495a85 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -72,6 +72,8 @@ def setup_platform( class Concord232Alarm(alarm.AlarmControlPanelEntity): """Representation of the Concord232-based alarm panel.""" + _attr_code_format = alarm.CodeFormat.NUMBER + _attr_state: str | None _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY @@ -80,29 +82,13 @@ class Concord232Alarm(alarm.AlarmControlPanelEntity): def __init__(self, url, name, code, mode): """Initialize the Concord232 alarm panel.""" - self._state = None - self._name = name + self._attr_name = name self._code = code self._mode = mode self._url = url self._alarm = concord232_client.Client(self._url) self._alarm.partitions = self._alarm.list_partitions() - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def code_format(self): - """Return the characters if code is defined.""" - return alarm.CodeFormat.NUMBER - - @property - def state(self): - """Return the state of the device.""" - return self._state - def update(self) -> None: """Update values from API.""" try: @@ -118,11 +104,11 @@ class Concord232Alarm(alarm.AlarmControlPanelEntity): return if part["arming_level"] == "Off": - self._state = STATE_ALARM_DISARMED + self._attr_state = STATE_ALARM_DISARMED elif "Home" in part["arming_level"]: - self._state = STATE_ALARM_ARMED_HOME + self._attr_state = STATE_ALARM_ARMED_HOME else: - self._state = STATE_ALARM_ARMED_AWAY + self._attr_state = STATE_ALARM_ARMED_AWAY def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" @@ -152,7 +138,7 @@ class Concord232Alarm(alarm.AlarmControlPanelEntity): if isinstance(self._code, str): alarm_code = self._code else: - alarm_code = self._code.render(from_state=self._state, to_state=state) + alarm_code = self._code.render(from_state=self._attr_state, to_state=state) check = not alarm_code or code == alarm_code if not check: _LOGGER.warning("Invalid code given for %s", state) From ae63cd8677c029bb368ab79ed688c479b55b6e5a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 28 Jun 2022 11:12:14 +0200 Subject: [PATCH 1853/3516] Add support for unavailable to cover groups (#74053) --- homeassistant/components/group/cover.py | 24 ++++++++--- tests/components/group/test_cover.py | 57 ++++++++++++++----------- 2 files changed, 49 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index c2cd3c6e9d2..a867c92d956 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -35,6 +35,8 @@ from homeassistant.const import ( STATE_CLOSING, STATE_OPEN, STATE_OPENING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er @@ -98,6 +100,7 @@ async def async_setup_entry( class CoverGroup(GroupEntity, CoverEntity): """Representation of a CoverGroup.""" + _attr_available: bool = False _attr_is_closed: bool | None = None _attr_is_opening: bool | None = False _attr_is_closing: bool | None = False @@ -267,29 +270,38 @@ class CoverGroup(GroupEntity, CoverEntity): """Update state and attributes.""" self._attr_assumed_state = False + states = [ + state.state + for entity_id in self._entities + if (state := self.hass.states.get(entity_id)) is not None + ] + + valid_state = any( + state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states + ) + + # Set group as unavailable if all members are unavailable or missing + self._attr_available = any(state != STATE_UNAVAILABLE for state in states) + 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: if not (state := self.hass.states.get(entity_id)): continue if state.state == STATE_OPEN: self._attr_is_closed = False - has_valid_state = True continue if state.state == STATE_CLOSED: - 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: + if not valid_state: + # Set as unknown if all members are unknown or unavailable self._attr_is_closed = None position_covers = self._covers[KEY_POSITION] diff --git a/tests/components/group/test_cover.py b/tests/components/group/test_cover.py index 83c85a70b63..57c54c7c502 100644 --- a/tests/components/group/test_cover.py +++ b/tests/components/group/test_cover.py @@ -109,32 +109,36 @@ async def test_state(hass, setup_comp): Otherwise, the group state is closed. """ state = hass.states.get(COVER_GROUP) - # No entity has a valid state -> group state unknown - assert state.state == STATE_UNKNOWN + # No entity has a valid state -> group state unavailable + assert state.state == STATE_UNAVAILABLE assert state.attributes[ATTR_FRIENDLY_NAME] == DEFAULT_NAME + assert ATTR_ENTITY_ID not in state.attributes + 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 + + # Test group members exposed as attribute + hass.states.async_set(DEMO_COVER, STATE_UNKNOWN, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) 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 + + # The group state is unavailable if all group members are unavailable. + hass.states.async_set(DEMO_COVER, STATE_UNAVAILABLE, {}) + hass.states.async_set(DEMO_COVER_POS, STATE_UNAVAILABLE, {}) + hass.states.async_set(DEMO_COVER_TILT, STATE_UNAVAILABLE, {}) + hass.states.async_set(DEMO_TILT, STATE_UNAVAILABLE, {}) + await hass.async_block_till_done() + state = hass.states.get(COVER_GROUP) + assert state.state == STATE_UNAVAILABLE # The group state is unknown if all group members are unknown or unavailable. - for state_1 in (STATE_UNAVAILABLE, STATE_UNKNOWN): - for state_2 in (STATE_UNAVAILABLE, STATE_UNKNOWN): - for state_3 in (STATE_UNAVAILABLE, STATE_UNKNOWN): - hass.states.async_set(DEMO_COVER, state_1, {}) - hass.states.async_set(DEMO_COVER_POS, state_2, {}) - hass.states.async_set(DEMO_COVER_TILT, state_3, {}) - hass.states.async_set(DEMO_TILT, STATE_UNAVAILABLE, {}) - await hass.async_block_till_done() - state = hass.states.get(COVER_GROUP) - assert state.state == STATE_UNKNOWN - for state_1 in (STATE_UNAVAILABLE, STATE_UNKNOWN): for state_2 in (STATE_UNAVAILABLE, STATE_UNKNOWN): for state_3 in (STATE_UNAVAILABLE, STATE_UNKNOWN): @@ -233,28 +237,23 @@ async def test_state(hass, setup_comp): state = hass.states.get(COVER_GROUP) assert state.state == STATE_CLOSED - # All group members removed from the state machine -> unknown + # All group members removed from the state machine -> unavailable hass.states.async_remove(DEMO_COVER) hass.states.async_remove(DEMO_COVER_POS) hass.states.async_remove(DEMO_COVER_TILT) hass.states.async_remove(DEMO_TILT) await hass.async_block_till_done() state = hass.states.get(COVER_GROUP) - assert state.state == STATE_UNKNOWN + assert state.state == STATE_UNAVAILABLE @pytest.mark.parametrize("config_count", [(CONFIG_ATTRIBUTES, 1)]) async def test_attributes(hass, setup_comp): """Test handling of state attributes.""" state = hass.states.get(COVER_GROUP) - assert state.state == STATE_UNKNOWN + assert state.state == STATE_UNAVAILABLE 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_ENTITY_ID not in state.attributes assert ATTR_ASSUMED_STATE not in state.attributes assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 assert ATTR_CURRENT_POSITION not in state.attributes @@ -266,6 +265,12 @@ async def test_attributes(hass, setup_comp): state = hass.states.get(COVER_GROUP) assert state.state == STATE_CLOSED + assert state.attributes[ATTR_ENTITY_ID] == [ + DEMO_COVER, + DEMO_COVER_POS, + DEMO_COVER_TILT, + DEMO_TILT, + ] # Set entity as opening hass.states.async_set(DEMO_COVER, STATE_OPENING, {}) From bc33818b20d145cba370247f5bb3b69d078cd9f3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 11:12:46 +0200 Subject: [PATCH 1854/3516] Use attributes in egardia alarm (#74098) --- .../components/egardia/alarm_control_panel.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/egardia/alarm_control_panel.py b/homeassistant/components/egardia/alarm_control_panel.py index f35d248c968..de179c248bb 100644 --- a/homeassistant/components/egardia/alarm_control_panel.py +++ b/homeassistant/components/egardia/alarm_control_panel.py @@ -63,6 +63,7 @@ def setup_platform( class EgardiaAlarm(alarm.AlarmControlPanelEntity): """Representation of a Egardia alarm.""" + _attr_state: str | None _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY @@ -72,9 +73,8 @@ class EgardiaAlarm(alarm.AlarmControlPanelEntity): self, name, egardiasystem, rs_enabled=False, rs_codes=None, rs_port=52010 ): """Initialize the Egardia alarm.""" - self._name = name + self._attr_name = name self._egardiasystem = egardiasystem - self._status = None self._rs_enabled = rs_enabled self._rs_codes = rs_codes self._rs_port = rs_port @@ -86,17 +86,7 @@ class EgardiaAlarm(alarm.AlarmControlPanelEntity): self.hass.data[EGARDIA_SERVER].register_callback(self.handle_status_event) @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._status - - @property - def should_poll(self): + def should_poll(self) -> bool: """Poll if no report server is enabled.""" if not self._rs_enabled: return True @@ -130,7 +120,7 @@ class EgardiaAlarm(alarm.AlarmControlPanelEntity): _LOGGER.debug("Not ignoring status %s", status) newstatus = STATES.get(status.upper()) _LOGGER.debug("newstatus %s", newstatus) - self._status = newstatus + self._attr_state = newstatus else: _LOGGER.error("Ignoring status") From 5787eb058de9f075403f87a91a5c72e450032f57 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 28 Jun 2022 11:14:06 +0200 Subject: [PATCH 1855/3516] Build opencv at core build pipeline (#73961) --- .github/workflows/wheels.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e65b0e7091a..95f1d8e437e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -50,6 +50,10 @@ jobs: # Fix out of memory issues with rust echo "CARGO_NET_GIT_FETCH_WITH_CLI=true" + + # OpenCV headless installation + echo "CI_BUILD=1" + echo "ENABLE_HEADLESS=1" ) > .env_file - name: Upload env_file @@ -138,14 +142,21 @@ jobs: sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file} sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} + sed -i "s|# opencv-python-headless|opencv-python-headless|g" ${requirement_file} done - - name: Adjust ENV + - name: Adjust build env run: | if [ "${{ matrix.arch }}" = "i386" ]; then echo "NPY_DISABLE_SVML=1" >> .env_file fi + ( + # cmake > 3.22.2 have issue on arm + # Tested until 3.22.5 + echo "cmake==3.22.2" + ) >> homeassistant/package_constraints.txt + - name: Build wheels uses: home-assistant/wheels@2022.06.7 with: @@ -154,7 +165,7 @@ jobs: arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true - apk: "libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran" + apk: "libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev" skip-binary: aiohttp;grpcio legacy: true constraints: "homeassistant/package_constraints.txt" From 45cdfa10491f46c88180d62edc4607d3547cf618 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 12:53:50 +0200 Subject: [PATCH 1856/3516] Use attributes in point alarm (#74111) --- .../components/point/alarm_control_panel.py | 49 ++++++------------- 1 file changed, 15 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index bd3deb6e2c9..bfffb934407 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -1,6 +1,7 @@ """Support for Minut Point.""" from __future__ import annotations +from collections.abc import Callable import logging from homeassistant.components.alarm_control_panel import ( @@ -19,6 +20,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import MinutPointClient from .const import DOMAIN as POINT_DOMAIN, POINT_DISCOVERY_NEW, SIGNAL_WEBHOOK _LOGGER = logging.getLogger(__name__) @@ -53,12 +55,20 @@ class MinutPointAlarmControl(AlarmControlPanelEntity): _attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY - def __init__(self, point_client, home_id): + def __init__(self, point_client: MinutPointClient, home_id: str) -> None: """Initialize the entity.""" self._client = point_client self._home_id = home_id - self._async_unsub_hook_dispatcher_connect = None - self._changed_by = None + self._async_unsub_hook_dispatcher_connect: Callable[[], None] | None = None + self._home = point_client.homes[self._home_id] + + self._attr_name = self._home["name"] + self._attr_unique_id = f"point.{home_id}" + self._attr_device_info = DeviceInfo( + identifiers={(POINT_DOMAIN, home_id)}, + manufacturer="Minut", + name=self._attr_name, + ) async def async_added_to_hass(self) -> None: """Call when entity is added to HOme Assistant.""" @@ -85,29 +95,14 @@ class MinutPointAlarmControl(AlarmControlPanelEntity): return _LOGGER.debug("Received webhook: %s", _type) self._home["alarm_status"] = _type - self._changed_by = _changed_by + self._attr_changed_by = _changed_by self.async_write_ha_state() @property - def _home(self): - """Return the home object.""" - return self._client.homes[self._home_id] - - @property - def name(self): - """Return name of the device.""" - return self._home["name"] - - @property - def state(self): + def state(self) -> str: """Return state of the device.""" return EVENT_MAP.get(self._home["alarm_status"], STATE_ALARM_ARMED_AWAY) - @property - def changed_by(self): - """Return the user the last change was triggered by.""" - return self._changed_by - async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" status = await self._client.async_alarm_disarm(self._home_id) @@ -119,17 +114,3 @@ class MinutPointAlarmControl(AlarmControlPanelEntity): status = await self._client.async_alarm_arm(self._home_id) if status: self._home["alarm_status"] = "on" - - @property - def unique_id(self): - """Return the unique id of the sensor.""" - return f"point.{self._home_id}" - - @property - def device_info(self) -> DeviceInfo: - """Return a device description for device registry.""" - return DeviceInfo( - identifiers={(POINT_DOMAIN, self._home_id)}, - manufacturer="Minut", - name=self.name, - ) From 389664e37c9657c7b0899e938d0918e8f48b8036 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 13:05:30 +0200 Subject: [PATCH 1857/3516] Use attributes in lupusec alarm (#74109) --- .../components/lupusec/alarm_control_panel.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/lupusec/alarm_control_panel.py b/homeassistant/components/lupusec/alarm_control_panel.py index 425b813d18a..2ae0b5944bd 100644 --- a/homeassistant/components/lupusec/alarm_control_panel.py +++ b/homeassistant/components/lupusec/alarm_control_panel.py @@ -19,8 +19,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DOMAIN as LUPUSEC_DOMAIN, LupusecDevice -ICON = "mdi:security" - SCAN_INTERVAL = timedelta(seconds=2) @@ -44,18 +42,14 @@ def setup_platform( class LupusecAlarm(LupusecDevice, AlarmControlPanelEntity): """An alarm_control_panel implementation for Lupusec.""" + _attr_icon = "mdi:security" _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY ) @property - def icon(self): - """Return the icon.""" - return ICON - - @property - def state(self): + def state(self) -> str | None: """Return the state of the device.""" if self._device.is_standby: state = STATE_ALARM_DISARMED From 03d2d503932e3b26e279d8bb37dd3dbb8b4acb69 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 13:07:46 +0200 Subject: [PATCH 1858/3516] Use attributes in ifttt alarm (#74107) --- .../components/ifttt/alarm_control_panel.py | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/ifttt/alarm_control_panel.py b/homeassistant/components/ifttt/alarm_control_panel.py index 840dd2fed62..8bd267891a6 100644 --- a/homeassistant/components/ifttt/alarm_control_panel.py +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -127,6 +127,7 @@ def setup_platform( class IFTTTAlarmPanel(AlarmControlPanelEntity): """Representation of an alarm control panel controlled through IFTTT.""" + _attr_assumed_state = True _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY @@ -145,7 +146,7 @@ class IFTTTAlarmPanel(AlarmControlPanelEntity): optimistic, ): """Initialize the alarm control panel.""" - self._name = name + self._attr_name = name self._code = code self._code_arm_required = code_arm_required self._event_away = event_away @@ -153,25 +154,9 @@ class IFTTTAlarmPanel(AlarmControlPanelEntity): self._event_night = event_night self._event_disarm = event_disarm self._optimistic = optimistic - self._state = None @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the device.""" - return self._state - - @property - def assumed_state(self): - """Notify that this platform return an assumed state.""" - return True - - @property - def code_format(self): + def code_format(self) -> CodeFormat | None: """Return one or more digits/characters.""" if self._code is None: return None @@ -210,13 +195,13 @@ class IFTTTAlarmPanel(AlarmControlPanelEntity): self.hass.services.call(DOMAIN, SERVICE_TRIGGER, data) _LOGGER.debug("Called IFTTT integration to trigger event %s", event) if self._optimistic: - self._state = state + self._attr_state = state def push_alarm_state(self, value): """Push the alarm state to the given value.""" if value in ALLOWED_STATES: _LOGGER.debug("Pushed the alarm state to %s", value) - self._state = value + self._attr_state = value - def _check_code(self, code): + def _check_code(self, code: str | None) -> bool: return self._code is None or self._code == code From 79b3865b6014fe1fb45dea5d792cdc3d1e44ed91 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 13:15:38 +0200 Subject: [PATCH 1859/3516] Use attributes in ialarm alarm (#74099) --- homeassistant/components/ialarm/__init__.py | 12 ++++--- .../components/ialarm/alarm_control_panel.py | 35 +++++++++---------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/ialarm/__init__.py b/homeassistant/components/ialarm/__init__.py index db9aa000066..254bf6f685f 100644 --- a/homeassistant/components/ialarm/__init__.py +++ b/homeassistant/components/ialarm/__init__.py @@ -1,4 +1,6 @@ """iAlarm integration.""" +from __future__ import annotations + import asyncio import logging @@ -20,8 +22,8 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up iAlarm config.""" - host = entry.data[CONF_HOST] - port = entry.data[CONF_PORT] + host: str = entry.data[CONF_HOST] + port: int = entry.data[CONF_PORT] ialarm = IAlarm(host, port) try: @@ -55,11 +57,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class IAlarmDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching iAlarm data.""" - def __init__(self, hass, ialarm, mac): + def __init__(self, hass: HomeAssistant, ialarm: IAlarm, mac: str) -> None: """Initialize global iAlarm data updater.""" self.ialarm = ialarm - self.state = None - self.host = ialarm.host + self.state: str | None = None + self.host: str = ialarm.host self.mac = mac super().__init__( diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py index 74310d940e7..6a4e3d191eb 100644 --- a/homeassistant/components/ialarm/alarm_control_panel.py +++ b/homeassistant/components/ialarm/alarm_control_panel.py @@ -11,6 +11,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import IAlarmDataUpdateCoordinator from .const import DATA_COORDINATOR, DOMAIN @@ -18,39 +19,35 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up a iAlarm alarm control panel based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + coordinator: IAlarmDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR + ] async_add_entities([IAlarmPanel(coordinator)], False) -class IAlarmPanel(CoordinatorEntity, AlarmControlPanelEntity): +class IAlarmPanel( + CoordinatorEntity[IAlarmDataUpdateCoordinator], AlarmControlPanelEntity +): """Representation of an iAlarm device.""" + _attr_name = "iAlarm" _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY ) - @property - def device_info(self) -> DeviceInfo: - """Return device info for this device.""" - return DeviceInfo( - identifiers={(DOMAIN, self.unique_id)}, + def __init__(self, coordinator: IAlarmDataUpdateCoordinator) -> None: + """Create the entity with a DataUpdateCoordinator.""" + super().__init__(coordinator) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.mac)}, manufacturer="Antifurto365 - Meian", - name=self.name, + name="iAlarm", ) + self._attr_unique_id = coordinator.mac @property - def unique_id(self): - """Return a unique id.""" - return self.coordinator.mac - - @property - def name(self): - """Return the name.""" - return "iAlarm" - - @property - def state(self): + def state(self) -> str | None: """Return the state of the device.""" return self.coordinator.state From 39c7056be58455f80fe8715c11844160013f7073 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 28 Jun 2022 13:39:37 +0200 Subject: [PATCH 1860/3516] Migrate climacell to native_* (#74039) --- homeassistant/components/climacell/weather.py | 63 +++++-------------- tests/components/climacell/test_weather.py | 2 +- 2 files changed, 15 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index 0167cb72513..2b284114981 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -23,13 +23,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_VERSION, CONF_NAME, - LENGTH_FEET, - LENGTH_KILOMETERS, - LENGTH_METERS, + LENGTH_INCHES, LENGTH_MILES, - PRESSURE_HPA, PRESSURE_INHG, - SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR, TEMP_FAHRENHEIT, ) @@ -37,8 +33,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.sun import is_up from homeassistant.util import dt as dt_util -from homeassistant.util.distance import convert as distance_convert -from homeassistant.util.pressure import convert as pressure_convert from homeassistant.util.speed import convert as speed_convert from . import ClimaCellDataUpdateCoordinator, ClimaCellEntity @@ -89,6 +83,12 @@ async def async_setup_entry( class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): """Base ClimaCell weather entity.""" + _attr_native_precipitation_unit = LENGTH_INCHES + _attr_native_pressure_unit = PRESSURE_INHG + _attr_native_temperature_unit = TEMP_FAHRENHEIT + _attr_native_visibility_unit = LENGTH_MILES + _attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR + def __init__( self, config_entry: ConfigEntry, @@ -132,21 +132,6 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): else: translated_condition = self._translate_condition(condition, True) - if self.hass.config.units.is_metric: - if precipitation: - precipitation = round( - distance_convert(precipitation / 12, LENGTH_FEET, LENGTH_METERS) - * 1000, - 4, - ) - if wind_speed: - wind_speed = round( - speed_convert( - wind_speed, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR - ), - 4, - ) - data = { ATTR_FORECAST_TIME: forecast_dt.isoformat(), ATTR_FORECAST_CONDITION: translated_condition, @@ -164,13 +149,10 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return additional state attributes.""" wind_gust = self.wind_gust - if wind_gust and self.hass.config.units.is_metric: - wind_gust = round( - speed_convert( - self.wind_gust, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR - ), - 4, - ) + wind_gust = round( + speed_convert(self.wind_gust, SPEED_MILES_PER_HOUR, self._wind_speed_unit), + 4, + ) cloud_cover = self.cloud_cover return { ATTR_CLOUD_COVER: cloud_cover, @@ -199,12 +181,8 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): """Return the raw pressure.""" @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" - if self.hass.config.units.is_metric and self._pressure: - return round( - pressure_convert(self._pressure, PRESSURE_INHG, PRESSURE_HPA), 4 - ) return self._pressure @property @@ -213,15 +191,8 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): """Return the raw wind speed.""" @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" - if self.hass.config.units.is_metric and self._wind_speed: - return round( - speed_convert( - self._wind_speed, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR - ), - 4, - ) return self._wind_speed @property @@ -230,20 +201,14 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): """Return the raw visibility.""" @property - def visibility(self): + def native_visibility(self): """Return the visibility.""" - if self.hass.config.units.is_metric and self._visibility: - return round( - distance_convert(self._visibility, LENGTH_MILES, LENGTH_KILOMETERS), 4 - ) return self._visibility class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity): """Entity that talks to ClimaCell v3 API to retrieve weather data.""" - _attr_temperature_unit = TEMP_FAHRENHEIT - @staticmethod def _translate_condition( condition: int | str | None, sun_is_up: bool = True diff --git a/tests/components/climacell/test_weather.py b/tests/components/climacell/test_weather.py index 593caa7755f..e3326f267d4 100644 --- a/tests/components/climacell/test_weather.py +++ b/tests/components/climacell/test_weather.py @@ -156,7 +156,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY, ATTR_FORECAST_TIME: "2021-03-15T00:00:00-07:00", # DST starts - ATTR_FORECAST_PRECIPITATION: 7.3, + ATTR_FORECAST_PRECIPITATION: 7.31, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95, ATTR_FORECAST_TEMP: 1.2, ATTR_FORECAST_TEMP_LOW: 0.2, From 2a0b2ecca195edcbf660b6f7cb5673043d758fb5 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 28 Jun 2022 13:40:36 +0200 Subject: [PATCH 1861/3516] Fix depreciation period for Weather (#74106) Fix period --- homeassistant/components/weather/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index f09e76b0073..6f2de6a3cb0 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -304,7 +304,7 @@ class WeatherEntity(Entity): _LOGGER.warning( "%s::%s is overriding deprecated methods on an instance of " "WeatherEntity, this is not valid and will be unsupported " - "from Home Assistant 2022.10. Please %s", + "from Home Assistant 2023.1. Please %s", cls.__module__, cls.__name__, report_issue, From dac8f242e0e0650a42ad781be6387907823e8912 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 13:41:23 +0200 Subject: [PATCH 1862/3516] Improve type hints in mqtt and template alarms (#74101) --- homeassistant/components/mqtt/alarm_control_panel.py | 11 +++++------ .../components/template/alarm_control_panel.py | 12 ++++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index bd2495fd5d1..8e5ee54d688 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -172,7 +172,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): def __init__(self, hass, config, config_entry, discovery_data): """Init the MQTT Alarm Control Panel.""" - self._state = None + self._state: str | None = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @@ -233,7 +233,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): await subscription.async_subscribe_topics(self.hass, self._sub_state) @property - def state(self): + def state(self) -> str | None: """Return the state of the device.""" return self._state @@ -250,7 +250,7 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): ) @property - def code_format(self): + def code_format(self) -> alarm.CodeFormat | None: """Return one or more digits/characters.""" if (code := self._config.get(CONF_CODE)) is None: return None @@ -259,10 +259,9 @@ class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): return alarm.CodeFormat.TEXT @property - def code_arm_required(self): + def code_arm_required(self) -> bool: """Whether the code is required for arm actions.""" - code_required = self._config.get(CONF_CODE_ARM_REQUIRED) - return code_required + return self._config[CONF_CODE_ARM_REQUIRED] async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command. diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 74d794d703a..ae26e58ac04 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -144,8 +144,8 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): name = self._attr_name self._template = config.get(CONF_VALUE_TEMPLATE) self._disarm_script = None - self._code_arm_required = config[CONF_CODE_ARM_REQUIRED] - self._code_format = config[CONF_CODE_FORMAT] + self._code_arm_required: bool = config[CONF_CODE_ARM_REQUIRED] + self._code_format: TemplateCodeFormat = config[CONF_CODE_FORMAT] if (disarm_action := config.get(CONF_DISARM_ACTION)) is not None: self._disarm_script = Script(hass, disarm_action, name, DOMAIN) self._arm_away_script = None @@ -158,10 +158,10 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): if (arm_night_action := config.get(CONF_ARM_NIGHT_ACTION)) is not None: self._arm_night_script = Script(hass, arm_night_action, name, DOMAIN) - self._state = None + self._state: str | None = None @property - def state(self): + def state(self) -> str | None: """Return the state of the device.""" return self._state @@ -187,12 +187,12 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): return supported_features @property - def code_format(self): + def code_format(self) -> CodeFormat | None: """Regex for code format or None if no code is required.""" return self._code_format.value @property - def code_arm_required(self): + def code_arm_required(self) -> bool: """Whether the code is required for arm actions.""" return self._code_arm_required From c1f621e9c0b30e8b1df22dd8f2ad41d3fba8adbf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 13:42:43 +0200 Subject: [PATCH 1863/3516] Use attributes in nx584 alarm (#74105) --- .../components/nx584/alarm_control_panel.py | 44 +++++++------------ 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index 735c8104ef5..3eaaf07ad1c 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -55,9 +55,9 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the NX584 platform.""" - name = config.get(CONF_NAME) - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) + name: str = config[CONF_NAME] + host: str = config[CONF_HOST] + port: int = config[CONF_PORT] url = f"http://{host}:{port}" @@ -92,33 +92,19 @@ async def async_setup_platform( class NX584Alarm(alarm.AlarmControlPanelEntity): """Representation of a NX584-based alarm panel.""" + _attr_code_format = alarm.CodeFormat.NUMBER + _attr_state: str | None _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY ) - def __init__(self, name, alarm_client, url): + def __init__(self, name: str, alarm_client: client.Client, url: str) -> None: """Init the nx584 alarm panel.""" - self._name = name - self._state = None + self._attr_name = name self._alarm = alarm_client self._url = url - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def code_format(self): - """Return one or more digits/characters.""" - return alarm.CodeFormat.NUMBER - - @property - def state(self): - """Return the state of the device.""" - return self._state - def update(self) -> None: """Process new events from panel.""" try: @@ -129,11 +115,11 @@ class NX584Alarm(alarm.AlarmControlPanelEntity): "Unable to connect to %(host)s: %(reason)s", {"host": self._url, "reason": ex}, ) - self._state = None + self._attr_state = None zones = [] except IndexError: _LOGGER.error("NX584 reports no partitions") - self._state = None + self._attr_state = None zones = [] bypassed = False @@ -147,15 +133,15 @@ class NX584Alarm(alarm.AlarmControlPanelEntity): break if not part["armed"]: - self._state = STATE_ALARM_DISARMED + self._attr_state = STATE_ALARM_DISARMED elif bypassed: - self._state = STATE_ALARM_ARMED_HOME + self._attr_state = STATE_ALARM_ARMED_HOME else: - self._state = STATE_ALARM_ARMED_AWAY + self._attr_state = STATE_ALARM_ARMED_AWAY for flag in part["condition_flags"]: if flag == "Siren on": - self._state = STATE_ALARM_TRIGGERED + self._attr_state = STATE_ALARM_TRIGGERED def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" @@ -169,10 +155,10 @@ class NX584Alarm(alarm.AlarmControlPanelEntity): """Send arm away command.""" self._alarm.arm("exit") - def alarm_bypass(self, zone): + def alarm_bypass(self, zone: int) -> None: """Send bypass command.""" self._alarm.set_bypass(zone, True) - def alarm_unbypass(self, zone): + def alarm_unbypass(self, zone: int) -> None: """Send bypass command.""" self._alarm.set_bypass(zone, False) From 4b5c0be89673269bbe9dbefd6af5c7540aec3818 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 28 Jun 2022 13:42:58 +0200 Subject: [PATCH 1864/3516] Native to Weather Template (#74060) * Native to Weather template * Add validation --- homeassistant/components/template/weather.py | 61 +++++++++++++------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index 5d1a48269aa..d65e03b9656 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -20,15 +20,22 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, ENTITY_ID_FORMAT, + Forecast, WeatherEntity, ) -from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID +from homeassistant.const import CONF_NAME, CONF_TEMPERATURE_UNIT, CONF_UNIQUE_ID from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import ( + distance as distance_util, + pressure as pressure_util, + speed as speed_util, + temperature as temp_util, +) from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_conf @@ -61,6 +68,10 @@ CONF_WIND_BEARING_TEMPLATE = "wind_bearing_template" CONF_OZONE_TEMPLATE = "ozone_template" CONF_VISIBILITY_TEMPLATE = "visibility_template" CONF_FORECAST_TEMPLATE = "forecast_template" +CONF_PRESSURE_UNIT = "pressure_unit" +CONF_WIND_SPEED_UNIT = "wind_speed_unit" +CONF_VISIBILITY_UNIT = "visibility_unit" +CONF_PRECIPITATION_UNIT = "precipitation_unit" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -76,6 +87,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_VISIBILITY_TEMPLATE): cv.template, vol.Optional(CONF_FORECAST_TEMPLATE): cv.template, vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_TEMPERATURE_UNIT): vol.In(temp_util.VALID_UNITS), + vol.Optional(CONF_PRESSURE_UNIT): vol.In(pressure_util.VALID_UNITS), + vol.Optional(CONF_WIND_SPEED_UNIT): vol.In(speed_util.VALID_UNITS), + vol.Optional(CONF_VISIBILITY_UNIT): vol.In(distance_util.VALID_UNITS), + vol.Optional(CONF_PRECIPITATION_UNIT): vol.In(distance_util.VALID_UNITS), } ) @@ -109,10 +125,10 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): def __init__( self, - hass, - config, - unique_id, - ): + hass: HomeAssistant, + config: ConfigType, + unique_id: str | None, + ) -> None: """Initialize the Template weather.""" super().__init__(hass, config=config, unique_id=unique_id) @@ -128,6 +144,12 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): self._visibility_template = config.get(CONF_VISIBILITY_TEMPLATE) self._forecast_template = config.get(CONF_FORECAST_TEMPLATE) + self._attr_native_precipitation_unit = config.get(CONF_PRECIPITATION_UNIT) + self._attr_native_pressure_unit = config.get(CONF_PRESSURE_UNIT) + self._attr_native_temperature_unit = config.get(CONF_TEMPERATURE_UNIT) + self._attr_native_visibility_unit = config.get(CONF_VISIBILITY_UNIT) + self._attr_native_wind_speed_unit = config.get(CONF_WIND_SPEED_UNIT) + self.entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, name, hass=hass) self._condition = None @@ -139,66 +161,61 @@ class WeatherTemplate(TemplateEntity, WeatherEntity): self._wind_bearing = None self._ozone = None self._visibility = None - self._forecast = [] + self._forecast: list[Forecast] = [] @property - def condition(self): + def condition(self) -> str | None: """Return the current condition.""" return self._condition @property - def temperature(self): + def native_temperature(self) -> float | None: """Return the temperature.""" return self._temperature @property - def temperature_unit(self): - """Return the unit of measurement.""" - return self.hass.config.units.temperature_unit - - @property - def humidity(self): + def humidity(self) -> float | None: """Return the humidity.""" return self._humidity @property - def wind_speed(self): + def native_wind_speed(self) -> float | None: """Return the wind speed.""" return self._wind_speed @property - def wind_bearing(self): + def wind_bearing(self) -> float | str | None: """Return the wind bearing.""" return self._wind_bearing @property - def ozone(self): + def ozone(self) -> float | None: """Return the ozone level.""" return self._ozone @property - def visibility(self): + def native_visibility(self) -> float | None: """Return the visibility.""" return self._visibility @property - def pressure(self): + def native_pressure(self) -> float | None: """Return the air pressure.""" return self._pressure @property - def forecast(self): + def forecast(self) -> list[Forecast]: """Return the forecast.""" return self._forecast @property - def attribution(self): + def attribution(self) -> str | None: """Return the attribution.""" if self._attribution is None: return "Powered by Home Assistant" return self._attribution - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" if self._condition_template: From 8bed2e6459bfc1efb25d6a55aaea2eb1b9953cf9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 13:53:20 +0200 Subject: [PATCH 1865/3516] Remove zha from mypy ignore list (#73603) --- .../components/zha/core/channels/__init__.py | 4 +- .../components/zha/core/discovery.py | 8 +-- .../components/zha/core/registries.py | 61 ++++++++++--------- mypy.ini | 6 -- script/hassfest/mypy_config.py | 2 - 5 files changed, 38 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 2da7462f3eb..9042856b456 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -221,7 +221,7 @@ class ChannelPool: return self._channels.zha_device.is_mains_powered @property - def manufacturer(self) -> str | None: + def manufacturer(self) -> str: """Return device manufacturer.""" return self._channels.zha_device.manufacturer @@ -236,7 +236,7 @@ class ChannelPool: return self._channels.zha_device.hass @property - def model(self) -> str | None: + def model(self) -> str: """Return device model.""" return self._channels.zha_device.model diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index cdad57834eb..6b690f4da08 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -6,7 +6,7 @@ from collections.abc import Callable import logging from typing import TYPE_CHECKING -from homeassistant import const as ha_const +from homeassistant.const import CONF_TYPE, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -86,9 +86,7 @@ class ProbeEndpoint: unique_id = channel_pool.unique_id - component: str | None = self._device_configs.get(unique_id, {}).get( - ha_const.CONF_TYPE - ) + component: str | None = self._device_configs.get(unique_id, {}).get(CONF_TYPE) if component is None: ep_profile_id = channel_pool.endpoint.profile_id ep_device_type = channel_pool.endpoint.device_type @@ -136,7 +134,7 @@ class ProbeEndpoint: @staticmethod def probe_single_cluster( - component: str, + component: Platform | None, channel: base.ZigbeeChannel, ep_channels: ChannelPool, ) -> None: diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 7e2114b5911..d271f2ecba3 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -110,7 +110,7 @@ CLIENT_CHANNELS_REGISTRY: DictRegistry[type[ClientChannel]] = DictRegistry() ZIGBEE_CHANNEL_REGISTRY: DictRegistry[type[ZigbeeChannel]] = DictRegistry() -def set_or_callable(value): +def set_or_callable(value) -> frozenset[str] | Callable: """Convert single str or None to a set. Pass through callables and sets.""" if value is None: return frozenset() @@ -121,22 +121,26 @@ def set_or_callable(value): return frozenset([str(value)]) +def _get_empty_frozenset() -> frozenset[str]: + return frozenset() + + @attr.s(frozen=True) class MatchRule: """Match a ZHA Entity to a channel name or generic id.""" - channel_names: set[str] | str = attr.ib( + channel_names: frozenset[str] = attr.ib( factory=frozenset, converter=set_or_callable ) - generic_ids: set[str] | str = attr.ib(factory=frozenset, converter=set_or_callable) - manufacturers: Callable | set[str] | str = attr.ib( - factory=frozenset, converter=set_or_callable + generic_ids: frozenset[str] = attr.ib(factory=frozenset, converter=set_or_callable) + manufacturers: frozenset[str] | Callable = attr.ib( + factory=_get_empty_frozenset, converter=set_or_callable ) - models: Callable | set[str] | str = attr.ib( - factory=frozenset, converter=set_or_callable + models: frozenset[str] | Callable = attr.ib( + factory=_get_empty_frozenset, converter=set_or_callable ) - aux_channels: Callable | set[str] | str = attr.ib( - factory=frozenset, converter=set_or_callable + aux_channels: frozenset[str] | Callable = attr.ib( + factory=_get_empty_frozenset, converter=set_or_callable ) @property @@ -162,7 +166,8 @@ class MatchRule: weight += 10 * len(self.channel_names) weight += 5 * len(self.generic_ids) - weight += 1 * len(self.aux_channels) + if isinstance(self.aux_channels, frozenset): + weight += 1 * len(self.aux_channels) return weight def claim_channels(self, channel_pool: list[ZigbeeChannel]) -> list[ZigbeeChannel]: @@ -321,11 +326,11 @@ class ZHAEntityRegistry: def strict_match( self, component: str, - channel_names: set[str] | str = None, - generic_ids: set[str] | str = None, - manufacturers: Callable | set[str] | str = None, - models: Callable | set[str] | str = None, - aux_channels: Callable | set[str] | str = None, + channel_names: set[str] | str | None = None, + generic_ids: set[str] | str | None = None, + manufacturers: Callable | set[str] | str | None = None, + models: Callable | set[str] | str | None = None, + aux_channels: Callable | set[str] | str | None = None, ) -> Callable[[_ZhaEntityT], _ZhaEntityT]: """Decorate a strict match rule.""" @@ -346,11 +351,11 @@ class ZHAEntityRegistry: def multipass_match( self, component: str, - channel_names: set[str] | str = None, - generic_ids: set[str] | str = None, - manufacturers: Callable | set[str] | str = None, - models: Callable | set[str] | str = None, - aux_channels: Callable | set[str] | str = None, + channel_names: set[str] | str | None = None, + generic_ids: set[str] | str | None = None, + manufacturers: Callable | set[str] | str | None = None, + models: Callable | set[str] | str | None = None, + aux_channels: Callable | set[str] | str | None = None, stop_on_match_group: int | str | None = None, ) -> Callable[[_ZhaEntityT], _ZhaEntityT]: """Decorate a loose match rule.""" @@ -379,11 +384,11 @@ class ZHAEntityRegistry: def config_diagnostic_match( self, component: str, - channel_names: set[str] | str = None, - generic_ids: set[str] | str = None, - manufacturers: Callable | set[str] | str = None, - models: Callable | set[str] | str = None, - aux_channels: Callable | set[str] | str = None, + channel_names: set[str] | str | None = None, + generic_ids: set[str] | str | None = None, + manufacturers: Callable | set[str] | str | None = None, + models: Callable | set[str] | str | None = None, + aux_channels: Callable | set[str] | str | None = None, stop_on_match_group: int | str | None = None, ) -> Callable[[_ZhaEntityT], _ZhaEntityT]: """Decorate a loose match rule.""" @@ -432,9 +437,9 @@ class ZHAEntityRegistry: def clean_up(self) -> None: """Clean up post discovery.""" - self.single_device_matches: dict[ - Platform, dict[EUI64, list[str]] - ] = collections.defaultdict(lambda: collections.defaultdict(list)) + self.single_device_matches = collections.defaultdict( + lambda: collections.defaultdict(list) + ) ZHA_ENTITIES = ZHAEntityRegistry() diff --git a/mypy.ini b/mypy.ini index 0f8c8fc0b61..fb67983a31c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2995,9 +2995,3 @@ ignore_errors = true [mypy-homeassistant.components.xiaomi_miio.switch] ignore_errors = true - -[mypy-homeassistant.components.zha.core.discovery] -ignore_errors = true - -[mypy-homeassistant.components.zha.core.registries] -ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 0fd62d49ae2..bbb628a76bb 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -144,8 +144,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.xiaomi_miio.light", "homeassistant.components.xiaomi_miio.sensor", "homeassistant.components.xiaomi_miio.switch", - "homeassistant.components.zha.core.discovery", - "homeassistant.components.zha.core.registries", ] # Component modules which should set no_implicit_reexport = true. From 3b30d8a279e2c1632e0359a9a01a560d932a6d12 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 13:56:14 +0200 Subject: [PATCH 1866/3516] Use attributes in satel_integra alarm (#74103) --- .../satel_integra/alarm_control_panel.py | 34 +++++-------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/satel_integra/alarm_control_panel.py b/homeassistant/components/satel_integra/alarm_control_panel.py index 054909cd3c2..79ef4c048b3 100644 --- a/homeassistant/components/satel_integra/alarm_control_panel.py +++ b/homeassistant/components/satel_integra/alarm_control_panel.py @@ -61,6 +61,9 @@ async def async_setup_platform( class SatelIntegraAlarmPanel(alarm.AlarmControlPanelEntity): """Representation of an AlarmDecoder-based alarm panel.""" + _attr_code_format = alarm.CodeFormat.NUMBER + _attr_should_poll = False + _attr_state: str | None _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY @@ -68,8 +71,7 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanelEntity): def __init__(self, controller, name, arm_home_mode, partition_id): """Initialize the alarm panel.""" - self._name = name - self._state = None + self._attr_name = name self._arm_home_mode = arm_home_mode self._partition_id = partition_id self._satel = controller @@ -89,8 +91,8 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanelEntity): """Handle alarm status update.""" state = self._read_alarm_state() _LOGGER.debug("Got status update, current status: %s", state) - if state != self._state: - self._state = state + if state != self._attr_state: + self._attr_state = state self.async_write_ha_state() else: _LOGGER.debug("Ignoring alarm status message, same state") @@ -129,35 +131,15 @@ class SatelIntegraAlarmPanel(alarm.AlarmControlPanelEntity): return hass_alarm_status - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def should_poll(self): - """Return the polling state.""" - return False - - @property - def code_format(self): - """Return the regex for code format or None if no code is required.""" - return alarm.CodeFormat.NUMBER - - @property - def state(self): - """Return the state of the device.""" - return self._state - async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if not code: _LOGGER.debug("Code was empty or None") return - clear_alarm_necessary = self._state == STATE_ALARM_TRIGGERED + clear_alarm_necessary = self._attr_state == STATE_ALARM_TRIGGERED - _LOGGER.debug("Disarming, self._state: %s", self._state) + _LOGGER.debug("Disarming, self._attr_state: %s", self._attr_state) await self._satel.disarm(code, [self._partition_id]) From 4335cafb3f0b84d2281caa0cdfbd8c49743981ca Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 14:01:49 +0200 Subject: [PATCH 1867/3516] Use attributes in totalconnect alarm (#74113) --- .../totalconnect/alarm_control_panel.py | 55 +++++++------------ 1 file changed, 21 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index 6ed29dbcad3..5798f4d31d3 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -20,6 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -85,7 +86,7 @@ class TotalConnectAlarm(CoordinatorEntity, alarm.AlarmControlPanelEntity): self._partition = self._location.partitions[partition_id] self._device = self._location.devices[self._location.security_device_id] self._state = None - self._extra_state_attributes = {} + self._attr_extra_state_attributes = {} """ Set unique_id to location_id for partition 1 to avoid breaking change @@ -93,35 +94,25 @@ class TotalConnectAlarm(CoordinatorEntity, alarm.AlarmControlPanelEntity): Add _# for partition 2 and beyond. """ if partition_id == 1: - self._name = name - self._unique_id = f"{location_id}" + self._attr_name = name + self._attr_unique_id = f"{location_id}" else: - self._name = f"{name} partition {partition_id}" - self._unique_id = f"{location_id}_{partition_id}" + self._attr_name = f"{name} partition {partition_id}" + self._attr_unique_id = f"{location_id}_{partition_id}" @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def unique_id(self): - """Return the unique id.""" - return self._unique_id - - @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device info.""" - return { - "identifiers": {(DOMAIN, self._device.serial_number)}, - "name": self._device.name, - } + return DeviceInfo( + identifiers={(DOMAIN, self._device.serial_number)}, + name=self._device.name, + ) @property - def state(self): + def state(self) -> str | None: """Return the state of the device.""" attr = { - "location_name": self._name, + "location_name": self.name, "location_id": self._location_id, "partition": self._partition_id, "ac_loss": self._location.ac_loss, @@ -131,6 +122,7 @@ class TotalConnectAlarm(CoordinatorEntity, alarm.AlarmControlPanelEntity): "triggered_zone": None, } + state = None if self._partition.arming_state.is_disarmed(): state = STATE_ALARM_DISARMED elif self._partition.arming_state.is_armed_night(): @@ -156,15 +148,10 @@ class TotalConnectAlarm(CoordinatorEntity, alarm.AlarmControlPanelEntity): attr["triggered_source"] = "Carbon Monoxide" self._state = state - self._extra_state_attributes = attr + self._attr_extra_state_attributes = attr return self._state - @property - def extra_state_attributes(self): - """Return the state attributes of the device.""" - return self._extra_state_attributes - async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" try: @@ -176,7 +163,7 @@ class TotalConnectAlarm(CoordinatorEntity, alarm.AlarmControlPanelEntity): ) from error except BadResultCodeError as error: raise HomeAssistantError( - f"TotalConnect failed to disarm {self._name}." + f"TotalConnect failed to disarm {self.name}." ) from error await self.coordinator.async_request_refresh() @@ -195,7 +182,7 @@ class TotalConnectAlarm(CoordinatorEntity, alarm.AlarmControlPanelEntity): ) from error except BadResultCodeError as error: raise HomeAssistantError( - f"TotalConnect failed to arm home {self._name}." + f"TotalConnect failed to arm home {self.name}." ) from error await self.coordinator.async_request_refresh() @@ -214,7 +201,7 @@ class TotalConnectAlarm(CoordinatorEntity, alarm.AlarmControlPanelEntity): ) from error except BadResultCodeError as error: raise HomeAssistantError( - f"TotalConnect failed to arm away {self._name}." + f"TotalConnect failed to arm away {self.name}." ) from error await self.coordinator.async_request_refresh() @@ -233,7 +220,7 @@ class TotalConnectAlarm(CoordinatorEntity, alarm.AlarmControlPanelEntity): ) from error except BadResultCodeError as error: raise HomeAssistantError( - f"TotalConnect failed to arm night {self._name}." + f"TotalConnect failed to arm night {self.name}." ) from error await self.coordinator.async_request_refresh() @@ -252,7 +239,7 @@ class TotalConnectAlarm(CoordinatorEntity, alarm.AlarmControlPanelEntity): ) from error except BadResultCodeError as error: raise HomeAssistantError( - f"TotalConnect failed to arm home instant {self._name}." + f"TotalConnect failed to arm home instant {self.name}." ) from error await self.coordinator.async_request_refresh() @@ -271,7 +258,7 @@ class TotalConnectAlarm(CoordinatorEntity, alarm.AlarmControlPanelEntity): ) from error except BadResultCodeError as error: raise HomeAssistantError( - f"TotalConnect failed to arm away instant {self._name}." + f"TotalConnect failed to arm away instant {self.name}." ) from error await self.coordinator.async_request_refresh() From 50cba60d1fc77dcdff028ffbae3c3c1f123bd4bc Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 28 Jun 2022 14:07:28 +0200 Subject: [PATCH 1868/3516] Update base image to 2022.06.2 (#74114) --- build.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.yaml b/build.yaml index 23486fb5510..7bd4abcfc20 100644 --- a/build.yaml +++ b/build.yaml @@ -1,11 +1,11 @@ image: homeassistant/{arch}-homeassistant shadow_repository: ghcr.io/home-assistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.06.1 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.06.1 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.06.1 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.06.1 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.06.1 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.06.2 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.06.2 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.06.2 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.06.2 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.06.2 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io From c4ff317ec65586f8d6c204988ffdf49facfe1506 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 28 Jun 2022 14:29:00 +0200 Subject: [PATCH 1869/3516] Smhi minor fixes (#72606) * Initial commit * Tests * From review comments --- homeassistant/components/smhi/__init__.py | 43 ++++++++++++++- homeassistant/components/smhi/config_flow.py | 32 ++++------- homeassistant/components/smhi/weather.py | 7 ++- tests/components/smhi/__init__.py | 13 ++++- tests/components/smhi/test_config_flow.py | 58 +++++++++++++------- tests/components/smhi/test_init.py | 50 +++++++++++++++-- tests/components/smhi/test_weather.py | 18 +++--- 7 files changed, 161 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/smhi/__init__.py b/homeassistant/components/smhi/__init__.py index 398932bf4d6..e3f55904b77 100644 --- a/homeassistant/components/smhi/__init__.py +++ b/homeassistant/components/smhi/__init__.py @@ -1,7 +1,14 @@ """Support for the Swedish weather institute weather service.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LOCATION, + CONF_LONGITUDE, + CONF_NAME, + Platform, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries PLATFORMS = [Platform.WEATHER] @@ -11,7 +18,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Setting unique id where missing if entry.unique_id is None: - unique_id = f"{entry.data[CONF_LATITUDE]}-{entry.data[CONF_LONGITUDE]}" + unique_id = f"{entry.data[CONF_LOCATION][CONF_LATITUDE]}-{entry.data[CONF_LOCATION][CONF_LONGITUDE]}" hass.config_entries.async_update_entry(entry, unique_id=unique_id) hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -21,3 +28,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Migrate old entry.""" + if entry.version == 1: + new_data = { + CONF_NAME: entry.data[CONF_NAME], + CONF_LOCATION: { + CONF_LATITUDE: entry.data[CONF_LATITUDE], + CONF_LONGITUDE: entry.data[CONF_LONGITUDE], + }, + } + new_unique_id = f"smhi-{entry.data[CONF_LATITUDE]}-{entry.data[CONF_LONGITUDE]}" + + if not hass.config_entries.async_update_entry( + entry, data=new_data, unique_id=new_unique_id + ): + return False + + entry.version = 2 + new_unique_id_entity = f"smhi-{entry.data[CONF_LOCATION][CONF_LATITUDE]}-{entry.data[CONF_LOCATION][CONF_LONGITUDE]}" + + @callback + def update_unique_id(entity_entry: RegistryEntry) -> dict[str, str]: + """Update unique ID of entity entry.""" + return {"new_unique_id": new_unique_id_entity} + + await async_migrate_entries(hass, entry.entry_id, update_unique_id) + + return True diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py index 770f549efe0..8d338e3eb3e 100644 --- a/homeassistant/components/smhi/config_flow.py +++ b/homeassistant/components/smhi/config_flow.py @@ -7,11 +7,11 @@ from smhi.smhi_lib import Smhi, SmhiForecastException import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.selector import LocationSelector from .const import DEFAULT_NAME, DOMAIN, HOME_LOCATION_NAME @@ -33,7 +33,7 @@ async def async_check_location( class SmhiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for SMHI component.""" - VERSION = 1 + VERSION = 2 async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -43,8 +43,8 @@ class SmhiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: - lat: float = user_input[CONF_LATITUDE] - lon: float = user_input[CONF_LONGITUDE] + lat: float = user_input[CONF_LOCATION][CONF_LATITUDE] + lon: float = user_input[CONF_LOCATION][CONF_LONGITUDE] if await async_check_location(self.hass, lon, lat): name = f"{DEFAULT_NAME} {round(lat, 6)} {round(lon, 6)}" if ( @@ -57,30 +57,20 @@ class SmhiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): HOME_LOCATION_NAME if name == HOME_LOCATION_NAME else DEFAULT_NAME ) - await self.async_set_unique_id(f"{lat}-{lon}") + await self.async_set_unique_id(f"smhi-{lat}-{lon}") self._abort_if_unique_id_configured() return self.async_create_entry(title=name, data=user_input) errors["base"] = "wrong_location" - default_lat: float = self.hass.config.latitude - default_lon: float = self.hass.config.longitude - - for entry in self.hass.config_entries.async_entries(DOMAIN): - if ( - entry.data[CONF_LATITUDE] == self.hass.config.latitude - and entry.data[CONF_LONGITUDE] == self.hass.config.longitude - ): - default_lat = 0 - default_lon = 0 - + home_location = { + CONF_LATITUDE: self.hass.config.latitude, + CONF_LONGITUDE: self.hass.config.longitude, + } return self.async_show_form( step_id="user", data_schema=vol.Schema( - { - vol.Required(CONF_LATITUDE, default=default_lat): cv.latitude, - vol.Required(CONF_LONGITUDE, default=default_lon): cv.longitude, - } + {vol.Required(CONF_LOCATION, default=home_location): LocationSelector()} ), errors=errors, ) diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index cbe2ad37fe9..5d1f3e3c87e 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -42,6 +42,7 @@ from homeassistant.components.weather import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_LATITUDE, + CONF_LOCATION, CONF_LONGITUDE, CONF_NAME, LENGTH_KILOMETERS, @@ -106,8 +107,8 @@ async def async_setup_entry( entity = SmhiWeather( location[CONF_NAME], - location[CONF_LATITUDE], - location[CONF_LONGITUDE], + location[CONF_LOCATION][CONF_LATITUDE], + location[CONF_LOCATION][CONF_LONGITUDE], session=session, ) entity.entity_id = ENTITY_ID_SENSOR_FORMAT.format(name) @@ -135,7 +136,7 @@ class SmhiWeather(WeatherEntity): """Initialize the SMHI weather entity.""" self._attr_name = name - self._attr_unique_id = f"{latitude}, {longitude}" + self._attr_unique_id = f"smhi-{latitude}-{longitude}" self._forecasts: list[SmhiForecast] | None = None self._fail_count = 0 self._smhi_api = Smhi(longitude, latitude, session=session) diff --git a/tests/components/smhi/__init__.py b/tests/components/smhi/__init__.py index d815aafc8f5..377552da4d5 100644 --- a/tests/components/smhi/__init__.py +++ b/tests/components/smhi/__init__.py @@ -1,3 +1,14 @@ """Tests for the SMHI component.""" ENTITY_ID = "weather.smhi_test" -TEST_CONFIG = {"name": "test", "longitude": "17.84197", "latitude": "59.32624"} +TEST_CONFIG = { + "name": "test", + "location": { + "longitude": "17.84197", + "latitude": "59.32624", + }, +} +TEST_CONFIG_MIGRATE = { + "name": "test", + "longitude": "17.84197", + "latitude": "17.84197", +} diff --git a/tests/components/smhi/test_config_flow.py b/tests/components/smhi/test_config_flow.py index 60879e8af75..ab3e36f81de 100644 --- a/tests/components/smhi/test_config_flow.py +++ b/tests/components/smhi/test_config_flow.py @@ -7,7 +7,7 @@ from smhi.smhi_lib import SmhiForecastException from homeassistant import config_entries from homeassistant.components.smhi.const import DOMAIN -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -40,8 +40,10 @@ async def test_form(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_LATITUDE: 0.0, - CONF_LONGITUDE: 0.0, + CONF_LOCATION: { + CONF_LATITUDE: 0.0, + CONF_LONGITUDE: 0.0, + } }, ) await hass.async_block_till_done() @@ -49,8 +51,10 @@ async def test_form(hass: HomeAssistant) -> None: assert result2["type"] == RESULT_TYPE_CREATE_ENTRY assert result2["title"] == "Home" assert result2["data"] == { - "latitude": 0.0, - "longitude": 0.0, + "location": { + "latitude": 0.0, + "longitude": 0.0, + }, "name": "Home", } assert len(mock_setup_entry.mock_calls) == 1 @@ -69,8 +73,10 @@ async def test_form(hass: HomeAssistant) -> None: result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], { - CONF_LATITUDE: 1.0, - CONF_LONGITUDE: 1.0, + CONF_LOCATION: { + CONF_LATITUDE: 1.0, + CONF_LONGITUDE: 1.0, + } }, ) await hass.async_block_till_done() @@ -78,8 +84,10 @@ async def test_form(hass: HomeAssistant) -> None: assert result4["type"] == RESULT_TYPE_CREATE_ENTRY assert result4["title"] == "Weather 1.0 1.0" assert result4["data"] == { - "latitude": 1.0, - "longitude": 1.0, + "location": { + "latitude": 1.0, + "longitude": 1.0, + }, "name": "Weather", } @@ -97,8 +105,10 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_LATITUDE: 0.0, - CONF_LONGITUDE: 0.0, + CONF_LOCATION: { + CONF_LATITUDE: 0.0, + CONF_LONGITUDE: 0.0, + } }, ) await hass.async_block_till_done() @@ -117,8 +127,10 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: result3 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_LATITUDE: 2.0, - CONF_LONGITUDE: 2.0, + CONF_LOCATION: { + CONF_LATITUDE: 2.0, + CONF_LONGITUDE: 2.0, + } }, ) await hass.async_block_till_done() @@ -126,8 +138,10 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: assert result3["type"] == RESULT_TYPE_CREATE_ENTRY assert result3["title"] == "Weather 2.0 2.0" assert result3["data"] == { - "latitude": 2.0, - "longitude": 2.0, + "location": { + "latitude": 2.0, + "longitude": 2.0, + }, "name": "Weather", } @@ -136,10 +150,12 @@ async def test_form_unique_id_exist(hass: HomeAssistant) -> None: """Test we handle unique id already exist.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id="1.0-1.0", + unique_id="smhi-1.0-1.0", data={ - "latitude": 1.0, - "longitude": 1.0, + "location": { + "latitude": 1.0, + "longitude": 1.0, + }, "name": "Weather", }, ) @@ -155,8 +171,10 @@ async def test_form_unique_id_exist(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_LATITUDE: 1.0, - CONF_LONGITUDE: 1.0, + CONF_LOCATION: { + CONF_LATITUDE: 1.0, + CONF_LONGITUDE: 1.0, + } }, ) await hass.async_block_till_done() diff --git a/tests/components/smhi/test_init.py b/tests/components/smhi/test_init.py index 2cf54ba7533..ea6d55fabf6 100644 --- a/tests/components/smhi/test_init.py +++ b/tests/components/smhi/test_init.py @@ -3,8 +3,9 @@ from smhi.smhi_lib import APIURL_TEMPLATE from homeassistant.components.smhi.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import async_get -from . import ENTITY_ID, TEST_CONFIG +from . import ENTITY_ID, TEST_CONFIG, TEST_CONFIG_MIGRATE from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -14,9 +15,11 @@ async def test_setup_entry( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str ) -> None: """Test setup entry.""" - uri = APIURL_TEMPLATE.format(TEST_CONFIG["longitude"], TEST_CONFIG["latitude"]) + uri = APIURL_TEMPLATE.format( + TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] + ) aioclient_mock.get(uri, text=api_response) - entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) + entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG, version=2) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -30,9 +33,11 @@ async def test_remove_entry( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str ) -> None: """Test remove entry.""" - uri = APIURL_TEMPLATE.format(TEST_CONFIG["longitude"], TEST_CONFIG["latitude"]) + uri = APIURL_TEMPLATE.format( + TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] + ) aioclient_mock.get(uri, text=api_response) - entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) + entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG, version=2) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -46,3 +51,38 @@ async def test_remove_entry( state = hass.states.get(ENTITY_ID) assert not state + + +async def test_migrate_entry( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str +) -> None: + """Test migrate entry and entities unique id.""" + uri = APIURL_TEMPLATE.format( + TEST_CONFIG_MIGRATE["longitude"], TEST_CONFIG_MIGRATE["latitude"] + ) + aioclient_mock.get(uri, text=api_response) + entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG_MIGRATE) + entry.add_to_hass(hass) + assert entry.version == 1 + + entity_reg = async_get(hass) + entity = entity_reg.async_get_or_create( + domain="weather", + config_entry=entry, + original_name="Weather", + platform="smhi", + supported_features=0, + unique_id="17.84197, 17.84197", + ) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity.entity_id) + assert state + + assert entry.version == 2 + assert entry.unique_id == "smhi-17.84197-17.84197" + + entity_get = entity_reg.async_get(entity.entity_id) + assert entity_get.unique_id == "smhi-17.84197-17.84197" diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 0097a7a5c5a..f33e8c9fa71 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -46,10 +46,12 @@ async def test_setup_hass( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str ) -> None: """Test for successfully setting up the smhi integration.""" - uri = APIURL_TEMPLATE.format(TEST_CONFIG["longitude"], TEST_CONFIG["latitude"]) + uri = APIURL_TEMPLATE.format( + TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] + ) aioclient_mock.get(uri, text=api_response) - entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -87,7 +89,7 @@ async def test_setup_hass( async def test_properties_no_data(hass: HomeAssistant) -> None: """Test properties when no API data available.""" - entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) entry.add_to_hass(hass) with patch( @@ -176,7 +178,7 @@ async def test_properties_unknown_symbol(hass: HomeAssistant) -> None: testdata = [data, data2, data3] - entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) entry.add_to_hass(hass) with patch( @@ -203,7 +205,7 @@ async def test_refresh_weather_forecast_retry( hass: HomeAssistant, error: Exception ) -> None: """Test the refresh weather forecast function.""" - entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) entry.add_to_hass(hass) now = utcnow() @@ -320,10 +322,12 @@ async def test_custom_speed_unit( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str ) -> None: """Test Wind Gust speed with custom unit.""" - uri = APIURL_TEMPLATE.format(TEST_CONFIG["longitude"], TEST_CONFIG["latitude"]) + uri = APIURL_TEMPLATE.format( + TEST_CONFIG["location"]["longitude"], TEST_CONFIG["location"]["latitude"] + ) aioclient_mock.get(uri, text=api_response) - entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG, version=2) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) From b75a6d265d18d1732f8572dc21c50d03809a4c4a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 14:49:01 +0200 Subject: [PATCH 1870/3516] Use attributes in spc alarm and binary sensor (#74120) --- .../components/spc/alarm_control_panel.py | 22 +++++++-------- homeassistant/components/spc/binary_sensor.py | 27 +++++++++---------- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/spc/alarm_control_panel.py b/homeassistant/components/spc/alarm_control_panel.py index a1b4e2b2392..b78703666bc 100644 --- a/homeassistant/components/spc/alarm_control_panel.py +++ b/homeassistant/components/spc/alarm_control_panel.py @@ -1,6 +1,8 @@ """Support for Vanderbilt (formerly Siemens) SPC alarm systems.""" from __future__ import annotations +from pyspcwebgw import SpcWebGateway +from pyspcwebgw.area import Area from pyspcwebgw.const import AreaMode import homeassistant.components.alarm_control_panel as alarm @@ -20,7 +22,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DATA_API, SIGNAL_UPDATE_ALARM -def _get_alarm_state(area): +def _get_alarm_state(area: Area) -> str | None: """Get the alarm state.""" if area.verified_alarm: @@ -44,20 +46,21 @@ async def async_setup_platform( """Set up the SPC alarm control panel platform.""" if discovery_info is None: return - api = hass.data[DATA_API] + api: SpcWebGateway = hass.data[DATA_API] async_add_entities([SpcAlarm(area=area, api=api) for area in api.areas.values()]) class SpcAlarm(alarm.AlarmControlPanelEntity): """Representation of the SPC alarm panel.""" + _attr_should_poll = False _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_NIGHT ) - def __init__(self, area, api): + def __init__(self, area: Area, api: SpcWebGateway) -> None: """Initialize the SPC alarm panel.""" self._area = area self._api = api @@ -73,27 +76,22 @@ class SpcAlarm(alarm.AlarmControlPanelEntity): ) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return self._area.name @property - def changed_by(self): + def changed_by(self) -> str: """Return the user the last change was triggered by.""" return self._area.last_changed_by @property - def state(self): + def state(self) -> str | None: """Return the state of the device.""" return _get_alarm_state(self._area) diff --git a/homeassistant/components/spc/binary_sensor.py b/homeassistant/components/spc/binary_sensor.py index 87068d97b8a..c4aaefdd518 100644 --- a/homeassistant/components/spc/binary_sensor.py +++ b/homeassistant/components/spc/binary_sensor.py @@ -1,7 +1,9 @@ """Support for Vanderbilt (formerly Siemens) SPC alarm systems.""" from __future__ import annotations +from pyspcwebgw import SpcWebGateway from pyspcwebgw.const import ZoneInput, ZoneType +from pyspcwebgw.zone import Zone from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -15,12 +17,12 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DATA_API, SIGNAL_UPDATE_SENSOR -def _get_device_class(zone_type): +def _get_device_class(zone_type: ZoneType) -> BinarySensorDeviceClass | None: return { ZoneType.ALARM: BinarySensorDeviceClass.MOTION, ZoneType.ENTRY_EXIT: BinarySensorDeviceClass.OPENING, ZoneType.FIRE: BinarySensorDeviceClass.SMOKE, - ZoneType.TECHNICAL: "power", + ZoneType.TECHNICAL: BinarySensorDeviceClass.POWER, }.get(zone_type) @@ -33,7 +35,7 @@ async def async_setup_platform( """Set up the SPC binary sensor.""" if discovery_info is None: return - api = hass.data[DATA_API] + api: SpcWebGateway = hass.data[DATA_API] async_add_entities( [ SpcBinarySensor(zone) @@ -46,11 +48,13 @@ async def async_setup_platform( class SpcBinarySensor(BinarySensorEntity): """Representation of a sensor based on a SPC zone.""" - def __init__(self, zone): + _attr_should_poll = False + + def __init__(self, zone: Zone) -> None: """Initialize the sensor device.""" self._zone = zone - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call for adding new entities.""" self.async_on_remove( async_dispatcher_connect( @@ -61,26 +65,21 @@ class SpcBinarySensor(BinarySensorEntity): ) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return self._zone.name @property - def is_on(self): + def is_on(self) -> bool: """Whether the device is switched on.""" return self._zone.input == ZoneInput.OPEN @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def device_class(self): + def device_class(self) -> BinarySensorDeviceClass | None: """Return the device class.""" return _get_device_class(self._zone.type) From 3836da48b36107feadfcb1a451af1b62eda55530 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 15:12:56 +0200 Subject: [PATCH 1871/3516] Use attributes in ness_alarm alarm (#74121) --- .../ness_alarm/alarm_control_panel.py | 45 ++++++------------- 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/ness_alarm/alarm_control_panel.py b/homeassistant/components/ness_alarm/alarm_control_panel.py index 0e80ac57e01..2f54b3abde6 100644 --- a/homeassistant/components/ness_alarm/alarm_control_panel.py +++ b/homeassistant/components/ness_alarm/alarm_control_panel.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from nessclient import ArmingState +from nessclient import ArmingState, Client import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature @@ -41,17 +41,18 @@ async def async_setup_platform( class NessAlarmPanel(alarm.AlarmControlPanelEntity): """Representation of a Ness alarm panel.""" + _attr_code_format = alarm.CodeFormat.NUMBER + _attr_should_poll = False _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.TRIGGER ) - def __init__(self, client, name): + def __init__(self, client: Client, name: str) -> None: """Initialize the alarm panel.""" self._client = client - self._name = name - self._state = None + self._attr_name = name async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -61,26 +62,6 @@ class NessAlarmPanel(alarm.AlarmControlPanelEntity): ) ) - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def should_poll(self): - """Return the polling state.""" - return False - - @property - def code_format(self): - """Return the regex for code format or None if no code is required.""" - return alarm.CodeFormat.NUMBER - - @property - def state(self): - """Return the state of the device.""" - return self._state - async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" await self._client.disarm(code) @@ -98,23 +79,23 @@ class NessAlarmPanel(alarm.AlarmControlPanelEntity): await self._client.panic(code) @callback - def _handle_arming_state_change(self, arming_state): + def _handle_arming_state_change(self, arming_state: ArmingState) -> None: """Handle arming state update.""" if arming_state == ArmingState.UNKNOWN: - self._state = None + self._attr_state = None elif arming_state == ArmingState.DISARMED: - self._state = STATE_ALARM_DISARMED + self._attr_state = STATE_ALARM_DISARMED elif arming_state == ArmingState.ARMING: - self._state = STATE_ALARM_ARMING + self._attr_state = STATE_ALARM_ARMING elif arming_state == ArmingState.EXIT_DELAY: - self._state = STATE_ALARM_ARMING + self._attr_state = STATE_ALARM_ARMING elif arming_state == ArmingState.ARMED: - self._state = STATE_ALARM_ARMED_AWAY + self._attr_state = STATE_ALARM_ARMED_AWAY elif arming_state == ArmingState.ENTRY_DELAY: - self._state = STATE_ALARM_PENDING + self._attr_state = STATE_ALARM_PENDING elif arming_state == ArmingState.TRIGGERED: - self._state = STATE_ALARM_TRIGGERED + self._attr_state = STATE_ALARM_TRIGGERED else: _LOGGER.warning("Unhandled arming state: %s", arming_state) From 29c389b34230dfcbd9af6bd26d0f7c44848900b1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 15:22:46 +0200 Subject: [PATCH 1872/3516] Adjust remaining type hints in alarm properties (#74126) --- homeassistant/components/envisalink/alarm_control_panel.py | 4 ++-- homeassistant/components/tuya/alarm_control_panel.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index 35cfe8558a8..ad65bf70275 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -141,14 +141,14 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity): self.async_write_ha_state() @property - def code_format(self): + def code_format(self) -> CodeFormat | None: """Regex for code format or None if no code is required.""" if self._code: return None return CodeFormat.NUMBER @property - def state(self): + def state(self) -> str: """Return the state of the device.""" state = STATE_UNKNOWN diff --git a/homeassistant/components/tuya/alarm_control_panel.py b/homeassistant/components/tuya/alarm_control_panel.py index aca09131b4c..aae50902d03 100644 --- a/homeassistant/components/tuya/alarm_control_panel.py +++ b/homeassistant/components/tuya/alarm_control_panel.py @@ -116,7 +116,7 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity): self._attr_supported_features |= AlarmControlPanelEntityFeature.TRIGGER @property - def state(self): + def state(self) -> str | None: """Return the state of the device.""" if not (status := self.device.status.get(self.entity_description.key)): return None From 0063274f83b4744d68f47f51b9d847420cf05397 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Jun 2022 06:24:20 -0700 Subject: [PATCH 1873/3516] Bump HAP-python to 4.5.0 (#74127) --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index bde540d6372..2ca78b6d915 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.4.0", + "HAP-python==4.5.0", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1" diff --git a/requirements_all.txt b/requirements_all.txt index 5f5fc9fe4d6..b15ce59108e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -8,7 +8,7 @@ AEMET-OpenData==0.2.1 Adax-local==0.1.4 # homeassistant.components.homekit -HAP-python==4.4.0 +HAP-python==4.5.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24903c43b30..a1a8604659e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -10,7 +10,7 @@ AEMET-OpenData==0.2.1 Adax-local==0.1.4 # homeassistant.components.homekit -HAP-python==4.4.0 +HAP-python==4.5.0 # homeassistant.components.flick_electric PyFlick==0.0.2 From 670af6fde342f5a1bcd7fa0cf6630b1546d5dcc1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 15:26:50 +0200 Subject: [PATCH 1874/3516] Use attributes in risco alarm (#74117) --- .../components/risco/alarm_control_panel.py | 44 +++++++------------ 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/risco/alarm_control_panel.py b/homeassistant/components/risco/alarm_control_panel.py index f814be0f2bd..3bad03fda10 100644 --- a/homeassistant/components/risco/alarm_control_panel.py +++ b/homeassistant/components/risco/alarm_control_panel.py @@ -20,6 +20,7 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -65,44 +66,46 @@ async def async_setup_entry( class RiscoAlarm(AlarmControlPanelEntity, RiscoEntity): """Representation of a Risco partition.""" + _attr_code_format = CodeFormat.NUMBER + def __init__(self, coordinator, partition_id, code, options): """Init the partition.""" super().__init__(coordinator) self._partition_id = partition_id self._partition = self.coordinator.data.partitions[self._partition_id] self._code = code - self._code_arm_required = options[CONF_CODE_ARM_REQUIRED] + self._attr_code_arm_required = options[CONF_CODE_ARM_REQUIRED] self._code_disarm_required = options[CONF_CODE_DISARM_REQUIRED] self._risco_to_ha = options[CONF_RISCO_STATES_TO_HA] self._ha_to_risco = options[CONF_HA_STATES_TO_RISCO] - self._supported_states = 0 + self._attr_supported_features = 0 for state in self._ha_to_risco: - self._supported_states |= STATES_TO_SUPPORTED_FEATURES[state] + self._attr_supported_features |= STATES_TO_SUPPORTED_FEATURES[state] def _get_data_from_coordinator(self): self._partition = self.coordinator.data.partitions[self._partition_id] @property - def device_info(self): + 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)}, + name=self.name, + manufacturer="Risco", + ) @property - def name(self): + def name(self) -> str: """Return the name of the partition.""" return f"Risco {self._risco.site_name} Partition {self._partition_id}" @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique id for that partition.""" return f"{self._risco.site_uuid}_{self._partition_id}" @property - def state(self): + def state(self) -> str | None: """Return the state of the device.""" if self._partition.triggered: return STATE_ALARM_TRIGGERED @@ -121,21 +124,6 @@ class RiscoAlarm(AlarmControlPanelEntity, RiscoEntity): return None - @property - def supported_features(self): - """Return the list of supported features.""" - return self._supported_states - - @property - def code_arm_required(self): - """Whether the code is required for arm actions.""" - return self._code_arm_required - - @property - def code_format(self): - """Return one or more digits/characters.""" - return CodeFormat.NUMBER - def _validate_code(self, code): """Validate given code.""" return code == self._code @@ -164,7 +152,7 @@ class RiscoAlarm(AlarmControlPanelEntity, RiscoEntity): await self._arm(STATE_ALARM_ARMED_CUSTOM_BYPASS, code) async def _arm(self, mode, code): - if self._code_arm_required and not self._validate_code(code): + if self.code_arm_required and not self._validate_code(code): _LOGGER.warning("Wrong code entered for %s", mode) return From f3a24d5a45acd8221d82ae5a09d889cfb241a2f4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 15:29:33 +0200 Subject: [PATCH 1875/3516] Use attributes in xiaomi_miio alarm (#74125) --- .../xiaomi_miio/alarm_control_panel.py | 64 ++++--------------- 1 file changed, 14 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/alarm_control_panel.py b/homeassistant/components/xiaomi_miio/alarm_control_panel.py index be7daf5e077..b5057a4a3dd 100644 --- a/homeassistant/components/xiaomi_miio/alarm_control_panel.py +++ b/homeassistant/components/xiaomi_miio/alarm_control_panel.py @@ -51,6 +51,7 @@ async def async_setup_entry( class XiaomiGatewayAlarm(AlarmControlPanelEntity): """Representation of the XiaomiGatewayAlarm.""" + _attr_icon = "mdi:shield-home" _attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY def __init__( @@ -58,50 +59,13 @@ class XiaomiGatewayAlarm(AlarmControlPanelEntity): ): """Initialize the entity.""" self._gateway = gateway_device - self._name = gateway_name - self._gateway_device_id = gateway_device_id - self._unique_id = f"{model}-{mac_address}" - self._icon = "mdi:shield-home" - self._available = None - self._state = None - - @property - def unique_id(self): - """Return an unique ID.""" - return self._unique_id - - @property - def device_id(self): - """Return the device id of the gateway.""" - return self._gateway_device_id - - @property - def device_info(self) -> DeviceInfo: - """Return the device info of the gateway.""" - return DeviceInfo( - identifiers={(DOMAIN, self._gateway_device_id)}, + self._attr_name = gateway_name + self._attr_unique_id = f"{model}-{mac_address}" + self._attr_available = False + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, gateway_device_id)}, ) - @property - def name(self): - """Return the name of this entity, if any.""" - return self._name - - @property - def icon(self): - """Return the icon to use for device if any.""" - return self._icon - - @property - def available(self): - """Return true when state is known.""" - return self._available - - @property - def state(self): - """Return the state of the device.""" - return self._state - async def _try_command(self, mask_error, func, *args, **kwargs): """Call a device command handling error messages.""" try: @@ -129,22 +93,22 @@ class XiaomiGatewayAlarm(AlarmControlPanelEntity): try: state = await self.hass.async_add_executor_job(self._gateway.alarm.status) except DeviceException as ex: - if self._available: - self._available = False + if self._attr_available: + self._attr_available = False _LOGGER.error("Got exception while fetching the state: %s", ex) return _LOGGER.debug("Got new state: %s", state) - self._available = True + self._attr_available = True if state == XIAOMI_STATE_ARMED_VALUE: - self._state = STATE_ALARM_ARMED_AWAY + self._attr_state = STATE_ALARM_ARMED_AWAY elif state == XIAOMI_STATE_DISARMED_VALUE: - self._state = STATE_ALARM_DISARMED + self._attr_state = STATE_ALARM_DISARMED elif state == XIAOMI_STATE_ARMING_VALUE: - self._state = STATE_ALARM_ARMING + self._attr_state = STATE_ALARM_ARMING else: _LOGGER.warning( "New state (%s) doesn't match expected values: %s/%s/%s", @@ -153,6 +117,6 @@ class XiaomiGatewayAlarm(AlarmControlPanelEntity): XIAOMI_STATE_DISARMED_VALUE, XIAOMI_STATE_ARMING_VALUE, ) - self._state = None + self._attr_state = None - _LOGGER.debug("State value: %s", self._state) + _LOGGER.debug("State value: %s", self._attr_state) From ed7ea1423a08eb56a41455c620be6438f54b1ca9 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 28 Jun 2022 15:53:00 +0200 Subject: [PATCH 1876/3516] Fix ZHA color mode not being set correctly when changing light state (#74018) --- homeassistant/components/zha/light.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 7c9e8d738a4..2f3379fa6b1 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -262,6 +262,7 @@ class BaseLight(LogMixin, light.LightEntity): if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return + self._color_mode = ColorMode.COLOR_TEMP self._color_temp = temperature self._hs_color = None @@ -275,6 +276,7 @@ class BaseLight(LogMixin, light.LightEntity): if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return + self._color_mode = ColorMode.HS self._hs_color = hs_color self._color_temp = None From 0e9164b082f5f460d47c5f567f09f0c7c8e7e0be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=5Bp=CA=B2=C9=B5s=5D?= Date: Tue, 28 Jun 2022 16:22:09 +0200 Subject: [PATCH 1877/3516] Add bool template filter and function (#74068) Co-authored-by: Erik --- homeassistant/helpers/template.py | 41 ++++++++++++++++++++++++------- tests/helpers/test_template.py | 28 +++++++++++++++++++++ 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 53e2eb122f6..36f5dc9b22c 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -19,7 +19,7 @@ import re import statistics from struct import error as StructError, pack, unpack_from import sys -from typing import Any, cast +from typing import Any, NoReturn, TypeVar, cast, overload from urllib.parse import urlencode as urllib_urlencode import weakref @@ -92,6 +92,8 @@ _COLLECTABLE_STATE_ATTRIBUTES = { "name", } +_T = TypeVar("_T") + ALL_STATES_RATE_LIMIT = timedelta(minutes=1) DOMAIN_STATES_RATE_LIMIT = timedelta(seconds=1) @@ -946,6 +948,31 @@ def _resolve_state( return None +@overload +def forgiving_boolean(value: Any) -> bool | object: + ... + + +@overload +def forgiving_boolean(value: Any, default: _T) -> bool | _T: + ... + + +def forgiving_boolean( + value: Any, default: _T | object = _SENTINEL +) -> bool | _T | object: + """Try to convert value to a boolean.""" + try: + # Import here, not at top-level to avoid circular import + from . import config_validation as cv # pylint: disable=import-outside-toplevel + + return cv.boolean(value) + except vol.Invalid: + if default is _SENTINEL: + raise_no_default("bool", value) + return default + + def result_as_boolean(template_result: Any | None) -> bool: """Convert the template result to a boolean. @@ -956,13 +983,7 @@ def result_as_boolean(template_result: Any | None) -> bool: if template_result is None: return False - try: - # Import here, not at top-level to avoid circular import - from . import config_validation as cv # pylint: disable=import-outside-toplevel - - return cv.boolean(template_result) - except vol.Invalid: - return False + return forgiving_boolean(template_result, default=False) def expand(hass: HomeAssistant, *args: Any) -> Iterable[State]: @@ -1368,7 +1389,7 @@ def utcnow(hass: HomeAssistant) -> datetime: return dt_util.utcnow() -def raise_no_default(function, value): +def raise_no_default(function: str, value: Any) -> NoReturn: """Log warning if no default is specified.""" template, action = template_cv.get() or ("", "rendering or compiling") raise ValueError( @@ -1981,6 +2002,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["relative_time"] = relative_time self.filters["slugify"] = slugify self.filters["iif"] = iif + self.filters["bool"] = forgiving_boolean self.globals["log"] = logarithm self.globals["sin"] = sine self.globals["cos"] = cosine @@ -2012,6 +2034,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["unpack"] = struct_unpack self.globals["slugify"] = slugify self.globals["iif"] = iif + self.globals["bool"] = forgiving_boolean self.tests["is_number"] = is_number 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 59b653bc23e..fb57eff7685 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -296,6 +296,34 @@ def test_int_function(hass): assert render(hass, "{{ int('bad', default=1) }}") == 1 +def test_bool_function(hass): + """Test bool function.""" + assert render(hass, "{{ bool(true) }}") is True + assert render(hass, "{{ bool(false) }}") is False + assert render(hass, "{{ bool('on') }}") is True + assert render(hass, "{{ bool('off') }}") is False + with pytest.raises(TemplateError): + render(hass, "{{ bool('unknown') }}") + with pytest.raises(TemplateError): + render(hass, "{{ bool(none) }}") + assert render(hass, "{{ bool('unavailable', none) }}") is None + assert render(hass, "{{ bool('unavailable', default=none) }}") is None + + +def test_bool_filter(hass): + """Test bool filter.""" + assert render(hass, "{{ true | bool }}") is True + assert render(hass, "{{ false | bool }}") is False + assert render(hass, "{{ 'on' | bool }}") is True + assert render(hass, "{{ 'off' | bool }}") is False + with pytest.raises(TemplateError): + render(hass, "{{ 'unknown' | bool }}") + with pytest.raises(TemplateError): + render(hass, "{{ none | bool }}") + assert render(hass, "{{ 'unavailable' | bool(none) }}") is None + assert render(hass, "{{ 'unavailable' | bool(default=none) }}") is None + + @pytest.mark.parametrize( "value, expected", [ From 12c49e1c94e8e871ff4953fcfa6b41f499f51902 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 28 Jun 2022 10:41:21 -0400 Subject: [PATCH 1878/3516] Add Aqara FP1 configuration entities to ZHA (#73027) * add multi state input * config entities * remove multistate input sensor used for testing * mypy --- homeassistant/components/zha/button.py | 14 +++++++- .../zha/core/channels/manufacturerspecific.py | 7 ++++ homeassistant/components/zha/select.py | 35 ++++++++++++++++++- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py index 29bfb2ca248..0f98bfaad51 100644 --- a/homeassistant/components/zha/button.py +++ b/homeassistant/components/zha/button.py @@ -154,9 +154,21 @@ class ZHAAttributeButton(ZhaEntity, ButtonEntity): }, ) class FrostLockResetButton(ZHAAttributeButton, id_suffix="reset_frost_lock"): - """Defines a ZHA identify button.""" + """Defines a ZHA frost lock reset button.""" _attribute_name = "frost_lock_reset" _attribute_value = 0 _attr_device_class = ButtonDeviceClass.RESTART _attr_entity_category = EntityCategory.CONFIG + + +@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.motion.ac01"}) +class NoPresenceStatusResetButton( + ZHAAttributeButton, id_suffix="reset_no_presence_status" +): + """Defines a ZHA no presence status reset button.""" + + _attribute_name = "reset_no_presence_status" + _attribute_value = 1 + _attr_device_class = ButtonDeviceClass.RESTART + _attr_entity_category = EntityCategory.CONFIG diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 0c246e28db7..101db65a66e 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -71,6 +71,13 @@ class OppleRemote(ZigbeeChannel): "motion_sensitivity": True, "trigger_indicator": True, } + elif self.cluster.endpoint.model == "lumi.motion.ac01": + self.ZCL_INIT_ATTRS = { # pylint: disable=invalid-name + "presence": True, + "monitoring_mode": True, + "motion_sensitivity": True, + "approach_distance": True, + } async def async_initialize_channel_specific(self, from_cache: bool) -> None: """Initialize channel specific.""" diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 6d202ccceb2..79349273e38 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -225,9 +225,42 @@ class AqaraMotionSensitivities(types.enum8): High = 0x03 -@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.motion.ac02"}) +@CONFIG_DIAGNOSTIC_MATCH( + channel_names="opple_cluster", models={"lumi.motion.ac01", "lumi.motion.ac02"} +) class AqaraMotionSensitivity(ZCLEnumSelectEntity, id_suffix="motion_sensitivity"): """Representation of a ZHA on off transition time configuration entity.""" _select_attr = "motion_sensitivity" _enum = AqaraMotionSensitivities + + +class AqaraMonitoringModess(types.enum8): + """Aqara monitoring modes.""" + + Undirected = 0x00 + Left_Right = 0x01 + + +@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.motion.ac01"}) +class AqaraMonitoringMode(ZCLEnumSelectEntity, id_suffix="monitoring_mode"): + """Representation of a ZHA monitoring mode configuration entity.""" + + _select_attr = "monitoring_mode" + _enum = AqaraMonitoringModess + + +class AqaraApproachDistances(types.enum8): + """Aqara approach distances.""" + + Far = 0x00 + Medium = 0x01 + Near = 0x02 + + +@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.motion.ac01"}) +class AqaraApproachDistance(ZCLEnumSelectEntity, id_suffix="approach_distance"): + """Representation of a ZHA approach distance configuration entity.""" + + _select_attr = "approach_distance" + _enum = AqaraApproachDistances From a053a3a8a42a79b97d83b0d3ce990bb69fe2be7d Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 28 Jun 2022 11:01:27 -0400 Subject: [PATCH 1879/3516] Add cluster attr data to ZHA device diagnostics (#70238) * Add cluster attr data to ZHA device diagnostics * add unsupported attributes and refactor * remove await * make parseable --- homeassistant/components/zha/diagnostics.py | 84 ++++++++++++++++++++- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/diagnostics.py b/homeassistant/components/zha/diagnostics.py index 5ae2ff23e96..697e7be336c 100644 --- a/homeassistant/components/zha/diagnostics.py +++ b/homeassistant/components/zha/diagnostics.py @@ -8,6 +8,8 @@ import bellows import pkg_resources import zigpy from zigpy.config import CONF_NWK_EXTENDED_PAN_ID +from zigpy.profiles import PROFILES +from zigpy.zcl import Cluster import zigpy_deconz import zigpy_xbee import zigpy_zigate @@ -15,11 +17,24 @@ import zigpy_znp from homeassistant.components.diagnostics.util import async_redact_data from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_UNIQUE_ID +from homeassistant.const import CONF_ID, CONF_NAME, CONF_UNIQUE_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from .core.const import ATTR_IEEE, DATA_ZHA, DATA_ZHA_CONFIG, DATA_ZHA_GATEWAY +from .core.const import ( + ATTR_ATTRIBUTE_NAME, + ATTR_DEVICE_TYPE, + ATTR_IEEE, + ATTR_IN_CLUSTERS, + ATTR_OUT_CLUSTERS, + ATTR_PROFILE_ID, + ATTR_VALUE, + CONF_ALARM_MASTER_CODE, + DATA_ZHA, + DATA_ZHA_CONFIG, + DATA_ZHA_GATEWAY, + UNKNOWN, +) from .core.device import ZHADevice from .core.gateway import ZHAGateway from .core.helpers import async_get_zha_device @@ -27,11 +42,16 @@ from .core.helpers import async_get_zha_device KEYS_TO_REDACT = { ATTR_IEEE, CONF_UNIQUE_ID, + CONF_ALARM_MASTER_CODE, "network_key", CONF_NWK_EXTENDED_PAN_ID, "partner_ieee", } +ATTRIBUTES = "attributes" +CLUSTER_DETAILS = "cluster_details" +UNSUPPORTED_ATTRIBUTES = "unsupported_attributes" + def shallow_asdict(obj: Any) -> dict: """Return a shallow copy of a dataclass as a dict.""" @@ -77,4 +97,62 @@ async def async_get_device_diagnostics( ) -> dict: """Return diagnostics for a device.""" zha_device: ZHADevice = async_get_zha_device(hass, device.id) - return async_redact_data(zha_device.zha_device_info, KEYS_TO_REDACT) + device_info: dict[str, Any] = zha_device.zha_device_info + device_info[CLUSTER_DETAILS] = get_endpoint_cluster_attr_data(zha_device) + return async_redact_data(device_info, KEYS_TO_REDACT) + + +def get_endpoint_cluster_attr_data(zha_device: ZHADevice) -> dict: + """Return endpoint cluster attribute data.""" + cluster_details = {} + for ep_id, endpoint in zha_device.device.endpoints.items(): + if ep_id == 0: + continue + endpoint_key = ( + f"{PROFILES.get(endpoint.profile_id).DeviceType(endpoint.device_type).name}" + if PROFILES.get(endpoint.profile_id) is not None + and endpoint.device_type is not None + else UNKNOWN + ) + cluster_details[ep_id] = { + ATTR_DEVICE_TYPE: { + CONF_NAME: endpoint_key, + CONF_ID: endpoint.device_type, + }, + ATTR_PROFILE_ID: endpoint.profile_id, + ATTR_IN_CLUSTERS: { + f"0x{cluster_id:04x}": { + "endpoint_attribute": cluster.ep_attribute, + **get_cluster_attr_data(cluster), + } + for cluster_id, cluster in endpoint.in_clusters.items() + }, + ATTR_OUT_CLUSTERS: { + f"0x{cluster_id:04x}": { + "endpoint_attribute": cluster.ep_attribute, + **get_cluster_attr_data(cluster), + } + for cluster_id, cluster in endpoint.out_clusters.items() + }, + } + return cluster_details + + +def get_cluster_attr_data(cluster: Cluster) -> dict: + """Return cluster attribute data.""" + return { + ATTRIBUTES: { + f"0x{attr_id:04x}": { + ATTR_ATTRIBUTE_NAME: attr_def.name, + ATTR_VALUE: attr_value, + } + for attr_id, attr_def in cluster.attributes.items() + if (attr_value := cluster.get(attr_def.name)) is not None + }, + UNSUPPORTED_ATTRIBUTES: { + f"0x{cluster.find_attribute(u_attr).id:04x}": { + ATTR_ATTRIBUTE_NAME: cluster.find_attribute(u_attr).name + } + for u_attr in cluster.unsupported_attributes + }, + } From 2225d0e899a16870f7da2673988929cf87d9aa41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Jun 2022 10:07:40 -0500 Subject: [PATCH 1880/3516] Enable serialization of float subclasses with orjson (#74136) --- homeassistant/helpers/json.py | 2 ++ tests/helpers/test_json.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index dbe3163da08..8b91f5eb2b5 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -35,6 +35,8 @@ def json_encoder_default(obj: Any) -> Any: """ if isinstance(obj, set): return list(obj) + if isinstance(obj, float): + return float(obj) if hasattr(obj, "as_dict"): return obj.as_dict() if isinstance(obj, Path): diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py index 17066b682af..cfb403ca4a9 100644 --- a/tests/helpers/test_json.py +++ b/tests/helpers/test_json.py @@ -8,6 +8,7 @@ from homeassistant import core from homeassistant.helpers.json import ( ExtendedJSONEncoder, JSONEncoder, + json_dumps, json_dumps_sorted, ) from homeassistant.util import dt as dt_util @@ -77,3 +78,12 @@ def test_json_dumps_sorted(): assert json_dumps_sorted(data) == json.dumps( data, sort_keys=True, separators=(",", ":") ) + + +def test_json_dumps_float_subclass(): + """Test the json dumps a float subclass.""" + + class FloatSubclass(float): + """A float subclass.""" + + assert json_dumps({"c": FloatSubclass(1.2)}) == '{"c":1.2}' From dc039f52185c74c501da57a5f090f423184461ae Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 17:12:32 +0200 Subject: [PATCH 1881/3516] Use standard argument name in async_step_reauth (#74137) --- homeassistant/components/aladdin_connect/config_flow.py | 2 +- homeassistant/components/ambee/config_flow.py | 2 +- homeassistant/components/awair/config_flow.py | 2 +- homeassistant/components/brunt/config_flow.py | 2 +- homeassistant/components/cloudflare/config_flow.py | 2 +- homeassistant/components/deluge/config_flow.py | 2 +- homeassistant/components/discord/config_flow.py | 2 +- homeassistant/components/efergy/config_flow.py | 2 +- homeassistant/components/esphome/config_flow.py | 2 +- homeassistant/components/geocaching/config_flow.py | 2 +- homeassistant/components/google/config_flow.py | 2 +- homeassistant/components/laundrify/config_flow.py | 2 +- homeassistant/components/motioneye/config_flow.py | 5 +---- homeassistant/components/neato/config_flow.py | 2 +- homeassistant/components/netatmo/config_flow.py | 2 +- homeassistant/components/powerwall/config_flow.py | 2 +- homeassistant/components/pvoutput/config_flow.py | 2 +- homeassistant/components/sensibo/config_flow.py | 2 +- homeassistant/components/sonarr/config_flow.py | 2 +- homeassistant/components/steam_online/config_flow.py | 2 +- homeassistant/components/tailscale/config_flow.py | 2 +- homeassistant/components/tankerkoenig/config_flow.py | 2 +- homeassistant/components/tautulli/config_flow.py | 2 +- homeassistant/components/tractive/config_flow.py | 2 +- homeassistant/components/trafikverket_ferry/config_flow.py | 2 +- homeassistant/components/trafikverket_train/config_flow.py | 2 +- homeassistant/components/transmission/config_flow.py | 2 +- homeassistant/components/unifi/config_flow.py | 2 +- homeassistant/components/unifiprotect/config_flow.py | 2 +- homeassistant/components/uptimerobot/config_flow.py | 2 +- homeassistant/components/verisure/config_flow.py | 2 +- homeassistant/components/vlc_telnet/config_flow.py | 2 +- homeassistant/components/wallbox/config_flow.py | 2 +- homeassistant/components/yale_smart_alarm/config_flow.py | 2 +- homeassistant/components/yolink/config_flow.py | 2 +- 35 files changed, 35 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index f0f622b3ab7..0b928e9d423 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -45,7 +45,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 entry: config_entries.ConfigEntry | None - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle re-authentication with Aladdin Connect.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) diff --git a/homeassistant/components/ambee/config_flow.py b/homeassistant/components/ambee/config_flow.py index 6c11f01b759..7bfc1fa11af 100644 --- a/homeassistant/components/ambee/config_flow.py +++ b/homeassistant/components/ambee/config_flow.py @@ -72,7 +72,7 @@ class AmbeeFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle initiation of re-authentication with Ambee.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index fc7fd1e79a4..1e83144945d 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -48,7 +48,7 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle re-auth if token invalid.""" return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/brunt/config_flow.py b/homeassistant/components/brunt/config_flow.py index bba58deea45..cfd3bfa69cb 100644 --- a/homeassistant/components/brunt/config_flow.py +++ b/homeassistant/components/brunt/config_flow.py @@ -78,7 +78,7 @@ class BruntConfigFlow(ConfigFlow, domain=DOMAIN): data=user_input, ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/cloudflare/config_flow.py b/homeassistant/components/cloudflare/config_flow.py index af67cbe8ffc..215411bc667 100644 --- a/homeassistant/components/cloudflare/config_flow.py +++ b/homeassistant/components/cloudflare/config_flow.py @@ -98,7 +98,7 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN): self.zones: list[str] | None = None self.records: list[str] | None = None - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle initiation of re-authentication with Cloudflare.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/deluge/config_flow.py b/homeassistant/components/deluge/config_flow.py index a4bb1893b7e..359ed1635c5 100644 --- a/homeassistant/components/deluge/config_flow.py +++ b/homeassistant/components/deluge/config_flow.py @@ -76,7 +76,7 @@ class DelugeFlowHandler(ConfigFlow, domain=DOMAIN): ) return self.async_show_form(step_id="user", data_schema=schema, errors=errors) - async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle a reauthorization flow request.""" return await self.async_step_user() diff --git a/homeassistant/components/discord/config_flow.py b/homeassistant/components/discord/config_flow.py index 93027132850..b28c55b022f 100644 --- a/homeassistant/components/discord/config_flow.py +++ b/homeassistant/components/discord/config_flow.py @@ -23,7 +23,7 @@ CONFIG_SCHEMA = vol.Schema({vol.Required(CONF_API_TOKEN): str}) class DiscordFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Discord.""" - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle a reauthorization flow request.""" return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/efergy/config_flow.py b/homeassistant/components/efergy/config_flow.py index 0abf99c2504..b2f2a368a9e 100644 --- a/homeassistant/components/efergy/config_flow.py +++ b/homeassistant/components/efergy/config_flow.py @@ -53,7 +53,7 @@ class EfergyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle a reauthorization flow request.""" return await self.async_step_user() diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 552d7ed420e..76359cda4e7 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -64,7 +64,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a flow initialized by the user.""" return await self._async_step_user_base(user_input=user_input) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle a flow initialized by a reauth event.""" entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) assert entry is not None diff --git a/homeassistant/components/geocaching/config_flow.py b/homeassistant/components/geocaching/config_flow.py index 9ce3cb76775..56fa56a1f82 100644 --- a/homeassistant/components/geocaching/config_flow.py +++ b/homeassistant/components/geocaching/config_flow.py @@ -25,7 +25,7 @@ class GeocachingFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): """Return logger.""" return logging.getLogger(__name__) - async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index a5951edec22..cbe1de69f9e 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -156,7 +156,7 @@ class OAuth2FlowHandler( }, ) - async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" self._reauth_config_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/laundrify/config_flow.py b/homeassistant/components/laundrify/config_flow.py index c091324d9a7..55a29fec2e7 100644 --- a/homeassistant/components/laundrify/config_flow.py +++ b/homeassistant/components/laundrify/config_flow.py @@ -77,7 +77,7 @@ class LaundrifyConfigFlow(ConfigFlow, domain=DOMAIN): step_id="init", data_schema=CONFIG_SCHEMA, errors=errors ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index 6b88f47a588..662c4d23660 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -157,10 +157,7 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): data=user_input, ) - async def async_step_reauth( - self, - config_data: Mapping[str, Any], - ) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle a reauthentication flow.""" return await self.async_step_user() diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py index 1c180fe1dbd..6b31cf9c05d 100644 --- a/homeassistant/components/neato/config_flow.py +++ b/homeassistant/components/neato/config_flow.py @@ -35,7 +35,7 @@ class OAuth2FlowHandler( return await super().async_step_user(user_input=user_input) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon migration of old entries.""" return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index 125e6ee38e5..ba63c76ad66 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -68,7 +68,7 @@ class NetatmoFlowHandler( return await super().async_step_user(user_input) - async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index b541c1b4bf7..b9f6f3969fd 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -206,7 +206,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/pvoutput/config_flow.py b/homeassistant/components/pvoutput/config_flow.py index 25cc68acc24..2016f87e611 100644 --- a/homeassistant/components/pvoutput/config_flow.py +++ b/homeassistant/components/pvoutput/config_flow.py @@ -84,7 +84,7 @@ class PVOutputFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle initiation of re-authentication with PVOutput.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/sensibo/config_flow.py b/homeassistant/components/sensibo/config_flow.py index a3214bdad56..c7aaa30b3db 100644 --- a/homeassistant/components/sensibo/config_flow.py +++ b/homeassistant/components/sensibo/config_flow.py @@ -29,7 +29,7 @@ class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): entry: config_entries.ConfigEntry | None - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle re-authentication with Sensibo.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py index 8e34d7a7ed4..3ea386faa78 100644 --- a/homeassistant/components/sonarr/config_flow.py +++ b/homeassistant/components/sonarr/config_flow.py @@ -63,7 +63,7 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return SonarrOptionsFlowHandler(config_entry) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) diff --git a/homeassistant/components/steam_online/config_flow.py b/homeassistant/components/steam_online/config_flow.py index 94b3e8d809c..7ed1f0a3610 100644 --- a/homeassistant/components/steam_online/config_flow.py +++ b/homeassistant/components/steam_online/config_flow.py @@ -126,7 +126,7 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): import_config[CONF_ACCOUNT] = import_config[CONF_ACCOUNTS][0] return await self.async_step_user(import_config) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle a reauthorization flow request.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) diff --git a/homeassistant/components/tailscale/config_flow.py b/homeassistant/components/tailscale/config_flow.py index a51cb722988..5f28c566801 100644 --- a/homeassistant/components/tailscale/config_flow.py +++ b/homeassistant/components/tailscale/config_flow.py @@ -82,7 +82,7 @@ class TailscaleFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle initiation of re-authentication with Tailscale.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/tankerkoenig/config_flow.py b/homeassistant/components/tankerkoenig/config_flow.py index 77baeddfce9..e3d273825a5 100644 --- a/homeassistant/components/tankerkoenig/config_flow.py +++ b/homeassistant/components/tankerkoenig/config_flow.py @@ -144,7 +144,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): options={CONF_SHOW_ON_MAP: True}, ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/tautulli/config_flow.py b/homeassistant/components/tautulli/config_flow.py index b4f3e3985ec..d70384c5485 100644 --- a/homeassistant/components/tautulli/config_flow.py +++ b/homeassistant/components/tautulli/config_flow.py @@ -71,7 +71,7 @@ class TautulliConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle a reauthorization flow request.""" return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/tractive/config_flow.py b/homeassistant/components/tractive/config_flow.py index 647d97f7179..ba42aeb600d 100644 --- a/homeassistant/components/tractive/config_flow.py +++ b/homeassistant/components/tractive/config_flow.py @@ -70,7 +70,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=USER_DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/trafikverket_ferry/config_flow.py b/homeassistant/components/trafikverket_ferry/config_flow.py index 2b0a1dec655..1f5d19118eb 100644 --- a/homeassistant/components/trafikverket_ferry/config_flow.py +++ b/homeassistant/components/trafikverket_ferry/config_flow.py @@ -60,7 +60,7 @@ class TVFerryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ferry_api = TrafikverketFerry(web_session, api_key) await ferry_api.async_get_next_ferry_stop(ferry_from, ferry_to) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle re-authentication with Trafikverket.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) diff --git a/homeassistant/components/trafikverket_train/config_flow.py b/homeassistant/components/trafikverket_train/config_flow.py index 521e499ec5d..c620e264142 100644 --- a/homeassistant/components/trafikverket_train/config_flow.py +++ b/homeassistant/components/trafikverket_train/config_flow.py @@ -55,7 +55,7 @@ class TVTrainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await train_api.async_get_train_station(train_from) await train_api.async_get_train_station(train_to) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle re-authentication with Trafikverket.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index c0bc51b0683..a21fe9b8837 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -92,7 +92,7 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 50e578b1dae..fcf5970bf6c 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -202,7 +202,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): errors=errors, ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Trigger a reauthentication flow.""" config_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 8e114c4f38b..330a5e530a1 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -266,7 +266,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return nvr_data, errors - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) diff --git a/homeassistant/components/uptimerobot/config_flow.py b/homeassistant/components/uptimerobot/config_flow.py index 83371bdd4a7..14ec1ae6cdc 100644 --- a/homeassistant/components/uptimerobot/config_flow.py +++ b/homeassistant/components/uptimerobot/config_flow.py @@ -85,7 +85,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Return the reauth confirm step.""" return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py index 91bde6db219..41687dbc6a4 100644 --- a/homeassistant/components/verisure/config_flow.py +++ b/homeassistant/components/verisure/config_flow.py @@ -109,7 +109,7 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle initiation of re-authentication with Verisure.""" self.entry = cast( ConfigEntry, diff --git a/homeassistant/components/vlc_telnet/config_flow.py b/homeassistant/components/vlc_telnet/config_flow.py index 9c97e876e1b..35898e91b34 100644 --- a/homeassistant/components/vlc_telnet/config_flow.py +++ b/homeassistant/components/vlc_telnet/config_flow.py @@ -105,7 +105,7 @@ class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=user_form_schema(user_input), errors=errors ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle reauth flow.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) assert self.entry diff --git a/homeassistant/components/wallbox/config_flow.py b/homeassistant/components/wallbox/config_flow.py index dbd1f3612a5..85f5d02ba99 100644 --- a/homeassistant/components/wallbox/config_flow.py +++ b/homeassistant/components/wallbox/config_flow.py @@ -48,7 +48,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=COMPONENT_DOMAIN): """Start the Wallbox config flow.""" self._reauth_entry: config_entries.ConfigEntry | None = None - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/yale_smart_alarm/config_flow.py b/homeassistant/components/yale_smart_alarm/config_flow.py index a3f350cef23..a2462df41cb 100644 --- a/homeassistant/components/yale_smart_alarm/config_flow.py +++ b/homeassistant/components/yale_smart_alarm/config_flow.py @@ -54,7 +54,7 @@ class YaleConfigFlow(ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return YaleOptionsFlowHandler(config_entry) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle initiation of re-authentication with Yale.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/yolink/config_flow.py b/homeassistant/components/yolink/config_flow.py index 68eabdfa183..128cd6cb35c 100644 --- a/homeassistant/components/yolink/config_flow.py +++ b/homeassistant/components/yolink/config_flow.py @@ -31,7 +31,7 @@ class OAuth2FlowHandler( scopes = ["create"] return {"scope": " ".join(scopes)} - async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] From b51ad16db9e5cefb7224c08d6658940af626ed36 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 17:19:03 +0200 Subject: [PATCH 1882/3516] Adjust button type hints in components (#74132) --- homeassistant/components/august/button.py | 2 +- homeassistant/components/bond/button.py | 3 +-- homeassistant/components/esphome/button.py | 3 +-- homeassistant/components/mqtt/button.py | 2 +- homeassistant/components/octoprint/button.py | 3 ++- homeassistant/components/tuya/button.py | 4 +--- homeassistant/components/xiaomi_miio/button.py | 3 +-- homeassistant/components/yale_smart_alarm/button.py | 4 ++-- homeassistant/components/zwave_me/button.py | 4 +--- 9 files changed, 11 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/august/button.py b/homeassistant/components/august/button.py index 5f4032153a2..c96db61ca1a 100644 --- a/homeassistant/components/august/button.py +++ b/homeassistant/components/august/button.py @@ -30,7 +30,7 @@ class AugustWakeLockButton(AugustEntityMixin, ButtonEntity): self._attr_name = f"{device.device_name} Wake" self._attr_unique_id = f"{self._device_id}_wake" - async def async_press(self, **kwargs): + async def async_press(self) -> None: """Wake the device.""" await self._data.async_status_async(self._device_id, self._hyper_bridge) diff --git a/homeassistant/components/bond/button.py b/homeassistant/components/bond/button.py index 2c6ffc69693..9a82309e347 100644 --- a/homeassistant/components/bond/button.py +++ b/homeassistant/components/bond/button.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any from bond_async import Action, BPUPSubscriptions @@ -290,7 +289,7 @@ class BondButtonEntity(BondEntity, ButtonEntity): hub, device, bpup_subs, description.name, description.key.lower() ) - async def async_press(self, **kwargs: Any) -> None: + async def async_press(self) -> None: """Press the button.""" if self.entity_description.argument: action = Action( diff --git a/homeassistant/components/esphome/button.py b/homeassistant/components/esphome/button.py index 5b6f2c153c8..3f610c8bbfa 100644 --- a/homeassistant/components/esphome/button.py +++ b/homeassistant/components/esphome/button.py @@ -2,7 +2,6 @@ from __future__ import annotations from contextlib import suppress -from typing import Any from aioesphomeapi import ButtonInfo, EntityState @@ -46,6 +45,6 @@ class EsphomeButton(EsphomeEntity[ButtonInfo, EntityState], ButtonEntity): # never gets a state update. self._on_state_update() - async def async_press(self, **kwargs: Any) -> None: + async def async_press(self) -> None: """Press the button.""" await self._client.button_command(self._static_info.key) diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index 370243c3579..0374727bf7d 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -130,7 +130,7 @@ class MqttButton(MqttEntity, ButtonEntity): """Return the device class of the sensor.""" return self._config.get(CONF_DEVICE_CLASS) - async def async_press(self, **kwargs): + async def async_press(self) -> None: """Turn the device on. This method is a coroutine. diff --git a/homeassistant/components/octoprint/button.py b/homeassistant/components/octoprint/button.py index e16f123a73a..0d403c3ec87 100644 --- a/homeassistant/components/octoprint/button.py +++ b/homeassistant/components/octoprint/button.py @@ -5,6 +5,7 @@ from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -54,7 +55,7 @@ class OctoprintButton(CoordinatorEntity[OctoprintDataUpdateCoordinator], ButtonE self._attr_unique_id = f"{button_type}-{device_id}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Device info.""" return self.coordinator.device_info diff --git a/homeassistant/components/tuya/button.py b/homeassistant/components/tuya/button.py index 3b4a2883266..26014e53b74 100644 --- a/homeassistant/components/tuya/button.py +++ b/homeassistant/components/tuya/button.py @@ -1,8 +1,6 @@ """Support for Tuya buttons.""" from __future__ import annotations -from typing import Any - from tuya_iot import TuyaDevice, TuyaDeviceManager from homeassistant.components.button import ButtonEntity, ButtonEntityDescription @@ -109,6 +107,6 @@ class TuyaButtonEntity(TuyaEntity, ButtonEntity): self.entity_description = description self._attr_unique_id = f"{super().unique_id}{description.key}" - def press(self, **kwargs: Any) -> None: + def press(self) -> None: """Press the button.""" self._send_command([{"code": self.entity_description.key, "value": True}]) diff --git a/homeassistant/components/xiaomi_miio/button.py b/homeassistant/components/xiaomi_miio/button.py index 6a69289f7ef..0f5b59a262d 100644 --- a/homeassistant/components/xiaomi_miio/button.py +++ b/homeassistant/components/xiaomi_miio/button.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any from homeassistant.components.button import ( ButtonDeviceClass, @@ -111,7 +110,7 @@ class XiaomiGenericCoordinatedButton(XiaomiCoordinatedMiioEntity, ButtonEntity): super().__init__(name, device, entry, unique_id, coordinator) self.entity_description = description - async def async_press(self, **kwargs: Any) -> None: + async def async_press(self) -> None: """Press the button.""" method = getattr(self._device, self.entity_description.method_press) await self._try_command( diff --git a/homeassistant/components/yale_smart_alarm/button.py b/homeassistant/components/yale_smart_alarm/button.py index 081c25c4342..cd312e79ceb 100644 --- a/homeassistant/components/yale_smart_alarm/button.py +++ b/homeassistant/components/yale_smart_alarm/button.py @@ -1,7 +1,7 @@ """Support for Yale Smart Alarm button.""" from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry @@ -50,7 +50,7 @@ class YalePanicButton(YaleAlarmEntity, ButtonEntity): self._attr_name = f"{coordinator.entry.data[CONF_NAME]} {description.name}" self._attr_unique_id = f"yale_smart_alarm-{description.key}" - async def async_press(self, **kwargs: Any) -> None: + async def async_press(self) -> None: """Press the button.""" if TYPE_CHECKING: assert self.coordinator.yale, "Connection to API is missing" diff --git a/homeassistant/components/zwave_me/button.py b/homeassistant/components/zwave_me/button.py index 69354daad43..7e0b4f02728 100644 --- a/homeassistant/components/zwave_me/button.py +++ b/homeassistant/components/zwave_me/button.py @@ -1,6 +1,4 @@ """Representation of a toggleButton.""" -from typing import Any - from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -41,6 +39,6 @@ async def async_setup_entry( class ZWaveMeButton(ZWaveMeEntity, ButtonEntity): """Representation of a ZWaveMe button.""" - def press(self, **kwargs: Any) -> None: + def press(self) -> None: """Turn the entity on.""" self.controller.zwave_api.send_command(self.device.id, "on") From b9c135870a4bb7de38451bf9e9dbfadaa26c5be4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 17:54:07 +0200 Subject: [PATCH 1883/3516] Fix model in vicare device_info (#74135) --- .../components/vicare/binary_sensor.py | 19 +++++++++--------- homeassistant/components/vicare/button.py | 20 +++++++++---------- homeassistant/components/vicare/climate.py | 19 +++++++++--------- homeassistant/components/vicare/sensor.py | 19 +++++++++--------- .../components/vicare/water_heater.py | 19 +++++++++--------- 5 files changed, 50 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/vicare/binary_sensor.py b/homeassistant/components/vicare/binary_sensor.py index 01cfff59357..3f54e5bd7e7 100644 --- a/homeassistant/components/vicare/binary_sensor.py +++ b/homeassistant/components/vicare/binary_sensor.py @@ -19,6 +19,7 @@ from homeassistant.components.binary_sensor import ( ) 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 . import ViCareRequiredKeysMixin @@ -198,15 +199,15 @@ class ViCareBinarySensor(BinarySensorEntity): self._state = None @property - def device_info(self): + def device_info(self) -> DeviceInfo: """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()), - "configuration_url": "https://developer.viessmann.com/", - } + return DeviceInfo( + identifiers={(DOMAIN, self._device_config.getConfig().serial)}, + name=self._device_config.getModel(), + manufacturer="Viessmann", + model=self._device_config.getModel(), + configuration_url="https://developer.viessmann.com/", + ) @property def available(self): @@ -214,7 +215,7 @@ class ViCareBinarySensor(BinarySensorEntity): return self._state is not None @property - def unique_id(self): + def unique_id(self) -> str: """Return unique ID for this device.""" tmp_id = ( f"{self._device_config.getConfig().serial}-{self.entity_description.key}" diff --git a/homeassistant/components/vicare/button.py b/homeassistant/components/vicare/button.py index e1d6bc4223c..b691c01796b 100644 --- a/homeassistant/components/vicare/button.py +++ b/homeassistant/components/vicare/button.py @@ -15,7 +15,7 @@ import requests from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ViCareRequiredKeysMixin @@ -94,18 +94,18 @@ class ViCareButton(ButtonEntity): _LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """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()), - "configuration_url": "https://developer.viessmann.com/", - } + return DeviceInfo( + identifiers={(DOMAIN, self._device_config.getConfig().serial)}, + name=self._device_config.getModel(), + manufacturer="Viessmann", + model=self._device_config.getModel(), + configuration_url="https://developer.viessmann.com/", + ) @property - def unique_id(self): + def unique_id(self) -> str: """Return unique ID for this device.""" tmp_id = ( f"{self._device_config.getConfig().serial}-{self.entity_description.key}" diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index fb8e60c3318..8f00f9e6c3b 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -31,6 +31,7 @@ from homeassistant.const import ( 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 .const import ( @@ -161,20 +162,20 @@ class ViCareClimate(ClimateEntity): self._current_action = None @property - def unique_id(self): + def unique_id(self) -> str: """Return unique ID for this device.""" return f"{self._device_config.getConfig().serial}-{self._circuit.id}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """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()), - "configuration_url": "https://developer.viessmann.com/", - } + return DeviceInfo( + identifiers={(DOMAIN, self._device_config.getConfig().serial)}, + name=self._device_config.getModel(), + manufacturer="Viessmann", + model=self._device_config.getModel(), + configuration_url="https://developer.viessmann.com/", + ) def update(self): """Let HA know there has been an update from the ViCare API.""" diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 06ed618ec86..e1deef0df00 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -30,6 +30,7 @@ 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 . import ViCareRequiredKeysMixin @@ -597,15 +598,15 @@ class ViCareSensor(SensorEntity): self._state = None @property - def device_info(self): + def device_info(self) -> DeviceInfo: """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()), - "configuration_url": "https://developer.viessmann.com/", - } + return DeviceInfo( + identifiers={(DOMAIN, self._device_config.getConfig().serial)}, + name=self._device_config.getModel(), + manufacturer="Viessmann", + model=self._device_config.getModel(), + configuration_url="https://developer.viessmann.com/", + ) @property def available(self): @@ -613,7 +614,7 @@ class ViCareSensor(SensorEntity): return self._state is not None @property - def unique_id(self): + def unique_id(self) -> str: """Return unique ID for this device.""" tmp_id = ( f"{self._device_config.getConfig().serial}-{self.entity_description.key}" diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index 6f9262200ec..ae8456cac6f 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -21,6 +21,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -140,20 +141,20 @@ class ViCareWater(WaterHeaterEntity): _LOGGER.error("Invalid data from Vicare server: %s", invalid_data_exception) @property - def unique_id(self): + def unique_id(self) -> str: """Return unique ID for this device.""" return f"{self._device_config.getConfig().serial}-{self._circuit.id}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """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()), - "configuration_url": "https://developer.viessmann.com/", - } + return DeviceInfo( + identifiers={(DOMAIN, self._device_config.getConfig().serial)}, + name=self._device_config.getModel(), + manufacturer="Viessmann", + model=self._device_config.getModel(), + configuration_url="https://developer.viessmann.com/", + ) @property def name(self): From 54138cda418de49e824644824f0b124f3a85b3c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Tue, 28 Jun 2022 18:08:31 +0200 Subject: [PATCH 1884/3516] Fix app browsing and local file streaming in Apple TV integration (#74112) --- homeassistant/components/apple_tv/browse_media.py | 4 ++-- homeassistant/components/apple_tv/media_player.py | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/apple_tv/browse_media.py b/homeassistant/components/apple_tv/browse_media.py index 8d0a94ca858..0673c9923fb 100644 --- a/homeassistant/components/apple_tv/browse_media.py +++ b/homeassistant/components/apple_tv/browse_media.py @@ -21,8 +21,8 @@ def build_app_list(app_list): media_content_id="apps", media_content_type=MEDIA_TYPE_APPS, title="Apps", - can_play=True, - can_expand=False, + can_play=False, + can_expand=True, children=[item_payload(item) for item in app_list], children_media_class=MEDIA_CLASS_APP, ) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 30a397d953c..362a09fb5fc 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -282,22 +282,20 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): # RAOP. Otherwise try to play it with regular AirPlay. if media_type == MEDIA_TYPE_APP: await self.atv.apps.launch_app(media_id) + return if media_source.is_media_source_id(media_id): play_item = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) - media_id = play_item.url + media_id = async_process_play_media_url(self.hass, play_item.url) media_type = MEDIA_TYPE_MUSIC - media_id = async_process_play_media_url(self.hass, media_id) - if self._is_feature_available(FeatureName.StreamFile) and ( media_type == MEDIA_TYPE_MUSIC or await is_streamable(media_id) ): _LOGGER.debug("Streaming %s via RAOP", media_id) await self.atv.stream.stream_file(media_id) - elif self._is_feature_available(FeatureName.PlayUrl): _LOGGER.debug("Playing %s via AirPlay", media_id) await self.atv.stream.play_url(media_id) From 2f60db6f80cedc99ebdeccc926986942c4f77970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 28 Jun 2022 18:20:56 +0200 Subject: [PATCH 1885/3516] Pin charset-normalizer to 2.0.12 (#74104) --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index dfbf056936f..9f849146fe3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -114,3 +114,7 @@ backoff<2.0 # Breaking change in version # https://github.com/samuelcolvin/pydantic/issues/4092 pydantic!=1.9.1 + +# Pin charset-normalizer to 2.0.12 due to version conflict. +# https://github.com/home-assistant/core/pull/74104 +charset-normalizer==2.0.12 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index a2a0eab897a..c6b50c6bd32 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -132,6 +132,10 @@ backoff<2.0 # Breaking change in version # https://github.com/samuelcolvin/pydantic/issues/4092 pydantic!=1.9.1 + +# Pin charset-normalizer to 2.0.12 due to version conflict. +# https://github.com/home-assistant/core/pull/74104 +charset-normalizer==2.0.12 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From c883aec7116f3474c3ad8b11fcc4757095289117 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 28 Jun 2022 18:21:54 +0200 Subject: [PATCH 1886/3516] Bump pynetgear to 0.10.6 (#74123) --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index f65f5aa6686..5fd59faac83 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.10.4"], + "requirements": ["pynetgear==0.10.6"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index b15ce59108e..ecf268c8b1d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1673,7 +1673,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.10.4 +pynetgear==0.10.6 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a1a8604659e..820e6134b82 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1134,7 +1134,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.10.4 +pynetgear==0.10.6 # homeassistant.components.nina pynina==0.1.8 From 26a85c6644991f626ccce62c05665095c2577234 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 28 Jun 2022 18:38:05 +0200 Subject: [PATCH 1887/3516] Add Entity.has_entity_name attribute (#73217) --- .../components/config/entity_registry.py | 1 + homeassistant/helpers/entity.py | 34 ++++++++++- homeassistant/helpers/entity_platform.py | 25 +++++--- homeassistant/helpers/entity_registry.py | 19 +++++- tests/common.py | 5 ++ .../components/config/test_entity_registry.py | 8 +++ tests/helpers/test_entity.py | 60 +++++++++++++++++-- tests/helpers/test_entity_platform.py | 46 ++++++++++++++ tests/helpers/test_entity_registry.py | 6 ++ 9 files changed, 186 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 2bb585e12c6..e6b91ee5a50 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -218,6 +218,7 @@ def _entry_ext_dict(entry): data = _entry_dict(entry) data["capabilities"] = entry.capabilities data["device_class"] = entry.device_class + data["has_entity_name"] = entry.has_entity_name data["options"] = entry.options data["original_device_class"] = entry.original_device_class data["original_icon"] = entry.original_icon diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 39af16892f5..f00f7d85e76 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -38,7 +38,7 @@ from homeassistant.exceptions import HomeAssistantError, NoEntitySpecifiedError from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util, ensure_unique_string, slugify -from . import entity_registry as er +from . import device_registry as dr, entity_registry as er from .device_registry import DeviceEntryType from .entity_platform import EntityPlatform from .event import async_track_entity_registry_updated_event @@ -221,6 +221,7 @@ class EntityDescription: entity_registry_visible_default: bool = True force_update: bool = False icon: str | None = None + has_entity_name: bool = False name: str | None = None unit_of_measurement: str | None = None @@ -277,6 +278,7 @@ class Entity(ABC): _attr_device_class: str | None _attr_device_info: DeviceInfo | None = None _attr_entity_category: EntityCategory | None + _attr_has_entity_name: bool _attr_entity_picture: str | None = None _attr_entity_registry_enabled_default: bool _attr_entity_registry_visible_default: bool @@ -303,6 +305,15 @@ class Entity(ABC): """Return a unique ID.""" return self._attr_unique_id + @property + def has_entity_name(self) -> bool: + """Return if the name of the entity is describing only the entity itself.""" + if hasattr(self, "_attr_has_entity_name"): + return self._attr_has_entity_name + if hasattr(self, "entity_description"): + return self.entity_description.has_entity_name + return False + @property def name(self) -> str | None: """Return the name of the entity.""" @@ -583,7 +594,26 @@ class Entity(ABC): if (icon := (entry and entry.icon) or self.icon) is not None: attr[ATTR_ICON] = icon - if (name := (entry and entry.name) or self.name) is not None: + def friendly_name() -> str | None: + """Return the friendly name. + + If has_entity_name is False, this returns self.name + If has_entity_name is True, this returns device.name + self.name + """ + if not self.has_entity_name or not self.registry_entry: + return self.name + + device_registry = dr.async_get(self.hass) + if not (device_id := self.registry_entry.device_id) or not ( + device_entry := device_registry.async_get(device_id) + ): + return self.name + + if not self.name: + return device_entry.name_by_user or device_entry.name + return f"{device_entry.name_by_user or device_entry.name} {self.name}" + + if (name := (entry and entry.name) or friendly_name()) is not None: attr[ATTR_FRIENDLY_NAME] = name if (supported_features := self.supported_features) is not None: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index ecf2125962a..ec71778af12 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -440,15 +440,6 @@ class EntityPlatform: # Get entity_id from unique ID registration if entity.unique_id is not None: - if entity.entity_id is not None: - requested_entity_id = entity.entity_id - suggested_object_id = split_entity_id(entity.entity_id)[1] - else: - suggested_object_id = entity.name # type: ignore[unreachable] - - if self.entity_namespace is not None: - suggested_object_id = f"{self.entity_namespace} {suggested_object_id}" - if self.config_entry is not None: config_entry_id: str | None = self.config_entry.entry_id else: @@ -503,6 +494,22 @@ class EntityPlatform: except RequiredParameterMissing: pass + if entity.entity_id is not None: + requested_entity_id = entity.entity_id + suggested_object_id = split_entity_id(entity.entity_id)[1] + else: + if device and entity.has_entity_name: # type: ignore[unreachable] + device_name = device.name_by_user or device.name + if not entity.name: + suggested_object_id = device_name + else: + suggested_object_id = f"{device_name} {entity.name}" + if not suggested_object_id: + suggested_object_id = entity.name + + if self.entity_namespace is not None: + suggested_object_id = f"{self.entity_namespace} {suggested_object_id}" + disabled_by: RegistryEntryDisabler | None = None if not entity.entity_registry_enabled_default: disabled_by = RegistryEntryDisabler.INTEGRATION diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index eb5590b7fdf..ff38a48da75 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -60,7 +60,7 @@ SAVE_DELAY = 10 _LOGGER = logging.getLogger(__name__) STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 6 +STORAGE_VERSION_MINOR = 7 STORAGE_KEY = "core.entity_registry" # Attributes relevant to describing entity @@ -111,6 +111,7 @@ class RegistryEntry: hidden_by: RegistryEntryHider | None = attr.ib(default=None) icon: str | None = attr.ib(default=None) id: str = attr.ib(factory=uuid_util.random_uuid_hex) + has_entity_name: bool = attr.ib(default=False) name: str | None = attr.ib(default=None) options: Mapping[str, Mapping[str, Any]] = attr.ib( default=None, converter=attr.converters.default_if_none(factory=dict) # type: ignore[misc] @@ -328,6 +329,7 @@ class EntityRegistry: config_entry: ConfigEntry | None = None, device_id: str | None = None, entity_category: EntityCategory | None = None, + has_entity_name: bool | None = None, original_device_class: str | None = None, original_icon: str | None = None, original_name: str | None = None, @@ -349,6 +351,9 @@ class EntityRegistry: config_entry_id=config_entry_id or UNDEFINED, device_id=device_id or UNDEFINED, entity_category=entity_category or UNDEFINED, + has_entity_name=has_entity_name + if has_entity_name is not None + else UNDEFINED, original_device_class=original_device_class or UNDEFINED, original_icon=original_icon or UNDEFINED, original_name=original_name or UNDEFINED, @@ -393,6 +398,7 @@ class EntityRegistry: entity_category=entity_category, entity_id=entity_id, hidden_by=hidden_by, + has_entity_name=has_entity_name or False, original_device_class=original_device_class, original_icon=original_icon, original_name=original_name, @@ -499,6 +505,7 @@ class EntityRegistry: entity_category: EntityCategory | None | UndefinedType = UNDEFINED, hidden_by: RegistryEntryHider | None | UndefinedType = UNDEFINED, icon: str | None | UndefinedType = UNDEFINED, + has_entity_name: bool | UndefinedType = UNDEFINED, name: str | None | UndefinedType = UNDEFINED, new_entity_id: str | UndefinedType = UNDEFINED, new_unique_id: str | UndefinedType = UNDEFINED, @@ -548,6 +555,7 @@ class EntityRegistry: ("entity_category", entity_category), ("hidden_by", hidden_by), ("icon", icon), + ("has_entity_name", has_entity_name), ("name", name), ("original_device_class", original_device_class), ("original_icon", original_icon), @@ -621,6 +629,7 @@ class EntityRegistry: entity_category: EntityCategory | None | UndefinedType = UNDEFINED, hidden_by: RegistryEntryHider | None | UndefinedType = UNDEFINED, icon: str | None | UndefinedType = UNDEFINED, + has_entity_name: bool | UndefinedType = UNDEFINED, name: str | None | UndefinedType = UNDEFINED, new_entity_id: str | UndefinedType = UNDEFINED, new_unique_id: str | UndefinedType = UNDEFINED, @@ -642,6 +651,7 @@ class EntityRegistry: entity_category=entity_category, hidden_by=hidden_by, icon=icon, + has_entity_name=has_entity_name, name=name, new_entity_id=new_entity_id, new_unique_id=new_unique_id, @@ -742,6 +752,7 @@ class EntityRegistry: else None, icon=entity["icon"], id=entity["id"], + has_entity_name=entity["has_entity_name"], name=entity["name"], options=entity["options"], original_device_class=entity["original_device_class"], @@ -778,6 +789,7 @@ class EntityRegistry: "hidden_by": entry.hidden_by, "icon": entry.icon, "id": entry.id, + "has_entity_name": entry.has_entity_name, "name": entry.name, "options": entry.options, "original_device_class": entry.original_device_class, @@ -944,6 +956,11 @@ async def _async_migrate( for entity in data["entities"]: entity["hidden_by"] = None + if old_major_version == 1 and old_minor_version < 7: + # Version 1.6 adds has_entity_name + for entity in data["entities"]: + entity["has_entity_name"] = False + if old_major_version > 1: raise NotImplementedError return data diff --git a/tests/common.py b/tests/common.py index 1a29d0d6dc4..80f0913cace 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1007,6 +1007,11 @@ class MockEntity(entity.Entity): """Return the entity category.""" return self._handle("entity_category") + @property + def has_entity_name(self): + """Return the has_entity_name name flag.""" + return self._handle("has_entity_name") + @property def entity_registry_enabled_default(self): """Return if the entity should be enabled when first added to the entity registry.""" diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index e74e43de701..69744817a27 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -117,6 +117,7 @@ async def test_get_entity(hass, client): "entity_id": "test_domain.name", "hidden_by": None, "icon": None, + "has_entity_name": False, "name": "Hello World", "options": {}, "original_device_class": None, @@ -146,6 +147,7 @@ async def test_get_entity(hass, client): "entity_id": "test_domain.no_name", "hidden_by": None, "icon": None, + "has_entity_name": False, "name": None, "options": {}, "original_device_class": None, @@ -208,6 +210,7 @@ async def test_update_entity(hass, client): "entity_id": "test_domain.world", "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", + "has_entity_name": False, "name": "after update", "options": {}, "original_device_class": None, @@ -279,6 +282,7 @@ async def test_update_entity(hass, client): "entity_id": "test_domain.world", "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", + "has_entity_name": False, "name": "after update", "options": {}, "original_device_class": None, @@ -315,6 +319,7 @@ async def test_update_entity(hass, client): "entity_id": "test_domain.world", "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", + "has_entity_name": False, "name": "after update", "options": {"sensor": {"unit_of_measurement": "beard_second"}}, "original_device_class": None, @@ -373,6 +378,7 @@ async def test_update_entity_require_restart(hass, client): "entity_id": "test_domain.world", "icon": None, "hidden_by": None, + "has_entity_name": False, "name": None, "options": {}, "original_device_class": None, @@ -479,6 +485,7 @@ async def test_update_entity_no_changes(hass, client): "entity_id": "test_domain.world", "hidden_by": None, "icon": None, + "has_entity_name": False, "name": "name of entity", "options": {}, "original_device_class": None, @@ -564,6 +571,7 @@ async def test_update_entity_id(hass, client): "entity_id": "test_domain.planet", "hidden_by": None, "icon": None, + "has_entity_name": False, "name": None, "options": {}, "original_device_class": None, diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 7141c5f0903..b9067a3db1c 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -12,16 +12,18 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import Context, HomeAssistantError -from homeassistant.helpers import entity, entity_registry +from homeassistant.helpers import device_registry as dr, entity, entity_registry as er from tests.common import ( MockConfigEntry, MockEntity, MockEntityPlatform, + MockPlatform, get_test_home_assistant, mock_registry, ) @@ -594,11 +596,11 @@ async def test_set_context_expired(hass): async def test_warn_disabled(hass, caplog): """Test we warn once if we write to a disabled entity.""" - entry = entity_registry.RegistryEntry( + entry = er.RegistryEntry( entity_id="hello.world", unique_id="test-unique-id", platform="test-platform", - disabled_by=entity_registry.RegistryEntryDisabler.USER, + disabled_by=er.RegistryEntryDisabler.USER, ) mock_registry(hass, {"hello.world": entry}) @@ -621,7 +623,7 @@ async def test_warn_disabled(hass, caplog): async def test_disabled_in_entity_registry(hass): """Test entity is removed if we disable entity registry entry.""" - entry = entity_registry.RegistryEntry( + entry = er.RegistryEntry( entity_id="hello.world", unique_id="test-unique-id", platform="test-platform", @@ -640,7 +642,7 @@ async def test_disabled_in_entity_registry(hass): assert hass.states.get("hello.world") is not None entry2 = registry.async_update_entity( - "hello.world", disabled_by=entity_registry.RegistryEntryDisabler.USER + "hello.world", disabled_by=er.RegistryEntryDisabler.USER ) await hass.async_block_till_done() assert entry2 != entry @@ -749,7 +751,7 @@ async def test_setup_source(hass): async def test_removing_entity_unavailable(hass): """Test removing an entity that is still registered creates an unavailable state.""" - entry = entity_registry.RegistryEntry( + entry = er.RegistryEntry( entity_id="hello.world", unique_id="test-unique-id", platform="test-platform", @@ -886,3 +888,49 @@ async def test_entity_description_fallback(): continue assert getattr(ent, field.name) == getattr(ent_with_description, field.name) + + +@pytest.mark.parametrize( + "has_entity_name, entity_name, expected_friendly_name", + ( + (False, "Entity Blu", "Entity Blu"), + (False, None, None), + (True, "Entity Blu", "Device Bla Entity Blu"), + (True, None, "Device Bla"), + ), +) +async def test_friendly_name( + hass, has_entity_name, entity_name, expected_friendly_name +): + """Test entity_id is influenced by entity name.""" + + async def async_setup_entry(hass, config_entry, async_add_entities): + """Mock setup entry method.""" + async_add_entities( + [ + MockEntity( + unique_id="qwer", + device_info={ + "identifiers": {("hue", "1234")}, + "connections": {(dr.CONNECTION_NETWORK_MAC, "abcd")}, + "name": "Device Bla", + }, + has_entity_name=has_entity_name, + name=entity_name, + ), + ] + ) + 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 + state = hass.states.async_all()[0] + assert state.attributes.get(ATTR_FRIENDLY_NAME) == expected_friendly_name diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 933669ebc53..80a37f9f2fd 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1392,3 +1392,49 @@ class SlowEntity(MockEntity): """Make sure control is returned to the event loop on add.""" await asyncio.sleep(0.1) await super().async_added_to_hass() + + +@pytest.mark.parametrize( + "has_entity_name, entity_name, expected_entity_id", + ( + (False, "Entity Blu", "test_domain.entity_blu"), + (False, None, "test_domain.test_qwer"), # Set to _ + (True, "Entity Blu", "test_domain.device_bla_entity_blu"), + (True, None, "test_domain.device_bla"), + ), +) +async def test_entity_name_influences_entity_id( + hass, has_entity_name, entity_name, expected_entity_id +): + """Test entity_id is influenced by entity name.""" + registry = er.async_get(hass) + + async def async_setup_entry(hass, config_entry, async_add_entities): + """Mock setup entry method.""" + async_add_entities( + [ + MockEntity( + unique_id="qwer", + device_info={ + "identifiers": {("hue", "1234")}, + "connections": {(dr.CONNECTION_NETWORK_MAC, "abcd")}, + "name": "Device Bla", + }, + has_entity_name=has_entity_name, + name=entity_name, + ), + ] + ) + 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 + assert registry.async_get(expected_entity_id) is not None diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 8f5b4a7d333..ba69a98d5a8 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -80,6 +80,7 @@ def test_get_or_create_updates_data(registry): disabled_by=er.RegistryEntryDisabler.HASS, entity_category=EntityCategory.CONFIG, hidden_by=er.RegistryEntryHider.INTEGRATION, + has_entity_name=True, original_device_class="mock-device-class", original_icon="initial-original_icon", original_name="initial-original_name", @@ -101,6 +102,7 @@ def test_get_or_create_updates_data(registry): hidden_by=er.RegistryEntryHider.INTEGRATION, icon=None, id=orig_entry.id, + has_entity_name=True, name=None, original_device_class="mock-device-class", original_icon="initial-original_icon", @@ -122,6 +124,7 @@ def test_get_or_create_updates_data(registry): disabled_by=er.RegistryEntryDisabler.USER, entity_category=None, hidden_by=er.RegistryEntryHider.USER, + has_entity_name=False, original_device_class="new-mock-device-class", original_icon="updated-original_icon", original_name="updated-original_name", @@ -143,6 +146,7 @@ def test_get_or_create_updates_data(registry): hidden_by=er.RegistryEntryHider.INTEGRATION, # Should not be updated icon=None, id=orig_entry.id, + has_entity_name=False, name=None, original_device_class="new-mock-device-class", original_icon="updated-original_icon", @@ -196,6 +200,7 @@ async def test_loading_saving_data(hass, registry): disabled_by=er.RegistryEntryDisabler.HASS, entity_category=EntityCategory.CONFIG, hidden_by=er.RegistryEntryHider.INTEGRATION, + has_entity_name=True, original_device_class="mock-device-class", original_icon="hass:original-icon", original_name="Original Name", @@ -237,6 +242,7 @@ async def test_loading_saving_data(hass, registry): assert new_entry2.entity_category == "config" assert new_entry2.icon == "hass:user-icon" assert new_entry2.hidden_by == er.RegistryEntryHider.INTEGRATION + assert new_entry2.has_entity_name is True assert new_entry2.name == "User Name" assert new_entry2.options == {"light": {"minimum_brightness": 20}} assert new_entry2.original_device_class == "mock-device-class" From 9fef1004a27df4102f6dc0c40e090b6c7a3b9209 Mon Sep 17 00:00:00 2001 From: Bryton Hall Date: Tue, 28 Jun 2022 12:38:30 -0400 Subject: [PATCH 1888/3516] Bump venstarcolortouch to 0.16 (#73038) --- homeassistant/components/venstar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index 42a97020fa3..e63c75792bf 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -3,7 +3,7 @@ "name": "Venstar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/venstar", - "requirements": ["venstarcolortouch==0.15"], + "requirements": ["venstarcolortouch==0.16"], "codeowners": ["@garbled1"], "iot_class": "local_polling", "loggers": ["venstarcolortouch"] diff --git a/requirements_all.txt b/requirements_all.txt index ecf268c8b1d..d0e96e0e0e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2387,7 +2387,7 @@ vehicle==0.4.0 velbus-aio==2022.6.1 # homeassistant.components.venstar -venstarcolortouch==0.15 +venstarcolortouch==0.16 # homeassistant.components.vilfo vilfo-api-client==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 820e6134b82..903c9479b24 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1584,7 +1584,7 @@ vehicle==0.4.0 velbus-aio==2022.6.1 # homeassistant.components.venstar -venstarcolortouch==0.15 +venstarcolortouch==0.16 # homeassistant.components.vilfo vilfo-api-client==0.3.2 From 040ece76ab690bf10af5490a921bc61db3f43d6b Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Tue, 28 Jun 2022 18:41:29 +0200 Subject: [PATCH 1889/3516] Add velbus buttons platform (#73323) --- .coveragerc | 1 + homeassistant/components/velbus/__init__.py | 1 + homeassistant/components/velbus/button.py | 42 +++++++++++++++++++ homeassistant/components/velbus/manifest.json | 2 +- homeassistant/components/velbus/sensor.py | 4 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/velbus/button.py diff --git a/.coveragerc b/.coveragerc index 928f4d7789e..8253ed56ccd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1361,6 +1361,7 @@ omit = homeassistant/components/vasttrafik/sensor.py homeassistant/components/velbus/__init__.py homeassistant/components/velbus/binary_sensor.py + homeassistant/components/velbus/button.py homeassistant/components/velbus/climate.py homeassistant/components/velbus/const.py homeassistant/components/velbus/cover.py diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index d2aa9531467..9b5a52306d8 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -28,6 +28,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [ Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.CLIMATE, Platform.COVER, Platform.LIGHT, diff --git a/homeassistant/components/velbus/button.py b/homeassistant/components/velbus/button.py new file mode 100644 index 00000000000..189cfb495e4 --- /dev/null +++ b/homeassistant/components/velbus/button.py @@ -0,0 +1,42 @@ +"""Support for Velbus Buttons.""" +from __future__ import annotations + +from velbusaio.channels import ( + Button as VelbusaioButton, + ButtonCounter as VelbusaioButtonCounter, +) + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import VelbusEntity +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Velbus switch based on config_entry.""" + await hass.data[DOMAIN][entry.entry_id]["tsk"] + cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] + entities = [] + for channel in cntrl.get_all("button"): + entities.append(VelbusButton(channel)) + async_add_entities(entities) + + +class VelbusButton(VelbusEntity, ButtonEntity): + """Representation of a Velbus Binary Sensor.""" + + _channel: VelbusaioButton | VelbusaioButtonCounter + _attr_entity_registry_enabled_default = False + _attr_entity_category = EntityCategory.CONFIG + + async def async_press(self) -> None: + """Handle the button press.""" + await self._channel.press() diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index e627412a00e..ec0c0f5f2d9 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==2022.6.1"], + "requirements": ["velbus-aio==2022.6.2"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "dependencies": ["usb"], diff --git a/homeassistant/components/velbus/sensor.py b/homeassistant/components/velbus/sensor.py index 86e9a606d36..a0bd9b6c173 100644 --- a/homeassistant/components/velbus/sensor.py +++ b/homeassistant/components/velbus/sensor.py @@ -53,9 +53,9 @@ class VelbusSensor(VelbusEntity, SensorEntity): self._attr_name = f"{self._attr_name}-counter" # define the device class if self._is_counter: - self._attr_device_class = SensorDeviceClass.ENERGY - elif channel.is_counter_channel(): self._attr_device_class = SensorDeviceClass.POWER + elif channel.is_counter_channel(): + self._attr_device_class = SensorDeviceClass.ENERGY elif channel.is_temperature(): self._attr_device_class = SensorDeviceClass.TEMPERATURE # define the icon diff --git a/requirements_all.txt b/requirements_all.txt index d0e96e0e0e1..318511f800d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2384,7 +2384,7 @@ vallox-websocket-api==2.11.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.6.1 +velbus-aio==2022.6.2 # homeassistant.components.venstar venstarcolortouch==0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 903c9479b24..3b8e17fcb21 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1581,7 +1581,7 @@ vallox-websocket-api==2.11.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.6.1 +velbus-aio==2022.6.2 # homeassistant.components.venstar venstarcolortouch==0.16 From a8349a4866d22cddbca9ac9367d4affae39a8325 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Jun 2022 11:42:51 -0500 Subject: [PATCH 1890/3516] Adjust entity filters to make includes stronger than excludes (#74080) * Adjust entity filters to make includes stronger than excludes Fixes #59080 * adjust test for stronger entity glob includes * sync with docs --- homeassistant/components/recorder/filters.py | 57 ++++---- homeassistant/helpers/entityfilter.py | 74 ++++++----- tests/components/apache_kafka/test_init.py | 2 +- tests/components/azure_event_hub/test_init.py | 2 +- tests/components/google_pubsub/test_init.py | 2 +- tests/components/history/test_init.py | 12 +- tests/components/influxdb/test_init.py | 2 +- tests/components/logbook/test_init.py | 3 +- .../test_filters_with_entityfilter.py | 125 ++++++++++++++++++ tests/helpers/test_entityfilter.py | 120 ++++++++++++++--- 10 files changed, 312 insertions(+), 87 deletions(-) diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 02c342441a7..45db64e0097 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -139,49 +139,52 @@ class Filters: have_exclude = self._have_exclude have_include = self._have_include - # Case 1 - no includes or excludes - pass all entities + # Case 1 - No filter + # - All entities included if not have_include and not have_exclude: return None - # Case 2 - includes, no excludes - only include specified entities + # Case 2 - Only includes + # - Entity listed in entities include: include + # - Otherwise, entity matches domain include: include + # - Otherwise, entity matches glob include: include + # - Otherwise: exclude if have_include and not have_exclude: return or_(*includes).self_group() - # Case 3 - excludes, no includes - only exclude specified entities + # Case 3 - Only excludes + # - Entity listed in exclude: exclude + # - Otherwise, entity matches domain exclude: exclude + # - Otherwise, entity matches glob exclude: exclude + # - Otherwise: include if not have_include and have_exclude: return not_(or_(*excludes).self_group()) - # Case 4 - both includes and excludes specified - # Case 4a - include domain or glob specified - # - if domain is included, pass if entity not excluded - # - if glob is included, pass if entity and domain not excluded - # - if domain and glob are not included, pass if entity is included - # note: if both include domain matches then exclude domains ignored. - # If glob matches then exclude domains and glob checked + # Case 4 - Domain and/or glob includes (may also have excludes) + # - Entity listed in entities include: include + # - Otherwise, entity listed in entities exclude: exclude + # - Otherwise, entity matches glob include: include + # - Otherwise, entity matches glob exclude: exclude + # - Otherwise, entity matches domain include: include + # - Otherwise: exclude if self.included_domains or self.included_entity_globs: return or_( - (i_domains & ~(e_entities | e_entity_globs)), - ( - ~i_domains - & or_( - (i_entity_globs & ~(or_(*excludes))), - (~i_entity_globs & i_entities), - ) - ), + i_entities, + (~e_entities & (i_entity_globs | (~e_entity_globs & i_domains))), ).self_group() - # Case 4b - exclude domain or glob specified, include has no domain or glob - # In this one case the traditional include logic is inverted. Even though an - # include is specified since its only a list of entity IDs its used only to - # expose specific entities excluded by domain or glob. Any entities not - # excluded are then presumed included. Logic is as follows - # - if domain or glob is excluded, pass if entity is included - # - if domain is not excluded, pass if entity not excluded by ID + # Case 5 - Domain and/or glob excludes (no domain and/or glob includes) + # - Entity listed in entities include: include + # - Otherwise, entity listed in exclude: exclude + # - Otherwise, entity matches glob exclude: exclude + # - Otherwise, entity matches domain exclude: exclude + # - Otherwise: include if self.excluded_domains or self.excluded_entity_globs: return (not_(or_(*excludes)) | i_entities).self_group() - # Case 4c - neither include or exclude domain specified - # - Only pass if entity is included. Ignore entity excludes. + # Case 6 - No Domain and/or glob includes or excludes + # - Entity listed in entities include: include + # - Otherwise: exclude return i_entities def states_entity_filter(self) -> ClauseList: diff --git a/homeassistant/helpers/entityfilter.py b/homeassistant/helpers/entityfilter.py index d4722eeca44..109c5454cc2 100644 --- a/homeassistant/helpers/entityfilter.py +++ b/homeassistant/helpers/entityfilter.py @@ -145,11 +145,7 @@ def _glob_to_re(glob: str) -> re.Pattern[str]: def _test_against_patterns(patterns: list[re.Pattern[str]], entity_id: str) -> bool: """Test entity against list of patterns, true if any match.""" - for pattern in patterns: - if pattern.match(entity_id): - return True - - return False + return any(pattern.match(entity_id) for pattern in patterns) def _convert_globs_to_pattern_list(globs: list[str] | None) -> list[re.Pattern[str]]: @@ -193,7 +189,7 @@ def _generate_filter_from_sets_and_pattern_lists( return ( entity_id in include_e or domain in include_d - or bool(include_eg and _test_against_patterns(include_eg, entity_id)) + or _test_against_patterns(include_eg, entity_id) ) def entity_excluded(domain: str, entity_id: str) -> bool: @@ -201,14 +197,19 @@ def _generate_filter_from_sets_and_pattern_lists( return ( entity_id in exclude_e or domain in exclude_d - or bool(exclude_eg and _test_against_patterns(exclude_eg, entity_id)) + or _test_against_patterns(exclude_eg, entity_id) ) - # Case 1 - no includes or excludes - pass all entities + # Case 1 - No filter + # - All entities included if not have_include and not have_exclude: return lambda entity_id: True - # Case 2 - includes, no excludes - only include specified entities + # Case 2 - Only includes + # - Entity listed in entities include: include + # - Otherwise, entity matches domain include: include + # - Otherwise, entity matches glob include: include + # - Otherwise: exclude if have_include and not have_exclude: def entity_filter_2(entity_id: str) -> bool: @@ -218,7 +219,11 @@ def _generate_filter_from_sets_and_pattern_lists( return entity_filter_2 - # Case 3 - excludes, no includes - only exclude specified entities + # Case 3 - Only excludes + # - Entity listed in exclude: exclude + # - Otherwise, entity matches domain exclude: exclude + # - Otherwise, entity matches glob exclude: exclude + # - Otherwise: include if not have_include and have_exclude: def entity_filter_3(entity_id: str) -> bool: @@ -228,38 +233,36 @@ def _generate_filter_from_sets_and_pattern_lists( return entity_filter_3 - # Case 4 - both includes and excludes specified - # Case 4a - include domain or glob specified - # - if domain is included, pass if entity not excluded - # - if glob is included, pass if entity and domain not excluded - # - if domain and glob are not included, pass if entity is included - # note: if both include domain matches then exclude domains ignored. - # If glob matches then exclude domains and glob checked + # Case 4 - Domain and/or glob includes (may also have excludes) + # - Entity listed in entities include: include + # - Otherwise, entity listed in entities exclude: exclude + # - Otherwise, entity matches glob include: include + # - Otherwise, entity matches glob exclude: exclude + # - Otherwise, entity matches domain include: include + # - Otherwise: exclude if include_d or include_eg: def entity_filter_4a(entity_id: str) -> bool: """Return filter function for case 4a.""" - domain = split_entity_id(entity_id)[0] - if domain in include_d: - return not ( - entity_id in exclude_e - or bool( - exclude_eg and _test_against_patterns(exclude_eg, entity_id) + return entity_id in include_e or ( + entity_id not in exclude_e + and ( + _test_against_patterns(include_eg, entity_id) + or ( + split_entity_id(entity_id)[0] in include_d + and not _test_against_patterns(exclude_eg, entity_id) ) ) - if _test_against_patterns(include_eg, entity_id): - return not entity_excluded(domain, entity_id) - return entity_id in include_e + ) return entity_filter_4a - # Case 4b - exclude domain or glob specified, include has no domain or glob - # In this one case the traditional include logic is inverted. Even though an - # include is specified since its only a list of entity IDs its used only to - # expose specific entities excluded by domain or glob. Any entities not - # excluded are then presumed included. Logic is as follows - # - if domain or glob is excluded, pass if entity is included - # - if domain is not excluded, pass if entity not excluded by ID + # Case 5 - Domain and/or glob excludes (no domain and/or glob includes) + # - Entity listed in entities include: include + # - Otherwise, entity listed in exclude: exclude + # - Otherwise, entity matches glob exclude: exclude + # - Otherwise, entity matches domain exclude: exclude + # - Otherwise: include if exclude_d or exclude_eg: def entity_filter_4b(entity_id: str) -> bool: @@ -273,6 +276,7 @@ def _generate_filter_from_sets_and_pattern_lists( return entity_filter_4b - # Case 4c - neither include or exclude domain specified - # - Only pass if entity is included. Ignore entity excludes. + # Case 6 - No Domain and/or glob includes or excludes + # - Entity listed in entities include: include + # - Otherwise: exclude return lambda entity_id: entity_id in include_e diff --git a/tests/components/apache_kafka/test_init.py b/tests/components/apache_kafka/test_init.py index 3f594b3fce3..9f5fa2800bc 100644 --- a/tests/components/apache_kafka/test_init.py +++ b/tests/components/apache_kafka/test_init.py @@ -169,7 +169,7 @@ async def test_filtered_allowlist(hass, mock_client): FilterTest("light.excluded_test", False), FilterTest("light.excluded", False), FilterTest("sensor.included_test", True), - FilterTest("climate.included_test", False), + FilterTest("climate.included_test", True), ] await _run_filter_tests(hass, tests, mock_client) diff --git a/tests/components/azure_event_hub/test_init.py b/tests/components/azure_event_hub/test_init.py index cf7226e20b0..c1393483c8c 100644 --- a/tests/components/azure_event_hub/test_init.py +++ b/tests/components/azure_event_hub/test_init.py @@ -176,7 +176,7 @@ async def test_full_batch(hass, entry_with_one_event, mock_create_batch): FilterTest("light.excluded_test", 0), FilterTest("light.excluded", 0), FilterTest("sensor.included_test", 1), - FilterTest("climate.included_test", 0), + FilterTest("climate.included_test", 1), ], ), ( diff --git a/tests/components/google_pubsub/test_init.py b/tests/components/google_pubsub/test_init.py index 1b6d1dbf4b4..71fab923972 100644 --- a/tests/components/google_pubsub/test_init.py +++ b/tests/components/google_pubsub/test_init.py @@ -222,7 +222,7 @@ async def test_filtered_allowlist(hass, mock_client): FilterTest("light.excluded_test", False), FilterTest("light.excluded", False), FilterTest("sensor.included_test", True), - FilterTest("climate.included_test", False), + FilterTest("climate.included_test", True), ] for test in tests: diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 56ac68d944d..5eb4894c72a 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -753,7 +753,7 @@ async def test_fetch_period_api_with_entity_glob_include_and_exclude( { "history": { "exclude": { - "entity_globs": ["light.many*"], + "entity_globs": ["light.many*", "binary_sensor.*"], }, "include": { "entity_globs": ["light.m*"], @@ -769,6 +769,7 @@ async def test_fetch_period_api_with_entity_glob_include_and_exclude( hass.states.async_set("light.many_state_changes", "on") hass.states.async_set("switch.match", "on") hass.states.async_set("media_player.test", "on") + hass.states.async_set("binary_sensor.exclude", "on") await async_wait_recording_done(hass) @@ -778,10 +779,11 @@ async def test_fetch_period_api_with_entity_glob_include_and_exclude( ) assert response.status == HTTPStatus.OK response_json = await response.json() - assert len(response_json) == 3 - assert response_json[0][0]["entity_id"] == "light.match" - assert response_json[1][0]["entity_id"] == "media_player.test" - assert response_json[2][0]["entity_id"] == "switch.match" + assert len(response_json) == 4 + assert response_json[0][0]["entity_id"] == "light.many_state_changes" + assert response_json[1][0]["entity_id"] == "light.match" + assert response_json[2][0]["entity_id"] == "media_player.test" + assert response_json[3][0]["entity_id"] == "switch.match" async def test_entity_ids_limit_via_api(hass, hass_client, recorder_mock): diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index 94071e849c2..27b9ac82ade 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -866,7 +866,7 @@ async def test_event_listener_filtered_allowlist( FilterTest("fake.excluded", False), FilterTest("another_fake.denied", False), FilterTest("fake.excluded_entity", False), - FilterTest("another_fake.included_entity", False), + FilterTest("another_fake.included_entity", True), ] execute_filter_test(hass, tests, handler_method, write_api, get_mock_call) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index d16b3476d84..31ca1610250 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -2153,7 +2153,7 @@ async def test_include_exclude_events_with_glob_filters( client = await hass_client() entries = await _async_fetch_logbook(client) - assert len(entries) == 6 + assert len(entries) == 7 _assert_entry( entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN ) @@ -2162,6 +2162,7 @@ async def test_include_exclude_events_with_glob_filters( _assert_entry(entries[3], name="bla", entity_id=entity_id, state="20") _assert_entry(entries[4], name="blu", entity_id=entity_id2, state="20") _assert_entry(entries[5], name="included", entity_id=entity_id4, state="30") + _assert_entry(entries[6], name="included", entity_id=entity_id5, state="30") async def test_empty_config(hass, hass_client, recorder_mock): diff --git a/tests/components/recorder/test_filters_with_entityfilter.py b/tests/components/recorder/test_filters_with_entityfilter.py index ed4d4efe066..62bb1b3fa8d 100644 --- a/tests/components/recorder/test_filters_with_entityfilter.py +++ b/tests/components/recorder/test_filters_with_entityfilter.py @@ -514,3 +514,128 @@ async def test_same_entity_included_excluded_include_domain_wins(hass, recorder_ assert filtered_events_entity_ids == filter_accept assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_specificly_included_entity_always_wins(hass, recorder_mock): + """Test specificlly included entity always wins.""" + filter_accept = { + "media_player.test2", + "media_player.test3", + "thermostat.test", + "binary_sensor.specific_include", + } + filter_reject = { + "binary_sensor.test2", + "binary_sensor.home", + "binary_sensor.can_cancel_this_one", + } + conf = { + CONF_INCLUDE: { + CONF_ENTITIES: ["binary_sensor.specific_include"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["binary_sensor"], + CONF_ENTITY_GLOBS: ["binary_sensor.*"], + }, + } + + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) + + +async def test_specificly_included_entity_always_wins_over_glob(hass, recorder_mock): + """Test specificlly included entity always wins over a glob.""" + filter_accept = { + "sensor.apc900va_status", + "sensor.apc900va_battery_charge", + "sensor.apc900va_battery_runtime", + "sensor.apc900va_load", + "sensor.energy_x", + } + filter_reject = { + "sensor.apc900va_not_included", + } + conf = { + CONF_EXCLUDE: { + CONF_DOMAINS: [ + "updater", + "camera", + "group", + "media_player", + "script", + "sun", + "automation", + "zone", + "weblink", + "scene", + "calendar", + "weather", + "remote", + "notify", + "switch", + "shell_command", + "media_player", + ], + CONF_ENTITY_GLOBS: ["sensor.apc900va_*"], + }, + CONF_INCLUDE: { + CONF_DOMAINS: [ + "binary_sensor", + "climate", + "device_tracker", + "input_boolean", + "sensor", + ], + CONF_ENTITY_GLOBS: ["sensor.energy_*"], + CONF_ENTITIES: [ + "sensor.apc900va_status", + "sensor.apc900va_battery_charge", + "sensor.apc900va_battery_runtime", + "sensor.apc900va_load", + ], + }, + } + extracted_filter = extract_include_exclude_filter_conf(conf) + entity_filter = convert_include_exclude_filter(extracted_filter) + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(extracted_filter) + assert sqlalchemy_filter is not None + + for entity_id in filter_accept: + assert entity_filter(entity_id) is True + + for entity_id in filter_reject: + assert entity_filter(entity_id) is False + + ( + filtered_states_entity_ids, + filtered_events_entity_ids, + ) = await _async_get_states_and_events_with_filter( + hass, sqlalchemy_filter, filter_accept | filter_reject + ) + + assert filtered_states_entity_ids == filter_accept + assert not filtered_states_entity_ids.intersection(filter_reject) + + assert filtered_events_entity_ids == filter_accept + assert not filtered_events_entity_ids.intersection(filter_reject) diff --git a/tests/helpers/test_entityfilter.py b/tests/helpers/test_entityfilter.py index 043fb44a95a..f9d7ca47b4c 100644 --- a/tests/helpers/test_entityfilter.py +++ b/tests/helpers/test_entityfilter.py @@ -91,8 +91,8 @@ def test_excludes_only_with_glob_case_3(): assert testfilter("cover.garage_door") -def test_with_include_domain_case4a(): - """Test case 4a - include and exclude specified, with included domain.""" +def test_with_include_domain_case4(): + """Test case 4 - include and exclude specified, with included domain.""" incl_dom = {"light", "sensor"} incl_ent = {"binary_sensor.working"} excl_dom = {} @@ -108,8 +108,30 @@ def test_with_include_domain_case4a(): assert testfilter("sun.sun") is False -def test_with_include_glob_case4a(): - """Test case 4a - include and exclude specified, with included glob.""" +def test_with_include_domain_exclude_glob_case4(): + """Test case 4 - include and exclude specified, with included domain but excluded by glob.""" + incl_dom = {"light", "sensor"} + incl_ent = {"binary_sensor.working"} + incl_glob = {} + excl_dom = {} + excl_ent = {"light.ignoreme", "sensor.notworking"} + excl_glob = {"sensor.busted"} + testfilter = generate_filter( + incl_dom, incl_ent, excl_dom, excl_ent, incl_glob, excl_glob + ) + + assert testfilter("sensor.test") + assert testfilter("sensor.busted") is False + assert testfilter("sensor.notworking") is False + assert testfilter("light.test") + assert testfilter("light.ignoreme") is False + assert testfilter("binary_sensor.working") + assert testfilter("binary_sensor.another") is False + assert testfilter("sun.sun") is False + + +def test_with_include_glob_case4(): + """Test case 4 - include and exclude specified, with included glob.""" incl_dom = {} incl_glob = {"light.*", "sensor.*"} incl_ent = {"binary_sensor.working"} @@ -129,8 +151,8 @@ def test_with_include_glob_case4a(): assert testfilter("sun.sun") is False -def test_with_include_domain_glob_filtering_case4a(): - """Test case 4a - include and exclude specified, both have domains and globs.""" +def test_with_include_domain_glob_filtering_case4(): + """Test case 4 - include and exclude specified, both have domains and globs.""" incl_dom = {"light"} incl_glob = {"*working"} incl_ent = {} @@ -142,17 +164,64 @@ def test_with_include_domain_glob_filtering_case4a(): ) assert testfilter("sensor.working") - assert testfilter("sensor.notworking") is False + assert testfilter("sensor.notworking") is True # include is stronger assert testfilter("light.test") - assert testfilter("light.notworking") is False + assert testfilter("light.notworking") is True # include is stronger assert testfilter("light.ignoreme") is False - assert testfilter("binary_sensor.not_working") is False + assert testfilter("binary_sensor.not_working") is True # include is stronger assert testfilter("binary_sensor.another") is False assert testfilter("sun.sun") is False -def test_exclude_domain_case4b(): - """Test case 4b - include and exclude specified, with excluded domain.""" +def test_with_include_domain_glob_filtering_case4a_include_strong(): + """Test case 4 - include and exclude specified, both have domains and globs, and a specifically included entity.""" + incl_dom = {"light"} + incl_glob = {"*working"} + incl_ent = {"binary_sensor.specificly_included"} + excl_dom = {"binary_sensor"} + excl_glob = {"*notworking"} + excl_ent = {"light.ignoreme"} + testfilter = generate_filter( + incl_dom, incl_ent, excl_dom, excl_ent, incl_glob, excl_glob + ) + + assert testfilter("sensor.working") + assert testfilter("sensor.notworking") is True # iclude is stronger + assert testfilter("light.test") + assert testfilter("light.notworking") is True # iclude is stronger + assert testfilter("light.ignoreme") is False + assert testfilter("binary_sensor.not_working") is True # iclude is stronger + assert testfilter("binary_sensor.another") is False + assert testfilter("binary_sensor.specificly_included") is True + assert testfilter("sun.sun") is False + + +def test_with_include_glob_filtering_case4a_include_strong(): + """Test case 4 - include and exclude specified, both have globs, and a specifically included entity.""" + incl_dom = {} + incl_glob = {"*working"} + incl_ent = {"binary_sensor.specificly_included"} + excl_dom = {} + excl_glob = {"*broken", "*notworking", "binary_sensor.*"} + excl_ent = {"light.ignoreme"} + testfilter = generate_filter( + incl_dom, incl_ent, excl_dom, excl_ent, incl_glob, excl_glob + ) + + assert testfilter("sensor.working") is True + assert testfilter("sensor.notworking") is True # include is stronger + assert testfilter("sensor.broken") is False + assert testfilter("light.test") is False + assert testfilter("light.notworking") is True # include is stronger + assert testfilter("light.ignoreme") is False + assert testfilter("binary_sensor.not_working") is True # include is stronger + assert testfilter("binary_sensor.another") is False + assert testfilter("binary_sensor.specificly_included") is True + assert testfilter("sun.sun") is False + + +def test_exclude_domain_case5(): + """Test case 5 - include and exclude specified, with excluded domain.""" incl_dom = {} incl_ent = {"binary_sensor.working"} excl_dom = {"binary_sensor"} @@ -168,8 +237,8 @@ def test_exclude_domain_case4b(): assert testfilter("sun.sun") is True -def test_exclude_glob_case4b(): - """Test case 4b - include and exclude specified, with excluded glob.""" +def test_exclude_glob_case5(): + """Test case 5 - include and exclude specified, with excluded glob.""" incl_dom = {} incl_glob = {} incl_ent = {"binary_sensor.working"} @@ -189,8 +258,29 @@ def test_exclude_glob_case4b(): assert testfilter("sun.sun") is True -def test_no_domain_case4c(): - """Test case 4c - include and exclude specified, with no domains.""" +def test_exclude_glob_case5_include_strong(): + """Test case 5 - include and exclude specified, with excluded glob, and a specifically included entity.""" + incl_dom = {} + incl_glob = {} + incl_ent = {"binary_sensor.working"} + excl_dom = {"binary_sensor"} + excl_glob = {"binary_sensor.*"} + excl_ent = {"light.ignoreme", "sensor.notworking"} + testfilter = generate_filter( + incl_dom, incl_ent, excl_dom, excl_ent, incl_glob, excl_glob + ) + + assert testfilter("sensor.test") + assert testfilter("sensor.notworking") is False + assert testfilter("light.test") + assert testfilter("light.ignoreme") is False + assert testfilter("binary_sensor.working") + assert testfilter("binary_sensor.another") is False + assert testfilter("sun.sun") is True + + +def test_no_domain_case6(): + """Test case 6 - include and exclude specified, with no domains.""" incl_dom = {} incl_ent = {"binary_sensor.working"} excl_dom = {} From e0d2344db3cd805c399e3a4847f0ed0c4077ee16 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 19:31:33 +0200 Subject: [PATCH 1891/3516] Use attributes in manual_mqtt alarm (#74124) --- .../manual_mqtt/alarm_control_panel.py | 33 ++++++------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index 719e85bf16c..66c75b5ed0e 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -5,6 +5,7 @@ import copy import datetime import logging import re +from typing import Any import voluptuous as vol @@ -204,6 +205,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): A trigger_time of zero disables the alarm_trigger service. """ + _attr_should_poll = False _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY @@ -231,7 +233,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): """Init the manual MQTT alarm panel.""" self._state = STATE_ALARM_DISARMED self._hass = hass - self._name = name + self._attr_name = name if code_template: self._code = code_template self._code.hass = hass @@ -257,24 +259,14 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): self._state_topic = state_topic self._command_topic = command_topic self._qos = qos - self._code_arm_required = code_arm_required + self._attr_code_arm_required = code_arm_required self._payload_disarm = payload_disarm self._payload_arm_home = payload_arm_home self._payload_arm_away = payload_arm_away self._payload_arm_night = payload_arm_night @property - def should_poll(self): - """Return the polling state.""" - return False - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): + def state(self) -> str: """Return the state of the device.""" if self._state == STATE_ALARM_TRIGGERED: if self._within_pending_time(self._state): @@ -314,7 +306,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): return self._state_ts + self._pending_time(state) > dt_util.utcnow() @property - def code_format(self): + def code_format(self) -> alarm.CodeFormat | None: """Return one or more digits/characters.""" if self._code is None: return None @@ -322,11 +314,6 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): return alarm.CodeFormat.NUMBER return alarm.CodeFormat.TEXT - @property - def code_arm_required(self): - """Whether the code is required for arm actions.""" - return self._code_arm_required - def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if not self._validate_code(code, STATE_ALARM_DISARMED): @@ -338,7 +325,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): def alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" - if self._code_arm_required and not self._validate_code( + if self.code_arm_required and not self._validate_code( code, STATE_ALARM_ARMED_HOME ): return @@ -347,7 +334,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): def alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" - if self._code_arm_required and not self._validate_code( + if self.code_arm_required and not self._validate_code( code, STATE_ALARM_ARMED_AWAY ): return @@ -356,7 +343,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): def alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" - if self._code_arm_required and not self._validate_code( + if self.code_arm_required and not self._validate_code( code, STATE_ALARM_ARMED_NIGHT ): return @@ -417,7 +404,7 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): return check @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" if self.state != STATE_ALARM_PENDING: return {} From c4ad5aa68ae181c1166953f921c5df631a5b85dc Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 28 Jun 2022 13:11:29 -0600 Subject: [PATCH 1892/3516] Bump simplisafe-python to 2022.06.1 (#74142) --- 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 4da8f09eff8..a09c273076c 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==2022.06.0"], + "requirements": ["simplisafe-python==2022.06.1"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 318511f800d..a9644498d50 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2168,7 +2168,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.06.0 +simplisafe-python==2022.06.1 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b8e17fcb21..32ff0c21c48 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1437,7 +1437,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.06.0 +simplisafe-python==2022.06.1 # homeassistant.components.slack slackclient==2.5.0 From abe44a100feaf4df5fe0120fea6bba4b512da64d Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 28 Jun 2022 16:02:16 -0400 Subject: [PATCH 1893/3516] Bump all of ZHA's zigpy dependencies (#73964) Bump zigpy and radio library versions --- homeassistant/components/zha/core/gateway.py | 2 +- homeassistant/components/zha/manifest.json | 14 +++++++------- requirements_all.txt | 14 +++++++------- requirements_test_all.txt | 14 +++++++------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index b9465d5e2aa..099abfe5e88 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -687,7 +687,7 @@ class ZHAGateway: _LOGGER.debug("Shutting down ZHA ControllerApplication") for unsubscribe in self._unsubs: unsubscribe() - await self.application_controller.pre_shutdown() + await self.application_controller.shutdown() def handle_message( self, diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 71131f240a9..78aea7c9fb6 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,15 +4,15 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.30.0", + "bellows==0.31.0", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.75", - "zigpy-deconz==0.16.0", - "zigpy==0.46.0", - "zigpy-xbee==0.14.0", - "zigpy-zigate==0.7.4", - "zigpy-znp==0.7.0" + "zha-quirks==0.0.77", + "zigpy-deconz==0.18.0", + "zigpy==0.47.2", + "zigpy-xbee==0.15.0", + "zigpy-zigate==0.9.0", + "zigpy-znp==0.8.0" ], "usb": [ { diff --git a/requirements_all.txt b/requirements_all.txt index a9644498d50..3421120eafa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -390,7 +390,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.30.0 +bellows==0.31.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.9.6 @@ -2498,7 +2498,7 @@ zengge==0.2 zeroconf==0.38.7 # homeassistant.components.zha -zha-quirks==0.0.75 +zha-quirks==0.0.77 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2507,19 +2507,19 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.16.0 +zigpy-deconz==0.18.0 # homeassistant.components.zha -zigpy-xbee==0.14.0 +zigpy-xbee==0.15.0 # homeassistant.components.zha -zigpy-zigate==0.7.4 +zigpy-zigate==0.9.0 # homeassistant.components.zha -zigpy-znp==0.7.0 +zigpy-znp==0.8.0 # homeassistant.components.zha -zigpy==0.46.0 +zigpy==0.47.2 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 32ff0c21c48..b18aad6e18b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -305,7 +305,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.30.0 +bellows==0.31.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.9.6 @@ -1659,22 +1659,22 @@ youless-api==0.16 zeroconf==0.38.7 # homeassistant.components.zha -zha-quirks==0.0.75 +zha-quirks==0.0.77 # homeassistant.components.zha -zigpy-deconz==0.16.0 +zigpy-deconz==0.18.0 # homeassistant.components.zha -zigpy-xbee==0.14.0 +zigpy-xbee==0.15.0 # homeassistant.components.zha -zigpy-zigate==0.7.4 +zigpy-zigate==0.9.0 # homeassistant.components.zha -zigpy-znp==0.7.0 +zigpy-znp==0.8.0 # homeassistant.components.zha -zigpy==0.46.0 +zigpy==0.47.2 # homeassistant.components.zwave_js zwave-js-server-python==0.39.0 From 4bfdb1433e95dfe504e376ca082def5257c23bcb Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 28 Jun 2022 15:19:27 -0500 Subject: [PATCH 1894/3516] Optimize Sonos unjoin behavior when using `media_player.unjoin` (#74086) * Coalesce Sonos unjoins to process together * Refactor for readability * Skip unjoin call if already ungrouped * Store unjoin data in a dedicated dataclass * Revert import adjustment --- homeassistant/components/sonos/__init__.py | 10 +++++++ .../components/sonos/media_player.py | 30 ++++++++++++++++--- homeassistant/components/sonos/speaker.py | 2 ++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 2e2290dff04..e3f65d754dd 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections import OrderedDict +from dataclasses import dataclass, field import datetime from functools import partial import logging @@ -74,6 +75,14 @@ CONFIG_SCHEMA = vol.Schema( ) +@dataclass +class UnjoinData: + """Class to track data necessary for unjoin coalescing.""" + + speakers: list[SonosSpeaker] + event: asyncio.Event = field(default_factory=asyncio.Event) + + class SonosData: """Storage class for platform global data.""" @@ -89,6 +98,7 @@ class SonosData: self.boot_counts: dict[str, int] = {} self.mdns_names: dict[str, str] = {} self.entity_id_mappings: dict[str, SonosSpeaker] = {} + self.unjoin_data: dict[str, UnjoinData] = {} async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 0f766f33e6f..7b18b102919 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -44,8 +44,9 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform, service from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_call_later -from . import media_browser +from . import UnjoinData, media_browser from .const import ( DATA_SONOS, DOMAIN as SONOS_DOMAIN, @@ -777,6 +778,27 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): await self.hass.async_add_executor_job(self.speaker.join, speakers) async def async_unjoin_player(self): - """Remove this player from any group.""" - async with self.hass.data[DATA_SONOS].topology_condition: - await self.hass.async_add_executor_job(self.speaker.unjoin) + """Remove this player from any group. + + Coalesces all calls within 0.5s to allow use of SonosSpeaker.unjoin_multi() + which optimizes the order in which speakers are removed from their groups. + Removing coordinators last better preserves playqueues on the speakers. + """ + sonos_data = self.hass.data[DATA_SONOS] + household_id = self.speaker.household_id + + async def async_process_unjoin(now: datetime.datetime) -> None: + """Process the unjoin with all remove requests within the coalescing period.""" + unjoin_data = sonos_data.unjoin_data.pop(household_id) + await SonosSpeaker.unjoin_multi(self.hass, unjoin_data.speakers) + unjoin_data.event.set() + + if unjoin_data := sonos_data.unjoin_data.get(household_id): + unjoin_data.speakers.append(self.speaker) + else: + unjoin_data = sonos_data.unjoin_data[household_id] = UnjoinData( + speakers=[self.speaker] + ) + async_call_later(self.hass, 0.5, async_process_unjoin) + + await unjoin_data.event.wait() diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 729d1a1457f..f4d1d89aa2f 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -906,6 +906,8 @@ class SonosSpeaker: @soco_error() def unjoin(self) -> None: """Unjoin the player from a group.""" + if self.sonos_group == [self]: + return self.soco.unjoin() self.coordinator = None From 1d185388a9c9bb1967dd1ca091ccb6f298885fd4 Mon Sep 17 00:00:00 2001 From: Stefan Rado Date: Tue, 28 Jun 2022 22:22:53 +0200 Subject: [PATCH 1895/3516] Bump homeconnect to 0.7.1 (#74130) --- homeassistant/components/home_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index 0a055c971c5..ca6e0f012ac 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/home_connect", "dependencies": ["application_credentials"], "codeowners": ["@DavidMStraub"], - "requirements": ["homeconnect==0.7.0"], + "requirements": ["homeconnect==0.7.1"], "config_flow": true, "iot_class": "cloud_push", "loggers": ["homeconnect"] diff --git a/requirements_all.txt b/requirements_all.txt index 3421120eafa..a30fd52a2a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -831,7 +831,7 @@ holidays==0.14.2 home-assistant-frontend==20220624.0 # homeassistant.components.home_connect -homeconnect==0.7.0 +homeconnect==0.7.1 # homeassistant.components.homematicip_cloud homematicip==1.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b18aad6e18b..3be3fc1fda1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -598,7 +598,7 @@ holidays==0.14.2 home-assistant-frontend==20220624.0 # homeassistant.components.home_connect -homeconnect==0.7.0 +homeconnect==0.7.1 # homeassistant.components.homematicip_cloud homematicip==1.0.2 From 02d16763015c56a9973c7aa8f6c68a05a7e3c7a9 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 28 Jun 2022 13:29:11 -0700 Subject: [PATCH 1896/3516] Fix unexpected exception in Google Calendar OAuth exchange (#73963) --- homeassistant/components/google/api.py | 81 ++++++++++++------- .../components/google/config_flow.py | 8 +- tests/components/google/test_config_flow.py | 50 +++++++----- 3 files changed, 86 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/google/api.py b/homeassistant/components/google/api.py index f4a4912a1b9..47aa32dcd11 100644 --- a/homeassistant/components/google/api.py +++ b/homeassistant/components/google/api.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable import datetime import logging from typing import Any, cast @@ -19,9 +18,12 @@ from oauth2client.client import ( from homeassistant.components.application_credentials import AuthImplementation from homeassistant.config_entries import ConfigEntry -from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import config_entry_oauth2_flow -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.event import ( + async_track_point_in_utc_time, + async_track_time_interval, +) from homeassistant.util import dt from .const import ( @@ -76,6 +78,9 @@ class DeviceFlow: self._oauth_flow = oauth_flow self._device_flow_info: DeviceFlowInfo = device_flow_info self._exchange_task_unsub: CALLBACK_TYPE | None = None + self._timeout_unsub: CALLBACK_TYPE | None = None + self._listener: CALLBACK_TYPE | None = None + self._creds: Credentials | None = None @property def verification_url(self) -> str: @@ -87,15 +92,22 @@ class DeviceFlow: """Return the code that the user should enter at the verification url.""" return self._device_flow_info.user_code # type: ignore[no-any-return] - async def start_exchange_task( - self, finished_cb: Callable[[Credentials | None], Awaitable[None]] + @callback + def async_set_listener( + self, + update_callback: CALLBACK_TYPE, ) -> None: - """Start the device auth exchange flow polling. + """Invoke the update callback when the exchange finishes or on timeout.""" + self._listener = update_callback - The callback is invoked with the valid credentials or with None on timeout. - """ + @property + def creds(self) -> Credentials | None: + """Return result of exchange step or None on timeout.""" + return self._creds + + def async_start_exchange(self) -> None: + """Start the device auth exchange flow polling.""" _LOGGER.debug("Starting exchange flow") - assert not self._exchange_task_unsub max_timeout = dt.utcnow() + datetime.timedelta(seconds=EXCHANGE_TIMEOUT_SECONDS) # For some reason, oauth.step1_get_device_and_user_codes() returns a datetime # object without tzinfo. For the comparison below to work, it needs one. @@ -104,31 +116,40 @@ class DeviceFlow: ) expiration_time = min(user_code_expiry, max_timeout) - def _exchange() -> Credentials: - return self._oauth_flow.step2_exchange( - device_flow_info=self._device_flow_info - ) - - async def _poll_attempt(now: datetime.datetime) -> None: - assert self._exchange_task_unsub - _LOGGER.debug("Attempting OAuth code exchange") - # Note: The callback is invoked with None when the device code has expired - creds: Credentials | None = None - if now < expiration_time: - try: - creds = await self._hass.async_add_executor_job(_exchange) - except FlowExchangeError: - _LOGGER.debug("Token not yet ready; trying again later") - return - self._exchange_task_unsub() - self._exchange_task_unsub = None - await finished_cb(creds) - self._exchange_task_unsub = async_track_time_interval( self._hass, - _poll_attempt, + self._async_poll_attempt, datetime.timedelta(seconds=self._device_flow_info.interval), ) + self._timeout_unsub = async_track_point_in_utc_time( + self._hass, self._async_timeout, expiration_time + ) + + async def _async_poll_attempt(self, now: datetime.datetime) -> None: + _LOGGER.debug("Attempting OAuth code exchange") + try: + self._creds = await self._hass.async_add_executor_job(self._exchange) + except FlowExchangeError: + _LOGGER.debug("Token not yet ready; trying again later") + return + self._finish() + + def _exchange(self) -> Credentials: + return self._oauth_flow.step2_exchange(device_flow_info=self._device_flow_info) + + @callback + def _async_timeout(self, now: datetime.datetime) -> None: + _LOGGER.debug("OAuth token exchange timeout") + self._finish() + + @callback + def _finish(self) -> None: + if self._exchange_task_unsub: + self._exchange_task_unsub() + if self._timeout_unsub: + self._timeout_unsub() + if self._listener: + self._listener() def get_feature_access( diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index cbe1de69f9e..22b62094e76 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -7,7 +7,6 @@ from typing import Any from gcal_sync.api import GoogleCalendarService from gcal_sync.exceptions import ApiException -from oauth2client.client import Credentials import voluptuous as vol from homeassistant import config_entries @@ -97,9 +96,9 @@ class OAuth2FlowHandler( return self.async_abort(reason="oauth_error") self._device_flow = device_flow - async def _exchange_finished(creds: Credentials | None) -> None: + def _exchange_finished() -> None: self.external_data = { - DEVICE_AUTH_CREDS: creds + DEVICE_AUTH_CREDS: device_flow.creds } # is None on timeout/expiration self.hass.async_create_task( self.hass.config_entries.flow.async_configure( @@ -107,7 +106,8 @@ class OAuth2FlowHandler( ) ) - await device_flow.start_exchange_task(_exchange_finished) + device_flow.async_set_listener(_exchange_finished) + device_flow.async_start_exchange() return self.async_show_progress( step_id="auth", diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index 00f50e129e4..24ad8a7b769 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -10,6 +10,7 @@ from unittest.mock import Mock, patch from aiohttp.client_exceptions import ClientError from freezegun.api import FrozenDateTimeFactory from oauth2client.client import ( + DeviceFlowInfo, FlowExchangeError, OAuth2Credentials, OAuth2DeviceCodeError, @@ -59,10 +60,17 @@ async def mock_code_flow( ) -> YieldFixture[Mock]: """Fixture for initiating OAuth flow.""" with patch( - "oauth2client.client.OAuth2WebServerFlow.step1_get_device_and_user_codes", + "homeassistant.components.google.api.OAuth2WebServerFlow.step1_get_device_and_user_codes", ) as mock_flow: - mock_flow.return_value.user_code_expiry = utcnow() + code_expiration_delta - mock_flow.return_value.interval = CODE_CHECK_INTERVAL + mock_flow.return_value = DeviceFlowInfo.FromResponse( + { + "device_code": "4/4-GMMhmHCXhWEzkobqIHGG_EnNYYsAkukHspeYUk9E8", + "user_code": "GQVQ-JKEC", + "verification_url": "https://www.google.com/device", + "expires_in": code_expiration_delta.total_seconds(), + "interval": CODE_CHECK_INTERVAL, + } + ) yield mock_flow @@ -70,7 +78,8 @@ async def mock_code_flow( async def mock_exchange(creds: OAuth2Credentials) -> YieldFixture[Mock]: """Fixture for mocking out the exchange for credentials.""" with patch( - "oauth2client.client.OAuth2WebServerFlow.step2_exchange", return_value=creds + "homeassistant.components.google.api.OAuth2WebServerFlow.step2_exchange", + return_value=creds, ) as mock: yield mock @@ -108,7 +117,6 @@ async def fire_alarm(hass, point_in_time): await hass.async_block_till_done() -@pytest.mark.freeze_time("2022-06-03 15:19:59-00:00") async def test_full_flow_yaml_creds( hass: HomeAssistant, mock_code_flow: Mock, @@ -131,9 +139,8 @@ async def test_full_flow_yaml_creds( "homeassistant.components.google.async_setup_entry", return_value=True ) as mock_setup: # Run one tick to invoke the credential exchange check - freezer.tick(CODE_CHECK_ALARM_TIMEDELTA) - await fire_alarm(hass, datetime.datetime.utcnow()) - await hass.async_block_till_done() + now = utcnow() + await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) result = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"] ) @@ -143,11 +150,12 @@ async def test_full_flow_yaml_creds( assert "data" in result data = result["data"] assert "token" in data + assert 0 < data["token"]["expires_in"] <= 60 * 60 assert ( - data["token"]["expires_in"] - == 60 * 60 - CODE_CHECK_ALARM_TIMEDELTA.total_seconds() + datetime.datetime.now().timestamp() + <= data["token"]["expires_at"] + < (datetime.datetime.now() + datetime.timedelta(days=8)).timestamp() ) - assert data["token"]["expires_at"] == 1654273199.0 data["token"].pop("expires_at") data["token"].pop("expires_in") assert data == { @@ -238,7 +246,7 @@ async def test_code_error( assert await component_setup() with patch( - "oauth2client.client.OAuth2WebServerFlow.step1_get_device_and_user_codes", + "homeassistant.components.google.api.OAuth2WebServerFlow.step1_get_device_and_user_codes", side_effect=OAuth2DeviceCodeError("Test Failure"), ): result = await hass.config_entries.flow.async_init( @@ -248,13 +256,13 @@ async def test_code_error( assert result.get("reason") == "oauth_error" -@pytest.mark.parametrize("code_expiration_delta", [datetime.timedelta(minutes=-5)]) +@pytest.mark.parametrize("code_expiration_delta", [datetime.timedelta(seconds=50)]) async def test_expired_after_exchange( hass: HomeAssistant, mock_code_flow: Mock, component_setup: ComponentSetup, ) -> None: - """Test successful creds setup.""" + """Test credential exchange expires.""" assert await component_setup() result = await hass.config_entries.flow.async_init( @@ -265,10 +273,14 @@ async def test_expired_after_exchange( assert "description_placeholders" in result assert "url" in result["description_placeholders"] - # Run one tick to invoke the credential exchange check - now = utcnow() - await fire_alarm(hass, now + CODE_CHECK_ALARM_TIMEDELTA) - await hass.async_block_till_done() + # Fail first attempt then advance clock past exchange timeout + with patch( + "homeassistant.components.google.api.OAuth2WebServerFlow.step2_exchange", + side_effect=FlowExchangeError(), + ): + now = utcnow() + await fire_alarm(hass, now + datetime.timedelta(seconds=65)) + await hass.async_block_till_done() result = await hass.config_entries.flow.async_configure(flow_id=result["flow_id"]) assert result.get("type") == "abort" @@ -295,7 +307,7 @@ async def test_exchange_error( # Run one tick to invoke the credential exchange check now = utcnow() with patch( - "oauth2client.client.OAuth2WebServerFlow.step2_exchange", + "homeassistant.components.google.api.OAuth2WebServerFlow.step2_exchange", side_effect=FlowExchangeError(), ): now += CODE_CHECK_ALARM_TIMEDELTA From a284ebe771c7930e578678ce0ee9b44cdcd81a88 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Tue, 28 Jun 2022 22:39:37 +0200 Subject: [PATCH 1897/3516] Add support for Atlantic Electrical Towel Dryer to Overkiz integration (#73788) --- .../overkiz/climate_entities/__init__.py | 2 + .../atlantic_electrical_towel_dryer.py | 122 ++++++++++++++++++ homeassistant/components/overkiz/const.py | 1 + 3 files changed, 125 insertions(+) create mode 100644 homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py diff --git a/homeassistant/components/overkiz/climate_entities/__init__.py b/homeassistant/components/overkiz/climate_entities/__init__.py index d44aa85d167..737ea342c40 100644 --- a/homeassistant/components/overkiz/climate_entities/__init__.py +++ b/homeassistant/components/overkiz/climate_entities/__init__.py @@ -2,11 +2,13 @@ from pyoverkiz.enums.ui import UIWidget from .atlantic_electrical_heater import AtlanticElectricalHeater +from .atlantic_electrical_towel_dryer import AtlanticElectricalTowelDryer from .atlantic_pass_apc_zone_control import AtlanticPassAPCZoneControl from .somfy_thermostat import SomfyThermostat WIDGET_TO_CLIMATE_ENTITY = { UIWidget.ATLANTIC_ELECTRICAL_HEATER: AtlanticElectricalHeater, + UIWidget.ATLANTIC_ELECTRICAL_TOWEL_DRYER: AtlanticElectricalTowelDryer, UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: AtlanticPassAPCZoneControl, UIWidget.SOMFY_THERMOSTAT: SomfyThermostat, } diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py new file mode 100644 index 00000000000..9a24a7bf1a9 --- /dev/null +++ b/homeassistant/components/overkiz/climate_entities/atlantic_electrical_towel_dryer.py @@ -0,0 +1,122 @@ +"""Support for Atlantic Electrical Towel Dryer.""" +from __future__ import annotations + +from typing import Any, cast + +from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + PRESET_BOOST, + PRESET_NONE, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.components.overkiz.coordinator import OverkizDataUpdateCoordinator +from homeassistant.components.overkiz.entity import OverkizEntity +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS + +PRESET_DRYING = "drying" + +OVERKIZ_TO_HVAC_MODE: dict[str, str] = { + OverkizCommandParam.EXTERNAL: HVACMode.HEAT, # manu + OverkizCommandParam.INTERNAL: HVACMode.AUTO, # prog + OverkizCommandParam.STANDBY: HVACMode.OFF, +} +HVAC_MODE_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_HVAC_MODE.items()} + +OVERKIZ_TO_PRESET_MODE: dict[str, str] = { + OverkizCommandParam.PERMANENT_HEATING: PRESET_NONE, + OverkizCommandParam.BOOST: PRESET_BOOST, + OverkizCommandParam.DRYING: PRESET_DRYING, +} + +PRESET_MODE_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_PRESET_MODE.items()} + +TEMPERATURE_SENSOR_DEVICE_INDEX = 7 + + +class AtlanticElectricalTowelDryer(OverkizEntity, ClimateEntity): + """Representation of Atlantic Electrical Towel Dryer.""" + + _attr_hvac_modes = [*HVAC_MODE_TO_OVERKIZ] + _attr_preset_modes = [*PRESET_MODE_TO_OVERKIZ] + _attr_temperature_unit = TEMP_CELSIUS + + def __init__( + self, device_url: str, coordinator: OverkizDataUpdateCoordinator + ) -> None: + """Init method.""" + super().__init__(device_url, coordinator) + self.temperature_device = self.executor.linked_device( + TEMPERATURE_SENSOR_DEVICE_INDEX + ) + + self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + + # Not all AtlanticElectricalTowelDryer models support presets, thus we need to check if the command is available + if self.executor.has_command(OverkizCommand.SET_TOWEL_DRYER_TEMPORARY_STATE): + self._attr_supported_features += ClimateEntityFeature.PRESET_MODE + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode.""" + if OverkizState.CORE_OPERATING_MODE in self.device.states: + return OVERKIZ_TO_HVAC_MODE[ + cast(str, self.executor.select_state(OverkizState.CORE_OPERATING_MODE)) + ] + + return HVACMode.OFF + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + await self.executor.async_execute_command( + OverkizCommand.SET_TOWEL_DRYER_OPERATING_MODE, + HVAC_MODE_TO_OVERKIZ[hvac_mode], + ) + + @property + def target_temperature(self) -> None: + """Return the temperature.""" + if self.hvac_mode == HVACMode.AUTO: + self.executor.select_state(OverkizState.IO_EFFECTIVE_TEMPERATURE_SETPOINT) + else: + self.executor.select_state(OverkizState.CORE_TARGET_TEMPERATURE) + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + if temperature := self.temperature_device.states[OverkizState.CORE_TEMPERATURE]: + return cast(float, temperature.value) + + return None + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new temperature.""" + temperature = kwargs[ATTR_TEMPERATURE] + + if self.hvac_mode == HVACMode.AUTO: + await self.executor.async_execute_command( + OverkizCommand.SET_DEROGATED_TARGET_TEMPERATURE, temperature + ) + else: + await self.executor.async_execute_command( + OverkizCommand.SET_TARGET_TEMPERATURE, temperature + ) + + @property + def preset_mode(self) -> str | None: + """Return the current preset mode, e.g., home, away, temp.""" + return OVERKIZ_TO_PRESET_MODE[ + cast( + str, + self.executor.select_state(OverkizState.IO_TOWEL_DRYER_TEMPORARY_STATE), + ) + ] + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + await self.executor.async_execute_command( + OverkizCommand.SET_TOWEL_DRYER_TEMPORARY_STATE, + PRESET_MODE_TO_OVERKIZ[preset_mode], + ) diff --git a/homeassistant/components/overkiz/const.py b/homeassistant/components/overkiz/const.py index 447ebc5ac42..9091cd35998 100644 --- a/homeassistant/components/overkiz/const.py +++ b/homeassistant/components/overkiz/const.py @@ -62,6 +62,7 @@ OVERKIZ_DEVICE_TO_PLATFORM: dict[UIClass | UIWidget, Platform | None] = { UIClass.WINDOW: Platform.COVER, UIWidget.ALARM_PANEL_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) UIWidget.ATLANTIC_ELECTRICAL_HEATER: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) + UIWidget.ATLANTIC_ELECTRICAL_TOWEL_DRYER: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.ATLANTIC_PASS_APC_ZONE_CONTROL: Platform.CLIMATE, # widgetName, uiClass is HeatingSystem (not supported) UIWidget.DOMESTIC_HOT_WATER_TANK: Platform.SWITCH, # widgetName, uiClass is WaterHeatingSystem (not supported) UIWidget.MY_FOX_ALARM_CONTROLLER: Platform.ALARM_CONTROL_PANEL, # widgetName, uiClass is Alarm (not supported) From abf67c3153e971a76ee6f4752d9f524a8fc5e822 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 28 Jun 2022 22:45:25 +0200 Subject: [PATCH 1898/3516] Normalize deCONZ binary sensor unique IDs (#73657) --- .../components/deconz/binary_sensor.py | 30 +++++++++++-- tests/components/deconz/test_binary_sensor.py | 43 +++++++++++++------ 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index d109fb8b34d..0d090751edd 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -27,8 +27,9 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.helpers.entity_registry as er -from .const import ATTR_DARK, ATTR_ON +from .const import ATTR_DARK, ATTR_ON, DOMAIN as DECONZ_DOMAIN from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry @@ -179,6 +180,27 @@ BINARY_SENSOR_DESCRIPTIONS = [ ] +@callback +def async_update_unique_id( + hass: HomeAssistant, unique_id: str, description: DeconzBinarySensorDescription +) -> None: + """Update unique ID to always have a suffix. + + Introduced with release 2022.7. + """ + ent_reg = er.async_get(hass) + + new_unique_id = f"{unique_id}-{description.key}" + if ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, new_unique_id): + return + + if description.suffix: + unique_id = f'{unique_id.split("-", 1)[0]}-{description.suffix.lower()}' + + if entity_id := ent_reg.async_get_entity_id(DOMAIN, DECONZ_DOMAIN, unique_id): + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -205,6 +227,8 @@ async def async_setup_entry( ): continue + async_update_unique_id(hass, sensor.unique_id, description) + async_add_entities([DeconzBinarySensor(sensor, gateway, description)]) gateway.register_platform_add_device_callback( @@ -255,9 +279,7 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): @property def unique_id(self) -> str: """Return a unique identifier for this device.""" - if self.entity_description.suffix: - return f"{self.serial}-{self.entity_description.suffix.lower()}" - return super().unique_id + return f"{super().unique_id}-{self.entity_description.key}" @callback def async_update_callback(self) -> None: diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index b6a21fb9fa6..ae62205c636 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 import pytest -from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDeviceClass from homeassistant.components.deconz.const import ( CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_NEW_DEVICES, @@ -63,7 +63,8 @@ TEST_DATA = [ "entity_count": 3, "device_count": 3, "entity_id": "binary_sensor.alarm_10", - "unique_id": "00:15:8d:00:02:b5:d1:80-01-0500", + "unique_id": "00:15:8d:00:02:b5:d1:80-01-0500-alarm", + "old_unique_id": "00:15:8d:00:02:b5:d1:80-01-0500", "state": STATE_OFF, "entity_category": None, "device_class": BinarySensorDeviceClass.SAFETY, @@ -104,7 +105,8 @@ TEST_DATA = [ "entity_count": 4, "device_count": 3, "entity_id": "binary_sensor.cave_co", - "unique_id": "00:15:8d:00:02:a5:21:24-01-0101", + "unique_id": "00:15:8d:00:02:a5:21:24-01-0101-carbon_monoxide", + "old_unique_id": "00:15:8d:00:02:a5:21:24-01-0101", "state": STATE_OFF, "entity_category": None, "device_class": BinarySensorDeviceClass.CO, @@ -139,7 +141,8 @@ TEST_DATA = [ "entity_count": 2, "device_count": 3, "entity_id": "binary_sensor.sensor_kitchen_smoke", - "unique_id": "00:15:8d:00:01:d9:3e:7c-01-0500", + "unique_id": "00:15:8d:00:01:d9:3e:7c-01-0500-fire", + "old_unique_id": "00:15:8d:00:01:d9:3e:7c-01-0500", "state": STATE_OFF, "entity_category": None, "device_class": BinarySensorDeviceClass.SMOKE, @@ -175,7 +178,8 @@ TEST_DATA = [ "entity_count": 2, "device_count": 3, "entity_id": "binary_sensor.sensor_kitchen_smoke_test_mode", - "unique_id": "00:15:8d:00:01:d9:3e:7c-test mode", + "unique_id": "00:15:8d:00:01:d9:3e:7c-01-0500-in_test_mode", + "old_unique_id": "00:15:8d:00:01:d9:3e:7c-test mode", "state": STATE_OFF, "entity_category": EntityCategory.DIAGNOSTIC, "device_class": BinarySensorDeviceClass.SMOKE, @@ -207,7 +211,8 @@ TEST_DATA = [ "entity_count": 1, "device_count": 2, "entity_id": "binary_sensor.kitchen_switch", - "unique_id": "kitchen-switch", + "unique_id": "kitchen-switch-flag", + "old_unique_id": "kitchen-switch", "state": STATE_ON, "entity_category": None, "device_class": None, @@ -244,7 +249,8 @@ TEST_DATA = [ "entity_count": 3, "device_count": 3, "entity_id": "binary_sensor.back_door", - "unique_id": "00:15:8d:00:02:2b:96:b4-01-0006", + "unique_id": "00:15:8d:00:02:2b:96:b4-01-0006-open", + "old_unique_id": "00:15:8d:00:02:2b:96:b4-01-0006", "state": STATE_OFF, "entity_category": None, "device_class": BinarySensorDeviceClass.OPENING, @@ -290,7 +296,8 @@ TEST_DATA = [ "entity_count": 3, "device_count": 3, "entity_id": "binary_sensor.motion_sensor_4", - "unique_id": "00:17:88:01:03:28:8c:9b-02-0406", + "unique_id": "00:17:88:01:03:28:8c:9b-02-0406-presence", + "old_unique_id": "00:17:88:01:03:28:8c:9b-02-0406", "state": STATE_OFF, "entity_category": None, "device_class": BinarySensorDeviceClass.MOTION, @@ -331,7 +338,8 @@ TEST_DATA = [ "entity_count": 5, "device_count": 3, "entity_id": "binary_sensor.water2", - "unique_id": "00:15:8d:00:02:2f:07:db-01-0500", + "unique_id": "00:15:8d:00:02:2f:07:db-01-0500-water", + "old_unique_id": "00:15:8d:00:02:2f:07:db-01-0500", "state": STATE_OFF, "entity_category": None, "device_class": BinarySensorDeviceClass.MOISTURE, @@ -376,7 +384,8 @@ TEST_DATA = [ "entity_count": 3, "device_count": 3, "entity_id": "binary_sensor.vibration_1", - "unique_id": "00:15:8d:00:02:a5:21:24-01-0101", + "unique_id": "00:15:8d:00:02:a5:21:24-01-0101-vibration", + "old_unique_id": "00:15:8d:00:02:a5:21:24-01-0101", "state": STATE_ON, "entity_category": None, "device_class": BinarySensorDeviceClass.VIBRATION, @@ -414,7 +423,8 @@ TEST_DATA = [ "entity_count": 4, "device_count": 3, "entity_id": "binary_sensor.presence_sensor_tampered", - "unique_id": "00:00:00:00:00:00:00:00-tampered", + "unique_id": "00:00:00:00:00:00:00:00-00-tampered", + "old_unique_id": "00:00:00:00:00:00:00:00-tampered", "state": STATE_OFF, "entity_category": EntityCategory.DIAGNOSTIC, "device_class": BinarySensorDeviceClass.TAMPER, @@ -447,7 +457,8 @@ TEST_DATA = [ "entity_count": 4, "device_count": 3, "entity_id": "binary_sensor.presence_sensor_low_battery", - "unique_id": "00:00:00:00:00:00:00:00-low battery", + "unique_id": "00:00:00:00:00:00:00:00-00-low_battery", + "old_unique_id": "00:00:00:00:00:00:00:00-low battery", "state": STATE_OFF, "entity_category": EntityCategory.DIAGNOSTIC, "device_class": BinarySensorDeviceClass.BATTERY, @@ -470,6 +481,14 @@ async def test_binary_sensors( ent_reg = er.async_get(hass) dev_reg = dr.async_get(hass) + # Create entity entry to migrate to new unique ID + ent_reg.async_get_or_create( + DOMAIN, + DECONZ_DOMAIN, + expected["old_unique_id"], + suggested_object_id=expected["entity_id"].replace(DOMAIN, ""), + ) + with patch.dict(DECONZ_WEB_REQUEST, {"sensors": {"1": sensor_data}}): config_entry = await setup_deconz_integration( hass, aioclient_mock, options={CONF_ALLOW_CLIP_SENSOR: True} From 146ff83a1612c8d8d3fe6641cefc1aeda439671a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 28 Jun 2022 22:53:38 +0200 Subject: [PATCH 1899/3516] Migrate rest binary_sensor and switch to TemplateEntity (#73307) --- .../components/rest/binary_sensor.py | 52 ++++---- homeassistant/components/rest/entity.py | 23 +--- homeassistant/components/rest/schema.py | 9 +- homeassistant/components/rest/sensor.py | 6 +- homeassistant/components/rest/switch.py | 118 +++++++----------- .../components/template/binary_sensor.py | 2 - homeassistant/helpers/template_entity.py | 4 +- tests/components/rest/test_binary_sensor.py | 39 ++++++ tests/components/rest/test_switch.py | 67 ++++++++-- 9 files changed, 178 insertions(+), 142 deletions(-) diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index 2beed53522a..bc51433c3c5 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -11,18 +11,20 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, - CONF_NAME, CONF_RESOURCE, CONF_RESOURCE_TEMPLATE, + CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.template_entity import TemplateEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import async_get_config_and_coordinator, create_rest_data_from_config +from .const import DEFAULT_BINARY_SENSOR_NAME from .entity import RestEntity from .schema import BINARY_SENSOR_SCHEMA, RESOURCE_SCHEMA @@ -57,51 +59,55 @@ async def async_setup_platform( raise PlatformNotReady from rest.last_exception raise PlatformNotReady - name = conf.get(CONF_NAME) - device_class = conf.get(CONF_DEVICE_CLASS) - value_template = conf.get(CONF_VALUE_TEMPLATE) - force_update = conf.get(CONF_FORCE_UPDATE) - resource_template = conf.get(CONF_RESOURCE_TEMPLATE) - - if value_template is not None: - value_template.hass = hass + unique_id = conf.get(CONF_UNIQUE_ID) async_add_entities( [ RestBinarySensor( + hass, coordinator, rest, - name, - device_class, - value_template, - force_update, - resource_template, + conf, + unique_id, ) ], ) -class RestBinarySensor(RestEntity, BinarySensorEntity): +class RestBinarySensor(RestEntity, TemplateEntity, BinarySensorEntity): """Representation of a REST binary sensor.""" def __init__( self, + hass, coordinator, rest, - name, - device_class, - value_template, - force_update, - resource_template, + config, + unique_id, ): """Initialize a REST binary sensor.""" - super().__init__(coordinator, rest, name, resource_template, force_update) + RestEntity.__init__( + self, + coordinator, + rest, + config.get(CONF_RESOURCE_TEMPLATE), + config.get(CONF_FORCE_UPDATE), + ) + TemplateEntity.__init__( + self, + hass, + config=config, + fallback_name=DEFAULT_BINARY_SENSOR_NAME, + unique_id=unique_id, + ) self._state = False self._previous_data = None - self._value_template = value_template + self._value_template = config.get(CONF_VALUE_TEMPLATE) + if (value_template := self._value_template) is not None: + value_template.hass = hass self._is_on = None - self._attr_device_class = device_class + self._attr_device_class = config.get(CONF_DEVICE_CLASS) @property def is_on(self): diff --git a/homeassistant/components/rest/entity.py b/homeassistant/components/rest/entity.py index f0476dc7d33..5d7a65b3d48 100644 --- a/homeassistant/components/rest/entity.py +++ b/homeassistant/components/rest/entity.py @@ -10,7 +10,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .data import RestData -class BaseRestEntity(Entity): +class RestEntity(Entity): """A class for entities using DataUpdateCoordinator or rest data directly.""" def __init__( @@ -72,24 +72,3 @@ class BaseRestEntity(Entity): @abstractmethod def _update_from_rest_data(self): """Update state from the rest data.""" - - -class RestEntity(BaseRestEntity): - """A class for entities using DataUpdateCoordinator or rest data directly.""" - - def __init__( - self, - coordinator: DataUpdateCoordinator[Any], - rest: RestData, - name, - resource_template, - force_update, - ) -> None: - """Create the entity that may have a coordinator.""" - self._name = name - super().__init__(coordinator, rest, resource_template, force_update) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name diff --git a/homeassistant/components/rest/schema.py b/homeassistant/components/rest/schema.py index d25bb50167b..f881dc8b028 100644 --- a/homeassistant/components/rest/schema.py +++ b/homeassistant/components/rest/schema.py @@ -13,7 +13,6 @@ from homeassistant.const import ( CONF_FORCE_UPDATE, CONF_HEADERS, CONF_METHOD, - CONF_NAME, CONF_PARAMS, CONF_PASSWORD, CONF_PAYLOAD, @@ -28,12 +27,14 @@ from homeassistant.const import ( HTTP_DIGEST_AUTHENTICATION, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.template_entity import TEMPLATE_SENSOR_BASE_SCHEMA +from homeassistant.helpers.template_entity import ( + TEMPLATE_ENTITY_BASE_SCHEMA, + TEMPLATE_SENSOR_BASE_SCHEMA, +) from .const import ( CONF_JSON_ATTRS, CONF_JSON_ATTRS_PATH, - DEFAULT_BINARY_SENSOR_NAME, DEFAULT_FORCE_UPDATE, DEFAULT_METHOD, DEFAULT_VERIFY_SSL, @@ -67,7 +68,7 @@ SENSOR_SCHEMA = { } BINARY_SENSOR_SCHEMA = { - vol.Optional(CONF_NAME, default=DEFAULT_BINARY_SENSOR_NAME): cv.string, + **TEMPLATE_ENTITY_BASE_SCHEMA.schema, vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 93a96aeb94f..ff571b0c9dc 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -31,7 +31,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import async_get_config_and_coordinator, create_rest_data_from_config from .const import CONF_JSON_ATTRS, CONF_JSON_ATTRS_PATH, DEFAULT_SENSOR_NAME -from .entity import BaseRestEntity +from .entity import RestEntity from .schema import RESOURCE_SCHEMA, SENSOR_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -82,7 +82,7 @@ async def async_setup_platform( ) -class RestSensor(BaseRestEntity, TemplateSensor): +class RestSensor(RestEntity, TemplateSensor): """Implementation of a REST sensor.""" def __init__( @@ -94,7 +94,7 @@ class RestSensor(BaseRestEntity, TemplateSensor): unique_id, ): """Initialize the REST sensor.""" - BaseRestEntity.__init__( + RestEntity.__init__( self, coordinator, rest, diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index 10214970cce..c45eb581645 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -18,11 +18,11 @@ from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_HEADERS, CONF_METHOD, - CONF_NAME, CONF_PARAMS, CONF_PASSWORD, CONF_RESOURCE, CONF_TIMEOUT, + CONF_UNIQUE_ID, CONF_USERNAME, CONF_VERIFY_SSL, ) @@ -30,6 +30,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.template_entity import ( + TEMPLATE_ENTITY_BASE_SCHEMA, + TemplateEntity, +) from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -49,6 +53,7 @@ SUPPORT_REST_METHODS = ["post", "put", "patch"] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { + **TEMPLATE_ENTITY_BASE_SCHEMA.schema, vol.Required(CONF_RESOURCE): cv.url, vol.Optional(CONF_STATE_RESOURCE): cv.url, vol.Optional(CONF_HEADERS): {cv.string: cv.template}, @@ -59,7 +64,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.All( 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, @@ -76,50 +80,11 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the RESTful switch.""" - body_off = config.get(CONF_BODY_OFF) - body_on = config.get(CONF_BODY_ON) - is_on_template = config.get(CONF_IS_ON_TEMPLATE) - method = config.get(CONF_METHOD) - 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) resource = config.get(CONF_RESOURCE) - state_resource = config.get(CONF_STATE_RESOURCE) or resource - verify_ssl = config.get(CONF_VERIFY_SSL) - - auth = None - if username: - auth = aiohttp.BasicAuth(username, password=config[CONF_PASSWORD]) - - if is_on_template is not None: - is_on_template.hass = hass - if body_on is not None: - body_on.hass = hass - if body_off is not None: - body_off.hass = hass - - template.attach(hass, headers) - template.attach(hass, params) - timeout = config.get(CONF_TIMEOUT) + unique_id = config.get(CONF_UNIQUE_ID) try: - switch = RestSwitch( - name, - device_class, - resource, - state_resource, - method, - headers, - params, - auth, - body_on, - body_off, - is_on_template, - timeout, - verify_ssl, - ) + switch = RestSwitch(hass, config, unique_id) req = await switch.get_device_state(hass) if req.status >= HTTPStatus.BAD_REQUEST: @@ -135,46 +100,53 @@ async def async_setup_platform( _LOGGER.error("No route to resource/endpoint: %s", resource) -class RestSwitch(SwitchEntity): +class RestSwitch(TemplateEntity, SwitchEntity): """Representation of a switch that can be toggled using REST.""" def __init__( self, - name, - device_class, - resource, - state_resource, - method, - headers, - params, - auth, - body_on, - body_off, - is_on_template, - timeout, - verify_ssl, + hass, + config, + unique_id, ): """Initialize the REST switch.""" + TemplateEntity.__init__( + self, + hass, + config=config, + fallback_name=DEFAULT_NAME, + unique_id=unique_id, + ) + self._state = None - self._name = name - self._resource = resource - self._state_resource = state_resource - self._method = method - self._headers = headers - self._params = params + + auth = None + if username := config.get(CONF_USERNAME): + auth = aiohttp.BasicAuth(username, password=config[CONF_PASSWORD]) + + self._resource = config.get(CONF_RESOURCE) + self._state_resource = config.get(CONF_STATE_RESOURCE) or self._resource + self._method = config.get(CONF_METHOD) + self._headers = config.get(CONF_HEADERS) + self._params = config.get(CONF_PARAMS) self._auth = auth - self._body_on = body_on - self._body_off = body_off - self._is_on_template = is_on_template - self._timeout = timeout - self._verify_ssl = verify_ssl + self._body_on = config.get(CONF_BODY_ON) + self._body_off = config.get(CONF_BODY_OFF) + self._is_on_template = config.get(CONF_IS_ON_TEMPLATE) + self._timeout = config.get(CONF_TIMEOUT) + self._verify_ssl = config.get(CONF_VERIFY_SSL) - self._attr_device_class = device_class + self._attr_device_class = config.get(CONF_DEVICE_CLASS) - @property - def name(self): - """Return the name of the switch.""" - return self._name + if (is_on_template := self._is_on_template) is not None: + is_on_template.hass = hass + if (body_on := self._body_on) is not None: + body_on.hass = hass + if (body_off := self._body_off) is not None: + body_off.hass = hass + + template.attach(hass, self._headers) + template.attach(hass, self._params) @property def is_on(self): diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index e91a2925b06..ab7c88e8b8c 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -82,9 +82,7 @@ BINARY_SENSOR_SCHEMA = vol.Schema( vol.Optional(CONF_DELAY_OFF): vol.Any(cv.positive_time_period, cv.template), vol.Optional(CONF_DELAY_ON): vol.Any(cv.positive_time_period, cv.template), vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_NAME): cv.template, vol.Required(CONF_STATE): cv.template, - vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } ).extend(TEMPLATE_ENTITY_COMMON_SCHEMA.schema) diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py index e92ba233121..83d321e3fa9 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -43,16 +43,16 @@ CONF_PICTURE = "picture" TEMPLATE_ENTITY_BASE_SCHEMA = vol.Schema( { vol.Optional(CONF_ICON): cv.template, + vol.Optional(CONF_NAME): cv.template, vol.Optional(CONF_PICTURE): cv.template, + vol.Optional(CONF_UNIQUE_ID): cv.string, } ) TEMPLATE_SENSOR_BASE_SCHEMA = vol.Schema( { vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_NAME): cv.template, vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, - vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } ).extend(TEMPLATE_ENTITY_BASE_SCHEMA.schema) diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 8383d53b51f..a6655f6ddbc 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -19,6 +19,8 @@ from homeassistant.const import ( STATE_UNAVAILABLE, Platform, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from tests.common import get_fixture_path @@ -431,3 +433,40 @@ async def test_setup_query_params(hass): ) await hass.async_block_till_done() assert len(hass.states.async_all("binary_sensor")) == 1 + + +@respx.mock +async def test_entity_config(hass: HomeAssistant) -> None: + """Test entity configuration.""" + + config = { + Platform.BINARY_SENSOR: { + # REST configuration + "platform": "rest", + "method": "GET", + "resource": "http://localhost", + # Entity configuration + "icon": "{{'mdi:one_two_three'}}", + "picture": "{{'blabla.png'}}", + "name": "{{'REST' + ' ' + 'Binary Sensor'}}", + "unique_id": "very_unique", + }, + } + + respx.get("http://localhost") % HTTPStatus.OK + assert await async_setup_component(hass, Platform.BINARY_SENSOR, config) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + assert ( + entity_registry.async_get("binary_sensor.rest_binary_sensor").unique_id + == "very_unique" + ) + + state = hass.states.get("binary_sensor.rest_binary_sensor") + assert state.state == "off" + assert state.attributes == { + "entity_picture": "blabla.png", + "friendly_name": "REST Binary Sensor", + "icon": "mdi:one_two_three", + } diff --git a/tests/components/rest/test_switch.py b/tests/components/rest/test_switch.py index 3dbef91ffb5..a3c0f78db1c 100644 --- a/tests/components/rest/test_switch.py +++ b/tests/components/rest/test_switch.py @@ -8,6 +8,7 @@ from homeassistant.components.rest import DOMAIN import homeassistant.components.rest.switch as rest from homeassistant.components.switch import SwitchDeviceClass from homeassistant.const import ( + CONF_DEVICE_CLASS, CONF_HEADERS, CONF_NAME, CONF_PARAMS, @@ -16,17 +17,19 @@ from homeassistant.const import ( CONTENT_TYPE_JSON, Platform, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component from tests.common import assert_setup_component +from tests.test_util.aiohttp import AiohttpClientMocker NAME = "foo" DEVICE_CLASS = SwitchDeviceClass.SWITCH METHOD = "post" RESOURCE = "http://localhost/" STATE_RESOURCE = RESOURCE -AUTH = None PARAMS = None @@ -187,19 +190,22 @@ def _setup_test_switch(hass): body_off = Template("off", hass) headers = {"Content-type": Template(CONTENT_TYPE_JSON, hass)} switch = rest.RestSwitch( - NAME, - DEVICE_CLASS, - RESOURCE, - STATE_RESOURCE, - METHOD, - headers, - PARAMS, - AUTH, - body_on, - body_off, + hass, + { + CONF_NAME: Template(NAME, hass), + CONF_DEVICE_CLASS: DEVICE_CLASS, + CONF_RESOURCE: RESOURCE, + rest.CONF_STATE_RESOURCE: STATE_RESOURCE, + rest.CONF_METHOD: METHOD, + rest.CONF_HEADERS: headers, + rest.CONF_PARAMS: PARAMS, + rest.CONF_BODY_ON: body_on, + rest.CONF_BODY_OFF: body_off, + rest.CONF_IS_ON_TEMPLATE: None, + rest.CONF_TIMEOUT: 10, + rest.CONF_VERIFY_SSL: True, + }, None, - 10, - True, ) switch.hass = hass return switch, body_on, body_off @@ -315,3 +321,38 @@ async def test_update_timeout(hass, aioclient_mock): await switch.async_update() assert switch.is_on is None + + +async def test_entity_config( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test entity configuration.""" + + aioclient_mock.get("http://localhost", status=HTTPStatus.OK) + config = { + Platform.SWITCH: { + # REST configuration + "platform": "rest", + "method": "POST", + "resource": "http://localhost", + # Entity configuration + "icon": "{{'mdi:one_two_three'}}", + "picture": "{{'blabla.png'}}", + "name": "{{'REST' + ' ' + 'Switch'}}", + "unique_id": "very_unique", + }, + } + + assert await async_setup_component(hass, Platform.SWITCH, config) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + assert entity_registry.async_get("switch.rest_switch").unique_id == "very_unique" + + state = hass.states.get("switch.rest_switch") + assert state.state == "unknown" + assert state.attributes == { + "entity_picture": "blabla.png", + "friendly_name": "REST Switch", + "icon": "mdi:one_two_three", + } From 9e61c7ec49dd47dc3f7b8744c40739af0f2348c6 Mon Sep 17 00:00:00 2001 From: elBoz73 <26686772+elBoz73@users.noreply.github.com> Date: Tue, 28 Jun 2022 22:57:47 +0200 Subject: [PATCH 1900/3516] Add target management for the service call (#73332) --- homeassistant/components/sms/notify.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sms/notify.py b/homeassistant/components/sms/notify.py index 1bd3c60b9b9..433144773f7 100644 --- a/homeassistant/components/sms/notify.py +++ b/homeassistant/components/sms/notify.py @@ -5,7 +5,7 @@ import gammu # pylint: disable=import-error import voluptuous as vol from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService -from homeassistant.const import CONF_NAME, CONF_RECIPIENT +from homeassistant.const import CONF_NAME, CONF_RECIPIENT, CONF_TARGET import homeassistant.helpers.config_validation as cv from .const import DOMAIN, SMS_GATEWAY @@ -44,6 +44,8 @@ class SMSNotificationService(BaseNotificationService): async def async_send_message(self, message="", **kwargs): """Send SMS message.""" + + targets = kwargs.get(CONF_TARGET, [self.number]) smsinfo = { "Class": -1, "Unicode": True, @@ -60,9 +62,11 @@ class SMSNotificationService(BaseNotificationService): for encoded_message in encoded: # Fill in numbers encoded_message["SMSC"] = {"Location": 1} - encoded_message["Number"] = self.number - try: - # Actually send the message - await self.gateway.send_sms_async(encoded_message) - except gammu.GSMError as exc: - _LOGGER.error("Sending to %s failed: %s", self.number, exc) + + for target in targets: + encoded_message["Number"] = target + try: + # Actually send the message + await self.gateway.send_sms_async(encoded_message) + except gammu.GSMError as exc: + _LOGGER.error("Sending to %s failed: %s", target, exc) From 389f1f4edac9ac40270ec046ce456762b2ba926d Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Tue, 28 Jun 2022 23:01:18 +0200 Subject: [PATCH 1901/3516] Add lcn_codelock event and corresponding device trigger (#73022) --- homeassistant/components/lcn/__init__.py | 2 +- .../components/lcn/device_trigger.py | 4 +- homeassistant/components/lcn/strings.json | 1 + .../components/lcn/translations/en.json | 1 + tests/components/lcn/test_device_trigger.py | 53 ++++++++++++++++++- tests/components/lcn/test_events.py | 18 +++++++ 6 files changed, 76 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index b0b231cb9e9..d1486fe0d32 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -191,7 +191,7 @@ def async_host_input_received( def _async_fire_access_control_event( hass: HomeAssistant, device: dr.DeviceEntry, address: AddressType, inp: InputType ) -> None: - """Fire access control event (transponder, transmitter, fingerprint).""" + """Fire access control event (transponder, transmitter, fingerprint, codelock).""" event_data = { "segment_id": address[0], "module_id": address[1], diff --git a/homeassistant/components/lcn/device_trigger.py b/homeassistant/components/lcn/device_trigger.py index a6fc17759b8..8ae640cf6c2 100644 --- a/homeassistant/components/lcn/device_trigger.py +++ b/homeassistant/components/lcn/device_trigger.py @@ -16,13 +16,14 @@ from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, KEY_ACTIONS, SENDKEYS -TRIGGER_TYPES = {"transmitter", "transponder", "fingerprint", "send_keys"} +TRIGGER_TYPES = {"transmitter", "transponder", "fingerprint", "codelock", "send_keys"} LCN_DEVICE_TRIGGER_BASE_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( {vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES)} ) ACCESS_CONTROL_SCHEMA = {vol.Optional("code"): vol.All(vol.Lower, cv.string)} + TRANSMITTER_SCHEMA = { **ACCESS_CONTROL_SCHEMA, vol.Optional("level"): cv.positive_int, @@ -45,6 +46,7 @@ TYPE_SCHEMAS = { "transmitter": {"extra_fields": vol.Schema(TRANSMITTER_SCHEMA)}, "transponder": {"extra_fields": vol.Schema(ACCESS_CONTROL_SCHEMA)}, "fingerprint": {"extra_fields": vol.Schema(ACCESS_CONTROL_SCHEMA)}, + "codelock": {"extra_fields": vol.Schema(ACCESS_CONTROL_SCHEMA)}, "send_keys": {"extra_fields": vol.Schema(SENDKEYS_SCHEMA)}, } diff --git a/homeassistant/components/lcn/strings.json b/homeassistant/components/lcn/strings.json index 2ed8cb8d1c7..c0e46250c1e 100644 --- a/homeassistant/components/lcn/strings.json +++ b/homeassistant/components/lcn/strings.json @@ -4,6 +4,7 @@ "transmitter": "transmitter code received", "transponder": "transponder code received", "fingerprint": "fingerprint code received", + "codelock": "code lock code received", "send_keys": "send keys received" } } diff --git a/homeassistant/components/lcn/translations/en.json b/homeassistant/components/lcn/translations/en.json index ad42b1ffc8f..37f092cbde7 100644 --- a/homeassistant/components/lcn/translations/en.json +++ b/homeassistant/components/lcn/translations/en.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "code lock code received", "fingerprint": "fingerprint code received", "send_keys": "send keys received", "transmitter": "transmitter code received", diff --git a/tests/components/lcn/test_device_trigger.py b/tests/components/lcn/test_device_trigger.py index a52df987e5e..b908d21d5f5 100644 --- a/tests/components/lcn/test_device_trigger.py +++ b/tests/components/lcn/test_device_trigger.py @@ -29,7 +29,13 @@ async def test_get_triggers_module_device(hass, entry, lcn_connection): CONF_DEVICE_ID: device.id, "metadata": {}, } - for trigger in ["transmitter", "transponder", "fingerprint", "send_keys"] + for trigger in [ + "transmitter", + "transponder", + "fingerprint", + "codelock", + "send_keys", + ] ] triggers = await async_get_device_automations( @@ -147,6 +153,51 @@ async def test_if_fires_on_fingerprint_event(hass, calls, entry, lcn_connection) } +async def test_if_fires_on_codelock_event(hass, calls, entry, lcn_connection): + """Test for codelock event triggers firing.""" + address = (0, 7, False) + device = get_device(hass, entry, address) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device.id, + CONF_TYPE: "codelock", + }, + "action": { + "service": "test.automation", + "data_template": { + "test": "test_trigger_codelock", + "code": "{{ trigger.event.data.code }}", + }, + }, + }, + ] + }, + ) + + inp = ModStatusAccessControl( + LcnAddr(*address), + periphery=AccessControlPeriphery.CODELOCK, + code="aabbcc", + ) + + await lcn_connection.async_process_input(inp) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data == { + "test": "test_trigger_codelock", + "code": "aabbcc", + } + + async def test_if_fires_on_transmitter_event(hass, calls, entry, lcn_connection): """Test for transmitter event triggers firing.""" address = (0, 7, False) diff --git a/tests/components/lcn/test_events.py b/tests/components/lcn/test_events.py index 38a685ad663..518af46b02a 100644 --- a/tests/components/lcn/test_events.py +++ b/tests/components/lcn/test_events.py @@ -42,6 +42,24 @@ async def test_fire_fingerprint_event(hass, lcn_connection): assert events[0].data["code"] == "aabbcc" +async def test_fire_codelock_event(hass, lcn_connection): + """Test the codelock event is fired.""" + events = async_capture_events(hass, "lcn_codelock") + + inp = ModStatusAccessControl( + LcnAddr(0, 7, False), + periphery=AccessControlPeriphery.CODELOCK, + code="aabbcc", + ) + + await lcn_connection.async_process_input(inp) + await hass.async_block_till_done() + + assert len(events) == 1 + assert events[0].event_type == "lcn_codelock" + assert events[0].data["code"] == "aabbcc" + + async def test_fire_transmitter_event(hass, lcn_connection): """Test the transmitter event is fired.""" events = async_capture_events(hass, "lcn_transmitter") From efbd47c828c6c2e1cd967df2a4cefd2b00c60c25 Mon Sep 17 00:00:00 2001 From: Stefan Rado Date: Tue, 28 Jun 2022 23:02:39 +0200 Subject: [PATCH 1902/3516] Rewrite SoundTouch tests to use mocked payloads (#72984) --- .../components/soundtouch/media_player.py | 2 +- tests/components/soundtouch/conftest.py | 286 +++++ .../fixtures/device1_getZone_master.xml | 6 + .../soundtouch/fixtures/device1_info.xml | 32 + .../fixtures/device1_now_playing_aux.xml | 7 + .../device1_now_playing_bluetooth.xml | 16 + .../fixtures/device1_now_playing_radio.xml | 16 + .../fixtures/device1_now_playing_standby.xml | 4 + .../fixtures/device1_now_playing_upnp.xml | 13 + .../device1_now_playing_upnp_paused.xml | 13 + .../soundtouch/fixtures/device1_presets.xml | 12 + .../soundtouch/fixtures/device1_volume.xml | 6 + .../fixtures/device1_volume_muted.xml | 6 + .../fixtures/device2_getZone_slave.xml | 4 + .../soundtouch/fixtures/device2_info.xml | 32 + .../fixtures/device2_now_playing_standby.xml | 4 + .../soundtouch/fixtures/device2_volume.xml | 6 + .../soundtouch/test_media_player.py | 1140 ++++++----------- 18 files changed, 846 insertions(+), 759 deletions(-) create mode 100644 tests/components/soundtouch/conftest.py create mode 100644 tests/components/soundtouch/fixtures/device1_getZone_master.xml create mode 100644 tests/components/soundtouch/fixtures/device1_info.xml create mode 100644 tests/components/soundtouch/fixtures/device1_now_playing_aux.xml create mode 100644 tests/components/soundtouch/fixtures/device1_now_playing_bluetooth.xml create mode 100644 tests/components/soundtouch/fixtures/device1_now_playing_radio.xml create mode 100644 tests/components/soundtouch/fixtures/device1_now_playing_standby.xml create mode 100644 tests/components/soundtouch/fixtures/device1_now_playing_upnp.xml create mode 100644 tests/components/soundtouch/fixtures/device1_now_playing_upnp_paused.xml create mode 100644 tests/components/soundtouch/fixtures/device1_presets.xml create mode 100644 tests/components/soundtouch/fixtures/device1_volume.xml create mode 100644 tests/components/soundtouch/fixtures/device1_volume_muted.xml create mode 100644 tests/components/soundtouch/fixtures/device2_getZone_slave.xml create mode 100644 tests/components/soundtouch/fixtures/device2_info.xml create mode 100644 tests/components/soundtouch/fixtures/device2_now_playing_standby.xml create mode 100644 tests/components/soundtouch/fixtures/device2_volume.xml diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index 7c9ade3bee1..f8a5191d9db 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -523,7 +523,7 @@ class SoundTouchDevice(MediaPlayerEntity): for slave in zone_slaves: slave_instance = self._get_instance_by_ip(slave.device_ip) - if slave_instance: + if slave_instance and slave_instance.entity_id != master: slaves.append(slave_instance.entity_id) attributes = { diff --git a/tests/components/soundtouch/conftest.py b/tests/components/soundtouch/conftest.py new file mode 100644 index 00000000000..dcac360d253 --- /dev/null +++ b/tests/components/soundtouch/conftest.py @@ -0,0 +1,286 @@ +"""Fixtures for Bose SoundTouch integration tests.""" +import pytest +from requests_mock import Mocker + +from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN +from homeassistant.components.soundtouch.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PLATFORM + +from tests.common import load_fixture + +DEVICE_1_ID = "020000000001" +DEVICE_2_ID = "020000000002" +DEVICE_1_IP = "192.168.42.1" +DEVICE_2_IP = "192.168.42.2" +DEVICE_1_URL = f"http://{DEVICE_1_IP}:8090" +DEVICE_2_URL = f"http://{DEVICE_2_IP}:8090" +DEVICE_1_NAME = "My Soundtouch 1" +DEVICE_2_NAME = "My Soundtouch 2" +DEVICE_1_ENTITY_ID = f"{MEDIA_PLAYER_DOMAIN}.my_soundtouch_1" +DEVICE_2_ENTITY_ID = f"{MEDIA_PLAYER_DOMAIN}.my_soundtouch_2" + + +# pylint: disable=redefined-outer-name + + +@pytest.fixture +def device1_config() -> dict[str, str]: + """Mock SoundTouch device 1 config.""" + yield {CONF_PLATFORM: DOMAIN, CONF_HOST: DEVICE_1_IP, CONF_NAME: DEVICE_1_NAME} + + +@pytest.fixture +def device2_config() -> dict[str, str]: + """Mock SoundTouch device 2 config.""" + yield {CONF_PLATFORM: DOMAIN, CONF_HOST: DEVICE_2_IP, CONF_NAME: DEVICE_2_NAME} + + +@pytest.fixture(scope="session") +def device1_info() -> str: + """Load SoundTouch device 1 info response and return it.""" + return load_fixture("soundtouch/device1_info.xml") + + +@pytest.fixture(scope="session") +def device1_now_playing_aux() -> str: + """Load SoundTouch device 1 now_playing response and return it.""" + return load_fixture("soundtouch/device1_now_playing_aux.xml") + + +@pytest.fixture(scope="session") +def device1_now_playing_bluetooth() -> str: + """Load SoundTouch device 1 now_playing response and return it.""" + return load_fixture("soundtouch/device1_now_playing_bluetooth.xml") + + +@pytest.fixture(scope="session") +def device1_now_playing_radio() -> str: + """Load SoundTouch device 1 now_playing response and return it.""" + return load_fixture("soundtouch/device1_now_playing_radio.xml") + + +@pytest.fixture(scope="session") +def device1_now_playing_standby() -> str: + """Load SoundTouch device 1 now_playing response and return it.""" + return load_fixture("soundtouch/device1_now_playing_standby.xml") + + +@pytest.fixture(scope="session") +def device1_now_playing_upnp() -> str: + """Load SoundTouch device 1 now_playing response and return it.""" + return load_fixture("soundtouch/device1_now_playing_upnp.xml") + + +@pytest.fixture(scope="session") +def device1_now_playing_upnp_paused() -> str: + """Load SoundTouch device 1 now_playing response and return it.""" + return load_fixture("soundtouch/device1_now_playing_upnp_paused.xml") + + +@pytest.fixture(scope="session") +def device1_presets() -> str: + """Load SoundTouch device 1 presets response and return it.""" + return load_fixture("soundtouch/device1_presets.xml") + + +@pytest.fixture(scope="session") +def device1_volume() -> str: + """Load SoundTouch device 1 volume response and return it.""" + return load_fixture("soundtouch/device1_volume.xml") + + +@pytest.fixture(scope="session") +def device1_volume_muted() -> str: + """Load SoundTouch device 1 volume response and return it.""" + return load_fixture("soundtouch/device1_volume_muted.xml") + + +@pytest.fixture(scope="session") +def device1_zone_master() -> str: + """Load SoundTouch device 1 getZone response and return it.""" + return load_fixture("soundtouch/device1_getZone_master.xml") + + +@pytest.fixture(scope="session") +def device2_info() -> str: + """Load SoundTouch device 2 info response and return it.""" + return load_fixture("soundtouch/device2_info.xml") + + +@pytest.fixture(scope="session") +def device2_volume() -> str: + """Load SoundTouch device 2 volume response and return it.""" + return load_fixture("soundtouch/device2_volume.xml") + + +@pytest.fixture(scope="session") +def device2_now_playing_standby() -> str: + """Load SoundTouch device 2 now_playing response and return it.""" + return load_fixture("soundtouch/device2_now_playing_standby.xml") + + +@pytest.fixture(scope="session") +def device2_zone_slave() -> str: + """Load SoundTouch device 2 getZone response and return it.""" + return load_fixture("soundtouch/device2_getZone_slave.xml") + + +@pytest.fixture(scope="session") +def zone_empty() -> str: + """Load empty SoundTouch getZone response and return it.""" + return load_fixture("soundtouch/getZone_empty.xml") + + +@pytest.fixture +def device1_requests_mock( + requests_mock: Mocker, + device1_info: str, + device1_volume: str, + device1_presets: str, + device1_zone_master: str, +) -> Mocker: + """Mock SoundTouch device 1 API - base URLs.""" + requests_mock.get(f"{DEVICE_1_URL}/info", text=device1_info) + requests_mock.get(f"{DEVICE_1_URL}/volume", text=device1_volume) + requests_mock.get(f"{DEVICE_1_URL}/presets", text=device1_presets) + requests_mock.get(f"{DEVICE_1_URL}/getZone", text=device1_zone_master) + yield requests_mock + + +@pytest.fixture +def device1_requests_mock_standby( + device1_requests_mock: Mocker, + device1_now_playing_standby: str, +): + """Mock SoundTouch device 1 API - standby.""" + device1_requests_mock.get( + f"{DEVICE_1_URL}/now_playing", text=device1_now_playing_standby + ) + + +@pytest.fixture +def device1_requests_mock_aux( + device1_requests_mock: Mocker, + device1_now_playing_aux: str, +): + """Mock SoundTouch device 1 API - playing AUX.""" + device1_requests_mock.get( + f"{DEVICE_1_URL}/now_playing", text=device1_now_playing_aux + ) + + +@pytest.fixture +def device1_requests_mock_bluetooth( + device1_requests_mock: Mocker, + device1_now_playing_bluetooth: str, +): + """Mock SoundTouch device 1 API - playing bluetooth.""" + device1_requests_mock.get( + f"{DEVICE_1_URL}/now_playing", text=device1_now_playing_bluetooth + ) + + +@pytest.fixture +def device1_requests_mock_radio( + device1_requests_mock: Mocker, + device1_now_playing_radio: str, +): + """Mock SoundTouch device 1 API - playing radio.""" + device1_requests_mock.get( + f"{DEVICE_1_URL}/now_playing", text=device1_now_playing_radio + ) + + +@pytest.fixture +def device1_requests_mock_upnp( + device1_requests_mock: Mocker, + device1_now_playing_upnp: str, +): + """Mock SoundTouch device 1 API - playing UPNP.""" + device1_requests_mock.get( + f"{DEVICE_1_URL}/now_playing", text=device1_now_playing_upnp + ) + + +@pytest.fixture +def device1_requests_mock_upnp_paused( + device1_requests_mock: Mocker, + device1_now_playing_upnp_paused: str, +): + """Mock SoundTouch device 1 API - playing UPNP (paused).""" + device1_requests_mock.get( + f"{DEVICE_1_URL}/now_playing", text=device1_now_playing_upnp_paused + ) + + +@pytest.fixture +def device1_requests_mock_key( + device1_requests_mock: Mocker, +): + """Mock SoundTouch device 1 API - key endpoint.""" + yield device1_requests_mock.post(f"{DEVICE_1_URL}/key") + + +@pytest.fixture +def device1_requests_mock_volume( + device1_requests_mock: Mocker, +): + """Mock SoundTouch device 1 API - volume endpoint.""" + yield device1_requests_mock.post(f"{DEVICE_1_URL}/volume") + + +@pytest.fixture +def device1_requests_mock_select( + device1_requests_mock: Mocker, +): + """Mock SoundTouch device 1 API - select endpoint.""" + yield device1_requests_mock.post(f"{DEVICE_1_URL}/select") + + +@pytest.fixture +def device1_requests_mock_set_zone( + device1_requests_mock: Mocker, +): + """Mock SoundTouch device 1 API - setZone endpoint.""" + yield device1_requests_mock.post(f"{DEVICE_1_URL}/setZone") + + +@pytest.fixture +def device1_requests_mock_add_zone_slave( + device1_requests_mock: Mocker, +): + """Mock SoundTouch device 1 API - addZoneSlave endpoint.""" + yield device1_requests_mock.post(f"{DEVICE_1_URL}/addZoneSlave") + + +@pytest.fixture +def device1_requests_mock_remove_zone_slave( + device1_requests_mock: Mocker, +): + """Mock SoundTouch device 1 API - removeZoneSlave endpoint.""" + yield device1_requests_mock.post(f"{DEVICE_1_URL}/removeZoneSlave") + + +@pytest.fixture +def device1_requests_mock_dlna( + device1_requests_mock: Mocker, +): + """Mock SoundTouch device 1 API - DLNA endpoint.""" + yield device1_requests_mock.post(f"http://{DEVICE_1_IP}:8091/AVTransport/Control") + + +@pytest.fixture +def device2_requests_mock_standby( + requests_mock: Mocker, + device2_info: str, + device2_volume: str, + device2_now_playing_standby: str, + device2_zone_slave: str, +) -> Mocker: + """Mock SoundTouch device 2 API.""" + requests_mock.get(f"{DEVICE_2_URL}/info", text=device2_info) + requests_mock.get(f"{DEVICE_2_URL}/volume", text=device2_volume) + requests_mock.get(f"{DEVICE_2_URL}/now_playing", text=device2_now_playing_standby) + requests_mock.get(f"{DEVICE_2_URL}/getZone", text=device2_zone_slave) + + yield requests_mock diff --git a/tests/components/soundtouch/fixtures/device1_getZone_master.xml b/tests/components/soundtouch/fixtures/device1_getZone_master.xml new file mode 100644 index 00000000000..f4b0fd05a51 --- /dev/null +++ b/tests/components/soundtouch/fixtures/device1_getZone_master.xml @@ -0,0 +1,6 @@ + + + 020000000001 + 020000000002 + 020000000003 + \ No newline at end of file diff --git a/tests/components/soundtouch/fixtures/device1_info.xml b/tests/components/soundtouch/fixtures/device1_info.xml new file mode 100644 index 00000000000..27878969ca0 --- /dev/null +++ b/tests/components/soundtouch/fixtures/device1_info.xml @@ -0,0 +1,32 @@ + + + My SoundTouch 1 + SoundTouch 10 + 0 + + + SCM + 27.0.3.46298.4608935 epdbuild.trunk.hepdswbld04.2021-10-06T16:35:02 + P0000000000000000000001 + + + PackagedProduct + 27.0.3.46298.4608935 epdbuild.trunk.hepdswbld04.2021-10-06T16:35:02 + 000000P00000001AE + + + https://streaming.bose.com + + 020000000001 + 192.168.42.1 + + + 060000000001 + 192.168.42.1 + + sm2 + rhino + normal + US + US + \ No newline at end of file diff --git a/tests/components/soundtouch/fixtures/device1_now_playing_aux.xml b/tests/components/soundtouch/fixtures/device1_now_playing_aux.xml new file mode 100644 index 00000000000..e19dc1dd954 --- /dev/null +++ b/tests/components/soundtouch/fixtures/device1_now_playing_aux.xml @@ -0,0 +1,7 @@ + + + + AUX IN + + PLAY_STATE + \ No newline at end of file diff --git a/tests/components/soundtouch/fixtures/device1_now_playing_bluetooth.xml b/tests/components/soundtouch/fixtures/device1_now_playing_bluetooth.xml new file mode 100644 index 00000000000..c43fe187f2f --- /dev/null +++ b/tests/components/soundtouch/fixtures/device1_now_playing_bluetooth.xml @@ -0,0 +1,16 @@ + + + + MockPairedBluetoothDevice + + MockTrack + MockArtist + MockAlbum + MockPairedBluetoothDevice + + + PLAY_STATE + + + + \ No newline at end of file diff --git a/tests/components/soundtouch/fixtures/device1_now_playing_radio.xml b/tests/components/soundtouch/fixtures/device1_now_playing_radio.xml new file mode 100644 index 00000000000..b9d47216b3a --- /dev/null +++ b/tests/components/soundtouch/fixtures/device1_now_playing_radio.xml @@ -0,0 +1,16 @@ + + + + MockStation + http://cdn-profiles.tunein.com/sXXXXX/images/logoq.png + + MockTrack + MockArtist + MockAlbum + MockStation + http://cdn-profiles.tunein.com/sXXXXX/images/logoq.png + + PLAY_STATE + RADIO_STREAMING + + \ No newline at end of file diff --git a/tests/components/soundtouch/fixtures/device1_now_playing_standby.xml b/tests/components/soundtouch/fixtures/device1_now_playing_standby.xml new file mode 100644 index 00000000000..67acae6a0ef --- /dev/null +++ b/tests/components/soundtouch/fixtures/device1_now_playing_standby.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/tests/components/soundtouch/fixtures/device1_now_playing_upnp.xml b/tests/components/soundtouch/fixtures/device1_now_playing_upnp.xml new file mode 100644 index 00000000000..e58e62072ce --- /dev/null +++ b/tests/components/soundtouch/fixtures/device1_now_playing_upnp.xml @@ -0,0 +1,13 @@ + + + + MockTrack + MockArtist + MockAlbum + + + + PLAY_STATE + + TRACK_ONDEMAND + \ No newline at end of file diff --git a/tests/components/soundtouch/fixtures/device1_now_playing_upnp_paused.xml b/tests/components/soundtouch/fixtures/device1_now_playing_upnp_paused.xml new file mode 100644 index 00000000000..6275ada6e4b --- /dev/null +++ b/tests/components/soundtouch/fixtures/device1_now_playing_upnp_paused.xml @@ -0,0 +1,13 @@ + + + + MockTrack + MockArtist + MockAlbum + + + + PAUSE_STATE + + TRACK_ONDEMAND + \ No newline at end of file diff --git a/tests/components/soundtouch/fixtures/device1_presets.xml b/tests/components/soundtouch/fixtures/device1_presets.xml new file mode 100644 index 00000000000..6bacfa48732 --- /dev/null +++ b/tests/components/soundtouch/fixtures/device1_presets.xml @@ -0,0 +1,12 @@ + + + + + + + + MockStation + http://cdn-profiles.tunein.com/sXXXXX/images/logoq.png + + + \ No newline at end of file diff --git a/tests/components/soundtouch/fixtures/device1_volume.xml b/tests/components/soundtouch/fixtures/device1_volume.xml new file mode 100644 index 00000000000..cef90efa37d --- /dev/null +++ b/tests/components/soundtouch/fixtures/device1_volume.xml @@ -0,0 +1,6 @@ + + + 12 + 12 + false + \ No newline at end of file diff --git a/tests/components/soundtouch/fixtures/device1_volume_muted.xml b/tests/components/soundtouch/fixtures/device1_volume_muted.xml new file mode 100644 index 00000000000..e26fbd55e08 --- /dev/null +++ b/tests/components/soundtouch/fixtures/device1_volume_muted.xml @@ -0,0 +1,6 @@ + + + 12 + 12 + true + \ No newline at end of file diff --git a/tests/components/soundtouch/fixtures/device2_getZone_slave.xml b/tests/components/soundtouch/fixtures/device2_getZone_slave.xml new file mode 100644 index 00000000000..fa9db0bf748 --- /dev/null +++ b/tests/components/soundtouch/fixtures/device2_getZone_slave.xml @@ -0,0 +1,4 @@ + + + 020000000002 + \ No newline at end of file diff --git a/tests/components/soundtouch/fixtures/device2_info.xml b/tests/components/soundtouch/fixtures/device2_info.xml new file mode 100644 index 00000000000..a93a19fb52a --- /dev/null +++ b/tests/components/soundtouch/fixtures/device2_info.xml @@ -0,0 +1,32 @@ + + + My SoundTouch 2 + SoundTouch 10 + 0 + + + SCM + 27.0.3.46298.4608935 epdbuild.trunk.hepdswbld04.2021-10-06T16:35:02 + P0000000000000000000002 + + + PackagedProduct + 27.0.3.46298.4608935 epdbuild.trunk.hepdswbld04.2021-10-06T16:35:02 + 000000P00000002AE + + + https://streaming.bose.com + + 020000000002 + 192.168.42.2 + + + 060000000002 + 192.168.42.2 + + sm2 + rhino + normal + US + US + \ No newline at end of file diff --git a/tests/components/soundtouch/fixtures/device2_now_playing_standby.xml b/tests/components/soundtouch/fixtures/device2_now_playing_standby.xml new file mode 100644 index 00000000000..1b8bf8a5a3c --- /dev/null +++ b/tests/components/soundtouch/fixtures/device2_now_playing_standby.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/tests/components/soundtouch/fixtures/device2_volume.xml b/tests/components/soundtouch/fixtures/device2_volume.xml new file mode 100644 index 00000000000..436bd888980 --- /dev/null +++ b/tests/components/soundtouch/fixtures/device2_volume.xml @@ -0,0 +1,6 @@ + + + 10 + 10 + false + \ No newline at end of file diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index 797b5b440d1..1b16508bb88 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -1,1062 +1,686 @@ -"""Test the Soundtouch component.""" -from unittest.mock import call, patch +"""Test the SoundTouch component.""" +from typing import Any -from libsoundtouch.device import ( - Config, - Preset, - SoundTouchDevice as STD, - Status, - Volume, - ZoneSlave, - ZoneStatus, -) -import pytest +from requests_mock import Mocker from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, + ATTR_MEDIA_ALBUM_NAME, + ATTR_MEDIA_ARTIST, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_DURATION, + ATTR_MEDIA_TITLE, + ATTR_MEDIA_TRACK, + ATTR_MEDIA_VOLUME_MUTED, + DOMAIN as MEDIA_PLAYER_DOMAIN, +) +from homeassistant.components.soundtouch.const import ( + DOMAIN, + SERVICE_ADD_ZONE_SLAVE, + SERVICE_CREATE_ZONE, + SERVICE_PLAY_EVERYWHERE, + SERVICE_REMOVE_ZONE_SLAVE, ) -from homeassistant.components.soundtouch import media_player as soundtouch -from homeassistant.components.soundtouch.const import DOMAIN from homeassistant.components.soundtouch.media_player import ( ATTR_SOUNDTOUCH_GROUP, ATTR_SOUNDTOUCH_ZONE, DATA_SOUNDTOUCH, ) from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING -from homeassistant.helpers.discovery import async_load_platform +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -# pylint: disable=super-init-not-called +from .conftest import DEVICE_1_ENTITY_ID, DEVICE_2_ENTITY_ID -DEVICE_1_IP = "192.168.0.1" -DEVICE_2_IP = "192.168.0.2" -DEVICE_1_ID = 1 -DEVICE_2_ID = 2 - - -def get_config(host=DEVICE_1_IP, port=8090, name="soundtouch"): - """Return a default component.""" - return {"platform": DOMAIN, "host": host, "port": port, "name": name} - - -DEVICE_1_CONFIG = {**get_config(), "name": "soundtouch_1"} -DEVICE_2_CONFIG = {**get_config(), "host": DEVICE_2_IP, "name": "soundtouch_2"} - - -@pytest.fixture(name="one_device") -def one_device_fixture(): - """Mock one master device.""" - device_1 = MockDevice() - device_patch = patch( - "homeassistant.components.soundtouch.media_player.soundtouch_device", - return_value=device_1, +async def setup_soundtouch(hass: HomeAssistant, *configs: dict[str, str]): + """Initialize media_player for tests.""" + assert await async_setup_component( + hass, MEDIA_PLAYER_DOMAIN, {MEDIA_PLAYER_DOMAIN: list(configs)} ) - with device_patch as device: - yield device - - -@pytest.fixture(name="two_zones") -def two_zones_fixture(): - """Mock one master and one slave.""" - device_1 = MockDevice( - DEVICE_1_ID, - MockZoneStatus( - is_master=True, - master_id=DEVICE_1_ID, - master_ip=DEVICE_1_IP, - slaves=[MockZoneSlave(DEVICE_2_IP)], - ), - ) - device_2 = MockDevice( - DEVICE_2_ID, - MockZoneStatus( - is_master=False, - master_id=DEVICE_1_ID, - master_ip=DEVICE_1_IP, - slaves=[MockZoneSlave(DEVICE_2_IP)], - ), - ) - devices = {DEVICE_1_IP: device_1, DEVICE_2_IP: device_2} - device_patch = patch( - "homeassistant.components.soundtouch.media_player.soundtouch_device", - side_effect=lambda host, _: devices[host], - ) - with device_patch as device: - yield device - - -@pytest.fixture(name="mocked_status") -def status_fixture(): - """Mock the device status.""" - status_patch = patch( - "libsoundtouch.device.SoundTouchDevice.status", side_effect=MockStatusPlaying - ) - with status_patch as status: - yield status - - -@pytest.fixture(name="mocked_volume") -def volume_fixture(): - """Mock the device volume.""" - volume_patch = patch("libsoundtouch.device.SoundTouchDevice.volume") - with volume_patch as volume: - yield volume - - -async def setup_soundtouch(hass, config): - """Set up soundtouch integration.""" - assert await async_setup_component(hass, "media_player", {"media_player": config}) await hass.async_block_till_done() await hass.async_start() -class MockDevice(STD): - """Mock device.""" - - def __init__(self, id=None, zone_status=None): - """Init the class.""" - self._config = MockConfig(id) - self._zone_status = zone_status or MockZoneStatus() - - def zone_status(self, refresh=True): - """Zone status mock object.""" - return self._zone_status - - -class MockConfig(Config): - """Mock config.""" - - def __init__(self, id=None): - """Init class.""" - self._name = "name" - self._id = id or DEVICE_1_ID - - -class MockZoneStatus(ZoneStatus): - """Mock zone status.""" - - def __init__(self, is_master=True, master_id=None, master_ip=None, slaves=None): - """Init the class.""" - self._is_master = is_master - self._master_id = master_id - self._master_ip = master_ip - self._slaves = slaves or [] - - -class MockZoneSlave(ZoneSlave): - """Mock zone slave.""" - - def __init__(self, device_ip=None, role=None): - """Init the class.""" - self._ip = device_ip - self._role = role - - -def _mocked_presets(*args, **kwargs): - """Return a list of mocked presets.""" - return [MockPreset("1")] - - -class MockPreset(Preset): - """Mock preset.""" - - def __init__(self, id_): - """Init the class.""" - self._id = id_ - self._name = "preset" - - -class MockVolume(Volume): - """Mock volume with value.""" - - def __init__(self): - """Init class.""" - self._actual = 12 - self._muted = False - - -class MockVolumeMuted(Volume): - """Mock volume muted.""" - - def __init__(self): - """Init the class.""" - self._actual = 12 - self._muted = True - - -class MockStatusStandby(Status): - """Mock status standby.""" - - def __init__(self): - """Init the class.""" - self._source = "STANDBY" - - -class MockStatusPlaying(Status): - """Mock status playing media.""" - - def __init__(self): - """Init the class.""" - self._source = "" - self._play_status = "PLAY_STATE" - self._image = "image.url" - self._artist = "artist" - self._track = "track" - self._album = "album" - self._duration = 1 - self._station_name = None - - -class MockStatusPlayingRadio(Status): - """Mock status radio.""" - - def __init__(self): - """Init the class.""" - self._source = "" - self._play_status = "PLAY_STATE" - self._image = "image.url" - self._artist = None - self._track = None - self._album = None - self._duration = None - self._station_name = "station" - - -class MockStatusUnknown(Status): - """Mock status unknown media.""" - - def __init__(self): - """Init the class.""" - self._source = "" - self._play_status = "PLAY_STATE" - self._image = "image.url" - self._artist = None - self._track = None - self._album = None - self._duration = None - self._station_name = None - - -class MockStatusPause(Status): - """Mock status pause.""" - - def __init__(self): - """Init the class.""" - self._source = "" - self._play_status = "PAUSE_STATE" - self._image = "image.url" - self._artist = None - self._track = None - self._album = None - self._duration = None - self._station_name = None - - -class MockStatusPlayingAux(Status): - """Mock status AUX.""" - - def __init__(self): - """Init the class.""" - self._source = "AUX" - self._play_status = "PLAY_STATE" - self._image = "image.url" - self._artist = None - self._track = None - self._album = None - self._duration = None - self._station_name = None - - -class MockStatusPlayingBluetooth(Status): - """Mock status Bluetooth.""" - - def __init__(self): - """Init the class.""" - self._source = "BLUETOOTH" - self._play_status = "PLAY_STATE" - self._image = "image.url" - self._artist = "artist" - self._track = "track" - self._album = "album" - self._duration = None - self._station_name = None - - -async def test_ensure_setup_config(mocked_status, mocked_volume, hass, one_device): - """Test setup OK with custom config.""" - await setup_soundtouch( - hass, get_config(host="192.168.1.44", port=8888, name="custom_sound") - ) - - assert one_device.call_count == 1 - assert one_device.call_args == call("192.168.1.44", 8888) - assert len(hass.states.async_all()) == 1 - state = hass.states.get("media_player.custom_sound") - assert state.name == "custom_sound" - - -async def test_ensure_setup_discovery(mocked_status, mocked_volume, hass, one_device): - """Test setup with discovery.""" - new_device = { - "port": "8090", - "host": "192.168.1.1", - "properties": {}, - "hostname": "hostname.local", - } - await async_load_platform( - hass, "media_player", DOMAIN, new_device, {"media_player": {}} - ) - await hass.async_block_till_done() - - assert one_device.call_count == 1 - assert one_device.call_args == call("192.168.1.1", 8090) - assert len(hass.states.async_all()) == 1 - - -async def test_ensure_setup_discovery_no_duplicate( - mocked_status, mocked_volume, hass, one_device +async def _test_key_service( + hass: HomeAssistant, + requests_mock_key, + service: str, + service_data: dict[str, Any], + key_name: str, ): - """Test setup OK if device already exists.""" - await setup_soundtouch(hass, DEVICE_1_CONFIG) - - assert one_device.call_count == 1 - assert len(hass.states.async_all()) == 1 - - new_device = { - "port": "8090", - "host": "192.168.1.1", - "properties": {}, - "hostname": "hostname.local", - } - await async_load_platform( - hass, "media_player", DOMAIN, new_device, {"media_player": DEVICE_1_CONFIG} - ) - await hass.async_block_till_done() - assert one_device.call_count == 2 - assert len(hass.states.async_all()) == 2 - - existing_device = { - "port": "8090", - "host": "192.168.0.1", - "properties": {}, - "hostname": "hostname.local", - } - await async_load_platform( - hass, "media_player", DOMAIN, existing_device, {"media_player": DEVICE_1_CONFIG} - ) - await hass.async_block_till_done() - assert one_device.call_count == 2 - assert len(hass.states.async_all()) == 2 + """Test API calls that use the /key endpoint to emulate physical button clicks.""" + requests_mock_key.reset() + await hass.services.async_call("media_player", service, service_data, True) + assert requests_mock_key.call_count == 2 + assert f">{key_name}" in requests_mock_key.last_request.text -async def test_playing_media(mocked_status, mocked_volume, hass, one_device): +async def test_playing_media( + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_upnp, +): """Test playing media info.""" - await setup_soundtouch(hass, DEVICE_1_CONFIG) + await setup_soundtouch(hass, device1_config) - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 - - entity_1_state = hass.states.get("media_player.soundtouch_1") - assert entity_1_state.state == STATE_PLAYING - assert entity_1_state.attributes["media_title"] == "artist - track" - assert entity_1_state.attributes["media_track"] == "track" - assert entity_1_state.attributes["media_artist"] == "artist" - assert entity_1_state.attributes["media_album_name"] == "album" - assert entity_1_state.attributes["media_duration"] == 1 + entity_state = hass.states.get(DEVICE_1_ENTITY_ID) + assert entity_state.state == STATE_PLAYING + assert entity_state.attributes[ATTR_MEDIA_TITLE] == "MockArtist - MockTrack" + assert entity_state.attributes[ATTR_MEDIA_TRACK] == "MockTrack" + assert entity_state.attributes[ATTR_MEDIA_ARTIST] == "MockArtist" + assert entity_state.attributes[ATTR_MEDIA_ALBUM_NAME] == "MockAlbum" + assert entity_state.attributes[ATTR_MEDIA_DURATION] == 42 -async def test_playing_unknown_media(mocked_status, mocked_volume, hass, one_device): - """Test playing media info.""" - mocked_status.side_effect = MockStatusUnknown - await setup_soundtouch(hass, DEVICE_1_CONFIG) - - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 - - entity_1_state = hass.states.get("media_player.soundtouch_1") - assert entity_1_state.state == STATE_PLAYING - - -async def test_playing_radio(mocked_status, mocked_volume, hass, one_device): +async def test_playing_radio( + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_radio, +): """Test playing radio info.""" - mocked_status.side_effect = MockStatusPlayingRadio - await setup_soundtouch(hass, DEVICE_1_CONFIG) + await setup_soundtouch(hass, device1_config) - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 - - entity_1_state = hass.states.get("media_player.soundtouch_1") - assert entity_1_state.state == STATE_PLAYING - assert entity_1_state.attributes["media_title"] == "station" + entity_state = hass.states.get(DEVICE_1_ENTITY_ID) + assert entity_state.state == STATE_PLAYING + assert entity_state.attributes[ATTR_MEDIA_TITLE] == "MockStation" -async def test_playing_aux(mocked_status, mocked_volume, hass, one_device): +async def test_playing_aux( + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_aux, +): """Test playing AUX info.""" - mocked_status.side_effect = MockStatusPlayingAux - await setup_soundtouch(hass, DEVICE_1_CONFIG) + await setup_soundtouch(hass, device1_config) - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 - - entity_1_state = hass.states.get("media_player.soundtouch_1") - assert entity_1_state.state == STATE_PLAYING - assert entity_1_state.attributes["source"] == "AUX" + entity_state = hass.states.get(DEVICE_1_ENTITY_ID) + assert entity_state.state == STATE_PLAYING + assert entity_state.attributes[ATTR_INPUT_SOURCE] == "AUX" -async def test_playing_bluetooth(mocked_status, mocked_volume, hass, one_device): +async def test_playing_bluetooth( + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_bluetooth, +): """Test playing Bluetooth info.""" - mocked_status.side_effect = MockStatusPlayingBluetooth - await setup_soundtouch(hass, DEVICE_1_CONFIG) + await setup_soundtouch(hass, device1_config) - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 - - entity_1_state = hass.states.get("media_player.soundtouch_1") - assert entity_1_state.state == STATE_PLAYING - assert entity_1_state.attributes["source"] == "BLUETOOTH" - assert entity_1_state.attributes["media_track"] == "track" - assert entity_1_state.attributes["media_artist"] == "artist" - assert entity_1_state.attributes["media_album_name"] == "album" + entity_state = hass.states.get(DEVICE_1_ENTITY_ID) + assert entity_state.state == STATE_PLAYING + assert entity_state.attributes[ATTR_INPUT_SOURCE] == "BLUETOOTH" + assert entity_state.attributes[ATTR_MEDIA_TRACK] == "MockTrack" + assert entity_state.attributes[ATTR_MEDIA_ARTIST] == "MockArtist" + assert entity_state.attributes[ATTR_MEDIA_ALBUM_NAME] == "MockAlbum" -async def test_get_volume_level(mocked_status, mocked_volume, hass, one_device): +async def test_get_volume_level( + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_upnp, +): """Test volume level.""" - mocked_volume.side_effect = MockVolume - await setup_soundtouch(hass, DEVICE_1_CONFIG) + await setup_soundtouch(hass, device1_config) - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 - - entity_1_state = hass.states.get("media_player.soundtouch_1") - assert entity_1_state.attributes["volume_level"] == 0.12 + entity_state = hass.states.get(DEVICE_1_ENTITY_ID) + assert entity_state.attributes["volume_level"] == 0.12 -async def test_get_state_off(mocked_status, mocked_volume, hass, one_device): +async def test_get_state_off( + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_standby, +): """Test state device is off.""" - mocked_status.side_effect = MockStatusStandby - await setup_soundtouch(hass, DEVICE_1_CONFIG) + await setup_soundtouch(hass, device1_config) - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 - - entity_1_state = hass.states.get("media_player.soundtouch_1") - assert entity_1_state.state == STATE_OFF + entity_state = hass.states.get(DEVICE_1_ENTITY_ID) + assert entity_state.state == STATE_OFF -async def test_get_state_pause(mocked_status, mocked_volume, hass, one_device): +async def test_get_state_pause( + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_upnp_paused, +): """Test state device is paused.""" - mocked_status.side_effect = MockStatusPause - await setup_soundtouch(hass, DEVICE_1_CONFIG) + await setup_soundtouch(hass, device1_config) - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 - - entity_1_state = hass.states.get("media_player.soundtouch_1") - assert entity_1_state.state == STATE_PAUSED + entity_state = hass.states.get(DEVICE_1_ENTITY_ID) + assert entity_state.state == STATE_PAUSED -async def test_is_muted(mocked_status, mocked_volume, hass, one_device): +async def test_is_muted( + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_upnp, + device1_volume_muted: str, +): """Test device volume is muted.""" - mocked_volume.side_effect = MockVolumeMuted - await setup_soundtouch(hass, DEVICE_1_CONFIG) + with Mocker(real_http=True) as mocker: + mocker.get("/volume", text=device1_volume_muted) - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 + await setup_soundtouch(hass, device1_config) - entity_1_state = hass.states.get("media_player.soundtouch_1") - assert entity_1_state.attributes["is_volume_muted"] + entity_state = hass.states.get(DEVICE_1_ENTITY_ID) + assert entity_state.attributes[ATTR_MEDIA_VOLUME_MUTED] -async def test_media_commands(mocked_status, mocked_volume, hass, one_device): - """Test supported media commands.""" - await setup_soundtouch(hass, DEVICE_1_CONFIG) - - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 - - entity_1_state = hass.states.get("media_player.soundtouch_1") - assert entity_1_state.attributes["supported_features"] == 151485 - - -@patch("libsoundtouch.device.SoundTouchDevice.power_off") async def test_should_turn_off( - mocked_power_off, mocked_status, mocked_volume, hass, one_device + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_upnp, + device1_requests_mock_key, ): """Test device is turned off.""" - await setup_soundtouch(hass, DEVICE_1_CONFIG) - - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 - - await hass.services.async_call( - "media_player", + await setup_soundtouch(hass, device1_config) + await _test_key_service( + hass, + device1_requests_mock_key, "turn_off", - {"entity_id": "media_player.soundtouch_1"}, - True, + {"entity_id": DEVICE_1_ENTITY_ID}, + "POWER", ) - assert mocked_status.call_count == 3 - assert mocked_power_off.call_count == 1 -@patch("libsoundtouch.device.SoundTouchDevice.power_on") async def test_should_turn_on( - mocked_power_on, mocked_status, mocked_volume, hass, one_device + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_standby, + device1_requests_mock_key, ): """Test device is turned on.""" - mocked_status.side_effect = MockStatusStandby - await setup_soundtouch(hass, DEVICE_1_CONFIG) - - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 - - await hass.services.async_call( - "media_player", + await setup_soundtouch(hass, device1_config) + await _test_key_service( + hass, + device1_requests_mock_key, "turn_on", - {"entity_id": "media_player.soundtouch_1"}, - True, + {"entity_id": DEVICE_1_ENTITY_ID}, + "POWER", ) - assert mocked_status.call_count == 3 - assert mocked_power_on.call_count == 1 -@patch("libsoundtouch.device.SoundTouchDevice.volume_up") async def test_volume_up( - mocked_volume_up, mocked_status, mocked_volume, hass, one_device + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_upnp, + device1_requests_mock_key, ): """Test volume up.""" - await setup_soundtouch(hass, DEVICE_1_CONFIG) - - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 - - await hass.services.async_call( - "media_player", + await setup_soundtouch(hass, device1_config) + await _test_key_service( + hass, + device1_requests_mock_key, "volume_up", - {"entity_id": "media_player.soundtouch_1"}, - True, + {"entity_id": DEVICE_1_ENTITY_ID}, + "VOLUME_UP", ) - assert mocked_volume.call_count == 3 - assert mocked_volume_up.call_count == 1 -@patch("libsoundtouch.device.SoundTouchDevice.volume_down") async def test_volume_down( - mocked_volume_down, mocked_status, mocked_volume, hass, one_device + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_upnp, + device1_requests_mock_key, ): """Test volume down.""" - await setup_soundtouch(hass, DEVICE_1_CONFIG) - - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 - - await hass.services.async_call( - "media_player", + await setup_soundtouch(hass, device1_config) + await _test_key_service( + hass, + device1_requests_mock_key, "volume_down", - {"entity_id": "media_player.soundtouch_1"}, - True, + {"entity_id": DEVICE_1_ENTITY_ID}, + "VOLUME_DOWN", ) - assert mocked_volume.call_count == 3 - assert mocked_volume_down.call_count == 1 -@patch("libsoundtouch.device.SoundTouchDevice.set_volume") async def test_set_volume_level( - mocked_set_volume, mocked_status, mocked_volume, hass, one_device + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_upnp, + device1_requests_mock_volume, ): """Test set volume level.""" - await setup_soundtouch(hass, DEVICE_1_CONFIG) - - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 + await setup_soundtouch(hass, device1_config) + assert device1_requests_mock_volume.call_count == 0 await hass.services.async_call( "media_player", "volume_set", - {"entity_id": "media_player.soundtouch_1", "volume_level": 0.17}, + {"entity_id": DEVICE_1_ENTITY_ID, "volume_level": 0.17}, True, ) - assert mocked_volume.call_count == 3 - mocked_set_volume.assert_called_with(17) + assert device1_requests_mock_volume.call_count == 1 + assert "17" in device1_requests_mock_volume.last_request.text -@patch("libsoundtouch.device.SoundTouchDevice.mute") -async def test_mute(mocked_mute, mocked_status, mocked_volume, hass, one_device): +async def test_mute( + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_upnp, + device1_requests_mock_key, +): """Test mute volume.""" - await setup_soundtouch(hass, DEVICE_1_CONFIG) - - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 - - await hass.services.async_call( - "media_player", + await setup_soundtouch(hass, device1_config) + await _test_key_service( + hass, + device1_requests_mock_key, "volume_mute", - {"entity_id": "media_player.soundtouch_1", "is_volume_muted": True}, - True, + {"entity_id": DEVICE_1_ENTITY_ID, "is_volume_muted": True}, + "MUTE", ) - assert mocked_volume.call_count == 3 - assert mocked_mute.call_count == 1 -@patch("libsoundtouch.device.SoundTouchDevice.play") -async def test_play(mocked_play, mocked_status, mocked_volume, hass, one_device): +async def test_play( + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_upnp_paused, + device1_requests_mock_key, +): """Test play command.""" - await setup_soundtouch(hass, DEVICE_1_CONFIG) - - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 - - await hass.services.async_call( - "media_player", + await setup_soundtouch(hass, device1_config) + await _test_key_service( + hass, + device1_requests_mock_key, "media_play", - {"entity_id": "media_player.soundtouch_1"}, - True, + {"entity_id": DEVICE_1_ENTITY_ID}, + "PLAY", ) - assert mocked_status.call_count == 3 - assert mocked_play.call_count == 1 -@patch("libsoundtouch.device.SoundTouchDevice.pause") -async def test_pause(mocked_pause, mocked_status, mocked_volume, hass, one_device): +async def test_pause( + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_upnp, + device1_requests_mock_key, +): """Test pause command.""" - await setup_soundtouch(hass, DEVICE_1_CONFIG) - - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 - - await hass.services.async_call( - "media_player", + await setup_soundtouch(hass, device1_config) + await _test_key_service( + hass, + device1_requests_mock_key, "media_pause", - {"entity_id": "media_player.soundtouch_1"}, - True, + {"entity_id": DEVICE_1_ENTITY_ID}, + "PAUSE", ) - assert mocked_status.call_count == 3 - assert mocked_pause.call_count == 1 -@patch("libsoundtouch.device.SoundTouchDevice.play_pause") async def test_play_pause( - mocked_play_pause, mocked_status, mocked_volume, hass, one_device + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_upnp, + device1_requests_mock_key, ): """Test play/pause.""" - await setup_soundtouch(hass, DEVICE_1_CONFIG) - - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 - - await hass.services.async_call( - "media_player", + await setup_soundtouch(hass, device1_config) + await _test_key_service( + hass, + device1_requests_mock_key, "media_play_pause", - {"entity_id": "media_player.soundtouch_1"}, - True, + {"entity_id": DEVICE_1_ENTITY_ID}, + "PLAY_PAUSE", ) - assert mocked_status.call_count == 3 - assert mocked_play_pause.call_count == 1 -@patch("libsoundtouch.device.SoundTouchDevice.previous_track") -@patch("libsoundtouch.device.SoundTouchDevice.next_track") async def test_next_previous_track( - mocked_next_track, - mocked_previous_track, - mocked_status, - mocked_volume, - hass, - one_device, + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_upnp, + device1_requests_mock_key, ): """Test next/previous track.""" - await setup_soundtouch(hass, DEVICE_1_CONFIG) - - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 - - await hass.services.async_call( - "media_player", + await setup_soundtouch(hass, device1_config) + await _test_key_service( + hass, + device1_requests_mock_key, "media_next_track", - {"entity_id": "media_player.soundtouch_1"}, - True, + {"entity_id": DEVICE_1_ENTITY_ID}, + "NEXT_TRACK", ) - assert mocked_status.call_count == 3 - assert mocked_next_track.call_count == 1 - await hass.services.async_call( - "media_player", + await _test_key_service( + hass, + device1_requests_mock_key, "media_previous_track", - {"entity_id": "media_player.soundtouch_1"}, - True, + {"entity_id": DEVICE_1_ENTITY_ID}, + "PREV_TRACK", ) - assert mocked_status.call_count == 4 - assert mocked_previous_track.call_count == 1 -@patch("libsoundtouch.device.SoundTouchDevice.select_preset") -@patch("libsoundtouch.device.SoundTouchDevice.presets", side_effect=_mocked_presets) async def test_play_media( - mocked_presets, mocked_select_preset, mocked_status, mocked_volume, hass, one_device + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_standby, + device1_requests_mock_select, ): """Test play preset 1.""" - await setup_soundtouch(hass, DEVICE_1_CONFIG) - - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 + await setup_soundtouch(hass, device1_config) + assert device1_requests_mock_select.call_count == 0 await hass.services.async_call( "media_player", "play_media", { - "entity_id": "media_player.soundtouch_1", + "entity_id": DEVICE_1_ENTITY_ID, ATTR_MEDIA_CONTENT_TYPE: "PLAYLIST", ATTR_MEDIA_CONTENT_ID: 1, }, True, ) - assert mocked_presets.call_count == 1 - assert mocked_select_preset.call_count == 1 + assert device1_requests_mock_select.call_count == 1 + assert ( + 'location="http://homeassistant:8123/media/local/test.mp3"' + in device1_requests_mock_select.last_request.text + ) await hass.services.async_call( "media_player", "play_media", { - "entity_id": "media_player.soundtouch_1", + "entity_id": DEVICE_1_ENTITY_ID, ATTR_MEDIA_CONTENT_TYPE: "PLAYLIST", ATTR_MEDIA_CONTENT_ID: 2, }, True, ) - assert mocked_presets.call_count == 2 - assert mocked_select_preset.call_count == 1 + assert device1_requests_mock_select.call_count == 2 + assert "MockStation" in device1_requests_mock_select.last_request.text -@patch("libsoundtouch.device.SoundTouchDevice.play_url") async def test_play_media_url( - mocked_play_url, mocked_status, mocked_volume, hass, one_device + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_standby, + device1_requests_mock_dlna, ): """Test play preset 1.""" - await setup_soundtouch(hass, DEVICE_1_CONFIG) - - assert one_device.call_count == 1 - assert mocked_status.call_count == 2 - assert mocked_volume.call_count == 2 + await setup_soundtouch(hass, device1_config) + assert device1_requests_mock_dlna.call_count == 0 await hass.services.async_call( "media_player", "play_media", { - "entity_id": "media_player.soundtouch_1", + "entity_id": DEVICE_1_ENTITY_ID, ATTR_MEDIA_CONTENT_TYPE: "MUSIC", ATTR_MEDIA_CONTENT_ID: "http://fqdn/file.mp3", }, True, ) - mocked_play_url.assert_called_with("http://fqdn/file.mp3") + assert device1_requests_mock_dlna.call_count == 1 + assert "http://fqdn/file.mp3" in device1_requests_mock_dlna.last_request.text -@patch("libsoundtouch.device.SoundTouchDevice.select_source_aux") async def test_select_source_aux( - mocked_select_source_aux, mocked_status, mocked_volume, hass, one_device + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_standby, + device1_requests_mock_select, ): """Test select AUX.""" - await setup_soundtouch(hass, DEVICE_1_CONFIG) + await setup_soundtouch(hass, device1_config) - assert mocked_select_source_aux.call_count == 0 + assert device1_requests_mock_select.call_count == 0 await hass.services.async_call( "media_player", "select_source", - {"entity_id": "media_player.soundtouch_1", ATTR_INPUT_SOURCE: "AUX"}, + {"entity_id": DEVICE_1_ENTITY_ID, ATTR_INPUT_SOURCE: "AUX"}, True, ) - - assert mocked_select_source_aux.call_count == 1 + assert device1_requests_mock_select.call_count == 1 + assert "AUX" in device1_requests_mock_select.last_request.text -@patch("libsoundtouch.device.SoundTouchDevice.select_source_bluetooth") async def test_select_source_bluetooth( - mocked_select_source_bluetooth, mocked_status, mocked_volume, hass, one_device + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_standby, + device1_requests_mock_select, ): """Test select Bluetooth.""" - await setup_soundtouch(hass, DEVICE_1_CONFIG) + await setup_soundtouch(hass, device1_config) - assert mocked_select_source_bluetooth.call_count == 0 + assert device1_requests_mock_select.call_count == 0 await hass.services.async_call( "media_player", "select_source", - {"entity_id": "media_player.soundtouch_1", ATTR_INPUT_SOURCE: "BLUETOOTH"}, + {"entity_id": DEVICE_1_ENTITY_ID, ATTR_INPUT_SOURCE: "BLUETOOTH"}, True, ) - - assert mocked_select_source_bluetooth.call_count == 1 + assert device1_requests_mock_select.call_count == 1 + assert "BLUETOOTH" in device1_requests_mock_select.last_request.text -@patch("libsoundtouch.device.SoundTouchDevice.select_source_bluetooth") -@patch("libsoundtouch.device.SoundTouchDevice.select_source_aux") async def test_select_source_invalid_source( - mocked_select_source_aux, - mocked_select_source_bluetooth, - mocked_status, - mocked_volume, - hass, - one_device, + hass: HomeAssistant, + device1_config: dict[str, str], + device1_requests_mock_standby, + device1_requests_mock_select, ): """Test select unsupported source.""" - await setup_soundtouch(hass, DEVICE_1_CONFIG) - - assert mocked_select_source_aux.call_count == 0 - assert mocked_select_source_bluetooth.call_count == 0 + await setup_soundtouch(hass, device1_config) + assert not device1_requests_mock_select.called await hass.services.async_call( "media_player", "select_source", { - "entity_id": "media_player.soundtouch_1", + "entity_id": DEVICE_1_ENTITY_ID, ATTR_INPUT_SOURCE: "SOMETHING_UNSUPPORTED", }, True, ) - - assert mocked_select_source_aux.call_count == 0 - assert mocked_select_source_bluetooth.call_count == 0 + assert not device1_requests_mock_select.called -@patch("libsoundtouch.device.SoundTouchDevice.create_zone") async def test_play_everywhere( - mocked_create_zone, mocked_status, mocked_volume, hass, two_zones + hass: HomeAssistant, + device1_config: dict[str, str], + device2_config: dict[str, str], + device1_requests_mock_standby, + device2_requests_mock_standby, + device1_requests_mock_set_zone, ): """Test play everywhere.""" - mocked_device = two_zones - await setup_soundtouch(hass, [DEVICE_1_CONFIG, DEVICE_2_CONFIG]) + await setup_soundtouch(hass, device1_config, device2_config) - assert mocked_device.call_count == 2 - assert mocked_status.call_count == 4 - assert mocked_volume.call_count == 4 - - # one master, one slave => create zone + # one master, one slave => set zone await hass.services.async_call( - soundtouch.DOMAIN, - soundtouch.SERVICE_PLAY_EVERYWHERE, - {"master": "media_player.soundtouch_1"}, + DOMAIN, + SERVICE_PLAY_EVERYWHERE, + {"master": DEVICE_1_ENTITY_ID}, True, ) - assert mocked_create_zone.call_count == 1 + assert device1_requests_mock_set_zone.call_count == 1 - # unknown master, create zone must not be called + # unknown master, set zone must not be called await hass.services.async_call( - soundtouch.DOMAIN, - soundtouch.SERVICE_PLAY_EVERYWHERE, + DOMAIN, + SERVICE_PLAY_EVERYWHERE, {"master": "media_player.entity_X"}, True, ) - assert mocked_create_zone.call_count == 1 + assert device1_requests_mock_set_zone.call_count == 1 - # no slaves, create zone must not be called + # remove second device for entity in list(hass.data[DATA_SOUNDTOUCH]): - if entity.entity_id == "media_player.soundtouch_1": + if entity.entity_id == DEVICE_1_ENTITY_ID: continue hass.data[DATA_SOUNDTOUCH].remove(entity) await entity.async_remove() + + # no slaves, set zone must not be called await hass.services.async_call( - soundtouch.DOMAIN, - soundtouch.SERVICE_PLAY_EVERYWHERE, - {"master": "media_player.soundtouch_1"}, + DOMAIN, + SERVICE_PLAY_EVERYWHERE, + {"master": DEVICE_1_ENTITY_ID}, True, ) - assert mocked_create_zone.call_count == 1 + assert device1_requests_mock_set_zone.call_count == 1 -@patch("libsoundtouch.device.SoundTouchDevice.create_zone") async def test_create_zone( - mocked_create_zone, mocked_status, mocked_volume, hass, two_zones + hass: HomeAssistant, + device1_config: dict[str, str], + device2_config: dict[str, str], + device1_requests_mock_standby, + device2_requests_mock_standby, + device1_requests_mock_set_zone, ): """Test creating a zone.""" - mocked_device = two_zones - await setup_soundtouch(hass, [DEVICE_1_CONFIG, DEVICE_2_CONFIG]) + await setup_soundtouch(hass, device1_config, device2_config) - assert mocked_device.call_count == 2 - assert mocked_status.call_count == 4 - assert mocked_volume.call_count == 4 + assert device1_requests_mock_set_zone.call_count == 0 - # one master, one slave => create zone + # one master, one slave => set zone await hass.services.async_call( - soundtouch.DOMAIN, - soundtouch.SERVICE_CREATE_ZONE, + DOMAIN, + SERVICE_CREATE_ZONE, { - "master": "media_player.soundtouch_1", - "slaves": ["media_player.soundtouch_2"], + "master": DEVICE_1_ENTITY_ID, + "slaves": [DEVICE_2_ENTITY_ID], }, True, ) - assert mocked_create_zone.call_count == 1 + assert device1_requests_mock_set_zone.call_count == 1 - # unknown master, create zone must not be called + # unknown master, set zone must not be called await hass.services.async_call( - soundtouch.DOMAIN, - soundtouch.SERVICE_CREATE_ZONE, - {"master": "media_player.entity_X", "slaves": ["media_player.soundtouch_2"]}, + DOMAIN, + SERVICE_CREATE_ZONE, + {"master": "media_player.entity_X", "slaves": [DEVICE_2_ENTITY_ID]}, True, ) - assert mocked_create_zone.call_count == 1 + assert device1_requests_mock_set_zone.call_count == 1 - # no slaves, create zone must not be called + # no slaves, set zone must not be called await hass.services.async_call( - soundtouch.DOMAIN, - soundtouch.SERVICE_CREATE_ZONE, - {"master": "media_player.soundtouch_1", "slaves": []}, + DOMAIN, + SERVICE_CREATE_ZONE, + {"master": DEVICE_1_ENTITY_ID, "slaves": []}, True, ) - assert mocked_create_zone.call_count == 1 + assert device1_requests_mock_set_zone.call_count == 1 -@patch("libsoundtouch.device.SoundTouchDevice.remove_zone_slave") async def test_remove_zone_slave( - mocked_remove_zone_slave, mocked_status, mocked_volume, hass, two_zones + hass: HomeAssistant, + device1_config: dict[str, str], + device2_config: dict[str, str], + device1_requests_mock_standby, + device2_requests_mock_standby, + device1_requests_mock_remove_zone_slave, ): - """Test adding a slave to an existing zone.""" - mocked_device = two_zones - await setup_soundtouch(hass, [DEVICE_1_CONFIG, DEVICE_2_CONFIG]) - - assert mocked_device.call_count == 2 - assert mocked_status.call_count == 4 - assert mocked_volume.call_count == 4 + """Test removing a slave from an existing zone.""" + await setup_soundtouch(hass, device1_config, device2_config) # remove one slave await hass.services.async_call( - soundtouch.DOMAIN, - soundtouch.SERVICE_REMOVE_ZONE_SLAVE, + DOMAIN, + SERVICE_REMOVE_ZONE_SLAVE, { - "master": "media_player.soundtouch_1", - "slaves": ["media_player.soundtouch_2"], + "master": DEVICE_1_ENTITY_ID, + "slaves": [DEVICE_2_ENTITY_ID], }, True, ) - assert mocked_remove_zone_slave.call_count == 1 + assert device1_requests_mock_remove_zone_slave.call_count == 1 - # unknown master. add zone slave is not called + # unknown master, remove zone slave is not called await hass.services.async_call( - soundtouch.DOMAIN, - soundtouch.SERVICE_REMOVE_ZONE_SLAVE, - {"master": "media_player.entity_X", "slaves": ["media_player.soundtouch_2"]}, + DOMAIN, + SERVICE_REMOVE_ZONE_SLAVE, + {"master": "media_player.entity_X", "slaves": [DEVICE_2_ENTITY_ID]}, True, ) - assert mocked_remove_zone_slave.call_count == 1 + assert device1_requests_mock_remove_zone_slave.call_count == 1 - # no slave to add, add zone slave is not called + # no slave to remove, remove zone slave is not called await hass.services.async_call( - soundtouch.DOMAIN, - soundtouch.SERVICE_REMOVE_ZONE_SLAVE, - {"master": "media_player.soundtouch_1", "slaves": []}, + DOMAIN, + SERVICE_REMOVE_ZONE_SLAVE, + {"master": DEVICE_1_ENTITY_ID, "slaves": []}, True, ) - assert mocked_remove_zone_slave.call_count == 1 + assert device1_requests_mock_remove_zone_slave.call_count == 1 -@patch("libsoundtouch.device.SoundTouchDevice.add_zone_slave") async def test_add_zone_slave( - mocked_add_zone_slave, - mocked_status, - mocked_volume, - hass, - two_zones, + hass: HomeAssistant, + device1_config: dict[str, str], + device2_config: dict[str, str], + device1_requests_mock_standby, + device2_requests_mock_standby, + device1_requests_mock_add_zone_slave, ): - """Test removing a slave from a zone.""" - mocked_device = two_zones - await setup_soundtouch(hass, [DEVICE_1_CONFIG, DEVICE_2_CONFIG]) - - assert mocked_device.call_count == 2 - assert mocked_status.call_count == 4 - assert mocked_volume.call_count == 4 + """Test adding a slave to a zone.""" + await setup_soundtouch(hass, device1_config, device2_config) # add one slave await hass.services.async_call( - soundtouch.DOMAIN, - soundtouch.SERVICE_ADD_ZONE_SLAVE, + DOMAIN, + SERVICE_ADD_ZONE_SLAVE, { - "master": "media_player.soundtouch_1", - "slaves": ["media_player.soundtouch_2"], + "master": DEVICE_1_ENTITY_ID, + "slaves": [DEVICE_2_ENTITY_ID], }, True, ) - assert mocked_add_zone_slave.call_count == 1 + assert device1_requests_mock_add_zone_slave.call_count == 1 # unknown master, add zone slave is not called await hass.services.async_call( - soundtouch.DOMAIN, - soundtouch.SERVICE_ADD_ZONE_SLAVE, - {"master": "media_player.entity_X", "slaves": ["media_player.soundtouch_2"]}, + DOMAIN, + SERVICE_ADD_ZONE_SLAVE, + {"master": "media_player.entity_X", "slaves": [DEVICE_2_ENTITY_ID]}, True, ) - assert mocked_add_zone_slave.call_count == 1 + assert device1_requests_mock_add_zone_slave.call_count == 1 # no slave to add, add zone slave is not called await hass.services.async_call( - soundtouch.DOMAIN, - soundtouch.SERVICE_ADD_ZONE_SLAVE, - {"master": "media_player.soundtouch_1", "slaves": ["media_player.entity_X"]}, + DOMAIN, + SERVICE_ADD_ZONE_SLAVE, + {"master": DEVICE_1_ENTITY_ID, "slaves": ["media_player.entity_X"]}, True, ) - assert mocked_add_zone_slave.call_count == 1 + assert device1_requests_mock_add_zone_slave.call_count == 1 -@patch("libsoundtouch.device.SoundTouchDevice.create_zone") async def test_zone_attributes( - mocked_create_zone, - mocked_status, - mocked_volume, - hass, - two_zones, + hass: HomeAssistant, + device1_config: dict[str, str], + device2_config: dict[str, str], + device1_requests_mock_standby, + device2_requests_mock_standby, ): - """Test play everywhere.""" - mocked_device = two_zones - await setup_soundtouch(hass, [DEVICE_1_CONFIG, DEVICE_2_CONFIG]) + """Test zone attributes.""" + await setup_soundtouch(hass, device1_config, device2_config) - assert mocked_device.call_count == 2 - assert mocked_status.call_count == 4 - assert mocked_volume.call_count == 4 - - entity_1_state = hass.states.get("media_player.soundtouch_1") + entity_1_state = hass.states.get(DEVICE_1_ENTITY_ID) assert entity_1_state.attributes[ATTR_SOUNDTOUCH_ZONE]["is_master"] assert ( - entity_1_state.attributes[ATTR_SOUNDTOUCH_ZONE]["master"] - == "media_player.soundtouch_1" + entity_1_state.attributes[ATTR_SOUNDTOUCH_ZONE]["master"] == DEVICE_1_ENTITY_ID ) assert entity_1_state.attributes[ATTR_SOUNDTOUCH_ZONE]["slaves"] == [ - "media_player.soundtouch_2" + DEVICE_2_ENTITY_ID ] assert entity_1_state.attributes[ATTR_SOUNDTOUCH_GROUP] == [ - "media_player.soundtouch_1", - "media_player.soundtouch_2", + DEVICE_1_ENTITY_ID, + DEVICE_2_ENTITY_ID, ] - entity_2_state = hass.states.get("media_player.soundtouch_2") + + entity_2_state = hass.states.get(DEVICE_2_ENTITY_ID) assert not entity_2_state.attributes[ATTR_SOUNDTOUCH_ZONE]["is_master"] assert ( - entity_2_state.attributes[ATTR_SOUNDTOUCH_ZONE]["master"] - == "media_player.soundtouch_1" + entity_2_state.attributes[ATTR_SOUNDTOUCH_ZONE]["master"] == DEVICE_1_ENTITY_ID ) assert entity_2_state.attributes[ATTR_SOUNDTOUCH_ZONE]["slaves"] == [ - "media_player.soundtouch_2" + DEVICE_2_ENTITY_ID ] assert entity_2_state.attributes[ATTR_SOUNDTOUCH_GROUP] == [ - "media_player.soundtouch_1", - "media_player.soundtouch_2", + DEVICE_1_ENTITY_ID, + DEVICE_2_ENTITY_ID, ] From 1a55c7db34c87513eb75de7eff7a1154bbc4326e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 29 Jun 2022 00:13:26 +0300 Subject: [PATCH 1903/3516] Take Huawei LTE XML parse errors to mean unsupported endpoint (#72781) --- homeassistant/components/huawei_lte/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 601a3b9af8d..f4e2cb209db 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -9,6 +9,7 @@ from datetime import timedelta import logging import time from typing import Any, NamedTuple, cast +from xml.parsers.expat import ExpatError from huawei_lte_api.Client import Client from huawei_lte_api.Connection import Connection @@ -204,14 +205,13 @@ class Router: "%s requires authorization, excluding from future updates", key ) self.subscriptions.pop(key) - except ResponseErrorException as exc: + except (ResponseErrorException, ExpatError) as exc: + # Take ResponseErrorNotSupportedException, ExpatError, and generic + # ResponseErrorException with a few select codes to mean the endpoint is + # not supported. if not isinstance( - exc, ResponseErrorNotSupportedException - ) and exc.code not in ( - # additional codes treated as unusupported - -1, - 100006, - ): + exc, (ResponseErrorNotSupportedException, ExpatError) + ) and exc.code not in (-1, 100006): raise _LOGGER.info( "%s apparently not supported by device, excluding from future updates", From 7d74301045c5567f7f597928c936416f4f4cc21d Mon Sep 17 00:00:00 2001 From: Thijs W Date: Tue, 28 Jun 2022 23:13:43 +0200 Subject: [PATCH 1904/3516] Add sound mode to frontier silicon (#72760) --- .../components/frontier_silicon/media_player.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 555e0517d4c..61dc6e69726 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -100,6 +100,7 @@ class AFSAPIDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF | MediaPlayerEntityFeature.SELECT_SOURCE + | MediaPlayerEntityFeature.SELECT_SOUND_MODE ) def __init__(self, name: str | None, afsapi: AFSAPI) -> None: @@ -115,6 +116,7 @@ class AFSAPIDevice(MediaPlayerEntity): self._max_volume = None self.__modes_by_label = None + self.__sound_modes_by_label = None async def async_update(self): """Get the latest date and update device state.""" @@ -156,6 +158,13 @@ class AFSAPIDevice(MediaPlayerEntity): } self._attr_source_list = list(self.__modes_by_label) + if not self._attr_sound_mode_list: + self.__sound_modes_by_label = { + sound_mode.label: sound_mode.key + for sound_mode in await afsapi.get_equalisers() + } + self._attr_sound_mode_list = list(self.__sound_modes_by_label) + # The API seems to include 'zero' in the number of steps (e.g. if the range is # 0-40 then get_volume_steps returns 41) subtract one to get the max volume. # If call to get_volume fails set to 0 and try again next time. @@ -174,6 +183,7 @@ class AFSAPIDevice(MediaPlayerEntity): self._attr_is_volume_muted = await afsapi.get_mute() self._attr_media_image_url = await afsapi.get_play_graphic() + self._attr_sound_mode = (await afsapi.get_eq_preset()).label volume = await self.fs_device.get_volume() @@ -188,6 +198,7 @@ class AFSAPIDevice(MediaPlayerEntity): self._attr_is_volume_muted = None self._attr_media_image_url = None + self._attr_sound_mode = None self._attr_volume_level = None @@ -255,3 +266,7 @@ class AFSAPIDevice(MediaPlayerEntity): """Select input source.""" await self.fs_device.set_power(True) await self.fs_device.set_mode(self.__modes_by_label.get(source)) + + async def async_select_sound_mode(self, sound_mode): + """Select EQ Preset.""" + await self.fs_device.set_eq_preset(self.__sound_modes_by_label[sound_mode]) From c3a2fce5ccdfe2c777156ce481f8075838cffcc3 Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 28 Jun 2022 17:22:18 -0400 Subject: [PATCH 1905/3516] Move to async for aladdin connect integration (#73954) * Moved to AIOAladdinConnect API * Added callback logic for door status * close unused connections * Close connection after verification * Matched to current version * Matched __init__.py to current release * Matched cover.py to existing version * added missing awaits * Moved callback * Bumped AIOAladdinConnect to 0.1.3 * Removed await from callback config * Finished tests * Added callback test * Bumped AIOAladdinConnect to 0.1.4 * Finished tests * Callback correct call to update HA * Modified calls to state machine * Modified update path * Removed unused status * Bumped AIOAladdinConnect to 0.1.7 * Revised test_cover cover tests and bumped AIOAladdinConnect to 0.1.10 * Bumped AIOAladdinConnect to 0.1.11 * Bumped AIOAladdinConenct to 0.1.12 * Bumped AIOAladdinConnect to 0.1.13 * Bumped AIOAladdinConnect to 0.1.14 * Added ability to handle multiple doors * Added timout errors to config flow * asyncio timout error added to setup retry * Cleanup added to hass proceedure * Bumped AIOAladdinConnect to 0.1.16 * Bumped AIOAladdinConnect to 0.1.18 * Bumped AIOAladdinConnect to 0.1.19 * Bumped AIOAladdinConnect to 0.1.20 * Addressed recommended changes: SCAN_INTERVAL and spelling * Moved to async_get_clientsession and bumped AIOAladdinConnect to 0.1.21 * Missing test for new code structure * removed extra call to write_ha_state, callback decorator, cleaned up tests * Update tests/components/aladdin_connect/test_init.py Co-authored-by: Martin Hjelmare * Removed extra_attributes. * Added typing to variable acc Co-authored-by: Martin Hjelmare --- .../components/aladdin_connect/__init__.py | 17 +- .../components/aladdin_connect/config_flow.py | 21 +- .../components/aladdin_connect/cover.py | 70 ++++-- .../components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/aladdin_connect/conftest.py | 39 +++ .../aladdin_connect/test_config_flow.py | 155 ++++++++---- .../components/aladdin_connect/test_cover.py | 238 +++++++++--------- tests/components/aladdin_connect/test_init.py | 113 +++++++-- 10 files changed, 458 insertions(+), 209 deletions(-) create mode 100644 tests/components/aladdin_connect/conftest.py diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index 048624641bd..af996c9f5b2 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -1,13 +1,16 @@ """The aladdin_connect component.""" +import asyncio import logging from typing import Final -from aladdin_connect import AladdinConnectClient +from AIOAladdinConnect import AladdinConnectClient +from aiohttp import ClientConnectionError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -20,9 +23,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up platform from a ConfigEntry.""" username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] - acc = AladdinConnectClient(username, password) - if not await hass.async_add_executor_job(acc.login): - raise ConfigEntryAuthFailed("Incorrect Password") + acc = AladdinConnectClient(username, password, async_get_clientsession(hass)) + try: + if not await acc.login(): + raise ConfigEntryAuthFailed("Incorrect Password") + except (ClientConnectionError, asyncio.TimeoutError) as ex: + raise ConfigEntryNotReady("Can not connect to host") from ex + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = acc hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index 0b928e9d423..0d45ea9a8ef 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -1,11 +1,14 @@ """Config flow for Aladdin Connect cover integration.""" from __future__ import annotations +import asyncio from collections.abc import Mapping import logging from typing import Any -from aladdin_connect import AladdinConnectClient +from AIOAladdinConnect import AladdinConnectClient +from aiohttp import ClientError +from aiohttp.client_exceptions import ClientConnectionError import voluptuous as vol from homeassistant import config_entries @@ -13,6 +16,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -33,8 +37,11 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ - acc = AladdinConnectClient(data[CONF_USERNAME], data[CONF_PASSWORD]) - login = await hass.async_add_executor_job(acc.login) + acc = AladdinConnectClient( + data[CONF_USERNAME], data[CONF_PASSWORD], async_get_clientsession(hass) + ) + login = await acc.login() + await acc.close() if not login: raise InvalidAuth @@ -67,8 +74,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: await validate_input(self.hass, data) + except InvalidAuth: errors["base"] = "invalid_auth" + + except (ClientConnectionError, asyncio.TimeoutError, ClientError): + errors["base"] = "cannot_connect" + else: self.hass.config_entries.async_update_entry( @@ -103,6 +115,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except InvalidAuth: errors["base"] = "invalid_auth" + except (ClientConnectionError, asyncio.TimeoutError, ClientError): + errors["base"] = "cannot_connect" + else: await self.async_set_unique_id( user_input["username"].lower(), raise_on_progress=False diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index da3e6b81663..9c03cd322b6 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -1,10 +1,11 @@ """Platform for the Aladdin Connect cover component.""" from __future__ import annotations +from datetime import timedelta import logging from typing import Any, Final -from aladdin_connect import AladdinConnectClient +from AIOAladdinConnect import AladdinConnectClient import voluptuous as vol from homeassistant.components.cover import ( @@ -34,6 +35,7 @@ _LOGGER: Final = logging.getLogger(__name__) PLATFORM_SCHEMA: Final = BASE_PLATFORM_SCHEMA.extend( {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} ) +SCAN_INTERVAL = timedelta(seconds=300) async def async_setup_platform( @@ -62,14 +64,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Aladdin Connect platform.""" - acc = hass.data[DOMAIN][config_entry.entry_id] - doors = await hass.async_add_executor_job(acc.get_doors) - + acc: AladdinConnectClient = hass.data[DOMAIN][config_entry.entry_id] + doors = await acc.get_doors() if doors is None: raise PlatformNotReady("Error from Aladdin Connect getting doors") async_add_entities( - (AladdinDevice(acc, door) for door in doors), - update_before_add=True, + (AladdinDevice(acc, door, config_entry) for door in doors), ) @@ -79,27 +79,63 @@ class AladdinDevice(CoverEntity): _attr_device_class = CoverDeviceClass.GARAGE _attr_supported_features = SUPPORTED_FEATURES - def __init__(self, acc: AladdinConnectClient, device: DoorDevice) -> None: + def __init__( + self, acc: AladdinConnectClient, device: DoorDevice, entry: ConfigEntry + ) -> None: """Initialize the Aladdin Connect cover.""" self._acc = acc + self._device_id = device["device_id"] self._number = device["door_number"] self._attr_name = device["name"] self._attr_unique_id = f"{self._device_id}-{self._number}" - def close_cover(self, **kwargs: Any) -> None: + async def async_added_to_hass(self) -> None: + """Connect Aladdin Connect to the cloud.""" + + async def update_callback() -> None: + """Schedule a state update.""" + self.async_write_ha_state() + + self._acc.register_callback(update_callback, self._number) + await self._acc.get_doors(self._number) + + async def async_will_remove_from_hass(self) -> None: + """Close Aladdin Connect before removing.""" + await self._acc.close() + + async def async_close_cover(self, **kwargs: Any) -> None: """Issue close command to cover.""" - self._acc.close_door(self._device_id, self._number) + await self._acc.close_door(self._device_id, self._number) - def open_cover(self, **kwargs: Any) -> None: + async def async_open_cover(self, **kwargs: Any) -> None: """Issue open command to cover.""" - self._acc.open_door(self._device_id, self._number) + await self._acc.open_door(self._device_id, self._number) - def update(self) -> None: + async def async_update(self) -> None: """Update status of cover.""" - status = STATES_MAP.get( - self._acc.get_door_status(self._device_id, self._number) + await self._acc.get_doors(self._number) + + @property + def is_closed(self) -> bool | None: + """Update is closed attribute.""" + value = STATES_MAP.get(self._acc.get_door_status(self._device_id, self._number)) + if value is None: + return None + return value == STATE_CLOSED + + @property + def is_closing(self) -> bool: + """Update is closing attribute.""" + return ( + STATES_MAP.get(self._acc.get_door_status(self._device_id, self._number)) + == STATE_CLOSING + ) + + @property + def is_opening(self) -> bool: + """Update is opening attribute.""" + return ( + STATES_MAP.get(self._acc.get_door_status(self._device_id, self._number)) + == STATE_OPENING ) - self._attr_is_opening = status == STATE_OPENING - self._attr_is_closing = status == STATE_CLOSING - self._attr_is_closed = None if status is None else status == STATE_CLOSED diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index b9ea214d996..3a9e295a08f 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["aladdin_connect==0.4"], + "requirements": ["AIOAladdinConnect==0.1.21"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index a30fd52a2a1..f8aad0172a3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -4,6 +4,9 @@ # homeassistant.components.aemet AEMET-OpenData==0.2.1 +# homeassistant.components.aladdin_connect +AIOAladdinConnect==0.1.21 + # homeassistant.components.adax Adax-local==0.1.4 @@ -282,9 +285,6 @@ airthings_cloud==0.1.0 # homeassistant.components.airtouch4 airtouch4pyapi==1.0.5 -# homeassistant.components.aladdin_connect -aladdin_connect==0.4 - # homeassistant.components.alpha_vantage alpha_vantage==2.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3be3fc1fda1..cb3fa2af723 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -6,6 +6,9 @@ # homeassistant.components.aemet AEMET-OpenData==0.2.1 +# homeassistant.components.aladdin_connect +AIOAladdinConnect==0.1.21 + # homeassistant.components.adax Adax-local==0.1.4 @@ -251,9 +254,6 @@ airthings_cloud==0.1.0 # homeassistant.components.airtouch4 airtouch4pyapi==1.0.5 -# homeassistant.components.aladdin_connect -aladdin_connect==0.4 - # homeassistant.components.ambee ambee==0.4.0 diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py new file mode 100644 index 00000000000..ee68d207361 --- /dev/null +++ b/tests/components/aladdin_connect/conftest.py @@ -0,0 +1,39 @@ +"""Fixtures for the Aladdin Connect integration tests.""" +from unittest import mock +from unittest.mock import AsyncMock + +import pytest + +DEVICE_CONFIG_OPEN = { + "device_id": 533255, + "door_number": 1, + "name": "home", + "status": "open", + "link_status": "Connected", +} + + +@pytest.fixture(name="mock_aladdinconnect_api") +def fixture_mock_aladdinconnect_api(): + """Set up aladdin connect API fixture.""" + with mock.patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient" + ) as mock_opener: + mock_opener.login = AsyncMock(return_value=True) + mock_opener.close = AsyncMock(return_value=True) + + mock_opener.async_get_door_status = AsyncMock(return_value="open") + mock_opener.get_door_status.return_value = "open" + mock_opener.async_get_door_link_status = AsyncMock(return_value="connected") + mock_opener.get_door_link_status.return_value = "connected" + mock_opener.async_get_battery_status = AsyncMock(return_value="99") + mock_opener.get_battery_status.return_value = "99" + mock_opener.async_get_rssi_status = AsyncMock(return_value="-55") + mock_opener.get_rssi_status.return_value = "-55" + mock_opener.get_doors = AsyncMock(return_value=[DEVICE_CONFIG_OPEN]) + + mock_opener.register_callback = mock.Mock(return_value=True) + mock_opener.open_door = AsyncMock(return_value=True) + mock_opener.close_door = AsyncMock(return_value=True) + + yield mock_opener diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py index 899aa0a7e55..33117c64110 100644 --- a/tests/components/aladdin_connect/test_config_flow.py +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -1,5 +1,7 @@ """Test the Aladdin Connect config flow.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch + +from aiohttp.client_exceptions import ClientConnectionError from homeassistant import config_entries from homeassistant.components.aladdin_connect.const import DOMAIN @@ -14,8 +16,9 @@ from homeassistant.data_entry_flow import ( from tests.common import MockConfigEntry -async def test_form(hass: HomeAssistant) -> None: +async def test_form(hass: HomeAssistant, mock_aladdinconnect_api: MagicMock) -> None: """Test we get the form.""" + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -23,11 +26,10 @@ async def test_form(hass: HomeAssistant) -> None: assert result["errors"] is None with patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", - return_value=True, + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ), patch( - "homeassistant.components.aladdin_connect.async_setup_entry", - return_value=True, + "homeassistant.components.aladdin_connect.async_setup_entry", return_value=True ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -44,33 +46,21 @@ async def test_form(hass: HomeAssistant) -> None: CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password", } + assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_failed_auth(hass: HomeAssistant) -> None: +async def test_form_failed_auth( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +) -> None: """Test we handle failed authentication error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - + mock_aladdinconnect_api.login.return_value = False with patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", - return_value=False, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, - ) - - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "invalid_auth"} - - with patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", - return_value=False, + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -84,7 +74,33 @@ async def test_form_failed_auth(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "invalid_auth"} -async def test_form_already_configured(hass): +async def test_form_connection_timeout( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +) -> None: + """Test we handle http timeout error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + mock_aladdinconnect_api.login.side_effect = ClientConnectionError + with patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_already_configured( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +): """Test we handle already configured error.""" mock_entry = MockConfigEntry( domain=DOMAIN, @@ -101,8 +117,8 @@ async def test_form_already_configured(hass): assert result["step_id"] == config_entries.SOURCE_USER with patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", - return_value=True, + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -117,18 +133,15 @@ async def test_form_already_configured(hass): assert result2["reason"] == "already_configured" -async def test_import_flow_success(hass: HomeAssistant) -> None: +async def test_import_flow_success( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +) -> None: """Test a successful import of yaml.""" - with patch( - "homeassistant.components.aladdin_connect.cover.async_setup_platform", - return_value=True, + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ), patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", - return_value=True, - ), patch( - "homeassistant.components.aladdin_connect.cover.async_setup_entry", - return_value=True, + "homeassistant.components.aladdin_connect.async_setup_entry", return_value=True ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_init( DOMAIN, @@ -149,7 +162,9 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_reauth_flow(hass: HomeAssistant) -> None: +async def test_reauth_flow( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +) -> None: """Test a successful reauth flow.""" mock_entry = MockConfigEntry( @@ -174,14 +189,11 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert result["errors"] == {} with patch( - "homeassistant.components.aladdin_connect.cover.async_setup_platform", + "homeassistant.components.aladdin_connect.async_setup_entry", return_value=True, ), patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", - return_value=True, - ), patch( - "homeassistant.components.aladdin_connect.cover.async_setup_entry", - return_value=True, + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -197,7 +209,9 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: } -async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None: +async def test_reauth_flow_auth_error( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +) -> None: """Test an authorization error reauth flow.""" mock_entry = MockConfigEntry( @@ -220,13 +234,13 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None: assert result["step_id"] == "reauth_confirm" assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {} - + mock_aladdinconnect_api.login.return_value = False with patch( - "homeassistant.components.aladdin_connect.cover.async_setup_platform", - return_value=True, + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ), patch( - "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient.login", - return_value=False, + "homeassistant.components.aladdin_connect.cover.async_setup_entry", + return_value=True, ), patch( "homeassistant.components.aladdin_connect.cover.async_setup_entry", return_value=True, @@ -239,3 +253,44 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None: assert result2["type"] == RESULT_TYPE_FORM assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_reauth_flow_connnection_error( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +) -> None: + """Test a connection error reauth flow.""" + + 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_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={"username": "test-username", "password": "new-password"}, + ) + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + mock_aladdinconnect_api.login.side_effect = ClientConnectionError + + with patch( + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/aladdin_connect/test_cover.py b/tests/components/aladdin_connect/test_cover.py index c1571ed9fa2..54ec4ee5de1 100644 --- a/tests/components/aladdin_connect/test_cover.py +++ b/tests/components/aladdin_connect/test_cover.py @@ -1,23 +1,29 @@ """Test the Aladdin Connect Cover.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest from homeassistant.components.aladdin_connect.const import DOMAIN +from homeassistant.components.aladdin_connect.cover import SCAN_INTERVAL from homeassistant.components.cover import DOMAIN as COVER_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_PASSWORD, CONF_USERNAME, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING, + STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed YAML_CONFIG = {"username": "test-user", "password": "test-password"} @@ -76,63 +82,11 @@ DEVICE_CONFIG_BAD_NO_DOOR = { } -async def test_setup_get_doors_errors(hass: HomeAssistant) -> None: - """Test component setup Get Doors Errors.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=YAML_CONFIG, - unique_id="test-id", - ) - config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", - return_value=True, - ), patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=None, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) is True - await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 - - -async def test_setup_login_error(hass: HomeAssistant) -> None: - """Test component setup Login Errors.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=YAML_CONFIG, - unique_id="test-id", - ) - config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", - return_value=False, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) is False - - -async def test_setup_component_noerror(hass: HomeAssistant) -> None: - """Test component setup No Error.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=YAML_CONFIG, - unique_id="test-id", - ) - config_entry.add_to_hass(hass) - with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", - return_value=True, - ): - - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert config_entry.state == ConfigEntryState.LOADED - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - - -async def test_cover_operation(hass: HomeAssistant) -> None: - """Test component setup open cover, close cover.""" +async def test_cover_operation( + hass: HomeAssistant, + mock_aladdinconnect_api: MagicMock, +) -> None: + """Test Cover Operation states (open,close,opening,closing) cover.""" config_entry = MockConfigEntry( domain=DOMAIN, data=YAML_CONFIG, @@ -142,92 +96,116 @@ async def test_cover_operation(hass: HomeAssistant) -> None: assert await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() - + mock_aladdinconnect_api.async_get_door_status = AsyncMock(return_value=STATE_OPEN) + mock_aladdinconnect_api.get_door_status.return_value = STATE_OPEN with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", - return_value=True, - ), patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=[DEVICE_CONFIG_OPEN], + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ): - 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 == ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert COVER_DOMAIN in hass.config.components - with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.open_door", - return_value=True, - ), patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=[DEVICE_CONFIG_OPEN], - ): - await hass.services.async_call( - "cover", "open_cover", {"entity_id": "cover.home"}, blocking=True - ) + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.home"}, + blocking=True, + ) + await hass.async_block_till_done() assert hass.states.get("cover.home").state == STATE_OPEN + mock_aladdinconnect_api.async_get_door_status = AsyncMock(return_value=STATE_CLOSED) + mock_aladdinconnect_api.get_door_status.return_value = STATE_CLOSED with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.close_door", - return_value=True, - ), patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=[DEVICE_CONFIG_CLOSED], + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ): + await hass.services.async_call( - "cover", "close_cover", {"entity_id": "cover.home"}, blocking=True + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.home"}, + blocking=True, ) + await hass.async_block_till_done() + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + assert hass.states.get("cover.home").state == STATE_CLOSED - with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=[DEVICE_CONFIG_OPENING], - ): - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True - ) - assert hass.states.get("cover.home").state == STATE_OPENING + mock_aladdinconnect_api.async_get_door_status = AsyncMock( + return_value=STATE_CLOSING + ) + mock_aladdinconnect_api.get_door_status.return_value = STATE_CLOSING with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=[DEVICE_CONFIG_CLOSING], + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ): - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, ) + await hass.async_block_till_done() assert hass.states.get("cover.home").state == STATE_CLOSING - with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=[DEVICE_CONFIG_BAD], - ): - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True - ) - assert hass.states.get("cover.home").state + mock_aladdinconnect_api.async_get_door_status = AsyncMock( + return_value=STATE_OPENING + ) + mock_aladdinconnect_api.get_door_status.return_value = STATE_OPENING with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=[DEVICE_CONFIG_BAD_NO_DOOR], + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ): - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": "cover.home"}, blocking=True + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, ) - assert hass.states.get("cover.home").state + await hass.async_block_till_done() + assert hass.states.get("cover.home").state == STATE_OPENING + + mock_aladdinconnect_api.async_get_door_status = AsyncMock(return_value=None) + mock_aladdinconnect_api.get_door_status.return_value = None + with patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ): + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.home"}, + blocking=True, + ) + await hass.async_block_till_done() + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + + assert hass.states.get("cover.home").state == STATE_UNKNOWN -async def test_yaml_import(hass: HomeAssistant, caplog: pytest.LogCaptureFixture): +async def test_yaml_import( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + mock_aladdinconnect_api: MagicMock, +): """Test setup YAML import.""" assert COVER_DOMAIN not in hass.config.components with patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", - return_value=True, - ), patch( - "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", - return_value=[DEVICE_CONFIG_CLOSED], + "homeassistant.components.aladdin_connect.config_flow.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ): await async_setup_component( hass, @@ -248,3 +226,37 @@ async def test_yaml_import(hass: HomeAssistant, caplog: pytest.LogCaptureFixture config_data = hass.config_entries.async_entries(DOMAIN)[0].data assert config_data[CONF_USERNAME] == "test-user" assert config_data[CONF_PASSWORD] == "test-password" + + +async def test_callback( + hass: HomeAssistant, + mock_aladdinconnect_api: MagicMock, +): + """Test callback from Aladdin Connect API.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=YAML_CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + mock_aladdinconnect_api.async_get_door_status.return_value = STATE_CLOSING + mock_aladdinconnect_api.get_door_status.return_value = STATE_CLOSING + with patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ), patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient._call_back", + AsyncMock(), + ): + callback = mock_aladdinconnect_api.register_callback.call_args[0][0] + await callback() + assert hass.states.get("cover.home").state == STATE_CLOSING diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py index 0ba9b317dfb..4c422ae29ba 100644 --- a/tests/components/aladdin_connect/test_init.py +++ b/tests/components/aladdin_connect/test_init.py @@ -1,35 +1,77 @@ """Test for Aladdin Connect init logic.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch + +from aiohttp import ClientConnectionError from homeassistant.components.aladdin_connect.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry +from tests.common import AsyncMock, MockConfigEntry YAML_CONFIG = {"username": "test-user", "password": "test-password"} -async def test_entry_password_fail(hass: HomeAssistant): - """Test password fail during entry.""" - entry = MockConfigEntry( +async def test_setup_get_doors_errors(hass: HomeAssistant) -> None: + """Test component setup Get Doors Errors.""" + config_entry = MockConfigEntry( domain=DOMAIN, - data={"username": "test-user", "password": "test-password"}, + data=YAML_CONFIG, + unique_id="test-id", ) - entry.add_to_hass(hass) - + config_entry.add_to_hass(hass) with patch( "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.login", - return_value=False, + return_value=True, + ), patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient.get_doors", + return_value=None, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) is True + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + + +async def test_setup_login_error( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +) -> None: + """Test component setup Login Errors.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=YAML_CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + mock_aladdinconnect_api.login.return_value = False + with patch( + "homeassistant.components.aladdin_connect.cover.AladdinConnectClient", + return_value=mock_aladdinconnect_api, ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - assert entry.state is ConfigEntryState.SETUP_ERROR + assert await hass.config_entries.async_setup(config_entry.entry_id) is False -async def test_load_and_unload(hass: HomeAssistant) -> None: - """Test loading and unloading Aladdin Connect entry.""" +async def test_setup_connection_error( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +) -> None: + """Test component setup Login Errors.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=YAML_CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + mock_aladdinconnect_api.login.side_effect = ClientConnectionError + with patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ): + + assert await hass.config_entries.async_setup(config_entry.entry_id) is False + + +async def test_setup_component_no_error(hass: HomeAssistant) -> None: + """Test component setup No Error.""" config_entry = MockConfigEntry( domain=DOMAIN, data=YAML_CONFIG, @@ -41,6 +83,49 @@ async def test_load_and_unload(hass: HomeAssistant) -> None: return_value=True, ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + +async def test_entry_password_fail( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +): + """Test password fail during entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={"username": "test-user", "password": "test-password"}, + ) + entry.add_to_hass(hass) + mock_aladdinconnect_api.login = AsyncMock(return_value=False) + with patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ): + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_load_and_unload( + hass: HomeAssistant, mock_aladdinconnect_api: MagicMock +) -> None: + """Test loading and unloading Aladdin Connect entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=YAML_CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ): + await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() From 48c7e414f68b814dcf5b6dc9e7360deeb2861369 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 28 Jun 2022 23:23:17 +0200 Subject: [PATCH 1906/3516] Update xknx to 0.21.5 - Fix discovery of IP-Secure interfaces (#74147) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index b8f9bdcbd30..0c34428f0a1 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.21.3"], + "requirements": ["xknx==0.21.5"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index f8aad0172a3..04c48d20c70 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2457,7 +2457,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.21.3 +xknx==0.21.5 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb3fa2af723..8520fb665fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1630,7 +1630,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.21.3 +xknx==0.21.5 # homeassistant.components.bluesound # homeassistant.components.fritz From ef5fccad9ed80c7e2e08bedf48186e6acb399804 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 28 Jun 2022 23:23:32 +0200 Subject: [PATCH 1907/3516] Use standard argument name in async_step_reauth (#74139) --- homeassistant/components/abode/config_flow.py | 4 ++-- homeassistant/components/airvisual/config_flow.py | 6 +++--- .../components/aussie_broadband/config_flow.py | 4 ++-- homeassistant/components/axis/config_flow.py | 12 ++++++------ homeassistant/components/deconz/config_flow.py | 8 ++++---- .../components/devolo_home_control/config_flow.py | 6 +++--- homeassistant/components/fritz/config_flow.py | 10 +++++----- homeassistant/components/fritzbox/config_flow.py | 8 ++++---- homeassistant/components/meater/config_flow.py | 4 ++-- homeassistant/components/nam/config_flow.py | 4 ++-- homeassistant/components/nanoleaf/config_flow.py | 6 ++++-- homeassistant/components/nest/config_flow.py | 4 ++-- homeassistant/components/notion/config_flow.py | 4 ++-- homeassistant/components/overkiz/config_flow.py | 4 ++-- homeassistant/components/renault/config_flow.py | 4 ++-- homeassistant/components/ridwell/config_flow.py | 4 ++-- homeassistant/components/simplisafe/config_flow.py | 6 +++--- homeassistant/components/sleepiq/config_flow.py | 4 ++-- homeassistant/components/surepetcare/config_flow.py | 4 ++-- homeassistant/components/synology_dsm/config_flow.py | 4 ++-- homeassistant/components/tile/config_flow.py | 4 ++-- homeassistant/components/watttime/config_flow.py | 4 ++-- homeassistant/components/xiaomi_miio/config_flow.py | 10 +++++----- 23 files changed, 65 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py index 1cbab2bdbe4..4c3d44bebbe 100644 --- a/homeassistant/components/abode/config_flow.py +++ b/homeassistant/components/abode/config_flow.py @@ -150,9 +150,9 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self._async_abode_mfa_login() - async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle reauthorization request from Abode.""" - self._username = config[CONF_USERNAME] + self._username = entry_data[CONF_USERNAME] return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 516b8906092..f97616c38fc 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -221,10 +221,10 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data={**user_input, CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO}, ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" - self._entry_data_for_reauth = data - self._geo_id = async_get_geography_id(data) + self._entry_data_for_reauth = entry_data + self._geo_id = async_get_geography_id(entry_data) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( diff --git a/homeassistant/components/aussie_broadband/config_flow.py b/homeassistant/components/aussie_broadband/config_flow.py index 37de59d4767..c71570b73fb 100644 --- a/homeassistant/components/aussie_broadband/config_flow.py +++ b/homeassistant/components/aussie_broadband/config_flow.py @@ -77,9 +77,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle reauth on credential failure.""" - self._reauth_username = data[CONF_USERNAME] + self._reauth_username = entry_data[CONF_USERNAME] return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 2c5239a8fb3..f94c27dc2ac 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -139,18 +139,18 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): title = f"{model} - {serial}" return self.async_create_entry(title=title, data=self.device_config) - async def async_step_reauth(self, device_config: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Trigger a reauthentication flow.""" self.context["title_placeholders"] = { - CONF_NAME: device_config[CONF_NAME], - CONF_HOST: device_config[CONF_HOST], + CONF_NAME: entry_data[CONF_NAME], + CONF_HOST: entry_data[CONF_HOST], } self.discovery_schema = { - vol.Required(CONF_HOST, default=device_config[CONF_HOST]): str, - vol.Required(CONF_USERNAME, default=device_config[CONF_USERNAME]): str, + vol.Required(CONF_HOST, default=entry_data[CONF_HOST]): str, + vol.Required(CONF_USERNAME, default=entry_data[CONF_USERNAME]): str, vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_PORT, default=device_config[CONF_PORT]): int, + vol.Required(CONF_PORT, default=entry_data[CONF_PORT]): int, } return await self.async_step_user() diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 6e2a286c168..d94b40e8525 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -205,12 +205,12 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Trigger a reauthentication flow.""" - self.context["title_placeholders"] = {CONF_HOST: config[CONF_HOST]} + self.context["title_placeholders"] = {CONF_HOST: entry_data[CONF_HOST]} - self.host = config[CONF_HOST] - self.port = config[CONF_PORT] + self.host = entry_data[CONF_HOST] + self.port = entry_data[CONF_PORT] return await self.async_step_link() diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py index fc1689e0742..aef9592d2e9 100644 --- a/homeassistant/components/devolo_home_control/config_flow.py +++ b/homeassistant/components/devolo_home_control/config_flow.py @@ -68,14 +68,14 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="zeroconf_confirm", errors={"base": "invalid_auth"} ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle reauthentication.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] ) - self._url = data[CONF_MYDEVOLO] + self._url = entry_data[CONF_MYDEVOLO] self.data_schema = { - vol.Required(CONF_USERNAME, default=data[CONF_USERNAME]): str, + vol.Required(CONF_USERNAME, default=entry_data[CONF_USERNAME]): str, vol.Required(CONF_PASSWORD): str, } return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index afb1708cac1..ddc09cb73a9 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -231,13 +231,13 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): return self._async_create_entry() - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle flow upon an API authentication error.""" self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) - self._host = data[CONF_HOST] - self._port = data[CONF_PORT] - self._username = data[CONF_USERNAME] - self._password = data[CONF_PASSWORD] + self._host = entry_data[CONF_HOST] + self._port = entry_data[CONF_PORT] + self._username = entry_data[CONF_USERNAME] + self._password = entry_data[CONF_PASSWORD] return await self.async_step_reauth_confirm() def _show_setup_form_reauth_confirm( diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index d183c4a8d5e..b4e86b92568 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -176,14 +176,14 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Trigger a reauthentication flow.""" entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) assert entry is not None self._entry = entry - self._host = data[CONF_HOST] - self._name = str(data[CONF_HOST]) - self._username = data[CONF_USERNAME] + self._host = entry_data[CONF_HOST] + self._name = str(entry_data[CONF_HOST]) + self._username = entry_data[CONF_USERNAME] return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/meater/config_flow.py b/homeassistant/components/meater/config_flow.py index 8d5e459fbce..91a927a5fb2 100644 --- a/homeassistant/components/meater/config_flow.py +++ b/homeassistant/components/meater/config_flow.py @@ -45,10 +45,10 @@ class MeaterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self._try_connect_meater("user", None, username, password) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._data_schema = REAUTH_SCHEMA - self._username = data[CONF_USERNAME] + self._username = entry_data[CONF_USERNAME] return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py index 451148c22fe..3dc2d7f0ba0 100644 --- a/homeassistant/components/nam/config_flow.py +++ b/homeassistant/components/nam/config_flow.py @@ -190,11 +190,11 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" if entry := self.hass.config_entries.async_get_entry(self.context["entry_id"]): self.entry = entry - self.host = data[CONF_HOST] + self.host = entry_data[CONF_HOST] self.context["title_placeholders"] = {"host": self.host} return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/nanoleaf/config_flow.py b/homeassistant/components/nanoleaf/config_flow.py index eb8bbb1ec66..cab6c8003d0 100644 --- a/homeassistant/components/nanoleaf/config_flow.py +++ b/homeassistant/components/nanoleaf/config_flow.py @@ -78,13 +78,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_step_link() - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle Nanoleaf reauth flow if token is invalid.""" self.reauth_entry = cast( config_entries.ConfigEntry, self.hass.config_entries.async_get_entry(self.context["entry_id"]), ) - self.nanoleaf = Nanoleaf(async_get_clientsession(self.hass), data[CONF_HOST]) + self.nanoleaf = Nanoleaf( + async_get_clientsession(self.hass), entry_data[CONF_HOST] + ) self.context["title_placeholders"] = {"name": self.reauth_entry.title} return await self.async_step_link() diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index 2e89f7970fa..1288592be74 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -218,10 +218,10 @@ class NestFlowHandler( return await self.async_step_finish() return await self.async_step_pubsub() - async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" assert self.config_mode != ConfigMode.LEGACY, "Step only supported for SDM API" - self._data.update(user_input) + self._data.update(entry_data) return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/notion/config_flow.py b/homeassistant/components/notion/config_flow.py index 56093067711..917c0f8ebb9 100644 --- a/homeassistant/components/notion/config_flow.py +++ b/homeassistant/components/notion/config_flow.py @@ -74,9 +74,9 @@ class NotionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=self._username, data=data) - async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" - self._username = config[CONF_USERNAME] + self._username = entry_data[CONF_USERNAME] return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( diff --git a/homeassistant/components/overkiz/config_flow.py b/homeassistant/components/overkiz/config_flow.py index c70a551f4c7..2808c309938 100644 --- a/homeassistant/components/overkiz/config_flow.py +++ b/homeassistant/components/overkiz/config_flow.py @@ -155,7 +155,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle reauth.""" self._config_entry = cast( ConfigEntry, @@ -169,4 +169,4 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._default_user = self._config_entry.data[CONF_USERNAME] self._default_hub = self._config_entry.data[CONF_HUB] - return await self.async_step_user(dict(data)) + return await self.async_step_user(dict(entry_data)) diff --git a/homeassistant/components/renault/config_flow.py b/homeassistant/components/renault/config_flow.py index 539ba7549b3..8f5b99972d1 100644 --- a/homeassistant/components/renault/config_flow.py +++ b/homeassistant/components/renault/config_flow.py @@ -93,9 +93,9 @@ class RenaultFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ), ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" - self._original_data = data + self._original_data = entry_data return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( diff --git a/homeassistant/components/ridwell/config_flow.py b/homeassistant/components/ridwell/config_flow.py index 2d6444fede9..722c20336d4 100644 --- a/homeassistant/components/ridwell/config_flow.py +++ b/homeassistant/components/ridwell/config_flow.py @@ -81,9 +81,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): data={CONF_USERNAME: self._username, CONF_PASSWORD: self._password}, ) - async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" - self._username = config[CONF_USERNAME] + self._username = entry_data[CONF_USERNAME] return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 3fcab1f3966..0b95de2c186 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -98,16 +98,16 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Define the config flow to handle options.""" return SimpliSafeOptionsFlowHandler(config_entry) - async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._reauth = True - if CONF_USERNAME not in config: + if CONF_USERNAME not in entry_data: # Old versions of the config flow may not have the username by this point; # in that case, we reauth them by making them go through the user flow: return await self.async_step_user() - self._username = config[CONF_USERNAME] + self._username = entry_data[CONF_USERNAME] return await self.async_step_reauth_confirm() async def _async_get_email_2fa(self) -> None: diff --git a/homeassistant/components/sleepiq/config_flow.py b/homeassistant/components/sleepiq/config_flow.py index c78daa76fbc..16034b64e8b 100644 --- a/homeassistant/components/sleepiq/config_flow.py +++ b/homeassistant/components/sleepiq/config_flow.py @@ -80,12 +80,12 @@ class SleepIQFlowHandler(ConfigFlow, domain=DOMAIN): last_step=True, ) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] ) - return await self.async_step_reauth_confirm(dict(data)) + return await self.async_step_reauth_confirm(dict(entry_data)) async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/surepetcare/config_flow.py b/homeassistant/components/surepetcare/config_flow.py index b8b3d690a8b..7c4509259ad 100644 --- a/homeassistant/components/surepetcare/config_flow.py +++ b/homeassistant/components/surepetcare/config_flow.py @@ -87,9 +87,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=USER_DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" - self._username = config[CONF_USERNAME] + self._username = entry_data[CONF_USERNAME] return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index bc35caf300e..89bbc4ae8c2 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -300,9 +300,9 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): user_input = {**self.discovered_conf, **user_input} return await self.async_validate_input_create_entry(user_input, step_id=step) - async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" - self.reauth_conf = data + self.reauth_conf = entry_data return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( diff --git a/homeassistant/components/tile/config_flow.py b/homeassistant/components/tile/config_flow.py index c47a46b3b10..3ba1dc411ae 100644 --- a/homeassistant/components/tile/config_flow.py +++ b/homeassistant/components/tile/config_flow.py @@ -75,9 +75,9 @@ class TileFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_config) - async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" - self._username = config[CONF_USERNAME] + self._username = entry_data[CONF_USERNAME] return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( diff --git a/homeassistant/components/watttime/config_flow.py b/homeassistant/components/watttime/config_flow.py index 4d6985ec616..a5d9c6925c8 100644 --- a/homeassistant/components/watttime/config_flow.py +++ b/homeassistant/components/watttime/config_flow.py @@ -190,9 +190,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return await self.async_step_coordinates() - async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" - self._data = {**config} + self._data = {**entry_data} return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index e5b5275757c..4e2ba24bc05 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -129,12 +129,12 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow.""" return OptionsFlowHandler(config_entry) - async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an authentication error or missing cloud credentials.""" - self.host = user_input[CONF_HOST] - self.token = user_input[CONF_TOKEN] - self.mac = user_input[CONF_MAC] - self.model = user_input.get(CONF_MODEL) + self.host = entry_data[CONF_HOST] + self.token = entry_data[CONF_TOKEN] + self.mac = entry_data[CONF_MAC] + self.model = entry_data.get(CONF_MODEL) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( From 4aa8570107a37f77cdb17cbc5f14ce42c7ab4b4e Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Tue, 28 Jun 2022 18:26:25 -0300 Subject: [PATCH 1908/3516] Set Google Cast audio devices as speakers (#73832) --- homeassistant/components/cast/media_player.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index ea21259ccc4..958c53ae394 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -30,6 +30,7 @@ from homeassistant.components import media_source, zeroconf from homeassistant.components.media_player import ( BrowseError, BrowseMedia, + MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, async_process_play_media_url, @@ -300,6 +301,12 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): name=str(cast_info.friendly_name), ) + if cast_info.cast_info.cast_type in [ + pychromecast.const.CAST_TYPE_AUDIO, + pychromecast.const.CAST_TYPE_GROUP, + ]: + self._attr_device_class = MediaPlayerDeviceClass.SPEAKER + async def async_added_to_hass(self): """Create chromecast object when added to hass.""" self._async_setup(self.entity_id) From ef76073d8336717c865e2560076f2b0379d2f456 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 28 Jun 2022 23:31:15 +0200 Subject: [PATCH 1909/3516] Add Netgear ethernet link status (#72582) --- homeassistant/components/netgear/__init__.py | 14 ++++++++++++++ homeassistant/components/netgear/const.py | 1 + homeassistant/components/netgear/router.py | 5 +++++ homeassistant/components/netgear/sensor.py | 16 ++++++++++++++++ 4 files changed, 36 insertions(+) diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index 679a93f8da1..953008ae9f5 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -15,6 +15,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( DOMAIN, KEY_COORDINATOR, + KEY_COORDINATOR_LINK, KEY_COORDINATOR_SPEED, KEY_COORDINATOR_TRAFFIC, KEY_ROUTER, @@ -83,6 +84,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Fetch data from the router.""" return await router.async_get_speed_test() + async def async_check_link_status() -> dict[str, Any] | None: + """Fetch data from the router.""" + return await router.async_get_link_status() + # Create update coordinators coordinator = DataUpdateCoordinator( hass, @@ -105,16 +110,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=async_update_speed_test, update_interval=SPEED_TEST_INTERVAL, ) + coordinator_link = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"{router.device_name} Ethernet Link Status", + update_method=async_check_link_status, + update_interval=SCAN_INTERVAL, + ) if router.track_devices: await coordinator.async_config_entry_first_refresh() await coordinator_traffic_meter.async_config_entry_first_refresh() + await coordinator_link.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = { KEY_ROUTER: router, KEY_COORDINATOR: coordinator, KEY_COORDINATOR_TRAFFIC: coordinator_traffic_meter, KEY_COORDINATOR_SPEED: coordinator_speed_test, + KEY_COORDINATOR_LINK: coordinator_link, } hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/netgear/const.py b/homeassistant/components/netgear/const.py index 936777a7961..c8939208047 100644 --- a/homeassistant/components/netgear/const.py +++ b/homeassistant/components/netgear/const.py @@ -13,6 +13,7 @@ KEY_ROUTER = "router" KEY_COORDINATOR = "coordinator" KEY_COORDINATOR_TRAFFIC = "coordinator_traffic" KEY_COORDINATOR_SPEED = "coordinator_speed" +KEY_COORDINATOR_LINK = "coordinator_link" DEFAULT_CONSIDER_HOME = timedelta(seconds=180) DEFAULT_NAME = "Netgear router" diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 67e573d0e92..6284c6f4ac2 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -228,6 +228,11 @@ class NetgearRouter: self._api.get_new_speed_test_result ) + async def async_get_link_status(self) -> dict[str, Any] | None: + """Check the ethernet link status of the router.""" + async with self._api_lock: + return await self.hass.async_add_executor_job(self._api.check_ethernet_link) + async def async_allow_block_device(self, mac: str, allow_block: str) -> None: """Allow or block a device connected to the router.""" async with self._api_lock: diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index a1cf134beda..f860b65e10f 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -29,6 +29,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( DOMAIN, KEY_COORDINATOR, + KEY_COORDINATOR_LINK, KEY_COORDINATOR_SPEED, KEY_COORDINATOR_TRAFFIC, KEY_ROUTER, @@ -244,6 +245,15 @@ SENSOR_SPEED_TYPES = [ ), ] +SENSOR_LINK_TYPES = [ + NetgearSensorEntityDescription( + key="NewEthernetLinkStatus", + name="Ethernet Link Status", + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:ethernet", + ), +] + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -253,6 +263,7 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR] coordinator_traffic = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR_TRAFFIC] coordinator_speed = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR_SPEED] + coordinator_link = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR_LINK] # Router entities router_entities = [] @@ -267,6 +278,11 @@ async def async_setup_entry( NetgearRouterSensorEntity(coordinator_speed, router, description) ) + for description in SENSOR_LINK_TYPES: + router_entities.append( + NetgearRouterSensorEntity(coordinator_link, router, description) + ) + async_add_entities(router_entities) # Entities per network device From f5fe210eca9286bed3bdaeff2274e0ea74565ba1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 29 Jun 2022 00:23:03 +0000 Subject: [PATCH 1910/3516] [ci skip] Translation update --- .../components/aemet/translations/bg.json | 3 +++ .../components/agent_dvr/translations/bg.json | 3 +++ .../components/airvisual/translations/bg.json | 3 ++- .../ambient_station/translations/bg.json | 3 +++ .../components/asuswrt/translations/bg.json | 2 ++ .../components/atag/translations/bg.json | 3 +++ .../components/august/translations/bg.json | 14 +++++++++++++ .../components/awair/translations/ca.json | 7 +++++++ .../components/awair/translations/de.json | 9 +++++++- .../components/awair/translations/et.json | 7 +++++++ .../components/awair/translations/fr.json | 7 +++++++ .../components/awair/translations/hu.json | 7 +++++++ .../components/awair/translations/it.json | 7 +++++++ .../components/awair/translations/nl.json | 6 ++++++ .../components/awair/translations/no.json | 7 +++++++ .../components/awair/translations/pt-BR.json | 7 +++++++ .../azure_devops/translations/bg.json | 3 ++- .../components/cast/translations/bg.json | 3 +++ .../components/directv/translations/bg.json | 1 + .../components/econet/translations/bg.json | 4 ++++ .../components/emonitor/translations/bg.json | 5 +++++ .../enphase_envoy/translations/bg.json | 3 +++ .../components/ezviz/translations/bg.json | 1 + .../flunearyou/translations/bg.json | 14 +++++++++++++ .../components/freebox/translations/bg.json | 3 ++- .../components/fritz/translations/bg.json | 1 + .../fritzbox_callmonitor/translations/bg.json | 5 +++++ .../geonetnz_quakes/translations/bg.json | 3 +++ .../components/google/translations/bg.json | 1 + .../components/google/translations/nl.json | 1 + .../components/group/translations/bg.json | 9 +++++++- .../components/hangouts/translations/de.json | 1 + .../components/harmony/translations/bg.json | 8 +++++++ .../components/heos/translations/bg.json | 3 +++ .../components/hive/translations/bg.json | 21 +++++++++++++++++++ .../components/hive/translations/nl.json | 7 +++++++ .../components/icloud/translations/bg.json | 5 +++++ .../integration/translations/bg.json | 1 + .../components/ipp/translations/bg.json | 3 +++ .../keenetic_ndms2/translations/bg.json | 3 +++ .../kostal_plenticore/translations/bg.json | 8 +++++++ .../components/lcn/translations/de.json | 1 + .../components/lcn/translations/it.json | 1 + .../components/lcn/translations/pt-BR.json | 1 + .../components/life360/translations/bg.json | 3 ++- .../components/litejet/translations/bg.json | 3 +++ .../litterrobot/translations/bg.json | 3 +++ .../components/mazda/translations/bg.json | 2 ++ .../met_eireann/translations/bg.json | 1 + .../motion_blinds/translations/bg.json | 1 + .../components/motioneye/translations/bg.json | 4 ++++ .../components/mullvad/translations/bg.json | 4 ++++ .../components/myq/translations/bg.json | 17 +++++++++++++++ .../components/mysensors/translations/bg.json | 4 ++++ .../components/nest/translations/nl.json | 1 + .../components/nexia/translations/bg.json | 6 ++++++ .../components/nuheat/translations/bg.json | 1 + .../components/nut/translations/bg.json | 3 +++ .../overkiz/translations/sensor.nl.json | 5 +++++ .../panasonic_viera/translations/bg.json | 3 ++- .../components/picnic/translations/bg.json | 7 +++++++ .../components/rfxtrx/translations/bg.json | 1 + .../components/risco/translations/bg.json | 9 ++++++++ .../translations/bg.json | 3 +++ .../components/roomba/translations/bg.json | 3 +++ .../ruckus_unleashed/translations/bg.json | 4 ++++ .../screenlogic/translations/bg.json | 1 + .../components/sense/translations/bg.json | 3 +++ .../simplepush/translations/nl.json | 11 ++++++++++ .../components/sma/translations/bg.json | 11 ++++++++++ .../components/smappee/translations/bg.json | 1 + .../components/smarttub/translations/bg.json | 3 +++ .../somfy_mylink/translations/bg.json | 3 +++ .../components/subaru/translations/bg.json | 4 ++++ .../components/syncthru/translations/bg.json | 3 ++- .../transmission/translations/nl.json | 10 ++++++++- .../twentemilieu/translations/bg.json | 1 + .../components/vacuum/translations/no.json | 2 +- .../components/verisure/translations/bg.json | 10 +++++++++ .../components/vulcan/translations/bg.json | 3 ++- .../wolflink/translations/sensor.bg.json | 3 +++ 81 files changed, 361 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/august/translations/bg.json create mode 100644 homeassistant/components/emonitor/translations/bg.json create mode 100644 homeassistant/components/flunearyou/translations/bg.json create mode 100644 homeassistant/components/harmony/translations/bg.json create mode 100644 homeassistant/components/kostal_plenticore/translations/bg.json create mode 100644 homeassistant/components/myq/translations/bg.json create mode 100644 homeassistant/components/simplepush/translations/nl.json create mode 100644 homeassistant/components/sma/translations/bg.json create mode 100644 homeassistant/components/verisure/translations/bg.json diff --git a/homeassistant/components/aemet/translations/bg.json b/homeassistant/components/aemet/translations/bg.json index 62d0a34441a..4823a30fd97 100644 --- a/homeassistant/components/aemet/translations/bg.json +++ b/homeassistant/components/aemet/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" + }, "error": { "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447" }, diff --git a/homeassistant/components/agent_dvr/translations/bg.json b/homeassistant/components/agent_dvr/translations/bg.json index 527adb67bf7..cc5f200ef95 100644 --- a/homeassistant/components/agent_dvr/translations/bg.json +++ b/homeassistant/components/agent_dvr/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\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/airvisual/translations/bg.json b/homeassistant/components/airvisual/translations/bg.json index 114a1547549..807b9556240 100644 --- a/homeassistant/components/airvisual/translations/bg.json +++ b/homeassistant/components/airvisual/translations/bg.json @@ -11,7 +11,8 @@ "step": { "geography_by_coords": { "data": { - "api_key": "API \u043a\u043b\u044e\u0447" + "api_key": "API \u043a\u043b\u044e\u0447", + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430" } }, "geography_by_name": { diff --git a/homeassistant/components/ambient_station/translations/bg.json b/homeassistant/components/ambient_station/translations/bg.json index 173b1c39c5f..9e55323228f 100644 --- a/homeassistant/components/ambient_station/translations/bg.json +++ b/homeassistant/components/ambient_station/translations/bg.json @@ -1,5 +1,8 @@ { "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": { "invalid_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447 \u0438/\u0438\u043b\u0438 Application \u043a\u043b\u044e\u0447", "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" diff --git a/homeassistant/components/asuswrt/translations/bg.json b/homeassistant/components/asuswrt/translations/bg.json index dbb5f415f92..df452e48980 100644 --- a/homeassistant/components/asuswrt/translations/bg.json +++ b/homeassistant/components/asuswrt/translations/bg.json @@ -1,11 +1,13 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \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": { + "mode": "\u0420\u0435\u0436\u0438\u043c", "name": "\u0418\u043c\u0435", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "port": "\u041f\u043e\u0440\u0442", diff --git a/homeassistant/components/atag/translations/bg.json b/homeassistant/components/atag/translations/bg.json index 2dd2ff1750c..0d30d7c1e16 100644 --- a/homeassistant/components/atag/translations/bg.json +++ b/homeassistant/components/atag/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" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/august/translations/bg.json b/homeassistant/components/august/translations/bg.json new file mode 100644 index 00000000000..224e3324cb6 --- /dev/null +++ b/homeassistant/components/august/translations/bg.json @@ -0,0 +1,14 @@ +{ + "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_validate": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/ca.json b/homeassistant/components/awair/translations/ca.json index 12384b088bb..3510d3a3a8b 100644 --- a/homeassistant/components/awair/translations/ca.json +++ b/homeassistant/components/awair/translations/ca.json @@ -17,6 +17,13 @@ }, "description": "Torna a introduir el token d'acc\u00e9s de desenvolupador d'Awair." }, + "reauth_confirm": { + "data": { + "access_token": "Token d'acc\u00e9s", + "email": "Correu electr\u00f2nic" + }, + "description": "Torna a introduir el 'token' d'acc\u00e9s de desenvolupador d'Awair." + }, "user": { "data": { "access_token": "Token d'acc\u00e9s", diff --git a/homeassistant/components/awair/translations/de.json b/homeassistant/components/awair/translations/de.json index 1dacaf099dc..c28ee6bc016 100644 --- a/homeassistant/components/awair/translations/de.json +++ b/homeassistant/components/awair/translations/de.json @@ -15,7 +15,14 @@ "access_token": "Zugangstoken", "email": "E-Mail" }, - "description": "Bitte gib dein Awair-Entwicklerzugriffstoken erneut ein." + "description": "Bitte gib deinen Awair-Entwicklerzugriffstoken erneut ein." + }, + "reauth_confirm": { + "data": { + "access_token": "Zugangstoken", + "email": "E-Mail" + }, + "description": "Bitte gib deinen Awair-Entwicklerzugriffstoken erneut ein." }, "user": { "data": { diff --git a/homeassistant/components/awair/translations/et.json b/homeassistant/components/awair/translations/et.json index 374db23e18e..70632b292e4 100644 --- a/homeassistant/components/awair/translations/et.json +++ b/homeassistant/components/awair/translations/et.json @@ -17,6 +17,13 @@ }, "description": "Taassisesta oma Awairi arendaja juurdep\u00e4\u00e4suluba." }, + "reauth_confirm": { + "data": { + "access_token": "Juurdep\u00e4\u00e4sut\u00f5end", + "email": "Meiliaadress" + }, + "description": "Taassisesta oma Awairi arendaja juurdep\u00e4\u00e4suluba." + }, "user": { "data": { "access_token": "Juurdep\u00e4\u00e4sut\u00f5end", diff --git a/homeassistant/components/awair/translations/fr.json b/homeassistant/components/awair/translations/fr.json index 7182117fa53..fd915507762 100644 --- a/homeassistant/components/awair/translations/fr.json +++ b/homeassistant/components/awair/translations/fr.json @@ -17,6 +17,13 @@ }, "description": "Veuillez ressaisir votre jeton d'acc\u00e8s d\u00e9veloppeur Awair." }, + "reauth_confirm": { + "data": { + "access_token": "Jeton d'acc\u00e8s", + "email": "Courriel" + }, + "description": "Veuillez ressaisir votre jeton d'acc\u00e8s d\u00e9veloppeur Awair." + }, "user": { "data": { "access_token": "Jeton d'acc\u00e8s", diff --git a/homeassistant/components/awair/translations/hu.json b/homeassistant/components/awair/translations/hu.json index e3994430a8b..2e81b31f187 100644 --- a/homeassistant/components/awair/translations/hu.json +++ b/homeassistant/components/awair/translations/hu.json @@ -17,6 +17,13 @@ }, "description": "Adja meg \u00fajra az Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si tokent." }, + "reauth_confirm": { + "data": { + "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", + "email": "E-mail" + }, + "description": "Adja meg \u00fajra az Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si tokent." + }, "user": { "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", diff --git a/homeassistant/components/awair/translations/it.json b/homeassistant/components/awair/translations/it.json index c9480ecaaa0..27ec006fb06 100644 --- a/homeassistant/components/awair/translations/it.json +++ b/homeassistant/components/awair/translations/it.json @@ -17,6 +17,13 @@ }, "description": "Inserisci nuovamente il tuo token di accesso per sviluppatori Awair." }, + "reauth_confirm": { + "data": { + "access_token": "Token di accesso", + "email": "Email" + }, + "description": "Inserisci nuovamente il tuo token di accesso sviluppatore Awair." + }, "user": { "data": { "access_token": "Token di accesso", diff --git a/homeassistant/components/awair/translations/nl.json b/homeassistant/components/awair/translations/nl.json index 1b58405af32..ff270b6084f 100644 --- a/homeassistant/components/awair/translations/nl.json +++ b/homeassistant/components/awair/translations/nl.json @@ -17,6 +17,12 @@ }, "description": "Voer uw Awair-ontwikkelaarstoegangstoken opnieuw in." }, + "reauth_confirm": { + "data": { + "access_token": "Toegangstoken", + "email": "E-mail" + } + }, "user": { "data": { "access_token": "Toegangstoken", diff --git a/homeassistant/components/awair/translations/no.json b/homeassistant/components/awair/translations/no.json index 98486a28b09..13232ca37df 100644 --- a/homeassistant/components/awair/translations/no.json +++ b/homeassistant/components/awair/translations/no.json @@ -17,6 +17,13 @@ }, "description": "Skriv inn tilgangstokenet for Awair-utviklere p\u00e5 nytt." }, + "reauth_confirm": { + "data": { + "access_token": "Tilgangstoken", + "email": "E-post" + }, + "description": "Skriv inn Awair-utviklertilgangstokenet ditt p\u00e5 nytt." + }, "user": { "data": { "access_token": "Tilgangstoken", diff --git a/homeassistant/components/awair/translations/pt-BR.json b/homeassistant/components/awair/translations/pt-BR.json index 635a7373b75..7406bdf3ee0 100644 --- a/homeassistant/components/awair/translations/pt-BR.json +++ b/homeassistant/components/awair/translations/pt-BR.json @@ -17,6 +17,13 @@ }, "description": "Insira novamente seu token de acesso de desenvolvedor Awair." }, + "reauth_confirm": { + "data": { + "access_token": "Token de acesso", + "email": "E-mail" + }, + "description": "Insira novamente seu token de acesso de desenvolvedor Awair." + }, "user": { "data": { "access_token": "Token de acesso", diff --git a/homeassistant/components/azure_devops/translations/bg.json b/homeassistant/components/azure_devops/translations/bg.json index 60c6d07d013..28af7ef6e00 100644 --- a/homeassistant/components/azure_devops/translations/bg.json +++ b/homeassistant/components/azure_devops/translations/bg.json @@ -12,7 +12,8 @@ "step": { "user": { "data": { - "organization": "\u041e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044f" + "organization": "\u041e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044f", + "project": "\u041f\u0440\u043e\u0435\u043a\u0442" } } } diff --git a/homeassistant/components/cast/translations/bg.json b/homeassistant/components/cast/translations/bg.json index 0ab9d863eff..d5103f596e8 100644 --- a/homeassistant/components/cast/translations/bg.json +++ b/homeassistant/components/cast/translations/bg.json @@ -4,6 +4,9 @@ "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 Google Cast." }, "step": { + "config": { + "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0430 Google Cast" + }, "confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Google Cast?" } diff --git a/homeassistant/components/directv/translations/bg.json b/homeassistant/components/directv/translations/bg.json index ffb69776060..b43da9ecb18 100644 --- a/homeassistant/components/directv/translations/bg.json +++ b/homeassistant/components/directv/translations/bg.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/econet/translations/bg.json b/homeassistant/components/econet/translations/bg.json index cef3726d759..3468d506903 100644 --- a/homeassistant/components/econet/translations/bg.json +++ b/homeassistant/components/econet/translations/bg.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "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" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/emonitor/translations/bg.json b/homeassistant/components/emonitor/translations/bg.json new file mode 100644 index 00000000000..e8940bef26a --- /dev/null +++ b/homeassistant/components/emonitor/translations/bg.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/bg.json b/homeassistant/components/enphase_envoy/translations/bg.json index c0ccf23f5b5..ffb593eb287 100644 --- a/homeassistant/components/enphase_envoy/translations/bg.json +++ b/homeassistant/components/enphase_envoy/translations/bg.json @@ -2,6 +2,9 @@ "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": { + "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/ezviz/translations/bg.json b/homeassistant/components/ezviz/translations/bg.json index e198a4e53f4..702e3b80001 100644 --- a/homeassistant/components/ezviz/translations/bg.json +++ b/homeassistant/components/ezviz/translations/bg.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{serial}", "step": { "user": { "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Ezviz Cloud" diff --git a/homeassistant/components/flunearyou/translations/bg.json b/homeassistant/components/flunearyou/translations/bg.json new file mode 100644 index 00000000000..360abac2642 --- /dev/null +++ b/homeassistant/components/flunearyou/translations/bg.json @@ -0,0 +1,14 @@ +{ + "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": { + "latitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0448\u0438\u0440\u0438\u043d\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/bg.json b/homeassistant/components/freebox/translations/bg.json index c8526b8367d..9a63019cd8a 100644 --- a/homeassistant/components/freebox/translations/bg.json +++ b/homeassistant/components/freebox/translations/bg.json @@ -1,7 +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" + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { "user": { diff --git a/homeassistant/components/fritz/translations/bg.json b/homeassistant/components/fritz/translations/bg.json index 43f9aaf5357..b699cec829c 100644 --- a/homeassistant/components/fritz/translations/bg.json +++ b/homeassistant/components/fritz/translations/bg.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/bg.json b/homeassistant/components/fritzbox_callmonitor/translations/bg.json index fc2115d9ca0..ed2dd868df7 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/bg.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/bg.json @@ -1,6 +1,11 @@ { "config": { "step": { + "phonebook": { + "data": { + "phonebook": "\u0422\u0435\u043b\u0435\u0444\u043e\u043d\u0435\u043d \u0443\u043a\u0430\u0437\u0430\u0442\u0435\u043b" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/geonetnz_quakes/translations/bg.json b/homeassistant/components/geonetnz_quakes/translations/bg.json index 8b4d3e91f2c..23e5c0241d9 100644 --- a/homeassistant/components/geonetnz_quakes/translations/bg.json +++ b/homeassistant/components/geonetnz_quakes/translations/bg.json @@ -1,5 +1,8 @@ { "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" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/google/translations/bg.json b/homeassistant/components/google/translations/bg.json index 0d82b088635..cd81f011d5a 100644 --- a/homeassistant/components/google/translations/bg.json +++ b/homeassistant/components/google/translations/bg.json @@ -2,6 +2,7 @@ "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", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "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" }, diff --git a/homeassistant/components/google/translations/nl.json b/homeassistant/components/google/translations/nl.json index 419259d66f8..2f8d67af1e2 100644 --- a/homeassistant/components/google/translations/nl.json +++ b/homeassistant/components/google/translations/nl.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Account is al geconfigureerd", "already_in_progress": "De configuratie is momenteel al bezig", + "cannot_connect": "Kan geen verbinding maken", "code_expired": "De authenticatiecode is verlopen of de instelling van de inloggegevens is ongeldig, probeer het opnieuw.", "invalid_access_token": "Ongeldig toegangstoken", "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", diff --git a/homeassistant/components/group/translations/bg.json b/homeassistant/components/group/translations/bg.json index 5ecb4d66328..dea9870e1b7 100644 --- a/homeassistant/components/group/translations/bg.json +++ b/homeassistant/components/group/translations/bg.json @@ -11,7 +11,14 @@ "fan": { "data": { "name": "\u0418\u043c\u0435" - } + }, + "title": "\u0414\u043e\u0431\u0430\u0432\u044f\u043d\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0430" + }, + "light": { + "data": { + "name": "\u0418\u043c\u0435" + }, + "title": "\u0414\u043e\u0431\u0430\u0432\u044f\u043d\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0430" }, "lock": { "data": { diff --git a/homeassistant/components/hangouts/translations/de.json b/homeassistant/components/hangouts/translations/de.json index b26618940be..53225644a2d 100644 --- a/homeassistant/components/hangouts/translations/de.json +++ b/homeassistant/components/hangouts/translations/de.json @@ -14,6 +14,7 @@ "data": { "2fa": "2FA PIN" }, + "description": "Leer", "title": "2-Faktor-Authentifizierung" }, "user": { diff --git a/homeassistant/components/harmony/translations/bg.json b/homeassistant/components/harmony/translations/bg.json new file mode 100644 index 00000000000..b961225fc1d --- /dev/null +++ b/homeassistant/components/harmony/translations/bg.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/translations/bg.json b/homeassistant/components/heos/translations/bg.json index ced8d049372..aa967795627 100644 --- a/homeassistant/components/heos/translations/bg.json +++ b/homeassistant/components/heos/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/hive/translations/bg.json b/homeassistant/components/hive/translations/bg.json index ac28082fbed..082fb940fca 100644 --- a/homeassistant/components/hive/translations/bg.json +++ b/homeassistant/components/hive/translations/bg.json @@ -2,6 +2,27 @@ "config": { "error": { "no_internet_available": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u043e\u0441\u0442 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0436\u0435\u0442\u0435 \u0441 Hive." + }, + "step": { + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043d\u0430 \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435 (\u0441\u0435\u043a\u0443\u043d\u0434\u0438)" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/hive/translations/nl.json b/homeassistant/components/hive/translations/nl.json index 2cbc7c94fc6..af0c9bcbfb2 100644 --- a/homeassistant/components/hive/translations/nl.json +++ b/homeassistant/components/hive/translations/nl.json @@ -20,6 +20,13 @@ "description": "Voer uw Hive-verificatiecode in. \n \n Voer code 0000 in om een andere code aan te vragen.", "title": "Hive tweefactorauthenticatie" }, + "configuration": { + "data": { + "device_name": "Apparaatnaam" + }, + "description": "Voer uw Hive-configuratie in", + "title": "Hive-configuratie." + }, "reauth": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/icloud/translations/bg.json b/homeassistant/components/icloud/translations/bg.json index 52c6ed9b018..4ee0a45d19d 100644 --- a/homeassistant/components/icloud/translations/bg.json +++ b/homeassistant/components/icloud/translations/bg.json @@ -8,6 +8,11 @@ "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": { + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430", diff --git a/homeassistant/components/integration/translations/bg.json b/homeassistant/components/integration/translations/bg.json index 35cfa0ad1d7..ac35010224f 100644 --- a/homeassistant/components/integration/translations/bg.json +++ b/homeassistant/components/integration/translations/bg.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "method": "\u041c\u0435\u0442\u043e\u0434 \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u0430\u043d\u0435", "name": "\u0418\u043c\u0435" } } diff --git a/homeassistant/components/ipp/translations/bg.json b/homeassistant/components/ipp/translations/bg.json index d454fe68170..19680bdcfb4 100644 --- a/homeassistant/components/ipp/translations/bg.json +++ b/homeassistant/components/ipp/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/keenetic_ndms2/translations/bg.json b/homeassistant/components/keenetic_ndms2/translations/bg.json index 3bebf2d185e..42c3174a4c4 100644 --- a/homeassistant/components/keenetic_ndms2/translations/bg.json +++ b/homeassistant/components/keenetic_ndms2/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "flow_title": "{name} ({host})", "step": { "user": { diff --git a/homeassistant/components/kostal_plenticore/translations/bg.json b/homeassistant/components/kostal_plenticore/translations/bg.json new file mode 100644 index 00000000000..23968d0a06a --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/bg.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/de.json b/homeassistant/components/lcn/translations/de.json index b4a731fc1f6..f74b3d25dcf 100644 --- a/homeassistant/components/lcn/translations/de.json +++ b/homeassistant/components/lcn/translations/de.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "Codeschloss Code erhalten", "fingerprint": "Fingerabdruckcode empfangen", "send_keys": "Sende Tasten empfangen", "transmitter": "Sendercode empfangen", diff --git a/homeassistant/components/lcn/translations/it.json b/homeassistant/components/lcn/translations/it.json index e42f52b9c62..a39ef71ffd7 100644 --- a/homeassistant/components/lcn/translations/it.json +++ b/homeassistant/components/lcn/translations/it.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "codice di blocco ricevuto", "fingerprint": "codice impronta digitale ricevuto", "send_keys": "invia chiavi ricevute", "transmitter": "codice trasmettitore ricevuto", diff --git a/homeassistant/components/lcn/translations/pt-BR.json b/homeassistant/components/lcn/translations/pt-BR.json index 9898533ea72..7a062fe9578 100644 --- a/homeassistant/components/lcn/translations/pt-BR.json +++ b/homeassistant/components/lcn/translations/pt-BR.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "c\u00f3digo de bloqueio de c\u00f3digo recebido", "fingerprint": "c\u00f3digo de impress\u00e3o digital recebido", "send_keys": "enviar chaves recebidas", "transmitter": "c\u00f3digo do transmissor recebido", diff --git a/homeassistant/components/life360/translations/bg.json b/homeassistant/components/life360/translations/bg.json index 5436cfcf718..d206a606b89 100644 --- a/homeassistant/components/life360/translations/bg.json +++ b/homeassistant/components/life360/translations/bg.json @@ -8,7 +8,8 @@ }, "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" + "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", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { "user": { diff --git a/homeassistant/components/litejet/translations/bg.json b/homeassistant/components/litejet/translations/bg.json index 8c1b2bfb218..c4ccfa52041 100644 --- a/homeassistant/components/litejet/translations/bg.json +++ b/homeassistant/components/litejet/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "\u0412\u0435\u0447\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": { "data": { diff --git a/homeassistant/components/litterrobot/translations/bg.json b/homeassistant/components/litterrobot/translations/bg.json index 67a484573aa..bad1fba5a87 100644 --- a/homeassistant/components/litterrobot/translations/bg.json +++ b/homeassistant/components/litterrobot/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/mazda/translations/bg.json b/homeassistant/components/mazda/translations/bg.json index e51e1112202..6e9ce8d9a6a 100644 --- a/homeassistant/components/mazda/translations/bg.json +++ b/homeassistant/components/mazda/translations/bg.json @@ -1,11 +1,13 @@ { "config": { "error": { + "account_locked": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0435 \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d. \u041c\u043e\u043b\u044f, \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u043f\u043e-\u043a\u044a\u0441\u043d\u043e.", "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", "region": "\u0420\u0435\u0433\u0438\u043e\u043d" } diff --git a/homeassistant/components/met_eireann/translations/bg.json b/homeassistant/components/met_eireann/translations/bg.json index 35cfa0ad1d7..2c39cd06b7d 100644 --- a/homeassistant/components/met_eireann/translations/bg.json +++ b/homeassistant/components/met_eireann/translations/bg.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "longitude": "\u0413\u0435\u043e\u0433\u0440\u0430\u0444\u0441\u043a\u0430 \u0434\u044a\u043b\u0436\u0438\u043d\u0430", "name": "\u0418\u043c\u0435" } } diff --git a/homeassistant/components/motion_blinds/translations/bg.json b/homeassistant/components/motion_blinds/translations/bg.json index e78d7032040..1629a8d981c 100644 --- a/homeassistant/components/motion_blinds/translations/bg.json +++ b/homeassistant/components/motion_blinds/translations/bg.json @@ -4,6 +4,7 @@ "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" }, + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/motioneye/translations/bg.json b/homeassistant/components/motioneye/translations/bg.json index 02c83a6e916..ef56ca3f2df 100644 --- a/homeassistant/components/motioneye/translations/bg.json +++ b/homeassistant/components/motioneye/translations/bg.json @@ -1,8 +1,12 @@ { "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": { + "admin_password": "\u0410\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0441\u043a\u0430 \u043f\u0430\u0440\u043e\u043b\u0430", "url": "URL" } } diff --git a/homeassistant/components/mullvad/translations/bg.json b/homeassistant/components/mullvad/translations/bg.json index 5d274ec2b73..9862b6b3a2a 100644 --- a/homeassistant/components/mullvad/translations/bg.json +++ b/homeassistant/components/mullvad/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": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \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" } } diff --git a/homeassistant/components/myq/translations/bg.json b/homeassistant/components/myq/translations/bg.json new file mode 100644 index 00000000000..9c1d3ecccb8 --- /dev/null +++ b/homeassistant/components/myq/translations/bg.json @@ -0,0 +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" + }, + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "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/mysensors/translations/bg.json b/homeassistant/components/mysensors/translations/bg.json index 69f11b05ce4..7c8e0080bc2 100644 --- a/homeassistant/components/mysensors/translations/bg.json +++ b/homeassistant/components/mysensors/translations/bg.json @@ -1,12 +1,15 @@ { "config": { "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "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": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_ip": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441", "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", @@ -20,6 +23,7 @@ }, "gw_tcp": { "data": { + "device": "IP \u0430\u0434\u0440\u0435\u0441 \u043d\u0430 \u0448\u043b\u044e\u0437\u0430", "tcp_port": "\u043f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index a2f8ba77d78..8f5e0db9900 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Account is al geconfigureerd", "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", "invalid_access_token": "Ongeldig toegangstoken", "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", diff --git a/homeassistant/components/nexia/translations/bg.json b/homeassistant/components/nexia/translations/bg.json index 78264e2adbd..7aa8fb275ea 100644 --- a/homeassistant/components/nexia/translations/bg.json +++ b/homeassistant/components/nexia/translations/bg.json @@ -1,5 +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": { + "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/nuheat/translations/bg.json b/homeassistant/components/nuheat/translations/bg.json index 5d274ec2b73..03ace4428b1 100644 --- a/homeassistant/components/nuheat/translations/bg.json +++ b/homeassistant/components/nuheat/translations/bg.json @@ -1,6 +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", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" } } diff --git a/homeassistant/components/nut/translations/bg.json b/homeassistant/components/nut/translations/bg.json index 09f0ff26e5d..0ea2b4d6cb3 100644 --- a/homeassistant/components/nut/translations/bg.json +++ b/homeassistant/components/nut/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/overkiz/translations/sensor.nl.json b/homeassistant/components/overkiz/translations/sensor.nl.json index aef0b1e0394..8254719cf59 100644 --- a/homeassistant/components/overkiz/translations/sensor.nl.json +++ b/homeassistant/components/overkiz/translations/sensor.nl.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "Schoon", "dirty": "Vuil" + }, + "overkiz__three_way_handle_direction": { + "closed": "Gesloten", + "open": "Open", + "tilt": "Kantelen" } } } \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/bg.json b/homeassistant/components/panasonic_viera/translations/bg.json index 6433e60193d..2ceff63752c 100644 --- a/homeassistant/components/panasonic_viera/translations/bg.json +++ b/homeassistant/components/panasonic_viera/translations/bg.json @@ -19,7 +19,8 @@ "data": { "host": "IP \u0430\u0434\u0440\u0435\u0441", "name": "\u0418\u043c\u0435" - } + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0432\u0430\u0448\u0438\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440" } } } diff --git a/homeassistant/components/picnic/translations/bg.json b/homeassistant/components/picnic/translations/bg.json index ffb593eb287..32ea4287182 100644 --- a/homeassistant/components/picnic/translations/bg.json +++ b/homeassistant/components/picnic/translations/bg.json @@ -5,6 +5,13 @@ }, "error": { "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "country_code": "\u041a\u043e\u0434 \u043d\u0430 \u0434\u044a\u0440\u0436\u0430\u0432\u0430\u0442\u0430" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/bg.json b/homeassistant/components/rfxtrx/translations/bg.json index dec08bcfaa5..21a6c42913d 100644 --- a/homeassistant/components/rfxtrx/translations/bg.json +++ b/homeassistant/components/rfxtrx/translations/bg.json @@ -33,6 +33,7 @@ "step": { "prompt_options": { "data": { + "automatic_add": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e\u0442\u043e \u0434\u043e\u0431\u0430\u0432\u044f\u043d\u0435", "protocols": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0438" } }, diff --git a/homeassistant/components/risco/translations/bg.json b/homeassistant/components/risco/translations/bg.json index b9092f75d6c..805d72102aa 100644 --- a/homeassistant/components/risco/translations/bg.json +++ b/homeassistant/components/risco/translations/bg.json @@ -17,5 +17,14 @@ } } } + }, + "options": { + "step": { + "risco_to_ha": { + "data": { + "A": "\u0413\u0440\u0443\u043f\u0430 \u0410" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rituals_perfume_genie/translations/bg.json b/homeassistant/components/rituals_perfume_genie/translations/bg.json index cef3726d759..05ef3ed780e 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/bg.json +++ b/homeassistant/components/rituals_perfume_genie/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/roomba/translations/bg.json b/homeassistant/components/roomba/translations/bg.json index 3da613d9394..948c4afd258 100644 --- a/homeassistant/components/roomba/translations/bg.json +++ b/homeassistant/components/roomba/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \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" }, diff --git a/homeassistant/components/ruckus_unleashed/translations/bg.json b/homeassistant/components/ruckus_unleashed/translations/bg.json index ffb69776060..dcdcdcfc186 100644 --- a/homeassistant/components/ruckus_unleashed/translations/bg.json +++ b/homeassistant/components/ruckus_unleashed/translations/bg.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \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": { diff --git a/homeassistant/components/screenlogic/translations/bg.json b/homeassistant/components/screenlogic/translations/bg.json index 1c611d756fd..b8fccb94a47 100644 --- a/homeassistant/components/screenlogic/translations/bg.json +++ b/homeassistant/components/screenlogic/translations/bg.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "gateway_entry": { "data": { diff --git a/homeassistant/components/sense/translations/bg.json b/homeassistant/components/sense/translations/bg.json index d42d6dba5c1..2be0802eef9 100644 --- a/homeassistant/components/sense/translations/bg.json +++ b/homeassistant/components/sense/translations/bg.json @@ -3,6 +3,9 @@ "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\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "reauth_validate": { "data": { diff --git a/homeassistant/components/simplepush/translations/nl.json b/homeassistant/components/simplepush/translations/nl.json new file mode 100644 index 00000000000..900bac61bc5 --- /dev/null +++ b/homeassistant/components/simplepush/translations/nl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Naam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sma/translations/bg.json b/homeassistant/components/sma/translations/bg.json new file mode 100644 index 00000000000..cef3726d759 --- /dev/null +++ b/homeassistant/components/sma/translations/bg.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/bg.json b/homeassistant/components/smappee/translations/bg.json index 9173bdc0bc7..7b8f03499a6 100644 --- a/homeassistant/components/smappee/translations/bg.json +++ b/homeassistant/components/smappee/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "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." }, "flow_title": "{name}", diff --git a/homeassistant/components/smarttub/translations/bg.json b/homeassistant/components/smarttub/translations/bg.json index cef3726d759..ebfcda2158d 100644 --- a/homeassistant/components/smarttub/translations/bg.json +++ b/homeassistant/components/smarttub/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/somfy_mylink/translations/bg.json b/homeassistant/components/somfy_mylink/translations/bg.json index 4983c9a14b2..ca0ed419f99 100644 --- a/homeassistant/components/somfy_mylink/translations/bg.json +++ b/homeassistant/components/somfy_mylink/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/subaru/translations/bg.json b/homeassistant/components/subaru/translations/bg.json index a3c6d55e3e9..9031d8b47ce 100644 --- a/homeassistant/components/subaru/translations/bg.json +++ b/homeassistant/components/subaru/translations/bg.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "error": { + "bad_pin_format": "\u041f\u0418\u041d \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0435 4 \u0446\u0438\u0444\u0440\u0438", "incorrect_validation_code": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u0435\u043d \u043a\u043e\u0434 \u0437\u0430 \u0432\u0430\u043b\u0438\u0434\u0438\u0440\u0430\u043d\u0435" }, "step": { diff --git a/homeassistant/components/syncthru/translations/bg.json b/homeassistant/components/syncthru/translations/bg.json index bd8d6d4bf88..f957aba1fe6 100644 --- a/homeassistant/components/syncthru/translations/bg.json +++ b/homeassistant/components/syncthru/translations/bg.json @@ -10,7 +10,8 @@ "step": { "confirm": { "data": { - "name": "\u0418\u043c\u0435" + "name": "\u0418\u043c\u0435", + "url": "URL \u043d\u0430 \u0443\u0435\u0431 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430" } }, "user": { diff --git a/homeassistant/components/transmission/translations/nl.json b/homeassistant/components/transmission/translations/nl.json index fcc1e05e7ab..e80cda5ac8e 100644 --- a/homeassistant/components/transmission/translations/nl.json +++ b/homeassistant/components/transmission/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -9,6 +10,13 @@ "name_exists": "Naam bestaat al" }, "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "description": "Het wachtwoord voor {username} is onjuist.", + "title": "Integratie herauthenticeren" + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/twentemilieu/translations/bg.json b/homeassistant/components/twentemilieu/translations/bg.json index 6ddc25a367e..7aa70f6d7f5 100644 --- a/homeassistant/components/twentemilieu/translations/bg.json +++ b/homeassistant/components/twentemilieu/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_address": "\u0410\u0434\u0440\u0435\u0441\u044a\u0442 \u043d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d \u0432 \u0437\u043e\u043d\u0430 \u0437\u0430 \u043e\u0431\u0441\u043b\u0443\u0436\u0432\u0430\u043d\u0435 \u043d\u0430 Twente Milieu." }, "step": { diff --git a/homeassistant/components/vacuum/translations/no.json b/homeassistant/components/vacuum/translations/no.json index 3d722c0927c..c467018b249 100644 --- a/homeassistant/components/vacuum/translations/no.json +++ b/homeassistant/components/vacuum/translations/no.json @@ -15,7 +15,7 @@ }, "state": { "_": { - "cleaning": "Rengj\u00f8ring", + "cleaning": "Rengj\u00f8r", "docked": "Dokket", "error": "Feil", "idle": "Inaktiv", diff --git a/homeassistant/components/verisure/translations/bg.json b/homeassistant/components/verisure/translations/bg.json new file mode 100644 index 00000000000..0f10e122185 --- /dev/null +++ b/homeassistant/components/verisure/translations/bg.json @@ -0,0 +1,10 @@ +{ + "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": { + "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/vulcan/translations/bg.json b/homeassistant/components/vulcan/translations/bg.json index 187ad8cff4b..f99cd3cca14 100644 --- a/homeassistant/components/vulcan/translations/bg.json +++ b/homeassistant/components/vulcan/translations/bg.json @@ -4,7 +4,8 @@ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { - "cannot_connect": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435\u0442\u043e - \u043c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0432\u0430\u0448\u0430\u0442\u0430 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0432\u0440\u044a\u0437\u043a\u0430" + "cannot_connect": "\u0413\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435\u0442\u043e - \u043c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0432\u0430\u0448\u0430\u0442\u0430 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442 \u0432\u0440\u044a\u0437\u043a\u0430", + "unknown": "\u0412\u044a\u0437\u043d\u0438\u043a\u043d\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { "select_saved_credentials": { diff --git a/homeassistant/components/wolflink/translations/sensor.bg.json b/homeassistant/components/wolflink/translations/sensor.bg.json index 4a402cfe75b..8d0335dcc31 100644 --- a/homeassistant/components/wolflink/translations/sensor.bg.json +++ b/homeassistant/components/wolflink/translations/sensor.bg.json @@ -3,6 +3,9 @@ "wolflink__state": { "1_x_warmwasser": "1 x DHW", "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u043d", + "dhw_prior": "DHWPrior", + "gasdruck": "\u041d\u0430\u043b\u044f\u0433\u0430\u043d\u0435 \u043d\u0430 \u0433\u0430\u0437\u0430", + "kalibration": "\u041a\u0430\u043b\u0438\u0431\u0440\u0438\u0440\u0430\u043d\u0435", "test": "\u0422\u0435\u0441\u0442", "tpw": "TPW", "urlaubsmodus": "\u0412\u0430\u043a\u0430\u043d\u0446\u0438\u043e\u043d\u0435\u043d \u0440\u0435\u0436\u0438\u043c", From ee6866b8a31e4ab94b29a1afb2fca4406d39a79a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Jun 2022 19:48:58 -0500 Subject: [PATCH 1911/3516] Bump nexia to 2.0.1 (#74148) --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 1cb410ad1a8..cc5e6de8641 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==2.0.0"], + "requirements": ["nexia==2.0.1"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 04c48d20c70..104e01db741 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1086,7 +1086,7 @@ nettigo-air-monitor==1.3.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==2.0.0 +nexia==2.0.1 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8520fb665fe..c9675da97f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -751,7 +751,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.3.0 # homeassistant.components.nexia -nexia==2.0.0 +nexia==2.0.1 # homeassistant.components.discord nextcord==2.0.0a8 From 629c68221e1756635e202242fffe51d77dd2c61a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Jun 2022 19:54:27 -0500 Subject: [PATCH 1912/3516] Avoid retriggering HomeKit doorbells on forced updates (#74141) --- .../components/homekit/type_cameras.py | 14 +++-- homeassistant/components/homekit/util.py | 10 +++- tests/components/homekit/test_type_cameras.py | 55 ++++++++++++++++++- 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index 3a09e3a48ff..e612c8248be 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -14,7 +14,7 @@ from pyhap.const import CATEGORY_CAMERA from homeassistant.components import camera from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.const import STATE_ON -from homeassistant.core import callback +from homeassistant.core import Event, callback from homeassistant.helpers.event import ( async_track_state_change_event, async_track_time_interval, @@ -56,7 +56,7 @@ from .const import ( SERV_SPEAKER, SERV_STATELESS_PROGRAMMABLE_SWITCH, ) -from .util import pid_is_alive +from .util import pid_is_alive, state_changed_event_is_same_state _LOGGER = logging.getLogger(__name__) @@ -265,9 +265,10 @@ class Camera(HomeAccessory, PyhapCamera): await super().run() @callback - def _async_update_motion_state_event(self, event): + def _async_update_motion_state_event(self, event: Event) -> None: """Handle state change event listener callback.""" - self._async_update_motion_state(event.data.get("new_state")) + if not state_changed_event_is_same_state(event): + self._async_update_motion_state(event.data.get("new_state")) @callback def _async_update_motion_state(self, new_state): @@ -288,9 +289,10 @@ class Camera(HomeAccessory, PyhapCamera): ) @callback - def _async_update_doorbell_state_event(self, event): + def _async_update_doorbell_state_event(self, event: Event) -> None: """Handle state change event listener callback.""" - self._async_update_doorbell_state(event.data.get("new_state")) + if not state_changed_event_is_same_state(event): + self._async_update_doorbell_state(event.data.get("new_state")) @callback def _async_update_doorbell_state(self, new_state): diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 8b010f85fb6..34df1008e76 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -37,7 +37,7 @@ from homeassistant.const import ( CONF_TYPE, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant, State, callback, split_entity_id +from homeassistant.core import Event, HomeAssistant, State, callback, split_entity_id import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import STORAGE_DIR import homeassistant.util.temperature as temp_util @@ -572,3 +572,11 @@ def state_needs_accessory_mode(state: State) -> bool: and state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) & RemoteEntityFeature.ACTIVITY ) + + +def state_changed_event_is_same_state(event: Event) -> bool: + """Check if a state changed event is the same state.""" + event_data = event.data + old_state: State | None = event_data.get("old_state") + new_state: State | None = event_data.get("new_state") + return bool(new_state and old_state and new_state.state == old_state.state) diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index 9a8b284b97e..83afcedd839 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -642,11 +642,15 @@ async def test_camera_with_linked_motion_sensor(hass, run_driver, events): assert char assert char.value is True + broker = MagicMock() + char.broker = broker hass.states.async_set( motion_entity_id, STATE_OFF, {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION} ) await hass.async_block_till_done() + assert len(broker.mock_calls) == 2 + broker.reset_mock() assert char.value is False char.set_value(True) @@ -654,8 +658,28 @@ async def test_camera_with_linked_motion_sensor(hass, run_driver, events): motion_entity_id, STATE_ON, {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION} ) await hass.async_block_till_done() + assert len(broker.mock_calls) == 2 + broker.reset_mock() assert char.value is True + hass.states.async_set( + motion_entity_id, + STATE_ON, + {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION}, + force_update=True, + ) + await hass.async_block_till_done() + assert len(broker.mock_calls) == 0 + broker.reset_mock() + + hass.states.async_set( + motion_entity_id, + STATE_ON, + {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION, "other": "attr"}, + ) + await hass.async_block_till_done() + assert len(broker.mock_calls) == 0 + broker.reset_mock() # Ensure we do not throw when the linked # motion sensor is removed hass.states.async_remove(motion_entity_id) @@ -747,7 +771,8 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events): assert service2 char2 = service.get_characteristic(CHAR_PROGRAMMABLE_SWITCH_EVENT) assert char2 - + broker = MagicMock() + char2.broker = broker assert char2.value is None hass.states.async_set( @@ -758,9 +783,12 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events): await hass.async_block_till_done() assert char.value is None assert char2.value is None + assert len(broker.mock_calls) == 0 char.set_value(True) char2.set_value(True) + broker.reset_mock() + hass.states.async_set( doorbell_entity_id, STATE_ON, @@ -769,6 +797,31 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events): await hass.async_block_till_done() assert char.value is None assert char2.value is None + assert len(broker.mock_calls) == 2 + broker.reset_mock() + + hass.states.async_set( + doorbell_entity_id, + STATE_ON, + {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.OCCUPANCY}, + force_update=True, + ) + await hass.async_block_till_done() + assert char.value is None + assert char2.value is None + assert len(broker.mock_calls) == 0 + broker.reset_mock() + + hass.states.async_set( + doorbell_entity_id, + STATE_ON, + {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.OCCUPANCY, "other": "attr"}, + ) + await hass.async_block_till_done() + assert char.value is None + assert char2.value is None + assert len(broker.mock_calls) == 0 + broker.reset_mock() # Ensure we do not throw when the linked # doorbell sensor is removed From 309cf030b0efb5248c257670f88442b548b99711 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Jun 2022 19:57:17 -0500 Subject: [PATCH 1913/3516] Fix typo in enphase doc string (#74155) --- homeassistant/components/enphase_envoy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 61bf9b64bcf..0c6c893df64 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -102,7 +102,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry ) -> bool: - """Remove a enphase_envoy config entry from a device.""" + """Remove an enphase_envoy config entry from a device.""" dev_ids = {dev_id[1] for dev_id in device_entry.identifiers if dev_id[0] == DOMAIN} data: dict = hass.data[DOMAIN][config_entry.entry_id] coordinator: DataUpdateCoordinator = data[COORDINATOR] From 54320ff1340a15bdb4bc6dc9f7c234b6a0252c82 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Tue, 28 Jun 2022 23:00:26 -0400 Subject: [PATCH 1914/3516] UniFi Protect bugfixes (#74156) --- .../components/unifiprotect/binary_sensor.py | 6 ++-- .../components/unifiprotect/button.py | 4 ++- .../components/unifiprotect/camera.py | 8 +++-- homeassistant/components/unifiprotect/data.py | 34 +++++++++---------- .../components/unifiprotect/entity.py | 2 +- .../components/unifiprotect/light.py | 4 ++- homeassistant/components/unifiprotect/lock.py | 4 ++- .../components/unifiprotect/media_player.py | 4 ++- .../components/unifiprotect/number.py | 4 ++- .../components/unifiprotect/select.py | 4 ++- .../components/unifiprotect/sensor.py | 4 ++- .../components/unifiprotect/switch.py | 4 ++- tests/components/unifiprotect/test_init.py | 5 ++- 13 files changed, 54 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 598e0632fbb..d3bf71a4274 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -271,7 +271,7 @@ SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ufp_perm=PermRequired.NO_WRITE, ), ProtectBinaryEntityDescription( - key="motion", + key="motion_enabled", name="Motion Detection", icon="mdi:walk", entity_category=EntityCategory.DIAGNOSTIC, @@ -383,7 +383,9 @@ async def async_setup_entry( entities += _async_motion_entities(data, ufp_device=device) async_add_entities(entities) - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + entry.async_on_unload( + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + ) entities: list[ProtectDeviceEntity] = async_all_device_entities( data, diff --git a/homeassistant/components/unifiprotect/button.py b/homeassistant/components/unifiprotect/button.py index 901139109d3..9440e46b936 100644 --- a/homeassistant/components/unifiprotect/button.py +++ b/homeassistant/components/unifiprotect/button.py @@ -92,7 +92,9 @@ async def async_setup_entry( ) async_add_entities(entities) - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + entry.async_on_unload( + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + ) entities: list[ProtectDeviceEntity] = async_all_device_entities( data, diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index 336a5ae9187..76bfc72408d 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -120,8 +120,12 @@ async def async_setup_entry( entities = _async_camera_entities(data, ufp_device=device) async_add_entities(entities) - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_CHANNELS), _add_new_device) + entry.async_on_unload( + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + ) + entry.async_on_unload( + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_CHANNELS), _add_new_device) + ) entities = _async_camera_entities(data) async_add_entities(entities) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 30887f04235..9e0783a99b1 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -4,17 +4,17 @@ from __future__ import annotations from collections.abc import Callable, Generator, Iterable from datetime import timedelta import logging -from typing import Any +from typing import Any, Union from pyunifiprotect import ProtectApiClient from pyunifiprotect.data import ( + NVR, Bootstrap, Event, EventType, Liveview, ModelType, ProtectAdoptableDeviceModel, - ProtectModelWithId, WSSubscriptionMessage, ) from pyunifiprotect.exceptions import ClientError, NotAuthorized @@ -27,7 +27,6 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( CONF_DISABLE_RTSP, DEVICES_THAT_ADOPT, - DEVICES_WITH_ENTITIES, DISPATCH_ADOPT, DISPATCH_CHANNELS, DOMAIN, @@ -39,6 +38,7 @@ from .utils import ( ) _LOGGER = logging.getLogger(__name__) +ProtectDeviceType = Union[ProtectAdoptableDeviceModel, NVR] @callback @@ -68,7 +68,7 @@ class ProtectData: self._entry = entry self._hass = hass self._update_interval = update_interval - self._subscriptions: dict[str, list[Callable[[ProtectModelWithId], None]]] = {} + self._subscriptions: dict[str, list[Callable[[ProtectDeviceType], None]]] = {} self._pending_camera_ids: set[str] = set() self._unsub_interval: CALLBACK_TYPE | None = None self._unsub_websocket: CALLBACK_TYPE | None = None @@ -152,7 +152,7 @@ class ProtectData: return obj = message.new_obj - if obj.model in DEVICES_WITH_ENTITIES: + if isinstance(obj, (ProtectAdoptableDeviceModel, NVR)): self._async_signal_device_update(obj) if ( obj.model == ModelType.CAMERA @@ -211,41 +211,41 @@ class ProtectData: @callback def async_subscribe_device_id( - self, device_id: str, update_callback: Callable[[ProtectModelWithId], None] + self, mac: str, update_callback: Callable[[ProtectDeviceType], None] ) -> CALLBACK_TYPE: """Add an callback subscriber.""" if not self._subscriptions: self._unsub_interval = async_track_time_interval( self._hass, self.async_refresh, self._update_interval ) - self._subscriptions.setdefault(device_id, []).append(update_callback) + self._subscriptions.setdefault(mac, []).append(update_callback) def _unsubscribe() -> None: - self.async_unsubscribe_device_id(device_id, update_callback) + self.async_unsubscribe_device_id(mac, update_callback) return _unsubscribe @callback def async_unsubscribe_device_id( - self, device_id: str, update_callback: Callable[[ProtectModelWithId], None] + self, mac: str, update_callback: Callable[[ProtectDeviceType], None] ) -> None: """Remove a callback subscriber.""" - self._subscriptions[device_id].remove(update_callback) - if not self._subscriptions[device_id]: - del self._subscriptions[device_id] + self._subscriptions[mac].remove(update_callback) + if not self._subscriptions[mac]: + del self._subscriptions[mac] if not self._subscriptions and self._unsub_interval: self._unsub_interval() self._unsub_interval = None @callback - def _async_signal_device_update(self, device: ProtectModelWithId) -> None: + def _async_signal_device_update(self, device: ProtectDeviceType) -> None: """Call the callbacks for a device_id.""" - device_id = device.id - if not self._subscriptions.get(device_id): + + if not self._subscriptions.get(device.mac): return - _LOGGER.debug("Updating device: %s", device_id) - for update_callback in self._subscriptions[device_id]: + _LOGGER.debug("Updating device: %s (%s)", device.name, device.mac) + for update_callback in self._subscriptions[device.mac]: update_callback(device) diff --git a/homeassistant/components/unifiprotect/entity.py b/homeassistant/components/unifiprotect/entity.py index b7419d0a41e..e68e5cfb81d 100644 --- a/homeassistant/components/unifiprotect/entity.py +++ b/homeassistant/components/unifiprotect/entity.py @@ -215,7 +215,7 @@ class ProtectDeviceEntity(Entity): await super().async_added_to_hass() self.async_on_remove( self.data.async_subscribe_device_id( - self.device.id, self._async_updated_event + self.device.mac, self._async_updated_event ) ) diff --git a/homeassistant/components/unifiprotect/light.py b/homeassistant/components/unifiprotect/light.py index fdfe41bca3c..588b99b38d7 100644 --- a/homeassistant/components/unifiprotect/light.py +++ b/homeassistant/components/unifiprotect/light.py @@ -42,7 +42,9 @@ async def async_setup_entry( ): async_add_entities([ProtectLight(data, device)]) - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + entry.async_on_unload( + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + ) entities = [] for device in data.api.bootstrap.lights.values(): diff --git a/homeassistant/components/unifiprotect/lock.py b/homeassistant/components/unifiprotect/lock.py index 400d463050e..0a203308d1e 100644 --- a/homeassistant/components/unifiprotect/lock.py +++ b/homeassistant/components/unifiprotect/lock.py @@ -40,7 +40,9 @@ async def async_setup_entry( if isinstance(device, Doorlock): async_add_entities([ProtectLock(data, device)]) - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + entry.async_on_unload( + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + ) entities = [] for device in data.api.bootstrap.doorlocks.values(): diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index 41109c053f6..d4046e4b8b7 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -49,7 +49,9 @@ async def async_setup_entry( if isinstance(device, Camera) and device.feature_flags.has_speaker: async_add_entities([ProtectMediaPlayer(data, device)]) - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + entry.async_on_unload( + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + ) entities = [] for device in data.api.bootstrap.cameras.values(): diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index a017d2330b6..ed9faf4da40 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -206,7 +206,9 @@ async def async_setup_entry( ) async_add_entities(entities) - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + entry.async_on_unload( + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + ) entities: list[ProtectDeviceEntity] = async_all_device_entities( data, diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 5ea956ca603..e398e6692b0 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -336,7 +336,9 @@ async def async_setup_entry( ) async_add_entities(entities) - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + entry.async_on_unload( + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + ) entities: list[ProtectDeviceEntity] = async_all_device_entities( data, diff --git a/homeassistant/components/unifiprotect/sensor.py b/homeassistant/components/unifiprotect/sensor.py index a46e4c790b7..7a9f4652a2e 100644 --- a/homeassistant/components/unifiprotect/sensor.py +++ b/homeassistant/components/unifiprotect/sensor.py @@ -613,7 +613,9 @@ async def async_setup_entry( entities += _async_motion_entities(data, ufp_device=device) async_add_entities(entities) - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + entry.async_on_unload( + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + ) entities: list[ProtectDeviceEntity] = async_all_device_entities( data, diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 71812459b95..5bc4e1f17eb 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -318,7 +318,9 @@ async def async_setup_entry( ) async_add_entities(entities) - async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + entry.async_on_unload( + async_dispatcher_connect(hass, _ufpd(entry, DISPATCH_ADOPT), _add_new_device) + ) entities: list[ProtectDeviceEntity] = async_all_device_entities( data, diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index c0ad30ad115..f6f0645df18 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -109,11 +109,10 @@ async def test_reload(hass: HomeAssistant, ufp: MockUFPFixture): assert ufp.api.async_disconnect_ws.called -async def test_unload(hass: HomeAssistant, ufp: MockUFPFixture): +async def test_unload(hass: HomeAssistant, ufp: MockUFPFixture, light: Light): """Test unloading of unifiprotect entry.""" - await hass.config_entries.async_setup(ufp.entry.entry_id) - await hass.async_block_till_done() + await init_entry(hass, ufp, [light]) assert ufp.entry.state == ConfigEntryState.LOADED await hass.config_entries.async_unload(ufp.entry.entry_id) From 9b60b0c23f2b05377b9a44c9ca5e187407c0ab09 Mon Sep 17 00:00:00 2001 From: mletenay Date: Wed, 29 Jun 2022 06:09:24 +0200 Subject: [PATCH 1915/3516] Keep sum energy sensors always available (#69218) --- homeassistant/components/goodwe/sensor.py | 30 +++++++++++++++++------ 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/goodwe/sensor.py b/homeassistant/components/goodwe/sensor.py index 96a8ef49af3..6dcdc6e8cb1 100644 --- a/homeassistant/components/goodwe/sensor.py +++ b/homeassistant/components/goodwe/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Any +from typing import Any, cast from goodwe import Inverter, Sensor, SensorKind @@ -49,7 +49,7 @@ _MAIN_SENSORS = ( "e_bat_discharge_total", ) -_ICONS = { +_ICONS: dict[SensorKind, str] = { SensorKind.PV: "mdi:solar-power", SensorKind.AC: "mdi:power-plug-outline", SensorKind.UPS: "mdi:power-plug-off-outline", @@ -62,10 +62,13 @@ _ICONS = { class GoodweSensorEntityDescription(SensorEntityDescription): """Class describing Goodwe sensor entities.""" - value: Callable[[str, Any, Any], Any] = lambda sensor, prev, val: val + value: Callable[[Any, Any], Any] = lambda prev, val: val + available: Callable[ + [CoordinatorEntity], bool + ] = lambda entity: entity.coordinator.last_update_success -_DESCRIPTIONS = { +_DESCRIPTIONS: dict[str, GoodweSensorEntityDescription] = { "A": GoodweSensorEntityDescription( key="A", device_class=SensorDeviceClass.CURRENT, @@ -89,7 +92,8 @@ _DESCRIPTIONS = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - value=lambda sensor, prev, val: prev if "total" in sensor and not val else val, + value=lambda prev, val: prev if not val else val, + available=lambda entity: entity.coordinator.data is not None, ), "C": GoodweSensorEntityDescription( key="C", @@ -167,10 +171,22 @@ class InverterSensor(CoordinatorEntity, SensorEntity): @property def native_value(self): """Return the value reported by the sensor.""" - value = self.entity_description.value( - self._sensor.id_, + value = cast(GoodweSensorEntityDescription, self.entity_description).value( self._previous_value, self.coordinator.data.get(self._sensor.id_, self._previous_value), ) self._previous_value = value return value + + @property + def available(self) -> bool: + """Return if entity is available. + + We delegate the behavior to entity description lambda, since + some sensors (like energy produced today) should report themselves + as available even when the (non-battery) pv inverter is off-line during night + and most of the sensors are actually unavailable. + """ + return cast(GoodweSensorEntityDescription, self.entity_description).available( + self + ) From 305dff0dc1d053f5e954cbda9067d60aa8bc8a55 Mon Sep 17 00:00:00 2001 From: stegm Date: Wed, 29 Jun 2022 06:29:21 +0200 Subject: [PATCH 1916/3516] Add number platform for kostal_plenticore (#64927) --- .../components/kostal_plenticore/__init__.py | 2 +- .../components/kostal_plenticore/const.py | 74 ++++--- .../components/kostal_plenticore/helper.py | 26 ++- .../components/kostal_plenticore/number.py | 157 ++++++++++++++ .../components/kostal_plenticore/sensor.py | 45 +--- .../kostal_plenticore/test_number.py | 197 ++++++++++++++++++ 6 files changed, 430 insertions(+), 71 deletions(-) create mode 100644 homeassistant/components/kostal_plenticore/number.py create mode 100644 tests/components/kostal_plenticore/test_number.py diff --git a/homeassistant/components/kostal_plenticore/__init__.py b/homeassistant/components/kostal_plenticore/__init__.py index a42ad0a64ff..b431960caef 100644 --- a/homeassistant/components/kostal_plenticore/__init__.py +++ b/homeassistant/components/kostal_plenticore/__init__.py @@ -12,7 +12,7 @@ from .helper import Plenticore _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.SELECT, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.SELECT, Platform.SENSOR, Platform.SWITCH, Platform.NUMBER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py index ac2ecb44fd5..ba850ed58bd 100644 --- a/homeassistant/components/kostal_plenticore/const.py +++ b/homeassistant/components/kostal_plenticore/const.py @@ -1,6 +1,8 @@ """Constants for the Kostal Plenticore Solar Inverter integration.""" +from dataclasses import dataclass from typing import NamedTuple +from homeassistant.components.number import NumberEntityDescription from homeassistant.components.sensor import ( ATTR_STATE_CLASS, SensorDeviceClass, @@ -16,6 +18,7 @@ from homeassistant.const import ( PERCENTAGE, POWER_WATT, ) +from homeassistant.helpers.entity import EntityCategory DOMAIN = "kostal_plenticore" @@ -790,31 +793,54 @@ SENSOR_PROCESS_DATA = [ ), ] -# Defines all entities for settings. -# -# Each entry is defined with a tuple of these values: -# - module id (str) -# - process data id (str) -# - entity name suffix (str) -# - sensor properties (dict) -# - value formatter (str) -SENSOR_SETTINGS_DATA = [ - ( - "devices:local", - "Battery:MinHomeComsumption", - "Battery min Home Consumption", - { - ATTR_UNIT_OF_MEASUREMENT: POWER_WATT, - ATTR_DEVICE_CLASS: SensorDeviceClass.POWER, - }, - "format_round", + +@dataclass +class PlenticoreNumberEntityDescriptionMixin: + """Define an entity description mixin for number entities.""" + + module_id: str + data_id: str + fmt_from: str + fmt_to: str + + +@dataclass +class PlenticoreNumberEntityDescription( + NumberEntityDescription, PlenticoreNumberEntityDescriptionMixin +): + """Describes a Plenticore number entity.""" + + +NUMBER_SETTINGS_DATA = [ + PlenticoreNumberEntityDescription( + key="battery_min_soc", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:battery-negative", + name="Battery min SoC", + unit_of_measurement=PERCENTAGE, + max_value=100, + min_value=5, + step=5, + module_id="devices:local", + data_id="Battery:MinSoc", + fmt_from="format_round", + fmt_to="format_round_back", ), - ( - "devices:local", - "Battery:MinSoc", - "Battery min Soc", - {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, ATTR_ICON: "mdi:battery-negative"}, - "format_round", + PlenticoreNumberEntityDescription( + key="battery_min_home_consumption", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + name="Battery min Home Consumption", + unit_of_measurement=POWER_WATT, + max_value=38000, + min_value=50, + step=1, + module_id="devices:local", + data_id="Battery:MinHomeComsumption", + fmt_from="format_round", + fmt_to="format_round_back", ), ] diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index e047c0dafba..c87d96161a4 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -3,9 +3,10 @@ from __future__ import annotations import asyncio from collections import defaultdict -from collections.abc import Iterable +from collections.abc import Callable, Iterable from datetime import datetime, timedelta import logging +from typing import Any from aiohttp.client_exceptions import ClientError from kostal.plenticore import ( @@ -122,7 +123,7 @@ class DataUpdateCoordinatorMixin: """Base implementation for read and write data.""" async def async_read_data(self, module_id: str, data_id: str) -> list[str, bool]: - """Write settings back to Plenticore.""" + """Read data from Plenticore.""" if (client := self._plenticore.client) is None: return False @@ -138,6 +139,10 @@ class DataUpdateCoordinatorMixin: if (client := self._plenticore.client) is None: return False + _LOGGER.debug( + "Setting value for %s in module %s to %s", self.name, module_id, value + ) + try: await client.set_setting_values(module_id, value) except PlenticoreApiException: @@ -328,7 +333,7 @@ class PlenticoreDataFormatter: } @classmethod - def get_method(cls, name: str) -> callable: + def get_method(cls, name: str) -> Callable[[Any], Any]: """Return a callable formatter of the given name.""" return getattr(cls, name) @@ -340,6 +345,21 @@ class PlenticoreDataFormatter: except (TypeError, ValueError): return state + @staticmethod + def format_round_back(value: float) -> str: + """Return a rounded integer value from a float.""" + try: + if isinstance(value, float) and value.is_integer(): + int_value = int(value) + elif isinstance(value, int): + int_value = value + else: + int_value = round(value) + + return str(int_value) + except (TypeError, ValueError): + return "" + @staticmethod def format_float(state: str) -> int | str: """Return the given state value as float rounded to three decimal places.""" diff --git a/homeassistant/components/kostal_plenticore/number.py b/homeassistant/components/kostal_plenticore/number.py new file mode 100644 index 00000000000..33d431d6396 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/number.py @@ -0,0 +1,157 @@ +"""Platform for Kostal Plenticore numbers.""" +from __future__ import annotations + +from abc import ABC +from datetime import timedelta +from functools import partial +import logging + +from kostal.plenticore import SettingsData + +from homeassistant.components.number import NumberEntity, NumberMode +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 DOMAIN, NUMBER_SETTINGS_DATA, PlenticoreNumberEntityDescription +from .helper import PlenticoreDataFormatter, SettingDataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add Kostal Plenticore Number entities.""" + plenticore = hass.data[DOMAIN][entry.entry_id] + + entities = [] + + available_settings_data = await plenticore.client.get_settings() + settings_data_update_coordinator = SettingDataUpdateCoordinator( + hass, + _LOGGER, + "Settings Data", + timedelta(seconds=30), + plenticore, + ) + + for description in NUMBER_SETTINGS_DATA: + if ( + description.module_id not in available_settings_data + or description.data_id + not in ( + setting.id for setting in available_settings_data[description.module_id] + ) + ): + _LOGGER.debug( + "Skipping non existing setting data %s/%s", + description.module_id, + description.data_id, + ) + continue + + setting_data = next( + filter( + partial(lambda id, sd: id == sd.id, description.data_id), + available_settings_data[description.module_id], + ) + ) + + entities.append( + PlenticoreDataNumber( + settings_data_update_coordinator, + entry.entry_id, + entry.title, + plenticore.device_info, + description, + setting_data, + ) + ) + + async_add_entities(entities) + + +class PlenticoreDataNumber(CoordinatorEntity, NumberEntity, ABC): + """Representation of a Kostal Plenticore Number entity.""" + + entity_description: PlenticoreNumberEntityDescription + coordinator: SettingDataUpdateCoordinator + + def __init__( + self, + coordinator: SettingDataUpdateCoordinator, + entry_id: str, + platform_name: str, + device_info: DeviceInfo, + description: PlenticoreNumberEntityDescription, + setting_data: SettingsData, + ) -> None: + """Initialize the Plenticore Number entity.""" + super().__init__(coordinator) + + self.entity_description = description + self.entry_id = entry_id + + self._attr_device_info = device_info + self._attr_unique_id = f"{self.entry_id}_{self.module_id}_{self.data_id}" + self._attr_name = f"{platform_name} {description.name}" + self._attr_mode = NumberMode.BOX + + self._formatter = PlenticoreDataFormatter.get_method(description.fmt_from) + self._formatter_back = PlenticoreDataFormatter.get_method(description.fmt_to) + + # overwrite from retrieved setting data + if setting_data.min is not None: + self._attr_min_value = self._formatter(setting_data.min) + if setting_data.max is not None: + self._attr_max_value = self._formatter(setting_data.max) + + @property + def module_id(self) -> str: + """Return the plenticore module id of this entity.""" + return self.entity_description.module_id + + @property + def data_id(self) -> str: + """Return the plenticore data id for this entity.""" + return self.entity_description.data_id + + @property + def available(self) -> bool: + """Return if entity is available.""" + return ( + super().available + and self.coordinator.data is not None + and self.module_id in self.coordinator.data + and self.data_id in self.coordinator.data[self.module_id] + ) + + async def async_added_to_hass(self) -> None: + """Register this entity on the Update Coordinator.""" + await super().async_added_to_hass() + self.coordinator.start_fetch_data(self.module_id, self.data_id) + + async def async_will_remove_from_hass(self) -> None: + """Unregister this entity from the Update Coordinator.""" + self.coordinator.stop_fetch_data(self.module_id, self.data_id) + await super().async_will_remove_from_hass() + + @property + def value(self) -> float | None: + """Return the current value.""" + if self.available: + raw_value = self.coordinator.data[self.module_id][self.data_id] + return self._formatter(raw_value) + + return None + + async def async_set_value(self, value: float) -> None: + """Set a new value.""" + str_value = self._formatter_back(value) + await self.coordinator.async_write_data( + self.module_id, {self.data_id: str_value} + ) + await self.coordinator.async_refresh() diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index 0b6b01aca71..5f8fb47e85a 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -14,17 +14,8 @@ from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ( - ATTR_ENABLED_DEFAULT, - DOMAIN, - SENSOR_PROCESS_DATA, - SENSOR_SETTINGS_DATA, -) -from .helper import ( - PlenticoreDataFormatter, - ProcessDataUpdateCoordinator, - SettingDataUpdateCoordinator, -) +from .const import ATTR_ENABLED_DEFAULT, DOMAIN, SENSOR_PROCESS_DATA +from .helper import PlenticoreDataFormatter, ProcessDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -70,38 +61,6 @@ async def async_setup_entry( ) ) - available_settings_data = await plenticore.client.get_settings() - settings_data_update_coordinator = SettingDataUpdateCoordinator( - hass, - _LOGGER, - "Settings Data", - timedelta(seconds=300), - plenticore, - ) - for module_id, data_id, name, sensor_data, fmt in SENSOR_SETTINGS_DATA: - if module_id not in available_settings_data or data_id not in ( - setting.id for setting in available_settings_data[module_id] - ): - _LOGGER.debug( - "Skipping non existing setting data %s/%s", module_id, data_id - ) - continue - - entities.append( - PlenticoreDataSensor( - settings_data_update_coordinator, - entry.entry_id, - entry.title, - module_id, - data_id, - name, - sensor_data, - PlenticoreDataFormatter.get_method(fmt), - plenticore.device_info, - EntityCategory.DIAGNOSTIC, - ) - ) - async_add_entities(entities) diff --git a/tests/components/kostal_plenticore/test_number.py b/tests/components/kostal_plenticore/test_number.py new file mode 100644 index 00000000000..43d693642d9 --- /dev/null +++ b/tests/components/kostal_plenticore/test_number.py @@ -0,0 +1,197 @@ +"""Test Kostal Plenticore number.""" + +from unittest.mock import AsyncMock, MagicMock + +from kostal.plenticore import SettingsData +import pytest + +from homeassistant.components.kostal_plenticore.const import ( + PlenticoreNumberEntityDescription, +) +from homeassistant.components.kostal_plenticore.number import PlenticoreDataNumber +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import async_get + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_coordinator() -> MagicMock: + """Return a mocked coordinator for tests.""" + coordinator = MagicMock() + coordinator.async_write_data = AsyncMock() + coordinator.async_refresh = AsyncMock() + return coordinator + + +@pytest.fixture +def mock_number_description() -> PlenticoreNumberEntityDescription: + """Return a PlenticoreNumberEntityDescription for tests.""" + return PlenticoreNumberEntityDescription( + key="mock key", + module_id="moduleid", + data_id="dataid", + min_value=0, + max_value=1000, + fmt_from="format_round", + fmt_to="format_round_back", + ) + + +@pytest.fixture +def mock_setting_data() -> SettingsData: + """Return a default SettingsData for tests.""" + return SettingsData( + { + "default": None, + "min": None, + "access": None, + "max": None, + "unit": None, + "type": None, + "id": "data_id", + } + ) + + +async def test_setup_all_entries( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_plenticore: MagicMock +): + """Test if all available entries are setup up.""" + mock_plenticore.client.get_settings.return_value = { + "devices:local": [ + SettingsData({"id": "Battery:MinSoc", "min": None, "max": None}), + SettingsData( + {"id": "Battery:MinHomeComsumption", "min": None, "max": None} + ), + ] + } + + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + ent_reg = async_get(hass) + assert ent_reg.async_get("number.scb_battery_min_soc") is not None + assert ent_reg.async_get("number.scb_battery_min_home_consumption") is not None + + +async def test_setup_no_entries( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_plenticore: MagicMock +): + """Test that no entries are setup up.""" + mock_plenticore.client.get_settings.return_value = [] + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + ent_reg = async_get(hass) + assert ent_reg.async_get("number.scb_battery_min_soc") is None + assert ent_reg.async_get("number.scb_battery_min_home_consumption") is None + + +def test_number_returns_value_if_available( + mock_coordinator: MagicMock, + mock_number_description: PlenticoreNumberEntityDescription, + mock_setting_data: SettingsData, +): + """Test if value property on PlenticoreDataNumber returns an int if available.""" + + mock_coordinator.data = {"moduleid": {"dataid": "42"}} + + entity = PlenticoreDataNumber( + mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data + ) + + assert entity.value == 42 + assert type(entity.value) == int + + +def test_number_returns_none_if_unavailable( + mock_coordinator: MagicMock, + mock_number_description: PlenticoreNumberEntityDescription, + mock_setting_data: SettingsData, +): + """Test if value property on PlenticoreDataNumber returns none if unavailable.""" + + mock_coordinator.data = {} # makes entity not available + + entity = PlenticoreDataNumber( + mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data + ) + + assert entity.value is None + + +async def test_set_value( + mock_coordinator: MagicMock, + mock_number_description: PlenticoreNumberEntityDescription, + mock_setting_data: SettingsData, +): + """Test if set value calls coordinator with new value.""" + + entity = PlenticoreDataNumber( + mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data + ) + + await entity.async_set_value(42) + + mock_coordinator.async_write_data.assert_called_once_with( + "moduleid", {"dataid": "42"} + ) + mock_coordinator.async_refresh.assert_called_once() + + +async def test_minmax_overwrite( + mock_coordinator: MagicMock, + mock_number_description: PlenticoreNumberEntityDescription, +): + """Test if min/max value is overwritten from retrieved settings data.""" + + setting_data = SettingsData( + { + "min": "5", + "max": "100", + } + ) + + entity = PlenticoreDataNumber( + mock_coordinator, "42", "scb", None, mock_number_description, setting_data + ) + + assert entity.min_value == 5 + assert entity.max_value == 100 + + +async def test_added_to_hass( + mock_coordinator: MagicMock, + mock_number_description: PlenticoreNumberEntityDescription, + mock_setting_data: SettingsData, +): + """Test if coordinator starts fetching after added to hass.""" + + entity = PlenticoreDataNumber( + mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data + ) + + await entity.async_added_to_hass() + + mock_coordinator.start_fetch_data.assert_called_once_with("moduleid", "dataid") + + +async def test_remove_from_hass( + mock_coordinator: MagicMock, + mock_number_description: PlenticoreNumberEntityDescription, + mock_setting_data: SettingsData, +): + """Test if coordinator stops fetching after remove from hass.""" + + entity = PlenticoreDataNumber( + mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data + ) + + await entity.async_will_remove_from_hass() + + mock_coordinator.stop_fetch_data.assert_called_once_with("moduleid", "dataid") From 551929a175bab94e8703f06dd81e3c8c0f277899 Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Wed, 29 Jun 2022 07:37:23 +0300 Subject: [PATCH 1917/3516] More sensors for SMS integration (#70486) Co-authored-by: Paulus Schoutsen --- homeassistant/components/sms/__init__.py | 79 +++++++++++++++++- homeassistant/components/sms/const.py | 67 +++++++++++++++ homeassistant/components/sms/gateway.py | 4 + homeassistant/components/sms/notify.py | 4 +- homeassistant/components/sms/sensor.py | 101 ++++++++++------------- 5 files changed, 191 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index b1c2703409c..0b63a3d0366 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -1,6 +1,9 @@ """The sms component.""" +from datetime import timedelta import logging +import async_timeout +import gammu # pylint: disable=import-error import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -8,8 +11,18 @@ from homeassistant.const import CONF_DEVICE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_BAUD_SPEED, DEFAULT_BAUD_SPEED, DOMAIN, SMS_GATEWAY +from .const import ( + CONF_BAUD_SPEED, + DEFAULT_BAUD_SPEED, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + GATEWAY, + NETWORK_COORDINATOR, + SIGNAL_COORDINATOR, + SMS_GATEWAY, +) from .gateway import create_sms_gateway _LOGGER = logging.getLogger(__name__) @@ -30,6 +43,8 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +_LOGGER = logging.getLogger(__name__) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Configure Gammu state machine.""" @@ -61,7 +76,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: gateway = await create_sms_gateway(config, hass) if not gateway: return False - hass.data[DOMAIN][SMS_GATEWAY] = gateway + + signal_coordinator = SignalCoordinator(hass, gateway) + network_coordinator = NetworkCoordinator(hass, gateway) + + # Fetch initial data so we have data when entities subscribe + await signal_coordinator.async_config_entry_first_refresh() + await network_coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][SMS_GATEWAY] = { + SIGNAL_COORDINATOR: signal_coordinator, + NETWORK_COORDINATOR: network_coordinator, + GATEWAY: gateway, + } + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -71,7 +100,51 @@ 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: - gateway = hass.data[DOMAIN].pop(SMS_GATEWAY) + gateway = hass.data[DOMAIN].pop(SMS_GATEWAY)[GATEWAY] await gateway.terminate_async() return unload_ok + + +class SignalCoordinator(DataUpdateCoordinator): + """Signal strength coordinator.""" + + def __init__(self, hass, gateway): + """Initialize signal strength coordinator.""" + super().__init__( + hass, + _LOGGER, + name="Device signal state", + update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + ) + self._gateway = gateway + + async def _async_update_data(self): + """Fetch device signal quality.""" + try: + async with async_timeout.timeout(10): + return await self._gateway.get_signal_quality_async() + except gammu.GSMError as exc: + raise UpdateFailed(f"Error communicating with device: {exc}") from exc + + +class NetworkCoordinator(DataUpdateCoordinator): + """Network info coordinator.""" + + def __init__(self, hass, gateway): + """Initialize network info coordinator.""" + super().__init__( + hass, + _LOGGER, + name="Device network state", + update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + ) + self._gateway = gateway + + async def _async_update_data(self): + """Fetch device network info.""" + try: + async with async_timeout.timeout(10): + return await self._gateway.get_network_info_async() + except gammu.GSMError as exc: + raise UpdateFailed(f"Error communicating with device: {exc}") from exc diff --git a/homeassistant/components/sms/const.py b/homeassistant/components/sms/const.py index 7c40a04073c..858e53d9808 100644 --- a/homeassistant/components/sms/const.py +++ b/homeassistant/components/sms/const.py @@ -1,8 +1,17 @@ """Constants for sms Component.""" +from typing import Final + +from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription +from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS +from homeassistant.helpers.entity import EntityCategory DOMAIN = "sms" SMS_GATEWAY = "SMS_GATEWAY" SMS_STATE_UNREAD = "UnRead" +SIGNAL_COORDINATOR = "signal_coordinator" +NETWORK_COORDINATOR = "network_coordinator" +GATEWAY = "gateway" +DEFAULT_SCAN_INTERVAL = 30 CONF_BAUD_SPEED = "baud_speed" DEFAULT_BAUD_SPEED = "0" DEFAULT_BAUD_SPEEDS = [ @@ -27,3 +36,61 @@ DEFAULT_BAUD_SPEEDS = [ {"value": "76800", "label": "76800"}, {"value": "115200", "label": "115200"}, ] + +SIGNAL_SENSORS: Final[dict[str, SensorEntityDescription]] = { + "SignalStrength": SensorEntityDescription( + key="SignalStrength", + name="Signal Strength", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + entity_registry_enabled_default=False, + ), + "SignalPercent": SensorEntityDescription( + key="SignalPercent", + icon="mdi:signal-cellular-3", + name="Signal Percent", + native_unit_of_measurement=PERCENTAGE, + entity_registry_enabled_default=True, + ), + "BitErrorRate": SensorEntityDescription( + key="BitErrorRate", + name="Bit Error Rate", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + entity_registry_enabled_default=False, + ), +} + +NETWORK_SENSORS: Final[dict[str, SensorEntityDescription]] = { + "NetworkName": SensorEntityDescription( + key="NetworkName", + name="Network Name", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "State": SensorEntityDescription( + key="State", + name="Network Status", + entity_registry_enabled_default=True, + ), + "NetworkCode": SensorEntityDescription( + key="NetworkCode", + name="GSM network code", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "CID": SensorEntityDescription( + key="CID", + name="Cell ID", + icon="mdi:radio-tower", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "LAC": SensorEntityDescription( + key="LAC", + name="Local Area Code", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), +} diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py index 09992600943..c469e688737 100644 --- a/homeassistant/components/sms/gateway.py +++ b/homeassistant/components/sms/gateway.py @@ -154,6 +154,10 @@ class Gateway: """Get the current signal level of the modem.""" return await self._worker.get_signal_quality_async() + async def get_network_info_async(self): + """Get the current network info of the modem.""" + return await self._worker.get_network_info_async() + async def terminate_async(self): """Terminate modem connection.""" return await self._worker.terminate_async() diff --git a/homeassistant/components/sms/notify.py b/homeassistant/components/sms/notify.py index 433144773f7..d076f3625ba 100644 --- a/homeassistant/components/sms/notify.py +++ b/homeassistant/components/sms/notify.py @@ -8,7 +8,7 @@ from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationSer from homeassistant.const import CONF_NAME, CONF_RECIPIENT, CONF_TARGET import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, SMS_GATEWAY +from .const import DOMAIN, GATEWAY, SMS_GATEWAY _LOGGER = logging.getLogger(__name__) @@ -24,7 +24,7 @@ def get_service(hass, config, discovery_info=None): _LOGGER.error("SMS gateway not found, cannot initialize service") return - gateway = hass.data[DOMAIN][SMS_GATEWAY] + gateway = hass.data[DOMAIN][SMS_GATEWAY][GATEWAY] if discovery_info is None: number = config[CONF_RECIPIENT] diff --git a/homeassistant/components/sms/sensor.py b/homeassistant/components/sms/sensor.py index dcc85c4f8c6..de20a5b5d0f 100644 --- a/homeassistant/components/sms/sensor.py +++ b/homeassistant/components/sms/sensor.py @@ -1,22 +1,20 @@ """Support for SMS dongle sensor.""" -import logging - -import gammu # pylint: disable=import-error - -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, -) +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import SIGNAL_STRENGTH_DECIBELS 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 DOMAIN, SMS_GATEWAY - -_LOGGER = logging.getLogger(__name__) +from .const import ( + DOMAIN, + GATEWAY, + NETWORK_COORDINATOR, + NETWORK_SENSORS, + SIGNAL_COORDINATOR, + SIGNAL_SENSORS, + SMS_GATEWAY, +) async def async_setup_entry( @@ -24,61 +22,46 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the GSM Signal Sensor sensor.""" - gateway = hass.data[DOMAIN][SMS_GATEWAY] - imei = await gateway.get_imei_async() - async_add_entities( - [ - GSMSignalSensor( - hass, - gateway, - imei, - SensorEntityDescription( - key="signal", - name=f"gsm_signal_imei_{imei}", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, - entity_registry_enabled_default=False, - ), + """Set up all device sensors.""" + sms_data = hass.data[DOMAIN][SMS_GATEWAY] + signal_coordinator = sms_data[SIGNAL_COORDINATOR] + network_coordinator = sms_data[NETWORK_COORDINATOR] + gateway = sms_data[GATEWAY] + unique_id = str(await gateway.get_imei_async()) + entities = [] + for description in SIGNAL_SENSORS.values(): + entities.append( + DeviceSensor( + signal_coordinator, + description, + unique_id, ) - ], - True, - ) + ) + for description in NETWORK_SENSORS.values(): + entities.append( + DeviceSensor( + network_coordinator, + description, + unique_id, + ) + ) + async_add_entities(entities, True) -class GSMSignalSensor(SensorEntity): - """Implementation of a GSM Signal sensor.""" +class DeviceSensor(CoordinatorEntity, SensorEntity): + """Implementation of a device sensor.""" - def __init__(self, hass, gateway, imei, description): - """Initialize the GSM Signal sensor.""" + def __init__(self, coordinator, description, unique_id): + """Initialize the device sensor.""" + super().__init__(coordinator) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, str(imei))}, + identifiers={(DOMAIN, unique_id)}, name="SMS Gateway", ) - self._attr_unique_id = str(imei) - self._hass = hass - self._gateway = gateway - self._state = None + self._attr_unique_id = f"{unique_id}_{description.key}" self.entity_description = description - @property - def available(self): - """Return if the sensor data are available.""" - return self._state is not None - @property def native_value(self): """Return the state of the device.""" - return self._state["SignalStrength"] - - async def async_update(self): - """Get the latest data from the modem.""" - try: - self._state = await self._gateway.get_signal_quality_async() - except gammu.GSMError as exc: - _LOGGER.error("Failed to read signal quality: %s", exc) - - @property - def extra_state_attributes(self): - """Return the sensor attributes.""" - return self._state + return self.coordinator.data.get(self.entity_description.key) From 9f15234b92075164c720df37d01642dd802b3d4e Mon Sep 17 00:00:00 2001 From: Nick Dawson Date: Wed, 29 Jun 2022 14:48:30 +1000 Subject: [PATCH 1918/3516] Add Anywair in IntesisHome (#71686) --- homeassistant/components/intesishome/climate.py | 3 ++- homeassistant/components/intesishome/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index 0f7fe6b33ca..050bed8c721 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -40,13 +40,14 @@ _LOGGER = logging.getLogger(__name__) IH_DEVICE_INTESISHOME = "IntesisHome" IH_DEVICE_AIRCONWITHME = "airconwithme" +IH_DEVICE_ANYWAIR = "anywair" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_DEVICE, default=IH_DEVICE_INTESISHOME): vol.In( - [IH_DEVICE_AIRCONWITHME, IH_DEVICE_INTESISHOME] + [IH_DEVICE_AIRCONWITHME, IH_DEVICE_ANYWAIR, IH_DEVICE_INTESISHOME] ), } ) diff --git a/homeassistant/components/intesishome/manifest.json b/homeassistant/components/intesishome/manifest.json index 6b84f735c12..d4ec7f6d744 100644 --- a/homeassistant/components/intesishome/manifest.json +++ b/homeassistant/components/intesishome/manifest.json @@ -3,7 +3,7 @@ "name": "IntesisHome", "documentation": "https://www.home-assistant.io/integrations/intesishome", "codeowners": ["@jnimmo"], - "requirements": ["pyintesishome==1.7.6"], + "requirements": ["pyintesishome==1.8.0"], "iot_class": "cloud_push", "loggers": ["pyintesishome"] } diff --git a/requirements_all.txt b/requirements_all.txt index 104e01db741..8adf795c977 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1562,7 +1562,7 @@ pyicloud==1.0.0 pyinsteon==1.1.1 # homeassistant.components.intesishome -pyintesishome==1.7.6 +pyintesishome==1.8.0 # homeassistant.components.ipma pyipma==2.0.5 From 22b8afe96673ca88029ae2f8ac4727ece704fae5 Mon Sep 17 00:00:00 2001 From: Edward Date: Wed, 29 Jun 2022 14:52:17 +1000 Subject: [PATCH 1919/3516] Propagate destination of watched folder moves (#70252) --- .../components/folder_watcher/__init__.py | 27 ++++++++---- tests/components/folder_watcher/test_init.py | 41 ++++++++++++++++++- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/folder_watcher/__init__.py b/homeassistant/components/folder_watcher/__init__.py index e718c3d3bf2..cd979d51457 100644 --- a/homeassistant/components/folder_watcher/__init__.py +++ b/homeassistant/components/folder_watcher/__init__.py @@ -63,19 +63,30 @@ def create_event_handler(patterns, hass): super().__init__(patterns) self.hass = hass - def process(self, event): + def process(self, event, moved=False): """On Watcher event, fire HA event.""" _LOGGER.debug("process(%s)", event) if not event.is_directory: folder, file_name = os.path.split(event.src_path) + fireable = { + "event_type": event.event_type, + "path": event.src_path, + "file": file_name, + "folder": folder, + } + + if moved: + dest_folder, dest_file_name = os.path.split(event.dest_path) + fireable.update( + { + "dest_path": event.dest_path, + "dest_file": dest_file_name, + "dest_folder": dest_folder, + } + ) self.hass.bus.fire( DOMAIN, - { - "event_type": event.event_type, - "path": event.src_path, - "file": file_name, - "folder": folder, - }, + fireable, ) def on_modified(self, event): @@ -84,7 +95,7 @@ def create_event_handler(patterns, hass): def on_moved(self, event): """File moved.""" - self.process(event) + self.process(event, moved=True) def on_created(self, event): """File created.""" diff --git a/tests/components/folder_watcher/test_init.py b/tests/components/folder_watcher/test_init.py index b0a522cb7fc..babac930c2d 100644 --- a/tests/components/folder_watcher/test_init.py +++ b/tests/components/folder_watcher/test_init.py @@ -1,5 +1,6 @@ """The tests for the folder_watcher component.""" import os +from types import SimpleNamespace from unittest.mock import Mock, patch from homeassistant.components import folder_watcher @@ -43,7 +44,9 @@ def test_event(): hass = Mock() handler = folder_watcher.create_event_handler(["*"], hass) handler.on_created( - Mock(is_directory=False, src_path="/hello/world.txt", event_type="created") + SimpleNamespace( + is_directory=False, src_path="/hello/world.txt", event_type="created" + ) ) assert hass.bus.fire.called assert hass.bus.fire.mock_calls[0][1][0] == folder_watcher.DOMAIN @@ -53,3 +56,39 @@ def test_event(): "file": "world.txt", "folder": "/hello", } + + +def test_move_event(): + """Check that Home Assistant events are fired correctly on watchdog event.""" + + class MockPatternMatchingEventHandler: + """Mock base class for the pattern matcher event handler.""" + + def __init__(self, patterns): + pass + + with patch( + "homeassistant.components.folder_watcher.PatternMatchingEventHandler", + MockPatternMatchingEventHandler, + ): + hass = Mock() + handler = folder_watcher.create_event_handler(["*"], hass) + handler.on_moved( + SimpleNamespace( + is_directory=False, + src_path="/hello/world.txt", + dest_path="/hello/earth.txt", + event_type="moved", + ) + ) + assert hass.bus.fire.called + assert hass.bus.fire.mock_calls[0][1][0] == folder_watcher.DOMAIN + assert hass.bus.fire.mock_calls[0][1][1] == { + "event_type": "moved", + "path": "/hello/world.txt", + "dest_path": "/hello/earth.txt", + "file": "world.txt", + "dest_file": "earth.txt", + "folder": "/hello", + "dest_folder": "/hello", + } From b7b8feda0ffb7487954545c96c50e7f64e2195bc Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Wed, 29 Jun 2022 06:59:19 +0200 Subject: [PATCH 1920/3516] Add tests for LCN sensor and binary_sensor platforms (#67263) --- .coveragerc | 2 - tests/components/lcn/fixtures/config.json | 41 +++++ .../lcn/fixtures/config_entry_pchk.json | 67 +++++++++ tests/components/lcn/test_binary_sensor.py | 140 ++++++++++++++++++ tests/components/lcn/test_cover.py | 115 +++++++------- tests/components/lcn/test_events.py | 37 +++-- tests/components/lcn/test_light.py | 74 ++++----- tests/components/lcn/test_sensor.py | 131 ++++++++++++++++ tests/components/lcn/test_switch.py | 63 ++++---- 9 files changed, 532 insertions(+), 138 deletions(-) create mode 100644 tests/components/lcn/test_binary_sensor.py create mode 100644 tests/components/lcn/test_sensor.py diff --git a/.coveragerc b/.coveragerc index 8253ed56ccd..981e2d06680 100644 --- a/.coveragerc +++ b/.coveragerc @@ -633,11 +633,9 @@ omit = homeassistant/components/launch_library/const.py homeassistant/components/launch_library/diagnostics.py homeassistant/components/launch_library/sensor.py - homeassistant/components/lcn/binary_sensor.py homeassistant/components/lcn/climate.py homeassistant/components/lcn/helpers.py homeassistant/components/lcn/scene.py - homeassistant/components/lcn/sensor.py homeassistant/components/lcn/services.py homeassistant/components/lg_netcast/media_player.py homeassistant/components/lg_soundbar/media_player.py diff --git a/tests/components/lcn/fixtures/config.json b/tests/components/lcn/fixtures/config.json index cc615b6083b..13b3dd5feed 100644 --- a/tests/components/lcn/fixtures/config.json +++ b/tests/components/lcn/fixtures/config.json @@ -90,6 +90,47 @@ "address": "s0.m7", "motor": "motor1" } + ], + "binary_sensors": [ + { + "name": "Sensor_LockRegulator1", + "address": "s0.m7", + "source": "r1varsetpoint" + }, + { + "name": "Binary_Sensor1", + "address": "s0.m7", + "source": "binsensor1" + }, + { + "name": "Sensor_KeyLock", + "address": "s0.m7", + "source": "a5" + } + ], + "sensors": [ + { + "name": "Sensor_Var1", + "address": "s0.m7", + "source": "var1", + "unit_of_measurement": "°C" + }, + { + "name": "Sensor_Setpoint1", + "address": "s0.m7", + "source": "r1varsetpoint", + "unit_of_measurement": "°C" + }, + { + "name": "Sensor_Led6", + "address": "s0.m7", + "source": "led6" + }, + { + "name": "Sensor_LogicOp1", + "address": "s0.m7", + "source": "logicop1" + } ] } } diff --git a/tests/components/lcn/fixtures/config_entry_pchk.json b/tests/components/lcn/fixtures/config_entry_pchk.json index 620bbb673f5..31b51adfce7 100644 --- a/tests/components/lcn/fixtures/config_entry_pchk.json +++ b/tests/components/lcn/fixtures/config_entry_pchk.json @@ -120,6 +120,73 @@ "motor": "MOTOR1", "reverse_time": "RT1200" } + }, + { + "address": [0, 7, false], + "name": "Sensor_LockRegulator1", + "resource": "r1varsetpoint", + "domain": "binary_sensor", + "domain_data": { + "source": "R1VARSETPOINT" + } + }, + { + "address": [0, 7, false], + "name": "Binary_Sensor1", + "resource": "binsensor1", + "domain": "binary_sensor", + "domain_data": { + "source": "BINSENSOR1" + } + }, + { + "address": [0, 7, false], + "name": "Sensor_KeyLock", + "resource": "a5", + "domain": "binary_sensor", + "domain_data": { + "source": "A5" + } + }, + { + "address": [0, 7, false], + "name": "Sensor_Var1", + "resource": "var1", + "domain": "sensor", + "domain_data": { + "source": "VAR1", + "unit_of_measurement": "°C" + } + }, + { + "address": [0, 7, false], + "name": "Sensor_Setpoint1", + "resource": "r1varsetpoint", + "domain": "sensor", + "domain_data": { + "source": "R1VARSETPOINT", + "unit_of_measurement": "°C" + } + }, + { + "address": [0, 7, false], + "name": "Sensor_Led6", + "resource": "led6", + "domain": "sensor", + "domain_data": { + "source": "LED6", + "unit_of_measurement": "NATIVE" + } + }, + { + "address": [0, 7, false], + "name": "Sensor_LogicOp1", + "resource": "logicop1", + "domain": "sensor", + "domain_data": { + "source": "LOGICOP1", + "unit_of_measurement": "NATIVE" + } } ] } diff --git a/tests/components/lcn/test_binary_sensor.py b/tests/components/lcn/test_binary_sensor.py new file mode 100644 index 00000000000..3f9adb34295 --- /dev/null +++ b/tests/components/lcn/test_binary_sensor.py @@ -0,0 +1,140 @@ +"""Test for the LCN binary sensor platform.""" +from pypck.inputs import ModStatusBinSensors, ModStatusKeyLocks, ModStatusVar +from pypck.lcn_addr import LcnAddr +from pypck.lcn_defs import Var, VarValue + +from homeassistant.components.lcn.helpers import get_device_connection +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.helpers import entity_registry as er + +BINARY_SENSOR_LOCKREGULATOR1 = "binary_sensor.sensor_lockregulator1" +BINARY_SENSOR_SENSOR1 = "binary_sensor.binary_sensor1" +BINARY_SENSOR_KEYLOCK = "binary_sensor.sensor_keylock" + + +async def test_setup_lcn_binary_sensor(hass, lcn_connection): + """Test the setup of binary sensor.""" + for entity_id in ( + BINARY_SENSOR_LOCKREGULATOR1, + BINARY_SENSOR_SENSOR1, + BINARY_SENSOR_KEYLOCK, + ): + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_UNKNOWN + + +async def test_entity_state(hass, lcn_connection): + """Test state of entity.""" + state = hass.states.get(BINARY_SENSOR_LOCKREGULATOR1) + assert state + + state = hass.states.get(BINARY_SENSOR_SENSOR1) + assert state + + state = hass.states.get(BINARY_SENSOR_KEYLOCK) + assert state + + +async def test_entity_attributes(hass, entry, lcn_connection): + """Test the attributes of an entity.""" + entity_registry = er.async_get(hass) + + entity_setpoint1 = entity_registry.async_get(BINARY_SENSOR_LOCKREGULATOR1) + assert entity_setpoint1 + assert entity_setpoint1.unique_id == f"{entry.entry_id}-m000007-r1varsetpoint" + assert entity_setpoint1.original_name == "Sensor_LockRegulator1" + + entity_binsensor1 = entity_registry.async_get(BINARY_SENSOR_SENSOR1) + assert entity_binsensor1 + assert entity_binsensor1.unique_id == f"{entry.entry_id}-m000007-binsensor1" + assert entity_binsensor1.original_name == "Binary_Sensor1" + + entity_keylock = entity_registry.async_get(BINARY_SENSOR_KEYLOCK) + assert entity_keylock + assert entity_keylock.unique_id == f"{entry.entry_id}-m000007-a5" + assert entity_keylock.original_name == "Sensor_KeyLock" + + +async def test_pushed_lock_setpoint_status_change(hass, entry, lcn_connection): + """Test the lock setpoint sensor changes its state on status received.""" + device_connection = get_device_connection(hass, (0, 7, False), entry) + address = LcnAddr(0, 7, False) + + # push status lock setpoint + inp = ModStatusVar(address, Var.R1VARSETPOINT, VarValue(0x8000)) + await device_connection.async_process_input(inp) + await hass.async_block_till_done() + + state = hass.states.get(BINARY_SENSOR_LOCKREGULATOR1) + assert state is not None + assert state.state == STATE_ON + + # push status unlock setpoint + inp = ModStatusVar(address, Var.R1VARSETPOINT, VarValue(0x7FFF)) + await device_connection.async_process_input(inp) + await hass.async_block_till_done() + + state = hass.states.get(BINARY_SENSOR_LOCKREGULATOR1) + assert state is not None + assert state.state == STATE_OFF + + +async def test_pushed_binsensor_status_change(hass, entry, lcn_connection): + """Test the binary port sensor changes its state on status received.""" + device_connection = get_device_connection(hass, (0, 7, False), entry) + address = LcnAddr(0, 7, False) + states = [False] * 8 + + # push status binary port "off" + inp = ModStatusBinSensors(address, states) + await device_connection.async_process_input(inp) + await hass.async_block_till_done() + + state = hass.states.get(BINARY_SENSOR_SENSOR1) + assert state is not None + assert state.state == STATE_OFF + + # push status binary port "on" + states[0] = True + inp = ModStatusBinSensors(address, states) + await device_connection.async_process_input(inp) + await hass.async_block_till_done() + + state = hass.states.get(BINARY_SENSOR_SENSOR1) + assert state is not None + assert state.state == STATE_ON + + +async def test_pushed_keylock_status_change(hass, entry, lcn_connection): + """Test the keylock sensor changes its state on status received.""" + device_connection = get_device_connection(hass, (0, 7, False), entry) + address = LcnAddr(0, 7, False) + states = [[False] * 8 for i in range(4)] + + # push status keylock "off" + inp = ModStatusKeyLocks(address, states) + await device_connection.async_process_input(inp) + await hass.async_block_till_done() + + state = hass.states.get(BINARY_SENSOR_KEYLOCK) + assert state is not None + assert state.state == STATE_OFF + + # push status keylock "on" + states[0][4] = True + inp = ModStatusKeyLocks(address, states) + await device_connection.async_process_input(inp) + await hass.async_block_till_done() + + state = hass.states.get(BINARY_SENSOR_KEYLOCK) + assert state is not None + assert state.state == STATE_ON + + +async def test_unload_config_entry(hass, entry, lcn_connection): + """Test the binary sensor is removed when the config entry is unloaded.""" + await hass.config_entries.async_unload(entry.entry_id) + assert hass.states.get(BINARY_SENSOR_LOCKREGULATOR1).state == STATE_UNAVAILABLE + assert hass.states.get(BINARY_SENSOR_SENSOR1).state == STATE_UNAVAILABLE + assert hass.states.get(BINARY_SENSOR_KEYLOCK).state == STATE_UNAVAILABLE diff --git a/tests/components/lcn/test_cover.py b/tests/components/lcn/test_cover.py index 8c6b814e525..b7eab5f2ecc 100644 --- a/tests/components/lcn/test_cover.py +++ b/tests/components/lcn/test_cover.py @@ -22,12 +22,15 @@ from homeassistant.helpers import entity_registry as er from .conftest import MockModuleConnection +COVER_OUTPUTS = "cover.cover_outputs" +COVER_RELAYS = "cover.cover_relays" + async def test_setup_lcn_cover(hass, entry, lcn_connection): """Test the setup of cover.""" for entity_id in ( - "cover.cover_outputs", - "cover.cover_relays", + COVER_OUTPUTS, + COVER_RELAYS, ): state = hass.states.get(entity_id) assert state is not None @@ -38,13 +41,13 @@ async def test_entity_attributes(hass, entry, lcn_connection): """Test the attributes of an entity.""" entity_registry = er.async_get(hass) - entity_outputs = entity_registry.async_get("cover.cover_outputs") + entity_outputs = entity_registry.async_get(COVER_OUTPUTS) assert entity_outputs assert entity_outputs.unique_id == f"{entry.entry_id}-m000007-outputs" assert entity_outputs.original_name == "Cover_Outputs" - entity_relays = entity_registry.async_get("cover.cover_relays") + entity_relays = entity_registry.async_get(COVER_RELAYS) assert entity_relays assert entity_relays.unique_id == f"{entry.entry_id}-m000007-motor1" @@ -54,7 +57,7 @@ async def test_entity_attributes(hass, entry, lcn_connection): @patch.object(MockModuleConnection, "control_motors_outputs") async def test_outputs_open(control_motors_outputs, hass, lcn_connection): """Test the outputs cover opens.""" - state = hass.states.get("cover.cover_outputs") + state = hass.states.get(COVER_OUTPUTS) state.state = STATE_CLOSED # command failed @@ -63,7 +66,7 @@ async def test_outputs_open(control_motors_outputs, hass, lcn_connection): await hass.services.async_call( DOMAIN_COVER, SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.cover_outputs"}, + {ATTR_ENTITY_ID: COVER_OUTPUTS}, blocking=True, ) await hass.async_block_till_done() @@ -71,7 +74,7 @@ async def test_outputs_open(control_motors_outputs, hass, lcn_connection): MotorStateModifier.UP, MotorReverseTime.RT1200 ) - state = hass.states.get("cover.cover_outputs") + state = hass.states.get(COVER_OUTPUTS) assert state is not None assert state.state != STATE_OPENING @@ -82,7 +85,7 @@ async def test_outputs_open(control_motors_outputs, hass, lcn_connection): await hass.services.async_call( DOMAIN_COVER, SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.cover_outputs"}, + {ATTR_ENTITY_ID: COVER_OUTPUTS}, blocking=True, ) await hass.async_block_till_done() @@ -90,7 +93,7 @@ async def test_outputs_open(control_motors_outputs, hass, lcn_connection): MotorStateModifier.UP, MotorReverseTime.RT1200 ) - state = hass.states.get("cover.cover_outputs") + state = hass.states.get(COVER_OUTPUTS) assert state is not None assert state.state == STATE_OPENING @@ -98,7 +101,7 @@ async def test_outputs_open(control_motors_outputs, hass, lcn_connection): @patch.object(MockModuleConnection, "control_motors_outputs") async def test_outputs_close(control_motors_outputs, hass, lcn_connection): """Test the outputs cover closes.""" - state = hass.states.get("cover.cover_outputs") + state = hass.states.get(COVER_OUTPUTS) state.state = STATE_OPEN # command failed @@ -107,7 +110,7 @@ async def test_outputs_close(control_motors_outputs, hass, lcn_connection): await hass.services.async_call( DOMAIN_COVER, SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.cover_outputs"}, + {ATTR_ENTITY_ID: COVER_OUTPUTS}, blocking=True, ) await hass.async_block_till_done() @@ -115,7 +118,7 @@ async def test_outputs_close(control_motors_outputs, hass, lcn_connection): MotorStateModifier.DOWN, MotorReverseTime.RT1200 ) - state = hass.states.get("cover.cover_outputs") + state = hass.states.get(COVER_OUTPUTS) assert state is not None assert state.state != STATE_CLOSING @@ -126,7 +129,7 @@ async def test_outputs_close(control_motors_outputs, hass, lcn_connection): await hass.services.async_call( DOMAIN_COVER, SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.cover_outputs"}, + {ATTR_ENTITY_ID: COVER_OUTPUTS}, blocking=True, ) await hass.async_block_till_done() @@ -134,7 +137,7 @@ async def test_outputs_close(control_motors_outputs, hass, lcn_connection): MotorStateModifier.DOWN, MotorReverseTime.RT1200 ) - state = hass.states.get("cover.cover_outputs") + state = hass.states.get(COVER_OUTPUTS) assert state is not None assert state.state == STATE_CLOSING @@ -142,7 +145,7 @@ async def test_outputs_close(control_motors_outputs, hass, lcn_connection): @patch.object(MockModuleConnection, "control_motors_outputs") async def test_outputs_stop(control_motors_outputs, hass, lcn_connection): """Test the outputs cover stops.""" - state = hass.states.get("cover.cover_outputs") + state = hass.states.get(COVER_OUTPUTS) state.state = STATE_CLOSING # command failed @@ -151,13 +154,13 @@ async def test_outputs_stop(control_motors_outputs, hass, lcn_connection): await hass.services.async_call( DOMAIN_COVER, SERVICE_STOP_COVER, - {ATTR_ENTITY_ID: "cover.cover_outputs"}, + {ATTR_ENTITY_ID: COVER_OUTPUTS}, blocking=True, ) await hass.async_block_till_done() control_motors_outputs.assert_awaited_with(MotorStateModifier.STOP) - state = hass.states.get("cover.cover_outputs") + state = hass.states.get(COVER_OUTPUTS) assert state is not None assert state.state == STATE_CLOSING @@ -168,13 +171,13 @@ async def test_outputs_stop(control_motors_outputs, hass, lcn_connection): await hass.services.async_call( DOMAIN_COVER, SERVICE_STOP_COVER, - {ATTR_ENTITY_ID: "cover.cover_outputs"}, + {ATTR_ENTITY_ID: COVER_OUTPUTS}, blocking=True, ) await hass.async_block_till_done() control_motors_outputs.assert_awaited_with(MotorStateModifier.STOP) - state = hass.states.get("cover.cover_outputs") + state = hass.states.get(COVER_OUTPUTS) assert state is not None assert state.state not in (STATE_CLOSING, STATE_OPENING) @@ -185,7 +188,7 @@ async def test_relays_open(control_motors_relays, hass, lcn_connection): states = [MotorStateModifier.NOCHANGE] * 4 states[0] = MotorStateModifier.UP - state = hass.states.get("cover.cover_relays") + state = hass.states.get(COVER_RELAYS) state.state = STATE_CLOSED # command failed @@ -194,13 +197,13 @@ async def test_relays_open(control_motors_relays, hass, lcn_connection): await hass.services.async_call( DOMAIN_COVER, SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.cover_relays"}, + {ATTR_ENTITY_ID: COVER_RELAYS}, blocking=True, ) await hass.async_block_till_done() control_motors_relays.assert_awaited_with(states) - state = hass.states.get("cover.cover_relays") + state = hass.states.get(COVER_RELAYS) assert state is not None assert state.state != STATE_OPENING @@ -211,13 +214,13 @@ async def test_relays_open(control_motors_relays, hass, lcn_connection): await hass.services.async_call( DOMAIN_COVER, SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.cover_relays"}, + {ATTR_ENTITY_ID: COVER_RELAYS}, blocking=True, ) await hass.async_block_till_done() control_motors_relays.assert_awaited_with(states) - state = hass.states.get("cover.cover_relays") + state = hass.states.get(COVER_RELAYS) assert state is not None assert state.state == STATE_OPENING @@ -228,7 +231,7 @@ async def test_relays_close(control_motors_relays, hass, lcn_connection): states = [MotorStateModifier.NOCHANGE] * 4 states[0] = MotorStateModifier.DOWN - state = hass.states.get("cover.cover_relays") + state = hass.states.get(COVER_RELAYS) state.state = STATE_OPEN # command failed @@ -237,13 +240,13 @@ async def test_relays_close(control_motors_relays, hass, lcn_connection): await hass.services.async_call( DOMAIN_COVER, SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.cover_relays"}, + {ATTR_ENTITY_ID: COVER_RELAYS}, blocking=True, ) await hass.async_block_till_done() control_motors_relays.assert_awaited_with(states) - state = hass.states.get("cover.cover_relays") + state = hass.states.get(COVER_RELAYS) assert state is not None assert state.state != STATE_CLOSING @@ -254,13 +257,13 @@ async def test_relays_close(control_motors_relays, hass, lcn_connection): await hass.services.async_call( DOMAIN_COVER, SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.cover_relays"}, + {ATTR_ENTITY_ID: COVER_RELAYS}, blocking=True, ) await hass.async_block_till_done() control_motors_relays.assert_awaited_with(states) - state = hass.states.get("cover.cover_relays") + state = hass.states.get(COVER_RELAYS) assert state is not None assert state.state == STATE_CLOSING @@ -271,7 +274,7 @@ async def test_relays_stop(control_motors_relays, hass, lcn_connection): states = [MotorStateModifier.NOCHANGE] * 4 states[0] = MotorStateModifier.STOP - state = hass.states.get("cover.cover_relays") + state = hass.states.get(COVER_RELAYS) state.state = STATE_CLOSING # command failed @@ -280,13 +283,13 @@ async def test_relays_stop(control_motors_relays, hass, lcn_connection): await hass.services.async_call( DOMAIN_COVER, SERVICE_STOP_COVER, - {ATTR_ENTITY_ID: "cover.cover_relays"}, + {ATTR_ENTITY_ID: COVER_RELAYS}, blocking=True, ) await hass.async_block_till_done() control_motors_relays.assert_awaited_with(states) - state = hass.states.get("cover.cover_relays") + state = hass.states.get(COVER_RELAYS) assert state is not None assert state.state == STATE_CLOSING @@ -297,13 +300,13 @@ async def test_relays_stop(control_motors_relays, hass, lcn_connection): await hass.services.async_call( DOMAIN_COVER, SERVICE_STOP_COVER, - {ATTR_ENTITY_ID: "cover.cover_relays"}, + {ATTR_ENTITY_ID: COVER_RELAYS}, blocking=True, ) await hass.async_block_till_done() control_motors_relays.assert_awaited_with(states) - state = hass.states.get("cover.cover_relays") + state = hass.states.get(COVER_RELAYS) assert state is not None assert state.state not in (STATE_CLOSING, STATE_OPENING) @@ -313,33 +316,33 @@ async def test_pushed_outputs_status_change(hass, entry, lcn_connection): device_connection = get_device_connection(hass, (0, 7, False), entry) address = LcnAddr(0, 7, False) - state = hass.states.get("cover.cover_outputs") + state = hass.states.get(COVER_OUTPUTS) state.state = STATE_CLOSED # push status "open" - input = ModStatusOutput(address, 0, 100) - await device_connection.async_process_input(input) + inp = ModStatusOutput(address, 0, 100) + await device_connection.async_process_input(inp) await hass.async_block_till_done() - state = hass.states.get("cover.cover_outputs") + state = hass.states.get(COVER_OUTPUTS) assert state is not None assert state.state == STATE_OPENING # push status "stop" - input = ModStatusOutput(address, 0, 0) - await device_connection.async_process_input(input) + inp = ModStatusOutput(address, 0, 0) + await device_connection.async_process_input(inp) await hass.async_block_till_done() - state = hass.states.get("cover.cover_outputs") + state = hass.states.get(COVER_OUTPUTS) assert state is not None assert state.state not in (STATE_OPENING, STATE_CLOSING) # push status "close" - input = ModStatusOutput(address, 1, 100) - await device_connection.async_process_input(input) + inp = ModStatusOutput(address, 1, 100) + await device_connection.async_process_input(inp) await hass.async_block_till_done() - state = hass.states.get("cover.cover_outputs") + state = hass.states.get(COVER_OUTPUTS) assert state is not None assert state.state == STATE_CLOSING @@ -350,36 +353,36 @@ async def test_pushed_relays_status_change(hass, entry, lcn_connection): address = LcnAddr(0, 7, False) states = [False] * 8 - state = hass.states.get("cover.cover_relays") + state = hass.states.get(COVER_RELAYS) state.state = STATE_CLOSED # push status "open" states[0:2] = [True, False] - input = ModStatusRelays(address, states) - await device_connection.async_process_input(input) + inp = ModStatusRelays(address, states) + await device_connection.async_process_input(inp) await hass.async_block_till_done() - state = hass.states.get("cover.cover_relays") + state = hass.states.get(COVER_RELAYS) assert state is not None assert state.state == STATE_OPENING # push status "stop" states[0] = False - input = ModStatusRelays(address, states) - await device_connection.async_process_input(input) + inp = ModStatusRelays(address, states) + await device_connection.async_process_input(inp) await hass.async_block_till_done() - state = hass.states.get("cover.cover_relays") + state = hass.states.get(COVER_RELAYS) assert state is not None assert state.state not in (STATE_OPENING, STATE_CLOSING) # push status "close" states[0:2] = [True, True] - input = ModStatusRelays(address, states) - await device_connection.async_process_input(input) + inp = ModStatusRelays(address, states) + await device_connection.async_process_input(inp) await hass.async_block_till_done() - state = hass.states.get("cover.cover_relays") + state = hass.states.get(COVER_RELAYS) assert state is not None assert state.state == STATE_CLOSING @@ -387,5 +390,5 @@ async def test_pushed_relays_status_change(hass, entry, lcn_connection): async def test_unload_config_entry(hass, entry, lcn_connection): """Test the cover is removed when the config entry is unloaded.""" await hass.config_entries.async_unload(entry.entry_id) - assert hass.states.get("cover.cover_outputs").state == STATE_UNAVAILABLE - assert hass.states.get("cover.cover_relays").state == STATE_UNAVAILABLE + assert hass.states.get(COVER_OUTPUTS).state == STATE_UNAVAILABLE + assert hass.states.get(COVER_RELAYS).state == STATE_UNAVAILABLE diff --git a/tests/components/lcn/test_events.py b/tests/components/lcn/test_events.py index 518af46b02a..9786d1895da 100644 --- a/tests/components/lcn/test_events.py +++ b/tests/components/lcn/test_events.py @@ -5,10 +5,15 @@ from pypck.lcn_defs import AccessControlPeriphery, KeyAction, SendKeyCommand from tests.common import async_capture_events +LCN_TRANSPONDER = "lcn_transponder" +LCN_FINGERPRINT = "lcn_fingerprint" +LCN_TRANSMITTER = "lcn_transmitter" +LCN_SEND_KEYS = "lcn_send_keys" + async def test_fire_transponder_event(hass, lcn_connection): """Test the transponder event is fired.""" - events = async_capture_events(hass, "lcn_transponder") + events = async_capture_events(hass, LCN_TRANSPONDER) inp = ModStatusAccessControl( LcnAddr(0, 7, False), @@ -20,13 +25,13 @@ async def test_fire_transponder_event(hass, lcn_connection): await hass.async_block_till_done() assert len(events) == 1 - assert events[0].event_type == "lcn_transponder" + assert events[0].event_type == LCN_TRANSPONDER assert events[0].data["code"] == "aabbcc" async def test_fire_fingerprint_event(hass, lcn_connection): """Test the fingerprint event is fired.""" - events = async_capture_events(hass, "lcn_fingerprint") + events = async_capture_events(hass, LCN_FINGERPRINT) inp = ModStatusAccessControl( LcnAddr(0, 7, False), @@ -38,7 +43,7 @@ async def test_fire_fingerprint_event(hass, lcn_connection): await hass.async_block_till_done() assert len(events) == 1 - assert events[0].event_type == "lcn_fingerprint" + assert events[0].event_type == LCN_FINGERPRINT assert events[0].data["code"] == "aabbcc" @@ -62,7 +67,7 @@ async def test_fire_codelock_event(hass, lcn_connection): async def test_fire_transmitter_event(hass, lcn_connection): """Test the transmitter event is fired.""" - events = async_capture_events(hass, "lcn_transmitter") + events = async_capture_events(hass, LCN_TRANSMITTER) inp = ModStatusAccessControl( LcnAddr(0, 7, False), @@ -77,7 +82,7 @@ async def test_fire_transmitter_event(hass, lcn_connection): await hass.async_block_till_done() assert len(events) == 1 - assert events[0].event_type == "lcn_transmitter" + assert events[0].event_type == LCN_TRANSMITTER assert events[0].data["code"] == "aabbcc" assert events[0].data["level"] == 0 assert events[0].data["key"] == 0 @@ -86,7 +91,7 @@ async def test_fire_transmitter_event(hass, lcn_connection): async def test_fire_sendkeys_event(hass, lcn_connection): """Test the send_keys event is fired.""" - events = async_capture_events(hass, "lcn_send_keys") + events = async_capture_events(hass, LCN_SEND_KEYS) inp = ModSendKeysHost( LcnAddr(0, 7, False), @@ -98,16 +103,16 @@ async def test_fire_sendkeys_event(hass, lcn_connection): await hass.async_block_till_done() assert len(events) == 4 - assert events[0].event_type == "lcn_send_keys" + assert events[0].event_type == LCN_SEND_KEYS assert events[0].data["key"] == "a1" assert events[0].data["action"] == "hit" - assert events[1].event_type == "lcn_send_keys" + assert events[1].event_type == LCN_SEND_KEYS assert events[1].data["key"] == "a2" assert events[1].data["action"] == "hit" - assert events[2].event_type == "lcn_send_keys" + assert events[2].event_type == LCN_SEND_KEYS assert events[2].data["key"] == "b1" assert events[2].data["action"] == "make" - assert events[3].event_type == "lcn_send_keys" + assert events[3].event_type == LCN_SEND_KEYS assert events[3].data["key"] == "b2" assert events[3].data["action"] == "make" @@ -117,10 +122,10 @@ async def test_dont_fire_on_non_module_input(hass, lcn_connection): inp = Input() for event_name in ( - "lcn_transponder", - "lcn_fingerprint", - "lcn_transmitter", - "lcn_send_keys", + LCN_TRANSPONDER, + LCN_FINGERPRINT, + LCN_TRANSMITTER, + LCN_SEND_KEYS, ): events = async_capture_events(hass, event_name) await lcn_connection.async_process_input(inp) @@ -136,7 +141,7 @@ async def test_dont_fire_on_unknown_module(hass, lcn_connection): code="aabbcc", ) - events = async_capture_events(hass, "lcn_fingerprint") + events = async_capture_events(hass, LCN_FINGERPRINT) await lcn_connection.async_process_input(inp) await hass.async_block_till_done() assert len(events) == 0 diff --git a/tests/components/lcn/test_light.py b/tests/components/lcn/test_light.py index efde0daa68f..1795f716868 100644 --- a/tests/components/lcn/test_light.py +++ b/tests/components/lcn/test_light.py @@ -27,13 +27,17 @@ from homeassistant.helpers import entity_registry as er from .conftest import MockModuleConnection +LIGHT_OUTPUT1 = "light.light_output1" +LIGHT_OUTPUT2 = "light.light_output2" +LIGHT_RELAY1 = "light.light_relay1" + async def test_setup_lcn_light(hass, lcn_connection): """Test the setup of light.""" for entity_id in ( - "light.light_output1", - "light.light_output2", - "light.light_relay1", + LIGHT_OUTPUT1, + LIGHT_OUTPUT2, + LIGHT_RELAY1, ): state = hass.states.get(entity_id) assert state is not None @@ -42,12 +46,12 @@ async def test_setup_lcn_light(hass, lcn_connection): async def test_entity_state(hass, lcn_connection): """Test state of entity.""" - state = hass.states.get("light.light_output1") + state = hass.states.get(LIGHT_OUTPUT1) assert state assert state.attributes[ATTR_SUPPORTED_FEATURES] == LightEntityFeature.TRANSITION assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.BRIGHTNESS] - state = hass.states.get("light.light_output2") + state = hass.states.get(LIGHT_OUTPUT2) assert state assert state.attributes[ATTR_SUPPORTED_FEATURES] == LightEntityFeature.TRANSITION assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.ONOFF] @@ -57,13 +61,13 @@ async def test_entity_attributes(hass, entry, lcn_connection): """Test the attributes of an entity.""" entity_registry = er.async_get(hass) - entity_output = entity_registry.async_get("light.light_output1") + entity_output = entity_registry.async_get(LIGHT_OUTPUT1) assert entity_output assert entity_output.unique_id == f"{entry.entry_id}-m000007-output1" assert entity_output.original_name == "Light_Output1" - entity_relay = entity_registry.async_get("light.light_relay1") + entity_relay = entity_registry.async_get(LIGHT_RELAY1) assert entity_relay assert entity_relay.unique_id == f"{entry.entry_id}-m000007-relay1" @@ -79,13 +83,13 @@ async def test_output_turn_on(dim_output, hass, lcn_connection): await hass.services.async_call( DOMAIN_LIGHT, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.light_output1"}, + {ATTR_ENTITY_ID: LIGHT_OUTPUT1}, blocking=True, ) await hass.async_block_till_done() dim_output.assert_awaited_with(0, 100, 9) - state = hass.states.get("light.light_output1") + state = hass.states.get(LIGHT_OUTPUT1) assert state is not None assert state.state != STATE_ON @@ -96,13 +100,13 @@ async def test_output_turn_on(dim_output, hass, lcn_connection): await hass.services.async_call( DOMAIN_LIGHT, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.light_output1"}, + {ATTR_ENTITY_ID: LIGHT_OUTPUT1}, blocking=True, ) await hass.async_block_till_done() dim_output.assert_awaited_with(0, 100, 9) - state = hass.states.get("light.light_output1") + state = hass.states.get(LIGHT_OUTPUT1) assert state is not None assert state.state == STATE_ON @@ -116,7 +120,7 @@ async def test_output_turn_on_with_attributes(dim_output, hass, lcn_connection): DOMAIN_LIGHT, SERVICE_TURN_ON, { - ATTR_ENTITY_ID: "light.light_output1", + ATTR_ENTITY_ID: LIGHT_OUTPUT1, ATTR_BRIGHTNESS: 50, ATTR_TRANSITION: 2, }, @@ -125,7 +129,7 @@ async def test_output_turn_on_with_attributes(dim_output, hass, lcn_connection): await hass.async_block_till_done() dim_output.assert_awaited_with(0, 19, 6) - state = hass.states.get("light.light_output1") + state = hass.states.get(LIGHT_OUTPUT1) assert state is not None assert state.state == STATE_ON @@ -133,7 +137,7 @@ async def test_output_turn_on_with_attributes(dim_output, hass, lcn_connection): @patch.object(MockModuleConnection, "dim_output") async def test_output_turn_off(dim_output, hass, lcn_connection): """Test the output light turns off.""" - state = hass.states.get("light.light_output1") + state = hass.states.get(LIGHT_OUTPUT1) state.state = STATE_ON # command failed @@ -142,13 +146,13 @@ async def test_output_turn_off(dim_output, hass, lcn_connection): await hass.services.async_call( DOMAIN_LIGHT, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.light_output1"}, + {ATTR_ENTITY_ID: LIGHT_OUTPUT1}, blocking=True, ) await hass.async_block_till_done() dim_output.assert_awaited_with(0, 0, 9) - state = hass.states.get("light.light_output1") + state = hass.states.get(LIGHT_OUTPUT1) assert state is not None assert state.state != STATE_OFF @@ -159,13 +163,13 @@ async def test_output_turn_off(dim_output, hass, lcn_connection): await hass.services.async_call( DOMAIN_LIGHT, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.light_output1"}, + {ATTR_ENTITY_ID: LIGHT_OUTPUT1}, blocking=True, ) await hass.async_block_till_done() dim_output.assert_awaited_with(0, 0, 9) - state = hass.states.get("light.light_output1") + state = hass.states.get(LIGHT_OUTPUT1) assert state is not None assert state.state == STATE_OFF @@ -175,14 +179,14 @@ async def test_output_turn_off_with_attributes(dim_output, hass, lcn_connection) """Test the output light turns off.""" dim_output.return_value = True - state = hass.states.get("light.light_output1") + state = hass.states.get(LIGHT_OUTPUT1) state.state = STATE_ON await hass.services.async_call( DOMAIN_LIGHT, SERVICE_TURN_OFF, { - ATTR_ENTITY_ID: "light.light_output1", + ATTR_ENTITY_ID: LIGHT_OUTPUT1, ATTR_TRANSITION: 2, }, blocking=True, @@ -190,7 +194,7 @@ async def test_output_turn_off_with_attributes(dim_output, hass, lcn_connection) await hass.async_block_till_done() dim_output.assert_awaited_with(0, 0, 6) - state = hass.states.get("light.light_output1") + state = hass.states.get(LIGHT_OUTPUT1) assert state is not None assert state.state == STATE_OFF @@ -207,13 +211,13 @@ async def test_relay_turn_on(control_relays, hass, lcn_connection): await hass.services.async_call( DOMAIN_LIGHT, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.light_relay1"}, + {ATTR_ENTITY_ID: LIGHT_RELAY1}, blocking=True, ) await hass.async_block_till_done() control_relays.assert_awaited_with(states) - state = hass.states.get("light.light_relay1") + state = hass.states.get(LIGHT_RELAY1) assert state is not None assert state.state != STATE_ON @@ -224,13 +228,13 @@ async def test_relay_turn_on(control_relays, hass, lcn_connection): await hass.services.async_call( DOMAIN_LIGHT, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.light_relay1"}, + {ATTR_ENTITY_ID: LIGHT_RELAY1}, blocking=True, ) await hass.async_block_till_done() control_relays.assert_awaited_with(states) - state = hass.states.get("light.light_relay1") + state = hass.states.get(LIGHT_RELAY1) assert state is not None assert state.state == STATE_ON @@ -241,7 +245,7 @@ async def test_relay_turn_off(control_relays, hass, lcn_connection): states = [RelayStateModifier.NOCHANGE] * 8 states[0] = RelayStateModifier.OFF - state = hass.states.get("light.light_relay1") + state = hass.states.get(LIGHT_RELAY1) state.state = STATE_ON # command failed @@ -250,13 +254,13 @@ async def test_relay_turn_off(control_relays, hass, lcn_connection): await hass.services.async_call( DOMAIN_LIGHT, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.light_relay1"}, + {ATTR_ENTITY_ID: LIGHT_RELAY1}, blocking=True, ) await hass.async_block_till_done() control_relays.assert_awaited_with(states) - state = hass.states.get("light.light_relay1") + state = hass.states.get(LIGHT_RELAY1) assert state is not None assert state.state != STATE_OFF @@ -267,13 +271,13 @@ async def test_relay_turn_off(control_relays, hass, lcn_connection): await hass.services.async_call( DOMAIN_LIGHT, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.light_relay1"}, + {ATTR_ENTITY_ID: LIGHT_RELAY1}, blocking=True, ) await hass.async_block_till_done() control_relays.assert_awaited_with(states) - state = hass.states.get("light.light_relay1") + state = hass.states.get(LIGHT_RELAY1) assert state is not None assert state.state == STATE_OFF @@ -288,7 +292,7 @@ async def test_pushed_output_status_change(hass, entry, lcn_connection): await device_connection.async_process_input(inp) await hass.async_block_till_done() - state = hass.states.get("light.light_output1") + state = hass.states.get(LIGHT_OUTPUT1) assert state is not None assert state.state == STATE_ON assert state.attributes[ATTR_BRIGHTNESS] == 127 @@ -298,7 +302,7 @@ async def test_pushed_output_status_change(hass, entry, lcn_connection): await device_connection.async_process_input(inp) await hass.async_block_till_done() - state = hass.states.get("light.light_output1") + state = hass.states.get(LIGHT_OUTPUT1) assert state is not None assert state.state == STATE_OFF @@ -315,7 +319,7 @@ async def test_pushed_relay_status_change(hass, entry, lcn_connection): await device_connection.async_process_input(inp) await hass.async_block_till_done() - state = hass.states.get("light.light_relay1") + state = hass.states.get(LIGHT_RELAY1) assert state is not None assert state.state == STATE_ON @@ -325,7 +329,7 @@ async def test_pushed_relay_status_change(hass, entry, lcn_connection): await device_connection.async_process_input(inp) await hass.async_block_till_done() - state = hass.states.get("light.light_relay1") + state = hass.states.get(LIGHT_RELAY1) assert state is not None assert state.state == STATE_OFF @@ -333,4 +337,4 @@ async def test_pushed_relay_status_change(hass, entry, lcn_connection): async def test_unload_config_entry(hass, entry, lcn_connection): """Test the light is removed when the config entry is unloaded.""" await hass.config_entries.async_unload(entry.entry_id) - assert hass.states.get("light.light_output1").state == STATE_UNAVAILABLE + assert hass.states.get(LIGHT_OUTPUT1).state == STATE_UNAVAILABLE diff --git a/tests/components/lcn/test_sensor.py b/tests/components/lcn/test_sensor.py new file mode 100644 index 00000000000..4b6c0beb7e2 --- /dev/null +++ b/tests/components/lcn/test_sensor.py @@ -0,0 +1,131 @@ +"""Test for the LCN sensor platform.""" +from pypck.inputs import ModStatusLedsAndLogicOps, ModStatusVar +from pypck.lcn_addr import LcnAddr +from pypck.lcn_defs import LedStatus, LogicOpStatus, Var, VarValue + +from homeassistant.components.lcn.helpers import get_device_connection +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + TEMP_CELSIUS, +) +from homeassistant.helpers import entity_registry as er + +SENSOR_VAR1 = "sensor.sensor_var1" +SENSOR_SETPOINT1 = "sensor.sensor_setpoint1" +SENSOR_LED6 = "sensor.sensor_led6" +SENSOR_LOGICOP1 = "sensor.sensor_logicop1" + + +async def test_setup_lcn_sensor(hass, entry, lcn_connection): + """Test the setup of sensor.""" + for entity_id in ( + SENSOR_VAR1, + SENSOR_SETPOINT1, + SENSOR_LED6, + SENSOR_LOGICOP1, + ): + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_UNKNOWN + + +async def test_entity_state(hass, lcn_connection): + """Test state of entity.""" + state = hass.states.get(SENSOR_VAR1) + assert state + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + + state = hass.states.get(SENSOR_SETPOINT1) + assert state + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + + state = hass.states.get(SENSOR_LED6) + assert state + + state = hass.states.get(SENSOR_LOGICOP1) + assert state + + +async def test_entity_attributes(hass, entry, lcn_connection): + """Test the attributes of an entity.""" + entity_registry = er.async_get(hass) + + entity_var1 = entity_registry.async_get(SENSOR_VAR1) + assert entity_var1 + assert entity_var1.unique_id == f"{entry.entry_id}-m000007-var1" + assert entity_var1.original_name == "Sensor_Var1" + + entity_r1varsetpoint = entity_registry.async_get(SENSOR_SETPOINT1) + assert entity_r1varsetpoint + assert entity_r1varsetpoint.unique_id == f"{entry.entry_id}-m000007-r1varsetpoint" + assert entity_r1varsetpoint.original_name == "Sensor_Setpoint1" + + entity_led6 = entity_registry.async_get(SENSOR_LED6) + assert entity_led6 + assert entity_led6.unique_id == f"{entry.entry_id}-m000007-led6" + assert entity_led6.original_name == "Sensor_Led6" + + entity_logicop1 = entity_registry.async_get(SENSOR_LOGICOP1) + assert entity_logicop1 + assert entity_logicop1.unique_id == f"{entry.entry_id}-m000007-logicop1" + assert entity_logicop1.original_name == "Sensor_LogicOp1" + + +async def test_pushed_variable_status_change(hass, entry, lcn_connection): + """Test the variable sensor changes its state on status received.""" + device_connection = get_device_connection(hass, (0, 7, False), entry) + address = LcnAddr(0, 7, False) + + # push status variable + inp = ModStatusVar(address, Var.VAR1, VarValue.from_celsius(42)) + await device_connection.async_process_input(inp) + await hass.async_block_till_done() + + state = hass.states.get(SENSOR_VAR1) + assert state is not None + assert float(state.state) == 42.0 + + # push status setpoint + inp = ModStatusVar(address, Var.R1VARSETPOINT, VarValue.from_celsius(42)) + await device_connection.async_process_input(inp) + await hass.async_block_till_done() + + state = hass.states.get(SENSOR_SETPOINT1) + assert state is not None + assert float(state.state) == 42.0 + + +async def test_pushed_ledlogicop_status_change(hass, entry, lcn_connection): + """Test the led and logicop sensor changes its state on status received.""" + device_connection = get_device_connection(hass, (0, 7, False), entry) + address = LcnAddr(0, 7, False) + + states_led = [LedStatus.OFF] * 12 + states_logicop = [LogicOpStatus.NONE] * 4 + + states_led[5] = LedStatus.ON + states_logicop[0] = LogicOpStatus.ALL + + # push status led and logicop + inp = ModStatusLedsAndLogicOps(address, states_led, states_logicop) + await device_connection.async_process_input(inp) + await hass.async_block_till_done() + + state = hass.states.get(SENSOR_LED6) + assert state is not None + assert state.state == "on" + + state = hass.states.get(SENSOR_LOGICOP1) + assert state is not None + assert state.state == "all" + + +async def test_unload_config_entry(hass, entry, lcn_connection): + """Test the sensor is removed when the config entry is unloaded.""" + await hass.config_entries.async_unload(entry.entry_id) + assert hass.states.get(SENSOR_VAR1).state == STATE_UNAVAILABLE + assert hass.states.get(SENSOR_SETPOINT1).state == STATE_UNAVAILABLE + assert hass.states.get(SENSOR_LED6).state == STATE_UNAVAILABLE + assert hass.states.get(SENSOR_LOGICOP1).state == STATE_UNAVAILABLE diff --git a/tests/components/lcn/test_switch.py b/tests/components/lcn/test_switch.py index 8c4fb1ff0a8..a21bd35db09 100644 --- a/tests/components/lcn/test_switch.py +++ b/tests/components/lcn/test_switch.py @@ -19,14 +19,19 @@ from homeassistant.helpers import entity_registry as er from .conftest import MockModuleConnection +SWITCH_OUTPUT1 = "switch.switch_output1" +SWITCH_OUTPUT2 = "switch.switch_output2" +SWITCH_RELAY1 = "switch.switch_relay1" +SWITCH_RELAY2 = "switch.switch_relay2" + async def test_setup_lcn_switch(hass, lcn_connection): """Test the setup of switch.""" for entity_id in ( - "switch.switch_output1", - "switch.switch_output2", - "switch.switch_relay1", - "switch.switch_relay2", + SWITCH_OUTPUT1, + SWITCH_OUTPUT2, + SWITCH_RELAY1, + SWITCH_RELAY2, ): state = hass.states.get(entity_id) assert state is not None @@ -37,13 +42,13 @@ async def test_entity_attributes(hass, entry, lcn_connection): """Test the attributes of an entity.""" entity_registry = er.async_get(hass) - entity_output = entity_registry.async_get("switch.switch_output1") + entity_output = entity_registry.async_get(SWITCH_OUTPUT1) assert entity_output assert entity_output.unique_id == f"{entry.entry_id}-m000007-output1" assert entity_output.original_name == "Switch_Output1" - entity_relay = entity_registry.async_get("switch.switch_relay1") + entity_relay = entity_registry.async_get(SWITCH_RELAY1) assert entity_relay assert entity_relay.unique_id == f"{entry.entry_id}-m000007-relay1" @@ -59,13 +64,13 @@ async def test_output_turn_on(dim_output, hass, lcn_connection): await hass.services.async_call( DOMAIN_SWITCH, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.switch_output1"}, + {ATTR_ENTITY_ID: SWITCH_OUTPUT1}, blocking=True, ) await hass.async_block_till_done() dim_output.assert_awaited_with(0, 100, 0) - state = hass.states.get("switch.switch_output1") + state = hass.states.get(SWITCH_OUTPUT1) assert state.state == STATE_OFF # command success @@ -75,20 +80,20 @@ async def test_output_turn_on(dim_output, hass, lcn_connection): await hass.services.async_call( DOMAIN_SWITCH, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.switch_output1"}, + {ATTR_ENTITY_ID: SWITCH_OUTPUT1}, blocking=True, ) await hass.async_block_till_done() dim_output.assert_awaited_with(0, 100, 0) - state = hass.states.get("switch.switch_output1") + state = hass.states.get(SWITCH_OUTPUT1) assert state.state == STATE_ON @patch.object(MockModuleConnection, "dim_output") async def test_output_turn_off(dim_output, hass, lcn_connection): """Test the output switch turns off.""" - state = hass.states.get("switch.switch_output1") + state = hass.states.get(SWITCH_OUTPUT1) state.state = STATE_ON # command failed @@ -97,13 +102,13 @@ async def test_output_turn_off(dim_output, hass, lcn_connection): await hass.services.async_call( DOMAIN_SWITCH, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.switch_output1"}, + {ATTR_ENTITY_ID: SWITCH_OUTPUT1}, blocking=True, ) await hass.async_block_till_done() dim_output.assert_awaited_with(0, 0, 0) - state = hass.states.get("switch.switch_output1") + state = hass.states.get(SWITCH_OUTPUT1) assert state.state == STATE_ON # command success @@ -113,13 +118,13 @@ async def test_output_turn_off(dim_output, hass, lcn_connection): await hass.services.async_call( DOMAIN_SWITCH, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.switch_output1"}, + {ATTR_ENTITY_ID: SWITCH_OUTPUT1}, blocking=True, ) await hass.async_block_till_done() dim_output.assert_awaited_with(0, 0, 0) - state = hass.states.get("switch.switch_output1") + state = hass.states.get(SWITCH_OUTPUT1) assert state.state == STATE_OFF @@ -135,13 +140,13 @@ async def test_relay_turn_on(control_relays, hass, lcn_connection): await hass.services.async_call( DOMAIN_SWITCH, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.switch_relay1"}, + {ATTR_ENTITY_ID: SWITCH_RELAY1}, blocking=True, ) await hass.async_block_till_done() control_relays.assert_awaited_with(states) - state = hass.states.get("switch.switch_relay1") + state = hass.states.get(SWITCH_RELAY1) assert state.state == STATE_OFF # command success @@ -151,13 +156,13 @@ async def test_relay_turn_on(control_relays, hass, lcn_connection): await hass.services.async_call( DOMAIN_SWITCH, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.switch_relay1"}, + {ATTR_ENTITY_ID: SWITCH_RELAY1}, blocking=True, ) await hass.async_block_till_done() control_relays.assert_awaited_with(states) - state = hass.states.get("switch.switch_relay1") + state = hass.states.get(SWITCH_RELAY1) assert state.state == STATE_ON @@ -167,7 +172,7 @@ async def test_relay_turn_off(control_relays, hass, lcn_connection): states = [RelayStateModifier.NOCHANGE] * 8 states[0] = RelayStateModifier.OFF - state = hass.states.get("switch.switch_relay1") + state = hass.states.get(SWITCH_RELAY1) state.state = STATE_ON # command failed @@ -176,13 +181,13 @@ async def test_relay_turn_off(control_relays, hass, lcn_connection): await hass.services.async_call( DOMAIN_SWITCH, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.switch_relay1"}, + {ATTR_ENTITY_ID: SWITCH_RELAY1}, blocking=True, ) await hass.async_block_till_done() control_relays.assert_awaited_with(states) - state = hass.states.get("switch.switch_relay1") + state = hass.states.get(SWITCH_RELAY1) assert state.state == STATE_ON # command success @@ -192,13 +197,13 @@ async def test_relay_turn_off(control_relays, hass, lcn_connection): await hass.services.async_call( DOMAIN_SWITCH, SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.switch_relay1"}, + {ATTR_ENTITY_ID: SWITCH_RELAY1}, blocking=True, ) await hass.async_block_till_done() control_relays.assert_awaited_with(states) - state = hass.states.get("switch.switch_relay1") + state = hass.states.get(SWITCH_RELAY1) assert state.state == STATE_OFF @@ -212,7 +217,7 @@ async def test_pushed_output_status_change(hass, entry, lcn_connection): await device_connection.async_process_input(inp) await hass.async_block_till_done() - state = hass.states.get("switch.switch_output1") + state = hass.states.get(SWITCH_OUTPUT1) assert state.state == STATE_ON # push status "off" @@ -220,7 +225,7 @@ async def test_pushed_output_status_change(hass, entry, lcn_connection): await device_connection.async_process_input(inp) await hass.async_block_till_done() - state = hass.states.get("switch.switch_output1") + state = hass.states.get(SWITCH_OUTPUT1) assert state.state == STATE_OFF @@ -236,7 +241,7 @@ async def test_pushed_relay_status_change(hass, entry, lcn_connection): await device_connection.async_process_input(inp) await hass.async_block_till_done() - state = hass.states.get("switch.switch_relay1") + state = hass.states.get(SWITCH_RELAY1) assert state.state == STATE_ON # push status "off" @@ -245,11 +250,11 @@ async def test_pushed_relay_status_change(hass, entry, lcn_connection): await device_connection.async_process_input(inp) await hass.async_block_till_done() - state = hass.states.get("switch.switch_relay1") + state = hass.states.get(SWITCH_RELAY1) assert state.state == STATE_OFF async def test_unload_config_entry(hass, entry, lcn_connection): """Test the switch is removed when the config entry is unloaded.""" await hass.config_entries.async_unload(entry.entry_id) - assert hass.states.get("switch.switch_output1").state == STATE_UNAVAILABLE + assert hass.states.get(SWITCH_OUTPUT1).state == STATE_UNAVAILABLE From 596f60bdb5eeff13bca93795554439be6145bbbd Mon Sep 17 00:00:00 2001 From: Chris Browet Date: Wed, 29 Jun 2022 07:03:56 +0200 Subject: [PATCH 1921/3516] Universal media player: ordered states (#68036) --- .../components/universal/media_player.py | 29 ++++++++++++++----- .../components/universal/test_media_player.py | 6 ++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 7ffd8b9d13d..f33db3827af 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -72,6 +72,8 @@ from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_ON, + STATE_PAUSED, + STATE_PLAYING, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -94,8 +96,15 @@ CONF_ATTRS = "attributes" CONF_CHILDREN = "children" CONF_COMMANDS = "commands" -OFF_STATES = [STATE_IDLE, STATE_OFF, STATE_UNAVAILABLE, STATE_UNKNOWN] - +STATES_ORDER = [ + STATE_UNKNOWN, + STATE_UNAVAILABLE, + STATE_OFF, + STATE_IDLE, + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, +] ATTRS_SCHEMA = cv.schema_with_slug_keys(cv.string) CMD_SCHEMA = cv.schema_with_slug_keys(cv.SERVICE_SCHEMA) @@ -614,9 +623,15 @@ class UniversalMediaPlayer(MediaPlayerEntity): async def async_update(self): """Update state in HA.""" - for child_name in self._children: - child_state = self.hass.states.get(child_name) - if child_state and child_state.state not in OFF_STATES: - self._child_state = child_state - return self._child_state = None + for child_name in self._children: + if (child_state := self.hass.states.get(child_name)) and STATES_ORDER.index( + child_state.state + ) >= STATES_ORDER.index(STATE_IDLE): + if self._child_state: + if STATES_ORDER.index(child_state.state) > STATES_ORDER.index( + self._child_state.state + ): + self._child_state = child_state + else: + self._child_state = child_state diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index d4fe2ce64ae..c50c3e97713 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -422,6 +422,12 @@ async def test_active_child_state(hass, mock_states): await ump.async_update() assert mock_states.mock_mp_1.entity_id == ump._child_state.entity_id + mock_states.mock_mp_1._state = STATE_PAUSED + mock_states.mock_mp_1.async_schedule_update_ha_state() + await hass.async_block_till_done() + await ump.async_update() + assert mock_states.mock_mp_2.entity_id == ump._child_state.entity_id + mock_states.mock_mp_1._state = STATE_OFF mock_states.mock_mp_1.async_schedule_update_ha_state() await hass.async_block_till_done() From 90c68085bef5b91e9f15fb11f4eb9fc83072c1fc Mon Sep 17 00:00:00 2001 From: Thomas Schamm Date: Wed, 29 Jun 2022 07:08:16 +0200 Subject: [PATCH 1922/3516] Differ device and domain entities in bosch_shc integration (#67957) --- .../components/bosch_shc/__init__.py | 7 +- homeassistant/components/bosch_shc/entity.py | 81 +++++++++++++++---- 2 files changed, 70 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/bosch_shc/__init__.py b/homeassistant/components/bosch_shc/__init__.py index 2b95702e44c..4d076a784d1 100644 --- a/homeassistant/components/bosch_shc/__init__.py +++ b/homeassistant/components/bosch_shc/__init__.py @@ -19,7 +19,12 @@ from .const import ( DOMAIN, ) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.COVER, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.COVER, + Platform.SENSOR, + Platform.SWITCH, +] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bosch_shc/entity.py b/homeassistant/components/bosch_shc/entity.py index c3a981aa658..4ef0e37132d 100644 --- a/homeassistant/components/bosch_shc/entity.py +++ b/homeassistant/components/bosch_shc/entity.py @@ -1,5 +1,7 @@ """Bosch Smart Home Controller base entity.""" -from boschshcpy.device import SHCDevice +from __future__ import annotations + +from boschshcpy import SHCDevice, SHCIntrusionSystem from homeassistant.helpers.device_registry import async_get as get_dev_reg from homeassistant.helpers.entity import DeviceInfo, Entity @@ -7,7 +9,7 @@ from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN -async def async_remove_devices(hass, entity, entry_id): +async def async_remove_devices(hass, entity, entry_id) -> None: """Get item that is removed from session.""" dev_registry = get_dev_reg(hass) device = dev_registry.async_get_device( @@ -17,16 +19,42 @@ async def async_remove_devices(hass, entity, entry_id): dev_registry.async_update_device(device.id, remove_config_entry_id=entry_id) -class SHCEntity(Entity): - """Representation of a SHC base entity.""" +class SHCBaseEntity(Entity): + """Base representation of a SHC entity.""" _attr_should_poll = False - def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + def __init__( + self, device: SHCDevice | SHCIntrusionSystem, parent_id: str, entry_id: str + ) -> None: """Initialize the generic SHC device.""" self._device = device self._entry_id = entry_id self._attr_name = device.name + + async def async_added_to_hass(self) -> None: + """Subscribe to SHC events.""" + await super().async_added_to_hass() + + def on_state_changed() -> None: + if self._device.deleted: + self.hass.add_job(async_remove_devices(self.hass, self, self._entry_id)) + else: + self.schedule_update_ha_state() + + self._device.subscribe_callback(self.entity_id, on_state_changed) + + async def async_will_remove_from_hass(self) -> None: + """Unsubscribe from SHC events.""" + await super().async_will_remove_from_hass() + self._device.unsubscribe_callback(self.entity_id) + + +class SHCEntity(SHCBaseEntity): + """Representation of a SHC device entity.""" + + def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None: + """Initialize generic SHC device.""" self._attr_unique_id = device.serial self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, device.id)}, @@ -40,32 +68,51 @@ class SHCEntity(Entity): else parent_id, ), ) + super().__init__(device=device, parent_id=parent_id, entry_id=entry_id) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to SHC events.""" await super().async_added_to_hass() - def on_state_changed(): + def on_state_changed() -> None: self.schedule_update_ha_state() - def update_entity_information(): - if self._device.deleted: - self.hass.add_job(async_remove_devices(self.hass, self, self._entry_id)) - else: - self.schedule_update_ha_state() - for service in self._device.device_services: service.subscribe_callback(self.entity_id, on_state_changed) - self._device.subscribe_callback(self.entity_id, update_entity_information) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Unsubscribe from SHC events.""" await super().async_will_remove_from_hass() for service in self._device.device_services: service.unsubscribe_callback(self.entity_id) - self._device.unsubscribe_callback(self.entity_id) @property - def available(self): + def available(self) -> bool: """Return false if status is unavailable.""" return self._device.status == "AVAILABLE" + + +class SHCDomainEntity(SHCBaseEntity): + """Representation of a SHC domain service entity.""" + + def __init__( + self, domain: SHCIntrusionSystem, parent_id: str, entry_id: str + ) -> None: + """Initialize the generic SHC device.""" + self._attr_unique_id = domain.id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, domain.id)}, + manufacturer=domain.manufacturer, + model=domain.device_model, + name=domain.name, + via_device=( + DOMAIN, + parent_id, + ), + ) + super().__init__(device=domain, parent_id=parent_id, entry_id=entry_id) + + @property + def available(self) -> bool: + """Return false if status is unavailable.""" + return self._device.system_availability From 00810235c92b492a966c6021021d49360ffb3cdd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 29 Jun 2022 09:38:35 +0200 Subject: [PATCH 1923/3516] Track tasks adding entities (#73828) * Track tasks adding entities * Update homeassistant/config_entries.py * fix cast tests Co-authored-by: J. Nick Koston --- homeassistant/config_entries.py | 37 ++++++++++++++++++---- homeassistant/helpers/entity_platform.py | 17 +++++++++- tests/components/cast/test_media_player.py | 2 +- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 2aa5b1b8c62..c832bab7eb4 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -73,6 +73,7 @@ PATH_CONFIG = ".config_entries.json" SAVE_DELAY = 1 _T = TypeVar("_T", bound="ConfigEntryState") +_R = TypeVar("_R") class ConfigEntryState(Enum): @@ -193,6 +194,7 @@ class ConfigEntry: "_async_cancel_retry_setup", "_on_unload", "reload_lock", + "_pending_tasks", ) def __init__( @@ -285,6 +287,8 @@ class ConfigEntry: # Reload lock to prevent conflicting reloads self.reload_lock = asyncio.Lock() + self._pending_tasks: list[asyncio.Future[Any]] = [] + async def async_setup( self, hass: HomeAssistant, @@ -366,7 +370,7 @@ class ConfigEntry: self.domain, auth_message, ) - self._async_process_on_unload() + await self._async_process_on_unload() self.async_start_reauth(hass) result = False except ConfigEntryNotReady as ex: @@ -406,7 +410,7 @@ class ConfigEntry: EVENT_HOMEASSISTANT_STARTED, setup_again ) - self._async_process_on_unload() + await self._async_process_on_unload() return except Exception: # pylint: disable=broad-except _LOGGER.exception( @@ -494,7 +498,7 @@ class ConfigEntry: self.state = ConfigEntryState.NOT_LOADED self.reason = None - self._async_process_on_unload() + await self._async_process_on_unload() # https://github.com/python/mypy/issues/11839 return result # type: ignore[no-any-return] @@ -619,13 +623,18 @@ class ConfigEntry: self._on_unload = [] self._on_unload.append(func) - @callback - def _async_process_on_unload(self) -> None: - """Process the on_unload callbacks.""" + async def _async_process_on_unload(self) -> None: + """Process the on_unload callbacks and wait for pending tasks.""" if self._on_unload is not None: while self._on_unload: self._on_unload.pop()() + while self._pending_tasks: + pending = [task for task in self._pending_tasks if not task.done()] + self._pending_tasks.clear() + if pending: + await asyncio.gather(*pending) + @callback def async_start_reauth(self, hass: HomeAssistant) -> None: """Start a reauth flow.""" @@ -648,6 +657,22 @@ class ConfigEntry: ) ) + @callback + def async_create_task( + self, hass: HomeAssistant, target: Coroutine[Any, Any, _R] + ) -> asyncio.Task[_R]: + """Create a task from within the eventloop. + + This method must be run in the event loop. + + target: target to call. + """ + task = hass.async_create_task(target) + + self._pending_tasks.append(task) + + return task + current_entry: ContextVar[ConfigEntry | None] = ContextVar( "current_entry", default=None diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index ec71778af12..6253b939bed 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -214,8 +214,9 @@ class EntityPlatform: def async_create_setup_task() -> Coroutine: """Get task to set up platform.""" config_entries.current_entry.set(config_entry) + return platform.async_setup_entry( # type: ignore[no-any-return,union-attr] - self.hass, config_entry, self._async_schedule_add_entities + self.hass, config_entry, self._async_schedule_add_entities_for_entry ) return await self._async_setup_platform(async_create_setup_task) @@ -334,6 +335,20 @@ class EntityPlatform: if not self._setup_complete: self._tasks.append(task) + @callback + def _async_schedule_add_entities_for_entry( + self, new_entities: Iterable[Entity], update_before_add: bool = False + ) -> None: + """Schedule adding entities for a single platform async and track the task.""" + assert self.config_entry + task = self.config_entry.async_create_task( + self.hass, + self.async_add_entities(new_entities, update_before_add=update_before_add), + ) + + if not self._setup_complete: + self._tasks.append(task) + def add_entities( self, new_entities: Iterable[Entity], update_before_add: bool = False ) -> None: diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index e4df84f6443..00626cc8c16 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -127,7 +127,7 @@ async def async_setup_cast(hass, config=None): config = {} data = {**{"ignore_cec": [], "known_hosts": [], "uuid": []}, **config} with patch( - "homeassistant.helpers.entity_platform.EntityPlatform._async_schedule_add_entities" + "homeassistant.helpers.entity_platform.EntityPlatform._async_schedule_add_entities_for_entry" ) as add_entities: entry = MockConfigEntry(data=data, domain="cast") entry.add_to_hass(hass) From aca0fd317840df4f9b76c820c1757130b4872cf1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 29 Jun 2022 09:46:05 +0200 Subject: [PATCH 1924/3516] Adjust type hints in rflink cover (#73946) * Adjust type hints in rflink cover * Move definition back to init * Use attributes * Revert "Use attributes" This reverts commit ff4851015d5e15b1b1304554228ca274d586977d. * Use _attr_should_poll --- homeassistant/components/rflink/__init__.py | 10 ++++------ homeassistant/components/rflink/cover.py | 11 +++-------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 563ecedec3d..7bc87de9f46 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -1,4 +1,6 @@ """Support for Rflink devices.""" +from __future__ import annotations + import asyncio from collections import defaultdict import logging @@ -315,8 +317,9 @@ class RflinkDevice(Entity): """ platform = None - _state = None + _state: bool | None = None _available = True + _attr_should_poll = False def __init__( self, @@ -369,11 +372,6 @@ class RflinkDevice(Entity): """Platform specific event handler.""" raise NotImplementedError() - @property - def should_poll(self): - """No polling needed.""" - return False - @property def name(self): """Return a name for the device.""" diff --git a/homeassistant/components/rflink/cover.py b/homeassistant/components/rflink/cover.py index 91e68fa0fb8..9492611f439 100644 --- a/homeassistant/components/rflink/cover.py +++ b/homeassistant/components/rflink/cover.py @@ -125,7 +125,7 @@ async def async_setup_platform( class RflinkCover(RflinkCommand, CoverEntity, RestoreEntity): """Rflink entity which can switch on/stop/off (eg: cover).""" - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore RFLink cover state (OPEN/CLOSE).""" await super().async_added_to_hass() if (old_state := await self.async_get_last_state()) is not None: @@ -142,17 +142,12 @@ class RflinkCover(RflinkCommand, CoverEntity, RestoreEntity): self._state = False @property - def should_poll(self): - """No polling available in RFlink cover.""" - return False - - @property - def is_closed(self): + def is_closed(self) -> bool | None: """Return if the cover is closed.""" return not self._state @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return True because covers can be stopped midway.""" return True From 20680535ecd8a4d653b806828b1bf23681622129 Mon Sep 17 00:00:00 2001 From: Maximilian <43999966+DeerMaximum@users.noreply.github.com> Date: Wed, 29 Jun 2022 09:52:21 +0200 Subject: [PATCH 1925/3516] Add options flow to NINA (#65890) * Added options flow * Resolve conflicts * Fix lint * Implement improvements --- homeassistant/components/nina/__init__.py | 12 + homeassistant/components/nina/config_flow.py | 209 ++++++++++++++---- homeassistant/components/nina/strings.json | 22 ++ .../components/nina/translations/en.json | 22 ++ tests/components/nina/__init__.py | 12 +- .../nina/fixtures/sample_regions.json | 37 +++- tests/components/nina/test_config_flow.py | 194 +++++++++++++++- 7 files changed, 453 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/nina/__init__.py b/homeassistant/components/nina/__init__.py index 16b6d01b8c2..c5375c96785 100644 --- a/homeassistant/components/nina/__init__.py +++ b/homeassistant/components/nina/__init__.py @@ -42,6 +42,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -49,6 +51,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + class NINADataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching NINA data API.""" diff --git a/homeassistant/components/nina/config_flow.py b/homeassistant/components/nina/config_flow.py index 0574de96681..aa06b00e0ad 100644 --- a/homeassistant/components/nina/config_flow.py +++ b/homeassistant/components/nina/config_flow.py @@ -7,9 +7,14 @@ from pynina import ApiError, Nina import voluptuous as vol from homeassistant import config_entries +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_registry import ( + async_entries_for_config_entry, + async_get, +) from .const import ( _LOGGER, @@ -22,6 +27,58 @@ from .const import ( ) +def swap_key_value(dict_to_sort: dict[str, str]) -> dict[str, str]: + """Swap keys and values in dict.""" + all_region_codes_swaped: dict[str, str] = {} + + for key, value in dict_to_sort.items(): + if value not in all_region_codes_swaped: + all_region_codes_swaped[value] = key + else: + for i in range(len(dict_to_sort)): + tmp_value: str = f"{value}_{i}" + if tmp_value not in all_region_codes_swaped: + all_region_codes_swaped[tmp_value] = key + break + + return dict(sorted(all_region_codes_swaped.items(), key=lambda ele: ele[1])) + + +def split_regions( + _all_region_codes_sorted: dict[str, str], regions: dict[str, dict[str, Any]] +) -> dict[str, dict[str, Any]]: + """Split regions alphabetical.""" + for index, name in _all_region_codes_sorted.items(): + for region_name, grouping_letters in CONST_REGION_MAPPING.items(): + if name[0] in grouping_letters: + regions[region_name][index] = name + break + return regions + + +def prepare_user_input( + user_input: dict[str, Any], _all_region_codes_sorted: dict[str, str] +) -> dict[str, Any]: + """Prepare the user inputs.""" + tmp: dict[str, Any] = {} + + for reg in user_input[CONF_REGIONS]: + tmp[_all_region_codes_sorted[reg]] = reg.split("_", 1)[0] + + compact: dict[str, Any] = {} + + for key, val in tmp.items(): + if val in compact: + # Abenberg, St + Abenberger Wald + compact[val] = f"{compact[val]} + {key}" + break + compact[val] = key + + user_input[CONF_REGIONS] = compact + + return user_input + + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for NINA.""" @@ -50,7 +107,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): nina: Nina = Nina(async_get_clientsession(self.hass)) try: - self._all_region_codes_sorted = self.swap_key_value( + self._all_region_codes_sorted = swap_key_value( await nina.getAllRegionalCodes() ) except ApiError: @@ -59,7 +116,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception: %s", err) return self.async_abort(reason="unknown") - self.split_regions() + self.regions = split_regions(self._all_region_codes_sorted, self.regions) if user_input is not None and not errors: user_input[CONF_REGIONS] = [] @@ -69,23 +126,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_REGIONS] += group_input if user_input[CONF_REGIONS]: - tmp: dict[str, Any] = {} - for reg in user_input[CONF_REGIONS]: - tmp[self._all_region_codes_sorted[reg]] = reg.split("_", 1)[0] - - compact: dict[str, Any] = {} - - for key, val in tmp.items(): - if val in compact: - # Abenberg, St + Abenberger Wald - compact[val] = f"{compact[val]} + {key}" - break - compact[val] = key - - user_input[CONF_REGIONS] = compact - - return self.async_create_entry(title="NINA", data=user_input) + return self.async_create_entry( + title="NINA", + data=prepare_user_input(user_input, self._all_region_codes_sorted), + ) errors["base"] = "no_selection" @@ -107,26 +152,114 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) @staticmethod - def swap_key_value(dict_to_sort: dict[str, str]) -> dict[str, str]: - """Swap keys and values in dict.""" - all_region_codes_swaped: dict[str, str] = {} + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) - for key, value in dict_to_sort.items(): - if value not in all_region_codes_swaped: - all_region_codes_swaped[value] = key - else: - for i in range(len(dict_to_sort)): - tmp_value: str = f"{value}_{i}" - if tmp_value not in all_region_codes_swaped: - all_region_codes_swaped[tmp_value] = key - break - return dict(sorted(all_region_codes_swaped.items(), key=lambda ele: ele[1])) +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for nut.""" - def split_regions(self) -> None: - """Split regions alphabetical.""" - for index, name in self._all_region_codes_sorted.items(): - for region_name, grouping_letters in CONST_REGION_MAPPING.items(): - if name[0] in grouping_letters: - self.regions[region_name][index] = name - break + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + self.data = dict(self.config_entry.data) + + self._all_region_codes_sorted: dict[str, str] = {} + self.regions: dict[str, dict[str, Any]] = {} + + for name in CONST_REGIONS: + self.regions[name] = {} + if name not in self.data: + self.data[name] = [] + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + errors: dict[str, Any] = {} + + if not self._all_region_codes_sorted: + nina: Nina = Nina(async_get_clientsession(self.hass)) + + try: + self._all_region_codes_sorted = swap_key_value( + await nina.getAllRegionalCodes() + ) + except ApiError: + errors["base"] = "cannot_connect" + except Exception as err: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception: %s", err) + return self.async_abort(reason="unknown") + + self.regions = split_regions(self._all_region_codes_sorted, self.regions) + + if user_input is not None and not errors: + user_input[CONF_REGIONS] = [] + + for group in CONST_REGIONS: + if group_input := user_input.get(group): + user_input[CONF_REGIONS] += group_input + + if user_input[CONF_REGIONS]: + + user_input = prepare_user_input( + user_input, self._all_region_codes_sorted + ) + + entity_registry = async_get(self.hass) + + entries = async_entries_for_config_entry( + entity_registry, self.config_entry.entry_id + ) + + removed_entities_slots = [ + f"{region}-{slot_id}" + for region in self.data[CONF_REGIONS] + for slot_id in range(0, self.data[CONF_MESSAGE_SLOTS] + 1) + if slot_id > user_input[CONF_MESSAGE_SLOTS] + ] + + removed_entites_area = [ + f"{cfg_region}-{slot_id}" + for slot_id in range(1, self.data[CONF_MESSAGE_SLOTS] + 1) + for cfg_region in self.data[CONF_REGIONS] + if cfg_region not in user_input[CONF_REGIONS] + ] + + for entry in entries: + for entity_uid in list( + set(removed_entities_slots + removed_entites_area) + ): + if entry.unique_id == entity_uid: + entity_registry.async_remove(entry.entity_id) + + self.hass.config_entries.async_update_entry( + self.config_entry, data=user_input + ) + + return self.async_create_entry(title="", data=None) + + errors["base"] = "no_selection" + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + **{ + vol.Optional( + region, default=self.data[region] + ): cv.multi_select(self.regions[region]) + for region in CONST_REGIONS + }, + vol.Required( + CONF_MESSAGE_SLOTS, + default=self.data[CONF_MESSAGE_SLOTS], + ): vol.All(int, vol.Range(min=1, max=20)), + vol.Required( + CONF_FILTER_CORONA, + default=self.data[CONF_FILTER_CORONA], + ): cv.boolean, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/nina/strings.json b/homeassistant/components/nina/strings.json index 49ecf7fa7fa..b22c2640084 100644 --- a/homeassistant/components/nina/strings.json +++ b/homeassistant/components/nina/strings.json @@ -23,5 +23,27 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" } + }, + "options": { + "step": { + "init": { + "title": "Options", + "data": { + "_a_to_d": "City/county (A-D)", + "_e_to_h": "City/county (E-H)", + "_i_to_l": "City/county (I-L)", + "_m_to_q": "City/county (M-Q)", + "_r_to_u": "City/county (R-U)", + "_v_to_z": "City/county (V-Z)", + "slots": "Maximum warnings per city/county", + "corona_filter": "Remove Corona Warnings" + } + } + }, + "error": { + "no_selection": "Please select at least one city/county", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } } } diff --git a/homeassistant/components/nina/translations/en.json b/homeassistant/components/nina/translations/en.json index 793cbf595f1..0e46b30512d 100644 --- a/homeassistant/components/nina/translations/en.json +++ b/homeassistant/components/nina/translations/en.json @@ -23,5 +23,27 @@ "title": "Select city/county" } } + }, + "options": { + "error": { + "cannot_connect": "Failed to connect", + "no_selection": "Please select at least one city/county", + "unknown": "Unexpected error" + }, + "step": { + "init": { + "data": { + "_a_to_d": "City/county (A-D)", + "_e_to_h": "City/county (E-H)", + "_i_to_l": "City/county (I-L)", + "_m_to_q": "City/county (M-Q)", + "_r_to_u": "City/county (R-U)", + "_v_to_z": "City/county (V-Z)", + "corona_filter": "Remove Corona Warnings", + "slots": "Maximum warnings per city/county" + }, + "title": "Options" + } + } } } \ No newline at end of file diff --git a/tests/components/nina/__init__.py b/tests/components/nina/__init__.py index d6c9fffdfa7..da09b0ba17b 100644 --- a/tests/components/nina/__init__.py +++ b/tests/components/nina/__init__.py @@ -15,9 +15,19 @@ def mocked_request_function(url: str) -> dict[str, Any]: load_fixture("sample_warning_details.json", "nina") ) - if url == "https://warnung.bund.de/api31/dashboard/083350000000.json": + dummy_response_regions: dict[str, Any] = json.loads( + load_fixture("sample_regions.json", "nina") + ) + + if "https://warnung.bund.de/api31/dashboard/" in url: return dummy_response + if ( + url + == "https://www.xrepository.de/api/xrepository/urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs_2021-07-31/download/Regionalschl_ssel_2021-07-31.json" + ): + return dummy_response_regions + warning_id = url.replace("https://warnung.bund.de/api31/warnings/", "").replace( ".json", "" ) diff --git a/tests/components/nina/fixtures/sample_regions.json b/tests/components/nina/fixtures/sample_regions.json index 4fbc0638604..140feb39c3b 100644 --- a/tests/components/nina/fixtures/sample_regions.json +++ b/tests/components/nina/fixtures/sample_regions.json @@ -3,14 +3,28 @@ "kennung": "urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs_2021-07-31", "kennungInhalt": "urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs", "version": "2021-07-31", - "nameKurz": "Regionalschlüssel", - "nameLang": "Gemeinden, dargestellt durch den Amtlichen Regionalschlüssel (ARS) des Statistischen Bundesamtes", + "nameKurz": [{ "value": "Regionalschlüssel", "lang": null }], + "nameLang": [ + { + "value": "Gemeinden, dargestellt durch den Amtlichen Regionalschlüssel (ARS) des Statistischen Bundesamtes", + "lang": null + } + ], "nameTechnisch": "Regionalschluessel", - "herausgebernameLang": "Statistisches Bundesamt, Wiesbaden", - "herausgebernameKurz": "Destatis", - "beschreibung": "Diese Codeliste stellt alle Gemeinden Deutschlands durch den Amtlichen Regionalschlüssel (ARS) dar, wie im Gemeindeverzeichnis des Statistischen Bundesamtes enthalten. Darüber hinaus enthält die Codeliste für die Stadtstaaten Hamburg, Bremen und Berlin Einträge für Stadt-/Ortsteile bzw. Stadtbezirke. Diese Einträge sind mit einem entsprechenden Hinweis versehen.", - "versionBeschreibung": null, - "aenderungZurVorversion": "Mehrere Aenderungen", + "herausgebernameLang": [ + { "value": "Statistisches Bundesamt, Wiesbaden", "lang": null } + ], + "herausgebernameKurz": [{ "value": "Destatis", "lang": null }], + "beschreibung": [ + { + "value": "Diese Codeliste stellt alle Gemeinden Deutschlands durch den Amtlichen Regionalschlüssel (ARS) dar, wie im Gemeindeverzeichnis des Statistischen Bundesamtes enthalten. Darüber hinaus enthält die Codeliste für die Stadtstaaten Hamburg, Bremen und Berlin Einträge für Stadt-/Ortsteile bzw. Stadtbezirke. Diese Einträge sind mit einem entsprechenden Hinweis versehen.", + "lang": null + } + ], + "versionBeschreibung": [], + "aenderungZurVorversion": [ + { "value": "Mehrere Aenderungen", "lang": null } + ], "handbuchVersion": "1.0", "xoevHandbuch": false, "gueltigAb": 1627682400000, @@ -23,7 +37,8 @@ "datentyp": "string", "codeSpalte": true, "verwendung": { "code": "REQUIRED" }, - "empfohleneCodeSpalte": true + "empfohleneCodeSpalte": true, + "sprache": null }, { "spaltennameLang": "Bezeichnung", @@ -31,7 +46,8 @@ "datentyp": "string", "codeSpalte": false, "verwendung": { "code": "REQUIRED" }, - "empfohleneCodeSpalte": false + "empfohleneCodeSpalte": false, + "sprache": null }, { "spaltennameLang": "Hinweis", @@ -39,7 +55,8 @@ "datentyp": "string", "codeSpalte": false, "verwendung": { "code": "OPTIONAL" }, - "empfohleneCodeSpalte": false + "empfohleneCodeSpalte": false, + "sprache": null } ], "daten": [ diff --git a/tests/components/nina/test_config_flow.py b/tests/components/nina/test_config_flow.py index a1aa97e0fbe..1578991ba11 100644 --- a/tests/components/nina/test_config_flow.py +++ b/tests/components/nina/test_config_flow.py @@ -11,6 +11,7 @@ from homeassistant import data_entry_flow from homeassistant.components.nina.const import ( CONF_FILTER_CORONA, CONF_MESSAGE_SLOTS, + CONF_REGIONS, CONST_REGION_A_TO_D, CONST_REGION_E_TO_H, CONST_REGION_I_TO_L, @@ -21,8 +22,11 @@ from homeassistant.components.nina.const import ( ) from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er -from tests.common import load_fixture +from . import mocked_request_function + +from tests.common import MockConfigEntry, load_fixture DUMMY_DATA: dict[str, Any] = { CONF_MESSAGE_SLOTS: 5, @@ -35,14 +39,19 @@ DUMMY_DATA: dict[str, Any] = { CONF_FILTER_CORONA: True, } -DUMMY_RESPONSE: dict[str, Any] = json.loads(load_fixture("sample_regions.json", "nina")) +DUMMY_RESPONSE_REGIONS: dict[str, Any] = json.loads( + load_fixture("sample_regions.json", "nina") +) +DUMMY_RESPONSE_WARNIGNS: dict[str, Any] = json.loads( + load_fixture("sample_warnings.json", "nina") +) async def test_show_set_form(hass: HomeAssistant) -> None: """Test that the setup form is served.""" with patch( "pynina.baseApi.BaseAPI._makeRequest", - return_value=DUMMY_RESPONSE, + wraps=mocked_request_function, ): result: dict[str, Any] = await hass.config_entries.flow.async_init( @@ -86,7 +95,7 @@ async def test_step_user(hass: HomeAssistant) -> None: """Test starting a flow by user with valid values.""" with patch( "pynina.baseApi.BaseAPI._makeRequest", - return_value=DUMMY_RESPONSE, + wraps=mocked_request_function, ), patch( "homeassistant.components.nina.async_setup_entry", return_value=True, @@ -104,7 +113,7 @@ async def test_step_user_no_selection(hass: HomeAssistant) -> None: """Test starting a flow by user with no selection.""" with patch( "pynina.baseApi.BaseAPI._makeRequest", - return_value=DUMMY_RESPONSE, + wraps=mocked_request_function, ): result: dict[str, Any] = await hass.config_entries.flow.async_init( @@ -120,7 +129,7 @@ async def test_step_user_already_configured(hass: HomeAssistant) -> None: """Test starting a flow by user but it was already configured.""" with patch( "pynina.baseApi.BaseAPI._makeRequest", - return_value=DUMMY_RESPONSE, + wraps=mocked_request_function, ): result: dict[str, Any] = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA @@ -132,3 +141,176 @@ async def test_step_user_already_configured(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "single_instance_allowed" + + +async def test_options_flow_init(hass: HomeAssistant) -> None: + """Test config flow options.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="NINA", + data={ + CONF_FILTER_CORONA: DUMMY_DATA[CONF_FILTER_CORONA], + CONF_MESSAGE_SLOTS: DUMMY_DATA[CONF_MESSAGE_SLOTS], + CONST_REGION_A_TO_D: DUMMY_DATA[CONST_REGION_A_TO_D], + CONF_REGIONS: {"095760000000": "Aach"}, + }, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.nina.async_setup_entry", return_value=True + ), patch( + "pynina.baseApi.BaseAPI._makeRequest", + wraps=mocked_request_function, + ): + 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_REGION_A_TO_D: ["072350000000_1"], + CONST_REGION_E_TO_H: [], + CONST_REGION_I_TO_L: [], + CONST_REGION_M_TO_Q: [], + CONST_REGION_R_TO_U: [], + CONST_REGION_V_TO_Z: [], + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] is None + + assert dict(config_entry.data) == { + CONF_FILTER_CORONA: DUMMY_DATA[CONF_FILTER_CORONA], + CONF_MESSAGE_SLOTS: DUMMY_DATA[CONF_MESSAGE_SLOTS], + CONST_REGION_A_TO_D: ["072350000000_1"], + CONST_REGION_E_TO_H: [], + CONST_REGION_I_TO_L: [], + CONST_REGION_M_TO_Q: [], + CONST_REGION_R_TO_U: [], + CONST_REGION_V_TO_Z: [], + CONF_REGIONS: { + "072350000000": "Damflos (Trier-Saarburg - Rheinland-Pfalz)" + }, + } + + +async def test_options_flow_with_no_selection(hass: HomeAssistant) -> None: + """Test config flow options with no selection.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="NINA", + data=DUMMY_DATA, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.nina.async_setup_entry", return_value=True + ), patch( + "pynina.baseApi.BaseAPI._makeRequest", + wraps=mocked_request_function, + ): + + 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_REGION_A_TO_D: [], + CONST_REGION_E_TO_H: [], + CONST_REGION_I_TO_L: [], + CONST_REGION_M_TO_Q: [], + CONST_REGION_R_TO_U: [], + CONST_REGION_V_TO_Z: [], + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + assert result["errors"] == {"base": "no_selection"} + + +async def test_options_flow_connection_error(hass: HomeAssistant) -> None: + """Test config flow options but no connection.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="NINA", + data=DUMMY_DATA, + ) + config_entry.add_to_hass(hass) + + with patch( + "pynina.baseApi.BaseAPI._makeRequest", + side_effect=ApiError("Could not connect to Api"), + ): + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_options_flow_unexpected_exception(hass: HomeAssistant) -> None: + """Test config flow options but with an unexpected exception.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="NINA", + data=DUMMY_DATA, + ) + config_entry.add_to_hass(hass) + + with patch( + "pynina.baseApi.BaseAPI._makeRequest", + side_effect=Exception("DUMMY"), + ): + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +async def test_options_flow_entity_removal(hass: HomeAssistant) -> None: + """Test if old entities are removed.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="NINA", + data=DUMMY_DATA, + ) + config_entry.add_to_hass(hass) + + with patch( + "pynina.baseApi.BaseAPI._makeRequest", + wraps=mocked_request_function, + ): + 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) + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_MESSAGE_SLOTS: 2, + CONST_REGION_A_TO_D: ["072350000000", "095760000000"], + CONST_REGION_E_TO_H: [], + CONST_REGION_I_TO_L: [], + CONST_REGION_M_TO_Q: [], + CONST_REGION_R_TO_U: [], + CONST_REGION_V_TO_Z: [], + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + entity_registry: er = er.async_get(hass) + entries = er.async_entries_for_config_entry( + entity_registry, config_entry.entry_id + ) + + assert len(entries) == 2 From 6a0ca2b36d247cdc7656e90b559c5b91d2f6a83b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 29 Jun 2022 10:04:22 +0200 Subject: [PATCH 1926/3516] Migrate kostal_plenticore number to native_* (#74159) --- .../components/kostal_plenticore/const.py | 16 ++++++++-------- .../components/kostal_plenticore/number.py | 8 ++++---- .../components/kostal_plenticore/test_number.py | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py index ba850ed58bd..11bb794f799 100644 --- a/homeassistant/components/kostal_plenticore/const.py +++ b/homeassistant/components/kostal_plenticore/const.py @@ -818,10 +818,10 @@ NUMBER_SETTINGS_DATA = [ entity_registry_enabled_default=False, icon="mdi:battery-negative", name="Battery min SoC", - unit_of_measurement=PERCENTAGE, - max_value=100, - min_value=5, - step=5, + native_unit_of_measurement=PERCENTAGE, + native_max_value=100, + native_min_value=5, + native_step=5, module_id="devices:local", data_id="Battery:MinSoc", fmt_from="format_round", @@ -833,10 +833,10 @@ NUMBER_SETTINGS_DATA = [ entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, name="Battery min Home Consumption", - unit_of_measurement=POWER_WATT, - max_value=38000, - min_value=50, - step=1, + native_unit_of_measurement=POWER_WATT, + native_max_value=38000, + native_min_value=50, + native_step=1, module_id="devices:local", data_id="Battery:MinHomeComsumption", fmt_from="format_round", diff --git a/homeassistant/components/kostal_plenticore/number.py b/homeassistant/components/kostal_plenticore/number.py index 33d431d6396..1ad911f6d15 100644 --- a/homeassistant/components/kostal_plenticore/number.py +++ b/homeassistant/components/kostal_plenticore/number.py @@ -105,9 +105,9 @@ class PlenticoreDataNumber(CoordinatorEntity, NumberEntity, ABC): # overwrite from retrieved setting data if setting_data.min is not None: - self._attr_min_value = self._formatter(setting_data.min) + self._attr_native_min_value = self._formatter(setting_data.min) if setting_data.max is not None: - self._attr_max_value = self._formatter(setting_data.max) + self._attr_native_max_value = self._formatter(setting_data.max) @property def module_id(self) -> str: @@ -140,7 +140,7 @@ class PlenticoreDataNumber(CoordinatorEntity, NumberEntity, ABC): await super().async_will_remove_from_hass() @property - def value(self) -> float | None: + def native_value(self) -> float | None: """Return the current value.""" if self.available: raw_value = self.coordinator.data[self.module_id][self.data_id] @@ -148,7 +148,7 @@ class PlenticoreDataNumber(CoordinatorEntity, NumberEntity, ABC): return None - async def async_set_value(self, value: float) -> None: + async def async_set_native_value(self, value: float) -> None: """Set a new value.""" str_value = self._formatter_back(value) await self.coordinator.async_write_data( diff --git a/tests/components/kostal_plenticore/test_number.py b/tests/components/kostal_plenticore/test_number.py index 43d693642d9..f0fb42f6e78 100644 --- a/tests/components/kostal_plenticore/test_number.py +++ b/tests/components/kostal_plenticore/test_number.py @@ -31,8 +31,8 @@ def mock_number_description() -> PlenticoreNumberEntityDescription: key="mock key", module_id="moduleid", data_id="dataid", - min_value=0, - max_value=1000, + native_min_value=0, + native_max_value=1000, fmt_from="format_round", fmt_to="format_round_back", ) @@ -136,7 +136,7 @@ async def test_set_value( mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data ) - await entity.async_set_value(42) + await entity.async_set_native_value(42) mock_coordinator.async_write_data.assert_called_once_with( "moduleid", {"dataid": "42"} From 99329ef04f8c0276ee247e03fe7d92de366f4b94 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Jun 2022 03:13:10 -0500 Subject: [PATCH 1927/3516] Wait for discovery to complete before starting apple_tv (#74133) --- homeassistant/components/apple_tv/__init__.py | 105 +++++++++++------- 1 file changed, 63 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index d61c21972fb..45250451f37 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -23,6 +23,7 @@ from homeassistant.const import ( Platform, ) 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.dispatcher import ( @@ -49,6 +50,13 @@ PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry for Apple TV.""" manager = AppleTVManager(hass, entry) + + if manager.is_on: + await manager.connect_once(raise_missing_credentials=True) + if not manager.atv: + address = entry.data[CONF_ADDRESS] + raise ConfigEntryNotReady(f"Not found at {address}, waiting for discovery") + hass.data.setdefault(DOMAIN, {})[entry.unique_id] = manager async def on_hass_stop(event): @@ -148,14 +156,14 @@ class AppleTVManager: self.config_entry = config_entry self.hass = hass self.atv = None - self._is_on = not config_entry.options.get(CONF_START_OFF, False) + self.is_on = not config_entry.options.get(CONF_START_OFF, False) self._connection_attempts = 0 self._connection_was_lost = False self._task = None async def init(self): """Initialize power management.""" - if self._is_on: + if self.is_on: await self.connect() def connection_lost(self, _): @@ -186,13 +194,13 @@ class AppleTVManager: async def connect(self): """Connect to device.""" - self._is_on = True + self.is_on = True self._start_connect_loop() async def disconnect(self): """Disconnect from device.""" _LOGGER.debug("Disconnecting from device") - self._is_on = False + self.is_on = False try: if self.atv: self.atv.close() @@ -205,50 +213,53 @@ class AppleTVManager: def _start_connect_loop(self): """Start background connect loop to device.""" - if not self._task and self.atv is None and self._is_on: + if not self._task and self.atv is None and self.is_on: self._task = asyncio.create_task(self._connect_loop()) else: _LOGGER.debug( - "Not starting connect loop (%s, %s)", self.atv is None, self._is_on + "Not starting connect loop (%s, %s)", self.atv is None, self.is_on ) + async def connect_once(self, raise_missing_credentials): + """Try to connect once.""" + try: + if conf := await self._scan(): + await self._connect(conf, raise_missing_credentials) + except exceptions.AuthenticationError: + self.config_entry.async_start_reauth(self.hass) + asyncio.create_task(self.disconnect()) + _LOGGER.exception( + "Authentication failed for %s, try reconfiguring device", + self.config_entry.data[CONF_NAME], + ) + return + except asyncio.CancelledError: + pass + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Failed to connect") + self.atv = None + async def _connect_loop(self): """Connect loop background task function.""" _LOGGER.debug("Starting connect loop") # Try to find device and connect as long as the user has said that # we are allowed to connect and we are not already connected. - while self._is_on and self.atv is None: - try: - conf = await self._scan() - if conf: - await self._connect(conf) - except exceptions.AuthenticationError: - self.config_entry.async_start_reauth(self.hass) - asyncio.create_task(self.disconnect()) - _LOGGER.exception( - "Authentication failed for %s, try reconfiguring device", - self.config_entry.data[CONF_NAME], - ) + while self.is_on and self.atv is None: + await self.connect_once(raise_missing_credentials=False) + if self.atv is not None: break - except asyncio.CancelledError: - pass - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Failed to connect") - self.atv = None + self._connection_attempts += 1 + backoff = min( + max( + BACKOFF_TIME_LOWER_LIMIT, + randrange(2**self._connection_attempts), + ), + BACKOFF_TIME_UPPER_LIMIT, + ) - if self.atv is None: - self._connection_attempts += 1 - backoff = min( - max( - BACKOFF_TIME_LOWER_LIMIT, - randrange(2**self._connection_attempts), - ), - BACKOFF_TIME_UPPER_LIMIT, - ) - - _LOGGER.debug("Reconnecting in %d seconds", backoff) - await asyncio.sleep(backoff) + _LOGGER.debug("Reconnecting in %d seconds", backoff) + await asyncio.sleep(backoff) _LOGGER.debug("Connect loop ended") self._task = None @@ -287,23 +298,33 @@ class AppleTVManager: # it will update the address and reload the config entry when the device is found. return None - async def _connect(self, conf): + async def _connect(self, conf, raise_missing_credentials): """Connect to device.""" credentials = self.config_entry.data[CONF_CREDENTIALS] - session = async_get_clientsession(self.hass) - + name = self.config_entry.data[CONF_NAME] + missing_protocols = [] for protocol_int, creds in credentials.items(): protocol = Protocol(int(protocol_int)) if conf.get_service(protocol) is not None: conf.set_credentials(protocol, creds) else: - _LOGGER.warning( - "Protocol %s not found for %s, functionality will be reduced", - protocol.name, - self.config_entry.data[CONF_NAME], + missing_protocols.append(protocol.name) + + if missing_protocols: + missing_protocols_str = ", ".join(missing_protocols) + if raise_missing_credentials: + raise ConfigEntryNotReady( + f"Protocol(s) {missing_protocols_str} not yet found for {name}, waiting for discovery." ) + _LOGGER.info( + "Protocol(s) %s not yet found for %s, trying later", + missing_protocols_str, + name, + ) + return _LOGGER.debug("Connecting to device %s", self.config_entry.data[CONF_NAME]) + session = async_get_clientsession(self.hass) self.atv = await connect(conf, self.hass.loop, session=session) self.atv.listener = self From f45afe737979726b2b85c5f66dccd02a33c570a4 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 29 Jun 2022 16:15:22 +0800 Subject: [PATCH 1928/3516] Use bitstream filter to allow ADTS AAC audio in stream (#74151) --- .../components/generic/manifest.json | 2 +- homeassistant/components/stream/manifest.json | 2 +- homeassistant/components/stream/worker.py | 52 ++++++++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/stream/test_worker.py | 19 ------- 6 files changed, 36 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index 3e8e7717a10..5ef47f0c941 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -2,7 +2,7 @@ "domain": "generic", "name": "Generic Camera", "config_flow": true, - "requirements": ["ha-av==10.0.0b3", "pillow==9.1.1"], + "requirements": ["ha-av==10.0.0b4", "pillow==9.1.1"], "documentation": "https://www.home-assistant.io/integrations/generic", "codeowners": ["@davet2001"], "iot_class": "local_push" diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index eb525700bb0..e9411e53224 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -2,7 +2,7 @@ "domain": "stream", "name": "Stream", "documentation": "https://www.home-assistant.io/integrations/stream", - "requirements": ["PyTurboJPEG==1.6.6", "ha-av==10.0.0b3"], + "requirements": ["PyTurboJPEG==1.6.6", "ha-av==10.0.0b4"], "dependencies": ["http"], "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"], "quality_scale": "internal", diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 1d29bd17c33..e46d83542f7 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -108,6 +108,7 @@ class StreamMuxer: hass: HomeAssistant, video_stream: av.video.VideoStream, audio_stream: av.audio.stream.AudioStream | None, + audio_bsf: av.BitStreamFilterContext | None, stream_state: StreamState, stream_settings: StreamSettings, ) -> None: @@ -118,6 +119,7 @@ class StreamMuxer: self._av_output: av.container.OutputContainer = None self._input_video_stream: av.video.VideoStream = video_stream self._input_audio_stream: av.audio.stream.AudioStream | None = audio_stream + self._audio_bsf = audio_bsf self._output_video_stream: av.video.VideoStream = None self._output_audio_stream: av.audio.stream.AudioStream | None = None self._segment: Segment | None = None @@ -192,7 +194,9 @@ class StreamMuxer: # Check if audio is requested output_astream = None if input_astream: - output_astream = container.add_stream(template=input_astream) + output_astream = container.add_stream( + template=self._audio_bsf or input_astream + ) return container, output_vstream, output_astream def reset(self, video_dts: int) -> None: @@ -234,6 +238,12 @@ class StreamMuxer: self._part_has_keyframe |= packet.is_keyframe elif packet.stream == self._input_audio_stream: + if self._audio_bsf: + self._audio_bsf.send(packet) + while packet := self._audio_bsf.recv(): + packet.stream = self._output_audio_stream + self._av_output.mux(packet) + return packet.stream = self._output_audio_stream self._av_output.mux(packet) @@ -355,12 +365,6 @@ class PeekIterator(Iterator): """Return and consume the next item available.""" return self._next() - def replace_underlying_iterator(self, new_iterator: Iterator) -> None: - """Replace the underlying iterator while preserving the buffer.""" - self._iterator = new_iterator - if not self._buffer: - self._next = self._iterator.__next__ - def _pop_buffer(self) -> av.Packet: """Consume items from the buffer until exhausted.""" if self._buffer: @@ -422,10 +426,12 @@ def is_keyframe(packet: av.Packet) -> Any: return packet.is_keyframe -def unsupported_audio(packets: Iterator[av.Packet], audio_stream: Any) -> bool: - """Detect ADTS AAC, which is not supported by pyav.""" +def get_audio_bitstream_filter( + packets: Iterator[av.Packet], audio_stream: Any +) -> av.BitStreamFilterContext | None: + """Return the aac_adtstoasc bitstream filter if ADTS AAC is detected.""" if not audio_stream: - return False + return None for count, packet in enumerate(packets): if count >= PACKETS_TO_WAIT_FOR_AUDIO: # Some streams declare an audio stream and never send any packets @@ -436,10 +442,15 @@ def unsupported_audio(packets: Iterator[av.Packet], audio_stream: Any) -> bool: if audio_stream.codec.name == "aac" and packet.size > 2: with memoryview(packet) as packet_view: if packet_view[0] == 0xFF and packet_view[1] & 0xF0 == 0xF0: - _LOGGER.warning("ADTS AAC detected - disabling audio stream") - return True + _LOGGER.debug( + "ADTS AAC detected. Adding aac_adtstoaac bitstream filter" + ) + bsf = av.BitStreamFilter("aac_adtstoasc") + bsf_context = bsf.create() + bsf_context.set_input_stream(audio_stream) + return bsf_context break - return False + return None def stream_worker( @@ -500,12 +511,8 @@ def stream_worker( # Use a peeking iterator to peek into the start of the stream, ensuring # everything looks good, then go back to the start when muxing below. try: - if audio_stream and unsupported_audio(container_packets.peek(), audio_stream): - audio_stream = None - container_packets.replace_underlying_iterator( - filter(dts_validator.is_valid, container.demux(video_stream)) - ) - + # Get the required bitstream filter + audio_bsf = get_audio_bitstream_filter(container_packets.peek(), audio_stream) # Advance to the first keyframe for muxing, then rewind so the muxing # loop below can consume. first_keyframe = next( @@ -535,7 +542,12 @@ def stream_worker( ) from ex muxer = StreamMuxer( - stream_state.hass, video_stream, audio_stream, stream_state, stream_settings + stream_state.hass, + video_stream, + audio_stream, + audio_bsf, + stream_state, + stream_settings, ) muxer.reset(start_dts) diff --git a/requirements_all.txt b/requirements_all.txt index 8adf795c977..11ed4a2113a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -780,7 +780,7 @@ guppy3==3.1.2 # homeassistant.components.generic # homeassistant.components.stream -ha-av==10.0.0b3 +ha-av==10.0.0b4 # homeassistant.components.ffmpeg ha-ffmpeg==3.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9675da97f7..dc7366f151b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -559,7 +559,7 @@ guppy3==3.1.2 # homeassistant.components.generic # homeassistant.components.stream -ha-av==10.0.0b3 +ha-av==10.0.0b4 # homeassistant.components.ffmpeg ha-ffmpeg==3.0.2 diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 863a289c2c5..8717e23a476 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -552,25 +552,6 @@ async def test_audio_packets_not_found(hass): assert len(decoded_stream.audio_packets) == 0 -async def test_adts_aac_audio(hass): - """Set up an ADTS AAC audio stream and disable audio.""" - py_av = MockPyAv(audio=True) - - num_packets = PACKETS_TO_WAIT_FOR_AUDIO + 1 - packets = list(PacketSequence(num_packets)) - packets[1].stream = AUDIO_STREAM - packets[1].dts = int(packets[0].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) - packets[1].pts = int(packets[0].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) - # The following is packet data is a sign of ADTS AAC - packets[1][0] = 255 - packets[1][1] = 241 - - decoded_stream = await async_decode_stream(hass, packets, py_av=py_av) - assert len(decoded_stream.audio_packets) == 0 - # All decoded video packets are still preserved - assert len(decoded_stream.video_packets) == num_packets - 1 - - async def test_audio_is_first_packet(hass): """Set up an audio stream and audio packet is the first packet in the stream.""" py_av = MockPyAv(audio=True) From 0769b33e19ac81a875b16947d0485f7d24c71691 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 29 Jun 2022 10:16:23 +0200 Subject: [PATCH 1929/3516] Migrate darksky to native_* (#74047) --- homeassistant/components/darksky/weather.py | 86 +++++++++------------ 1 file changed, 38 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index 4965505150a..1bc56706007 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -21,12 +21,12 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_CONDITION_WINDY, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, PLATFORM_SCHEMA, WeatherEntity, ) @@ -36,10 +36,11 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_MODE, CONF_NAME, - PRESSURE_HPA, - PRESSURE_INHG, + LENGTH_KILOMETERS, + LENGTH_MILLIMETERS, + PRESSURE_MBAR, + SPEED_METERS_PER_SECOND, TEMP_CELSIUS, - TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -47,7 +48,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle from homeassistant.util.dt import utc_from_timestamp -from homeassistant.util.pressure import convert as convert_pressure _LOGGER = logging.getLogger(__name__) @@ -75,15 +75,18 @@ CONF_UNITS = "units" DEFAULT_NAME = "Dark Sky" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_MODE, default="hourly"): vol.In(FORECAST_MODE), - vol.Optional(CONF_UNITS): vol.In(["auto", "si", "us", "ca", "uk", "uk2"]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } +PLATFORM_SCHEMA = vol.All( + cv.removed(CONF_UNITS), + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_MODE, default="hourly"): vol.In(FORECAST_MODE), + vol.Optional(CONF_UNITS): vol.In(["auto", "si", "us", "ca", "uk", "uk2"]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } + ), ) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=3) @@ -101,9 +104,7 @@ def setup_platform( name = config.get(CONF_NAME) mode = config.get(CONF_MODE) - if not (units := config.get(CONF_UNITS)): - units = "ca" if hass.config.units.is_metric else "us" - + units = "si" dark_sky = DarkSkyData(config.get(CONF_API_KEY), latitude, longitude, units) add_entities([DarkSkyWeather(name, dark_sky, mode)], True) @@ -112,6 +113,12 @@ def setup_platform( class DarkSkyWeather(WeatherEntity): """Representation of a weather condition.""" + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_MBAR + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_visibility_unit = LENGTH_KILOMETERS + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + def __init__(self, name, dark_sky, mode): """Initialize Dark Sky weather.""" self._name = name @@ -139,24 +146,17 @@ class DarkSkyWeather(WeatherEntity): return self._name @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self._ds_currently.get("temperature") - @property - def temperature_unit(self): - """Return the unit of measurement.""" - if self._dark_sky.units is None: - return None - return TEMP_FAHRENHEIT if "us" in self._dark_sky.units else TEMP_CELSIUS - @property def humidity(self): """Return the humidity.""" return round(self._ds_currently.get("humidity") * 100.0, 2) @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" return self._ds_currently.get("windSpeed") @@ -171,15 +171,12 @@ class DarkSkyWeather(WeatherEntity): return self._ds_currently.get("ozone") @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" - pressure = self._ds_currently.get("pressure") - if "us" in self._dark_sky.units: - return round(convert_pressure(pressure, PRESSURE_HPA, PRESSURE_INHG), 2) - return pressure + return self._ds_currently.get("pressure") @property - def visibility(self): + def native_visibility(self): """Return the visibility.""" return self._ds_currently.get("visibility") @@ -208,12 +205,12 @@ class DarkSkyWeather(WeatherEntity): ATTR_FORECAST_TIME: utc_from_timestamp( entry.d.get("time") ).isoformat(), - ATTR_FORECAST_TEMP: entry.d.get("temperatureHigh"), - ATTR_FORECAST_TEMP_LOW: entry.d.get("temperatureLow"), - ATTR_FORECAST_PRECIPITATION: calc_precipitation( + ATTR_FORECAST_NATIVE_TEMP: entry.d.get("temperatureHigh"), + ATTR_FORECAST_NATIVE_TEMP_LOW: entry.d.get("temperatureLow"), + ATTR_FORECAST_NATIVE_PRECIPITATION: calc_precipitation( entry.d.get("precipIntensity"), 24 ), - ATTR_FORECAST_WIND_SPEED: entry.d.get("windSpeed"), + ATTR_FORECAST_NATIVE_WIND_SPEED: entry.d.get("windSpeed"), ATTR_FORECAST_WIND_BEARING: entry.d.get("windBearing"), ATTR_FORECAST_CONDITION: MAP_CONDITION.get(entry.d.get("icon")), } @@ -225,8 +222,8 @@ class DarkSkyWeather(WeatherEntity): ATTR_FORECAST_TIME: utc_from_timestamp( entry.d.get("time") ).isoformat(), - ATTR_FORECAST_TEMP: entry.d.get("temperature"), - ATTR_FORECAST_PRECIPITATION: calc_precipitation( + ATTR_FORECAST_NATIVE_TEMP: entry.d.get("temperature"), + ATTR_FORECAST_NATIVE_PRECIPITATION: calc_precipitation( entry.d.get("precipIntensity"), 1 ), ATTR_FORECAST_CONDITION: MAP_CONDITION.get(entry.d.get("icon")), @@ -281,10 +278,3 @@ class DarkSkyData: self._connect_error = True _LOGGER.error("Unable to connect to Dark Sky. %s", error) self.data = None - - @property - def units(self): - """Get the unit system of returned data.""" - if self.data is None: - return None - return self.data.json.get("flags").get("units") From 6dc6e71f01be1757251f46c5c21abd47ef59bce0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 29 Jun 2022 10:19:39 +0200 Subject: [PATCH 1930/3516] Use attributes in manual alarm (#74122) --- .../components/manual/alarm_control_panel.py | 60 +++++++------------ 1 file changed, 22 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/manual/alarm_control_panel.py b/homeassistant/components/manual/alarm_control_panel.py index cfa81a816c3..9a5d84f5997 100644 --- a/homeassistant/components/manual/alarm_control_panel.py +++ b/homeassistant/components/manual/alarm_control_panel.py @@ -5,6 +5,7 @@ import copy import datetime import logging import re +from typing import Any import voluptuous as vol @@ -185,6 +186,16 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): A trigger_time of zero disables the alarm_trigger service. """ + _attr_should_poll = False + _attr_supported_features = ( + AlarmControlPanelEntityFeature.ARM_HOME + | AlarmControlPanelEntityFeature.ARM_AWAY + | AlarmControlPanelEntityFeature.ARM_NIGHT + | AlarmControlPanelEntityFeature.ARM_VACATION + | AlarmControlPanelEntityFeature.TRIGGER + | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS + ) + def __init__( self, hass, @@ -198,13 +209,13 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): """Init the manual alarm panel.""" self._state = STATE_ALARM_DISARMED self._hass = hass - self._name = name + self._attr_name = name if code_template: self._code = code_template self._code.hass = hass else: self._code = code or None - self._code_arm_required = code_arm_required + self._attr_code_arm_required = code_arm_required self._disarm_after_trigger = disarm_after_trigger self._previous_state = self._state self._state_ts = None @@ -222,17 +233,7 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): } @property - def should_poll(self): - """Return the polling state.""" - return False - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): + def state(self) -> str: """Return the state of the device.""" if self._state == STATE_ALARM_TRIGGERED: if self._within_pending_time(self._state): @@ -253,18 +254,6 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): return self._state - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return ( - AlarmControlPanelEntityFeature.ARM_HOME - | AlarmControlPanelEntityFeature.ARM_AWAY - | AlarmControlPanelEntityFeature.ARM_NIGHT - | AlarmControlPanelEntityFeature.ARM_VACATION - | AlarmControlPanelEntityFeature.TRIGGER - | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS - ) - @property def _active_state(self): """Get the current state.""" @@ -289,7 +278,7 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): return self._state_ts + self._pending_time(state) > dt_util.utcnow() @property - def code_format(self): + def code_format(self) -> alarm.CodeFormat | None: """Return one or more digits/characters.""" if self._code is None: return None @@ -297,11 +286,6 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): return alarm.CodeFormat.NUMBER return alarm.CodeFormat.TEXT - @property - def code_arm_required(self): - """Whether the code is required for arm actions.""" - return self._code_arm_required - def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if not self._validate_code(code, STATE_ALARM_DISARMED): @@ -313,7 +297,7 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): def alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" - if self._code_arm_required and not self._validate_code( + if self.code_arm_required and not self._validate_code( code, STATE_ALARM_ARMED_HOME ): return @@ -322,7 +306,7 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): def alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" - if self._code_arm_required and not self._validate_code( + if self.code_arm_required and not self._validate_code( code, STATE_ALARM_ARMED_AWAY ): return @@ -331,7 +315,7 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): def alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" - if self._code_arm_required and not self._validate_code( + if self.code_arm_required and not self._validate_code( code, STATE_ALARM_ARMED_NIGHT ): return @@ -340,7 +324,7 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): def alarm_arm_vacation(self, code: str | None = None) -> None: """Send arm vacation command.""" - if self._code_arm_required and not self._validate_code( + if self.code_arm_required and not self._validate_code( code, STATE_ALARM_ARMED_VACATION ): return @@ -349,7 +333,7 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): def alarm_arm_custom_bypass(self, code: str | None = None) -> None: """Send arm custom bypass command.""" - if self._code_arm_required and not self._validate_code( + if self.code_arm_required and not self._validate_code( code, STATE_ALARM_ARMED_CUSTOM_BYPASS ): return @@ -367,7 +351,7 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): return self._update_state(STATE_ALARM_TRIGGERED) - def _update_state(self, state): + def _update_state(self, state: str) -> None: """Update the state.""" if self._state == state: return @@ -414,7 +398,7 @@ class ManualAlarm(alarm.AlarmControlPanelEntity, RestoreEntity): return check @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" if self.state in (STATE_ALARM_PENDING, STATE_ALARM_ARMING): return { From edc1ee2985e855f630f45d7bb00ddaa3e28d50c7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 29 Jun 2022 10:21:58 +0200 Subject: [PATCH 1931/3516] Add type hints to async_step_reauth in components (#74138) --- homeassistant/components/bosch_shc/config_flow.py | 4 +++- homeassistant/components/enphase_envoy/config_flow.py | 3 ++- homeassistant/components/flume/config_flow.py | 5 ++++- homeassistant/components/intellifire/config_flow.py | 5 ++++- homeassistant/components/lyric/config_flow.py | 5 ++++- homeassistant/components/myq/config_flow.py | 5 ++++- homeassistant/components/picnic/config_flow.py | 5 ++++- homeassistant/components/prosegur/config_flow.py | 10 ++++++++-- homeassistant/components/vulcan/config_flow.py | 5 ++++- 9 files changed, 37 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/bosch_shc/config_flow.py b/homeassistant/components/bosch_shc/config_flow.py index 6ad1a374a5a..7cc4527a64f 100644 --- a/homeassistant/components/bosch_shc/config_flow.py +++ b/homeassistant/components/bosch_shc/config_flow.py @@ -1,6 +1,8 @@ """Config flow for Bosch Smart Home Controller integration.""" +from collections.abc import Mapping import logging from os import makedirs +from typing import Any from boschshcpy import SHCRegisterClient, SHCSession from boschshcpy.exceptions import ( @@ -83,7 +85,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): host = None hostname = None - async def async_step_reauth(self, user_input=None): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/enphase_envoy/config_flow.py b/homeassistant/components/enphase_envoy/config_flow.py index 88310579e72..3707733b1af 100644 --- a/homeassistant/components/enphase_envoy/config_flow.py +++ b/homeassistant/components/enphase_envoy/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Enphase Envoy integration.""" from __future__ import annotations +from collections.abc import Mapping import contextlib import logging from typing import Any @@ -110,7 +111,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() - async def async_step_reauth(self, user_input): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] diff --git a/homeassistant/components/flume/config_flow.py b/homeassistant/components/flume/config_flow.py index 6d9554f42c0..049b702dc3c 100644 --- a/homeassistant/components/flume/config_flow.py +++ b/homeassistant/components/flume/config_flow.py @@ -1,6 +1,8 @@ """Config flow for flume integration.""" +from collections.abc import Mapping import logging import os +from typing import Any from pyflume import FlumeAuth, FlumeDeviceList from requests.exceptions import RequestException @@ -13,6 +15,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, ) +from homeassistant.data_entry_flow import FlowResult from .const import BASE_TOKEN_FILENAME, DOMAIN @@ -103,7 +106,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, user_input=None): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle reauth.""" self._reauth_unique_id = self.context["unique_id"] return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/intellifire/config_flow.py b/homeassistant/components/intellifire/config_flow.py index 6066d703729..df13795a4ed 100644 --- a/homeassistant/components/intellifire/config_flow.py +++ b/homeassistant/components/intellifire/config_flow.py @@ -1,6 +1,7 @@ """Config flow for IntelliFire integration.""" from __future__ import annotations +from collections.abc import Mapping from dataclasses import dataclass from typing import Any @@ -220,10 +221,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.debug("Running Step: manual_device_entry") return await self.async_step_manual_device_entry() - async def async_step_reauth(self, user_input=None): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" LOGGER.debug("STEP: reauth") entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert entry + assert entry.unique_id # populate the expected vars self._serial = entry.unique_id diff --git a/homeassistant/components/lyric/config_flow.py b/homeassistant/components/lyric/config_flow.py index 698e7e19a26..12f91cfe206 100644 --- a/homeassistant/components/lyric/config_flow.py +++ b/homeassistant/components/lyric/config_flow.py @@ -1,6 +1,9 @@ """Config flow for Honeywell Lyric.""" +from collections.abc import Mapping import logging +from typing import Any +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN @@ -18,7 +21,7 @@ class OAuth2FlowHandler( """Return logger.""" return logging.getLogger(__name__) - async def async_step_reauth(self, user_input=None): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/myq/config_flow.py b/homeassistant/components/myq/config_flow.py index 8c088de6715..c26b54d7332 100644 --- a/homeassistant/components/myq/config_flow.py +++ b/homeassistant/components/myq/config_flow.py @@ -1,5 +1,7 @@ """Config flow for MyQ integration.""" +from collections.abc import Mapping import logging +from typing import Any import pymyq from pymyq.errors import InvalidCredentialsError, MyQError @@ -7,6 +9,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import DOMAIN @@ -60,7 +63,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, user_input=None): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle reauth.""" self._reauth_unique_id = self.context["unique_id"] return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/picnic/config_flow.py b/homeassistant/components/picnic/config_flow.py index c2d48ca9415..904b68e3d32 100644 --- a/homeassistant/components/picnic/config_flow.py +++ b/homeassistant/components/picnic/config_flow.py @@ -1,7 +1,9 @@ """Config flow for Picnic integration.""" from __future__ import annotations +from collections.abc import Mapping import logging +from typing import Any from python_picnic_api import PicnicAPI from python_picnic_api.session import PicnicAuthError @@ -11,6 +13,7 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from .const import CONF_COUNTRY_CODE, COUNTRY_CODES, DOMAIN @@ -72,7 +75,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_reauth(self, _): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform the re-auth step upon an API authentication error.""" return await self.async_step_user() diff --git a/homeassistant/components/prosegur/config_flow.py b/homeassistant/components/prosegur/config_flow.py index 1807561663b..ee2fa795f2d 100644 --- a/homeassistant/components/prosegur/config_flow.py +++ b/homeassistant/components/prosegur/config_flow.py @@ -1,5 +1,7 @@ """Config flow for Prosegur Alarm integration.""" +from collections.abc import Mapping import logging +from typing import Any, cast from pyprosegur.auth import COUNTRY, Auth from pyprosegur.installation import Installation @@ -8,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import CONF_COUNTRY, DOMAIN @@ -75,9 +78,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, data): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle initiation of re-authentication with Prosegur.""" - self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + self.entry = cast( + ConfigEntry, + self.hass.config_entries.async_get_entry(self.context["entry_id"]), + ) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm(self, user_input=None): diff --git a/homeassistant/components/vulcan/config_flow.py b/homeassistant/components/vulcan/config_flow.py index 09acb13ea27..f1e1c13871c 100644 --- a/homeassistant/components/vulcan/config_flow.py +++ b/homeassistant/components/vulcan/config_flow.py @@ -1,5 +1,7 @@ """Adds config flow for Vulcan.""" +from collections.abc import Mapping import logging +from typing import Any from aiohttp import ClientConnectionError import voluptuous as vol @@ -16,6 +18,7 @@ from vulcan import ( from homeassistant import config_entries from homeassistant.const import CONF_PIN, CONF_REGION, CONF_TOKEN +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from . import DOMAIN @@ -236,7 +239,7 @@ class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, user_input=None): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() From 808798219352e1cd184b63c061ac31f3672005ee Mon Sep 17 00:00:00 2001 From: Lorenzo Milesi Date: Wed, 29 Jun 2022 10:25:38 +0200 Subject: [PATCH 1932/3516] Update base url for ViaggiaTreno API (#71974) --- homeassistant/components/viaggiatreno/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py index 3fdca6653d0..0e7a047f6da 100644 --- a/homeassistant/components/viaggiatreno/sensor.py +++ b/homeassistant/components/viaggiatreno/sensor.py @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Powered by ViaggiaTreno Data" VIAGGIATRENO_ENDPOINT = ( - "http://www.viaggiatreno.it/viaggiatrenonew/" + "http://www.viaggiatreno.it/infomobilita/" "resteasy/viaggiatreno/andamentoTreno/" "{station_id}/{train_id}/{timestamp}" ) From 0404c76c41bf4701e28cf761caf1c0cd5605dfdc Mon Sep 17 00:00:00 2001 From: alexanv1 <44785744+alexanv1@users.noreply.github.com> Date: Wed, 29 Jun 2022 01:29:19 -0700 Subject: [PATCH 1933/3516] Add Tuya Sous Vide Cooker (#69777) --- homeassistant/components/tuya/const.py | 4 ++++ homeassistant/components/tuya/number.py | 23 +++++++++++++++++++++++ homeassistant/components/tuya/sensor.py | 22 ++++++++++++++++++++++ homeassistant/components/tuya/switch.py | 10 ++++++++++ 4 files changed, 59 insertions(+) diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 5486e94786d..8a3e59b1ac9 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -181,6 +181,7 @@ class DPCode(StrEnum): CLEAN_AREA = "clean_area" CLEAN_TIME = "clean_time" CLICK_SUSTAIN_TIME = "click_sustain_time" + CLOUD_RECIPE_NUMBER = "cloud_recipe_number" CO_STATE = "co_state" CO_STATUS = "co_status" CO_VALUE = "co_value" @@ -191,6 +192,8 @@ class DPCode(StrEnum): COLOUR_DATA = "colour_data" # Colored light mode COLOUR_DATA_HSV = "colour_data_hsv" # Colored light mode COLOUR_DATA_V2 = "colour_data_v2" # Colored light mode + COOK_TEMPERATURE = "cook_temperature" + COOK_TIME = "cook_time" CONCENTRATION_SET = "concentration_set" # Concentration setting CONTROL = "control" CONTROL_2 = "control_2" @@ -295,6 +298,7 @@ class DPCode(StrEnum): RECORD_MODE = "record_mode" RECORD_SWITCH = "record_switch" # Recording switch RELAY_STATUS = "relay_status" + REMAIN_TIME = "remain_time" RESET_DUSTER_CLOTH = "reset_duster_cloth" RESET_EDGE_BRUSH = "reset_edge_brush" RESET_FILTER = "reset_filter" diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index bee5242d4ae..b1afeb0364c 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -9,6 +9,7 @@ from homeassistant.components.number import ( NumberEntityDescription, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TIME_MINUTES from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory @@ -132,6 +133,28 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), + # Sous Vide Cooker + # https://developer.tuya.com/en/docs/iot/categorymzj?id=Kaiuz2vy130ux + "mzj": ( + NumberEntityDescription( + key=DPCode.COOK_TEMPERATURE, + name="Cook Temperature", + icon="mdi:thermometer", + entity_category=EntityCategory.CONFIG, + ), + NumberEntityDescription( + key=DPCode.COOK_TIME, + name="Cook Time", + icon="mdi:timer", + unit_of_measurement=TIME_MINUTES, + entity_category=EntityCategory.CONFIG, + ), + NumberEntityDescription( + key=DPCode.CLOUD_RECIPE_NUMBER, + name="Cloud Recipe", + entity_category=EntityCategory.CONFIG, + ), + ), # Robot Vacuum # https://developer.tuya.com/en/docs/iot/fsd?id=K9gf487ck1tlo "sd": ( diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index acb2ffe7987..dd2996f61ba 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import ( ELECTRIC_POTENTIAL_VOLT, PERCENTAGE, POWER_KILO_WATT, + TIME_MINUTES, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -379,6 +380,27 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { # Door Window Sensor # https://developer.tuya.com/en/docs/iot/s?id=K9gf48hm02l8m "mcs": BATTERY_SENSORS, + # Sous Vide Cooker + # https://developer.tuya.com/en/docs/iot/categorymzj?id=Kaiuz2vy130ux + "mzj": ( + TuyaSensorEntityDescription( + key=DPCode.TEMP_CURRENT, + name="Current Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.STATUS, + name="Status", + device_class=TuyaDeviceClass.STATUS, + ), + TuyaSensorEntityDescription( + key=DPCode.REMAIN_TIME, + name="Remaining Time", + native_unit_of_measurement=TIME_MINUTES, + icon="mdi:timer", + ), + ), # PIR Detector # https://developer.tuya.com/en/docs/iot/categorypir?id=Kaiuz3ss11b80 "pir": BATTERY_SENSORS, diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index d587e8ea54b..37834f0f273 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -269,6 +269,16 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), + # Sous Vide Cooker + # https://developer.tuya.com/en/docs/iot/categorymzj?id=Kaiuz2vy130ux + "mzj": ( + SwitchEntityDescription( + key=DPCode.SWITCH, + name="Switch", + icon="mdi:pot-steam", + entity_category=EntityCategory.CONFIG, + ), + ), # Power Socket # https://developer.tuya.com/en/docs/iot/s?id=K9gf7o5prgf7s "pc": ( From 1590c0a46c885589d2f25e6ee387284dd88ab0c2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 29 Jun 2022 10:42:24 +0200 Subject: [PATCH 1934/3516] Migrate abode light to color_mode (#69070) --- homeassistant/components/abode/light.py | 31 +++++++++++++++++++------ tests/components/abode/test_light.py | 13 +++++++++-- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index f998e21510d..6d1123cd233 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -11,9 +11,10 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_ONOFF, LightEntity, ) from homeassistant.config_entries import ConfigEntry @@ -101,11 +102,27 @@ class AbodeLight(AbodeDevice, LightEntity): _hs = self._device.color return _hs + @property + def color_mode(self) -> str | None: + """Return the color mode of the light.""" + if self._device.is_dimmable and self._device.is_color_capable: + if self.hs_color is not None: + return COLOR_MODE_HS + return COLOR_MODE_COLOR_TEMP + if self._device.is_dimmable: + return COLOR_MODE_BRIGHTNESS + return COLOR_MODE_ONOFF + + @property + def supported_color_modes(self) -> set[str] | None: + """Flag supported color modes.""" + if self._device.is_dimmable and self._device.is_color_capable: + return {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + if self._device.is_dimmable: + return {COLOR_MODE_BRIGHTNESS} + return {COLOR_MODE_ONOFF} + @property def supported_features(self) -> int: """Flag supported features.""" - if self._device.is_dimmable and self._device.is_color_capable: - return SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_COLOR_TEMP - if self._device.is_dimmable: - return SUPPORT_BRIGHTNESS return 0 diff --git a/tests/components/abode/test_light.py b/tests/components/abode/test_light.py index d27a07227d0..3a1adc069e4 100644 --- a/tests/components/abode/test_light.py +++ b/tests/components/abode/test_light.py @@ -4,8 +4,12 @@ from unittest.mock import patch from homeassistant.components.abode import ATTR_DEVICE_ID from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, + ATTR_SUPPORTED_COLOR_MODES, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, DOMAIN as LIGHT_DOMAIN, ) from homeassistant.const import ( @@ -41,13 +45,18 @@ async def test_attributes(hass: HomeAssistant) -> None: assert state.state == STATE_ON assert state.attributes.get(ATTR_BRIGHTNESS) == 204 assert state.attributes.get(ATTR_RGB_COLOR) == (0, 63, 255) - assert state.attributes.get(ATTR_COLOR_TEMP) == 280 + assert state.attributes.get(ATTR_COLOR_TEMP) is None assert state.attributes.get(ATTR_DEVICE_ID) == "ZB:db5b1a" assert not state.attributes.get("battery_low") assert not state.attributes.get("no_response") assert state.attributes.get("device_type") == "RGB Dimmer" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Living Room Lamp" - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 19 + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 0 + assert state.attributes.get(ATTR_COLOR_MODE) == COLOR_MODE_HS + assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [ + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + ] async def test_switch_off(hass: HomeAssistant) -> None: From fbaff21b67913ba829614dc8b6e5c221ac309cf9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 29 Jun 2022 10:43:58 +0200 Subject: [PATCH 1935/3516] Format viaggiatreno/sensor.py (#74161) --- homeassistant/components/viaggiatreno/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/viaggiatreno/sensor.py b/homeassistant/components/viaggiatreno/sensor.py index 0e7a047f6da..95eeb154f9c 100644 --- a/homeassistant/components/viaggiatreno/sensor.py +++ b/homeassistant/components/viaggiatreno/sensor.py @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Powered by ViaggiaTreno Data" VIAGGIATRENO_ENDPOINT = ( - "http://www.viaggiatreno.it/infomobilita/" + "http://www.viaggiatreno.it/infomobilita/" "resteasy/viaggiatreno/andamentoTreno/" "{station_id}/{train_id}/{timestamp}" ) From e64336cb91d1ce97ac82c57e98477acedfcbcf71 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Wed, 29 Jun 2022 09:54:04 +0100 Subject: [PATCH 1936/3516] Allow configuring username and password in generic camera config flow (#73804) * Add ability to use user & pw not in stream url * Increase test coverage to 100% * Increase test coverage * Verify that stream source includes user:pass * Code review: refactor test to use MockConfigEntry * Code review: Improve test docstring * Edit comment; retrigger CI. Co-authored-by: Dave T --- homeassistant/components/generic/camera.py | 9 +++- .../components/generic/config_flow.py | 8 ++++ tests/components/generic/test_camera.py | 44 +++++++++++++------ tests/components/generic/test_config_flow.py | 12 +++++ 4 files changed, 59 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 5f1f9ba9c2c..8b03f0a8ed3 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -7,6 +7,7 @@ from typing import Any import httpx import voluptuous as vol +import yarl from homeassistant.components.camera import ( DEFAULT_CONTENT_TYPE, @@ -146,6 +147,8 @@ class GenericCamera(Camera): self.hass = hass self._attr_unique_id = identifier self._authentication = device_info.get(CONF_AUTHENTICATION) + self._username = device_info.get(CONF_USERNAME) + self._password = device_info.get(CONF_PASSWORD) self._name = device_info.get(CONF_NAME, title) self._still_image_url = device_info.get(CONF_STILL_IMAGE_URL) if ( @@ -223,7 +226,11 @@ class GenericCamera(Camera): return None try: - return self._stream_source.async_render(parse_result=False) + stream_url = self._stream_source.async_render(parse_result=False) + url = yarl.URL(stream_url) + if not url.user and not url.password and self._username and self._password: + url = url.with_user(self._username).with_password(self._password) + return str(url) except TemplateError as err: _LOGGER.error("Error parsing template %s: %s", self._stream_source, err) return None diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index b6abdc5eec8..9096f2ce87e 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -221,6 +221,14 @@ async def async_test_stream( stream_options[CONF_RTSP_TRANSPORT] = rtsp_transport if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True + + url = yarl.URL(stream_source) + if not url.user and not url.password: + username = info.get(CONF_USERNAME) + password = info.get(CONF_PASSWORD) + if username and password: + url = url.with_user(username).with_password(password) + stream_source = str(url) try: stream = create_stream(hass, stream_source, stream_options, "test_stream") hls_provider = stream.add_provider(HLS_PROVIDER) diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index ec0d89eb0eb..f7e1898f735 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -8,12 +8,24 @@ import httpx import pytest import respx -from homeassistant.components.camera import async_get_mjpeg_stream +from homeassistant.components.camera import ( + async_get_mjpeg_stream, + async_get_stream_source, +) +from homeassistant.components.generic.const import ( + CONF_CONTENT_TYPE, + CONF_FRAMERATE, + CONF_LIMIT_REFETCH_TO_URL_CHANGE, + CONF_STILL_IMAGE_URL, + CONF_STREAM_SOURCE, + DOMAIN, +) from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL from homeassistant.setup import async_setup_component -from tests.common import AsyncMock, Mock +from tests.common import AsyncMock, Mock, MockConfigEntry @respx.mock @@ -184,23 +196,29 @@ async def test_stream_source(hass, hass_client, hass_ws_client, fakeimgbytes_png respx.get("http://example.com/0a").respond(stream=fakeimgbytes_png) hass.states.async_set("sensor.temp", "0") - assert await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "generic", - "still_image_url": "http://example.com", - "stream_source": 'http://example.com/{{ states.sensor.temp.state + "a" }}', - "limit_refetch_to_url_change": True, - }, + mock_entry = MockConfigEntry( + title="config_test", + domain=DOMAIN, + data={}, + options={ + CONF_STILL_IMAGE_URL: "http://example.com", + CONF_STREAM_SOURCE: 'http://example.com/{{ states.sensor.temp.state + "a" }}', + CONF_LIMIT_REFETCH_TO_URL_CHANGE: True, + CONF_FRAMERATE: 2, + CONF_CONTENT_TYPE: "image/png", + CONF_VERIFY_SSL: False, + CONF_USERNAME: "barney", + CONF_PASSWORD: "betty", }, ) + mock_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_entry.entry_id) assert await async_setup_component(hass, "stream", {}) await hass.async_block_till_done() hass.states.async_set("sensor.temp", "5") + stream_source = await async_get_stream_source(hass, "camera.config_test") + assert stream_source == "http://barney:betty@example.com/5a" with patch( "homeassistant.components.camera.Stream.endpoint_url", diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 2979513e5c0..f0589301014 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -10,6 +10,7 @@ import respx from homeassistant import config_entries, data_entry_flow from homeassistant.components.camera import async_get_image +from homeassistant.components.generic.config_flow import slug from homeassistant.components.generic.const import ( CONF_CONTENT_TYPE, CONF_FRAMERATE, @@ -517,6 +518,17 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream assert result5["errors"] == {"stream_source": "template_error"} +async def test_slug(hass, caplog): + """ + Test that the slug function generates an error in case of invalid template. + + Other paths in the slug function are already tested by other tests. + """ + result = slug(hass, "http://127.0.0.2/testurl/{{1/0}}") + assert result is None + assert "Syntax error in" in caplog.text + + @respx.mock async def test_options_only_stream(hass, fakeimgbytes_png, mock_create_stream): """Test the options flow without a still_image_url.""" From 9d73f9a2c5bf0848ae7581cdf8263ec214f81fb3 Mon Sep 17 00:00:00 2001 From: mbo18 Date: Wed, 29 Jun 2022 11:02:20 +0200 Subject: [PATCH 1937/3516] Move power and energy attributes to sensors for SmartThings Air conditioner (#72594) Move power and energy attribute to sensor for Air conditioner --- .../components/smartthings/climate.py | 5 ---- .../components/smartthings/sensor.py | 24 +++++++++++++++---- tests/components/smartthings/test_climate.py | 11 --------- tests/components/smartthings/test_sensor.py | 2 ++ 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 9bc287b054c..87d20b4533f 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -106,7 +106,6 @@ def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: Capability.air_conditioner_mode, Capability.demand_response_load_control, Capability.air_conditioner_fan_mode, - Capability.power_consumption_report, Capability.relative_humidity_measurement, Capability.switch, Capability.temperature_measurement, @@ -422,10 +421,6 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity): "drlc_status_level", "drlc_status_start", "drlc_status_override", - "power_consumption_start", - "power_consumption_power", - "power_consumption_energy", - "power_consumption_end", ] state_attributes = {} for attribute in attributes: diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 872921199f0..64869347228 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -553,7 +553,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Add binary sensors for a config entry.""" + """Add sensors for a config entry.""" broker = hass.data[DOMAIN][DATA_BROKERS][config_entry.entry_id] entities: list[SensorEntity] = [] for device in broker.devices.values(): @@ -641,7 +641,7 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): @property def name(self) -> str: - """Return the name of the binary sensor.""" + """Return the name of the sensor.""" return f"{self._device.label} {self._name}" @property @@ -681,7 +681,7 @@ class SmartThingsThreeAxisSensor(SmartThingsEntity, SensorEntity): @property def name(self) -> str: - """Return the name of the binary sensor.""" + """Return the name of the sensor.""" return f"{self._device.label} {THREE_AXIS_NAMES[self._index]}" @property @@ -716,7 +716,7 @@ class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity): @property def name(self) -> str: - """Return the name of the binary sensor.""" + """Return the name of the sensor.""" return f"{self._device.label} {self.report_name}" @property @@ -747,3 +747,19 @@ class SmartThingsPowerConsumptionSensor(SmartThingsEntity, SensorEntity): if self.report_name == "power": return POWER_WATT return ENERGY_KILO_WATT_HOUR + + @property + def extra_state_attributes(self): + """Return specific state attributes.""" + if self.report_name == "power": + attributes = [ + "power_consumption_start", + "power_consumption_end", + ] + state_attributes = {} + for attribute in attributes: + value = getattr(self._device.status, attribute) + if value is not None: + state_attributes[attribute] = value + return state_attributes + return None diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 5c3d61d8b41..825b8259276 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -148,7 +148,6 @@ def air_conditioner_fixture(device_factory): Capability.air_conditioner_mode, Capability.demand_response_load_control, Capability.air_conditioner_fan_mode, - Capability.power_consumption_report, Capability.switch, Capability.temperature_measurement, Capability.thermostat_cooling_setpoint, @@ -177,12 +176,6 @@ def air_conditioner_fixture(device_factory): "high", "turbo", ], - Attribute.power_consumption: { - "start": "2019-02-24T21:03:04Z", - "power": 0, - "energy": 500, - "end": "2019-02-26T02:05:55Z", - }, Attribute.switch: "on", Attribute.cooling_setpoint: 23, }, @@ -320,10 +313,6 @@ async def test_air_conditioner_entity_state(hass, air_conditioner): assert state.attributes["drlc_status_level"] == -1 assert state.attributes["drlc_status_start"] == "1970-01-01T00:00:00Z" assert state.attributes["drlc_status_override"] is False - assert state.attributes["power_consumption_start"] == "2019-02-24T21:03:04Z" - assert state.attributes["power_consumption_power"] == 0 - assert state.attributes["power_consumption_energy"] == 500 - assert state.attributes["power_consumption_end"] == "2019-02-26T02:05:55Z" async def test_set_fan_mode(hass, thermostat, air_conditioner): diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index 98464af24af..a4e89ebe5c7 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -190,6 +190,8 @@ async def test_power_consumption_sensor(hass, device_factory): state = hass.states.get("sensor.refrigerator_power") assert state assert state.state == "109" + assert state.attributes["power_consumption_start"] == "2021-07-30T16:45:25Z" + assert state.attributes["power_consumption_end"] == "2021-07-30T16:58:33Z" entry = entity_registry.async_get("sensor.refrigerator_power") assert entry assert entry.unique_id == f"{device.device_id}.power_meter" From 1970e36f10362822dba4e9d973d1936f3538adf3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 29 Jun 2022 11:03:53 +0200 Subject: [PATCH 1938/3516] Fix CI (tuya number and abode light) (#74163) * Fix tuya unit_of_measurement * Fix abode ColorMode --- homeassistant/components/abode/light.py | 19 ++++++++----------- homeassistant/components/tuya/number.py | 2 +- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index 6d1123cd233..1bb9d41f461 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -11,10 +11,7 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, - COLOR_MODE_BRIGHTNESS, - COLOR_MODE_COLOR_TEMP, - COLOR_MODE_HS, - COLOR_MODE_ONOFF, + ColorMode, LightEntity, ) from homeassistant.config_entries import ConfigEntry @@ -107,20 +104,20 @@ class AbodeLight(AbodeDevice, LightEntity): """Return the color mode of the light.""" if self._device.is_dimmable and self._device.is_color_capable: if self.hs_color is not None: - return COLOR_MODE_HS - return COLOR_MODE_COLOR_TEMP + return ColorMode.HS + return ColorMode.COLOR_TEMP if self._device.is_dimmable: - return COLOR_MODE_BRIGHTNESS - return COLOR_MODE_ONOFF + return ColorMode.BRIGHTNESS + return ColorMode.ONOFF @property def supported_color_modes(self) -> set[str] | None: """Flag supported color modes.""" if self._device.is_dimmable and self._device.is_color_capable: - return {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + return {ColorMode.COLOR_TEMP, ColorMode.HS} if self._device.is_dimmable: - return {COLOR_MODE_BRIGHTNESS} - return {COLOR_MODE_ONOFF} + return {ColorMode.BRIGHTNESS} + return {ColorMode.ONOFF} @property def supported_features(self) -> int: diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index b1afeb0364c..e7712dcf630 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -146,7 +146,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { key=DPCode.COOK_TIME, name="Cook Time", icon="mdi:timer", - unit_of_measurement=TIME_MINUTES, + native_unit_of_measurement=TIME_MINUTES, entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( From 981249d330b9605b878bdf87195b8cb31ca8421e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 29 Jun 2022 03:16:06 -0600 Subject: [PATCH 1939/3516] Ensure `_attr_native_value` type matches what `SensorExtraStoredData` produces (#73970) --- homeassistant/components/sensor/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 2c0d75ff471..6d35c2a4635 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -264,7 +264,7 @@ class SensorEntity(Entity): _attr_device_class: SensorDeviceClass | str | None _attr_last_reset: datetime | None _attr_native_unit_of_measurement: str | None - _attr_native_value: StateType | date | datetime = None + _attr_native_value: StateType | date | datetime | Decimal = None _attr_state_class: SensorStateClass | str | None _attr_state: None = None # Subclasses of SensorEntity should not set this _attr_unit_of_measurement: None = ( @@ -349,7 +349,7 @@ class SensorEntity(Entity): return None @property - def native_value(self) -> StateType | date | datetime: + def native_value(self) -> StateType | date | datetime | Decimal: """Return the value reported by the sensor.""" return self._attr_native_value From 500105fa86fb99031a818eda019dfed20d8ec250 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 29 Jun 2022 05:21:01 -0400 Subject: [PATCH 1940/3516] Move Tautulli attributes to their own sensors (#71712) --- homeassistant/components/tautulli/__init__.py | 10 +- homeassistant/components/tautulli/const.py | 2 + homeassistant/components/tautulli/sensor.py | 285 +++++++++++++++--- 3 files changed, 247 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/tautulli/__init__.py b/homeassistant/components/tautulli/__init__.py index fe6eeb9e303..339ec6eb895 100644 --- a/homeassistant/components/tautulli/__init__.py +++ b/homeassistant/components/tautulli/__init__.py @@ -1,7 +1,7 @@ """The Tautulli integration.""" from __future__ import annotations -from pytautulli import PyTautulli, PyTautulliHostConfiguration +from pytautulli import PyTautulli, PyTautulliApiUser, PyTautulliHostConfiguration from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL, Platform @@ -50,14 +50,18 @@ class TautulliEntity(CoordinatorEntity[TautulliDataUpdateCoordinator]): self, coordinator: TautulliDataUpdateCoordinator, description: EntityDescription, + user: PyTautulliApiUser | None = None, ) -> None: """Initialize the Tautulli entity.""" super().__init__(coordinator) + entry_id = coordinator.config_entry.entry_id + self._attr_unique_id = f"{entry_id}_{description.key}" self.entity_description = description - self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" + self.user = user self._attr_device_info = DeviceInfo( configuration_url=coordinator.host_configuration.base_url, entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + identifiers={(DOMAIN, user.user_id if user else entry_id)}, manufacturer=DEFAULT_NAME, + name=user.username if user else DEFAULT_NAME, ) diff --git a/homeassistant/components/tautulli/const.py b/homeassistant/components/tautulli/const.py index 5c0a1b56cda..49b86ec6ef7 100644 --- a/homeassistant/components/tautulli/const.py +++ b/homeassistant/components/tautulli/const.py @@ -1,6 +1,8 @@ """Constants for the Tautulli integration.""" from logging import Logger, getLogger +ATTR_TOP_USER = "top_user" + CONF_MONITORED_USERS = "monitored_users" DEFAULT_NAME = "Tautulli" DEFAULT_PATH = "" diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py index b1af6e3ce47..5981992c946 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -1,14 +1,23 @@ """A platform which allows you to get information from Tautulli.""" from __future__ import annotations -from typing import Any +from collections.abc import Callable +from dataclasses import dataclass +from typing import cast +from pytautulli import ( + PyTautulliApiActivity, + PyTautulliApiHomeStats, + PyTautulliApiSession, + PyTautulliApiUser, +) import voluptuous as vol from homeassistant.components.sensor import ( PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( @@ -20,14 +29,18 @@ from homeassistant.const import ( CONF_PORT, CONF_SSL, CONF_VERIFY_SSL, + DATA_KILOBITS, + PERCENTAGE, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import EntityCategory, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from . import TautulliEntity from .const import ( + ATTR_TOP_USER, CONF_MONITORED_USERS, DEFAULT_NAME, DEFAULT_PATH, @@ -53,12 +66,188 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( + +def get_top_stats( + home_stats: PyTautulliApiHomeStats, activity: PyTautulliApiActivity, key: str +) -> str | None: + """Get top statistics.""" + value = None + for stat in home_stats: + if stat.rows and stat.stat_id == key: + value = stat.rows[0].title + elif stat.rows and stat.stat_id == "top_users" and key == ATTR_TOP_USER: + value = stat.rows[0].user + return value + + +@dataclass +class TautulliSensorEntityMixin: + """Mixin for Tautulli sensor.""" + + value_fn: Callable[[PyTautulliApiHomeStats, PyTautulliApiActivity, str], StateType] + + +@dataclass +class TautulliSensorEntityDescription( + SensorEntityDescription, TautulliSensorEntityMixin +): + """Describes a Tautulli sensor.""" + + +SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( + TautulliSensorEntityDescription( icon="mdi:plex", key="watching_count", name="Tautulli", native_unit_of_measurement="Watching", + value_fn=lambda home_stats, activity, _: cast(int, activity.stream_count), + ), + TautulliSensorEntityDescription( + icon="mdi:plex", + key="stream_count_direct_play", + name="Direct Plays", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement="Streams", + entity_registry_enabled_default=False, + value_fn=lambda home_stats, activity, _: cast( + int, activity.stream_count_direct_play + ), + ), + TautulliSensorEntityDescription( + icon="mdi:plex", + key="stream_count_direct_stream", + name="Direct Streams", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement="Streams", + entity_registry_enabled_default=False, + value_fn=lambda home_stats, activity, _: cast( + int, activity.stream_count_direct_stream + ), + ), + TautulliSensorEntityDescription( + icon="mdi:plex", + key="stream_count_transcode", + name="Transcodes", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement="Streams", + entity_registry_enabled_default=False, + value_fn=lambda home_stats, activity, _: cast( + int, activity.stream_count_transcode + ), + ), + TautulliSensorEntityDescription( + key="total_bandwidth", + name="Total Bandwidth", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_KILOBITS, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda home_stats, activity, _: cast(int, activity.total_bandwidth), + ), + TautulliSensorEntityDescription( + key="lan_bandwidth", + name="LAN Bandwidth", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_KILOBITS, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda home_stats, activity, _: cast(int, activity.lan_bandwidth), + ), + TautulliSensorEntityDescription( + key="wan_bandwidth", + name="WAN Bandwidth", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_KILOBITS, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda home_stats, activity, _: cast(int, activity.wan_bandwidth), + ), + TautulliSensorEntityDescription( + icon="mdi:movie-open", + key="top_movies", + name="Top Movie", + entity_registry_enabled_default=False, + value_fn=get_top_stats, + ), + TautulliSensorEntityDescription( + icon="mdi:television", + key="top_tv", + name="Top TV Show", + entity_registry_enabled_default=False, + value_fn=get_top_stats, + ), + TautulliSensorEntityDescription( + icon="mdi:walk", + key=ATTR_TOP_USER, + name="Top User", + entity_registry_enabled_default=False, + value_fn=get_top_stats, + ), +) + + +@dataclass +class TautulliSessionSensorEntityMixin: + """Mixin for Tautulli session sensor.""" + + value_fn: Callable[[PyTautulliApiSession], StateType] + + +@dataclass +class TautulliSessionSensorEntityDescription( + SensorEntityDescription, TautulliSessionSensorEntityMixin +): + """Describes a Tautulli session sensor.""" + + +SESSION_SENSOR_TYPES: tuple[TautulliSessionSensorEntityDescription, ...] = ( + TautulliSessionSensorEntityDescription( + icon="mdi:plex", + key="state", + name="State", + value_fn=lambda session: cast(str, session.state), + ), + TautulliSessionSensorEntityDescription( + key="full_title", + name="Full Title", + entity_registry_enabled_default=False, + value_fn=lambda session: cast(str, session.full_title), + ), + TautulliSessionSensorEntityDescription( + icon="mdi:progress-clock", + key="progress", + name="Progress", + native_unit_of_measurement=PERCENTAGE, + entity_registry_enabled_default=False, + value_fn=lambda session: cast(str, session.progress_percent), + ), + TautulliSessionSensorEntityDescription( + key="stream_resolution", + name="Stream Resolution", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda session: cast(str, session.stream_video_resolution), + ), + TautulliSessionSensorEntityDescription( + icon="mdi:plex", + key="transcode_decision", + name="Transcode Decision", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda session: cast(str, session.transcode_decision), + ), + TautulliSessionSensorEntityDescription( + key="session_thumb", + name="session Thumbnail", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda session: cast(str, session.user_thumb), + ), + TautulliSessionSensorEntityDescription( + key="video_resolution", + name="Video Resolution", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda session: cast(str, session.video_resolution), ), ) @@ -82,62 +271,64 @@ async def async_setup_entry( ) -> None: """Set up Tautulli sensor.""" coordinator: TautulliDataUpdateCoordinator = hass.data[DOMAIN] - async_add_entities( + entities: list[TautulliSensor | TautulliSessionSensor] = [ TautulliSensor( coordinator, description, ) for description in SENSOR_TYPES - ) + ] + if coordinator.users: + entities.extend( + TautulliSessionSensor( + coordinator, + description, + user, + ) + for description in SESSION_SENSOR_TYPES + for user in coordinator.users + if user.username != "Local" + ) + async_add_entities(entities) class TautulliSensor(TautulliEntity, SensorEntity): """Representation of a Tautulli sensor.""" + entity_description: TautulliSensorEntityDescription + @property def native_value(self) -> StateType: """Return the state of the sensor.""" - if not self.coordinator.activity: - return 0 - return self.coordinator.activity.stream_count or 0 + return self.entity_description.value_fn( + self.coordinator.home_stats, + self.coordinator.activity, + self.entity_description.key, + ) + + +class TautulliSessionSensor(TautulliEntity, SensorEntity): + """Representation of a Tautulli session sensor.""" + + entity_description: TautulliSessionSensorEntityDescription + + def __init__( + self, + coordinator: TautulliDataUpdateCoordinator, + description: EntityDescription, + user: PyTautulliApiUser, + ) -> None: + """Initialize the Tautulli entity.""" + super().__init__(coordinator, description, user) + entry_id = coordinator.config_entry.entry_id + self._attr_unique_id = f"{entry_id}_{user.user_id}_{description.key}" + self._attr_name = f"{user.username} {description.name}" @property - def extra_state_attributes(self) -> dict[str, Any] | None: - """Return attributes for the sensor.""" - if ( - not self.coordinator.activity - or not self.coordinator.home_stats - or not self.coordinator.users - ): - return None - - _attributes = { - "stream_count": self.coordinator.activity.stream_count, - "stream_count_direct_play": self.coordinator.activity.stream_count_direct_play, - "stream_count_direct_stream": self.coordinator.activity.stream_count_direct_stream, - "stream_count_transcode": self.coordinator.activity.stream_count_transcode, - "total_bandwidth": self.coordinator.activity.total_bandwidth, - "lan_bandwidth": self.coordinator.activity.lan_bandwidth, - "wan_bandwidth": self.coordinator.activity.wan_bandwidth, - } - - for stat in self.coordinator.home_stats: - if stat.stat_id == "top_movies": - _attributes["Top Movie"] = stat.rows[0].title if stat.rows else None - elif stat.stat_id == "top_tv": - _attributes["Top TV Show"] = stat.rows[0].title if stat.rows else None - elif stat.stat_id == "top_users": - _attributes["Top User"] = stat.rows[0].user if stat.rows else None - - for user in self.coordinator.users: - if user.username == "Local": - continue - _attributes.setdefault(user.username, {})["Activity"] = None - - for session in self.coordinator.activity.sessions: - if not _attributes.get(session.username) or "null" in session.state: - continue - - _attributes[session.username]["Activity"] = session.state - - return _attributes + def native_value(self) -> StateType: + """Return the state of the sensor.""" + if self.coordinator.activity: + for session in self.coordinator.activity.sessions: + if self.user and session.user_id == self.user.user_id: + return self.entity_description.value_fn(session) + return None From d323508f796e1b0ffb7d02b90a8ea08a356cd229 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 29 Jun 2022 11:25:37 +0200 Subject: [PATCH 1941/3516] Add type hints to async_step_reauth (#74164) --- homeassistant/components/august/config_flow.py | 7 +++++-- .../components/azure_devops/config_flow.py | 15 ++++++++++----- homeassistant/components/elmax/config_flow.py | 7 ++++--- homeassistant/components/hive/config_flow.py | 10 +++++++--- homeassistant/components/hyperion/config_flow.py | 7 ++----- homeassistant/components/mazda/config_flow.py | 9 ++++++--- homeassistant/components/nuki/config_flow.py | 6 ++++-- homeassistant/components/plex/config_flow.py | 7 +++++-- homeassistant/components/sense/config_flow.py | 9 ++++++--- homeassistant/components/spotify/config_flow.py | 5 +++-- .../components/totalconnect/config_flow.py | 10 +++++++--- 11 files changed, 59 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/august/config_flow.py b/homeassistant/components/august/config_flow.py index eb7bac9ae1a..067f986c4e6 100644 --- a/homeassistant/components/august/config_flow.py +++ b/homeassistant/components/august/config_flow.py @@ -1,11 +1,14 @@ """Config flow for August integration.""" +from collections.abc import Mapping import logging +from typing import Any import voluptuous as vol from yalexs.authenticator import ValidationResult from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from .const import CONF_LOGIN_METHOD, DOMAIN, LOGIN_METHODS, VERIFICATION_CODE_KEY from .exceptions import CannotConnect, InvalidAuth, RequireValidation @@ -109,9 +112,9 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_reauth(self, data): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" - self._user_auth_details = dict(data) + self._user_auth_details = dict(entry_data) self._mode = "reauth" self._needs_reset = True self._august_gateway = AugustGateway(self.hass) diff --git a/homeassistant/components/azure_devops/config_flow.py b/homeassistant/components/azure_devops/config_flow.py index 350bad5852a..8fba3378886 100644 --- a/homeassistant/components/azure_devops/config_flow.py +++ b/homeassistant/components/azure_devops/config_flow.py @@ -1,9 +1,13 @@ """Config flow to configure the Azure DevOps integration.""" +from collections.abc import Mapping +from typing import Any + from aioazuredevops.client import DevOpsClient import aiohttp import voluptuous as vol from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult from .const import CONF_ORG, CONF_PAT, CONF_PROJECT, DOMAIN @@ -82,12 +86,12 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): return await self._show_setup_form(errors) return self._async_create_entry() - async def async_step_reauth(self, user_input): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" - if user_input.get(CONF_ORG) and user_input.get(CONF_PROJECT): - self._organization = user_input[CONF_ORG] - self._project = user_input[CONF_PROJECT] - self._pat = user_input[CONF_PAT] + if entry_data.get(CONF_ORG) and entry_data.get(CONF_PROJECT): + self._organization = entry_data[CONF_ORG] + self._project = entry_data[CONF_PROJECT] + self._pat = entry_data[CONF_PAT] self.context["title_placeholders"] = { "project_url": f"{self._organization}/{self._project}", @@ -100,6 +104,7 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): return await self._show_reauth_form(errors) entry = await self.async_set_unique_id(self.unique_id) + assert entry self.hass.config_entries.async_update_entry( entry, data={ diff --git a/homeassistant/components/elmax/config_flow.py b/homeassistant/components/elmax/config_flow.py index 6872a555b8a..0c1a0148205 100644 --- a/homeassistant/components/elmax/config_flow.py +++ b/homeassistant/components/elmax/config_flow.py @@ -1,6 +1,7 @@ """Config flow for elmax-cloud integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -167,10 +168,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="panels", data_schema=self._panels_schema, errors=errors ) - async def async_step_reauth(self, user_input=None): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" - self._reauth_username = user_input.get(CONF_ELMAX_USERNAME) - self._reauth_panelid = user_input.get(CONF_ELMAX_PANEL_ID) + self._reauth_username = entry_data.get(CONF_ELMAX_USERNAME) + self._reauth_panelid = entry_data.get(CONF_ELMAX_PANEL_ID) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm(self, user_input=None): diff --git a/homeassistant/components/hive/config_flow.py b/homeassistant/components/hive/config_flow.py index 5368aa22c3f..ec1c4f78e87 100644 --- a/homeassistant/components/hive/config_flow.py +++ b/homeassistant/components/hive/config_flow.py @@ -1,6 +1,9 @@ """Config Flow for Hive.""" from __future__ import annotations +from collections.abc import Mapping +from typing import Any + from apyhiveapi import Auth from apyhiveapi.helper.hive_exceptions import ( HiveApiError, @@ -13,6 +16,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from .const import CONF_CODE, CONF_DEVICE_NAME, CONFIG_ENTRY_VERSION, DOMAIN @@ -136,11 +140,11 @@ class HiveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="reauth_successful") return self.async_create_entry(title=self.data["username"], data=self.data) - async def async_step_reauth(self, user_input=None): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Re Authenticate a user.""" data = { - CONF_USERNAME: user_input[CONF_USERNAME], - CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_USERNAME: entry_data[CONF_USERNAME], + CONF_PASSWORD: entry_data[CONF_PASSWORD], } return await self.async_step_user(data) diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index 8b8f8b62c47..97e97cd835d 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -141,12 +141,9 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_auth() return await self.async_step_confirm() - async def async_step_reauth( - self, - config_data: Mapping[str, Any], - ) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle a reauthentication flow.""" - self._data = dict(config_data) + self._data = dict(entry_data) async with self._create_client(raw_connection=True) as hyperion_client: if not hyperion_client: return self.async_abort(reason="cannot_connect") diff --git a/homeassistant/components/mazda/config_flow.py b/homeassistant/components/mazda/config_flow.py index b1b0ce35b11..0b255483da1 100644 --- a/homeassistant/components/mazda/config_flow.py +++ b/homeassistant/components/mazda/config_flow.py @@ -1,5 +1,7 @@ """Config flow for Mazda Connected Services integration.""" +from collections.abc import Mapping import logging +from typing import Any import aiohttp from pymazda import ( @@ -11,6 +13,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import aiohttp_client from .const import DOMAIN, MAZDA_REGIONS @@ -97,11 +100,11 @@ class MazdaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, user_input=None): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth if the user credentials have changed.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] ) - self._email = user_input[CONF_EMAIL] - self._region = user_input[CONF_REGION] + self._email = entry_data[CONF_EMAIL] + self._region = entry_data[CONF_REGION] return await self.async_step_user() diff --git a/homeassistant/components/nuki/config_flow.py b/homeassistant/components/nuki/config_flow.py index 054d0aaa219..85144d9bb77 100644 --- a/homeassistant/components/nuki/config_flow.py +++ b/homeassistant/components/nuki/config_flow.py @@ -1,5 +1,7 @@ """Config flow to configure the Nuki integration.""" +from collections.abc import Mapping import logging +from typing import Any from pynuki import NukiBridge from pynuki.bridge import InvalidCredentialsException @@ -80,9 +82,9 @@ class NukiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_validate() - async def async_step_reauth(self, data): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" - self._data = data + self._data = entry_data return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index 3489e41364e..42d227154a6 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -1,8 +1,10 @@ """Config flow for Plex.""" from __future__ import annotations +from collections.abc import Mapping import copy import logging +from typing import Any from aiohttp import web_response import plexapi.exceptions @@ -26,6 +28,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -329,9 +332,9 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): server_config = {CONF_TOKEN: self.token} return await self.async_step_server_validate(server_config) - async def async_step_reauth(self, data): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle a reauthorization flow request.""" - self.current_login = dict(data) + self.current_login = dict(entry_data) return await self.async_step_user() diff --git a/homeassistant/components/sense/config_flow.py b/homeassistant/components/sense/config_flow.py index 8769d4cb83f..0690344ccf1 100644 --- a/homeassistant/components/sense/config_flow.py +++ b/homeassistant/components/sense/config_flow.py @@ -1,5 +1,7 @@ """Config flow for Sense integration.""" +from collections.abc import Mapping import logging +from typing import Any from sense_energy import ( ASyncSenseable, @@ -10,6 +12,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_CODE, CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, DOMAIN, SENSE_CONNECT_EXCEPTIONS @@ -120,10 +123,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, data): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" - self._auth_data = dict(data) - return await self.async_step_reauth_validate(data) + self._auth_data = dict(entry_data) + return await self.async_step_reauth_validate(entry_data) async def async_step_reauth_validate(self, user_input=None): """Handle reauth and validation.""" diff --git a/homeassistant/components/spotify/config_flow.py b/homeassistant/components/spotify/config_flow.py index 30ad74f8e26..013063308bb 100644 --- a/homeassistant/components/spotify/config_flow.py +++ b/homeassistant/components/spotify/config_flow.py @@ -57,7 +57,7 @@ class SpotifyFlowHandler( return self.async_create_entry(title=name, data=data) - async def async_step_reauth(self, entry: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon migration of old entries.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -65,7 +65,8 @@ class SpotifyFlowHandler( persistent_notification.async_create( self.hass, - f"Spotify integration for account {entry['id']} needs to be re-authenticated. Please go to the integrations page to re-configure it.", + f"Spotify integration for account {entry_data['id']} needs to be " + "re-authenticated. Please go to the integrations page to re-configure it.", "Spotify re-authentication", "spotify_reauth", ) diff --git a/homeassistant/components/totalconnect/config_flow.py b/homeassistant/components/totalconnect/config_flow.py index 057328cffb0..8d35506af0f 100644 --- a/homeassistant/components/totalconnect/config_flow.py +++ b/homeassistant/components/totalconnect/config_flow.py @@ -1,6 +1,9 @@ """Config flow for the Total Connect component.""" from __future__ import annotations +from collections.abc import Mapping +from typing import Any + from total_connect_client.client import TotalConnectClient from total_connect_client.exceptions import AuthenticationError import voluptuous as vol @@ -8,6 +11,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_LOCATION, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from .const import AUTO_BYPASS, CONF_USERCODES, DOMAIN @@ -121,10 +125,10 @@ class TotalConnectConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders={"location_id": location_for_user}, ) - async def async_step_reauth(self, config): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an authentication error or no usercode.""" - self.username = config[CONF_USERNAME] - self.usercodes = config[CONF_USERCODES] + self.username = entry_data[CONF_USERNAME] + self.usercodes = entry_data[CONF_USERCODES] return await self.async_step_reauth_confirm() From f2809262d527c1676f47b743ee02eeeb3c1ca99f Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 29 Jun 2022 11:28:46 +0200 Subject: [PATCH 1942/3516] Netgear add CPU and Memory utilization sensors (#72667) --- homeassistant/components/netgear/__init__.py | 14 ++++++++++ homeassistant/components/netgear/const.py | 1 + homeassistant/components/netgear/router.py | 5 ++++ homeassistant/components/netgear/sensor.py | 27 ++++++++++++++++++++ 4 files changed, 47 insertions(+) diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index 953008ae9f5..6e56aef91c5 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -18,6 +18,7 @@ from .const import ( KEY_COORDINATOR_LINK, KEY_COORDINATOR_SPEED, KEY_COORDINATOR_TRAFFIC, + KEY_COORDINATOR_UTIL, KEY_ROUTER, PLATFORMS, ) @@ -84,6 +85,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Fetch data from the router.""" return await router.async_get_speed_test() + async def async_update_utilization() -> dict[str, Any] | None: + """Fetch data from the router.""" + return await router.async_get_utilization() + async def async_check_link_status() -> dict[str, Any] | None: """Fetch data from the router.""" return await router.async_get_link_status() @@ -110,6 +115,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=async_update_speed_test, update_interval=SPEED_TEST_INTERVAL, ) + coordinator_utilization = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"{router.device_name} Utilization", + update_method=async_update_utilization, + update_interval=SCAN_INTERVAL, + ) coordinator_link = DataUpdateCoordinator( hass, _LOGGER, @@ -121,6 +133,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if router.track_devices: await coordinator.async_config_entry_first_refresh() await coordinator_traffic_meter.async_config_entry_first_refresh() + await coordinator_utilization.async_config_entry_first_refresh() await coordinator_link.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = { @@ -128,6 +141,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: KEY_COORDINATOR: coordinator, KEY_COORDINATOR_TRAFFIC: coordinator_traffic_meter, KEY_COORDINATOR_SPEED: coordinator_speed_test, + KEY_COORDINATOR_UTIL: coordinator_utilization, KEY_COORDINATOR_LINK: coordinator_link, } diff --git a/homeassistant/components/netgear/const.py b/homeassistant/components/netgear/const.py index c8939208047..f9b9a93b767 100644 --- a/homeassistant/components/netgear/const.py +++ b/homeassistant/components/netgear/const.py @@ -13,6 +13,7 @@ KEY_ROUTER = "router" KEY_COORDINATOR = "coordinator" KEY_COORDINATOR_TRAFFIC = "coordinator_traffic" KEY_COORDINATOR_SPEED = "coordinator_speed" +KEY_COORDINATOR_UTIL = "coordinator_utilization" KEY_COORDINATOR_LINK = "coordinator_link" DEFAULT_CONSIDER_HOME = timedelta(seconds=180) diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 6284c6f4ac2..57b0efa1bf2 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -240,6 +240,11 @@ class NetgearRouter: self._api.allow_block_device, mac, allow_block ) + async def async_get_utilization(self) -> dict[str, Any] | None: + """Get the system information about utilization of the router.""" + async with self._api_lock: + return await self.hass.async_add_executor_job(self._api.get_system_info) + async def async_reboot(self) -> None: """Reboot the router.""" async with self._api_lock: diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index f860b65e10f..1ada340d1e1 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -32,6 +33,7 @@ from .const import ( KEY_COORDINATOR_LINK, KEY_COORDINATOR_SPEED, KEY_COORDINATOR_TRAFFIC, + KEY_COORDINATOR_UTIL, KEY_ROUTER, ) from .router import NetgearDeviceEntity, NetgearRouter, NetgearRouterEntity @@ -245,6 +247,25 @@ SENSOR_SPEED_TYPES = [ ), ] +SENSOR_UTILIZATION = [ + NetgearSensorEntityDescription( + key="NewCPUUtilization", + name="CPU Utilization", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + icon="mdi:cpu-64-bit", + state_class=SensorStateClass.MEASUREMENT, + ), + NetgearSensorEntityDescription( + key="NewMemoryUtilization", + name="Memory Utilization", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, + ), +] + SENSOR_LINK_TYPES = [ NetgearSensorEntityDescription( key="NewEthernetLinkStatus", @@ -263,6 +284,7 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR] coordinator_traffic = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR_TRAFFIC] coordinator_speed = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR_SPEED] + coordinator_utilization = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR_UTIL] coordinator_link = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR_LINK] # Router entities @@ -278,6 +300,11 @@ async def async_setup_entry( NetgearRouterSensorEntity(coordinator_speed, router, description) ) + for description in SENSOR_UTILIZATION: + router_entities.append( + NetgearRouterSensorEntity(coordinator_utilization, router, description) + ) + for description in SENSOR_LINK_TYPES: router_entities.append( NetgearRouterSensorEntity(coordinator_link, router, description) From ebf21d1bd7cf25f81bfc0153059226f0444ccdbe Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 29 Jun 2022 11:41:56 +0200 Subject: [PATCH 1943/3516] Add BinarySensorEntity to pylint checks (#74131) --- pylint/plugins/hass_enforce_type_hints.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index f3c7a01a10f..f08e6d932e7 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -678,6 +678,25 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "binary_sensor": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="BinarySensorEntity", + matches=[ + TypeHintMatch( + function_name="device_class", + return_type=["BinarySensorDeviceClass", "str", None], + ), + TypeHintMatch( + function_name="is_on", + return_type=["bool", None], + ), + ], + ), + ], "cover": [ ClassTypeHintMatch( base_class="Entity", From 5b73cb10c18192815e71d0e68cfb6adaabbf7312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Hansl=C3=ADk?= Date: Wed, 29 Jun 2022 11:42:52 +0200 Subject: [PATCH 1944/3516] MWh is valid unit for energy dashboard (#73929) MWh is valid unit for energy --- homeassistant/components/energy/validate.py | 7 ++++++- tests/components/energy/test_validate.py | 21 ++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index e48a576f44e..3baae348770 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -10,6 +10,7 @@ from homeassistant.components import recorder, sensor from homeassistant.const import ( ATTR_DEVICE_CLASS, ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, ENERGY_WATT_HOUR, STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -23,7 +24,11 @@ from .const import DOMAIN ENERGY_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.ENERGY,) ENERGY_USAGE_UNITS = { - sensor.SensorDeviceClass.ENERGY: (ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR) + sensor.SensorDeviceClass.ENERGY: ( + ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, + ENERGY_WATT_HOUR, + ) } ENERGY_PRICE_UNITS = tuple( f"/{unit}" for units in ENERGY_USAGE_UNITS.values() for unit in units diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py index e802688daaf..fe71663d41b 100644 --- a/tests/components/energy/test_validate.py +++ b/tests/components/energy/test_validate.py @@ -4,6 +4,11 @@ from unittest.mock import patch import pytest from homeassistant.components.energy import async_get_manager, validate +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, + ENERGY_WATT_HOUR, +) from homeassistant.helpers.json import JSON_DUMP from homeassistant.setup import async_setup_component @@ -60,16 +65,18 @@ async def test_validation_empty_config(hass): @pytest.mark.parametrize( - "state_class, extra", + "state_class, energy_unit, extra", [ - ("total_increasing", {}), - ("total", {}), - ("total", {"last_reset": "abc"}), - ("measurement", {"last_reset": "abc"}), + ("total_increasing", ENERGY_KILO_WATT_HOUR, {}), + ("total_increasing", ENERGY_MEGA_WATT_HOUR, {}), + ("total_increasing", ENERGY_WATT_HOUR, {}), + ("total", ENERGY_KILO_WATT_HOUR, {}), + ("total", ENERGY_KILO_WATT_HOUR, {"last_reset": "abc"}), + ("measurement", ENERGY_KILO_WATT_HOUR, {"last_reset": "abc"}), ], ) async def test_validation( - hass, mock_energy_manager, mock_get_metadata, state_class, extra + hass, mock_energy_manager, mock_get_metadata, state_class, energy_unit, extra ): """Test validating success.""" for key in ("device_cons", "battery_import", "battery_export", "solar_production"): @@ -78,7 +85,7 @@ async def test_validation( "123", { "device_class": "energy", - "unit_of_measurement": "kWh", + "unit_of_measurement": energy_unit, "state_class": state_class, **extra, }, From 79fdb0d847e4ecd514e8586b7869f34355aef6fb Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 29 Jun 2022 11:43:51 +0200 Subject: [PATCH 1945/3516] Netgear add update entity (#72429) --- .coveragerc | 1 + homeassistant/components/netgear/__init__.py | 15 ++++ homeassistant/components/netgear/const.py | 9 ++- homeassistant/components/netgear/router.py | 10 +++ homeassistant/components/netgear/update.py | 81 ++++++++++++++++++++ 5 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/netgear/update.py diff --git a/.coveragerc b/.coveragerc index 981e2d06680..02c643ae757 100644 --- a/.coveragerc +++ b/.coveragerc @@ -793,6 +793,7 @@ omit = homeassistant/components/netgear/router.py homeassistant/components/netgear/sensor.py homeassistant/components/netgear/switch.py + homeassistant/components/netgear/update.py homeassistant/components/netgear_lte/* homeassistant/components/netio/switch.py homeassistant/components/neurio_energy/sensor.py diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index 6e56aef91c5..a996699ab9e 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -15,6 +15,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( DOMAIN, KEY_COORDINATOR, + KEY_COORDINATOR_FIRMWARE, KEY_COORDINATOR_LINK, KEY_COORDINATOR_SPEED, KEY_COORDINATOR_TRAFFIC, @@ -29,6 +30,7 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=30) SPEED_TEST_INTERVAL = timedelta(seconds=1800) +SCAN_INTERVAL_FIRMWARE = timedelta(seconds=18000) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -85,6 +87,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Fetch data from the router.""" return await router.async_get_speed_test() + async def async_check_firmware() -> dict[str, Any] | None: + """Check for new firmware of the router.""" + return await router.async_check_new_firmware() + async def async_update_utilization() -> dict[str, Any] | None: """Fetch data from the router.""" return await router.async_get_utilization() @@ -115,6 +121,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=async_update_speed_test, update_interval=SPEED_TEST_INTERVAL, ) + coordinator_firmware = DataUpdateCoordinator( + hass, + _LOGGER, + name=f"{router.device_name} Firmware", + update_method=async_check_firmware, + update_interval=SCAN_INTERVAL_FIRMWARE, + ) coordinator_utilization = DataUpdateCoordinator( hass, _LOGGER, @@ -133,6 +146,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if router.track_devices: await coordinator.async_config_entry_first_refresh() await coordinator_traffic_meter.async_config_entry_first_refresh() + await coordinator_firmware.async_config_entry_first_refresh() await coordinator_utilization.async_config_entry_first_refresh() await coordinator_link.async_config_entry_first_refresh() @@ -141,6 +155,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: KEY_COORDINATOR: coordinator, KEY_COORDINATOR_TRAFFIC: coordinator_traffic_meter, KEY_COORDINATOR_SPEED: coordinator_speed_test, + KEY_COORDINATOR_FIRMWARE: coordinator_firmware, KEY_COORDINATOR_UTIL: coordinator_utilization, KEY_COORDINATOR_LINK: coordinator_link, } diff --git a/homeassistant/components/netgear/const.py b/homeassistant/components/netgear/const.py index f9b9a93b767..eaa32362baf 100644 --- a/homeassistant/components/netgear/const.py +++ b/homeassistant/components/netgear/const.py @@ -5,7 +5,13 @@ from homeassistant.const import Platform DOMAIN = "netgear" -PLATFORMS = [Platform.BUTTON, Platform.DEVICE_TRACKER, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [ + Platform.BUTTON, + Platform.DEVICE_TRACKER, + Platform.SENSOR, + Platform.SWITCH, + Platform.UPDATE, +] CONF_CONSIDER_HOME = "consider_home" @@ -13,6 +19,7 @@ KEY_ROUTER = "router" KEY_COORDINATOR = "coordinator" KEY_COORDINATOR_TRAFFIC = "coordinator_traffic" KEY_COORDINATOR_SPEED = "coordinator_speed" +KEY_COORDINATOR_FIRMWARE = "coordinator_firmware" KEY_COORDINATOR_UTIL = "coordinator_utilization" KEY_COORDINATOR_LINK = "coordinator_link" diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 57b0efa1bf2..a4f8a4df14e 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -250,6 +250,16 @@ class NetgearRouter: async with self._api_lock: await self.hass.async_add_executor_job(self._api.reboot) + async def async_check_new_firmware(self) -> None: + """Check for new firmware of the router.""" + async with self._api_lock: + return await self.hass.async_add_executor_job(self._api.check_new_firmware) + + async def async_update_new_firmware(self) -> None: + """Update the router to the latest firmware.""" + async with self._api_lock: + await self.hass.async_add_executor_job(self._api.update_new_firmware) + @property def port(self) -> int: """Port used by the API.""" diff --git a/homeassistant/components/netgear/update.py b/homeassistant/components/netgear/update.py new file mode 100644 index 00000000000..8d4a9b4912a --- /dev/null +++ b/homeassistant/components/netgear/update.py @@ -0,0 +1,81 @@ +"""Update entities for Netgear devices.""" +from __future__ import annotations + +import logging +from typing import Any + +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, KEY_COORDINATOR_FIRMWARE, KEY_ROUTER +from .router import NetgearRouter, NetgearRouterEntity + +LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up update entities for Netgear component.""" + router = hass.data[DOMAIN][entry.entry_id][KEY_ROUTER] + coordinator = hass.data[DOMAIN][entry.entry_id][KEY_COORDINATOR_FIRMWARE] + entities = [NetgearUpdateEntity(coordinator, router)] + + async_add_entities(entities) + + +class NetgearUpdateEntity(NetgearRouterEntity, UpdateEntity): + """Update entity for a Netgear device.""" + + _attr_device_class = UpdateDeviceClass.FIRMWARE + _attr_supported_features = UpdateEntityFeature.INSTALL + + def __init__( + self, + coordinator: DataUpdateCoordinator, + router: NetgearRouter, + ) -> None: + """Initialize a Netgear device.""" + super().__init__(coordinator, router) + self._name = f"{router.device_name} Update" + self._unique_id = f"{router.serial_number}-update" + + @property + def installed_version(self) -> str | None: + """Version currently in use.""" + if self.coordinator.data is not None: + return self.coordinator.data.get("CurrentVersion") + return None + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + if self.coordinator.data is not None: + new_version = self.coordinator.data.get("NewVersion") + if new_version is not None: + return new_version + return self.installed_version + + @property + def release_summary(self) -> str | None: + """Release summary.""" + if self.coordinator.data is not None: + return self.coordinator.data.get("ReleaseNote") + return None + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install the latest firmware version.""" + await self._router.async_update_new_firmware() + + @callback + def async_update_device(self) -> None: + """Update the Netgear device.""" From 75efb54cc29f7eae34de6b8d8f8a011cdd6c80d2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 29 Jun 2022 11:45:31 +0200 Subject: [PATCH 1946/3516] Adjust async_step_reauth in apple_tv (#74166) --- homeassistant/components/apple_tv/config_flow.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/apple_tv/config_flow.py b/homeassistant/components/apple_tv/config_flow.py index 1f3133d7e16..b78add3260e 100644 --- a/homeassistant/components/apple_tv/config_flow.py +++ b/homeassistant/components/apple_tv/config_flow.py @@ -3,9 +3,11 @@ from __future__ import annotations import asyncio from collections import deque +from collections.abc import Mapping from ipaddress import ip_address import logging from random import randrange +from typing import Any from pyatv import exceptions, pair, scan from pyatv.const import DeviceModel, PairingRequirement, Protocol @@ -13,10 +15,11 @@ from pyatv.convert import model_str, protocol_str from pyatv.helpers import get_unique_id import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PIN from homeassistant.core import callback +from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.network import is_ipv6_address @@ -118,10 +121,10 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return entry.unique_id return None - async def async_step_reauth(self, user_input=None): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle initial step when updating invalid credentials.""" self.context["title_placeholders"] = { - "name": user_input[CONF_NAME], + "name": entry_data[CONF_NAME], "type": "Apple TV", } self.scan_filter = self.unique_id @@ -166,7 +169,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> data_entry_flow.FlowResult: + ) -> FlowResult: """Handle device found via zeroconf.""" host = discovery_info.host if is_ipv6_address(host): @@ -250,7 +253,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ): # Add potentially new identifiers from this device to the existing flow context["all_identifiers"].append(unique_id) - raise data_entry_flow.AbortFlow("already_in_progress") + raise AbortFlow("already_in_progress") async def async_found_zeroconf_device(self, user_input=None): """Handle device found after Zeroconf discovery.""" From 1b85929617c2852854bfc4b5992f1b856a076122 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 29 Jun 2022 11:46:59 +0200 Subject: [PATCH 1947/3516] Adjust async_step_reauth in samsungtv (#74165) --- .../components/samsungtv/config_flow.py | 63 ++++++++----------- 1 file changed, 27 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/samsungtv/config_flow.py b/homeassistant/components/samsungtv/config_flow.py index 130b1d28d5f..099f0afbae8 100644 --- a/homeassistant/components/samsungtv/config_flow.py +++ b/homeassistant/components/samsungtv/config_flow.py @@ -11,7 +11,7 @@ import getmac from samsungtvws.encrypted.authenticator import SamsungTVEncryptedWSAsyncAuthenticator import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.const import ( CONF_HOST, @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac @@ -123,7 +124,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_SSDP_MAIN_TV_AGENT_LOCATION: self._ssdp_main_tv_agent_location, } - def _get_entry_from_bridge(self) -> data_entry_flow.FlowResult: + def _get_entry_from_bridge(self) -> FlowResult: """Get device entry.""" assert self._bridge data = self._base_config_entry() @@ -137,7 +138,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_set_device_unique_id(self, raise_on_progress: bool = True) -> None: """Set device unique_id.""" if not await self._async_get_and_check_device_info(): - raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) + raise AbortFlow(RESULT_NOT_SUPPORTED) await self._async_set_unique_id_from_udn(raise_on_progress) self._async_update_and_abort_for_matching_unique_id() @@ -156,7 +157,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._ssdp_rendering_control_location, self._ssdp_main_tv_agent_location, ): - raise data_entry_flow.AbortFlow("already_configured") + raise AbortFlow("already_configured") # Now that we have updated the config entry, we can raise # if another one is progressing if raise_on_progress: @@ -184,7 +185,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): result, method, _info = await self._async_get_device_info_and_method() if result not in SUCCESSFUL_RESULTS: LOGGER.debug("No working config found for %s", self._host) - raise data_entry_flow.AbortFlow(result) + raise AbortFlow(result) assert method is not None self._bridge = SamsungTVBridge.get_bridge(self.hass, method, self._host) return @@ -207,7 +208,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Try to get the device info.""" result, _method, info = await self._async_get_device_info_and_method() if result not in SUCCESSFUL_RESULTS: - raise data_entry_flow.AbortFlow(result) + raise AbortFlow(result) if not info: return False dev_info = info.get("device", {}) @@ -216,7 +217,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.debug( "Host:%s has type: %s which is not supported", self._host, device_type ) - raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) + raise AbortFlow(RESULT_NOT_SUPPORTED) self._model = dev_info.get("modelName") name = dev_info.get("name") self._name = name.replace("[TV] ", "") if name else device_type @@ -230,9 +231,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._mac = mac return True - async def async_step_import( - self, user_input: dict[str, Any] - ) -> data_entry_flow.FlowResult: + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: """Handle configuration by yaml file.""" # We need to import even if we cannot validate # since the TV may be off at startup @@ -257,13 +256,13 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): socket.gethostbyname, user_input[CONF_HOST] ) except socket.gaierror as err: - raise data_entry_flow.AbortFlow(RESULT_UNKNOWN_HOST) from err + raise AbortFlow(RESULT_UNKNOWN_HOST) from err self._name = user_input.get(CONF_NAME, self._host) or "" self._title = self._name async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResult: + ) -> FlowResult: """Handle a flow initialized by the user.""" if user_input is not None: await self._async_set_name_host_from_input(user_input) @@ -281,7 +280,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pairing( self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResult: + ) -> FlowResult: """Handle a pairing by accepting the message on the TV.""" assert self._bridge is not None errors: dict[str, str] = {} @@ -290,7 +289,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if result == RESULT_SUCCESS: return self._get_entry_from_bridge() if result != RESULT_AUTH_MISSING: - raise data_entry_flow.AbortFlow(result) + raise AbortFlow(result) errors = {"base": RESULT_AUTH_MISSING} self.context["title_placeholders"] = {"device": self._title} @@ -303,7 +302,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_encrypted_pairing( self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResult: + ) -> FlowResult: """Handle a encrypted pairing.""" assert self._host is not None await self._async_start_encrypted_pairing(self._host) @@ -421,7 +420,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if (entry := self._async_update_existing_matching_entry()) and entry.unique_id: # If we have the unique id and the mac we abort # as we do not need anything else - raise data_entry_flow.AbortFlow("already_configured") + raise AbortFlow("already_configured") self._async_abort_if_host_already_in_progress() @callback @@ -429,18 +428,16 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.context[CONF_HOST] = self._host for progress in self._async_in_progress(): if progress.get("context", {}).get(CONF_HOST) == self._host: - raise data_entry_flow.AbortFlow("already_in_progress") + raise AbortFlow("already_in_progress") @callback def _abort_if_manufacturer_is_not_samsung(self) -> None: if not self._manufacturer or not self._manufacturer.lower().startswith( "samsung" ): - raise data_entry_flow.AbortFlow(RESULT_NOT_SUPPORTED) + raise AbortFlow(RESULT_NOT_SUPPORTED) - async def async_step_ssdp( - self, discovery_info: ssdp.SsdpServiceInfo - ) -> data_entry_flow.FlowResult: + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a flow initialized by ssdp discovery.""" LOGGER.debug("Samsung device found via SSDP: %s", discovery_info) model_name: str = discovery_info.upnp.get(ssdp.ATTR_UPNP_MODEL_NAME) or "" @@ -485,9 +482,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = {"device": self._title} return await self.async_step_confirm() - async def async_step_dhcp( - self, discovery_info: dhcp.DhcpServiceInfo - ) -> data_entry_flow.FlowResult: + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle a flow initialized by dhcp discovery.""" LOGGER.debug("Samsung device found via DHCP: %s", discovery_info) self._mac = discovery_info.macaddress @@ -499,7 +494,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo - ) -> data_entry_flow.FlowResult: + ) -> FlowResult: """Handle a flow initialized by zeroconf discovery.""" LOGGER.debug("Samsung device found via ZEROCONF: %s", discovery_info) self._mac = format_mac(discovery_info.properties["deviceid"]) @@ -511,7 +506,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResult: + ) -> FlowResult: """Handle user-confirmation of discovered node.""" if user_input is not None: await self._async_create_bridge() @@ -524,24 +519,20 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="confirm", description_placeholders={"device": self._title} ) - async def async_step_reauth( - self, data: Mapping[str, Any] - ) -> data_entry_flow.FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] ) - assert self._reauth_entry - data = self._reauth_entry.data - if data.get(CONF_MODEL) and data.get(CONF_NAME): - self._title = f"{data[CONF_NAME]} ({data[CONF_MODEL]})" + if entry_data.get(CONF_MODEL) and entry_data.get(CONF_NAME): + self._title = f"{entry_data[CONF_NAME]} ({entry_data[CONF_MODEL]})" else: - self._title = data.get(CONF_NAME) or data[CONF_HOST] + self._title = entry_data.get(CONF_NAME) or 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 - ) -> data_entry_flow.FlowResult: + ) -> FlowResult: """Confirm reauth.""" errors = {} assert self._reauth_entry @@ -586,7 +577,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm_encrypted( self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResult: + ) -> FlowResult: """Confirm reauth (encrypted method).""" errors = {} assert self._reauth_entry From 306486edfda581e9b30de8bcea0be58f7d66936f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 29 Jun 2022 11:51:57 +0200 Subject: [PATCH 1948/3516] Adjust async_step_reauth in smarttub (#74170) --- homeassistant/components/smarttub/config_flow.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/smarttub/config_flow.py b/homeassistant/components/smarttub/config_flow.py index 88ec38e8d63..ec3dcb9c97f 100644 --- a/homeassistant/components/smarttub/config_flow.py +++ b/homeassistant/components/smarttub/config_flow.py @@ -1,9 +1,15 @@ """Config flow to configure the SmartTub integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + from smarttub import LoginFailed import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN from .controller import SmartTubController @@ -21,8 +27,8 @@ class SmartTubConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Instantiate config flow.""" super().__init__() - self._reauth_input = None - self._reauth_entry = None + self._reauth_input: Mapping[str, Any] | None = None + self._reauth_entry: config_entries.ConfigEntry | None = None async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" @@ -60,9 +66,9 @@ class SmartTubConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_reauth(self, user_input=None): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Get new credentials if the current ones don't work anymore.""" - self._reauth_input = dict(user_input) + self._reauth_input = entry_data self._reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] ) From 9c991d9c6fcd6bc7683f21342c0dcaf8fbd6e091 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 29 Jun 2022 11:53:08 +0200 Subject: [PATCH 1949/3516] Adjust async_step_reauth in isy994 (#74169) --- .../components/isy994/config_flow.py | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index ff14c9bfc33..0dab84878b0 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -13,10 +13,11 @@ from pyisy.configuration import Configuration from pyisy.connection import Connection import voluptuous as vol -from homeassistant import config_entries, core, data_entry_flow, exceptions +from homeassistant import config_entries, core, exceptions from homeassistant.components import dhcp, ssdp from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback +from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import aiohttp_client from .const import ( @@ -132,7 +133,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResult: + ) -> FlowResult: """Handle the initial step.""" errors = {} info: dict[str, str] = {} @@ -160,9 +161,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import( - self, user_input: dict[str, Any] - ) -> data_entry_flow.FlowResult: + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: """Handle import.""" return await self.async_step_user(user_input) @@ -174,7 +173,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not existing_entry: return if existing_entry.source == config_entries.SOURCE_IGNORE: - raise data_entry_flow.AbortFlow("already_configured") + raise AbortFlow("already_configured") parsed_url = urlparse(existing_entry.data[CONF_HOST]) if parsed_url.hostname != ip_address: new_netloc = ip_address @@ -198,11 +197,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ), }, ) - raise data_entry_flow.AbortFlow("already_configured") + raise AbortFlow("already_configured") - async def async_step_dhcp( - self, discovery_info: dhcp.DhcpServiceInfo - ) -> data_entry_flow.FlowResult: + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle a discovered isy994 via dhcp.""" friendly_name = discovery_info.hostname if friendly_name.startswith("polisy"): @@ -223,9 +220,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = self.discovered_conf return await self.async_step_user() - async def async_step_ssdp( - self, discovery_info: ssdp.SsdpServiceInfo - ) -> data_entry_flow.FlowResult: + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: """Handle a discovered isy994.""" friendly_name = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] url = discovery_info.ssdp_location @@ -254,16 +249,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = self.discovered_conf return await self.async_step_user() - async def async_step_reauth( - self, data: Mapping[str, Any] - ) -> data_entry_flow.FlowResult: + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle reauth.""" self._existing_entry = await self.async_set_unique_id(self.context["unique_id"]) return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResult: + ) -> FlowResult: """Handle reauth input.""" errors = {} assert self._existing_entry is not None @@ -315,7 +308,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> data_entry_flow.FlowResult: + ) -> FlowResult: """Handle options flow.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) From 2fce301b34c8cf37822cc120958a6024c41ff391 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 29 Jun 2022 11:53:35 +0200 Subject: [PATCH 1950/3516] Adjust async_step_reauth in broadlink (#74168) --- .../components/broadlink/config_flow.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/broadlink/config_flow.py b/homeassistant/components/broadlink/config_flow.py index 8a32ba02ee8..5a0ed45b2ba 100644 --- a/homeassistant/components/broadlink/config_flow.py +++ b/homeassistant/components/broadlink/config_flow.py @@ -1,8 +1,10 @@ """Config flow for Broadlink devices.""" +from collections.abc import Mapping import errno from functools import partial import logging import socket +from typing import Any import broadlink as blk from broadlink.exceptions import ( @@ -12,9 +14,10 @@ from broadlink.exceptions import ( ) import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_TIMEOUT, CONF_TYPE +from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import config_validation as cv from .const import DEFAULT_PORT, DEFAULT_TIMEOUT, DEVICE_TYPES, DOMAIN @@ -40,7 +43,7 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): "an issue at https://github.com/home-assistant/core/issues", hex(device.devtype), ) - raise data_entry_flow.AbortFlow("not_supported") + raise AbortFlow("not_supported") await self.async_set_unique_id( device.mac.hex(), raise_on_progress=raise_on_progress @@ -53,9 +56,7 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): "host": device.host[0], } - async def async_step_dhcp( - self, discovery_info: dhcp.DhcpServiceInfo - ) -> data_entry_flow.FlowResult: + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle dhcp discovery.""" host = discovery_info.ip unique_id = discovery_info.macaddress.lower().replace(":", "") @@ -300,14 +301,14 @@ class BroadlinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._async_abort_entries_match({CONF_HOST: import_info[CONF_HOST]}) return await self.async_step_user(import_info) - async def async_step_reauth(self, data): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Reauthenticate to the device.""" device = blk.gendevice( - data[CONF_TYPE], - (data[CONF_HOST], DEFAULT_PORT), - bytes.fromhex(data[CONF_MAC]), - name=data[CONF_NAME], + entry_data[CONF_TYPE], + (entry_data[CONF_HOST], DEFAULT_PORT), + bytes.fromhex(entry_data[CONF_MAC]), + name=entry_data[CONF_NAME], ) - device.timeout = data[CONF_TIMEOUT] + device.timeout = entry_data[CONF_TIMEOUT] await self.async_set_device(device) return await self.async_step_reset() From 078c5cea86816031e4ce0a92e6b23fdd0ad11faf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 29 Jun 2022 11:54:21 +0200 Subject: [PATCH 1951/3516] Adjust async_step_reauth in blink (#74167) --- homeassistant/components/blink/config_flow.py | 7 +++++-- tests/components/blink/test_config_flow.py | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/blink/config_flow.py b/homeassistant/components/blink/config_flow.py index b62c7414f46..4f1c1997cad 100644 --- a/homeassistant/components/blink/config_flow.py +++ b/homeassistant/components/blink/config_flow.py @@ -1,7 +1,9 @@ """Config flow to configure Blink.""" from __future__ import annotations +from collections.abc import Mapping import logging +from typing import Any from blinkpy.auth import Auth, LoginError, TokenRefreshFailed from blinkpy.blinkpy import Blink, BlinkSetupError @@ -15,6 +17,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_SCAN_INTERVAL, DEVICE_ID, DOMAIN @@ -120,9 +123,9 @@ class BlinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry_data): + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon migration of old entries.""" - return await self.async_step_user(entry_data) + return await self.async_step_user(dict(entry_data)) @callback def _async_finish_flow(self): diff --git a/tests/components/blink/test_config_flow.py b/tests/components/blink/test_config_flow.py index 5e3b89002bf..5ea03eb2b62 100644 --- a/tests/components/blink/test_config_flow.py +++ b/tests/components/blink/test_config_flow.py @@ -246,7 +246,9 @@ async def test_form_unknown_error(hass): async def test_reauth_shows_user_step(hass): """Test reauth shows the user form.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_REAUTH} + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data={"username": "blink@example.com", "password": "invalid_password"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" From b5af96e4bb201c9bb43515ea11283bdc8c4212b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huryn?= Date: Wed, 29 Jun 2022 11:57:55 +0200 Subject: [PATCH 1952/3516] Bump blebox_uniapi to 2.0.0 and adapt integration (#73834) --- CODEOWNERS | 4 +- homeassistant/components/blebox/__init__.py | 8 +- .../components/blebox/air_quality.py | 4 + homeassistant/components/blebox/climate.py | 4 + .../components/blebox/config_flow.py | 6 +- homeassistant/components/blebox/light.py | 122 +++++++++++++++--- homeassistant/components/blebox/manifest.json | 4 +- homeassistant/components/blebox/switch.py | 4 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/blebox/conftest.py | 12 +- tests/components/blebox/test_config_flow.py | 4 +- tests/components/blebox/test_light.py | 9 +- 13 files changed, 140 insertions(+), 45 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index e2d0cbdaa3f..e349ebeacf0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -129,8 +129,8 @@ build.json @home-assistant/supervisor /homeassistant/components/binary_sensor/ @home-assistant/core /tests/components/binary_sensor/ @home-assistant/core /homeassistant/components/bizkaibus/ @UgaitzEtxebarria -/homeassistant/components/blebox/ @bbx-a @bbx-jp -/tests/components/blebox/ @bbx-a @bbx-jp +/homeassistant/components/blebox/ @bbx-a @bbx-jp @riokuu +/tests/components/blebox/ @bbx-a @bbx-jp @riokuu /homeassistant/components/blink/ @fronzbot /tests/components/blink/ @fronzbot /homeassistant/components/blueprint/ @home-assistant/core diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index b6a0045940d..a8f93dd0122 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -1,8 +1,8 @@ """The BleBox devices integration.""" import logging +from blebox_uniapi.box import Box from blebox_uniapi.error import Error -from blebox_uniapi.products import Products from blebox_uniapi.session import ApiHost from homeassistant.config_entries import ConfigEntry @@ -30,7 +30,6 @@ PARALLEL_UPDATES = 0 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up BleBox devices from a config entry.""" - websession = async_get_clientsession(hass) host = entry.data[CONF_HOST] @@ -40,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api_host = ApiHost(host, port, timeout, websession, hass.loop) try: - product = await Products.async_from_host(api_host) + product = await Box.async_from_host(api_host) except Error as ex: _LOGGER.error("Identify failed at %s:%d (%s)", api_host.host, api_host.port, ex) raise ConfigEntryNotReady from ex @@ -50,7 +49,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: product = domain_entry.setdefault(PRODUCT, product) hass.config_entries.async_setup_platforms(entry, PLATFORMS) - return True @@ -71,8 +69,8 @@ def create_blebox_entities( """Create entities from a BleBox product's features.""" product = hass.data[DOMAIN][config_entry.entry_id][PRODUCT] - entities = [] + if entity_type in product.features: for feature in product.features[entity_type]: entities.append(entity_klass(feature)) diff --git a/homeassistant/components/blebox/air_quality.py b/homeassistant/components/blebox/air_quality.py index 31efd797678..daadbc831b6 100644 --- a/homeassistant/components/blebox/air_quality.py +++ b/homeassistant/components/blebox/air_quality.py @@ -1,4 +1,6 @@ """BleBox air quality entity.""" +from datetime import timedelta + from homeassistant.components.air_quality import AirQualityEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -6,6 +8,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BleBoxEntity, create_blebox_entities +SCAN_INTERVAL = timedelta(seconds=5) + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/blebox/climate.py b/homeassistant/components/blebox/climate.py index 20a019ec0ec..e279991df20 100644 --- a/homeassistant/components/blebox/climate.py +++ b/homeassistant/components/blebox/climate.py @@ -1,4 +1,6 @@ """BleBox climate entity.""" +from datetime import timedelta + from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ClimateEntityFeature, @@ -12,6 +14,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BleBoxEntity, create_blebox_entities +SCAN_INTERVAL = timedelta(seconds=5) + async def async_setup_entry( hass: HomeAssistant, diff --git a/homeassistant/components/blebox/config_flow.py b/homeassistant/components/blebox/config_flow.py index 17dffe154d1..5ae975f83d9 100644 --- a/homeassistant/components/blebox/config_flow.py +++ b/homeassistant/components/blebox/config_flow.py @@ -1,8 +1,8 @@ """Config flow for BleBox devices integration.""" import logging +from blebox_uniapi.box import Box from blebox_uniapi.error import Error, UnsupportedBoxVersion -from blebox_uniapi.products import Products from blebox_uniapi.session import ApiHost import voluptuous as vol @@ -65,7 +65,6 @@ class BleBoxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, step, exception, schema, host, port, message_id, log_fn ): """Handle step exceptions.""" - log_fn("%s at %s:%d (%s)", LOG_MSG[message_id], host, port, exception) return self.async_show_form( @@ -101,9 +100,8 @@ class BleBoxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): websession = async_get_clientsession(hass) api_host = ApiHost(*addr, DEFAULT_SETUP_TIMEOUT, websession, hass.loop, _LOGGER) - try: - product = await Products.async_from_host(api_host) + product = await Box.async_from_host(api_host) except UnsupportedBoxVersion as ex: return self.handle_step_exception( diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index a582fe133c1..32cc6360db1 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -1,23 +1,34 @@ """BleBox light entities implementation.""" +from __future__ import annotations + +from datetime import timedelta import logging from blebox_uniapi.error import BadOnValueError +import blebox_uniapi.light +from blebox_uniapi.light import BleboxColorMode from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_RGB_COLOR, ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, ColorMode, LightEntity, + LightEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util.color import color_rgb_to_hex, rgb_hex_to_rgb_list from . import BleBoxEntity, create_blebox_entities _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(seconds=5) + async def async_setup_entry( hass: HomeAssistant, @@ -31,6 +42,17 @@ async def async_setup_entry( ) +COLOR_MODE_MAP = { + BleboxColorMode.RGBW: ColorMode.RGBW, + BleboxColorMode.RGB: ColorMode.RGB, + BleboxColorMode.MONO: ColorMode.BRIGHTNESS, + BleboxColorMode.RGBorW: ColorMode.RGBW, # white hex is prioritised over RGB channel + BleboxColorMode.CT: ColorMode.COLOR_TEMP, + BleboxColorMode.CTx2: ColorMode.COLOR_TEMP, # two instances + BleboxColorMode.RGBWW: ColorMode.RGBWW, +} + + class BleBoxLightEntity(BleBoxEntity, LightEntity): """Representation of BleBox lights.""" @@ -38,6 +60,7 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity): """Initialize a BleBox light.""" super().__init__(feature) self._attr_supported_color_modes = {self.color_mode} + self._attr_supported_features = LightEntityFeature.EFFECT @property def is_on(self) -> bool: @@ -49,46 +72,105 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity): """Return the name.""" return self._feature.brightness + @property + def color_temp(self): + """Return color temperature.""" + return self._feature.color_temp + @property def color_mode(self): - """Return the color mode.""" - if self._feature.supports_white and self._feature.supports_color: - return ColorMode.RGBW - if self._feature.supports_brightness: - return ColorMode.BRIGHTNESS - return ColorMode.ONOFF + """Return the color mode. + + Set values to _attr_ibutes if needed. + """ + color_mode_tmp = COLOR_MODE_MAP.get(self._feature.color_mode, ColorMode.ONOFF) + if color_mode_tmp == ColorMode.COLOR_TEMP: + self._attr_min_mireds = 1 + self._attr_max_mireds = 255 + + return color_mode_tmp + + @property + def effect_list(self) -> list[str] | None: + """Return the list of supported effects.""" + return self._feature.effect_list + + @property + def effect(self) -> str | None: + """Return the current effect.""" + return self._feature.effect + + @property + def rgb_color(self): + """Return value for rgb.""" + if (rgb_hex := self._feature.rgb_hex) is None: + return None + return tuple( + blebox_uniapi.light.Light.normalise_elements_of_rgb( + blebox_uniapi.light.Light.rgb_hex_to_rgb_list(rgb_hex)[0:3] + ) + ) @property def rgbw_color(self): """Return the hue and saturation.""" if (rgbw_hex := self._feature.rgbw_hex) is None: return None + return tuple(blebox_uniapi.light.Light.rgb_hex_to_rgb_list(rgbw_hex)[0:4]) - return tuple(rgb_hex_to_rgb_list(rgbw_hex)[0:4]) + @property + def rgbww_color(self): + """Return value for rgbww.""" + if (rgbww_hex := self._feature.rgbww_hex) is None: + return None + return tuple(blebox_uniapi.light.Light.rgb_hex_to_rgb_list(rgbww_hex)) async def async_turn_on(self, **kwargs): """Turn the light on.""" rgbw = kwargs.get(ATTR_RGBW_COLOR) brightness = kwargs.get(ATTR_BRIGHTNESS) - + effect = kwargs.get(ATTR_EFFECT) + color_temp = kwargs.get(ATTR_COLOR_TEMP) + rgbww = kwargs.get(ATTR_RGBWW_COLOR) feature = self._feature value = feature.sensible_on_value - - if brightness is not None: - value = feature.apply_brightness(value, brightness) + rgb = kwargs.get(ATTR_RGB_COLOR) if rgbw is not None: - value = feature.apply_white(value, rgbw[3]) - value = feature.apply_color(value, color_rgb_to_hex(*rgbw[0:3])) - - try: - await self._feature.async_on(value) - except BadOnValueError as ex: - _LOGGER.error( - "Turning on '%s' failed: Bad value %s (%s)", self.name, value, ex + value = list(rgbw) + if color_temp is not None: + value = feature.return_color_temp_with_brightness( + int(color_temp), self.brightness ) + if rgbww is not None: + value = list(rgbww) + + if rgb is not None: + if self.color_mode == ColorMode.RGB and brightness is None: + brightness = self.brightness + value = list(rgb) + + if brightness is not None: + if self.color_mode == ATTR_COLOR_TEMP: + value = feature.return_color_temp_with_brightness( + self.color_temp, brightness + ) + else: + value = feature.apply_brightness(value, brightness) + + if effect is not None: + effect_value = self.effect_list.index(effect) + await self._feature.async_api_command("effect", effect_value) + else: + try: + await self._feature.async_on(value) + except BadOnValueError as ex: + _LOGGER.error( + "Turning on '%s' failed: Bad value %s (%s)", self.name, value, ex + ) + async def async_turn_off(self, **kwargs): """Turn the light off.""" await self._feature.async_off() diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index d9c0481fff6..5c57d5f6b9f 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -3,8 +3,8 @@ "name": "BleBox devices", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blebox", - "requirements": ["blebox_uniapi==1.3.3"], - "codeowners": ["@bbx-a", "@bbx-jp"], + "requirements": ["blebox_uniapi==2.0.0"], + "codeowners": ["@bbx-a", "@bbx-jp", "@riokuu"], "iot_class": "local_polling", "loggers": ["blebox_uniapi"] } diff --git a/homeassistant/components/blebox/switch.py b/homeassistant/components/blebox/switch.py index 9586b37558f..50eba1d2c4a 100644 --- a/homeassistant/components/blebox/switch.py +++ b/homeassistant/components/blebox/switch.py @@ -1,4 +1,6 @@ """BleBox switch implementation.""" +from datetime import timedelta + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -7,6 +9,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BleBoxEntity, create_blebox_entities from .const import BLEBOX_TO_HASS_DEVICE_CLASSES +SCAN_INTERVAL = timedelta(seconds=5) + async def async_setup_entry( hass: HomeAssistant, diff --git a/requirements_all.txt b/requirements_all.txt index 11ed4a2113a..58d8cfb604a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -399,7 +399,7 @@ bimmer_connected==0.9.6 bizkaibus==0.1.1 # homeassistant.components.blebox -blebox_uniapi==1.3.3 +blebox_uniapi==2.0.0 # homeassistant.components.blink blinkpy==0.19.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc7366f151b..af336175e95 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -311,7 +311,7 @@ bellows==0.31.0 bimmer_connected==0.9.6 # homeassistant.components.blebox -blebox_uniapi==1.3.3 +blebox_uniapi==2.0.0 # homeassistant.components.blink blinkpy==0.19.0 diff --git a/tests/components/blebox/conftest.py b/tests/components/blebox/conftest.py index a63a0090c3a..548c7a5dc38 100644 --- a/tests/components/blebox/conftest.py +++ b/tests/components/blebox/conftest.py @@ -16,12 +16,11 @@ from tests.components.light.conftest import mock_light_profiles # noqa: F401 def patch_product_identify(path=None, **kwargs): """Patch the blebox_uniapi Products class.""" - if path is None: - path = "homeassistant.components.blebox.Products" - patcher = patch(path, mock.DEFAULT, blebox_uniapi.products.Products, True, True) - products_class = patcher.start() - products_class.async_from_host = AsyncMock(**kwargs) - return products_class + patcher = patch.object( + blebox_uniapi.box.Box, "async_from_host", AsyncMock(**kwargs) + ) + patcher.start() + return blebox_uniapi.box.Box def setup_product_mock(category, feature_mocks, path=None): @@ -84,7 +83,6 @@ async def async_setup_entities(hass, config, entity_ids): config_entry.add_to_hass(hass) assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() - entity_registry = er.async_get(hass) return [entity_registry.async_get(entity_id) for entity_id in entity_ids] diff --git a/tests/components/blebox/test_config_flow.py b/tests/components/blebox/test_config_flow.py index 03f5d0b4f2a..3f40880abf7 100644 --- a/tests/components/blebox/test_config_flow.py +++ b/tests/components/blebox/test_config_flow.py @@ -77,8 +77,8 @@ async def test_flow_works(hass, valid_feature_mock, flow_feature_mock): @pytest.fixture(name="product_class_mock") def product_class_mock_fixture(): """Return a mocked feature.""" - path = "homeassistant.components.blebox.config_flow.Products" - patcher = patch(path, DEFAULT, blebox_uniapi.products.Products, True, True) + path = "homeassistant.components.blebox.config_flow.Box" + patcher = patch(path, DEFAULT, blebox_uniapi.box.Box, True, True) yield patcher diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index a546424e14b..4663c216136 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -39,6 +39,8 @@ def dimmer_fixture(): is_on=True, supports_color=False, supports_white=False, + color_mode=blebox_uniapi.light.BleboxColorMode.MONO, + effect_list=None, ) product = feature.product type(product).name = PropertyMock(return_value="My dimmer") @@ -210,6 +212,8 @@ def wlightboxs_fixture(): is_on=None, supports_color=False, supports_white=False, + color_mode=blebox_uniapi.light.BleboxColorMode.MONO, + effect_list=["NONE", "PL", "RELAX"], ) product = feature.product type(product).name = PropertyMock(return_value="My wLightBoxS") @@ -310,6 +314,9 @@ def wlightbox_fixture(): supports_white=True, white_value=None, rgbw_hex=None, + color_mode=blebox_uniapi.light.BleboxColorMode.RGBW, + effect="NONE", + effect_list=["NONE", "PL", "POLICE"], ) product = feature.product type(product).name = PropertyMock(return_value="My wLightBox") @@ -379,7 +386,7 @@ async def test_wlightbox_on_rgbw(wlightbox, hass, config): def turn_on(value): feature_mock.is_on = True - assert value == "c1d2f3c7" + assert value == [193, 210, 243, 199] feature_mock.white_value = 0xC7 # on feature_mock.rgbw_hex = "c1d2f3c7" From c186a73e572137e96b832d50a72bb670fdac8620 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 29 Jun 2022 12:01:09 +0200 Subject: [PATCH 1953/3516] Tweak speed util (#74160) --- homeassistant/util/speed.py | 20 ++++++++++---------- tests/util/test_speed.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/util/speed.py b/homeassistant/util/speed.py index f3fc652e90f..14b28bde676 100644 --- a/homeassistant/util/speed.py +++ b/homeassistant/util/speed.py @@ -15,27 +15,27 @@ from homeassistant.const import ( ) VALID_UNITS: tuple[str, ...] = ( - SPEED_METERS_PER_SECOND, - SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, - SPEED_MILLIMETERS_PER_DAY, SPEED_INCHES_PER_DAY, SPEED_INCHES_PER_HOUR, + SPEED_KILOMETERS_PER_HOUR, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + SPEED_MILLIMETERS_PER_DAY, ) HRS_TO_SECS = 60 * 60 # 1 hr = 3600 seconds +IN_TO_M = 0.0254 KM_TO_M = 1000 # 1 km = 1000 m -KM_TO_MILE = 0.62137119 # 1 km = 0.62137119 mi -M_TO_IN = 39.3700787 # 1 m = 39.3700787 in +MILE_TO_M = 1609.344 # Units in terms of m/s UNIT_CONVERSION: dict[str, float] = { - SPEED_METERS_PER_SECOND: 1, + SPEED_INCHES_PER_DAY: (24 * HRS_TO_SECS) / IN_TO_M, + SPEED_INCHES_PER_HOUR: HRS_TO_SECS / IN_TO_M, SPEED_KILOMETERS_PER_HOUR: HRS_TO_SECS / KM_TO_M, - SPEED_MILES_PER_HOUR: HRS_TO_SECS * KM_TO_MILE / KM_TO_M, + SPEED_METERS_PER_SECOND: 1, + SPEED_MILES_PER_HOUR: HRS_TO_SECS / MILE_TO_M, SPEED_MILLIMETERS_PER_DAY: (24 * HRS_TO_SECS) * 1000, - SPEED_INCHES_PER_DAY: (24 * HRS_TO_SECS) * M_TO_IN, - SPEED_INCHES_PER_HOUR: HRS_TO_SECS * M_TO_IN, } diff --git a/tests/util/test_speed.py b/tests/util/test_speed.py index 7f52c67ed50..f0a17e6ae15 100644 --- a/tests/util/test_speed.py +++ b/tests/util/test_speed.py @@ -59,7 +59,7 @@ def test_convert_nonnumeric_value(): (5, SPEED_INCHES_PER_HOUR, 3048, SPEED_MILLIMETERS_PER_DAY), # 5 m/s * 39.3701 in/m * 3600 s/hr = 708661 (5, SPEED_METERS_PER_SECOND, 708661, SPEED_INCHES_PER_HOUR), - # 5000 in/hr / 39.3701 in/m / 3600 s/hr = 0.03528 m/s + # 5000 in/h / 39.3701 in/m / 3600 s/h = 0.03528 m/s (5000, SPEED_INCHES_PER_HOUR, 0.03528, SPEED_METERS_PER_SECOND), ], ) From 21e765207cf225a0e61fd93964917642498353f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Wed, 29 Jun 2022 12:03:32 +0200 Subject: [PATCH 1954/3516] Bump pyatv to 0.10.2 (#74119) --- homeassistant/components/apple_tv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index 26a8e2737c8..dec195fddee 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -3,7 +3,7 @@ "name": "Apple TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", - "requirements": ["pyatv==0.10.0"], + "requirements": ["pyatv==0.10.2"], "dependencies": ["zeroconf"], "zeroconf": [ "_mediaremotetv._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 58d8cfb604a..f63ff73eccf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1381,7 +1381,7 @@ pyatmo==6.2.4 pyatome==0.1.1 # homeassistant.components.apple_tv -pyatv==0.10.0 +pyatv==0.10.2 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af336175e95..1e25b245d52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -938,7 +938,7 @@ pyatag==0.3.5.3 pyatmo==6.2.4 # homeassistant.components.apple_tv -pyatv==0.10.0 +pyatv==0.10.2 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 From 21d28dd35629a7f4fc086bf9ff4f65ee9270873b Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 29 Jun 2022 20:13:33 +1000 Subject: [PATCH 1955/3516] Migrate usgs_earthquakes_feed to async library (#68370) * use new async integration library * migrate to new async integration library * updated unit tests * updated logger * fix tests and improve test coverage * fix test * fix requirements * time control to fix tests --- .../usgs_earthquakes_feed/geo_location.py | 68 +++++++++++-------- .../usgs_earthquakes_feed/manifest.json | 4 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- .../test_geo_location.py | 33 +++++---- 5 files changed, 66 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/usgs_earthquakes_feed/geo_location.py b/homeassistant/components/usgs_earthquakes_feed/geo_location.py index d26f97b295d..6e3eb9b2337 100644 --- a/homeassistant/components/usgs_earthquakes_feed/geo_location.py +++ b/homeassistant/components/usgs_earthquakes_feed/geo_location.py @@ -4,9 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging -from geojson_client.usgs_earthquake_hazards_program_feed import ( - UsgsEarthquakeHazardsProgramFeedManager, -) +from aio_geojson_usgs_earthquakes import UsgsEarthquakeHazardsProgramFeedManager import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent @@ -21,10 +19,14 @@ from homeassistant.const import ( LENGTH_KILOMETERS, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -87,10 +89,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the USGS Earthquake Hazards Program Feed platform.""" @@ -103,21 +105,22 @@ def setup_platform( radius_in_km = config[CONF_RADIUS] minimum_magnitude = config[CONF_MINIMUM_MAGNITUDE] # Initialize the entity manager. - feed = UsgsEarthquakesFeedEntityManager( + manager = UsgsEarthquakesFeedEntityManager( hass, - add_entities, + async_add_entities, scan_interval, coordinates, feed_type, radius_in_km, minimum_magnitude, ) + await manager.async_init() - def start_feed_manager(event): + async def start_feed_manager(event=None): """Start feed manager.""" - feed.startup() + await manager.async_update() - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_feed_manager) class UsgsEarthquakesFeedEntityManager: @@ -126,7 +129,7 @@ class UsgsEarthquakesFeedEntityManager: def __init__( self, hass, - add_entities, + async_add_entities, scan_interval, coordinates, feed_type, @@ -136,7 +139,9 @@ class UsgsEarthquakesFeedEntityManager: """Initialize the Feed Entity Manager.""" self._hass = hass + websession = aiohttp_client.async_get_clientsession(hass) self._feed_manager = UsgsEarthquakeHazardsProgramFeedManager( + websession, self._generate_entity, self._update_entity, self._remove_entity, @@ -145,37 +150,42 @@ class UsgsEarthquakesFeedEntityManager: filter_radius=radius_in_km, filter_minimum_magnitude=minimum_magnitude, ) - self._add_entities = add_entities + self._async_add_entities = async_add_entities self._scan_interval = scan_interval - def startup(self): - """Start up this manager.""" - self._feed_manager.update() - self._init_regular_updates() + async def async_init(self): + """Schedule initial and regular updates based on configured time interval.""" - def _init_regular_updates(self): - """Schedule regular updates at the specified interval.""" - track_time_interval( - self._hass, lambda now: self._feed_manager.update(), self._scan_interval - ) + async def update(event_time): + """Update.""" + await self.async_update() + + # Trigger updates at regular intervals. + async_track_time_interval(self._hass, update, self._scan_interval) + _LOGGER.debug("Feed entity manager initialized") + + async def async_update(self): + """Refresh data.""" + await self._feed_manager.update() + _LOGGER.debug("Feed entity manager updated") def get_entry(self, external_id): """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - def _generate_entity(self, external_id): + async def _generate_entity(self, external_id): """Generate new entity.""" new_entity = UsgsEarthquakesEvent(self, external_id) # Add new entities to HA. - self._add_entities([new_entity], True) + self._async_add_entities([new_entity], True) - def _update_entity(self, external_id): + async def _update_entity(self, external_id): """Update entity.""" - dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) + async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) - def _remove_entity(self, external_id): + async def _remove_entity(self, external_id): """Remove entity.""" - dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) + async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) class UsgsEarthquakesEvent(GeolocationEvent): diff --git a/homeassistant/components/usgs_earthquakes_feed/manifest.json b/homeassistant/components/usgs_earthquakes_feed/manifest.json index 9c1f4566dc3..bd8ec9633bd 100644 --- a/homeassistant/components/usgs_earthquakes_feed/manifest.json +++ b/homeassistant/components/usgs_earthquakes_feed/manifest.json @@ -2,8 +2,8 @@ "domain": "usgs_earthquakes_feed", "name": "U.S. Geological Survey Earthquake Hazards (USGS)", "documentation": "https://www.home-assistant.io/integrations/usgs_earthquakes_feed", - "requirements": ["geojson_client==0.6"], + "requirements": ["aio_geojson_usgs_earthquakes==0.1"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", - "loggers": ["geojson_client"] + "loggers": ["aio_geojson_usgs_earthquakes"] } diff --git a/requirements_all.txt b/requirements_all.txt index f63ff73eccf..d758df0a97c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -106,6 +106,9 @@ aio_geojson_geonetnz_volcano==0.6 # homeassistant.components.nsw_rural_fire_service_feed aio_geojson_nsw_rfs_incidents==0.4 +# homeassistant.components.usgs_earthquakes_feed +aio_geojson_usgs_earthquakes==0.1 + # homeassistant.components.gdacs aio_georss_gdacs==0.7 @@ -699,9 +702,6 @@ geniushub-client==0.6.30 # homeassistant.components.geocaching geocachingapi==0.2.1 -# homeassistant.components.usgs_earthquakes_feed -geojson_client==0.6 - # homeassistant.components.aprs geopy==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e25b245d52..c2cf31965f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -93,6 +93,9 @@ aio_geojson_geonetnz_volcano==0.6 # homeassistant.components.nsw_rural_fire_service_feed aio_geojson_nsw_rfs_incidents==0.4 +# homeassistant.components.usgs_earthquakes_feed +aio_geojson_usgs_earthquakes==0.1 + # homeassistant.components.gdacs aio_georss_gdacs==0.7 @@ -499,9 +502,6 @@ gcal-sync==0.10.0 # homeassistant.components.geocaching geocachingapi==0.2.1 -# homeassistant.components.usgs_earthquakes_feed -geojson_client==0.6 - # homeassistant.components.aprs geopy==2.1.0 diff --git a/tests/components/usgs_earthquakes_feed/test_geo_location.py b/tests/components/usgs_earthquakes_feed/test_geo_location.py index ee845701d81..b8fcbbcbe7d 100644 --- a/tests/components/usgs_earthquakes_feed/test_geo_location.py +++ b/tests/components/usgs_earthquakes_feed/test_geo_location.py @@ -1,6 +1,9 @@ """The tests for the USGS Earthquake Hazards Program Feed platform.""" import datetime -from unittest.mock import MagicMock, call, patch +from unittest.mock import ANY, MagicMock, call, patch + +from aio_geojson_usgs_earthquakes import UsgsEarthquakeHazardsProgramFeed +from freezegun import freeze_time from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE @@ -111,11 +114,10 @@ async def test_setup(hass): # Patching 'utcnow' to gain more control over the timed update. utcnow = dt_util.utcnow() - with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( - "geojson_client.usgs_earthquake_hazards_program_feed." - "UsgsEarthquakeHazardsProgramFeed" - ) as mock_feed: - mock_feed.return_value.update.return_value = ( + with freeze_time(utcnow), patch( + "aio_geojson_client.feed.GeoJsonFeed.update" + ) as mock_feed_update: + mock_feed_update.return_value = ( "OK", [mock_entry_1, mock_entry_2, mock_entry_3], ) @@ -184,9 +186,9 @@ async def test_setup(hass): } assert round(abs(float(state.state) - 25.5), 7) == 0 - # Simulate an update - one existing, one new entry, + # Simulate an update - two existing, one new entry, # one outdated entry - mock_feed.return_value.update.return_value = ( + mock_feed_update.return_value = ( "OK", [mock_entry_1, mock_entry_4, mock_entry_3], ) @@ -198,7 +200,7 @@ async def test_setup(hass): # Simulate an update - empty data, but successful update, # so no changes to entities. - mock_feed.return_value.update.return_value = "OK_NO_DATA", None + mock_feed_update.return_value = "OK_NO_DATA", None async_fire_time_changed(hass, utcnow + 2 * SCAN_INTERVAL) await hass.async_block_till_done() @@ -206,7 +208,7 @@ async def test_setup(hass): assert len(all_states) == 3 # Simulate an update - empty data, removes all entities - mock_feed.return_value.update.return_value = "ERROR", None + mock_feed_update.return_value = "ERROR", None async_fire_time_changed(hass, utcnow + 3 * SCAN_INTERVAL) await hass.async_block_till_done() @@ -220,10 +222,12 @@ async def test_setup_with_custom_location(hass): mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 20.5, (-31.1, 150.1)) with patch( - "geojson_client.usgs_earthquake_hazards_program_feed." - "UsgsEarthquakeHazardsProgramFeed" - ) as mock_feed: - mock_feed.return_value.update.return_value = "OK", [mock_entry_1] + "aio_geojson_usgs_earthquakes.feed_manager.UsgsEarthquakeHazardsProgramFeed", + wraps=UsgsEarthquakeHazardsProgramFeed, + ) as mock_feed, patch( + "aio_geojson_client.feed.GeoJsonFeed.update" + ) as mock_feed_update: + mock_feed_update.return_value = "OK", [mock_entry_1] with assert_setup_component(1, geo_location.DOMAIN): assert await async_setup_component( @@ -240,6 +244,7 @@ async def test_setup_with_custom_location(hass): assert len(all_states) == 1 assert mock_feed.call_args == call( + ANY, (15.1, 25.2), "past_hour_m25_earthquakes", filter_minimum_magnitude=0.0, From 29a546f4e8a85cdfa83bf46ef8a9077127861343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 29 Jun 2022 12:29:47 +0200 Subject: [PATCH 1956/3516] Remove deprecated YAML import for Tautulli (#74172) --- .../components/tautulli/config_flow.py | 54 ++----------------- homeassistant/components/tautulli/const.py | 4 -- homeassistant/components/tautulli/sensor.py | 42 +-------------- tests/components/tautulli/test_config_flow.py | 36 +------------ 4 files changed, 7 insertions(+), 129 deletions(-) diff --git a/homeassistant/components/tautulli/config_flow.py b/homeassistant/components/tautulli/config_flow.py index d70384c5485..f06405825c9 100644 --- a/homeassistant/components/tautulli/config_flow.py +++ b/homeassistant/components/tautulli/config_flow.py @@ -4,36 +4,15 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any -from pytautulli import ( - PyTautulli, - PyTautulliException, - PyTautulliHostConfiguration, - exceptions, -) +from pytautulli import PyTautulli, PyTautulliException, exceptions import voluptuous as vol -from homeassistant.components.sensor import _LOGGER from homeassistant.config_entries import ConfigFlow -from homeassistant.const import ( - CONF_API_KEY, - CONF_HOST, - CONF_PATH, - CONF_PORT, - CONF_SSL, - CONF_URL, - CONF_VERIFY_SSL, -) +from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import ( - DEFAULT_NAME, - DEFAULT_PATH, - DEFAULT_PORT, - DEFAULT_SSL, - DEFAULT_VERIFY_SSL, - DOMAIN, -) +from .const import DEFAULT_NAME, DOMAIN class TautulliConfigFlow(ConfigFlow, domain=DOMAIN): @@ -95,33 +74,6 @@ class TautulliConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Import a config entry from configuration.yaml.""" - _LOGGER.warning( - "Configuration of the Tautulli platform in YAML is deprecated and will be " - "removed in Home Assistant 2022.6; Your existing configuration for host %s" - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file", - config[CONF_HOST], - ) - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - host_configuration = PyTautulliHostConfiguration( - config[CONF_API_KEY], - ipaddress=config[CONF_HOST], - port=config.get(CONF_PORT, DEFAULT_PORT), - ssl=config.get(CONF_SSL, DEFAULT_SSL), - verify_ssl=config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL), - base_api_path=config.get(CONF_PATH, DEFAULT_PATH), - ) - return await self.async_step_user( - { - CONF_API_KEY: host_configuration.api_token, - CONF_URL: host_configuration.base_url, - CONF_VERIFY_SSL: host_configuration.verify_ssl, - } - ) - async def validate_input(self, user_input: dict[str, Any]) -> str | None: """Try connecting to Tautulli.""" try: diff --git a/homeassistant/components/tautulli/const.py b/homeassistant/components/tautulli/const.py index 49b86ec6ef7..c0ca923c3e5 100644 --- a/homeassistant/components/tautulli/const.py +++ b/homeassistant/components/tautulli/const.py @@ -5,9 +5,5 @@ ATTR_TOP_USER = "top_user" CONF_MONITORED_USERS = "monitored_users" DEFAULT_NAME = "Tautulli" -DEFAULT_PATH = "" -DEFAULT_PORT = "8181" -DEFAULT_SSL = False -DEFAULT_VERIFY_SSL = True DOMAIN = "tautulli" LOGGER: Logger = getLogger(__package__) diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py index 5981992c946..1d5efde7cc7 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -11,61 +11,23 @@ from pytautulli import ( PyTautulliApiSession, PyTautulliApiUser, ) -import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorEntity, SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_API_KEY, - CONF_HOST, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - CONF_PATH, - CONF_PORT, - CONF_SSL, - CONF_VERIFY_SSL, - DATA_KILOBITS, - PERCENTAGE, -) +from homeassistant.const import DATA_KILOBITS, PERCENTAGE from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import EntityCategory, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from . import TautulliEntity -from .const import ( - ATTR_TOP_USER, - CONF_MONITORED_USERS, - DEFAULT_NAME, - DEFAULT_PATH, - DEFAULT_PORT, - DEFAULT_SSL, - DEFAULT_VERIFY_SSL, - DOMAIN, -) +from .const import ATTR_TOP_USER, DOMAIN from .coordinator import TautulliDataUpdateCoordinator -# Deprecated in Home Assistant 2022.4 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_MONITORED_USERS): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, - vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - } -) - def get_top_stats( home_stats: PyTautulliApiHomeStats, activity: PyTautulliApiActivity, key: str diff --git a/tests/components/tautulli/test_config_flow.py b/tests/components/tautulli/test_config_flow.py index 95ccfbaa9b7..d37e4401275 100644 --- a/tests/components/tautulli/test_config_flow.py +++ b/tests/components/tautulli/test_config_flow.py @@ -2,11 +2,10 @@ from unittest.mock import AsyncMock, patch from pytautulli import exceptions -from pytest import LogCaptureFixture from homeassistant import data_entry_flow -from homeassistant.components.tautulli.const import DEFAULT_NAME, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER +from homeassistant.components.tautulli.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_SOURCE from homeassistant.core import HomeAssistant @@ -127,37 +126,6 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: assert result2["data"] == CONF_DATA -async def test_flow_import(hass: HomeAssistant, caplog: LogCaptureFixture) -> None: - """Test import step.""" - with patch_config_flow_tautulli(AsyncMock()): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=CONF_IMPORT_DATA, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == DEFAULT_NAME - assert result["data"] == CONF_DATA - assert "Tautulli platform in YAML" in caplog.text - - -async def test_flow_import_single_instance_allowed(hass: HomeAssistant) -> None: - """Test import step single instance allowed.""" - entry = MockConfigEntry(domain=DOMAIN, data=CONF_DATA) - entry.add_to_hass(hass) - - with patch_config_flow_tautulli(AsyncMock()): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=CONF_IMPORT_DATA, - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "single_instance_allowed" - - async def test_flow_reauth( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: From fd89108483e958b8e5328a29af887f312c9009aa Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 29 Jun 2022 12:31:50 +0200 Subject: [PATCH 1957/3516] Move add/remove logic of deCONZ groups to gateway class (#73952) --- homeassistant/components/deconz/gateway.py | 53 +++++++++++++--------- homeassistant/components/deconz/light.py | 31 ++----------- 2 files changed, 36 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index c94b2c6d86d..25471d1448a 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any, cast import async_timeout from pydeconz import DeconzSession, errors from pydeconz.interfaces.api import APIItems, GroupedAPIItems +from pydeconz.interfaces.groups import Groups from pydeconz.models.event import EventType from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry @@ -59,18 +60,18 @@ class DeconzGateway: self.ignore_state_updates = False self.signal_reachable = f"deconz-reachable-{config_entry.entry_id}" - self.signal_reload_groups = f"deconz_reload_group_{config_entry.entry_id}" self.signal_reload_clip_sensors = f"deconz_reload_clip_{config_entry.entry_id}" self.deconz_ids: dict[str, str] = {} self.entities: dict[str, set[str]] = {} self.events: list[DeconzAlarmEvent | DeconzEvent] = [] self.ignored_devices: set[tuple[Callable[[EventType, str], None], str]] = set() + self.deconz_groups: set[tuple[Callable[[EventType, str], None], str]] = set() - self._option_allow_deconz_groups = self.config_entry.options.get( + self.option_allow_deconz_groups = config_entry.options.get( CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS ) - self.option_allow_new_devices = self.config_entry.options.get( + self.option_allow_new_devices = config_entry.options.get( CONF_ALLOW_NEW_DEVICES, DEFAULT_ALLOW_NEW_DEVICES ) @@ -98,13 +99,6 @@ class DeconzGateway: CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR ) - @property - def option_allow_deconz_groups(self) -> bool: - """Allow loading deCONZ groups from gateway.""" - return self.config_entry.options.get( - CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS - ) - @callback def register_platform_add_device_callback( self, @@ -113,16 +107,28 @@ class DeconzGateway: ) -> None: """Wrap add_device_callback to check allow_new_devices option.""" - def async_add_device(event: EventType, device_id: str) -> None: + initializing = True + + def async_add_device(_: EventType, device_id: str) -> None: """Add device or add it to ignored_devices set. If ignore_state_updates is True means device_refresh service is used. Device_refresh is expected to load new devices. """ - if not self.option_allow_new_devices and not self.ignore_state_updates: + if ( + not initializing + and not self.option_allow_new_devices + and not self.ignore_state_updates + ): self.ignored_devices.add((async_add_device, device_id)) return - add_device_callback(event, device_id) + + if isinstance(deconz_device_interface, Groups): + self.deconz_groups.add((async_add_device, device_id)) + if not self.option_allow_deconz_groups: + return + + add_device_callback(EventType.ADDED, device_id) self.config_entry.async_on_unload( deconz_device_interface.subscribe( @@ -132,7 +138,9 @@ class DeconzGateway: ) for device_id in deconz_device_interface: - add_device_callback(EventType.ADDED, device_id) + async_add_device(EventType.ADDED, device_id) + + initializing = False @callback def load_ignored_devices(self) -> None: @@ -216,13 +224,16 @@ class DeconzGateway: # Allow Groups - if self.option_allow_deconz_groups: - if not self._option_allow_deconz_groups: - async_dispatcher_send(self.hass, self.signal_reload_groups) - else: - deconz_ids += [group.deconz_id for group in self.api.groups.values()] - - self._option_allow_deconz_groups = self.option_allow_deconz_groups + option_allow_deconz_groups = self.config_entry.options.get( + CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS + ) + if option_allow_deconz_groups != self.option_allow_deconz_groups: + self.option_allow_deconz_groups = option_allow_deconz_groups + if option_allow_deconz_groups: + for add_device, device_id in self.deconz_groups: + add_device(EventType.ADDED, device_id) + else: + deconz_ids += [group.deconz_id for group in self.api.groups.values()] # Allow adding new devices diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 0cca007742a..669800e2662 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -33,7 +33,6 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.color import color_hs_to_xy @@ -109,11 +108,7 @@ async def async_setup_entry( Update group states based on its sum of related lights. """ - if ( - not gateway.option_allow_deconz_groups - or (group := gateway.api.groups[group_id]) - and not group.lights - ): + if (group := gateway.api.groups[group_id]) and not group.lights: return first = True @@ -128,29 +123,11 @@ async def async_setup_entry( async_add_entities([DeconzGroup(group, gateway)]) - config_entry.async_on_unload( - gateway.api.groups.subscribe( - async_add_group, - EventType.ADDED, - ) + gateway.register_platform_add_device_callback( + async_add_group, + gateway.api.groups, ) - @callback - def async_load_groups() -> None: - """Load deCONZ groups.""" - for group_id in gateway.api.groups: - async_add_group(EventType.ADDED, group_id) - - config_entry.async_on_unload( - async_dispatcher_connect( - hass, - gateway.signal_reload_groups, - async_load_groups, - ) - ) - - async_load_groups() - class DeconzBaseLight(Generic[_L], DeconzDevice, LightEntity): """Representation of a deCONZ light.""" From a6ef330b63eaa1b683c4940b8df66183ac46eb66 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 29 Jun 2022 12:36:57 +0200 Subject: [PATCH 1958/3516] Add ButtonEntity to pylint checks (#74171) --- pylint/plugins/hass_enforce_type_hints.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index f08e6d932e7..c6e2fd1a553 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -697,6 +697,26 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "button": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="ButtonEntity", + matches=[ + TypeHintMatch( + function_name="device_class", + return_type=["ButtonDeviceClass", "str", None], + ), + TypeHintMatch( + function_name="press", + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ], "cover": [ ClassTypeHintMatch( base_class="Entity", From f6f7fa1c2d0dfb8fd25c93f72f8ad89305c88f1d Mon Sep 17 00:00:00 2001 From: Khole Date: Wed, 29 Jun 2022 11:39:35 +0100 Subject: [PATCH 1959/3516] Add Hive power usage sensor (#74011) --- homeassistant/components/hive/sensor.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index bab8648407a..5bac23fdb3d 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -5,9 +5,10 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE +from homeassistant.const import PERCENTAGE, POWER_KILO_WATT from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -23,6 +24,12 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, ), + SensorEntityDescription( + key="Power", + native_unit_of_measurement=POWER_KILO_WATT, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER, + ), ) From e6daed971927836a3940dd19aada689b50d5ba20 Mon Sep 17 00:00:00 2001 From: Frank <46161394+BraveChicken1@users.noreply.github.com> Date: Wed, 29 Jun 2022 12:45:55 +0200 Subject: [PATCH 1960/3516] Add support for services to Home Connect (#58768) Co-authored-by: Erik Montnemery --- .../components/home_connect/__init__.py | 209 +++++++++++++++++- homeassistant/components/home_connect/api.py | 1 + .../components/home_connect/const.py | 12 + .../components/home_connect/entity.py | 1 + .../components/home_connect/services.yaml | 169 ++++++++++++++ 5 files changed, 388 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/home_connect/services.yaml diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index 345eeeddaaa..f57c7aeb8af 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -1,4 +1,5 @@ """Support for BSH Home Connect appliances.""" +from __future__ import annotations from datetime import timedelta import logging @@ -11,14 +12,39 @@ from homeassistant.components.application_credentials import ( async_import_client_credential, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform +from homeassistant.const import ( + ATTR_DEVICE_ID, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_DEVICE, + Platform, +) from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv +from homeassistant.helpers import ( + config_entry_oauth2_flow, + config_validation as cv, + device_registry as dr, +) from homeassistant.helpers.typing import ConfigType from homeassistant.util import Throttle from . import api -from .const import DOMAIN +from .const import ( + ATTR_KEY, + ATTR_PROGRAM, + ATTR_UNIT, + ATTR_VALUE, + BSH_PAUSE, + BSH_RESUME, + DOMAIN, + SERVICE_OPTION_ACTIVE, + SERVICE_OPTION_SELECTED, + SERVICE_PAUSE_PROGRAM, + SERVICE_RESUME_PROGRAM, + SERVICE_SELECT_PROGRAM, + SERVICE_SETTING, + SERVICE_START_PROGRAM, +) _LOGGER = logging.getLogger(__name__) @@ -39,9 +65,55 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +SERVICE_SETTING_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): str, + vol.Required(ATTR_KEY): str, + vol.Required(ATTR_VALUE): vol.Any(str, int, bool), + } +) + +SERVICE_OPTION_SCHEMA = vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): str, + vol.Required(ATTR_KEY): str, + vol.Required(ATTR_VALUE): vol.Any(str, int, bool), + vol.Optional(ATTR_UNIT): str, + } +) + +SERVICE_PROGRAM_SCHEMA = vol.Any( + { + vol.Required(ATTR_DEVICE_ID): str, + vol.Required(ATTR_PROGRAM): str, + vol.Required(ATTR_KEY): str, + vol.Required(ATTR_VALUE): vol.Any(int, str), + vol.Optional(ATTR_UNIT): str, + }, + { + vol.Required(ATTR_DEVICE_ID): str, + vol.Required(ATTR_PROGRAM): str, + }, +) + +SERVICE_COMMAND_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_ID): str}) + PLATFORMS = [Platform.BINARY_SENSOR, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH] +def _get_appliance_by_device_id( + hass: HomeAssistant, device_id: str +) -> api.HomeConnectDevice | None: + """Return a Home Connect appliance instance given an device_id.""" + for hc_api in hass.data[DOMAIN].values(): + for dev_dict in hc_api.devices: + device = dev_dict[CONF_DEVICE] + if device.device_id == device_id: + return device.appliance + _LOGGER.error("Appliance for device id %s not found", device_id) + return None + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Home Connect component.""" hass.data[DOMAIN] = {} @@ -65,6 +137,121 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "configuration.yaml file" ) + async def _async_service_program(call, method): + """Execute calls to services taking a program.""" + program = call.data[ATTR_PROGRAM] + device_id = call.data[ATTR_DEVICE_ID] + options = { + ATTR_KEY: call.data.get(ATTR_KEY), + ATTR_VALUE: call.data.get(ATTR_VALUE), + ATTR_UNIT: call.data.get(ATTR_UNIT), + } + + appliance = _get_appliance_by_device_id(hass, device_id) + if appliance is not None: + await hass.async_add_executor_job( + getattr(appliance, method), program, options + ) + + async def _async_service_command(call, command): + """Execute calls to services executing a command.""" + device_id = call.data[ATTR_DEVICE_ID] + + appliance = _get_appliance_by_device_id(hass, device_id) + if appliance is not None: + await hass.async_add_executor_job(appliance.execute_command, command) + + async def _async_service_key_value(call, method): + """Execute calls to services taking a key and value.""" + key = call.data[ATTR_KEY] + value = call.data[ATTR_VALUE] + unit = call.data.get(ATTR_UNIT) + device_id = call.data[ATTR_DEVICE_ID] + + appliance = _get_appliance_by_device_id(hass, device_id) + if appliance is not None: + if unit is not None: + await hass.async_add_executor_job( + getattr(appliance, method), + key, + value, + unit, + ) + else: + await hass.async_add_executor_job( + getattr(appliance, method), + key, + value, + ) + + async def async_service_option_active(call): + """Service for setting an option for an active program.""" + await _async_service_key_value(call, "set_options_active_program") + + async def async_service_option_selected(call): + """Service for setting an option for a selected program.""" + await _async_service_key_value(call, "set_options_selected_program") + + async def async_service_setting(call): + """Service for changing a setting.""" + await _async_service_key_value(call, "set_setting") + + async def async_service_pause_program(call): + """Service for pausing a program.""" + await _async_service_command(call, BSH_PAUSE) + + async def async_service_resume_program(call): + """Service for resuming a paused program.""" + await _async_service_command(call, BSH_RESUME) + + async def async_service_select_program(call): + """Service for selecting a program.""" + await _async_service_program(call, "select_program") + + async def async_service_start_program(call): + """Service for starting a program.""" + await _async_service_program(call, "start_program") + + hass.services.async_register( + DOMAIN, + SERVICE_OPTION_ACTIVE, + async_service_option_active, + schema=SERVICE_OPTION_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_OPTION_SELECTED, + async_service_option_selected, + schema=SERVICE_OPTION_SCHEMA, + ) + hass.services.async_register( + DOMAIN, SERVICE_SETTING, async_service_setting, schema=SERVICE_SETTING_SCHEMA + ) + hass.services.async_register( + DOMAIN, + SERVICE_PAUSE_PROGRAM, + async_service_pause_program, + schema=SERVICE_COMMAND_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_RESUME_PROGRAM, + async_service_resume_program, + schema=SERVICE_COMMAND_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_SELECT_PROGRAM, + async_service_select_program, + schema=SERVICE_PROGRAM_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_START_PROGRAM, + async_service_start_program, + schema=SERVICE_PROGRAM_SCHEMA, + ) + return True @@ -101,9 +288,23 @@ async def update_all_devices(hass, entry): """Update all the devices.""" data = hass.data[DOMAIN] hc_api = data[entry.entry_id] + + device_registry = dr.async_get(hass) try: await hass.async_add_executor_job(hc_api.get_devices) for device_dict in hc_api.devices: - await hass.async_add_executor_job(device_dict["device"].initialize) + device = device_dict["device"] + + device_entry = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, device.appliance.haId)}, + name=device.appliance.name, + manufacturer=device.appliance.brand, + model=device.appliance.vib, + ) + + device.device_id = device_entry.id + + await hass.async_add_executor_job(device.initialize) except HTTPError as err: _LOGGER.warning("Cannot update devices: %s", err.response.status_code) diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py index f3c98e618b8..00d759b47d5 100644 --- a/homeassistant/components/home_connect/api.py +++ b/homeassistant/components/home_connect/api.py @@ -113,6 +113,7 @@ class HomeConnectDevice: """Initialize the device class.""" self.hass = hass self.appliance = appliance + self.entities = [] def initialize(self): """Fetch the info needed to initialize the device.""" diff --git a/homeassistant/components/home_connect/const.py b/homeassistant/components/home_connect/const.py index 438ee5ace16..9eabc9b5d43 100644 --- a/homeassistant/components/home_connect/const.py +++ b/homeassistant/components/home_connect/const.py @@ -30,12 +30,24 @@ BSH_DOOR_STATE_CLOSED = "BSH.Common.EnumType.DoorState.Closed" BSH_DOOR_STATE_LOCKED = "BSH.Common.EnumType.DoorState.Locked" BSH_DOOR_STATE_OPEN = "BSH.Common.EnumType.DoorState.Open" +BSH_PAUSE = "BSH.Common.Command.PauseProgram" +BSH_RESUME = "BSH.Common.Command.ResumeProgram" + SIGNAL_UPDATE_ENTITIES = "home_connect.update_entities" +SERVICE_OPTION_ACTIVE = "set_option_active" +SERVICE_OPTION_SELECTED = "set_option_selected" +SERVICE_PAUSE_PROGRAM = "pause_program" +SERVICE_RESUME_PROGRAM = "resume_program" +SERVICE_SELECT_PROGRAM = "select_program" +SERVICE_SETTING = "change_setting" +SERVICE_START_PROGRAM = "start_program" + ATTR_AMBIENT = "ambient" ATTR_DESC = "desc" ATTR_DEVICE = "device" ATTR_KEY = "key" +ATTR_PROGRAM = "program" ATTR_SENSOR_TYPE = "sensor_type" ATTR_SIGN = "sign" ATTR_UNIT = "unit" diff --git a/homeassistant/components/home_connect/entity.py b/homeassistant/components/home_connect/entity.py index b27988f997d..60a0c3974cd 100644 --- a/homeassistant/components/home_connect/entity.py +++ b/homeassistant/components/home_connect/entity.py @@ -20,6 +20,7 @@ class HomeConnectEntity(Entity): self.device = device self.desc = desc self._name = f"{self.device.appliance.name} {desc}" + self.device.entities.append(self) async def async_added_to_hass(self): """Register callbacks.""" diff --git a/homeassistant/components/home_connect/services.yaml b/homeassistant/components/home_connect/services.yaml new file mode 100644 index 00000000000..06a646dd481 --- /dev/null +++ b/homeassistant/components/home_connect/services.yaml @@ -0,0 +1,169 @@ +start_program: + name: Start program + description: Selects a program and starts it. + fields: + device_id: + name: Device ID + description: Id of the device. + required: true + selector: + device: + integration: home_connect + program: + name: Program + description: Program to select + example: "Dishcare.Dishwasher.Program.Auto2" + required: true + selector: + text: + key: + name: Option key + description: Key of the option. + example: "BSH.Common.Option.StartInRelative" + selector: + text: + value: + name: Option value + description: Value of the option. + example: 1800 + selector: + object: + unit: + name: Option unit + description: Unit for the option. + example: "seconds" + selector: + text: +select_program: + name: Select program + description: Selects a program without starting it. + fields: + device_id: + name: Device ID + description: Id of the device. + required: true + selector: + device: + integration: home_connect + program: + name: Program + description: Program to select + example: "Dishcare.Dishwasher.Program.Auto2" + required: true + selector: + text: + key: + name: Option key + description: Key of the option. + example: "BSH.Common.Option.StartInRelative" + selector: + text: + value: + name: Option value + description: Value of the option. + example: 1800 + selector: + object: + unit: + name: Option unit + description: Unit for the option. + example: "seconds" + selector: + text: +pause_program: + name: Pause program + description: Pauses the current running program. + fields: + device_id: + name: Device ID + description: Id of the device. + required: true + selector: + device: + integration: home_connect +resume_program: + name: Resume program + description: Resumes a paused program. + fields: + device_id: + name: Device ID + description: Id of the device. + required: true + selector: + device: + integration: home_connect +set_option_active: + name: Set active program option + description: Sets an option for the active program. + fields: + device_id: + name: Device ID + description: Id of the device. + required: true + selector: + device: + integration: home_connect + key: + name: Key + description: Key of the option. + example: "LaundryCare.Dryer.Option.DryingTarget" + required: true + selector: + text: + value: + name: Value + description: Value of the option. + example: "LaundryCare.Dryer.EnumType.DryingTarget.IronDry" + required: true + selector: + object: +set_option_selected: + name: Set selected program option + description: Sets an option for the selected program. + fields: + device_id: + name: Device ID + description: Id of the device. + required: true + selector: + device: + integration: home_connect + key: + name: Key + description: Key of the option. + example: "LaundryCare.Dryer.Option.DryingTarget" + required: true + selector: + text: + value: + name: Value + description: Value of the option. + example: "LaundryCare.Dryer.EnumType.DryingTarget.IronDry" + required: true + selector: + object: +change_setting: + name: Change setting + description: Changes a setting. + fields: + device_id: + name: Device ID + description: Id of the device. + required: true + selector: + device: + integration: home_connect + key: + name: Key + description: Key of the setting. + example: "BSH.Common.Setting.ChildLock" + required: true + selector: + text: + value: + name: Value + description: Value of the setting. + example: "true" + required: true + selector: + object: From 5167535b03d4184983071b9c9050018d62a9ca0b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 29 Jun 2022 12:51:37 +0200 Subject: [PATCH 1961/3516] Add LightEntity type hint checks to pylint plugin (#73826) --- pylint/plugins/hass_enforce_type_hints.py | 104 ++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index c6e2fd1a553..5f4f641cbae 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -893,6 +893,110 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "light": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="ToggleEntity", + matches=_TOGGLE_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="LightEntity", + matches=[ + TypeHintMatch( + function_name="brightness", + return_type=["int", None], + ), + TypeHintMatch( + function_name="color_mode", + return_type=["ColorMode", "str", None], + ), + TypeHintMatch( + function_name="hs_color", + return_type=["tuple[float, float]", None], + ), + TypeHintMatch( + function_name="xy_color", + return_type=["tuple[float, float]", None], + ), + TypeHintMatch( + function_name="rgb_color", + return_type=["tuple[int, int, int]", None], + ), + TypeHintMatch( + function_name="rgbw_color", + return_type=["tuple[int, int, int, int]", None], + ), + TypeHintMatch( + function_name="rgbww_color", + return_type=["tuple[int, int, int, int, int]", None], + ), + TypeHintMatch( + function_name="color_temp", + return_type=["int", None], + ), + TypeHintMatch( + function_name="min_mireds", + return_type="int", + ), + TypeHintMatch( + function_name="max_mireds", + return_type="int", + ), + TypeHintMatch( + function_name="white_value", + return_type=["int", None], + ), + TypeHintMatch( + function_name="effect_list", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="effect", + return_type=["str", None], + ), + TypeHintMatch( + function_name="capability_attributes", + return_type=["dict[str, Any]", None], + ), + TypeHintMatch( + function_name="supported_color_modes", + return_type=["set[ColorMode]", "set[str]", None], + ), + TypeHintMatch( + function_name="supported_features", + return_type="int", + ), + TypeHintMatch( + function_name="turn_on", + named_arg_types={ + "brightness": "int | None", + "brightness_pct": "float | None", + "brightness_step": "int | None", + "brightness_step_pct": "float | None", + "color_name": "str | None", + "color_temp": "int | None", + "effect": "str | None", + "flash": "str | None", + "kelvin": "int | None", + "hs_color": "tuple[float, float] | None", + "rgb_color": "tuple[int, int, int] | None", + "rgbw_color": "tuple[int, int, int, int] | None", + "rgbww_color": "tuple[int, int, int, int, int] | None", + "transition": "float | None", + "xy_color": "tuple[float, float] | None", + "white": "int | None", + "white_value": "int | None", + }, + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ], "lock": [ ClassTypeHintMatch( base_class="Entity", From e32694c146bec200c6583f48959d058899581db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Sv=C3=A4rd?= Date: Wed, 29 Jun 2022 12:53:55 +0200 Subject: [PATCH 1962/3516] Make SolarEdge energy value validation a bit less aggressive (#69998) * Make energy value validation a bit less aggressive Attempt to solve issue 69600 introduced by previous fix for issue 59285. - Introduce a tolerance factor for energy value validation. - Only skip update the specific invalid energy entity. An energy entity with invalid values will now show "State unknown". * Remove the tolerance factor. Let's just ignore the specific invalid energy entity. --- .../components/solaredge/coordinator.py | 6 ++++-- tests/components/solaredge/test_coordinator.py | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/solaredge/coordinator.py b/homeassistant/components/solaredge/coordinator.py index 4e93571f8a4..fe8f2f86a8e 100644 --- a/homeassistant/components/solaredge/coordinator.py +++ b/homeassistant/components/solaredge/coordinator.py @@ -94,8 +94,10 @@ class SolarEdgeOverviewDataService(SolarEdgeDataService): for index, key in enumerate(energy_keys, start=1): # All coming values in list should be larger than the current value. if any(self.data[k] > self.data[key] for k in energy_keys[index:]): - self.data = {} - raise UpdateFailed("Invalid energy values, skipping update") + LOGGER.info( + "Ignoring invalid energy value %s for %s", self.data[key], key + ) + self.data.pop(key) LOGGER.debug("Updated SolarEdge overview: %s", self.data) diff --git a/tests/components/solaredge/test_coordinator.py b/tests/components/solaredge/test_coordinator.py index b3c9227648e..eb5d033f112 100644 --- a/tests/components/solaredge/test_coordinator.py +++ b/tests/components/solaredge/test_coordinator.py @@ -6,8 +6,9 @@ from homeassistant.components.solaredge.const import ( DEFAULT_NAME, DOMAIN, OVERVIEW_UPDATE_DELAY, + SENSOR_TYPES, ) -from homeassistant.const import CONF_API_KEY, CONF_NAME, STATE_UNAVAILABLE +from homeassistant.const import CONF_API_KEY, CONF_NAME, STATE_UNKNOWN from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util @@ -29,6 +30,9 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( ) mock_solaredge().get_details.return_value = {"details": {"status": "active"}} mock_config_entry.add_to_hass(hass) + for description in SENSOR_TYPES: + description.entity_registry_enabled_default = True + await hass.config_entries.async_setup(mock_config_entry.entry_id) # Valid energy values update @@ -56,7 +60,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( state = hass.states.get("sensor.solaredge_lifetime_energy") assert state - assert state.state == STATE_UNAVAILABLE + assert state.state == STATE_UNKNOWN # New valid energy values update mock_overview_data["overview"]["lifeTimeData"]["energy"] = 100001 @@ -74,9 +78,13 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( async_fire_time_changed(hass, dt_util.utcnow() + OVERVIEW_UPDATE_DELAY) await hass.async_block_till_done() - state = hass.states.get("sensor.solaredge_lifetime_energy") + state = hass.states.get("sensor.solaredge_energy_this_year") assert state - assert state.state == STATE_UNAVAILABLE + assert state.state == STATE_UNKNOWN + # Check that the valid lastMonthData is still available + state = hass.states.get("sensor.solaredge_energy_this_month") + assert state + assert state.state == str(mock_overview_data["overview"]["lastMonthData"]["energy"]) # All zero energy values should also be valid. mock_overview_data["overview"]["lifeTimeData"]["energy"] = 0.0 From 4bdec1589d8a54ca8f8ba1059f4e40d6e956a6df Mon Sep 17 00:00:00 2001 From: beren12 Date: Wed, 29 Jun 2022 07:10:48 -0400 Subject: [PATCH 1963/3516] Ambient sensors are not diagnostic/internal (#73928) --- homeassistant/components/nut/const.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 9f6b43974b7..64dc95d7b95 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -555,8 +555,6 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, ), "ambient.temperature": SensorEntityDescription( key="ambient.temperature", @@ -564,8 +562,6 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, ), "watts": SensorEntityDescription( key="watts", From 9392f599136f9e30767751393c97cca62c15e350 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Wed, 29 Jun 2022 07:56:02 -0400 Subject: [PATCH 1964/3516] Trigger Alexa routines from toggles and buttons (#67889) --- .../components/alexa/capabilities.py | 10 ++- homeassistant/components/alexa/entities.py | 8 ++- tests/components/alexa/test_capabilities.py | 51 +++++++++++++++ tests/components/alexa/test_smart_home.py | 62 +++++++++++++++++-- 4 files changed, 122 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 5b675779a22..818b4b794cf 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -4,9 +4,11 @@ from __future__ import annotations import logging from homeassistant.components import ( + button, cover, fan, image_processing, + input_button, input_number, light, timer, @@ -1891,7 +1893,10 @@ class AlexaEventDetectionSensor(AlexaCapability): if self.entity.domain == image_processing.DOMAIN: if int(state): human_presence = "DETECTED" - elif state == STATE_ON: + elif state == STATE_ON or self.entity.domain in [ + input_button.DOMAIN, + button.DOMAIN, + ]: human_presence = "DETECTED" return {"value": human_presence} @@ -1903,7 +1908,8 @@ class AlexaEventDetectionSensor(AlexaCapability): "detectionModes": { "humanPresence": { "featureAvailability": "ENABLED", - "supportsNotDetected": True, + "supportsNotDetected": self.entity.domain + not in [input_button.DOMAIN, button.DOMAIN], } }, } diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 9ee4ad3411f..f380f990449 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -382,7 +382,6 @@ def async_get_entities(hass, config) -> list[AlexaEntity]: @ENTITY_ADAPTERS.register(alert.DOMAIN) @ENTITY_ADAPTERS.register(automation.DOMAIN) @ENTITY_ADAPTERS.register(group.DOMAIN) -@ENTITY_ADAPTERS.register(input_boolean.DOMAIN) class GenericCapabilities(AlexaEntity): """A generic, on/off device. @@ -405,12 +404,16 @@ class GenericCapabilities(AlexaEntity): ] +@ENTITY_ADAPTERS.register(input_boolean.DOMAIN) @ENTITY_ADAPTERS.register(switch.DOMAIN) class SwitchCapabilities(AlexaEntity): """Class to represent Switch capabilities.""" def default_display_categories(self): """Return the display categories for this entity.""" + if self.entity.domain == input_boolean.DOMAIN: + return [DisplayCategory.OTHER] + device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) if device_class == switch.SwitchDeviceClass.OUTLET: return [DisplayCategory.SMARTPLUG] @@ -421,6 +424,7 @@ class SwitchCapabilities(AlexaEntity): """Yield the supported interfaces.""" return [ AlexaPowerController(self.entity), + AlexaContactSensor(self.hass, self.entity), AlexaEndpointHealth(self.hass, self.entity), Alexa(self.hass), ] @@ -439,6 +443,8 @@ class ButtonCapabilities(AlexaEntity): """Yield the supported interfaces.""" return [ AlexaSceneController(self.entity, supports_deactivation=False), + AlexaEventDetectionSensor(self.hass, self.entity), + AlexaEndpointHealth(self.hass, self.entity), Alexa(self.hass), ] diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 4cccae1f083..3e176b0fb8c 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -846,6 +846,57 @@ async def test_report_image_processing(hass): ) +@pytest.mark.parametrize("domain", ["button", "input_button"]) +async def test_report_button_pressed(hass, domain): + """Test button presses report human presence detection events to trigger routines.""" + hass.states.async_set( + f"{domain}.test_button", "now", {"friendly_name": "Test button"} + ) + + properties = await reported_properties(hass, f"{domain}#test_button") + properties.assert_equal( + "Alexa.EventDetectionSensor", + "humanPresenceDetectionState", + {"value": "DETECTED"}, + ) + + +@pytest.mark.parametrize("domain", ["switch", "input_boolean"]) +async def test_toggle_entities_report_contact_events(hass, domain): + """Test toggles and switches report contact sensor events to trigger routines.""" + hass.states.async_set( + f"{domain}.test_toggle", "on", {"friendly_name": "Test toggle"} + ) + + properties = await reported_properties(hass, f"{domain}#test_toggle") + properties.assert_equal( + "Alexa.PowerController", + "powerState", + "ON", + ) + properties.assert_equal( + "Alexa.ContactSensor", + "detectionState", + "DETECTED", + ) + + hass.states.async_set( + f"{domain}.test_toggle", "off", {"friendly_name": "Test toggle"} + ) + + properties = await reported_properties(hass, f"{domain}#test_toggle") + properties.assert_equal( + "Alexa.PowerController", + "powerState", + "OFF", + ) + properties.assert_equal( + "Alexa.ContactSensor", + "detectionState", + "NOT_DETECTED", + ) + + async def test_get_property_blowup(hass, caplog): """Test we handle a property blowing up.""" hass.states.async_set( diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 37888a2c415..0169eeff9d5 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -182,8 +182,12 @@ async def test_switch(hass, events): assert appliance["endpointId"] == "switch#test" assert appliance["displayCategories"][0] == "SWITCH" assert appliance["friendlyName"] == "Test switch" - assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PowerController", + "Alexa.ContactSensor", + "Alexa.EndpointHealth", + "Alexa", ) await assert_power_controller_works( @@ -192,6 +196,14 @@ async def test_switch(hass, events): properties = await reported_properties(hass, "switch#test") properties.assert_equal("Alexa.PowerController", "powerState", "ON") + properties.assert_equal("Alexa.ContactSensor", "detectionState", "DETECTED") + properties.assert_equal("Alexa.EndpointHealth", "connectivity", {"value": "OK"}) + + contact_sensor_capability = get_capability(capabilities, "Alexa.ContactSensor") + assert contact_sensor_capability is not None + properties = contact_sensor_capability["properties"] + assert properties["retrievable"] is True + assert {"name": "detectionState"} in properties["supported"] async def test_outlet(hass, events): @@ -207,7 +219,11 @@ async def test_outlet(hass, events): assert appliance["displayCategories"][0] == "SMARTPLUG" assert appliance["friendlyName"] == "Test switch" assert_endpoint_capabilities( - appliance, "Alexa", "Alexa.PowerController", "Alexa.EndpointHealth" + appliance, + "Alexa", + "Alexa.PowerController", + "Alexa.EndpointHealth", + "Alexa.ContactSensor", ) @@ -335,8 +351,12 @@ async def test_input_boolean(hass): assert appliance["endpointId"] == "input_boolean#test" assert appliance["displayCategories"][0] == "OTHER" assert appliance["friendlyName"] == "Test input boolean" - assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PowerController", + "Alexa.ContactSensor", + "Alexa.EndpointHealth", + "Alexa", ) await assert_power_controller_works( @@ -347,6 +367,17 @@ async def test_input_boolean(hass): "2022-04-19T07:53:05Z", ) + properties = await reported_properties(hass, "input_boolean#test") + properties.assert_equal("Alexa.PowerController", "powerState", "OFF") + properties.assert_equal("Alexa.ContactSensor", "detectionState", "NOT_DETECTED") + properties.assert_equal("Alexa.EndpointHealth", "connectivity", {"value": "OK"}) + + contact_sensor_capability = get_capability(capabilities, "Alexa.ContactSensor") + assert contact_sensor_capability is not None + properties = contact_sensor_capability["properties"] + assert properties["retrievable"] is True + assert {"name": "detectionState"} in properties["supported"] + @freeze_time("2022-04-19 07:53:05") async def test_scene(hass): @@ -4003,7 +4034,11 @@ async def test_button(hass, domain): assert appliance["friendlyName"] == "Ring Doorbell" capabilities = assert_endpoint_capabilities( - appliance, "Alexa.SceneController", "Alexa" + appliance, + "Alexa.SceneController", + "Alexa.EventDetectionSensor", + "Alexa.EndpointHealth", + "Alexa", ) scene_capability = get_capability(capabilities, "Alexa.SceneController") assert scene_capability["supportsDeactivation"] is False @@ -4016,6 +4051,21 @@ async def test_button(hass, domain): "2022-04-19T07:53:05Z", ) + event_detection_capability = get_capability( + capabilities, "Alexa.EventDetectionSensor" + ) + assert event_detection_capability is not None + properties = event_detection_capability["properties"] + assert properties["proactivelyReported"] is True + assert not properties["retrievable"] + assert {"name": "humanPresenceDetectionState"} in properties["supported"] + assert ( + event_detection_capability["configuration"]["detectionModes"]["humanPresence"][ + "supportsNotDetected" + ] + is False + ) + async def test_api_message_sets_authorized(hass): """Test an incoming API messages sets the authorized flag.""" From 46b4be5b41c22751dc45de869e4c5057ac502915 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Wed, 29 Jun 2022 13:24:50 +0100 Subject: [PATCH 1965/3516] Add boot time sensor to System Bridge (#73039) * Add boot time to System Bridge * Update homeassistant/components/system_bridge/sensor.py Co-authored-by: Paulus Schoutsen * Add missing import * Update homeassistant/components/system_bridge/sensor.py Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen Co-authored-by: Franck Nijhof --- homeassistant/components/system_bridge/sensor.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/system_bridge/sensor.py b/homeassistant/components/system_bridge/sensor.py index b37ff66896e..bdfe5047e56 100644 --- a/homeassistant/components/system_bridge/sensor.py +++ b/homeassistant/components/system_bridge/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Final, cast from homeassistant.components.sensor import ( @@ -125,6 +125,15 @@ def memory_used(data: SystemBridgeCoordinatorData) -> float | None: BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = ( + SystemBridgeSensorEntityDescription( + key="boot_time", + name="Boot Time", + device_class=SensorDeviceClass.TIMESTAMP, + icon="mdi:av-timer", + value=lambda data: datetime.fromtimestamp( + data.system.boot_time, tz=timezone.utc + ), + ), SystemBridgeSensorEntityDescription( key="cpu_speed", name="CPU Speed", From 329ecc74c4ed47385e8625e485b2d1a14fc8f7e0 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 29 Jun 2022 08:23:22 -0500 Subject: [PATCH 1966/3516] Optimize Sonos join behavior when using `media_player.join` (#74174) Optimize Sonos media_player.join service --- .../components/sonos/media_player.py | 19 +++++++------------ homeassistant/components/sonos/speaker.py | 4 ++-- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 7b18b102919..c68110d9763 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -763,19 +763,14 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): async def async_join_players(self, group_members): """Join `group_members` as a player group with the current player.""" - async with self.hass.data[DATA_SONOS].topology_condition: - speakers = [] - for entity_id in group_members: - if speaker := self.hass.data[DATA_SONOS].entity_id_mappings.get( - entity_id - ): - speakers.append(speaker) - else: - raise HomeAssistantError( - f"Not a known Sonos entity_id: {entity_id}" - ) + speakers = [] + for entity_id in group_members: + if speaker := self.hass.data[DATA_SONOS].entity_id_mappings.get(entity_id): + speakers.append(speaker) + else: + raise HomeAssistantError(f"Not a known Sonos entity_id: {entity_id}") - await self.hass.async_add_executor_job(self.speaker.join, speakers) + await SonosSpeaker.join_multi(self.hass, self.speaker, speakers) async def async_unjoin_player(self): """Remove this player from any group. diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index f4d1d89aa2f..0c5bec06dfb 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -883,9 +883,9 @@ class SonosSpeaker: for speaker in speakers: if speaker.soco.uid != self.soco.uid: - speaker.soco.join(self.soco) - speaker.coordinator = self if speaker not in group: + speaker.soco.join(self.soco) + speaker.coordinator = self group.append(speaker) return group From 8905e6f7266e54ffbc4273da7240440b44f20971 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Wed, 29 Jun 2022 16:32:29 +0300 Subject: [PATCH 1967/3516] Use DataUpdateCoordinator for `mikrotik` (#72954) --- homeassistant/components/mikrotik/__init__.py | 9 +- .../components/mikrotik/device_tracker.py | 46 ++-------- homeassistant/components/mikrotik/hub.py | 52 +++-------- .../mikrotik/test_device_tracker.py | 4 +- tests/components/mikrotik/test_hub.py | 72 +-------------- tests/components/mikrotik/test_init.py | 92 +++++++++---------- 6 files changed, 81 insertions(+), 194 deletions(-) diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index 25aa2eb1468..856495dc0f2 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -4,7 +4,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, device_registry as dr from .const import ATTR_MANUFACTURER, DOMAIN, PLATFORMS -from .hub import MikrotikHub +from .hub import MikrotikDataUpdateCoordinator CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @@ -12,11 +12,16 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up the Mikrotik component.""" - hub = MikrotikHub(hass, config_entry) + hub = MikrotikDataUpdateCoordinator(hass, config_entry) if not await hub.async_setup(): return False + await hub.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = hub + + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 16c3ed233d8..9389d3bea5c 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -1,8 +1,6 @@ """Support for Mikrotik routers as device tracker.""" from __future__ import annotations -import logging - from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import ( DOMAIN as DEVICE_TRACKER, @@ -11,13 +9,12 @@ from homeassistant.components.device_tracker.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity import homeassistant.util.dt as dt_util from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .hub import MikrotikDataUpdateCoordinator # These are normalized to ATTR_IP and ATTR_MAC to conform # to device_tracker @@ -32,7 +29,7 @@ async def async_setup_entry( """Set up device tracker for Mikrotik component.""" hub = hass.data[DOMAIN][config_entry.entry_id] - tracked: dict[str, MikrotikHubTracker] = {} + tracked: dict[str, MikrotikDataUpdateCoordinatorTracker] = {} registry = entity_registry.async_get(hass) @@ -56,7 +53,7 @@ async def async_setup_entry( """Update the status of the device.""" update_items(hub, async_add_entities, tracked) - async_dispatcher_connect(hass, hub.signal_update, update_hub) + config_entry.async_on_unload(hub.async_add_listener(update_hub)) update_hub() @@ -67,21 +64,22 @@ def update_items(hub, async_add_entities, tracked): new_tracked = [] for mac, device in hub.api.devices.items(): if mac not in tracked: - tracked[mac] = MikrotikHubTracker(device, hub) + tracked[mac] = MikrotikDataUpdateCoordinatorTracker(device, hub) new_tracked.append(tracked[mac]) if new_tracked: async_add_entities(new_tracked) -class MikrotikHubTracker(ScannerEntity): +class MikrotikDataUpdateCoordinatorTracker(CoordinatorEntity, ScannerEntity): """Representation of network device.""" + coordinator: MikrotikDataUpdateCoordinator + def __init__(self, device, hub): """Initialize the tracked device.""" + super().__init__(hub) self.device = device - self.hub = hub - self.unsub_dispatcher = None @property def is_connected(self): @@ -89,7 +87,7 @@ class MikrotikHubTracker(ScannerEntity): if ( self.device.last_seen and (dt_util.utcnow() - self.device.last_seen) - < self.hub.option_detection_time + < self.coordinator.option_detection_time ): return True return False @@ -125,33 +123,9 @@ class MikrotikHubTracker(ScannerEntity): """Return a unique identifier for this device.""" return self.device.mac - @property - def available(self) -> bool: - """Return if controller is available.""" - return self.hub.available - @property def extra_state_attributes(self): """Return the device state attributes.""" if self.is_connected: return {k: v for k, v in self.device.attrs.items() if k not in FILTER_ATTRS} return None - - async def async_added_to_hass(self): - """Client entity created.""" - _LOGGER.debug("New network device tracker %s (%s)", self.name, self.unique_id) - self.unsub_dispatcher = async_dispatcher_connect( - self.hass, self.hub.signal_update, self.async_write_ha_state - ) - - async def async_update(self): - """Synchronize state with hub.""" - _LOGGER.debug( - "Updating Mikrotik tracked client %s (%s)", self.entity_id, self.unique_id - ) - await self.hub.request_update() - - async def will_remove_from_hass(self): - """Disconnect from dispatcher.""" - if self.unsub_dispatcher: - self.unsub_dispatcher() diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 63be0a4a358..7f2314bd057 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -9,7 +9,7 @@ from librouteros.login import plain as login_plain, token as login_token from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import slugify import homeassistant.util.dt as dt_util @@ -25,13 +25,13 @@ from .const import ( CONF_FORCE_DHCP, DEFAULT_DETECTION_TIME, DHCP, + DOMAIN, IDENTITY, INFO, IS_CAPSMAN, IS_WIRELESS, MIKROTIK_SERVICES, NAME, - PLATFORMS, WIRELESS, ) from .errors import CannotConnect, LoginError @@ -154,10 +154,8 @@ class MikrotikData: """Connect to hub.""" try: self.api = get_api(self.hass, self.config_entry.data) - self.available = True return True except (LoginError, CannotConnect): - self.available = False return False def get_list_from_interface(self, interface): @@ -194,9 +192,8 @@ class MikrotikData: # get new hub firmware version if updated self.firmware = self.get_info(ATTR_FIRMWARE) - except (CannotConnect, socket.timeout, OSError): - self.available = False - return + except (CannotConnect, socket.timeout, OSError) as err: + raise UpdateFailed from err if not device_list: return @@ -263,7 +260,8 @@ class MikrotikData: socket.timeout, ) as api_error: _LOGGER.error("Mikrotik %s connection error %s", self._host, api_error) - raise CannotConnect from api_error + if not self.connect_to_hub(): + raise CannotConnect from api_error except librouteros.exceptions.ProtocolError as api_error: _LOGGER.warning( "Mikrotik %s failed to retrieve data. cmd=[%s] Error: %s", @@ -275,15 +273,8 @@ class MikrotikData: return response if response else None - def update(self): - """Update device_tracker from Mikrotik API.""" - if (not self.available or not self.api) and not self.connect_to_hub(): - return - _LOGGER.debug("updating network devices for host: %s", self._host) - self.update_devices() - -class MikrotikHub: +class MikrotikDataUpdateCoordinator(DataUpdateCoordinator): """Mikrotik Hub Object.""" def __init__(self, hass, config_entry): @@ -291,7 +282,13 @@ class MikrotikHub: self.hass = hass self.config_entry = config_entry self._mk_data = None - self.progress = None + super().__init__( + self.hass, + _LOGGER, + name=f"{DOMAIN} - {self.host}", + update_method=self.async_update, + update_interval=timedelta(seconds=10), + ) @property def host(self): @@ -328,11 +325,6 @@ class MikrotikHub: """Config entry option defining number of seconds from last seen to away.""" return timedelta(seconds=self.config_entry.options[CONF_DETECTION_TIME]) - @property - def signal_update(self): - """Event specific per Mikrotik entry to signal updates.""" - return f"mikrotik-update-{self.host}" - @property def api(self): """Represent Mikrotik data object.""" @@ -354,21 +346,9 @@ class MikrotikHub: self.config_entry, data=data, options=options ) - async def request_update(self): - """Request an update.""" - if self.progress is not None: - await self.progress - return - - self.progress = self.hass.async_create_task(self.async_update()) - await self.progress - - self.progress = None - async def async_update(self): """Update Mikrotik devices information.""" - await self.hass.async_add_executor_job(self._mk_data.update) - async_dispatcher_send(self.hass, self.signal_update) + await self.hass.async_add_executor_job(self._mk_data.update_devices) async def async_setup(self): """Set up the Mikrotik hub.""" @@ -384,9 +364,7 @@ class MikrotikHub: self._mk_data = MikrotikData(self.hass, self.config_entry, api) await self.async_add_options() await self.hass.async_add_executor_job(self._mk_data.get_hub_details) - await self.hass.async_add_executor_job(self._mk_data.update) - self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) return True diff --git a/tests/components/mikrotik/test_device_tracker.py b/tests/components/mikrotik/test_device_tracker.py index fd2f71b2589..fbbb016d09f 100644 --- a/tests/components/mikrotik/test_device_tracker.py +++ b/tests/components/mikrotik/test_device_tracker.py @@ -90,7 +90,7 @@ async def test_device_trackers(hass, mock_device_registry_devices): # test device_2 is added after connecting to wireless network WIRELESS_DATA.append(DEVICE_2_WIRELESS) - await hub.async_update() + await hub.async_refresh() await hass.async_block_till_done() device_2 = hass.states.get("device_tracker.device_2") @@ -117,7 +117,7 @@ async def test_device_trackers(hass, mock_device_registry_devices): hub.api.devices["00:00:00:00:00:02"]._last_seen = dt_util.utcnow() - timedelta( minutes=5 ) - await hub.async_update() + await hub.async_refresh() await hass.async_block_till_done() device_2 = hass.states.get("device_tracker.device_2") diff --git a/tests/components/mikrotik/test_hub.py b/tests/components/mikrotik/test_hub.py index 2116d73826f..1e056071236 100644 --- a/tests/components/mikrotik/test_hub.py +++ b/tests/components/mikrotik/test_hub.py @@ -1,18 +1,7 @@ """Test Mikrotik hub.""" from unittest.mock import patch -import librouteros - -from homeassistant import config_entries from homeassistant.components import mikrotik -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - CONF_VERIFY_SSL, -) from . import ARP_DATA, DHCP_DATA, MOCK_DATA, MOCK_OPTIONS, WIRELESS_DATA @@ -55,63 +44,6 @@ async def setup_mikrotik_entry(hass, **kwargs): return hass.data[mikrotik.DOMAIN][config_entry.entry_id] -async def test_hub_setup_successful(hass): - """Successful setup of Mikrotik hub.""" - with patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", - return_value=True, - ) as forward_entry_setup: - hub = await setup_mikrotik_entry(hass) - - assert hub.config_entry.data == { - CONF_NAME: "Mikrotik", - CONF_HOST: "0.0.0.0", - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", - CONF_PORT: 8278, - CONF_VERIFY_SSL: False, - } - assert hub.config_entry.options == { - mikrotik.const.CONF_FORCE_DHCP: False, - mikrotik.const.CONF_ARP_PING: False, - mikrotik.const.CONF_DETECTION_TIME: 300, - } - - assert hub.api.available is True - assert hub.signal_update == "mikrotik-update-0.0.0.0" - assert forward_entry_setup.mock_calls[0][1] == (hub.config_entry, "device_tracker") - - -async def test_hub_setup_failed(hass): - """Failed setup of Mikrotik hub.""" - - config_entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=MOCK_DATA) - config_entry.add_to_hass(hass) - # error when connection fails - with patch( - "librouteros.connect", side_effect=librouteros.exceptions.ConnectionClosed - ): - - await hass.config_entries.async_setup(config_entry.entry_id) - - assert config_entry.state is config_entries.ConfigEntryState.SETUP_RETRY - - # error when username or password is invalid - config_entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=MOCK_DATA) - config_entry.add_to_hass(hass) - with patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup" - ) as forward_entry_setup, patch( - "librouteros.connect", - side_effect=librouteros.exceptions.TrapError("invalid user name or password"), - ): - - result = await hass.config_entries.async_setup(config_entry.entry_id) - - assert result is False - assert len(forward_entry_setup.mock_calls) == 0 - - async def test_update_failed(hass): """Test failing to connect during update.""" @@ -120,9 +52,9 @@ async def test_update_failed(hass): with patch.object( mikrotik.hub.MikrotikData, "command", side_effect=mikrotik.errors.CannotConnect ): - await hub.async_update() + await hub.async_refresh() - assert hub.api.available is False + assert not hub.last_update_success async def test_hub_not_support_wireless(hass): diff --git a/tests/components/mikrotik/test_init.py b/tests/components/mikrotik/test_init.py index bc00602789c..5ac408928d8 100644 --- a/tests/components/mikrotik/test_init.py +++ b/tests/components/mikrotik/test_init.py @@ -1,7 +1,12 @@ """Test Mikrotik setup process.""" -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import patch + +from librouteros.exceptions import ConnectionClosed, LibRouterosError +import pytest from homeassistant.components import mikrotik +from homeassistant.components.mikrotik.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.setup import async_setup_component from . import MOCK_DATA @@ -9,6 +14,15 @@ from . import MOCK_DATA from tests.common import MockConfigEntry +@pytest.fixture(autouse=True) +def mock_api(): + """Mock api.""" + with patch("librouteros.create_transport"), patch( + "librouteros.Api.readResponse" + ) as mock_api: + yield mock_api + + async def test_setup_with_no_config(hass): """Test that we do not discover anything or try to set up a hub.""" assert await async_setup_component(hass, mikrotik.DOMAIN, {}) is True @@ -22,37 +36,13 @@ async def test_successful_config_entry(hass): data=MOCK_DATA, ) entry.add_to_hass(hass) - mock_registry = Mock() - with patch.object(mikrotik, "MikrotikHub") as mock_hub, patch( - "homeassistant.components.mikrotik.dr.async_get", - return_value=mock_registry, - ): - mock_hub.return_value.async_setup = AsyncMock(return_value=True) - mock_hub.return_value.serial_num = "12345678" - mock_hub.return_value.model = "RB750" - mock_hub.return_value.hostname = "mikrotik" - mock_hub.return_value.firmware = "3.65" - assert await mikrotik.async_setup_entry(hass, entry) is True - - assert len(mock_hub.mock_calls) == 2 - p_hass, p_entry = mock_hub.mock_calls[0][1] - - assert p_hass is hass - assert p_entry is entry - - assert len(mock_registry.mock_calls) == 1 - assert mock_registry.mock_calls[0][2] == { - "config_entry_id": entry.entry_id, - "connections": {("mikrotik", "12345678")}, - "manufacturer": mikrotik.ATTR_MANUFACTURER, - "model": "RB750", - "name": "mikrotik", - "sw_version": "3.65", - } + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ConfigEntryState.LOADED + assert hass.data[DOMAIN][entry.entry_id] -async def test_hub_fail_setup(hass): +async def test_hub_conn_error(hass, mock_api): """Test that a failed setup will not store the hub.""" entry = MockConfigEntry( domain=mikrotik.DOMAIN, @@ -60,14 +50,29 @@ async def test_hub_fail_setup(hass): ) entry.add_to_hass(hass) - with patch.object(mikrotik, "MikrotikHub") as mock_hub: - mock_hub.return_value.async_setup = AsyncMock(return_value=False) - assert await mikrotik.async_setup_entry(hass, entry) is False + mock_api.side_effect = ConnectionClosed - assert mikrotik.DOMAIN not in hass.data + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_RETRY -async def test_unload_entry(hass): +async def test_hub_auth_error(hass, mock_api): + """Test that a failed setup will not store the hub.""" + entry = MockConfigEntry( + domain=mikrotik.DOMAIN, + data=MOCK_DATA, + ) + entry.add_to_hass(hass) + + mock_api.side_effect = LibRouterosError("invalid user name or password") + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_ERROR + + +async def test_unload_entry(hass) -> None: """Test being able to unload an entry.""" entry = MockConfigEntry( domain=mikrotik.DOMAIN, @@ -75,18 +80,11 @@ async def test_unload_entry(hass): ) entry.add_to_hass(hass) - with patch.object(mikrotik, "MikrotikHub") as mock_hub, patch( - "homeassistant.helpers.device_registry.async_get", - return_value=Mock(), - ): - mock_hub.return_value.async_setup = AsyncMock(return_value=True) - mock_hub.return_value.serial_num = "12345678" - mock_hub.return_value.model = "RB750" - mock_hub.return_value.hostname = "mikrotik" - mock_hub.return_value.firmware = "3.65" - assert await mikrotik.async_setup_entry(hass, entry) is True + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() - assert len(mock_hub.return_value.mock_calls) == 1 + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() - assert await mikrotik.async_unload_entry(hass, entry) - assert entry.entry_id not in hass.data[mikrotik.DOMAIN] + assert entry.state == ConfigEntryState.NOT_LOADED + assert entry.entry_id not in hass.data[DOMAIN] From 8dd5f25da988dc933a95ee176703b887def3047c Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Wed, 29 Jun 2022 15:46:32 +0200 Subject: [PATCH 1968/3516] Add cover tests for devolo_home_control (#72428) --- .coveragerc | 1 - tests/components/devolo_home_control/mocks.py | 26 ++++- .../devolo_home_control/test_cover.py | 103 ++++++++++++++++++ 3 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 tests/components/devolo_home_control/test_cover.py diff --git a/.coveragerc b/.coveragerc index 02c643ae757..eae8060449a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -210,7 +210,6 @@ omit = homeassistant/components/denonavr/media_player.py homeassistant/components/denonavr/receiver.py homeassistant/components/deutsche_bahn/sensor.py - homeassistant/components/devolo_home_control/cover.py homeassistant/components/devolo_home_control/light.py homeassistant/components/devolo_home_control/sensor.py homeassistant/components/devolo_home_control/switch.py diff --git a/tests/components/devolo_home_control/mocks.py b/tests/components/devolo_home_control/mocks.py index b43cb77ad71..e9dae0b70b1 100644 --- a/tests/components/devolo_home_control/mocks.py +++ b/tests/components/devolo_home_control/mocks.py @@ -51,7 +51,6 @@ class MultiLevelSwitchPropertyMock(MultiLevelSwitchProperty): self.element_uid = "Test" self.min = 4 self.max = 24 - self.switch_type = "temperature" self._value = 20 self._logger = MagicMock() @@ -120,9 +119,21 @@ class ClimateMock(DeviceMock): super().__init__() self.device_model_uid = "devolo.model.Room:Thermostat" self.multi_level_switch_property = {"Test": MultiLevelSwitchPropertyMock()} + self.multi_level_switch_property["Test"].switch_type = "temperature" self.multi_level_sensor_property = {"Test": MultiLevelSensorPropertyMock()} +class CoverMock(DeviceMock): + """devolo Home Control cover device mock.""" + + def __init__(self) -> None: + """Initialize the mock.""" + super().__init__() + self.multi_level_switch_property = { + "devolo.Blinds": MultiLevelSwitchPropertyMock() + } + + class RemoteControlMock(DeviceMock): """devolo Home Control remote control device mock.""" @@ -195,6 +206,19 @@ class HomeControlMockClimate(HomeControlMock): self.publisher.unregister = MagicMock() +class HomeControlMockCover(HomeControlMock): + """devolo Home Control gateway mock with cover devices.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize the mock.""" + super().__init__() + self.devices = { + "Test": CoverMock(), + } + self.publisher = Publisher(self.devices.keys()) + self.publisher.unregister = MagicMock() + + class HomeControlMockRemoteControl(HomeControlMock): """devolo Home Control gateway mock with remote control device.""" diff --git a/tests/components/devolo_home_control/test_cover.py b/tests/components/devolo_home_control/test_cover.py new file mode 100644 index 00000000000..1c05c00370b --- /dev/null +++ b/tests/components/devolo_home_control/test_cover.py @@ -0,0 +1,103 @@ +"""Tests for the devolo Home Control cover platform.""" +from unittest.mock import patch + +from homeassistant.components.cover import ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, + STATE_CLOSED, + STATE_OPEN, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant + +from . import configure_integration +from .mocks import HomeControlMock, HomeControlMockCover + + +async def test_cover(hass: HomeAssistant): + """Test setup and state change of a cover device.""" + entry = configure_integration(hass) + test_gateway = HomeControlMockCover() + test_gateway.devices["Test"].multi_level_switch_property["devolo.Blinds"].value = 20 + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test") + assert state is not None + assert state.state == STATE_OPEN + assert ( + state.attributes[ATTR_CURRENT_POSITION] + == test_gateway.devices["Test"] + .multi_level_switch_property["devolo.Blinds"] + .value + ) + + # Emulate websocket message: position changed + test_gateway.publisher.dispatch("Test", ("devolo.Blinds", 0.0)) + await hass.async_block_till_done() + state = hass.states.get(f"{DOMAIN}.test") + assert state.state == STATE_CLOSED + assert state.attributes[ATTR_CURRENT_POSITION] == 0.0 + + # Test setting position + with patch( + "devolo_home_control_api.properties.multi_level_switch_property.MultiLevelSwitchProperty.set" + ) as set_value: + await hass.services.async_call( + DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: f"{DOMAIN}.test"}, + blocking=True, + ) # In reality, this leads to a websocket message like already tested above + set_value.assert_called_once_with(100) + + set_value.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: f"{DOMAIN}.test"}, + blocking=True, + ) # In reality, this leads to a websocket message like already tested above + set_value.assert_called_once_with(0) + + set_value.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: f"{DOMAIN}.test", ATTR_POSITION: 50}, + blocking=True, + ) # In reality, this leads to a websocket message like already tested above + set_value.assert_called_once_with(50) + + # Emulate websocket message: device went offline + test_gateway.devices["Test"].status = 1 + test_gateway.publisher.dispatch("Test", ("Status", False, "status")) + await hass.async_block_till_done() + assert hass.states.get(f"{DOMAIN}.test").state == STATE_UNAVAILABLE + + +async def test_remove_from_hass(hass: HomeAssistant): + """Test removing entity.""" + entry = configure_integration(hass) + test_gateway = HomeControlMockCover() + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test") + assert state is not None + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + assert test_gateway.publisher.unregister.call_count == 1 From f5d8487768cebe137a22c8db0cbe5664c895822e Mon Sep 17 00:00:00 2001 From: Anders Liljekvist Date: Wed, 29 Jun 2022 16:08:58 +0200 Subject: [PATCH 1969/3516] Add send_poll to telegram bot (#68666) --- .../components/telegram_bot/__init__.py | 53 ++++++++++++++++++ .../components/telegram_bot/services.yaml | 54 +++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index 29dbabcbbfe..be1c8325c5f 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -80,6 +80,12 @@ ATTR_VERIFY_SSL = "verify_ssl" ATTR_TIMEOUT = "timeout" ATTR_MESSAGE_TAG = "message_tag" ATTR_CHANNEL_POST = "channel_post" +ATTR_QUESTION = "question" +ATTR_OPTIONS = "options" +ATTR_ANSWERS = "answers" +ATTR_OPEN_PERIOD = "open_period" +ATTR_IS_ANONYMOUS = "is_anonymous" +ATTR_ALLOWS_MULTIPLE_ANSWERS = "allows_multiple_answers" CONF_ALLOWED_CHAT_IDS = "allowed_chat_ids" CONF_PROXY_URL = "proxy_url" @@ -96,6 +102,7 @@ SERVICE_SEND_VIDEO = "send_video" SERVICE_SEND_VOICE = "send_voice" SERVICE_SEND_DOCUMENT = "send_document" SERVICE_SEND_LOCATION = "send_location" +SERVICE_SEND_POLL = "send_poll" SERVICE_EDIT_MESSAGE = "edit_message" SERVICE_EDIT_CAPTION = "edit_caption" SERVICE_EDIT_REPLYMARKUP = "edit_replymarkup" @@ -184,6 +191,19 @@ SERVICE_SCHEMA_SEND_LOCATION = BASE_SERVICE_SCHEMA.extend( } ) +SERVICE_SCHEMA_SEND_POLL = vol.Schema( + { + vol.Optional(ATTR_TARGET): vol.All(cv.ensure_list, [vol.Coerce(int)]), + vol.Required(ATTR_QUESTION): cv.string, + vol.Required(ATTR_OPTIONS): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(ATTR_OPEN_PERIOD): cv.positive_int, + vol.Optional(ATTR_IS_ANONYMOUS, default=True): cv.boolean, + vol.Optional(ATTR_ALLOWS_MULTIPLE_ANSWERS, default=False): cv.boolean, + vol.Optional(ATTR_DISABLE_NOTIF): cv.boolean, + vol.Optional(ATTR_TIMEOUT): cv.positive_int, + } +) + SERVICE_SCHEMA_EDIT_MESSAGE = SERVICE_SCHEMA_SEND_MESSAGE.extend( { vol.Required(ATTR_MESSAGEID): vol.Any( @@ -246,6 +266,7 @@ SERVICE_MAP = { SERVICE_SEND_VOICE: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_DOCUMENT: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_LOCATION: SERVICE_SCHEMA_SEND_LOCATION, + SERVICE_SEND_POLL: SERVICE_SCHEMA_SEND_POLL, SERVICE_EDIT_MESSAGE: SERVICE_SCHEMA_EDIT_MESSAGE, SERVICE_EDIT_CAPTION: SERVICE_SCHEMA_EDIT_CAPTION, SERVICE_EDIT_REPLYMARKUP: SERVICE_SCHEMA_EDIT_REPLYMARKUP, @@ -399,6 +420,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await hass.async_add_executor_job( partial(notify_service.send_location, **kwargs) ) + elif msgtype == SERVICE_SEND_POLL: + await hass.async_add_executor_job( + partial(notify_service.send_poll, **kwargs) + ) elif msgtype == SERVICE_ANSWER_CALLBACK_QUERY: await hass.async_add_executor_job( partial(notify_service.answer_callback_query, **kwargs) @@ -847,6 +872,34 @@ class TelegramNotificationService: timeout=params[ATTR_TIMEOUT], ) + def send_poll( + self, + question, + options, + is_anonymous, + allows_multiple_answers, + target=None, + **kwargs, + ): + """Send a poll.""" + params = self._get_msg_kwargs(kwargs) + openperiod = kwargs.get(ATTR_OPEN_PERIOD) + for chat_id in self._get_target_chat_ids(target): + _LOGGER.debug("Send poll '%s' to chat ID %s", question, chat_id) + self._send_msg( + self.bot.send_poll, + "Error sending poll", + params[ATTR_MESSAGE_TAG], + chat_id=chat_id, + question=question, + options=options, + is_anonymous=is_anonymous, + allows_multiple_answers=allows_multiple_answers, + open_period=openperiod, + disable_notification=params[ATTR_DISABLE_NOTIF], + timeout=params[ATTR_TIMEOUT], + ) + def leave_chat(self, chat_id=None): """Remove bot from chat.""" chat_id = self._get_target_chat_ids(chat_id)[0] diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index 6afd42dffb8..31876bd542d 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -678,6 +678,60 @@ send_location: selector: text: +send_poll: + name: Send poll + description: Send a poll. + fields: + target: + name: Target + description: An array of pre-authorized chat_ids to send the location to. If not present, first allowed chat_id is the default. + example: "[12345, 67890] or 12345" + selector: + object: + question: + name: Question + description: Poll question, 1-300 characters + required: true + selector: + text: + options: + name: Options + description: List of answer options, 2-10 strings 1-100 characters each + required: true + selector: + object: + is_anonymous: + name: Is Anonymous + description: If the poll needs to be anonymous, defaults to True + selector: + boolean: + allows_multiple_answers: + name: Allow Multiple Answers + description: If the poll allows multiple answers, defaults to False + selector: + boolean: + open_period: + name: Open Period + description: Amount of time in seconds the poll will be active after creation, 5-600. + selector: + number: + min: 5 + max: 600 + unit_of_measurement: seconds + disable_notification: + name: Disable notification + description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. + selector: + boolean: + timeout: + name: Timeout + description: Timeout for send poll. Will help with timeout errors (poor internet connection, etc) + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds + edit_message: name: Edit message description: Edit a previously sent message. From e6d115e765f13c0ac33f6100cdf5dcd11f6e6290 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 29 Jun 2022 08:27:34 -0600 Subject: [PATCH 1970/3516] Add time remaining sensors for RainMachine programs (#73878) --- .../components/rainmachine/sensor.py | 171 +++++++++++++----- 1 file changed, 129 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 1144ceea159..7550756f8c4 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -3,10 +3,12 @@ from __future__ import annotations from dataclasses import dataclass from datetime import datetime, timedelta +from typing import Any, cast from regenmaschine.controller import Controller from homeassistant.components.sensor import ( + RestoreSensor, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -24,6 +26,7 @@ from . import RainMachineEntity from .const import ( DATA_CONTROLLER, DATA_COORDINATOR, + DATA_PROGRAMS, DATA_PROVISION_SETTINGS, DATA_RESTRICTIONS_UNIVERSAL, DATA_ZONES, @@ -44,6 +47,7 @@ TYPE_FLOW_SENSOR_CONSUMED_LITERS = "flow_sensor_consumed_liters" TYPE_FLOW_SENSOR_START_INDEX = "flow_sensor_start_index" TYPE_FLOW_SENSOR_WATERING_CLICKS = "flow_sensor_watering_clicks" TYPE_FREEZE_TEMP = "freeze_protect_temp" +TYPE_PROGRAM_RUN_COMPLETION_TIME = "program_run_completion_time" TYPE_ZONE_RUN_COMPLETION_TIME = "zone_run_completion_time" @@ -143,7 +147,26 @@ async def async_setup_entry( ) ] + program_coordinator = coordinators[DATA_PROGRAMS] zone_coordinator = coordinators[DATA_ZONES] + + for uid, program in program_coordinator.data.items(): + sensors.append( + ProgramTimeRemainingSensor( + entry, + program_coordinator, + zone_coordinator, + controller, + RainMachineSensorDescriptionUid( + key=f"{TYPE_PROGRAM_RUN_COMPLETION_TIME}_{uid}", + name=f"{program['name']} Run Completion Time", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + uid=uid, + ), + ) + ) + for uid, zone in zone_coordinator.data.items(): sensors.append( ZoneTimeRemainingSensor( @@ -163,6 +186,106 @@ async def async_setup_entry( async_add_entities(sensors) +class TimeRemainingSensor(RainMachineEntity, RestoreSensor): + """Define a sensor that shows the amount of time remaining for an activity.""" + + entity_description: RainMachineSensorDescriptionUid + + def __init__( + self, + entry: ConfigEntry, + coordinator: DataUpdateCoordinator, + controller: Controller, + description: EntityDescription, + ) -> None: + """Initialize.""" + super().__init__(entry, coordinator, controller, description) + + self._current_run_state: RunStates | None = None + self._previous_run_state: RunStates | None = None + + @property + def activity_data(self) -> dict[str, Any]: + """Return the core data for this entity.""" + return cast(dict[str, Any], self.coordinator.data[self.entity_description.uid]) + + @property + def status_key(self) -> str: + """Return the data key that contains the activity status.""" + return "state" + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + if restored_data := await self.async_get_last_sensor_data(): + self._attr_native_value = restored_data.native_value + await super().async_added_to_hass() + + def calculate_seconds_remaining(self) -> int: + """Calculate the number of seconds remaining.""" + raise NotImplementedError + + @callback + def update_from_latest_data(self) -> None: + """Update the state.""" + self._previous_run_state = self._current_run_state + self._current_run_state = RUN_STATE_MAP.get(self.activity_data[self.status_key]) + + now = utcnow() + + if ( + self._current_run_state == RunStates.NOT_RUNNING + and self._previous_run_state in (RunStates.QUEUED, RunStates.RUNNING) + ): + # If the activity goes from queued/running to not running, update the + # state to be right now (i.e., the time the zone stopped running): + self._attr_native_value = now + elif self._current_run_state == RunStates.RUNNING: + seconds_remaining = self.calculate_seconds_remaining() + new_timestamp = now + timedelta(seconds=seconds_remaining) + + assert isinstance(self._attr_native_value, datetime) + + if ( + self._attr_native_value + and new_timestamp - self._attr_native_value + < DEFAULT_ZONE_COMPLETION_TIME_WOBBLE_TOLERANCE + ): + # If the deviation between the previous and new timestamps is less + # than a "wobble tolerance," don't spam the state machine: + return + + self._attr_native_value = new_timestamp + + +class ProgramTimeRemainingSensor(TimeRemainingSensor): + """Define a sensor that shows the amount of time remaining for a program.""" + + def __init__( + self, + entry: ConfigEntry, + program_coordinator: DataUpdateCoordinator, + zone_coordinator: DataUpdateCoordinator, + controller: Controller, + description: EntityDescription, + ) -> None: + """Initialize.""" + super().__init__(entry, program_coordinator, controller, description) + + self._zone_coordinator = zone_coordinator + + @property + def status_key(self) -> str: + """Return the data key that contains the activity status.""" + return "status" + + def calculate_seconds_remaining(self) -> int: + """Calculate the number of seconds remaining.""" + return sum( + self._zone_coordinator.data[zone["id"]]["remaining"] + for zone in [z for z in self.activity_data["wateringTimes"] if z["active"]] + ) + + class ProvisionSettingsSensor(RainMachineEntity, SensorEntity): """Define a sensor that handles provisioning data.""" @@ -203,47 +326,11 @@ class UniversalRestrictionsSensor(RainMachineEntity, SensorEntity): self._attr_native_value = self.coordinator.data.get("freezeProtectTemp") -class ZoneTimeRemainingSensor(RainMachineEntity, SensorEntity): +class ZoneTimeRemainingSensor(TimeRemainingSensor): """Define a sensor that shows the amount of time remaining for a zone.""" - entity_description: RainMachineSensorDescriptionUid - - def __init__( - self, - entry: ConfigEntry, - coordinator: DataUpdateCoordinator, - controller: Controller, - description: EntityDescription, - ) -> None: - """Initialize.""" - super().__init__(entry, coordinator, controller, description) - - self._running_or_queued: bool = False - - @callback - def update_from_latest_data(self) -> None: - """Update the state.""" - data = self.coordinator.data[self.entity_description.uid] - now = utcnow() - - if RUN_STATE_MAP.get(data["state"]) == RunStates.NOT_RUNNING: - if self._running_or_queued: - # If we go from running to not running, update the state to be right - # now (i.e., the time the zone stopped running): - self._attr_native_value = now - self._running_or_queued = False - return - - self._running_or_queued = True - new_timestamp = now + timedelta(seconds=data["remaining"]) - - if self._attr_native_value: - assert isinstance(self._attr_native_value, datetime) - if ( - new_timestamp - self._attr_native_value - ) < DEFAULT_ZONE_COMPLETION_TIME_WOBBLE_TOLERANCE: - # If the deviation between the previous and new timestamps is less than - # a "wobble tolerance," don't spam the state machine: - return - - self._attr_native_value = new_timestamp + def calculate_seconds_remaining(self) -> int: + """Calculate the number of seconds remaining.""" + return cast( + int, self.coordinator.data[self.entity_description.uid]["remaining"] + ) From d3f4108a91bfb8c7607e145ab48cfd489143d972 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 29 Jun 2022 16:34:41 +0200 Subject: [PATCH 1971/3516] Support knots and ft/s in weather wind speed (#74175) --- homeassistant/components/weather/__init__.py | 6 +++++- homeassistant/const.py | 2 ++ homeassistant/util/speed.py | 8 ++++++++ tests/util/test_speed.py | 6 ++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 6f2de6a3cb0..1fdb9173646 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -22,7 +22,9 @@ from homeassistant.const import ( PRESSURE_INHG, PRESSURE_MBAR, PRESSURE_MMHG, + SPEED_FEET_PER_SECOND, SPEED_KILOMETERS_PER_HOUR, + SPEED_KNOTS, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR, TEMP_CELSIUS, @@ -118,8 +120,10 @@ VALID_UNITS_VISIBILITY: tuple[str, ...] = ( LENGTH_MILES, ) VALID_UNITS_WIND_SPEED: tuple[str, ...] = ( - SPEED_METERS_PER_SECOND, + SPEED_FEET_PER_SECOND, SPEED_KILOMETERS_PER_HOUR, + SPEED_KNOTS, + SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR, ) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7b91973a930..698f6bee240 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -608,10 +608,12 @@ CONCENTRATION_PARTS_PER_BILLION: Final = "ppb" # Speed units SPEED_MILLIMETERS_PER_DAY: Final = "mm/d" +SPEED_FEET_PER_SECOND: Final = "ft/s" SPEED_INCHES_PER_DAY: Final = "in/d" SPEED_METERS_PER_SECOND: Final = "m/s" SPEED_INCHES_PER_HOUR: Final = "in/h" SPEED_KILOMETERS_PER_HOUR: Final = "km/h" +SPEED_KNOTS: Final = "kn" SPEED_MILES_PER_HOUR: Final = "mph" # Signal_strength units diff --git a/homeassistant/util/speed.py b/homeassistant/util/speed.py index 14b28bde676..12618e020f8 100644 --- a/homeassistant/util/speed.py +++ b/homeassistant/util/speed.py @@ -5,9 +5,11 @@ from numbers import Number from homeassistant.const import ( SPEED, + SPEED_FEET_PER_SECOND, SPEED_INCHES_PER_DAY, SPEED_INCHES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR, + SPEED_KNOTS, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR, SPEED_MILLIMETERS_PER_DAY, @@ -15,24 +17,30 @@ from homeassistant.const import ( ) VALID_UNITS: tuple[str, ...] = ( + SPEED_FEET_PER_SECOND, SPEED_INCHES_PER_DAY, SPEED_INCHES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR, + SPEED_KNOTS, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR, SPEED_MILLIMETERS_PER_DAY, ) +FOOT_TO_M = 0.3048 HRS_TO_SECS = 60 * 60 # 1 hr = 3600 seconds IN_TO_M = 0.0254 KM_TO_M = 1000 # 1 km = 1000 m MILE_TO_M = 1609.344 +NAUTICAL_MILE_TO_M = 1852 # 1 nautical mile = 1852 m # Units in terms of m/s UNIT_CONVERSION: dict[str, float] = { + SPEED_FEET_PER_SECOND: 1 / FOOT_TO_M, SPEED_INCHES_PER_DAY: (24 * HRS_TO_SECS) / IN_TO_M, SPEED_INCHES_PER_HOUR: HRS_TO_SECS / IN_TO_M, SPEED_KILOMETERS_PER_HOUR: HRS_TO_SECS / KM_TO_M, + SPEED_KNOTS: HRS_TO_SECS / NAUTICAL_MILE_TO_M, SPEED_METERS_PER_SECOND: 1, SPEED_MILES_PER_HOUR: HRS_TO_SECS / MILE_TO_M, SPEED_MILLIMETERS_PER_DAY: (24 * HRS_TO_SECS) * 1000, diff --git a/tests/util/test_speed.py b/tests/util/test_speed.py index f0a17e6ae15..9c7fd070313 100644 --- a/tests/util/test_speed.py +++ b/tests/util/test_speed.py @@ -2,9 +2,11 @@ import pytest from homeassistant.const import ( + SPEED_FEET_PER_SECOND, SPEED_INCHES_PER_DAY, SPEED_INCHES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR, + SPEED_KNOTS, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR, SPEED_MILLIMETERS_PER_DAY, @@ -61,6 +63,10 @@ def test_convert_nonnumeric_value(): (5, SPEED_METERS_PER_SECOND, 708661, SPEED_INCHES_PER_HOUR), # 5000 in/h / 39.3701 in/m / 3600 s/h = 0.03528 m/s (5000, SPEED_INCHES_PER_HOUR, 0.03528, SPEED_METERS_PER_SECOND), + # 5 kt * 1852 m/nmi / 3600 s/h = 2.5722 m/s + (5, SPEED_KNOTS, 2.5722, SPEED_METERS_PER_SECOND), + # 5 ft/s * 0.3048 m/ft = 1.524 m/s + (5, SPEED_FEET_PER_SECOND, 1.524, SPEED_METERS_PER_SECOND), ], ) def test_convert_different_units(from_value, from_unit, expected, to_unit): From 4e079c4417611f6a5cc0ce849556afe321972de1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 29 Jun 2022 16:50:24 +0200 Subject: [PATCH 1972/3516] Fix typo in recorder (#74178) --- homeassistant/components/recorder/websocket_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index d0499fbf9cb..45e2cf5620b 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -1,4 +1,4 @@ -"""The Energy websocket API.""" +"""The Recorder websocket API.""" from __future__ import annotations import logging From 97dcfe4445ab049d2db3824f54a24062c657951c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 29 Jun 2022 17:13:07 +0200 Subject: [PATCH 1973/3516] Smhi reverse change of unique id change (#74176) --- homeassistant/components/smhi/__init__.py | 16 ++--------- homeassistant/components/smhi/config_flow.py | 2 +- homeassistant/components/smhi/weather.py | 2 +- tests/components/smhi/test_config_flow.py | 20 ++++++------- tests/components/smhi/test_init.py | 30 ++++++++++++++++++-- 5 files changed, 39 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/smhi/__init__.py b/homeassistant/components/smhi/__init__.py index e3f55904b77..98c8de87032 100644 --- a/homeassistant/components/smhi/__init__.py +++ b/homeassistant/components/smhi/__init__.py @@ -7,8 +7,7 @@ from homeassistant.const import ( CONF_NAME, Platform, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries +from homeassistant.core import HomeAssistant PLATFORMS = [Platform.WEATHER] @@ -40,21 +39,10 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: CONF_LONGITUDE: entry.data[CONF_LONGITUDE], }, } - new_unique_id = f"smhi-{entry.data[CONF_LATITUDE]}-{entry.data[CONF_LONGITUDE]}" - if not hass.config_entries.async_update_entry( - entry, data=new_data, unique_id=new_unique_id - ): + if not hass.config_entries.async_update_entry(entry, data=new_data): return False entry.version = 2 - new_unique_id_entity = f"smhi-{entry.data[CONF_LOCATION][CONF_LATITUDE]}-{entry.data[CONF_LOCATION][CONF_LONGITUDE]}" - - @callback - def update_unique_id(entity_entry: RegistryEntry) -> dict[str, str]: - """Update unique ID of entity entry.""" - return {"new_unique_id": new_unique_id_entity} - - await async_migrate_entries(hass, entry.entry_id, update_unique_id) return True diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py index 8d338e3eb3e..bfa38f317a9 100644 --- a/homeassistant/components/smhi/config_flow.py +++ b/homeassistant/components/smhi/config_flow.py @@ -57,7 +57,7 @@ class SmhiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): HOME_LOCATION_NAME if name == HOME_LOCATION_NAME else DEFAULT_NAME ) - await self.async_set_unique_id(f"smhi-{lat}-{lon}") + await self.async_set_unique_id(f"{lat}-{lon}") self._abort_if_unique_id_configured() return self.async_create_entry(title=name, data=user_input) diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index 5d1f3e3c87e..d7df54957c0 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -136,7 +136,7 @@ class SmhiWeather(WeatherEntity): """Initialize the SMHI weather entity.""" self._attr_name = name - self._attr_unique_id = f"smhi-{latitude}-{longitude}" + self._attr_unique_id = f"{latitude}, {longitude}" self._forecasts: list[SmhiForecast] | None = None self._fail_count = 0 self._smhi_api = Smhi(longitude, latitude, session=session) diff --git a/tests/components/smhi/test_config_flow.py b/tests/components/smhi/test_config_flow.py index ab3e36f81de..f33849694c8 100644 --- a/tests/components/smhi/test_config_flow.py +++ b/tests/components/smhi/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant import config_entries from homeassistant.components.smhi.const import DOMAIN from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -27,7 +23,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -48,7 +44,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Home" assert result2["data"] == { "location": { @@ -81,7 +77,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == FlowResultType.CREATE_ENTRY assert result4["title"] == "Weather 1.0 1.0" assert result4["data"] == { "location": { @@ -113,7 +109,7 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "wrong_location"} # Continue flow with new coordinates @@ -135,7 +131,7 @@ async def test_form_invalid_coordinates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Weather 2.0 2.0" assert result3["data"] == { "location": { @@ -150,7 +146,7 @@ async def test_form_unique_id_exist(hass: HomeAssistant) -> None: """Test we handle unique id already exist.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id="smhi-1.0-1.0", + unique_id="1.0-1.0", data={ "location": { "latitude": 1.0, @@ -179,5 +175,5 @@ async def test_form_unique_id_exist(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/smhi/test_init.py b/tests/components/smhi/test_init.py index ea6d55fabf6..ec6e4c417bb 100644 --- a/tests/components/smhi/test_init.py +++ b/tests/components/smhi/test_init.py @@ -1,4 +1,6 @@ """Test SMHI component setup process.""" +from unittest.mock import patch + from smhi.smhi_lib import APIURL_TEMPLATE from homeassistant.components.smhi.const import DOMAIN @@ -56,7 +58,7 @@ async def test_remove_entry( async def test_migrate_entry( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str ) -> None: - """Test migrate entry and entities unique id.""" + """Test migrate entry data.""" uri = APIURL_TEMPLATE.format( TEST_CONFIG_MIGRATE["longitude"], TEST_CONFIG_MIGRATE["latitude"] ) @@ -82,7 +84,29 @@ async def test_migrate_entry( assert state assert entry.version == 2 - assert entry.unique_id == "smhi-17.84197-17.84197" + assert entry.unique_id == "17.84197-17.84197" entity_get = entity_reg.async_get(entity.entity_id) - assert entity_get.unique_id == "smhi-17.84197-17.84197" + assert entity_get.unique_id == "17.84197, 17.84197" + + +async def test_migrate_entry_failed( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str +) -> None: + """Test migrate entry data that fails.""" + uri = APIURL_TEMPLATE.format( + TEST_CONFIG_MIGRATE["longitude"], TEST_CONFIG_MIGRATE["latitude"] + ) + aioclient_mock.get(uri, text=api_response) + entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG_MIGRATE) + entry.add_to_hass(hass) + assert entry.version == 1 + + with patch( + "homeassistant.config_entries.ConfigEntries.async_update_entry", + return_value=False, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.version == 1 From 73bff4dee5cd80e69e12d9ae38769e3d8cdbd759 Mon Sep 17 00:00:00 2001 From: Daniel Baulig Date: Wed, 29 Jun 2022 08:30:55 -0700 Subject: [PATCH 1974/3516] Expose Envisalink's zone number as an attribute (#71468) Co-authored-by: J. Nick Koston --- homeassistant/components/envisalink/binary_sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/envisalink/binary_sensor.py b/homeassistant/components/envisalink/binary_sensor.py index 3fd7daeea86..d82f90aa4f4 100644 --- a/homeassistant/components/envisalink/binary_sensor.py +++ b/homeassistant/components/envisalink/binary_sensor.py @@ -93,6 +93,12 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorEntity): last_trip_time = None attr[ATTR_LAST_TRIP_TIME] = last_trip_time + + # Expose the zone number as an attribute to allow + # for easier entity to zone mapping (e.g. to bypass + # the zone). + attr["zone"] = self._zone_number + return attr @property From fa678d04085f443934c388ae2c6b43da3e5461d4 Mon Sep 17 00:00:00 2001 From: Arne Mauer Date: Wed, 29 Jun 2022 17:44:40 +0200 Subject: [PATCH 1975/3516] New sensors and manufacturer cluster to support IKEA STARKVIND (with Quirk) (#73450) * Add Particulate Matter 2.5 of ZCL concentration clusters to ZHA component * Fixed black and flake8 test * New sensors and manufacturer cluster to support IKEA STARKVIND (with quirk) * Isort and codespell fixes * Instead using the fan cluster, i've created a Ikea air purifier cluster/channel that supports all sensors and fan modes * update sensors to support the new ikea_airpurifier channel * Fix black, flake8, isort * Mylint/mypy fixes + Use a TypedDict for REPORT_CONFIG in zha #73629 * Last fix for test_fan.py * fix fan test Co-authored-by: David F. Mulcahey --- homeassistant/components/zha/binary_sensor.py | 8 + .../zha/core/channels/manufacturerspecific.py | 58 +++++- .../components/zha/core/registries.py | 1 + homeassistant/components/zha/fan.py | 99 ++++++++++ homeassistant/components/zha/number.py | 18 ++ homeassistant/components/zha/sensor.py | 32 +++ homeassistant/components/zha/switch.py | 18 ++ tests/components/zha/test_fan.py | 186 +++++++++++++++++- 8 files changed, 416 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 8130e7d5f98..709515d7ca2 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -186,3 +186,11 @@ class FrostLock(BinarySensor, id_suffix="frost_lock"): SENSOR_ATTR = "frost_lock" _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.LOCK + + +@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) +class ReplaceFilter(BinarySensor, id_suffix="replace_filter"): + """ZHA BinarySensor.""" + + SENSOR_ATTR = "replace_filter" + _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.PROBLEM diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 101db65a66e..943d13a57d6 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -2,8 +2,9 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any +from zigpy.exceptions import ZigbeeException import zigpy.zcl from homeassistant.core import callback @@ -14,6 +15,8 @@ from ..const import ( ATTR_ATTRIBUTE_NAME, ATTR_VALUE, REPORT_CONFIG_ASAP, + REPORT_CONFIG_DEFAULT, + REPORT_CONFIG_IMMEDIATE, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MIN_INT, SIGNAL_ATTR_UPDATED, @@ -129,3 +132,56 @@ class InovelliCluster(ClientChannel): """Inovelli Button Press Event channel.""" REPORT_CONFIG = () + + +@registries.CHANNEL_ONLY_CLUSTERS.register(registries.IKEA_AIR_PURIFIER_CLUSTER) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(registries.IKEA_AIR_PURIFIER_CLUSTER) +class IkeaAirPurifierChannel(ZigbeeChannel): + """IKEA Air Purifier channel.""" + + REPORT_CONFIG = ( + AttrReportConfig(attr="filter_run_time", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="replace_filter", config=REPORT_CONFIG_IMMEDIATE), + AttrReportConfig(attr="filter_life_time", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="disable_led", config=REPORT_CONFIG_IMMEDIATE), + AttrReportConfig(attr="air_quality_25pm", config=REPORT_CONFIG_IMMEDIATE), + AttrReportConfig(attr="child_lock", config=REPORT_CONFIG_IMMEDIATE), + AttrReportConfig(attr="fan_mode", config=REPORT_CONFIG_IMMEDIATE), + AttrReportConfig(attr="fan_speed", config=REPORT_CONFIG_IMMEDIATE), + AttrReportConfig(attr="device_run_time", config=REPORT_CONFIG_DEFAULT), + ) + + @property + def fan_mode(self) -> int | None: + """Return current fan mode.""" + return self.cluster.get("fan_mode") + + @property + def fan_mode_sequence(self) -> int | None: + """Return possible fan mode speeds.""" + return self.cluster.get("fan_mode_sequence") + + async def async_set_speed(self, value) -> None: + """Set the speed of the fan.""" + + try: + await self.cluster.write_attributes({"fan_mode": value}) + except ZigbeeException as ex: + self.error("Could not set speed: %s", ex) + return + + async def async_update(self) -> None: + """Retrieve latest state.""" + await self.get_attribute_value("fan_mode", from_cache=False) + + @callback + def attribute_updated(self, attrid: int, value: Any) -> None: + """Handle attribute update from fan cluster.""" + attr_name = self._get_attribute_name(attrid) + self.debug( + "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value + ) + if attr_name == "fan_mode": + self.async_send_signal( + f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", attrid, attr_name, value + ) diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index d271f2ecba3..2480cf1cd43 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -28,6 +28,7 @@ _ZhaGroupEntityT = TypeVar("_ZhaGroupEntityT", bound=type["ZhaGroupEntity"]) GROUP_ENTITY_DOMAINS = [Platform.LIGHT, Platform.SWITCH, Platform.FAN] +IKEA_AIR_PURIFIER_CLUSTER = 0xFC7D PHILLIPS_REMOTE_CLUSTER = 0xFC00 SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 8e24427b679..d947fca10ab 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -51,6 +51,7 @@ DEFAULT_ON_PERCENTAGE = 50 STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.FAN) GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.FAN) +MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.FAN) async def async_setup_entry( @@ -228,3 +229,101 @@ class FanGroup(BaseFan, ZhaGroupEntity): """Run when about to be added to hass.""" await self.async_update() await super().async_added_to_hass() + + +IKEA_SPEED_RANGE = (1, 10) # off is not included +IKEA_PRESET_MODES_TO_NAME = { + 1: PRESET_MODE_AUTO, + 2: "Speed 1", + 3: "Speed 1.5", + 4: "Speed 2", + 5: "Speed 2.5", + 6: "Speed 3", + 7: "Speed 3.5", + 8: "Speed 4", + 9: "Speed 4.5", + 10: "Speed 5", +} +IKEA_NAME_TO_PRESET_MODE = {v: k for k, v in IKEA_PRESET_MODES_TO_NAME.items()} +IKEA_PRESET_MODES = list(IKEA_NAME_TO_PRESET_MODE) + + +@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) +class IkeaFan(BaseFan, ZhaEntity): + """Representation of a ZHA fan.""" + + def __init__(self, unique_id, zha_device, channels, **kwargs): + """Init this sensor.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + self._fan_channel = self.cluster_channels.get("ikea_airpurifier") + + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + await super().async_added_to_hass() + self.async_accept_signal( + self._fan_channel, SIGNAL_ATTR_UPDATED, self.async_set_state + ) + + @property + def preset_modes(self) -> list[str]: + """Return the available preset modes.""" + return IKEA_PRESET_MODES + + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports.""" + return int_states_in_range(IKEA_SPEED_RANGE) + + async def async_set_percentage(self, percentage: int | None) -> None: + """Set the speed percenage of the fan.""" + if percentage is None: + percentage = 0 + fan_mode = math.ceil(percentage_to_ranged_value(IKEA_SPEED_RANGE, percentage)) + await self._async_set_fan_mode(fan_mode) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode for the fan.""" + if preset_mode not in self.preset_modes: + raise NotValidPresetModeError( + f"The preset_mode {preset_mode} is not a valid preset_mode: {self.preset_modes}" + ) + await self._async_set_fan_mode(IKEA_NAME_TO_PRESET_MODE[preset_mode]) + + @property + def percentage(self) -> int | None: + """Return the current speed percentage.""" + if ( + self._fan_channel.fan_mode is None + or self._fan_channel.fan_mode > IKEA_SPEED_RANGE[1] + ): + return None + if self._fan_channel.fan_mode == 0: + return 0 + return ranged_value_to_percentage(IKEA_SPEED_RANGE, self._fan_channel.fan_mode) + + @property + def preset_mode(self) -> str | None: + """Return the current preset mode.""" + return IKEA_PRESET_MODES_TO_NAME.get(self._fan_channel.fan_mode) + + async def async_turn_on(self, percentage=None, preset_mode=None, **kwargs) -> None: + """Turn the entity on.""" + if percentage is None: + percentage = (100 / self.speed_count) * IKEA_NAME_TO_PRESET_MODE[ + PRESET_MODE_AUTO + ] + await self.async_set_percentage(percentage) + + async def async_turn_off(self, **kwargs) -> None: + """Turn the entity off.""" + await self.async_set_percentage(0) + + @callback + def async_set_state(self, attr_id, attr_name, value): + """Handle state update from channel.""" + self.async_write_ha_state() + + async def _async_set_fan_mode(self, fan_mode: int) -> None: + """Set the fan mode for the fan.""" + await self._fan_channel.async_set_speed(fan_mode) + self.async_set_state(0, "fan_mode", fan_mode) diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 9103dd2e364..c3d7f352318 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -523,3 +523,21 @@ class TimerDurationMinutes(ZHANumberConfigurationEntity, id_suffix="timer_durati _attr_native_max_value: float = 0x257 _attr_unit_of_measurement: str | None = UNITS[72] _zcl_attribute: str = "timer_duration" + + +@CONFIG_DIAGNOSTIC_MATCH( + channel_names="ikea_manufacturer", + manufacturers={ + "IKEA of Sweden", + }, + models={"STARKVIND Air purifier"}, +) +class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time"): + """Representation of a ZHA timer duration configuration entity.""" + + _attr_entity_category = EntityCategory.CONFIG + _attr_icon: str = ICONS[14] + _attr_native_min_value: float = 0x00 + _attr_native_max_value: float = 0xFFFFFFFF + _attr_unit_of_measurement: str | None = UNITS[72] + _zcl_attribute: str = "filter_life_time" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index e66f1569b81..2fe38193ecb 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -808,3 +808,35 @@ class TimeLeft(Sensor, id_suffix="time_left"): _attr_device_class: SensorDeviceClass = SensorDeviceClass.DURATION _attr_icon = "mdi:timer" _unit = TIME_MINUTES + + +@MULTI_MATCH( + channel_names="ikea_manufacturer", + manufacturers={ + "IKEA of Sweden", + }, + models={"STARKVIND Air purifier"}, +) +class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"): + """Sensor that displays device run time (in minutes).""" + + SENSOR_ATTR = "device_run_time" + _attr_device_class: SensorDeviceClass = SensorDeviceClass.DURATION + _attr_icon = "mdi:timer" + _unit = TIME_MINUTES + + +@MULTI_MATCH( + channel_names="ikea_manufacturer", + manufacturers={ + "IKEA of Sweden", + }, + models={"STARKVIND Air purifier"}, +) +class IkeaFilterRunTime(Sensor, id_suffix="filter_run_time"): + """Sensor that displays run time of the current filter (in minutes).""" + + SENSOR_ATTR = "filter_run_time" + _attr_device_class: SensorDeviceClass = SensorDeviceClass.DURATION + _attr_icon = "mdi:timer" + _unit = TIME_MINUTES diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index fe7526586f9..3b044bb7646 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -285,3 +285,21 @@ class P1MotionTriggerIndicatorSwitch( """Representation of a ZHA motion triggering configuration entity.""" _zcl_attribute: str = "trigger_indicator" + + +@CONFIG_DIAGNOSTIC_MATCH( + channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"} +) +class ChildLock(ZHASwitchConfigurationEntity, id_suffix="child_lock"): + """ZHA BinarySensor.""" + + _zcl_attribute: str = "child_lock" + + +@CONFIG_DIAGNOSTIC_MATCH( + channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"} +) +class DisableLed(ZHASwitchConfigurationEntity, id_suffix="disable_led"): + """ZHA BinarySensor.""" + + _zcl_attribute: str = "disable_led" diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 423634db035..9ebc5ae1c79 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -2,10 +2,10 @@ from unittest.mock import AsyncMock, call, patch import pytest +import zhaquirks.ikea.starkvind from zigpy.exceptions import ZigbeeException -import zigpy.profiles.zha as zha -import zigpy.zcl.clusters.general as general -import zigpy.zcl.clusters.hvac as hvac +from zigpy.profiles import zha +from zigpy.zcl.clusters import general, hvac import zigpy.zcl.foundation as zcl_f from homeassistant.components.fan import ( @@ -57,11 +57,15 @@ def fan_platform_only(): with patch( "homeassistant.components.zha.PLATFORMS", ( + Platform.BUTTON, + Platform.BINARY_SENSOR, Platform.FAN, Platform.LIGHT, Platform.DEVICE_TRACKER, Platform.NUMBER, + Platform.SENSOR, Platform.SELECT, + Platform.SWITCH, ), ): yield @@ -516,3 +520,179 @@ async def test_fan_update_entity( assert cluster.read_attributes.await_count == 4 else: assert cluster.read_attributes.await_count == 6 + + +@pytest.fixture +def zigpy_device_ikea(zigpy_device_mock): + """Device tracker zigpy device.""" + endpoints = { + 1: { + SIG_EP_INPUT: [ + general.Basic.cluster_id, + general.Identify.cluster_id, + general.Groups.cluster_id, + general.Scenes.cluster_id, + 64637, + ], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zha.DeviceType.COMBINED_INTERFACE, + SIG_EP_PROFILE: zha.PROFILE_ID, + }, + } + return zigpy_device_mock( + endpoints, + manufacturer="IKEA of Sweden", + model="STARKVIND Air purifier", + quirk=zhaquirks.ikea.starkvind.IkeaSTARKVIND, + node_descriptor=b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00", + ) + + +async def test_fan_ikea(hass, zha_device_joined_restored, zigpy_device_ikea): + """Test zha fan Ikea platform.""" + zha_device = await zha_device_joined_restored(zigpy_device_ikea) + cluster = zigpy_device_ikea.endpoints.get(1).ikea_airpurifier + entity_id = await find_entity_id(Platform.FAN, zha_device, hass) + assert entity_id is not None + + assert hass.states.get(entity_id).state == STATE_OFF + await async_enable_traffic(hass, [zha_device], enabled=False) + # test that the fan was created and that it is unavailable + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, [zha_device]) + + # test that the state has changed from unavailable to off + assert hass.states.get(entity_id).state == STATE_OFF + + # turn on at fan + await send_attributes_report(hass, cluster, {6: 1}) + assert hass.states.get(entity_id).state == STATE_ON + + # turn off at fan + await send_attributes_report(hass, cluster, {6: 0}) + assert hass.states.get(entity_id).state == STATE_OFF + + # turn on from HA + cluster.write_attributes.reset_mock() + await async_turn_on(hass, entity_id) + assert len(cluster.write_attributes.mock_calls) == 1 + assert cluster.write_attributes.call_args == call({"fan_mode": 1}) + + # turn off from HA + cluster.write_attributes.reset_mock() + await async_turn_off(hass, entity_id) + assert len(cluster.write_attributes.mock_calls) == 1 + assert cluster.write_attributes.call_args == call({"fan_mode": 0}) + + # change speed from HA + cluster.write_attributes.reset_mock() + await async_set_percentage(hass, entity_id, percentage=100) + assert len(cluster.write_attributes.mock_calls) == 1 + assert cluster.write_attributes.call_args == call({"fan_mode": 10}) + + # change preset_mode from HA + cluster.write_attributes.reset_mock() + await async_set_preset_mode(hass, entity_id, preset_mode=PRESET_MODE_AUTO) + assert len(cluster.write_attributes.mock_calls) == 1 + assert cluster.write_attributes.call_args == call({"fan_mode": 1}) + + # set invalid preset_mode from HA + cluster.write_attributes.reset_mock() + with pytest.raises(NotValidPresetModeError): + await async_set_preset_mode( + hass, entity_id, preset_mode="invalid does not exist" + ) + assert len(cluster.write_attributes.mock_calls) == 0 + + # test adding new fan to the network and HA + await async_test_rejoin(hass, zigpy_device_ikea, [cluster], (9,)) + + +@pytest.mark.parametrize( + "ikea_plug_read, ikea_expected_state, ikea_expected_percentage, ikea_preset_mode", + ( + (None, STATE_OFF, None, None), + ({"fan_mode": 0}, STATE_OFF, 0, None), + ({"fan_mode": 1}, STATE_ON, 10, PRESET_MODE_AUTO), + ({"fan_mode": 10}, STATE_ON, 20, "Speed 1"), + ({"fan_mode": 15}, STATE_ON, 30, "Speed 1.5"), + ({"fan_mode": 20}, STATE_ON, 40, "Speed 2"), + ({"fan_mode": 25}, STATE_ON, 50, "Speed 2.5"), + ({"fan_mode": 30}, STATE_ON, 60, "Speed 3"), + ({"fan_mode": 35}, STATE_ON, 70, "Speed 3.5"), + ({"fan_mode": 40}, STATE_ON, 80, "Speed 4"), + ({"fan_mode": 45}, STATE_ON, 90, "Speed 4.5"), + ({"fan_mode": 50}, STATE_ON, 100, "Speed 5"), + ), +) +async def test_fan_ikea_init( + hass, + zha_device_joined_restored, + zigpy_device_ikea, + ikea_plug_read, + ikea_expected_state, + ikea_expected_percentage, + ikea_preset_mode, +): + """Test zha fan platform.""" + cluster = zigpy_device_ikea.endpoints.get(1).ikea_airpurifier + cluster.PLUGGED_ATTR_READS = ikea_plug_read + + zha_device = await zha_device_joined_restored(zigpy_device_ikea) + entity_id = await find_entity_id(Platform.FAN, zha_device, hass) + assert entity_id is not None + assert hass.states.get(entity_id).state == ikea_expected_state + assert ( + hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] + == ikea_expected_percentage + ) + assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] == ikea_preset_mode + + +async def test_fan_ikea_update_entity( + hass, + zha_device_joined_restored, + zigpy_device_ikea, +): + """Test zha fan platform.""" + cluster = zigpy_device_ikea.endpoints.get(1).ikea_airpurifier + cluster.PLUGGED_ATTR_READS = {"fan_mode": 0} + + zha_device = await zha_device_joined_restored(zigpy_device_ikea) + entity_id = await find_entity_id(Platform.FAN, zha_device, hass) + assert entity_id is not None + assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 0 + assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None + assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 10 + if zha_device_joined_restored.name == "zha_device_joined": + assert cluster.read_attributes.await_count == 3 + else: + assert cluster.read_attributes.await_count == 6 + + await async_setup_component(hass, "homeassistant", {}) + await hass.async_block_till_done() + + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_OFF + if zha_device_joined_restored.name == "zha_device_joined": + assert cluster.read_attributes.await_count == 4 + else: + assert cluster.read_attributes.await_count == 7 + + cluster.PLUGGED_ATTR_READS = {"fan_mode": 1} + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_ON + assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 10 + assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is PRESET_MODE_AUTO + assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 10 + if zha_device_joined_restored.name == "zha_device_joined": + assert cluster.read_attributes.await_count == 5 + else: + assert cluster.read_attributes.await_count == 8 From b6f16f87a786b007e744b214974189e08d4b7e73 Mon Sep 17 00:00:00 2001 From: Jeef Date: Wed, 29 Jun 2022 09:51:39 -0600 Subject: [PATCH 1976/3516] Bump intellifire4py to 2.0.0 (#72563) * Enable Flame/Pilot switch * Enable Flame/Pilot switch * Update homeassistant/components/intellifire/switch.py Co-authored-by: J. Nick Koston * Update homeassistant/components/intellifire/switch.py Thats a great fix! Co-authored-by: J. Nick Koston * write not update * fixed forced upates * removed data field * Refactor to support update to backing library * pre-push-ninja-style * moving over * fixed coverage * removed tuple junk * re-added description * Update homeassistant/components/intellifire/translations/en.json Co-authored-by: Paulus Schoutsen * adressing PR comments * actually store generated values * Update homeassistant/components/intellifire/__init__.py Way better option! Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: Paulus Schoutsen --- .../components/intellifire/__init__.py | 54 ++++++++++++++++--- .../components/intellifire/config_flow.py | 35 ++++++------ homeassistant/components/intellifire/const.py | 2 + .../components/intellifire/coordinator.py | 48 +++++++++-------- .../components/intellifire/manifest.json | 2 +- .../components/intellifire/switch.py | 25 ++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/intellifire/conftest.py | 2 +- .../intellifire/test_config_flow.py | 27 ++++++---- 10 files changed, 126 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/intellifire/__init__.py b/homeassistant/components/intellifire/__init__.py index 83c6e05f572..034e74c2aa6 100644 --- a/homeassistant/components/intellifire/__init__.py +++ b/homeassistant/components/intellifire/__init__.py @@ -2,15 +2,22 @@ from __future__ import annotations from aiohttp import ClientConnectionError -from intellifire4py import IntellifireAsync, IntellifireControlAsync +from intellifire4py import IntellifireControlAsync from intellifire4py.exceptions import LoginException +from intellifire4py.intellifire import IntellifireAPICloud, IntellifireAPILocal from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from .const import DOMAIN, LOGGER +from .const import CONF_USER_ID, DOMAIN, LOGGER from .coordinator import IntellifireDataUpdateCoordinator PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] @@ -24,8 +31,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: LOGGER.debug("Old config entry format detected: %s", entry.unique_id) raise ConfigEntryAuthFailed - # Define the API Objects - read_object = IntellifireAsync(entry.data[CONF_HOST]) ift_control = IntellifireControlAsync( fireplace_ip=entry.data[CONF_HOST], ) @@ -42,9 +47,46 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: finally: await ift_control.close() + # Extract API Key and User_ID from ift_control + # Eventually this will migrate to using IntellifireAPICloud + + if CONF_USER_ID not in entry.data or CONF_API_KEY not in entry.data: + LOGGER.info( + "Updating intellifire config entry for %s with api information", + entry.unique_id, + ) + cloud_api = IntellifireAPICloud() + await cloud_api.login( + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + ) + api_key = cloud_api.get_fireplace_api_key() + user_id = cloud_api.get_user_id() + # Update data entry + hass.config_entries.async_update_entry( + entry, + data={ + **entry.data, + CONF_API_KEY: api_key, + CONF_USER_ID: user_id, + }, + ) + + else: + api_key = entry.data[CONF_API_KEY] + user_id = entry.data[CONF_USER_ID] + + # Instantiate local control + api = IntellifireAPILocal( + fireplace_ip=entry.data[CONF_HOST], + api_key=api_key, + user_id=user_id, + ) + # Define the update coordinator coordinator = IntellifireDataUpdateCoordinator( - hass=hass, read_api=read_object, control_api=ift_control + hass=hass, + api=api, ) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/intellifire/config_flow.py b/homeassistant/components/intellifire/config_flow.py index df13795a4ed..23bd92b0715 100644 --- a/homeassistant/components/intellifire/config_flow.py +++ b/homeassistant/components/intellifire/config_flow.py @@ -6,20 +6,17 @@ from dataclasses import dataclass from typing import Any from aiohttp import ClientConnectionError -from intellifire4py import ( - AsyncUDPFireplaceFinder, - IntellifireAsync, - IntellifireControlAsync, -) +from intellifire4py import AsyncUDPFireplaceFinder from intellifire4py.exceptions import LoginException +from intellifire4py.intellifire import IntellifireAPICloud, IntellifireAPILocal import voluptuous as vol from homeassistant import config_entries from homeassistant.components.dhcp import DhcpServiceInfo -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult -from .const import DOMAIN, LOGGER +from .const import CONF_USER_ID, DOMAIN, LOGGER STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) @@ -39,7 +36,8 @@ async def validate_host_input(host: str) -> str: Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ - api = IntellifireAsync(host) + LOGGER.debug("Instantiating IntellifireAPI with host: [%s]", host) + api = IntellifireAPILocal(fireplace_ip=host) await api.poll() serial = api.data.serial LOGGER.debug("Found a fireplace: %s", serial) @@ -83,17 +81,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, *, host: str, username: str, password: str, serial: str ): """Validate username/password against api.""" - ift_control = IntellifireControlAsync(fireplace_ip=host) - LOGGER.debug("Attempting login to iftapi with: %s", username) - # This can throw an error which will be handled above - try: - await ift_control.login(username=username, password=password) - await ift_control.get_username() - finally: - await ift_control.close() - data = {CONF_HOST: host, CONF_PASSWORD: password, CONF_USERNAME: username} + ift_cloud = IntellifireAPICloud() + await ift_cloud.login(username=username, password=password) + api_key = ift_cloud.get_fireplace_api_key() + user_id = ift_cloud.get_user_id() + + data = { + CONF_HOST: host, + CONF_PASSWORD: password, + CONF_USERNAME: username, + CONF_API_KEY: api_key, + CONF_USER_ID: user_id, + } # Update or Create existing_entry = await self.async_set_unique_id(serial) diff --git a/homeassistant/components/intellifire/const.py b/homeassistant/components/intellifire/const.py index fe715c3ce8a..2e9a2fabc06 100644 --- a/homeassistant/components/intellifire/const.py +++ b/homeassistant/components/intellifire/const.py @@ -5,6 +5,8 @@ import logging DOMAIN = "intellifire" +CONF_USER_ID = "user_id" + LOGGER = logging.getLogger(__package__) CONF_SERIAL = "serial" diff --git a/homeassistant/components/intellifire/coordinator.py b/homeassistant/components/intellifire/coordinator.py index 9b74bd81653..39f197285d4 100644 --- a/homeassistant/components/intellifire/coordinator.py +++ b/homeassistant/components/intellifire/coordinator.py @@ -5,11 +5,8 @@ from datetime import timedelta from aiohttp import ClientConnectionError from async_timeout import timeout -from intellifire4py import ( - IntellifireAsync, - IntellifireControlAsync, - IntellifirePollData, -) +from intellifire4py import IntellifirePollData +from intellifire4py.intellifire import IntellifireAPILocal from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo @@ -24,8 +21,7 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData def __init__( self, hass: HomeAssistant, - read_api: IntellifireAsync, - control_api: IntellifireControlAsync, + api: IntellifireAPILocal, ) -> None: """Initialize the Coordinator.""" super().__init__( @@ -34,27 +30,37 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData name=DOMAIN, update_interval=timedelta(seconds=15), ) - self._read_api = read_api - self._control_api = control_api + self._api = api async def _async_update_data(self) -> IntellifirePollData: - LOGGER.debug("Calling update loop on IntelliFire") - async with timeout(100): - try: - await self._read_api.poll() - except (ConnectionError, ClientConnectionError) as exception: - raise UpdateFailed from exception - return self._read_api.data + + if not self._api.is_polling_in_background: + LOGGER.info("Starting Intellifire Background Polling Loop") + await self._api.start_background_polling() + + # Don't return uninitialized poll data + async with timeout(15): + try: + await self._api.poll() + except (ConnectionError, ClientConnectionError) as exception: + raise UpdateFailed from exception + + LOGGER.info("Failure Count %d", self._api.failed_poll_attempts) + if self._api.failed_poll_attempts > 10: + LOGGER.debug("Too many polling errors - raising exception") + raise UpdateFailed + + return self._api.data @property - def read_api(self) -> IntellifireAsync: + def read_api(self) -> IntellifireAPILocal: """Return the Status API pointer.""" - return self._read_api + return self._api @property - def control_api(self) -> IntellifireControlAsync: + def control_api(self) -> IntellifireAPILocal: """Return the control API.""" - return self._control_api + return self._api @property def device_info(self) -> DeviceInfo: @@ -65,5 +71,5 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData name="IntelliFire Fireplace", identifiers={("IntelliFire", f"{self.read_api.data.serial}]")}, sw_version=self.read_api.data.fw_ver_str, - configuration_url=f"http://{self.read_api.ip}/poll", + configuration_url=f"http://{self._api.fireplace_ip}/poll", ) diff --git a/homeassistant/components/intellifire/manifest.json b/homeassistant/components/intellifire/manifest.json index 388ce0c86cb..cd1a12a36bf 100644 --- a/homeassistant/components/intellifire/manifest.json +++ b/homeassistant/components/intellifire/manifest.json @@ -3,7 +3,7 @@ "name": "IntelliFire", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/intellifire", - "requirements": ["intellifire4py==1.0.2"], + "requirements": ["intellifire4py==2.0.0"], "codeowners": ["@jeeftor"], "iot_class": "local_polling", "loggers": ["intellifire4py"], diff --git a/homeassistant/components/intellifire/switch.py b/homeassistant/components/intellifire/switch.py index 9c196a59fd4..ef0363696c4 100644 --- a/homeassistant/components/intellifire/switch.py +++ b/homeassistant/components/intellifire/switch.py @@ -5,7 +5,8 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass from typing import Any -from intellifire4py import IntellifireControlAsync, IntellifirePollData +from intellifire4py import IntellifirePollData +from intellifire4py.intellifire import IntellifireAPILocal from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry @@ -21,8 +22,8 @@ from .entity import IntellifireEntity class IntellifireSwitchRequiredKeysMixin: """Mixin for required keys.""" - on_fn: Callable[[IntellifireControlAsync], Awaitable] - off_fn: Callable[[IntellifireControlAsync], Awaitable] + on_fn: Callable[[IntellifireAPILocal], Awaitable] + off_fn: Callable[[IntellifireAPILocal], Awaitable] value_fn: Callable[[IntellifirePollData], bool] @@ -37,24 +38,16 @@ INTELLIFIRE_SWITCHES: tuple[IntellifireSwitchEntityDescription, ...] = ( IntellifireSwitchEntityDescription( key="on_off", name="Flame", - on_fn=lambda control_api: control_api.flame_on( - fireplace=control_api.default_fireplace - ), - off_fn=lambda control_api: control_api.flame_off( - fireplace=control_api.default_fireplace - ), + on_fn=lambda control_api: control_api.flame_on(), + off_fn=lambda control_api: control_api.flame_off(), value_fn=lambda data: data.is_on, ), IntellifireSwitchEntityDescription( key="pilot", name="Pilot Light", icon="mdi:fire-alert", - on_fn=lambda control_api: control_api.pilot_on( - fireplace=control_api.default_fireplace - ), - off_fn=lambda control_api: control_api.pilot_off( - fireplace=control_api.default_fireplace - ), + on_fn=lambda control_api: control_api.pilot_on(), + off_fn=lambda control_api: control_api.pilot_off(), value_fn=lambda data: data.pilot_on, ), ) @@ -82,10 +75,12 @@ class IntellifireSwitch(IntellifireEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" await self.entity_description.on_fn(self.coordinator.control_api) + await self.async_update_ha_state(force_refresh=True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" await self.entity_description.off_fn(self.coordinator.control_api) + await self.async_update_ha_state(force_refresh=True) @property def is_on(self) -> bool | None: diff --git a/requirements_all.txt b/requirements_all.txt index d758df0a97c..064f686dc49 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -894,7 +894,7 @@ influxdb==5.3.1 insteon-frontend-home-assistant==0.1.1 # homeassistant.components.intellifire -intellifire4py==1.0.2 +intellifire4py==2.0.0 # homeassistant.components.iotawatt iotawattpy==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2cf31965f6..f579b9395d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -637,7 +637,7 @@ influxdb==5.3.1 insteon-frontend-home-assistant==0.1.1 # homeassistant.components.intellifire -intellifire4py==1.0.2 +intellifire4py==2.0.0 # homeassistant.components.iotawatt iotawattpy==0.1.0 diff --git a/tests/components/intellifire/conftest.py b/tests/components/intellifire/conftest.py index 3f73834226c..8940acd9d8e 100644 --- a/tests/components/intellifire/conftest.py +++ b/tests/components/intellifire/conftest.py @@ -44,7 +44,7 @@ def mock_intellifire_config_flow() -> Generator[None, MagicMock, None]: data_mock.serial = "12345" with patch( - "homeassistant.components.intellifire.config_flow.IntellifireAsync", + "homeassistant.components.intellifire.config_flow.IntellifireAPILocal", autospec=True, ) as intellifire_mock: intellifire = intellifire_mock.return_value diff --git a/tests/components/intellifire/test_config_flow.py b/tests/components/intellifire/test_config_flow.py index 2f48e645708..06fcbea5bfa 100644 --- a/tests/components/intellifire/test_config_flow.py +++ b/tests/components/intellifire/test_config_flow.py @@ -6,8 +6,8 @@ from intellifire4py.exceptions import LoginException from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.intellifire.config_flow import MANUAL_ENTRY_STRING -from homeassistant.components.intellifire.const import DOMAIN -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.intellifire.const import CONF_USER_ID, DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -20,9 +20,10 @@ from tests.components.intellifire.conftest import mock_api_connection_error @patch.multiple( - "homeassistant.components.intellifire.config_flow.IntellifireControlAsync", + "homeassistant.components.intellifire.config_flow.IntellifireAPICloud", login=AsyncMock(), - get_username=AsyncMock(return_value="intellifire"), + get_user_id=MagicMock(return_value="intellifire"), + get_fireplace_api_key=MagicMock(return_value="key"), ) async def test_no_discovery( hass: HomeAssistant, @@ -64,14 +65,17 @@ async def test_no_discovery( CONF_HOST: "1.1.1.1", CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE", + CONF_API_KEY: "key", + CONF_USER_ID: "intellifire", } assert len(mock_setup_entry.mock_calls) == 1 @patch.multiple( - "homeassistant.components.intellifire.config_flow.IntellifireControlAsync", + "homeassistant.components.intellifire.config_flow.IntellifireAPICloud", login=AsyncMock(side_effect=mock_api_connection_error()), - get_username=AsyncMock(return_value="intellifire"), + get_user_id=MagicMock(return_value="intellifire"), + get_fireplace_api_key=MagicMock(return_value="key"), ) async def test_single_discovery( hass: HomeAssistant, @@ -101,8 +105,10 @@ async def test_single_discovery( @patch.multiple( - "homeassistant.components.intellifire.config_flow.IntellifireControlAsync", - login=AsyncMock(side_effect=LoginException()), + "homeassistant.components.intellifire.config_flow.IntellifireAPICloud", + login=AsyncMock(side_effect=LoginException), + get_user_id=MagicMock(return_value="intellifire"), + get_fireplace_api_key=MagicMock(return_value="key"), ) async def test_single_discovery_loign_error( hass: HomeAssistant, @@ -265,9 +271,10 @@ async def test_picker_already_discovered( @patch.multiple( - "homeassistant.components.intellifire.config_flow.IntellifireControlAsync", + "homeassistant.components.intellifire.config_flow.IntellifireAPICloud", login=AsyncMock(), - get_username=AsyncMock(return_value="intellifire"), + get_user_id=MagicMock(return_value="intellifire"), + get_fireplace_api_key=MagicMock(return_value="key"), ) async def test_reauth_flow( hass: HomeAssistant, From 0a65f53356e124592cae37ea1f1873b789e0726b Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 29 Jun 2022 11:40:02 -0500 Subject: [PATCH 1977/3516] Convert life360 integration to entity based (#72461) * Convert life360 integration to entity based * Improve config_flow.py type checking * Add tests for config flow Fix form defaults for reauth flow. * Cover reauth when config entry loaded * Update per review (except for dataclasses) * Restore check for missing location information This is in current code but was accidentally removed in this PR. * Fix updates from review * Update tests per review changes * Change IntegData to a dataclass * Use dataclasses to represent fetched Life360 data * Always add extra attributes * Update per review take 2 * Tweak handling of bad last_seen or location_accuracy * Fix type of Life360Member.gps_accuracy * Update per review take 3 * Update .coveragerc * Parametrize successful reauth flow test * Fix test coverage failure * Update per review take 4 * Fix config schema --- .coveragerc | 2 +- CODEOWNERS | 1 + homeassistant/components/life360/__init__.py | 248 +++---- .../components/life360/config_flow.py | 263 +++++--- homeassistant/components/life360/const.py | 27 + .../components/life360/coordinator.py | 201 ++++++ .../components/life360/device_tracker.py | 620 ++++++------------ homeassistant/components/life360/helpers.py | 7 - homeassistant/components/life360/strings.json | 33 +- .../components/life360/translations/en.json | 65 +- requirements_test_all.txt | 3 + tests/components/life360/__init__.py | 1 + tests/components/life360/test_config_flow.py | 309 +++++++++ 13 files changed, 1109 insertions(+), 671 deletions(-) create mode 100644 homeassistant/components/life360/coordinator.py delete mode 100644 homeassistant/components/life360/helpers.py create mode 100644 tests/components/life360/__init__.py create mode 100644 tests/components/life360/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index eae8060449a..f52631a57bd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -640,8 +640,8 @@ omit = homeassistant/components/lg_soundbar/media_player.py homeassistant/components/life360/__init__.py homeassistant/components/life360/const.py + homeassistant/components/life360/coordinator.py homeassistant/components/life360/device_tracker.py - homeassistant/components/life360/helpers.py homeassistant/components/lifx/__init__.py homeassistant/components/lifx/const.py homeassistant/components/lifx/light.py diff --git a/CODEOWNERS b/CODEOWNERS index e349ebeacf0..1b6d88b5464 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -572,6 +572,7 @@ build.json @home-assistant/supervisor /tests/components/lcn/ @alengwenus /homeassistant/components/lg_netcast/ @Drafteed /homeassistant/components/life360/ @pnbruckner +/tests/components/life360/ @pnbruckner /homeassistant/components/lifx/ @Djelibeybi /homeassistant/components/light/ @home-assistant/core /tests/components/light/ @home-assistant/core diff --git a/homeassistant/components/life360/__init__.py b/homeassistant/components/life360/__init__.py index 89e7ee680a5..66c9416a1c1 100644 --- a/homeassistant/components/life360/__init__.py +++ b/homeassistant/components/life360/__init__.py @@ -1,11 +1,14 @@ """Life360 integration.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass, field +from typing import Any + import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.device_tracker import CONF_SCAN_INTERVAL -from homeassistant.components.device_tracker.const import ( - SCAN_INTERVAL as DEFAULT_SCAN_INTERVAL, -) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_EXCLUDE, @@ -16,12 +19,10 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType from .const import ( - CONF_AUTHORIZATION, CONF_CIRCLES, CONF_DRIVING_SPEED, CONF_ERROR_THRESHOLD, @@ -30,182 +31,147 @@ from .const import ( CONF_MEMBERS, CONF_SHOW_AS_STATE, CONF_WARNING_THRESHOLD, + DEFAULT_OPTIONS, DOMAIN, + LOGGER, SHOW_DRIVING, SHOW_MOVING, ) -from .helpers import get_api +from .coordinator import Life360DataUpdateCoordinator -DEFAULT_PREFIX = DOMAIN +PLATFORMS = [Platform.DEVICE_TRACKER] CONF_ACCOUNTS = "accounts" SHOW_AS_STATE_OPTS = [SHOW_DRIVING, SHOW_MOVING] -def _excl_incl_list_to_filter_dict(value): - return { - "include": CONF_INCLUDE in value, - "list": value.get(CONF_EXCLUDE) or value.get(CONF_INCLUDE), - } - - -def _prefix(value): - if not value: - return "" - if not value.endswith("_"): - return f"{value}_" - return value - - -def _thresholds(config): - error_threshold = config.get(CONF_ERROR_THRESHOLD) - warning_threshold = config.get(CONF_WARNING_THRESHOLD) - if error_threshold and warning_threshold: - if error_threshold <= warning_threshold: - raise vol.Invalid( - f"{CONF_ERROR_THRESHOLD} must be larger than {CONF_WARNING_THRESHOLD}" +def _show_as_state(config: dict) -> dict: + if opts := config.pop(CONF_SHOW_AS_STATE): + if SHOW_DRIVING in opts: + config[SHOW_DRIVING] = True + if SHOW_MOVING in opts: + LOGGER.warning( + "%s is no longer supported as an option for %s", + SHOW_MOVING, + CONF_SHOW_AS_STATE, ) - elif not error_threshold and warning_threshold: - config[CONF_ERROR_THRESHOLD] = warning_threshold + 1 - elif error_threshold and not warning_threshold: - # Make them the same which effectively prevents warnings. - config[CONF_WARNING_THRESHOLD] = error_threshold - else: - # Log all errors as errors. - config[CONF_ERROR_THRESHOLD] = 1 - config[CONF_WARNING_THRESHOLD] = 1 return config -ACCOUNT_SCHEMA = vol.Schema( - {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} -) +def _unsupported(unsupported: set[str]) -> Callable[[dict], dict]: + """Warn about unsupported options and remove from config.""" -_SLUG_LIST = vol.All( - cv.ensure_list, [cv.slugify], vol.Length(min=1, msg="List cannot be empty") -) + def validator(config: dict) -> dict: + if unsupported_keys := unsupported & set(config): + LOGGER.warning( + "The following options are no longer supported: %s", + ", ".join(sorted(unsupported_keys)), + ) + return {k: v for k, v in config.items() if k not in unsupported} -_LOWER_STRING_LIST = vol.All( - cv.ensure_list, - [vol.All(cv.string, vol.Lower)], - vol.Length(min=1, msg="List cannot be empty"), -) + return validator -_EXCL_INCL_SLUG_LIST = vol.All( - vol.Schema( - { - vol.Exclusive(CONF_EXCLUDE, "incl_excl"): _SLUG_LIST, - vol.Exclusive(CONF_INCLUDE, "incl_excl"): _SLUG_LIST, - } - ), - cv.has_at_least_one_key(CONF_EXCLUDE, CONF_INCLUDE), - _excl_incl_list_to_filter_dict, -) - -_EXCL_INCL_LOWER_STRING_LIST = vol.All( - vol.Schema( - { - vol.Exclusive(CONF_EXCLUDE, "incl_excl"): _LOWER_STRING_LIST, - vol.Exclusive(CONF_INCLUDE, "incl_excl"): _LOWER_STRING_LIST, - } - ), - cv.has_at_least_one_key(CONF_EXCLUDE, CONF_INCLUDE), - _excl_incl_list_to_filter_dict, -) - -_THRESHOLD = vol.All(vol.Coerce(int), vol.Range(min=1)) +ACCOUNT_SCHEMA = { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, +} +CIRCLES_MEMBERS = { + vol.Optional(CONF_EXCLUDE): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_INCLUDE): vol.All(cv.ensure_list, [cv.string]), +} LIFE360_SCHEMA = vol.All( vol.Schema( { - vol.Optional(CONF_ACCOUNTS): vol.All( - cv.ensure_list, [ACCOUNT_SCHEMA], vol.Length(min=1) - ), - vol.Optional(CONF_CIRCLES): _EXCL_INCL_LOWER_STRING_LIST, + vol.Optional(CONF_ACCOUNTS): vol.All(cv.ensure_list, [ACCOUNT_SCHEMA]), + vol.Optional(CONF_CIRCLES): CIRCLES_MEMBERS, vol.Optional(CONF_DRIVING_SPEED): vol.Coerce(float), - vol.Optional(CONF_ERROR_THRESHOLD): _THRESHOLD, + vol.Optional(CONF_ERROR_THRESHOLD): vol.Coerce(int), vol.Optional(CONF_MAX_GPS_ACCURACY): vol.Coerce(float), - vol.Optional(CONF_MAX_UPDATE_WAIT): vol.All( - cv.time_period, cv.positive_timedelta - ), - vol.Optional(CONF_MEMBERS): _EXCL_INCL_SLUG_LIST, - vol.Optional(CONF_PREFIX, default=DEFAULT_PREFIX): vol.All( - vol.Any(None, cv.string), _prefix - ), - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, + vol.Optional(CONF_MAX_UPDATE_WAIT): cv.time_period, + vol.Optional(CONF_MEMBERS): CIRCLES_MEMBERS, + vol.Optional(CONF_PREFIX): vol.Any(None, cv.string), + vol.Optional(CONF_SCAN_INTERVAL): cv.time_period, vol.Optional(CONF_SHOW_AS_STATE, default=[]): vol.All( cv.ensure_list, [vol.In(SHOW_AS_STATE_OPTS)] ), - vol.Optional(CONF_WARNING_THRESHOLD): _THRESHOLD, + vol.Optional(CONF_WARNING_THRESHOLD): vol.Coerce(int), } ), - _thresholds, + _unsupported( + { + CONF_ACCOUNTS, + CONF_CIRCLES, + CONF_ERROR_THRESHOLD, + CONF_MAX_UPDATE_WAIT, + CONF_MEMBERS, + CONF_PREFIX, + CONF_SCAN_INTERVAL, + CONF_WARNING_THRESHOLD, + } + ), + _show_as_state, +) +CONFIG_SCHEMA = vol.Schema( + vol.All({DOMAIN: LIFE360_SCHEMA}, cv.removed(DOMAIN, raise_if_present=False)), + extra=vol.ALLOW_EXTRA, ) -CONFIG_SCHEMA = vol.Schema({DOMAIN: LIFE360_SCHEMA}, extra=vol.ALLOW_EXTRA) + +@dataclass +class IntegData: + """Integration data.""" + + cfg_options: dict[str, Any] | None = None + # ConfigEntry.unique_id: Life360DataUpdateCoordinator + coordinators: dict[str, Life360DataUpdateCoordinator] = field( + init=False, default_factory=dict + ) + # member_id: ConfigEntry.unique_id + tracked_members: dict[str, str] = field(init=False, default_factory=dict) + logged_circles: list[str] = field(init=False, default_factory=list) + logged_places: list[str] = field(init=False, default_factory=list) + + def __post_init__(self): + """Finish initialization of cfg_options.""" + self.cfg_options = self.cfg_options or {} -def setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up integration.""" - conf = config.get(DOMAIN, LIFE360_SCHEMA({})) - hass.data[DOMAIN] = {"config": conf, "apis": {}} - discovery.load_platform(hass, Platform.DEVICE_TRACKER, DOMAIN, None, config) - - if CONF_ACCOUNTS not in conf: - return True - - # Check existing config entries. For any that correspond to an entry in - # configuration.yaml, and whose password has not changed, nothing needs to - # be done with that config entry or that account from configuration.yaml. - # But if the config entry was created by import and the account no longer - # exists in configuration.yaml, or if the password has changed, then delete - # that out-of-date config entry. - already_configured = [] - for entry in hass.config_entries.async_entries(DOMAIN): - # Find corresponding configuration.yaml entry and its password. - password = None - for account in conf[CONF_ACCOUNTS]: - if account[CONF_USERNAME] == entry.data[CONF_USERNAME]: - password = account[CONF_PASSWORD] - if password == entry.data[CONF_PASSWORD]: - already_configured.append(entry.data[CONF_USERNAME]) - continue - if ( - not password - and entry.source == config_entries.SOURCE_IMPORT - or password - and password != entry.data[CONF_PASSWORD] - ): - hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) - - # Create config entries for accounts listed in configuration. - for account in conf[CONF_ACCOUNTS]: - if account[CONF_USERNAME] not in already_configured: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=account, - ) - ) + hass.data.setdefault(DOMAIN, IntegData(config.get(DOMAIN))) return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up config entry.""" - hass.data[DOMAIN]["apis"][entry.data[CONF_USERNAME]] = get_api( - entry.data[CONF_AUTHORIZATION] - ) + hass.data.setdefault(DOMAIN, IntegData()) + + # Check if this entry was created when this was a "legacy" tracker. If it was, + # update with missing data. + if not entry.unique_id: + hass.config_entries.async_update_entry( + entry, + unique_id=entry.data[CONF_USERNAME].lower(), + options=DEFAULT_OPTIONS | hass.data[DOMAIN].cfg_options, + ) + + coordinator = Life360DataUpdateCoordinator(hass, entry) + + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN].coordinators[entry.entry_id] = coordinator + + # Set up components for our platforms. + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload config entry.""" - try: - hass.data[DOMAIN]["apis"].pop(entry.data[CONF_USERNAME]) - return True - except KeyError: - return False + del hass.data[DOMAIN].coordinators[entry.entry_id] + + # Unload components for our platforms. + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/life360/config_flow.py b/homeassistant/components/life360/config_flow.py index 0a200e72097..331882aa991 100644 --- a/homeassistant/components/life360/config_flow.py +++ b/homeassistant/components/life360/config_flow.py @@ -1,108 +1,199 @@ """Config flow to configure Life360 integration.""" -from collections import OrderedDict -import logging -from life360 import Life360Error, LoginError +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, cast + +from life360 import Life360, Life360Error, LoginError import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv -from .const import CONF_AUTHORIZATION, DOMAIN -from .helpers import get_api +from .const import ( + COMM_MAX_RETRIES, + COMM_TIMEOUT, + CONF_AUTHORIZATION, + CONF_DRIVING_SPEED, + CONF_MAX_GPS_ACCURACY, + DEFAULT_OPTIONS, + DOMAIN, + LOGGER, + OPTIONS, + SHOW_DRIVING, +) -_LOGGER = logging.getLogger(__name__) - -DOCS_URL = "https://www.home-assistant.io/integrations/life360" +LIMIT_GPS_ACC = "limit_gps_acc" +SET_DRIVE_SPEED = "set_drive_speed" -class Life360ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +def account_schema( + def_username: str | vol.UNDEFINED = vol.UNDEFINED, + def_password: str | vol.UNDEFINED = vol.UNDEFINED, +) -> dict[vol.Marker, Any]: + """Return schema for an account with optional default values.""" + return { + vol.Required(CONF_USERNAME, default=def_username): cv.string, + vol.Required(CONF_PASSWORD, default=def_password): cv.string, + } + + +def password_schema( + def_password: str | vol.UNDEFINED = vol.UNDEFINED, +) -> dict[vol.Marker, Any]: + """Return schema for a password with optional default value.""" + return {vol.Required(CONF_PASSWORD, default=def_password): cv.string} + + +class Life360ConfigFlow(ConfigFlow, domain=DOMAIN): """Life360 integration config flow.""" VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize.""" - self._api = get_api() - self._username = vol.UNDEFINED - self._password = vol.UNDEFINED + self._api = Life360(timeout=COMM_TIMEOUT, max_retries=COMM_MAX_RETRIES) + self._username: str | vol.UNDEFINED = vol.UNDEFINED + self._password: str | vol.UNDEFINED = vol.UNDEFINED + self._reauth_entry: ConfigEntry | None = None - @property - def configured_usernames(self): - """Return tuple of configured usernames.""" - entries = self._async_current_entries() - if entries: - return (entry.data[CONF_USERNAME] for entry in entries) - return () + @staticmethod + @callback + def async_get_options_flow(config_entry: ConfigEntry) -> Life360OptionsFlow: + """Get the options flow for this handler.""" + return Life360OptionsFlow(config_entry) - async def async_step_user(self, user_input=None): - """Handle a user initiated config flow.""" - errors = {} - - if user_input is not None: - self._username = user_input[CONF_USERNAME] - self._password = user_input[CONF_PASSWORD] - try: - # pylint: disable=no-value-for-parameter - vol.Email()(self._username) - authorization = await self.hass.async_add_executor_job( - self._api.get_authorization, self._username, self._password - ) - except vol.Invalid: - errors[CONF_USERNAME] = "invalid_username" - except LoginError: - errors["base"] = "invalid_auth" - except Life360Error as error: - _LOGGER.error( - "Unexpected error communicating with Life360 server: %s", error - ) - errors["base"] = "unknown" - else: - if self._username in self.configured_usernames: - errors["base"] = "already_configured" - else: - return self.async_create_entry( - title=self._username, - data={ - CONF_USERNAME: self._username, - CONF_PASSWORD: self._password, - CONF_AUTHORIZATION: authorization, - }, - description_placeholders={"docs_url": DOCS_URL}, - ) - - data_schema = OrderedDict() - data_schema[vol.Required(CONF_USERNAME, default=self._username)] = str - data_schema[vol.Required(CONF_PASSWORD, default=self._password)] = str - - return self.async_show_form( - step_id="user", - data_schema=vol.Schema(data_schema), - errors=errors, - description_placeholders={"docs_url": DOCS_URL}, - ) - - async def async_step_import(self, user_input): - """Import a config flow from configuration.""" - username = user_input[CONF_USERNAME] - password = user_input[CONF_PASSWORD] + async def _async_verify(self, step_id: str) -> FlowResult: + """Attempt to authorize the provided credentials.""" + errors: dict[str, str] = {} try: authorization = await self.hass.async_add_executor_job( - self._api.get_authorization, username, password + self._api.get_authorization, self._username, self._password ) - except LoginError: - _LOGGER.error("Invalid credentials for %s", username) - return self.async_abort(reason="invalid_auth") - except Life360Error as error: - _LOGGER.error( - "Unexpected error communicating with Life360 server: %s", error + except LoginError as exc: + LOGGER.debug("Login error: %s", exc) + errors["base"] = "invalid_auth" + except Life360Error as exc: + LOGGER.debug("Unexpected error communicating with Life360 server: %s", exc) + errors["base"] = "cannot_connect" + if errors: + if step_id == "user": + schema = account_schema(self._username, self._password) + else: + schema = password_schema(self._password) + return self.async_show_form( + step_id=step_id, data_schema=vol.Schema(schema), errors=errors ) - return self.async_abort(reason="unknown") + + data = { + CONF_USERNAME: self._username, + CONF_PASSWORD: self._password, + CONF_AUTHORIZATION: authorization, + } + + if self._reauth_entry: + LOGGER.debug("Reauthorization successful") + self.hass.config_entries.async_update_entry(self._reauth_entry, data=data) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + return self.async_create_entry( - title=f"{username} (from configuration)", - data={ - CONF_USERNAME: username, - CONF_PASSWORD: password, - CONF_AUTHORIZATION: authorization, - }, + title=cast(str, self.unique_id), data=data, options=DEFAULT_OPTIONS ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a config flow initiated by the user.""" + if not user_input: + return self.async_show_form( + step_id="user", data_schema=vol.Schema(account_schema()) + ) + + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] + + await self.async_set_unique_id(self._username.lower()) + self._abort_if_unique_id_configured() + + return await self._async_verify("user") + + async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + """Handle reauthorization.""" + self._username = data[CONF_USERNAME] + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + # Always start with current credentials since they may still be valid and a + # simple reauthorization will be successful. + return await self.async_step_reauth_confirm(dict(data)) + + async def async_step_reauth_confirm(self, user_input: dict[str, Any]) -> FlowResult: + """Handle reauthorization completion.""" + self._password = user_input[CONF_PASSWORD] + return await self._async_verify("reauth_confirm") + + +class Life360OptionsFlow(OptionsFlow): + """Life360 integration options flow.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle account options.""" + options = self.config_entry.options + + if user_input is not None: + new_options = _extract_account_options(user_input) + return self.async_create_entry(title="", data=new_options) + + return self.async_show_form( + step_id="init", data_schema=vol.Schema(_account_options_schema(options)) + ) + + +def _account_options_schema(options: Mapping[str, Any]) -> dict[vol.Marker, Any]: + """Create schema for account options form.""" + def_limit_gps_acc = options[CONF_MAX_GPS_ACCURACY] is not None + def_max_gps = options[CONF_MAX_GPS_ACCURACY] or vol.UNDEFINED + def_set_drive_speed = options[CONF_DRIVING_SPEED] is not None + def_speed = options[CONF_DRIVING_SPEED] or vol.UNDEFINED + def_show_driving = options[SHOW_DRIVING] + + return { + vol.Required(LIMIT_GPS_ACC, default=def_limit_gps_acc): bool, + vol.Optional(CONF_MAX_GPS_ACCURACY, default=def_max_gps): vol.Coerce(float), + vol.Required(SET_DRIVE_SPEED, default=def_set_drive_speed): bool, + vol.Optional(CONF_DRIVING_SPEED, default=def_speed): vol.Coerce(float), + vol.Optional(SHOW_DRIVING, default=def_show_driving): bool, + } + + +def _extract_account_options(user_input: dict) -> dict[str, Any]: + """Remove options from user input and return as a separate dict.""" + result = {} + + for key in OPTIONS: + value = user_input.pop(key, None) + # Was "include" checkbox (if there was one) corresponding to option key True + # (meaning option should be included)? + incl = user_input.pop( + { + CONF_MAX_GPS_ACCURACY: LIMIT_GPS_ACC, + CONF_DRIVING_SPEED: SET_DRIVE_SPEED, + }.get(key), + True, + ) + result[key] = value if incl else None + + return result diff --git a/homeassistant/components/life360/const.py b/homeassistant/components/life360/const.py index 41f4a990e67..ccaf69877d6 100644 --- a/homeassistant/components/life360/const.py +++ b/homeassistant/components/life360/const.py @@ -1,5 +1,25 @@ """Constants for Life360 integration.""" + +from datetime import timedelta +import logging + DOMAIN = "life360" +LOGGER = logging.getLogger(__package__) + +ATTRIBUTION = "Data provided by life360.com" +COMM_MAX_RETRIES = 2 +COMM_TIMEOUT = 3.05 +SPEED_FACTOR_MPH = 2.25 +SPEED_DIGITS = 1 +UPDATE_INTERVAL = timedelta(seconds=10) + +ATTR_ADDRESS = "address" +ATTR_AT_LOC_SINCE = "at_loc_since" +ATTR_DRIVING = "driving" +ATTR_LAST_SEEN = "last_seen" +ATTR_PLACE = "place" +ATTR_SPEED = "speed" +ATTR_WIFI_ON = "wifi_on" CONF_AUTHORIZATION = "authorization" CONF_CIRCLES = "circles" @@ -13,3 +33,10 @@ CONF_WARNING_THRESHOLD = "warning_threshold" SHOW_DRIVING = "driving" SHOW_MOVING = "moving" + +DEFAULT_OPTIONS = { + CONF_DRIVING_SPEED: None, + CONF_MAX_GPS_ACCURACY: None, + SHOW_DRIVING: False, +} +OPTIONS = list(DEFAULT_OPTIONS.keys()) diff --git a/homeassistant/components/life360/coordinator.py b/homeassistant/components/life360/coordinator.py new file mode 100644 index 00000000000..dc7fdb73a8c --- /dev/null +++ b/homeassistant/components/life360/coordinator.py @@ -0,0 +1,201 @@ +"""DataUpdateCoordinator for the Life360 integration.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import datetime +from typing import Any + +from life360 import Life360, Life360Error, LoginError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + LENGTH_FEET, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util.distance import convert +import homeassistant.util.dt as dt_util + +from .const import ( + COMM_MAX_RETRIES, + COMM_TIMEOUT, + CONF_AUTHORIZATION, + DOMAIN, + LOGGER, + SPEED_DIGITS, + SPEED_FACTOR_MPH, + UPDATE_INTERVAL, +) + + +@dataclass +class Life360Place: + """Life360 Place data.""" + + name: str + latitude: float + longitude: float + radius: float + + +@dataclass +class Life360Circle: + """Life360 Circle data.""" + + name: str + places: dict[str, Life360Place] + + +@dataclass +class Life360Member: + """Life360 Member data.""" + + # Don't include address field in eq comparison because it often changes (back and + # forth) between updates. If it was included there would be way more state changes + # and database updates than is useful. + address: str | None = field(compare=False) + at_loc_since: datetime + battery_charging: bool + battery_level: int + driving: bool + entity_picture: str + gps_accuracy: int + last_seen: datetime + latitude: float + longitude: float + name: str + place: str | None + speed: float + wifi_on: bool + + +@dataclass +class Life360Data: + """Life360 data.""" + + circles: dict[str, Life360Circle] = field(init=False, default_factory=dict) + members: dict[str, Life360Member] = field(init=False, default_factory=dict) + + +class Life360DataUpdateCoordinator(DataUpdateCoordinator): + """Life360 data update coordinator.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize data update coordinator.""" + super().__init__( + hass, + LOGGER, + name=f"{DOMAIN} ({entry.unique_id})", + update_interval=UPDATE_INTERVAL, + ) + self._hass = hass + self._api = Life360( + timeout=COMM_TIMEOUT, + max_retries=COMM_MAX_RETRIES, + authorization=entry.data[CONF_AUTHORIZATION], + ) + + async def _retrieve_data(self, func: str, *args: Any) -> list[dict[str, Any]]: + """Get data from Life360.""" + try: + return await self._hass.async_add_executor_job( + getattr(self._api, func), *args + ) + except LoginError as exc: + LOGGER.debug("Login error: %s", exc) + raise ConfigEntryAuthFailed from exc + except Life360Error as exc: + LOGGER.debug("%s: %s", exc.__class__.__name__, exc) + raise UpdateFailed from exc + + async def _async_update_data(self) -> Life360Data: + """Get & process data from Life360.""" + + data = Life360Data() + + for circle in await self._retrieve_data("get_circles"): + circle_id = circle["id"] + circle_members = await self._retrieve_data("get_circle_members", circle_id) + circle_places = await self._retrieve_data("get_circle_places", circle_id) + + data.circles[circle_id] = Life360Circle( + circle["name"], + { + place["id"]: Life360Place( + place["name"], + float(place["latitude"]), + float(place["longitude"]), + float(place["radius"]), + ) + for place in circle_places + }, + ) + + for member in circle_members: + # Member isn't sharing location. + if not int(member["features"]["shareLocation"]): + continue + + # Note that member may be in more than one circle. If that's the case just + # go ahead and process the newly retrieved data (overwriting the older + # data), since it might be slightly newer than what was retrieved while + # processing another circle. + + first = member["firstName"] + last = member["lastName"] + if first and last: + name = " ".join([first, last]) + else: + name = first or last + + loc = member["location"] + if not loc: + if err_msg := member["issues"]["title"]: + if member["issues"]["dialog"]: + err_msg += f": {member['issues']['dialog']}" + else: + err_msg = "Location information missing" + LOGGER.error("%s: %s", name, err_msg) + continue + + place = loc["name"] or None + + if place: + address: str | None = place + else: + address1 = loc["address1"] or None + address2 = loc["address2"] or None + if address1 and address2: + address = ", ".join([address1, address2]) + else: + address = address1 or address2 + + speed = max(0, float(loc["speed"]) * SPEED_FACTOR_MPH) + if self._hass.config.units.is_metric: + speed = convert(speed, LENGTH_MILES, LENGTH_KILOMETERS) + + data.members[member["id"]] = Life360Member( + address, + dt_util.utc_from_timestamp(int(loc["since"])), + bool(int(loc["charge"])), + int(float(loc["battery"])), + bool(int(loc["isDriving"])), + member["avatar"], + # Life360 reports accuracy in feet, but Device Tracker expects + # gps_accuracy in meters. + round(convert(float(loc["accuracy"]), LENGTH_FEET, LENGTH_METERS)), + dt_util.utc_from_timestamp(int(loc["timestamp"])), + float(loc["latitude"]), + float(loc["longitude"]), + name, + place, + round(speed, SPEED_DIGITS), + bool(int(loc["wifiState"])), + ) + + return data diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index 2451a237a1e..a38181a6830 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -1,432 +1,244 @@ """Support for Life360 device tracking.""" + from __future__ import annotations -from collections.abc import Callable -from datetime import timedelta -import logging +from collections.abc import Mapping +from typing import Any, cast -from life360 import Life360Error -import voluptuous as vol - -from homeassistant.components.device_tracker import ( - CONF_SCAN_INTERVAL, - DOMAIN as DEVICE_TRACKER_DOMAIN, +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_BATTERY_CHARGING +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, ) -from homeassistant.components.zone import async_active_zone -from homeassistant.const import ( - ATTR_BATTERY_CHARGING, - ATTR_ENTITY_ID, - CONF_PREFIX, - LENGTH_FEET, - LENGTH_KILOMETERS, - LENGTH_METERS, - LENGTH_MILES, - STATE_UNKNOWN, -) -from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_time_interval -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util.async_ import run_callback_threadsafe -from homeassistant.util.distance import convert -import homeassistant.util.dt as dt_util from .const import ( - CONF_CIRCLES, + ATTR_ADDRESS, + ATTR_AT_LOC_SINCE, + ATTR_DRIVING, + ATTR_LAST_SEEN, + ATTR_PLACE, + ATTR_SPEED, + ATTR_WIFI_ON, + ATTRIBUTION, CONF_DRIVING_SPEED, - CONF_ERROR_THRESHOLD, CONF_MAX_GPS_ACCURACY, - CONF_MAX_UPDATE_WAIT, - CONF_MEMBERS, - CONF_SHOW_AS_STATE, - CONF_WARNING_THRESHOLD, DOMAIN, + LOGGER, SHOW_DRIVING, - SHOW_MOVING, ) -_LOGGER = logging.getLogger(__name__) - -SPEED_FACTOR_MPH = 2.25 -EVENT_DELAY = timedelta(seconds=30) - -ATTR_ADDRESS = "address" -ATTR_AT_LOC_SINCE = "at_loc_since" -ATTR_DRIVING = "driving" -ATTR_LAST_SEEN = "last_seen" -ATTR_MOVING = "moving" -ATTR_PLACE = "place" -ATTR_RAW_SPEED = "raw_speed" -ATTR_SPEED = "speed" -ATTR_WAIT = "wait" -ATTR_WIFI_ON = "wifi_on" - -EVENT_UPDATE_OVERDUE = "life360_update_overdue" -EVENT_UPDATE_RESTORED = "life360_update_restored" +_LOC_ATTRS = ( + "address", + "at_loc_since", + "driving", + "gps_accuracy", + "last_seen", + "latitude", + "longitude", + "place", + "speed", +) -def _include_name(filter_dict, name): - if not name: +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the device tracker platform.""" + coordinator = hass.data[DOMAIN].coordinators[entry.entry_id] + tracked_members = hass.data[DOMAIN].tracked_members + logged_circles = hass.data[DOMAIN].logged_circles + logged_places = hass.data[DOMAIN].logged_places + + @callback + def process_data(new_members_only: bool = True) -> None: + """Process new Life360 data.""" + for circle_id, circle in coordinator.data.circles.items(): + if circle_id not in logged_circles: + logged_circles.append(circle_id) + LOGGER.debug("Circle: %s", circle.name) + + new_places = [] + for place_id, place in circle.places.items(): + if place_id not in logged_places: + logged_places.append(place_id) + new_places.append(place) + if new_places: + msg = f"Places from {circle.name}:" + for place in new_places: + msg += f"\n- name: {place.name}" + msg += f"\n latitude: {place.latitude}" + msg += f"\n longitude: {place.longitude}" + msg += f"\n radius: {place.radius}" + LOGGER.debug(msg) + + new_entities = [] + for member_id, member in coordinator.data.members.items(): + tracked_by_account = tracked_members.get(member_id) + if new_member := not tracked_by_account: + tracked_members[member_id] = entry.unique_id + LOGGER.debug("Member: %s", member.name) + if ( + new_member + or tracked_by_account == entry.unique_id + and not new_members_only + ): + new_entities.append(Life360DeviceTracker(coordinator, member_id)) + if new_entities: + async_add_entities(new_entities) + + process_data(new_members_only=False) + entry.async_on_unload(coordinator.async_add_listener(process_data)) + + +class Life360DeviceTracker(CoordinatorEntity, TrackerEntity): + """Life360 Device Tracker.""" + + _attr_attribution = ATTRIBUTION + + def __init__(self, coordinator: DataUpdateCoordinator, member_id: str) -> None: + """Initialize Life360 Entity.""" + super().__init__(coordinator) + self._attr_unique_id = member_id + + self._data = coordinator.data.members[self.unique_id] + + self._attr_name = self._data.name + self._attr_entity_picture = self._data.entity_picture + + self._prev_data = self._data + + @property + def _options(self) -> Mapping[str, Any]: + """Shortcut to config entry options.""" + return cast(Mapping[str, Any], self.coordinator.config_entry.options) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + # Get a shortcut to this member's data. Can't guarantee it's the same dict every + # update, or that there is even data for this member every update, so need to + # update shortcut each time. + self._data = self.coordinator.data.members.get(self.unique_id) + + if self.available: + # If nothing important has changed, then skip the update altogether. + if self._data == self._prev_data: + return + + # Check if we should effectively throw out new location data. + last_seen = self._data.last_seen + prev_seen = self._prev_data.last_seen + max_gps_acc = self._options.get(CONF_MAX_GPS_ACCURACY) + bad_last_seen = last_seen < prev_seen + bad_accuracy = ( + max_gps_acc is not None and self.location_accuracy > max_gps_acc + ) + if bad_last_seen or bad_accuracy: + if bad_last_seen: + LOGGER.warning( + "%s: Ignoring location update because " + "last_seen (%s) < previous last_seen (%s)", + self.entity_id, + last_seen, + prev_seen, + ) + if bad_accuracy: + LOGGER.warning( + "%s: Ignoring location update because " + "expected GPS accuracy (%0.1f) is not met: %i", + self.entity_id, + max_gps_acc, + self.location_accuracy, + ) + # Overwrite new location related data with previous values. + for attr in _LOC_ATTRS: + setattr(self._data, attr, getattr(self._prev_data, attr)) + + self._prev_data = self._data + + super()._handle_coordinator_update() + + @property + def force_update(self) -> bool: + """Return True if state updates should be forced.""" return False - if not filter_dict: - return True - name = name.lower() - if filter_dict["include"]: - return name in filter_dict["list"] - return name not in filter_dict["list"] + @property + def available(self) -> bool: + """Return if entity is available.""" + # Guard against member not being in last update for some reason. + return super().available and self._data is not None -def _exc_msg(exc): - return f"{exc.__class__.__name__}: {exc}" + @property + def entity_picture(self) -> str | None: + """Return the entity picture to use in the frontend, if any.""" + if self.available: + self._attr_entity_picture = self._data.entity_picture + return super().entity_picture + # All of the following will only be called if self.available is True. -def _dump_filter(filter_dict, desc, func=lambda x: x): - if not filter_dict: - return - _LOGGER.debug( - "%scluding %s: %s", - "In" if filter_dict["include"] else "Ex", - desc, - ", ".join([func(name) for name in filter_dict["list"]]), - ) + @property + def battery_level(self) -> int | None: + """Return the battery level of the device. + Percentage from 0-100. + """ + return self._data.battery_level -def setup_scanner( - hass: HomeAssistant, - config: ConfigType, - see: Callable[..., None], - discovery_info: DiscoveryInfoType | None = None, -) -> bool: - """Set up device scanner.""" - config = hass.data[DOMAIN]["config"] - apis = hass.data[DOMAIN]["apis"] - Life360Scanner(hass, config, see, apis) - return True + @property + def source_type(self) -> str: + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_GPS + @property + def location_accuracy(self) -> int: + """Return the location accuracy of the device. -def _utc_from_ts(val): - try: - return dt_util.utc_from_timestamp(float(val)) - except (TypeError, ValueError): + Value in meters. + """ + return self._data.gps_accuracy + + @property + def driving(self) -> bool: + """Return if driving.""" + if (driving_speed := self._options.get(CONF_DRIVING_SPEED)) is not None: + if self._data.speed >= driving_speed: + return True + return self._data.driving + + @property + def location_name(self) -> str | None: + """Return a location name for the current location of the device.""" + if self._options.get(SHOW_DRIVING) and self.driving: + return "Driving" return None + @property + def latitude(self) -> float | None: + """Return latitude value of the device.""" + return self._data.latitude -def _dt_attr_from_ts(timestamp): - utc = _utc_from_ts(timestamp) - if utc: - return utc - return STATE_UNKNOWN + @property + def longitude(self) -> float | None: + """Return longitude value of the device.""" + return self._data.longitude - -def _bool_attr_from_int(val): - try: - return bool(int(val)) - except (TypeError, ValueError): - return STATE_UNKNOWN - - -class Life360Scanner: - """Life360 device scanner.""" - - def __init__(self, hass, config, see, apis): - """Initialize Life360Scanner.""" - self._hass = hass - self._see = see - self._max_gps_accuracy = config.get(CONF_MAX_GPS_ACCURACY) - self._max_update_wait = config.get(CONF_MAX_UPDATE_WAIT) - self._prefix = config[CONF_PREFIX] - self._circles_filter = config.get(CONF_CIRCLES) - self._members_filter = config.get(CONF_MEMBERS) - self._driving_speed = config.get(CONF_DRIVING_SPEED) - self._show_as_state = config[CONF_SHOW_AS_STATE] - self._apis = apis - self._errs = {} - self._error_threshold = config[CONF_ERROR_THRESHOLD] - self._warning_threshold = config[CONF_WARNING_THRESHOLD] - self._max_errs = self._error_threshold + 1 - self._dev_data = {} - self._circles_logged = set() - self._members_logged = set() - - _dump_filter(self._circles_filter, "Circles") - _dump_filter(self._members_filter, "device IDs", self._dev_id) - - self._started = dt_util.utcnow() - self._update_life360() - track_time_interval( - self._hass, self._update_life360, config[CONF_SCAN_INTERVAL] - ) - - def _dev_id(self, name): - return self._prefix + name - - def _ok(self, key): - if self._errs.get(key, 0) >= self._max_errs: - _LOGGER.error("%s: OK again", key) - self._errs[key] = 0 - - def _err(self, key, err_msg): - _errs = self._errs.get(key, 0) - if _errs < self._max_errs: - self._errs[key] = _errs = _errs + 1 - msg = f"{key}: {err_msg}" - if _errs >= self._error_threshold: - if _errs == self._max_errs: - msg = f"Suppressing further errors until OK: {msg}" - _LOGGER.error(msg) - elif _errs >= self._warning_threshold: - _LOGGER.warning(msg) - - def _exc(self, key, exc): - self._err(key, _exc_msg(exc)) - - def _prev_seen(self, dev_id, last_seen): - prev_seen, reported = self._dev_data.get(dev_id, (None, False)) - - if self._max_update_wait: - now = dt_util.utcnow() - most_recent_update = last_seen or prev_seen or self._started - overdue = now - most_recent_update > self._max_update_wait - if overdue and not reported and now - self._started > EVENT_DELAY: - self._hass.bus.fire( - EVENT_UPDATE_OVERDUE, - {ATTR_ENTITY_ID: f"{DEVICE_TRACKER_DOMAIN}.{dev_id}"}, - ) - reported = True - elif not overdue and reported: - self._hass.bus.fire( - EVENT_UPDATE_RESTORED, - { - ATTR_ENTITY_ID: f"{DEVICE_TRACKER_DOMAIN}.{dev_id}", - ATTR_WAIT: str(last_seen - (prev_seen or self._started)).split( - ".", maxsplit=1 - )[0], - }, - ) - reported = False - - # Don't remember last_seen unless it's really an update. - if not last_seen or prev_seen and last_seen <= prev_seen: - last_seen = prev_seen - self._dev_data[dev_id] = last_seen, reported - - return prev_seen - - def _update_member(self, member, dev_id): - loc = member.get("location") - try: - last_seen = _utc_from_ts(loc.get("timestamp")) - except AttributeError: - last_seen = None - prev_seen = self._prev_seen(dev_id, last_seen) - - if not loc: - if err_msg := member["issues"]["title"]: - if member["issues"]["dialog"]: - err_msg += f": {member['issues']['dialog']}" - else: - err_msg = "Location information missing" - self._err(dev_id, err_msg) - return - - # Only update when we truly have an update. - if not last_seen: - _LOGGER.warning("%s: Ignoring update because timestamp is missing", dev_id) - return - if prev_seen and last_seen < prev_seen: - _LOGGER.warning( - "%s: Ignoring update because timestamp is older than last timestamp", - dev_id, - ) - _LOGGER.debug("%s < %s", last_seen, prev_seen) - return - if last_seen == prev_seen: - return - - lat = loc.get("latitude") - lon = loc.get("longitude") - gps_accuracy = loc.get("accuracy") - try: - lat = float(lat) - lon = float(lon) - # Life360 reports accuracy in feet, but Device Tracker expects - # gps_accuracy in meters. - gps_accuracy = round( - convert(float(gps_accuracy), LENGTH_FEET, LENGTH_METERS) - ) - except (TypeError, ValueError): - self._err(dev_id, f"GPS data invalid: {lat}, {lon}, {gps_accuracy}") - return - - self._ok(dev_id) - - msg = f"Updating {dev_id}" - if prev_seen: - msg += f"; Time since last update: {last_seen - prev_seen}" - _LOGGER.debug(msg) - - if self._max_gps_accuracy is not None and gps_accuracy > self._max_gps_accuracy: - _LOGGER.warning( - "%s: Ignoring update because expected GPS " - "accuracy (%.0f) is not met: %.0f", - dev_id, - self._max_gps_accuracy, - gps_accuracy, - ) - return - - # Get raw attribute data, converting empty strings to None. - place = loc.get("name") or None - address1 = loc.get("address1") or None - address2 = loc.get("address2") or None - if address1 and address2: - address = ", ".join([address1, address2]) - else: - address = address1 or address2 - raw_speed = loc.get("speed") or None - driving = _bool_attr_from_int(loc.get("isDriving")) - moving = _bool_attr_from_int(loc.get("inTransit")) - try: - battery = int(float(loc.get("battery"))) - except (TypeError, ValueError): - battery = None - - # Try to convert raw speed into real speed. - try: - speed = float(raw_speed) * SPEED_FACTOR_MPH - if self._hass.config.units.is_metric: - speed = convert(speed, LENGTH_MILES, LENGTH_KILOMETERS) - speed = max(0, round(speed)) - except (TypeError, ValueError): - speed = STATE_UNKNOWN - - # Make driving attribute True if it isn't and we can derive that it - # should be True from other data. - if ( - driving in (STATE_UNKNOWN, False) - and self._driving_speed is not None - and speed != STATE_UNKNOWN - ): - driving = speed >= self._driving_speed - - attrs = { - ATTR_ADDRESS: address, - ATTR_AT_LOC_SINCE: _dt_attr_from_ts(loc.get("since")), - ATTR_BATTERY_CHARGING: _bool_attr_from_int(loc.get("charge")), - ATTR_DRIVING: driving, - ATTR_LAST_SEEN: last_seen, - ATTR_MOVING: moving, - ATTR_PLACE: place, - ATTR_RAW_SPEED: raw_speed, - ATTR_SPEED: speed, - ATTR_WIFI_ON: _bool_attr_from_int(loc.get("wifiState")), - } - - # If user wants driving or moving to be shown as state, and current - # location is not in a HA zone, then set location name accordingly. - loc_name = None - active_zone = run_callback_threadsafe( - self._hass.loop, async_active_zone, self._hass, lat, lon, gps_accuracy - ).result() - if not active_zone: - if SHOW_DRIVING in self._show_as_state and driving is True: - loc_name = SHOW_DRIVING - elif SHOW_MOVING in self._show_as_state and moving is True: - loc_name = SHOW_MOVING - - self._see( - dev_id=dev_id, - location_name=loc_name, - gps=(lat, lon), - gps_accuracy=gps_accuracy, - battery=battery, - attributes=attrs, - picture=member.get("avatar"), - ) - - def _update_members(self, members, members_updated): - for member in members: - member_id = member["id"] - if member_id in members_updated: - continue - err_key = "Member data" - try: - first = member.get("firstName") - last = member.get("lastName") - if first and last: - full_name = " ".join([first, last]) - else: - full_name = first or last - slug_name = cv.slugify(full_name) - include_member = _include_name(self._members_filter, slug_name) - dev_id = self._dev_id(slug_name) - if member_id not in self._members_logged: - self._members_logged.add(member_id) - _LOGGER.debug( - "%s -> %s: will%s be tracked, id=%s", - full_name, - dev_id, - "" if include_member else " NOT", - member_id, - ) - sharing = bool(int(member["features"]["shareLocation"])) - except (KeyError, TypeError, ValueError, vol.Invalid): - self._err(err_key, member) - continue - self._ok(err_key) - - if include_member and sharing: - members_updated.append(member_id) - self._update_member(member, dev_id) - - def _update_life360(self, now=None): - circles_updated = [] - members_updated = [] - - for api in self._apis.values(): - err_key = "get_circles" - try: - circles = api.get_circles() - except Life360Error as exc: - self._exc(err_key, exc) - continue - self._ok(err_key) - - for circle in circles: - circle_id = circle["id"] - if circle_id in circles_updated: - continue - circles_updated.append(circle_id) - circle_name = circle["name"] - incl_circle = _include_name(self._circles_filter, circle_name) - if circle_id not in self._circles_logged: - self._circles_logged.add(circle_id) - _LOGGER.debug( - "%s Circle: will%s be included, id=%s", - circle_name, - "" if incl_circle else " NOT", - circle_id, - ) - try: - places = api.get_circle_places(circle_id) - place_data = "Circle's Places:" - for place in places: - place_data += f"\n- name: {place['name']}" - place_data += f"\n latitude: {place['latitude']}" - place_data += f"\n longitude: {place['longitude']}" - place_data += f"\n radius: {place['radius']}" - if not places: - place_data += " None" - _LOGGER.debug(place_data) - except (Life360Error, KeyError): - pass - if incl_circle: - err_key = f'get_circle_members "{circle_name}"' - try: - members = api.get_circle_members(circle_id) - except Life360Error as exc: - self._exc(err_key, exc) - continue - self._ok(err_key) - - self._update_members(members, members_updated) + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return entity specific state attributes.""" + attrs = {} + attrs[ATTR_ADDRESS] = self._data.address + attrs[ATTR_AT_LOC_SINCE] = self._data.at_loc_since + attrs[ATTR_BATTERY_CHARGING] = self._data.battery_charging + attrs[ATTR_DRIVING] = self.driving + attrs[ATTR_LAST_SEEN] = self._data.last_seen + attrs[ATTR_PLACE] = self._data.place + attrs[ATTR_SPEED] = self._data.speed + attrs[ATTR_WIFI_ON] = self._data.wifi_on + return attrs diff --git a/homeassistant/components/life360/helpers.py b/homeassistant/components/life360/helpers.py deleted file mode 100644 index 0eb215743df..00000000000 --- a/homeassistant/components/life360/helpers.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Life360 integration helpers.""" -from life360 import Life360 - - -def get_api(authorization=None): - """Create Life360 api object.""" - return Life360(timeout=3.05, max_retries=2, authorization=authorization) diff --git a/homeassistant/components/life360/strings.json b/homeassistant/components/life360/strings.json index 06ac88467ef..cc31ca64a08 100644 --- a/homeassistant/components/life360/strings.json +++ b/homeassistant/components/life360/strings.json @@ -2,26 +2,43 @@ "config": { "step": { "user": { - "title": "Life360 Account Info", + "title": "Configure Life360 Account", "data": { "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]" - }, - "description": "To set advanced options, see [Life360 documentation]({docs_url}).\nYou may want to do that before adding accounts." + } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { - "invalid_username": "Invalid username", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, - "create_entry": { - "default": "To set advanced options, see [Life360 documentation]({docs_url})." - }, "abort": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + }, + "options": { + "step": { + "init": { + "title": "Account Options", + "data": { + "limit_gps_acc": "Limit GPS accuracy", + "max_gps_accuracy": "Max GPS accuracy (meters)", + "set_drive_speed": "Set driving speed threshold", + "driving_speed": "Driving speed", + "driving": "Show driving as state" + } + } } } } diff --git a/homeassistant/components/life360/translations/en.json b/homeassistant/components/life360/translations/en.json index fa836c62b61..b4c9eb452f6 100644 --- a/homeassistant/components/life360/translations/en.json +++ b/homeassistant/components/life360/translations/en.json @@ -1,27 +1,44 @@ { - "config": { - "abort": { - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, - "create_entry": { - "default": "To set advanced options, see [Life360 documentation]({docs_url})." - }, - "error": { - "already_configured": "Account is already configured", - "invalid_auth": "Invalid authentication", - "invalid_username": "Invalid username", - "unknown": "Unexpected error" - }, - "step": { - "user": { - "data": { - "password": "Password", - "username": "Username" - }, - "description": "To set advanced options, see [Life360 documentation]({docs_url}).\nYou may want to do that before adding accounts.", - "title": "Life360 Account Info" - } + "config": { + "step": { + "user": { + "title": "Configure Life360 Account", + "data": { + "username": "Username", + "password": "Password" } + }, + "reauth_confirm": { + "title": "Reauthenticate Integration", + "data": { + "password": "Password" + } + } + }, + "error": { + "invalid_auth": "Invalid authentication", + "already_configured": "Account is already configured", + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "abort": { + "invalid_auth": "Invalid authentication", + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" } -} \ No newline at end of file + }, + "options": { + "step": { + "init": { + "title": "Account Options", + "data": { + "limit_gps_acc": "Limit GPS accuracy", + "max_gps_accuracy": "Max GPS accuracy (meters)", + "set_drive_speed": "Set driving speed threshold", + "driving_speed": "Driving speed", + "driving": "Show driving as state" + } + } + } + } +} diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f579b9395d9..94d59b40526 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -672,6 +672,9 @@ librouteros==3.2.0 # homeassistant.components.soundtouch libsoundtouch==0.8 +# homeassistant.components.life360 +life360==4.1.1 + # homeassistant.components.logi_circle logi_circle==0.2.3 diff --git a/tests/components/life360/__init__.py b/tests/components/life360/__init__.py new file mode 100644 index 00000000000..0f68b4a343c --- /dev/null +++ b/tests/components/life360/__init__.py @@ -0,0 +1 @@ +"""Tests for the Life360 integration.""" diff --git a/tests/components/life360/test_config_flow.py b/tests/components/life360/test_config_flow.py new file mode 100644 index 00000000000..0b5b850ac23 --- /dev/null +++ b/tests/components/life360/test_config_flow.py @@ -0,0 +1,309 @@ +"""Test the Life360 config flow.""" + +from unittest.mock import patch + +from life360 import Life360Error, LoginError +import pytest +import voluptuous as vol + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.life360.const import ( + CONF_AUTHORIZATION, + CONF_DRIVING_SPEED, + CONF_MAX_GPS_ACCURACY, + DEFAULT_OPTIONS, + DOMAIN, + SHOW_DRIVING, +) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry + +TEST_USER = "Test@Test.com" +TEST_PW = "password" +TEST_PW_3 = "password_3" +TEST_AUTHORIZATION = "authorization_string" +TEST_AUTHORIZATION_2 = "authorization_string_2" +TEST_AUTHORIZATION_3 = "authorization_string_3" +TEST_MAX_GPS_ACCURACY = "300" +TEST_DRIVING_SPEED = "18" +TEST_SHOW_DRIVING = True + +USER_INPUT = {CONF_USERNAME: TEST_USER, CONF_PASSWORD: TEST_PW} + +TEST_CONFIG_DATA = { + CONF_USERNAME: TEST_USER, + CONF_PASSWORD: TEST_PW, + CONF_AUTHORIZATION: TEST_AUTHORIZATION, +} +TEST_CONFIG_DATA_2 = { + CONF_USERNAME: TEST_USER, + CONF_PASSWORD: TEST_PW, + CONF_AUTHORIZATION: TEST_AUTHORIZATION_2, +} +TEST_CONFIG_DATA_3 = { + CONF_USERNAME: TEST_USER, + CONF_PASSWORD: TEST_PW_3, + CONF_AUTHORIZATION: TEST_AUTHORIZATION_3, +} + +USER_OPTIONS = { + "limit_gps_acc": True, + CONF_MAX_GPS_ACCURACY: TEST_MAX_GPS_ACCURACY, + "set_drive_speed": True, + CONF_DRIVING_SPEED: TEST_DRIVING_SPEED, + SHOW_DRIVING: TEST_SHOW_DRIVING, +} +TEST_OPTIONS = { + CONF_MAX_GPS_ACCURACY: float(TEST_MAX_GPS_ACCURACY), + CONF_DRIVING_SPEED: float(TEST_DRIVING_SPEED), + SHOW_DRIVING: TEST_SHOW_DRIVING, +} + + +# ========== Common Fixtures & Functions =============================================== + + +@pytest.fixture(name="life360", autouse=True) +def life360_fixture(): + """Mock life360 config entry setup & unload.""" + with patch( + "homeassistant.components.life360.async_setup_entry", return_value=True + ), patch("homeassistant.components.life360.async_unload_entry", return_value=True): + yield + + +@pytest.fixture +def life360_api(): + """Mock Life360 api.""" + with patch("homeassistant.components.life360.config_flow.Life360") as mock: + yield mock.return_value + + +def create_config_entry(hass, state=None): + """Create mock config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=TEST_CONFIG_DATA, + version=1, + state=state, + options=DEFAULT_OPTIONS, + unique_id=TEST_USER.lower(), + ) + config_entry.add_to_hass(hass) + return config_entry + + +# ========== User Flow Tests =========================================================== + + +async def test_user_show_form(hass, life360_api): + """Test that the form is served with no input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + life360_api.get_authorization.assert_not_called() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert not result["errors"] + + schema = result["data_schema"].schema + assert set(schema) == set(USER_INPUT) + # username and password fields should be empty. + keys = list(schema) + for key in USER_INPUT: + assert keys[keys.index(key)].default == vol.UNDEFINED + + +async def test_user_config_flow_success(hass, life360_api): + """Test a successful user config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + life360_api.get_authorization.return_value = TEST_AUTHORIZATION + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], USER_INPUT + ) + await hass.async_block_till_done() + + life360_api.get_authorization.assert_called_once() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == TEST_USER.lower() + assert result["data"] == TEST_CONFIG_DATA + assert result["options"] == DEFAULT_OPTIONS + + +@pytest.mark.parametrize( + "exception,error", [(LoginError, "invalid_auth"), (Life360Error, "cannot_connect")] +) +async def test_user_config_flow_error(hass, life360_api, caplog, exception, error): + """Test a user config flow with an error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + life360_api.get_authorization.side_effect = exception("test reason") + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], USER_INPUT + ) + await hass.async_block_till_done() + + life360_api.get_authorization.assert_called_once() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] + assert result["errors"]["base"] == error + + assert "test reason" in caplog.text + + schema = result["data_schema"].schema + assert set(schema) == set(USER_INPUT) + # username and password fields should be prefilled with current values. + keys = list(schema) + for key, val in USER_INPUT.items(): + default = keys[keys.index(key)].default + assert default != vol.UNDEFINED + assert default() == val + + +async def test_user_config_flow_already_configured(hass, life360_api): + """Test a user config flow with an account already configured.""" + create_config_entry(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], USER_INPUT + ) + await hass.async_block_till_done() + + life360_api.get_authorization.assert_not_called() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +# ========== Reauth Flow Tests ========================================================= + + +@pytest.mark.parametrize("state", [None, config_entries.ConfigEntryState.LOADED]) +async def test_reauth_config_flow_success(hass, life360_api, caplog, state): + """Test a successful reauthorization config flow.""" + config_entry = create_config_entry(hass, state=state) + + # Simulate current username & password are still valid, but authorization string has + # expired, such that getting a new authorization string from server is successful. + life360_api.get_authorization.return_value = TEST_AUTHORIZATION_2 + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + "title_placeholders": {"name": config_entry.title}, + "unique_id": config_entry.unique_id, + }, + data=config_entry.data, + ) + await hass.async_block_till_done() + + life360_api.get_authorization.assert_called_once() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + + assert "Reauthorization successful" in caplog.text + + assert config_entry.data == TEST_CONFIG_DATA_2 + + +async def test_reauth_config_flow_login_error(hass, life360_api, caplog): + """Test a reauthorization config flow with a login error.""" + config_entry = create_config_entry(hass) + + # Simulate current username & password are invalid, which results in a form + # requesting new password, with old password as default value. + life360_api.get_authorization.side_effect = LoginError("test reason") + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + "title_placeholders": {"name": config_entry.title}, + "unique_id": config_entry.unique_id, + }, + data=config_entry.data, + ) + await hass.async_block_till_done() + + life360_api.get_authorization.assert_called_once() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] + assert result["errors"]["base"] == "invalid_auth" + + assert "test reason" in caplog.text + + schema = result["data_schema"].schema + assert len(schema) == 1 + assert "password" in schema + key = list(schema)[0] + assert key.default() == TEST_PW + + # Simulate getting a new, valid password. + life360_api.get_authorization.reset_mock(side_effect=True) + life360_api.get_authorization.return_value = TEST_AUTHORIZATION_3 + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PASSWORD: TEST_PW_3} + ) + await hass.async_block_till_done() + + life360_api.get_authorization.assert_called_once() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + + assert "Reauthorization successful" in caplog.text + + assert config_entry.data == TEST_CONFIG_DATA_3 + + +# ========== Option flow Tests ========================================================= + + +async def test_options_flow(hass): + """Test an options flow.""" + config_entry = create_config_entry(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + assert not result["errors"] + + schema = result["data_schema"].schema + assert set(schema) == set(USER_OPTIONS) + + flow_id = result["flow_id"] + + result = await hass.config_entries.options.async_configure(flow_id, USER_OPTIONS) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == TEST_OPTIONS + + assert config_entry.options == TEST_OPTIONS From d6e9118f36ff43055b2148744b304a5208398496 Mon Sep 17 00:00:00 2001 From: Jeef Date: Wed, 29 Jun 2022 11:01:18 -0600 Subject: [PATCH 1978/3516] IntelliFire DHCP Discovery Patch (#72617) Co-authored-by: J. Nick Koston --- homeassistant/components/intellifire/config_flow.py | 12 ++++++++---- homeassistant/components/intellifire/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/intellifire/config_flow.py b/homeassistant/components/intellifire/config_flow.py index 23bd92b0715..4556668b702 100644 --- a/homeassistant/components/intellifire/config_flow.py +++ b/homeassistant/components/intellifire/config_flow.py @@ -31,15 +31,16 @@ class DiscoveredHostInfo: serial: str | None -async def validate_host_input(host: str) -> str: +async def validate_host_input(host: str, dhcp_mode: bool = False) -> str: """Validate the user input allows us to connect. Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ LOGGER.debug("Instantiating IntellifireAPI with host: [%s]", host) api = IntellifireAPILocal(fireplace_ip=host) - await api.poll() + await api.poll(supress_warnings=dhcp_mode) serial = api.data.serial + LOGGER.debug("Found a fireplace: %s", serial) # Return the serial number which will be used to calculate a unique ID for the device/sensors return serial @@ -240,14 +241,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: """Handle DHCP Discovery.""" - LOGGER.debug("STEP: dhcp") # Run validation logic on ip host = discovery_info.ip + LOGGER.debug("STEP: dhcp for host %s", host) self._async_abort_entries_match({CONF_HOST: host}) try: - self._serial = await validate_host_input(host) + self._serial = await validate_host_input(host, dhcp_mode=True) except (ConnectionError, ClientConnectionError): + LOGGER.debug( + "DHCP Discovery has determined %s is not an IntelliFire device", host + ) return self.async_abort(reason="not_intellifire_device") await self.async_set_unique_id(self._serial) diff --git a/homeassistant/components/intellifire/manifest.json b/homeassistant/components/intellifire/manifest.json index cd1a12a36bf..e2ae4bb8abe 100644 --- a/homeassistant/components/intellifire/manifest.json +++ b/homeassistant/components/intellifire/manifest.json @@ -3,7 +3,7 @@ "name": "IntelliFire", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/intellifire", - "requirements": ["intellifire4py==2.0.0"], + "requirements": ["intellifire4py==2.0.1"], "codeowners": ["@jeeftor"], "iot_class": "local_polling", "loggers": ["intellifire4py"], diff --git a/requirements_all.txt b/requirements_all.txt index 064f686dc49..e67cd8dc3de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -894,7 +894,7 @@ influxdb==5.3.1 insteon-frontend-home-assistant==0.1.1 # homeassistant.components.intellifire -intellifire4py==2.0.0 +intellifire4py==2.0.1 # homeassistant.components.iotawatt iotawattpy==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 94d59b40526..9b9681b38bb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -637,7 +637,7 @@ influxdb==5.3.1 insteon-frontend-home-assistant==0.1.1 # homeassistant.components.intellifire -intellifire4py==2.0.0 +intellifire4py==2.0.1 # homeassistant.components.iotawatt iotawattpy==0.1.0 From 4d673278c7a6405a21201015490c8d1dc433b872 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Wed, 29 Jun 2022 19:09:52 +0200 Subject: [PATCH 1979/3516] Fix color transition when turning on a ZHA light (#74024) * Initial implementation of fixing color transition when turning on a ZHA light * Add off_with_transition attribute, very slightly cleanup * Fix unnecessarily using last off_brightness when just turning on light Now it uses the Zigbee on_off call again if possible (instead of always move_to_level_with_on_off) * Use DEFAULT_TRANSITION constant for color transition, add DEFAULT_MIN_BRIGHTNESS constant * Add _DEFAULT_COLOR_FROM_OFF_TRANSITION = 0 but override transition for Sengled lights to 0.1s --- homeassistant/components/zha/light.py | 93 ++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 2f3379fa6b1..309fdf2699b 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -74,6 +74,7 @@ CAPABILITIES_COLOR_XY = 0x08 CAPABILITIES_COLOR_TEMP = 0x10 DEFAULT_TRANSITION = 1 +DEFAULT_MIN_BRIGHTNESS = 2 UPDATE_COLORLOOP_ACTION = 0x1 UPDATE_COLORLOOP_DIRECTION = 0x2 @@ -118,12 +119,14 @@ class BaseLight(LogMixin, light.LightEntity): """Operations common to all light entities.""" _FORCE_ON = False + _DEFAULT_COLOR_FROM_OFF_TRANSITION = 0 def __init__(self, *args, **kwargs): """Initialize the light.""" super().__init__(*args, **kwargs) self._available: bool = False self._brightness: int | None = None + self._off_with_transition: bool = False self._off_brightness: int | None = None self._hs_color: tuple[float, float] | None = None self._color_temp: int | None = None @@ -143,7 +146,10 @@ class BaseLight(LogMixin, light.LightEntity): @property def extra_state_attributes(self) -> dict[str, Any]: """Return state attributes.""" - attributes = {"off_brightness": self._off_brightness} + attributes = { + "off_with_transition": self._off_with_transition, + "off_brightness": self._off_brightness, + } return attributes @property @@ -224,17 +230,53 @@ class BaseLight(LogMixin, light.LightEntity): effect = kwargs.get(light.ATTR_EFFECT) flash = kwargs.get(light.ATTR_FLASH) - if brightness is None and self._off_brightness is not None: + # If the light is currently off but a turn_on call with a color/temperature is sent, + # the light needs to be turned on first at a low brightness level where the light is immediately transitioned + # to the correct color. Afterwards, the transition is only from the low brightness to the new brightness. + # Otherwise, the transition is from the color the light had before being turned on to the new color. + # This can look especially bad with transitions longer than a second. + color_provided_from_off = ( + not self._state + and brightness_supported(self._attr_supported_color_modes) + and (light.ATTR_COLOR_TEMP in kwargs or light.ATTR_HS_COLOR in kwargs) + ) + final_duration = duration + if color_provided_from_off: + # Set the duration for the color changing commands to 0. + duration = 0 + + if ( + brightness is None + and (self._off_with_transition or color_provided_from_off) + and self._off_brightness is not None + ): brightness = self._off_brightness + if brightness is not None: + level = min(254, brightness) + else: + level = self._brightness or 254 + t_log = {} - if (brightness is not None or transition) and brightness_supported( - self._attr_supported_color_modes + + if color_provided_from_off: + # If the light is currently off, we first need to turn it on at a low brightness level with no transition. + # After that, we set it to the desired color/temperature with no transition. + result = await self._level_channel.move_to_level_with_on_off( + DEFAULT_MIN_BRIGHTNESS, self._DEFAULT_COLOR_FROM_OFF_TRANSITION + ) + t_log["move_to_level_with_on_off"] = result + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + self.debug("turned on: %s", t_log) + return + # Currently only setting it to "on", as the correct level state will be set at the second move_to_level call + self._state = True + + if ( + (brightness is not None or transition) + and not color_provided_from_off + and brightness_supported(self._attr_supported_color_modes) ): - if brightness is not None: - level = min(254, brightness) - else: - level = self._brightness or 254 result = await self._level_channel.move_to_level_with_on_off( level, duration ) @@ -246,7 +288,11 @@ class BaseLight(LogMixin, light.LightEntity): if level: self._brightness = level - if brightness is None or (self._FORCE_ON and brightness): + if ( + brightness is None + and not color_provided_from_off + or (self._FORCE_ON and brightness) + ): # since some lights don't always turn on with move_to_level_with_on_off, # we should call the on command on the on_off cluster if brightness is not 0. result = await self._on_off_channel.on() @@ -255,6 +301,7 @@ class BaseLight(LogMixin, light.LightEntity): self.debug("turned on: %s", t_log) return self._state = True + if light.ATTR_COLOR_TEMP in kwargs: temperature = kwargs[light.ATTR_COLOR_TEMP] result = await self._color_channel.move_to_color_temp(temperature, duration) @@ -280,6 +327,17 @@ class BaseLight(LogMixin, light.LightEntity): self._hs_color = hs_color self._color_temp = None + if color_provided_from_off: + # The light is has the correct color, so we can now transition it to the correct brightness level. + result = await self._level_channel.move_to_level(level, final_duration) + t_log["move_to_level_if_color"] = result + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + self.debug("turned on: %s", t_log) + return + self._state = bool(level) + if level: + self._brightness = level + if effect == light.EFFECT_COLORLOOP: result = await self._color_channel.color_loop_set( UPDATE_COLORLOOP_ACTION @@ -311,6 +369,7 @@ class BaseLight(LogMixin, light.LightEntity): ) t_log["trigger_effect"] = result + self._off_with_transition = False self._off_brightness = None self.debug("turned on: %s", t_log) self.async_write_ha_state() @@ -331,8 +390,9 @@ class BaseLight(LogMixin, light.LightEntity): return self._state = False - if duration and supports_level: + if supports_level: # store current brightness so that the next turn_on uses it. + self._off_with_transition = bool(duration) self._off_brightness = self._brightness self.async_write_ha_state() @@ -453,6 +513,8 @@ class Light(BaseLight, ZhaEntity): self._state = last_state.state == STATE_ON if "brightness" in last_state.attributes: self._brightness = last_state.attributes["brightness"] + if "off_with_transition" in last_state.attributes: + self._off_with_transition = last_state.attributes["off_with_transition"] if "off_brightness" in last_state.attributes: self._off_brightness = last_state.attributes["off_brightness"] if "color_mode" in last_state.attributes: @@ -556,6 +618,17 @@ class ForceOnLight(Light): _FORCE_ON = True +@STRICT_MATCH( + channel_names=CHANNEL_ON_OFF, + aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}, + manufacturers={"Sengled"}, +) +class SengledLight(Light): + """Representation of a Sengled light which does not react to move_to_color_temp with 0 as a transition.""" + + _DEFAULT_COLOR_FROM_OFF_TRANSITION = 1 + + @GROUP_MATCH() class LightGroup(BaseLight, ZhaGroupEntity): """Representation of a light group.""" From 466ba47b359d46f1fb03a32aa634c06d936de15c Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Wed, 29 Jun 2022 12:10:21 -0500 Subject: [PATCH 1980/3516] Frontend bump to 20220629.0 (#74180) --- 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 b7c1c0ddeff..23f056ba0da 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220624.0"], + "requirements": ["home-assistant-frontend==20220629.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9f849146fe3..11ffd9c4c0c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220624.0 +home-assistant-frontend==20220629.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index e67cd8dc3de..98a6c6f5a95 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220624.0 +home-assistant-frontend==20220629.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b9681b38bb..64ad3f769b9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -595,7 +595,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220624.0 +home-assistant-frontend==20220629.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 78fe1fb102bc5b514406e0fc9a361ac6959e517d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 Jun 2022 19:29:36 +0200 Subject: [PATCH 1981/3516] Bumped version to 2022.7.0b0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 698f6bee240..df160e1cc6b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 59df3967b6e..2cf2db3f240 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.0.dev0" +version = "2022.7.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 6127a9a0418f7bf08f9eff874f3a76add5698fba Mon Sep 17 00:00:00 2001 From: Jeef Date: Wed, 29 Jun 2022 12:01:38 -0600 Subject: [PATCH 1982/3516] Intellifire climate Entity (#70818) Co-authored-by: J. Nick Koston --- .coveragerc | 1 + .../components/intellifire/__init__.py | 2 +- .../components/intellifire/climate.py | 120 ++++++++++++++++++ homeassistant/components/intellifire/const.py | 2 + .../components/intellifire/coordinator.py | 2 +- 5 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/intellifire/climate.py diff --git a/.coveragerc b/.coveragerc index f52631a57bd..2443e30a2c3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -555,6 +555,7 @@ omit = homeassistant/components/insteon/utils.py homeassistant/components/intellifire/__init__.py homeassistant/components/intellifire/coordinator.py + homeassistant/components/intellifire/climate.py homeassistant/components/intellifire/binary_sensor.py homeassistant/components/intellifire/sensor.py homeassistant/components/intellifire/switch.py diff --git a/homeassistant/components/intellifire/__init__.py b/homeassistant/components/intellifire/__init__.py index 034e74c2aa6..5fb8ac92a72 100644 --- a/homeassistant/components/intellifire/__init__.py +++ b/homeassistant/components/intellifire/__init__.py @@ -20,7 +20,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import CONF_USER_ID, DOMAIN, LOGGER from .coordinator import IntellifireDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.CLIMATE, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/intellifire/climate.py b/homeassistant/components/intellifire/climate.py new file mode 100644 index 00000000000..37a126bf3d6 --- /dev/null +++ b/homeassistant/components/intellifire/climate.py @@ -0,0 +1,120 @@ +"""Intellifire Climate Entities.""" +from __future__ import annotations + +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityDescription, + ClimateEntityFeature, +) +from homeassistant.components.climate.const import HVACMode +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import IntellifireDataUpdateCoordinator +from .const import DEFAULT_THERMOSTAT_TEMP, DOMAIN, LOGGER +from .entity import IntellifireEntity + +INTELLIFIRE_CLIMATES: tuple[ClimateEntityDescription, ...] = ( + ClimateEntityDescription(key="climate", name="Thermostat"), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Configure the fan entry..""" + coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + if coordinator.data.has_thermostat: + async_add_entities( + IntellifireClimate( + coordinator=coordinator, + description=description, + ) + for description in INTELLIFIRE_CLIMATES + ) + + +class IntellifireClimate(IntellifireEntity, ClimateEntity): + """Intellifire climate entity.""" + + entity_description: ClimateEntityDescription + + _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] + _attr_min_temp = 0 + _attr_max_temp = 37 + _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE + _attr_target_temperature_step = 1.0 + _attr_temperature_unit = TEMP_CELSIUS + last_temp = DEFAULT_THERMOSTAT_TEMP + + def __init__( + self, + coordinator: IntellifireDataUpdateCoordinator, + description: ClimateEntityDescription, + ) -> None: + """Configure climate entry - and override last_temp if the thermostat is currently on.""" + super().__init__(coordinator, description) + + if coordinator.data.thermostat_on: + self.last_temp = coordinator.data.thermostat_setpoint_c + + @property + def hvac_mode(self) -> str: + """Return current hvac mode.""" + if self.coordinator.read_api.data.thermostat_on: + return HVACMode.HEAT + return HVACMode.OFF + + async def async_set_temperature(self, **kwargs) -> None: + """Turn on thermostat by setting a target temperature.""" + raw_target_temp = kwargs[ATTR_TEMPERATURE] + self.last_temp = int(raw_target_temp) + LOGGER.debug( + "Setting target temp to %sc %sf", + int(raw_target_temp), + (raw_target_temp * 9 / 5) + 32, + ) + await self.coordinator.control_api.set_thermostat_c( + fireplace=self.coordinator.control_api.default_fireplace, + temp_c=self.last_temp, + ) + + @property + def current_temperature(self) -> float: + """Return the current temperature.""" + return float(self.coordinator.read_api.data.temperature_c) + + @property + def target_temperature(self) -> float: + """Return target temperature.""" + return float(self.coordinator.read_api.data.thermostat_setpoint_c) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set HVAC mode to normal or thermostat control.""" + LOGGER.debug( + "Setting mode to [%s] - using last temp: %s", hvac_mode, self.last_temp + ) + + if hvac_mode == HVACMode.OFF: + await self.coordinator.control_api.turn_off_thermostat( + fireplace=self.coordinator.control_api.default_fireplace + ) + return + + # hvac_mode == HVACMode.HEAT + # 1) Set the desired target temp + await self.coordinator.control_api.set_thermostat_c( + fireplace=self.coordinator.control_api.default_fireplace, + temp_c=self.last_temp, + ) + + # 2) Make sure the fireplace is on! + if not self.coordinator.read_api.data.is_on: + await self.coordinator.control_api.flame_on( + fireplace=self.coordinator.control_api.default_fireplace, + ) diff --git a/homeassistant/components/intellifire/const.py b/homeassistant/components/intellifire/const.py index 2e9a2fabc06..cae25ea11ae 100644 --- a/homeassistant/components/intellifire/const.py +++ b/homeassistant/components/intellifire/const.py @@ -10,3 +10,5 @@ CONF_USER_ID = "user_id" LOGGER = logging.getLogger(__package__) CONF_SERIAL = "serial" + +DEFAULT_THERMOSTAT_TEMP = 21 diff --git a/homeassistant/components/intellifire/coordinator.py b/homeassistant/components/intellifire/coordinator.py index 39f197285d4..356ddedf16d 100644 --- a/homeassistant/components/intellifire/coordinator.py +++ b/homeassistant/components/intellifire/coordinator.py @@ -45,7 +45,7 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData except (ConnectionError, ClientConnectionError) as exception: raise UpdateFailed from exception - LOGGER.info("Failure Count %d", self._api.failed_poll_attempts) + LOGGER.debug("Failure Count %d", self._api.failed_poll_attempts) if self._api.failed_poll_attempts > 10: LOGGER.debug("Too many polling errors - raising exception") raise UpdateFailed From fe68c15a4a4a703081c9b9b810b1f09608c6714c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 29 Jun 2022 20:20:57 +0200 Subject: [PATCH 1983/3516] Bump version to 2022.8.0dev0 (#74184) --- .github/workflows/ci.yaml | 2 +- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index bf690740c6d..54a1997b28e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ on: env: CACHE_VERSION: 10 PIP_CACHE_VERSION: 4 - HA_SHORT_VERSION: 2022.7 + HA_SHORT_VERSION: 2022.8 DEFAULT_PYTHON: 3.9 PRE_COMMIT_CACHE: ~/.cache/pre-commit PIP_CACHE: /tmp/pip-cache diff --git a/homeassistant/const.py b/homeassistant/const.py index 698f6bee240..10523aa6d53 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -6,7 +6,7 @@ from typing import Final from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 -MINOR_VERSION: Final = 7 +MINOR_VERSION: Final = 8 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" diff --git a/pyproject.toml b/pyproject.toml index 59df3967b6e..5e5974d142f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.0.dev0" +version = "2022.8.0.dev0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From e4bd53b39510062b6b340435cd2614bcfa80c158 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 29 Jun 2022 18:52:54 -0400 Subject: [PATCH 1984/3516] Fix duplicate key for motion sensor for UniFi Protect (#74202) --- homeassistant/components/unifiprotect/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index d3bf71a4274..62a4893692b 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -150,7 +150,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ufp_perm=PermRequired.NO_WRITE, ), ProtectBinaryEntityDescription( - key="motion", + key="motion_enabled", name="Detections: Motion", icon="mdi:run-fast", ufp_value="recording_settings.enable_motion_detection", From 0028dc46e652634fd28c53a584a75a11b4cb434e Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 30 Jun 2022 02:10:25 +0300 Subject: [PATCH 1985/3516] Fix Shelly Duo RGBW color mode attribute (#74193) --- homeassistant/components/shelly/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 79db9c509f4..b75e1ad2377 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -215,7 +215,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): def color_mode(self) -> ColorMode: """Return the color mode of the light.""" if self.mode == "color": - if hasattr(self.block, "white"): + if self.wrapper.model in RGBW_MODELS: return ColorMode.RGBW return ColorMode.RGB From 1555f40bad9cdd5198e70f28a8c08ceea6b3c703 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 29 Jun 2022 19:10:38 -0400 Subject: [PATCH 1986/3516] Add UniFi Protect views (#74190) Co-authored-by: J. Nick Koston --- .../components/unifiprotect/__init__.py | 3 + .../components/unifiprotect/views.py | 211 +++++++++ tests/components/unifiprotect/conftest.py | 6 + tests/components/unifiprotect/test_views.py | 427 ++++++++++++++++++ 4 files changed, 647 insertions(+) create mode 100644 homeassistant/components/unifiprotect/views.py create mode 100644 tests/components/unifiprotect/test_views.py diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 40214b60766..5b4059ee1d4 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -40,6 +40,7 @@ from .discovery import async_start_discovery from .migrate import async_migrate_data from .services import async_cleanup_services, async_setup_services from .utils import _async_unifi_mac_from_hass, async_get_devices +from .views import ThumbnailProxyView, VideoProxyView _LOGGER = logging.getLogger(__name__) @@ -92,6 +93,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_service hass.config_entries.async_setup_platforms(entry, PLATFORMS) async_setup_services(hass) + hass.http.register_view(ThumbnailProxyView(hass)) + hass.http.register_view(VideoProxyView(hass)) entry.async_on_unload(entry.add_update_listener(_async_options_updated)) entry.async_on_unload( diff --git a/homeassistant/components/unifiprotect/views.py b/homeassistant/components/unifiprotect/views.py new file mode 100644 index 00000000000..ea523d36dd2 --- /dev/null +++ b/homeassistant/components/unifiprotect/views.py @@ -0,0 +1,211 @@ +"""UniFi Protect Integration views.""" +from __future__ import annotations + +from datetime import datetime +from http import HTTPStatus +import logging +from typing import Any +from urllib.parse import urlencode + +from aiohttp import web +from pyunifiprotect.data import Event +from pyunifiprotect.exceptions import ClientError + +from homeassistant.components.http import HomeAssistantView +from homeassistant.core import HomeAssistant, callback + +from .const import DOMAIN +from .data import ProtectData + +_LOGGER = logging.getLogger(__name__) + + +@callback +def async_generate_thumbnail_url( + event_id: str, + nvr_id: str, + width: int | None = None, + height: int | None = None, +) -> str: + """Generate URL for event thumbnail.""" + + url_format = ThumbnailProxyView.url or "{nvr_id}/{event_id}" + url = url_format.format(nvr_id=nvr_id, event_id=event_id) + + params = {} + if width is not None: + params["width"] = str(width) + if height is not None: + params["height"] = str(height) + + return f"{url}?{urlencode(params)}" + + +@callback +def async_generate_event_video_url(event: Event) -> str: + """Generate URL for event video.""" + + _validate_event(event) + if event.start is None or event.end is None: + raise ValueError("Event is ongoing") + + url_format = VideoProxyView.url or "{nvr_id}/{camera_id}/{start}/{end}" + url = url_format.format( + nvr_id=event.api.bootstrap.nvr.id, + camera_id=event.camera_id, + start=event.start.isoformat(), + end=event.end.isoformat(), + ) + + return url + + +@callback +def _client_error(message: Any, code: HTTPStatus) -> web.Response: + _LOGGER.warning("Client error (%s): %s", code.value, message) + if code == HTTPStatus.BAD_REQUEST: + return web.Response(body=message, status=code) + return web.Response(status=code) + + +@callback +def _400(message: Any) -> web.Response: + return _client_error(message, HTTPStatus.BAD_REQUEST) + + +@callback +def _403(message: Any) -> web.Response: + return _client_error(message, HTTPStatus.FORBIDDEN) + + +@callback +def _404(message: Any) -> web.Response: + return _client_error(message, HTTPStatus.NOT_FOUND) + + +@callback +def _validate_event(event: Event) -> None: + if event.camera is None: + raise ValueError("Event does not have a camera") + if not event.camera.can_read_media(event.api.bootstrap.auth_user): + raise PermissionError(f"User cannot read media from camera: {event.camera.id}") + + +class ProtectProxyView(HomeAssistantView): + """Base class to proxy request to UniFi Protect console.""" + + requires_auth = True + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize a thumbnail proxy view.""" + self.hass = hass + self.data = hass.data[DOMAIN] + + def _get_data_or_404(self, nvr_id: str) -> ProtectData | web.Response: + all_data: list[ProtectData] = [] + + for data in self.data.values(): + if isinstance(data, ProtectData): + if data.api.bootstrap.nvr.id == nvr_id: + return data + all_data.append(data) + return _404("Invalid NVR ID") + + +class ThumbnailProxyView(ProtectProxyView): + """View to proxy event thumbnails from UniFi Protect.""" + + url = "/api/unifiprotect/thumbnail/{nvr_id}/{event_id}" + name = "api:unifiprotect_thumbnail" + + async def get( + self, request: web.Request, nvr_id: str, event_id: str + ) -> web.Response: + """Get Event Thumbnail.""" + + data = self._get_data_or_404(nvr_id) + if isinstance(data, web.Response): + return data + + width: int | str | None = request.query.get("width") + height: int | str | None = request.query.get("height") + + if width is not None: + try: + width = int(width) + except ValueError: + return _400("Invalid width param") + if height is not None: + try: + height = int(height) + except ValueError: + return _400("Invalid height param") + + try: + thumbnail = await data.api.get_event_thumbnail( + event_id, width=width, height=height + ) + except ClientError as err: + return _404(err) + + if thumbnail is None: + return _404("Event thumbnail not found") + + return web.Response(body=thumbnail, content_type="image/jpeg") + + +class VideoProxyView(ProtectProxyView): + """View to proxy video clips from UniFi Protect.""" + + url = "/api/unifiprotect/video/{nvr_id}/{camera_id}/{start}/{end}" + name = "api:unifiprotect_thumbnail" + + async def get( + self, request: web.Request, nvr_id: str, camera_id: str, start: str, end: str + ) -> web.StreamResponse: + """Get Camera Video clip.""" + + data = self._get_data_or_404(nvr_id) + if isinstance(data, web.Response): + return data + + camera = data.api.bootstrap.cameras.get(camera_id) + if camera is None: + return _404(f"Invalid camera ID: {camera_id}") + if not camera.can_read_media(data.api.bootstrap.auth_user): + return _403(f"User cannot read media from camera: {camera.id}") + + try: + start_dt = datetime.fromisoformat(start) + except ValueError: + return _400("Invalid start") + + try: + end_dt = datetime.fromisoformat(end) + except ValueError: + return _400("Invalid end") + + response = web.StreamResponse( + status=200, + reason="OK", + headers={ + "Content-Type": "video/mp4", + }, + ) + + async def iterator(total: int, chunk: bytes | None) -> None: + if not response.prepared: + response.content_length = total + await response.prepare(request) + + if chunk is not None: + await response.write(chunk) + + try: + await camera.get_video(start_dt, end_dt, iterator_callback=iterator) + except ClientError as err: + return _404(err) + + if response.prepared: + await response.write_eof() + return response diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index 51cef190e2f..2a9edb605e7 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from datetime import datetime, timedelta +from functools import partial from ipaddress import IPv4Address import json from typing import Any @@ -102,6 +103,11 @@ def mock_ufp_client(bootstrap: Bootstrap): """Mock ProtectApiClient for testing.""" client = Mock() client.bootstrap = bootstrap + client._bootstrap = bootstrap + client.api_path = "/api" + # functionality from API client tests actually need + client._stream_response = partial(ProtectApiClient._stream_response, client) + client.get_camera_video = partial(ProtectApiClient.get_camera_video, client) nvr = client.bootstrap.nvr nvr._api = client diff --git a/tests/components/unifiprotect/test_views.py b/tests/components/unifiprotect/test_views.py new file mode 100644 index 00000000000..e64a0a87377 --- /dev/null +++ b/tests/components/unifiprotect/test_views.py @@ -0,0 +1,427 @@ +"""Test UniFi Protect views.""" + +from datetime import datetime, timedelta +from typing import Any, cast +from unittest.mock import AsyncMock, Mock + +from aiohttp import ClientResponse +import pytest +from pyunifiprotect.data import Camera, Event, EventType +from pyunifiprotect.exceptions import ClientError + +from homeassistant.components.unifiprotect.views import ( + async_generate_event_video_url, + async_generate_thumbnail_url, +) +from homeassistant.core import HomeAssistant + +from .utils import MockUFPFixture, init_entry + +from tests.test_util.aiohttp import mock_aiohttp_client + + +async def test_thumbnail_bad_nvr_id( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, +) -> None: + """Test invalid NVR ID in URL.""" + + ufp.api.get_event_thumbnail = AsyncMock() + + await init_entry(hass, ufp, [camera]) + url = async_generate_thumbnail_url("test_id", "bad_id") + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.get_event_thumbnail.assert_not_called + + +@pytest.mark.parametrize("width,height", [("test", None), (None, "test")]) +async def test_thumbnail_bad_params( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + width: Any, + height: Any, +) -> None: + """Test invalid bad query parameters.""" + + ufp.api.get_event_thumbnail = AsyncMock() + + await init_entry(hass, ufp, [camera]) + url = async_generate_thumbnail_url( + "test_id", ufp.api.bootstrap.nvr.id, width=width, height=height + ) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 400 + ufp.api.get_event_thumbnail.assert_not_called + + +async def test_thumbnail_bad_event( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, +) -> None: + """Test invalid with error raised.""" + + ufp.api.get_event_thumbnail = AsyncMock(side_effect=ClientError()) + + await init_entry(hass, ufp, [camera]) + url = async_generate_thumbnail_url("test_id", ufp.api.bootstrap.nvr.id) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.get_event_thumbnail.assert_called_with("test_id", width=None, height=None) + + +async def test_thumbnail_no_data( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, +) -> None: + """Test invalid no thumbnail returned.""" + + ufp.api.get_event_thumbnail = AsyncMock(return_value=None) + + await init_entry(hass, ufp, [camera]) + url = async_generate_thumbnail_url("test_id", ufp.api.bootstrap.nvr.id) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.get_event_thumbnail.assert_called_with("test_id", width=None, height=None) + + +async def test_thumbnail( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, +) -> None: + """Test invalid NVR ID in URL.""" + + ufp.api.get_event_thumbnail = AsyncMock(return_value=b"testtest") + + await init_entry(hass, ufp, [camera]) + url = async_generate_thumbnail_url("test_id", ufp.api.bootstrap.nvr.id) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 200 + assert response.content_type == "image/jpeg" + assert await response.content.read() == b"testtest" + ufp.api.get_event_thumbnail.assert_called_with("test_id", width=None, height=None) + + +async def test_video_bad_event( + hass: HomeAssistant, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test generating event with bad camera ID.""" + + await init_entry(hass, ufp, [camera]) + + event = Event( + api=ufp.api, + camera_id="test_id", + start=fixed_now - timedelta(seconds=30), + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + with pytest.raises(ValueError): + async_generate_event_video_url(event) + + +async def test_video_bad_event_ongoing( + hass: HomeAssistant, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test generating event with bad camera ID.""" + + await init_entry(hass, ufp, [camera]) + + event = Event( + api=ufp.api, + camera_id=camera.id, + start=fixed_now - timedelta(seconds=30), + end=None, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + with pytest.raises(ValueError): + async_generate_event_video_url(event) + + +async def test_video_bad_perms( + hass: HomeAssistant, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test generating event with bad user permissions.""" + + ufp.api.bootstrap.auth_user.all_permissions = [] + await init_entry(hass, ufp, [camera]) + + event = Event( + api=ufp.api, + camera_id=camera.id, + start=fixed_now - timedelta(seconds=30), + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + with pytest.raises(PermissionError): + async_generate_event_video_url(event) + + +async def test_video_bad_nvr_id( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test video URL with bad NVR id.""" + + ufp.api.request = AsyncMock() + await init_entry(hass, ufp, [camera]) + + event = Event( + api=ufp.api, + camera_id=camera.id, + start=fixed_now - timedelta(seconds=30), + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + url = async_generate_event_video_url(event) + url = url.replace(ufp.api.bootstrap.nvr.id, "bad_id") + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.request.assert_not_called + + +async def test_video_bad_camera_id( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test video URL with bad camera id.""" + + ufp.api.request = AsyncMock() + await init_entry(hass, ufp, [camera]) + + event = Event( + api=ufp.api, + camera_id=camera.id, + start=fixed_now - timedelta(seconds=30), + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + url = async_generate_event_video_url(event) + url = url.replace(camera.id, "bad_id") + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.request.assert_not_called + + +async def test_video_bad_camera_perms( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test video URL with bad camera perms.""" + + ufp.api.request = AsyncMock() + await init_entry(hass, ufp, [camera]) + + event = Event( + api=ufp.api, + camera_id=camera.id, + start=fixed_now - timedelta(seconds=30), + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + url = async_generate_event_video_url(event) + + ufp.api.bootstrap.auth_user.all_permissions = [] + ufp.api.bootstrap.auth_user._perm_cache = {} + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 403 + ufp.api.request.assert_not_called + + +@pytest.mark.parametrize("start,end", [("test", None), (None, "test")]) +async def test_video_bad_params( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, + start: Any, + end: Any, +) -> None: + """Test video URL with bad start/end params.""" + + ufp.api.request = AsyncMock() + await init_entry(hass, ufp, [camera]) + + event_start = fixed_now - timedelta(seconds=30) + event = Event( + api=ufp.api, + camera_id=camera.id, + start=event_start, + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + url = async_generate_event_video_url(event) + from_value = event_start if start is not None else fixed_now + to_value = start if start is not None else end + url = url.replace(from_value.isoformat(), to_value) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 400 + ufp.api.request.assert_not_called + + +async def test_video_bad_video( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test video URL with no video.""" + + ufp.api.request = AsyncMock(side_effect=ClientError) + await init_entry(hass, ufp, [camera]) + + event_start = fixed_now - timedelta(seconds=30) + event = Event( + api=ufp.api, + camera_id=camera.id, + start=event_start, + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + url = async_generate_event_video_url(event) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + + assert response.status == 404 + ufp.api.request.assert_called_once + + +async def test_video( + hass: HomeAssistant, + hass_client: mock_aiohttp_client, + ufp: MockUFPFixture, + camera: Camera, + fixed_now: datetime, +) -> None: + """Test video URL with no video.""" + + content = Mock() + content.__anext__ = AsyncMock(side_effect=[b"test", b"test", StopAsyncIteration()]) + content.__aiter__ = Mock(return_value=content) + + mock_response = Mock() + mock_response.content_length = 8 + mock_response.content.iter_chunked = Mock(return_value=content) + + ufp.api.request = AsyncMock(return_value=mock_response) + await init_entry(hass, ufp, [camera]) + + event_start = fixed_now - timedelta(seconds=30) + event = Event( + api=ufp.api, + camera_id=camera.id, + start=event_start, + end=fixed_now, + id="test_id", + type=EventType.MOTION, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + ) + + url = async_generate_event_video_url(event) + + http_client = await hass_client() + response = cast(ClientResponse, await http_client.get(url)) + assert await response.content.read() == b"testtest" + + assert response.status == 200 + ufp.api.request.assert_called_once From e2fe1a1c5dcfc385b000d5a7edbd4a03e6ec18cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Jun 2022 19:14:56 -0500 Subject: [PATCH 1987/3516] Allow tuple subclasses to be json serialized (#74207) --- homeassistant/helpers/json.py | 2 +- tests/helpers/test_json.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index 8b91f5eb2b5..74a2f542910 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -33,7 +33,7 @@ def json_encoder_default(obj: Any) -> Any: Hand other objects to the original method. """ - if isinstance(obj, set): + if isinstance(obj, (set, tuple)): return list(obj) if isinstance(obj, float): return float(obj) diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py index cfb403ca4a9..54c488690fa 100644 --- a/tests/helpers/test_json.py +++ b/tests/helpers/test_json.py @@ -1,6 +1,7 @@ """Test Home Assistant remote methods and classes.""" import datetime import json +import time import pytest @@ -87,3 +88,11 @@ def test_json_dumps_float_subclass(): """A float subclass.""" assert json_dumps({"c": FloatSubclass(1.2)}) == '{"c":1.2}' + + +def test_json_dumps_tuple_subclass(): + """Test the json dumps a tuple subclass.""" + + tt = time.struct_time((1999, 3, 17, 32, 44, 55, 2, 76, 0)) + + assert json_dumps(tt) == "[1999,3,17,32,44,55,2,76,0]" From 721741281ec1f71f97771995033568b5505c0eb4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 30 Jun 2022 00:23:07 +0000 Subject: [PATCH 1988/3516] [ci skip] Translation update --- .../alarmdecoder/translations/bg.json | 3 + .../components/androidtv/translations/bg.json | 9 ++ .../components/asuswrt/translations/bg.json | 1 + .../components/awair/translations/el.json | 7 ++ .../components/awair/translations/ja.json | 7 ++ .../awair/translations/zh-Hant.json | 7 ++ .../components/dexcom/translations/bg.json | 1 + .../enphase_envoy/translations/bg.json | 7 ++ .../components/ezviz/translations/bg.json | 8 ++ .../faa_delays/translations/bg.json | 1 + .../components/fritz/translations/bg.json | 3 + .../components/ialarm/translations/bg.json | 1 + .../keenetic_ndms2/translations/bg.json | 3 + .../components/kmtronic/translations/bg.json | 1 + .../components/lcn/translations/ca.json | 1 + .../components/lcn/translations/el.json | 1 + .../components/lcn/translations/ja.json | 1 + .../components/lcn/translations/no.json | 1 + .../components/lcn/translations/zh-Hant.json | 1 + .../components/life360/translations/ca.json | 25 +++++- .../components/life360/translations/de.json | 23 +++++ .../components/life360/translations/en.json | 84 ++++++++++--------- .../components/life360/translations/fr.json | 25 +++++- .../life360/translations/pt-BR.json | 25 +++++- .../life360/translations/zh-Hant.json | 25 +++++- .../components/lyric/translations/bg.json | 7 ++ .../components/monoprice/translations/bg.json | 3 + .../components/mqtt/translations/bg.json | 5 ++ .../components/mysensors/translations/bg.json | 1 + .../components/nina/translations/ca.json | 22 +++++ .../components/nina/translations/de.json | 22 +++++ .../components/nina/translations/el.json | 22 +++++ .../components/nina/translations/fr.json | 40 +++++++-- .../components/nina/translations/ja.json | 21 +++++ .../components/nina/translations/pt-BR.json | 22 +++++ .../components/nina/translations/zh-Hant.json | 22 +++++ .../components/picnic/translations/bg.json | 2 + .../components/roku/translations/bg.json | 3 + .../components/sense/translations/bg.json | 1 + .../somfy_mylink/translations/bg.json | 1 + .../components/subaru/translations/bg.json | 1 + .../components/verisure/translations/bg.json | 3 +- .../wolflink/translations/sensor.bg.json | 1 + .../zoneminder/translations/bg.json | 3 +- 44 files changed, 419 insertions(+), 54 deletions(-) create mode 100644 homeassistant/components/lyric/translations/bg.json diff --git a/homeassistant/components/alarmdecoder/translations/bg.json b/homeassistant/components/alarmdecoder/translations/bg.json index b918c0c7710..bf4f4b7175c 100644 --- a/homeassistant/components/alarmdecoder/translations/bg.json +++ b/homeassistant/components/alarmdecoder/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\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "protocol": { "data": { diff --git a/homeassistant/components/androidtv/translations/bg.json b/homeassistant/components/androidtv/translations/bg.json index f912f17d257..5407acc2f55 100644 --- a/homeassistant/components/androidtv/translations/bg.json +++ b/homeassistant/components/androidtv/translations/bg.json @@ -16,5 +16,14 @@ } } } + }, + "options": { + "step": { + "apps": { + "data": { + "app_name": "\u0418\u043c\u0435 \u043d\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/bg.json b/homeassistant/components/asuswrt/translations/bg.json index df452e48980..a407de4c584 100644 --- a/homeassistant/components/asuswrt/translations/bg.json +++ b/homeassistant/components/asuswrt/translations/bg.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \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": { diff --git a/homeassistant/components/awair/translations/el.json b/homeassistant/components/awair/translations/el.json index 0acefe23c02..e878c370d93 100644 --- a/homeassistant/components/awair/translations/el.json +++ b/homeassistant/components/awair/translations/el.json @@ -17,6 +17,13 @@ }, "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03c4\u03ae Awair." }, + "reauth_confirm": { + "data": { + "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "email": "Email" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03c4\u03ae Awair." + }, "user": { "data": { "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/awair/translations/ja.json b/homeassistant/components/awair/translations/ja.json index 83121c9fe42..7c7b73b312f 100644 --- a/homeassistant/components/awair/translations/ja.json +++ b/homeassistant/components/awair/translations/ja.json @@ -17,6 +17,13 @@ }, "description": "Awair developer access token\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, + "reauth_confirm": { + "data": { + "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", + "email": "E\u30e1\u30fc\u30eb" + }, + "description": "Awair developer access token\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + }, "user": { "data": { "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", diff --git a/homeassistant/components/awair/translations/zh-Hant.json b/homeassistant/components/awair/translations/zh-Hant.json index 0bd7749c65f..f14acef8550 100644 --- a/homeassistant/components/awair/translations/zh-Hant.json +++ b/homeassistant/components/awair/translations/zh-Hant.json @@ -17,6 +17,13 @@ }, "description": "\u8acb\u91cd\u65b0\u8f38\u5165 Awair \u958b\u767c\u8005\u5b58\u53d6\u6b0a\u6756\u3002" }, + "reauth_confirm": { + "data": { + "access_token": "\u5b58\u53d6\u6b0a\u6756", + "email": "\u96fb\u5b50\u90f5\u4ef6" + }, + "description": "\u8acb\u91cd\u65b0\u8f38\u5165 Awair \u958b\u767c\u8005\u5b58\u53d6\u6b0a\u6756\u3002" + }, "user": { "data": { "access_token": "\u5b58\u53d6\u6b0a\u6756", diff --git a/homeassistant/components/dexcom/translations/bg.json b/homeassistant/components/dexcom/translations/bg.json index ec574a06c2f..f0b893b6182 100644 --- a/homeassistant/components/dexcom/translations/bg.json +++ b/homeassistant/components/dexcom/translations/bg.json @@ -2,6 +2,7 @@ "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": { diff --git a/homeassistant/components/enphase_envoy/translations/bg.json b/homeassistant/components/enphase_envoy/translations/bg.json index ffb593eb287..1fce5cf396e 100644 --- a/homeassistant/components/enphase_envoy/translations/bg.json +++ b/homeassistant/components/enphase_envoy/translations/bg.json @@ -5,6 +5,13 @@ }, "error": { "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" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/bg.json b/homeassistant/components/ezviz/translations/bg.json index 702e3b80001..7e54efd88a9 100644 --- a/homeassistant/components/ezviz/translations/bg.json +++ b/homeassistant/components/ezviz/translations/bg.json @@ -1,7 +1,15 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "flow_title": "{serial}", "step": { + "confirm": { + "data": { + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + }, "user": { "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Ezviz Cloud" } diff --git a/homeassistant/components/faa_delays/translations/bg.json b/homeassistant/components/faa_delays/translations/bg.json index 93fa3f04d6c..ac15641f743 100644 --- a/homeassistant/components/faa_delays/translations/bg.json +++ b/homeassistant/components/faa_delays/translations/bg.json @@ -4,6 +4,7 @@ "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": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "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" }, diff --git a/homeassistant/components/fritz/translations/bg.json b/homeassistant/components/fritz/translations/bg.json index b699cec829c..fa080a7662e 100644 --- a/homeassistant/components/fritz/translations/bg.json +++ b/homeassistant/components/fritz/translations/bg.json @@ -1,5 +1,8 @@ { "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\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, diff --git a/homeassistant/components/ialarm/translations/bg.json b/homeassistant/components/ialarm/translations/bg.json index 4983c9a14b2..09f0ff26e5d 100644 --- a/homeassistant/components/ialarm/translations/bg.json +++ b/homeassistant/components/ialarm/translations/bg.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" } } diff --git a/homeassistant/components/keenetic_ndms2/translations/bg.json b/homeassistant/components/keenetic_ndms2/translations/bg.json index 42c3174a4c4..4105afcbe4d 100644 --- a/homeassistant/components/keenetic_ndms2/translations/bg.json +++ b/homeassistant/components/keenetic_ndms2/translations/bg.json @@ -1,5 +1,8 @@ { "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" }, diff --git a/homeassistant/components/kmtronic/translations/bg.json b/homeassistant/components/kmtronic/translations/bg.json index d152ddfcf20..737855f7b76 100644 --- a/homeassistant/components/kmtronic/translations/bg.json +++ b/homeassistant/components/kmtronic/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "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" }, diff --git a/homeassistant/components/lcn/translations/ca.json b/homeassistant/components/lcn/translations/ca.json index e1c08f18137..940e212a9f0 100644 --- a/homeassistant/components/lcn/translations/ca.json +++ b/homeassistant/components/lcn/translations/ca.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "codi de bloqueig rebut", "fingerprint": "codi d'empremta rebut", "send_keys": "claus d'enviament rebudes", "transmitter": "codi del transmissor rebut", diff --git a/homeassistant/components/lcn/translations/el.json b/homeassistant/components/lcn/translations/el.json index ae71f96d361..e0086482539 100644 --- a/homeassistant/components/lcn/translations/el.json +++ b/homeassistant/components/lcn/translations/el.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "\u03b5\u03bb\u03ae\u03c6\u03b8\u03b7 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03ba\u03bb\u03b5\u03b9\u03b4\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd", "fingerprint": "\u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b4\u03b1\u03ba\u03c4\u03c5\u03bb\u03b9\u03ba\u03bf\u03cd \u03b1\u03c0\u03bf\u03c4\u03c5\u03c0\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b5\u03bb\u03ae\u03c6\u03b8\u03b7", "send_keys": "\u03b1\u03c0\u03bf\u03c3\u03c4\u03bf\u03bb\u03ae \u03ba\u03bf\u03c5\u03bc\u03c0\u03b9\u03ce\u03bd \u03b5\u03bb\u03ae\u03c6\u03b8\u03b7", "transmitter": "\u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03bf\u03bc\u03c0\u03bf\u03cd \u03b5\u03bb\u03ae\u03c6\u03b8\u03b7", diff --git a/homeassistant/components/lcn/translations/ja.json b/homeassistant/components/lcn/translations/ja.json index b656835dcbc..30849a56dc5 100644 --- a/homeassistant/components/lcn/translations/ja.json +++ b/homeassistant/components/lcn/translations/ja.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "\u30b3\u30fc\u30c9\u30ed\u30c3\u30af\u30b3\u30fc\u30c9\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f", "fingerprint": "\u6307\u7d0b\u30b3\u30fc\u30c9\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f(fingerprint code received)", "send_keys": "\u9001\u4fe1\u30ad\u30fc\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f(send keys received)", "transmitter": "\u9001\u4fe1\u6a5f\u30b3\u30fc\u30c9\u53d7\u4fe1\u3057\u307e\u3057\u305f(transmitter code received)", diff --git a/homeassistant/components/lcn/translations/no.json b/homeassistant/components/lcn/translations/no.json index 7ae5b70fe2f..cf780a06ba8 100644 --- a/homeassistant/components/lcn/translations/no.json +++ b/homeassistant/components/lcn/translations/no.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "kodel\u00e5skode mottatt", "fingerprint": "fingeravtrykkkode mottatt", "send_keys": "sende n\u00f8kler mottatt", "transmitter": "senderkode mottatt", diff --git a/homeassistant/components/lcn/translations/zh-Hant.json b/homeassistant/components/lcn/translations/zh-Hant.json index fe80da6694f..e6b104596b0 100644 --- a/homeassistant/components/lcn/translations/zh-Hant.json +++ b/homeassistant/components/lcn/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "\u5df2\u6536\u5230\u9396\u5b9a\u78bc", "fingerprint": "\u5df2\u6536\u5230\u6307\u7d0b\u78bc", "send_keys": "\u5df2\u6536\u5230\u50b3\u9001\u91d1\u9470", "transmitter": "\u5df2\u6536\u5230\u767c\u5c04\u5668\u78bc", diff --git a/homeassistant/components/life360/translations/ca.json b/homeassistant/components/life360/translations/ca.json index 875692a661a..f6b7a081863 100644 --- a/homeassistant/components/life360/translations/ca.json +++ b/homeassistant/components/life360/translations/ca.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "El compte ja est\u00e0 configurat", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "unknown": "Error inesperat" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "El compte ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "invalid_username": "Nom d'usuari incorrecte", "unknown": "Error inesperat" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "password": "Contrasenya", "username": "Nom d'usuari" }, "description": "Per configurar les opcions avan\u00e7ades mira la [documentaci\u00f3 de Life360]({docs_url}). Pot ser que ho hagis de fer abans d'afegir cap compte.", - "title": "Informaci\u00f3 del compte Life360" + "title": "Configuraci\u00f3 del compte Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Mostra la conducci\u00f3 com a estat", + "driving_speed": "Velocitat de conducci\u00f3", + "limit_gps_acc": "Limita la precisi\u00f3 del GPS", + "max_gps_accuracy": "Precisi\u00f3 m\u00e0xima del GPS (metres)", + "set_drive_speed": "Configura el llindar de velocitat de conducci\u00f3" + }, + "title": "Opcions del compte" } } } diff --git a/homeassistant/components/life360/translations/de.json b/homeassistant/components/life360/translations/de.json index 516b0255349..67f014e0a2c 100644 --- a/homeassistant/components/life360/translations/de.json +++ b/homeassistant/components/life360/translations/de.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Konto wurde bereits konfiguriert", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "unknown": "Unerwarteter Fehler" }, "create_entry": { @@ -9,11 +11,18 @@ }, "error": { "already_configured": "Konto wurde bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_username": "Ung\u00fcltiger Benutzername", "unknown": "Unerwarteter Fehler" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "password": "Passwort", @@ -23,5 +32,19 @@ "title": "Life360-Kontoinformationen" } } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Fahren als Zustand anzeigen", + "driving_speed": "Fahrgeschwindigkeit", + "limit_gps_acc": "GPS-Genauigkeit einschr\u00e4nken", + "max_gps_accuracy": "Maximale GPS-Genauigkeit (Meter)", + "set_drive_speed": "Schwellenwert f\u00fcr die Fahrgeschwindigkeit festlegen" + }, + "title": "Kontoeinstellungen" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/en.json b/homeassistant/components/life360/translations/en.json index b4c9eb452f6..4e7ff35c814 100644 --- a/homeassistant/components/life360/translations/en.json +++ b/homeassistant/components/life360/translations/en.json @@ -1,44 +1,50 @@ { - "config": { - "step": { - "user": { - "title": "Configure Life360 Account", - "data": { - "username": "Username", - "password": "Password" + "config": { + "abort": { + "already_configured": "Account is already configured", + "invalid_auth": "Invalid authentication", + "reauth_successful": "Re-authentication was successful", + "unknown": "Unexpected error" + }, + "create_entry": { + "default": "To set advanced options, see [Life360 documentation]({docs_url})." + }, + "error": { + "already_configured": "Account is already configured", + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_username": "Invalid username", + "unknown": "Unexpected error" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "title": "Reauthenticate Integration" + }, + "user": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "To set advanced options, see [Life360 documentation]({docs_url}).\nYou may want to do that before adding accounts.", + "title": "Configure Life360 Account" + } } - }, - "reauth_confirm": { - "title": "Reauthenticate Integration", - "data": { - "password": "Password" - } - } }, - "error": { - "invalid_auth": "Invalid authentication", - "already_configured": "Account is already configured", - "cannot_connect": "Failed to connect", - "unknown": "Unexpected error" - }, - "abort": { - "invalid_auth": "Invalid authentication", - "already_configured": "Account is already configured", - "reauth_successful": "Re-authentication was successful" - } - }, - "options": { - "step": { - "init": { - "title": "Account Options", - "data": { - "limit_gps_acc": "Limit GPS accuracy", - "max_gps_accuracy": "Max GPS accuracy (meters)", - "set_drive_speed": "Set driving speed threshold", - "driving_speed": "Driving speed", - "driving": "Show driving as state" + "options": { + "step": { + "init": { + "data": { + "driving": "Show driving as state", + "driving_speed": "Driving speed", + "limit_gps_acc": "Limit GPS accuracy", + "max_gps_accuracy": "Max GPS accuracy (meters)", + "set_drive_speed": "Set driving speed threshold" + }, + "title": "Account Options" + } } - } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/fr.json b/homeassistant/components/life360/translations/fr.json index f58b789b267..ce1fd3f7757 100644 --- a/homeassistant/components/life360/translations/fr.json +++ b/homeassistant/components/life360/translations/fr.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "invalid_auth": "Authentification non valide", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", "unknown": "Erreur inattendue" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification non valide", "invalid_username": "Nom d'utilisateur non valide", "unknown": "Erreur inattendue" }, "step": { + "reauth_confirm": { + "data": { + "password": "Mot de passe" + }, + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, "user": { "data": { "password": "Mot de passe", "username": "Nom d'utilisateur" }, "description": "Pour d\u00e9finir des options avanc\u00e9es, voir [Documentation Life360]({docs_url}).\nVous pouvez le faire avant d'ajouter des comptes.", - "title": "Informations sur le compte Life360" + "title": "Configuration du compte Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Afficher la conduite comme \u00e9tat", + "driving_speed": "Vitesse de conduite", + "limit_gps_acc": "Limiter la pr\u00e9cision du GPS", + "max_gps_accuracy": "Pr\u00e9cision maximale du GPS (en m\u00e8tres)", + "set_drive_speed": "D\u00e9finition du seuil de vitesse de conduite" + }, + "title": "Options de compte" } } } diff --git a/homeassistant/components/life360/translations/pt-BR.json b/homeassistant/components/life360/translations/pt-BR.json index 7753c0f84dc..13349bcef67 100644 --- a/homeassistant/components/life360/translations/pt-BR.json +++ b/homeassistant/components/life360/translations/pt-BR.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "A conta j\u00e1 est\u00e1 configurada", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "reauth_successful": "Integra\u00e7\u00e3o Reautenticar", "unknown": "Erro inesperado" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "A conta j\u00e1 foi configurada", + "cannot_connect": "Falhou ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_username": "Nome de usu\u00e1rio Inv\u00e1lido", "unknown": "Erro inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "title": "Reautenticar Integra\u00e7\u00e3o" + }, "user": { "data": { "password": "Senha", "username": "Usu\u00e1rio" }, "description": "Para definir op\u00e7\u00f5es avan\u00e7adas, consulte [Documenta\u00e7\u00e3o da Life360] ({docs_url}). \n Voc\u00ea pode querer fazer isso antes de adicionar contas.", - "title": "Informa\u00e7\u00f5es da conta Life360" + "title": "Configurar conta Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Mostrar condu\u00e7\u00e3o como estado", + "driving_speed": "Velocidade de condu\u00e7\u00e3o", + "limit_gps_acc": "Limitar a precis\u00e3o do GPS", + "max_gps_accuracy": "Precis\u00e3o m\u00e1xima do GPS (metros)", + "set_drive_speed": "Definir limite de velocidade de condu\u00e7\u00e3o" + }, + "title": "Op\u00e7\u00f5es da conta" } } } diff --git a/homeassistant/components/life360/translations/zh-Hant.json b/homeassistant/components/life360/translations/zh-Hant.json index ad7fde2e21d..55e55bb30c7 100644 --- a/homeassistant/components/life360/translations/zh-Hant.json +++ b/homeassistant/components/life360/translations/zh-Hant.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "invalid_username": "\u4f7f\u7528\u8005\u540d\u7a31\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, "user": { "data": { "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, "description": "\u6b32\u8a2d\u5b9a\u9032\u968e\u9078\u9805\uff0c\u8acb\u53c3\u95b1 [Life360 \u6587\u4ef6]({docs_url})\u3002\n\u5efa\u8b70\u65bc\u65b0\u589e\u5e33\u865f\u524d\uff0c\u5148\u9032\u884c\u4e86\u89e3\u3002", - "title": "Life360 \u5e33\u865f\u8cc7\u8a0a" + "title": "\u8a2d\u5b9a Life360 \u5e33\u865f" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "\u5c07\u884c\u99db\u4e2d\u986f\u793a\u70ba\u72c0\u614b", + "driving_speed": "\u884c\u99db\u901f\u5ea6", + "limit_gps_acc": "\u9650\u5236 GPS \u7cbe\u51c6\u5ea6", + "max_gps_accuracy": "\u6700\u9ad8 GPS \u7cbe\u78ba\u5ea6\uff08\u516c\u5c3a\uff09", + "set_drive_speed": "\u8a2d\u5b9a\u901f\u9650\u503c" + }, + "title": "\u5e33\u865f\u9078\u9805" } } } diff --git a/homeassistant/components/lyric/translations/bg.json b/homeassistant/components/lyric/translations/bg.json new file mode 100644 index 00000000000..2f756377e31 --- /dev/null +++ b/homeassistant/components/lyric/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\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/monoprice/translations/bg.json b/homeassistant/components/monoprice/translations/bg.json index 40cf11b9651..0dbc6308db2 100644 --- a/homeassistant/components/monoprice/translations/bg.json +++ b/homeassistant/components/monoprice/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/mqtt/translations/bg.json b/homeassistant/components/mqtt/translations/bg.json index f69eb1b4cc9..65260eacabb 100644 --- a/homeassistant/components/mqtt/translations/bg.json +++ b/homeassistant/components/mqtt/translations/bg.json @@ -27,6 +27,11 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button_6": "\u0428\u0435\u0441\u0442\u0438 \u0431\u0443\u0442\u043e\u043d" + } + }, "options": { "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" diff --git a/homeassistant/components/mysensors/translations/bg.json b/homeassistant/components/mysensors/translations/bg.json index 7c8e0080bc2..8abf7bbd1fa 100644 --- a/homeassistant/components/mysensors/translations/bg.json +++ b/homeassistant/components/mysensors/translations/bg.json @@ -8,6 +8,7 @@ "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", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_ip": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d IP \u0430\u0434\u0440\u0435\u0441", "invalid_port": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d \u043d\u043e\u043c\u0435\u0440 \u043d\u0430 \u043f\u043e\u0440\u0442", diff --git a/homeassistant/components/nina/translations/ca.json b/homeassistant/components/nina/translations/ca.json index 41d274d779d..1ff79090d3c 100644 --- a/homeassistant/components/nina/translations/ca.json +++ b/homeassistant/components/nina/translations/ca.json @@ -23,5 +23,27 @@ "title": "Selecciona ciutat/comtat" } } + }, + "options": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "no_selection": "Seleccioneu almenys una ciutat/comtat", + "unknown": "Error inesperat" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Ciutat/comtat (A-D)", + "_e_to_h": "Ciutat/comtat (E-H)", + "_i_to_l": "Ciutat/comtat (I-L)", + "_m_to_q": "Ciutat/comtat (M-Q)", + "_r_to_u": "Ciutat/comtat (R-U)", + "_v_to_z": "Ciutat/comtat (V-Z)", + "corona_filter": "Elimina els avisos de corona", + "slots": "Nombre d'avisos m\u00e0xims per ciutat/comtat" + }, + "title": "Opcions" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/de.json b/homeassistant/components/nina/translations/de.json index 1e7e1a3e70e..4e6b881b051 100644 --- a/homeassistant/components/nina/translations/de.json +++ b/homeassistant/components/nina/translations/de.json @@ -23,5 +23,27 @@ "title": "Stadt/Landkreis ausw\u00e4hlen" } } + }, + "options": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "no_selection": "Bitte w\u00e4hle mindestens eine Stadt/einen Landkreis aus", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Stadt/Landkreis (A-D)", + "_e_to_h": "Stadt/Landkreis (E-H)", + "_i_to_l": "Stadt/Landkreis (I-L)", + "_m_to_q": "Stadt/Landkreis (M-Q)", + "_r_to_u": "Stadt/Landkreis (R-U)", + "_v_to_z": "Stadt/Landkreis (V-Z)", + "corona_filter": "Corona-Warnungen entfernen", + "slots": "Maximale Warnungen pro Stadt/Landkreis" + }, + "title": "Optionen" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/el.json b/homeassistant/components/nina/translations/el.json index 9faf567662a..8466a9e5215 100644 --- a/homeassistant/components/nina/translations/el.json +++ b/homeassistant/components/nina/translations/el.json @@ -23,5 +23,27 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c0\u03cc\u03bb\u03b7\u03c2/\u03bd\u03bf\u03bc\u03bf\u03cd" } } + }, + "options": { + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "no_selection": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03bc\u03af\u03b1 \u03c0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "init": { + "data": { + "_a_to_d": "\u03a0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc\u03c2 (\u0391-D)", + "_e_to_h": "\u03a0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc\u03c2 (\u0395-\u0397)", + "_i_to_l": "\u03a0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc\u03c2 (I-L)", + "_m_to_q": "\u03a0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc\u03c2 (\u039c-Q)", + "_r_to_u": "\u03a0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc\u03c2 (R-U)", + "_v_to_z": "\u03a0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc\u03c2 (V-Z)", + "corona_filter": "\u039a\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03c0\u03c1\u03bf\u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c9\u03bd Corona", + "slots": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03b5\u03c2 \u03c0\u03c1\u03bf\u03b5\u03b9\u03b4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2 \u03b1\u03bd\u03ac \u03c0\u03cc\u03bb\u03b7/\u03bd\u03bf\u03bc\u03cc" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/fr.json b/homeassistant/components/nina/translations/fr.json index 83f994b79a4..7d76b2aa5db 100644 --- a/homeassistant/components/nina/translations/fr.json +++ b/homeassistant/components/nina/translations/fr.json @@ -5,22 +5,44 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "no_selection": "Veuillez s\u00e9lectionner au moins une ville/une region", + "no_selection": "Veuillez s\u00e9lectionner au moins une ville ou un comt\u00e9", "unknown": "Erreur inattendue" }, "step": { "user": { "data": { - "_a_to_d": "Ville/R\u00e9gion (A-D)", - "_e_to_h": "Ville/R\u00e9gion (E-H)", - "_i_to_l": "Ville/R\u00e9gion (I-L)", - "_m_to_q": "Ville/R\u00e9gion (M-Q)", - "_r_to_u": "Ville/R\u00e9gion (R-U)", - "_v_to_z": "Ville/R\u00e9gion (V-Z)", + "_a_to_d": "Ville / comt\u00e9 (A-D)", + "_e_to_h": "Ville / comt\u00e9 (E-H)", + "_i_to_l": "Ville / comt\u00e9 (I-L)", + "_m_to_q": "Ville / comt\u00e9 (M-Q)", + "_r_to_u": "Ville / comt\u00e9 (R-U)", + "_v_to_z": "Ville / comt\u00e9 (V-Z)", "corona_filter": "Supprimer les avertissements Corona", - "slots": "Nombre maximal d'avertissements par ville/region" + "slots": "Nombre maximal d'avertissements par ville ou comt\u00e9" }, - "title": "S\u00e9lectionnez la ville/la region" + "title": "S\u00e9lectionnez la ville ou le comt\u00e9" + } + } + }, + "options": { + "error": { + "cannot_connect": "\u00c9chec de connexion", + "no_selection": "Veuillez s\u00e9lectionner au moins une ville ou un comt\u00e9", + "unknown": "Erreur inattendue" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Ville / comt\u00e9 (A-D)", + "_e_to_h": "Ville / comt\u00e9 (E-H)", + "_i_to_l": "Ville / comt\u00e9 (I-L)", + "_m_to_q": "Ville / comt\u00e9 (M-Q)", + "_r_to_u": "Ville / comt\u00e9 (R-U)", + "_v_to_z": "Ville / comt\u00e9 (V-Z)", + "corona_filter": "Supprimer les avertissements Corona", + "slots": "Nombre maximal d'avertissements par ville ou comt\u00e9" + }, + "title": "Options" } } } diff --git a/homeassistant/components/nina/translations/ja.json b/homeassistant/components/nina/translations/ja.json index 7c765025ae8..5e3978f4777 100644 --- a/homeassistant/components/nina/translations/ja.json +++ b/homeassistant/components/nina/translations/ja.json @@ -23,5 +23,26 @@ "title": "\u5e02\u533a\u753a\u6751/\u7fa4\u3092\u9078\u629e" } } + }, + "options": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "no_selection": "\u5c11\u306a\u304f\u3068\u30821\u3064\u306e\u5e02\u533a\u753a\u6751/\u90e1\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "init": { + "data": { + "_a_to_d": "City/county (A-D)", + "_i_to_l": "City/county (I-L)", + "_m_to_q": "City/county (M-Q)", + "_r_to_u": "City/county (R-U)", + "_v_to_z": "City/county (V-Z)", + "corona_filter": "\u30b3\u30ed\u30ca\u8b66\u544a\u306e\u524a\u9664", + "slots": "1\u5e02\u533a\u753a\u6751/\u90e1\u3042\u305f\u308a\u306e\u6700\u5927\u8b66\u544a\u6570" + }, + "title": "\u30aa\u30d7\u30b7\u30e7\u30f3" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/pt-BR.json b/homeassistant/components/nina/translations/pt-BR.json index c22d3e06530..3da9c79f05f 100644 --- a/homeassistant/components/nina/translations/pt-BR.json +++ b/homeassistant/components/nina/translations/pt-BR.json @@ -23,5 +23,27 @@ "title": "Selecione a cidade/munic\u00edpio" } } + }, + "options": { + "error": { + "cannot_connect": "Falhou ao conectar", + "no_selection": "Selecione pelo menos uma cidade/munic\u00edpio", + "unknown": "Erro inesperado" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Cidade/munic\u00edpio (A-D)", + "_e_to_h": "Cidade/munic\u00edpio (E-H)", + "_i_to_l": "Cidade/munic\u00edpio (I-L)", + "_m_to_q": "Cidade/munic\u00edpio (M-Q)", + "_r_to_u": "Cidade/munic\u00edpio (R-U)", + "_v_to_z": "Cidade/munic\u00edpio (V-Z)", + "corona_filter": "Remover avisos de corona", + "slots": "M\u00e1ximo de avisos por cidade/munic\u00edpio" + }, + "title": "Op\u00e7\u00f5es" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/zh-Hant.json b/homeassistant/components/nina/translations/zh-Hant.json index 212c65070d8..dde251012a0 100644 --- a/homeassistant/components/nina/translations/zh-Hant.json +++ b/homeassistant/components/nina/translations/zh-Hant.json @@ -23,5 +23,27 @@ "title": "\u9078\u64c7\u57ce\u5e02/\u7e23\u5e02" } } + }, + "options": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "no_selection": "\u8acb\u81f3\u5c11\u9078\u64c7\u4e00\u500b\u57ce\u5e02/\u7e23\u5e02", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "init": { + "data": { + "_a_to_d": "\u57ce\u5e02/\u7e23\u5e02\uff08A-D\uff09", + "_e_to_h": "\u57ce\u5e02/\u7e23\u5e02\uff08E-H\uff09", + "_i_to_l": "\u57ce\u5e02/\u7e23\u5e02\uff08I-L\uff09", + "_m_to_q": "\u57ce\u5e02/\u7e23\u5e02\uff08M-Q\uff09", + "_r_to_u": "\u57ce\u5e02/\u7e23\u5e02\uff08R-U\uff09", + "_v_to_z": "\u57ce\u5e02/\u7e23\u5e02\uff08V-Z\uff09", + "corona_filter": "\u79fb\u9664 Corona \u8b66\u544a", + "slots": "\u6bcf\u500b\u57ce\u5e02/\u7e23\u5e02\u6700\u5927\u8b66\u544a\u503c" + }, + "title": "\u9078\u9805" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/bg.json b/homeassistant/components/picnic/translations/bg.json index 32ea4287182..aaf9f767fff 100644 --- a/homeassistant/components/picnic/translations/bg.json +++ b/homeassistant/components/picnic/translations/bg.json @@ -1,9 +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", "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": { diff --git a/homeassistant/components/roku/translations/bg.json b/homeassistant/components/roku/translations/bg.json index ef4bfded40c..efb4c18e1b0 100644 --- a/homeassistant/components/roku/translations/bg.json +++ b/homeassistant/components/roku/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, "step": { "discovery_confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" diff --git a/homeassistant/components/sense/translations/bg.json b/homeassistant/components/sense/translations/bg.json index 2be0802eef9..f81ad124c51 100644 --- a/homeassistant/components/sense/translations/bg.json +++ b/homeassistant/components/sense/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" }, "error": { diff --git a/homeassistant/components/somfy_mylink/translations/bg.json b/homeassistant/components/somfy_mylink/translations/bg.json index ca0ed419f99..5ee98a5d46c 100644 --- a/homeassistant/components/somfy_mylink/translations/bg.json +++ b/homeassistant/components/somfy_mylink/translations/bg.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, + "flow_title": "{mac} ({ip})", "step": { "user": { "data": { diff --git a/homeassistant/components/subaru/translations/bg.json b/homeassistant/components/subaru/translations/bg.json index 9031d8b47ce..212303991b0 100644 --- a/homeassistant/components/subaru/translations/bg.json +++ b/homeassistant/components/subaru/translations/bg.json @@ -1,6 +1,7 @@ { "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", "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "error": { diff --git a/homeassistant/components/verisure/translations/bg.json b/homeassistant/components/verisure/translations/bg.json index 0f10e122185..cf602238bf3 100644 --- a/homeassistant/components/verisure/translations/bg.json +++ b/homeassistant/components/verisure/translations/bg.json @@ -1,7 +1,8 @@ { "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" + "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", + "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": { "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" diff --git a/homeassistant/components/wolflink/translations/sensor.bg.json b/homeassistant/components/wolflink/translations/sensor.bg.json index 8d0335dcc31..6d41058320d 100644 --- a/homeassistant/components/wolflink/translations/sensor.bg.json +++ b/homeassistant/components/wolflink/translations/sensor.bg.json @@ -5,6 +5,7 @@ "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u043d", "dhw_prior": "DHWPrior", "gasdruck": "\u041d\u0430\u043b\u044f\u0433\u0430\u043d\u0435 \u043d\u0430 \u0433\u0430\u0437\u0430", + "heizung": "\u041e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435", "kalibration": "\u041a\u0430\u043b\u0438\u0431\u0440\u0438\u0440\u0430\u043d\u0435", "test": "\u0422\u0435\u0441\u0442", "tpw": "TPW", diff --git a/homeassistant/components/zoneminder/translations/bg.json b/homeassistant/components/zoneminder/translations/bg.json index a19fe59b023..ad4605eceb9 100644 --- a/homeassistant/components/zoneminder/translations/bg.json +++ b/homeassistant/components/zoneminder/translations/bg.json @@ -2,7 +2,8 @@ "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" + "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": { "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.", From ab6e92f99696a8f5fab587dd56d07cc508c5b898 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 02:35:49 +0200 Subject: [PATCH 1989/3516] Patch out life360 entry setup in tests (#74212) --- tests/components/life360/test_config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/life360/test_config_flow.py b/tests/components/life360/test_config_flow.py index 0b5b850ac23..c24852fddf7 100644 --- a/tests/components/life360/test_config_flow.py +++ b/tests/components/life360/test_config_flow.py @@ -118,7 +118,7 @@ async def test_user_show_form(hass, life360_api): assert keys[keys.index(key)].default == vol.UNDEFINED -async def test_user_config_flow_success(hass, life360_api): +async def test_user_config_flow_success(hass, life360_api, life360): """Test a successful user config flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -228,7 +228,7 @@ async def test_reauth_config_flow_success(hass, life360_api, caplog, state): assert config_entry.data == TEST_CONFIG_DATA_2 -async def test_reauth_config_flow_login_error(hass, life360_api, caplog): +async def test_reauth_config_flow_login_error(hass, life360_api, life360, caplog): """Test a reauthorization config flow with a login error.""" config_entry = create_config_entry(hass) @@ -285,7 +285,7 @@ async def test_reauth_config_flow_login_error(hass, life360_api, caplog): # ========== Option flow Tests ========================================================= -async def test_options_flow(hass): +async def test_options_flow(hass, life360): """Test an options flow.""" config_entry = create_config_entry(hass) From 42533ebbb33369f666bcd9bec34b60d58e9898f7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 03:40:58 +0200 Subject: [PATCH 1990/3516] Update requests to 2.28.1 (#74210) --- homeassistant/package_constraints.txt | 6 +----- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_test.txt | 2 +- script/gen_requirements_all.py | 4 ---- 5 files changed, 4 insertions(+), 12 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 11ffd9c4c0c..9ba945c3e2b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ pyserial==3.5 python-slugify==4.0.1 pyudev==0.22.0 pyyaml==6.0 -requests==2.28.0 +requests==2.28.1 scapy==2.4.5 sqlalchemy==1.4.38 typing-extensions>=3.10.0.2,<5.0 @@ -114,7 +114,3 @@ backoff<2.0 # Breaking change in version # https://github.com/samuelcolvin/pydantic/issues/4092 pydantic!=1.9.1 - -# Pin charset-normalizer to 2.0.12 due to version conflict. -# https://github.com/home-assistant/core/pull/74104 -charset-normalizer==2.0.12 diff --git a/pyproject.toml b/pyproject.toml index 5e5974d142f..f50c835e58f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ dependencies = [ "pip>=21.0,<22.2", "python-slugify==4.0.1", "pyyaml==6.0", - "requests==2.28.0", + "requests==2.28.1", "typing-extensions>=3.10.0.2,<5.0", "voluptuous==0.13.1", "voluptuous-serialize==2.5.0", diff --git a/requirements.txt b/requirements.txt index 7506201eae1..98b148fa923 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ orjson==3.7.5 pip>=21.0,<22.2 python-slugify==4.0.1 pyyaml==6.0 -requests==2.28.0 +requests==2.28.1 typing-extensions>=3.10.0.2,<5.0 voluptuous==0.13.1 voluptuous-serialize==2.5.0 diff --git a/requirements_test.txt b/requirements_test.txt index 046d8bfb400..6072ce896ee 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -42,6 +42,6 @@ types-pkg-resources==0.1.3 types-python-slugify==0.1.2 types-pytz==2021.1.2 types-PyYAML==5.4.6 -types-requests==2.27.30 +types-requests==2.28.0 types-toml==0.1.5 types-ujson==0.1.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index c6b50c6bd32..a2a0eab897a 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -132,10 +132,6 @@ backoff<2.0 # Breaking change in version # https://github.com/samuelcolvin/pydantic/issues/4092 pydantic!=1.9.1 - -# Pin charset-normalizer to 2.0.12 due to version conflict. -# https://github.com/home-assistant/core/pull/74104 -charset-normalizer==2.0.12 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From f721b9e3df8d08d496fc96bf4a2ad24e78882b33 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 03:43:14 +0200 Subject: [PATCH 1991/3516] Fix clicksend request content type headers (#74189) --- homeassistant/components/clicksend/notify.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/clicksend/notify.py b/homeassistant/components/clicksend/notify.py index 74f1c2e1ae5..ec6bed3c55d 100644 --- a/homeassistant/components/clicksend/notify.py +++ b/homeassistant/components/clicksend/notify.py @@ -3,7 +3,6 @@ from http import HTTPStatus import json import logging -from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol @@ -23,7 +22,7 @@ BASE_API_URL = "https://rest.clicksend.com/v3" DEFAULT_SENDER = "hass" TIMEOUT = 5 -HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON} +HEADERS = {"Content-Type": CONTENT_TYPE_JSON} PLATFORM_SCHEMA = vol.Schema( From 555e9c676275c21f8e3d24cc52bd67136cc815d4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 07:06:35 +0200 Subject: [PATCH 1992/3516] Fix input_number invalid state restore handling (#74213) Co-authored-by: J. Nick Koston --- .../components/input_number/__init__.py | 7 ++++-- tests/components/input_number/test_init.py | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index a6ee8dd0f7d..8e922687e59 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -1,6 +1,7 @@ """Support to set a numeric value from a slider or text box.""" from __future__ import annotations +from contextlib import suppress import logging import voluptuous as vol @@ -281,8 +282,10 @@ class InputNumber(RestoreEntity): if self._current_value is not None: return - state = await self.async_get_last_state() - value = state and float(state.state) + value: float | None = None + if state := await self.async_get_last_state(): + with suppress(ValueError): + value = float(state.state) # Check against None because value can be 0 if value is not None and self._minimum <= value <= self._maximum: diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index ca496723d99..4149627720b 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -255,6 +255,29 @@ async def test_restore_state(hass): assert float(state.state) == 10 +async def test_restore_invalid_state(hass): + """Ensure an invalid restore state is handled.""" + mock_restore_cache( + hass, (State("input_number.b1", "="), State("input_number.b2", "200")) + ) + + hass.state = CoreState.starting + + await async_setup_component( + hass, + DOMAIN, + {DOMAIN: {"b1": {"min": 2, "max": 100}, "b2": {"min": 10, "max": 100}}}, + ) + + state = hass.states.get("input_number.b1") + assert state + assert float(state.state) == 2 + + state = hass.states.get("input_number.b2") + assert state + assert float(state.state) == 10 + + async def test_initial_state_overrules_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( From bef512c4255e117680c5cf7024f79b217ce73b10 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Thu, 30 Jun 2022 07:09:52 +0200 Subject: [PATCH 1993/3516] Split attributes into sensors for here_travel_time (#72405) --- .../components/here_travel_time/__init__.py | 4 +- .../components/here_travel_time/const.py | 11 +- .../components/here_travel_time/sensor.py | 178 ++++++++++++------ .../here_travel_time/test_sensor.py | 114 ++++++----- 4 files changed, 187 insertions(+), 120 deletions(-) diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index 2b9853c3b10..24da0bc2673 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -187,8 +187,8 @@ class HereTravelTimeDataUpdateCoordinator(DataUpdateCoordinator): return HERERoutingData( { ATTR_ATTRIBUTION: attribution, - ATTR_DURATION: summary["baseTime"] / 60, # type: ignore[misc] - ATTR_DURATION_IN_TRAFFIC: traffic_time / 60, + ATTR_DURATION: round(summary["baseTime"] / 60), # type: ignore[misc] + ATTR_DURATION_IN_TRAFFIC: round(traffic_time / 60), ATTR_DISTANCE: distance, ATTR_ROUTE: response.route_short, ATTR_ORIGIN: ",".join(origin), diff --git a/homeassistant/components/here_travel_time/const.py b/homeassistant/components/here_travel_time/const.py index bde17f5c306..b3768b2d69d 100644 --- a/homeassistant/components/here_travel_time/const.py +++ b/homeassistant/components/here_travel_time/const.py @@ -24,8 +24,6 @@ CONF_DEPARTURE_TIME = "departure_time" DEFAULT_NAME = "HERE Travel Time" -TRACKABLE_DOMAINS = ["device_tracker", "sensor", "zone", "person"] - TRAVEL_MODE_BICYCLE = "bicycle" TRAVEL_MODE_CAR = "car" TRAVEL_MODE_PEDESTRIAN = "pedestrian" @@ -41,7 +39,6 @@ TRAVEL_MODES = [ TRAVEL_MODE_TRUCK, ] -TRAVEL_MODES_PUBLIC = [TRAVEL_MODE_PUBLIC, TRAVEL_MODE_PUBLIC_TIME_TABLE] TRAVEL_MODES_VEHICLE = [TRAVEL_MODE_CAR, TRAVEL_MODE_TRUCK] TRAFFIC_MODE_ENABLED = "traffic_enabled" @@ -58,6 +55,14 @@ ICON_PEDESTRIAN = "mdi:walk" ICON_PUBLIC = "mdi:bus" ICON_TRUCK = "mdi:truck" +ICONS = { + TRAVEL_MODE_BICYCLE: ICON_BICYCLE, + TRAVEL_MODE_PEDESTRIAN: ICON_PEDESTRIAN, + TRAVEL_MODE_PUBLIC: ICON_PUBLIC, + TRAVEL_MODE_PUBLIC_TIME_TABLE: ICON_PUBLIC, + TRAVEL_MODE_TRUCK: ICON_TRUCK, +} + UNITS = [CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL] ATTR_DURATION = "duration" diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 75c9fd2ea3b..c4be60d5569 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -1,16 +1,24 @@ """Support for HERE travel time sensors.""" from __future__ import annotations +from collections.abc import Mapping from datetime import timedelta import logging +from typing import Any import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, - ATTR_MODE, + ATTR_LATITUDE, + ATTR_LONGITUDE, CONF_API_KEY, CONF_MODE, CONF_NAME, @@ -28,10 +36,14 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import HereTravelTimeDataUpdateCoordinator from .const import ( + ATTR_DESTINATION, + ATTR_DESTINATION_NAME, + ATTR_DISTANCE, ATTR_DURATION, ATTR_DURATION_IN_TRAFFIC, - ATTR_TRAFFIC_MODE, - ATTR_UNIT_SYSTEM, + ATTR_ORIGIN, + ATTR_ORIGIN_NAME, + ATTR_ROUTE, CONF_ARRIVAL, CONF_DEPARTURE, CONF_DESTINATION_ENTITY_ID, @@ -44,14 +56,10 @@ from .const import ( CONF_TRAFFIC_MODE, DEFAULT_NAME, DOMAIN, - ICON_BICYCLE, ICON_CAR, - ICON_PEDESTRIAN, - ICON_PUBLIC, - ICON_TRUCK, + ICONS, ROUTE_MODE_FASTEST, ROUTE_MODES, - TRAFFIC_MODE_ENABLED, TRAVEL_MODE_BICYCLE, TRAVEL_MODE_CAR, TRAVEL_MODE_PEDESTRIAN, @@ -59,7 +67,6 @@ from .const import ( TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAVEL_MODE_TRUCK, TRAVEL_MODES, - TRAVEL_MODES_PUBLIC, UNITS, ) @@ -115,6 +122,69 @@ PLATFORM_SCHEMA = vol.All( ) +def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...]: + """Construct SensorEntityDescriptions.""" + return ( + SensorEntityDescription( + name="Duration", + icon=ICONS.get(travel_mode, ICON_CAR), + key=ATTR_DURATION, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TIME_MINUTES, + ), + SensorEntityDescription( + name="Duration in Traffic", + icon=ICONS.get(travel_mode, ICON_CAR), + key=ATTR_DURATION_IN_TRAFFIC, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TIME_MINUTES, + ), + SensorEntityDescription( + name="Distance", + icon=ICONS.get(travel_mode, ICON_CAR), + key=ATTR_DISTANCE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + name="Route", + icon="mdi:directions", + key=ATTR_ROUTE, + ), + ) + + +def create_origin_sensor( + config_entry: ConfigEntry, hass: HomeAssistant +) -> OriginSensor: + """Create a origin sensor.""" + return OriginSensor( + config_entry.entry_id, + config_entry.data[CONF_NAME], + SensorEntityDescription( + name="Origin", + icon="mdi:store-marker", + key=ATTR_ORIGIN_NAME, + ), + hass.data[DOMAIN][config_entry.entry_id], + ) + + +def create_destination_sensor( + config_entry: ConfigEntry, hass: HomeAssistant +) -> DestinationSensor: + """Create a destination sensor.""" + return DestinationSensor( + config_entry.entry_id, + config_entry.data[CONF_NAME], + SensorEntityDescription( + name="Destination", + icon="mdi:store-marker", + key=ATTR_DESTINATION_NAME, + ), + hass.data[DOMAIN][config_entry.entry_id], + ) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -143,16 +213,20 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Add HERE travel time entities from a config_entry.""" - async_add_entities( - [ + + sensors: list[HERETravelTimeSensor] = [] + for sensor_description in sensor_descriptions(config_entry.data[CONF_MODE]): + sensors.append( HERETravelTimeSensor( config_entry.entry_id, config_entry.data[CONF_NAME], - config_entry.options[CONF_TRAFFIC_MODE], + sensor_description, hass.data[DOMAIN][config_entry.entry_id], ) - ], - ) + ) + sensors.append(create_origin_sensor(config_entry, hass)) + sensors.append(create_destination_sensor(config_entry, hass)) + async_add_entities(sensors) class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): @@ -162,15 +236,14 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): self, unique_id_prefix: str, name: str, - traffic_mode: str, + sensor_description: SensorEntityDescription, coordinator: HereTravelTimeDataUpdateCoordinator, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._traffic_mode = traffic_mode == TRAFFIC_MODE_ENABLED - self._attr_native_unit_of_measurement = TIME_MINUTES - self._attr_name = name - self._attr_unique_id = unique_id_prefix + self.entity_description = sensor_description + self._attr_name = f"{name} {sensor_description.name}" + self._attr_unique_id = f"{unique_id_prefix}_{sensor_description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, unique_id_prefix)}, entry_type=DeviceEntryType.SERVICE, @@ -188,34 +261,10 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): self.async_on_remove(async_at_start(self.hass, _update_at_start)) @property - def native_value(self) -> str | None: + def native_value(self) -> str | float | None: """Return the state of the sensor.""" if self.coordinator.data is not None: - return str( - round( - self.coordinator.data.get( - ATTR_DURATION_IN_TRAFFIC - if self._traffic_mode - else ATTR_DURATION - ) - ) - ) - return None - - @property - def extra_state_attributes( - self, - ) -> dict[str, None | float | str | bool] | None: - """Return the state attributes.""" - if self.coordinator.data is not None: - res = { - ATTR_UNIT_SYSTEM: self.coordinator.config.units, - ATTR_MODE: self.coordinator.config.travel_mode, - ATTR_TRAFFIC_MODE: self._traffic_mode, - **self.coordinator.data, - } - res.pop(ATTR_ATTRIBUTION) - return res + return self.coordinator.data.get(self.entity_description.key) return None @property @@ -225,15 +274,30 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): return self.coordinator.data.get(ATTR_ATTRIBUTION) return None + +class OriginSensor(HERETravelTimeSensor): + """Sensor holding information about the route origin.""" + @property - def icon(self) -> str: - """Icon to use in the frontend depending on travel_mode.""" - if self.coordinator.config.travel_mode == TRAVEL_MODE_BICYCLE: - return ICON_BICYCLE - if self.coordinator.config.travel_mode == TRAVEL_MODE_PEDESTRIAN: - return ICON_PEDESTRIAN - if self.coordinator.config.travel_mode in TRAVEL_MODES_PUBLIC: - return ICON_PUBLIC - if self.coordinator.config.travel_mode == TRAVEL_MODE_TRUCK: - return ICON_TRUCK - return ICON_CAR + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """GPS coordinates.""" + if self.coordinator.data is not None: + return { + ATTR_LATITUDE: self.coordinator.data[ATTR_ORIGIN].split(",")[0], + ATTR_LONGITUDE: self.coordinator.data[ATTR_ORIGIN].split(",")[1], + } + return None + + +class DestinationSensor(HERETravelTimeSensor): + """Sensor holding information about the route destination.""" + + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """GPS coordinates.""" + if self.coordinator.data is not None: + return { + ATTR_LATITUDE: self.coordinator.data[ATTR_DESTINATION].split(",")[0], + ATTR_LONGITUDE: self.coordinator.data[ATTR_DESTINATION].split(",")[1], + } + return None diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index dc9ba128c35..93fbd1bd204 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -7,14 +7,6 @@ import pytest from homeassistant.components.here_travel_time.config_flow import default_options from homeassistant.components.here_travel_time.const import ( - ATTR_DESTINATION, - ATTR_DESTINATION_NAME, - ATTR_DISTANCE, - ATTR_DURATION, - ATTR_DURATION_IN_TRAFFIC, - ATTR_ORIGIN, - ATTR_ORIGIN_NAME, - ATTR_ROUTE, CONF_ARRIVAL_TIME, CONF_DEPARTURE_TIME, CONF_DESTINATION_ENTITY_ID, @@ -24,7 +16,6 @@ from homeassistant.components.here_travel_time.const import ( CONF_ORIGIN_LATITUDE, CONF_ORIGIN_LONGITUDE, CONF_ROUTE_MODE, - CONF_TRAFFIC_MODE, CONF_UNIT_SYSTEM, DOMAIN, ICON_BICYCLE, @@ -34,18 +25,18 @@ from homeassistant.components.here_travel_time.const import ( ICON_TRUCK, NO_ROUTE_ERROR_MESSAGE, ROUTE_MODE_FASTEST, - TRAFFIC_MODE_DISABLED, TRAFFIC_MODE_ENABLED, TRAVEL_MODE_BICYCLE, TRAVEL_MODE_CAR, TRAVEL_MODE_PEDESTRIAN, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAVEL_MODE_TRUCK, - TRAVEL_MODES_VEHICLE, ) from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_ICON, + ATTR_LATITUDE, + ATTR_LONGITUDE, CONF_API_KEY, CONF_MODE, CONF_NAME, @@ -67,62 +58,57 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize( - "mode,icon,traffic_mode,unit_system,arrival_time,departure_time,expected_state,expected_distance,expected_duration_in_traffic", + "mode,icon,unit_system,arrival_time,departure_time,expected_duration,expected_distance,expected_duration_in_traffic", [ ( TRAVEL_MODE_CAR, ICON_CAR, - TRAFFIC_MODE_ENABLED, "metric", None, None, + "30", + "23.903", "31", - 23.903, - 31.016666666666666, ), ( TRAVEL_MODE_BICYCLE, ICON_BICYCLE, - TRAFFIC_MODE_DISABLED, "metric", None, None, "30", - 23.903, - 30.05, + "23.903", + "30", ), ( TRAVEL_MODE_PEDESTRIAN, ICON_PEDESTRIAN, - TRAFFIC_MODE_DISABLED, "imperial", None, None, "30", - 14.852631013, - 30.05, + "14.852631013", + "30", ), ( TRAVEL_MODE_PUBLIC_TIME_TABLE, ICON_PUBLIC, - TRAFFIC_MODE_DISABLED, "imperial", "08:00:00", None, "30", - 14.852631013, - 30.05, + "14.852631013", + "30", ), ( TRAVEL_MODE_TRUCK, ICON_TRUCK, - TRAFFIC_MODE_ENABLED, "metric", None, "08:00:00", + "30", + "23.903", "31", - 23.903, - 31.016666666666666, ), ], ) @@ -131,11 +117,10 @@ async def test_sensor( hass: HomeAssistant, mode, icon, - traffic_mode, unit_system, arrival_time, departure_time, - expected_state, + expected_duration, expected_distance, expected_duration_in_traffic, ): @@ -153,7 +138,6 @@ async def test_sensor( CONF_NAME: "test", }, options={ - CONF_TRAFFIC_MODE: traffic_mode, CONF_ROUTE_MODE: ROUTE_MODE_FASTEST, CONF_ARRIVAL_TIME: arrival_time, CONF_DEPARTURE_TIME: departure_time, @@ -166,44 +150,57 @@ async def test_sensor( hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - sensor = hass.states.get("sensor.test") - assert sensor.attributes.get("unit_of_measurement") == TIME_MINUTES + duration = hass.states.get("sensor.test_duration") + assert duration.attributes.get("unit_of_measurement") == TIME_MINUTES assert ( - sensor.attributes.get(ATTR_ATTRIBUTION) + duration.attributes.get(ATTR_ATTRIBUTION) == "With the support of HERE Technologies. All information is provided without warranty of any kind." ) - assert sensor.state == expected_state + assert duration.attributes.get(ATTR_ICON) == icon + assert duration.state == expected_duration - assert sensor.attributes.get(ATTR_DURATION) == 30.05 - assert sensor.attributes.get(ATTR_DISTANCE) == expected_distance - assert sensor.attributes.get(ATTR_ROUTE) == ( + assert ( + hass.states.get("sensor.test_duration_in_traffic").state + == expected_duration_in_traffic + ) + assert hass.states.get("sensor.test_distance").state == expected_distance + assert hass.states.get("sensor.test_route").state == ( "US-29 - K St NW; US-29 - Whitehurst Fwy; " "I-495 N - Capital Beltway; MD-187 S - Old Georgetown Rd" ) - assert sensor.attributes.get(CONF_UNIT_SYSTEM) == unit_system assert ( - sensor.attributes.get(ATTR_DURATION_IN_TRAFFIC) == expected_duration_in_traffic + hass.states.get("sensor.test_duration_in_traffic").state + == expected_duration_in_traffic ) - assert sensor.attributes.get(ATTR_ORIGIN) == ",".join( - [CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE] + assert hass.states.get("sensor.test_origin").state == "22nd St NW" + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LATITUDE) + == CAR_ORIGIN_LATITUDE ) - assert sensor.attributes.get(ATTR_DESTINATION) == ",".join( - [CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE] - ) - assert sensor.attributes.get(ATTR_ORIGIN_NAME) == "22nd St NW" - assert sensor.attributes.get(ATTR_DESTINATION_NAME) == "Service Rd S" - assert sensor.attributes.get(CONF_MODE) == mode - assert sensor.attributes.get(CONF_TRAFFIC_MODE) is ( - traffic_mode == TRAFFIC_MODE_ENABLED + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LONGITUDE) + == CAR_ORIGIN_LONGITUDE ) - assert sensor.attributes.get(ATTR_ICON) == icon + assert hass.states.get("sensor.test_origin").state == "22nd St NW" + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LATITUDE) + == CAR_ORIGIN_LATITUDE + ) + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LONGITUDE) + == CAR_ORIGIN_LONGITUDE + ) - # Test traffic mode disabled for vehicles - if mode in TRAVEL_MODES_VEHICLE: - assert sensor.attributes.get(ATTR_DURATION) != sensor.attributes.get( - ATTR_DURATION_IN_TRAFFIC - ) + assert hass.states.get("sensor.test_destination").state == "Service Rd S" + assert ( + hass.states.get("sensor.test_destination").attributes.get(ATTR_LATITUDE) + == CAR_DESTINATION_LATITUDE + ) + assert ( + hass.states.get("sensor.test_destination").attributes.get(ATTR_LONGITUDE) + == CAR_DESTINATION_LONGITUDE + ) @pytest.mark.usefixtures("valid_response") @@ -261,7 +258,9 @@ async def test_no_attribution(hass: HomeAssistant): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - assert hass.states.get("sensor.test").attributes.get(ATTR_ATTRIBUTION) is None + assert ( + hass.states.get("sensor.test_duration").attributes.get(ATTR_ATTRIBUTION) is None + ) async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock): @@ -305,8 +304,7 @@ async def test_entity_ids(hass: HomeAssistant, valid_response: MagicMock): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - sensor = hass.states.get("sensor.test") - assert sensor.attributes.get(ATTR_DISTANCE) == 23.903 + assert hass.states.get("sensor.test_distance").state == "23.903" valid_response.assert_called_with( [CAR_ORIGIN_LATITUDE, CAR_ORIGIN_LONGITUDE], From 48c5aab5eebedd81e1dd887fa577d3e2950cdea3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 30 Jun 2022 08:16:05 +0200 Subject: [PATCH 1994/3516] Fix netgear method return type annotation (#74200) --- homeassistant/components/netgear/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index a4f8a4df14e..ae4186abbb5 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -250,7 +250,7 @@ class NetgearRouter: async with self._api_lock: await self.hass.async_add_executor_job(self._api.reboot) - async def async_check_new_firmware(self) -> None: + async def async_check_new_firmware(self) -> dict[str, Any] | None: """Check for new firmware of the router.""" async with self._api_lock: return await self.hass.async_add_executor_job(self._api.check_new_firmware) From 0cf922cc4e558d88b8093d417774d1fdf7993b4a Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 29 Jun 2022 23:54:51 -0700 Subject: [PATCH 1995/3516] Allow legacy nest integration with no configuration.yaml (#74222) --- homeassistant/components/nest/__init__.py | 2 +- tests/components/nest/common.py | 12 +++++++++++- tests/components/nest/test_init_legacy.py | 5 ++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 0e0128136ad..b31354b598c 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -177,7 +177,7 @@ class SignalUpdateCallback: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Nest from a config entry with dispatch between old/new flows.""" config_mode = config_flow.get_config_mode(hass) - if config_mode == config_flow.ConfigMode.LEGACY: + if DATA_SDM not in entry.data or config_mode == config_flow.ConfigMode.LEGACY: return await async_setup_legacy_entry(hass, entry) if config_mode == config_flow.ConfigMode.SDM: diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index 765a954b6de..f86112ada75 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -147,7 +147,17 @@ TEST_CONFIG_LEGACY = NestTestConfig( }, }, }, - credential=None, +) +TEST_CONFIG_ENTRY_LEGACY = NestTestConfig( + config_entry_data={ + "auth_implementation": "local", + "tokens": { + "expires_at": time.time() + 86400, + "access_token": { + "token": "some-token", + }, + }, + }, ) diff --git a/tests/components/nest/test_init_legacy.py b/tests/components/nest/test_init_legacy.py index cbf1bfe2d48..fc4e6070faf 100644 --- a/tests/components/nest/test_init_legacy.py +++ b/tests/components/nest/test_init_legacy.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, PropertyMock, patch import pytest -from .common import TEST_CONFIG_LEGACY +from .common import TEST_CONFIG_ENTRY_LEGACY, TEST_CONFIG_LEGACY DOMAIN = "nest" @@ -33,6 +33,9 @@ def make_thermostat(): return device +@pytest.mark.parametrize( + "nest_test_config", [TEST_CONFIG_LEGACY, TEST_CONFIG_ENTRY_LEGACY] +) async def test_thermostat(hass, setup_base_platform): """Test simple initialization for thermostat entities.""" From 42d7f2a3b29fcb1e24bdde26546ff6120a9185db Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 30 Jun 2022 08:56:43 +0200 Subject: [PATCH 1996/3516] Update pylint to 2.14.4 (#74192) --- .../components/zha/core/channels/manufacturerspecific.py | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 943d13a57d6..b9f0ec1aaca 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -75,7 +75,7 @@ class OppleRemote(ZigbeeChannel): "trigger_indicator": True, } elif self.cluster.endpoint.model == "lumi.motion.ac01": - self.ZCL_INIT_ATTRS = { # pylint: disable=invalid-name + self.ZCL_INIT_ATTRS = { "presence": True, "monitoring_mode": True, "motion_sensitivity": True, diff --git a/requirements_test.txt b/requirements_test.txt index 6072ce896ee..3172c1f5544 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ freezegun==1.2.1 mock-open==1.4.0 mypy==0.961 pre-commit==2.19.0 -pylint==2.14.3 +pylint==2.14.4 pipdeptree==2.2.1 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 From 3a57f4363f9743c9a04e346e3dd85176eb7a7308 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 09:13:25 +0200 Subject: [PATCH 1997/3516] Revert "Patch out life360 entry setup in tests" (#74223) Revert "Patch out life360 entry setup in tests (#74212)" This reverts commit ab6e92f99696a8f5fab587dd56d07cc508c5b898. --- tests/components/life360/test_config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/life360/test_config_flow.py b/tests/components/life360/test_config_flow.py index c24852fddf7..0b5b850ac23 100644 --- a/tests/components/life360/test_config_flow.py +++ b/tests/components/life360/test_config_flow.py @@ -118,7 +118,7 @@ async def test_user_show_form(hass, life360_api): assert keys[keys.index(key)].default == vol.UNDEFINED -async def test_user_config_flow_success(hass, life360_api, life360): +async def test_user_config_flow_success(hass, life360_api): """Test a successful user config flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -228,7 +228,7 @@ async def test_reauth_config_flow_success(hass, life360_api, caplog, state): assert config_entry.data == TEST_CONFIG_DATA_2 -async def test_reauth_config_flow_login_error(hass, life360_api, life360, caplog): +async def test_reauth_config_flow_login_error(hass, life360_api, caplog): """Test a reauthorization config flow with a login error.""" config_entry = create_config_entry(hass) @@ -285,7 +285,7 @@ async def test_reauth_config_flow_login_error(hass, life360_api, life360, caplog # ========== Option flow Tests ========================================================= -async def test_options_flow(hass, life360): +async def test_options_flow(hass): """Test an options flow.""" config_entry = create_config_entry(hass) From 407da8c4b89b6ddd858fb0dc24719599f5650eca Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 30 Jun 2022 09:42:15 +0200 Subject: [PATCH 1998/3516] Correct native_pressure_unit for zamg weather (#74225) --- homeassistant/components/zamg/weather.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zamg/weather.py b/homeassistant/components/zamg/weather.py index 2bf4a5b39f6..6910955fcf7 100644 --- a/homeassistant/components/zamg/weather.py +++ b/homeassistant/components/zamg/weather.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - LENGTH_MILLIMETERS, + PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) @@ -87,9 +87,7 @@ def setup_platform( class ZamgWeather(WeatherEntity): """Representation of a weather condition.""" - _attr_native_pressure_unit = ( - LENGTH_MILLIMETERS # API reports l/m², equivalent to mm - ) + _attr_native_pressure_unit = PRESSURE_HPA _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR From fdb7a23171fd5cc9f18a0fef0294c95f1a0bbc31 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 10:08:06 +0200 Subject: [PATCH 1999/3516] Update black to 22.6.0 (#74209) Co-authored-by: J. Nick Koston --- .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 18b34a222aa..d6400e113c5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 22.6.0 hooks: - id: black args: diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 0d204771e40..23d675afaf1 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,7 +1,7 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.7.4 -black==22.3.0 +black==22.6.0 codespell==2.1.0 flake8-comprehensions==3.8.0 flake8-docstrings==1.6.0 From 57fd84e20c9e98df52a6e81af1fa84ee86028aa8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 Jun 2022 15:34:48 +0200 Subject: [PATCH 2000/3516] Improve type hints in demo (#74236) --- .../components/demo/binary_sensor.py | 8 +- homeassistant/components/demo/calendar.py | 15 ++- homeassistant/components/demo/camera.py | 8 +- homeassistant/components/demo/climate.py | 96 ++++++++++--------- homeassistant/components/demo/config_flow.py | 9 +- homeassistant/components/demo/light.py | 19 ++-- 6 files changed, 82 insertions(+), 73 deletions(-) diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index bf2731d3d72..f2b759a9cf3 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -73,7 +73,7 @@ class DemoBinarySensor(BinarySensorEntity): ) @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique id.""" return self._unique_id @@ -83,16 +83,16 @@ class DemoBinarySensor(BinarySensorEntity): return self._sensor_type @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed for a demo binary sensor.""" return False @property - def name(self): + def name(self) -> str: """Return the name of the binary sensor.""" return self._name @property - def is_on(self): + def is_on(self) -> bool: """Return true if the binary sensor is on.""" return self._state diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index 3a8b909bd0c..f8a803ba860 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -72,7 +72,12 @@ class DemoCalendar(CalendarEntity): """Return the name of the entity.""" return self._name - async def async_get_events(self, hass, start_date, end_date) -> list[CalendarEvent]: + async def async_get_events( + self, + hass: HomeAssistant, + start_date: datetime.datetime, + end_date: datetime.datetime, + ) -> list[CalendarEvent]: """Return calendar events within a datetime range.""" return [self._event] @@ -80,15 +85,15 @@ class DemoCalendar(CalendarEntity): class LegacyDemoCalendar(CalendarEventDevice): """Calendar for exercising shim API.""" - def __init__(self, name): + def __init__(self, name: str) -> None: """Initialize demo calendar.""" self._name = name - one_hour_from_now = dt_util.now() + dt_util.dt.timedelta(minutes=30) + one_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30) self._event = { "start": {"dateTime": one_hour_from_now.isoformat()}, "end": { "dateTime": ( - one_hour_from_now + dt_util.dt.timedelta(minutes=60) + one_hour_from_now + datetime.timedelta(minutes=60) ).isoformat() }, "summary": "Future Event", @@ -102,7 +107,7 @@ class LegacyDemoCalendar(CalendarEventDevice): return self._event @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return self._name diff --git a/homeassistant/components/demo/camera.py b/homeassistant/components/demo/camera.py index 25026bce11b..b8e0a714253 100644 --- a/homeassistant/components/demo/camera.py +++ b/homeassistant/components/demo/camera.py @@ -58,23 +58,23 @@ class DemoCamera(Camera): return await self.hass.async_add_executor_job(image_path.read_bytes) - async def async_enable_motion_detection(self): + async def async_enable_motion_detection(self) -> None: """Enable the Motion detection in base station (Arm).""" self._attr_motion_detection_enabled = True self.async_write_ha_state() - async def async_disable_motion_detection(self): + async def async_disable_motion_detection(self) -> None: """Disable the motion detection in base station (Disarm).""" self._attr_motion_detection_enabled = False self.async_write_ha_state() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off camera.""" self._attr_is_streaming = False self._attr_is_on = False self.async_write_ha_state() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on camera.""" self._attr_is_streaming = True self._attr_is_on = True diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index a4d6ca6da07..ae633c5937a 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -1,6 +1,8 @@ """Demo platform that offers a fake climate device.""" from __future__ import annotations +from typing import Any + from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, @@ -103,24 +105,24 @@ class DemoClimate(ClimateEntity): def __init__( self, - unique_id, - name, - target_temperature, - unit_of_measurement, - preset, - current_temperature, - fan_mode, - target_humidity, - current_humidity, - swing_mode, - hvac_mode, - hvac_action, - aux, - target_temp_high, - target_temp_low, - hvac_modes, - preset_modes=None, - ): + unique_id: str, + name: str, + target_temperature: float | None, + unit_of_measurement: str, + preset: str | None, + current_temperature: float, + fan_mode: str | None, + target_humidity: int | None, + current_humidity: int | None, + swing_mode: str | None, + hvac_mode: HVACMode, + hvac_action: HVACAction | None, + aux: bool | None, + target_temp_high: float | None, + target_temp_low: float | None, + hvac_modes: list[HVACMode], + preset_modes: list[str] | None = None, + ) -> None: """Initialize the climate device.""" self._unique_id = unique_id self._name = name @@ -175,111 +177,111 @@ class DemoClimate(ClimateEntity): ) @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique id.""" return self._unique_id @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" return self._support_flags @property - def should_poll(self): + def should_poll(self) -> bool: """Return the polling state.""" return False @property - def name(self): + def name(self) -> str: """Return the name of the climate device.""" return self._name @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" return self._unit_of_measurement @property - def current_temperature(self): + def current_temperature(self) -> float: """Return the current temperature.""" return self._current_temperature @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._target_temperature @property - def target_temperature_high(self): + def target_temperature_high(self) -> float | None: """Return the highbound target temperature we try to reach.""" return self._target_temperature_high @property - def target_temperature_low(self): + def target_temperature_low(self) -> float | None: """Return the lowbound target temperature we try to reach.""" return self._target_temperature_low @property - def current_humidity(self): + def current_humidity(self) -> int | None: """Return the current humidity.""" return self._current_humidity @property - def target_humidity(self): + def target_humidity(self) -> int | None: """Return the humidity we try to reach.""" return self._target_humidity @property - def hvac_action(self): + def hvac_action(self) -> HVACAction | None: """Return current operation ie. heat, cool, idle.""" return self._hvac_action @property - def hvac_mode(self): + def hvac_mode(self) -> HVACMode: """Return hvac target hvac state.""" return self._hvac_mode @property - def hvac_modes(self): + def hvac_modes(self) -> list[HVACMode]: """Return the list of available operation modes.""" return self._hvac_modes @property - def preset_mode(self): + def preset_mode(self) -> str | None: """Return preset mode.""" return self._preset @property - def preset_modes(self): + def preset_modes(self) -> list[str] | None: """Return preset modes.""" return self._preset_modes @property - def is_aux_heat(self): + def is_aux_heat(self) -> bool | None: """Return true if aux heat is on.""" return self._aux @property - def fan_mode(self): + def fan_mode(self) -> str | None: """Return the fan setting.""" return self._current_fan_mode @property - def fan_modes(self): + def fan_modes(self) -> list[str]: """Return the list of available fan modes.""" return self._fan_modes @property - def swing_mode(self): + def swing_mode(self) -> str | None: """Return the swing setting.""" return self._current_swing_mode @property - def swing_modes(self): + def swing_modes(self) -> list[str]: """List of available swing modes.""" return self._swing_modes - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if kwargs.get(ATTR_TEMPERATURE) is not None: self._target_temperature = kwargs.get(ATTR_TEMPERATURE) @@ -291,37 +293,37 @@ class DemoClimate(ClimateEntity): self._target_temperature_low = kwargs.get(ATTR_TARGET_TEMP_LOW) self.async_write_ha_state() - async def async_set_humidity(self, humidity): + async def async_set_humidity(self, humidity: int) -> None: """Set new humidity level.""" self._target_humidity = humidity self.async_write_ha_state() - async def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" self._current_swing_mode = swing_mode self.async_write_ha_state() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" self._current_fan_mode = fan_mode self.async_write_ha_state() - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" self._hvac_mode = hvac_mode self.async_write_ha_state() - async def async_set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Update preset_mode on.""" self._preset = preset_mode self.async_write_ha_state() - async def async_turn_aux_heat_on(self): + async def async_turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" self._aux = True self.async_write_ha_state() - async def async_turn_aux_heat_off(self): + async def async_turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" self._aux = False self.async_write_ha_state() diff --git a/homeassistant/components/demo/config_flow.py b/homeassistant/components/demo/config_flow.py index e389574c658..0163123b578 100644 --- a/homeassistant/components/demo/config_flow.py +++ b/homeassistant/components/demo/config_flow.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from . import DOMAIN @@ -29,7 +30,7 @@ class DemoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - async def async_step_import(self, import_info): + async def async_step_import(self, import_info) -> FlowResult: """Set the config entry up from yaml.""" return self.async_create_entry(title="Demo", data={}) @@ -42,11 +43,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self.config_entry = config_entry self.options = dict(config_entry.options) - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input=None) -> FlowResult: """Manage the options.""" return await self.async_step_options_1() - async def async_step_options_1(self, user_input=None): + async def async_step_options_1(self, user_input=None) -> FlowResult: """Manage the options.""" if user_input is not None: self.options.update(user_input) @@ -69,7 +70,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ), ) - async def async_step_options_2(self, user_input=None): + async def async_step_options_2(self, user_input=None) -> FlowResult: """Manage the options 2.""" if user_input is not None: self.options.update(user_input) diff --git a/homeassistant/components/demo/light.py b/homeassistant/components/demo/light.py index a9fc6cf2044..00e5fd4def1 100644 --- a/homeassistant/components/demo/light.py +++ b/homeassistant/components/demo/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import random +from typing import Any from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -107,18 +108,18 @@ class DemoLight(LightEntity): def __init__( self, - unique_id, - name, + unique_id: str, + name: str, state, available=False, brightness=180, ct=None, # pylint: disable=invalid-name - effect_list=None, + effect_list: list[str] | None = None, effect=None, hs_color=None, rgbw_color=None, rgbww_color=None, - supported_color_modes=None, + supported_color_modes: set[ColorMode] | None = None, ): """Initialize the light.""" self._available = True @@ -169,7 +170,7 @@ class DemoLight(LightEntity): return self._name @property - def unique_id(self): + def unique_id(self) -> str: """Return unique ID for light.""" return self._unique_id @@ -211,7 +212,7 @@ class DemoLight(LightEntity): return self._ct @property - def effect_list(self) -> list: + def effect_list(self) -> list[str] | None: """Return the list of supported effects.""" return self._effect_list @@ -231,11 +232,11 @@ class DemoLight(LightEntity): return self._features @property - def supported_color_modes(self) -> set | None: + def supported_color_modes(self) -> set[ColorMode]: """Flag supported color modes.""" return self._color_modes - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" self._state = True @@ -269,7 +270,7 @@ class DemoLight(LightEntity): # Home Assistant about updates in our state ourselves. self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" self._state = False From f5c6a6be3a0c085c016c3be31985f6bbfd185fc8 Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Fri, 1 Jul 2022 02:13:08 +1200 Subject: [PATCH 2001/3516] Add config flow to AnthemAV integration (#53268) * Add config flow to AnthemAV integration * Add importing of existing configuration * Change setting to optional and add default value * Use entity attribute * Reduce changes by removing additional media player properties * Remove title from translation * Refactor config flow and fix PR comments * Fix a failing test because of wrong renaming * Add typing and use existing class and enum * Bump dependency to v1.3.1 * Remove unecessary async_reload_entry * Fix requirements_test_all after rebase * Add const for timeout and remove async_block in test * Reapply CodeOwner and configflow after rebase * Remove name from configflow * Fix manifest prettier failure * Simplify code and avoid catching broad exception * Removed unused strings and translations * Avoid asserting hass.data --- CODEOWNERS | 2 + homeassistant/components/anthemav/__init__.py | 60 ++++++++- .../components/anthemav/config_flow.py | 96 ++++++++++++++ homeassistant/components/anthemav/const.py | 7 ++ .../components/anthemav/manifest.json | 5 +- .../components/anthemav/media_player.py | 117 ++++++++++-------- .../components/anthemav/strings.json | 19 +++ .../components/anthemav/translations/en.json | 19 +++ .../components/anthemav/translations/fr.json | 19 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/anthemav/__init__.py | 1 + tests/components/anthemav/conftest.py | 28 +++++ tests/components/anthemav/test_config_flow.py | 115 +++++++++++++++++ tests/components/anthemav/test_init.py | 65 ++++++++++ 16 files changed, 506 insertions(+), 53 deletions(-) create mode 100644 homeassistant/components/anthemav/config_flow.py create mode 100644 homeassistant/components/anthemav/const.py create mode 100644 homeassistant/components/anthemav/strings.json create mode 100644 homeassistant/components/anthemav/translations/en.json create mode 100644 homeassistant/components/anthemav/translations/fr.json create mode 100644 tests/components/anthemav/__init__.py create mode 100644 tests/components/anthemav/conftest.py create mode 100644 tests/components/anthemav/test_config_flow.py create mode 100644 tests/components/anthemav/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 1b6d88b5464..ed4ab888541 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -74,6 +74,8 @@ build.json @home-assistant/supervisor /tests/components/analytics/ @home-assistant/core @ludeeus /homeassistant/components/androidtv/ @JeffLIrion @ollo69 /tests/components/androidtv/ @JeffLIrion @ollo69 +/homeassistant/components/anthemav/ @hyralex +/tests/components/anthemav/ @hyralex /homeassistant/components/apache_kafka/ @bachya /tests/components/apache_kafka/ @bachya /homeassistant/components/api/ @home-assistant/core diff --git a/homeassistant/components/anthemav/__init__.py b/homeassistant/components/anthemav/__init__.py index 56b06e865c2..5ad845b52d6 100644 --- a/homeassistant/components/anthemav/__init__.py +++ b/homeassistant/components/anthemav/__init__.py @@ -1 +1,59 @@ -"""The anthemav component.""" +"""The Anthem A/V Receivers integration.""" +from __future__ import annotations + +import logging + +import anthemav + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import ANTHEMAV_UDATE_SIGNAL, DOMAIN + +PLATFORMS = [Platform.MEDIA_PLAYER] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Anthem A/V Receivers from a config entry.""" + + @callback + def async_anthemav_update_callback(message): + """Receive notification from transport that new data exists.""" + _LOGGER.debug("Received update callback from AVR: %s", message) + async_dispatcher_send(hass, f"{ANTHEMAV_UDATE_SIGNAL}_{entry.data[CONF_NAME]}") + + try: + avr = await anthemav.Connection.create( + host=entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + update_callback=async_anthemav_update_callback, + ) + + except OSError as err: + raise ConfigEntryNotReady from err + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = avr + + 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) + + avr = hass.data[DOMAIN][entry.entry_id] + + if avr is not None: + _LOGGER.debug("Close avr connection") + avr.close() + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/anthemav/config_flow.py b/homeassistant/components/anthemav/config_flow.py new file mode 100644 index 00000000000..55e1fd42e07 --- /dev/null +++ b/homeassistant/components/anthemav/config_flow.py @@ -0,0 +1,96 @@ +"""Config flow for Anthem A/V Receivers integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import anthemav +from anthemav.connection import Connection +from anthemav.device_error import DeviceError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import format_mac + +from .const import CONF_MODEL, DEFAULT_NAME, DEFAULT_PORT, DOMAIN + +DEVICE_TIMEOUT_SECONDS = 4.0 + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } +) + + +async def connect_device(user_input: dict[str, Any]) -> Connection: + """Connect to the AVR device.""" + avr = await anthemav.Connection.create( + host=user_input[CONF_HOST], port=user_input[CONF_PORT], auto_reconnect=False + ) + await avr.reconnect() + await avr.protocol.wait_for_device_initialised(DEVICE_TIMEOUT_SECONDS) + return avr + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Anthem A/V Receivers.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + if CONF_NAME not in user_input: + user_input[CONF_NAME] = DEFAULT_NAME + + errors = {} + + avr: Connection | None = None + try: + avr = await connect_device(user_input) + except OSError: + _LOGGER.error( + "Couldn't establish connection to %s:%s", + user_input[CONF_HOST], + user_input[CONF_PORT], + ) + errors["base"] = "cannot_connect" + except DeviceError: + _LOGGER.error( + "Couldn't receive device information from %s:%s", + user_input[CONF_HOST], + user_input[CONF_PORT], + ) + errors["base"] = "cannot_receive_deviceinfo" + else: + user_input[CONF_MAC] = format_mac(avr.protocol.macaddress) + user_input[CONF_MODEL] = avr.protocol.model + await self.async_set_unique_id(user_input[CONF_MAC]) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) + finally: + if avr is not None: + avr.close() + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_import( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/anthemav/const.py b/homeassistant/components/anthemav/const.py new file mode 100644 index 00000000000..c63a477f7f9 --- /dev/null +++ b/homeassistant/components/anthemav/const.py @@ -0,0 +1,7 @@ +"""Constants for the Anthem A/V Receivers integration.""" +ANTHEMAV_UDATE_SIGNAL = "anthemav_update" +CONF_MODEL = "model" +DEFAULT_NAME = "Anthem AV" +DEFAULT_PORT = 14999 +DOMAIN = "anthemav" +MANUFACTURER = "Anthem" diff --git a/homeassistant/components/anthemav/manifest.json b/homeassistant/components/anthemav/manifest.json index c43b976416d..33fe565ad03 100644 --- a/homeassistant/components/anthemav/manifest.json +++ b/homeassistant/components/anthemav/manifest.json @@ -2,8 +2,9 @@ "domain": "anthemav", "name": "Anthem A/V Receivers", "documentation": "https://www.home-assistant.io/integrations/anthemav", - "requirements": ["anthemav==1.2.0"], - "codeowners": [], + "requirements": ["anthemav==1.3.2"], + "codeowners": ["@hyralex"], + "config_flow": true, "iot_class": "local_push", "loggers": ["anthemav"] } diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index 05bfea7ef45..93fdedef054 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -2,8 +2,9 @@ from __future__ import annotations import logging +from typing import Any -import anthemav +from anthemav.connection import Connection import voluptuous as vol from homeassistant.components.media_player import ( @@ -11,33 +12,37 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, + CONF_MAC, CONF_NAME, CONF_PORT, - EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant 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_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import ( + ANTHEMAV_UDATE_SIGNAL, + CONF_MODEL, + DEFAULT_NAME, + DEFAULT_PORT, + DOMAIN, + MANUFACTURER, +) + _LOGGER = logging.getLogger(__name__) -DOMAIN = "anthemav" - -DEFAULT_PORT = 14999 - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, } ) @@ -50,30 +55,33 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up our socket to the AVR.""" - - host = config[CONF_HOST] - port = config[CONF_PORT] - name = config.get(CONF_NAME) - device = None - - _LOGGER.info("Provisioning Anthem AVR device at %s:%d", host, port) - - @callback - def async_anthemav_update_callback(message): - """Receive notification from transport that new data exists.""" - _LOGGER.debug("Received update callback from AVR: %s", message) - async_dispatcher_send(hass, DOMAIN) - - avr = await anthemav.Connection.create( - host=host, port=port, update_callback=async_anthemav_update_callback + _LOGGER.warning( + "AnthemAV configuration is deprecated and has been automatically imported. Please remove the integration from your configuration file" + ) + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, ) - device = AnthemAVR(avr, name) + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up entry.""" + name = config_entry.data[CONF_NAME] + macaddress = config_entry.data[CONF_MAC] + model = config_entry.data[CONF_MODEL] + + avr = hass.data[DOMAIN][config_entry.entry_id] + + device = AnthemAVR(avr, name, macaddress, model) _LOGGER.debug("dump_devicedata: %s", device.dump_avrdata) _LOGGER.debug("dump_conndata: %s", avr.dump_conndata) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.avr.close) async_add_entities([device]) @@ -89,23 +97,34 @@ class AnthemAVR(MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOURCE ) - def __init__(self, avr, name): + def __init__(self, avr: Connection, name: str, macaddress: str, model: str) -> None: """Initialize entity with transport.""" super().__init__() self.avr = avr - self._attr_name = name or self._lookup("model") + self._attr_name = name + self._attr_unique_id = macaddress + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, macaddress)}, + name=name, + manufacturer=MANUFACTURER, + model=model, + ) - def _lookup(self, propname, dval=None): + def _lookup(self, propname: str, dval: Any | None = None) -> Any | None: return getattr(self.avr.protocol, propname, dval) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """When entity is added to hass.""" self.async_on_remove( - async_dispatcher_connect(self.hass, DOMAIN, self.async_write_ha_state) + async_dispatcher_connect( + self.hass, + f"{ANTHEMAV_UDATE_SIGNAL}_{self._attr_name}", + self.async_write_ha_state, + ) ) @property - def state(self): + def state(self) -> str | None: """Return state of power on/off.""" pwrstate = self._lookup("power") @@ -116,22 +135,22 @@ class AnthemAVR(MediaPlayerEntity): return None @property - def is_volume_muted(self): + def is_volume_muted(self) -> bool | None: """Return boolean reflecting mute state on device.""" return self._lookup("mute", False) @property - def volume_level(self): + def volume_level(self) -> float | None: """Return volume level from 0 to 1.""" return self._lookup("volume_as_percentage", 0.0) @property - def media_title(self): + def media_title(self) -> str | None: """Return current input name (closest we have to media title).""" return self._lookup("input_name", "No Source") @property - def app_name(self): + def app_name(self) -> str | None: """Return details about current video and audio stream.""" return ( f"{self._lookup('video_input_resolution_text', '')} " @@ -139,38 +158,38 @@ class AnthemAVR(MediaPlayerEntity): ) @property - def source(self): + def source(self) -> str | None: """Return currently selected input.""" return self._lookup("input_name", "Unknown") @property - def source_list(self): + def source_list(self) -> list[str] | None: """Return all active, configured inputs.""" return self._lookup("input_list", ["Unknown"]) - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Change AVR to the designated source (by name).""" self._update_avr("input_name", source) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn AVR power off.""" self._update_avr("power", False) - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn AVR power on.""" self._update_avr("power", True) - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set AVR volume (0 to 1).""" self._update_avr("volume_as_percentage", volume) - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Engage AVR mute.""" self._update_avr("mute", mute) - def _update_avr(self, propname, value): + def _update_avr(self, propname: str, value: Any | None) -> None: """Update a property in the AVR.""" - _LOGGER.info("Sending command to AVR: set %s to %s", propname, str(value)) + _LOGGER.debug("Sending command to AVR: set %s to %s", propname, str(value)) setattr(self.avr.protocol, propname, value) @property diff --git a/homeassistant/components/anthemav/strings.json b/homeassistant/components/anthemav/strings.json new file mode 100644 index 00000000000..1f1dd0ec75b --- /dev/null +++ b/homeassistant/components/anthemav/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "cannot_receive_deviceinfo": "Failed to retreive MAC Address. Make sure the device is turned on" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/anthemav/translations/en.json b/homeassistant/components/anthemav/translations/en.json new file mode 100644 index 00000000000..9177d5a6e70 --- /dev/null +++ b/homeassistant/components/anthemav/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "cannot_receive_deviceinfo": "Failed to retreive MAC Address. Make sure the device is turned on" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/fr.json b/homeassistant/components/anthemav/translations/fr.json new file mode 100644 index 00000000000..cf1e7c7aded --- /dev/null +++ b/homeassistant/components/anthemav/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "cannot_receive_deviceinfo": "Erreur lors de la d\u00e9couverte de l'addresse MAC. V\u00e9rifiez que l'appareil est allum\u00e9." + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3c6ad94a21f..f344e3acb5c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -29,6 +29,7 @@ FLOWS = { "ambiclimate", "ambient_station", "androidtv", + "anthemav", "apple_tv", "arcam_fmj", "aseko_pool_live", diff --git a/requirements_all.txt b/requirements_all.txt index 98a6c6f5a95..f992d73f894 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -310,7 +310,7 @@ androidtv[async]==0.0.67 anel_pwrctrl-homeassistant==0.0.1.dev2 # homeassistant.components.anthemav -anthemav==1.2.0 +anthemav==1.3.2 # homeassistant.components.apcupsd apcaccess==0.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64ad3f769b9..12e59e5d423 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -269,6 +269,9 @@ ambiclimate==0.2.1 # homeassistant.components.androidtv androidtv[async]==0.0.67 +# homeassistant.components.anthemav +anthemav==1.3.2 + # homeassistant.components.apprise apprise==0.9.9 diff --git a/tests/components/anthemav/__init__.py b/tests/components/anthemav/__init__.py new file mode 100644 index 00000000000..829f99b10b5 --- /dev/null +++ b/tests/components/anthemav/__init__.py @@ -0,0 +1 @@ +"""Tests for the Anthem A/V Receivers integration.""" diff --git a/tests/components/anthemav/conftest.py b/tests/components/anthemav/conftest.py new file mode 100644 index 00000000000..8fbdf3145c3 --- /dev/null +++ b/tests/components/anthemav/conftest.py @@ -0,0 +1,28 @@ +"""Fixtures for anthemav integration tests.""" +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + + +@pytest.fixture +def mock_anthemav() -> AsyncMock: + """Return the default mocked anthemav.""" + avr = AsyncMock() + avr.protocol.macaddress = "000000000001" + avr.protocol.model = "MRX 520" + avr.reconnect = AsyncMock() + avr.close = MagicMock() + avr.protocol.input_list = [] + avr.protocol.audio_listening_mode_list = [] + return avr + + +@pytest.fixture +def mock_connection_create(mock_anthemav: AsyncMock) -> AsyncMock: + """Return the default mocked connection.create.""" + + with patch( + "anthemav.Connection.create", + return_value=mock_anthemav, + ) as mock: + yield mock diff --git a/tests/components/anthemav/test_config_flow.py b/tests/components/anthemav/test_config_flow.py new file mode 100644 index 00000000000..98ed0f0abf0 --- /dev/null +++ b/tests/components/anthemav/test_config_flow.py @@ -0,0 +1,115 @@ +"""Test the Anthem A/V Receivers config flow.""" +from unittest.mock import AsyncMock, patch + +from anthemav.device_error import DeviceError + +from homeassistant import config_entries +from homeassistant.components.anthemav.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + + +async def test_form_with_valid_connection( + hass: HomeAssistant, mock_connection_create: AsyncMock, mock_anthemav: AsyncMock +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.anthemav.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 14999, + }, + ) + + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == { + "host": "1.1.1.1", + "port": 14999, + "name": "Anthem AV", + "mac": "00:00:00:00:00:01", + "model": "MRX 520", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_device_info_error(hass: HomeAssistant) -> None: + """Test we handle DeviceError from library.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "anthemav.Connection.create", + side_effect=DeviceError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 14999, + }, + ) + + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_receive_deviceinfo"} + + +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( + "anthemav.Connection.create", + side_effect=OSError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 14999, + }, + ) + + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_import_configuration( + hass: HomeAssistant, mock_connection_create: AsyncMock, mock_anthemav: AsyncMock +) -> None: + """Test we import existing configuration.""" + config = { + "host": "1.1.1.1", + "port": 14999, + "name": "Anthem Av Import", + } + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "host": "1.1.1.1", + "port": 14999, + "name": "Anthem Av Import", + "mac": "00:00:00:00:00:01", + "model": "MRX 520", + } diff --git a/tests/components/anthemav/test_init.py b/tests/components/anthemav/test_init.py new file mode 100644 index 00000000000..866925f4e46 --- /dev/null +++ b/tests/components/anthemav/test_init.py @@ -0,0 +1,65 @@ +"""Test the Anthem A/V Receivers config flow.""" +from unittest.mock import ANY, AsyncMock, patch + +from homeassistant import config_entries +from homeassistant.components.anthemav.const import CONF_MODEL, DOMAIN +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, mock_connection_create: AsyncMock, mock_anthemav: AsyncMock +) -> None: + """Test load and unload AnthemAv component.""" + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 14999, + CONF_NAME: "Anthem AV", + CONF_MAC: "aabbccddeeff", + CONF_MODEL: "MRX 520", + }, + ) + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # assert avr is created + mock_connection_create.assert_called_with( + host="1.1.1.1", port=14999, update_callback=ANY + ) + assert mock_config_entry.state == config_entries.ConfigEntryState.LOADED + + # unload + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + # assert unload and avr is closed + assert mock_config_entry.state == config_entries.ConfigEntryState.NOT_LOADED + mock_anthemav.close.assert_called_once() + + +async def test_config_entry_not_ready(hass: HomeAssistant) -> None: + """Test AnthemAV configuration entry not ready.""" + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 14999, + CONF_NAME: "Anthem AV", + CONF_MAC: "aabbccddeeff", + CONF_MODEL: "MRX 520", + }, + ) + + with patch( + "anthemav.Connection.create", + side_effect=OSError, + ): + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is config_entries.ConfigEntryState.SETUP_RETRY From d38e8e213a0434f53dd043ac2c9976c761dd9476 Mon Sep 17 00:00:00 2001 From: Jeef Date: Thu, 30 Jun 2022 09:35:06 -0600 Subject: [PATCH 2002/3516] Fix intellifire climate control not needing a default fireplace (#74253) --- homeassistant/components/intellifire/climate.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/intellifire/climate.py b/homeassistant/components/intellifire/climate.py index 37a126bf3d6..a00a20f64f1 100644 --- a/homeassistant/components/intellifire/climate.py +++ b/homeassistant/components/intellifire/climate.py @@ -80,7 +80,6 @@ class IntellifireClimate(IntellifireEntity, ClimateEntity): (raw_target_temp * 9 / 5) + 32, ) await self.coordinator.control_api.set_thermostat_c( - fireplace=self.coordinator.control_api.default_fireplace, temp_c=self.last_temp, ) @@ -101,20 +100,15 @@ class IntellifireClimate(IntellifireEntity, ClimateEntity): ) if hvac_mode == HVACMode.OFF: - await self.coordinator.control_api.turn_off_thermostat( - fireplace=self.coordinator.control_api.default_fireplace - ) + await self.coordinator.control_api.turn_off_thermostat() return # hvac_mode == HVACMode.HEAT # 1) Set the desired target temp await self.coordinator.control_api.set_thermostat_c( - fireplace=self.coordinator.control_api.default_fireplace, temp_c=self.last_temp, ) # 2) Make sure the fireplace is on! if not self.coordinator.read_api.data.is_on: - await self.coordinator.control_api.flame_on( - fireplace=self.coordinator.control_api.default_fireplace, - ) + await self.coordinator.control_api.flame_on() From de700e7859eb2251c6a90f128aef5278d3be38a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Thu, 30 Jun 2022 18:59:46 +0200 Subject: [PATCH 2003/3516] Make media_player.toggle turn on a standby device (#74221) --- homeassistant/components/media_player/__init__.py | 3 ++- .../components/media_player/test_async_helpers.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index dc2f3624a0e..14546a36ec8 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -52,6 +52,7 @@ from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_PLAYING, + STATE_STANDBY, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -888,7 +889,7 @@ class MediaPlayerEntity(Entity): await self.hass.async_add_executor_job(self.toggle) return - if self.state in (STATE_OFF, STATE_IDLE): + if self.state in (STATE_OFF, STATE_IDLE, STATE_STANDBY): await self.async_turn_on() else: await self.async_turn_off() diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index 53c80bfc8de..8be263e7ee0 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -8,6 +8,7 @@ from homeassistant.const import ( STATE_ON, STATE_PAUSED, STATE_PLAYING, + STATE_STANDBY, ) @@ -79,9 +80,13 @@ class ExtendedMediaPlayer(mp.MediaPlayerEntity): """Turn off state.""" self._state = STATE_OFF + def standby(self): + """Put device in standby.""" + self._state = STATE_STANDBY + def toggle(self): """Toggle the power on the media player.""" - if self._state in [STATE_OFF, STATE_IDLE]: + if self._state in [STATE_OFF, STATE_IDLE, STATE_STANDBY]: self._state = STATE_ON else: self._state = STATE_OFF @@ -138,6 +143,10 @@ class SimpleMediaPlayer(mp.MediaPlayerEntity): """Turn off state.""" self._state = STATE_OFF + def standby(self): + """Put device in standby.""" + self._state = STATE_STANDBY + @pytest.fixture(params=[ExtendedMediaPlayer, SimpleMediaPlayer]) def player(hass, request): @@ -188,3 +197,7 @@ async def test_toggle(player): assert player.state == STATE_ON await player.async_toggle() assert player.state == STATE_OFF + player.standby() + assert player.state == STATE_STANDBY + await player.async_toggle() + assert player.state == STATE_ON From 7573dc34aa1ee8a8949d2306c37b59aaa40ff345 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jun 2022 10:00:10 -0700 Subject: [PATCH 2004/3516] Treat thermostat unknown state like unavailable in alexa (#74220) --- homeassistant/components/alexa/capabilities.py | 2 ++ tests/components/alexa/test_capabilities.py | 17 ++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 818b4b794cf..25ec43b689c 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1046,6 +1046,8 @@ class AlexaThermostatController(AlexaCapability): if preset in API_THERMOSTAT_PRESETS: mode = API_THERMOSTAT_PRESETS[preset] + elif self.entity.state == STATE_UNKNOWN: + return None else: mode = API_THERMOSTAT_MODES.get(self.entity.state) if mode is None: diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 3e176b0fb8c..ea6c96bbaef 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -658,13 +658,16 @@ async def test_report_climate_state(hass): "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} ) - hass.states.async_set( - "climate.unavailable", - "unavailable", - {"friendly_name": "Climate Unavailable", "supported_features": 91}, - ) - properties = await reported_properties(hass, "climate.unavailable") - properties.assert_not_has_property("Alexa.ThermostatController", "thermostatMode") + for state in "unavailable", "unknown": + hass.states.async_set( + f"climate.{state}", + state, + {"friendly_name": f"Climate {state}", "supported_features": 91}, + ) + properties = await reported_properties(hass, f"climate.{state}") + properties.assert_not_has_property( + "Alexa.ThermostatController", "thermostatMode" + ) hass.states.async_set( "climate.unsupported", From 781e4571b27b6aaaabd885e7af53cbf2465b41f3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 Jun 2022 19:00:34 +0200 Subject: [PATCH 2005/3516] Add CalendarEntity checks to pylint plugin (#74228) * Add CalendarEntity checks to pylint plugin * adjust air_quality ignore * Mark state property as final --- homeassistant/components/calendar/__init__.py | 6 +++-- pylint/plugins/hass_enforce_type_hints.py | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 432b6943473..4623f490302 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -222,8 +222,9 @@ class CalendarEventDevice(Entity): "description": event["description"], } + @final @property - def state(self) -> str | None: + def state(self) -> str: """Return the state of the calendar event.""" if (event := self.event) is None: return STATE_OFF @@ -276,8 +277,9 @@ class CalendarEntity(Entity): "description": event.description if event.description else "", } + @final @property - def state(self) -> str | None: + def state(self) -> str: """Return the state of the calendar event.""" if (event := self.event) is None: return STATE_OFF diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 5f4f641cbae..3ac072373b8 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -591,6 +591,7 @@ _TOGGLE_ENTITY_MATCH: list[TypeHintMatch] = [ ), ] _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { + # "air_quality": [], # ignored as deprecated "alarm_control_panel": [ ClassTypeHintMatch( base_class="Entity", @@ -717,6 +718,30 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "calendar": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="CalendarEntity", + matches=[ + TypeHintMatch( + function_name="event", + return_type=["CalendarEvent", None], + ), + TypeHintMatch( + function_name="async_get_events", + arg_types={ + 1: "HomeAssistant", + 2: "datetime", + 3: "datetime", + }, + return_type="list[CalendarEvent]", + ), + ], + ), + ], "cover": [ ClassTypeHintMatch( base_class="Entity", From 5fa3b90b2ca309cab3ee03d2a27bf6aaa4dc5a68 Mon Sep 17 00:00:00 2001 From: MasonCrawford Date: Fri, 1 Jul 2022 01:00:39 +0800 Subject: [PATCH 2006/3516] Add config flow to lg_soundbar (#71153) Co-authored-by: Paulus Schoutsen --- .../components/discovery/__init__.py | 2 +- .../components/lg_soundbar/__init__.py | 37 ++++++++ .../components/lg_soundbar/config_flow.py | 78 +++++++++++++++ homeassistant/components/lg_soundbar/const.py | 4 + .../components/lg_soundbar/manifest.json | 3 +- .../components/lg_soundbar/media_player.py | 51 +++++----- .../components/lg_soundbar/strings.json | 18 ++++ .../lg_soundbar/translations/en.json | 18 ++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/lg_soundbar/__init__.py | 1 + .../lg_soundbar/test_config_flow.py | 95 +++++++++++++++++++ 13 files changed, 284 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/lg_soundbar/config_flow.py create mode 100644 homeassistant/components/lg_soundbar/const.py create mode 100644 homeassistant/components/lg_soundbar/strings.json create mode 100644 homeassistant/components/lg_soundbar/translations/en.json create mode 100644 tests/components/lg_soundbar/__init__.py create mode 100644 tests/components/lg_soundbar/test_config_flow.py diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index a0ffbf235ab..3c3538c1ca0 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -63,7 +63,6 @@ SERVICE_HANDLERS = { "openhome": ServiceDetails("media_player", "openhome"), "bose_soundtouch": ServiceDetails("media_player", "soundtouch"), "bluesound": ServiceDetails("media_player", "bluesound"), - "lg_smart_device": ServiceDetails("media_player", "lg_soundbar"), } OPTIONAL_SERVICE_HANDLERS: dict[str, tuple[str, str | None]] = {} @@ -98,6 +97,7 @@ MIGRATED_SERVICE_HANDLERS = [ SERVICE_YEELIGHT, SERVICE_SABNZBD, "nanoleaf_aurora", + "lg_smart_device", ] DEFAULT_ENABLED = ( diff --git a/homeassistant/components/lg_soundbar/__init__.py b/homeassistant/components/lg_soundbar/__init__.py index 175153556f9..75b2109b22a 100644 --- a/homeassistant/components/lg_soundbar/__init__.py +++ b/homeassistant/components/lg_soundbar/__init__.py @@ -1 +1,38 @@ """The lg_soundbar component.""" +import logging + +from homeassistant import config_entries, core +from homeassistant.const import CONF_HOST, CONF_PORT, Platform +from homeassistant.exceptions import ConfigEntryNotReady + +from .config_flow import test_connect +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [Platform.MEDIA_PLAYER] + + +async def async_setup_entry( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: + """Set up platform from a ConfigEntry.""" + hass.data.setdefault(DOMAIN, {}) + # Verify the device is reachable with the given config before setting up the platform + try: + await hass.async_add_executor_job( + test_connect, entry.data[CONF_HOST], entry.data[CONF_PORT] + ) + except ConnectionError as err: + raise ConfigEntryNotReady from err + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: + """Unload a config entry.""" + result = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + return result diff --git a/homeassistant/components/lg_soundbar/config_flow.py b/homeassistant/components/lg_soundbar/config_flow.py new file mode 100644 index 00000000000..bd9a727d1f4 --- /dev/null +++ b/homeassistant/components/lg_soundbar/config_flow.py @@ -0,0 +1,78 @@ +"""Config flow to configure the LG Soundbar integration.""" +from queue import Queue +import socket + +import temescal +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT + +from .const import DEFAULT_PORT, DOMAIN + +DATA_SCHEMA = { + vol.Required(CONF_HOST): str, +} + + +def test_connect(host, port): + """LG Soundbar config flow test_connect.""" + uuid_q = Queue(maxsize=1) + name_q = Queue(maxsize=1) + + def msg_callback(response): + if response["msg"] == "MAC_INFO_DEV" and "s_uuid" in response["data"]: + uuid_q.put_nowait(response["data"]["s_uuid"]) + if ( + response["msg"] == "SPK_LIST_VIEW_INFO" + and "s_user_name" in response["data"] + ): + name_q.put_nowait(response["data"]["s_user_name"]) + + try: + connection = temescal.temescal(host, port=port, callback=msg_callback) + connection.get_mac_info() + connection.get_info() + details = {"name": name_q.get(timeout=10), "uuid": uuid_q.get(timeout=10)} + return details + except socket.timeout as err: + raise ConnectionError(f"Connection timeout with server: {host}:{port}") from err + except OSError as err: + raise ConnectionError(f"Cannot resolve hostname: {host}") from err + + +class LGSoundbarConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """LG Soundbar config flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + if user_input is None: + return self._show_form() + + errors = {} + try: + details = await self.hass.async_add_executor_job( + test_connect, user_input[CONF_HOST], DEFAULT_PORT + ) + except ConnectionError: + errors["base"] = "cannot_connect" + else: + await self.async_set_unique_id(details["uuid"]) + self._abort_if_unique_id_configured() + info = { + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: DEFAULT_PORT, + } + return self.async_create_entry(title=details["name"], data=info) + + return self._show_form(errors) + + def _show_form(self, errors=None): + """Show the form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema(DATA_SCHEMA), + errors=errors if errors else {}, + ) diff --git a/homeassistant/components/lg_soundbar/const.py b/homeassistant/components/lg_soundbar/const.py new file mode 100644 index 00000000000..c71e43c0d60 --- /dev/null +++ b/homeassistant/components/lg_soundbar/const.py @@ -0,0 +1,4 @@ +"""Constants for the LG Soundbar integration.""" +DOMAIN = "lg_soundbar" + +DEFAULT_PORT = 9741 diff --git a/homeassistant/components/lg_soundbar/manifest.json b/homeassistant/components/lg_soundbar/manifest.json index f40ad1d194c..c05174a8938 100644 --- a/homeassistant/components/lg_soundbar/manifest.json +++ b/homeassistant/components/lg_soundbar/manifest.json @@ -1,8 +1,9 @@ { "domain": "lg_soundbar", + "config_flow": true, "name": "LG Soundbars", "documentation": "https://www.home-assistant.io/integrations/lg_soundbar", - "requirements": ["temescal==0.3"], + "requirements": ["temescal==0.5"], "codeowners": [], "iot_class": "local_polling", "loggers": ["temescal"] diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index 569678c8c15..f8f6fcf26fd 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -7,26 +7,33 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, ) -from homeassistant.const import STATE_ON +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the LG platform.""" - if discovery_info is not None: - add_entities([LGDevice(discovery_info)]) + """Set up media_player from a config entry created in the integrations UI.""" + async_add_entities( + [ + LGDevice( + config_entry.data[CONF_HOST], + config_entry.data[CONF_PORT], + config_entry.unique_id, + ) + ] + ) class LGDevice(MediaPlayerEntity): """Representation of an LG soundbar device.""" + _attr_should_poll = False _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_MUTE @@ -34,13 +41,13 @@ class LGDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOUND_MODE ) - def __init__(self, discovery_info): + def __init__(self, host, port, unique_id): """Initialize the LG speakers.""" - self._host = discovery_info["host"] - self._port = discovery_info["port"] - self._hostname = discovery_info["hostname"] + self._host = host + self._port = port + self._attr_unique_id = unique_id - self._name = self._hostname.split(".")[0] + self._name = None self._volume = 0 self._volume_min = 0 self._volume_max = 0 @@ -68,6 +75,8 @@ class LGDevice(MediaPlayerEntity): self._device = temescal.temescal( self._host, port=self._port, callback=self.handle_event ) + self._device.get_product_info() + self._device.get_mac_info() self.update() def handle_event(self, response): @@ -116,7 +125,8 @@ class LGDevice(MediaPlayerEntity): if "i_curr_eq" in data: self._equaliser = data["i_curr_eq"] if "s_user_name" in data: - self._name = data["s_user_name"] + self._attr_name = data["s_user_name"] + self.schedule_update_ha_state() def update(self): @@ -125,17 +135,6 @@ class LGDevice(MediaPlayerEntity): self._device.get_info() self._device.get_func() self._device.get_settings() - self._device.get_product_info() - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the device.""" - return self._name @property def volume_level(self): diff --git a/homeassistant/components/lg_soundbar/strings.json b/homeassistant/components/lg_soundbar/strings.json new file mode 100644 index 00000000000..ef7bf32a051 --- /dev/null +++ b/homeassistant/components/lg_soundbar/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "existing_instance_updated": "Updated existing configuration.", + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + } +} diff --git a/homeassistant/components/lg_soundbar/translations/en.json b/homeassistant/components/lg_soundbar/translations/en.json new file mode 100644 index 00000000000..a646279203f --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "existing_instance_updated": "Updated existing configuration." + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index f344e3acb5c..b0da8f79418 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -190,6 +190,7 @@ FLOWS = { "kulersky", "launch_library", "laundrify", + "lg_soundbar", "life360", "lifx", "litejet", diff --git a/requirements_all.txt b/requirements_all.txt index f992d73f894..d437757bacd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2289,7 +2289,7 @@ tellcore-py==1.1.2 tellduslive==0.10.11 # homeassistant.components.lg_soundbar -temescal==0.3 +temescal==0.5 # homeassistant.components.temper temperusb==1.5.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 12e59e5d423..e29624cd2f4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1524,6 +1524,9 @@ tailscale==0.2.0 # homeassistant.components.tellduslive tellduslive==0.10.11 +# homeassistant.components.lg_soundbar +temescal==0.5 + # homeassistant.components.powerwall tesla-powerwall==0.3.18 diff --git a/tests/components/lg_soundbar/__init__.py b/tests/components/lg_soundbar/__init__.py new file mode 100644 index 00000000000..8756d343130 --- /dev/null +++ b/tests/components/lg_soundbar/__init__.py @@ -0,0 +1 @@ +"""Tests for the lg_soundbar component.""" diff --git a/tests/components/lg_soundbar/test_config_flow.py b/tests/components/lg_soundbar/test_config_flow.py new file mode 100644 index 00000000000..3fafc2c7628 --- /dev/null +++ b/tests/components/lg_soundbar/test_config_flow.py @@ -0,0 +1,95 @@ +"""Test the lg_soundbar config flow.""" +from unittest.mock import MagicMock, patch + +from homeassistant import config_entries +from homeassistant.components.lg_soundbar.const import DEFAULT_PORT, DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT + +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 result["errors"] == {} + + with patch( + "homeassistant.components.lg_soundbar.config_flow.temescal", + return_value=MagicMock(), + ), patch( + "homeassistant.components.lg_soundbar.config_flow.test_connect", + return_value={"uuid": "uuid", "name": "name"}, + ), patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "name" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.lg_soundbar.config_flow.test_connect", + side_effect=ConnectionError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_already_configured(hass): + """Test we handle already configured error.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 0000, + }, + unique_id="uuid", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.lg_soundbar.config_flow.test_connect", + return_value={"uuid": "uuid", "name": "name"}, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" From 8bcccb17f9119148e456455143064c19b5ea8363 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 30 Jun 2022 13:03:39 -0400 Subject: [PATCH 2007/3516] Fix ZHA events for logbook (#74245) --- homeassistant/components/zha/logbook.py | 10 +++++++--- tests/components/zha/test_logbook.py | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/logbook.py b/homeassistant/components/zha/logbook.py index e2d238ddbe8..8140a5244f1 100644 --- a/homeassistant/components/zha/logbook.py +++ b/homeassistant/components/zha/logbook.py @@ -62,14 +62,18 @@ def async_describe_events( break if event_type is None: - event_type = event_data[ATTR_COMMAND] + event_type = event_data.get(ATTR_COMMAND, ZHA_EVENT) if event_subtype is not None and event_subtype != event_type: event_type = f"{event_type} - {event_subtype}" - event_type = event_type.replace("_", " ").title() + if event_type is not None: + event_type = event_type.replace("_", " ").title() + if "event" in event_type.lower(): + message = f"{event_type} was fired" + else: + message = f"{event_type} event was fired" - message = f"{event_type} event was fired" if event_data["params"]: message = f"{message} with parameters: {event_data['params']}" diff --git a/tests/components/zha/test_logbook.py b/tests/components/zha/test_logbook.py index 33b758fd0a7..6c28284b1e6 100644 --- a/tests/components/zha/test_logbook.py +++ b/tests/components/zha/test_logbook.py @@ -172,6 +172,19 @@ async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): }, }, ), + MockRow( + ZHA_EVENT, + { + CONF_DEVICE_ID: reg_device.id, + "device_ieee": str(ieee_address), + CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + "params": { + "test": "test", + }, + }, + ), ], ) @@ -182,6 +195,12 @@ async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): == "Shake event was fired with parameters: {'test': 'test'}" ) + assert events[1]["name"] == "FakeManufacturer FakeModel" + assert events[1]["domain"] == "zha" + assert ( + events[1]["message"] == "Zha Event was fired with parameters: {'test': 'test'}" + ) + async def test_zha_logbook_event_device_no_device(hass, mock_devices): """Test zha logbook events without device and without triggers.""" From 105b1b9d58cdc72abaf6fa12deeaa73d42b1f208 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 19:04:59 +0200 Subject: [PATCH 2008/3516] Update numpy to 1.23.0 (#74250) --- homeassistant/components/compensation/manifest.json | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/gen_requirements_all.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index eb3135954b0..509d5740b22 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,7 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.22.4"], + "requirements": ["numpy==1.23.0"], "codeowners": ["@Petro31"], "iot_class": "calculated" } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 8b0dacd3575..7485ff9d608 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.22.4", "pyiqvia==2022.04.0"], + "requirements": ["numpy==1.23.0", "pyiqvia==2022.04.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyiqvia"] diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 8cd1604f106..0272feb0f9e 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.22.4", "opencv-python-headless==4.6.0.66"], + "requirements": ["numpy==1.23.0", "opencv-python-headless==4.6.0.66"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 4168d820fb6..42d0eae1ecd 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.5.0", "tf-models-official==2.5.0", "pycocotools==2.0.1", - "numpy==1.22.4", + "numpy==1.23.0", "pillow==9.1.1" ], "codeowners": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 578aea3bbc6..b579cc036bb 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.22.4"], + "requirements": ["numpy==1.23.0"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9ba945c3e2b..6b7b689b93d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -88,7 +88,7 @@ httpcore==0.15.0 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy>=1.22.0 +numpy==1.23.0 # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index d437757bacd..945fa01b959 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1126,7 +1126,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.22.4 +numpy==1.23.0 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e29624cd2f4..cc7aedb43c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -782,7 +782,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.22.4 +numpy==1.23.0 # homeassistant.components.google oauth2client==4.1.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index a2a0eab897a..11e88976e83 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -106,7 +106,7 @@ httpcore==0.15.0 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy>=1.22.0 +numpy==1.23.0 # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 From ce03157f16bc60abae6fbb2d8936eca23064183d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Jun 2022 12:05:29 -0500 Subject: [PATCH 2009/3516] Add debug logging to esphome state updates (#74260) --- homeassistant/components/esphome/entry_data.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 7980d1a6a17..d4bcc67db4a 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable from dataclasses import dataclass, field +import logging from typing import Any, cast from aioesphomeapi import ( @@ -36,6 +37,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store SAVE_DELAY = 120 +_LOGGER = logging.getLogger(__name__) # Mapping from ESPHome info type to HA platform INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { @@ -128,6 +130,12 @@ class RuntimeEntryData: component_key = self.key_to_component[state.key] self.state[component_key][state.key] = state signal = f"esphome_{self.entry_id}_update_{component_key}_{state.key}" + _LOGGER.debug( + "Dispatching update for component %s with state key %s: %s", + component_key, + state.key, + state, + ) async_dispatcher_send(hass, signal) @callback From f311d53c60a11c18ae4dbbcc9d1ab1ebfd3cdbf7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 1 Jul 2022 05:07:03 +1200 Subject: [PATCH 2010/3516] ESPHome use dhcp responses to update connection host of known devices (#74206) * ESPHome use dhcp responses to update connection host of known devices * Add test for dhcp * Add another test to cover when there are no changes required --- .../components/esphome/config_flow.py | 45 +++++++++++++- .../components/esphome/manifest.json | 1 + homeassistant/generated/dhcp.py | 1 + tests/components/esphome/test_config_flow.py | 60 ++++++++++++++++++- 4 files changed, 105 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 76359cda4e7..ecfa381bc69 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -16,7 +16,7 @@ from aioesphomeapi import ( ) import voluptuous as vol -from homeassistant.components import zeroconf +from homeassistant.components import dhcp, zeroconf from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback @@ -189,6 +189,49 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): return await self.async_step_discovery_confirm() + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Handle DHCP discovery.""" + node_name = discovery_info.hostname + + await self.async_set_unique_id(node_name) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) + + for entry in self._async_current_entries(): + found = False + + if CONF_HOST in entry.data and entry.data[CONF_HOST] in ( + discovery_info.ip, + f"{node_name}.local", + ): + # Is this address or IP address already configured? + found = True + elif DomainData.get(self.hass).is_entry_loaded(entry): + # Does a config entry with this name already exist? + data = DomainData.get(self.hass).get_entry_data(entry) + + # Node names are unique in the network + if data.device_info is not None: + found = data.device_info.name == node_name + + if found: + # Backwards compat, we update old entries + if not entry.unique_id: + self.hass.config_entries.async_update_entry( + entry, + data={ + **entry.data, + CONF_HOST: discovery_info.ip, + }, + unique_id=node_name, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + + break + + return self.async_abort(reason="already_configured") + @callback def _async_get_entry(self) -> FlowResult: config_data = { diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index b89671c6f90..a8a76c2b0c8 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -5,6 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": ["aioesphomeapi==10.10.0"], "zeroconf": ["_esphomelib._tcp.local."], + "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], "iot_class": "local_push", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 91398ed00ef..e9cf6ca4c06 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -28,6 +28,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'elkm1', 'macaddress': '00409D*'}, {'domain': 'emonitor', 'hostname': 'emonitor*', 'macaddress': '0090C2*'}, {'domain': 'emonitor', 'registered_devices': True}, + {'domain': 'esphome', 'registered_devices': True}, {'domain': 'flume', 'hostname': 'flume-gw-*'}, {'domain': 'flux_led', 'registered_devices': True}, {'domain': 'flux_led', 'hostname': '[ba][lk]*', 'macaddress': '18B905*'}, diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index f7da5d66bd5..1d2cff051ae 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -12,7 +12,7 @@ from aioesphomeapi import ( import pytest from homeassistant import config_entries -from homeassistant.components import zeroconf +from homeassistant.components import dhcp, zeroconf from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN, DomainData from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.data_entry_flow import ( @@ -532,3 +532,61 @@ async def test_reauth_confirm_invalid(hass, mock_client, mock_zeroconf): assert result["step_id"] == "reauth_confirm" assert result["errors"] assert result["errors"]["base"] == "invalid_psk" + + +async def test_discovery_dhcp_updates_host(hass, mock_client): + """Test dhcp discovery updates host and aborts.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + ) + entry.add_to_hass(hass) + + mock_entry_data = MagicMock() + mock_entry_data.device_info.name = "test8266" + domain_data = DomainData.get(hass) + domain_data.set_entry_data(entry, mock_entry_data) + + service_info = dhcp.DhcpServiceInfo( + ip="192.168.43.184", + hostname="test8266", + macaddress="00:00:00:00:00:00", + ) + result = await hass.config_entries.flow.async_init( + "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + assert entry.unique_id == "test8266" + assert entry.data[CONF_HOST] == "192.168.43.184" + + +async def test_discovery_dhcp_no_changes(hass, mock_client): + """Test dhcp discovery updates host and aborts.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + ) + entry.add_to_hass(hass) + + mock_entry_data = MagicMock() + mock_entry_data.device_info.name = "test8266" + domain_data = DomainData.get(hass) + domain_data.set_entry_data(entry, mock_entry_data) + + service_info = dhcp.DhcpServiceInfo( + ip="192.168.43.183", + hostname="test8266", + macaddress="00:00:00:00:00:00", + ) + result = await hass.config_entries.flow.async_init( + "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + assert entry.unique_id == "test8266" + assert entry.data[CONF_HOST] == "192.168.43.183" From 1bdd93cc776ac95095f42666e8c0ef5b3c5ca9ae Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Thu, 30 Jun 2022 12:10:05 -0500 Subject: [PATCH 2011/3516] Fix Life360 unload (#74263) * Fix life360 async_unload_entry * Update tracked_members when unloading config entry --- homeassistant/components/life360/__init__.py | 14 ++++++++++---- homeassistant/components/life360/device_tracker.py | 10 +++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/life360/__init__.py b/homeassistant/components/life360/__init__.py index 66c9416a1c1..4527f6ac298 100644 --- a/homeassistant/components/life360/__init__.py +++ b/homeassistant/components/life360/__init__.py @@ -124,11 +124,11 @@ class IntegData: """Integration data.""" cfg_options: dict[str, Any] | None = None - # ConfigEntry.unique_id: Life360DataUpdateCoordinator + # ConfigEntry.entry_id: Life360DataUpdateCoordinator coordinators: dict[str, Life360DataUpdateCoordinator] = field( init=False, default_factory=dict ) - # member_id: ConfigEntry.unique_id + # member_id: ConfigEntry.entry_id tracked_members: dict[str, str] = field(init=False, default_factory=dict) logged_circles: list[str] = field(init=False, default_factory=list) logged_places: list[str] = field(init=False, default_factory=list) @@ -171,7 +171,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload config entry.""" - del hass.data[DOMAIN].coordinators[entry.entry_id] # Unload components for our platforms. - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + del hass.data[DOMAIN].coordinators[entry.entry_id] + # Remove any members that were tracked by this entry. + for member_id, entry_id in hass.data[DOMAIN].tracked_members.copy().items(): + if entry_id == entry.entry_id: + del hass.data[DOMAIN].tracked_members[member_id] + + return unload_ok diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index a38181a6830..5a18422487e 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -78,13 +78,13 @@ async def async_setup_entry( new_entities = [] for member_id, member in coordinator.data.members.items(): - tracked_by_account = tracked_members.get(member_id) - if new_member := not tracked_by_account: - tracked_members[member_id] = entry.unique_id - LOGGER.debug("Member: %s", member.name) + tracked_by_entry = tracked_members.get(member_id) + if new_member := not tracked_by_entry: + tracked_members[member_id] = entry.entry_id + LOGGER.debug("Member: %s (%s)", member.name, entry.unique_id) if ( new_member - or tracked_by_account == entry.unique_id + or tracked_by_entry == entry.entry_id and not new_members_only ): new_entities.append(Life360DeviceTracker(coordinator, member_id)) From f05b4a0ca05c5090ce007bbd0e22f2e76c643270 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 30 Jun 2022 19:15:25 +0200 Subject: [PATCH 2012/3516] Fire event_mqtt_reloaded only after reload is completed (#74226) --- homeassistant/components/mqtt/__init__.py | 70 +++++++++++-------- .../components/mqtt/alarm_control_panel.py | 6 +- .../components/mqtt/binary_sensor.py | 6 +- homeassistant/components/mqtt/button.py | 6 +- homeassistant/components/mqtt/camera.py | 6 +- homeassistant/components/mqtt/climate.py | 6 +- homeassistant/components/mqtt/const.py | 3 +- homeassistant/components/mqtt/cover.py | 6 +- homeassistant/components/mqtt/fan.py | 4 +- homeassistant/components/mqtt/humidifier.py | 6 +- .../components/mqtt/light/__init__.py | 6 +- homeassistant/components/mqtt/lock.py | 6 +- homeassistant/components/mqtt/mixins.py | 46 +++++------- homeassistant/components/mqtt/number.py | 6 +- homeassistant/components/mqtt/scene.py | 6 +- homeassistant/components/mqtt/select.py | 6 +- homeassistant/components/mqtt/sensor.py | 6 +- homeassistant/components/mqtt/siren.py | 6 +- homeassistant/components/mqtt/switch.py | 6 +- .../components/mqtt/vacuum/__init__.py | 6 +- 20 files changed, 96 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 6fd288a86cf..a099e7b580c 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -28,14 +28,12 @@ from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.device_registry import DeviceEntry -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import ( async_integration_yaml_config, - async_setup_reload_service, + async_reload_integration_platforms, ) +from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType # Loading the config flow file will register the flow @@ -78,10 +76,10 @@ from .const import ( # noqa: F401 DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, - MQTT_RELOADED, PLATFORMS, RELOADABLE_PLATFORMS, ) +from .mixins import async_discover_yaml_entities from .models import ( # noqa: F401 MqttCommandTemplate, MqttValueTemplate, @@ -241,7 +239,9 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) - await _async_setup_discovery(hass, mqtt_client.conf, entry) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( # noqa: C901 + hass: HomeAssistant, entry: ConfigEntry +) -> bool: """Load a config entry.""" # Merge basic configuration, and add missing defaults for basic options _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) @@ -378,16 +378,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[CONFIG_ENTRY_IS_SETUP] = set() - # Setup reload service. Once support for legacy config is removed in 2022.9, we - # should no longer call async_setup_reload_service but instead implement a custom - # service - await async_setup_reload_service(hass, DOMAIN, RELOADABLE_PLATFORMS) + async def async_setup_reload_service() -> None: + """Create the reload service for the MQTT domain.""" + if hass.services.has_service(DOMAIN, SERVICE_RELOAD): + return - async def _async_reload_platforms(_: Event | None) -> None: - """Discover entities for a platform.""" - config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {} - hass.data[DATA_MQTT_UPDATED_CONFIG] = config_yaml.get(DOMAIN, {}) - async_dispatcher_send(hass, MQTT_RELOADED) + async def _reload_config(call: ServiceCall) -> None: + """Reload the platforms.""" + # Reload the legacy yaml platform + await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS) + + # Reload the modern yaml platforms + config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {} + hass.data[DATA_MQTT_UPDATED_CONFIG] = config_yaml.get(DOMAIN, {}) + await asyncio.gather( + *( + [ + async_discover_yaml_entities(hass, component) + for component in RELOADABLE_PLATFORMS + ] + ) + ) + + # Fire event + hass.bus.async_fire(f"event_{DOMAIN}_reloaded", context=call.context) + + async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, _reload_config) async def async_forward_entry_setup_and_setup_discovery(config_entry): """Forward the config entry setup to the platforms and set up discovery.""" @@ -411,21 +427,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if conf.get(CONF_DISCOVERY): await _async_setup_discovery(hass, conf, entry) # Setup reload service after all platforms have loaded - entry.async_on_unload( - hass.bus.async_listen("event_mqtt_reloaded", _async_reload_platforms) - ) + await async_setup_reload_service() + + if DATA_MQTT_RELOAD_NEEDED in hass.data: + hass.data.pop(DATA_MQTT_RELOAD_NEEDED) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=False, + ) hass.async_create_task(async_forward_entry_setup_and_setup_discovery(entry)) - if DATA_MQTT_RELOAD_NEEDED in hass.data: - hass.data.pop(DATA_MQTT_RELOAD_NEEDED) - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - {}, - blocking=False, - ) - return True diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 8e5ee54d688..b6f2f8f236e 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -44,8 +44,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -147,9 +147,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT alarm control panel through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, alarm.DOMAIN) - ) + await async_discover_yaml_entities(hass, alarm.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 39fd87c8b02..9e0a049b15e 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -41,8 +41,8 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -102,9 +102,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT binary sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, binary_sensor.DOMAIN) - ) + await async_discover_yaml_entities(hass, binary_sensor.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index 0374727bf7d..b75fbe4b97f 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -25,8 +25,8 @@ from .const import ( from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -82,9 +82,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT button through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, button.DOMAIN) - ) + await async_discover_yaml_entities(hass, button.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 5c8d3bc48b2..69af7992229 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -22,8 +22,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -80,9 +80,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT camera through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, camera.DOMAIN) - ) + await async_discover_yaml_entities(hass, camera.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index a26e9cba8df..6b09891483c 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -50,8 +50,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -391,9 +391,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT climate device through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, climate.DOMAIN) - ) + await async_discover_yaml_entities(hass, climate.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 67a9208faba..6ac77021337 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -64,7 +64,6 @@ DOMAIN = "mqtt" MQTT_CONNECTED = "mqtt_connected" MQTT_DISCONNECTED = "mqtt_disconnected" -MQTT_RELOADED = "mqtt_reloaded" PAYLOAD_EMPTY_JSON = "{}" PAYLOAD_NONE = "None" @@ -105,8 +104,8 @@ RELOADABLE_PLATFORMS = [ Platform.LIGHT, Platform.LOCK, Platform.NUMBER, - Platform.SELECT, Platform.SCENE, + Platform.SELECT, Platform.SENSOR, Platform.SIREN, Platform.SWITCH, diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 54ed4f2b0a0..14746329250 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -46,8 +46,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -242,9 +242,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT cover through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, cover.DOMAIN) - ) + await async_discover_yaml_entities(hass, cover.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 721fa93f244..15e4a80f3e7 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -50,8 +50,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -232,7 +232,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload(await async_setup_platform_discovery(hass, fan.DOMAIN)) + await async_discover_yaml_entities(hass, fan.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index d2856767cf0..5f09fc0d513 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -45,8 +45,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -187,9 +187,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT humidifier through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, humidifier.DOMAIN) - ) + await async_discover_yaml_entities(hass, humidifier.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index d4914cb9506..c7f3395ba4e 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -13,8 +13,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ..mixins import ( + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -111,9 +111,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT lights configured under the light platform key (deprecated).""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, light.DOMAIN) - ) + await async_discover_yaml_entities(hass, light.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 1d6a40c2331..b4788f1db0c 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -28,8 +28,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -103,9 +103,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT lock through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, lock.DOMAIN) - ) + await async_discover_yaml_entities(hass, lock.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 10fe6cb6cc5..8e59d09dfce 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -26,7 +26,7 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -69,7 +69,6 @@ from .const import ( DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, - MQTT_RELOADED, ) from .debug_info import log_message, log_messages from .discovery import ( @@ -261,34 +260,27 @@ class SetupEntity(Protocol): """Define setup_entities type.""" -async def async_setup_platform_discovery( +async def async_discover_yaml_entities( hass: HomeAssistant, platform_domain: str -) -> CALLBACK_TYPE: - """Set up platform discovery for manual config.""" - - async def _async_discover_entities() -> None: - """Discover entities for a platform.""" - if DATA_MQTT_UPDATED_CONFIG in hass.data: - # The platform has been reloaded - config_yaml = hass.data[DATA_MQTT_UPDATED_CONFIG] - else: - config_yaml = hass.data.get(DATA_MQTT_CONFIG, {}) - if not config_yaml: - return - if platform_domain not in config_yaml: - return - await asyncio.gather( - *( - discovery.async_load_platform(hass, platform_domain, DOMAIN, config, {}) - for config in await async_get_platform_config_from_yaml( - hass, platform_domain, config_yaml - ) +) -> None: + """Discover entities for a platform.""" + if DATA_MQTT_UPDATED_CONFIG in hass.data: + # The platform has been reloaded + config_yaml = hass.data[DATA_MQTT_UPDATED_CONFIG] + else: + config_yaml = hass.data.get(DATA_MQTT_CONFIG, {}) + if not config_yaml: + return + if platform_domain not in config_yaml: + return + await asyncio.gather( + *( + discovery.async_load_platform(hass, platform_domain, DOMAIN, config, {}) + for config in await async_get_platform_config_from_yaml( + hass, platform_domain, config_yaml ) ) - - unsub = async_dispatcher_connect(hass, MQTT_RELOADED, _async_discover_entities) - await _async_discover_entities() - return unsub + ) async def async_get_platform_config_from_yaml( diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 660ffe987f0..dc27a740720 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -41,8 +41,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -135,9 +135,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT number through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, number.DOMAIN) - ) + await async_discover_yaml_entities(hass, number.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index cc911cc3431..8b654f7cca0 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -22,8 +22,8 @@ from .mixins import ( CONF_OBJECT_ID, MQTT_AVAILABILITY_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -79,9 +79,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT scene through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, scene.DOMAIN) - ) + await async_discover_yaml_entities(hass, scene.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 0d9f1411fd1..4c302446b19 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -30,8 +30,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -94,9 +94,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT select through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, select.DOMAIN) - ) + await async_discover_yaml_entities(hass, select.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 672e22f632f..6948e173039 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -41,8 +41,8 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -147,9 +147,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, sensor.DOMAIN) - ) + await async_discover_yaml_entities(hass, sensor.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index a6cff4cf91d..dfb89d2ee79 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -51,8 +51,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -143,9 +143,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT siren through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, siren.DOMAIN) - ) + await async_discover_yaml_entities(hass, siren.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index dadd5f86f20..b04f2433659 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -37,8 +37,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -97,9 +97,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT switch through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, switch.DOMAIN) - ) + await async_discover_yaml_entities(hass, switch.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 694e9530939..c49b8cfa012 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -12,8 +12,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ..mixins import ( + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, ) from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE @@ -91,9 +91,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT vacuum through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, vacuum.DOMAIN) - ) + await async_discover_yaml_entities(hass, vacuum.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry From 2723ca0b850536011d79498e6c21f16c88d66269 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Jun 2022 12:39:36 -0500 Subject: [PATCH 2013/3516] Filter out CONF_SCAN_INTERVAL from scrape import (#74254) --- homeassistant/components/scrape/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 1c447439820..a73dbc17c1c 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -24,6 +24,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_RESOURCE, + CONF_SCAN_INTERVAL, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_VALUE_TEMPLATE, @@ -90,7 +91,7 @@ async def async_setup_platform( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, - data=config, + data={k: v for k, v in config.items() if k != CONF_SCAN_INTERVAL}, ) ) From dc22726425107d5fdb8595a4d80ecd238f18b0cc Mon Sep 17 00:00:00 2001 From: Maximilian <43999966+DeerMaximum@users.noreply.github.com> Date: Thu, 30 Jun 2022 19:45:11 +0200 Subject: [PATCH 2014/3516] Optimize optionflow tests (#74262) * Optimize optionflow tests * Extend mocking --- tests/components/nina/test_config_flow.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/components/nina/test_config_flow.py b/tests/components/nina/test_config_flow.py index 1578991ba11..738eb80e190 100644 --- a/tests/components/nina/test_config_flow.py +++ b/tests/components/nina/test_config_flow.py @@ -163,6 +163,9 @@ async def test_options_flow_init(hass: HomeAssistant) -> None: "pynina.baseApi.BaseAPI._makeRequest", wraps=mocked_request_function, ): + 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 @@ -213,6 +216,8 @@ async def test_options_flow_with_no_selection(hass: HomeAssistant) -> None: "pynina.baseApi.BaseAPI._makeRequest", wraps=mocked_request_function, ): + 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) @@ -248,7 +253,12 @@ async def test_options_flow_connection_error(hass: HomeAssistant) -> None: with patch( "pynina.baseApi.BaseAPI._makeRequest", side_effect=ApiError("Could not connect to Api"), + ), patch( + "homeassistant.components.nina.async_setup_entry", + return_value=True, ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -268,7 +278,12 @@ async def test_options_flow_unexpected_exception(hass: HomeAssistant) -> None: with patch( "pynina.baseApi.BaseAPI._makeRequest", side_effect=Exception("DUMMY"), + ), patch( + "homeassistant.components.nina.async_setup_entry", + return_value=True, ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) From 25a5ebe0c77c53b489c50a6c70187184a9fd243d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 30 Jun 2022 19:47:29 +0200 Subject: [PATCH 2015/3516] Met.no use native_* (#74259) --- homeassistant/components/met/const.py | 16 +++---- homeassistant/components/met/weather.py | 57 ++++++------------------- 2 files changed, 20 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/met/const.py b/homeassistant/components/met/const.py index 93f9e3414dd..5b2a756847e 100644 --- a/homeassistant/components/met/const.py +++ b/homeassistant/components/met/const.py @@ -11,13 +11,13 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY_RAINY, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, @@ -173,13 +173,13 @@ CONDITIONS_MAP = { FORECAST_MAP = { ATTR_FORECAST_CONDITION: "condition", - ATTR_FORECAST_PRECIPITATION: "precipitation", + ATTR_FORECAST_NATIVE_PRECIPITATION: "precipitation", ATTR_FORECAST_PRECIPITATION_PROBABILITY: "precipitation_probability", - ATTR_FORECAST_TEMP: "temperature", - ATTR_FORECAST_TEMP_LOW: "templow", + ATTR_FORECAST_NATIVE_TEMP: "temperature", + ATTR_FORECAST_NATIVE_TEMP_LOW: "templow", ATTR_FORECAST_TIME: "datetime", ATTR_FORECAST_WIND_BEARING: "wind_bearing", - ATTR_FORECAST_WIND_SPEED: "wind_speed", + ATTR_FORECAST_NATIVE_WIND_SPEED: "wind_speed", } ATTR_MAP = { diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 251d99ad295..0ff0a60bfa1 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -6,7 +6,6 @@ from typing import Any from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, @@ -21,12 +20,9 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - LENGTH_INCHES, LENGTH_MILLIMETERS, PRESSURE_HPA, - PRESSURE_INHG, SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -34,19 +30,9 @@ from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util.distance import convert as convert_distance -from homeassistant.util.pressure import convert as convert_pressure -from homeassistant.util.speed import convert as convert_speed from . import MetDataUpdateCoordinator -from .const import ( - ATTR_FORECAST_PRECIPITATION, - ATTR_MAP, - CONDITIONS_MAP, - CONF_TRACK_HOME, - DOMAIN, - FORECAST_MAP, -) +from .const import ATTR_MAP, CONDITIONS_MAP, CONF_TRACK_HOME, DOMAIN, FORECAST_MAP ATTRIBUTION = ( "Weather forecast from met.no, delivered by the Norwegian " @@ -85,6 +71,11 @@ def format_condition(condition: str) -> str: class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): """Implementation of a Met.no weather condition.""" + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__( self, coordinator: MetDataUpdateCoordinator, @@ -144,27 +135,18 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): return format_condition(condition) @property - def temperature(self) -> float | None: + def native_temperature(self) -> float | None: """Return the temperature.""" return self.coordinator.data.current_weather_data.get( ATTR_MAP[ATTR_WEATHER_TEMPERATURE] ) @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def pressure(self) -> float | None: + def native_pressure(self) -> float | None: """Return the pressure.""" - pressure_hpa = self.coordinator.data.current_weather_data.get( + return self.coordinator.data.current_weather_data.get( ATTR_MAP[ATTR_WEATHER_PRESSURE] ) - if self._is_metric or pressure_hpa is None: - return pressure_hpa - - return round(convert_pressure(pressure_hpa, PRESSURE_HPA, PRESSURE_INHG), 2) @property def humidity(self) -> float | None: @@ -174,18 +156,11 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): ) @property - def wind_speed(self) -> float | None: + def native_wind_speed(self) -> float | None: """Return the wind speed.""" - speed_km_h = self.coordinator.data.current_weather_data.get( + return self.coordinator.data.current_weather_data.get( ATTR_MAP[ATTR_WEATHER_WIND_SPEED] ) - if self._is_metric or speed_km_h is None: - return speed_km_h - - speed_mi_h = convert_speed( - speed_km_h, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR - ) - return int(round(speed_mi_h)) @property def wind_bearing(self) -> float | str | None: @@ -206,7 +181,7 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): met_forecast = self.coordinator.data.hourly_forecast else: met_forecast = self.coordinator.data.daily_forecast - required_keys = {ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME} + required_keys = {"temperature", ATTR_FORECAST_TIME} ha_forecast: list[Forecast] = [] for met_item in met_forecast: if not set(met_item).issuperset(required_keys): @@ -216,14 +191,6 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): for k, v in FORECAST_MAP.items() if met_item.get(v) is not None } - if not self._is_metric and ATTR_FORECAST_PRECIPITATION in ha_item: - if ha_item[ATTR_FORECAST_PRECIPITATION] is not None: - precip_inches = convert_distance( - ha_item[ATTR_FORECAST_PRECIPITATION], - LENGTH_MILLIMETERS, - LENGTH_INCHES, - ) - ha_item[ATTR_FORECAST_PRECIPITATION] = round(precip_inches, 2) if ha_item.get(ATTR_FORECAST_CONDITION): ha_item[ATTR_FORECAST_CONDITION] = format_condition( ha_item[ATTR_FORECAST_CONDITION] From 382b5d5073362b96976ce94f718f7fa4db6b628a Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 30 Jun 2022 13:01:23 -0500 Subject: [PATCH 2016/3516] Bump frontend to 20220630.0 (#74266) --- 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 23f056ba0da..27ff0a73f20 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220629.0"], + "requirements": ["home-assistant-frontend==20220630.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6b7b689b93d..7ee5a9fe8d3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220629.0 +home-assistant-frontend==20220630.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 945fa01b959..eac0800e73a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220629.0 +home-assistant-frontend==20220630.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cc7aedb43c8..f4b5e587f7a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -598,7 +598,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220629.0 +home-assistant-frontend==20220630.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 71c2f99ee4d1562546cbf3144ec6c0af48b56a13 Mon Sep 17 00:00:00 2001 From: MasonCrawford Date: Fri, 1 Jul 2022 01:00:39 +0800 Subject: [PATCH 2017/3516] Add config flow to lg_soundbar (#71153) Co-authored-by: Paulus Schoutsen --- .../components/discovery/__init__.py | 2 +- .../components/lg_soundbar/__init__.py | 37 ++++++++ .../components/lg_soundbar/config_flow.py | 78 +++++++++++++++ homeassistant/components/lg_soundbar/const.py | 4 + .../components/lg_soundbar/manifest.json | 3 +- .../components/lg_soundbar/media_player.py | 51 +++++----- .../components/lg_soundbar/strings.json | 18 ++++ .../lg_soundbar/translations/en.json | 18 ++++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/lg_soundbar/__init__.py | 1 + .../lg_soundbar/test_config_flow.py | 95 +++++++++++++++++++ 13 files changed, 284 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/lg_soundbar/config_flow.py create mode 100644 homeassistant/components/lg_soundbar/const.py create mode 100644 homeassistant/components/lg_soundbar/strings.json create mode 100644 homeassistant/components/lg_soundbar/translations/en.json create mode 100644 tests/components/lg_soundbar/__init__.py create mode 100644 tests/components/lg_soundbar/test_config_flow.py diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index a0ffbf235ab..3c3538c1ca0 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -63,7 +63,6 @@ SERVICE_HANDLERS = { "openhome": ServiceDetails("media_player", "openhome"), "bose_soundtouch": ServiceDetails("media_player", "soundtouch"), "bluesound": ServiceDetails("media_player", "bluesound"), - "lg_smart_device": ServiceDetails("media_player", "lg_soundbar"), } OPTIONAL_SERVICE_HANDLERS: dict[str, tuple[str, str | None]] = {} @@ -98,6 +97,7 @@ MIGRATED_SERVICE_HANDLERS = [ SERVICE_YEELIGHT, SERVICE_SABNZBD, "nanoleaf_aurora", + "lg_smart_device", ] DEFAULT_ENABLED = ( diff --git a/homeassistant/components/lg_soundbar/__init__.py b/homeassistant/components/lg_soundbar/__init__.py index 175153556f9..75b2109b22a 100644 --- a/homeassistant/components/lg_soundbar/__init__.py +++ b/homeassistant/components/lg_soundbar/__init__.py @@ -1 +1,38 @@ """The lg_soundbar component.""" +import logging + +from homeassistant import config_entries, core +from homeassistant.const import CONF_HOST, CONF_PORT, Platform +from homeassistant.exceptions import ConfigEntryNotReady + +from .config_flow import test_connect +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [Platform.MEDIA_PLAYER] + + +async def async_setup_entry( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: + """Set up platform from a ConfigEntry.""" + hass.data.setdefault(DOMAIN, {}) + # Verify the device is reachable with the given config before setting up the platform + try: + await hass.async_add_executor_job( + test_connect, entry.data[CONF_HOST], entry.data[CONF_PORT] + ) + except ConnectionError as err: + raise ConfigEntryNotReady from err + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: + """Unload a config entry.""" + result = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + return result diff --git a/homeassistant/components/lg_soundbar/config_flow.py b/homeassistant/components/lg_soundbar/config_flow.py new file mode 100644 index 00000000000..bd9a727d1f4 --- /dev/null +++ b/homeassistant/components/lg_soundbar/config_flow.py @@ -0,0 +1,78 @@ +"""Config flow to configure the LG Soundbar integration.""" +from queue import Queue +import socket + +import temescal +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT + +from .const import DEFAULT_PORT, DOMAIN + +DATA_SCHEMA = { + vol.Required(CONF_HOST): str, +} + + +def test_connect(host, port): + """LG Soundbar config flow test_connect.""" + uuid_q = Queue(maxsize=1) + name_q = Queue(maxsize=1) + + def msg_callback(response): + if response["msg"] == "MAC_INFO_DEV" and "s_uuid" in response["data"]: + uuid_q.put_nowait(response["data"]["s_uuid"]) + if ( + response["msg"] == "SPK_LIST_VIEW_INFO" + and "s_user_name" in response["data"] + ): + name_q.put_nowait(response["data"]["s_user_name"]) + + try: + connection = temescal.temescal(host, port=port, callback=msg_callback) + connection.get_mac_info() + connection.get_info() + details = {"name": name_q.get(timeout=10), "uuid": uuid_q.get(timeout=10)} + return details + except socket.timeout as err: + raise ConnectionError(f"Connection timeout with server: {host}:{port}") from err + except OSError as err: + raise ConnectionError(f"Cannot resolve hostname: {host}") from err + + +class LGSoundbarConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """LG Soundbar config flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + if user_input is None: + return self._show_form() + + errors = {} + try: + details = await self.hass.async_add_executor_job( + test_connect, user_input[CONF_HOST], DEFAULT_PORT + ) + except ConnectionError: + errors["base"] = "cannot_connect" + else: + await self.async_set_unique_id(details["uuid"]) + self._abort_if_unique_id_configured() + info = { + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: DEFAULT_PORT, + } + return self.async_create_entry(title=details["name"], data=info) + + return self._show_form(errors) + + def _show_form(self, errors=None): + """Show the form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema(DATA_SCHEMA), + errors=errors if errors else {}, + ) diff --git a/homeassistant/components/lg_soundbar/const.py b/homeassistant/components/lg_soundbar/const.py new file mode 100644 index 00000000000..c71e43c0d60 --- /dev/null +++ b/homeassistant/components/lg_soundbar/const.py @@ -0,0 +1,4 @@ +"""Constants for the LG Soundbar integration.""" +DOMAIN = "lg_soundbar" + +DEFAULT_PORT = 9741 diff --git a/homeassistant/components/lg_soundbar/manifest.json b/homeassistant/components/lg_soundbar/manifest.json index f40ad1d194c..c05174a8938 100644 --- a/homeassistant/components/lg_soundbar/manifest.json +++ b/homeassistant/components/lg_soundbar/manifest.json @@ -1,8 +1,9 @@ { "domain": "lg_soundbar", + "config_flow": true, "name": "LG Soundbars", "documentation": "https://www.home-assistant.io/integrations/lg_soundbar", - "requirements": ["temescal==0.3"], + "requirements": ["temescal==0.5"], "codeowners": [], "iot_class": "local_polling", "loggers": ["temescal"] diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index 569678c8c15..f8f6fcf26fd 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -7,26 +7,33 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, ) -from homeassistant.const import STATE_ON +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -def setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the LG platform.""" - if discovery_info is not None: - add_entities([LGDevice(discovery_info)]) + """Set up media_player from a config entry created in the integrations UI.""" + async_add_entities( + [ + LGDevice( + config_entry.data[CONF_HOST], + config_entry.data[CONF_PORT], + config_entry.unique_id, + ) + ] + ) class LGDevice(MediaPlayerEntity): """Representation of an LG soundbar device.""" + _attr_should_poll = False _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_MUTE @@ -34,13 +41,13 @@ class LGDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOUND_MODE ) - def __init__(self, discovery_info): + def __init__(self, host, port, unique_id): """Initialize the LG speakers.""" - self._host = discovery_info["host"] - self._port = discovery_info["port"] - self._hostname = discovery_info["hostname"] + self._host = host + self._port = port + self._attr_unique_id = unique_id - self._name = self._hostname.split(".")[0] + self._name = None self._volume = 0 self._volume_min = 0 self._volume_max = 0 @@ -68,6 +75,8 @@ class LGDevice(MediaPlayerEntity): self._device = temescal.temescal( self._host, port=self._port, callback=self.handle_event ) + self._device.get_product_info() + self._device.get_mac_info() self.update() def handle_event(self, response): @@ -116,7 +125,8 @@ class LGDevice(MediaPlayerEntity): if "i_curr_eq" in data: self._equaliser = data["i_curr_eq"] if "s_user_name" in data: - self._name = data["s_user_name"] + self._attr_name = data["s_user_name"] + self.schedule_update_ha_state() def update(self): @@ -125,17 +135,6 @@ class LGDevice(MediaPlayerEntity): self._device.get_info() self._device.get_func() self._device.get_settings() - self._device.get_product_info() - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the device.""" - return self._name @property def volume_level(self): diff --git a/homeassistant/components/lg_soundbar/strings.json b/homeassistant/components/lg_soundbar/strings.json new file mode 100644 index 00000000000..ef7bf32a051 --- /dev/null +++ b/homeassistant/components/lg_soundbar/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "existing_instance_updated": "Updated existing configuration.", + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + } +} diff --git a/homeassistant/components/lg_soundbar/translations/en.json b/homeassistant/components/lg_soundbar/translations/en.json new file mode 100644 index 00000000000..a646279203f --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "existing_instance_updated": "Updated existing configuration." + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3c6ad94a21f..af4b8481873 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -189,6 +189,7 @@ FLOWS = { "kulersky", "launch_library", "laundrify", + "lg_soundbar", "life360", "lifx", "litejet", diff --git a/requirements_all.txt b/requirements_all.txt index 98a6c6f5a95..6f677d1d092 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2289,7 +2289,7 @@ tellcore-py==1.1.2 tellduslive==0.10.11 # homeassistant.components.lg_soundbar -temescal==0.3 +temescal==0.5 # homeassistant.components.temper temperusb==1.5.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64ad3f769b9..d0c9bc54ddd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1521,6 +1521,9 @@ tailscale==0.2.0 # homeassistant.components.tellduslive tellduslive==0.10.11 +# homeassistant.components.lg_soundbar +temescal==0.5 + # homeassistant.components.powerwall tesla-powerwall==0.3.18 diff --git a/tests/components/lg_soundbar/__init__.py b/tests/components/lg_soundbar/__init__.py new file mode 100644 index 00000000000..8756d343130 --- /dev/null +++ b/tests/components/lg_soundbar/__init__.py @@ -0,0 +1 @@ +"""Tests for the lg_soundbar component.""" diff --git a/tests/components/lg_soundbar/test_config_flow.py b/tests/components/lg_soundbar/test_config_flow.py new file mode 100644 index 00000000000..3fafc2c7628 --- /dev/null +++ b/tests/components/lg_soundbar/test_config_flow.py @@ -0,0 +1,95 @@ +"""Test the lg_soundbar config flow.""" +from unittest.mock import MagicMock, patch + +from homeassistant import config_entries +from homeassistant.components.lg_soundbar.const import DEFAULT_PORT, DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT + +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 result["errors"] == {} + + with patch( + "homeassistant.components.lg_soundbar.config_flow.temescal", + return_value=MagicMock(), + ), patch( + "homeassistant.components.lg_soundbar.config_flow.test_connect", + return_value={"uuid": "uuid", "name": "name"}, + ), patch( + "homeassistant.components.lg_soundbar.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "name" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: DEFAULT_PORT, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.lg_soundbar.config_flow.test_connect", + side_effect=ConnectionError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_already_configured(hass): + """Test we handle already configured error.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 0000, + }, + unique_id="uuid", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.lg_soundbar.config_flow.test_connect", + return_value={"uuid": "uuid", "name": "name"}, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" From 7690ecc4ff533ec333dd276c867c04fc9be9a9cb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 03:43:14 +0200 Subject: [PATCH 2018/3516] Fix clicksend request content type headers (#74189) --- homeassistant/components/clicksend/notify.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/clicksend/notify.py b/homeassistant/components/clicksend/notify.py index 74f1c2e1ae5..ec6bed3c55d 100644 --- a/homeassistant/components/clicksend/notify.py +++ b/homeassistant/components/clicksend/notify.py @@ -3,7 +3,6 @@ from http import HTTPStatus import json import logging -from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol @@ -23,7 +22,7 @@ BASE_API_URL = "https://rest.clicksend.com/v3" DEFAULT_SENDER = "hass" TIMEOUT = 5 -HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON} +HEADERS = {"Content-Type": CONTENT_TYPE_JSON} PLATFORM_SCHEMA = vol.Schema( From 9d727d2a710c76249b88ceae4a85f4cdde072c8f Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 30 Jun 2022 02:10:25 +0300 Subject: [PATCH 2019/3516] Fix Shelly Duo RGBW color mode attribute (#74193) --- homeassistant/components/shelly/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 79db9c509f4..b75e1ad2377 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -215,7 +215,7 @@ class BlockShellyLight(ShellyBlockEntity, LightEntity): def color_mode(self) -> ColorMode: """Return the color mode of the light.""" if self.mode == "color": - if hasattr(self.block, "white"): + if self.wrapper.model in RGBW_MODELS: return ColorMode.RGBW return ColorMode.RGB From d36643947d43683c53fb2e26a33340b4e9f6547f Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 29 Jun 2022 18:52:54 -0400 Subject: [PATCH 2020/3516] Fix duplicate key for motion sensor for UniFi Protect (#74202) --- homeassistant/components/unifiprotect/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index d3bf71a4274..62a4893692b 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -150,7 +150,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = ( ufp_perm=PermRequired.NO_WRITE, ), ProtectBinaryEntityDescription( - key="motion", + key="motion_enabled", name="Detections: Motion", icon="mdi:run-fast", ufp_value="recording_settings.enable_motion_detection", From dbe552b1a1ea29aed1add941ac05637330213da3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 1 Jul 2022 05:07:03 +1200 Subject: [PATCH 2021/3516] ESPHome use dhcp responses to update connection host of known devices (#74206) * ESPHome use dhcp responses to update connection host of known devices * Add test for dhcp * Add another test to cover when there are no changes required --- .../components/esphome/config_flow.py | 45 +++++++++++++- .../components/esphome/manifest.json | 1 + homeassistant/generated/dhcp.py | 1 + tests/components/esphome/test_config_flow.py | 60 ++++++++++++++++++- 4 files changed, 105 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 76359cda4e7..ecfa381bc69 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -16,7 +16,7 @@ from aioesphomeapi import ( ) import voluptuous as vol -from homeassistant.components import zeroconf +from homeassistant.components import dhcp, zeroconf from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback @@ -189,6 +189,49 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): return await self.async_step_discovery_confirm() + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Handle DHCP discovery.""" + node_name = discovery_info.hostname + + await self.async_set_unique_id(node_name) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) + + for entry in self._async_current_entries(): + found = False + + if CONF_HOST in entry.data and entry.data[CONF_HOST] in ( + discovery_info.ip, + f"{node_name}.local", + ): + # Is this address or IP address already configured? + found = True + elif DomainData.get(self.hass).is_entry_loaded(entry): + # Does a config entry with this name already exist? + data = DomainData.get(self.hass).get_entry_data(entry) + + # Node names are unique in the network + if data.device_info is not None: + found = data.device_info.name == node_name + + if found: + # Backwards compat, we update old entries + if not entry.unique_id: + self.hass.config_entries.async_update_entry( + entry, + data={ + **entry.data, + CONF_HOST: discovery_info.ip, + }, + unique_id=node_name, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + + break + + return self.async_abort(reason="already_configured") + @callback def _async_get_entry(self) -> FlowResult: config_data = { diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index b89671c6f90..a8a76c2b0c8 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -5,6 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": ["aioesphomeapi==10.10.0"], "zeroconf": ["_esphomelib._tcp.local."], + "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], "after_dependencies": ["zeroconf", "tag"], "iot_class": "local_push", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 91398ed00ef..e9cf6ca4c06 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -28,6 +28,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'elkm1', 'macaddress': '00409D*'}, {'domain': 'emonitor', 'hostname': 'emonitor*', 'macaddress': '0090C2*'}, {'domain': 'emonitor', 'registered_devices': True}, + {'domain': 'esphome', 'registered_devices': True}, {'domain': 'flume', 'hostname': 'flume-gw-*'}, {'domain': 'flux_led', 'registered_devices': True}, {'domain': 'flux_led', 'hostname': '[ba][lk]*', 'macaddress': '18B905*'}, diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index f7da5d66bd5..1d2cff051ae 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -12,7 +12,7 @@ from aioesphomeapi import ( import pytest from homeassistant import config_entries -from homeassistant.components import zeroconf +from homeassistant.components import dhcp, zeroconf from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN, DomainData from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.data_entry_flow import ( @@ -532,3 +532,61 @@ async def test_reauth_confirm_invalid(hass, mock_client, mock_zeroconf): assert result["step_id"] == "reauth_confirm" assert result["errors"] assert result["errors"]["base"] == "invalid_psk" + + +async def test_discovery_dhcp_updates_host(hass, mock_client): + """Test dhcp discovery updates host and aborts.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + ) + entry.add_to_hass(hass) + + mock_entry_data = MagicMock() + mock_entry_data.device_info.name = "test8266" + domain_data = DomainData.get(hass) + domain_data.set_entry_data(entry, mock_entry_data) + + service_info = dhcp.DhcpServiceInfo( + ip="192.168.43.184", + hostname="test8266", + macaddress="00:00:00:00:00:00", + ) + result = await hass.config_entries.flow.async_init( + "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + assert entry.unique_id == "test8266" + assert entry.data[CONF_HOST] == "192.168.43.184" + + +async def test_discovery_dhcp_no_changes(hass, mock_client): + """Test dhcp discovery updates host and aborts.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "192.168.43.183", CONF_PORT: 6053, CONF_PASSWORD: ""}, + ) + entry.add_to_hass(hass) + + mock_entry_data = MagicMock() + mock_entry_data.device_info.name = "test8266" + domain_data = DomainData.get(hass) + domain_data.set_entry_data(entry, mock_entry_data) + + service_info = dhcp.DhcpServiceInfo( + ip="192.168.43.183", + hostname="test8266", + macaddress="00:00:00:00:00:00", + ) + result = await hass.config_entries.flow.async_init( + "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + assert entry.unique_id == "test8266" + assert entry.data[CONF_HOST] == "192.168.43.183" From b135560274ba666e1a896008fdda5718f02bd84a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Jun 2022 19:14:56 -0500 Subject: [PATCH 2022/3516] Allow tuple subclasses to be json serialized (#74207) --- homeassistant/helpers/json.py | 2 +- tests/helpers/test_json.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index 8b91f5eb2b5..74a2f542910 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -33,7 +33,7 @@ def json_encoder_default(obj: Any) -> Any: Hand other objects to the original method. """ - if isinstance(obj, set): + if isinstance(obj, (set, tuple)): return list(obj) if isinstance(obj, float): return float(obj) diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py index cfb403ca4a9..54c488690fa 100644 --- a/tests/helpers/test_json.py +++ b/tests/helpers/test_json.py @@ -1,6 +1,7 @@ """Test Home Assistant remote methods and classes.""" import datetime import json +import time import pytest @@ -87,3 +88,11 @@ def test_json_dumps_float_subclass(): """A float subclass.""" assert json_dumps({"c": FloatSubclass(1.2)}) == '{"c":1.2}' + + +def test_json_dumps_tuple_subclass(): + """Test the json dumps a tuple subclass.""" + + tt = time.struct_time((1999, 3, 17, 32, 44, 55, 2, 76, 0)) + + assert json_dumps(tt) == "[1999,3,17,32,44,55,2,76,0]" From 1e8c897702b2f642bdd09f3caf6dc445c9a3e160 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 03:40:58 +0200 Subject: [PATCH 2023/3516] Update requests to 2.28.1 (#74210) --- homeassistant/package_constraints.txt | 6 +----- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_test.txt | 2 +- script/gen_requirements_all.py | 4 ---- 5 files changed, 4 insertions(+), 12 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 11ffd9c4c0c..9ba945c3e2b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -28,7 +28,7 @@ pyserial==3.5 python-slugify==4.0.1 pyudev==0.22.0 pyyaml==6.0 -requests==2.28.0 +requests==2.28.1 scapy==2.4.5 sqlalchemy==1.4.38 typing-extensions>=3.10.0.2,<5.0 @@ -114,7 +114,3 @@ backoff<2.0 # Breaking change in version # https://github.com/samuelcolvin/pydantic/issues/4092 pydantic!=1.9.1 - -# Pin charset-normalizer to 2.0.12 due to version conflict. -# https://github.com/home-assistant/core/pull/74104 -charset-normalizer==2.0.12 diff --git a/pyproject.toml b/pyproject.toml index 2cf2db3f240..69c5a539f3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ dependencies = [ "pip>=21.0,<22.2", "python-slugify==4.0.1", "pyyaml==6.0", - "requests==2.28.0", + "requests==2.28.1", "typing-extensions>=3.10.0.2,<5.0", "voluptuous==0.13.1", "voluptuous-serialize==2.5.0", diff --git a/requirements.txt b/requirements.txt index 7506201eae1..98b148fa923 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,7 @@ orjson==3.7.5 pip>=21.0,<22.2 python-slugify==4.0.1 pyyaml==6.0 -requests==2.28.0 +requests==2.28.1 typing-extensions>=3.10.0.2,<5.0 voluptuous==0.13.1 voluptuous-serialize==2.5.0 diff --git a/requirements_test.txt b/requirements_test.txt index 046d8bfb400..6072ce896ee 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -42,6 +42,6 @@ types-pkg-resources==0.1.3 types-python-slugify==0.1.2 types-pytz==2021.1.2 types-PyYAML==5.4.6 -types-requests==2.27.30 +types-requests==2.28.0 types-toml==0.1.5 types-ujson==0.1.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index c6b50c6bd32..a2a0eab897a 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -132,10 +132,6 @@ backoff<2.0 # Breaking change in version # https://github.com/samuelcolvin/pydantic/issues/4092 pydantic!=1.9.1 - -# Pin charset-normalizer to 2.0.12 due to version conflict. -# https://github.com/home-assistant/core/pull/74104 -charset-normalizer==2.0.12 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From f4df584f1326b5f19bbe6d120c5aa64075057b96 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 07:06:35 +0200 Subject: [PATCH 2024/3516] Fix input_number invalid state restore handling (#74213) Co-authored-by: J. Nick Koston --- .../components/input_number/__init__.py | 7 ++++-- tests/components/input_number/test_init.py | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index a6ee8dd0f7d..8e922687e59 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -1,6 +1,7 @@ """Support to set a numeric value from a slider or text box.""" from __future__ import annotations +from contextlib import suppress import logging import voluptuous as vol @@ -281,8 +282,10 @@ class InputNumber(RestoreEntity): if self._current_value is not None: return - state = await self.async_get_last_state() - value = state and float(state.state) + value: float | None = None + if state := await self.async_get_last_state(): + with suppress(ValueError): + value = float(state.state) # Check against None because value can be 0 if value is not None and self._minimum <= value <= self._maximum: diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index ca496723d99..4149627720b 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -255,6 +255,29 @@ async def test_restore_state(hass): assert float(state.state) == 10 +async def test_restore_invalid_state(hass): + """Ensure an invalid restore state is handled.""" + mock_restore_cache( + hass, (State("input_number.b1", "="), State("input_number.b2", "200")) + ) + + hass.state = CoreState.starting + + await async_setup_component( + hass, + DOMAIN, + {DOMAIN: {"b1": {"min": 2, "max": 100}, "b2": {"min": 10, "max": 100}}}, + ) + + state = hass.states.get("input_number.b1") + assert state + assert float(state.state) == 2 + + state = hass.states.get("input_number.b2") + assert state + assert float(state.state) == 10 + + async def test_initial_state_overrules_restore_state(hass): """Ensure states are restored on startup.""" mock_restore_cache( From e3b99fe62ab5e16611df7a3740f26b28ad74deb5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jun 2022 10:00:10 -0700 Subject: [PATCH 2025/3516] Treat thermostat unknown state like unavailable in alexa (#74220) --- homeassistant/components/alexa/capabilities.py | 2 ++ tests/components/alexa/test_capabilities.py | 17 ++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 818b4b794cf..25ec43b689c 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1046,6 +1046,8 @@ class AlexaThermostatController(AlexaCapability): if preset in API_THERMOSTAT_PRESETS: mode = API_THERMOSTAT_PRESETS[preset] + elif self.entity.state == STATE_UNKNOWN: + return None else: mode = API_THERMOSTAT_MODES.get(self.entity.state) if mode is None: diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 3e176b0fb8c..ea6c96bbaef 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -658,13 +658,16 @@ async def test_report_climate_state(hass): "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} ) - hass.states.async_set( - "climate.unavailable", - "unavailable", - {"friendly_name": "Climate Unavailable", "supported_features": 91}, - ) - properties = await reported_properties(hass, "climate.unavailable") - properties.assert_not_has_property("Alexa.ThermostatController", "thermostatMode") + for state in "unavailable", "unknown": + hass.states.async_set( + f"climate.{state}", + state, + {"friendly_name": f"Climate {state}", "supported_features": 91}, + ) + properties = await reported_properties(hass, f"climate.{state}") + properties.assert_not_has_property( + "Alexa.ThermostatController", "thermostatMode" + ) hass.states.async_set( "climate.unsupported", From b71205acd74588df57d4f8108ccd6cf7e1dd5f76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Thu, 30 Jun 2022 18:59:46 +0200 Subject: [PATCH 2026/3516] Make media_player.toggle turn on a standby device (#74221) --- homeassistant/components/media_player/__init__.py | 3 ++- .../components/media_player/test_async_helpers.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index dc2f3624a0e..14546a36ec8 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -52,6 +52,7 @@ from homeassistant.const import ( STATE_IDLE, STATE_OFF, STATE_PLAYING, + STATE_STANDBY, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -888,7 +889,7 @@ class MediaPlayerEntity(Entity): await self.hass.async_add_executor_job(self.toggle) return - if self.state in (STATE_OFF, STATE_IDLE): + if self.state in (STATE_OFF, STATE_IDLE, STATE_STANDBY): await self.async_turn_on() else: await self.async_turn_off() diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index 53c80bfc8de..8be263e7ee0 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -8,6 +8,7 @@ from homeassistant.const import ( STATE_ON, STATE_PAUSED, STATE_PLAYING, + STATE_STANDBY, ) @@ -79,9 +80,13 @@ class ExtendedMediaPlayer(mp.MediaPlayerEntity): """Turn off state.""" self._state = STATE_OFF + def standby(self): + """Put device in standby.""" + self._state = STATE_STANDBY + def toggle(self): """Toggle the power on the media player.""" - if self._state in [STATE_OFF, STATE_IDLE]: + if self._state in [STATE_OFF, STATE_IDLE, STATE_STANDBY]: self._state = STATE_ON else: self._state = STATE_OFF @@ -138,6 +143,10 @@ class SimpleMediaPlayer(mp.MediaPlayerEntity): """Turn off state.""" self._state = STATE_OFF + def standby(self): + """Put device in standby.""" + self._state = STATE_STANDBY + @pytest.fixture(params=[ExtendedMediaPlayer, SimpleMediaPlayer]) def player(hass, request): @@ -188,3 +197,7 @@ async def test_toggle(player): assert player.state == STATE_ON await player.async_toggle() assert player.state == STATE_OFF + player.standby() + assert player.state == STATE_STANDBY + await player.async_toggle() + assert player.state == STATE_ON From 518468a70bd2b423bab6590055affd9590210f73 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 29 Jun 2022 23:54:51 -0700 Subject: [PATCH 2027/3516] Allow legacy nest integration with no configuration.yaml (#74222) --- homeassistant/components/nest/__init__.py | 2 +- tests/components/nest/common.py | 12 +++++++++++- tests/components/nest/test_init_legacy.py | 5 ++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 0e0128136ad..b31354b598c 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -177,7 +177,7 @@ class SignalUpdateCallback: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Nest from a config entry with dispatch between old/new flows.""" config_mode = config_flow.get_config_mode(hass) - if config_mode == config_flow.ConfigMode.LEGACY: + if DATA_SDM not in entry.data or config_mode == config_flow.ConfigMode.LEGACY: return await async_setup_legacy_entry(hass, entry) if config_mode == config_flow.ConfigMode.SDM: diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index 765a954b6de..f86112ada75 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -147,7 +147,17 @@ TEST_CONFIG_LEGACY = NestTestConfig( }, }, }, - credential=None, +) +TEST_CONFIG_ENTRY_LEGACY = NestTestConfig( + config_entry_data={ + "auth_implementation": "local", + "tokens": { + "expires_at": time.time() + 86400, + "access_token": { + "token": "some-token", + }, + }, + }, ) diff --git a/tests/components/nest/test_init_legacy.py b/tests/components/nest/test_init_legacy.py index cbf1bfe2d48..fc4e6070faf 100644 --- a/tests/components/nest/test_init_legacy.py +++ b/tests/components/nest/test_init_legacy.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, PropertyMock, patch import pytest -from .common import TEST_CONFIG_LEGACY +from .common import TEST_CONFIG_ENTRY_LEGACY, TEST_CONFIG_LEGACY DOMAIN = "nest" @@ -33,6 +33,9 @@ def make_thermostat(): return device +@pytest.mark.parametrize( + "nest_test_config", [TEST_CONFIG_LEGACY, TEST_CONFIG_ENTRY_LEGACY] +) async def test_thermostat(hass, setup_base_platform): """Test simple initialization for thermostat entities.""" From a36a2d53ecf1be8e567400c6b4f42b0bf616b87b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 30 Jun 2022 09:42:15 +0200 Subject: [PATCH 2028/3516] Correct native_pressure_unit for zamg weather (#74225) --- homeassistant/components/zamg/weather.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zamg/weather.py b/homeassistant/components/zamg/weather.py index 2bf4a5b39f6..6910955fcf7 100644 --- a/homeassistant/components/zamg/weather.py +++ b/homeassistant/components/zamg/weather.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - LENGTH_MILLIMETERS, + PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) @@ -87,9 +87,7 @@ def setup_platform( class ZamgWeather(WeatherEntity): """Representation of a weather condition.""" - _attr_native_pressure_unit = ( - LENGTH_MILLIMETERS # API reports l/m², equivalent to mm - ) + _attr_native_pressure_unit = PRESSURE_HPA _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR From e1fc2ed0466173daf165cfd869734f99deaef534 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 30 Jun 2022 19:15:25 +0200 Subject: [PATCH 2029/3516] Fire event_mqtt_reloaded only after reload is completed (#74226) --- homeassistant/components/mqtt/__init__.py | 70 +++++++++++-------- .../components/mqtt/alarm_control_panel.py | 6 +- .../components/mqtt/binary_sensor.py | 6 +- homeassistant/components/mqtt/button.py | 6 +- homeassistant/components/mqtt/camera.py | 6 +- homeassistant/components/mqtt/climate.py | 6 +- homeassistant/components/mqtt/const.py | 3 +- homeassistant/components/mqtt/cover.py | 6 +- homeassistant/components/mqtt/fan.py | 4 +- homeassistant/components/mqtt/humidifier.py | 6 +- .../components/mqtt/light/__init__.py | 6 +- homeassistant/components/mqtt/lock.py | 6 +- homeassistant/components/mqtt/mixins.py | 46 +++++------- homeassistant/components/mqtt/number.py | 6 +- homeassistant/components/mqtt/scene.py | 6 +- homeassistant/components/mqtt/select.py | 6 +- homeassistant/components/mqtt/sensor.py | 6 +- homeassistant/components/mqtt/siren.py | 6 +- homeassistant/components/mqtt/switch.py | 6 +- .../components/mqtt/vacuum/__init__.py | 6 +- 20 files changed, 96 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 6fd288a86cf..a099e7b580c 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -28,14 +28,12 @@ from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.device_registry import DeviceEntry -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import ( async_integration_yaml_config, - async_setup_reload_service, + async_reload_integration_platforms, ) +from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType # Loading the config flow file will register the flow @@ -78,10 +76,10 @@ from .const import ( # noqa: F401 DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, - MQTT_RELOADED, PLATFORMS, RELOADABLE_PLATFORMS, ) +from .mixins import async_discover_yaml_entities from .models import ( # noqa: F401 MqttCommandTemplate, MqttValueTemplate, @@ -241,7 +239,9 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) - await _async_setup_discovery(hass, mqtt_client.conf, entry) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry( # noqa: C901 + hass: HomeAssistant, entry: ConfigEntry +) -> bool: """Load a config entry.""" # Merge basic configuration, and add missing defaults for basic options _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) @@ -378,16 +378,32 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[CONFIG_ENTRY_IS_SETUP] = set() - # Setup reload service. Once support for legacy config is removed in 2022.9, we - # should no longer call async_setup_reload_service but instead implement a custom - # service - await async_setup_reload_service(hass, DOMAIN, RELOADABLE_PLATFORMS) + async def async_setup_reload_service() -> None: + """Create the reload service for the MQTT domain.""" + if hass.services.has_service(DOMAIN, SERVICE_RELOAD): + return - async def _async_reload_platforms(_: Event | None) -> None: - """Discover entities for a platform.""" - config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {} - hass.data[DATA_MQTT_UPDATED_CONFIG] = config_yaml.get(DOMAIN, {}) - async_dispatcher_send(hass, MQTT_RELOADED) + async def _reload_config(call: ServiceCall) -> None: + """Reload the platforms.""" + # Reload the legacy yaml platform + await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS) + + # Reload the modern yaml platforms + config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {} + hass.data[DATA_MQTT_UPDATED_CONFIG] = config_yaml.get(DOMAIN, {}) + await asyncio.gather( + *( + [ + async_discover_yaml_entities(hass, component) + for component in RELOADABLE_PLATFORMS + ] + ) + ) + + # Fire event + hass.bus.async_fire(f"event_{DOMAIN}_reloaded", context=call.context) + + async_register_admin_service(hass, DOMAIN, SERVICE_RELOAD, _reload_config) async def async_forward_entry_setup_and_setup_discovery(config_entry): """Forward the config entry setup to the platforms and set up discovery.""" @@ -411,21 +427,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if conf.get(CONF_DISCOVERY): await _async_setup_discovery(hass, conf, entry) # Setup reload service after all platforms have loaded - entry.async_on_unload( - hass.bus.async_listen("event_mqtt_reloaded", _async_reload_platforms) - ) + await async_setup_reload_service() + + if DATA_MQTT_RELOAD_NEEDED in hass.data: + hass.data.pop(DATA_MQTT_RELOAD_NEEDED) + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=False, + ) hass.async_create_task(async_forward_entry_setup_and_setup_discovery(entry)) - if DATA_MQTT_RELOAD_NEEDED in hass.data: - hass.data.pop(DATA_MQTT_RELOAD_NEEDED) - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - {}, - blocking=False, - ) - return True diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 8e5ee54d688..b6f2f8f236e 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -44,8 +44,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -147,9 +147,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT alarm control panel through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, alarm.DOMAIN) - ) + await async_discover_yaml_entities(hass, alarm.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 39fd87c8b02..9e0a049b15e 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -41,8 +41,8 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -102,9 +102,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT binary sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, binary_sensor.DOMAIN) - ) + await async_discover_yaml_entities(hass, binary_sensor.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index 0374727bf7d..b75fbe4b97f 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -25,8 +25,8 @@ from .const import ( from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -82,9 +82,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT button through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, button.DOMAIN) - ) + await async_discover_yaml_entities(hass, button.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 5c8d3bc48b2..69af7992229 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -22,8 +22,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -80,9 +80,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT camera through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, camera.DOMAIN) - ) + await async_discover_yaml_entities(hass, camera.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index a26e9cba8df..6b09891483c 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -50,8 +50,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -391,9 +391,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT climate device through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, climate.DOMAIN) - ) + await async_discover_yaml_entities(hass, climate.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 67a9208faba..6ac77021337 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -64,7 +64,6 @@ DOMAIN = "mqtt" MQTT_CONNECTED = "mqtt_connected" MQTT_DISCONNECTED = "mqtt_disconnected" -MQTT_RELOADED = "mqtt_reloaded" PAYLOAD_EMPTY_JSON = "{}" PAYLOAD_NONE = "None" @@ -105,8 +104,8 @@ RELOADABLE_PLATFORMS = [ Platform.LIGHT, Platform.LOCK, Platform.NUMBER, - Platform.SELECT, Platform.SCENE, + Platform.SELECT, Platform.SENSOR, Platform.SIREN, Platform.SWITCH, diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 54ed4f2b0a0..14746329250 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -46,8 +46,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -242,9 +242,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT cover through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, cover.DOMAIN) - ) + await async_discover_yaml_entities(hass, cover.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 721fa93f244..15e4a80f3e7 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -50,8 +50,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -232,7 +232,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload(await async_setup_platform_discovery(hass, fan.DOMAIN)) + await async_discover_yaml_entities(hass, fan.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index d2856767cf0..5f09fc0d513 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -45,8 +45,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -187,9 +187,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT humidifier through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, humidifier.DOMAIN) - ) + await async_discover_yaml_entities(hass, humidifier.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index d4914cb9506..c7f3395ba4e 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -13,8 +13,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ..mixins import ( + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -111,9 +111,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT lights configured under the light platform key (deprecated).""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, light.DOMAIN) - ) + await async_discover_yaml_entities(hass, light.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 1d6a40c2331..b4788f1db0c 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -28,8 +28,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -103,9 +103,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT lock through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, lock.DOMAIN) - ) + await async_discover_yaml_entities(hass, lock.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 10fe6cb6cc5..8e59d09dfce 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -26,7 +26,7 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, ) -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -69,7 +69,6 @@ from .const import ( DOMAIN, MQTT_CONNECTED, MQTT_DISCONNECTED, - MQTT_RELOADED, ) from .debug_info import log_message, log_messages from .discovery import ( @@ -261,34 +260,27 @@ class SetupEntity(Protocol): """Define setup_entities type.""" -async def async_setup_platform_discovery( +async def async_discover_yaml_entities( hass: HomeAssistant, platform_domain: str -) -> CALLBACK_TYPE: - """Set up platform discovery for manual config.""" - - async def _async_discover_entities() -> None: - """Discover entities for a platform.""" - if DATA_MQTT_UPDATED_CONFIG in hass.data: - # The platform has been reloaded - config_yaml = hass.data[DATA_MQTT_UPDATED_CONFIG] - else: - config_yaml = hass.data.get(DATA_MQTT_CONFIG, {}) - if not config_yaml: - return - if platform_domain not in config_yaml: - return - await asyncio.gather( - *( - discovery.async_load_platform(hass, platform_domain, DOMAIN, config, {}) - for config in await async_get_platform_config_from_yaml( - hass, platform_domain, config_yaml - ) +) -> None: + """Discover entities for a platform.""" + if DATA_MQTT_UPDATED_CONFIG in hass.data: + # The platform has been reloaded + config_yaml = hass.data[DATA_MQTT_UPDATED_CONFIG] + else: + config_yaml = hass.data.get(DATA_MQTT_CONFIG, {}) + if not config_yaml: + return + if platform_domain not in config_yaml: + return + await asyncio.gather( + *( + discovery.async_load_platform(hass, platform_domain, DOMAIN, config, {}) + for config in await async_get_platform_config_from_yaml( + hass, platform_domain, config_yaml ) ) - - unsub = async_dispatcher_connect(hass, MQTT_RELOADED, _async_discover_entities) - await _async_discover_entities() - return unsub + ) async def async_get_platform_config_from_yaml( diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 660ffe987f0..dc27a740720 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -41,8 +41,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -135,9 +135,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT number through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, number.DOMAIN) - ) + await async_discover_yaml_entities(hass, number.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index cc911cc3431..8b654f7cca0 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -22,8 +22,8 @@ from .mixins import ( CONF_OBJECT_ID, MQTT_AVAILABILITY_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -79,9 +79,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT scene through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, scene.DOMAIN) - ) + await async_discover_yaml_entities(hass, scene.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 0d9f1411fd1..4c302446b19 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -30,8 +30,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -94,9 +94,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT select through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, select.DOMAIN) - ) + await async_discover_yaml_entities(hass, select.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 672e22f632f..6948e173039 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -41,8 +41,8 @@ from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttAvailability, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -147,9 +147,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT sensor through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, sensor.DOMAIN) - ) + await async_discover_yaml_entities(hass, sensor.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index a6cff4cf91d..dfb89d2ee79 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -51,8 +51,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -143,9 +143,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT siren through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, siren.DOMAIN) - ) + await async_discover_yaml_entities(hass, siren.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index dadd5f86f20..b04f2433659 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -37,8 +37,8 @@ from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, warn_for_legacy_schema, ) @@ -97,9 +97,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT switch through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, switch.DOMAIN) - ) + await async_discover_yaml_entities(hass, switch.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 694e9530939..c49b8cfa012 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -12,8 +12,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ..mixins import ( + async_discover_yaml_entities, async_setup_entry_helper, - async_setup_platform_discovery, async_setup_platform_helper, ) from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE @@ -91,9 +91,7 @@ async def async_setup_entry( ) -> None: """Set up MQTT vacuum through configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml - config_entry.async_on_unload( - await async_setup_platform_discovery(hass, vacuum.DOMAIN) - ) + await async_discover_yaml_entities(hass, vacuum.DOMAIN) # setup for discovery setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry From 15149f4aa1c9667deb796e0575bf7fe41f4a3f23 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 30 Jun 2022 13:03:39 -0400 Subject: [PATCH 2030/3516] Fix ZHA events for logbook (#74245) --- homeassistant/components/zha/logbook.py | 10 +++++++--- tests/components/zha/test_logbook.py | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/logbook.py b/homeassistant/components/zha/logbook.py index e2d238ddbe8..8140a5244f1 100644 --- a/homeassistant/components/zha/logbook.py +++ b/homeassistant/components/zha/logbook.py @@ -62,14 +62,18 @@ def async_describe_events( break if event_type is None: - event_type = event_data[ATTR_COMMAND] + event_type = event_data.get(ATTR_COMMAND, ZHA_EVENT) if event_subtype is not None and event_subtype != event_type: event_type = f"{event_type} - {event_subtype}" - event_type = event_type.replace("_", " ").title() + if event_type is not None: + event_type = event_type.replace("_", " ").title() + if "event" in event_type.lower(): + message = f"{event_type} was fired" + else: + message = f"{event_type} event was fired" - message = f"{event_type} event was fired" if event_data["params"]: message = f"{message} with parameters: {event_data['params']}" diff --git a/tests/components/zha/test_logbook.py b/tests/components/zha/test_logbook.py index 33b758fd0a7..6c28284b1e6 100644 --- a/tests/components/zha/test_logbook.py +++ b/tests/components/zha/test_logbook.py @@ -172,6 +172,19 @@ async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): }, }, ), + MockRow( + ZHA_EVENT, + { + CONF_DEVICE_ID: reg_device.id, + "device_ieee": str(ieee_address), + CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + "params": { + "test": "test", + }, + }, + ), ], ) @@ -182,6 +195,12 @@ async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): == "Shake event was fired with parameters: {'test': 'test'}" ) + assert events[1]["name"] == "FakeManufacturer FakeModel" + assert events[1]["domain"] == "zha" + assert ( + events[1]["message"] == "Zha Event was fired with parameters: {'test': 'test'}" + ) + async def test_zha_logbook_event_device_no_device(hass, mock_devices): """Test zha logbook events without device and without triggers.""" From 00468db5afbfdf9b41b1c57a750f9302ed82603b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 19:04:59 +0200 Subject: [PATCH 2031/3516] Update numpy to 1.23.0 (#74250) --- homeassistant/components/compensation/manifest.json | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/gen_requirements_all.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index eb3135954b0..509d5740b22 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,7 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.22.4"], + "requirements": ["numpy==1.23.0"], "codeowners": ["@Petro31"], "iot_class": "calculated" } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 8b0dacd3575..7485ff9d608 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.22.4", "pyiqvia==2022.04.0"], + "requirements": ["numpy==1.23.0", "pyiqvia==2022.04.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyiqvia"] diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 8cd1604f106..0272feb0f9e 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.22.4", "opencv-python-headless==4.6.0.66"], + "requirements": ["numpy==1.23.0", "opencv-python-headless==4.6.0.66"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 4168d820fb6..42d0eae1ecd 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.5.0", "tf-models-official==2.5.0", "pycocotools==2.0.1", - "numpy==1.22.4", + "numpy==1.23.0", "pillow==9.1.1" ], "codeowners": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 578aea3bbc6..b579cc036bb 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.22.4"], + "requirements": ["numpy==1.23.0"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9ba945c3e2b..6b7b689b93d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -88,7 +88,7 @@ httpcore==0.15.0 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy>=1.22.0 +numpy==1.23.0 # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6f677d1d092..2ebabd89a5a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1126,7 +1126,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.22.4 +numpy==1.23.0 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d0c9bc54ddd..a19338e5715 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -779,7 +779,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.22.4 +numpy==1.23.0 # homeassistant.components.google oauth2client==4.1.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index a2a0eab897a..11e88976e83 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -106,7 +106,7 @@ httpcore==0.15.0 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy>=1.22.0 +numpy==1.23.0 # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 From d47e1d28dea6f91b5f50470b015263b189b57a55 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Jun 2022 12:39:36 -0500 Subject: [PATCH 2032/3516] Filter out CONF_SCAN_INTERVAL from scrape import (#74254) --- homeassistant/components/scrape/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 1c447439820..a73dbc17c1c 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -24,6 +24,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_RESOURCE, + CONF_SCAN_INTERVAL, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_VALUE_TEMPLATE, @@ -90,7 +91,7 @@ async def async_setup_platform( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, - data=config, + data={k: v for k, v in config.items() if k != CONF_SCAN_INTERVAL}, ) ) From 249af3a78dc7a6201333df6b4b3626bf39ff2189 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 30 Jun 2022 19:47:29 +0200 Subject: [PATCH 2033/3516] Met.no use native_* (#74259) --- homeassistant/components/met/const.py | 16 +++---- homeassistant/components/met/weather.py | 57 ++++++------------------- 2 files changed, 20 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/met/const.py b/homeassistant/components/met/const.py index 93f9e3414dd..5b2a756847e 100644 --- a/homeassistant/components/met/const.py +++ b/homeassistant/components/met/const.py @@ -11,13 +11,13 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY_RAINY, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, @@ -173,13 +173,13 @@ CONDITIONS_MAP = { FORECAST_MAP = { ATTR_FORECAST_CONDITION: "condition", - ATTR_FORECAST_PRECIPITATION: "precipitation", + ATTR_FORECAST_NATIVE_PRECIPITATION: "precipitation", ATTR_FORECAST_PRECIPITATION_PROBABILITY: "precipitation_probability", - ATTR_FORECAST_TEMP: "temperature", - ATTR_FORECAST_TEMP_LOW: "templow", + ATTR_FORECAST_NATIVE_TEMP: "temperature", + ATTR_FORECAST_NATIVE_TEMP_LOW: "templow", ATTR_FORECAST_TIME: "datetime", ATTR_FORECAST_WIND_BEARING: "wind_bearing", - ATTR_FORECAST_WIND_SPEED: "wind_speed", + ATTR_FORECAST_NATIVE_WIND_SPEED: "wind_speed", } ATTR_MAP = { diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 251d99ad295..0ff0a60bfa1 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -6,7 +6,6 @@ from typing import Any from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, @@ -21,12 +20,9 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - LENGTH_INCHES, LENGTH_MILLIMETERS, PRESSURE_HPA, - PRESSURE_INHG, SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -34,19 +30,9 @@ from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.util.distance import convert as convert_distance -from homeassistant.util.pressure import convert as convert_pressure -from homeassistant.util.speed import convert as convert_speed from . import MetDataUpdateCoordinator -from .const import ( - ATTR_FORECAST_PRECIPITATION, - ATTR_MAP, - CONDITIONS_MAP, - CONF_TRACK_HOME, - DOMAIN, - FORECAST_MAP, -) +from .const import ATTR_MAP, CONDITIONS_MAP, CONF_TRACK_HOME, DOMAIN, FORECAST_MAP ATTRIBUTION = ( "Weather forecast from met.no, delivered by the Norwegian " @@ -85,6 +71,11 @@ def format_condition(condition: str) -> str: class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): """Implementation of a Met.no weather condition.""" + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__( self, coordinator: MetDataUpdateCoordinator, @@ -144,27 +135,18 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): return format_condition(condition) @property - def temperature(self) -> float | None: + def native_temperature(self) -> float | None: """Return the temperature.""" return self.coordinator.data.current_weather_data.get( ATTR_MAP[ATTR_WEATHER_TEMPERATURE] ) @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def pressure(self) -> float | None: + def native_pressure(self) -> float | None: """Return the pressure.""" - pressure_hpa = self.coordinator.data.current_weather_data.get( + return self.coordinator.data.current_weather_data.get( ATTR_MAP[ATTR_WEATHER_PRESSURE] ) - if self._is_metric or pressure_hpa is None: - return pressure_hpa - - return round(convert_pressure(pressure_hpa, PRESSURE_HPA, PRESSURE_INHG), 2) @property def humidity(self) -> float | None: @@ -174,18 +156,11 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): ) @property - def wind_speed(self) -> float | None: + def native_wind_speed(self) -> float | None: """Return the wind speed.""" - speed_km_h = self.coordinator.data.current_weather_data.get( + return self.coordinator.data.current_weather_data.get( ATTR_MAP[ATTR_WEATHER_WIND_SPEED] ) - if self._is_metric or speed_km_h is None: - return speed_km_h - - speed_mi_h = convert_speed( - speed_km_h, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR - ) - return int(round(speed_mi_h)) @property def wind_bearing(self) -> float | str | None: @@ -206,7 +181,7 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): met_forecast = self.coordinator.data.hourly_forecast else: met_forecast = self.coordinator.data.daily_forecast - required_keys = {ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME} + required_keys = {"temperature", ATTR_FORECAST_TIME} ha_forecast: list[Forecast] = [] for met_item in met_forecast: if not set(met_item).issuperset(required_keys): @@ -216,14 +191,6 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): for k, v in FORECAST_MAP.items() if met_item.get(v) is not None } - if not self._is_metric and ATTR_FORECAST_PRECIPITATION in ha_item: - if ha_item[ATTR_FORECAST_PRECIPITATION] is not None: - precip_inches = convert_distance( - ha_item[ATTR_FORECAST_PRECIPITATION], - LENGTH_MILLIMETERS, - LENGTH_INCHES, - ) - ha_item[ATTR_FORECAST_PRECIPITATION] = round(precip_inches, 2) if ha_item.get(ATTR_FORECAST_CONDITION): ha_item[ATTR_FORECAST_CONDITION] = format_condition( ha_item[ATTR_FORECAST_CONDITION] From 6f63cd731bc0a761f4257e60a0dd1e0400c9eda0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Jun 2022 12:05:29 -0500 Subject: [PATCH 2034/3516] Add debug logging to esphome state updates (#74260) --- homeassistant/components/esphome/entry_data.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 7980d1a6a17..d4bcc67db4a 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable from dataclasses import dataclass, field +import logging from typing import Any, cast from aioesphomeapi import ( @@ -36,6 +37,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store SAVE_DELAY = 120 +_LOGGER = logging.getLogger(__name__) # Mapping from ESPHome info type to HA platform INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { @@ -128,6 +130,12 @@ class RuntimeEntryData: component_key = self.key_to_component[state.key] self.state[component_key][state.key] = state signal = f"esphome_{self.entry_id}_update_{component_key}_{state.key}" + _LOGGER.debug( + "Dispatching update for component %s with state key %s: %s", + component_key, + state.key, + state, + ) async_dispatcher_send(hass, signal) @callback From 3db1552f26c1e7d7ebfd66be02e14c69546d412f Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Thu, 30 Jun 2022 12:10:05 -0500 Subject: [PATCH 2035/3516] Fix Life360 unload (#74263) * Fix life360 async_unload_entry * Update tracked_members when unloading config entry --- homeassistant/components/life360/__init__.py | 14 ++++++++++---- homeassistant/components/life360/device_tracker.py | 10 +++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/life360/__init__.py b/homeassistant/components/life360/__init__.py index 66c9416a1c1..4527f6ac298 100644 --- a/homeassistant/components/life360/__init__.py +++ b/homeassistant/components/life360/__init__.py @@ -124,11 +124,11 @@ class IntegData: """Integration data.""" cfg_options: dict[str, Any] | None = None - # ConfigEntry.unique_id: Life360DataUpdateCoordinator + # ConfigEntry.entry_id: Life360DataUpdateCoordinator coordinators: dict[str, Life360DataUpdateCoordinator] = field( init=False, default_factory=dict ) - # member_id: ConfigEntry.unique_id + # member_id: ConfigEntry.entry_id tracked_members: dict[str, str] = field(init=False, default_factory=dict) logged_circles: list[str] = field(init=False, default_factory=list) logged_places: list[str] = field(init=False, default_factory=list) @@ -171,7 +171,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload config entry.""" - del hass.data[DOMAIN].coordinators[entry.entry_id] # Unload components for our platforms. - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + del hass.data[DOMAIN].coordinators[entry.entry_id] + # Remove any members that were tracked by this entry. + for member_id, entry_id in hass.data[DOMAIN].tracked_members.copy().items(): + if entry_id == entry.entry_id: + del hass.data[DOMAIN].tracked_members[member_id] + + return unload_ok diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index a38181a6830..5a18422487e 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -78,13 +78,13 @@ async def async_setup_entry( new_entities = [] for member_id, member in coordinator.data.members.items(): - tracked_by_account = tracked_members.get(member_id) - if new_member := not tracked_by_account: - tracked_members[member_id] = entry.unique_id - LOGGER.debug("Member: %s", member.name) + tracked_by_entry = tracked_members.get(member_id) + if new_member := not tracked_by_entry: + tracked_members[member_id] = entry.entry_id + LOGGER.debug("Member: %s (%s)", member.name, entry.unique_id) if ( new_member - or tracked_by_account == entry.unique_id + or tracked_by_entry == entry.entry_id and not new_members_only ): new_entities.append(Life360DeviceTracker(coordinator, member_id)) From 4755f0054936cbb948e2e2065c56c6f4308c69fd Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 30 Jun 2022 13:01:23 -0500 Subject: [PATCH 2036/3516] Bump frontend to 20220630.0 (#74266) --- 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 23f056ba0da..27ff0a73f20 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220629.0"], + "requirements": ["home-assistant-frontend==20220630.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6b7b689b93d..7ee5a9fe8d3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220629.0 +home-assistant-frontend==20220630.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 2ebabd89a5a..84fb3e6894d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220629.0 +home-assistant-frontend==20220630.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a19338e5715..b15b9446de3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -595,7 +595,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220629.0 +home-assistant-frontend==20220630.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 762fe17f48cd01a324192696f4ab2127aa15d30f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 30 Jun 2022 11:02:38 -0700 Subject: [PATCH 2037/3516] Bumped version to 2022.7.0b1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index df160e1cc6b..60a3d6817b5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 69c5a539f3f..2b1e14a40f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.0b0" +version = "2022.7.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From e3d250a6234d0f19673d07d4e274a80d0b1d7e12 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 30 Jun 2022 20:12:58 +0200 Subject: [PATCH 2038/3516] Do not pin numpy in wheels (#74268) --- .github/workflows/wheels.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 95f1d8e437e..2ffc2f1f721 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -157,6 +157,9 @@ jobs: echo "cmake==3.22.2" ) >> homeassistant/package_constraints.txt + # Do not pin numpy in wheels building + sed -i "/numpy/d" homeassistant/package_constraints.txt + - name: Build wheels uses: home-assistant/wheels@2022.06.7 with: From 3899c9e6d34a8254b03bc11f1f69f1a0c5478739 Mon Sep 17 00:00:00 2001 From: "R. de Veen" Date: Thu, 30 Jun 2022 20:27:31 +0200 Subject: [PATCH 2039/3516] Links to Esphomelib.com is changed to esphome.io (#72680) --- homeassistant/components/esphome/config_flow.py | 6 +++++- homeassistant/components/esphome/strings.json | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index ecfa381bc69..9fd12634e43 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -25,6 +25,7 @@ from homeassistant.data_entry_flow import FlowResult from . import CONF_NOISE_PSK, DOMAIN, DomainData ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key" +ESPHOME_URL = "https://esphome.io/" class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): @@ -55,7 +56,10 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): errors["base"] = error return self.async_show_form( - step_id="user", data_schema=vol.Schema(fields), errors=errors + step_id="user", + data_schema=vol.Schema(fields), + errors=errors, + description_placeholders={"esphome_url": ESPHOME_URL}, ) async def async_step_user( diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index 62814f2723b..b1b1ba94e3f 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -6,7 +6,7 @@ "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { - "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", + "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address", "connection_error": "Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_psk": "The transport encryption key is invalid. Please ensure it matches what you have in your configuration" @@ -17,7 +17,7 @@ "host": "[%key:common::config_flow::data::host%]", "port": "[%key:common::config_flow::data::port%]" }, - "description": "Please enter connection settings of your [ESPHome](https://esphomelib.com/) node." + "description": "Please enter connection settings of your [ESPHome]({esphome_url}) node." }, "authenticate": { "data": { From 1bfd8b1a7610963894c58e0291df257656d467b3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 Jun 2022 20:49:41 +0200 Subject: [PATCH 2040/3516] Add enforce_type_hints to vscode tasks (#74227) --- .github/workflows/ci.yaml | 4 ++-- .pre-commit-config.yaml | 2 +- pylint/plugins/hass_enforce_type_hints.py | 2 +- tests/pylint/test_enforce_type_hints.py | 6 ++++++ 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 54a1997b28e..1d7a5c95986 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -648,14 +648,14 @@ jobs: run: | . venv/bin/activate python --version - pylint homeassistant + pylint --ignore-missing-annotations=y homeassistant - name: Run pylint (partially) if: needs.changes.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate python --version - pylint homeassistant/components/${{ needs.changes.outputs.integrations_glob }} + pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.changes.outputs.integrations_glob }} mypy: name: Check mypy diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d6400e113c5..5da3a6b21ff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -96,7 +96,7 @@ repos: files: ^(homeassistant|pylint)/.+\.py$ - id: pylint name: pylint - entry: script/run-in-env.sh pylint -j 0 + entry: script/run-in-env.sh pylint -j 0 --ignore-missing-annotations=y language: script types: [python] files: ^homeassistant/.+\.py$ diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 3ac072373b8..2696c9965e4 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1268,7 +1268,7 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] ( "ignore-missing-annotations", { - "default": True, + "default": False, "type": "yn", "metavar": "", "help": "Set to ``no`` if you wish to check functions that do not " diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 8b4b8d4d058..df10c7514c6 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -120,6 +120,9 @@ def test_ignore_no_annotations( hass_enforce_type_hints: ModuleType, type_hint_checker: BaseChecker, code: str ) -> None: """Ensure that _is_valid_type is not run if there are no annotations.""" + # Set ignore option + type_hint_checker.config.ignore_missing_annotations = True + func_node = astroid.extract_node( code, "homeassistant.components.pylint_test", @@ -539,6 +542,9 @@ def test_ignore_invalid_entity_properties( linter: UnittestLinter, type_hint_checker: BaseChecker ) -> None: """Check invalid entity properties are ignored by default.""" + # Set ignore option + type_hint_checker.config.ignore_missing_annotations = True + class_node = astroid.extract_node( """ class LockEntity(): From 0caeeb56c5efd5b07d110a44c297cbda85a43c60 Mon Sep 17 00:00:00 2001 From: "Erik J. Olson" Date: Thu, 30 Jun 2022 14:00:29 -0500 Subject: [PATCH 2041/3516] Add Matrix.io HTML message format support (#69951) --- homeassistant/components/matrix/__init__.py | 18 ++++++++++++++---- homeassistant/components/matrix/const.py | 3 +++ homeassistant/components/matrix/services.yaml | 4 ++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index 47646a586ca..17df46a0849 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -22,7 +22,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType from homeassistant.util.json import load_json, save_json -from .const import DOMAIN, SERVICE_SEND_MESSAGE +from .const import DOMAIN, FORMAT_HTML, FORMAT_TEXT, SERVICE_SEND_MESSAGE _LOGGER = logging.getLogger(__name__) @@ -36,8 +36,12 @@ CONF_EXPRESSION = "expression" DEFAULT_CONTENT_TYPE = "application/octet-stream" +MESSAGE_FORMATS = [FORMAT_HTML, FORMAT_TEXT] +DEFAULT_MESSAGE_FORMAT = FORMAT_TEXT + EVENT_MATRIX_COMMAND = "matrix_command" +ATTR_FORMAT = "format" # optional message format ATTR_IMAGES = "images" # optional images COMMAND_SCHEMA = vol.All( @@ -74,7 +78,10 @@ CONFIG_SCHEMA = vol.Schema( SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema( { vol.Required(ATTR_MESSAGE): cv.string, - vol.Optional(ATTR_DATA): { + vol.Optional(ATTR_DATA, default={}): { + vol.Optional(ATTR_FORMAT, default=DEFAULT_MESSAGE_FORMAT): vol.In( + MESSAGE_FORMATS + ), vol.Optional(ATTR_IMAGES): vol.All(cv.ensure_list, [cv.string]), }, vol.Required(ATTR_TARGET): vol.All(cv.ensure_list, [cv.string]), @@ -377,7 +384,10 @@ class MatrixBot: try: room = self._join_or_get_room(target_room) if message is not None: - _LOGGER.debug(room.send_text(message)) + if data.get(ATTR_FORMAT) == FORMAT_HTML: + _LOGGER.debug(room.send_html(message)) + else: + _LOGGER.debug(room.send_text(message)) except MatrixRequestError as ex: _LOGGER.error( "Unable to deliver message to room '%s': %d, %s", @@ -385,7 +395,7 @@ class MatrixBot: ex.code, ex.content, ) - if data is not None: + if ATTR_IMAGES in data: for img in data.get(ATTR_IMAGES, []): self._send_image(img, target_rooms) diff --git a/homeassistant/components/matrix/const.py b/homeassistant/components/matrix/const.py index 6b082bde121..b7e0c22e2ac 100644 --- a/homeassistant/components/matrix/const.py +++ b/homeassistant/components/matrix/const.py @@ -2,3 +2,6 @@ DOMAIN = "matrix" SERVICE_SEND_MESSAGE = "send_message" + +FORMAT_HTML = "html" +FORMAT_TEXT = "text" diff --git a/homeassistant/components/matrix/services.yaml b/homeassistant/components/matrix/services.yaml index c58a27c3370..9b5171d1483 100644 --- a/homeassistant/components/matrix/services.yaml +++ b/homeassistant/components/matrix/services.yaml @@ -18,7 +18,7 @@ send_message: text: data: name: Data - description: Extended information of notification. Supports list of images. Optional. - example: "{'images': ['/tmp/test.jpg']}" + description: Extended information of notification. Supports list of images. Supports message format. Optional. + example: "{'images': ['/tmp/test.jpg'], 'format': 'text'}" selector: object: From 768b98ae77329e98b2993c2e4fa7435ad6f1b49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 30 Jun 2022 21:09:08 +0200 Subject: [PATCH 2042/3516] Add QNAP QSW Update platform (#71019) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * qnap_qsw: add Update platform Signed-off-by: Álvaro Fernández Rojas * qnap_qsw: update: allow init if firmware coordinator fails QSW API can return an error if update servers aren't reachable and this prevents the integration from loading. Signed-off-by: Álvaro Fernández Rojas * tests: qnap_qsw: achieve 100% coverage Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/qnap_qsw/__init__.py | 23 +++-- .../components/qnap_qsw/binary_sensor.py | 8 +- homeassistant/components/qnap_qsw/button.py | 12 +-- homeassistant/components/qnap_qsw/const.py | 3 + .../components/qnap_qsw/coordinator.py | 35 +++++++- .../components/qnap_qsw/diagnostics.py | 11 ++- homeassistant/components/qnap_qsw/entity.py | 43 ++++++++- homeassistant/components/qnap_qsw/sensor.py | 8 +- homeassistant/components/qnap_qsw/update.py | 89 +++++++++++++++++++ tests/components/qnap_qsw/test_coordinator.py | 35 ++++++-- tests/components/qnap_qsw/test_init.py | 3 + tests/components/qnap_qsw/test_update.py | 26 ++++++ tests/components/qnap_qsw/util.py | 27 ++++++ 13 files changed, 286 insertions(+), 37 deletions(-) create mode 100644 homeassistant/components/qnap_qsw/update.py create mode 100644 tests/components/qnap_qsw/test_update.py diff --git a/homeassistant/components/qnap_qsw/__init__.py b/homeassistant/components/qnap_qsw/__init__.py index 26ed8066686..4d2ee76cd2b 100644 --- a/homeassistant/components/qnap_qsw/__init__.py +++ b/homeassistant/components/qnap_qsw/__init__.py @@ -8,10 +8,15 @@ from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client -from .const import DOMAIN -from .coordinator import QswUpdateCoordinator +from .const import DOMAIN, QSW_COORD_DATA, QSW_COORD_FW +from .coordinator import QswDataCoordinator, QswFirmwareCoordinator -PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR] +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.SENSOR, + Platform.UPDATE, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -24,10 +29,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: qsw = QnapQswApi(aiohttp_client.async_get_clientsession(hass), options) - coordinator = QswUpdateCoordinator(hass, qsw) - await coordinator.async_config_entry_first_refresh() + coord_data = QswDataCoordinator(hass, qsw) + await coord_data.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + coord_fw = QswFirmwareCoordinator(hass, qsw) + await coord_fw.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + QSW_COORD_DATA: coord_data, + QSW_COORD_FW: coord_fw, + } hass.config_entries.async_setup_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/qnap_qsw/binary_sensor.py b/homeassistant/components/qnap_qsw/binary_sensor.py index 467a3314070..71af89778b8 100644 --- a/homeassistant/components/qnap_qsw/binary_sensor.py +++ b/homeassistant/components/qnap_qsw/binary_sensor.py @@ -16,8 +16,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_MESSAGE, DOMAIN -from .coordinator import QswUpdateCoordinator +from .const import ATTR_MESSAGE, DOMAIN, QSW_COORD_DATA +from .coordinator import QswDataCoordinator from .entity import QswEntityDescription, QswSensorEntity @@ -48,7 +48,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add QNAP QSW binary sensors from a config_entry.""" - coordinator: QswUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: QswDataCoordinator = hass.data[DOMAIN][entry.entry_id][QSW_COORD_DATA] async_add_entities( QswBinarySensor(coordinator, description, entry) for description in BINARY_SENSOR_TYPES @@ -66,7 +66,7 @@ class QswBinarySensor(QswSensorEntity, BinarySensorEntity): def __init__( self, - coordinator: QswUpdateCoordinator, + coordinator: QswDataCoordinator, description: QswBinarySensorEntityDescription, entry: ConfigEntry, ) -> None: diff --git a/homeassistant/components/qnap_qsw/button.py b/homeassistant/components/qnap_qsw/button.py index 1c13310fe05..9aad411b992 100644 --- a/homeassistant/components/qnap_qsw/button.py +++ b/homeassistant/components/qnap_qsw/button.py @@ -17,9 +17,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, QSW_REBOOT -from .coordinator import QswUpdateCoordinator -from .entity import QswEntity +from .const import DOMAIN, QSW_COORD_DATA, QSW_REBOOT +from .coordinator import QswDataCoordinator +from .entity import QswDataEntity @dataclass @@ -49,20 +49,20 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add QNAP QSW buttons from a config_entry.""" - coordinator: QswUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: QswDataCoordinator = hass.data[DOMAIN][entry.entry_id][QSW_COORD_DATA] async_add_entities( QswButton(coordinator, description, entry) for description in BUTTON_TYPES ) -class QswButton(QswEntity, ButtonEntity): +class QswButton(QswDataEntity, ButtonEntity): """Define a QNAP QSW button.""" entity_description: QswButtonDescription def __init__( self, - coordinator: QswUpdateCoordinator, + coordinator: QswDataCoordinator, description: QswButtonDescription, entry: ConfigEntry, ) -> None: diff --git a/homeassistant/components/qnap_qsw/const.py b/homeassistant/components/qnap_qsw/const.py index e583c0250f4..4b5fa9a4a2c 100644 --- a/homeassistant/components/qnap_qsw/const.py +++ b/homeassistant/components/qnap_qsw/const.py @@ -10,5 +10,8 @@ MANUFACTURER: Final = "QNAP" RPM: Final = "rpm" +QSW_COORD_DATA: Final = "coordinator-data" +QSW_COORD_FW: Final = "coordinator-firmware" QSW_REBOOT = "reboot" QSW_TIMEOUT_SEC: Final = 25 +QSW_UPDATE: Final = "update" diff --git a/homeassistant/components/qnap_qsw/coordinator.py b/homeassistant/components/qnap_qsw/coordinator.py index c018c1f3848..73af2f74cc5 100644 --- a/homeassistant/components/qnap_qsw/coordinator.py +++ b/homeassistant/components/qnap_qsw/coordinator.py @@ -5,7 +5,7 @@ from datetime import timedelta import logging from typing import Any -from aioqsw.exceptions import QswError +from aioqsw.exceptions import APIError, QswError from aioqsw.localapi import QnapQswApi import async_timeout @@ -14,12 +14,13 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DOMAIN, QSW_TIMEOUT_SEC -SCAN_INTERVAL = timedelta(seconds=60) +DATA_SCAN_INTERVAL = timedelta(seconds=60) +FW_SCAN_INTERVAL = timedelta(hours=12) _LOGGER = logging.getLogger(__name__) -class QswUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): +class QswDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Class to manage fetching data from the QNAP QSW device.""" def __init__(self, hass: HomeAssistant, qsw: QnapQswApi) -> None: @@ -30,7 +31,7 @@ class QswUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): hass, _LOGGER, name=DOMAIN, - update_interval=SCAN_INTERVAL, + update_interval=DATA_SCAN_INTERVAL, ) async def _async_update_data(self) -> dict[str, Any]: @@ -41,3 +42,29 @@ class QswUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): except QswError as error: raise UpdateFailed(error) from error return self.qsw.data() + + +class QswFirmwareCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Class to manage fetching firmware data from the QNAP QSW device.""" + + def __init__(self, hass: HomeAssistant, qsw: QnapQswApi) -> None: + """Initialize.""" + self.qsw = qsw + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=FW_SCAN_INTERVAL, + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Update firmware data via library.""" + async with async_timeout.timeout(QSW_TIMEOUT_SEC): + try: + await self.qsw.check_firmware() + except APIError as error: + _LOGGER.warning(error) + except QswError as error: + raise UpdateFailed(error) from error + return self.qsw.data() diff --git a/homeassistant/components/qnap_qsw/diagnostics.py b/homeassistant/components/qnap_qsw/diagnostics.py index 3730bab24a8..2467e9181a3 100644 --- a/homeassistant/components/qnap_qsw/diagnostics.py +++ b/homeassistant/components/qnap_qsw/diagnostics.py @@ -10,8 +10,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME from homeassistant.core import HomeAssistant -from .const import DOMAIN -from .coordinator import QswUpdateCoordinator +from .const import DOMAIN, QSW_COORD_DATA, QSW_COORD_FW +from .coordinator import QswDataCoordinator, QswFirmwareCoordinator TO_REDACT_CONFIG = [ CONF_USERNAME, @@ -29,9 +29,12 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: QswUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + entry_data = hass.data[DOMAIN][config_entry.entry_id] + coord_data: QswDataCoordinator = entry_data[QSW_COORD_DATA] + coord_fw: QswFirmwareCoordinator = entry_data[QSW_COORD_FW] return { "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT_CONFIG), - "coord_data": async_redact_data(coordinator.data, TO_REDACT_DATA), + "coord_data": async_redact_data(coord_data.data, TO_REDACT_DATA), + "coord_fw": async_redact_data(coord_fw.data, TO_REDACT_DATA), } diff --git a/homeassistant/components/qnap_qsw/entity.py b/homeassistant/components/qnap_qsw/entity.py index c3550610d83..7da47f9734f 100644 --- a/homeassistant/components/qnap_qsw/entity.py +++ b/homeassistant/components/qnap_qsw/entity.py @@ -20,15 +20,15 @@ from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import MANUFACTURER -from .coordinator import QswUpdateCoordinator +from .coordinator import QswDataCoordinator, QswFirmwareCoordinator -class QswEntity(CoordinatorEntity[QswUpdateCoordinator]): +class QswDataEntity(CoordinatorEntity[QswDataCoordinator]): """Define an QNAP QSW entity.""" def __init__( self, - coordinator: QswUpdateCoordinator, + coordinator: QswDataCoordinator, entry: ConfigEntry, ) -> None: """Initialize.""" @@ -72,7 +72,7 @@ class QswEntityDescription(EntityDescription, QswEntityDescriptionMixin): attributes: dict[str, list[str]] | None = None -class QswSensorEntity(QswEntity): +class QswSensorEntity(QswDataEntity): """Base class for QSW sensor entities.""" entity_description: QswEntityDescription @@ -91,3 +91,38 @@ class QswSensorEntity(QswEntity): key: self.get_device_value(val[0], val[1]) for key, val in self.entity_description.attributes.items() } + + +class QswFirmwareEntity(CoordinatorEntity[QswFirmwareCoordinator]): + """Define a QNAP QSW firmware entity.""" + + def __init__( + self, + coordinator: QswFirmwareCoordinator, + entry: ConfigEntry, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + + self._attr_device_info = DeviceInfo( + configuration_url=entry.data[CONF_URL], + connections={ + ( + CONNECTION_NETWORK_MAC, + self.get_device_value(QSD_SYSTEM_BOARD, QSD_MAC), + ) + }, + manufacturer=MANUFACTURER, + model=self.get_device_value(QSD_SYSTEM_BOARD, QSD_PRODUCT), + name=self.get_device_value(QSD_SYSTEM_BOARD, QSD_PRODUCT), + sw_version=self.get_device_value(QSD_FIRMWARE_INFO, QSD_FIRMWARE), + ) + + def get_device_value(self, key: str, subkey: str) -> Any: + """Return device value by key.""" + value = None + if key in self.coordinator.data: + data = self.coordinator.data[key] + if subkey in data: + value = data[subkey] + return value diff --git a/homeassistant/components/qnap_qsw/sensor.py b/homeassistant/components/qnap_qsw/sensor.py index 0de8ec4a39e..618c20b4cc2 100644 --- a/homeassistant/components/qnap_qsw/sensor.py +++ b/homeassistant/components/qnap_qsw/sensor.py @@ -26,8 +26,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_MAX, DOMAIN, RPM -from .coordinator import QswUpdateCoordinator +from .const import ATTR_MAX, DOMAIN, QSW_COORD_DATA, RPM +from .coordinator import QswDataCoordinator from .entity import QswEntityDescription, QswSensorEntity @@ -82,7 +82,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add QNAP QSW sensors from a config_entry.""" - coordinator: QswUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: QswDataCoordinator = hass.data[DOMAIN][entry.entry_id][QSW_COORD_DATA] async_add_entities( QswSensor(coordinator, description, entry) for description in SENSOR_TYPES @@ -100,7 +100,7 @@ class QswSensor(QswSensorEntity, SensorEntity): def __init__( self, - coordinator: QswUpdateCoordinator, + coordinator: QswDataCoordinator, description: QswSensorEntityDescription, entry: ConfigEntry, ) -> None: diff --git a/homeassistant/components/qnap_qsw/update.py b/homeassistant/components/qnap_qsw/update.py new file mode 100644 index 00000000000..8dfd985ffef --- /dev/null +++ b/homeassistant/components/qnap_qsw/update.py @@ -0,0 +1,89 @@ +"""Support for the QNAP QSW update.""" +from __future__ import annotations + +from typing import Final + +from aioqsw.const import ( + QSD_DESCRIPTION, + QSD_FIRMWARE_CHECK, + QSD_FIRMWARE_INFO, + QSD_PRODUCT, + QSD_SYSTEM_BOARD, + QSD_VERSION, +) + +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, QSW_COORD_FW, QSW_UPDATE +from .coordinator import QswFirmwareCoordinator +from .entity import QswFirmwareEntity + +UPDATE_TYPES: Final[tuple[UpdateEntityDescription, ...]] = ( + UpdateEntityDescription( + device_class=UpdateDeviceClass.FIRMWARE, + entity_category=EntityCategory.CONFIG, + key=QSW_UPDATE, + name="Firmware Update", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add QNAP QSW updates from a config_entry.""" + coordinator: QswFirmwareCoordinator = hass.data[DOMAIN][entry.entry_id][ + QSW_COORD_FW + ] + async_add_entities( + QswUpdate(coordinator, description, entry) for description in UPDATE_TYPES + ) + + +class QswUpdate(QswFirmwareEntity, UpdateEntity): + """Define a QNAP QSW update.""" + + entity_description: UpdateEntityDescription + + def __init__( + self, + coordinator: QswFirmwareCoordinator, + description: UpdateEntityDescription, + entry: ConfigEntry, + ) -> None: + """Initialize.""" + super().__init__(coordinator, entry) + self._attr_name = ( + f"{self.get_device_value(QSD_SYSTEM_BOARD, QSD_PRODUCT)} {description.name}" + ) + self._attr_unique_id = f"{entry.unique_id}_{description.key}" + self.entity_description = description + + self._attr_installed_version = self.get_device_value( + QSD_FIRMWARE_INFO, QSD_VERSION + ) + self._async_update_attrs() + + @callback + def _handle_coordinator_update(self) -> None: + """Update attributes when the coordinator updates.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + @callback + def _async_update_attrs(self) -> None: + """Update attributes.""" + self._attr_latest_version = self.get_device_value( + QSD_FIRMWARE_CHECK, QSD_VERSION + ) + self._attr_release_summary = self.get_device_value( + QSD_FIRMWARE_CHECK, QSD_DESCRIPTION + ) diff --git a/tests/components/qnap_qsw/test_coordinator.py b/tests/components/qnap_qsw/test_coordinator.py index b8e4855fea9..107cfa580b7 100644 --- a/tests/components/qnap_qsw/test_coordinator.py +++ b/tests/components/qnap_qsw/test_coordinator.py @@ -2,10 +2,13 @@ from unittest.mock import patch -from aioqsw.exceptions import QswError +from aioqsw.exceptions import APIError, QswError from homeassistant.components.qnap_qsw.const import DOMAIN -from homeassistant.components.qnap_qsw.coordinator import SCAN_INTERVAL +from homeassistant.components.qnap_qsw.coordinator import ( + DATA_SCAN_INTERVAL, + FW_SCAN_INTERVAL, +) from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow @@ -14,6 +17,7 @@ from .util import ( CONFIG, FIRMWARE_CONDITION_MOCK, FIRMWARE_INFO_MOCK, + FIRMWARE_UPDATE_CHECK_MOCK, SYSTEM_BOARD_MOCK, SYSTEM_SENSOR_MOCK, SYSTEM_TIME_MOCK, @@ -37,6 +41,9 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_info", return_value=FIRMWARE_INFO_MOCK, ) as mock_firmware_info, patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_update_check", + return_value=FIRMWARE_UPDATE_CHECK_MOCK, + ) as mock_firmware_update_check, patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_system_board", return_value=SYSTEM_BOARD_MOCK, ) as mock_system_board, patch( @@ -57,14 +64,16 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: mock_firmware_condition.assert_called_once() mock_firmware_info.assert_called_once() + mock_firmware_update_check.assert_called_once() mock_system_board.assert_called_once() mock_system_sensor.assert_called_once() mock_system_time.assert_called_once() - mock_users_verification.assert_not_called() + mock_users_verification.assert_called_once() mock_users_login.assert_called_once() mock_firmware_condition.reset_mock() mock_firmware_info.reset_mock() + mock_firmware_update_check.reset_mock() mock_system_board.reset_mock() mock_system_sensor.reset_mock() mock_system_time.reset_mock() @@ -72,12 +81,28 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: mock_users_login.reset_mock() mock_system_sensor.side_effect = QswError - async_fire_time_changed(hass, utcnow() + SCAN_INTERVAL) + async_fire_time_changed(hass, utcnow() + DATA_SCAN_INTERVAL) await hass.async_block_till_done() mock_system_sensor.assert_called_once() - mock_users_verification.assert_called_once() + mock_users_verification.assert_called() mock_users_login.assert_not_called() state = hass.states.get("sensor.qsw_m408_4c_temperature") assert state.state == STATE_UNAVAILABLE + + mock_firmware_update_check.side_effect = APIError + async_fire_time_changed(hass, utcnow() + FW_SCAN_INTERVAL) + await hass.async_block_till_done() + + mock_firmware_update_check.assert_called_once() + mock_firmware_update_check.reset_mock() + + mock_firmware_update_check.side_effect = QswError + async_fire_time_changed(hass, utcnow() + FW_SCAN_INTERVAL) + await hass.async_block_till_done() + + mock_firmware_update_check.assert_called_once() + + update = hass.states.get("update.qsw_m408_4c_firmware_update") + assert update.state == STATE_UNAVAILABLE diff --git a/tests/components/qnap_qsw/test_init.py b/tests/components/qnap_qsw/test_init.py index 211cd7ed41d..75fa9ec8adc 100644 --- a/tests/components/qnap_qsw/test_init.py +++ b/tests/components/qnap_qsw/test_init.py @@ -20,6 +20,9 @@ async def test_unload_entry(hass: HomeAssistant) -> None: config_entry.add_to_hass(hass) with patch( + "homeassistant.components.qnap_qsw.QnapQswApi.check_firmware", + return_value=None, + ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.validate", return_value=None, ), patch( diff --git a/tests/components/qnap_qsw/test_update.py b/tests/components/qnap_qsw/test_update.py new file mode 100644 index 00000000000..69f4a3d08b4 --- /dev/null +++ b/tests/components/qnap_qsw/test_update.py @@ -0,0 +1,26 @@ +"""The sensor tests for the QNAP QSW platform.""" + +from aioqsw.const import API_RESULT, API_VERSION + +from homeassistant.const import STATE_OFF +from homeassistant.core import HomeAssistant + +from .util import FIRMWARE_INFO_MOCK, FIRMWARE_UPDATE_CHECK_MOCK, async_init_integration + + +async def test_qnap_qsw_update(hass: HomeAssistant) -> None: + """Test creation of update entities.""" + + await async_init_integration(hass) + + update = hass.states.get("update.qsw_m408_4c_firmware_update") + assert update is not None + assert update.state == STATE_OFF + assert ( + update.attributes.get("installed_version") + == FIRMWARE_INFO_MOCK[API_RESULT][API_VERSION] + ) + assert ( + update.attributes.get("latest_version") + == FIRMWARE_UPDATE_CHECK_MOCK[API_RESULT][API_VERSION] + ) diff --git a/tests/components/qnap_qsw/util.py b/tests/components/qnap_qsw/util.py index 28e7f7881d5..a057dfbe3ac 100644 --- a/tests/components/qnap_qsw/util.py +++ b/tests/components/qnap_qsw/util.py @@ -12,6 +12,8 @@ from aioqsw.const import ( API_COMMIT_CPSS, API_COMMIT_ISS, API_DATE, + API_DESCRIPTION, + API_DOWNLOAD_URL, API_ERROR_CODE, API_ERROR_MESSAGE, API_FAN1_SPEED, @@ -20,6 +22,7 @@ from aioqsw.const import ( API_MAX_SWITCH_TEMP, API_MESSAGE, API_MODEL, + API_NEWER, API_NUMBER, API_PORT_NUM, API_PRODUCT, @@ -90,6 +93,24 @@ FIRMWARE_INFO_MOCK = { }, } +FIRMWARE_UPDATE_CHECK_MOCK = { + API_ERROR_CODE: 200, + API_ERROR_MESSAGE: "OK", + API_RESULT: { + API_VERSION: "1.2.0", + API_NUMBER: "29649", + API_BUILD_NUMBER: "20220128", + API_DATE: "Fri, 28 Jan 2022 01:17:39 +0800", + API_DESCRIPTION: "", + API_DOWNLOAD_URL: [ + "https://download.qnap.com/Storage/Networking/QSW408FW/QSW-M408AC3-FW.v1.2.0_S20220128_29649.img", + "https://eu1.qnap.com/Storage/Networking/QSW408FW/QSW-M408AC3-FW.v1.2.0_S20220128_29649.img", + "https://us1.qnap.com/Storage/Networking/QSW408FW/QSW-M408AC3-FW.v1.2.0_S20220128_29649.img", + ], + API_NEWER: False, + }, +} + SYSTEM_COMMAND_MOCK = { API_ERROR_CODE: 200, API_ERROR_MESSAGE: "OK", @@ -146,6 +167,9 @@ async def async_init_integration( ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_info", return_value=FIRMWARE_INFO_MOCK, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_update_check", + return_value=FIRMWARE_UPDATE_CHECK_MOCK, ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_system_board", return_value=SYSTEM_BOARD_MOCK, @@ -155,6 +179,9 @@ async def async_init_integration( ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_system_time", return_value=SYSTEM_TIME_MOCK, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_users_verification", + return_value=USERS_VERIFICATION_MOCK, ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.post_users_login", return_value=USERS_LOGIN_MOCK, From 9a9fea442321c478a4be3b7b596900a67c5e9b4b Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 30 Jun 2022 22:10:19 +0300 Subject: [PATCH 2043/3516] Migrate `glances` unique_id to new format (#74033) --- homeassistant/components/glances/sensor.py | 46 ++++++++++++++++++---- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 7dfd0c503ef..e37cfaca211 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -1,8 +1,9 @@ """Support gathering system information of hosts which are running glances.""" from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE +from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -18,14 +19,34 @@ async def async_setup_entry( ) -> None: """Set up the Glances sensors.""" - client = hass.data[DOMAIN][config_entry.entry_id] + client: GlancesData = hass.data[DOMAIN][config_entry.entry_id] name = config_entry.data[CONF_NAME] dev = [] + @callback + def _migrate_old_unique_ids( + hass: HomeAssistant, old_unique_id: str, new_key: str + ) -> None: + """Migrate unique IDs to the new format.""" + ent_reg = entity_registry.async_get(hass) + + if entity_id := ent_reg.async_get_entity_id( + Platform.SENSOR, DOMAIN, old_unique_id + ): + + ent_reg.async_update_entity( + entity_id, new_unique_id=f"{config_entry.entry_id}-{new_key}" + ) + for description in SENSOR_TYPES: if description.type == "fs": # fs will provide a list of disks attached for disk in client.api.data[description.type]: + _migrate_old_unique_ids( + hass, + f"{client.host}-{name} {disk['mnt_point']} {description.name_suffix}", + f"{disk['mnt_point']}-{description.key}", + ) dev.append( GlancesSensor( client, @@ -38,6 +59,11 @@ async def async_setup_entry( # sensors will provide temp for different devices for sensor in client.api.data[description.type]: if sensor["type"] == description.key: + _migrate_old_unique_ids( + hass, + f"{client.host}-{name} {sensor['label']} {description.name_suffix}", + f"{sensor['label']}-{description.key}", + ) dev.append( GlancesSensor( client, @@ -48,8 +74,18 @@ async def async_setup_entry( ) elif description.type == "raid": for raid_device in client.api.data[description.type]: + _migrate_old_unique_ids( + hass, + f"{client.host}-{name} {raid_device} {description.name_suffix}", + f"{raid_device}-{description.key}", + ) dev.append(GlancesSensor(client, name, raid_device, description)) elif client.api.data[description.type]: + _migrate_old_unique_ids( + hass, + f"{client.host}-{name} {description.name_suffix}", + f"-{description.key}", + ) dev.append( GlancesSensor( client, @@ -87,11 +123,7 @@ class GlancesSensor(SensorEntity): manufacturer="Glances", name=name, ) - - @property - def unique_id(self): - """Set unique_id for sensor.""" - return f"{self.glances_data.host}-{self.name}" + self._attr_unique_id = f"{self.glances_data.config_entry.entry_id}-{sensor_name_prefix}-{description.key}" @property def available(self): From 8ef87205f928948f7c2b4d10ec24788726f02650 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 Jun 2022 21:11:45 +0200 Subject: [PATCH 2044/3516] Improve type hints in mqtt (#74247) --- .../components/mqtt/binary_sensor.py | 5 ++-- homeassistant/components/mqtt/config_flow.py | 30 +++++++++++++------ tests/components/mqtt/test_config_flow.py | 4 +-- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 9e0a049b15e..012c6e81ac8 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -10,6 +10,7 @@ import voluptuous as vol from homeassistant.components import binary_sensor from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -291,12 +292,12 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): return self._state @property - def device_class(self): + def device_class(self) -> BinarySensorDeviceClass | None: """Return the class of this sensor.""" return self._config.get(CONF_DEVICE_CLASS) @property - def force_update(self): + def force_update(self) -> bool: """Force update.""" return self._config[CONF_FORCE_UPDATE] diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index a8d0957921d..538c12d258c 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import OrderedDict import queue +from typing import Any import voluptuous as vol @@ -55,14 +56,18 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return MQTTOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") return await self.async_step_broker() - async def async_step_broker(self, user_input=None): + async def async_step_broker( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Confirm the setup.""" errors = {} @@ -102,9 +107,12 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_hassio_confirm() - async def async_step_hassio_confirm(self, user_input=None): + async def async_step_hassio_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Confirm a Hass.io discovery.""" errors = {} + assert self._hassio_discovery if user_input is not None: data = self._hassio_discovery @@ -148,11 +156,13 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): self.broker_config: dict[str, str | int] = {} self.options = dict(config_entry.options) - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input: None = None) -> FlowResult: """Manage the MQTT options.""" return await self.async_step_broker() - async def async_step_broker(self, user_input=None): + async def async_step_broker( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the MQTT broker configuration.""" errors = {} current_config = self.config_entry.data @@ -200,12 +210,14 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): last_step=False, ) - async def async_step_options(self, user_input=None): + async def async_step_options( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the MQTT options.""" errors = {} current_config = self.config_entry.data yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {}) - options_config = {} + options_config: dict[str, Any] = {} if user_input is not None: bad_birth = False bad_will = False @@ -251,7 +263,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): self.hass.config_entries.async_update_entry( self.config_entry, data=updated_config ) - return self.async_create_entry(title="", data=None) + return self.async_create_entry(title="", data={}) birth = { **DEFAULT_BIRTH, @@ -269,7 +281,7 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): CONF_DISCOVERY, yaml_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY) ) - fields = OrderedDict() + fields: OrderedDict[vol.Marker, Any] = OrderedDict() fields[vol.Optional(CONF_DISCOVERY, default=discovery)] = bool # Birth message is disabled if CONF_BIRTH_MESSAGE = {} diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 6fe781335f0..6f1b662e282 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -312,7 +312,7 @@ async def test_option_flow(hass, mqtt_mock_entry_no_yaml_config, mock_try_connec }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"] is None + assert result["data"] == {} assert config_entry.data == { mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345, @@ -387,7 +387,7 @@ async def test_disable_birth_will( }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"] is None + assert result["data"] == {} assert config_entry.data == { mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345, From 7656ca8313a7b423ad49fdbc677d66e6edf90582 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Thu, 30 Jun 2022 21:12:12 +0200 Subject: [PATCH 2045/3516] Add presence detection to devolo_home_network (#72030) --- .../components/devolo_home_network/const.py | 13 +- .../devolo_home_network/device_tracker.py | 159 ++++++++++++++++++ .../devolo_home_network/__init__.py | 4 +- tests/components/devolo_home_network/const.py | 4 + .../test_device_tracker.py | 107 ++++++++++++ 5 files changed, 285 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/devolo_home_network/device_tracker.py create mode 100644 tests/components/devolo_home_network/test_device_tracker.py diff --git a/homeassistant/components/devolo_home_network/const.py b/homeassistant/components/devolo_home_network/const.py index 2dfdd3c1d9a..1a49beb5d02 100644 --- a/homeassistant/components/devolo_home_network/const.py +++ b/homeassistant/components/devolo_home_network/const.py @@ -5,8 +5,9 @@ from datetime import timedelta from homeassistant.const import Platform DOMAIN = "devolo_home_network" -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, Platform.SENSOR] +MAC_ADDRESS = "mac_address" PRODUCT = "product" SERIAL_NUMBER = "serial_number" TITLE = "title" @@ -15,6 +16,16 @@ LONG_UPDATE_INTERVAL = timedelta(minutes=5) SHORT_UPDATE_INTERVAL = timedelta(seconds=15) CONNECTED_PLC_DEVICES = "connected_plc_devices" +CONNECTED_STATIONS = "connected_stations" CONNECTED_TO_ROUTER = "connected_to_router" CONNECTED_WIFI_CLIENTS = "connected_wifi_clients" NEIGHBORING_WIFI_NETWORKS = "neighboring_wifi_networks" + +WIFI_APTYPE = { + "WIFI_VAP_MAIN_AP": "Main", + "WIFI_VAP_GUEST_AP": "Guest", +} +WIFI_BANDS = { + "WIFI_BAND_2G": 2.4, + "WIFI_BAND_5G": 5, +} diff --git a/homeassistant/components/devolo_home_network/device_tracker.py b/homeassistant/components/devolo_home_network/device_tracker.py new file mode 100644 index 00000000000..9dffeef7db9 --- /dev/null +++ b/homeassistant/components/devolo_home_network/device_tracker.py @@ -0,0 +1,159 @@ +"""Platform for device tracker integration.""" +from __future__ import annotations + +from typing import Any + +from devolo_plc_api.device import Device + +from homeassistant.components.device_tracker import ( + DOMAIN as DEVICE_TRACKER_DOMAIN, + SOURCE_TYPE_ROUTER, +) +from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import FREQUENCY_GIGAHERTZ, STATE_UNKNOWN +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import ( + CONNECTED_STATIONS, + CONNECTED_WIFI_CLIENTS, + DOMAIN, + MAC_ADDRESS, + WIFI_APTYPE, + WIFI_BANDS, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Get all devices and sensors and setup them via config entry.""" + device: Device = hass.data[DOMAIN][entry.entry_id]["device"] + coordinators: dict[str, DataUpdateCoordinator] = hass.data[DOMAIN][entry.entry_id][ + "coordinators" + ] + registry = entity_registry.async_get(hass) + tracked = set() + + @callback + def new_device_callback() -> None: + """Add new devices if needed.""" + new_entities = [] + for station in coordinators[CONNECTED_WIFI_CLIENTS].data[CONNECTED_STATIONS]: + if station[MAC_ADDRESS] in tracked: + continue + + new_entities.append( + DevoloScannerEntity( + coordinators[CONNECTED_WIFI_CLIENTS], device, station[MAC_ADDRESS] + ) + ) + tracked.add(station[MAC_ADDRESS]) + if new_entities: + async_add_entities(new_entities) + + @callback + def restore_entities() -> None: + """Restore clients that are not a part of active clients list.""" + missing = [] + for entity in entity_registry.async_entries_for_config_entry( + registry, entry.entry_id + ): + if ( + entity.platform == DOMAIN + and entity.domain == DEVICE_TRACKER_DOMAIN + and ( + mac_address := entity.unique_id.replace( + f"{device.serial_number}_", "" + ) + ) + not in tracked + ): + missing.append( + DevoloScannerEntity( + coordinators[CONNECTED_WIFI_CLIENTS], device, mac_address + ) + ) + tracked.add(mac_address) + + if missing: + async_add_entities(missing) + + if device.device and "wifi1" in device.device.features: + restore_entities() + entry.async_on_unload( + coordinators[CONNECTED_WIFI_CLIENTS].async_add_listener(new_device_callback) + ) + + +class DevoloScannerEntity(CoordinatorEntity, ScannerEntity): + """Representation of a devolo device tracker.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, device: Device, mac: str + ) -> None: + """Initialize entity.""" + super().__init__(coordinator) + self._device = device + self._mac = mac + + @property + def extra_state_attributes(self) -> dict[str, str]: + """Return the attributes.""" + attrs: dict[str, str] = {} + if not self.coordinator.data[CONNECTED_STATIONS]: + return {} + + station: dict[str, Any] = next( + ( + station + for station in self.coordinator.data[CONNECTED_STATIONS] + if station[MAC_ADDRESS] == self.mac_address + ), + {}, + ) + if station: + attrs["wifi"] = WIFI_APTYPE.get(station["vap_type"], STATE_UNKNOWN) + attrs["band"] = ( + f"{WIFI_BANDS.get(station['band'])} {FREQUENCY_GIGAHERTZ}" + if WIFI_BANDS.get(station["band"]) + else STATE_UNKNOWN + ) + return attrs + + @property + def icon(self) -> str: + """Return device icon.""" + if self.is_connected: + return "mdi:lan-connect" + return "mdi:lan-disconnect" + + @property + def is_connected(self) -> bool: + """Return true if the device is connected to the network.""" + return any( + station + for station in self.coordinator.data[CONNECTED_STATIONS] + if station[MAC_ADDRESS] == self.mac_address + ) + + @property + def mac_address(self) -> str: + """Return mac_address.""" + return self._mac + + @property + def source_type(self) -> str: + """Return tracker source type.""" + return SOURCE_TYPE_ROUTER + + @property + def unique_id(self) -> str: + """Return unique ID of the entity.""" + return f"{self._device.serial_number}_{self._mac}" diff --git a/tests/components/devolo_home_network/__init__.py b/tests/components/devolo_home_network/__init__.py index 1c10d7a59ef..bb861081517 100644 --- a/tests/components/devolo_home_network/__init__.py +++ b/tests/components/devolo_home_network/__init__.py @@ -28,6 +28,8 @@ def configure_integration(hass: HomeAssistant) -> MockConfigEntry: async def async_connect(self, session_instance: Any = None): """Give a mocked device the needed properties.""" - self.mac = DISCOVERY_INFO.properties["PlcMacAddress"] self.plcnet = PlcNetApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) self.device = DeviceApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) + self.mac = DISCOVERY_INFO.properties["PlcMacAddress"] + self.product = DISCOVERY_INFO.properties["Product"] + self.serial_number = DISCOVERY_INFO.properties["SN"] diff --git a/tests/components/devolo_home_network/const.py b/tests/components/devolo_home_network/const.py index 516a19f3421..aec27ce6a8b 100644 --- a/tests/components/devolo_home_network/const.py +++ b/tests/components/devolo_home_network/const.py @@ -16,6 +16,10 @@ CONNECTED_STATIONS = { ], } +NO_CONNECTED_STATIONS = { + "connected_stations": [], +} + DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( host=IP, addresses=[IP], diff --git a/tests/components/devolo_home_network/test_device_tracker.py b/tests/components/devolo_home_network/test_device_tracker.py new file mode 100644 index 00000000000..233a480b5e3 --- /dev/null +++ b/tests/components/devolo_home_network/test_device_tracker.py @@ -0,0 +1,107 @@ +"""Tests for the devolo Home Network device tracker.""" +from unittest.mock import AsyncMock, patch + +from devolo_plc_api.exceptions.device import DeviceUnavailable +import pytest + +from homeassistant.components.device_tracker import DOMAIN as PLATFORM +from homeassistant.components.devolo_home_network.const import ( + DOMAIN, + LONG_UPDATE_INTERVAL, + WIFI_APTYPE, + WIFI_BANDS, +) +from homeassistant.const import ( + FREQUENCY_GIGAHERTZ, + STATE_HOME, + STATE_NOT_HOME, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry +from homeassistant.util import dt + +from . import configure_integration +from .const import CONNECTED_STATIONS, DISCOVERY_INFO, NO_CONNECTED_STATIONS + +from tests.common import async_fire_time_changed + +STATION = CONNECTED_STATIONS["connected_stations"][0] +SERIAL = DISCOVERY_INFO.properties["SN"] + + +@pytest.mark.usefixtures("mock_device") +async def test_device_tracker(hass: HomeAssistant): + """Test device tracker states.""" + state_key = f"{PLATFORM}.{DOMAIN}_{SERIAL}_{STATION['mac_address'].lower().replace(':', '_')}" + entry = configure_integration(hass) + er = entity_registry.async_get(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + # Enable entity + er.async_update_entity(state_key, disabled_by=None) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_HOME + assert state.attributes["wifi"] == WIFI_APTYPE[STATION["vap_type"]] + assert ( + state.attributes["band"] + == f"{WIFI_BANDS[STATION['band']]} {FREQUENCY_GIGAHERTZ}" + ) + + # Emulate state change + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_connected_station", + new=AsyncMock(return_value=NO_CONNECTED_STATIONS), + ): + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_NOT_HOME + + # Emulate device failure + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_connected_station", + side_effect=DeviceUnavailable, + ): + async_fire_time_changed(hass, dt.utcnow() + LONG_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_unload(entry.entry_id) + + +@pytest.mark.usefixtures("mock_device") +async def test_restoring_clients(hass: HomeAssistant): + """Test restoring existing device_tracker entities.""" + state_key = f"{PLATFORM}.{DOMAIN}_{SERIAL}_{STATION['mac_address'].lower().replace(':', '_')}" + entry = configure_integration(hass) + er = entity_registry.async_get(hass) + er.async_get_or_create( + PLATFORM, + DOMAIN, + f"{SERIAL}_{STATION['mac_address']}", + config_entry=entry, + ) + + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_get_wifi_connected_station", + new=AsyncMock(return_value=NO_CONNECTED_STATIONS), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_NOT_HOME From 24d2d6212135bea2e1a2e8d8f35d967001124811 Mon Sep 17 00:00:00 2001 From: mbo18 Date: Thu, 30 Jun 2022 21:22:03 +0200 Subject: [PATCH 2046/3516] Add vacation mode to Template Alarm Control Panel (#74261) --- .../components/template/alarm_control_panel.py | 18 ++++++++++++++++++ .../template/test_alarm_control_panel.py | 9 +++++++++ 2 files changed, 27 insertions(+) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index ae26e58ac04..132e9fb0ca5 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -21,6 +21,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, @@ -43,6 +44,7 @@ _VALID_STATES = [ STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, @@ -53,6 +55,7 @@ _VALID_STATES = [ CONF_ARM_AWAY_ACTION = "arm_away" CONF_ARM_HOME_ACTION = "arm_home" CONF_ARM_NIGHT_ACTION = "arm_night" +CONF_ARM_VACATION_ACTION = "arm_vacation" CONF_DISARM_ACTION = "disarm" CONF_ALARM_CONTROL_PANELS = "panels" CONF_CODE_ARM_REQUIRED = "code_arm_required" @@ -74,6 +77,7 @@ ALARM_CONTROL_PANEL_SCHEMA = vol.Schema( vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name): cv.enum( TemplateCodeFormat @@ -157,6 +161,9 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): self._arm_night_script = None if (arm_night_action := config.get(CONF_ARM_NIGHT_ACTION)) is not None: self._arm_night_script = Script(hass, arm_night_action, name, DOMAIN) + self._arm_vacation_script = None + if (arm_vacation_action := config.get(CONF_ARM_VACATION_ACTION)) is not None: + self._arm_vacation_script = Script(hass, arm_vacation_action, name, DOMAIN) self._state: str | None = None @@ -184,6 +191,11 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): supported_features | AlarmControlPanelEntityFeature.ARM_AWAY ) + if self._arm_vacation_script is not None: + supported_features = ( + supported_features | AlarmControlPanelEntityFeature.ARM_VACATION + ) + return supported_features @property @@ -257,6 +269,12 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): STATE_ALARM_ARMED_NIGHT, script=self._arm_night_script, code=code ) + async def async_alarm_arm_vacation(self, code: str | None = None) -> None: + """Arm the panel to Vacation.""" + await self._async_alarm_arm( + STATE_ALARM_ARMED_VACATION, script=self._arm_vacation_script, code=code + ) + async def async_alarm_disarm(self, code: str | None = None) -> None: """Disarm the panel.""" await self._async_alarm_arm( diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index 8f9ab39f7c0..9e51b48dcc6 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -10,6 +10,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, @@ -56,6 +57,11 @@ OPTIMISTIC_TEMPLATE_ALARM_CONFIG = { "entity_id": "alarm_control_panel.test", "data": {"code": "{{ this.entity_id }}"}, }, + "arm_vacation": { + "service": "alarm_control_panel.alarm_arm_vacation", + "entity_id": "alarm_control_panel.test", + "data": {"code": "{{ this.entity_id }}"}, + }, "disarm": { "service": "alarm_control_panel.alarm_disarm", "entity_id": "alarm_control_panel.test", @@ -89,6 +95,7 @@ async def test_template_state_text(hass, start_ha): STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMED_VACATION, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, @@ -128,6 +135,7 @@ async def test_optimistic_states(hass, start_ha): ("alarm_arm_away", STATE_ALARM_ARMED_AWAY), ("alarm_arm_home", STATE_ALARM_ARMED_HOME), ("alarm_arm_night", STATE_ALARM_ARMED_NIGHT), + ("alarm_arm_vacation", STATE_ALARM_ARMED_VACATION), ("alarm_disarm", STATE_ALARM_DISARMED), ]: await hass.services.async_call( @@ -250,6 +258,7 @@ async def test_name(hass, start_ha): "alarm_arm_home", "alarm_arm_away", "alarm_arm_night", + "alarm_arm_vacation", "alarm_disarm", ], ) From f80d522c6a68451a1d1fbe6bd532d0658393094f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 Jun 2022 21:50:57 +0200 Subject: [PATCH 2047/3516] Add Camera checks to pylint plugin (#74264) --- pylint/plugins/hass_enforce_type_hints.py | 114 ++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 2696c9965e4..36c198c7218 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -742,6 +742,120 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "camera": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="Camera", + matches=[ + TypeHintMatch( + function_name="entity_picture", + return_type="str", + ), + TypeHintMatch( + function_name="supported_features", + return_type="int", + ), + TypeHintMatch( + function_name="is_recording", + return_type="bool", + ), + TypeHintMatch( + function_name="is_streaming", + return_type="bool", + ), + TypeHintMatch( + function_name="brand", + return_type=["str", None], + ), + TypeHintMatch( + function_name="motion_detection_enabled", + return_type="bool", + ), + TypeHintMatch( + function_name="model", + return_type=["str", None], + ), + TypeHintMatch( + function_name="frame_interval", + return_type="float", + ), + TypeHintMatch( + function_name="frontend_stream_type", + return_type=["StreamType", None], + ), + TypeHintMatch( + function_name="available", + return_type="bool", + ), + TypeHintMatch( + function_name="async_create_stream", + return_type=["Stream", None], + ), + TypeHintMatch( + function_name="stream_source", + return_type=["str", None], + ), + TypeHintMatch( + function_name="async_handle_web_rtc_offer", + arg_types={ + 1: "str", + }, + return_type=["str", None], + ), + TypeHintMatch( + function_name="camera_image", + named_arg_types={ + "width": "int | None", + "height": "int | None", + }, + return_type=["bytes", None], + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="handle_async_still_stream", + arg_types={ + 1: "Request", + 2: "float", + }, + return_type="StreamResponse", + ), + TypeHintMatch( + function_name="handle_async_mjpeg_stream", + arg_types={ + 1: "Request", + }, + return_type=["StreamResponse", None], + ), + TypeHintMatch( + function_name="is_on", + return_type="bool", + ), + TypeHintMatch( + function_name="turn_off", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_on", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="enable_motion_detection", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="disable_motion_detection", + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ], "cover": [ ClassTypeHintMatch( base_class="Entity", From dc559f2439df34d9cec1b4c284fb07bde7ab5899 Mon Sep 17 00:00:00 2001 From: Christopher Hoage Date: Thu, 30 Jun 2022 13:06:22 -0700 Subject: [PATCH 2048/3516] Bump venstarcolortouch to 0.17 (#74271) --- homeassistant/components/venstar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index e63c75792bf..2f3331af6e2 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -3,7 +3,7 @@ "name": "Venstar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/venstar", - "requirements": ["venstarcolortouch==0.16"], + "requirements": ["venstarcolortouch==0.17"], "codeowners": ["@garbled1"], "iot_class": "local_polling", "loggers": ["venstarcolortouch"] diff --git a/requirements_all.txt b/requirements_all.txt index eac0800e73a..6a515ec37e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2387,7 +2387,7 @@ vehicle==0.4.0 velbus-aio==2022.6.2 # homeassistant.components.venstar -venstarcolortouch==0.16 +venstarcolortouch==0.17 # homeassistant.components.vilfo vilfo-api-client==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f4b5e587f7a..8c3f01ecca7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1593,7 +1593,7 @@ vehicle==0.4.0 velbus-aio==2022.6.2 # homeassistant.components.venstar -venstarcolortouch==0.16 +venstarcolortouch==0.17 # homeassistant.components.vilfo vilfo-api-client==0.3.2 From 269fa1472105ed8b841e4f5d1f671563dce2c5e4 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 30 Jun 2022 16:59:35 -0400 Subject: [PATCH 2049/3516] Fix bad conditional in ZHA logbook (#74277) * Fix bad conditional in ZHA logbook * change syntax --- homeassistant/components/zha/logbook.py | 4 ++-- tests/components/zha/test_logbook.py | 29 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/logbook.py b/homeassistant/components/zha/logbook.py index 8140a5244f1..90d433be210 100644 --- a/homeassistant/components/zha/logbook.py +++ b/homeassistant/components/zha/logbook.py @@ -74,8 +74,8 @@ def async_describe_events( else: message = f"{event_type} event was fired" - if event_data["params"]: - message = f"{message} with parameters: {event_data['params']}" + if params := event_data.get("params"): + message = f"{message} with parameters: {params}" return { LOGBOOK_ENTRY_NAME: device_name, diff --git a/tests/components/zha/test_logbook.py b/tests/components/zha/test_logbook.py index 6c28284b1e6..373a48c2d47 100644 --- a/tests/components/zha/test_logbook.py +++ b/tests/components/zha/test_logbook.py @@ -185,6 +185,27 @@ async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): }, }, ), + MockRow( + ZHA_EVENT, + { + CONF_DEVICE_ID: reg_device.id, + "device_ieee": str(ieee_address), + CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + "params": {}, + }, + ), + MockRow( + ZHA_EVENT, + { + CONF_DEVICE_ID: reg_device.id, + "device_ieee": str(ieee_address), + CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + }, + ), ], ) @@ -201,6 +222,14 @@ async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): events[1]["message"] == "Zha Event was fired with parameters: {'test': 'test'}" ) + assert events[2]["name"] == "FakeManufacturer FakeModel" + assert events[2]["domain"] == "zha" + assert events[2]["message"] == "Zha Event was fired" + + assert events[3]["name"] == "FakeManufacturer FakeModel" + assert events[3]["domain"] == "zha" + assert events[3]["message"] == "Zha Event was fired" + async def test_zha_logbook_event_device_no_device(hass, mock_devices): """Test zha logbook events without device and without triggers.""" From 4f842014ee96506c3198a2da373b91067b8eec9d Mon Sep 17 00:00:00 2001 From: Khole Date: Thu, 30 Jun 2022 22:47:02 +0100 Subject: [PATCH 2050/3516] Add ability to forget hive device when removing integration (#74144) --- homeassistant/components/hive/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 52cf7f719e6..3693f0183ce 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -7,7 +7,7 @@ import logging from typing import Any, TypeVar from aiohttp.web_exceptions import HTTPException -from apyhiveapi import Hive +from apyhiveapi import Auth, Hive from apyhiveapi.helper.hive_exceptions import HiveReauthRequired from typing_extensions import Concatenate, ParamSpec import voluptuous as vol @@ -112,6 +112,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Remove a config entry.""" + hive = Auth(entry.data["username"], entry.data["password"]) + await hive.forget_device( + entry.data["tokens"]["AuthenticationResult"]["AccessToken"], + entry.data["device_data"][1], + ) + + def refresh_system( func: Callable[Concatenate[_HiveEntityT, _P], Awaitable[Any]] ) -> Callable[Concatenate[_HiveEntityT, _P], Coroutine[Any, Any, None]]: From 11cdf542ac169bf72c4a0fb7b06693c9c1e91cfb Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 30 Jun 2022 23:48:50 +0200 Subject: [PATCH 2051/3516] Bump pyRFXtrx to 0.30.0 (#74146) --- homeassistant/components/rfxtrx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/rfxtrx/test_config_flow.py | 12 ++++-- tests/components/rfxtrx/test_cover.py | 37 ++++++++++--------- tests/components/rfxtrx/test_switch.py | 4 +- 6 files changed, 34 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index cfe1049c888..3439fbba70c 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -2,7 +2,7 @@ "domain": "rfxtrx", "name": "RFXCOM RFXtrx", "documentation": "https://www.home-assistant.io/integrations/rfxtrx", - "requirements": ["pyRFXtrx==0.29.0"], + "requirements": ["pyRFXtrx==0.30.0"], "codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 6a515ec37e6..0cb6d5122e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1336,7 +1336,7 @@ pyMetEireann==2021.8.0 pyMetno==0.9.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.29.0 +pyRFXtrx==0.30.0 # homeassistant.components.switchmate # pySwitchmate==0.4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c3f01ecca7..4d60f632108 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -917,7 +917,7 @@ pyMetEireann==2021.8.0 pyMetno==0.9.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.29.0 +pyRFXtrx==0.30.0 # homeassistant.components.tibber pyTibber==0.22.3 diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index a756bf26b9f..2c695d71d2e 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -847,7 +847,7 @@ async def test_options_configure_rfy_cover_device(hass): result["flow_id"], user_input={ "automatic_add": True, - "event_code": "071a000001020301", + "event_code": "0C1a0000010203010000000000", }, ) @@ -863,7 +863,10 @@ async def test_options_configure_rfy_cover_device(hass): await hass.async_block_till_done() - assert entry.data["devices"]["071a000001020301"]["venetian_blind_mode"] == "EU" + assert ( + entry.data["devices"]["0C1a0000010203010000000000"]["venetian_blind_mode"] + == "EU" + ) device_registry = dr.async_get(hass) device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) @@ -897,7 +900,10 @@ async def test_options_configure_rfy_cover_device(hass): await hass.async_block_till_done() - assert entry.data["devices"]["071a000001020301"]["venetian_blind_mode"] == "EU" + assert ( + entry.data["devices"]["0C1a0000010203010000000000"]["venetian_blind_mode"] + == "EU" + ) def test_get_serial_by_id_no_dir(): diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py index e3d44edda82..3be41d9233e 100644 --- a/tests/components/rfxtrx/test_cover.py +++ b/tests/components/rfxtrx/test_cover.py @@ -146,8 +146,11 @@ async def test_rfy_cover(hass, rfxtrx): "071a000001020301": { "venetian_blind_mode": "Unknown", }, - "071a000001020302": {"venetian_blind_mode": "US"}, - "071a000001020303": {"venetian_blind_mode": "EU"}, + "0c1a0000010203010000000000": { + "venetian_blind_mode": "Unknown", + }, + "0c1a0000010203020000000000": {"venetian_blind_mode": "US"}, + "0c1a0000010203030000000000": {"venetian_blind_mode": "EU"}, } ) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) @@ -199,9 +202,9 @@ async def test_rfy_cover(hass, rfxtrx): ) assert rfxtrx.transport.send.mock_calls == [ - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x01\x00")), - call(bytearray(b"\x08\x1a\x00\x01\x01\x02\x03\x01\x01")), - call(bytearray(b"\x08\x1a\x00\x02\x01\x02\x03\x01\x03")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x01\x00\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x01\x01\x02\x03\x01\x01\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x02\x01\x02\x03\x01\x03\x00\x00\x00\x00")), ] # Test a blind with venetian mode set to US @@ -252,12 +255,12 @@ async def test_rfy_cover(hass, rfxtrx): ) assert rfxtrx.transport.send.mock_calls == [ - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x02\x00")), - call(bytearray(b"\x08\x1a\x00\x01\x01\x02\x03\x02\x0F")), - call(bytearray(b"\x08\x1a\x00\x02\x01\x02\x03\x02\x10")), - call(bytearray(b"\x08\x1a\x00\x03\x01\x02\x03\x02\x11")), - call(bytearray(b"\x08\x1a\x00\x04\x01\x02\x03\x02\x12")), - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x02\x00")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x02\x00\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x01\x01\x02\x03\x02\x0F\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x02\x01\x02\x03\x02\x10\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x03\x01\x02\x03\x02\x11\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x04\x01\x02\x03\x02\x12\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x02\x00\x00\x00\x00\x00")), ] # Test a blind with venetian mode set to EU @@ -308,10 +311,10 @@ async def test_rfy_cover(hass, rfxtrx): ) assert rfxtrx.transport.send.mock_calls == [ - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x03\x00")), - call(bytearray(b"\x08\x1a\x00\x01\x01\x02\x03\x03\x11")), - call(bytearray(b"\x08\x1a\x00\x02\x01\x02\x03\x03\x12")), - call(bytearray(b"\x08\x1a\x00\x03\x01\x02\x03\x03\x0F")), - call(bytearray(b"\x08\x1a\x00\x04\x01\x02\x03\x03\x10")), - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x03\x00")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x03\x00\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x01\x01\x02\x03\x03\x11\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x02\x01\x02\x03\x03\x12\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x03\x01\x02\x03\x03\x0F\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x04\x01\x02\x03\x03\x10\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x03\x00\x00\x00\x00\x00")), ] diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index 4da7f1d9881..4d92c6fa332 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -11,8 +11,8 @@ from homeassistant.core import State from tests.common import MockConfigEntry, mock_restore_cache from tests.components.rfxtrx.conftest import create_rfx_test_cfg -EVENT_RFY_ENABLE_SUN_AUTO = "081a00000301010113" -EVENT_RFY_DISABLE_SUN_AUTO = "081a00000301010114" +EVENT_RFY_ENABLE_SUN_AUTO = "0C1a0000030101011300000003" +EVENT_RFY_DISABLE_SUN_AUTO = "0C1a0000030101011400000003" async def test_one_switch(hass, rfxtrx): From 2c171e30fa32a85d8a6a5cd3f3a40433b384c094 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 30 Jun 2022 23:49:23 +0200 Subject: [PATCH 2052/3516] Add ClimateEntity checks to pylint plugin (#74275) * Add ClimateEntity checks to pylint plugin * Update pylint/plugins/hass_enforce_type_hints.py --- pylint/plugins/hass_enforce_type_hints.py | 173 ++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 36c198c7218..8e846cf5db0 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -856,6 +856,179 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "climate": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="ClimateEntity", + matches=[ + TypeHintMatch( + function_name="precision", + return_type="float", + ), + TypeHintMatch( + function_name="temperature_unit", + return_type="str", + ), + TypeHintMatch( + function_name="current_humidity", + return_type=["int", None], + ), + TypeHintMatch( + function_name="target_humidity", + return_type=["int", None], + ), + TypeHintMatch( + function_name="hvac_mode", + return_type=["HVACMode", "str", None], + ), + TypeHintMatch( + function_name="hvac_modes", + return_type=["list[HVACMode]", "list[str]"], + ), + TypeHintMatch( + function_name="hvac_action", + return_type=["HVACAction", "str", None], + ), + TypeHintMatch( + function_name="current_temperature", + return_type=["float", None], + ), + TypeHintMatch( + function_name="target_temperature", + return_type=["float", None], + ), + TypeHintMatch( + function_name="target_temperature_step", + return_type=["float", None], + ), + TypeHintMatch( + function_name="target_temperature_high", + return_type=["float", None], + ), + TypeHintMatch( + function_name="target_temperature_low", + return_type=["float", None], + ), + TypeHintMatch( + function_name="preset_mode", + return_type=["str", None], + ), + TypeHintMatch( + function_name="preset_modes", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="is_aux_heat", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="fan_mode", + return_type=["str", None], + ), + TypeHintMatch( + function_name="fan_modes", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="swing_mode", + return_type=["str", None], + ), + TypeHintMatch( + function_name="swing_modes", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="set_temperature", + kwargs_type="Any", + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_humidity", + arg_types={ + 1: "int", + }, + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_fan_mode", + arg_types={ + 1: "str", + }, + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_hvac_mode", + arg_types={ + 1: "HVACMode", + }, + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_swing_mode", + arg_types={ + 1: "str", + }, + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_preset_mode", + arg_types={ + 1: "str", + }, + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_aux_heat_on", + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_aux_heat_off", + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_on", + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_off", + return_type="None", + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="supported_features", + return_type="int", + ), + TypeHintMatch( + function_name="min_temp", + return_type="float", + ), + TypeHintMatch( + function_name="max_temp", + return_type="float", + ), + TypeHintMatch( + function_name="min_humidity", + return_type="int", + ), + TypeHintMatch( + function_name="max_humidity", + return_type="int", + ), + ], + ), + ], "cover": [ ClassTypeHintMatch( base_class="Entity", From 73a0197cac501c428bdf54bd23fe70f7e80b4b8d Mon Sep 17 00:00:00 2001 From: Alberto Geniola Date: Thu, 30 Jun 2022 23:55:57 +0200 Subject: [PATCH 2053/3516] Elmax/sensor platform (#64090) --- .coveragerc | 1 + .../components/elmax/binary_sensor.py | 68 +++++++++++++++++++ homeassistant/components/elmax/common.py | 6 ++ homeassistant/components/elmax/const.py | 2 +- 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/elmax/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 2443e30a2c3..f12d1ce88fa 100644 --- a/.coveragerc +++ b/.coveragerc @@ -276,6 +276,7 @@ omit = homeassistant/components/elmax/__init__.py homeassistant/components/elmax/common.py homeassistant/components/elmax/const.py + homeassistant/components/elmax/binary_sensor.py homeassistant/components/elmax/switch.py homeassistant/components/elv/* homeassistant/components/emby/media_player.py diff --git a/homeassistant/components/elmax/binary_sensor.py b/homeassistant/components/elmax/binary_sensor.py new file mode 100644 index 00000000000..71588b4687f --- /dev/null +++ b/homeassistant/components/elmax/binary_sensor.py @@ -0,0 +1,68 @@ +"""Elmax sensor platform.""" +from __future__ import annotations + +from elmax_api.model.panel import PanelStatus + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ElmaxCoordinator +from .common import ElmaxEntity +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Elmax sensor platform.""" + coordinator: ElmaxCoordinator = hass.data[DOMAIN][config_entry.entry_id] + known_devices = set() + + def _discover_new_devices(): + panel_status: PanelStatus = coordinator.data + # In case the panel is offline, its status will be None. In that case, simply do nothing + if panel_status is None: + return + + # Otherwise, add all the entities we found + entities = [] + for zone in panel_status.zones: + # Skip already handled devices + if zone.endpoint_id in known_devices: + continue + entity = ElmaxSensor( + panel=coordinator.panel_entry, + elmax_device=zone, + panel_version=panel_status.release, + coordinator=coordinator, + ) + entities.append(entity) + async_add_entities(entities, True) + known_devices.update([e.unique_id for e in entities]) + + # Register a listener for the discovery of new devices + coordinator.async_add_listener(_discover_new_devices) + + # Immediately run a discovery, so we don't need to wait for the next update + _discover_new_devices() + + +class ElmaxSensor(ElmaxEntity, BinarySensorEntity): + """Elmax Sensor entity implementation.""" + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return self.coordinator.get_zone_state(self._device.endpoint_id).opened + + @property + def device_class(self) -> BinarySensorDeviceClass: + """Return the class of this device, from component DEVICE_CLASSES.""" + return BinarySensorDeviceClass.DOOR diff --git a/homeassistant/components/elmax/common.py b/homeassistant/components/elmax/common.py index 2d66ca9f72e..4116ff05f44 100644 --- a/homeassistant/components/elmax/common.py +++ b/homeassistant/components/elmax/common.py @@ -65,6 +65,12 @@ class ElmaxCoordinator(DataUpdateCoordinator[PanelStatus]): return self._state_by_endpoint.get(actuator_id) raise HomeAssistantError("Unknown actuator") + def get_zone_state(self, zone_id: str) -> Actuator: + """Return state of a specific zone.""" + if self._state_by_endpoint is not None: + return self._state_by_endpoint.get(zone_id) + raise HomeAssistantError("Unknown zone") + @property def http_client(self): """Return the current http client being used by this instance.""" diff --git a/homeassistant/components/elmax/const.py b/homeassistant/components/elmax/const.py index 21864e98f1a..514412d6897 100644 --- a/homeassistant/components/elmax/const.py +++ b/homeassistant/components/elmax/const.py @@ -11,7 +11,7 @@ CONF_ELMAX_PANEL_NAME = "panel_name" CONF_CONFIG_ENTRY_ID = "config_entry_id" CONF_ENDPOINT_ID = "endpoint_id" -ELMAX_PLATFORMS = [Platform.SWITCH] +ELMAX_PLATFORMS = [Platform.SWITCH, Platform.BINARY_SENSOR] POLLING_SECONDS = 30 DEFAULT_TIMEOUT = 10.0 From 7eae3691c2d219b30a9c2a7d964cf82f6e8d1798 Mon Sep 17 00:00:00 2001 From: rappenze Date: Thu, 30 Jun 2022 23:57:35 +0200 Subject: [PATCH 2054/3516] Add device info to fibaro integration (#73352) --- homeassistant/components/fibaro/__init__.py | 74 +++++++++++++++++-- .../components/fibaro/config_flow.py | 8 +- homeassistant/components/fibaro/scene.py | 10 +++ tests/components/fibaro/test_config_flow.py | 12 ++- 4 files changed, 93 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index bd7c3a09ec0..3b5eece1a14 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -29,8 +29,9 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +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 homeassistant.helpers.typing import ConfigType from homeassistant.util import slugify @@ -156,16 +157,21 @@ class FibaroController: ) # List of devices by entity platform self._callbacks: dict[Any, Any] = {} # Update value callbacks by deviceId self._state_handler = None # Fiblary's StateHandler object - self.hub_serial = None # Unique serial number of the hub - self.name = None # The friendly name of the hub + self.hub_serial: str # Unique serial number of the hub + self.hub_name: str # The friendly name of the hub + self.hub_software_version: str + self.hub_api_url: str = config[CONF_URL] + # Device infos by fibaro device id + self._device_infos: dict[int, DeviceInfo] = {} def connect(self): """Start the communication with the Fibaro controller.""" try: login = self._client.login.get() info = self._client.info.get() - self.hub_serial = slugify(info.serialNumber) - self.name = slugify(info.hcName) + self.hub_serial = info.serialNumber + self.hub_name = info.hcName + self.hub_software_version = info.softVersion except AssertionError: _LOGGER.error("Can't connect to Fibaro HC. Please check URL") return False @@ -305,6 +311,44 @@ class FibaroController: platform = Platform.LIGHT return platform + def _create_device_info(self, device: Any, devices: list) -> None: + """Create the device info. Unrooted entities are directly shown below the home center.""" + + # The home center is always id 1 (z-wave primary controller) + if "parentId" not in device or device.parentId <= 1: + return + + master_entity: Any | None = None + if device.parentId == 1: + master_entity = device + else: + for parent in devices: + if "id" in parent and parent.id == device.parentId: + master_entity = parent + if master_entity is None: + _LOGGER.error("Parent with id %s not found", device.parentId) + return + + if "zwaveCompany" in master_entity.properties: + manufacturer = master_entity.properties.zwaveCompany + else: + manufacturer = "Unknown" + + self._device_infos[master_entity.id] = DeviceInfo( + identifiers={(DOMAIN, master_entity.id)}, + manufacturer=manufacturer, + name=master_entity.name, + via_device=(DOMAIN, self.hub_serial), + ) + + def get_device_info(self, device: Any) -> DeviceInfo: + """Get the device info by fibaro device id.""" + if device.id in self._device_infos: + return self._device_infos[device.id] + if "parentId" in device and device.parentId in self._device_infos: + return self._device_infos[device.parentId] + return DeviceInfo(identifiers={(DOMAIN, self.hub_serial)}) + def _read_scenes(self): scenes = self._client.scenes.list() self._scene_map = {} @@ -321,14 +365,14 @@ class FibaroController: device.ha_id = ( f"scene_{slugify(room_name)}_{slugify(device.name)}_{device.id}" ) - device.unique_id_str = f"{self.hub_serial}.scene.{device.id}" + device.unique_id_str = f"{slugify(self.hub_serial)}.scene.{device.id}" self._scene_map[device.id] = device self.fibaro_devices[Platform.SCENE].append(device) _LOGGER.debug("%s scene -> %s", device.ha_id, device) def _read_devices(self): """Read and process the device list.""" - devices = self._client.devices.list() + devices = list(self._client.devices.list()) self._device_map = {} last_climate_parent = None last_endpoint = None @@ -355,7 +399,8 @@ class FibaroController: device.mapped_platform = None if (platform := device.mapped_platform) is None: continue - device.unique_id_str = f"{self.hub_serial}.{device.id}" + device.unique_id_str = f"{slugify(self.hub_serial)}.{device.id}" + self._create_device_info(device, devices) self._device_map[device.id] = device _LOGGER.debug( "%s (%s, %s) -> %s %s", @@ -462,6 +507,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for platform in PLATFORMS: devices[platform] = [*controller.fibaro_devices[platform]] + # register the hub device info separately as the hub has sometimes no entities + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, controller.hub_serial)}, + manufacturer="Fibaro", + name=controller.hub_name, + model=controller.hub_serial, + sw_version=controller.hub_software_version, + configuration_url=controller.hub_api_url.removesuffix("/api/"), + ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) controller.enable_state_handler() @@ -490,6 +547,7 @@ class FibaroDevice(Entity): self.ha_id = fibaro_device.ha_id self._attr_name = fibaro_device.friendly_name self._attr_unique_id = fibaro_device.unique_id_str + self._attr_device_info = self.controller.get_device_info(fibaro_device) # propagate hidden attribute set in fibaro home center to HA if "visible" in fibaro_device and fibaro_device.visible is False: self._attr_entity_registry_visible_default = False diff --git a/homeassistant/components/fibaro/config_flow.py b/homeassistant/components/fibaro/config_flow.py index b0ea05e49e1..fd53bd5b94f 100644 --- a/homeassistant/components/fibaro/config_flow.py +++ b/homeassistant/components/fibaro/config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging from typing import Any +from slugify import slugify import voluptuous as vol from homeassistant import config_entries @@ -44,9 +45,12 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str _LOGGER.debug( "Successfully connected to fibaro home center %s with name %s", controller.hub_serial, - controller.name, + controller.hub_name, ) - return {"serial_number": controller.hub_serial, "name": controller.name} + return { + "serial_number": slugify(controller.hub_serial), + "name": controller.hub_name, + } class FibaroConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/fibaro/scene.py b/homeassistant/components/fibaro/scene.py index e4e8b19d308..045adce5764 100644 --- a/homeassistant/components/fibaro/scene.py +++ b/homeassistant/components/fibaro/scene.py @@ -7,6 +7,7 @@ from homeassistant.components.scene import Scene from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import FIBARO_DEVICES, FibaroDevice @@ -33,6 +34,15 @@ async def async_setup_entry( class FibaroScene(FibaroDevice, Scene): """Representation of a Fibaro scene entity.""" + def __init__(self, fibaro_device: Any) -> None: + """Initialize the Fibaro scene.""" + super().__init__(fibaro_device) + + # All scenes are shown on hub device + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.controller.hub_serial)} + ) + def activate(self, **kwargs: Any) -> None: """Activate the scene.""" self.fibaro_device.start() diff --git a/tests/components/fibaro/test_config_flow.py b/tests/components/fibaro/test_config_flow.py index f056f484a58..14f28257588 100644 --- a/tests/components/fibaro/test_config_flow.py +++ b/tests/components/fibaro/test_config_flow.py @@ -14,17 +14,23 @@ TEST_NAME = "my_fibaro_home_center" TEST_URL = "http://192.168.1.1/api/" TEST_USERNAME = "user" TEST_PASSWORD = "password" +TEST_VERSION = "4.360" @pytest.fixture(name="fibaro_client", autouse=True) def fibaro_client_fixture(): """Mock common methods and attributes of fibaro client.""" info_mock = Mock() - info_mock.get.return_value = Mock(serialNumber=TEST_SERIALNUMBER, hcName=TEST_NAME) + info_mock.get.return_value = Mock( + serialNumber=TEST_SERIALNUMBER, hcName=TEST_NAME, softVersion=TEST_VERSION + ) array_mock = Mock() array_mock.list.return_value = [] + client_mock = Mock() + client_mock.base_url.return_value = TEST_URL + with patch("fiblary3.client.v4.client.Client.__init__", return_value=None,), patch( "fiblary3.client.v4.client.Client.info", info_mock, @@ -37,6 +43,10 @@ def fibaro_client_fixture(): "fiblary3.client.v4.client.Client.scenes", array_mock, create=True, + ), patch( + "fiblary3.client.v4.client.Client.client", + client_mock, + create=True, ): yield From 3970639c3410394631aeb2d3d2f56a86873da802 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 1 Jul 2022 00:27:03 +0000 Subject: [PATCH 2055/3516] [ci skip] Translation update --- .../components/anthemav/translations/de.json | 19 ++++++++++++++ .../components/anthemav/translations/et.json | 19 ++++++++++++++ .../components/anthemav/translations/fr.json | 2 +- .../anthemav/translations/zh-Hant.json | 19 ++++++++++++++ .../components/braviatv/translations/de.json | 2 +- .../components/deconz/translations/de.json | 2 +- .../components/deluge/translations/de.json | 2 +- .../components/esphome/translations/de.json | 4 +-- .../components/esphome/translations/en.json | 4 +-- .../components/esphome/translations/fr.json | 4 +-- .../forked_daapd/translations/de.json | 2 +- .../components/lcn/translations/et.json | 1 + .../lg_soundbar/translations/de.json | 18 +++++++++++++ .../lg_soundbar/translations/fr.json | 18 +++++++++++++ .../components/life360/translations/de.json | 2 +- .../components/life360/translations/el.json | 23 +++++++++++++++++ .../components/life360/translations/et.json | 23 +++++++++++++++++ .../components/life360/translations/ja.json | 23 +++++++++++++++++ .../components/life360/translations/nl.json | 9 +++++++ .../components/life360/translations/no.json | 25 ++++++++++++++++++- .../components/mutesync/translations/de.json | 2 +- .../components/nina/translations/et.json | 22 ++++++++++++++++ .../components/nina/translations/ja.json | 1 + .../components/nina/translations/nl.json | 11 ++++++++ .../components/nina/translations/no.json | 22 ++++++++++++++++ .../components/owntracks/translations/de.json | 2 +- .../simplepush/translations/nl.json | 3 +++ .../smartthings/translations/de.json | 4 +-- .../components/starline/translations/de.json | 2 +- .../components/threshold/translations/sv.json | 3 ++- .../components/zwave_me/translations/de.json | 2 +- 31 files changed, 275 insertions(+), 20 deletions(-) create mode 100644 homeassistant/components/anthemav/translations/de.json create mode 100644 homeassistant/components/anthemav/translations/et.json create mode 100644 homeassistant/components/anthemav/translations/zh-Hant.json create mode 100644 homeassistant/components/lg_soundbar/translations/de.json create mode 100644 homeassistant/components/lg_soundbar/translations/fr.json diff --git a/homeassistant/components/anthemav/translations/de.json b/homeassistant/components/anthemav/translations/de.json new file mode 100644 index 00000000000..622384629fe --- /dev/null +++ b/homeassistant/components/anthemav/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "cannot_receive_deviceinfo": "MAC-Adresse konnte nicht abgerufen werden. Stelle sicher, dass das Ger\u00e4t eingeschaltet ist." + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/et.json b/homeassistant/components/anthemav/translations/et.json new file mode 100644 index 00000000000..4ec356c8902 --- /dev/null +++ b/homeassistant/components/anthemav/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "cannot_receive_deviceinfo": "MAC-aadressi toomine eba\u00f5nnestus. Veendu, et seade oleks sisse l\u00fclitatud" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/fr.json b/homeassistant/components/anthemav/translations/fr.json index cf1e7c7aded..faf417552ce 100644 --- a/homeassistant/components/anthemav/translations/fr.json +++ b/homeassistant/components/anthemav/translations/fr.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "cannot_receive_deviceinfo": "Erreur lors de la d\u00e9couverte de l'addresse MAC. V\u00e9rifiez que l'appareil est allum\u00e9." + "cannot_receive_deviceinfo": "\u00c9chec de r\u00e9cup\u00e9ration de l'adresse MAC. Assurez-vous que l'appareil est allum\u00e9" }, "step": { "user": { diff --git a/homeassistant/components/anthemav/translations/zh-Hant.json b/homeassistant/components/anthemav/translations/zh-Hant.json new file mode 100644 index 00000000000..d1b286afd81 --- /dev/null +++ b/homeassistant/components/anthemav/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "cannot_receive_deviceinfo": "\u63a5\u6536 MAC \u4f4d\u5740\u5931\u6557\uff0c\u8acb\u78ba\u5b9a\u88dd\u7f6e\u70ba\u958b\u555f\u72c0Address. Make sure the device is turned on" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json index ff00828c0f3..035de7bc060 100644 --- a/homeassistant/components/braviatv/translations/de.json +++ b/homeassistant/components/braviatv/translations/de.json @@ -14,7 +14,7 @@ "data": { "pin": "PIN-Code" }, - "description": "Gib den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, musst du die Registrierung von Home Assistant auf deinem Fernseher aufheben, gehe daf\u00fcr zu: Einstellungen -> Netzwerk -> Remote - Ger\u00e4teeinstellungen -> Registrierung des entfernten Ger\u00e4ts aufheben.", + "description": "Gib den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, musst du die Registrierung von Home Assistant auf deinem Fernseher aufheben, gehe daf\u00fcr zu: Einstellungen \u2192 Netzwerk \u2192 Remote - Ger\u00e4teeinstellungen \u2192 Registrierung des entfernten Ger\u00e4ts aufheben.", "title": "Autorisiere Sony Bravia TV" }, "user": { diff --git a/homeassistant/components/deconz/translations/de.json b/homeassistant/components/deconz/translations/de.json index 29b322466d5..b8b260709e8 100644 --- a/homeassistant/components/deconz/translations/de.json +++ b/homeassistant/components/deconz/translations/de.json @@ -18,7 +18,7 @@ "title": "deCONZ Zigbee Gateway \u00fcber das Supervisor Add-on" }, "link": { - "description": "Entsperre dein deCONZ-Gateway, um es bei Home Assistant zu registrieren. \n\n 1. Gehe in die deCONZ-Systemeinstellungen \n 2. Dr\u00fccke die Taste \"Gateway entsperren\"", + "description": "Entsperre dein deCONZ-Gateway, um es bei Home Assistant zu registrieren. \n\n 1. Gehe in die deCONZ-Systemeinstellungen \u2192 Gateway \u2192 Erweitert\n 2. Dr\u00fccke die Taste \"Gateway entsperren\"", "title": "Mit deCONZ verbinden" }, "manual_input": { diff --git a/homeassistant/components/deluge/translations/de.json b/homeassistant/components/deluge/translations/de.json index 9e8d559a523..4fa07b82d0f 100644 --- a/homeassistant/components/deluge/translations/de.json +++ b/homeassistant/components/deluge/translations/de.json @@ -16,7 +16,7 @@ "username": "Benutzername", "web_port": "Webport (f\u00fcr Besuchsdienste)" }, - "description": "Um diese Integration nutzen zu k\u00f6nnen, musst du die folgende Option in den Deluge-Einstellungen aktivieren: Daemon > Fernsteuerungen zulassen" + "description": "Um diese Integration nutzen zu k\u00f6nnen, musst du die folgende Option in den Deluge-Einstellungen aktivieren: Daemon \u2192 Fernsteuerungen zulassen" } } } diff --git a/homeassistant/components/esphome/translations/de.json b/homeassistant/components/esphome/translations/de.json index 6229c09a03e..7556739ce93 100644 --- a/homeassistant/components/esphome/translations/de.json +++ b/homeassistant/components/esphome/translations/de.json @@ -9,7 +9,7 @@ "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" + "resolve_error": "Adresse des ESP kann nicht aufgel\u00f6st werden. Wenn dieser Fehler weiterhin besteht, lege bitte eine statische IP-Adresse fest" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Host", "port": "Port" }, - "description": "Bitte gib die Verbindungseinstellungen deines [ESPHome](https://esphomelib.com/)-Knotens ein." + "description": "Bitte gib die Verbindungseinstellungen deines [ESPHome]( {esphome_url} )-Knotens ein." } } } diff --git a/homeassistant/components/esphome/translations/en.json b/homeassistant/components/esphome/translations/en.json index 5ca5c03f8e9..b0b502631df 100644 --- a/homeassistant/components/esphome/translations/en.json +++ b/homeassistant/components/esphome/translations/en.json @@ -9,7 +9,7 @@ "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" + "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Host", "port": "Port" }, - "description": "Please enter connection settings of your [ESPHome](https://esphomelib.com/) node." + "description": "Please enter connection settings of your [ESPHome]({esphome_url}) node." } } } diff --git a/homeassistant/components/esphome/translations/fr.json b/homeassistant/components/esphome/translations/fr.json index 330c2823409..8ac9feb8a93 100644 --- a/homeassistant/components/esphome/translations/fr.json +++ b/homeassistant/components/esphome/translations/fr.json @@ -9,7 +9,7 @@ "connection_error": "Impossible de se connecter \u00e0 ESP. Assurez-vous que votre fichier YAML contient une ligne 'api:'.", "invalid_auth": "Authentification non valide", "invalid_psk": "La cl\u00e9 de chiffrement de transport n\u2019est pas valide. Assurez-vous qu\u2019elle correspond \u00e0 ce que vous avez dans votre configuration", - "resolve_error": "Impossible de r\u00e9soudre l'adresse de l'ESP. Si cette erreur persiste, veuillez d\u00e9finir une adresse IP statique: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "Impossible de r\u00e9soudre l'adresse de l'ESP. Si cette erreur persiste, essayez de d\u00e9finir une adresse IP statique" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "H\u00f4te", "port": "Port" }, - "description": "Veuillez saisir les param\u00e8tres de connexion de votre n\u0153ud [ESPHome] (https://esphomelib.com/)." + "description": "Veuillez saisir les param\u00e8tres de connexion de votre n\u0153ud [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/forked_daapd/translations/de.json b/homeassistant/components/forked_daapd/translations/de.json index 51fd312fd6d..984358f02ba 100644 --- a/homeassistant/components/forked_daapd/translations/de.json +++ b/homeassistant/components/forked_daapd/translations/de.json @@ -10,7 +10,7 @@ "websocket_not_enabled": "Forked-Daapd-Server-Websocket nicht aktiviert.", "wrong_host_or_port": "Verbindung konnte nicht hergestellt werden. Bitte Host und Port pr\u00fcfen.", "wrong_password": "Ung\u00fcltiges Passwort", - "wrong_server_type": "F\u00fcr die forked-daapd Integration ist ein forked-daapd Server mit der Version > = 27.0 erforderlich." + "wrong_server_type": "F\u00fcr die forked-daapd Integration ist ein forked-daapd Server mit der Version >= 27.0 erforderlich." }, "flow_title": "{name} ({host})", "step": { diff --git a/homeassistant/components/lcn/translations/et.json b/homeassistant/components/lcn/translations/et.json index 058873e63c5..e390ed3a2f3 100644 --- a/homeassistant/components/lcn/translations/et.json +++ b/homeassistant/components/lcn/translations/et.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "koodluku kood vastu v\u00f5etud", "fingerprint": "vastu v\u00f5etud s\u00f5rmej\u00e4ljekood", "send_keys": "vastuv\u00f5etud v\u00f5tmete saatmine", "transmitter": "saatja kood vastu v\u00f5etud", diff --git a/homeassistant/components/lg_soundbar/translations/de.json b/homeassistant/components/lg_soundbar/translations/de.json new file mode 100644 index 00000000000..a840fb04abe --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/fr.json b/homeassistant/components/lg_soundbar/translations/fr.json new file mode 100644 index 00000000000..b13f3d0d595 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/de.json b/homeassistant/components/life360/translations/de.json index 67f014e0a2c..9e6e819a179 100644 --- a/homeassistant/components/life360/translations/de.json +++ b/homeassistant/components/life360/translations/de.json @@ -29,7 +29,7 @@ "username": "Benutzername" }, "description": "Erweiterte Optionen sind in der [Life360-Dokumentation]({docs_url}) zu finden.\nDies sollte vor dem Hinzuf\u00fcgen von Kontoinformationen getan werden.", - "title": "Life360-Kontoinformationen" + "title": "Life360-Konto konfigurieren" } } }, diff --git a/homeassistant/components/life360/translations/el.json b/homeassistant/components/life360/translations/el.json index 07106d89d63..f0db8e10ed6 100644 --- a/homeassistant/components/life360/translations/el.json +++ b/homeassistant/components/life360/translations/el.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "create_entry": { @@ -9,11 +11,18 @@ }, "error": { "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "invalid_username": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, "user": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", @@ -23,5 +32,19 @@ "title": "\u03a0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Life360" } } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "\u0395\u03bc\u03c6\u03ac\u03bd\u03b9\u03c3\u03b7 \u03bf\u03b4\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2 \u03c9\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7", + "driving_speed": "\u03a4\u03b1\u03c7\u03cd\u03c4\u03b7\u03c4\u03b1 \u03bf\u03b4\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2", + "limit_gps_acc": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c4\u03b7\u03c2 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1\u03c2 GPS", + "max_gps_accuracy": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03b7 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 GPS (\u03bc\u03ad\u03c4\u03c1\u03b1)", + "set_drive_speed": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf \u03cc\u03c1\u03b9\u03bf \u03c4\u03b1\u03c7\u03cd\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bf\u03b4\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/et.json b/homeassistant/components/life360/translations/et.json index d9cbbbb30f5..c6e66af5d08 100644 --- a/homeassistant/components/life360/translations/et.json +++ b/homeassistant/components/life360/translations/et.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Konto on juba h\u00e4\u00e4lestatud", "invalid_auth": "Tuvastamise viga", + "reauth_successful": "Taastuvastamine \u00f5nnestus", "unknown": "Ootamatu t\u00f5rge" }, "create_entry": { @@ -9,11 +11,18 @@ }, "error": { "already_configured": "Kasutaja on juba seadistatud", + "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamise viga", "invalid_username": "Vale kasutajanimi", "unknown": "Ootamatu t\u00f5rge" }, "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "title": "Taastuvasta sidumine" + }, "user": { "data": { "password": "Salas\u00f5na", @@ -23,5 +32,19 @@ "title": "Life360 konto teave" } } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Kuva s\u00f5itmist olekuna", + "driving_speed": "S\u00f5idukiirus", + "limit_gps_acc": "GPS-i t\u00e4psuse piiramine", + "max_gps_accuracy": "GPS-i maksimaalne t\u00e4psus (meetrites)", + "set_drive_speed": "M\u00e4\u00e4ra s\u00f5idukiiruse l\u00e4vi" + }, + "title": "Konto suvandid" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/ja.json b/homeassistant/components/life360/translations/ja.json index 772b44b31d8..4776e3409ea 100644 --- a/homeassistant/components/life360/translations/ja.json +++ b/homeassistant/components/life360/translations/ja.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "create_entry": { @@ -9,11 +11,18 @@ }, "error": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_username": "\u7121\u52b9\u306a\u30e6\u30fc\u30b6\u30fc\u540d", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", @@ -23,5 +32,19 @@ "title": "Life360\u30a2\u30ab\u30a6\u30f3\u30c8\u60c5\u5831" } } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "\u30c9\u30e9\u30a4\u30d3\u30f3\u30b0\u3092\u72b6\u614b\u3068\u3057\u3066\u8868\u793a\u3059\u308b", + "driving_speed": "\u8d70\u884c\u901f\u5ea6", + "limit_gps_acc": "GPS\u306e\u7cbe\u5ea6\u3092\u5236\u9650\u3059\u308b", + "max_gps_accuracy": "GPS\u306e\u6700\u5927\u7cbe\u5ea6(\u30e1\u30fc\u30c8\u30eb)", + "set_drive_speed": "\u8d70\u884c\u901f\u5ea6\u306e\u3057\u304d\u3044\u5024\u3092\u8a2d\u5b9a\u3059\u308b" + }, + "title": "\u30a2\u30ab\u30a6\u30f3\u30c8\u30aa\u30d7\u30b7\u30e7\u30f3" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/nl.json b/homeassistant/components/life360/translations/nl.json index 612b0d5c4f7..cdcc937a33e 100644 --- a/homeassistant/components/life360/translations/nl.json +++ b/homeassistant/components/life360/translations/nl.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Account is al geconfigureerd", "invalid_auth": "Ongeldige authenticatie", + "reauth_successful": "Herauthenticatie geslaagd", "unknown": "Onverwachte fout" }, "create_entry": { @@ -9,11 +11,18 @@ }, "error": { "already_configured": "Account is al geconfigureerd", + "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "invalid_username": "Ongeldige gebruikersnaam", "unknown": "Onverwachte fout" }, "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "title": "Integratie herauthenticeren" + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/life360/translations/no.json b/homeassistant/components/life360/translations/no.json index 9a95a976657..7213a665607 100644 --- a/homeassistant/components/life360/translations/no.json +++ b/homeassistant/components/life360/translations/no.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Kontoen er allerede konfigurert", "invalid_auth": "Ugyldig godkjenning", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "unknown": "Uventet feil" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "Kontoen er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", "invalid_username": "Ugyldig brukernavn", "unknown": "Uventet feil" }, "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "title": "Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "password": "Passord", "username": "Brukernavn" }, "description": "For \u00e5 angi avanserte alternativer, se [Life360 dokumentasjon]({docs_url}). \nDet kan hende du vil gj\u00f8re det f\u00f8r du legger til kontoer.", - "title": "Life360 Kontoinformasjon" + "title": "Konfigurer Life360-konto" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Vis kj\u00f8ring som tilstand", + "driving_speed": "Kj\u00f8rehastighet", + "limit_gps_acc": "Begrens GPS-n\u00f8yaktigheten", + "max_gps_accuracy": "Maksimal GPS-n\u00f8yaktighet (meter)", + "set_drive_speed": "Still inn kj\u00f8rehastighetsterskel" + }, + "title": "Kontoalternativer" } } } diff --git a/homeassistant/components/mutesync/translations/de.json b/homeassistant/components/mutesync/translations/de.json index dccab9e8d1e..9ff12a5fad5 100644 --- a/homeassistant/components/mutesync/translations/de.json +++ b/homeassistant/components/mutesync/translations/de.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "invalid_auth": "Aktiviere die Authentifizierung in den Einstellungen von m\u00fctesync > Authentifizierung", + "invalid_auth": "Aktiviere die Authentifizierung in den Einstellungen von m\u00fctesync \u2192 Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/nina/translations/et.json b/homeassistant/components/nina/translations/et.json index db454b7996c..4eb59ab43ae 100644 --- a/homeassistant/components/nina/translations/et.json +++ b/homeassistant/components/nina/translations/et.json @@ -23,5 +23,27 @@ "title": "Vali linn/maakond" } } + }, + "options": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "no_selection": "Vali v\u00e4hemalt \u00fcks linn/maakond", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Linn/maakond (A-D)", + "_e_to_h": "Linn/maakond (E-H)", + "_i_to_l": "Linn/maakond (I-L)", + "_m_to_q": "Linn/maakond (M-Q)", + "_r_to_u": "Linn/maakond (R-U)", + "_v_to_z": "Linn/maakond (V-Z)", + "corona_filter": "Eemalda koroonahoiatused", + "slots": "Maksimaalne hoiatuste arv linna/maakonna kohta" + }, + "title": "Valikud" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/ja.json b/homeassistant/components/nina/translations/ja.json index 5e3978f4777..fa3fa6f4995 100644 --- a/homeassistant/components/nina/translations/ja.json +++ b/homeassistant/components/nina/translations/ja.json @@ -34,6 +34,7 @@ "init": { "data": { "_a_to_d": "City/county (A-D)", + "_e_to_h": "City/county (E-H)", "_i_to_l": "City/county (I-L)", "_m_to_q": "City/county (M-Q)", "_r_to_u": "City/county (R-U)", diff --git a/homeassistant/components/nina/translations/nl.json b/homeassistant/components/nina/translations/nl.json index 4b0100f9f07..1b407576bbd 100644 --- a/homeassistant/components/nina/translations/nl.json +++ b/homeassistant/components/nina/translations/nl.json @@ -23,5 +23,16 @@ "title": "Selecteer stad/provincie" } } + }, + "options": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "step": { + "init": { + "title": "Opties" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/no.json b/homeassistant/components/nina/translations/no.json index a4e062a7812..6a13c698941 100644 --- a/homeassistant/components/nina/translations/no.json +++ b/homeassistant/components/nina/translations/no.json @@ -23,5 +23,27 @@ "title": "Velg by/fylke" } } + }, + "options": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "no_selection": "Velg minst \u00e9n by/fylke", + "unknown": "Uventet feil" + }, + "step": { + "init": { + "data": { + "_a_to_d": "By/fylke (AD)", + "_e_to_h": "By/fylke (EH)", + "_i_to_l": "By/fylke (IL)", + "_m_to_q": "By/fylke (MQ)", + "_r_to_u": "By/fylke (RU)", + "_v_to_z": "By/fylke (VZ)", + "corona_filter": "Fjern koronaadvarsler", + "slots": "Maksimal advarsler per by/fylke" + }, + "title": "Alternativer" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/owntracks/translations/de.json b/homeassistant/components/owntracks/translations/de.json index 46ca14c81ac..d02571a6a50 100644 --- a/homeassistant/components/owntracks/translations/de.json +++ b/homeassistant/components/owntracks/translations/de.json @@ -5,7 +5,7 @@ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "create_entry": { - "default": "\n\nUnter Android \u00f6ffne [die OwnTracks App]({android_url}), gehe zu Einstellungen -> Verbindung. \u00c4ndere die folgenden Einstellungen:\n - Modus: Privat HTTP\n - Host: {webhook_url}\n - Identifikation:\n - Benutzername: `''`\n - Ger\u00e4te-ID: `''`\n\nUnter iOS \u00f6ffne [die OwnTracks App]({ios_url}), tippe auf das (i)-Symbol oben links -> Einstellungen. \u00c4ndere die folgenden Einstellungen:\n - Modus: HTTP\n - URL: {webhook_url}\n - Authentifizierung einschalten\n - UserID: `''`\n\n{secret}\n\nWeitere Informationen findest du in [der Dokumentation]({docs_url})." + "default": "\n\nUnter Android \u00f6ffne [die OwnTracks App]({android_url}), gehe zu Einstellungen \u2192 Verbindung. \u00c4ndere die folgenden Einstellungen:\n - Modus: Privat HTTP\n - Host: {webhook_url}\n - Identifikation:\n - Benutzername: `''`\n - Ger\u00e4te-ID: `''`\n\nUnter iOS \u00f6ffne [die OwnTracks App]({ios_url}), tippe auf das (i)-Symbol oben links \u2192 Einstellungen. \u00c4ndere die folgenden Einstellungen:\n - Modus: HTTP\n - URL: {webhook_url}\n - Authentifizierung einschalten\n - UserID: `''`\n\n{secret}\n\nWeitere Informationen findest du in [der Dokumentation]({docs_url})." }, "step": { "user": { diff --git a/homeassistant/components/simplepush/translations/nl.json b/homeassistant/components/simplepush/translations/nl.json index 900bac61bc5..ee691ac3901 100644 --- a/homeassistant/components/simplepush/translations/nl.json +++ b/homeassistant/components/simplepush/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/smartthings/translations/de.json b/homeassistant/components/smartthings/translations/de.json index 6cd7157b702..b6a97013784 100644 --- a/homeassistant/components/smartthings/translations/de.json +++ b/homeassistant/components/smartthings/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "invalid_webhook_url": "Home Assistant ist nicht richtig konfiguriert, um Updates von SmartThings zu erhalten. Die Webhook-URL ist ung\u00fcltig: \n > {webhook_url} \n\nBitte aktualisiere deine Konfiguration gem\u00e4\u00df den [Anweisungen] ({component_url}), starte den Home Assistant neu und versuche es erneut.", + "invalid_webhook_url": "Home Assistant ist nicht richtig konfiguriert, um Updates von SmartThings zu erhalten. Die Webhook-URL ist ung\u00fcltig: \n \u2192 {webhook_url} \n\nBitte aktualisiere deine Konfiguration gem\u00e4\u00df den [Anweisungen] ({component_url}), starte den Home Assistant neu und versuche es erneut.", "no_available_locations": "In Home Assistant sind keine SmartThings-Standorte zum Einrichten verf\u00fcgbar." }, "error": { @@ -30,7 +30,7 @@ "title": "Standort ausw\u00e4hlen" }, "user": { - "description": "SmartThings wird so konfiguriert, dass Push-Updates an Home Assistant gesendet werden an die URL: \n > {webhook_url} \n\nWenn dies nicht korrekt ist, aktualisiere bitte deine Konfiguration, starte Home Assistant neu und versuche es erneut.", + "description": "SmartThings wird so konfiguriert, dass Push-Updates an Home Assistant gesendet werden an die URL: \n \u2192 {webhook_url} \n\nWenn dies nicht korrekt ist, aktualisiere bitte deine Konfiguration, starte Home Assistant neu und versuche es erneut.", "title": "R\u00fcckruf-URL best\u00e4tigen" } } diff --git a/homeassistant/components/starline/translations/de.json b/homeassistant/components/starline/translations/de.json index 87a9249475e..22e60dd10c0 100644 --- a/homeassistant/components/starline/translations/de.json +++ b/homeassistant/components/starline/translations/de.json @@ -26,7 +26,7 @@ "mfa_code": "SMS Code" }, "description": "Gib den an das Telefon gesendeten Code ein {Telefon_Nummer}", - "title": "2-Faktor-Authentifizierung" + "title": "Zwei-Faktor-Authentifizierung" }, "auth_user": { "data": { diff --git a/homeassistant/components/threshold/translations/sv.json b/homeassistant/components/threshold/translations/sv.json index 613b2c25412..d19b6cabb91 100644 --- a/homeassistant/components/threshold/translations/sv.json +++ b/homeassistant/components/threshold/translations/sv.json @@ -12,5 +12,6 @@ } } } - } + }, + "title": "Gr\u00e4nsv\u00e4rdessensor" } \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/de.json b/homeassistant/components/zwave_me/translations/de.json index 747ccf0c9c8..6e20c28ce07 100644 --- a/homeassistant/components/zwave_me/translations/de.json +++ b/homeassistant/components/zwave_me/translations/de.json @@ -13,7 +13,7 @@ "token": "API-Token", "url": "URL" }, - "description": "Gib die IP-Adresse mit Port und Zugangs-Token des Z-Way-Servers ein. Um das Token zu erhalten, gehe zur Z-Way-Benutzeroberfl\u00e4che Smart Home UI > Men\u00fc > Einstellungen > Benutzer > Administrator > API-Token.\n\nBeispiel f\u00fcr die Verbindung zu Z-Way im lokalen Netzwerk:\nURL: {local_url}\nToken: {local_token}\n\nBeispiel f\u00fcr die Verbindung zu Z-Way \u00fcber den Fernzugriff find.z-wave.me:\nURL: {find_url}\nToken: {find_token}\n\nBeispiel f\u00fcr eine Verbindung zu Z-Way mit einer statischen \u00f6ffentlichen IP-Adresse:\nURL: {remote_url}\nToken: {local_token}\n\nWenn du dich \u00fcber find.z-wave.me verbindest, musst du ein Token mit globalem Geltungsbereich verwenden (logge dich dazu \u00fcber find.z-wave.me bei Z-Way ein)." + "description": "Gib die IP-Adresse mit Port und Zugangs-Token des Z-Way-Servers ein. Um das Token zu erhalten, gehe zur Z-Way-Benutzeroberfl\u00e4che Smart Home UI \u2192 Men\u00fc \u2192 Einstellungen \u2192 Benutzer \u2192 Administrator \u2192 API-Token.\n\nBeispiel f\u00fcr die Verbindung zu Z-Way im lokalen Netzwerk:\nURL: {local_url}\nToken: {local_token}\n\nBeispiel f\u00fcr die Verbindung zu Z-Way \u00fcber den Fernzugriff find.z-wave.me:\nURL: {find_url}\nToken: {find_token}\n\nBeispiel f\u00fcr eine Verbindung zu Z-Way mit einer statischen \u00f6ffentlichen IP-Adresse:\nURL: {remote_url}\nToken: {local_token}\n\nWenn du dich \u00fcber find.z-wave.me verbindest, musst du ein Token mit globalem Geltungsbereich verwenden (logge dich dazu \u00fcber find.z-wave.me bei Z-Way ein)." } } } From 43595f7e17adb95db5a7287d583c40f00ba80fa1 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Fri, 1 Jul 2022 06:08:21 +0200 Subject: [PATCH 2056/3516] Add light tests for devolo_home_control (#74183) --- .coveragerc | 1 - tests/components/devolo_home_control/mocks.py | 41 +++++ .../devolo_home_control/test_light.py | 165 ++++++++++++++++++ 3 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 tests/components/devolo_home_control/test_light.py diff --git a/.coveragerc b/.coveragerc index f12d1ce88fa..21534333583 100644 --- a/.coveragerc +++ b/.coveragerc @@ -210,7 +210,6 @@ omit = homeassistant/components/denonavr/media_player.py homeassistant/components/denonavr/receiver.py homeassistant/components/deutsche_bahn/sensor.py - homeassistant/components/devolo_home_control/light.py homeassistant/components/devolo_home_control/sensor.py homeassistant/components/devolo_home_control/switch.py homeassistant/components/digital_ocean/* diff --git a/tests/components/devolo_home_control/mocks.py b/tests/components/devolo_home_control/mocks.py index e9dae0b70b1..129c4a377ef 100644 --- a/tests/components/devolo_home_control/mocks.py +++ b/tests/components/devolo_home_control/mocks.py @@ -8,6 +8,9 @@ from devolo_home_control_api.homecontrol import HomeControl from devolo_home_control_api.properties.binary_sensor_property import ( BinarySensorProperty, ) +from devolo_home_control_api.properties.binary_switch_property import ( + BinarySwitchProperty, +) from devolo_home_control_api.properties.multi_level_sensor_property import ( MultiLevelSensorProperty, ) @@ -31,6 +34,15 @@ class BinarySensorPropertyMock(BinarySensorProperty): self.state = False +class BinarySwitchPropertyMock(BinarySwitchProperty): + """devolo Home Control binary sensor mock.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize the mock.""" + self._logger = MagicMock() + self.element_uid = "Test" + + class MultiLevelSensorPropertyMock(MultiLevelSensorProperty): """devolo Home Control multi level sensor mock.""" @@ -134,6 +146,22 @@ class CoverMock(DeviceMock): } +class LightMock(DeviceMock): + """devolo Home Control light device mock.""" + + def __init__(self) -> None: + """Initialize the mock.""" + super().__init__() + self.binary_switch_property = {} + self.multi_level_switch_property = { + "devolo.Dimmer:Test": MultiLevelSwitchPropertyMock() + } + self.multi_level_switch_property["devolo.Dimmer:Test"].switch_type = "dimmer" + self.multi_level_switch_property[ + "devolo.Dimmer:Test" + ].element_uid = "devolo.Dimmer:Test" + + class RemoteControlMock(DeviceMock): """devolo Home Control remote control device mock.""" @@ -219,6 +247,19 @@ class HomeControlMockCover(HomeControlMock): self.publisher.unregister = MagicMock() +class HomeControlMockLight(HomeControlMock): + """devolo Home Control gateway mock with light devices.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize the mock.""" + super().__init__() + self.devices = { + "Test": LightMock(), + } + self.publisher = Publisher(self.devices.keys()) + self.publisher.unregister = MagicMock() + + class HomeControlMockRemoteControl(HomeControlMock): """devolo Home Control gateway mock with remote control device.""" diff --git a/tests/components/devolo_home_control/test_light.py b/tests/components/devolo_home_control/test_light.py new file mode 100644 index 00000000000..7b18b28a493 --- /dev/null +++ b/tests/components/devolo_home_control/test_light.py @@ -0,0 +1,165 @@ +"""Tests for the devolo Home Control light platform.""" +from unittest.mock import patch + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, + ATTR_SUPPORTED_COLOR_MODES, + DOMAIN, + ColorMode, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant + +from . import configure_integration +from .mocks import BinarySwitchPropertyMock, HomeControlMock, HomeControlMockLight + + +async def test_light_without_binary_sensor(hass: HomeAssistant): + """Test setup and state change of a light device that does not have an additional binary sensor.""" + entry = configure_integration(hass) + test_gateway = HomeControlMockLight() + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test") + assert state is not None + assert state.state == STATE_ON + assert state.attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.BRIGHTNESS] + assert state.attributes[ATTR_BRIGHTNESS] == round( + test_gateway.devices["Test"] + .multi_level_switch_property["devolo.Dimmer:Test"] + .value + / 100 + * 255 + ) + + # Emulate websocket message: brightness changed + test_gateway.publisher.dispatch("Test", ("devolo.Dimmer:Test", 0.0)) + await hass.async_block_till_done() + state = hass.states.get(f"{DOMAIN}.test") + assert state.state == STATE_OFF + test_gateway.publisher.dispatch("Test", ("devolo.Dimmer:Test", 100.0)) + await hass.async_block_till_done() + state = hass.states.get(f"{DOMAIN}.test") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 255 + + # Test setting brightness + with patch( + "devolo_home_control_api.properties.multi_level_switch_property.MultiLevelSwitchProperty.set" + ) as set_value: + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: f"{DOMAIN}.test"}, + blocking=True, + ) # In reality, this leads to a websocket message like already tested above + set_value.assert_called_once_with(100) + + set_value.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: f"{DOMAIN}.test"}, + blocking=True, + ) # In reality, this leads to a websocket message like already tested above + set_value.assert_called_once_with(0) + + set_value.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: f"{DOMAIN}.test", ATTR_BRIGHTNESS: 50}, + blocking=True, + ) # In reality, this leads to a websocket message like already tested above + set_value.assert_called_once_with(round(50 / 255 * 100)) + + # Emulate websocket message: device went offline + test_gateway.devices["Test"].status = 1 + test_gateway.publisher.dispatch("Test", ("Status", False, "status")) + await hass.async_block_till_done() + assert hass.states.get(f"{DOMAIN}.test").state == STATE_UNAVAILABLE + + +async def test_light_with_binary_sensor(hass: HomeAssistant): + """Test setup and state change of a light device that has an additional binary sensor.""" + entry = configure_integration(hass) + test_gateway = HomeControlMockLight() + test_gateway.devices["Test"].binary_switch_property = { + "devolo.BinarySwitch:Test": BinarySwitchPropertyMock() + } + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test") + assert state is not None + assert state.state == STATE_ON + + # Emulate websocket message: brightness changed + test_gateway.publisher.dispatch("Test", ("devolo.Dimmer:Test", 0.0)) + await hass.async_block_till_done() + state = hass.states.get(f"{DOMAIN}.test") + assert state.state == STATE_OFF + test_gateway.publisher.dispatch("Test", ("devolo.Dimmer:Test", 100.0)) + await hass.async_block_till_done() + state = hass.states.get(f"{DOMAIN}.test") + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 255 + + # Test setting brightness + with patch( + "devolo_home_control_api.properties.binary_switch_property.BinarySwitchProperty.set" + ) as set_value: + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: f"{DOMAIN}.test"}, + blocking=True, + ) # In reality, this leads to a websocket message like already tested above + set_value.assert_called_once_with(True) + + set_value.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: f"{DOMAIN}.test"}, + blocking=True, + ) # In reality, this leads to a websocket message like already tested above + set_value.assert_called_once_with(False) + + +async def test_remove_from_hass(hass: HomeAssistant): + """Test removing entity.""" + entry = configure_integration(hass) + test_gateway = HomeControlMockLight() + with patch( + "homeassistant.components.devolo_home_control.HomeControl", + side_effect=[test_gateway, HomeControlMock()], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.test") + assert state is not None + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + assert test_gateway.publisher.unregister.call_count == 1 From 7655b84494f304fe48f0bb0fd93ab9cfb12c9ff8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Jul 2022 00:19:40 -0500 Subject: [PATCH 2057/3516] Fix key collision between platforms in esphome state updates (#74273) --- homeassistant/components/esphome/__init__.py | 19 +--- .../components/esphome/entry_data.py | 92 ++++++++++++++----- 2 files changed, 73 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 2e88a883dc1..0c1eac3aa45 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -150,11 +150,6 @@ async def async_setup_entry( # noqa: C901 hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop) ) - @callback - def async_on_state(state: EntityState) -> None: - """Send dispatcher updates when a new state is received.""" - entry_data.async_update_state(hass, state) - @callback def async_on_service_call(service: HomeassistantServiceCall) -> None: """Call service when user automation in ESPHome config is triggered.""" @@ -288,7 +283,7 @@ async def async_setup_entry( # noqa: C901 entity_infos, services = await cli.list_entities_services() await entry_data.async_update_static_infos(hass, entry, entity_infos) await _setup_services(hass, entry_data, services) - await cli.subscribe_states(async_on_state) + await cli.subscribe_states(entry_data.async_update_state) await cli.subscribe_service_calls(async_on_service_call) await cli.subscribe_home_assistant_states(async_on_state_subscription) @@ -568,7 +563,6 @@ async def platform_async_setup_entry( @callback def async_list_entities(infos: list[EntityInfo]) -> None: """Update entities of this platform when entities are listed.""" - key_to_component = entry_data.key_to_component old_infos = entry_data.info[component_key] new_infos: dict[int, EntityInfo] = {} add_entities = [] @@ -587,12 +581,10 @@ async def platform_async_setup_entry( entity = entity_type(entry_data, component_key, info.key) add_entities.append(entity) new_infos[info.key] = info - key_to_component[info.key] = component_key # Remove old entities for info in old_infos.values(): entry_data.async_remove_entity(hass, component_key, info.key) - key_to_component.pop(info.key, None) # First copy the now-old info into the backup object entry_data.old_info[component_key] = entry_data.info[component_key] @@ -714,13 +706,8 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): ) self.async_on_remove( - async_dispatcher_connect( - self.hass, - ( - f"esphome_{self._entry_id}" - f"_update_{self._component_key}_{self._key}" - ), - self._on_state_update, + self._entry_data.async_subscribe_state_update( + self._component_key, self._key, self._on_state_update ) ) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index d4bcc67db4a..8eb56e6fdb6 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -12,26 +12,40 @@ from aioesphomeapi import ( APIClient, APIVersion, BinarySensorInfo, + BinarySensorState, CameraInfo, + CameraState, ClimateInfo, + ClimateState, CoverInfo, + CoverState, DeviceInfo, EntityInfo, EntityState, FanInfo, + FanState, LightInfo, + LightState, LockInfo, + LockState, MediaPlayerInfo, + MediaPlayerState, NumberInfo, + NumberState, SelectInfo, + SelectState, SensorInfo, + SensorState, SwitchInfo, + SwitchState, TextSensorInfo, + TextSensorState, UserService, ) from aioesphomeapi.model import ButtonInfo from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store @@ -41,20 +55,37 @@ _LOGGER = logging.getLogger(__name__) # Mapping from ESPHome info type to HA platform INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { - BinarySensorInfo: "binary_sensor", - ButtonInfo: "button", - CameraInfo: "camera", - ClimateInfo: "climate", - CoverInfo: "cover", - FanInfo: "fan", - LightInfo: "light", - LockInfo: "lock", - MediaPlayerInfo: "media_player", - NumberInfo: "number", - SelectInfo: "select", - SensorInfo: "sensor", - SwitchInfo: "switch", - TextSensorInfo: "sensor", + BinarySensorInfo: Platform.BINARY_SENSOR, + ButtonInfo: Platform.BINARY_SENSOR, + CameraInfo: Platform.BINARY_SENSOR, + ClimateInfo: Platform.CLIMATE, + CoverInfo: Platform.COVER, + FanInfo: Platform.FAN, + LightInfo: Platform.LIGHT, + LockInfo: Platform.LOCK, + MediaPlayerInfo: Platform.MEDIA_PLAYER, + NumberInfo: Platform.NUMBER, + SelectInfo: Platform.SELECT, + SensorInfo: Platform.SENSOR, + SwitchInfo: Platform.SWITCH, + TextSensorInfo: Platform.SENSOR, +} + +STATE_TYPE_TO_COMPONENT_KEY = { + BinarySensorState: Platform.BINARY_SENSOR, + EntityState: Platform.BINARY_SENSOR, + CameraState: Platform.BINARY_SENSOR, + ClimateState: Platform.CLIMATE, + CoverState: Platform.COVER, + FanState: Platform.FAN, + LightState: Platform.LIGHT, + LockState: Platform.LOCK, + MediaPlayerState: Platform.MEDIA_PLAYER, + NumberState: Platform.NUMBER, + SelectState: Platform.SELECT, + SensorState: Platform.SENSOR, + SwitchState: Platform.SWITCH, + TextSensorState: Platform.SENSOR, } @@ -67,7 +98,6 @@ class RuntimeEntryData: store: Store state: dict[str, dict[int, EntityState]] = field(default_factory=dict) info: dict[str, dict[int, EntityInfo]] = field(default_factory=dict) - key_to_component: dict[int, str] = field(default_factory=dict) # A second list of EntityInfo objects # This is necessary for when an entity is being removed. HA requires @@ -81,6 +111,9 @@ class RuntimeEntryData: api_version: APIVersion = field(default_factory=APIVersion) cleanup_callbacks: list[Callable[[], None]] = field(default_factory=list) disconnect_callbacks: list[Callable[[], None]] = field(default_factory=list) + state_subscriptions: dict[tuple[str, int], Callable[[], None]] = field( + default_factory=dict + ) loaded_platforms: set[str] = field(default_factory=set) platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock) _storage_contents: dict[str, Any] | None = None @@ -125,18 +158,33 @@ class RuntimeEntryData: async_dispatcher_send(hass, signal, infos) @callback - def async_update_state(self, hass: HomeAssistant, state: EntityState) -> None: + def async_subscribe_state_update( + self, + component_key: str, + state_key: int, + entity_callback: Callable[[], None], + ) -> Callable[[], None]: + """Subscribe to state updates.""" + + def _unsubscribe() -> None: + self.state_subscriptions.pop((component_key, state_key)) + + self.state_subscriptions[(component_key, state_key)] = entity_callback + return _unsubscribe + + @callback + def async_update_state(self, state: EntityState) -> None: """Distribute an update of state information to the target.""" - component_key = self.key_to_component[state.key] + component_key = STATE_TYPE_TO_COMPONENT_KEY[type(state)] + subscription_key = (component_key, state.key) self.state[component_key][state.key] = state - signal = f"esphome_{self.entry_id}_update_{component_key}_{state.key}" _LOGGER.debug( - "Dispatching update for component %s with state key %s: %s", - component_key, - state.key, + "Dispatching update with key %s: %s", + subscription_key, state, ) - async_dispatcher_send(hass, signal) + if subscription_key in self.state_subscriptions: + self.state_subscriptions[subscription_key]() @callback def async_update_device_state(self, hass: HomeAssistant) -> None: From 273e9b287f482f5d1f0718ebf09a56c49c66513d Mon Sep 17 00:00:00 2001 From: Stefan Rado Date: Fri, 1 Jul 2022 07:20:00 +0200 Subject: [PATCH 2058/3516] Add config flow for Bose SoundTouch (#72967) Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 2 + .../components/discovery/__init__.py | 2 +- .../components/soundtouch/__init__.py | 141 +++++++++++ .../components/soundtouch/config_flow.py | 104 ++++++++ homeassistant/components/soundtouch/const.py | 2 +- .../components/soundtouch/manifest.json | 9 +- .../components/soundtouch/media_player.py | 238 ++++++------------ .../components/soundtouch/services.yaml | 8 +- .../components/soundtouch/strings.json | 21 ++ .../soundtouch/translations/en.json | 21 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 5 + tests/components/soundtouch/conftest.py | 34 ++- .../components/soundtouch/test_config_flow.py | 113 +++++++++ .../soundtouch/test_media_player.py | 119 ++++----- 15 files changed, 584 insertions(+), 236 deletions(-) create mode 100644 homeassistant/components/soundtouch/config_flow.py create mode 100644 homeassistant/components/soundtouch/strings.json create mode 100644 homeassistant/components/soundtouch/translations/en.json create mode 100644 tests/components/soundtouch/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index ed4ab888541..9845f5f7e5d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -979,6 +979,8 @@ build.json @home-assistant/supervisor /tests/components/songpal/ @rytilahti @shenxn /homeassistant/components/sonos/ @cgtobi @jjlawren /tests/components/sonos/ @cgtobi @jjlawren +/homeassistant/components/soundtouch/ @kroimon +/tests/components/soundtouch/ @kroimon /homeassistant/components/spaceapi/ @fabaff /tests/components/spaceapi/ @fabaff /homeassistant/components/speedtestdotnet/ @rohankapoorcom @engrbm87 diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 3c3538c1ca0..cc104cc2110 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -61,7 +61,6 @@ SERVICE_HANDLERS = { "yamaha": ServiceDetails("media_player", "yamaha"), "frontier_silicon": ServiceDetails("media_player", "frontier_silicon"), "openhome": ServiceDetails("media_player", "openhome"), - "bose_soundtouch": ServiceDetails("media_player", "soundtouch"), "bluesound": ServiceDetails("media_player", "bluesound"), } @@ -70,6 +69,7 @@ OPTIONAL_SERVICE_HANDLERS: dict[str, tuple[str, str | None]] = {} MIGRATED_SERVICE_HANDLERS = [ SERVICE_APPLE_TV, "axis", + "bose_soundtouch", "deconz", SERVICE_DAIKIN, "denonavr", diff --git a/homeassistant/components/soundtouch/__init__.py b/homeassistant/components/soundtouch/__init__.py index 6cd3c88fefc..69e0eef687e 100644 --- a/homeassistant/components/soundtouch/__init__.py +++ b/homeassistant/components/soundtouch/__init__.py @@ -1 +1,142 @@ """The soundtouch component.""" +import logging + +from libsoundtouch import soundtouch_device +from libsoundtouch.device import SoundTouchDevice +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, Platform +from homeassistant.core import HomeAssistant, ServiceCall +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType + +from .const import ( + DOMAIN, + SERVICE_ADD_ZONE_SLAVE, + SERVICE_CREATE_ZONE, + SERVICE_PLAY_EVERYWHERE, + SERVICE_REMOVE_ZONE_SLAVE, +) + +_LOGGER = logging.getLogger(__name__) + +SERVICE_PLAY_EVERYWHERE_SCHEMA = vol.Schema({vol.Required("master"): cv.entity_id}) +SERVICE_CREATE_ZONE_SCHEMA = vol.Schema( + { + vol.Required("master"): cv.entity_id, + vol.Required("slaves"): cv.entity_ids, + } +) +SERVICE_ADD_ZONE_SCHEMA = vol.Schema( + { + vol.Required("master"): cv.entity_id, + vol.Required("slaves"): cv.entity_ids, + } +) +SERVICE_REMOVE_ZONE_SCHEMA = vol.Schema( + { + vol.Required("master"): cv.entity_id, + vol.Required("slaves"): cv.entity_ids, + } +) + +PLATFORMS = [Platform.MEDIA_PLAYER] + + +class SoundTouchData: + """SoundTouch data stored in the Home Assistant data object.""" + + def __init__(self, device: SoundTouchDevice) -> None: + """Initialize the SoundTouch data object for a device.""" + self.device = device + self.media_player = None + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Bose SoundTouch component.""" + + async def service_handle(service: ServiceCall) -> None: + """Handle the applying of a service.""" + master_id = service.data.get("master") + slaves_ids = service.data.get("slaves") + slaves = [] + if slaves_ids: + slaves = [ + data.media_player + for data in hass.data[DOMAIN].values() + if data.media_player.entity_id in slaves_ids + ] + + master = next( + iter( + [ + data.media_player + for data in hass.data[DOMAIN].values() + if data.media_player.entity_id == master_id + ] + ), + None, + ) + + if master is None: + _LOGGER.warning("Unable to find master with entity_id: %s", str(master_id)) + return + + if service.service == SERVICE_PLAY_EVERYWHERE: + slaves = [ + data.media_player + for data in hass.data[DOMAIN].values() + if data.media_player.entity_id != master_id + ] + await hass.async_add_executor_job(master.create_zone, slaves) + elif service.service == SERVICE_CREATE_ZONE: + await hass.async_add_executor_job(master.create_zone, slaves) + elif service.service == SERVICE_REMOVE_ZONE_SLAVE: + await hass.async_add_executor_job(master.remove_zone_slave, slaves) + elif service.service == SERVICE_ADD_ZONE_SLAVE: + await hass.async_add_executor_job(master.add_zone_slave, slaves) + + hass.services.async_register( + DOMAIN, + SERVICE_PLAY_EVERYWHERE, + service_handle, + schema=SERVICE_PLAY_EVERYWHERE_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_CREATE_ZONE, + service_handle, + schema=SERVICE_CREATE_ZONE_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_REMOVE_ZONE_SLAVE, + service_handle, + schema=SERVICE_REMOVE_ZONE_SCHEMA, + ) + hass.services.async_register( + DOMAIN, + SERVICE_ADD_ZONE_SLAVE, + service_handle, + schema=SERVICE_ADD_ZONE_SCHEMA, + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Bose SoundTouch from a config entry.""" + device = await hass.async_add_executor_job(soundtouch_device, entry.data[CONF_HOST]) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SoundTouchData(device) + + 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): + del hass.data[DOMAIN][entry.entry_id] + return unload_ok diff --git a/homeassistant/components/soundtouch/config_flow.py b/homeassistant/components/soundtouch/config_flow.py new file mode 100644 index 00000000000..47b10912436 --- /dev/null +++ b/homeassistant/components/soundtouch/config_flow.py @@ -0,0 +1,104 @@ +"""Config flow for Bose SoundTouch integration.""" +import logging + +from libsoundtouch import soundtouch_device +from requests import RequestException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST +from homeassistant.helpers import config_validation as cv + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class SoundtouchConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Bose SoundTouch.""" + + VERSION = 1 + + def __init__(self): + """Initialize a new SoundTouch config flow.""" + self.host = None + self.name = None + + async def async_step_import(self, import_data): + """Handle a flow initiated by configuration file.""" + self.host = import_data[CONF_HOST] + + try: + await self._async_get_device_id() + except RequestException: + return self.async_abort(reason="cannot_connect") + + return await self._async_create_soundtouch_entry() + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + errors = {} + + if user_input is not None: + self.host = user_input[CONF_HOST] + + try: + await self._async_get_device_id(raise_on_progress=False) + except RequestException: + errors["base"] = "cannot_connect" + else: + return await self._async_create_soundtouch_entry() + + return self.async_show_form( + step_id="user", + last_step=True, + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + } + ), + errors=errors, + ) + + async def async_step_zeroconf(self, discovery_info): + """Handle a flow initiated by a zeroconf discovery.""" + self.host = discovery_info.host + + try: + await self._async_get_device_id() + except RequestException: + return self.async_abort(reason="cannot_connect") + + self.context["title_placeholders"] = {"name": self.name} + return await self.async_step_zeroconf_confirm() + + async def async_step_zeroconf_confirm(self, user_input=None): + """Handle user-confirmation of discovered node.""" + if user_input is not None: + return await self._async_create_soundtouch_entry() + return self.async_show_form( + step_id="zeroconf_confirm", + last_step=True, + description_placeholders={"name": self.name}, + ) + + async def _async_get_device_id(self, raise_on_progress: bool = True) -> None: + """Get device ID from SoundTouch device.""" + device = await self.hass.async_add_executor_job(soundtouch_device, self.host) + + # Check if already configured + await self.async_set_unique_id( + device.config.device_id, raise_on_progress=raise_on_progress + ) + self._abort_if_unique_id_configured(updates={CONF_HOST: self.host}) + + self.name = device.config.name + + async def _async_create_soundtouch_entry(self): + """Finish config flow and create a SoundTouch config entry.""" + return self.async_create_entry( + title=self.name, + data={ + CONF_HOST: self.host, + }, + ) diff --git a/homeassistant/components/soundtouch/const.py b/homeassistant/components/soundtouch/const.py index 37bf1d8cc2b..a6b2b3c9f5f 100644 --- a/homeassistant/components/soundtouch/const.py +++ b/homeassistant/components/soundtouch/const.py @@ -1,4 +1,4 @@ -"""Constants for the Bose Soundtouch component.""" +"""Constants for the Bose SoundTouch component.""" DOMAIN = "soundtouch" SERVICE_PLAY_EVERYWHERE = "play_everywhere" SERVICE_CREATE_ZONE = "create_zone" diff --git a/homeassistant/components/soundtouch/manifest.json b/homeassistant/components/soundtouch/manifest.json index 15091ec04f7..c1c2abd3b80 100644 --- a/homeassistant/components/soundtouch/manifest.json +++ b/homeassistant/components/soundtouch/manifest.json @@ -1,10 +1,11 @@ { "domain": "soundtouch", - "name": "Bose Soundtouch", + "name": "Bose SoundTouch", "documentation": "https://www.home-assistant.io/integrations/soundtouch", "requirements": ["libsoundtouch==0.8"], - "after_dependencies": ["zeroconf"], - "codeowners": [], + "zeroconf": ["_soundtouch._tcp.local."], + "codeowners": ["@kroimon"], "iot_class": "local_polling", - "loggers": ["libsoundtouch"] + "loggers": ["libsoundtouch"], + "config_flow": true } diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index f8a5191d9db..2ed3dd9beea 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -1,23 +1,25 @@ -"""Support for interface with a Bose Soundtouch.""" +"""Support for interface with a Bose SoundTouch.""" from __future__ import annotations from functools import partial import logging import re -from libsoundtouch import soundtouch_device +from libsoundtouch.device import SoundTouchDevice from libsoundtouch.utils import Source import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( PLATFORM_SCHEMA, + MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, ) from homeassistant.components.media_player.browse_media import ( async_process_play_media_url, ) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -27,19 +29,16 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import ( - DOMAIN, - SERVICE_ADD_ZONE_SLAVE, - SERVICE_CREATE_ZONE, - SERVICE_PLAY_EVERYWHERE, - SERVICE_REMOVE_ZONE_SLAVE, -) +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -50,137 +49,57 @@ MAP_STATUS = { "STOP_STATE": STATE_OFF, } -DATA_SOUNDTOUCH = "soundtouch" ATTR_SOUNDTOUCH_GROUP = "soundtouch_group" ATTR_SOUNDTOUCH_ZONE = "soundtouch_zone" -SOUNDTOUCH_PLAY_EVERYWHERE = vol.Schema({vol.Required("master"): cv.entity_id}) - -SOUNDTOUCH_CREATE_ZONE_SCHEMA = vol.Schema( - {vol.Required("master"): cv.entity_id, vol.Required("slaves"): cv.entity_ids} -) - -SOUNDTOUCH_ADD_ZONE_SCHEMA = vol.Schema( - {vol.Required("master"): cv.entity_id, vol.Required("slaves"): cv.entity_ids} -) - -SOUNDTOUCH_REMOVE_ZONE_SCHEMA = vol.Schema( - {vol.Required("master"): cv.entity_id, vol.Required("slaves"): cv.entity_ids} -) - -DEFAULT_NAME = "Bose Soundtouch" -DEFAULT_PORT = 8090 - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - } +PLATFORM_SCHEMA = vol.All( + PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_NAME, default=""): cv.string, + } + ), ) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the Bose Soundtouch platform.""" - if DATA_SOUNDTOUCH not in hass.data: - hass.data[DATA_SOUNDTOUCH] = [] - - if discovery_info: - host = discovery_info["host"] - port = int(discovery_info["port"]) - - # if device already exists by config - if host in [device.config["host"] for device in hass.data[DATA_SOUNDTOUCH]]: - return - - remote_config = {"id": "ha.component.soundtouch", "host": host, "port": port} - bose_soundtouch_entity = SoundTouchDevice(None, remote_config) - hass.data[DATA_SOUNDTOUCH].append(bose_soundtouch_entity) - add_entities([bose_soundtouch_entity], True) - else: - name = config.get(CONF_NAME) - remote_config = { - "id": "ha.component.soundtouch", - "port": config.get(CONF_PORT), - "host": config.get(CONF_HOST), - } - bose_soundtouch_entity = SoundTouchDevice(name, remote_config) - hass.data[DATA_SOUNDTOUCH].append(bose_soundtouch_entity) - add_entities([bose_soundtouch_entity], True) - - def service_handle(service: ServiceCall) -> None: - """Handle the applying of a service.""" - master_device_id = service.data.get("master") - slaves_ids = service.data.get("slaves") - slaves = [] - if slaves_ids: - slaves = [ - device - for device in hass.data[DATA_SOUNDTOUCH] - if device.entity_id in slaves_ids - ] - - master = next( - iter( - [ - device - for device in hass.data[DATA_SOUNDTOUCH] - if device.entity_id == master_device_id - ] - ), - None, + """Set up the Bose SoundTouch platform.""" + _LOGGER.warning( + "Configuration of the Bose SoundTouch platform in YAML is deprecated and will be " + "removed in a future release; Your existing configuration " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, ) - - if master is None: - _LOGGER.warning( - "Unable to find master with entity_id: %s", str(master_device_id) - ) - return - - if service.service == SERVICE_PLAY_EVERYWHERE: - slaves = [ - d for d in hass.data[DATA_SOUNDTOUCH] if d.entity_id != master_device_id - ] - master.create_zone(slaves) - elif service.service == SERVICE_CREATE_ZONE: - master.create_zone(slaves) - elif service.service == SERVICE_REMOVE_ZONE_SLAVE: - master.remove_zone_slave(slaves) - elif service.service == SERVICE_ADD_ZONE_SLAVE: - master.add_zone_slave(slaves) - - hass.services.register( - DOMAIN, - SERVICE_PLAY_EVERYWHERE, - service_handle, - schema=SOUNDTOUCH_PLAY_EVERYWHERE, - ) - hass.services.register( - DOMAIN, - SERVICE_CREATE_ZONE, - service_handle, - schema=SOUNDTOUCH_CREATE_ZONE_SCHEMA, - ) - hass.services.register( - DOMAIN, - SERVICE_REMOVE_ZONE_SLAVE, - service_handle, - schema=SOUNDTOUCH_REMOVE_ZONE_SCHEMA, - ) - hass.services.register( - DOMAIN, - SERVICE_ADD_ZONE_SLAVE, - service_handle, - schema=SOUNDTOUCH_ADD_ZONE_SCHEMA, ) -class SoundTouchDevice(MediaPlayerEntity): +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Bose SoundTouch media player based on a config entry.""" + device = hass.data[DOMAIN][entry.entry_id].device + media_player = SoundTouchMediaPlayer(device) + + async_add_entities([media_player], True) + + hass.data[DOMAIN][entry.entry_id].media_player = media_player + + +class SoundTouchMediaPlayer(MediaPlayerEntity): """Representation of a SoundTouch Bose device.""" _attr_supported_features = ( @@ -197,28 +116,32 @@ class SoundTouchDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.BROWSE_MEDIA ) + _attr_device_class = MediaPlayerDeviceClass.SPEAKER - def __init__(self, name, config): - """Create Soundtouch Entity.""" + def __init__(self, device: SoundTouchDevice) -> None: + """Create SoundTouch media player entity.""" + + self._device = device + + self._attr_unique_id = self._device.config.device_id + self._attr_name = self._device.config.name + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self._device.config.device_id)}, + connections={ + (CONNECTION_NETWORK_MAC, format_mac(self._device.config.mac_address)) + }, + manufacturer="Bose Corporation", + model=self._device.config.type, + name=self._device.config.name, + ) - self._device = soundtouch_device(config["host"], config["port"]) - if name is None: - self._name = self._device.config.name - else: - self._name = name self._status = None self._volume = None - self._config = config self._zone = None - @property - def config(self): - """Return specific soundtouch configuration.""" - return self._config - @property def device(self): - """Return Soundtouch device.""" + """Return SoundTouch device.""" return self._device def update(self): @@ -232,17 +155,15 @@ class SoundTouchDevice(MediaPlayerEntity): """Volume level of the media player (0..1).""" return self._volume.actual / 100 - @property - def name(self): - """Return the name of the device.""" - return self._name - @property def state(self): """Return the state of the device.""" - if self._status.source == "STANDBY": + if self._status is None or self._status.source == "STANDBY": return STATE_OFF + if self._status.source == "INVALID_SOURCE": + return STATE_UNKNOWN + return MAP_STATUS.get(self._status.play_status, STATE_UNAVAILABLE) @property @@ -478,15 +399,12 @@ class SoundTouchDevice(MediaPlayerEntity): if not zone_status: return None - # Due to a bug in the SoundTouch API itself client devices do NOT return their - # siblings as part of the "slaves" list. Only the master has the full list of - # slaves for some reason. To compensate for this shortcoming we have to fetch - # the zone info from the master when the current device is a slave until this is - # fixed in the SoundTouch API or libsoundtouch, or of course until somebody has a - # better idea on how to fix this. + # Client devices do NOT return their siblings as part of the "slaves" list. + # Only the master has the full list of slaves. To compensate for this shortcoming + # we have to fetch the zone info from the master when the current device is a slave. # In addition to this shortcoming, libsoundtouch seems to report the "is_master" # property wrong on some slaves, so the only reliable way to detect if the current - # devices is the master, is by comparing the master_id of the zone with the device_id + # devices is the master, is by comparing the master_id of the zone with the device_id. if zone_status.master_id == self._device.config.device_id: return self._build_zone_info(self.entity_id, zone_status.slaves) @@ -505,16 +423,16 @@ class SoundTouchDevice(MediaPlayerEntity): def _get_instance_by_ip(self, ip_address): """Search and return a SoundTouchDevice instance by it's IP address.""" - for instance in self.hass.data[DATA_SOUNDTOUCH]: - if instance and instance.config["host"] == ip_address: - return instance + for data in self.hass.data[DOMAIN].values(): + if data.device.config.device_ip == ip_address: + return data.media_player return None def _get_instance_by_id(self, instance_id): """Search and return a SoundTouchDevice instance by it's ID (aka MAC address).""" - for instance in self.hass.data[DATA_SOUNDTOUCH]: - if instance and instance.device.config.device_id == instance_id: - return instance + for data in self.hass.data[DOMAIN].values(): + if data.device.config.device_id == instance_id: + return data.media_player return None def _build_zone_info(self, master, zone_slaves): diff --git a/homeassistant/components/soundtouch/services.yaml b/homeassistant/components/soundtouch/services.yaml index 8d255e5f069..82709053496 100644 --- a/homeassistant/components/soundtouch/services.yaml +++ b/homeassistant/components/soundtouch/services.yaml @@ -1,6 +1,6 @@ play_everywhere: name: Play everywhere - description: Play on all Bose Soundtouch devices. + description: Play on all Bose SoundTouch devices. fields: master: name: Master @@ -13,7 +13,7 @@ play_everywhere: create_zone: name: Create zone - description: Create a Soundtouch multi-room zone. + description: Create a SoundTouch multi-room zone. fields: master: name: Master @@ -35,7 +35,7 @@ create_zone: add_zone_slave: name: Add zone slave - description: Add a slave to a Soundtouch multi-room zone. + description: Add a slave to a SoundTouch multi-room zone. fields: master: name: Master @@ -57,7 +57,7 @@ add_zone_slave: remove_zone_slave: name: Remove zone slave - description: Remove a slave from the Soundtouch multi-room zone. + description: Remove a slave from the SoundTouch multi-room zone. fields: master: name: Master diff --git a/homeassistant/components/soundtouch/strings.json b/homeassistant/components/soundtouch/strings.json new file mode 100644 index 00000000000..7ebcd4c5285 --- /dev/null +++ b/homeassistant/components/soundtouch/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "zeroconf_confirm": { + "title": "Confirm adding Bose SoundTouch device", + "description": "You are about to add the SoundTouch device named `{name}` to Home Assistant." + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/soundtouch/translations/en.json b/homeassistant/components/soundtouch/translations/en.json new file mode 100644 index 00000000000..2e025d3f187 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "You are about to add the SoundTouch device named `{name}` to Home Assistant.", + "title": "Confirm adding Bose SoundTouch device" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b0da8f79418..0b985f6e161 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -334,6 +334,7 @@ FLOWS = { "sonarr", "songpal", "sonos", + "soundtouch", "speedtestdotnet", "spider", "spotify", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index faca1c17854..3c9d21d1d95 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -347,6 +347,11 @@ ZEROCONF = { "domain": "sonos" } ], + "_soundtouch._tcp.local.": [ + { + "domain": "soundtouch" + } + ], "_spotify-connect._tcp.local.": [ { "domain": "spotify" diff --git a/tests/components/soundtouch/conftest.py b/tests/components/soundtouch/conftest.py index dcac360d253..21de9e2ed47 100644 --- a/tests/components/soundtouch/conftest.py +++ b/tests/components/soundtouch/conftest.py @@ -4,9 +4,9 @@ from requests_mock import Mocker from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.components.soundtouch.const import DOMAIN -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PLATFORM +from homeassistant.const import CONF_HOST, CONF_NAME -from tests.common import load_fixture +from tests.common import MockConfigEntry, load_fixture DEVICE_1_ID = "020000000001" DEVICE_2_ID = "020000000002" @@ -14,8 +14,8 @@ DEVICE_1_IP = "192.168.42.1" DEVICE_2_IP = "192.168.42.2" DEVICE_1_URL = f"http://{DEVICE_1_IP}:8090" DEVICE_2_URL = f"http://{DEVICE_2_IP}:8090" -DEVICE_1_NAME = "My Soundtouch 1" -DEVICE_2_NAME = "My Soundtouch 2" +DEVICE_1_NAME = "My SoundTouch 1" +DEVICE_2_NAME = "My SoundTouch 2" DEVICE_1_ENTITY_ID = f"{MEDIA_PLAYER_DOMAIN}.my_soundtouch_1" DEVICE_2_ENTITY_ID = f"{MEDIA_PLAYER_DOMAIN}.my_soundtouch_2" @@ -24,15 +24,29 @@ DEVICE_2_ENTITY_ID = f"{MEDIA_PLAYER_DOMAIN}.my_soundtouch_2" @pytest.fixture -def device1_config() -> dict[str, str]: - """Mock SoundTouch device 1 config.""" - yield {CONF_PLATFORM: DOMAIN, CONF_HOST: DEVICE_1_IP, CONF_NAME: DEVICE_1_NAME} +def device1_config() -> MockConfigEntry: + """Mock SoundTouch device 1 config entry.""" + yield MockConfigEntry( + domain=DOMAIN, + unique_id=DEVICE_1_ID, + data={ + CONF_HOST: DEVICE_1_IP, + CONF_NAME: "", + }, + ) @pytest.fixture -def device2_config() -> dict[str, str]: - """Mock SoundTouch device 2 config.""" - yield {CONF_PLATFORM: DOMAIN, CONF_HOST: DEVICE_2_IP, CONF_NAME: DEVICE_2_NAME} +def device2_config() -> MockConfigEntry: + """Mock SoundTouch device 2 config entry.""" + yield MockConfigEntry( + domain=DOMAIN, + unique_id=DEVICE_2_ID, + data={ + CONF_HOST: DEVICE_2_IP, + CONF_NAME: "", + }, + ) @pytest.fixture(scope="session") diff --git a/tests/components/soundtouch/test_config_flow.py b/tests/components/soundtouch/test_config_flow.py new file mode 100644 index 00000000000..cbeb27be979 --- /dev/null +++ b/tests/components/soundtouch/test_config_flow.py @@ -0,0 +1,113 @@ +"""Test config flow.""" +from unittest.mock import patch + +from requests import RequestException +from requests_mock import ANY, Mocker + +from homeassistant.components.soundtouch.const import DOMAIN +from homeassistant.components.zeroconf import ZeroconfServiceInfo +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_HOST, CONF_SOURCE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .conftest import DEVICE_1_ID, DEVICE_1_IP, DEVICE_1_NAME + + +async def test_user_flow_create_entry( + hass: HomeAssistant, device1_requests_mock_standby: Mocker +) -> None: + """Test the full manual user flow from start to finish.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + ) + + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "user" + assert "flow_id" in result + + with patch( + "homeassistant.components.soundtouch.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: DEVICE_1_IP, + }, + ) + + assert len(mock_setup_entry.mock_calls) == 1 + + assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("title") == DEVICE_1_NAME + assert result.get("data") == { + CONF_HOST: DEVICE_1_IP, + } + assert "result" in result + assert result["result"].unique_id == DEVICE_1_ID + assert result["result"].title == DEVICE_1_NAME + + +async def test_user_flow_cannot_connect( + hass: HomeAssistant, requests_mock: Mocker +) -> None: + """Test a manual user flow with an invalid host.""" + requests_mock.get(ANY, exc=RequestException()) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data={ + CONF_HOST: "invalid-hostname", + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_zeroconf_flow_create_entry( + hass: HomeAssistant, device1_requests_mock_standby: Mocker +) -> None: + """Test the zeroconf flow from start to finish.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data=ZeroconfServiceInfo( + host=DEVICE_1_IP, + addresses=[DEVICE_1_IP], + port=8090, + hostname="Bose-SM2-060000000001.local.", + type="_soundtouch._tcp.local.", + name=f"{DEVICE_1_NAME}._soundtouch._tcp.local.", + properties={ + "DESCRIPTION": "SoundTouch", + "MAC": DEVICE_1_ID, + "MANUFACTURER": "Bose Corporation", + "MODEL": "SoundTouch", + }, + ), + ) + + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "zeroconf_confirm" + assert result.get("description_placeholders") == {"name": DEVICE_1_NAME} + + with patch( + "homeassistant.components.soundtouch.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert len(mock_setup_entry.mock_calls) == 1 + + assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("title") == DEVICE_1_NAME + assert result.get("data") == { + CONF_HOST: DEVICE_1_IP, + } + assert "result" in result + assert result["result"].unique_id == DEVICE_1_ID + assert result["result"].title == DEVICE_1_NAME diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index 1b16508bb88..5105d07479c 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -1,4 +1,5 @@ """Test the SoundTouch component.""" +from datetime import timedelta from typing import Any from requests_mock import Mocker @@ -25,22 +26,26 @@ from homeassistant.components.soundtouch.const import ( from homeassistant.components.soundtouch.media_player import ( ATTR_SOUNDTOUCH_GROUP, ATTR_SOUNDTOUCH_ZONE, - DATA_SOUNDTOUCH, ) +from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from homeassistant.util import dt from .conftest import DEVICE_1_ENTITY_ID, DEVICE_2_ENTITY_ID +from tests.common import MockConfigEntry, async_fire_time_changed -async def setup_soundtouch(hass: HomeAssistant, *configs: dict[str, str]): + +async def setup_soundtouch(hass: HomeAssistant, *mock_entries: MockConfigEntry): """Initialize media_player for tests.""" - assert await async_setup_component( - hass, MEDIA_PLAYER_DOMAIN, {MEDIA_PLAYER_DOMAIN: list(configs)} - ) + assert await async_setup_component(hass, MEDIA_PLAYER_DOMAIN, {}) + + for mock_entry in mock_entries: + mock_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - await hass.async_start() async def _test_key_service( @@ -59,7 +64,7 @@ async def _test_key_service( async def test_playing_media( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, ): """Test playing media info.""" @@ -76,7 +81,7 @@ async def test_playing_media( async def test_playing_radio( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_radio, ): """Test playing radio info.""" @@ -89,7 +94,7 @@ async def test_playing_radio( async def test_playing_aux( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_aux, ): """Test playing AUX info.""" @@ -102,7 +107,7 @@ async def test_playing_aux( async def test_playing_bluetooth( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_bluetooth, ): """Test playing Bluetooth info.""" @@ -118,7 +123,7 @@ async def test_playing_bluetooth( async def test_get_volume_level( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, ): """Test volume level.""" @@ -130,7 +135,7 @@ async def test_get_volume_level( async def test_get_state_off( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, ): """Test state device is off.""" @@ -142,7 +147,7 @@ async def test_get_state_off( async def test_get_state_pause( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp_paused, ): """Test state device is paused.""" @@ -154,7 +159,7 @@ async def test_get_state_pause( async def test_is_muted( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_volume_muted: str, ): @@ -170,7 +175,7 @@ async def test_is_muted( async def test_should_turn_off( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -187,7 +192,7 @@ async def test_should_turn_off( async def test_should_turn_on( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_key, ): @@ -204,7 +209,7 @@ async def test_should_turn_on( async def test_volume_up( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -221,7 +226,7 @@ async def test_volume_up( async def test_volume_down( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -238,7 +243,7 @@ async def test_volume_down( async def test_set_volume_level( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_volume, ): @@ -258,7 +263,7 @@ async def test_set_volume_level( async def test_mute( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -275,7 +280,7 @@ async def test_mute( async def test_play( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp_paused, device1_requests_mock_key, ): @@ -292,7 +297,7 @@ async def test_play( async def test_pause( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -309,7 +314,7 @@ async def test_pause( async def test_play_pause( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -326,7 +331,7 @@ async def test_play_pause( async def test_next_previous_track( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_upnp, device1_requests_mock_key, ): @@ -351,7 +356,7 @@ async def test_next_previous_track( async def test_play_media( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_select, ): @@ -391,7 +396,7 @@ async def test_play_media( async def test_play_media_url( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_dlna, ): @@ -415,7 +420,7 @@ async def test_play_media_url( async def test_select_source_aux( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_select, ): @@ -435,7 +440,7 @@ async def test_select_source_aux( async def test_select_source_bluetooth( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_select, ): @@ -455,7 +460,7 @@ async def test_select_source_bluetooth( async def test_select_source_invalid_source( hass: HomeAssistant, - device1_config: dict[str, str], + device1_config: MockConfigEntry, device1_requests_mock_standby, device1_requests_mock_select, ): @@ -477,14 +482,25 @@ async def test_select_source_invalid_source( async def test_play_everywhere( hass: HomeAssistant, - device1_config: dict[str, str], - device2_config: dict[str, str], + device1_config: MockConfigEntry, + device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, device1_requests_mock_set_zone, ): """Test play everywhere.""" - await setup_soundtouch(hass, device1_config, device2_config) + await setup_soundtouch(hass, device1_config) + + # no slaves, set zone must not be called + await hass.services.async_call( + DOMAIN, + SERVICE_PLAY_EVERYWHERE, + {"master": DEVICE_1_ENTITY_ID}, + True, + ) + assert device1_requests_mock_set_zone.call_count == 0 + + await setup_soundtouch(hass, device2_config) # one master, one slave => set zone await hass.services.async_call( @@ -504,27 +520,11 @@ async def test_play_everywhere( ) assert device1_requests_mock_set_zone.call_count == 1 - # remove second device - for entity in list(hass.data[DATA_SOUNDTOUCH]): - if entity.entity_id == DEVICE_1_ENTITY_ID: - continue - hass.data[DATA_SOUNDTOUCH].remove(entity) - await entity.async_remove() - - # no slaves, set zone must not be called - await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_EVERYWHERE, - {"master": DEVICE_1_ENTITY_ID}, - True, - ) - assert device1_requests_mock_set_zone.call_count == 1 - async def test_create_zone( hass: HomeAssistant, - device1_config: dict[str, str], - device2_config: dict[str, str], + device1_config: MockConfigEntry, + device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, device1_requests_mock_set_zone, @@ -567,8 +567,8 @@ async def test_create_zone( async def test_remove_zone_slave( hass: HomeAssistant, - device1_config: dict[str, str], - device2_config: dict[str, str], + device1_config: MockConfigEntry, + device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, device1_requests_mock_remove_zone_slave, @@ -609,8 +609,8 @@ async def test_remove_zone_slave( async def test_add_zone_slave( hass: HomeAssistant, - device1_config: dict[str, str], - device2_config: dict[str, str], + device1_config: MockConfigEntry, + device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, device1_requests_mock_add_zone_slave, @@ -651,14 +651,21 @@ async def test_add_zone_slave( async def test_zone_attributes( hass: HomeAssistant, - device1_config: dict[str, str], - device2_config: dict[str, str], + device1_config: MockConfigEntry, + device2_config: MockConfigEntry, device1_requests_mock_standby, device2_requests_mock_standby, ): """Test zone attributes.""" await setup_soundtouch(hass, device1_config, device2_config) + # Fast-forward time to allow all entities to be set up and updated again + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + entity_1_state = hass.states.get(DEVICE_1_ENTITY_ID) assert entity_1_state.attributes[ATTR_SOUNDTOUCH_ZONE]["is_master"] assert ( From 57b63db5677ece177b94dc27ffdb29c945564332 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Jul 2022 01:23:00 -0500 Subject: [PATCH 2059/3516] Improve typing for device_automation (#74282) --- .../components/device_automation/__init__.py | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 0a1ec495e70..93119d1b4a0 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Iterable, Mapping +from collections.abc import Awaitable, Callable, Coroutine, Iterable, Mapping from enum import Enum from functools import wraps import logging @@ -13,6 +13,7 @@ import voluptuous as vol import voluptuous_serialize from homeassistant.components import websocket_api +from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DEVICE_ID, @@ -43,10 +44,9 @@ if TYPE_CHECKING: DeviceAutomationActionProtocol, ] -# mypy: allow-untyped-calls, allow-untyped-defs DOMAIN = "device_automation" -DEVICE_TRIGGER_BASE_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( +DEVICE_TRIGGER_BASE_SCHEMA: vol.Schema = cv.TRIGGER_BASE_SCHEMA.extend( { vol.Required(CONF_PLATFORM): "device", vol.Required(CONF_DOMAIN): str, @@ -310,11 +310,17 @@ async def _async_get_device_automation_capabilities( return capabilities # type: ignore[no-any-return] -def handle_device_errors(func): +def handle_device_errors( + func: Callable[[HomeAssistant, ActiveConnection, dict[str, Any]], Awaitable[None]] +) -> Callable[ + [HomeAssistant, ActiveConnection, dict[str, Any]], Coroutine[Any, Any, None] +]: """Handle device automation errors.""" @wraps(func) - async def with_error_handling(hass, connection, msg): + async def with_error_handling( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] + ) -> None: try: await func(hass, connection, msg) except DeviceNotFound: @@ -333,7 +339,9 @@ def handle_device_errors(func): ) @websocket_api.async_response @handle_device_errors -async def websocket_device_automation_list_actions(hass, connection, msg): +async def websocket_device_automation_list_actions( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle request for device actions.""" device_id = msg["device_id"] actions = ( @@ -352,7 +360,9 @@ async def websocket_device_automation_list_actions(hass, connection, msg): ) @websocket_api.async_response @handle_device_errors -async def websocket_device_automation_list_conditions(hass, connection, msg): +async def websocket_device_automation_list_conditions( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle request for device conditions.""" device_id = msg["device_id"] conditions = ( @@ -371,7 +381,9 @@ async def websocket_device_automation_list_conditions(hass, connection, msg): ) @websocket_api.async_response @handle_device_errors -async def websocket_device_automation_list_triggers(hass, connection, msg): +async def websocket_device_automation_list_triggers( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle request for device triggers.""" device_id = msg["device_id"] triggers = ( @@ -390,7 +402,9 @@ async def websocket_device_automation_list_triggers(hass, connection, msg): ) @websocket_api.async_response @handle_device_errors -async def websocket_device_automation_get_action_capabilities(hass, connection, msg): +async def websocket_device_automation_get_action_capabilities( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle request for device action capabilities.""" action = msg["action"] capabilities = await _async_get_device_automation_capabilities( @@ -409,7 +423,9 @@ async def websocket_device_automation_get_action_capabilities(hass, connection, ) @websocket_api.async_response @handle_device_errors -async def websocket_device_automation_get_condition_capabilities(hass, connection, msg): +async def websocket_device_automation_get_condition_capabilities( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle request for device condition capabilities.""" condition = msg["condition"] capabilities = await _async_get_device_automation_capabilities( @@ -428,7 +444,9 @@ async def websocket_device_automation_get_condition_capabilities(hass, connectio ) @websocket_api.async_response @handle_device_errors -async def websocket_device_automation_get_trigger_capabilities(hass, connection, msg): +async def websocket_device_automation_get_trigger_capabilities( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle request for device trigger capabilities.""" trigger = msg["trigger"] capabilities = await _async_get_device_automation_capabilities( From 7094b7e62f617257f0855d8f83ed9fbb4f36467a Mon Sep 17 00:00:00 2001 From: BrianWithAHat <19786223+BrianWithAHat@users.noreply.github.com> Date: Fri, 1 Jul 2022 03:17:27 -0400 Subject: [PATCH 2060/3516] Bump quantum_gateway to v0.0.8. (#74284) --- homeassistant/components/quantum_gateway/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/quantum_gateway/manifest.json b/homeassistant/components/quantum_gateway/manifest.json index 5ce387dc9f3..49f674fdec6 100644 --- a/homeassistant/components/quantum_gateway/manifest.json +++ b/homeassistant/components/quantum_gateway/manifest.json @@ -2,7 +2,7 @@ "domain": "quantum_gateway", "name": "Quantum Gateway", "documentation": "https://www.home-assistant.io/integrations/quantum_gateway", - "requirements": ["quantum-gateway==0.0.6"], + "requirements": ["quantum-gateway==0.0.8"], "codeowners": ["@cisasteelersfan"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 0cb6d5122e1..75c6552de30 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2047,7 +2047,7 @@ pyzerproc==0.4.8 qnapstats==0.4.0 # homeassistant.components.quantum_gateway -quantum-gateway==0.0.6 +quantum-gateway==0.0.8 # homeassistant.components.rachio rachiopy==1.0.3 From c78c159d722669a642ce89f631f46e4a9390b10c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Jul 2022 00:40:05 -0700 Subject: [PATCH 2061/3516] Add scan interval to scrape sensor (#74285) --- homeassistant/components/scrape/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index a73dbc17c1c..b6b8828ca73 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -1,6 +1,7 @@ """Support for getting data from websites with scraping.""" from __future__ import annotations +from datetime import timedelta import logging from typing import Any @@ -44,6 +45,7 @@ from .const import CONF_INDEX, CONF_SELECT, DEFAULT_NAME, DEFAULT_VERIFY_SSL, DO _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(minutes=10) ICON = "mdi:web" PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( From a58301a97d7f90a791643607eff0ed74d5f59eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 1 Jul 2022 09:48:59 +0200 Subject: [PATCH 2062/3516] Improve qnap_qsw firmware coordinator failures (#74288) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit qnap_qsw: update: improve firmware coordinator failures Address late comments from @MartinHjelmare (MartinHjelmare). Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/qnap_qsw/__init__.py | 10 +++++++- .../components/qnap_qsw/coordinator.py | 4 +-- tests/components/qnap_qsw/test_init.py | 25 +++++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/qnap_qsw/__init__.py b/homeassistant/components/qnap_qsw/__init__.py index 4d2ee76cd2b..040f8eb52f2 100644 --- a/homeassistant/components/qnap_qsw/__init__.py +++ b/homeassistant/components/qnap_qsw/__init__.py @@ -1,16 +1,21 @@ """The QNAP QSW integration.""" from __future__ import annotations +import logging + from aioqsw.localapi import ConnectionOptions, QnapQswApi from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from .const import DOMAIN, QSW_COORD_DATA, QSW_COORD_FW from .coordinator import QswDataCoordinator, QswFirmwareCoordinator +_LOGGER = logging.getLogger(__name__) + PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, Platform.BUTTON, @@ -33,7 +38,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coord_data.async_config_entry_first_refresh() coord_fw = QswFirmwareCoordinator(hass, qsw) - await coord_fw.async_config_entry_first_refresh() + try: + await coord_fw.async_config_entry_first_refresh() + except ConfigEntryNotReady as error: + _LOGGER.warning(error) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { QSW_COORD_DATA: coord_data, diff --git a/homeassistant/components/qnap_qsw/coordinator.py b/homeassistant/components/qnap_qsw/coordinator.py index 73af2f74cc5..eb4e60bf9bd 100644 --- a/homeassistant/components/qnap_qsw/coordinator.py +++ b/homeassistant/components/qnap_qsw/coordinator.py @@ -5,7 +5,7 @@ from datetime import timedelta import logging from typing import Any -from aioqsw.exceptions import APIError, QswError +from aioqsw.exceptions import QswError from aioqsw.localapi import QnapQswApi import async_timeout @@ -63,8 +63,6 @@ class QswFirmwareCoordinator(DataUpdateCoordinator[dict[str, Any]]): async with async_timeout.timeout(QSW_TIMEOUT_SEC): try: await self.qsw.check_firmware() - except APIError as error: - _LOGGER.warning(error) except QswError as error: raise UpdateFailed(error) from error return self.qsw.data() diff --git a/tests/components/qnap_qsw/test_init.py b/tests/components/qnap_qsw/test_init.py index 75fa9ec8adc..fedfdd26543 100644 --- a/tests/components/qnap_qsw/test_init.py +++ b/tests/components/qnap_qsw/test_init.py @@ -2,6 +2,8 @@ from unittest.mock import patch +from aioqsw.exceptions import APIError + from homeassistant.components.qnap_qsw.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant @@ -11,6 +13,29 @@ from .util import CONFIG from tests.common import MockConfigEntry +async def test_firmware_check_error(hass: HomeAssistant) -> None: + """Test firmware update check error.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, unique_id="qsw_unique_id", data=CONFIG + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.qnap_qsw.QnapQswApi.check_firmware", + side_effect=APIError, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.validate", + return_value=None, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.update", + return_value=None, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + async def test_unload_entry(hass: HomeAssistant) -> None: """Test unload.""" From c0ea1a38a6b42763d945a47d0a5a8894e07142f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 1 Jul 2022 11:52:46 +0200 Subject: [PATCH 2063/3516] Fix QNAP QSW DHCP discover bugs (#74291) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit qnqp_qsw: fix DHCP discover bugs Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/qnap_qsw/config_flow.py | 3 ++- homeassistant/components/qnap_qsw/strings.json | 6 ++++++ homeassistant/components/qnap_qsw/translations/en.json | 6 ++++++ tests/components/qnap_qsw/test_config_flow.py | 5 +++-- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qnap_qsw/config_flow.py b/homeassistant/components/qnap_qsw/config_flow.py index e9d11433021..bb42c9ea294 100644 --- a/homeassistant/components/qnap_qsw/config_flow.py +++ b/homeassistant/components/qnap_qsw/config_flow.py @@ -113,9 +113,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except LoginError: errors[CONF_PASSWORD] = "invalid_auth" except QswError: - errors[CONF_URL] = "cannot_connect" + errors["base"] = "cannot_connect" else: title = f"QNAP {system_board.get_product()} {self._discovered_mac}" + user_input[CONF_URL] = self._discovered_url return self.async_create_entry(title=title, data=user_input) return self.async_show_form( diff --git a/homeassistant/components/qnap_qsw/strings.json b/homeassistant/components/qnap_qsw/strings.json index 351245a9591..ba0cb28ba77 100644 --- a/homeassistant/components/qnap_qsw/strings.json +++ b/homeassistant/components/qnap_qsw/strings.json @@ -9,6 +9,12 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "step": { + "discovered_connection": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, "user": { "data": { "url": "[%key:common::config_flow::data::url%]", diff --git a/homeassistant/components/qnap_qsw/translations/en.json b/homeassistant/components/qnap_qsw/translations/en.json index b6f68f2f062..c75c2d76ac8 100644 --- a/homeassistant/components/qnap_qsw/translations/en.json +++ b/homeassistant/components/qnap_qsw/translations/en.json @@ -9,6 +9,12 @@ "invalid_auth": "Invalid authentication" }, "step": { + "discovered_connection": { + "data": { + "password": "Password", + "username": "Username" + } + }, "user": { "data": { "password": "Password", diff --git a/tests/components/qnap_qsw/test_config_flow.py b/tests/components/qnap_qsw/test_config_flow.py index 0b7072dd602..02f873c6a4a 100644 --- a/tests/components/qnap_qsw/test_config_flow.py +++ b/tests/components/qnap_qsw/test_config_flow.py @@ -24,7 +24,7 @@ DHCP_SERVICE_INFO = dhcp.DhcpServiceInfo( ) TEST_PASSWORD = "test-password" -TEST_URL = "test-url" +TEST_URL = f"http://{DHCP_SERVICE_INFO.ip}" TEST_USERNAME = "test-username" @@ -187,6 +187,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: assert result2["data"] == { CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, + CONF_URL: TEST_URL, } assert len(mock_setup_entry.mock_calls) == 1 @@ -237,7 +238,7 @@ async def test_dhcp_connection_error(hass: HomeAssistant): }, ) - assert result["errors"] == {CONF_URL: "cannot_connect"} + assert result["errors"] == {"base": "cannot_connect"} async def test_dhcp_login_error(hass: HomeAssistant): From 72917f1d2c3a0a476b58f0004827530b0d62171d Mon Sep 17 00:00:00 2001 From: danaues Date: Fri, 1 Jul 2022 11:39:00 -0400 Subject: [PATCH 2064/3516] Lutron caseta ra3keypads (#74217) Co-authored-by: J. Nick Koston --- .../components/lutron_caseta/__init__.py | 31 +++-- .../lutron_caseta/device_trigger.py | 112 ++++++++++++------ .../lutron_caseta/test_device_trigger.py | 81 +++++++++++-- 3 files changed, 163 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index b4ce82a36c6..f2225900aad 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -250,6 +250,18 @@ def _area_and_name_from_name(device_name: str) -> tuple[str, str]: return UNASSIGNED_AREA, device_name +@callback +def async_get_lip_button(device_type: str, leap_button: int) -> int | None: + """Get the LIP button for a given LEAP button.""" + if ( + lip_buttons_name_to_num := DEVICE_TYPE_SUBTYPE_MAP_TO_LIP.get(device_type) + ) is None or ( + leap_button_num_to_name := LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP.get(device_type) + ) is None: + return None + return lip_buttons_name_to_num[leap_button_num_to_name[leap_button]] + + @callback def _async_subscribe_pico_remote_events( hass: HomeAssistant, @@ -271,21 +283,8 @@ def _async_subscribe_pico_remote_events( type_ = device["type"] area, name = _area_and_name_from_name(device["name"]) - button_number = device["button_number"] - # The original implementation used LIP instead of LEAP - # so we need to convert the button number to maintain compat - sub_type_to_lip_button = DEVICE_TYPE_SUBTYPE_MAP_TO_LIP[type_] - leap_button_to_sub_type = LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP[type_] - if (sub_type := leap_button_to_sub_type.get(button_number)) is None: - _LOGGER.error( - "Unknown LEAP button number %s is not in %s for %s (%s)", - button_number, - leap_button_to_sub_type, - name, - type_, - ) - return - lip_button_number = sub_type_to_lip_button[sub_type] + leap_button_number = device["button_number"] + lip_button_number = async_get_lip_button(type_, leap_button_number) hass_device = dev_reg.async_get_device({(DOMAIN, device["serial"])}) hass.bus.async_fire( @@ -294,7 +293,7 @@ def _async_subscribe_pico_remote_events( ATTR_SERIAL: device["serial"], ATTR_TYPE: type_, ATTR_BUTTON_NUMBER: lip_button_number, - ATTR_LEAP_BUTTON_NUMBER: button_number, + ATTR_LEAP_BUTTON_NUMBER: leap_button_number, ATTR_DEVICE_NAME: name, ATTR_DEVICE_ID: hass_device.id, ATTR_AREA_NAME: area, diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index e762e79a8d7..dcdf5d584a4 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -27,7 +27,7 @@ from .const import ( ACTION_PRESS, ACTION_RELEASE, ATTR_ACTION, - ATTR_BUTTON_NUMBER, + ATTR_LEAP_BUTTON_NUMBER, ATTR_SERIAL, CONF_SUBTYPE, DOMAIN, @@ -35,6 +35,12 @@ from .const import ( ) from .models import LutronCasetaData + +def _reverse_dict(forward_dict: dict) -> dict: + """Reverse a dictionary.""" + return {v: k for k, v in forward_dict.items()} + + SUPPORTED_INPUTS_EVENTS_TYPES = [ACTION_PRESS, ACTION_RELEASE] LUTRON_BUTTON_TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( @@ -52,9 +58,6 @@ PICO_2_BUTTON_BUTTON_TYPES_TO_LEAP = { "on": 0, "off": 2, } -LEAP_TO_PICO_2_BUTTON_BUTTON_TYPES = { - v: k for k, v in PICO_2_BUTTON_BUTTON_TYPES_TO_LEAP.items() -} PICO_2_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In(PICO_2_BUTTON_BUTTON_TYPES_TO_LIP), @@ -74,9 +77,6 @@ PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP = { "raise": 3, "lower": 4, } -LEAP_TO_PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES = { - v: k for k, v in PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP.items() -} PICO_2_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In( @@ -96,9 +96,6 @@ PICO_3_BUTTON_BUTTON_TYPES_TO_LEAP = { "stop": 1, "off": 2, } -LEAP_TO_PICO_3_BUTTON_BUTTON_TYPES = { - v: k for k, v in PICO_3_BUTTON_BUTTON_TYPES_TO_LEAP.items() -} PICO_3_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In(PICO_3_BUTTON_BUTTON_TYPES_TO_LIP), @@ -119,9 +116,6 @@ PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP = { "raise": 3, "lower": 4, } -LEAP_TO_PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES = { - v: k for k, v in PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP.items() -} PICO_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In( @@ -186,9 +180,6 @@ PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LEAP = { "button_3": 3, "off": 4, } -LEAP_TO_PICO_4_BUTTON_SCENE_BUTTON_TYPES = { - v: k for k, v in PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LEAP.items() -} PICO_4_BUTTON_SCENE_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LIP), @@ -208,9 +199,6 @@ PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LEAP = { "group_2_button_1": 3, "group_2_button_2": 4, } -LEAP_TO_PICO_4_BUTTON_2_GROUP_BUTTON_TYPES = { - v: k for k, v in PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LEAP.items() -} PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LIP), @@ -271,15 +259,58 @@ FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LEAP = { "raise_4": 23, "lower_4": 24, } -LEAP_TO_FOUR_GROUP_REMOTE_BUTTON_TYPES = { - v: k for k, v in FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LEAP.items() -} FOUR_GROUP_REMOTE_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( { vol.Required(CONF_SUBTYPE): vol.In(FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LIP), } ) + +SUNNATA_KEYPAD_2_BUTTON_BUTTON_TYPES_TO_LEAP = { + "button_1": 1, + "button_2": 2, +} +SUNNATA_KEYPAD_2_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In( + SUNNATA_KEYPAD_2_BUTTON_BUTTON_TYPES_TO_LEAP + ), + } +) + + +SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP = { + "button_1": 1, + "button_2": 2, + "button_3": 3, + "raise": 19, + "lower": 18, +} +SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA = ( + LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In( + SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP + ), + } + ) +) + +SUNNATA_KEYPAD_4_BUTTON_BUTTON_TYPES_TO_LEAP = { + "button_1": 1, + "button_2": 2, + "button_3": 3, + "button_4": 4, +} +SUNNATA_KEYPAD_4_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In( + SUNNATA_KEYPAD_4_BUTTON_BUTTON_TYPES_TO_LEAP + ), + } +) + + DEVICE_TYPE_SCHEMA_MAP = { "Pico2Button": PICO_2_BUTTON_TRIGGER_SCHEMA, "Pico2ButtonRaiseLower": PICO_2_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA, @@ -290,6 +321,9 @@ DEVICE_TYPE_SCHEMA_MAP = { "Pico4ButtonZone": PICO_4_BUTTON_ZONE_TRIGGER_SCHEMA, "Pico4Button2Group": PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA, "FourGroupRemote": FOUR_GROUP_REMOTE_TRIGGER_SCHEMA, + "SunnataKeypad_2Button": SUNNATA_KEYPAD_2_BUTTON_TRIGGER_SCHEMA, + "SunnataKeypad_3ButtonRaiseLower": SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA, + "SunnataKeypad_4Button": SUNNATA_KEYPAD_4_BUTTON_TRIGGER_SCHEMA, } DEVICE_TYPE_SUBTYPE_MAP_TO_LIP = { @@ -304,16 +338,23 @@ DEVICE_TYPE_SUBTYPE_MAP_TO_LIP = { "FourGroupRemote": FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LIP, } +DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP = { + "Pico2Button": PICO_2_BUTTON_BUTTON_TYPES_TO_LEAP, + "Pico2ButtonRaiseLower": PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP, + "Pico3Button": PICO_3_BUTTON_BUTTON_TYPES_TO_LEAP, + "Pico3ButtonRaiseLower": PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP, + "Pico4Button": PICO_4_BUTTON_BUTTON_TYPES_TO_LEAP, + "Pico4ButtonScene": PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LEAP, + "Pico4ButtonZone": PICO_4_BUTTON_ZONE_BUTTON_TYPES_TO_LEAP, + "Pico4Button2Group": PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LEAP, + "FourGroupRemote": FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LEAP, + "SunnataKeypad_2Button": SUNNATA_KEYPAD_2_BUTTON_BUTTON_TYPES_TO_LEAP, + "SunnataKeypad_3ButtonRaiseLower": SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP, + "SunnataKeypad_4Button": SUNNATA_KEYPAD_4_BUTTON_BUTTON_TYPES_TO_LEAP, +} + LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP = { - "Pico2Button": LEAP_TO_PICO_2_BUTTON_BUTTON_TYPES, - "Pico2ButtonRaiseLower": LEAP_TO_PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES, - "Pico3Button": LEAP_TO_PICO_3_BUTTON_BUTTON_TYPES, - "Pico3ButtonRaiseLower": LEAP_TO_PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES, - "Pico4Button": LEAP_TO_PICO_4_BUTTON_BUTTON_TYPES, - "Pico4ButtonScene": LEAP_TO_PICO_4_BUTTON_SCENE_BUTTON_TYPES, - "Pico4ButtonZone": LEAP_TO_PICO_4_BUTTON_ZONE_BUTTON_TYPES, - "Pico4Button2Group": LEAP_TO_PICO_4_BUTTON_2_GROUP_BUTTON_TYPES, - "FourGroupRemote": LEAP_TO_FOUR_GROUP_REMOTE_BUTTON_TYPES, + k: _reverse_dict(v) for k, v in DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.items() } TRIGGER_SCHEMA = vol.Any( @@ -324,6 +365,9 @@ TRIGGER_SCHEMA = vol.Any( PICO_4_BUTTON_ZONE_TRIGGER_SCHEMA, PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA, FOUR_GROUP_REMOTE_TRIGGER_SCHEMA, + SUNNATA_KEYPAD_2_BUTTON_TRIGGER_SCHEMA, + SUNNATA_KEYPAD_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA, + SUNNATA_KEYPAD_4_BUTTON_TRIGGER_SCHEMA, ) @@ -354,7 +398,7 @@ async def async_get_triggers( if not (device := get_button_device_by_dr_id(hass, device_id)): raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}") - valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LIP.get(device["type"], {}) + valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.get(device["type"], {}) for trigger in SUPPORTED_INPUTS_EVENTS_TYPES: for subtype in valid_buttons: @@ -389,14 +433,14 @@ async def async_attach_trigger( 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_TO_LIP.get(device_type) + valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP[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: serial, - ATTR_BUTTON_NUMBER: valid_buttons[config[CONF_SUBTYPE]], + ATTR_LEAP_BUTTON_NUMBER: valid_buttons[config[CONF_SUBTYPE]], ATTR_ACTION: config[CONF_TYPE], }, } diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index bdf1e359673..54cd842f0ee 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -11,12 +11,12 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.components.lutron_caseta import ( ATTR_ACTION, ATTR_AREA_NAME, - ATTR_BUTTON_NUMBER, ATTR_DEVICE_NAME, ATTR_SERIAL, ATTR_TYPE, ) from homeassistant.components.lutron_caseta.const import ( + ATTR_LEAP_BUTTON_NUMBER, DOMAIN, LUTRON_CASETA_BUTTON_EVENT, MANUFACTURER, @@ -51,7 +51,23 @@ MOCK_BUTTON_DEVICES = [ "type": "Pico3ButtonRaiseLower", "model": "PJ2-3BRL-GXX-X01", "serial": 43845548, - } + }, + { + "Name": "Front Steps Sunnata Keypad", + "ID": 3, + "Area": {"Name": "Front Steps"}, + "Buttons": [ + {"Number": 7}, + {"Number": 8}, + {"Number": 9}, + {"Number": 10}, + {"Number": 11}, + ], + "leap_name": "Front Steps_Front Steps Sunnata Keypad", + "type": "SunnataKeypad_3ButtonRaiseLower", + "model": "PJ2-3BRL-GXX-X01", + "serial": 43845547, + }, ] @@ -144,12 +160,11 @@ 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.""" - - config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg) - data: LutronCasetaData = hass.data[DOMAIN][config_entry_id] - dr_button_devices = data.button_devices - device_id = list(dr_button_devices)[0] - device = dr_button_devices[device_id] + await _async_setup_lutron_with_picos(hass, device_reg) + device = MOCK_BUTTON_DEVICES[0] + dr = device_registry.async_get(hass) + dr_device = dr.async_get_device(identifiers={(DOMAIN, device["serial"])}) + device_id = dr_device.id assert await async_setup_component( hass, automation.DOMAIN, @@ -175,7 +190,51 @@ async def test_if_fires_on_button_event(hass, calls, device_reg): message = { ATTR_SERIAL: device.get("serial"), ATTR_TYPE: device.get("type"), - ATTR_BUTTON_NUMBER: 2, + ATTR_LEAP_BUTTON_NUMBER: 0, + ATTR_DEVICE_NAME: device["Name"], + ATTR_AREA_NAME: device.get("Area", {}).get("Name"), + ATTR_ACTION: "press", + } + hass.bus.async_fire(LUTRON_CASETA_BUTTON_EVENT, message) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["some"] == "test_trigger_button_press" + + +async def test_if_fires_on_button_event_without_lip(hass, calls, device_reg): + """Test for press trigger firing on a device that does not support lip.""" + await _async_setup_lutron_with_picos(hass, device_reg) + device = MOCK_BUTTON_DEVICES[1] + dr = device_registry.async_get(hass) + dr_device = dr.async_get_device(identifiers={(DOMAIN, device["serial"])}) + device_id = dr_device.id + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device_id, + CONF_TYPE: "press", + CONF_SUBTYPE: "button_1", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_button_press"}, + }, + }, + ] + }, + ) + + message = { + ATTR_SERIAL: device.get("serial"), + ATTR_TYPE: device.get("type"), + ATTR_LEAP_BUTTON_NUMBER: 1, ATTR_DEVICE_NAME: device["Name"], ATTR_AREA_NAME: device.get("Area", {}).get("Name"), ATTR_ACTION: "press", @@ -214,7 +273,7 @@ async def test_validate_trigger_config_no_device(hass, calls, device_reg): message = { ATTR_SERIAL: "123", ATTR_TYPE: "any", - ATTR_BUTTON_NUMBER: 3, + ATTR_LEAP_BUTTON_NUMBER: 0, ATTR_DEVICE_NAME: "any", ATTR_AREA_NAME: "area", ATTR_ACTION: "press", @@ -259,7 +318,7 @@ async def test_validate_trigger_config_unknown_device(hass, calls, device_reg): message = { ATTR_SERIAL: "123", ATTR_TYPE: "any", - ATTR_BUTTON_NUMBER: 3, + ATTR_LEAP_BUTTON_NUMBER: 0, ATTR_DEVICE_NAME: "any", ATTR_AREA_NAME: "area", ATTR_ACTION: "press", From 9211ba8371cdc935a6afbcfcf675a0fae6b5bdc5 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 1 Jul 2022 19:05:37 +0200 Subject: [PATCH 2065/3516] Improve type hints in template (#74294) --- .../components/template/binary_sensor.py | 13 +++++---- homeassistant/components/template/light.py | 27 ++++++++++--------- homeassistant/components/template/sensor.py | 2 +- homeassistant/components/template/switch.py | 14 +++++----- homeassistant/components/template/vacuum.py | 4 +-- 5 files changed, 33 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index ab7c88e8b8c..bad4a7d7059 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.binary_sensor import ( DOMAIN as BINARY_SENSOR_DOMAIN, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, + BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.const import ( @@ -208,16 +209,18 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity): ENTITY_ID_FORMAT, object_id, hass=hass ) - self._device_class = config.get(CONF_DEVICE_CLASS) + self._device_class: BinarySensorDeviceClass | None = config.get( + CONF_DEVICE_CLASS + ) self._template = config[CONF_STATE] - self._state = None + self._state: bool | None = None self._delay_cancel = None self._delay_on = None self._delay_on_raw = config.get(CONF_DELAY_ON) self._delay_off = None self._delay_off_raw = config.get(CONF_DELAY_OFF) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore state and register callbacks.""" if ( (self._delay_on_raw is not None or self._delay_off_raw is not None) @@ -283,12 +286,12 @@ class BinarySensorTemplate(TemplateEntity, BinarySensorEntity, RestoreEntity): self._delay_cancel = async_call_later(self.hass, delay, _set_state) @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if sensor is on.""" return self._state @property - def device_class(self): + def device_class(self) -> BinarySensorDeviceClass | None: """Return the sensor class of the binary sensor.""" return self._device_class diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index 807e3e79ef8..f4ad971c1d6 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -197,17 +198,17 @@ class LightTemplate(TemplateEntity, LightEntity): self._supports_transition = False @property - def brightness(self): + def brightness(self) -> int | None: """Return the brightness of the light.""" return self._brightness @property - def color_temp(self): + def color_temp(self) -> int | None: """Return the CT color value in mireds.""" return self._temperature @property - def max_mireds(self): + def max_mireds(self) -> int: """Return the max mireds value in mireds.""" if self._max_mireds is not None: return self._max_mireds @@ -215,7 +216,7 @@ class LightTemplate(TemplateEntity, LightEntity): return super().max_mireds @property - def min_mireds(self): + def min_mireds(self) -> int: """Return the min mireds value in mireds.""" if self._min_mireds is not None: return self._min_mireds @@ -223,27 +224,27 @@ class LightTemplate(TemplateEntity, LightEntity): return super().min_mireds @property - def white_value(self): + def white_value(self) -> int | None: """Return the white value.""" return self._white_value @property - def hs_color(self): + def hs_color(self) -> tuple[float, float] | None: """Return the hue and saturation color value [float, float].""" return self._color @property - def effect(self): + def effect(self) -> str | None: """Return the effect.""" return self._effect @property - def effect_list(self): + def effect_list(self) -> list[str] | None: """Return the effect list.""" return self._effect_list @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" supported_features = 0 if self._level_script is not None: @@ -261,11 +262,11 @@ class LightTemplate(TemplateEntity, LightEntity): return supported_features @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if device is on.""" return self._state - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" if self._template: self.add_template_attribute( @@ -345,7 +346,7 @@ class LightTemplate(TemplateEntity, LightEntity): ) await super().async_added_to_hass() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" optimistic_set = False # set optimistic states @@ -448,7 +449,7 @@ class LightTemplate(TemplateEntity, LightEntity): if optimistic_set: self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" if ATTR_TRANSITION in kwargs and self._supports_transition is True: await self.async_run_script( diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index ee1ddfa8c2d..8dcda988e9f 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -211,7 +211,7 @@ class SensorTemplate(TemplateSensor): ENTITY_ID_FORMAT, object_id, hass=hass ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.add_template_attribute( "_attr_native_value", self._template, None, self._update_state diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index f04f2b5ba7a..9f282cb9b11 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -1,6 +1,8 @@ """Support for switches which integrates with other components.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.components.switch import ( @@ -110,7 +112,7 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): self._template = config.get(CONF_VALUE_TEMPLATE) self._on_script = Script(hass, config[ON_ACTION], friendly_name, DOMAIN) self._off_script = Script(hass, config[OFF_ACTION], friendly_name, DOMAIN) - self._state = False + self._state: bool | None = False @callback def _update_state(self, result): @@ -129,7 +131,7 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): self._state = False - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" if self._template is None: @@ -147,18 +149,18 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): await super().async_added_to_hass() @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if device is on.""" return self._state - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Fire the on action.""" await self.async_run_script(self._on_script, context=self._context) if self._template is None: self._state = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Fire the off action.""" await self.async_run_script(self._off_script, context=self._context) if self._template is None: @@ -166,6 +168,6 @@ class SwitchTemplate(TemplateEntity, SwitchEntity, RestoreEntity): self.async_write_ha_state() @property - def assumed_state(self): + def assumed_state(self) -> bool: """State is assumed, if no template given.""" return self._template is None diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 5f306bfa5e1..0a74ee5c5fc 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -200,7 +200,7 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST] @property - def state(self): + def state(self) -> str | None: """Return the status of the vacuum cleaner.""" return self._state @@ -263,7 +263,7 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): self._attr_fan_speed_list, ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" if self._template is not None: self.add_template_attribute( From 1288085b31eb5bf15fb0e8f777b4101b168ec616 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Jul 2022 12:10:40 -0500 Subject: [PATCH 2066/3516] Revert scrape changes to 2022.6.6 (#74305) --- CODEOWNERS | 4 +- homeassistant/components/scrape/__init__.py | 63 ------ homeassistant/components/scrape/manifest.json | 3 +- homeassistant/components/scrape/sensor.py | 104 ++++----- homeassistant/generated/config_flows.py | 1 - tests/components/scrape/__init__.py | 40 +--- tests/components/scrape/test_config_flow.py | 194 ---------------- tests/components/scrape/test_init.py | 89 -------- tests/components/scrape/test_sensor.py | 209 ++++++++---------- 9 files changed, 137 insertions(+), 570 deletions(-) delete mode 100644 tests/components/scrape/test_config_flow.py delete mode 100644 tests/components/scrape/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 9845f5f7e5d..0c3f73db76d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -893,8 +893,8 @@ build.json @home-assistant/supervisor /homeassistant/components/scene/ @home-assistant/core /tests/components/scene/ @home-assistant/core /homeassistant/components/schluter/ @prairieapps -/homeassistant/components/scrape/ @fabaff @gjohansson-ST -/tests/components/scrape/ @fabaff @gjohansson-ST +/homeassistant/components/scrape/ @fabaff +/tests/components/scrape/ @fabaff /homeassistant/components/screenlogic/ @dieselrabbit @bdraco /tests/components/screenlogic/ @dieselrabbit @bdraco /homeassistant/components/script/ @home-assistant/core diff --git a/homeassistant/components/scrape/__init__.py b/homeassistant/components/scrape/__init__.py index 684be76b80d..f9222c126b5 100644 --- a/homeassistant/components/scrape/__init__.py +++ b/homeassistant/components/scrape/__init__.py @@ -1,64 +1 @@ """The scrape component.""" -from __future__ import annotations - -import httpx - -from homeassistant.components.rest.data import RestData -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_AUTHENTICATION, - CONF_HEADERS, - CONF_PASSWORD, - CONF_RESOURCE, - CONF_USERNAME, - CONF_VERIFY_SSL, - HTTP_DIGEST_AUTHENTICATION, -) -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady - -from .const import DOMAIN, PLATFORMS - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Scrape from a config entry.""" - - resource: str = entry.options[CONF_RESOURCE] - method: str = "GET" - payload: str | None = None - headers: str | None = entry.options.get(CONF_HEADERS) - verify_ssl: bool = entry.options[CONF_VERIFY_SSL] - username: str | None = entry.options.get(CONF_USERNAME) - password: str | None = entry.options.get(CONF_PASSWORD) - - auth: httpx.DigestAuth | tuple[str, str] | None = None - if username and password: - if entry.options.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: - auth = httpx.DigestAuth(username, password) - else: - auth = (username, password) - - rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl) - await rest.async_update() - - if rest.data is None: - raise ConfigEntryNotReady - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = rest - - entry.async_on_unload(entry.add_update_listener(async_update_listener)) - - hass.config_entries.async_setup_platforms(entry, PLATFORMS) - - return True - - -async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Update listener for options.""" - await hass.config_entries.async_reload(entry.entry_id) - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload Scrape config entry.""" - - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index 631af2e6051..b1ccbb354a9 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/scrape", "requirements": ["beautifulsoup4==4.11.1", "lxml==4.8.0"], "after_dependencies": ["rest"], - "codeowners": ["@fabaff", "@gjohansson-ST"], - "config_flow": true, + "codeowners": ["@fabaff"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index b6b8828ca73..e15f7c5ba97 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -1,11 +1,11 @@ """Support for getting data from websites with scraping.""" from __future__ import annotations -from datetime import timedelta import logging from typing import Any from bs4 import BeautifulSoup +import httpx import voluptuous as vol from homeassistant.components.rest.data import RestData @@ -16,16 +16,13 @@ from homeassistant.components.sensor import ( STATE_CLASSES_SCHEMA, SensorEntity, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( - CONF_ATTRIBUTE, CONF_AUTHENTICATION, CONF_DEVICE_CLASS, CONF_HEADERS, CONF_NAME, CONF_PASSWORD, CONF_RESOURCE, - CONF_SCAN_INTERVAL, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_VALUE_TEMPLATE, @@ -34,25 +31,26 @@ from homeassistant.const import ( HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import CONF_INDEX, CONF_SELECT, DEFAULT_NAME, DEFAULT_VERIFY_SSL, DOMAIN - _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=10) -ICON = "mdi:web" +CONF_ATTR = "attribute" +CONF_SELECT = "select" +CONF_INDEX = "index" + +DEFAULT_NAME = "Web scrape" +DEFAULT_VERIFY_SSL = True PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCE): cv.string, vol.Required(CONF_SELECT): cv.string, - vol.Optional(CONF_ATTRIBUTE): cv.string, + vol.Optional(CONF_ATTR): cv.string, vol.Optional(CONF_INDEX, default=0): cv.positive_int, vol.Optional(CONF_AUTHENTICATION): vol.In( [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] @@ -64,7 +62,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( 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.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, } ) @@ -77,47 +75,37 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Web scrape sensor.""" - _LOGGER.warning( - # Config flow added in Home Assistant Core 2022.7, remove import flow in 2022.9 - "Loading Scrape via platform setup has been deprecated in Home Assistant 2022.7 " - "Your configuration has been automatically imported and you can " - "remove it from your configuration.yaml" - ) + name: str = config[CONF_NAME] + resource: str = config[CONF_RESOURCE] + method: str = "GET" + payload: str | None = None + headers: str | None = config.get(CONF_HEADERS) + verify_ssl: bool = config[CONF_VERIFY_SSL] + select: str | None = config.get(CONF_SELECT) + attr: str | None = config.get(CONF_ATTR) + index: int = config[CONF_INDEX] + unit: str | None = config.get(CONF_UNIT_OF_MEASUREMENT) + device_class: str | None = config.get(CONF_DEVICE_CLASS) + state_class: str | None = config.get(CONF_STATE_CLASS) + username: str | None = config.get(CONF_USERNAME) + password: str | None = config.get(CONF_PASSWORD) + value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) - if config.get(CONF_VALUE_TEMPLATE): - template: Template = Template(config[CONF_VALUE_TEMPLATE]) - template.ensure_valid() - config[CONF_VALUE_TEMPLATE] = template.template - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={k: v for k, v in config.items() if k != CONF_SCAN_INTERVAL}, - ) - ) - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up the Scrape sensor entry.""" - name: str = entry.options[CONF_NAME] - resource: str = entry.options[CONF_RESOURCE] - select: str | None = entry.options.get(CONF_SELECT) - attr: str | None = entry.options.get(CONF_ATTRIBUTE) - index: int = int(entry.options[CONF_INDEX]) - unit: str | None = entry.options.get(CONF_UNIT_OF_MEASUREMENT) - device_class: str | None = entry.options.get(CONF_DEVICE_CLASS) - state_class: str | None = entry.options.get(CONF_STATE_CLASS) - value_template: str | None = entry.options.get(CONF_VALUE_TEMPLATE) - entry_id: str = entry.entry_id - - val_template: Template | None = None if value_template is not None: - val_template = Template(value_template, hass) + value_template.hass = hass - rest = hass.data[DOMAIN][entry.entry_id] + auth: httpx.DigestAuth | tuple[str, str] | None = None + if username and password: + if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: + auth = httpx.DigestAuth(username, password) + else: + auth = (username, password) + + rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl) + await rest.async_update() + + if rest.data is None: + raise PlatformNotReady async_add_entities( [ @@ -127,12 +115,10 @@ async def async_setup_entry( select, attr, index, - val_template, + value_template, unit, device_class, state_class, - entry_id, - resource, ) ], True, @@ -142,8 +128,6 @@ async def async_setup_entry( class ScrapeSensor(SensorEntity): """Representation of a web scrape sensor.""" - _attr_icon = ICON - def __init__( self, rest: RestData, @@ -155,8 +139,6 @@ class ScrapeSensor(SensorEntity): unit: str | None, device_class: str | None, state_class: str | None, - entry_id: str, - resource: str, ) -> None: """Initialize a web scrape sensor.""" self.rest = rest @@ -169,14 +151,6 @@ class ScrapeSensor(SensorEntity): self._attr_native_unit_of_measurement = unit self._attr_device_class = device_class self._attr_state_class = state_class - self._attr_unique_id = entry_id - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, entry_id)}, - manufacturer="Scrape", - name=name, - configuration_url=resource, - ) def _extract_value(self) -> Any: """Parse the html extraction in the executor.""" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 0b985f6e161..eba8bfe10c4 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -301,7 +301,6 @@ FLOWS = { "ruckus_unleashed", "sabnzbd", "samsungtv", - "scrape", "screenlogic", "season", "sense", diff --git a/tests/components/scrape/__init__.py b/tests/components/scrape/__init__.py index 37abb061e75..0ba9266a79d 100644 --- a/tests/components/scrape/__init__.py +++ b/tests/components/scrape/__init__.py @@ -2,42 +2,6 @@ from __future__ import annotations from typing import Any -from unittest.mock import patch - -from homeassistant.components.scrape.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry - - -async def init_integration( - hass: HomeAssistant, - config: dict[str, Any], - data: str, - entry_id: str = "1", - source: str = SOURCE_USER, -) -> MockConfigEntry: - """Set up the Scrape integration in Home Assistant.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - source=source, - data={}, - options=config, - entry_id=entry_id, - ) - - config_entry.add_to_hass(hass) - mocker = MockRestData(data) - with patch( - "homeassistant.components.scrape.RestData", - return_value=mocker, - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry def return_config( @@ -61,8 +25,6 @@ def return_config( "resource": "https://www.home-assistant.io", "select": select, "name": name, - "index": 0, - "verify_ssl": True, } if attribute: config["attribute"] = attribute @@ -76,7 +38,7 @@ def return_config( config["device_class"] = device_class if state_class: config["state_class"] = state_class - if username: + if authentication: config["authentication"] = authentication config["username"] = username config["password"] = password diff --git a/tests/components/scrape/test_config_flow.py b/tests/components/scrape/test_config_flow.py deleted file mode 100644 index 287004b1dd3..00000000000 --- a/tests/components/scrape/test_config_flow.py +++ /dev/null @@ -1,194 +0,0 @@ -"""Test the Scrape config flow.""" -from __future__ import annotations - -from unittest.mock import patch - -from homeassistant import config_entries -from homeassistant.components.scrape.const import CONF_INDEX, CONF_SELECT, DOMAIN -from homeassistant.const import ( - CONF_NAME, - CONF_RESOURCE, - CONF_VALUE_TEMPLATE, - CONF_VERIFY_SSL, -) -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) - -from . import MockRestData - -from tests.common import MockConfigEntry - - -async def test_form(hass: HomeAssistant) -> None: - """Test we get the form.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == RESULT_TYPE_FORM - - with patch( - "homeassistant.components.scrape.sensor.RestData", - return_value=MockRestData("test_scrape_sensor"), - ), patch( - "homeassistant.components.scrape.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_RESOURCE: "https://www.home-assistant.io", - CONF_NAME: "Release", - CONF_SELECT: ".current-version h1", - CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "Release" - assert result2["options"] == { - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0.0, - "verify_ssl": True, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_flow_success(hass: HomeAssistant) -> None: - """Test a successful import of yaml.""" - - with patch( - "homeassistant.components.scrape.sensor.RestData", - return_value=MockRestData("test_scrape_sensor"), - ), patch( - "homeassistant.components.scrape.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_RESOURCE: "https://www.home-assistant.io", - CONF_NAME: "Release", - CONF_SELECT: ".current-version h1", - CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}", - CONF_INDEX: 0, - CONF_VERIFY_SSL: True, - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "Release" - assert result2["options"] == { - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0, - "verify_ssl": True, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_flow_already_exist(hass: HomeAssistant) -> None: - """Test import of yaml already exist.""" - - MockConfigEntry( - domain=DOMAIN, - data={}, - options={ - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0, - "verify_ssl": True, - }, - ).add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.sensor.RestData", - return_value=MockRestData("test_scrape_sensor"), - ), patch( - "homeassistant.components.scrape.async_setup_entry", - return_value=True, - ): - result3 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_RESOURCE: "https://www.home-assistant.io", - CONF_NAME: "Release", - CONF_SELECT: ".current-version h1", - CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}", - CONF_INDEX: 0, - CONF_VERIFY_SSL: True, - }, - ) - await hass.async_block_till_done() - - assert result3["type"] == RESULT_TYPE_ABORT - assert result3["reason"] == "already_configured" - - -async def test_options_form(hass: HomeAssistant) -> None: - """Test we get the form in options.""" - - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={ - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0, - "verify_ssl": True, - }, - entry_id="1", - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor"), - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(entry.entry_id) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor"), - ): - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - "value_template": "{{ value.split(':')[1] }}", - "index": 1.0, - "verify_ssl": True, - }, - ) - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["data"] == { - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 1.0, - "verify_ssl": True, - } - entry_check = hass.config_entries.async_get_entry("1") - assert entry_check.state == config_entries.ConfigEntryState.LOADED - assert entry_check.update_listeners is not None diff --git a/tests/components/scrape/test_init.py b/tests/components/scrape/test_init.py deleted file mode 100644 index 021790e65c3..00000000000 --- a/tests/components/scrape/test_init.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Test Scrape component setup process.""" -from __future__ import annotations - -from unittest.mock import patch - -from homeassistant.components.scrape.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant - -from . import MockRestData - -from tests.common import MockConfigEntry - -TEST_CONFIG = { - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0, - "verify_ssl": True, -} - - -async def test_setup_entry(hass: HomeAssistant) -> None: - """Test setup entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options=TEST_CONFIG, - title="Release", - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor"), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.release") - assert state - - -async def test_setup_entry_no_data_fails(hass: HomeAssistant) -> None: - """Test setup entry no data fails.""" - entry = MockConfigEntry( - domain=DOMAIN, data={}, options=TEST_CONFIG, title="Release", entry_id="1" - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor_no_data"), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.ha_version") - assert state is None - entry = hass.config_entries.async_get_entry("1") - assert entry.state == ConfigEntryState.SETUP_RETRY - - -async def test_remove_entry(hass: HomeAssistant) -> None: - """Test remove entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options=TEST_CONFIG, - title="Release", - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor"), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.release") - assert state - - await hass.config_entries.async_remove(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.release") - assert not state diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index cd4e27e88a2..aaf156208ef 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -3,15 +3,10 @@ from __future__ import annotations from unittest.mock import patch -import pytest - from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.sensor.const import CONF_STATE_CLASS -from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_DEVICE_CLASS, - CONF_NAME, - CONF_RESOURCE, CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, @@ -20,20 +15,22 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component -from . import MockRestData, init_integration, return_config - -from tests.common import MockConfigEntry +from . import MockRestData, return_config DOMAIN = "scrape" async def test_scrape_sensor(hass: HomeAssistant) -> None: """Test Scrape sensor minimal.""" - await init_integration( - hass, - return_config(select=".current-version h1", name="HA version"), - "test_scrape_sensor", - ) + config = {"sensor": return_config(select=".current-version h1", name="HA version")} + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") assert state.state == "Current Version: 2021.12.10" @@ -41,15 +38,21 @@ async def test_scrape_sensor(hass: HomeAssistant) -> None: async def test_scrape_sensor_value_template(hass: HomeAssistant) -> None: """Test Scrape sensor with value template.""" - await init_integration( - hass, - return_config( + config = { + "sensor": return_config( select=".current-version h1", name="HA version", template="{{ value.split(':')[1] }}", - ), - "test_scrape_sensor", - ) + ) + } + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") assert state.state == "2021.12.10" @@ -57,18 +60,24 @@ async def test_scrape_sensor_value_template(hass: HomeAssistant) -> None: async def test_scrape_uom_and_classes(hass: HomeAssistant) -> None: """Test Scrape sensor for unit of measurement, device class and state class.""" - await init_integration( - hass, - return_config( + config = { + "sensor": return_config( select=".current-temp h3", name="Current Temp", template="{{ value.split(':')[1] }}", uom="°C", device_class="temperature", state_class="measurement", - ), - "test_scrape_uom_and_classes", - ) + ) + } + + mocker = MockRestData("test_scrape_uom_and_classes") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.current_temp") assert state.state == "22.1" @@ -79,28 +88,31 @@ async def test_scrape_uom_and_classes(hass: HomeAssistant) -> None: async def test_scrape_sensor_authentication(hass: HomeAssistant) -> None: """Test Scrape sensor with authentication.""" - await init_integration( - hass, - return_config( - select=".return", - name="Auth page", - username="user@secret.com", - password="12345678", - authentication="digest", - ), - "test_scrape_sensor_authentication", - ) - await init_integration( - hass, - return_config( - select=".return", - name="Auth page2", - username="user@secret.com", - password="12345678", - ), - "test_scrape_sensor_authentication", - entry_id="2", - ) + config = { + "sensor": [ + return_config( + select=".return", + name="Auth page", + username="user@secret.com", + password="12345678", + authentication="digest", + ), + return_config( + select=".return", + name="Auth page2", + username="user@secret.com", + password="12345678", + ), + ] + } + + mocker = MockRestData("test_scrape_sensor_authentication") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.auth_page") assert state.state == "secret text" @@ -110,11 +122,15 @@ async def test_scrape_sensor_authentication(hass: HomeAssistant) -> None: async def test_scrape_sensor_no_data(hass: HomeAssistant) -> None: """Test Scrape sensor fails on no data.""" - await init_integration( - hass, - return_config(select=".current-version h1", name="HA version"), - "test_scrape_sensor_no_data", - ) + config = {"sensor": return_config(select=".current-version h1", name="HA version")} + + mocker = MockRestData("test_scrape_sensor_no_data") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") assert state is None @@ -122,21 +138,14 @@ async def test_scrape_sensor_no_data(hass: HomeAssistant) -> None: async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: """Test Scrape sensor no data on refresh.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - source=SOURCE_USER, - data={}, - options=return_config(select=".current-version h1", name="HA version"), - entry_id="1", - ) + config = {"sensor": return_config(select=".current-version h1", name="HA version")} - config_entry.add_to_hass(hass) mocker = MockRestData("test_scrape_sensor") with patch( - "homeassistant.components.scrape.RestData", + "homeassistant.components.scrape.sensor.RestData", return_value=mocker, ): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") @@ -153,17 +162,20 @@ async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: async def test_scrape_sensor_attribute_and_tag(hass: HomeAssistant) -> None: """Test Scrape sensor with attribute and tag.""" - await init_integration( - hass, - return_config(select="div", name="HA class", index=1, attribute="class"), - "test_scrape_sensor", - ) - await init_integration( - hass, - return_config(select="template", name="HA template"), - "test_scrape_sensor", - entry_id="2", - ) + config = { + "sensor": [ + return_config(select="div", name="HA class", index=1, attribute="class"), + return_config(select="template", name="HA template"), + ] + } + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.ha_class") assert state.state == "['links']" @@ -173,55 +185,22 @@ async def test_scrape_sensor_attribute_and_tag(hass: HomeAssistant) -> None: async def test_scrape_sensor_errors(hass: HomeAssistant) -> None: """Test Scrape sensor handle errors.""" - await init_integration( - hass, - return_config(select="div", name="HA class", index=5, attribute="class"), - "test_scrape_sensor", - ) - await init_integration( - hass, - return_config(select="div", name="HA class2", attribute="classes"), - "test_scrape_sensor", - entry_id="2", - ) - - state = hass.states.get("sensor.ha_class") - assert state.state == STATE_UNKNOWN - state2 = hass.states.get("sensor.ha_class2") - assert state2.state == STATE_UNKNOWN - - -async def test_import(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None: - """Test the Scrape sensor import.""" config = { - "sensor": { - "platform": "scrape", - "resource": "https://www.home-assistant.io", - "select": ".current-version h1", - "name": "HA Version", - "index": 0, - "verify_ssl": True, - "value_template": "{{ value.split(':')[1] }}", - } + "sensor": [ + return_config(select="div", name="HA class", index=5, attribute="class"), + return_config(select="div", name="HA class2", attribute="classes"), + ] } mocker = MockRestData("test_scrape_sensor") with patch( - "homeassistant.components.scrape.RestData", + "homeassistant.components.scrape.sensor.RestData", return_value=mocker, ): assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() - assert ( - "Loading Scrape via platform setup has been deprecated in Home Assistant" - in caplog.text - ) - - assert hass.config_entries.async_entries(DOMAIN) - options = hass.config_entries.async_entries(DOMAIN)[0].options - assert options[CONF_NAME] == "HA Version" - assert options[CONF_RESOURCE] == "https://www.home-assistant.io" - - state = hass.states.get("sensor.ha_version") - assert state.state == "2021.12.10" + state = hass.states.get("sensor.ha_class") + assert state.state == STATE_UNKNOWN + state2 = hass.states.get("sensor.ha_class2") + assert state2.state == STATE_UNKNOWN From 135d104430f7dc4c30ef420120b80c2a7b366a57 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 30 Jun 2022 23:48:50 +0200 Subject: [PATCH 2067/3516] Bump pyRFXtrx to 0.30.0 (#74146) --- homeassistant/components/rfxtrx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/rfxtrx/test_config_flow.py | 12 ++++-- tests/components/rfxtrx/test_cover.py | 37 ++++++++++--------- tests/components/rfxtrx/test_switch.py | 4 +- 6 files changed, 34 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index cfe1049c888..3439fbba70c 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -2,7 +2,7 @@ "domain": "rfxtrx", "name": "RFXCOM RFXtrx", "documentation": "https://www.home-assistant.io/integrations/rfxtrx", - "requirements": ["pyRFXtrx==0.29.0"], + "requirements": ["pyRFXtrx==0.30.0"], "codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 84fb3e6894d..ca304ff6764 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1336,7 +1336,7 @@ pyMetEireann==2021.8.0 pyMetno==0.9.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.29.0 +pyRFXtrx==0.30.0 # homeassistant.components.switchmate # pySwitchmate==0.4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b15b9446de3..cd67a1a72d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -914,7 +914,7 @@ pyMetEireann==2021.8.0 pyMetno==0.9.0 # homeassistant.components.rfxtrx -pyRFXtrx==0.29.0 +pyRFXtrx==0.30.0 # homeassistant.components.tibber pyTibber==0.22.3 diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index a756bf26b9f..2c695d71d2e 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -847,7 +847,7 @@ async def test_options_configure_rfy_cover_device(hass): result["flow_id"], user_input={ "automatic_add": True, - "event_code": "071a000001020301", + "event_code": "0C1a0000010203010000000000", }, ) @@ -863,7 +863,10 @@ async def test_options_configure_rfy_cover_device(hass): await hass.async_block_till_done() - assert entry.data["devices"]["071a000001020301"]["venetian_blind_mode"] == "EU" + assert ( + entry.data["devices"]["0C1a0000010203010000000000"]["venetian_blind_mode"] + == "EU" + ) device_registry = dr.async_get(hass) device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) @@ -897,7 +900,10 @@ async def test_options_configure_rfy_cover_device(hass): await hass.async_block_till_done() - assert entry.data["devices"]["071a000001020301"]["venetian_blind_mode"] == "EU" + assert ( + entry.data["devices"]["0C1a0000010203010000000000"]["venetian_blind_mode"] + == "EU" + ) def test_get_serial_by_id_no_dir(): diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py index e3d44edda82..3be41d9233e 100644 --- a/tests/components/rfxtrx/test_cover.py +++ b/tests/components/rfxtrx/test_cover.py @@ -146,8 +146,11 @@ async def test_rfy_cover(hass, rfxtrx): "071a000001020301": { "venetian_blind_mode": "Unknown", }, - "071a000001020302": {"venetian_blind_mode": "US"}, - "071a000001020303": {"venetian_blind_mode": "EU"}, + "0c1a0000010203010000000000": { + "venetian_blind_mode": "Unknown", + }, + "0c1a0000010203020000000000": {"venetian_blind_mode": "US"}, + "0c1a0000010203030000000000": {"venetian_blind_mode": "EU"}, } ) mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) @@ -199,9 +202,9 @@ async def test_rfy_cover(hass, rfxtrx): ) assert rfxtrx.transport.send.mock_calls == [ - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x01\x00")), - call(bytearray(b"\x08\x1a\x00\x01\x01\x02\x03\x01\x01")), - call(bytearray(b"\x08\x1a\x00\x02\x01\x02\x03\x01\x03")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x01\x00\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x01\x01\x02\x03\x01\x01\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x02\x01\x02\x03\x01\x03\x00\x00\x00\x00")), ] # Test a blind with venetian mode set to US @@ -252,12 +255,12 @@ async def test_rfy_cover(hass, rfxtrx): ) assert rfxtrx.transport.send.mock_calls == [ - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x02\x00")), - call(bytearray(b"\x08\x1a\x00\x01\x01\x02\x03\x02\x0F")), - call(bytearray(b"\x08\x1a\x00\x02\x01\x02\x03\x02\x10")), - call(bytearray(b"\x08\x1a\x00\x03\x01\x02\x03\x02\x11")), - call(bytearray(b"\x08\x1a\x00\x04\x01\x02\x03\x02\x12")), - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x02\x00")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x02\x00\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x01\x01\x02\x03\x02\x0F\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x02\x01\x02\x03\x02\x10\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x03\x01\x02\x03\x02\x11\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x04\x01\x02\x03\x02\x12\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x02\x00\x00\x00\x00\x00")), ] # Test a blind with venetian mode set to EU @@ -308,10 +311,10 @@ async def test_rfy_cover(hass, rfxtrx): ) assert rfxtrx.transport.send.mock_calls == [ - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x03\x00")), - call(bytearray(b"\x08\x1a\x00\x01\x01\x02\x03\x03\x11")), - call(bytearray(b"\x08\x1a\x00\x02\x01\x02\x03\x03\x12")), - call(bytearray(b"\x08\x1a\x00\x03\x01\x02\x03\x03\x0F")), - call(bytearray(b"\x08\x1a\x00\x04\x01\x02\x03\x03\x10")), - call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x03\x00")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x03\x00\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x01\x01\x02\x03\x03\x11\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x02\x01\x02\x03\x03\x12\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x03\x01\x02\x03\x03\x0F\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x04\x01\x02\x03\x03\x10\x00\x00\x00\x00")), + call(bytearray(b"\x0C\x1a\x00\x00\x01\x02\x03\x03\x00\x00\x00\x00\x00")), ] diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index 4da7f1d9881..4d92c6fa332 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -11,8 +11,8 @@ from homeassistant.core import State from tests.common import MockConfigEntry, mock_restore_cache from tests.components.rfxtrx.conftest import create_rfx_test_cfg -EVENT_RFY_ENABLE_SUN_AUTO = "081a00000301010113" -EVENT_RFY_DISABLE_SUN_AUTO = "081a00000301010114" +EVENT_RFY_ENABLE_SUN_AUTO = "0C1a0000030101011300000003" +EVENT_RFY_DISABLE_SUN_AUTO = "0C1a0000030101011400000003" async def test_one_switch(hass, rfxtrx): From 1351b83731aa8b19231b0f053f60b2d408147d6e Mon Sep 17 00:00:00 2001 From: Christopher Hoage Date: Thu, 30 Jun 2022 13:06:22 -0700 Subject: [PATCH 2068/3516] Bump venstarcolortouch to 0.17 (#74271) --- homeassistant/components/venstar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index e63c75792bf..2f3331af6e2 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -3,7 +3,7 @@ "name": "Venstar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/venstar", - "requirements": ["venstarcolortouch==0.16"], + "requirements": ["venstarcolortouch==0.17"], "codeowners": ["@garbled1"], "iot_class": "local_polling", "loggers": ["venstarcolortouch"] diff --git a/requirements_all.txt b/requirements_all.txt index ca304ff6764..84501dbce79 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2387,7 +2387,7 @@ vehicle==0.4.0 velbus-aio==2022.6.2 # homeassistant.components.venstar -venstarcolortouch==0.16 +venstarcolortouch==0.17 # homeassistant.components.vilfo vilfo-api-client==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd67a1a72d7..de9ca72060d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1590,7 +1590,7 @@ vehicle==0.4.0 velbus-aio==2022.6.2 # homeassistant.components.venstar -venstarcolortouch==0.16 +venstarcolortouch==0.17 # homeassistant.components.vilfo vilfo-api-client==0.3.2 From b61530742ef2a887a979b14d6aaf0e1094d0b72a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Jul 2022 00:19:40 -0500 Subject: [PATCH 2069/3516] Fix key collision between platforms in esphome state updates (#74273) --- homeassistant/components/esphome/__init__.py | 19 +--- .../components/esphome/entry_data.py | 92 ++++++++++++++----- 2 files changed, 73 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 2e88a883dc1..0c1eac3aa45 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -150,11 +150,6 @@ async def async_setup_entry( # noqa: C901 hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop) ) - @callback - def async_on_state(state: EntityState) -> None: - """Send dispatcher updates when a new state is received.""" - entry_data.async_update_state(hass, state) - @callback def async_on_service_call(service: HomeassistantServiceCall) -> None: """Call service when user automation in ESPHome config is triggered.""" @@ -288,7 +283,7 @@ async def async_setup_entry( # noqa: C901 entity_infos, services = await cli.list_entities_services() await entry_data.async_update_static_infos(hass, entry, entity_infos) await _setup_services(hass, entry_data, services) - await cli.subscribe_states(async_on_state) + await cli.subscribe_states(entry_data.async_update_state) await cli.subscribe_service_calls(async_on_service_call) await cli.subscribe_home_assistant_states(async_on_state_subscription) @@ -568,7 +563,6 @@ async def platform_async_setup_entry( @callback def async_list_entities(infos: list[EntityInfo]) -> None: """Update entities of this platform when entities are listed.""" - key_to_component = entry_data.key_to_component old_infos = entry_data.info[component_key] new_infos: dict[int, EntityInfo] = {} add_entities = [] @@ -587,12 +581,10 @@ async def platform_async_setup_entry( entity = entity_type(entry_data, component_key, info.key) add_entities.append(entity) new_infos[info.key] = info - key_to_component[info.key] = component_key # Remove old entities for info in old_infos.values(): entry_data.async_remove_entity(hass, component_key, info.key) - key_to_component.pop(info.key, None) # First copy the now-old info into the backup object entry_data.old_info[component_key] = entry_data.info[component_key] @@ -714,13 +706,8 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): ) self.async_on_remove( - async_dispatcher_connect( - self.hass, - ( - f"esphome_{self._entry_id}" - f"_update_{self._component_key}_{self._key}" - ), - self._on_state_update, + self._entry_data.async_subscribe_state_update( + self._component_key, self._key, self._on_state_update ) ) diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index d4bcc67db4a..8eb56e6fdb6 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -12,26 +12,40 @@ from aioesphomeapi import ( APIClient, APIVersion, BinarySensorInfo, + BinarySensorState, CameraInfo, + CameraState, ClimateInfo, + ClimateState, CoverInfo, + CoverState, DeviceInfo, EntityInfo, EntityState, FanInfo, + FanState, LightInfo, + LightState, LockInfo, + LockState, MediaPlayerInfo, + MediaPlayerState, NumberInfo, + NumberState, SelectInfo, + SelectState, SensorInfo, + SensorState, SwitchInfo, + SwitchState, TextSensorInfo, + TextSensorState, UserService, ) from aioesphomeapi.model import ButtonInfo from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store @@ -41,20 +55,37 @@ _LOGGER = logging.getLogger(__name__) # Mapping from ESPHome info type to HA platform INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { - BinarySensorInfo: "binary_sensor", - ButtonInfo: "button", - CameraInfo: "camera", - ClimateInfo: "climate", - CoverInfo: "cover", - FanInfo: "fan", - LightInfo: "light", - LockInfo: "lock", - MediaPlayerInfo: "media_player", - NumberInfo: "number", - SelectInfo: "select", - SensorInfo: "sensor", - SwitchInfo: "switch", - TextSensorInfo: "sensor", + BinarySensorInfo: Platform.BINARY_SENSOR, + ButtonInfo: Platform.BINARY_SENSOR, + CameraInfo: Platform.BINARY_SENSOR, + ClimateInfo: Platform.CLIMATE, + CoverInfo: Platform.COVER, + FanInfo: Platform.FAN, + LightInfo: Platform.LIGHT, + LockInfo: Platform.LOCK, + MediaPlayerInfo: Platform.MEDIA_PLAYER, + NumberInfo: Platform.NUMBER, + SelectInfo: Platform.SELECT, + SensorInfo: Platform.SENSOR, + SwitchInfo: Platform.SWITCH, + TextSensorInfo: Platform.SENSOR, +} + +STATE_TYPE_TO_COMPONENT_KEY = { + BinarySensorState: Platform.BINARY_SENSOR, + EntityState: Platform.BINARY_SENSOR, + CameraState: Platform.BINARY_SENSOR, + ClimateState: Platform.CLIMATE, + CoverState: Platform.COVER, + FanState: Platform.FAN, + LightState: Platform.LIGHT, + LockState: Platform.LOCK, + MediaPlayerState: Platform.MEDIA_PLAYER, + NumberState: Platform.NUMBER, + SelectState: Platform.SELECT, + SensorState: Platform.SENSOR, + SwitchState: Platform.SWITCH, + TextSensorState: Platform.SENSOR, } @@ -67,7 +98,6 @@ class RuntimeEntryData: store: Store state: dict[str, dict[int, EntityState]] = field(default_factory=dict) info: dict[str, dict[int, EntityInfo]] = field(default_factory=dict) - key_to_component: dict[int, str] = field(default_factory=dict) # A second list of EntityInfo objects # This is necessary for when an entity is being removed. HA requires @@ -81,6 +111,9 @@ class RuntimeEntryData: api_version: APIVersion = field(default_factory=APIVersion) cleanup_callbacks: list[Callable[[], None]] = field(default_factory=list) disconnect_callbacks: list[Callable[[], None]] = field(default_factory=list) + state_subscriptions: dict[tuple[str, int], Callable[[], None]] = field( + default_factory=dict + ) loaded_platforms: set[str] = field(default_factory=set) platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock) _storage_contents: dict[str, Any] | None = None @@ -125,18 +158,33 @@ class RuntimeEntryData: async_dispatcher_send(hass, signal, infos) @callback - def async_update_state(self, hass: HomeAssistant, state: EntityState) -> None: + def async_subscribe_state_update( + self, + component_key: str, + state_key: int, + entity_callback: Callable[[], None], + ) -> Callable[[], None]: + """Subscribe to state updates.""" + + def _unsubscribe() -> None: + self.state_subscriptions.pop((component_key, state_key)) + + self.state_subscriptions[(component_key, state_key)] = entity_callback + return _unsubscribe + + @callback + def async_update_state(self, state: EntityState) -> None: """Distribute an update of state information to the target.""" - component_key = self.key_to_component[state.key] + component_key = STATE_TYPE_TO_COMPONENT_KEY[type(state)] + subscription_key = (component_key, state.key) self.state[component_key][state.key] = state - signal = f"esphome_{self.entry_id}_update_{component_key}_{state.key}" _LOGGER.debug( - "Dispatching update for component %s with state key %s: %s", - component_key, - state.key, + "Dispatching update with key %s: %s", + subscription_key, state, ) - async_dispatcher_send(hass, signal) + if subscription_key in self.state_subscriptions: + self.state_subscriptions[subscription_key]() @callback def async_update_device_state(self, hass: HomeAssistant) -> None: From 78e5296d07aa7f9428e95a3b372e86d07c784467 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 30 Jun 2022 16:59:35 -0400 Subject: [PATCH 2070/3516] Fix bad conditional in ZHA logbook (#74277) * Fix bad conditional in ZHA logbook * change syntax --- homeassistant/components/zha/logbook.py | 4 ++-- tests/components/zha/test_logbook.py | 29 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/logbook.py b/homeassistant/components/zha/logbook.py index 8140a5244f1..90d433be210 100644 --- a/homeassistant/components/zha/logbook.py +++ b/homeassistant/components/zha/logbook.py @@ -74,8 +74,8 @@ def async_describe_events( else: message = f"{event_type} event was fired" - if event_data["params"]: - message = f"{message} with parameters: {event_data['params']}" + if params := event_data.get("params"): + message = f"{message} with parameters: {params}" return { LOGBOOK_ENTRY_NAME: device_name, diff --git a/tests/components/zha/test_logbook.py b/tests/components/zha/test_logbook.py index 6c28284b1e6..373a48c2d47 100644 --- a/tests/components/zha/test_logbook.py +++ b/tests/components/zha/test_logbook.py @@ -185,6 +185,27 @@ async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): }, }, ), + MockRow( + ZHA_EVENT, + { + CONF_DEVICE_ID: reg_device.id, + "device_ieee": str(ieee_address), + CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + "params": {}, + }, + ), + MockRow( + ZHA_EVENT, + { + CONF_DEVICE_ID: reg_device.id, + "device_ieee": str(ieee_address), + CONF_UNIQUE_ID: f"{str(ieee_address)}:1:0x0006", + "endpoint_id": 1, + "cluster_id": 6, + }, + ), ], ) @@ -201,6 +222,14 @@ async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): events[1]["message"] == "Zha Event was fired with parameters: {'test': 'test'}" ) + assert events[2]["name"] == "FakeManufacturer FakeModel" + assert events[2]["domain"] == "zha" + assert events[2]["message"] == "Zha Event was fired" + + assert events[3]["name"] == "FakeManufacturer FakeModel" + assert events[3]["domain"] == "zha" + assert events[3]["message"] == "Zha Event was fired" + async def test_zha_logbook_event_device_no_device(hass, mock_devices): """Test zha logbook events without device and without triggers.""" From 4817f9f905d9474b1b9810032ef7bd7c35482afe Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Jul 2022 00:40:05 -0700 Subject: [PATCH 2071/3516] Add scan interval to scrape sensor (#74285) --- homeassistant/components/scrape/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index a73dbc17c1c..b6b8828ca73 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -1,6 +1,7 @@ """Support for getting data from websites with scraping.""" from __future__ import annotations +from datetime import timedelta import logging from typing import Any @@ -44,6 +45,7 @@ from .const import CONF_INDEX, CONF_SELECT, DEFAULT_NAME, DEFAULT_VERIFY_SSL, DO _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(minutes=10) ICON = "mdi:web" PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( From 877803169bc2d0c50b0eb2d5d75a1b7201f7df63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Fri, 1 Jul 2022 11:52:46 +0200 Subject: [PATCH 2072/3516] Fix QNAP QSW DHCP discover bugs (#74291) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit qnqp_qsw: fix DHCP discover bugs Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/qnap_qsw/config_flow.py | 3 ++- homeassistant/components/qnap_qsw/strings.json | 6 ++++++ homeassistant/components/qnap_qsw/translations/en.json | 6 ++++++ tests/components/qnap_qsw/test_config_flow.py | 5 +++-- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qnap_qsw/config_flow.py b/homeassistant/components/qnap_qsw/config_flow.py index e9d11433021..bb42c9ea294 100644 --- a/homeassistant/components/qnap_qsw/config_flow.py +++ b/homeassistant/components/qnap_qsw/config_flow.py @@ -113,9 +113,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except LoginError: errors[CONF_PASSWORD] = "invalid_auth" except QswError: - errors[CONF_URL] = "cannot_connect" + errors["base"] = "cannot_connect" else: title = f"QNAP {system_board.get_product()} {self._discovered_mac}" + user_input[CONF_URL] = self._discovered_url return self.async_create_entry(title=title, data=user_input) return self.async_show_form( diff --git a/homeassistant/components/qnap_qsw/strings.json b/homeassistant/components/qnap_qsw/strings.json index 351245a9591..ba0cb28ba77 100644 --- a/homeassistant/components/qnap_qsw/strings.json +++ b/homeassistant/components/qnap_qsw/strings.json @@ -9,6 +9,12 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "step": { + "discovered_connection": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, "user": { "data": { "url": "[%key:common::config_flow::data::url%]", diff --git a/homeassistant/components/qnap_qsw/translations/en.json b/homeassistant/components/qnap_qsw/translations/en.json index b6f68f2f062..c75c2d76ac8 100644 --- a/homeassistant/components/qnap_qsw/translations/en.json +++ b/homeassistant/components/qnap_qsw/translations/en.json @@ -9,6 +9,12 @@ "invalid_auth": "Invalid authentication" }, "step": { + "discovered_connection": { + "data": { + "password": "Password", + "username": "Username" + } + }, "user": { "data": { "password": "Password", diff --git a/tests/components/qnap_qsw/test_config_flow.py b/tests/components/qnap_qsw/test_config_flow.py index 0b7072dd602..02f873c6a4a 100644 --- a/tests/components/qnap_qsw/test_config_flow.py +++ b/tests/components/qnap_qsw/test_config_flow.py @@ -24,7 +24,7 @@ DHCP_SERVICE_INFO = dhcp.DhcpServiceInfo( ) TEST_PASSWORD = "test-password" -TEST_URL = "test-url" +TEST_URL = f"http://{DHCP_SERVICE_INFO.ip}" TEST_USERNAME = "test-username" @@ -187,6 +187,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: assert result2["data"] == { CONF_USERNAME: TEST_USERNAME, CONF_PASSWORD: TEST_PASSWORD, + CONF_URL: TEST_URL, } assert len(mock_setup_entry.mock_calls) == 1 @@ -237,7 +238,7 @@ async def test_dhcp_connection_error(hass: HomeAssistant): }, ) - assert result["errors"] == {CONF_URL: "cannot_connect"} + assert result["errors"] == {"base": "cannot_connect"} async def test_dhcp_login_error(hass: HomeAssistant): From 2305c625fb4c87b75e2389f6316649498e392cd3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 1 Jul 2022 12:10:40 -0500 Subject: [PATCH 2073/3516] Revert scrape changes to 2022.6.6 (#74305) --- CODEOWNERS | 4 +- homeassistant/components/scrape/__init__.py | 63 ------ homeassistant/components/scrape/manifest.json | 3 +- homeassistant/components/scrape/sensor.py | 104 ++++----- homeassistant/generated/config_flows.py | 1 - tests/components/scrape/__init__.py | 40 +--- tests/components/scrape/test_config_flow.py | 194 ---------------- tests/components/scrape/test_init.py | 89 -------- tests/components/scrape/test_sensor.py | 209 ++++++++---------- 9 files changed, 137 insertions(+), 570 deletions(-) delete mode 100644 tests/components/scrape/test_config_flow.py delete mode 100644 tests/components/scrape/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 1b6d88b5464..5f5588c0b91 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -891,8 +891,8 @@ build.json @home-assistant/supervisor /homeassistant/components/scene/ @home-assistant/core /tests/components/scene/ @home-assistant/core /homeassistant/components/schluter/ @prairieapps -/homeassistant/components/scrape/ @fabaff @gjohansson-ST -/tests/components/scrape/ @fabaff @gjohansson-ST +/homeassistant/components/scrape/ @fabaff +/tests/components/scrape/ @fabaff /homeassistant/components/screenlogic/ @dieselrabbit @bdraco /tests/components/screenlogic/ @dieselrabbit @bdraco /homeassistant/components/script/ @home-assistant/core diff --git a/homeassistant/components/scrape/__init__.py b/homeassistant/components/scrape/__init__.py index 684be76b80d..f9222c126b5 100644 --- a/homeassistant/components/scrape/__init__.py +++ b/homeassistant/components/scrape/__init__.py @@ -1,64 +1 @@ """The scrape component.""" -from __future__ import annotations - -import httpx - -from homeassistant.components.rest.data import RestData -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_AUTHENTICATION, - CONF_HEADERS, - CONF_PASSWORD, - CONF_RESOURCE, - CONF_USERNAME, - CONF_VERIFY_SSL, - HTTP_DIGEST_AUTHENTICATION, -) -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady - -from .const import DOMAIN, PLATFORMS - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Scrape from a config entry.""" - - resource: str = entry.options[CONF_RESOURCE] - method: str = "GET" - payload: str | None = None - headers: str | None = entry.options.get(CONF_HEADERS) - verify_ssl: bool = entry.options[CONF_VERIFY_SSL] - username: str | None = entry.options.get(CONF_USERNAME) - password: str | None = entry.options.get(CONF_PASSWORD) - - auth: httpx.DigestAuth | tuple[str, str] | None = None - if username and password: - if entry.options.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: - auth = httpx.DigestAuth(username, password) - else: - auth = (username, password) - - rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl) - await rest.async_update() - - if rest.data is None: - raise ConfigEntryNotReady - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = rest - - entry.async_on_unload(entry.add_update_listener(async_update_listener)) - - hass.config_entries.async_setup_platforms(entry, PLATFORMS) - - return True - - -async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Update listener for options.""" - await hass.config_entries.async_reload(entry.entry_id) - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload Scrape config entry.""" - - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index 631af2e6051..b1ccbb354a9 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/scrape", "requirements": ["beautifulsoup4==4.11.1", "lxml==4.8.0"], "after_dependencies": ["rest"], - "codeowners": ["@fabaff", "@gjohansson-ST"], - "config_flow": true, + "codeowners": ["@fabaff"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index b6b8828ca73..e15f7c5ba97 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -1,11 +1,11 @@ """Support for getting data from websites with scraping.""" from __future__ import annotations -from datetime import timedelta import logging from typing import Any from bs4 import BeautifulSoup +import httpx import voluptuous as vol from homeassistant.components.rest.data import RestData @@ -16,16 +16,13 @@ from homeassistant.components.sensor import ( STATE_CLASSES_SCHEMA, SensorEntity, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( - CONF_ATTRIBUTE, CONF_AUTHENTICATION, CONF_DEVICE_CLASS, CONF_HEADERS, CONF_NAME, CONF_PASSWORD, CONF_RESOURCE, - CONF_SCAN_INTERVAL, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_VALUE_TEMPLATE, @@ -34,25 +31,26 @@ from homeassistant.const import ( HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import CONF_INDEX, CONF_SELECT, DEFAULT_NAME, DEFAULT_VERIFY_SSL, DOMAIN - _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=10) -ICON = "mdi:web" +CONF_ATTR = "attribute" +CONF_SELECT = "select" +CONF_INDEX = "index" + +DEFAULT_NAME = "Web scrape" +DEFAULT_VERIFY_SSL = True PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_RESOURCE): cv.string, vol.Required(CONF_SELECT): cv.string, - vol.Optional(CONF_ATTRIBUTE): cv.string, + vol.Optional(CONF_ATTR): cv.string, vol.Optional(CONF_INDEX, default=0): cv.positive_int, vol.Optional(CONF_AUTHENTICATION): vol.In( [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] @@ -64,7 +62,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( 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.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, } ) @@ -77,47 +75,37 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Web scrape sensor.""" - _LOGGER.warning( - # Config flow added in Home Assistant Core 2022.7, remove import flow in 2022.9 - "Loading Scrape via platform setup has been deprecated in Home Assistant 2022.7 " - "Your configuration has been automatically imported and you can " - "remove it from your configuration.yaml" - ) + name: str = config[CONF_NAME] + resource: str = config[CONF_RESOURCE] + method: str = "GET" + payload: str | None = None + headers: str | None = config.get(CONF_HEADERS) + verify_ssl: bool = config[CONF_VERIFY_SSL] + select: str | None = config.get(CONF_SELECT) + attr: str | None = config.get(CONF_ATTR) + index: int = config[CONF_INDEX] + unit: str | None = config.get(CONF_UNIT_OF_MEASUREMENT) + device_class: str | None = config.get(CONF_DEVICE_CLASS) + state_class: str | None = config.get(CONF_STATE_CLASS) + username: str | None = config.get(CONF_USERNAME) + password: str | None = config.get(CONF_PASSWORD) + value_template: Template | None = config.get(CONF_VALUE_TEMPLATE) - if config.get(CONF_VALUE_TEMPLATE): - template: Template = Template(config[CONF_VALUE_TEMPLATE]) - template.ensure_valid() - config[CONF_VALUE_TEMPLATE] = template.template - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={k: v for k, v in config.items() if k != CONF_SCAN_INTERVAL}, - ) - ) - - -async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback -) -> None: - """Set up the Scrape sensor entry.""" - name: str = entry.options[CONF_NAME] - resource: str = entry.options[CONF_RESOURCE] - select: str | None = entry.options.get(CONF_SELECT) - attr: str | None = entry.options.get(CONF_ATTRIBUTE) - index: int = int(entry.options[CONF_INDEX]) - unit: str | None = entry.options.get(CONF_UNIT_OF_MEASUREMENT) - device_class: str | None = entry.options.get(CONF_DEVICE_CLASS) - state_class: str | None = entry.options.get(CONF_STATE_CLASS) - value_template: str | None = entry.options.get(CONF_VALUE_TEMPLATE) - entry_id: str = entry.entry_id - - val_template: Template | None = None if value_template is not None: - val_template = Template(value_template, hass) + value_template.hass = hass - rest = hass.data[DOMAIN][entry.entry_id] + auth: httpx.DigestAuth | tuple[str, str] | None = None + if username and password: + if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: + auth = httpx.DigestAuth(username, password) + else: + auth = (username, password) + + rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl) + await rest.async_update() + + if rest.data is None: + raise PlatformNotReady async_add_entities( [ @@ -127,12 +115,10 @@ async def async_setup_entry( select, attr, index, - val_template, + value_template, unit, device_class, state_class, - entry_id, - resource, ) ], True, @@ -142,8 +128,6 @@ async def async_setup_entry( class ScrapeSensor(SensorEntity): """Representation of a web scrape sensor.""" - _attr_icon = ICON - def __init__( self, rest: RestData, @@ -155,8 +139,6 @@ class ScrapeSensor(SensorEntity): unit: str | None, device_class: str | None, state_class: str | None, - entry_id: str, - resource: str, ) -> None: """Initialize a web scrape sensor.""" self.rest = rest @@ -169,14 +151,6 @@ class ScrapeSensor(SensorEntity): self._attr_native_unit_of_measurement = unit self._attr_device_class = device_class self._attr_state_class = state_class - self._attr_unique_id = entry_id - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, entry_id)}, - manufacturer="Scrape", - name=name, - configuration_url=resource, - ) def _extract_value(self) -> Any: """Parse the html extraction in the executor.""" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index af4b8481873..d7ed9159d7f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -300,7 +300,6 @@ FLOWS = { "ruckus_unleashed", "sabnzbd", "samsungtv", - "scrape", "screenlogic", "season", "sense", diff --git a/tests/components/scrape/__init__.py b/tests/components/scrape/__init__.py index 37abb061e75..0ba9266a79d 100644 --- a/tests/components/scrape/__init__.py +++ b/tests/components/scrape/__init__.py @@ -2,42 +2,6 @@ from __future__ import annotations from typing import Any -from unittest.mock import patch - -from homeassistant.components.scrape.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER -from homeassistant.core import HomeAssistant - -from tests.common import MockConfigEntry - - -async def init_integration( - hass: HomeAssistant, - config: dict[str, Any], - data: str, - entry_id: str = "1", - source: str = SOURCE_USER, -) -> MockConfigEntry: - """Set up the Scrape integration in Home Assistant.""" - - config_entry = MockConfigEntry( - domain=DOMAIN, - source=source, - data={}, - options=config, - entry_id=entry_id, - ) - - config_entry.add_to_hass(hass) - mocker = MockRestData(data) - with patch( - "homeassistant.components.scrape.RestData", - return_value=mocker, - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry def return_config( @@ -61,8 +25,6 @@ def return_config( "resource": "https://www.home-assistant.io", "select": select, "name": name, - "index": 0, - "verify_ssl": True, } if attribute: config["attribute"] = attribute @@ -76,7 +38,7 @@ def return_config( config["device_class"] = device_class if state_class: config["state_class"] = state_class - if username: + if authentication: config["authentication"] = authentication config["username"] = username config["password"] = password diff --git a/tests/components/scrape/test_config_flow.py b/tests/components/scrape/test_config_flow.py deleted file mode 100644 index 287004b1dd3..00000000000 --- a/tests/components/scrape/test_config_flow.py +++ /dev/null @@ -1,194 +0,0 @@ -"""Test the Scrape config flow.""" -from __future__ import annotations - -from unittest.mock import patch - -from homeassistant import config_entries -from homeassistant.components.scrape.const import CONF_INDEX, CONF_SELECT, DOMAIN -from homeassistant.const import ( - CONF_NAME, - CONF_RESOURCE, - CONF_VALUE_TEMPLATE, - CONF_VERIFY_SSL, -) -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) - -from . import MockRestData - -from tests.common import MockConfigEntry - - -async def test_form(hass: HomeAssistant) -> None: - """Test we get the form.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == RESULT_TYPE_FORM - - with patch( - "homeassistant.components.scrape.sensor.RestData", - return_value=MockRestData("test_scrape_sensor"), - ), patch( - "homeassistant.components.scrape.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_RESOURCE: "https://www.home-assistant.io", - CONF_NAME: "Release", - CONF_SELECT: ".current-version h1", - CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "Release" - assert result2["options"] == { - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0.0, - "verify_ssl": True, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_flow_success(hass: HomeAssistant) -> None: - """Test a successful import of yaml.""" - - with patch( - "homeassistant.components.scrape.sensor.RestData", - return_value=MockRestData("test_scrape_sensor"), - ), patch( - "homeassistant.components.scrape.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_RESOURCE: "https://www.home-assistant.io", - CONF_NAME: "Release", - CONF_SELECT: ".current-version h1", - CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}", - CONF_INDEX: 0, - CONF_VERIFY_SSL: True, - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "Release" - assert result2["options"] == { - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0, - "verify_ssl": True, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_flow_already_exist(hass: HomeAssistant) -> None: - """Test import of yaml already exist.""" - - MockConfigEntry( - domain=DOMAIN, - data={}, - options={ - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0, - "verify_ssl": True, - }, - ).add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.sensor.RestData", - return_value=MockRestData("test_scrape_sensor"), - ), patch( - "homeassistant.components.scrape.async_setup_entry", - return_value=True, - ): - result3 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_RESOURCE: "https://www.home-assistant.io", - CONF_NAME: "Release", - CONF_SELECT: ".current-version h1", - CONF_VALUE_TEMPLATE: "{{ value.split(':')[1] }}", - CONF_INDEX: 0, - CONF_VERIFY_SSL: True, - }, - ) - await hass.async_block_till_done() - - assert result3["type"] == RESULT_TYPE_ABORT - assert result3["reason"] == "already_configured" - - -async def test_options_form(hass: HomeAssistant) -> None: - """Test we get the form in options.""" - - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={ - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0, - "verify_ssl": True, - }, - entry_id="1", - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor"), - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(entry.entry_id) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor"), - ): - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - "value_template": "{{ value.split(':')[1] }}", - "index": 1.0, - "verify_ssl": True, - }, - ) - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["data"] == { - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 1.0, - "verify_ssl": True, - } - entry_check = hass.config_entries.async_get_entry("1") - assert entry_check.state == config_entries.ConfigEntryState.LOADED - assert entry_check.update_listeners is not None diff --git a/tests/components/scrape/test_init.py b/tests/components/scrape/test_init.py deleted file mode 100644 index 021790e65c3..00000000000 --- a/tests/components/scrape/test_init.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Test Scrape component setup process.""" -from __future__ import annotations - -from unittest.mock import patch - -from homeassistant.components.scrape.const import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant - -from . import MockRestData - -from tests.common import MockConfigEntry - -TEST_CONFIG = { - "resource": "https://www.home-assistant.io", - "name": "Release", - "select": ".current-version h1", - "value_template": "{{ value.split(':')[1] }}", - "index": 0, - "verify_ssl": True, -} - - -async def test_setup_entry(hass: HomeAssistant) -> None: - """Test setup entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options=TEST_CONFIG, - title="Release", - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor"), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.release") - assert state - - -async def test_setup_entry_no_data_fails(hass: HomeAssistant) -> None: - """Test setup entry no data fails.""" - entry = MockConfigEntry( - domain=DOMAIN, data={}, options=TEST_CONFIG, title="Release", entry_id="1" - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor_no_data"), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.ha_version") - assert state is None - entry = hass.config_entries.async_get_entry("1") - assert entry.state == ConfigEntryState.SETUP_RETRY - - -async def test_remove_entry(hass: HomeAssistant) -> None: - """Test remove entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options=TEST_CONFIG, - title="Release", - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.scrape.RestData", - return_value=MockRestData("test_scrape_sensor"), - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.release") - assert state - - await hass.config_entries.async_remove(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.release") - assert not state diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index cd4e27e88a2..aaf156208ef 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -3,15 +3,10 @@ from __future__ import annotations from unittest.mock import patch -import pytest - from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.sensor.const import CONF_STATE_CLASS -from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_DEVICE_CLASS, - CONF_NAME, - CONF_RESOURCE, CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CELSIUS, @@ -20,20 +15,22 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component -from . import MockRestData, init_integration, return_config - -from tests.common import MockConfigEntry +from . import MockRestData, return_config DOMAIN = "scrape" async def test_scrape_sensor(hass: HomeAssistant) -> None: """Test Scrape sensor minimal.""" - await init_integration( - hass, - return_config(select=".current-version h1", name="HA version"), - "test_scrape_sensor", - ) + config = {"sensor": return_config(select=".current-version h1", name="HA version")} + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") assert state.state == "Current Version: 2021.12.10" @@ -41,15 +38,21 @@ async def test_scrape_sensor(hass: HomeAssistant) -> None: async def test_scrape_sensor_value_template(hass: HomeAssistant) -> None: """Test Scrape sensor with value template.""" - await init_integration( - hass, - return_config( + config = { + "sensor": return_config( select=".current-version h1", name="HA version", template="{{ value.split(':')[1] }}", - ), - "test_scrape_sensor", - ) + ) + } + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") assert state.state == "2021.12.10" @@ -57,18 +60,24 @@ async def test_scrape_sensor_value_template(hass: HomeAssistant) -> None: async def test_scrape_uom_and_classes(hass: HomeAssistant) -> None: """Test Scrape sensor for unit of measurement, device class and state class.""" - await init_integration( - hass, - return_config( + config = { + "sensor": return_config( select=".current-temp h3", name="Current Temp", template="{{ value.split(':')[1] }}", uom="°C", device_class="temperature", state_class="measurement", - ), - "test_scrape_uom_and_classes", - ) + ) + } + + mocker = MockRestData("test_scrape_uom_and_classes") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.current_temp") assert state.state == "22.1" @@ -79,28 +88,31 @@ async def test_scrape_uom_and_classes(hass: HomeAssistant) -> None: async def test_scrape_sensor_authentication(hass: HomeAssistant) -> None: """Test Scrape sensor with authentication.""" - await init_integration( - hass, - return_config( - select=".return", - name="Auth page", - username="user@secret.com", - password="12345678", - authentication="digest", - ), - "test_scrape_sensor_authentication", - ) - await init_integration( - hass, - return_config( - select=".return", - name="Auth page2", - username="user@secret.com", - password="12345678", - ), - "test_scrape_sensor_authentication", - entry_id="2", - ) + config = { + "sensor": [ + return_config( + select=".return", + name="Auth page", + username="user@secret.com", + password="12345678", + authentication="digest", + ), + return_config( + select=".return", + name="Auth page2", + username="user@secret.com", + password="12345678", + ), + ] + } + + mocker = MockRestData("test_scrape_sensor_authentication") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.auth_page") assert state.state == "secret text" @@ -110,11 +122,15 @@ async def test_scrape_sensor_authentication(hass: HomeAssistant) -> None: async def test_scrape_sensor_no_data(hass: HomeAssistant) -> None: """Test Scrape sensor fails on no data.""" - await init_integration( - hass, - return_config(select=".current-version h1", name="HA version"), - "test_scrape_sensor_no_data", - ) + config = {"sensor": return_config(select=".current-version h1", name="HA version")} + + mocker = MockRestData("test_scrape_sensor_no_data") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") assert state is None @@ -122,21 +138,14 @@ async def test_scrape_sensor_no_data(hass: HomeAssistant) -> None: async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: """Test Scrape sensor no data on refresh.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - source=SOURCE_USER, - data={}, - options=return_config(select=".current-version h1", name="HA version"), - entry_id="1", - ) + config = {"sensor": return_config(select=".current-version h1", name="HA version")} - config_entry.add_to_hass(hass) mocker = MockRestData("test_scrape_sensor") with patch( - "homeassistant.components.scrape.RestData", + "homeassistant.components.scrape.sensor.RestData", return_value=mocker, ): - await hass.config_entries.async_setup(config_entry.entry_id) + assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() state = hass.states.get("sensor.ha_version") @@ -153,17 +162,20 @@ async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: async def test_scrape_sensor_attribute_and_tag(hass: HomeAssistant) -> None: """Test Scrape sensor with attribute and tag.""" - await init_integration( - hass, - return_config(select="div", name="HA class", index=1, attribute="class"), - "test_scrape_sensor", - ) - await init_integration( - hass, - return_config(select="template", name="HA template"), - "test_scrape_sensor", - entry_id="2", - ) + config = { + "sensor": [ + return_config(select="div", name="HA class", index=1, attribute="class"), + return_config(select="template", name="HA template"), + ] + } + + mocker = MockRestData("test_scrape_sensor") + with patch( + "homeassistant.components.scrape.sensor.RestData", + return_value=mocker, + ): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() state = hass.states.get("sensor.ha_class") assert state.state == "['links']" @@ -173,55 +185,22 @@ async def test_scrape_sensor_attribute_and_tag(hass: HomeAssistant) -> None: async def test_scrape_sensor_errors(hass: HomeAssistant) -> None: """Test Scrape sensor handle errors.""" - await init_integration( - hass, - return_config(select="div", name="HA class", index=5, attribute="class"), - "test_scrape_sensor", - ) - await init_integration( - hass, - return_config(select="div", name="HA class2", attribute="classes"), - "test_scrape_sensor", - entry_id="2", - ) - - state = hass.states.get("sensor.ha_class") - assert state.state == STATE_UNKNOWN - state2 = hass.states.get("sensor.ha_class2") - assert state2.state == STATE_UNKNOWN - - -async def test_import(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None: - """Test the Scrape sensor import.""" config = { - "sensor": { - "platform": "scrape", - "resource": "https://www.home-assistant.io", - "select": ".current-version h1", - "name": "HA Version", - "index": 0, - "verify_ssl": True, - "value_template": "{{ value.split(':')[1] }}", - } + "sensor": [ + return_config(select="div", name="HA class", index=5, attribute="class"), + return_config(select="div", name="HA class2", attribute="classes"), + ] } mocker = MockRestData("test_scrape_sensor") with patch( - "homeassistant.components.scrape.RestData", + "homeassistant.components.scrape.sensor.RestData", return_value=mocker, ): assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() - assert ( - "Loading Scrape via platform setup has been deprecated in Home Assistant" - in caplog.text - ) - - assert hass.config_entries.async_entries(DOMAIN) - options = hass.config_entries.async_entries(DOMAIN)[0].options - assert options[CONF_NAME] == "HA Version" - assert options[CONF_RESOURCE] == "https://www.home-assistant.io" - - state = hass.states.get("sensor.ha_version") - assert state.state == "2021.12.10" + state = hass.states.get("sensor.ha_class") + assert state.state == STATE_UNKNOWN + state2 = hass.states.get("sensor.ha_class2") + assert state2.state == STATE_UNKNOWN From bc41832c71ee41df9cc2070e044697fbded1f5fa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Jul 2022 10:12:02 -0700 Subject: [PATCH 2074/3516] Bumped version to 2022.7.0b2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 60a3d6817b5..de1148a3932 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 2b1e14a40f6..92a7a962cbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.0b1" +version = "2022.7.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 810e29f1ef668ae7d2a2c74188eb243949a62040 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Jul 2022 11:01:07 -0700 Subject: [PATCH 2075/3516] Guard creating areas in onboarding (#74306) --- homeassistant/components/onboarding/views.py | 8 +++++--- tests/components/onboarding/test_views.py | 7 ++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index 7f40ad87e84..c29fb7edf3a 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -156,9 +156,11 @@ class UserOnboardingView(_BaseOnboardingView): area_registry = ar.async_get(hass) for area in DEFAULT_AREAS: - area_registry.async_create( - translations[f"component.onboarding.area.{area}"] - ) + name = translations[f"component.onboarding.area.{area}"] + # Guard because area might have been created by an automatically + # set up integration. + if not area_registry.async_get_area_by_name(name): + area_registry.async_create(name) await self._async_mark_done(hass) diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 982f5b86e65..204eb6bf772 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -144,6 +144,12 @@ async def test_onboarding_user_already_done(hass, hass_storage, hass_client_no_a async def test_onboarding_user(hass, hass_storage, hass_client_no_auth): """Test creating a new user.""" + area_registry = ar.async_get(hass) + + # Create an existing area to mimic an integration creating an area + # before onboarding is done. + area_registry.async_create("Living Room") + assert await async_setup_component(hass, "person", {}) assert await async_setup_component(hass, "onboarding", {}) await hass.async_block_till_done() @@ -194,7 +200,6 @@ async def test_onboarding_user(hass, hass_storage, hass_client_no_auth): ) # Validate created areas - area_registry = ar.async_get(hass) assert len(area_registry.areas) == 3 assert sorted(area.name for area in area_registry.async_list_areas()) == [ "Bedroom", From 647a023776bedb5e1605390c91b8a89e5a68dfff Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 2 Jul 2022 00:25:27 +0000 Subject: [PATCH 2076/3516] [ci skip] Translation update --- .../components/adguard/translations/hu.json | 2 +- .../components/anthemav/translations/ca.json | 19 ++++++++++++++ .../components/anthemav/translations/hu.json | 19 ++++++++++++++ .../components/anthemav/translations/ja.json | 19 ++++++++++++++ .../components/anthemav/translations/no.json | 19 ++++++++++++++ .../anthemav/translations/pt-BR.json | 19 ++++++++++++++ .../components/anthemav/translations/tr.json | 19 ++++++++++++++ .../components/awair/translations/tr.json | 7 ++++++ .../components/climacell/translations/hu.json | 2 +- .../components/esphome/translations/ca.json | 4 +-- .../components/esphome/translations/et.json | 4 +-- .../components/esphome/translations/no.json | 4 +-- .../esphome/translations/pt-BR.json | 4 +-- .../components/esphome/translations/tr.json | 6 ++--- .../esphome/translations/zh-Hant.json | 4 +-- .../components/hive/translations/tr.json | 9 ++++++- .../components/lcn/translations/hu.json | 1 + .../components/lcn/translations/tr.json | 1 + .../lg_soundbar/translations/ca.json | 18 +++++++++++++ .../lg_soundbar/translations/et.json | 18 +++++++++++++ .../lg_soundbar/translations/hu.json | 18 +++++++++++++ .../lg_soundbar/translations/ja.json | 18 +++++++++++++ .../lg_soundbar/translations/no.json | 18 +++++++++++++ .../lg_soundbar/translations/pt-BR.json | 18 +++++++++++++ .../lg_soundbar/translations/tr.json | 18 +++++++++++++ .../lg_soundbar/translations/zh-Hant.json | 18 +++++++++++++ .../components/life360/translations/et.json | 2 +- .../components/life360/translations/hu.json | 23 +++++++++++++++++ .../components/life360/translations/tr.json | 25 ++++++++++++++++++- .../components/nest/translations/ja.json | 1 + .../components/nest/translations/tr.json | 2 +- .../components/nina/translations/hu.json | 22 ++++++++++++++++ .../components/nina/translations/tr.json | 22 ++++++++++++++++ .../overkiz/translations/sensor.tr.json | 5 ++++ .../components/plugwise/translations/tr.json | 4 +-- .../components/qnap_qsw/translations/ca.json | 6 +++++ .../components/qnap_qsw/translations/de.json | 6 +++++ .../components/qnap_qsw/translations/fr.json | 6 +++++ .../components/qnap_qsw/translations/hu.json | 6 +++++ .../components/qnap_qsw/translations/ja.json | 6 +++++ .../components/qnap_qsw/translations/no.json | 6 +++++ .../qnap_qsw/translations/pt-BR.json | 6 +++++ .../components/qnap_qsw/translations/tr.json | 6 +++++ .../qnap_qsw/translations/zh-Hant.json | 6 +++++ .../simplepush/translations/tr.json | 21 ++++++++++++++++ .../soundtouch/translations/ca.json | 21 ++++++++++++++++ .../soundtouch/translations/de.json | 21 ++++++++++++++++ .../soundtouch/translations/et.json | 21 ++++++++++++++++ .../soundtouch/translations/fr.json | 21 ++++++++++++++++ .../soundtouch/translations/hu.json | 21 ++++++++++++++++ .../soundtouch/translations/ja.json | 21 ++++++++++++++++ .../soundtouch/translations/no.json | 21 ++++++++++++++++ .../soundtouch/translations/pt-BR.json | 21 ++++++++++++++++ .../soundtouch/translations/tr.json | 21 ++++++++++++++++ .../soundtouch/translations/zh-Hant.json | 21 ++++++++++++++++ 55 files changed, 676 insertions(+), 21 deletions(-) create mode 100644 homeassistant/components/anthemav/translations/ca.json create mode 100644 homeassistant/components/anthemav/translations/hu.json create mode 100644 homeassistant/components/anthemav/translations/ja.json create mode 100644 homeassistant/components/anthemav/translations/no.json create mode 100644 homeassistant/components/anthemav/translations/pt-BR.json create mode 100644 homeassistant/components/anthemav/translations/tr.json create mode 100644 homeassistant/components/lg_soundbar/translations/ca.json create mode 100644 homeassistant/components/lg_soundbar/translations/et.json create mode 100644 homeassistant/components/lg_soundbar/translations/hu.json create mode 100644 homeassistant/components/lg_soundbar/translations/ja.json create mode 100644 homeassistant/components/lg_soundbar/translations/no.json create mode 100644 homeassistant/components/lg_soundbar/translations/pt-BR.json create mode 100644 homeassistant/components/lg_soundbar/translations/tr.json create mode 100644 homeassistant/components/lg_soundbar/translations/zh-Hant.json create mode 100644 homeassistant/components/simplepush/translations/tr.json create mode 100644 homeassistant/components/soundtouch/translations/ca.json create mode 100644 homeassistant/components/soundtouch/translations/de.json create mode 100644 homeassistant/components/soundtouch/translations/et.json create mode 100644 homeassistant/components/soundtouch/translations/fr.json create mode 100644 homeassistant/components/soundtouch/translations/hu.json create mode 100644 homeassistant/components/soundtouch/translations/ja.json create mode 100644 homeassistant/components/soundtouch/translations/no.json create mode 100644 homeassistant/components/soundtouch/translations/pt-BR.json create mode 100644 homeassistant/components/soundtouch/translations/tr.json create mode 100644 homeassistant/components/soundtouch/translations/zh-Hant.json diff --git a/homeassistant/components/adguard/translations/hu.json b/homeassistant/components/adguard/translations/hu.json index ffbf454b62b..59f33efee29 100644 --- a/homeassistant/components/adguard/translations/hu.json +++ b/homeassistant/components/adguard/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", - "existing_instance_updated": "Friss\u00edtette a megl\u00e9v\u0151 konfigur\u00e1ci\u00f3t." + "existing_instance_updated": "A megl\u00e9v\u0151 konfigur\u00e1ci\u00f3 friss\u00edtve." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s" diff --git a/homeassistant/components/anthemav/translations/ca.json b/homeassistant/components/anthemav/translations/ca.json new file mode 100644 index 00000000000..723883d5c1a --- /dev/null +++ b/homeassistant/components/anthemav/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "cannot_receive_deviceinfo": "No s'ha pogut obtenir l'adre\u00e7a MAC. Assegura't que el dispositiu estigui enc\u00e8s" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/hu.json b/homeassistant/components/anthemav/translations/hu.json new file mode 100644 index 00000000000..f13544fff61 --- /dev/null +++ b/homeassistant/components/anthemav/translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "cannot_receive_deviceinfo": "Nem siker\u00fclt lek\u00e9rni a MAC-c\u00edmet. Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a k\u00e9sz\u00fcl\u00e9k be van kapcsolva" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/ja.json b/homeassistant/components/anthemav/translations/ja.json new file mode 100644 index 00000000000..8c87d02e557 --- /dev/null +++ b/homeassistant/components/anthemav/translations/ja.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "cannot_receive_deviceinfo": "MAC\u30a2\u30c9\u30ec\u30b9\u306e\u53d6\u5f97\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u30c7\u30d0\u30a4\u30b9\u306e\u96fb\u6e90\u304c\u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "port": "\u30dd\u30fc\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/no.json b/homeassistant/components/anthemav/translations/no.json new file mode 100644 index 00000000000..e7b3f66ae8d --- /dev/null +++ b/homeassistant/components/anthemav/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "cannot_receive_deviceinfo": "Kunne ikke hente MAC-adressen. S\u00f8rg for at enheten er sl\u00e5tt p\u00e5" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/pt-BR.json b/homeassistant/components/anthemav/translations/pt-BR.json new file mode 100644 index 00000000000..309aca9b8ef --- /dev/null +++ b/homeassistant/components/anthemav/translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "cannot_receive_deviceinfo": "Falha ao recuperar o endere\u00e7o MAC. Verifique se o dispositivo est\u00e1 ligado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/tr.json b/homeassistant/components/anthemav/translations/tr.json new file mode 100644 index 00000000000..cbe85a5319c --- /dev/null +++ b/homeassistant/components/anthemav/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "cannot_receive_deviceinfo": "MAC Adresi al\u0131namad\u0131. Cihaz\u0131n a\u00e7\u0131k oldu\u011fundan emin olun" + }, + "step": { + "user": { + "data": { + "host": "Sunucu", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/tr.json b/homeassistant/components/awair/translations/tr.json index e8fa8ca1027..ef34620b5f7 100644 --- a/homeassistant/components/awair/translations/tr.json +++ b/homeassistant/components/awair/translations/tr.json @@ -17,6 +17,13 @@ }, "description": "L\u00fctfen Awair geli\u015ftirici eri\u015fim anahtar\u0131n\u0131 yeniden girin." }, + "reauth_confirm": { + "data": { + "access_token": "Eri\u015fim Anahtar\u0131", + "email": "E-posta" + }, + "description": "L\u00fctfen Awair geli\u015ftirici eri\u015fim anahtar\u0131n\u0131 yeniden girin." + }, "user": { "data": { "access_token": "Eri\u015fim Anahtar\u0131", diff --git a/homeassistant/components/climacell/translations/hu.json b/homeassistant/components/climacell/translations/hu.json index f65fa638ced..4cad1eaaa0f 100644 --- a/homeassistant/components/climacell/translations/hu.json +++ b/homeassistant/components/climacell/translations/hu.json @@ -6,7 +6,7 @@ "timestep": "Min. A NowCast el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt" }, "description": "Ha a `nowcast` el\u0151rejelz\u00e9si entit\u00e1s enged\u00e9lyez\u00e9s\u00e9t v\u00e1lasztja, be\u00e1ll\u00edthatja az egyes el\u0151rejelz\u00e9sek k\u00f6z\u00f6tti percek sz\u00e1m\u00e1t. A megadott el\u0151rejelz\u00e9sek sz\u00e1ma az el\u0151rejelz\u00e9sek k\u00f6z\u00f6tt kiv\u00e1lasztott percek sz\u00e1m\u00e1t\u00f3l f\u00fcgg.", - "title": "[%key:component::climacell::title%] be\u00e1ll\u00edt\u00e1sok friss\u00edt\u00e9se" + "title": "ClimaCell be\u00e1ll\u00edt\u00e1sok friss\u00edt\u00e9se" } } } diff --git a/homeassistant/components/esphome/translations/ca.json b/homeassistant/components/esphome/translations/ca.json index 4c990994e47..ef9814d3ac8 100644 --- a/homeassistant/components/esphome/translations/ca.json +++ b/homeassistant/components/esphome/translations/ca.json @@ -9,7 +9,7 @@ "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" + "resolve_error": "No s'ha pogut trobar l'adre\u00e7a de l'ESP. Si l'error persisteix, configura una adre\u00e7a IP est\u00e0tica." }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Amfitri\u00f3", "port": "Port" }, - "description": "Introdueix la informaci\u00f3 de connexi\u00f3 del teu node [ESPHome](https://esphomelib.com/)." + "description": "Introdueix la configuraci\u00f3 de connexi\u00f3 del node [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/esphome/translations/et.json b/homeassistant/components/esphome/translations/et.json index ea5119b190d..29fa98bafb0 100644 --- a/homeassistant/components/esphome/translations/et.json +++ b/homeassistant/components/esphome/translations/et.json @@ -9,7 +9,7 @@ "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" + "resolve_error": "ESP aadressi ei saa lahendada. Kui see t\u00f5rge p\u00fcsib, m\u00e4\u00e4ra staatiline IP-aadress" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "", "port": "Port" }, - "description": "Sisesta oma [ESPHome](https://esphomelib.com/) s\u00f5lme \u00fchenduse s\u00e4tted." + "description": "Sisesta oma [ESPHome]({esphome_url}) s\u00f5lme \u00fchenduse s\u00e4tted." } } } diff --git a/homeassistant/components/esphome/translations/no.json b/homeassistant/components/esphome/translations/no.json index 0d583893570..1fabb774aa2 100644 --- a/homeassistant/components/esphome/translations/no.json +++ b/homeassistant/components/esphome/translations/no.json @@ -9,7 +9,7 @@ "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)" + "resolve_error": "Kan ikke l\u00f8se adressen til ESP. Hvis denne feilen vedvarer, vennligst angi en statisk IP-adresse" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Vert", "port": "Port" }, - "description": "Vennligst fyll inn tilkoblingsinnstillinger for din [ESPHome](https://esphomelib.com/) node." + "description": "Vennligst skriv inn tilkoblingsinnstillingene for [ESPHome]( {esphome_url} )-noden." } } } diff --git a/homeassistant/components/esphome/translations/pt-BR.json b/homeassistant/components/esphome/translations/pt-BR.json index 737bc5020af..21fd9067be1 100644 --- a/homeassistant/components/esphome/translations/pt-BR.json +++ b/homeassistant/components/esphome/translations/pt-BR.json @@ -9,7 +9,7 @@ "connection_error": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao ESP. Por favor, verifique se o seu arquivo YAML cont\u00e9m uma linha 'api:'.", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_psk": "A chave de criptografia de transporte \u00e9 inv\u00e1lida. Certifique-se de que corresponde ao que voc\u00ea tem em sua configura\u00e7\u00e3o", - "resolve_error": "N\u00e3o \u00e9 poss\u00edvel resolver o endere\u00e7o do ESP. Se este erro persistir, por favor, defina um endere\u00e7o IP est\u00e1tico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "N\u00e3o \u00e9 poss\u00edvel resolver o endere\u00e7o do ESP. Se o erro persistir, defina um endere\u00e7o IP est\u00e1tico" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Nome do host", "port": "Porta" }, - "description": "Por favor insira as configura\u00e7\u00f5es de conex\u00e3o de seu n\u00f3 de [ESPHome] (https://esphomelib.com/)." + "description": "Insira as configura\u00e7\u00f5es de conex\u00e3o do seu n\u00f3 [ESPHome]( {esphome_url} )." } } } diff --git a/homeassistant/components/esphome/translations/tr.json b/homeassistant/components/esphome/translations/tr.json index b2cede2b572..d4388dde8b8 100644 --- a/homeassistant/components/esphome/translations/tr.json +++ b/homeassistant/components/esphome/translations/tr.json @@ -9,7 +9,7 @@ "connection_error": "ESP'ye ba\u011flan\u0131lam\u0131yor. L\u00fctfen YAML dosyan\u0131z\u0131n bir 'api:' sat\u0131r\u0131 i\u00e7erdi\u011finden emin olun.", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "invalid_psk": "Aktar\u0131m \u015fifreleme anahtar\u0131 ge\u00e7ersiz. L\u00fctfen yap\u0131land\u0131rman\u0131zda sahip oldu\u011funuzla e\u015fle\u015fti\u011finden emin olun", - "resolve_error": "ESP'nin adresi \u00e7\u00f6z\u00fclemiyor. Bu hata devam ederse, l\u00fctfen statik bir IP adresi ayarlay\u0131n: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "ESP'nin adresi \u00e7\u00f6z\u00fclemiyor. Bu hata devam ederse, l\u00fctfen statik bir IP adresi ayarlay\u0131n" }, "flow_title": "{name}", "step": { @@ -21,7 +21,7 @@ }, "discovery_confirm": { "description": "ESPHome d\u00fc\u011f\u00fcm\u00fcn\u00fc ` {name} ` Home Assistant'a eklemek istiyor musunuz?", - "title": "Ke\u015ffedilen ESPHome d\u00fc\u011f\u00fcm\u00fc" + "title": "ESPHome d\u00fc\u011f\u00fcm\u00fc ke\u015ffedildi" }, "encryption_key": { "data": { @@ -40,7 +40,7 @@ "host": "Sunucu", "port": "Port" }, - "description": "L\u00fctfen [ESPHome](https://esphomelib.com/) d\u00fc\u011f\u00fcm\u00fcn\u00fcz\u00fcn ba\u011flant\u0131 ayarlar\u0131n\u0131 girin." + "description": "L\u00fctfen [ESPHome]( {esphome_url} ) d\u00fc\u011f\u00fcm\u00fcn\u00fcz\u00fcn ba\u011flant\u0131 ayarlar\u0131n\u0131 girin." } } } diff --git a/homeassistant/components/esphome/translations/zh-Hant.json b/homeassistant/components/esphome/translations/zh-Hant.json index 976d0317faa..44f50a433e3 100644 --- a/homeassistant/components/esphome/translations/zh-Hant.json +++ b/homeassistant/components/esphome/translations/zh-Hant.json @@ -9,7 +9,7 @@ "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\u91d1\u9470\u7121\u6548\u3002\u8acb\u78ba\u5b9a\u8207\u8a2d\u5b9a\u5167\u6240\u8a2d\u5b9a\u4e4b\u91d1\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" + "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" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" }, - "description": "\u8acb\u8f38\u5165 [ESPHome](https://esphomelib.com/) \u7bc0\u9ede\u9023\u7dda\u8cc7\u8a0a\u3002" + "description": "\u8acb\u8f38\u5165 [ESPHome]({esphome_url}) \u7bc0\u9ede\u9023\u7dda\u8cc7\u8a0a\u3002" } } } diff --git a/homeassistant/components/hive/translations/tr.json b/homeassistant/components/hive/translations/tr.json index afc0ee66f03..158424f996a 100644 --- a/homeassistant/components/hive/translations/tr.json +++ b/homeassistant/components/hive/translations/tr.json @@ -20,6 +20,13 @@ "description": "Hive kimlik do\u011frulama kodunuzu girin. \n\n Ba\u015fka bir kod istemek i\u00e7in l\u00fctfen 0000 kodunu girin.", "title": "Hive \u0130ki Fakt\u00f6rl\u00fc Kimlik Do\u011frulama." }, + "configuration": { + "data": { + "device_name": "Cihaz ad\u0131" + }, + "description": "Hive yap\u0131land\u0131rman\u0131z\u0131 girin", + "title": "Hive Yap\u0131land\u0131rmas\u0131." + }, "reauth": { "data": { "password": "Parola", @@ -34,7 +41,7 @@ "scan_interval": "Tarama Aral\u0131\u011f\u0131 (saniye)", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "Hive oturum a\u00e7ma bilgilerinizi ve yap\u0131land\u0131rman\u0131z\u0131 girin.", + "description": "Hive giri\u015f bilgilerinizi girin.", "title": "Hive Giri\u015f yap" } } diff --git a/homeassistant/components/lcn/translations/hu.json b/homeassistant/components/lcn/translations/hu.json index 3e56a4a149b..f2b7566d838 100644 --- a/homeassistant/components/lcn/translations/hu.json +++ b/homeassistant/components/lcn/translations/hu.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "k\u00f3dz\u00e1rol\u00e1si k\u00f3d \u00e9rkezett", "fingerprint": "ujjlenyomatk\u00f3d \u00e9rkezett", "send_keys": "kulcs/gomb \u00e9rkezett", "transmitter": "t\u00e1vvez\u00e9rl\u0151 k\u00f3d \u00e9rkezett", diff --git a/homeassistant/components/lcn/translations/tr.json b/homeassistant/components/lcn/translations/tr.json index 2c2d94611d0..f3a40e165c6 100644 --- a/homeassistant/components/lcn/translations/tr.json +++ b/homeassistant/components/lcn/translations/tr.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "kod kilit kodu al\u0131nd\u0131", "fingerprint": "al\u0131nan parmak izi kodu", "send_keys": "al\u0131nan anahtarlar\u0131 g\u00f6nder", "transmitter": "verici kodu al\u0131nd\u0131", diff --git a/homeassistant/components/lg_soundbar/translations/ca.json b/homeassistant/components/lg_soundbar/translations/ca.json new file mode 100644 index 00000000000..8c445361eb8 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/et.json b/homeassistant/components/lg_soundbar/translations/et.json new file mode 100644 index 00000000000..227250382c0 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/et.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", + "existing_instance_updated": "V\u00e4rskendati olemasolevat konfiguratsiooni." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/hu.json b/homeassistant/components/lg_soundbar/translations/hu.json new file mode 100644 index 00000000000..c39033d273a --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "existing_instance_updated": "A megl\u00e9v\u0151 konfigur\u00e1ci\u00f3 friss\u00edtve." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/ja.json b/homeassistant/components/lg_soundbar/translations/ja.json new file mode 100644 index 00000000000..cd40cd88044 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/ja.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "existing_instance_updated": "\u65e2\u5b58\u306e\u69cb\u6210\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/no.json b/homeassistant/components/lg_soundbar/translations/no.json new file mode 100644 index 00000000000..41eef0e4c16 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "existing_instance_updated": "Oppdatert eksisterende konfigurasjon." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "host": "Vert" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/pt-BR.json b/homeassistant/components/lg_soundbar/translations/pt-BR.json new file mode 100644 index 00000000000..dfbff8cddc8 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "existing_instance_updated": "Configura\u00e7\u00e3o existente atualizada." + }, + "error": { + "cannot_connect": "Falhou ao conectar" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/tr.json b/homeassistant/components/lg_soundbar/translations/tr.json new file mode 100644 index 00000000000..5eb581847fb --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "existing_instance_updated": "Mevcut yap\u0131land\u0131rma g\u00fcncellendi." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Sunucu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/zh-Hant.json b/homeassistant/components/lg_soundbar/translations/zh-Hant.json new file mode 100644 index 00000000000..8680a863901 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/et.json b/homeassistant/components/life360/translations/et.json index c6e66af5d08..360aa8275f7 100644 --- a/homeassistant/components/life360/translations/et.json +++ b/homeassistant/components/life360/translations/et.json @@ -29,7 +29,7 @@ "username": "Kasutajanimi" }, "description": "T\u00e4psemate suvandite kohta leiad teemat [Life360 documentation]({docs_url}).\nTee seda enne uute kontode lisamist.", - "title": "Life360 konto teave" + "title": "Seadista Life360 konto" } } }, diff --git a/homeassistant/components/life360/translations/hu.json b/homeassistant/components/life360/translations/hu.json index 5dbd2898971..33f615d00c7 100644 --- a/homeassistant/components/life360/translations/hu.json +++ b/homeassistant/components/life360/translations/hu.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "create_entry": { @@ -9,11 +11,18 @@ }, "error": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_username": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "password": "Jelsz\u00f3", @@ -23,5 +32,19 @@ "title": "Life360 fi\u00f3kadatok" } } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Vezet\u00e9s megjelen\u00edt\u00e9se \u00e1llapotk\u00e9nt", + "driving_speed": "Menetsebess\u00e9g", + "limit_gps_acc": "A GPS pontoss\u00e1g\u00e1nak korl\u00e1toz\u00e1sa", + "max_gps_accuracy": "Maxim\u00e1lis GPS pontoss\u00e1g (m\u00e9ter)", + "set_drive_speed": "Vezet\u00e9si sebess\u00e9g k\u00fcsz\u00f6b\u00e9rt\u00e9k\u00e9nek be\u00e1ll\u00edt\u00e1sa" + }, + "title": "Fi\u00f3kbe\u00e1ll\u00edt\u00e1sok" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/tr.json b/homeassistant/components/life360/translations/tr.json index 1304151d948..52b083b83fd 100644 --- a/homeassistant/components/life360/translations/tr.json +++ b/homeassistant/components/life360/translations/tr.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", "unknown": "Beklenmeyen hata" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "invalid_username": "Ge\u00e7ersiz kullan\u0131c\u0131 ad\u0131", "unknown": "Beklenmeyen hata" }, "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, "description": "Geli\u015fmi\u015f se\u00e7enekleri ayarlamak i\u00e7in [Life360 belgelerine]( {docs_url} ) bak\u0131n.\n Bunu hesap eklemeden \u00f6nce yapmak isteyebilirsiniz.", - "title": "Life360 Hesap Bilgileri" + "title": "Life360 Hesab\u0131n\u0131 Yap\u0131land\u0131r" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "S\u00fcr\u00fc\u015f\u00fc durum olarak g\u00f6ster", + "driving_speed": "S\u00fcr\u00fc\u015f h\u0131z\u0131", + "limit_gps_acc": "GPS do\u011frulu\u011funu s\u0131n\u0131rlay\u0131n", + "max_gps_accuracy": "Maksimum GPS do\u011frulu\u011fu (metre)", + "set_drive_speed": "S\u00fcr\u00fc\u015f h\u0131z\u0131 e\u015fi\u011fini ayarla" + }, + "title": "Hesap Se\u00e7enekleri" } } } diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index ee9fcbba98d..9aff487997c 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -37,6 +37,7 @@ "data": { "cloud_project_id": "Google Cloud\u30d7\u30ed\u30b8\u30a7\u30af\u30c8ID" }, + "description": "\u4ee5\u4e0b\u306bCloud Project ID\u3092\u5165\u529b\u3057\u307e\u3059\u3002\u4f8b: *example-project-12345*\u3002[Google Cloud Console]({cloud_console_url}) \u307e\u305f\u306f[\u8a73\u7d30]({more_info_url})\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "\u30cd\u30b9\u30c8: \u30af\u30e9\u30a6\u30c9\u30d7\u30ed\u30b8\u30a7\u30af\u30c8ID\u3092\u5165\u529b" }, "create_cloud_project": { diff --git a/homeassistant/components/nest/translations/tr.json b/homeassistant/components/nest/translations/tr.json index 1732443184e..bc69b928ba1 100644 --- a/homeassistant/components/nest/translations/tr.json +++ b/homeassistant/components/nest/translations/tr.json @@ -23,7 +23,7 @@ "subscriber_error": "Bilinmeyen abone hatas\u0131, g\u00fcnl\u00fcklere bak\u0131n\u0131z", "timeout": "Zaman a\u015f\u0131m\u0131 do\u011frulama kodu", "unknown": "Beklenmeyen hata", - "wrong_project_id": "L\u00fctfen ge\u00e7erli bir Bulut Projesi Kimli\u011fi girin (bulunan Ayg\u0131t Eri\u015fimi Proje Kimli\u011fi)" + "wrong_project_id": "L\u00fctfen ge\u00e7erli bir Bulut Projesi Kimli\u011fi girin (Cihaz Eri\u015fimi Proje Kimli\u011fi ile ayn\u0131yd\u0131)" }, "step": { "auth": { diff --git a/homeassistant/components/nina/translations/hu.json b/homeassistant/components/nina/translations/hu.json index 24a0d59cbae..73c76f45124 100644 --- a/homeassistant/components/nina/translations/hu.json +++ b/homeassistant/components/nina/translations/hu.json @@ -23,5 +23,27 @@ "title": "V\u00e1lasszon v\u00e1rost/megy\u00e9t" } } + }, + "options": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "no_selection": "K\u00e9rem, v\u00e1lasszon legal\u00e1bb egy v\u00e1rost/megy\u00e9t", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "init": { + "data": { + "_a_to_d": "V\u00e1ros/megye (A-D)", + "_e_to_h": "V\u00e1ros/megye (E-H)", + "_i_to_l": "V\u00e1ros/megye (I-L)", + "_m_to_q": "V\u00e1ros/megye (M-Q)", + "_r_to_u": "V\u00e1ros/megye (R-U)", + "_v_to_z": "V\u00e1ros/megye (V-Z)", + "corona_filter": "Corona figyelmeztet\u00e9sek bez\u00e1r\u00e1sa", + "slots": "A figyelmeztet\u00e9sek maxim\u00e1lis sz\u00e1ma v\u00e1rosonk\u00e9nt/megy\u00e9nk\u00e9nt" + }, + "title": "Be\u00e1ll\u00edt\u00e1sok" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/tr.json b/homeassistant/components/nina/translations/tr.json index 5fffe4c4f69..5311219271b 100644 --- a/homeassistant/components/nina/translations/tr.json +++ b/homeassistant/components/nina/translations/tr.json @@ -23,5 +23,27 @@ "title": "\u015eehir/il\u00e7e se\u00e7in" } } + }, + "options": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "no_selection": "L\u00fctfen en az bir \u015fehir/il\u00e7e se\u00e7in", + "unknown": "Beklenmeyen hata" + }, + "step": { + "init": { + "data": { + "_a_to_d": "\u015eehir/il\u00e7e (A-D)", + "_e_to_h": "\u015eehir/il\u00e7e (E-H)", + "_i_to_l": "\u015eehir/il\u00e7e (I-L)", + "_m_to_q": "\u015eehir/il\u00e7e (M-Q)", + "_r_to_u": "\u015eehir/il\u00e7e (R-U)", + "_v_to_z": "\u015eehir/il\u00e7e (V-Z)", + "corona_filter": "Korona Uyar\u0131lar\u0131n\u0131 Kald\u0131r", + "slots": "\u015eehir/il\u00e7e ba\u015f\u0131na maksimum uyar\u0131 say\u0131s\u0131" + }, + "title": "Se\u00e7enekler" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.tr.json b/homeassistant/components/overkiz/translations/sensor.tr.json index b04b5a793bc..b9e88b93d41 100644 --- a/homeassistant/components/overkiz/translations/sensor.tr.json +++ b/homeassistant/components/overkiz/translations/sensor.tr.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "Temiz", "dirty": "Kirli" + }, + "overkiz__three_way_handle_direction": { + "closed": "Kapal\u0131", + "open": "A\u00e7\u0131k", + "tilt": "E\u011fim" } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json index b4b49a5e52d..def3c6f8436 100644 --- a/homeassistant/components/plugwise/translations/tr.json +++ b/homeassistant/components/plugwise/translations/tr.json @@ -19,8 +19,8 @@ "port": "Port", "username": "Smile Kullan\u0131c\u0131 Ad\u0131" }, - "description": "\u00dcr\u00fcn:", - "title": "Plugwise tipi" + "description": "L\u00fctfen girin", + "title": "Smile'a Ba\u011flan\u0131n" }, "user_gateway": { "data": { diff --git a/homeassistant/components/qnap_qsw/translations/ca.json b/homeassistant/components/qnap_qsw/translations/ca.json index 575ed369b7b..6864ced54e2 100644 --- a/homeassistant/components/qnap_qsw/translations/ca.json +++ b/homeassistant/components/qnap_qsw/translations/ca.json @@ -9,6 +9,12 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { + "discovered_connection": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/qnap_qsw/translations/de.json b/homeassistant/components/qnap_qsw/translations/de.json index ad2599e24c4..a2b89d77ea7 100644 --- a/homeassistant/components/qnap_qsw/translations/de.json +++ b/homeassistant/components/qnap_qsw/translations/de.json @@ -9,6 +9,12 @@ "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { + "discovered_connection": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + }, "user": { "data": { "password": "Passwort", diff --git a/homeassistant/components/qnap_qsw/translations/fr.json b/homeassistant/components/qnap_qsw/translations/fr.json index 2631bcdfc87..2c885fee397 100644 --- a/homeassistant/components/qnap_qsw/translations/fr.json +++ b/homeassistant/components/qnap_qsw/translations/fr.json @@ -9,6 +9,12 @@ "invalid_auth": "Authentification non valide" }, "step": { + "discovered_connection": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + }, "user": { "data": { "password": "Mot de passe", diff --git a/homeassistant/components/qnap_qsw/translations/hu.json b/homeassistant/components/qnap_qsw/translations/hu.json index c41ba084d95..60eb41375c1 100644 --- a/homeassistant/components/qnap_qsw/translations/hu.json +++ b/homeassistant/components/qnap_qsw/translations/hu.json @@ -9,6 +9,12 @@ "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { + "discovered_connection": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + }, "user": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/qnap_qsw/translations/ja.json b/homeassistant/components/qnap_qsw/translations/ja.json index 0658405fd3f..09b93bc58c5 100644 --- a/homeassistant/components/qnap_qsw/translations/ja.json +++ b/homeassistant/components/qnap_qsw/translations/ja.json @@ -9,6 +9,12 @@ "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { + "discovered_connection": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", diff --git a/homeassistant/components/qnap_qsw/translations/no.json b/homeassistant/components/qnap_qsw/translations/no.json index 9eff9c22080..97f04b406a5 100644 --- a/homeassistant/components/qnap_qsw/translations/no.json +++ b/homeassistant/components/qnap_qsw/translations/no.json @@ -9,6 +9,12 @@ "invalid_auth": "Ugyldig godkjenning" }, "step": { + "discovered_connection": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + }, "user": { "data": { "password": "Passord", diff --git a/homeassistant/components/qnap_qsw/translations/pt-BR.json b/homeassistant/components/qnap_qsw/translations/pt-BR.json index b9829d78944..fe2016bdce5 100644 --- a/homeassistant/components/qnap_qsw/translations/pt-BR.json +++ b/homeassistant/components/qnap_qsw/translations/pt-BR.json @@ -9,6 +9,12 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "discovered_connection": { + "data": { + "password": "Senha", + "username": "Nome de usu\u00e1rio" + } + }, "user": { "data": { "password": "Senha", diff --git a/homeassistant/components/qnap_qsw/translations/tr.json b/homeassistant/components/qnap_qsw/translations/tr.json index 309f2da3a90..ce11f88deca 100644 --- a/homeassistant/components/qnap_qsw/translations/tr.json +++ b/homeassistant/components/qnap_qsw/translations/tr.json @@ -9,6 +9,12 @@ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, "step": { + "discovered_connection": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + }, "user": { "data": { "password": "Parola", diff --git a/homeassistant/components/qnap_qsw/translations/zh-Hant.json b/homeassistant/components/qnap_qsw/translations/zh-Hant.json index f29c42d6eb6..1461a38c7d4 100644 --- a/homeassistant/components/qnap_qsw/translations/zh-Hant.json +++ b/homeassistant/components/qnap_qsw/translations/zh-Hant.json @@ -9,6 +9,12 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { + "discovered_connection": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + }, "user": { "data": { "password": "\u5bc6\u78bc", diff --git a/homeassistant/components/simplepush/translations/tr.json b/homeassistant/components/simplepush/translations/tr.json new file mode 100644 index 00000000000..0c969465da9 --- /dev/null +++ b/homeassistant/components/simplepush/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "device_key": "Cihaz\u0131n\u0131z\u0131n cihaz anahtar\u0131", + "event": "Olaylar i\u00e7in olay.", + "name": "Ad", + "password": "Cihaz\u0131n\u0131z taraf\u0131ndan kullan\u0131lan \u015fifreleme parolas\u0131", + "salt": "Cihaz\u0131n\u0131z taraf\u0131ndan kullan\u0131lan tuz." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ca.json b/homeassistant/components/soundtouch/translations/ca.json new file mode 100644 index 00000000000..baba19644fb --- /dev/null +++ b/homeassistant/components/soundtouch/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + } + }, + "zeroconf_confirm": { + "description": "Est\u00e0s a punt d'afegir el dispositiu SoundTouch amb nom `{name}` a Home Assistant.", + "title": "Confirma l'addici\u00f3 del dispositiu Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/de.json b/homeassistant/components/soundtouch/translations/de.json new file mode 100644 index 00000000000..8d28b988834 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Du bist im Begriff, das SoundTouch-Ger\u00e4t mit dem Namen `{name}` zu Home Assistant hinzuzuf\u00fcgen.", + "title": "Best\u00e4tige das Hinzuf\u00fcgen des Bose SoundTouch-Ger\u00e4ts" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/et.json b/homeassistant/components/soundtouch/translations/et.json new file mode 100644 index 00000000000..0adb9ddba8b --- /dev/null +++ b/homeassistant/components/soundtouch/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Oled lisamas SoundTouchi seadet nimega `{name}` Home Assistantisse.", + "title": "Kinnita Bose SoundTouchi seadme lisamine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/fr.json b/homeassistant/components/soundtouch/translations/fr.json new file mode 100644 index 00000000000..e0e187756af --- /dev/null +++ b/homeassistant/components/soundtouch/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + }, + "zeroconf_confirm": { + "description": "Vous \u00eates sur le point d'ajouter l'appareil SoundTouch nomm\u00e9 `{name}` \u00e0 Home Assistant.", + "title": "Confirmer l'ajout de l'appareil Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/hu.json b/homeassistant/components/soundtouch/translations/hu.json new file mode 100644 index 00000000000..a0f5c364af6 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm" + } + }, + "zeroconf_confirm": { + "description": "Hamarosan hozz\u00e1adja a \"{name}\" nev\u0171 SoundTouch eszk\u00f6zt a Home Assistanthoz.", + "title": "Bose SoundTouch eszk\u00f6z hozz\u00e1ad\u00e1s\u00e1nak meger\u0151s\u00edt\u00e9se" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ja.json b/homeassistant/components/soundtouch/translations/ja.json new file mode 100644 index 00000000000..c4a94a23a7e --- /dev/null +++ b/homeassistant/components/soundtouch/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + }, + "zeroconf_confirm": { + "description": "`{name}` \u3068\u3044\u3046\u540d\u524d\u306eSoundTouch\u30c7\u30d0\u30a4\u30b9\u3092\u3001Home Assistant\u306b\u8ffd\u52a0\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u307e\u3059\u3002", + "title": "Bose SoundTouch\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u52a0\u3092\u78ba\u8a8d\u3059\u308b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/no.json b/homeassistant/components/soundtouch/translations/no.json new file mode 100644 index 00000000000..bdaee03da54 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "host": "Vert" + } + }, + "zeroconf_confirm": { + "description": "Du er i ferd med \u00e5 legge til SoundTouch-enheten med navnet ` {name} ` til Home Assistant.", + "title": "Bekreft \u00e5 legge til Bose SoundTouch-enhet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/pt-BR.json b/homeassistant/components/soundtouch/translations/pt-BR.json new file mode 100644 index 00000000000..e707219f342 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Voc\u00ea est\u00e1 prestes a adicionar o dispositivo SoundTouch chamado ` {name} ` ao Home Assistant.", + "title": "Confirme a adi\u00e7\u00e3o do dispositivo Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/tr.json b/homeassistant/components/soundtouch/translations/tr.json new file mode 100644 index 00000000000..957215dd8b3 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Sunucu" + } + }, + "zeroconf_confirm": { + "description": "` {name} ` adl\u0131 SoundTouch cihaz\u0131n\u0131 Home Assistant'a eklemek \u00fczeresiniz.", + "title": "Bose SoundTouch cihaz\u0131 eklemeyi onaylay\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/zh-Hant.json b/homeassistant/components/soundtouch/translations/zh-Hant.json new file mode 100644 index 00000000000..08231b8571d --- /dev/null +++ b/homeassistant/components/soundtouch/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + }, + "zeroconf_confirm": { + "description": "\u6b63\u8981\u65b0\u589e\u540d\u70ba `{name}` \u7684 SoundTouch \u88dd\u7f6e\u81f3 Home Assistant\u3002", + "title": "\u78ba\u8a8d\u65b0\u589e Bose SoundTouch \u88dd\u7f6e" + } + } + } +} \ No newline at end of file From 255c3c5b9a82639cf7807e76abb59657cb3569d1 Mon Sep 17 00:00:00 2001 From: Khole Date: Sat, 2 Jul 2022 13:33:10 +0100 Subject: [PATCH 2077/3516] Hive add entity categories to entities (#74324) add enitity categories to entities --- homeassistant/components/hive/sensor.py | 3 +++ homeassistant/components/hive/switch.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index 5bac23fdb3d..d224ec085f3 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -10,6 +10,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, POWER_KILO_WATT from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HiveEntity @@ -23,12 +24,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( key="Battery", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="Power", native_unit_of_measurement=POWER_KILO_WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, ), ) diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 64a8276521a..f72f228c595 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -6,6 +6,7 @@ from datetime import timedelta from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HiveEntity, refresh_system @@ -19,7 +20,10 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SwitchEntityDescription( key="activeplug", ), - SwitchEntityDescription(key="Heating_Heat_On_Demand"), + SwitchEntityDescription( + key="Heating_Heat_On_Demand", + entity_category=EntityCategory.CONFIG, + ), ) From da11cef29a7dc2bff65229cc3f2809e7344e4e3d Mon Sep 17 00:00:00 2001 From: atlflyer <31390797+atlflyer@users.noreply.github.com> Date: Sat, 2 Jul 2022 09:58:08 -0400 Subject: [PATCH 2078/3516] Report error code in log when command fails (#74319) --- homeassistant/components/command_line/__init__.py | 12 +++++++++--- homeassistant/components/command_line/cover.py | 7 +++++-- homeassistant/components/command_line/notify.py | 6 +++++- tests/components/command_line/test_binary_sensor.py | 13 +++++++++++++ tests/components/command_line/test_cover.py | 3 ++- tests/components/command_line/test_notify.py | 1 + tests/components/command_line/test_sensor.py | 11 +++++++++++ tests/components/command_line/test_switch.py | 13 +++++++++++++ 8 files changed, 59 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/command_line/__init__.py b/homeassistant/components/command_line/__init__.py index 1bcaaaa60a6..172c321c0ec 100644 --- a/homeassistant/components/command_line/__init__.py +++ b/homeassistant/components/command_line/__init__.py @@ -23,7 +23,11 @@ def call_shell_with_timeout( return 0 except subprocess.CalledProcessError as proc_exception: if log_return_code: - _LOGGER.error("Command failed: %s", command) + _LOGGER.error( + "Command failed (with return code %s): %s", + proc_exception.returncode, + command, + ) return proc_exception.returncode except subprocess.TimeoutExpired: _LOGGER.error("Timeout for command: %s", command) @@ -40,8 +44,10 @@ def check_output_or_log(command: str, timeout: int) -> str | None: command, shell=True, timeout=timeout # nosec # shell by design ) return return_value.strip().decode("utf-8") - except subprocess.CalledProcessError: - _LOGGER.error("Command failed: %s", command) + except subprocess.CalledProcessError as err: + _LOGGER.error( + "Command failed (with return code %s): %s", err.returncode, command + ) except subprocess.TimeoutExpired: _LOGGER.error("Timeout for command: %s", command) except subprocess.SubprocessError: diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index 609166f2d16..8298201228f 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -115,10 +115,13 @@ class CommandCover(CoverEntity): """Execute the actual commands.""" _LOGGER.info("Running command: %s", command) - success = call_shell_with_timeout(command, self._timeout) == 0 + returncode = call_shell_with_timeout(command, self._timeout) + success = returncode == 0 if not success: - _LOGGER.error("Command failed: %s", command) + _LOGGER.error( + "Command failed (with return code %s): %s", returncode, command + ) return success diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index 6f364947775..7bce5010d45 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -57,7 +57,11 @@ class CommandLineNotificationService(BaseNotificationService): try: proc.communicate(input=message, timeout=self._timeout) if proc.returncode != 0: - _LOGGER.error("Command failed: %s", self.command) + _LOGGER.error( + "Command failed (with return code %s): %s", + proc.returncode, + self.command, + ) except subprocess.TimeoutExpired: _LOGGER.error("Timeout for command: %s", self.command) kill_subprocess(proc) diff --git a/tests/components/command_line/test_binary_sensor.py b/tests/components/command_line/test_binary_sensor.py index b523b02aa64..18f680fbb02 100644 --- a/tests/components/command_line/test_binary_sensor.py +++ b/tests/components/command_line/test_binary_sensor.py @@ -3,6 +3,8 @@ from __future__ import annotations from typing import Any +from pytest import LogCaptureFixture + from homeassistant import setup from homeassistant.components.binary_sensor import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON @@ -112,3 +114,14 @@ async def test_unique_id(hass: HomeAssistant) -> None: ) is not None ) + + +async def test_return_code(caplog: LogCaptureFixture, hass: HomeAssistant) -> None: + """Test setting the state with a template.""" + await setup_test_entity( + hass, + { + "command": "exit 33", + }, + ) + assert "return code 33" in caplog.text diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index 98c917f51ba..deb80953428 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -155,7 +155,7 @@ async def test_reload(hass: HomeAssistant) -> None: async def test_move_cover_failure( caplog: LogCaptureFixture, hass: HomeAssistant ) -> None: - """Test with state value.""" + """Test command failure.""" await setup_test_entity( hass, @@ -165,6 +165,7 @@ async def test_move_cover_failure( DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: "cover.test"}, blocking=True ) assert "Command failed" in caplog.text + assert "return code 1" in caplog.text async def test_unique_id(hass: HomeAssistant) -> None: diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index 26b53a827e7..5cef13e45b4 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -77,6 +77,7 @@ async def test_error_for_none_zero_exit_code( DOMAIN, "test", {"message": "error"}, blocking=True ) assert "Command failed" in caplog.text + assert "return code 1" in caplog.text async def test_timeout(caplog: LogCaptureFixture, hass: HomeAssistant) -> None: diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index 62ec1dbe97b..bdb36eebaa1 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -128,6 +128,17 @@ async def test_bad_command(hass: HomeAssistant) -> None: assert entity_state.state == "unknown" +async def test_return_code(caplog: LogCaptureFixture, hass: HomeAssistant) -> None: + """Test that an error return code is logged.""" + await setup_test_entities( + hass, + { + "command": "exit 33", + }, + ) + assert "return code 33" in caplog.text + + async def test_update_with_json_attrs(hass: HomeAssistant) -> None: """Test attributes get extracted from a JSON result.""" await setup_test_entities( diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index 307974ab3fe..267f7cf7b06 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -419,3 +419,16 @@ async def test_unique_id(hass: HomeAssistant) -> None: ent_reg.async_get_entity_id("switch", "command_line", "not-so-unique-anymore") is not None ) + + +async def test_command_failure(caplog: LogCaptureFixture, hass: HomeAssistant) -> None: + """Test command failure.""" + + await setup_test_entity( + hass, + {"test": {"command_off": "exit 33"}}, + ) + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "switch.test"}, blocking=True + ) + assert "return code 33" in caplog.text From 2464322dc5693b1a0dc979d5da4b4e4acf1fb573 Mon Sep 17 00:00:00 2001 From: Frank <46161394+BraveChicken1@users.noreply.github.com> Date: Sat, 2 Jul 2022 16:06:07 +0200 Subject: [PATCH 2079/3516] Address HomeConnect late review (#74308) * Small changes as requested in PR 58768 * Fix ValueError message formatting * Use f-string * Remove None as return type of _get_appliance_by_device_id Co-authored-by: Martin Hjelmare --- .../components/home_connect/__init__.py | 40 ++++++++----------- homeassistant/components/home_connect/api.py | 1 - .../components/home_connect/entity.py | 1 - 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index f57c7aeb8af..50e51b4ae1e 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -103,15 +103,14 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.LIGHT, Platform.SENSOR, Platform.S def _get_appliance_by_device_id( hass: HomeAssistant, device_id: str -) -> api.HomeConnectDevice | None: +) -> api.HomeConnectDevice: """Return a Home Connect appliance instance given an device_id.""" for hc_api in hass.data[DOMAIN].values(): for dev_dict in hc_api.devices: device = dev_dict[CONF_DEVICE] if device.device_id == device_id: return device.appliance - _LOGGER.error("Appliance for device id %s not found", device_id) - return None + raise ValueError(f"Appliance for device id {device_id} not found") async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -148,18 +147,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: } appliance = _get_appliance_by_device_id(hass, device_id) - if appliance is not None: - await hass.async_add_executor_job( - getattr(appliance, method), program, options - ) + await hass.async_add_executor_job(getattr(appliance, method), program, options) async def _async_service_command(call, command): """Execute calls to services executing a command.""" device_id = call.data[ATTR_DEVICE_ID] appliance = _get_appliance_by_device_id(hass, device_id) - if appliance is not None: - await hass.async_add_executor_job(appliance.execute_command, command) + await hass.async_add_executor_job(appliance.execute_command, command) async def _async_service_key_value(call, method): """Execute calls to services taking a key and value.""" @@ -169,20 +164,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: device_id = call.data[ATTR_DEVICE_ID] appliance = _get_appliance_by_device_id(hass, device_id) - if appliance is not None: - if unit is not None: - await hass.async_add_executor_job( - getattr(appliance, method), - key, - value, - unit, - ) - else: - await hass.async_add_executor_job( - getattr(appliance, method), - key, - value, - ) + if unit is not None: + await hass.async_add_executor_job( + getattr(appliance, method), + key, + value, + unit, + ) + else: + await hass.async_add_executor_job( + getattr(appliance, method), + key, + value, + ) async def async_service_option_active(call): """Service for setting an option for an active program.""" diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py index 00d759b47d5..f3c98e618b8 100644 --- a/homeassistant/components/home_connect/api.py +++ b/homeassistant/components/home_connect/api.py @@ -113,7 +113,6 @@ class HomeConnectDevice: """Initialize the device class.""" self.hass = hass self.appliance = appliance - self.entities = [] def initialize(self): """Fetch the info needed to initialize the device.""" diff --git a/homeassistant/components/home_connect/entity.py b/homeassistant/components/home_connect/entity.py index 60a0c3974cd..b27988f997d 100644 --- a/homeassistant/components/home_connect/entity.py +++ b/homeassistant/components/home_connect/entity.py @@ -20,7 +20,6 @@ class HomeConnectEntity(Entity): self.device = device self.desc = desc self._name = f"{self.device.appliance.name} {desc}" - self.device.entities.append(self) async def async_added_to_hass(self): """Register callbacks.""" From 6f67ae1dfcce397b52e2de3d180fea8df9906b4c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 2 Jul 2022 18:04:35 +0200 Subject: [PATCH 2080/3516] Add nightly frontend to nightly builds (#74327) --- .github/workflows/builder.yml | 24 ++++++++++++++++++++++++ Dockerfile | 7 +++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 6eea7cea953..870bfb9b1e9 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -102,6 +102,17 @@ jobs: - name: Checkout the repository uses: actions/checkout@v3.0.2 + - name: Download nightly wheels of frontend + if: needs.init.outputs.channel == 'dev' + uses: dawidd6/action-download-artifact@v2 + with: + github_token: ${{secrets.GITHUB_TOKEN}} + repo: home-assistant/frontend + branch: dev + workflow: nightly.yml + workflow_conclusion: success + name: wheels + - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' uses: actions/setup-python@v4.0.0 @@ -116,6 +127,19 @@ jobs: python3 -m pip install --use-deprecated=legacy-resolver . version="$(python3 script/version_bump.py nightly)" + if [[ "$(ls home_assistant_frontend*.whl)" =~ ^home_assistant_frontend-(.*)-py3-none-any.whl$ ]]; then + echo "Found frontend wheel, setting version to: ${BASH_REMATCH[1]}" + frontend_version="${BASH_REMATCH[1]}" yq \ + --inplace e -o json \ + '.requirements = ["home-assistant-frontend=="+env(frontend_version)]' \ + homeassistant/components/frontend/manifest.json + + sed -i "s|home-assistant-frontend==.*|home-assistant-frontend==${BASH_REMATCH[1]}|" \ + homeassistant/package_constraints.txt + + python -m script.gen_requirements_all + fi + - name: Write meta info file shell: bash run: | diff --git a/Dockerfile b/Dockerfile index 13552d55a3d..03bd9131ea0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,9 +13,12 @@ COPY homeassistant/package_constraints.txt homeassistant/homeassistant/ RUN \ pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ -r homeassistant/requirements.txt --use-deprecated=legacy-resolver -COPY requirements_all.txt homeassistant/ +COPY requirements_all.txt home_assistant_frontend-* homeassistant/ RUN \ - pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ + if ls homeassistant/home_assistant_frontend*.whl 1> /dev/null 2>&1; then \ + pip3 install --no-cache-dir --no-index homeassistant/home_assistant_frontend-*.whl; \ + fi \ + && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ -r homeassistant/requirements_all.txt --use-deprecated=legacy-resolver ## Setup Home Assistant Core From 64bfa20f6a6fb44255763abe47c304b4227db162 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 2 Jul 2022 19:15:54 +0200 Subject: [PATCH 2081/3516] Improve type hints in mqtt (#74295) --- homeassistant/components/mqtt/climate.py | 56 ++++++++++----------- homeassistant/components/mqtt/humidifier.py | 10 ++-- homeassistant/components/mqtt/mixins.py | 4 +- homeassistant/components/mqtt/number.py | 5 +- homeassistant/components/mqtt/select.py | 2 +- homeassistant/components/mqtt/sensor.py | 2 +- homeassistant/components/mqtt/siren.py | 8 +-- homeassistant/components/mqtt/switch.py | 15 ++++-- 8 files changed, 53 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 6b09891483c..80ff33309ba 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -3,6 +3,7 @@ from __future__ import annotations import functools import logging +from typing import Any import voluptuous as vol @@ -754,29 +755,29 @@ class MqttClimate(MqttEntity, ClimateEntity): await subscription.async_subscribe_topics(self.hass, self._sub_state) @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" - if self._config.get(CONF_TEMPERATURE_UNIT): - return self._config.get(CONF_TEMPERATURE_UNIT) + if unit := self._config.get(CONF_TEMPERATURE_UNIT): + return unit return self.hass.config.units.temperature_unit @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the current temperature.""" return self._current_temp @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" return self._target_temp @property - def target_temperature_low(self): + def target_temperature_low(self) -> float | None: """Return the low target temperature we try to reach.""" return self._target_temp_low @property - def target_temperature_high(self): + def target_temperature_high(self) -> float | None: """Return the high target temperature we try to reach.""" return self._target_temp_high @@ -796,7 +797,7 @@ class MqttClimate(MqttEntity, ClimateEntity): return self._config[CONF_MODE_LIST] @property - def target_temperature_step(self): + def target_temperature_step(self) -> float: """Return the supported step of target temperature.""" return self._config[CONF_TEMP_STEP] @@ -813,7 +814,7 @@ class MqttClimate(MqttEntity, ClimateEntity): return PRESET_NONE @property - def preset_modes(self) -> list: + def preset_modes(self) -> list[str]: """Return preset modes.""" presets = [] presets.extend(self._preset_modes) @@ -834,17 +835,17 @@ class MqttClimate(MqttEntity, ClimateEntity): return presets @property - def is_aux_heat(self): + def is_aux_heat(self) -> bool | None: """Return true if away mode is on.""" return self._aux @property - def fan_mode(self): + def fan_mode(self) -> str | None: """Return the fan setting.""" return self._current_fan_mode @property - def fan_modes(self): + def fan_modes(self) -> list[str]: """Return the list of available fan modes.""" return self._config[CONF_FAN_MODE_LIST] @@ -871,10 +872,9 @@ class MqttClimate(MqttEntity, ClimateEntity): payload = self._command_templates[cmnd_template](temp) await self._publish(cmnd_topic, payload) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" - if kwargs.get(ATTR_HVAC_MODE) is not None: - operation_mode = kwargs.get(ATTR_HVAC_MODE) + if (operation_mode := kwargs.get(ATTR_HVAC_MODE)) is not None: await self.async_set_hvac_mode(operation_mode) await self._set_temperature( @@ -904,7 +904,7 @@ class MqttClimate(MqttEntity, ClimateEntity): # Always optimistic? self.async_write_ha_state() - async def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 if self._send_if_off or self._current_operation != HVACMode.OFF: @@ -917,7 +917,7 @@ class MqttClimate(MqttEntity, ClimateEntity): self._current_swing_mode = swing_mode self.async_write_ha_state() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target temperature.""" # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 if self._send_if_off or self._current_operation != HVACMode.OFF: @@ -928,7 +928,7 @@ class MqttClimate(MqttEntity, ClimateEntity): self._current_fan_mode = fan_mode self.async_write_ha_state() - async def async_set_hvac_mode(self, hvac_mode) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" if hvac_mode == HVACMode.OFF: await self._publish( @@ -945,12 +945,12 @@ class MqttClimate(MqttEntity, ClimateEntity): self.async_write_ha_state() @property - def swing_mode(self): + def swing_mode(self) -> str | None: """Return the swing setting.""" return self._current_swing_mode @property - def swing_modes(self): + def swing_modes(self) -> list[str]: """List of available swing modes.""" return self._config[CONF_SWING_MODE_LIST] @@ -1027,16 +1027,16 @@ class MqttClimate(MqttEntity, ClimateEntity): self._aux = state self.async_write_ha_state() - async def async_turn_aux_heat_on(self): + async def async_turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" await self._set_aux_heat(True) - async def async_turn_aux_heat_off(self): + async def async_turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" await self._set_aux_heat(False) @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" support = 0 @@ -1083,18 +1083,18 @@ class MqttClimate(MqttEntity, ClimateEntity): return support @property - def min_temp(self): + def min_temp(self) -> float: """Return the minimum temperature.""" return self._config[CONF_TEMP_MIN] @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum temperature.""" return self._config[CONF_TEMP_MAX] @property - def precision(self): + def precision(self) -> float: """Return the precision of the system.""" - if self._config.get(CONF_PRECISION) is not None: - return self._config.get(CONF_PRECISION) + if (precision := self._config.get(CONF_PRECISION)) is not None: + return precision return super().precision diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 5f09fc0d513..43ff2af65d4 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -3,6 +3,7 @@ from __future__ import annotations import functools import logging +from typing import Any import voluptuous as vol @@ -407,7 +408,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): await subscription.async_subscribe_topics(self.hass, self._sub_state) @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic @@ -431,10 +432,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): """Return the current mode.""" return self._mode - async def async_turn_on( - self, - **kwargs, - ) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the entity. This method is a coroutine. @@ -451,7 +449,7 @@ class MqttHumidifier(MqttEntity, HumidifierEntity): self._state = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the entity. This method is a coroutine. diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 8e59d09dfce..8fe9ee564de 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -355,7 +355,7 @@ class MqttAttributes(Entity): def __init__(self, config: dict) -> None: """Initialize the JSON attributes mixin.""" - self._attributes: dict | None = None + self._attributes: dict[str, Any] | None = None self._attributes_sub_state = None self._attributes_config = config @@ -426,7 +426,7 @@ class MqttAttributes(Entity): ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" return self._attributes diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index dc27a740720..9df9dbf818c 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -12,6 +12,7 @@ from homeassistant.components.number import ( DEFAULT_MIN_VALUE, DEFAULT_STEP, DEVICE_CLASSES_SCHEMA, + NumberDeviceClass, RestoreNumber, ) from homeassistant.config_entries import ConfigEntry @@ -292,11 +293,11 @@ class MqttNumber(MqttEntity, RestoreNumber): ) @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic @property - def device_class(self) -> str | None: + def device_class(self) -> NumberDeviceClass | None: """Return the device class of the sensor.""" return self._config.get(CONF_DEVICE_CLASS) diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 4c302446b19..bdf55f895f3 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -211,6 +211,6 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): ) @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 6948e173039..0dda382bec4 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -347,7 +347,7 @@ class MqttSensor(MqttEntity, RestoreSensor): return self._config.get(CONF_UNIT_OF_MEASUREMENT) @property - def force_update(self): + def force_update(self) -> bool: """Force update.""" return self._config[CONF_FORCE_UPDATE] diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index dfb89d2ee79..f1de0d27de7 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -309,12 +309,12 @@ class MqttSiren(MqttEntity, SirenEntity): await subscription.async_subscribe_topics(self.hass, self._sub_state) @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic @property - def extra_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" mqtt_attributes = super().extra_state_attributes attributes = ( @@ -353,7 +353,7 @@ class MqttSiren(MqttEntity, SirenEntity): self._config[CONF_ENCODING], ) - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the siren on. This method is a coroutine. @@ -371,7 +371,7 @@ class MqttSiren(MqttEntity, SirenEntity): self._update(kwargs) self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the siren off. This method is a coroutine. diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index b04f2433659..34009695257 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -2,11 +2,16 @@ from __future__ import annotations import functools +from typing import Any import voluptuous as vol from homeassistant.components import switch -from homeassistant.components.switch import DEVICE_CLASSES_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + DEVICE_CLASSES_SCHEMA, + SwitchDeviceClass, + SwitchEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_CLASS, @@ -195,16 +200,16 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): return self._state @property - def assumed_state(self): + def assumed_state(self) -> bool: """Return true if we do optimistic updates.""" return self._optimistic @property - def device_class(self) -> str | None: + def device_class(self) -> SwitchDeviceClass | None: """Return the device class of the sensor.""" return self._config.get(CONF_DEVICE_CLASS) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on. This method is a coroutine. @@ -221,7 +226,7 @@ class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): self._state = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off. This method is a coroutine. From d56a487169cb039f53da31bff8cda982c6ca3ff9 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 2 Jul 2022 21:20:40 +0300 Subject: [PATCH 2082/3516] Fix CI failure due to integrations leaving dirty known_devices.yaml (#74329) --- tests/components/demo/test_init.py | 13 +++++-------- tests/components/device_tracker/test_init.py | 13 ++++++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index b00028e34d9..fa0aff8223b 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -1,12 +1,10 @@ """The tests for the Demo component.""" -from contextlib import suppress import json -import os +from unittest.mock import patch import pytest from homeassistant.components.demo import DOMAIN -from homeassistant.components.device_tracker.legacy import YAML_DEVICES from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.statistics import list_statistic_ids from homeassistant.helpers.json import JSONEncoder @@ -22,11 +20,10 @@ def mock_history(hass): @pytest.fixture(autouse=True) -def demo_cleanup(hass): - """Clean up device tracker demo file.""" - yield - with suppress(FileNotFoundError): - os.remove(hass.config.path(YAML_DEVICES)) +def mock_device_tracker_update_config(hass): + """Prevent device tracker from creating known devices file.""" + with patch("homeassistant.components.device_tracker.legacy.update_config"): + yield async def test_setting_up_demo(hass): diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 0953fc67b0a..d8914032f36 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -28,6 +28,8 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from . import common + from tests.common import ( assert_setup_component, async_fire_time_changed, @@ -35,7 +37,6 @@ from tests.common import ( mock_restore_cache, patch_yaml_files, ) -from tests.components.device_tracker import common TEST_PLATFORM = {device_tracker.DOMAIN: {CONF_PLATFORM: "test"}} @@ -165,6 +166,7 @@ async def test_setup_without_yaml_file(hass, enable_custom_integrations): """Test with no YAML file.""" with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) + await hass.async_block_till_done() async def test_gravatar(hass): @@ -210,10 +212,11 @@ async def test_gravatar_and_picture(hass): @patch("homeassistant.components.demo.device_tracker.setup_scanner", autospec=True) async def test_discover_platform(mock_demo_setup_scanner, mock_see, hass): """Test discovery of device_tracker demo platform.""" - await discovery.async_load_platform( - hass, device_tracker.DOMAIN, "demo", {"test_key": "test_val"}, {"bla": {}} - ) - await hass.async_block_till_done() + with patch("homeassistant.components.device_tracker.legacy.update_config"): + await discovery.async_load_platform( + hass, device_tracker.DOMAIN, "demo", {"test_key": "test_val"}, {"bla": {}} + ) + await hass.async_block_till_done() assert device_tracker.DOMAIN in hass.config.components assert mock_demo_setup_scanner.called assert mock_demo_setup_scanner.call_args[0] == ( From d9b326dd48189f67f6d6e72c092b2ea65b249afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 2 Jul 2022 21:44:20 +0200 Subject: [PATCH 2083/3516] Remove duplicated QNAP QSW format_mac call in config_flow (#74333) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit qnap_qsw: config_flow: remove duplicated format_mac Signed-off-by: Álvaro Fernández Rojas --- homeassistant/components/qnap_qsw/config_flow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/qnap_qsw/config_flow.py b/homeassistant/components/qnap_qsw/config_flow.py index bb42c9ea294..794e8c67baa 100644 --- a/homeassistant/components/qnap_qsw/config_flow.py +++ b/homeassistant/components/qnap_qsw/config_flow.py @@ -78,7 +78,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.debug("DHCP discovery detected QSW: %s", self._discovered_mac) - mac = format_mac(self._discovered_mac) options = ConnectionOptions(self._discovered_url, "", "") qsw = QnapQswApi(aiohttp_client.async_get_clientsession(self.hass), options) @@ -87,7 +86,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except QswError as err: raise AbortFlow("cannot_connect") from err - await self.async_set_unique_id(format_mac(mac)) + await self.async_set_unique_id(format_mac(self._discovered_mac)) self._abort_if_unique_id_configured() return await self.async_step_discovered_connection() From c24c6b38b1121d4eff953bfe8a775c8c5c06d106 Mon Sep 17 00:00:00 2001 From: David Barshow Date: Sat, 2 Jul 2022 13:04:38 -0700 Subject: [PATCH 2084/3516] Support climate reproduce state fan_mode (#74317) climate reproduce state fan_mode support --- homeassistant/components/climate/reproduce_state.py | 5 +++++ tests/components/climate/test_reproduce_state.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/components/climate/reproduce_state.py b/homeassistant/components/climate/reproduce_state.py index f7e63f475ea..0bbc6fce7ec 100644 --- a/homeassistant/components/climate/reproduce_state.py +++ b/homeassistant/components/climate/reproduce_state.py @@ -10,6 +10,7 @@ from homeassistant.core import Context, HomeAssistant, State from .const import ( ATTR_AUX_HEAT, + ATTR_FAN_MODE, ATTR_HUMIDITY, ATTR_HVAC_MODE, ATTR_PRESET_MODE, @@ -19,6 +20,7 @@ from .const import ( DOMAIN, HVAC_MODES, SERVICE_SET_AUX_HEAT, + SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, @@ -70,6 +72,9 @@ async def _async_reproduce_states( if ATTR_SWING_MODE in state.attributes: await call_service(SERVICE_SET_SWING_MODE, [ATTR_SWING_MODE]) + if ATTR_FAN_MODE in state.attributes: + await call_service(SERVICE_SET_FAN_MODE, [ATTR_FAN_MODE]) + if ATTR_HUMIDITY in state.attributes: await call_service(SERVICE_SET_HUMIDITY, [ATTR_HUMIDITY]) diff --git a/tests/components/climate/test_reproduce_state.py b/tests/components/climate/test_reproduce_state.py index af1b14299ae..a6839043e62 100644 --- a/tests/components/climate/test_reproduce_state.py +++ b/tests/components/climate/test_reproduce_state.py @@ -4,6 +4,7 @@ import pytest from homeassistant.components.climate.const import ( ATTR_AUX_HEAT, + ATTR_FAN_MODE, ATTR_HUMIDITY, ATTR_PRESET_MODE, ATTR_SWING_MODE, @@ -14,6 +15,7 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, HVAC_MODE_OFF, SERVICE_SET_AUX_HEAT, + SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, @@ -99,6 +101,7 @@ async def test_state_with_context(hass): (SERVICE_SET_AUX_HEAT, ATTR_AUX_HEAT), (SERVICE_SET_PRESET_MODE, ATTR_PRESET_MODE), (SERVICE_SET_SWING_MODE, ATTR_SWING_MODE), + (SERVICE_SET_FAN_MODE, ATTR_FAN_MODE), (SERVICE_SET_HUMIDITY, ATTR_HUMIDITY), (SERVICE_SET_TEMPERATURE, ATTR_TEMPERATURE), (SERVICE_SET_TEMPERATURE, ATTR_TARGET_TEMP_HIGH), From 3bebbf79700e5affb5b26021c3a64396a38b8032 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 2 Jul 2022 22:10:38 +0200 Subject: [PATCH 2085/3516] Add configuration directory to system health (#74318) --- homeassistant/components/homeassistant/strings.json | 3 ++- homeassistant/components/homeassistant/system_health.py | 1 + homeassistant/components/homeassistant/translations/en.json | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 43180b237b9..317e9d1dfcd 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -2,15 +2,16 @@ "system_health": { "info": { "arch": "CPU Architecture", + "config_dir": "Configuration Directory", "dev": "Development", "docker": "Docker", - "user": "User", "hassio": "Supervisor", "installation_type": "Installation Type", "os_name": "Operating System Family", "os_version": "Operating System Version", "python_version": "Python Version", "timezone": "Timezone", + "user": "User", "version": "Version", "virtualenv": "Virtual Environment" } diff --git a/homeassistant/components/homeassistant/system_health.py b/homeassistant/components/homeassistant/system_health.py index f13278ddfeb..4006228de25 100644 --- a/homeassistant/components/homeassistant/system_health.py +++ b/homeassistant/components/homeassistant/system_health.py @@ -29,4 +29,5 @@ async def system_health_info(hass): "os_version": info.get("os_version"), "arch": info.get("arch"), "timezone": info.get("timezone"), + "config_dir": hass.config.config_dir, } diff --git a/homeassistant/components/homeassistant/translations/en.json b/homeassistant/components/homeassistant/translations/en.json index 977bc203fea..37c4498b32b 100644 --- a/homeassistant/components/homeassistant/translations/en.json +++ b/homeassistant/components/homeassistant/translations/en.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU Architecture", + "config_dir": "Configuration Directory", "dev": "Development", "docker": "Docker", "hassio": "Supervisor", From 08c2bd82bd704a348c2b81cae4f73e2155cc728a Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Sat, 2 Jul 2022 21:27:47 +0100 Subject: [PATCH 2086/3516] Migrate metoffice to native_* (#74312) --- homeassistant/components/metoffice/weather.py | 25 ++++++++-------- tests/components/metoffice/test_weather.py | 29 +++++++++++-------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index daf37bcf83f..2e9a55b4415 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -1,15 +1,15 @@ """Support for UK Met Office weather service.""" from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import PRESSURE_HPA, SPEED_MILES_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -55,11 +55,11 @@ def _build_forecast_data(timestep): if timestep.precipitation: data[ATTR_FORECAST_PRECIPITATION_PROBABILITY] = timestep.precipitation.value if timestep.temperature: - data[ATTR_FORECAST_TEMP] = timestep.temperature.value + data[ATTR_FORECAST_NATIVE_TEMP] = timestep.temperature.value if timestep.wind_direction: data[ATTR_FORECAST_WIND_BEARING] = timestep.wind_direction.value if timestep.wind_speed: - data[ATTR_FORECAST_WIND_SPEED] = timestep.wind_speed.value + data[ATTR_FORECAST_NATIVE_WIND_SPEED] = timestep.wind_speed.value return data @@ -73,6 +73,10 @@ def _get_weather_condition(metoffice_code): class MetOfficeWeather(CoordinatorEntity, WeatherEntity): """Implementation of a Met Office weather condition.""" + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR + def __init__(self, coordinator, hass_data, use_3hourly): """Initialise the platform with a data instance.""" super().__init__(coordinator) @@ -94,17 +98,12 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): return None @property - def temperature(self): + def native_temperature(self): """Return the platform temperature.""" if self.coordinator.data.now.temperature: return self.coordinator.data.now.temperature.value return None - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def visibility(self): """Return the platform visibility.""" @@ -119,7 +118,7 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): return _visibility @property - def pressure(self): + def native_pressure(self): """Return the mean sea-level pressure.""" weather_now = self.coordinator.data.now if weather_now and weather_now.pressure: @@ -135,7 +134,7 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): return None @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" weather_now = self.coordinator.data.now if weather_now and weather_now.wind_speed: diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index bf279ff3cf7..3b7ebd08678 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -133,7 +133,8 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 17 - assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_speed") == 14.48 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 @@ -148,7 +149,7 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.attributes.get("forecast")[26]["condition"] == "cloudy" assert weather.attributes.get("forecast")[26]["precipitation_probability"] == 9 assert weather.attributes.get("forecast")[26]["temperature"] == 10 - assert weather.attributes.get("forecast")[26]["wind_speed"] == 4 + assert weather.attributes.get("forecast")[26]["wind_speed"] == 6.44 assert weather.attributes.get("forecast")[26]["wind_bearing"] == "NNE" # Wavertree daily weather platform expected results @@ -157,7 +158,7 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 19 - assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_bearing") == "SSE" assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 @@ -172,7 +173,7 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.attributes.get("forecast")[3]["condition"] == "rainy" assert weather.attributes.get("forecast")[3]["precipitation_probability"] == 59 assert weather.attributes.get("forecast")[3]["temperature"] == 13 - assert weather.attributes.get("forecast")[3]["wind_speed"] == 13 + assert weather.attributes.get("forecast")[3]["wind_speed"] == 20.92 assert weather.attributes.get("forecast")[3]["wind_bearing"] == "SE" @@ -229,7 +230,8 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 17 - assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_speed") == 14.48 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 @@ -244,7 +246,7 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("forecast")[18]["condition"] == "clear-night" assert weather.attributes.get("forecast")[18]["precipitation_probability"] == 1 assert weather.attributes.get("forecast")[18]["temperature"] == 9 - assert weather.attributes.get("forecast")[18]["wind_speed"] == 4 + assert weather.attributes.get("forecast")[18]["wind_speed"] == 6.44 assert weather.attributes.get("forecast")[18]["wind_bearing"] == "NW" # Wavertree daily weather platform expected results @@ -253,7 +255,8 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 19 - assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_speed") == 14.48 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 @@ -268,7 +271,7 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("forecast")[3]["condition"] == "rainy" assert weather.attributes.get("forecast")[3]["precipitation_probability"] == 59 assert weather.attributes.get("forecast")[3]["temperature"] == 13 - assert weather.attributes.get("forecast")[3]["wind_speed"] == 13 + assert weather.attributes.get("forecast")[3]["wind_speed"] == 20.92 assert weather.attributes.get("forecast")[3]["wind_bearing"] == "SE" # King's Lynn 3-hourly weather platform expected results @@ -277,7 +280,8 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 14 - assert weather.attributes.get("wind_speed") == 2 + assert weather.attributes.get("wind_speed") == 3.22 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "E" assert weather.attributes.get("visibility") == "Very Good - 20-40" assert weather.attributes.get("humidity") == 60 @@ -292,7 +296,7 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("forecast")[18]["condition"] == "cloudy" assert weather.attributes.get("forecast")[18]["precipitation_probability"] == 9 assert weather.attributes.get("forecast")[18]["temperature"] == 10 - assert weather.attributes.get("forecast")[18]["wind_speed"] == 7 + assert weather.attributes.get("forecast")[18]["wind_speed"] == 11.27 assert weather.attributes.get("forecast")[18]["wind_bearing"] == "SE" # King's Lynn daily weather platform expected results @@ -301,7 +305,8 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.state == "cloudy" assert weather.attributes.get("temperature") == 9 - assert weather.attributes.get("wind_speed") == 4 + assert weather.attributes.get("wind_speed") == 6.44 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "ESE" assert weather.attributes.get("visibility") == "Very Good - 20-40" assert weather.attributes.get("humidity") == 75 @@ -316,5 +321,5 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("forecast")[2]["condition"] == "cloudy" assert weather.attributes.get("forecast")[2]["precipitation_probability"] == 14 assert weather.attributes.get("forecast")[2]["temperature"] == 11 - assert weather.attributes.get("forecast")[2]["wind_speed"] == 7 + assert weather.attributes.get("forecast")[2]["wind_speed"] == 11.27 assert weather.attributes.get("forecast")[2]["wind_bearing"] == "ESE" From 98052646f2fc759bee72c4361f39679abd8fe9f0 Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Sat, 2 Jul 2022 21:42:58 +0100 Subject: [PATCH 2087/3516] Remove visibility from metoffice weather (#74314) Co-authored-by: Paulus Schoutsen --- homeassistant/components/metoffice/weather.py | 15 --------------- tests/components/metoffice/test_weather.py | 6 ------ 2 files changed, 21 deletions(-) diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index 2e9a55b4415..f4e0bf61d30 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -27,8 +27,6 @@ from .const import ( MODE_3HOURLY_LABEL, MODE_DAILY, MODE_DAILY_LABEL, - VISIBILITY_CLASSES, - VISIBILITY_DISTANCE_CLASSES, ) @@ -104,19 +102,6 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): return self.coordinator.data.now.temperature.value return None - @property - def visibility(self): - """Return the platform visibility.""" - _visibility = None - weather_now = self.coordinator.data.now - if hasattr(weather_now, "visibility"): - visibility_class = VISIBILITY_CLASSES.get(weather_now.visibility.value) - visibility_distance = VISIBILITY_DISTANCE_CLASSES.get( - weather_now.visibility.value - ) - _visibility = f"{visibility_class} - {visibility_distance}" - return _visibility - @property def native_pressure(self): """Return the mean sea-level pressure.""" diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 3b7ebd08678..a93b1ea6b62 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -136,7 +136,6 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 # Forecasts added - just pick out 1 entry to check @@ -160,7 +159,6 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.attributes.get("temperature") == 19 assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 # Also has Forecasts added - again, just pick out 1 entry to check @@ -233,7 +231,6 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 # Forecasts added - just pick out 1 entry to check @@ -258,7 +255,6 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 # Also has Forecasts added - again, just pick out 1 entry to check @@ -283,7 +279,6 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 3.22 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "E" - assert weather.attributes.get("visibility") == "Very Good - 20-40" assert weather.attributes.get("humidity") == 60 # Also has Forecast added - just pick out 1 entry to check @@ -308,7 +303,6 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 6.44 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "ESE" - assert weather.attributes.get("visibility") == "Very Good - 20-40" assert weather.attributes.get("humidity") == 75 # All should have Forecast added - again, just picking out 1 entry to check From 8581db1da70349fa2fa2c531e8f6ac61fe898246 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 3 Jul 2022 00:26:52 +0000 Subject: [PATCH 2088/3516] [ci skip] Translation update --- .../components/anthemav/translations/el.json | 19 ++++++++++++++ .../components/anthemav/translations/it.json | 19 ++++++++++++++ .../components/esphome/translations/it.json | 4 +-- .../homeassistant/translations/fr.json | 1 + .../homeassistant/translations/it.json | 1 + .../homeassistant/translations/pt-BR.json | 1 + .../lg_soundbar/translations/el.json | 18 +++++++++++++ .../lg_soundbar/translations/it.json | 18 +++++++++++++ .../components/life360/translations/it.json | 25 ++++++++++++++++++- .../components/nina/translations/it.json | 22 ++++++++++++++++ .../components/qnap_qsw/translations/el.json | 6 +++++ .../components/qnap_qsw/translations/et.json | 6 +++++ .../components/qnap_qsw/translations/it.json | 6 +++++ .../components/qnap_qsw/translations/ko.json | 12 +++++++++ .../soundtouch/translations/el.json | 21 ++++++++++++++++ .../soundtouch/translations/it.json | 21 ++++++++++++++++ .../soundtouch/translations/ko.json | 11 ++++++++ 17 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/anthemav/translations/el.json create mode 100644 homeassistant/components/anthemav/translations/it.json create mode 100644 homeassistant/components/lg_soundbar/translations/el.json create mode 100644 homeassistant/components/lg_soundbar/translations/it.json create mode 100644 homeassistant/components/qnap_qsw/translations/ko.json create mode 100644 homeassistant/components/soundtouch/translations/el.json create mode 100644 homeassistant/components/soundtouch/translations/it.json create mode 100644 homeassistant/components/soundtouch/translations/ko.json diff --git a/homeassistant/components/anthemav/translations/el.json b/homeassistant/components/anthemav/translations/el.json new file mode 100644 index 00000000000..983e89155e8 --- /dev/null +++ b/homeassistant/components/anthemav/translations/el.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "cannot_receive_deviceinfo": "\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 MAC. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/it.json b/homeassistant/components/anthemav/translations/it.json new file mode 100644 index 00000000000..12b0df56f0f --- /dev/null +++ b/homeassistant/components/anthemav/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "cannot_receive_deviceinfo": "Impossibile recuperare l'indirizzo MAC. Assicurati che il dispositivo sia acceso" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/it.json b/homeassistant/components/esphome/translations/it.json index 62c75238378..b1c4e12d088 100644 --- a/homeassistant/components/esphome/translations/it.json +++ b/homeassistant/components/esphome/translations/it.json @@ -9,7 +9,7 @@ "connection_error": "Impossibile connettersi a ESP. Assicurati che il tuo file YAML contenga una riga \"api:\".", "invalid_auth": "Autenticazione non valida", "invalid_psk": "La chiave di cifratura 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, imposta un indirizzo IP statico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "Impossibile risolvere l'indirizzo dell'ESP. Se l'errore persiste, impostare un indirizzo IP statico" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Host", "port": "Porta" }, - "description": "Inserisci le impostazioni di connessione del tuo nodo [ESPHome](https://esphomelib.com/)." + "description": "Inserisci le impostazioni di connessione del tuo nodo [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/homeassistant/translations/fr.json b/homeassistant/components/homeassistant/translations/fr.json index ae9dfb0a7da..48e38978ba6 100644 --- a/homeassistant/components/homeassistant/translations/fr.json +++ b/homeassistant/components/homeassistant/translations/fr.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Architecture du processeur", + "config_dir": "R\u00e9pertoire de configuration", "dev": "D\u00e9veloppement", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/it.json b/homeassistant/components/homeassistant/translations/it.json index 3052a536338..432fb9dea46 100644 --- a/homeassistant/components/homeassistant/translations/it.json +++ b/homeassistant/components/homeassistant/translations/it.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Architettura della CPU", + "config_dir": "Cartella di configurazione", "dev": "Sviluppo", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/pt-BR.json b/homeassistant/components/homeassistant/translations/pt-BR.json index f30a1775e0e..5ea540d67f3 100644 --- a/homeassistant/components/homeassistant/translations/pt-BR.json +++ b/homeassistant/components/homeassistant/translations/pt-BR.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Arquitetura da CPU", + "config_dir": "Diret\u00f3rio de configura\u00e7\u00e3o", "dev": "Desenvolvimento", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/lg_soundbar/translations/el.json b/homeassistant/components/lg_soundbar/translations/el.json new file mode 100644 index 00000000000..7fa31f8fe9d --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/el.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "existing_instance_updated": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03b7 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7." + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/it.json b/homeassistant/components/lg_soundbar/translations/it.json new file mode 100644 index 00000000000..c9f58f15aa2 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "existing_instance_updated": "Configurazione esistente aggiornata." + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/it.json b/homeassistant/components/life360/translations/it.json index 04d88e75378..4f139301274 100644 --- a/homeassistant/components/life360/translations/it.json +++ b/homeassistant/components/life360/translations/it.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", "invalid_auth": "Autenticazione non valida", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", "unknown": "Errore imprevisto" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", "invalid_username": "Nome utente non valido", "unknown": "Errore imprevisto" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "title": "Autentica nuovamente l'integrazione" + }, "user": { "data": { "password": "Password", "username": "Nome utente" }, "description": "Per impostare le opzioni avanzate, vedere [Documentazione di Life360]({docs_url}).\n\u00c8 consigliabile eseguire questa operazione prima di aggiungere gli account.", - "title": "Informazioni sull'account Life360" + "title": "Configura l'account Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Mostra la guida come stato", + "driving_speed": "Velocit\u00e0 di guida", + "limit_gps_acc": "Limita la precisione del GPS", + "max_gps_accuracy": "Precisione GPS massima (metri)", + "set_drive_speed": "Impostare la soglia di velocit\u00e0 di guida" + }, + "title": "Opzioni dell'account" } } } diff --git a/homeassistant/components/nina/translations/it.json b/homeassistant/components/nina/translations/it.json index de54f8f9c1d..0e329cef6bd 100644 --- a/homeassistant/components/nina/translations/it.json +++ b/homeassistant/components/nina/translations/it.json @@ -23,5 +23,27 @@ "title": "Seleziona citt\u00e0/provincia" } } + }, + "options": { + "error": { + "cannot_connect": "Impossibile connettersi", + "no_selection": "Seleziona almeno una citt\u00e0/provincia", + "unknown": "Errore imprevisto" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Citt\u00e0/provincia (A-D)", + "_e_to_h": "Citt\u00e0/provincia (E-H)", + "_i_to_l": "Citt\u00e0/provincia (I-L)", + "_m_to_q": "Citt\u00e0/provincia (M-Q)", + "_r_to_u": "Citt\u00e0/provincia (R-U)", + "_v_to_z": "Citt\u00e0/provincia (V-Z)", + "corona_filter": "Rimuovi avvisi Corona", + "slots": "Avvisi massimi per citt\u00e0/provincia" + }, + "title": "Opzioni" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/el.json b/homeassistant/components/qnap_qsw/translations/el.json index 1bea5dcc9d7..5aeea0a6d22 100644 --- a/homeassistant/components/qnap_qsw/translations/el.json +++ b/homeassistant/components/qnap_qsw/translations/el.json @@ -9,6 +9,12 @@ "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" }, "step": { + "discovered_connection": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + }, "user": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/qnap_qsw/translations/et.json b/homeassistant/components/qnap_qsw/translations/et.json index eb7ddb6dbe7..7b43f0b00f8 100644 --- a/homeassistant/components/qnap_qsw/translations/et.json +++ b/homeassistant/components/qnap_qsw/translations/et.json @@ -9,6 +9,12 @@ "invalid_auth": "Tuvastamine nurjus" }, "step": { + "discovered_connection": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + }, "user": { "data": { "password": "Salas\u00f5na", diff --git a/homeassistant/components/qnap_qsw/translations/it.json b/homeassistant/components/qnap_qsw/translations/it.json index 0759ef4ca8a..557e937217b 100644 --- a/homeassistant/components/qnap_qsw/translations/it.json +++ b/homeassistant/components/qnap_qsw/translations/it.json @@ -9,6 +9,12 @@ "invalid_auth": "Autenticazione non valida" }, "step": { + "discovered_connection": { + "data": { + "password": "Password", + "username": "Nome utente" + } + }, "user": { "data": { "password": "Password", diff --git a/homeassistant/components/qnap_qsw/translations/ko.json b/homeassistant/components/qnap_qsw/translations/ko.json new file mode 100644 index 00000000000..d31f3ea7fcd --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/ko.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "discovered_connection": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/el.json b/homeassistant/components/soundtouch/translations/el.json new file mode 100644 index 00000000000..7346aeca660 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + } + }, + "zeroconf_confirm": { + "description": "\u03a0\u03c1\u03cc\u03ba\u03b5\u03b9\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae SoundTouch \u03bc\u03b5 \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 `{name}` \u03c3\u03c4\u03bf Home Assistant.", + "title": "\u0395\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03af\u03c9\u03c3\u03b7 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/it.json b/homeassistant/components/soundtouch/translations/it.json new file mode 100644 index 00000000000..a14492bf5c3 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Stai per aggiungere il dispositivo SoundTouch denominato `{name}` a Home Assistant.", + "title": "Conferma l'aggiunta del dispositivo Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ko.json b/homeassistant/components/soundtouch/translations/ko.json new file mode 100644 index 00000000000..fc4995bcf2c --- /dev/null +++ b/homeassistant/components/soundtouch/translations/ko.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8" + } + } + } + } +} \ No newline at end of file From 820a782f94cafebe214fc558651fb78e9defc9ac Mon Sep 17 00:00:00 2001 From: shbatm Date: Sat, 2 Jul 2022 20:38:48 -0500 Subject: [PATCH 2089/3516] Onvif: bump onvif-zeep-async to 1.2.1 (#74341) * Update requirements_all.txt * Update requirements_test_all.txt * Update manifest.json --- homeassistant/components/onvif/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index cd220500751..2df7c3004f0 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -2,7 +2,7 @@ "domain": "onvif", "name": "ONVIF", "documentation": "https://www.home-assistant.io/integrations/onvif", - "requirements": ["onvif-zeep-async==1.2.0", "WSDiscovery==2.0.0"], + "requirements": ["onvif-zeep-async==1.2.1", "WSDiscovery==2.0.0"], "dependencies": ["ffmpeg"], "codeowners": ["@hunterjm"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 75c6552de30..d71232f1fd5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1150,7 +1150,7 @@ ondilo==0.2.0 onkyo-eiscp==1.2.7 # homeassistant.components.onvif -onvif-zeep-async==1.2.0 +onvif-zeep-async==1.2.1 # homeassistant.components.opengarage open-garage==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4d60f632108..c2a194ca559 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -797,7 +797,7 @@ omnilogic==0.4.5 ondilo==0.2.0 # homeassistant.components.onvif -onvif-zeep-async==1.2.0 +onvif-zeep-async==1.2.1 # homeassistant.components.opengarage open-garage==0.2.0 From 5919082a53a710ee00a6eec901cdbc8bbb211580 Mon Sep 17 00:00:00 2001 From: mbo18 Date: Sun, 3 Jul 2022 16:47:06 +0200 Subject: [PATCH 2090/3516] Expose temperature and humidity sensors from AC (#74328) Expose temperature and humidity from AC --- homeassistant/components/smartthings/climate.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 87d20b4533f..db13b5e6b37 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -106,9 +106,7 @@ def get_capabilities(capabilities: Sequence[str]) -> Sequence[str] | None: Capability.air_conditioner_mode, Capability.demand_response_load_control, Capability.air_conditioner_fan_mode, - Capability.relative_humidity_measurement, Capability.switch, - Capability.temperature_measurement, Capability.thermostat, Capability.thermostat_cooling_setpoint, Capability.thermostat_fan_mode, From a4c6cd2fbe3c3e65a5cfc39e2ffa7b3429d3ebc2 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 3 Jul 2022 08:05:38 -0700 Subject: [PATCH 2091/3516] Cleanup Google Calendar unused test fixtures (#74353) --- tests/components/google/test_calendar.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 9a0cc2e47fa..f92519e2553 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -101,9 +101,7 @@ def upcoming_event_url(entity: str = TEST_ENTITY) -> str: return f"/api/calendars/{entity}?start={urllib.parse.quote(start)}&end={urllib.parse.quote(end)}" -async def test_all_day_event( - hass, mock_events_list_items, mock_token_read, component_setup -): +async def test_all_day_event(hass, mock_events_list_items, component_setup): """Test that we can create an event trigger on device.""" week_from_today = dt_util.now().date() + datetime.timedelta(days=7) end_event = week_from_today + datetime.timedelta(days=1) @@ -672,7 +670,6 @@ async def test_future_event_offset_update_behavior( async def test_unique_id( hass, mock_events_list_items, - mock_token_read, component_setup, config_entry, ): @@ -695,7 +692,6 @@ async def test_unique_id( async def test_unique_id_migration( hass, mock_events_list_items, - mock_token_read, component_setup, config_entry, old_unique_id, @@ -751,7 +747,6 @@ async def test_unique_id_migration( async def test_invalid_unique_id_cleanup( hass, mock_events_list_items, - mock_token_read, component_setup, config_entry, mock_calendars_yaml, From e7e940afa50cd050e679a9fd9d1b93b94fb68154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huryn?= Date: Sun, 3 Jul 2022 17:06:38 +0200 Subject: [PATCH 2092/3516] Address Blebox uniapi review sidenotes (#74298) * Changes accordingly to sidenotes given by @MartinHjelmare in pull #73834. * Mini version bump according to notes in pull #73834. * Error message fix, test adjustment. --- CODEOWNERS | 4 ++-- homeassistant/components/blebox/const.py | 21 +------------------ homeassistant/components/blebox/cover.py | 18 ++++++++++++++-- homeassistant/components/blebox/light.py | 9 ++++---- homeassistant/components/blebox/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/blebox/test_light.py | 19 +++++++++-------- 8 files changed, 37 insertions(+), 42 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 0c3f73db76d..9e3c1e8e0b8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -131,8 +131,8 @@ build.json @home-assistant/supervisor /homeassistant/components/binary_sensor/ @home-assistant/core /tests/components/binary_sensor/ @home-assistant/core /homeassistant/components/bizkaibus/ @UgaitzEtxebarria -/homeassistant/components/blebox/ @bbx-a @bbx-jp @riokuu -/tests/components/blebox/ @bbx-a @bbx-jp @riokuu +/homeassistant/components/blebox/ @bbx-a @riokuu +/tests/components/blebox/ @bbx-a @riokuu /homeassistant/components/blink/ @fronzbot /tests/components/blink/ @fronzbot /homeassistant/components/blueprint/ @home-assistant/core diff --git a/homeassistant/components/blebox/const.py b/homeassistant/components/blebox/const.py index f0c6a2d7586..013a6501068 100644 --- a/homeassistant/components/blebox/const.py +++ b/homeassistant/components/blebox/const.py @@ -3,13 +3,7 @@ from homeassistant.components.cover import CoverDeviceClass from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.switch import SwitchDeviceClass -from homeassistant.const import ( - STATE_CLOSED, - STATE_CLOSING, - STATE_OPEN, - STATE_OPENING, - TEMP_CELSIUS, -) +from homeassistant.const import TEMP_CELSIUS DOMAIN = "blebox" PRODUCT = "product" @@ -30,19 +24,6 @@ BLEBOX_TO_HASS_DEVICE_CLASSES = { "temperature": SensorDeviceClass.TEMPERATURE, } -BLEBOX_TO_HASS_COVER_STATES = { - None: None, - 0: STATE_CLOSING, # moving down - 1: STATE_OPENING, # moving up - 2: STATE_OPEN, # manually stopped - 3: STATE_CLOSED, # lower limit - 4: STATE_OPEN, # upper limit / open - # gateController - 5: STATE_OPEN, # overload - 6: STATE_OPEN, # motor failure - # 7 is not used - 8: STATE_OPEN, # safety stop -} BLEBOX_TO_UNIT_MAP = {"celsius": TEMP_CELSIUS} diff --git a/homeassistant/components/blebox/cover.py b/homeassistant/components/blebox/cover.py index 368443988a4..8763ec34d34 100644 --- a/homeassistant/components/blebox/cover.py +++ b/homeassistant/components/blebox/cover.py @@ -9,12 +9,26 @@ from homeassistant.components.cover import ( CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING +from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BleBoxEntity, create_blebox_entities -from .const import BLEBOX_TO_HASS_COVER_STATES, BLEBOX_TO_HASS_DEVICE_CLASSES +from .const import BLEBOX_TO_HASS_DEVICE_CLASSES + +BLEBOX_TO_HASS_COVER_STATES = { + None: None, + 0: STATE_CLOSING, # moving down + 1: STATE_OPENING, # moving up + 2: STATE_OPEN, # manually stopped + 3: STATE_CLOSED, # lower limit + 4: STATE_OPEN, # upper limit / open + # gateController + 5: STATE_OPEN, # overload + 6: STATE_OPEN, # motor failure + # 7 is not used + 8: STATE_OPEN, # safety stop +} async def async_setup_entry( diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index 32cc6360db1..2bb6bc91762 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -4,7 +4,6 @@ from __future__ import annotations from datetime import timedelta import logging -from blebox_uniapi.error import BadOnValueError import blebox_uniapi.light from blebox_uniapi.light import BleboxColorMode @@ -166,10 +165,10 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity): else: try: await self._feature.async_on(value) - except BadOnValueError as ex: - _LOGGER.error( - "Turning on '%s' failed: Bad value %s (%s)", self.name, value, ex - ) + except ValueError as exc: + raise ValueError( + f"Turning on '{self.name}' failed: Bad value {value}" + ) from exc async def async_turn_off(self, **kwargs): """Turn the light off.""" diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 5c57d5f6b9f..08554695316 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -3,8 +3,8 @@ "name": "BleBox devices", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blebox", - "requirements": ["blebox_uniapi==2.0.0"], - "codeowners": ["@bbx-a", "@bbx-jp", "@riokuu"], + "requirements": ["blebox_uniapi==2.0.1"], + "codeowners": ["@bbx-a", "@riokuu"], "iot_class": "local_polling", "loggers": ["blebox_uniapi"] } diff --git a/requirements_all.txt b/requirements_all.txt index d71232f1fd5..9468e937b57 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -402,7 +402,7 @@ bimmer_connected==0.9.6 bizkaibus==0.1.1 # homeassistant.components.blebox -blebox_uniapi==2.0.0 +blebox_uniapi==2.0.1 # homeassistant.components.blink blinkpy==0.19.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2a194ca559..c168bb44ce5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -317,7 +317,7 @@ bellows==0.31.0 bimmer_connected==0.9.6 # homeassistant.components.blebox -blebox_uniapi==2.0.0 +blebox_uniapi==2.0.1 # homeassistant.components.blink blinkpy==0.19.0 diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index 4663c216136..f61496714fb 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -509,17 +509,18 @@ async def test_turn_on_failure(feature, hass, config, caplog): caplog.set_level(logging.ERROR) feature_mock, entity_id = feature - feature_mock.async_on = AsyncMock(side_effect=blebox_uniapi.error.BadOnValueError) + feature_mock.async_on = AsyncMock(side_effect=ValueError) await async_setup_entity(hass, config, entity_id) feature_mock.sensible_on_value = 123 - await hass.services.async_call( - "light", - SERVICE_TURN_ON, - {"entity_id": entity_id}, - blocking=True, - ) + with pytest.raises(ValueError) as info: + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {"entity_id": entity_id}, + blocking=True, + ) - assert ( - f"Turning on '{feature_mock.full_name}' failed: Bad value 123 ()" in caplog.text + assert f"Turning on '{feature_mock.full_name}' failed: Bad value 123" in str( + info.value ) From 84119eefaac50d827ee184ecd79fb9e7d0f6f49b Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 3 Jul 2022 18:51:50 +0200 Subject: [PATCH 2093/3516] Add NextDNS integration (#74150) * Initial commit * Update manifest * Add first test * Simplify init * More tests * Update tests * More tests * More tests * Add tests for sensor platform * More tests for sensor platform * Add tests for system_health * Fix typo * Improve test coverage * Improve test coverage * Add tests for diagnostics * Add comment * Run hassfest * Fix typo * Run gen_requirements_all * Fix tests * Change key name in diagnostics * Remove diagnostics and system_health platforms * Bump library --- CODEOWNERS | 2 + homeassistant/components/nextdns/__init__.py | 185 +++++++ .../components/nextdns/config_flow.py | 88 +++ homeassistant/components/nextdns/const.py | 15 + .../components/nextdns/manifest.json | 10 + homeassistant/components/nextdns/sensor.py | 299 +++++++++++ homeassistant/components/nextdns/strings.json | 24 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/nextdns/__init__.py | 63 +++ tests/components/nextdns/test_config_flow.py | 100 ++++ tests/components/nextdns/test_init.py | 47 ++ tests/components/nextdns/test_sensor.py | 502 ++++++++++++++++++ 14 files changed, 1342 insertions(+) create mode 100644 homeassistant/components/nextdns/__init__.py create mode 100644 homeassistant/components/nextdns/config_flow.py create mode 100644 homeassistant/components/nextdns/const.py create mode 100644 homeassistant/components/nextdns/manifest.json create mode 100644 homeassistant/components/nextdns/sensor.py create mode 100644 homeassistant/components/nextdns/strings.json create mode 100644 tests/components/nextdns/__init__.py create mode 100644 tests/components/nextdns/test_config_flow.py create mode 100644 tests/components/nextdns/test_init.py create mode 100644 tests/components/nextdns/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 9e3c1e8e0b8..6ae1c9605db 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -698,6 +698,8 @@ build.json @home-assistant/supervisor /homeassistant/components/nextbus/ @vividboarder /tests/components/nextbus/ @vividboarder /homeassistant/components/nextcloud/ @meichthys +/homeassistant/components/nextdns/ @bieniu +/tests/components/nextdns/ @bieniu /homeassistant/components/nfandroidtv/ @tkdrob /tests/components/nfandroidtv/ @tkdrob /homeassistant/components/nightscout/ @marciogranzotto diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py new file mode 100644 index 00000000000..71ddeb9e8eb --- /dev/null +++ b/homeassistant/components/nextdns/__init__.py @@ -0,0 +1,185 @@ +"""The NextDNS component.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +import logging + +from aiohttp.client_exceptions import ClientConnectorError +from async_timeout import timeout +from nextdns import ( + AnalyticsDnssec, + AnalyticsEncryption, + AnalyticsIpVersions, + AnalyticsProtocols, + AnalyticsStatus, + ApiError, + InvalidApiKeyError, + NextDns, +) +from nextdns.model import NextDnsData + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + ATTR_DNSSEC, + ATTR_ENCRYPTION, + ATTR_IP_VERSIONS, + ATTR_PROTOCOLS, + ATTR_STATUS, + CONF_PROFILE_ID, + DOMAIN, + UPDATE_INTERVAL_ANALYTICS, +) + + +class NextDnsUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching NextDNS data API.""" + + def __init__( + self, + hass: HomeAssistant, + nextdns: NextDns, + profile_id: str, + update_interval: timedelta, + ) -> None: + """Initialize.""" + self.nextdns = nextdns + self.profile_id = profile_id + self.profile_name = nextdns.get_profile_name(profile_id) + self.device_info = DeviceInfo( + configuration_url=f"https://my.nextdns.io/{profile_id}/setup", + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, str(profile_id))}, + manufacturer="NextDNS Inc.", + name=self.profile_name, + ) + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + async def _async_update_data(self) -> NextDnsData: + """Update data via library.""" + raise NotImplementedError("Update method not implemented") + + +class NextDnsStatusUpdateCoordinator(NextDnsUpdateCoordinator): + """Class to manage fetching NextDNS analytics status data from API.""" + + async def _async_update_data(self) -> AnalyticsStatus: + """Update data via library.""" + try: + async with timeout(10): + return await self.nextdns.get_analytics_status(self.profile_id) + except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: + raise UpdateFailed(err) from err + + +class NextDnsDnssecUpdateCoordinator(NextDnsUpdateCoordinator): + """Class to manage fetching NextDNS analytics Dnssec data from API.""" + + async def _async_update_data(self) -> AnalyticsDnssec: + """Update data via library.""" + try: + async with timeout(10): + return await self.nextdns.get_analytics_dnssec(self.profile_id) + except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: + raise UpdateFailed(err) from err + + +class NextDnsEncryptionUpdateCoordinator(NextDnsUpdateCoordinator): + """Class to manage fetching NextDNS analytics encryption data from API.""" + + async def _async_update_data(self) -> AnalyticsEncryption: + """Update data via library.""" + try: + async with timeout(10): + return await self.nextdns.get_analytics_encryption(self.profile_id) + except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: + raise UpdateFailed(err) from err + + +class NextDnsIpVersionsUpdateCoordinator(NextDnsUpdateCoordinator): + """Class to manage fetching NextDNS analytics IP versions data from API.""" + + async def _async_update_data(self) -> AnalyticsIpVersions: + """Update data via library.""" + try: + async with timeout(10): + return await self.nextdns.get_analytics_ip_versions(self.profile_id) + except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: + raise UpdateFailed(err) from err + + +class NextDnsProtocolsUpdateCoordinator(NextDnsUpdateCoordinator): + """Class to manage fetching NextDNS analytics protocols data from API.""" + + async def _async_update_data(self) -> AnalyticsProtocols: + """Update data via library.""" + try: + async with timeout(10): + return await self.nextdns.get_analytics_protocols(self.profile_id) + except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: + raise UpdateFailed(err) from err + + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["sensor"] +COORDINATORS = [ + (ATTR_DNSSEC, NextDnsDnssecUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), + (ATTR_ENCRYPTION, NextDnsEncryptionUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), + (ATTR_IP_VERSIONS, NextDnsIpVersionsUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), + (ATTR_PROTOCOLS, NextDnsProtocolsUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), + (ATTR_STATUS, NextDnsStatusUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), +] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up NextDNS as config entry.""" + api_key = entry.data[CONF_API_KEY] + profile_id = entry.data[CONF_PROFILE_ID] + + websession = async_get_clientsession(hass) + try: + async with timeout(10): + nextdns = await NextDns.create(websession, api_key) + except (ApiError, ClientConnectorError, asyncio.TimeoutError) as err: + raise ConfigEntryNotReady from err + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN].setdefault(entry.entry_id, {}) + + tasks = [] + + # Independent DataUpdateCoordinator is used for each API endpoint to avoid + # unnecessary requests when entities using this endpoint are disabled. + for coordinator_name, coordinator_class, update_interval in COORDINATORS: + hass.data[DOMAIN][entry.entry_id][coordinator_name] = coordinator_class( + hass, nextdns, profile_id, update_interval + ) + tasks.append( + hass.data[DOMAIN][entry.entry_id][coordinator_name].async_refresh() + ) + + await asyncio.gather(*tasks) + + 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: bool = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/nextdns/config_flow.py b/homeassistant/components/nextdns/config_flow.py new file mode 100644 index 00000000000..c621accfe81 --- /dev/null +++ b/homeassistant/components/nextdns/config_flow.py @@ -0,0 +1,88 @@ +"""Adds config flow for NextDNS.""" +from __future__ import annotations + +import asyncio +from typing import Any + +from aiohttp.client_exceptions import ClientConnectorError +from async_timeout import timeout +from nextdns import ApiError, InvalidApiKeyError, NextDns +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 .const import CONF_PROFILE_ID, CONF_PROFILE_NAME, DOMAIN + + +class NextDnsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for NextDNS.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self.nextdns: NextDns + self.api_key: str + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors: dict[str, str] = {} + + websession = async_get_clientsession(self.hass) + + if user_input is not None: + self.api_key = user_input[CONF_API_KEY] + try: + async with timeout(10): + self.nextdns = await NextDns.create( + websession, user_input[CONF_API_KEY] + ) + except InvalidApiKeyError: + errors["base"] = "invalid_api_key" + except (ApiError, ClientConnectorError, asyncio.TimeoutError): + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + errors["base"] = "unknown" + else: + return await self.async_step_profiles() + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}), + errors=errors, + ) + + async def async_step_profiles( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the profiles step.""" + errors: dict[str, str] = {} + + if user_input is not None: + profile_name = user_input[CONF_PROFILE_NAME] + profile_id = self.nextdns.get_profile_id(profile_name) + + await self.async_set_unique_id(profile_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=profile_name, + data={CONF_PROFILE_ID: profile_id, CONF_API_KEY: self.api_key}, + ) + + return self.async_show_form( + step_id="profiles", + data_schema=vol.Schema( + { + vol.Required(CONF_PROFILE_NAME): vol.In( + [profile.name for profile in self.nextdns.profiles] + ) + } + ), + errors=errors, + ) diff --git a/homeassistant/components/nextdns/const.py b/homeassistant/components/nextdns/const.py new file mode 100644 index 00000000000..04bab44354b --- /dev/null +++ b/homeassistant/components/nextdns/const.py @@ -0,0 +1,15 @@ +"""Constants for NextDNS integration.""" +from datetime import timedelta + +ATTR_DNSSEC = "dnssec" +ATTR_ENCRYPTION = "encryption" +ATTR_IP_VERSIONS = "ip_versions" +ATTR_PROTOCOLS = "protocols" +ATTR_STATUS = "status" + +CONF_PROFILE_ID = "profile_id" +CONF_PROFILE_NAME = "profile_name" + +UPDATE_INTERVAL_ANALYTICS = timedelta(minutes=10) + +DOMAIN = "nextdns" diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json new file mode 100644 index 00000000000..fd3dd46f846 --- /dev/null +++ b/homeassistant/components/nextdns/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "nextdns", + "name": "NextDNS", + "documentation": "https://www.home-assistant.io/integrations/nextdns", + "codeowners": ["@bieniu"], + "requirements": ["nextdns==1.0.0"], + "config_flow": true, + "iot_class": "cloud_polling", + "loggers": ["nextdns"] +} diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py new file mode 100644 index 00000000000..35a15f010b3 --- /dev/null +++ b/homeassistant/components/nextdns/sensor.py @@ -0,0 +1,299 @@ +"""Support for the NextDNS service.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import NextDnsUpdateCoordinator +from .const import ( + ATTR_DNSSEC, + ATTR_ENCRYPTION, + ATTR_IP_VERSIONS, + ATTR_PROTOCOLS, + ATTR_STATUS, + DOMAIN, +) + +PARALLEL_UPDATES = 1 + + +@dataclass +class NextDnsSensorRequiredKeysMixin: + """Class for NextDNS entity required keys.""" + + coordinator_type: str + + +@dataclass +class NextDnsSensorEntityDescription( + SensorEntityDescription, NextDnsSensorRequiredKeysMixin +): + """NextDNS sensor entity description.""" + + +SENSORS = ( + NextDnsSensorEntityDescription( + key="all_queries", + coordinator_type=ATTR_STATUS, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:dns", + name="{profile_name} DNS Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="blocked_queries", + coordinator_type=ATTR_STATUS, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:dns", + name="{profile_name} DNS Queries Blocked", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="relayed_queries", + coordinator_type=ATTR_STATUS, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:dns", + name="{profile_name} DNS Queries Relayed", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="blocked_queries_ratio", + coordinator_type=ATTR_STATUS, + entity_category=EntityCategory.DIAGNOSTIC, + icon="mdi:dns", + name="{profile_name} DNS Queries Blocked Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="doh_queries", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="{profile_name} DNS-over-HTTPS Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="dot_queries", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="{profile_name} DNS-over-TLS Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="doq_queries", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="{profile_name} DNS-over-QUIC Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="udp_queries", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="{profile_name} UDP Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="doh_queries_ratio", + coordinator_type=ATTR_PROTOCOLS, + entity_registry_enabled_default=False, + icon="mdi:dns", + entity_category=EntityCategory.DIAGNOSTIC, + name="{profile_name} DNS-over-HTTPS Queries Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="dot_queries_ratio", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="{profile_name} DNS-over-TLS Queries Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="doq_queries_ratio", + coordinator_type=ATTR_PROTOCOLS, + entity_registry_enabled_default=False, + icon="mdi:dns", + entity_category=EntityCategory.DIAGNOSTIC, + name="{profile_name} DNS-over-QUIC Queries Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="udp_queries_ratio", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="{profile_name} UDP Queries Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="encrypted_queries", + coordinator_type=ATTR_ENCRYPTION, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:lock", + name="{profile_name} Encrypted Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="unencrypted_queries", + coordinator_type=ATTR_ENCRYPTION, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:lock-open", + name="{profile_name} Unencrypted Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="encrypted_queries_ratio", + coordinator_type=ATTR_ENCRYPTION, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:lock", + name="{profile_name} Encrypted Queries Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="ipv4_queries", + coordinator_type=ATTR_IP_VERSIONS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:ip", + name="{profile_name} IPv4 Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="ipv6_queries", + coordinator_type=ATTR_IP_VERSIONS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:ip", + name="{profile_name} IPv6 Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="ipv6_queries_ratio", + coordinator_type=ATTR_IP_VERSIONS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:ip", + name="{profile_name} IPv6 Queries Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="validated_queries", + coordinator_type=ATTR_DNSSEC, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:lock-check", + name="{profile_name} DNSSEC Validated Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="not_validated_queries", + coordinator_type=ATTR_DNSSEC, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:lock-alert", + name="{profile_name} DNSSEC Not Validated Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.MEASUREMENT, + ), + NextDnsSensorEntityDescription( + key="validated_queries_ratio", + coordinator_type=ATTR_DNSSEC, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:lock-check", + name="{profile_name} DNSSEC Validated Queries Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add a NextDNS entities from a config_entry.""" + sensors: list[NextDnsSensor] = [] + coordinators = hass.data[DOMAIN][entry.entry_id] + + for description in SENSORS: + sensors.append( + NextDnsSensor(coordinators[description.coordinator_type], description) + ) + + async_add_entities(sensors) + + +class NextDnsSensor(CoordinatorEntity, SensorEntity): + """Define an NextDNS sensor.""" + + coordinator: NextDnsUpdateCoordinator + + def __init__( + self, + coordinator: NextDnsUpdateCoordinator, + description: SensorEntityDescription, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + self._attr_device_info = coordinator.device_info + self._attr_unique_id = f"{coordinator.profile_id}_{description.key}" + self._attr_name = cast(str, description.name).format( + profile_name=coordinator.profile_name + ) + self._attr_native_value = getattr(coordinator.data, description.key) + self.entity_description = description + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_native_value = getattr( + self.coordinator.data, self.entity_description.key + ) + self.async_write_ha_state() diff --git a/homeassistant/components/nextdns/strings.json b/homeassistant/components/nextdns/strings.json new file mode 100644 index 00000000000..db3cf88cf39 --- /dev/null +++ b/homeassistant/components/nextdns/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + }, + "profiles": { + "data": { + "profile": "Profile" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "This NextDNS profile is already configured." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index eba8bfe10c4..d3be2c5e674 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -235,6 +235,7 @@ FLOWS = { "netatmo", "netgear", "nexia", + "nextdns", "nfandroidtv", "nightscout", "nina", diff --git a/requirements_all.txt b/requirements_all.txt index 9468e937b57..87123abeba7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1094,6 +1094,9 @@ nextcloudmonitor==1.1.0 # homeassistant.components.discord nextcord==2.0.0a8 +# homeassistant.components.nextdns +nextdns==1.0.0 + # homeassistant.components.niko_home_control niko-home-control==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c168bb44ce5..b47aefebf16 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -762,6 +762,9 @@ nexia==2.0.1 # homeassistant.components.discord nextcord==2.0.0a8 +# homeassistant.components.nextdns +nextdns==1.0.0 + # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py new file mode 100644 index 00000000000..24063d957d4 --- /dev/null +++ b/tests/components/nextdns/__init__.py @@ -0,0 +1,63 @@ +"""Tests for the NextDNS integration.""" +from unittest.mock import patch + +from nextdns import ( + AnalyticsDnssec, + AnalyticsEncryption, + AnalyticsIpVersions, + AnalyticsProtocols, + AnalyticsStatus, +) + +from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN +from homeassistant.const import CONF_API_KEY + +from tests.common import MockConfigEntry + +PROFILES = [{"id": "xyz12", "fingerprint": "aabbccdd123", "name": "Fake Profile"}] +STATUS = AnalyticsStatus( + default_queries=40, allowed_queries=30, blocked_queries=20, relayed_queries=10 +) +DNSSEC = AnalyticsDnssec(not_validated_queries=25, validated_queries=75) +ENCRYPTION = AnalyticsEncryption(encrypted_queries=60, unencrypted_queries=40) +IP_VERSIONS = AnalyticsIpVersions(ipv4_queries=90, ipv6_queries=10) +PROTOCOLS = AnalyticsProtocols( + doh_queries=20, doq_queries=10, dot_queries=30, udp_queries=40 +) + + +async def init_integration(hass, add_to_hass=True) -> MockConfigEntry: + """Set up the NextDNS integration in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Fake Profile", + unique_id="xyz12", + data={CONF_API_KEY: "fake_api_key", CONF_PROFILE_ID: "xyz12"}, + ) + + if not add_to_hass: + return entry + + with patch( + "homeassistant.components.nextdns.NextDns.get_profiles", return_value=PROFILES + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_status", + return_value=STATUS, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_encryption", + return_value=ENCRYPTION, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_dnssec", + return_value=DNSSEC, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_ip_versions", + return_value=IP_VERSIONS, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_protocols", + return_value=PROTOCOLS, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/nextdns/test_config_flow.py b/tests/components/nextdns/test_config_flow.py new file mode 100644 index 00000000000..0b3ac8a798e --- /dev/null +++ b/tests/components/nextdns/test_config_flow.py @@ -0,0 +1,100 @@ +"""Define tests for the NextDNS config flow.""" +import asyncio +from unittest.mock import patch + +from nextdns import ApiError, InvalidApiKeyError +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.nextdns.const import ( + CONF_PROFILE_ID, + CONF_PROFILE_NAME, + DOMAIN, +) +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_API_KEY + +from . import PROFILES, init_integration + + +async def test_form_create_entry(hass): + """Test that the user step works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + assert result["errors"] == {} + + with patch( + "homeassistant.components.nextdns.NextDns.get_profiles", return_value=PROFILES + ), patch( + "homeassistant.components.nextdns.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "fake_api_key"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "profiles" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PROFILE_NAME: "Fake Profile"} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Fake Profile" + assert result["data"][CONF_API_KEY] == "fake_api_key" + assert result["data"][CONF_PROFILE_ID] == "xyz12" + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "error", + [ + (ApiError("API Error"), "cannot_connect"), + (InvalidApiKeyError, "invalid_api_key"), + (asyncio.TimeoutError, "cannot_connect"), + (ValueError, "unknown"), + ], +) +async def test_form_errors(hass, error): + """Test we handle errors.""" + exc, base_error = error + with patch( + "homeassistant.components.nextdns.NextDns.get_profiles", side_effect=exc + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_API_KEY: "fake_api_key"}, + ) + + assert result["errors"] == {"base": base_error} + + +async def test_form_already_configured(hass): + """Test that errors are shown when duplicates are added.""" + entry = await init_integration(hass) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + with patch( + "homeassistant.components.nextdns.NextDns.get_profiles", return_value=PROFILES + ): + await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "fake_api_key"}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_PROFILE_NAME: "Fake Profile"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/nextdns/test_init.py b/tests/components/nextdns/test_init.py new file mode 100644 index 00000000000..c16fad4e812 --- /dev/null +++ b/tests/components/nextdns/test_init.py @@ -0,0 +1,47 @@ +"""Test init of NextDNS integration.""" +from unittest.mock import patch + +from nextdns import ApiError + +from homeassistant.components.nextdns.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import STATE_UNAVAILABLE + +from . import init_integration + + +async def test_async_setup_entry(hass): + """Test a successful setup entry.""" + await init_integration(hass) + + state = hass.states.get("sensor.fake_profile_dns_queries_blocked_ratio") + assert state is not None + assert state.state != STATE_UNAVAILABLE + assert state.state == "20.0" + + +async def test_config_not_ready(hass): + """Test for setup failure if the connection to the service fails.""" + entry = await init_integration(hass, add_to_hass=False) + + with patch( + "homeassistant.components.nextdns.NextDns.get_profiles", + side_effect=ApiError("API Error"), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_unload_entry(hass): + """Test successful unload of entry.""" + entry = await init_integration(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state is 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) diff --git a/tests/components/nextdns/test_sensor.py b/tests/components/nextdns/test_sensor.py new file mode 100644 index 00000000000..fef2abafc85 --- /dev/null +++ b/tests/components/nextdns/test_sensor.py @@ -0,0 +1,502 @@ +"""Test sensor of NextDNS integration.""" +from datetime import timedelta +from unittest.mock import patch + +from nextdns import ApiError + +from homeassistant.components.nextdns.const import DOMAIN +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + SensorStateClass, +) +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNAVAILABLE +from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow + +from . import DNSSEC, ENCRYPTION, IP_VERSIONS, PROTOCOLS, STATUS, init_integration + +from tests.common import async_fire_time_changed + + +async def test_sensor(hass): + """Test states of sensors.""" + registry = er.async_get(hass) + + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_doh_queries", + suggested_object_id="fake_profile_dns_over_https_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_doh_queries_ratio", + suggested_object_id="fake_profile_dns_over_https_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_doq_queries", + suggested_object_id="fake_profile_dns_over_quic_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_doq_queries_ratio", + suggested_object_id="fake_profile_dns_over_quic_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_dot_queries", + suggested_object_id="fake_profile_dns_over_tls_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_dot_queries_ratio", + suggested_object_id="fake_profile_dns_over_tls_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_not_validated_queries", + suggested_object_id="fake_profile_dnssec_not_validated_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_validated_queries", + suggested_object_id="fake_profile_dnssec_validated_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_validated_queries_ratio", + suggested_object_id="fake_profile_dnssec_validated_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_encrypted_queries", + suggested_object_id="fake_profile_encrypted_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_encrypted_queries_ratio", + suggested_object_id="fake_profile_encrypted_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_ipv4_queries", + suggested_object_id="fake_profile_ipv4_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_ipv6_queries", + suggested_object_id="fake_profile_ipv6_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_ipv6_queries_ratio", + suggested_object_id="fake_profile_ipv6_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_udp_queries", + suggested_object_id="fake_profile_udp_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_udp_queries_ratio", + suggested_object_id="fake_profile_udp_queries_ratio", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_unencrypted_queries", + suggested_object_id="fake_profile_unencrypted_queries", + disabled_by=None, + ) + + await init_integration(hass) + + state = hass.states.get("sensor.fake_profile_dns_queries") + assert state + assert state.state == "100" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_queries") + assert entry + assert entry.unique_id == "xyz12_all_queries" + + state = hass.states.get("sensor.fake_profile_dns_queries_blocked") + assert state + assert state.state == "20" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_queries_blocked") + assert entry + assert entry.unique_id == "xyz12_blocked_queries" + + state = hass.states.get("sensor.fake_profile_dns_queries_blocked_ratio") + assert state + assert state.state == "20.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_dns_queries_blocked_ratio") + assert entry + assert entry.unique_id == "xyz12_blocked_queries_ratio" + + state = hass.states.get("sensor.fake_profile_dns_queries_relayed") + assert state + assert state.state == "10" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_queries_relayed") + assert entry + assert entry.unique_id == "xyz12_relayed_queries" + + state = hass.states.get("sensor.fake_profile_dns_over_https_queries") + assert state + assert state.state == "20" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_over_https_queries") + assert entry + assert entry.unique_id == "xyz12_doh_queries" + + state = hass.states.get("sensor.fake_profile_dns_over_https_queries_ratio") + assert state + assert state.state == "22.2" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_dns_over_https_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_doh_queries_ratio" + + state = hass.states.get("sensor.fake_profile_dns_over_quic_queries") + assert state + assert state.state == "10" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_over_quic_queries") + assert entry + assert entry.unique_id == "xyz12_doq_queries" + + state = hass.states.get("sensor.fake_profile_dns_over_quic_queries_ratio") + assert state + assert state.state == "11.1" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_dns_over_quic_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_doq_queries_ratio" + + state = hass.states.get("sensor.fake_profile_dns_over_tls_queries") + assert state + assert state.state == "30" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_over_tls_queries") + assert entry + assert entry.unique_id == "xyz12_dot_queries" + + state = hass.states.get("sensor.fake_profile_dns_over_tls_queries_ratio") + assert state + assert state.state == "33.3" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_dns_over_tls_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_dot_queries_ratio" + + state = hass.states.get("sensor.fake_profile_dnssec_not_validated_queries") + assert state + assert state.state == "25" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dnssec_not_validated_queries") + assert entry + assert entry.unique_id == "xyz12_not_validated_queries" + + state = hass.states.get("sensor.fake_profile_dnssec_validated_queries") + assert state + assert state.state == "75" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dnssec_validated_queries") + assert entry + assert entry.unique_id == "xyz12_validated_queries" + + state = hass.states.get("sensor.fake_profile_dnssec_validated_queries_ratio") + assert state + assert state.state == "75.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_dnssec_validated_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_validated_queries_ratio" + + state = hass.states.get("sensor.fake_profile_encrypted_queries") + assert state + assert state.state == "60" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_encrypted_queries") + assert entry + assert entry.unique_id == "xyz12_encrypted_queries" + + state = hass.states.get("sensor.fake_profile_unencrypted_queries") + assert state + assert state.state == "40" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_unencrypted_queries") + assert entry + assert entry.unique_id == "xyz12_unencrypted_queries" + + state = hass.states.get("sensor.fake_profile_encrypted_queries_ratio") + assert state + assert state.state == "60.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_encrypted_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_encrypted_queries_ratio" + + state = hass.states.get("sensor.fake_profile_ipv4_queries") + assert state + assert state.state == "90" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_ipv4_queries") + assert entry + assert entry.unique_id == "xyz12_ipv4_queries" + + state = hass.states.get("sensor.fake_profile_ipv6_queries") + assert state + assert state.state == "10" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_ipv6_queries") + assert entry + assert entry.unique_id == "xyz12_ipv6_queries" + + state = hass.states.get("sensor.fake_profile_ipv6_queries_ratio") + assert state + assert state.state == "10.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_ipv6_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_ipv6_queries_ratio" + + state = hass.states.get("sensor.fake_profile_udp_queries") + assert state + assert state.state == "40" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_udp_queries") + assert entry + assert entry.unique_id == "xyz12_udp_queries" + + state = hass.states.get("sensor.fake_profile_udp_queries_ratio") + assert state + assert state.state == "44.4" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_udp_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_udp_queries_ratio" + + +async def test_availability(hass): + """Ensure that we mark the entities unavailable correctly when service causes an error.""" + registry = er.async_get(hass) + + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_doh_queries", + suggested_object_id="fake_profile_dns_over_https_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_validated_queries", + suggested_object_id="fake_profile_dnssec_validated_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_encrypted_queries", + suggested_object_id="fake_profile_encrypted_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_ipv4_queries", + suggested_object_id="fake_profile_ipv4_queries", + disabled_by=None, + ) + + await init_integration(hass) + + state = hass.states.get("sensor.fake_profile_dns_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "100" + + state = hass.states.get("sensor.fake_profile_dns_over_https_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "20" + + state = hass.states.get("sensor.fake_profile_dnssec_validated_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "75" + + state = hass.states.get("sensor.fake_profile_encrypted_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "60" + + state = hass.states.get("sensor.fake_profile_ipv4_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "90" + + future = utcnow() + timedelta(minutes=10) + with patch( + "homeassistant.components.nextdns.NextDns.get_analytics_status", + side_effect=ApiError("API Error"), + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_dnssec", + side_effect=ApiError("API Error"), + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_encryption", + side_effect=ApiError("API Error"), + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_ip_versions", + side_effect=ApiError("API Error"), + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_protocols", + side_effect=ApiError("API Error"), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.fake_profile_dns_queries") + assert state + assert state.state == STATE_UNAVAILABLE + + state = hass.states.get("sensor.fake_profile_dns_over_https_queries") + assert state + assert state.state == STATE_UNAVAILABLE + + state = hass.states.get("sensor.fake_profile_dnssec_validated_queries") + assert state + assert state.state == STATE_UNAVAILABLE + + state = hass.states.get("sensor.fake_profile_encrypted_queries") + assert state + assert state.state == STATE_UNAVAILABLE + + state = hass.states.get("sensor.fake_profile_ipv4_queries") + assert state + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=20) + with patch( + "homeassistant.components.nextdns.NextDns.get_analytics_status", + return_value=STATUS, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_encryption", + return_value=ENCRYPTION, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_dnssec", + return_value=DNSSEC, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_ip_versions", + return_value=IP_VERSIONS, + ), patch( + "homeassistant.components.nextdns.NextDns.get_analytics_protocols", + return_value=PROTOCOLS, + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.fake_profile_dns_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "100" + + state = hass.states.get("sensor.fake_profile_dns_over_https_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "20" + + state = hass.states.get("sensor.fake_profile_dnssec_validated_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "75" + + state = hass.states.get("sensor.fake_profile_encrypted_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "60" + + state = hass.states.get("sensor.fake_profile_ipv4_queries") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "90" From 936b271448c43cd19b1a0cce836ba494fe7b7b06 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 3 Jul 2022 22:06:50 +0200 Subject: [PATCH 2094/3516] Fix typo in nightly build (#74363) --- .github/workflows/builder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 870bfb9b1e9..860d11c62bf 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -109,7 +109,7 @@ jobs: github_token: ${{secrets.GITHUB_TOKEN}} repo: home-assistant/frontend branch: dev - workflow: nightly.yml + workflow: nightly.yaml workflow_conclusion: success name: wheels From 57114c1a558d8e861a6c994606805f02145d1e20 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 3 Jul 2022 22:16:29 +0200 Subject: [PATCH 2095/3516] Add tomli as nightly build dependency (#74364) --- .github/workflows/builder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 860d11c62bf..826af2eb9fe 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -123,7 +123,7 @@ jobs: if: needs.init.outputs.channel == 'dev' shell: bash run: | - python3 -m pip install packaging + python3 -m pip install packaging tomli python3 -m pip install --use-deprecated=legacy-resolver . version="$(python3 script/version_bump.py nightly)" From 737a1fd9fa4b36c37ad07624adf180a2438826f8 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Sun, 3 Jul 2022 21:26:00 +0100 Subject: [PATCH 2096/3516] Dont substitute user/pass for relative stream urls on generic camera (#74201) Co-authored-by: Dave T --- homeassistant/components/generic/camera.py | 8 +++- .../components/generic/config_flow.py | 13 +++++- homeassistant/components/generic/strings.json | 4 ++ .../components/generic/translations/en.json | 11 ++--- tests/components/generic/test_config_flow.py | 46 +++++++++++++++++-- 5 files changed, 68 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 8b03f0a8ed3..961d3cecfb7 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -228,7 +228,13 @@ class GenericCamera(Camera): try: stream_url = self._stream_source.async_render(parse_result=False) url = yarl.URL(stream_url) - if not url.user and not url.password and self._username and self._password: + if ( + not url.user + and not url.password + and self._username + and self._password + and url.is_absolute() + ): url = url.with_user(self._username).with_password(self._password) return str(url) except TemplateError as err: diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 9096f2ce87e..514264f919e 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -150,6 +150,12 @@ async def async_test_still( except TemplateError as err: _LOGGER.warning("Problem rendering template %s: %s", url, err) return {CONF_STILL_IMAGE_URL: "template_error"}, None + try: + yarl_url = yarl.URL(url) + except ValueError: + return {CONF_STILL_IMAGE_URL: "malformed_url"}, None + if not yarl_url.is_absolute(): + return {CONF_STILL_IMAGE_URL: "relative_url"}, None verify_ssl = info[CONF_VERIFY_SSL] auth = generate_auth(info) try: @@ -222,7 +228,12 @@ async def async_test_stream( if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True - url = yarl.URL(stream_source) + try: + url = yarl.URL(stream_source) + except ValueError: + return {CONF_STREAM_SOURCE: "malformed_url"} + if not url.is_absolute(): + return {CONF_STREAM_SOURCE: "relative_url"} if not url.user and not url.password: username = info.get(CONF_USERNAME) password = info.get(CONF_PASSWORD) diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index 7d3cab19aa5..608c85c1379 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -6,6 +6,8 @@ "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", "invalid_still_image": "URL did not return a valid still image", + "malformed_url": "Malformed URL", + "relative_url": "Relative URLs are not allowed", "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", @@ -75,6 +77,8 @@ "unable_still_load": "[%key:component::generic::config::error::unable_still_load%]", "no_still_image_or_stream_url": "[%key:component::generic::config::error::no_still_image_or_stream_url%]", "invalid_still_image": "[%key:component::generic::config::error::invalid_still_image%]", + "malformed_url": "[%key:component::generic::config::error::malformed_url%]", + "relative_url": "[%key:component::generic::config::error::relative_url%]", "template_error": "[%key:component::generic::config::error::template_error%]", "timeout": "[%key:component::generic::config::error::timeout%]", "stream_no_route_to_host": "[%key:component::generic::config::error::stream_no_route_to_host%]", diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json index d01e6e59a4b..cb2200f9755 100644 --- a/homeassistant/components/generic/translations/en.json +++ b/homeassistant/components/generic/translations/en.json @@ -1,20 +1,19 @@ { "config": { "abort": { - "no_devices_found": "No devices found on the network", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { "already_exists": "A camera with these URL settings already exists.", "invalid_still_image": "URL did not return a valid still image", + "malformed_url": "Malformed URL", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", + "relative_url": "Relative URLs are not allowed", "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", - "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", - "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", @@ -50,14 +49,12 @@ "error": { "already_exists": "A camera with these URL settings already exists.", "invalid_still_image": "URL did not return a valid still image", + "malformed_url": "Malformed URL", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", - "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", - "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", + "relative_url": "Relative URLs are not allowed", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", - "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", - "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index f0589301014..592d139f92e 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -180,33 +180,43 @@ async def test_form_only_still_sample(hass, user_flow, image_file): @respx.mock @pytest.mark.parametrize( - ("template", "url", "expected_result"), + ("template", "url", "expected_result", "expected_errors"), [ # Test we can handle templates in strange parts of the url, #70961. ( "http://localhost:812{{3}}/static/icons/favicon-apple-180x180.png", "http://localhost:8123/static/icons/favicon-apple-180x180.png", data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + None, ), ( "{% if 1 %}https://bla{% else %}https://yo{% endif %}", "https://bla/", data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + None, ), ( "http://{{example.org", "http://example.org", data_entry_flow.RESULT_TYPE_FORM, + {"still_image_url": "template_error"}, ), ( "invalid1://invalid:4\\1", "invalid1://invalid:4%5c1", - data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + data_entry_flow.RESULT_TYPE_FORM, + {"still_image_url": "malformed_url"}, + ), + ( + "relative/urls/are/not/allowed.jpg", + "relative/urls/are/not/allowed.jpg", + data_entry_flow.RESULT_TYPE_FORM, + {"still_image_url": "relative_url"}, ), ], ) async def test_still_template( - hass, user_flow, fakeimgbytes_png, template, url, expected_result + hass, user_flow, fakeimgbytes_png, template, url, expected_result, expected_errors ) -> None: """Test we can handle various templates.""" respx.get(url).respond(stream=fakeimgbytes_png) @@ -220,6 +230,7 @@ async def test_still_template( ) await hass.async_block_till_done() assert result2["type"] == expected_result + assert result2.get("errors") == expected_errors @respx.mock @@ -514,8 +525,29 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream result4["flow_id"], user_input=data, ) - assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM - assert result5["errors"] == {"stream_source": "template_error"} + + assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result5["errors"] == {"stream_source": "template_error"} + + # verify that an relative stream url is rejected. + data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/1" + data[CONF_STREAM_SOURCE] = "relative/stream.mjpeg" + result6 = await hass.config_entries.options.async_configure( + result5["flow_id"], + user_input=data, + ) + assert result6.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result6["errors"] == {"stream_source": "relative_url"} + + # verify that an malformed stream url is rejected. + data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/1" + data[CONF_STREAM_SOURCE] = "http://example.com:45:56" + result7 = await hass.config_entries.options.async_configure( + result6["flow_id"], + user_input=data, + ) + assert result7.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result7["errors"] == {"stream_source": "malformed_url"} async def test_slug(hass, caplog): @@ -528,6 +560,10 @@ async def test_slug(hass, caplog): assert result is None assert "Syntax error in" in caplog.text + result = slug(hass, "http://example.com:999999999999/stream") + assert result is None + assert "Syntax error in" in caplog.text + @respx.mock async def test_options_only_stream(hass, fakeimgbytes_png, mock_create_stream): From 30a5df58952ad45610ec322128af3fd0bdd3945c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Jul 2022 15:47:54 -0500 Subject: [PATCH 2097/3516] Append name char value from the service to HomeKit Controller Entities (#74359) --- .../components/homekit_controller/__init__.py | 25 ++- .../components/homekit_controller/button.py | 2 +- .../components/homekit_controller/number.py | 8 +- .../components/homekit_controller/sensor.py | 60 +++-- .../components/homekit_controller/switch.py | 8 +- .../components/homekit_controller/utils.py | 5 + .../homekit_controller/fixtures/mss425f.json | 212 ++++++++++++++++++ .../homekit_controller/fixtures/mss565.json | 132 +++++++++++ .../specific_devices/test_aqara_gateway.py | 12 +- .../specific_devices/test_aqara_switch.py | 4 +- .../specific_devices/test_arlo_baby.py | 8 +- .../specific_devices/test_connectsense.py | 8 +- .../specific_devices/test_koogeek_ls1.py | 10 +- .../specific_devices/test_koogeek_p1eu.py | 4 +- .../specific_devices/test_koogeek_sw2.py | 10 +- .../specific_devices/test_mss425f.py | 73 ++++++ .../specific_devices/test_mss565.py | 42 ++++ .../specific_devices/test_mysa_living.py | 8 +- .../test_ryse_smart_bridge.py | 48 ++-- .../test_simpleconnect_fan.py | 4 +- .../specific_devices/test_velux_gateway.py | 16 +- .../test_vocolinc_flowerbud.py | 4 +- .../specific_devices/test_vocolinc_vp3.py | 4 +- .../homekit_controller/test_diagnostics.py | 92 ++++---- .../homekit_controller/test_init.py | 6 +- .../homekit_controller/test_light.py | 2 +- 26 files changed, 662 insertions(+), 145 deletions(-) create mode 100644 tests/components/homekit_controller/fixtures/mss425f.json create mode 100644 tests/components/homekit_controller/fixtures/mss565.json create mode 100644 tests/components/homekit_controller/specific_devices/test_mss425f.py create mode 100644 tests/components/homekit_controller/specific_devices/test_mss565.py diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 6909b226556..e5853c8ba66 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -27,7 +27,7 @@ from .config_flow import normalize_hkid from .connection import HKDevice, valid_serial_number from .const import ENTITY_MAP, KNOWN_DEVICES, TRIGGERS from .storage import EntityMapStorage -from .utils import async_get_controller +from .utils import async_get_controller, folded_name _LOGGER = logging.getLogger(__name__) @@ -42,6 +42,7 @@ class HomeKitEntity(Entity): self._accessory = accessory self._aid = devinfo["aid"] self._iid = devinfo["iid"] + self._char_name: str | None = None self._features = 0 self.setup() @@ -127,6 +128,9 @@ class HomeKitEntity(Entity): if CharacteristicPermissions.events in char.perms: self.watchable_characteristics.append((self._aid, char.iid)) + if self._char_name is None: + self._char_name = char.service.value(CharacteristicsTypes.NAME) + @property def unique_id(self) -> str: """Return the ID of this device.""" @@ -137,10 +141,27 @@ class HomeKitEntity(Entity): # Some accessories do not have a serial number return f"homekit-{self._accessory.unique_id}-{self._aid}-{self._iid}" + @property + def default_name(self) -> str | None: + """Return the default name of the device.""" + return None + @property def name(self) -> str | None: """Return the name of the device if any.""" - return self.accessory.name + accessory_name = self.accessory.name + # If the service has a name char, use that, if not + # fallback to the default name provided by the subclass + device_name = self._char_name or self.default_name + folded_device_name = folded_name(device_name or "") + folded_accessory_name = folded_name(accessory_name) + if ( + device_name + and folded_accessory_name not in folded_device_name + and folded_device_name not in folded_accessory_name + ): + return f"{accessory_name} {device_name}" + return accessory_name @property def available(self) -> bool: diff --git a/homeassistant/components/homekit_controller/button.py b/homeassistant/components/homekit_controller/button.py index 7d2c737b509..e9c85dbe876 100644 --- a/homeassistant/components/homekit_controller/button.py +++ b/homeassistant/components/homekit_controller/button.py @@ -106,7 +106,7 @@ class HomeKitButton(CharacteristicEntity, ButtonEntity): @property def name(self) -> str: """Return the name of the device if any.""" - if name := super().name: + if name := self.accessory.name: return f"{name} {self.entity_description.name}" return f"{self.entity_description.name}" diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index 4e0f5cfa077..07d22c27314 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -93,11 +93,11 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity): super().__init__(conn, info, char) @property - def name(self) -> str | None: + def name(self) -> str: """Return the name of the device if any.""" - if prefix := super().name: - return f"{prefix} {self.entity_description.name}" - return self.entity_description.name + if name := self.accessory.name: + return f"{name} {self.entity_description.name}" + return f"{self.entity_description.name}" def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity is tracking.""" diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index b7d7b8005ed..a3a722fea67 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -32,6 +32,7 @@ from homeassistant.helpers.typing import ConfigType from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity from .connection import HKDevice +from .utils import folded_name CO2_ICON = "mdi:molecule-co2" @@ -199,7 +200,24 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { } -class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): +class HomeKitSensor(HomeKitEntity): + """Representation of a HomeKit sensor.""" + + @property + def name(self) -> str | None: + """Return the name of the device.""" + full_name = super().name + default_name = self.default_name + if ( + default_name + and full_name + and folded_name(default_name) not in folded_name(full_name) + ): + return f"{full_name} {default_name}" + return full_name + + +class HomeKitHumiditySensor(HomeKitSensor, SensorEntity): """Representation of a Homekit humidity sensor.""" _attr_device_class = SensorDeviceClass.HUMIDITY @@ -210,9 +228,9 @@ class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): return [CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT] @property - def name(self) -> str: - """Return the name of the device.""" - return f"{super().name} Humidity" + def default_name(self) -> str: + """Return the default name of the device.""" + return "Humidity" @property def native_value(self) -> float: @@ -220,7 +238,7 @@ class HomeKitHumiditySensor(HomeKitEntity, SensorEntity): return self.service.value(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT) -class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): +class HomeKitTemperatureSensor(HomeKitSensor, SensorEntity): """Representation of a Homekit temperature sensor.""" _attr_device_class = SensorDeviceClass.TEMPERATURE @@ -231,9 +249,9 @@ class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): return [CharacteristicsTypes.TEMPERATURE_CURRENT] @property - def name(self) -> str: - """Return the name of the device.""" - return f"{super().name} Temperature" + def default_name(self) -> str: + """Return the default name of the device.""" + return "Temperature" @property def native_value(self) -> float: @@ -241,7 +259,7 @@ class HomeKitTemperatureSensor(HomeKitEntity, SensorEntity): return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) -class HomeKitLightSensor(HomeKitEntity, SensorEntity): +class HomeKitLightSensor(HomeKitSensor, SensorEntity): """Representation of a Homekit light level sensor.""" _attr_device_class = SensorDeviceClass.ILLUMINANCE @@ -252,9 +270,9 @@ class HomeKitLightSensor(HomeKitEntity, SensorEntity): return [CharacteristicsTypes.LIGHT_LEVEL_CURRENT] @property - def name(self) -> str: - """Return the name of the device.""" - return f"{super().name} Light Level" + def default_name(self) -> str: + """Return the default name of the device.""" + return "Light Level" @property def native_value(self) -> int: @@ -273,9 +291,9 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): return [CharacteristicsTypes.CARBON_DIOXIDE_LEVEL] @property - def name(self) -> str: - """Return the name of the device.""" - return f"{super().name} CO2" + def default_name(self) -> str: + """Return the default name of the device.""" + return "CO2" @property def native_value(self) -> int: @@ -283,7 +301,7 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): return self.service.value(CharacteristicsTypes.CARBON_DIOXIDE_LEVEL) -class HomeKitBatterySensor(HomeKitEntity, SensorEntity): +class HomeKitBatterySensor(HomeKitSensor, SensorEntity): """Representation of a Homekit battery sensor.""" _attr_device_class = SensorDeviceClass.BATTERY @@ -298,9 +316,9 @@ class HomeKitBatterySensor(HomeKitEntity, SensorEntity): ] @property - def name(self) -> str: - """Return the name of the device.""" - return f"{super().name} Battery" + def default_name(self) -> str: + """Return the default name of the device.""" + return "Battery" @property def icon(self) -> str: @@ -374,7 +392,9 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): @property def name(self) -> str: """Return the name of the device if any.""" - return f"{super().name} {self.entity_description.name}" + if name := self.accessory.name: + return f"{name} {self.entity_description.name}" + return f"{self.entity_description.name}" @property def native_value(self) -> str | int | float: diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 07d0e21e59f..53b3958ecf6 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -147,11 +147,11 @@ class DeclarativeCharacteristicSwitch(CharacteristicEntity, SwitchEntity): super().__init__(conn, info, char) @property - def name(self) -> str | None: + def name(self) -> str: """Return the name of the device if any.""" - if prefix := super().name: - return f"{prefix} {self.entity_description.name}" - return self.entity_description.name + if name := self.accessory.name: + return f"{name} {self.entity_description.name}" + return f"{self.entity_description.name}" def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" diff --git a/homeassistant/components/homekit_controller/utils.py b/homeassistant/components/homekit_controller/utils.py index 6831c3cee4a..892040d535f 100644 --- a/homeassistant/components/homekit_controller/utils.py +++ b/homeassistant/components/homekit_controller/utils.py @@ -10,6 +10,11 @@ from homeassistant.core import Event, HomeAssistant from .const import CONTROLLER +def folded_name(name: str) -> str: + """Return a name that is used for matching a similar string.""" + return name.casefold().replace(" ", "") + + async def async_get_controller(hass: HomeAssistant) -> Controller: """Get or create an aiohomekit Controller instance.""" if existing := hass.data.get(CONTROLLER): diff --git a/tests/components/homekit_controller/fixtures/mss425f.json b/tests/components/homekit_controller/fixtures/mss425f.json new file mode 100644 index 00000000000..35766b32d22 --- /dev/null +++ b/tests/components/homekit_controller/fixtures/mss425f.json @@ -0,0 +1,212 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "Meross", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "MSS425F", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "MSS425F-15cc", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pr"], + "format": "string", + "value": "HH41234", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": ["pr"], + "format": "string", + "value": "4.2.3", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000053-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "4.0.0", + "description": "Hardware Revision", + "maxLen": 64 + }, + { + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "iid": 9, + "perms": ["pr"], + "format": "string", + "value": "2.0.1;16A75", + "maxLen": 64 + } + ] + }, + { + "iid": 10, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 11, + "perms": ["pr"], + "format": "string", + "value": "1.1.0", + "description": "Version", + "maxLen": 64 + } + ] + }, + { + "iid": 12, + "type": "00000047-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 13, + "perms": ["pr"], + "format": "string", + "value": "Outlet-1", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 14, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + } + ] + }, + { + "iid": 15, + "type": "00000047-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 16, + "perms": ["pr"], + "format": "string", + "value": "Outlet-2", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 17, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + } + ] + }, + { + "iid": 18, + "type": "00000047-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 19, + "perms": ["pr"], + "format": "string", + "value": "Outlet-3", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 20, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + } + ] + }, + { + "iid": 21, + "type": "00000047-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 22, + "perms": ["pr"], + "format": "string", + "value": "Outlet-4", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 23, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + } + ] + }, + { + "iid": 24, + "type": "00000047-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 25, + "perms": ["pr"], + "format": "string", + "value": "USB", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 26, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/fixtures/mss565.json b/tests/components/homekit_controller/fixtures/mss565.json new file mode 100644 index 00000000000..9ecb735dce3 --- /dev/null +++ b/tests/components/homekit_controller/fixtures/mss565.json @@ -0,0 +1,132 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "Meross", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "MSS565", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "MSS565-28da", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pr"], + "format": "string", + "value": "BB1121", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": ["pr"], + "format": "string", + "value": "4.1.9", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000053-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "4.0.0", + "description": "Hardware Revision", + "maxLen": 64 + }, + { + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "iid": 9, + "perms": ["pr"], + "format": "string", + "value": "2.0.1;16A75", + "maxLen": 64 + } + ] + }, + { + "iid": 10, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 11, + "perms": ["pr"], + "format": "string", + "value": "1.1.0", + "description": "Version", + "maxLen": 64 + } + ] + }, + { + "iid": 12, + "type": "00000043-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 13, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + }, + { + "type": "00000008-0000-1000-8000-0026BB765291", + "iid": 14, + "perms": ["pr", "pw", "ev"], + "format": "int", + "value": 67, + "description": "Brightness", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 15, + "perms": ["pr"], + "format": "string", + "value": "Dimmer Switch", + "description": "Name", + "maxLen": 64 + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py index b2428cdc42b..6950f4cb61e 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_gateway.py @@ -39,8 +39,8 @@ async def test_aqara_gateway_setup(hass): devices=[], entities=[ EntityTestInfo( - "alarm_control_panel.aqara_hub_1563", - friendly_name="Aqara Hub-1563", + "alarm_control_panel.aqara_hub_1563_security_system", + friendly_name="Aqara Hub-1563 Security System", unique_id="homekit-0000000123456789-66304", supported_features=SUPPORT_ALARM_ARM_NIGHT | SUPPORT_ALARM_ARM_HOME @@ -48,8 +48,8 @@ async def test_aqara_gateway_setup(hass): state="disarmed", ), EntityTestInfo( - "light.aqara_hub_1563", - friendly_name="Aqara Hub-1563", + "light.aqara_hub_1563_lightbulb_1563", + friendly_name="Aqara Hub-1563 Lightbulb-1563", unique_id="homekit-0000000123456789-65792", supported_features=0, capabilities={"supported_color_modes": ["hs"]}, @@ -98,8 +98,8 @@ async def test_aqara_gateway_e1_setup(hass): devices=[], entities=[ EntityTestInfo( - "alarm_control_panel.aqara_hub_e1_00a0", - friendly_name="Aqara-Hub-E1-00A0", + "alarm_control_panel.aqara_hub_e1_00a0_security_system", + friendly_name="Aqara-Hub-E1-00A0 Security System", unique_id="homekit-00aa00000a0-16", supported_features=SUPPORT_ALARM_ARM_NIGHT | SUPPORT_ALARM_ARM_HOME diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py index e6dce42a1f7..16bef749429 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py @@ -38,8 +38,8 @@ async def test_aqara_switch_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="sensor.programmable_switch_battery", - friendly_name="Programmable Switch Battery", + entity_id="sensor.programmable_switch_battery_sensor", + friendly_name="Programmable Switch Battery Sensor", unique_id="homekit-111a1111a1a111-5", unit_of_measurement=PERCENTAGE, state="100", diff --git a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py index 9afe152c7b3..2cb312fc7f5 100644 --- a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py +++ b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py @@ -37,9 +37,9 @@ async def test_arlo_baby_setup(hass): state="idle", ), EntityTestInfo( - entity_id="binary_sensor.arlobabya0", + entity_id="binary_sensor.arlobabya0_motion", unique_id="homekit-00A0000000000-500", - friendly_name="ArloBabyA0", + friendly_name="ArloBabyA0 Motion", state="off", ), EntityTestInfo( @@ -71,9 +71,9 @@ async def test_arlo_baby_setup(hass): state="1", ), EntityTestInfo( - entity_id="light.arlobabya0", + entity_id="light.arlobabya0_nightlight", unique_id="homekit-00A0000000000-1100", - friendly_name="ArloBabyA0", + friendly_name="ArloBabyA0 Nightlight", supported_features=0, capabilities={"supported_color_modes": ["hs"]}, state="off", diff --git a/tests/components/homekit_controller/specific_devices/test_connectsense.py b/tests/components/homekit_controller/specific_devices/test_connectsense.py index 2cbdf924319..fbb95fc3d89 100644 --- a/tests/components/homekit_controller/specific_devices/test_connectsense.py +++ b/tests/components/homekit_controller/specific_devices/test_connectsense.py @@ -59,8 +59,8 @@ async def test_connectsense_setup(hass): state="379.69299", ), EntityTestInfo( - entity_id="switch.inwall_outlet_0394de", - friendly_name="InWall Outlet-0394DE", + entity_id="switch.inwall_outlet_0394de_outlet_a", + friendly_name="InWall Outlet-0394DE Outlet A", unique_id="homekit-1020301376-13", state="on", ), @@ -89,8 +89,8 @@ async def test_connectsense_setup(hass): state="175.85001", ), EntityTestInfo( - entity_id="switch.inwall_outlet_0394de_2", - friendly_name="InWall Outlet-0394DE", + entity_id="switch.inwall_outlet_0394de_outlet_b", + friendly_name="InWall Outlet-0394DE Outlet B", unique_id="homekit-1020301376-25", state="on", ), diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py index 88d483bd5bc..74525af1daf 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_ls1.py @@ -43,8 +43,8 @@ async def test_koogeek_ls1_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="light.koogeek_ls1_20833f", - friendly_name="Koogeek-LS1-20833F", + entity_id="light.koogeek_ls1_20833f_light_strip", + friendly_name="Koogeek-LS1-20833F Light Strip", unique_id="homekit-AAAA011111111111-7", supported_features=0, capabilities={"supported_color_modes": ["hs"]}, @@ -75,7 +75,11 @@ async def test_recover_from_failure(hass, utcnow, failure_cls): pairing.testing.events_enabled = False helper = Helper( - hass, "light.koogeek_ls1_20833f", pairing, accessories[0], config_entry + hass, + "light.koogeek_ls1_20833f_light_strip", + pairing, + accessories[0], + config_entry, ) # Set light state on fake device to off diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py index f93adc732ba..bf8c86b7a7d 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py @@ -31,8 +31,8 @@ async def test_koogeek_p1eu_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="switch.koogeek_p1_a00aa0", - friendly_name="Koogeek-P1-A00AA0", + entity_id="switch.koogeek_p1_a00aa0_outlet", + friendly_name="Koogeek-P1-A00AA0 outlet", unique_id="homekit-EUCP03190xxxxx48-7", state="off", ), diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py index ed940cb6376..8307dc72f22 100644 --- a/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_sw2.py @@ -37,11 +37,17 @@ async def test_koogeek_sw2_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="switch.koogeek_sw2_187a91", - friendly_name="Koogeek-SW2-187A91", + entity_id="switch.koogeek_sw2_187a91_switch_1", + friendly_name="Koogeek-SW2-187A91 Switch 1", unique_id="homekit-CNNT061751001372-8", state="off", ), + EntityTestInfo( + entity_id="switch.koogeek_sw2_187a91_switch_2", + friendly_name="Koogeek-SW2-187A91 Switch 2", + unique_id="homekit-CNNT061751001372-11", + state="off", + ), EntityTestInfo( entity_id="sensor.koogeek_sw2_187a91_power", friendly_name="Koogeek-SW2-187A91 Power", diff --git a/tests/components/homekit_controller/specific_devices/test_mss425f.py b/tests/components/homekit_controller/specific_devices/test_mss425f.py new file mode 100644 index 00000000000..3fe0ee739e6 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_mss425f.py @@ -0,0 +1,73 @@ +"""Tests for the Meross MSS425f power strip.""" + + +from homeassistant.const import STATE_ON, STATE_UNKNOWN +from homeassistant.helpers.entity import EntityCategory + +from tests.components.homekit_controller.common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_meross_mss425f_setup(hass): + """Test that a MSS425f can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "mss425f.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="MSS425F-15cc", + model="MSS425F", + manufacturer="Meross", + sw_version="4.2.3", + hw_version="4.0.0", + serial_number="HH41234", + devices=[], + entities=[ + EntityTestInfo( + entity_id="button.mss425f_15cc_identify", + friendly_name="MSS425F-15cc Identify", + unique_id="homekit-HH41234-aid:1-sid:1-cid:2", + entity_category=EntityCategory.DIAGNOSTIC, + state=STATE_UNKNOWN, + ), + EntityTestInfo( + entity_id="switch.mss425f_15cc_outlet_1", + friendly_name="MSS425F-15cc Outlet-1", + unique_id="homekit-HH41234-12", + state=STATE_ON, + ), + EntityTestInfo( + entity_id="switch.mss425f_15cc_outlet_2", + friendly_name="MSS425F-15cc Outlet-2", + unique_id="homekit-HH41234-15", + state=STATE_ON, + ), + EntityTestInfo( + entity_id="switch.mss425f_15cc_outlet_3", + friendly_name="MSS425F-15cc Outlet-3", + unique_id="homekit-HH41234-18", + state=STATE_ON, + ), + EntityTestInfo( + entity_id="switch.mss425f_15cc_outlet_4", + friendly_name="MSS425F-15cc Outlet-4", + unique_id="homekit-HH41234-21", + state=STATE_ON, + ), + EntityTestInfo( + entity_id="switch.mss425f_15cc_usb", + friendly_name="MSS425F-15cc USB", + unique_id="homekit-HH41234-24", + state=STATE_ON, + ), + ], + ), + ) diff --git a/tests/components/homekit_controller/specific_devices/test_mss565.py b/tests/components/homekit_controller/specific_devices/test_mss565.py new file mode 100644 index 00000000000..0045a5ec507 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_mss565.py @@ -0,0 +1,42 @@ +"""Tests for the Meross MSS565 wall switch.""" + + +from homeassistant.const import STATE_ON + +from tests.components.homekit_controller.common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_meross_mss565_setup(hass): + """Test that a MSS565 can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "mss565.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="MSS565-28da", + model="MSS565", + manufacturer="Meross", + sw_version="4.1.9", + hw_version="4.0.0", + serial_number="BB1121", + devices=[], + entities=[ + EntityTestInfo( + entity_id="light.mss565_28da_dimmer_switch", + friendly_name="MSS565-28da Dimmer Switch", + unique_id="homekit-BB1121-12", + capabilities={"supported_color_modes": ["brightness"]}, + state=STATE_ON, + ), + ], + ), + ) diff --git a/tests/components/homekit_controller/specific_devices/test_mysa_living.py b/tests/components/homekit_controller/specific_devices/test_mysa_living.py index 5829bd4e165..1a3bcdb2271 100644 --- a/tests/components/homekit_controller/specific_devices/test_mysa_living.py +++ b/tests/components/homekit_controller/specific_devices/test_mysa_living.py @@ -32,8 +32,8 @@ async def test_mysa_living_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="climate.mysa_85dda9", - friendly_name="Mysa-85dda9", + entity_id="climate.mysa_85dda9_thermostat", + friendly_name="Mysa-85dda9 Thermostat", unique_id="homekit-AAAAAAA000-20", supported_features=SUPPORT_TARGET_TEMPERATURE, capabilities={ @@ -60,8 +60,8 @@ async def test_mysa_living_setup(hass): state="24.1", ), EntityTestInfo( - entity_id="light.mysa_85dda9", - friendly_name="Mysa-85dda9", + entity_id="light.mysa_85dda9_display", + friendly_name="Mysa-85dda9 Display", unique_id="homekit-AAAAAAA000-40", supported_features=0, capabilities={"supported_color_modes": ["brightness"]}, diff --git a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py index 51ebbfdc345..eb9c36d0b79 100644 --- a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py @@ -45,15 +45,15 @@ async def test_ryse_smart_bridge_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.master_bath_south", - friendly_name="Master Bath South", + entity_id="cover.master_bath_south_ryse_shade", + friendly_name="Master Bath South RYSE Shade", unique_id="homekit-00:00:00:00:00:00-2-48", supported_features=RYSE_SUPPORTED_FEATURES, state="closed", ), EntityTestInfo( - entity_id="sensor.master_bath_south_battery", - friendly_name="Master Bath South Battery", + entity_id="sensor.master_bath_south_ryse_shade_battery", + friendly_name="Master Bath South RYSE Shade Battery", unique_id="homekit-00:00:00:00:00:00-2-64", unit_of_measurement=PERCENTAGE, state="100", @@ -71,15 +71,15 @@ async def test_ryse_smart_bridge_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.ryse_smartshade", - friendly_name="RYSE SmartShade", + entity_id="cover.ryse_smartshade_ryse_shade", + friendly_name="RYSE SmartShade RYSE Shade", unique_id="homekit-00:00:00:00:00:00-3-48", supported_features=RYSE_SUPPORTED_FEATURES, state="open", ), EntityTestInfo( - entity_id="sensor.ryse_smartshade_battery", - friendly_name="RYSE SmartShade Battery", + entity_id="sensor.ryse_smartshade_ryse_shade_battery", + friendly_name="RYSE SmartShade RYSE Shade Battery", unique_id="homekit-00:00:00:00:00:00-3-64", unit_of_measurement=PERCENTAGE, state="100", @@ -120,15 +120,15 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.lr_left", - friendly_name="LR Left", + entity_id="cover.lr_left_ryse_shade", + friendly_name="LR Left RYSE Shade", unique_id="homekit-00:00:00:00:00:00-2-48", supported_features=RYSE_SUPPORTED_FEATURES, state="closed", ), EntityTestInfo( - entity_id="sensor.lr_left_battery", - friendly_name="LR Left Battery", + entity_id="sensor.lr_left_ryse_shade_battery", + friendly_name="LR Left RYSE Shade Battery", unique_id="homekit-00:00:00:00:00:00-2-64", unit_of_measurement=PERCENTAGE, state="89", @@ -146,15 +146,15 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.lr_right", - friendly_name="LR Right", + entity_id="cover.lr_right_ryse_shade", + friendly_name="LR Right RYSE Shade", unique_id="homekit-00:00:00:00:00:00-3-48", supported_features=RYSE_SUPPORTED_FEATURES, state="closed", ), EntityTestInfo( - entity_id="sensor.lr_right_battery", - friendly_name="LR Right Battery", + entity_id="sensor.lr_right_ryse_shade_battery", + friendly_name="LR Right RYSE Shade Battery", unique_id="homekit-00:00:00:00:00:00-3-64", unit_of_measurement=PERCENTAGE, state="100", @@ -172,15 +172,15 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.br_left", - friendly_name="BR Left", + entity_id="cover.br_left_ryse_shade", + friendly_name="BR Left RYSE Shade", unique_id="homekit-00:00:00:00:00:00-4-48", supported_features=RYSE_SUPPORTED_FEATURES, state="open", ), EntityTestInfo( - entity_id="sensor.br_left_battery", - friendly_name="BR Left Battery", + entity_id="sensor.br_left_ryse_shade_battery", + friendly_name="BR Left RYSE Shade Battery", unique_id="homekit-00:00:00:00:00:00-4-64", unit_of_measurement=PERCENTAGE, state="100", @@ -198,15 +198,15 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.rzss", - friendly_name="RZSS", + entity_id="cover.rzss_ryse_shade", + friendly_name="RZSS RYSE Shade", unique_id="homekit-00:00:00:00:00:00-5-48", supported_features=RYSE_SUPPORTED_FEATURES, state="open", ), EntityTestInfo( - entity_id="sensor.rzss_battery", - friendly_name="RZSS Battery", + entity_id="sensor.rzss_ryse_shade_battery", + friendly_name="RZSS RYSE Shade Battery", unique_id="homekit-00:00:00:00:00:00-5-64", unit_of_measurement=PERCENTAGE, state="0", diff --git a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py index d3531e1c65f..f160169a43c 100644 --- a/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py +++ b/tests/components/homekit_controller/specific_devices/test_simpleconnect_fan.py @@ -34,8 +34,8 @@ async def test_simpleconnect_fan_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="fan.simpleconnect_fan_06f674", - friendly_name="SIMPLEconnect Fan-06F674", + entity_id="fan.simpleconnect_fan_06f674_hunter_fan", + friendly_name="SIMPLEconnect Fan-06F674 Hunter Fan", unique_id="homekit-1234567890abcd-8", supported_features=SUPPORT_DIRECTION | SUPPORT_SET_SPEED, capabilities={ diff --git a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py index d8d73709c49..21aa91fc933 100644 --- a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py @@ -52,8 +52,8 @@ async def test_velux_cover_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="cover.velux_window", - friendly_name="VELUX Window", + entity_id="cover.velux_window_roof_window", + friendly_name="VELUX Window Roof Window", unique_id="homekit-1111111a114a111a-8", supported_features=SUPPORT_CLOSE | SUPPORT_SET_POSITION @@ -73,22 +73,22 @@ async def test_velux_cover_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="sensor.velux_sensor_temperature", - friendly_name="VELUX Sensor Temperature", + entity_id="sensor.velux_sensor_temperature_sensor", + friendly_name="VELUX Sensor Temperature sensor", unique_id="homekit-a11b111-8", unit_of_measurement=TEMP_CELSIUS, state="18.9", ), EntityTestInfo( - entity_id="sensor.velux_sensor_humidity", - friendly_name="VELUX Sensor Humidity", + entity_id="sensor.velux_sensor_humidity_sensor", + friendly_name="VELUX Sensor Humidity sensor", unique_id="homekit-a11b111-11", unit_of_measurement=PERCENTAGE, state="58", ), EntityTestInfo( - entity_id="sensor.velux_sensor_co2", - friendly_name="VELUX Sensor CO2", + entity_id="sensor.velux_sensor_carbon_dioxide_sensor", + friendly_name="VELUX Sensor Carbon Dioxide sensor", unique_id="homekit-a11b111-14", unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state="400", diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py index 6fa9ff63690..f788b016ba2 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_flowerbud.py @@ -46,8 +46,8 @@ async def test_vocolinc_flowerbud_setup(hass): state="off", ), EntityTestInfo( - entity_id="light.vocolinc_flowerbud_0d324b", - friendly_name="VOCOlinc-Flowerbud-0d324b", + entity_id="light.vocolinc_flowerbud_0d324b_mood_light", + friendly_name="VOCOlinc-Flowerbud-0d324b Mood Light", unique_id="homekit-AM01121849000327-9", supported_features=0, capabilities={"supported_color_modes": ["hs"]}, diff --git a/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py b/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py index da69b7fe309..3a3579b8781 100644 --- a/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py +++ b/tests/components/homekit_controller/specific_devices/test_vocolinc_vp3.py @@ -31,8 +31,8 @@ async def test_vocolinc_vp3_setup(hass): devices=[], entities=[ EntityTestInfo( - entity_id="switch.vocolinc_vp3_123456", - friendly_name="VOCOlinc-VP3-123456", + entity_id="switch.vocolinc_vp3_123456_outlet", + friendly_name="VOCOlinc-VP3-123456 Outlet", unique_id="homekit-EU0121203xxxxx07-48", state="on", ), diff --git a/tests/components/homekit_controller/test_diagnostics.py b/tests/components/homekit_controller/test_diagnostics.py index 72ef571e214..e770279752e 100644 --- a/tests/components/homekit_controller/test_diagnostics.py +++ b/tests/components/homekit_controller/test_diagnostics.py @@ -234,28 +234,6 @@ async def test_config_entry(hass: HomeAssistant, hass_client: ClientSession, utc "sw_version": "2.2.15", "hw_version": "", "entities": [ - { - "original_name": "Koogeek-LS1-20833F", - "disabled": False, - "disabled_by": None, - "entity_category": None, - "device_class": None, - "original_device_class": None, - "icon": None, - "original_icon": None, - "unit_of_measurement": None, - "state": { - "entity_id": "light.koogeek_ls1_20833f", - "state": "off", - "attributes": { - "supported_color_modes": ["hs"], - "friendly_name": "Koogeek-LS1-20833F", - "supported_features": 0, - }, - "last_changed": "2023-01-01T00:00:00+00:00", - "last_updated": "2023-01-01T00:00:00+00:00", - }, - }, { "device_class": None, "disabled": False, @@ -276,6 +254,28 @@ async def test_config_entry(hass: HomeAssistant, hass_client: ClientSession, utc }, "unit_of_measurement": None, }, + { + "device_class": None, + "disabled": False, + "disabled_by": None, + "entity_category": None, + "icon": None, + "original_device_class": None, + "original_icon": None, + "original_name": "Koogeek-LS1-20833F Light Strip", + "state": { + "attributes": { + "friendly_name": "Koogeek-LS1-20833F Light Strip", + "supported_color_modes": ["hs"], + "supported_features": 0, + }, + "entity_id": "light.koogeek_ls1_20833f_light_strip", + "last_changed": "2023-01-01T00:00:00+00:00", + "last_updated": "2023-01-01T00:00:00+00:00", + "state": "off", + }, + "unit_of_measurement": None, + }, ], } ], @@ -504,28 +504,6 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow): "sw_version": "2.2.15", "hw_version": "", "entities": [ - { - "original_name": "Koogeek-LS1-20833F", - "disabled": False, - "disabled_by": None, - "entity_category": None, - "device_class": None, - "original_device_class": None, - "icon": None, - "original_icon": None, - "unit_of_measurement": None, - "state": { - "entity_id": "light.koogeek_ls1_20833f", - "state": "off", - "attributes": { - "supported_color_modes": ["hs"], - "friendly_name": "Koogeek-LS1-20833F", - "supported_features": 0, - }, - "last_changed": "2023-01-01T00:00:00+00:00", - "last_updated": "2023-01-01T00:00:00+00:00", - }, - }, { "device_class": None, "disabled": False, @@ -536,7 +514,9 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow): "original_icon": None, "original_name": "Koogeek-LS1-20833F Identify", "state": { - "attributes": {"friendly_name": "Koogeek-LS1-20833F Identify"}, + "attributes": { + "friendly_name": "Koogeek-LS1-20833F " "Identify" + }, "entity_id": "button.koogeek_ls1_20833f_identify", "last_changed": "2023-01-01T00:00:00+00:00", "last_updated": "2023-01-01T00:00:00+00:00", @@ -544,6 +524,28 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow): }, "unit_of_measurement": None, }, + { + "device_class": None, + "disabled": False, + "disabled_by": None, + "entity_category": None, + "icon": None, + "original_device_class": None, + "original_icon": None, + "original_name": "Koogeek-LS1-20833F Light Strip", + "state": { + "attributes": { + "friendly_name": "Koogeek-LS1-20833F Light Strip", + "supported_color_modes": ["hs"], + "supported_features": 0, + }, + "entity_id": "light.koogeek_ls1_20833f_light_strip", + "last_changed": "2023-01-01T00:00:00+00:00", + "last_updated": "2023-01-01T00:00:00+00:00", + "state": "off", + }, + "unit_of_measurement": None, + }, ], }, } diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 820b89e587d..84a6c8b86bf 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -16,8 +16,8 @@ from .common import Helper, remove_device from tests.components.homekit_controller.common import setup_test_component -ALIVE_DEVICE_NAME = "Light Bulb" -ALIVE_DEEVICE_ENTITY_ID = "light.testdevice" +ALIVE_DEVICE_NAME = "testdevice" +ALIVE_DEVICE_ENTITY_ID = "light.testdevice" def create_motion_sensor_service(accessory): @@ -72,7 +72,7 @@ async def test_device_remove_devices(hass, hass_ws_client): entry_id = config_entry.entry_id registry: EntityRegistry = er.async_get(hass) - entity = registry.entities[ALIVE_DEEVICE_ENTITY_ID] + entity = registry.entities[ALIVE_DEVICE_ENTITY_ID] device_registry = dr.async_get(hass) live_device_entry = device_registry.async_get(entity.device_id) diff --git a/tests/components/homekit_controller/test_light.py b/tests/components/homekit_controller/test_light.py index 39e44c57cc5..83bddf3d26a 100644 --- a/tests/components/homekit_controller/test_light.py +++ b/tests/components/homekit_controller/test_light.py @@ -12,7 +12,7 @@ from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_UNAVAILABLE from tests.components.homekit_controller.common import setup_test_component -LIGHT_BULB_NAME = "Light Bulb" +LIGHT_BULB_NAME = "TestDevice" LIGHT_BULB_ENTITY_ID = "light.testdevice" From 40ed44cbea2018270c1a2a97b2b3a32d445701aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Jul 2022 15:48:34 -0500 Subject: [PATCH 2098/3516] Fix esphome state mapping (#74337) --- homeassistant/components/esphome/__init__.py | 17 +++--- .../components/esphome/entry_data.py | 53 ++++--------------- 2 files changed, 22 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 0c1eac3aa45..ddedaf11ceb 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -558,7 +558,7 @@ async def platform_async_setup_entry( entry_data: RuntimeEntryData = DomainData.get(hass).get_entry_data(entry) entry_data.info[component_key] = {} entry_data.old_info[component_key] = {} - entry_data.state[component_key] = {} + entry_data.state.setdefault(state_type, {}) @callback def async_list_entities(infos: list[EntityInfo]) -> None: @@ -578,7 +578,7 @@ async def platform_async_setup_entry( old_infos.pop(info.key) else: # Create new entity - entity = entity_type(entry_data, component_key, info.key) + entity = entity_type(entry_data, component_key, info.key, state_type) add_entities.append(entity) new_infos[info.key] = info @@ -677,12 +677,17 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): """Define a base esphome entity.""" def __init__( - self, entry_data: RuntimeEntryData, component_key: str, key: int + self, + entry_data: RuntimeEntryData, + component_key: str, + key: int, + state_type: type[_StateT], ) -> None: """Initialize.""" self._entry_data = entry_data self._component_key = component_key self._key = key + self._state_type = state_type async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -707,7 +712,7 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): self.async_on_remove( self._entry_data.async_subscribe_state_update( - self._component_key, self._key, self._on_state_update + self._state_type, self._key, self._on_state_update ) ) @@ -755,11 +760,11 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): @property def _state(self) -> _StateT: - return cast(_StateT, self._entry_data.state[self._component_key][self._key]) + return cast(_StateT, self._entry_data.state[self._state_type][self._key]) @property def _has_state(self) -> bool: - return self._key in self._entry_data.state[self._component_key] + return self._key in self._entry_data.state[self._state_type] @property def available(self) -> bool: diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 8eb56e6fdb6..41a0e89245e 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -12,34 +12,21 @@ from aioesphomeapi import ( APIClient, APIVersion, BinarySensorInfo, - BinarySensorState, CameraInfo, - CameraState, ClimateInfo, - ClimateState, CoverInfo, - CoverState, DeviceInfo, EntityInfo, EntityState, FanInfo, - FanState, LightInfo, - LightState, LockInfo, - LockState, MediaPlayerInfo, - MediaPlayerState, NumberInfo, - NumberState, SelectInfo, - SelectState, SensorInfo, - SensorState, SwitchInfo, - SwitchState, TextSensorInfo, - TextSensorState, UserService, ) from aioesphomeapi.model import ButtonInfo @@ -56,8 +43,8 @@ _LOGGER = logging.getLogger(__name__) # Mapping from ESPHome info type to HA platform INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { BinarySensorInfo: Platform.BINARY_SENSOR, - ButtonInfo: Platform.BINARY_SENSOR, - CameraInfo: Platform.BINARY_SENSOR, + ButtonInfo: Platform.BUTTON, + CameraInfo: Platform.CAMERA, ClimateInfo: Platform.CLIMATE, CoverInfo: Platform.COVER, FanInfo: Platform.FAN, @@ -71,23 +58,6 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { TextSensorInfo: Platform.SENSOR, } -STATE_TYPE_TO_COMPONENT_KEY = { - BinarySensorState: Platform.BINARY_SENSOR, - EntityState: Platform.BINARY_SENSOR, - CameraState: Platform.BINARY_SENSOR, - ClimateState: Platform.CLIMATE, - CoverState: Platform.COVER, - FanState: Platform.FAN, - LightState: Platform.LIGHT, - LockState: Platform.LOCK, - MediaPlayerState: Platform.MEDIA_PLAYER, - NumberState: Platform.NUMBER, - SelectState: Platform.SELECT, - SensorState: Platform.SENSOR, - SwitchState: Platform.SWITCH, - TextSensorState: Platform.SENSOR, -} - @dataclass class RuntimeEntryData: @@ -96,7 +66,7 @@ class RuntimeEntryData: entry_id: str client: APIClient store: Store - state: dict[str, dict[int, EntityState]] = field(default_factory=dict) + state: dict[type[EntityState], dict[int, EntityState]] = field(default_factory=dict) info: dict[str, dict[int, EntityInfo]] = field(default_factory=dict) # A second list of EntityInfo objects @@ -111,9 +81,9 @@ class RuntimeEntryData: api_version: APIVersion = field(default_factory=APIVersion) cleanup_callbacks: list[Callable[[], None]] = field(default_factory=list) disconnect_callbacks: list[Callable[[], None]] = field(default_factory=list) - state_subscriptions: dict[tuple[str, int], Callable[[], None]] = field( - default_factory=dict - ) + state_subscriptions: dict[ + tuple[type[EntityState], int], Callable[[], None] + ] = field(default_factory=dict) loaded_platforms: set[str] = field(default_factory=set) platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock) _storage_contents: dict[str, Any] | None = None @@ -160,24 +130,23 @@ class RuntimeEntryData: @callback def async_subscribe_state_update( self, - component_key: str, + state_type: type[EntityState], state_key: int, entity_callback: Callable[[], None], ) -> Callable[[], None]: """Subscribe to state updates.""" def _unsubscribe() -> None: - self.state_subscriptions.pop((component_key, state_key)) + self.state_subscriptions.pop((state_type, state_key)) - self.state_subscriptions[(component_key, state_key)] = entity_callback + self.state_subscriptions[(state_type, state_key)] = entity_callback return _unsubscribe @callback def async_update_state(self, state: EntityState) -> None: """Distribute an update of state information to the target.""" - component_key = STATE_TYPE_TO_COMPONENT_KEY[type(state)] - subscription_key = (component_key, state.key) - self.state[component_key][state.key] = state + subscription_key = (type(state), state.key) + self.state[type(state)][state.key] = state _LOGGER.debug( "Dispatching update with key %s: %s", subscription_key, From 5bd9c5aee8e748d0d27e73c80e71cf3b62cf07ff Mon Sep 17 00:00:00 2001 From: mbo18 Date: Sun, 3 Jul 2022 22:49:03 +0200 Subject: [PATCH 2099/3516] Migrate Meteo_france to native_* (#74297) --- .../components/meteo_france/weather.py | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index cca1f6fe684..a30a65304b0 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -4,16 +4,22 @@ import time from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MODE, TEMP_CELSIUS +from homeassistant.const import ( + CONF_MODE, + LENGTH_MILLIMETERS, + PRESSURE_HPA, + SPEED_METERS_PER_SECOND, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -71,6 +77,11 @@ async def async_setup_entry( class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): """Representation of a weather condition.""" + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + def __init__(self, coordinator: DataUpdateCoordinator, mode: str) -> None: """Initialise the platform with a data instance and station name.""" super().__init__(coordinator) @@ -107,17 +118,12 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): ) @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data.current_forecast["T"]["value"] @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" return self.coordinator.data.current_forecast["sea_level"] @@ -127,10 +133,9 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): return self.coordinator.data.current_forecast["humidity"] @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" - # convert from API m/s to km/h - return round(self.coordinator.data.current_forecast["wind"]["speed"] * 3.6) + return self.coordinator.data.current_forecast["wind"]["speed"] @property def wind_bearing(self): @@ -158,9 +163,9 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): ATTR_FORECAST_CONDITION: format_condition( forecast["weather"]["desc"] ), - ATTR_FORECAST_TEMP: forecast["T"]["value"], - ATTR_FORECAST_PRECIPITATION: forecast["rain"].get("1h"), - ATTR_FORECAST_WIND_SPEED: forecast["wind"]["speed"], + ATTR_FORECAST_NATIVE_TEMP: forecast["T"]["value"], + ATTR_FORECAST_NATIVE_PRECIPITATION: forecast["rain"].get("1h"), + ATTR_FORECAST_NATIVE_WIND_SPEED: forecast["wind"]["speed"], ATTR_FORECAST_WIND_BEARING: forecast["wind"]["direction"] if forecast["wind"]["direction"] != -1 else None, @@ -179,9 +184,11 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): ATTR_FORECAST_CONDITION: format_condition( forecast["weather12H"]["desc"] ), - ATTR_FORECAST_TEMP: forecast["T"]["max"], - ATTR_FORECAST_TEMP_LOW: forecast["T"]["min"], - ATTR_FORECAST_PRECIPITATION: forecast["precipitation"]["24h"], + ATTR_FORECAST_NATIVE_TEMP: forecast["T"]["max"], + ATTR_FORECAST_NATIVE_TEMP_LOW: forecast["T"]["min"], + ATTR_FORECAST_NATIVE_PRECIPITATION: forecast["precipitation"][ + "24h" + ], } ) return forecast_data From d91f5b77c876de156fedd00cf02e109ee63ec3b9 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 3 Jul 2022 22:53:44 +0200 Subject: [PATCH 2100/3516] Fix unique id issue for OpenWeatherMap (#74335) --- .../components/openweathermap/const.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index f180f2a9bbf..06f13daa9c2 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -22,10 +22,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_NATIVE_PRESSURE, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TIME, ) @@ -72,6 +68,11 @@ ATTR_API_FORECAST = "forecast" UPDATE_LISTENER = "update_listener" PLATFORMS = [Platform.SENSOR, Platform.WEATHER] +ATTR_FORECAST_PRECIPITATION = "precipitation" +ATTR_FORECAST_PRESSURE = "pressure" +ATTR_FORECAST_TEMP = "temperature" +ATTR_FORECAST_TEMP_LOW = "templow" + FORECAST_MODE_HOURLY = "hourly" FORECAST_MODE_DAILY = "daily" FORECAST_MODE_FREE_DAILY = "freedaily" @@ -266,7 +267,7 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_PRECIPITATION, + key=ATTR_FORECAST_PRECIPITATION, name="Precipitation", native_unit_of_measurement=LENGTH_MILLIMETERS, ), @@ -276,19 +277,19 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_PRESSURE, + key=ATTR_FORECAST_PRESSURE, name="Pressure", native_unit_of_measurement=PRESSURE_HPA, device_class=SensorDeviceClass.PRESSURE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_TEMP, + key=ATTR_FORECAST_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_TEMP_LOW, + key=ATTR_FORECAST_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, From 4e793a51ba1da105846544bb3b73400cc6796be8 Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Sun, 3 Jul 2022 21:26:00 +0100 Subject: [PATCH 2101/3516] Dont substitute user/pass for relative stream urls on generic camera (#74201) Co-authored-by: Dave T --- homeassistant/components/generic/camera.py | 8 +++- .../components/generic/config_flow.py | 13 +++++- homeassistant/components/generic/strings.json | 4 ++ .../components/generic/translations/en.json | 11 ++--- tests/components/generic/test_config_flow.py | 46 +++++++++++++++++-- 5 files changed, 68 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 8b03f0a8ed3..961d3cecfb7 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -228,7 +228,13 @@ class GenericCamera(Camera): try: stream_url = self._stream_source.async_render(parse_result=False) url = yarl.URL(stream_url) - if not url.user and not url.password and self._username and self._password: + if ( + not url.user + and not url.password + and self._username + and self._password + and url.is_absolute() + ): url = url.with_user(self._username).with_password(self._password) return str(url) except TemplateError as err: diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 9096f2ce87e..514264f919e 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -150,6 +150,12 @@ async def async_test_still( except TemplateError as err: _LOGGER.warning("Problem rendering template %s: %s", url, err) return {CONF_STILL_IMAGE_URL: "template_error"}, None + try: + yarl_url = yarl.URL(url) + except ValueError: + return {CONF_STILL_IMAGE_URL: "malformed_url"}, None + if not yarl_url.is_absolute(): + return {CONF_STILL_IMAGE_URL: "relative_url"}, None verify_ssl = info[CONF_VERIFY_SSL] auth = generate_auth(info) try: @@ -222,7 +228,12 @@ async def async_test_stream( if info.get(CONF_USE_WALLCLOCK_AS_TIMESTAMPS): stream_options[CONF_USE_WALLCLOCK_AS_TIMESTAMPS] = True - url = yarl.URL(stream_source) + try: + url = yarl.URL(stream_source) + except ValueError: + return {CONF_STREAM_SOURCE: "malformed_url"} + if not url.is_absolute(): + return {CONF_STREAM_SOURCE: "relative_url"} if not url.user and not url.password: username = info.get(CONF_USERNAME) password = info.get(CONF_PASSWORD) diff --git a/homeassistant/components/generic/strings.json b/homeassistant/components/generic/strings.json index 7d3cab19aa5..608c85c1379 100644 --- a/homeassistant/components/generic/strings.json +++ b/homeassistant/components/generic/strings.json @@ -6,6 +6,8 @@ "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", "invalid_still_image": "URL did not return a valid still image", + "malformed_url": "Malformed URL", + "relative_url": "Relative URLs are not allowed", "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", @@ -75,6 +77,8 @@ "unable_still_load": "[%key:component::generic::config::error::unable_still_load%]", "no_still_image_or_stream_url": "[%key:component::generic::config::error::no_still_image_or_stream_url%]", "invalid_still_image": "[%key:component::generic::config::error::invalid_still_image%]", + "malformed_url": "[%key:component::generic::config::error::malformed_url%]", + "relative_url": "[%key:component::generic::config::error::relative_url%]", "template_error": "[%key:component::generic::config::error::template_error%]", "timeout": "[%key:component::generic::config::error::timeout%]", "stream_no_route_to_host": "[%key:component::generic::config::error::stream_no_route_to_host%]", diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json index d01e6e59a4b..cb2200f9755 100644 --- a/homeassistant/components/generic/translations/en.json +++ b/homeassistant/components/generic/translations/en.json @@ -1,20 +1,19 @@ { "config": { "abort": { - "no_devices_found": "No devices found on the network", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { "already_exists": "A camera with these URL settings already exists.", "invalid_still_image": "URL did not return a valid still image", + "malformed_url": "Malformed URL", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", + "relative_url": "Relative URLs are not allowed", "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", - "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", - "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", @@ -50,14 +49,12 @@ "error": { "already_exists": "A camera with these URL settings already exists.", "invalid_still_image": "URL did not return a valid still image", + "malformed_url": "Malformed URL", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", - "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", - "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", + "relative_url": "Relative URLs are not allowed", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", - "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", - "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index f0589301014..592d139f92e 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -180,33 +180,43 @@ async def test_form_only_still_sample(hass, user_flow, image_file): @respx.mock @pytest.mark.parametrize( - ("template", "url", "expected_result"), + ("template", "url", "expected_result", "expected_errors"), [ # Test we can handle templates in strange parts of the url, #70961. ( "http://localhost:812{{3}}/static/icons/favicon-apple-180x180.png", "http://localhost:8123/static/icons/favicon-apple-180x180.png", data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + None, ), ( "{% if 1 %}https://bla{% else %}https://yo{% endif %}", "https://bla/", data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + None, ), ( "http://{{example.org", "http://example.org", data_entry_flow.RESULT_TYPE_FORM, + {"still_image_url": "template_error"}, ), ( "invalid1://invalid:4\\1", "invalid1://invalid:4%5c1", - data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + data_entry_flow.RESULT_TYPE_FORM, + {"still_image_url": "malformed_url"}, + ), + ( + "relative/urls/are/not/allowed.jpg", + "relative/urls/are/not/allowed.jpg", + data_entry_flow.RESULT_TYPE_FORM, + {"still_image_url": "relative_url"}, ), ], ) async def test_still_template( - hass, user_flow, fakeimgbytes_png, template, url, expected_result + hass, user_flow, fakeimgbytes_png, template, url, expected_result, expected_errors ) -> None: """Test we can handle various templates.""" respx.get(url).respond(stream=fakeimgbytes_png) @@ -220,6 +230,7 @@ async def test_still_template( ) await hass.async_block_till_done() assert result2["type"] == expected_result + assert result2.get("errors") == expected_errors @respx.mock @@ -514,8 +525,29 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream result4["flow_id"], user_input=data, ) - assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM - assert result5["errors"] == {"stream_source": "template_error"} + + assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result5["errors"] == {"stream_source": "template_error"} + + # verify that an relative stream url is rejected. + data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/1" + data[CONF_STREAM_SOURCE] = "relative/stream.mjpeg" + result6 = await hass.config_entries.options.async_configure( + result5["flow_id"], + user_input=data, + ) + assert result6.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result6["errors"] == {"stream_source": "relative_url"} + + # verify that an malformed stream url is rejected. + data[CONF_STILL_IMAGE_URL] = "http://127.0.0.1/testurl/1" + data[CONF_STREAM_SOURCE] = "http://example.com:45:56" + result7 = await hass.config_entries.options.async_configure( + result6["flow_id"], + user_input=data, + ) + assert result7.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result7["errors"] == {"stream_source": "malformed_url"} async def test_slug(hass, caplog): @@ -528,6 +560,10 @@ async def test_slug(hass, caplog): assert result is None assert "Syntax error in" in caplog.text + result = slug(hass, "http://example.com:999999999999/stream") + assert result is None + assert "Syntax error in" in caplog.text + @respx.mock async def test_options_only_stream(hass, fakeimgbytes_png, mock_create_stream): From 373347950cc963a7fe14b2f7a91a0b1210fb9f9f Mon Sep 17 00:00:00 2001 From: mbo18 Date: Sun, 3 Jul 2022 22:49:03 +0200 Subject: [PATCH 2102/3516] Migrate Meteo_france to native_* (#74297) --- .../components/meteo_france/weather.py | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index cca1f6fe684..a30a65304b0 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -4,16 +4,22 @@ import time from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MODE, TEMP_CELSIUS +from homeassistant.const import ( + CONF_MODE, + LENGTH_MILLIMETERS, + PRESSURE_HPA, + SPEED_METERS_PER_SECOND, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -71,6 +77,11 @@ async def async_setup_entry( class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): """Representation of a weather condition.""" + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + def __init__(self, coordinator: DataUpdateCoordinator, mode: str) -> None: """Initialise the platform with a data instance and station name.""" super().__init__(coordinator) @@ -107,17 +118,12 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): ) @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data.current_forecast["T"]["value"] @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" return self.coordinator.data.current_forecast["sea_level"] @@ -127,10 +133,9 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): return self.coordinator.data.current_forecast["humidity"] @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" - # convert from API m/s to km/h - return round(self.coordinator.data.current_forecast["wind"]["speed"] * 3.6) + return self.coordinator.data.current_forecast["wind"]["speed"] @property def wind_bearing(self): @@ -158,9 +163,9 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): ATTR_FORECAST_CONDITION: format_condition( forecast["weather"]["desc"] ), - ATTR_FORECAST_TEMP: forecast["T"]["value"], - ATTR_FORECAST_PRECIPITATION: forecast["rain"].get("1h"), - ATTR_FORECAST_WIND_SPEED: forecast["wind"]["speed"], + ATTR_FORECAST_NATIVE_TEMP: forecast["T"]["value"], + ATTR_FORECAST_NATIVE_PRECIPITATION: forecast["rain"].get("1h"), + ATTR_FORECAST_NATIVE_WIND_SPEED: forecast["wind"]["speed"], ATTR_FORECAST_WIND_BEARING: forecast["wind"]["direction"] if forecast["wind"]["direction"] != -1 else None, @@ -179,9 +184,11 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): ATTR_FORECAST_CONDITION: format_condition( forecast["weather12H"]["desc"] ), - ATTR_FORECAST_TEMP: forecast["T"]["max"], - ATTR_FORECAST_TEMP_LOW: forecast["T"]["min"], - ATTR_FORECAST_PRECIPITATION: forecast["precipitation"]["24h"], + ATTR_FORECAST_NATIVE_TEMP: forecast["T"]["max"], + ATTR_FORECAST_NATIVE_TEMP_LOW: forecast["T"]["min"], + ATTR_FORECAST_NATIVE_PRECIPITATION: forecast["precipitation"][ + "24h" + ], } ) return forecast_data From 08ee73d671697cdfef86aaa4fea86395ef7fe6c0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 1 Jul 2022 11:01:07 -0700 Subject: [PATCH 2103/3516] Guard creating areas in onboarding (#74306) --- homeassistant/components/onboarding/views.py | 8 +++++--- tests/components/onboarding/test_views.py | 7 ++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index 7f40ad87e84..c29fb7edf3a 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -156,9 +156,11 @@ class UserOnboardingView(_BaseOnboardingView): area_registry = ar.async_get(hass) for area in DEFAULT_AREAS: - area_registry.async_create( - translations[f"component.onboarding.area.{area}"] - ) + name = translations[f"component.onboarding.area.{area}"] + # Guard because area might have been created by an automatically + # set up integration. + if not area_registry.async_get_area_by_name(name): + area_registry.async_create(name) await self._async_mark_done(hass) diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 982f5b86e65..204eb6bf772 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -144,6 +144,12 @@ async def test_onboarding_user_already_done(hass, hass_storage, hass_client_no_a async def test_onboarding_user(hass, hass_storage, hass_client_no_auth): """Test creating a new user.""" + area_registry = ar.async_get(hass) + + # Create an existing area to mimic an integration creating an area + # before onboarding is done. + area_registry.async_create("Living Room") + assert await async_setup_component(hass, "person", {}) assert await async_setup_component(hass, "onboarding", {}) await hass.async_block_till_done() @@ -194,7 +200,6 @@ async def test_onboarding_user(hass, hass_storage, hass_client_no_auth): ) # Validate created areas - area_registry = ar.async_get(hass) assert len(area_registry.areas) == 3 assert sorted(area.name for area in area_registry.async_list_areas()) == [ "Bedroom", From 0ca4e81e13295d4ff72af033597a1cfe01722bd6 Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Sat, 2 Jul 2022 21:27:47 +0100 Subject: [PATCH 2104/3516] Migrate metoffice to native_* (#74312) --- homeassistant/components/metoffice/weather.py | 25 ++++++++-------- tests/components/metoffice/test_weather.py | 29 +++++++++++-------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index daf37bcf83f..2e9a55b4415 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -1,15 +1,15 @@ """Support for UK Met Office weather service.""" from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import PRESSURE_HPA, SPEED_MILES_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -55,11 +55,11 @@ def _build_forecast_data(timestep): if timestep.precipitation: data[ATTR_FORECAST_PRECIPITATION_PROBABILITY] = timestep.precipitation.value if timestep.temperature: - data[ATTR_FORECAST_TEMP] = timestep.temperature.value + data[ATTR_FORECAST_NATIVE_TEMP] = timestep.temperature.value if timestep.wind_direction: data[ATTR_FORECAST_WIND_BEARING] = timestep.wind_direction.value if timestep.wind_speed: - data[ATTR_FORECAST_WIND_SPEED] = timestep.wind_speed.value + data[ATTR_FORECAST_NATIVE_WIND_SPEED] = timestep.wind_speed.value return data @@ -73,6 +73,10 @@ def _get_weather_condition(metoffice_code): class MetOfficeWeather(CoordinatorEntity, WeatherEntity): """Implementation of a Met Office weather condition.""" + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR + def __init__(self, coordinator, hass_data, use_3hourly): """Initialise the platform with a data instance.""" super().__init__(coordinator) @@ -94,17 +98,12 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): return None @property - def temperature(self): + def native_temperature(self): """Return the platform temperature.""" if self.coordinator.data.now.temperature: return self.coordinator.data.now.temperature.value return None - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def visibility(self): """Return the platform visibility.""" @@ -119,7 +118,7 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): return _visibility @property - def pressure(self): + def native_pressure(self): """Return the mean sea-level pressure.""" weather_now = self.coordinator.data.now if weather_now and weather_now.pressure: @@ -135,7 +134,7 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): return None @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" weather_now = self.coordinator.data.now if weather_now and weather_now.wind_speed: diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index bf279ff3cf7..3b7ebd08678 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -133,7 +133,8 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 17 - assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_speed") == 14.48 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 @@ -148,7 +149,7 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.attributes.get("forecast")[26]["condition"] == "cloudy" assert weather.attributes.get("forecast")[26]["precipitation_probability"] == 9 assert weather.attributes.get("forecast")[26]["temperature"] == 10 - assert weather.attributes.get("forecast")[26]["wind_speed"] == 4 + assert weather.attributes.get("forecast")[26]["wind_speed"] == 6.44 assert weather.attributes.get("forecast")[26]["wind_bearing"] == "NNE" # Wavertree daily weather platform expected results @@ -157,7 +158,7 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 19 - assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_bearing") == "SSE" assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 @@ -172,7 +173,7 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.attributes.get("forecast")[3]["condition"] == "rainy" assert weather.attributes.get("forecast")[3]["precipitation_probability"] == 59 assert weather.attributes.get("forecast")[3]["temperature"] == 13 - assert weather.attributes.get("forecast")[3]["wind_speed"] == 13 + assert weather.attributes.get("forecast")[3]["wind_speed"] == 20.92 assert weather.attributes.get("forecast")[3]["wind_bearing"] == "SE" @@ -229,7 +230,8 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 17 - assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_speed") == 14.48 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 @@ -244,7 +246,7 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("forecast")[18]["condition"] == "clear-night" assert weather.attributes.get("forecast")[18]["precipitation_probability"] == 1 assert weather.attributes.get("forecast")[18]["temperature"] == 9 - assert weather.attributes.get("forecast")[18]["wind_speed"] == 4 + assert weather.attributes.get("forecast")[18]["wind_speed"] == 6.44 assert weather.attributes.get("forecast")[18]["wind_bearing"] == "NW" # Wavertree daily weather platform expected results @@ -253,7 +255,8 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 19 - assert weather.attributes.get("wind_speed") == 9 + assert weather.attributes.get("wind_speed") == 14.48 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 @@ -268,7 +271,7 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("forecast")[3]["condition"] == "rainy" assert weather.attributes.get("forecast")[3]["precipitation_probability"] == 59 assert weather.attributes.get("forecast")[3]["temperature"] == 13 - assert weather.attributes.get("forecast")[3]["wind_speed"] == 13 + assert weather.attributes.get("forecast")[3]["wind_speed"] == 20.92 assert weather.attributes.get("forecast")[3]["wind_bearing"] == "SE" # King's Lynn 3-hourly weather platform expected results @@ -277,7 +280,8 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.state == "sunny" assert weather.attributes.get("temperature") == 14 - assert weather.attributes.get("wind_speed") == 2 + assert weather.attributes.get("wind_speed") == 3.22 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "E" assert weather.attributes.get("visibility") == "Very Good - 20-40" assert weather.attributes.get("humidity") == 60 @@ -292,7 +296,7 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("forecast")[18]["condition"] == "cloudy" assert weather.attributes.get("forecast")[18]["precipitation_probability"] == 9 assert weather.attributes.get("forecast")[18]["temperature"] == 10 - assert weather.attributes.get("forecast")[18]["wind_speed"] == 7 + assert weather.attributes.get("forecast")[18]["wind_speed"] == 11.27 assert weather.attributes.get("forecast")[18]["wind_bearing"] == "SE" # King's Lynn daily weather platform expected results @@ -301,7 +305,8 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.state == "cloudy" assert weather.attributes.get("temperature") == 9 - assert weather.attributes.get("wind_speed") == 4 + assert weather.attributes.get("wind_speed") == 6.44 + assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "ESE" assert weather.attributes.get("visibility") == "Very Good - 20-40" assert weather.attributes.get("humidity") == 75 @@ -316,5 +321,5 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("forecast")[2]["condition"] == "cloudy" assert weather.attributes.get("forecast")[2]["precipitation_probability"] == 14 assert weather.attributes.get("forecast")[2]["temperature"] == 11 - assert weather.attributes.get("forecast")[2]["wind_speed"] == 7 + assert weather.attributes.get("forecast")[2]["wind_speed"] == 11.27 assert weather.attributes.get("forecast")[2]["wind_bearing"] == "ESE" From ec036313395090f3cff86d4aedbc8b425ab73eac Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Sat, 2 Jul 2022 21:42:58 +0100 Subject: [PATCH 2105/3516] Remove visibility from metoffice weather (#74314) Co-authored-by: Paulus Schoutsen --- homeassistant/components/metoffice/weather.py | 15 --------------- tests/components/metoffice/test_weather.py | 6 ------ 2 files changed, 21 deletions(-) diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index 2e9a55b4415..f4e0bf61d30 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -27,8 +27,6 @@ from .const import ( MODE_3HOURLY_LABEL, MODE_DAILY, MODE_DAILY_LABEL, - VISIBILITY_CLASSES, - VISIBILITY_DISTANCE_CLASSES, ) @@ -104,19 +102,6 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): return self.coordinator.data.now.temperature.value return None - @property - def visibility(self): - """Return the platform visibility.""" - _visibility = None - weather_now = self.coordinator.data.now - if hasattr(weather_now, "visibility"): - visibility_class = VISIBILITY_CLASSES.get(weather_now.visibility.value) - visibility_distance = VISIBILITY_DISTANCE_CLASSES.get( - weather_now.visibility.value - ) - _visibility = f"{visibility_class} - {visibility_distance}" - return _visibility - @property def native_pressure(self): """Return the mean sea-level pressure.""" diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 3b7ebd08678..a93b1ea6b62 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -136,7 +136,6 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 # Forecasts added - just pick out 1 entry to check @@ -160,7 +159,6 @@ async def test_one_weather_site_running(hass, requests_mock): assert weather.attributes.get("temperature") == 19 assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 # Also has Forecasts added - again, just pick out 1 entry to check @@ -233,7 +231,6 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 # Forecasts added - just pick out 1 entry to check @@ -258,7 +255,6 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 14.48 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "SSE" - assert weather.attributes.get("visibility") == "Good - 10-20" assert weather.attributes.get("humidity") == 50 # Also has Forecasts added - again, just pick out 1 entry to check @@ -283,7 +279,6 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 3.22 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "E" - assert weather.attributes.get("visibility") == "Very Good - 20-40" assert weather.attributes.get("humidity") == 60 # Also has Forecast added - just pick out 1 entry to check @@ -308,7 +303,6 @@ async def test_two_weather_sites_running(hass, requests_mock): assert weather.attributes.get("wind_speed") == 6.44 assert weather.attributes.get("wind_speed_unit") == "km/h" assert weather.attributes.get("wind_bearing") == "ESE" - assert weather.attributes.get("visibility") == "Very Good - 20-40" assert weather.attributes.get("humidity") == 75 # All should have Forecast added - again, just picking out 1 entry to check From fe437cc9b7fe03db5b96f8451fb4272e996a4ea4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 2 Jul 2022 22:10:38 +0200 Subject: [PATCH 2106/3516] Add configuration directory to system health (#74318) --- homeassistant/components/homeassistant/strings.json | 3 ++- homeassistant/components/homeassistant/system_health.py | 1 + homeassistant/components/homeassistant/translations/en.json | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index 43180b237b9..317e9d1dfcd 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -2,15 +2,16 @@ "system_health": { "info": { "arch": "CPU Architecture", + "config_dir": "Configuration Directory", "dev": "Development", "docker": "Docker", - "user": "User", "hassio": "Supervisor", "installation_type": "Installation Type", "os_name": "Operating System Family", "os_version": "Operating System Version", "python_version": "Python Version", "timezone": "Timezone", + "user": "User", "version": "Version", "virtualenv": "Virtual Environment" } diff --git a/homeassistant/components/homeassistant/system_health.py b/homeassistant/components/homeassistant/system_health.py index f13278ddfeb..4006228de25 100644 --- a/homeassistant/components/homeassistant/system_health.py +++ b/homeassistant/components/homeassistant/system_health.py @@ -29,4 +29,5 @@ async def system_health_info(hass): "os_version": info.get("os_version"), "arch": info.get("arch"), "timezone": info.get("timezone"), + "config_dir": hass.config.config_dir, } diff --git a/homeassistant/components/homeassistant/translations/en.json b/homeassistant/components/homeassistant/translations/en.json index 977bc203fea..37c4498b32b 100644 --- a/homeassistant/components/homeassistant/translations/en.json +++ b/homeassistant/components/homeassistant/translations/en.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU Architecture", + "config_dir": "Configuration Directory", "dev": "Development", "docker": "Docker", "hassio": "Supervisor", From 2a1a6301a9d5f060b6cfae2f64c3141d37e7ee90 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 3 Jul 2022 22:53:44 +0200 Subject: [PATCH 2107/3516] Fix unique id issue for OpenWeatherMap (#74335) --- .../components/openweathermap/const.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index f180f2a9bbf..06f13daa9c2 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -22,10 +22,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_NATIVE_PRESSURE, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TIME, ) @@ -72,6 +68,11 @@ ATTR_API_FORECAST = "forecast" UPDATE_LISTENER = "update_listener" PLATFORMS = [Platform.SENSOR, Platform.WEATHER] +ATTR_FORECAST_PRECIPITATION = "precipitation" +ATTR_FORECAST_PRESSURE = "pressure" +ATTR_FORECAST_TEMP = "temperature" +ATTR_FORECAST_TEMP_LOW = "templow" + FORECAST_MODE_HOURLY = "hourly" FORECAST_MODE_DAILY = "daily" FORECAST_MODE_FREE_DAILY = "freedaily" @@ -266,7 +267,7 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_PRECIPITATION, + key=ATTR_FORECAST_PRECIPITATION, name="Precipitation", native_unit_of_measurement=LENGTH_MILLIMETERS, ), @@ -276,19 +277,19 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_PRESSURE, + key=ATTR_FORECAST_PRESSURE, name="Pressure", native_unit_of_measurement=PRESSURE_HPA, device_class=SensorDeviceClass.PRESSURE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_TEMP, + key=ATTR_FORECAST_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_TEMP_LOW, + key=ATTR_FORECAST_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, From c2072cc92b5da2eefe322bd93269b06c9fd90a4b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Jul 2022 15:48:34 -0500 Subject: [PATCH 2108/3516] Fix esphome state mapping (#74337) --- homeassistant/components/esphome/__init__.py | 17 +++--- .../components/esphome/entry_data.py | 53 ++++--------------- 2 files changed, 22 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 0c1eac3aa45..ddedaf11ceb 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -558,7 +558,7 @@ async def platform_async_setup_entry( entry_data: RuntimeEntryData = DomainData.get(hass).get_entry_data(entry) entry_data.info[component_key] = {} entry_data.old_info[component_key] = {} - entry_data.state[component_key] = {} + entry_data.state.setdefault(state_type, {}) @callback def async_list_entities(infos: list[EntityInfo]) -> None: @@ -578,7 +578,7 @@ async def platform_async_setup_entry( old_infos.pop(info.key) else: # Create new entity - entity = entity_type(entry_data, component_key, info.key) + entity = entity_type(entry_data, component_key, info.key, state_type) add_entities.append(entity) new_infos[info.key] = info @@ -677,12 +677,17 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): """Define a base esphome entity.""" def __init__( - self, entry_data: RuntimeEntryData, component_key: str, key: int + self, + entry_data: RuntimeEntryData, + component_key: str, + key: int, + state_type: type[_StateT], ) -> None: """Initialize.""" self._entry_data = entry_data self._component_key = component_key self._key = key + self._state_type = state_type async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -707,7 +712,7 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): self.async_on_remove( self._entry_data.async_subscribe_state_update( - self._component_key, self._key, self._on_state_update + self._state_type, self._key, self._on_state_update ) ) @@ -755,11 +760,11 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): @property def _state(self) -> _StateT: - return cast(_StateT, self._entry_data.state[self._component_key][self._key]) + return cast(_StateT, self._entry_data.state[self._state_type][self._key]) @property def _has_state(self) -> bool: - return self._key in self._entry_data.state[self._component_key] + return self._key in self._entry_data.state[self._state_type] @property def available(self) -> bool: diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 8eb56e6fdb6..41a0e89245e 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -12,34 +12,21 @@ from aioesphomeapi import ( APIClient, APIVersion, BinarySensorInfo, - BinarySensorState, CameraInfo, - CameraState, ClimateInfo, - ClimateState, CoverInfo, - CoverState, DeviceInfo, EntityInfo, EntityState, FanInfo, - FanState, LightInfo, - LightState, LockInfo, - LockState, MediaPlayerInfo, - MediaPlayerState, NumberInfo, - NumberState, SelectInfo, - SelectState, SensorInfo, - SensorState, SwitchInfo, - SwitchState, TextSensorInfo, - TextSensorState, UserService, ) from aioesphomeapi.model import ButtonInfo @@ -56,8 +43,8 @@ _LOGGER = logging.getLogger(__name__) # Mapping from ESPHome info type to HA platform INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { BinarySensorInfo: Platform.BINARY_SENSOR, - ButtonInfo: Platform.BINARY_SENSOR, - CameraInfo: Platform.BINARY_SENSOR, + ButtonInfo: Platform.BUTTON, + CameraInfo: Platform.CAMERA, ClimateInfo: Platform.CLIMATE, CoverInfo: Platform.COVER, FanInfo: Platform.FAN, @@ -71,23 +58,6 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { TextSensorInfo: Platform.SENSOR, } -STATE_TYPE_TO_COMPONENT_KEY = { - BinarySensorState: Platform.BINARY_SENSOR, - EntityState: Platform.BINARY_SENSOR, - CameraState: Platform.BINARY_SENSOR, - ClimateState: Platform.CLIMATE, - CoverState: Platform.COVER, - FanState: Platform.FAN, - LightState: Platform.LIGHT, - LockState: Platform.LOCK, - MediaPlayerState: Platform.MEDIA_PLAYER, - NumberState: Platform.NUMBER, - SelectState: Platform.SELECT, - SensorState: Platform.SENSOR, - SwitchState: Platform.SWITCH, - TextSensorState: Platform.SENSOR, -} - @dataclass class RuntimeEntryData: @@ -96,7 +66,7 @@ class RuntimeEntryData: entry_id: str client: APIClient store: Store - state: dict[str, dict[int, EntityState]] = field(default_factory=dict) + state: dict[type[EntityState], dict[int, EntityState]] = field(default_factory=dict) info: dict[str, dict[int, EntityInfo]] = field(default_factory=dict) # A second list of EntityInfo objects @@ -111,9 +81,9 @@ class RuntimeEntryData: api_version: APIVersion = field(default_factory=APIVersion) cleanup_callbacks: list[Callable[[], None]] = field(default_factory=list) disconnect_callbacks: list[Callable[[], None]] = field(default_factory=list) - state_subscriptions: dict[tuple[str, int], Callable[[], None]] = field( - default_factory=dict - ) + state_subscriptions: dict[ + tuple[type[EntityState], int], Callable[[], None] + ] = field(default_factory=dict) loaded_platforms: set[str] = field(default_factory=set) platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock) _storage_contents: dict[str, Any] | None = None @@ -160,24 +130,23 @@ class RuntimeEntryData: @callback def async_subscribe_state_update( self, - component_key: str, + state_type: type[EntityState], state_key: int, entity_callback: Callable[[], None], ) -> Callable[[], None]: """Subscribe to state updates.""" def _unsubscribe() -> None: - self.state_subscriptions.pop((component_key, state_key)) + self.state_subscriptions.pop((state_type, state_key)) - self.state_subscriptions[(component_key, state_key)] = entity_callback + self.state_subscriptions[(state_type, state_key)] = entity_callback return _unsubscribe @callback def async_update_state(self, state: EntityState) -> None: """Distribute an update of state information to the target.""" - component_key = STATE_TYPE_TO_COMPONENT_KEY[type(state)] - subscription_key = (component_key, state.key) - self.state[component_key][state.key] = state + subscription_key = (type(state), state.key) + self.state[type(state)][state.key] = state _LOGGER.debug( "Dispatching update with key %s: %s", subscription_key, From c530e965f83940412c9a9d92210521bb7c1aab63 Mon Sep 17 00:00:00 2001 From: shbatm Date: Sat, 2 Jul 2022 20:38:48 -0500 Subject: [PATCH 2109/3516] Onvif: bump onvif-zeep-async to 1.2.1 (#74341) * Update requirements_all.txt * Update requirements_test_all.txt * Update manifest.json --- homeassistant/components/onvif/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index cd220500751..2df7c3004f0 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -2,7 +2,7 @@ "domain": "onvif", "name": "ONVIF", "documentation": "https://www.home-assistant.io/integrations/onvif", - "requirements": ["onvif-zeep-async==1.2.0", "WSDiscovery==2.0.0"], + "requirements": ["onvif-zeep-async==1.2.1", "WSDiscovery==2.0.0"], "dependencies": ["ffmpeg"], "codeowners": ["@hunterjm"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 84501dbce79..8812e1b8cfe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1150,7 +1150,7 @@ ondilo==0.2.0 onkyo-eiscp==1.2.7 # homeassistant.components.onvif -onvif-zeep-async==1.2.0 +onvif-zeep-async==1.2.1 # homeassistant.components.opengarage open-garage==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de9ca72060d..dd2f423a7cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -794,7 +794,7 @@ omnilogic==0.4.5 ondilo==0.2.0 # homeassistant.components.onvif -onvif-zeep-async==1.2.0 +onvif-zeep-async==1.2.1 # homeassistant.components.opengarage open-garage==0.2.0 From b7a02d946589eaa63ff951ff46f81073fdb652a7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jul 2022 13:56:29 -0700 Subject: [PATCH 2110/3516] Bumped version to 2022.7.0b3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index de1148a3932..c07650c8ac1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 92a7a962cbb..57707cf8af1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.0b2" +version = "2022.7.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 02c7261b74edcf7b4a6f10dc998bb50983ca9360 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Jul 2022 16:32:30 -0500 Subject: [PATCH 2111/3516] Small naming improvements and basic tests for ecobee 501s (#74370) --- .../components/homekit_controller/__init__.py | 16 +- .../fixtures/ecobee_501.json | 475 ++++++++++++++++++ .../specific_devices/test_ecobee_501.py | 65 +++ .../specific_devices/test_hue_bridge.py | 2 +- 4 files changed, 551 insertions(+), 7 deletions(-) create mode 100644 tests/components/homekit_controller/fixtures/ecobee_501.json create mode 100644 tests/components/homekit_controller/specific_devices/test_ecobee_501.py diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index e5853c8ba66..67da7a1068f 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -155,12 +155,16 @@ class HomeKitEntity(Entity): device_name = self._char_name or self.default_name folded_device_name = folded_name(device_name or "") folded_accessory_name = folded_name(accessory_name) - if ( - device_name - and folded_accessory_name not in folded_device_name - and folded_device_name not in folded_accessory_name - ): - return f"{accessory_name} {device_name}" + if device_name: + # Sometimes the device name includes the accessory + # name already like My ecobee Occupancy / My ecobee + if folded_device_name.startswith(folded_accessory_name): + return device_name + if ( + folded_accessory_name not in folded_device_name + and folded_device_name not in folded_accessory_name + ): + return f"{accessory_name} {device_name}" return accessory_name @property diff --git a/tests/components/homekit_controller/fixtures/ecobee_501.json b/tests/components/homekit_controller/fixtures/ecobee_501.json new file mode 100644 index 00000000000..245abe974de --- /dev/null +++ b/tests/components/homekit_controller/fixtures/ecobee_501.json @@ -0,0 +1,475 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "ecobee Inc.", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "ECB501", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pr"], + "format": "string", + "value": "My ecobee", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "123456789016", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "4.7.340214", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "iid": 11, + "perms": ["pr", "hd"], + "format": "string", + "value": "4.1;3fac0fb4", + "maxLen": 64 + }, + { + "type": "00000220-0000-1000-8000-0026BB765291", + "iid": 10, + "perms": ["pr", "hd"], + "format": "data", + "value": "u4qz9c7m80Y=" + }, + { + "type": "000000A6-0000-1000-8000-0026BB765291", + "iid": 9, + "perms": ["pr", "ev"], + "format": "uint32", + "value": 0, + "description": "Accessory Flags" + } + ] + }, + { + "iid": 30, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 31, + "perms": ["pr"], + "format": "string", + "value": "1.1.0", + "description": "Version", + "maxLen": 64 + } + ] + }, + { + "iid": 16, + "type": "0000004A-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000000F-0000-1000-8000-0026BB765291", + "iid": 17, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Current Heating Cooling State", + "minValue": 0, + "maxValue": 2, + "minStep": 1, + "valid-values": [0, 1, 2] + }, + { + "type": "00000033-0000-1000-8000-0026BB765291", + "iid": 18, + "perms": ["pr", "pw", "ev"], + "format": "uint8", + "value": 3, + "description": "Target Heating Cooling State", + "minValue": 0, + "maxValue": 3, + "minStep": 1, + "valid-values": [0, 1, 2, 3] + }, + { + "type": "00000011-0000-1000-8000-0026BB765291", + "iid": 19, + "perms": ["pr", "ev"], + "format": "float", + "value": 21.3, + "description": "Current Temperature", + "unit": "celsius", + "minValue": 0, + "maxValue": 40.0, + "minStep": 0.1 + }, + { + "type": "00000035-0000-1000-8000-0026BB765291", + "iid": 20, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 25.6, + "description": "Target Temperature", + "unit": "celsius", + "minValue": 7.2, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "00000036-0000-1000-8000-0026BB765291", + "iid": 21, + "perms": ["pr", "pw", "ev"], + "format": "uint8", + "value": 1, + "description": "Temperature Display Units", + "minValue": 0, + "maxValue": 1, + "minStep": 1, + "valid-values": [0, 1] + }, + { + "type": "0000000D-0000-1000-8000-0026BB765291", + "iid": 22, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 25.6, + "description": "Cooling Threshold Temperature", + "unit": "celsius", + "minValue": 18.3, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "00000012-0000-1000-8000-0026BB765291", + "iid": 23, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 7.2, + "description": "Heating Threshold Temperature", + "unit": "celsius", + "minValue": 7.2, + "maxValue": 26.1, + "minStep": 0.1 + }, + { + "type": "00000010-0000-1000-8000-0026BB765291", + "iid": 24, + "perms": ["pr", "ev"], + "format": "float", + "value": 55.0, + "description": "Current Relative Humidity", + "unit": "percentage", + "minValue": 0, + "maxValue": 100.0, + "minStep": 1.0 + }, + { + "type": "00000034-0000-1000-8000-0026BB765291", + "iid": 25, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 36.0, + "description": "Target Relative Humidity", + "unit": "percentage", + "minValue": 20.0, + "maxValue": 50.0, + "minStep": 1.0 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 27, + "perms": ["pr"], + "format": "string", + "value": "My ecobee", + "description": "Name", + "maxLen": 64 + }, + { + "type": "000000BF-0000-1000-8000-0026BB765291", + "iid": 75, + "perms": ["pr", "pw", "ev"], + "format": "uint8", + "value": 1, + "description": "Target Fan State", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "000000AF-0000-1000-8000-0026BB765291", + "iid": 76, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "description": "Current Fan State", + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "type": "B7DDB9A3-54BB-4572-91D2-F1F5B0510F8C", + "iid": 33, + "perms": ["pr"], + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "type": "E4489BBC-5227-4569-93E5-B345E3E5508F", + "iid": 34, + "perms": ["pr", "pw"], + "format": "float", + "value": 20.6, + "unit": "celsius", + "minValue": 7.2, + "maxValue": 26.1, + "minStep": 0.1 + }, + { + "type": "7D381BAA-20F9-40E5-9BE9-AEB92D4BECEF", + "iid": 35, + "perms": ["pr", "pw"], + "format": "float", + "value": 25.6, + "unit": "celsius", + "minValue": 18.3, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "73AAB542-892A-4439-879A-D2A883724B69", + "iid": 36, + "perms": ["pr", "pw"], + "format": "float", + "value": 17.8, + "unit": "celsius", + "minValue": 7.2, + "maxValue": 26.1, + "minStep": 0.1 + }, + { + "type": "5DA985F0-898A-4850-B987-B76C6C78D670", + "iid": 37, + "perms": ["pr", "pw"], + "format": "float", + "value": 27.8, + "unit": "celsius", + "minValue": 18.3, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "05B97374-6DC0-439B-A0FA-CA33F612D425", + "iid": 38, + "perms": ["pr", "pw"], + "format": "float", + "value": 18.3, + "unit": "celsius", + "minValue": 7.2, + "maxValue": 26.1, + "minStep": 0.1 + }, + { + "type": "A251F6E7-AC46-4190-9C5D-3D06277BDF9F", + "iid": 39, + "perms": ["pr", "pw"], + "format": "float", + "value": 27.8, + "unit": "celsius", + "minValue": 18.3, + "maxValue": 33.3, + "minStep": 0.1 + }, + { + "type": "1B300BC2-CFFC-47FF-89F9-BD6CCF5F2853", + "iid": 40, + "perms": ["pw"], + "format": "uint8", + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "type": "1621F556-1367-443C-AF19-82AF018E99DE", + "iid": 41, + "perms": ["pr", "pw"], + "format": "string", + "value": "2014-01-03T00:00:00-04:00Q", + "maxLen": 64 + }, + { + "type": "FA128DE6-9D7D-49A4-B6D8-4E4E234DEE38", + "iid": 48, + "perms": ["pw"], + "format": "bool" + }, + { + "type": "4A6AE4F6-036C-495D-87CC-B3702B437741", + "iid": 49, + "perms": ["pr"], + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 4, + "minStep": 1 + }, + { + "type": "DB7BF261-7042-4194-8BD1-3AA22830AEDD", + "iid": 50, + "perms": ["pr"], + "format": "uint8", + "value": 2, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "type": "41935E3E-B54D-42E9-B8B9-D33C6319F0AF", + "iid": 51, + "perms": ["pr"], + "format": "bool", + "value": false + }, + { + "type": "C35DA3C0-E004-40E3-B153-46655CDD9214", + "iid": 52, + "perms": ["pr", "pw"], + "format": "uint8", + "value": 0, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "48F62AEC-4171-4B4A-8F0E-1EEB6708B3FB", + "iid": 53, + "perms": ["pr"], + "format": "uint8", + "value": 0, + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "1B1515F2-CC45-409F-991F-C480987F92C3", + "iid": 54, + "perms": ["pr"], + "format": "string", + "value": "The Hive is humming along. You have no pending alerts or reminders.", + "maxLen": 64 + } + ] + }, + { + "iid": 56, + "type": "00000085-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 28, + "perms": ["pr"], + "format": "string", + "value": "My ecobee Motion", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000022-0000-1000-8000-0026BB765291", + "iid": 66, + "perms": ["pr", "ev"], + "format": "bool", + "value": true, + "description": "Motion Detected" + }, + { + "type": "BFE61C70-4A40-11E6-BDF4-0800200C9A66", + "iid": 67, + "perms": ["pr", "ev"], + "format": "int", + "value": 14, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + }, + { + "iid": 57, + "type": "00000086-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 29, + "perms": ["pr"], + "format": "string", + "value": "My ecobee Occupancy", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000071-0000-1000-8000-0026BB765291", + "iid": 65, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 1, + "description": "Occupancy Detected", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "A8F798E0-4A40-11E6-BDF4-0800200C9A66", + "iid": 68, + "perms": ["pr", "ev"], + "format": "int", + "value": 44, + "unit": "seconds", + "minValue": -1, + "maxValue": 86400, + "minStep": 1 + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee_501.py b/tests/components/homekit_controller/specific_devices/test_ecobee_501.py new file mode 100644 index 00000000000..89443008683 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_ecobee_501.py @@ -0,0 +1,65 @@ +"""Tests for Ecobee 501.""" + + +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, +) +from homeassistant.const import STATE_ON + +from tests.components.homekit_controller.common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_ecobee501_setup(hass): + """Test that a Ecobee 501 can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "ecobee_501.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="My ecobee", + model="ECB501", + manufacturer="ecobee Inc.", + sw_version="4.7.340214", + hw_version="", + serial_number="123456789016", + devices=[], + entities=[ + EntityTestInfo( + entity_id="climate.my_ecobee", + friendly_name="My ecobee", + unique_id="homekit-123456789016-16", + supported_features=( + SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE + | SUPPORT_TARGET_HUMIDITY + ), + capabilities={ + "hvac_modes": ["off", "heat", "cool", "heat_cool"], + "min_temp": 7.2, + "max_temp": 33.3, + "min_humidity": 20, + "max_humidity": 50, + }, + state="heat_cool", + ), + EntityTestInfo( + entity_id="binary_sensor.my_ecobee_occupancy", + friendly_name="My ecobee Occupancy", + unique_id="homekit-123456789016-57", + unit_of_measurement=None, + state=STATE_ON, + ), + ], + ), + ) diff --git a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py index 76d09629064..52a5fb83972 100644 --- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py @@ -41,7 +41,7 @@ async def test_hue_bridge_setup(hass): entities=[ EntityTestInfo( entity_id="sensor.hue_dimmer_switch_battery", - friendly_name="Hue dimmer switch Battery", + friendly_name="Hue dimmer switch battery", unique_id="homekit-6623462389072572-644245094400", unit_of_measurement=PERCENTAGE, state="100", From 269e414e848853b7f7f3d50e6415b5708364fbae Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 4 Jul 2022 00:27:42 +0000 Subject: [PATCH 2112/3516] [ci skip] Translation update --- .../accuweather/translations/pl.json | 2 +- .../components/airly/translations/pl.json | 2 +- .../components/aurora/translations/sv.json | 11 ++++++ .../components/generic/translations/en.json | 7 ++++ .../components/generic/translations/fr.json | 4 +++ .../components/gios/translations/pl.json | 2 +- .../homeassistant/translations/de.json | 1 + .../homeassistant/translations/el.json | 1 + .../homeassistant/translations/ja.json | 1 + .../homeassistant/translations/pl.json | 1 + .../homeassistant/translations/zh-Hant.json | 1 + .../components/nextdns/translations/en.json | 24 +++++++++++++ .../components/nextdns/translations/fr.json | 24 +++++++++++++ .../components/nextdns/translations/pl.json | 24 +++++++++++++ .../components/threshold/translations/sv.json | 5 +++ .../utility_meter/translations/sv.json | 34 ++++++++++++++++++- 16 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/aurora/translations/sv.json create mode 100644 homeassistant/components/nextdns/translations/en.json create mode 100644 homeassistant/components/nextdns/translations/fr.json create mode 100644 homeassistant/components/nextdns/translations/pl.json diff --git a/homeassistant/components/accuweather/translations/pl.json b/homeassistant/components/accuweather/translations/pl.json index 764218f8e11..4ec4d05a932 100644 --- a/homeassistant/components/accuweather/translations/pl.json +++ b/homeassistant/components/accuweather/translations/pl.json @@ -34,7 +34,7 @@ }, "system_health": { "info": { - "can_reach_server": "Dost\u0119p do serwera AccuWeather", + "can_reach_server": "Dost\u0119p do serwera", "remaining_requests": "Pozosta\u0142o dozwolonych \u017c\u0105da\u0144" } } diff --git a/homeassistant/components/airly/translations/pl.json b/homeassistant/components/airly/translations/pl.json index 72d35ec97a7..9ae6243d2fb 100644 --- a/homeassistant/components/airly/translations/pl.json +++ b/homeassistant/components/airly/translations/pl.json @@ -21,7 +21,7 @@ }, "system_health": { "info": { - "can_reach_server": "Dost\u0119p do serwera Airly", + "can_reach_server": "Dost\u0119p do serwera", "requests_per_day": "Dozwolone dzienne \u017c\u0105dania", "requests_remaining": "Pozosta\u0142o dozwolonych \u017c\u0105da\u0144" } diff --git a/homeassistant/components/aurora/translations/sv.json b/homeassistant/components/aurora/translations/sv.json new file mode 100644 index 00000000000..7e16a2c036e --- /dev/null +++ b/homeassistant/components/aurora/translations/sv.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "threshold": "Gr\u00e4nsv\u00e4rde (%)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/en.json b/homeassistant/components/generic/translations/en.json index cb2200f9755..a4e96718225 100644 --- a/homeassistant/components/generic/translations/en.json +++ b/homeassistant/components/generic/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "no_devices_found": "No devices found on the network", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { @@ -13,7 +14,9 @@ "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", + "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", @@ -52,9 +55,13 @@ "malformed_url": "Malformed URL", "no_still_image_or_stream_url": "You must specify at least a still image or stream URL", "relative_url": "Relative URLs are not allowed", + "stream_file_not_found": "File not found while trying to connect to stream (is ffmpeg installed?)", + "stream_http_not_found": "HTTP 404 Not found while trying to connect to stream", "stream_io_error": "Input/Output error while trying to connect to stream. Wrong RTSP transport protocol?", "stream_no_route_to_host": "Could not find host while trying to connect to stream", + "stream_no_video": "Stream has no video", "stream_not_permitted": "Operation not permitted while trying to connect to stream. Wrong RTSP transport protocol?", + "stream_unauthorised": "Authorisation failed while trying to connect to stream", "template_error": "Error rendering template. Review log for more info.", "timeout": "Timeout while loading URL", "unable_still_load": "Unable to load valid image from still image URL (e.g. invalid host, URL or authentication failure). Review log for more info.", diff --git a/homeassistant/components/generic/translations/fr.json b/homeassistant/components/generic/translations/fr.json index 0d517a846e7..2c992c2fa4f 100644 --- a/homeassistant/components/generic/translations/fr.json +++ b/homeassistant/components/generic/translations/fr.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Une cam\u00e9ra avec ces param\u00e8tres d'URL existe d\u00e9j\u00e0.", "invalid_still_image": "L'URL n'a pas renvoy\u00e9 d'image fixe valide", + "malformed_url": "URL mal form\u00e9e", "no_still_image_or_stream_url": "Vous devez au moins renseigner une URL d'image fixe ou de flux", + "relative_url": "Les URL relatives ne sont pas autoris\u00e9es", "stream_file_not_found": "Fichier non trouv\u00e9 lors de la tentative de connexion au flux (ffmpeg est-il install\u00e9\u00a0?)", "stream_http_not_found": "Erreur\u00a0404 (introuvable) lors de la tentative de connexion au flux", "stream_io_error": "Erreur d'entr\u00e9e/sortie lors de la tentative de connexion au flux. Mauvais protocole de transport RTSP\u00a0?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Une cam\u00e9ra avec ces param\u00e8tres d'URL existe d\u00e9j\u00e0.", "invalid_still_image": "L'URL n'a pas renvoy\u00e9 d'image fixe valide", + "malformed_url": "URL mal form\u00e9e", "no_still_image_or_stream_url": "Vous devez au moins renseigner une URL d'image fixe ou de flux", + "relative_url": "Les URL relatives ne sont pas autoris\u00e9es", "stream_file_not_found": "Fichier non trouv\u00e9 lors de la tentative de connexion au flux (ffmpeg est-il install\u00e9\u00a0?)", "stream_http_not_found": "Erreur\u00a0404 (introuvable) lors de la tentative de connexion au flux", "stream_io_error": "Erreur d'entr\u00e9e/sortie lors de la tentative de connexion au flux. Mauvais protocole de transport RTSP\u00a0?", diff --git a/homeassistant/components/gios/translations/pl.json b/homeassistant/components/gios/translations/pl.json index 6a7d2aa7064..475ab76b576 100644 --- a/homeassistant/components/gios/translations/pl.json +++ b/homeassistant/components/gios/translations/pl.json @@ -20,7 +20,7 @@ }, "system_health": { "info": { - "can_reach_server": "Dost\u0119p do serwera GIO\u015a" + "can_reach_server": "Dost\u0119p do serwera" } } } \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/de.json b/homeassistant/components/homeassistant/translations/de.json index 54909cb3c24..756670e5a47 100644 --- a/homeassistant/components/homeassistant/translations/de.json +++ b/homeassistant/components/homeassistant/translations/de.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU-Architektur", + "config_dir": "Konfigurationsverzeichnis", "dev": "Entwicklung", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/el.json b/homeassistant/components/homeassistant/translations/el.json index 616ad96a867..9c345d511e1 100644 --- a/homeassistant/components/homeassistant/translations/el.json +++ b/homeassistant/components/homeassistant/translations/el.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "\u0391\u03c1\u03c7\u03b9\u03c4\u03b5\u03ba\u03c4\u03bf\u03bd\u03b9\u03ba\u03ae CPU", + "config_dir": "\u039a\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd", "dev": "\u0391\u03bd\u03ac\u03c0\u03c4\u03c5\u03be\u03b7", "docker": "Docker", "hassio": "\u0395\u03c0\u03cc\u03c0\u03c4\u03b7\u03c2", diff --git a/homeassistant/components/homeassistant/translations/ja.json b/homeassistant/components/homeassistant/translations/ja.json index 14b1deb55c8..8be012c7c1a 100644 --- a/homeassistant/components/homeassistant/translations/ja.json +++ b/homeassistant/components/homeassistant/translations/ja.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU\u30a2\u30fc\u30ad\u30c6\u30af\u30c1\u30e3", + "config_dir": "\u30b3\u30f3\u30d5\u30a3\u30ae\u30e5\u30ec\u30fc\u30b7\u30e7\u30f3\u30c7\u30a3\u30ec\u30af\u30c8\u30ea", "dev": "\u30c7\u30a3\u30d9\u30ed\u30c3\u30d7\u30e1\u30f3\u30c8", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/pl.json b/homeassistant/components/homeassistant/translations/pl.json index 9f85cc4ff15..bdf26a8b49d 100644 --- a/homeassistant/components/homeassistant/translations/pl.json +++ b/homeassistant/components/homeassistant/translations/pl.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Architektura procesora", + "config_dir": "Folder konfiguracji", "dev": "Wersja deweloperska", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/zh-Hant.json b/homeassistant/components/homeassistant/translations/zh-Hant.json index 03812830b26..c42acf960c6 100644 --- a/homeassistant/components/homeassistant/translations/zh-Hant.json +++ b/homeassistant/components/homeassistant/translations/zh-Hant.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU \u67b6\u69cb", + "config_dir": "\u8a2d\u5b9a\u76ee\u9304", "dev": "\u958b\u767c\u7248", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/nextdns/translations/en.json b/homeassistant/components/nextdns/translations/en.json new file mode 100644 index 00000000000..c765d53a4d9 --- /dev/null +++ b/homeassistant/components/nextdns/translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "This NextDNS profile is already configured." + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_api_key": "Invalid API key", + "unknown": "Unexpected error" + }, + "step": { + "profiles": { + "data": { + "profile": "Profile" + } + }, + "user": { + "data": { + "api_key": "API Key" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/fr.json b/homeassistant/components/nextdns/translations/fr.json new file mode 100644 index 00000000000..bfebc9078a5 --- /dev/null +++ b/homeassistant/components/nextdns/translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Ce profil NextDNS est d\u00e9j\u00e0 configur\u00e9." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_api_key": "Cl\u00e9 d'API non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "Cl\u00e9 d'API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/pl.json b/homeassistant/components/nextdns/translations/pl.json new file mode 100644 index 00000000000..b7b74af7f1b --- /dev/null +++ b/homeassistant/components/nextdns/translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Ten profil NextDNS jest ju\u017c skonfigurowany." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_api_key": "Nieprawid\u0142owy klucz API", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "Klucz API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/sv.json b/homeassistant/components/threshold/translations/sv.json index d19b6cabb91..dd247031a4e 100644 --- a/homeassistant/components/threshold/translations/sv.json +++ b/homeassistant/components/threshold/translations/sv.json @@ -2,6 +2,11 @@ "config": { "error": { "need_lower_upper": "Undre och \u00f6vre gr\u00e4ns kan inte vara tomma" + }, + "step": { + "user": { + "title": "L\u00e4gg till gr\u00e4nsv\u00e4rdessensor" + } } }, "options": { diff --git a/homeassistant/components/utility_meter/translations/sv.json b/homeassistant/components/utility_meter/translations/sv.json index 38d433e7b42..6ef91919b73 100644 --- a/homeassistant/components/utility_meter/translations/sv.json +++ b/homeassistant/components/utility_meter/translations/sv.json @@ -1,3 +1,35 @@ { - "title": "Tj\u00e4nsten \u00e4r redan konfigurerad" + "config": { + "step": { + "user": { + "data": { + "cycle": "\u00c5terst\u00e4ll m\u00e4tarcykel", + "delta_values": "Delta-v\u00e4rden", + "name": "Namn", + "net_consumption": "Nettof\u00f6rbrukning", + "offset": "\u00c5terst\u00e4ll m\u00e4taroffset", + "source": "Sensorsk\u00e4lla", + "tariffs": "Tariffer som st\u00f6ds" + }, + "data_description": { + "delta_values": "Aktivera om k\u00e4llv\u00e4rdena \u00e4r deltav\u00e4rden sedan den senaste avl\u00e4sningen ist\u00e4llet f\u00f6r absoluta v\u00e4rden.", + "net_consumption": "Aktivera om k\u00e4llan \u00e4r en nettom\u00e4tare, vilket betyder att den b\u00e5de kan \u00f6ka och minska.", + "offset": "F\u00f6rskjut dagen f\u00f6r en m\u00e5natlig m\u00e4tar\u00e5terst\u00e4llning.", + "tariffs": "En lista \u00f6ver st\u00f6dda tariffer, l\u00e4mna tom om bara en enda tariff beh\u00f6vs." + }, + "description": "Skapa en sensor som \u00f6vervakar f\u00f6rbrukningen av t.ex. energi, gas, vatten, v\u00e4rme, \u00f6ver en konfigurerad tidsperiod, vanligtvis m\u00e5nadsvis. M\u00e4tarsensorn st\u00f6djer valfritt att dela upp f\u00f6rbrukningen efter tariffer, i s\u00e5 fall skapas en sensor f\u00f6r varje tariff samt en val entitet f\u00f6r att v\u00e4lja den aktuella tariffen.", + "title": "L\u00e4gg till m\u00e4tare" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source": "Sensork\u00e4lla" + } + } + } + }, + "title": "M\u00e4tare" } \ No newline at end of file From b62c0dcb32eef24be519c6da946f933a34813a64 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jul 2022 22:03:13 -0700 Subject: [PATCH 2113/3516] Guard invalid data sensor significant change (#74369) --- .../components/sensor/significant_change.py | 18 ++++++++++++++---- .../sensor/test_significant_change.py | 2 ++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/significant_change.py b/homeassistant/components/sensor/significant_change.py index 31b4f00c37f..6ff23b43508 100644 --- a/homeassistant/components/sensor/significant_change.py +++ b/homeassistant/components/sensor/significant_change.py @@ -63,13 +63,23 @@ def async_check_significant_change( absolute_change = 1.0 percentage_change = 2.0 + try: + # New state is invalid, don't report it + new_state_f = float(new_state) + except ValueError: + return False + + try: + # Old state was invalid, we should report again + old_state_f = float(old_state) + except ValueError: + return True + if absolute_change is not None and percentage_change is not None: return _absolute_and_relative_change( - float(old_state), float(new_state), absolute_change, percentage_change + old_state_f, new_state_f, absolute_change, percentage_change ) if absolute_change is not None: - return check_absolute_change( - float(old_state), float(new_state), absolute_change - ) + return check_absolute_change(old_state_f, new_state_f, absolute_change) return None diff --git a/tests/components/sensor/test_significant_change.py b/tests/components/sensor/test_significant_change.py index 051a92f3b07..bfa01d6eb08 100644 --- a/tests/components/sensor/test_significant_change.py +++ b/tests/components/sensor/test_significant_change.py @@ -52,6 +52,8 @@ TEMP_FREEDOM_ATTRS = { ("12.1", "12.2", TEMP_CELSIUS_ATTRS, False), ("70", "71", TEMP_FREEDOM_ATTRS, True), ("70", "70.5", TEMP_FREEDOM_ATTRS, False), + ("fail", "70", TEMP_FREEDOM_ATTRS, True), + ("70", "fail", TEMP_FREEDOM_ATTRS, False), ], ) async def test_significant_change_temperature(old_state, new_state, attrs, result): From 810b2a2bd6e281f45d7b58f4faee35d4d4e45f4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Jul 2022 00:36:42 -0500 Subject: [PATCH 2114/3516] Inline building entity registry dict (#74378) --- .../components/config/entity_registry.py | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index e6b91ee5a50..5f484c10472 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -29,7 +29,22 @@ def websocket_list_entities(hass, connection, msg): registry = er.async_get(hass) connection.send_message( websocket_api.result_message( - msg["id"], [_entry_dict(entry) for entry in registry.entities.values()] + msg["id"], + [ + { + "area_id": entry.area_id, + "config_entry_id": entry.config_entry_id, + "device_id": entry.device_id, + "disabled_by": entry.disabled_by, + "entity_category": entry.entity_category, + "entity_id": entry.entity_id, + "hidden_by": entry.hidden_by, + "icon": entry.icon, + "name": entry.name, + "platform": entry.platform, + } + for entry in registry.entities.values() + ], ) ) @@ -196,7 +211,7 @@ def websocket_remove_entity(hass, connection, msg): @callback -def _entry_dict(entry): +def _entry_ext_dict(entry): """Convert entry to API format.""" return { "area_id": entry.area_id, @@ -209,19 +224,12 @@ def _entry_dict(entry): "icon": entry.icon, "name": entry.name, "platform": entry.platform, + "capabilities": entry.capabilities, + "device_class": entry.device_class, + "has_entity_name": entry.has_entity_name, + "options": entry.options, + "original_device_class": entry.original_device_class, + "original_icon": entry.original_icon, + "original_name": entry.original_name, + "unique_id": entry.unique_id, } - - -@callback -def _entry_ext_dict(entry): - """Convert entry to API format.""" - data = _entry_dict(entry) - data["capabilities"] = entry.capabilities - data["device_class"] = entry.device_class - data["has_entity_name"] = entry.has_entity_name - data["options"] = entry.options - data["original_device_class"] = entry.original_device_class - data["original_icon"] = entry.original_icon - data["original_name"] = entry.original_name - data["unique_id"] = entry.unique_id - return data From 07f677e9e8fda1bf1e008da4a3f780261f8f7736 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 10:47:59 +0200 Subject: [PATCH 2115/3516] Migrate knx weather to native_* (#74386) --- homeassistant/components/knx/weather.py | 39 ++++++++++++------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 6e71c09501f..32f37ad2ac2 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -6,7 +6,14 @@ from xknx.devices import Weather as XknxWeather from homeassistant import config_entries from homeassistant.components.weather import WeatherEntity -from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, TEMP_CELSIUS, Platform +from homeassistant.const import ( + CONF_ENTITY_CATEGORY, + CONF_NAME, + PRESSURE_PA, + SPEED_METERS_PER_SECOND, + TEMP_CELSIUS, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType @@ -68,7 +75,9 @@ class KNXWeather(KnxEntity, WeatherEntity): """Representation of a KNX weather device.""" _device: XknxWeather - _attr_temperature_unit = TEMP_CELSIUS + _attr_native_pressure_unit = PRESSURE_PA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of a KNX sensor.""" @@ -77,19 +86,14 @@ class KNXWeather(KnxEntity, WeatherEntity): self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) @property - def temperature(self) -> float | None: - """Return current temperature.""" + def native_temperature(self) -> float | None: + """Return current temperature in C.""" return self._device.temperature @property - def pressure(self) -> float | None: - """Return current air pressure.""" - # KNX returns pA - HA requires hPa - return ( - self._device.air_pressure / 100 - if self._device.air_pressure is not None - else None - ) + def native_pressure(self) -> float | None: + """Return current air pressure in Pa.""" + return self._device.air_pressure @property def condition(self) -> str: @@ -107,11 +111,6 @@ class KNXWeather(KnxEntity, WeatherEntity): return self._device.wind_bearing @property - def wind_speed(self) -> float | None: - """Return current wind speed in km/h.""" - # KNX only supports wind speed in m/s - return ( - self._device.wind_speed * 3.6 - if self._device.wind_speed is not None - else None - ) + def native_wind_speed(self) -> float | None: + """Return current wind speed in m/s.""" + return self._device.wind_speed From 227d8b69a7e1cde0d1646bebbeb52ef1c18a125e Mon Sep 17 00:00:00 2001 From: Thanasis Date: Mon, 4 Jul 2022 14:11:36 +0300 Subject: [PATCH 2116/3516] Allowing for TOON cost sensors to work with Energy (#74315) --- homeassistant/components/toon/sensor.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index 62829bf20ad..371672e184e 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -182,6 +182,8 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( name="Gas Cost Today", section="gas_usage", measurement="day_cost", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=CURRENCY_EUR, icon="mdi:gas-cylinder", cls=ToonGasMeterDeviceSensor, @@ -230,6 +232,8 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( name="Energy Cost Today", section="power_usage", measurement="day_cost", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=CURRENCY_EUR, icon="mdi:power-plug", cls=ToonElectricityMeterDeviceSensor, @@ -350,6 +354,8 @@ SENSOR_ENTITIES: tuple[ToonSensorEntityDescription, ...] = ( name="Water Cost Today", section="water_usage", measurement="day_cost", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=CURRENCY_EUR, icon="mdi:water-pump", entity_registry_enabled_default=False, From f851877449a66cf88b6db77396046a6cb4c9e25a Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 4 Jul 2022 14:14:27 +0300 Subject: [PATCH 2117/3516] Bump aioimaplib to 1.0.0 (#74393) --- homeassistant/components/imap/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index 655590005bf..f4bbadfa6ac 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -2,7 +2,7 @@ "domain": "imap", "name": "IMAP", "documentation": "https://www.home-assistant.io/integrations/imap", - "requirements": ["aioimaplib==0.9.0"], + "requirements": ["aioimaplib==1.0.0"], "codeowners": [], "iot_class": "cloud_push", "loggers": ["aioimaplib"] diff --git a/requirements_all.txt b/requirements_all.txt index 87123abeba7..b5798a96679 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -178,7 +178,7 @@ aiohttp_cors==0.7.0 aiohue==4.4.2 # homeassistant.components.imap -aioimaplib==0.9.0 +aioimaplib==1.0.0 # homeassistant.components.apache_kafka aiokafka==0.6.0 From b5387ed769be041dc374e95b6bfd26830ef06245 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 4 Jul 2022 13:16:01 +0200 Subject: [PATCH 2118/3516] Update Pillow to 9.2.0 (#74371) --- homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/generic/manifest.json | 2 +- homeassistant/components/image/manifest.json | 2 +- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- homeassistant/components/seven_segments/manifest.json | 2 +- homeassistant/components/sighthound/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index ec170733e44..766167b7af9 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -2,7 +2,7 @@ "domain": "doods", "name": "DOODS - Dedicated Open Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": ["pydoods==1.0.2", "pillow==9.1.1"], + "requirements": ["pydoods==1.0.2", "pillow==9.2.0"], "codeowners": [], "iot_class": "local_polling", "loggers": ["pydoods"] diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index 5ef47f0c941..98ca2986a86 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -2,7 +2,7 @@ "domain": "generic", "name": "Generic Camera", "config_flow": true, - "requirements": ["ha-av==10.0.0b4", "pillow==9.1.1"], + "requirements": ["ha-av==10.0.0b4", "pillow==9.2.0"], "documentation": "https://www.home-assistant.io/integrations/generic", "codeowners": ["@davet2001"], "iot_class": "local_push" diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json index d2e08b77b23..4f967dbcc89 100644 --- a/homeassistant/components/image/manifest.json +++ b/homeassistant/components/image/manifest.json @@ -3,7 +3,7 @@ "name": "Image", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/image", - "requirements": ["pillow==9.1.1"], + "requirements": ["pillow==9.2.0"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index d12f5ed92fd..59d19bc785e 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,6 +2,6 @@ "domain": "proxy", "name": "Camera Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["pillow==9.1.1"], + "requirements": ["pillow==9.2.0"], "codeowners": [] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 6715d1ba6db..666cc0c93ce 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -2,7 +2,7 @@ "domain": "qrcode", "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", - "requirements": ["pillow==9.1.1", "pyzbar==0.1.7"], + "requirements": ["pillow==9.2.0", "pyzbar==0.1.7"], "codeowners": [], "iot_class": "calculated", "loggers": ["pyzbar"] diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index caee1d72c38..09457fc4ca5 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,7 +2,7 @@ "domain": "seven_segments", "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": ["pillow==9.1.1"], + "requirements": ["pillow==9.2.0"], "codeowners": ["@fabaff"], "iot_class": "local_polling" } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index e87e1d37304..400664079e2 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -2,7 +2,7 @@ "domain": "sighthound", "name": "Sighthound", "documentation": "https://www.home-assistant.io/integrations/sighthound", - "requirements": ["pillow==9.1.1", "simplehound==0.3"], + "requirements": ["pillow==9.2.0", "simplehound==0.3"], "codeowners": ["@robmarkcole"], "iot_class": "cloud_polling", "loggers": ["simplehound"] diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 42d0eae1ecd..dd88fd7e277 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -7,7 +7,7 @@ "tf-models-official==2.5.0", "pycocotools==2.0.1", "numpy==1.23.0", - "pillow==9.1.1" + "pillow==9.2.0" ], "codeowners": [], "iot_class": "local_polling", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7ee5a9fe8d3..a62ae0b6fd1 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ jinja2==3.1.2 lru-dict==1.1.7 orjson==3.7.5 paho-mqtt==1.6.1 -pillow==9.1.1 +pillow==9.2.0 pip>=21.0,<22.2 pyserial==3.5 python-slugify==4.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index b5798a96679..4e910c36412 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1239,7 +1239,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.1.1 +pillow==9.2.0 # homeassistant.components.dominos pizzapi==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b47aefebf16..fd3756a1c80 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -850,7 +850,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.1.1 +pillow==9.2.0 # homeassistant.components.plex plexapi==4.11.2 From ab37f59345bd452186a074ce1635145cc0705670 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 13:38:53 +0200 Subject: [PATCH 2119/3516] Migrate meteoclimatic weather to native_* (#74392) --- .../components/meteoclimatic/weather.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/meteoclimatic/weather.py b/homeassistant/components/meteoclimatic/weather.py index 4faecdaa3ac..8044dd04aa8 100644 --- a/homeassistant/components/meteoclimatic/weather.py +++ b/homeassistant/components/meteoclimatic/weather.py @@ -3,7 +3,7 @@ from meteoclimatic import Condition from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -38,6 +38,10 @@ async def async_setup_entry( class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity): """Representation of a weather condition.""" + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__(self, coordinator: DataUpdateCoordinator) -> None: """Initialise the weather platform.""" super().__init__(coordinator) @@ -71,27 +75,22 @@ class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity): return format_condition(self.coordinator.data["weather"].condition) @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data["weather"].temp_current - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def humidity(self): """Return the humidity.""" return self.coordinator.data["weather"].humidity_current @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" return self.coordinator.data["weather"].pressure_current @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" return self.coordinator.data["weather"].wind_current From 0768ed453d7aa267ac853e481843c5515bb00ea9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 14:06:32 +0200 Subject: [PATCH 2120/3516] Migrate aemet to native_* (#74037) --- homeassistant/components/aemet/__init__.py | 39 ++++++++- homeassistant/components/aemet/const.py | 26 ++++-- homeassistant/components/aemet/sensor.py | 16 ++-- homeassistant/components/aemet/weather.py | 20 +++-- .../aemet/weather_update_coordinator.py | 20 ++--- tests/components/aemet/test_init.py | 84 +++++++++++++++++++ 6 files changed, 168 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index a914a23a0da..7b86a5559e0 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -1,18 +1,30 @@ """The AEMET OpenData component.""" +from __future__ import annotations + import logging +from typing import Any from aemet_opendata.interface import AEMET from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + Platform, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from .const import ( CONF_STATION_UPDATES, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, + FORECAST_MODES, PLATFORMS, + RENAMED_FORECAST_SENSOR_KEYS, ) from .weather_update_coordinator import WeatherUpdateCoordinator @@ -21,6 +33,8 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AEMET OpenData as config entry.""" + await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry) + name = entry.data[CONF_NAME] api_key = entry.data[CONF_API_KEY] latitude = entry.data[CONF_LATITUDE] @@ -60,3 +74,24 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +@callback +def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None: + """Migrate AEMET entity entries. + + - Migrates unique ID from old forecast sensors to the new unique ID + """ + if entry.domain != Platform.SENSOR: + return None + for old_key, new_key in RENAMED_FORECAST_SENSOR_KEYS.items(): + for forecast_mode in FORECAST_MODES: + old_suffix = f"-forecast-{forecast_mode}-{old_key}" + if entry.unique_id.endswith(old_suffix): + new_suffix = f"-forecast-{forecast_mode}-{new_key}" + return { + "new_unique_id": entry.unique_id.replace(old_suffix, new_suffix) + } + + # No migration needed + return None diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index 4be90011f5a..48e7335934f 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -18,6 +18,10 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, @@ -159,13 +163,13 @@ CONDITIONS_MAP = { FORECAST_MONITORED_CONDITIONS = [ ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, + ATTR_FORECAST_NATIVE_WIND_SPEED, ] MONITORED_CONDITIONS = [ ATTR_API_CONDITION, @@ -206,7 +210,7 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION, + key=ATTR_FORECAST_NATIVE_PRECIPITATION, name="Precipitation", native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, ), @@ -216,13 +220,13 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP, + key=ATTR_FORECAST_NATIVE_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP_LOW, + key=ATTR_FORECAST_NATIVE_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, @@ -238,11 +242,17 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=DEGREE, ), SensorEntityDescription( - key=ATTR_FORECAST_WIND_SPEED, + key=ATTR_FORECAST_NATIVE_WIND_SPEED, name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, ), ) +RENAMED_FORECAST_SENSOR_KEYS = { + ATTR_FORECAST_PRECIPITATION: ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_TEMP_LOW: ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, +} WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=ATTR_API_CONDITION, diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index f98e3fff49e..8439b166a47 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -45,17 +45,13 @@ async def async_setup_entry( entities.extend( [ AemetForecastSensor( - name_prefix, - unique_id_prefix, + f"{domain_data[ENTRY_NAME]} {mode} Forecast", + f"{unique_id}-forecast-{mode}", weather_coordinator, mode, description, ) for mode in FORECAST_MODES - if ( - (name_prefix := f"{domain_data[ENTRY_NAME]} {mode} Forecast") - and (unique_id_prefix := f"{unique_id}-forecast-{mode}") - ) for description in FORECAST_SENSOR_TYPES if description.key in FORECAST_MONITORED_CONDITIONS ] @@ -89,14 +85,14 @@ class AemetSensor(AbstractAemetSensor): def __init__( self, name, - unique_id, + unique_id_prefix, weather_coordinator: WeatherUpdateCoordinator, description: SensorEntityDescription, ): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id}-{description.key}", + unique_id=f"{unique_id_prefix}-{description.key}", coordinator=weather_coordinator, description=description, ) @@ -113,7 +109,7 @@ class AemetForecastSensor(AbstractAemetSensor): def __init__( self, name, - unique_id, + unique_id_prefix, weather_coordinator: WeatherUpdateCoordinator, forecast_mode, description: SensorEntityDescription, @@ -121,7 +117,7 @@ class AemetForecastSensor(AbstractAemetSensor): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id}-{description.key}", + unique_id=f"{unique_id_prefix}-{description.key}", coordinator=weather_coordinator, description=description, ) diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index d05442b621e..a67726d1f51 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -1,7 +1,12 @@ """Support for the AEMET OpenData service.""" from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS +from homeassistant.const import ( + LENGTH_MILLIMETERS, + PRESSURE_HPA, + SPEED_KILOMETERS_PER_HOUR, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -47,9 +52,10 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): """Implementation of an AEMET OpenData sensor.""" _attr_attribution = ATTRIBUTION - _attr_temperature_unit = TEMP_CELSIUS - _attr_pressure_unit = PRESSURE_HPA - _attr_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR def __init__( self, @@ -83,12 +89,12 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_HUMIDITY] @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" return self.coordinator.data[ATTR_API_PRESSURE] @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data[ATTR_API_TEMPERATURE] @@ -98,6 +104,6 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_WIND_BEARING] @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" return self.coordinator.data[ATTR_API_WIND_SPEED] diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index c86465ea8f1..4f0bf6ac5ea 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -44,13 +44,13 @@ import async_timeout from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, ) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -406,10 +406,10 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day( day ), - ATTR_FORECAST_TEMP: self._get_temperature_day(day), - ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), + ATTR_FORECAST_NATIVE_TEMP: self._get_temperature_day(day), + ATTR_FORECAST_NATIVE_TEMP_LOW: self._get_temperature_low_day(day), ATTR_FORECAST_TIME: dt_util.as_utc(date).isoformat(), - ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), + ATTR_FORECAST_NATIVE_WIND_SPEED: self._get_wind_speed_day(day), ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), } @@ -421,13 +421,13 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return { ATTR_FORECAST_CONDITION: condition, - ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour), + ATTR_FORECAST_NATIVE_PRECIPITATION: self._calc_precipitation(day, hour), ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob( day, hour ), - ATTR_FORECAST_TEMP: self._get_temperature(day, hour), + ATTR_FORECAST_NATIVE_TEMP: self._get_temperature(day, hour), ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), - ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), + ATTR_FORECAST_NATIVE_WIND_SPEED: self._get_wind_speed(day, hour), ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), } diff --git a/tests/components/aemet/test_init.py b/tests/components/aemet/test_init.py index b1f452c1b46..8dd177a145d 100644 --- a/tests/components/aemet/test_init.py +++ b/tests/components/aemet/test_init.py @@ -2,11 +2,15 @@ from unittest.mock import patch +import pytest import requests_mock from homeassistant.components.aemet.const import DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util from .util import aemet_requests_mock @@ -42,3 +46,83 @@ async def test_unload_entry(hass): await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + "old_unique_id,new_unique_id", + [ + # Sensors which should be migrated + ( + "aemet_unique_id-forecast-daily-precipitation", + "aemet_unique_id-forecast-daily-native_precipitation", + ), + ( + "aemet_unique_id-forecast-daily-temperature", + "aemet_unique_id-forecast-daily-native_temperature", + ), + ( + "aemet_unique_id-forecast-daily-templow", + "aemet_unique_id-forecast-daily-native_templow", + ), + ( + "aemet_unique_id-forecast-daily-wind_speed", + "aemet_unique_id-forecast-daily-native_wind_speed", + ), + ( + "aemet_unique_id-forecast-hourly-precipitation", + "aemet_unique_id-forecast-hourly-native_precipitation", + ), + ( + "aemet_unique_id-forecast-hourly-temperature", + "aemet_unique_id-forecast-hourly-native_temperature", + ), + ( + "aemet_unique_id-forecast-hourly-templow", + "aemet_unique_id-forecast-hourly-native_templow", + ), + ( + "aemet_unique_id-forecast-hourly-wind_speed", + "aemet_unique_id-forecast-hourly-native_wind_speed", + ), + # Already migrated + ( + "aemet_unique_id-forecast-daily-native_templow", + "aemet_unique_id-forecast-daily-native_templow", + ), + # No migration needed + ( + "aemet_unique_id-forecast-daily-condition", + "aemet_unique_id-forecast-daily-condition", + ), + ], +) +async def test_migrate_unique_id_sensor( + hass: HomeAssistant, + old_unique_id: str, + new_unique_id: str, +) -> None: + """Test migration of unique_id.""" + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + with patch("homeassistant.util.dt.now", return_value=now), patch( + "homeassistant.util.dt.utcnow", return_value=now + ), requests_mock.mock() as _m: + aemet_requests_mock(_m) + config_entry = MockConfigEntry( + domain=DOMAIN, unique_id="aemet_unique_id", data=CONFIG + ) + config_entry.add_to_hass(hass) + + entity_registry = er.async_get(hass) + entity: er.RegistryEntry = entity_registry.async_get_or_create( + domain=SENSOR_DOMAIN, + platform=DOMAIN, + unique_id=old_unique_id, + config_entry=config_entry, + ) + assert entity.unique_id == old_unique_id + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entity_migrated = entity_registry.async_get(entity.entity_id) + assert entity_migrated + assert entity_migrated.unique_id == new_unique_id From 14e5001d0c69745c243582d7dfc6c2dbe2f54a23 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Jul 2022 14:10:25 +0200 Subject: [PATCH 2121/3516] Cleanup known_devices.yaml in device_tracker tests (#74404) --- tests/components/device_tracker/test_init.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index d8914032f36..3bafa59fb96 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -162,7 +162,7 @@ async def test_duplicate_mac_dev_id(mock_warning, hass): assert "Duplicate device IDs" in args[0], "Duplicate device IDs warning expected" -async def test_setup_without_yaml_file(hass, enable_custom_integrations): +async def test_setup_without_yaml_file(hass, yaml_devices, enable_custom_integrations): """Test with no YAML file.""" with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) From 1c0ece6ac1e051c9b0c78d36cd84ed1648537ffb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 14:20:47 +0200 Subject: [PATCH 2122/3516] Migrate met_eireann weather to native_* (#74391) Co-authored-by: avee87 <6134677+avee87@users.noreply.github.com> Co-authored-by: Franck Nijhof --- homeassistant/components/met_eireann/const.py | 20 +++---- .../components/met_eireann/weather.py | 59 +++++-------------- 2 files changed, 22 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/met_eireann/const.py b/homeassistant/components/met_eireann/const.py index 98d862183c4..efe80cb9d17 100644 --- a/homeassistant/components/met_eireann/const.py +++ b/homeassistant/components/met_eireann/const.py @@ -1,6 +1,4 @@ """Constants for Met Éireann component.""" -import logging - from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -12,13 +10,13 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY_RAINY, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_PRESSURE, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRESSURE, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, DOMAIN as WEATHER_DOMAIN, ) @@ -32,17 +30,15 @@ HOME_LOCATION_NAME = "Home" ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.met_eireann_{HOME_LOCATION_NAME}" -_LOGGER = logging.getLogger(".") - FORECAST_MAP = { ATTR_FORECAST_CONDITION: "condition", - ATTR_FORECAST_PRESSURE: "pressure", + ATTR_FORECAST_NATIVE_PRESSURE: "pressure", ATTR_FORECAST_PRECIPITATION: "precipitation", - ATTR_FORECAST_TEMP: "temperature", - ATTR_FORECAST_TEMP_LOW: "templow", + ATTR_FORECAST_NATIVE_TEMP: "temperature", + ATTR_FORECAST_NATIVE_TEMP_LOW: "templow", ATTR_FORECAST_TIME: "datetime", ATTR_FORECAST_WIND_BEARING: "wind_bearing", - ATTR_FORECAST_WIND_SPEED: "wind_speed", + ATTR_FORECAST_NATIVE_WIND_SPEED: "wind_speed", } CONDITION_MAP = { diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index cbf5c99342a..f20f0e1254a 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -3,8 +3,6 @@ import logging from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, WeatherEntity, ) @@ -13,12 +11,9 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - LENGTH_INCHES, LENGTH_MILLIMETERS, PRESSURE_HPA, - PRESSURE_INHG, SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -27,9 +22,6 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util -from homeassistant.util.distance import convert as convert_distance -from homeassistant.util.pressure import convert as convert_pressure -from homeassistant.util.speed import convert as convert_speed from .const import ATTRIBUTION, CONDITION_MAP, DEFAULT_NAME, DOMAIN, FORECAST_MAP @@ -54,12 +46,8 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( [ - MetEireannWeather( - coordinator, config_entry.data, hass.config.units.is_metric, False - ), - MetEireannWeather( - coordinator, config_entry.data, hass.config.units.is_metric, True - ), + MetEireannWeather(coordinator, config_entry.data, False), + MetEireannWeather(coordinator, config_entry.data, True), ] ) @@ -67,11 +55,15 @@ async def async_setup_entry( class MetEireannWeather(CoordinatorEntity, WeatherEntity): """Implementation of a Met Éireann weather condition.""" - def __init__(self, coordinator, config, is_metric, hourly): + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + + def __init__(self, coordinator, config, hourly): """Initialise the platform with a data instance and site.""" super().__init__(coordinator) self._config = config - self._is_metric = is_metric self._hourly = hourly @property @@ -109,23 +101,14 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): ) @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data.current_weather_data.get("temperature") @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" - pressure_hpa = self.coordinator.data.current_weather_data.get("pressure") - if self._is_metric or pressure_hpa is None: - return pressure_hpa - - return round(convert_pressure(pressure_hpa, PRESSURE_HPA, PRESSURE_INHG), 2) + return self.coordinator.data.current_weather_data.get("pressure") @property def humidity(self): @@ -133,16 +116,9 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): return self.coordinator.data.current_weather_data.get("humidity") @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" - speed_m_s = self.coordinator.data.current_weather_data.get("wind_speed") - if self._is_metric or speed_m_s is None: - return speed_m_s - - speed_mi_h = convert_speed( - speed_m_s, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR - ) - return int(round(speed_mi_h)) + return self.coordinator.data.current_weather_data.get("wind_speed") @property def wind_bearing(self): @@ -161,7 +137,7 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): me_forecast = self.coordinator.data.hourly_forecast else: me_forecast = self.coordinator.data.daily_forecast - required_keys = {ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME} + required_keys = {"temperature", "datetime"} ha_forecast = [] @@ -171,13 +147,6 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): ha_item = { k: item[v] for k, v in FORECAST_MAP.items() if item.get(v) is not None } - if not self._is_metric and ATTR_FORECAST_PRECIPITATION in ha_item: - precip_inches = convert_distance( - ha_item[ATTR_FORECAST_PRECIPITATION], - LENGTH_MILLIMETERS, - LENGTH_INCHES, - ) - ha_item[ATTR_FORECAST_PRECIPITATION] = round(precip_inches, 2) if ha_item.get(ATTR_FORECAST_CONDITION): ha_item[ATTR_FORECAST_CONDITION] = format_condition( ha_item[ATTR_FORECAST_CONDITION] From dd57d7d77f7d960966d04352269707c8f23e8e71 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Mon, 4 Jul 2022 14:24:21 +0200 Subject: [PATCH 2123/3516] Support unload for multiple adguard entries (#74360) --- homeassistant/components/adguard/__init__.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 1f2645e227c..2a244a5fe80 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -115,14 +115,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload AdGuard Home config entry.""" - hass.services.async_remove(DOMAIN, SERVICE_ADD_URL) - hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL) - hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL) - hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL) - hass.services.async_remove(DOMAIN, SERVICE_REFRESH) - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + if not hass.data[DOMAIN]: + hass.services.async_remove(DOMAIN, SERVICE_ADD_URL) + hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL) + hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL) + hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL) + hass.services.async_remove(DOMAIN, SERVICE_REFRESH) del hass.data[DOMAIN] return unload_ok From 6c3baf03aa8e1bb42ac76da1cc0b5afb222d2c41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Jul 2022 07:58:35 -0500 Subject: [PATCH 2124/3516] Make dispatcher setup lazy (#74374) --- homeassistant/helpers/dispatcher.py | 40 ++++++++++++---------- tests/components/cast/test_media_player.py | 12 ++++--- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index d1f7b2b97f9..12a6616b009 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -41,26 +41,13 @@ def async_dispatcher_connect( """ if DATA_DISPATCHER not in hass.data: hass.data[DATA_DISPATCHER] = {} - - job = HassJob( - catch_log_exception( - target, - lambda *args: "Exception in {} when dispatching '{}': {}".format( - # Functions wrapped in partial do not have a __name__ - getattr(target, "__name__", None) or str(target), - signal, - args, - ), - ) - ) - - hass.data[DATA_DISPATCHER].setdefault(signal, []).append(job) + hass.data[DATA_DISPATCHER].setdefault(signal, {})[target] = None @callback def async_remove_dispatcher() -> None: """Remove signal listener.""" try: - hass.data[DATA_DISPATCHER][signal].remove(job) + del hass.data[DATA_DISPATCHER][signal][target] except (KeyError, ValueError): # KeyError is key target listener did not exist # ValueError if listener did not exist within signal @@ -75,6 +62,21 @@ def dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: hass.loop.call_soon_threadsafe(async_dispatcher_send, hass, signal, *args) +def _generate_job(signal: str, target: Callable[..., Any]) -> HassJob: + """Generate a HassJob for a signal and target.""" + return HassJob( + catch_log_exception( + target, + lambda *args: "Exception in {} when dispatching '{}': {}".format( + # Functions wrapped in partial do not have a __name__ + getattr(target, "__name__", None) or str(target), + signal, + args, + ), + ) + ) + + @callback @bind_hass def async_dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: @@ -82,7 +84,9 @@ def async_dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: This method must be run in the event loop. """ - target_list = hass.data.get(DATA_DISPATCHER, {}).get(signal, []) - - for job in target_list: + target_list = hass.data.get(DATA_DISPATCHER, {}).get(signal, {}) + for target, job in target_list.items(): + if job is None: + job = _generate_job(signal, target) + target_list[target] = job hass.async_add_hass_job(job, *args) diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 00626cc8c16..61a6067578a 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -311,7 +311,7 @@ async def test_internal_discovery_callback_fill_out_group_fail( await hass.async_block_till_done() # when called with incomplete info, it should use HTTP to get missing - discover = signal.mock_calls[0][1][0] + discover = signal.mock_calls[-1][1][0] assert discover == full_info get_multizone_status_mock.assert_called_once() @@ -352,7 +352,7 @@ async def test_internal_discovery_callback_fill_out_group( await hass.async_block_till_done() # when called with incomplete info, it should use HTTP to get missing - discover = signal.mock_calls[0][1][0] + discover = signal.mock_calls[-1][1][0] assert discover == full_info get_multizone_status_mock.assert_called_once() @@ -423,23 +423,25 @@ async def test_internal_discovery_callback_fill_out_cast_type_manufacturer( # when called with incomplete info, it should use HTTP to get missing get_cast_type_mock.assert_called_once() assert get_cast_type_mock.call_count == 1 - discover = signal.mock_calls[0][1][0] + discover = signal.mock_calls[2][1][0] assert discover == full_info assert "Fetched cast details for unknown model 'Chromecast'" in caplog.text + signal.reset_mock() # Call again, the model name should be fetched from cache discover_cast(FAKE_MDNS_SERVICE, info) await hass.async_block_till_done() assert get_cast_type_mock.call_count == 1 # No additional calls - discover = signal.mock_calls[1][1][0] + discover = signal.mock_calls[0][1][0] assert discover == full_info + signal.reset_mock() # Call for another model, need to call HTTP again get_cast_type_mock.return_value = full_info2.cast_info discover_cast(FAKE_MDNS_SERVICE, info2) await hass.async_block_till_done() assert get_cast_type_mock.call_count == 2 - discover = signal.mock_calls[2][1][0] + discover = signal.mock_calls[0][1][0] assert discover == full_info2 From 402a40c1080d33ad9921150833e6b0337fe1420a Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Mon, 4 Jul 2022 14:59:24 +0200 Subject: [PATCH 2125/3516] Remove explicit use of mock_zeroconf in devolo Home Network (#74390) --- tests/components/devolo_home_network/test_init.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/components/devolo_home_network/test_init.py b/tests/components/devolo_home_network/test_init.py index 4f0c5b3fb58..1d15f337c17 100644 --- a/tests/components/devolo_home_network/test_init.py +++ b/tests/components/devolo_home_network/test_init.py @@ -12,7 +12,6 @@ from . import configure_integration @pytest.mark.usefixtures("mock_device") -@pytest.mark.usefixtures("mock_zeroconf") async def test_setup_entry(hass: HomeAssistant): """Test setup entry.""" entry = configure_integration(hass) @@ -24,7 +23,6 @@ async def test_setup_entry(hass: HomeAssistant): assert entry.state is ConfigEntryState.LOADED -@pytest.mark.usefixtures("mock_zeroconf") async def test_setup_device_not_found(hass: HomeAssistant): """Test setup entry.""" entry = configure_integration(hass) @@ -37,7 +35,6 @@ async def test_setup_device_not_found(hass: HomeAssistant): @pytest.mark.usefixtures("mock_device") -@pytest.mark.usefixtures("mock_zeroconf") async def test_unload_entry(hass: HomeAssistant): """Test unload entry.""" entry = configure_integration(hass) @@ -48,7 +45,6 @@ async def test_unload_entry(hass: HomeAssistant): @pytest.mark.usefixtures("mock_device") -@pytest.mark.usefixtures("mock_zeroconf") async def test_hass_stop(hass: HomeAssistant): """Test homeassistant stop event.""" entry = configure_integration(hass) From 5f5f1343cd231e6a98638096550066c64bd8daaf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 15:12:45 +0200 Subject: [PATCH 2126/3516] Migrate accuweather weather to native_* (#74407) --- .../components/accuweather/weather.py | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 536f66a3cb9..ae1824aef4a 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -6,19 +6,26 @@ from typing import Any, cast from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, Forecast, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_NAME, + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_MILES, + LENGTH_MILLIMETERS, + PRESSURE_HPA, + PRESSURE_INHG, + SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR, TEMP_CELSIUS, TEMP_FAHRENHEIT, @@ -66,19 +73,25 @@ class AccuWeatherEntity( ) -> None: """Initialize.""" super().__init__(coordinator) - self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL - wind_speed_unit = self.coordinator.data["Wind"]["Speed"][self._unit_system][ - "Unit" - ] - if wind_speed_unit == "mi/h": - self._attr_wind_speed_unit = SPEED_MILES_PER_HOUR + # Coordinator data is used also for sensors which don't have units automatically + # converted, hence the weather entity's native units follow the configured unit + # system + if coordinator.is_metric: + self._attr_native_precipitation_unit = LENGTH_MILLIMETERS + self._attr_native_pressure_unit = PRESSURE_HPA + self._attr_native_temperature_unit = TEMP_CELSIUS + self._attr_native_visibility_unit = LENGTH_KILOMETERS + self._attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + self._unit_system = API_METRIC else: - self._attr_wind_speed_unit = wind_speed_unit + self._unit_system = API_IMPERIAL + self._attr_native_precipitation_unit = LENGTH_INCHES + self._attr_native_pressure_unit = PRESSURE_INHG + self._attr_native_temperature_unit = TEMP_FAHRENHEIT + self._attr_native_visibility_unit = LENGTH_MILES + self._attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR self._attr_name = name self._attr_unique_id = coordinator.location_key - self._attr_temperature_unit = ( - TEMP_CELSIUS if coordinator.is_metric else TEMP_FAHRENHEIT - ) self._attr_attribution = ATTRIBUTION self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, @@ -106,14 +119,14 @@ class AccuWeatherEntity( return None @property - def temperature(self) -> float: + def native_temperature(self) -> float: """Return the temperature.""" return cast( float, self.coordinator.data["Temperature"][self._unit_system]["Value"] ) @property - def pressure(self) -> float: + def native_pressure(self) -> float: """Return the pressure.""" return cast( float, self.coordinator.data["Pressure"][self._unit_system]["Value"] @@ -125,7 +138,7 @@ class AccuWeatherEntity( return cast(int, self.coordinator.data["RelativeHumidity"]) @property - def wind_speed(self) -> float: + def native_wind_speed(self) -> float: """Return the wind speed.""" return cast( float, self.coordinator.data["Wind"]["Speed"][self._unit_system]["Value"] @@ -137,7 +150,7 @@ class AccuWeatherEntity( return cast(int, self.coordinator.data["Wind"]["Direction"]["Degrees"]) @property - def visibility(self) -> float: + def native_visibility(self) -> float: """Return the visibility.""" return cast( float, self.coordinator.data["Visibility"][self._unit_system]["Value"] @@ -162,9 +175,9 @@ class AccuWeatherEntity( return [ { ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(), - ATTR_FORECAST_TEMP: item["TemperatureMax"]["Value"], - ATTR_FORECAST_TEMP_LOW: item["TemperatureMin"]["Value"], - ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(item), + ATTR_FORECAST_NATIVE_TEMP: item["TemperatureMax"]["Value"], + ATTR_FORECAST_NATIVE_TEMP_LOW: item["TemperatureMin"]["Value"], + ATTR_FORECAST_NATIVE_PRECIPITATION: self._calc_precipitation(item), ATTR_FORECAST_PRECIPITATION_PROBABILITY: round( mean( [ @@ -173,7 +186,7 @@ class AccuWeatherEntity( ] ) ), - ATTR_FORECAST_WIND_SPEED: item["WindDay"]["Speed"]["Value"], + ATTR_FORECAST_NATIVE_WIND_SPEED: item["WindDay"]["Speed"]["Value"], ATTR_FORECAST_WIND_BEARING: item["WindDay"]["Direction"]["Degrees"], ATTR_FORECAST_CONDITION: [ k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v From 4e1359e2cc8676b9021d02164371a164b059727c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 15:18:57 +0200 Subject: [PATCH 2127/3516] Migrate ipma weather to native_* (#74387) --- homeassistant/components/ipma/weather.py | 33 ++++++++++++------------ 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index 731c3d7fb60..dd585b88802 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -25,12 +25,12 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, PLATFORM_SCHEMA, WeatherEntity, ) @@ -40,6 +40,8 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_MODE, CONF_NAME, + PRESSURE_HPA, + SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback @@ -174,6 +176,10 @@ async def async_get_location(hass, api, latitude, longitude): class IPMAWeather(WeatherEntity): """Representation of a weather condition.""" + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__(self, location: Location, api: IPMA_API, config): """Initialise the platform with a data instance and station name.""" self._api = api @@ -237,7 +243,7 @@ class IPMAWeather(WeatherEntity): ) @property - def temperature(self): + def native_temperature(self): """Return the current temperature.""" if not self._observation: return None @@ -245,7 +251,7 @@ class IPMAWeather(WeatherEntity): return self._observation.temperature @property - def pressure(self): + def native_pressure(self): """Return the current pressure.""" if not self._observation: return None @@ -261,7 +267,7 @@ class IPMAWeather(WeatherEntity): return self._observation.humidity @property - def wind_speed(self): + def native_wind_speed(self): """Return the current windspeed.""" if not self._observation: return None @@ -276,11 +282,6 @@ class IPMAWeather(WeatherEntity): return self._observation.wind_direction - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def forecast(self): """Return the forecast array.""" @@ -307,13 +308,13 @@ class IPMAWeather(WeatherEntity): ), None, ), - ATTR_FORECAST_TEMP: float(data_in.feels_like_temperature), + ATTR_FORECAST_NATIVE_TEMP: float(data_in.feels_like_temperature), ATTR_FORECAST_PRECIPITATION_PROBABILITY: ( int(float(data_in.precipitation_probability)) if int(float(data_in.precipitation_probability)) >= 0 else None ), - ATTR_FORECAST_WIND_SPEED: data_in.wind_strength, + ATTR_FORECAST_NATIVE_WIND_SPEED: data_in.wind_strength, ATTR_FORECAST_WIND_BEARING: data_in.wind_direction, } for data_in in forecast_filtered @@ -331,10 +332,10 @@ class IPMAWeather(WeatherEntity): ), None, ), - ATTR_FORECAST_TEMP_LOW: data_in.min_temperature, - ATTR_FORECAST_TEMP: data_in.max_temperature, + ATTR_FORECAST_NATIVE_TEMP_LOW: data_in.min_temperature, + ATTR_FORECAST_NATIVE_TEMP: data_in.max_temperature, ATTR_FORECAST_PRECIPITATION_PROBABILITY: data_in.precipitation_probability, - ATTR_FORECAST_WIND_SPEED: data_in.wind_strength, + ATTR_FORECAST_NATIVE_WIND_SPEED: data_in.wind_strength, ATTR_FORECAST_WIND_BEARING: data_in.wind_direction, } for data_in in forecast_filtered From 8d0e54d7762ab664c5ed0836bb3e76546a60bea4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Jul 2022 08:41:23 -0500 Subject: [PATCH 2128/3516] Use the orjson equivalent default encoder when save_json is passed the default encoder (#74377) --- homeassistant/util/json.py | 27 +++++++++++++++++++++++++-- tests/util/test_json.py | 18 +++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index d69a4106728..68273c89743 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -11,6 +11,10 @@ import orjson from homeassistant.core import Event, State from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.json import ( + JSONEncoder as DefaultHASSJSONEncoder, + json_encoder_default as default_hass_orjson_encoder, +) from .file import write_utf8_file, write_utf8_file_atomic @@ -52,6 +56,15 @@ def _orjson_encoder(data: Any) -> str: ).decode("utf-8") +def _orjson_default_encoder(data: Any) -> str: + """JSON encoder that uses orjson with hass defaults.""" + return orjson.dumps( + data, + option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS, + default=default_hass_orjson_encoder, + ).decode("utf-8") + + def save_json( filename: str, data: list | dict, @@ -64,10 +77,20 @@ def save_json( Returns True on success. """ - dump: Callable[[Any], Any] = json.dumps + dump: Callable[[Any], Any] try: if encoder: - json_data = json.dumps(data, indent=2, cls=encoder) + # For backwards compatibility, if they pass in the + # default json encoder we use _orjson_default_encoder + # which is the orjson equivalent to the default encoder. + if encoder is DefaultHASSJSONEncoder: + dump = _orjson_default_encoder + json_data = _orjson_default_encoder(data) + # If they pass a custom encoder that is not the + # DefaultHASSJSONEncoder, we use the slow path of json.dumps + else: + dump = json.dumps + json_data = json.dumps(data, indent=2, cls=encoder) else: dump = _orjson_encoder json_data = _orjson_encoder(data) diff --git a/tests/util/test_json.py b/tests/util/test_json.py index 9974cbb9628..28d321036c5 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -5,12 +5,13 @@ from json import JSONEncoder, dumps import math import os from tempfile import mkdtemp -from unittest.mock import Mock +from unittest.mock import Mock, patch import pytest from homeassistant.core import Event, State from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.json import JSONEncoder as DefaultHASSJSONEncoder from homeassistant.helpers.template import TupleWrapper from homeassistant.util.json import ( SerializationError, @@ -127,6 +128,21 @@ def test_custom_encoder(): assert data == "9" +def test_default_encoder_is_passed(): + """Test we use orjson if they pass in the default encoder.""" + fname = _path_for("test6") + with patch( + "homeassistant.util.json.orjson.dumps", return_value=b"{}" + ) as mock_orjson_dumps: + save_json(fname, {"any": 1}, encoder=DefaultHASSJSONEncoder) + assert len(mock_orjson_dumps.mock_calls) == 1 + # Patch json.dumps to make sure we are using the orjson path + with patch("homeassistant.util.json.json.dumps", side_effect=Exception): + save_json(fname, {"any": {1}}, encoder=DefaultHASSJSONEncoder) + data = load_json(fname) + assert data == {"any": [1]} + + def test_find_unserializable_data(): """Find unserializeable data.""" assert find_paths_unserializable_data(1) == {} From 18840c8af59bfd12c262ca1c6bb68a4cb5f0445c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Jul 2022 16:43:53 +0200 Subject: [PATCH 2129/3516] Add instance attributes to GeolocationEvent (#74389) --- .../components/geo_location/__init__.py | 25 +++++++++++-------- tests/components/geo_location/test_init.py | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index e58391ca84a..54542fa8503 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import final +from typing import Any, final from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE @@ -54,8 +54,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class GeolocationEvent(Entity): """Base class for an external event with an associated geolocation.""" + # Entity Properties + _attr_source: str + _attr_distance: float | None = None + _attr_latitude: float | None = None + _attr_longitude: float | None = None + + @final @property - def state(self): + def state(self) -> float | None: """Return the state of the sensor.""" if self.distance is not None: return round(self.distance, 1) @@ -64,32 +71,30 @@ class GeolocationEvent(Entity): @property def source(self) -> str: """Return source value of this external event.""" - raise NotImplementedError + return self._attr_source @property def distance(self) -> float | None: """Return distance value of this external event.""" - return None + return self._attr_distance @property def latitude(self) -> float | None: """Return latitude value of this external event.""" - return None + return self._attr_latitude @property def longitude(self) -> float | None: """Return longitude value of this external event.""" - return None + return self._attr_longitude @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, Any]: """Return the state attributes of this external event.""" - data = {} + data: dict[str, Any] = {ATTR_SOURCE: self.source} if self.latitude is not None: data[ATTR_LATITUDE] = round(self.latitude, 5) if self.longitude is not None: data[ATTR_LONGITUDE] = round(self.longitude, 5) - if self.source is not None: - data[ATTR_SOURCE] = self.source return data diff --git a/tests/components/geo_location/test_init.py b/tests/components/geo_location/test_init.py index 00cb2a872d2..f2cb5c6b108 100644 --- a/tests/components/geo_location/test_init.py +++ b/tests/components/geo_location/test_init.py @@ -20,5 +20,5 @@ async def test_event(hass): assert entity.distance is None assert entity.latitude is None assert entity.longitude is None - with pytest.raises(NotImplementedError): + with pytest.raises(AttributeError): assert entity.source is None From 035e96a79b29e49537a685eaa811eb49de8da513 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Jul 2022 16:46:59 +0200 Subject: [PATCH 2130/3516] Remove system_health from mypy ignore list (#74415) --- .../components/system_health/__init__.py | 26 +++++++++++-------- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/system_health/__init__.py b/homeassistant/components/system_health/__init__.py index be7232245e1..4821537fc8b 100644 --- a/homeassistant/components/system_health/__init__.py +++ b/homeassistant/components/system_health/__init__.py @@ -6,6 +6,7 @@ from collections.abc import Awaitable, Callable import dataclasses from datetime import datetime import logging +from typing import Any import aiohttp import async_timeout @@ -29,7 +30,7 @@ INFO_CALLBACK_TIMEOUT = 5 def async_register_info( hass: HomeAssistant, domain: str, - info_callback: Callable[[HomeAssistant], dict], + info_callback: Callable[[HomeAssistant], Awaitable[dict]], ): """Register an info callback. @@ -61,9 +62,10 @@ async def _register_system_health_platform(hass, integration_domain, platform): async def get_integration_info( hass: HomeAssistant, registration: SystemHealthRegistration -): +) -> dict[str, Any]: """Get integration system health.""" try: + assert registration.info_callback async with async_timeout.timeout(INFO_CALLBACK_TIMEOUT): data = await registration.info_callback(hass) except asyncio.TimeoutError: @@ -72,7 +74,7 @@ async def get_integration_info( _LOGGER.exception("Error fetching info") data = {"error": {"type": "failed", "error": "unknown"}} - result = {"info": data} + result: dict[str, Any] = {"info": data} if registration.manage_url: result["manage_url"] = registration.manage_url @@ -88,15 +90,15 @@ def _format_value(val): return val -@websocket_api.async_response @websocket_api.websocket_command({vol.Required("type"): "system_health/info"}) +@websocket_api.async_response async def handle_info( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict -): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: """Handle an info request via a subscription.""" registrations: dict[str, SystemHealthRegistration] = hass.data[DOMAIN] data = {} - pending_info = {} + pending_info: dict[tuple[str, str], asyncio.Task] = {} for domain, domain_data in zip( registrations, @@ -138,7 +140,10 @@ async def handle_info( ) return - tasks = [asyncio.create_task(stop_event.wait()), *pending_info.values()] + tasks: set[asyncio.Task] = { + asyncio.create_task(stop_event.wait()), + *pending_info.values(), + } pending_lookup = {val: key for key, val in pending_info.items()} # One task is the stop_event.wait() and is always there @@ -160,8 +165,7 @@ async def handle_info( "key": key, } - if result.exception(): - exception = result.exception() + if exception := result.exception(): _LOGGER.error( "Error fetching system info for %s - %s", domain, @@ -206,7 +210,7 @@ class SystemHealthRegistration: async def async_check_can_reach_url( hass: HomeAssistant, url: str, more_info: str | None = None -) -> str: +) -> str | dict[str, str]: """Test if the url can be reached.""" session = aiohttp_client.async_get_clientsession(hass) diff --git a/mypy.ini b/mypy.ini index fb67983a31c..cab794e7bfe 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2900,9 +2900,6 @@ ignore_errors = true [mypy-homeassistant.components.sonos.statistics] ignore_errors = true -[mypy-homeassistant.components.system_health] -ignore_errors = true - [mypy-homeassistant.components.telegram_bot.polling] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index bbb628a76bb..94bb2682f3e 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -112,7 +112,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.sonos.sensor", "homeassistant.components.sonos.speaker", "homeassistant.components.sonos.statistics", - "homeassistant.components.system_health", "homeassistant.components.telegram_bot.polling", "homeassistant.components.template.number", "homeassistant.components.template.sensor", From e3bd63934b9c31df55399cfb007d0895da464b8e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Jul 2022 16:47:35 +0200 Subject: [PATCH 2131/3516] Remove gree from mypy ignore list (#74411) --- homeassistant/components/gree/climate.py | 8 ++++---- mypy.ini | 6 ------ script/hassfest/mypy_config.py | 2 -- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 0d1c7d53f8b..2b6833dff2c 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -198,14 +198,14 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): return TARGET_TEMPERATURE_STEP @property - def hvac_mode(self) -> str: + def hvac_mode(self) -> HVACMode | None: """Return the current HVAC mode for the device.""" if not self.coordinator.device.power: return HVACMode.OFF return HVAC_MODES.get(self.coordinator.device.mode) - async def async_set_hvac_mode(self, hvac_mode) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" if hvac_mode not in self.hvac_modes: raise ValueError(f"Invalid hvac_mode: {hvac_mode}") @@ -246,7 +246,7 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): self.async_write_ha_state() @property - def hvac_modes(self) -> list[str]: + def hvac_modes(self) -> list[HVACMode]: """Return the HVAC modes support by the device.""" modes = [*HVAC_MODES_REVERSE] modes.append(HVACMode.OFF) @@ -299,7 +299,7 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): return PRESET_MODES @property - def fan_mode(self) -> str: + def fan_mode(self) -> str | None: """Return the current fan mode for the device.""" speed = self.coordinator.device.fan_speed return FAN_MODES.get(speed) diff --git a/mypy.ini b/mypy.ini index cab794e7bfe..22a6fd801c3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2663,12 +2663,6 @@ ignore_errors = true [mypy-homeassistant.components.google_assistant.trait] ignore_errors = true -[mypy-homeassistant.components.gree.climate] -ignore_errors = true - -[mypy-homeassistant.components.gree.switch] -ignore_errors = true - [mypy-homeassistant.components.harmony] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 94bb2682f3e..e9e78c2892b 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -33,8 +33,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.google_assistant.http", "homeassistant.components.google_assistant.report_state", "homeassistant.components.google_assistant.trait", - "homeassistant.components.gree.climate", - "homeassistant.components.gree.switch", "homeassistant.components.harmony", "homeassistant.components.harmony.config_flow", "homeassistant.components.harmony.data", From fde829c4f0e3b0924bba34af051b2df8a885a90b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 16:59:36 +0200 Subject: [PATCH 2132/3516] Correct climacell weather migration to native_* (#74409) --- homeassistant/components/climacell/weather.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index 2b284114981..6aee9b54f6c 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -10,13 +10,13 @@ from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry @@ -135,12 +135,12 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): data = { ATTR_FORECAST_TIME: forecast_dt.isoformat(), ATTR_FORECAST_CONDITION: translated_condition, - ATTR_FORECAST_PRECIPITATION: precipitation, + ATTR_FORECAST_NATIVE_PRECIPITATION: precipitation, ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability, - ATTR_FORECAST_TEMP: temp, - ATTR_FORECAST_TEMP_LOW: temp_low, + ATTR_FORECAST_NATIVE_TEMP: temp, + ATTR_FORECAST_NATIVE_TEMP_LOW: temp_low, ATTR_FORECAST_WIND_BEARING: wind_direction, - ATTR_FORECAST_WIND_SPEED: wind_speed, + ATTR_FORECAST_NATIVE_WIND_SPEED: wind_speed, } return {k: v for k, v in data.items() if v is not None} @@ -224,7 +224,7 @@ class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity): return CONDITIONS_V3[condition] @property - def temperature(self): + def native_temperature(self): """Return the platform temperature.""" return self._get_cc_value( self.coordinator.data[CURRENT], CC_V3_ATTR_TEMPERATURE From b082764e300970ba990bd3cf672e06a924b73eac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Jul 2022 10:36:56 -0500 Subject: [PATCH 2133/3516] Bump rflink to 0.0.63 (#74417) --- homeassistant/components/rflink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json index debc12ae4e0..6cef409a736 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -2,7 +2,7 @@ "domain": "rflink", "name": "RFLink", "documentation": "https://www.home-assistant.io/integrations/rflink", - "requirements": ["rflink==0.0.62"], + "requirements": ["rflink==0.0.63"], "codeowners": ["@javicalle"], "iot_class": "assumed_state", "loggers": ["rflink"] diff --git a/requirements_all.txt b/requirements_all.txt index 4e910c36412..2cae4c3846a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2080,7 +2080,7 @@ restrictedpython==5.2 rfk101py==0.0.1 # homeassistant.components.rflink -rflink==0.0.62 +rflink==0.0.63 # homeassistant.components.ring ring_doorbell==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd3756a1c80..39bdf05db89 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1391,7 +1391,7 @@ renault-api==0.1.11 restrictedpython==5.2 # homeassistant.components.rflink -rflink==0.0.62 +rflink==0.0.63 # homeassistant.components.ring ring_doorbell==0.7.2 From b3fec4c40147d98bc74a9047d8f34ba803ab5413 Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Mon, 4 Jul 2022 17:12:41 +0100 Subject: [PATCH 2134/3516] Typehints and cleanup for metoffice (#74338) * Typehints and cleanup for metoffice * add myself as owner --- .strict-typing | 1 + CODEOWNERS | 4 +- .../components/metoffice/config_flow.py | 12 ++- homeassistant/components/metoffice/const.py | 2 +- homeassistant/components/metoffice/data.py | 16 ++-- homeassistant/components/metoffice/helpers.py | 14 ++-- .../components/metoffice/manifest.json | 2 +- homeassistant/components/metoffice/sensor.py | 43 ++++++----- homeassistant/components/metoffice/weather.py | 73 ++++++++++++------- mypy.ini | 11 +++ 10 files changed, 115 insertions(+), 63 deletions(-) diff --git a/.strict-typing b/.strict-typing index 1832a83641a..ebfed5dfa5b 100644 --- a/.strict-typing +++ b/.strict-typing @@ -153,6 +153,7 @@ homeassistant.components.luftdaten.* homeassistant.components.mailbox.* homeassistant.components.media_player.* homeassistant.components.media_source.* +homeassistant.components.metoffice.* homeassistant.components.mjpeg.* homeassistant.components.modbus.* homeassistant.components.modem_callerid.* diff --git a/CODEOWNERS b/CODEOWNERS index 6ae1c9605db..9f53aeab34e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -630,8 +630,8 @@ build.json @home-assistant/supervisor /homeassistant/components/meteoalarm/ @rolfberkenbosch /homeassistant/components/meteoclimatic/ @adrianmo /tests/components/meteoclimatic/ @adrianmo -/homeassistant/components/metoffice/ @MrHarcombe -/tests/components/metoffice/ @MrHarcombe +/homeassistant/components/metoffice/ @MrHarcombe @avee87 +/tests/components/metoffice/ @MrHarcombe @avee87 /homeassistant/components/miflora/ @danielhiversen @basnijholt /homeassistant/components/mikrotik/ @engrbm87 /tests/components/mikrotik/ @engrbm87 diff --git a/homeassistant/components/metoffice/config_flow.py b/homeassistant/components/metoffice/config_flow.py index 959680a90ec..3cf3b0fcda0 100644 --- a/homeassistant/components/metoffice/config_flow.py +++ b/homeassistant/components/metoffice/config_flow.py @@ -1,11 +1,15 @@ """Config flow for Met Office integration.""" +from __future__ import annotations + import logging +from typing import Any import datapoint import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from .const import DOMAIN @@ -14,7 +18,9 @@ from .helpers import fetch_site _LOGGER = logging.getLogger(__name__) -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input( + hass: core.HomeAssistant, data: dict[str, Any] +) -> dict[str, str]: """Validate that the user input allows us to connect to DataPoint. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -40,7 +46,9 @@ class MetOfficeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/metoffice/const.py b/homeassistant/components/metoffice/const.py index e413b102898..12f88cc6d56 100644 --- a/homeassistant/components/metoffice/const.py +++ b/homeassistant/components/metoffice/const.py @@ -37,7 +37,7 @@ MODE_3HOURLY_LABEL = "3-Hourly" MODE_DAILY = "daily" MODE_DAILY_LABEL = "Daily" -CONDITION_CLASSES = { +CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_CLEAR_NIGHT: ["0"], ATTR_CONDITION_CLOUDY: ["7", "8"], ATTR_CONDITION_FOG: ["5", "6"], diff --git a/homeassistant/components/metoffice/data.py b/homeassistant/components/metoffice/data.py index 607c09e90b6..4b2741ce0fb 100644 --- a/homeassistant/components/metoffice/data.py +++ b/homeassistant/components/metoffice/data.py @@ -1,11 +1,17 @@ """Common Met Office Data class used by both sensor and entity.""" +from dataclasses import dataclass + +from datapoint.Forecast import Forecast +from datapoint.Site import Site +from datapoint.Timestep import Timestep + + +@dataclass class MetOfficeData: """Data structure for MetOffice weather and forecast.""" - def __init__(self, now, forecast, site): - """Initialize the data object.""" - self.now = now - self.forecast = forecast - self.site = site + now: Forecast + forecast: list[Timestep] + site: Site diff --git a/homeassistant/components/metoffice/helpers.py b/homeassistant/components/metoffice/helpers.py index 00d5e73501d..ecef7e5ddcb 100644 --- a/homeassistant/components/metoffice/helpers.py +++ b/homeassistant/components/metoffice/helpers.py @@ -1,8 +1,10 @@ """Helpers used for Met Office integration.""" +from __future__ import annotations import logging import datapoint +from datapoint.Site import Site from homeassistant.helpers.update_coordinator import UpdateFailed from homeassistant.util.dt import utcnow @@ -13,7 +15,9 @@ from .data import MetOfficeData _LOGGER = logging.getLogger(__name__) -def fetch_site(connection: datapoint.Manager, latitude, longitude): +def fetch_site( + connection: datapoint.Manager, latitude: float, longitude: float +) -> Site | None: """Fetch site information from Datapoint API.""" try: return connection.get_nearest_forecast_site( @@ -24,7 +28,7 @@ def fetch_site(connection: datapoint.Manager, latitude, longitude): return None -def fetch_data(connection: datapoint.Manager, site, mode) -> MetOfficeData: +def fetch_data(connection: datapoint.Manager, site: Site, mode: str) -> MetOfficeData: """Fetch weather and forecast from Datapoint API.""" try: forecast = connection.get_forecast_for_site(site.id, mode) @@ -34,8 +38,8 @@ def fetch_data(connection: datapoint.Manager, site, mode) -> MetOfficeData: else: time_now = utcnow() return MetOfficeData( - forecast.now(), - [ + now=forecast.now(), + forecast=[ timestep for day in forecast.days for timestep in day.timesteps @@ -44,5 +48,5 @@ def fetch_data(connection: datapoint.Manager, site, mode) -> MetOfficeData: mode == MODE_3HOURLY or timestep.date.hour > 6 ) # ensures only one result per day in MODE_DAILY ], - site, + site=site, ) diff --git a/homeassistant/components/metoffice/manifest.json b/homeassistant/components/metoffice/manifest.json index d38d2d8cffe..887ecb3578d 100644 --- a/homeassistant/components/metoffice/manifest.json +++ b/homeassistant/components/metoffice/manifest.json @@ -3,7 +3,7 @@ "name": "Met Office", "documentation": "https://www.home-assistant.io/integrations/metoffice", "requirements": ["datapoint==0.9.8"], - "codeowners": ["@MrHarcombe"], + "codeowners": ["@MrHarcombe", "@avee87"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["datapoint"] diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 699b137c55f..e24e2299be4 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -1,6 +1,10 @@ """Support for UK Met Office weather service.""" from __future__ import annotations +from typing import Any + +from datapoint.Element import Element + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -17,7 +21,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from . import get_device_info from .const import ( @@ -34,6 +41,7 @@ from .const import ( VISIBILITY_CLASSES, VISIBILITY_DISTANCE_CLASSES, ) +from .data import MetOfficeData ATTR_LAST_UPDATE = "last_update" ATTR_SENSOR_ID = "sensor_id" @@ -170,21 +178,24 @@ async def async_setup_entry( ) -class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): +class MetOfficeCurrentSensor( + CoordinatorEntity[DataUpdateCoordinator[MetOfficeData]], SensorEntity +): """Implementation of a Met Office current weather condition sensor.""" def __init__( self, - coordinator, - hass_data, - use_3hourly, + coordinator: DataUpdateCoordinator[MetOfficeData], + hass_data: dict[str, Any], + use_3hourly: bool, description: SensorEntityDescription, - ): + ) -> None: """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL + self._attr_device_info = get_device_info( coordinates=hass_data[METOFFICE_COORDINATES], name=hass_data[METOFFICE_NAME] ) @@ -192,11 +203,12 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): self._attr_unique_id = f"{description.name}_{hass_data[METOFFICE_COORDINATES]}" if not use_3hourly: self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" - - self.use_3hourly = use_3hourly + self._attr_entity_registry_enabled_default = ( + self.entity_description.entity_registry_enabled_default and use_3hourly + ) @property - def native_value(self): + def native_value(self) -> Any | None: """Return the state of the sensor.""" value = None @@ -224,13 +236,13 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): elif hasattr(self.coordinator.data.now, self.entity_description.key): value = getattr(self.coordinator.data.now, self.entity_description.key) - if hasattr(value, "value"): + if isinstance(value, Element): value = value.value return value @property - def icon(self): + def icon(self) -> str | None: """Return the icon for the entity card.""" value = self.entity_description.icon if self.entity_description.key == "weather": @@ -244,7 +256,7 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): return value @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the device.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, @@ -253,10 +265,3 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): ATTR_SITE_ID: self.coordinator.data.site.id, ATTR_SITE_NAME: self.coordinator.data.site.name, } - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return ( - self.entity_description.entity_registry_enabled_default and self.use_3hourly - ) diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index f4e0bf61d30..184782d4c12 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -1,18 +1,27 @@ """Support for UK Met Office weather service.""" +from __future__ import annotations + +from typing import Any + +from datapoint.Timestep import Timestep + from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + Forecast, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PRESSURE_HPA, SPEED_MILES_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from . import get_device_info from .const import ( @@ -28,6 +37,7 @@ from .const import ( MODE_DAILY, MODE_DAILY_LABEL, ) +from .data import MetOfficeData async def async_setup_entry( @@ -45,9 +55,8 @@ async def async_setup_entry( ) -def _build_forecast_data(timestep): - data = {} - data[ATTR_FORECAST_TIME] = timestep.date.isoformat() +def _build_forecast_data(timestep: Timestep) -> Forecast: + data = Forecast(datetime=timestep.date.isoformat()) if timestep.weather: data[ATTR_FORECAST_CONDITION] = _get_weather_condition(timestep.weather.value) if timestep.precipitation: @@ -61,21 +70,30 @@ def _build_forecast_data(timestep): return data -def _get_weather_condition(metoffice_code): +def _get_weather_condition(metoffice_code: str) -> str | None: for hass_name, metoffice_codes in CONDITION_CLASSES.items(): if metoffice_code in metoffice_codes: return hass_name return None -class MetOfficeWeather(CoordinatorEntity, WeatherEntity): +class MetOfficeWeather( + CoordinatorEntity[DataUpdateCoordinator[MetOfficeData]], WeatherEntity +): """Implementation of a Met Office weather condition.""" + _attr_attribution = ATTRIBUTION + _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_pressure_unit = PRESSURE_HPA _attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR - def __init__(self, coordinator, hass_data, use_3hourly): + def __init__( + self, + coordinator: DataUpdateCoordinator[MetOfficeData], + hass_data: dict[str, Any], + use_3hourly: bool, + ) -> None: """Initialise the platform with a data instance.""" super().__init__(coordinator) @@ -89,62 +107,61 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" @property - def condition(self): + def condition(self) -> str | None: """Return the current condition.""" if self.coordinator.data.now: return _get_weather_condition(self.coordinator.data.now.weather.value) return None @property - def native_temperature(self): + def native_temperature(self) -> float | None: """Return the platform temperature.""" - if self.coordinator.data.now.temperature: - return self.coordinator.data.now.temperature.value + weather_now = self.coordinator.data.now + if weather_now.temperature: + value = weather_now.temperature.value + return float(value) if value is not None else None return None @property - def native_pressure(self): + def native_pressure(self) -> float | None: """Return the mean sea-level pressure.""" weather_now = self.coordinator.data.now if weather_now and weather_now.pressure: - return weather_now.pressure.value + value = weather_now.pressure.value + return float(value) if value is not None else None return None @property - def humidity(self): + def humidity(self) -> float | None: """Return the relative humidity.""" weather_now = self.coordinator.data.now if weather_now and weather_now.humidity: - return weather_now.humidity.value + value = weather_now.humidity.value + return float(value) if value is not None else None return None @property - def native_wind_speed(self): + def native_wind_speed(self) -> float | None: """Return the wind speed.""" weather_now = self.coordinator.data.now if weather_now and weather_now.wind_speed: - return weather_now.wind_speed.value + value = weather_now.wind_speed.value + return float(value) if value is not None else None return None @property - def wind_bearing(self): + def wind_bearing(self) -> str | None: """Return the wind bearing.""" weather_now = self.coordinator.data.now if weather_now and weather_now.wind_direction: - return weather_now.wind_direction.value + value = weather_now.wind_direction.value + return str(value) if value is not None else None return None @property - def forecast(self): + def forecast(self) -> list[Forecast] | None: """Return the forecast array.""" - if self.coordinator.data.forecast is None: - return None return [ _build_forecast_data(timestep) for timestep in self.coordinator.data.forecast ] - - @property - def attribution(self): - """Return the attribution.""" - return ATTRIBUTION diff --git a/mypy.ini b/mypy.ini index 22a6fd801c3..77830664696 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1446,6 +1446,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.metoffice.*] +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.mjpeg.*] check_untyped_defs = true disallow_incomplete_defs = true From 1536936177cf685e8db073ba979443931f40cd18 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Jul 2022 20:39:14 +0200 Subject: [PATCH 2135/3516] Remove harmony from mypy ignore list (#74425) --- homeassistant/components/harmony/config_flow.py | 3 ++- homeassistant/components/harmony/data.py | 7 ++++--- mypy.ini | 9 --------- script/hassfest/mypy_config.py | 3 --- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index 16101f18cff..675acf600cb 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import logging +from typing import Any from urllib.parse import urlparse from aioharmony.hubconnector_websocket import HubConnector @@ -57,7 +58,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the Harmony config flow.""" - self.harmony_config = {} + self.harmony_config: dict[str, Any] = {} async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index aa373d5813a..fbbbbd38e3a 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -21,13 +21,14 @@ _LOGGER = logging.getLogger(__name__) class HarmonyData(HarmonySubscriberMixin): """HarmonyData registers for Harmony hub updates.""" - def __init__(self, hass, address: str, name: str, unique_id: str): + _client: HarmonyClient + + def __init__(self, hass, address: str, name: str, unique_id: str | None) -> None: """Initialize a data object.""" super().__init__(hass) self._name = name self._unique_id = unique_id self._available = False - self._client = None self._address = address @property @@ -99,7 +100,7 @@ class HarmonyData(HarmonySubscriberMixin): configuration_url="https://www.logitech.com/en-us/my-account", ) - async def connect(self) -> bool: + async def connect(self) -> None: """Connect to the Harmony Hub.""" _LOGGER.debug("%s: Connecting", self._name) diff --git a/mypy.ini b/mypy.ini index 77830664696..e7897368189 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2674,15 +2674,6 @@ ignore_errors = true [mypy-homeassistant.components.google_assistant.trait] ignore_errors = true -[mypy-homeassistant.components.harmony] -ignore_errors = true - -[mypy-homeassistant.components.harmony.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.harmony.data] -ignore_errors = true - [mypy-homeassistant.components.hassio] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index e9e78c2892b..f7b8bb1be2f 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -33,9 +33,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.google_assistant.http", "homeassistant.components.google_assistant.report_state", "homeassistant.components.google_assistant.trait", - "homeassistant.components.harmony", - "homeassistant.components.harmony.config_flow", - "homeassistant.components.harmony.data", "homeassistant.components.hassio", "homeassistant.components.hassio.auth", "homeassistant.components.hassio.binary_sensor", From e02574c6d911b31d2edd8ef56d59ea6acdfafd21 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Jul 2022 13:53:25 -0500 Subject: [PATCH 2136/3516] Bump pyunifiprotect to 4.0.9 (#74424) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index da82871d313..9aeb8b48050 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.8", "unifi-discovery==1.1.4"], + "requirements": ["pyunifiprotect==4.0.9", "unifi-discovery==1.1.4"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 2cae4c3846a..3c6a0ca8d63 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1996,7 +1996,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.8 +pyunifiprotect==4.0.9 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39bdf05db89..18f38d1d230 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1337,7 +1337,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.8 +pyunifiprotect==4.0.9 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 02a0b8b6494780db0e41015a873a1c3e341f324d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Jul 2022 20:59:52 +0200 Subject: [PATCH 2137/3516] Add more type hints to demo (#74408) --- homeassistant/components/demo/geo_location.py | 23 ++++-- homeassistant/components/demo/humidifier.py | 10 ++- .../components/demo/image_processing.py | 21 ++--- homeassistant/components/demo/switch.py | 6 +- homeassistant/components/demo/vacuum.py | 77 ++++++++++--------- homeassistant/components/demo/weather.py | 71 ++++++++--------- 6 files changed, 109 insertions(+), 99 deletions(-) diff --git a/homeassistant/components/demo/geo_location.py b/homeassistant/components/demo/geo_location.py index fae626f37b2..27935300959 100644 --- a/homeassistant/components/demo/geo_location.py +++ b/homeassistant/components/demo/geo_location.py @@ -54,15 +54,15 @@ def setup_platform( class DemoManager: """Device manager for demo geolocation events.""" - def __init__(self, hass, add_entities): + def __init__(self, hass: HomeAssistant, add_entities: AddEntitiesCallback) -> None: """Initialise the demo geolocation event manager.""" self._hass = hass self._add_entities = add_entities - self._managed_devices = [] + self._managed_devices: list[DemoGeolocationEvent] = [] self._update(count=NUMBER_OF_DEMO_DEVICES) self._init_regular_updates() - def _generate_random_event(self): + def _generate_random_event(self) -> DemoGeolocationEvent: """Generate a random event in vicinity of this HA instance.""" home_latitude = self._hass.config.latitude home_longitude = self._hass.config.longitude @@ -83,13 +83,13 @@ class DemoManager: event_name, radius_in_km, latitude, longitude, LENGTH_KILOMETERS ) - def _init_regular_updates(self): + def _init_regular_updates(self) -> None: """Schedule regular updates based on configured time interval.""" track_time_interval( self._hass, lambda now: self._update(), DEFAULT_UPDATE_INTERVAL ) - def _update(self, count=1): + def _update(self, count: int = 1) -> None: """Remove events and add new random events.""" # Remove devices. for _ in range(1, count + 1): @@ -112,7 +112,14 @@ class DemoManager: class DemoGeolocationEvent(GeolocationEvent): """This represents a demo geolocation event.""" - def __init__(self, name, distance, latitude, longitude, unit_of_measurement): + def __init__( + self, + name: str, + distance: float, + latitude: float, + longitude: float, + unit_of_measurement: str, + ) -> None: """Initialize entity with data provided.""" self._name = name self._distance = distance @@ -131,7 +138,7 @@ class DemoGeolocationEvent(GeolocationEvent): return self._name @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed for a demo geolocation event.""" return False @@ -151,6 +158,6 @@ class DemoGeolocationEvent(GeolocationEvent): return self._longitude @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement.""" return self._unit_of_measurement diff --git a/homeassistant/components/demo/humidifier.py b/homeassistant/components/demo/humidifier.py index c04c44cd8c5..c998a32ab55 100644 --- a/homeassistant/components/demo/humidifier.py +++ b/homeassistant/components/demo/humidifier.py @@ -1,6 +1,8 @@ """Demo platform that offers a fake humidifier device.""" from __future__ import annotations +from typing import Any + from homeassistant.components.humidifier import HumidifierDeviceClass, HumidifierEntity from homeassistant.components.humidifier.const import HumidifierEntityFeature from homeassistant.config_entries import ConfigEntry @@ -78,22 +80,22 @@ class DemoHumidifier(HumidifierEntity): self._attr_available_modes = available_modes self._attr_device_class = device_class - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._attr_is_on = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._attr_is_on = False self.async_write_ha_state() - async def async_set_humidity(self, humidity): + async def async_set_humidity(self, humidity: int) -> None: """Set new humidity level.""" self._attr_target_humidity = humidity self.async_write_ha_state() - async def async_set_mode(self, mode): + async def async_set_mode(self, mode: str) -> None: """Update mode.""" self._attr_mode = mode self.async_write_ha_state() diff --git a/homeassistant/components/demo/image_processing.py b/homeassistant/components/demo/image_processing.py index 070d8dcfa9c..58c884cc439 100644 --- a/homeassistant/components/demo/image_processing.py +++ b/homeassistant/components/demo/image_processing.py @@ -1,6 +1,7 @@ """Support for the demo image processing.""" from __future__ import annotations +from homeassistant.components.camera import Image from homeassistant.components.image_processing import ( ATTR_AGE, ATTR_CONFIDENCE, @@ -34,7 +35,7 @@ def setup_platform( class DemoImageProcessingAlpr(ImageProcessingAlprEntity): """Demo ALPR image processing entity.""" - def __init__(self, camera_entity, name): + def __init__(self, camera_entity: str, name: str) -> None: """Initialize demo ALPR image processing entity.""" super().__init__() @@ -42,21 +43,21 @@ class DemoImageProcessingAlpr(ImageProcessingAlprEntity): self._camera = camera_entity @property - def camera_entity(self): + def camera_entity(self) -> str: """Return camera entity id from process pictures.""" return self._camera @property - def confidence(self): + def confidence(self) -> int: """Return minimum confidence for send events.""" return 80 @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return self._name - def process_image(self, image): + def process_image(self, image: Image) -> None: """Process image.""" demo_data = { "AC3829": 98.3, @@ -71,7 +72,7 @@ class DemoImageProcessingAlpr(ImageProcessingAlprEntity): class DemoImageProcessingFace(ImageProcessingFaceEntity): """Demo face identify image processing entity.""" - def __init__(self, camera_entity, name): + def __init__(self, camera_entity: str, name: str) -> None: """Initialize demo face image processing entity.""" super().__init__() @@ -79,21 +80,21 @@ class DemoImageProcessingFace(ImageProcessingFaceEntity): self._camera = camera_entity @property - def camera_entity(self): + def camera_entity(self) -> str: """Return camera entity id from process pictures.""" return self._camera @property - def confidence(self): + def confidence(self) -> int: """Return minimum confidence for send events.""" return 80 @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" return self._name - def process_image(self, image): + def process_image(self, image: Image) -> None: """Process image.""" demo_data = [ { diff --git a/homeassistant/components/demo/switch.py b/homeassistant/components/demo/switch.py index 217119e9372..2ad400ff3f7 100644 --- a/homeassistant/components/demo/switch.py +++ b/homeassistant/components/demo/switch.py @@ -1,6 +1,8 @@ """Demo platform that has two fake switches.""" from __future__ import annotations +from typing import Any + from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DEVICE_DEFAULT_NAME @@ -69,12 +71,12 @@ class DemoSwitch(SwitchEntity): name=self.name, ) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self._attr_is_on = True self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._attr_is_on = False self.schedule_update_ha_state() diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index feda379558b..58b76ba6347 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -1,6 +1,9 @@ """Demo platform for the vacuum component.""" from __future__ import annotations +from datetime import datetime +from typing import Any + from homeassistant.components.vacuum import ( ATTR_CLEANED_AREA, STATE_CLEANING, @@ -101,62 +104,62 @@ async def async_setup_platform( class DemoVacuum(VacuumEntity): """Representation of a demo vacuum.""" - def __init__(self, name, supported_features): + def __init__(self, name: str, supported_features: int) -> None: """Initialize the vacuum.""" self._name = name self._supported_features = supported_features self._state = False self._status = "Charging" self._fan_speed = FAN_SPEEDS[1] - self._cleaned_area = 0 + self._cleaned_area: float = 0 self._battery_level = 100 @property - def name(self): + def name(self) -> str: """Return the name of the vacuum.""" return self._name @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed for a demo vacuum.""" return False @property - def is_on(self): + def is_on(self) -> bool: """Return true if vacuum is on.""" return self._state @property - def status(self): + def status(self) -> str: """Return the status of the vacuum.""" return self._status @property - def fan_speed(self): + def fan_speed(self) -> str: """Return the status of the vacuum.""" return self._fan_speed @property - def fan_speed_list(self): + def fan_speed_list(self) -> list[str]: """Return the status of the vacuum.""" return FAN_SPEEDS @property - def battery_level(self): + def battery_level(self) -> int: """Return the status of the vacuum.""" return max(0, min(100, self._battery_level)) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device state attributes.""" return {ATTR_CLEANED_AREA: round(self._cleaned_area, 2)} @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return self._supported_features - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the vacuum on.""" if self.supported_features & VacuumEntityFeature.TURN_ON == 0: return @@ -167,7 +170,7 @@ class DemoVacuum(VacuumEntity): self._status = "Cleaning" self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the vacuum off.""" if self.supported_features & VacuumEntityFeature.TURN_OFF == 0: return @@ -176,7 +179,7 @@ class DemoVacuum(VacuumEntity): self._status = "Charging" self.schedule_update_ha_state() - def stop(self, **kwargs): + def stop(self, **kwargs: Any) -> None: """Stop the vacuum.""" if self.supported_features & VacuumEntityFeature.STOP == 0: return @@ -185,7 +188,7 @@ class DemoVacuum(VacuumEntity): self._status = "Stopping the current task" self.schedule_update_ha_state() - def clean_spot(self, **kwargs): + def clean_spot(self, **kwargs: Any) -> None: """Perform a spot clean-up.""" if self.supported_features & VacuumEntityFeature.CLEAN_SPOT == 0: return @@ -196,7 +199,7 @@ class DemoVacuum(VacuumEntity): self._status = "Cleaning spot" self.schedule_update_ha_state() - def locate(self, **kwargs): + def locate(self, **kwargs: Any) -> None: """Locate the vacuum (usually by playing a song).""" if self.supported_features & VacuumEntityFeature.LOCATE == 0: return @@ -204,7 +207,7 @@ class DemoVacuum(VacuumEntity): self._status = "Hi, I'm over here!" self.schedule_update_ha_state() - def start_pause(self, **kwargs): + def start_pause(self, **kwargs: Any) -> None: """Start, pause or resume the cleaning task.""" if self.supported_features & VacuumEntityFeature.PAUSE == 0: return @@ -218,7 +221,7 @@ class DemoVacuum(VacuumEntity): self._status = "Pausing the current task" self.schedule_update_ha_state() - def set_fan_speed(self, fan_speed, **kwargs): + def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set the vacuum's fan speed.""" if self.supported_features & VacuumEntityFeature.FAN_SPEED == 0: return @@ -227,7 +230,7 @@ class DemoVacuum(VacuumEntity): self._fan_speed = fan_speed self.schedule_update_ha_state() - def return_to_base(self, **kwargs): + def return_to_base(self, **kwargs: Any) -> None: """Tell the vacuum to return to its dock.""" if self.supported_features & VacuumEntityFeature.RETURN_HOME == 0: return @@ -237,7 +240,7 @@ class DemoVacuum(VacuumEntity): self._battery_level += 5 self.schedule_update_ha_state() - def send_command(self, command, params=None, **kwargs): + def send_command(self, command, params=None, **kwargs: Any) -> None: """Send a command to the vacuum.""" if self.supported_features & VacuumEntityFeature.SEND_COMMAND == 0: return @@ -250,56 +253,56 @@ class DemoVacuum(VacuumEntity): class StateDemoVacuum(StateVacuumEntity): """Representation of a demo vacuum supporting states.""" - def __init__(self, name): + def __init__(self, name: str) -> None: """Initialize the vacuum.""" self._name = name self._supported_features = SUPPORT_STATE_SERVICES self._state = STATE_DOCKED self._fan_speed = FAN_SPEEDS[1] - self._cleaned_area = 0 + self._cleaned_area: float = 0 self._battery_level = 100 @property - def name(self): + def name(self) -> str: """Return the name of the vacuum.""" return self._name @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed for a demo vacuum.""" return False @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" return self._supported_features @property - def state(self): + def state(self) -> str: """Return the current state of the vacuum.""" return self._state @property - def battery_level(self): + def battery_level(self) -> int: """Return the current battery level of the vacuum.""" return max(0, min(100, self._battery_level)) @property - def fan_speed(self): + def fan_speed(self) -> str: """Return the current fan speed of the vacuum.""" return self._fan_speed @property - def fan_speed_list(self): + def fan_speed_list(self) -> list[str]: """Return the list of supported fan speeds.""" return FAN_SPEEDS @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return device state attributes.""" return {ATTR_CLEANED_AREA: round(self._cleaned_area, 2)} - def start(self): + def start(self) -> None: """Start or resume the cleaning task.""" if self.supported_features & VacuumEntityFeature.START == 0: return @@ -310,7 +313,7 @@ class StateDemoVacuum(StateVacuumEntity): self._battery_level -= 1 self.schedule_update_ha_state() - def pause(self): + def pause(self) -> None: """Pause the cleaning task.""" if self.supported_features & VacuumEntityFeature.PAUSE == 0: return @@ -319,7 +322,7 @@ class StateDemoVacuum(StateVacuumEntity): self._state = STATE_PAUSED self.schedule_update_ha_state() - def stop(self, **kwargs): + def stop(self, **kwargs: Any) -> None: """Stop the cleaning task, do not return to dock.""" if self.supported_features & VacuumEntityFeature.STOP == 0: return @@ -327,7 +330,7 @@ class StateDemoVacuum(StateVacuumEntity): self._state = STATE_IDLE self.schedule_update_ha_state() - def return_to_base(self, **kwargs): + def return_to_base(self, **kwargs: Any) -> None: """Return dock to charging base.""" if self.supported_features & VacuumEntityFeature.RETURN_HOME == 0: return @@ -337,7 +340,7 @@ class StateDemoVacuum(StateVacuumEntity): event.call_later(self.hass, 30, self.__set_state_to_dock) - def clean_spot(self, **kwargs): + def clean_spot(self, **kwargs: Any) -> None: """Perform a spot clean-up.""" if self.supported_features & VacuumEntityFeature.CLEAN_SPOT == 0: return @@ -347,7 +350,7 @@ class StateDemoVacuum(StateVacuumEntity): self._battery_level -= 1 self.schedule_update_ha_state() - def set_fan_speed(self, fan_speed, **kwargs): + def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set the vacuum's fan speed.""" if self.supported_features & VacuumEntityFeature.FAN_SPEED == 0: return @@ -356,6 +359,6 @@ class StateDemoVacuum(StateVacuumEntity): self._fan_speed = fan_speed self.schedule_update_ha_state() - def __set_state_to_dock(self, _): + def __set_state_to_dock(self, _: datetime) -> None: self._state = STATE_DOCKED self.schedule_update_ha_state() diff --git a/homeassistant/components/demo/weather.py b/homeassistant/components/demo/weather.py index eed3e970b12..dabaf8d066c 100644 --- a/homeassistant/components/demo/weather.py +++ b/homeassistant/components/demo/weather.py @@ -18,12 +18,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, + Forecast, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry @@ -40,7 +35,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -CONDITION_CLASSES = { +CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_CLOUDY: [], ATTR_CONDITION_FOG: [], ATTR_CONDITION_HAIL: [], @@ -125,17 +120,17 @@ class DemoWeather(WeatherEntity): def __init__( self, - name, - condition, - temperature, - humidity, - pressure, - wind_speed, - temperature_unit, - pressure_unit, - wind_speed_unit, - forecast, - ): + name: str, + condition: str, + temperature: float, + humidity: float, + pressure: float, + wind_speed: float, + temperature_unit: str, + pressure_unit: str, + wind_speed_unit: str, + forecast: list[list], + ) -> None: """Initialize the Demo weather.""" self._name = name self._condition = condition @@ -149,77 +144,77 @@ class DemoWeather(WeatherEntity): self._forecast = forecast @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return f"Demo Weather {self._name}" @property - def should_poll(self): + def should_poll(self) -> bool: """No polling needed for a demo weather condition.""" return False @property - def native_temperature(self): + def native_temperature(self) -> float: """Return the temperature.""" return self._native_temperature @property - def native_temperature_unit(self): + def native_temperature_unit(self) -> str: """Return the unit of measurement.""" return self._native_temperature_unit @property - def humidity(self): + def humidity(self) -> float: """Return the humidity.""" return self._humidity @property - def native_wind_speed(self): + def native_wind_speed(self) -> float: """Return the wind speed.""" return self._native_wind_speed @property - def native_wind_speed_unit(self): + def native_wind_speed_unit(self) -> str: """Return the wind speed.""" return self._native_wind_speed_unit @property - def native_pressure(self): + def native_pressure(self) -> float: """Return the pressure.""" return self._native_pressure @property - def native_pressure_unit(self): + def native_pressure_unit(self) -> str: """Return the pressure.""" return self._native_pressure_unit @property - def condition(self): + def condition(self) -> str: """Return the weather condition.""" return [ k for k, v in CONDITION_CLASSES.items() if self._condition.lower() in v ][0] @property - def attribution(self): + def attribution(self) -> str: """Return the attribution.""" return "Powered by Home Assistant" @property - def forecast(self): + def forecast(self) -> list[Forecast]: """Return the forecast.""" reftime = dt_util.now().replace(hour=16, minute=00) forecast_data = [] for entry in self._forecast: - data_dict = { - ATTR_FORECAST_TIME: reftime.isoformat(), - ATTR_FORECAST_CONDITION: entry[0], - ATTR_FORECAST_PRECIPITATION: entry[1], - ATTR_FORECAST_TEMP: entry[2], - ATTR_FORECAST_TEMP_LOW: entry[3], - ATTR_FORECAST_PRECIPITATION_PROBABILITY: entry[4], - } + data_dict = Forecast( + datetime=reftime.isoformat(), + condition=entry[0], + precipitation=entry[1], + temperature=entry[2], + templow=entry[3], + precipitation_probability=entry[4], + ) reftime = reftime + timedelta(hours=4) forecast_data.append(data_dict) From 560fbd1a0e685988ec2759dbe95fb1d19d86718a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Jul 2022 21:10:26 +0200 Subject: [PATCH 2138/3516] Remove lutron_caseta from mypy ignore list (#74427) --- homeassistant/components/lutron_caseta/__init__.py | 13 +++++++------ homeassistant/components/lutron_caseta/cover.py | 4 ++-- .../components/lutron_caseta/device_trigger.py | 4 +++- homeassistant/components/lutron_caseta/models.py | 4 +--- mypy.ini | 9 --------- script/hassfest/mypy_config.py | 3 --- 6 files changed, 13 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index f2225900aad..7faf70f0d7a 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -215,10 +215,10 @@ def _async_register_button_devices( config_entry_id: str, bridge_device, button_devices_by_id: dict[int, dict], -) -> dict[str, dr.DeviceEntry]: +) -> dict[str, dict]: """Register button devices (Pico Remotes) in the device registry.""" device_registry = dr.async_get(hass) - button_devices_by_dr_id = {} + button_devices_by_dr_id: dict[str, dict] = {} seen = set() for device in button_devices_by_id.values(): @@ -226,7 +226,7 @@ def _async_register_button_devices( continue seen.add(device["serial"]) area, name = _area_and_name_from_name(device["name"]) - device_args = { + device_args: dict[str, Any] = { "name": f"{area} {name}", "manufacturer": MANUFACTURER, "config_entry_id": config_entry_id, @@ -246,7 +246,8 @@ def _async_register_button_devices( def _area_and_name_from_name(device_name: str) -> tuple[str, str]: """Return the area and name from the devices internal name.""" if "_" in device_name: - return device_name.split("_", 1) + area_device_name = device_name.split("_", 1) + return area_device_name[0], area_device_name[1] return UNASSIGNED_AREA, device_name @@ -381,13 +382,13 @@ class LutronCasetaDevice(Entity): class LutronCasetaDeviceUpdatableEntity(LutronCasetaDevice): """A lutron_caseta entity that can update by syncing data from the bridge.""" - async def async_update(self): + async def async_update(self) -> None: """Update when forcing a refresh of the device.""" self._device = self._smartbridge.get_device_by_id(self.device_id) _LOGGER.debug(self._device) -def _id_to_identifier(lutron_id: str) -> None: +def _id_to_identifier(lutron_id: str) -> tuple[str, str]: """Convert a lutron caseta identifier to a device identifier.""" return (DOMAIN, lutron_id) diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py index b74642a8589..d63c1191d57 100644 --- a/homeassistant/components/lutron_caseta/cover.py +++ b/homeassistant/components/lutron_caseta/cover.py @@ -66,13 +66,13 @@ class LutronCasetaCover(LutronCasetaDeviceUpdatableEntity, CoverEntity): async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self._smartbridge.lower_cover(self.device_id) - self.async_update() + await self.async_update() self.async_write_ha_state() async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self._smartbridge.raise_cover(self.device_id) - self.async_update() + await self.async_update() self.async_write_ha_state() async def async_set_cover_position(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index dcdf5d584a4..d938ad6e7f2 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -430,9 +430,11 @@ async def async_attach_trigger( """Attach a trigger.""" device_registry = dr.async_get(hass) device = device_registry.async_get(config[CONF_DEVICE_ID]) + assert device + assert device.model device_type = _device_model_to_type(device.model) _, serial = list(device.identifiers)[0] - schema = DEVICE_TYPE_SCHEMA_MAP.get(device_type) + schema = DEVICE_TYPE_SCHEMA_MAP[device_type] valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP[device_type] config = schema(config) event_config = { diff --git a/homeassistant/components/lutron_caseta/models.py b/homeassistant/components/lutron_caseta/models.py index 5845c888a2e..362760b0caf 100644 --- a/homeassistant/components/lutron_caseta/models.py +++ b/homeassistant/components/lutron_caseta/models.py @@ -6,8 +6,6 @@ from typing import Any from pylutron_caseta.smartbridge import Smartbridge -from homeassistant.helpers.device_registry import DeviceEntry - @dataclass class LutronCasetaData: @@ -15,4 +13,4 @@ class LutronCasetaData: bridge: Smartbridge bridge_device: dict[str, Any] - button_devices: dict[str, DeviceEntry] + button_devices: dict[str, dict] diff --git a/mypy.ini b/mypy.ini index e7897368189..30bc97f545a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2752,15 +2752,6 @@ ignore_errors = true [mypy-homeassistant.components.lovelace.websocket] ignore_errors = true -[mypy-homeassistant.components.lutron_caseta] -ignore_errors = true - -[mypy-homeassistant.components.lutron_caseta.device_trigger] -ignore_errors = true - -[mypy-homeassistant.components.lutron_caseta.switch] -ignore_errors = true - [mypy-homeassistant.components.lyric.climate] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index f7b8bb1be2f..939290ca9ab 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -59,9 +59,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.lovelace.dashboard", "homeassistant.components.lovelace.resources", "homeassistant.components.lovelace.websocket", - "homeassistant.components.lutron_caseta", - "homeassistant.components.lutron_caseta.device_trigger", - "homeassistant.components.lutron_caseta.switch", "homeassistant.components.lyric.climate", "homeassistant.components.lyric.config_flow", "homeassistant.components.lyric.sensor", From a7da8673bf44a9126ec4c0f91e2736eb841b004d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Jul 2022 21:26:33 +0200 Subject: [PATCH 2139/3516] Use instance attributes in usgs_earthquakes_feed (#74403) --- .../usgs_earthquakes_feed/geo_location.py | 133 +++++++----------- 1 file changed, 51 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/usgs_earthquakes_feed/geo_location.py b/homeassistant/components/usgs_earthquakes_feed/geo_location.py index 6e3eb9b2337..c82705174fe 100644 --- a/homeassistant/components/usgs_earthquakes_feed/geo_location.py +++ b/homeassistant/components/usgs_earthquakes_feed/geo_location.py @@ -1,15 +1,19 @@ """Support for U.S. Geological Survey Earthquake Hazards Program Feeds.""" from __future__ import annotations -from datetime import timedelta +from collections.abc import Callable +from datetime import datetime, timedelta import logging +from typing import Any from aio_geojson_usgs_earthquakes import UsgsEarthquakeHazardsProgramFeedManager +from aio_geojson_usgs_earthquakes.feed_entry import ( + UsgsEarthquakeHazardsProgramFeedEntry, +) import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_TIME, CONF_LATITUDE, CONF_LONGITUDE, @@ -96,14 +100,14 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the USGS Earthquake Hazards Program Feed platform.""" - scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - feed_type = config[CONF_FEED_TYPE] - coordinates = ( + scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + feed_type: str = config[CONF_FEED_TYPE] + coordinates: tuple[float, float] = ( config.get(CONF_LATITUDE, hass.config.latitude), config.get(CONF_LONGITUDE, hass.config.longitude), ) - radius_in_km = config[CONF_RADIUS] - minimum_magnitude = config[CONF_MINIMUM_MAGNITUDE] + radius_in_km: float = config[CONF_RADIUS] + minimum_magnitude: float = config[CONF_MINIMUM_MAGNITUDE] # Initialize the entity manager. manager = UsgsEarthquakesFeedEntityManager( hass, @@ -128,14 +132,14 @@ class UsgsEarthquakesFeedEntityManager: def __init__( self, - hass, - async_add_entities, - scan_interval, - coordinates, - feed_type, - radius_in_km, - minimum_magnitude, - ): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + scan_interval: timedelta, + coordinates: tuple[float, float], + feed_type: str, + radius_in_km: float, + minimum_magnitude: float, + ) -> None: """Initialize the Feed Entity Manager.""" self._hass = hass @@ -153,10 +157,10 @@ class UsgsEarthquakesFeedEntityManager: self._async_add_entities = async_add_entities self._scan_interval = scan_interval - async def async_init(self): + async def async_init(self) -> None: """Schedule initial and regular updates based on configured time interval.""" - async def update(event_time): + async def update(event_time: datetime) -> None: """Update.""" await self.async_update() @@ -164,26 +168,28 @@ class UsgsEarthquakesFeedEntityManager: async_track_time_interval(self._hass, update, self._scan_interval) _LOGGER.debug("Feed entity manager initialized") - async def async_update(self): + async def async_update(self) -> None: """Refresh data.""" await self._feed_manager.update() _LOGGER.debug("Feed entity manager updated") - def get_entry(self, external_id): + def get_entry( + self, external_id: str + ) -> UsgsEarthquakeHazardsProgramFeedEntry | None: """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - async def _generate_entity(self, external_id): + async def _generate_entity(self, external_id: str) -> None: """Generate new entity.""" new_entity = UsgsEarthquakesEvent(self, external_id) # Add new entities to HA. self._async_add_entities([new_entity], True) - async def _update_entity(self, external_id): + async def _update_entity(self, external_id: str) -> None: """Update entity.""" async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) - async def _remove_entity(self, external_id): + async def _remove_entity(self, external_id: str) -> None: """Remove entity.""" async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) @@ -191,15 +197,17 @@ class UsgsEarthquakesFeedEntityManager: class UsgsEarthquakesEvent(GeolocationEvent): """This represents an external event with USGS Earthquake data.""" - def __init__(self, feed_manager, external_id): + _attr_icon = "mdi:pulse" + _attr_should_poll = False + _attr_source = SOURCE + _attr_unit_of_measurement = DEFAULT_UNIT_OF_MEASUREMENT + + def __init__( + self, feed_manager: UsgsEarthquakesFeedEntityManager, external_id: str + ) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager self._external_id = external_id - self._name = None - self._distance = None - self._latitude = None - self._longitude = None - self._attribution = None self._place = None self._magnitude = None self._time = None @@ -207,10 +215,10 @@ class UsgsEarthquakesEvent(GeolocationEvent): self._status = None self._type = None self._alert = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self._remove_signal_delete = async_dispatcher_connect( self.hass, @@ -224,36 +232,33 @@ class UsgsEarthquakesEvent(GeolocationEvent): ) @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for USGS Earthquake events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed( + self, feed_entry: UsgsEarthquakeHazardsProgramFeedEntry + ) -> None: """Update the internal state from the provided feed entry.""" - self._name = feed_entry.title - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] - self._attribution = feed_entry.attribution + self._attr_name = feed_entry.title + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] + self._attr_attribution = feed_entry.attribution self._place = feed_entry.place self._magnitude = feed_entry.magnitude self._time = feed_entry.time @@ -263,42 +268,7 @@ class UsgsEarthquakesEvent(GeolocationEvent): self._alert = feed_entry.alert @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:pulse" - - @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._name - - @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return DEFAULT_UNIT_OF_MEASUREMENT - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = {} for key, value in ( @@ -310,7 +280,6 @@ class UsgsEarthquakesEvent(GeolocationEvent): (ATTR_STATUS, self._status), (ATTR_TYPE, self._type), (ATTR_ALERT, self._alert), - (ATTR_ATTRIBUTION, self._attribution), ): if value or isinstance(value, bool): attributes[key] = value From dd6725b80a5efebda36c64c78250f3374e85c3a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 5 Jul 2022 00:04:56 +0300 Subject: [PATCH 2140/3516] Replace pylint-strict-informational with `fail-on=I` (#74311) `fail-on` is available since pylint 2.9.0. https://pylint.pycqa.org/en/latest/user_guide/configuration/all-options.html#fail-on https://github.com/PyCQA/pylint/issues/3251#issuecomment-1170941337 Co-authored-by: J. Nick Koston --- pyproject.toml | 4 +++- requirements_test.txt | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f50c835e58f..51f1d115e7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,7 +109,6 @@ init-hook = """\ load-plugins = [ "pylint.extensions.code_style", "pylint.extensions.typing", - "pylint_strict_informational", "hass_constructor", "hass_enforce_type_hints", "hass_imports", @@ -123,6 +122,9 @@ extension-pkg-allow-list = [ "orjson", "cv2", ] +fail-on = [ + "I", +] [tool.pylint.BASIC] class-const-naming-style = "any" diff --git a/requirements_test.txt b/requirements_test.txt index 3172c1f5544..113794bda1c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -15,7 +15,6 @@ mypy==0.961 pre-commit==2.19.0 pylint==2.14.4 pipdeptree==2.2.1 -pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 pytest-cov==3.0.0 pytest-freezegun==0.4.2 From ebc8fba5bfc413a293f1cd71fc67e870304398c6 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 5 Jul 2022 00:23:09 +0000 Subject: [PATCH 2141/3516] [ci skip] Translation update --- .../components/anthemav/translations/pl.json | 19 ++++++++++++++ .../components/awair/translations/pl.json | 7 ++++++ .../components/esphome/translations/pl.json | 4 +-- .../components/generic/translations/ca.json | 4 +++ .../components/generic/translations/de.json | 4 +++ .../components/generic/translations/el.json | 4 +++ .../components/generic/translations/et.json | 4 +++ .../components/generic/translations/hu.json | 4 +++ .../components/generic/translations/ja.json | 4 +++ .../components/generic/translations/pl.json | 4 +++ .../generic/translations/pt-BR.json | 4 +++ .../generic/translations/zh-Hant.json | 4 +++ .../components/hive/translations/pl.json | 9 ++++++- .../homeassistant/translations/ca.json | 1 + .../homeassistant/translations/et.json | 1 + .../homeassistant/translations/hu.json | 1 + .../components/lcn/translations/pl.json | 1 + .../lg_soundbar/translations/pl.json | 18 +++++++++++++ .../components/life360/translations/pl.json | 25 ++++++++++++++++++- .../components/nextdns/translations/ca.json | 24 ++++++++++++++++++ .../components/nextdns/translations/de.json | 24 ++++++++++++++++++ .../components/nextdns/translations/el.json | 24 ++++++++++++++++++ .../components/nextdns/translations/et.json | 24 ++++++++++++++++++ .../components/nextdns/translations/hu.json | 24 ++++++++++++++++++ .../components/nextdns/translations/ja.json | 24 ++++++++++++++++++ .../nextdns/translations/pt-BR.json | 24 ++++++++++++++++++ .../nextdns/translations/zh-Hant.json | 24 ++++++++++++++++++ .../components/nina/translations/pl.json | 22 ++++++++++++++++ .../components/qnap_qsw/translations/pl.json | 6 +++++ .../simplepush/translations/pl.json | 21 ++++++++++++++++ .../soundtouch/translations/pl.json | 21 ++++++++++++++++ 31 files changed, 380 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/anthemav/translations/pl.json create mode 100644 homeassistant/components/lg_soundbar/translations/pl.json create mode 100644 homeassistant/components/nextdns/translations/ca.json create mode 100644 homeassistant/components/nextdns/translations/de.json create mode 100644 homeassistant/components/nextdns/translations/el.json create mode 100644 homeassistant/components/nextdns/translations/et.json create mode 100644 homeassistant/components/nextdns/translations/hu.json create mode 100644 homeassistant/components/nextdns/translations/ja.json create mode 100644 homeassistant/components/nextdns/translations/pt-BR.json create mode 100644 homeassistant/components/nextdns/translations/zh-Hant.json create mode 100644 homeassistant/components/simplepush/translations/pl.json create mode 100644 homeassistant/components/soundtouch/translations/pl.json diff --git a/homeassistant/components/anthemav/translations/pl.json b/homeassistant/components/anthemav/translations/pl.json new file mode 100644 index 00000000000..18eaecb9845 --- /dev/null +++ b/homeassistant/components/anthemav/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "cannot_receive_deviceinfo": "Nie uda\u0142o si\u0119 pobra\u0107 adresu MAC. Upewnij si\u0119, \u017ce urz\u0105dzenie jest w\u0142\u0105czone." + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/pl.json b/homeassistant/components/awair/translations/pl.json index 38bd24f714b..b0ee9e85adf 100644 --- a/homeassistant/components/awair/translations/pl.json +++ b/homeassistant/components/awair/translations/pl.json @@ -17,6 +17,13 @@ }, "description": "Wprowad\u017a ponownie token dost\u0119pu programisty Awair." }, + "reauth_confirm": { + "data": { + "access_token": "Token dost\u0119pu", + "email": "Adres e-mail" + }, + "description": "Wprowad\u017a ponownie token dost\u0119pu programisty Awair." + }, "user": { "data": { "access_token": "Token dost\u0119pu", diff --git a/homeassistant/components/esphome/translations/pl.json b/homeassistant/components/esphome/translations/pl.json index da6154e9fb6..dd495f0f097 100644 --- a/homeassistant/components/esphome/translations/pl.json +++ b/homeassistant/components/esphome/translations/pl.json @@ -9,7 +9,7 @@ "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:'.", "invalid_auth": "Niepoprawne uwierzytelnienie", "invalid_psk": "Klucz szyfruj\u0105cy transport jest nieprawid\u0142owy. Upewnij si\u0119, \u017ce pasuje do tego, kt\u00f3ry masz w swojej konfiguracji.", - "resolve_error": "Nie mo\u017cna rozpozna\u0107 adresu ESP. Je\u015bli ten b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, nale\u017cy ustawi\u0107 statyczny adres IP: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "Nie mo\u017cna rozpozna\u0107 adresu ESP. Je\u015bli ten b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, nale\u017cy ustawi\u0107 statyczny adres IP." }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Nazwa hosta lub adres IP", "port": "Port" }, - "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia [ESPHome](https://esphomelib.com/) w\u0119z\u0142a." + "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia w\u0119z\u0142a [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/generic/translations/ca.json b/homeassistant/components/generic/translations/ca.json index 818a030c8a7..90e12b8ea69 100644 --- a/homeassistant/components/generic/translations/ca.json +++ b/homeassistant/components/generic/translations/ca.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Ja hi ha una c\u00e0mera amb aquest URL de configuraci\u00f3.", "invalid_still_image": "L'URL no ha retornat una imatge fixa v\u00e0lida", + "malformed_url": "URL mal format", "no_still_image_or_stream_url": "Has d'especificar almenys una imatge un URL de flux", + "relative_url": "Els URL relatius no s'admeten", "stream_file_not_found": "Fitxer no trobat mentre s'intentava connectar al flux de dades (est\u00e0 instal\u00b7lat ffmpeg?)", "stream_http_not_found": "HTTP 404 'Not found' a l'intentar connectar-se al flux de dades ('stream')", "stream_io_error": "Error d'entrada/sortida mentre s'intentava connectar al flux de dades. Protocol de transport RTSP incorrecte?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Ja hi ha una c\u00e0mera amb aquest URL de configuraci\u00f3.", "invalid_still_image": "L'URL no ha retornat una imatge fixa v\u00e0lida", + "malformed_url": "URL mal format", "no_still_image_or_stream_url": "Has d'especificar almenys una imatge un URL de flux", + "relative_url": "Els URL relatius no s'admeten", "stream_file_not_found": "Fitxer no trobat mentre s'intentava connectar al flux de dades (est\u00e0 instal\u00b7lat ffmpeg?)", "stream_http_not_found": "HTTP 404 'Not found' a l'intentar connectar-se al flux de dades ('stream')", "stream_io_error": "Error d'entrada/sortida mentre s'intentava connectar al flux de dades. Protocol de transport RTSP incorrecte?", diff --git a/homeassistant/components/generic/translations/de.json b/homeassistant/components/generic/translations/de.json index 0c14e95a683..57d15a8efea 100644 --- a/homeassistant/components/generic/translations/de.json +++ b/homeassistant/components/generic/translations/de.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Es existiert bereits eine Kamera mit diesen URL-Einstellungen.", "invalid_still_image": "URL hat kein g\u00fcltiges Standbild zur\u00fcckgegeben", + "malformed_url": "Falsch formatierte URL", "no_still_image_or_stream_url": "Du musst mindestens eine Standbild- oder Stream-URL angeben", + "relative_url": "Relative URLs sind nicht zul\u00e4ssig", "stream_file_not_found": "Datei nicht gefunden beim Versuch, eine Verbindung zum Stream herzustellen (ist ffmpeg installiert?)", "stream_http_not_found": "HTTP 404 Not found beim Versuch, eine Verbindung zum Stream herzustellen", "stream_io_error": "Eingabe-/Ausgabefehler beim Versuch, eine Verbindung zum Stream herzustellen. Falsches RTSP-Transportprotokoll?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Es existiert bereits eine Kamera mit diesen URL-Einstellungen.", "invalid_still_image": "URL hat kein g\u00fcltiges Standbild zur\u00fcckgegeben", + "malformed_url": "Falsch formatierte URL", "no_still_image_or_stream_url": "Du musst mindestens eine Standbild- oder Stream-URL angeben", + "relative_url": "Relative URLs sind nicht zul\u00e4ssig", "stream_file_not_found": "Datei nicht gefunden beim Versuch, eine Verbindung zum Stream herzustellen (ist ffmpeg installiert?)", "stream_http_not_found": "HTTP 404 Not found beim Versuch, eine Verbindung zum Stream herzustellen", "stream_io_error": "Eingabe-/Ausgabefehler beim Versuch, eine Verbindung zum Stream herzustellen. Falsches RTSP-Transportprotokoll?", diff --git a/homeassistant/components/generic/translations/el.json b/homeassistant/components/generic/translations/el.json index f97714a53c1..29063cdc216 100644 --- a/homeassistant/components/generic/translations/el.json +++ b/homeassistant/components/generic/translations/el.json @@ -7,7 +7,9 @@ "error": { "already_exists": "\u03a5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 URL.", "invalid_still_image": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b4\u03b5\u03bd \u03b5\u03c0\u03ad\u03c3\u03c4\u03c1\u03b5\u03c8\u03b5 \u03bc\u03b9\u03b1 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1", + "malformed_url": "\u039b\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae URL", "no_still_image_or_stream_url": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03bc\u03b9\u03b1 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1 \u03ae \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c1\u03bf\u03ae\u03c2", + "relative_url": "\u0394\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bf\u03b9 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 URL", "stream_file_not_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03c1\u03bf\u03ae (\u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b5\u03c3\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf \u03c4\u03bf ffmpeg;)", "stream_http_not_found": "HTTP 404 \u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", "stream_io_error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5/\u03b5\u03be\u03cc\u03b4\u03bf\u03c5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae. \u039b\u03ac\u03b8\u03bf\u03c2 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP;", @@ -50,7 +52,9 @@ "error": { "already_exists": "\u03a5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 URL.", "invalid_still_image": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b4\u03b5\u03bd \u03b5\u03c0\u03ad\u03c3\u03c4\u03c1\u03b5\u03c8\u03b5 \u03bc\u03b9\u03b1 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1", + "malformed_url": "\u039b\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae URL", "no_still_image_or_stream_url": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd \u03bc\u03b9\u03b1 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1 \u03ae \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c1\u03bf\u03ae\u03c2", + "relative_url": "\u0394\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bf\u03b9 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ad\u03c2 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 URL", "stream_file_not_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03c1\u03bf\u03ae (\u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b5\u03c3\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf \u03c4\u03bf ffmpeg;)", "stream_http_not_found": "HTTP 404 \u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae", "stream_io_error": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5/\u03b5\u03be\u03cc\u03b4\u03bf\u03c5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae. \u039b\u03ac\u03b8\u03bf\u03c2 \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 RTSP;", diff --git a/homeassistant/components/generic/translations/et.json b/homeassistant/components/generic/translations/et.json index a50e4d4aaa7..2746f84b9a1 100644 --- a/homeassistant/components/generic/translations/et.json +++ b/homeassistant/components/generic/translations/et.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Nende URL-i seadetega kaamera on juba olemas.", "invalid_still_image": "URL ei tagastanud kehtivat pilti", + "malformed_url": "Vigane URL", "no_still_image_or_stream_url": "Pead m\u00e4\u00e4rama v\u00e4hemalt liikumatu pildi v\u00f5i voo URL-i", + "relative_url": "Osalised URL-id pole lubatud", "stream_file_not_found": "Vooga \u00fchenduse loomisel ei leitud faili (kas ffmpeg on installitud?)", "stream_http_not_found": "HTTP 404 viga kui \u00fcritatakse vooga \u00fchendust luua", "stream_io_error": "Sisend-/v\u00e4ljundviga vooga \u00fchenduse loomisel. Vale RTSP transpordiprotokoll?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Nende URL-i seadetega kaamera on juba olemas.", "invalid_still_image": "URL ei tagastanud kehtivat pilti", + "malformed_url": "Vigane URL", "no_still_image_or_stream_url": "Pead m\u00e4\u00e4rama v\u00e4hemalt liikumatu pildi v\u00f5i voo URL-i", + "relative_url": "Osalised URL-id pole lubatud", "stream_file_not_found": "Vooga \u00fchenduse loomisel ei leitud faili (kas ffmpeg on installitud?)", "stream_http_not_found": "HTTP 404 viga kui \u00fcritatakse vooga \u00fchendust luua", "stream_io_error": "Sisend-/v\u00e4ljundviga vooga \u00fchenduse loomisel. Vale RTSP transpordiprotokoll?", diff --git a/homeassistant/components/generic/translations/hu.json b/homeassistant/components/generic/translations/hu.json index 76992a1fde7..bf03ec88d96 100644 --- a/homeassistant/components/generic/translations/hu.json +++ b/homeassistant/components/generic/translations/hu.json @@ -7,7 +7,9 @@ "error": { "already_exists": "M\u00e1r l\u00e9tezik egy kamera ezekkel az URL-be\u00e1ll\u00edt\u00e1sokkal.", "invalid_still_image": "Az URL nem adott vissza \u00e9rv\u00e9nyes \u00e1ll\u00f3k\u00e9pet", + "malformed_url": "Hib\u00e1s URL", "no_still_image_or_stream_url": "Legal\u00e1bb egy \u00e1ll\u00f3k\u00e9pet vagy stream URL-c\u00edmet kell megadnia.", + "relative_url": "A relat\u00edv URL-ek nem enged\u00e9lyezettek", "stream_file_not_found": "F\u00e1jl nem tal\u00e1lhat\u00f3 a streamhez val\u00f3 csatlakoz\u00e1s sor\u00e1n (telep\u00edtve van az ffmpeg?)", "stream_http_not_found": "HTTP 404 Not found - hiba az adatfolyamhoz val\u00f3 csatlakoz\u00e1s k\u00f6zben", "stream_io_error": "Bemeneti/kimeneti hiba t\u00f6rt\u00e9nt az adatfolyamhoz val\u00f3 kapcsol\u00f3d\u00e1s k\u00f6zben. Rossz RTSP sz\u00e1ll\u00edt\u00e1si protokoll?", @@ -50,7 +52,9 @@ "error": { "already_exists": "M\u00e1r l\u00e9tezik egy kamera ezekkel az URL-be\u00e1ll\u00edt\u00e1sokkal.", "invalid_still_image": "Az URL nem adott vissza \u00e9rv\u00e9nyes \u00e1ll\u00f3k\u00e9pet", + "malformed_url": "Hib\u00e1s URL", "no_still_image_or_stream_url": "Legal\u00e1bb egy \u00e1ll\u00f3k\u00e9pet vagy stream URL-c\u00edmet kell megadnia.", + "relative_url": "A relat\u00edv URL-ek nem enged\u00e9lyezettek", "stream_file_not_found": "F\u00e1jl nem tal\u00e1lhat\u00f3 a streamhez val\u00f3 csatlakoz\u00e1s sor\u00e1n (telep\u00edtve van az ffmpeg?)", "stream_http_not_found": "HTTP 404 Not found - hiba az adatfolyamhoz val\u00f3 csatlakoz\u00e1s k\u00f6zben", "stream_io_error": "Bemeneti/kimeneti hiba t\u00f6rt\u00e9nt az adatfolyamhoz val\u00f3 kapcsol\u00f3d\u00e1s k\u00f6zben. Rossz RTSP sz\u00e1ll\u00edt\u00e1si protokoll?", diff --git a/homeassistant/components/generic/translations/ja.json b/homeassistant/components/generic/translations/ja.json index f4fd7d8ec46..499de6409e6 100644 --- a/homeassistant/components/generic/translations/ja.json +++ b/homeassistant/components/generic/translations/ja.json @@ -7,7 +7,9 @@ "error": { "already_exists": "\u3053\u306eURL\u8a2d\u5b9a\u306e\u30ab\u30e1\u30e9\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059\u3002", "invalid_still_image": "URL\u304c\u6709\u52b9\u306a\u9759\u6b62\u753b\u50cf\u3092\u8fd4\u3057\u307e\u305b\u3093\u3067\u3057\u305f", + "malformed_url": "\u4e0d\u6b63\u306a\u5f62\u5f0f\u306eURL", "no_still_image_or_stream_url": "\u9759\u6b62\u753b\u50cf\u3082\u3057\u304f\u306f\u3001\u30b9\u30c8\u30ea\u30fc\u30e0URL\u306e\u3069\u3061\u3089\u304b\u3092\u6307\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "relative_url": "\u76f8\u5bfeURL(Relative URLs)\u306f\u8a31\u53ef\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "stream_file_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u3068\u304d\u306b\u30d5\u30a1\u30a4\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093(ffmpeg\u304c\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u3066\u3044\u307e\u3059\u304b\uff1f)", "stream_http_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u3078\u306e\u63a5\u7d9a\u6642\u306b\u3001HTTP 404\u3067\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "stream_io_error": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u305f\u3068\u304d\u306b\u5165\u51fa\u529b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002RTSP\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30d7\u30ed\u30c8\u30b3\u30eb\u3092\u9593\u9055\u3048\u305f\uff1f", @@ -50,7 +52,9 @@ "error": { "already_exists": "\u3053\u306eURL\u8a2d\u5b9a\u306e\u30ab\u30e1\u30e9\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059\u3002", "invalid_still_image": "URL\u304c\u6709\u52b9\u306a\u9759\u6b62\u753b\u50cf\u3092\u8fd4\u3057\u307e\u305b\u3093\u3067\u3057\u305f", + "malformed_url": "\u4e0d\u6b63\u306a\u5f62\u5f0f\u306eURL", "no_still_image_or_stream_url": "\u9759\u6b62\u753b\u50cf\u3082\u3057\u304f\u306f\u3001\u30b9\u30c8\u30ea\u30fc\u30e0URL\u306e\u3069\u3061\u3089\u304b\u3092\u6307\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "relative_url": "\u76f8\u5bfeURL(Relative URLs)\u306f\u8a31\u53ef\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "stream_file_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b\u3068\u304d\u306b\u30d5\u30a1\u30a4\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093(ffmpeg\u304c\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u3066\u3044\u307e\u3059\u304b\uff1f)", "stream_http_not_found": "\u30b9\u30c8\u30ea\u30fc\u30e0\u3078\u306e\u63a5\u7d9a\u6642\u306b\u3001HTTP 404\u3067\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "stream_io_error": "\u30b9\u30c8\u30ea\u30fc\u30e0\u306b\u63a5\u7d9a\u3057\u3088\u3046\u3068\u3057\u305f\u3068\u304d\u306b\u5165\u51fa\u529b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002RTSP\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u30d7\u30ed\u30c8\u30b3\u30eb\u3092\u9593\u9055\u3048\u305f\uff1f", diff --git a/homeassistant/components/generic/translations/pl.json b/homeassistant/components/generic/translations/pl.json index 81817faf236..71c50148957 100644 --- a/homeassistant/components/generic/translations/pl.json +++ b/homeassistant/components/generic/translations/pl.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Kamera z tymi ustawieniami adresu URL ju\u017c istnieje.", "invalid_still_image": "Adres URL nie zwr\u00f3ci\u0142 prawid\u0142owego obrazu nieruchomego (still image)", + "malformed_url": "Nieprawid\u0142owy adres URL", "no_still_image_or_stream_url": "Musisz poda\u0107 przynajmniej nieruchomy obraz (still image) lub adres URL strumienia", + "relative_url": "Wzgl\u0119dne adresy URL s\u0105 niedozwolone", "stream_file_not_found": "Nie znaleziono pliku podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem (czy ffmpeg jest zainstalowany?)", "stream_http_not_found": "\"HTTP 404 Nie znaleziono\" podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", "stream_io_error": "B\u0142\u0105d wej\u015bcia/wyj\u015bcia podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem. Z\u0142y protok\u00f3\u0142 transportowy RTSP?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Kamera z tymi ustawieniami adresu URL ju\u017c istnieje.", "invalid_still_image": "Adres URL nie zwr\u00f3ci\u0142 prawid\u0142owego obrazu nieruchomego (still image)", + "malformed_url": "Nieprawid\u0142owy adres URL", "no_still_image_or_stream_url": "Musisz poda\u0107 przynajmniej nieruchomy obraz (still image) lub adres URL strumienia", + "relative_url": "Wzgl\u0119dne adresy URL s\u0105 niedozwolone", "stream_file_not_found": "Nie znaleziono pliku podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem (czy ffmpeg jest zainstalowany?)", "stream_http_not_found": "\"HTTP 404 Nie znaleziono\" podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem", "stream_io_error": "B\u0142\u0105d wej\u015bcia/wyj\u015bcia podczas pr\u00f3by po\u0142\u0105czenia ze strumieniem. Z\u0142y protok\u00f3\u0142 transportowy RTSP?", diff --git a/homeassistant/components/generic/translations/pt-BR.json b/homeassistant/components/generic/translations/pt-BR.json index 1a61cdeac97..86ac7a01efb 100644 --- a/homeassistant/components/generic/translations/pt-BR.json +++ b/homeassistant/components/generic/translations/pt-BR.json @@ -7,7 +7,9 @@ "error": { "already_exists": "J\u00e1 existe uma c\u00e2mera com essas configura\u00e7\u00f5es de URL.", "invalid_still_image": "A URL n\u00e3o retornou uma imagem est\u00e1tica v\u00e1lida", + "malformed_url": "URL malformada", "no_still_image_or_stream_url": "Voc\u00ea deve especificar pelo menos uma imagem est\u00e1tica ou uma URL de stream", + "relative_url": "URLs relativas n\u00e3o s\u00e3o permitidas", "stream_file_not_found": "Arquivo n\u00e3o encontrado ao tentar se conectar a stream (o ffmpeg est\u00e1 instalado?)", "stream_http_not_found": "HTTP 404 n\u00e3o encontrado ao tentar se conectar a stream", "stream_io_error": "Erro de entrada/sa\u00edda ao tentar se conectar a stream. Protocolo RTSP errado?", @@ -50,7 +52,9 @@ "error": { "already_exists": "J\u00e1 existe uma c\u00e2mera com essas configura\u00e7\u00f5es de URL.", "invalid_still_image": "A URL n\u00e3o retornou uma imagem est\u00e1tica v\u00e1lida", + "malformed_url": "URL malformada", "no_still_image_or_stream_url": "Voc\u00ea deve especificar pelo menos uma imagem est\u00e1tica ou uma URL de stream", + "relative_url": "URLs relativas n\u00e3o s\u00e3o permitidas", "stream_file_not_found": "Arquivo n\u00e3o encontrado ao tentar se conectar a stream (o ffmpeg est\u00e1 instalado?)", "stream_http_not_found": "HTTP 404 n\u00e3o encontrado ao tentar se conectar a stream", "stream_io_error": "Erro de entrada/sa\u00edda ao tentar se conectar a stream. Protocolo RTSP errado?", diff --git a/homeassistant/components/generic/translations/zh-Hant.json b/homeassistant/components/generic/translations/zh-Hant.json index 595bf019f64..ded2ea569c4 100644 --- a/homeassistant/components/generic/translations/zh-Hant.json +++ b/homeassistant/components/generic/translations/zh-Hant.json @@ -7,7 +7,9 @@ "error": { "already_exists": "\u5df2\u7d93\u6709\u4f7f\u7528\u76f8\u540c URL \u7684\u651d\u5f71\u6a5f\u3002", "invalid_still_image": "URL \u56de\u50b3\u4e26\u975e\u6709\u6548\u975c\u614b\u5f71\u50cf", + "malformed_url": "URL \u683c\u5f0f\u932f\u8aa4", "no_still_image_or_stream_url": "\u5fc5\u9808\u81f3\u5c11\u6307\u5b9a\u975c\u614b\u5f71\u50cf\u6216\u4e32\u6d41 URL", + "relative_url": "\u4e0d\u5141\u8a31\u4f7f\u7528\u76f8\u5c0d\u61c9 URL", "stream_file_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u627e\u4e0d\u5230\u6a94\u6848\u932f\u8aa4\uff08\u662f\u5426\u5df2\u5b89\u88dd ffmpeg\uff1f\uff09", "stream_http_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe HTTP 404 \u672a\u627e\u5230\u932f\u8aa4", "stream_io_error": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u8f38\u5165/\u8f38\u51fa\u932f\u8aa4\u3002\u8f38\u5165\u932f\u8aa4\u7684 RTSP \u50b3\u8f38\u5354\u5b9a\uff1f", @@ -50,7 +52,9 @@ "error": { "already_exists": "\u5df2\u7d93\u6709\u4f7f\u7528\u76f8\u540c URL \u7684\u651d\u5f71\u6a5f\u3002", "invalid_still_image": "URL \u56de\u50b3\u4e26\u975e\u6709\u6548\u975c\u614b\u5f71\u50cf", + "malformed_url": "URL \u683c\u5f0f\u932f\u8aa4", "no_still_image_or_stream_url": "\u5fc5\u9808\u81f3\u5c11\u6307\u5b9a\u975c\u614b\u5f71\u50cf\u6216\u4e32\u6d41 URL", + "relative_url": "\u4e0d\u5141\u8a31\u4f7f\u7528\u76f8\u5c0d\u61c9 URL", "stream_file_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u627e\u4e0d\u5230\u6a94\u6848\u932f\u8aa4\uff08\u662f\u5426\u5df2\u5b89\u88dd ffmpeg\uff1f\uff09", "stream_http_not_found": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe HTTP 404 \u672a\u627e\u5230\u932f\u8aa4", "stream_io_error": "\u5617\u8a66\u9023\u7dda\u4e32\u6d41\u6642\u51fa\u73fe\u8f38\u5165/\u8f38\u51fa\u932f\u8aa4\u3002\u8f38\u5165\u932f\u8aa4\u7684 RTSP \u50b3\u8f38\u5354\u5b9a\uff1f", diff --git a/homeassistant/components/hive/translations/pl.json b/homeassistant/components/hive/translations/pl.json index 0c61fa74feb..b227e5b1e9a 100644 --- a/homeassistant/components/hive/translations/pl.json +++ b/homeassistant/components/hive/translations/pl.json @@ -20,6 +20,13 @@ "description": "Wprowad\u017a sw\u00f3j kod uwierzytelniaj\u0105cy Hive. \n\nWprowad\u017a kod 0000, aby poprosi\u0107 o kolejny kod.", "title": "Uwierzytelnianie dwusk\u0142adnikowe Hive" }, + "configuration": { + "data": { + "device_name": "Nazwa urz\u0105dzenia" + }, + "description": "Wprowad\u017a konfiguracj\u0119 Hive", + "title": "Konfiguracja Hive." + }, "reauth": { "data": { "password": "Has\u0142o", @@ -34,7 +41,7 @@ "scan_interval": "Cz\u0119stotliwo\u015b\u0107 skanowania (w sekundach)", "username": "Nazwa u\u017cytkownika" }, - "description": "Wprowad\u017a dane logowania i konfiguracj\u0119 Hive.", + "description": "Wprowad\u017a dane logowania Hive.", "title": "Login Hive" } } diff --git a/homeassistant/components/homeassistant/translations/ca.json b/homeassistant/components/homeassistant/translations/ca.json index e9c91d9df20..8bcf5d96a86 100644 --- a/homeassistant/components/homeassistant/translations/ca.json +++ b/homeassistant/components/homeassistant/translations/ca.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Arquitectura de la CPU", + "config_dir": "Directori de configuraci\u00f3", "dev": "Desenvolupador", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/et.json b/homeassistant/components/homeassistant/translations/et.json index 529b84120d7..7b9b675ed6f 100644 --- a/homeassistant/components/homeassistant/translations/et.json +++ b/homeassistant/components/homeassistant/translations/et.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Protsessori arhitektuur", + "config_dir": "Konfiguratsiooni kaust", "dev": "Arendus", "docker": "Docker", "hassio": "Haldur", diff --git a/homeassistant/components/homeassistant/translations/hu.json b/homeassistant/components/homeassistant/translations/hu.json index b4da84596bf..7261dfa1f7a 100644 --- a/homeassistant/components/homeassistant/translations/hu.json +++ b/homeassistant/components/homeassistant/translations/hu.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Processzor architekt\u00fara", + "config_dir": "Konfigur\u00e1ci\u00f3s k\u00f6nyvt\u00e1r", "dev": "Fejleszt\u00e9s", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/lcn/translations/pl.json b/homeassistant/components/lcn/translations/pl.json index 18446607167..2a2415d4138 100644 --- a/homeassistant/components/lcn/translations/pl.json +++ b/homeassistant/components/lcn/translations/pl.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "otrzymano kod blokady", "fingerprint": "otrzymano kod odcisku palca", "send_keys": "otrzymano klucze wysy\u0142ania", "transmitter": "otrzymano kod nadajnika", diff --git a/homeassistant/components/lg_soundbar/translations/pl.json b/homeassistant/components/lg_soundbar/translations/pl.json new file mode 100644 index 00000000000..1b84366cfa4 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/pl.json b/homeassistant/components/life360/translations/pl.json index 30ba7ddc5b6..2b1c7138079 100644 --- a/homeassistant/components/life360/translations/pl.json +++ b/homeassistant/components/life360/translations/pl.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", "invalid_auth": "Niepoprawne uwierzytelnienie", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "Konto jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", "invalid_username": "Nieprawid\u0142owa nazwa u\u017cytkownika", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "title": "Ponownie uwierzytelnij integracj\u0119" + }, "user": { "data": { "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" }, "description": "Aby skonfigurowa\u0107 zaawansowane ustawienia, zapoznaj si\u0119 z [dokumentacj\u0105 Life360]({docs_url}). Mo\u017cesz to zrobi\u0107 przed dodaniem kont.", - "title": "Informacje o koncie Life360" + "title": "Konfiguracja konta Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Poka\u017c dane drogowe jako stan", + "driving_speed": "Pr\u0119dko\u015b\u0107 jazdy", + "limit_gps_acc": "Ogranicz dok\u0142adno\u015b\u0107 GPS", + "max_gps_accuracy": "Maksymalna dok\u0142adno\u015b\u0107 GPS (w metrach)", + "set_drive_speed": "Ustaw pr\u00f3g dla pr\u0119dko\u015bci jazdy" + }, + "title": "Opcje konta" } } } diff --git a/homeassistant/components/nextdns/translations/ca.json b/homeassistant/components/nextdns/translations/ca.json new file mode 100644 index 00000000000..83a2c018da5 --- /dev/null +++ b/homeassistant/components/nextdns/translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Aquest perfil NextDNS ja est\u00e0 configurat." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_api_key": "Clau API inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "profiles": { + "data": { + "profile": "Perfil" + } + }, + "user": { + "data": { + "api_key": "Clau API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/de.json b/homeassistant/components/nextdns/translations/de.json new file mode 100644 index 00000000000..58e470e06ff --- /dev/null +++ b/homeassistant/components/nextdns/translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Dieses NextDNS-Profil ist bereits konfiguriert." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "API-Schl\u00fcssel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/el.json b/homeassistant/components/nextdns/translations/el.json new file mode 100644 index 00000000000..2d5003a1d7a --- /dev/null +++ b/homeassistant/components/nextdns/translations/el.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0391\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03bf\u03c6\u03af\u03bb NextDNS \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af." + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_api_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "profiles": { + "data": { + "profile": "\u03a0\u03c1\u03bf\u03c6\u03af\u03bb" + } + }, + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/et.json b/homeassistant/components/nextdns/translations/et.json new file mode 100644 index 00000000000..9b4d56f51d4 --- /dev/null +++ b/homeassistant/components/nextdns/translations/et.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "NextDNS-profiil on juba seadistatud." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_api_key": "Vigane API v\u00f5ti", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "profiles": { + "data": { + "profile": "Profiil" + } + }, + "user": { + "data": { + "api_key": "API v\u00f5ti" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/hu.json b/homeassistant/components/nextdns/translations/hu.json new file mode 100644 index 00000000000..33b5d858e8a --- /dev/null +++ b/homeassistant/components/nextdns/translations/hu.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Ez a NextDNS profil m\u00e1r be van \u00e1ll\u00edtva." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "API kulcs" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/ja.json b/homeassistant/components/nextdns/translations/ja.json new file mode 100644 index 00000000000..7c1f0614fc1 --- /dev/null +++ b/homeassistant/components/nextdns/translations/ja.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u3053\u306eNextDNS\u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "profiles": { + "data": { + "profile": "\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb" + } + }, + "user": { + "data": { + "api_key": "API\u30ad\u30fc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/pt-BR.json b/homeassistant/components/nextdns/translations/pt-BR.json new file mode 100644 index 00000000000..2982a6daad7 --- /dev/null +++ b/homeassistant/components/nextdns/translations/pt-BR.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Este perfil NextDNS j\u00e1 est\u00e1 configurado." + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_api_key": "Chave de API inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "profiles": { + "data": { + "profile": "Perfil" + } + }, + "user": { + "data": { + "api_key": "Chave de API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/zh-Hant.json b/homeassistant/components/nextdns/translations/zh-Hant.json new file mode 100644 index 00000000000..19d2f137a39 --- /dev/null +++ b/homeassistant/components/nextdns/translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "NextDNS \u500b\u4eba\u8a2d\u5b9a\u5df2\u5b8c\u6210\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_api_key": "API \u91d1\u9470\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "profiles": { + "data": { + "profile": "\u500b\u4eba\u8a2d\u5b9a" + } + }, + "user": { + "data": { + "api_key": "API \u91d1\u9470" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/pl.json b/homeassistant/components/nina/translations/pl.json index 631127679c1..60c97a1c1c0 100644 --- a/homeassistant/components/nina/translations/pl.json +++ b/homeassistant/components/nina/translations/pl.json @@ -23,5 +23,27 @@ "title": "Wybierz miasto/powiat" } } + }, + "options": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "no_selection": "Prosz\u0119 wybra\u0107 przynajmniej jedno miasto lub powiat", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Miasto/powiat (A-D)", + "_e_to_h": "Miasto/powiat (E-H)", + "_i_to_l": "Miasto/powiat (I-L)", + "_m_to_q": "Miasto/powiat (M-Q)", + "_r_to_u": "Miasto/powiat (R-U)", + "_v_to_z": "Miasto/powiat (V-Z)", + "corona_filter": "Usu\u0144 ostrze\u017cenia o koronawirusie", + "slots": "Maksymalna liczba ostrze\u017ce\u0144 na miasto/powiat" + }, + "title": "Opcje" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/pl.json b/homeassistant/components/qnap_qsw/translations/pl.json index e90d527a7a7..f33acb2c0d6 100644 --- a/homeassistant/components/qnap_qsw/translations/pl.json +++ b/homeassistant/components/qnap_qsw/translations/pl.json @@ -9,6 +9,12 @@ "invalid_auth": "Niepoprawne uwierzytelnienie" }, "step": { + "discovered_connection": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + }, "user": { "data": { "password": "Has\u0142o", diff --git a/homeassistant/components/simplepush/translations/pl.json b/homeassistant/components/simplepush/translations/pl.json new file mode 100644 index 00000000000..fe19feb39a1 --- /dev/null +++ b/homeassistant/components/simplepush/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" + }, + "step": { + "user": { + "data": { + "device_key": "Klucz Twojego urz\u0105dzenia", + "event": "Wydarzenie dla wydarze\u0144.", + "name": "Nazwa", + "password": "Has\u0142o szyfrowania u\u017cywane przez Twoje urz\u0105dzenie", + "salt": "Salt u\u017cywane przez Twoje urz\u0105dzenie." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/pl.json b/homeassistant/components/soundtouch/translations/pl.json new file mode 100644 index 00000000000..10a760e1acf --- /dev/null +++ b/homeassistant/components/soundtouch/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" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + }, + "zeroconf_confirm": { + "description": "Zamierzasz doda\u0107 urz\u0105dzenie SoundTouch o nazwie `{name}` do Home Assistanta.", + "title": "Potwierd\u017a dodanie urz\u0105dzenia Bose SoundTouch" + } + } + } +} \ No newline at end of file From 110d9232cd527b471f883e67bfc8e3db0163775c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 5 Jul 2022 08:05:53 +0200 Subject: [PATCH 2142/3516] Remove melcloud from mypy ignore list (#74410) --- homeassistant/components/melcloud/__init__.py | 4 +-- homeassistant/components/melcloud/climate.py | 33 +++++++++++-------- mypy.ini | 6 ---- script/hassfest/mypy_config.py | 2 -- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 6342c2bc3be..1bb65a943f8 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -142,7 +142,7 @@ class MelCloudDevice: ) -async def mel_devices_setup(hass, token) -> list[MelCloudDevice]: +async def mel_devices_setup(hass, token) -> dict[str, list[MelCloudDevice]]: """Query connected devices from MELCloud.""" session = async_get_clientsession(hass) try: @@ -156,7 +156,7 @@ async def mel_devices_setup(hass, token) -> list[MelCloudDevice]: except (asyncio.TimeoutError, ClientConnectionError) as ex: raise ConfigEntryNotReady() from ex - wrapped_devices = {} + wrapped_devices: dict[str, list[MelCloudDevice]] = {} for device_type, devices in all_devices.items(): wrapped_devices[device_type] = [MelCloudDevice(device) for device in devices] return wrapped_devices diff --git a/homeassistant/components/melcloud/climate.py b/homeassistant/components/melcloud/climate.py index b16221923de..a0ffe3a68bb 100644 --- a/homeassistant/components/melcloud/climate.py +++ b/homeassistant/components/melcloud/climate.py @@ -66,16 +66,19 @@ async def async_setup_entry( ) -> None: """Set up MelCloud device climate based on config_entry.""" mel_devices = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + entities: list[AtaDeviceClimate | AtwDeviceZoneClimate] = [ + AtaDeviceClimate(mel_device, mel_device.device) + for mel_device in mel_devices[DEVICE_TYPE_ATA] + ] + entities.extend( [ - AtaDeviceClimate(mel_device, mel_device.device) - for mel_device in mel_devices[DEVICE_TYPE_ATA] - ] - + [ AtwDeviceZoneClimate(mel_device, mel_device.device, zone) for mel_device in mel_devices[DEVICE_TYPE_ATW] for zone in mel_device.device.zones - ], + ] + ) + async_add_entities( + entities, True, ) @@ -157,14 +160,16 @@ class AtaDeviceClimate(MelCloudClimate): return attr @property - def hvac_mode(self) -> HVACMode: + def hvac_mode(self) -> HVACMode | None: """Return hvac operation ie. heat, cool mode.""" mode = self._device.operation_mode if not self._device.power or mode is None: return HVACMode.OFF return ATA_HVAC_MODE_LOOKUP.get(mode) - def _apply_set_hvac_mode(self, hvac_mode: str, set_dict: dict[str, Any]) -> None: + def _apply_set_hvac_mode( + self, hvac_mode: HVACMode, set_dict: dict[str, Any] + ) -> None: """Apply hvac mode changes to a dict used to call _device.set.""" if hvac_mode == HVACMode.OFF: set_dict["power"] = False @@ -180,7 +185,7 @@ class AtaDeviceClimate(MelCloudClimate): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" - set_dict = {} + set_dict: dict[str, Any] = {} self._apply_set_hvac_mode(hvac_mode, set_dict) await self._device.set(set_dict) @@ -188,7 +193,9 @@ class AtaDeviceClimate(MelCloudClimate): def hvac_modes(self) -> list[HVACMode]: """Return the list of available hvac operation modes.""" return [HVACMode.OFF] + [ - ATA_HVAC_MODE_LOOKUP.get(mode) for mode in self._device.operation_modes + ATA_HVAC_MODE_LOOKUP[mode] + for mode in self._device.operation_modes + if mode in ATA_HVAC_MODE_LOOKUP ] @property @@ -201,9 +208,9 @@ class AtaDeviceClimate(MelCloudClimate): """Return the temperature we try to reach.""" return self._device.target_temperature - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - set_dict = {} + set_dict: dict[str, Any] = {} if ATTR_HVAC_MODE in kwargs: self._apply_set_hvac_mode( kwargs.get(ATTR_HVAC_MODE, self.hvac_mode), set_dict @@ -255,7 +262,7 @@ class AtaDeviceClimate(MelCloudClimate): await self.async_set_vane_vertical(swing_mode) @property - def swing_modes(self) -> str | None: + def swing_modes(self) -> list[str] | None: """Return a list of available vertical vane positions and modes.""" return self._device.vane_vertical_positions diff --git a/mypy.ini b/mypy.ini index 30bc97f545a..91dc7f9e71f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2761,12 +2761,6 @@ ignore_errors = true [mypy-homeassistant.components.lyric.sensor] ignore_errors = true -[mypy-homeassistant.components.melcloud] -ignore_errors = true - -[mypy-homeassistant.components.melcloud.climate] -ignore_errors = true - [mypy-homeassistant.components.meteo_france.sensor] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 939290ca9ab..d987a76d41d 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -62,8 +62,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.lyric.climate", "homeassistant.components.lyric.config_flow", "homeassistant.components.lyric.sensor", - "homeassistant.components.melcloud", - "homeassistant.components.melcloud.climate", "homeassistant.components.meteo_france.sensor", "homeassistant.components.meteo_france.weather", "homeassistant.components.minecraft_server", From b09aaba421d6d6178d582bef9ea363017e55639d Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Tue, 5 Jul 2022 10:16:38 +0300 Subject: [PATCH 2143/3516] Add type hints and code cleanup for mikrotik (#74296) * Add type hints and code cleanup for mikrotik * update test and increase coverage * move setup_mikrotik_entry to __init__.py --- .coveragerc | 1 - homeassistant/components/mikrotik/__init__.py | 33 +-- .../components/mikrotik/config_flow.py | 17 +- homeassistant/components/mikrotik/const.py | 48 +++-- .../components/mikrotik/device_tracker.py | 50 +++-- homeassistant/components/mikrotik/hub.py | 203 ++++++++---------- tests/components/mikrotik/__init__.py | 40 ++++ .../mikrotik/test_device_tracker.py | 122 ++++++++--- tests/components/mikrotik/test_hub.py | 120 ----------- tests/components/mikrotik/test_init.py | 1 - 10 files changed, 302 insertions(+), 333 deletions(-) delete mode 100644 tests/components/mikrotik/test_hub.py diff --git a/.coveragerc b/.coveragerc index 21534333583..97f26d8adc5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -717,7 +717,6 @@ omit = homeassistant/components/microsoft/tts.py homeassistant/components/miflora/sensor.py homeassistant/components/mikrotik/hub.py - homeassistant/components/mikrotik/device_tracker.py homeassistant/components/mill/climate.py homeassistant/components/mill/const.py homeassistant/components/mill/sensor.py diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index 856495dc0f2..f72c79c1559 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -1,35 +1,41 @@ """The Mikrotik component.""" from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from .const import ATTR_MANUFACTURER, DOMAIN, PLATFORMS -from .hub import MikrotikDataUpdateCoordinator +from .errors import CannotConnect, LoginError +from .hub import MikrotikDataUpdateCoordinator, get_api CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up the Mikrotik component.""" - - hub = MikrotikDataUpdateCoordinator(hass, config_entry) - if not await hub.async_setup(): + try: + api = await hass.async_add_executor_job(get_api, dict(config_entry.data)) + except CannotConnect as api_error: + raise ConfigEntryNotReady from api_error + except LoginError: return False - await hub.async_config_entry_first_refresh() + coordinator = MikrotikDataUpdateCoordinator(hass, config_entry, api) + await hass.async_add_executor_job(coordinator.api.get_hub_details) + await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = hub + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={(DOMAIN, hub.serial_num)}, + connections={(DOMAIN, coordinator.serial_num)}, manufacturer=ATTR_MANUFACTURER, - model=hub.model, - name=hub.hostname, - sw_version=hub.firmware, + model=coordinator.model, + name=coordinator.hostname, + sw_version=coordinator.firmware, ) return True @@ -37,10 +43,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms( + if unload_ok := await hass.config_entries.async_unload_platforms( config_entry, PLATFORMS - ) - - hass.data[DOMAIN].pop(config_entry.entry_id) + ): + hass.data[DOMAIN].pop(config_entry.entry_id) return unload_ok diff --git a/homeassistant/components/mikrotik/config_flow.py b/homeassistant/components/mikrotik/config_flow.py index 36b65b6f2ba..d506c2c75e4 100644 --- a/homeassistant/components/mikrotik/config_flow.py +++ b/homeassistant/components/mikrotik/config_flow.py @@ -1,6 +1,8 @@ """Config flow for Mikrotik.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant import config_entries @@ -13,6 +15,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from .const import ( CONF_ARP_PING, @@ -40,7 +43,9 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return MikrotikOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: @@ -52,7 +57,7 @@ class MikrotikFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): break try: - await self.hass.async_add_executor_job(get_api, self.hass, user_input) + await self.hass.async_add_executor_job(get_api, user_input) except CannotConnect: errors["base"] = "cannot_connect" except LoginError: @@ -86,11 +91,15 @@ class MikrotikOptionsFlowHandler(config_entries.OptionsFlow): """Initialize Mikrotik options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the Mikrotik options.""" return await self.async_step_device_tracker() - async def async_step_device_tracker(self, user_input=None): + async def async_step_device_tracker( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the device tracker options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/mikrotik/const.py b/homeassistant/components/mikrotik/const.py index b328c10a602..2942e6981fa 100644 --- a/homeassistant/components/mikrotik/const.py +++ b/homeassistant/components/mikrotik/const.py @@ -1,33 +1,35 @@ """Constants used in the Mikrotik components.""" +from typing import Final + from homeassistant.const import Platform -DOMAIN = "mikrotik" -DEFAULT_NAME = "Mikrotik" -DEFAULT_API_PORT = 8728 -DEFAULT_DETECTION_TIME = 300 +DOMAIN: Final = "mikrotik" +DEFAULT_NAME: Final = "Mikrotik" +DEFAULT_API_PORT: Final = 8728 +DEFAULT_DETECTION_TIME: Final = 300 -ATTR_MANUFACTURER = "Mikrotik" -ATTR_SERIAL_NUMBER = "serial-number" -ATTR_FIRMWARE = "current-firmware" -ATTR_MODEL = "model" +ATTR_MANUFACTURER: Final = "Mikrotik" +ATTR_SERIAL_NUMBER: Final = "serial-number" +ATTR_FIRMWARE: Final = "current-firmware" +ATTR_MODEL: Final = "model" -CONF_ARP_PING = "arp_ping" -CONF_FORCE_DHCP = "force_dhcp" -CONF_DETECTION_TIME = "detection_time" +CONF_ARP_PING: Final = "arp_ping" +CONF_FORCE_DHCP: Final = "force_dhcp" +CONF_DETECTION_TIME: Final = "detection_time" -NAME = "name" -INFO = "info" -IDENTITY = "identity" -ARP = "arp" +NAME: Final = "name" +INFO: Final = "info" +IDENTITY: Final = "identity" +ARP: Final = "arp" -CAPSMAN = "capsman" -DHCP = "dhcp" -WIRELESS = "wireless" -IS_WIRELESS = "is_wireless" -IS_CAPSMAN = "is_capsman" +CAPSMAN: Final = "capsman" +DHCP: Final = "dhcp" +WIRELESS: Final = "wireless" +IS_WIRELESS: Final = "is_wireless" +IS_CAPSMAN: Final = "is_capsman" -MIKROTIK_SERVICES = { +MIKROTIK_SERVICES: Final = { ARP: "/ip/arp/getall", CAPSMAN: "/caps-man/registration-table/getall", DHCP: "/ip/dhcp-server/lease/getall", @@ -38,9 +40,9 @@ MIKROTIK_SERVICES = { IS_CAPSMAN: "/caps-man/interface/print", } -PLATFORMS = [Platform.DEVICE_TRACKER] +PLATFORMS: Final = [Platform.DEVICE_TRACKER] -ATTR_DEVICE_TRACKER = [ +ATTR_DEVICE_TRACKER: Final = [ "comment", "mac-address", "ssid", diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 9389d3bea5c..4e002cdbcfe 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -1,6 +1,8 @@ """Support for Mikrotik routers as device tracker.""" from __future__ import annotations +from typing import Any + from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import ( DOMAIN as DEVICE_TRACKER, @@ -14,7 +16,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity import homeassistant.util.dt as dt_util from .const import DOMAIN -from .hub import MikrotikDataUpdateCoordinator +from .hub import Device, MikrotikDataUpdateCoordinator # These are normalized to ATTR_IP and ATTR_MAC to conform # to device_tracker @@ -27,7 +29,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up device tracker for Mikrotik component.""" - hub = hass.data[DOMAIN][config_entry.entry_id] + coordinator: MikrotikDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] tracked: dict[str, MikrotikDataUpdateCoordinatorTracker] = {} @@ -42,47 +46,53 @@ async def async_setup_entry( ): if ( - entity.unique_id in hub.api.devices - or entity.unique_id not in hub.api.all_devices + entity.unique_id in coordinator.api.devices + or entity.unique_id not in coordinator.api.all_devices ): continue - hub.api.restore_device(entity.unique_id) + coordinator.api.restore_device(entity.unique_id) @callback - def update_hub(): + def update_hub() -> None: """Update the status of the device.""" - update_items(hub, async_add_entities, tracked) + update_items(coordinator, async_add_entities, tracked) - config_entry.async_on_unload(hub.async_add_listener(update_hub)) + config_entry.async_on_unload(coordinator.async_add_listener(update_hub)) update_hub() @callback -def update_items(hub, async_add_entities, tracked): +def update_items( + coordinator: MikrotikDataUpdateCoordinator, + async_add_entities: AddEntitiesCallback, + tracked: dict[str, MikrotikDataUpdateCoordinatorTracker], +): """Update tracked device state from the hub.""" - new_tracked = [] - for mac, device in hub.api.devices.items(): + new_tracked: list[MikrotikDataUpdateCoordinatorTracker] = [] + for mac, device in coordinator.api.devices.items(): if mac not in tracked: - tracked[mac] = MikrotikDataUpdateCoordinatorTracker(device, hub) + tracked[mac] = MikrotikDataUpdateCoordinatorTracker(device, coordinator) new_tracked.append(tracked[mac]) if new_tracked: async_add_entities(new_tracked) -class MikrotikDataUpdateCoordinatorTracker(CoordinatorEntity, ScannerEntity): +class MikrotikDataUpdateCoordinatorTracker( + CoordinatorEntity[MikrotikDataUpdateCoordinator], ScannerEntity +): """Representation of network device.""" - coordinator: MikrotikDataUpdateCoordinator - - def __init__(self, device, hub): + def __init__( + self, device: Device, coordinator: MikrotikDataUpdateCoordinator + ) -> None: """Initialize the tracked device.""" - super().__init__(hub) + super().__init__(coordinator) self.device = device @property - def is_connected(self): + def is_connected(self) -> bool: """Return true if the client is connected to the network.""" if ( self.device.last_seen @@ -93,7 +103,7 @@ class MikrotikDataUpdateCoordinatorTracker(CoordinatorEntity, ScannerEntity): return False @property - def source_type(self): + def source_type(self) -> str: """Return the source type of the client.""" return SOURCE_TYPE_ROUTER @@ -124,7 +134,7 @@ class MikrotikDataUpdateCoordinatorTracker(CoordinatorEntity, ScannerEntity): return self.device.mac @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the device state attributes.""" if self.is_connected: return {k: v for k, v in self.device.attrs.items() if k not in FILTER_ATTRS} diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 7f2314bd057..9219159ca74 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -1,14 +1,18 @@ """The Mikrotik router class.""" -from datetime import timedelta +from __future__ import annotations + +from datetime import datetime, timedelta import logging import socket import ssl +from typing import Any import librouteros from librouteros.login import plain as login_plain, token as login_token +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import slugify import homeassistant.util.dt as dt_util @@ -42,36 +46,36 @@ _LOGGER = logging.getLogger(__name__) class Device: """Represents a network device.""" - def __init__(self, mac, params): + def __init__(self, mac: str, params: dict[str, Any]) -> None: """Initialize the network device.""" self._mac = mac self._params = params - self._last_seen = None - self._attrs = {} - self._wireless_params = None + self._last_seen: datetime | None = None + self._attrs: dict[str, Any] = {} + self._wireless_params: dict[str, Any] = {} @property - def name(self): + def name(self) -> str: """Return device name.""" return self._params.get("host-name", self.mac) @property - def ip_address(self): + def ip_address(self) -> str: """Return device primary ip address.""" - return self._params.get("address") + return self._params["address"] @property - def mac(self): + def mac(self) -> str: """Return device mac.""" return self._mac @property - def last_seen(self): + def last_seen(self) -> datetime | None: """Return device last seen.""" return self._last_seen @property - def attrs(self): + def attrs(self) -> dict[str, Any]: """Return device attributes.""" attr_data = self._wireless_params if self._wireless_params else self._params for attr in ATTR_DEVICE_TRACKER: @@ -80,7 +84,12 @@ class Device: self._attrs["ip_address"] = self._params.get("active-address") return self._attrs - def update(self, wireless_params=None, params=None, active=False): + def update( + self, + wireless_params: dict[str, Any] | None = None, + params: dict[str, Any] | None = None, + active: bool = False, + ) -> None: """Update Device params.""" if wireless_params: self._wireless_params = wireless_params @@ -93,27 +102,26 @@ class Device: class MikrotikData: """Handle all communication with the Mikrotik API.""" - def __init__(self, hass, config_entry, api): + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, api: librouteros.Api + ) -> None: """Initialize the Mikrotik Client.""" self.hass = hass self.config_entry = config_entry self.api = api - self._host = self.config_entry.data[CONF_HOST] - self.all_devices = {} - self.devices = {} - self.available = True - self.support_capsman = False - self.support_wireless = False - self.hostname = None - self.model = None - self.firmware = None - self.serial_number = None + self._host: str = self.config_entry.data[CONF_HOST] + self.all_devices: dict[str, dict[str, Any]] = {} + self.devices: dict[str, Device] = {} + self.support_capsman: bool = False + self.support_wireless: bool = False + self.hostname: str = "" + self.model: str = "" + self.firmware: str = "" + self.serial_number: str = "" @staticmethod - def load_mac(devices=None): + def load_mac(devices: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: """Load dictionary using MAC address as key.""" - if not devices: - return None mac_devices = {} for device in devices: if "mac-address" in device: @@ -122,26 +130,23 @@ class MikrotikData: return mac_devices @property - def arp_enabled(self): + def arp_enabled(self) -> bool: """Return arp_ping option setting.""" - return self.config_entry.options[CONF_ARP_PING] + return self.config_entry.options.get(CONF_ARP_PING, False) @property - def force_dhcp(self): + def force_dhcp(self) -> bool: """Return force_dhcp option setting.""" - return self.config_entry.options[CONF_FORCE_DHCP] + return self.config_entry.options.get(CONF_FORCE_DHCP, False) - def get_info(self, param): + def get_info(self, param: str) -> str: """Return device model name.""" cmd = IDENTITY if param == NAME else INFO - data = self.command(MIKROTIK_SERVICES[cmd]) - return ( - data[0].get(param) # pylint: disable=unsubscriptable-object - if data - else None - ) + if data := self.command(MIKROTIK_SERVICES[cmd]): + return str(data[0].get(param)) + return "" - def get_hub_details(self): + def get_hub_details(self) -> None: """Get Hub info.""" self.hostname = self.get_info(NAME) self.model = self.get_info(ATTR_MODEL) @@ -150,24 +155,17 @@ class MikrotikData: self.support_capsman = bool(self.command(MIKROTIK_SERVICES[IS_CAPSMAN])) self.support_wireless = bool(self.command(MIKROTIK_SERVICES[IS_WIRELESS])) - def connect_to_hub(self): - """Connect to hub.""" - try: - self.api = get_api(self.hass, self.config_entry.data) - return True - except (LoginError, CannotConnect): - return False - - def get_list_from_interface(self, interface): + def get_list_from_interface(self, interface: str) -> dict[str, dict[str, Any]]: """Get devices from interface.""" - result = self.command(MIKROTIK_SERVICES[interface]) - return self.load_mac(result) if result else {} + if result := self.command(MIKROTIK_SERVICES[interface]): + return self.load_mac(result) + return {} - def restore_device(self, mac): + def restore_device(self, mac: str) -> None: """Restore a missing device after restart.""" self.devices[mac] = Device(mac, self.all_devices[mac]) - def update_devices(self): + def update_devices(self) -> None: """Get list of devices with latest status.""" arp_devices = {} device_list = {} @@ -192,7 +190,7 @@ class MikrotikData: # get new hub firmware version if updated self.firmware = self.get_info(ATTR_FIRMWARE) - except (CannotConnect, socket.timeout, OSError) as err: + except (CannotConnect, LoginError) as err: raise UpdateFailed from err if not device_list: @@ -218,11 +216,12 @@ class MikrotikData: active = True if self.arp_enabled and mac in arp_devices: active = self.do_arp_ping( - params.get("active-address"), arp_devices[mac].get("interface") + str(params.get("active-address")), + str(arp_devices[mac].get("interface")), ) self.devices[mac].update(active=active) - def do_arp_ping(self, ip_address, interface): + def do_arp_ping(self, ip_address: str, interface: str) -> bool: """Attempt to arp ping MAC address via interface.""" _LOGGER.debug("pinging - %s", ip_address) params = { @@ -234,9 +233,9 @@ class MikrotikData: } cmd = "/ping" data = self.command(cmd, params) - if data is not None: + if data: status = 0 - for result in data: # pylint: disable=not-an-iterable + for result in data: if "status" in result: status += 1 if status == len(data): @@ -246,22 +245,25 @@ class MikrotikData: return False return True - def command(self, cmd, params=None): + def command( + self, cmd: str, params: dict[str, Any] | None = None + ) -> list[dict[str, Any]]: """Retrieve data from Mikrotik API.""" try: _LOGGER.info("Running command %s", cmd) if params: - response = list(self.api(cmd=cmd, **params)) - else: - response = list(self.api(cmd=cmd)) + return list(self.api(cmd=cmd, **params)) + return list(self.api(cmd=cmd)) except ( librouteros.exceptions.ConnectionClosed, OSError, socket.timeout, ) as api_error: _LOGGER.error("Mikrotik %s connection error %s", self._host, api_error) - if not self.connect_to_hub(): - raise CannotConnect from api_error + # try to reconnect + self.api = get_api(dict(self.config_entry.data)) + # we still have to raise CannotConnect to fail the update. + raise CannotConnect from api_error except librouteros.exceptions.ProtocolError as api_error: _LOGGER.warning( "Mikrotik %s failed to retrieve data. cmd=[%s] Error: %s", @@ -269,106 +271,71 @@ class MikrotikData: cmd, api_error, ) - return None - - return response if response else None + return [] class MikrotikDataUpdateCoordinator(DataUpdateCoordinator): """Mikrotik Hub Object.""" - def __init__(self, hass, config_entry): + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, api: librouteros.Api + ) -> None: """Initialize the Mikrotik Client.""" self.hass = hass - self.config_entry = config_entry - self._mk_data = None + self.config_entry: ConfigEntry = config_entry + self._mk_data = MikrotikData(self.hass, self.config_entry, api) super().__init__( self.hass, _LOGGER, name=f"{DOMAIN} - {self.host}", - update_method=self.async_update, update_interval=timedelta(seconds=10), ) @property - def host(self): + def host(self) -> str: """Return the host of this hub.""" return self.config_entry.data[CONF_HOST] @property - def hostname(self): + def hostname(self) -> str: """Return the hostname of the hub.""" return self._mk_data.hostname @property - def model(self): + def model(self) -> str: """Return the model of the hub.""" return self._mk_data.model @property - def firmware(self): + def firmware(self) -> str: """Return the firmware of the hub.""" return self._mk_data.firmware @property - def serial_num(self): + def serial_num(self) -> str: """Return the serial number of the hub.""" return self._mk_data.serial_number @property - def available(self): - """Return if the hub is connected.""" - return self._mk_data.available - - @property - def option_detection_time(self): + def option_detection_time(self) -> timedelta: """Config entry option defining number of seconds from last seen to away.""" - return timedelta(seconds=self.config_entry.options[CONF_DETECTION_TIME]) + return timedelta( + seconds=self.config_entry.options.get( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ) + ) @property - def api(self): + def api(self) -> MikrotikData: """Represent Mikrotik data object.""" return self._mk_data - async def async_add_options(self): - """Populate default options for Mikrotik.""" - if not self.config_entry.options: - data = dict(self.config_entry.data) - options = { - CONF_ARP_PING: data.pop(CONF_ARP_PING, False), - CONF_FORCE_DHCP: data.pop(CONF_FORCE_DHCP, False), - CONF_DETECTION_TIME: data.pop( - CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME - ), - } - - self.hass.config_entries.async_update_entry( - self.config_entry, data=data, options=options - ) - - async def async_update(self): + async def _async_update_data(self) -> None: """Update Mikrotik devices information.""" await self.hass.async_add_executor_job(self._mk_data.update_devices) - async def async_setup(self): - """Set up the Mikrotik hub.""" - try: - api = await self.hass.async_add_executor_job( - get_api, self.hass, self.config_entry.data - ) - except CannotConnect as api_error: - raise ConfigEntryNotReady from api_error - except LoginError: - return False - self._mk_data = MikrotikData(self.hass, self.config_entry, api) - await self.async_add_options() - await self.hass.async_add_executor_job(self._mk_data.get_hub_details) - - return True - - -def get_api(hass, entry): +def get_api(entry: dict[str, Any]) -> librouteros.Api: """Connect to Mikrotik hub.""" _LOGGER.debug("Connecting to Mikrotik hub [%s]", entry[CONF_HOST]) diff --git a/tests/components/mikrotik/__init__.py b/tests/components/mikrotik/__init__.py index 6f67eea1a0a..cebbd982350 100644 --- a/tests/components/mikrotik/__init__.py +++ b/tests/components/mikrotik/__init__.py @@ -1,4 +1,7 @@ """Tests for the Mikrotik component.""" +from unittest.mock import patch + +from homeassistant.components import mikrotik from homeassistant.components.mikrotik.const import ( CONF_ARP_PING, CONF_DETECTION_TIME, @@ -14,6 +17,8 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) +from tests.common import MockConfigEntry + MOCK_DATA = { CONF_NAME: "Mikrotik", CONF_HOST: "0.0.0.0", @@ -130,3 +135,38 @@ ARP_DATA = [ "disabled": False, }, ] + + +async def setup_mikrotik_entry(hass, **kwargs): + """Set up Mikrotik integration successfully.""" + support_wireless = kwargs.get("support_wireless", True) + dhcp_data = kwargs.get("dhcp_data", DHCP_DATA) + wireless_data = kwargs.get("wireless_data", WIRELESS_DATA) + + def mock_command(self, cmd, params=None): + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.IS_WIRELESS]: + return support_wireless + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.DHCP]: + return dhcp_data + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.WIRELESS]: + return wireless_data + if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.ARP]: + return ARP_DATA + return {} + + config_entry = MockConfigEntry( + domain=mikrotik.DOMAIN, data=MOCK_DATA, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + + if "force_dhcp" in kwargs: + config_entry.options = {**config_entry.options, "force_dhcp": True} + + if "arp_ping" in kwargs: + config_entry.options = {**config_entry.options, "arp_ping": True} + + with patch("librouteros.connect"), patch.object( + mikrotik.hub.MikrotikData, "command", new=mock_command + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/mikrotik/test_device_tracker.py b/tests/components/mikrotik/test_device_tracker.py index fbbb016d09f..e3efe6bd39d 100644 --- a/tests/components/mikrotik/test_device_tracker.py +++ b/tests/components/mikrotik/test_device_tracker.py @@ -1,13 +1,14 @@ """The tests for the Mikrotik device tracker platform.""" from datetime import timedelta +from freezegun import freeze_time import pytest from homeassistant.components import mikrotik import homeassistant.components.device_tracker as device_tracker +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util +from homeassistant.util.dt import utcnow from . import ( DEVICE_2_WIRELESS, @@ -17,12 +18,10 @@ from . import ( MOCK_DATA, MOCK_OPTIONS, WIRELESS_DATA, + setup_mikrotik_entry, ) -from .test_hub import setup_mikrotik_entry -from tests.common import MockConfigEntry, patch - -DEFAULT_DETECTION_TIME = timedelta(seconds=300) +from tests.common import MockConfigEntry, async_fire_time_changed, patch @pytest.fixture @@ -56,24 +55,11 @@ def mock_command(self, cmd, params=None): return {} -async def test_platform_manually_configured(hass): - """Test that nothing happens when configuring mikrotik through device tracker platform.""" - assert ( - await async_setup_component( - hass, - device_tracker.DOMAIN, - {device_tracker.DOMAIN: {"platform": "mikrotik"}}, - ) - is False - ) - assert mikrotik.DOMAIN not in hass.data - - async def test_device_trackers(hass, mock_device_registry_devices): """Test device_trackers created by mikrotik.""" # test devices are added from wireless list only - hub = await setup_mikrotik_entry(hass) + await setup_mikrotik_entry(hass) device_1 = hass.states.get("device_tracker.device_1") assert device_1 is not None @@ -90,7 +76,7 @@ async def test_device_trackers(hass, mock_device_registry_devices): # test device_2 is added after connecting to wireless network WIRELESS_DATA.append(DEVICE_2_WIRELESS) - await hub.async_refresh() + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() device_2 = hass.states.get("device_tracker.device_2") @@ -104,26 +90,72 @@ async def test_device_trackers(hass, mock_device_registry_devices): # test state remains home if last_seen consider_home_interval del WIRELESS_DATA[1] # device 2 is removed from wireless list - hub.api.devices["00:00:00:00:00:02"]._last_seen = dt_util.utcnow() - timedelta( - minutes=4 - ) - await hub.async_update() - await hass.async_block_till_done() + with freeze_time(utcnow() + timedelta(minutes=4)): + async_fire_time_changed(hass, utcnow() + timedelta(minutes=4)) + await hass.async_block_till_done() device_2 = hass.states.get("device_tracker.device_2") - assert device_2.state != "not_home" + assert device_2.state == "home" # test state changes to away if last_seen > consider_home_interval - hub.api.devices["00:00:00:00:00:02"]._last_seen = dt_util.utcnow() - timedelta( - minutes=5 - ) - await hub.async_refresh() - await hass.async_block_till_done() + with freeze_time(utcnow() + timedelta(minutes=6)): + async_fire_time_changed(hass, utcnow() + timedelta(minutes=6)) + await hass.async_block_till_done() device_2 = hass.states.get("device_tracker.device_2") assert device_2.state == "not_home" +async def test_force_dhcp(hass, mock_device_registry_devices): + """Test updating hub that supports wireless with forced dhcp method.""" + + # hub supports wireless by default, force_dhcp is enabled to override + await setup_mikrotik_entry(hass, force_dhcp=False) + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 + assert device_1.state == "home" + # device_2 is not on the wireless list but it is still added from DHCP + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 + assert device_2.state == "home" + + +async def test_hub_not_support_wireless(hass, mock_device_registry_devices): + """Test device_trackers created when hub doesn't support wireless.""" + + await setup_mikrotik_entry(hass, support_wireless=False) + device_1 = hass.states.get("device_tracker.device_1") + assert device_1 + assert device_1.state == "home" + # device_2 is added from DHCP + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 + assert device_2.state == "home" + + +async def test_arp_ping_success(hass, mock_device_registry_devices): + """Test arp ping devices to confirm they are connected.""" + + with patch.object(mikrotik.hub.MikrotikData, "do_arp_ping", return_value=True): + await setup_mikrotik_entry(hass, arp_ping=True, force_dhcp=True) + + # test wired device_2 show as home if arp ping returns True + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 + assert device_2.state == "home" + + +async def test_arp_ping_timeout(hass, mock_device_registry_devices): + """Test arp ping timeout so devices are shown away.""" + with patch.object(mikrotik.hub.MikrotikData, "do_arp_ping", return_value=False): + await setup_mikrotik_entry(hass, arp_ping=True, force_dhcp=True) + + # test wired device_2 show as not_home if arp ping times out + device_2 = hass.states.get("device_tracker.device_2") + assert device_2 + assert device_2.state == "not_home" + + async def test_device_trackers_numerical_name(hass, mock_device_registry_devices): """Test device_trackers created by mikrotik with numerical device name.""" @@ -164,6 +196,13 @@ async def test_restoring_devices(hass): suggested_object_id="device_2", config_entry=config_entry, ) + registry.async_get_or_create( + device_tracker.DOMAIN, + mikrotik.DOMAIN, + "00:00:00:00:00:03", + suggested_object_id="device_3", + config_entry=config_entry, + ) await setup_mikrotik_entry(hass) @@ -174,3 +213,22 @@ async def test_restoring_devices(hass): device_2 = hass.states.get("device_tracker.device_2") assert device_2 is not None assert device_2.state == "not_home" + # device_3 is not on the DHCP list or wireless list + # so it won't be restored. + device_3 = hass.states.get("device_tracker.device_3") + assert device_3 is None + + +async def test_update_failed(hass, mock_device_registry_devices): + """Test failing to connect during update.""" + + await setup_mikrotik_entry(hass) + + with patch.object( + mikrotik.hub.MikrotikData, "command", side_effect=mikrotik.errors.CannotConnect + ): + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1.state == STATE_UNAVAILABLE diff --git a/tests/components/mikrotik/test_hub.py b/tests/components/mikrotik/test_hub.py deleted file mode 100644 index 1e056071236..00000000000 --- a/tests/components/mikrotik/test_hub.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Test Mikrotik hub.""" -from unittest.mock import patch - -from homeassistant.components import mikrotik - -from . import ARP_DATA, DHCP_DATA, MOCK_DATA, MOCK_OPTIONS, WIRELESS_DATA - -from tests.common import MockConfigEntry - - -async def setup_mikrotik_entry(hass, **kwargs): - """Set up Mikrotik integration successfully.""" - support_wireless = kwargs.get("support_wireless", True) - dhcp_data = kwargs.get("dhcp_data", DHCP_DATA) - wireless_data = kwargs.get("wireless_data", WIRELESS_DATA) - - def mock_command(self, cmd, params=None): - if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.IS_WIRELESS]: - return support_wireless - if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.DHCP]: - return dhcp_data - if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.WIRELESS]: - return wireless_data - if cmd == mikrotik.const.MIKROTIK_SERVICES[mikrotik.const.ARP]: - return ARP_DATA - return {} - - config_entry = MockConfigEntry( - domain=mikrotik.DOMAIN, data=MOCK_DATA, options=MOCK_OPTIONS - ) - config_entry.add_to_hass(hass) - - if "force_dhcp" in kwargs: - config_entry.options = {**config_entry.options, "force_dhcp": True} - - if "arp_ping" in kwargs: - config_entry.options = {**config_entry.options, "arp_ping": True} - - with patch("librouteros.connect"), patch.object( - mikrotik.hub.MikrotikData, "command", new=mock_command - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - return hass.data[mikrotik.DOMAIN][config_entry.entry_id] - - -async def test_update_failed(hass): - """Test failing to connect during update.""" - - hub = await setup_mikrotik_entry(hass) - - with patch.object( - mikrotik.hub.MikrotikData, "command", side_effect=mikrotik.errors.CannotConnect - ): - await hub.async_refresh() - - assert not hub.last_update_success - - -async def test_hub_not_support_wireless(hass): - """Test updating hub devices when hub doesn't support wireless interfaces.""" - - # test that the devices are constructed from dhcp data - - hub = await setup_mikrotik_entry(hass, support_wireless=False) - - assert hub.api.devices["00:00:00:00:00:01"]._params == DHCP_DATA[0] - assert hub.api.devices["00:00:00:00:00:01"]._wireless_params is None - assert hub.api.devices["00:00:00:00:00:02"]._params == DHCP_DATA[1] - assert hub.api.devices["00:00:00:00:00:02"]._wireless_params is None - - -async def test_hub_support_wireless(hass): - """Test updating hub devices when hub support wireless interfaces.""" - - # test that the device list is from wireless data list - - hub = await setup_mikrotik_entry(hass) - - assert hub.api.support_wireless is True - assert hub.api.devices["00:00:00:00:00:01"]._params == DHCP_DATA[0] - assert hub.api.devices["00:00:00:00:00:01"]._wireless_params == WIRELESS_DATA[0] - - # devices not in wireless list will not be added - assert "00:00:00:00:00:02" not in hub.api.devices - - -async def test_force_dhcp(hass): - """Test updating hub devices with forced dhcp method.""" - - # test that the devices are constructed from dhcp data - - hub = await setup_mikrotik_entry(hass, force_dhcp=True) - - assert hub.api.support_wireless is True - assert hub.api.devices["00:00:00:00:00:01"]._params == DHCP_DATA[0] - assert hub.api.devices["00:00:00:00:00:01"]._wireless_params == WIRELESS_DATA[0] - - # devices not in wireless list are added from dhcp - assert hub.api.devices["00:00:00:00:00:02"]._params == DHCP_DATA[1] - assert hub.api.devices["00:00:00:00:00:02"]._wireless_params is None - - -async def test_arp_ping(hass): - """Test arp ping devices to confirm they are connected.""" - - # test device show as home if arp ping returns value - with patch.object(mikrotik.hub.MikrotikData, "do_arp_ping", return_value=True): - hub = await setup_mikrotik_entry(hass, arp_ping=True, force_dhcp=True) - - assert hub.api.devices["00:00:00:00:00:01"].last_seen is not None - assert hub.api.devices["00:00:00:00:00:02"].last_seen is not None - - # test device show as away if arp ping times out - with patch.object(mikrotik.hub.MikrotikData, "do_arp_ping", return_value=False): - hub = await setup_mikrotik_entry(hass, arp_ping=True, force_dhcp=True) - - assert hub.api.devices["00:00:00:00:00:01"].last_seen is not None - # this device is not wireless so it will show as away - assert hub.api.devices["00:00:00:00:00:02"].last_seen is None diff --git a/tests/components/mikrotik/test_init.py b/tests/components/mikrotik/test_init.py index 5ac408928d8..3d7927174b5 100644 --- a/tests/components/mikrotik/test_init.py +++ b/tests/components/mikrotik/test_init.py @@ -39,7 +39,6 @@ async def test_successful_config_entry(hass): await hass.config_entries.async_setup(entry.entry_id) assert entry.state == ConfigEntryState.LOADED - assert hass.data[DOMAIN][entry.entry_id] async def test_hub_conn_error(hass, mock_api): From 2ee5ac02cf315aff59fb1c86846c4a1b8bd291d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Jul 2022 09:50:36 +0200 Subject: [PATCH 2144/3516] Bump home-assistant/builder from 2022.06.2 to 2022.07.0 (#74446) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 826af2eb9fe..d49f1e3a9c7 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -159,7 +159,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.06.2 + uses: home-assistant/builder@2022.07.0 with: args: | $BUILD_ARGS \ @@ -225,7 +225,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2022.06.2 + uses: home-assistant/builder@2022.07.0 with: args: | $BUILD_ARGS \ From 6422040262be42d11f1da4cd74a852f3b609c31a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 02:51:53 -0500 Subject: [PATCH 2145/3516] Remove asserts from lutron_caseta async_attach_trigger (#74429) --- .../components/lutron_caseta/device_trigger.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index d938ad6e7f2..27227619d45 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -20,6 +20,7 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import ConfigType @@ -429,9 +430,13 @@ async def async_attach_trigger( ) -> CALLBACK_TYPE: """Attach a trigger.""" device_registry = dr.async_get(hass) - device = device_registry.async_get(config[CONF_DEVICE_ID]) - assert device - assert device.model + if ( + not (device := device_registry.async_get(config[CONF_DEVICE_ID])) + or not device.model + ): + raise HomeAssistantError( + f"Cannot attach trigger {config} because device with id {config[CONF_DEVICE_ID]} is missing or invalid" + ) device_type = _device_model_to_type(device.model) _, serial = list(device.identifiers)[0] schema = DEVICE_TYPE_SCHEMA_MAP[device_type] From f975d30258209ad4d133990d5c96e870e71fcf86 Mon Sep 17 00:00:00 2001 From: Arne Mauer Date: Tue, 5 Jul 2022 10:35:05 +0200 Subject: [PATCH 2146/3516] Fix multi_match to match with the IKEA airpurifier channel (#74432) Fix multi_match for FilterLifeTime, device_run_time, filter_run_time sensors for ikea starkvind --- homeassistant/components/zha/number.py | 6 +----- homeassistant/components/zha/sensor.py | 16 ++-------------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index c3d7f352318..e1268e29190 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -526,11 +526,7 @@ class TimerDurationMinutes(ZHANumberConfigurationEntity, id_suffix="timer_durati @CONFIG_DIAGNOSTIC_MATCH( - channel_names="ikea_manufacturer", - manufacturers={ - "IKEA of Sweden", - }, - models={"STARKVIND Air purifier"}, + channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"} ) class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time"): """Representation of a ZHA timer duration configuration entity.""" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 2fe38193ecb..4a4700b3c4c 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -810,13 +810,7 @@ class TimeLeft(Sensor, id_suffix="time_left"): _unit = TIME_MINUTES -@MULTI_MATCH( - channel_names="ikea_manufacturer", - manufacturers={ - "IKEA of Sweden", - }, - models={"STARKVIND Air purifier"}, -) +@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"): """Sensor that displays device run time (in minutes).""" @@ -826,13 +820,7 @@ class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"): _unit = TIME_MINUTES -@MULTI_MATCH( - channel_names="ikea_manufacturer", - manufacturers={ - "IKEA of Sweden", - }, - models={"STARKVIND Air purifier"}, -) +@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) class IkeaFilterRunTime(Sensor, id_suffix="filter_run_time"): """Sensor that displays run time of the current filter (in minutes).""" From b5c55311807de0a1abfc5b9a25a3588e33d0e536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Br=C3=BCckmann?= Date: Tue, 5 Jul 2022 12:25:20 +0200 Subject: [PATCH 2147/3516] Fix unreachable DenonAVR reporting as available when polling fails (#74344) --- homeassistant/components/denonavr/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 8d3102c441b..85e28c29d7c 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -204,12 +204,14 @@ class DenonDevice(MediaPlayerEntity): ) self._available = False except AvrCommandError as err: + available = False _LOGGER.error( "Command %s failed with error: %s", func.__name__, err, ) except DenonAvrError as err: + available = False _LOGGER.error( "Error %s occurred in method %s for Denon AVR receiver", err, From a1a887ddac810eed524de728edb8743845e8f438 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 5 Jul 2022 12:27:00 +0200 Subject: [PATCH 2148/3516] Add GeolocationEvent checks to pylint plugin (#74286) --- pylint/plugins/hass_enforce_type_hints.py | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 8e846cf5db0..7996afdc545 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1205,6 +1205,33 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "geo_location": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="GeolocationEvent", + matches=[ + TypeHintMatch( + function_name="source", + return_type="str", + ), + TypeHintMatch( + function_name="distance", + return_type=["float", None], + ), + TypeHintMatch( + function_name="latitude", + return_type=["float", None], + ), + TypeHintMatch( + function_name="longitude", + return_type=["float", None], + ), + ], + ), + ], "light": [ ClassTypeHintMatch( base_class="Entity", From 809f101f5536e229601d7eec7a85e895e1e52216 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Jul 2022 13:41:33 +0200 Subject: [PATCH 2149/3516] Re-introduce default scan interval in Scrape sensor (#74455) --- homeassistant/components/scrape/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index e15f7c5ba97..88c9b564b29 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -1,6 +1,7 @@ """Support for getting data from websites with scraping.""" from __future__ import annotations +from datetime import timedelta import logging from typing import Any @@ -39,6 +40,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(minutes=10) + CONF_ATTR = "attribute" CONF_SELECT = "select" CONF_INDEX = "index" From f6cb2833cacf0c197b358dec9e7758b5b3d09917 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 09:25:30 -0500 Subject: [PATCH 2150/3516] Improve fans in homekit_controller (#74440) --- .../components/homekit_controller/climate.py | 84 +++++++++++------ .../components/homekit_controller/fan.py | 37 +++++++- .../components/homekit_controller/number.py | 71 --------------- .../homekit_controller/fixtures/haa_fan.json | 4 +- .../specific_devices/test_ecobee_501.py | 3 + .../specific_devices/test_haa_fan.py | 8 +- .../homekit_controller/test_climate.py | 30 +++++++ .../components/homekit_controller/test_fan.py | 77 +++++++++++++++- .../homekit_controller/test_number.py | 89 ------------------- 9 files changed, 205 insertions(+), 198 deletions(-) diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 44ecee13875..b76ed1ea6a9 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -25,6 +25,8 @@ from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + FAN_AUTO, + FAN_ON, SWING_OFF, SWING_VERTICAL, ClimateEntityFeature, @@ -72,6 +74,7 @@ TARGET_HEATER_COOLER_STATE_HOMEKIT_TO_HASS = { TargetHeaterCoolerStateValues.COOL: HVACMode.COOL, } + # Map of hass operation modes to homekit modes MODE_HASS_TO_HOMEKIT = {v: k for k, v in MODE_HOMEKIT_TO_HASS.items()} @@ -104,19 +107,65 @@ async def async_setup_entry( conn.add_listener(async_add_service) -class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity): - """Representation of a Homekit climate device.""" +class HomeKitBaseClimateEntity(HomeKitEntity, ClimateEntity): + """The base HomeKit Controller climate entity.""" + + _attr_temperature_unit = TEMP_CELSIUS def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" return [ + CharacteristicsTypes.TEMPERATURE_CURRENT, + CharacteristicsTypes.FAN_STATE_TARGET, + ] + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) + + @property + def fan_modes(self) -> list[str] | None: + """Return the available fan modes.""" + if self.service.has(CharacteristicsTypes.FAN_STATE_TARGET): + return [FAN_ON, FAN_AUTO] + return None + + @property + def fan_mode(self) -> str | None: + """Return the current fan mode.""" + fan_mode = self.service.value(CharacteristicsTypes.FAN_STATE_TARGET) + return FAN_AUTO if fan_mode else FAN_ON + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Turn fan to manual/auto.""" + await self.async_put_characteristics( + {CharacteristicsTypes.FAN_STATE_TARGET: int(fan_mode == FAN_AUTO)} + ) + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + features = 0 + + if self.service.has(CharacteristicsTypes.FAN_STATE_TARGET): + features |= ClimateEntityFeature.FAN_MODE + + return features + + +class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity): + """Representation of a Homekit climate device.""" + + def get_characteristic_types(self) -> list[str]: + """Define the homekit characteristics the entity cares about.""" + return super().get_characteristic_types() + [ CharacteristicsTypes.ACTIVE, CharacteristicsTypes.CURRENT_HEATER_COOLER_STATE, CharacteristicsTypes.TARGET_HEATER_COOLER_STATE, CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD, CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD, CharacteristicsTypes.SWING_MODE, - CharacteristicsTypes.TEMPERATURE_CURRENT, ] async def async_set_temperature(self, **kwargs: Any) -> None: @@ -162,11 +211,6 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity): } ) - @property - def current_temperature(self) -> float: - """Return the current temperature.""" - return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) - @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" @@ -321,7 +365,7 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity): @property def supported_features(self) -> int: """Return the list of supported features.""" - features = 0 + features = super().supported_features if self.service.has(CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD): features |= ClimateEntityFeature.TARGET_TEMPERATURE @@ -334,22 +378,16 @@ class HomeKitHeaterCoolerEntity(HomeKitEntity, ClimateEntity): return features - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - -class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): +class HomeKitClimateEntity(HomeKitBaseClimateEntity): """Representation of a Homekit climate device.""" def get_characteristic_types(self) -> list[str]: """Define the homekit characteristics the entity cares about.""" - return [ + return super().get_characteristic_types() + [ CharacteristicsTypes.HEATING_COOLING_CURRENT, CharacteristicsTypes.HEATING_COOLING_TARGET, CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD, - CharacteristicsTypes.TEMPERATURE_CURRENT, CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD, CharacteristicsTypes.TEMPERATURE_TARGET, CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT, @@ -411,11 +449,6 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): } ) - @property - def current_temperature(self) -> float | None: - """Return the current temperature.""" - return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) - @property def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" @@ -558,7 +591,7 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): @property def supported_features(self) -> int: """Return the list of supported features.""" - features = 0 + features = super().supported_features if self.service.has(CharacteristicsTypes.TEMPERATURE_TARGET): features |= ClimateEntityFeature.TARGET_TEMPERATURE @@ -573,11 +606,6 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): return features - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - ENTITY_TYPES = { ServicesTypes.HEATER_COOLER: HomeKitHeaterCoolerEntity, diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index 80c2f9870c1..159a1d936fa 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -15,6 +15,10 @@ from homeassistant.components.fan import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) from . import KNOWN_DEVICES, HomeKitEntity @@ -48,13 +52,32 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): """Return true if device is on.""" return self.service.value(self.on_characteristic) == 1 + @property + def _speed_range(self) -> tuple[int, int]: + """Return the speed range.""" + return (self._min_speed, self._max_speed) + + @property + def _min_speed(self) -> int: + """Return the minimum speed.""" + return ( + round(self.service[CharacteristicsTypes.ROTATION_SPEED].minValue or 0) + 1 + ) + + @property + def _max_speed(self) -> int: + """Return the minimum speed.""" + return round(self.service[CharacteristicsTypes.ROTATION_SPEED].maxValue or 100) + @property def percentage(self) -> int: """Return the current speed percentage.""" if not self.is_on: return 0 - return self.service.value(CharacteristicsTypes.ROTATION_SPEED) + return ranged_value_to_percentage( + self._speed_range, self.service.value(CharacteristicsTypes.ROTATION_SPEED) + ) @property def current_direction(self) -> str: @@ -88,7 +111,7 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): def speed_count(self) -> int: """Speed count for the fan.""" return round( - min(self.service[CharacteristicsTypes.ROTATION_SPEED].maxValue or 100, 100) + min(self._max_speed, 100) / max(1, self.service[CharacteristicsTypes.ROTATION_SPEED].minStep or 0) ) @@ -104,7 +127,11 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): return await self.async_turn_off() await self.async_put_characteristics( - {CharacteristicsTypes.ROTATION_SPEED: percentage} + { + CharacteristicsTypes.ROTATION_SPEED: round( + percentage_to_ranged_value(self._speed_range, percentage) + ) + } ) async def async_oscillate(self, oscillating: bool) -> None: @@ -129,7 +156,9 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): percentage is not None and self.supported_features & FanEntityFeature.SET_SPEED ): - characteristics[CharacteristicsTypes.ROTATION_SPEED] = percentage + characteristics[CharacteristicsTypes.ROTATION_SPEED] = round( + percentage_to_ranged_value(self._speed_range, percentage) + ) if characteristics: await self.async_put_characteristics(characteristics) diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index 07d22c27314..7a6d0a01ab6 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -67,8 +67,6 @@ async def async_setup_entry( if description := NUMBER_ENTITIES.get(char.type): entities.append(HomeKitNumber(conn, info, char, description)) - elif entity_type := NUMBER_ENTITY_CLASSES.get(char.type): - entities.append(entity_type(conn, info, char)) else: return False @@ -130,72 +128,3 @@ class HomeKitNumber(CharacteristicEntity, NumberEntity): self._char.type: value, } ) - - -class HomeKitEcobeeFanModeNumber(CharacteristicEntity, NumberEntity): - """Representation of a Number control for Ecobee Fan Mode request.""" - - def get_characteristic_types(self) -> list[str]: - """Define the homekit characteristics the entity is tracking.""" - return [self._char.type] - - @property - def name(self) -> str: - """Return the name of the device if any.""" - prefix = "" - if name := super().name: - prefix = name - return f"{prefix} Fan Mode" - - @property - def native_min_value(self) -> float: - """Return the minimum value.""" - return self._char.minValue or DEFAULT_MIN_VALUE - - @property - def native_max_value(self) -> float: - """Return the maximum value.""" - return self._char.maxValue or DEFAULT_MAX_VALUE - - @property - def native_step(self) -> float: - """Return the increment/decrement step.""" - return self._char.minStep or DEFAULT_STEP - - @property - def native_value(self) -> float: - """Return the current characteristic value.""" - return self._char.value - - async def async_set_native_value(self, value: float) -> None: - """Set the characteristic to this value.""" - - # Sending the fan mode request sometimes ends up getting ignored by ecobee - # and this might be because it the older value instead of newer, and ecobee - # thinks there is nothing to do. - # So in order to make sure that the request is executed by ecobee, we need - # to send a different value before sending the target value. - # Fan mode value is a value from 0 to 100. We send a value off by 1 first. - - if value > self.min_value: - other_value = value - 1 - else: - other_value = self.min_value + 1 - - if value != other_value: - await self.async_put_characteristics( - { - self._char.type: other_value, - } - ) - - await self.async_put_characteristics( - { - self._char.type: value, - } - ) - - -NUMBER_ENTITY_CLASSES: dict[str, type] = { - CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: HomeKitEcobeeFanModeNumber, -} diff --git a/tests/components/homekit_controller/fixtures/haa_fan.json b/tests/components/homekit_controller/fixtures/haa_fan.json index 14a215d01fe..a144a9501ba 100644 --- a/tests/components/homekit_controller/fixtures/haa_fan.json +++ b/tests/components/homekit_controller/fixtures/haa_fan.json @@ -70,7 +70,7 @@ "perms": ["pr", "pw", "ev"], "ev": true, "format": "bool", - "value": false + "value": true }, { "aid": 1, @@ -83,7 +83,7 @@ "minValue": 0, "maxValue": 3, "minStep": 1, - "value": 3 + "value": 2 } ] }, diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee_501.py b/tests/components/homekit_controller/specific_devices/test_ecobee_501.py index 89443008683..ca91607bd09 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee_501.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee_501.py @@ -2,6 +2,7 @@ from homeassistant.components.climate.const import ( + SUPPORT_FAN_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, @@ -43,9 +44,11 @@ async def test_ecobee501_setup(hass): SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_TARGET_HUMIDITY + | SUPPORT_FAN_MODE ), capabilities={ "hvac_modes": ["off", "heat", "cool", "heat_cool"], + "fan_modes": ["on", "auto"], "min_temp": 7.2, "max_temp": 33.3, "min_humidity": 20, diff --git a/tests/components/homekit_controller/specific_devices/test_haa_fan.py b/tests/components/homekit_controller/specific_devices/test_haa_fan.py index 9d5983650d7..39169ea5af9 100644 --- a/tests/components/homekit_controller/specific_devices/test_haa_fan.py +++ b/tests/components/homekit_controller/specific_devices/test_haa_fan.py @@ -1,6 +1,6 @@ """Make sure that a H.A.A. fan can be setup.""" -from homeassistant.components.fan import SUPPORT_SET_SPEED +from homeassistant.components.fan import ATTR_PERCENTAGE, SUPPORT_SET_SPEED from homeassistant.helpers.entity import EntityCategory from tests.components.homekit_controller.common import ( @@ -18,7 +18,9 @@ async def test_haa_fan_setup(hass): accessories = await setup_accessories_from_file(hass, "haa_fan.json") await setup_test_accessories(hass, accessories) - # FIXME: assert round(state.attributes["percentage_step"], 2) == 33.33 + haa_fan_state = hass.states.get("fan.haa_c718b3") + attributes = haa_fan_state.attributes + assert attributes[ATTR_PERCENTAGE] == 66 await assert_devices_and_entities_created( hass, @@ -55,7 +57,7 @@ async def test_haa_fan_setup(hass): entity_id="fan.haa_c718b3", friendly_name="HAA-C718B3", unique_id="homekit-C718B3-1-8", - state="off", + state="on", supported_features=SUPPORT_SET_SPEED, capabilities={ "preset_modes": None, diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 646804a86d6..c750c428437 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -10,6 +10,7 @@ from aiohomekit.model.services import ServicesTypes from homeassistant.components.climate.const import ( DOMAIN, + SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, SERVICE_SET_SWING_MODE, @@ -32,6 +33,9 @@ def create_thermostat_service(accessory): char = service.add_char(CharacteristicsTypes.HEATING_COOLING_CURRENT) char.value = 0 + char = service.add_char(CharacteristicsTypes.FAN_STATE_TARGET) + char.value = 0 + char = service.add_char(CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD) char.minValue = 15 char.maxValue = 40 @@ -144,6 +148,32 @@ async def test_climate_change_thermostat_state(hass, utcnow): }, ) + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {"entity_id": "climate.testdevice", "fan_mode": "on"}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.FAN_STATE_TARGET: 0, + }, + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_FAN_MODE, + {"entity_id": "climate.testdevice", "fan_mode": "auto"}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.THERMOSTAT, + { + CharacteristicsTypes.FAN_STATE_TARGET: 1, + }, + ) + async def test_climate_check_min_max_values_per_mode(hass, utcnow): """Test that we we get the appropriate min/max values for each mode.""" diff --git a/tests/components/homekit_controller/test_fan.py b/tests/components/homekit_controller/test_fan.py index faaaa2e666f..9d166531562 100644 --- a/tests/components/homekit_controller/test_fan.py +++ b/tests/components/homekit_controller/test_fan.py @@ -1,4 +1,4 @@ -"""Basic checks for HomeKit motion sensors and contact sensors.""" +"""Basic checks for HomeKit fans.""" from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes @@ -41,6 +41,20 @@ def create_fanv2_service(accessory): swing_mode.value = 0 +def create_fanv2_service_non_standard_rotation_range(accessory): + """Define fan v2 with a non-standard rotation range.""" + service = accessory.add_service(ServicesTypes.FAN_V2) + + cur_state = service.add_char(CharacteristicsTypes.ACTIVE) + cur_state.value = 0 + + speed = service.add_char(CharacteristicsTypes.ROTATION_SPEED) + speed.value = 0 + speed.minValue = 0 + speed.maxValue = 3 + speed.minStep = 1 + + def create_fanv2_service_with_min_step(accessory): """Define fan v2 characteristics as per HAP spec.""" service = accessory.add_service(ServicesTypes.FAN_V2) @@ -730,3 +744,64 @@ async def test_v2_oscillate_read(hass, utcnow): ServicesTypes.FAN_V2, {CharacteristicsTypes.SWING_MODE: 1} ) assert state.attributes["oscillating"] is True + + +async def test_v2_set_percentage_non_standard_rotation_range(hass, utcnow): + """Test that we set fan speed with a non-standard rotation range.""" + helper = await setup_test_component( + hass, create_fanv2_service_non_standard_rotation_range + ) + + await helper.async_update(ServicesTypes.FAN_V2, {CharacteristicsTypes.ACTIVE: 1}) + + await hass.services.async_call( + "fan", + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 100}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ROTATION_SPEED: 3, + }, + ) + + await hass.services.async_call( + "fan", + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 66}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ROTATION_SPEED: 2, + }, + ) + + await hass.services.async_call( + "fan", + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 33}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ROTATION_SPEED: 1, + }, + ) + + await hass.services.async_call( + "fan", + "set_percentage", + {"entity_id": "fan.testdevice", "percentage": 0}, + blocking=True, + ) + helper.async_assert_service_values( + ServicesTypes.FAN_V2, + { + CharacteristicsTypes.ACTIVE: 0, + }, + ) diff --git a/tests/components/homekit_controller/test_number.py b/tests/components/homekit_controller/test_number.py index 78bdb394f0c..6b375b60d9b 100644 --- a/tests/components/homekit_controller/test_number.py +++ b/tests/components/homekit_controller/test_number.py @@ -26,26 +26,6 @@ def create_switch_with_spray_level(accessory): return service -def create_switch_with_ecobee_fan_mode(accessory): - """Define battery level characteristics.""" - service = accessory.add_service(ServicesTypes.OUTLET) - - ecobee_fan_mode = service.add_char( - CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED - ) - - ecobee_fan_mode.value = 0 - ecobee_fan_mode.minStep = 1 - ecobee_fan_mode.minValue = 0 - ecobee_fan_mode.maxValue = 100 - ecobee_fan_mode.format = "float" - - cur_state = service.add_char(CharacteristicsTypes.ON) - cur_state.value = True - - return service - - async def test_read_number(hass, utcnow): """Test a switch service that has a sensor characteristic is correctly handled.""" helper = await setup_test_component(hass, create_switch_with_spray_level) @@ -106,72 +86,3 @@ async def test_write_number(hass, utcnow): ServicesTypes.OUTLET, {CharacteristicsTypes.VENDOR_VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: 3}, ) - - -async def test_write_ecobee_fan_mode_number(hass, utcnow): - """Test a switch service that has a sensor characteristic is correctly handled.""" - helper = await setup_test_component(hass, create_switch_with_ecobee_fan_mode) - - # Helper will be for the primary entity, which is the outlet. Make a helper for the sensor. - fan_mode = Helper( - hass, - "number.testdevice_fan_mode", - helper.pairing, - helper.accessory, - helper.config_entry, - ) - - await hass.services.async_call( - "number", - "set_value", - {"entity_id": "number.testdevice_fan_mode", "value": 1}, - blocking=True, - ) - fan_mode.async_assert_service_values( - ServicesTypes.OUTLET, - {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 1}, - ) - - await hass.services.async_call( - "number", - "set_value", - {"entity_id": "number.testdevice_fan_mode", "value": 2}, - blocking=True, - ) - fan_mode.async_assert_service_values( - ServicesTypes.OUTLET, - {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 2}, - ) - - await hass.services.async_call( - "number", - "set_value", - {"entity_id": "number.testdevice_fan_mode", "value": 99}, - blocking=True, - ) - fan_mode.async_assert_service_values( - ServicesTypes.OUTLET, - {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 99}, - ) - - await hass.services.async_call( - "number", - "set_value", - {"entity_id": "number.testdevice_fan_mode", "value": 100}, - blocking=True, - ) - fan_mode.async_assert_service_values( - ServicesTypes.OUTLET, - {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 100}, - ) - - await hass.services.async_call( - "number", - "set_value", - {"entity_id": "number.testdevice_fan_mode", "value": 0}, - blocking=True, - ) - fan_mode.async_assert_service_values( - ServicesTypes.OUTLET, - {CharacteristicsTypes.VENDOR_ECOBEE_FAN_WRITE_SPEED: 0}, - ) From 3aafa0cf498849593f31ed18d2ff09952e600e2b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 14:06:32 +0200 Subject: [PATCH 2151/3516] Migrate aemet to native_* (#74037) --- homeassistant/components/aemet/__init__.py | 39 ++++++++- homeassistant/components/aemet/const.py | 26 ++++-- homeassistant/components/aemet/sensor.py | 16 ++-- homeassistant/components/aemet/weather.py | 20 +++-- .../aemet/weather_update_coordinator.py | 20 ++--- tests/components/aemet/test_init.py | 84 +++++++++++++++++++ 6 files changed, 168 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index a914a23a0da..7b86a5559e0 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -1,18 +1,30 @@ """The AEMET OpenData component.""" +from __future__ import annotations + import logging +from typing import Any from aemet_opendata.interface import AEMET from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + Platform, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er from .const import ( CONF_STATION_UPDATES, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, + FORECAST_MODES, PLATFORMS, + RENAMED_FORECAST_SENSOR_KEYS, ) from .weather_update_coordinator import WeatherUpdateCoordinator @@ -21,6 +33,8 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AEMET OpenData as config entry.""" + await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry) + name = entry.data[CONF_NAME] api_key = entry.data[CONF_API_KEY] latitude = entry.data[CONF_LATITUDE] @@ -60,3 +74,24 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +@callback +def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None: + """Migrate AEMET entity entries. + + - Migrates unique ID from old forecast sensors to the new unique ID + """ + if entry.domain != Platform.SENSOR: + return None + for old_key, new_key in RENAMED_FORECAST_SENSOR_KEYS.items(): + for forecast_mode in FORECAST_MODES: + old_suffix = f"-forecast-{forecast_mode}-{old_key}" + if entry.unique_id.endswith(old_suffix): + new_suffix = f"-forecast-{forecast_mode}-{new_key}" + return { + "new_unique_id": entry.unique_id.replace(old_suffix, new_suffix) + } + + # No migration needed + return None diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index 4be90011f5a..48e7335934f 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -18,6 +18,10 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, @@ -159,13 +163,13 @@ CONDITIONS_MAP = { FORECAST_MONITORED_CONDITIONS = [ ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, + ATTR_FORECAST_NATIVE_WIND_SPEED, ] MONITORED_CONDITIONS = [ ATTR_API_CONDITION, @@ -206,7 +210,7 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION, + key=ATTR_FORECAST_NATIVE_PRECIPITATION, name="Precipitation", native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, ), @@ -216,13 +220,13 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP, + key=ATTR_FORECAST_NATIVE_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP_LOW, + key=ATTR_FORECAST_NATIVE_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, @@ -238,11 +242,17 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=DEGREE, ), SensorEntityDescription( - key=ATTR_FORECAST_WIND_SPEED, + key=ATTR_FORECAST_NATIVE_WIND_SPEED, name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, ), ) +RENAMED_FORECAST_SENSOR_KEYS = { + ATTR_FORECAST_PRECIPITATION: ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_TEMP_LOW: ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, +} WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=ATTR_API_CONDITION, diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index f98e3fff49e..8439b166a47 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -45,17 +45,13 @@ async def async_setup_entry( entities.extend( [ AemetForecastSensor( - name_prefix, - unique_id_prefix, + f"{domain_data[ENTRY_NAME]} {mode} Forecast", + f"{unique_id}-forecast-{mode}", weather_coordinator, mode, description, ) for mode in FORECAST_MODES - if ( - (name_prefix := f"{domain_data[ENTRY_NAME]} {mode} Forecast") - and (unique_id_prefix := f"{unique_id}-forecast-{mode}") - ) for description in FORECAST_SENSOR_TYPES if description.key in FORECAST_MONITORED_CONDITIONS ] @@ -89,14 +85,14 @@ class AemetSensor(AbstractAemetSensor): def __init__( self, name, - unique_id, + unique_id_prefix, weather_coordinator: WeatherUpdateCoordinator, description: SensorEntityDescription, ): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id}-{description.key}", + unique_id=f"{unique_id_prefix}-{description.key}", coordinator=weather_coordinator, description=description, ) @@ -113,7 +109,7 @@ class AemetForecastSensor(AbstractAemetSensor): def __init__( self, name, - unique_id, + unique_id_prefix, weather_coordinator: WeatherUpdateCoordinator, forecast_mode, description: SensorEntityDescription, @@ -121,7 +117,7 @@ class AemetForecastSensor(AbstractAemetSensor): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id}-{description.key}", + unique_id=f"{unique_id_prefix}-{description.key}", coordinator=weather_coordinator, description=description, ) diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index d05442b621e..a67726d1f51 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -1,7 +1,12 @@ """Support for the AEMET OpenData service.""" from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS +from homeassistant.const import ( + LENGTH_MILLIMETERS, + PRESSURE_HPA, + SPEED_KILOMETERS_PER_HOUR, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -47,9 +52,10 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): """Implementation of an AEMET OpenData sensor.""" _attr_attribution = ATTRIBUTION - _attr_temperature_unit = TEMP_CELSIUS - _attr_pressure_unit = PRESSURE_HPA - _attr_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR def __init__( self, @@ -83,12 +89,12 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_HUMIDITY] @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" return self.coordinator.data[ATTR_API_PRESSURE] @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data[ATTR_API_TEMPERATURE] @@ -98,6 +104,6 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_WIND_BEARING] @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" return self.coordinator.data[ATTR_API_WIND_SPEED] diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index c86465ea8f1..4f0bf6ac5ea 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -44,13 +44,13 @@ import async_timeout from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, ) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -406,10 +406,10 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day( day ), - ATTR_FORECAST_TEMP: self._get_temperature_day(day), - ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), + ATTR_FORECAST_NATIVE_TEMP: self._get_temperature_day(day), + ATTR_FORECAST_NATIVE_TEMP_LOW: self._get_temperature_low_day(day), ATTR_FORECAST_TIME: dt_util.as_utc(date).isoformat(), - ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), + ATTR_FORECAST_NATIVE_WIND_SPEED: self._get_wind_speed_day(day), ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), } @@ -421,13 +421,13 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return { ATTR_FORECAST_CONDITION: condition, - ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour), + ATTR_FORECAST_NATIVE_PRECIPITATION: self._calc_precipitation(day, hour), ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob( day, hour ), - ATTR_FORECAST_TEMP: self._get_temperature(day, hour), + ATTR_FORECAST_NATIVE_TEMP: self._get_temperature(day, hour), ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), - ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), + ATTR_FORECAST_NATIVE_WIND_SPEED: self._get_wind_speed(day, hour), ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), } diff --git a/tests/components/aemet/test_init.py b/tests/components/aemet/test_init.py index b1f452c1b46..8dd177a145d 100644 --- a/tests/components/aemet/test_init.py +++ b/tests/components/aemet/test_init.py @@ -2,11 +2,15 @@ from unittest.mock import patch +import pytest import requests_mock from homeassistant.components.aemet.const import DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util from .util import aemet_requests_mock @@ -42,3 +46,83 @@ async def test_unload_entry(hass): await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + "old_unique_id,new_unique_id", + [ + # Sensors which should be migrated + ( + "aemet_unique_id-forecast-daily-precipitation", + "aemet_unique_id-forecast-daily-native_precipitation", + ), + ( + "aemet_unique_id-forecast-daily-temperature", + "aemet_unique_id-forecast-daily-native_temperature", + ), + ( + "aemet_unique_id-forecast-daily-templow", + "aemet_unique_id-forecast-daily-native_templow", + ), + ( + "aemet_unique_id-forecast-daily-wind_speed", + "aemet_unique_id-forecast-daily-native_wind_speed", + ), + ( + "aemet_unique_id-forecast-hourly-precipitation", + "aemet_unique_id-forecast-hourly-native_precipitation", + ), + ( + "aemet_unique_id-forecast-hourly-temperature", + "aemet_unique_id-forecast-hourly-native_temperature", + ), + ( + "aemet_unique_id-forecast-hourly-templow", + "aemet_unique_id-forecast-hourly-native_templow", + ), + ( + "aemet_unique_id-forecast-hourly-wind_speed", + "aemet_unique_id-forecast-hourly-native_wind_speed", + ), + # Already migrated + ( + "aemet_unique_id-forecast-daily-native_templow", + "aemet_unique_id-forecast-daily-native_templow", + ), + # No migration needed + ( + "aemet_unique_id-forecast-daily-condition", + "aemet_unique_id-forecast-daily-condition", + ), + ], +) +async def test_migrate_unique_id_sensor( + hass: HomeAssistant, + old_unique_id: str, + new_unique_id: str, +) -> None: + """Test migration of unique_id.""" + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + with patch("homeassistant.util.dt.now", return_value=now), patch( + "homeassistant.util.dt.utcnow", return_value=now + ), requests_mock.mock() as _m: + aemet_requests_mock(_m) + config_entry = MockConfigEntry( + domain=DOMAIN, unique_id="aemet_unique_id", data=CONFIG + ) + config_entry.add_to_hass(hass) + + entity_registry = er.async_get(hass) + entity: er.RegistryEntry = entity_registry.async_get_or_create( + domain=SENSOR_DOMAIN, + platform=DOMAIN, + unique_id=old_unique_id, + config_entry=config_entry, + ) + assert entity.unique_id == old_unique_id + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entity_migrated = entity_registry.async_get(entity.entity_id) + assert entity_migrated + assert entity_migrated.unique_id == new_unique_id From a90654bd630b016705557dd33093d9bdce2156e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Br=C3=BCckmann?= Date: Tue, 5 Jul 2022 12:25:20 +0200 Subject: [PATCH 2152/3516] Fix unreachable DenonAVR reporting as available when polling fails (#74344) --- homeassistant/components/denonavr/media_player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 8d3102c441b..85e28c29d7c 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -204,12 +204,14 @@ class DenonDevice(MediaPlayerEntity): ) self._available = False except AvrCommandError as err: + available = False _LOGGER.error( "Command %s failed with error: %s", func.__name__, err, ) except DenonAvrError as err: + available = False _LOGGER.error( "Error %s occurred in method %s for Denon AVR receiver", err, From ce04480e60107ffbcb77141ea67f5aea2df1406a Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Mon, 4 Jul 2022 14:24:21 +0200 Subject: [PATCH 2153/3516] Support unload for multiple adguard entries (#74360) --- homeassistant/components/adguard/__init__.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 1f2645e227c..2a244a5fe80 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -115,14 +115,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload AdGuard Home config entry.""" - hass.services.async_remove(DOMAIN, SERVICE_ADD_URL) - hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL) - hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL) - hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL) - hass.services.async_remove(DOMAIN, SERVICE_REFRESH) - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + if not hass.data[DOMAIN]: + hass.services.async_remove(DOMAIN, SERVICE_ADD_URL) + hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL) + hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL) + hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL) + hass.services.async_remove(DOMAIN, SERVICE_REFRESH) del hass.data[DOMAIN] return unload_ok From bb844840965b8a866f324595f29486590db10003 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 3 Jul 2022 22:03:13 -0700 Subject: [PATCH 2154/3516] Guard invalid data sensor significant change (#74369) --- .../components/sensor/significant_change.py | 18 ++++++++++++++---- .../sensor/test_significant_change.py | 2 ++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/significant_change.py b/homeassistant/components/sensor/significant_change.py index 31b4f00c37f..6ff23b43508 100644 --- a/homeassistant/components/sensor/significant_change.py +++ b/homeassistant/components/sensor/significant_change.py @@ -63,13 +63,23 @@ def async_check_significant_change( absolute_change = 1.0 percentage_change = 2.0 + try: + # New state is invalid, don't report it + new_state_f = float(new_state) + except ValueError: + return False + + try: + # Old state was invalid, we should report again + old_state_f = float(old_state) + except ValueError: + return True + if absolute_change is not None and percentage_change is not None: return _absolute_and_relative_change( - float(old_state), float(new_state), absolute_change, percentage_change + old_state_f, new_state_f, absolute_change, percentage_change ) if absolute_change is not None: - return check_absolute_change( - float(old_state), float(new_state), absolute_change - ) + return check_absolute_change(old_state_f, new_state_f, absolute_change) return None diff --git a/tests/components/sensor/test_significant_change.py b/tests/components/sensor/test_significant_change.py index 051a92f3b07..bfa01d6eb08 100644 --- a/tests/components/sensor/test_significant_change.py +++ b/tests/components/sensor/test_significant_change.py @@ -52,6 +52,8 @@ TEMP_FREEDOM_ATTRS = { ("12.1", "12.2", TEMP_CELSIUS_ATTRS, False), ("70", "71", TEMP_FREEDOM_ATTRS, True), ("70", "70.5", TEMP_FREEDOM_ATTRS, False), + ("fail", "70", TEMP_FREEDOM_ATTRS, True), + ("70", "fail", TEMP_FREEDOM_ATTRS, False), ], ) async def test_significant_change_temperature(old_state, new_state, attrs, result): From f0993ca4a85a8cc27995d9cd6e17bd5bf3f91e85 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 10:47:59 +0200 Subject: [PATCH 2155/3516] Migrate knx weather to native_* (#74386) --- homeassistant/components/knx/weather.py | 39 ++++++++++++------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/knx/weather.py b/homeassistant/components/knx/weather.py index 6e71c09501f..32f37ad2ac2 100644 --- a/homeassistant/components/knx/weather.py +++ b/homeassistant/components/knx/weather.py @@ -6,7 +6,14 @@ from xknx.devices import Weather as XknxWeather from homeassistant import config_entries from homeassistant.components.weather import WeatherEntity -from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, TEMP_CELSIUS, Platform +from homeassistant.const import ( + CONF_ENTITY_CATEGORY, + CONF_NAME, + PRESSURE_PA, + SPEED_METERS_PER_SECOND, + TEMP_CELSIUS, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType @@ -68,7 +75,9 @@ class KNXWeather(KnxEntity, WeatherEntity): """Representation of a KNX weather device.""" _device: XknxWeather - _attr_temperature_unit = TEMP_CELSIUS + _attr_native_pressure_unit = PRESSURE_PA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND def __init__(self, xknx: XKNX, config: ConfigType) -> None: """Initialize of a KNX sensor.""" @@ -77,19 +86,14 @@ class KNXWeather(KnxEntity, WeatherEntity): self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) @property - def temperature(self) -> float | None: - """Return current temperature.""" + def native_temperature(self) -> float | None: + """Return current temperature in C.""" return self._device.temperature @property - def pressure(self) -> float | None: - """Return current air pressure.""" - # KNX returns pA - HA requires hPa - return ( - self._device.air_pressure / 100 - if self._device.air_pressure is not None - else None - ) + def native_pressure(self) -> float | None: + """Return current air pressure in Pa.""" + return self._device.air_pressure @property def condition(self) -> str: @@ -107,11 +111,6 @@ class KNXWeather(KnxEntity, WeatherEntity): return self._device.wind_bearing @property - def wind_speed(self) -> float | None: - """Return current wind speed in km/h.""" - # KNX only supports wind speed in m/s - return ( - self._device.wind_speed * 3.6 - if self._device.wind_speed is not None - else None - ) + def native_wind_speed(self) -> float | None: + """Return current wind speed in m/s.""" + return self._device.wind_speed From d0a86b3cd20e89cf83d44472d10df0e0e4752060 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 15:18:57 +0200 Subject: [PATCH 2156/3516] Migrate ipma weather to native_* (#74387) --- homeassistant/components/ipma/weather.py | 33 ++++++++++++------------ 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index 731c3d7fb60..dd585b88802 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -25,12 +25,12 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, PLATFORM_SCHEMA, WeatherEntity, ) @@ -40,6 +40,8 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_MODE, CONF_NAME, + PRESSURE_HPA, + SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback @@ -174,6 +176,10 @@ async def async_get_location(hass, api, latitude, longitude): class IPMAWeather(WeatherEntity): """Representation of a weather condition.""" + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__(self, location: Location, api: IPMA_API, config): """Initialise the platform with a data instance and station name.""" self._api = api @@ -237,7 +243,7 @@ class IPMAWeather(WeatherEntity): ) @property - def temperature(self): + def native_temperature(self): """Return the current temperature.""" if not self._observation: return None @@ -245,7 +251,7 @@ class IPMAWeather(WeatherEntity): return self._observation.temperature @property - def pressure(self): + def native_pressure(self): """Return the current pressure.""" if not self._observation: return None @@ -261,7 +267,7 @@ class IPMAWeather(WeatherEntity): return self._observation.humidity @property - def wind_speed(self): + def native_wind_speed(self): """Return the current windspeed.""" if not self._observation: return None @@ -276,11 +282,6 @@ class IPMAWeather(WeatherEntity): return self._observation.wind_direction - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def forecast(self): """Return the forecast array.""" @@ -307,13 +308,13 @@ class IPMAWeather(WeatherEntity): ), None, ), - ATTR_FORECAST_TEMP: float(data_in.feels_like_temperature), + ATTR_FORECAST_NATIVE_TEMP: float(data_in.feels_like_temperature), ATTR_FORECAST_PRECIPITATION_PROBABILITY: ( int(float(data_in.precipitation_probability)) if int(float(data_in.precipitation_probability)) >= 0 else None ), - ATTR_FORECAST_WIND_SPEED: data_in.wind_strength, + ATTR_FORECAST_NATIVE_WIND_SPEED: data_in.wind_strength, ATTR_FORECAST_WIND_BEARING: data_in.wind_direction, } for data_in in forecast_filtered @@ -331,10 +332,10 @@ class IPMAWeather(WeatherEntity): ), None, ), - ATTR_FORECAST_TEMP_LOW: data_in.min_temperature, - ATTR_FORECAST_TEMP: data_in.max_temperature, + ATTR_FORECAST_NATIVE_TEMP_LOW: data_in.min_temperature, + ATTR_FORECAST_NATIVE_TEMP: data_in.max_temperature, ATTR_FORECAST_PRECIPITATION_PROBABILITY: data_in.precipitation_probability, - ATTR_FORECAST_WIND_SPEED: data_in.wind_strength, + ATTR_FORECAST_NATIVE_WIND_SPEED: data_in.wind_strength, ATTR_FORECAST_WIND_BEARING: data_in.wind_direction, } for data_in in forecast_filtered From 6ba06c0f5343c2421c998d3d99657a339a3928f6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 14:20:47 +0200 Subject: [PATCH 2157/3516] Migrate met_eireann weather to native_* (#74391) Co-authored-by: avee87 <6134677+avee87@users.noreply.github.com> Co-authored-by: Franck Nijhof --- homeassistant/components/met_eireann/const.py | 20 +++---- .../components/met_eireann/weather.py | 59 +++++-------------- 2 files changed, 22 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/met_eireann/const.py b/homeassistant/components/met_eireann/const.py index 98d862183c4..efe80cb9d17 100644 --- a/homeassistant/components/met_eireann/const.py +++ b/homeassistant/components/met_eireann/const.py @@ -1,6 +1,4 @@ """Constants for Met Éireann component.""" -import logging - from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -12,13 +10,13 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY_RAINY, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_PRESSURE, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRESSURE, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, DOMAIN as WEATHER_DOMAIN, ) @@ -32,17 +30,15 @@ HOME_LOCATION_NAME = "Home" ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.met_eireann_{HOME_LOCATION_NAME}" -_LOGGER = logging.getLogger(".") - FORECAST_MAP = { ATTR_FORECAST_CONDITION: "condition", - ATTR_FORECAST_PRESSURE: "pressure", + ATTR_FORECAST_NATIVE_PRESSURE: "pressure", ATTR_FORECAST_PRECIPITATION: "precipitation", - ATTR_FORECAST_TEMP: "temperature", - ATTR_FORECAST_TEMP_LOW: "templow", + ATTR_FORECAST_NATIVE_TEMP: "temperature", + ATTR_FORECAST_NATIVE_TEMP_LOW: "templow", ATTR_FORECAST_TIME: "datetime", ATTR_FORECAST_WIND_BEARING: "wind_bearing", - ATTR_FORECAST_WIND_SPEED: "wind_speed", + ATTR_FORECAST_NATIVE_WIND_SPEED: "wind_speed", } CONDITION_MAP = { diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index cbf5c99342a..f20f0e1254a 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -3,8 +3,6 @@ import logging from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, WeatherEntity, ) @@ -13,12 +11,9 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - LENGTH_INCHES, LENGTH_MILLIMETERS, PRESSURE_HPA, - PRESSURE_INHG, SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant @@ -27,9 +22,6 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util -from homeassistant.util.distance import convert as convert_distance -from homeassistant.util.pressure import convert as convert_pressure -from homeassistant.util.speed import convert as convert_speed from .const import ATTRIBUTION, CONDITION_MAP, DEFAULT_NAME, DOMAIN, FORECAST_MAP @@ -54,12 +46,8 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN][config_entry.entry_id] async_add_entities( [ - MetEireannWeather( - coordinator, config_entry.data, hass.config.units.is_metric, False - ), - MetEireannWeather( - coordinator, config_entry.data, hass.config.units.is_metric, True - ), + MetEireannWeather(coordinator, config_entry.data, False), + MetEireannWeather(coordinator, config_entry.data, True), ] ) @@ -67,11 +55,15 @@ async def async_setup_entry( class MetEireannWeather(CoordinatorEntity, WeatherEntity): """Implementation of a Met Éireann weather condition.""" - def __init__(self, coordinator, config, is_metric, hourly): + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + + def __init__(self, coordinator, config, hourly): """Initialise the platform with a data instance and site.""" super().__init__(coordinator) self._config = config - self._is_metric = is_metric self._hourly = hourly @property @@ -109,23 +101,14 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): ) @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data.current_weather_data.get("temperature") @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" - pressure_hpa = self.coordinator.data.current_weather_data.get("pressure") - if self._is_metric or pressure_hpa is None: - return pressure_hpa - - return round(convert_pressure(pressure_hpa, PRESSURE_HPA, PRESSURE_INHG), 2) + return self.coordinator.data.current_weather_data.get("pressure") @property def humidity(self): @@ -133,16 +116,9 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): return self.coordinator.data.current_weather_data.get("humidity") @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" - speed_m_s = self.coordinator.data.current_weather_data.get("wind_speed") - if self._is_metric or speed_m_s is None: - return speed_m_s - - speed_mi_h = convert_speed( - speed_m_s, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR - ) - return int(round(speed_mi_h)) + return self.coordinator.data.current_weather_data.get("wind_speed") @property def wind_bearing(self): @@ -161,7 +137,7 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): me_forecast = self.coordinator.data.hourly_forecast else: me_forecast = self.coordinator.data.daily_forecast - required_keys = {ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME} + required_keys = {"temperature", "datetime"} ha_forecast = [] @@ -171,13 +147,6 @@ class MetEireannWeather(CoordinatorEntity, WeatherEntity): ha_item = { k: item[v] for k, v in FORECAST_MAP.items() if item.get(v) is not None } - if not self._is_metric and ATTR_FORECAST_PRECIPITATION in ha_item: - precip_inches = convert_distance( - ha_item[ATTR_FORECAST_PRECIPITATION], - LENGTH_MILLIMETERS, - LENGTH_INCHES, - ) - ha_item[ATTR_FORECAST_PRECIPITATION] = round(precip_inches, 2) if ha_item.get(ATTR_FORECAST_CONDITION): ha_item[ATTR_FORECAST_CONDITION] = format_condition( ha_item[ATTR_FORECAST_CONDITION] From 25639ccf25b81099a3230133cc4498e65d1e2560 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 13:38:53 +0200 Subject: [PATCH 2158/3516] Migrate meteoclimatic weather to native_* (#74392) --- .../components/meteoclimatic/weather.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/meteoclimatic/weather.py b/homeassistant/components/meteoclimatic/weather.py index 4faecdaa3ac..8044dd04aa8 100644 --- a/homeassistant/components/meteoclimatic/weather.py +++ b/homeassistant/components/meteoclimatic/weather.py @@ -3,7 +3,7 @@ from meteoclimatic import Condition from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -38,6 +38,10 @@ async def async_setup_entry( class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity): """Representation of a weather condition.""" + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__(self, coordinator: DataUpdateCoordinator) -> None: """Initialise the weather platform.""" super().__init__(coordinator) @@ -71,27 +75,22 @@ class MeteoclimaticWeather(CoordinatorEntity, WeatherEntity): return format_condition(self.coordinator.data["weather"].condition) @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data["weather"].temp_current - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def humidity(self): """Return the humidity.""" return self.coordinator.data["weather"].humidity_current @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" return self.coordinator.data["weather"].pressure_current @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" return self.coordinator.data["weather"].wind_current From db83c784786c4553f61002c25fc8eca50aca52b2 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 4 Jul 2022 14:14:27 +0300 Subject: [PATCH 2159/3516] Bump aioimaplib to 1.0.0 (#74393) --- homeassistant/components/imap/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index 655590005bf..f4bbadfa6ac 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -2,7 +2,7 @@ "domain": "imap", "name": "IMAP", "documentation": "https://www.home-assistant.io/integrations/imap", - "requirements": ["aioimaplib==0.9.0"], + "requirements": ["aioimaplib==1.0.0"], "codeowners": [], "iot_class": "cloud_push", "loggers": ["aioimaplib"] diff --git a/requirements_all.txt b/requirements_all.txt index 8812e1b8cfe..8956b7ba238 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -178,7 +178,7 @@ aiohttp_cors==0.7.0 aiohue==4.4.2 # homeassistant.components.imap -aioimaplib==0.9.0 +aioimaplib==1.0.0 # homeassistant.components.apache_kafka aiokafka==0.6.0 From 3ff0218326105c16ed634091882013672326ca24 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 15:12:45 +0200 Subject: [PATCH 2160/3516] Migrate accuweather weather to native_* (#74407) --- .../components/accuweather/weather.py | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index 536f66a3cb9..ae1824aef4a 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -6,19 +6,26 @@ from typing import Any, cast from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, Forecast, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_NAME, + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_MILES, + LENGTH_MILLIMETERS, + PRESSURE_HPA, + PRESSURE_INHG, + SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR, TEMP_CELSIUS, TEMP_FAHRENHEIT, @@ -66,19 +73,25 @@ class AccuWeatherEntity( ) -> None: """Initialize.""" super().__init__(coordinator) - self._unit_system = API_METRIC if coordinator.is_metric else API_IMPERIAL - wind_speed_unit = self.coordinator.data["Wind"]["Speed"][self._unit_system][ - "Unit" - ] - if wind_speed_unit == "mi/h": - self._attr_wind_speed_unit = SPEED_MILES_PER_HOUR + # Coordinator data is used also for sensors which don't have units automatically + # converted, hence the weather entity's native units follow the configured unit + # system + if coordinator.is_metric: + self._attr_native_precipitation_unit = LENGTH_MILLIMETERS + self._attr_native_pressure_unit = PRESSURE_HPA + self._attr_native_temperature_unit = TEMP_CELSIUS + self._attr_native_visibility_unit = LENGTH_KILOMETERS + self._attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + self._unit_system = API_METRIC else: - self._attr_wind_speed_unit = wind_speed_unit + self._unit_system = API_IMPERIAL + self._attr_native_precipitation_unit = LENGTH_INCHES + self._attr_native_pressure_unit = PRESSURE_INHG + self._attr_native_temperature_unit = TEMP_FAHRENHEIT + self._attr_native_visibility_unit = LENGTH_MILES + self._attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR self._attr_name = name self._attr_unique_id = coordinator.location_key - self._attr_temperature_unit = ( - TEMP_CELSIUS if coordinator.is_metric else TEMP_FAHRENHEIT - ) self._attr_attribution = ATTRIBUTION self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, @@ -106,14 +119,14 @@ class AccuWeatherEntity( return None @property - def temperature(self) -> float: + def native_temperature(self) -> float: """Return the temperature.""" return cast( float, self.coordinator.data["Temperature"][self._unit_system]["Value"] ) @property - def pressure(self) -> float: + def native_pressure(self) -> float: """Return the pressure.""" return cast( float, self.coordinator.data["Pressure"][self._unit_system]["Value"] @@ -125,7 +138,7 @@ class AccuWeatherEntity( return cast(int, self.coordinator.data["RelativeHumidity"]) @property - def wind_speed(self) -> float: + def native_wind_speed(self) -> float: """Return the wind speed.""" return cast( float, self.coordinator.data["Wind"]["Speed"][self._unit_system]["Value"] @@ -137,7 +150,7 @@ class AccuWeatherEntity( return cast(int, self.coordinator.data["Wind"]["Direction"]["Degrees"]) @property - def visibility(self) -> float: + def native_visibility(self) -> float: """Return the visibility.""" return cast( float, self.coordinator.data["Visibility"][self._unit_system]["Value"] @@ -162,9 +175,9 @@ class AccuWeatherEntity( return [ { ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(), - ATTR_FORECAST_TEMP: item["TemperatureMax"]["Value"], - ATTR_FORECAST_TEMP_LOW: item["TemperatureMin"]["Value"], - ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(item), + ATTR_FORECAST_NATIVE_TEMP: item["TemperatureMax"]["Value"], + ATTR_FORECAST_NATIVE_TEMP_LOW: item["TemperatureMin"]["Value"], + ATTR_FORECAST_NATIVE_PRECIPITATION: self._calc_precipitation(item), ATTR_FORECAST_PRECIPITATION_PROBABILITY: round( mean( [ @@ -173,7 +186,7 @@ class AccuWeatherEntity( ] ) ), - ATTR_FORECAST_WIND_SPEED: item["WindDay"]["Speed"]["Value"], + ATTR_FORECAST_NATIVE_WIND_SPEED: item["WindDay"]["Speed"]["Value"], ATTR_FORECAST_WIND_BEARING: item["WindDay"]["Direction"]["Degrees"], ATTR_FORECAST_CONDITION: [ k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v From edb5c7d2c88cb20e1fdd43f6fbbb1e77055f30f3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 4 Jul 2022 16:59:36 +0200 Subject: [PATCH 2161/3516] Correct climacell weather migration to native_* (#74409) --- homeassistant/components/climacell/weather.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index 2b284114981..6aee9b54f6c 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -10,13 +10,13 @@ from pyclimacell.const import CURRENT, DAILY, FORECASTS, HOURLY, NOWCAST from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry @@ -135,12 +135,12 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): data = { ATTR_FORECAST_TIME: forecast_dt.isoformat(), ATTR_FORECAST_CONDITION: translated_condition, - ATTR_FORECAST_PRECIPITATION: precipitation, + ATTR_FORECAST_NATIVE_PRECIPITATION: precipitation, ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability, - ATTR_FORECAST_TEMP: temp, - ATTR_FORECAST_TEMP_LOW: temp_low, + ATTR_FORECAST_NATIVE_TEMP: temp, + ATTR_FORECAST_NATIVE_TEMP_LOW: temp_low, ATTR_FORECAST_WIND_BEARING: wind_direction, - ATTR_FORECAST_WIND_SPEED: wind_speed, + ATTR_FORECAST_NATIVE_WIND_SPEED: wind_speed, } return {k: v for k, v in data.items() if v is not None} @@ -224,7 +224,7 @@ class ClimaCellV3WeatherEntity(BaseClimaCellWeatherEntity): return CONDITIONS_V3[condition] @property - def temperature(self): + def native_temperature(self): """Return the platform temperature.""" return self._get_cc_value( self.coordinator.data[CURRENT], CC_V3_ATTR_TEMPERATURE From 82b9eae88218bb98756bc85acdb188ffeef9d885 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Jul 2022 10:36:56 -0500 Subject: [PATCH 2162/3516] Bump rflink to 0.0.63 (#74417) --- homeassistant/components/rflink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json index debc12ae4e0..6cef409a736 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -2,7 +2,7 @@ "domain": "rflink", "name": "RFLink", "documentation": "https://www.home-assistant.io/integrations/rflink", - "requirements": ["rflink==0.0.62"], + "requirements": ["rflink==0.0.63"], "codeowners": ["@javicalle"], "iot_class": "assumed_state", "loggers": ["rflink"] diff --git a/requirements_all.txt b/requirements_all.txt index 8956b7ba238..bcf326c4817 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2077,7 +2077,7 @@ restrictedpython==5.2 rfk101py==0.0.1 # homeassistant.components.rflink -rflink==0.0.62 +rflink==0.0.63 # homeassistant.components.ring ring_doorbell==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd2f423a7cc..3ab1015f872 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1385,7 +1385,7 @@ renault-api==0.1.11 restrictedpython==5.2 # homeassistant.components.rflink -rflink==0.0.62 +rflink==0.0.63 # homeassistant.components.ring ring_doorbell==0.7.2 From 54516ee9392f1ba5e74f027f08d5d332f2bbf82e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Jul 2022 13:53:25 -0500 Subject: [PATCH 2163/3516] Bump pyunifiprotect to 4.0.9 (#74424) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index da82871d313..9aeb8b48050 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.8", "unifi-discovery==1.1.4"], + "requirements": ["pyunifiprotect==4.0.9", "unifi-discovery==1.1.4"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index bcf326c4817..0fbbfa8a154 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1993,7 +1993,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.8 +pyunifiprotect==4.0.9 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3ab1015f872..72c7ccb569c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1331,7 +1331,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.8 +pyunifiprotect==4.0.9 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 18b3ffbf9932473a8a58625fd28a1dae5aa5b17b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 4 Jul 2022 21:10:26 +0200 Subject: [PATCH 2164/3516] Remove lutron_caseta from mypy ignore list (#74427) --- homeassistant/components/lutron_caseta/__init__.py | 13 +++++++------ homeassistant/components/lutron_caseta/cover.py | 4 ++-- .../components/lutron_caseta/device_trigger.py | 6 ++++-- homeassistant/components/lutron_caseta/models.py | 4 +--- mypy.ini | 9 --------- script/hassfest/mypy_config.py | 3 --- 6 files changed, 14 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index b4ce82a36c6..c6ad8781478 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -215,10 +215,10 @@ def _async_register_button_devices( config_entry_id: str, bridge_device, button_devices_by_id: dict[int, dict], -) -> dict[str, dr.DeviceEntry]: +) -> dict[str, dict]: """Register button devices (Pico Remotes) in the device registry.""" device_registry = dr.async_get(hass) - button_devices_by_dr_id = {} + button_devices_by_dr_id: dict[str, dict] = {} seen = set() for device in button_devices_by_id.values(): @@ -226,7 +226,7 @@ def _async_register_button_devices( continue seen.add(device["serial"]) area, name = _area_and_name_from_name(device["name"]) - device_args = { + device_args: dict[str, Any] = { "name": f"{area} {name}", "manufacturer": MANUFACTURER, "config_entry_id": config_entry_id, @@ -246,7 +246,8 @@ def _async_register_button_devices( def _area_and_name_from_name(device_name: str) -> tuple[str, str]: """Return the area and name from the devices internal name.""" if "_" in device_name: - return device_name.split("_", 1) + area_device_name = device_name.split("_", 1) + return area_device_name[0], area_device_name[1] return UNASSIGNED_AREA, device_name @@ -382,13 +383,13 @@ class LutronCasetaDevice(Entity): class LutronCasetaDeviceUpdatableEntity(LutronCasetaDevice): """A lutron_caseta entity that can update by syncing data from the bridge.""" - async def async_update(self): + async def async_update(self) -> None: """Update when forcing a refresh of the device.""" self._device = self._smartbridge.get_device_by_id(self.device_id) _LOGGER.debug(self._device) -def _id_to_identifier(lutron_id: str) -> None: +def _id_to_identifier(lutron_id: str) -> tuple[str, str]: """Convert a lutron caseta identifier to a device identifier.""" return (DOMAIN, lutron_id) diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py index b74642a8589..d63c1191d57 100644 --- a/homeassistant/components/lutron_caseta/cover.py +++ b/homeassistant/components/lutron_caseta/cover.py @@ -66,13 +66,13 @@ class LutronCasetaCover(LutronCasetaDeviceUpdatableEntity, CoverEntity): async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" await self._smartbridge.lower_cover(self.device_id) - self.async_update() + await self.async_update() self.async_write_ha_state() async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self._smartbridge.raise_cover(self.device_id) - self.async_update() + await self.async_update() self.async_write_ha_state() async def async_set_cover_position(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index e762e79a8d7..ed809e0994a 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -386,10 +386,12 @@ async def async_attach_trigger( """Attach a trigger.""" device_registry = dr.async_get(hass) device = device_registry.async_get(config[CONF_DEVICE_ID]) + assert device + assert device.model 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_TO_LIP.get(device_type) + schema = DEVICE_TYPE_SCHEMA_MAP[device_type] + valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LIP[device_type] config = schema(config) event_config = { event_trigger.CONF_PLATFORM: CONF_EVENT, diff --git a/homeassistant/components/lutron_caseta/models.py b/homeassistant/components/lutron_caseta/models.py index 5845c888a2e..362760b0caf 100644 --- a/homeassistant/components/lutron_caseta/models.py +++ b/homeassistant/components/lutron_caseta/models.py @@ -6,8 +6,6 @@ from typing import Any from pylutron_caseta.smartbridge import Smartbridge -from homeassistant.helpers.device_registry import DeviceEntry - @dataclass class LutronCasetaData: @@ -15,4 +13,4 @@ class LutronCasetaData: bridge: Smartbridge bridge_device: dict[str, Any] - button_devices: dict[str, DeviceEntry] + button_devices: dict[str, dict] diff --git a/mypy.ini b/mypy.ini index fb67983a31c..2b657041865 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2756,15 +2756,6 @@ ignore_errors = true [mypy-homeassistant.components.lovelace.websocket] ignore_errors = true -[mypy-homeassistant.components.lutron_caseta] -ignore_errors = true - -[mypy-homeassistant.components.lutron_caseta.device_trigger] -ignore_errors = true - -[mypy-homeassistant.components.lutron_caseta.switch] -ignore_errors = true - [mypy-homeassistant.components.lyric.climate] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index bbb628a76bb..b4df9e00495 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -64,9 +64,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.lovelace.dashboard", "homeassistant.components.lovelace.resources", "homeassistant.components.lovelace.websocket", - "homeassistant.components.lutron_caseta", - "homeassistant.components.lutron_caseta.device_trigger", - "homeassistant.components.lutron_caseta.switch", "homeassistant.components.lyric.climate", "homeassistant.components.lyric.config_flow", "homeassistant.components.lyric.sensor", From c933a49c719b8952ed3e3e9a5c01779015c6477b Mon Sep 17 00:00:00 2001 From: Arne Mauer Date: Tue, 5 Jul 2022 10:35:05 +0200 Subject: [PATCH 2165/3516] Fix multi_match to match with the IKEA airpurifier channel (#74432) Fix multi_match for FilterLifeTime, device_run_time, filter_run_time sensors for ikea starkvind --- homeassistant/components/zha/number.py | 6 +----- homeassistant/components/zha/sensor.py | 16 ++-------------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index c3d7f352318..e1268e29190 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -526,11 +526,7 @@ class TimerDurationMinutes(ZHANumberConfigurationEntity, id_suffix="timer_durati @CONFIG_DIAGNOSTIC_MATCH( - channel_names="ikea_manufacturer", - manufacturers={ - "IKEA of Sweden", - }, - models={"STARKVIND Air purifier"}, + channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"} ) class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time"): """Representation of a ZHA timer duration configuration entity.""" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 2fe38193ecb..4a4700b3c4c 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -810,13 +810,7 @@ class TimeLeft(Sensor, id_suffix="time_left"): _unit = TIME_MINUTES -@MULTI_MATCH( - channel_names="ikea_manufacturer", - manufacturers={ - "IKEA of Sweden", - }, - models={"STARKVIND Air purifier"}, -) +@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"): """Sensor that displays device run time (in minutes).""" @@ -826,13 +820,7 @@ class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"): _unit = TIME_MINUTES -@MULTI_MATCH( - channel_names="ikea_manufacturer", - manufacturers={ - "IKEA of Sweden", - }, - models={"STARKVIND Air purifier"}, -) +@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) class IkeaFilterRunTime(Sensor, id_suffix="filter_run_time"): """Sensor that displays run time of the current filter (in minutes).""" From c79b741971cf8633d4e6b6e58f5d36f773db312f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 5 Jul 2022 13:41:33 +0200 Subject: [PATCH 2166/3516] Re-introduce default scan interval in Scrape sensor (#74455) --- homeassistant/components/scrape/sensor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index e15f7c5ba97..88c9b564b29 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -1,6 +1,7 @@ """Support for getting data from websites with scraping.""" from __future__ import annotations +from datetime import timedelta import logging from typing import Any @@ -39,6 +40,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) +SCAN_INTERVAL = timedelta(minutes=10) + CONF_ATTR = "attribute" CONF_SELECT = "select" CONF_INDEX = "index" From 98d5c415b3d5aeff696b5f193e8744a8c1718448 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Jul 2022 08:50:00 -0700 Subject: [PATCH 2167/3516] Bumped version to 2022.7.0b4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c07650c8ac1..5a8d761e83b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 57707cf8af1..27cd670e809 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.0b3" +version = "2022.7.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From b590e51f887d6e552296b35dc1131162638473bb Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 5 Jul 2022 18:54:03 +0200 Subject: [PATCH 2168/3516] Bump deCONZ dependency to v96 (#74460) --- homeassistant/components/deconz/light.py | 6 +----- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 669800e2662..7f3a47d719d 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -113,11 +113,7 @@ async def async_setup_entry( first = True for light_id in group.lights: - if ( - (light := gateway.api.lights.lights.get(light_id)) - and light.ZHATYPE == Light.ZHATYPE - and light.reachable - ): + if (light := gateway.api.lights.lights.get(light_id)) and light.reachable: group.update_color_state(light, update_all_attributes=first) first = False diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 09dcc190a4f..c19d75ec054 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==95"], + "requirements": ["pydeconz==96"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 3c6a0ca8d63..52ce487b265 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1447,7 +1447,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==95 +pydeconz==96 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18f38d1d230..0198c535090 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -980,7 +980,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==95 +pydeconz==96 # homeassistant.components.dexcom pydexcom==0.2.3 From e7b2d4672c8424679cd06d4d31a65b44b6a7d85a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 12:27:27 -0500 Subject: [PATCH 2169/3516] Avoid loading mqtt for type checking (#74464) --- homeassistant/helpers/config_entry_flow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 1190e947eba..3617c0b1f29 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -6,7 +6,7 @@ import logging from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, cast from homeassistant import config_entries -from homeassistant.components import dhcp, mqtt, onboarding, ssdp, zeroconf +from homeassistant.components import dhcp, onboarding, ssdp, zeroconf from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult @@ -15,6 +15,9 @@ from .typing import UNDEFINED, DiscoveryInfoType, UndefinedType if TYPE_CHECKING: import asyncio + from homeassistant.components import mqtt + + _R = TypeVar("_R", bound="Awaitable[bool] | bool") DiscoveryFunctionType = Callable[[HomeAssistant], _R] From 87d7c024bf3a8c3d8de2248a263526979074fff1 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 5 Jul 2022 12:43:10 -0500 Subject: [PATCH 2170/3516] Bump Frontend to 20220705.0 (#74467) --- 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 27ff0a73f20..6b378fe1098 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220630.0"], + "requirements": ["home-assistant-frontend==20220705.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a62ae0b6fd1..e05a9e7e800 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220630.0 +home-assistant-frontend==20220705.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 52ce487b265..e5e6a2a073f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220630.0 +home-assistant-frontend==20220705.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0198c535090..9735a85fec3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -598,7 +598,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220630.0 +home-assistant-frontend==20220705.0 # homeassistant.components.home_connect homeconnect==0.7.1 From e4fd5100c4e7fa9766283a6a1d92ee08016239cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 12:43:38 -0500 Subject: [PATCH 2171/3516] Bump aiohomekit to 0.7.19 (#74463) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 3b3c5e51cf8..a15c576c313 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.7.18"], + "requirements": ["aiohomekit==0.7.19"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index e5e6a2a073f..e688588c8d7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.18 +aiohomekit==0.7.19 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9735a85fec3..06f4e581de0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.18 +aiohomekit==0.7.19 # homeassistant.components.emulated_hue # homeassistant.components.http From 89ab78371f3a2eebf6c9b52afcd371f5984e639e Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 5 Jul 2022 20:18:14 +0200 Subject: [PATCH 2172/3516] Bump Sensibo dependency (#74466) --- homeassistant/components/sensibo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 18e93d5efa6..5ef8ff6fa4e 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,7 +2,7 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": ["pysensibo==1.0.17"], + "requirements": ["pysensibo==1.0.18"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index e688588c8d7..82a7b2aa99c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1795,7 +1795,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.17 +pysensibo==1.0.18 # homeassistant.components.serial # homeassistant.components.zha diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 06f4e581de0..beda687d9f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1223,7 +1223,7 @@ pyruckus==0.12 pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.17 +pysensibo==1.0.18 # homeassistant.components.serial # homeassistant.components.zha From cbe9eda0a8201c988d95a73da0f12a1b57bcf868 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 5 Jul 2022 20:24:18 +0200 Subject: [PATCH 2173/3516] Remove lyric from mypy ignore list (#74451) --- homeassistant/components/lyric/climate.py | 5 +++-- homeassistant/components/lyric/config_flow.py | 8 ++++++-- homeassistant/components/lyric/sensor.py | 8 +++++--- mypy.ini | 9 --------- script/hassfest/mypy_config.py | 3 --- 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/lyric/climate.py b/homeassistant/components/lyric/climate.py index b5c1fb05efa..8353ae15b3c 100644 --- a/homeassistant/components/lyric/climate.py +++ b/homeassistant/components/lyric/climate.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import logging from time import localtime, strftime, time +from typing import Any from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation @@ -186,7 +187,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): return self.device.indoorTemperature @property - def hvac_action(self) -> HVACAction: + def hvac_action(self) -> HVACAction | None: """Return the current hvac action.""" action = HVAC_ACTIONS.get(self.device.operationStatus.mode, None) if action == HVACAction.OFF and self.hvac_mode != HVACMode.OFF: @@ -265,7 +266,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity): return device.maxHeatSetpoint return device.maxCoolSetpoint - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if self.hvac_mode == HVACMode.OFF: return diff --git a/homeassistant/components/lyric/config_flow.py b/homeassistant/components/lyric/config_flow.py index 12f91cfe206..de6808dd0de 100644 --- a/homeassistant/components/lyric/config_flow.py +++ b/homeassistant/components/lyric/config_flow.py @@ -1,4 +1,6 @@ """Config flow for Honeywell Lyric.""" +from __future__ import annotations + from collections.abc import Mapping import logging from typing import Any @@ -25,13 +27,15 @@ class OAuth2FlowHandler( """Perform reauth upon an API authentication error.""" return await self.async_step_reauth_confirm() - async def async_step_reauth_confirm(self, user_input=None): + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | 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") return await self.async_step_user() - async def async_oauth_create_entry(self, data: dict) -> dict: + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Create an oauth config entry or update existing entry for reauth.""" existing_entry = await self.async_set_unique_id(DOMAIN) if existing_entry: diff --git a/homeassistant/components/lyric/sensor.py b/homeassistant/components/lyric/sensor.py index 5b60935d667..d727b24eee4 100644 --- a/homeassistant/components/lyric/sensor.py +++ b/homeassistant/components/lyric/sensor.py @@ -47,9 +47,11 @@ class LyricSensorEntityDescription(SensorEntityDescription): value: Callable[[LyricDevice], StateType | datetime] = round -def get_datetime_from_future_time(time: str) -> datetime: +def get_datetime_from_future_time(time_str: str) -> datetime: """Get datetime from future time provided.""" - time = dt_util.parse_time(time) + time = dt_util.parse_time(time_str) + if time is None: + raise ValueError(f"Unable to parse time {time_str}") now = dt_util.utcnow() if time <= now.time(): now = now + timedelta(days=1) @@ -64,7 +66,7 @@ async def async_setup_entry( entities = [] - def get_setpoint_status(status: str, time: str) -> str: + def get_setpoint_status(status: str, time: str) -> str | None: if status == PRESET_HOLD_UNTIL: return f"Held until {time}" return LYRIC_SETPOINT_STATUS_NAMES.get(status, None) diff --git a/mypy.ini b/mypy.ini index 91dc7f9e71f..52c4dce061f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2752,15 +2752,6 @@ ignore_errors = true [mypy-homeassistant.components.lovelace.websocket] ignore_errors = true -[mypy-homeassistant.components.lyric.climate] -ignore_errors = true - -[mypy-homeassistant.components.lyric.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.lyric.sensor] -ignore_errors = true - [mypy-homeassistant.components.meteo_france.sensor] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index d987a76d41d..521c98ea93d 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -59,9 +59,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.lovelace.dashboard", "homeassistant.components.lovelace.resources", "homeassistant.components.lovelace.websocket", - "homeassistant.components.lyric.climate", - "homeassistant.components.lyric.config_flow", - "homeassistant.components.lyric.sensor", "homeassistant.components.meteo_france.sensor", "homeassistant.components.meteo_france.weather", "homeassistant.components.minecraft_server", From a7158fee6784939fccaa10639757137dd136f0ae Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 5 Jul 2022 22:24:08 +0200 Subject: [PATCH 2174/3516] Revert "Migrate aemet to native_*" (#74471) --- homeassistant/components/aemet/__init__.py | 39 +-------- homeassistant/components/aemet/const.py | 26 ++---- homeassistant/components/aemet/sensor.py | 16 ++-- homeassistant/components/aemet/weather.py | 20 ++--- .../aemet/weather_update_coordinator.py | 20 ++--- tests/components/aemet/test_init.py | 84 ------------------- 6 files changed, 37 insertions(+), 168 deletions(-) diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index 7b86a5559e0..a914a23a0da 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -1,30 +1,18 @@ """The AEMET OpenData component.""" -from __future__ import annotations - import logging -from typing import Any from aemet_opendata.interface import AEMET from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_API_KEY, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - Platform, -) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant from .const import ( CONF_STATION_UPDATES, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, - FORECAST_MODES, PLATFORMS, - RENAMED_FORECAST_SENSOR_KEYS, ) from .weather_update_coordinator import WeatherUpdateCoordinator @@ -33,8 +21,6 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AEMET OpenData as config entry.""" - await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry) - name = entry.data[CONF_NAME] api_key = entry.data[CONF_API_KEY] latitude = entry.data[CONF_LATITUDE] @@ -74,24 +60,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok - - -@callback -def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None: - """Migrate AEMET entity entries. - - - Migrates unique ID from old forecast sensors to the new unique ID - """ - if entry.domain != Platform.SENSOR: - return None - for old_key, new_key in RENAMED_FORECAST_SENSOR_KEYS.items(): - for forecast_mode in FORECAST_MODES: - old_suffix = f"-forecast-{forecast_mode}-{old_key}" - if entry.unique_id.endswith(old_suffix): - new_suffix = f"-forecast-{forecast_mode}-{new_key}" - return { - "new_unique_id": entry.unique_id.replace(old_suffix, new_suffix) - } - - # No migration needed - return None diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index 48e7335934f..4be90011f5a 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -18,10 +18,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, - ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, @@ -163,13 +159,13 @@ CONDITIONS_MAP = { FORECAST_MONITORED_CONDITIONS = [ ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_WIND_SPEED, ] MONITORED_CONDITIONS = [ ATTR_API_CONDITION, @@ -210,7 +206,7 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_PRECIPITATION, + key=ATTR_FORECAST_PRECIPITATION, name="Precipitation", native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, ), @@ -220,13 +216,13 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_TEMP, + key=ATTR_FORECAST_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_TEMP_LOW, + key=ATTR_FORECAST_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, @@ -242,17 +238,11 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=DEGREE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_WIND_SPEED, + key=ATTR_FORECAST_WIND_SPEED, name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, ), ) -RENAMED_FORECAST_SENSOR_KEYS = { - ATTR_FORECAST_PRECIPITATION: ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_TEMP_LOW: ATTR_FORECAST_NATIVE_TEMP_LOW, - ATTR_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, -} WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=ATTR_API_CONDITION, diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index 8439b166a47..f98e3fff49e 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -45,13 +45,17 @@ async def async_setup_entry( entities.extend( [ AemetForecastSensor( - f"{domain_data[ENTRY_NAME]} {mode} Forecast", - f"{unique_id}-forecast-{mode}", + name_prefix, + unique_id_prefix, weather_coordinator, mode, description, ) for mode in FORECAST_MODES + if ( + (name_prefix := f"{domain_data[ENTRY_NAME]} {mode} Forecast") + and (unique_id_prefix := f"{unique_id}-forecast-{mode}") + ) for description in FORECAST_SENSOR_TYPES if description.key in FORECAST_MONITORED_CONDITIONS ] @@ -85,14 +89,14 @@ class AemetSensor(AbstractAemetSensor): def __init__( self, name, - unique_id_prefix, + unique_id, weather_coordinator: WeatherUpdateCoordinator, description: SensorEntityDescription, ): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id_prefix}-{description.key}", + unique_id=f"{unique_id}-{description.key}", coordinator=weather_coordinator, description=description, ) @@ -109,7 +113,7 @@ class AemetForecastSensor(AbstractAemetSensor): def __init__( self, name, - unique_id_prefix, + unique_id, weather_coordinator: WeatherUpdateCoordinator, forecast_mode, description: SensorEntityDescription, @@ -117,7 +121,7 @@ class AemetForecastSensor(AbstractAemetSensor): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id_prefix}-{description.key}", + unique_id=f"{unique_id}-{description.key}", coordinator=weather_coordinator, description=description, ) diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index a67726d1f51..d05442b621e 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -1,12 +1,7 @@ """Support for the AEMET OpenData service.""" from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - LENGTH_MILLIMETERS, - PRESSURE_HPA, - SPEED_KILOMETERS_PER_HOUR, - TEMP_CELSIUS, -) +from homeassistant.const import PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -52,10 +47,9 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): """Implementation of an AEMET OpenData sensor.""" _attr_attribution = ATTRIBUTION - _attr_native_precipitation_unit = LENGTH_MILLIMETERS - _attr_native_pressure_unit = PRESSURE_HPA - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_temperature_unit = TEMP_CELSIUS + _attr_pressure_unit = PRESSURE_HPA + _attr_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR def __init__( self, @@ -89,12 +83,12 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_HUMIDITY] @property - def native_pressure(self): + def pressure(self): """Return the pressure.""" return self.coordinator.data[ATTR_API_PRESSURE] @property - def native_temperature(self): + def temperature(self): """Return the temperature.""" return self.coordinator.data[ATTR_API_TEMPERATURE] @@ -104,6 +98,6 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_WIND_BEARING] @property - def native_wind_speed(self): + def wind_speed(self): """Return the wind speed.""" return self.coordinator.data[ATTR_API_WIND_SPEED] diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index 4f0bf6ac5ea..c86465ea8f1 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -44,13 +44,13 @@ import async_timeout from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, - ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, ) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -406,10 +406,10 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day( day ), - ATTR_FORECAST_NATIVE_TEMP: self._get_temperature_day(day), - ATTR_FORECAST_NATIVE_TEMP_LOW: self._get_temperature_low_day(day), + ATTR_FORECAST_TEMP: self._get_temperature_day(day), + ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), ATTR_FORECAST_TIME: dt_util.as_utc(date).isoformat(), - ATTR_FORECAST_NATIVE_WIND_SPEED: self._get_wind_speed_day(day), + ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), } @@ -421,13 +421,13 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return { ATTR_FORECAST_CONDITION: condition, - ATTR_FORECAST_NATIVE_PRECIPITATION: self._calc_precipitation(day, hour), + ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour), ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob( day, hour ), - ATTR_FORECAST_NATIVE_TEMP: self._get_temperature(day, hour), + ATTR_FORECAST_TEMP: self._get_temperature(day, hour), ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), - ATTR_FORECAST_NATIVE_WIND_SPEED: self._get_wind_speed(day, hour), + ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), } diff --git a/tests/components/aemet/test_init.py b/tests/components/aemet/test_init.py index 8dd177a145d..b1f452c1b46 100644 --- a/tests/components/aemet/test_init.py +++ b/tests/components/aemet/test_init.py @@ -2,15 +2,11 @@ from unittest.mock import patch -import pytest import requests_mock from homeassistant.components.aemet.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util from .util import aemet_requests_mock @@ -46,83 +42,3 @@ async def test_unload_entry(hass): await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.NOT_LOADED - - -@pytest.mark.parametrize( - "old_unique_id,new_unique_id", - [ - # Sensors which should be migrated - ( - "aemet_unique_id-forecast-daily-precipitation", - "aemet_unique_id-forecast-daily-native_precipitation", - ), - ( - "aemet_unique_id-forecast-daily-temperature", - "aemet_unique_id-forecast-daily-native_temperature", - ), - ( - "aemet_unique_id-forecast-daily-templow", - "aemet_unique_id-forecast-daily-native_templow", - ), - ( - "aemet_unique_id-forecast-daily-wind_speed", - "aemet_unique_id-forecast-daily-native_wind_speed", - ), - ( - "aemet_unique_id-forecast-hourly-precipitation", - "aemet_unique_id-forecast-hourly-native_precipitation", - ), - ( - "aemet_unique_id-forecast-hourly-temperature", - "aemet_unique_id-forecast-hourly-native_temperature", - ), - ( - "aemet_unique_id-forecast-hourly-templow", - "aemet_unique_id-forecast-hourly-native_templow", - ), - ( - "aemet_unique_id-forecast-hourly-wind_speed", - "aemet_unique_id-forecast-hourly-native_wind_speed", - ), - # Already migrated - ( - "aemet_unique_id-forecast-daily-native_templow", - "aemet_unique_id-forecast-daily-native_templow", - ), - # No migration needed - ( - "aemet_unique_id-forecast-daily-condition", - "aemet_unique_id-forecast-daily-condition", - ), - ], -) -async def test_migrate_unique_id_sensor( - hass: HomeAssistant, - old_unique_id: str, - new_unique_id: str, -) -> None: - """Test migration of unique_id.""" - now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") - with patch("homeassistant.util.dt.now", return_value=now), patch( - "homeassistant.util.dt.utcnow", return_value=now - ), requests_mock.mock() as _m: - aemet_requests_mock(_m) - config_entry = MockConfigEntry( - domain=DOMAIN, unique_id="aemet_unique_id", data=CONFIG - ) - config_entry.add_to_hass(hass) - - entity_registry = er.async_get(hass) - entity: er.RegistryEntry = entity_registry.async_get_or_create( - domain=SENSOR_DOMAIN, - platform=DOMAIN, - unique_id=old_unique_id, - config_entry=config_entry, - ) - assert entity.unique_id == old_unique_id - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - entity_migrated = entity_registry.async_get(entity.entity_id) - assert entity_migrated - assert entity_migrated.unique_id == new_unique_id From 59aba0bc75d92b4ba332a76ce4409cc811ed8003 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 12:43:38 -0500 Subject: [PATCH 2175/3516] Bump aiohomekit to 0.7.19 (#74463) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 3b3c5e51cf8..a15c576c313 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.7.18"], + "requirements": ["aiohomekit==0.7.19"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 0fbbfa8a154..d280f382e74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.18 +aiohomekit==0.7.19 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72c7ccb569c..98e516d4336 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.18 +aiohomekit==0.7.19 # homeassistant.components.emulated_hue # homeassistant.components.http From 43fe351f1bac2a821296986b85bde1d35a63f096 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 12:27:27 -0500 Subject: [PATCH 2176/3516] Avoid loading mqtt for type checking (#74464) --- homeassistant/helpers/config_entry_flow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 1190e947eba..3617c0b1f29 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -6,7 +6,7 @@ import logging from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, cast from homeassistant import config_entries -from homeassistant.components import dhcp, mqtt, onboarding, ssdp, zeroconf +from homeassistant.components import dhcp, onboarding, ssdp, zeroconf from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult @@ -15,6 +15,9 @@ from .typing import UNDEFINED, DiscoveryInfoType, UndefinedType if TYPE_CHECKING: import asyncio + from homeassistant.components import mqtt + + _R = TypeVar("_R", bound="Awaitable[bool] | bool") DiscoveryFunctionType = Callable[[HomeAssistant], _R] From 9cbb684d50664332cff6c41b6605f47f459eec86 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 5 Jul 2022 12:43:10 -0500 Subject: [PATCH 2177/3516] Bump Frontend to 20220705.0 (#74467) --- 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 27ff0a73f20..6b378fe1098 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220630.0"], + "requirements": ["home-assistant-frontend==20220705.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7ee5a9fe8d3..00f5c776712 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220630.0 +home-assistant-frontend==20220705.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index d280f382e74..726c499b88c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220630.0 +home-assistant-frontend==20220705.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 98e516d4336..0a7d822a5d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -595,7 +595,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220630.0 +home-assistant-frontend==20220705.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 89360516d74fdb18f43b212e700f24d74f364ff9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 5 Jul 2022 22:24:08 +0200 Subject: [PATCH 2178/3516] Revert "Migrate aemet to native_*" (#74471) --- homeassistant/components/aemet/__init__.py | 39 +-------- homeassistant/components/aemet/const.py | 26 ++---- homeassistant/components/aemet/sensor.py | 16 ++-- homeassistant/components/aemet/weather.py | 20 ++--- .../aemet/weather_update_coordinator.py | 20 ++--- tests/components/aemet/test_init.py | 84 ------------------- 6 files changed, 37 insertions(+), 168 deletions(-) diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index 7b86a5559e0..a914a23a0da 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -1,30 +1,18 @@ """The AEMET OpenData component.""" -from __future__ import annotations - import logging -from typing import Any from aemet_opendata.interface import AEMET from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_API_KEY, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - Platform, -) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import HomeAssistant from .const import ( CONF_STATION_UPDATES, DOMAIN, ENTRY_NAME, ENTRY_WEATHER_COORDINATOR, - FORECAST_MODES, PLATFORMS, - RENAMED_FORECAST_SENSOR_KEYS, ) from .weather_update_coordinator import WeatherUpdateCoordinator @@ -33,8 +21,6 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AEMET OpenData as config entry.""" - await er.async_migrate_entries(hass, entry.entry_id, async_migrate_entity_entry) - name = entry.data[CONF_NAME] api_key = entry.data[CONF_API_KEY] latitude = entry.data[CONF_LATITUDE] @@ -74,24 +60,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok - - -@callback -def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None: - """Migrate AEMET entity entries. - - - Migrates unique ID from old forecast sensors to the new unique ID - """ - if entry.domain != Platform.SENSOR: - return None - for old_key, new_key in RENAMED_FORECAST_SENSOR_KEYS.items(): - for forecast_mode in FORECAST_MODES: - old_suffix = f"-forecast-{forecast_mode}-{old_key}" - if entry.unique_id.endswith(old_suffix): - new_suffix = f"-forecast-{forecast_mode}-{new_key}" - return { - "new_unique_id": entry.unique_id.replace(old_suffix, new_suffix) - } - - # No migration needed - return None diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index 48e7335934f..4be90011f5a 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -18,10 +18,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SNOWY, ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, - ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TEMP, @@ -163,13 +159,13 @@ CONDITIONS_MAP = { FORECAST_MONITORED_CONDITIONS = [ ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_WIND_SPEED, ] MONITORED_CONDITIONS = [ ATTR_API_CONDITION, @@ -210,7 +206,7 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_PRECIPITATION, + key=ATTR_FORECAST_PRECIPITATION, name="Precipitation", native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, ), @@ -220,13 +216,13 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_TEMP, + key=ATTR_FORECAST_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_TEMP_LOW, + key=ATTR_FORECAST_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, @@ -242,17 +238,11 @@ FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( native_unit_of_measurement=DEGREE, ), SensorEntityDescription( - key=ATTR_FORECAST_NATIVE_WIND_SPEED, + key=ATTR_FORECAST_WIND_SPEED, name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, ), ) -RENAMED_FORECAST_SENSOR_KEYS = { - ATTR_FORECAST_PRECIPITATION: ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_TEMP_LOW: ATTR_FORECAST_NATIVE_TEMP_LOW, - ATTR_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, -} WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=ATTR_API_CONDITION, diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index 8439b166a47..f98e3fff49e 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -45,13 +45,17 @@ async def async_setup_entry( entities.extend( [ AemetForecastSensor( - f"{domain_data[ENTRY_NAME]} {mode} Forecast", - f"{unique_id}-forecast-{mode}", + name_prefix, + unique_id_prefix, weather_coordinator, mode, description, ) for mode in FORECAST_MODES + if ( + (name_prefix := f"{domain_data[ENTRY_NAME]} {mode} Forecast") + and (unique_id_prefix := f"{unique_id}-forecast-{mode}") + ) for description in FORECAST_SENSOR_TYPES if description.key in FORECAST_MONITORED_CONDITIONS ] @@ -85,14 +89,14 @@ class AemetSensor(AbstractAemetSensor): def __init__( self, name, - unique_id_prefix, + unique_id, weather_coordinator: WeatherUpdateCoordinator, description: SensorEntityDescription, ): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id_prefix}-{description.key}", + unique_id=f"{unique_id}-{description.key}", coordinator=weather_coordinator, description=description, ) @@ -109,7 +113,7 @@ class AemetForecastSensor(AbstractAemetSensor): def __init__( self, name, - unique_id_prefix, + unique_id, weather_coordinator: WeatherUpdateCoordinator, forecast_mode, description: SensorEntityDescription, @@ -117,7 +121,7 @@ class AemetForecastSensor(AbstractAemetSensor): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id_prefix}-{description.key}", + unique_id=f"{unique_id}-{description.key}", coordinator=weather_coordinator, description=description, ) diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index a67726d1f51..d05442b621e 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -1,12 +1,7 @@ """Support for the AEMET OpenData service.""" from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - LENGTH_MILLIMETERS, - PRESSURE_HPA, - SPEED_KILOMETERS_PER_HOUR, - TEMP_CELSIUS, -) +from homeassistant.const import PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -52,10 +47,9 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): """Implementation of an AEMET OpenData sensor.""" _attr_attribution = ATTRIBUTION - _attr_native_precipitation_unit = LENGTH_MILLIMETERS - _attr_native_pressure_unit = PRESSURE_HPA - _attr_native_temperature_unit = TEMP_CELSIUS - _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_temperature_unit = TEMP_CELSIUS + _attr_pressure_unit = PRESSURE_HPA + _attr_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR def __init__( self, @@ -89,12 +83,12 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_HUMIDITY] @property - def native_pressure(self): + def pressure(self): """Return the pressure.""" return self.coordinator.data[ATTR_API_PRESSURE] @property - def native_temperature(self): + def temperature(self): """Return the temperature.""" return self.coordinator.data[ATTR_API_TEMPERATURE] @@ -104,6 +98,6 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_WIND_BEARING] @property - def native_wind_speed(self): + def wind_speed(self): """Return the wind speed.""" return self.coordinator.data[ATTR_API_WIND_SPEED] diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index 4f0bf6ac5ea..c86465ea8f1 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -44,13 +44,13 @@ import async_timeout from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, - ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + ATTR_FORECAST_WIND_SPEED, ) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -406,10 +406,10 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day( day ), - ATTR_FORECAST_NATIVE_TEMP: self._get_temperature_day(day), - ATTR_FORECAST_NATIVE_TEMP_LOW: self._get_temperature_low_day(day), + ATTR_FORECAST_TEMP: self._get_temperature_day(day), + ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), ATTR_FORECAST_TIME: dt_util.as_utc(date).isoformat(), - ATTR_FORECAST_NATIVE_WIND_SPEED: self._get_wind_speed_day(day), + ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), } @@ -421,13 +421,13 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return { ATTR_FORECAST_CONDITION: condition, - ATTR_FORECAST_NATIVE_PRECIPITATION: self._calc_precipitation(day, hour), + ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour), ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob( day, hour ), - ATTR_FORECAST_NATIVE_TEMP: self._get_temperature(day, hour), + ATTR_FORECAST_TEMP: self._get_temperature(day, hour), ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), - ATTR_FORECAST_NATIVE_WIND_SPEED: self._get_wind_speed(day, hour), + ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), } diff --git a/tests/components/aemet/test_init.py b/tests/components/aemet/test_init.py index 8dd177a145d..b1f452c1b46 100644 --- a/tests/components/aemet/test_init.py +++ b/tests/components/aemet/test_init.py @@ -2,15 +2,11 @@ from unittest.mock import patch -import pytest import requests_mock from homeassistant.components.aemet.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util from .util import aemet_requests_mock @@ -46,83 +42,3 @@ async def test_unload_entry(hass): await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.NOT_LOADED - - -@pytest.mark.parametrize( - "old_unique_id,new_unique_id", - [ - # Sensors which should be migrated - ( - "aemet_unique_id-forecast-daily-precipitation", - "aemet_unique_id-forecast-daily-native_precipitation", - ), - ( - "aemet_unique_id-forecast-daily-temperature", - "aemet_unique_id-forecast-daily-native_temperature", - ), - ( - "aemet_unique_id-forecast-daily-templow", - "aemet_unique_id-forecast-daily-native_templow", - ), - ( - "aemet_unique_id-forecast-daily-wind_speed", - "aemet_unique_id-forecast-daily-native_wind_speed", - ), - ( - "aemet_unique_id-forecast-hourly-precipitation", - "aemet_unique_id-forecast-hourly-native_precipitation", - ), - ( - "aemet_unique_id-forecast-hourly-temperature", - "aemet_unique_id-forecast-hourly-native_temperature", - ), - ( - "aemet_unique_id-forecast-hourly-templow", - "aemet_unique_id-forecast-hourly-native_templow", - ), - ( - "aemet_unique_id-forecast-hourly-wind_speed", - "aemet_unique_id-forecast-hourly-native_wind_speed", - ), - # Already migrated - ( - "aemet_unique_id-forecast-daily-native_templow", - "aemet_unique_id-forecast-daily-native_templow", - ), - # No migration needed - ( - "aemet_unique_id-forecast-daily-condition", - "aemet_unique_id-forecast-daily-condition", - ), - ], -) -async def test_migrate_unique_id_sensor( - hass: HomeAssistant, - old_unique_id: str, - new_unique_id: str, -) -> None: - """Test migration of unique_id.""" - now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") - with patch("homeassistant.util.dt.now", return_value=now), patch( - "homeassistant.util.dt.utcnow", return_value=now - ), requests_mock.mock() as _m: - aemet_requests_mock(_m) - config_entry = MockConfigEntry( - domain=DOMAIN, unique_id="aemet_unique_id", data=CONFIG - ) - config_entry.add_to_hass(hass) - - entity_registry = er.async_get(hass) - entity: er.RegistryEntry = entity_registry.async_get_or_create( - domain=SENSOR_DOMAIN, - platform=DOMAIN, - unique_id=old_unique_id, - config_entry=config_entry, - ) - assert entity.unique_id == old_unique_id - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - entity_migrated = entity_registry.async_get(entity.entity_id) - assert entity_migrated - assert entity_migrated.unique_id == new_unique_id From 56e90dd30b3c4d9883cf6b951839abc9ddd9b184 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 5 Jul 2022 13:56:38 -0700 Subject: [PATCH 2179/3516] Bumped version to 2022.7.0b5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5a8d761e83b..69ebbacb0e2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 27cd670e809..61ff9ba39aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.0b4" +version = "2022.7.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2e81be77216315c31c38334d912beca55a7197ad Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 6 Jul 2022 00:27:50 +0000 Subject: [PATCH 2180/3516] [ci skip] Translation update --- .../components/generic/translations/it.json | 2 ++ .../components/nextdns/translations/it.json | 14 ++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 homeassistant/components/nextdns/translations/it.json diff --git a/homeassistant/components/generic/translations/it.json b/homeassistant/components/generic/translations/it.json index 1cd63544700..80f8c90ce16 100644 --- a/homeassistant/components/generic/translations/it.json +++ b/homeassistant/components/generic/translations/it.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Esiste gi\u00e0 una telecamera con queste impostazioni URL.", "invalid_still_image": "L'URL non ha restituito un'immagine fissa valida", + "malformed_url": "URL malformato", "no_still_image_or_stream_url": "Devi specificare almeno un'immagine fissa o un URL di un flusso", + "relative_url": "Non sono consentiti URL relativi", "stream_file_not_found": "File non trovato durante il tentativo di connessione al (\u00e8 installato ffmpeg?)", "stream_http_not_found": "HTTP 404 Non trovato durante il tentativo di connessione al flusso", "stream_io_error": "Errore di input/output durante il tentativo di connessione al flusso. Protocollo di trasporto RTSP errato?", diff --git a/homeassistant/components/nextdns/translations/it.json b/homeassistant/components/nextdns/translations/it.json new file mode 100644 index 00000000000..67c9520f2d4 --- /dev/null +++ b/homeassistant/components/nextdns/translations/it.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Questo profilo NextDNS \u00e8 gi\u00e0 configurato." + }, + "step": { + "profiles": { + "data": { + "profile": "Profilo" + } + } + } + } +} \ No newline at end of file From f5e3344bfc4616b2b4bf4a71b3e725d876f59875 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 6 Jul 2022 05:10:04 +0200 Subject: [PATCH 2181/3516] Add NextDNS system health (#74368) Add system_health --- homeassistant/components/nextdns/strings.json | 5 +++ .../components/nextdns/system_health.py | 25 ++++++++++++ .../components/nextdns/test_system_health.py | 40 +++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 homeassistant/components/nextdns/system_health.py create mode 100644 tests/components/nextdns/test_system_health.py diff --git a/homeassistant/components/nextdns/strings.json b/homeassistant/components/nextdns/strings.json index db3cf88cf39..59319881c02 100644 --- a/homeassistant/components/nextdns/strings.json +++ b/homeassistant/components/nextdns/strings.json @@ -20,5 +20,10 @@ "abort": { "already_configured": "This NextDNS profile is already configured." } + }, + "system_health": { + "info": { + "can_reach_server": "Reach server" + } } } diff --git a/homeassistant/components/nextdns/system_health.py b/homeassistant/components/nextdns/system_health.py new file mode 100644 index 00000000000..0fa31e75f1e --- /dev/null +++ b/homeassistant/components/nextdns/system_health.py @@ -0,0 +1,25 @@ +"""Provide info to system health.""" +from __future__ import annotations + +from typing import Any + +from nextdns.const import API_ENDPOINT + +from homeassistant.components import system_health +from homeassistant.core import HomeAssistant, callback + + +@callback +def async_register( # pylint:disable=unused-argument + hass: HomeAssistant, + register: system_health.SystemHealthRegistration, +) -> None: + """Register system health callbacks.""" + register.async_register_info(system_health_info) + + +async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: + """Get info for the info page.""" + return { + "can_reach_server": system_health.async_check_can_reach_url(hass, API_ENDPOINT) + } diff --git a/tests/components/nextdns/test_system_health.py b/tests/components/nextdns/test_system_health.py new file mode 100644 index 00000000000..4fc3a1a7e72 --- /dev/null +++ b/tests/components/nextdns/test_system_health.py @@ -0,0 +1,40 @@ +"""Test NextDNS system health.""" +import asyncio + +from aiohttp import ClientError +from nextdns.const import API_ENDPOINT + +from homeassistant.components.nextdns.const import DOMAIN +from homeassistant.setup import async_setup_component + +from tests.common import get_system_health_info + + +async def test_nextdns_system_health(hass, aioclient_mock): + """Test NextDNS system health.""" + aioclient_mock.get(API_ENDPOINT, text="") + hass.config.components.add(DOMAIN) + assert await async_setup_component(hass, "system_health", {}) + + info = await get_system_health_info(hass, DOMAIN) + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info == {"can_reach_server": "ok"} + + +async def test_nextdns_system_health_fail(hass, aioclient_mock): + """Test NextDNS system health.""" + aioclient_mock.get(API_ENDPOINT, exc=ClientError) + hass.config.components.add(DOMAIN) + assert await async_setup_component(hass, "system_health", {}) + + info = await get_system_health_info(hass, DOMAIN) + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info == {"can_reach_server": {"type": "failed", "error": "unreachable"}} From a2a4361d6edaeaa69db0ad24076359e26a7e1f7f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 6 Jul 2022 05:12:09 +0200 Subject: [PATCH 2182/3516] Address late comment for NextDNS (#74365) * Simplify code * Use async_config_entry_first_refresh() * Use lambda to get value --- homeassistant/components/nextdns/__init__.py | 7 ++-- homeassistant/components/nextdns/sensor.py | 36 ++++++++++++++++---- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index 71ddeb9e8eb..7df172da8ab 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -153,8 +153,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (ApiError, ClientConnectorError, asyncio.TimeoutError) as err: raise ConfigEntryNotReady from err - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN].setdefault(entry.entry_id, {}) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {} tasks = [] @@ -165,7 +164,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, nextdns, profile_id, update_interval ) tasks.append( - hass.data[DOMAIN][entry.entry_id][coordinator_name].async_refresh() + hass.data[DOMAIN][entry.entry_id][ + coordinator_name + ].async_config_entry_first_refresh() ) await asyncio.gather(*tasks) diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index 35a15f010b3..fc5a8169526 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -1,8 +1,9 @@ """Support for the NextDNS service.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass -from typing import cast +from typing import Any, cast from homeassistant.components.sensor import ( SensorEntity, @@ -14,6 +15,7 @@ from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import NextDnsUpdateCoordinator @@ -34,6 +36,7 @@ class NextDnsSensorRequiredKeysMixin: """Class for NextDNS entity required keys.""" coordinator_type: str + value: Callable[[Any], StateType] @dataclass @@ -52,6 +55,7 @@ SENSORS = ( name="{profile_name} DNS Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.all_queries, ), NextDnsSensorEntityDescription( key="blocked_queries", @@ -61,6 +65,7 @@ SENSORS = ( name="{profile_name} DNS Queries Blocked", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.blocked_queries, ), NextDnsSensorEntityDescription( key="relayed_queries", @@ -70,6 +75,7 @@ SENSORS = ( name="{profile_name} DNS Queries Relayed", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.relayed_queries, ), NextDnsSensorEntityDescription( key="blocked_queries_ratio", @@ -79,6 +85,7 @@ SENSORS = ( name="{profile_name} DNS Queries Blocked Ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.blocked_queries_ratio, ), NextDnsSensorEntityDescription( key="doh_queries", @@ -89,6 +96,7 @@ SENSORS = ( name="{profile_name} DNS-over-HTTPS Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.doh_queries, ), NextDnsSensorEntityDescription( key="dot_queries", @@ -99,6 +107,7 @@ SENSORS = ( name="{profile_name} DNS-over-TLS Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.dot_queries, ), NextDnsSensorEntityDescription( key="doq_queries", @@ -109,6 +118,7 @@ SENSORS = ( name="{profile_name} DNS-over-QUIC Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.doq_queries, ), NextDnsSensorEntityDescription( key="udp_queries", @@ -119,6 +129,7 @@ SENSORS = ( name="{profile_name} UDP Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.udp_queries, ), NextDnsSensorEntityDescription( key="doh_queries_ratio", @@ -129,6 +140,7 @@ SENSORS = ( name="{profile_name} DNS-over-HTTPS Queries Ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.doh_queries_ratio, ), NextDnsSensorEntityDescription( key="dot_queries_ratio", @@ -139,6 +151,7 @@ SENSORS = ( name="{profile_name} DNS-over-TLS Queries Ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.dot_queries_ratio, ), NextDnsSensorEntityDescription( key="doq_queries_ratio", @@ -149,6 +162,7 @@ SENSORS = ( name="{profile_name} DNS-over-QUIC Queries Ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.doq_queries_ratio, ), NextDnsSensorEntityDescription( key="udp_queries_ratio", @@ -159,6 +173,7 @@ SENSORS = ( name="{profile_name} UDP Queries Ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.udp_queries_ratio, ), NextDnsSensorEntityDescription( key="encrypted_queries", @@ -169,6 +184,7 @@ SENSORS = ( name="{profile_name} Encrypted Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.encrypted_queries, ), NextDnsSensorEntityDescription( key="unencrypted_queries", @@ -179,6 +195,7 @@ SENSORS = ( name="{profile_name} Unencrypted Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.unencrypted_queries, ), NextDnsSensorEntityDescription( key="encrypted_queries_ratio", @@ -189,6 +206,7 @@ SENSORS = ( name="{profile_name} Encrypted Queries Ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.encrypted_queries_ratio, ), NextDnsSensorEntityDescription( key="ipv4_queries", @@ -199,6 +217,7 @@ SENSORS = ( name="{profile_name} IPv4 Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.ipv4_queries, ), NextDnsSensorEntityDescription( key="ipv6_queries", @@ -209,6 +228,7 @@ SENSORS = ( name="{profile_name} IPv6 Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.ipv6_queries, ), NextDnsSensorEntityDescription( key="ipv6_queries_ratio", @@ -219,6 +239,7 @@ SENSORS = ( name="{profile_name} IPv6 Queries Ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.ipv6_queries_ratio, ), NextDnsSensorEntityDescription( key="validated_queries", @@ -229,6 +250,7 @@ SENSORS = ( name="{profile_name} DNSSEC Validated Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.validated_queries, ), NextDnsSensorEntityDescription( key="not_validated_queries", @@ -239,6 +261,7 @@ SENSORS = ( name="{profile_name} DNSSEC Not Validated Queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.not_validated_queries, ), NextDnsSensorEntityDescription( key="validated_queries_ratio", @@ -249,6 +272,7 @@ SENSORS = ( name="{profile_name} DNSSEC Validated Queries Ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.validated_queries_ratio, ), ) @@ -278,7 +302,7 @@ class NextDnsSensor(CoordinatorEntity, SensorEntity): def __init__( self, coordinator: NextDnsUpdateCoordinator, - description: SensorEntityDescription, + description: NextDnsSensorEntityDescription, ) -> None: """Initialize.""" super().__init__(coordinator) @@ -287,13 +311,11 @@ class NextDnsSensor(CoordinatorEntity, SensorEntity): self._attr_name = cast(str, description.name).format( profile_name=coordinator.profile_name ) - self._attr_native_value = getattr(coordinator.data, description.key) - self.entity_description = description + self._attr_native_value = description.value(coordinator.data) + self.entity_description: NextDnsSensorEntityDescription = description @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - self._attr_native_value = getattr( - self.coordinator.data, self.entity_description.key - ) + self._attr_native_value = self.entity_description.value(self.coordinator.data) self.async_write_ha_state() From a70cb8af782deb5f2b8701567f6a7a47a72db5d3 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 6 Jul 2022 05:14:56 +0200 Subject: [PATCH 2183/3516] Add NextDNS diagnostics platform (#74367) Add diagnostics platform --- .../components/nextdns/diagnostics.py | 45 ++++++++++++++ tests/components/nextdns/test_diagnostics.py | 59 +++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 homeassistant/components/nextdns/diagnostics.py create mode 100644 tests/components/nextdns/test_diagnostics.py diff --git a/homeassistant/components/nextdns/diagnostics.py b/homeassistant/components/nextdns/diagnostics.py new file mode 100644 index 00000000000..4be22684395 --- /dev/null +++ b/homeassistant/components/nextdns/diagnostics.py @@ -0,0 +1,45 @@ +"""Diagnostics support for NextDNS.""" +from __future__ import annotations + +from dataclasses import asdict + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_UNIQUE_ID +from homeassistant.core import HomeAssistant + +from .const import ( + ATTR_DNSSEC, + ATTR_ENCRYPTION, + ATTR_IP_VERSIONS, + ATTR_PROTOCOLS, + ATTR_STATUS, + CONF_PROFILE_ID, + DOMAIN, +) + +TO_REDACT = {CONF_API_KEY, CONF_PROFILE_ID, CONF_UNIQUE_ID} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict: + """Return diagnostics for a config entry.""" + coordinators = hass.data[DOMAIN][config_entry.entry_id] + + dnssec_coordinator = coordinators[ATTR_DNSSEC] + encryption_coordinator = coordinators[ATTR_ENCRYPTION] + ip_versions_coordinator = coordinators[ATTR_IP_VERSIONS] + protocols_coordinator = coordinators[ATTR_PROTOCOLS] + status_coordinator = coordinators[ATTR_STATUS] + + diagnostics_data = { + "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), + "dnssec_coordinator_data": asdict(dnssec_coordinator.data), + "encryption_coordinator_data": asdict(encryption_coordinator.data), + "ip_versions_coordinator_data": asdict(ip_versions_coordinator.data), + "protocols_coordinator_data": asdict(protocols_coordinator.data), + "status_coordinator_data": asdict(status_coordinator.data), + } + + return diagnostics_data diff --git a/tests/components/nextdns/test_diagnostics.py b/tests/components/nextdns/test_diagnostics.py new file mode 100644 index 00000000000..73844b6a2e3 --- /dev/null +++ b/tests/components/nextdns/test_diagnostics.py @@ -0,0 +1,59 @@ +"""Test NextDNS diagnostics.""" +from homeassistant.components.diagnostics import REDACTED + +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.components.nextdns import init_integration + + +async def test_entry_diagnostics(hass, hass_client): + """Test config entry diagnostics.""" + entry = await init_integration(hass) + + result = await get_diagnostics_for_config_entry(hass, hass_client, entry) + + assert result["config_entry"] == { + "entry_id": entry.entry_id, + "version": 1, + "domain": "nextdns", + "title": "Fake Profile", + "data": {"profile_id": REDACTED, "api_key": REDACTED}, + "options": {}, + "pref_disable_new_entities": False, + "pref_disable_polling": False, + "source": "user", + "unique_id": REDACTED, + "disabled_by": None, + } + assert result["dnssec_coordinator_data"] == { + "not_validated_queries": 25, + "validated_queries": 75, + "validated_queries_ratio": 75.0, + } + assert result["encryption_coordinator_data"] == { + "encrypted_queries": 60, + "unencrypted_queries": 40, + "encrypted_queries_ratio": 60.0, + } + assert result["ip_versions_coordinator_data"] == { + "ipv6_queries": 10, + "ipv4_queries": 90, + "ipv6_queries_ratio": 10.0, + } + assert result["protocols_coordinator_data"] == { + "doh_queries": 20, + "doq_queries": 10, + "dot_queries": 30, + "udp_queries": 40, + "doh_queries_ratio": 22.2, + "doq_queries_ratio": 11.1, + "dot_queries_ratio": 33.3, + "udp_queries_ratio": 44.4, + } + assert result["status_coordinator_data"] == { + "all_queries": 100, + "allowed_queries": 30, + "blocked_queries": 20, + "default_queries": 40, + "relayed_queries": 10, + "blocked_queries_ratio": 20.0, + } From 3d63d4fb36d02cdf2186a377695e81f12767af88 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 23:00:40 -0500 Subject: [PATCH 2184/3516] Fix apple tv not coming online if connected before entity created (#74488) --- homeassistant/components/apple_tv/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 45250451f37..5177c6f3486 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -123,6 +123,10 @@ class AppleTVEntity(Entity): self.atv = None self.async_write_ha_state() + if self.manager.atv: + # ATV is already connected + _async_connected(self.manager.atv) + self.async_on_remove( async_dispatcher_connect( self.hass, f"{SIGNAL_CONNECTED}_{self.unique_id}", _async_connected From df6892b90862dea911b9748865e4dbd1342b3fa4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 23:02:18 -0500 Subject: [PATCH 2185/3516] Offer HKC local push control option when there are multiple zeroconf homekit matches (#74441) --- homeassistant/components/zeroconf/__init__.py | 9 +++- tests/components/zeroconf/test_init.py | 41 ++++++++++++++++++- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 29afd5fc236..1bfa44f3894 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -424,11 +424,16 @@ class ZeroconfDiscovery: # Since we prefer local control, if the integration that is being discovered # is cloud AND the homekit device is UNPAIRED we still want to discovery it. # + # Additionally if the integration is polling, HKC offers a local push + # experience for the user to control the device so we want to offer that + # as well. + # # As soon as the device becomes paired, the config flow will be dismissed # in the event the user does not want to pair with Home Assistant. # - if not integration.iot_class or not integration.iot_class.startswith( - "cloud" + if not integration.iot_class or ( + not integration.iot_class.startswith("cloud") + and "polling" not in integration.iot_class ): return diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 88dceb9d464..6bc37e10da2 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -530,7 +530,8 @@ async def test_homekit_match_partial_space(hass, mock_async_zeroconf): await hass.async_block_till_done() assert len(mock_service_browser.mock_calls) == 1 - assert len(mock_config_flow.mock_calls) == 1 + # One for HKC, and one for LIFX since lifx is local polling + assert len(mock_config_flow.mock_calls) == 2 assert mock_config_flow.mock_calls[0][1][0] == "lifx" @@ -741,6 +742,44 @@ async def test_homekit_controller_still_discovered_unpaired_for_cloud( assert mock_config_flow.mock_calls[1][1][0] == "homekit_controller" +async def test_homekit_controller_still_discovered_unpaired_for_polling( + hass, mock_async_zeroconf +): + """Test discovery is still passed to homekit controller when unpaired and discovered by polling integration. + + Since we prefer local push, if the integration that is being discovered + is polling AND the homekit device is unpaired we still want to discovery it + """ + with patch.dict( + zc_gen.ZEROCONF, + {"_hap._udp.local.": [{"domain": "homekit_controller"}]}, + clear=True, + ), patch.dict( + zc_gen.HOMEKIT, + {"iSmartGate": "gogogate2"}, + clear=True, + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, + "HaAsyncServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._udp.local." + ), + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.AsyncServiceInfo", + side_effect=get_homekit_info_mock("iSmartGate", HOMEKIT_STATUS_UNPAIRED), + ): + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 2 + assert mock_config_flow.mock_calls[0][1][0] == "gogogate2" + assert mock_config_flow.mock_calls[1][1][0] == "homekit_controller" + + async def test_info_from_service_non_utf8(hass): """Test info_from_service handles non UTF-8 property keys and values correctly.""" service_type = "_test._tcp.local." From 3875fc59530d7794f80bb650c0d329499aade113 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 23:08:39 -0500 Subject: [PATCH 2186/3516] Cache the response from config/device_registry/list (#74483) --- .../components/config/device_registry.py | 56 ++++++++++++++----- .../components/config/test_device_registry.py | 30 +++++++++- 2 files changed, 71 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index e811d43d502..587710a8c2a 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -1,12 +1,23 @@ """HTTP views to interact with the device registry.""" +from __future__ import annotations + import voluptuous as vol from homeassistant import loader from homeassistant.components import websocket_api from homeassistant.components.websocket_api.decorators import require_admin -from homeassistant.core import HomeAssistant, callback +from homeassistant.components.websocket_api.messages import ( + IDEN_JSON_TEMPLATE, + IDEN_TEMPLATE, + message_to_json, +) +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import DeviceEntryDisabler, async_get +from homeassistant.helpers.device_registry import ( + EVENT_DEVICE_REGISTRY_UPDATED, + DeviceEntryDisabler, + async_get, +) WS_TYPE_LIST = "config/device_registry/list" SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( @@ -29,6 +40,36 @@ SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( async def async_setup(hass): """Enable the Device Registry views.""" + + cached_list_devices: str | None = None + + @callback + def _async_clear_list_device_cache(event: Event) -> None: + nonlocal cached_list_devices + cached_list_devices = None + + @callback + def websocket_list_devices(hass, connection, msg): + """Handle list devices command.""" + nonlocal cached_list_devices + if not cached_list_devices: + registry = async_get(hass) + cached_list_devices = message_to_json( + websocket_api.result_message( + IDEN_TEMPLATE, + [_entry_dict(entry) for entry in registry.devices.values()], + ) + ) + connection.send_message( + cached_list_devices.replace(IDEN_JSON_TEMPLATE, str(msg["id"]), 1) + ) + + hass.bus.async_listen( + EVENT_DEVICE_REGISTRY_UPDATED, + _async_clear_list_device_cache, + run_immediately=True, + ) + websocket_api.async_register_command( hass, WS_TYPE_LIST, websocket_list_devices, SCHEMA_WS_LIST ) @@ -41,17 +82,6 @@ async def async_setup(hass): return True -@callback -def websocket_list_devices(hass, connection, msg): - """Handle list devices command.""" - registry = async_get(hass) - connection.send_message( - websocket_api.result_message( - msg["id"], [_entry_dict(entry) for entry in registry.devices.values()] - ) - ) - - @require_admin @callback def websocket_update_device(hass, connection, msg): diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index f923b326100..f9289b6e3b3 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -29,14 +29,14 @@ def registry(hass): async def test_list_devices(hass, client, registry): """Test list entries.""" - registry.async_get_or_create( + device1 = registry.async_get_or_create( config_entry_id="1234", connections={("ethernet", "12:34:56:78:90:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", model="model", ) - registry.async_get_or_create( + device2 = registry.async_get_or_create( config_entry_id="1234", identifiers={("bridgeid", "1234")}, manufacturer="manufacturer", @@ -85,6 +85,32 @@ async def test_list_devices(hass, client, registry): }, ] + registry.async_remove_device(device2.id) + await hass.async_block_till_done() + + await client.send_json({"id": 6, "type": "config/device_registry/list"}) + msg = await client.receive_json() + + assert msg["result"] == [ + { + "area_id": None, + "config_entries": ["1234"], + "configuration_url": None, + "connections": [["ethernet", "12:34:56:78:90:AB:CD:EF"]], + "disabled_by": None, + "entry_type": None, + "hw_version": None, + "id": device1.id, + "identifiers": [["bridgeid", "0123"]], + "manufacturer": "manufacturer", + "model": "model", + "name": None, + "name_by_user": None, + "sw_version": None, + "via_device_id": None, + } + ] + @pytest.mark.parametrize( "payload_key,payload_value", From 8dfb0cb4e7da11c599f90414d0f98ea28a6c2e87 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 23:09:33 -0500 Subject: [PATCH 2187/3516] Fix SIGN_QUERY_PARAM in check in auth_middleware (#74479) --- homeassistant/components/http/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 09ef6e13e03..18f68cc386f 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -218,7 +218,7 @@ async def async_setup_auth(hass: HomeAssistant, app: Application) -> None: # for every request. elif ( request.method == "GET" - and SIGN_QUERY_PARAM in request.query + and SIGN_QUERY_PARAM in request.query_string and await async_validate_signed_request(request) ): authenticated = True From ce35324e7391f0e06c415f414e7a52276ff4d1d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 23:11:51 -0500 Subject: [PATCH 2188/3516] Cache the response from config/entity_registry/list (#74443) --- .../components/config/entity_registry.py | 95 +++++++++++-------- .../components/config/test_entity_registry.py | 35 +++++++ 2 files changed, 92 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 5f484c10472..445ca96c8b0 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -1,11 +1,18 @@ """HTTP views to interact with the entity registry.""" +from __future__ import annotations + import voluptuous as vol from homeassistant import config_entries from homeassistant.components import websocket_api from homeassistant.components.websocket_api.const import ERR_NOT_FOUND from homeassistant.components.websocket_api.decorators import require_admin -from homeassistant.core import callback +from homeassistant.components.websocket_api.messages import ( + IDEN_JSON_TEMPLATE, + IDEN_TEMPLATE, + message_to_json, +) +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -13,8 +20,40 @@ from homeassistant.helpers import ( ) -async def async_setup(hass): +async def async_setup(hass: HomeAssistant) -> bool: """Enable the Entity Registry views.""" + + cached_list_entities: str | None = None + + @callback + def _async_clear_list_entities_cache(event: Event) -> None: + nonlocal cached_list_entities + cached_list_entities = None + + @websocket_api.websocket_command( + {vol.Required("type"): "config/entity_registry/list"} + ) + @callback + def websocket_list_entities(hass, connection, msg): + """Handle list registry entries command.""" + nonlocal cached_list_entities + if not cached_list_entities: + registry = er.async_get(hass) + cached_list_entities = message_to_json( + websocket_api.result_message( + IDEN_TEMPLATE, + [_entry_dict(entry) for entry in registry.entities.values()], + ) + ) + connection.send_message( + cached_list_entities.replace(IDEN_JSON_TEMPLATE, str(msg["id"]), 1) + ) + + hass.bus.async_listen( + er.EVENT_ENTITY_REGISTRY_UPDATED, + _async_clear_list_entities_cache, + run_immediately=True, + ) websocket_api.async_register_command(hass, websocket_list_entities) websocket_api.async_register_command(hass, websocket_get_entity) websocket_api.async_register_command(hass, websocket_update_entity) @@ -22,33 +61,6 @@ async def async_setup(hass): return True -@websocket_api.websocket_command({vol.Required("type"): "config/entity_registry/list"}) -@callback -def websocket_list_entities(hass, connection, msg): - """Handle list registry entries command.""" - registry = er.async_get(hass) - connection.send_message( - websocket_api.result_message( - msg["id"], - [ - { - "area_id": entry.area_id, - "config_entry_id": entry.config_entry_id, - "device_id": entry.device_id, - "disabled_by": entry.disabled_by, - "entity_category": entry.entity_category, - "entity_id": entry.entity_id, - "hidden_by": entry.hidden_by, - "icon": entry.icon, - "name": entry.name, - "platform": entry.platform, - } - for entry in registry.entities.values() - ], - ) - ) - - @websocket_api.websocket_command( { vol.Required("type"): "config/entity_registry/get", @@ -211,7 +223,7 @@ def websocket_remove_entity(hass, connection, msg): @callback -def _entry_ext_dict(entry): +def _entry_dict(entry): """Convert entry to API format.""" return { "area_id": entry.area_id, @@ -224,12 +236,19 @@ def _entry_ext_dict(entry): "icon": entry.icon, "name": entry.name, "platform": entry.platform, - "capabilities": entry.capabilities, - "device_class": entry.device_class, - "has_entity_name": entry.has_entity_name, - "options": entry.options, - "original_device_class": entry.original_device_class, - "original_icon": entry.original_icon, - "original_name": entry.original_name, - "unique_id": entry.unique_id, } + + +@callback +def _entry_ext_dict(entry): + """Convert entry to API format.""" + data = _entry_dict(entry) + data["capabilities"] = entry.capabilities + data["device_class"] = entry.device_class + data["has_entity_name"] = entry.has_entity_name + data["options"] = entry.options + data["original_device_class"] = entry.original_device_class + data["original_icon"] = entry.original_icon + data["original_name"] = entry.original_name + data["unique_id"] = entry.unique_id + return data diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 69744817a27..9c5984f751e 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -5,6 +5,7 @@ from homeassistant.components.config import entity_registry from homeassistant.const import ATTR_ICON from homeassistant.helpers.device_registry import DeviceEntryDisabler from homeassistant.helpers.entity_registry import ( + EVENT_ENTITY_REGISTRY_UPDATED, RegistryEntry, RegistryEntryDisabler, RegistryEntryHider, @@ -81,6 +82,40 @@ async def test_list_entities(hass, client): }, ] + mock_registry( + hass, + { + "test_domain.name": RegistryEntry( + entity_id="test_domain.name", + unique_id="1234", + platform="test_platform", + name="Hello World", + ), + }, + ) + + hass.bus.async_fire( + EVENT_ENTITY_REGISTRY_UPDATED, + {"action": "create", "entity_id": "test_domain.no_name"}, + ) + await client.send_json({"id": 6, "type": "config/entity_registry/list"}) + msg = await client.receive_json() + + assert msg["result"] == [ + { + "config_entry_id": None, + "device_id": None, + "area_id": None, + "disabled_by": None, + "entity_id": "test_domain.name", + "hidden_by": None, + "name": "Hello World", + "icon": None, + "platform": "test_platform", + "entity_category": None, + }, + ] + async def test_get_entity(hass, client): """Test get entry.""" From 148035c8ca8c6d706ec2ddcc9a52475b086ccb0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 6 Jul 2022 00:52:41 -0500 Subject: [PATCH 2189/3516] Bump aiohomekit to 0.7.20 (#74489) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index a15c576c313..955f5e37177 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.7.19"], + "requirements": ["aiohomekit==0.7.20"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 82a7b2aa99c..5696bbe1db1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.19 +aiohomekit==0.7.20 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index beda687d9f8..07a71a6915e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.19 +aiohomekit==0.7.20 # homeassistant.components.emulated_hue # homeassistant.components.http From 05416f56aa8f136126c85f72f6d53f93b5c5c3a7 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 6 Jul 2022 09:45:30 +0200 Subject: [PATCH 2190/3516] Use FlowResultType in deCONZ config flow tests (#74495) --- tests/components/deconz/test_config_flow.py | 76 ++++++++++----------- 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 1a7031a0fd6..2f21081a5ae 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -29,11 +29,7 @@ from homeassistant.config_entries import ( SOURCE_USER, ) from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .test_gateway import API_KEY, BRIDGEID, setup_deconz_integration @@ -55,14 +51,14 @@ async def test_flow_discovered_bridges(hass, aioclient_mock): DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "1.2.3.4"} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -75,7 +71,7 @@ async def test_flow_discovered_bridges(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -100,7 +96,7 @@ async def test_flow_manual_configuration_decision(hass, aioclient_mock): result["flow_id"], user_input={CONF_HOST: CONF_MANUAL_INPUT} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -108,7 +104,7 @@ async def test_flow_manual_configuration_decision(hass, aioclient_mock): user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -127,7 +123,7 @@ async def test_flow_manual_configuration_decision(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -148,7 +144,7 @@ async def test_flow_manual_configuration(hass, aioclient_mock): DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -156,7 +152,7 @@ async def test_flow_manual_configuration(hass, aioclient_mock): user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -175,7 +171,7 @@ async def test_flow_manual_configuration(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -192,7 +188,7 @@ async def test_manual_configuration_after_discovery_timeout(hass, aioclient_mock DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" assert not hass.config_entries.flow._progress[result["flow_id"]].bridges @@ -205,7 +201,7 @@ async def test_manual_configuration_after_discovery_ResponseError(hass, aioclien DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" assert not hass.config_entries.flow._progress[result["flow_id"]].bridges @@ -224,7 +220,7 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock): DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -232,7 +228,7 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock): user_input={CONF_HOST: "2.3.4.5", CONF_PORT: 80}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -251,7 +247,7 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "2.3.4.5" @@ -270,7 +266,7 @@ async def test_manual_configuration_dont_update_configuration(hass, aioclient_mo DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -278,7 +274,7 @@ async def test_manual_configuration_dont_update_configuration(hass, aioclient_mo user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -297,7 +293,7 @@ async def test_manual_configuration_dont_update_configuration(hass, aioclient_mo result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -313,7 +309,7 @@ async def test_manual_configuration_timeout_get_bridge(hass, aioclient_mock): DECONZ_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_input" result = await hass.config_entries.flow.async_configure( @@ -321,7 +317,7 @@ async def test_manual_configuration_timeout_get_bridge(hass, aioclient_mock): user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 80}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post( @@ -338,7 +334,7 @@ async def test_manual_configuration_timeout_get_bridge(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_bridges" @@ -367,7 +363,7 @@ async def test_link_step_fails(hass, aioclient_mock, raised_error, error_string) result["flow_id"], user_input={CONF_HOST: "1.2.3.4"} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" aioclient_mock.post("http://1.2.3.4:80/api", exc=raised_error) @@ -376,7 +372,7 @@ async def test_link_step_fails(hass, aioclient_mock, raised_error, error_string) result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": error_string} @@ -391,7 +387,7 @@ async def test_reauth_flow_update_configuration(hass, aioclient_mock): context={"source": SOURCE_REAUTH}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" new_api_key = "new_key" @@ -412,7 +408,7 @@ async def test_reauth_flow_update_configuration(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_API_KEY] == new_api_key @@ -433,7 +429,7 @@ async def test_flow_ssdp_discovery(hass, aioclient_mock): context={"source": SOURCE_SSDP}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "link" flows = hass.config_entries.flow.async_progress() @@ -450,7 +446,7 @@ async def test_flow_ssdp_discovery(hass, aioclient_mock): result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == BRIDGEID assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -482,7 +478,7 @@ async def test_ssdp_discovery_update_configuration(hass, aioclient_mock): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "2.3.4.5" assert len(mock_setup_entry.mock_calls) == 1 @@ -506,7 +502,7 @@ async def test_ssdp_discovery_dont_update_configuration(hass, aioclient_mock): context={"source": SOURCE_SSDP}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "1.2.3.4" @@ -533,7 +529,7 @@ async def test_ssdp_discovery_dont_update_existing_hassio_configuration( context={"source": SOURCE_SSDP}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "1.2.3.4" @@ -553,7 +549,7 @@ async def test_flow_hassio_discovery(hass): ), context={"source": SOURCE_HASSIO}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "hassio_confirm" assert result["description_placeholders"] == {"addon": "Mock Addon"} @@ -572,7 +568,7 @@ async def test_flow_hassio_discovery(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["result"].data == { CONF_HOST: "mock-deconz", CONF_PORT: 80, @@ -603,7 +599,7 @@ async def test_hassio_discovery_update_configuration(hass, aioclient_mock): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == "2.3.4.5" assert config_entry.data[CONF_PORT] == 8080 @@ -628,7 +624,7 @@ async def test_hassio_discovery_dont_update_configuration(hass, aioclient_mock): context={"source": SOURCE_HASSIO}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -638,7 +634,7 @@ async def test_option_flow(hass, aioclient_mock): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "deconz_devices" result = await hass.config_entries.options.async_configure( @@ -650,7 +646,7 @@ async def test_option_flow(hass, aioclient_mock): }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_ALLOW_CLIP_SENSOR: False, CONF_ALLOW_DECONZ_GROUPS: False, From 1b37d9cbc6fa255fbbde3335c85ef7ed3fb871ca Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Wed, 6 Jul 2022 09:52:41 +0200 Subject: [PATCH 2191/3516] Use FlowResultType in Devolo Home Control tests (#74490) * Use FlowResultType in devolo Home Control tests * Add return types --- .../devolo_home_control/test_config_flow.py | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index d2a359b9438..4f7140b7980 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -3,8 +3,10 @@ from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components.devolo_home_control.const import DEFAULT_MYDEVOLO, DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult, FlowResultType from .const import ( DISCOVERY_INFO, @@ -15,28 +17,28 @@ from .const import ( from tests.common import MockConfigEntry -async def test_form(hass): +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["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} await _setup(hass, result) @pytest.mark.credentials_invalid -async def test_form_invalid_credentials_user(hass): +async def test_form_invalid_credentials_user(hass: HomeAssistant) -> None: """Test if we get the error message on invalid credentials.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( @@ -47,7 +49,7 @@ async def test_form_invalid_credentials_user(hass): assert result["errors"] == {"base": "invalid_auth"} -async def test_form_already_configured(hass): +async def test_form_already_configured(hass: HomeAssistant) -> None: """Test if we get the error message on already configured.""" with patch( "homeassistant.components.devolo_home_control.Mydevolo.uuid", @@ -59,17 +61,17 @@ async def test_form_already_configured(hass): context={"source": config_entries.SOURCE_USER}, data={"username": "test-username", "password": "test-password"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" -async def test_form_advanced_options(hass): +async def test_form_advanced_options(hass: HomeAssistant) -> None: """Test if we get the advanced options if user has enabled it.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -100,7 +102,7 @@ async def test_form_advanced_options(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_zeroconf(hass): +async def test_form_zeroconf(hass: HomeAssistant) -> None: """Test that the zeroconf confirmation form is served.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -109,13 +111,13 @@ async def test_form_zeroconf(hass): ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM await _setup(hass, result) @pytest.mark.credentials_invalid -async def test_form_invalid_credentials_zeroconf(hass): +async def test_form_invalid_credentials_zeroconf(hass: HomeAssistant) -> None: """Test if we get the error message on invalid credentials.""" result = await hass.config_entries.flow.async_init( @@ -125,7 +127,7 @@ async def test_form_invalid_credentials_zeroconf(hass): ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -135,7 +137,7 @@ async def test_form_invalid_credentials_zeroconf(hass): assert result["errors"] == {"base": "invalid_auth"} -async def test_zeroconf_wrong_device(hass): +async def test_zeroconf_wrong_device(hass: HomeAssistant) -> None: """Test that the zeroconf ignores wrong devices.""" result = await hass.config_entries.flow.async_init( DOMAIN, @@ -144,7 +146,7 @@ async def test_zeroconf_wrong_device(hass): ) assert result["reason"] == "Not a devolo Home Control gateway." - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT result = await hass.config_entries.flow.async_init( DOMAIN, @@ -153,10 +155,10 @@ async def test_zeroconf_wrong_device(hass): ) assert result["reason"] == "Not a devolo Home Control gateway." - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT -async def test_form_reauth(hass): +async def test_form_reauth(hass: HomeAssistant) -> None: """Test that the reauth confirmation form is served.""" mock_config = MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}) mock_config.add_to_hass(hass) @@ -174,7 +176,7 @@ async def test_form_reauth(hass): ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM with patch( "homeassistant.components.devolo_home_control.async_setup_entry", @@ -189,12 +191,12 @@ async def test_form_reauth(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert len(mock_setup_entry.mock_calls) == 1 @pytest.mark.credentials_invalid -async def test_form_invalid_credentials_reauth(hass): +async def test_form_invalid_credentials_reauth(hass: HomeAssistant) -> None: """Test if we get the error message on invalid credentials.""" mock_config = MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}) mock_config.add_to_hass(hass) @@ -219,7 +221,7 @@ async def test_form_invalid_credentials_reauth(hass): assert result["errors"] == {"base": "invalid_auth"} -async def test_form_uuid_change_reauth(hass): +async def test_form_uuid_change_reauth(hass: HomeAssistant) -> None: """Test that the reauth confirmation form is served.""" mock_config = MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}) mock_config.add_to_hass(hass) @@ -237,7 +239,7 @@ async def test_form_uuid_change_reauth(hass): ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM with patch( "homeassistant.components.devolo_home_control.async_setup_entry", @@ -252,11 +254,11 @@ async def test_form_uuid_change_reauth(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "reauth_failed"} -async def _setup(hass, result): +async def _setup(hass: HomeAssistant, result: FlowResult) -> None: """Finish configuration steps.""" with patch( "homeassistant.components.devolo_home_control.async_setup_entry", From ef6fd78ede448ac6d88e92f005b4e9520c6b00e8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 6 Jul 2022 09:54:26 +0200 Subject: [PATCH 2192/3516] Use FlowResultType in Axis config flow tests (#74496) --- tests/components/axis/test_config_flow.py | 43 ++++++++++------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index f09e87020c3..1459ae215d9 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -4,7 +4,6 @@ from unittest.mock import patch import pytest import respx -from homeassistant import data_entry_flow from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.components.axis import config_flow from homeassistant.components.axis.const import ( @@ -31,11 +30,7 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, ) -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .test_device import ( DEFAULT_HOST, @@ -57,7 +52,7 @@ async def test_flow_manual_configuration(hass): AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with respx.mock: @@ -72,7 +67,7 @@ async def test_flow_manual_configuration(hass): }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -93,7 +88,7 @@ async def test_manual_configuration_update_configuration(hass): AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -112,7 +107,7 @@ async def test_manual_configuration_update_configuration(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert device.host == "2.3.4.5" assert len(mock_setup_entry.mock_calls) == 1 @@ -124,7 +119,7 @@ async def test_flow_fails_faulty_credentials(hass): AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -150,7 +145,7 @@ async def test_flow_fails_cannot_connect(hass): AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -187,7 +182,7 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): AXIS_DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with respx.mock: @@ -202,7 +197,7 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -227,7 +222,7 @@ async def test_reauth_flow_update_configuration(hass): data=config_entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with respx.mock: @@ -243,7 +238,7 @@ async def test_reauth_flow_update_configuration(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert device.host == "2.3.4.5" assert device.username == "user2" @@ -317,7 +312,7 @@ async def test_discovery_flow(hass, source: str, discovery_info: dict): AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER flows = hass.config_entries.flow.async_progress() @@ -336,7 +331,7 @@ async def test_discovery_flow(hass, source: str, discovery_info: dict): }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -398,7 +393,7 @@ async def test_discovered_device_already_configured( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == DEFAULT_HOST @@ -467,7 +462,7 @@ async def test_discovery_flow_updated_configuration( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data == { CONF_HOST: "2.3.4.5", @@ -525,7 +520,7 @@ async def test_discovery_flow_ignore_non_axis_device( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_axis_device" @@ -574,7 +569,7 @@ async def test_discovery_flow_ignore_link_local_address( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "link_local_address" @@ -591,7 +586,7 @@ async def test_option_flow(hass): device.config_entry.entry_id ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "configure_stream" assert set(result["data_schema"].schema[CONF_STREAM_PROFILE].container) == { DEFAULT_STREAM_PROFILE, @@ -608,7 +603,7 @@ async def test_option_flow(hass): user_input={CONF_STREAM_PROFILE: "profile_1", CONF_VIDEO_SOURCE: 1}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_EVENTS: True, CONF_STREAM_PROFILE: "profile_1", From a27d483009a969cedd6156071811674c1e19b62a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 6 Jul 2022 10:25:53 +0200 Subject: [PATCH 2193/3516] Remove unifi from mypy ignore list (#74456) * Remove unifi diagnostics from mypy ignore list * Remove unifi init from mypy ignore list * Remove unifi device tracker from mypy ignore list * Adjust doc string * Adjust doc string * Remove unifi entity base from mypy ignore list * Keep comprehension * Remove unifi config flow from mypy ignore list * Fix circular import --- homeassistant/components/unifi/__init__.py | 8 +++++- homeassistant/components/unifi/config_flow.py | 28 ++++++++----------- .../components/unifi/device_tracker.py | 12 +++----- homeassistant/components/unifi/diagnostics.py | 5 ++-- .../components/unifi/unifi_client.py | 8 ++++-- .../components/unifi/unifi_entity_base.py | 15 +++++++--- mypy.ini | 15 ---------- script/hassfest/mypy_config.py | 5 ---- 8 files changed, 43 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 7a874aff993..1369bb69e1b 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -1,4 +1,7 @@ """Integration to UniFi Network and its various features.""" +from collections.abc import Mapping +from typing import Any + from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback @@ -92,7 +95,10 @@ async def async_flatten_entry_data( Keep controller key layer in case user rollbacks. """ - data: dict = {**config_entry.data, **config_entry.data[CONF_CONTROLLER]} + data: Mapping[str, Any] = { + **config_entry.data, + **config_entry.data[CONF_CONTROLLER], + } if config_entry.data != data: hass.config_entries.async_update_entry(config_entry, data=data) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index fcf5970bf6c..2f49c15e4d8 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -46,7 +46,7 @@ from .const import ( DEFAULT_POE_CLIENTS, DOMAIN as UNIFI_DOMAIN, ) -from .controller import get_controller +from .controller import UniFiController, get_controller from .errors import AuthenticationRequired, CannotConnect DEFAULT_PORT = 443 @@ -75,11 +75,11 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): def __init__(self) -> None: """Initialize the UniFi Network flow.""" - self.config = {} - self.site_ids = {} - self.site_names = {} - self.reauth_config_entry = None - self.reauth_schema = {} + self.config: dict[str, Any] = {} + self.site_ids: dict[str, str] = {} + self.site_names: dict[str, str] = {} + self.reauth_config_entry: config_entries.ConfigEntry | None = None + self.reauth_schema: dict[vol.Marker, Any] = {} async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -156,8 +156,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Select site to control.""" - errors = {} - if user_input is not None: unique_id = user_input[CONF_SITE_ID] @@ -173,9 +171,9 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): abort_reason = "reauth_successful" if config_entry: - controller = self.hass.data.get(UNIFI_DOMAIN, {}).get( - config_entry.entry_id - ) + controller: UniFiController | None = self.hass.data.get( + UNIFI_DOMAIN, {} + ).get(config_entry.entry_id) if controller and controller.available: return self.async_abort(reason="already_configured") @@ -199,7 +197,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): data_schema=vol.Schema( {vol.Required(CONF_SITE_ID): vol.In(self.site_names)} ), - errors=errors, ) async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: @@ -207,6 +204,7 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): config_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] ) + assert config_entry self.reauth_config_entry = config_entry self.context["title_placeholders"] = { @@ -258,11 +256,12 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): class UnifiOptionsFlowHandler(config_entries.OptionsFlow): """Handle Unifi Network options.""" + controller: UniFiController + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize UniFi Network options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) - self.controller = None async def async_step_init( self, user_input: dict[str, Any] | None = None @@ -379,8 +378,6 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage configuration of network access controlled clients.""" - errors = {} - if user_input is not None: self.options.update(user_input) return await self.async_step_statistics_sensors() @@ -417,7 +414,6 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): ): bool, } ), - errors=errors, last_step=False, ) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index b2070362d02..f2c2230b9e0 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -27,7 +27,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util from .const import DOMAIN as UNIFI_DOMAIN -from .unifi_client import UniFiClient +from .controller import UniFiController +from .unifi_client import UniFiClientBase from .unifi_entity_base import UniFiBase LOGGER = logging.getLogger(__name__) @@ -79,7 +80,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up device tracker for UniFi Network integration.""" - controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] controller.entities[DOMAIN] = {CLIENT_TRACKER: set(), DEVICE_TRACKER: set()} @callback @@ -144,7 +145,7 @@ def add_device_entities(controller, async_add_entities, devices): async_add_entities(trackers) -class UniFiClientTracker(UniFiClient, ScannerEntity): +class UniFiClientTracker(UniFiClientBase, ScannerEntity): """Representation of a network client.""" DOMAIN = DOMAIN @@ -261,11 +262,6 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): self.async_write_ha_state() self._async_log_debug_data("make_disconnected") - @property - def device_info(self) -> None: - """Return no device info.""" - return None - @property def is_connected(self): """Return true if the client is connected to the network.""" diff --git a/homeassistant/components/unifi/diagnostics.py b/homeassistant/components/unifi/diagnostics.py index ed059856881..b35fd520ab0 100644 --- a/homeassistant/components/unifi/diagnostics.py +++ b/homeassistant/components/unifi/diagnostics.py @@ -12,6 +12,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import format_mac from .const import CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN +from .controller import UniFiController TO_REDACT = {CONF_CONTROLLER, CONF_PASSWORD} REDACT_CONFIG = {CONF_CONTROLLER, CONF_HOST, CONF_PASSWORD, CONF_USERNAME} @@ -56,7 +57,7 @@ def async_replace_list_data( """Redact sensitive data in a list.""" redacted = [] for item in data: - new_value = None + new_value: Any | None = None if isinstance(item, (list, set, tuple)): new_value = async_replace_list_data(item, to_replace) elif isinstance(item, Mapping): @@ -74,7 +75,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] diag: dict[str, Any] = {} macs_to_redact: dict[str, str] = {} diff --git a/homeassistant/components/unifi/unifi_client.py b/homeassistant/components/unifi/unifi_client.py index 9e90eef518a..82aece81b6d 100644 --- a/homeassistant/components/unifi/unifi_client.py +++ b/homeassistant/components/unifi/unifi_client.py @@ -5,8 +5,8 @@ from homeassistant.helpers.entity import DeviceInfo from .unifi_entity_base import UniFiBase -class UniFiClient(UniFiBase): - """Base class for UniFi clients.""" +class UniFiClientBase(UniFiBase): + """Base class for UniFi clients (without device info).""" def __init__(self, client, controller) -> None: """Set up client.""" @@ -44,6 +44,10 @@ class UniFiClient(UniFiBase): """Return if controller is available.""" return self.controller.available + +class UniFiClient(UniFiClientBase): + """Base class for UniFi clients (with device info).""" + @property def device_info(self) -> DeviceInfo: """Return a client description for device registry.""" diff --git a/homeassistant/components/unifi/unifi_entity_base.py b/homeassistant/components/unifi/unifi_entity_base.py index c611fc1ee60..466764714cf 100644 --- a/homeassistant/components/unifi/unifi_entity_base.py +++ b/homeassistant/components/unifi/unifi_entity_base.py @@ -1,12 +1,18 @@ """Base class for UniFi Network entities.""" +from __future__ import annotations + +from collections.abc import Callable import logging -from typing import Any +from typing import TYPE_CHECKING, Any from homeassistant.core import callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +if TYPE_CHECKING: + from .controller import UniFiController + _LOGGER = logging.getLogger(__name__) @@ -16,7 +22,7 @@ class UniFiBase(Entity): DOMAIN = "" TYPE = "" - def __init__(self, item, controller) -> None: + def __init__(self, item, controller: UniFiController) -> None: """Set up UniFi Network entity base. Register mac to controller entities to cover disabled entities. @@ -38,11 +44,12 @@ class UniFiBase(Entity): self.entity_id, self.key, ) - for signal, method in ( + signals: tuple[tuple[str, Callable[..., Any]], ...] = ( (self.controller.signal_reachable, self.async_signal_reachable_callback), (self.controller.signal_options_update, self.options_updated), (self.controller.signal_remove, self.remove_item), - ): + ) + for signal, method in signals: self.async_on_remove(async_dispatcher_connect(self.hass, signal, method)) self._item.register_callback(self.async_update_callback) diff --git a/mypy.ini b/mypy.ini index 52c4dce061f..08cd36dfaea 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2890,21 +2890,6 @@ ignore_errors = true [mypy-homeassistant.components.toon.models] ignore_errors = true -[mypy-homeassistant.components.unifi] -ignore_errors = true - -[mypy-homeassistant.components.unifi.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.unifi.device_tracker] -ignore_errors = true - -[mypy-homeassistant.components.unifi.diagnostics] -ignore_errors = true - -[mypy-homeassistant.components.unifi.unifi_entity_base] -ignore_errors = true - [mypy-homeassistant.components.withings] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 521c98ea93d..69e9d8fafa4 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -105,11 +105,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.toon", "homeassistant.components.toon.config_flow", "homeassistant.components.toon.models", - "homeassistant.components.unifi", - "homeassistant.components.unifi.config_flow", - "homeassistant.components.unifi.device_tracker", - "homeassistant.components.unifi.diagnostics", - "homeassistant.components.unifi.unifi_entity_base", "homeassistant.components.withings", "homeassistant.components.withings.binary_sensor", "homeassistant.components.withings.common", From 8fb9b45e422df75b3c54a4373e1e809af01a2604 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 6 Jul 2022 10:26:24 +0200 Subject: [PATCH 2194/3516] Remove input_datetime from mypy ignore list (#74447) * Remove input_datetime from mypy ignore list * Use assert * Use cast * Use common logic for initial parsing --- .../components/input_datetime/__init__.py | 41 +++++++++---------- mypy.ini | 3 -- script/hassfest/mypy_config.py | 1 - 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index fe034a8edb5..2a64c8d3b89 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import datetime as py_datetime import logging +from typing import Any import voluptuous as vol @@ -84,23 +85,32 @@ def has_date_or_time(conf): raise vol.Invalid("Entity needs at least a date or a time") -def valid_initial(conf): +def valid_initial(conf: dict[str, Any]) -> dict[str, Any]: """Check the initial value is valid.""" - if not (initial := conf.get(CONF_INITIAL)): + if not (conf.get(CONF_INITIAL)): return conf + # Ensure we can parse the initial value, raise vol.Invalid on failure + parse_initial_datetime(conf) + return conf + + +def parse_initial_datetime(conf: dict[str, Any]) -> py_datetime.datetime: + """Check the initial value is valid.""" + initial: str = conf[CONF_INITIAL] + if conf[CONF_HAS_DATE] and conf[CONF_HAS_TIME]: - if dt_util.parse_datetime(initial) is not None: - return conf + if (datetime := dt_util.parse_datetime(initial)) is not None: + return datetime raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a datetime") if conf[CONF_HAS_DATE]: - if dt_util.parse_date(initial) is not None: - return conf + if (date := dt_util.parse_date(initial)) is not None: + return py_datetime.datetime.combine(date, DEFAULT_TIME) raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a date") - if dt_util.parse_time(initial) is not None: - return conf + if (time := dt_util.parse_time(initial)) is not None: + return py_datetime.datetime.combine(py_datetime.date.today(), time) raise vol.Invalid(f"Initial value '{initial}' can't be parsed as a time") @@ -230,21 +240,10 @@ class InputDatetime(RestoreEntity): self.editable = True self._current_datetime = None - if not (initial := config.get(CONF_INITIAL)): + if not config.get(CONF_INITIAL): return - if self.has_date and self.has_time: - current_datetime = dt_util.parse_datetime(initial) - - elif self.has_date: - date = dt_util.parse_date(initial) - current_datetime = py_datetime.datetime.combine(date, DEFAULT_TIME) - - else: - time = dt_util.parse_time(initial) - current_datetime = py_datetime.datetime.combine( - py_datetime.date.today(), time - ) + current_datetime = parse_initial_datetime(config) # If the user passed in an initial value with a timezone, convert it to right tz if current_datetime.tzinfo is not None: diff --git a/mypy.ini b/mypy.ini index 08cd36dfaea..38cb7d4c7f0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2716,9 +2716,6 @@ ignore_errors = true [mypy-homeassistant.components.influxdb] ignore_errors = true -[mypy-homeassistant.components.input_datetime] -ignore_errors = true - [mypy-homeassistant.components.izone.climate] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 69e9d8fafa4..d540ed61cd8 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -47,7 +47,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.icloud.device_tracker", "homeassistant.components.icloud.sensor", "homeassistant.components.influxdb", - "homeassistant.components.input_datetime", "homeassistant.components.izone.climate", "homeassistant.components.konnected", "homeassistant.components.konnected.config_flow", From 8b97271c26cec1075d6c4a914d4f0e6af4436df7 Mon Sep 17 00:00:00 2001 From: Lerosen Date: Wed, 6 Jul 2022 11:21:15 +0200 Subject: [PATCH 2195/3516] Telegram bot map user data for callback query (#74302) fix(component/telegram-bot): map user data for callback query --- .../components/telegram_bot/__init__.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index be1c8325c5f..f4738334745 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -19,6 +19,7 @@ from telegram import ( ReplyKeyboardMarkup, ReplyKeyboardRemove, Update, + User, ) from telegram.error import TelegramError from telegram.ext import CallbackContext, Filters @@ -966,16 +967,17 @@ class BaseTelegramBotEntity: event_data[ATTR_TEXT] = message.text if message.from_user: - event_data.update( - { - ATTR_USER_ID: message.from_user.id, - ATTR_FROM_FIRST: message.from_user.first_name, - ATTR_FROM_LAST: message.from_user.last_name, - } - ) + event_data.update(self._get_user_event_data(message.from_user)) return event_type, event_data + def _get_user_event_data(self, user: User) -> dict[str, Any]: + return { + ATTR_USER_ID: user.id, + ATTR_FROM_FIRST: user.first_name, + ATTR_FROM_LAST: user.last_name, + } + def _get_callback_query_event_data( self, callback_query: CallbackQuery ) -> tuple[str, dict[str, Any]]: @@ -991,6 +993,9 @@ class BaseTelegramBotEntity: event_data[ATTR_MSG] = callback_query.message.to_dict() event_data[ATTR_CHAT_ID] = callback_query.message.chat.id + if callback_query.from_user: + event_data.update(self._get_user_event_data(callback_query.from_user)) + # Split data into command and args if possible event_data.update(self._get_command_event_data(callback_query.data)) From c4855909fa4907dafef7dc6185bd74f2fc144c2e Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 6 Jul 2022 13:35:25 +0200 Subject: [PATCH 2196/3516] Bump aioslimproto to 2.1.1 (#74499) --- homeassistant/components/slimproto/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/slimproto/manifest.json b/homeassistant/components/slimproto/manifest.json index 23b3198d7e4..1e076046b44 100644 --- a/homeassistant/components/slimproto/manifest.json +++ b/homeassistant/components/slimproto/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "iot_class": "local_push", "documentation": "https://www.home-assistant.io/integrations/slimproto", - "requirements": ["aioslimproto==2.0.1"], + "requirements": ["aioslimproto==2.1.1"], "codeowners": ["@marcelveldt"], "after_dependencies": ["media_source"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5696bbe1db1..4350315268d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -250,7 +250,7 @@ aioshelly==2.0.0 aioskybell==22.6.1 # homeassistant.components.slimproto -aioslimproto==2.0.1 +aioslimproto==2.1.1 # homeassistant.components.steamist aiosteamist==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07a71a6915e..882637eab89 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -219,7 +219,7 @@ aioshelly==2.0.0 aioskybell==22.6.1 # homeassistant.components.slimproto -aioslimproto==2.0.1 +aioslimproto==2.1.1 # homeassistant.components.steamist aiosteamist==0.3.2 From 8ccb008834638b847643b8a78b4c605b421d6bb2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 6 Jul 2022 14:29:26 +0200 Subject: [PATCH 2197/3516] Address NextDNS late review (#74503) * Fix coordinator type * Remove pylint disable --- homeassistant/components/nextdns/sensor.py | 4 +--- homeassistant/components/nextdns/system_health.py | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index fc5a8169526..ef0176b071a 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -294,11 +294,9 @@ async def async_setup_entry( async_add_entities(sensors) -class NextDnsSensor(CoordinatorEntity, SensorEntity): +class NextDnsSensor(CoordinatorEntity[NextDnsUpdateCoordinator], SensorEntity): """Define an NextDNS sensor.""" - coordinator: NextDnsUpdateCoordinator - def __init__( self, coordinator: NextDnsUpdateCoordinator, diff --git a/homeassistant/components/nextdns/system_health.py b/homeassistant/components/nextdns/system_health.py index 0fa31e75f1e..a56a89914b8 100644 --- a/homeassistant/components/nextdns/system_health.py +++ b/homeassistant/components/nextdns/system_health.py @@ -10,9 +10,8 @@ from homeassistant.core import HomeAssistant, callback @callback -def async_register( # pylint:disable=unused-argument - hass: HomeAssistant, - register: system_health.SystemHealthRegistration, +def async_register( + hass: HomeAssistant, register: system_health.SystemHealthRegistration ) -> None: """Register system health callbacks.""" register.async_register_info(system_health_info) From 47048e4df4a2e1128a0c5258f993a1e26416c413 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Jul 2022 16:22:45 +0200 Subject: [PATCH 2198/3516] Migrate aemet weather to native_* (#74494) --- homeassistant/components/aemet/const.py | 48 ++++++------- homeassistant/components/aemet/sensor.py | 20 +++--- homeassistant/components/aemet/weather.py | 69 ++++++++++++++++--- .../aemet/weather_update_coordinator.py | 46 ++++++------- 4 files changed, 114 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index 4be90011f5a..645c1ad0ea2 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -17,14 +17,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_RAINY, ATTR_CONDITION_SNOWY, ATTR_CONDITION_SUNNY, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, ) from homeassistant.const import ( DEGREE, @@ -45,8 +37,16 @@ ENTRY_NAME = "name" ENTRY_WEATHER_COORDINATOR = "weather_coordinator" ATTR_API_CONDITION = "condition" +ATTR_API_FORECAST_CONDITION = "condition" ATTR_API_FORECAST_DAILY = "forecast-daily" ATTR_API_FORECAST_HOURLY = "forecast-hourly" +ATTR_API_FORECAST_PRECIPITATION = "precipitation" +ATTR_API_FORECAST_PRECIPITATION_PROBABILITY = "precipitation_probability" +ATTR_API_FORECAST_TEMP = "temperature" +ATTR_API_FORECAST_TEMP_LOW = "templow" +ATTR_API_FORECAST_TIME = "datetime" +ATTR_API_FORECAST_WIND_BEARING = "wind_bearing" +ATTR_API_FORECAST_WIND_SPEED = "wind_speed" ATTR_API_HUMIDITY = "humidity" ATTR_API_PRESSURE = "pressure" ATTR_API_RAIN = "rain" @@ -158,14 +158,14 @@ CONDITIONS_MAP = { } FORECAST_MONITORED_CONDITIONS = [ - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, + ATTR_API_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ] MONITORED_CONDITIONS = [ ATTR_API_CONDITION, @@ -202,43 +202,43 @@ FORECAST_MODE_ATTR_API = { FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( - key=ATTR_FORECAST_CONDITION, + key=ATTR_API_FORECAST_CONDITION, name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION, + key=ATTR_API_FORECAST_PRECIPITATION, name="Precipitation", native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION_PROBABILITY, + key=ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, name="Precipitation probability", native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP, + key=ATTR_API_FORECAST_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP_LOW, + key=ATTR_API_FORECAST_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TIME, + key=ATTR_API_FORECAST_TIME, name="Time", device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( - key=ATTR_FORECAST_WIND_BEARING, + key=ATTR_API_FORECAST_WIND_BEARING, name="Wind bearing", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( - key=ATTR_FORECAST_WIND_SPEED, + key=ATTR_API_FORECAST_WIND_SPEED, name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, ), diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index f98e3fff49e..e34583148e1 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -10,7 +10,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util from .const import ( - ATTR_FORECAST_TIME, + ATTR_API_FORECAST_TIME, ATTRIBUTION, DOMAIN, ENTRY_NAME, @@ -45,17 +45,13 @@ async def async_setup_entry( entities.extend( [ AemetForecastSensor( - name_prefix, - unique_id_prefix, + f"{domain_data[ENTRY_NAME]} {mode} Forecast", + f"{unique_id}-forecast-{mode}", weather_coordinator, mode, description, ) for mode in FORECAST_MODES - if ( - (name_prefix := f"{domain_data[ENTRY_NAME]} {mode} Forecast") - and (unique_id_prefix := f"{unique_id}-forecast-{mode}") - ) for description in FORECAST_SENSOR_TYPES if description.key in FORECAST_MONITORED_CONDITIONS ] @@ -89,14 +85,14 @@ class AemetSensor(AbstractAemetSensor): def __init__( self, name, - unique_id, + unique_id_prefix, weather_coordinator: WeatherUpdateCoordinator, description: SensorEntityDescription, ): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id}-{description.key}", + unique_id=f"{unique_id_prefix}-{description.key}", coordinator=weather_coordinator, description=description, ) @@ -113,7 +109,7 @@ class AemetForecastSensor(AbstractAemetSensor): def __init__( self, name, - unique_id, + unique_id_prefix, weather_coordinator: WeatherUpdateCoordinator, forecast_mode, description: SensorEntityDescription, @@ -121,7 +117,7 @@ class AemetForecastSensor(AbstractAemetSensor): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id}-{description.key}", + unique_id=f"{unique_id_prefix}-{description.key}", coordinator=weather_coordinator, description=description, ) @@ -139,6 +135,6 @@ class AemetForecastSensor(AbstractAemetSensor): ) if forecasts: forecast = forecasts[0].get(self.entity_description.key) - if self.entity_description.key == ATTR_FORECAST_TIME: + if self.entity_description.key == ATTR_API_FORECAST_TIME: forecast = dt_util.parse_datetime(forecast) return forecast diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index d05442b621e..a7ff3630e78 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -1,13 +1,36 @@ """Support for the AEMET OpenData service.""" -from homeassistant.components.weather import WeatherEntity +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + WeatherEntity, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS +from homeassistant.const import ( + LENGTH_MILLIMETERS, + PRESSURE_HPA, + SPEED_KILOMETERS_PER_HOUR, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_API_CONDITION, + ATTR_API_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ATTR_API_HUMIDITY, ATTR_API_PRESSURE, ATTR_API_TEMPERATURE, @@ -19,10 +42,32 @@ from .const import ( ENTRY_WEATHER_COORDINATOR, FORECAST_MODE_ATTR_API, FORECAST_MODE_DAILY, + FORECAST_MODE_HOURLY, FORECAST_MODES, ) from .weather_update_coordinator import WeatherUpdateCoordinator +FORECAST_MAP = { + FORECAST_MODE_DAILY: { + ATTR_API_FORECAST_CONDITION: ATTR_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP_LOW: ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, + ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, + }, + FORECAST_MODE_HOURLY: { + ATTR_API_FORECAST_CONDITION: ATTR_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_PRECIPITATION: ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, + ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, + }, +} + async def async_setup_entry( hass: HomeAssistant, @@ -47,9 +92,10 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): """Implementation of an AEMET OpenData sensor.""" _attr_attribution = ATTRIBUTION - _attr_temperature_unit = TEMP_CELSIUS - _attr_pressure_unit = PRESSURE_HPA - _attr_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR def __init__( self, @@ -75,7 +121,12 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): @property def forecast(self): """Return the forecast array.""" - return self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]] + forecasts = self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]] + forecast_map = FORECAST_MAP[self._forecast_mode] + return [ + {ha_key: forecast[api_key] for api_key, ha_key in forecast_map.items()} + for forecast in forecasts + ] @property def humidity(self): @@ -83,12 +134,12 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_HUMIDITY] @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" return self.coordinator.data[ATTR_API_PRESSURE] @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data[ATTR_API_TEMPERATURE] @@ -98,6 +149,6 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_WIND_BEARING] @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" return self.coordinator.data[ATTR_API_WIND_SPEED] diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index c86465ea8f1..1c64206891c 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -42,23 +42,21 @@ from aemet_opendata.helpers import ( ) import async_timeout -from homeassistant.components.weather import ( - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, -) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util from .const import ( ATTR_API_CONDITION, + ATTR_API_FORECAST_CONDITION, ATTR_API_FORECAST_DAILY, ATTR_API_FORECAST_HOURLY, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ATTR_API_HUMIDITY, ATTR_API_PRESSURE, ATTR_API_RAIN, @@ -402,15 +400,15 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return None return { - ATTR_FORECAST_CONDITION: condition, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day( + ATTR_API_FORECAST_CONDITION: condition, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day( day ), - ATTR_FORECAST_TEMP: self._get_temperature_day(day), - ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), - ATTR_FORECAST_TIME: dt_util.as_utc(date).isoformat(), - ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), - ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), + ATTR_API_FORECAST_TEMP: self._get_temperature_day(day), + ATTR_API_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), + ATTR_API_FORECAST_TIME: dt_util.as_utc(date).isoformat(), + ATTR_API_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), + ATTR_API_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), } def _convert_forecast_hour(self, date, day, hour): @@ -420,15 +418,15 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): forecast_dt = date.replace(hour=hour, minute=0, second=0) return { - ATTR_FORECAST_CONDITION: condition, - ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour), - ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob( + ATTR_API_FORECAST_CONDITION: condition, + ATTR_API_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour), + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob( day, hour ), - ATTR_FORECAST_TEMP: self._get_temperature(day, hour), - ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), - ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), - ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), + ATTR_API_FORECAST_TEMP: self._get_temperature(day, hour), + ATTR_API_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), + ATTR_API_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), + ATTR_API_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), } def _calc_precipitation(self, day, hour): From 41fd1a24bb0823b4a24a0132bf994f32dbd6e84d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 6 Jul 2022 16:29:53 +0200 Subject: [PATCH 2199/3516] Add NextDNS button platform (#74492) * Add button platform * Add button tests * Fix typo * Use Platform enum * Fix coordinator type --- homeassistant/components/nextdns/__init__.py | 4 +- homeassistant/components/nextdns/button.py | 58 ++++++++++++++++++++ tests/components/nextdns/test_button.py | 47 ++++++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/nextdns/button.py create mode 100644 tests/components/nextdns/test_button.py diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index 7df172da8ab..f9cb1f60cd1 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -20,7 +20,7 @@ from nextdns import ( from nextdns.model import NextDnsData from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY +from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -131,7 +131,7 @@ class NextDnsProtocolsUpdateCoordinator(NextDnsUpdateCoordinator): _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.BUTTON, Platform.SENSOR] COORDINATORS = [ (ATTR_DNSSEC, NextDnsDnssecUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), (ATTR_ENCRYPTION, NextDnsEncryptionUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), diff --git a/homeassistant/components/nextdns/button.py b/homeassistant/components/nextdns/button.py new file mode 100644 index 00000000000..efe742a6113 --- /dev/null +++ b/homeassistant/components/nextdns/button.py @@ -0,0 +1,58 @@ +"""Support for the NextDNS service.""" +from __future__ import annotations + +from typing import cast + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import NextDnsStatusUpdateCoordinator +from .const import ATTR_STATUS, DOMAIN + +PARALLEL_UPDATES = 1 + +CLEAR_LOGS_BUTTON = ButtonEntityDescription( + key="clear_logs", + name="{profile_name} Clear Logs", + entity_category=EntityCategory.CONFIG, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add aNextDNS entities from a config_entry.""" + coordinator: NextDnsStatusUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + ATTR_STATUS + ] + + buttons: list[NextDnsButton] = [] + buttons.append(NextDnsButton(coordinator, CLEAR_LOGS_BUTTON)) + + async_add_entities(buttons) + + +class NextDnsButton(CoordinatorEntity[NextDnsStatusUpdateCoordinator], ButtonEntity): + """Define an NextDNS button.""" + + def __init__( + self, + coordinator: NextDnsStatusUpdateCoordinator, + description: ButtonEntityDescription, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + self._attr_device_info = coordinator.device_info + self._attr_unique_id = f"{coordinator.profile_id}_{description.key}" + self._attr_name = cast(str, description.name).format( + profile_name=coordinator.profile_name + ) + self.entity_description = description + + async def async_press(self) -> None: + """Trigger cleaning logs.""" + await self.coordinator.nextdns.clear_logs(self.coordinator.profile_id) diff --git a/tests/components/nextdns/test_button.py b/tests/components/nextdns/test_button.py new file mode 100644 index 00000000000..39201a668a4 --- /dev/null +++ b/tests/components/nextdns/test_button.py @@ -0,0 +1,47 @@ +"""Test button of NextDNS integration.""" +from unittest.mock import patch + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util + +from . import init_integration + + +async def test_button(hass): + """Test states of the button.""" + registry = er.async_get(hass) + + await init_integration(hass) + + state = hass.states.get("button.fake_profile_clear_logs") + assert state + assert state.state == STATE_UNKNOWN + + entry = registry.async_get("button.fake_profile_clear_logs") + assert entry + assert entry.unique_id == "xyz12_clear_logs" + + +async def test_button_press(hass): + """Test button press.""" + await init_integration(hass) + + now = dt_util.utcnow() + with patch( + "homeassistant.components.nextdns.NextDns.clear_logs" + ) as mock_clear_logs, patch("homeassistant.core.dt_util.utcnow", return_value=now): + await hass.services.async_call( + BUTTON_DOMAIN, + "press", + {ATTR_ENTITY_ID: "button.fake_profile_clear_logs"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_clear_logs.assert_called_once() + + state = hass.states.get("button.fake_profile_clear_logs") + assert state + assert state.state == now.isoformat() From 85dac3d47ec9e04690d429c9dbb216917aedb77b Mon Sep 17 00:00:00 2001 From: Gyosa3 <51777889+Gyosa3@users.noreply.github.com> Date: Wed, 6 Jul 2022 17:48:12 +0200 Subject: [PATCH 2200/3516] Add new alias for valid Celcius temperature units in Tuya (#74511) --- 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 8a3e59b1ac9..727e505200b 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -579,7 +579,7 @@ UNITS = ( ), UnitOfMeasurement( unit=TEMP_CELSIUS, - aliases={"°c", "c", "celsius"}, + aliases={"°c", "c", "celsius", "℃"}, device_classes={SensorDeviceClass.TEMPERATURE}, ), UnitOfMeasurement( From d6df26465fa45c17e0433b79425b28ca2401efe7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Jul 2022 17:49:06 +0200 Subject: [PATCH 2201/3516] Fix openweathermap forecast sensors (#74513) --- .../components/openweathermap/const.py | 30 ++++++------- .../components/openweathermap/weather.py | 41 +++++++++++++++++- .../weather_update_coordinator.py | 42 +++++++++---------- 3 files changed, 74 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 06f13daa9c2..836a56c70b2 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -21,9 +21,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TIME, ) from homeassistant.const import ( DEGREE, @@ -68,10 +65,15 @@ ATTR_API_FORECAST = "forecast" UPDATE_LISTENER = "update_listener" PLATFORMS = [Platform.SENSOR, Platform.WEATHER] -ATTR_FORECAST_PRECIPITATION = "precipitation" -ATTR_FORECAST_PRESSURE = "pressure" -ATTR_FORECAST_TEMP = "temperature" -ATTR_FORECAST_TEMP_LOW = "templow" +ATTR_API_FORECAST_CONDITION = "condition" +ATTR_API_FORECAST_PRECIPITATION = "precipitation" +ATTR_API_FORECAST_PRECIPITATION_PROBABILITY = "precipitation_probability" +ATTR_API_FORECAST_PRESSURE = "pressure" +ATTR_API_FORECAST_TEMP = "temperature" +ATTR_API_FORECAST_TEMP_LOW = "templow" +ATTR_API_FORECAST_TIME = "datetime" +ATTR_API_FORECAST_WIND_BEARING = "wind_bearing" +ATTR_API_FORECAST_WIND_SPEED = "wind_speed" FORECAST_MODE_HOURLY = "hourly" FORECAST_MODE_DAILY = "daily" @@ -263,39 +265,39 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ) FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( - key=ATTR_FORECAST_CONDITION, + key=ATTR_API_FORECAST_CONDITION, name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION, + key=ATTR_API_FORECAST_PRECIPITATION, name="Precipitation", native_unit_of_measurement=LENGTH_MILLIMETERS, ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION_PROBABILITY, + key=ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, name="Precipitation probability", native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_PRESSURE, + key=ATTR_API_FORECAST_PRESSURE, name="Pressure", native_unit_of_measurement=PRESSURE_HPA, device_class=SensorDeviceClass.PRESSURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP, + key=ATTR_API_FORECAST_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP_LOW, + key=ATTR_API_FORECAST_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TIME, + key=ATTR_API_FORECAST_TIME, name="Time", device_class=SensorDeviceClass.TIMESTAMP, ), diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index fce6efdf3c5..ea439a35586 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -1,7 +1,20 @@ """Support for the OpenWeatherMap (OWM) service.""" from __future__ import annotations -from homeassistant.components.weather import Forecast, WeatherEntity +from typing import cast + +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + Forecast, + WeatherEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( LENGTH_MILLIMETERS, @@ -17,6 +30,14 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ATTR_API_CONDITION, ATTR_API_FORECAST, + ATTR_API_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ATTR_API_HUMIDITY, ATTR_API_PRESSURE, ATTR_API_TEMPERATURE, @@ -31,6 +52,17 @@ from .const import ( ) from .weather_update_coordinator import WeatherUpdateCoordinator +FORECAST_MAP = { + ATTR_API_FORECAST_CONDITION: ATTR_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION: ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP_LOW: ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, + ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, +} + async def async_setup_entry( hass: HomeAssistant, @@ -109,7 +141,12 @@ class OpenWeatherMapWeather(WeatherEntity): @property def forecast(self) -> list[Forecast] | None: """Return the forecast array.""" - return self._weather_coordinator.data[ATTR_API_FORECAST] + api_forecasts = self._weather_coordinator.data[ATTR_API_FORECAST] + forecasts = [ + {ha_key: forecast[api_key] for api_key, ha_key in FORECAST_MAP.items()} + for forecast in api_forecasts + ] + return cast(list[Forecast], forecasts) @property def available(self) -> bool: diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index 36511424737..98c3b56fb5e 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -8,15 +8,6 @@ from pyowm.commons.exceptions import APIRequestError, UnauthorizedError from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_NATIVE_PRESSURE, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, - ATTR_FORECAST_NATIVE_WIND_SPEED, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, ) from homeassistant.helpers import sun from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -29,6 +20,15 @@ from .const import ( ATTR_API_DEW_POINT, ATTR_API_FEELS_LIKE_TEMPERATURE, ATTR_API_FORECAST, + ATTR_API_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_PRESSURE, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ATTR_API_HUMIDITY, ATTR_API_PRECIPITATION_KIND, ATTR_API_PRESSURE, @@ -158,19 +158,19 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): def _convert_forecast(self, entry): """Convert the forecast data.""" forecast = { - ATTR_FORECAST_TIME: dt.utc_from_timestamp( + ATTR_API_FORECAST_TIME: dt.utc_from_timestamp( entry.reference_time("unix") ).isoformat(), - ATTR_FORECAST_NATIVE_PRECIPITATION: self._calc_precipitation( + ATTR_API_FORECAST_PRECIPITATION: self._calc_precipitation( entry.rain, entry.snow ), - ATTR_FORECAST_PRECIPITATION_PROBABILITY: ( + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ( round(entry.precipitation_probability * 100) ), - ATTR_FORECAST_NATIVE_PRESSURE: entry.pressure.get("press"), - ATTR_FORECAST_NATIVE_WIND_SPEED: entry.wind().get("speed"), - ATTR_FORECAST_WIND_BEARING: entry.wind().get("deg"), - ATTR_FORECAST_CONDITION: self._get_condition( + ATTR_API_FORECAST_PRESSURE: entry.pressure.get("press"), + ATTR_API_FORECAST_WIND_SPEED: entry.wind().get("speed"), + ATTR_API_FORECAST_WIND_BEARING: entry.wind().get("deg"), + ATTR_API_FORECAST_CONDITION: self._get_condition( entry.weather_code, entry.reference_time("unix") ), ATTR_API_CLOUDS: entry.clouds, @@ -178,16 +178,12 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): temperature_dict = entry.temperature("celsius") if "max" in temperature_dict and "min" in temperature_dict: - forecast[ATTR_FORECAST_NATIVE_TEMP] = entry.temperature("celsius").get( - "max" - ) - forecast[ATTR_FORECAST_NATIVE_TEMP_LOW] = entry.temperature("celsius").get( + forecast[ATTR_API_FORECAST_TEMP] = entry.temperature("celsius").get("max") + forecast[ATTR_API_FORECAST_TEMP_LOW] = entry.temperature("celsius").get( "min" ) else: - forecast[ATTR_FORECAST_NATIVE_TEMP] = entry.temperature("celsius").get( - "temp" - ) + forecast[ATTR_API_FORECAST_TEMP] = entry.temperature("celsius").get("temp") return forecast From d3d2e250902dfe8cf47d28faea6f7189fc5376e7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Jul 2022 18:34:51 +0200 Subject: [PATCH 2202/3516] Update homematicip to 1.0.3 (#74516) --- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index b13c8ca19b2..40f7e67fd07 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==1.0.2"], + "requirements": ["homematicip==1.0.3"], "codeowners": [], "quality_scale": "platinum", "iot_class": "cloud_push", diff --git a/requirements_all.txt b/requirements_all.txt index 4350315268d..26f05cc1505 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -834,7 +834,7 @@ home-assistant-frontend==20220705.0 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.2 +homematicip==1.0.3 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 882637eab89..6a3f3bf5fd5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -604,7 +604,7 @@ home-assistant-frontend==20220705.0 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.2 +homematicip==1.0.3 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 From 113ccfe6afc5726a290e698f6dcc81929e4c0fd2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Jul 2022 19:31:57 +0200 Subject: [PATCH 2203/3516] Update Home Assistant Frontend to 20220706.0 (#74520) Bump Home Assistant Frontend to 20220706.0 --- 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 6b378fe1098..85ead380485 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220705.0"], + "requirements": ["home-assistant-frontend==20220706.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e05a9e7e800..8795e570216 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220705.0 +home-assistant-frontend==20220706.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 26f05cc1505..b63bb91e20f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220705.0 +home-assistant-frontend==20220706.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6a3f3bf5fd5..876944367d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -598,7 +598,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220705.0 +home-assistant-frontend==20220706.0 # homeassistant.components.home_connect homeconnect==0.7.1 From cd4255523881fc7b8a7b221f5f9b9d9b74b020ae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Jul 2022 23:00:40 -0500 Subject: [PATCH 2204/3516] Fix apple tv not coming online if connected before entity created (#74488) --- homeassistant/components/apple_tv/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 45250451f37..5177c6f3486 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -123,6 +123,10 @@ class AppleTVEntity(Entity): self.atv = None self.async_write_ha_state() + if self.manager.atv: + # ATV is already connected + _async_connected(self.manager.atv) + self.async_on_remove( async_dispatcher_connect( self.hass, f"{SIGNAL_CONNECTED}_{self.unique_id}", _async_connected From 06aa92b0b6409ced10521ca2b1e298f764675816 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 6 Jul 2022 00:52:41 -0500 Subject: [PATCH 2205/3516] Bump aiohomekit to 0.7.20 (#74489) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index a15c576c313..955f5e37177 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.7.19"], + "requirements": ["aiohomekit==0.7.20"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 726c499b88c..b0171ae3ee2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.19 +aiohomekit==0.7.20 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a7d822a5d9..bd0800310a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.19 +aiohomekit==0.7.20 # homeassistant.components.emulated_hue # homeassistant.components.http From c7c88877191d774af76ef0028cf5b309de8bdff9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Jul 2022 16:22:45 +0200 Subject: [PATCH 2206/3516] Migrate aemet weather to native_* (#74494) --- homeassistant/components/aemet/const.py | 48 ++++++------- homeassistant/components/aemet/sensor.py | 20 +++--- homeassistant/components/aemet/weather.py | 69 ++++++++++++++++--- .../aemet/weather_update_coordinator.py | 46 ++++++------- 4 files changed, 114 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/aemet/const.py b/homeassistant/components/aemet/const.py index 4be90011f5a..645c1ad0ea2 100644 --- a/homeassistant/components/aemet/const.py +++ b/homeassistant/components/aemet/const.py @@ -17,14 +17,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_RAINY, ATTR_CONDITION_SNOWY, ATTR_CONDITION_SUNNY, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, ) from homeassistant.const import ( DEGREE, @@ -45,8 +37,16 @@ ENTRY_NAME = "name" ENTRY_WEATHER_COORDINATOR = "weather_coordinator" ATTR_API_CONDITION = "condition" +ATTR_API_FORECAST_CONDITION = "condition" ATTR_API_FORECAST_DAILY = "forecast-daily" ATTR_API_FORECAST_HOURLY = "forecast-hourly" +ATTR_API_FORECAST_PRECIPITATION = "precipitation" +ATTR_API_FORECAST_PRECIPITATION_PROBABILITY = "precipitation_probability" +ATTR_API_FORECAST_TEMP = "temperature" +ATTR_API_FORECAST_TEMP_LOW = "templow" +ATTR_API_FORECAST_TIME = "datetime" +ATTR_API_FORECAST_WIND_BEARING = "wind_bearing" +ATTR_API_FORECAST_WIND_SPEED = "wind_speed" ATTR_API_HUMIDITY = "humidity" ATTR_API_PRESSURE = "pressure" ATTR_API_RAIN = "rain" @@ -158,14 +158,14 @@ CONDITIONS_MAP = { } FORECAST_MONITORED_CONDITIONS = [ - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, + ATTR_API_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ] MONITORED_CONDITIONS = [ ATTR_API_CONDITION, @@ -202,43 +202,43 @@ FORECAST_MODE_ATTR_API = { FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( - key=ATTR_FORECAST_CONDITION, + key=ATTR_API_FORECAST_CONDITION, name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION, + key=ATTR_API_FORECAST_PRECIPITATION, name="Precipitation", native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION_PROBABILITY, + key=ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, name="Precipitation probability", native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP, + key=ATTR_API_FORECAST_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP_LOW, + key=ATTR_API_FORECAST_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TIME, + key=ATTR_API_FORECAST_TIME, name="Time", device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( - key=ATTR_FORECAST_WIND_BEARING, + key=ATTR_API_FORECAST_WIND_BEARING, name="Wind bearing", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( - key=ATTR_FORECAST_WIND_SPEED, + key=ATTR_API_FORECAST_WIND_SPEED, name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, ), diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index f98e3fff49e..e34583148e1 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -10,7 +10,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util from .const import ( - ATTR_FORECAST_TIME, + ATTR_API_FORECAST_TIME, ATTRIBUTION, DOMAIN, ENTRY_NAME, @@ -45,17 +45,13 @@ async def async_setup_entry( entities.extend( [ AemetForecastSensor( - name_prefix, - unique_id_prefix, + f"{domain_data[ENTRY_NAME]} {mode} Forecast", + f"{unique_id}-forecast-{mode}", weather_coordinator, mode, description, ) for mode in FORECAST_MODES - if ( - (name_prefix := f"{domain_data[ENTRY_NAME]} {mode} Forecast") - and (unique_id_prefix := f"{unique_id}-forecast-{mode}") - ) for description in FORECAST_SENSOR_TYPES if description.key in FORECAST_MONITORED_CONDITIONS ] @@ -89,14 +85,14 @@ class AemetSensor(AbstractAemetSensor): def __init__( self, name, - unique_id, + unique_id_prefix, weather_coordinator: WeatherUpdateCoordinator, description: SensorEntityDescription, ): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id}-{description.key}", + unique_id=f"{unique_id_prefix}-{description.key}", coordinator=weather_coordinator, description=description, ) @@ -113,7 +109,7 @@ class AemetForecastSensor(AbstractAemetSensor): def __init__( self, name, - unique_id, + unique_id_prefix, weather_coordinator: WeatherUpdateCoordinator, forecast_mode, description: SensorEntityDescription, @@ -121,7 +117,7 @@ class AemetForecastSensor(AbstractAemetSensor): """Initialize the sensor.""" super().__init__( name=name, - unique_id=f"{unique_id}-{description.key}", + unique_id=f"{unique_id_prefix}-{description.key}", coordinator=weather_coordinator, description=description, ) @@ -139,6 +135,6 @@ class AemetForecastSensor(AbstractAemetSensor): ) if forecasts: forecast = forecasts[0].get(self.entity_description.key) - if self.entity_description.key == ATTR_FORECAST_TIME: + if self.entity_description.key == ATTR_API_FORECAST_TIME: forecast = dt_util.parse_datetime(forecast) return forecast diff --git a/homeassistant/components/aemet/weather.py b/homeassistant/components/aemet/weather.py index d05442b621e..a7ff3630e78 100644 --- a/homeassistant/components/aemet/weather.py +++ b/homeassistant/components/aemet/weather.py @@ -1,13 +1,36 @@ """Support for the AEMET OpenData service.""" -from homeassistant.components.weather import WeatherEntity +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + WeatherEntity, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS +from homeassistant.const import ( + LENGTH_MILLIMETERS, + PRESSURE_HPA, + SPEED_KILOMETERS_PER_HOUR, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_API_CONDITION, + ATTR_API_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ATTR_API_HUMIDITY, ATTR_API_PRESSURE, ATTR_API_TEMPERATURE, @@ -19,10 +42,32 @@ from .const import ( ENTRY_WEATHER_COORDINATOR, FORECAST_MODE_ATTR_API, FORECAST_MODE_DAILY, + FORECAST_MODE_HOURLY, FORECAST_MODES, ) from .weather_update_coordinator import WeatherUpdateCoordinator +FORECAST_MAP = { + FORECAST_MODE_DAILY: { + ATTR_API_FORECAST_CONDITION: ATTR_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP_LOW: ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, + ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, + }, + FORECAST_MODE_HOURLY: { + ATTR_API_FORECAST_CONDITION: ATTR_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_PRECIPITATION: ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, + ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, + }, +} + async def async_setup_entry( hass: HomeAssistant, @@ -47,9 +92,10 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): """Implementation of an AEMET OpenData sensor.""" _attr_attribution = ATTRIBUTION - _attr_temperature_unit = TEMP_CELSIUS - _attr_pressure_unit = PRESSURE_HPA - _attr_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + _attr_native_precipitation_unit = LENGTH_MILLIMETERS + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR def __init__( self, @@ -75,7 +121,12 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): @property def forecast(self): """Return the forecast array.""" - return self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]] + forecasts = self.coordinator.data[FORECAST_MODE_ATTR_API[self._forecast_mode]] + forecast_map = FORECAST_MAP[self._forecast_mode] + return [ + {ha_key: forecast[api_key] for api_key, ha_key in forecast_map.items()} + for forecast in forecasts + ] @property def humidity(self): @@ -83,12 +134,12 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_HUMIDITY] @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" return self.coordinator.data[ATTR_API_PRESSURE] @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" return self.coordinator.data[ATTR_API_TEMPERATURE] @@ -98,6 +149,6 @@ class AemetWeather(CoordinatorEntity[WeatherUpdateCoordinator], WeatherEntity): return self.coordinator.data[ATTR_API_WIND_BEARING] @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" return self.coordinator.data[ATTR_API_WIND_SPEED] diff --git a/homeassistant/components/aemet/weather_update_coordinator.py b/homeassistant/components/aemet/weather_update_coordinator.py index c86465ea8f1..1c64206891c 100644 --- a/homeassistant/components/aemet/weather_update_coordinator.py +++ b/homeassistant/components/aemet/weather_update_coordinator.py @@ -42,23 +42,21 @@ from aemet_opendata.helpers import ( ) import async_timeout -from homeassistant.components.weather import ( - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, -) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util from .const import ( ATTR_API_CONDITION, + ATTR_API_FORECAST_CONDITION, ATTR_API_FORECAST_DAILY, ATTR_API_FORECAST_HOURLY, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ATTR_API_HUMIDITY, ATTR_API_PRESSURE, ATTR_API_RAIN, @@ -402,15 +400,15 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): return None return { - ATTR_FORECAST_CONDITION: condition, - ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day( + ATTR_API_FORECAST_CONDITION: condition, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: self._get_precipitation_prob_day( day ), - ATTR_FORECAST_TEMP: self._get_temperature_day(day), - ATTR_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), - ATTR_FORECAST_TIME: dt_util.as_utc(date).isoformat(), - ATTR_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), - ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), + ATTR_API_FORECAST_TEMP: self._get_temperature_day(day), + ATTR_API_FORECAST_TEMP_LOW: self._get_temperature_low_day(day), + ATTR_API_FORECAST_TIME: dt_util.as_utc(date).isoformat(), + ATTR_API_FORECAST_WIND_SPEED: self._get_wind_speed_day(day), + ATTR_API_FORECAST_WIND_BEARING: self._get_wind_bearing_day(day), } def _convert_forecast_hour(self, date, day, hour): @@ -420,15 +418,15 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): forecast_dt = date.replace(hour=hour, minute=0, second=0) return { - ATTR_FORECAST_CONDITION: condition, - ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour), - ATTR_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob( + ATTR_API_FORECAST_CONDITION: condition, + ATTR_API_FORECAST_PRECIPITATION: self._calc_precipitation(day, hour), + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: self._calc_precipitation_prob( day, hour ), - ATTR_FORECAST_TEMP: self._get_temperature(day, hour), - ATTR_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), - ATTR_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), - ATTR_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), + ATTR_API_FORECAST_TEMP: self._get_temperature(day, hour), + ATTR_API_FORECAST_TIME: dt_util.as_utc(forecast_dt).isoformat(), + ATTR_API_FORECAST_WIND_SPEED: self._get_wind_speed(day, hour), + ATTR_API_FORECAST_WIND_BEARING: self._get_wind_bearing(day, hour), } def _calc_precipitation(self, day, hour): From b277c28ed7227ce8981534e44882de4a48a1f005 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 6 Jul 2022 13:35:25 +0200 Subject: [PATCH 2207/3516] Bump aioslimproto to 2.1.1 (#74499) --- homeassistant/components/slimproto/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/slimproto/manifest.json b/homeassistant/components/slimproto/manifest.json index 23b3198d7e4..1e076046b44 100644 --- a/homeassistant/components/slimproto/manifest.json +++ b/homeassistant/components/slimproto/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "iot_class": "local_push", "documentation": "https://www.home-assistant.io/integrations/slimproto", - "requirements": ["aioslimproto==2.0.1"], + "requirements": ["aioslimproto==2.1.1"], "codeowners": ["@marcelveldt"], "after_dependencies": ["media_source"] } diff --git a/requirements_all.txt b/requirements_all.txt index b0171ae3ee2..9e6a6aa2b0a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -250,7 +250,7 @@ aioshelly==2.0.0 aioskybell==22.6.1 # homeassistant.components.slimproto -aioslimproto==2.0.1 +aioslimproto==2.1.1 # homeassistant.components.steamist aiosteamist==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd0800310a0..030ca8ab7ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -219,7 +219,7 @@ aioshelly==2.0.0 aioskybell==22.6.1 # homeassistant.components.slimproto -aioslimproto==2.0.1 +aioslimproto==2.1.1 # homeassistant.components.steamist aiosteamist==0.3.2 From 519d15428c488f9ac81d282b2216e0f24dc3e778 Mon Sep 17 00:00:00 2001 From: Gyosa3 <51777889+Gyosa3@users.noreply.github.com> Date: Wed, 6 Jul 2022 17:48:12 +0200 Subject: [PATCH 2208/3516] Add new alias for valid Celcius temperature units in Tuya (#74511) --- 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 8a3e59b1ac9..727e505200b 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -579,7 +579,7 @@ UNITS = ( ), UnitOfMeasurement( unit=TEMP_CELSIUS, - aliases={"°c", "c", "celsius"}, + aliases={"°c", "c", "celsius", "℃"}, device_classes={SensorDeviceClass.TEMPERATURE}, ), UnitOfMeasurement( From 9d3dde60ff1dd6eb4e67798de49f66099ebf95d5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Jul 2022 17:49:06 +0200 Subject: [PATCH 2209/3516] Fix openweathermap forecast sensors (#74513) --- .../components/openweathermap/const.py | 30 ++++++------- .../components/openweathermap/weather.py | 41 +++++++++++++++++- .../weather_update_coordinator.py | 42 +++++++++---------- 3 files changed, 74 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 06f13daa9c2..836a56c70b2 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -21,9 +21,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_CONDITION_WINDY, ATTR_CONDITION_WINDY_VARIANT, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TIME, ) from homeassistant.const import ( DEGREE, @@ -68,10 +65,15 @@ ATTR_API_FORECAST = "forecast" UPDATE_LISTENER = "update_listener" PLATFORMS = [Platform.SENSOR, Platform.WEATHER] -ATTR_FORECAST_PRECIPITATION = "precipitation" -ATTR_FORECAST_PRESSURE = "pressure" -ATTR_FORECAST_TEMP = "temperature" -ATTR_FORECAST_TEMP_LOW = "templow" +ATTR_API_FORECAST_CONDITION = "condition" +ATTR_API_FORECAST_PRECIPITATION = "precipitation" +ATTR_API_FORECAST_PRECIPITATION_PROBABILITY = "precipitation_probability" +ATTR_API_FORECAST_PRESSURE = "pressure" +ATTR_API_FORECAST_TEMP = "temperature" +ATTR_API_FORECAST_TEMP_LOW = "templow" +ATTR_API_FORECAST_TIME = "datetime" +ATTR_API_FORECAST_WIND_BEARING = "wind_bearing" +ATTR_API_FORECAST_WIND_SPEED = "wind_speed" FORECAST_MODE_HOURLY = "hourly" FORECAST_MODE_DAILY = "daily" @@ -263,39 +265,39 @@ WEATHER_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ) FORECAST_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( - key=ATTR_FORECAST_CONDITION, + key=ATTR_API_FORECAST_CONDITION, name="Condition", ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION, + key=ATTR_API_FORECAST_PRECIPITATION, name="Precipitation", native_unit_of_measurement=LENGTH_MILLIMETERS, ), SensorEntityDescription( - key=ATTR_FORECAST_PRECIPITATION_PROBABILITY, + key=ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, name="Precipitation probability", native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( - key=ATTR_FORECAST_PRESSURE, + key=ATTR_API_FORECAST_PRESSURE, name="Pressure", native_unit_of_measurement=PRESSURE_HPA, device_class=SensorDeviceClass.PRESSURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP, + key=ATTR_API_FORECAST_TEMP, name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TEMP_LOW, + key=ATTR_API_FORECAST_TEMP_LOW, name="Temperature Low", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), SensorEntityDescription( - key=ATTR_FORECAST_TIME, + key=ATTR_API_FORECAST_TIME, name="Time", device_class=SensorDeviceClass.TIMESTAMP, ), diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index fce6efdf3c5..ea439a35586 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -1,7 +1,20 @@ """Support for the OpenWeatherMap (OWM) service.""" from __future__ import annotations -from homeassistant.components.weather import Forecast, WeatherEntity +from typing import cast + +from homeassistant.components.weather import ( + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, + ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, + Forecast, + WeatherEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( LENGTH_MILLIMETERS, @@ -17,6 +30,14 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ATTR_API_CONDITION, ATTR_API_FORECAST, + ATTR_API_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ATTR_API_HUMIDITY, ATTR_API_PRESSURE, ATTR_API_TEMPERATURE, @@ -31,6 +52,17 @@ from .const import ( ) from .weather_update_coordinator import WeatherUpdateCoordinator +FORECAST_MAP = { + ATTR_API_FORECAST_CONDITION: ATTR_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION: ATTR_FORECAST_NATIVE_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ATTR_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_TEMP_LOW: ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_API_FORECAST_TEMP: ATTR_FORECAST_NATIVE_TEMP, + ATTR_API_FORECAST_TIME: ATTR_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING: ATTR_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED: ATTR_FORECAST_NATIVE_WIND_SPEED, +} + async def async_setup_entry( hass: HomeAssistant, @@ -109,7 +141,12 @@ class OpenWeatherMapWeather(WeatherEntity): @property def forecast(self) -> list[Forecast] | None: """Return the forecast array.""" - return self._weather_coordinator.data[ATTR_API_FORECAST] + api_forecasts = self._weather_coordinator.data[ATTR_API_FORECAST] + forecasts = [ + {ha_key: forecast[api_key] for api_key, ha_key in FORECAST_MAP.items()} + for forecast in api_forecasts + ] + return cast(list[Forecast], forecasts) @property def available(self) -> bool: diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index 36511424737..98c3b56fb5e 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -8,15 +8,6 @@ from pyowm.commons.exceptions import APIRequestError, UnauthorizedError from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_NATIVE_PRECIPITATION, - ATTR_FORECAST_NATIVE_PRESSURE, - ATTR_FORECAST_NATIVE_TEMP, - ATTR_FORECAST_NATIVE_TEMP_LOW, - ATTR_FORECAST_NATIVE_WIND_SPEED, - ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, ) from homeassistant.helpers import sun from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -29,6 +20,15 @@ from .const import ( ATTR_API_DEW_POINT, ATTR_API_FEELS_LIKE_TEMPERATURE, ATTR_API_FORECAST, + ATTR_API_FORECAST_CONDITION, + ATTR_API_FORECAST_PRECIPITATION, + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY, + ATTR_API_FORECAST_PRESSURE, + ATTR_API_FORECAST_TEMP, + ATTR_API_FORECAST_TEMP_LOW, + ATTR_API_FORECAST_TIME, + ATTR_API_FORECAST_WIND_BEARING, + ATTR_API_FORECAST_WIND_SPEED, ATTR_API_HUMIDITY, ATTR_API_PRECIPITATION_KIND, ATTR_API_PRESSURE, @@ -158,19 +158,19 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): def _convert_forecast(self, entry): """Convert the forecast data.""" forecast = { - ATTR_FORECAST_TIME: dt.utc_from_timestamp( + ATTR_API_FORECAST_TIME: dt.utc_from_timestamp( entry.reference_time("unix") ).isoformat(), - ATTR_FORECAST_NATIVE_PRECIPITATION: self._calc_precipitation( + ATTR_API_FORECAST_PRECIPITATION: self._calc_precipitation( entry.rain, entry.snow ), - ATTR_FORECAST_PRECIPITATION_PROBABILITY: ( + ATTR_API_FORECAST_PRECIPITATION_PROBABILITY: ( round(entry.precipitation_probability * 100) ), - ATTR_FORECAST_NATIVE_PRESSURE: entry.pressure.get("press"), - ATTR_FORECAST_NATIVE_WIND_SPEED: entry.wind().get("speed"), - ATTR_FORECAST_WIND_BEARING: entry.wind().get("deg"), - ATTR_FORECAST_CONDITION: self._get_condition( + ATTR_API_FORECAST_PRESSURE: entry.pressure.get("press"), + ATTR_API_FORECAST_WIND_SPEED: entry.wind().get("speed"), + ATTR_API_FORECAST_WIND_BEARING: entry.wind().get("deg"), + ATTR_API_FORECAST_CONDITION: self._get_condition( entry.weather_code, entry.reference_time("unix") ), ATTR_API_CLOUDS: entry.clouds, @@ -178,16 +178,12 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): temperature_dict = entry.temperature("celsius") if "max" in temperature_dict and "min" in temperature_dict: - forecast[ATTR_FORECAST_NATIVE_TEMP] = entry.temperature("celsius").get( - "max" - ) - forecast[ATTR_FORECAST_NATIVE_TEMP_LOW] = entry.temperature("celsius").get( + forecast[ATTR_API_FORECAST_TEMP] = entry.temperature("celsius").get("max") + forecast[ATTR_API_FORECAST_TEMP_LOW] = entry.temperature("celsius").get( "min" ) else: - forecast[ATTR_FORECAST_NATIVE_TEMP] = entry.temperature("celsius").get( - "temp" - ) + forecast[ATTR_API_FORECAST_TEMP] = entry.temperature("celsius").get("temp") return forecast From 380244fa7b67e8cccdaa5ca2cc18012d424e851c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Jul 2022 18:34:51 +0200 Subject: [PATCH 2210/3516] Update homematicip to 1.0.3 (#74516) --- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index b13c8ca19b2..40f7e67fd07 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==1.0.2"], + "requirements": ["homematicip==1.0.3"], "codeowners": [], "quality_scale": "platinum", "iot_class": "cloud_push", diff --git a/requirements_all.txt b/requirements_all.txt index 9e6a6aa2b0a..769235b30a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -834,7 +834,7 @@ home-assistant-frontend==20220705.0 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.2 +homematicip==1.0.3 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 030ca8ab7ec..44daeef21fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -601,7 +601,7 @@ home-assistant-frontend==20220705.0 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.2 +homematicip==1.0.3 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 From 8e5b6ff185872484767f25247163847406e0c244 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Jul 2022 19:31:57 +0200 Subject: [PATCH 2211/3516] Update Home Assistant Frontend to 20220706.0 (#74520) Bump Home Assistant Frontend to 20220706.0 --- 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 6b378fe1098..85ead380485 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220705.0"], + "requirements": ["home-assistant-frontend==20220706.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 00f5c776712..c291d969219 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220705.0 +home-assistant-frontend==20220706.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 769235b30a1..99be85cc418 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220705.0 +home-assistant-frontend==20220706.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44daeef21fd..b6a493d37f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -595,7 +595,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220705.0 +home-assistant-frontend==20220706.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 06c6ddb2d63cb41ff075e09919a699e9adf472ec Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 6 Jul 2022 19:33:46 +0200 Subject: [PATCH 2212/3516] Bumped version to 2022.7.0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 69ebbacb0e2..c2ee7de691f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 61ff9ba39aa..b4994d55edb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.0b5" +version = "2022.7.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 332cf3cd2d674bcf27c7d381f575d69f440355c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 6 Jul 2022 13:49:48 -0500 Subject: [PATCH 2213/3516] Resolve and caches paths for CachingStaticResource in the executor (#74474) --- homeassistant/components/http/static.py | 48 +++++++++++++------ .../components/recorder/manifest.json | 2 +- pyproject.toml | 1 + requirements.txt | 1 + requirements_all.txt | 3 -- requirements_test_all.txt | 3 -- tests/components/frontend/test_init.py | 32 +++++++++++++ 7 files changed, 69 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index 112549553eb..e5e84ca141d 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -9,11 +9,31 @@ from aiohttp import hdrs from aiohttp.web import FileResponse, Request, StreamResponse from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound from aiohttp.web_urldispatcher import StaticResource +from lru import LRU # pylint: disable=no-name-in-module + +from homeassistant.core import HomeAssistant + +from .const import KEY_HASS CACHE_TIME: Final = 31 * 86400 # = 1 month CACHE_HEADERS: Final[Mapping[str, str]] = { hdrs.CACHE_CONTROL: f"public, max-age={CACHE_TIME}" } +PATH_CACHE = LRU(512) + + +def _get_file_path( + filename: str, directory: Path, follow_symlinks: bool +) -> Path | None: + filepath = directory.joinpath(filename).resolve() + if not follow_symlinks: + filepath.relative_to(directory) + # on opening a dir, load its contents if allowed + if filepath.is_dir(): + return None + if filepath.is_file(): + return filepath + raise HTTPNotFound class CachingStaticResource(StaticResource): @@ -21,16 +41,19 @@ class CachingStaticResource(StaticResource): async def _handle(self, request: Request) -> StreamResponse: rel_url = request.match_info["filename"] + hass: HomeAssistant = request.app[KEY_HASS] + filename = Path(rel_url) + if filename.anchor: + # rel_url is an absolute name like + # /static/\\machine_name\c$ or /static/D:\path + # where the static dir is totally different + raise HTTPForbidden() try: - filename = Path(rel_url) - if filename.anchor: - # rel_url is an absolute name like - # /static/\\machine_name\c$ or /static/D:\path - # where the static dir is totally different - raise HTTPForbidden() - filepath = self._directory.joinpath(filename).resolve() - if not self._follow_symlinks: - filepath.relative_to(self._directory) + key = (filename, self._directory, self._follow_symlinks) + if (filepath := PATH_CACHE.get(key)) is None: + filepath = PATH_CACHE[key] = await hass.async_add_executor_job( + _get_file_path, filename, self._directory, self._follow_symlinks + ) except (ValueError, FileNotFoundError) as error: # relatively safe raise HTTPNotFound() from error @@ -39,13 +62,10 @@ class CachingStaticResource(StaticResource): request.app.logger.exception(error) raise HTTPNotFound() from error - # on opening a dir, load its contents if allowed - if filepath.is_dir(): - return await super()._handle(request) - if filepath.is_file(): + if filepath: return FileResponse( filepath, chunk_size=self._chunk_size, headers=CACHE_HEADERS, ) - raise HTTPNotFound + return await super()._handle(request) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 3d22781906a..f0c1f81689a 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.38", "fnvhash==0.1.0", "lru-dict==1.1.7"], + "requirements": ["sqlalchemy==1.4.38", "fnvhash==0.1.0"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/pyproject.toml b/pyproject.toml index 51f1d115e7f..59ad04c4b11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ dependencies = [ "httpx==0.23.0", "ifaddr==0.1.7", "jinja2==3.1.2", + "lru-dict==1.1.7", "PyJWT==2.4.0", # PyJWT has loose dependency. We want the latest one. "cryptography==36.0.2", diff --git a/requirements.txt b/requirements.txt index 98b148fa923..a8ec77333e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,7 @@ ciso8601==2.2.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 +lru-dict==1.1.7 PyJWT==2.4.0 cryptography==36.0.2 orjson==3.7.5 diff --git a/requirements_all.txt b/requirements_all.txt index b63bb91e20f..f4895707f06 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -974,9 +974,6 @@ logi_circle==0.2.3 # homeassistant.components.london_underground london-tube-status==0.5 -# homeassistant.components.recorder -lru-dict==1.1.7 - # homeassistant.components.luftdaten luftdaten==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 876944367d9..d5c616edf2e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -681,9 +681,6 @@ life360==4.1.1 # homeassistant.components.logi_circle logi_circle==0.2.3 -# homeassistant.components.recorder -lru-dict==1.1.7 - # homeassistant.components.luftdaten luftdaten==0.7.2 diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 84ca04df3ba..661b3ace38a 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -578,3 +578,35 @@ async def test_manifest_json(hass, frontend_themes, mock_http_client): json = await resp.json() assert json["theme_color"] != DEFAULT_THEME_COLOR + + +async def test_static_path_cache(hass, mock_http_client): + """Test static paths cache.""" + resp = await mock_http_client.get("/lovelace/default_view", allow_redirects=False) + assert resp.status == 404 + + resp = await mock_http_client.get("/frontend_latest/", allow_redirects=False) + assert resp.status == 403 + + resp = await mock_http_client.get( + "/static/icons/favicon.ico", allow_redirects=False + ) + assert resp.status == 200 + + # and again to make sure the cache works + resp = await mock_http_client.get( + "/static/icons/favicon.ico", allow_redirects=False + ) + assert resp.status == 200 + + resp = await mock_http_client.get( + "/static/fonts/roboto/Roboto-Bold.woff2", allow_redirects=False + ) + assert resp.status == 200 + + resp = await mock_http_client.get("/static/does-not-exist", allow_redirects=False) + assert resp.status == 404 + + # and again to make sure the cache works + resp = await mock_http_client.get("/static/does-not-exist", allow_redirects=False) + assert resp.status == 404 From 5e63a44e710e128f7ff55763676d323f16ee76bf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 6 Jul 2022 21:45:54 +0200 Subject: [PATCH 2214/3516] Remove home_plus_control from mypy ignore list (#74448) --- homeassistant/components/home_plus_control/__init__.py | 3 ++- homeassistant/components/home_plus_control/api.py | 3 +++ mypy.ini | 6 ------ script/hassfest/mypy_config.py | 2 -- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index 78e31c83caa..e222b65e293 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -83,7 +83,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api = hass_entry_data[API] = HomePlusControlAsyncApi(hass, entry, implementation) # Set of entity unique identifiers of this integration - uids = hass_entry_data[ENTITY_UIDS] = set() + uids: set[str] = set() + hass_entry_data[ENTITY_UIDS] = uids # Integration dispatchers hass_entry_data[DISPATCHER_REMOVERS] = [] diff --git a/homeassistant/components/home_plus_control/api.py b/homeassistant/components/home_plus_control/api.py index d9db95323de..9f092b28920 100644 --- a/homeassistant/components/home_plus_control/api.py +++ b/homeassistant/components/home_plus_control/api.py @@ -5,6 +5,7 @@ from homeassistant import config_entries, core from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow from .const import DEFAULT_UPDATE_INTERVALS +from .helpers import HomePlusControlOAuth2Implementation class HomePlusControlAsyncApi(HomePlusControlAPI): @@ -40,6 +41,8 @@ class HomePlusControlAsyncApi(HomePlusControlAPI): hass, config_entry, implementation ) + assert isinstance(implementation, HomePlusControlOAuth2Implementation) + # Create the API authenticated client - external library super().__init__( subscription_key=implementation.subscription_key, diff --git a/mypy.ini b/mypy.ini index 38cb7d4c7f0..f29bcf92720 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2695,12 +2695,6 @@ ignore_errors = true [mypy-homeassistant.components.hassio.websocket_api] ignore_errors = true -[mypy-homeassistant.components.home_plus_control] -ignore_errors = true - -[mypy-homeassistant.components.home_plus_control.api] -ignore_errors = true - [mypy-homeassistant.components.icloud] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index d540ed61cd8..f058cfd391f 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -40,8 +40,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.hassio.sensor", "homeassistant.components.hassio.system_health", "homeassistant.components.hassio.websocket_api", - "homeassistant.components.home_plus_control", - "homeassistant.components.home_plus_control.api", "homeassistant.components.icloud", "homeassistant.components.icloud.account", "homeassistant.components.icloud.device_tracker", From b071affcb4251190884257ba96e7f737920ed5e4 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 7 Jul 2022 00:31:47 +0200 Subject: [PATCH 2215/3516] Use pydeconz interface controls for cover platform (#74535) --- homeassistant/components/deconz/cover.py | 52 ++++++++--- tests/components/deconz/test_cover.py | 105 +++-------------------- 2 files changed, 50 insertions(+), 107 deletions(-) diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 9efbeac366f..3e56882f15a 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any, cast +from pydeconz.interfaces.lights import CoverAction from pydeconz.models.event import EventType from pydeconz.models.light.cover import Cover @@ -21,7 +22,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry -DEVICE_CLASS = { +DECONZ_TYPE_TO_DEVICE_CLASS = { "Level controllable output": CoverDeviceClass.DAMPER, "Window covering controller": CoverDeviceClass.SHADE, "Window covering device": CoverDeviceClass.SHADE, @@ -40,8 +41,7 @@ async def async_setup_entry( @callback def async_add_cover(_: EventType, cover_id: str) -> None: """Add cover from deCONZ.""" - cover = gateway.api.lights.covers[cover_id] - async_add_entities([DeconzCover(cover, gateway)]) + async_add_entities([DeconzCover(cover_id, gateway)]) gateway.register_platform_add_device_callback( async_add_cover, @@ -55,9 +55,9 @@ class DeconzCover(DeconzDevice, CoverEntity): TYPE = DOMAIN _device: Cover - def __init__(self, device: Cover, gateway: DeconzGateway) -> None: + def __init__(self, cover_id: str, gateway: DeconzGateway) -> None: """Set up cover device.""" - super().__init__(device, gateway) + super().__init__(cover := gateway.api.lights.covers[cover_id], gateway) self._attr_supported_features = CoverEntityFeature.OPEN self._attr_supported_features |= CoverEntityFeature.CLOSE @@ -70,7 +70,7 @@ class DeconzCover(DeconzDevice, CoverEntity): self._attr_supported_features |= CoverEntityFeature.STOP_TILT self._attr_supported_features |= CoverEntityFeature.SET_TILT_POSITION - self._attr_device_class = DEVICE_CLASS.get(self._device.type) + self._attr_device_class = DECONZ_TYPE_TO_DEVICE_CLASS.get(cover.type) @property def current_cover_position(self) -> int: @@ -85,19 +85,31 @@ class DeconzCover(DeconzDevice, CoverEntity): async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" position = 100 - cast(int, kwargs[ATTR_POSITION]) - await self._device.set_position(lift=position) + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + lift=position, + ) async def async_open_cover(self, **kwargs: Any) -> None: """Open cover.""" - await self._device.open() + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + action=CoverAction.OPEN, + ) async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" - await self._device.close() + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + action=CoverAction.CLOSE, + ) async def async_stop_cover(self, **kwargs: Any) -> None: """Stop cover.""" - await self._device.stop() + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + action=CoverAction.STOP, + ) @property def current_cover_tilt_position(self) -> int | None: @@ -109,16 +121,28 @@ class DeconzCover(DeconzDevice, CoverEntity): async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Tilt the cover to a specific position.""" position = 100 - cast(int, kwargs[ATTR_TILT_POSITION]) - await self._device.set_position(tilt=position) + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + tilt=position, + ) async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open cover tilt.""" - await self._device.set_position(tilt=0) + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + tilt=0, + ) async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close cover tilt.""" - await self._device.set_position(tilt=100) + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + tilt=100, + ) async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop cover tilt.""" - await self._device.stop() + await self.gateway.api.lights.covers.set_state( + id=self._device.resource_id, + action=CoverAction.STOP, + ) diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index c252b00a228..0c37edc221d 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -42,68 +42,48 @@ async def test_cover(hass, aioclient_mock, mock_deconz_websocket): data = { "lights": { "1": { - "name": "Level controllable cover", - "type": "Level controllable output", - "state": {"bri": 254, "on": False, "reachable": True}, - "modelid": "Not zigbee spec", - "uniqueid": "00:00:00:00:00:00:00:00-00", - }, - "2": { "name": "Window covering device", "type": "Window covering device", "state": {"lift": 100, "open": False, "reachable": True}, "modelid": "lumi.curtain", "uniqueid": "00:00:00:00:00:00:00:01-00", }, - "3": { + "2": { "name": "Unsupported cover", "type": "Not a cover", "state": {"reachable": True}, "uniqueid": "00:00:00:00:00:00:00:02-00", }, - "4": { - "name": "deconz old brightness cover", - "type": "Level controllable output", - "state": {"bri": 255, "on": False, "reachable": True}, - "modelid": "Not zigbee spec", - "uniqueid": "00:00:00:00:00:00:00:03-00", - }, - "5": { - "name": "Window covering controller", - "type": "Window covering controller", - "state": {"bri": 253, "on": True, "reachable": True}, - "modelid": "Motor controller", - "uniqueid": "00:00:00:00:00:00:00:04-00", - }, } } with patch.dict(DECONZ_WEB_REQUEST, data): config_entry = await setup_deconz_integration(hass, aioclient_mock) - assert len(hass.states.async_all()) == 5 - assert hass.states.get("cover.level_controllable_cover").state == STATE_OPEN - assert hass.states.get("cover.window_covering_device").state == STATE_CLOSED + assert len(hass.states.async_all()) == 2 + cover = hass.states.get("cover.window_covering_device") + assert cover.state == STATE_CLOSED + assert cover.attributes[ATTR_CURRENT_POSITION] == 0 assert not hass.states.get("cover.unsupported_cover") - assert hass.states.get("cover.deconz_old_brightness_cover").state == STATE_OPEN - assert hass.states.get("cover.window_covering_controller").state == STATE_CLOSED - # Event signals cover is closed + # Event signals cover is open event_changed_light = { "t": "event", "e": "changed", "r": "lights", "id": "1", - "state": {"on": True}, + "state": {"lift": 0, "open": True}, } await mock_deconz_websocket(data=event_changed_light) await hass.async_block_till_done() - assert hass.states.get("cover.level_controllable_cover").state == STATE_CLOSED + cover = hass.states.get("cover.window_covering_device") + assert cover.state == STATE_OPEN + assert cover.attributes[ATTR_CURRENT_POSITION] == 100 # Verify service calls for cover - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/2/state") + mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") # Service open cover @@ -145,71 +125,10 @@ async def test_cover(hass, aioclient_mock, mock_deconz_websocket): ) assert aioclient_mock.mock_calls[4][2] == {"stop": True} - # Verify service calls for legacy cover - - mock_deconz_put_request(aioclient_mock, config_entry.data, "/lights/1/state") - - # Service open cover - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_OPEN_COVER, - {ATTR_ENTITY_ID: "cover.level_controllable_cover"}, - blocking=True, - ) - assert aioclient_mock.mock_calls[5][2] == {"on": False} - - # Service close cover - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_CLOSE_COVER, - {ATTR_ENTITY_ID: "cover.level_controllable_cover"}, - blocking=True, - ) - assert aioclient_mock.mock_calls[6][2] == {"on": True} - - # Service set cover position - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_SET_COVER_POSITION, - {ATTR_ENTITY_ID: "cover.level_controllable_cover", ATTR_POSITION: 40}, - blocking=True, - ) - assert aioclient_mock.mock_calls[7][2] == {"bri": 152} - - # Service stop cover movement - - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_STOP_COVER, - {ATTR_ENTITY_ID: "cover.level_controllable_cover"}, - blocking=True, - ) - assert aioclient_mock.mock_calls[8][2] == {"bri_inc": 0} - - # Test that a reported cover position of 255 (deconz-rest-api < 2.05.73) is interpreted correctly. - assert hass.states.get("cover.deconz_old_brightness_cover").state == STATE_OPEN - - event_changed_light = { - "t": "event", - "e": "changed", - "r": "lights", - "id": "4", - "state": {"on": True}, - } - await mock_deconz_websocket(data=event_changed_light) - await hass.async_block_till_done() - - deconz_old_brightness_cover = hass.states.get("cover.deconz_old_brightness_cover") - assert deconz_old_brightness_cover.state == STATE_CLOSED - assert deconz_old_brightness_cover.attributes[ATTR_CURRENT_POSITION] == 0 - await hass.config_entries.async_unload(config_entry.entry_id) states = hass.states.async_all() - assert len(states) == 5 + assert len(states) == 2 for state in states: assert state.state == STATE_UNAVAILABLE From 099e7e0637229e127e026ce7288d2d0ce100d29d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 6 Jul 2022 18:20:29 -0500 Subject: [PATCH 2216/3516] Add oui for tplink es20m (#74526) --- homeassistant/components/tplink/manifest.json | 4 ++++ homeassistant/generated/dhcp.py | 1 + 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index e9cc687cc02..dfb873564c5 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -10,6 +10,10 @@ "iot_class": "local_polling", "dhcp": [ { "registered_devices": true }, + { + "hostname": "es*", + "macaddress": "54AF97*" + }, { "hostname": "ep*", "macaddress": "E848B8*" diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index e9cf6ca4c06..57be9a62138 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -128,6 +128,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'tolo', 'hostname': 'usr-tcp232-ed2'}, {'domain': 'toon', 'hostname': 'eneco-*', 'macaddress': '74C63B*'}, {'domain': 'tplink', 'registered_devices': True}, + {'domain': 'tplink', 'hostname': 'es*', 'macaddress': '54AF97*'}, {'domain': 'tplink', 'hostname': 'ep*', 'macaddress': 'E848B8*'}, {'domain': 'tplink', 'hostname': 'ep*', 'macaddress': '003192*'}, {'domain': 'tplink', 'hostname': 'hs*', 'macaddress': '1C3BF3*'}, From fef78939e16d53e3fa2e14e77f56c51d89dec114 Mon Sep 17 00:00:00 2001 From: c-soft Date: Thu, 7 Jul 2022 02:02:08 +0200 Subject: [PATCH 2217/3516] Bump satel_integra to 0.3.7 to fix compat with python 3.10 (#74543) --- homeassistant/components/satel_integra/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/satel_integra/manifest.json b/homeassistant/components/satel_integra/manifest.json index 6c4a391698b..262507be6bb 100644 --- a/homeassistant/components/satel_integra/manifest.json +++ b/homeassistant/components/satel_integra/manifest.json @@ -2,7 +2,7 @@ "domain": "satel_integra", "name": "Satel Integra", "documentation": "https://www.home-assistant.io/integrations/satel_integra", - "requirements": ["satel_integra==0.3.4"], + "requirements": ["satel_integra==0.3.7"], "codeowners": [], "iot_class": "local_push", "loggers": ["satel_integra"] diff --git a/requirements_all.txt b/requirements_all.txt index f4895707f06..17b8c5d7e42 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2125,7 +2125,7 @@ samsungctl[websocket]==0.7.1 samsungtvws[async,encrypted]==2.5.0 # homeassistant.components.satel_integra -satel_integra==0.3.4 +satel_integra==0.3.7 # homeassistant.components.dhcp scapy==2.4.5 From 235abb0c10bfe994f1088691472eb07ee4247228 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 7 Jul 2022 00:27:55 +0000 Subject: [PATCH 2218/3516] [ci skip] Translation update --- .../components/anthemav/translations/id.json | 14 ++++++++++++ .../components/apple_tv/translations/hu.json | 2 +- .../components/awair/translations/id.json | 3 +++ .../components/brunt/translations/hu.json | 2 +- .../components/esphome/translations/hu.json | 4 ++-- .../components/generic/translations/id.json | 2 ++ .../homeassistant/translations/id.json | 1 + .../humidifier/translations/hu.json | 2 +- .../components/isy994/translations/hu.json | 2 +- .../lg_soundbar/translations/id.json | 14 ++++++++++++ .../components/life360/translations/id.json | 19 +++++++++++++++- .../components/nextdns/translations/ca.json | 5 +++++ .../components/nextdns/translations/el.json | 5 +++++ .../components/nextdns/translations/en.json | 5 +++++ .../components/nextdns/translations/fr.json | 5 +++++ .../components/nextdns/translations/hu.json | 5 +++++ .../components/nextdns/translations/id.json | 22 +++++++++++++++++++ .../components/nextdns/translations/pl.json | 5 +++++ .../nextdns/translations/pt-BR.json | 5 +++++ .../nextdns/translations/zh-Hant.json | 5 +++++ .../components/nina/translations/id.json | 17 ++++++++++++++ .../components/rfxtrx/translations/hu.json | 2 +- .../soundtouch/translations/id.json | 16 ++++++++++++++ .../unifiprotect/translations/hu.json | 2 +- 24 files changed, 155 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/anthemav/translations/id.json create mode 100644 homeassistant/components/lg_soundbar/translations/id.json create mode 100644 homeassistant/components/nextdns/translations/id.json create mode 100644 homeassistant/components/soundtouch/translations/id.json diff --git a/homeassistant/components/anthemav/translations/id.json b/homeassistant/components/anthemav/translations/id.json new file mode 100644 index 00000000000..d6bac04f480 --- /dev/null +++ b/homeassistant/components/anthemav/translations/id.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/hu.json b/homeassistant/components/apple_tv/translations/hu.json index 4c3a8cdee94..9105c545758 100644 --- a/homeassistant/components/apple_tv/translations/hu.json +++ b/homeassistant/components/apple_tv/translations/hu.json @@ -41,7 +41,7 @@ "title": "Jelsz\u00f3 sz\u00fcks\u00e9ges" }, "protocol_disabled": { - "description": "P\u00e1ros\u00edt\u00e1s sz\u00fcks\u00e9ges a `{protokoll}` miatt, de az eszk\u00f6z\u00f6n le van tiltva. K\u00e9rj\u00fck, vizsg\u00e1lja meg az eszk\u00f6z\u00f6n az esetleges hozz\u00e1f\u00e9r\u00e9si korl\u00e1toz\u00e1sokat (pl. enged\u00e9lyezze a helyi h\u00e1l\u00f3zaton l\u00e9v\u0151 \u00f6sszes eszk\u00f6z csatlakoztat\u00e1s\u00e1t).\n\nFolytathatja a protokoll p\u00e1ros\u00edt\u00e1sa n\u00e9lk\u00fcl is, de bizonyos funkci\u00f3k korl\u00e1tozottak lesznek.", + "description": "P\u00e1ros\u00edt\u00e1s sz\u00fcks\u00e9ges a `{protocol}` miatt, de az eszk\u00f6z\u00f6n le van tiltva. K\u00e9rj\u00fck, vizsg\u00e1lja meg az eszk\u00f6z\u00f6n az esetleges hozz\u00e1f\u00e9r\u00e9si korl\u00e1toz\u00e1sokat (pl. enged\u00e9lyezze a helyi h\u00e1l\u00f3zaton l\u00e9v\u0151 \u00f6sszes eszk\u00f6z csatlakoztat\u00e1s\u00e1t).\n\nFolytathatja a protokoll p\u00e1ros\u00edt\u00e1sa n\u00e9lk\u00fcl is, de bizonyos funkci\u00f3k korl\u00e1tozottak lesznek.", "title": "A p\u00e1ros\u00edt\u00e1s nem lehets\u00e9ges" }, "reconfigure": { diff --git a/homeassistant/components/awair/translations/id.json b/homeassistant/components/awair/translations/id.json index 2c6fab90909..ab9431a6a46 100644 --- a/homeassistant/components/awair/translations/id.json +++ b/homeassistant/components/awair/translations/id.json @@ -17,6 +17,9 @@ }, "description": "Masukkan kembali token akses pengembang Awair Anda." }, + "reauth_confirm": { + "description": "Masukkan kembali token akses pengembang Awair Anda." + }, "user": { "data": { "access_token": "Token Akses", diff --git a/homeassistant/components/brunt/translations/hu.json b/homeassistant/components/brunt/translations/hu.json index 3abb5cbf297..bc7be29ba1b 100644 --- a/homeassistant/components/brunt/translations/hu.json +++ b/homeassistant/components/brunt/translations/hu.json @@ -14,7 +14,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "K\u00e9rem, adja meg \u00fajra a jelsz\u00f3t: {felhaszn\u00e1l\u00f3n\u00e9v}", + "description": "K\u00e9rem, adja meg \u00fajra a jelsz\u00f3t: {username}", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "user": { diff --git a/homeassistant/components/esphome/translations/hu.json b/homeassistant/components/esphome/translations/hu.json index e8e6c9b2dc2..3eaf276b309 100644 --- a/homeassistant/components/esphome/translations/hu.json +++ b/homeassistant/components/esphome/translations/hu.json @@ -9,7 +9,7 @@ "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" + "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." }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "C\u00edm", "port": "Port" }, - "description": "K\u00e9rem, adja meg az [ESPHome](https://esphomelib.com/) csom\u00f3pontj\u00e1nak kapcsol\u00f3d\u00e1si be\u00e1ll\u00edt\u00e1sait." + "description": "K\u00e9rem, adja meg az [ESPHome]({esphome_url}) csom\u00f3pontj\u00e1nak kapcsol\u00f3d\u00e1si be\u00e1ll\u00edt\u00e1sait." } } } diff --git a/homeassistant/components/generic/translations/id.json b/homeassistant/components/generic/translations/id.json index a9c553580ca..77ea5f01f0b 100644 --- a/homeassistant/components/generic/translations/id.json +++ b/homeassistant/components/generic/translations/id.json @@ -7,6 +7,7 @@ "error": { "already_exists": "Kamera dengan setelan URL ini sudah ada.", "invalid_still_image": "URL tidak mengembalikan gambar diam yang valid", + "malformed_url": "URL salah format", "no_still_image_or_stream_url": "Anda harus menentukan setidaknya gambar diam atau URL streaming", "stream_file_not_found": "File tidak ditemukan saat mencoba menyambung ke streaming (sudahkah ffmpeg diinstal?)", "stream_http_not_found": "HTTP 404 Tidak ditemukan saat mencoba menyambung ke streaming", @@ -50,6 +51,7 @@ "error": { "already_exists": "Kamera dengan setelan URL ini sudah ada.", "invalid_still_image": "URL tidak mengembalikan gambar diam yang valid", + "malformed_url": "URL salah format", "no_still_image_or_stream_url": "Anda harus menentukan setidaknya gambar diam atau URL streaming", "stream_file_not_found": "File tidak ditemukan saat mencoba menyambung ke streaming (sudahkah ffmpeg diinstal?)", "stream_http_not_found": "HTTP 404 Tidak ditemukan saat mencoba menyambung ke streaming", diff --git a/homeassistant/components/homeassistant/translations/id.json b/homeassistant/components/homeassistant/translations/id.json index f795a47ee20..7c2994d8bbb 100644 --- a/homeassistant/components/homeassistant/translations/id.json +++ b/homeassistant/components/homeassistant/translations/id.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Arsitektur CPU", + "config_dir": "Direktori Konfigurasi", "dev": "Pengembangan", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/humidifier/translations/hu.json b/homeassistant/components/humidifier/translations/hu.json index 6572f0e4955..8c8312dc627 100644 --- a/homeassistant/components/humidifier/translations/hu.json +++ b/homeassistant/components/humidifier/translations/hu.json @@ -14,7 +14,7 @@ }, "trigger_type": { "changed_states": "{entity_name} be- vagy kikapcsolt", - "target_humidity_changed": "{name} k\u00edv\u00e1nt p\u00e1ratartalom megv\u00e1ltozott", + "target_humidity_changed": "{entity_name} k\u00edv\u00e1nt p\u00e1ratartalom megv\u00e1ltozott", "turned_off": "{entity_name} ki lett kapcsolva", "turned_on": "{entity_name} be lett kapcsolva" } diff --git a/homeassistant/components/isy994/translations/hu.json b/homeassistant/components/isy994/translations/hu.json index e5a5c4d6f98..4ea107b469f 100644 --- a/homeassistant/components/isy994/translations/hu.json +++ b/homeassistant/components/isy994/translations/hu.json @@ -27,7 +27,7 @@ "tls": "Az ISY vez\u00e9rl\u0151 TLS verzi\u00f3ja.", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "A c\u00edm 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 ISY-hez" } } diff --git a/homeassistant/components/lg_soundbar/translations/id.json b/homeassistant/components/lg_soundbar/translations/id.json new file mode 100644 index 00000000000..70ce3eb75e9 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/id.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/id.json b/homeassistant/components/life360/translations/id.json index 21a93366c44..b219745572d 100644 --- a/homeassistant/components/life360/translations/id.json +++ b/homeassistant/components/life360/translations/id.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Akun sudah dikonfigurasi", "invalid_auth": "Autentikasi tidak valid", + "reauth_successful": "Autentikasi ulang berhasil", "unknown": "Kesalahan yang tidak diharapkan" }, "create_entry": { @@ -9,6 +11,7 @@ }, "error": { "already_configured": "Akun sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", "invalid_username": "Nama pengguna tidak valid", "unknown": "Kesalahan yang tidak diharapkan" @@ -20,7 +23,21 @@ "username": "Nama Pengguna" }, "description": "Untuk mengatur opsi tingkat lanjut, baca [dokumentasi Life360]({docs_url}).\nAnda mungkin ingin melakukannya sebelum menambahkan akun.", - "title": "Info Akun Life360" + "title": "Konfigurasikan Akun Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Tampilkan mengemudi sebagai status", + "driving_speed": "Kecepatan mengemudi", + "limit_gps_acc": "Batasi akurasi GPS", + "max_gps_accuracy": "Akurasi GPS maksimum (meter)", + "set_drive_speed": "Tetapkan ambang batas kecepatan mengemudi" + }, + "title": "Opsi Akun" } } } diff --git a/homeassistant/components/nextdns/translations/ca.json b/homeassistant/components/nextdns/translations/ca.json index 83a2c018da5..dd38c4f0b91 100644 --- a/homeassistant/components/nextdns/translations/ca.json +++ b/homeassistant/components/nextdns/translations/ca.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Servidor accessible" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/el.json b/homeassistant/components/nextdns/translations/el.json index 2d5003a1d7a..14c913febcf 100644 --- a/homeassistant/components/nextdns/translations/el.json +++ b/homeassistant/components/nextdns/translations/el.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "\u03a0\u03c1\u03bf\u03c3\u03ad\u03b3\u03b3\u03b9\u03c3\u03b7 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/en.json b/homeassistant/components/nextdns/translations/en.json index c765d53a4d9..d5634514010 100644 --- a/homeassistant/components/nextdns/translations/en.json +++ b/homeassistant/components/nextdns/translations/en.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Reach server" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/fr.json b/homeassistant/components/nextdns/translations/fr.json index bfebc9078a5..c5f91ef80cc 100644 --- a/homeassistant/components/nextdns/translations/fr.json +++ b/homeassistant/components/nextdns/translations/fr.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Serveur atteint" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/hu.json b/homeassistant/components/nextdns/translations/hu.json index 33b5d858e8a..2491f625b4f 100644 --- a/homeassistant/components/nextdns/translations/hu.json +++ b/homeassistant/components/nextdns/translations/hu.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Szerver el\u00e9r\u00e9se" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/id.json b/homeassistant/components/nextdns/translations/id.json new file mode 100644 index 00000000000..9ebb40ae2d8 --- /dev/null +++ b/homeassistant/components/nextdns/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Profil NextDNS ini sudah dikonfigurasi." + }, + "error": { + "invalid_api_key": "Kunci API tidak valid" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "Kunci API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/pl.json b/homeassistant/components/nextdns/translations/pl.json index b7b74af7f1b..f23d4da29d3 100644 --- a/homeassistant/components/nextdns/translations/pl.json +++ b/homeassistant/components/nextdns/translations/pl.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Dost\u0119p do serwera" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/pt-BR.json b/homeassistant/components/nextdns/translations/pt-BR.json index 2982a6daad7..90d7cf3f31e 100644 --- a/homeassistant/components/nextdns/translations/pt-BR.json +++ b/homeassistant/components/nextdns/translations/pt-BR.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Servidor de alcance" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/zh-Hant.json b/homeassistant/components/nextdns/translations/zh-Hant.json index 19d2f137a39..5544f29662f 100644 --- a/homeassistant/components/nextdns/translations/zh-Hant.json +++ b/homeassistant/components/nextdns/translations/zh-Hant.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Reach \u4f3a\u670d\u5668" + } } } \ No newline at end of file diff --git a/homeassistant/components/nina/translations/id.json b/homeassistant/components/nina/translations/id.json index 03f6feecc22..7e3642a04ef 100644 --- a/homeassistant/components/nina/translations/id.json +++ b/homeassistant/components/nina/translations/id.json @@ -23,5 +23,22 @@ "title": "Pilih kota/kabupaten" } } + }, + "options": { + "error": { + "no_selection": "Pilih setidaknya satu kota/kabupaten" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Kota/kabupaten (A-D)", + "_e_to_h": "Kota/kabupaten (E-H)", + "_i_to_l": "Kota/kabupaten (I-L)", + "_m_to_q": "Kota/kabupaten (M-Q)", + "_r_to_u": "Kota/kabupaten (R-U)" + }, + "title": "Opsi" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json index 158411ef618..ebdde5c9428 100644 --- a/homeassistant/components/rfxtrx/translations/hu.json +++ b/homeassistant/components/rfxtrx/translations/hu.json @@ -45,7 +45,7 @@ "send_status": "\u00c1llapotfriss\u00edt\u00e9s k\u00fcld\u00e9se: {subtype}" }, "trigger_type": { - "command": "Be\u00e9rkezett parancs: {alt\u00edpus}", + "command": "Be\u00e9rkezett parancs: {subtype}", "status": "Be\u00e9rkezett st\u00e1tusz: {subtype}" } }, diff --git a/homeassistant/components/soundtouch/translations/id.json b/homeassistant/components/soundtouch/translations/id.json new file mode 100644 index 00000000000..71b7e84f1e2 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/id.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "zeroconf_confirm": { + "description": "Anda akan menambahkan perangkat SoundTouch bernama `{name}` ke Home Assistant.", + "title": "Konfirmasi penambahan perangkat Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/hu.json b/homeassistant/components/unifiprotect/translations/hu.json index 11e0151f165..cbef293c7e9 100644 --- a/homeassistant/components/unifiprotect/translations/hu.json +++ b/homeassistant/components/unifiprotect/translations/hu.json @@ -16,7 +16,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({ipaddress})? Egy helyi felhaszn\u00e1l\u00f3t kell l\u00e9trehoznia Unifi OS-ben.", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({ipaddress})? A bejelentkez\u00e9shez egy helyi felhaszn\u00e1l\u00f3ra lesz sz\u00fcks\u00e9g, amelyet az UniFi OS Console-ban hoztak l\u00e9tre. Az Ubiquiti Cloud Users nem fog m\u0171k\u00f6dni. Tov\u00e1bbi inform\u00e1ci\u00f3: {local_user_documentation_url}", "title": "UniFi Protect felfedezve" }, "reauth_confirm": { From 104d236646b1a15d39c4eb80512ea7b0fa6c9dda Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 7 Jul 2022 08:40:10 +0200 Subject: [PATCH 2219/3516] fjaraskupan: Make sure we stop bleak on home assistant stop (#74545) * Make sure we stop bleak on home assistant stop * Fix typing Co-authored-by: Martin Hjelmare --- homeassistant/components/fjaraskupan/__init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 4c4f19403a6..85e95db5513 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -12,8 +12,8 @@ from bleak.backends.scanner import AdvertisementData from fjaraskupan import Device, State, device_filter from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -131,6 +131,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: scanner.register_detection_callback(detection_callback) await scanner.start() + async def on_hass_stop(event: Event) -> None: + await scanner.stop() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True From 4a4dabaaa5769ee69b12b0f808a09b3423603596 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 7 Jul 2022 09:03:43 +0200 Subject: [PATCH 2220/3516] Fix openweathermap hourly forecast (#74578) --- homeassistant/components/openweathermap/weather.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index ea439a35586..9948ecc0428 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -143,7 +143,11 @@ class OpenWeatherMapWeather(WeatherEntity): """Return the forecast array.""" api_forecasts = self._weather_coordinator.data[ATTR_API_FORECAST] forecasts = [ - {ha_key: forecast[api_key] for api_key, ha_key in FORECAST_MAP.items()} + { + ha_key: forecast[api_key] + for api_key, ha_key in FORECAST_MAP.items() + if api_key in forecast + } for forecast in api_forecasts ] return cast(list[Forecast], forecasts) From 46f2abc38c7776ce2dc0c583ffe90d0172cc691b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 7 Jul 2022 09:22:35 +0200 Subject: [PATCH 2221/3516] Use generics in NextDNS (#74517) Use generics in nextdns --- homeassistant/components/nextdns/__init__.py | 17 ++++--- homeassistant/components/nextdns/sensor.py | 49 ++++++++++++++++---- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index f9cb1f60cd1..7e7f5ff2dd8 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from datetime import timedelta import logging +from typing import TypeVar from aiohttp.client_exceptions import ClientConnectorError from async_timeout import timeout @@ -39,8 +40,10 @@ from .const import ( UPDATE_INTERVAL_ANALYTICS, ) +TCoordinatorData = TypeVar("TCoordinatorData", bound=NextDnsData) -class NextDnsUpdateCoordinator(DataUpdateCoordinator): + +class NextDnsUpdateCoordinator(DataUpdateCoordinator[TCoordinatorData]): """Class to manage fetching NextDNS data API.""" def __init__( @@ -64,12 +67,12 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator): super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) - async def _async_update_data(self) -> NextDnsData: + async def _async_update_data(self) -> TCoordinatorData: """Update data via library.""" raise NotImplementedError("Update method not implemented") -class NextDnsStatusUpdateCoordinator(NextDnsUpdateCoordinator): +class NextDnsStatusUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsStatus]): """Class to manage fetching NextDNS analytics status data from API.""" async def _async_update_data(self) -> AnalyticsStatus: @@ -81,7 +84,7 @@ class NextDnsStatusUpdateCoordinator(NextDnsUpdateCoordinator): raise UpdateFailed(err) from err -class NextDnsDnssecUpdateCoordinator(NextDnsUpdateCoordinator): +class NextDnsDnssecUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsDnssec]): """Class to manage fetching NextDNS analytics Dnssec data from API.""" async def _async_update_data(self) -> AnalyticsDnssec: @@ -93,7 +96,7 @@ class NextDnsDnssecUpdateCoordinator(NextDnsUpdateCoordinator): raise UpdateFailed(err) from err -class NextDnsEncryptionUpdateCoordinator(NextDnsUpdateCoordinator): +class NextDnsEncryptionUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsEncryption]): """Class to manage fetching NextDNS analytics encryption data from API.""" async def _async_update_data(self) -> AnalyticsEncryption: @@ -105,7 +108,7 @@ class NextDnsEncryptionUpdateCoordinator(NextDnsUpdateCoordinator): raise UpdateFailed(err) from err -class NextDnsIpVersionsUpdateCoordinator(NextDnsUpdateCoordinator): +class NextDnsIpVersionsUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsIpVersions]): """Class to manage fetching NextDNS analytics IP versions data from API.""" async def _async_update_data(self) -> AnalyticsIpVersions: @@ -117,7 +120,7 @@ class NextDnsIpVersionsUpdateCoordinator(NextDnsUpdateCoordinator): raise UpdateFailed(err) from err -class NextDnsProtocolsUpdateCoordinator(NextDnsUpdateCoordinator): +class NextDnsProtocolsUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsProtocols]): """Class to manage fetching NextDNS analytics protocols data from API.""" async def _async_update_data(self) -> AnalyticsProtocols: diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index ef0176b071a..71d71ff287b 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -3,7 +3,15 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Any, cast +from typing import Generic, cast + +from nextdns import ( + AnalyticsDnssec, + AnalyticsEncryption, + AnalyticsIpVersions, + AnalyticsProtocols, + AnalyticsStatus, +) from homeassistant.components.sensor import ( SensorEntity, @@ -18,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import NextDnsUpdateCoordinator +from . import NextDnsUpdateCoordinator, TCoordinatorData from .const import ( ATTR_DNSSEC, ATTR_ENCRYPTION, @@ -32,23 +40,26 @@ PARALLEL_UPDATES = 1 @dataclass -class NextDnsSensorRequiredKeysMixin: +class NextDnsSensorRequiredKeysMixin(Generic[TCoordinatorData]): """Class for NextDNS entity required keys.""" + coordinator_class: type[TCoordinatorData] coordinator_type: str - value: Callable[[Any], StateType] + value: Callable[[TCoordinatorData], StateType] @dataclass class NextDnsSensorEntityDescription( - SensorEntityDescription, NextDnsSensorRequiredKeysMixin + SensorEntityDescription, + NextDnsSensorRequiredKeysMixin[TCoordinatorData], ): """NextDNS sensor entity description.""" -SENSORS = ( +SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( NextDnsSensorEntityDescription( key="all_queries", + coordinator_class=AnalyticsStatus, coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", @@ -59,6 +70,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="blocked_queries", + coordinator_class=AnalyticsStatus, coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", @@ -69,6 +81,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="relayed_queries", + coordinator_class=AnalyticsStatus, coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", @@ -79,6 +92,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="blocked_queries_ratio", + coordinator_class=AnalyticsStatus, coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", @@ -89,6 +103,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="doh_queries", + coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -100,6 +115,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="dot_queries", + coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -111,6 +127,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="doq_queries", + coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -122,6 +139,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="udp_queries", + coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -133,6 +151,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="doh_queries_ratio", + coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_registry_enabled_default=False, icon="mdi:dns", @@ -144,6 +163,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="dot_queries_ratio", + coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -155,6 +175,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="doq_queries_ratio", + coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_registry_enabled_default=False, icon="mdi:dns", @@ -166,6 +187,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="udp_queries_ratio", + coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -177,6 +199,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="encrypted_queries", + coordinator_class=AnalyticsEncryption, coordinator_type=ATTR_ENCRYPTION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -188,6 +211,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="unencrypted_queries", + coordinator_class=AnalyticsEncryption, coordinator_type=ATTR_ENCRYPTION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -199,6 +223,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="encrypted_queries_ratio", + coordinator_class=AnalyticsEncryption, coordinator_type=ATTR_ENCRYPTION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -210,6 +235,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="ipv4_queries", + coordinator_class=AnalyticsIpVersions, coordinator_type=ATTR_IP_VERSIONS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -221,6 +247,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="ipv6_queries", + coordinator_class=AnalyticsIpVersions, coordinator_type=ATTR_IP_VERSIONS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -232,6 +259,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="ipv6_queries_ratio", + coordinator_class=AnalyticsIpVersions, coordinator_type=ATTR_IP_VERSIONS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -243,6 +271,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="validated_queries", + coordinator_class=AnalyticsDnssec, coordinator_type=ATTR_DNSSEC, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -254,6 +283,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="not_validated_queries", + coordinator_class=AnalyticsDnssec, coordinator_type=ATTR_DNSSEC, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -265,6 +295,7 @@ SENSORS = ( ), NextDnsSensorEntityDescription( key="validated_queries_ratio", + coordinator_class=AnalyticsDnssec, coordinator_type=ATTR_DNSSEC, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -294,12 +325,14 @@ async def async_setup_entry( async_add_entities(sensors) -class NextDnsSensor(CoordinatorEntity[NextDnsUpdateCoordinator], SensorEntity): +class NextDnsSensor( + CoordinatorEntity[NextDnsUpdateCoordinator[TCoordinatorData]], SensorEntity +): """Define an NextDNS sensor.""" def __init__( self, - coordinator: NextDnsUpdateCoordinator, + coordinator: NextDnsUpdateCoordinator[TCoordinatorData], description: NextDnsSensorEntityDescription, ) -> None: """Initialize.""" From e56357b4f2efb0b979238c4096bfca09437fde54 Mon Sep 17 00:00:00 2001 From: ufodone <35497351+ufodone@users.noreply.github.com> Date: Thu, 7 Jul 2022 00:33:32 -0700 Subject: [PATCH 2222/3516] Bump pyenvisalink version to 4.6 (#74561) --- homeassistant/components/envisalink/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json index 44a40991a37..8d0fc735b90 100644 --- a/homeassistant/components/envisalink/manifest.json +++ b/homeassistant/components/envisalink/manifest.json @@ -2,7 +2,7 @@ "domain": "envisalink", "name": "Envisalink", "documentation": "https://www.home-assistant.io/integrations/envisalink", - "requirements": ["pyenvisalink==4.5"], + "requirements": ["pyenvisalink==4.6"], "codeowners": ["@ufodone"], "iot_class": "local_push", "loggers": ["pyenvisalink"] diff --git a/requirements_all.txt b/requirements_all.txt index 17b8c5d7e42..90742fd4875 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1477,7 +1477,7 @@ pyeight==0.3.0 pyemby==1.8 # homeassistant.components.envisalink -pyenvisalink==4.5 +pyenvisalink==4.6 # homeassistant.components.ephember pyephember==0.3.1 From 681735b94cfc5ff1668c0eb54995f25ebde62d2b Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 7 Jul 2022 03:39:37 -0400 Subject: [PATCH 2223/3516] Bump aioskybell to 22.7.0 (#74559) --- homeassistant/components/skybell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index c0e66aa5462..bfef4bc3422 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -3,7 +3,7 @@ "name": "SkyBell", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/skybell", - "requirements": ["aioskybell==22.6.1"], + "requirements": ["aioskybell==22.7.0"], "dependencies": ["ffmpeg"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 90742fd4875..97d62c96420 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -247,7 +247,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.skybell -aioskybell==22.6.1 +aioskybell==22.7.0 # homeassistant.components.slimproto aioslimproto==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d5c616edf2e..06dcf27cbe5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -216,7 +216,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.skybell -aioskybell==22.6.1 +aioskybell==22.7.0 # homeassistant.components.slimproto aioslimproto==2.1.1 From d203cb0658f3729c75d3966306d3931a9fc763c8 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 7 Jul 2022 02:43:10 -0500 Subject: [PATCH 2224/3516] Minimize Sonos `media_player.unjoin` timeout (#74549) --- homeassistant/components/sonos/media_player.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index c68110d9763..470386c341d 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -69,6 +69,7 @@ from .speaker import SonosMedia, SonosSpeaker _LOGGER = logging.getLogger(__name__) LONG_SERVICE_TIMEOUT = 30.0 +UNJOIN_SERVICE_TIMEOUT = 0.1 VOLUME_INCREMENT = 2 REPEAT_TO_SONOS = { @@ -775,7 +776,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): async def async_unjoin_player(self): """Remove this player from any group. - Coalesces all calls within 0.5s to allow use of SonosSpeaker.unjoin_multi() + Coalesces all calls within UNJOIN_SERVICE_TIMEOUT to allow use of SonosSpeaker.unjoin_multi() which optimizes the order in which speakers are removed from their groups. Removing coordinators last better preserves playqueues on the speakers. """ @@ -785,6 +786,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): async def async_process_unjoin(now: datetime.datetime) -> None: """Process the unjoin with all remove requests within the coalescing period.""" unjoin_data = sonos_data.unjoin_data.pop(household_id) + _LOGGER.debug( + "Processing unjoins for %s", [x.zone_name for x in unjoin_data.speakers] + ) await SonosSpeaker.unjoin_multi(self.hass, unjoin_data.speakers) unjoin_data.event.set() @@ -794,6 +798,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): unjoin_data = sonos_data.unjoin_data[household_id] = UnjoinData( speakers=[self.speaker] ) - async_call_later(self.hass, 0.5, async_process_unjoin) + async_call_later(self.hass, UNJOIN_SERVICE_TIMEOUT, async_process_unjoin) + _LOGGER.debug("Requesting unjoin for %s", self.speaker.zone_name) await unjoin_data.event.wait() From ae295f1bf5fc9564533747e04ee22c624f905308 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 7 Jul 2022 04:49:32 -0400 Subject: [PATCH 2225/3516] Add three decimal places of sub-second resolution to root logger timestamps (#74518) --- homeassistant/bootstrap.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 81d0fa72134..939d3073f57 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -285,7 +285,9 @@ def async_enable_logging( This method must be run in the event loop. """ - fmt = "%(asctime)s %(levelname)s (%(threadName)s) [%(name)s] %(message)s" + fmt = ( + "%(asctime)s.%(msecs)03d %(levelname)s (%(threadName)s) [%(name)s] %(message)s" + ) datefmt = "%Y-%m-%d %H:%M:%S" if not log_no_color: From 0c29b68cf82c777ec6fd70ce38911f1d1e39d26a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Jul 2022 03:57:44 -0500 Subject: [PATCH 2226/3516] Switch linear search to a dict lookup for ip bans (#74482) --- homeassistant/components/http/ban.py | 101 ++++++++++++---------- tests/components/conftest.py | 3 +- tests/components/http/test_ban.py | 124 ++++++++++++++++++++++++--- 3 files changed, 170 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 620bdc7613c..81349fe95a1 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -26,7 +26,7 @@ from .view import HomeAssistantView _LOGGER: Final = logging.getLogger(__name__) -KEY_BANNED_IPS: Final = "ha_banned_ips" +KEY_BAN_MANAGER: Final = "ha_banned_ips_manager" KEY_FAILED_LOGIN_ATTEMPTS: Final = "ha_failed_login_attempts" KEY_LOGIN_THRESHOLD: Final = "ha_login_threshold" @@ -50,9 +50,9 @@ def setup_bans(hass: HomeAssistant, app: Application, login_threshold: int) -> N async def ban_startup(app: Application) -> None: """Initialize bans when app starts up.""" - app[KEY_BANNED_IPS] = await async_load_ip_bans_config( - hass, hass.config.path(IP_BANS_FILE) - ) + ban_manager = IpBanManager(hass) + await ban_manager.async_load() + app[KEY_BAN_MANAGER] = ban_manager app.on_startup.append(ban_startup) @@ -62,18 +62,17 @@ async def ban_middleware( request: Request, handler: Callable[[Request], Awaitable[StreamResponse]] ) -> StreamResponse: """IP Ban middleware.""" - if KEY_BANNED_IPS not in request.app: + ban_manager: IpBanManager | None = request.app.get(KEY_BAN_MANAGER) + if ban_manager is None: _LOGGER.error("IP Ban middleware loaded but banned IPs not loaded") return await handler(request) - # Verify if IP is not banned - ip_address_ = ip_address(request.remote) # type: ignore[arg-type] - is_banned = any( - ip_ban.ip_address == ip_address_ for ip_ban in request.app[KEY_BANNED_IPS] - ) - - if is_banned: - raise HTTPForbidden() + ip_bans_lookup = ban_manager.ip_bans_lookup + if ip_bans_lookup: + # Verify if IP is not banned + ip_address_ = ip_address(request.remote) # type: ignore[arg-type] + if ip_address_ in ip_bans_lookup: + raise HTTPForbidden() try: return await handler(request) @@ -129,7 +128,7 @@ async def process_wrong_login(request: Request) -> None: ) # Check if ban middleware is loaded - if KEY_BANNED_IPS not in request.app or request.app[KEY_LOGIN_THRESHOLD] < 1: + if KEY_BAN_MANAGER not in request.app or request.app[KEY_LOGIN_THRESHOLD] < 1: return request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] += 1 @@ -146,14 +145,9 @@ async def process_wrong_login(request: Request) -> None: request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] >= request.app[KEY_LOGIN_THRESHOLD] ): - new_ban = IpBan(remote_addr) - request.app[KEY_BANNED_IPS].append(new_ban) - - await hass.async_add_executor_job( - update_ip_bans_config, hass.config.path(IP_BANS_FILE), new_ban - ) - + ban_manager: IpBanManager = request.app[KEY_BAN_MANAGER] _LOGGER.warning("Banned IP %s for too many login attempts", remote_addr) + await ban_manager.async_add_ban(remote_addr) persistent_notification.async_create( hass, @@ -173,7 +167,7 @@ async def process_success_login(request: Request) -> None: remote_addr = ip_address(request.remote) # type: ignore[arg-type] # Check if ban middleware is loaded - if KEY_BANNED_IPS not in request.app or request.app[KEY_LOGIN_THRESHOLD] < 1: + if KEY_BAN_MANAGER not in request.app or request.app[KEY_LOGIN_THRESHOLD] < 1: return if ( @@ -199,32 +193,49 @@ class IpBan: self.banned_at = banned_at or dt_util.utcnow() -async def async_load_ip_bans_config(hass: HomeAssistant, path: str) -> list[IpBan]: - """Load list of banned IPs from config file.""" - ip_list: list[IpBan] = [] +class IpBanManager: + """Manage IP bans.""" - try: - list_ = await hass.async_add_executor_job(load_yaml_config_file, path) - except FileNotFoundError: - return ip_list - except HomeAssistantError as err: - _LOGGER.error("Unable to load %s: %s", path, str(err)) - return ip_list + def __init__(self, hass: HomeAssistant) -> None: + """Init the ban manager.""" + self.hass = hass + self.path = hass.config.path(IP_BANS_FILE) + self.ip_bans_lookup: dict[IPv4Address | IPv6Address, IpBan] = {} - for ip_ban, ip_info in list_.items(): + async def async_load(self) -> None: + """Load the existing IP bans.""" try: - ip_info = SCHEMA_IP_BAN_ENTRY(ip_info) - ip_list.append(IpBan(ip_ban, ip_info["banned_at"])) - except vol.Invalid as err: - _LOGGER.error("Failed to load IP ban %s: %s", ip_info, err) - continue + list_ = await self.hass.async_add_executor_job( + load_yaml_config_file, self.path + ) + except FileNotFoundError: + return + except HomeAssistantError as err: + _LOGGER.error("Unable to load %s: %s", self.path, str(err)) + return - return ip_list + ip_bans_lookup: dict[IPv4Address | IPv6Address, IpBan] = {} + for ip_ban, ip_info in list_.items(): + try: + ip_info = SCHEMA_IP_BAN_ENTRY(ip_info) + ban = IpBan(ip_ban, ip_info["banned_at"]) + ip_bans_lookup[ban.ip_address] = ban + except vol.Invalid as err: + _LOGGER.error("Failed to load IP ban %s: %s", ip_info, err) + continue + self.ip_bans_lookup = ip_bans_lookup -def update_ip_bans_config(path: str, ip_ban: IpBan) -> None: - """Update config file with new banned IP address.""" - with open(path, "a", encoding="utf8") as out: - ip_ = {str(ip_ban.ip_address): {ATTR_BANNED_AT: ip_ban.banned_at.isoformat()}} - out.write("\n") - out.write(yaml.dump(ip_)) + def _add_ban(self, ip_ban: IpBan) -> None: + """Update config file with new banned IP address.""" + with open(self.path, "a", encoding="utf8") as out: + ip_ = { + str(ip_ban.ip_address): {ATTR_BANNED_AT: ip_ban.banned_at.isoformat()} + } + # Write in a single write call to avoid interleaved writes + out.write("\n" + yaml.dump(ip_)) + + async def async_add_ban(self, remote_addr: IPv4Address | IPv6Address) -> None: + """Add a new IP address to the banned list.""" + new_ban = self.ip_bans_lookup[remote_addr] = IpBan(remote_addr) + await self.hass.async_add_executor_job(self._add_ban, new_ban) diff --git a/tests/components/conftest.py b/tests/components/conftest.py index f153263cbc6..6cad53aea72 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -19,8 +19,7 @@ def patch_zeroconf_multiple_catcher(): def prevent_io(): """Fixture to prevent certain I/O from happening.""" with patch( - "homeassistant.components.http.ban.async_load_ip_bans_config", - return_value=[], + "homeassistant.components.http.ban.load_yaml_config_file", ): yield diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index 5e482d16248..05a6493c9c2 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -15,12 +15,13 @@ import homeassistant.components.http as http from homeassistant.components.http import KEY_AUTHENTICATED from homeassistant.components.http.ban import ( IP_BANS_FILE, - KEY_BANNED_IPS, + KEY_BAN_MANAGER, KEY_FAILED_LOGIN_ATTEMPTS, - IpBan, + IpBanManager, setup_bans, ) from homeassistant.components.http.view import request_handler_factory +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from . import mock_real_ip @@ -58,8 +59,10 @@ async def test_access_from_banned_ip(hass, aiohttp_client): set_real_ip = mock_real_ip(app) with patch( - "homeassistant.components.http.ban.async_load_ip_bans_config", - return_value=[IpBan(banned_ip) for banned_ip in BANNED_IPS], + "homeassistant.components.http.ban.load_yaml_config_file", + return_value={ + banned_ip: {"banned_at": "2016-11-16T19:20:03"} for banned_ip in BANNED_IPS + }, ): client = await aiohttp_client(app) @@ -69,6 +72,99 @@ async def test_access_from_banned_ip(hass, aiohttp_client): assert resp.status == HTTPStatus.FORBIDDEN +async def test_access_from_banned_ip_with_partially_broken_yaml_file( + hass, aiohttp_client, caplog +): + """Test accessing to server from banned IP. Both trusted and not. + + We inject some garbage into the yaml file to make sure it can + still load the bans. + """ + app = web.Application() + app["hass"] = hass + setup_bans(hass, app, 5) + set_real_ip = mock_real_ip(app) + + data = {banned_ip: {"banned_at": "2016-11-16T19:20:03"} for banned_ip in BANNED_IPS} + data["5.3.3.3"] = {"banned_at": "garbage"} + + with patch( + "homeassistant.components.http.ban.load_yaml_config_file", + return_value=data, + ): + client = await aiohttp_client(app) + + for remote_addr in BANNED_IPS: + set_real_ip(remote_addr) + resp = await client.get("/") + assert resp.status == HTTPStatus.FORBIDDEN + + # Ensure garbage data is ignored + set_real_ip("5.3.3.3") + resp = await client.get("/") + assert resp.status == HTTPStatus.NOT_FOUND + + assert "Failed to load IP ban" in caplog.text + + +async def test_no_ip_bans_file(hass, aiohttp_client): + """Test no ip bans file.""" + app = web.Application() + app["hass"] = hass + setup_bans(hass, app, 5) + set_real_ip = mock_real_ip(app) + + with patch( + "homeassistant.components.http.ban.load_yaml_config_file", + side_effect=FileNotFoundError, + ): + client = await aiohttp_client(app) + + set_real_ip("4.3.2.1") + resp = await client.get("/") + assert resp.status == HTTPStatus.NOT_FOUND + + +async def test_failure_loading_ip_bans_file(hass, aiohttp_client): + """Test failure loading ip bans file.""" + app = web.Application() + app["hass"] = hass + setup_bans(hass, app, 5) + set_real_ip = mock_real_ip(app) + + with patch( + "homeassistant.components.http.ban.load_yaml_config_file", + side_effect=HomeAssistantError, + ): + client = await aiohttp_client(app) + + set_real_ip("4.3.2.1") + resp = await client.get("/") + assert resp.status == HTTPStatus.NOT_FOUND + + +async def test_ip_ban_manager_never_started(hass, aiohttp_client, caplog): + """Test we handle the ip ban manager not being started.""" + app = web.Application() + app["hass"] = hass + setup_bans(hass, app, 5) + set_real_ip = mock_real_ip(app) + + with patch( + "homeassistant.components.http.ban.load_yaml_config_file", + side_effect=FileNotFoundError, + ): + client = await aiohttp_client(app) + + # Mock the manager never being started + del app[KEY_BAN_MANAGER] + + set_real_ip("4.3.2.1") + resp = await client.get("/") + assert resp.status == HTTPStatus.NOT_FOUND + assert "IP Ban middleware loaded but banned IPs not loaded" in caplog.text + + @pytest.mark.parametrize( "remote_addr, bans, status", list( @@ -95,10 +191,13 @@ async def test_access_from_supervisor_ip( mock_real_ip(app)(remote_addr) with patch( - "homeassistant.components.http.ban.async_load_ip_bans_config", return_value=[] + "homeassistant.components.http.ban.load_yaml_config_file", + return_value={}, ): client = await aiohttp_client(app) + manager: IpBanManager = app[KEY_BAN_MANAGER] + assert await async_setup_component(hass, "hassio", {"hassio": {}}) m_open = mock_open() @@ -108,13 +207,13 @@ async def test_access_from_supervisor_ip( ): resp = await client.get("/") assert resp.status == HTTPStatus.UNAUTHORIZED - assert len(app[KEY_BANNED_IPS]) == bans + assert len(manager.ip_bans_lookup) == bans assert m_open.call_count == bans # second request should be forbidden if banned resp = await client.get("/") assert resp.status == status - assert len(app[KEY_BANNED_IPS]) == bans + assert len(manager.ip_bans_lookup) == bans async def test_ban_middleware_not_loaded_by_config(hass): @@ -149,22 +248,25 @@ async def test_ip_bans_file_creation(hass, aiohttp_client): mock_real_ip(app)("200.201.202.204") with patch( - "homeassistant.components.http.ban.async_load_ip_bans_config", - return_value=[IpBan(banned_ip) for banned_ip in BANNED_IPS], + "homeassistant.components.http.ban.load_yaml_config_file", + return_value={ + banned_ip: {"banned_at": "2016-11-16T19:20:03"} for banned_ip in BANNED_IPS + }, ): client = await aiohttp_client(app) + manager: IpBanManager = app[KEY_BAN_MANAGER] m_open = mock_open() with patch("homeassistant.components.http.ban.open", m_open, create=True): resp = await client.get("/") assert resp.status == HTTPStatus.UNAUTHORIZED - assert len(app[KEY_BANNED_IPS]) == len(BANNED_IPS) + assert len(manager.ip_bans_lookup) == len(BANNED_IPS) assert m_open.call_count == 0 resp = await client.get("/") assert resp.status == HTTPStatus.UNAUTHORIZED - assert len(app[KEY_BANNED_IPS]) == len(BANNED_IPS) + 1 + assert len(manager.ip_bans_lookup) == len(BANNED_IPS) + 1 m_open.assert_called_once_with( hass.config.path(IP_BANS_FILE), "a", encoding="utf8" ) From 42615950788569d72a93a730fcac26447c950601 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 11:00:34 +0200 Subject: [PATCH 2227/3516] Update orjson to 3.7.7 (#74581) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8795e570216..d72cb6445a5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -20,7 +20,7 @@ httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.7 -orjson==3.7.5 +orjson==3.7.7 paho-mqtt==1.6.1 pillow==9.2.0 pip>=21.0,<22.2 diff --git a/pyproject.toml b/pyproject.toml index 59ad04c4b11..60e0865fab9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ dependencies = [ "PyJWT==2.4.0", # PyJWT has loose dependency. We want the latest one. "cryptography==36.0.2", - "orjson==3.7.5", + "orjson==3.7.7", "pip>=21.0,<22.2", "python-slugify==4.0.1", "pyyaml==6.0", diff --git a/requirements.txt b/requirements.txt index a8ec77333e0..7826a9a0f26 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ jinja2==3.1.2 lru-dict==1.1.7 PyJWT==2.4.0 cryptography==36.0.2 -orjson==3.7.5 +orjson==3.7.7 pip>=21.0,<22.2 python-slugify==4.0.1 pyyaml==6.0 From 5ae593672e5ef2949560921c289905fbba208350 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 7 Jul 2022 11:35:45 +0200 Subject: [PATCH 2228/3516] Remove google_assistant from mypy ignore list (#74587) --- homeassistant/components/google_assistant/helpers.py | 2 +- homeassistant/components/google_assistant/http.py | 2 +- .../components/google_assistant/report_state.py | 3 ++- homeassistant/components/google_assistant/trait.py | 10 ++++++---- mypy.ini | 12 ------------ script/hassfest/mypy_config.py | 4 ---- 6 files changed, 10 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 6f81ddebdb4..0b9b12f2f4c 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -51,7 +51,7 @@ LOCAL_SDK_MIN_VERSION = AwesomeVersion("2.1.5") @callback def _get_registry_entries( hass: HomeAssistant, entity_id: str -) -> tuple[device_registry.DeviceEntry, area_registry.AreaEntry]: +) -> tuple[device_registry.DeviceEntry | None, area_registry.AreaEntry | None]: """Get registry entries.""" ent_reg = entity_registry.async_get(hass) dev_reg = device_registry.async_get(hass) diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 3f3db1f2b5b..84d5e4a3364 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -59,7 +59,7 @@ def _get_homegraph_jwt(time, iss, key): async def _get_homegraph_token( hass: HomeAssistant, jwt_signed: str -) -> dict[str, Any] | list[str, Any] | Any: +) -> dict[str, Any] | list[Any] | Any: headers = { "Authorization": f"Bearer {jwt_signed}", "Content-Type": "application/x-www-form-urlencoded", diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index 4e8ac1624cc..737b54c8b1e 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import deque import logging +from typing import Any from homeassistant.const import MATCH_ALL from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback @@ -28,7 +29,7 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig """Enable state reporting.""" checker = None unsub_pending: CALLBACK_TYPE | None = None - pending = deque([{}]) + pending: deque[dict[str, Any]] = deque([{}]) async def report_states(now=None): """Report the states.""" diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 42fc43197ea..ee41dd0c678 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -176,6 +176,8 @@ def _next_selected(items: list[str], selected: str | None) -> str | None: If selected is missing in items, None is returned """ + if selected is None: + return None try: index = items.index(selected) except ValueError: @@ -188,7 +190,7 @@ def _next_selected(items: list[str], selected: str | None) -> str | None: class _Trait: """Represents a Trait inside Google Assistant skill.""" - commands = [] + commands: list[str] = [] @staticmethod def might_2fa(domain, features, device_class): @@ -1701,7 +1703,7 @@ class InputSelectorTrait(_Trait): name = TRAIT_INPUTSELECTOR commands = [COMMAND_INPUT, COMMAND_NEXT_INPUT, COMMAND_PREVIOUS_INPUT] - SYNONYMS = {} + SYNONYMS: dict[str, list[str]] = {} @staticmethod def supported(domain, features, device_class, _): @@ -2197,7 +2199,7 @@ class MediaStateTrait(_Trait): """ name = TRAIT_MEDIA_STATE - commands = [] + commands: list[str] = [] activity_lookup = { STATE_OFF: "INACTIVE", @@ -2314,7 +2316,7 @@ class SensorStateTrait(_Trait): } name = TRAIT_SENSOR_STATE - commands = [] + commands: list[str] = [] @classmethod def supported(cls, domain, features, device_class, _): diff --git a/mypy.ini b/mypy.ini index f29bcf92720..5c5675bcb37 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2662,18 +2662,6 @@ ignore_errors = true [mypy-homeassistant.components.evohome.water_heater] ignore_errors = true -[mypy-homeassistant.components.google_assistant.helpers] -ignore_errors = true - -[mypy-homeassistant.components.google_assistant.http] -ignore_errors = true - -[mypy-homeassistant.components.google_assistant.report_state] -ignore_errors = true - -[mypy-homeassistant.components.google_assistant.trait] -ignore_errors = true - [mypy-homeassistant.components.hassio] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index f058cfd391f..a1955c51993 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -29,10 +29,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.evohome", "homeassistant.components.evohome.climate", "homeassistant.components.evohome.water_heater", - "homeassistant.components.google_assistant.helpers", - "homeassistant.components.google_assistant.http", - "homeassistant.components.google_assistant.report_state", - "homeassistant.components.google_assistant.trait", "homeassistant.components.hassio", "homeassistant.components.hassio.auth", "homeassistant.components.hassio.binary_sensor", From 4604694255701b591e484c2573d722b955cde8f3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 11:40:17 +0200 Subject: [PATCH 2229/3516] Add deprecation to PR template (#74583) --- .github/PULL_REQUEST_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 92de30ffe5a..52d25226930 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -33,6 +33,7 @@ - [ ] Bugfix (non-breaking change which fixes an issue) - [ ] New integration (thank you!) - [ ] New feature (which adds functionality to an existing integration) +- [ ] Deprecation (breaking change to happen in the future) - [ ] Breaking change (fix/feature causing existing functionality to break) - [ ] Code quality improvements to existing code or addition of tests From f19c542d6d470f2ba1f4e41c3b47b535ea5e5fd7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 7 Jul 2022 12:14:46 +0200 Subject: [PATCH 2230/3516] Remove denonavr from mypy ignore list (#74580) --- .../components/denonavr/config_flow.py | 15 +- .../components/denonavr/media_player.py | 152 ++++++++++-------- homeassistant/components/denonavr/receiver.py | 7 +- mypy.ini | 9 -- script/hassfest/mypy_config.py | 3 - 5 files changed, 94 insertions(+), 92 deletions(-) diff --git a/homeassistant/components/denonavr/config_flow.py b/homeassistant/components/denonavr/config_flow.py index fe6c05b3aca..2ec58df8126 100644 --- a/homeassistant/components/denonavr/config_flow.py +++ b/homeassistant/components/denonavr/config_flow.py @@ -90,14 +90,14 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the Denon AVR flow.""" - self.host = None - self.serial_number = None - self.model_name = None + self.host: str | None = None + self.serial_number: str | None = None + self.model_name: str | None = None self.timeout = DEFAULT_TIMEOUT self.show_all_sources = DEFAULT_SHOW_SOURCES self.zone2 = DEFAULT_ZONE2 self.zone3 = DEFAULT_ZONE3 - self.d_receivers = [] + self.d_receivers: list[dict[str, Any]] = [] @staticmethod @callback @@ -138,7 +138,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle multiple receivers found.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: self.host = user_input["select_host"] return await self.async_step_connect() @@ -169,6 +169,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Connect to the receiver.""" + assert self.host connect_denonavr = ConnectDenonAVR( self.host, self.timeout, @@ -185,6 +186,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not success: return self.async_abort(reason="cannot_connect") receiver = connect_denonavr.receiver + assert receiver if not self.serial_number: self.serial_number = receiver.serial_number @@ -238,6 +240,7 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): "*", "" ) self.serial_number = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] + assert discovery_info.ssdp_location is not None self.host = urlparse(discovery_info.ssdp_location).hostname if self.model_name in IGNORED_MODELS: @@ -260,6 +263,6 @@ class DenonAvrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_confirm() @staticmethod - def construct_unique_id(model_name: str, serial_number: str) -> str: + def construct_unique_id(model_name: str | None, serial_number: str | None) -> str: """Construct the unique id from the ssdp discovery or user_step.""" return f"{model_name}-{serial_number}" diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 85e28c29d7c..10c3cb4f6b0 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -1,10 +1,11 @@ """Support for Denon AVR receivers using their HTTP interface.""" from __future__ import annotations -from collections.abc import Coroutine +from collections.abc import Awaitable, Callable, Coroutine from datetime import timedelta from functools import wraps import logging +from typing import Any, TypeVar from denonavr import DenonAVR from denonavr.const import POWER_ON @@ -15,6 +16,7 @@ from denonavr.exceptions import ( AvrTimoutError, DenonAvrError, ) +from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.components.media_player import ( @@ -79,6 +81,10 @@ SERVICE_GET_COMMAND = "get_command" SERVICE_SET_DYNAMIC_EQ = "set_dynamic_eq" SERVICE_UPDATE_AUDYSSEY = "update_audyssey" +_DenonDeviceT = TypeVar("_DenonDeviceT", bound="DenonDevice") +_R = TypeVar("_R") +_P = ParamSpec("_P") + async def async_setup_entry( hass: HomeAssistant, @@ -131,6 +137,79 @@ async def async_setup_entry( async_add_entities(entities, update_before_add=True) +def async_log_errors( + func: Callable[Concatenate[_DenonDeviceT, _P], Awaitable[_R]], +) -> Callable[Concatenate[_DenonDeviceT, _P], Coroutine[Any, Any, _R | None]]: + """ + Log errors occurred when calling a Denon AVR receiver. + + Decorates methods of DenonDevice class. + Declaration of staticmethod for this method is at the end of this class. + """ + + @wraps(func) + async def wrapper( + self: _DenonDeviceT, *args: _P.args, **kwargs: _P.kwargs + ) -> _R | None: + # pylint: disable=protected-access + available = True + try: + return await func(self, *args, **kwargs) + except AvrTimoutError: + available = False + if self._available is True: + _LOGGER.warning( + "Timeout connecting to Denon AVR receiver at host %s. " + "Device is unavailable", + self._receiver.host, + ) + self._available = False + except AvrNetworkError: + available = False + if self._available is True: + _LOGGER.warning( + "Network error connecting to Denon AVR receiver at host %s. " + "Device is unavailable", + self._receiver.host, + ) + self._available = False + except AvrForbiddenError: + available = False + if self._available is True: + _LOGGER.warning( + "Denon AVR receiver at host %s responded with HTTP 403 error. " + "Device is unavailable. Please consider power cycling your " + "receiver", + self._receiver.host, + ) + self._available = False + except AvrCommandError as err: + available = False + _LOGGER.error( + "Command %s failed with error: %s", + func.__name__, + err, + ) + except DenonAvrError as err: + available = False + _LOGGER.error( + "Error %s occurred in method %s for Denon AVR receiver", + err, + func.__name__, + exc_info=True, + ) + finally: + if available is True and self._available is False: + _LOGGER.info( + "Denon AVR receiver at host %s is available again", + self._receiver.host, + ) + self._available = True + return None + + return wrapper + + class DenonDevice(MediaPlayerEntity): """Representation of a Denon Media Player Device.""" @@ -144,6 +223,7 @@ class DenonDevice(MediaPlayerEntity): """Initialize the device.""" self._attr_name = receiver.name self._attr_unique_id = unique_id + assert config_entry.unique_id self._attr_device_info = DeviceInfo( configuration_url=f"http://{config_entry.data[CONF_HOST]}/", identifiers={(DOMAIN, config_entry.unique_id)}, @@ -163,71 +243,6 @@ class DenonDevice(MediaPlayerEntity): ) self._available = True - def async_log_errors( - func: Coroutine, - ) -> Coroutine: - """ - Log errors occurred when calling a Denon AVR receiver. - - Decorates methods of DenonDevice class. - Declaration of staticmethod for this method is at the end of this class. - """ - - @wraps(func) - async def wrapper(self, *args, **kwargs): - # pylint: disable=protected-access - available = True - try: - return await func(self, *args, **kwargs) - except AvrTimoutError: - available = False - if self._available is True: - _LOGGER.warning( - "Timeout connecting to Denon AVR receiver at host %s. Device is unavailable", - self._receiver.host, - ) - self._available = False - except AvrNetworkError: - available = False - if self._available is True: - _LOGGER.warning( - "Network error connecting to Denon AVR receiver at host %s. Device is unavailable", - self._receiver.host, - ) - self._available = False - except AvrForbiddenError: - available = False - if self._available is True: - _LOGGER.warning( - "Denon AVR receiver at host %s responded with HTTP 403 error. Device is unavailable. Please consider power cycling your receiver", - self._receiver.host, - ) - self._available = False - except AvrCommandError as err: - available = False - _LOGGER.error( - "Command %s failed with error: %s", - func.__name__, - err, - ) - except DenonAvrError as err: - available = False - _LOGGER.error( - "Error %s occurred in method %s for Denon AVR receiver", - err, - func.__name__, - exc_info=True, - ) - finally: - if available is True and self._available is False: - _LOGGER.info( - "Denon AVR receiver at host %s is available again", - self._receiver.host, - ) - self._available = True - - return wrapper - @async_log_errors async def async_update(self) -> None: """Get the latest status information from device.""" @@ -466,8 +481,3 @@ class DenonDevice(MediaPlayerEntity): if self._update_audyssey: await self._receiver.async_update_audyssey() - - # Decorator defined before is a staticmethod - async_log_errors = staticmethod( # pylint: disable=no-staticmethod-decorator - async_log_errors - ) diff --git a/homeassistant/components/denonavr/receiver.py b/homeassistant/components/denonavr/receiver.py index 5c15468e6d4..28969d25792 100644 --- a/homeassistant/components/denonavr/receiver.py +++ b/homeassistant/components/denonavr/receiver.py @@ -23,12 +23,12 @@ class ConnectDenonAVR: ) -> None: """Initialize the class.""" self._async_client_getter = async_client_getter - self._receiver = None + self._receiver: DenonAVR | None = None self._host = host self._show_all_inputs = show_all_inputs self._timeout = timeout - self._zones = {} + self._zones: dict[str, str | None] = {} if zone2: self._zones["Zone2"] = None if zone3: @@ -42,6 +42,7 @@ class ConnectDenonAVR: async def async_connect_receiver(self) -> bool: """Connect to the DenonAVR receiver.""" await self.async_init_receiver_class() + assert self._receiver if ( self._receiver.manufacturer is None @@ -70,7 +71,7 @@ class ConnectDenonAVR: return True - async def async_init_receiver_class(self) -> bool: + async def async_init_receiver_class(self) -> None: """Initialize the DenonAVR class asynchronously.""" receiver = DenonAVR( host=self._host, diff --git a/mypy.ini b/mypy.ini index 5c5675bcb37..c4a135322fe 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2644,15 +2644,6 @@ ignore_errors = true [mypy-homeassistant.components.conversation.default_agent] ignore_errors = true -[mypy-homeassistant.components.denonavr.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.denonavr.media_player] -ignore_errors = true - -[mypy-homeassistant.components.denonavr.receiver] -ignore_errors = true - [mypy-homeassistant.components.evohome] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index a1955c51993..e7c5e0ed129 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -23,9 +23,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.cloud.http_api", "homeassistant.components.conversation", "homeassistant.components.conversation.default_agent", - "homeassistant.components.denonavr.config_flow", - "homeassistant.components.denonavr.media_player", - "homeassistant.components.denonavr.receiver", "homeassistant.components.evohome", "homeassistant.components.evohome.climate", "homeassistant.components.evohome.water_heater", From ba7ad1029ca6ef7e650404e948ce546a7e02c046 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 7 Jul 2022 05:17:13 -0500 Subject: [PATCH 2231/3516] Remove legacy Sonos grouping services (#74476) --- .../components/sonos/media_player.py | 35 +-------------- homeassistant/components/sonos/services.yaml | 29 ------------- tests/components/sonos/test_media_player.py | 17 -------- tests/components/sonos/test_services.py | 43 +++++++++++++++++++ 4 files changed, 44 insertions(+), 80 deletions(-) create mode 100644 tests/components/sonos/test_services.py diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 470386c341d..4e7998c8e4e 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -82,8 +82,6 @@ SONOS_TO_REPEAT = {meaning: mode for mode, meaning in REPEAT_TO_SONOS.items()} UPNP_ERRORS_TO_IGNORE = ["701", "711", "712"] -SERVICE_JOIN = "join" -SERVICE_UNJOIN = "unjoin" SERVICE_SNAPSHOT = "snapshot" SERVICE_RESTORE = "restore" SERVICE_SET_TIMER = "set_sleep_timer" @@ -130,24 +128,7 @@ async def async_setup_entry( assert isinstance(entity, SonosMediaPlayerEntity) speakers.append(entity.speaker) - if service_call.service == SERVICE_JOIN: - _LOGGER.warning( - "Service 'sonos.join' is deprecated and will be removed in 2022.8, please use 'media_player.join'" - ) - master = platform.entities.get(service_call.data[ATTR_MASTER]) - if master: - await SonosSpeaker.join_multi(hass, master.speaker, speakers) # type: ignore[arg-type] - else: - _LOGGER.error( - "Invalid master specified for join service: %s", - service_call.data[ATTR_MASTER], - ) - elif service_call.service == SERVICE_UNJOIN: - _LOGGER.warning( - "Service 'sonos.unjoin' is deprecated and will be removed in 2022.8, please use 'media_player.unjoin'" - ) - await SonosSpeaker.unjoin_multi(hass, speakers) # type: ignore[arg-type] - elif service_call.service == SERVICE_SNAPSHOT: + if service_call.service == SERVICE_SNAPSHOT: await SonosSpeaker.snapshot_multi( hass, speakers, service_call.data[ATTR_WITH_GROUP] # type: ignore[arg-type] ) @@ -160,20 +141,6 @@ async def async_setup_entry( async_dispatcher_connect(hass, SONOS_CREATE_MEDIA_PLAYER, async_create_entities) ) - hass.services.async_register( - SONOS_DOMAIN, - SERVICE_JOIN, - async_service_handle, - cv.make_entity_service_schema({vol.Required(ATTR_MASTER): cv.entity_id}), - ) - - hass.services.async_register( - SONOS_DOMAIN, - SERVICE_UNJOIN, - async_service_handle, - cv.make_entity_service_schema({}), - ) - join_unjoin_schema = cv.make_entity_service_schema( {vol.Optional(ATTR_WITH_GROUP, default=True): cv.boolean} ) diff --git a/homeassistant/components/sonos/services.yaml b/homeassistant/components/sonos/services.yaml index a172b45ecd9..9d61c20f7cb 100644 --- a/homeassistant/components/sonos/services.yaml +++ b/homeassistant/components/sonos/services.yaml @@ -1,32 +1,3 @@ -join: - name: Join group - description: Group player together. - fields: - master: - name: Master - description: Entity ID of the player that should become the coordinator of the group. - required: true - selector: - entity: - integration: sonos - domain: media_player - entity_id: - name: Entity - description: Name of entity that will join the master. - required: true - selector: - entity: - integration: sonos - domain: media_player - -unjoin: - name: Unjoin group - description: Unjoin the player from a group. - target: - entity: - integration: sonos - domain: media_player - snapshot: name: Snapshot description: Take a snapshot of the media player. diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index 425b11fc35c..9c2119c8331 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1,25 +1,8 @@ """Tests for the Sonos Media Player platform.""" -import pytest - -from homeassistant.components.sonos import DOMAIN, media_player from homeassistant.const import STATE_IDLE -from homeassistant.core import Context -from homeassistant.exceptions import Unauthorized from homeassistant.helpers import device_registry as dr -async def test_services(hass, async_autosetup_sonos, hass_read_only_user): - """Test join/unjoin requires control access.""" - with pytest.raises(Unauthorized): - await hass.services.async_call( - DOMAIN, - media_player.SERVICE_JOIN, - {"master": "media_player.bla", "entity_id": "media_player.blub"}, - blocking=True, - context=Context(user_id=hass_read_only_user.id), - ) - - async def test_device_registry(hass, async_autosetup_sonos, soco): """Test sonos device registered in the device registry.""" device_registry = dr.async_get(hass) diff --git a/tests/components/sonos/test_services.py b/tests/components/sonos/test_services.py new file mode 100644 index 00000000000..d0bf3bf3a06 --- /dev/null +++ b/tests/components/sonos/test_services.py @@ -0,0 +1,43 @@ +"""Tests for Sonos services.""" +from unittest.mock import Mock, patch + +import pytest + +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.components.media_player.const import SERVICE_JOIN +from homeassistant.components.sonos.const import DATA_SONOS +from homeassistant.exceptions import HomeAssistantError + + +async def test_media_player_join(hass, async_autosetup_sonos): + """Test join service.""" + valid_entity_id = "media_player.zone_a" + mocked_entity_id = "media_player.mocked" + + # Ensure an error is raised if the entity is unknown + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + MP_DOMAIN, + SERVICE_JOIN, + {"entity_id": valid_entity_id, "group_members": mocked_entity_id}, + blocking=True, + ) + + # Ensure SonosSpeaker.join_multi is called if entity is found + mocked_speaker = Mock() + mock_entity_id_mappings = {mocked_entity_id: mocked_speaker} + + with patch.dict( + hass.data[DATA_SONOS].entity_id_mappings, mock_entity_id_mappings + ), patch( + "homeassistant.components.sonos.speaker.SonosSpeaker.join_multi" + ) as mock_join_multi: + await hass.services.async_call( + MP_DOMAIN, + SERVICE_JOIN, + {"entity_id": valid_entity_id, "group_members": mocked_entity_id}, + blocking=True, + ) + + found_speaker = hass.data[DATA_SONOS].entity_id_mappings[valid_entity_id] + mock_join_multi.assert_called_with(hass, found_speaker, [mocked_speaker]) From 19f33d205cfc6a53b230b5395b7ca1e42b4ae84c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 12:27:25 +0200 Subject: [PATCH 2232/3516] Fix mix of aiohttp and requests in Bloomsky (#74598) --- homeassistant/components/bloomsky/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/bloomsky/__init__.py b/homeassistant/components/bloomsky/__init__.py index 8311f6cbd96..ed2ce1ebc70 100644 --- a/homeassistant/components/bloomsky/__init__.py +++ b/homeassistant/components/bloomsky/__init__.py @@ -3,7 +3,6 @@ from datetime import timedelta from http import HTTPStatus import logging -from aiohttp.hdrs import AUTHORIZATION import requests import voluptuous as vol @@ -67,7 +66,7 @@ class BloomSky: _LOGGER.debug("Fetching BloomSky update") response = requests.get( f"{self.API_URL}?{self._endpoint_argument}", - headers={AUTHORIZATION: self._api_key}, + headers={"Authorization": self._api_key}, timeout=10, ) if response.status_code == HTTPStatus.UNAUTHORIZED: From 4fcf8280f6efd59dec63b00d49c0e2861417ae13 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 12:31:19 +0200 Subject: [PATCH 2233/3516] Use FlowResultType enum in Tuya tests (#74596) --- tests/components/tuya/test_config_flow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index 46db598e1d8..82e7bfa4b51 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -84,7 +84,7 @@ async def test_user_flow( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" tuya().connect = MagicMock(side_effect=side_effects) @@ -95,7 +95,7 @@ async def test_user_flow( 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["type"] == data_entry_flow.FlowResultType.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 @@ -115,7 +115,7 @@ async def test_error_on_invalid_credentials(hass, tuya): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" tuya().connect = MagicMock(return_value=RESPONSE_ERROR) From 2169b70874c06c9ba3fe59e1d40690faf4634470 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 12:31:54 +0200 Subject: [PATCH 2234/3516] Use FlowResultType enum in WLED tests (#74594) --- tests/components/wled/test_config_flow.py | 34 ++++++++++------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index e1cf08069da..600bf0eb0d2 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.wled.const import CONF_KEEP_MASTER_LIGHT, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -27,7 +23,7 @@ async def test_full_user_flow_implementation( ) assert result.get("step_id") == "user" - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result result = await hass.config_entries.flow.async_configure( @@ -35,7 +31,7 @@ async def test_full_user_flow_implementation( ) assert result.get("title") == "WLED RGB Light" - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == "192.168.1.123" assert "result" in result @@ -68,7 +64,7 @@ async def test_full_zeroconf_flow_implementation( ) assert result.get("description_placeholders") == {CONF_NAME: "WLED RGB Light"} assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( @@ -76,7 +72,7 @@ async def test_full_zeroconf_flow_implementation( ) assert result2.get("title") == "WLED RGB Light" - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert "data" in result2 assert result2["data"][CONF_HOST] == "192.168.1.123" @@ -106,7 +102,7 @@ async def test_zeroconf_during_onboarding( ) assert result.get("title") == "WLED RGB Light" - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("data") == {CONF_HOST: "192.168.1.123"} assert "result" in result @@ -127,7 +123,7 @@ async def test_connection_error( data={CONF_HOST: "example.com"}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -152,7 +148,7 @@ async def test_zeroconf_connection_error( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -169,7 +165,7 @@ async def test_user_device_exists_abort( data={CONF_HOST: "192.168.1.123"}, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -186,7 +182,7 @@ async def test_user_with_cct_channel_abort( data={CONF_HOST: "192.168.1.123"}, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "cct_unsupported" @@ -211,7 +207,7 @@ async def test_zeroconf_without_mac_device_exists_abort( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -236,7 +232,7 @@ async def test_zeroconf_with_mac_device_exists_abort( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -261,7 +257,7 @@ async def test_zeroconf_with_cct_channel_abort( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "cct_unsupported" @@ -273,7 +269,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -282,7 +278,7 @@ async def test_options_flow( user_input={CONF_KEEP_MASTER_LIGHT: True}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("data") == { CONF_KEEP_MASTER_LIGHT: True, } From 996544da2d3f47ef9ea0392f66a9e3483e2e1852 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 7 Jul 2022 14:51:16 +0200 Subject: [PATCH 2235/3516] Poll cast groups when media player is added or reconnected (#74610) --- homeassistant/components/cast/helpers.py | 2 +- homeassistant/components/cast/media_player.py | 13 ++++ tests/components/cast/test_media_player.py | 75 +++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index d7419f69563..48f57c39bd5 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -37,7 +37,7 @@ class ChromecastInfo: @property def friendly_name(self) -> str: - """Return the UUID.""" + """Return the Friendly Name.""" return self.cast_info.friendly_name @property diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 958c53ae394..c8a6a82571e 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -441,6 +441,19 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): connection_status.status, ) self._attr_available = new_available + if new_available and not self._cast_info.is_audio_group: + # Poll current group status + for group_uuid in self.mz_mgr.get_multizone_memberships( + self._cast_info.uuid + ): + group_media_controller = self.mz_mgr.get_multizone_mediacontroller( + group_uuid + ) + if not group_media_controller: + continue + self.multizone_new_media_status( + group_uuid, group_media_controller.status + ) self.schedule_update_ha_state() def multizone_new_media_status(self, group_uuid, media_status): diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 61a6067578a..9d3e1a6d534 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -733,6 +733,20 @@ async def test_entity_availability(hass: HomeAssistant): state = hass.states.get(entity_id) assert state.state == "off" + connection_status = MagicMock() + connection_status.status = "LOST" + conn_status_cb(connection_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == "unavailable" + + connection_status = MagicMock() + connection_status.status = "CONNECTED" + conn_status_cb(connection_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == "off" + connection_status = MagicMock() connection_status.status = "DISCONNECTED" conn_status_cb(connection_status) @@ -740,6 +754,14 @@ async def test_entity_availability(hass: HomeAssistant): state = hass.states.get(entity_id) assert state.state == "unavailable" + # Can't reconnect after receiving DISCONNECTED + connection_status = MagicMock() + connection_status.status = "CONNECTED" + conn_status_cb(connection_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == "unavailable" + @pytest.mark.parametrize("port,entry_type", ((8009, None), (12345, None))) async def test_device_registry(hass: HomeAssistant, hass_ws_client, port, entry_type): @@ -1677,6 +1699,59 @@ async def test_group_media_states(hass, mz_mock): assert state.state == "playing" +async def test_group_media_states_early(hass, mz_mock): + """Test media states are read from group if entity has no state. + + This tests case asserts group state is polled when the player is created. + """ + entity_id = "media_player.speaker" + reg = er.async_get(hass) + + info = get_fake_chromecast_info() + + mz_mock.get_multizone_memberships = MagicMock(return_value=[str(FakeGroupUUID)]) + mz_mock.get_multizone_mediacontroller = MagicMock( + return_value=MagicMock(status=MagicMock(images=None, player_state="BUFFERING")) + ) + + chromecast, _ = await async_setup_media_player_cast(hass, info) + _, conn_status_cb, _, _ = get_status_callbacks(chromecast, mz_mock) + + state = hass.states.get(entity_id) + assert state is not None + assert state.name == "Speaker" + assert state.state == "unavailable" + assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) + + # Check group state is polled when player is first created + connection_status = MagicMock() + connection_status.status = "CONNECTED" + conn_status_cb(connection_status) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "buffering" + + connection_status = MagicMock() + connection_status.status = "LOST" + conn_status_cb(connection_status) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "unavailable" + + # Check group state is polled when player reconnects + mz_mock.get_multizone_mediacontroller = MagicMock( + return_value=MagicMock(status=MagicMock(images=None, player_state="PLAYING")) + ) + + connection_status = MagicMock() + connection_status.status = "CONNECTED" + conn_status_cb(connection_status) + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "playing" + + async def test_group_media_control(hass, mz_mock, quick_play_mock): """Test media controls are handled by group if entity has no state.""" entity_id = "media_player.speaker" From 6540ba623978813668a30e5822b97e076fc05a93 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 7 Jul 2022 15:14:36 +0200 Subject: [PATCH 2236/3516] Remove hassio from mypy ignore list (#74603) * Remove hassio from mypy ignore list * Avoid if TYPE_CHECKING --- homeassistant/components/hassio/__init__.py | 7 +++++-- homeassistant/components/hassio/auth.py | 1 + .../components/hassio/binary_sensor.py | 2 +- homeassistant/components/hassio/ingress.py | 3 +++ homeassistant/components/hassio/sensor.py | 2 +- .../components/hassio/system_health.py | 4 ++++ .../components/hassio/websocket_api.py | 9 ++++---- mypy.ini | 21 ------------------- script/hassfest/mypy_config.py | 7 ------- 9 files changed, 20 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index cd3c704d4c9..5b5cc48eed8 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -504,7 +504,7 @@ def is_hassio(hass: HomeAssistant) -> bool: @callback -def get_supervisor_ip() -> str: +def get_supervisor_ip() -> str | None: """Return the supervisor ip address.""" if "SUPERVISOR" not in os.environ: return None @@ -537,6 +537,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: if (data := await store.async_load()) is None: data = {} + assert isinstance(data, dict) + refresh_token = None if "hassio_user" in data: user = await hass.auth.async_get_user(data["hassio_user"]) @@ -710,6 +712,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: async_setup_discovery_view(hass, hassio) # Init auth Hass.io feature + assert user is not None async_setup_auth_view(hass, user) # Init ingress Hass.io feature @@ -877,7 +880,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator): except HassioAPIError as err: raise UpdateFailed(f"Error on Supervisor API: {err}") from err - new_data = {} + new_data: dict[str, Any] = {} supervisor_info = get_supervisor_info(self.hass) addons_info = get_addons_info(self.hass) addons_stats = get_addons_stats(self.hass) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index f52a8ef0617..37687ee70df 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -43,6 +43,7 @@ class HassIOBaseAuth(HomeAssistantView): """Check if this call is from Supervisor.""" # Check caller IP hassio_ip = os.environ["SUPERVISOR"].split(":")[0] + assert request.transport if ip_address(request.transport.get_extra_info("peername")[0]) != ip_address( hassio_ip ): diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py index c2bcd5eaf68..85cb402b0ca 100644 --- a/homeassistant/components/hassio/binary_sensor.py +++ b/homeassistant/components/hassio/binary_sensor.py @@ -59,7 +59,7 @@ async def async_setup_entry( """Binary sensor set up for Hass.io config entry.""" coordinator = hass.data[ADDONS_COORDINATOR] - entities = [] + entities: list[HassioAddonBinarySensor | HassioOSBinarySensor] = [] for entity_description in ADDON_ENTITY_DESCRIPTIONS: for addon in coordinator.data[DATA_KEY_ADDONS].values(): diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 6caa97b788f..8aacbac99f6 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Iterable from ipaddress import ip_address import logging import os @@ -73,6 +74,7 @@ class HassIOIngress(HomeAssistantView): self, request: web.Request, token: str, path: str ) -> web.WebSocketResponse: """Ingress route for websocket.""" + req_protocols: Iterable[str] if hdrs.SEC_WEBSOCKET_PROTOCOL in request.headers: req_protocols = [ str(proto.strip()) @@ -190,6 +192,7 @@ def _init_header(request: web.Request, token: str) -> CIMultiDict | dict[str, st # Set X-Forwarded-For forward_for = request.headers.get(hdrs.X_FORWARDED_FOR) + assert request.transport if (peername := request.transport.get_extra_info("peername")) is None: _LOGGER.error("Can't set forward_for header, missing peername") raise HTTPBadRequest() diff --git a/homeassistant/components/hassio/sensor.py b/homeassistant/components/hassio/sensor.py index 42be1ff4b0a..55fcb0bcd28 100644 --- a/homeassistant/components/hassio/sensor.py +++ b/homeassistant/components/hassio/sensor.py @@ -65,7 +65,7 @@ async def async_setup_entry( """Sensor set up for Hass.io config entry.""" coordinator = hass.data[ADDONS_COORDINATOR] - entities = [] + entities: list[HassioOSSensor | HassioAddonSensor] = [] for addon in coordinator.data[DATA_KEY_ADDONS].values(): for entity_description in ADDON_ENTITY_DESCRIPTIONS: diff --git a/homeassistant/components/hassio/system_health.py b/homeassistant/components/hassio/system_health.py index b1fc208de80..d8d29f44d68 100644 --- a/homeassistant/components/hassio/system_health.py +++ b/homeassistant/components/hassio/system_health.py @@ -1,4 +1,6 @@ """Provide info to system health.""" +from __future__ import annotations + import os from homeassistant.components import system_health @@ -24,6 +26,7 @@ async def system_health_info(hass: HomeAssistant): host_info = get_host_info(hass) supervisor_info = get_supervisor_info(hass) + healthy: bool | dict[str, str] if supervisor_info.get("healthy"): healthy = True else: @@ -32,6 +35,7 @@ async def system_health_info(hass: HomeAssistant): "error": "Unhealthy", } + supported: bool | dict[str, str] if supervisor_info.get("supported"): supported = True else: diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index 7eb037d8432..b25d9fda7a0 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -1,5 +1,6 @@ """Websocekt API handlers for the hassio integration.""" import logging +from numbers import Number import re import voluptuous as vol @@ -56,8 +57,8 @@ def async_load_websocket_api(hass: HomeAssistant): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command({vol.Required(WS_TYPE): WS_TYPE_SUBSCRIBE}) +@websocket_api.async_response async def websocket_subscribe( hass: HomeAssistant, connection: ActiveConnection, msg: dict ): @@ -74,13 +75,13 @@ async def websocket_subscribe( connection.send_message(websocket_api.result_message(msg[WS_ID])) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(WS_TYPE): WS_TYPE_EVENT, vol.Required(ATTR_DATA): SCHEMA_WEBSOCKET_EVENT, } ) +@websocket_api.async_response async def websocket_supervisor_event( hass: HomeAssistant, connection: ActiveConnection, msg: dict ): @@ -89,16 +90,16 @@ async def websocket_supervisor_event( connection.send_result(msg[WS_ID]) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required(WS_TYPE): WS_TYPE_API, vol.Required(ATTR_ENDPOINT): cv.string, vol.Required(ATTR_METHOD): cv.string, vol.Optional(ATTR_DATA): dict, - vol.Optional(ATTR_TIMEOUT): vol.Any(cv.Number, None), + vol.Optional(ATTR_TIMEOUT): vol.Any(Number, None), } ) +@websocket_api.async_response async def websocket_supervisor_api( hass: HomeAssistant, connection: ActiveConnection, msg: dict ): diff --git a/mypy.ini b/mypy.ini index c4a135322fe..fc99119cda3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2653,27 +2653,6 @@ ignore_errors = true [mypy-homeassistant.components.evohome.water_heater] ignore_errors = true -[mypy-homeassistant.components.hassio] -ignore_errors = true - -[mypy-homeassistant.components.hassio.auth] -ignore_errors = true - -[mypy-homeassistant.components.hassio.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.hassio.ingress] -ignore_errors = true - -[mypy-homeassistant.components.hassio.sensor] -ignore_errors = true - -[mypy-homeassistant.components.hassio.system_health] -ignore_errors = true - -[mypy-homeassistant.components.hassio.websocket_api] -ignore_errors = true - [mypy-homeassistant.components.icloud] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index e7c5e0ed129..7f1faf00bb6 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -26,13 +26,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.evohome", "homeassistant.components.evohome.climate", "homeassistant.components.evohome.water_heater", - "homeassistant.components.hassio", - "homeassistant.components.hassio.auth", - "homeassistant.components.hassio.binary_sensor", - "homeassistant.components.hassio.ingress", - "homeassistant.components.hassio.sensor", - "homeassistant.components.hassio.system_health", - "homeassistant.components.hassio.websocket_api", "homeassistant.components.icloud", "homeassistant.components.icloud.account", "homeassistant.components.icloud.device_tracker", From dfdd0378784ab93c26138ccaa59e904f8c6fad89 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 16:02:36 +0200 Subject: [PATCH 2237/3516] Update aiokafka to 0.7.2 (#74601) --- homeassistant/components/apache_kafka/__init__.py | 1 - homeassistant/components/apache_kafka/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/apache_kafka/__init__.py b/homeassistant/components/apache_kafka/__init__.py index 1b293bd2c04..38a70b450ab 100644 --- a/homeassistant/components/apache_kafka/__init__.py +++ b/homeassistant/components/apache_kafka/__init__.py @@ -102,7 +102,6 @@ class KafkaManager: self._hass = hass ssl_context = ssl_util.client_context() self._producer = AIOKafkaProducer( - loop=hass.loop, bootstrap_servers=f"{ip_address}:{port}", compression_type="gzip", security_protocol=security_protocol, diff --git a/homeassistant/components/apache_kafka/manifest.json b/homeassistant/components/apache_kafka/manifest.json index 3b290146a09..3fa1f70c57b 100644 --- a/homeassistant/components/apache_kafka/manifest.json +++ b/homeassistant/components/apache_kafka/manifest.json @@ -2,7 +2,7 @@ "domain": "apache_kafka", "name": "Apache Kafka", "documentation": "https://www.home-assistant.io/integrations/apache_kafka", - "requirements": ["aiokafka==0.6.0"], + "requirements": ["aiokafka==0.7.2"], "codeowners": ["@bachya"], "iot_class": "local_push", "loggers": ["aiokafka", "kafka_python"] diff --git a/requirements_all.txt b/requirements_all.txt index 97d62c96420..5970f541f8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -181,7 +181,7 @@ aiohue==4.4.2 aioimaplib==1.0.0 # homeassistant.components.apache_kafka -aiokafka==0.6.0 +aiokafka==0.7.2 # homeassistant.components.kef aiokef==0.2.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 06dcf27cbe5..b07ebcd33b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -162,7 +162,7 @@ aiohttp_cors==0.7.0 aiohue==4.4.2 # homeassistant.components.apache_kafka -aiokafka==0.6.0 +aiokafka==0.7.2 # homeassistant.components.lookin aiolookin==0.1.1 From 29cbd9d469df1d4b0a1712464b7551d53c8f2c93 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 7 Jul 2022 16:19:56 +0200 Subject: [PATCH 2238/3516] Update frontend to 20220707.0 (#74625) --- 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 85ead380485..5c7fd1a20be 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220706.0"], + "requirements": ["home-assistant-frontend==20220707.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d72cb6445a5..a388f178df3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220706.0 +home-assistant-frontend==20220707.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 5970f541f8e..838e1278d75 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220706.0 +home-assistant-frontend==20220707.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b07ebcd33b4..b9c98ba9fb2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -598,7 +598,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220706.0 +home-assistant-frontend==20220707.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 4e2de2479a3f1ae861c7683fe3c5c0881a242c17 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 7 Jul 2022 15:25:44 +0100 Subject: [PATCH 2239/3516] Add SetSystemDateandTime Button (#66419) * add SetSystemDateandTime * fix * address review * follow recommendation to set date and time on start * add set date and time button test --- homeassistant/components/onvif/button.py | 19 ++++++++++++-- homeassistant/components/onvif/device.py | 32 ++++++++++++++++++++++++ tests/components/onvif/test_button.py | 30 ++++++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onvif/button.py b/homeassistant/components/onvif/button.py index 23ea5124e61..0af4a16d269 100644 --- a/homeassistant/components/onvif/button.py +++ b/homeassistant/components/onvif/button.py @@ -1,5 +1,4 @@ """ONVIF Buttons.""" - from homeassistant.components.button import ButtonDeviceClass, ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -18,7 +17,7 @@ async def async_setup_entry( ) -> None: """Set up ONVIF button based on a config entry.""" device = hass.data[DOMAIN][config_entry.unique_id] - async_add_entities([RebootButton(device)]) + async_add_entities([RebootButton(device), SetSystemDateAndTimeButton(device)]) class RebootButton(ONVIFBaseEntity, ButtonEntity): @@ -39,3 +38,19 @@ class RebootButton(ONVIFBaseEntity, ButtonEntity): """Send out a SystemReboot command.""" device_mgmt = self.device.device.create_devicemgmt_service() await device_mgmt.SystemReboot() + + +class SetSystemDateAndTimeButton(ONVIFBaseEntity, ButtonEntity): + """Defines a ONVIF SetSystemDateAndTime button.""" + + _attr_entity_category = EntityCategory.CONFIG + + def __init__(self, device: ONVIFDevice) -> None: + """Initialize the button entity.""" + super().__init__(device) + self._attr_name = f"{self.device.name} Set System Date and Time" + self._attr_unique_id = f"{self.device.info.mac or self.device.info.serial_number}_setsystemdatetime" + + async def async_press(self) -> None: + """Send out a SetSystemDateAndTime command.""" + await self.device.async_manually_set_date_and_time() diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index d376b7fe258..5907ea90124 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -5,6 +5,7 @@ import asyncio from contextlib import suppress import datetime as dt import os +import time from httpx import RequestError import onvif @@ -148,6 +149,32 @@ class ONVIFDevice: await self.events.async_stop() await self.device.close() + async def async_manually_set_date_and_time(self) -> None: + """Set Date and Time Manually using SetSystemDateAndTime command.""" + device_mgmt = self.device.create_devicemgmt_service() + + # Retrieve DateTime object from camera to use as template for Set operation + device_time = await device_mgmt.GetSystemDateAndTime() + + system_date = dt_util.utcnow() + LOGGER.debug("System date (UTC): %s", system_date) + + dt_param = device_mgmt.create_type("SetSystemDateAndTime") + dt_param.DateTimeType = "Manual" + # Retrieve DST setting from system + dt_param.DaylightSavings = bool(time.localtime().tm_isdst) + dt_param.UTCDateTime = device_time.UTCDateTime + # Retrieve timezone from system + dt_param.TimeZone = str(system_date.astimezone().tzinfo) + dt_param.UTCDateTime.Date.Year = system_date.year + dt_param.UTCDateTime.Date.Month = system_date.month + dt_param.UTCDateTime.Date.Day = system_date.day + dt_param.UTCDateTime.Time.Hour = system_date.hour + dt_param.UTCDateTime.Time.Minute = system_date.minute + dt_param.UTCDateTime.Time.Second = system_date.second + LOGGER.debug("SetSystemDateAndTime: %s", dt_param) + await device_mgmt.SetSystemDateAndTime(dt_param) + async def async_check_date_and_time(self) -> None: """Warns if device and system date not synced.""" LOGGER.debug("Setting up the ONVIF device management service") @@ -165,6 +192,8 @@ class ONVIFDevice: ) return + LOGGER.debug("Device time: %s", device_time) + tzone = dt_util.DEFAULT_TIME_ZONE cdate = device_time.LocalDateTime if device_time.UTCDateTime: @@ -207,6 +236,9 @@ class ONVIFDevice: cam_date_utc, system_date, ) + if device_time.DateTimeType == "Manual": + # Set Date and Time ourselves if Date and Time is set manually in the camera. + await self.async_manually_set_date_and_time() except RequestError as err: LOGGER.warning( "Couldn't get device '%s' date/time. Error: %s", self.name, err diff --git a/tests/components/onvif/test_button.py b/tests/components/onvif/test_button.py index a8ac24da524..be418acd1e0 100644 --- a/tests/components/onvif/test_button.py +++ b/tests/components/onvif/test_button.py @@ -38,3 +38,33 @@ async def test_reboot_button_press(hass): await hass.async_block_till_done() devicemgmt.SystemReboot.assert_called_once() + + +async def test_set_dateandtime_button(hass): + """Test states of the SetDateAndTime button.""" + await setup_onvif_integration(hass) + + state = hass.states.get("button.testcamera_set_system_date_and_time") + assert state + assert state.state == STATE_UNKNOWN + + registry = er.async_get(hass) + entry = registry.async_get("button.testcamera_set_system_date_and_time") + assert entry + assert entry.unique_id == f"{MAC}_setsystemdatetime" + + +async def test_set_dateandtime_button_press(hass): + """Test SetDateAndTime button press.""" + _, camera, device = await setup_onvif_integration(hass) + device.async_manually_set_date_and_time = AsyncMock(return_value=True) + + await hass.services.async_call( + BUTTON_DOMAIN, + "press", + {ATTR_ENTITY_ID: "button.testcamera_set_system_date_and_time"}, + blocking=True, + ) + await hass.async_block_till_done() + + device.async_manually_set_date_and_time.assert_called_once() From c01f7d75d5ec8355a14f74fee1ded20a7340b624 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 16:59:49 +0200 Subject: [PATCH 2240/3516] Fix mix of aiohttp and requests in ZAMG (#74628) --- homeassistant/components/zamg/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 87a1175b7cd..c32aa942625 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -10,7 +10,6 @@ import logging import os from typing import Union -from aiohttp.hdrs import USER_AGENT import requests import voluptuous as vol @@ -275,7 +274,7 @@ class ZamgData: """The class for handling the data retrieval.""" API_URL = "http://www.zamg.ac.at/ogd/" - API_HEADERS = {USER_AGENT: f"home-assistant.zamg/ {__version__}"} + API_HEADERS = {"User-Agent": f"home-assistant.zamg/ {__version__}"} def __init__(self, station_id): """Initialize the probe.""" From 323d4a0e1bf08f1bc3c5fc627889b4aaafa7d5c5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 17:25:23 +0200 Subject: [PATCH 2241/3516] Use FlowResultType enum in Plugwise tests (#74638) --- tests/components/plugwise/test_config_flow.py | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index f2a2ef3bc43..66f0986682e 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -21,11 +21,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -88,7 +84,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" assert "flow_id" in result @@ -102,7 +98,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Test Smile Name" assert result2.get("data") == { CONF_HOST: TEST_HOST, @@ -136,7 +132,7 @@ async def test_zeroconf_flow( context={CONF_SOURCE: SOURCE_ZEROCONF}, data=discovery, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" assert "flow_id" in result @@ -147,7 +143,7 @@ async def test_zeroconf_flow( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Test Smile Name" assert result2.get("data") == { CONF_HOST: TEST_HOST, @@ -172,7 +168,7 @@ async def test_zeroconf_flow_stretch( context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY2, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" assert "flow_id" in result @@ -183,7 +179,7 @@ async def test_zeroconf_flow_stretch( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Test Smile Name" assert result2.get("data") == { CONF_HOST: TEST_HOST, @@ -224,7 +220,7 @@ async def test_zercoconf_discovery_update_configuration( context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" assert entry.data[CONF_HOST] == "0.0.0.0" @@ -235,7 +231,7 @@ async def test_zercoconf_discovery_update_configuration( data=TEST_DISCOVERY, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" assert entry.data[CONF_HOST] == "1.1.1.1" @@ -262,7 +258,7 @@ async def test_flow_errors( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" assert "flow_id" in result @@ -273,7 +269,7 @@ async def test_flow_errors( user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("errors") == {"base": reason} assert result2.get("step_id") == "user" @@ -286,7 +282,7 @@ async def test_flow_errors( user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "Test Smile Name" assert result3.get("data") == { CONF_HOST: TEST_HOST, From 1dd9e705f2ea696b1cf8cbee178c374455c04384 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Jul 2022 10:39:05 -0500 Subject: [PATCH 2242/3516] Switch dispatcher to use async_run_hass_job (#74514) * Switch dispatcher to use async_run_hass_job - Since we already wrap all the callbacks in catch_log_exception we can use async_run_hass_job here - The overhead of wrapping the call in a call_soon, queuing it and running it later usually exceeds the overhead of running the job itself * fix size change during iteration * fix out of order send * fix missing mocking in unifi test * Fix Legrand Home+ Control updating entities before the coordinator update had finished * stray debug --- .../components/hassio/websocket_api.py | 2 +- .../components/home_plus_control/__init__.py | 34 +++++++++++-------- homeassistant/helpers/dispatcher.py | 12 ++++++- tests/components/unifi/test_controller.py | 17 +++++++++- 4 files changed, 48 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index b25d9fda7a0..eb0d6c54077 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -86,8 +86,8 @@ async def websocket_supervisor_event( hass: HomeAssistant, connection: ActiveConnection, msg: dict ): """Publish events from the Supervisor.""" - async_dispatcher_send(hass, EVENT_SUPERVISOR_EVENT, msg[ATTR_DATA]) connection.send_result(msg[WS_ID]) + async_dispatcher_send(hass, EVENT_SUPERVISOR_EVENT, msg[ATTR_DATA]) @websocket_api.websocket_command( diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index e222b65e293..fbae1ae970a 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( config_entry_oauth2_flow, config_validation as cv, @@ -102,12 +102,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Note: asyncio.TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. async with async_timeout.timeout(10): - module_data = await api.async_get_modules() + return await api.async_get_modules() except HomePlusControlApiError as err: raise UpdateFailed( f"Error communicating with API: {err} [{type(err)}]" ) from err + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="home_plus_control_module", + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=300), + ) + hass_entry_data[DATA_COORDINATOR] = coordinator + + @callback + def _async_update_entities(): + """Process entities and add or remove them based after an update.""" + if not (module_data := coordinator.data): + return + # Remove obsolete entities from Home Assistant entity_uids_to_remove = uids - set(module_data) for uid in entity_uids_to_remove: @@ -126,18 +143,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator, ) - return module_data - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - # Name of the data. For logging purposes. - name="home_plus_control_module", - update_method=async_update_data, - # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=300), - ) - hass_entry_data[DATA_COORDINATOR] = coordinator + entry.async_on_unload(coordinator.async_add_listener(_async_update_entities)) async def start_platforms(): """Continue setting up the platforms.""" diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index 12a6616b009..07a3e3b3b28 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -85,8 +85,18 @@ def async_dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: This method must be run in the event loop. """ target_list = hass.data.get(DATA_DISPATCHER, {}).get(signal, {}) + + run: list[HassJob] = [] for target, job in target_list.items(): if job is None: job = _generate_job(signal, target) target_list[target] = job - hass.async_add_hass_job(job, *args) + + # Run the jobs all at the end + # to ensure no jobs add more disptachers + # which can result in the target_list + # changing size during iteration + run.append(job) + + for job in run: + hass.async_run_hass_job(job, *args) diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 8a41ada9b62..625afbb4ec6 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -379,7 +379,21 @@ async def test_wireless_client_event_calls_update_wireless_devices( hass, aioclient_mock, mock_unifi_websocket ): """Call update_wireless_devices method when receiving wireless client event.""" - await setup_unifi_integration(hass, aioclient_mock) + client_1_dict = { + "essid": "ssid", + "disabled": False, + "hostname": "client_1", + "ip": "10.0.0.4", + "is_wired": False, + "last_seen": dt_util.as_timestamp(dt_util.utcnow()), + "mac": "00:00:00:00:00:01", + } + await setup_unifi_integration( + hass, + aioclient_mock, + clients_response=[client_1_dict], + known_wireless_clients=(client_1_dict["mac"],), + ) with patch( "homeassistant.components.unifi.controller.UniFiController.update_wireless_clients", @@ -391,6 +405,7 @@ async def test_wireless_client_event_calls_update_wireless_devices( "data": [ { "datetime": "2020-01-20T19:37:04Z", + "user": "00:00:00:00:00:01", "key": aiounifi.events.WIRELESS_CLIENT_CONNECTED, "msg": "User[11:22:33:44:55:66] has connected to WLAN", "time": 1579549024893, From 68ccb9608973262309ed4f7709bc27d03447b313 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 18:53:24 +0200 Subject: [PATCH 2243/3516] Refactor CI (#74014) --- .github/workflows/ci.yaml | 459 +++++++++--------- tests/components/homekit/test_type_cameras.py | 1 - tests/components/motioneye/test_camera.py | 8 +- tests/components/stream/test_recorder.py | 8 +- tests/components/stream/test_worker.py | 10 +- 5 files changed, 241 insertions(+), 245 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1d7a5c95986..6a4413e3668 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,8 +20,8 @@ on: type: boolean env: - CACHE_VERSION: 10 - PIP_CACHE_VERSION: 4 + CACHE_VERSION: 0 + PIP_CACHE_VERSION: 0 HA_SHORT_VERSION: 2022.8 DEFAULT_PYTHON: 3.9 PRE_COMMIT_CACHE: ~/.cache/pre-commit @@ -35,24 +35,38 @@ concurrency: cancel-in-progress: true jobs: - changes: - name: Determine what has changed + info: + name: Collect information & changes data outputs: # In case of issues with the partial run, use the following line instead: # test_full_suite: 'true' - test_full_suite: ${{ steps.info.outputs.test_full_suite }} core: ${{ steps.core.outputs.changes }} - integrations: ${{ steps.integrations.outputs.changes }} integrations_glob: ${{ steps.info.outputs.integrations_glob }} - tests: ${{ steps.info.outputs.tests }} - tests_glob: ${{ steps.info.outputs.tests_glob }} - test_groups: ${{ steps.info.outputs.test_groups }} - test_group_count: ${{ steps.info.outputs.test_group_count }} + integrations: ${{ steps.integrations.outputs.changes }} + pre-commit_cache_key: ${{ steps.generate_pre-commit_cache_key.outputs.key }} + python_cache_key: ${{ steps.generate_python_cache_key.outputs.key }} requirements: ${{ steps.core.outputs.requirements }} - runs-on: ubuntu-latest + test_full_suite: ${{ steps.info.outputs.test_full_suite }} + test_group_count: ${{ steps.info.outputs.test_group_count }} + test_groups: ${{ steps.info.outputs.test_groups }} + tests_glob: ${{ steps.info.outputs.tests_glob }} + tests: ${{ steps.info.outputs.tests }} + runs-on: ubuntu-20.04 steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 + - name: Generate partial Python venv restore key + id: generate_python_cache_key + run: >- + echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('requirements_all.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }}" + - name: Generate partial pre-commit restore key + id: generate_pre-commit_cache_key + run: >- + echo "::set-output name=key::${{ env.CACHE_VERSION }}-${{ env.DEFAULT_PYTHON }}-${{ + hashFiles('.pre-commit-config.yaml') }}" - name: Filter for core changes uses: dorny/paths-filter@v2.10.2 id: core @@ -142,15 +156,11 @@ jobs: echo "tests_glob: ${tests_glob}" echo "::set-output name=tests_glob::${tests_glob}" - # Separate job to pre-populate the base dependency cache - # This prevent upcoming jobs to do the same individually - prepare-base: - name: Prepare base dependencies - runs-on: ubuntu-latest - timeout-minutes: 20 - outputs: - python-key: ${{ steps.generate-python-key.outputs.key }} - pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }} + pre-commit: + name: Prepare pre-commit base + runs-on: ubuntu-20.04 + needs: + - info steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 @@ -159,67 +169,26 @@ jobs: uses: actions/setup-python@v4.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - - name: Generate partial Python venv restore key - id: generate-python-key - run: >- - echo "::set-output name=key::base-venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('requirements.txt') }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }}" - - name: Generate partial pip restore key - id: generate-pip-key - run: >- - echo "::set-output name=key::base-pip-${{ env.PIP_CACHE_VERSION }}-${{ - env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" + cache: "pip" - name: Restore base Python virtual environment id: cache-venv uses: actions/cache@v3.0.4 with: path: venv - key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - steps.generate-python-key.outputs.key }} - # Temporary disabling the restore of environments when bumping - # a dependency. It seems that we are experiencing issues with - # restoring environments in GitHub Actions, although unclear why. - # First attempt: https://github.com/home-assistant/core/pull/62383 - # - # restore-keys: | - # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}- - # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements.txt') }}- - # ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-venv-${{ env.CACHE_VERSION }}- - - name: Restore pip wheel cache - if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.4 - with: - path: ${{ env.PIP_CACHE }} - key: >- - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - steps.generate-pip-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ steps.python.outputs.python-version }}-base-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- + key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Create Python virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | python -m venv venv . venv/bin/activate python --version - pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.2" setuptools wheel - pip install --cache-dir=$PIP_CACHE -r requirements.txt -r requirements_test.txt --use-deprecated=legacy-resolver - - name: Generate partial pre-commit restore key - id: generate-pre-commit-key - run: >- - echo "::set-output name=key::pre-commit-${{ env.CACHE_VERSION }}-${{ - hashFiles('.pre-commit-config.yaml') }}" + pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit uses: actions/cache@v3.0.4 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: >- - ${{ runner.os }}-${{ steps.generate-pre-commit-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-pre-commit-${{ env.CACHE_VERSION }}- + key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} - name: Install pre-commit dependencies if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -228,10 +197,10 @@ jobs: lint-black: name: Check black - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: - - changes - - prepare-base + - info + - pre-commit steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 @@ -245,8 +214,7 @@ jobs: uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} + key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -257,31 +225,31 @@ jobs: uses: actions/cache@v3.0.4 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if pre-commit cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true' run: | echo "Failed to restore pre-commit environment from cache" exit 1 - name: Run black (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate pre-commit run --hook-stage manual black --all-files --show-diff-on-failure - name: Run black (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate shopt -s globstar - pre-commit run --hook-stage manual black --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/**/* --show-diff-on-failure + pre-commit run --hook-stage manual black --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/**/* --show-diff-on-failure lint-flake8: name: Check flake8 - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: - - changes - - prepare-base + - info + - pre-commit steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 @@ -295,8 +263,7 @@ jobs: uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} + key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -307,7 +274,7 @@ jobs: uses: actions/cache@v3.0.4 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if pre-commit cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -317,22 +284,24 @@ jobs: run: | echo "::add-matcher::.github/workflows/matchers/flake8.json" - name: Run flake8 (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate pre-commit run --hook-stage manual flake8 --all-files - name: Run flake8 (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate shopt -s globstar - pre-commit run --hook-stage manual flake8 --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/**/* + pre-commit run --hook-stage manual flake8 --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/**/* lint-isort: name: Check isort - runs-on: ubuntu-latest - needs: prepare-base + runs-on: ubuntu-20.04 + needs: + - info + - pre-commit steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 @@ -346,8 +315,7 @@ jobs: uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} + key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -358,7 +326,7 @@ jobs: uses: actions/cache@v3.0.4 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if pre-commit cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -371,10 +339,10 @@ jobs: lint-other: name: Check other linters - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: - - changes - - prepare-base + - info + - pre-commit steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 @@ -388,8 +356,7 @@ jobs: uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} + key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -400,7 +367,7 @@ jobs: uses: actions/cache@v3.0.4 with: path: ${{ env.PRE_COMMIT_CACHE }} - key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} - name: Fail job if pre-commit cache restore failed if: steps.cache-precommit.outputs.cache-hit != 'true' run: | @@ -408,17 +375,17 @@ jobs: exit 1 - name: Run pyupgrade (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate pre-commit run --hook-stage manual pyupgrade --all-files --show-diff-on-failure - name: Run pyupgrade (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate shopt -s globstar - pre-commit run --hook-stage manual pyupgrade --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/**/* --show-diff-on-failure + pre-commit run --hook-stage manual pyupgrade --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/**/* --show-diff-on-failure - name: Register yamllint problem matcher run: | @@ -437,17 +404,17 @@ jobs: pre-commit run --hook-stage manual check-json --all-files - name: Run prettier (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate pre-commit run --hook-stage manual prettier --all-files - name: Run prettier (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate - pre-commit run --hook-stage manual prettier --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/**/* + pre-commit run --hook-stage manual prettier --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/**/* - name: Register check executables problem matcher run: | @@ -478,36 +445,105 @@ jobs: args: hadolint Dockerfile.dev - name: Run bandit (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate pre-commit run --hook-stage manual bandit --all-files --show-diff-on-failure - name: Run bandit (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate shopt -s globstar - pre-commit run --hook-stage manual bandit --files {homeassistant,tests}/components/${{ needs.changes.outputs.integrations_glob }}/**/* --show-diff-on-failure + pre-commit run --hook-stage manual bandit --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/**/* --show-diff-on-failure - hassfest: - name: Check hassfest - runs-on: ubuntu-latest - needs: prepare-tests + base: + name: Prepare dependencies + runs-on: ubuntu-20.04 + needs: info + timeout-minutes: 60 strategy: matrix: - python-version: [3.9] - container: homeassistant/ci-azure:${{ matrix.python-version }} + python-version: ["3.9", "3.10"] steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - - name: Restore full Python ${{ matrix.python-version }} virtual environment + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v4.0.0 + with: + python-version: ${{ matrix.python-version }} + - name: Generate partial pip restore key + id: generate-pip-key + run: >- + echo "::set-output name=key::pip-${{ env.PIP_CACHE_VERSION }}-${{ + env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" + - name: Restore base Python virtual environment id: cache-venv uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ - needs.prepare-tests.outputs.python-key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} + - name: Restore pip wheel cache + if: steps.cache-venv.outputs.cache-hit != 'true' + uses: actions/cache@v3.0.4 + with: + path: ${{ env.PIP_CACHE }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + steps.generate-pip-key.outputs.key }} + restore-keys: | + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- + - name: Install additional OS dependencies + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + sudo apt-get update + sudo apt-get -y install \ + bluez \ + ffmpeg \ + libavcodec-dev \ + libavdevice-dev \ + libavfilter-dev \ + libavformat-dev \ + libavutil-dev \ + libswresample-dev \ + libswscale-dev \ + libudev-dev + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + python -m venv venv + . venv/bin/activate + python --version + pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.2" setuptools wheel + pip install --cache-dir=$PIP_CACHE -r requirements_all.txt --use-deprecated=legacy-resolver + pip install --cache-dir=$PIP_CACHE -r requirements_test.txt --use-deprecated=legacy-resolver + pip install -e . + + hassfest: + name: Check hassfest + runs-on: ubuntu-20.04 + needs: + - info + - base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.0.2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v4.0.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment + id: cache-venv + uses: actions/cache@v3.0.4 + with: + path: venv + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -520,14 +556,16 @@ jobs: gen-requirements-all: name: Check all requirements - runs-on: ubuntu-latest - needs: prepare-base + runs-on: ubuntu-20.04 + needs: + - info + - base steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 id: python + uses: actions/setup-python@v4.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment @@ -535,8 +573,9 @@ jobs: uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ - needs.prepare-base.outputs.python-key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -547,94 +586,29 @@ jobs: . venv/bin/activate python -m script.gen_requirements_all validate - prepare-tests: - name: Prepare tests for Python ${{ matrix.python-version }} - runs-on: ubuntu-latest - timeout-minutes: 60 - strategy: - matrix: - python-version: ["3.9", "3.10"] - outputs: - python-key: ${{ steps.generate-python-key.outputs.key }} - container: homeassistant/ci-azure:${{ matrix.python-version }} - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3.0.2 - - name: Generate partial Python venv restore key - id: generate-python-key - run: >- - echo "::set-output name=key::venv-${{ env.CACHE_VERSION }}-${{ - hashFiles('requirements_test.txt') }}-${{ - hashFiles('requirements_all.txt') }}-${{ - hashFiles('homeassistant/package_constraints.txt') }}" - - name: Generate partial pip restore key - id: generate-pip-key - run: >- - echo "::set-output name=key::pip-${{ env.PIP_CACHE_VERSION }}-${{ - env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - - name: Restore full Python ${{ matrix.python-version }} virtual environment - id: cache-venv - uses: actions/cache@v3.0.4 - with: - path: venv - key: >- - ${{ runner.os }}-${{ matrix.python-version }}-${{ - steps.generate-python-key.outputs.key }} - # Temporary disabling the restore of environments when bumping - # a dependency. It seems that we are experiencing issues with - # restoring environments in GitHub Actions, although unclear why. - # First attempt: https://github.com/home-assistant/core/pull/62383 - # - # restore-keys: | - # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }}- - # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}-${{ hashFiles('requirements_test.txt') }}- - # ${{ runner.os }}-${{ matrix.python-version }}-venv-${{ env.CACHE_VERSION }}- - - name: Restore pip wheel cache - if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.4 - with: - path: ${{ env.PIP_CACHE }} - key: >- - ${{ runner.os }}-${{ matrix.python-version }}-${{ - steps.generate-pip-key.outputs.key }} - restore-keys: | - ${{ runner.os }}-${{ matrix.python-version }}-pip-${{ env.PIP_CACHE_VERSION }}-${{ env.HA_SHORT_VERSION }}- - - name: Create full Python ${{ matrix.python-version }} virtual environment - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - # Temporary addition of cmake, needed to build some Python 3.9 packages - apt-get update - apt-get -y install cmake - - python -m venv venv - . venv/bin/activate - python --version - pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.2" setuptools wheel - pip install --cache-dir=$PIP_CACHE -r requirements_all.txt --use-deprecated=legacy-resolver - pip install --cache-dir=$PIP_CACHE -r requirements_test.txt --use-deprecated=legacy-resolver - pip install -e . - pylint: name: Check pylint - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 timeout-minutes: 20 needs: - - changes - - prepare-tests - strategy: - matrix: - python-version: [3.9] - container: homeassistant/ci-azure:${{ matrix.python-version }} + - info + - base steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - - name: Restore full Python ${{ matrix.python-version }} virtual environment + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v4.0.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ - needs.prepare-tests.outputs.python-key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -644,39 +618,41 @@ jobs: run: | echo "::add-matcher::.github/workflows/matchers/pylint.json" - name: Run pylint (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate python --version pylint --ignore-missing-annotations=y homeassistant - name: Run pylint (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate python --version - pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.changes.outputs.integrations_glob }} + pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.info.outputs.integrations_glob }} mypy: name: Check mypy - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: - - changes - - prepare-tests - strategy: - matrix: - python-version: [3.9] - container: homeassistant/ci-azure:${{ matrix.python-version }} + - info + - base steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - - name: Restore full Python ${{ matrix.python-version }} virtual environment + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v4.0.0 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ - needs.prepare-tests.outputs.python-key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -686,41 +662,46 @@ jobs: run: | echo "::add-matcher::.github/workflows/matchers/mypy.json" - name: Run mypy (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' run: | . venv/bin/activate python --version mypy homeassistant pylint - name: Run mypy (partially) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' shell: bash run: | . venv/bin/activate python --version - mypy homeassistant/components/${{ needs.changes.outputs.integrations_glob }} + mypy homeassistant/components/${{ needs.info.outputs.integrations_glob }} pip-check: - runs-on: ubuntu-latest - if: needs.changes.outputs.requirements == 'true' || github.event.inputs.full == 'true' + runs-on: ubuntu-20.04 + if: needs.info.outputs.requirements == 'true' || github.event.inputs.full == 'true' needs: - - changes - - prepare-tests + - info + - base strategy: fail-fast: false matrix: - python-version: [3.9] + python-version: ["3.9", "3.10"] name: Run pip check ${{ matrix.python-version }} - container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v4.0.0 + with: + python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ - needs.prepare-tests.outputs.python-key }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -732,38 +713,48 @@ jobs: ./script/pip_check $PIP_CACHE pytest: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 if: | (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') && github.event.inputs.lint-only != 'true' - && (needs.changes.outputs.test_full_suite == 'true' || needs.changes.outputs.tests_glob) + && (needs.info.outputs.test_full_suite == 'true' || needs.info.outputs.tests_glob) needs: - - changes + - info + - base - gen-requirements-all - hassfest - lint-black - lint-other - lint-isort - mypy - - prepare-tests strategy: fail-fast: false matrix: - group: ${{ fromJson(needs.changes.outputs.test_groups) }} + group: ${{ fromJson(needs.info.outputs.test_groups) }} python-version: ["3.9", "3.10"] name: >- Run tests Python ${{ matrix.python-version }} (${{ matrix.group }}) - container: homeassistant/ci-azure:${{ matrix.python-version }} steps: + - name: Install additional OS dependencies + run: | + sudo apt-get update + sudo apt-get -y install \ + bluez \ + ffmpeg - name: Check out code from GitHub uses: actions/checkout@v3.0.2 + - name: Set up Python ${{ matrix.python-version }} + id: python + uses: actions/setup-python@v4.0.0 + with: + python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v3.0.4 with: path: venv - key: ${{ runner.os }}-${{ matrix.python-version }}-${{ - needs.prepare-tests.outputs.python-key }} + key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.python_cache_key }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -783,7 +774,7 @@ jobs: run: | echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" - name: Run pytest (fully) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' timeout-minutes: 60 run: | . venv/bin/activate @@ -794,7 +785,7 @@ jobs: --durations=10 \ -n auto \ --dist=loadfile \ - --test-group-count ${{ needs.changes.outputs.test_group_count }} \ + --test-group-count ${{ needs.info.outputs.test_group_count }} \ --test-group=${{ matrix.group }} \ --cov="homeassistant" \ --cov-report=xml \ @@ -802,8 +793,8 @@ jobs: -p no:sugar \ tests - name: Run pytest (partially) - if: needs.changes.outputs.test_full_suite == 'false' - timeout-minutes: 20 + if: needs.info.outputs.test_full_suite == 'false' + timeout-minutes: 10 shell: bash run: | . venv/bin/activate @@ -838,9 +829,9 @@ jobs: coverage: name: Upload test coverage to Codecov - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 needs: - - changes + - info - pytest steps: - name: Check out code from GitHub @@ -848,10 +839,10 @@ jobs: - name: Download all coverage artifacts uses: actions/download-artifact@v3 - name: Upload coverage to Codecov (full coverage) - if: needs.changes.outputs.test_full_suite == 'true' + if: needs.info.outputs.test_full_suite == 'true' uses: codecov/codecov-action@v3.1.0 with: flags: full-suite - name: Upload coverage to Codecov (partial coverage) - if: needs.changes.outputs.test_full_suite == 'false' + if: needs.info.outputs.test_full_suite == 'false' uses: codecov/codecov-action@v3.1.0 diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index 83afcedd839..f6855ca3cbb 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -501,7 +501,6 @@ async def test_camera_stream_source_configured_and_copy_codec(hass, run_driver, ): await _async_start_streaming(hass, acc) await _async_reconfigure_stream(hass, acc, session_info, {}) - await _async_stop_stream(hass, acc, session_info) await _async_stop_all_streams(hass, acc) expected_output = ( diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index 8ba9fb07715..3e2db3bf897 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -265,12 +265,12 @@ async def test_get_stream_from_camera(aiohttp_server: Any, hass: HomeAssistant) client = create_mock_motioneye_client() client.get_camera_stream_url = Mock( - return_value=f"http://localhost:{stream_server.port}/" + return_value=f"http://127.0.0.1:{stream_server.port}/" ) config_entry = create_mock_motioneye_config_entry( hass, data={ - CONF_URL: f"http://localhost:{stream_server.port}", + CONF_URL: f"http://127.0.0.1:{stream_server.port}", # The port won't be used as the client is a mock. CONF_SURVEILLANCE_USERNAME: TEST_SURVEILLANCE_USERNAME, }, @@ -351,13 +351,13 @@ async def test_camera_option_stream_url_template( config_entry = create_mock_motioneye_config_entry( hass, data={ - CONF_URL: f"http://localhost:{stream_server.port}", + CONF_URL: f"http://127.0.0.1:{stream_server.port}", # The port won't be used as the client is a mock. CONF_SURVEILLANCE_USERNAME: TEST_SURVEILLANCE_USERNAME, }, options={ CONF_STREAM_URL_TEMPLATE: ( - f"http://localhost:{stream_server.port}/" "{{ name }}/{{ id }}" + f"http://127.0.0.1:{stream_server.port}/" "{{ name }}/{{ id }}" ) }, ) diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index d7595b47679..a070f609129 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -71,7 +71,7 @@ async def test_record_stream(hass, filename, h264_video): assert os.path.exists(filename) -async def test_record_lookback(hass, h264_video): +async def test_record_lookback(hass, filename, h264_video): """Exercise record with loopback.""" stream = create_stream(hass, h264_video, {}) @@ -81,7 +81,7 @@ async def test_record_lookback(hass, h264_video): await stream.start() with patch.object(hass.config, "is_allowed_path", return_value=True): - await stream.async_record("/example/path", lookback=4) + await stream.async_record(filename, lookback=4) # This test does not need recorder cleanup since it is not fully exercised @@ -245,10 +245,10 @@ async def test_record_stream_audio( await hass.async_block_till_done() -async def test_recorder_log(hass, caplog): +async def test_recorder_log(hass, filename, caplog): """Test starting a stream to record logs the url without username and password.""" stream = create_stream(hass, "https://abcd:efgh@foo.bar", {}) with patch.object(hass.config, "is_allowed_path", return_value=True): - await stream.async_record("/example/path") + await stream.async_record(filename) assert "https://abcd:efgh@foo.bar" not in caplog.text assert "https://****:****@foo.bar" in caplog.text diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 8717e23a476..d887a165b44 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -71,6 +71,12 @@ SEGMENTS_PER_PACKET = PACKET_DURATION / SEGMENT_DURATION TIMEOUT = 15 +@pytest.fixture +def filename(tmpdir): + """Use this filename for the tests.""" + return f"{tmpdir}/test.mp4" + + @pytest.fixture(autouse=True) def mock_stream_settings(hass): """Set the stream settings data in hass before each test.""" @@ -895,7 +901,7 @@ async def test_h265_video_is_hvc1(hass, worker_finished_stream): } -async def test_get_image(hass): +async def test_get_image(hass, filename): """Test that the has_keyframe metadata matches the media.""" await async_setup_component(hass, "stream", {"stream": {}}) @@ -909,7 +915,7 @@ async def test_get_image(hass): stream = create_stream(hass, source, {}) with patch.object(hass.config, "is_allowed_path", return_value=True): - make_recording = hass.async_create_task(stream.async_record("/example/path")) + make_recording = hass.async_create_task(stream.async_record(filename)) await make_recording assert stream._keyframe_converter._image is None From 7cd68381f1d4f58930ffd631dfbfc7159d459832 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 18:57:36 +0200 Subject: [PATCH 2244/3516] Search/replace RESULT_TYPE_* by FlowResultType enum (#74642) --- .../auth/mfa_modules/test_insecure_example.py | 16 +- tests/auth/mfa_modules/test_notify.py | 34 ++-- tests/auth/mfa_modules/test_totp.py | 12 +- tests/auth/providers/test_command_line.py | 8 +- tests/auth/providers/test_homeassistant.py | 16 +- .../providers/test_legacy_api_password.py | 6 +- tests/auth/test_init.py | 30 ++-- tests/components/abode/test_config_flow.py | 14 +- tests/components/abode/test_init.py | 2 +- .../accuweather/test_config_flow.py | 8 +- tests/components/acmeda/test_config_flow.py | 8 +- tests/components/adguard/test_config_flow.py | 18 +- .../advantage_air/test_config_flow.py | 8 +- tests/components/aemet/test_config_flow.py | 12 +- .../components/agent_dvr/test_config_flow.py | 10 +- tests/components/airly/test_config_flow.py | 6 +- tests/components/airnow/test_config_flow.py | 4 +- .../components/airvisual/test_config_flow.py | 26 +-- tests/components/airzone/test_config_flow.py | 10 +- .../alarmdecoder/test_config_flow.py | 60 +++---- tests/components/almond/test_config_flow.py | 12 +- .../amberelectric/test_config_flow.py | 16 +- .../ambiclimate/test_config_flow.py | 18 +- .../ambient_station/test_config_flow.py | 8 +- .../components/androidtv/test_config_flow.py | 58 +++---- tests/components/apple_tv/test_config_flow.py | 102 ++++++------ .../application_credentials/test_init.py | 24 +-- .../components/arcam_fmj/test_config_flow.py | 20 +-- tests/components/asuswrt/test_config_flow.py | 22 +-- tests/components/atag/test_config_flow.py | 12 +- tests/components/aurora/test_config_flow.py | 4 +- .../aurora_abb_powerone/test_config_flow.py | 2 +- .../components/aussie_broadband/test_init.py | 2 +- tests/components/auth/test_mfa_setup_flow.py | 4 +- tests/components/awair/test_config_flow.py | 14 +- .../azure_devops/test_config_flow.py | 34 ++-- .../azure_event_hub/test_config_flow.py | 14 +- tests/components/balboa/test_config_flow.py | 4 +- tests/components/blebox/test_config_flow.py | 2 +- tests/components/blink/test_config_flow.py | 6 +- .../bmw_connected_drive/test_config_flow.py | 10 +- tests/components/braviatv/test_config_flow.py | 18 +- tests/components/brother/test_config_flow.py | 24 +-- tests/components/brunt/test_config_flow.py | 12 +- tests/components/bsblan/test_config_flow.py | 14 +- .../components/buienradar/test_config_flow.py | 2 +- tests/components/cast/test_config_flow.py | 14 +- .../cert_expiry/test_config_flow.py | 28 ++-- .../components/climacell/test_config_flow.py | 4 +- tests/components/cloud/test_account_link.py | 2 +- .../components/config/test_config_entries.py | 2 +- .../components/crownstone/test_config_flow.py | 50 +++--- tests/components/denonavr/test_config_flow.py | 4 +- tests/components/dexcom/test_config_flow.py | 18 +- tests/components/dialogflow/test_init.py | 4 +- tests/components/discord/test_config_flow.py | 22 +-- tests/components/dlna_dmr/test_config_flow.py | 70 ++++---- tests/components/dlna_dms/test_config_flow.py | 36 ++-- tests/components/doorbird/test_config_flow.py | 12 +- tests/components/dsmr/test_config_flow.py | 2 +- tests/components/dunehd/test_config_flow.py | 4 +- tests/components/ecobee/test_config_flow.py | 16 +- tests/components/elmax/test_config_flow.py | 26 +-- tests/components/enocean/test_config_flow.py | 20 +-- .../environment_canada/test_config_flow.py | 6 +- .../components/faa_delays/test_config_flow.py | 2 +- .../fireservicerota/test_config_flow.py | 10 +- .../flick_electric/test_config_flow.py | 10 +- tests/components/flipr/test_config_flow.py | 8 +- .../components/flunearyou/test_config_flow.py | 6 +- .../forked_daapd/test_config_flow.py | 24 +-- tests/components/foscam/test_config_flow.py | 24 +-- tests/components/freebox/test_config_flow.py | 20 +-- .../components/freedompro/test_config_flow.py | 4 +- tests/components/gdacs/test_config_flow.py | 8 +- tests/components/generic/test_config_flow.py | 52 +++--- tests/components/geofency/test_init.py | 4 +- .../geonetnz_quakes/test_config_flow.py | 8 +- .../geonetnz_volcano/test_config_flow.py | 6 +- tests/components/gios/test_config_flow.py | 4 +- tests/components/glances/test_config_flow.py | 6 +- tests/components/goalzero/test_config_flow.py | 22 +-- .../google_travel_time/test_config_flow.py | 24 +-- tests/components/gpslogger/test_init.py | 4 +- tests/components/gree/test_config_flow.py | 8 +- .../growatt_server/test_config_flow.py | 10 +- tests/components/guardian/test_config_flow.py | 24 +-- tests/components/hangouts/test_config_flow.py | 16 +- tests/components/harmony/test_config_flow.py | 4 +- tests/components/heos/test_config_flow.py | 14 +- .../here_travel_time/test_config_flow.py | 42 ++--- tests/components/hisense_aehw4a1/test_init.py | 4 +- tests/components/hive/test_config_flow.py | 74 ++++----- .../home_connect/test_config_flow.py | 2 +- .../home_plus_control/test_config_flow.py | 12 +- tests/components/homekit/test_config_flow.py | 154 +++++++++--------- .../components/honeywell/test_config_flow.py | 8 +- .../components/huawei_lte/test_config_flow.py | 16 +- .../components/huisbaasje/test_config_flow.py | 10 +- .../hvv_departures/test_config_flow.py | 8 +- tests/components/hyperion/test_config_flow.py | 82 +++++----- tests/components/ialarm/test_config_flow.py | 10 +- tests/components/icloud/test_config_flow.py | 34 ++-- tests/components/ifttt/test_init.py | 4 +- tests/components/insteon/test_config_flow.py | 38 ++--- tests/components/iqvia/test_config_flow.py | 8 +- .../islamic_prayer_times/test_config_flow.py | 10 +- tests/components/iss/test_config_flow.py | 8 +- tests/components/isy994/test_config_flow.py | 54 +++--- tests/components/izone/test_config_flow.py | 8 +- tests/components/jellyfin/test_config_flow.py | 2 +- .../keenetic_ndms2/test_config_flow.py | 26 +-- tests/components/kmtronic/test_config_flow.py | 4 +- .../launch_library/test_config_flow.py | 6 +- tests/components/lcn/test_config_flow.py | 6 +- tests/components/life360/test_config_flow.py | 18 +- tests/components/litejet/test_config_flow.py | 4 +- tests/components/local_ip/test_config_flow.py | 6 +- tests/components/locative/test_init.py | 4 +- .../logi_circle/test_config_flow.py | 20 +-- .../lutron_caseta/test_config_flow.py | 8 +- tests/components/lyric/test_config_flow.py | 8 +- tests/components/mailgun/test_init.py | 8 +- tests/components/mazda/test_config_flow.py | 36 ++-- tests/components/meater/test_config_flow.py | 10 +- .../met_eireann/test_config_flow.py | 6 +- .../meteo_france/test_config_flow.py | 22 +-- .../meteoclimatic/test_config_flow.py | 8 +- tests/components/mikrotik/test_config_flow.py | 12 +- .../modem_callerid/test_config_flow.py | 20 +-- .../components/monoprice/test_config_flow.py | 4 +- .../motion_blinds/test_config_flow.py | 4 +- .../components/motioneye/test_config_flow.py | 32 ++-- tests/components/mqtt/test_config_flow.py | 30 ++-- tests/components/nam/test_config_flow.py | 36 ++-- tests/components/neato/test_config_flow.py | 6 +- .../nest/test_config_flow_legacy.py | 32 ++-- tests/components/netatmo/test_config_flow.py | 24 +-- tests/components/netgear/test_config_flow.py | 32 ++-- tests/components/nextdns/test_config_flow.py | 8 +- .../nfandroidtv/test_config_flow.py | 8 +- .../components/nightscout/test_config_flow.py | 12 +- tests/components/nina/test_config_flow.py | 26 +-- .../nmap_tracker/test_config_flow.py | 4 +- tests/components/notion/test_config_flow.py | 10 +- tests/components/nuki/test_config_flow.py | 34 ++-- tests/components/nut/test_config_flow.py | 38 ++--- .../components/octoprint/test_config_flow.py | 8 +- .../components/omnilogic/test_config_flow.py | 4 +- .../components/ondilo_ico/test_config_flow.py | 2 +- tests/components/onvif/test_config_flow.py | 34 ++-- .../opentherm_gw/test_config_flow.py | 12 +- tests/components/openuv/test_config_flow.py | 12 +- .../openweathermap/test_config_flow.py | 12 +- tests/components/overkiz/test_config_flow.py | 20 +-- .../components/ovo_energy/test_config_flow.py | 26 +-- .../components/owntracks/test_config_flow.py | 12 +- .../components/philips_js/test_config_flow.py | 4 +- tests/components/picnic/test_config_flow.py | 18 +- tests/components/plaato/test_config_flow.py | 8 +- tests/components/point/test_config_flow.py | 24 +-- .../components/poolsense/test_config_flow.py | 4 +- tests/components/ps4/test_config_flow.py | 82 +++++----- tests/components/ps4/test_init.py | 4 +- .../pvpc_hourly_pricing/test_config_flow.py | 14 +- tests/components/qnap_qsw/test_config_flow.py | 12 +- .../components/radiotherm/test_config_flow.py | 22 +-- .../rainmachine/test_config_flow.py | 22 +-- .../recollect_waste/test_config_flow.py | 12 +- tests/components/renault/test_config_flow.py | 26 +-- tests/components/rfxtrx/test_config_flow.py | 16 +- tests/components/risco/test_config_flow.py | 8 +- tests/components/roomba/test_config_flow.py | 72 ++++---- tests/components/roon/test_config_flow.py | 2 +- tests/components/sabnzbd/test_config_flow.py | 4 +- tests/components/shelly/test_config_flow.py | 58 +++---- .../shopping_list/test_config_flow.py | 6 +- tests/components/sia/test_config_flow.py | 14 +- .../components/simplepush/test_config_flow.py | 12 +- .../components/simplisafe/test_config_flow.py | 56 +++---- tests/components/slack/test_config_flow.py | 16 +- tests/components/sleepiq/test_config_flow.py | 8 +- tests/components/smappee/test_config_flow.py | 64 ++++---- .../smartthings/test_config_flow.py | 102 ++++++------ tests/components/smarttub/test_config_flow.py | 8 +- .../components/solaredge/test_config_flow.py | 16 +- tests/components/solarlog/test_config_flow.py | 18 +- tests/components/soma/test_config_flow.py | 14 +- .../somfy_mylink/test_config_flow.py | 10 +- tests/components/sonos/test_init.py | 4 +- .../speedtestdotnet/test_config_flow.py | 16 +- tests/components/spider/test_config_flow.py | 10 +- tests/components/spotify/test_config_flow.py | 14 +- .../components/srp_energy/test_config_flow.py | 6 +- .../steam_online/test_config_flow.py | 38 ++--- .../components/syncthing/test_config_flow.py | 10 +- tests/components/syncthru/test_config_flow.py | 12 +- .../synology_dsm/test_config_flow.py | 46 +++--- tests/components/synology_dsm/test_init.py | 2 +- .../system_bridge/test_config_flow.py | 56 +++---- tests/components/tautulli/test_config_flow.py | 26 +-- .../tellduslive/test_config_flow.py | 36 ++-- tests/components/tile/test_config_flow.py | 14 +- .../components/tomorrowio/test_config_flow.py | 26 +-- tests/components/toon/test_config_flow.py | 18 +- .../totalconnect/test_config_flow.py | 24 +-- tests/components/traccar/test_init.py | 4 +- tests/components/tradfri/test_config_flow.py | 20 +-- .../transmission/test_config_flow.py | 20 +-- tests/components/twilio/test_init.py | 4 +- tests/components/unifi/test_config_flow.py | 42 ++--- tests/components/upcloud/test_config_flow.py | 10 +- tests/components/upnp/test_config_flow.py | 30 ++-- tests/components/velbus/test_config_flow.py | 20 +-- tests/components/vera/test_config_flow.py | 4 +- tests/components/vesync/test_config_flow.py | 8 +- tests/components/vicare/test_config_flow.py | 14 +- tests/components/vilfo/test_config_flow.py | 14 +- tests/components/vizio/test_config_flow.py | 118 +++++++------- tests/components/vulcan/test_config_flow.py | 110 ++++++------- tests/components/wallbox/test_config_flow.py | 2 +- tests/components/watttime/test_config_flow.py | 4 +- .../waze_travel_time/test_config_flow.py | 22 +-- tests/components/wemo/test_config_flow.py | 2 +- tests/components/wiffi/test_config_flow.py | 4 +- tests/components/withings/common.py | 2 +- tests/components/wolflink/test_config_flow.py | 8 +- tests/components/ws66i/test_config_flow.py | 4 +- tests/components/xbox/test_config_flow.py | 2 +- .../xiaomi_miio/test_config_flow.py | 8 +- .../yamaha_musiccast/test_config_flow.py | 30 ++-- tests/components/yolink/test_config_flow.py | 10 +- tests/helpers/test_config_entry_flow.py | 52 +++--- .../helpers/test_config_entry_oauth2_flow.py | 32 ++-- .../helpers/test_helper_config_entry_flow.py | 26 +-- tests/test_config_entries.py | 60 +++---- tests/test_data_entry_flow.py | 34 ++-- 237 files changed, 2284 insertions(+), 2280 deletions(-) diff --git a/tests/auth/mfa_modules/test_insecure_example.py b/tests/auth/mfa_modules/test_insecure_example.py index 035433986d4..a182de6d01f 100644 --- a/tests/auth/mfa_modules/test_insecure_example.py +++ b/tests/auth/mfa_modules/test_insecure_example.py @@ -100,37 +100,37 @@ async def test_login(hass): provider = hass.auth.auth_providers[0] result = await hass.auth.login_flow.async_init((provider.type, provider.id)) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "incorrect-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "incorrect-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["data_schema"].schema.get("pin") == str result = await hass.auth.login_flow.async_configure( result["flow_id"], {"pin": "invalid-code"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_code" result = await hass.auth.login_flow.async_configure( result["flow_id"], {"pin": "123456"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"].id == "mock-id" @@ -147,9 +147,9 @@ async def test_setup_flow(hass): flow = await auth_module.async_setup_flow("new-user") result = await flow.async_step_init() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await flow.async_step_init({"pin": "abcdefg"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert auth_module._data[1]["user_id"] == "new-user" assert auth_module._data[1]["pin"] == "abcdefg" diff --git a/tests/auth/mfa_modules/test_notify.py b/tests/auth/mfa_modules/test_notify.py index 1d08ad70cc8..e69f11155a5 100644 --- a/tests/auth/mfa_modules/test_notify.py +++ b/tests/auth/mfa_modules/test_notify.py @@ -133,25 +133,25 @@ async def test_login_flow_validates_mfa(hass): provider = hass.auth.auth_providers[0] result = await hass.auth.login_flow.async_init((provider.type, provider.id)) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "incorrect-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "incorrect-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" with patch("pyotp.HOTP.at", return_value=MOCK_CODE): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["data_schema"].schema.get("code") == str @@ -170,7 +170,7 @@ async def test_login_flow_validates_mfa(hass): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"code": "invalid-code"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["errors"]["base"] == "invalid_code" @@ -187,7 +187,7 @@ async def test_login_flow_validates_mfa(hass): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"code": "invalid-code"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["errors"]["base"] == "invalid_code" @@ -195,7 +195,7 @@ async def test_login_flow_validates_mfa(hass): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"code": "invalid-code"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "too_many_retry" # wait service call finished @@ -203,13 +203,13 @@ async def test_login_flow_validates_mfa(hass): # restart login result = await hass.auth.login_flow.async_init((provider.type, provider.id)) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch("pyotp.HOTP.at", return_value=MOCK_CODE): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["data_schema"].schema.get("code") == str @@ -228,7 +228,7 @@ async def test_login_flow_validates_mfa(hass): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"code": MOCK_CODE} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"].id == "mock-id" @@ -243,14 +243,14 @@ async def test_setup_user_notify_service(hass): flow = await notify_auth_module.async_setup_flow("test-user") step = await flow.async_step_init() - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "init" schema = step["data_schema"] schema({"notify_service": "test2"}) with patch("pyotp.HOTP.at", return_value=MOCK_CODE): step = await flow.async_step_init({"notify_service": "test1"}) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "setup" # wait service call finished @@ -266,7 +266,7 @@ async def test_setup_user_notify_service(hass): with patch("pyotp.HOTP.at", return_value=MOCK_CODE_2): step = await flow.async_step_setup({"code": "invalid"}) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "setup" assert step["errors"]["base"] == "invalid_code" @@ -283,7 +283,7 @@ async def test_setup_user_notify_service(hass): with patch("pyotp.HOTP.verify", return_value=True): step = await flow.async_step_setup({"code": MOCK_CODE_2}) - assert step["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_include_exclude_config(hass): @@ -332,7 +332,7 @@ async def test_setup_user_no_notify_service(hass): flow = await notify_auth_module.async_setup_flow("test-user") step = await flow.async_step_init() - assert step["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert step["type"] == data_entry_flow.FlowResultType.ABORT assert step["reason"] == "no_available_service" @@ -369,13 +369,13 @@ async def test_not_raise_exception_when_service_not_exist(hass): provider = hass.auth.auth_providers[0] result = await hass.auth.login_flow.async_init((provider.type, provider.id)) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch("pyotp.HOTP.at", return_value=MOCK_CODE): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown_error" # wait service call finished diff --git a/tests/auth/mfa_modules/test_totp.py b/tests/auth/mfa_modules/test_totp.py index 2e4aad98066..53374e2d015 100644 --- a/tests/auth/mfa_modules/test_totp.py +++ b/tests/auth/mfa_modules/test_totp.py @@ -93,24 +93,24 @@ async def test_login_flow_validates_mfa(hass): provider = hass.auth.auth_providers[0] result = await hass.auth.login_flow.async_init((provider.type, provider.id)) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "incorrect-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "incorrect-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await hass.auth.login_flow.async_configure( result["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["data_schema"].schema.get("code") == str @@ -118,7 +118,7 @@ async def test_login_flow_validates_mfa(hass): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"code": "invalid-code"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" assert result["errors"]["base"] == "invalid_code" @@ -126,7 +126,7 @@ async def test_login_flow_validates_mfa(hass): result = await hass.auth.login_flow.async_configure( result["flow_id"], {"code": MOCK_CODE} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"].id == "mock-id" diff --git a/tests/auth/providers/test_command_line.py b/tests/auth/providers/test_command_line.py index e437ca9e331..22169812156 100644 --- a/tests/auth/providers/test_command_line.py +++ b/tests/auth/providers/test_command_line.py @@ -114,18 +114,18 @@ async def test_login_flow_validates(provider): """Test login flow.""" flow = await provider.async_login_flow({}) result = await flow.async_step_init() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await flow.async_step_init( {"username": "bad-user", "password": "bad-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await flow.async_step_init( {"username": "good-user", "password": "good-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["username"] == "good-user" @@ -135,5 +135,5 @@ async def test_strip_username(provider): result = await flow.async_step_init( {"username": "\t\ngood-user ", "password": "good-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["username"] == "good-user" diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index 62093df7210..e255135a0bd 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -115,24 +115,24 @@ async def test_login_flow_validates(data, hass): ) flow = await provider.async_login_flow({}) result = await flow.async_step_init() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await flow.async_step_init( {"username": "incorrect-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await flow.async_step_init( {"username": "TEST-user ", "password": "incorrect-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await flow.async_step_init( {"username": "test-USER", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["username"] == "test-USER" @@ -216,24 +216,24 @@ async def test_legacy_login_flow_validates(legacy_data, hass): ) flow = await provider.async_login_flow({}) result = await flow.async_step_init() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await flow.async_step_init( {"username": "incorrect-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await flow.async_step_init( {"username": "test-user", "password": "incorrect-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await flow.async_step_init( {"username": "test-user", "password": "test-pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["username"] == "test-user" diff --git a/tests/auth/providers/test_legacy_api_password.py b/tests/auth/providers/test_legacy_api_password.py index c77cb676a6b..19af0a1e746 100644 --- a/tests/auth/providers/test_legacy_api_password.py +++ b/tests/auth/providers/test_legacy_api_password.py @@ -55,15 +55,15 @@ async def test_verify_login(hass, provider): async def test_login_flow_works(hass, manager): """Test wrong config.""" result = await manager.login_flow.async_init(handler=("legacy_api_password", None)) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await manager.login_flow.async_configure( flow_id=result["flow_id"], user_input={"password": "not-hello"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_auth" result = await manager.login_flow.async_configure( flow_id=result["flow_id"], user_input={"password": "test-password"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index 22d720da587..49c2c776684 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -168,12 +168,12 @@ async def test_create_new_user(hass): ) step = await manager.login_flow.async_init(("insecure_example", None)) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM step = await manager.login_flow.async_configure( step["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert step["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY credential = step["result"] assert credential is not None @@ -237,12 +237,12 @@ async def test_login_as_existing_user(mock_hass): ) step = await manager.login_flow.async_init(("insecure_example", None)) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM step = await manager.login_flow.async_configure( step["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert step["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY credential = step["result"] user = await manager.async_get_user_by_credentials(credential) @@ -728,14 +728,14 @@ async def test_login_with_auth_module(mock_hass): ) step = await manager.login_flow.async_init(("insecure_example", None)) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM step = await manager.login_flow.async_configure( step["flow_id"], {"username": "test-user", "password": "test-pass"} ) # After auth_provider validated, request auth module input form - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "mfa" step = await manager.login_flow.async_configure( @@ -743,7 +743,7 @@ async def test_login_with_auth_module(mock_hass): ) # Invalid code error - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "mfa" assert step["errors"] == {"base": "invalid_code"} @@ -752,7 +752,7 @@ async def test_login_with_auth_module(mock_hass): ) # Finally passed, get credential - assert step["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert step["result"] assert step["result"].id == "mock-id" @@ -803,21 +803,21 @@ async def test_login_with_multi_auth_module(mock_hass): ) step = await manager.login_flow.async_init(("insecure_example", None)) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM step = await manager.login_flow.async_configure( step["flow_id"], {"username": "test-user", "password": "test-pass"} ) # After auth_provider validated, request select auth module - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "select_mfa_module" step = await manager.login_flow.async_configure( step["flow_id"], {"multi_factor_auth_module": "module2"} ) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "mfa" step = await manager.login_flow.async_configure( @@ -825,7 +825,7 @@ async def test_login_with_multi_auth_module(mock_hass): ) # Finally passed, get credential - assert step["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert step["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert step["result"] assert step["result"].id == "mock-id" @@ -871,13 +871,13 @@ async def test_auth_module_expired_session(mock_hass): ) step = await manager.login_flow.async_init(("insecure_example", None)) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM step = await manager.login_flow.async_configure( step["flow_id"], {"username": "test-user", "password": "test-pass"} ) - assert step["type"] == data_entry_flow.RESULT_TYPE_FORM + assert step["type"] == data_entry_flow.FlowResultType.FORM assert step["step_id"] == "mfa" with patch( @@ -888,7 +888,7 @@ async def test_auth_module_expired_session(mock_hass): step["flow_id"], {"pin": "test-pin"} ) # login flow abort due session timeout - assert step["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert step["type"] == data_entry_flow.FlowResultType.ABORT assert step["reason"] == "login_expired" diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py index adbea237d34..987a0b74996 100644 --- a/tests/components/abode/test_config_flow.py +++ b/tests/components/abode/test_config_flow.py @@ -23,7 +23,7 @@ async def test_show_form(hass: HomeAssistant) -> None: result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -39,7 +39,7 @@ async def test_one_config_allowed(hass: HomeAssistant) -> None: step_user_result = await flow.async_step_user() - assert step_user_result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert step_user_result["type"] == data_entry_flow.FlowResultType.ABORT assert step_user_result["reason"] == "single_instance_allowed" @@ -104,7 +104,7 @@ async def test_step_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", @@ -125,7 +125,7 @@ async def test_step_mfa(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mfa" with patch( @@ -147,7 +147,7 @@ async def test_step_mfa(hass: HomeAssistant) -> None: result["flow_id"], user_input={"mfa_code": "123456"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", @@ -175,7 +175,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: data=conf, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch("homeassistant.config_entries.ConfigEntries.async_reload"): @@ -184,7 +184,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: user_input=conf, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py index 32bb8bf7c70..9ed2fc82595 100644 --- a/tests/components/abode/test_init.py +++ b/tests/components/abode/test_init.py @@ -75,7 +75,7 @@ async def test_invalid_credentials(hass: HomeAssistant) -> None: ), ), patch( "homeassistant.components.abode.config_flow.AbodeFlowHandler.async_step_reauth", - return_value={"type": data_entry_flow.RESULT_TYPE_FORM}, + return_value={"type": data_entry_flow.FlowResultType.FORM}, ) as mock_async_step_reauth: await setup_platform(hass, ALARM_DOMAIN) diff --git a/tests/components/accuweather/test_config_flow.py b/tests/components/accuweather/test_config_flow.py index c8f2d3c8c89..b4d804dceae 100644 --- a/tests/components/accuweather/test_config_flow.py +++ b/tests/components/accuweather/test_config_flow.py @@ -25,7 +25,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -134,7 +134,7 @@ async def test_create_entry(hass): data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "abcd" assert result["data"][CONF_NAME] == "abcd" assert result["data"][CONF_LATITUDE] == 55.55 @@ -171,14 +171,14 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_FORECAST: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_FORECAST: True} await hass.async_block_till_done() diff --git a/tests/components/acmeda/test_config_flow.py b/tests/components/acmeda/test_config_flow.py index c50e0b9971f..4990d196a08 100644 --- a/tests/components/acmeda/test_config_flow.py +++ b/tests/components/acmeda/test_config_flow.py @@ -47,7 +47,7 @@ async def test_show_form_no_hubs(hass, mock_hub_discover): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" # Check we performed the discovery @@ -66,7 +66,7 @@ async def test_show_form_one_hub(hass, mock_hub_discover, mock_hub_run): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == dummy_hub_1.id assert result["result"].data == { CONF_HOST: DUMMY_HOST1, @@ -91,7 +91,7 @@ async def test_show_form_two_hubs(hass, mock_hub_discover): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Check we performed the discovery @@ -117,7 +117,7 @@ async def test_create_second_entry(hass, mock_hub_run, mock_hub_discover): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == dummy_hub_2.id assert result["result"].data == { CONF_HOST: DUMMY_HOST2, diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index f5b30308a52..4bcfb60e7b6 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -35,7 +35,7 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -55,7 +55,7 @@ async def test_connection_error( ) assert result - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -78,14 +78,14 @@ async def test_full_flow_implementation( assert result assert result.get("flow_id") - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "user" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=FIXTURE_USER_INPUT ) assert result2 - assert result2.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2.get("title") == FIXTURE_USER_INPUT[CONF_HOST] data = result2.get("data") @@ -132,7 +132,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -154,7 +154,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -176,14 +176,14 @@ async def test_hassio_confirm( context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("description_placeholders") == {"addon": "AdGuard Home Addon"} result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result2 - assert result2.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2.get("title") == "AdGuard Home Addon" data = result2.get("data") @@ -215,6 +215,6 @@ async def test_hassio_connection_error( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/advantage_air/test_config_flow.py b/tests/components/advantage_air/test_config_flow.py index a8e219fff89..533cf2a17a4 100644 --- a/tests/components/advantage_air/test_config_flow.py +++ b/tests/components/advantage_air/test_config_flow.py @@ -19,7 +19,7 @@ async def test_form(hass, aioclient_mock): result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result1["type"] == data_entry_flow.FlowResultType.FORM assert result1["step_id"] == "user" assert result1["errors"] == {} @@ -33,7 +33,7 @@ async def test_form(hass, aioclient_mock): ) assert len(aioclient_mock.mock_calls) == 1 - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "testname" assert result2["data"] == USER_INPUT await hass.async_block_till_done() @@ -47,7 +47,7 @@ async def test_form(hass, aioclient_mock): result3["flow_id"], USER_INPUT, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result4["type"] == data_entry_flow.FlowResultType.ABORT async def test_form_cannot_connect(hass, aioclient_mock): @@ -66,7 +66,7 @@ async def test_form_cannot_connect(hass, aioclient_mock): USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} assert len(aioclient_mock.mock_calls) == 1 diff --git a/tests/components/aemet/test_config_flow.py b/tests/components/aemet/test_config_flow.py index af14c170f40..bba177b96d2 100644 --- a/tests/components/aemet/test_config_flow.py +++ b/tests/components/aemet/test_config_flow.py @@ -35,7 +35,7 @@ async def test_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -49,7 +49,7 @@ async def test_form(hass): entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] @@ -79,14 +79,14 @@ async def test_form_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_STATION_UPDATES: False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { CONF_STATION_UPDATES: False, } @@ -97,14 +97,14 @@ async def test_form_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_STATION_UPDATES: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { CONF_STATION_UPDATES: True, } diff --git a/tests/components/agent_dvr/test_config_flow.py b/tests/components/agent_dvr/test_config_flow.py index 01cb31b3f19..b36044e45b1 100644 --- a/tests/components/agent_dvr/test_config_flow.py +++ b/tests/components/agent_dvr/test_config_flow.py @@ -20,7 +20,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_user_device_exists_abort( @@ -35,7 +35,7 @@ async def test_user_device_exists_abort( data={CONF_HOST: "example.local", CONF_PORT: 8090}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_connection_error( @@ -53,7 +53,7 @@ async def test_connection_error( assert result["errors"]["base"] == "cannot_connect" assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_full_user_flow_implementation( @@ -78,7 +78,7 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "example.local", CONF_PORT: 8090} @@ -88,7 +88,7 @@ async def test_full_user_flow_implementation( assert result["data"][CONF_PORT] == 8090 assert result["data"][SERVER_URL] == "http://example.local:8090/" assert result["title"] == "DESKTOP" - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entries = hass.config_entries.async_entries(config_flow.DOMAIN) assert entries[0].unique_id == "c0715bba-c2d0-48ef-9e3e-bc81c9ea4447" diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index 8b593e85cf4..b7c2a65812e 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -26,7 +26,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -84,7 +84,7 @@ async def test_create_entry(hass, aioclient_mock): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] @@ -106,7 +106,7 @@ async def test_create_entry_with_nearest_method(hass, aioclient_mock): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] diff --git a/tests/components/airnow/test_config_flow.py b/tests/components/airnow/test_config_flow.py index b26775d7051..02236e826e5 100644 --- a/tests/components/airnow/test_config_flow.py +++ b/tests/components/airnow/test_config_flow.py @@ -72,7 +72,7 @@ async def test_form(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch("pyairnow.WebServiceAPI._get", return_value=MOCK_RESPONSE), patch( @@ -86,7 +86,7 @@ async def test_form(hass): await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"] == CONFIG assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index f0a75417487..12a9e67122f 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -66,7 +66,7 @@ async def test_duplicate_error(hass, config, config_entry, data): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -126,7 +126,7 @@ async def test_errors(hass, data, exc, errors, integration_type): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == errors @@ -183,14 +183,14 @@ async def test_options_flow(hass, config_entry): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.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 result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_SHOW_ON_MAP: False} @@ -205,7 +205,7 @@ async def test_step_geography_by_coords(hass, config, setup_airvisual): result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Cloud API (51.528308, -0.3817765)" assert result["data"] == { CONF_API_KEY: "abcde12345", @@ -237,7 +237,7 @@ async def test_step_geography_by_name(hass, config, setup_airvisual): result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Cloud API (Beijing, Beijing, China)" assert result["data"] == { CONF_API_KEY: "abcde12345", @@ -265,7 +265,7 @@ async def test_step_node_pro(hass, config, setup_airvisual): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Node/Pro (192.168.1.100)" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -282,7 +282,7 @@ async def test_step_reauth(hass, config_entry, setup_airvisual): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" new_api_key = "defgh67890" @@ -293,7 +293,7 @@ async def test_step_reauth(hass, config_entry, setup_airvisual): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_API_KEY: new_api_key} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -306,7 +306,7 @@ async def test_step_user(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -315,7 +315,7 @@ async def test_step_user(hass): data={"type": INTEGRATION_TYPE_GEOGRAPHY_COORDS}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "geography_by_coords" result = await hass.config_entries.flow.async_init( @@ -324,7 +324,7 @@ async def test_step_user(hass): data={"type": INTEGRATION_TYPE_GEOGRAPHY_NAME}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "geography_by_name" result = await hass.config_entries.flow.async_init( @@ -333,5 +333,5 @@ async def test_step_user(hass): data={"type": INTEGRATION_TYPE_NODE_PRO}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "node_pro" diff --git a/tests/components/airzone/test_config_flow.py b/tests/components/airzone/test_config_flow.py index 251dcf01b60..32eaade93ee 100644 --- a/tests/components/airzone/test_config_flow.py +++ b/tests/components/airzone/test_config_flow.py @@ -41,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -55,7 +55,7 @@ async def test_form(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"Airzone {CONFIG[CONF_HOST]}:{CONFIG[CONF_PORT]}" assert result["data"][CONF_HOST] == CONFIG[CONF_HOST] assert result["data"][CONF_PORT] == CONFIG[CONF_PORT] @@ -84,7 +84,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {CONF_ID: "invalid_system_id"} @@ -95,7 +95,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: result["flow_id"], CONFIG_ID1 ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -103,7 +103,7 @@ async def test_form_invalid_system_id(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert ( result["title"] == f"Airzone {CONFIG_ID1[CONF_HOST]}:{CONFIG_ID1[CONF_PORT]}" diff --git a/tests/components/alarmdecoder/test_config_flow.py b/tests/components/alarmdecoder/test_config_flow.py index 8a2aae48f9b..e3fdb05ca00 100644 --- a/tests/components/alarmdecoder/test_config_flow.py +++ b/tests/components/alarmdecoder/test_config_flow.py @@ -62,7 +62,7 @@ async def test_setups(hass: HomeAssistant, protocol, connection, title): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -70,7 +70,7 @@ async def test_setups(hass: HomeAssistant, protocol, connection, title): {CONF_PROTOCOL: protocol}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "protocol" with patch("homeassistant.components.alarmdecoder.config_flow.AdExt.open"), patch( @@ -82,7 +82,7 @@ async def test_setups(hass: HomeAssistant, protocol, connection, title): result = await hass.config_entries.flow.async_configure( result["flow_id"], connection ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == title assert result["data"] == { **connection, @@ -105,7 +105,7 @@ async def test_setup_connection_error(hass: HomeAssistant): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -113,7 +113,7 @@ async def test_setup_connection_error(hass: HomeAssistant): {CONF_PROTOCOL: protocol}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "protocol" with patch( @@ -123,7 +123,7 @@ async def test_setup_connection_error(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure( result["flow_id"], connection_settings ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with patch( @@ -133,7 +133,7 @@ async def test_setup_connection_error(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure( result["flow_id"], connection_settings ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -152,7 +152,7 @@ async def test_options_arm_flow(hass: HomeAssistant): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -160,7 +160,7 @@ async def test_options_arm_flow(hass: HomeAssistant): user_input={"edit_selection": "Arming Settings"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "arm_settings" with patch( @@ -171,7 +171,7 @@ async def test_options_arm_flow(hass: HomeAssistant): user_input=user_input, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: user_input, OPTIONS_ZONES: DEFAULT_ZONE_OPTIONS, @@ -190,7 +190,7 @@ async def test_options_zone_flow(hass: HomeAssistant): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -198,7 +198,7 @@ async def test_options_zone_flow(hass: HomeAssistant): user_input={"edit_selection": "Zones"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_select" result = await hass.config_entries.options.async_configure( @@ -214,7 +214,7 @@ async def test_options_zone_flow(hass: HomeAssistant): user_input=zone_settings, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: DEFAULT_ARM_OPTIONS, OPTIONS_ZONES: {zone_number: zone_settings}, @@ -223,7 +223,7 @@ async def test_options_zone_flow(hass: HomeAssistant): # Make sure zone can be removed... result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -231,7 +231,7 @@ async def test_options_zone_flow(hass: HomeAssistant): user_input={"edit_selection": "Zones"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_select" result = await hass.config_entries.options.async_configure( @@ -247,7 +247,7 @@ async def test_options_zone_flow(hass: HomeAssistant): user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: DEFAULT_ARM_OPTIONS, OPTIONS_ZONES: {}, @@ -266,7 +266,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -274,7 +274,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={"edit_selection": "Zones"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_select" # Zone Number must be int @@ -283,7 +283,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={CONF_ZONE_NUMBER: "asd"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_select" assert result["errors"] == {CONF_ZONE_NUMBER: "int"} @@ -292,7 +292,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={CONF_ZONE_NUMBER: zone_number}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" # CONF_RELAY_ADDR & CONF_RELAY_CHAN are inclusive @@ -301,7 +301,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={**zone_settings, CONF_RELAY_ADDR: "1"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {"base": "relay_inclusive"} @@ -310,7 +310,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={**zone_settings, CONF_RELAY_CHAN: "1"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {"base": "relay_inclusive"} @@ -320,7 +320,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={**zone_settings, CONF_RELAY_ADDR: "abc", CONF_RELAY_CHAN: "abc"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == { CONF_RELAY_ADDR: "int", @@ -333,7 +333,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={**zone_settings, CONF_ZONE_LOOP: "1"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {CONF_ZONE_LOOP: "loop_rfid"} @@ -343,7 +343,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={**zone_settings, CONF_ZONE_RFID: "rfid123", CONF_ZONE_LOOP: "ab"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {CONF_ZONE_LOOP: "int"} @@ -353,7 +353,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): user_input={**zone_settings, CONF_ZONE_RFID: "rfid123", CONF_ZONE_LOOP: "5"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zone_details" assert result["errors"] == {CONF_ZONE_LOOP: "loop_range"} @@ -372,7 +372,7 @@ async def test_options_zone_flow_validation(hass: HomeAssistant): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { OPTIONS_ARM: DEFAULT_ARM_OPTIONS, OPTIONS_ZONES: { @@ -420,7 +420,7 @@ async def test_one_device_allowed(hass, protocol, connection): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -428,11 +428,11 @@ async def test_one_device_allowed(hass, protocol, connection): {CONF_PROTOCOL: protocol}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "protocol" result = await hass.config_entries.flow.async_configure( result["flow_id"], connection ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py index 32f49cff043..3bf2db14b95 100644 --- a/tests/components/almond/test_config_flow.py +++ b/tests/components/almond/test_config_flow.py @@ -57,7 +57,7 @@ async def test_hassio(hass): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "hassio_confirm" with patch( @@ -67,7 +67,7 @@ async def test_hassio(hass): assert len(mock_setup.mock_calls) == 1 - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(hass.config_entries.async_entries(DOMAIN)) == 1 entry = hass.config_entries.async_entries(DOMAIN)[0] @@ -83,15 +83,15 @@ async def test_abort_if_existing_entry(hass): flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" result = await flow.async_step_import({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" result = await flow.async_step_hassio(HassioServiceInfo(config={})) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -123,7 +123,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( "https://almond.stanford.edu/me/api/oauth2/authorize" f"?response_type=code&client_id={CLIENT_ID_VALUE}" diff --git a/tests/components/amberelectric/test_config_flow.py b/tests/components/amberelectric/test_config_flow.py index ce474be1b3d..2be77f19bf1 100644 --- a/tests/components/amberelectric/test_config_flow.py +++ b/tests/components/amberelectric/test_config_flow.py @@ -67,7 +67,7 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None: initial_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert initial_result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert initial_result.get("type") == data_entry_flow.FlowResultType.FORM assert initial_result.get("step_id") == "user" # Test filling in API key @@ -76,7 +76,7 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_API_TOKEN: API_KEY}, ) - assert enter_api_key_result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert enter_api_key_result.get("type") == data_entry_flow.FlowResultType.FORM assert enter_api_key_result.get("step_id") == "site" select_site_result = await hass.config_entries.flow.async_configure( @@ -85,7 +85,7 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None: ) # Show available sites - assert select_site_result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert select_site_result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert select_site_result.get("title") == "Home" data = select_site_result.get("data") assert data @@ -102,7 +102,7 @@ async def test_no_site(hass: HomeAssistant, no_site_api: Mock) -> None: data={CONF_API_TOKEN: "psk_123456789"}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM # Goes back to the user step assert result.get("step_id") == "user" assert result.get("errors") == {"api_token": "no_site"} @@ -113,7 +113,7 @@ async def test_invalid_key(hass: HomeAssistant, invalid_key_api: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "user" # Test filling in API key @@ -122,7 +122,7 @@ async def test_invalid_key(hass: HomeAssistant, invalid_key_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_API_TOKEN: "psk_123456789"}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM # Goes back to the user step assert result.get("step_id") == "user" assert result.get("errors") == {"api_token": "invalid_api_token"} @@ -133,7 +133,7 @@ async def test_unknown_error(hass: HomeAssistant, api_error: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "user" # Test filling in API key @@ -142,7 +142,7 @@ async def test_unknown_error(hass: HomeAssistant, api_error: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_API_TOKEN: "psk_123456789"}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM # Goes back to the user step assert result.get("step_id") == "user" assert result.get("errors") == {"api_token": "unknown_error"} diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py index 2da550afd42..e2c8e46dc46 100644 --- a/tests/components/ambiclimate/test_config_flow.py +++ b/tests/components/ambiclimate/test_config_flow.py @@ -35,7 +35,7 @@ async def test_abort_if_no_implementation_registered(hass): flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_configuration" @@ -48,12 +48,12 @@ async def test_abort_if_already_setup(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" with pytest.raises(data_entry_flow.AbortFlow): result = await flow.async_step_code() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -63,7 +63,7 @@ async def test_full_flow_implementation(hass): flow = await init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert ( result["description_placeholders"]["cb_url"] @@ -78,7 +78,7 @@ async def test_full_flow_implementation(hass): with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value="test"): result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Ambiclimate" assert result["data"]["callback_url"] == "https://example.com/api/ambiclimate" assert result["data"][CONF_CLIENT_SECRET] == "secret" @@ -86,14 +86,14 @@ async def test_full_flow_implementation(hass): with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value=None): result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT with patch( "ambiclimate.AmbiclimateOAuth.get_access_token", side_effect=ambiclimate.AmbiclimateOauthError(), ): result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_abort_invalid_code(hass): @@ -103,7 +103,7 @@ async def test_abort_invalid_code(hass): with patch("ambiclimate.AmbiclimateOAuth.get_access_token", return_value=None): result = await flow.async_step_code("invalid") - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "access_token" @@ -115,7 +115,7 @@ async def test_already_setup(hass): context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/ambient_station/test_config_flow.py b/tests/components/ambient_station/test_config_flow.py index a72534b8478..0e298c40c0e 100644 --- a/tests/components/ambient_station/test_config_flow.py +++ b/tests/components/ambient_station/test_config_flow.py @@ -15,7 +15,7 @@ async def test_duplicate_error(hass, config, config_entry, setup_ambient_station result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -31,7 +31,7 @@ async def test_errors(hass, config, devices, error, setup_ambient_station): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": error} @@ -40,7 +40,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -49,7 +49,7 @@ async def test_step_user(hass, config, setup_ambient_station): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "67890fghij67" assert result["data"] == { CONF_API_KEY: "12345abcde12345abcde", diff --git a/tests/components/androidtv/test_config_flow.py b/tests/components/androidtv/test_config_flow.py index d5301f7ada3..a0fb86eb803 100644 --- a/tests/components/androidtv/test_config_flow.py +++ b/tests/components/androidtv/test_config_flow.py @@ -97,7 +97,7 @@ async def test_user(hass, config, eth_mac, wifi_mac): flow_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} ) - assert flow_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert flow_result["type"] == data_entry_flow.FlowResultType.FORM assert flow_result["step_id"] == "user" # test with all provided @@ -110,7 +110,7 @@ async def test_user(hass, config, eth_mac, wifi_mac): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == config @@ -134,7 +134,7 @@ async def test_user_adbkey(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == config_data @@ -152,7 +152,7 @@ async def test_error_both_key_server(hass): data=config_data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "key_and_server"} with patch( @@ -164,7 +164,7 @@ async def test_error_both_key_server(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == HOST assert result2["data"] == CONFIG_ADB_SERVER @@ -179,7 +179,7 @@ async def test_error_invalid_key(hass): data=config_data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "adbkey_not_file"} with patch( @@ -191,7 +191,7 @@ async def test_error_invalid_key(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == HOST assert result2["data"] == CONFIG_ADB_SERVER @@ -219,7 +219,7 @@ async def test_invalid_mac(hass, config, eth_mac, wifi_mac): data=config, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "invalid_unique_id" @@ -237,7 +237,7 @@ async def test_abort_if_host_exist(hass): data=config_data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -260,7 +260,7 @@ async def test_abort_if_unique_exist(hass): data=CONFIG_ADB_SERVER, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -275,7 +275,7 @@ async def test_on_connect_failed(hass): result = await hass.config_entries.flow.async_configure( flow_result["flow_id"], user_input=CONFIG_ADB_SERVER ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with patch( @@ -285,7 +285,7 @@ async def test_on_connect_failed(hass): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=CONFIG_ADB_SERVER ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} with patch( @@ -297,7 +297,7 @@ async def test_on_connect_failed(hass): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == HOST assert result3["data"] == CONFIG_ADB_SERVER @@ -320,7 +320,7 @@ async def test_options_flow(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # test app form with existing app @@ -330,7 +330,7 @@ async def test_options_flow(hass): CONF_APPS: "app1", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "apps" # test change value in apps form @@ -340,7 +340,7 @@ async def test_options_flow(hass): CONF_APP_NAME: "Appl1", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # test app form with new app @@ -350,7 +350,7 @@ async def test_options_flow(hass): CONF_APPS: APPS_NEW_ID, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "apps" # test save value for new app @@ -361,7 +361,7 @@ async def test_options_flow(hass): CONF_APP_NAME: "Appl2", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # test app form for delete @@ -371,7 +371,7 @@ async def test_options_flow(hass): CONF_APPS: "app1", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "apps" # test delete app1 @@ -382,7 +382,7 @@ async def test_options_flow(hass): CONF_APP_DELETE: True, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # test rules form with existing rule @@ -392,7 +392,7 @@ async def test_options_flow(hass): CONF_STATE_DETECTION_RULES: "com.plexapp.android", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "rules" # test change value in rule form with invalid json rule @@ -402,7 +402,7 @@ async def test_options_flow(hass): CONF_RULE_VALUES: "a", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "rules" assert result["errors"] == {"base": "invalid_det_rules"} @@ -413,7 +413,7 @@ async def test_options_flow(hass): CONF_RULE_VALUES: json.dumps({"a": "b"}), }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "rules" assert result["errors"] == {"base": "invalid_det_rules"} @@ -424,7 +424,7 @@ async def test_options_flow(hass): CONF_RULE_VALUES: json.dumps(["standby"]), }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # test rule form with new rule @@ -434,7 +434,7 @@ async def test_options_flow(hass): CONF_STATE_DETECTION_RULES: RULES_NEW_ID, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "rules" # test save value for new rule @@ -445,7 +445,7 @@ async def test_options_flow(hass): CONF_RULE_VALUES: json.dumps(VALID_DETECT_RULE), }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # test rules form with delete existing rule @@ -455,7 +455,7 @@ async def test_options_flow(hass): CONF_STATE_DETECTION_RULES: "com.plexapp.android", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "rules" # test delete rule @@ -465,7 +465,7 @@ async def test_options_flow(hass): CONF_RULE_DELETE: True, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -479,7 +479,7 @@ async def test_options_flow(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY apps_options = config_entry.options[CONF_APPS] assert apps_options.get("app1") is None diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index 6efb4820564..862019b529e 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -71,7 +71,7 @@ async def test_user_input_device_not_found(hass, mrp_device): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -79,7 +79,7 @@ async def test_user_input_device_not_found(hass, mrp_device): {"device_input": "none"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "no_devices_found"} @@ -95,7 +95,7 @@ async def test_user_input_unexpected_error(hass, mock_scan): {"device_input": "dummy"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -104,31 +104,31 @@ async def test_user_adds_full_device(hass, full_device, pairing): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"device_input": "MRP Device"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == { "name": "MRP Device", "type": "Unknown", } result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["description_placeholders"] == {"protocol": "MRP"} result4 = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": 1111} ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["type"] == data_entry_flow.FlowResultType.FORM assert result4["description_placeholders"] == {"protocol": "DMAP", "pin": 1111} result5 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result5["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result5["type"] == data_entry_flow.FlowResultType.FORM assert result5["description_placeholders"] == {"protocol": "AirPlay"} result6 = await hass.config_entries.flow.async_configure( @@ -157,14 +157,14 @@ async def test_user_adds_dmap_device(hass, dmap_device, dmap_pin, pairing): result["flow_id"], {"device_input": "DMAP Device"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == { "name": "DMAP Device", "type": "Unknown", } result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["description_placeholders"] == {"pin": 1111, "protocol": "DMAP"} result6 = await hass.config_entries.flow.async_configure( @@ -195,7 +195,7 @@ async def test_user_adds_dmap_device_failed(hass, dmap_device, dmap_pin, pairing await hass.config_entries.flow.async_configure(result["flow_id"], {}) result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "device_did_not_pair" @@ -211,7 +211,7 @@ async def test_user_adds_device_with_ip_filter( result["flow_id"], {"device_input": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == { "name": "DMAP Device", "type": "Unknown", @@ -268,7 +268,7 @@ async def test_user_adds_existing_device(hass, mrp_device): result["flow_id"], {"device_input": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "already_configured"} @@ -294,7 +294,7 @@ async def test_user_connection_failed(hass, mrp_device, pairing_mock): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "setup_failed" @@ -315,7 +315,7 @@ async def test_user_start_pair_error_failed(hass, mrp_device, pairing_mock): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "invalid_auth" @@ -336,14 +336,14 @@ async def test_user_pair_service_with_password( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "password" result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "setup_failed" @@ -363,14 +363,14 @@ async def test_user_pair_disabled_service(hass, dmap_with_requirement, pairing_m result["flow_id"], {}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "protocol_disabled" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "setup_failed" @@ -390,7 +390,7 @@ async def test_user_pair_ignore_unsupported(hass, dmap_with_requirement, pairing result["flow_id"], {}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "setup_failed" @@ -416,7 +416,7 @@ async def test_user_pair_invalid_pin(hass, mrp_device, pairing_mock): result["flow_id"], {"pin": 1111}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -442,7 +442,7 @@ async def test_user_pair_unexpected_error(hass, mrp_device, pairing_mock): result["flow_id"], {"pin": 1111}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -463,7 +463,7 @@ async def test_user_pair_backoff_error(hass, mrp_device, pairing_mock): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "backoff" @@ -484,7 +484,7 @@ async def test_user_pair_begin_unexpected_error(hass, mrp_device, pairing_mock): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "unknown" @@ -499,14 +499,14 @@ async def test_ignores_disabled_service(hass, airplay_with_disabled_mrp, pairing result["flow_id"], {"device_input": "mrpid"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["description_placeholders"] == { "name": "AirPlay Device", "type": "Unknown", } result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "AirPlay"} result3 = await hass.config_entries.flow.async_configure( @@ -541,7 +541,7 @@ async def test_zeroconf_unsupported_service_aborts(hass): properties={}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" @@ -560,7 +560,7 @@ async def test_zeroconf_add_mrp_device(hass, mrp_device, pairing): type="_mediaremotetv._tcp.local.", ), ) - assert unrelated_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert unrelated_result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_init( DOMAIN, @@ -575,7 +575,7 @@ async def test_zeroconf_add_mrp_device(hass, mrp_device, pairing): type="_mediaremotetv._tcp.local.", ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["description_placeholders"] == { "name": "MRP Device", "type": "Unknown", @@ -585,7 +585,7 @@ async def test_zeroconf_add_mrp_device(hass, mrp_device, pairing): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "MRP"} result3 = await hass.config_entries.flow.async_configure( @@ -605,7 +605,7 @@ async def test_zeroconf_add_dmap_device(hass, dmap_device, dmap_pin, pairing): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["description_placeholders"] == { "name": "DMAP Device", "type": "Unknown", @@ -615,7 +615,7 @@ async def test_zeroconf_add_dmap_device(hass, dmap_device, dmap_pin, pairing): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "DMAP", "pin": 1111} result3 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) @@ -654,7 +654,7 @@ async def test_zeroconf_ip_change(hass, mock_scan): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_async_setup.mock_calls) == 2 assert entry.data[CONF_ADDRESS] == "127.0.0.1" @@ -693,7 +693,7 @@ async def test_zeroconf_ip_change_via_secondary_identifier(hass, mock_scan): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_async_setup.mock_calls) == 2 assert entry.data[CONF_ADDRESS] == "127.0.0.1" @@ -709,7 +709,7 @@ async def test_zeroconf_add_existing_aborts(hass, dmap_device): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -718,7 +718,7 @@ async def test_zeroconf_add_but_device_not_found(hass, mock_scan): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -729,7 +729,7 @@ async def test_zeroconf_add_existing_device(hass, dmap_device): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -740,7 +740,7 @@ async def test_zeroconf_unexpected_error(hass, mock_scan): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=DMAP_SERVICE ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" @@ -764,7 +764,7 @@ async def test_zeroconf_abort_if_other_in_progress(hass, mock_scan): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" mock_scan.result = [ @@ -786,7 +786,7 @@ async def test_zeroconf_abort_if_other_in_progress(hass, mock_scan): properties={"UniqueIdentifier": "mrpid", "Name": "Kitchen"}, ), ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -844,7 +844,7 @@ async def test_zeroconf_missing_device_during_protocol_resolve( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "device_not_found" @@ -904,7 +904,7 @@ async def test_zeroconf_additional_protocol_resolve_failure( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "inconsistent_device" @@ -930,7 +930,7 @@ async def test_zeroconf_pair_additionally_found_protocols( properties={"deviceid": "airplayid"}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM await hass.async_block_till_done() mock_scan.result = [ @@ -981,7 +981,7 @@ async def test_zeroconf_pair_additionally_found_protocols( {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "pair_no_pin" assert result2["description_placeholders"] == {"pin": ANY, "protocol": "RAOP"} @@ -991,7 +991,7 @@ async def test_zeroconf_pair_additionally_found_protocols( {}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == "pair_with_pin" assert result3["description_placeholders"] == {"protocol": "MRP"} @@ -999,7 +999,7 @@ async def test_zeroconf_pair_additionally_found_protocols( result["flow_id"], {"pin": 1234}, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["type"] == data_entry_flow.FlowResultType.FORM assert result4["step_id"] == "pair_with_pin" assert result4["description_placeholders"] == {"protocol": "AirPlay"} @@ -1007,7 +1007,7 @@ async def test_zeroconf_pair_additionally_found_protocols( result["flow_id"], {"pin": 1234}, ) - assert result5["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result5["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Re-configuration @@ -1030,13 +1030,13 @@ async def test_reconfigure_update_credentials(hass, mrp_device, pairing): result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == {"protocol": "MRP"} result3 = await hass.config_entries.flow.async_configure( result["flow_id"], {"pin": 1111} ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert config_entry.data == { @@ -1058,7 +1058,7 @@ async def test_option_start_off(hass): config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_START_OFF: True} @@ -1083,5 +1083,5 @@ async def test_zeroconf_rejects_ipv6(hass): properties={"CtlN": "Apple TV"}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "ipv6_not_supported" diff --git a/tests/components/application_credentials/test_init.py b/tests/components/application_credentials/test_init.py index dd5995a8f4f..3b7a414305f 100644 --- a/tests/components/application_credentials/test_init.py +++ b/tests/components/application_credentials/test_init.py @@ -160,7 +160,7 @@ class OAuthFixture: ) result = await self.hass.config_entries.flow.async_configure(result["flow_id"]) - assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result.get("title") == self.title assert "data" in result assert "token" in result["data"] @@ -417,7 +417,7 @@ async def test_config_flow_no_credentials(hass): result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "missing_configuration" @@ -444,7 +444,7 @@ async def test_config_flow_other_domain( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "missing_configuration" @@ -467,7 +467,7 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP result = await oauth_fixture.complete_external_step(result) assert ( result["data"].get("auth_implementation") == "fake_integration_some_client_id" @@ -511,14 +511,14 @@ async def test_config_flow_multiple_entries( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "pick_implementation" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"implementation": "fake_integration_some_client_id2"}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP oauth_fixture.client_id = CLIENT_ID + "2" oauth_fixture.title = CLIENT_ID + "2" result = await oauth_fixture.complete_external_step(result) @@ -548,7 +548,7 @@ async def test_config_flow_create_delete_credential( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "missing_configuration" @@ -565,7 +565,7 @@ async def test_config_flow_with_config_credential( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP oauth_fixture.title = DEFAULT_IMPORT_NAME result = await oauth_fixture.complete_external_step(result) # Uses the imported auth domain for compatibility @@ -583,7 +583,7 @@ async def test_import_without_setup(hass, config_credential): result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "missing_configuration" @@ -612,7 +612,7 @@ async def test_websocket_without_platform( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "missing_configuration" @@ -687,7 +687,7 @@ async def test_platform_with_auth_implementation( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP oauth_fixture.title = DEFAULT_IMPORT_NAME result = await oauth_fixture.complete_external_step(result) # Uses the imported auth domain for compatibility @@ -745,7 +745,7 @@ async def test_name( result = await hass.config_entries.flow.async_init( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == data_entry_flow.FlowResultType.EXTERNAL_STEP oauth_fixture.title = NAME result = await oauth_fixture.complete_external_step(result) assert ( diff --git a/tests/components/arcam_fmj/test_config_flow.py b/tests/components/arcam_fmj/test_config_flow.py index efcbb0b691c..eeb5adbc7e3 100644 --- a/tests/components/arcam_fmj/test_config_flow.py +++ b/tests/components/arcam_fmj/test_config_flow.py @@ -65,11 +65,11 @@ async def test_ssdp(hass, dummy_client): context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["data"] == MOCK_CONFIG_ENTRY @@ -86,7 +86,7 @@ async def test_ssdp_abort(hass): context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -99,11 +99,11 @@ async def test_ssdp_unable_to_connect(hass, dummy_client): context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -122,7 +122,7 @@ async def test_ssdp_update(hass): context={CONF_SOURCE: SOURCE_SSDP}, data=MOCK_DISCOVER, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == MOCK_HOST @@ -137,7 +137,7 @@ async def test_user(hass, aioclient_mock): data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" user_input = { @@ -149,7 +149,7 @@ async def test_user(hass, aioclient_mock): 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["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["data"] == MOCK_CONFIG_ENTRY assert result["result"].unique_id == MOCK_UUID @@ -168,7 +168,7 @@ async def test_invalid_ssdp(hass, aioclient_mock): context={CONF_SOURCE: SOURCE_USER}, data=user_input, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["data"] == MOCK_CONFIG_ENTRY assert result["result"].unique_id is None @@ -187,7 +187,7 @@ async def test_user_wrong(hass, aioclient_mock): context={CONF_SOURCE: SOURCE_USER}, data=user_input, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"Arcam FMJ ({MOCK_HOST})" assert result["result"].unique_id is None diff --git a/tests/components/asuswrt/test_config_flow.py b/tests/components/asuswrt/test_config_flow.py index 8475bd48f9a..013a81e7184 100644 --- a/tests/components/asuswrt/test_config_flow.py +++ b/tests/components/asuswrt/test_config_flow.py @@ -81,7 +81,7 @@ async def test_user(hass, mock_unique_id, unique_id): flow_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} ) - assert flow_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert flow_result["type"] == data_entry_flow.FlowResultType.FORM assert flow_result["step_id"] == "user" # test with all provided @@ -92,7 +92,7 @@ async def test_user(hass, mock_unique_id, unique_id): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == CONFIG_DATA @@ -116,7 +116,7 @@ async def test_error_wrong_password_ssh(hass, config, error): data=config_data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": error} @@ -136,7 +136,7 @@ async def test_error_invalid_ssh(hass): data=config_data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "ssh_not_file"} @@ -152,7 +152,7 @@ async def test_error_invalid_host(hass): data=CONFIG_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_host"} @@ -168,7 +168,7 @@ async def test_abort_if_not_unique_id_setup(hass): context={"source": SOURCE_USER}, data=CONFIG_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_unique_id" @@ -192,7 +192,7 @@ async def test_update_uniqueid_exist(hass, mock_unique_id): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == CONFIG_DATA prev_entry = hass.config_entries.async_get_entry(existing_entry.entry_id) @@ -214,7 +214,7 @@ async def test_abort_invalid_unique_id(hass): context={"source": SOURCE_USER}, data=CONFIG_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "invalid_unique_id" @@ -244,7 +244,7 @@ async def test_on_connect_failed(hass, side_effect, error): result = await hass.config_entries.flow.async_configure( flow_result["flow_id"], user_input=CONFIG_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": error} @@ -262,7 +262,7 @@ async def test_options_flow(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -276,7 +276,7 @@ async def test_options_flow(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_CONSIDER_HOME] == 20 assert config_entry.options[CONF_TRACK_UNKNOWN] is True assert config_entry.options[CONF_INTERFACE] == "aaa" diff --git a/tests/components/atag/test_config_flow.py b/tests/components/atag/test_config_flow.py index a92e73ae18e..9eeddff9a45 100644 --- a/tests/components/atag/test_config_flow.py +++ b/tests/components/atag/test_config_flow.py @@ -17,7 +17,7 @@ async def test_show_form(hass: HomeAssistant, aioclient_mock: AiohttpClientMocke DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -30,7 +30,7 @@ async def test_adding_second_device( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" with patch( "pyatag.AtagOne.id", @@ -39,7 +39,7 @@ async def test_adding_second_device( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_connection_error( @@ -53,7 +53,7 @@ async def test_connection_error( data=USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -66,7 +66,7 @@ async def test_unauthorized(hass: HomeAssistant, aioclient_mock: AiohttpClientMo context={"source": config_entries.SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unauthorized"} @@ -81,6 +81,6 @@ async def test_full_flow_implementation( context={"source": config_entries.SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == UID assert result["result"].unique_id == UID diff --git a/tests/components/aurora/test_config_flow.py b/tests/components/aurora/test_config_flow.py index 9e83810978a..f106b19cea9 100644 --- a/tests/components/aurora/test_config_flow.py +++ b/tests/components/aurora/test_config_flow.py @@ -101,7 +101,7 @@ async def test_option_flow(hass): data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -109,6 +109,6 @@ async def test_option_flow(hass): user_input={"forecast_threshold": 65}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"]["forecast_threshold"] == 65 diff --git a/tests/components/aurora_abb_powerone/test_config_flow.py b/tests/components/aurora_abb_powerone/test_config_flow.py index b30d6dc5eeb..73b4b67bc04 100644 --- a/tests/components/aurora_abb_powerone/test_config_flow.py +++ b/tests/components/aurora_abb_powerone/test_config_flow.py @@ -58,7 +58,7 @@ async def test_form(hass): {CONF_PORT: "/dev/ttyUSB7", CONF_ADDRESS: 7}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_PORT: "/dev/ttyUSB7", diff --git a/tests/components/aussie_broadband/test_init.py b/tests/components/aussie_broadband/test_init.py index 9e2e0b7cccc..3eb1972011c 100644 --- a/tests/components/aussie_broadband/test_init.py +++ b/tests/components/aussie_broadband/test_init.py @@ -23,7 +23,7 @@ async def test_auth_failure(hass: HomeAssistant) -> None: """Test init with an authentication failure.""" with patch( "homeassistant.components.aussie_broadband.config_flow.ConfigFlow.async_step_reauth", - return_value={"type": data_entry_flow.RESULT_TYPE_FORM}, + return_value={"type": data_entry_flow.FlowResultType.FORM}, ) as mock_async_step_reauth: await setup_platform(hass, side_effect=AuthenticationException()) mock_async_step_reauth.assert_called_once() diff --git a/tests/components/auth/test_mfa_setup_flow.py b/tests/components/auth/test_mfa_setup_flow.py index edf45742dbd..b0851a3cfe6 100644 --- a/tests/components/auth/test_mfa_setup_flow.py +++ b/tests/components/auth/test_mfa_setup_flow.py @@ -64,7 +64,7 @@ async def test_ws_setup_depose_mfa(hass, hass_ws_client): assert result["success"] flow = result["result"] - assert flow["type"] == data_entry_flow.RESULT_TYPE_FORM + assert flow["type"] == data_entry_flow.FlowResultType.FORM assert flow["handler"] == "example_module" assert flow["step_id"] == "init" assert flow["data_schema"][0] == {"type": "string", "name": "pin", "required": True} @@ -83,7 +83,7 @@ async def test_ws_setup_depose_mfa(hass, hass_ws_client): assert result["success"] flow = result["result"] - assert flow["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert flow["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert flow["handler"] == "example_module" assert flow["data"]["result"] is None diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index 47e58fea421..b5bc5f23eaa 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -21,7 +21,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -95,7 +95,7 @@ async def test_reauth(hass: HomeAssistant) -> None: context={"source": SOURCE_REAUTH, "unique_id": UNIQUE_ID}, data={**CONFIG, CONF_ACCESS_TOKEN: "blah"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -105,7 +105,7 @@ async def test_reauth(hass: HomeAssistant) -> None: user_input=CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {CONF_ACCESS_TOKEN: "invalid_access_token"} @@ -117,7 +117,7 @@ async def test_reauth(hass: HomeAssistant) -> None: user_input=CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -133,7 +133,7 @@ async def test_reauth_error(hass: HomeAssistant) -> None: context={"source": SOURCE_REAUTH, "unique_id": UNIQUE_ID}, data={**CONFIG, CONF_ACCESS_TOKEN: "blah"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -143,7 +143,7 @@ async def test_reauth_error(hass: HomeAssistant) -> None: user_input=CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" @@ -160,7 +160,7 @@ async def test_create_entry(hass): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "foo@bar.com (32406)" assert result["data"][CONF_ACCESS_TOKEN] == CONFIG[CONF_ACCESS_TOKEN] assert result["result"].unique_id == UNIQUE_ID diff --git a/tests/components/azure_devops/test_config_flow.py b/tests/components/azure_devops/test_config_flow.py index 7817f6fc570..713d3f31a2f 100644 --- a/tests/components/azure_devops/test_config_flow.py +++ b/tests/components/azure_devops/test_config_flow.py @@ -27,7 +27,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -41,7 +41,7 @@ async def test_authorization_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -50,7 +50,7 @@ async def test_authorization_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -67,7 +67,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -76,7 +76,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "invalid_auth"} @@ -91,7 +91,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -100,7 +100,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -117,7 +117,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -126,7 +126,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "cannot_connect"} @@ -146,7 +146,7 @@ async def test_project_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -155,7 +155,7 @@ async def test_project_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "project_error"} @@ -177,7 +177,7 @@ async def test_reauth_project_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -186,7 +186,7 @@ async def test_reauth_project_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "project_error"} @@ -208,7 +208,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" assert result["errors"] == {"base": "invalid_auth"} @@ -229,7 +229,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -253,7 +253,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -263,7 +263,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert ( result2["title"] == f"{FIXTURE_USER_INPUT[CONF_ORG]}/{FIXTURE_USER_INPUT[CONF_PROJECT]}" diff --git a/tests/components/azure_event_hub/test_config_flow.py b/tests/components/azure_event_hub/test_config_flow.py index 4e135d55555..93615561289 100644 --- a/tests/components/azure_event_hub/test_config_flow.py +++ b/tests/components/azure_event_hub/test_config_flow.py @@ -63,7 +63,7 @@ async def test_form( result2["flow_id"], step2_config.copy(), ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "test-instance" assert result3["data"] == data_config mock_setup_entry.assert_called_once() @@ -79,7 +79,7 @@ async def test_import(hass, mock_setup_entry): data=IMPORT_CONFIG.copy(), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "test-instance" options = { CONF_SEND_INTERVAL: import_config.pop(CONF_SEND_INTERVAL), @@ -109,7 +109,7 @@ async def test_single_instance(hass, source): context={"source": source}, data=BASE_CONFIG_CS.copy(), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -138,7 +138,7 @@ async def test_connection_error_sas( result["flow_id"], SAS_CONFIG.copy(), ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": error_message} @@ -168,7 +168,7 @@ async def test_connection_error_cs( result["flow_id"], CS_CONFIG.copy(), ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": error_message} @@ -176,13 +176,13 @@ async def test_options_flow(hass, entry): """Test options flow.""" result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["last_step"] updated = await hass.config_entries.options.async_configure( result["flow_id"], UPDATE_OPTIONS ) - assert updated["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert updated["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert updated["data"] == UPDATE_OPTIONS await hass.async_block_till_done() diff --git a/tests/components/balboa/test_config_flow.py b/tests/components/balboa/test_config_flow.py index ffb8c80a29e..2b448a3df56 100644 --- a/tests/components/balboa/test_config_flow.py +++ b/tests/components/balboa/test_config_flow.py @@ -124,7 +124,7 @@ async def test_options_flow(hass: HomeAssistant, client: MagicMock) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" with patch( @@ -137,5 +137,5 @@ async def test_options_flow(hass: HomeAssistant, client: MagicMock) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert dict(config_entry.options) == {CONF_SYNC_TIME: True} diff --git a/tests/components/blebox/test_config_flow.py b/tests/components/blebox/test_config_flow.py index 3f40880abf7..7db216e294e 100644 --- a/tests/components/blebox/test_config_flow.py +++ b/tests/components/blebox/test_config_flow.py @@ -159,7 +159,7 @@ async def test_already_configured(hass, valid_feature_mock): context={"source": config_entries.SOURCE_USER}, data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "address_already_configured" diff --git a/tests/components/blink/test_config_flow.py b/tests/components/blink/test_config_flow.py index 5ea03eb2b62..999204a2d91 100644 --- a/tests/components/blink/test_config_flow.py +++ b/tests/components/blink/test_config_flow.py @@ -250,7 +250,7 @@ async def test_reauth_shows_user_step(hass): context={"source": config_entries.SOURCE_REAUTH}, data={"username": "blink@example.com", "password": "invalid_password"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -280,7 +280,7 @@ async def test_options_flow(hass): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "simple_options" result = await hass.config_entries.options.async_configure( @@ -288,6 +288,6 @@ async def test_options_flow(hass): user_input={"scan_interval": 5}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {"scan_interval": 5} assert mock_blink.refresh_rate == 5 diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index 10178c22de8..3f22f984a54 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -35,7 +35,7 @@ async def test_show_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -55,7 +55,7 @@ async def test_connection_error(hass): data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -75,7 +75,7 @@ async def test_full_user_flow_implementation(hass): context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == FIXTURE_COMPLETE_ENTRY[CONF_USERNAME] assert result2["data"] == FIXTURE_COMPLETE_ENTRY @@ -98,7 +98,7 @@ async def test_options_flow_implementation(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "account_options" result = await hass.config_entries.options.async_configure( @@ -107,7 +107,7 @@ async def test_options_flow_implementation(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_READ_ONLY: True, } diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index 16d76c5d753..f61a8d312b2 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -38,7 +38,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -88,7 +88,7 @@ async def test_authorize_no_ip_control(hass): DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_ip_control" @@ -117,7 +117,7 @@ async def test_duplicate_error(hass): result["flow_id"], user_input={CONF_PIN: "1234"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -135,14 +135,14 @@ async def test_create_entry(hass): DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "1234"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "very_unique_string" assert result["title"] == "TV-Model" assert result["data"] == { @@ -168,14 +168,14 @@ async def test_create_entry_with_ipv6_address(hass): data={CONF_HOST: "2001:db8::1428:57ab"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authorize" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "1234"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "very_unique_string" assert result["title"] == "TV-Model" assert result["data"] == { @@ -214,7 +214,7 @@ async def test_options_flow(hass): ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.options.async_configure( @@ -222,5 +222,5 @@ async def test_options_flow(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_IGNORED_SOURCES: ["HDMI 1", "HDMI 2"]} diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 00493011500..60f29dbd901 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -21,7 +21,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -37,7 +37,7 @@ async def test_create_entry_with_hostname(hass): data={CONF_HOST: "example.local", CONF_TYPE: "laser"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "example.local" assert result["data"][CONF_TYPE] == "laser" @@ -53,7 +53,7 @@ async def test_create_entry_with_ipv4_address(hass): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "127.0.0.1" assert result["data"][CONF_TYPE] == "laser" @@ -71,7 +71,7 @@ async def test_create_entry_with_ipv6_address(hass): data={CONF_HOST: "2001:db8::1428:57ab", CONF_TYPE: "laser"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "2001:db8::1428:57ab" assert result["data"][CONF_TYPE] == "laser" @@ -116,7 +116,7 @@ async def test_unsupported_model_error(hass): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unsupported_model" @@ -133,7 +133,7 @@ async def test_device_exists_abort(hass): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +155,7 @@ async def test_zeroconf_snmp_error(hass): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -176,7 +176,7 @@ async def test_zeroconf_unsupported_model(hass): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unsupported_model" assert len(mock_get_data.mock_calls) == 0 @@ -208,7 +208,7 @@ async def test_zeroconf_device_exists_abort(hass): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -235,7 +235,7 @@ async def test_zeroconf_no_probe_existing_device(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_get_data.mock_calls) == 0 @@ -264,13 +264,13 @@ async def test_zeroconf_confirm_create_entry(hass): assert result["step_id"] == "zeroconf_confirm" assert result["description_placeholders"]["model"] == "HL-L2340DW" assert result["description_placeholders"]["serial_number"] == "0123456789" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_TYPE: "laser"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "HL-L2340DW 0123456789" assert result["data"][CONF_HOST] == "127.0.0.1" assert result["data"][CONF_TYPE] == "laser" diff --git a/tests/components/brunt/test_config_flow.py b/tests/components/brunt/test_config_flow.py index 4a949911ca6..b2eea79d463 100644 --- a/tests/components/brunt/test_config_flow.py +++ b/tests/components/brunt/test_config_flow.py @@ -35,7 +35,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -57,7 +57,7 @@ async def test_form_duplicate_login(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -80,17 +80,17 @@ async def test_form_error(hass, side_effect, error_message): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": error_message} @pytest.mark.parametrize( "side_effect, result_type, password, step_id, reason", [ - (None, data_entry_flow.RESULT_TYPE_ABORT, "test", None, "reauth_successful"), + (None, data_entry_flow.FlowResultType.ABORT, "test", None, "reauth_successful"), ( Exception, - data_entry_flow.RESULT_TYPE_FORM, + data_entry_flow.FlowResultType.FORM, CONFIG[CONF_PASSWORD], "reauth_confirm", None, @@ -115,7 +115,7 @@ async def test_reauth(hass, side_effect, result_type, password, step_id, reason) }, data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( "homeassistant.components.brunt.config_flow.BruntClientAsync.async_login", diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py index 38485fb7959..b8efa960fca 100644 --- a/tests/components/bsblan/test_config_flow.py +++ b/tests/components/bsblan/test_config_flow.py @@ -28,7 +28,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_connection_error( @@ -54,7 +54,7 @@ async def test_connection_error( assert result["errors"] == {"base": "cannot_connect"} assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_user_device_exists_abort( @@ -75,7 +75,7 @@ async def test_user_device_exists_abort( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_full_user_flow_implementation( @@ -94,7 +94,7 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -114,7 +114,7 @@ async def test_full_user_flow_implementation( assert result["data"][CONF_PORT] == 80 assert result["data"][CONF_DEVICE_IDENT] == "RVS21.831F/127" assert result["title"] == "RVS21.831F/127" - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entries = hass.config_entries.async_entries(config_flow.DOMAIN) assert entries[0].unique_id == "RVS21.831F/127" @@ -136,7 +136,7 @@ async def test_full_user_flow_implementation_without_auth( ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -153,7 +153,7 @@ async def test_full_user_flow_implementation_without_auth( assert result["data"][CONF_PORT] == 80 assert result["data"][CONF_DEVICE_IDENT] == "RVS21.831F/127" assert result["title"] == "RVS21.831F/127" - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entries = hass.config_entries.async_entries(config_flow.DOMAIN) assert entries[0].unique_id == "RVS21.831F/127" diff --git a/tests/components/buienradar/test_config_flow.py b/tests/components/buienradar/test_config_flow.py index 828101bf77e..420e89b5bc2 100644 --- a/tests/components/buienradar/test_config_flow.py +++ b/tests/components/buienradar/test_config_flow.py @@ -96,7 +96,7 @@ async def test_options_flow(hass): ), patch( "homeassistant.components.buienradar.async_unload_entry", return_value=True ): - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index d7aa0fdeda9..97218a396dd 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -24,10 +24,10 @@ async def test_creating_entry_sets_up_media_player(hass): ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -190,7 +190,7 @@ async def test_option_flow(hass, parameter_data): # Test ignore_cec and uuid options are hidden if advanced options are disabled result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "basic_options" data_schema = result["data_schema"].schema assert set(data_schema) == {"known_hosts"} @@ -201,7 +201,7 @@ async def test_option_flow(hass, parameter_data): result = await hass.config_entries.options.async_init( config_entry.entry_id, context=context ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "basic_options" data_schema = result["data_schema"].schema for other_param in basic_parameters: @@ -218,7 +218,7 @@ async def test_option_flow(hass, parameter_data): result["flow_id"], user_input=user_input_dict, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "advanced_options" for other_param in basic_parameters: if other_param == parameter: @@ -243,7 +243,7 @@ async def test_option_flow(hass, parameter_data): result["flow_id"], user_input=user_input_dict, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] is None for other_param in advanced_parameters: if other_param == parameter: @@ -257,7 +257,7 @@ async def test_option_flow(hass, parameter_data): result["flow_id"], user_input={"known_hosts": ""}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] is None expected_data = {**orig_data, "known_hosts": []} if parameter in advanced_parameters: diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index a1cd1367e5f..4c6409b56bd 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -18,7 +18,7 @@ async def test_user(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -27,7 +27,7 @@ async def test_user(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST, CONF_PORT: PORT} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -42,7 +42,7 @@ async def test_user_with_bad_cert(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -53,7 +53,7 @@ async def test_user_with_bad_cert(hass): result["flow_id"], user_input={CONF_HOST: HOST, CONF_PORT: PORT} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -78,7 +78,7 @@ async def test_import_host_only(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == DEFAULT_PORT @@ -100,7 +100,7 @@ async def test_import_host_and_port(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -122,7 +122,7 @@ async def test_import_non_default_port(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"{HOST}:888" assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == 888 @@ -144,7 +144,7 @@ async def test_import_with_name(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -163,7 +163,7 @@ async def test_bad_import(hass): data={CONF_HOST: HOST}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "import_failed" @@ -180,7 +180,7 @@ async def test_abort_if_already_setup(hass): context={"source": config_entries.SOURCE_IMPORT}, data={CONF_HOST: HOST, CONF_PORT: PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" result = await hass.config_entries.flow.async_init( @@ -188,7 +188,7 @@ async def test_abort_if_already_setup(hass): context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: HOST, CONF_PORT: PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -205,7 +205,7 @@ async def test_abort_on_socket_failed(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "resolve_failed"} with patch( @@ -215,7 +215,7 @@ async def test_abort_on_socket_failed(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "connection_timeout"} with patch( @@ -225,5 +225,5 @@ async def test_abort_on_socket_failed(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "connection_refused"} diff --git a/tests/components/climacell/test_config_flow.py b/tests/components/climacell/test_config_flow.py index 9aa16b8a7c5..69bcf5f9819 100644 --- a/tests/components/climacell/test_config_flow.py +++ b/tests/components/climacell/test_config_flow.py @@ -33,14 +33,14 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_TIMESTEP: 1} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_TIMESTEP] == 1 assert entry.options[CONF_TIMESTEP] == 1 diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index c1022dc6aae..16b43d8a3c8 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -132,7 +132,7 @@ async def test_implementation(hass, flow_handler, current_request_with_host): TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == "http://example.com/auth" flow_finished.set_result( diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 611a7f75939..0de4bf44401 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -1075,7 +1075,7 @@ async def test_ignore_flow(hass, hass_ws_client): result = await hass.config_entries.flow.async_init( "test", context={"source": core_ce.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM await ws_client.send_json( { diff --git a/tests/components/crownstone/test_config_flow.py b/tests/components/crownstone/test_config_flow.py index fdc0df108ee..edc026c9304 100644 --- a/tests/components/crownstone/test_config_flow.py +++ b/tests/components/crownstone/test_config_flow.py @@ -183,7 +183,7 @@ async def test_no_user_input(crownstone_setup: MockFixture, hass: HomeAssistant) DOMAIN, context={"source": "user"} ) # show the login form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert crownstone_setup.call_count == 0 @@ -211,7 +211,7 @@ async def test_abort_if_configured(crownstone_setup: MockFixture, hass: HomeAssi result = await start_config_flow(hass, get_mocked_crownstone_cloud()) # test if we abort if we try to configure the same entry - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert crownstone_setup.call_count == 0 @@ -228,7 +228,7 @@ async def test_authentication_errors( result = await start_config_flow(hass, cloud) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} # side effect: auth error account not verified @@ -238,7 +238,7 @@ async def test_authentication_errors( result = await start_config_flow(hass, cloud) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "account_not_verified"} assert crownstone_setup.call_count == 0 @@ -251,7 +251,7 @@ async def test_unknown_error(crownstone_setup: MockFixture, hass: HomeAssistant) result = await start_config_flow(hass, cloud) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown_error"} assert crownstone_setup.call_count == 0 @@ -271,14 +271,14 @@ async def test_successful_login_no_usb( result = await start_config_flow(hass, get_mocked_crownstone_cloud()) # should show usb form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_config" # don't setup USB dongle, create entry result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USB_PATH: DONT_USE_USB} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == entry_data_without_usb assert result["options"] == entry_options_without_usb assert crownstone_setup.call_count == 1 @@ -304,7 +304,7 @@ async def test_successful_login_with_usb( hass, get_mocked_crownstone_cloud(create_mocked_spheres(2)) ) # should show usb form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports_none_types.call_count == 1 @@ -324,7 +324,7 @@ async def test_successful_login_with_usb( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USB_PATH: port_select} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_sphere_config" assert pyserial_comports_none_types.call_count == 2 assert usb_path.call_count == 1 @@ -333,7 +333,7 @@ async def test_successful_login_with_usb( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_USB_SPHERE: "sphere_name_1"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == entry_data_with_usb assert result["options"] == entry_options_with_usb assert crownstone_setup.call_count == 1 @@ -356,7 +356,7 @@ async def test_successful_login_with_manual_usb_path( hass, get_mocked_crownstone_cloud(create_mocked_spheres(1)) ) # should show usb form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports.call_count == 1 @@ -365,7 +365,7 @@ async def test_successful_login_with_manual_usb_path( result["flow_id"], user_input={CONF_USB_PATH: MANUAL_PATH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_manual_config" assert pyserial_comports.call_count == 2 @@ -377,7 +377,7 @@ async def test_successful_login_with_manual_usb_path( # since we only have 1 sphere here, test that it's automatically selected and # creating entry without asking for user input - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == entry_data_with_manual_usb assert result["options"] == entry_options_with_manual_usb assert crownstone_setup.call_count == 1 @@ -413,7 +413,7 @@ async def test_options_flow_setup_usb( ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema @@ -427,7 +427,7 @@ async def test_options_flow_setup_usb( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USE_USB_OPTION: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports.call_count == 1 @@ -447,7 +447,7 @@ async def test_options_flow_setup_usb( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USB_PATH: port_select} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_sphere_config" assert pyserial_comports.call_count == 2 assert usb_path.call_count == 1 @@ -456,7 +456,7 @@ async def test_options_flow_setup_usb( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USB_SPHERE: "sphere_name_1"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path="/dev/serial/by-id/crownstone-usb", usb_sphere="sphere_id_1" ) @@ -490,7 +490,7 @@ async def test_options_flow_remove_usb(hass: HomeAssistant): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema @@ -507,7 +507,7 @@ async def test_options_flow_remove_usb(hass: HomeAssistant): CONF_USB_SPHERE_OPTION: "sphere_name_0", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path=None, usb_sphere=None ) @@ -543,13 +543,13 @@ async def test_options_flow_manual_usb_path( ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USE_USB_OPTION: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_config" assert pyserial_comports.call_count == 1 @@ -558,7 +558,7 @@ async def test_options_flow_manual_usb_path( result["flow_id"], user_input={CONF_USB_PATH: MANUAL_PATH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_manual_config" assert pyserial_comports.call_count == 2 @@ -568,7 +568,7 @@ async def test_options_flow_manual_usb_path( result["flow_id"], user_input={CONF_USB_MANUAL_PATH: path} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path=path, usb_sphere="sphere_id_0" ) @@ -602,14 +602,14 @@ async def test_options_flow_change_usb_sphere(hass: HomeAssistant): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_USE_USB_OPTION: True, CONF_USB_SPHERE_OPTION: "sphere_name_2"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == create_mocked_entry_options_conf( usb_path="/dev/serial/by-id/crownstone-usb", usb_sphere="sphere_id_2" ) diff --git a/tests/components/denonavr/test_config_flow.py b/tests/components/denonavr/test_config_flow.py index 91197927e95..39127f972fe 100644 --- a/tests/components/denonavr/test_config_flow.py +++ b/tests/components/denonavr/test_config_flow.py @@ -425,7 +425,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -433,7 +433,7 @@ async def test_options_flow(hass): user_input={CONF_SHOW_ALL_SOURCES: True, CONF_ZONE2: True, CONF_ZONE3: True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_SHOW_ALL_SOURCES: True, CONF_ZONE2: True, diff --git a/tests/components/dexcom/test_config_flow.py b/tests/components/dexcom/test_config_flow.py index 48544ca0158..b20321277fb 100644 --- a/tests/components/dexcom/test_config_flow.py +++ b/tests/components/dexcom/test_config_flow.py @@ -17,7 +17,7 @@ async def test_form(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -33,7 +33,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == CONFIG[CONF_USERNAME] assert result2["data"] == CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -54,7 +54,7 @@ async def test_form_account_error(hass): CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -73,7 +73,7 @@ async def test_form_session_error(hass): CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -92,7 +92,7 @@ async def test_form_unknown_error(hass): CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -107,14 +107,14 @@ async def test_option_flow_default(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"] == { CONF_UNIT_OF_MEASUREMENT: MG_DL, } @@ -131,14 +131,14 @@ async def test_option_flow(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_UNIT_OF_MEASUREMENT: MMOL_L}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_UNIT_OF_MEASUREMENT: MMOL_L, } diff --git a/tests/components/dialogflow/test_init.py b/tests/components/dialogflow/test_init.py index 5c4ef3e99f9..8bd185e8011 100644 --- a/tests/components/dialogflow/test_init.py +++ b/tests/components/dialogflow/test_init.py @@ -87,10 +87,10 @@ async def fixture(hass, hass_client_no_auth): result = await hass.config_entries.flow.async_init( "dialogflow", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY webhook_id = result["result"].data["webhook_id"] return await hass_client_no_auth(), webhook_id diff --git a/tests/components/discord/test_config_flow.py b/tests/components/discord/test_config_flow.py index 9d4966929be..b6504e851ca 100644 --- a/tests/components/discord/test_config_flow.py +++ b/tests/components/discord/test_config_flow.py @@ -28,7 +28,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -45,7 +45,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -58,7 +58,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -67,7 +67,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -81,7 +81,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -90,7 +90,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -104,7 +104,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -113,7 +113,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -131,7 +131,7 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: data=entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" new_conf = {CONF_API_TOKEN: "1234567890123"} @@ -141,7 +141,7 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: result["flow_id"], user_input=new_conf, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -150,6 +150,6 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: result["flow_id"], user_input=new_conf, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == CONF_DATA | new_conf diff --git a/tests/components/dlna_dmr/test_config_flow.py b/tests/components/dlna_dmr/test_config_flow.py index 7ec25906f99..8035b7ee822 100644 --- a/tests/components/dlna_dmr/test_config_flow.py +++ b/tests/components/dlna_dmr/test_config_flow.py @@ -84,7 +84,7 @@ async def test_user_flow_undiscovered_manual(hass: HomeAssistant) -> 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -92,7 +92,7 @@ async def test_user_flow_undiscovered_manual(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -118,7 +118,7 @@ async def test_user_flow_discovered_manual( 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -126,7 +126,7 @@ async def test_user_flow_discovered_manual( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -134,7 +134,7 @@ async def test_user_flow_discovered_manual( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -158,7 +158,7 @@ async def test_user_flow_selected(hass: HomeAssistant, ssdp_scanner_mock: Mock) 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -166,7 +166,7 @@ async def test_user_flow_selected(hass: HomeAssistant, ssdp_scanner_mock: Mock) result["flow_id"], user_input={CONF_HOST: MOCK_DEVICE_NAME} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -188,7 +188,7 @@ async def test_user_flow_uncontactable( 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -196,7 +196,7 @@ async def test_user_flow_uncontactable( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} assert result["step_id"] == "manual" @@ -221,7 +221,7 @@ async def test_user_flow_embedded_st( 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -229,7 +229,7 @@ async def test_user_flow_embedded_st( result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -251,7 +251,7 @@ async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) - 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual" @@ -259,7 +259,7 @@ async def test_user_flow_wrong_st(hass: HomeAssistant, domain_data_mock: Mock) - result["flow_id"], user_input={CONF_URL: MOCK_DEVICE_LOCATION} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "not_dmr"} assert result["step_id"] == "manual" @@ -271,7 +271,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -279,7 +279,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -302,7 +302,7 @@ async def test_ssdp_flow_unavailable( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" domain_data_mock.upnp_factory.async_create_device.side_effect = UpnpError @@ -312,7 +312,7 @@ async def test_ssdp_flow_unavailable( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -342,7 +342,7 @@ async def test_ssdp_flow_existing( }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -357,7 +357,7 @@ async def test_ssdp_flow_duplicate_location( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == MOCK_DEVICE_LOCATION @@ -382,7 +382,7 @@ async def test_ssdp_flow_upnp_udn( }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -398,7 +398,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dmr" # Service list does not contain services @@ -410,7 +410,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dmr" # AVTransport service is missing @@ -426,7 +426,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: 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["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dmr" @@ -448,7 +448,7 @@ async def test_ssdp_single_service(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dmr" @@ -462,7 +462,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "alternative_integration" discovery = dataclasses.replace(MOCK_DISCOVERY) @@ -475,7 +475,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "alternative_integration" for manufacturer, model in [ @@ -493,7 +493,7 @@ async def test_ssdp_ignore_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "alternative_integration" @@ -507,7 +507,7 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == {} @@ -526,7 +526,7 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No context={"source": config_entries.SOURCE_UNIGNORE}, data={"unique_id": MOCK_DEVICE_UDN}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -534,7 +534,7 @@ async def test_unignore_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> No ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -559,7 +559,7 @@ async def test_unignore_flow_offline( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == {} @@ -572,7 +572,7 @@ async def test_unignore_flow_offline( context={"source": config_entries.SOURCE_UNIGNORE}, data={"unique_id": MOCK_DEVICE_UDN}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "discovery_error" @@ -583,7 +583,7 @@ async def test_options_flow( config_entry_mock.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry_mock.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {} @@ -597,7 +597,7 @@ async def test_options_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "invalid_url"} @@ -612,7 +612,7 @@ async def test_options_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_LISTEN_PORT: 2222, CONF_CALLBACK_URL_OVERRIDE: "http://override/callback", diff --git a/tests/components/dlna_dms/test_config_flow.py b/tests/components/dlna_dms/test_config_flow.py index 591457b23c6..1d6ac0eaf80 100644 --- a/tests/components/dlna_dms/test_config_flow.py +++ b/tests/components/dlna_dms/test_config_flow.py @@ -86,7 +86,7 @@ async def test_user_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None: 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -95,7 +95,7 @@ async def test_user_flow(hass: HomeAssistant, ssdp_scanner_mock: Mock) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -119,7 +119,7 @@ async def test_user_flow_no_devices( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -130,7 +130,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -138,7 +138,7 @@ async def test_ssdp_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -161,7 +161,7 @@ async def test_ssdp_flow_unavailable( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" upnp_factory_mock.async_create_device.side_effect = UpnpError @@ -171,7 +171,7 @@ async def test_ssdp_flow_unavailable( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: MOCK_DEVICE_LOCATION, @@ -201,7 +201,7 @@ async def test_ssdp_flow_existing( }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -216,7 +216,7 @@ async def test_ssdp_flow_duplicate_location( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == MOCK_DEVICE_LOCATION @@ -230,7 +230,7 @@ async def test_ssdp_flow_bad_data(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "bad_ssdp" # Missing USN @@ -240,7 +240,7 @@ async def test_ssdp_flow_bad_data(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "bad_ssdp" @@ -280,7 +280,7 @@ async def test_duplicate_name( context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -288,7 +288,7 @@ async def test_duplicate_name( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_DEVICE_NAME assert result["data"] == { CONF_URL: new_device_location, @@ -318,7 +318,7 @@ async def test_ssdp_flow_upnp_udn( }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry_mock.data[CONF_URL] == NEW_DEVICE_LOCATION @@ -334,7 +334,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dms" # Service list does not contain services @@ -346,7 +346,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dms" # ContentDirectory service is missing @@ -362,7 +362,7 @@ async def test_ssdp_missing_services(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=discovery ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dms" @@ -384,5 +384,5 @@ async def test_ssdp_single_service(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_dms" diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index cbf455653ff..f79a60e3265 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -43,7 +43,7 @@ async def test_user_form(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} doorbirdapi = _get_mock_doorbirdapi_return_values( @@ -188,7 +188,7 @@ async def test_form_zeroconf_correct_oui(hass): ), ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -251,7 +251,7 @@ async def test_form_zeroconf_correct_oui_wrong_device(hass, doorbell_state_side_ ), ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_doorbird_device" @@ -271,7 +271,7 @@ async def test_form_user_cannot_connect(hass): VALID_CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -312,12 +312,12 @@ async def test_options_flow(hass): ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_EVENTS: "eventa, eventc, eventq"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_EVENTS: ["eventa", "eventc", "eventq"]} diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index ddec7bda888..50fd1e4b7a8 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -562,7 +562,7 @@ async def test_options_flow(hass): with patch( "homeassistant.components.dsmr.async_setup_entry", return_value=True ), patch("homeassistant.components.dsmr.async_unload_entry", return_value=True): - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/dunehd/test_config_flow.py b/tests/components/dunehd/test_config_flow.py index 76585ac73a1..9cc32a40371 100644 --- a/tests/components/dunehd/test_config_flow.py +++ b/tests/components/dunehd/test_config_flow.py @@ -74,7 +74,7 @@ async def test_create_entry(hass): DOMAIN, context={"source": SOURCE_USER}, data=CONFIG_HOSTNAME ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "dunehd-host" assert result["data"] == {CONF_HOST: "dunehd-host"} @@ -90,6 +90,6 @@ async def test_create_entry_with_ipv6_address(hass): data={CONF_HOST: "2001:db8::1428:57ab"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "2001:db8::1428:57ab" assert result["data"] == {CONF_HOST: "2001:db8::1428:57ab"} diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py index 8311b4aba2c..0c80f20859f 100644 --- a/tests/components/ecobee/test_config_flow.py +++ b/tests/components/ecobee/test_config_flow.py @@ -25,7 +25,7 @@ async def test_abort_if_already_setup(hass): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -36,7 +36,7 @@ async def test_user_step_without_user_input(hass): flow.hass.data[DATA_ECOBEE_CONFIG] = {} result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -53,7 +53,7 @@ async def test_pin_request_succeeds(hass): result = await flow.async_step_user(user_input={CONF_API_KEY: "api-key"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authorize" assert result["description_placeholders"] == {"pin": "test-pin"} @@ -70,7 +70,7 @@ async def test_pin_request_fails(hass): result = await flow.async_step_user(user_input={CONF_API_KEY: "api-key"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "pin_request_failed" @@ -92,7 +92,7 @@ async def test_token_request_succeeds(hass): result = await flow.async_step_authorize(user_input={}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"] == { CONF_API_KEY: "test-api-key", @@ -116,7 +116,7 @@ async def test_token_request_fails(hass): result = await flow.async_step_authorize(user_input={}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authorize" assert result["errors"]["base"] == "token_request_failed" assert result["description_placeholders"] == {"pin": "test-pin"} @@ -131,7 +131,7 @@ async def test_import_flow_triggered_but_no_ecobee_conf(hass): result = await flow.async_step_import(import_data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -155,7 +155,7 @@ async def test_import_flow_triggered_with_ecobee_conf_and_valid_data_and_valid_t result = await flow.async_step_import(import_data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"] == { CONF_API_KEY: "test-api-key", diff --git a/tests/components/elmax/test_config_flow.py b/tests/components/elmax/test_config_flow.py index 5b8d42799e9..e4e9889aadd 100644 --- a/tests/components/elmax/test_config_flow.py +++ b/tests/components/elmax/test_config_flow.py @@ -31,7 +31,7 @@ async def test_show_form(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -60,7 +60,7 @@ async def test_standard_setup(hass): }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_one_config_allowed(hass): @@ -94,7 +94,7 @@ async def test_one_config_allowed(hass): CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -115,7 +115,7 @@ async def test_invalid_credentials(hass): }, ) assert login_result["step_id"] == "user" - assert login_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert login_result["type"] == data_entry_flow.FlowResultType.FORM assert login_result["errors"] == {"base": "invalid_auth"} @@ -136,7 +136,7 @@ async def test_connection_error(hass): }, ) assert login_result["step_id"] == "user" - assert login_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert login_result["type"] == data_entry_flow.FlowResultType.FORM assert login_result["errors"] == {"base": "network_error"} @@ -164,7 +164,7 @@ async def test_unhandled_error(hass): }, ) assert result["step_id"] == "panels" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -193,7 +193,7 @@ async def test_invalid_pin(hass): }, ) assert result["step_id"] == "panels" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_pin"} @@ -215,7 +215,7 @@ async def test_no_online_panel(hass): }, ) assert login_result["step_id"] == "user" - assert login_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert login_result["type"] == data_entry_flow.FlowResultType.FORM assert login_result["errors"] == {"base": "no_panel_online"} @@ -231,7 +231,7 @@ async def test_show_reauth(hass): CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -272,7 +272,7 @@ async def test_reauth_flow(hass): CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT await hass.async_block_till_done() assert result["reason"] == "reauth_successful" @@ -316,7 +316,7 @@ async def test_reauth_panel_disappeared(hass): }, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "reauth_panel_disappeared"} @@ -358,7 +358,7 @@ async def test_reauth_invalid_pin(hass): }, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_pin"} @@ -400,5 +400,5 @@ async def test_reauth_bad_login(hass): }, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} diff --git a/tests/components/enocean/test_config_flow.py b/tests/components/enocean/test_config_flow.py index d12b9a580c7..7585c2699cd 100644 --- a/tests/components/enocean/test_config_flow.py +++ b/tests/components/enocean/test_config_flow.py @@ -24,7 +24,7 @@ async def test_user_flow_cannot_create_multiple_instances(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -37,7 +37,7 @@ async def test_user_flow_with_detected_dongle(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "detect" devices = result["data_schema"].schema.get("device").container assert FAKE_DONGLE_PATH in devices @@ -51,7 +51,7 @@ async def test_user_flow_with_no_detected_dongle(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "manual" @@ -64,7 +64,7 @@ async def test_detection_flow_with_valid_path(hass): DOMAIN, context={"source": "detect"}, data={CONF_DEVICE: USER_PROVIDED_PATH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_DEVICE] == USER_PROVIDED_PATH @@ -82,7 +82,7 @@ async def test_detection_flow_with_custom_path(hass): data={CONF_DEVICE: USER_PROVIDED_PATH}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "manual" @@ -100,7 +100,7 @@ async def test_detection_flow_with_invalid_path(hass): data={CONF_DEVICE: USER_PROVIDED_PATH}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "detect" assert CONF_DEVICE in result["errors"] @@ -114,7 +114,7 @@ async def test_manual_flow_with_valid_path(hass): DOMAIN, context={"source": "manual"}, data={CONF_DEVICE: USER_PROVIDED_PATH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_DEVICE] == USER_PROVIDED_PATH @@ -130,7 +130,7 @@ async def test_manual_flow_with_invalid_path(hass): DOMAIN, context={"source": "manual"}, data={CONF_DEVICE: USER_PROVIDED_PATH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "manual" assert CONF_DEVICE in result["errors"] @@ -146,7 +146,7 @@ async def test_import_flow_with_valid_path(hass): data=DATA_TO_IMPORT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_DEVICE] == DATA_TO_IMPORT[CONF_DEVICE] @@ -164,5 +164,5 @@ async def test_import_flow_with_invalid_path(hass): data=DATA_TO_IMPORT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "invalid_dongle_path" diff --git a/tests/components/environment_canada/test_config_flow.py b/tests/components/environment_canada/test_config_flow.py index de3ff516eae..9d484d984a8 100644 --- a/tests/components/environment_canada/test_config_flow.py +++ b/tests/components/environment_canada/test_config_flow.py @@ -64,7 +64,7 @@ async def test_create_entry(hass): flow["flow_id"], FAKE_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == FAKE_CONFIG assert result["title"] == FAKE_TITLE @@ -89,7 +89,7 @@ async def test_create_same_entry_twice(hass): flow["flow_id"], FAKE_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -135,6 +135,6 @@ async def test_lat_lon_not_specified(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=fake_config ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == FAKE_CONFIG assert result["title"] == FAKE_TITLE diff --git a/tests/components/faa_delays/test_config_flow.py b/tests/components/faa_delays/test_config_flow.py index 2ab4aaf6dd6..8b869d4830e 100644 --- a/tests/components/faa_delays/test_config_flow.py +++ b/tests/components/faa_delays/test_config_flow.py @@ -56,7 +56,7 @@ async def test_duplicate_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/fireservicerota/test_config_flow.py b/tests/components/fireservicerota/test_config_flow.py index 0553574ae77..35467ffc449 100644 --- a/tests/components/fireservicerota/test_config_flow.py +++ b/tests/components/fireservicerota/test_config_flow.py @@ -42,7 +42,7 @@ async def test_show_form(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -55,7 +55,7 @@ async def test_abort_if_already_setup(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -91,7 +91,7 @@ async def test_step_user(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_CONF[CONF_USERNAME] assert result["data"] == { "auth_implementation": "fireservicerota", @@ -131,7 +131,7 @@ async def test_reauth(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch( "homeassistant.components.fireservicerota.config_flow.FireServiceRota" @@ -147,5 +147,5 @@ async def test_reauth(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/flick_electric/test_config_flow.py b/tests/components/flick_electric/test_config_flow.py index be4f240efa3..ad6f32c5611 100644 --- a/tests/components/flick_electric/test_config_flow.py +++ b/tests/components/flick_electric/test_config_flow.py @@ -43,7 +43,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "Flick Electric: test-username" assert result2["data"] == CONF assert len(mock_setup_entry.mock_calls) == 1 @@ -65,7 +65,7 @@ async def test_form_duplicate_login(hass): ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -77,7 +77,7 @@ async def test_form_invalid_auth(hass): ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -89,7 +89,7 @@ async def test_form_cannot_connect(hass): ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -101,5 +101,5 @@ async def test_form_generic_exception(hass): ): result = await _flow_submit(hass) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/flipr/test_config_flow.py b/tests/components/flipr/test_config_flow.py index 00c8d7e2401..484cba2f4f4 100644 --- a/tests/components/flipr/test_config_flow.py +++ b/tests/components/flipr/test_config_flow.py @@ -25,7 +25,7 @@ async def test_show_form(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER @@ -67,7 +67,7 @@ async def test_nominal_case(hass, mock_setup): assert len(mock_flipr_client.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "flipid" assert result["data"] == { CONF_EMAIL: "dummylogin", @@ -91,7 +91,7 @@ async def test_multiple_flip_id(hass, mock_setup): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "flipr_id" result = await hass.config_entries.flow.async_configure( @@ -101,7 +101,7 @@ async def test_multiple_flip_id(hass, mock_setup): assert len(mock_flipr_client.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "FLIP2" assert result["data"] == { CONF_EMAIL: "dummylogin", diff --git a/tests/components/flunearyou/test_config_flow.py b/tests/components/flunearyou/test_config_flow.py index 8c22d5fc915..c0fe58ea811 100644 --- a/tests/components/flunearyou/test_config_flow.py +++ b/tests/components/flunearyou/test_config_flow.py @@ -14,7 +14,7 @@ async def test_duplicate_error(hass, config, config_entry, setup_flunearyou): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -35,7 +35,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -44,7 +44,7 @@ async def test_step_user(hass, config, setup_flunearyou): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "51.528308, -0.3817765" assert result["data"] == { CONF_LATITUDE: 51.528308, diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index fd4d82b177c..e810b0eb957 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -58,7 +58,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -78,7 +78,7 @@ async def test_config_flow(hass, config_entry): DOMAIN, context={"source": SOURCE_USER}, data=config_data ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "My Music on myhost" assert result["data"][CONF_HOST] == config_data[CONF_HOST] assert result["data"][CONF_PORT] == config_data[CONF_PORT] @@ -91,7 +91,7 @@ async def test_config_flow(hass, config_entry): data=config_entry.data, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_zeroconf_updates_title(hass, config_entry): @@ -112,7 +112,7 @@ async def test_zeroconf_updates_title(hass, config_entry): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert config_entry.title == "zeroconf_test" assert len(hass.config_entries.async_entries(DOMAIN)) == 2 @@ -128,7 +128,7 @@ async def test_config_flow_no_websocket(hass, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config_entry.data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_config_flow_zeroconf_invalid(hass): @@ -146,7 +146,7 @@ async def test_config_flow_zeroconf_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" # test with forked-daapd version < 27 discovery_info = zeroconf.ZeroconfServiceInfo( @@ -161,7 +161,7 @@ async def test_config_flow_zeroconf_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" # test with verbose mtd-version from Firefly discovery_info = zeroconf.ZeroconfServiceInfo( @@ -176,7 +176,7 @@ async def test_config_flow_zeroconf_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" # test with svn mtd-version from Firefly discovery_info = zeroconf.ZeroconfServiceInfo( @@ -191,7 +191,7 @@ async def test_config_flow_zeroconf_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) # doesn't create the entry, tries to show form but gets abort - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_forked_daapd" @@ -213,7 +213,7 @@ async def test_config_flow_zeroconf_valid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_options_flow(hass, config_entry): @@ -229,7 +229,7 @@ async def test_options_flow(hass, config_entry): 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["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -240,4 +240,4 @@ async def test_options_flow(hass, config_entry): CONF_MAX_PLAYLISTS: 8, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY diff --git a/tests/components/foscam/test_config_flow.py b/tests/components/foscam/test_config_flow.py index 63c30c16bab..e8bdb6900f2 100644 --- a/tests/components/foscam/test_config_flow.py +++ b/tests/components/foscam/test_config_flow.py @@ -80,7 +80,7 @@ async def test_user_valid(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -98,7 +98,7 @@ async def test_user_valid(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == CAMERA_NAME assert result["data"] == VALID_CONFIG @@ -111,7 +111,7 @@ async def test_user_invalid_auth(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -129,7 +129,7 @@ async def test_user_invalid_auth(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -139,7 +139,7 @@ async def test_user_cannot_connect(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -157,7 +157,7 @@ async def test_user_cannot_connect(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -167,7 +167,7 @@ async def test_user_invalid_response(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -187,7 +187,7 @@ async def test_user_invalid_response(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_response"} @@ -203,7 +203,7 @@ async def test_user_already_configured(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -218,7 +218,7 @@ async def test_user_already_configured(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -228,7 +228,7 @@ async def test_user_unknown_exception(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -243,5 +243,5 @@ async def test_user_unknown_exception(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index cee1c28cebd..7922655b3b6 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -44,7 +44,7 @@ async def test_user(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # test with all provided @@ -53,7 +53,7 @@ async def test_user(hass: HomeAssistant): context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" @@ -64,7 +64,7 @@ async def test_import(hass: HomeAssistant): context={"source": SOURCE_IMPORT}, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" @@ -75,7 +75,7 @@ async def test_zeroconf(hass: HomeAssistant): context={"source": SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" @@ -94,7 +94,7 @@ async def test_link(hass: HomeAssistant, router: Mock): ) result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == MOCK_HOST assert result["title"] == MOCK_HOST assert result["data"][CONF_HOST] == MOCK_HOST @@ -118,7 +118,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant): context={"source": SOURCE_IMPORT}, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Should fail, same MOCK_HOST (flow) @@ -127,7 +127,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant): context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST, CONF_PORT: MOCK_PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -144,7 +144,7 @@ async def test_on_link_failed(hass: HomeAssistant): side_effect=AuthorizationError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "register_failed"} with patch( @@ -152,7 +152,7 @@ async def test_on_link_failed(hass: HomeAssistant): side_effect=HttpRequestError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} with patch( @@ -160,5 +160,5 @@ async def test_on_link_failed(hass: HomeAssistant): side_effect=InvalidTokenError(), ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/freedompro/test_config_flow.py b/tests/components/freedompro/test_config_flow.py index 42dc0674d07..d1804437a59 100644 --- a/tests/components/freedompro/test_config_flow.py +++ b/tests/components/freedompro/test_config_flow.py @@ -19,7 +19,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -77,6 +77,6 @@ async def test_create_entry(hass): data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Freedompro" assert result["data"][CONF_API_KEY] == "ksdjfgslkjdfksjdfksjgfksjd" diff --git a/tests/components/gdacs/test_config_flow.py b/tests/components/gdacs/test_config_flow.py index 8496f0ca5a2..a928f5f5810 100644 --- a/tests/components/gdacs/test_config_flow.py +++ b/tests/components/gdacs/test_config_flow.py @@ -29,7 +29,7 @@ async def test_duplicate_error(hass, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -38,7 +38,7 @@ async def test_show_form(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -55,7 +55,7 @@ async def test_step_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, @@ -75,7 +75,7 @@ async def test_step_user(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, diff --git a/tests/components/generic/test_config_flow.py b/tests/components/generic/test_config_flow.py index 592d139f92e..d303e064c1f 100644 --- a/tests/components/generic/test_config_flow.py +++ b/tests/components/generic/test_config_flow.py @@ -68,7 +68,7 @@ async def test_form(hass, fakeimg_png, user_flow, mock_create_stream): user_flow["flow_id"], TESTDATA, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", @@ -104,7 +104,7 @@ async def test_form_only_stillimage(hass, fakeimg_png, user_flow): data, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", @@ -131,7 +131,7 @@ async def test_form_only_stillimage_gif(hass, fakeimg_gif, user_flow): data, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["options"][CONF_CONTENT_TYPE] == "image/gif" @@ -148,7 +148,7 @@ async def test_form_only_svg_whitespace(hass, fakeimgbytes_svg, user_flow): data, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @respx.mock @@ -175,7 +175,7 @@ async def test_form_only_still_sample(hass, user_flow, image_file): data, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @respx.mock @@ -186,31 +186,31 @@ async def test_form_only_still_sample(hass, user_flow, image_file): ( "http://localhost:812{{3}}/static/icons/favicon-apple-180x180.png", "http://localhost:8123/static/icons/favicon-apple-180x180.png", - data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + data_entry_flow.FlowResultType.CREATE_ENTRY, None, ), ( "{% if 1 %}https://bla{% else %}https://yo{% endif %}", "https://bla/", - data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + data_entry_flow.FlowResultType.CREATE_ENTRY, None, ), ( "http://{{example.org", "http://example.org", - data_entry_flow.RESULT_TYPE_FORM, + data_entry_flow.FlowResultType.FORM, {"still_image_url": "template_error"}, ), ( "invalid1://invalid:4\\1", "invalid1://invalid:4%5c1", - data_entry_flow.RESULT_TYPE_FORM, + data_entry_flow.FlowResultType.FORM, {"still_image_url": "malformed_url"}, ), ( "relative/urls/are/not/allowed.jpg", "relative/urls/are/not/allowed.jpg", - data_entry_flow.RESULT_TYPE_FORM, + data_entry_flow.FlowResultType.FORM, {"still_image_url": "relative_url"}, ), ], @@ -246,7 +246,7 @@ async def test_form_rtsp_mode(hass, fakeimg_png, user_flow, mock_create_stream): user_flow["flow_id"], data ) assert "errors" not in result2, f"errors={result2['errors']}" - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "127_0_0_1" assert result2["options"] == { CONF_STILL_IMAGE_URL: "http://127.0.0.1/testurl/1", @@ -280,7 +280,7 @@ async def test_form_only_stream(hass, fakeimgbytes_jpg, mock_create_stream): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "127_0_0_1" assert result3["options"] == { CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, @@ -314,7 +314,7 @@ async def test_form_still_and_stream_not_provided(hass, user_flow): CONF_VERIFY_SSL: False, }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "no_still_image_or_stream_url"} @@ -492,7 +492,7 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # try updating the still image url @@ -503,10 +503,10 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream result["flow_id"], user_input=data, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY result3 = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == "init" # verify that an invalid template reports the correct UI error. @@ -515,7 +515,7 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream result3["flow_id"], user_input=data, ) - assert result4.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result4.get("type") == data_entry_flow.FlowResultType.FORM assert result4["errors"] == {"still_image_url": "template_error"} # verify that an invalid template reports the correct UI error. @@ -526,7 +526,7 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream user_input=data, ) - assert result5.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result5.get("type") == data_entry_flow.FlowResultType.FORM assert result5["errors"] == {"stream_source": "template_error"} # verify that an relative stream url is rejected. @@ -536,7 +536,7 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream result5["flow_id"], user_input=data, ) - assert result6.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result6.get("type") == data_entry_flow.FlowResultType.FORM assert result6["errors"] == {"stream_source": "relative_url"} # verify that an malformed stream url is rejected. @@ -546,7 +546,7 @@ async def test_options_template_error(hass, fakeimgbytes_png, mock_create_stream result6["flow_id"], user_input=data, ) - assert result7.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result7.get("type") == data_entry_flow.FlowResultType.FORM assert result7["errors"] == {"stream_source": "malformed_url"} @@ -583,7 +583,7 @@ async def test_options_only_stream(hass, fakeimgbytes_png, mock_create_stream): await hass.async_block_till_done() result = await hass.config_entries.options.async_init(mock_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # try updating the config options @@ -592,7 +592,7 @@ async def test_options_only_stream(hass, fakeimgbytes_png, mock_create_stream): result["flow_id"], user_input=data, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["data"][CONF_CONTENT_TYPE] == "image/jpeg" @@ -607,12 +607,12 @@ async def test_import(hass, fakeimg_png): result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TESTDATA_YAML ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Yaml Defined Name" await hass.async_block_till_done() # Any name defined in yaml should end up as the entity id. assert hass.states.get("camera.yaml_defined_name") - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT # These above can be deleted after deprecation period is finished. @@ -707,7 +707,7 @@ async def test_use_wallclock_as_timestamps_option( result = await hass.config_entries.options.async_init( mock_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" with patch( "homeassistant.components.generic.async_setup_entry", return_value=True @@ -716,4 +716,4 @@ async def test_use_wallclock_as_timestamps_option( result["flow_id"], user_input={CONF_USE_WALLCLOCK_AS_TIMESTAMPS: True, **TESTDATA}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 013450212ed..9959023e8a6 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -157,10 +157,10 @@ async def webhook_id(hass, geofency_client): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/geonetnz_quakes/test_config_flow.py b/tests/components/geonetnz_quakes/test_config_flow.py index 9b471051656..4e00f691884 100644 --- a/tests/components/geonetnz_quakes/test_config_flow.py +++ b/tests/components/geonetnz_quakes/test_config_flow.py @@ -25,7 +25,7 @@ async def test_duplicate_error(hass, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -34,7 +34,7 @@ async def test_show_form(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -56,7 +56,7 @@ async def test_step_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, @@ -81,7 +81,7 @@ async def test_step_user(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, diff --git a/tests/components/geonetnz_volcano/test_config_flow.py b/tests/components/geonetnz_volcano/test_config_flow.py index 92c25e00927..a4e1f0587b8 100644 --- a/tests/components/geonetnz_volcano/test_config_flow.py +++ b/tests/components/geonetnz_volcano/test_config_flow.py @@ -32,7 +32,7 @@ async def test_show_form(hass): result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -55,7 +55,7 @@ async def test_step_import(hass): "homeassistant.components.geonetnz_volcano.async_setup", return_value=True ): result = await flow.async_step_import(import_config=conf) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, @@ -81,7 +81,7 @@ async def test_step_user(hass): "homeassistant.components.geonetnz_volcano.async_setup", return_value=True ): result = await flow.async_step_user(user_input=conf) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "-41.2, 174.7" assert result["data"] == { CONF_LATITUDE: -41.2, diff --git a/tests/components/gios/test_config_flow.py b/tests/components/gios/test_config_flow.py index 21fc9d8bfda..8ebb514a4d3 100644 --- a/tests/components/gios/test_config_flow.py +++ b/tests/components/gios/test_config_flow.py @@ -25,7 +25,7 @@ async def test_show_form(hass): result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -98,7 +98,7 @@ async def test_create_entry(hass): result = await flow.async_step_user(user_input=CONFIG) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Test Name 1" assert result["data"][CONF_STATION_ID] == CONFIG[CONF_STATION_ID] diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py index 1b2c2434fab..d996c3af533 100644 --- a/tests/components/glances/test_config_flow.py +++ b/tests/components/glances/test_config_flow.py @@ -35,7 +35,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( glances.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch("homeassistant.components.glances.Glances.get_data", autospec=True): @@ -109,14 +109,14 @@ async def test_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={glances.CONF_SCAN_INTERVAL: 10} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { glances.CONF_SCAN_INTERVAL: 10, } diff --git a/tests/components/goalzero/test_config_flow.py b/tests/components/goalzero/test_config_flow.py index 669cace729b..5ebc894fab7 100644 --- a/tests/components/goalzero/test_config_flow.py +++ b/tests/components/goalzero/test_config_flow.py @@ -34,7 +34,7 @@ async def test_flow_user(hass: HomeAssistant): result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA assert result["result"].unique_id == MAC @@ -47,7 +47,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant): DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -58,7 +58,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -70,7 +70,7 @@ async def test_flow_user_invalid_host(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_host" @@ -82,7 +82,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -97,14 +97,14 @@ async def test_dhcp_discovery(hass: HomeAssistant): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == MANUFACTURER assert result["data"] == CONF_DATA assert result["result"].unique_id == MAC @@ -114,7 +114,7 @@ async def test_dhcp_discovery(hass: HomeAssistant): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -128,7 +128,7 @@ async def test_dhcp_discovery_failed(hass: HomeAssistant): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" with patch_config_flow_yeti(mocked_yeti) as yetimock: @@ -138,7 +138,7 @@ async def test_dhcp_discovery_failed(hass: HomeAssistant): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "invalid_host" with patch_config_flow_yeti(mocked_yeti) as yetimock: @@ -148,5 +148,5 @@ async def test_dhcp_discovery_failed(hass: HomeAssistant): context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py index 1426c749552..7f6371c1446 100644 --- a/tests/components/google_travel_time/test_config_flow.py +++ b/tests/components/google_travel_time/test_config_flow.py @@ -36,7 +36,7 @@ async def test_minimum_fields(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -44,7 +44,7 @@ async def test_minimum_fields(hass): MOCK_CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == DEFAULT_NAME assert result2["data"] == { CONF_NAME: DEFAULT_NAME, @@ -60,14 +60,14 @@ async def test_invalid_config_entry(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -91,7 +91,7 @@ async def test_options_flow(hass, mock_config): mock_config.entry_id, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -108,7 +108,7 @@ async def test_options_flow(hass, mock_config): CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"] == { CONF_MODE: "driving", @@ -144,7 +144,7 @@ async def test_options_flow_departure_time(hass, mock_config): mock_config.entry_id, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -161,7 +161,7 @@ async def test_options_flow_departure_time(hass, mock_config): CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"] == { CONF_MODE: "driving", @@ -192,7 +192,7 @@ async def test_dupe(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -204,13 +204,13 @@ async def test_dupe(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -223,4 +223,4 @@ async def test_dupe(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index a885699ca05..4d246cff589 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -65,10 +65,10 @@ async def webhook_id(hass, gpslogger_client): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/gree/test_config_flow.py b/tests/components/gree/test_config_flow.py index 27d290e3b90..81a379e8fd8 100644 --- a/tests/components/gree/test_config_flow.py +++ b/tests/components/gree/test_config_flow.py @@ -23,10 +23,10 @@ async def test_creating_entry_sets_up_climate(hass): ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -52,10 +52,10 @@ async def test_creating_entry_has_no_devices(hass): ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT await hass.async_block_till_done() diff --git a/tests/components/growatt_server/test_config_flow.py b/tests/components/growatt_server/test_config_flow.py index ba52e09296c..f4e499a3eda 100644 --- a/tests/components/growatt_server/test_config_flow.py +++ b/tests/components/growatt_server/test_config_flow.py @@ -50,7 +50,7 @@ async def test_show_authenticate_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -68,7 +68,7 @@ async def test_incorrect_login(hass): result["flow_id"], FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -110,7 +110,7 @@ async def test_multiple_plant_ids(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "plant" user_input = {CONF_PLANT_ID: "123456"} @@ -119,7 +119,7 @@ async def test_multiple_plant_ids(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] assert result["data"][CONF_PLANT_ID] == "123456" @@ -144,7 +144,7 @@ async def test_one_plant_on_account(hass): result["flow_id"], user_input ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] assert result["data"][CONF_PLANT_ID] == "123456" diff --git a/tests/components/guardian/test_config_flow.py b/tests/components/guardian/test_config_flow.py index a56aa6355e8..5e14fc07223 100644 --- a/tests/components/guardian/test_config_flow.py +++ b/tests/components/guardian/test_config_flow.py @@ -21,7 +21,7 @@ async def test_duplicate_error(hass, config, config_entry, setup_guardian): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -34,7 +34,7 @@ async def test_connect_error(hass, config): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_IP_ADDRESS: "cannot_connect"} @@ -55,13 +55,13 @@ async def test_step_user(hass, config, setup_guardian): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "ABCDEF123456" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -85,13 +85,13 @@ async def test_step_zeroconf(hass, setup_guardian): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovery_confirm" 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["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "ABCDEF123456" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -115,7 +115,7 @@ async def test_step_zeroconf_already_in_progress(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( @@ -136,13 +136,13 @@ async def test_step_dhcp(hass, setup_guardian): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovery_confirm" 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["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "ABCDEF123456" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -162,7 +162,7 @@ async def test_step_dhcp_already_in_progress(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( @@ -188,7 +188,7 @@ async def test_step_dhcp_already_setup_match_mac(hass): macaddress="aa:bb:cc:dd:ab:cd", ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -210,5 +210,5 @@ async def test_step_dhcp_already_setup_match_ip(hass): macaddress="aa:bb:cc:dd:ab:cd", ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/hangouts/test_config_flow.py b/tests/components/hangouts/test_config_flow.py index 93f909d3bd4..5df675a0f05 100644 --- a/tests/components/hangouts/test_config_flow.py +++ b/tests/components/hangouts/test_config_flow.py @@ -20,7 +20,7 @@ async def test_flow_works(hass, aioclient_mock): result = await flow.async_step_user( {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == EMAIL @@ -38,7 +38,7 @@ async def test_flow_works_with_authcode(hass, aioclient_mock): "authorization_code": "c29tZXJhbmRvbXN0cmluZw==", } ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == EMAIL @@ -57,12 +57,12 @@ async def test_flow_works_with_2fa(hass, aioclient_mock): result = await flow.async_step_user( {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "2fa" with patch("homeassistant.components.hangouts.config_flow.get_auth"): result = await flow.async_step_2fa({"2fa": 123456}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == EMAIL @@ -81,7 +81,7 @@ async def test_flow_with_unknown_2fa(hass, aioclient_mock): result = await flow.async_step_user( {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_2fa_method" @@ -100,7 +100,7 @@ async def test_flow_invalid_login(hass, aioclient_mock): result = await flow.async_step_user( {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_login" @@ -119,7 +119,7 @@ async def test_flow_invalid_2fa(hass, aioclient_mock): result = await flow.async_step_user( {CONF_EMAIL: EMAIL, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "2fa" with patch( @@ -128,5 +128,5 @@ async def test_flow_invalid_2fa(hass, aioclient_mock): ): result = await flow.async_step_2fa({"2fa": 123456}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_2fa" diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index 7725e9752f5..252cfe8923c 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -201,7 +201,7 @@ async def test_options_flow(hass, mock_hc, mock_write_config): assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -209,7 +209,7 @@ async def test_options_flow(hass, mock_hc, mock_write_config): user_input={"activity": PREVIOUS_ACTIVE_ACTIVITY, "delay_secs": 0.4}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "activity": PREVIOUS_ACTIVE_ACTIVITY, "delay_secs": 0.4, diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index d1d940671b8..e7a37ab7105 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -18,7 +18,7 @@ async def test_flow_aborts_already_setup(hass, config_entry): flow = HeosFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -27,7 +27,7 @@ async def test_no_host_shows_form(hass): flow = HeosFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -38,7 +38,7 @@ async def test_cannot_connect_shows_error_form(hass, controller): result = await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "127.0.0.1"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_HOST] == "cannot_connect" assert controller.connect.call_count == 1 @@ -54,7 +54,7 @@ async def test_create_entry_when_host_valid(hass, controller): result = await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": SOURCE_USER}, data=data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DOMAIN assert result["title"] == "Controller (127.0.0.1)" assert result["data"] == data @@ -70,7 +70,7 @@ async def test_create_entry_when_friendly_name_valid(hass, controller): result = await hass.config_entries.flow.async_init( heos.DOMAIN, context={"source": SOURCE_USER}, data=data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DOMAIN assert result["title"] == "Controller (127.0.0.1)" assert result["data"] == {CONF_HOST: "127.0.0.1"} @@ -118,7 +118,7 @@ async def test_discovery_flow_aborts_already_setup( flow = HeosFlowHandler() flow.hass = hass result = await flow.async_step_ssdp(discovery_data) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -151,5 +151,5 @@ async def test_import_sets_the_unique_id(hass, controller): data={CONF_HOST: "127.0.0.2"}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == DOMAIN diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py index c1ce6f823ae..dc9eedfa3f6 100644 --- a/tests/components/here_travel_time/test_config_flow.py +++ b/tests/components/here_travel_time/test_config_flow.py @@ -141,7 +141,7 @@ async def test_step_user(hass: HomeAssistant, menu_options) -> None: 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -166,7 +166,7 @@ async def test_step_origin_coordinates( menu_result = await hass.config_entries.flow.async_configure( user_step_result["flow_id"], {"next_step_id": "origin_coordinates"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert menu_result["type"] == data_entry_flow.FlowResultType.FORM location_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], @@ -189,7 +189,7 @@ async def test_step_origin_entity( menu_result = await hass.config_entries.flow.async_configure( user_step_result["flow_id"], {"next_step_id": "origin_entity"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert menu_result["type"] == data_entry_flow.FlowResultType.FORM entity_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], @@ -206,7 +206,7 @@ async def test_step_destination_coordinates( menu_result = await hass.config_entries.flow.async_configure( origin_step_result["flow_id"], {"next_step_id": "destination_coordinates"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert menu_result["type"] == data_entry_flow.FlowResultType.FORM location_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], @@ -218,7 +218,9 @@ async def test_step_destination_coordinates( } }, ) - assert location_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert ( + location_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + ) entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.data == { CONF_NAME: "test", @@ -239,13 +241,13 @@ async def test_step_destination_entity( menu_result = await hass.config_entries.flow.async_configure( origin_step_result["flow_id"], {"next_step_id": "destination_entity"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert menu_result["type"] == data_entry_flow.FlowResultType.FORM entity_selector_result = await hass.config_entries.flow.async_configure( menu_result["flow_id"], {"destination_entity_id": "zone.home"}, ) - assert entity_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert entity_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.data == { CONF_NAME: "test", @@ -327,7 +329,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -350,7 +352,7 @@ async def test_options_flow_arrival_time_step( menu_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], {"next_step_id": "arrival_time"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert menu_result["type"] == data_entry_flow.FlowResultType.FORM time_selector_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], user_input={ @@ -358,7 +360,7 @@ async def test_options_flow_arrival_time_step( }, ) - assert time_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert time_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.options == { CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, @@ -376,7 +378,7 @@ async def test_options_flow_departure_time_step( menu_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], {"next_step_id": "departure_time"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert menu_result["type"] == data_entry_flow.FlowResultType.FORM time_selector_result = await hass.config_entries.options.async_configure( option_init_result["flow_id"], user_input={ @@ -384,7 +386,7 @@ async def test_options_flow_departure_time_step( }, ) - assert time_selector_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert time_selector_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.options == { CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, @@ -403,7 +405,7 @@ async def test_options_flow_no_time_step( option_init_result["flow_id"], {"next_step_id": "no_time"} ) - assert menu_result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert menu_result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.options == { CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC, @@ -434,7 +436,7 @@ async def test_import_flow_entity_id(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "namespace test_name" entry = hass.config_entries.async_entries(DOMAIN)[0] @@ -476,7 +478,7 @@ async def test_import_flow_coordinates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "test_name" entry = hass.config_entries.async_entries(DOMAIN)[0] @@ -519,7 +521,7 @@ async def test_dupe_import(hass: HomeAssistant) -> None: CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( @@ -539,7 +541,7 @@ async def test_dupe_import(hass: HomeAssistant) -> None: CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( @@ -559,7 +561,7 @@ async def test_dupe_import(hass: HomeAssistant) -> None: CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( @@ -579,7 +581,7 @@ async def test_dupe_import(hass: HomeAssistant) -> None: CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( @@ -599,5 +601,5 @@ async def test_dupe_import(hass: HomeAssistant) -> None: CONF_TRAFFIC_MODE: TRAFFIC_MODE_ENABLED, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/hisense_aehw4a1/test_init.py b/tests/components/hisense_aehw4a1/test_init.py index ef99c3d1c96..4905638a594 100644 --- a/tests/components/hisense_aehw4a1/test_init.py +++ b/tests/components/hisense_aehw4a1/test_init.py @@ -22,10 +22,10 @@ async def test_creating_entry_sets_up_climate_discovery(hass): ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/hive/test_config_flow.py b/tests/components/hive/test_config_flow.py index e6e2a06501a..21cae9f8366 100644 --- a/tests/components/hive/test_config_flow.py +++ b/tests/components/hive/test_config_flow.py @@ -46,7 +46,7 @@ async def test_import_flow(hass): data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == USERNAME assert result["data"] == { CONF_USERNAME: USERNAME, @@ -70,7 +70,7 @@ async def test_user_flow(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -94,7 +94,7 @@ async def test_user_flow(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == USERNAME assert result2["data"] == { CONF_USERNAME: USERNAME, @@ -119,7 +119,7 @@ async def test_user_flow_2fa(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -136,7 +136,7 @@ async def test_user_flow_2fa(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -157,7 +157,7 @@ async def test_user_flow_2fa(hass): }, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == "configuration" assert result3["errors"] == {} @@ -185,7 +185,7 @@ async def test_user_flow_2fa(hass): ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result4["title"] == USERNAME assert result4["data"] == { CONF_USERNAME: USERNAME, @@ -239,7 +239,7 @@ async def test_reauth_flow(hass): data=mock_config.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_password"} with patch( @@ -263,7 +263,7 @@ async def test_reauth_flow(hass): assert mock_config.data.get("username") == USERNAME assert mock_config.data.get("password") == UPDATED_PASSWORD - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -298,7 +298,7 @@ async def test_reauth_2fa_flow(hass): data=mock_config.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_password"} with patch( @@ -338,7 +338,7 @@ async def test_reauth_2fa_flow(hass): assert mock_config.data.get("username") == USERNAME assert mock_config.data.get("password") == UPDATED_PASSWORD - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -370,14 +370,14 @@ async def test_option_flow(hass): data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SCAN_INTERVAL: UPDATED_SCAN_INTERVAL} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_SCAN_INTERVAL] == UPDATED_SCAN_INTERVAL @@ -387,7 +387,7 @@ async def test_user_flow_2fa_send_new_code(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -404,7 +404,7 @@ async def test_user_flow_2fa_send_new_code(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -419,7 +419,7 @@ async def test_user_flow_2fa_send_new_code(hass): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == CONF_CODE assert result3["errors"] == {} @@ -440,7 +440,7 @@ async def test_user_flow_2fa_send_new_code(hass): }, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["type"] == data_entry_flow.FlowResultType.FORM assert result4["step_id"] == "configuration" assert result4["errors"] == {} @@ -465,7 +465,7 @@ async def test_user_flow_2fa_send_new_code(hass): ) await hass.async_block_till_done() - assert result5["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result5["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result5["title"] == USERNAME assert result5["data"] == { CONF_USERNAME: USERNAME, @@ -507,7 +507,7 @@ async def test_abort_if_existing_entry(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -517,7 +517,7 @@ async def test_user_flow_invalid_username(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -529,7 +529,7 @@ async def test_user_flow_invalid_username(hass): {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_username"} @@ -540,7 +540,7 @@ async def test_user_flow_invalid_password(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -552,7 +552,7 @@ async def test_user_flow_invalid_password(hass): {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_password"} @@ -564,7 +564,7 @@ async def test_user_flow_no_internet_connection(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -576,7 +576,7 @@ async def test_user_flow_no_internet_connection(hass): {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "no_internet_available"} @@ -588,7 +588,7 @@ async def test_user_flow_2fa_no_internet_connection(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -602,7 +602,7 @@ async def test_user_flow_2fa_no_internet_connection(hass): {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -615,7 +615,7 @@ async def test_user_flow_2fa_no_internet_connection(hass): {CONF_CODE: MFA_CODE}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == CONF_CODE assert result3["errors"] == {"base": "no_internet_available"} @@ -626,7 +626,7 @@ async def test_user_flow_2fa_invalid_code(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -640,7 +640,7 @@ async def test_user_flow_2fa_invalid_code(hass): {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == CONF_CODE assert result2["errors"] == {} @@ -652,7 +652,7 @@ async def test_user_flow_2fa_invalid_code(hass): result["flow_id"], {CONF_CODE: MFA_INVALID_CODE}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == CONF_CODE assert result3["errors"] == {"base": "invalid_code"} @@ -663,7 +663,7 @@ async def test_user_flow_unknown_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -676,7 +676,7 @@ async def test_user_flow_unknown_error(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -686,7 +686,7 @@ async def test_user_flow_2fa_unknown_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -700,7 +700,7 @@ async def test_user_flow_2fa_unknown_error(hass): {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == CONF_CODE with patch( @@ -712,7 +712,7 @@ async def test_user_flow_2fa_unknown_error(hass): {CONF_CODE: MFA_CODE}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == "configuration" assert result3["errors"] == {} @@ -733,6 +733,6 @@ async def test_user_flow_2fa_unknown_error(hass): ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["type"] == data_entry_flow.FlowResultType.FORM assert result4["step_id"] == "configuration" assert result4["errors"] == {"base": "unknown"} diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index fa7d3fee8f0..bb1eb8d1897 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -42,7 +42,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" diff --git a/tests/components/home_plus_control/test_config_flow.py b/tests/components/home_plus_control/test_config_flow.py index cdf1f85f187..e86362c3e11 100644 --- a/tests/components/home_plus_control/test_config_flow.py +++ b/tests/components/home_plus_control/test_config_flow.py @@ -47,7 +47,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "auth" assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" @@ -77,7 +77,7 @@ async def test_full_flow( result = await hass.config_entries.flow.async_configure(result["flow_id"]) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Home+ Control" config_data = result["data"] assert config_data["token"]["refresh_token"] == "mock-refresh-token" @@ -109,7 +109,7 @@ async def test_abort_if_entry_in_progress(hass, current_request_with_host): result = await hass.config_entries.flow.async_init( "home_plus_control", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -134,7 +134,7 @@ async def test_abort_if_entry_exists(hass, current_request_with_host): result = await hass.config_entries.flow.async_init( "home_plus_control", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -165,7 +165,7 @@ async def test_abort_if_invalid_token( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "auth" assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" @@ -189,5 +189,5 @@ async def test_abort_if_invalid_token( ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "oauth_error" diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index ce0bed0ff52..1b2a0b4e211 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -56,7 +56,7 @@ async def test_setup_in_bridge_mode(hass, mock_get_source_ip): result["flow_id"], {"include_domains": ["light"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "pairing" with patch( @@ -74,7 +74,7 @@ async def test_setup_in_bridge_mode(hass, mock_get_source_ip): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY bridge_name = (result3["title"].split(":"))[0] assert bridge_name == SHORT_BRIDGE_NAME assert result3["data"] == { @@ -112,7 +112,7 @@ async def test_setup_in_bridge_mode_name_taken(hass, mock_get_source_ip): result["flow_id"], {"include_domains": ["light"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "pairing" with patch( @@ -130,7 +130,7 @@ async def test_setup_in_bridge_mode_name_taken(hass, mock_get_source_ip): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] != SHORT_BRIDGE_NAME assert result3["title"].startswith(SHORT_BRIDGE_NAME) bridge_name = (result3["title"].split(":"))[0] @@ -194,7 +194,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices( result["flow_id"], {"include_domains": ["camera", "media_player", "light", "lock", "remote"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "pairing" with patch( @@ -212,7 +212,7 @@ async def test_setup_creates_entries_for_accessory_mode_devices( ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"][:11] == "HASS Bridge" bridge_name = (result3["title"].split(":"))[0] assert result3["data"] == { @@ -257,7 +257,7 @@ async def test_import(hass, mock_get_source_ip): context={"source": config_entries.SOURCE_IMPORT}, data={CONF_NAME: "mock_name", CONF_PORT: 12345}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "port_name_in_use" with patch( @@ -273,7 +273,7 @@ async def test_import(hass, mock_get_source_ip): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "othername:56789" assert result2["data"] == { "name": "othername", @@ -296,7 +296,7 @@ async def test_options_flow_exclude_mode_advanced(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -307,14 +307,14 @@ async def test_options_flow_exclude_mode_advanced(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"entities": ["climate.old"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "advanced" with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): @@ -323,7 +323,7 @@ async def test_options_flow_exclude_mode_advanced(hass, mock_get_source_ip): user_input={}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "devices": [], "mode": "bridge", @@ -351,7 +351,7 @@ async def test_options_flow_exclude_mode_basic(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -362,7 +362,7 @@ async def test_options_flow_exclude_mode_basic(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" entities = result["data_schema"]({})["entities"] assert entities == ["climate.front_gate"] @@ -375,7 +375,7 @@ async def test_options_flow_exclude_mode_basic(hass, mock_get_source_ip): result["flow_id"], user_input={"entities": ["climate.old"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -430,7 +430,7 @@ async def test_options_flow_devices( config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -441,7 +441,7 @@ async def test_options_flow_devices( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" entry = entity_reg.async_get("light.ceiling_lights") @@ -461,7 +461,7 @@ async def test_options_flow_devices( user_input={"devices": [device_id]}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "devices": [device_id], "mode": "bridge", @@ -510,7 +510,7 @@ async def test_options_flow_devices_preserved_when_advanced_off( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -521,7 +521,7 @@ async def test_options_flow_devices_preserved_when_advanced_off( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( @@ -531,7 +531,7 @@ async def test_options_flow_devices_preserved_when_advanced_off( }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "devices": ["1fabcabcabcabcabcabcabcabcabc"], "mode": "bridge", @@ -567,7 +567,7 @@ async def test_options_flow_include_mode_with_non_existant_entity( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -578,7 +578,7 @@ async def test_options_flow_include_mode_with_non_existant_entity( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "include" entities = result["data_schema"]({})["entities"] @@ -590,7 +590,7 @@ async def test_options_flow_include_mode_with_non_existant_entity( "entities": ["climate.new", "climate.front_gate"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -626,7 +626,7 @@ async def test_options_flow_exclude_mode_with_non_existant_entity( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -637,7 +637,7 @@ async def test_options_flow_exclude_mode_with_non_existant_entity( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" entities = result["data_schema"]({})["entities"] @@ -649,7 +649,7 @@ async def test_options_flow_exclude_mode_with_non_existant_entity( "entities": ["climate.new", "climate.front_gate"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -676,7 +676,7 @@ async def test_options_flow_include_mode_basic(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -687,14 +687,14 @@ async def test_options_flow_include_mode_basic(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "include" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"entities": ["climate.new"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -723,7 +723,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -734,7 +734,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( @@ -743,7 +743,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -751,7 +751,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): user_input={"camera_copy": ["camera.native_h264"]}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -769,7 +769,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -780,7 +780,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" result2 = await hass.config_entries.options.async_configure( @@ -789,7 +789,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -797,7 +797,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass, mock_get_source_ip): user_input={"camera_copy": ["camera.native_h264"]}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", @@ -828,7 +828,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -839,7 +839,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "include" result2 = await hass.config_entries.options.async_configure( @@ -848,7 +848,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): "entities": ["camera.native_h264", "camera.transcode_h264"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -856,7 +856,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): user_input={"camera_copy": ["camera.native_h264"]}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -874,7 +874,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": ["fan", "vacuum", "climate", "camera"], @@ -899,7 +899,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" assert result["data_schema"]({}) == { "entities": ["camera.native_h264", "camera.transcode_h264"], @@ -916,7 +916,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" assert result2["data_schema"]({}) == { "camera_copy": ["camera.native_h264"], @@ -930,7 +930,7 @@ async def test_options_flow_include_mode_with_cameras(hass, mock_get_source_ip): user_input={"camera_copy": []}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "entity_config": {"camera.native_h264": {}}, "filter": { @@ -960,7 +960,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -971,7 +971,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "include" result2 = await hass.config_entries.options.async_configure( @@ -980,7 +980,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): "entities": ["camera.audio", "camera.no_audio"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" result3 = await hass.config_entries.options.async_configure( @@ -988,7 +988,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): user_input={"camera_audio": ["camera.audio"]}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -1006,7 +1006,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": ["fan", "vacuum", "climate", "camera"], @@ -1031,7 +1031,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "exclude" assert result["data_schema"]({}) == { "entities": ["camera.audio", "camera.no_audio"], @@ -1048,7 +1048,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): "entities": ["climate.old", "camera.excluded"], }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" assert result2["data_schema"]({}) == { "camera_copy": [], @@ -1062,7 +1062,7 @@ async def test_options_flow_with_camera_audio(hass, mock_get_source_ip): user_input={"camera_audio": []}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "entity_config": {"camera.audio": {}}, "filter": { @@ -1103,7 +1103,7 @@ async def test_options_flow_blocked_when_from_yaml(hass, mock_get_source_ip): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "yaml" with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): @@ -1111,7 +1111,7 @@ async def test_options_flow_blocked_when_from_yaml(hass, mock_get_source_ip): result["flow_id"], user_input={}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @patch(f"{PATH_HOMEKIT}.async_port_is_available", return_value=True) @@ -1131,7 +1131,7 @@ async def test_options_flow_include_mode_basic_accessory( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1151,7 +1151,7 @@ async def test_options_flow_include_mode_basic_accessory( user_input={"domains": ["media_player"], "mode": "accessory"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "accessory" assert _get_schema_default(result2["data_schema"].schema, "entities") is None @@ -1159,7 +1159,7 @@ async def test_options_flow_include_mode_basic_accessory( result2["flow_id"], user_input={"entities": "media_player.tv"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "accessory", "filter": { @@ -1177,7 +1177,7 @@ async def test_options_flow_include_mode_basic_accessory( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": ["media_player"], @@ -1190,7 +1190,7 @@ async def test_options_flow_include_mode_basic_accessory( user_input={"domains": ["media_player"], "mode": "accessory"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "accessory" assert ( _get_schema_default(result2["data_schema"].schema, "entities") @@ -1201,7 +1201,7 @@ async def test_options_flow_include_mode_basic_accessory( result2["flow_id"], user_input={"entities": "media_player.tv"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "accessory", "filter": { @@ -1226,7 +1226,7 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou result["flow_id"], {"include_domains": ["light"]}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "pairing" # We need to actually setup the config entry or the data @@ -1244,7 +1244,7 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"][:11] == "HASS Bridge" bridge_name = (result3["title"].split(":"))[0] assert result3["data"] == { @@ -1272,7 +1272,7 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert _get_schema_default(schema, "mode") == "bridge" @@ -1283,14 +1283,14 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou user_input={"domains": ["camera"], "mode": "accessory"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "accessory" result2 = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"entities": "camera.tv"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "cameras" with patch( @@ -1305,7 +1305,7 @@ async def test_converting_bridge_to_accessory_mode(hass, hk_driver, mock_get_sou ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "entity_config": {"camera.tv": {"video_codec": "copy"}}, "mode": "accessory", @@ -1363,7 +1363,7 @@ async def test_options_flow_exclude_mode_skips_category_entities( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1387,7 +1387,7 @@ async def test_options_flow_exclude_mode_skips_category_entities( }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "exclude" assert _get_schema_default(result2["data_schema"].schema, "entities") == [] @@ -1409,7 +1409,7 @@ async def test_options_flow_exclude_mode_skips_category_entities( ] }, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -1451,7 +1451,7 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1475,7 +1475,7 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "exclude" assert _get_schema_default(result2["data_schema"].schema, "entities") == [] @@ -1491,7 +1491,7 @@ async def test_options_flow_exclude_mode_skips_hidden_entities( result2["flow_id"], user_input={"entities": ["media_player.tv", "switch.other"]}, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { @@ -1529,7 +1529,7 @@ async def test_options_flow_include_mode_allows_hidden_entities( config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { "domains": [ @@ -1553,7 +1553,7 @@ async def test_options_flow_include_mode_allows_hidden_entities( }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "include" assert _get_schema_default(result2["data_schema"].schema, "entities") == [] @@ -1569,7 +1569,7 @@ async def test_options_flow_include_mode_allows_hidden_entities( ] }, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { "mode": "bridge", "filter": { diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index b93e359c1ac..d877133bdcd 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -29,7 +29,7 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -53,7 +53,7 @@ async def test_create_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == FAKE_CONFIG @@ -70,7 +70,7 @@ async def test_show_option_form( result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" @@ -91,7 +91,7 @@ async def test_create_option_entry( user_input={CONF_COOL_AWAY_TEMPERATURE: 1, CONF_HEAT_AWAY_TEMPERATURE: 2}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_COOL_AWAY_TEMPERATURE: 1, CONF_HEAT_AWAY_TEMPERATURE: 2, diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index b363836cc4f..84f66e8f0ab 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -41,7 +41,7 @@ async def test_show_set_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -54,7 +54,7 @@ async def test_urlize_plain_host(hass, requests_mock): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=user_input ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert user_input[CONF_URL] == f"http://{host}/" @@ -85,7 +85,7 @@ async def test_already_configured(hass, requests_mock, login_requests_mock): data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -96,7 +96,7 @@ async def test_connection_error(hass, requests_mock): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_URL: "unknown"} @@ -142,7 +142,7 @@ async def test_login_error(hass, login_requests_mock, code, errors): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == errors @@ -164,7 +164,7 @@ async def test_success(hass, login_requests_mock): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] @@ -195,7 +195,7 @@ async def test_ssdp(hass): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["data_schema"]({})[CONF_URL] == url @@ -209,7 +209,7 @@ async def test_options(hass): config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" recipient = "+15555550000" diff --git a/tests/components/huisbaasje/test_config_flow.py b/tests/components/huisbaasje/test_config_flow.py index 4b0ae908a2d..8aac11baf6d 100644 --- a/tests/components/huisbaasje/test_config_flow.py +++ b/tests/components/huisbaasje/test_config_flow.py @@ -17,7 +17,7 @@ async def test_form(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -68,7 +68,7 @@ async def test_form_invalid_auth(hass): }, ) - assert form_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert form_result["type"] == data_entry_flow.FlowResultType.FORM assert form_result["errors"] == {"base": "invalid_auth"} @@ -90,7 +90,7 @@ async def test_form_cannot_connect(hass): }, ) - assert form_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert form_result["type"] == data_entry_flow.FlowResultType.FORM assert form_result["errors"] == {"base": "cannot_connect"} @@ -112,7 +112,7 @@ async def test_form_unknown_error(hass): }, ) - assert form_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert form_result["type"] == data_entry_flow.FlowResultType.FORM assert form_result["errors"] == {"base": "unknown"} @@ -148,5 +148,5 @@ async def test_form_entry_exists(hass): }, ) - assert form_result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert form_result["type"] == data_entry_flow.FlowResultType.ABORT assert form_result["reason"] == "already_configured" diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index 9c510bb3db0..126e61cf629 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -273,7 +273,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -281,7 +281,7 @@ async def test_options_flow(hass): user_input={CONF_FILTER: ["0"], CONF_OFFSET: 15, CONF_REAL_TIME: False}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_FILTER: [ { @@ -329,7 +329,7 @@ async def test_options_flow_invalid_auth(hass): ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "invalid_auth"} @@ -364,7 +364,7 @@ async def test_options_flow_cannot_connect(hass): ): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index 5d2e77e8adb..27f6f25856d 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -166,7 +166,7 @@ async def test_user_if_no_configuration(hass: HomeAssistant) -> None: """Check flow behavior when no configuration is present.""" result = await _init_flow(hass) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["handler"] == DOMAIN @@ -181,7 +181,7 @@ async def test_user_existing_id_abort(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -197,7 +197,7 @@ async def test_user_client_errors(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" # Fail the auth check call. @@ -207,7 +207,7 @@ async def test_user_client_errors(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "auth_required_error" @@ -226,7 +226,7 @@ async def test_user_confirm_cannot_connect(hass: HomeAssistant) -> None: side_effect=[good_client, bad_client], ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -242,7 +242,7 @@ async def test_user_confirm_id_error(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_id" @@ -256,7 +256,7 @@ async def test_user_noauth_flow_success(hass: HomeAssistant) -> None: ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -275,7 +275,7 @@ async def test_user_auth_required(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -289,7 +289,7 @@ async def test_auth_static_token_auth_required_fail(hass: HomeAssistant) -> None "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "auth_required_error" @@ -309,7 +309,7 @@ async def test_auth_static_token_success(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -335,7 +335,7 @@ async def test_auth_static_token_login_connect_fail(hass: HomeAssistant) -> None hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -358,7 +358,7 @@ async def test_auth_static_token_login_fail(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "invalid_access_token" @@ -373,7 +373,7 @@ async def test_auth_create_token_approval_declined(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_FAIL) @@ -387,7 +387,7 @@ async def test_auth_create_token_approval_declined(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "create_token" assert result["description_placeholders"] == { CONF_AUTH_ID: TEST_AUTH_ID, @@ -395,13 +395,13 @@ async def test_auth_create_token_approval_declined(hass: HomeAssistant) -> None: result = await _configure_flow(hass, result) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "create_token_external" # The flow will be automatically advanced by the auth token response. result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "auth_new_token_not_granted_error" @@ -479,7 +479,7 @@ async def test_auth_create_token_when_issued_token_fails( "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_SUCCESS) @@ -492,14 +492,14 @@ async def test_auth_create_token_when_issued_token_fails( result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "create_token" assert result["description_placeholders"] == { CONF_AUTH_ID: TEST_AUTH_ID, } result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "create_token_external" # The flow will be automatically advanced by the auth token response. @@ -508,7 +508,7 @@ async def test_auth_create_token_when_issued_token_fails( client.async_client_connect = AsyncMock(return_value=False) result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -523,7 +523,7 @@ async def test_auth_create_token_success(hass: HomeAssistant) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ): result = await _configure_flow(hass, result, user_input=TEST_HOST_PORT) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_SUCCESS) @@ -536,19 +536,19 @@ async def test_auth_create_token_success(hass: HomeAssistant) -> None: result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "create_token" assert result["description_placeholders"] == { CONF_AUTH_ID: TEST_AUTH_ID, } result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "create_token_external" # The flow will be automatically advanced by the auth token response. result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -594,7 +594,7 @@ async def test_auth_create_token_success_but_login_fail( # The flow will be automatically advanced by the auth token response. result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "auth_new_token_not_work_error" @@ -614,7 +614,7 @@ async def test_ssdp_success(hass: HomeAssistant) -> None: ): result = await _configure_flow(hass, result) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["handler"] == DOMAIN assert result["title"] == TEST_TITLE assert result["data"] == { @@ -635,7 +635,7 @@ async def test_ssdp_cannot_connect(hass: HomeAssistant) -> None: result = await _init_flow(hass, source=SOURCE_SSDP, data=TEST_SSDP_SERVICE_INFO) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -653,7 +653,7 @@ async def test_ssdp_missing_serial(hass: HomeAssistant) -> None: result = await _init_flow(hass, source=SOURCE_SSDP, data=bad_data) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_id" @@ -672,7 +672,7 @@ async def test_ssdp_failure_bad_port_json(hass: HomeAssistant) -> None: result = await _configure_flow(hass, result) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_PORT] == const.DEFAULT_PORT_JSON @@ -693,7 +693,7 @@ async def test_ssdp_failure_bad_port_ui(hass: HomeAssistant) -> None: ): result = await _init_flow(hass, source=SOURCE_SSDP, data=bad_data) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" client.async_request_token = AsyncMock(return_value=TEST_REQUEST_TOKEN_FAIL) @@ -702,7 +702,7 @@ async def test_ssdp_failure_bad_port_ui(hass: HomeAssistant) -> None: hass, result, user_input={CONF_CREATE_TOKEN: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "create_token" # Verify a working URL is used despite the bad port number @@ -726,8 +726,8 @@ async def test_ssdp_abort_duplicates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result_1["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result_2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result_1["type"] == data_entry_flow.FlowResultType.FORM + assert result_2["type"] == data_entry_flow.FlowResultType.ABORT assert result_2["reason"] == "already_in_progress" @@ -745,7 +745,7 @@ async def test_options_priority(hass: HomeAssistant) -> None: assert hass.states.get(TEST_ENTITY_ID_1) is not None result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" new_priority = 1 @@ -754,7 +754,7 @@ async def test_options_priority(hass: HomeAssistant) -> None: user_input={CONF_PRIORITY: new_priority}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_PRIORITY] == new_priority # Turn the light on and ensure the new priority is used. @@ -788,14 +788,14 @@ async def test_options_effect_show_list(hass: HomeAssistant) -> None: 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["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_EFFECT_SHOW_LIST: ["effect1", "effect3"]}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # effect1 and effect3 only, so effect2 & external sources are hidden. assert result["data"][CONF_EFFECT_HIDE_LIST] == sorted( @@ -818,7 +818,7 @@ async def test_options_effect_hide_list_cannot_connect(hass: HomeAssistant) -> N client.async_client_connect = AsyncMock(return_value=False) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -845,13 +845,13 @@ async def test_reauth_success(hass: HomeAssistant) -> None: data=config_data, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await _configure_flow( hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert CONF_TOKEN in config_entry.data @@ -877,5 +877,5 @@ async def test_reauth_cannot_connect(hass: HomeAssistant) -> None: data=config_data, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/ialarm/test_config_flow.py b/tests/components/ialarm/test_config_flow.py index 102b2edb0a6..f7c0a4ed338 100644 --- a/tests/components/ialarm/test_config_flow.py +++ b/tests/components/ialarm/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch( @@ -36,7 +36,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_DATA["host"] assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -56,7 +56,7 @@ async def test_form_cannot_connect(hass): result["flow_id"], TEST_DATA ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -74,7 +74,7 @@ async def test_form_exception(hass): result["flow_id"], TEST_DATA ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -100,5 +100,5 @@ async def test_form_already_exists(hass): result["flow_id"], TEST_DATA ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index cd72aae0eff..3c854d468b8 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -162,7 +162,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # test with required @@ -171,7 +171,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock): context={"source": SOURCE_USER}, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE @@ -187,7 +187,7 @@ async def test_user_with_cookie(hass: HomeAssistant, service_authenticated: Magi CONF_WITH_FAMILY: WITH_FAMILY, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME @@ -208,7 +208,7 @@ async def test_login_failed(hass: HomeAssistant): context={"source": SOURCE_USER}, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_PASSWORD: "invalid_auth"} @@ -221,7 +221,7 @@ async def test_no_device( context={"source": SOURCE_USER}, data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_device" @@ -234,7 +234,7 @@ async def test_trusted_device(hass: HomeAssistant, service: MagicMock): ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE @@ -249,7 +249,7 @@ async def test_trusted_device_success(hass: HomeAssistant, service: MagicMock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_TRUSTED_DEVICE: 0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_VERIFICATION_CODE @@ -266,7 +266,7 @@ async def test_send_verification_code_failed( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_TRUSTED_DEVICE: 0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE assert result["errors"] == {CONF_TRUSTED_DEVICE: "send_verification_code"} @@ -283,7 +283,7 @@ async def test_verification_code(hass: HomeAssistant, service: MagicMock): ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_VERIFICATION_CODE @@ -302,7 +302,7 @@ async def test_verification_code_success(hass: HomeAssistant, service: MagicMock result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME @@ -328,7 +328,7 @@ async def test_validate_verification_code_failed( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_TRUSTED_DEVICE assert result["errors"] == {"base": "validate_verification_code"} @@ -347,7 +347,7 @@ async def test_2fa_code_success(hass: HomeAssistant, service_2fa: MagicMock): result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == USERNAME assert result["title"] == USERNAME assert result["data"][CONF_USERNAME] == USERNAME @@ -371,7 +371,7 @@ async def test_validate_2fa_code_failed( result["flow_id"], {CONF_VERIFICATION_CODE: "0"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == CONF_VERIFICATION_CODE assert result["errors"] == {"base": "validate_verification_code"} @@ -389,13 +389,13 @@ async def test_password_update(hass: HomeAssistant, service_authenticated: Magic data={**MOCK_CONFIG, "unique_id": USERNAME}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD_2} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert config_entry.data[CONF_PASSWORD] == PASSWORD_2 @@ -413,7 +413,7 @@ async def test_password_update_wrong_password(hass: HomeAssistant): data={**MOCK_CONFIG, "unique_id": USERNAME}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch( "homeassistant.components.icloud.config_flow.PyiCloudService.authenticate", @@ -423,5 +423,5 @@ async def test_password_update_wrong_password(hass: HomeAssistant): result["flow_id"], {CONF_PASSWORD: PASSWORD_2} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_PASSWORD: "invalid_auth"} diff --git a/tests/components/ifttt/test_init.py b/tests/components/ifttt/test_init.py index 1ca8395e11f..59bf51de9bd 100644 --- a/tests/components/ifttt/test_init.py +++ b/tests/components/ifttt/test_init.py @@ -15,10 +15,10 @@ async def test_config_flow_registers_webhook(hass, hass_client_no_auth): result = await hass.config_entries.flow.async_init( "ifttt", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY webhook_id = result["result"].data["webhook_id"] ifttt_events = [] diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 878b540b721..35a32ef969c 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -71,7 +71,7 @@ async def _init_form(hass, modem_type): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -118,7 +118,7 @@ async def test_fail_on_existing(hass: HomeAssistant): data={**MOCK_USER_INPUT_HUB_V2, CONF_HUB_VERSION: 2}, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -223,7 +223,7 @@ async def _options_init_form(hass, entry_id, step): with patch(PATCH_ASYNC_SETUP_ENTRY, return_value=True): result = await hass.config_entries.options.async_init(entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -279,7 +279,7 @@ async def test_import_existing(hass: HomeAssistant): result = await _import_config( hass, {**MOCK_IMPORT_MINIMUM_HUB_V2, CONF_PORT: 25105, CONF_HUB_VERSION: 2} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -295,7 +295,7 @@ async def test_import_failed_connection(hass: HomeAssistant): data={**MOCK_IMPORT_MINIMUM_HUB_V2, CONF_PORT: 25105, CONF_HUB_VERSION: 2}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -329,7 +329,7 @@ async def test_options_change_hub_config(hass: HomeAssistant): } result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {} assert config_entry.data == {**user_input, CONF_HUB_VERSION: 2} @@ -353,7 +353,7 @@ async def test_options_add_device_override(hass: HomeAssistant): } result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_OVERRIDE]) == 1 assert config_entry.options[CONF_OVERRIDE][0][CONF_ADDRESS] == "1A.2B.3C" assert config_entry.options[CONF_OVERRIDE][0][CONF_CAT] == 4 @@ -397,7 +397,7 @@ async def test_options_remove_device_override(hass: HomeAssistant): user_input = {CONF_ADDRESS: "1A.2B.3C"} result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_OVERRIDE]) == 1 @@ -429,7 +429,7 @@ async def test_options_remove_device_override_with_x10(hass: HomeAssistant): user_input = {CONF_ADDRESS: "1A.2B.3C"} result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_OVERRIDE]) == 1 assert len(config_entry.options[CONF_X10]) == 1 @@ -454,7 +454,7 @@ async def test_options_add_x10_device(hass: HomeAssistant): } result2, _ = await _options_form(hass, result["flow_id"], user_input) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_X10]) == 1 assert config_entry.options[CONF_X10][0][CONF_HOUSECODE] == "c" assert config_entry.options[CONF_X10][0][CONF_UNITCODE] == 12 @@ -470,7 +470,7 @@ async def test_options_add_x10_device(hass: HomeAssistant): } result3, _ = await _options_form(hass, result["flow_id"], user_input) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_X10]) == 2 assert config_entry.options[CONF_X10][1][CONF_HOUSECODE] == "d" assert config_entry.options[CONF_X10][1][CONF_UNITCODE] == 10 @@ -511,7 +511,7 @@ async def test_options_remove_x10_device(hass: HomeAssistant): user_input = {CONF_DEVICE: "Housecode: C, Unitcode: 4"} result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_X10]) == 1 @@ -546,7 +546,7 @@ async def test_options_remove_x10_device_with_override(hass: HomeAssistant): user_input = {CONF_DEVICE: "Housecode: C, Unitcode: 4"} result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(config_entry.options[CONF_X10]) == 1 assert len(config_entry.options[CONF_OVERRIDE]) == 1 @@ -562,14 +562,14 @@ async def test_options_dup_selection(hass: HomeAssistant): config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( result["flow_id"], {STEP_ADD_OVERRIDE: True, STEP_ADD_X10: True}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "select_single"} @@ -593,7 +593,7 @@ async def test_options_override_bad_data(hass: HomeAssistant): } result, _ = await _options_form(hass, result["flow_id"], user_input) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "input_error"} @@ -611,7 +611,7 @@ async def test_discovery_via_usb(hass): "insteon", context={"source": config_entries.SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm_usb" with patch("homeassistant.components.insteon.config_flow.async_connect"), patch( @@ -622,7 +622,7 @@ async def test_discovery_via_usb(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"] == {"device": "/dev/ttyINSTEON"} @@ -646,5 +646,5 @@ async def test_discovery_via_usb_already_setup(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/iqvia/test_config_flow.py b/tests/components/iqvia/test_config_flow.py index 315098a44d8..0d881c30a35 100644 --- a/tests/components/iqvia/test_config_flow.py +++ b/tests/components/iqvia/test_config_flow.py @@ -9,7 +9,7 @@ async def test_duplicate_error(hass, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -18,7 +18,7 @@ async def test_invalid_zip_code(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_ZIP_CODE: "bad"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_ZIP_CODE: "invalid_zip_code"} @@ -27,7 +27,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -36,6 +36,6 @@ async def test_step_user(hass, config, setup_iqvia): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "12345" assert result["data"] == {CONF_ZIP_CODE: "12345"} diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index 730c5634770..d447da1c61e 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -26,13 +26,13 @@ async def test_flow_works(hass): result = await hass.config_entries.flow.async_init( islamic_prayer_times.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM 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_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Islamic Prayer Times" @@ -48,14 +48,14 @@ async def test_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_CALC_METHOD: "makkah"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_CALC_METHOD] == "makkah" @@ -71,5 +71,5 @@ async def test_integration_already_configured(hass): islamic_prayer_times.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/iss/test_config_flow.py b/tests/components/iss/test_config_flow.py index 3260b76432f..eabca610ddf 100644 --- a/tests/components/iss/test_config_flow.py +++ b/tests/components/iss/test_config_flow.py @@ -18,7 +18,7 @@ async def test_create_entry(hass: HomeAssistant): DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == SOURCE_USER with patch("homeassistant.components.iss.async_setup_entry", return_value=True): @@ -28,7 +28,7 @@ async def test_create_entry(hass: HomeAssistant): {}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result.get("result").data == {} @@ -44,7 +44,7 @@ async def test_integration_already_exists(hass: HomeAssistant): DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -60,7 +60,7 @@ async def test_abort_no_home(hass: HomeAssistant): DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "latitude_longitude_not_defined" diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index 8458dc0dc67..b87662718e5 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -116,7 +116,7 @@ async def test_form(hass: HomeAssistant): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( @@ -130,7 +130,7 @@ async def test_form(hass: HomeAssistant): MOCK_USER_INPUT, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_USER_INPUT @@ -154,7 +154,7 @@ async def test_form_invalid_host(hass: HomeAssistant): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_host"} @@ -172,7 +172,7 @@ async def test_form_invalid_auth(hass: HomeAssistant): MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} @@ -190,7 +190,7 @@ async def test_form_unknown_exeption(hass: HomeAssistant): MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -208,7 +208,7 @@ async def test_form_isy_connection_error(hass: HomeAssistant): MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -226,7 +226,7 @@ async def test_form_isy_parse_response_error(hass: HomeAssistant, caplog): MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert "ISY Could not parse response, poorly formatted XML." in caplog.text @@ -246,7 +246,7 @@ async def test_form_no_name_in_response(hass: HomeAssistant): MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -257,7 +257,7 @@ async def test_form_existing_config_entry(hass: HomeAssistant): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE): @@ -265,7 +265,7 @@ async def test_form_existing_config_entry(hass: HomeAssistant): result["flow_id"], MOCK_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT async def test_import_flow_some_fields(hass: HomeAssistant) -> None: @@ -282,7 +282,7 @@ async def test_import_flow_some_fields(hass: HomeAssistant) -> None: data=MOCK_IMPORT_BASIC_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == f"http://{MOCK_HOSTNAME}" assert result["data"][CONF_USERNAME] == MOCK_USERNAME assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD @@ -303,7 +303,7 @@ async def test_import_flow_with_https(hass: HomeAssistant) -> None: data=MOCK_IMPORT_WITH_SSL, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == f"https://{MOCK_HOSTNAME}" assert result["data"][CONF_USERNAME] == MOCK_USERNAME assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD @@ -323,7 +323,7 @@ async def test_import_flow_all_fields(hass: HomeAssistant) -> None: data=MOCK_IMPORT_FULL_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == f"http://{MOCK_HOSTNAME}" assert result["data"][CONF_USERNAME] == MOCK_USERNAME assert result["data"][CONF_PASSWORD] == MOCK_PASSWORD @@ -356,7 +356,7 @@ async def test_form_ssdp_already_configured(hass: HomeAssistant) -> None: }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_form_ssdp(hass: HomeAssistant): @@ -375,7 +375,7 @@ async def test_form_ssdp(hass: HomeAssistant): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -391,7 +391,7 @@ async def test_form_ssdp(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_USER_INPUT @@ -425,7 +425,7 @@ async def test_form_ssdp_existing_entry(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://3.3.3.3:80{ISY_URL_POSTFIX}" @@ -456,7 +456,7 @@ async def test_form_ssdp_existing_entry_with_no_port(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://3.3.3.3:80/{ISY_URL_POSTFIX}" @@ -487,7 +487,7 @@ async def test_form_ssdp_existing_entry_with_alternate_port(hass: HomeAssistant) ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://3.3.3.3:1443/{ISY_URL_POSTFIX}" @@ -518,7 +518,7 @@ async def test_form_ssdp_existing_entry_no_port_https(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"https://3.3.3.3:443/{ISY_URL_POSTFIX}" @@ -535,7 +535,7 @@ async def test_form_dhcp(hass: HomeAssistant): macaddress=MOCK_MAC, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -551,7 +551,7 @@ async def test_form_dhcp(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_USER_INPUT @@ -571,7 +571,7 @@ async def test_form_dhcp_with_polisy(hass: HomeAssistant): macaddress=MOCK_POLISY_MAC, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} assert ( @@ -591,7 +591,7 @@ async def test_form_dhcp_with_polisy(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID assert result2["data"] == MOCK_POLISY_USER_INPUT @@ -621,7 +621,7 @@ async def test_form_dhcp_existing_entry(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://1.2.3.4{ISY_URL_POSTFIX}" @@ -651,7 +651,7 @@ async def test_form_dhcp_existing_entry_preserves_port(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == f"http://1.2.3.4:1443{ISY_URL_POSTFIX}" assert entry.data[CONF_USERNAME] == "bob" @@ -677,7 +677,7 @@ async def test_form_dhcp_existing_ignored_entry(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/izone/test_config_flow.py b/tests/components/izone/test_config_flow.py index a548e59930b..d48e19181c8 100644 --- a/tests/components/izone/test_config_flow.py +++ b/tests/components/izone/test_config_flow.py @@ -42,10 +42,10 @@ async def test_not_found(hass, mock_disco): ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT await hass.async_block_till_done() @@ -71,10 +71,10 @@ async def test_found(hass, mock_disco): ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/jellyfin/test_config_flow.py b/tests/components/jellyfin/test_config_flow.py index cc23265e011..e898f8ac5ce 100644 --- a/tests/components/jellyfin/test_config_flow.py +++ b/tests/components/jellyfin/test_config_flow.py @@ -27,7 +27,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/keenetic_ndms2/test_config_flow.py b/tests/components/keenetic_ndms2/test_config_flow.py index 18d5f3df1fb..e1cb083dc73 100644 --- a/tests/components/keenetic_ndms2/test_config_flow.py +++ b/tests/components/keenetic_ndms2/test_config_flow.py @@ -51,7 +51,7 @@ async def test_flow_works(hass: HomeAssistant, connect) -> None: result = await hass.config_entries.flow.async_init( keenetic.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -63,7 +63,7 @@ async def test_flow_works(hass: HomeAssistant, connect) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == MOCK_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -98,7 +98,7 @@ async def test_options(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.options.async_configure( @@ -106,7 +106,7 @@ async def test_options(hass: HomeAssistant) -> None: user_input=MOCK_OPTIONS, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"] == MOCK_OPTIONS @@ -126,7 +126,7 @@ async def test_host_already_configured(hass: HomeAssistant, connect) -> None: result["flow_id"], user_input=MOCK_DATA ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -139,7 +139,7 @@ async def test_connection_error(hass: HomeAssistant, connect_error) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -153,7 +153,7 @@ async def test_ssdp_works(hass: HomeAssistant, connect) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -168,7 +168,7 @@ async def test_ssdp_works(hass: HomeAssistant, connect) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == MOCK_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -189,7 +189,7 @@ async def test_ssdp_already_configured(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -210,7 +210,7 @@ async def test_ssdp_ignored(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -236,7 +236,7 @@ async def test_ssdp_update_host(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == new_ip @@ -254,7 +254,7 @@ async def test_ssdp_reject_no_udn(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_udn" @@ -270,5 +270,5 @@ async def test_ssdp_reject_non_keenetic(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_keenetic_ndms2" diff --git a/tests/components/kmtronic/test_config_flow.py b/tests/components/kmtronic/test_config_flow.py index fabd8738259..c2965dfa083 100644 --- a/tests/components/kmtronic/test_config_flow.py +++ b/tests/components/kmtronic/test_config_flow.py @@ -69,14 +69,14 @@ async def test_form_options(hass, aioclient_mock): assert config_entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_REVERSE: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_REVERSE: True} await hass.async_block_till_done() diff --git a/tests/components/launch_library/test_config_flow.py b/tests/components/launch_library/test_config_flow.py index 5b6cb85cfee..a9dd794d05e 100644 --- a/tests/components/launch_library/test_config_flow.py +++ b/tests/components/launch_library/test_config_flow.py @@ -15,7 +15,7 @@ async def test_create_entry(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == SOURCE_USER with patch( @@ -27,7 +27,7 @@ async def test_create_entry(hass): {}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result.get("result").data == {} @@ -43,5 +43,5 @@ async def test_integration_already_exists(hass): DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" diff --git a/tests/components/lcn/test_config_flow.py b/tests/components/lcn/test_config_flow.py index 36b5d23739a..6f084f939d8 100644 --- a/tests/components/lcn/test_config_flow.py +++ b/tests/components/lcn/test_config_flow.py @@ -43,7 +43,7 @@ async def test_step_import(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "pchk" assert result["data"] == IMPORT_DATA @@ -65,7 +65,7 @@ async def test_step_import_existing_host(hass): await hass.async_block_till_done() # Check if config entry was updated - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "existing_configuration_updated" assert mock_entry.source == config_entries.SOURCE_IMPORT assert mock_entry.data == IMPORT_DATA @@ -91,5 +91,5 @@ async def test_step_import_error(hass, error, reason): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == reason diff --git a/tests/components/life360/test_config_flow.py b/tests/components/life360/test_config_flow.py index 0b5b850ac23..02d5539e117 100644 --- a/tests/components/life360/test_config_flow.py +++ b/tests/components/life360/test_config_flow.py @@ -106,7 +106,7 @@ async def test_user_show_form(hass, life360_api): life360_api.get_authorization.assert_not_called() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -134,7 +134,7 @@ async def test_user_config_flow_success(hass, life360_api): life360_api.get_authorization.assert_called_once() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USER.lower() assert result["data"] == TEST_CONFIG_DATA assert result["options"] == DEFAULT_OPTIONS @@ -159,7 +159,7 @@ async def test_user_config_flow_error(hass, life360_api, caplog, exception, erro life360_api.get_authorization.assert_called_once() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] assert result["errors"]["base"] == error @@ -190,7 +190,7 @@ async def test_user_config_flow_already_configured(hass, life360_api): life360_api.get_authorization.assert_not_called() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -220,7 +220,7 @@ async def test_reauth_config_flow_success(hass, life360_api, caplog, state): life360_api.get_authorization.assert_called_once() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert "Reauthorization successful" in caplog.text @@ -250,7 +250,7 @@ async def test_reauth_config_flow_login_error(hass, life360_api, caplog): life360_api.get_authorization.assert_called_once() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] assert result["errors"]["base"] == "invalid_auth" @@ -274,7 +274,7 @@ async def test_reauth_config_flow_login_error(hass, life360_api, caplog): life360_api.get_authorization.assert_called_once() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert "Reauthorization successful" in caplog.text @@ -292,7 +292,7 @@ async def test_options_flow(hass): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert not result["errors"] @@ -303,7 +303,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_configure(flow_id, USER_OPTIONS) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == TEST_OPTIONS assert config_entry.options == TEST_OPTIONS diff --git a/tests/components/litejet/test_config_flow.py b/tests/components/litejet/test_config_flow.py index cfae178f792..18d5dff80db 100644 --- a/tests/components/litejet/test_config_flow.py +++ b/tests/components/litejet/test_config_flow.py @@ -85,7 +85,7 @@ async def test_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -93,5 +93,5 @@ async def test_options(hass): user_input={CONF_DEFAULT_TRANSITION: 12}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEFAULT_TRANSITION: 12} diff --git a/tests/components/local_ip/test_config_flow.py b/tests/components/local_ip/test_config_flow.py index 4804ab83aca..1b84e3e8552 100644 --- a/tests/components/local_ip/test_config_flow.py +++ b/tests/components/local_ip/test_config_flow.py @@ -11,10 +11,10 @@ async def test_config_flow(hass, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get(f"sensor.{DOMAIN}") @@ -32,5 +32,5 @@ async def test_already_setup(hass, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index f65e9d8a6af..94b94c2ec93 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -41,10 +41,10 @@ async def webhook_id(hass, locative_client): result = await hass.config_entries.flow.async_init( "locative", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index 536d370f16a..2f1d69b8d47 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -66,7 +66,7 @@ async def test_step_import( flow = init_config_flow(hass) result = await flow.async_step_import() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -86,18 +86,18 @@ async def test_full_flow_implementation( flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({"flow_impl": "test-other"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["description_placeholders"] == { "authorization_url": "http://example.com" } result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Logi Circle ({})".format("testId") @@ -115,7 +115,7 @@ async def test_abort_if_no_implementation_registered(hass): flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_configuration" @@ -128,21 +128,21 @@ async def test_abort_if_already_setup(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" with pytest.raises(data_entry_flow.AbortFlow): result = await flow.async_step_code() result = await flow.async_step_auth() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "external_setup" @@ -161,7 +161,7 @@ async def test_abort_if_authorize_fails( mock_logi_circle.authorize.side_effect = side_effect result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "external_error" result = await flow.async_step_auth() @@ -173,7 +173,7 @@ async def test_not_pick_implementation_if_only_one(hass): flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index 821bf07cf08..b5e8271d351 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -101,7 +101,7 @@ async def test_bridge_cannot_connect(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT @@ -124,7 +124,7 @@ async def test_bridge_cannot_connect_unknown_error(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT @@ -144,7 +144,7 @@ async def test_bridge_invalid_ssl_error(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT @@ -171,7 +171,7 @@ async def test_duplicate_bridge_import(hass): data=entry_mock_data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index d9262e215fb..6eb97f03f89 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -40,7 +40,7 @@ async def test_abort_if_no_configuration(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_credentials" @@ -71,7 +71,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -132,7 +132,7 @@ async def test_abort_if_authorization_timeout( ): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -196,7 +196,7 @@ async def test_reauthentication_flow( ) as mock_setup: result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/mailgun/test_init.py b/tests/components/mailgun/test_init.py index c6eeb9f99ad..2dc18b63d5b 100644 --- a/tests/components/mailgun/test_init.py +++ b/tests/components/mailgun/test_init.py @@ -37,10 +37,10 @@ async def webhook_id_with_api_key(hass): result = await hass.config_entries.flow.async_init( "mailgun", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY return result["result"].data["webhook_id"] @@ -57,10 +57,10 @@ async def webhook_id_without_api_key(hass): result = await hass.config_entries.flow.async_init( "mailgun", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY return result["result"].data["webhook_id"] diff --git a/tests/components/mazda/test_config_flow.py b/tests/components/mazda/test_config_flow.py index c2628f2aa11..1090d928952 100644 --- a/tests/components/mazda/test_config_flow.py +++ b/tests/components/mazda/test_config_flow.py @@ -36,7 +36,7 @@ async def test_form(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -71,7 +71,7 @@ async def test_account_already_exists(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -95,7 +95,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -109,7 +109,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -120,7 +120,7 @@ async def test_form_account_locked(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -134,7 +134,7 @@ async def test_form_account_locked(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "account_locked"} @@ -206,7 +206,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -220,7 +220,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -249,7 +249,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -258,7 +258,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -288,7 +288,7 @@ async def test_reauth_account_locked(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -297,7 +297,7 @@ async def test_reauth_account_locked(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "account_locked"} @@ -327,7 +327,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -336,7 +336,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -366,7 +366,7 @@ async def test_reauth_unknown_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -375,7 +375,7 @@ async def test_reauth_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -405,7 +405,7 @@ async def test_reauth_user_has_new_email_address(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Change the email and ensure the entry and its unique id gets @@ -419,5 +419,5 @@ async def test_reauth_user_has_new_email_address(hass: HomeAssistant) -> None: assert ( mock_config.unique_id == FIXTURE_USER_INPUT_REAUTH_CHANGED_EMAIL[CONF_EMAIL] ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/meater/test_config_flow.py b/tests/components/meater/test_config_flow.py index 11312111311..0d05466e464 100644 --- a/tests/components/meater/test_config_flow.py +++ b/tests/components/meater/test_config_flow.py @@ -37,7 +37,7 @@ async def test_duplicate_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -83,7 +83,7 @@ async def test_user_flow(hass, mock_meater): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -93,7 +93,7 @@ async def test_user_flow(hass, mock_meater): result = await hass.config_entries.flow.async_configure(result["flow_id"], conf) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123", @@ -126,7 +126,7 @@ async def test_reauth_flow(hass, mock_meater): data=data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] is None @@ -136,7 +136,7 @@ async def test_reauth_flow(hass, mock_meater): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" config_entry = hass.config_entries.async_entries(DOMAIN)[0] diff --git a/tests/components/met_eireann/test_config_flow.py b/tests/components/met_eireann/test_config_flow.py index 50060541be5..334e4a52ac3 100644 --- a/tests/components/met_eireann/test_config_flow.py +++ b/tests/components/met_eireann/test_config_flow.py @@ -64,7 +64,7 @@ async def test_create_entry(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == test_data.get("name") assert result["data"] == test_data @@ -85,11 +85,11 @@ async def test_flow_entry_already_exists(hass): result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result1["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result1["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Create the second entry and assert that it is aborted result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=test_data ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/meteo_france/test_config_flow.py b/tests/components/meteo_france/test_config_flow.py index 29c08e41d1d..295e8a1e5e8 100644 --- a/tests/components/meteo_france/test_config_flow.py +++ b/tests/components/meteo_france/test_config_flow.py @@ -121,7 +121,7 @@ async def test_user(hass, client_single): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # test with all provided with search returning only 1 place @@ -130,7 +130,7 @@ async def test_user(hass, client_single): context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == f"{CITY_1_LAT}, {CITY_1_LON}" assert result["title"] == f"{CITY_1}" assert result["data"][CONF_LATITUDE] == str(CITY_1_LAT) @@ -146,14 +146,14 @@ async def test_user_list(hass, client_multiple): context={"source": SOURCE_USER}, data={CONF_CITY: CITY_2_NAME}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "cities" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_CITY: f"{CITY_3};{CITY_3_LAT};{CITY_3_LON}"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == f"{CITY_3_LAT}, {CITY_3_LON}" assert result["title"] == f"{CITY_3}" assert result["data"][CONF_LATITUDE] == str(CITY_3_LAT) @@ -168,7 +168,7 @@ async def test_import(hass, client_multiple): context={"source": SOURCE_IMPORT}, data={CONF_CITY: CITY_2_NAME}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == f"{CITY_2_LAT}, {CITY_2_LON}" assert result["title"] == f"{CITY_2}" assert result["data"][CONF_LATITUDE] == str(CITY_2_LAT) @@ -183,7 +183,7 @@ async def test_search_failed(hass, client_empty): data={CONF_CITY: CITY_1_POSTAL}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_CITY: "empty"} @@ -201,7 +201,7 @@ async def test_abort_if_already_setup(hass, client_single): context={"source": SOURCE_IMPORT}, data={CONF_CITY: CITY_1_POSTAL}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Should fail, same CITY same postal code (flow) @@ -210,7 +210,7 @@ async def test_abort_if_already_setup(hass, client_single): context={"source": SOURCE_USER}, data={CONF_CITY: CITY_1_POSTAL}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -226,7 +226,7 @@ async def test_options_flow(hass: HomeAssistant): assert config_entry.options == {} result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # Default @@ -234,7 +234,7 @@ async def test_options_flow(hass: HomeAssistant): result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_MODE] == FORECAST_MODE_DAILY # Manual @@ -243,5 +243,5 @@ async def test_options_flow(hass: HomeAssistant): result["flow_id"], user_input={CONF_MODE: FORECAST_MODE_HOURLY}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_MODE] == FORECAST_MODE_HOURLY diff --git a/tests/components/meteoclimatic/test_config_flow.py b/tests/components/meteoclimatic/test_config_flow.py index e5daaea1978..4b94862553e 100644 --- a/tests/components/meteoclimatic/test_config_flow.py +++ b/tests/components/meteoclimatic/test_config_flow.py @@ -42,7 +42,7 @@ async def test_user(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # test with all provided @@ -51,7 +51,7 @@ async def test_user(hass, client): context={"source": SOURCE_USER}, data={CONF_STATION_CODE: TEST_STATION_CODE}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == TEST_STATION_CODE assert result["title"] == TEST_STATION_NAME assert result["data"][CONF_STATION_CODE] == TEST_STATION_CODE @@ -68,7 +68,7 @@ async def test_not_found(hass): context={"source": SOURCE_USER}, data={CONF_STATION_CODE: TEST_STATION_CODE}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "not_found" @@ -84,5 +84,5 @@ async def test_unknown_error(hass): context={"source": SOURCE_USER}, data={CONF_STATION_CODE: TEST_STATION_CODE}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/mikrotik/test_config_flow.py b/tests/components/mikrotik/test_config_flow.py index b4c087a436d..704bf92066e 100644 --- a/tests/components/mikrotik/test_config_flow.py +++ b/tests/components/mikrotik/test_config_flow.py @@ -89,14 +89,14 @@ async def test_flow_works(hass, api): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Home router" assert result["data"][CONF_NAME] == "Home router" assert result["data"][CONF_HOST] == "0.0.0.0" @@ -115,7 +115,7 @@ async def test_options(hass, api): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "device_tracker" result = await hass.config_entries.options.async_configure( @@ -127,7 +127,7 @@ async def test_options(hass, api): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_DETECTION_TIME: 30, CONF_ARP_PING: True, @@ -179,7 +179,7 @@ async def test_connection_error(hass, conn_error): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=DEMO_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -193,7 +193,7 @@ async def test_wrong_credentials(hass, auth_error): result["flow_id"], user_input=DEMO_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == { CONF_USERNAME: "invalid_auth", CONF_PASSWORD: "invalid_auth", diff --git a/tests/components/modem_callerid/test_config_flow.py b/tests/components/modem_callerid/test_config_flow.py index 19a98106c63..8dc7eccf25a 100644 --- a/tests/components/modem_callerid/test_config_flow.py +++ b/tests/components/modem_callerid/test_config_flow.py @@ -37,14 +37,14 @@ async def test_flow_usb(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_DEVICE: phone_modem.DEFAULT_PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEVICE: com_port().device} @@ -56,7 +56,7 @@ async def test_flow_usb_cannot_connect(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -78,7 +78,7 @@ async def test_flow_user(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEVICE: port.device} result = await hass.config_entries.flow.async_init( @@ -86,7 +86,7 @@ async def test_flow_user(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -107,7 +107,7 @@ async def test_flow_user_error(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: port_select} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -116,7 +116,7 @@ async def test_flow_user_error(hass: HomeAssistant): result["flow_id"], user_input={CONF_DEVICE: port_select}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_DEVICE: port.device} @@ -129,7 +129,7 @@ async def test_flow_user_no_port_list(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USER}, data={CONF_DEVICE: phone_modem.DEFAULT_PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -141,7 +141,7 @@ async def test_abort_user_with_existing_flow(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "usb_confirm" result2 = await hass.config_entries.flow.async_init( @@ -150,5 +150,5 @@ async def test_abort_user_with_existing_flow(hass: HomeAssistant): data={}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_in_progress" diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index 0ed4ac35eaa..b88d4d240bd 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -107,7 +107,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -115,5 +115,5 @@ async def test_options_flow(hass): user_input={CONF_SOURCE_1: "one", CONF_SOURCE_4: "", CONF_SOURCE_5: "five"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SOURCES] == {"1": "one", "5": "five"} diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index 57ab45d9dbb..23defadde9a 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -372,7 +372,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -380,7 +380,7 @@ async def test_options_flow(hass): user_input={const.CONF_WAIT_FOR_PUSH: False}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { const.CONF_WAIT_FOR_PUSH: False, } diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index 269a2b8a4c4..6fe38ccf7a1 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -79,13 +79,13 @@ async def test_hassio_success(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("description_placeholders") == {"addon": "motionEye"} assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result2.get("type") == data_entry_flow.FlowResultType.FORM assert result2.get("step_id") == "user" assert "flow_id" in result2 @@ -109,7 +109,7 @@ async def test_hassio_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3.get("title") == "Add-on" assert result3.get("data") == { CONF_URL: TEST_URL, @@ -287,7 +287,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert dict(config_entry.data) == {**new_data, CONF_WEBHOOK_ID: "test-webhook-id"} @@ -337,7 +337,7 @@ async def test_duplicate(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_client.async_client_close.called @@ -354,7 +354,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -369,7 +369,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -378,14 +378,14 @@ async def test_hassio_abort_if_already_in_progress(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_init( DOMAIN, data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result2.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result2.get("type") == data_entry_flow.FlowResultType.ABORT assert result2.get("reason") == "already_in_progress" @@ -397,12 +397,12 @@ async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: data=HassioServiceInfo(config={"addon": "motionEye", "url": TEST_URL}), context={"source": config_entries.SOURCE_HASSIO}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result2.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result2.get("type") == data_entry_flow.FlowResultType.FORM assert "flow_id" in result2 mock_client = create_mock_motioneye_client() @@ -426,7 +426,7 @@ async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(mock_setup_entry.mock_calls) == 1 flows = hass.config_entries.flow.async_progress() @@ -449,7 +449,7 @@ async def test_options(hass: HomeAssistant) -> None: 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -460,7 +460,7 @@ async def test_options(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_SET] assert result["data"][CONF_WEBHOOK_SET_OVERWRITE] assert CONF_STREAM_URL_TEMPLATE not in result["data"] @@ -492,7 +492,7 @@ async def test_advanced_options(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_SET] assert result["data"][CONF_WEBHOOK_SET_OVERWRITE] assert CONF_STREAM_URL_TEMPLATE not in result["data"] @@ -511,7 +511,7 @@ async def test_advanced_options(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_SET] assert result["data"][CONF_WEBHOOK_SET_OVERWRITE] assert result["data"][CONF_STREAM_URL_TEMPLATE] == "http://moo" diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 6f1b662e282..208a6ce2d61 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -218,7 +218,7 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_HASSIO}, ) assert result - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -277,7 +277,7 @@ async def test_option_flow(hass, mqtt_mock_entry_no_yaml_config, mock_try_connec mqtt_mock.async_connect.reset_mock() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "broker" result = await hass.config_entries.options.async_configure( @@ -289,7 +289,7 @@ async def test_option_flow(hass, mqtt_mock_entry_no_yaml_config, mock_try_connec mqtt.CONF_PASSWORD: "pass", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" await hass.async_block_till_done() @@ -311,7 +311,7 @@ async def test_option_flow(hass, mqtt_mock_entry_no_yaml_config, mock_try_connec "will_retain": True, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {} assert config_entry.data == { mqtt.CONF_BROKER: "another-broker", @@ -352,7 +352,7 @@ async def test_disable_birth_will( mqtt_mock.async_connect.reset_mock() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "broker" result = await hass.config_entries.options.async_configure( @@ -364,7 +364,7 @@ async def test_disable_birth_will( mqtt.CONF_PASSWORD: "pass", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" await hass.async_block_till_done() @@ -386,7 +386,7 @@ async def test_disable_birth_will( "will_retain": True, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {} assert config_entry.data == { mqtt.CONF_BROKER: "another-broker", @@ -448,7 +448,7 @@ async def test_option_flow_default_suggested_values( # Test default/suggested values from config result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "broker" defaults = { mqtt.CONF_BROKER: "test-broker", @@ -472,7 +472,7 @@ async def test_option_flow_default_suggested_values( mqtt.CONF_PASSWORD: "p4ss", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" defaults = { mqtt.CONF_DISCOVERY: True, @@ -506,11 +506,11 @@ async def test_option_flow_default_suggested_values( "will_retain": True, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Test updated default/suggested values from config result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "broker" defaults = { mqtt.CONF_BROKER: "another-broker", @@ -529,7 +529,7 @@ async def test_option_flow_default_suggested_values( result["flow_id"], user_input={mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" defaults = { mqtt.CONF_DISCOVERY: False, @@ -563,7 +563,7 @@ async def test_option_flow_default_suggested_values( "will_retain": True, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Make sure all MQTT related jobs are done before ending the test await hass.async_block_till_done() @@ -717,7 +717,7 @@ async def test_try_connection_with_advanced_parameters( # Test default/suggested values from config result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "broker" defaults = { mqtt.CONF_BROKER: "test-broker", @@ -741,7 +741,7 @@ async def test_try_connection_with_advanced_parameters( mqtt.CONF_PASSWORD: "p4ss", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" # check if the username and password was set from config flow and not from configuration.yaml diff --git a/tests/components/nam/test_config_flow.py b/tests/components/nam/test_config_flow.py index 67274cf1c78..aef0fa41fe5 100644 --- a/tests/components/nam/test_config_flow.py +++ b/tests/components/nam/test_config_flow.py @@ -32,7 +32,7 @@ async def test_form_create_entry_without_auth(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -51,7 +51,7 @@ async def test_form_create_entry_without_auth(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"]["host"] == "10.10.2.3" assert len(mock_setup_entry.mock_calls) == 1 @@ -62,7 +62,7 @@ async def test_form_create_entry_with_auth(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -80,7 +80,7 @@ async def test_form_create_entry_with_auth(hass): VALID_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "credentials" result = await hass.config_entries.flow.async_configure( @@ -89,7 +89,7 @@ async def test_form_create_entry_with_auth(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"]["host"] == "10.10.2.3" assert result["data"]["username"] == "fake_username" @@ -120,7 +120,7 @@ async def test_reauth_successful(hass): data=entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -128,7 +128,7 @@ async def test_reauth_successful(hass): user_input=VALID_AUTH, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -152,7 +152,7 @@ async def test_reauth_unsuccessful(hass): data=entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -160,7 +160,7 @@ async def test_reauth_unsuccessful(hass): user_input=VALID_AUTH, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_unsuccessful" @@ -189,7 +189,7 @@ async def test_form_with_auth_errors(hass, error): data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "credentials" with patch( @@ -244,7 +244,7 @@ async def test_form_abort(hass): data=VALID_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "device_unsupported" @@ -271,7 +271,7 @@ async def test_form_already_configured(hass): {"host": "1.1.1.1"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -298,7 +298,7 @@ async def test_zeroconf(hass): if flow["flow_id"] == result["flow_id"] ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert context["title_placeholders"]["host"] == "10.10.2.3" assert context["confirm_only"] is True @@ -313,7 +313,7 @@ async def test_zeroconf(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"] == {"host": "10.10.2.3"} assert len(mock_setup_entry.mock_calls) == 1 @@ -339,7 +339,7 @@ async def test_zeroconf_with_auth(hass): if flow["flow_id"] == result["flow_id"] ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "credentials" assert result["errors"] == {} assert context["title_placeholders"]["host"] == "10.10.2.3" @@ -359,7 +359,7 @@ async def test_zeroconf_with_auth(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.2.3" assert result["data"]["host"] == "10.10.2.3" assert result["data"]["username"] == "fake_username" @@ -380,7 +380,7 @@ async def test_zeroconf_host_already_configured(hass): context={"source": SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -404,5 +404,5 @@ async def test_zeroconf_errors(hass, error): context={"source": SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == reason diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 7e187f1e2fd..14571c75588 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -85,7 +85,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( "neato", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -111,7 +111,7 @@ async def test_reauth( result = await hass.config_entries.flow.async_init( "neato", context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # Confirm reauth flow @@ -148,7 +148,7 @@ async def test_reauth( new_entry = hass.config_entries.async_get_entry("my_entry") - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert new_entry.state == config_entries.ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(NEATO_DOMAIN)) == 1 diff --git a/tests/components/nest/test_config_flow_legacy.py b/tests/components/nest/test_config_flow_legacy.py index f199d2ec7dd..3784304ac8b 100644 --- a/tests/components/nest/test_config_flow_legacy.py +++ b/tests/components/nest/test_config_flow_legacy.py @@ -24,7 +24,7 @@ async def test_abort_if_single_instance_allowed(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -40,14 +40,14 @@ async def test_full_flow_implementation(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"flow_impl": "nest"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert ( result["description_placeholders"] @@ -69,7 +69,7 @@ async def test_full_flow_implementation(hass): ) await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["tokens"] == {"access_token": "yoo"} assert result["data"]["impl_domain"] == "nest" assert result["title"] == "Nest (via configuration.yaml)" @@ -83,7 +83,7 @@ async def test_not_pick_implementation_if_only_one(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" @@ -99,7 +99,7 @@ async def test_abort_if_timeout_generating_auth_url(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -115,7 +115,7 @@ async def test_abort_if_exception_generating_auth_url(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown_authorize_url_generation" @@ -127,7 +127,7 @@ async def test_verify_code_timeout(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" with patch( @@ -137,7 +137,7 @@ async def test_verify_code_timeout(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"code": "123ABC"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"code": "timeout"} @@ -150,7 +150,7 @@ async def test_verify_code_invalid(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" with patch( @@ -160,7 +160,7 @@ async def test_verify_code_invalid(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"code": "123ABC"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"code": "invalid_pin"} @@ -173,7 +173,7 @@ async def test_verify_code_unknown_error(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" with patch( @@ -183,7 +183,7 @@ async def test_verify_code_unknown_error(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"code": "123ABC"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"code": "unknown"} @@ -196,7 +196,7 @@ async def test_verify_code_exception(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" with patch( @@ -206,7 +206,7 @@ async def test_verify_code_exception(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], {"code": "123ABC"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"code": "internal_error"} @@ -220,7 +220,7 @@ async def test_step_import(hass): flow = hass.config_entries.flow.async_progress()[0] result = await hass.config_entries.flow.async_configure(flow["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 30fb5fd3d47..ae13268eda3 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -34,7 +34,7 @@ async def test_abort_if_existing_entry(hass): result = await hass.config_entries.flow.async_init( "netatmo", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" result = await hass.config_entries.flow.async_init( @@ -50,7 +50,7 @@ async def test_abort_if_existing_entry(hass): type="mock_type", ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -142,28 +142,28 @@ async def test_option_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_NEW_AREA: "Home"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "public_weather" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=valid_option ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY for k, v in expected_result.items(): assert config_entry.options[CONF_WEATHER_AREAS]["Home"][k] == v @@ -200,28 +200,28 @@ async def test_option_flow_wrong_coordinates(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_NEW_AREA: "Home"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "public_weather" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=valid_option ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "public_weather_areas" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY for k, v in expected_result.items(): assert config_entry.options[CONF_WEATHER_AREAS]["Home"][k] == v @@ -289,7 +289,7 @@ async def test_reauth( 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # Confirm reauth flow @@ -326,7 +326,7 @@ async def test_reauth( new_entry2 = hass.config_entries.async_entries(DOMAIN)[0] - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "reauth_successful" assert new_entry2.state == config_entries.ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/netgear/test_config_flow.py b/tests/components/netgear/test_config_flow.py index d46284f5049..69dc57b1d2c 100644 --- a/tests/components/netgear/test_config_flow.py +++ b/tests/components/netgear/test_config_flow.py @@ -115,7 +115,7 @@ async def test_user(hass, service): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Have to provide all config @@ -127,7 +127,7 @@ async def test_user(hass, service): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE assert result["data"].get(CONF_HOST) == HOST @@ -142,7 +142,7 @@ async def test_user_connect_error(hass, service_failed): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Have to provide all config @@ -154,7 +154,7 @@ async def test_user_connect_error(hass, service_failed): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "config"} @@ -164,7 +164,7 @@ async def test_user_incomplete_info(hass, service_incomplete): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Have to provide all config @@ -176,7 +176,7 @@ async def test_user_incomplete_info(hass, service_incomplete): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE_INCOMPLETE assert result["data"].get(CONF_HOST) == HOST @@ -198,14 +198,14 @@ async def test_abort_if_already_setup(hass, service): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -231,7 +231,7 @@ async def test_ssdp_already_configured(hass): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -257,7 +257,7 @@ async def test_ssdp_ipv6(hass): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "not_ipv4_address" @@ -277,13 +277,13 @@ async def test_ssdp(hass, service): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE assert result["data"].get(CONF_HOST) == HOST @@ -309,13 +309,13 @@ async def test_ssdp_port_5555(hass, service_5555): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == TITLE assert result["data"].get(CONF_HOST) == HOST @@ -340,7 +340,7 @@ async def test_options_flow(hass, service): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -350,7 +350,7 @@ async def test_options_flow(hass, service): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_CONSIDER_HOME: 1800, } diff --git a/tests/components/nextdns/test_config_flow.py b/tests/components/nextdns/test_config_flow.py index 0b3ac8a798e..ad17de1c150 100644 --- a/tests/components/nextdns/test_config_flow.py +++ b/tests/components/nextdns/test_config_flow.py @@ -22,7 +22,7 @@ async def test_form_create_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -36,7 +36,7 @@ async def test_form_create_entry(hass): {CONF_API_KEY: "fake_api_key"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "profiles" result = await hass.config_entries.flow.async_configure( @@ -44,7 +44,7 @@ async def test_form_create_entry(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Fake Profile" assert result["data"][CONF_API_KEY] == "fake_api_key" assert result["data"][CONF_PROFILE_ID] == "xyz12" @@ -96,5 +96,5 @@ async def test_form_already_configured(hass): result["flow_id"], {CONF_PROFILE_NAME: "Fake Profile"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/nfandroidtv/test_config_flow.py b/tests/components/nfandroidtv/test_config_flow.py index a8b7b5fef53..cfafced5202 100644 --- a/tests/components/nfandroidtv/test_config_flow.py +++ b/tests/components/nfandroidtv/test_config_flow.py @@ -37,7 +37,7 @@ async def test_flow_user(hass): result["flow_id"], user_input=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_DATA @@ -62,7 +62,7 @@ async def test_flow_user_already_configured(hass): result["flow_id"], user_input=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -76,7 +76,7 @@ async def test_flow_user_cannot_connect(hass): context={"source": config_entries.SOURCE_USER}, data=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -91,6 +91,6 @@ async def test_flow_user_unknown_error(hass): context={"source": config_entries.SOURCE_USER}, data=CONF_CONFIG_FLOW, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/nightscout/test_config_flow.py b/tests/components/nightscout/test_config_flow.py index d7a54ba28fb..9a2b070c20c 100644 --- a/tests/components/nightscout/test_config_flow.py +++ b/tests/components/nightscout/test_config_flow.py @@ -25,7 +25,7 @@ async def test_form(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with _patch_glucose_readings(), _patch_server_status(), _patch_async_setup_entry() as mock_setup_entry: @@ -34,7 +34,7 @@ async def test_form(hass): CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == SERVER_STATUS.name # pylint: disable=maybe-no-member assert result2["data"] == CONFIG await hass.async_block_till_done() @@ -56,7 +56,7 @@ async def test_user_form_cannot_connect(hass): {CONF_URL: "https://some.url:1234"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -78,7 +78,7 @@ async def test_user_form_api_key_required(hass): {CONF_URL: "https://some.url:1234"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -97,7 +97,7 @@ async def test_user_form_unexpected_exception(hass): {CONF_URL: "https://some.url:1234"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -112,7 +112,7 @@ async def test_user_form_duplicate(hass): context={"source": config_entries.SOURCE_USER}, data=CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/nina/test_config_flow.py b/tests/components/nina/test_config_flow.py index 738eb80e190..a93cecdd102 100644 --- a/tests/components/nina/test_config_flow.py +++ b/tests/components/nina/test_config_flow.py @@ -58,7 +58,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -73,7 +73,7 @@ async def test_step_user_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -88,7 +88,7 @@ async def test_step_user_unexpected_exception(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_step_user(hass: HomeAssistant) -> None: @@ -105,7 +105,7 @@ async def test_step_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "NINA" @@ -120,7 +120,7 @@ async def test_step_user_no_selection(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "no_selection"} @@ -139,7 +139,7 @@ async def test_step_user_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -168,7 +168,7 @@ async def test_options_flow_init(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -183,7 +183,7 @@ async def test_options_flow_init(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] is None assert dict(config_entry.data) == { @@ -221,7 +221,7 @@ async def test_options_flow_with_no_selection(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -236,7 +236,7 @@ async def test_options_flow_with_no_selection(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "no_selection"} @@ -262,7 +262,7 @@ async def test_options_flow_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -287,7 +287,7 @@ async def test_options_flow_unexpected_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_options_flow_entity_removal(hass: HomeAssistant) -> None: @@ -321,7 +321,7 @@ async def test_options_flow_entity_removal(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entity_registry: er = er.async_get(hass) entries = er.async_entries_for_config_entry( diff --git a/tests/components/nmap_tracker/test_config_flow.py b/tests/components/nmap_tracker/test_config_flow.py index 3016727f7be..9a94efb6968 100644 --- a/tests/components/nmap_tracker/test_config_flow.py +++ b/tests/components/nmap_tracker/test_config_flow.py @@ -202,7 +202,7 @@ async def test_options_flow(hass: HomeAssistant, mock_get_source_ip) -> None: result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["data_schema"]({}) == { @@ -231,7 +231,7 @@ async def test_options_flow(hass: HomeAssistant, mock_get_source_ip) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_HOSTS: "192.168.1.0/24,192.168.2.0/24", CONF_HOME_INTERVAL: 5, diff --git a/tests/components/notion/test_config_flow.py b/tests/components/notion/test_config_flow.py index 45bcedde155..92e285ba899 100644 --- a/tests/components/notion/test_config_flow.py +++ b/tests/components/notion/test_config_flow.py @@ -15,7 +15,7 @@ async def test_duplicate_error(hass, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -45,7 +45,7 @@ async def test_step_reauth(hass, config, config_entry, setup_notion): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch("homeassistant.components.notion.async_setup_entry", return_value=True): @@ -54,7 +54,7 @@ async def test_step_reauth(hass, config, config_entry, setup_notion): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -64,7 +64,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -73,7 +73,7 @@ async def test_step_user(hass, config, setup_notion): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "user@host.com" assert result["data"] == { CONF_USERNAME: "user@host.com", diff --git a/tests/components/nuki/test_config_flow.py b/tests/components/nuki/test_config_flow.py index 77966bd7e5f..72713e24e99 100644 --- a/tests/components/nuki/test_config_flow.py +++ b/tests/components/nuki/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -38,7 +38,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "75BCD15" assert result2["data"] == { "host": "1.1.1.1", @@ -67,7 +67,7 @@ async def test_form_invalid_auth(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -90,7 +90,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -113,7 +113,7 @@ async def test_form_unknown_exception(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -137,7 +137,7 @@ async def test_form_already_configured(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -149,7 +149,7 @@ async def test_dhcp_flow(hass): context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -168,7 +168,7 @@ async def test_dhcp_flow(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "75BCD15" assert result2["data"] == { "host": "1.1.1.1", @@ -189,7 +189,7 @@ async def test_dhcp_flow_already_configured(hass): context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -200,7 +200,7 @@ async def test_reauth_success(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -216,7 +216,7 @@ async def test_reauth_success(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data[CONF_TOKEN] == "new-token" @@ -228,7 +228,7 @@ async def test_reauth_invalid_auth(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -240,7 +240,7 @@ async def test_reauth_invalid_auth(hass): user_input={CONF_TOKEN: "new-token"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth_confirm" assert result2["errors"] == {"base": "invalid_auth"} @@ -252,7 +252,7 @@ async def test_reauth_cannot_connect(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -264,7 +264,7 @@ async def test_reauth_cannot_connect(hass): user_input={CONF_TOKEN: "new-token"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth_confirm" assert result2["errors"] == {"base": "cannot_connect"} @@ -276,7 +276,7 @@ async def test_reauth_unknown_exception(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -288,6 +288,6 @@ async def test_reauth_unknown_exception(hass): user_input={CONF_TOKEN: "new-token"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth_confirm" assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index 2261fec3a86..2bb6a5d8286 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -45,7 +45,7 @@ async def test_form_zeroconf(hass): type="mock_type", ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -66,7 +66,7 @@ async def test_form_zeroconf(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "192.168.1.5:1234" assert result2["data"] == { CONF_HOST: "192.168.1.5", @@ -84,7 +84,7 @@ async def test_form_user_one_ups(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} mock_pynut = _get_mock_pynutclient( @@ -109,7 +109,7 @@ async def test_form_user_one_ups(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1:2222" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -133,7 +133,7 @@ async def test_form_user_multiple_ups(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} mock_pynut = _get_mock_pynutclient( @@ -156,7 +156,7 @@ async def test_form_user_multiple_ups(hass): ) assert result2["step_id"] == "ups" - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM with patch( "homeassistant.components.nut.PyNUTClient", @@ -171,7 +171,7 @@ async def test_form_user_multiple_ups(hass): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "ups2@1.1.1.1:2222" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -194,7 +194,7 @@ async def test_form_user_one_ups_with_ignored_entry(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} mock_pynut = _get_mock_pynutclient( @@ -219,7 +219,7 @@ async def test_form_user_one_ups_with_ignored_entry(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1:2222" assert result2["data"] == { CONF_HOST: "1.1.1.1", @@ -252,7 +252,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} with patch( @@ -272,7 +272,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} with patch( @@ -292,7 +292,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -329,7 +329,7 @@ async def test_abort_if_already_setup(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -368,7 +368,7 @@ async def test_abort_if_already_setup_alias(hass): ) assert result2["step_id"] == "ups" - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM with patch( "homeassistant.components.nut.PyNUTClient", @@ -379,7 +379,7 @@ async def test_abort_if_already_setup_alias(hass): {CONF_ALIAS: "ups1"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -396,14 +396,14 @@ async def test_options_flow(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_SCAN_INTERVAL: 60, } @@ -411,7 +411,7 @@ async def test_options_flow(hass): 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 + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -419,7 +419,7 @@ async def test_options_flow(hass): user_input={CONF_SCAN_INTERVAL: 12}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_SCAN_INTERVAL: 12, } diff --git a/tests/components/octoprint/test_config_flow.py b/tests/components/octoprint/test_config_flow.py index 7769e082016..e9de98206d1 100644 --- a/tests/components/octoprint/test_config_flow.py +++ b/tests/components/octoprint/test_config_flow.py @@ -230,7 +230,7 @@ async def test_show_zerconf_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_show_ssdp_form(hass: HomeAssistant) -> None: @@ -296,7 +296,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_import_yaml(hass: HomeAssistant) -> None: @@ -328,7 +328,7 @@ async def test_import_yaml(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert "errors" not in result @@ -362,7 +362,7 @@ async def test_import_duplicate_yaml(hass: HomeAssistant) -> None: 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["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/omnilogic/test_config_flow.py b/tests/components/omnilogic/test_config_flow.py index bd8f32b05bd..f3a60479b46 100644 --- a/tests/components/omnilogic/test_config_flow.py +++ b/tests/components/omnilogic/test_config_flow.py @@ -129,7 +129,7 @@ async def test_option_flow(hass): data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -137,6 +137,6 @@ async def test_option_flow(hass): user_input={"polling_interval": 9}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"]["polling_interval"] == 9 diff --git a/tests/components/ondilo_ico/test_config_flow.py b/tests/components/ondilo_ico/test_config_flow.py index e1edfc2a63c..17db65f6b41 100644 --- a/tests/components/ondilo_ico/test_config_flow.py +++ b/tests/components/ondilo_ico/test_config_flow.py @@ -25,7 +25,7 @@ async def test_abort_if_existing_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index fec1e2b8132..31c2f06f352 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -73,7 +73,7 @@ async def test_flow_discovered_devices(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -91,7 +91,7 @@ async def test_flow_discovered_devices(hass): result["flow_id"], user_input={"auto": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "device" assert len(result["data_schema"].schema[config_flow.CONF_HOST].container) == 3 @@ -99,7 +99,7 @@ async def test_flow_discovered_devices(hass): result["flow_id"], user_input={config_flow.CONF_HOST: f"{URN} ({HOST})"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "configure" with patch( @@ -116,7 +116,7 @@ async def test_flow_discovered_devices(hass): await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"{URN} - {MAC}" assert result["data"] == { config_flow.CONF_NAME: URN, @@ -135,7 +135,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -153,7 +153,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input(hass): result["flow_id"], user_input={"auto": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "device" assert len(result["data_schema"].schema[config_flow.CONF_HOST].container) == 2 @@ -162,7 +162,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input(hass): user_input={config_flow.CONF_HOST: config_flow.CONF_MANUAL_INPUT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "configure" @@ -174,7 +174,7 @@ async def test_flow_discovered_no_device(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -192,7 +192,7 @@ async def test_flow_discovered_no_device(hass): result["flow_id"], user_input={"auto": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "configure" @@ -216,7 +216,7 @@ async def test_flow_discovery_ignore_existing_and_abort(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -235,7 +235,7 @@ async def test_flow_discovery_ignore_existing_and_abort(hass): ) # It should skip to manual entry if the only devices are already configured - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "configure" result = await hass.config_entries.flow.async_configure( @@ -250,7 +250,7 @@ async def test_flow_discovery_ignore_existing_and_abort(hass): ) # It should abort if already configured and entered manually - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_flow_manual_entry(hass): @@ -259,7 +259,7 @@ async def test_flow_manual_entry(hass): config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -279,7 +279,7 @@ async def test_flow_manual_entry(hass): user_input={"auto": False}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "configure" with patch( @@ -299,7 +299,7 @@ async def test_flow_manual_entry(hass): await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"{NAME} - {MAC}" assert result["data"] == { config_flow.CONF_NAME: NAME, @@ -318,7 +318,7 @@ async def test_option_flow(hass): entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "onvif_devices" result = await hass.config_entries.options.async_configure( @@ -330,7 +330,7 @@ async def test_option_flow(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { config_flow.CONF_EXTRA_ARGUMENTS: "", config_flow.CONF_RTSP_TRANSPORT: list(config_flow.RTSP_TRANSPORTS)[1], diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index 99133cf17c3..8244ece6b9f 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -204,7 +204,7 @@ async def test_options_migration(hass): entry.entry_id, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -212,7 +212,7 @@ async def test_options_migration(hass): user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == PRECISION_TENTHS assert result["data"][CONF_SET_PRECISION] == PRECISION_TENTHS assert result["data"][CONF_FLOOR_TEMP] is True @@ -243,7 +243,7 @@ async def test_options_form(hass): result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -256,7 +256,7 @@ async def test_options_form(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == PRECISION_HALVES assert result["data"][CONF_SET_PRECISION] == PRECISION_HALVES assert result["data"][CONF_TEMPORARY_OVRD_MODE] is True @@ -270,7 +270,7 @@ async def test_options_form(hass): result["flow_id"], user_input={CONF_READ_PRECISION: 0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == 0.0 assert result["data"][CONF_SET_PRECISION] == PRECISION_HALVES assert result["data"][CONF_TEMPORARY_OVRD_MODE] is True @@ -290,7 +290,7 @@ async def test_options_form(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_READ_PRECISION] == PRECISION_TENTHS assert result["data"][CONF_SET_PRECISION] == PRECISION_HALVES assert result["data"][CONF_TEMPORARY_OVRD_MODE] is False diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index 8960d39a5b9..9f51728365b 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -19,7 +19,7 @@ async def test_duplicate_error(hass, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -32,7 +32,7 @@ async def test_invalid_api_key(hass, config): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} @@ -41,13 +41,13 @@ async def test_options_flow(hass, config_entry): with patch("homeassistant.components.openuv.async_setup_entry", return_value=True): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 2.0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 2.0} @@ -56,13 +56,13 @@ async def test_step_user(hass, config, setup_openuv): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "51.528308, -0.3817765" assert result["data"] == { CONF_API_KEY: "abcde12345", diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index 5225aad83cd..12ee849d3d2 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -45,7 +45,7 @@ async def test_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -63,7 +63,7 @@ async def test_form(hass): await hass.async_block_till_done() assert entry.state == ConfigEntryState.NOT_LOADED - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] @@ -90,14 +90,14 @@ async def test_form_options(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_MODE: "daily"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_MODE: "daily", CONF_LANGUAGE: DEFAULT_LANGUAGE, @@ -109,14 +109,14 @@ async def test_form_options(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_MODE: "onecall_daily"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_MODE: "onecall_daily", CONF_LANGUAGE: DEFAULT_LANGUAGE, diff --git a/tests/components/overkiz/test_config_flow.py b/tests/components/overkiz/test_config_flow.py index 967d6cbb8c8..0542f4dc9fc 100644 --- a/tests/components/overkiz/test_config_flow.py +++ b/tests/components/overkiz/test_config_flow.py @@ -106,7 +106,7 @@ async def test_form_invalid_auth( ) assert result["step_id"] == config_entries.SOURCE_USER - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": error} @@ -131,7 +131,7 @@ async def test_abort_on_duplicate_entry(hass: HomeAssistant) -> None: {"username": TEST_EMAIL, "password": TEST_PASSWORD}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -177,7 +177,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( @@ -220,7 +220,7 @@ async def test_dhcp_flow_already_configured(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -232,7 +232,7 @@ async def test_zeroconf_flow(hass): context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( @@ -271,7 +271,7 @@ async def test_zeroconf_flow_already_configured(hass): context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -295,7 +295,7 @@ async def test_reauth_success(hass): data=mock_entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( @@ -311,7 +311,7 @@ async def test_reauth_success(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_entry.data["username"] == TEST_EMAIL assert mock_entry.data["password"] == TEST_PASSWORD2 @@ -337,7 +337,7 @@ async def test_reauth_wrong_account(hass): data=mock_entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( @@ -353,5 +353,5 @@ async def test_reauth_wrong_account(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_wrong_account" diff --git a/tests/components/ovo_energy/test_config_flow.py b/tests/components/ovo_energy/test_config_flow.py index a8f0c098aba..5fbd4586d12 100644 --- a/tests/components/ovo_energy/test_config_flow.py +++ b/tests/components/ovo_energy/test_config_flow.py @@ -22,7 +22,7 @@ async def test_show_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -32,7 +32,7 @@ async def test_authorization_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -44,7 +44,7 @@ async def test_authorization_error(hass: HomeAssistant) -> None: FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -55,7 +55,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -67,7 +67,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -78,7 +78,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -93,7 +93,7 @@ async def test_full_flow_implementation(hass: HomeAssistant) -> None: FIXTURE_USER_INPUT, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result2["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] @@ -110,7 +110,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -119,7 +119,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "authorization_error"} @@ -136,7 +136,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" result2 = await hass.config_entries.flow.async_configure( @@ -145,7 +145,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "reauth" assert result2["errors"] == {"base": "connection_error"} @@ -167,7 +167,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth" assert result["errors"] == {"base": "authorization_error"} @@ -184,5 +184,5 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 31a3a327d1c..e6d337f9420 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -64,11 +64,11 @@ async def test_user(hass, webhook_id, secret): flow = await init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "OwnTracks" assert result["data"][CONF_WEBHOOK_ID] == WEBHOOK_ID assert result["data"][CONF_SECRET] == SECRET @@ -98,7 +98,7 @@ async def test_abort_if_already_setup(hass): # Should fail, already setup (flow) result = await flow.async_step_user({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -107,7 +107,7 @@ async def test_user_not_supports_encryption(hass, not_supports_encryption): flow = await init_config_flow(hass) result = await flow.async_step_user({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert ( result["description_placeholders"]["secret"] == "Encryption is not supported because nacl is not installed." @@ -165,7 +165,7 @@ async def test_with_cloud_sub(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = result["result"] assert entry.data["cloudhook"] assert ( @@ -192,5 +192,5 @@ async def test_with_cloud_sub_not_connected(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cloud_not_connected" diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py index ace62195115..c6bade94ea4 100644 --- a/tests/components/philips_js/test_config_flow.py +++ b/tests/components/philips_js/test_config_flow.py @@ -219,12 +219,12 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_ALLOW_NOTIFY: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_ALLOW_NOTIFY: True} diff --git a/tests/components/picnic/test_config_flow.py b/tests/components/picnic/test_config_flow.py index 3ea54cee593..24878a1b701 100644 --- a/tests/components/picnic/test_config_flow.py +++ b/tests/components/picnic/test_config_flow.py @@ -39,7 +39,7 @@ async def test_form(hass, picnic_api): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -85,7 +85,7 @@ async def test_form_invalid_auth(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -108,7 +108,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -131,7 +131,7 @@ async def test_form_exception(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -158,7 +158,7 @@ async def test_form_already_configured(hass, picnic_api): ) await hass.async_block_till_done() - assert result_configure["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result_configure["type"] == data_entry_flow.FlowResultType.ABORT assert result_configure["reason"] == "already_configured" @@ -177,7 +177,7 @@ async def test_step_reauth(hass, picnic_api): result_init = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=conf ) - assert result_init["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result_init["type"] == data_entry_flow.FlowResultType.FORM assert result_init["step_id"] == "user" with patch( @@ -195,7 +195,7 @@ async def test_step_reauth(hass, picnic_api): await hass.async_block_till_done() # Check that the returned flow has type abort because of successful re-authentication - assert result_configure["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result_configure["type"] == data_entry_flow.FlowResultType.ABORT assert result_configure["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -217,7 +217,7 @@ async def test_step_reauth_failed(hass): result_init = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=conf ) - assert result_init["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result_init["type"] == data_entry_flow.FlowResultType.FORM assert result_init["step_id"] == "user" with patch( @@ -256,7 +256,7 @@ async def test_step_reauth_different_account(hass, picnic_api): result_init = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=conf ) - assert result_init["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result_init["type"] == data_entry_flow.FlowResultType.FORM assert result_init["step_id"] == "user" with patch( diff --git a/tests/components/plaato/test_config_flow.py b/tests/components/plaato/test_config_flow.py index ba244138469..b7b39aa50b4 100644 --- a/tests/components/plaato/test_config_flow.py +++ b/tests/components/plaato/test_config_flow.py @@ -304,7 +304,7 @@ async def test_options(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.options.async_configure( @@ -314,7 +314,7 @@ async def test_options(hass): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_SCAN_INTERVAL] == 10 assert len(mock_setup_entry.mock_calls) == 1 @@ -339,7 +339,7 @@ async def test_options_webhook(hass, webhook_id): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "webhook" assert result["description_placeholders"] == {"webhook_url": ""} @@ -350,7 +350,7 @@ async def test_options_webhook(hass, webhook_id): await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_WEBHOOK_ID] == CONF_WEBHOOK_ID assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index 93ea18f21b6..e9587592175 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -48,7 +48,7 @@ async def test_abort_if_no_implementation_registered(hass): flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_flows" @@ -58,12 +58,12 @@ async def test_abort_if_already_setup(hass): with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_setup" with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_import() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_setup" @@ -75,18 +75,18 @@ async def test_full_flow_implementation( flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({"flow_impl": "test"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["description_placeholders"] == { "authorization_url": "https://example.com" } result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["refresh_args"] == { CONF_CLIENT_ID: "id", CONF_CLIENT_SECRET: "secret", @@ -100,7 +100,7 @@ async def test_step_import(hass, mock_pypoint): # pylint: disable=redefined-out flow = init_config_flow(hass) result = await flow.async_step_import() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -112,7 +112,7 @@ async def test_wrong_code_flow_implementation( flow = init_config_flow(hass) result = await flow.async_step_code("123ABC") - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "auth_error" @@ -121,7 +121,7 @@ async def test_not_pick_implementation_if_only_one(hass): flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -130,7 +130,7 @@ async def test_abort_if_timeout_generating_auth_url(hass): flow = init_config_flow(hass, side_effect=asyncio.TimeoutError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -139,7 +139,7 @@ async def test_abort_if_exception_generating_auth_url(hass): flow = init_config_flow(hass, side_effect=ValueError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown_authorize_url_generation" @@ -148,5 +148,5 @@ async def test_abort_no_code(hass): flow = init_config_flow(hass) result = await flow.async_step_code() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_code" diff --git a/tests/components/poolsense/test_config_flow.py b/tests/components/poolsense/test_config_flow.py index 71fa76df7ab..0710ff1d26d 100644 --- a/tests/components/poolsense/test_config_flow.py +++ b/tests/components/poolsense/test_config_flow.py @@ -13,7 +13,7 @@ async def test_show_form(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER @@ -47,7 +47,7 @@ async def test_valid_credentials(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "test-email" assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 57e65d15be4..346b9718165 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -103,7 +103,7 @@ async def test_full_flow_implementation(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. @@ -111,7 +111,7 @@ async def test_full_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. @@ -121,7 +121,7 @@ async def test_full_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" # User Input results in created entry. @@ -131,7 +131,7 @@ async def test_full_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert result["data"]["devices"] == [MOCK_DEVICE] assert result["title"] == MOCK_TITLE @@ -144,7 +144,7 @@ async def test_multiple_flow_implementation(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. @@ -152,7 +152,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. @@ -163,7 +163,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" # User Input results in created entry. @@ -174,7 +174,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert result["data"]["devices"] == [MOCK_DEVICE] assert result["title"] == MOCK_TITLE @@ -196,7 +196,7 @@ async def test_multiple_flow_implementation(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. @@ -204,7 +204,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. @@ -215,7 +215,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" # Step Link @@ -226,7 +226,7 @@ async def test_multiple_flow_implementation(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG_ADDITIONAL ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert len(result["data"]["devices"]) == 1 assert result["title"] == MOCK_TITLE @@ -249,7 +249,7 @@ async def test_port_bind_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == reason with patch("pyps4_2ndscreen.Helper.port_bind", return_value=MOCK_TCP_PORT): @@ -257,7 +257,7 @@ async def test_port_bind_abort(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == reason @@ -269,14 +269,14 @@ async def test_duplicate_abort(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -285,7 +285,7 @@ async def test_duplicate_abort(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -299,14 +299,14 @@ async def test_additional_device(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -322,7 +322,7 @@ async def test_additional_device(hass): result["flow_id"], user_input=MOCK_CONFIG_ADDITIONAL ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS assert len(result["data"]["devices"]) == 1 assert result["title"] == MOCK_TITLE @@ -336,7 +336,7 @@ async def test_0_pin(hass): context={"source": "creds"}, data={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -348,7 +348,7 @@ async def test_0_pin(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_AUTO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" mock_config = MOCK_CONFIG @@ -372,14 +372,14 @@ async def test_no_devices_found_abort(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" with patch("pyps4_2ndscreen.Helper.has_devices", return_value=[]): @@ -387,7 +387,7 @@ async def test_no_devices_found_abort(hass): result["flow_id"], user_input=MOCK_AUTO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -397,14 +397,14 @@ async def test_manual_mode(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" # Step Mode with User Input: manual, results in Step Link. @@ -415,7 +415,7 @@ async def test_manual_mode(hass): result["flow_id"], user_input=MOCK_MANUAL ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" @@ -425,7 +425,7 @@ async def test_credential_abort(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=None): @@ -433,7 +433,7 @@ async def test_credential_abort(hass): result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "credential_error" @@ -443,7 +443,7 @@ async def test_credential_timeout(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", side_effect=CredentialTimeout): @@ -451,7 +451,7 @@ async def test_credential_timeout(hass): result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" assert result["errors"] == {"base": "credential_timeout"} @@ -462,14 +462,14 @@ async def test_wrong_pin_error(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -483,7 +483,7 @@ async def test_wrong_pin_error(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "login_failed"} @@ -494,14 +494,14 @@ async def test_device_connection_error(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" with patch( @@ -515,7 +515,7 @@ async def test_device_connection_error(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {"base": "cannot_connect"} @@ -526,20 +526,20 @@ async def test_manual_mode_no_ip_error(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "creds" with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"Config Mode": "Manual Entry"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "mode" assert result["errors"] == {CONF_IP_ADDRESS: "no_ipaddress"} diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index a84adba2a70..7cda5b42fa2 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -43,7 +43,7 @@ MOCK_DATA = {CONF_TOKEN: MOCK_CREDS, "devices": [MOCK_DEVICE]} MOCK_FLOW_RESULT = { "version": VERSION, "handler": DOMAIN, - "type": data_entry_flow.RESULT_TYPE_CREATE_ENTRY, + "type": data_entry_flow.FlowResultType.CREATE_ENTRY, "title": "test_ps4", "data": MOCK_DATA, "options": {}, @@ -125,7 +125,7 @@ async def test_creating_entry_sets_up_media_player(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index db1a72147eb..e67aca154c4 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -43,12 +43,12 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): 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["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], tst_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get("sensor.test") @@ -59,11 +59,11 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): 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["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], tst_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert pvpc_aioclient_mock.call_count == 1 # Check removal @@ -75,12 +75,12 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): 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["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], tst_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get("sensor.test") @@ -96,7 +96,7 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): config_entry = current_entries[0] result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( diff --git a/tests/components/qnap_qsw/test_config_flow.py b/tests/components/qnap_qsw/test_config_flow.py index 02f873c6a4a..fbc850f8fe1 100644 --- a/tests/components/qnap_qsw/test_config_flow.py +++ b/tests/components/qnap_qsw/test_config_flow.py @@ -48,7 +48,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {} @@ -62,7 +62,7 @@ async def test_form(hass: HomeAssistant) -> None: entry = conf_entries[0] assert entry.state is ConfigEntryState.LOADED - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert ( result["title"] == f"QNAP {SYSTEM_BOARD_MOCK[API_RESULT][API_PRODUCT]} {SYSTEM_BOARD_MOCK[API_RESULT][API_MAC_ADDR]}" @@ -159,7 +159,7 @@ async def test_dhcp_flow(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovered_connection" with patch( @@ -206,7 +206,7 @@ async def test_dhcp_flow_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -223,7 +223,7 @@ async def test_dhcp_connection_error(hass: HomeAssistant): context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovered_connection" with patch( @@ -254,7 +254,7 @@ async def test_dhcp_login_error(hass: HomeAssistant): context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovered_connection" with patch( diff --git a/tests/components/radiotherm/test_config_flow.py b/tests/components/radiotherm/test_config_flow.py index 56a361404f8..862b7b30032 100644 --- a/tests/components/radiotherm/test_config_flow.py +++ b/tests/components/radiotherm/test_config_flow.py @@ -47,7 +47,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "My Name" assert result2["data"] == { "host": "1.2.3.4", @@ -72,7 +72,7 @@ async def test_form_unknown_error(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -93,7 +93,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {CONF_HOST: "cannot_connect"} @@ -113,7 +113,7 @@ async def test_import(hass): data={CONF_HOST: "1.2.3.4"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "My Name" assert result["data"] == {CONF_HOST: "1.2.3.4"} assert len(mock_setup_entry.mock_calls) == 1 @@ -131,7 +131,7 @@ async def test_import_cannot_connect(hass): data={CONF_HOST: "1.2.3.4"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -153,7 +153,7 @@ async def test_dhcp_can_confirm(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { "host": "1.2.3.4", @@ -171,7 +171,7 @@ async def test_dhcp_can_confirm(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "My Name" assert result2["data"] == { "host": "1.2.3.4", @@ -197,7 +197,7 @@ async def test_dhcp_fails_to_connect(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -226,7 +226,7 @@ async def test_dhcp_already_exists(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -243,7 +243,7 @@ async def test_user_unique_id_already_exists(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -261,5 +261,5 @@ async def test_user_unique_id_already_exists(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index 8b313eb2fb5..deb03d65cb5 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -16,7 +16,7 @@ async def test_duplicate_error(hass, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -95,13 +95,13 @@ async def test_options_flow(hass, config, config_entry): ): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_ZONE_RUN_TIME: 600} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_ZONE_RUN_TIME: 600} @@ -112,7 +112,7 @@ async def test_show_form(hass): context={"source": config_entries.SOURCE_USER}, data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -123,7 +123,7 @@ async def test_step_user(hass, config, setup_rainmachine): context={"source": config_entries.SOURCE_USER}, data=config, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "12345" assert result["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -158,7 +158,7 @@ async def test_step_homekit_zeroconf_ip_already_exists( ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -184,7 +184,7 @@ async def test_step_homekit_zeroconf_ip_change(hass, client, config_entry, sourc ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_IP_ADDRESS] == "192.168.1.2" @@ -213,7 +213,7 @@ async def test_step_homekit_zeroconf_new_controller_when_some_exist( ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -231,7 +231,7 @@ async def test_step_homekit_zeroconf_new_controller_when_some_exist( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "12345" assert result2["data"] == { CONF_IP_ADDRESS: "192.168.1.100", @@ -261,7 +261,7 @@ async def test_discovery_by_homekit_and_zeroconf_same_time(hass, client): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -281,5 +281,5 @@ async def test_discovery_by_homekit_and_zeroconf_same_time(hass, client): ), ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_in_progress" diff --git a/tests/components/recollect_waste/test_config_flow.py b/tests/components/recollect_waste/test_config_flow.py index 4295f3777d5..ba09a2f6d6b 100644 --- a/tests/components/recollect_waste/test_config_flow.py +++ b/tests/components/recollect_waste/test_config_flow.py @@ -18,7 +18,7 @@ async def test_duplicate_error(hass, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -31,7 +31,7 @@ async def test_invalid_place_or_service_id(hass, config): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_place_or_service_id"} @@ -42,13 +42,13 @@ async def test_options_flow(hass, config, config_entry): ): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_FRIENDLY_NAME: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_FRIENDLY_NAME: True} @@ -57,7 +57,7 @@ async def test_show_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -67,6 +67,6 @@ async def test_step_user(hass, config, setup_recollect_waste): DOMAIN, context={"source": SOURCE_USER}, data=config ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "12345, 12345" assert result["data"] == {CONF_PLACE_ID: "12345", CONF_SERVICE_ID: "12345"} diff --git a/tests/components/renault/test_config_flow.py b/tests/components/renault/test_config_flow.py index ec5eae468fc..a269106c159 100644 --- a/tests/components/renault/test_config_flow.py +++ b/tests/components/renault/test_config_flow.py @@ -38,7 +38,7 @@ async def test_config_flow_single_account( 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} # Failed credentials @@ -55,7 +55,7 @@ async def test_config_flow_single_account( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_credentials"} renault_account = AsyncMock() @@ -82,7 +82,7 @@ async def test_config_flow_single_account( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "account_id_1" assert result["data"][CONF_USERNAME] == "email@test.com" assert result["data"][CONF_PASSWORD] == "test" @@ -97,7 +97,7 @@ async def test_config_flow_no_account(hass: HomeAssistant, mock_setup_entry: Asy 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} # Account list empty @@ -114,7 +114,7 @@ async def test_config_flow_no_account(hass: HomeAssistant, mock_setup_entry: Asy }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "kamereon_no_account" assert len(mock_setup_entry.mock_calls) == 0 @@ -127,7 +127,7 @@ async def test_config_flow_multiple_accounts( 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} renault_account_1 = RenaultAccount( @@ -153,7 +153,7 @@ async def test_config_flow_multiple_accounts( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "kamereon" # Account selected @@ -161,7 +161,7 @@ async def test_config_flow_multiple_accounts( result["flow_id"], user_input={CONF_KAMEREON_ACCOUNT_ID: "account_id_2"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "account_id_2" assert result["data"][CONF_USERNAME] == "email@test.com" assert result["data"][CONF_PASSWORD] == "test" @@ -179,7 +179,7 @@ async def test_config_flow_duplicate(hass: HomeAssistant, mock_setup_entry: Asyn 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} renault_account = RenaultAccount( @@ -199,7 +199,7 @@ async def test_config_flow_duplicate(hass: HomeAssistant, mock_setup_entry: Asyn }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() @@ -220,7 +220,7 @@ async def test_reauth(hass: HomeAssistant, config_entry: ConfigEntry): data=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["description_placeholders"] == {CONF_USERNAME: "email@test.com"} assert result["errors"] == {} @@ -234,7 +234,7 @@ async def test_reauth(hass: HomeAssistant, config_entry: ConfigEntry): user_input={CONF_PASSWORD: "any"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["description_placeholders"] == {CONF_USERNAME: "email@test.com"} assert result2["errors"] == {"base": "invalid_credentials"} @@ -245,5 +245,5 @@ async def test_reauth(hass: HomeAssistant, config_entry: ConfigEntry): user_input={CONF_PASSWORD: "any"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "reauth_successful" diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index 2c695d71d2e..23ce5d1393f 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -314,7 +314,7 @@ async def test_options_global(hass): user_input={"automatic_add": True, "protocols": SOME_PROTOCOLS}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -349,7 +349,7 @@ async def test_no_protocols(hass): user_input={"automatic_add": False, "protocols": []}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -404,7 +404,7 @@ async def test_options_add_device(hass): result["flow_id"], user_input={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -558,7 +558,7 @@ async def test_options_replace_sensor_device(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -691,7 +691,7 @@ async def test_options_replace_control_device(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -772,7 +772,7 @@ async def test_options_add_and_configure_device(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -816,7 +816,7 @@ async def test_options_add_and_configure_device(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() @@ -896,7 +896,7 @@ async def test_options_configure_rfy_cover_device(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index dfd182d4a24..8dd7c4bf7f7 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -165,14 +165,14 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=TEST_OPTIONS, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "risco_to_ha" result = await hass.config_entries.options.async_configure( @@ -180,7 +180,7 @@ async def test_options_flow(hass): user_input=TEST_RISCO_TO_HA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "ha_to_risco" with patch("homeassistant.components.risco.async_setup_entry", return_value=True): @@ -189,7 +189,7 @@ async def test_options_flow(hass): user_input=TEST_HA_TO_RISCO, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options == { **TEST_OPTIONS, "risco_states_to_ha": TEST_RISCO_TO_HA, diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index de66a1ae3b7..78834ed955a 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -128,7 +128,7 @@ async def test_form_user_discovery_and_password_fetch(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -137,7 +137,7 @@ async def test_form_user_discovery_and_password_fetch(hass): {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "link" @@ -157,7 +157,7 @@ async def test_form_user_discovery_and_password_fetch(hass): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "robot_name" assert result3["result"].unique_id == "BLID" assert result3["data"] == { @@ -184,7 +184,7 @@ async def test_form_user_discovery_skips_known(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -204,7 +204,7 @@ async def test_form_user_no_devices_found_discovery_aborts_already_configured(ha ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -213,7 +213,7 @@ async def test_form_user_no_devices_found_discovery_aborts_already_configured(ha {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -233,7 +233,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -242,7 +242,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch(hass): {CONF_HOST: None}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "manual" @@ -255,7 +255,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch(hass): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] is None with patch( @@ -274,7 +274,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch(hass): ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result4["title"] == "robot_name" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -302,7 +302,7 @@ async def test_form_user_discover_fails_aborts_already_configured(hass): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -311,7 +311,7 @@ async def test_form_user_discover_fails_aborts_already_configured(hass): {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -328,7 +328,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -337,7 +337,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con {CONF_HOST: None}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "manual" @@ -351,7 +351,7 @@ async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_con ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -372,7 +372,7 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch(hass ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -384,7 +384,7 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch(hass {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None with patch( @@ -403,7 +403,7 @@ async def test_form_user_discovery_no_devices_found_and_auto_password_fetch(hass ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "robot_name" assert result3["result"].unique_id == "BLID" assert result3["data"] == { @@ -433,7 +433,7 @@ async def test_form_user_discovery_no_devices_found_and_password_fetch_fails(has ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -445,7 +445,7 @@ async def test_form_user_discovery_no_devices_found_and_password_fetch_fails(has {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None with patch( @@ -471,7 +471,7 @@ async def test_form_user_discovery_no_devices_found_and_password_fetch_fails(has ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result4["title"] == "myroomba" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -504,7 +504,7 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -516,7 +516,7 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None with patch( @@ -542,7 +542,7 @@ async def test_form_user_discovery_not_devices_found_and_password_fetch_fails_an ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["type"] == data_entry_flow.FlowResultType.FORM assert result4["errors"] == {"base": "cannot_connect"} assert len(mock_setup_entry.mock_calls) == 0 @@ -563,7 +563,7 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused(ha ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -572,7 +572,7 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused(ha {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "link" @@ -599,7 +599,7 @@ async def test_form_user_discovery_and_password_fetch_gets_connection_refused(ha ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result4["title"] == "myroomba" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -631,7 +631,7 @@ async def test_dhcp_discovery_and_roomba_discovery_finds(hass, discovery_data): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "link" assert result["description_placeholders"] == {"name": "robot_name"} @@ -652,7 +652,7 @@ async def test_dhcp_discovery_and_roomba_discovery_finds(hass, discovery_data): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "robot_name" assert result2["result"].unique_id == "BLID" assert result2["data"] == { @@ -684,7 +684,7 @@ async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "user" @@ -693,7 +693,7 @@ async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): {}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None assert result2["step_id"] == "manual" @@ -705,7 +705,7 @@ async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] is None with patch( @@ -724,7 +724,7 @@ async def test_dhcp_discovery_falls_back_to_manual(hass, discovery_data): ) await hass.async_block_till_done() - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result4["title"] == "robot_name" assert result4["result"].unique_id == "BLID" assert result4["data"] == { @@ -757,7 +757,7 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual(hass, discovery_da ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "manual" @@ -769,7 +769,7 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual(hass, discovery_da {CONF_HOST: MOCK_IP}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] is None with patch( @@ -788,7 +788,7 @@ async def test_dhcp_discovery_no_devices_falls_back_to_manual(hass, discovery_da ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "robot_name" assert result3["result"].unique_id == "BLID" assert result3["data"] == { diff --git a/tests/components/roon/test_config_flow.py b/tests/components/roon/test_config_flow.py index 7cc37bc73cd..3e5bce8c1c2 100644 --- a/tests/components/roon/test_config_flow.py +++ b/tests/components/roon/test_config_flow.py @@ -185,7 +185,7 @@ async def test_duplicate_config(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/sabnzbd/test_config_flow.py b/tests/components/sabnzbd/test_config_flow.py index bc72dff2535..1444d82cc5b 100644 --- a/tests/components/sabnzbd/test_config_flow.py +++ b/tests/components/sabnzbd/test_config_flow.py @@ -55,7 +55,7 @@ async def test_create_entry(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "edc3eee7330e" assert result2["data"] == { CONF_API_KEY: "edc3eee7330e4fdda04489e3fbc283d0", @@ -93,7 +93,7 @@ async def test_import_flow(hass) -> None: data=VALID_CONFIG_OLD, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "edc3eee7330e" assert result["data"][CONF_NAME] == "Sabnzbd" assert result["data"][CONF_API_KEY] == "edc3eee7330e4fdda04489e3fbc283d0" diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 145bcbb3566..f47fdef0994 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -75,7 +75,7 @@ async def test_form(hass, gen): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -93,7 +93,7 @@ async def test_title_without_name(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} settings = MOCK_SETTINGS.copy() @@ -123,7 +123,7 @@ async def test_title_without_name(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "shelly1pm-12345" assert result2["data"] == { "host": "1.1.1.1", @@ -148,7 +148,7 @@ async def test_form_auth(hass, test_data): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -160,7 +160,7 @@ async def test_form_auth(hass, test_data): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -191,7 +191,7 @@ async def test_form_auth(hass, test_data): ) await hass.async_block_till_done() - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result3["title"] == "Test name" assert result3["data"] == { "host": "1.1.1.1", @@ -221,7 +221,7 @@ async def test_form_errors_get_info(hass, error): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": base_error} @@ -248,7 +248,7 @@ async def test_form_missing_model_key(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "firmware_not_fully_provisioned"} @@ -257,7 +257,7 @@ async def test_form_missing_model_key_auth_enabled(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -269,7 +269,7 @@ async def test_form_missing_model_key_auth_enabled(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -286,7 +286,7 @@ async def test_form_missing_model_key_auth_enabled(hass): result2["flow_id"], {"password": "1234"} ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] == {"base": "firmware_not_fully_provisioned"} @@ -311,14 +311,14 @@ async def test_form_missing_model_key_zeroconf(hass, caplog): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "firmware_not_fully_provisioned"} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "firmware_not_fully_provisioned"} @@ -342,7 +342,7 @@ async def test_form_errors_test_connection(hass, error): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": base_error} @@ -367,7 +367,7 @@ async def test_form_already_configured(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -416,7 +416,7 @@ async def test_user_setup_ignored_device(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Test config entry got updated with latest IP assert entry.data["host"] == "1.1.1.1" @@ -439,7 +439,7 @@ async def test_form_firmware_unsupported(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "unsupported_firmware" @@ -482,7 +482,7 @@ async def test_form_auth_errors_test_connection_gen1(hass, error): result2["flow_id"], {"username": "test username", "password": "test password"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] == {"base": base_error} @@ -524,7 +524,7 @@ async def test_form_auth_errors_test_connection_gen2(hass, error): result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {"password": "test password"} ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] == {"base": base_error} @@ -548,7 +548,7 @@ async def test_zeroconf(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} context = next( flow["context"] @@ -569,7 +569,7 @@ async def test_zeroconf(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -614,7 +614,7 @@ async def test_zeroconf_sleeping_device(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} context = next( flow["context"] @@ -634,7 +634,7 @@ async def test_zeroconf_sleeping_device(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -677,7 +677,7 @@ async def test_zeroconf_sleeping_device_error(hass, error): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -698,7 +698,7 @@ async def test_zeroconf_already_configured(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -717,7 +717,7 @@ async def test_zeroconf_firmware_unsupported(hass): context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unsupported_firmware" @@ -729,7 +729,7 @@ async def test_zeroconf_cannot_connect(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -745,7 +745,7 @@ async def test_zeroconf_require_auth(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch( @@ -768,7 +768,7 @@ async def test_zeroconf_require_auth(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", diff --git a/tests/components/shopping_list/test_config_flow.py b/tests/components/shopping_list/test_config_flow.py index dfc23e18504..552bf19bcd1 100644 --- a/tests/components/shopping_list/test_config_flow.py +++ b/tests/components/shopping_list/test_config_flow.py @@ -11,7 +11,7 @@ async def test_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_user(hass): @@ -21,7 +21,7 @@ async def test_user(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -32,5 +32,5 @@ async def test_user_confirm(hass): DOMAIN, context={"source": SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].data == {} diff --git a/tests/components/sia/test_config_flow.py b/tests/components/sia/test_config_flow.py index 6b1517601f4..85992d0d811 100644 --- a/tests/components/sia/test_config_flow.py +++ b/tests/components/sia/test_config_flow.py @@ -160,7 +160,9 @@ async def test_form_start_account(hass, flow_at_add_account_step): async def test_create(hass, entry_with_basic_config): """Test we create a entry through the form.""" - assert entry_with_basic_config["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert ( + entry_with_basic_config["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + ) assert ( entry_with_basic_config["title"] == f"SIA Alarm on port {BASIC_CONFIG[CONF_PORT]}" @@ -173,7 +175,7 @@ async def test_create_additional_account(hass, entry_with_additional_account_con """Test we create a config with two accounts.""" assert ( entry_with_additional_account_config["type"] - == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + == data_entry_flow.FlowResultType.CREATE_ENTRY ) assert ( entry_with_additional_account_config["title"] @@ -314,14 +316,14 @@ async def test_options_basic(hass): ) await setup_sia(hass, config_entry) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" assert result["last_step"] updated = await hass.config_entries.options.async_configure( result["flow_id"], BASIC_OPTIONS ) - assert updated["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert updated["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert updated["data"] == { CONF_ACCOUNTS: {BASIC_CONFIG[CONF_ACCOUNT]: BASIC_OPTIONS} } @@ -339,13 +341,13 @@ async def test_options_additional(hass): ) await setup_sia(hass, config_entry) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "options" assert not result["last_step"] updated = await hass.config_entries.options.async_configure( result["flow_id"], BASIC_OPTIONS ) - assert updated["type"] == data_entry_flow.RESULT_TYPE_FORM + assert updated["type"] == data_entry_flow.FlowResultType.FORM assert updated["step_id"] == "options" assert updated["last_step"] diff --git a/tests/components/simplepush/test_config_flow.py b/tests/components/simplepush/test_config_flow.py index 4636df6b28f..6c37fb7ffe6 100644 --- a/tests/components/simplepush/test_config_flow.py +++ b/tests/components/simplepush/test_config_flow.py @@ -45,7 +45,7 @@ async def test_flow_successful(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "simplepush" assert result["data"] == MOCK_CONFIG @@ -61,7 +61,7 @@ async def test_flow_with_password(hass: HomeAssistant) -> None: result["flow_id"], user_input=mock_config_pass, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "simplepush" assert result["data"] == mock_config_pass @@ -84,7 +84,7 @@ async def test_flow_user_device_key_already_configured(hass: HomeAssistant) -> N result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -109,7 +109,7 @@ async def test_flow_user_name_already_configured(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_error_on_connection_failure(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -139,6 +139,6 @@ async def test_flow_import(hass: HomeAssistant) -> None: data=MOCK_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "simplepush" assert result["data"] == MOCK_CONFIG diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 2e0b85bc6c6..9211ec8ea28 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -25,18 +25,18 @@ async def test_duplicate_error( DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=credentials_config ) assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=sms_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -47,13 +47,13 @@ async def test_options_flow(hass, config_entry): ): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_CODE: "4321"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_CODE: "4321"} @@ -72,18 +72,18 @@ async def test_step_reauth( DOMAIN, context={"source": SOURCE_REAUTH}, data=config ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=reauth_config ) assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=sms_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 2 @@ -112,12 +112,12 @@ async def test_step_reauth_errors(hass, config, error_string, exc, reauth_config DOMAIN, context={"source": SOURCE_REAUTH}, data=config ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=reauth_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": error_string} @@ -149,18 +149,18 @@ async def test_step_reauth_from_scratch( DOMAIN, context={"source": SOURCE_REAUTH}, data=config ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=credentials_config ) assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=sms_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -186,12 +186,12 @@ async def test_step_user_errors(hass, credentials_config, error_string, exc): DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=credentials_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": error_string} @@ -205,7 +205,7 @@ async def test_step_user_email_2fa( DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Patch API.async_verify_2fa_email to first return pending, then return all done: api.async_verify_2fa_email.side_effect = [Verify2FAPending, None] @@ -213,10 +213,10 @@ async def test_step_user_email_2fa( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=credentials_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS_DONE + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS_DONE result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -236,7 +236,7 @@ async def test_step_user_email_2fa_timeout( DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Patch API.async_verify_2fa_email to return pending: api.async_verify_2fa_email.side_effect = Verify2FAPending @@ -244,14 +244,14 @@ async def test_step_user_email_2fa_timeout( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=credentials_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS_DONE + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS_DONE assert result["step_id"] == "email_2fa_error" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "email_2fa_timed_out" @@ -263,18 +263,18 @@ async def test_step_user_sms_2fa( DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=credentials_config ) assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=sms_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(hass.config_entries.async_entries()) == 1 [config_entry] = hass.config_entries.async_entries(DOMAIN) @@ -300,13 +300,13 @@ async def test_step_user_sms_2fa_errors( DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=credentials_config ) assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Simulate entering the incorrect SMS code: api.async_verify_2fa_sms.side_effect = InvalidCredentialsError @@ -314,5 +314,5 @@ async def test_step_user_sms_2fa_errors( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=sms_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"code": error_string} diff --git a/tests/components/slack/test_config_flow.py b/tests/components/slack/test_config_flow.py index 850690783e8..97c8e6ee743 100644 --- a/tests/components/slack/test_config_flow.py +++ b/tests/components/slack/test_config_flow.py @@ -23,7 +23,7 @@ async def test_flow_user( result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEAM_NAME assert result["data"] == CONF_DATA @@ -42,7 +42,7 @@ async def test_flow_user_already_configured( result["flow_id"], user_input=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -56,7 +56,7 @@ async def test_flow_user_invalid_auth( context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -71,7 +71,7 @@ async def test_flow_user_cannot_connect( context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -87,7 +87,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -103,7 +103,7 @@ async def test_flow_import( data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEAM_NAME assert result["data"] == CONF_DATA @@ -119,7 +119,7 @@ async def test_flow_import_no_name( data=CONF_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEAM_NAME assert result["data"] == CONF_DATA @@ -136,5 +136,5 @@ async def test_flow_import_already_configured( data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/sleepiq/test_config_flow.py b/tests/components/sleepiq/test_config_flow.py index 3101a7ecdfe..75a2524e2bc 100644 --- a/tests/components/sleepiq/test_config_flow.py +++ b/tests/components/sleepiq/test_config_flow.py @@ -46,7 +46,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -67,7 +67,7 @@ async def test_login_failure(hass: HomeAssistant, side_effect, error) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SLEEPIQ_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": error} @@ -90,7 +90,7 @@ async def test_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["data"][CONF_USERNAME] == SLEEPIQ_CONFIG[CONF_USERNAME] assert result2["data"][CONF_PASSWORD] == SLEEPIQ_CONFIG[CONF_PASSWORD] assert len(mock_setup_entry.mock_calls) == 1 @@ -128,5 +128,5 @@ async def test_reauth_password(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index 16d330a21a8..b29207afbca 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -29,7 +29,7 @@ async def test_show_user_form(hass): ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_show_user_host_form(hass): @@ -39,14 +39,14 @@ async def test_show_user_host_form(hass): context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_show_zeroconf_connection_error_form(hass): @@ -67,14 +67,14 @@ async def test_show_zeroconf_connection_error_form(hass): ) assert result["description_placeholders"] == {CONF_SERIALNUMBER: "1006000212"} - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" assert len(hass.config_entries.async_entries(DOMAIN)) == 0 @@ -97,14 +97,14 @@ async def test_show_zeroconf_connection_error_form_next_generation(hass): ) assert result["description_placeholders"] == {CONF_SERIALNUMBER: "5001000212"} - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" assert len(hass.config_entries.async_entries(DOMAIN)) == 0 @@ -119,19 +119,19 @@ async def test_connection_error(hass): context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_user_local_connection_error(hass): @@ -148,19 +148,19 @@ async def test_user_local_connection_error(hass): context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_zeroconf_wrong_mdns(hass): @@ -180,7 +180,7 @@ async def test_zeroconf_wrong_mdns(hass): ) assert result["reason"] == "invalid_mdns" - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_full_user_wrong_mdns(hass): @@ -199,18 +199,18 @@ async def test_full_user_wrong_mdns(hass): context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "invalid_mdns" @@ -239,18 +239,18 @@ async def test_user_device_exists_abort(hass): context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"environment": ENV_LOCAL} ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -289,7 +289,7 @@ async def test_zeroconf_device_exists_abort(hass): properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -310,7 +310,7 @@ async def test_cloud_device_exists_abort(hass): context={"source": SOURCE_USER}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -339,7 +339,7 @@ async def test_zeroconf_abort_if_cloud_device_exists(hass): properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -371,7 +371,7 @@ async def test_zeroconf_confirm_abort_if_cloud_device_exists(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -396,7 +396,7 @@ async def test_abort_cloud_flow_if_local_device_exists(hass): result["flow_id"], {"environment": ENV_CLOUD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured_local_device" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -479,7 +479,7 @@ async def test_full_zeroconf_flow(hass): properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" assert result["description_placeholders"] == {CONF_SERIALNUMBER: "1006000212"} @@ -487,7 +487,7 @@ async def test_full_zeroconf_flow(hass): result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "smappee1006000212" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -513,7 +513,7 @@ async def test_full_user_local_flow(hass): context={"source": SOURCE_USER}, ) assert result["step_id"] == "environment" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["description_placeholders"] is None result = await hass.config_entries.flow.async_configure( @@ -521,12 +521,12 @@ async def test_full_user_local_flow(hass): {"environment": ENV_LOCAL}, ) assert result["step_id"] == ENV_LOCAL - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "smappee1006000212" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 @@ -555,7 +555,7 @@ async def test_full_zeroconf_flow_next_generation(hass): properties={"_raw": {}}, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "zeroconf_confirm" assert result["description_placeholders"] == {CONF_SERIALNUMBER: "5001000212"} @@ -563,7 +563,7 @@ async def test_full_zeroconf_flow_next_generation(hass): result["flow_id"], {"host": "1.2.3.4"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "smappee5001000212" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 420c07d2a04..93b646c44dc 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -27,7 +27,7 @@ async def test_import_shows_user_step(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -52,7 +52,7 @@ async def test_entry_created(hass, app, app_oauth_client, location, smartthings_ 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -60,7 +60,7 @@ async def test_entry_created(hass, app, app_oauth_client, location, smartthings_ # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -69,14 +69,14 @@ async def test_entry_created(hass, app, app_oauth_client, location, smartthings_ result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -85,7 +85,7 @@ async def test_entry_created(hass, app, app_oauth_client, location, smartthings_ # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -123,7 +123,7 @@ async def test_entry_created_from_update_event( 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -131,7 +131,7 @@ async def test_entry_created_from_update_event( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -140,14 +140,14 @@ async def test_entry_created_from_update_event( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -156,7 +156,7 @@ async def test_entry_created_from_update_event( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -194,7 +194,7 @@ async def test_entry_created_existing_app_new_oauth_client( 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -202,7 +202,7 @@ async def test_entry_created_existing_app_new_oauth_client( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -211,14 +211,14 @@ async def test_entry_created_existing_app_new_oauth_client( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -227,7 +227,7 @@ async def test_entry_created_existing_app_new_oauth_client( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -278,7 +278,7 @@ async def test_entry_created_existing_app_copies_oauth_client( 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -286,7 +286,7 @@ async def test_entry_created_existing_app_copies_oauth_client( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -297,14 +297,14 @@ async def test_entry_created_existing_app_copies_oauth_client( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -313,7 +313,7 @@ async def test_entry_created_existing_app_copies_oauth_client( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -370,7 +370,7 @@ async def test_entry_created_with_cloudhook( 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -379,7 +379,7 @@ async def test_entry_created_with_cloudhook( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -388,14 +388,14 @@ async def test_entry_created_with_cloudhook( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_location" # Select location and advance to external auth result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCATION_ID: location.location_id} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["step_id"] == "authorize" assert result["url"] == format_install_url(app.app_id, location.location_id) @@ -404,7 +404,7 @@ async def test_entry_created_with_cloudhook( # Finish result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["app_id"] == app.app_id assert result["data"]["installed_app_id"] == installed_app_id assert result["data"]["location_id"] == location.location_id @@ -432,7 +432,7 @@ async def test_invalid_webhook_aborts(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "invalid_webhook_url" assert result["description_placeholders"][ "webhook_url" @@ -448,7 +448,7 @@ async def test_invalid_token_shows_error(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -456,7 +456,7 @@ async def test_invalid_token_shows_error(hass): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -465,7 +465,7 @@ async def test_invalid_token_shows_error(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {CONF_ACCESS_TOKEN: "token_invalid_format"} @@ -485,7 +485,7 @@ async def test_unauthorized_token_shows_error(hass, smartthings_mock): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -493,7 +493,7 @@ async def test_unauthorized_token_shows_error(hass, smartthings_mock): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -502,7 +502,7 @@ async def test_unauthorized_token_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {CONF_ACCESS_TOKEN: "token_unauthorized"} @@ -522,7 +522,7 @@ async def test_forbidden_token_shows_error(hass, smartthings_mock): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -530,7 +530,7 @@ async def test_forbidden_token_shows_error(hass, smartthings_mock): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -539,7 +539,7 @@ async def test_forbidden_token_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {CONF_ACCESS_TOKEN: "token_forbidden"} @@ -565,7 +565,7 @@ async def test_webhook_problem_shows_error(hass, smartthings_mock): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -573,7 +573,7 @@ async def test_webhook_problem_shows_error(hass, smartthings_mock): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -582,7 +582,7 @@ async def test_webhook_problem_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "webhook_error"} @@ -607,7 +607,7 @@ async def test_api_error_shows_error(hass, smartthings_mock): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -615,7 +615,7 @@ async def test_api_error_shows_error(hass, smartthings_mock): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -624,7 +624,7 @@ async def test_api_error_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "app_setup_error"} @@ -645,7 +645,7 @@ async def test_unknown_response_error_shows_error(hass, smartthings_mock): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -653,7 +653,7 @@ async def test_unknown_response_error_shows_error(hass, smartthings_mock): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -662,7 +662,7 @@ async def test_unknown_response_error_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "app_setup_error"} @@ -679,7 +679,7 @@ async def test_unknown_error_shows_error(hass, smartthings_mock): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -687,7 +687,7 @@ async def test_unknown_error_shows_error(hass, smartthings_mock): # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -696,7 +696,7 @@ async def test_unknown_error_shows_error(hass, smartthings_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert result["data_schema"]({}) == {CONF_ACCESS_TOKEN: token} assert result["errors"] == {"base": "app_setup_error"} @@ -721,7 +721,7 @@ async def test_no_available_locations_aborts( 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["description_placeholders"][ "webhook_url" @@ -729,7 +729,7 @@ async def test_no_available_locations_aborts( # Advance to PAT screen result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pat" assert "token_url" in result["description_placeholders"] assert "component_url" in result["description_placeholders"] @@ -738,5 +738,5 @@ async def test_no_available_locations_aborts( result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_ACCESS_TOKEN: token} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_available_locations" diff --git a/tests/components/smarttub/test_config_flow.py b/tests/components/smarttub/test_config_flow.py index c6170afc30e..c388f17c3ba 100644 --- a/tests/components/smarttub/test_config_flow.py +++ b/tests/components/smarttub/test_config_flow.py @@ -73,14 +73,14 @@ async def test_reauth_success(hass, smarttub_api, account): data=mock_entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_EMAIL: "test-email3", CONF_PASSWORD: "test-password3"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_entry.data[CONF_EMAIL] == "test-email3" assert mock_entry.data[CONF_PASSWORD] == "test-password3" @@ -114,12 +114,12 @@ async def test_reauth_wrong_account(hass, smarttub_api, account): data=mock_entry2.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_EMAIL: "test-email1", CONF_PASSWORD: "test-password1"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index d0f0ac01235..c23d2578d1c 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -31,7 +31,7 @@ async def test_user(hass: HomeAssistant, test_api: Mock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "user" # test with all provided @@ -40,7 +40,7 @@ async def test_user(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result.get("title") == "solaredge_site_1_2_3" data = result.get("data") @@ -62,7 +62,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant, test_api: str) -> Non context={"source": SOURCE_USER}, data={CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "already_configured"} @@ -82,7 +82,7 @@ async def test_ignored_entry_does_not_cause_error( context={"source": SOURCE_USER}, data={CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "test" data = result["data"] @@ -102,7 +102,7 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "site_not_active"} # test with api_failure @@ -112,7 +112,7 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "invalid_api_key"} # test with ConnectionTimeout @@ -122,7 +122,7 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "could_not_connect"} # test with HTTPError @@ -132,5 +132,5 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: context={"source": SOURCE_USER}, data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "could_not_connect"} diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index ccbc5412562..765f1f569e6 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -63,12 +63,12 @@ async def test_user(hass, test_connect): flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # tets with all provided result = await flow.async_step_user({CONF_NAME: NAME, CONF_HOST: HOST}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == HOST @@ -79,19 +79,19 @@ async def test_import(hass, test_connect): # import with only host result = await flow.async_step_import({CONF_HOST: HOST}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog" assert result["data"][CONF_HOST] == HOST # import with only name result = await flow.async_step_import({CONF_NAME: NAME}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == DEFAULT_HOST # import with host and name result = await flow.async_step_import({CONF_HOST: HOST, CONF_NAME: NAME}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == HOST @@ -107,19 +107,19 @@ async def test_abort_if_already_setup(hass, test_connect): result = await flow.async_step_import( {CONF_HOST: HOST, CONF_NAME: "solarlog_test_7_8_9"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Should fail, same HOST and NAME result = await flow.async_step_user({CONF_HOST: HOST, CONF_NAME: NAME}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "already_configured"} # SHOULD pass, diff HOST (without http://), different NAME result = await flow.async_step_import( {CONF_HOST: "2.2.2.2", CONF_NAME: "solarlog_test_7_8_9"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_7_8_9" assert result["data"][CONF_HOST] == "http://2.2.2.2" @@ -127,6 +127,6 @@ async def test_abort_if_already_setup(hass, test_connect): result = await flow.async_step_import( {CONF_HOST: "http://2.2.2.2", CONF_NAME: NAME} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "solarlog_test_1_2_3" assert result["data"][CONF_HOST] == "http://2.2.2.2" diff --git a/tests/components/soma/test_config_flow.py b/tests/components/soma/test_config_flow.py index 1d00f83a608..66d16ebc480 100644 --- a/tests/components/soma/test_config_flow.py +++ b/tests/components/soma/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form(hass): flow = config_flow.SomaFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_import_abort(hass): @@ -27,7 +27,7 @@ async def test_import_abort(hass): flow.hass = hass MockConfigEntry(domain=DOMAIN).add_to_hass(hass) result = await flow.async_step_import() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_setup" @@ -37,7 +37,7 @@ async def test_import_create(hass): flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={"result": "success"}): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_error_status(hass): @@ -46,7 +46,7 @@ async def test_error_status(hass): flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={"result": "error"}): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "result_error" @@ -56,7 +56,7 @@ async def test_key_error(hass): flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={}): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "connection_error" @@ -66,7 +66,7 @@ async def test_exception(hass): flow.hass = hass with patch.object(SomaApi, "list_devices", side_effect=RequestException()): result = await flow.async_step_import({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "connection_error" @@ -77,4 +77,4 @@ async def test_full_flow(hass): flow.hass = hass with patch.object(SomaApi, "list_devices", return_value={"result": "success"}): result = await flow.async_step_user({"host": MOCK_HOST, "port": MOCK_PORT}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY diff --git a/tests/components/somfy_mylink/test_config_flow.py b/tests/components/somfy_mylink/test_config_flow.py index d98e7429d8b..1fbb55ca864 100644 --- a/tests/components/somfy_mylink/test_config_flow.py +++ b/tests/components/somfy_mylink/test_config_flow.py @@ -175,7 +175,7 @@ async def test_options_not_loaded(hass): ): result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT @pytest.mark.parametrize("reversed", [True, False]) @@ -204,7 +204,7 @@ async def test_options_with_targets(hass, reversed): await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -212,19 +212,19 @@ async def test_options_with_targets(hass, reversed): user_input={"target_id": "a"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM result3 = await hass.config_entries.options.async_configure( result2["flow_id"], user_input={"reverse": reversed}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM result4 = await hass.config_entries.options.async_configure( result3["flow_id"], user_input={"target_id": None}, ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { CONF_REVERSED_TARGET_IDS: {"a": reversed}, diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index 02897c523c1..3b7c86478e0 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -26,10 +26,10 @@ async def test_creating_entry_sets_up_media_player( ) # Confirmation form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() diff --git a/tests/components/speedtestdotnet/test_config_flow.py b/tests/components/speedtestdotnet/test_config_flow.py index 7f6f6970c4d..0344c09631b 100644 --- a/tests/components/speedtestdotnet/test_config_flow.py +++ b/tests/components/speedtestdotnet/test_config_flow.py @@ -21,13 +21,13 @@ async def test_flow_works(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM 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_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: @@ -42,7 +42,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -54,7 +54,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_SERVER_NAME: "Country1 - Sponsor1 - Server1", CONF_SERVER_ID: "1", @@ -67,7 +67,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: # test setting server name to "*Auto Detect" result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -79,7 +79,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_SERVER_NAME: "*Auto Detect", CONF_SERVER_ID: None, @@ -89,7 +89,7 @@ async def test_options(hass: HomeAssistant, mock_api: MagicMock) -> None: # test setting the option to update periodically result2 = await hass.config_entries.options.async_init(entry.entry_id) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -114,5 +114,5 @@ async def test_integration_already_configured(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( speedtestdotnet.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/spider/test_config_flow.py b/tests/components/spider/test_config_flow.py index d00ab53645d..ba8707ed43c 100644 --- a/tests/components/spider/test_config_flow.py +++ b/tests/components/spider/test_config_flow.py @@ -32,7 +32,7 @@ async def test_user(hass, spider): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -45,7 +45,7 @@ async def test_user(hass, spider): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD @@ -72,7 +72,7 @@ async def test_import(hass, spider): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD @@ -91,7 +91,7 @@ async def test_abort_if_already_setup(hass, spider): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=SPIDER_USER_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" # Should fail, config exist (flow) @@ -99,5 +99,5 @@ async def test_abort_if_already_setup(hass, spider): DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=SPIDER_USER_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 3b1e4851ff1..a47d25aa06d 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -30,14 +30,14 @@ async def test_abort_if_no_configuration(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_credentials" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_credentials" @@ -49,7 +49,7 @@ async def test_zeroconf_abort_if_existing_entry(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=BLANK_ZEROCONF_INFO ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -79,7 +79,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( "https://accounts.spotify.com/authorize" "?response_type=code&client_id=client" @@ -169,7 +169,7 @@ async def test_abort_if_spotify_error( ): result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "connection_error" @@ -305,7 +305,7 @@ async def test_reauth_account_mismatch( spotify_mock.return_value.current_user.return_value = {"id": "fake_id"} result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_account_mismatch" @@ -315,5 +315,5 @@ async def test_abort_if_no_reauth_entry(hass): DOMAIN, context={"source": "reauth_confirm"} ) - assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("type") == data_entry_flow.FlowResultType.ABORT assert result.get("reason") == "reauth_account_mismatch" diff --git a/tests/components/srp_energy/test_config_flow.py b/tests/components/srp_energy/test_config_flow.py index 54f8629a980..6eac5298ae4 100644 --- a/tests/components/srp_energy/test_config_flow.py +++ b/tests/components/srp_energy/test_config_flow.py @@ -13,7 +13,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( SRP_ENERGY_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -104,7 +104,7 @@ async def test_config(hass): context={"source": config_entries.SOURCE_IMPORT}, data=ENTRY_CONFIG, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 @@ -116,5 +116,5 @@ async def test_integration_already_configured(hass): result = await hass.config_entries.flow.async_init( SRP_ENERGY_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/steam_online/test_config_flow.py b/tests/components/steam_online/test_config_flow.py index c4504bf1641..51ecd77c508 100644 --- a/tests/components/steam_online/test_config_flow.py +++ b/tests/components/steam_online/test_config_flow.py @@ -40,7 +40,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == ACCOUNT_NAME_1 assert result["data"] == CONF_DATA assert result["options"] == CONF_OPTIONS @@ -54,7 +54,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -66,7 +66,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -77,7 +77,7 @@ async def test_flow_user_invalid_account(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_account" @@ -89,7 +89,7 @@ async def test_flow_user_unknown(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -102,7 +102,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -119,20 +119,20 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: }, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" new_conf = CONF_DATA | {CONF_API_KEY: "1234567890"} result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=new_conf, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == new_conf @@ -145,7 +145,7 @@ async def test_flow_import(hass: HomeAssistant) -> None: context={"source": SOURCE_IMPORT}, data=CONF_IMPORT_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == ACCOUNT_NAME_1 assert result["data"] == CONF_DATA assert result["options"] == CONF_IMPORT_OPTIONS @@ -161,7 +161,7 @@ async def test_flow_import_already_configured(hass: HomeAssistant) -> None: context={"source": SOURCE_IMPORT}, data=CONF_IMPORT_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -173,7 +173,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -182,7 +182,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == CONF_OPTIONS_2 assert len(er.async_get(hass).entities) == 2 @@ -195,7 +195,7 @@ async def test_options_flow_deselect(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -203,7 +203,7 @@ async def test_options_flow_deselect(hass: HomeAssistant) -> None: user_input={CONF_ACCOUNTS: []}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_ACCOUNTS: {}} assert len(er.async_get(hass).entities) == 0 @@ -215,7 +215,7 @@ async def test_options_flow_timeout(hass: HomeAssistant) -> None: servicemock.side_effect = steam.api.HTTPTimeoutError result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -224,7 +224,7 @@ async def test_options_flow_timeout(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == CONF_OPTIONS @@ -234,7 +234,7 @@ async def test_options_flow_unauthorized(hass: HomeAssistant) -> None: with patch_interface_private(): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -243,5 +243,5 @@ async def test_options_flow_unauthorized(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == CONF_OPTIONS diff --git a/tests/components/syncthing/test_config_flow.py b/tests/components/syncthing/test_config_flow.py index 80656c75990..6318f4da92e 100644 --- a/tests/components/syncthing/test_config_flow.py +++ b/tests/components/syncthing/test_config_flow.py @@ -29,7 +29,7 @@ async def test_show_setup_form(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "user" @@ -52,7 +52,7 @@ async def test_flow_successful(hass): CONF_VERIFY_SSL: VERIFY_SSL, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:8384" assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_URL] == URL @@ -74,7 +74,7 @@ async def test_flow_already_configured(hass): data=MOCK_ENTRY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -88,7 +88,7 @@ async def test_flow_invalid_auth(hass): data=MOCK_ENTRY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["token"] == "invalid_auth" @@ -102,5 +102,5 @@ async def test_flow_cannot_connect(hass): data=MOCK_ENTRY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"]["base"] == "cannot_connect" diff --git a/tests/components/syncthru/test_config_flow.py b/tests/components/syncthru/test_config_flow.py index 7a91ff7a735..40ed12d0d0b 100644 --- a/tests/components/syncthru/test_config_flow.py +++ b/tests/components/syncthru/test_config_flow.py @@ -43,7 +43,7 @@ async def test_show_setup_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -65,7 +65,7 @@ async def test_already_configured_by_url(hass, aioclient_mock): data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] assert result["data"][CONF_NAME] == FIXTURE_USER_INPUT[CONF_NAME] assert result["result"].unique_id == udn @@ -80,7 +80,7 @@ async def test_syncthru_not_supported(hass): data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_URL: "syncthru_not_supported"} @@ -96,7 +96,7 @@ async def test_unknown_state(hass): data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {CONF_URL: "unknown_state"} @@ -115,7 +115,7 @@ async def test_success(hass, aioclient_mock): data=FIXTURE_USER_INPUT, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_URL] == FIXTURE_USER_INPUT[CONF_URL] await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 @@ -144,7 +144,7 @@ async def test_ssdp(hass, aioclient_mock): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" assert CONF_URL in result["data_schema"].schema for k in result["data_schema"].schema: diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 8cec30abefa..015c3a2ab16 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -120,7 +120,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # test with all provided @@ -136,7 +136,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST @@ -163,7 +163,7 @@ async def test_user(hass: HomeAssistant, service: MagicMock): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL_2 assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST @@ -185,7 +185,7 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock): context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "2sa" # Failed the first time because was too slow to enter the code @@ -195,7 +195,7 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock): result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_OTP_CODE: "000000"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "2sa" assert result["errors"] == {CONF_OTP_CODE: "otp_failed"} @@ -206,7 +206,7 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock): result["flow_id"], {CONF_OTP_CODE: "123456"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST @@ -226,7 +226,7 @@ async def test_user_vdsm(hass: HomeAssistant, service_vdsm: MagicMock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # test with all provided @@ -242,7 +242,7 @@ async def test_user_vdsm(hass: HomeAssistant, service_vdsm: MagicMock): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST @@ -288,7 +288,7 @@ async def test_reauth(hass: HomeAssistant, service: MagicMock): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -298,7 +298,7 @@ async def test_reauth(hass: HomeAssistant, service: MagicMock): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" @@ -323,7 +323,7 @@ async def test_reconfig_user(hass: HomeAssistant, service: MagicMock): context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" @@ -338,7 +338,7 @@ async def test_login_failed(hass: HomeAssistant, service: MagicMock): context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_USERNAME: "invalid_auth"} @@ -354,7 +354,7 @@ async def test_connection_failed(hass: HomeAssistant, service: MagicMock): data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "cannot_connect"} @@ -368,7 +368,7 @@ async def test_unknown_failed(hass: HomeAssistant, service: MagicMock): data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -379,7 +379,7 @@ async def test_missing_data_after_login(hass: HomeAssistant, service_failed: Mag context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "missing_data"} @@ -399,7 +399,7 @@ async def test_form_ssdp(hass: HomeAssistant, service: MagicMock): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "link" assert result["errors"] == {} @@ -407,7 +407,7 @@ async def test_form_ssdp(hass: HomeAssistant, service: MagicMock): result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == "192.168.1.5" assert result["data"][CONF_HOST] == "192.168.1.5" @@ -450,7 +450,7 @@ async def test_reconfig_ssdp(hass: HomeAssistant, service: MagicMock): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" @@ -482,7 +482,7 @@ async def test_skip_reconfig_ssdp(hass: HomeAssistant, service: MagicMock): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -514,7 +514,7 @@ async def test_existing_ssdp(hass: HomeAssistant, service: MagicMock): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -535,7 +535,7 @@ async def test_options_flow(hass: HomeAssistant, service: MagicMock): assert config_entry.options == {} result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" # Scan interval @@ -544,7 +544,7 @@ async def test_options_flow(hass: HomeAssistant, service: MagicMock): result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL assert config_entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT assert config_entry.options[CONF_SNAPSHOT_QUALITY] == DEFAULT_SNAPSHOT_QUALITY @@ -555,7 +555,7 @@ async def test_options_flow(hass: HomeAssistant, service: MagicMock): result["flow_id"], user_input={CONF_SCAN_INTERVAL: 2, CONF_TIMEOUT: 30, CONF_SNAPSHOT_QUALITY: 0}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SCAN_INTERVAL] == 2 assert config_entry.options[CONF_TIMEOUT] == 30 assert config_entry.options[CONF_SNAPSHOT_QUALITY] == 0 diff --git a/tests/components/synology_dsm/test_init.py b/tests/components/synology_dsm/test_init.py index db373f41656..da2916b8c81 100644 --- a/tests/components/synology_dsm/test_init.py +++ b/tests/components/synology_dsm/test_init.py @@ -52,7 +52,7 @@ async def test_reauth_triggered(hass: HomeAssistant): side_effect=SynologyDSMLoginInvalidException(USERNAME), ), patch( "homeassistant.components.synology_dsm.config_flow.SynologyDSMFlowHandler.async_step_reauth", - return_value={"type": data_entry_flow.RESULT_TYPE_FORM}, + return_value={"type": data_entry_flow.FlowResultType.FORM}, ) as mock_async_step_reauth: entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py index 515146bc16c..45131353550 100644 --- a/tests/components/system_bridge/test_config_flow.py +++ b/tests/components/system_bridge/test_config_flow.py @@ -102,7 +102,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -112,7 +112,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch( @@ -129,7 +129,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "test-bridge" assert result2["data"] == FIXTURE_USER_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -141,7 +141,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch( @@ -153,7 +153,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -164,7 +164,7 @@ async def test_form_connection_closed_cannot_connect(hass: HomeAssistant) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -178,7 +178,7 @@ async def test_form_connection_closed_cannot_connect(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -189,7 +189,7 @@ async def test_form_timeout_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -203,7 +203,7 @@ async def test_form_timeout_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -214,7 +214,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -228,7 +228,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -239,7 +239,7 @@ async def test_form_uuid_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -253,7 +253,7 @@ async def test_form_uuid_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -264,7 +264,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -278,7 +278,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -289,7 +289,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -303,7 +303,7 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "invalid_auth"} @@ -314,7 +314,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" with patch( @@ -326,7 +326,7 @@ async def test_reauth_connection_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "cannot_connect"} @@ -337,7 +337,7 @@ async def test_reauth_connection_closed_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -351,7 +351,7 @@ async def test_reauth_connection_closed_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "cannot_connect"} @@ -367,7 +367,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -384,7 +384,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -399,7 +399,7 @@ async def test_zeroconf_flow(hass: HomeAssistant) -> None: data=FIXTURE_ZEROCONF, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert not result["errors"] with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( @@ -416,7 +416,7 @@ async def test_zeroconf_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "1.1.1.1" assert result2["data"] == FIXTURE_ZEROCONF_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -431,7 +431,7 @@ async def test_zeroconf_cannot_connect(hass: HomeAssistant) -> None: data=FIXTURE_ZEROCONF, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert not result["errors"] with patch( @@ -443,7 +443,7 @@ async def test_zeroconf_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "authenticate" assert result2["errors"] == {"base": "cannot_connect"} @@ -457,5 +457,5 @@ async def test_zeroconf_bad_zeroconf_info(hass: HomeAssistant) -> None: data=FIXTURE_ZEROCONF_BAD, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/tautulli/test_config_flow.py b/tests/components/tautulli/test_config_flow.py index d37e4401275..d846f0915d0 100644 --- a/tests/components/tautulli/test_config_flow.py +++ b/tests/components/tautulli/test_config_flow.py @@ -32,7 +32,7 @@ async def test_flow_user_single_instance_allowed(hass: HomeAssistant) -> None: context={"source": SOURCE_USER}, data=CONF_IMPORT_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -41,7 +41,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -52,7 +52,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -64,7 +64,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -75,7 +75,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -87,7 +87,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -98,7 +98,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -110,7 +110,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -121,7 +121,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == NAME assert result2["data"] == CONF_DATA @@ -141,7 +141,7 @@ async def test_flow_reauth( }, data=CONF_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -156,7 +156,7 @@ async def test_flow_reauth( ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == CONF_DATA assert len(mock_entry.mock_calls) == 1 @@ -182,7 +182,7 @@ async def test_flow_reauth_error( result["flow_id"], user_input={CONF_API_KEY: "efgh"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"]["base"] == "invalid_auth" @@ -191,5 +191,5 @@ async def test_flow_reauth_error( result["flow_id"], user_input={CONF_API_KEY: "efgh"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" diff --git a/tests/components/tellduslive/test_config_flow.py b/tests/components/tellduslive/test_config_flow.py index 7417c87c229..ba233d04a78 100644 --- a/tests/components/tellduslive/test_config_flow.py +++ b/tests/components/tellduslive/test_config_flow.py @@ -62,12 +62,12 @@ async def test_abort_if_already_setup(hass): with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_setup" with patch.object(hass.config_entries, "async_entries", return_value=[{}]): result = await flow.async_step_import(None) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_setup" @@ -76,16 +76,16 @@ async def test_full_flow_implementation(hass, mock_tellduslive): flow = init_config_flow(hass) flow.context = {"source": SOURCE_DISCOVERY} result = await flow.async_step_discovery(["localhost", "tellstick"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert len(flow._hosts) == 2 result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user({"host": "localhost"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["description_placeholders"] == { "auth_url": "https://example.com", @@ -93,7 +93,7 @@ async def test_full_flow_implementation(hass, mock_tellduslive): } result = await flow.async_step_auth("") - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "localhost" assert result["data"]["host"] == "localhost" assert result["data"]["scan_interval"] == 60 @@ -105,7 +105,7 @@ async def test_step_import(hass, mock_tellduslive): flow = init_config_flow(hass) result = await flow.async_step_import({CONF_HOST: DOMAIN, KEY_SCAN_INTERVAL: 0}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -116,7 +116,7 @@ async def test_step_import_add_host(hass, mock_tellduslive): result = await flow.async_step_import( {CONF_HOST: "localhost", KEY_SCAN_INTERVAL: 0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -127,7 +127,7 @@ async def test_step_import_no_config_file(hass, mock_tellduslive): result = await flow.async_step_import( {CONF_HOST: "localhost", KEY_SCAN_INTERVAL: 0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -142,7 +142,7 @@ async def test_step_import_load_json_matching_host(hass, mock_tellduslive): result = await flow.async_step_import( {CONF_HOST: "Cloud API", KEY_SCAN_INTERVAL: 0} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -157,7 +157,7 @@ async def test_step_import_load_json(hass, mock_tellduslive): result = await flow.async_step_import( {CONF_HOST: "localhost", KEY_SCAN_INTERVAL: SCAN_INTERVAL} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "localhost" assert result["data"]["host"] == "localhost" assert result["data"]["scan_interval"] == 60 @@ -171,7 +171,7 @@ async def test_step_disco_no_local_api(hass, mock_tellduslive): flow.context = {"source": SOURCE_DISCOVERY} result = await flow.async_step_discovery(["localhost", "tellstick"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert len(flow._hosts) == 1 @@ -182,7 +182,7 @@ async def test_step_auth(hass, mock_tellduslive): await flow.async_step_auth() result = await flow.async_step_auth(["localhost", "tellstick"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Cloud API" assert result["data"]["host"] == "Cloud API" assert result["data"]["scan_interval"] == 60 @@ -199,7 +199,7 @@ async def test_wrong_auth_flow_implementation(hass, mock_tellduslive): await flow.async_step_auth() result = await flow.async_step_auth("") - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"]["base"] == "invalid_auth" @@ -209,7 +209,7 @@ async def test_not_pick_host_if_only_one(hass, mock_tellduslive): flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -218,7 +218,7 @@ async def test_abort_if_timeout_generating_auth_url(hass, mock_tellduslive): flow = init_config_flow(hass, side_effect=asyncio.TimeoutError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -228,7 +228,7 @@ async def test_abort_no_auth_url(hass, mock_tellduslive): flow._get_auth_url = Mock(return_value=False) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown_authorize_url_generation" @@ -237,7 +237,7 @@ async def test_abort_if_exception_generating_auth_url(hass, mock_tellduslive): flow = init_config_flow(hass, side_effect=ValueError) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown_authorize_url_generation" diff --git a/tests/components/tile/test_config_flow.py b/tests/components/tile/test_config_flow.py index 7c623de4ded..9200ed3f382 100644 --- a/tests/components/tile/test_config_flow.py +++ b/tests/components/tile/test_config_flow.py @@ -15,7 +15,7 @@ async def test_duplicate_error(hass, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -35,7 +35,7 @@ async def test_errors(hass, config, err, err_string): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": err_string} @@ -44,7 +44,7 @@ async def test_step_import(hass, config, setup_tile): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "user@host.com" assert result["data"] == { CONF_USERNAME: "user@host.com", @@ -60,13 +60,13 @@ async def test_step_reauth(hass, config, config_entry, setup_tile): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -76,13 +76,13 @@ async def test_step_user(hass, config, setup_tile): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "user@host.com" assert result["data"] == { CONF_USERNAME: "user@host.com", diff --git a/tests/components/tomorrowio/test_config_flow.py b/tests/components/tomorrowio/test_config_flow.py index ca888210a46..77af05cdc7d 100644 --- a/tests/components/tomorrowio/test_config_flow.py +++ b/tests/components/tomorrowio/test_config_flow.py @@ -44,7 +44,7 @@ async def test_user_flow_minimum_fields(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -52,7 +52,7 @@ async def test_user_flow_minimum_fields(hass: HomeAssistant) -> None: user_input=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"][CONF_NAME] == DEFAULT_NAME assert result["data"][CONF_API_KEY] == API_KEY @@ -77,7 +77,7 @@ async def test_user_flow_minimum_fields_in_zone(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -85,7 +85,7 @@ async def test_user_flow_minimum_fields_in_zone(hass: HomeAssistant) -> None: user_input=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == f"{DEFAULT_NAME} - Home" assert result["data"][CONF_NAME] == f"{DEFAULT_NAME} - Home" assert result["data"][CONF_API_KEY] == API_KEY @@ -111,7 +111,7 @@ async def test_user_flow_same_unique_ids(hass: HomeAssistant) -> None: data=user_input, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -127,7 +127,7 @@ async def test_user_flow_cannot_connect(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -143,7 +143,7 @@ async def test_user_flow_invalid_api(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} @@ -159,7 +159,7 @@ async def test_user_flow_rate_limited(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_API_KEY: "rate_limited"} @@ -175,7 +175,7 @@ async def test_user_flow_unknown_exception(hass: HomeAssistant) -> None: data=_get_config_schema(hass, SOURCE_USER, MIN_CONFIG)(MIN_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -199,14 +199,14 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_TIMESTEP: 1} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_TIMESTEP] == 1 assert entry.options[CONF_TIMESTEP] == 1 @@ -256,13 +256,13 @@ async def test_import_flow_v3( data=old_entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_API_KEY: "this is a test"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_API_KEY: "this is a test", CONF_LOCATION: { diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index 826df81066b..3d7a0613269 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -37,7 +37,7 @@ async def test_abort_if_no_configuration(hass): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_configuration" @@ -51,7 +51,7 @@ async def test_full_flow_implementation( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" # pylint: disable=protected-access @@ -67,7 +67,7 @@ async def test_full_flow_implementation( result["flow_id"], {"implementation": "eneco"} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result2["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result2["url"] == ( "https://api.toon.eu/authorize" "?response_type=code&client_id=client" @@ -141,7 +141,7 @@ async def test_no_agreements( with patch("toonapi.Toon.agreements", return_value=[]): result3 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "no_agreements" @@ -185,7 +185,7 @@ async def test_multiple_agreements( ): result3 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["step_id"] == "agreement" result4 = await hass.config_entries.flow.async_configure( @@ -232,7 +232,7 @@ async def test_agreement_already_set_up( with patch("toonapi.Toon.agreements", return_value=[Agreement(agreement_id=123)]): result3 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["type"] == data_entry_flow.FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -271,7 +271,7 @@ async def test_toon_abort( with patch("toonapi.Toon.agreements", side_effect=ToonError): result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "connection_error" @@ -285,7 +285,7 @@ async def test_import(hass, current_request_with_host): DOMAIN, context={"source": SOURCE_IMPORT} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -333,7 +333,7 @@ async def test_import_migration( with patch("toonapi.Toon.agreements", return_value=[Agreement(agreement_id=123)]): result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index 78b121dda77..5e5124db71c 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -38,7 +38,7 @@ async def test_user(hass): data=None, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -65,7 +65,7 @@ async def test_user_show_locations(hass): ) # first it should show the locations form - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "locations" # client should have sent four requests for init assert mock_request.call_count == 4 @@ -75,7 +75,7 @@ async def test_user_show_locations(hass): result["flow_id"], user_input={CONF_USERCODES: "bad"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "locations" # client should have sent 5th request to validate usercode assert mock_request.call_count == 5 @@ -85,7 +85,7 @@ async def test_user_show_locations(hass): result2["flow_id"], user_input={CONF_USERCODES: "7890"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # client should have sent another request to validate usercode assert mock_request.call_count == 6 @@ -106,7 +106,7 @@ async def test_abort_if_already_setup(hass): data=CONFIG_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -122,7 +122,7 @@ async def test_login_failed(hass): data=CONFIG_DATA, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -138,7 +138,7 @@ async def test_reauth(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -152,7 +152,7 @@ async def test_reauth(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -162,7 +162,7 @@ async def test_reauth(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" await hass.async_block_till_done() @@ -190,7 +190,7 @@ async def test_no_locations(hass): context={"source": SOURCE_USER}, data=CONFIG_DATA_NO_USERCODES, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_locations" await hass.async_block_till_done() @@ -221,14 +221,14 @@ async def test_options_flow(hass: HomeAssistant): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={AUTO_BYPASS: True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {AUTO_BYPASS: True} await hass.async_block_till_done() diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index 2d0140db815..830670efc11 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -63,10 +63,10 @@ async def webhook_id_fixture(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() return result["result"].data["webhook_id"] diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 0ac431a7f35..e6de115c1ca 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -35,7 +35,7 @@ async def test_already_paired(hass, mock_entry_setup): result["flow_id"], {"host": "123.123.123.123", "security_code": "abcd"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_authenticate"} @@ -53,7 +53,7 @@ async def test_user_connection_successful(hass, mock_auth, mock_entry_setup): assert len(mock_entry_setup.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].data == { "host": "123.123.123.123", "gateway_id": "bla", @@ -74,7 +74,7 @@ async def test_user_connection_timeout(hass, mock_auth, mock_entry_setup): assert len(mock_entry_setup.mock_calls) == 0 - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "timeout"} @@ -92,7 +92,7 @@ async def test_user_connection_bad_key(hass, mock_auth, mock_entry_setup): assert len(mock_entry_setup.mock_calls) == 0 - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"security_code": "invalid_security_code"} @@ -120,7 +120,7 @@ async def test_discovery_connection(hass, mock_auth, mock_entry_setup): assert len(mock_entry_setup.mock_calls) == 1 - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == "homekit-id" assert result["result"].data == { "host": "123.123.123.123", @@ -149,7 +149,7 @@ async def test_discovery_duplicate_aborted(hass): ), ) - assert flow["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert flow["type"] == data_entry_flow.FlowResultType.ABORT assert flow["reason"] == "already_configured" assert entry.data["host"] == "new-host" @@ -165,7 +165,7 @@ async def test_import_duplicate_aborted(hass): data={"host": "some-host"}, ) - assert flow["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert flow["type"] == data_entry_flow.FlowResultType.ABORT assert flow["reason"] == "already_configured" @@ -185,7 +185,7 @@ async def test_duplicate_discovery(hass, mock_auth, mock_entry_setup): ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_init( "tradfri", @@ -201,7 +201,7 @@ async def test_duplicate_discovery(hass, mock_auth, mock_entry_setup): ), ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT async def test_discovery_updates_unique_id(hass): @@ -226,7 +226,7 @@ async def test_discovery_updates_unique_id(hass): ), ) - assert flow["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert flow["type"] == data_entry_flow.FlowResultType.ABORT assert flow["reason"] == "already_configured" assert entry.unique_id == "homekit-id" diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 7588736e997..24df92f536e 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -89,7 +89,7 @@ async def test_flow_user_config(hass, api): result = await hass.config_entries.flow.async_init( transmission.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -101,7 +101,7 @@ async def test_flow_required_fields(hass, api): data={CONF_NAME: NAME, CONF_HOST: HOST, CONF_PORT: PORT}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -116,7 +116,7 @@ async def test_flow_all_provided(hass, api): data=MOCK_ENTRY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -137,10 +137,10 @@ async def test_options(hass): options_flow = flow.async_get_options_flow(entry) result = await options_flow.async_step_init() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await options_flow.async_step_init({CONF_SCAN_INTERVAL: 10}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_SCAN_INTERVAL] == 10 @@ -171,7 +171,7 @@ async def test_host_already_configured(hass, api): context={"source": config_entries.SOURCE_USER}, data=mock_entry_unique_port, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY mock_entry_unique_host = MOCK_ENTRY.copy() mock_entry_unique_host[CONF_HOST] = "192.168.1.101" @@ -181,7 +181,7 @@ async def test_host_already_configured(hass, api): context={"source": config_entries.SOURCE_USER}, data=mock_entry_unique_host, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_name_already_configured(hass, api): @@ -218,7 +218,7 @@ async def test_error_on_wrong_credentials(hass, auth_error): CONF_PORT: PORT, } ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == { CONF_USERNAME: "invalid_auth", CONF_PASSWORD: "invalid_auth", @@ -238,7 +238,7 @@ async def test_error_on_connection_failure(hass, conn_error): CONF_PORT: PORT, } ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -255,7 +255,7 @@ async def test_error_on_unknown_error(hass, unknown_error): CONF_PORT: PORT, } ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/twilio/test_init.py b/tests/components/twilio/test_init.py index 8490f7541eb..d4fe42f10c7 100644 --- a/tests/components/twilio/test_init.py +++ b/tests/components/twilio/test_init.py @@ -14,10 +14,10 @@ async def test_config_flow_registers_webhook(hass, hass_client_no_auth): result = await hass.config_entries.flow.async_init( "twilio", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + assert result["type"] == data_entry_flow.FlowResultType.FORM, result result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY webhook_id = result["result"].data["webhook_id"] twilio_events = [] diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index e774c5a551d..a1f6f3d4b02 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -95,7 +95,7 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery): UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["data_schema"]({CONF_USERNAME: "", CONF_PASSWORD: ""}) == { CONF_HOST: "unifi", @@ -135,7 +135,7 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Site name" assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -161,7 +161,7 @@ async def test_flow_works_negative_discovery(hass, aioclient_mock, mock_discover UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["data_schema"]({CONF_USERNAME: "", CONF_PASSWORD: ""}) == { CONF_HOST: "", @@ -178,7 +178,7 @@ async def test_flow_multiple_sites(hass, aioclient_mock): UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -212,7 +212,7 @@ async def test_flow_multiple_sites(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "site" assert result["data_schema"]({"site": "1"}) assert result["data_schema"]({"site": "2"}) @@ -226,7 +226,7 @@ async def test_flow_raise_already_configured(hass, aioclient_mock): UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.clear_requests() @@ -261,7 +261,7 @@ async def test_flow_raise_already_configured(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -281,7 +281,7 @@ async def test_flow_aborts_configuration_updated(hass, aioclient_mock): UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -315,7 +315,7 @@ async def test_flow_aborts_configuration_updated(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "configuration_updated" @@ -325,7 +325,7 @@ async def test_flow_fails_user_credentials_faulty(hass, aioclient_mock): UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -342,7 +342,7 @@ async def test_flow_fails_user_credentials_faulty(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "faulty_credentials"} @@ -352,7 +352,7 @@ async def test_flow_fails_controller_unavailable(hass, aioclient_mock): UNIFI_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" aioclient_mock.get("https://1.2.3.4:1234", status=302) @@ -369,7 +369,7 @@ async def test_flow_fails_controller_unavailable(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "service_unavailable"} @@ -389,7 +389,7 @@ async def test_reauth_flow_update_configuration(hass, aioclient_mock): data=config_entry.data, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == SOURCE_USER aioclient_mock.clear_requests() @@ -424,7 +424,7 @@ async def test_reauth_flow_update_configuration(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert config_entry.data[CONF_HOST] == "1.2.3.4" assert config_entry.data[CONF_USERNAME] == "new_name" @@ -447,7 +447,7 @@ async def test_advanced_option_flow(hass, aioclient_mock): config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "device_tracker" assert not result["last_step"] assert set( @@ -465,7 +465,7 @@ async def test_advanced_option_flow(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "client_control" assert not result["last_step"] @@ -478,7 +478,7 @@ async def test_advanced_option_flow(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "statistics_sensors" assert result["last_step"] @@ -490,7 +490,7 @@ async def test_advanced_option_flow(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_TRACK_CLIENTS: False, CONF_TRACK_WIRED_CLIENTS: False, @@ -521,7 +521,7 @@ async def test_simple_option_flow(hass, aioclient_mock): config_entry.entry_id, context={"show_advanced_options": False} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "simple_options" assert result["last_step"] @@ -534,7 +534,7 @@ async def test_simple_option_flow(hass, aioclient_mock): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_TRACK_CLIENTS: False, CONF_TRACK_DEVICES: False, diff --git a/tests/components/upcloud/test_config_flow.py b/tests/components/upcloud/test_config_flow.py index 7fce853b5d3..4bbc7c51b9d 100644 --- a/tests/components/upcloud/test_config_flow.py +++ b/tests/components/upcloud/test_config_flow.py @@ -26,7 +26,7 @@ async def test_show_set_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -37,7 +37,7 @@ async def test_connection_error(hass, requests_mock): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -56,7 +56,7 @@ async def test_login_error(hass, requests_mock): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -68,7 +68,7 @@ async def test_success(hass, requests_mock): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=FIXTURE_USER_INPUT ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] @@ -82,7 +82,7 @@ async def test_options(hass): config_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index 66d84fe0862..e89b8274c18 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -45,7 +45,7 @@ async def test_flow_ssdp(hass: HomeAssistant): context={"source": config_entries.SOURCE_SSDP}, data=TEST_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" # Confirm via step ssdp_confirm. @@ -53,7 +53,7 @@ async def test_flow_ssdp(hass: HomeAssistant): result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEST_FRIENDLY_NAME assert result["data"] == { CONFIG_ENTRY_ST: TEST_ST, @@ -81,7 +81,7 @@ async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistant): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "incomplete_discovery" @@ -102,7 +102,7 @@ async def test_flow_ssdp_non_igd_device(hass: HomeAssistant): }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "non_igd_device" @@ -120,7 +120,7 @@ async def test_flow_ssdp_no_mac_address(hass: HomeAssistant): context={"source": config_entries.SOURCE_SSDP}, data=TEST_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" # Confirm via step ssdp_confirm. @@ -128,7 +128,7 @@ async def test_flow_ssdp_no_mac_address(hass: HomeAssistant): result["flow_id"], user_input={}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEST_FRIENDLY_NAME assert result["data"] == { CONFIG_ENTRY_ST: TEST_ST, @@ -167,7 +167,7 @@ async def test_flow_ssdp_discovery_changed_udn(hass: HomeAssistant): context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "config_entry_updated" @@ -207,7 +207,7 @@ async def test_flow_ssdp_discovery_changed_udn_but_st_differs(hass: HomeAssistan context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" # UDN + ST different: New discovery via step ssdp. @@ -225,7 +225,7 @@ async def test_flow_ssdp_discovery_changed_udn_but_st_differs(hass: HomeAssistan context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" @@ -256,7 +256,7 @@ async def test_flow_ssdp_discovery_changed_location(hass: HomeAssistant): context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" # Test if location is updated. @@ -285,7 +285,7 @@ async def test_flow_ssdp_discovery_ignored_entry(hass: HomeAssistant): context={"source": config_entries.SOURCE_SSDP}, data=TEST_DISCOVERY, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -316,7 +316,7 @@ async def test_flow_ssdp_discovery_changed_udn_ignored_entry(hass: HomeAssistant context={"source": config_entries.SOURCE_SSDP}, data=new_discovery, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "discovery_ignored" @@ -332,7 +332,7 @@ async def test_flow_user(hass: HomeAssistant): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Confirmed via step user. @@ -340,7 +340,7 @@ async def test_flow_user(hass: HomeAssistant): result["flow_id"], user_input={"unique_id": TEST_USN}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEST_FRIENDLY_NAME assert result["data"] == { CONFIG_ENTRY_ST: TEST_ST, @@ -362,5 +362,5 @@ async def test_flow_user_no_discovery(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 960eedcbd01..207f745e495 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -66,20 +66,20 @@ async def test_user(hass: HomeAssistant): flow = init_config_flow(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await flow.async_step_user( {CONF_NAME: "Velbus Test Serial", CONF_PORT: PORT_SERIAL} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "velbus_test_serial" assert result["data"][CONF_PORT] == PORT_SERIAL result = await flow.async_step_user( {CONF_NAME: "Velbus Test TCP", CONF_PORT: PORT_TCP} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "velbus_test_tcp" assert result["data"][CONF_PORT] == PORT_TCP @@ -92,13 +92,13 @@ async def test_user_fail(hass: HomeAssistant): result = await flow.async_step_user( {CONF_NAME: "Velbus Test Serial", CONF_PORT: PORT_SERIAL} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_PORT: "cannot_connect"} result = await flow.async_step_user( {CONF_NAME: "Velbus Test TCP", CONF_PORT: PORT_TCP} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_PORT: "cannot_connect"} @@ -108,7 +108,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant): flow = init_config_flow(hass) result = await flow.async_step_user({CONF_PORT: PORT_TCP, CONF_NAME: "velbus test"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"port": "already_configured"} @@ -121,14 +121,14 @@ async def test_flow_usb(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "discovery_confirm" 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["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # test an already configured discovery entry = MockConfigEntry( @@ -141,7 +141,7 @@ async def test_flow_usb(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -154,5 +154,5 @@ async def test_flow_usb_failed(hass: HomeAssistant): context={CONF_SOURCE: SOURCE_USB}, data=DISCOVERY_INFO, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index 780583e38ab..8448bddb288 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -138,7 +138,7 @@ async def test_options(hass): result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -148,7 +148,7 @@ async def test_options(hass): CONF_EXCLUDE: "8,9;10 11 12_13bb14", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_LIGHTS: [1, 2, 3, 4, 5, 6, 7], CONF_EXCLUDE: [8, 9, 10, 11, 12, 13, 14], diff --git a/tests/components/vesync/test_config_flow.py b/tests/components/vesync/test_config_flow.py index cd68a0b5877..a1f9914ed67 100644 --- a/tests/components/vesync/test_config_flow.py +++ b/tests/components/vesync/test_config_flow.py @@ -17,7 +17,7 @@ async def test_abort_already_setup(hass): ) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -29,7 +29,7 @@ async def test_invalid_login_error(hass): with patch("pyvesync.vesync.VeSync.login", return_value=False): result = await flow.async_step_user(user_input=test_dict) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -38,11 +38,11 @@ async def test_config_flow_user_input(hass): flow = config_flow.VeSyncFlowHandler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch("pyvesync.vesync.VeSync.login", return_value=True): result = await flow.async_step_user( {CONF_USERNAME: "user", CONF_PASSWORD: "pass"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"][CONF_USERNAME] == "user" assert result["data"][CONF_PASSWORD] == "pass" diff --git a/tests/components/vicare/test_config_flow.py b/tests/components/vicare/test_config_flow.py index 3096f8a492c..d2852b33606 100644 --- a/tests/components/vicare/test_config_flow.py +++ b/tests/components/vicare/test_config_flow.py @@ -18,7 +18,7 @@ async def test_form(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert len(result["errors"]) == 0 with patch( @@ -38,7 +38,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "ViCare" assert result2["data"] == ENTRY_CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -64,7 +64,7 @@ async def test_invalid_login(hass) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -81,7 +81,7 @@ async def test_form_dhcp(hass): macaddress=MOCK_MAC, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -102,7 +102,7 @@ async def test_form_dhcp(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "ViCare" assert result2["data"] == ENTRY_CONFIG assert len(mock_setup_entry.mock_calls) == 1 @@ -125,7 +125,7 @@ async def test_dhcp_single_instance_allowed(hass): macaddress=MOCK_MAC, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -141,5 +141,5 @@ async def test_user_input_single_instance_allowed(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/vilfo/test_config_flow.py b/tests/components/vilfo/test_config_flow.py index 571399949b4..308431782f3 100644 --- a/tests/components/vilfo/test_config_flow.py +++ b/tests/components/vilfo/test_config_flow.py @@ -15,7 +15,7 @@ async def test_form(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} with patch("vilfo.Client.ping", return_value=None), patch( @@ -29,7 +29,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == "testadmin.vilfo.com" assert result2["data"] == { "host": "testadmin.vilfo.com", @@ -56,7 +56,7 @@ async def test_form_invalid_auth(hass): {"host": "testadmin.vilfo.com", "access_token": "test-token"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -74,7 +74,7 @@ async def test_form_cannot_connect(hass): {"host": "testadmin.vilfo.com", "access_token": "test-token"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} with patch("vilfo.Client.ping", side_effect=vilfo.exceptions.VilfoException), patch( @@ -85,7 +85,7 @@ async def test_form_cannot_connect(hass): {"host": "testadmin.vilfo.com", "access_token": "test-token"}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["type"] == data_entry_flow.FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} @@ -128,8 +128,8 @@ async def test_form_already_configured(hass): {CONF_HOST: "testadmin.vilfo.com", CONF_ACCESS_TOKEN: "test-token"}, ) - assert first_flow_result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert second_flow_result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert first_flow_result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert second_flow_result2["type"] == data_entry_flow.FlowResultType.ABORT assert second_flow_result2["reason"] == "already_configured" diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index 3250163ef8e..dd338be1321 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -66,14 +66,14 @@ async def test_user_flow_minimum_fields( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_SPEAKER_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -91,14 +91,14 @@ async def test_user_flow_all_fields( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_VALID_TV_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -117,19 +117,19 @@ async def test_speaker_options_flow( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_SPEAKER_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_VOLUME_STEP: VOLUME_STEP} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS not in result["data"] @@ -145,12 +145,12 @@ async def test_tv_options_flow_no_apps( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" options = {CONF_VOLUME_STEP: VOLUME_STEP} @@ -160,7 +160,7 @@ async def test_tv_options_flow_no_apps( result["flow_id"], user_input=options ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS not in result["data"] @@ -176,12 +176,12 @@ async def test_tv_options_flow_with_apps( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" options = {CONF_VOLUME_STEP: VOLUME_STEP} @@ -191,7 +191,7 @@ async def test_tv_options_flow_with_apps( result["flow_id"], user_input=options ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS in result["data"] @@ -208,13 +208,13 @@ async def test_tv_options_flow_start_with_volume( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = result["result"] result = await hass.config_entries.options.async_init( entry.entry_id, data={CONF_VOLUME_STEP: VOLUME_STEP} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert entry.options assert entry.options == {CONF_VOLUME_STEP: VOLUME_STEP} @@ -223,7 +223,7 @@ async def test_tv_options_flow_start_with_volume( result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" options = {CONF_VOLUME_STEP: VOLUME_STEP} @@ -233,7 +233,7 @@ async def test_tv_options_flow_start_with_volume( result["flow_id"], user_input=options ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP assert CONF_APPS in result["data"] @@ -260,7 +260,7 @@ async def test_user_host_already_configured( DOMAIN, context={"source": SOURCE_USER}, data=fail_entry ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "existing_config_entry_found"} @@ -284,7 +284,7 @@ async def test_user_serial_number_already_exists( DOMAIN, context={"source": SOURCE_USER}, data=fail_entry ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "existing_config_entry_found"} @@ -296,7 +296,7 @@ async def test_user_error_on_could_not_connect( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {CONF_HOST: "cannot_connect"} @@ -308,7 +308,7 @@ async def test_user_error_on_could_not_connect_invalid_token( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_VALID_TV_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -323,19 +323,19 @@ async def test_user_tv_pairing_no_apps( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pairing_complete" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -354,7 +354,7 @@ async def test_user_start_pairing_failure( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -370,14 +370,14 @@ async def test_user_invalid_pin( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pair_tv" assert result["errors"] == {CONF_PIN: "complete_pairing_failed"} @@ -399,7 +399,7 @@ async def test_user_ignore( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_SPEAKER_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_import_flow_minimum_fields( @@ -416,7 +416,7 @@ async def test_import_flow_minimum_fields( ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"][CONF_NAME] == DEFAULT_NAME assert result["data"][CONF_HOST] == HOST @@ -436,7 +436,7 @@ async def test_import_flow_all_fields( data=vol.Schema(VIZIO_SCHEMA)(MOCK_IMPORT_VALID_TV_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -463,7 +463,7 @@ async def test_import_entity_already_configured( DOMAIN, context={"source": SOURCE_IMPORT}, data=fail_entry ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured_device" @@ -481,7 +481,7 @@ async def test_import_flow_update_options( await hass.async_block_till_done() assert result["result"].options == {CONF_VOLUME_STEP: DEFAULT_VOLUME_STEP} - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry_id = result["result"].entry_id updated_config = MOCK_SPEAKER_CONFIG.copy() @@ -492,7 +492,7 @@ async def test_import_flow_update_options( data=vol.Schema(VIZIO_SCHEMA)(updated_config), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "updated_entry" config_entry = hass.config_entries.async_get_entry(entry_id) assert config_entry.options[CONF_VOLUME_STEP] == VOLUME_STEP + 1 @@ -512,7 +512,7 @@ async def test_import_flow_update_name_and_apps( await hass.async_block_till_done() assert result["result"].data[CONF_NAME] == NAME - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry_id = result["result"].entry_id updated_config = MOCK_IMPORT_VALID_TV_CONFIG.copy() @@ -524,7 +524,7 @@ async def test_import_flow_update_name_and_apps( data=vol.Schema(VIZIO_SCHEMA)(updated_config), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "updated_entry" config_entry = hass.config_entries.async_get_entry(entry_id) assert config_entry.data[CONF_NAME] == NAME2 @@ -546,7 +546,7 @@ async def test_import_flow_update_remove_apps( await hass.async_block_till_done() assert result["result"].data[CONF_NAME] == NAME - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY config_entry = hass.config_entries.async_get_entry(result["result"].entry_id) assert CONF_APPS in config_entry.data assert CONF_APPS in config_entry.options @@ -559,7 +559,7 @@ async def test_import_flow_update_remove_apps( data=vol.Schema(VIZIO_SCHEMA)(updated_config), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "updated_entry" assert CONF_APPS not in config_entry.data assert CONF_APPS not in config_entry.options @@ -576,26 +576,26 @@ async def test_import_needs_pairing( DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_TV_CONFIG_NO_TOKEN ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pairing_complete_import" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -616,7 +616,7 @@ async def test_import_with_apps_needs_pairing( DOMAIN, context={"source": SOURCE_IMPORT}, data=import_config ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Mock inputting info without apps to make sure apps get stored @@ -625,19 +625,19 @@ async def test_import_with_apps_needs_pairing( user_input=_get_config_schema(MOCK_TV_CONFIG_NO_TOKEN)(MOCK_TV_CONFIG_NO_TOKEN), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pair_tv" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_PIN_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pairing_complete_import" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST @@ -659,7 +659,7 @@ async def test_import_flow_additional_configs( await hass.async_block_till_done() assert result["result"].data[CONF_NAME] == NAME - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY config_entry = hass.config_entries.async_get_entry(result["result"].entry_id) assert CONF_APPS in config_entry.data assert CONF_APPS not in config_entry.options @@ -688,7 +688,7 @@ async def test_import_error( data=vol.Schema(VIZIO_SCHEMA)(fail_entry), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Ensure error gets logged vizio_log_list = [ @@ -719,7 +719,7 @@ async def test_import_ignore( data=vol.Schema(VIZIO_SCHEMA)(MOCK_SPEAKER_CONFIG), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_zeroconf_flow( @@ -735,7 +735,7 @@ async def test_zeroconf_flow( ) # Form should always show even if all required properties are discovered - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" # Apply discovery updates to entry to mimic when user hits submit without changing @@ -752,7 +752,7 @@ async def test_zeroconf_flow( result["flow_id"], user_input=user_input ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_NAME] == NAME @@ -781,7 +781,7 @@ async def test_zeroconf_flow_already_configured( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -808,7 +808,7 @@ async def test_zeroconf_flow_with_port_in_host( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -825,7 +825,7 @@ async def test_zeroconf_dupe_fail( ) # Form should always show even if all required properties are discovered - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" discovery_info = dataclasses.replace(MOCK_ZEROCONF_SERVICE_INFO) @@ -834,7 +834,7 @@ async def test_zeroconf_dupe_fail( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -858,7 +858,7 @@ async def test_zeroconf_ignore( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_zeroconf_no_unique_id( @@ -873,7 +873,7 @@ async def test_zeroconf_no_unique_id( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -898,7 +898,7 @@ async def test_zeroconf_abort_when_ignored( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -927,7 +927,7 @@ async def test_zeroconf_flow_already_configured_hostname( ) # Flow should abort because device is already setup - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -952,7 +952,7 @@ async def test_import_flow_already_configured_hostname( ) # Flow should abort because device was updated - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "updated_entry" assert entry.data[CONF_HOST] == HOST diff --git a/tests/components/vulcan/test_config_flow.py b/tests/components/vulcan/test_config_flow.py index 20f030bb99f..c45ab430c2e 100644 --- a/tests/components/vulcan/test_config_flow.py +++ b/tests/components/vulcan/test_config_flow.py @@ -36,7 +36,7 @@ async def test_show_form(hass): result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" @@ -56,7 +56,7 @@ async def test_config_flow_auth_success( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -70,7 +70,7 @@ async def test_config_flow_auth_success( ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 1 @@ -93,7 +93,7 @@ async def test_config_flow_auth_success_with_multiple_students( const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -102,7 +102,7 @@ async def test_config_flow_auth_success_with_multiple_students( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_student" assert result["errors"] == {} @@ -115,7 +115,7 @@ async def test_config_flow_auth_success_with_multiple_students( {"student": "0"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 1 @@ -141,7 +141,7 @@ async def test_config_flow_reauth_success( const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -154,7 +154,7 @@ async def test_config_flow_reauth_success( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 @@ -180,7 +180,7 @@ async def test_config_flow_reauth_without_matching_entries( const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} @@ -189,7 +189,7 @@ async def test_config_flow_reauth_without_matching_entries( {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_matching_entries" @@ -202,7 +202,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) result = await hass.config_entries.flow.async_init( const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {} with patch( @@ -215,7 +215,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_token"} @@ -229,7 +229,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "expired_token"} @@ -243,7 +243,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_pin"} @@ -257,7 +257,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "invalid_symbol"} @@ -271,7 +271,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "cannot_connect"} @@ -285,7 +285,7 @@ async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass) {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {"base": "unknown"} @@ -310,7 +310,7 @@ async def test_multiple_config_entries(mock_account, mock_keystore, mock_student const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -319,7 +319,7 @@ async def test_multiple_config_entries(mock_account, mock_keystore, mock_student {"use_saved_credentials": False}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -332,7 +332,7 @@ async def test_multiple_config_entries(mock_account, mock_keystore, mock_student {CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 2 @@ -353,7 +353,7 @@ async def test_multiple_config_entries_using_saved_credentials(mock_student, has const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -366,7 +366,7 @@ async def test_multiple_config_entries_using_saved_credentials(mock_student, has {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 2 @@ -387,7 +387,7 @@ async def test_multiple_config_entries_using_saved_credentials_2(mock_student, h const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -396,7 +396,7 @@ async def test_multiple_config_entries_using_saved_credentials_2(mock_student, h {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_student" assert result["errors"] == {} @@ -409,7 +409,7 @@ async def test_multiple_config_entries_using_saved_credentials_2(mock_student, h {"student": "0"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 2 @@ -438,7 +438,7 @@ async def test_multiple_config_entries_using_saved_credentials_3(mock_student, h const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -447,7 +447,7 @@ async def test_multiple_config_entries_using_saved_credentials_3(mock_student, h {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -460,7 +460,7 @@ async def test_multiple_config_entries_using_saved_credentials_3(mock_student, h {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 3 @@ -489,7 +489,7 @@ async def test_multiple_config_entries_using_saved_credentials_4(mock_student, h const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -498,7 +498,7 @@ async def test_multiple_config_entries_using_saved_credentials_4(mock_student, h {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -507,7 +507,7 @@ async def test_multiple_config_entries_using_saved_credentials_4(mock_student, h {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_student" assert result["errors"] == {} @@ -520,7 +520,7 @@ async def test_multiple_config_entries_using_saved_credentials_4(mock_student, h {"student": "0"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Jan Kowalski" assert len(mock_setup_entry.mock_calls) == 3 @@ -545,7 +545,7 @@ async def test_multiple_config_entries_without_valid_saved_credentials(hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -557,7 +557,7 @@ async def test_multiple_config_entries_without_valid_saved_credentials(hass): "homeassistant.components.vulcan.config_flow.Vulcan.get_students", side_effect=UnauthorizedCertificateException, ): - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -566,7 +566,7 @@ async def test_multiple_config_entries_without_valid_saved_credentials(hass): {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "expired_credentials"} @@ -593,7 +593,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_connections_ const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -605,7 +605,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_connections_ "homeassistant.components.vulcan.config_flow.Vulcan.get_students", side_effect=ClientConnectionError, ): - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -614,7 +614,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_connections_ {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] == {"base": "cannot_connect"} @@ -639,7 +639,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_unknown_erro const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -651,7 +651,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_unknown_erro "homeassistant.components.vulcan.config_flow.Vulcan.get_students", side_effect=Exception, ): - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "select_saved_credentials" assert result["errors"] is None @@ -660,7 +660,7 @@ async def test_multiple_config_entries_using_saved_credentials_with_unknown_erro {"credentials": "123"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "unknown"} @@ -688,7 +688,7 @@ async def test_student_already_exists(mock_account, mock_keystore, mock_student, const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "add_next_config_entry" assert result["errors"] == {} @@ -697,7 +697,7 @@ async def test_student_already_exists(mock_account, mock_keystore, mock_student, {"use_saved_credentials": True}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "all_student_already_configured" @@ -713,7 +713,7 @@ async def test_config_flow_auth_invalid_token(mock_keystore, hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -722,7 +722,7 @@ async def test_config_flow_auth_invalid_token(mock_keystore, hass): {CONF_TOKEN: "3S20000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "invalid_token"} @@ -739,7 +739,7 @@ async def test_config_flow_auth_invalid_region(mock_keystore, hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -748,7 +748,7 @@ async def test_config_flow_auth_invalid_region(mock_keystore, hass): {CONF_TOKEN: "3S10000", CONF_REGION: "invalid_region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "invalid_symbol"} @@ -765,7 +765,7 @@ async def test_config_flow_auth_invalid_pin(mock_keystore, hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -774,7 +774,7 @@ async def test_config_flow_auth_invalid_pin(mock_keystore, hass): {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "invalid_pin"} @@ -791,7 +791,7 @@ async def test_config_flow_auth_expired_token(mock_keystore, hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -800,7 +800,7 @@ async def test_config_flow_auth_expired_token(mock_keystore, hass): {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "expired_token"} @@ -817,7 +817,7 @@ async def test_config_flow_auth_connection_error(mock_keystore, hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -826,7 +826,7 @@ async def test_config_flow_auth_connection_error(mock_keystore, hass): {CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "cannot_connect"} @@ -843,7 +843,7 @@ async def test_config_flow_auth_unknown_error(mock_keystore, hass): const.DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] is None @@ -852,6 +852,6 @@ async def test_config_flow_auth_unknown_error(mock_keystore, hass): {CONF_TOKEN: "3S10000", CONF_REGION: "invalid_region", CONF_PIN: "000000"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "auth" assert result["errors"] == {"base": "unknown"} diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py index 68f70878592..c9254f77aac 100644 --- a/tests/components/wallbox/test_config_flow.py +++ b/tests/components/wallbox/test_config_flow.py @@ -45,7 +45,7 @@ async def test_show_set_form(hass: HomeAssistant) -> None: flow.hass = hass result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" diff --git a/tests/components/watttime/test_config_flow.py b/tests/components/watttime/test_config_flow.py index 514376d58a5..8027759738f 100644 --- a/tests/components/watttime/test_config_flow.py +++ b/tests/components/watttime/test_config_flow.py @@ -106,13 +106,13 @@ async def test_options_flow(hass: HomeAssistant, config_entry): ): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.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 result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_SHOW_ON_MAP: False} diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py index c4414cdbefd..c4b8144b74d 100644 --- a/tests/components/waze_travel_time/test_config_flow.py +++ b/tests/components/waze_travel_time/test_config_flow.py @@ -29,7 +29,7 @@ async def test_minimum_fields(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -38,7 +38,7 @@ async def test_minimum_fields(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == DEFAULT_NAME assert result2["data"] == { CONF_NAME: DEFAULT_NAME, @@ -60,7 +60,7 @@ async def test_options(hass): result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -76,7 +76,7 @@ async def test_options(hass): CONF_VEHICLE_TYPE: "taxi", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"] == { CONF_AVOID_FERRIES: True, @@ -122,7 +122,7 @@ async def test_import(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() entry = hass.config_entries.async_entries(DOMAIN)[0] assert entry.data == { @@ -148,7 +148,7 @@ async def test_dupe(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -157,13 +157,13 @@ async def test_dupe(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( @@ -172,7 +172,7 @@ async def test_dupe(hass): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @pytest.mark.usefixtures("invalidate_config_entry") @@ -181,12 +181,12 @@ async def test_invalid_config_entry(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/wemo/test_config_flow.py b/tests/components/wemo/test_config_flow.py index 81040e44c9c..0ffcb9d7f5e 100644 --- a/tests/components/wemo/test_config_flow.py +++ b/tests/components/wemo/test_config_flow.py @@ -18,5 +18,5 @@ async def test_not_discovered(hass: HomeAssistant) -> None: with patch("homeassistant.components.wemo.config_flow.pywemo") as mock_pywemo: mock_pywemo.discover_devices.return_value = [] result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/wiffi/test_config_flow.py b/tests/components/wiffi/test_config_flow.py index 50433d377a9..655642dffc1 100644 --- a/tests/components/wiffi/test_config_flow.py +++ b/tests/components/wiffi/test_config_flow.py @@ -125,13 +125,13 @@ async def test_option_flow(hass): result = await hass.config_entries.options.async_init(entry.entry_id, data=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_TIMEOUT: 9} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_TIMEOUT] == 9 diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index b90a004ed0b..66e7d6b7055 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -199,7 +199,7 @@ class ComponentFactory: "redirect_uri": "http://127.0.0.1:8080/auth/external/callback", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( "https://account.withings.com/oauth2_user/authorize2?" f"response_type=code&client_id={self._client_id}&" diff --git a/tests/components/wolflink/test_config_flow.py b/tests/components/wolflink/test_config_flow.py index f0530524805..9a6105a4b8b 100644 --- a/tests/components/wolflink/test_config_flow.py +++ b/tests/components/wolflink/test_config_flow.py @@ -38,7 +38,7 @@ async def test_show_form(hass): 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["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -52,7 +52,7 @@ async def test_device_step_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER}, data=INPUT_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "device" @@ -71,7 +71,7 @@ async def test_create_entry(hass): {"device_name": CONFIG[DEVICE_NAME]}, ) - assert result_create_entry["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result_create_entry["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result_create_entry["title"] == CONFIG[DEVICE_NAME] assert result_create_entry["data"] == CONFIG @@ -138,5 +138,5 @@ async def test_already_configured_error(hass): {"device_name": CONFIG[DEVICE_NAME]}, ) - assert result_create_entry["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result_create_entry["type"] == data_entry_flow.FlowResultType.ABORT assert result_create_entry["reason"] == "already_configured" diff --git a/tests/components/ws66i/test_config_flow.py b/tests/components/ws66i/test_config_flow.py index 4fe3554941d..da5a16882e8 100644 --- a/tests/components/ws66i/test_config_flow.py +++ b/tests/components/ws66i/test_config_flow.py @@ -126,7 +126,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -141,7 +141,7 @@ async def test_options_flow(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_SOURCES] == { "1": "one", "2": "too", diff --git a/tests/components/xbox/test_config_flow.py b/tests/components/xbox/test_config_flow.py index f8c296dcbbe..2ce497bcbc1 100644 --- a/tests/components/xbox/test_config_flow.py +++ b/tests/components/xbox/test_config_flow.py @@ -19,7 +19,7 @@ async def test_abort_if_existing_entry(hass): result = await hass.config_entries.flow.async_init( "xbox", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 2aa27d703a1..e47a1a1ace5 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -872,7 +872,7 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -882,7 +882,7 @@ async def test_options_flow(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { const.CONF_CLOUD_SUBDEVICES: True, } @@ -912,7 +912,7 @@ async def test_options_flow_incomplete(hass): result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -921,7 +921,7 @@ async def test_options_flow_incomplete(hass): }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "cloud_credentials_incomplete"} diff --git a/tests/components/yamaha_musiccast/test_config_flow.py b/tests/components/yamaha_musiccast/test_config_flow.py index fc9a740d4a6..a3fb0cf6211 100644 --- a/tests/components/yamaha_musiccast/test_config_flow.py +++ b/tests/components/yamaha_musiccast/test_config_flow.py @@ -128,13 +128,13 @@ async def test_user_input_device_not_found( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "none"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -146,13 +146,13 @@ async def test_user_input_non_yamaha_device_found( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "no_musiccast_device"} @@ -176,7 +176,7 @@ async def test_user_input_device_already_existing( {"host": "192.168.188.18"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -188,13 +188,13 @@ async def test_user_input_unknown_error( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -209,13 +209,13 @@ async def test_user_input_device_found( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert isinstance(result2["result"], ConfigEntry) assert result2["data"] == { "host": "127.0.0.1", @@ -235,13 +235,13 @@ async def test_user_input_device_found_no_ssdp( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "127.0.0.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert isinstance(result2["result"], ConfigEntry) assert result2["data"] == { "host": "127.0.0.1", @@ -269,7 +269,7 @@ async def test_ssdp_discovery_failed(hass, mock_ssdp_no_yamaha, mock_get_source_ ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "yxc_control_url_missing" @@ -291,7 +291,7 @@ async def test_ssdp_discovery_successful_add_device( ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "confirm" @@ -300,7 +300,7 @@ async def test_ssdp_discovery_successful_add_device( {}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert isinstance(result2["result"], ConfigEntry) assert result2["data"] == { "host": "127.0.0.1", @@ -332,7 +332,7 @@ async def test_ssdp_discovery_existing_device_update( }, ), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_entry.data[CONF_HOST] == "127.0.0.1" assert mock_entry.data["upnp_description"] == "http://127.0.0.1/desc.xml" diff --git a/tests/components/yolink/test_config_flow.py b/tests/components/yolink/test_config_flow.py index e224bc3e1d2..f809596e816 100644 --- a/tests/components/yolink/test_config_flow.py +++ b/tests/components/yolink/test_config_flow.py @@ -22,7 +22,7 @@ async def test_abort_if_no_configuration(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_credentials" @@ -32,7 +32,7 @@ async def test_abort_if_existing_entry(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -60,7 +60,7 @@ async def test_full_flow( "redirect_uri": "https://example.com/auth/external/callback", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -126,7 +126,7 @@ async def test_abort_if_authorization_timeout(hass, current_request_with_host): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -203,6 +203,6 @@ async def test_reauthentication( assert token_data["refresh_token"] == "mock-refresh-token" assert token_data["type"] == "Bearer" assert token_data["expires_in"] == 60 - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup.mock_calls) == 1 diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 979aa8bf088..dcb0ee7abfd 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -51,7 +51,7 @@ async def test_single_entry_allowed(hass, discovery_flow_conf): MockConfigEntry(domain="test").add_to_hass(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -62,7 +62,7 @@ async def test_user_no_devices_found(hass, discovery_flow_conf): flow.context = {"source": config_entries.SOURCE_USER} result = await flow.async_step_confirm(user_input={}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -75,7 +75,7 @@ async def test_user_has_confirmation(hass, discovery_flow_conf): "test", context={"source": config_entries.SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" progress = hass.config_entries.flow.async_progress() @@ -88,7 +88,7 @@ async def test_user_has_confirmation(hass, discovery_flow_conf): } result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -110,7 +110,7 @@ async def test_discovery_single_instance(hass, discovery_flow_conf, source): MockConfigEntry(domain="test").add_to_hass(hass) result = await getattr(flow, f"async_step_{source}")({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -132,11 +132,11 @@ async def test_discovery_confirmation(hass, discovery_flow_conf, source): result = await getattr(flow, f"async_step_{source}")({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" result = await flow.async_step_confirm({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY @pytest.mark.parametrize( @@ -160,7 +160,7 @@ async def test_discovery_during_onboarding(hass, discovery_flow_conf, source): ): result = await getattr(flow, f"async_step_{source}")({}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_multiple_discoveries(hass, discovery_flow_conf): @@ -170,13 +170,13 @@ async def test_multiple_discoveries(hass, discovery_flow_conf): result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Second discovery result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_only_one_in_progress(hass, discovery_flow_conf): @@ -187,21 +187,21 @@ async def test_only_one_in_progress(hass, discovery_flow_conf): result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # User starts flow result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_USER}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Discovery flow has not been aborted assert len(hass.config_entries.flow.async_progress()) == 2 # Discovery should be aborted once user confirms result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(hass.config_entries.flow.async_progress()) == 0 @@ -213,14 +213,14 @@ async def test_import_abort_discovery(hass, discovery_flow_conf): result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Start import flow result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_IMPORT}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Discovery flow has been aborted assert len(hass.config_entries.flow.async_progress()) == 0 @@ -234,7 +234,7 @@ async def test_import_no_confirmation(hass, discovery_flow_conf): discovery_flow_conf["discovered"] = True result = await flow.async_step_import(None) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY async def test_import_single_instance(hass, discovery_flow_conf): @@ -246,7 +246,7 @@ async def test_import_single_instance(hass, discovery_flow_conf): MockConfigEntry(domain="test").add_to_hass(hass) result = await flow.async_step_import(None) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_ignored_discoveries(hass, discovery_flow_conf): @@ -256,7 +256,7 @@ async def test_ignored_discoveries(hass, discovery_flow_conf): result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM flow = next( ( @@ -278,7 +278,7 @@ async def test_ignored_discoveries(hass, discovery_flow_conf): result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT async def test_webhook_single_entry_allowed(hass, webhook_flow_conf): @@ -289,7 +289,7 @@ async def test_webhook_single_entry_allowed(hass, webhook_flow_conf): MockConfigEntry(domain="test_single").add_to_hass(hass) result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -302,7 +302,7 @@ async def test_webhook_multiple_entries_allowed(hass, webhook_flow_conf): hass.config.api = Mock(base_url="http://example.com") result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM async def test_webhook_config_flow_registers_webhook(hass, webhook_flow_conf): @@ -316,7 +316,7 @@ async def test_webhook_config_flow_registers_webhook(hass, webhook_flow_conf): ) result = await flow.async_step_user(user_input={}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"]["webhook_id"] is not None @@ -341,7 +341,7 @@ async def test_webhook_create_cloudhook(hass, webhook_flow_conf): result = await hass.config_entries.flow.async_init( "test_single", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch( "hass_nabucasa.cloudhooks.Cloudhooks.async_create", @@ -358,7 +358,7 @@ async def test_webhook_create_cloudhook(hass, webhook_flow_conf): ): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["description_placeholders"]["webhook_url"] == "https://example.com" assert len(mock_create.mock_calls) == 1 assert len(async_setup_entry.mock_calls) == 1 @@ -395,7 +395,7 @@ async def test_webhook_create_cloudhook_aborts_not_connected(hass, webhook_flow_ result = await hass.config_entries.flow.async_init( "test_single", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM with patch( "hass_nabucasa.cloudhooks.Cloudhooks.async_create", @@ -413,7 +413,7 @@ async def test_webhook_create_cloudhook_aborts_not_connected(hass, webhook_flow_ result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cloud_not_connected" diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index e5d220c55df..652ce69e57d 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -110,7 +110,7 @@ async def test_abort_if_no_implementation(hass, flow_handler): flow = flow_handler() flow.hass = hass result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_configuration" @@ -121,7 +121,7 @@ async def test_missing_credentials_for_domain(hass, flow_handler): with patch("homeassistant.loader.APPLICATION_CREDENTIALS", [TEST_DOMAIN]): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "missing_credentials" @@ -139,7 +139,7 @@ async def test_abort_if_authorization_timeout( ): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "authorize_url_timeout" @@ -157,7 +157,7 @@ async def test_abort_if_no_url_available( ): result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_url_available" @@ -179,7 +179,7 @@ async def test_abort_if_oauth_error( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" # Pick implementation @@ -195,7 +195,7 @@ async def test_abort_if_oauth_error( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -219,7 +219,7 @@ async def test_abort_if_oauth_error( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "oauth_error" @@ -241,7 +241,7 @@ async def test_abort_if_oauth_rejected( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" # Pick implementation @@ -257,7 +257,7 @@ async def test_abort_if_oauth_rejected( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" @@ -273,7 +273,7 @@ async def test_abort_if_oauth_rejected( result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "user_rejected_authorize" assert result["description_placeholders"] == {"error": "access_denied"} @@ -291,7 +291,7 @@ async def test_step_discovery(hass, flow_handler, local_impl): data=data_entry_flow.BaseServiceInfo(), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" @@ -308,7 +308,7 @@ async def test_abort_discovered_multiple(hass, flow_handler, local_impl): data=data_entry_flow.BaseServiceInfo(), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" result = await hass.config_entries.flow.async_init( @@ -317,7 +317,7 @@ async def test_abort_discovered_multiple(hass, flow_handler, local_impl): data=data_entry_flow.BaseServiceInfo(), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -340,7 +340,7 @@ async def test_abort_discovered_existing_entries(hass, flow_handler, local_impl) data=data_entry_flow.BaseServiceInfo(), ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -362,7 +362,7 @@ async def test_full_flow( TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" # Pick implementation @@ -378,7 +378,7 @@ async def test_full_flow( }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP assert result["url"] == ( f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" "&redirect_uri=https://example.com/auth/external/callback" diff --git a/tests/helpers/test_helper_config_entry_flow.py b/tests/helpers/test_helper_config_entry_flow.py index 46e8998c738..2967b202efe 100644 --- a/tests/helpers/test_helper_config_entry_flow.py +++ b/tests/helpers/test_helper_config_entry_flow.py @@ -38,7 +38,7 @@ def manager(): async def async_finish_flow(self, flow, result): """Test finish flow.""" - if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: result["source"] = flow.context.get("source") entries.append(result) return result @@ -110,11 +110,11 @@ async def test_config_flow_advanced_option( # Start flow in basic mode result = await manager.async_init("test") - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert list(result["data_schema"].schema.keys()) == ["option1"] result = await manager.async_configure(result["flow_id"], {"option1": "blabla"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {} assert result["options"] == { "advanced_default": "a very reasonable default", @@ -126,7 +126,7 @@ async def test_config_flow_advanced_option( # Start flow in advanced mode result = await manager.async_init("test", context={"show_advanced_options": True}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert list(result["data_schema"].schema.keys()) == [ "option1", "advanced_no_default", @@ -136,7 +136,7 @@ async def test_config_flow_advanced_option( result = await manager.async_configure( result["flow_id"], {"advanced_no_default": "abc123", "option1": "blabla"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {} assert result["options"] == { "advanced_default": "a very reasonable default", @@ -149,7 +149,7 @@ async def test_config_flow_advanced_option( # Start flow in advanced mode result = await manager.async_init("test", context={"show_advanced_options": True}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert list(result["data_schema"].schema.keys()) == [ "option1", "advanced_no_default", @@ -164,7 +164,7 @@ async def test_config_flow_advanced_option( "option1": "blabla", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {} assert result["options"] == { "advanced_default": "not default", @@ -216,13 +216,13 @@ async def test_options_flow_advanced_option( # Start flow in basic mode result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert list(result["data_schema"].schema.keys()) == ["option1"] result = await hass.config_entries.options.async_configure( result["flow_id"], {"option1": "blublu"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { "advanced_default": "not default", "advanced_no_default": "abc123", @@ -236,7 +236,7 @@ async def test_options_flow_advanced_option( result = await hass.config_entries.options.async_init( config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert list(result["data_schema"].schema.keys()) == [ "option1", "advanced_no_default", @@ -246,7 +246,7 @@ async def test_options_flow_advanced_option( result = await hass.config_entries.options.async_configure( result["flow_id"], {"advanced_no_default": "def456", "option1": "blabla"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { "advanced_default": "a very reasonable default", "advanced_no_default": "def456", @@ -260,7 +260,7 @@ async def test_options_flow_advanced_option( result = await hass.config_entries.options.async_init( config_entry.entry_id, context={"show_advanced_options": True} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert list(result["data_schema"].schema.keys()) == [ "option1", "advanced_no_default", @@ -275,7 +275,7 @@ async def test_options_flow_advanced_option( "option1": "blabla", }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { "advanced_default": "also not default", "advanced_no_default": "abc123", diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 9372a906f71..03da4d36853 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -713,14 +713,14 @@ async def test_discovery_notification(hass): ) flow1 = await hass.config_entries.flow.async_configure(flow1["flow_id"], {}) - assert flow1["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert flow1["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get("persistent_notification.config_entry_discovery") assert state is not None flow2 = await hass.config_entries.flow.async_configure(flow2["flow_id"], {}) - assert flow2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert flow2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY await hass.async_block_till_done() state = hass.states.get("persistent_notification.config_entry_discovery") @@ -780,14 +780,14 @@ async def test_reauth_notification(hass): ) flow1 = await hass.config_entries.flow.async_configure(flow1["flow_id"], {}) - assert flow1["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert flow1["type"] == data_entry_flow.FlowResultType.ABORT await hass.async_block_till_done() state = hass.states.get("persistent_notification.config_entry_reconfigure") assert state is not None flow2 = await hass.config_entries.flow.async_configure(flow2["flow_id"], {}) - assert flow2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert flow2["type"] == data_entry_flow.FlowResultType.ABORT await hass.async_block_till_done() state = hass.states.get("persistent_notification.config_entry_reconfigure") @@ -1059,7 +1059,7 @@ async def test_entry_options(hass, manager): await manager.options.async_finish_flow( flow, - {"data": {"second": True}, "type": data_entry_flow.RESULT_TYPE_CREATE_ENTRY}, + {"data": {"second": True}, "type": data_entry_flow.FlowResultType.CREATE_ENTRY}, ) assert entry.data == {"first": True} @@ -1092,7 +1092,7 @@ async def test_entry_options_abort(hass, manager): flow.handler = entry.entry_id # Used to keep reference to config entry assert await manager.options.async_finish_flow( - flow, {"type": data_entry_flow.RESULT_TYPE_ABORT, "reason": "test"} + flow, {"type": data_entry_flow.FlowResultType.ABORT, "reason": "test"} ) @@ -1574,7 +1574,7 @@ async def test_unique_id_existing_entry(hass, manager): "comp", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entries = hass.config_entries.async_entries("comp") assert len(entries) == 1 @@ -1860,14 +1860,14 @@ async def test_unique_id_in_progress(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Will be canceled result2 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -1898,14 +1898,14 @@ async def test_finish_flow_aborts_progress(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Will finish and cancel other one. result2 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER}, data={} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(hass.config_entries.flow.async_progress()) == 0 @@ -1931,7 +1931,7 @@ async def test_unique_id_ignore(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM result2 = await manager.flow.async_init( "comp", @@ -1939,7 +1939,7 @@ async def test_unique_id_ignore(hass, manager): data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # assert len(hass.config_entries.flow.async_progress()) == 0 @@ -1992,7 +1992,7 @@ async def test_manual_add_overrides_ignored_entry(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert entry.data["host"] == "1.1.1.1" assert entry.data["additional"] == "data" assert len(async_reload.mock_calls) == 0 @@ -2169,7 +2169,7 @@ async def test_unignore_step_form(hass, manager): context={"source": config_entries.SOURCE_IGNORE}, data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries("comp")[0] assert entry.source == "ignore" @@ -2214,7 +2214,7 @@ async def test_unignore_create_entry(hass, manager): context={"source": config_entries.SOURCE_IGNORE}, data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries("comp")[0] assert entry.source == "ignore" @@ -2256,7 +2256,7 @@ async def test_unignore_default_impl(hass, manager): context={"source": config_entries.SOURCE_IGNORE}, data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY entry = hass.config_entries.async_entries("comp")[0] assert entry.source == "ignore" @@ -2321,7 +2321,7 @@ async def test_partial_flows_hidden(hass, manager): # When it's complete it should now be visible in async_progress and have triggered # discovery notifications result = await init_task - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert len(hass.config_entries.flow.async_progress()) == 1 await hass.async_block_till_done() @@ -2531,7 +2531,7 @@ async def test_flow_with_default_discovery(hass, manager, discovery_source): result = await manager.flow.async_init( "comp", context={"source": discovery_source[0]}, data=discovery_source[1] ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2544,7 +2544,7 @@ async def test_flow_with_default_discovery(hass, manager, discovery_source): result2 = await manager.flow.async_configure( result["flow_id"], user_input={"fake": "data"} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(hass.config_entries.flow.async_progress()) == 0 @@ -2575,7 +2575,7 @@ async def test_flow_with_default_discovery_with_unique_id(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_DISCOVERY} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2600,7 +2600,7 @@ async def test_default_discovery_abort_existing_entries(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_DISCOVERY} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -2626,13 +2626,13 @@ async def test_default_discovery_in_progress(hass, manager): context={"source": config_entries.SOURCE_DISCOVERY}, data={"unique_id": "mock-unique-id"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Second discovery without a unique ID result2 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["type"] == data_entry_flow.FlowResultType.ABORT flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2660,7 +2660,7 @@ async def test_default_discovery_abort_on_new_unique_flow(hass, manager): result2 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.FlowResultType.FORM # Second discovery brings in a unique ID result = await manager.flow.async_init( @@ -2668,7 +2668,7 @@ async def test_default_discovery_abort_on_new_unique_flow(hass, manager): context={"source": config_entries.SOURCE_DISCOVERY}, data={"unique_id": "mock-unique-id"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM # Ensure the first one is cancelled and we end up with just the last one flows = hass.config_entries.flow.async_progress() @@ -2702,7 +2702,7 @@ async def test_default_discovery_abort_on_user_flow_complete(hass, manager): flow1 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_DISCOVERY}, data={} ) - assert flow1["type"] == data_entry_flow.RESULT_TYPE_FORM + assert flow1["type"] == data_entry_flow.FlowResultType.FORM flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2711,14 +2711,14 @@ async def test_default_discovery_abort_on_user_flow_complete(hass, manager): flow2 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_USER} ) - assert flow2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert flow2["type"] == data_entry_flow.FlowResultType.FORM flows = hass.config_entries.flow.async_progress() assert len(flows) == 2 # Complete the manual flow result = await hass.config_entries.flow.async_configure(flow2["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY # Ensure the first flow is gone now flows = hass.config_entries.flow.async_progress() @@ -2780,7 +2780,7 @@ async def test_flow_same_device_multiple_sources(hass, manager): result2 = await manager.flow.async_configure( flows[0]["flow_id"], user_input={"fake": "data"} ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(hass.config_entries.flow.async_progress()) == 0 diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 18d5469a162..301a61700c9 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -34,7 +34,7 @@ def manager(): async def async_finish_flow(self, flow, result): """Test finish flow.""" - if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: result["source"] = flow.context.get("source") entries.append(result) return result @@ -100,7 +100,7 @@ async def test_configure_two_steps(manager): form = await manager.async_configure(form["flow_id"], ["INIT-DATA"]) form = await manager.async_configure(form["flow_id"], ["SECOND-DATA"]) - assert form["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert form["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert len(manager.async_progress()) == 0 assert len(manager.mock_created_entries) == 1 result = manager.mock_created_entries[0] @@ -122,7 +122,7 @@ async def test_show_form(manager): ) form = await manager.async_init("test") - assert form["type"] == data_entry_flow.RESULT_TYPE_FORM + assert form["type"] == data_entry_flow.FlowResultType.FORM assert form["data_schema"] is schema assert form["errors"] == {"username": "Should be unique."} @@ -218,7 +218,7 @@ async def test_finish_callback_change_result_type(hass): async def async_finish_flow(self, flow, result): """Redirect to init form if count <= 1.""" - if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: if result["data"] is None or result["data"].get("count", 0) <= 1: return flow.async_show_form( step_id="init", data_schema=vol.Schema({"count": int}) @@ -230,16 +230,16 @@ async def test_finish_callback_change_result_type(hass): manager = FlowManager(hass) result = await manager.async_init("test") - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" result = await manager.async_configure(result["flow_id"], {"count": 0}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" assert "result" not in result result = await manager.async_configure(result["flow_id"], {"count": 2}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"] == 2 @@ -269,7 +269,7 @@ async def test_external_step(hass, manager): ) result = await manager.async_init("test") - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["type"] == data_entry_flow.FlowResultType.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" @@ -277,7 +277,7 @@ async def test_external_step(hass, manager): # Mimic external step # Called by integrations: `hass.config_entries.flow.async_configure(…)` result = await manager.async_configure(result["flow_id"], {"title": "Hello"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP_DONE + assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP_DONE await hass.async_block_till_done() assert len(events) == 1 @@ -289,7 +289,7 @@ async def test_external_step(hass, manager): # Frontend refreshses the flow result = await manager.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Hello" @@ -326,7 +326,7 @@ async def test_show_progress(hass, manager): ) result = await manager.async_init("test") - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "task_one" assert len(manager.async_progress()) == 1 assert len(manager.async_progress_by_handler("test")) == 1 @@ -335,7 +335,7 @@ async def test_show_progress(hass, manager): # Mimic task one done and moving to task two # Called by integrations: `hass.config_entries.flow.async_configure(…)` result = await manager.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "task_two" await hass.async_block_till_done() @@ -349,7 +349,7 @@ async def test_show_progress(hass, manager): # Mimic task two done and continuing step # Called by integrations: `hass.config_entries.flow.async_configure(…)` result = await manager.async_configure(result["flow_id"], {"title": "Hello"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS_DONE + assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS_DONE await hass.async_block_till_done() assert len(events) == 2 @@ -361,7 +361,7 @@ async def test_show_progress(hass, manager): # Frontend refreshes the flow result = await manager.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Hello" @@ -374,7 +374,7 @@ async def test_abort_flow_exception(manager): raise data_entry_flow.AbortFlow("mock-reason", {"placeholder": "yo"}) form = await manager.async_init("test") - assert form["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert form["type"] == data_entry_flow.FlowResultType.ABORT assert form["reason"] == "mock-reason" assert form["description_placeholders"] == {"placeholder": "yo"} @@ -432,7 +432,7 @@ async def test_async_has_matching_flow( 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["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS assert result["progress_action"] == "task_one" assert len(manager.async_progress()) == 1 assert len(manager.async_progress_by_handler("test")) == 1 @@ -528,5 +528,5 @@ async def test_show_menu(hass, manager, menu_options): result = await manager.async_configure( result["flow_id"], {"next_step_id": "target1"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "target1" From d3db4da11a668d64867289f6b5999f1eb0288d6e Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 7 Jul 2022 13:07:05 -0400 Subject: [PATCH 2245/3516] ElkM1 bump lib to support Python 3.10 SSL (#74569) Co-authored-by: J. Nick Koston --- homeassistant/components/elkm1/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 13f8ba8401f..7043fdfcb9d 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -2,7 +2,7 @@ "domain": "elkm1", "name": "Elk-M1 Control", "documentation": "https://www.home-assistant.io/integrations/elkm1", - "requirements": ["elkm1-lib==2.0.0"], + "requirements": ["elkm1-lib==2.0.2"], "dhcp": [{ "registered_devices": true }, { "macaddress": "00409D*" }], "codeowners": ["@gwww", "@bdraco"], "dependencies": ["network"], diff --git a/requirements_all.txt b/requirements_all.txt index 838e1278d75..7df6a75a946 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -586,7 +586,7 @@ elgato==3.0.0 eliqonline==1.2.2 # homeassistant.components.elkm1 -elkm1-lib==2.0.0 +elkm1-lib==2.0.2 # homeassistant.components.elmax elmax_api==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b9c98ba9fb2..02b5ef640d7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ eagle100==0.1.1 elgato==3.0.0 # homeassistant.components.elkm1 -elkm1-lib==2.0.0 +elkm1-lib==2.0.2 # homeassistant.components.elmax elmax_api==0.0.2 From ff877b8144a8c08eb82810dc997fb95987bbe43a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Jul 2022 12:46:19 -0500 Subject: [PATCH 2246/3516] Fix exception in doorbird logbook during startup (#74649) * Fix exception in doorbird logbook during startup Fixes ``` 2022-07-07 16:50:33.203 ERROR (MainThread) [homeassistant.helpers.integration_platform] Error processing platform doorbird.logbook Traceback (most recent call last): File "/usr/src/homeassistant/homeassistant/helpers/integration_platform.py", line 51, in _async_process_single_integration_platform_component await integration_platform.process_platform(hass, component_name, platform) File "/usr/src/homeassistant/homeassistant/components/logbook/__init__.py", line 159, in _process_logbook_platform platform.async_describe_events(hass, _async_describe_event) File "/usr/src/homeassistant/homeassistant/components/doorbird/logbook.py", line 43, in async_describe_events door_station = data[DOOR_STATION] KeyError: door_station ``` * py39 --- homeassistant/components/doorbird/logbook.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/doorbird/logbook.py b/homeassistant/components/doorbird/logbook.py index 110d3f22fbf..3b1563c2880 100644 --- a/homeassistant/components/doorbird/logbook.py +++ b/homeassistant/components/doorbird/logbook.py @@ -1,4 +1,7 @@ """Describe logbook events.""" +from __future__ import annotations + +from typing import Any from homeassistant.components.logbook.const import ( LOGBOOK_ENTRY_ENTITY_ID, @@ -28,12 +31,13 @@ def async_describe_events(hass, async_describe_event): ].get(doorbird_event, event.data.get(ATTR_ENTITY_ID)), } - domain_data = hass.data[DOMAIN] + domain_data: dict[str, Any] = hass.data[DOMAIN] - for config_entry_id in domain_data: - door_station = domain_data[config_entry_id][DOOR_STATION] - - for event in door_station.doorstation_events: + for data in domain_data.values(): + if DOOR_STATION not in data: + # We need to skip door_station_event_entity_ids + continue + for event in data[DOOR_STATION].doorstation_events: async_describe_event( DOMAIN, f"{DOMAIN}_{event}", async_describe_logbook_event ) From f2706aa8c0eb3edb6ffeee01c77977800d95749e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 19:48:39 +0200 Subject: [PATCH 2247/3516] Update debugpy to 1.6.1 (#74637) --- 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 00e4839be63..3f31c5de758 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -2,7 +2,7 @@ "domain": "debugpy", "name": "Remote Python Debugger", "documentation": "https://www.home-assistant.io/integrations/debugpy", - "requirements": ["debugpy==1.6.0"], + "requirements": ["debugpy==1.6.1"], "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 7df6a75a946..7cc20779883 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -518,7 +518,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.6.0 +debugpy==1.6.1 # homeassistant.components.decora # decora==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02b5ef640d7..870f830ac31 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -388,7 +388,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.6.0 +debugpy==1.6.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns From dc0c41982fe0626c487057cc63bcf4764f7fd5e4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 7 Jul 2022 19:50:08 +0200 Subject: [PATCH 2248/3516] Remove meteo_france from mypy ignore list (#74613) --- homeassistant/components/meteo_france/const.py | 2 +- homeassistant/components/meteo_france/sensor.py | 9 ++++++++- homeassistant/components/meteo_france/weather.py | 5 +++++ mypy.ini | 6 ------ script/hassfest/mypy_config.py | 2 -- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index df667b1964b..d6cbd34d88d 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -183,7 +183,7 @@ SENSOR_TYPES_PROBABILITY: tuple[MeteoFranceSensorEntityDescription, ...] = ( ) -CONDITION_CLASSES = { +CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_CLEAR_NIGHT: ["Nuit Claire", "Nuit claire"], ATTR_CONDITION_CLOUDY: ["Très nuageux", "Couvert"], ATTR_CONDITION_FOG: [ diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 0d90e4d33be..823018f405f 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -1,4 +1,6 @@ """Support for Meteo-France raining forecast sensor.""" +from __future__ import annotations + from meteofrance_api.helpers import ( get_warning_text_status_from_indice_color, readeable_phenomenoms_dict, @@ -97,6 +99,11 @@ class MeteoFranceSensor(CoordinatorEntity, SensorEntity): @property def device_info(self) -> DeviceInfo: """Return the device info.""" + assert ( + self.platform + and self.platform.config_entry + and self.platform.config_entry.unique_id + ) return DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self.platform.config_entry.unique_id)}, @@ -191,7 +198,7 @@ class MeteoFranceAlertSensor(MeteoFranceSensor): def _find_first_probability_forecast_not_null( probability_forecast: list, path: list -) -> int: +) -> int | None: """Search the first not None value in the first forecast elements.""" for forecast in probability_forecast[0:3]: if forecast[path[1]][path[2]] is not None: diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index a30a65304b0..2727b4470c1 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -102,6 +102,11 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): @property def device_info(self) -> DeviceInfo: """Return the device info.""" + assert ( + self.platform + and self.platform.config_entry + and self.platform.config_entry.unique_id + ) return DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, self.platform.config_entry.unique_id)}, diff --git a/mypy.ini b/mypy.ini index fc99119cda3..1f0018a4c05 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2701,12 +2701,6 @@ ignore_errors = true [mypy-homeassistant.components.lovelace.websocket] ignore_errors = true -[mypy-homeassistant.components.meteo_france.sensor] -ignore_errors = true - -[mypy-homeassistant.components.meteo_france.weather] -ignore_errors = true - [mypy-homeassistant.components.minecraft_server] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 7f1faf00bb6..3aedaced494 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -42,8 +42,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.lovelace.dashboard", "homeassistant.components.lovelace.resources", "homeassistant.components.lovelace.websocket", - "homeassistant.components.meteo_france.sensor", - "homeassistant.components.meteo_france.weather", "homeassistant.components.minecraft_server", "homeassistant.components.minecraft_server.helpers", "homeassistant.components.minecraft_server.sensor", From bd43f0393cc5f031bda02d232b6bd8906d8b81e1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 7 Jul 2022 19:51:38 +0200 Subject: [PATCH 2249/3516] Remove influxdb from mypy ignore list (#74612) --- homeassistant/components/influxdb/__init__.py | 28 ++++++++++++------- mypy.ini | 3 -- script/hassfest/mypy_config.py | 1 - 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index ecc76481e49..72871c75fc4 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -30,7 +30,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import event as event_helper, state as state_helper import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_values import EntityValues @@ -198,13 +198,13 @@ CONFIG_SCHEMA = vol.Schema( ) -def _generate_event_to_json(conf: dict) -> Callable[[dict], str]: +def _generate_event_to_json(conf: dict) -> Callable[[Event], dict[str, Any] | None]: """Build event to json converter and add to config.""" entity_filter = convert_include_exclude_filter(conf) tags = conf.get(CONF_TAGS) - tags_attributes = conf.get(CONF_TAGS_ATTRIBUTES) + tags_attributes: list[str] = conf[CONF_TAGS_ATTRIBUTES] default_measurement = conf.get(CONF_DEFAULT_MEASUREMENT) - measurement_attr = conf.get(CONF_MEASUREMENT_ATTR) + measurement_attr: str = conf[CONF_MEASUREMENT_ATTR] override_measurement = conf.get(CONF_OVERRIDE_MEASUREMENT) global_ignore_attributes = set(conf[CONF_IGNORE_ATTRIBUTES]) component_config = EntityValues( @@ -213,15 +213,15 @@ def _generate_event_to_json(conf: dict) -> Callable[[dict], str]: conf[CONF_COMPONENT_CONFIG_GLOB], ) - def event_to_json(event: dict) -> str: + def event_to_json(event: Event) -> dict[str, Any] | None: """Convert event into json in format Influx expects.""" - state = event.data.get(EVENT_NEW_STATE) + state: State | None = event.data.get(EVENT_NEW_STATE) if ( state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE) or not entity_filter(state.entity_id) ): - return + return None try: _include_state = _include_value = False @@ -263,7 +263,7 @@ def _generate_event_to_json(conf: dict) -> Callable[[dict], str]: else: include_uom = measurement_attr != "unit_of_measurement" - json = { + json: dict[str, Any] = { INFLUX_CONF_MEASUREMENT: measurement, INFLUX_CONF_TAGS: { CONF_DOMAIN: state.domain, @@ -328,7 +328,9 @@ class InfluxClient: close: Callable[[], None] -def get_influx_connection(conf, test_write=False, test_read=False): # noqa: C901 +def get_influx_connection( # noqa: C901 + conf, test_write=False, test_read=False +) -> InfluxClient: """Create the correct influx connection for the API version.""" kwargs = { CONF_TIMEOUT: TIMEOUT, @@ -470,6 +472,10 @@ def get_influx_connection(conf, test_write=False, test_read=False): # noqa: C90 return InfluxClient(databases, write_v1, query_v1, close_v1) +def _retry_setup(hass: HomeAssistant, config: ConfigType) -> None: + setup(hass, config) + + def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the InfluxDB component.""" conf = config[DOMAIN] @@ -477,7 +483,9 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: influx = get_influx_connection(conf, test_write=True) except ConnectionError as exc: _LOGGER.error(RETRY_MESSAGE, exc) - event_helper.call_later(hass, RETRY_INTERVAL, lambda _: setup(hass, config)) + event_helper.call_later( + hass, RETRY_INTERVAL, lambda _: _retry_setup(hass, config) + ) return True event_to_json = _generate_event_to_json(conf) diff --git a/mypy.ini b/mypy.ini index 1f0018a4c05..b139a40976b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2665,9 +2665,6 @@ ignore_errors = true [mypy-homeassistant.components.icloud.sensor] ignore_errors = true -[mypy-homeassistant.components.influxdb] -ignore_errors = true - [mypy-homeassistant.components.izone.climate] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 3aedaced494..55ed01aaa63 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -30,7 +30,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.icloud.account", "homeassistant.components.icloud.device_tracker", "homeassistant.components.icloud.sensor", - "homeassistant.components.influxdb", "homeassistant.components.izone.climate", "homeassistant.components.konnected", "homeassistant.components.konnected.config_flow", From ac85a3ce64ea815fd3530085c085e384cf8269fb Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 7 Jul 2022 20:13:03 +0200 Subject: [PATCH 2250/3516] Use pydeconz interface controls for button platform (#74654) --- homeassistant/components/deconz/button.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/button.py b/homeassistant/components/deconz/button.py index 498c88a2351..552723d6f9c 100644 --- a/homeassistant/components/deconz/button.py +++ b/homeassistant/components/deconz/button.py @@ -90,8 +90,11 @@ class DeconzButton(DeconzSceneMixin, ButtonEntity): async def async_press(self) -> None: """Store light states into scene.""" - async_button_fn = getattr(self._device, self.entity_description.button_fn) - await async_button_fn() + async_button_fn = getattr( + self.gateway.api.scenes, + self.entity_description.button_fn, + ) + await async_button_fn(self._device.group_id, self._device.id) def get_device_identifier(self) -> str: """Return a unique identifier for this scene.""" From b9b6ed33ee5c0d5ade23790c7c37f3a382269fcd Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 7 Jul 2022 20:27:48 +0200 Subject: [PATCH 2251/3516] Fix smart energy polling for Tuya plugs (#74640) * Add PolledSmartEnergySummation to poll summation_delivered for some ZHA plugs * Remove PolledSmartEnergyMetering, add stop_on_match_group to summation sensors --- homeassistant/components/zha/sensor.py | 37 ++++++++++++++------------ 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 4a4700b3c4c..d9aa44fe436 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -472,25 +472,8 @@ class SmartEnergyMetering(Sensor): @MULTI_MATCH( channel_names=CHANNEL_SMARTENERGY_METERING, - models={"TS011F"}, stop_on_match_group=CHANNEL_SMARTENERGY_METERING, ) -class PolledSmartEnergyMetering(SmartEnergyMetering): - """Polled metering sensor.""" - - @property - def should_poll(self) -> bool: - """Poll the entity for current state.""" - return True - - async def async_update(self) -> None: - """Retrieve latest state.""" - if not self.available: - return - await self._channel.async_force_update() - - -@MULTI_MATCH(channel_names=CHANNEL_SMARTENERGY_METERING) class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered"): """Smart Energy Metering summation sensor.""" @@ -523,6 +506,26 @@ class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered") return round(cooked, 3) +@MULTI_MATCH( + channel_names=CHANNEL_SMARTENERGY_METERING, + models={"TS011F"}, + stop_on_match_group=CHANNEL_SMARTENERGY_METERING, +) +class PolledSmartEnergySummation(SmartEnergySummation): + """Polled Smart Energy Metering summation sensor.""" + + @property + def should_poll(self) -> bool: + """Poll the entity for current state.""" + return True + + async def async_update(self) -> None: + """Retrieve latest state.""" + if not self.available: + return + await self._channel.async_force_update() + + @MULTI_MATCH(channel_names=CHANNEL_PRESSURE) class Pressure(Sensor): """Pressure sensor.""" From d8030ed9e7c92279316240e6d71095345883ebb6 Mon Sep 17 00:00:00 2001 From: Arne Mauer Date: Thu, 7 Jul 2022 20:28:33 +0200 Subject: [PATCH 2252/3516] Ikea Starkvind support all models (#74615) * Add Particulate Matter 2.5 of ZCL concentration clusters to ZHA component * Fixed black and flake8 test * New sensors and manufacturer cluster to support IKEA STARKVIND (with quirk) * Fix multi_match for FilterLifeTime, device_run_time, filter_run_time sensors for Ikea starkvind * Remove model match because sensors are matched with manufacturer channel * Update manufacturerspecific.py * Update number.py --- homeassistant/components/zha/binary_sensor.py | 2 +- homeassistant/components/zha/number.py | 4 +--- homeassistant/components/zha/sensor.py | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 709515d7ca2..23d26e4b13f 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -188,7 +188,7 @@ class FrostLock(BinarySensor, id_suffix="frost_lock"): _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.LOCK -@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) +@MULTI_MATCH(channel_names="ikea_airpurifier") class ReplaceFilter(BinarySensor, id_suffix="replace_filter"): """ZHA BinarySensor.""" diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index e1268e29190..36fc5267bd9 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -525,9 +525,7 @@ class TimerDurationMinutes(ZHANumberConfigurationEntity, id_suffix="timer_durati _zcl_attribute: str = "timer_duration" -@CONFIG_DIAGNOSTIC_MATCH( - channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"} -) +@CONFIG_DIAGNOSTIC_MATCH(channel_names="ikea_airpurifier") class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time"): """Representation of a ZHA timer duration configuration entity.""" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index d9aa44fe436..513ba5510b5 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -813,7 +813,7 @@ class TimeLeft(Sensor, id_suffix="time_left"): _unit = TIME_MINUTES -@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) +@MULTI_MATCH(channel_names="ikea_airpurifier") class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"): """Sensor that displays device run time (in minutes).""" @@ -823,7 +823,7 @@ class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"): _unit = TIME_MINUTES -@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) +@MULTI_MATCH(channel_names="ikea_airpurifier") class IkeaFilterRunTime(Sensor, id_suffix="filter_run_time"): """Sensor that displays run time of the current filter (in minutes).""" From 3f53ed5d5cddfebd302a2d4fad45d271943533d6 Mon Sep 17 00:00:00 2001 From: Kostas Chatzikokolakis Date: Thu, 7 Jul 2022 21:31:03 +0300 Subject: [PATCH 2253/3516] Add trigger/arm_custom_bypass to Template Alarm Control Panel (#74629) --- .../template/alarm_control_panel.py | 40 +++++++++++++++++++ .../template/test_alarm_control_panel.py | 16 ++++++++ 2 files changed, 56 insertions(+) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 132e9fb0ca5..d7c117c9be7 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -19,6 +19,7 @@ from homeassistant.const import ( CONF_UNIQUE_ID, CONF_VALUE_TEMPLATE, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_VACATION, @@ -42,6 +43,7 @@ from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_con _LOGGER = logging.getLogger(__name__) _VALID_STATES = [ STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_VACATION, @@ -53,10 +55,12 @@ _VALID_STATES = [ ] CONF_ARM_AWAY_ACTION = "arm_away" +CONF_ARM_CUSTOM_BYPASS_ACTION = "arm_custom_bypass" CONF_ARM_HOME_ACTION = "arm_home" CONF_ARM_NIGHT_ACTION = "arm_night" CONF_ARM_VACATION_ACTION = "arm_vacation" CONF_DISARM_ACTION = "disarm" +CONF_TRIGGER_ACTION = "trigger" CONF_ALARM_CONTROL_PANELS = "panels" CONF_CODE_ARM_REQUIRED = "code_arm_required" CONF_CODE_FORMAT = "code_format" @@ -75,9 +79,11 @@ ALARM_CONTROL_PANEL_SCHEMA = vol.Schema( vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name): cv.enum( TemplateCodeFormat @@ -164,6 +170,16 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): self._arm_vacation_script = None if (arm_vacation_action := config.get(CONF_ARM_VACATION_ACTION)) is not None: self._arm_vacation_script = Script(hass, arm_vacation_action, name, DOMAIN) + self._arm_custom_bypass_script = None + if ( + arm_custom_bypass_action := config.get(CONF_ARM_CUSTOM_BYPASS_ACTION) + ) is not None: + self._arm_custom_bypass_script = Script( + hass, arm_custom_bypass_action, name, DOMAIN + ) + self._trigger_script = None + if (trigger_action := config.get(CONF_TRIGGER_ACTION)) is not None: + self._trigger_script = Script(hass, trigger_action, name, DOMAIN) self._state: str | None = None @@ -196,6 +212,16 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): supported_features | AlarmControlPanelEntityFeature.ARM_VACATION ) + if self._arm_custom_bypass_script is not None: + supported_features = ( + supported_features | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS + ) + + if self._trigger_script is not None: + supported_features = ( + supported_features | AlarmControlPanelEntityFeature.TRIGGER + ) + return supported_features @property @@ -275,8 +301,22 @@ class AlarmControlPanelTemplate(TemplateEntity, AlarmControlPanelEntity): STATE_ALARM_ARMED_VACATION, script=self._arm_vacation_script, code=code ) + async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None: + """Arm the panel to Custom Bypass.""" + await self._async_alarm_arm( + STATE_ALARM_ARMED_CUSTOM_BYPASS, + script=self._arm_custom_bypass_script, + code=code, + ) + async def async_alarm_disarm(self, code: str | None = None) -> None: """Disarm the panel.""" await self._async_alarm_arm( STATE_ALARM_DISARMED, script=self._disarm_script, code=code ) + + async def async_alarm_trigger(self, code: str | None = None) -> None: + """Trigger the panel.""" + await self._async_alarm_arm( + STATE_ALARM_TRIGGERED, script=self._trigger_script, code=code + ) diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index 9e51b48dcc6..7ae69b2d563 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -8,6 +8,7 @@ from homeassistant.const import ( ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_VACATION, @@ -62,11 +63,21 @@ OPTIMISTIC_TEMPLATE_ALARM_CONFIG = { "entity_id": "alarm_control_panel.test", "data": {"code": "{{ this.entity_id }}"}, }, + "arm_custom_bypass": { + "service": "alarm_control_panel.alarm_arm_custom_bypass", + "entity_id": "alarm_control_panel.test", + "data": {"code": "{{ this.entity_id }}"}, + }, "disarm": { "service": "alarm_control_panel.alarm_disarm", "entity_id": "alarm_control_panel.test", "data": {"code": "{{ this.entity_id }}"}, }, + "trigger": { + "service": "alarm_control_panel.alarm_trigger", + "entity_id": "alarm_control_panel.test", + "data": {"code": "{{ this.entity_id }}"}, + }, } @@ -96,6 +107,7 @@ async def test_template_state_text(hass, start_ha): STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMED_VACATION, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, @@ -136,7 +148,9 @@ async def test_optimistic_states(hass, start_ha): ("alarm_arm_home", STATE_ALARM_ARMED_HOME), ("alarm_arm_night", STATE_ALARM_ARMED_NIGHT), ("alarm_arm_vacation", STATE_ALARM_ARMED_VACATION), + ("alarm_arm_custom_bypass", STATE_ALARM_ARMED_CUSTOM_BYPASS), ("alarm_disarm", STATE_ALARM_DISARMED), + ("alarm_trigger", STATE_ALARM_TRIGGERED), ]: await hass.services.async_call( ALARM_DOMAIN, service, {"entity_id": TEMPLATE_NAME}, blocking=True @@ -259,7 +273,9 @@ async def test_name(hass, start_ha): "alarm_arm_away", "alarm_arm_night", "alarm_arm_vacation", + "alarm_arm_custom_bypass", "alarm_disarm", + "alarm_trigger", ], ) async def test_actions(hass, service, start_ha, service_calls): From 46beae9061d33d3c88454d0bedb4984cf934e033 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 20:55:47 +0200 Subject: [PATCH 2254/3516] Bump number of test groups from 6 -> 10 (#74648) --- .github/workflows/ci.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6a4413e3668..2f97a8e4208 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -93,8 +93,8 @@ jobs: # Defaults integrations_glob="" test_full_suite="true" - test_groups="[1, 2, 3, 4, 5, 6]" - test_group_count=6 + test_groups="[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" + test_group_count=10 tests="[]" tests_glob="" @@ -137,8 +137,8 @@ jobs: || [[ "${{ github.event.inputs.full }}" == "true" ]] \ || [[ "${{ contains(github.event.pull_request.labels.*.name, 'ci-full-run') }}" == "true" ]]; then - test_groups="[1, 2, 3, 4, 5, 6]" - test_group_count=6 + test_groups="[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" + test_group_count=10 test_full_suite="true" fi From a6244eea28bc44e3c5f323f9280ea607fa7bd744 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 21:28:18 +0200 Subject: [PATCH 2255/3516] Search/replace RESULT_TYPE_* by FlowResultType enum (#74656) --- tests/auth/providers/test_trusted_networks.py | 14 +-- tests/components/adax/test_config_flow.py | 44 +++++----- .../components/airthings/test_config_flow.py | 12 +-- .../aladdin_connect/test_config_flow.py | 28 +++--- tests/components/ambee/test_config_flow.py | 30 +++---- tests/components/anthemav/test_config_flow.py | 12 +-- .../aseko_pool_live/test_config_flow.py | 8 +- .../aussie_broadband/test_config_flow.py | 22 ++--- tests/components/baf/test_config_flow.py | 22 ++--- tests/components/balboa/test_config_flow.py | 18 ++-- tests/components/canary/test_config_flow.py | 22 ++--- .../components/cloudflare/test_config_flow.py | 28 +++--- .../components/co2signal/test_config_flow.py | 24 ++--- tests/components/cpuspeed/test_config_flow.py | 16 ++-- tests/components/daikin/test_config_flow.py | 22 ++--- tests/components/deluge/test_config_flow.py | 18 ++-- .../components/derivative/test_config_flow.py | 10 +-- .../devolo_home_network/test_config_flow.py | 20 ++--- tests/components/directv/test_config_flow.py | 36 ++++---- tests/components/dnsip/test_config_flow.py | 20 ++--- tests/components/econet/test_config_flow.py | 22 ++--- tests/components/efergy/test_config_flow.py | 20 ++--- .../eight_sleep/test_config_flow.py | 18 ++-- tests/components/elgato/test_config_flow.py | 26 +++--- tests/components/elkm1/test_config_flow.py | 34 +++---- tests/components/esphome/test_config_flow.py | 60 ++++++------- .../evil_genius_labs/test_config_flow.py | 12 +-- tests/components/ezviz/test_config_flow.py | 64 +++++++------- tests/components/filesize/test_config_flow.py | 16 ++-- tests/components/fivem/test_config_flow.py | 14 +-- .../fjaraskupan/test_config_flow.py | 14 ++- tests/components/flux_led/test_config_flow.py | 26 +++--- .../forecast_solar/test_config_flow.py | 10 +-- tests/components/fritz/test_config_flow.py | 56 ++++++------ tests/components/fritzbox/test_config_flow.py | 68 +++++++------- .../fritzbox_callmonitor/test_config_flow.py | 34 ++++--- tests/components/fronius/test_config_flow.py | 32 +++---- .../garages_amsterdam/test_config_flow.py | 12 +-- .../components/geocaching/test_config_flow.py | 8 +- tests/components/github/test_config_flow.py | 19 ++-- .../components/gogogate2/test_config_flow.py | 32 +++---- tests/components/goodwe/test_config_flow.py | 18 ++-- tests/components/group/test_config_flow.py | 34 ++++--- .../components/hardkernel/test_config_flow.py | 6 +- .../here_travel_time/test_config_flow.py | 8 +- .../homeassistant_yellow/test_config_flow.py | 6 +- .../homekit_controller/test_config_flow.py | 6 +- .../components/homewizard/test_config_flow.py | 26 +++--- .../integration/test_config_flow.py | 10 +-- .../intellifire/test_config_flow.py | 34 ++++--- tests/components/iotawatt/test_config_flow.py | 18 ++-- tests/components/ipp/test_config_flow.py | 54 ++++++------ .../kaleidescape/test_config_flow.py | 24 +++-- tests/components/knx/test_config_flow.py | 88 +++++++++---------- .../components/laundrify/test_config_flow.py | 24 +++-- tests/components/lookin/test_config_flow.py | 20 ++--- .../components/luftdaten/test_config_flow.py | 26 +++--- tests/components/mill/test_config_flow.py | 32 +++---- tests/components/min_max/test_config_flow.py | 10 +-- .../minecraft_server/test_config_flow.py | 26 +++--- tests/components/mjpeg/test_config_flow.py | 38 ++++---- .../modern_forms/test_config_flow.py | 24 +++-- .../moehlenhoff_alpha2/test_config_flow.py | 16 ++-- tests/components/moon/test_config_flow.py | 14 ++- tests/components/mullvad/test_config_flow.py | 10 +-- tests/components/nzbget/test_config_flow.py | 24 +++-- tests/components/oncue/test_config_flow.py | 18 ++-- tests/components/onewire/test_config_flow.py | 16 ++-- tests/components/onewire/test_options_flow.py | 22 ++--- .../components/open_meteo/test_config_flow.py | 6 +- .../components/opengarage/test_config_flow.py | 18 ++-- .../components/p1_monitor/test_config_flow.py | 8 +- tests/components/peco/test_config_flow.py | 12 +-- tests/components/pi_hole/test_config_flow.py | 26 +++--- tests/components/plaato/test_config_flow.py | 38 ++++---- .../components/powerwall/test_config_flow.py | 8 +- .../progettihwsw/test_config_flow.py | 18 ++-- tests/components/prosegur/test_config_flow.py | 8 +- .../pure_energie/test_config_flow.py | 18 ++-- tests/components/pvoutput/test_config_flow.py | 34 ++++--- .../radio_browser/test_config_flow.py | 14 ++- .../rainforest_eagle/test_config_flow.py | 10 +-- .../raspberry_pi/test_config_flow.py | 6 +- tests/components/rdw/test_config_flow.py | 14 +-- tests/components/ridwell/test_config_flow.py | 16 ++-- tests/components/roku/test_config_flow.py | 38 ++++---- .../components/rpi_power/test_config_flow.py | 16 ++-- tests/components/sabnzbd/test_config_flow.py | 4 +- .../components/samsungtv/test_config_flow.py | 28 +++--- tests/components/season/test_config_flow.py | 14 ++- tests/components/senseme/test_config_flow.py | 46 +++++----- tests/components/sensibo/test_config_flow.py | 30 +++---- tests/components/sentry/test_config_flow.py | 18 ++-- tests/components/skybell/test_config_flow.py | 22 ++--- .../components/slimproto/test_config_flow.py | 6 +- tests/components/sma/test_config_flow.py | 20 ++--- tests/components/sonarr/test_config_flow.py | 32 +++---- tests/components/songpal/test_config_flow.py | 28 +++--- tests/components/sql/test_config_flow.py | 36 ++++---- .../components/squeezebox/test_config_flow.py | 30 +++---- tests/components/steamist/test_config_flow.py | 52 +++++------ .../components/stookalert/test_config_flow.py | 12 +-- tests/components/sun/test_config_flow.py | 14 ++- .../surepetcare/test_config_flow.py | 18 ++-- .../switch_as_x/test_config_flow.py | 10 +-- .../components/switchbot/test_config_flow.py | 28 +++--- .../switcher_kis/test_config_flow.py | 18 ++-- .../components/tailscale/test_config_flow.py | 32 +++---- .../tankerkoenig/test_config_flow.py | 38 ++++---- .../tesla_wall_connector/test_config_flow.py | 12 +-- .../components/threshold/test_config_flow.py | 14 +-- tests/components/tod/test_config_flow.py | 10 +-- tests/components/tolo/test_config_flow.py | 20 ++--- tests/components/tplink/test_config_flow.py | 14 +-- .../trafikverket_ferry/test_config_flow.py | 20 ++--- .../trafikverket_train/test_config_flow.py | 26 +++--- .../test_config_flow.py | 4 +- .../twentemilieu/test_config_flow.py | 20 ++--- .../ukraine_alarm/test_config_flow.py | 50 +++++------ .../unifiprotect/test_config_flow.py | 62 ++++++------- tests/components/uptime/test_config_flow.py | 14 ++- .../uptimerobot/test_config_flow.py | 36 ++++---- .../utility_meter/test_config_flow.py | 18 ++-- tests/components/vallox/test_config_flow.py | 36 ++++---- tests/components/venstar/test_config_flow.py | 18 ++-- tests/components/vera/test_config_flow.py | 10 +-- tests/components/verisure/test_config_flow.py | 38 ++++---- tests/components/version/test_config_flow.py | 30 +++---- .../components/vlc_telnet/test_config_flow.py | 28 +++--- tests/components/watttime/test_config_flow.py | 22 ++--- tests/components/webostv/test_config_flow.py | 42 ++++----- tests/components/whois/test_config_flow.py | 18 ++-- tests/components/wiffi/test_config_flow.py | 14 ++- tests/components/wilight/test_config_flow.py | 22 ++--- tests/components/wiz/test_config_flow.py | 16 ++-- .../yale_smart_alarm/test_config_flow.py | 32 +++---- tests/components/yeelight/test_config_flow.py | 32 +++---- tests/components/youless/test_config_flows.py | 10 +-- tests/components/zha/test_config_flow.py | 60 ++++++------- tests/components/zwave_me/test_config_flow.py | 25 +++--- tests/test_config_entries.py | 18 ++-- tests/test_data_entry_flow.py | 2 +- 142 files changed, 1463 insertions(+), 1849 deletions(-) diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index 406e9a033da..15e831f551a 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -10,7 +10,7 @@ from homeassistant import auth from homeassistant.auth import auth_store from homeassistant.auth.providers import trusted_networks as tn_auth from homeassistant.components.http import CONF_TRUSTED_PROXIES, CONF_USE_X_FORWARDED_FOR -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component @@ -209,7 +209,7 @@ async def test_login_flow(manager, provider): # not from trusted network flow = await provider.async_login_flow({"ip_address": ip_address("127.0.0.1")}) step = await flow.async_step_init() - assert step["type"] == RESULT_TYPE_ABORT + assert step["type"] == FlowResultType.ABORT assert step["reason"] == "not_allowed" # from trusted network, list users @@ -224,7 +224,7 @@ async def test_login_flow(manager, provider): # login with valid user step = await flow.async_step_init({"user": user.id}) - assert step["type"] == RESULT_TYPE_CREATE_ENTRY + assert step["type"] == FlowResultType.CREATE_ENTRY assert step["data"]["user"] == user.id @@ -248,7 +248,7 @@ async def test_trusted_users_login(manager_with_user, provider_with_user): {"ip_address": ip_address("127.0.0.1")} ) step = await flow.async_step_init() - assert step["type"] == RESULT_TYPE_ABORT + assert step["type"] == FlowResultType.ABORT assert step["reason"] == "not_allowed" # from trusted network, list users intersect trusted_users @@ -332,7 +332,7 @@ async def test_trusted_group_login(manager_with_user, provider_with_user): {"ip_address": ip_address("127.0.0.1")} ) step = await flow.async_step_init() - assert step["type"] == RESULT_TYPE_ABORT + assert step["type"] == FlowResultType.ABORT assert step["reason"] == "not_allowed" # from trusted network, list users intersect trusted_users @@ -370,7 +370,7 @@ async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login): {"ip_address": ip_address("127.0.0.1")} ) step = await flow.async_step_init() - assert step["type"] == RESULT_TYPE_ABORT + assert step["type"] == FlowResultType.ABORT assert step["reason"] == "not_allowed" # from trusted network, only one available user, bypass the login flow @@ -378,7 +378,7 @@ async def test_bypass_login_flow(manager_bypass_login, provider_bypass_login): {"ip_address": ip_address("192.168.0.1")} ) step = await flow.async_step_init() - assert step["type"] == RESULT_TYPE_CREATE_ENTRY + assert step["type"] == FlowResultType.CREATE_ENTRY assert step["data"]["user"] == owner.id user = await manager_bypass_login.async_create_user("test-user") diff --git a/tests/components/adax/test_config_flow.py b/tests/components/adax/test_config_flow.py index 998e4df8b15..0c12c486754 100644 --- a/tests/components/adax/test_config_flow.py +++ b/tests/components/adax/test_config_flow.py @@ -15,7 +15,7 @@ from homeassistant.components.adax.const import ( ) from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -30,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -39,7 +39,7 @@ async def test_form(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("adax.get_adax_token", return_value="test_token",), patch( "homeassistant.components.adax.async_setup_entry", @@ -73,7 +73,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch( "adax.get_adax_token", @@ -83,7 +83,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: result2["flow_id"], TEST_DATA, ) - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM assert result3["errors"] == {"base": "cannot_connect"} @@ -108,7 +108,7 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("adax.get_adax_token", return_value="token"): result3 = await hass.config_entries.flow.async_configure( @@ -129,7 +129,7 @@ async def test_local_create_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -138,7 +138,7 @@ async def test_local_create_entry(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -190,7 +190,7 @@ async def test_local_flow_entry_already_exists(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -199,7 +199,7 @@ async def test_local_flow_entry_already_exists(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -228,7 +228,7 @@ async def test_local_connection_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -237,7 +237,7 @@ async def test_local_connection_error(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -253,7 +253,7 @@ async def test_local_connection_error(hass): test_data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -263,7 +263,7 @@ async def test_local_heater_not_available(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -272,7 +272,7 @@ async def test_local_heater_not_available(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -288,7 +288,7 @@ async def test_local_heater_not_available(hass): test_data, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "heater_not_available" @@ -298,7 +298,7 @@ async def test_local_heater_not_found(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -307,7 +307,7 @@ async def test_local_heater_not_found(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -323,7 +323,7 @@ async def test_local_heater_not_found(hass): test_data, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "heater_not_found" @@ -333,7 +333,7 @@ async def test_local_invalid_wifi_cred(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -342,7 +342,7 @@ async def test_local_invalid_wifi_cred(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { WIFI_SSID: "ssid", @@ -358,5 +358,5 @@ async def test_local_invalid_wifi_cred(hass): test_data, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "invalid_auth" diff --git a/tests/components/airthings/test_config_flow.py b/tests/components/airthings/test_config_flow.py index 0ecb2c7a8dc..6e3a1579f70 100644 --- a/tests/components/airthings/test_config_flow.py +++ b/tests/components/airthings/test_config_flow.py @@ -6,7 +6,7 @@ import airthings 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 +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -22,7 +22,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch("airthings.get_token", return_value="test_token",), patch( @@ -35,7 +35,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Airthings" assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -56,7 +56,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -75,7 +75,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -94,7 +94,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/aladdin_connect/test_config_flow.py b/tests/components/aladdin_connect/test_config_flow.py index 33117c64110..19017e79570 100644 --- a/tests/components/aladdin_connect/test_config_flow.py +++ b/tests/components/aladdin_connect/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant import config_entries from homeassistant.components.aladdin_connect.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -22,7 +18,7 @@ async def test_form(hass: HomeAssistant, mock_aladdinconnect_api: MagicMock) -> result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -40,7 +36,7 @@ async def test_form(hass: HomeAssistant, mock_aladdinconnect_api: MagicMock) -> ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Aladdin Connect" assert result2["data"] == { CONF_USERNAME: "test-username", @@ -70,7 +66,7 @@ async def test_form_failed_auth( }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -94,7 +90,7 @@ async def test_form_connection_timeout( }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -153,7 +149,7 @@ async def test_import_flow_success( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Aladdin Connect" assert result2["data"] == { CONF_USERNAME: "test-user", @@ -185,7 +181,7 @@ async def test_reauth_flow( ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -201,7 +197,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert mock_entry.data == { CONF_USERNAME: "test-username", @@ -232,7 +228,7 @@ async def test_reauth_flow_auth_error( ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} mock_aladdinconnect_api.login.return_value = False with patch( @@ -251,7 +247,7 @@ async def test_reauth_flow_auth_error( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -278,7 +274,7 @@ async def test_reauth_flow_connnection_error( ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} mock_aladdinconnect_api.login.side_effect = ClientConnectionError @@ -292,5 +288,5 @@ async def test_reauth_flow_connnection_error( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/ambee/test_config_flow.py b/tests/components/ambee/test_config_flow.py index a6220418681..233bbaf232b 100644 --- a/tests/components/ambee/test_config_flow.py +++ b/tests/components/ambee/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.ambee.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -23,7 +19,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -42,7 +38,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Name" assert result2.get("data") == { CONF_API_KEY: "example", @@ -64,7 +60,7 @@ async def test_full_flow_with_authentication_error(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -82,7 +78,7 @@ async def test_full_flow_with_authentication_error(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": "invalid_api_key"} assert "flow_id" in result2 @@ -102,7 +98,7 @@ async def test_full_flow_with_authentication_error(hass: HomeAssistant) -> None: }, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "Name" assert result3.get("data") == { CONF_API_KEY: "example", @@ -131,7 +127,7 @@ async def test_api_error(hass: HomeAssistant) -> None: }, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} @@ -150,7 +146,7 @@ async def test_reauth_flow( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -165,7 +161,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_API_KEY: "other_key", @@ -196,7 +192,7 @@ async def test_reauth_with_authentication_error( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -211,7 +207,7 @@ async def test_reauth_with_authentication_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "invalid_api_key"} assert "flow_id" in result2 @@ -227,7 +223,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result3.get("type") == RESULT_TYPE_ABORT + assert result3.get("type") == FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_API_KEY: "other_key", @@ -267,6 +263,6 @@ async def test_reauth_api_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/anthemav/test_config_flow.py b/tests/components/anthemav/test_config_flow.py index 98ed0f0abf0..1f3dec8d5e1 100644 --- a/tests/components/anthemav/test_config_flow.py +++ b/tests/components/anthemav/test_config_flow.py @@ -6,7 +6,7 @@ from anthemav.device_error import DeviceError from homeassistant import config_entries from homeassistant.components.anthemav.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form_with_valid_connection( @@ -16,7 +16,7 @@ async def test_form_with_valid_connection( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -33,7 +33,7 @@ async def test_form_with_valid_connection( await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == { "host": "1.1.1.1", "port": 14999, @@ -64,7 +64,7 @@ async def test_form_device_info_error(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_receive_deviceinfo"} @@ -88,7 +88,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -105,7 +105,7 @@ async def test_import_configuration( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "host": "1.1.1.1", "port": 14999, diff --git a/tests/components/aseko_pool_live/test_config_flow.py b/tests/components/aseko_pool_live/test_config_flow.py index 5ab85c61a8b..754c7fd0a3c 100644 --- a/tests/components/aseko_pool_live/test_config_flow.py +++ b/tests/components/aseko_pool_live/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant import config_entries, setup from homeassistant.components.aseko_pool_live.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -17,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -41,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "aseko@example.com" assert result2["data"] == {CONF_ACCESS_TOKEN: "any_access_token"} assert len(mock_setup_entry.mock_calls) == 1 @@ -82,5 +82,5 @@ async def test_get_account_info_exceptions( }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": reason} diff --git a/tests/components/aussie_broadband/test_config_flow.py b/tests/components/aussie_broadband/test_config_flow.py index 8a5f6b6763f..ed98924e19d 100644 --- a/tests/components/aussie_broadband/test_config_flow.py +++ b/tests/components/aussie_broadband/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant import config_entries from homeassistant.components.aussie_broadband.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .common import FAKE_DATA, FAKE_SERVICES @@ -25,7 +21,7 @@ async def test_form(hass: HomeAssistant) -> None: result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result1["type"] == RESULT_TYPE_FORM + assert result1["type"] == FlowResultType.FORM assert result1["errors"] is None with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( @@ -42,7 +38,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_USERNAME assert result2["data"] == FAKE_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -87,7 +83,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == RESULT_TYPE_ABORT + assert result4["type"] == FlowResultType.ABORT assert len(mock_setup_entry.mock_calls) == 0 @@ -96,7 +92,7 @@ async def test_no_services(hass: HomeAssistant) -> None: result1 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result1["type"] == RESULT_TYPE_FORM + assert result1["type"] == FlowResultType.FORM assert result1["errors"] is None with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( @@ -111,7 +107,7 @@ async def test_no_services(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "no_services_found" assert len(mock_setup_entry.mock_calls) == 0 @@ -130,7 +126,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: FAKE_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -148,7 +144,7 @@ async def test_form_network_issue(hass: HomeAssistant) -> None: FAKE_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -176,7 +172,7 @@ async def test_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_USERNAME assert result2["data"] == FAKE_DATA diff --git a/tests/components/baf/test_config_flow.py b/tests/components/baf/test_config_flow.py index 77a83a9673b..0df9ce480a7 100644 --- a/tests/components/baf/test_config_flow.py +++ b/tests/components/baf/test_config_flow.py @@ -6,11 +6,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.baf.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import MOCK_NAME, MOCK_UUID, MockBAFDevice @@ -45,7 +41,7 @@ async def test_form_user(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.1"} assert len(mock_setup_entry.mock_calls) == 1 @@ -63,7 +59,7 @@ async def test_form_cannot_connect(hass): {CONF_IP_ADDRESS: "127.0.0.1"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {CONF_IP_ADDRESS: "cannot_connect"} @@ -79,7 +75,7 @@ async def test_form_unknown_exception(hass): {CONF_IP_ADDRESS: "127.0.0.1"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -99,7 +95,7 @@ async def test_zeroconf_discovery(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -137,7 +133,7 @@ async def test_zeroconf_updates_existing_ip(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "127.0.0.1" @@ -157,7 +153,7 @@ async def test_zeroconf_rejects_ipv6(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ipv6_not_supported" @@ -176,7 +172,7 @@ async def test_user_flow_is_not_blocked_by_discovery(hass): type="mock_type", ), ) - assert discovery_result["type"] == RESULT_TYPE_FORM + assert discovery_result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -194,7 +190,7 @@ async def test_user_flow_is_not_blocked_by_discovery(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_NAME assert result2["data"] == {CONF_IP_ADDRESS: "127.0.0.1"} assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/balboa/test_config_flow.py b/tests/components/balboa/test_config_flow.py index 2b448a3df56..d6be9feb727 100644 --- a/tests/components/balboa/test_config_flow.py +++ b/tests/components/balboa/test_config_flow.py @@ -6,11 +6,7 @@ from homeassistant.components.balboa.const import CONF_SYNC_TIME, DOMAIN from homeassistant.config_entries import SOURCE_USER 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 homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -25,7 +21,7 @@ async def test_form(hass: HomeAssistant, client: MagicMock) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -41,7 +37,7 @@ async def test_form(hass: HomeAssistant, client: MagicMock) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -62,7 +58,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, client: MagicMock) -> No TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -82,7 +78,7 @@ async def test_unknown_error(hass: HomeAssistant, client: MagicMock) -> None: TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -94,7 +90,7 @@ async def test_already_configured(hass: HomeAssistant, client: MagicMock) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -110,7 +106,7 @@ async def test_already_configured(hass: HomeAssistant, client: MagicMock) -> Non ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/canary/test_config_flow.py b/tests/components/canary/test_config_flow.py index b37c81407f7..2b1e4e9cd2e 100644 --- a/tests/components/canary/test_config_flow.py +++ b/tests/components/canary/test_config_flow.py @@ -11,11 +11,7 @@ from homeassistant.components.canary.const import ( ) from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_TIMEOUT -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import USER_INPUT, _patch_async_setup, _patch_async_setup_entry, init_integration @@ -26,7 +22,7 @@ async def test_user_form(hass, canary_config_flow): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: @@ -36,7 +32,7 @@ async def test_user_form(hass, canary_config_flow): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["data"] == {**USER_INPUT, CONF_TIMEOUT: DEFAULT_TIMEOUT} @@ -57,7 +53,7 @@ async def test_user_form_cannot_connect(hass, canary_config_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} canary_config_flow.side_effect = ConnectTimeout() @@ -67,7 +63,7 @@ async def test_user_form_cannot_connect(hass, canary_config_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -84,7 +80,7 @@ async def test_user_form_unexpected_exception(hass, canary_config_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -97,7 +93,7 @@ async def test_user_form_single_instance_allowed(hass, canary_config_flow): context={"source": SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -110,7 +106,7 @@ async def test_options_flow(hass, canary): assert entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" with _patch_async_setup(), _patch_async_setup_entry(): @@ -120,6 +116,6 @@ async def test_options_flow(hass, canary): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_FFMPEG_ARGUMENTS] == "-v" assert result["data"][CONF_TIMEOUT] == 7 diff --git a/tests/components/cloudflare/test_config_flow.py b/tests/components/cloudflare/test_config_flow.py index df8cff0f3a4..3d225e75803 100644 --- a/tests/components/cloudflare/test_config_flow.py +++ b/tests/components/cloudflare/test_config_flow.py @@ -8,11 +8,7 @@ from pycfdns.exceptions import ( from homeassistant.components.cloudflare.const import CONF_RECORDS, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_TOKEN, CONF_SOURCE, CONF_ZONE -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( ENTRY_CONFIG, @@ -31,7 +27,7 @@ async def test_user_form(hass, cfupdate_flow): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -41,7 +37,7 @@ async def test_user_form(hass, cfupdate_flow): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "zone" assert result["errors"] == {} @@ -51,7 +47,7 @@ async def test_user_form(hass, cfupdate_flow): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "records" assert result["errors"] is None @@ -62,7 +58,7 @@ async def test_user_form(hass, cfupdate_flow): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT_ZONE[CONF_ZONE] assert result["data"] @@ -90,7 +86,7 @@ async def test_user_form_cannot_connect(hass, cfupdate_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -108,7 +104,7 @@ async def test_user_form_invalid_auth(hass, cfupdate_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -126,7 +122,7 @@ async def test_user_form_invalid_zone(hass, cfupdate_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_zone"} @@ -144,7 +140,7 @@ async def test_user_form_unexpected_exception(hass, cfupdate_flow): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -158,7 +154,7 @@ async def test_user_form_single_instance_allowed(hass): context={CONF_SOURCE: SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -176,7 +172,7 @@ async def test_reauth_flow(hass, cfupdate_flow): }, data=entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with _patch_async_setup_entry() as mock_setup_entry: @@ -186,7 +182,7 @@ async def test_reauth_flow(hass, cfupdate_flow): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_API_TOKEN] == "other_token" diff --git a/tests/components/co2signal/test_config_flow.py b/tests/components/co2signal/test_config_flow.py index 7961e413135..d85279fc30c 100644 --- a/tests/components/co2signal/test_config_flow.py +++ b/tests/components/co2signal/test_config_flow.py @@ -6,7 +6,7 @@ import pytest 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 +from homeassistant.data_entry_flow import FlowResultType from . import VALID_PAYLOAD @@ -17,7 +17,7 @@ async def test_form_home(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch("CO2Signal.get_latest", return_value=VALID_PAYLOAD,), patch( @@ -33,7 +33,7 @@ async def test_form_home(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "CO2 Signal" assert result2["data"] == { "api_key": "api_key", @@ -47,7 +47,7 @@ async def test_form_coordinates(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -57,7 +57,7 @@ async def test_form_coordinates(hass: HomeAssistant) -> None: "api_key": "api_key", }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("CO2Signal.get_latest", return_value=VALID_PAYLOAD,), patch( "homeassistant.components.co2signal.async_setup_entry", @@ -72,7 +72,7 @@ async def test_form_coordinates(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "12.3, 45.6" assert result3["data"] == { "latitude": 12.3, @@ -88,7 +88,7 @@ async def test_form_country(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -98,7 +98,7 @@ async def test_form_country(hass: HomeAssistant) -> None: "api_key": "api_key", }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("CO2Signal.get_latest", return_value=VALID_PAYLOAD,), patch( "homeassistant.components.co2signal.async_setup_entry", @@ -112,7 +112,7 @@ async def test_form_country(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "fr" assert result3["data"] == { "country_code": "fr", @@ -147,7 +147,7 @@ async def test_form_error_handling(hass: HomeAssistant, err_str, err_code) -> No }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": err_code} @@ -169,7 +169,7 @@ async def test_form_error_unexpected_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -191,5 +191,5 @@ async def test_form_error_unexpected_data(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/cpuspeed/test_config_flow.py b/tests/components/cpuspeed/test_config_flow.py index 8f12092f389..c016cfdb1d9 100644 --- a/tests/components/cpuspeed/test_config_flow.py +++ b/tests/components/cpuspeed/test_config_flow.py @@ -5,11 +5,7 @@ from unittest.mock import AsyncMock, MagicMock from homeassistant.components.cpuspeed.const import DOMAIN from homeassistant.config_entries 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 homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -24,7 +20,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -33,7 +29,7 @@ async def test_full_user_flow( user_input={}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "CPU Speed" assert result2.get("data") == {} @@ -54,7 +50,7 @@ async def test_already_configured( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 @@ -71,7 +67,7 @@ async def test_not_compatible( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -81,7 +77,7 @@ async def test_not_compatible( user_input={}, ) - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "not_compatible" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index 3a6a56fe097..bd118884edc 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -11,11 +11,7 @@ from homeassistant.components import zeroconf from homeassistant.components.daikin.const import KEY_MAC from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -54,7 +50,7 @@ async def test_user(hass, mock_daikin): context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -62,7 +58,7 @@ async def test_user(hass, mock_daikin): context={"source": SOURCE_USER}, data={CONF_HOST: HOST}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"][CONF_HOST] == HOST assert result["data"][KEY_MAC] == MAC @@ -77,7 +73,7 @@ async def test_abort_if_already_setup(hass, mock_daikin): data={CONF_HOST: HOST, KEY_MAC: MAC}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -100,7 +96,7 @@ async def test_device_abort(hass, mock_daikin, s_effect, reason): context={"source": SOURCE_USER}, data={CONF_HOST: HOST, KEY_MAC: MAC}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": reason} assert result["step_id"] == "user" @@ -112,7 +108,7 @@ async def test_api_password_abort(hass): context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_API_KEY: "aa", CONF_PASSWORD: "aa"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "api_password"} assert result["step_id"] == "user" @@ -144,7 +140,7 @@ async def test_discovery_zeroconf( context={"source": source}, data=data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" MockConfigEntry(domain="daikin", unique_id=unique_id).add_to_hass(hass) @@ -154,7 +150,7 @@ async def test_discovery_zeroconf( data={CONF_HOST: HOST}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" result = await hass.config_entries.flow.async_init( @@ -163,5 +159,5 @@ async def test_discovery_zeroconf( data=data, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_in_progress" diff --git a/tests/components/deluge/test_config_flow.py b/tests/components/deluge/test_config_flow.py index b56c717e635..a32b72704e8 100644 --- a/tests/components/deluge/test_config_flow.py +++ b/tests/components/deluge/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components.deluge.const import DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import CONF_DATA @@ -60,7 +56,7 @@ async def test_flow_user(hass: HomeAssistant, api): context={"source": SOURCE_USER}, data=CONF_DATA, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA @@ -78,7 +74,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant, api): DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -87,7 +83,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant, conn_error): 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["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -97,7 +93,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant, unknown_error): 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["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -121,13 +117,13 @@ async def test_flow_reauth(hass: HomeAssistant, api): data=CONF_DATA, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.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_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == CONF_DATA diff --git a/tests/components/derivative/test_config_flow.py b/tests/components/derivative/test_config_flow.py index 61ab7251f8a..b6fa62e290f 100644 --- a/tests/components/derivative/test_config_flow.py +++ b/tests/components/derivative/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.derivative.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -39,7 +39,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My derivative" assert result["data"] == {} assert result["options"] == { @@ -98,7 +98,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "round") == 1.0 @@ -115,7 +115,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "unit_time": "h", }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "My derivative", "round": 2.0, diff --git a/tests/components/devolo_home_network/test_config_flow.py b/tests/components/devolo_home_network/test_config_flow.py index 7cd1ba5222c..f9d589eb638 100644 --- a/tests/components/devolo_home_network/test_config_flow.py +++ b/tests/components/devolo_home_network/test_config_flow.py @@ -16,11 +16,7 @@ from homeassistant.components.devolo_home_network.const import ( ) from homeassistant.const import CONF_BASE, CONF_IP_ADDRESS, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .const import DISCOVERY_INFO, DISCOVERY_INFO_WRONG_DEVICE, IP @@ -30,7 +26,7 @@ async def test_form(hass: HomeAssistant, info: dict[str, Any]): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -45,7 +41,7 @@ async def test_form(hass: HomeAssistant, info: dict[str, Any]): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["result"].unique_id == info["serial_number"] assert result2["title"] == info["title"] assert result2["data"] == { @@ -75,7 +71,7 @@ async def test_form_error(hass: HomeAssistant, exception_type, expected_error): }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {CONF_BASE: expected_error} @@ -88,7 +84,7 @@ async def test_zeroconf(hass: HomeAssistant): ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["description_placeholders"] == {"host_name": "test"} context = next( @@ -125,7 +121,7 @@ async def test_abort_zeroconf_wrong_device(hass: HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=DISCOVERY_INFO_WRONG_DEVICE, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "home_control" @@ -158,7 +154,7 @@ async def test_abort_if_configued(hass: HomeAssistant): }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" # Abort on concurrent zeroconf discovery flow @@ -167,7 +163,7 @@ async def test_abort_if_configued(hass: HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=DISCOVERY_INFO, ) - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_configured" diff --git a/tests/components/directv/test_config_flow.py b/tests/components/directv/test_config_flow.py index f80e0d781ef..7ef6bdde69d 100644 --- a/tests/components/directv/test_config_flow.py +++ b/tests/components/directv/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.ssdp import ATTR_UPNP_SERIAL from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.components.directv import ( HOST, @@ -35,7 +31,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_show_ssdp_form( @@ -49,7 +45,7 @@ async def test_show_ssdp_form( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" assert result["description_placeholders"] == {CONF_NAME: HOST} @@ -67,7 +63,7 @@ async def test_cannot_connect( data=user_input, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -85,7 +81,7 @@ async def test_ssdp_cannot_connect( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -102,7 +98,7 @@ async def test_ssdp_confirm_cannot_connect( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -119,7 +115,7 @@ async def test_user_device_exists_abort( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -136,7 +132,7 @@ async def test_ssdp_device_exists_abort( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -154,7 +150,7 @@ async def test_ssdp_with_receiver_id_device_exists_abort( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -173,7 +169,7 @@ async def test_unknown_error( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -192,7 +188,7 @@ async def test_ssdp_unknown_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -211,7 +207,7 @@ async def test_ssdp_confirm_unknown_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -226,7 +222,7 @@ async def test_full_user_flow_implementation( context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" user_input = MOCK_USER_INPUT.copy() @@ -236,7 +232,7 @@ async def test_full_user_flow_implementation( user_input=user_input, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] @@ -255,7 +251,7 @@ async def test_full_ssdp_flow_implementation( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "ssdp_confirm" assert result["description_placeholders"] == {CONF_NAME: HOST} @@ -263,7 +259,7 @@ async def test_full_ssdp_flow_implementation( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] diff --git a/tests/components/dnsip/test_config_flow.py b/tests/components/dnsip/test_config_flow.py index 51e169b8bb5..fdec45be7f5 100644 --- a/tests/components/dnsip/test_config_flow.py +++ b/tests/components/dnsip/test_config_flow.py @@ -18,11 +18,7 @@ from homeassistant.components.dnsip.const import ( ) from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -66,7 +62,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "home-assistant.io" assert result2["data"] == { "hostname": "home-assistant.io", @@ -108,7 +104,7 @@ async def test_form_adv(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "home-assistant.io" assert result2["data"] == { "hostname": "home-assistant.io", @@ -141,7 +137,7 @@ async def test_form_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_hostname"} @@ -183,7 +179,7 @@ async def test_flow_already_exist(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -217,7 +213,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -228,7 +224,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "resolver": "8.8.8.8", "resolver_ipv6": "2001:4860:4860::8888", @@ -287,7 +283,7 @@ async def test_options_error(hass: HomeAssistant, p_input: dict[str, str]) -> No ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "init" if p_input[CONF_IPV4]: assert result2["errors"] == {"resolver": "invalid_resolver"} diff --git a/tests/components/econet/test_config_flow.py b/tests/components/econet/test_config_flow.py index 68eb18a931e..635b9e2e40a 100644 --- a/tests/components/econet/test_config_flow.py +++ b/tests/components/econet/test_config_flow.py @@ -7,11 +7,7 @@ from pyeconet.errors import InvalidCredentialsError, PyeconetError from homeassistant.components.econet import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -22,7 +18,7 @@ async def test_bad_credentials(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -39,7 +35,7 @@ async def test_bad_credentials(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == { "base": "invalid_auth", @@ -52,7 +48,7 @@ async def test_generic_error_from_library(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -69,7 +65,7 @@ async def test_generic_error_from_library(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == { "base": "cannot_connect", @@ -82,7 +78,7 @@ async def test_auth_worked(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -99,7 +95,7 @@ async def test_auth_worked(hass): }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_EMAIL: "admin@localhost.com", CONF_PASSWORD: "password0", @@ -119,7 +115,7 @@ async def test_already_configured(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -136,5 +132,5 @@ async def test_already_configured(hass): }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/efergy/test_config_flow.py b/tests/components/efergy/test_config_flow.py index 89f3b266b7c..95effbfbd72 100644 --- a/tests/components/efergy/test_config_flow.py +++ b/tests/components/efergy/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components.efergy.const import DEFAULT_NAME, DOMAIN from homeassistant.config_entries 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 homeassistant.data_entry_flow import FlowResultType from . import CONF_DATA, HID, _patch_efergy, _patch_efergy_status, create_entry @@ -27,14 +23,14 @@ async def test_flow_user(hass: HomeAssistant): DOMAIN, context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.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["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == CONF_DATA assert result["result"].unique_id == HID @@ -47,7 +43,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant): 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["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "cannot_connect" @@ -59,7 +55,7 @@ async def test_flow_user_invalid_auth(hass: HomeAssistant): 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["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -71,7 +67,7 @@ async def test_flow_user_unknown(hass: HomeAssistant): 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["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "unknown" @@ -90,7 +86,7 @@ async def test_flow_reauth(hass: HomeAssistant): data=CONF_DATA, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" new_conf = {CONF_API_KEY: "1234567890"} @@ -98,6 +94,6 @@ async def test_flow_reauth(hass: HomeAssistant): result["flow_id"], user_input=new_conf, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data == new_conf diff --git a/tests/components/eight_sleep/test_config_flow.py b/tests/components/eight_sleep/test_config_flow.py index 8015fb6c69d..1cace5b31cd 100644 --- a/tests/components/eight_sleep/test_config_flow.py +++ b/tests/components/eight_sleep/test_config_flow.py @@ -1,11 +1,7 @@ """Test the Eight Sleep config flow.""" from homeassistant import config_entries from homeassistant.components.eight_sleep.const import DOMAIN -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass) -> None: @@ -13,7 +9,7 @@ async def test_form(hass) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -24,7 +20,7 @@ async def test_form(hass) -> None: }, ) - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -37,7 +33,7 @@ async def test_form_invalid_auth(hass, token_error) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -48,7 +44,7 @@ async def test_form_invalid_auth(hass, token_error) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -63,7 +59,7 @@ async def test_import(hass) -> None: }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["data"] == { "username": "test-username", @@ -81,5 +77,5 @@ async def test_import_invalid_auth(hass, token_error) -> None: "password": "bad-password", }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py index 0e3916a005e..dbdfbfed1d0 100644 --- a/tests/components/elgato/test_config_flow.py +++ b/tests/components/elgato/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.elgato.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PORT, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -28,7 +24,7 @@ async def test_full_user_flow_implementation( context={"source": SOURCE_USER}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -36,7 +32,7 @@ async def test_full_user_flow_implementation( result["flow_id"], user_input={CONF_HOST: "127.0.0.1", CONF_PORT: 9123} ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "CN11A1A00001" assert result2.get("data") == { CONF_HOST: "127.0.0.1", @@ -72,7 +68,7 @@ async def test_full_zeroconf_flow_implementation( assert result.get("description_placeholders") == {"serial_number": "CN11A1A00001"} assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result progress = hass.config_entries.flow.async_progress() @@ -85,7 +81,7 @@ async def test_full_zeroconf_flow_implementation( result["flow_id"], user_input={} ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "CN11A1A00001" assert result2.get("data") == { CONF_HOST: "127.0.0.1", @@ -111,7 +107,7 @@ async def test_connection_error( data={CONF_HOST: "127.0.0.1", CONF_PORT: 9123}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} assert result.get("step_id") == "user" @@ -137,7 +133,7 @@ async def test_zeroconf_connection_error( ) assert result.get("reason") == "cannot_connect" - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT async def test_user_device_exists_abort( @@ -153,7 +149,7 @@ async def test_user_device_exists_abort( data={CONF_HOST: "127.0.0.1", CONF_PORT: 9123}, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -178,7 +174,7 @@ async def test_zeroconf_device_exists_abort( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" entries = hass.config_entries.async_entries(DOMAIN) @@ -199,7 +195,7 @@ async def test_zeroconf_device_exists_abort( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" entries = hass.config_entries.async_entries(DOMAIN) @@ -227,7 +223,7 @@ async def test_zeroconf_during_onboarding( ), ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "CN11A1A00001" assert result.get("data") == { CONF_HOST: "127.0.0.1", diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 398dfd30d03..e47dc402b64 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.elkm1.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PASSWORD -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from . import ( ELK_DISCOVERY, @@ -47,7 +47,7 @@ async def test_discovery_ignored_entry(hass): data=ELK_DISCOVERY_INFO, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -242,7 +242,7 @@ async def test_form_user_with_insecure_elk_times_out(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -280,7 +280,7 @@ async def test_form_user_with_secure_elk_no_discovery_ip_already_configured(hass ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "address_already_configured" @@ -961,7 +961,7 @@ async def test_form_import_existing(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "address_already_configured" @@ -989,7 +989,7 @@ async def test_discovered_by_dhcp_or_discovery_mac_address_mismatch_host_already ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == "cc:cc:cc:cc:cc:cc" @@ -1018,7 +1018,7 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MOCK_MAC @@ -1034,7 +1034,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): data=ELK_DISCOVERY_INFO, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with _patch_discovery(), _patch_elk(): @@ -1044,7 +1044,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): data=DHCP_DISCOVERY, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_elk(): @@ -1058,7 +1058,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" @@ -1073,7 +1073,7 @@ async def test_discovered_by_discovery(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1118,7 +1118,7 @@ async def test_discovered_by_discovery_non_standard_port(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1169,7 +1169,7 @@ async def test_discovered_by_discovery_url_already_configured(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -1182,7 +1182,7 @@ async def test_discovered_by_dhcp_udp_responds(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1225,7 +1225,7 @@ async def test_discovered_by_dhcp_udp_responds_with_nonsecure_port(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1275,7 +1275,7 @@ async def test_discovered_by_dhcp_udp_responds_existing_config_entry(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovered_connection" assert result["errors"] == {} @@ -1315,5 +1315,5 @@ async def test_discovered_by_dhcp_no_udp_response(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 1d2cff051ae..43e2f916082 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -15,11 +15,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp, zeroconf from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN, DomainData from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -66,7 +62,7 @@ async def test_user_connection_works(hass, mock_client, mock_zeroconf): data=None, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) @@ -77,7 +73,7 @@ async def test_user_connection_works(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 80}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 80, @@ -109,7 +105,7 @@ async def test_user_resolve_error(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "resolve_error"} @@ -128,7 +124,7 @@ async def test_user_connection_error(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "connection_error"} @@ -147,14 +143,14 @@ async def test_user_with_password(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "authenticate" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password1"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -174,7 +170,7 @@ async def test_user_invalid_password(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "authenticate" mock_client.connect.side_effect = InvalidAuthAPIError @@ -183,7 +179,7 @@ async def test_user_invalid_password(hass, mock_client, mock_zeroconf): result["flow_id"], user_input={CONF_PASSWORD: "invalid"} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "authenticate" assert result["errors"] == {"base": "invalid_auth"} @@ -198,7 +194,7 @@ async def test_login_connection_error(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "authenticate" mock_client.connect.side_effect = APIConnectionError @@ -207,7 +203,7 @@ async def test_login_connection_error(hass, mock_client, mock_zeroconf): result["flow_id"], user_input={CONF_PASSWORD: "valid"} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "authenticate" assert result["errors"] == {"base": "connection_error"} @@ -233,7 +229,7 @@ async def test_discovery_initiation(hass, mock_client, mock_zeroconf): flow["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test8266" assert result["data"][CONF_HOST] == "192.168.43.183" assert result["data"][CONF_PORT] == 6053 @@ -264,7 +260,7 @@ async def test_discovery_already_configured_hostname(hass, mock_client): "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "test8266" @@ -292,7 +288,7 @@ async def test_discovery_already_configured_ip(hass, mock_client): "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "test8266" @@ -324,7 +320,7 @@ async def test_discovery_already_configured_name(hass, mock_client): "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "test8266" @@ -348,13 +344,13 @@ async def test_discovery_duplicate_data(hass, mock_client): result = await hass.config_entries.flow.async_init( "esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( "esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -380,7 +376,7 @@ async def test_discovery_updates_unique_id(hass, mock_client): "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "test8266" @@ -396,7 +392,7 @@ async def test_user_requires_psk(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "encryption_key" assert result["errors"] == {} @@ -416,7 +412,7 @@ async def test_encryption_key_valid_psk(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "encryption_key" mock_client.device_info = AsyncMock(return_value=MockDeviceInfo(False, "test")) @@ -424,7 +420,7 @@ async def test_encryption_key_valid_psk(hass, mock_client, mock_zeroconf): result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_HOST: "127.0.0.1", CONF_PORT: 6053, @@ -445,7 +441,7 @@ async def test_encryption_key_invalid_psk(hass, mock_client, mock_zeroconf): data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "encryption_key" mock_client.device_info.side_effect = InvalidEncryptionKeyAPIError @@ -453,7 +449,7 @@ async def test_encryption_key_invalid_psk(hass, mock_client, mock_zeroconf): result["flow_id"], user_input={CONF_NOISE_PSK: INVALID_NOISE_PSK} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "encryption_key" assert result["errors"] == {"base": "invalid_psk"} assert mock_client.noise_psk == INVALID_NOISE_PSK @@ -475,7 +471,7 @@ async def test_reauth_initiation(hass, mock_client, mock_zeroconf): "unique_id": entry.unique_id, }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -501,7 +497,7 @@ async def test_reauth_confirm_valid(hass, mock_client, mock_zeroconf): result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK @@ -528,7 +524,7 @@ async def test_reauth_confirm_invalid(hass, mock_client, mock_zeroconf): result["flow_id"], user_input={CONF_NOISE_PSK: INVALID_NOISE_PSK} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] assert result["errors"]["base"] == "invalid_psk" @@ -556,7 +552,7 @@ async def test_discovery_dhcp_updates_host(hass, mock_client): "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "test8266" @@ -585,7 +581,7 @@ async def test_discovery_dhcp_no_changes(hass, mock_client): "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == "test8266" diff --git a/tests/components/evil_genius_labs/test_config_flow.py b/tests/components/evil_genius_labs/test_config_flow.py index e3354f4b9cc..19434c77d3c 100644 --- a/tests/components/evil_genius_labs/test_config_flow.py +++ b/tests/components/evil_genius_labs/test_config_flow.py @@ -7,7 +7,7 @@ import aiohttp from homeassistant import config_entries from homeassistant.components.evil_genius_labs.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form( @@ -17,7 +17,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -41,7 +41,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Fibonacci256-23D4" assert result2["data"] == { "host": "1.1.1.1", @@ -66,7 +66,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, caplog) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} assert "Unable to connect" in caplog.text @@ -88,7 +88,7 @@ async def test_form_timeout(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "timeout"} @@ -109,5 +109,5 @@ async def test_form_unknown(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/ezviz/test_config_flow.py b/tests/components/ezviz/test_config_flow.py index 4ba3b5911f6..c31a43c1949 100644 --- a/tests/components/ezviz/test_config_flow.py +++ b/tests/components/ezviz/test_config_flow.py @@ -29,11 +29,7 @@ from homeassistant.const import ( CONF_URL, CONF_USERNAME, ) -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( DISCOVERY_INFO, @@ -50,7 +46,7 @@ async def test_user_form(hass, ezviz_config_flow): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -61,7 +57,7 @@ async def test_user_form(hass, ezviz_config_flow): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" assert result["data"] == {**USER_INPUT} @@ -70,7 +66,7 @@ async def test_user_form(hass, ezviz_config_flow): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured_account" @@ -85,7 +81,7 @@ async def test_user_custom_url(hass, ezviz_config_flow): {CONF_USERNAME: "test-user", CONF_PASSWORD: "test-pass", CONF_URL: "customize"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {} @@ -95,7 +91,7 @@ async def test_user_custom_url(hass, ezviz_config_flow): {CONF_URL: "test-user"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_PASSWORD: "test-pass", CONF_TYPE: ATTR_TYPE_CLOUD, @@ -112,7 +108,7 @@ async def test_step_discovery_abort_if_cloud_account_missing(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_INTEGRATION_DISCOVERY}, data=DISCOVERY_INFO ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -125,7 +121,7 @@ async def test_step_discovery_abort_if_cloud_account_missing(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ezviz_cloud_account_missing" @@ -139,7 +135,7 @@ async def test_async_step_integration_discovery( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_INTEGRATION_DISCOVERY}, data=DISCOVERY_INFO ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -153,7 +149,7 @@ async def test_async_step_integration_discovery( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { CONF_PASSWORD: "test-pass", CONF_TYPE: ATTR_TYPE_CAMERA, @@ -172,7 +168,7 @@ async def test_options_flow(hass): assert entry.options[CONF_TIMEOUT] == DEFAULT_TIMEOUT result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -182,7 +178,7 @@ async def test_options_flow(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_FFMPEG_ARGUMENTS] == "/H.264" assert result["data"][CONF_TIMEOUT] == 25 @@ -202,7 +198,7 @@ async def test_user_form_exception(hass, ezviz_config_flow): USER_INPUT_VALIDATE, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -213,7 +209,7 @@ async def test_user_form_exception(hass, ezviz_config_flow): USER_INPUT_VALIDATE, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_host"} @@ -224,7 +220,7 @@ async def test_user_form_exception(hass, ezviz_config_flow): USER_INPUT_VALIDATE, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -235,7 +231,7 @@ async def test_user_form_exception(hass, ezviz_config_flow): USER_INPUT_VALIDATE, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -252,7 +248,7 @@ async def test_discover_exception_step1( context={"source": SOURCE_INTEGRATION_DISCOVERY}, data={ATTR_SERIAL: "C66666", CONF_IP_ADDRESS: "test-ip"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -267,7 +263,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -281,7 +277,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_host"} @@ -295,7 +291,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_host"} @@ -309,7 +305,7 @@ async def test_discover_exception_step1( }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -327,7 +323,7 @@ async def test_discover_exception_step3( context={"source": SOURCE_INTEGRATION_DISCOVERY}, data={ATTR_SERIAL: "C66666", CONF_IP_ADDRESS: "test-ip"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -342,7 +338,7 @@ async def test_discover_exception_step3( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_auth"} @@ -356,7 +352,7 @@ async def test_discover_exception_step3( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {"base": "invalid_host"} @@ -370,7 +366,7 @@ async def test_discover_exception_step3( }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -390,7 +386,7 @@ async def test_user_custom_url_exception(hass, ezviz_config_flow): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {} @@ -399,7 +395,7 @@ async def test_user_custom_url_exception(hass, ezviz_config_flow): {CONF_URL: "test-user"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {"base": "invalid_auth"} @@ -410,7 +406,7 @@ async def test_user_custom_url_exception(hass, ezviz_config_flow): {CONF_URL: "test-user"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {"base": "invalid_host"} @@ -421,7 +417,7 @@ async def test_user_custom_url_exception(hass, ezviz_config_flow): {CONF_URL: "test-user"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user_custom_url" assert result["errors"] == {"base": "cannot_connect"} @@ -432,5 +428,5 @@ async def test_user_custom_url_exception(hass, ezviz_config_flow): {CONF_URL: "test-user"}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/filesize/test_config_flow.py b/tests/components/filesize/test_config_flow.py index 16f0ab38dc1..ed9d4004b1a 100644 --- a/tests/components/filesize/test_config_flow.py +++ b/tests/components/filesize/test_config_flow.py @@ -5,11 +5,7 @@ from homeassistant.components.filesize.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_FILE_PATH from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import TEST_DIR, TEST_FILE, TEST_FILE_NAME, async_create_file @@ -24,7 +20,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -33,7 +29,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: user_input={CONF_FILE_PATH: TEST_FILE}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == TEST_FILE_NAME assert result2.get("data") == {CONF_FILE_PATH: TEST_FILE} @@ -51,7 +47,7 @@ async def test_unique_path( DOMAIN, context={"source": SOURCE_USER}, data={CONF_FILE_PATH: TEST_FILE} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -64,7 +60,7 @@ async def test_flow_fails_on_validation(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER result2 = await hass.config_entries.flow.async_configure( @@ -103,7 +99,7 @@ async def test_flow_fails_on_validation(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == TEST_FILE_NAME assert result2["data"] == { CONF_FILE_PATH: TEST_FILE, diff --git a/tests/components/fivem/test_config_flow.py b/tests/components/fivem/test_config_flow.py index a1a1e8f2a37..121b416a110 100644 --- a/tests/components/fivem/test_config_flow.py +++ b/tests/components/fivem/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components.fivem.config_flow import DEFAULT_PORT from homeassistant.components.fivem.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType USER_INPUT = { CONF_HOST: "fivem.dummyserver.com", @@ -54,7 +54,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" @@ -63,7 +63,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -79,7 +79,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == USER_INPUT[CONF_HOST] assert result2["data"] == USER_INPUT assert len(mock_setup_entry.mock_calls) == 1 @@ -101,7 +101,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -121,7 +121,7 @@ async def test_form_invalid(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -141,5 +141,5 @@ async def test_form_invalid_game_name(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_game_name"} diff --git a/tests/components/fjaraskupan/test_config_flow.py b/tests/components/fjaraskupan/test_config_flow.py index 22808382c49..a51b8c6f9fa 100644 --- a/tests/components/fjaraskupan/test_config_flow.py +++ b/tests/components/fjaraskupan/test_config_flow.py @@ -9,11 +9,7 @@ from pytest import fixture from homeassistant import config_entries from homeassistant.components.fjaraskupan.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 homeassistant.data_entry_flow import FlowResultType @fixture(name="mock_setup_entry", autouse=True) @@ -32,10 +28,10 @@ async def test_configure(hass: HomeAssistant, mock_setup_entry) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Fjäråskupan" assert result["data"] == {} @@ -51,8 +47,8 @@ async def test_scan_no_devices(hass: HomeAssistant, scanner: list[BLEDevice]) -> DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index 9750600518e..8abdb8e955b 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -24,7 +24,7 @@ from homeassistant.components.flux_led.const import ( ) from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MODEL from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from . import ( DEFAULT_ENTRY_TITLE, @@ -386,7 +386,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): data=FLUX_DISCOVERY, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_wifibulb(): @@ -396,7 +396,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): data=DHCP_DISCOVERY, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_wifibulb(): @@ -410,7 +410,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" @@ -425,7 +425,7 @@ async def test_discovered_by_discovery(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_wifibulb(), patch( @@ -462,7 +462,7 @@ async def test_discovered_by_dhcp_udp_responds(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_wifibulb(), patch( @@ -499,7 +499,7 @@ async def test_discovered_by_dhcp_no_udp_response(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(no_device=True), _patch_wifibulb(), patch( @@ -529,7 +529,7 @@ async def test_discovered_by_dhcp_partial_udp_response_fallback_tcp(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(device=FLUX_DISCOVERY_PARTIAL), _patch_wifibulb(), patch( @@ -560,7 +560,7 @@ async def test_discovered_by_dhcp_no_udp_response_or_tcp_response(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -584,7 +584,7 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS @@ -605,7 +605,7 @@ async def test_mac_address_off_by_one_updated_via_discovery(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS @@ -624,7 +624,7 @@ async def test_mac_address_off_by_one_not_updated_from_dhcp(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS_ONE_OFF @@ -652,7 +652,7 @@ async def test_discovered_by_dhcp_or_discovery_mac_address_mismatch_host_already ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == MAC_ADDRESS_DIFFERENT diff --git a/tests/components/forecast_solar/test_config_flow.py b/tests/components/forecast_solar/test_config_flow.py index f0611d1678d..2380e65aabb 100644 --- a/tests/components/forecast_solar/test_config_flow.py +++ b/tests/components/forecast_solar/test_config_flow.py @@ -12,7 +12,7 @@ from homeassistant.components.forecast_solar.const import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -23,7 +23,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -42,7 +42,7 @@ async def test_user_flow(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Name" assert result2.get("data") == { CONF_LATITUDE: 52.42, @@ -70,7 +70,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -86,7 +86,7 @@ async def test_options_flow( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("data") == { CONF_API_KEY: "solarPOWER!", CONF_DECLINATION: 21, diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 619f06f5493..76f556d0743 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -18,11 +18,7 @@ from homeassistant.components.ssdp import ATTR_UPNP_UDN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .const import ( MOCK_FIRMWARE_INFO, @@ -62,13 +58,13 @@ async def test_user(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" @@ -113,13 +109,13 @@ async def test_user_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "already_configured" @@ -130,7 +126,7 @@ async def test_exception_security(hass: HomeAssistant, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -142,7 +138,7 @@ async def test_exception_security(hass: HomeAssistant, mock_get_source_ip): result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == ERROR_AUTH_INVALID @@ -153,7 +149,7 @@ async def test_exception_connection(hass: HomeAssistant, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -165,7 +161,7 @@ async def test_exception_connection(hass: HomeAssistant, mock_get_source_ip): result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == ERROR_CANNOT_CONNECT @@ -176,7 +172,7 @@ async def test_exception_unknown(hass: HomeAssistant, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -188,7 +184,7 @@ async def test_exception_unknown(hass: HomeAssistant, mock_get_source_ip): result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == ERROR_UNKNOWN @@ -226,7 +222,7 @@ async def test_reauth_successful( data=mock_config.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -237,7 +233,7 @@ async def test_reauth_successful( }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_setup_entry.called @@ -262,7 +258,7 @@ async def test_reauth_not_successful( data=mock_config.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -273,7 +269,7 @@ async def test_reauth_not_successful( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"]["base"] == "cannot_connect" @@ -301,7 +297,7 @@ async def test_ssdp_already_configured( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -328,7 +324,7 @@ async def test_ssdp_already_configured_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -355,7 +351,7 @@ async def test_ssdp_already_configured_host_uuid( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -371,7 +367,7 @@ async def test_ssdp_already_in_progress_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" MOCK_NO_UNIQUE_ID = dataclasses.replace(MOCK_SSDP_DATA) @@ -380,7 +376,7 @@ async def test_ssdp_already_in_progress_host( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_UNIQUE_ID ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -408,7 +404,7 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -419,7 +415,7 @@ async def test_ssdp(hass: HomeAssistant, fc_class_mock, mock_get_source_ip): }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_HOST] == MOCK_IPS["fritz.box"] assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" @@ -437,7 +433,7 @@ async def test_ssdp_exception(hass: HomeAssistant, mock_get_source_ip): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -448,7 +444,7 @@ async def test_ssdp_exception(hass: HomeAssistant, mock_get_source_ip): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" @@ -463,7 +459,7 @@ async def test_options_flow(hass: HomeAssistant, fc_class_mock, mock_get_source_ side_effect=fc_class_mock, ), patch("homeassistant.components.fritz.common.FritzBoxTools"): result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_init(mock_config.entry_id) @@ -473,5 +469,5 @@ async def test_options_flow(hass: HomeAssistant, fc_class_mock, mock_get_source_ CONF_CONSIDER_HOME: 37, }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert mock_config.options[CONF_CONSIDER_HOME] == 37 diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 442b2f4d568..e82101b3977 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -14,11 +14,7 @@ from homeassistant.components.ssdp import ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_UDN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .const import CONF_FAKE_NAME, MOCK_CONFIG @@ -70,13 +66,13 @@ async def test_user(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "10.0.0.1" assert result["data"][CONF_HOST] == "10.0.0.1" assert result["data"][CONF_PASSWORD] == "fake_pass" @@ -91,7 +87,7 @@ async def test_user_auth_failed(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"]["base"] == "invalid_auth" @@ -103,7 +99,7 @@ async def test_user_not_successful(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -112,13 +108,13 @@ async def test_user_already_configured(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert not result["result"].unique_id result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -132,7 +128,7 @@ async def test_reauth_success(hass: HomeAssistant, fritz: Mock): context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, data=mock_config.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -143,7 +139,7 @@ async def test_reauth_success(hass: HomeAssistant, fritz: Mock): }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert mock_config.data[CONF_USERNAME] == "other_fake_user" assert mock_config.data[CONF_PASSWORD] == "other_fake_password" @@ -161,7 +157,7 @@ async def test_reauth_auth_failed(hass: HomeAssistant, fritz: Mock): context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, data=mock_config.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -172,7 +168,7 @@ async def test_reauth_auth_failed(hass: HomeAssistant, fritz: Mock): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"]["base"] == "invalid_auth" @@ -189,7 +185,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, fritz: Mock): context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, data=mock_config.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -200,16 +196,16 @@ async def test_reauth_not_successful(hass: HomeAssistant, fritz: Mock): }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" @pytest.mark.parametrize( "test_data,expected_result", [ - (MOCK_SSDP_DATA["ip4_valid"], RESULT_TYPE_FORM), - (MOCK_SSDP_DATA["ip6_valid"], RESULT_TYPE_FORM), - (MOCK_SSDP_DATA["ip6_invalid"], RESULT_TYPE_ABORT), + (MOCK_SSDP_DATA["ip4_valid"], FlowResultType.FORM), + (MOCK_SSDP_DATA["ip6_valid"], FlowResultType.FORM), + (MOCK_SSDP_DATA["ip6_invalid"], FlowResultType.ABORT), ], ) async def test_ssdp( @@ -224,7 +220,7 @@ async def test_ssdp( ) assert result["type"] == expected_result - if expected_result == RESULT_TYPE_ABORT: + if expected_result == FlowResultType.ABORT: return assert result["step_id"] == "confirm" @@ -233,7 +229,7 @@ async def test_ssdp( result["flow_id"], user_input={CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == CONF_FAKE_NAME assert result["data"][CONF_HOST] == urlparse(test_data.ssdp_location).hostname assert result["data"][CONF_PASSWORD] == "fake_pass" @@ -249,14 +245,14 @@ async def test_ssdp_no_friendly_name(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_NAME ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "10.0.0.1" assert result["data"][CONF_HOST] == "10.0.0.1" assert result["data"][CONF_PASSWORD] == "fake_pass" @@ -271,7 +267,7 @@ async def test_ssdp_auth_failed(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] == {} @@ -279,7 +275,7 @@ async def test_ssdp_auth_failed(hass: HomeAssistant, fritz: Mock): result["flow_id"], user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"]["base"] == "invalid_auth" @@ -291,14 +287,14 @@ async def test_ssdp_not_successful(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -309,14 +305,14 @@ async def test_ssdp_not_supported(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_supported" @@ -325,13 +321,13 @@ async def test_ssdp_already_in_progress_unique_id(hass: HomeAssistant, fritz: Mo result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -340,7 +336,7 @@ async def test_ssdp_already_in_progress_host(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" MOCK_NO_UNIQUE_ID = dataclasses.replace(MOCK_SSDP_DATA["ip4_valid"]) @@ -349,7 +345,7 @@ async def test_ssdp_already_in_progress_host(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_NO_UNIQUE_ID ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -358,12 +354,12 @@ async def test_ssdp_already_configured(hass: HomeAssistant, fritz: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert not result["result"].unique_id result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=MOCK_SSDP_DATA["ip4_valid"] ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" assert result["result"].unique_id == "only-a-test" diff --git a/tests/components/fritzbox_callmonitor/test_config_flow.py b/tests/components/fritzbox_callmonitor/test_config_flow.py index 51884abe96f..94d5bdc8eeb 100644 --- a/tests/components/fritzbox_callmonitor/test_config_flow.py +++ b/tests/components/fritzbox_callmonitor/test_config_flow.py @@ -22,11 +22,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, patch @@ -74,7 +70,7 @@ async def test_setup_one_phonebook(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -104,7 +100,7 @@ async def test_setup_one_phonebook(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_PHONEBOOK_NAME_1 assert result["data"] == MOCK_CONFIG_ENTRY assert len(mock_setup_entry.mock_calls) == 1 @@ -116,7 +112,7 @@ async def test_setup_multiple_phonebooks(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -140,7 +136,7 @@ async def test_setup_multiple_phonebooks(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "phonebook" assert result["errors"] == {} @@ -156,7 +152,7 @@ async def test_setup_multiple_phonebooks(hass: HomeAssistant) -> None: {CONF_PHONEBOOK: MOCK_PHONEBOOK_NAME_2}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_PHONEBOOK_NAME_2 assert result["data"] == { CONF_HOST: MOCK_HOST, @@ -184,7 +180,7 @@ async def test_setup_cannot_connect(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == ConnectResult.NO_DEVIES_FOUND @@ -203,7 +199,7 @@ async def test_setup_insufficient_permissions(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == ConnectResult.INSUFFICIENT_PERMISSIONS @@ -222,7 +218,7 @@ async def test_setup_invalid_auth(hass: HomeAssistant) -> None: result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": ConnectResult.INVALID_AUTH} @@ -244,14 +240,14 @@ async def test_options_flow_correct_prefixes(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_PREFIXES: "+49, 491234"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_PREFIXES: ["+49", "491234"]} @@ -273,14 +269,14 @@ async def test_options_flow_incorrect_prefixes(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_PREFIXES: ""} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": ConnectResult.MALFORMED_PREFIXES} @@ -302,12 +298,12 @@ async def test_options_flow_no_prefixes(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert config_entry.options == {CONF_PREFIXES: None} diff --git a/tests/components/fronius/test_config_flow.py b/tests/components/fronius/test_config_flow.py index 256d64d4cbe..69f1dffa64b 100644 --- a/tests/components/fronius/test_config_flow.py +++ b/tests/components/fronius/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.fronius.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 homeassistant.data_entry_flow import FlowResultType from . import mock_responses @@ -51,7 +47,7 @@ async def test_form_with_logger(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -69,7 +65,7 @@ async def test_form_with_logger(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "SolarNet Datalogger at 10.9.8.1" assert result2["data"] == { "host": "10.9.8.1", @@ -83,7 +79,7 @@ async def test_form_with_inverter(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -104,7 +100,7 @@ async def test_form_with_inverter(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "SolarNet Inverter at 10.9.1.1" assert result2["data"] == { "host": "10.9.1.1", @@ -133,7 +129,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -157,7 +153,7 @@ async def test_form_no_device(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -178,7 +174,7 @@ async def test_form_unexpected(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -206,7 +202,7 @@ async def test_form_already_existing(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -246,7 +242,7 @@ async def test_form_updates_host(hass, aioclient_mock): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" mock_unload_entry.assert_called_with(hass, entry) @@ -269,13 +265,13 @@ async def test_dhcp(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm_discovery" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"SolarNet Datalogger at {MOCK_DHCP_DATA.ip}" assert result["data"] == { "host": MOCK_DHCP_DATA.ip, @@ -298,7 +294,7 @@ async def test_dhcp_already_configured(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -313,5 +309,5 @@ async def test_dhcp_invalid(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "invalid_host" diff --git a/tests/components/garages_amsterdam/test_config_flow.py b/tests/components/garages_amsterdam/test_config_flow.py index 3749cf039db..3dd9c67bc15 100644 --- a/tests/components/garages_amsterdam/test_config_flow.py +++ b/tests/components/garages_amsterdam/test_config_flow.py @@ -8,11 +8,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.garages_amsterdam.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 homeassistant.data_entry_flow import FlowResultType async def test_full_flow(hass: HomeAssistant) -> None: @@ -21,7 +17,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result with patch( @@ -34,7 +30,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "IJDok" assert "result" in result2 assert result2["result"].unique_id == "IJDok" @@ -63,5 +59,5 @@ async def test_error_handling( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == reason diff --git a/tests/components/geocaching/test_config_flow.py b/tests/components/geocaching/test_config_flow.py index 56a301e2f3c..a40668c627f 100644 --- a/tests/components/geocaching/test_config_flow.py +++ b/tests/components/geocaching/test_config_flow.py @@ -17,7 +17,7 @@ from homeassistant.components.geocaching.const import ( ) from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_EXTERNAL_STEP +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.setup import async_setup_component @@ -63,7 +63,7 @@ async def test_full_flow( }, ) - assert result.get("type") == RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == FlowResultType.EXTERNAL_STEP assert result.get("step_id") == "auth" assert result.get("url") == ( f"{CURRENT_ENVIRONMENT_URLS['authorize_url']}?response_type=code&client_id={CLIENT_ID}" @@ -161,7 +161,7 @@ async def test_oauth_error( "redirect_uri": REDIRECT_URI, }, ) - assert result.get("type") == RESULT_TYPE_EXTERNAL_STEP + assert result.get("type") == FlowResultType.EXTERNAL_STEP client = await hass_client_no_auth() resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") @@ -181,7 +181,7 @@ async def test_oauth_error( ) result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "oauth_error" assert len(hass.config_entries.async_entries(DOMAIN)) == 0 diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index 2bf0fac209f..174d70c3ae3 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -12,12 +12,7 @@ from homeassistant.components.github.const import ( DOMAIN, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_SHOW_PROGRESS, - RESULT_TYPE_SHOW_PROGRESS_DONE, -) +from homeassistant.data_entry_flow import FlowResultType from .common import MOCK_ACCESS_TOKEN @@ -63,7 +58,7 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "device" - assert result["type"] == RESULT_TYPE_SHOW_PROGRESS + assert result["type"] == FlowResultType.SHOW_PROGRESS assert "flow_id" in result result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -76,7 +71,7 @@ async def test_full_user_flow_implementation( ) assert result["title"] == "" - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_ACCESS_TOKEN] == MOCK_ACCESS_TOKEN assert "options" in result @@ -96,7 +91,7 @@ async def test_flow_with_registration_failure( DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result.get("reason") == "could_not_register" @@ -125,10 +120,10 @@ async def test_flow_with_activation_failure( context={"source": config_entries.SOURCE_USER}, ) assert result["step_id"] == "device" - assert result["type"] == RESULT_TYPE_SHOW_PROGRESS + assert result["type"] == FlowResultType.SHOW_PROGRESS result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == RESULT_TYPE_SHOW_PROGRESS_DONE + assert result["type"] == FlowResultType.SHOW_PROGRESS_DONE assert result["step_id"] == "could_not_register" @@ -144,7 +139,7 @@ async def test_already_configured( context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index 713295c0efd..cbe40a2b9cb 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -20,11 +20,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import _mocked_ismartgate_closed_door_response @@ -59,7 +55,7 @@ async def test_auth_fail( }, ) assert result - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == { "base": "invalid_auth", } @@ -79,7 +75,7 @@ async def test_auth_fail( }, ) assert result - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} api.reset_mock() @@ -97,7 +93,7 @@ async def test_auth_fail( }, ) assert result - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -117,7 +113,7 @@ async def test_form_homekit_unique_id_already_setup(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} flow = next( flow @@ -145,7 +141,7 @@ async def test_form_homekit_unique_id_already_setup(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT async def test_form_homekit_ip_address_already_setup(hass): @@ -170,7 +166,7 @@ async def test_form_homekit_ip_address_already_setup(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT async def test_form_homekit_ip_address(hass): @@ -189,7 +185,7 @@ async def test_form_homekit_ip_address(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} data_schema = result["data_schema"] @@ -219,7 +215,7 @@ async def test_discovered_dhcp( ip="1.2.3.4", macaddress=MOCK_MAC_ADDR, hostname="mock_hostname" ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -231,7 +227,7 @@ async def test_discovered_dhcp( }, ) assert result2 - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} api.reset_mock() @@ -247,7 +243,7 @@ async def test_discovered_dhcp( }, ) assert result3 - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["data"] == { "device": "ismartgate", "ip_address": "1.2.3.4", @@ -272,7 +268,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): type="mock_type", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_init( @@ -282,7 +278,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): ip="1.2.3.4", macaddress=MOCK_MAC_ADDR, hostname="mock_hostname" ), ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" result3 = await hass.config_entries.flow.async_init( @@ -292,5 +288,5 @@ async def test_discovered_by_homekit_and_dhcp(hass): ip="1.2.3.4", macaddress="00:00:00:00:00:00", hostname="mock_hostname" ), ) - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" diff --git a/tests/components/goodwe/test_config_flow.py b/tests/components/goodwe/test_config_flow.py index 89dfd68a783..fe1af93a472 100644 --- a/tests/components/goodwe/test_config_flow.py +++ b/tests/components/goodwe/test_config_flow.py @@ -11,11 +11,7 @@ from homeassistant.components.goodwe.const import ( from homeassistant.config_entries import SOURCE_USER 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 homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -35,7 +31,7 @@ async def test_manual_setup(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -50,7 +46,7 @@ async def test_manual_setup(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == DEFAULT_NAME assert result["data"] == { CONF_HOST: TEST_HOST, @@ -68,7 +64,7 @@ async def test_manual_setup_already_exists(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -81,7 +77,7 @@ async def test_manual_setup_already_exists(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -90,7 +86,7 @@ async def test_manual_setup_device_offline(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -103,5 +99,5 @@ async def test_manual_setup_device_offline(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {CONF_HOST: "connection_error"} diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 9d6b099557d..4c73e1d5add 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -6,11 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.group import DOMAIN, async_setup_entry from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, - RESULT_TYPE_MENU, -) +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry @@ -47,14 +43,14 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_MENU + assert result["type"] == FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": group_type}, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == group_type with patch( @@ -70,7 +66,7 @@ async def test_config_flow( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room" assert result["data"] == {} assert result["options"] == { @@ -136,14 +132,14 @@ async def test_config_flow_hides_members( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_MENU + assert result["type"] == FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": group_type}, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == group_type result = await hass.config_entries.flow.async_configure( @@ -157,7 +153,7 @@ async def test_config_flow_hides_members( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert registry.async_get(f"{group_type}.one").hidden_by == hidden_by assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by @@ -220,7 +216,7 @@ async def test_options( config_entry = hass.config_entries.async_entries(DOMAIN)[0] result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == group_type assert get_suggested(result["data_schema"].schema, "entities") == members1 assert "name" not in result["data_schema"].schema @@ -234,7 +230,7 @@ async def test_options( "entities": members2, }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "entities": members2, "group_type": group_type, @@ -261,14 +257,14 @@ async def test_options( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_MENU + assert result["type"] == FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], {"next_step_id": group_type}, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == group_type assert get_suggested(result["data_schema"].schema, "entities") is None @@ -318,7 +314,7 @@ async def test_all_options( result = await hass.config_entries.options.async_init( config_entry.entry_id, context={"show_advanced_options": advanced} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == group_type result = await hass.config_entries.options.async_configure( @@ -327,7 +323,7 @@ async def test_all_options( "entities": members2, }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "entities": members2, "group_type": group_type, @@ -414,7 +410,7 @@ async def test_options_flow_hides_members( await hass.async_block_till_done() result = await hass.config_entries.options.async_init(group_config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -425,7 +421,7 @@ async def test_options_flow_hides_members( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert registry.async_get(f"{group_type}.one").hidden_by == hidden_by assert registry.async_get(f"{group_type}.three").hidden_by == hidden_by diff --git a/tests/components/hardkernel/test_config_flow.py b/tests/components/hardkernel/test_config_flow.py index f74b4a4e658..309c796fcc3 100644 --- a/tests/components/hardkernel/test_config_flow.py +++ b/tests/components/hardkernel/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant.components.hardkernel.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, MockModule, mock_integration @@ -20,7 +20,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Hardkernel" assert result["data"] == {} assert result["options"] == {} @@ -53,6 +53,6 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() diff --git a/tests/components/here_travel_time/test_config_flow.py b/tests/components/here_travel_time/test_config_flow.py index dc9eedfa3f6..23e3c1c81c7 100644 --- a/tests/components/here_travel_time/test_config_flow.py +++ b/tests/components/here_travel_time/test_config_flow.py @@ -154,7 +154,7 @@ async def test_step_user(hass: HomeAssistant, menu_options) -> None: ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_MENU + assert result2["type"] == data_entry_flow.FlowResultType.MENU assert result2["menu_options"] == menu_options @@ -178,7 +178,7 @@ async def test_step_origin_coordinates( } }, ) - assert location_selector_result["type"] == data_entry_flow.RESULT_TYPE_MENU + assert location_selector_result["type"] == data_entry_flow.FlowResultType.MENU @pytest.mark.usefixtures("valid_response") @@ -195,7 +195,7 @@ async def test_step_origin_entity( menu_result["flow_id"], {"origin_entity_id": "zone.home"}, ) - assert entity_selector_result["type"] == data_entry_flow.RESULT_TYPE_MENU + assert entity_selector_result["type"] == data_entry_flow.FlowResultType.MENU @pytest.mark.usefixtures("valid_response") @@ -341,7 +341,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_MENU + assert result["type"] == data_entry_flow.FlowResultType.MENU @pytest.mark.usefixtures("valid_response") diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index 2e96b05a919..e6d5da11806 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant.components.homeassistant_yellow.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, MockModule, mock_integration @@ -20,7 +20,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Home Assistant Yellow" assert result["data"] == {} assert result["options"] == {} @@ -53,6 +53,6 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 3b106dab186..170a2a797d0 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -15,7 +15,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import KNOWN_DEVICES -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry from tests.common import MockConfigEntry, mock_device_registry @@ -943,10 +943,10 @@ async def test_mdns_update_to_paired_during_pairing(hass, controller): context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info_paired, ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_paired" mdns_update_to_paired.set() result = await task - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Koogeek-LS1-20833F" assert result["data"] == {} diff --git a/tests/components/homewizard/test_config_flow.py b/tests/components/homewizard/test_config_flow.py index fca00b71892..cdd67cbf7ea 100644 --- a/tests/components/homewizard/test_config_flow.py +++ b/tests/components/homewizard/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.homewizard.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .generator import get_mock_device @@ -95,7 +91,7 @@ async def test_discovery_flow_works(hass, aioclient_mock): result = await hass.config_entries.flow.async_configure( flow["flow_id"], user_input=None ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" with patch( @@ -109,7 +105,7 @@ async def test_discovery_flow_works(hass, aioclient_mock): flow["flow_id"], user_input={"ip_address": "192.168.43.183"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "P1 meter (aabbccddeeff)" assert result["data"][CONF_IP_ADDRESS] == "192.168.43.183" @@ -176,7 +172,7 @@ async def test_discovery_disabled_api(hass, aioclient_mock): data=service_info, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM with patch( "homeassistant.components.homewizard.async_setup_entry", @@ -189,7 +185,7 @@ async def test_discovery_disabled_api(hass, aioclient_mock): result["flow_id"], user_input={"ip_address": "192.168.43.183"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "api_not_enabled" @@ -218,7 +214,7 @@ async def test_discovery_missing_data_in_service_info(hass, aioclient_mock): data=service_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "invalid_discovery_parameters" @@ -247,7 +243,7 @@ async def test_discovery_invalid_api(hass, aioclient_mock): data=service_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unsupported_api_version" @@ -275,7 +271,7 @@ async def test_check_disabled_api(hass, aioclient_mock): result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "api_not_enabled" @@ -303,7 +299,7 @@ async def test_check_error_handling_api(hass, aioclient_mock): result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown_error" @@ -331,7 +327,7 @@ async def test_check_detects_invalid_api(hass, aioclient_mock): result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unsupported_api_version" @@ -359,5 +355,5 @@ async def test_check_requesterror(hass, aioclient_mock): result["flow_id"], {CONF_IP_ADDRESS: "2.2.2.2"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown_error" diff --git a/tests/components/integration/test_config_flow.py b/tests/components/integration/test_config_flow.py index 5992d480f80..9ad15096ad3 100644 --- a/tests/components/integration/test_config_flow.py +++ b/tests/components/integration/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.integration.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -39,7 +39,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My integration" assert result["data"] == {} assert result["options"] == { @@ -98,7 +98,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "round") == 1.0 @@ -109,7 +109,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "round": 2.0, }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "method": "left", "name": "My integration", diff --git a/tests/components/intellifire/test_config_flow.py b/tests/components/intellifire/test_config_flow.py index 06fcbea5bfa..95e5d735c35 100644 --- a/tests/components/intellifire/test_config_flow.py +++ b/tests/components/intellifire/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.intellifire.config_flow import MANUAL_ENTRY_STRING from homeassistant.components.intellifire.const import CONF_USER_ID, DOMAIN from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.components.intellifire.conftest import mock_api_connection_error @@ -38,7 +34,7 @@ async def test_no_discovery( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == "manual_device_entry" @@ -50,7 +46,7 @@ async def test_no_discovery( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "api_config" result3 = await hass.config_entries.flow.async_configure( @@ -59,7 +55,7 @@ async def test_no_discovery( ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Fireplace 12345" assert result3["data"] == { CONF_HOST: "1.1.1.1", @@ -100,7 +96,7 @@ async def test_single_discovery( {CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM assert result3["errors"] == {"base": "iftapi_connect"} @@ -133,7 +129,7 @@ async def test_single_discovery_loign_error( {CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM assert result3["errors"] == {"base": "api_error"} @@ -199,14 +195,14 @@ async def test_multi_discovery_cannot_connect( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pick_device" result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.33"} ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -221,7 +217,7 @@ async def test_form_cannot_connect_manual_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "manual_device_entry" result2 = await hass.config_entries.flow.async_configure( @@ -231,7 +227,7 @@ async def test_form_cannot_connect_manual_entry( }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -266,7 +262,7 @@ async def test_picker_already_discovered( CONF_HOST: "192.168.1.4", }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert len(mock_setup_entry.mock_calls) == 0 @@ -303,7 +299,7 @@ async def test_reauth_flow( }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_config" result3 = await hass.config_entries.flow.async_configure( @@ -311,7 +307,7 @@ async def test_reauth_flow( {CONF_USERNAME: "test", CONF_PASSWORD: "AROONIE"}, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert entry.data[CONF_PASSWORD] == "AROONIE" assert entry.data[CONF_USERNAME] == "test" @@ -331,10 +327,10 @@ async def test_dhcp_discovery_intellifire_device( hostname="zentrios-Test", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "dhcp_confirm" result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "dhcp_confirm" result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input={} diff --git a/tests/components/iotawatt/test_config_flow.py b/tests/components/iotawatt/test_config_flow.py index 6adb3534b15..06c9cce0da9 100644 --- a/tests/components/iotawatt/test_config_flow.py +++ b/tests/components/iotawatt/test_config_flow.py @@ -6,7 +6,7 @@ import httpx 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 +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -15,7 +15,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -34,7 +34,7 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == { "host": "1.1.1.1", } @@ -46,7 +46,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -59,7 +59,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "auth" with patch( @@ -75,7 +75,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM assert result3["step_id"] == "auth" assert result3["errors"] == {"base": "invalid_auth"} @@ -95,7 +95,7 @@ async def test_form_auth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == FlowResultType.CREATE_ENTRY assert len(mock_setup_entry.mock_calls) == 1 assert result4["data"] == { "host": "1.1.1.1", @@ -119,7 +119,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {"host": "1.1.1.1"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -138,5 +138,5 @@ async def test_form_setup_exception(hass: HomeAssistant) -> None: {"host": "1.1.1.1"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 8eefa4251f7..a37fdc9de52 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -6,11 +6,7 @@ from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SSL from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( MOCK_USER_INPUT, @@ -31,7 +27,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_show_zeroconf_form( @@ -48,7 +44,7 @@ async def test_show_zeroconf_form( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "EPSON XP-6000 Series"} @@ -66,7 +62,7 @@ async def test_connection_error( ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -83,7 +79,7 @@ async def test_zeroconf_connection_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -98,7 +94,7 @@ async def test_zeroconf_confirm_connection_error( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -116,7 +112,7 @@ async def test_user_connection_upgrade_required( ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "connection_upgrade"} @@ -133,7 +129,7 @@ async def test_zeroconf_connection_upgrade_required( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "connection_upgrade" @@ -150,7 +146,7 @@ async def test_user_parse_error( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "parse_error" @@ -167,7 +163,7 @@ async def test_zeroconf_parse_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "parse_error" @@ -184,7 +180,7 @@ async def test_user_ipp_error( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ipp_error" @@ -201,7 +197,7 @@ async def test_zeroconf_ipp_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ipp_error" @@ -218,7 +214,7 @@ async def test_user_ipp_version_error( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ipp_version_error" @@ -235,7 +231,7 @@ async def test_zeroconf_ipp_version_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ipp_version_error" @@ -252,7 +248,7 @@ async def test_user_device_exists_abort( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -269,7 +265,7 @@ async def test_zeroconf_device_exists_abort( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -291,7 +287,7 @@ async def test_zeroconf_with_uuid_device_exists_abort( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -312,7 +308,7 @@ async def test_zeroconf_empty_unique_id( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_zeroconf_no_unique_id( @@ -328,7 +324,7 @@ async def test_zeroconf_no_unique_id( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_full_user_flow_implementation( @@ -343,7 +339,7 @@ async def test_full_user_flow_implementation( ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM with patch("homeassistant.components.ipp.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( @@ -351,7 +347,7 @@ async def test_full_user_flow_implementation( user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.31" assert result["data"] @@ -376,14 +372,14 @@ async def test_full_zeroconf_flow_implementation( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM with patch("homeassistant.components.ipp.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "EPSON XP-6000 Series" assert result["data"] @@ -410,7 +406,7 @@ async def test_full_zeroconf_tls_flow_implementation( ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["description_placeholders"] == {CONF_NAME: "EPSON XP-6000 Series"} with patch("homeassistant.components.ipp.async_setup_entry", return_value=True): @@ -418,7 +414,7 @@ async def test_full_zeroconf_tls_flow_implementation( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "EPSON XP-6000 Series" assert result["data"] diff --git a/tests/components/kaleidescape/test_config_flow.py b/tests/components/kaleidescape/test_config_flow.py index a2cf8091d02..8171ed0955b 100644 --- a/tests/components/kaleidescape/test_config_flow.py +++ b/tests/components/kaleidescape/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components.kaleidescape.const import DOMAIN from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER 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 homeassistant.data_entry_flow import FlowResultType from . import MOCK_HOST, MOCK_SSDP_DISCOVERY_INFO @@ -25,7 +21,7 @@ async def test_user_config_flow_success( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -33,7 +29,7 @@ async def test_user_config_flow_success( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == MOCK_HOST @@ -48,7 +44,7 @@ async def test_user_config_flow_bad_connect_errors( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -63,7 +59,7 @@ async def test_user_config_flow_unsupported_device_errors( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unsupported"} @@ -75,7 +71,7 @@ async def test_user_config_flow_device_exists_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: MOCK_HOST} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -87,7 +83,7 @@ async def test_ssdp_config_flow_success( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( @@ -95,7 +91,7 @@ async def test_ssdp_config_flow_success( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == MOCK_HOST @@ -111,7 +107,7 @@ async def test_ssdp_config_flow_bad_connect_aborts( DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -126,5 +122,5 @@ async def test_ssdp_config_flow_unsupported_device_aborts( DOMAIN, context={"source": SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unsupported" diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 8d5d57567dc..30b8aa537a6 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -41,11 +41,7 @@ from homeassistant.components.knx.const import ( ) from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, - RESULT_TYPE_MENU, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -84,7 +80,7 @@ async def test_routing_setup(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -94,7 +90,7 @@ async def test_routing_setup(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "routing" assert not result2["errors"] @@ -111,7 +107,7 @@ async def test_routing_setup(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == CONF_KNX_ROUTING.capitalize() assert result3["data"] == { **DEFAULT_ENTRY_DATA, @@ -136,7 +132,7 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: "show_advanced_options": True, }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -146,7 +142,7 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "routing" assert not result2["errors"] @@ -161,7 +157,7 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result_invalid_input["type"] == RESULT_TYPE_FORM + assert result_invalid_input["type"] == FlowResultType.FORM assert result_invalid_input["step_id"] == "routing" assert result_invalid_input["errors"] == { CONF_KNX_MCAST_GRP: "invalid_ip_address", @@ -184,7 +180,7 @@ async def test_routing_setup_advanced(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == CONF_KNX_ROUTING.capitalize() assert result3["data"] == { **DEFAULT_ENTRY_DATA, @@ -261,7 +257,7 @@ async def test_tunneling_setup( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -271,7 +267,7 @@ async def test_tunneling_setup( }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual_tunnel" assert not result2["errors"] @@ -284,7 +280,7 @@ async def test_tunneling_setup( user_input, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Tunneling @ 192.168.0.1" assert result3["data"] == config_entry_data @@ -303,7 +299,7 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: "show_advanced_options": True, }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -313,7 +309,7 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual_tunnel" assert not result2["errors"] @@ -328,7 +324,7 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result_invalid_host["type"] == RESULT_TYPE_FORM + assert result_invalid_host["type"] == FlowResultType.FORM assert result_invalid_host["step_id"] == "manual_tunnel" assert result_invalid_host["errors"] == {CONF_HOST: "invalid_ip_address"} # invalid local ip address @@ -342,7 +338,7 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result_invalid_local["type"] == RESULT_TYPE_FORM + assert result_invalid_local["type"] == FlowResultType.FORM assert result_invalid_local["step_id"] == "manual_tunnel" assert result_invalid_local["errors"] == {CONF_KNX_LOCAL_IP: "invalid_ip_address"} @@ -361,7 +357,7 @@ async def test_tunneling_setup_for_local_ip(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Tunneling @ 192.168.0.2" assert result3["data"] == { **DEFAULT_ENTRY_DATA, @@ -385,7 +381,7 @@ async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] tunnel_flow = await hass.config_entries.flow.async_configure( @@ -395,7 +391,7 @@ async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) }, ) await hass.async_block_till_done() - assert tunnel_flow["type"] == RESULT_TYPE_FORM + assert tunnel_flow["type"] == FlowResultType.FORM assert tunnel_flow["step_id"] == "tunnel" assert not tunnel_flow["errors"] @@ -404,7 +400,7 @@ async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) {CONF_KNX_GATEWAY: str(gateway)}, ) await hass.async_block_till_done() - assert manual_tunnel["type"] == RESULT_TYPE_FORM + assert manual_tunnel["type"] == FlowResultType.FORM assert manual_tunnel["step_id"] == "manual_tunnel" with patch( @@ -420,7 +416,7 @@ async def test_tunneling_setup_for_multiple_found_gateways(hass: HomeAssistant) }, ) await hass.async_block_till_done() - assert manual_tunnel_flow["type"] == RESULT_TYPE_CREATE_ENTRY + assert manual_tunnel_flow["type"] == FlowResultType.CREATE_ENTRY assert manual_tunnel_flow["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING, @@ -441,7 +437,7 @@ async def test_manual_tunnel_step_when_no_gateway(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] tunnel_flow = await hass.config_entries.flow.async_configure( @@ -451,7 +447,7 @@ async def test_manual_tunnel_step_when_no_gateway(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert tunnel_flow["type"] == RESULT_TYPE_FORM + assert tunnel_flow["type"] == FlowResultType.FORM assert tunnel_flow["step_id"] == "manual_tunnel" assert not tunnel_flow["errors"] @@ -463,7 +459,7 @@ async def test_form_with_automatic_connection_handling(hass: HomeAssistant) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] with patch( @@ -478,7 +474,7 @@ async def test_form_with_automatic_connection_handling(hass: HomeAssistant) -> N ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == CONF_KNX_AUTOMATIC.capitalize() assert result2["data"] == { **DEFAULT_ENTRY_DATA, @@ -496,7 +492,7 @@ async def _get_menu_step(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -506,7 +502,7 @@ async def _get_menu_step(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual_tunnel" assert not result2["errors"] @@ -519,7 +515,7 @@ async def _get_menu_step(hass: HomeAssistant) -> None: }, ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_MENU + assert result3["type"] == FlowResultType.MENU assert result3["step_id"] == "secure_tunneling" return result3 @@ -532,7 +528,7 @@ async def test_configure_secure_manual(hass: HomeAssistant): menu_step["flow_id"], {"next_step_id": "secure_manual"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "secure_manual" assert not result["errors"] @@ -549,7 +545,7 @@ async def test_configure_secure_manual(hass: HomeAssistant): }, ) await hass.async_block_till_done() - assert secure_manual["type"] == RESULT_TYPE_CREATE_ENTRY + assert secure_manual["type"] == FlowResultType.CREATE_ENTRY assert secure_manual["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, @@ -574,7 +570,7 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant): menu_step["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "secure_knxkeys" assert not result["errors"] @@ -592,7 +588,7 @@ async def test_configure_secure_knxkeys(hass: HomeAssistant): }, ) await hass.async_block_till_done() - assert secure_knxkeys["type"] == RESULT_TYPE_CREATE_ENTRY + assert secure_knxkeys["type"] == FlowResultType.CREATE_ENTRY assert secure_knxkeys["data"] == { **DEFAULT_ENTRY_DATA, CONF_KNX_CONNECTION_TYPE: CONF_KNX_TUNNELING_TCP_SECURE, @@ -616,7 +612,7 @@ async def test_configure_secure_knxkeys_file_not_found(hass: HomeAssistant): menu_step["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "secure_knxkeys" assert not result["errors"] @@ -632,7 +628,7 @@ async def test_configure_secure_knxkeys_file_not_found(hass: HomeAssistant): }, ) await hass.async_block_till_done() - assert secure_knxkeys["type"] == RESULT_TYPE_FORM + assert secure_knxkeys["type"] == FlowResultType.FORM assert secure_knxkeys["errors"] assert secure_knxkeys["errors"][CONF_KNX_KNXKEY_FILENAME] == "file_not_found" @@ -645,7 +641,7 @@ async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant): menu_step["flow_id"], {"next_step_id": "secure_knxkeys"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "secure_knxkeys" assert not result["errors"] @@ -661,7 +657,7 @@ async def test_configure_secure_knxkeys_invalid_signature(hass: HomeAssistant): }, ) await hass.async_block_till_done() - assert secure_knxkeys["type"] == RESULT_TYPE_FORM + assert secure_knxkeys["type"] == FlowResultType.FORM assert secure_knxkeys["errors"] assert secure_knxkeys["errors"][CONF_KNX_KNXKEY_PASSWORD] == "invalid_signature" @@ -679,7 +675,7 @@ async def test_options_flow( mock_config_entry.entry_id ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -694,7 +690,7 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert not result2.get("data") assert mock_config_entry.data == { @@ -787,7 +783,7 @@ async def test_tunneling_options_flow( mock_config_entry.entry_id ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -801,7 +797,7 @@ async def test_tunneling_options_flow( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert not result2.get("data") assert "flow_id" in result2 @@ -811,7 +807,7 @@ async def test_tunneling_options_flow( ) await hass.async_block_till_done() - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert not result3.get("data") assert mock_config_entry.data == config_entry_data @@ -880,7 +876,7 @@ async def test_advanced_options( mock_config_entry.entry_id, context={"show_advanced_options": True} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -890,7 +886,7 @@ async def test_advanced_options( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert not result2.get("data") assert mock_config_entry.data == config_entry_data diff --git a/tests/components/laundrify/test_config_flow.py b/tests/components/laundrify/test_config_flow.py index 5ee3efe1e45..d9715c2d025 100644 --- a/tests/components/laundrify/test_config_flow.py +++ b/tests/components/laundrify/test_config_flow.py @@ -6,11 +6,7 @@ from homeassistant.components.laundrify.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CODE, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import create_entry from .const import VALID_ACCESS_TOKEN, VALID_AUTH_CODE, VALID_USER_INPUT @@ -21,7 +17,7 @@ async def test_form(hass: HomeAssistant, laundrify_setup_entry) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -30,7 +26,7 @@ async def test_form(hass: HomeAssistant, laundrify_setup_entry) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == DOMAIN assert result["data"] == { CONF_ACCESS_TOKEN: VALID_ACCESS_TOKEN, @@ -50,7 +46,7 @@ async def test_form_invalid_format( data={CONF_CODE: "invalidFormat"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {CONF_CODE: "invalid_format"} @@ -63,7 +59,7 @@ async def test_form_invalid_auth(hass: HomeAssistant, laundrify_exchange_code) - data=VALID_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {CONF_CODE: "invalid_auth"} @@ -76,7 +72,7 @@ async def test_form_cannot_connect(hass: HomeAssistant, laundrify_exchange_code) data=VALID_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -89,7 +85,7 @@ async def test_form_unkown_exception(hass: HomeAssistant, laundrify_exchange_cod data=VALID_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "unknown"} @@ -99,7 +95,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_REAUTH} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -108,7 +104,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_integration_already_exists(hass: HomeAssistant): @@ -125,5 +121,5 @@ async def test_integration_already_exists(hass: HomeAssistant): }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/lookin/test_config_flow.py b/tests/components/lookin/test_config_flow.py index e24b9c92221..daa35f31845 100644 --- a/tests/components/lookin/test_config_flow.py +++ b/tests/components/lookin/test_config_flow.py @@ -10,11 +10,7 @@ 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 homeassistant.data_entry_flow import FlowResultType from . import ( DEFAULT_ENTRY_TITLE, @@ -45,7 +41,7 @@ async def test_manual_setup(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_HOST: IP_ADDRESS} assert result["title"] == DEFAULT_ENTRY_TITLE assert len(mock_setup_entry.mock_calls) == 1 @@ -70,7 +66,7 @@ async def test_manual_setup_already_exists(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -123,7 +119,7 @@ async def test_discovered_zeroconf(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_get_info(), patch( @@ -132,7 +128,7 @@ async def test_discovered_zeroconf(hass): 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["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == {CONF_HOST: IP_ADDRESS} assert result2["title"] == DEFAULT_ENTRY_TITLE assert mock_async_setup_entry.called @@ -151,7 +147,7 @@ async def test_discovered_zeroconf(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "127.0.0.2" @@ -167,7 +163,7 @@ async def test_discovered_zeroconf_cannot_connect(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -182,5 +178,5 @@ async def test_discovered_zeroconf_unknown_exception(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/luftdaten/test_config_flow.py b/tests/components/luftdaten/test_config_flow.py index 595d4397200..25d41c0c2d0 100644 --- a/tests/components/luftdaten/test_config_flow.py +++ b/tests/components/luftdaten/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.luftdaten.const import CONF_SENSOR_ID from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_SHOW_ON_MAP from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -27,7 +23,7 @@ async def test_duplicate_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -36,7 +32,7 @@ async def test_duplicate_error( user_input={CONF_SENSOR_ID: 12345}, ) - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "already_configured" @@ -48,7 +44,7 @@ async def test_communication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -58,7 +54,7 @@ async def test_communication_error( user_input={CONF_SENSOR_ID: 12345}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {CONF_SENSOR_ID: "cannot_connect"} assert "flow_id" in result2 @@ -69,7 +65,7 @@ async def test_communication_error( user_input={CONF_SENSOR_ID: 12345}, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "12345" assert result3.get("data") == { CONF_SENSOR_ID: 12345, @@ -85,7 +81,7 @@ async def test_invalid_sensor( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -95,7 +91,7 @@ async def test_invalid_sensor( user_input={CONF_SENSOR_ID: 11111}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {CONF_SENSOR_ID: "invalid_sensor"} assert "flow_id" in result2 @@ -106,7 +102,7 @@ async def test_invalid_sensor( user_input={CONF_SENSOR_ID: 12345}, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "12345" assert result3.get("data") == { CONF_SENSOR_ID: 12345, @@ -124,7 +120,7 @@ async def test_step_user( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -136,7 +132,7 @@ async def test_step_user( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "12345" assert result2.get("data") == { CONF_SENSOR_ID: 12345, diff --git a/tests/components/mill/test_config_flow.py b/tests/components/mill/test_config_flow.py index ff2f7393c82..44fea69409b 100644 --- a/tests/components/mill/test_config_flow.py +++ b/tests/components/mill/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.mill.const import CLOUD, CONNECTION_TYPE, DOMAIN, LOCAL from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME -from homeassistant.data_entry_flow import RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -15,7 +15,7 @@ async def test_show_config_form(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" @@ -24,7 +24,7 @@ async def test_create_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -33,7 +33,7 @@ async def test_create_entry(hass): CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("mill.Mill.connect", return_value=True): result = await hass.config_entries.flow.async_configure( @@ -72,7 +72,7 @@ async def test_flow_entry_already_exists(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -81,7 +81,7 @@ async def test_flow_entry_already_exists(hass): CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("mill.Mill.connect", return_value=True): result = await hass.config_entries.flow.async_configure( @@ -99,7 +99,7 @@ async def test_connection_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -108,7 +108,7 @@ async def test_connection_error(hass): CONNECTION_TYPE: CLOUD, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch("mill.Mill.connect", return_value=False): result = await hass.config_entries.flow.async_configure( @@ -119,7 +119,7 @@ async def test_connection_error(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -128,7 +128,7 @@ async def test_local_create_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -137,7 +137,7 @@ async def test_local_create_entry(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { CONF_IP_ADDRESS: "192.168.1.59", @@ -180,7 +180,7 @@ async def test_local_flow_entry_already_exists(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -189,7 +189,7 @@ async def test_local_flow_entry_already_exists(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { CONF_IP_ADDRESS: "192.168.1.59", @@ -219,7 +219,7 @@ async def test_local_connection_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure( @@ -228,7 +228,7 @@ async def test_local_connection_error(hass): CONNECTION_TYPE: LOCAL, }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM test_data = { CONF_IP_ADDRESS: "192.168.1.59", @@ -243,5 +243,5 @@ async def test_local_connection_error(hass): test_data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/min_max/test_config_flow.py b/tests/components/min_max/test_config_flow.py index ac617eb938c..0eb334763d6 100644 --- a/tests/components/min_max/test_config_flow.py +++ b/tests/components/min_max/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.min_max.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -32,7 +32,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My min_max" assert result["data"] == {} assert result["options"] == { @@ -92,7 +92,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "entity_ids") == input_sensors1 @@ -107,7 +107,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "type": "mean", }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "entity_ids": input_sensors2, "name": "My min_max", diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py index 9717fa0052b..7d6b8227396 100644 --- a/tests/components/minecraft_server/test_config_flow.py +++ b/tests/components/minecraft_server/test_config_flow.py @@ -14,11 +14,7 @@ from homeassistant.components.minecraft_server.const import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -86,7 +82,7 @@ async def test_show_config_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" @@ -97,7 +93,7 @@ async def test_invalid_ip(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV4 ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_ip"} @@ -122,7 +118,7 @@ async def test_same_host(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -136,7 +132,7 @@ async def test_port_too_small(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_PORT_TOO_SMALL ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_port"} @@ -150,7 +146,7 @@ async def test_port_too_large(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_PORT_TOO_LARGE ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_port"} @@ -164,7 +160,7 @@ async def test_connection_failed(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -178,7 +174,7 @@ async def test_connection_succeeded_with_srv_record(hass: HomeAssistant) -> None DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_SRV ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT_SRV[CONF_HOST] assert result["data"][CONF_NAME] == USER_INPUT_SRV[CONF_NAME] assert result["data"][CONF_HOST] == USER_INPUT_SRV[CONF_HOST] @@ -194,7 +190,7 @@ async def test_connection_succeeded_with_host(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT[CONF_HOST] assert result["data"][CONF_NAME] == USER_INPUT[CONF_NAME] assert result["data"][CONF_HOST] == "mc.dummyserver.com" @@ -213,7 +209,7 @@ async def test_connection_succeeded_with_ip4(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV4 ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT_IPV4[CONF_HOST] assert result["data"][CONF_NAME] == USER_INPUT_IPV4[CONF_NAME] assert result["data"][CONF_HOST] == "1.1.1.1" @@ -232,7 +228,7 @@ async def test_connection_succeeded_with_ip6(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_IPV6 ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == USER_INPUT_IPV6[CONF_HOST] assert result["data"][CONF_NAME] == USER_INPUT_IPV6[CONF_NAME] assert result["data"][CONF_HOST] == "::ffff:0101:0101" diff --git a/tests/components/mjpeg/test_config_flow.py b/tests/components/mjpeg/test_config_flow.py index cdf56ab97c0..3d66af21f0a 100644 --- a/tests/components/mjpeg/test_config_flow.py +++ b/tests/components/mjpeg/test_config_flow.py @@ -20,11 +20,7 @@ from homeassistant.const import ( HTTP_BASIC_AUTHENTICATION, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -39,7 +35,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -55,7 +51,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Spy cam" assert result2.get("data") == {} assert result2.get("options") == { @@ -85,7 +81,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -102,7 +98,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"username": "invalid_auth"} assert "flow_id" in result2 @@ -121,7 +117,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "Sky cam" assert result3.get("data") == {} assert result3.get("options") == { @@ -147,7 +143,7 @@ async def test_connection_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -164,7 +160,7 @@ async def test_connection_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"mjpeg_url": "cannot_connect"} assert "flow_id" in result2 @@ -188,7 +184,7 @@ async def test_connection_error( }, ) - assert result3.get("type") == RESULT_TYPE_FORM + assert result3.get("type") == FlowResultType.FORM assert result3.get("step_id") == SOURCE_USER assert result3.get("errors") == {"still_image_url": "cannot_connect"} assert "flow_id" in result3 @@ -209,7 +205,7 @@ async def test_connection_error( }, ) - assert result4.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result4.get("type") == FlowResultType.CREATE_ENTRY assert result4.get("title") == "My cam" assert result4.get("data") == {} assert result4.get("options") == { @@ -247,7 +243,7 @@ async def test_already_configured( }, ) - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "already_configured" @@ -259,7 +255,7 @@ async def test_options_flow( """Test options config flow.""" result = await hass.config_entries.options.async_init(init_integration.entry_id) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -288,7 +284,7 @@ async def test_options_flow( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "init" assert result2.get("errors") == {"mjpeg_url": "already_configured"} assert "flow_id" in result2 @@ -307,7 +303,7 @@ async def test_options_flow( }, ) - assert result3.get("type") == RESULT_TYPE_FORM + assert result3.get("type") == FlowResultType.FORM assert result3.get("step_id") == "init" assert result3.get("errors") == {"mjpeg_url": "cannot_connect"} assert "flow_id" in result3 @@ -326,7 +322,7 @@ async def test_options_flow( }, ) - assert result4.get("type") == RESULT_TYPE_FORM + assert result4.get("type") == FlowResultType.FORM assert result4.get("step_id") == "init" assert result4.get("errors") == {"still_image_url": "cannot_connect"} assert "flow_id" in result4 @@ -346,7 +342,7 @@ async def test_options_flow( }, ) - assert result5.get("type") == RESULT_TYPE_FORM + assert result5.get("type") == FlowResultType.FORM assert result5.get("step_id") == "init" assert result5.get("errors") == {"username": "invalid_auth"} assert "flow_id" in result5 @@ -363,7 +359,7 @@ async def test_options_flow( }, ) - assert result6.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result6.get("type") == FlowResultType.CREATE_ENTRY assert result6.get("data") == { CONF_AUTHENTICATION: HTTP_BASIC_AUTHENTICATION, CONF_MJPEG_URL: "https://example.com/mjpeg", diff --git a/tests/components/modern_forms/test_config_flow.py b/tests/components/modern_forms/test_config_flow.py index 931d2918fe2..05cebb2fef5 100644 --- a/tests/components/modern_forms/test_config_flow.py +++ b/tests/components/modern_forms/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.modern_forms.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import init_integration @@ -37,7 +33,7 @@ async def test_full_user_flow_implementation( ) assert result.get("step_id") == "user" - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result with patch( @@ -50,7 +46,7 @@ async def test_full_user_flow_implementation( assert result2.get("title") == "ModernFormsFan" assert "data" in result2 - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2["data"][CONF_HOST] == "192.168.1.123" assert result2["data"][CONF_MAC] == "AA:BB:CC:DD:EE:FF" assert len(mock_setup_entry.mock_calls) == 1 @@ -85,7 +81,7 @@ async def test_full_zeroconf_flow_implementation( assert result.get("description_placeholders") == {CONF_NAME: "example"} assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result flow = flows[0] @@ -98,7 +94,7 @@ async def test_full_zeroconf_flow_implementation( ) assert result2.get("title") == "example" - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert "data" in result2 assert result2["data"][CONF_HOST] == "192.168.1.123" @@ -121,7 +117,7 @@ async def test_connection_error( data={CONF_HOST: "example.com"}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -150,7 +146,7 @@ async def test_zeroconf_connection_error( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -182,7 +178,7 @@ async def test_zeroconf_confirm_connection_error( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "cannot_connect" @@ -218,7 +214,7 @@ async def test_user_device_exists_abort( }, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -252,5 +248,5 @@ async def test_zeroconf_with_mac_device_exists_abort( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/moehlenhoff_alpha2/test_config_flow.py b/tests/components/moehlenhoff_alpha2/test_config_flow.py index ccfa98718e5..a1f98454015 100644 --- a/tests/components/moehlenhoff_alpha2/test_config_flow.py +++ b/tests/components/moehlenhoff_alpha2/test_config_flow.py @@ -5,11 +5,7 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.moehlenhoff_alpha2.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 homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -33,7 +29,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] with patch("moehlenhoff_alpha2.Alpha2Base.update_data", mock_update_data), patch( @@ -46,7 +42,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_BASE_NAME assert result2["data"] == {"host": MOCK_BASE_HOST} assert len(mock_setup_entry.mock_calls) == 1 @@ -70,7 +66,7 @@ async def test_form_duplicate_error(hass: HomeAssistant) -> None: data={"host": MOCK_BASE_HOST}, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -87,7 +83,7 @@ async def test_form_cannot_connect_error(hass: HomeAssistant) -> None: user_input={"host": MOCK_BASE_HOST}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -102,5 +98,5 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: user_input={"host": MOCK_BASE_HOST}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/moon/test_config_flow.py b/tests/components/moon/test_config_flow.py index 4bfb61166aa..9dfc186d492 100644 --- a/tests/components/moon/test_config_flow.py +++ b/tests/components/moon/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components.moon.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -25,7 +21,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -34,7 +30,7 @@ async def test_full_user_flow( user_input={}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Moon" assert result2.get("data") == {} @@ -52,7 +48,7 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -67,6 +63,6 @@ async def test_import_flow( data={CONF_NAME: "My Moon"}, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "My Moon" assert result.get("data") == {} diff --git a/tests/components/mullvad/test_config_flow.py b/tests/components/mullvad/test_config_flow.py index 967ad0dbcc4..451e9ec4b90 100644 --- a/tests/components/mullvad/test_config_flow.py +++ b/tests/components/mullvad/test_config_flow.py @@ -5,7 +5,7 @@ from mullvad_api import MullvadAPIError from homeassistant import config_entries, setup from homeassistant.components.mullvad.const import DOMAIN -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -16,7 +16,7 @@ async def test_form_user(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] with patch( @@ -45,7 +45,7 @@ async def test_form_user_only_once(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -66,7 +66,7 @@ async def test_connection_error(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -87,5 +87,5 @@ async def test_unknown_error(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/nzbget/test_config_flow.py b/tests/components/nzbget/test_config_flow.py index 8799e3adcf0..fd28f6efa41 100644 --- a/tests/components/nzbget/test_config_flow.py +++ b/tests/components/nzbget/test_config_flow.py @@ -6,11 +6,7 @@ from pynzbgetapi import NZBGetAPIException from homeassistant.components.nzbget.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_SCAN_INTERVAL, CONF_VERIFY_SSL -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( ENTRY_CONFIG, @@ -30,7 +26,7 @@ async def test_user_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with _patch_version(), _patch_status(), _patch_history(), _patch_async_setup_entry() as mock_setup_entry: @@ -40,7 +36,7 @@ async def test_user_form(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.10.30" assert result["data"] == {**USER_INPUT, CONF_VERIFY_SSL: False} @@ -53,7 +49,7 @@ async def test_user_form_show_advanced_options(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} user_input_advanced = { @@ -68,7 +64,7 @@ async def test_user_form_show_advanced_options(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "10.10.10.30" assert result["data"] == {**USER_INPUT, CONF_VERIFY_SSL: True} @@ -90,7 +86,7 @@ async def test_user_form_cannot_connect(hass): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -109,7 +105,7 @@ async def test_user_form_unexpected_exception(hass): USER_INPUT, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -123,7 +119,7 @@ async def test_user_form_single_instance_allowed(hass): context={"source": SOURCE_USER}, data=USER_INPUT, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -143,7 +139,7 @@ async def test_options_flow(hass, nzbget_api): assert entry.options[CONF_SCAN_INTERVAL] == 5 result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" with _patch_async_setup_entry(): @@ -153,5 +149,5 @@ async def test_options_flow(hass, nzbget_api): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_SCAN_INTERVAL] == 15 diff --git a/tests/components/oncue/test_config_flow.py b/tests/components/oncue/test_config_flow.py index df9de02a6b3..718a3b08adb 100644 --- a/tests/components/oncue/test_config_flow.py +++ b/tests/components/oncue/test_config_flow.py @@ -7,11 +7,7 @@ from aiooncue import LoginFailedException from homeassistant import config_entries from homeassistant.components.oncue.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 homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -21,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch("homeassistant.components.oncue.config_flow.Oncue.async_login"), patch( @@ -37,7 +33,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "TEST-username", @@ -64,7 +60,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -86,7 +82,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -108,7 +104,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -137,5 +133,5 @@ async def test_already_configured(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/onewire/test_config_flow.py b/tests/components/onewire/test_config_flow.py index d599c10ea90..57677fc5bff 100644 --- a/tests/components/onewire/test_config_flow.py +++ b/tests/components/onewire/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.onewire.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType @pytest.fixture(autouse=True, name="mock_setup_entry") @@ -29,7 +25,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] # Invalid server @@ -42,7 +38,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock): user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 1234}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -55,7 +51,7 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock): user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 1234}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "1.2.3.4" assert result["data"] == { CONF_HOST: "1.2.3.4", @@ -76,7 +72,7 @@ async def test_user_duplicate( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -85,7 +81,7 @@ async def test_user_duplicate( result["flow_id"], user_input={CONF_HOST: "1.2.3.4", CONF_PORT: 1234}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/onewire/test_options_flow.py b/tests/components/onewire/test_options_flow.py index e27b5a368d9..795f8a50c99 100644 --- a/tests/components/onewire/test_options_flow.py +++ b/tests/components/onewire/test_options_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.onewire.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import setup_owproxy_mock_devices from .const import MOCK_OWPROXY_DEVICES @@ -49,7 +45,7 @@ async def test_user_options_clear( result["flow_id"], user_input={INPUT_ENTRY_CLEAR_OPTIONS: True}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == {} @@ -78,7 +74,7 @@ async def test_user_options_empty_selection( result["flow_id"], user_input={INPUT_ENTRY_DEVICE_SELECTION: []}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "device_selection" assert result["errors"] == {"base": "device_not_selected"} @@ -111,7 +107,7 @@ async def test_user_options_set_single( result["flow_id"], user_input={INPUT_ENTRY_DEVICE_SELECTION: ["28.111111111111"]}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["description_placeholders"]["sensor_id"] == "28.111111111111" # Verify that the setting for the device comes back as default when no input is given @@ -119,7 +115,7 @@ async def test_user_options_set_single( result["flow_id"], user_input={}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert ( result["data"]["device_options"]["28.111111111111"]["precision"] == "temperature" @@ -167,7 +163,7 @@ async def test_user_options_set_multiple( ] }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert ( result["description_placeholders"]["sensor_id"] == "Given Name (28.222222222222)" @@ -178,7 +174,7 @@ async def test_user_options_set_multiple( result["flow_id"], user_input={"precision": "temperature"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert ( result["description_placeholders"]["sensor_id"] == "Given Name (28.111111111111)" @@ -189,7 +185,7 @@ async def test_user_options_set_multiple( result["flow_id"], user_input={"precision": "temperature9"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert ( result["data"]["device_options"]["28.222222222222"]["precision"] == "temperature" @@ -213,5 +209,5 @@ async def test_user_options_no_devices( # Verify that first config step comes back with an empty list of possible devices to choose from result = await hass.config_entries.options.async_init(config_entry.entry_id) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "No configurable devices found." diff --git a/tests/components/open_meteo/test_config_flow.py b/tests/components/open_meteo/test_config_flow.py index f985e2a6193..0dd81d35856 100644 --- a/tests/components/open_meteo/test_config_flow.py +++ b/tests/components/open_meteo/test_config_flow.py @@ -7,7 +7,7 @@ from homeassistant.components.zone import ENTITY_ID_HOME from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_ZONE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_full_user_flow( @@ -19,7 +19,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -28,6 +28,6 @@ async def test_full_user_flow( user_input={CONF_ZONE: ENTITY_ID_HOME}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "test home" assert result2.get("data") == {CONF_ZONE: ENTITY_ID_HOME} diff --git a/tests/components/opengarage/test_config_flow.py b/tests/components/opengarage/test_config_flow.py index 5406c40b3aa..39cbb9c4b6b 100644 --- a/tests/components/opengarage/test_config_flow.py +++ b/tests/components/opengarage/test_config_flow.py @@ -6,11 +6,7 @@ import aiohttp from homeassistant import config_entries from homeassistant.components.opengarage.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 homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -21,7 +17,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -37,7 +33,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Name of the device" assert result2["data"] == { "host": "http://1.1.1.1", @@ -63,7 +59,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: {"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -82,7 +78,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -101,7 +97,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: {"host": "http://1.1.1.1", "device_key": "AfsasdnfkjDD"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -132,5 +128,5 @@ async def test_flow_entry_already_exists(hass: HomeAssistant) -> None: }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/p1_monitor/test_config_flow.py b/tests/components/p1_monitor/test_config_flow.py index f6ce5fe5d9d..42a41789a92 100644 --- a/tests/components/p1_monitor/test_config_flow.py +++ b/tests/components/p1_monitor/test_config_flow.py @@ -7,7 +7,7 @@ from homeassistant.components.p1_monitor.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_full_user_flow(hass: HomeAssistant) -> None: @@ -16,7 +16,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -33,7 +33,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Name" assert result2.get("data") == { CONF_HOST: "example.com", @@ -58,5 +58,5 @@ async def test_api_error(hass: HomeAssistant) -> None: }, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/peco/test_config_flow.py b/tests/components/peco/test_config_flow.py index 98f704e3af8..afa158e2dd9 100644 --- a/tests/components/peco/test_config_flow.py +++ b/tests/components/peco/test_config_flow.py @@ -7,7 +7,7 @@ from voluptuous.error import MultipleInvalid from homeassistant import config_entries from homeassistant.components.peco.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -15,7 +15,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -30,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Philadelphia Outage Count" assert result2["data"] == { "county": "PHILADELPHIA", @@ -42,7 +42,7 @@ async def test_invalid_county(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with raises(MultipleInvalid): @@ -57,7 +57,7 @@ async def test_invalid_county(hass: HomeAssistant) -> None: second_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert second_result["type"] == RESULT_TYPE_FORM + assert second_result["type"] == FlowResultType.FORM assert second_result["errors"] is None with patch( @@ -72,7 +72,7 @@ async def test_invalid_county(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert second_result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert second_result2["type"] == FlowResultType.CREATE_ENTRY assert second_result2["title"] == "Philadelphia Outage Count" assert second_result2["data"] == { "county": "PHILADELPHIA", diff --git a/tests/components/pi_hole/test_config_flow.py b/tests/components/pi_hole/test_config_flow.py index 517697b0e8a..bc86922c89f 100644 --- a/tests/components/pi_hole/test_config_flow.py +++ b/tests/components/pi_hole/test_config_flow.py @@ -5,11 +5,7 @@ from unittest.mock import patch from homeassistant.components.pi_hole.const import CONF_STATISTICS_ONLY, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_API_KEY -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( CONF_CONFIG_ENTRY, @@ -44,7 +40,7 @@ async def test_flow_import(hass, caplog): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_CONFIG_ENTRY @@ -52,7 +48,7 @@ async def test_flow_import(hass, caplog): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -63,7 +59,7 @@ async def test_flow_import_invalid(hass, caplog): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" assert len([x for x in caplog.records if x.levelno == logging.ERROR]) == 1 @@ -76,7 +72,7 @@ async def test_flow_user(hass): DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} _flow_next(hass, result["flow_id"]) @@ -85,7 +81,7 @@ async def test_flow_user(hass): result["flow_id"], user_input=CONF_CONFIG_FLOW_USER, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_key" assert result["errors"] is None _flow_next(hass, result["flow_id"]) @@ -94,7 +90,7 @@ async def test_flow_user(hass): result["flow_id"], user_input=CONF_CONFIG_FLOW_API_KEY, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == CONF_CONFIG_ENTRY @@ -104,7 +100,7 @@ async def test_flow_user(hass): context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW_USER, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -116,7 +112,7 @@ async def test_flow_statistics_only(hass): DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} _flow_next(hass, result["flow_id"]) @@ -130,7 +126,7 @@ async def test_flow_statistics_only(hass): result["flow_id"], user_input=user_input, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == NAME assert result["data"] == config_entry_data @@ -142,6 +138,6 @@ async def test_flow_user_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW_USER ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/plaato/test_config_flow.py b/tests/components/plaato/test_config_flow.py index b7b39aa50b4..f3abd5a6905 100644 --- a/tests/components/plaato/test_config_flow.py +++ b/tests/components/plaato/test_config_flow.py @@ -12,11 +12,7 @@ from homeassistant.components.plaato.const import ( DOMAIN, ) from homeassistant.const import CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_WEBHOOK_ID -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -59,7 +55,7 @@ async def test_show_config_form_device_type_airlock(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" assert result["data_schema"].schema.get(CONF_TOKEN) == str assert result["data_schema"].schema.get(CONF_USE_WEBHOOK) == bool @@ -73,7 +69,7 @@ async def test_show_config_form_device_type_keg(hass): data={CONF_DEVICE_TYPE: PlaatoDeviceType.Keg, CONF_DEVICE_NAME: "device_name"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" assert result["data_schema"].schema.get(CONF_TOKEN) == str assert result["data_schema"].schema.get(CONF_USE_WEBHOOK) is None @@ -86,7 +82,7 @@ async def test_show_config_form_validate_webhook(hass, webhook_id): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -97,7 +93,7 @@ async def test_show_config_form_validate_webhook(hass, webhook_id): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" assert await async_setup_component(hass, "cloud", {}) @@ -119,7 +115,7 @@ async def test_show_config_form_validate_webhook(hass, webhook_id): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "webhook" @@ -130,7 +126,7 @@ async def test_show_config_form_validate_webhook_not_connected(hass, webhook_id) DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -141,7 +137,7 @@ async def test_show_config_form_validate_webhook_not_connected(hass, webhook_id) }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" assert await async_setup_component(hass, "cloud", {}) @@ -163,7 +159,7 @@ async def test_show_config_form_validate_webhook_not_connected(hass, webhook_id) }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cloud_not_connected" @@ -182,7 +178,7 @@ async def test_show_config_form_validate_token(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" with patch("homeassistant.components.plaato.async_setup_entry", return_value=True): @@ -190,7 +186,7 @@ async def test_show_config_form_validate_token(hass): result["flow_id"], user_input={CONF_TOKEN: "valid_token"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == PlaatoDeviceType.Keg.name assert result["data"] == { CONF_USE_WEBHOOK: False, @@ -215,7 +211,7 @@ async def test_show_config_form_no_cloud_webhook(hass, webhook_id): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" result = await hass.config_entries.flow.async_configure( @@ -226,7 +222,7 @@ async def test_show_config_form_no_cloud_webhook(hass, webhook_id): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "webhook" assert result["errors"] is None @@ -247,14 +243,14 @@ async def test_show_config_form_api_method_no_auth_token(hass, webhook_id): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_TOKEN: ""} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" assert len(result["errors"]) == 1 assert result["errors"]["base"] == "no_auth_token" @@ -272,14 +268,14 @@ async def test_show_config_form_api_method_no_auth_token(hass, webhook_id): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_TOKEN: ""} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "api_method" assert len(result["errors"]) == 1 assert result["errors"]["base"] == "no_api_method" diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index 3ef9e9c0fd1..f4dcfd87b8b 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -12,7 +12,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.powerwall.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT +from homeassistant.data_entry_flow import FlowResultType from .mocks import ( MOCK_GATEWAY_DIN, @@ -299,7 +299,7 @@ async def test_dhcp_discovery_cannot_connect(hass): hostname="00GGX", ), ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -370,7 +370,7 @@ async def test_dhcp_discovery_update_ip_address(hass): ), ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.1.1.1" @@ -402,7 +402,7 @@ async def test_dhcp_discovery_updates_unique_id(hass): ), ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_IP_ADDRESS] == "1.2.3.4" assert entry.unique_id == MOCK_GATEWAY_DIN diff --git a/tests/components/progettihwsw/test_config_flow.py b/tests/components/progettihwsw/test_config_flow.py index 8c850e0807a..cef6b87963a 100644 --- a/tests/components/progettihwsw/test_config_flow.py +++ b/tests/components/progettihwsw/test_config_flow.py @@ -4,11 +4,7 @@ from unittest.mock import patch 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 ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -26,7 +22,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -43,7 +39,7 @@ async def test_form(hass): {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "relay_modes" assert result2["errors"] == {} @@ -56,7 +52,7 @@ async def test_form(hass): mock_value_step_rm, ) - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["data"] assert result3["data"]["title"] == "1R & 1IN Board" assert result3["data"]["is_old"] is False @@ -80,7 +76,7 @@ async def test_form_cannot_connect(hass): {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "cannot_connect"} @@ -107,7 +103,7 @@ async def test_form_existing_entry_exception(hass): {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -128,6 +124,6 @@ async def test_form_user_exception(hass): {CONF_HOST: "", CONF_PORT: 80}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/prosegur/test_config_flow.py b/tests/components/prosegur/test_config_flow.py index 46832146cf8..8a608d4eeba 100644 --- a/tests/components/prosegur/test_config_flow.py +++ b/tests/components/prosegur/test_config_flow.py @@ -6,7 +6,7 @@ from pytest import mark 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 +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -145,7 +145,7 @@ async def test_reauth_flow(hass): data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} install = MagicMock() @@ -167,7 +167,7 @@ async def test_reauth_flow(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "country": "PT", @@ -223,5 +223,5 @@ async def test_reauth_flow_error(hass, exception, base_error): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"]["base"] == base_error diff --git a/tests/components/pure_energie/test_config_flow.py b/tests/components/pure_energie/test_config_flow.py index 441a5977a2d..d1ed8eeb578 100644 --- a/tests/components/pure_energie/test_config_flow.py +++ b/tests/components/pure_energie/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.pure_energie.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType async def test_full_user_flow_implementation( @@ -27,7 +23,7 @@ async def test_full_user_flow_implementation( ) assert result.get("step_id") == SOURCE_USER - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result result = await hass.config_entries.flow.async_configure( @@ -35,7 +31,7 @@ async def test_full_user_flow_implementation( ) assert result.get("title") == "Pure Energie Meter" - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert "data" in result assert result["data"][CONF_HOST] == "192.168.1.123" assert "result" in result @@ -67,7 +63,7 @@ async def test_full_zeroconf_flow_implementationn( CONF_NAME: "Pure Energie Meter", } assert result.get("step_id") == "zeroconf_confirm" - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( @@ -75,7 +71,7 @@ async def test_full_zeroconf_flow_implementationn( ) assert result2.get("title") == "Pure Energie Meter" - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert "data" in result2 assert result2["data"][CONF_HOST] == "192.168.1.123" @@ -94,7 +90,7 @@ async def test_connection_error( data={CONF_HOST: "example.com"}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "user" assert result.get("errors") == {"base": "cannot_connect"} @@ -119,5 +115,5 @@ async def test_zeroconf_connection_error( ), ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "cannot_connect" diff --git a/tests/components/pvoutput/test_config_flow.py b/tests/components/pvoutput/test_config_flow.py index 444a35565f6..9d6162e4d46 100644 --- a/tests/components/pvoutput/test_config_flow.py +++ b/tests/components/pvoutput/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.pvoutput.const import CONF_SYSTEM_ID, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -27,7 +23,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -39,7 +35,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "12345" assert result2.get("data") == { CONF_SYSTEM_ID: 12345, @@ -64,7 +60,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -77,7 +73,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": "invalid_auth"} assert "flow_id" in result2 @@ -94,7 +90,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "12345" assert result3.get("data") == { CONF_SYSTEM_ID: 12345, @@ -120,7 +116,7 @@ async def test_connection_error( }, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 @@ -147,7 +143,7 @@ async def test_already_configured( }, ) - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "already_configured" @@ -169,7 +165,7 @@ async def test_reauth_flow( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -179,7 +175,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_SYSTEM_ID: 12345, @@ -212,7 +208,7 @@ async def test_reauth_with_authentication_error( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -223,7 +219,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "invalid_auth"} assert "flow_id" in result2 @@ -238,7 +234,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result3.get("type") == RESULT_TYPE_ABORT + assert result3.get("type") == FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_SYSTEM_ID: 12345, @@ -266,7 +262,7 @@ async def test_reauth_api_error( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -277,6 +273,6 @@ async def test_reauth_api_error( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/radio_browser/test_config_flow.py b/tests/components/radio_browser/test_config_flow.py index 8a5a3d9ccce..56ed98f145b 100644 --- a/tests/components/radio_browser/test_config_flow.py +++ b/tests/components/radio_browser/test_config_flow.py @@ -4,11 +4,7 @@ from unittest.mock import AsyncMock from homeassistant.components.radio_browser.const import DOMAIN from homeassistant.config_entries 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 homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,7 +14,7 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") is None assert "flow_id" in result @@ -27,7 +23,7 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) user_input={}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Radio Browser" assert result2.get("data") == {} @@ -46,7 +42,7 @@ async def test_already_configured( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -58,7 +54,7 @@ async def test_onboarding_flow( DOMAIN, context={"source": "onboarding"} ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "Radio Browser" assert result.get("data") == {} diff --git a/tests/components/rainforest_eagle/test_config_flow.py b/tests/components/rainforest_eagle/test_config_flow.py index 370dfebee90..d9b66b0feec 100644 --- a/tests/components/rainforest_eagle/test_config_flow.py +++ b/tests/components/rainforest_eagle/test_config_flow.py @@ -12,7 +12,7 @@ from homeassistant.components.rainforest_eagle.const import ( from homeassistant.components.rainforest_eagle.data import CannotConnect, InvalidAuth from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -21,7 +21,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -41,7 +41,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "abcdef" assert result2["data"] == { CONF_TYPE: TYPE_EAGLE_200, @@ -72,7 +72,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -95,5 +95,5 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/raspberry_pi/test_config_flow.py b/tests/components/raspberry_pi/test_config_flow.py index dfad1100cad..68306b3ea9a 100644 --- a/tests/components/raspberry_pi/test_config_flow.py +++ b/tests/components/raspberry_pi/test_config_flow.py @@ -3,7 +3,7 @@ from unittest.mock import patch from homeassistant.components.raspberry_pi.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, MockModule, mock_integration @@ -20,7 +20,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Raspberry Pi" assert result["data"] == {} assert result["options"] == {} @@ -53,6 +53,6 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": "system"} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" mock_setup_entry.assert_not_called() diff --git a/tests/components/rdw/test_config_flow.py b/tests/components/rdw/test_config_flow.py index 20144768abe..0fe40c29dfa 100644 --- a/tests/components/rdw/test_config_flow.py +++ b/tests/components/rdw/test_config_flow.py @@ -7,7 +7,7 @@ from vehicle.exceptions import RDWConnectionError, RDWUnknownLicensePlateError from homeassistant.components.rdw.const import CONF_LICENSE_PLATE, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_full_user_flow( @@ -18,7 +18,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -29,7 +29,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "11-ZKZ-3" assert result2.get("data") == {CONF_LICENSE_PLATE: "11ZKZ3"} @@ -46,7 +46,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -58,7 +58,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": "unknown_license_plate"} assert "flow_id" in result2 @@ -71,7 +71,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "11-ZKZ-3" assert result3.get("data") == {CONF_LICENSE_PLATE: "11ZKZ3"} @@ -88,5 +88,5 @@ async def test_connection_error( data={CONF_LICENSE_PLATE: "0001TJ"}, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/ridwell/test_config_flow.py b/tests/components/ridwell/test_config_flow.py index 358ac6783ad..a28660bb7a4 100644 --- a/tests/components/ridwell/test_config_flow.py +++ b/tests/components/ridwell/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant import config_entries from homeassistant.components.ridwell.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType async def test_duplicate_error(hass: HomeAssistant, config, config_entry): @@ -20,7 +16,7 @@ async def test_duplicate_error(hass: HomeAssistant, config, config_entry): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -39,7 +35,7 @@ async def test_errors(hass: HomeAssistant, config, error, exc) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"]["base"] == error @@ -48,7 +44,7 @@ async def test_show_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -66,7 +62,7 @@ async def test_step_reauth( result["flow_id"], user_input={CONF_PASSWORD: "password"}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -76,4 +72,4 @@ async def test_step_user(hass: HomeAssistant, config, setup_ridwell) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index f5a3d270f70..bac6d7456a3 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.roku.const import DOMAIN from homeassistant.config_entries import SOURCE_HOMEKIT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.components.roku import ( @@ -39,7 +35,7 @@ async def test_duplicate_error( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" user_input = {CONF_HOST: mock_config_entry.data[CONF_HOST]} @@ -47,7 +43,7 @@ async def test_duplicate_error( DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" discovery_info = dataclasses.replace(MOCK_SSDP_DISCOVERY_INFO) @@ -55,7 +51,7 @@ async def test_duplicate_error( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -68,7 +64,7 @@ async def test_form( result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} user_input = {CONF_HOST: HOST} @@ -77,7 +73,7 @@ async def test_form( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My Roku 3" assert "data" in result @@ -101,7 +97,7 @@ async def test_form_cannot_connect( flow_id=result["flow_id"], user_input={CONF_HOST: HOST} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -120,7 +116,7 @@ async def test_form_unknown_error( flow_id=result["flow_id"], user_input=user_input ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -137,7 +133,7 @@ async def test_homekit_cannot_connect( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -154,7 +150,7 @@ async def test_homekit_unknown_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -170,7 +166,7 @@ async def test_homekit_discovery( DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" assert result["description_placeholders"] == {CONF_NAME: NAME_ROKUTV} @@ -179,7 +175,7 @@ async def test_homekit_discovery( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == NAME_ROKUTV assert "data" in result @@ -192,7 +188,7 @@ async def test_homekit_discovery( DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -209,7 +205,7 @@ async def test_ssdp_cannot_connect( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -226,7 +222,7 @@ async def test_ssdp_unknown_error( data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -241,7 +237,7 @@ async def test_ssdp_discovery( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" assert result["description_placeholders"] == {CONF_NAME: UPNP_FRIENDLY_NAME} @@ -250,7 +246,7 @@ async def test_ssdp_discovery( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == UPNP_FRIENDLY_NAME assert result["data"] diff --git a/tests/components/rpi_power/test_config_flow.py b/tests/components/rpi_power/test_config_flow.py index 7e302b51512..5c474fc0821 100644 --- a/tests/components/rpi_power/test_config_flow.py +++ b/tests/components/rpi_power/test_config_flow.py @@ -4,11 +4,7 @@ from unittest.mock import MagicMock from homeassistant.components.rpi_power.const import DOMAIN from homeassistant.config_entries 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 homeassistant.data_entry_flow import FlowResultType from tests.common import patch @@ -21,13 +17,13 @@ async def test_setup(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert not result["errors"] with patch(MODULE, return_value=MagicMock()): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY async def test_not_supported(hass: HomeAssistant) -> None: @@ -39,7 +35,7 @@ async def test_not_supported(hass: HomeAssistant) -> None: with patch(MODULE, return_value=None): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" @@ -50,7 +46,7 @@ async def test_onboarding(hass: HomeAssistant) -> None: DOMAIN, context={"source": "onboarding"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY async def test_onboarding_not_supported(hass: HomeAssistant) -> None: @@ -60,5 +56,5 @@ async def test_onboarding_not_supported(hass: HomeAssistant) -> None: DOMAIN, context={"source": "onboarding"}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" diff --git a/tests/components/sabnzbd/test_config_flow.py b/tests/components/sabnzbd/test_config_flow.py index 1444d82cc5b..2c5e9e1ffc9 100644 --- a/tests/components/sabnzbd/test_config_flow.py +++ b/tests/components/sabnzbd/test_config_flow.py @@ -15,7 +15,7 @@ from homeassistant.const import ( CONF_SSL, CONF_URL, ) -from homeassistant.data_entry_flow import RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType VALID_CONFIG = { CONF_NAME: "Sabnzbd", @@ -39,7 +39,7 @@ async def test_create_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 11aaf12d9ee..0b49a064a19 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -57,11 +57,7 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component from . import setup_samsungtv_entry @@ -363,7 +359,7 @@ async def test_user_legacy_missing_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} @@ -375,7 +371,7 @@ async def test_user_legacy_missing_auth(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == RESULT_CANNOT_CONNECT @@ -447,7 +443,7 @@ async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} with patch( @@ -459,7 +455,7 @@ async def test_user_websocket_auth_retry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Living Room (82GXARRS)" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "Living Room" @@ -536,7 +532,7 @@ async def test_ssdp_legacy_not_remote_control_receiver_udn( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=data ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -582,7 +578,7 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} @@ -591,7 +587,7 @@ async def test_ssdp_legacy_missing_auth(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "fake_model" assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "fake_model" @@ -611,7 +607,7 @@ async def test_ssdp_legacy_not_supported(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -743,7 +739,7 @@ async def test_ssdp_encrypted_websocket_not_supported( context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA_RENDERING_CONTROL_ST, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == RESULT_NOT_SUPPORTED @@ -1261,7 +1257,7 @@ async def test_autodetect_auth_missing(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" assert result["errors"] == {"base": "auth_missing"} @@ -1276,7 +1272,7 @@ async def test_autodetect_auth_missing(hass: HomeAssistant) -> None: {}, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == RESULT_CANNOT_CONNECT diff --git a/tests/components/season/test_config_flow.py b/tests/components/season/test_config_flow.py index 11ebea8f6d6..9a64bcd140a 100644 --- a/tests/components/season/test_config_flow.py +++ b/tests/components/season/test_config_flow.py @@ -11,11 +11,7 @@ from homeassistant.components.season.const import ( from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -29,7 +25,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -38,7 +34,7 @@ async def test_full_user_flow( user_input={CONF_TYPE: TYPE_ASTRONOMICAL}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Season" assert result2.get("data") == {CONF_TYPE: TYPE_ASTRONOMICAL} @@ -56,7 +52,7 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source}, data={CONF_TYPE: TYPE_ASTRONOMICAL} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" @@ -71,6 +67,6 @@ async def test_import_flow( data={CONF_NAME: "My Seasons", CONF_TYPE: TYPE_METEOROLOGICAL}, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "My Seasons" assert result.get("data") == {CONF_TYPE: TYPE_METEOROLOGICAL} diff --git a/tests/components/senseme/test_config_flow.py b/tests/components/senseme/test_config_flow.py index e85845dcace..63850dff3b5 100644 --- a/tests/components/senseme/test_config_flow.py +++ b/tests/components/senseme/test_config_flow.py @@ -6,11 +6,7 @@ from homeassistant.components import dhcp from homeassistant.components.senseme.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_ID from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( MOCK_ADDRESS, @@ -42,7 +38,7 @@ async def test_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -53,7 +49,7 @@ async def test_form_user(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Haiku Fan" assert result2["data"] == { "info": MOCK_DEVICE.get_device_info, @@ -68,7 +64,7 @@ async def test_form_user_manual_entry(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -79,7 +75,7 @@ async def test_form_user_manual_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual" with patch( @@ -97,7 +93,7 @@ async def test_form_user_manual_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Haiku Fan" assert result3["data"] == { "info": MOCK_DEVICE.get_device_info, @@ -118,7 +114,7 @@ async def test_form_user_no_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -129,7 +125,7 @@ async def test_form_user_no_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual" assert result2["errors"] == {CONF_HOST: "invalid_host"} @@ -141,7 +137,7 @@ async def test_form_user_no_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Haiku Fan" assert result3["data"] == { "info": MOCK_DEVICE.get_device_info, @@ -156,7 +152,7 @@ async def test_form_user_manual_entry_cannot_connect(hass: HomeAssistant) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -167,7 +163,7 @@ async def test_form_user_manual_entry_cannot_connect(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "manual" with patch( @@ -182,7 +178,7 @@ async def test_form_user_manual_entry_cannot_connect(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM assert result3["step_id"] == "manual" assert result3["errors"] == {CONF_HOST: "cannot_connect"} @@ -214,7 +210,7 @@ async def test_discovery(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ID: MOCK_UUID}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -225,7 +221,7 @@ async def test_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Haiku Fan" assert result2["data"] == { "info": MOCK_DEVICE.get_device_info, @@ -257,7 +253,7 @@ async def test_discovery_existing_device_no_ip_change(hass: HomeAssistant) -> No context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_ID: MOCK_UUID}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -286,7 +282,7 @@ async def test_discovery_existing_device_ip_change(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["info"]["address"] == "127.0.0.8" @@ -304,7 +300,7 @@ async def test_dhcp_discovery_existing_config_entry(hass: HomeAssistant) -> None result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -320,7 +316,7 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] result2 = await hass.config_entries.flow.async_configure( @@ -331,7 +327,7 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Haiku Fan" assert result2["data"] == { "info": MOCK_DEVICE.get_device_info, @@ -348,7 +344,7 @@ async def test_dhcp_discovery_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -361,5 +357,5 @@ async def test_dhcp_discovery_cannot_connect_no_uuid(hass: HomeAssistant) -> Non result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=DHCP_DISCOVERY ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/sensibo/test_config_flow.py b/tests/components/sensibo/test_config_flow.py index 7e92e1f2eb3..509fe8633d7 100644 --- a/tests/components/sensibo/test_config_flow.py +++ b/tests/components/sensibo/test_config_flow.py @@ -12,11 +12,7 @@ import pytest from homeassistant import config_entries from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -30,7 +26,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -51,7 +47,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["version"] == 2 assert result2["data"] == { "api_key": "1234567890", @@ -78,7 +74,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -111,7 +107,7 @@ async def test_flow_fails( }, ) - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Sensibo" assert result3["data"] == { "api_key": "1234567891", @@ -125,7 +121,7 @@ async def test_flow_get_no_devices(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -152,7 +148,7 @@ async def test_flow_get_no_username(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER with patch( @@ -192,7 +188,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -211,7 +207,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == {"api_key": "1234567891"} @@ -261,7 +257,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} with patch( @@ -280,7 +276,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == {"api_key": "1234567891"} @@ -330,7 +326,7 @@ async def test_flow_reauth_no_username_or_device( data=entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" with patch( @@ -349,5 +345,5 @@ async def test_flow_reauth_no_username_or_device( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py index 77dd440a2da..984c486c69e 100644 --- a/tests/components/sentry/test_config_flow.py +++ b/tests/components/sentry/test_config_flow.py @@ -17,11 +17,7 @@ from homeassistant.components.sentry.const import ( ) from homeassistant.config_entries 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 homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -32,7 +28,7 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert "flow_id" in result @@ -62,7 +58,7 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -82,7 +78,7 @@ async def test_user_flow_bad_dsn(hass: HomeAssistant) -> None: {"dsn": "foo"}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("errors") == {"base": "bad_dsn"} @@ -102,7 +98,7 @@ async def test_user_flow_unknown_exception(hass: HomeAssistant) -> None: {"dsn": "foo"}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("errors") == {"base": "unknown"} @@ -120,7 +116,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert "flow_id" in result @@ -138,7 +134,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("data") == { CONF_ENVIRONMENT: "Test", CONF_EVENT_CUSTOM_COMPONENTS: True, diff --git a/tests/components/skybell/test_config_flow.py b/tests/components/skybell/test_config_flow.py index 0171a522e50..cd2b5053ac7 100644 --- a/tests/components/skybell/test_config_flow.py +++ b/tests/components/skybell/test_config_flow.py @@ -6,11 +6,7 @@ from aioskybell import exceptions from homeassistant.components.skybell.const import 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 homeassistant.data_entry_flow import FlowResultType from . import CONF_CONFIG_FLOW, _patch_skybell, _patch_skybell_devices @@ -38,7 +34,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( @@ -46,7 +42,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: user_input=CONF_CONFIG_FLOW, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "user" assert result["data"] == CONF_CONFIG_FLOW @@ -64,7 +60,7 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -75,7 +71,7 @@ async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -88,7 +84,7 @@ async def test_invalid_credentials(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -100,7 +96,7 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} @@ -115,7 +111,7 @@ async def test_flow_import(hass: HomeAssistant) -> None: result["flow_id"], user_input=CONF_CONFIG_FLOW, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "user" assert result["data"] == CONF_CONFIG_FLOW @@ -133,5 +129,5 @@ async def test_flow_import_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_IMPORT}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/slimproto/test_config_flow.py b/tests/components/slimproto/test_config_flow.py index 0c0c843f0b5..15ea5434fc5 100644 --- a/tests/components/slimproto/test_config_flow.py +++ b/tests/components/slimproto/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock from homeassistant.components.slimproto.const import DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -15,7 +15,7 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == DEFAULT_NAME assert result.get("data") == {} @@ -34,5 +34,5 @@ async def test_already_configured( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" diff --git a/tests/components/sma/test_config_flow.py b/tests/components/sma/test_config_flow.py index 8cf22b3634e..eeaa0d75f07 100644 --- a/tests/components/sma/test_config_flow.py +++ b/tests/components/sma/test_config_flow.py @@ -9,11 +9,7 @@ from pysma.exceptions import ( from homeassistant.components.sma.const import DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import MOCK_DEVICE, MOCK_USER_INPUT, _patch_async_setup_entry @@ -24,7 +20,7 @@ async def test_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch("pysma.SMA.new_session", return_value=True), patch( @@ -36,7 +32,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_USER_INPUT["host"] assert result["data"] == MOCK_USER_INPUT @@ -57,7 +53,7 @@ async def test_form_cannot_connect(hass): MOCK_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} assert len(mock_setup_entry.mock_calls) == 0 @@ -76,7 +72,7 @@ async def test_form_invalid_auth(hass): MOCK_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} assert len(mock_setup_entry.mock_calls) == 0 @@ -95,7 +91,7 @@ async def test_form_cannot_retrieve_device_info(hass): MOCK_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_retrieve_device_info"} assert len(mock_setup_entry.mock_calls) == 0 @@ -114,7 +110,7 @@ async def test_form_unexpected_exception(hass): MOCK_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "unknown"} assert len(mock_setup_entry.mock_calls) == 0 @@ -138,6 +134,6 @@ async def test_form_already_configured(hass, mock_config_entry): MOCK_USER_INPUT, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/sonarr/test_config_flow.py b/tests/components/sonarr/test_config_flow.py index 59783995d23..5eea0974dee 100644 --- a/tests/components/sonarr/test_config_flow.py +++ b/tests/components/sonarr/test_config_flow.py @@ -13,11 +13,7 @@ from homeassistant.components.sonarr.const import ( from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_SOURCE, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.components.sonarr import MOCK_REAUTH_INPUT, MOCK_USER_INPUT @@ -31,7 +27,7 @@ async def test_show_user_form(hass: HomeAssistant) -> None: ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM async def test_cannot_connect( @@ -47,7 +43,7 @@ async def test_cannot_connect( data=user_input, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -67,7 +63,7 @@ async def test_invalid_auth( data=user_input, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "invalid_auth"} @@ -85,7 +81,7 @@ async def test_unknown_error( data=user_input, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -108,14 +104,14 @@ async def test_full_reauth_flow_implementation( data=entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" user_input = MOCK_REAUTH_INPUT.copy() @@ -124,7 +120,7 @@ async def test_full_reauth_flow_implementation( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert entry.data[CONF_API_KEY] == "test-api-key-reauth" @@ -141,7 +137,7 @@ async def test_full_user_flow_implementation( context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" user_input = MOCK_USER_INPUT.copy() @@ -151,7 +147,7 @@ async def test_full_user_flow_implementation( user_input=user_input, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.189" assert result["data"] @@ -168,7 +164,7 @@ async def test_full_user_flow_advanced_options( DOMAIN, context={CONF_SOURCE: SOURCE_USER, "show_advanced_options": True} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" user_input = { @@ -181,7 +177,7 @@ async def test_full_user_flow_advanced_options( user_input=user_input, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "192.168.1.189" assert result["data"] @@ -203,7 +199,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -212,6 +208,6 @@ async def test_options_flow( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_UPCOMING_DAYS] == 2 assert result["data"][CONF_WANTED_MAX_ITEMS] == 100 diff --git a/tests/components/songpal/test_config_flow.py b/tests/components/songpal/test_config_flow.py index 4d58639a1d0..85293d37d9c 100644 --- a/tests/components/songpal/test_config_flow.py +++ b/tests/components/songpal/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components import ssdp from homeassistant.components.songpal.const import CONF_ENDPOINT, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( CONF_DATA, @@ -79,7 +75,7 @@ async def test_flow_ssdp(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == FRIENDLY_NAME assert result["data"] == CONF_DATA @@ -93,7 +89,7 @@ async def test_flow_user(hass): DOMAIN, context={"source": SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None _flow_next(hass, result["flow_id"]) @@ -102,7 +98,7 @@ async def test_flow_user(hass): result["flow_id"], user_input={CONF_ENDPOINT: ENDPOINT}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MODEL assert result["data"] == { CONF_NAME: MODEL, @@ -121,7 +117,7 @@ async def test_flow_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == FRIENDLY_NAME assert result["data"] == CONF_DATA @@ -137,7 +133,7 @@ async def test_flow_import_without_name(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data={CONF_ENDPOINT: ENDPOINT} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MODEL assert result["data"] == {CONF_NAME: MODEL, CONF_ENDPOINT: ENDPOINT} @@ -165,7 +161,7 @@ async def test_ssdp_bravia(hass): context={"source": SOURCE_SSDP}, data=ssdp_data, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_songpal_device" @@ -177,7 +173,7 @@ async def test_sddp_exist(hass): context={"source": SOURCE_SSDP}, data=SSDP_DATA, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -190,7 +186,7 @@ async def test_user_exist(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" mocked_device.get_supported_methods.assert_called_once() @@ -206,7 +202,7 @@ async def test_import_exist(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" mocked_device.get_supported_methods.assert_called_once() @@ -222,7 +218,7 @@ async def test_user_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "cannot_connect"} @@ -239,7 +235,7 @@ async def test_import_invalid(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=CONF_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" mocked_device.get_supported_methods.assert_called_once() diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index 47957ead98e..7c3571f8f19 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -8,11 +8,7 @@ from sqlalchemy.exc import SQLAlchemyError from homeassistant import config_entries from homeassistant.components.sql.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 homeassistant.data_entry_flow import FlowResultType from . import ( ENTRY_CONFIG, @@ -30,7 +26,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -43,7 +39,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Get Value" assert result2["options"] == { "db_url": "sqlite://", @@ -70,7 +66,7 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Get Value" assert result2["options"] == { "db_url": "sqlite://", @@ -102,7 +98,7 @@ async def test_import_flow_already_exist(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -112,7 +108,7 @@ async def test_flow_fails_db_url(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == RESULT_TYPE_FORM + assert result4["type"] == FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( @@ -133,7 +129,7 @@ async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == RESULT_TYPE_FORM + assert result4["type"] == FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER result5 = await hass.config_entries.flow.async_configure( @@ -141,7 +137,7 @@ async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None: user_input=ENTRY_CONFIG_INVALID_QUERY, ) - assert result5["type"] == RESULT_TYPE_FORM + assert result5["type"] == FlowResultType.FORM assert result5["errors"] == { "query": "query_invalid", } @@ -151,7 +147,7 @@ async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None: user_input=ENTRY_CONFIG_NO_RESULTS, ) - assert result5["type"] == RESULT_TYPE_FORM + assert result5["type"] == FlowResultType.FORM assert result5["errors"] == { "query": "query_invalid", } @@ -161,7 +157,7 @@ async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None: user_input=ENTRY_CONFIG, ) - assert result5["type"] == RESULT_TYPE_CREATE_ENTRY + assert result5["type"] == FlowResultType.CREATE_ENTRY assert result5["title"] == "Get Value" assert result5["options"] == { "db_url": "sqlite://", @@ -198,7 +194,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -211,7 +207,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "Get Value", "db_url": "sqlite://", @@ -243,7 +239,7 @@ async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" with patch( @@ -262,7 +258,7 @@ async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "Get Value Title", "db_url": "sqlite://", @@ -347,7 +343,7 @@ async def test_options_flow_fails_invalid_query( user_input=ENTRY_CONFIG_INVALID_QUERY_OPT, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == { "query": "query_invalid", } @@ -362,7 +358,7 @@ async def test_options_flow_fails_invalid_query( }, ) - assert result4["type"] == RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == FlowResultType.CREATE_ENTRY assert result4["data"] == { "name": "Get Value", "value_template": None, diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index 22181d73fd3..a36abcc77aa 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant import config_entries from homeassistant.components import dhcp from homeassistant.components.squeezebox.const import DOMAIN 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, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -50,7 +46,7 @@ async def test_user_form(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "edit" assert CONF_HOST in result["data_schema"].schema for key in result["data_schema"].schema: @@ -62,7 +58,7 @@ async def test_user_form(hass): result["flow_id"], {CONF_HOST: HOST, CONF_PORT: PORT, CONF_USERNAME: "", CONF_PASSWORD: ""}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == HOST assert result["data"] == { CONF_HOST: HOST, @@ -84,14 +80,14 @@ async def test_user_form_timeout(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "no_server_found"} # simulate manual input of host result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_HOST: HOST2} ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "edit" assert CONF_HOST in result2["data_schema"].schema for key in result2["data_schema"].schema: @@ -113,7 +109,7 @@ async def test_user_form_duplicate(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "no_server_found"} @@ -138,7 +134,7 @@ async def test_form_invalid_auth(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} @@ -162,7 +158,7 @@ async def test_form_cannot_connect(hass): }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} @@ -177,7 +173,7 @@ async def test_discovery(hass): context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_HOST: HOST, CONF_PORT: PORT, "uuid": UUID}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "edit" @@ -189,7 +185,7 @@ async def test_discovery_no_uuid(hass): context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, data={CONF_HOST: HOST, CONF_PORT: PORT}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "edit" @@ -207,7 +203,7 @@ async def test_dhcp_discovery(hass): hostname="any", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "edit" @@ -226,7 +222,7 @@ async def test_dhcp_discovery_no_server_found(hass): hostname="any", ), ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" @@ -245,4 +241,4 @@ async def test_dhcp_discovery_existing_player(hass): hostname="any", ), ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT diff --git a/tests/components/steamist/test_config_flow.py b/tests/components/steamist/test_config_flow.py index ed887bb6049..0472b847e4e 100644 --- a/tests/components/steamist/test_config_flow.py +++ b/tests/components/steamist/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components import dhcp from homeassistant.components.steamist.const import DOMAIN from homeassistant.const import CONF_DEVICE, CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import ( DEFAULT_ENTRY_DATA, @@ -46,7 +42,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with _patch_discovery(no_device=True), patch( @@ -63,7 +59,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "127.0.0.1" assert result2["data"] == { "host": "127.0.0.1", @@ -76,7 +72,7 @@ async def test_form_with_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with _patch_discovery(), patch( @@ -93,7 +89,7 @@ async def test_form_with_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == DEVICE_NAME assert result2["data"] == DEFAULT_ENTRY_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -116,7 +112,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -137,7 +133,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -148,13 +144,13 @@ async def test_discovery(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.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"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "pick_device" assert not result2["errors"] @@ -162,7 +158,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -183,7 +179,7 @@ async def test_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == DEVICE_NAME assert result3["data"] == DEFAULT_ENTRY_DATA mock_setup.assert_called_once() @@ -193,7 +189,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert not result["errors"] @@ -201,7 +197,7 @@ async def test_discovery(hass: HomeAssistant) -> None: result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -215,7 +211,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data=DISCOVERY_30303, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE): @@ -225,7 +221,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: data=DHCP_DISCOVERY, ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE): @@ -239,7 +235,7 @@ async def test_discovered_by_discovery_and_dhcp(hass: HomeAssistant) -> None: ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" @@ -254,7 +250,7 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE), patch( @@ -265,7 +261,7 @@ async def test_discovered_by_discovery(hass: HomeAssistant) -> None: 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["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == DEFAULT_ENTRY_DATA assert mock_async_setup.called assert mock_async_setup_entry.called @@ -282,7 +278,7 @@ async def test_discovered_by_dhcp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_status(MOCK_ASYNC_GET_STATUS_INACTIVE), patch( @@ -293,7 +289,7 @@ async def test_discovered_by_dhcp(hass: HomeAssistant) -> None: 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["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == DEFAULT_ENTRY_DATA assert mock_async_setup.called assert mock_async_setup_entry.called @@ -312,7 +308,7 @@ async def test_discovered_by_dhcp_discovery_fails(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -331,7 +327,7 @@ async def test_discovered_by_dhcp_discovery_finds_non_steamist_device( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_steamist_device" @@ -359,7 +355,7 @@ async def test_discovered_by_dhcp_or_discovery_adds_missing_unique_id( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.unique_id == FORMATTED_MAC_ADDRESS @@ -393,7 +389,7 @@ async def test_discovered_by_dhcp_or_discovery_existing_unique_id_does_not_reloa ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert not mock_setup.called assert not mock_setup_entry.called diff --git a/tests/components/stookalert/test_config_flow.py b/tests/components/stookalert/test_config_flow.py index 6b5dd6fd4ce..50cd56341d6 100644 --- a/tests/components/stookalert/test_config_flow.py +++ b/tests/components/stookalert/test_config_flow.py @@ -4,11 +4,7 @@ from unittest.mock import patch from homeassistant.components.stookalert.const import CONF_PROVINCE, DOMAIN from homeassistant.config_entries 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 homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +15,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -33,7 +29,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Overijssel" assert result2.get("data") == { CONF_PROVINCE: "Overijssel", @@ -61,5 +57,5 @@ async def test_already_configured(hass: HomeAssistant) -> None: }, ) - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "already_configured" diff --git a/tests/components/sun/test_config_flow.py b/tests/components/sun/test_config_flow.py index 1712e8f6fc9..7d20a57ba27 100644 --- a/tests/components/sun/test_config_flow.py +++ b/tests/components/sun/test_config_flow.py @@ -6,11 +6,7 @@ import pytest from homeassistant.components.sun.const import 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 homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -21,7 +17,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -34,7 +30,7 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: user_input={}, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "Sun" assert result.get("data") == {} assert result.get("options") == {} @@ -55,7 +51,7 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -69,7 +65,7 @@ async def test_import_flow( data={}, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "Sun" assert result.get("data") == {} assert result.get("options") == {} diff --git a/tests/components/surepetcare/test_config_flow.py b/tests/components/surepetcare/test_config_flow.py index e504a807f08..5f6aa6311e0 100644 --- a/tests/components/surepetcare/test_config_flow.py +++ b/tests/components/surepetcare/test_config_flow.py @@ -6,11 +6,7 @@ from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError from homeassistant import config_entries from homeassistant.components.surepetcare.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 homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -26,7 +22,7 @@ async def test_form(hass: HomeAssistant, surepetcare: NonCallableMagicMock) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -42,7 +38,7 @@ async def test_form(hass: HomeAssistant, surepetcare: NonCallableMagicMock) -> N ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Sure Petcare" assert result2["data"] == { "username": "test-username", @@ -70,7 +66,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -92,7 +88,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -114,7 +110,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -145,7 +141,7 @@ async def test_flow_entry_already_exists( }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/switch_as_x/test_config_flow.py b/tests/components/switch_as_x/test_config_flow.py index d80f7e24bb1..8664e3e0379 100644 --- a/tests/components/switch_as_x/test_config_flow.py +++ b/tests/components/switch_as_x/test_config_flow.py @@ -9,7 +9,7 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components.switch_as_x.const import CONF_TARGET_DOMAIN, DOMAIN from homeassistant.const import CONF_ENTITY_ID, Platform from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er from tests.common import MockConfigEntry @@ -33,7 +33,7 @@ async def test_config_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -45,7 +45,7 @@ async def test_config_flow( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "ceiling" assert result["data"] == {} assert result["options"] == { @@ -88,7 +88,7 @@ async def test_config_flow_registered_entity( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -100,7 +100,7 @@ async def test_config_flow_registered_entity( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "ceiling" assert result["data"] == {} assert result["options"] == { diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index 59871681dfe..814e79cf591 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.switchbot.const import ( ) from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import USER_INPUT, USER_INPUT_CURTAIN, init_integration, patch_async_setup_entry @@ -26,7 +22,7 @@ async def test_user_form_valid_mac(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -37,7 +33,7 @@ async def test_user_form_valid_mac(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-name" assert result["data"] == { CONF_MAC: "e7:89:43:99:99:99", @@ -53,7 +49,7 @@ async def test_user_form_valid_mac(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -64,7 +60,7 @@ async def test_user_form_valid_mac(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-name" assert result["data"] == { CONF_MAC: "e7:89:43:90:90:90", @@ -80,7 +76,7 @@ async def test_user_form_valid_mac(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_unconfigured_devices" @@ -93,7 +89,7 @@ async def test_user_form_exception(hass, switchbot_config_flow): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" switchbot_config_flow.side_effect = Exception @@ -102,7 +98,7 @@ async def test_user_form_exception(hass, switchbot_config_flow): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -112,7 +108,7 @@ async def test_options_flow(hass): entry = await init_integration(hass) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -127,7 +123,7 @@ async def test_options_flow(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_TIME_BETWEEN_UPDATE_COMMAND] == 60 assert result["data"][CONF_RETRY_COUNT] == 3 assert result["data"][CONF_RETRY_TIMEOUT] == 5 @@ -141,7 +137,7 @@ async def test_options_flow(hass): entry = await init_integration(hass) result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] is None @@ -156,7 +152,7 @@ async def test_options_flow(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_TIME_BETWEEN_UPDATE_COMMAND] == 66 assert result["data"][CONF_RETRY_COUNT] == 6 assert result["data"][CONF_RETRY_TIMEOUT] == 6 diff --git a/tests/components/switcher_kis/test_config_flow.py b/tests/components/switcher_kis/test_config_flow.py index 2029e4a8ef3..f5ad68b6033 100644 --- a/tests/components/switcher_kis/test_config_flow.py +++ b/tests/components/switcher_kis/test_config_flow.py @@ -5,11 +5,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.switcher_kis.const import DATA_DISCOVERY, DOMAIN -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .consts import DUMMY_PLUG_DEVICE, DUMMY_WATER_HEATER_DEVICE @@ -25,7 +21,7 @@ async def test_import(hass): DOMAIN, context={"source": config_entries.SOURCE_IMPORT} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Switcher" assert result["data"] == {} @@ -53,7 +49,7 @@ async def test_user_setup(hass, mock_bridge): assert mock_bridge.is_running is False assert len(hass.data[DOMAIN][DATA_DISCOVERY].result()) == 2 - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None @@ -62,7 +58,7 @@ async def test_user_setup(hass, mock_bridge): ): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Switcher" assert result2["result"].data == {} @@ -78,13 +74,13 @@ async def test_user_setup_abort_no_devices_found(hass, mock_bridge): assert mock_bridge.is_running is False assert len(hass.data[DOMAIN][DATA_DISCOVERY].result()) == 0 - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["errors"] is None result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -104,5 +100,5 @@ async def test_single_instance(hass, source): DOMAIN, context={"source": source} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/tailscale/test_config_flow.py b/tests/components/tailscale/test_config_flow.py index eb070cfdbb2..78e3f20a61b 100644 --- a/tests/components/tailscale/test_config_flow.py +++ b/tests/components/tailscale/test_config_flow.py @@ -8,11 +8,7 @@ from homeassistant.components.tailscale.const import CONF_TAILNET, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -27,7 +23,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -39,7 +35,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "homeassistant.github" assert result2.get("data") == { CONF_TAILNET: "homeassistant.github", @@ -64,7 +60,7 @@ async def test_full_flow_with_authentication_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -77,7 +73,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": "invalid_auth"} assert "flow_id" in result2 @@ -94,7 +90,7 @@ async def test_full_flow_with_authentication_error( }, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "homeassistant.github" assert result3.get("data") == { CONF_TAILNET: "homeassistant.github", @@ -120,7 +116,7 @@ async def test_connection_error( }, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {"base": "cannot_connect"} assert len(mock_tailscale_config_flow.devices.mock_calls) == 1 @@ -144,7 +140,7 @@ async def test_reauth_flow( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -154,7 +150,7 @@ async def test_reauth_flow( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_ABORT + assert result2.get("type") == FlowResultType.ABORT assert result2.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_TAILNET: "homeassistant.github", @@ -187,7 +183,7 @@ async def test_reauth_with_authentication_error( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -198,7 +194,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "invalid_auth"} assert "flow_id" in result2 @@ -213,7 +209,7 @@ async def test_reauth_with_authentication_error( ) await hass.async_block_till_done() - assert result3.get("type") == RESULT_TYPE_ABORT + assert result3.get("type") == FlowResultType.ABORT assert result3.get("reason") == "reauth_successful" assert mock_config_entry.data == { CONF_TAILNET: "homeassistant.github", @@ -241,7 +237,7 @@ async def test_reauth_api_error( }, data=mock_config_entry.data, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" assert "flow_id" in result @@ -252,6 +248,6 @@ async def test_reauth_api_error( ) await hass.async_block_till_done() - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/tankerkoenig/test_config_flow.py b/tests/components/tankerkoenig/test_config_flow.py index cae78a447f8..600bfd98c73 100644 --- a/tests/components/tankerkoenig/test_config_flow.py +++ b/tests/components/tankerkoenig/test_config_flow.py @@ -19,11 +19,7 @@ from homeassistant.const import ( CONF_SHOW_ON_MAP, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -91,7 +87,7 @@ async def test_user(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -103,13 +99,13 @@ async def test_user(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "select_station" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_STATIONS_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_NAME] == "Home" assert result["data"][CONF_API_KEY] == "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx" assert result["data"][CONF_FUEL_TYPES] == ["e5"] @@ -139,14 +135,14 @@ async def test_user_already_configured(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -155,7 +151,7 @@ async def test_exception_security(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -166,7 +162,7 @@ async def test_exception_security(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_API_KEY] == "invalid_auth" @@ -176,7 +172,7 @@ async def test_user_no_stations(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" with patch( @@ -186,7 +182,7 @@ async def test_user_no_stations(hass: HomeAssistant): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"][CONF_RADIUS] == "no_stations" @@ -202,8 +198,8 @@ async def test_import(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_IMPORT_DATA ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_NAME] == "Home" assert result["data"][CONF_API_KEY] == "269534f6-xxxx-xxxx-xxxx-yyyyzzzzxxxx" assert result["data"][CONF_FUEL_TYPES] == ["e5"] @@ -243,7 +239,7 @@ async def test_reauth(hass: HomeAssistant): context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, data=mock_config.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" # re-auth unsuccessful @@ -254,7 +250,7 @@ async def test_reauth(hass: HomeAssistant): CONF_API_KEY: "269534f6-aaaa-bbbb-cccc-yyyyzzzzxxxx", }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" assert result["errors"] == {CONF_API_KEY: "invalid_auth"} @@ -266,7 +262,7 @@ async def test_reauth(hass: HomeAssistant): CONF_API_KEY: "269534f6-aaaa-bbbb-cccc-yyyyzzzzxxxx", }, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" mock_setup_entry.assert_called() @@ -297,7 +293,7 @@ async def test_options_flow(hass: HomeAssistant): assert mock_setup_entry.called result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -306,5 +302,5 @@ async def test_options_flow(hass: HomeAssistant): CONF_STATIONS: MOCK_OPTIONS_DATA[CONF_STATIONS], }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert not mock_config.options[CONF_SHOW_ON_MAP] diff --git a/tests/components/tesla_wall_connector/test_config_flow.py b/tests/components/tesla_wall_connector/test_config_flow.py index 2e286f75b61..5c4ba11b05a 100644 --- a/tests/components/tesla_wall_connector/test_config_flow.py +++ b/tests/components/tesla_wall_connector/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components import dhcp from homeassistant.components.tesla_wall_connector.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,7 +18,7 @@ async def test_form(mock_wall_connector_version, hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -31,7 +31,7 @@ async def test_form(mock_wall_connector_version, hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Tesla Wall Connector" assert result2["data"] == {CONF_HOST: "1.1.1.1"} assert len(mock_setup_entry.mock_calls) == 1 @@ -52,7 +52,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: {CONF_HOST: "1.1.1.1"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -73,7 +73,7 @@ async def test_form_other_error( {CONF_HOST: "1.1.1.1"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -128,7 +128,7 @@ async def test_dhcp_can_finish( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_HOST: "1.2.3.4"} diff --git a/tests/components/threshold/test_config_flow.py b/tests/components/threshold/test_config_flow.py index 0a10c24a2f6..3d0ff53a4a9 100644 --- a/tests/components/threshold/test_config_flow.py +++ b/tests/components/threshold/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.threshold.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,7 +18,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -36,7 +36,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My threshold sensor" assert result["data"] == {} assert result["options"] == { @@ -68,7 +68,7 @@ async def test_fail(hass: HomeAssistant, extra_input_data, error) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -80,7 +80,7 @@ async def test_fail(hass: HomeAssistant, extra_input_data, error) -> None: }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": error} @@ -118,7 +118,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "hysteresis") == 0.0 @@ -132,7 +132,7 @@ async def test_options(hass: HomeAssistant) -> None: "upper": 20.0, }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "entity_id": input_sensor, "hysteresis": 0.0, diff --git a/tests/components/tod/test_config_flow.py b/tests/components/tod/test_config_flow.py index 35f9ef0d5bd..e215a4f1ae9 100644 --- a/tests/components/tod/test_config_flow.py +++ b/tests/components/tod/test_config_flow.py @@ -7,7 +7,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.tod.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -18,7 +18,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -35,7 +35,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My tod" assert result["data"] == {} assert result["options"] == { @@ -85,7 +85,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "after_time") == "10:00" @@ -98,7 +98,7 @@ async def test_options(hass: HomeAssistant) -> None: "before_time": "17:05", }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "after_time": "10:00", "before_time": "17:05", diff --git a/tests/components/tolo/test_config_flow.py b/tests/components/tolo/test_config_flow.py index 9991decc511..38542ad7db5 100644 --- a/tests/components/tolo/test_config_flow.py +++ b/tests/components/tolo/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant.components.tolo.const import DOMAIN from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER 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 homeassistant.data_entry_flow import FlowResultType MOCK_DHCP_DATA = dhcp.DhcpServiceInfo( ip="127.0.0.2", macaddress="00:11:22:33:44:55", hostname="mock_hostname" @@ -37,7 +33,7 @@ async def test_user_with_timed_out_host(hass: HomeAssistant, toloclient: Mock): data={CONF_HOST: "127.0.0.1"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert result["errors"] == {"base": "cannot_connect"} @@ -48,7 +44,7 @@ async def test_user_walkthrough(hass: HomeAssistant, toloclient: Mock): DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER assert "flow_id" in result @@ -59,7 +55,7 @@ async def test_user_walkthrough(hass: HomeAssistant, toloclient: Mock): user_input={CONF_HOST: "127.0.0.2"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == SOURCE_USER assert result2["errors"] == {"base": "cannot_connect"} assert "flow_id" in result2 @@ -71,7 +67,7 @@ async def test_user_walkthrough(hass: HomeAssistant, toloclient: Mock): user_input={CONF_HOST: "127.0.0.1"}, ) - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "TOLO Sauna" assert result3["data"][CONF_HOST] == "127.0.0.1" @@ -83,7 +79,7 @@ async def test_dhcp(hass: HomeAssistant, toloclient: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" result = await hass.config_entries.flow.async_configure( @@ -91,7 +87,7 @@ async def test_dhcp(hass: HomeAssistant, toloclient: Mock): user_input={}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "TOLO Sauna" assert result["data"][CONF_HOST] == "127.0.0.2" assert result["result"].unique_id == "00:11:22:33:44:55" @@ -104,4 +100,4 @@ async def test_dhcp_invalid_device(hass: HomeAssistant, toloclient: Mock): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_DHCP}, data=MOCK_DHCP_DATA ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT diff --git a/tests/components/tplink/test_config_flow.py b/tests/components/tplink/test_config_flow.py index a3792238fb2..77ab43a05b5 100644 --- a/tests/components/tplink/test_config_flow.py +++ b/tests/components/tplink/test_config_flow.py @@ -8,7 +8,7 @@ from homeassistant.components import dhcp from homeassistant.components.tplink import DOMAIN from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from . import ( ALIAS, @@ -256,7 +256,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): data={CONF_HOST: IP_ADDRESS, CONF_MAC: MAC_ADDRESS, CONF_NAME: ALIAS}, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_single_discovery(): @@ -268,7 +268,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_single_discovery(): @@ -280,7 +280,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" with _patch_discovery(no_device=True), _patch_single_discovery(no_device=True): @@ -292,7 +292,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -318,7 +318,7 @@ async def test_discovered_by_dhcp_or_discovery(hass, source, data): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_single_discovery(), patch( @@ -358,5 +358,5 @@ async def test_discovered_by_dhcp_or_discovery_failed_to_get_device(hass, source DOMAIN, context={"source": source}, data=data ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" diff --git a/tests/components/trafikverket_ferry/test_config_flow.py b/tests/components/trafikverket_ferry/test_config_flow.py index c75937999d0..47befa55fed 100644 --- a/tests/components/trafikverket_ferry/test_config_flow.py +++ b/tests/components/trafikverket_ferry/test_config_flow.py @@ -14,11 +14,7 @@ from homeassistant.components.trafikverket_ferry.const import ( ) from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -29,7 +25,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -50,7 +46,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Ekerö to Slagsta at 10:00" assert result2["data"] == { "api_key": "1234567890", @@ -91,7 +87,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == RESULT_TYPE_FORM + assert result4["type"] == FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( @@ -137,7 +133,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -152,7 +148,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", @@ -220,7 +216,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} with patch( @@ -235,7 +231,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", diff --git a/tests/components/trafikverket_train/test_config_flow.py b/tests/components/trafikverket_train/test_config_flow.py index 37788fc285b..8a4baa18ec2 100644 --- a/tests/components/trafikverket_train/test_config_flow.py +++ b/tests/components/trafikverket_train/test_config_flow.py @@ -14,11 +14,7 @@ from homeassistant.components.trafikverket_train.const import ( ) from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, WEEKDAYS from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -29,7 +25,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -50,7 +46,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Stockholm C to Uppsala C at 10:00" assert result2["data"] == { "api_key": "1234567890", @@ -86,7 +82,7 @@ async def test_form_entry_already_exist(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -107,7 +103,7 @@ async def test_form_entry_already_exist(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -140,7 +136,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == RESULT_TYPE_FORM + assert result4["type"] == FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( @@ -165,7 +161,7 @@ async def test_flow_fails_incorrect_time(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result5["type"] == RESULT_TYPE_FORM + assert result5["type"] == FlowResultType.FORM assert result5["step_id"] == config_entries.SOURCE_USER with patch( @@ -210,7 +206,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -225,7 +221,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", @@ -297,7 +293,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} with patch( @@ -312,7 +308,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "api_key": "1234567891", diff --git a/tests/components/trafikverket_weatherstation/test_config_flow.py b/tests/components/trafikverket_weatherstation/test_config_flow.py index 458652f8175..89fac5fd5df 100644 --- a/tests/components/trafikverket_weatherstation/test_config_flow.py +++ b/tests/components/trafikverket_weatherstation/test_config_flow.py @@ -8,7 +8,7 @@ import pytest from homeassistant import config_entries from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType DOMAIN = "trafikverket_weatherstation" CONF_STATION = "station" @@ -76,7 +76,7 @@ async def test_flow_fails( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result4["type"] == RESULT_TYPE_FORM + assert result4["type"] == FlowResultType.FORM assert result4["step_id"] == config_entries.SOURCE_USER with patch( diff --git a/tests/components/twentemilieu/test_config_flow.py b/tests/components/twentemilieu/test_config_flow.py index aec0f29e590..05e20dd06bd 100644 --- a/tests/components/twentemilieu/test_config_flow.py +++ b/tests/components/twentemilieu/test_config_flow.py @@ -14,11 +14,7 @@ from homeassistant.components.twentemilieu.const import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -33,7 +29,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -46,7 +42,7 @@ async def test_full_user_flow( }, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "12345" assert result2.get("data") == { CONF_ID: 12345, @@ -70,7 +66,7 @@ async def test_invalid_address( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -83,7 +79,7 @@ async def test_invalid_address( }, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": "invalid_address"} assert "flow_id" in result2 @@ -97,7 +93,7 @@ async def test_invalid_address( }, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "12345" assert result3.get("data") == { CONF_ID: 12345, @@ -124,7 +120,7 @@ async def test_connection_error( }, ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert result.get("errors") == {"base": "cannot_connect"} @@ -146,5 +142,5 @@ async def test_address_already_set_up( }, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" diff --git a/tests/components/ukraine_alarm/test_config_flow.py b/tests/components/ukraine_alarm/test_config_flow.py index 7369816fdc7..66945e972de 100644 --- a/tests/components/ukraine_alarm/test_config_flow.py +++ b/tests/components/ukraine_alarm/test_config_flow.py @@ -10,11 +10,7 @@ from yarl import URL from homeassistant import config_entries from homeassistant.components.ukraine_alarm.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 homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -60,10 +56,10 @@ async def test_state(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -77,7 +73,7 @@ async def test_state(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "State 1" assert result3["data"] == { "region": "1", @@ -91,10 +87,10 @@ async def test_state_district(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -102,7 +98,7 @@ async def test_state_district(hass: HomeAssistant) -> None: "region": "2", }, ) - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -116,7 +112,7 @@ async def test_state_district(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == FlowResultType.CREATE_ENTRY assert result4["title"] == "District 2.2" assert result4["data"] == { "region": "2.2", @@ -130,10 +126,10 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -141,7 +137,7 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: "region": "2", }, ) - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -155,7 +151,7 @@ async def test_state_district_pick_region(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result4["type"] == RESULT_TYPE_CREATE_ENTRY + assert result4["type"] == FlowResultType.CREATE_ENTRY assert result4["title"] == "State 2" assert result4["data"] == { "region": "2", @@ -169,12 +165,12 @@ async def test_state_district_community(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure( result["flow_id"], ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -182,7 +178,7 @@ async def test_state_district_community(hass: HomeAssistant) -> None: "region": "3", }, ) - assert result3["type"] == RESULT_TYPE_FORM + assert result3["type"] == FlowResultType.FORM result4 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -190,7 +186,7 @@ async def test_state_district_community(hass: HomeAssistant) -> None: "region": "3.2", }, ) - assert result4["type"] == RESULT_TYPE_FORM + assert result4["type"] == FlowResultType.FORM with patch( "homeassistant.components.ukraine_alarm.async_setup_entry", @@ -204,7 +200,7 @@ async def test_state_district_community(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result5["type"] == RESULT_TYPE_CREATE_ENTRY + assert result5["type"] == FlowResultType.CREATE_ENTRY assert result5["title"] == "Community 3.2.1" assert result5["data"] == { "region": "3.2.1", @@ -235,7 +231,7 @@ async def test_rate_limit(hass: HomeAssistant, mock_get_regions: AsyncMock) -> N result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "rate_limit" @@ -247,7 +243,7 @@ async def test_server_error(hass: HomeAssistant, mock_get_regions) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -257,7 +253,7 @@ async def test_cannot_connect(hass: HomeAssistant, mock_get_regions: AsyncMock) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -269,7 +265,7 @@ async def test_unknown_client_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" @@ -279,7 +275,7 @@ async def test_timeout_error(hass: HomeAssistant, mock_get_regions: AsyncMock) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "timeout" @@ -291,5 +287,5 @@ async def test_no_regions_returned( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 3d561f2d781..5304e05fe13 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -19,11 +19,7 @@ from homeassistant.components.unifiprotect.const import ( ) 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 homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr from . import ( @@ -66,7 +62,7 @@ async def test_form(hass: HomeAssistant, nvr: NVR) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] with patch( @@ -86,7 +82,7 @@ async def test_form(hass: HomeAssistant, nvr: NVR) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": "1.1.1.1", @@ -118,7 +114,7 @@ async def test_form_version_too_old(hass: HomeAssistant, old_nvr: NVR) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "protect_version"} @@ -141,7 +137,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} @@ -164,7 +160,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -191,7 +187,7 @@ async def test_form_reauth_auth(hass: HomeAssistant, nvr: NVR) -> None: "entry_id": mock_config.entry_id, }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -211,7 +207,7 @@ async def test_form_reauth_auth(hass: HomeAssistant, nvr: NVR) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"password": "invalid_auth"} assert result2["step_id"] == "reauth_confirm" @@ -227,7 +223,7 @@ async def test_form_reauth_auth(hass: HomeAssistant, nvr: NVR) -> None: }, ) - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "reauth_successful" @@ -258,7 +254,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - assert mock_config.state == config_entries.ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(mock_config.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert not result["errors"] assert result["step_id"] == "init" @@ -267,7 +263,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - {CONF_DISABLE_RTSP: True, CONF_ALL_UPDATES: True, CONF_OVERRIDE_CHOST: True}, ) - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == { "all_updates": True, "disable_rtsp": True, @@ -295,7 +291,7 @@ async def test_discovered_by_ssdp_or_dhcp( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "discovery_started" @@ -312,7 +308,7 @@ async def test_discovered_by_unifi_discovery_direct_connect( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -338,7 +334,7 @@ async def test_discovered_by_unifi_discovery_direct_connect( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": DIRECT_CONNECT_DOMAIN, @@ -378,7 +374,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == DIRECT_CONNECT_DOMAIN @@ -413,7 +409,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_updated_but_not_usin ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == "127.0.0.1" @@ -448,7 +444,7 @@ async def test_discovered_by_unifi_discovery_does_not_update_ip_when_console_is_ ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == "1.2.2.2" @@ -479,7 +475,7 @@ async def test_discovered_host_not_updated_if_existing_is_a_hostname( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert mock_config.data[CONF_HOST] == "a.hostname" @@ -495,7 +491,7 @@ async def test_discovered_by_unifi_discovery(hass: HomeAssistant, nvr: NVR) -> N ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -521,7 +517,7 @@ async def test_discovered_by_unifi_discovery(hass: HomeAssistant, nvr: NVR) -> N ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": DEVICE_IP_ADDRESS, @@ -547,7 +543,7 @@ async def test_discovered_by_unifi_discovery_partial( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -573,7 +569,7 @@ async def test_discovered_by_unifi_discovery_partial( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": DEVICE_IP_ADDRESS, @@ -612,7 +608,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -642,7 +638,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -680,7 +676,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -716,7 +712,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) assert flows[0]["context"]["title_placeholders"] == { @@ -742,7 +738,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "UnifiProtect" assert result2["data"] == { "host": "nomatchsameip.ui.direct", @@ -785,7 +781,7 @@ async def test_discovered_by_unifi_discovery_direct_connect_on_different_interfa ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -806,5 +802,5 @@ async def test_discovery_can_be_ignored(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/uptime/test_config_flow.py b/tests/components/uptime/test_config_flow.py index 69ba00f6ac8..9db909003e9 100644 --- a/tests/components/uptime/test_config_flow.py +++ b/tests/components/uptime/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components.uptime.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -25,7 +21,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -34,7 +30,7 @@ async def test_full_user_flow( user_input={}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Uptime" assert result2.get("data") == {} @@ -52,7 +48,7 @@ async def test_single_instance_allowed( DOMAIN, context={"source": source} ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" @@ -67,6 +63,6 @@ async def test_import_flow( data={CONF_NAME: "My Uptime"}, ) - assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("type") == FlowResultType.CREATE_ENTRY assert result.get("title") == "My Uptime" assert result.get("data") == {} diff --git a/tests/components/uptimerobot/test_config_flow.py b/tests/components/uptimerobot/test_config_flow.py index c477e1bbc65..b058ceecbdc 100644 --- a/tests/components/uptimerobot/test_config_flow.py +++ b/tests/components/uptimerobot/test_config_flow.py @@ -9,11 +9,7 @@ from homeassistant import config_entries from homeassistant.components.uptimerobot.const import DOMAIN from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from .common import ( MOCK_UPTIMEROBOT_ACCOUNT, @@ -34,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -51,7 +47,7 @@ async def test_form(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["result"].unique_id == MOCK_UPTIMEROBOT_UNIQUE_ID - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == MOCK_UPTIMEROBOT_ACCOUNT["email"] assert result2["data"] == {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY} assert len(mock_setup_entry.mock_calls) == 1 @@ -63,7 +59,7 @@ async def test_form_read_only(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -76,7 +72,7 @@ async def test_form_read_only(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"]["base"] == "not_main_key" @@ -103,7 +99,7 @@ async def test_form_exception_thrown(hass: HomeAssistant, exception, error_key) {CONF_API_KEY: MOCK_UPTIMEROBOT_API_KEY}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"]["base"] == error_key @@ -136,7 +132,7 @@ async def test_user_unique_id_already_exists( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -153,7 +149,7 @@ async def test_user_unique_id_already_exists( await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 0 - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -174,7 +170,7 @@ async def test_reauthentication( data=old_entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -192,7 +188,7 @@ async def test_reauthentication( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" @@ -213,7 +209,7 @@ async def test_reauthentication_failure( data=old_entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -232,7 +228,7 @@ async def test_reauthentication_failure( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"]["base"] == "unknown" @@ -255,7 +251,7 @@ async def test_reauthentication_failure_no_existing_entry( data=old_entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -273,7 +269,7 @@ async def test_reauthentication_failure_no_existing_entry( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_failed_existing" @@ -294,7 +290,7 @@ async def test_reauthentication_failure_account_not_matching( data=old_entry.data, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None assert result["step_id"] == "reauth_confirm" @@ -316,5 +312,5 @@ async def test_reauthentication_failure_account_not_matching( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"]["base"] == "reauth_failed_matching_account" diff --git a/tests/components/utility_meter/test_config_flow.py b/tests/components/utility_meter/test_config_flow.py index 53f9d814f2f..29a8cab9677 100644 --- a/tests/components/utility_meter/test_config_flow.py +++ b/tests/components/utility_meter/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.utility_meter.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -38,7 +38,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Electricity meter" assert result["data"] == {} assert result["options"] == { @@ -73,7 +73,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -88,7 +88,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Electricity meter" assert result["data"] == {} assert result["options"] == { @@ -117,7 +117,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None result = await hass.config_entries.flow.async_configure( @@ -132,7 +132,7 @@ async def test_tariffs(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"]["base"] == "tariffs_not_unique" @@ -172,7 +172,7 @@ async def test_options(hass: HomeAssistant) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "source") == input_sensor1_entity_id @@ -181,7 +181,7 @@ async def test_options(hass: HomeAssistant) -> None: result["flow_id"], user_input={"source": input_sensor2_entity_id}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "cycle": "monthly", "delta_values": False, diff --git a/tests/components/vallox/test_config_flow.py b/tests/components/vallox/test_config_flow.py index ee6b05f1f8b..b0c951383b8 100644 --- a/tests/components/vallox/test_config_flow.py +++ b/tests/components/vallox/test_config_flow.py @@ -7,11 +7,7 @@ from homeassistant.components.vallox.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -23,7 +19,7 @@ async def test_form_no_input(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None @@ -33,7 +29,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert init["type"] == RESULT_TYPE_FORM + assert init["type"] == FlowResultType.FORM assert init["errors"] is None with patch( @@ -49,7 +45,7 @@ async def test_form_create_entry(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Vallox" assert result["data"] == {"host": "1.2.3.4", "name": "Vallox"} assert len(mock_setup_entry.mock_calls) == 1 @@ -67,7 +63,7 @@ async def test_form_invalid_ip(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"host": "invalid_host"} @@ -87,7 +83,7 @@ async def test_form_vallox_api_exception_cannot_connect(hass: HomeAssistant) -> ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"host": "cannot_connect"} @@ -107,7 +103,7 @@ async def test_form_os_error_cannot_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"host": "cannot_connect"} @@ -127,7 +123,7 @@ async def test_form_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"host": "unknown"} @@ -152,7 +148,7 @@ async def test_form_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -174,7 +170,7 @@ async def test_import_with_custom_name(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == name assert result["data"] == {"host": "1.2.3.4", "name": "Vallox 90 MV"} assert len(mock_setup_entry.mock_calls) == 1 @@ -196,7 +192,7 @@ async def test_import_without_custom_name(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Vallox" assert result["data"] == {"host": "1.2.3.4", "name": "Vallox"} assert len(mock_setup_entry.mock_calls) == 1 @@ -213,7 +209,7 @@ async def test_import_invalid_ip(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "invalid_host" @@ -237,7 +233,7 @@ async def test_import_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -256,7 +252,7 @@ async def test_import_cannot_connect_os_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -275,7 +271,7 @@ async def test_import_cannot_connect_vallox_api_exception(hass: HomeAssistant) - ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -294,5 +290,5 @@ async def test_import_unknown_exception(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "unknown" diff --git a/tests/components/venstar/test_config_flow.py b/tests/components/venstar/test_config_flow.py index f568655ec8d..f8f66f1b388 100644 --- a/tests/components/venstar/test_config_flow.py +++ b/tests/components/venstar/test_config_flow.py @@ -13,11 +13,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import VenstarColorTouchMock @@ -41,7 +37,7 @@ async def test_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -57,7 +53,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"] == TEST_DATA assert len(mock_setup_entry.mock_calls) == 1 @@ -77,7 +73,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -96,7 +92,7 @@ async def test_unknown_error(hass: HomeAssistant) -> None: TEST_DATA, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -108,7 +104,7 @@ async def test_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER with patch( @@ -124,5 +120,5 @@ async def test_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index 8448bddb288..759a9049969 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries, data_entry_flow from homeassistant.components.vera import CONF_CONTROLLER, CONF_LEGACY_UNIQUE_ID, DOMAIN from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry, mock_registry @@ -23,7 +23,7 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == config_entries.SOURCE_USER result = await hass.config_entries.flow.async_configure( @@ -34,7 +34,7 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: CONF_EXCLUDE: "14 15", }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:123" assert result["data"] == { CONF_CONTROLLER: "http://127.0.0.1:123", @@ -63,7 +63,7 @@ async def test_async_step_import_success(hass: HomeAssistant) -> None: data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:123" assert result["data"] == { CONF_CONTROLLER: "http://127.0.0.1:123", @@ -94,7 +94,7 @@ async def test_async_step_import_success_with_legacy_unique_id( data={CONF_CONTROLLER: "http://127.0.0.1:123/"}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "http://127.0.0.1:123" assert result["data"] == { CONF_CONTROLLER: "http://127.0.0.1:123", diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index 356cd8fd63c..03fa0fa82cc 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -17,11 +17,7 @@ from homeassistant.components.verisure.const import ( ) from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -38,7 +34,7 @@ async def test_full_user_flow_single_installation(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -61,7 +57,7 @@ async def test_full_user_flow_single_installation(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "ascending (12345th street)" assert result2["data"] == { CONF_GIID: "12345", @@ -79,7 +75,7 @@ async def test_full_user_flow_multiple_installations(hass: HomeAssistant) -> Non DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -100,7 +96,7 @@ async def test_full_user_flow_multiple_installations(hass: HomeAssistant) -> Non await hass.async_block_till_done() assert result2["step_id"] == "installation" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] is None with patch( @@ -112,7 +108,7 @@ async def test_full_user_flow_multiple_installations(hass: HomeAssistant) -> Non ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "descending (54321th street)" assert result3["data"] == { CONF_GIID: "54321", @@ -143,7 +139,7 @@ async def test_invalid_login(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "invalid_auth"} @@ -167,7 +163,7 @@ async def test_unknown_error(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "user" assert result2["errors"] == {"base": "unknown"} @@ -182,7 +178,7 @@ async def test_dhcp(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" @@ -209,7 +205,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -228,7 +224,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { CONF_GIID: "12345", @@ -277,7 +273,7 @@ async def test_reauth_flow_invalid_login(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -318,7 +314,7 @@ async def test_reauth_flow_unknown_error(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} @@ -366,7 +362,7 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -374,7 +370,7 @@ async def test_options_flow( user_input=input, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == output @@ -396,7 +392,7 @@ async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {} @@ -408,6 +404,6 @@ async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: }, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "code_format_mismatch"} diff --git a/tests/components/version/test_config_flow.py b/tests/components/version/test_config_flow.py index 64296498f35..9745272626c 100644 --- a/tests/components/version/test_config_flow.py +++ b/tests/components/version/test_config_flow.py @@ -19,7 +19,7 @@ from homeassistant.components.version.const import ( ) from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from homeassistant.util import dt from .common import MOCK_VERSION, MOCK_VERSION_DATA, setup_version_integration @@ -50,7 +50,7 @@ async def test_basic_form(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": False}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM with patch( "homeassistant.components.version.async_setup_entry", @@ -62,7 +62,7 @@ async def test_basic_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == VERSION_SOURCE_DOCKER_HUB assert result2["data"] == { **DEFAULT_CONFIGURATION, @@ -78,7 +78,7 @@ async def test_advanced_form_pypi(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -86,11 +86,11 @@ async def test_advanced_form_pypi(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "version_source" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "version_source" with patch( @@ -102,7 +102,7 @@ async def test_advanced_form_pypi(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == VERSION_SOURCE_PYPI assert result["data"] == { **DEFAULT_CONFIGURATION, @@ -119,7 +119,7 @@ async def test_advanced_form_container(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -127,11 +127,11 @@ async def test_advanced_form_container(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "version_source" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "version_source" with patch( @@ -143,7 +143,7 @@ async def test_advanced_form_container(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == VERSION_SOURCE_DOCKER_HUB assert result["data"] == { **DEFAULT_CONFIGURATION, @@ -160,7 +160,7 @@ async def test_advanced_form_supervisor(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER, "show_advanced_options": True}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -168,11 +168,11 @@ async def test_advanced_form_supervisor(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "version_source" result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "version_source" with patch( @@ -185,7 +185,7 @@ async def test_advanced_form_supervisor(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"{VERSION_SOURCE_VERSIONS} Dev" assert result["data"] == { **DEFAULT_CONFIGURATION, diff --git a/tests/components/vlc_telnet/test_config_flow.py b/tests/components/vlc_telnet/test_config_flow.py index f86644b3447..66ca5c2cb20 100644 --- a/tests/components/vlc_telnet/test_config_flow.py +++ b/tests/components/vlc_telnet/test_config_flow.py @@ -11,11 +11,7 @@ from homeassistant import config_entries from homeassistant.components.hassio import HassioServiceInfo 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 homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -56,7 +52,7 @@ async def test_user_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch("homeassistant.components.vlc_telnet.config_flow.Client.connect"), patch( @@ -73,7 +69,7 @@ async def test_user_flow( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == entry_data["host"] assert result["data"] == entry_data assert len(mock_setup_entry.mock_calls) == 1 @@ -98,7 +94,7 @@ async def test_abort_already_configured(hass: HomeAssistant, source: str) -> Non data=entry_data, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -137,7 +133,7 @@ async def test_errors( {"password": "test-password"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": error} @@ -177,7 +173,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(mock_setup_entry.mock_calls) == 1 assert dict(entry.data) == {**entry_data, "password": "new-password"} @@ -232,7 +228,7 @@ async def test_reauth_errors( {"password": "test-password"}, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": error} @@ -263,11 +259,11 @@ async def test_hassio_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == test_data.config["name"] assert result2["data"] == test_data.config assert len(mock_setup_entry.mock_calls) == 1 @@ -294,7 +290,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT @pytest.mark.parametrize( @@ -336,9 +332,9 @@ async def test_hassio_errors( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == error diff --git a/tests/components/watttime/test_config_flow.py b/tests/components/watttime/test_config_flow.py index 8027759738f..5e97bd2a396 100644 --- a/tests/components/watttime/test_config_flow.py +++ b/tests/components/watttime/test_config_flow.py @@ -22,11 +22,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType @pytest.mark.parametrize( @@ -43,7 +39,7 @@ async def test_auth_errors( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config_auth ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": error} @@ -78,7 +74,7 @@ async def test_coordinate_errors( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_coordinates ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == errors @@ -95,7 +91,7 @@ async def test_duplicate_error( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_location_type ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -130,7 +126,7 @@ async def test_show_form_coordinates( result["flow_id"], user_input=config_location_type ) result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "coordinates" assert result["errors"] is None @@ -140,7 +136,7 @@ async def test_show_form_user(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] is None @@ -166,7 +162,7 @@ async def test_step_reauth( user_input={CONF_PASSWORD: "password"}, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -188,7 +184,7 @@ async def test_step_user_coordinates( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_coordinates ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "32.87336, -117.22743" assert result["data"] == { CONF_USERNAME: "user", @@ -213,7 +209,7 @@ async def test_step_user_home( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config_location_type ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "32.87336, -117.22743" assert result["data"] == { CONF_USERNAME: "user", diff --git a/tests/components/webostv/test_config_flow.py b/tests/components/webostv/test_config_flow.py index 012682615dc..b5ad3f4cc2b 100644 --- a/tests/components/webostv/test_config_flow.py +++ b/tests/components/webostv/test_config_flow.py @@ -17,11 +17,7 @@ from homeassistant.const import ( CONF_SOURCE, CONF_UNIQUE_ID, ) -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from . import setup_webostv from .const import CLIENT_KEY, FAKE_UUID, HOST, MOCK_APPS, MOCK_INPUTS, TV_NAME @@ -55,7 +51,7 @@ async def test_form(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( @@ -65,7 +61,7 @@ async def test_form(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_init( @@ -75,7 +71,7 @@ async def test_form(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" with patch("homeassistant.components.webostv.async_setup_entry", return_value=True): @@ -85,7 +81,7 @@ async def test_form(hass, client): await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == TV_NAME @@ -115,7 +111,7 @@ async def test_options_flow_live_tv_in_apps(hass, client, apps, inputs): result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result2 = await hass.config_entries.options.async_configure( @@ -124,7 +120,7 @@ async def test_options_flow_live_tv_in_apps(hass, client, apps, inputs): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["data"][CONF_SOURCES] == ["Live TV", "Input01", "Input02"] @@ -136,7 +132,7 @@ async def test_options_flow_cannot_retrieve(hass, client): result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "cannot_retrieve"} @@ -154,7 +150,7 @@ async def test_form_cannot_connect(hass, client): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} @@ -172,7 +168,7 @@ async def test_form_pairexception(hass, client): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "error_pairing" @@ -187,7 +183,7 @@ async def test_entry_already_configured(hass, client): data=MOCK_YAML_CONFIG, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -201,7 +197,7 @@ async def test_form_ssdp(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" @@ -216,7 +212,7 @@ async def test_ssdp_in_progress(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" result2 = await hass.config_entries.flow.async_init( @@ -224,7 +220,7 @@ async def test_ssdp_in_progress(hass, client): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" @@ -239,7 +235,7 @@ async def test_ssdp_update_uuid(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.unique_id == MOCK_DISCOVERY_INFO.upnp[ssdp.ATTR_UPNP_UDN][5:] @@ -258,7 +254,7 @@ async def test_ssdp_not_update_uuid(hass, client): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "pairing" assert entry.unique_id is None @@ -276,7 +272,7 @@ async def test_form_abort_uuid_configured(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" user_config = { @@ -291,7 +287,7 @@ async def test_form_abort_uuid_configured(hass, client): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pairing" result = await hass.config_entries.flow.async_configure( @@ -300,6 +296,6 @@ async def test_form_abort_uuid_configured(hass, client): await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "new_host" diff --git a/tests/components/whois/test_config_flow.py b/tests/components/whois/test_config_flow.py index 4bf6d7e8731..7250a9d1567 100644 --- a/tests/components/whois/test_config_flow.py +++ b/tests/components/whois/test_config_flow.py @@ -13,11 +13,7 @@ from homeassistant.components.whois.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -32,7 +28,7 @@ async def test_full_user_flow( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -41,7 +37,7 @@ async def test_full_user_flow( user_input={CONF_DOMAIN: "Example.com"}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Example.com" assert result2.get("data") == {CONF_DOMAIN: "example.com"} @@ -73,7 +69,7 @@ async def test_full_flow_with_error( DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -83,7 +79,7 @@ async def test_full_flow_with_error( user_input={CONF_DOMAIN: "Example.com"}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": reason} assert "flow_id" in result2 @@ -97,7 +93,7 @@ async def test_full_flow_with_error( user_input={CONF_DOMAIN: "Example.com"}, ) - assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("type") == FlowResultType.CREATE_ENTRY assert result3.get("title") == "Example.com" assert result3.get("data") == {CONF_DOMAIN: "example.com"} @@ -120,7 +116,7 @@ async def test_already_configured( data={CONF_DOMAIN: "HOME-Assistant.io"}, ) - assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/wiffi/test_config_flow.py b/tests/components/wiffi/test_config_flow.py index 655642dffc1..b48641d7ea1 100644 --- a/tests/components/wiffi/test_config_flow.py +++ b/tests/components/wiffi/test_config_flow.py @@ -7,11 +7,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components.wiffi.const import DOMAIN from homeassistant.const import CONF_PORT, CONF_TIMEOUT -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -77,7 +73,7 @@ async def test_form(hass, dummy_tcp_server): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} assert result["step_id"] == config_entries.SOURCE_USER @@ -85,7 +81,7 @@ async def test_form(hass, dummy_tcp_server): result["flow_id"], user_input=MOCK_CONFIG, ) - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY async def test_form_addr_in_use(hass, addr_in_use): @@ -98,7 +94,7 @@ async def test_form_addr_in_use(hass, addr_in_use): result["flow_id"], user_input=MOCK_CONFIG, ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "addr_in_use" @@ -112,7 +108,7 @@ async def test_form_start_server_failed(hass, start_server_failed): result["flow_id"], user_input=MOCK_CONFIG, ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "start_server_failed" diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py index 326224b02c9..a209d55ba99 100644 --- a/tests/components/wilight/test_config_flow.py +++ b/tests/components/wilight/test_config_flow.py @@ -12,11 +12,7 @@ from homeassistant.components.wilight.config_flow import ( from homeassistant.config_entries import SOURCE_SSDP from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry from tests.components.wilight import ( @@ -61,7 +57,7 @@ async def test_show_ssdp_form(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { CONF_NAME: f"WL{WILIGHT_ID}", @@ -77,7 +73,7 @@ async def test_ssdp_not_wilight_abort_1(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_wilight_device" @@ -89,7 +85,7 @@ async def test_ssdp_not_wilight_abort_2(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_wilight_device" @@ -103,7 +99,7 @@ async def test_ssdp_not_wilight_abort_3( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_wilight_device" @@ -117,7 +113,7 @@ async def test_ssdp_not_supported_abort( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_supported_device" @@ -142,7 +138,7 @@ async def test_ssdp_device_exists_abort(hass: HomeAssistant) -> None: data=discovery_info, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -154,7 +150,7 @@ async def test_full_ssdp_flow_implementation(hass: HomeAssistant) -> None: DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" assert result["description_placeholders"] == { CONF_NAME: f"WL{WILIGHT_ID}", @@ -165,7 +161,7 @@ async def test_full_ssdp_flow_implementation(hass: HomeAssistant) -> None: result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"WL{WILIGHT_ID}" assert result["data"] diff --git a/tests/components/wiz/test_config_flow.py b/tests/components/wiz/test_config_flow.py index f37f2ba21a0..75ab1d1b188 100644 --- a/tests/components/wiz/test_config_flow.py +++ b/tests/components/wiz/test_config_flow.py @@ -9,7 +9,7 @@ from homeassistant.components import dhcp from homeassistant.components.wiz.config_flow import CONF_DEVICE from homeassistant.components.wiz.const import DOMAIN from homeassistant.const import CONF_HOST -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from . import ( FAKE_DIMMABLE_BULB, @@ -85,7 +85,7 @@ async def test_user_flow_enters_dns_name(hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "no_ip"} with _patch_wizlight(), patch( @@ -179,7 +179,7 @@ async def test_discovered_by_dhcp_connection_fails(hass, source, data): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -256,7 +256,7 @@ async def test_discovered_by_dhcp_or_integration_discovery( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" with _patch_wizlight( @@ -306,7 +306,7 @@ async def test_discovered_by_dhcp_or_integration_discovery_updates_host( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == FAKE_IP @@ -335,7 +335,7 @@ async def test_discovered_by_dhcp_or_integration_discovery_avoid_waiting_for_ret ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.state is config_entries.ConfigEntryState.LOADED @@ -458,7 +458,7 @@ async def test_setup_via_discovery_exception_finds_nothing(hass): result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "no_devices_found" @@ -476,7 +476,7 @@ async def test_discovery_with_firmware_update(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "discovery_confirm" # In between discovery and when the user clicks to set it up the firmware diff --git a/tests/components/yale_smart_alarm/test_config_flow.py b/tests/components/yale_smart_alarm/test_config_flow.py index 97a4cdfed6b..a53429967e6 100644 --- a/tests/components/yale_smart_alarm/test_config_flow.py +++ b/tests/components/yale_smart_alarm/test_config_flow.py @@ -9,11 +9,7 @@ from yalesmartalarmclient.exceptions import AuthenticationError, UnknownError 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_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -24,7 +20,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -43,7 +39,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -85,7 +81,7 @@ async def test_form_invalid_auth( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} with patch( @@ -104,7 +100,7 @@ async def test_form_invalid_auth( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" assert result2["data"] == { "username": "test-username", @@ -138,7 +134,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: data=entry.data, ) assert result["step_id"] == "reauth_confirm" - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} with patch( @@ -156,7 +152,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "username": "test-username", @@ -218,7 +214,7 @@ async def test_reauth_flow_error( await hass.async_block_till_done() assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": p_error} with patch( @@ -237,7 +233,7 @@ async def test_reauth_flow_error( ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "reauth_successful" assert entry.data == { "username": "test-username", @@ -265,7 +261,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -273,7 +269,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: user_input={"code": "123456", "lock_code_digits": 6}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == {"code": "123456", "lock_code_digits": 6} @@ -295,7 +291,7 @@ async def test_options_flow_format_mismatch(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {} @@ -304,7 +300,7 @@ async def test_options_flow_format_mismatch(hass: HomeAssistant) -> None: user_input={"code": "123", "lock_code_digits": 6}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "code_format_mismatch"} @@ -313,5 +309,5 @@ async def test_options_flow_format_mismatch(hass: HomeAssistant) -> None: user_input={"code": "123456", "lock_code_digits": 6}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == {"code": "123456", "lock_code_digits": 6} diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 7d9acc670b5..391c969d893 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -23,7 +23,7 @@ from homeassistant.components.yeelight.const import ( ) from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from . import ( CAPABILITIES, @@ -475,7 +475,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_discovery_interval(), patch( @@ -489,7 +489,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_discovery_interval(), patch( @@ -503,7 +503,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" with _patch_discovery( @@ -519,7 +519,7 @@ async def test_discovered_by_homekit_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -558,7 +558,7 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_discovery_interval(), patch( @@ -587,7 +587,7 @@ async def test_discovered_by_dhcp_or_homekit(hass, source, data): DOMAIN, context={"source": source}, data=data ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_configured" @@ -626,7 +626,7 @@ async def test_discovered_by_dhcp_or_homekit_failed_to_get_id(hass, source, data result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": source}, data=data ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -642,7 +642,7 @@ async def test_discovered_ssdp(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_discovery_interval(), patch( @@ -671,7 +671,7 @@ async def test_discovered_ssdp(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -689,7 +689,7 @@ async def test_discovered_zeroconf(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_discovery_interval(), patch( @@ -720,7 +720,7 @@ async def test_discovered_zeroconf(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" mocked_bulb = _mocked_bulb() @@ -734,7 +734,7 @@ async def test_discovered_zeroconf(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -756,7 +756,7 @@ async def test_discovery_updates_ip(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS @@ -784,7 +784,7 @@ async def test_discovery_updates_ip_no_reload_setup_in_progress(hass: HomeAssist ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS assert len(mock_setup_entry.mock_calls) == 0 @@ -806,7 +806,7 @@ async def test_discovery_adds_missing_ip_id_only(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS diff --git a/tests/components/youless/test_config_flows.py b/tests/components/youless/test_config_flows.py index d7d9a39ec6e..5bf119681c4 100644 --- a/tests/components/youless/test_config_flows.py +++ b/tests/components/youless/test_config_flows.py @@ -5,7 +5,7 @@ from urllib.error import URLError from homeassistant.components.youless import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType def _get_mock_youless_api(initialize=None): @@ -25,7 +25,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -42,7 +42,7 @@ async def test_full_flow(hass: HomeAssistant) -> None: {"host": "localhost"}, ) - assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "localhost" assert len(mocked_youless.mock_calls) == 1 @@ -53,7 +53,7 @@ async def test_not_found(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert result.get("type") == RESULT_TYPE_FORM + assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == SOURCE_USER assert "flow_id" in result @@ -68,5 +68,5 @@ async def test_not_found(hass: HomeAssistant) -> None: {"host": "localhost"}, ) - assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("type") == FlowResultType.FORM assert len(mocked_youless.mock_calls) == 1 diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 285c08d8a3e..84290595f12 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -25,11 +25,7 @@ from homeassistant.config_entries import ( SOURCE_ZEROCONF, ) from homeassistant.const import CONF_SOURCE -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, -) +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -72,7 +68,7 @@ async def test_discovery(detect_mock, hass): flow["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "socket://192.168.1.200:6638" assert result["data"] == { CONF_DEVICE: { @@ -104,7 +100,7 @@ async def test_zigate_via_zeroconf(probe_mock, hass): flow["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "socket://192.168.1.200:1234" assert result["data"] == { CONF_DEVICE: { @@ -134,7 +130,7 @@ async def test_efr32_via_zeroconf(probe_mock, hass): flow["flow_id"], user_input={"baudrate": 115200} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "socket://192.168.1.200:6638" assert result["data"] == { CONF_DEVICE: { @@ -176,7 +172,7 @@ async def test_discovery_via_zeroconf_ip_change(detect_mock, hass): "zha", context={"source": SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "socket://192.168.1.22:6638", @@ -209,7 +205,7 @@ async def test_discovery_via_zeroconf_ip_change_ignored(detect_mock, hass): "zha", context={"source": SOURCE_ZEROCONF}, data=service_info ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "socket://192.168.1.22:6638", @@ -231,7 +227,7 @@ async def test_discovery_via_usb(detect_mock, hass): "zha", context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" with patch("homeassistant.components.zha.async_setup_entry"): @@ -240,7 +236,7 @@ async def test_discovery_via_usb(detect_mock, hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert "zigbee radio" in result2["title"] assert result2["data"] == { "device": { @@ -267,7 +263,7 @@ async def test_zigate_discovery_via_usb(detect_mock, hass): "zha", context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" with patch("homeassistant.components.zha.async_setup_entry"): @@ -276,7 +272,7 @@ async def test_zigate_discovery_via_usb(detect_mock, hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert ( "zigate radio - /dev/ttyZIGBEE, s/n: 1234 - test - 6015:0403" in result2["title"] @@ -304,7 +300,7 @@ async def test_discovery_via_usb_no_radio(detect_mock, hass): "zha", context={"source": SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" with patch("homeassistant.components.zha.async_setup_entry"): @@ -313,7 +309,7 @@ async def test_discovery_via_usb_no_radio(detect_mock, hass): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "usb_probe_failed" @@ -338,7 +334,7 @@ async def test_discovery_via_usb_already_setup(detect_mock, hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -372,7 +368,7 @@ async def test_discovery_via_usb_path_changes(hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "/dev/ttyZIGBEE", @@ -457,7 +453,7 @@ async def test_discovery_via_usb_deconz_ignored(detect_mock, hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm" @@ -485,7 +481,7 @@ async def test_discovery_via_usb_zha_ignored_updates(detect_mock, hass): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_DEVICE] == { CONF_DEVICE_PATH: "/dev/ttyZIGBEE", @@ -535,7 +531,7 @@ async def test_user_flow(detect_mock, hass): context={CONF_SOURCE: SOURCE_USER}, data={zigpy.config.CONF_DEVICE_PATH: port_select}, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"].startswith(port.description) assert result["data"] == {CONF_RADIO_TYPE: "test_radio"} assert detect_mock.await_count == 1 @@ -559,7 +555,7 @@ async def test_user_flow_not_detected(detect_mock, hass): data={zigpy.config.CONF_DEVICE_PATH: port_select}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pick_radio" assert detect_mock.await_count == 1 assert detect_mock.await_args[0][0] == port.device @@ -573,7 +569,7 @@ async def test_user_flow_show_form(hass): context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" @@ -585,7 +581,7 @@ async def test_user_flow_show_manual(hass): context={CONF_SOURCE: SOURCE_USER}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pick_radio" @@ -597,7 +593,7 @@ async def test_user_flow_manual(hass): context={CONF_SOURCE: SOURCE_USER}, data={zigpy.config.CONF_DEVICE_PATH: config_flow.CONF_MANUAL_PATH}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pick_radio" @@ -608,7 +604,7 @@ async def test_pick_radio_flow(hass, radio_type): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: "pick_radio"}, data={CONF_RADIO_TYPE: radio_type} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "port_config" @@ -708,7 +704,7 @@ async def test_user_port_config_fail(probe_mock, hass): result["flow_id"], user_input={zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB33"}, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "port_config" assert result["errors"]["base"] == "cannot_connect" assert probe_mock.await_count == 1 @@ -793,7 +789,7 @@ async def test_hardware_not_onboarded(hass): "zha", context={"source": "hardware"}, data=data ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "/dev/ttyAMA1" assert result["data"] == { CONF_DEVICE: { @@ -823,14 +819,14 @@ async def test_hardware_onboarded(hass): "zha", context={"source": "hardware"}, data=data ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "confirm_hardware" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "/dev/ttyAMA1" assert result["data"] == { CONF_DEVICE: { @@ -861,7 +857,7 @@ async def test_hardware_already_setup(hass): "zha", context={"source": "hardware"}, data=data ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -875,5 +871,5 @@ async def test_hardware_invalid_data(hass, data): "zha", context={"source": "hardware"}, data=data ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "invalid_hardware_data" diff --git a/tests/components/zwave_me/test_config_flow.py b/tests/components/zwave_me/test_config_flow.py index 36a73c4fc06..57ead9a9e60 100644 --- a/tests/components/zwave_me/test_config_flow.py +++ b/tests/components/zwave_me/test_config_flow.py @@ -5,12 +5,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.zwave_me.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_CREATE_ENTRY, - RESULT_TYPE_FORM, - FlowResult, -) +from homeassistant.data_entry_flow import FlowResult, FlowResultType from tests.common import MockConfigEntry @@ -42,7 +37,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -53,7 +48,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "ws://192.168.1.14" assert result2["data"] == { "url": "ws://192.168.1.14", @@ -76,7 +71,7 @@ async def test_zeroconf(hass: HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" result2 = await hass.config_entries.flow.async_configure( @@ -87,7 +82,7 @@ async def test_zeroconf(hass: HomeAssistant): ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "ws://192.168.1.14" assert result2["data"] == { "url": "ws://192.168.1.14", @@ -104,7 +99,7 @@ async def test_error_handling_zeroconf(hass: HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_valid_uuid_set" @@ -114,7 +109,7 @@ async def test_handle_error_user(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -145,7 +140,7 @@ async def test_duplicate_user(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -154,7 +149,7 @@ async def test_duplicate_user(hass: HomeAssistant): "token": "test-token", }, ) - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_configured" @@ -181,5 +176,5 @@ async def test_duplicate_zeroconf(hass: HomeAssistant): context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DATA, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 03da4d36853..1970d883efc 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -17,7 +17,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import CoreState, Event, HomeAssistant, callback -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, BaseServiceInfo, FlowResult +from homeassistant.data_entry_flow import BaseServiceInfo, FlowResult, FlowResultType from homeassistant.exceptions import ( ConfigEntryAuthFailed, ConfigEntryNotReady, @@ -1659,7 +1659,7 @@ async def test_unique_id_update_existing_entry_without_reload(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "1.1.1.1" assert entry.data["additional"] == "data" @@ -1704,7 +1704,7 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "1.1.1.1" assert entry.data["additional"] == "data" @@ -1721,7 +1721,7 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "2.2.2.2" assert entry.data["additional"] == "data" @@ -1773,7 +1773,7 @@ async def test_unique_id_from_discovery_in_setup_retry(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(async_reload.mock_calls) == 0 @@ -1792,7 +1792,7 @@ async def test_unique_id_from_discovery_in_setup_retry(hass, manager): ) await hass.async_block_till_done() - assert discovery_result["type"] == RESULT_TYPE_ABORT + assert discovery_result["type"] == FlowResultType.ABORT assert discovery_result["reason"] == "already_configured" assert len(async_reload.mock_calls) == 1 @@ -1833,7 +1833,7 @@ async def test_unique_id_not_update_existing_entry(hass, manager): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "0.0.0.0" assert entry.data["additional"] == "data" @@ -3100,7 +3100,7 @@ async def test__async_abort_entries_match(hass, manager, matchers, reason): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == reason @@ -3250,7 +3250,7 @@ async def test_unique_id_update_while_setup_in_progress( ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data["host"] == "1.1.1.1" assert entry.data["additional"] == "data" diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 301a61700c9..136c97808d3 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -517,7 +517,7 @@ async def test_show_menu(hass, manager, menu_options): return self.async_show_form(step_id="target2") result = await manager.async_init("test") - assert result["type"] == data_entry_flow.RESULT_TYPE_MENU + assert result["type"] == data_entry_flow.FlowResultType.MENU assert result["menu_options"] == menu_options assert result["description_placeholders"] == {"name": "Paulus"} assert len(manager.async_progress()) == 1 From c6bff8ae186dfa953832bdc1da5a1b79ff5d58f1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 7 Jul 2022 22:00:19 +0200 Subject: [PATCH 2256/3516] Remove philips_js from mypy ignore list (#74659) * Remove philips_js from mypy ignore list * Use class attribute --- homeassistant/components/philips_js/__init__.py | 5 +++-- .../components/philips_js/config_flow.py | 6 +++--- .../components/philips_js/device_trigger.py | 4 ++++ homeassistant/components/philips_js/light.py | 2 +- .../components/philips_js/media_player.py | 3 +-- mypy.ini | 15 --------------- script/hassfest/mypy_config.py | 5 ----- 7 files changed, 12 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 29e92a6ffe3..8ff20d8b104 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -108,6 +108,8 @@ class PluggableAction: class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): """Coordinator to update data.""" + config_entry: ConfigEntry + def __init__(self, hass, api: PhilipsTV, options: Mapping) -> None: """Set up the coordinator.""" self.api = api @@ -136,8 +138,7 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): @property def unique_id(self) -> str: """Return the system descriptor.""" - entry: ConfigEntry = self.config_entry - assert entry + entry = self.config_entry if entry.unique_id: return entry.unique_id assert entry.entry_id diff --git a/homeassistant/components/philips_js/config_flow.py b/homeassistant/components/philips_js/config_flow.py index 9785aaf54a3..39f1c78b070 100644 --- a/homeassistant/components/philips_js/config_flow.py +++ b/homeassistant/components/philips_js/config_flow.py @@ -23,7 +23,7 @@ from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, CONST_APP_ID, CONST_APP_NAME, async def _validate_input( hass: core.HomeAssistant, host: str, api_version: int -) -> tuple[dict, PhilipsTV]: +) -> PhilipsTV: """Validate the user input allows us to connect.""" hub = PhilipsTV(host, api_version) @@ -44,7 +44,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize flow.""" super().__init__() - self._current = {} + self._current: dict[str, Any] = {} self._hub: PhilipsTV | None = None self._pair_state: Any = None @@ -62,7 +62,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Attempt to pair with device.""" assert self._hub - errors = {} + errors: dict[str, str] = {} schema = vol.Schema( { vol.Required(CONF_PIN): str, diff --git a/homeassistant/components/philips_js/device_trigger.py b/homeassistant/components/philips_js/device_trigger.py index 69a5932576f..eca3158fb15 100644 --- a/homeassistant/components/philips_js/device_trigger.py +++ b/homeassistant/components/philips_js/device_trigger.py @@ -65,6 +65,10 @@ async def async_attach_trigger( } device = registry.async_get(config[CONF_DEVICE_ID]) + if device is None: + raise HomeAssistantError( + f"Device id {config[CONF_DEVICE_ID]} not found in registry" + ) for config_entry_id in device.config_entries: coordinator: PhilipsTVDataUpdateCoordinator = hass.data[DOMAIN].get( config_entry_id diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index 5a32d2954a4..ab4f04a8ddc 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -146,7 +146,7 @@ class PhilipsTVLightEntity( self._hs = None self._brightness = None self._cache_keys = None - self._last_selected_effect: AmbilightEffect = None + self._last_selected_effect: AmbilightEffect | None = None super().__init__(coordinator) self._attr_supported_color_modes = {ColorMode.HS, ColorMode.ONOFF} diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 27cf41abd4f..3d63865fe31 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -80,8 +80,7 @@ class PhilipsTVMediaPlayer( ) -> None: """Initialize the Philips TV.""" self._tv = coordinator.api - self._sources = {} - self._channels = {} + self._sources: dict[str, str] = {} self._supports = SUPPORT_PHILIPS_JS self._system = coordinator.system self._attr_name = coordinator.system["name"] diff --git a/mypy.ini b/mypy.ini index b139a40976b..87dd265eeef 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2746,21 +2746,6 @@ ignore_errors = true [mypy-homeassistant.components.onvif.sensor] ignore_errors = true -[mypy-homeassistant.components.philips_js] -ignore_errors = true - -[mypy-homeassistant.components.philips_js.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.philips_js.device_trigger] -ignore_errors = true - -[mypy-homeassistant.components.philips_js.light] -ignore_errors = true - -[mypy-homeassistant.components.philips_js.media_player] -ignore_errors = true - [mypy-homeassistant.components.plex.media_player] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 55ed01aaa63..4e11028d574 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -57,11 +57,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.onvif.camera", "homeassistant.components.onvif.device", "homeassistant.components.onvif.sensor", - "homeassistant.components.philips_js", - "homeassistant.components.philips_js.config_flow", - "homeassistant.components.philips_js.device_trigger", - "homeassistant.components.philips_js.light", - "homeassistant.components.philips_js.media_player", "homeassistant.components.plex.media_player", "homeassistant.components.profiler", "homeassistant.components.solaredge.config_flow", From 6ebdf0580bfa28ff29d886833a3dd6b26bd84071 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 22:35:06 +0200 Subject: [PATCH 2257/3516] Remove last occurrences of RESULT_TYPE_* from codebase (#74670) Remove last ocurrances of RESULT_TYPE_* from codebase --- .../templates/config_flow/tests/test_config_flow.py | 10 +++++----- .../config_flow_helper/tests/test_config_flow.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) 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 d1ddc177690..f4413ea4e84 100644 --- a/script/scaffold/templates/config_flow/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py @@ -5,7 +5,7 @@ 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 -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -13,7 +13,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -33,7 +33,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "Name of the device" assert result2["data"] == { "host": "1.1.1.1", @@ -62,7 +62,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -85,5 +85,5 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py b/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py index b7bef2c63f4..7d2be8a6d82 100644 --- a/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow_helper/tests/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries from homeassistant.components.NEW_DOMAIN.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch( @@ -32,7 +32,7 @@ async def test_config_flow(hass: HomeAssistant, platform) -> None: ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "My NEW_DOMAIN" assert result["data"] == {} assert result["options"] == { @@ -82,7 +82,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: await hass.async_block_till_done() result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" schema = result["data_schema"].schema assert get_suggested(schema, "entity_id") == input_sensor_1_entity_id @@ -93,7 +93,7 @@ async def test_options(hass: HomeAssistant, platform) -> None: "entity_id": input_sensor_2_entity_id, }, ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { "entity_id": input_sensor_2_entity_id, "name": "My NEW_DOMAIN", From 3a61a0de2e12f4d104284fc858273c71b4d5d3b4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 22:38:10 +0200 Subject: [PATCH 2258/3516] Standardize EntityDescription in DSMR (#74671) --- homeassistant/components/dsmr/const.py | 273 ---------------------- homeassistant/components/dsmr/models.py | 14 -- homeassistant/components/dsmr/sensor.py | 292 +++++++++++++++++++++++- 3 files changed, 286 insertions(+), 293 deletions(-) delete mode 100644 homeassistant/components/dsmr/models.py diff --git a/homeassistant/components/dsmr/const.py b/homeassistant/components/dsmr/const.py index 9f08e812e04..5e1a54aedc4 100644 --- a/homeassistant/components/dsmr/const.py +++ b/homeassistant/components/dsmr/const.py @@ -3,13 +3,7 @@ from __future__ import annotations import logging -from dsmr_parser import obis_references - -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.const import Platform -from homeassistant.helpers.entity import EntityCategory - -from .models import DSMRSensorEntityDescription DOMAIN = "dsmr" @@ -40,270 +34,3 @@ DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"} DSMR_PROTOCOL = "dsmr_protocol" RFXTRX_DSMR_PROTOCOL = "rfxtrx_dsmr_protocol" - -SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( - DSMRSensorEntityDescription( - key=obis_references.CURRENT_ELECTRICITY_USAGE, - name="Power Consumption", - device_class=SensorDeviceClass.POWER, - force_update=True, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.CURRENT_ELECTRICITY_DELIVERY, - name="Power Production", - device_class=SensorDeviceClass.POWER, - force_update=True, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_ACTIVE_TARIFF, - name="Power Tariff", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - icon="mdi:flash", - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_USED_TARIFF_1, - name="Energy Consumption (tarif 1)", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - device_class=SensorDeviceClass.ENERGY, - force_update=True, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_USED_TARIFF_2, - name="Energy Consumption (tarif 2)", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - force_update=True, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, - name="Energy Production (tarif 1)", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - force_update=True, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, - name="Energy Production (tarif 2)", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - force_update=True, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, - name="Power Consumption Phase L1", - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, - name="Power Consumption Phase L2", - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, - name="Power Consumption Phase L3", - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, - name="Power Production Phase L1", - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, - name="Power Production Phase L2", - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, - name="Power Production Phase L3", - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - DSMRSensorEntityDescription( - key=obis_references.SHORT_POWER_FAILURE_COUNT, - name="Short Power Failure Count", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - icon="mdi:flash-off", - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.LONG_POWER_FAILURE_COUNT, - name="Long Power Failure Count", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - icon="mdi:flash-off", - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SAG_L1_COUNT, - name="Voltage Sags Phase L1", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SAG_L2_COUNT, - name="Voltage Sags Phase L2", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SAG_L3_COUNT, - name="Voltage Sags Phase L3", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SWELL_L1_COUNT, - name="Voltage Swells Phase L1", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - icon="mdi:pulse", - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SWELL_L2_COUNT, - name="Voltage Swells Phase L2", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - icon="mdi:pulse", - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SWELL_L3_COUNT, - name="Voltage Swells Phase L3", - dsmr_versions={"2.2", "4", "5", "5B", "5L"}, - entity_registry_enabled_default=False, - icon="mdi:pulse", - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_VOLTAGE_L1, - name="Voltage Phase L1", - device_class=SensorDeviceClass.VOLTAGE, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_VOLTAGE_L2, - name="Voltage Phase L2", - device_class=SensorDeviceClass.VOLTAGE, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_VOLTAGE_L3, - name="Voltage Phase L3", - device_class=SensorDeviceClass.VOLTAGE, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_CURRENT_L1, - name="Current Phase L1", - device_class=SensorDeviceClass.CURRENT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_CURRENT_L2, - name="Current Phase L2", - device_class=SensorDeviceClass.CURRENT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_CURRENT_L3, - name="Current Phase L3", - device_class=SensorDeviceClass.CURRENT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.BELGIUM_MAX_POWER_PER_PHASE, - name="Max power per phase", - dsmr_versions={"5B"}, - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.BELGIUM_MAX_CURRENT_PER_PHASE, - name="Max current per phase", - dsmr_versions={"5B"}, - device_class=SensorDeviceClass.POWER, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_IMPORTED_TOTAL, - name="Energy Consumption (total)", - dsmr_versions={"5L", "5S", "Q3D"}, - force_update=True, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_EXPORTED_TOTAL, - name="Energy Production (total)", - dsmr_versions={"5L", "5S", "Q3D"}, - force_update=True, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.HOURLY_GAS_METER_READING, - name="Gas Consumption", - dsmr_versions={"4", "5", "5L"}, - is_gas=True, - force_update=True, - device_class=SensorDeviceClass.GAS, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.BELGIUM_5MIN_GAS_METER_READING, - name="Gas Consumption", - dsmr_versions={"5B"}, - is_gas=True, - force_update=True, - device_class=SensorDeviceClass.GAS, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - DSMRSensorEntityDescription( - key=obis_references.GAS_METER_READING, - name="Gas Consumption", - dsmr_versions={"2.2"}, - is_gas=True, - force_update=True, - device_class=SensorDeviceClass.GAS, - state_class=SensorStateClass.TOTAL_INCREASING, - ), -) diff --git a/homeassistant/components/dsmr/models.py b/homeassistant/components/dsmr/models.py deleted file mode 100644 index e7b47d8b74d..00000000000 --- a/homeassistant/components/dsmr/models.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Models for the DSMR integration.""" -from __future__ import annotations - -from dataclasses import dataclass - -from homeassistant.components.sensor import SensorEntityDescription - - -@dataclass -class DSMRSensorEntityDescription(SensorEntityDescription): - """Represents an DSMR Sensor.""" - - dsmr_versions: set[str] | None = None - is_gas: bool = False diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 9c684493a3f..1337f209d5d 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -4,10 +4,11 @@ from __future__ import annotations import asyncio from asyncio import CancelledError from contextlib import suppress +from dataclasses import dataclass from datetime import timedelta from functools import partial -from dsmr_parser import obis_references as obis_ref +from dsmr_parser import obis_references from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader from dsmr_parser.clients.rfxtrx_protocol import ( create_rfxtrx_dsmr_reader, @@ -16,7 +17,12 @@ from dsmr_parser.clients.rfxtrx_protocol import ( from dsmr_parser.objects import DSMRObject import serial -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -25,7 +31,7 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) from homeassistant.core import CoreState, HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import EventType, StateType from homeassistant.util import Throttle @@ -47,13 +53,287 @@ from .const import ( DOMAIN, DSMR_PROTOCOL, LOGGER, - SENSORS, ) -from .models import DSMRSensorEntityDescription UNIT_CONVERSION = {"m3": VOLUME_CUBIC_METERS} +@dataclass +class DSMRSensorEntityDescription(SensorEntityDescription): + """Represents an DSMR Sensor.""" + + dsmr_versions: set[str] | None = None + is_gas: bool = False + + +SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( + DSMRSensorEntityDescription( + key=obis_references.CURRENT_ELECTRICITY_USAGE, + name="Power Consumption", + device_class=SensorDeviceClass.POWER, + force_update=True, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key=obis_references.CURRENT_ELECTRICITY_DELIVERY, + name="Power Production", + device_class=SensorDeviceClass.POWER, + force_update=True, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_ACTIVE_TARIFF, + name="Power Tariff", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + icon="mdi:flash", + ), + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_USED_TARIFF_1, + name="Energy Consumption (tarif 1)", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + device_class=SensorDeviceClass.ENERGY, + force_update=True, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_USED_TARIFF_2, + name="Energy Consumption (tarif 2)", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + force_update=True, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, + name="Energy Production (tarif 1)", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + force_update=True, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, + name="Energy Production (tarif 2)", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + force_update=True, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, + name="Power Consumption Phase L1", + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, + name="Power Consumption Phase L2", + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, + name="Power Consumption Phase L3", + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, + name="Power Production Phase L1", + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, + name="Power Production Phase L2", + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, + name="Power Production Phase L3", + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + DSMRSensorEntityDescription( + key=obis_references.SHORT_POWER_FAILURE_COUNT, + name="Short Power Failure Count", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + icon="mdi:flash-off", + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.LONG_POWER_FAILURE_COUNT, + name="Long Power Failure Count", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + icon="mdi:flash-off", + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.VOLTAGE_SAG_L1_COUNT, + name="Voltage Sags Phase L1", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.VOLTAGE_SAG_L2_COUNT, + name="Voltage Sags Phase L2", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.VOLTAGE_SAG_L3_COUNT, + name="Voltage Sags Phase L3", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.VOLTAGE_SWELL_L1_COUNT, + name="Voltage Swells Phase L1", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + icon="mdi:pulse", + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.VOLTAGE_SWELL_L2_COUNT, + name="Voltage Swells Phase L2", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + icon="mdi:pulse", + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.VOLTAGE_SWELL_L3_COUNT, + name="Voltage Swells Phase L3", + dsmr_versions={"2.2", "4", "5", "5B", "5L"}, + entity_registry_enabled_default=False, + icon="mdi:pulse", + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_VOLTAGE_L1, + name="Voltage Phase L1", + device_class=SensorDeviceClass.VOLTAGE, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_VOLTAGE_L2, + name="Voltage Phase L2", + device_class=SensorDeviceClass.VOLTAGE, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_VOLTAGE_L3, + name="Voltage Phase L3", + device_class=SensorDeviceClass.VOLTAGE, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_CURRENT_L1, + name="Current Phase L1", + device_class=SensorDeviceClass.CURRENT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_CURRENT_L2, + name="Current Phase L2", + device_class=SensorDeviceClass.CURRENT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.INSTANTANEOUS_CURRENT_L3, + name="Current Phase L3", + device_class=SensorDeviceClass.CURRENT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.BELGIUM_MAX_POWER_PER_PHASE, + name="Max power per phase", + dsmr_versions={"5B"}, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.BELGIUM_MAX_CURRENT_PER_PHASE, + name="Max current per phase", + dsmr_versions={"5B"}, + device_class=SensorDeviceClass.POWER, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_IMPORTED_TOTAL, + name="Energy Consumption (total)", + dsmr_versions={"5L", "5S", "Q3D"}, + force_update=True, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key=obis_references.ELECTRICITY_EXPORTED_TOTAL, + name="Energy Production (total)", + dsmr_versions={"5L", "5S", "Q3D"}, + force_update=True, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key=obis_references.HOURLY_GAS_METER_READING, + name="Gas Consumption", + dsmr_versions={"4", "5", "5L"}, + is_gas=True, + force_update=True, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key=obis_references.BELGIUM_5MIN_GAS_METER_READING, + name="Gas Consumption", + dsmr_versions={"5B"}, + is_gas=True, + force_update=True, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + DSMRSensorEntityDescription( + key=obis_references.GAS_METER_READING, + name="Gas Consumption", + dsmr_versions={"2.2"}, + is_gas=True, + force_update=True, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -240,7 +520,7 @@ class DSMREntity(SensorEntity): if (value := self.get_dsmr_object_attr("value")) is None: return None - if self.entity_description.key == obis_ref.ELECTRICITY_ACTIVE_TARIFF: + if self.entity_description.key == obis_references.ELECTRICITY_ACTIVE_TARIFF: return self.translate_tariff(value, self._entry.data[CONF_DSMR_VERSION]) with suppress(TypeError): From a84d92be4d87a52f2fc6952b19a9884e728bec9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Jul 2022 15:45:40 -0500 Subject: [PATCH 2259/3516] Fix climacell/tomorrowio config flow test failure (#74660) --- homeassistant/components/climacell/weather.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/climacell/weather.py b/homeassistant/components/climacell/weather.py index 6aee9b54f6c..73ff6361041 100644 --- a/homeassistant/components/climacell/weather.py +++ b/homeassistant/components/climacell/weather.py @@ -148,17 +148,16 @@ class BaseClimaCellWeatherEntity(ClimaCellEntity, WeatherEntity): @property def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return additional state attributes.""" - wind_gust = self.wind_gust - wind_gust = round( - speed_convert(self.wind_gust, SPEED_MILES_PER_HOUR, self._wind_speed_unit), - 4, - ) cloud_cover = self.cloud_cover - return { + attrs = { ATTR_CLOUD_COVER: cloud_cover, - ATTR_WIND_GUST: wind_gust, ATTR_PRECIPITATION_TYPE: self.precipitation_type, } + if (wind_gust := self.wind_gust) is not None: + attrs[ATTR_WIND_GUST] = round( + speed_convert(wind_gust, SPEED_MILES_PER_HOUR, self._wind_speed_unit), 4 + ) + return attrs @property @abstractmethod From 5dd8dfb4dc768782f3077795bdf28ceefa7154c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Fri, 8 Jul 2022 00:46:23 +0200 Subject: [PATCH 2260/3516] Update kaiterra-async-client to 1.0.0 (#74677) --- homeassistant/components/kaiterra/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/kaiterra/manifest.json b/homeassistant/components/kaiterra/manifest.json index 9f2a4c0013f..94b861524eb 100644 --- a/homeassistant/components/kaiterra/manifest.json +++ b/homeassistant/components/kaiterra/manifest.json @@ -2,7 +2,7 @@ "domain": "kaiterra", "name": "Kaiterra", "documentation": "https://www.home-assistant.io/integrations/kaiterra", - "requirements": ["kaiterra-async-client==0.0.2"], + "requirements": ["kaiterra-async-client==1.0.0"], "codeowners": ["@Michsior14"], "iot_class": "cloud_polling", "loggers": ["kaiterra_async_client"] diff --git a/requirements_all.txt b/requirements_all.txt index 7cc20779883..9f9423496fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -912,7 +912,7 @@ jellyfin-apiclient-python==1.8.1 jsonpath==0.82 # homeassistant.components.kaiterra -kaiterra-async-client==0.0.2 +kaiterra-async-client==1.0.0 # homeassistant.components.keba keba-kecontact==1.1.0 From 405d323709edc13501b1710cdc72959883fffa8c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 8 Jul 2022 00:27:47 +0000 Subject: [PATCH 2261/3516] [ci skip] Translation update --- .../accuweather/translations/ja.json | 2 +- .../components/aemet/translations/ja.json | 4 ++-- .../components/airly/translations/ja.json | 2 +- .../components/airnow/translations/ja.json | 2 +- .../components/airzone/translations/ja.json | 2 +- .../aladdin_connect/translations/ja.json | 4 ++-- .../components/anthemav/translations/nl.json | 18 ++++++++++++++++ .../components/apple_tv/translations/ja.json | 2 +- .../aussie_broadband/translations/ja.json | 2 +- .../azure_event_hub/translations/ja.json | 2 +- .../components/blink/translations/ja.json | 2 +- .../components/bosch_shc/translations/ja.json | 4 ++-- .../components/braviatv/translations/ja.json | 2 +- .../components/brunt/translations/ja.json | 4 ++-- .../cloudflare/translations/ja.json | 2 +- .../components/cpuspeed/translations/ja.json | 2 +- .../components/deluge/translations/ja.json | 2 +- .../devolo_home_network/translations/ja.json | 2 +- .../components/dexcom/translations/ja.json | 2 +- .../dialogflow/translations/ja.json | 2 +- .../components/dlna_dmr/translations/ja.json | 2 +- .../components/dunehd/translations/ja.json | 2 +- .../components/elmax/translations/ja.json | 2 +- .../forked_daapd/translations/ja.json | 4 ++-- .../geocaching/translations/ja.json | 4 ++-- .../components/github/translations/ja.json | 4 ++-- .../components/google/translations/ja.json | 4 ++-- .../homeassistant/translations/nl.json | 1 + .../homekit_controller/translations/ja.json | 2 +- .../components/icloud/translations/ja.json | 4 ++-- .../integration/translations/ja.json | 4 ++-- .../components/isy994/translations/ja.json | 2 +- .../components/kodi/translations/ja.json | 2 +- .../components/laundrify/translations/ja.json | 4 ++-- .../lg_soundbar/translations/nl.json | 18 ++++++++++++++++ .../components/life360/translations/ja.json | 2 +- .../components/life360/translations/nl.json | 7 +++++++ .../components/lyric/translations/ja.json | 4 ++-- .../components/melcloud/translations/ja.json | 2 +- .../mobile_app/translations/ja.json | 2 +- .../modem_callerid/translations/ja.json | 4 ++-- .../components/mullvad/translations/ja.json | 2 +- .../components/mysensors/translations/ja.json | 2 +- .../components/nam/translations/ja.json | 4 ++-- .../components/nest/translations/ja.json | 4 ++-- .../components/netatmo/translations/ja.json | 4 ++-- .../components/nextdns/translations/de.json | 5 +++++ .../components/nextdns/translations/et.json | 5 +++++ .../components/nextdns/translations/ja.json | 5 +++++ .../components/nextdns/translations/nl.json | 21 +++++++++++++++++++ .../nfandroidtv/translations/ja.json | 2 +- .../components/notion/translations/ja.json | 2 +- .../components/nuki/translations/ja.json | 4 ++-- .../openweathermap/translations/ja.json | 4 ++-- .../components/picnic/translations/ja.json | 2 +- .../components/plugwise/translations/ja.json | 2 +- .../components/qnap_qsw/translations/nl.json | 6 ++++++ .../components/renault/translations/ja.json | 2 +- .../components/ridwell/translations/ja.json | 2 +- .../rtsp_to_webrtc/translations/ja.json | 2 +- .../components/sense/translations/ja.json | 4 ++-- .../simplepush/translations/nl.json | 3 +++ .../simplisafe/translations/ja.json | 2 +- .../components/sleepiq/translations/ja.json | 4 ++-- .../components/smappee/translations/ja.json | 4 ++-- .../smartthings/translations/ja.json | 4 ++-- .../components/smarttub/translations/ja.json | 4 ++-- .../somfy_mylink/translations/ja.json | 2 +- .../components/sonarr/translations/ja.json | 4 ++-- .../soundtouch/translations/nl.json | 17 +++++++++++++++ .../components/spotify/translations/ja.json | 6 +++--- .../steam_online/translations/ja.json | 4 ++-- .../components/syncthing/translations/ja.json | 2 +- .../synology_dsm/translations/ja.json | 2 +- .../totalconnect/translations/ja.json | 2 +- .../components/tractive/translations/ja.json | 2 +- .../transmission/translations/ja.json | 2 +- .../ukraine_alarm/translations/ja.json | 2 +- .../components/unifi/translations/ja.json | 4 ++-- .../uptimerobot/translations/ja.json | 4 ++-- .../components/vicare/translations/ja.json | 2 +- .../components/vulcan/translations/ja.json | 4 ++-- .../components/watttime/translations/ja.json | 2 +- .../waze_travel_time/translations/ja.json | 2 +- .../components/whois/translations/ja.json | 2 +- .../components/withings/translations/ja.json | 2 +- .../components/wiz/translations/ja.json | 2 +- .../components/wled/translations/ja.json | 2 +- .../xiaomi_miio/translations/ja.json | 6 +++--- .../components/yolink/translations/ja.json | 4 ++-- .../components/zwave_js/translations/ja.json | 2 +- 91 files changed, 219 insertions(+), 113 deletions(-) create mode 100644 homeassistant/components/anthemav/translations/nl.json create mode 100644 homeassistant/components/lg_soundbar/translations/nl.json create mode 100644 homeassistant/components/nextdns/translations/nl.json create mode 100644 homeassistant/components/soundtouch/translations/nl.json diff --git a/homeassistant/components/accuweather/translations/ja.json b/homeassistant/components/accuweather/translations/ja.json index 21ec4e6068d..c7ea9f1d264 100644 --- a/homeassistant/components/accuweather/translations/ja.json +++ b/homeassistant/components/accuweather/translations/ja.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" }, "create_entry": { - "default": "\u4e00\u90e8\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u69cb\u6210\u5f8c\u3001\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30ec\u30b8\u30b9\u30c8\u30ea\u3067\u305d\u308c\u3089\u3092\u6709\u52b9\u306b\u3067\u304d\u307e\u3059\u3002\n\u5929\u6c17\u4e88\u5831\u306f\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u6709\u52b9\u306b\u3067\u304d\u307e\u3059\u3002" + "default": "\u4e00\u90e8\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u7d71\u5408\u306e\u69cb\u6210\u5f8c\u3001\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30ec\u30b8\u30b9\u30c8\u30ea\u3067\u305d\u308c\u3089\u3092\u6709\u52b9\u306b\u3067\u304d\u307e\u3059\u3002\n\u5929\u6c17\u4e88\u5831\u306f\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u7d71\u5408\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u6709\u52b9\u306b\u3067\u304d\u307e\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/aemet/translations/ja.json b/homeassistant/components/aemet/translations/ja.json index fd79c699cee..1279f90beaa 100644 --- a/homeassistant/components/aemet/translations/ja.json +++ b/homeassistant/components/aemet/translations/ja.json @@ -12,9 +12,9 @@ "api_key": "API\u30ad\u30fc", "latitude": "\u7def\u5ea6", "longitude": "\u7d4c\u5ea6", - "name": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u540d\u524d" + "name": "\u7d71\u5408\u306e\u540d\u524d" }, - "description": "AEMET OpenData\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://opendata.aemet.es/centrodedescargas/altaUsuario \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "AEMET OpenData\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://opendata.aemet.es/centrodedescargas/altaUsuario \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } }, diff --git a/homeassistant/components/airly/translations/ja.json b/homeassistant/components/airly/translations/ja.json index 6ff5e2216a6..5116cac487a 100644 --- a/homeassistant/components/airly/translations/ja.json +++ b/homeassistant/components/airly/translations/ja.json @@ -15,7 +15,7 @@ "longitude": "\u7d4c\u5ea6", "name": "\u540d\u524d" }, - "description": "Airly\u306e\u7a7a\u6c17\u54c1\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.airly.eu/register \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "Airly\u306e\u7a7a\u6c17\u54c1\u8cea\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.airly.eu/register \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } }, diff --git a/homeassistant/components/airnow/translations/ja.json b/homeassistant/components/airnow/translations/ja.json index 17a1b14e164..6cf1e763eda 100644 --- a/homeassistant/components/airnow/translations/ja.json +++ b/homeassistant/components/airnow/translations/ja.json @@ -17,7 +17,7 @@ "longitude": "\u7d4c\u5ea6", "radius": "\u30b9\u30c6\u30fc\u30b7\u30e7\u30f3\u534a\u5f84(\u30de\u30a4\u30eb; \u30aa\u30d7\u30b7\u30e7\u30f3)" }, - "description": "AirNow\u306e\u7a7a\u6c17\u54c1\u8cea\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://docs.airnowapi.org/account/request/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "AirNow\u306e\u7a7a\u6c17\u54c1\u8cea\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://docs.airnowapi.org/account/request/ \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } } diff --git a/homeassistant/components/airzone/translations/ja.json b/homeassistant/components/airzone/translations/ja.json index 28ebf809dbf..68bee99ba29 100644 --- a/homeassistant/components/airzone/translations/ja.json +++ b/homeassistant/components/airzone/translations/ja.json @@ -13,7 +13,7 @@ "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8" }, - "description": "Airzone\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "description": "Airzone\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/aladdin_connect/translations/ja.json b/homeassistant/components/aladdin_connect/translations/ja.json index 196b7f1d066..8859f24cd54 100644 --- a/homeassistant/components/aladdin_connect/translations/ja.json +++ b/homeassistant/components/aladdin_connect/translations/ja.json @@ -13,8 +13,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "Aladdin Connect\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Aladdin Connect\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/anthemav/translations/nl.json b/homeassistant/components/anthemav/translations/nl.json new file mode 100644 index 00000000000..c09dde1bfc3 --- /dev/null +++ b/homeassistant/components/anthemav/translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Poort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/ja.json b/homeassistant/components/apple_tv/translations/ja.json index 3661cbeedb3..860bf4c961b 100644 --- a/homeassistant/components/apple_tv/translations/ja.json +++ b/homeassistant/components/apple_tv/translations/ja.json @@ -22,7 +22,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "`{name}` \u3068\u3044\u3046\u540d\u524d\u306eApple TV\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u307e\u3059\u3002 \n\n **\u51e6\u7406\u3092\u5b8c\u4e86\u3059\u308b\u306b\u306f\u3001\u8907\u6570\u306ePIN\u30b3\u30fc\u30c9\u306e\u5165\u529b\u304c\u5fc5\u8981\u306b\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002** \n\n\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001Apple TV\u306e\u96fb\u6e90\u3092\u30aa\u30d5\u306b\u3059\u308b\u3053\u3068\u306f *\u3067\u304d\u306a\u3044* \u3053\u3068\u306b\u6ce8\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002 Home Assistant\u306e\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u307f\u304c\u30aa\u30d5\u306b\u306a\u308a\u307e\u3059\uff01", + "description": "`{name}` \u3068\u3044\u3046\u540d\u524d\u306eApple TV\u3092Home Assistant\u306b\u8ffd\u52a0\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u307e\u3059\u3002 \n\n **\u51e6\u7406\u3092\u5b8c\u4e86\u3059\u308b\u306b\u306f\u3001\u8907\u6570\u306ePIN\u30b3\u30fc\u30c9\u306e\u5165\u529b\u304c\u5fc5\u8981\u306b\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059\u3002** \n\n\u3053\u306e\u7d71\u5408\u3067\u306f\u3001Apple TV\u306e\u96fb\u6e90\u3092\u30aa\u30d5\u306b\u3059\u308b\u3053\u3068\u306f *\u3067\u304d\u306a\u3044* \u3053\u3068\u306b\u6ce8\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002 Home Assistant\u306e\u30e1\u30c7\u30a3\u30a2\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u307f\u304c\u30aa\u30d5\u306b\u306a\u308a\u307e\u3059\uff01", "title": "Apple TV\u306e\u8ffd\u52a0\u3092\u78ba\u8a8d\u3059\u308b" }, "pair_no_pin": { diff --git a/homeassistant/components/aussie_broadband/translations/ja.json b/homeassistant/components/aussie_broadband/translations/ja.json index d8351bbc98b..fb2f908851a 100644 --- a/homeassistant/components/aussie_broadband/translations/ja.json +++ b/homeassistant/components/aussie_broadband/translations/ja.json @@ -16,7 +16,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "service": { "data": { diff --git a/homeassistant/components/azure_event_hub/translations/ja.json b/homeassistant/components/azure_event_hub/translations/ja.json index d8c0407fbc5..b504f86a3bd 100644 --- a/homeassistant/components/azure_event_hub/translations/ja.json +++ b/homeassistant/components/azure_event_hub/translations/ja.json @@ -32,7 +32,7 @@ "event_hub_instance_name": "\u30a4\u30d9\u30f3\u30c8\u30cf\u30d6\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u540d", "use_connection_string": "\u63a5\u7d9a\u6587\u5b57\u5217\u3092\u4f7f\u7528\u3059\u308b" }, - "title": "Azure Event Hub\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "title": "Azure Event Hub\u7d71\u5408\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } }, diff --git a/homeassistant/components/blink/translations/ja.json b/homeassistant/components/blink/translations/ja.json index 40724c01d42..0ff7c2dc1b4 100644 --- a/homeassistant/components/blink/translations/ja.json +++ b/homeassistant/components/blink/translations/ja.json @@ -32,7 +32,7 @@ "data": { "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u79d2)" }, - "description": "Blink\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a", + "description": "Blink\u7d71\u5408\u306e\u8a2d\u5b9a", "title": "Blink \u30aa\u30d7\u30b7\u30e7\u30f3" } } diff --git a/homeassistant/components/bosch_shc/translations/ja.json b/homeassistant/components/bosch_shc/translations/ja.json index da4b471c621..6d7883de038 100644 --- a/homeassistant/components/bosch_shc/translations/ja.json +++ b/homeassistant/components/bosch_shc/translations/ja.json @@ -22,8 +22,8 @@ } }, "reauth_confirm": { - "description": "Bosch_shc\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Bosch_shc\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/ja.json b/homeassistant/components/braviatv/translations/ja.json index 860fd5c89ea..263de9e35b0 100644 --- a/homeassistant/components/braviatv/translations/ja.json +++ b/homeassistant/components/braviatv/translations/ja.json @@ -21,7 +21,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "\u30bd\u30cb\u30fc Bravia TV\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/braviatv \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "\u30bd\u30cb\u30fc Bravia TV\u306e\u7d71\u5408\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/braviatv \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30c6\u30ec\u30d3\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } }, diff --git a/homeassistant/components/brunt/translations/ja.json b/homeassistant/components/brunt/translations/ja.json index a0c477443b8..ac50c52ee09 100644 --- a/homeassistant/components/brunt/translations/ja.json +++ b/homeassistant/components/brunt/translations/ja.json @@ -15,14 +15,14 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044: {username}", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "title": "Brunt\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "title": "Brunt\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/cloudflare/translations/ja.json b/homeassistant/components/cloudflare/translations/ja.json index 81fdf638148..1057d4e7bc5 100644 --- a/homeassistant/components/cloudflare/translations/ja.json +++ b/homeassistant/components/cloudflare/translations/ja.json @@ -28,7 +28,7 @@ "data": { "api_token": "API\u30c8\u30fc\u30af\u30f3" }, - "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u5185\u306e\u3059\u3079\u3066\u306e\u30be\u30fc\u30f3\u306b\u5bfe\u3059\u308b\u3001 Zone:Zone:Read \u304a\u3088\u3073\u3001Zone:DNS:Edit\u306e\u6a29\u9650\u3067\u4f5c\u6210\u3055\u308c\u305fAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u3067\u3059\u3002", + "description": "\u3053\u306e\u7d71\u5408\u306b\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u5185\u306e\u3059\u3079\u3066\u306e\u30be\u30fc\u30f3\u306b\u5bfe\u3059\u308b\u3001 Zone:Zone:Read \u304a\u3088\u3073\u3001Zone:DNS:Edit\u306e\u6a29\u9650\u3067\u4f5c\u6210\u3055\u308c\u305fAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u3067\u3059\u3002", "title": "Cloudflare\u306b\u63a5\u7d9a" }, "zone": { diff --git a/homeassistant/components/cpuspeed/translations/ja.json b/homeassistant/components/cpuspeed/translations/ja.json index e2264b57dcb..12cec097b11 100644 --- a/homeassistant/components/cpuspeed/translations/ja.json +++ b/homeassistant/components/cpuspeed/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", - "not_compatible": "CPU\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3002\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306f\u3001\u304a\u4f7f\u3044\u306e\u30b7\u30b9\u30c6\u30e0\u3068\u4e92\u63db\u6027\u304c\u3042\u308a\u307e\u305b\u3093\u3002" + "not_compatible": "CPU\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3002\u3053\u306e\u7d71\u5408\u306f\u3001\u304a\u4f7f\u3044\u306e\u30b7\u30b9\u30c6\u30e0\u3068\u4e92\u63db\u6027\u304c\u3042\u308a\u307e\u305b\u3093\u3002" }, "step": { "user": { diff --git a/homeassistant/components/deluge/translations/ja.json b/homeassistant/components/deluge/translations/ja.json index ab796327717..d109e1570c5 100644 --- a/homeassistant/components/deluge/translations/ja.json +++ b/homeassistant/components/deluge/translations/ja.json @@ -16,7 +16,7 @@ "username": "\u30e6\u30fc\u30b6\u30fc\u540d", "web_port": "Web\u30dd\u30fc\u30c8\uff08\u8a2a\u554f\u30b5\u30fc\u30d3\u30b9\u7528\uff09" }, - "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u5229\u7528\u3059\u308b\u305f\u3081\u306b\u306f\u3001deluge\u306e\u8a2d\u5b9a\u3067\u4ee5\u4e0b\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u6709\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u30c7\u30fc\u30e2\u30f3 -> \u30ea\u30e2\u30fc\u30c8\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u306e\u8a31\u53ef" + "description": "\u3053\u306e\u7d71\u5408\u3092\u5229\u7528\u3059\u308b\u305f\u3081\u306b\u306f\u3001deluge\u306e\u8a2d\u5b9a\u3067\u4ee5\u4e0b\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u6709\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u30c7\u30fc\u30e2\u30f3 -> \u30ea\u30e2\u30fc\u30c8\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u306e\u8a31\u53ef" } } } diff --git a/homeassistant/components/devolo_home_network/translations/ja.json b/homeassistant/components/devolo_home_network/translations/ja.json index ee08879f713..03612804d2d 100644 --- a/homeassistant/components/devolo_home_network/translations/ja.json +++ b/homeassistant/components/devolo_home_network/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "home_control": "devolo Home Control Central Unit\u306f\u3001\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u52d5\u4f5c\u3057\u307e\u305b\u3093\u3002" + "home_control": "devolo Home Control Central Unit\u306f\u3001\u3053\u306e\u7d71\u5408\u3067\u306f\u52d5\u4f5c\u3057\u307e\u305b\u3093\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/dexcom/translations/ja.json b/homeassistant/components/dexcom/translations/ja.json index 21cc971beec..5353feeece4 100644 --- a/homeassistant/components/dexcom/translations/ja.json +++ b/homeassistant/components/dexcom/translations/ja.json @@ -16,7 +16,7 @@ "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, "description": "Dexcom Share\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u5165\u529b\u3059\u308b", - "title": "Dexcom\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "title": "Dexcom\u7d71\u5408\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } }, diff --git a/homeassistant/components/dialogflow/translations/ja.json b/homeassistant/components/dialogflow/translations/ja.json index 199db0c326c..27e5d35e35e 100644 --- a/homeassistant/components/dialogflow/translations/ja.json +++ b/homeassistant/components/dialogflow/translations/ja.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { - "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[Dialogflow\u306ewebhook\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3]({dialogflow_url})\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "default": "Home Assistant\u306b\u30a4\u30d9\u30f3\u30c8\u3092\u9001\u4fe1\u3059\u308b\u306b\u306f\u3001[Dialogflow\u306ewebhook\u7d71\u5408]({dialogflow_url})\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u6b21\u306e\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:\n\n- URL: `{webhook_url}`\n- Method(\u65b9\u5f0f): POST\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url}) \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { "user": { diff --git a/homeassistant/components/dlna_dmr/translations/ja.json b/homeassistant/components/dlna_dmr/translations/ja.json index 73e242dbb36..ea153d0d452 100644 --- a/homeassistant/components/dlna_dmr/translations/ja.json +++ b/homeassistant/components/dlna_dmr/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "alternative_integration": "\u30c7\u30d0\u30a4\u30b9\u306f\u5225\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3001\u3088\u308a\u9069\u5207\u306b\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059", + "alternative_integration": "\u30c7\u30d0\u30a4\u30b9\u306f\u5225\u306e\u7d71\u5408\u3067\u3001\u3088\u308a\u9069\u5207\u306b\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "discovery_error": "\u4e00\u81f4\u3059\u308bDLNA \u30c7\u30d0\u30a4\u30b9\u3092\u691c\u51fa\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f", "incomplete_config": "\u8a2d\u5b9a\u306b\u5fc5\u8981\u306a\u5909\u6570\u304c\u3042\u308a\u307e\u305b\u3093", diff --git a/homeassistant/components/dunehd/translations/ja.json b/homeassistant/components/dunehd/translations/ja.json index c82d7ce8f94..ebfa6f0c0ac 100644 --- a/homeassistant/components/dunehd/translations/ja.json +++ b/homeassistant/components/dunehd/translations/ja.json @@ -13,7 +13,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "Dune HD\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/dunehd \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "Dune HD\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u306b\u95a2\u3057\u3066\u554f\u984c\u304c\u767a\u751f\u3057\u305f\u5834\u5408\u306f\u3001https://www.home-assistant.io/integrations/dunehd \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044\n\n\u304d\u3061\u3093\u3068\u30d7\u30ec\u30fc\u30e4\u30fc\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3082\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" } } } diff --git a/homeassistant/components/elmax/translations/ja.json b/homeassistant/components/elmax/translations/ja.json index 8730ae4791b..2ae13e7a895 100644 --- a/homeassistant/components/elmax/translations/ja.json +++ b/homeassistant/components/elmax/translations/ja.json @@ -17,7 +17,7 @@ "panel_name": "\u30d1\u30cd\u30eb\u540d", "panel_pin": "PIN\u30b3\u30fc\u30c9" }, - "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u5236\u5fa1\u3059\u308b\u30d1\u30cd\u30eb\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u3059\u308b\u306b\u306f\u3001\u30d1\u30cd\u30eb\u304c\u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u308b\u5fc5\u8981\u304c\u3042\u308b\u3053\u3068\u306b\u6ce8\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "description": "\u3053\u306e\u7d71\u5408\u3067\u5236\u5fa1\u3059\u308b\u30d1\u30cd\u30eb\u3092\u9078\u629e\u3057\u307e\u3059\u3002\u8a2d\u5b9a\u3059\u308b\u306b\u306f\u3001\u30d1\u30cd\u30eb\u304c\u30aa\u30f3\u306b\u306a\u3063\u3066\u3044\u308b\u5fc5\u8981\u304c\u3042\u308b\u3053\u3068\u306b\u6ce8\u610f\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "user": { "data": { diff --git a/homeassistant/components/forked_daapd/translations/ja.json b/homeassistant/components/forked_daapd/translations/ja.json index 692b7ca8346..9fcc73679a7 100644 --- a/homeassistant/components/forked_daapd/translations/ja.json +++ b/homeassistant/components/forked_daapd/translations/ja.json @@ -10,7 +10,7 @@ "websocket_not_enabled": "forked-daapd server\u306eWebSocket\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002", "wrong_host_or_port": "\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002\u30db\u30b9\u30c8\u3068\u30dd\u30fc\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "wrong_password": "\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002", - "wrong_server_type": "forked-daapd \u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u306f\u3001\u30d0\u30fc\u30b8\u30e7\u30f3 >= 27.0 \u306eforked-daapd\u30b5\u30fc\u30d0\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002" + "wrong_server_type": "forked-daapd \u306e\u7d71\u5408\u306b\u306f\u3001\u30d0\u30fc\u30b8\u30e7\u30f3 >= 27.0 \u306eforked-daapd\u30b5\u30fc\u30d0\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002" }, "flow_title": "{name} ({host})", "step": { @@ -34,7 +34,7 @@ "tts_pause_time": "TTS\u306e\u524d\u5f8c\u3067\u4e00\u6642\u505c\u6b62\u3059\u308b\u79d2\u6570", "tts_volume": "TTS\u30dc\u30ea\u30e5\u30fc\u30e0(\u7bc4\u56f2\u306f\u3001[0,1]\u306e\u5c0f\u6570\u70b9)" }, - "description": "forked-daapd\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u3055\u307e\u3056\u307e\u306a\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002", + "description": "forked-daapd\u7d71\u5408\u306e\u3055\u307e\u3056\u307e\u306a\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002", "title": "forked-daapd\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a" } } diff --git a/homeassistant/components/geocaching/translations/ja.json b/homeassistant/components/geocaching/translations/ja.json index 41a238aa637..5de3aba8a04 100644 --- a/homeassistant/components/geocaching/translations/ja.json +++ b/homeassistant/components/geocaching/translations/ja.json @@ -17,8 +17,8 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "Geocaching\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Geocaching\u7d71\u5408\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/github/translations/ja.json b/homeassistant/components/github/translations/ja.json index 6b5c62e4afd..f7dfc37f64d 100644 --- a/homeassistant/components/github/translations/ja.json +++ b/homeassistant/components/github/translations/ja.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "could_not_register": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3068GitHub\u3068\u306e\u767b\u9332\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" + "could_not_register": "\u7d71\u5408\u3068GitHub\u3068\u306e\u767b\u9332\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" }, "progress": { - "wait_for_device": "1. {url} \u3092\u958b\u304f\n2. \u6b21\u306e\u30ad\u30fc\u3092\u8cbc\u308a\u4ed8\u3051\u3066\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u8a8d\u8a3c\u3057\u307e\u3059\u3002\n```\n{code}\n```\n" + "wait_for_device": "1. {url} \u3092\u958b\u304f\n2. \u6b21\u306e\u30ad\u30fc\u3092\u8cbc\u308a\u4ed8\u3051\u3066\u3001\u7d71\u5408\u3092\u8a8d\u8a3c\u3057\u307e\u3059\u3002\n```\n{code}\n```\n" }, "step": { "repositories": { diff --git a/homeassistant/components/google/translations/ja.json b/homeassistant/components/google/translations/ja.json index 4cb05958a76..e18ee9c4a02 100644 --- a/homeassistant/components/google/translations/ja.json +++ b/homeassistant/components/google/translations/ja.json @@ -27,8 +27,8 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "Nest\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Nest\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } }, diff --git a/homeassistant/components/homeassistant/translations/nl.json b/homeassistant/components/homeassistant/translations/nl.json index 1037d161c2b..0bafc7a3959 100644 --- a/homeassistant/components/homeassistant/translations/nl.json +++ b/homeassistant/components/homeassistant/translations/nl.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU-architectuur", + "config_dir": "Configuratiemap", "dev": "Ontwikkeling", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homekit_controller/translations/ja.json b/homeassistant/components/homekit_controller/translations/ja.json index 8e1e35c60e2..fbac8823897 100644 --- a/homeassistant/components/homekit_controller/translations/ja.json +++ b/homeassistant/components/homekit_controller/translations/ja.json @@ -5,7 +5,7 @@ "already_configured": "\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3053\u306e\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u3067\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "already_paired": "\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u3059\u3067\u306b\u4ed6\u306e\u30c7\u30d0\u30a4\u30b9\u3068\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b5\u30ea\u3092\u30ea\u30bb\u30c3\u30c8\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "ignored_model": "\u3053\u306e\u30e2\u30c7\u30eb\u306eHomeKit\u3067\u306e\u5bfe\u5fdc\u306f\u3001\u3088\u308a\u5b8c\u5168\u3067\u30cd\u30a4\u30c6\u30a3\u30d6\u306a\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u4f7f\u7528\u53ef\u80fd\u306a\u305f\u3081\u3001\u30d6\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059\u3002", + "ignored_model": "\u3053\u306e\u30e2\u30c7\u30eb\u306eHomeKit\u3067\u306e\u5bfe\u5fdc\u306f\u3001\u3088\u308a\u5b8c\u5168\u3067\u30cd\u30a4\u30c6\u30a3\u30d6\u306a\u7d71\u5408\u3092\u4f7f\u7528\u53ef\u80fd\u306a\u305f\u3081\u3001\u30d6\u30ed\u30c3\u30af\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "invalid_config_entry": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30da\u30a2\u30ea\u30f3\u30b0\u306e\u6e96\u5099\u304c\u3067\u304d\u3066\u3044\u308b\u3068\u8868\u793a\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001Home Assistant\u306b\u306f\u3059\u3067\u306b\u7af6\u5408\u3059\u308b\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u304c\u3042\u308b\u305f\u3081\u3001\u5148\u306b\u3053\u308c\u3092\u524a\u9664\u3057\u3066\u304a\u304f\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "invalid_properties": "\u7121\u52b9\u306a\u30d7\u30ed\u30d1\u30c6\u30a3\u304c\u30c7\u30d0\u30a4\u30b9\u306b\u3088\u3063\u3066\u77e5\u3089\u3055\u308c\u307e\u3057\u305f\u3002", "no_devices": "\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f" diff --git a/homeassistant/components/icloud/translations/ja.json b/homeassistant/components/icloud/translations/ja.json index 4b7947b8dfa..2295e72f0b2 100644 --- a/homeassistant/components/icloud/translations/ja.json +++ b/homeassistant/components/icloud/translations/ja.json @@ -15,8 +15,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "\u4ee5\u524d\u306b\u5165\u529b\u3057\u305f {username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u4f7f\u3048\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u5f15\u304d\u7d9a\u304d\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "\u4ee5\u524d\u306b\u5165\u529b\u3057\u305f {username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u4f7f\u3048\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\u3053\u306e\u7d71\u5408\u3092\u5f15\u304d\u7d9a\u304d\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "trusted_device": { "data": { diff --git a/homeassistant/components/integration/translations/ja.json b/homeassistant/components/integration/translations/ja.json index 4f35ba53a4a..237d7eef353 100644 --- a/homeassistant/components/integration/translations/ja.json +++ b/homeassistant/components/integration/translations/ja.json @@ -16,7 +16,7 @@ "unit_time": "\u51fa\u529b\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u6642\u9593\u5358\u4f4d\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002" }, "description": "\u7cbe\u5ea6\u306f\u3001\u51fa\u529b\u306e\u5c0f\u6570\u70b9\u4ee5\u4e0b\u306e\u6841\u6570\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002\n\u5408\u8a08\u306f\u3001\u9078\u629e\u3055\u308c\u305f\u5358\u4f4d\u306e\u30d7\u30ea\u30d5\u30a3\u30c3\u30af\u30b9\u3068\u7a4d\u5206\u6642\u9593\u306b\u5f93\u3063\u3066\u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\u3055\u308c\u307e\u3059\u3002", - "title": "\u65b0\u3057\u3044\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u30bb\u30f3\u30b5\u30fc" + "title": "\u65b0\u3057\u3044\u7d71\u5408\u30bb\u30f3\u30b5\u30fc" } } }, @@ -32,5 +32,5 @@ } } }, - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3 - \u30ea\u30fc\u30de\u30f3\u548c\u7a4d\u5206\u30bb\u30f3\u30b5\u30fc" + "title": "\u7d71\u5408 - \u30ea\u30fc\u30de\u30f3\u548c\u7a4d\u5206\u30bb\u30f3\u30b5\u30fc" } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/ja.json b/homeassistant/components/isy994/translations/ja.json index f1a447901b9..60b69d07363 100644 --- a/homeassistant/components/isy994/translations/ja.json +++ b/homeassistant/components/isy994/translations/ja.json @@ -41,7 +41,7 @@ "sensor_string": "\u30ce\u30fc\u30c9 \u30bb\u30f3\u30b5\u30fc\u6587\u5b57\u5217", "variable_sensor_string": "\u53ef\u5909\u30bb\u30f3\u30b5\u30fc\u6587\u5b57\u5217(Variable Sensor String)" }, - "description": "ISY\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a:\n \u2022 Node Sensor String: \u540d\u524d\u306b\u3001'Node Sensor String' \u3092\u542b\u3080\u4efb\u610f\u306e\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u30d5\u30a9\u30eb\u30c0\u306f\u3001\u30bb\u30f3\u30b5\u30fc\u307e\u305f\u306f\u30d0\u30a4\u30ca\u30ea\u30bb\u30f3\u30b5\u30fc\u3068\u3057\u3066\u6271\u308f\u308c\u307e\u3059\u3002\n \u2022 Ignore String: \u540d\u524d\u306b\u3001'Ignore String'\u3092\u6301\u3064\u30c7\u30d0\u30a4\u30b9\u306f\u7121\u8996\u3055\u308c\u307e\u3059\u3002\n \u2022 Variable Sensor String: 'Variable Sensor String' \u3092\u542b\u3080\u5909\u6570\u306f\u3001\u30bb\u30f3\u30b5\u30fc\u3068\u3057\u3066\u8ffd\u52a0\u3055\u308c\u307e\u3059\u3002\n \u2022 Restore Light Brightness: \u6709\u52b9\u306b\u3059\u308b\u3068\u3001\u30c7\u30d0\u30a4\u30b9\u7d44\u307f\u8fbc\u307f\u306e\u30aa\u30f3\u30ec\u30d9\u30eb\u3067\u306f\u306a\u304f\u3001\u30e9\u30a4\u30c8\u3092\u30aa\u30f3\u306b\u3057\u305f\u3068\u304d\u306b\u3001\u4ee5\u524d\u306e\u660e\u308b\u3055\u306b\u5fa9\u5143\u3055\u308c\u307e\u3059\u3002", + "description": "ISY\u7d71\u5408\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u8a2d\u5b9a:\n \u2022 Node Sensor String: \u540d\u524d\u306b\u3001'Node Sensor String' \u3092\u542b\u3080\u4efb\u610f\u306e\u30c7\u30d0\u30a4\u30b9\u307e\u305f\u306f\u30d5\u30a9\u30eb\u30c0\u306f\u3001\u30bb\u30f3\u30b5\u30fc\u307e\u305f\u306f\u30d0\u30a4\u30ca\u30ea\u30bb\u30f3\u30b5\u30fc\u3068\u3057\u3066\u6271\u308f\u308c\u307e\u3059\u3002\n \u2022 Ignore String: \u540d\u524d\u306b\u3001'Ignore String'\u3092\u6301\u3064\u30c7\u30d0\u30a4\u30b9\u306f\u7121\u8996\u3055\u308c\u307e\u3059\u3002\n \u2022 Variable Sensor String: 'Variable Sensor String' \u3092\u542b\u3080\u5909\u6570\u306f\u3001\u30bb\u30f3\u30b5\u30fc\u3068\u3057\u3066\u8ffd\u52a0\u3055\u308c\u307e\u3059\u3002\n \u2022 Restore Light Brightness: \u6709\u52b9\u306b\u3059\u308b\u3068\u3001\u30c7\u30d0\u30a4\u30b9\u7d44\u307f\u8fbc\u307f\u306e\u30aa\u30f3\u30ec\u30d9\u30eb\u3067\u306f\u306a\u304f\u3001\u30e9\u30a4\u30c8\u3092\u30aa\u30f3\u306b\u3057\u305f\u3068\u304d\u306b\u3001\u4ee5\u524d\u306e\u660e\u308b\u3055\u306b\u5fa9\u5143\u3055\u308c\u307e\u3059\u3002", "title": "ISY994\u30aa\u30d7\u30b7\u30e7\u30f3" } } diff --git a/homeassistant/components/kodi/translations/ja.json b/homeassistant/components/kodi/translations/ja.json index 88855ff4d77..26e81ebd818 100644 --- a/homeassistant/components/kodi/translations/ja.json +++ b/homeassistant/components/kodi/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "no_uuid": "Kodi \u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)ID\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u308c\u306f\u3001Kodi \u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u53e4\u3044(17.x \u4ee5\u4e0b)\u3053\u3068\u304c\u539f\u56e0\u3067\u3042\u308b\u53ef\u80fd\u6027\u304c\u9ad8\u3044\u3067\u3059\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b\u304b\u3001\u3088\u308a\u65b0\u3057\u3044Kodi\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u306b\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "no_uuid": "Kodi \u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u30e6\u30cb\u30fc\u30af(\u4e00\u610f)ID\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u308c\u306f\u3001Kodi \u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u53e4\u3044(17.x \u4ee5\u4e0b)\u3053\u3068\u304c\u539f\u56e0\u3067\u3042\u308b\u53ef\u80fd\u6027\u304c\u9ad8\u3044\u3067\u3059\u3002\u7d71\u5408\u3092\u624b\u52d5\u3067\u8a2d\u5b9a\u3059\u308b\u304b\u3001\u3088\u308a\u65b0\u3057\u3044Kodi\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u306b\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { diff --git a/homeassistant/components/laundrify/translations/ja.json b/homeassistant/components/laundrify/translations/ja.json index 153fcf99afb..020b7578e80 100644 --- a/homeassistant/components/laundrify/translations/ja.json +++ b/homeassistant/components/laundrify/translations/ja.json @@ -17,8 +17,8 @@ "description": "Laundrify-App\u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308b\u3001\u3042\u306a\u305f\u306e\u500b\u4eba\u8a8d\u8a3c\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "reauth_confirm": { - "description": "Laundrify\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Laundrify\u7d71\u5408\u3067\u306f\u3001\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/lg_soundbar/translations/nl.json b/homeassistant/components/lg_soundbar/translations/nl.json new file mode 100644 index 00000000000..7345479d97a --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Dienst is al geconfigureerd", + "existing_instance_updated": "Bestaande configuratie bijgewerkt." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/ja.json b/homeassistant/components/life360/translations/ja.json index 4776e3409ea..ab320748086 100644 --- a/homeassistant/components/life360/translations/ja.json +++ b/homeassistant/components/life360/translations/ja.json @@ -21,7 +21,7 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/life360/translations/nl.json b/homeassistant/components/life360/translations/nl.json index cdcc937a33e..b0e54bde3c5 100644 --- a/homeassistant/components/life360/translations/nl.json +++ b/homeassistant/components/life360/translations/nl.json @@ -32,5 +32,12 @@ "title": "Life360-accountgegevens" } } + }, + "options": { + "step": { + "init": { + "title": "Accountinstellingen" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/ja.json b/homeassistant/components/lyric/translations/ja.json index 98b2a11c5ff..2a5abdcb5b7 100644 --- a/homeassistant/components/lyric/translations/ja.json +++ b/homeassistant/components/lyric/translations/ja.json @@ -13,8 +13,8 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "(\u6b4c\u8a5e)Lyric\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "(\u6b4c\u8a5e)Lyric\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/melcloud/translations/ja.json b/homeassistant/components/melcloud/translations/ja.json index b8f2d14e5cd..377ba291786 100644 --- a/homeassistant/components/melcloud/translations/ja.json +++ b/homeassistant/components/melcloud/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u3053\u306e\u30e1\u30fc\u30eb\u306b\u306f\u3059\u3067\u306b\u3001MELCloud\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u304c\u66f4\u65b0\u3055\u308c\u307e\u3057\u305f\u3002" + "already_configured": "\u3053\u306e\u30e1\u30fc\u30eb\u306b\u306f\u3059\u3067\u306b\u3001MELCloud\u7d71\u5408\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3\u304c\u66f4\u65b0\u3055\u308c\u307e\u3057\u305f\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/mobile_app/translations/ja.json b/homeassistant/components/mobile_app/translations/ja.json index 1f848fa1988..fa8ae167b1d 100644 --- a/homeassistant/components/mobile_app/translations/ja.json +++ b/homeassistant/components/mobile_app/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "install_app": "Mobile app\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u958b\u3044\u3066\u3001Home Assistant\u3068\u306e\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u3002\u4e92\u63db\u6027\u306e\u3042\u308b\u30a2\u30d7\u30ea\u306e\u4e00\u89a7\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({apps_url})\u3092\u3054\u89a7\u304f\u3060\u3055\u3044\u3002" + "install_app": "Mobile app\u7d71\u5408\u3092\u958b\u3044\u3066\u3001Home Assistant\u3068\u306e\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u3002\u4e92\u63db\u6027\u306e\u3042\u308b\u30a2\u30d7\u30ea\u306e\u4e00\u89a7\u306f\u3001[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({apps_url})\u3092\u3054\u89a7\u304f\u3060\u3055\u3044\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/modem_callerid/translations/ja.json b/homeassistant/components/modem_callerid/translations/ja.json index a5b9df933ef..35ad69b3e1c 100644 --- a/homeassistant/components/modem_callerid/translations/ja.json +++ b/homeassistant/components/modem_callerid/translations/ja.json @@ -10,14 +10,14 @@ }, "step": { "usb_confirm": { - "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002" + "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u7d71\u5408\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002" }, "user": { "data": { "name": "\u540d\u524d", "port": "\u30dd\u30fc\u30c8" }, - "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002" + "description": "\u3053\u308c\u306f\u3001CX93001\u97f3\u58f0\u30e2\u30c7\u30e0\u3092\u4f7f\u7528\u3057\u305f\u56fa\u5b9a\u96fb\u8a71\u306e\u7d71\u5408\u3067\u3059\u3002\u767a\u4fe1\u8005\u756a\u53f7\u60c5\u5831\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u3067\u3001\u7740\u4fe1\u3092\u62d2\u5426\u3059\u308b\u30aa\u30d7\u30b7\u30e7\u30f3\u3082\u3042\u308a\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/mullvad/translations/ja.json b/homeassistant/components/mullvad/translations/ja.json index fc1791527f9..3119597977a 100644 --- a/homeassistant/components/mullvad/translations/ja.json +++ b/homeassistant/components/mullvad/translations/ja.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Mullvad VPN\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + "description": "Mullvad VPN\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" } } } diff --git a/homeassistant/components/mysensors/translations/ja.json b/homeassistant/components/mysensors/translations/ja.json index da0354fe3c7..4a73363bf16 100644 --- a/homeassistant/components/mysensors/translations/ja.json +++ b/homeassistant/components/mysensors/translations/ja.json @@ -33,7 +33,7 @@ "invalid_serial": "\u7121\u52b9\u306a\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8", "invalid_subscribe_topic": "\u7121\u52b9\u306a\u30b5\u30d6\u30b9\u30af\u30e9\u30a4\u30d6 \u30c8\u30d4\u30c3\u30af", "invalid_version": "MySensors\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u7121\u52b9\u3067\u3059", - "mqtt_required": "MQTT\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093", + "mqtt_required": "MQTT\u7d71\u5408\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "not_a_number": "\u6570\u5b57\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "port_out_of_range": "\u30dd\u30fc\u30c8\u756a\u53f7\u306f1\u4ee5\u4e0a65535\u4ee5\u4e0b\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "same_topic": "\u30b5\u30d6\u30b9\u30af\u30e9\u30a4\u30d6\u3068\u30d1\u30d6\u30ea\u30c3\u30b7\u30e5\u306e\u30c8\u30d4\u30c3\u30af\u304c\u540c\u3058\u3067\u3059", diff --git a/homeassistant/components/nam/translations/ja.json b/homeassistant/components/nam/translations/ja.json index 2125c9e3e38..d5147344c61 100644 --- a/homeassistant/components/nam/translations/ja.json +++ b/homeassistant/components/nam/translations/ja.json @@ -4,7 +4,7 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "device_unsupported": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "reauth_unsuccessful": "\u518d\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u305f\u306e\u3067\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + "reauth_unsuccessful": "\u518d\u8a8d\u8a3c\u306b\u5931\u6557\u3057\u305f\u306e\u3067\u3001\u7d71\u5408\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", @@ -34,7 +34,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "Nettigo Air Monitor\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "description": "Nettigo Air Monitor\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" } } } diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index 9aff487997c..398b09f92c1 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -77,8 +77,8 @@ "title": "Google Cloud\u306e\u8a2d\u5b9a" }, "reauth_confirm": { - "description": "Nest\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Nest\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } }, diff --git a/homeassistant/components/netatmo/translations/ja.json b/homeassistant/components/netatmo/translations/ja.json index b9f2f17960a..016411aef41 100644 --- a/homeassistant/components/netatmo/translations/ja.json +++ b/homeassistant/components/netatmo/translations/ja.json @@ -15,8 +15,8 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "Netatmo\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Netatmo\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } }, diff --git a/homeassistant/components/nextdns/translations/de.json b/homeassistant/components/nextdns/translations/de.json index 58e470e06ff..51a5eaf5edd 100644 --- a/homeassistant/components/nextdns/translations/de.json +++ b/homeassistant/components/nextdns/translations/de.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "Server erreichen" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/et.json b/homeassistant/components/nextdns/translations/et.json index 9b4d56f51d4..eaf8efc0341 100644 --- a/homeassistant/components/nextdns/translations/et.json +++ b/homeassistant/components/nextdns/translations/et.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "\u00dchendu serveriga" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/ja.json b/homeassistant/components/nextdns/translations/ja.json index 7c1f0614fc1..df57113cc58 100644 --- a/homeassistant/components/nextdns/translations/ja.json +++ b/homeassistant/components/nextdns/translations/ja.json @@ -20,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "\u30b5\u30fc\u30d0\u30fc\u306b\u5230\u9054" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/nl.json b/homeassistant/components/nextdns/translations/nl.json new file mode 100644 index 00000000000..436e1a68b7d --- /dev/null +++ b/homeassistant/components/nextdns/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_api_key": "Ongeldige API-sleutel", + "unknown": "Onverwachte fout" + }, + "step": { + "profiles": { + "data": { + "profile": "Profiel" + } + }, + "user": { + "data": { + "api_key": "API-sleutel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/ja.json b/homeassistant/components/nfandroidtv/translations/ja.json index fff28117234..3db6768efcb 100644 --- a/homeassistant/components/nfandroidtv/translations/ja.json +++ b/homeassistant/components/nfandroidtv/translations/ja.json @@ -13,7 +13,7 @@ "host": "\u30db\u30b9\u30c8", "name": "\u540d\u524d" }, - "description": "\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306b\u306f\u3001AndroidTV\u30a2\u30d7\u30ea\u306e\u901a\u77e5\u304c\u5fc5\u8981\u3067\u3059\u3002 \n\nAndroid TV\u306e\u5834\u5408: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV\u306e\u5834\u5408: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04((DHCP reservation)\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167))\u307e\u305f\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u306b\u9759\u7684IP\u30a2\u30c9\u30ec\u30b9\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u305d\u3046\u3067\u306a\u3044\u5834\u5408\u3001\u30c7\u30d0\u30a4\u30b9\u306f\u6700\u7d42\u7684\u306b\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002" + "description": "\u3053\u306e\u7d71\u5408\u306b\u306f\u3001AndroidTV\u30a2\u30d7\u30ea\u306e\u901a\u77e5\u304c\u5fc5\u8981\u3067\u3059\u3002 \n\nAndroid TV\u306e\u5834\u5408: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nFire TV\u306e\u5834\u5408: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\n\u30eb\u30fc\u30bf\u30fc\u306eDHCP\u4e88\u7d04((DHCP reservation)\u30eb\u30fc\u30bf\u30fc\u306e\u30e6\u30fc\u30b6\u30fc\u30de\u30cb\u30e5\u30a2\u30eb\u3092\u53c2\u7167))\u307e\u305f\u306f\u3001\u30c7\u30d0\u30a4\u30b9\u306b\u9759\u7684IP\u30a2\u30c9\u30ec\u30b9\u3092\u8a2d\u5b9a\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u305d\u3046\u3067\u306a\u3044\u5834\u5408\u3001\u30c7\u30d0\u30a4\u30b9\u306f\u6700\u7d42\u7684\u306b\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/notion/translations/ja.json b/homeassistant/components/notion/translations/ja.json index 8cd2a58e1e0..2171d2722a1 100644 --- a/homeassistant/components/notion/translations/ja.json +++ b/homeassistant/components/notion/translations/ja.json @@ -14,7 +14,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5ea6\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/nuki/translations/ja.json b/homeassistant/components/nuki/translations/ja.json index 6f54d0d8b4b..d4e1d68d780 100644 --- a/homeassistant/components/nuki/translations/ja.json +++ b/homeassistant/components/nuki/translations/ja.json @@ -13,8 +13,8 @@ "data": { "token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" }, - "description": "Nuki\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001bridge\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Nuki\u7d71\u5408\u3067\u306f\u3001bridge\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/openweathermap/translations/ja.json b/homeassistant/components/openweathermap/translations/ja.json index 511d59b2138..b89b1ef0985 100644 --- a/homeassistant/components/openweathermap/translations/ja.json +++ b/homeassistant/components/openweathermap/translations/ja.json @@ -15,9 +15,9 @@ "latitude": "\u7def\u5ea6", "longitude": "\u7d4c\u5ea6", "mode": "\u30e2\u30fc\u30c9", - "name": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u540d\u524d" + "name": "\u540d\u524d" }, - "description": "OpenWeatherMap\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://openweathermap.org/appid \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "OpenWeatherMap\u306e\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://openweathermap.org/appid \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } }, diff --git a/homeassistant/components/picnic/translations/ja.json b/homeassistant/components/picnic/translations/ja.json index fd9fe67db73..b4f73a91406 100644 --- a/homeassistant/components/picnic/translations/ja.json +++ b/homeassistant/components/picnic/translations/ja.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "different_account": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a\u3067\u4f7f\u7528\u3057\u305f\u3082\u306e\u3068\u540c\u3058\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "different_account": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3001\u7d71\u5408\u306e\u8a2d\u5b9a\u3067\u4f7f\u7528\u3057\u305f\u3082\u306e\u3068\u540c\u3058\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, diff --git a/homeassistant/components/plugwise/translations/ja.json b/homeassistant/components/plugwise/translations/ja.json index 87b8d501e0d..0103774c138 100644 --- a/homeassistant/components/plugwise/translations/ja.json +++ b/homeassistant/components/plugwise/translations/ja.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "invalid_setup": "Anna\u306e\u4ee3\u308f\u308a\u306b\u3001Adam\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001Home Assistant Plugwise\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "invalid_setup": "Anna\u306e\u4ee3\u308f\u308a\u306b\u3001Adam\u3092\u8ffd\u52a0\u3057\u307e\u3059\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001Home Assistant Plugwise\u7d71\u5408\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name}", diff --git a/homeassistant/components/qnap_qsw/translations/nl.json b/homeassistant/components/qnap_qsw/translations/nl.json index 10ec94f589d..73e0af1fe00 100644 --- a/homeassistant/components/qnap_qsw/translations/nl.json +++ b/homeassistant/components/qnap_qsw/translations/nl.json @@ -9,6 +9,12 @@ "invalid_auth": "Ongeldige authenticatie" }, "step": { + "discovered_connection": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/renault/translations/ja.json b/homeassistant/components/renault/translations/ja.json index 743dfa36fb5..42d819589b7 100644 --- a/homeassistant/components/renault/translations/ja.json +++ b/homeassistant/components/renault/translations/ja.json @@ -20,7 +20,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/ridwell/translations/ja.json b/homeassistant/components/ridwell/translations/ja.json index 33d7e12ebd4..746d237457f 100644 --- a/homeassistant/components/ridwell/translations/ja.json +++ b/homeassistant/components/ridwell/translations/ja.json @@ -14,7 +14,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/rtsp_to_webrtc/translations/ja.json b/homeassistant/components/rtsp_to_webrtc/translations/ja.json index 6359d66f50a..c904164a599 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/ja.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/ja.json @@ -19,7 +19,7 @@ "data": { "server_url": "RTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u306eURL \u4f8b: https://example.com" }, - "description": "RTSPtoWebRTC\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306f\u3001RTSP\u30b9\u30c8\u30ea\u30fc\u30e0\u3092WebRTC\u306b\u5909\u63db\u3059\u308b\u30b5\u30fc\u30d0\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002RTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u306eURL\u3092\u5165\u529b\u3057\u307e\u3059\u3002", + "description": "RTSPtoWebRTC\u7d71\u5408\u306f\u3001RTSP\u30b9\u30c8\u30ea\u30fc\u30e0\u3092WebRTC\u306b\u5909\u63db\u3059\u308b\u30b5\u30fc\u30d0\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002RTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u306eURL\u3092\u5165\u529b\u3057\u307e\u3059\u3002", "title": "RTSPtoWebRTC\u306e\u8a2d\u5b9a" } } diff --git a/homeassistant/components/sense/translations/ja.json b/homeassistant/components/sense/translations/ja.json index 437ce96d9f1..50dbc070ac9 100644 --- a/homeassistant/components/sense/translations/ja.json +++ b/homeassistant/components/sense/translations/ja.json @@ -14,8 +14,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "Sense\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8 {email} \u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Sense\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8 {email} \u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/simplepush/translations/nl.json b/homeassistant/components/simplepush/translations/nl.json index ee691ac3901..176318b3f3c 100644 --- a/homeassistant/components/simplepush/translations/nl.json +++ b/homeassistant/components/simplepush/translations/nl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, "error": { "cannot_connect": "Kan geen verbinding maken" }, diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index 57909d30f69..e264ec18aab 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -18,7 +18,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "\u30a2\u30af\u30bb\u30b9\u306e\u6709\u52b9\u671f\u9650\u304c\u5207\u308c\u3066\u3044\u308b\u304b\u3001\u53d6\u308a\u6d88\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u30ea\u30f3\u30af\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "sms_2fa": { "data": { diff --git a/homeassistant/components/sleepiq/translations/ja.json b/homeassistant/components/sleepiq/translations/ja.json index b1e0ec1d86e..fb81857cb5f 100644 --- a/homeassistant/components/sleepiq/translations/ja.json +++ b/homeassistant/components/sleepiq/translations/ja.json @@ -13,8 +13,8 @@ "data": { "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, - "description": "SleepIQ\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8 {username} \u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "SleepIQ\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8 {username} \u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/smappee/translations/ja.json b/homeassistant/components/smappee/translations/ja.json index 9dff009e3dd..aabbfd52564 100644 --- a/homeassistant/components/smappee/translations/ja.json +++ b/homeassistant/components/smappee/translations/ja.json @@ -5,7 +5,7 @@ "already_configured_local_device": "\u30ed\u30fc\u30ab\u30eb\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30af\u30e9\u30a6\u30c9\u30c7\u30d0\u30a4\u30b9\u3092\u8a2d\u5b9a\u3059\u308b\u524d\u306b\u3001\u307e\u305a\u305d\u308c\u3089\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "invalid_mdns": "Smappee\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u3002", + "invalid_mdns": "Smappee\u7d71\u5408\u3067\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u3002", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})" }, @@ -21,7 +21,7 @@ "data": { "host": "\u30db\u30b9\u30c8" }, - "description": "\u30db\u30b9\u30c8\u3092\u5165\u529b\u3057\u3066\u3001Smappee local\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u958b\u59cb\u3057\u307e\u3059" + "description": "\u30db\u30b9\u30c8\u3092\u5165\u529b\u3057\u3066\u3001Smappee local\u7d71\u5408\u3092\u958b\u59cb\u3057\u307e\u3059" }, "pick_implementation": { "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" diff --git a/homeassistant/components/smartthings/translations/ja.json b/homeassistant/components/smartthings/translations/ja.json index e522744cd4d..94d1aba2606 100644 --- a/homeassistant/components/smartthings/translations/ja.json +++ b/homeassistant/components/smartthings/translations/ja.json @@ -19,14 +19,14 @@ "data": { "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3" }, - "description": "[\u624b\u9806]({component_url})\u3054\u3068\u306b\u4f5c\u6210\u3055\u308c\u305f\u3001SmartThings[\u500b\u4eba\u7528\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3]({token_url})\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \u3053\u308c\u306f\u3001SmartThings account\u5185\u306bHome Assistant\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u4f5c\u6210\u3059\u308b\u305f\u3081\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002", + "description": "[\u624b\u9806]({component_url})\u3054\u3068\u306b\u4f5c\u6210\u3055\u308c\u305f\u3001SmartThings[\u500b\u4eba\u7528\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3]({token_url})\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \u3053\u308c\u306f\u3001SmartThings account\u5185\u306bHome Assistant\u7d71\u5408\u3092\u4f5c\u6210\u3059\u308b\u305f\u3081\u306b\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002", "title": "\u30d1\u30fc\u30bd\u30ca\u30eb \u30a2\u30af\u30bb\u30b9 \u30c8\u30fc\u30af\u30f3\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" }, "select_location": { "data": { "location_id": "\u30ed\u30b1\u30fc\u30b7\u30e7\u30f3" }, - "description": "Home Assistant\u306b\u8ffd\u52a0\u3057\u305f\u3044SmartThings\u306e\u5834\u6240\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3059\u308b\u3068\u3001\u65b0\u3057\u3044\u30a6\u30a3\u30f3\u30c9\u30a6\u304c\u958b\u304f\u306e\u3067\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u3001\u9078\u629e\u3057\u305f\u5834\u6240\u3078\u306eHome Assistant\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u627f\u8a8d\u3059\u308b\u3088\u3046\u6c42\u3081\u3089\u308c\u307e\u3059\u3002", + "description": "Home Assistant\u306b\u8ffd\u52a0\u3057\u305f\u3044SmartThings\u306e\u5834\u6240\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u3059\u308b\u3068\u3001\u65b0\u3057\u3044\u30a6\u30a3\u30f3\u30c9\u30a6\u304c\u958b\u304f\u306e\u3067\u30ed\u30b0\u30a4\u30f3\u3057\u3066\u3001\u9078\u629e\u3057\u305f\u5834\u6240\u3078\u306eHome Assistant\u7d71\u5408\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u627f\u8a8d\u3059\u308b\u3088\u3046\u6c42\u3081\u3089\u308c\u307e\u3059\u3002", "title": "\u5834\u6240\u3092\u9078\u629e" }, "user": { diff --git a/homeassistant/components/smarttub/translations/ja.json b/homeassistant/components/smarttub/translations/ja.json index 44d753fa22e..1488ddf9e16 100644 --- a/homeassistant/components/smarttub/translations/ja.json +++ b/homeassistant/components/smarttub/translations/ja.json @@ -9,8 +9,8 @@ }, "step": { "reauth_confirm": { - "description": "SmartTub\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "SmartTub\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/somfy_mylink/translations/ja.json b/homeassistant/components/somfy_mylink/translations/ja.json index 49f819659b4..97b8e990af7 100644 --- a/homeassistant/components/somfy_mylink/translations/ja.json +++ b/homeassistant/components/somfy_mylink/translations/ja.json @@ -16,7 +16,7 @@ "port": "\u30dd\u30fc\u30c8", "system_id": "\u30b7\u30b9\u30c6\u30e0ID" }, - "description": "\u30b7\u30b9\u30c6\u30e0ID \u306f\u3001\u30af\u30e9\u30a6\u30c9\u4ee5\u5916\u306e\u30b5\u30fc\u30d3\u30b9\u3092\u9078\u629e\u3059\u308b\u3053\u3068\u306b\u3088\u308a\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e MyLink \u30a2\u30d7\u30ea\u3067\u53d6\u5f97\u3067\u304d\u307e\u3059\u3002" + "description": "\u30b7\u30b9\u30c6\u30e0ID \u306f\u3001\u30af\u30e9\u30a6\u30c9\u4ee5\u5916\u306e\u30b5\u30fc\u30d3\u30b9\u3092\u9078\u629e\u3059\u308b\u3053\u3068\u306b\u3088\u308a\u3001\u7d71\u5408\u306e MyLink \u30a2\u30d7\u30ea\u3067\u53d6\u5f97\u3067\u304d\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/sonarr/translations/ja.json b/homeassistant/components/sonarr/translations/ja.json index cbe230bbc94..d37c4915481 100644 --- a/homeassistant/components/sonarr/translations/ja.json +++ b/homeassistant/components/sonarr/translations/ja.json @@ -12,8 +12,8 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "Sonarr\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306f\u3001\u30db\u30b9\u30c8\u3055\u308c\u3066\u3044\u308bSonarr API\u3067\u624b\u52d5\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {host}", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Sonarr\u7d71\u5408\u306f\u3001\u30db\u30b9\u30c8\u3055\u308c\u3066\u3044\u308bSonarr API\u3067\u624b\u52d5\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {host}", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/soundtouch/translations/nl.json b/homeassistant/components/soundtouch/translations/nl.json new file mode 100644 index 00000000000..0ccc8057ac8 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/ja.json b/homeassistant/components/spotify/translations/ja.json index 9c0e308dc18..b0b94ebe5ab 100644 --- a/homeassistant/components/spotify/translations/ja.json +++ b/homeassistant/components/spotify/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", - "missing_configuration": "Spotify\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "missing_configuration": "Spotify\u7d71\u5408\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", "reauth_account_mismatch": "\u8a8d\u8a3c\u3055\u308c\u305fSpotify\u30a2\u30ab\u30a6\u30f3\u30c8\u304c\u3001\u518d\u8a8d\u8a3c\u304c\u5fc5\u8981\u306a\u30a2\u30ab\u30a6\u30f3\u30c8\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" }, @@ -14,8 +14,8 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "Spotify\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001Spotify\u3067\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {account}", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Spotify\u7d71\u5408\u3067\u306f\u3001Spotify\u3067\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {account}", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } }, diff --git a/homeassistant/components/steam_online/translations/ja.json b/homeassistant/components/steam_online/translations/ja.json index e138d46319b..f62fd45e767 100644 --- a/homeassistant/components/steam_online/translations/ja.json +++ b/homeassistant/components/steam_online/translations/ja.json @@ -12,8 +12,8 @@ }, "step": { "reauth_confirm": { - "description": "Steam\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306f\u624b\u52d5\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\n\n\u3053\u3053\u3067\u3042\u306a\u305f\u306e\u30ad\u30fc\u3092\u898b\u3064\u3051\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059: {api_key_url}", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Steam\u7d71\u5408\u306f\u624b\u52d5\u3067\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\n\n\u3053\u3053\u3067\u3042\u306a\u305f\u306e\u30ad\u30fc\u3092\u898b\u3064\u3051\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059: {api_key_url}", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/syncthing/translations/ja.json b/homeassistant/components/syncthing/translations/ja.json index 2a725cbf3cc..ee9c4b02e26 100644 --- a/homeassistant/components/syncthing/translations/ja.json +++ b/homeassistant/components/syncthing/translations/ja.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "title": "Syncthing\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7", + "title": "Syncthing\u7d71\u5408\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7", "token": "\u30c8\u30fc\u30af\u30f3", "url": "URL", "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" diff --git a/homeassistant/components/synology_dsm/translations/ja.json b/homeassistant/components/synology_dsm/translations/ja.json index 47245b2ceb8..2654c5f3910 100644 --- a/homeassistant/components/synology_dsm/translations/ja.json +++ b/homeassistant/components/synology_dsm/translations/ja.json @@ -35,7 +35,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "title": "Synology DSM \u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "Synology DSM \u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/totalconnect/translations/ja.json b/homeassistant/components/totalconnect/translations/ja.json index 1e7750b2442..65f56bffc68 100644 --- a/homeassistant/components/totalconnect/translations/ja.json +++ b/homeassistant/components/totalconnect/translations/ja.json @@ -19,7 +19,7 @@ }, "reauth_confirm": { "description": "Total Connect\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/tractive/translations/ja.json b/homeassistant/components/tractive/translations/ja.json index 7f97d4c23f4..97defadf3ff 100644 --- a/homeassistant/components/tractive/translations/ja.json +++ b/homeassistant/components/tractive/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u7d71\u5408\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { diff --git a/homeassistant/components/transmission/translations/ja.json b/homeassistant/components/transmission/translations/ja.json index 6f3b3191c83..f12b734a7be 100644 --- a/homeassistant/components/transmission/translations/ja.json +++ b/homeassistant/components/transmission/translations/ja.json @@ -15,7 +15,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u7121\u52b9\u3067\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/ukraine_alarm/translations/ja.json b/homeassistant/components/ukraine_alarm/translations/ja.json index 81bdabdc8d3..babf5963357 100644 --- a/homeassistant/components/ukraine_alarm/translations/ja.json +++ b/homeassistant/components/ukraine_alarm/translations/ja.json @@ -25,7 +25,7 @@ "data": { "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" }, - "description": "\u30a6\u30af\u30e9\u30a4\u30ca\u306e\u8b66\u5831\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001 {api_url} \u306b\u79fb\u52d5\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "\u30a6\u30af\u30e9\u30a4\u30ca\u306e\u8b66\u5831\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001 {api_url} \u306b\u79fb\u52d5\u3057\u3066\u304f\u3060\u3055\u3044" } } } diff --git a/homeassistant/components/unifi/translations/ja.json b/homeassistant/components/unifi/translations/ja.json index fba092385c1..cd810e12d6a 100644 --- a/homeassistant/components/unifi/translations/ja.json +++ b/homeassistant/components/unifi/translations/ja.json @@ -27,7 +27,7 @@ }, "options": { "abort": { - "integration_not_setup": "UniFi\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u304c\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + "integration_not_setup": "UniFi\u7d71\u5408\u304c\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3055\u308c\u3066\u3044\u307e\u305b\u3093" }, "step": { "client_control": { @@ -57,7 +57,7 @@ "track_clients": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u3092\u8ffd\u8de1\u3059\u308b", "track_devices": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u8de1(\u30e6\u30d3\u30ad\u30c6\u30a3\u30c7\u30d0\u30a4\u30b9)" }, - "description": "UniFi\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u8a2d\u5b9a" + "description": "UniFi\u7d71\u5408\u306e\u8a2d\u5b9a" }, "statistics_sensors": { "data": { diff --git a/homeassistant/components/uptimerobot/translations/ja.json b/homeassistant/components/uptimerobot/translations/ja.json index e8c66b5088b..55ff808725f 100644 --- a/homeassistant/components/uptimerobot/translations/ja.json +++ b/homeassistant/components/uptimerobot/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "reauth_failed_existing": "\u69cb\u6210\u30a8\u30f3\u30c8\u30ea\u30fc\u3092\u66f4\u65b0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u7d71\u5408\u3092\u524a\u9664\u3057\u3066\u518d\u5ea6\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, @@ -19,7 +19,7 @@ "api_key": "API\u30ad\u30fc" }, "description": "UptimeRobot\u304b\u3089\u65b0\u898f\u306e\u8aad\u307f\u53d6\u308a\u5c02\u7528\u306eAPI\u30ad\u30fc\u3092\u5f97\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/vicare/translations/ja.json b/homeassistant/components/vicare/translations/ja.json index aa7e159778c..10ffe077aca 100644 --- a/homeassistant/components/vicare/translations/ja.json +++ b/homeassistant/components/vicare/translations/ja.json @@ -16,7 +16,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "E\u30e1\u30fc\u30eb" }, - "description": "ViCare\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.viessmann.com \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "ViCare\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.viessmann.com \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } } diff --git a/homeassistant/components/vulcan/translations/ja.json b/homeassistant/components/vulcan/translations/ja.json index 4f18d47b7ea..5d6f5bbcca7 100644 --- a/homeassistant/components/vulcan/translations/ja.json +++ b/homeassistant/components/vulcan/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "all_student_already_configured": "\u3059\u3079\u3066\u306e\u751f\u5f92\u306f\u3059\u3067\u306b\u8ffd\u52a0\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "already_configured": "\u305d\u306e\u5b66\u751f\u306f\u3059\u3067\u306b\u8ffd\u52a0\u3055\u308c\u3066\u3044\u307e\u3059\u3002", - "no_matching_entries": "\u4e00\u81f4\u3059\u308b\u30a8\u30f3\u30c8\u30ea\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002\u5225\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u4f7f\u7528\u3059\u308b\u304b\u3001outdated student\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044..", + "no_matching_entries": "\u4e00\u81f4\u3059\u308b\u30a8\u30f3\u30c8\u30ea\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002\u5225\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u4f7f\u7528\u3059\u308b\u304b\u3001outdated student\u7d71\u5408\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044..", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f" }, "error": { @@ -48,7 +48,7 @@ "data": { "student_name": "\u751f\u5f92\u3092\u9078\u629e(Select student)" }, - "description": "\u751f\u5f92\u3092\u9078\u629e\u3057\u307e\u3059(Select student)\u3002\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u518d\u5ea6\u8ffd\u52a0\u3059\u308b\u3053\u3068\u3067\u3001\u3088\u308a\u591a\u304f\u306e\u751f\u5f92\u3092\u8ffd\u52a0\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002" + "description": "\u751f\u5f92\u3092\u9078\u629e\u3057\u307e\u3059(Select student)\u3002\u7d71\u5408\u3092\u518d\u5ea6\u8ffd\u52a0\u3059\u308b\u3053\u3068\u3067\u3001\u3088\u308a\u591a\u304f\u306e\u751f\u5f92\u3092\u8ffd\u52a0\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/watttime/translations/ja.json b/homeassistant/components/watttime/translations/ja.json index 87b71e0675f..93d36406019 100644 --- a/homeassistant/components/watttime/translations/ja.json +++ b/homeassistant/components/watttime/translations/ja.json @@ -28,7 +28,7 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" }, "description": "{username} \u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u518d\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044:", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "user": { "data": { diff --git a/homeassistant/components/waze_travel_time/translations/ja.json b/homeassistant/components/waze_travel_time/translations/ja.json index 545c0cdb0d6..3463699509a 100644 --- a/homeassistant/components/waze_travel_time/translations/ja.json +++ b/homeassistant/components/waze_travel_time/translations/ja.json @@ -31,7 +31,7 @@ "units": "\u5358\u4f4d", "vehicle_type": "\u8eca\u4e21\u30bf\u30a4\u30d7" }, - "description": "`substring`\u30a4\u30f3\u30d7\u30c3\u30c8\u3092\u4f7f\u7528\u3059\u308b\u3068\u3001\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u7279\u5b9a\u306e\u30eb\u30fc\u30c8\u3092\u4f7f\u7528\u3059\u308b\u3088\u3046\u306b\u5f37\u5236\u3057\u305f\u308a\u3001\u9006\u306b\u7279\u5b9a\u306e\u30eb\u30fc\u30c8\u3092\u56de\u907f\u3057\u305f\u30bf\u30a4\u30e0\u30c8\u30e9\u30d9\u30eb\u306e\u8a08\u7b97\u3092\u884c\u3046\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002" + "description": "`substring`\u30a4\u30f3\u30d7\u30c3\u30c8\u3092\u4f7f\u7528\u3059\u308b\u3068\u3001\u7d71\u5408\u3067\u7279\u5b9a\u306e\u30eb\u30fc\u30c8\u3092\u4f7f\u7528\u3059\u308b\u3088\u3046\u306b\u5f37\u5236\u3057\u305f\u308a\u3001\u9006\u306b\u7279\u5b9a\u306e\u30eb\u30fc\u30c8\u3092\u56de\u907f\u3057\u305f\u30bf\u30a4\u30e0\u30c8\u30e9\u30d9\u30eb\u306e\u8a08\u7b97\u3092\u884c\u3046\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002" } } }, diff --git a/homeassistant/components/whois/translations/ja.json b/homeassistant/components/whois/translations/ja.json index 66a7868a265..132ebf9e070 100644 --- a/homeassistant/components/whois/translations/ja.json +++ b/homeassistant/components/whois/translations/ja.json @@ -6,7 +6,7 @@ "error": { "unexpected_response": "Whois\u30b5\u30fc\u30d0\u30fc\u304b\u3089\u306e\u4e88\u671f\u3057\u306a\u3044\u5fdc\u7b54", "unknown_date_format": "Whois\u30b5\u30fc\u30d0\u30fc\u306e\u5fdc\u7b54\u3067\u4e0d\u660e\u306a\u65e5\u4ed8\u30d5\u30a9\u30fc\u30de\u30c3\u30c8", - "unknown_tld": "\u6307\u5b9a\u3055\u308c\u305fTLD\u306f\u4e0d\u660e\u3001\u3082\u3057\u304f\u306f\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u5229\u7528\u3067\u304d\u307e\u305b\u3093", + "unknown_tld": "\u6307\u5b9a\u3055\u308c\u305fTLD\u306f\u4e0d\u660e\u3001\u3082\u3057\u304f\u306f\u3053\u306e\u7d71\u5408\u3067\u306f\u5229\u7528\u3067\u304d\u307e\u305b\u3093", "whois_command_failed": "Whois\u30b3\u30de\u30f3\u30c9\u304c\u5931\u6557\u3057\u307e\u3057\u305f: whois\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f" }, "step": { diff --git a/homeassistant/components/withings/translations/ja.json b/homeassistant/components/withings/translations/ja.json index bc70bf0c746..20f097e973c 100644 --- a/homeassistant/components/withings/translations/ja.json +++ b/homeassistant/components/withings/translations/ja.json @@ -26,7 +26,7 @@ }, "reauth": { "description": "Withings data\u306e\u53d7\u4fe1\u3092\u7d99\u7d9a\u3059\u308b\u306b\u306f\u3001\"{profile}\" \u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/wiz/translations/ja.json b/homeassistant/components/wiz/translations/ja.json index e062fbdb75b..b8e1536654b 100644 --- a/homeassistant/components/wiz/translations/ja.json +++ b/homeassistant/components/wiz/translations/ja.json @@ -9,7 +9,7 @@ "bulb_time_out": "\u96fb\u7403\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002\u96fb\u7403\u304c\u30aa\u30d5\u30e9\u30a4\u30f3\u306b\u306a\u3063\u3066\u3044\u308b\u304b\u3001\u9593\u9055\u3063\u305fIP/\u30db\u30b9\u30c8\u304c\u5165\u529b\u3055\u308c\u3066\u3044\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002\u96fb\u7403\u306e\u96fb\u6e90\u3092\u5165\u308c\u3066\u518d\u5ea6\u304a\u8a66\u3057\u304f\u3060\u3055\u3044\u3002", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "no_ip": "\u6709\u52b9\u306aIP\u30a2\u30c9\u30ec\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002", - "no_wiz_light": "\u3053\u306e\u96fb\u7403\u306f\u3001WiZ Platform\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u4ecb\u3057\u3066\u63a5\u7d9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002", + "no_wiz_light": "\u3053\u306e\u96fb\u7403\u306f\u3001WiZ Platform\u7d71\u5408\u3092\u4ecb\u3057\u3066\u63a5\u7d9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/wled/translations/ja.json b/homeassistant/components/wled/translations/ja.json index 5f30617d7eb..af3654ea2b0 100644 --- a/homeassistant/components/wled/translations/ja.json +++ b/homeassistant/components/wled/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "cct_unsupported": "\u3053\u306eWLED\u30c7\u30d0\u30a4\u30b9\u306fCCT\u30c1\u30e3\u30f3\u30cd\u30eb\u3092\u4f7f\u7528\u3057\u3066\u3044\u307e\u3059\u304c\u3001\u3053\u306e\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + "cct_unsupported": "\u3053\u306eWLED\u30c7\u30d0\u30a4\u30b9\u306fCCT\u30c1\u30e3\u30f3\u30cd\u30eb\u3092\u4f7f\u7528\u3057\u3066\u3044\u307e\u3059\u304c\u3001\u3053\u306e\u7d71\u5408\u3067\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" diff --git a/homeassistant/components/xiaomi_miio/translations/ja.json b/homeassistant/components/xiaomi_miio/translations/ja.json index 0877850f754..672a338f082 100644 --- a/homeassistant/components/xiaomi_miio/translations/ja.json +++ b/homeassistant/components/xiaomi_miio/translations/ja.json @@ -36,11 +36,11 @@ "host": "IP\u30a2\u30c9\u30ec\u30b9", "token": "API\u30c8\u30fc\u30af\u30f3" }, - "description": "32\u6587\u5b57\u306eAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u306b\u306a\u308a\u307e\u3059\u3002\u624b\u9806\u306f\u3001https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6ce8\u610f\u4e8b\u9805: \u3053\u306eAPI\u30c8\u30fc\u30af\u30f3\u306f\u3001Xiaomi Aqara\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u30ad\u30fc\u3068\u306f\u7570\u306a\u308a\u307e\u3059\u3002" + "description": "32\u6587\u5b57\u306eAPI\u30c8\u30fc\u30af\u30f3\u304c\u5fc5\u8981\u306b\u306a\u308a\u307e\u3059\u3002\u624b\u9806\u306f\u3001https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token \u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6ce8\u610f\u4e8b\u9805: \u3053\u306eAPI\u30c8\u30fc\u30af\u30f3\u306f\u3001Xiaomi Aqara\u7d71\u5408\u3067\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u30ad\u30fc\u3068\u306f\u7570\u306a\u308a\u307e\u3059\u3002" }, "reauth_confirm": { - "description": "Xiaomi Miio\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u66f4\u65b0\u3057\u305f\u308a\u3001\u4e0d\u8db3\u3057\u3066\u3044\u308b\u30af\u30e9\u30a6\u30c9\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u8ffd\u52a0\u3059\u308b\u305f\u3081\u306b\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Xiaomi Miio\u7d71\u5408\u3067\u306f\u3001\u30c8\u30fc\u30af\u30f3\u3092\u66f4\u65b0\u3057\u305f\u308a\u3001\u4e0d\u8db3\u3057\u3066\u3044\u308b\u30af\u30e9\u30a6\u30c9\u306e\u8a8d\u8a3c\u60c5\u5831\u3092\u8ffd\u52a0\u3059\u308b\u305f\u3081\u306b\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" }, "select": { "data": { diff --git a/homeassistant/components/yolink/translations/ja.json b/homeassistant/components/yolink/translations/ja.json index 7d2545803bf..274296a1240 100644 --- a/homeassistant/components/yolink/translations/ja.json +++ b/homeassistant/components/yolink/translations/ja.json @@ -17,8 +17,8 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" }, "reauth_confirm": { - "description": "Yolink\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", - "title": "\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u306e\u518d\u8a8d\u8a3c" + "description": "Yolink\u7d71\u5408\u3067\u306f\u3001\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index f4c5a62b050..94645e302f4 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -36,7 +36,7 @@ "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u8a2d\u5b9a\u3092\u5165\u529b" }, "hassio_confirm": { - "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u3068Z-Wave JS\u30a4\u30f3\u30c6\u30b0\u30ec\u30fc\u30b7\u30e7\u30f3\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u3068Z-Wave JS\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" }, "install_addon": { "title": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u304c\u958b\u59cb\u3055\u308c\u307e\u3057\u305f\u3002" From 0e3f7bc63aecc71685536f2121d3fecbc24bd642 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 8 Jul 2022 05:49:07 +0200 Subject: [PATCH 2262/3516] Resolution center MVP (#74243) Co-authored-by: Paulus Schoutsen --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/resolution_center/__init__.py | 20 + .../components/resolution_center/const.py | 3 + .../resolution_center/issue_handler.py | 62 ++++ .../resolution_center/issue_registry.py | 164 +++++++++ .../resolution_center/manifest.json | 7 + .../components/resolution_center/models.py | 12 + .../resolution_center/websocket_api.py | 62 ++++ mypy.ini | 11 + script/hassfest/manifest.py | 1 + .../components/resolution_center/__init__.py | 1 + .../components/resolution_center/test_init.py | 346 ++++++++++++++++++ .../resolution_center/test_issue_registry.py | 92 +++++ .../resolution_center/test_websocket_api.py | 151 ++++++++ 15 files changed, 935 insertions(+) create mode 100644 homeassistant/components/resolution_center/__init__.py create mode 100644 homeassistant/components/resolution_center/const.py create mode 100644 homeassistant/components/resolution_center/issue_handler.py create mode 100644 homeassistant/components/resolution_center/issue_registry.py create mode 100644 homeassistant/components/resolution_center/manifest.json create mode 100644 homeassistant/components/resolution_center/models.py create mode 100644 homeassistant/components/resolution_center/websocket_api.py create mode 100644 tests/components/resolution_center/__init__.py create mode 100644 tests/components/resolution_center/test_init.py create mode 100644 tests/components/resolution_center/test_issue_registry.py create mode 100644 tests/components/resolution_center/test_websocket_api.py diff --git a/.strict-typing b/.strict-typing index ebfed5dfa5b..c5ed000a7ce 100644 --- a/.strict-typing +++ b/.strict-typing @@ -191,6 +191,7 @@ homeassistant.components.recollect_waste.* homeassistant.components.recorder.* homeassistant.components.remote.* homeassistant.components.renault.* +homeassistant.components.resolution_center.* homeassistant.components.ridwell.* homeassistant.components.rituals_perfume_genie.* homeassistant.components.roku.* diff --git a/CODEOWNERS b/CODEOWNERS index 9f53aeab34e..fda94805214 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -857,6 +857,8 @@ build.json @home-assistant/supervisor /homeassistant/components/renault/ @epenet /tests/components/renault/ @epenet /homeassistant/components/repetier/ @MTrab @ShadowBr0ther +/homeassistant/components/resolution_center/ @home-assistant/core +/tests/components/resolution_center/ @home-assistant/core /homeassistant/components/rflink/ @javicalle /tests/components/rflink/ @javicalle /homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221 diff --git a/homeassistant/components/resolution_center/__init__.py b/homeassistant/components/resolution_center/__init__.py new file mode 100644 index 00000000000..1446aa68bba --- /dev/null +++ b/homeassistant/components/resolution_center/__init__.py @@ -0,0 +1,20 @@ +"""The resolution center integration.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + +from . import websocket_api +from .const import DOMAIN +from .issue_handler import async_create_issue, async_delete_issue +from .issue_registry import async_load as async_load_issue_registry + +__all__ = ["DOMAIN", "async_create_issue", "async_delete_issue"] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Resolution Center.""" + websocket_api.async_setup(hass) + await async_load_issue_registry(hass) + + return True diff --git a/homeassistant/components/resolution_center/const.py b/homeassistant/components/resolution_center/const.py new file mode 100644 index 00000000000..46d020e5118 --- /dev/null +++ b/homeassistant/components/resolution_center/const.py @@ -0,0 +1,3 @@ +"""Constants for the Resolution Center integration.""" + +DOMAIN = "resolution_center" diff --git a/homeassistant/components/resolution_center/issue_handler.py b/homeassistant/components/resolution_center/issue_handler.py new file mode 100644 index 00000000000..245895fa2db --- /dev/null +++ b/homeassistant/components/resolution_center/issue_handler.py @@ -0,0 +1,62 @@ +"""The resolution center integration.""" +from __future__ import annotations + +from awesomeversion import AwesomeVersion, AwesomeVersionStrategy + +from homeassistant.core import HomeAssistant, callback + +from .issue_registry import async_get as async_get_issue_registry +from .models import IssueSeverity + + +@callback +def async_create_issue( + hass: HomeAssistant, + domain: str, + issue_id: str, + *, + breaks_in_ha_version: str | None = None, + learn_more_url: str | None = None, + severity: IssueSeverity, + translation_key: str, + translation_placeholders: dict[str, str] | None = None, +) -> None: + """Create an issue, or replace an existing one.""" + # Verify the breaks_in_ha_version is a valid version string + if breaks_in_ha_version: + AwesomeVersion( + breaks_in_ha_version, + ensure_strategy=AwesomeVersionStrategy.CALVER, + find_first_match=False, + ) + + issue_registry = async_get_issue_registry(hass) + issue_registry.async_get_or_create( + domain, + issue_id, + breaks_in_ha_version=breaks_in_ha_version, + learn_more_url=learn_more_url, + severity=severity, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + + +@callback +def async_delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: + """Delete an issue. + + It is not an error to delete an issue that does not exist. + """ + issue_registry = async_get_issue_registry(hass) + issue_registry.async_delete(domain, issue_id) + + +@callback +def async_dismiss_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: + """Dismiss an issue. + + Will raise if the issue does not exist. + """ + issue_registry = async_get_issue_registry(hass) + issue_registry.async_dismiss(domain, issue_id) diff --git a/homeassistant/components/resolution_center/issue_registry.py b/homeassistant/components/resolution_center/issue_registry.py new file mode 100644 index 00000000000..d97ad73bbac --- /dev/null +++ b/homeassistant/components/resolution_center/issue_registry.py @@ -0,0 +1,164 @@ +"""Persistently store issues raised by integrations.""" +from __future__ import annotations + +import dataclasses +from typing import cast + +from homeassistant.const import __version__ as ha_version +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.storage import Store + +from .models import IssueSeverity + +DATA_REGISTRY = "issue_registry" +STORAGE_KEY = "resolution_center.issue_registry" +STORAGE_VERSION = 1 +SAVE_DELAY = 10 +SAVED_FIELDS = ("dismissed_version", "domain", "issue_id") + + +@dataclasses.dataclass(frozen=True) +class IssueEntry: + """Issue Registry Entry.""" + + active: bool + breaks_in_ha_version: str | None + dismissed_version: str | None + domain: str + issue_id: str + learn_more_url: str | None + severity: IssueSeverity | None + translation_key: str | None + translation_placeholders: dict[str, str] | None + + +class IssueRegistry: + """Class to hold a registry of issues.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the issue registry.""" + self.hass = hass + self.issues: dict[tuple[str, str], IssueEntry] = {} + self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True) + + @callback + def async_get_issue(self, domain: str, issue_id: str) -> IssueEntry | None: + """Get issue by id.""" + return self.issues.get((domain, issue_id)) + + @callback + def async_get_or_create( + self, + domain: str, + issue_id: str, + *, + breaks_in_ha_version: str | None = None, + learn_more_url: str | None = None, + severity: IssueSeverity, + translation_key: str, + translation_placeholders: dict[str, str] | None = None, + ) -> IssueEntry: + """Get issue. Create if it doesn't exist.""" + + if (issue := self.async_get_issue(domain, issue_id)) is None: + issue = IssueEntry( + active=True, + breaks_in_ha_version=breaks_in_ha_version, + dismissed_version=None, + domain=domain, + issue_id=issue_id, + learn_more_url=learn_more_url, + severity=severity, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + self.issues[(domain, issue_id)] = issue + self.async_schedule_save() + else: + issue = self.issues[(domain, issue_id)] = dataclasses.replace( + issue, + active=True, + breaks_in_ha_version=breaks_in_ha_version, + learn_more_url=learn_more_url, + severity=severity, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + + return issue + + @callback + def async_delete(self, domain: str, issue_id: str) -> None: + """Delete issue.""" + if self.issues.pop((domain, issue_id), None) is None: + return + + self.async_schedule_save() + + @callback + def async_dismiss(self, domain: str, issue_id: str) -> IssueEntry: + """Dismiss issue.""" + old = self.issues[(domain, issue_id)] + if old.dismissed_version == ha_version: + return old + + issue = self.issues[(domain, issue_id)] = dataclasses.replace( + old, + dismissed_version=ha_version, + ) + + self.async_schedule_save() + + return issue + + async def async_load(self) -> None: + """Load the issue registry.""" + data = await self._store.async_load() + + issues: dict[tuple[str, str], IssueEntry] = {} + + if isinstance(data, dict): + for issue in data["issues"]: + issues[(issue["domain"], issue["issue_id"])] = IssueEntry( + active=False, + breaks_in_ha_version=None, + dismissed_version=issue["dismissed_version"], + domain=issue["domain"], + issue_id=issue["issue_id"], + learn_more_url=None, + severity=None, + translation_key=None, + translation_placeholders=None, + ) + + self.issues = issues + + @callback + def async_schedule_save(self) -> None: + """Schedule saving the issue registry.""" + self._store.async_delay_save(self._data_to_save, SAVE_DELAY) + + @callback + def _data_to_save(self) -> dict[str, list[dict[str, str | None]]]: + """Return data of issue registry to store in a file.""" + data = {} + + data["issues"] = [ + {field: getattr(entry, field) for field in SAVED_FIELDS} + for entry in self.issues.values() + ] + + return data + + +@callback +def async_get(hass: HomeAssistant) -> IssueRegistry: + """Get issue registry.""" + return cast(IssueRegistry, hass.data[DATA_REGISTRY]) + + +async def async_load(hass: HomeAssistant) -> None: + """Load issue registry.""" + assert DATA_REGISTRY not in hass.data + hass.data[DATA_REGISTRY] = IssueRegistry(hass) + await hass.data[DATA_REGISTRY].async_load() diff --git a/homeassistant/components/resolution_center/manifest.json b/homeassistant/components/resolution_center/manifest.json new file mode 100644 index 00000000000..87cd309ad3d --- /dev/null +++ b/homeassistant/components/resolution_center/manifest.json @@ -0,0 +1,7 @@ +{ + "domain": "resolution_center", + "name": "Resolution Center", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/resolution_center", + "codeowners": ["@home-assistant/core"] +} diff --git a/homeassistant/components/resolution_center/models.py b/homeassistant/components/resolution_center/models.py new file mode 100644 index 00000000000..eabfd98cef3 --- /dev/null +++ b/homeassistant/components/resolution_center/models.py @@ -0,0 +1,12 @@ +"""Models for Resolution Center.""" +from __future__ import annotations + +from homeassistant.backports.enum import StrEnum + + +class IssueSeverity(StrEnum): + """Issue severity.""" + + CRITICAL = "critical" + ERROR = "error" + WARNING = "warning" diff --git a/homeassistant/components/resolution_center/websocket_api.py b/homeassistant/components/resolution_center/websocket_api.py new file mode 100644 index 00000000000..14793f0bd2d --- /dev/null +++ b/homeassistant/components/resolution_center/websocket_api.py @@ -0,0 +1,62 @@ +"""The resolution center websocket API.""" +from __future__ import annotations + +import dataclasses +from typing import Any + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant, callback + +from .issue_handler import async_dismiss_issue +from .issue_registry import async_get as async_get_issue_registry + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Set up the resolution center websocket API.""" + websocket_api.async_register_command(hass, ws_dismiss_issue) + websocket_api.async_register_command(hass, ws_list_issues) + + +@callback +@websocket_api.websocket_command( + { + vol.Required("type"): "resolution_center/dismiss_issue", + vol.Required("domain"): str, + vol.Required("issue_id"): str, + } +) +def ws_dismiss_issue( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Fix an issue.""" + async_dismiss_issue(hass, msg["domain"], msg["issue_id"]) + + connection.send_result(msg["id"]) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "resolution_center/list_issues", + } +) +@callback +def ws_list_issues( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Return a list of issues.""" + + def ws_dict(kv_pairs: list[tuple[Any, Any]]) -> dict[Any, Any]: + result = {k: v for k, v in kv_pairs if k != "active"} + result["dismissed"] = result["dismissed_version"] is not None + return result + + issue_registry = async_get_issue_registry(hass) + issues = [ + dataclasses.asdict(issue, dict_factory=ws_dict) + for issue in issue_registry.issues.values() + ] + + connection.send_result(msg["id"], {"issues": issues}) diff --git a/mypy.ini b/mypy.ini index 87dd265eeef..c47413b4af8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1864,6 +1864,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.resolution_center.*] +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.ridwell.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 0cd20364533..b847d384361 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -80,6 +80,7 @@ NO_IOT_CLASS = [ "proxy", "python_script", "raspberry_pi", + "resolution_center", "safe_mode", "script", "search", diff --git a/tests/components/resolution_center/__init__.py b/tests/components/resolution_center/__init__.py new file mode 100644 index 00000000000..a6a86bf99bc --- /dev/null +++ b/tests/components/resolution_center/__init__.py @@ -0,0 +1 @@ +"""Tests for the resolution center integration.""" diff --git a/tests/components/resolution_center/test_init.py b/tests/components/resolution_center/test_init.py new file mode 100644 index 00000000000..869c5d6c485 --- /dev/null +++ b/tests/components/resolution_center/test_init.py @@ -0,0 +1,346 @@ +"""Test the resolution center websocket API.""" +import pytest + +from homeassistant.components.resolution_center import ( + async_create_issue, + async_delete_issue, +) +from homeassistant.components.resolution_center.const import DOMAIN +from homeassistant.components.resolution_center.issue_handler import async_dismiss_issue +from homeassistant.const import __version__ as ha_version +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: + """Test creating and updating issues.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + issues = [ + { + "breaks_in_ha_version": "2022.9.0dev0", + "domain": "test", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + { + "breaks_in_ha_version": "2022.8", + "domain": "test", + "issue_id": "issue_2", + "learn_more_url": "https://theuselessweb.com/abc", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"def": "456"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 2, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + # Update an issue + async_create_issue( + hass, + issues[0]["domain"], + issues[0]["issue_id"], + breaks_in_ha_version=issues[0]["breaks_in_ha_version"], + learn_more_url="blablabla", + severity=issues[0]["severity"], + translation_key=issues[0]["translation_key"], + translation_placeholders=issues[0]["translation_placeholders"], + ) + + await client.send_json({"id": 3, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"]["issues"][0] == dict( + issues[0], + dismissed=False, + dismissed_version=None, + learn_more_url="blablabla", + ) + + +@pytest.mark.parametrize("ha_version", ("2022.9.cat", "In the future: 2023.1.1")) +async def test_create_issue_invalid_version( + hass: HomeAssistant, hass_ws_client, ha_version +) -> None: + """Test creating an issue with invalid breaks in version.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + issue = { + "breaks_in_ha_version": ha_version, + "domain": "test", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + } + + with pytest.raises(Exception): + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + +async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: + """Test dismissing issues.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + issues = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "test", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 2, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + # Dismiss a non-existing issue + with pytest.raises(KeyError): + async_dismiss_issue(hass, issues[0]["domain"], "no_such_issue") + + await client.send_json({"id": 3, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + # Dismiss an existing issue + async_dismiss_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + + await client.send_json({"id": 4, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=True, + dismissed_version=ha_version, + ) + for issue in issues + ] + } + + # Dismiss the same issue again + async_dismiss_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + + await client.send_json({"id": 5, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=True, + dismissed_version=ha_version, + ) + for issue in issues + ] + } + + # Update a dismissed issue + async_create_issue( + hass, + issues[0]["domain"], + issues[0]["issue_id"], + breaks_in_ha_version=issues[0]["breaks_in_ha_version"], + learn_more_url="blablabla", + severity=issues[0]["severity"], + translation_key=issues[0]["translation_key"], + translation_placeholders=issues[0]["translation_placeholders"], + ) + + await client.send_json({"id": 6, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"]["issues"][0] == dict( + issues[0], + dismissed=True, + dismissed_version=ha_version, + learn_more_url="blablabla", + ) + + +async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can delete an issue.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + issues = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "fake_integration", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + # Delete a non-existing issue + async_delete_issue(hass, issues[0]["domain"], "no_such_issue") + + await client.send_json({"id": 2, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + # Delete an existing issue + async_delete_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + + await client.send_json({"id": 3, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + # Delete the same issue again + async_delete_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + + await client.send_json({"id": 4, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} diff --git a/tests/components/resolution_center/test_issue_registry.py b/tests/components/resolution_center/test_issue_registry.py new file mode 100644 index 00000000000..c236e96adb2 --- /dev/null +++ b/tests/components/resolution_center/test_issue_registry.py @@ -0,0 +1,92 @@ +"""Test the resolution center websocket API.""" +from homeassistant.components.resolution_center import ( + async_create_issue, + issue_registry, +) +from homeassistant.components.resolution_center.const import DOMAIN +from homeassistant.components.resolution_center.issue_handler import async_dismiss_issue +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import flush_store + + +async def test_load_issues(hass: HomeAssistant) -> None: + """Make sure that we can load/save data correctly.""" + assert await async_setup_component(hass, DOMAIN, {}) + + issues = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "test", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + { + "breaks_in_ha_version": "2022.8", + "domain": "test", + "issue_id": "issue_2", + "learn_more_url": "https://theuselessweb.com/abc", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"def": "456"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + async_dismiss_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + + registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] + assert len(registry.issues) == 2 + issue1 = registry.async_get_issue("test", "issue_1") + issue2 = registry.async_get_issue("test", "issue_2") + + registry2 = issue_registry.IssueRegistry(hass) + await flush_store(registry._store) + await registry2.async_load() + + assert list(registry.issues) == list(registry2.issues) + + issue1_registry2 = registry2.async_get_issue("test", "issue_1") + assert issue1_registry2.dismissed_version == issue1.dismissed_version + issue2_registry2 = registry2.async_get_issue("test", "issue_2") + assert issue2_registry2.dismissed_version == issue2.dismissed_version + + +async def test_loading_issues_from_storage(hass: HomeAssistant, hass_storage) -> None: + """Test loading stored issues on start.""" + hass_storage[issue_registry.STORAGE_KEY] = { + "version": issue_registry.STORAGE_VERSION, + "data": { + "issues": [ + { + "dismissed_version": "2022.7.0.dev0", + "domain": "test", + "issue_id": "issue_1", + }, + { + "dismissed_version": None, + "domain": "test", + "issue_id": "issue_2", + }, + ] + }, + } + + assert await async_setup_component(hass, DOMAIN, {}) + + registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] + assert len(registry.issues) == 2 diff --git a/tests/components/resolution_center/test_websocket_api.py b/tests/components/resolution_center/test_websocket_api.py new file mode 100644 index 00000000000..9258a06f904 --- /dev/null +++ b/tests/components/resolution_center/test_websocket_api.py @@ -0,0 +1,151 @@ +"""Test the resolution center websocket API.""" +from homeassistant.components.resolution_center import async_create_issue +from homeassistant.components.resolution_center.const import DOMAIN +from homeassistant.const import __version__ as ha_version +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can dismiss an issue.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + issues = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "test", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + await client.send_json( + { + "id": 2, + "type": "resolution_center/dismiss_issue", + "domain": "test", + "issue_id": "no_such_issue", + } + ) + msg = await client.receive_json() + assert not msg["success"] + + await client.send_json( + { + "id": 3, + "type": "resolution_center/dismiss_issue", + "domain": "test", + "issue_id": "issue_1", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + + await client.send_json({"id": 4, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=True, + dismissed_version=ha_version, + ) + for issue in issues + ] + } + + +async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can list issues.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + issues = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "test", + "issue_id": "issue_1", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + }, + { + "breaks_in_ha_version": "2022.8", + "domain": "test", + "issue_id": "issue_2", + "learn_more_url": "https://theuselessweb.com/abc", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"def": "456"}, + }, + ] + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 2, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } From 561c9a77d87e15e81367556996d54f7b0332f704 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 7 Jul 2022 20:50:19 -0700 Subject: [PATCH 2263/3516] Add google calendar service proper exception handling (#74686) --- homeassistant/components/google/__init__.py | 27 ++++++++++++------- homeassistant/components/google/calendar.py | 23 +++++++++------- tests/components/google/conftest.py | 5 +++- tests/components/google/test_init.py | 29 +++++++++++++++++++++ 4 files changed, 63 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 5553350aa23..261c61d1a88 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -30,7 +30,11 @@ from homeassistant.const import ( CONF_OFFSET, ) from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -348,15 +352,18 @@ async def async_setup_add_event_service( "Missing required fields to set start or end date/datetime" ) - await calendar_service.async_create_event( - call.data[EVENT_CALENDAR_ID], - Event( - summary=call.data[EVENT_SUMMARY], - description=call.data[EVENT_DESCRIPTION], - start=start, - end=end, - ), - ) + try: + await calendar_service.async_create_event( + call.data[EVENT_CALENDAR_ID], + Event( + summary=call.data[EVENT_SUMMARY], + description=call.data[EVENT_DESCRIPTION], + start=start, + end=end, + ), + ) + except ApiException as err: + raise HomeAssistantError(str(err)) from err hass.services.async_register( DOMAIN, SERVICE_ADD_EVENT, _add_event, schema=ADD_EVENT_SERVICE_SCHEMA diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 3c271a2c3c3..a86cdf55e3a 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -22,7 +22,7 @@ from homeassistant.components.calendar import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import PlatformNotReady +from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers import ( config_validation as cv, entity_platform, @@ -362,12 +362,15 @@ async def async_create_event(entity: GoogleCalendarEntity, call: ServiceCall) -> if start is None or end is None: raise ValueError("Missing required fields to set start or end date/datetime") - await entity.calendar_service.async_create_event( - entity.calendar_id, - Event( - summary=call.data[EVENT_SUMMARY], - description=call.data[EVENT_DESCRIPTION], - start=start, - end=end, - ), - ) + try: + await entity.calendar_service.async_create_event( + entity.calendar_id, + Event( + summary=call.data[EVENT_SUMMARY], + description=call.data[EVENT_DESCRIPTION], + start=start, + end=end, + ), + ) + except ApiException as err: + raise HomeAssistantError(str(err)) from err diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 4e251b4b006..a871722c2e9 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -306,9 +306,12 @@ def mock_insert_event( ) -> Callable[[...], None]: """Fixture for capturing event creation.""" - def _expect_result(calendar_id: str = CALENDAR_ID) -> None: + def _expect_result( + calendar_id: str = CALENDAR_ID, exc: ClientError | None = None + ) -> None: aioclient_mock.post( f"{API_BASE_URL}/calendars/{calendar_id}/events", + exc=exc, ) return diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index d9b9ec8ed03..a40b499f769 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -8,6 +8,7 @@ import time from typing import Any from unittest.mock import Mock, patch +from aiohttp.client_exceptions import ClientError import pytest import voluptuous as vol @@ -21,6 +22,7 @@ from homeassistant.components.google.const import CONF_CALENDAR_ACCESS from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF from homeassistant.core import HomeAssistant, State +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow @@ -676,6 +678,33 @@ async def test_add_event_date_time( } +async def test_add_event_failure( + hass: HomeAssistant, + component_setup: ComponentSetup, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + mock_events_list: ApiResult, + mock_insert_event: Callable[[..., dict[str, Any]], None], + setup_config_entry: MockConfigEntry, + add_event_call_service: Callable[dict[str, Any], Awaitable[None]], +) -> None: + """Test service calls with incorrect fields.""" + + mock_calendars_list({"items": [test_api_calendar]}) + mock_events_list({}) + assert await component_setup() + + mock_insert_event( + calendar_id=CALENDAR_ID, + exc=ClientError(), + ) + + with pytest.raises(HomeAssistantError): + await add_event_call_service( + {"start_date": "2022-05-01", "end_date": "2022-05-01"} + ) + + @pytest.mark.parametrize( "config_entry_token_expiry", [datetime.datetime.max.timestamp() + 1] ) From 97426911a3bac20128f3558a3644b565b7024943 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 8 Jul 2022 05:50:31 +0200 Subject: [PATCH 2264/3516] Update lxml to 4.9.1 (#74663) --- homeassistant/components/scrape/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index b1ccbb354a9..4f8ea3d1481 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -2,7 +2,7 @@ "domain": "scrape", "name": "Scrape", "documentation": "https://www.home-assistant.io/integrations/scrape", - "requirements": ["beautifulsoup4==4.11.1", "lxml==4.8.0"], + "requirements": ["beautifulsoup4==4.11.1", "lxml==4.9.1"], "after_dependencies": ["rest"], "codeowners": ["@fabaff"], "iot_class": "cloud_polling" diff --git a/requirements_all.txt b/requirements_all.txt index 9f9423496fd..6cc0cce2cbf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -984,7 +984,7 @@ lupupy==0.0.24 lw12==0.9.2 # homeassistant.components.scrape -lxml==4.8.0 +lxml==4.9.1 # homeassistant.components.nmap_tracker mac-vendor-lookup==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 870f830ac31..32a642581d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -685,7 +685,7 @@ logi_circle==0.2.3 luftdaten==0.7.2 # homeassistant.components.scrape -lxml==4.8.0 +lxml==4.9.1 # homeassistant.components.nmap_tracker mac-vendor-lookup==0.1.11 From bf5633fa433041ff4ba1dfdf02e8bb6890c1e028 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 5 Jul 2022 18:54:03 +0200 Subject: [PATCH 2265/3516] Bump deCONZ dependency to v96 (#74460) --- homeassistant/components/deconz/light.py | 6 +----- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 669800e2662..7f3a47d719d 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -113,11 +113,7 @@ async def async_setup_entry( first = True for light_id in group.lights: - if ( - (light := gateway.api.lights.lights.get(light_id)) - and light.ZHATYPE == Light.ZHATYPE - and light.reachable - ): + if (light := gateway.api.lights.lights.get(light_id)) and light.reachable: group.update_color_state(light, update_all_attributes=first) first = False diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 09dcc190a4f..c19d75ec054 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==95"], + "requirements": ["pydeconz==96"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 99be85cc418..c561730dda7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1444,7 +1444,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==95 +pydeconz==96 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b6a493d37f9..7ae59c713b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -974,7 +974,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==95 +pydeconz==96 # homeassistant.components.dexcom pydexcom==0.2.3 From fa220c5c25341de0c110c51489a91ce8e84a8871 Mon Sep 17 00:00:00 2001 From: c-soft Date: Thu, 7 Jul 2022 02:02:08 +0200 Subject: [PATCH 2266/3516] Bump satel_integra to 0.3.7 to fix compat with python 3.10 (#74543) --- homeassistant/components/satel_integra/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/satel_integra/manifest.json b/homeassistant/components/satel_integra/manifest.json index 6c4a391698b..262507be6bb 100644 --- a/homeassistant/components/satel_integra/manifest.json +++ b/homeassistant/components/satel_integra/manifest.json @@ -2,7 +2,7 @@ "domain": "satel_integra", "name": "Satel Integra", "documentation": "https://www.home-assistant.io/integrations/satel_integra", - "requirements": ["satel_integra==0.3.4"], + "requirements": ["satel_integra==0.3.7"], "codeowners": [], "iot_class": "local_push", "loggers": ["satel_integra"] diff --git a/requirements_all.txt b/requirements_all.txt index c561730dda7..f5d2fec04a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2125,7 +2125,7 @@ samsungctl[websocket]==0.7.1 samsungtvws[async,encrypted]==2.5.0 # homeassistant.components.satel_integra -satel_integra==0.3.4 +satel_integra==0.3.7 # homeassistant.components.dhcp scapy==2.4.5 From 414ea6fd8cbd06b6b2a2d18edaa8976780a3952c Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 7 Jul 2022 08:40:10 +0200 Subject: [PATCH 2267/3516] fjaraskupan: Make sure we stop bleak on home assistant stop (#74545) * Make sure we stop bleak on home assistant stop * Fix typing Co-authored-by: Martin Hjelmare --- homeassistant/components/fjaraskupan/__init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 4c4f19403a6..85e95db5513 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -12,8 +12,8 @@ from bleak.backends.scanner import AdvertisementData from fjaraskupan import Device, State, device_filter from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -131,6 +131,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: scanner.register_detection_callback(detection_callback) await scanner.start() + async def on_hass_stop(event: Event) -> None: + await scanner.stop() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + ) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True From f6a23492fa9c276af15def2bb0ef35549c7c7c90 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 7 Jul 2022 02:43:10 -0500 Subject: [PATCH 2268/3516] Minimize Sonos `media_player.unjoin` timeout (#74549) --- homeassistant/components/sonos/media_player.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index c68110d9763..470386c341d 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -69,6 +69,7 @@ from .speaker import SonosMedia, SonosSpeaker _LOGGER = logging.getLogger(__name__) LONG_SERVICE_TIMEOUT = 30.0 +UNJOIN_SERVICE_TIMEOUT = 0.1 VOLUME_INCREMENT = 2 REPEAT_TO_SONOS = { @@ -775,7 +776,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): async def async_unjoin_player(self): """Remove this player from any group. - Coalesces all calls within 0.5s to allow use of SonosSpeaker.unjoin_multi() + Coalesces all calls within UNJOIN_SERVICE_TIMEOUT to allow use of SonosSpeaker.unjoin_multi() which optimizes the order in which speakers are removed from their groups. Removing coordinators last better preserves playqueues on the speakers. """ @@ -785,6 +786,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): async def async_process_unjoin(now: datetime.datetime) -> None: """Process the unjoin with all remove requests within the coalescing period.""" unjoin_data = sonos_data.unjoin_data.pop(household_id) + _LOGGER.debug( + "Processing unjoins for %s", [x.zone_name for x in unjoin_data.speakers] + ) await SonosSpeaker.unjoin_multi(self.hass, unjoin_data.speakers) unjoin_data.event.set() @@ -794,6 +798,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): unjoin_data = sonos_data.unjoin_data[household_id] = UnjoinData( speakers=[self.speaker] ) - async_call_later(self.hass, 0.5, async_process_unjoin) + async_call_later(self.hass, UNJOIN_SERVICE_TIMEOUT, async_process_unjoin) + _LOGGER.debug("Requesting unjoin for %s", self.speaker.zone_name) await unjoin_data.event.wait() From 606a1b57e6b73430437170a816e4024db74da7e6 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 7 Jul 2022 03:39:37 -0400 Subject: [PATCH 2269/3516] Bump aioskybell to 22.7.0 (#74559) --- homeassistant/components/skybell/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index c0e66aa5462..bfef4bc3422 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -3,7 +3,7 @@ "name": "SkyBell", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/skybell", - "requirements": ["aioskybell==22.6.1"], + "requirements": ["aioskybell==22.7.0"], "dependencies": ["ffmpeg"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index f5d2fec04a1..df719620b2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -247,7 +247,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.skybell -aioskybell==22.6.1 +aioskybell==22.7.0 # homeassistant.components.slimproto aioslimproto==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ae59c713b3..8d25e383315 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -216,7 +216,7 @@ aiosenz==1.0.0 aioshelly==2.0.0 # homeassistant.components.skybell -aioskybell==22.6.1 +aioskybell==22.7.0 # homeassistant.components.slimproto aioslimproto==2.1.1 From c9a31aab153c0d274ae1c2f47e45f447069b1068 Mon Sep 17 00:00:00 2001 From: ufodone <35497351+ufodone@users.noreply.github.com> Date: Thu, 7 Jul 2022 00:33:32 -0700 Subject: [PATCH 2270/3516] Bump pyenvisalink version to 4.6 (#74561) --- homeassistant/components/envisalink/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json index 44a40991a37..8d0fc735b90 100644 --- a/homeassistant/components/envisalink/manifest.json +++ b/homeassistant/components/envisalink/manifest.json @@ -2,7 +2,7 @@ "domain": "envisalink", "name": "Envisalink", "documentation": "https://www.home-assistant.io/integrations/envisalink", - "requirements": ["pyenvisalink==4.5"], + "requirements": ["pyenvisalink==4.6"], "codeowners": ["@ufodone"], "iot_class": "local_push", "loggers": ["pyenvisalink"] diff --git a/requirements_all.txt b/requirements_all.txt index df719620b2a..3ec631efb67 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1477,7 +1477,7 @@ pyeight==0.3.0 pyemby==1.8 # homeassistant.components.envisalink -pyenvisalink==4.5 +pyenvisalink==4.6 # homeassistant.components.ephember pyephember==0.3.1 From 174837dddfc3c8700f441e95241233a60351092e Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 7 Jul 2022 13:07:05 -0400 Subject: [PATCH 2271/3516] ElkM1 bump lib to support Python 3.10 SSL (#74569) Co-authored-by: J. Nick Koston --- homeassistant/components/elkm1/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 13f8ba8401f..7043fdfcb9d 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -2,7 +2,7 @@ "domain": "elkm1", "name": "Elk-M1 Control", "documentation": "https://www.home-assistant.io/integrations/elkm1", - "requirements": ["elkm1-lib==2.0.0"], + "requirements": ["elkm1-lib==2.0.2"], "dhcp": [{ "registered_devices": true }, { "macaddress": "00409D*" }], "codeowners": ["@gwww", "@bdraco"], "dependencies": ["network"], diff --git a/requirements_all.txt b/requirements_all.txt index 3ec631efb67..8844b7e2464 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -586,7 +586,7 @@ elgato==3.0.0 eliqonline==1.2.2 # homeassistant.components.elkm1 -elkm1-lib==2.0.0 +elkm1-lib==2.0.2 # homeassistant.components.elmax elmax_api==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d25e383315..737550785d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -426,7 +426,7 @@ eagle100==0.1.1 elgato==3.0.0 # homeassistant.components.elkm1 -elkm1-lib==2.0.0 +elkm1-lib==2.0.2 # homeassistant.components.elmax elmax_api==0.0.2 From fdc1b6ea9e8bd7decb6b23b8ed1813a23509f7ec Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 7 Jul 2022 09:03:43 +0200 Subject: [PATCH 2272/3516] Fix openweathermap hourly forecast (#74578) --- homeassistant/components/openweathermap/weather.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index ea439a35586..9948ecc0428 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -143,7 +143,11 @@ class OpenWeatherMapWeather(WeatherEntity): """Return the forecast array.""" api_forecasts = self._weather_coordinator.data[ATTR_API_FORECAST] forecasts = [ - {ha_key: forecast[api_key] for api_key, ha_key in FORECAST_MAP.items()} + { + ha_key: forecast[api_key] + for api_key, ha_key in FORECAST_MAP.items() + if api_key in forecast + } for forecast in api_forecasts ] return cast(list[Forecast], forecasts) From 9514b0f1000079c4ea2181b14324d77c3455b4ea Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 12:27:25 +0200 Subject: [PATCH 2273/3516] Fix mix of aiohttp and requests in Bloomsky (#74598) --- homeassistant/components/bloomsky/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/bloomsky/__init__.py b/homeassistant/components/bloomsky/__init__.py index 8311f6cbd96..ed2ce1ebc70 100644 --- a/homeassistant/components/bloomsky/__init__.py +++ b/homeassistant/components/bloomsky/__init__.py @@ -3,7 +3,6 @@ from datetime import timedelta from http import HTTPStatus import logging -from aiohttp.hdrs import AUTHORIZATION import requests import voluptuous as vol @@ -67,7 +66,7 @@ class BloomSky: _LOGGER.debug("Fetching BloomSky update") response = requests.get( f"{self.API_URL}?{self._endpoint_argument}", - headers={AUTHORIZATION: self._api_key}, + headers={"Authorization": self._api_key}, timeout=10, ) if response.status_code == HTTPStatus.UNAUTHORIZED: From 5a7e506c3844774e7fbaaf8b6e60be404c75243f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 16:02:36 +0200 Subject: [PATCH 2274/3516] Update aiokafka to 0.7.2 (#74601) --- homeassistant/components/apache_kafka/__init__.py | 1 - homeassistant/components/apache_kafka/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/apache_kafka/__init__.py b/homeassistant/components/apache_kafka/__init__.py index 1b293bd2c04..38a70b450ab 100644 --- a/homeassistant/components/apache_kafka/__init__.py +++ b/homeassistant/components/apache_kafka/__init__.py @@ -102,7 +102,6 @@ class KafkaManager: self._hass = hass ssl_context = ssl_util.client_context() self._producer = AIOKafkaProducer( - loop=hass.loop, bootstrap_servers=f"{ip_address}:{port}", compression_type="gzip", security_protocol=security_protocol, diff --git a/homeassistant/components/apache_kafka/manifest.json b/homeassistant/components/apache_kafka/manifest.json index 3b290146a09..3fa1f70c57b 100644 --- a/homeassistant/components/apache_kafka/manifest.json +++ b/homeassistant/components/apache_kafka/manifest.json @@ -2,7 +2,7 @@ "domain": "apache_kafka", "name": "Apache Kafka", "documentation": "https://www.home-assistant.io/integrations/apache_kafka", - "requirements": ["aiokafka==0.6.0"], + "requirements": ["aiokafka==0.7.2"], "codeowners": ["@bachya"], "iot_class": "local_push", "loggers": ["aiokafka", "kafka_python"] diff --git a/requirements_all.txt b/requirements_all.txt index 8844b7e2464..ea5bd8cca1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -181,7 +181,7 @@ aiohue==4.4.2 aioimaplib==1.0.0 # homeassistant.components.apache_kafka -aiokafka==0.6.0 +aiokafka==0.7.2 # homeassistant.components.kef aiokef==0.2.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 737550785d9..3cbc10480bb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -162,7 +162,7 @@ aiohttp_cors==0.7.0 aiohue==4.4.2 # homeassistant.components.apache_kafka -aiokafka==0.6.0 +aiokafka==0.7.2 # homeassistant.components.lookin aiolookin==0.1.1 From ed6a65156c50a195bb0b7caceab3f7b227f0f116 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 7 Jul 2022 14:51:16 +0200 Subject: [PATCH 2275/3516] Poll cast groups when media player is added or reconnected (#74610) --- homeassistant/components/cast/helpers.py | 2 +- homeassistant/components/cast/media_player.py | 13 ++++ tests/components/cast/test_media_player.py | 75 +++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index d7419f69563..48f57c39bd5 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -37,7 +37,7 @@ class ChromecastInfo: @property def friendly_name(self) -> str: - """Return the UUID.""" + """Return the Friendly Name.""" return self.cast_info.friendly_name @property diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 958c53ae394..c8a6a82571e 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -441,6 +441,19 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): connection_status.status, ) self._attr_available = new_available + if new_available and not self._cast_info.is_audio_group: + # Poll current group status + for group_uuid in self.mz_mgr.get_multizone_memberships( + self._cast_info.uuid + ): + group_media_controller = self.mz_mgr.get_multizone_mediacontroller( + group_uuid + ) + if not group_media_controller: + continue + self.multizone_new_media_status( + group_uuid, group_media_controller.status + ) self.schedule_update_ha_state() def multizone_new_media_status(self, group_uuid, media_status): diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 00626cc8c16..ed575c0fa22 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -731,6 +731,20 @@ async def test_entity_availability(hass: HomeAssistant): state = hass.states.get(entity_id) assert state.state == "off" + connection_status = MagicMock() + connection_status.status = "LOST" + conn_status_cb(connection_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == "unavailable" + + connection_status = MagicMock() + connection_status.status = "CONNECTED" + conn_status_cb(connection_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == "off" + connection_status = MagicMock() connection_status.status = "DISCONNECTED" conn_status_cb(connection_status) @@ -738,6 +752,14 @@ async def test_entity_availability(hass: HomeAssistant): state = hass.states.get(entity_id) assert state.state == "unavailable" + # Can't reconnect after receiving DISCONNECTED + connection_status = MagicMock() + connection_status.status = "CONNECTED" + conn_status_cb(connection_status) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state.state == "unavailable" + @pytest.mark.parametrize("port,entry_type", ((8009, None), (12345, None))) async def test_device_registry(hass: HomeAssistant, hass_ws_client, port, entry_type): @@ -1675,6 +1697,59 @@ async def test_group_media_states(hass, mz_mock): assert state.state == "playing" +async def test_group_media_states_early(hass, mz_mock): + """Test media states are read from group if entity has no state. + + This tests case asserts group state is polled when the player is created. + """ + entity_id = "media_player.speaker" + reg = er.async_get(hass) + + info = get_fake_chromecast_info() + + mz_mock.get_multizone_memberships = MagicMock(return_value=[str(FakeGroupUUID)]) + mz_mock.get_multizone_mediacontroller = MagicMock( + return_value=MagicMock(status=MagicMock(images=None, player_state="BUFFERING")) + ) + + chromecast, _ = await async_setup_media_player_cast(hass, info) + _, conn_status_cb, _, _ = get_status_callbacks(chromecast, mz_mock) + + state = hass.states.get(entity_id) + assert state is not None + assert state.name == "Speaker" + assert state.state == "unavailable" + assert entity_id == reg.async_get_entity_id("media_player", "cast", str(info.uuid)) + + # Check group state is polled when player is first created + connection_status = MagicMock() + connection_status.status = "CONNECTED" + conn_status_cb(connection_status) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "buffering" + + connection_status = MagicMock() + connection_status.status = "LOST" + conn_status_cb(connection_status) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "unavailable" + + # Check group state is polled when player reconnects + mz_mock.get_multizone_mediacontroller = MagicMock( + return_value=MagicMock(status=MagicMock(images=None, player_state="PLAYING")) + ) + + connection_status = MagicMock() + connection_status.status = "CONNECTED" + conn_status_cb(connection_status) + await hass.async_block_till_done() + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "playing" + + async def test_group_media_control(hass, mz_mock, quick_play_mock): """Test media controls are handled by group if entity has no state.""" entity_id = "media_player.speaker" From 8b01c132c1c441c615ad8ddec11d5e15bab74720 Mon Sep 17 00:00:00 2001 From: Arne Mauer Date: Thu, 7 Jul 2022 20:28:33 +0200 Subject: [PATCH 2276/3516] Ikea Starkvind support all models (#74615) * Add Particulate Matter 2.5 of ZCL concentration clusters to ZHA component * Fixed black and flake8 test * New sensors and manufacturer cluster to support IKEA STARKVIND (with quirk) * Fix multi_match for FilterLifeTime, device_run_time, filter_run_time sensors for Ikea starkvind * Remove model match because sensors are matched with manufacturer channel * Update manufacturerspecific.py * Update number.py --- homeassistant/components/zha/binary_sensor.py | 2 +- homeassistant/components/zha/number.py | 4 +--- homeassistant/components/zha/sensor.py | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 709515d7ca2..23d26e4b13f 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -188,7 +188,7 @@ class FrostLock(BinarySensor, id_suffix="frost_lock"): _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.LOCK -@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) +@MULTI_MATCH(channel_names="ikea_airpurifier") class ReplaceFilter(BinarySensor, id_suffix="replace_filter"): """ZHA BinarySensor.""" diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index e1268e29190..36fc5267bd9 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -525,9 +525,7 @@ class TimerDurationMinutes(ZHANumberConfigurationEntity, id_suffix="timer_durati _zcl_attribute: str = "timer_duration" -@CONFIG_DIAGNOSTIC_MATCH( - channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"} -) +@CONFIG_DIAGNOSTIC_MATCH(channel_names="ikea_airpurifier") class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time"): """Representation of a ZHA timer duration configuration entity.""" diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 4a4700b3c4c..c08c3f21201 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -810,7 +810,7 @@ class TimeLeft(Sensor, id_suffix="time_left"): _unit = TIME_MINUTES -@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) +@MULTI_MATCH(channel_names="ikea_airpurifier") class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"): """Sensor that displays device run time (in minutes).""" @@ -820,7 +820,7 @@ class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"): _unit = TIME_MINUTES -@MULTI_MATCH(channel_names="ikea_airpurifier", models={"STARKVIND Air purifier"}) +@MULTI_MATCH(channel_names="ikea_airpurifier") class IkeaFilterRunTime(Sensor, id_suffix="filter_run_time"): """Sensor that displays run time of the current filter (in minutes).""" From cd7f2eb73e97dd69c160bd28ae47629c3c2f706b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 7 Jul 2022 16:19:56 +0200 Subject: [PATCH 2277/3516] Update frontend to 20220707.0 (#74625) --- 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 85ead380485..5c7fd1a20be 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220706.0"], + "requirements": ["home-assistant-frontend==20220707.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c291d969219..de9df889eba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.0 -home-assistant-frontend==20220706.0 +home-assistant-frontend==20220707.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index ea5bd8cca1e..8578c571fbb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220706.0 +home-assistant-frontend==20220707.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3cbc10480bb..89b53d2b583 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -595,7 +595,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220706.0 +home-assistant-frontend==20220707.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 0da09ba47ec59afc814d5853a2c3c140f17ddec5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 7 Jul 2022 16:59:49 +0200 Subject: [PATCH 2278/3516] Fix mix of aiohttp and requests in ZAMG (#74628) --- homeassistant/components/zamg/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 87a1175b7cd..c32aa942625 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -10,7 +10,6 @@ import logging import os from typing import Union -from aiohttp.hdrs import USER_AGENT import requests import voluptuous as vol @@ -275,7 +274,7 @@ class ZamgData: """The class for handling the data retrieval.""" API_URL = "http://www.zamg.ac.at/ogd/" - API_HEADERS = {USER_AGENT: f"home-assistant.zamg/ {__version__}"} + API_HEADERS = {"User-Agent": f"home-assistant.zamg/ {__version__}"} def __init__(self, station_id): """Initialize the probe.""" From bac9af50df9004cc7bb6c16802722cb697889068 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 7 Jul 2022 20:27:48 +0200 Subject: [PATCH 2279/3516] Fix smart energy polling for Tuya plugs (#74640) * Add PolledSmartEnergySummation to poll summation_delivered for some ZHA plugs * Remove PolledSmartEnergyMetering, add stop_on_match_group to summation sensors --- homeassistant/components/zha/sensor.py | 37 ++++++++++++++------------ 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index c08c3f21201..513ba5510b5 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -472,25 +472,8 @@ class SmartEnergyMetering(Sensor): @MULTI_MATCH( channel_names=CHANNEL_SMARTENERGY_METERING, - models={"TS011F"}, stop_on_match_group=CHANNEL_SMARTENERGY_METERING, ) -class PolledSmartEnergyMetering(SmartEnergyMetering): - """Polled metering sensor.""" - - @property - def should_poll(self) -> bool: - """Poll the entity for current state.""" - return True - - async def async_update(self) -> None: - """Retrieve latest state.""" - if not self.available: - return - await self._channel.async_force_update() - - -@MULTI_MATCH(channel_names=CHANNEL_SMARTENERGY_METERING) class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered"): """Smart Energy Metering summation sensor.""" @@ -523,6 +506,26 @@ class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered") return round(cooked, 3) +@MULTI_MATCH( + channel_names=CHANNEL_SMARTENERGY_METERING, + models={"TS011F"}, + stop_on_match_group=CHANNEL_SMARTENERGY_METERING, +) +class PolledSmartEnergySummation(SmartEnergySummation): + """Polled Smart Energy Metering summation sensor.""" + + @property + def should_poll(self) -> bool: + """Poll the entity for current state.""" + return True + + async def async_update(self) -> None: + """Retrieve latest state.""" + if not self.available: + return + await self._channel.async_force_update() + + @MULTI_MATCH(channel_names=CHANNEL_PRESSURE) class Pressure(Sensor): """Pressure sensor.""" From 937d0d731d03348696046a9783524ad5959dcb31 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 7 Jul 2022 12:46:19 -0500 Subject: [PATCH 2280/3516] Fix exception in doorbird logbook during startup (#74649) * Fix exception in doorbird logbook during startup Fixes ``` 2022-07-07 16:50:33.203 ERROR (MainThread) [homeassistant.helpers.integration_platform] Error processing platform doorbird.logbook Traceback (most recent call last): File "/usr/src/homeassistant/homeassistant/helpers/integration_platform.py", line 51, in _async_process_single_integration_platform_component await integration_platform.process_platform(hass, component_name, platform) File "/usr/src/homeassistant/homeassistant/components/logbook/__init__.py", line 159, in _process_logbook_platform platform.async_describe_events(hass, _async_describe_event) File "/usr/src/homeassistant/homeassistant/components/doorbird/logbook.py", line 43, in async_describe_events door_station = data[DOOR_STATION] KeyError: door_station ``` * py39 --- homeassistant/components/doorbird/logbook.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/doorbird/logbook.py b/homeassistant/components/doorbird/logbook.py index 110d3f22fbf..3b1563c2880 100644 --- a/homeassistant/components/doorbird/logbook.py +++ b/homeassistant/components/doorbird/logbook.py @@ -1,4 +1,7 @@ """Describe logbook events.""" +from __future__ import annotations + +from typing import Any from homeassistant.components.logbook.const import ( LOGBOOK_ENTRY_ENTITY_ID, @@ -28,12 +31,13 @@ def async_describe_events(hass, async_describe_event): ].get(doorbird_event, event.data.get(ATTR_ENTITY_ID)), } - domain_data = hass.data[DOMAIN] + domain_data: dict[str, Any] = hass.data[DOMAIN] - for config_entry_id in domain_data: - door_station = domain_data[config_entry_id][DOOR_STATION] - - for event in door_station.doorstation_events: + for data in domain_data.values(): + if DOOR_STATION not in data: + # We need to skip door_station_event_entity_ids + continue + for event in data[DOOR_STATION].doorstation_events: async_describe_event( DOMAIN, f"{DOMAIN}_{event}", async_describe_logbook_event ) From b73cc32ffbf6f572947b4221a3b66773c3efe81a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Fri, 8 Jul 2022 00:46:23 +0200 Subject: [PATCH 2281/3516] Update kaiterra-async-client to 1.0.0 (#74677) --- homeassistant/components/kaiterra/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/kaiterra/manifest.json b/homeassistant/components/kaiterra/manifest.json index 9f2a4c0013f..94b861524eb 100644 --- a/homeassistant/components/kaiterra/manifest.json +++ b/homeassistant/components/kaiterra/manifest.json @@ -2,7 +2,7 @@ "domain": "kaiterra", "name": "Kaiterra", "documentation": "https://www.home-assistant.io/integrations/kaiterra", - "requirements": ["kaiterra-async-client==0.0.2"], + "requirements": ["kaiterra-async-client==1.0.0"], "codeowners": ["@Michsior14"], "iot_class": "cloud_polling", "loggers": ["kaiterra_async_client"] diff --git a/requirements_all.txt b/requirements_all.txt index 8578c571fbb..4a35ee93e08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -912,7 +912,7 @@ jellyfin-apiclient-python==1.8.1 jsonpath==0.82 # homeassistant.components.kaiterra -kaiterra-async-client==0.0.2 +kaiterra-async-client==1.0.0 # homeassistant.components.keba keba-kecontact==1.1.0 From 5018b91f6ce09455f5fd0a071c6cf45ea4a1bf37 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 7 Jul 2022 21:59:59 -0700 Subject: [PATCH 2282/3516] Bumped version to 2022.7.1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index c2ee7de691f..6902a728ea7 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index b4994d55edb..48e3ea452ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.0" +version = "2022.7.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From ba0b98ef3219fd5d47b83cfc3611aa02663e730b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 8 Jul 2022 08:30:01 +0200 Subject: [PATCH 2283/3516] Remove deprecated Spotify YAML configuration (#74604) --- homeassistant/components/spotify/__init__.py | 57 +----------- tests/components/spotify/test_config_flow.py | 91 ++++++++++---------- 2 files changed, 47 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index bf4e9d8deae..560620beed4 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -3,25 +3,14 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta -import logging from typing import Any import aiohttp import requests from spotipy import Spotify, SpotifyException -import voluptuous as vol -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_CREDENTIALS, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - Platform, -) +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv @@ -29,7 +18,6 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, async_get_config_entry_implementation, ) -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .browse_media import async_browse_media @@ -40,23 +28,7 @@ from .util import ( spotify_uri_from_media_browser_url, ) -_LOGGER = logging.getLogger(__name__) - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Inclusive(CONF_CLIENT_ID, ATTR_CREDENTIALS): cv.string, - vol.Inclusive(CONF_CLIENT_SECRET, ATTR_CREDENTIALS): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.MEDIA_PLAYER] @@ -79,31 +51,6 @@ class HomeAssistantSpotifyData: session: OAuth2Session -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Spotify integration.""" - if DOMAIN not in config: - return True - - if CONF_CLIENT_ID in config[DOMAIN]: - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential( - config[DOMAIN][CONF_CLIENT_ID], - config[DOMAIN][CONF_CLIENT_SECRET], - ), - ) - _LOGGER.warning( - "Configuration of Spotify integration in YAML is deprecated and " - "will be removed in a future release; Your existing OAuth " - "Application Credentials have been imported into the UI " - "automatically and can be safely removed from your " - "configuration.yaml file" - ) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Spotify from a config entry.""" implementation = await async_get_config_entry_implementation(hass, entry) diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index a47d25aa06d..8f3b5799374 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -2,14 +2,20 @@ from http import HTTPStatus from unittest.mock import patch +import pytest from spotipy import SpotifyException -from homeassistant import data_entry_flow, setup +from homeassistant import data_entry_flow from homeassistant.components import zeroconf +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.components.spotify.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -24,6 +30,19 @@ BLANK_ZEROCONF_INFO = zeroconf.ZeroconfServiceInfo( ) +@pytest.fixture +async def component_setup(hass: HomeAssistant) -> None: + """Fixture for setting up the integration.""" + result = await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + await async_import_client_credential( + hass, DOMAIN, ClientCredential("client", "secret"), "cred" + ) + + assert result + + async def test_abort_if_no_configuration(hass): """Check flow aborts when no configuration is present.""" result = await hass.config_entries.flow.async_init( @@ -54,18 +73,13 @@ async def test_zeroconf_abort_if_existing_entry(hass): async def test_full_flow( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host + hass, + component_setup, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, ): """Check a full flow.""" - assert await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}, - "http": {"base_url": "https://example.com"}, - }, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -114,7 +128,7 @@ async def test_full_flow( } result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["data"]["auth_implementation"] == DOMAIN + assert result["data"]["auth_implementation"] == "cred" result["data"]["token"].pop("expires_at") assert result["data"]["name"] == "frenck" assert result["data"]["token"] == { @@ -126,18 +140,13 @@ async def test_full_flow( async def test_abort_if_spotify_error( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host + hass, + component_setup, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, ): """Check Spotify errors causes flow to abort.""" - await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}, - "http": {"base_url": "https://example.com"}, - }, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -174,23 +183,18 @@ async def test_abort_if_spotify_error( async def test_reauthentication( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host + hass, + component_setup, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, ): """Test Spotify reauthentication.""" - await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}, - "http": {"base_url": "https://example.com"}, - }, - ) - old_entry = MockConfigEntry( domain=DOMAIN, unique_id=123, version=1, - data={"id": "frenck", "auth_implementation": DOMAIN}, + data={"id": "frenck", "auth_implementation": "cred"}, ) old_entry.add_to_hass(hass) @@ -236,7 +240,7 @@ async def test_reauthentication( spotify_mock.return_value.current_user.return_value = {"id": "frenck"} result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["data"]["auth_implementation"] == DOMAIN + assert result["data"]["auth_implementation"] == "cred" result["data"]["token"].pop("expires_at") assert result["data"]["token"] == { "refresh_token": "mock-refresh-token", @@ -247,23 +251,18 @@ async def test_reauthentication( async def test_reauth_account_mismatch( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host + hass, + component_setup, + hass_client_no_auth, + aioclient_mock, + current_request_with_host, ): """Test Spotify reauthentication with different account.""" - await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: {CONF_CLIENT_ID: "client", CONF_CLIENT_SECRET: "secret"}, - "http": {"base_url": "https://example.com"}, - }, - ) - old_entry = MockConfigEntry( domain=DOMAIN, unique_id=123, version=1, - data={"id": "frenck", "auth_implementation": DOMAIN}, + data={"id": "frenck", "auth_implementation": "cred"}, ) old_entry.add_to_hass(hass) From fd7330ea7700ac18ee1d073d0b55299ccb045d53 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 8 Jul 2022 09:48:46 +0200 Subject: [PATCH 2284/3516] Bump NextDNS backend library (#74611) --- homeassistant/components/nextdns/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nextdns/test_diagnostics.py | 10 ++++++---- tests/components/nextdns/test_sensor.py | 8 ++++---- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json index fd3dd46f846..a427f930db8 100644 --- a/homeassistant/components/nextdns/manifest.json +++ b/homeassistant/components/nextdns/manifest.json @@ -3,7 +3,7 @@ "name": "NextDNS", "documentation": "https://www.home-assistant.io/integrations/nextdns", "codeowners": ["@bieniu"], - "requirements": ["nextdns==1.0.0"], + "requirements": ["nextdns==1.0.1"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["nextdns"] diff --git a/requirements_all.txt b/requirements_all.txt index 6cc0cce2cbf..3019833e03d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1092,7 +1092,7 @@ nextcloudmonitor==1.1.0 nextcord==2.0.0a8 # homeassistant.components.nextdns -nextdns==1.0.0 +nextdns==1.0.1 # homeassistant.components.niko_home_control niko-home-control==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 32a642581d8..4e0136bcc15 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -760,7 +760,7 @@ nexia==2.0.1 nextcord==2.0.0a8 # homeassistant.components.nextdns -nextdns==1.0.0 +nextdns==1.0.1 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 diff --git a/tests/components/nextdns/test_diagnostics.py b/tests/components/nextdns/test_diagnostics.py index 73844b6a2e3..ba4a4d2ccb4 100644 --- a/tests/components/nextdns/test_diagnostics.py +++ b/tests/components/nextdns/test_diagnostics.py @@ -43,11 +43,13 @@ async def test_entry_diagnostics(hass, hass_client): "doh_queries": 20, "doq_queries": 10, "dot_queries": 30, + "tcp_queries": 0, "udp_queries": 40, - "doh_queries_ratio": 22.2, - "doq_queries_ratio": 11.1, - "dot_queries_ratio": 33.3, - "udp_queries_ratio": 44.4, + "doh_queries_ratio": 20.0, + "doq_queries_ratio": 10.0, + "dot_queries_ratio": 30.0, + "tcp_queries_ratio": 0.0, + "udp_queries_ratio": 40.0, } assert result["status_coordinator_data"] == { "all_queries": 100, diff --git a/tests/components/nextdns/test_sensor.py b/tests/components/nextdns/test_sensor.py index fef2abafc85..8a7d13866f3 100644 --- a/tests/components/nextdns/test_sensor.py +++ b/tests/components/nextdns/test_sensor.py @@ -197,7 +197,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dns_over_https_queries_ratio") assert state - assert state.state == "22.2" + assert state.state == "20.0" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -217,7 +217,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dns_over_quic_queries_ratio") assert state - assert state.state == "11.1" + assert state.state == "10.0" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -237,7 +237,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dns_over_tls_queries_ratio") assert state - assert state.state == "33.3" + assert state.state == "30.0" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -347,7 +347,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_udp_queries_ratio") assert state - assert state.state == "44.4" + assert state.state == "40.0" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE From 06530ebfa1b1ee0f52caa6a3152e1f470cbc7b63 Mon Sep 17 00:00:00 2001 From: siyuan-nz <91467287+siyuan-nz@users.noreply.github.com> Date: Fri, 8 Jul 2022 19:51:10 +1200 Subject: [PATCH 2285/3516] Add ssh-rsa as acceptable an host key algorithm (#74684) --- homeassistant/components/unifi_direct/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/unifi_direct/device_tracker.py b/homeassistant/components/unifi_direct/device_tracker.py index f71498faa11..7a81975c0ba 100644 --- a/homeassistant/components/unifi_direct/device_tracker.py +++ b/homeassistant/components/unifi_direct/device_tracker.py @@ -80,7 +80,7 @@ class UnifiDeviceScanner(DeviceScanner): def _connect(self): """Connect to the Unifi AP SSH server.""" - self.ssh = pxssh.pxssh() + self.ssh = pxssh.pxssh(options={"HostKeyAlgorithms": "ssh-rsa"}) try: self.ssh.login( self.host, self.username, password=self.password, port=self.port From 3f8cfa3b0af3933ab5eeb2d4d92d81e5c59cadcf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 8 Jul 2022 12:39:29 +0200 Subject: [PATCH 2286/3516] Always run pip_check in CI (#74706) --- .github/workflows/ci.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2f97a8e4208..5320b9fe474 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -677,7 +677,6 @@ jobs: pip-check: runs-on: ubuntu-20.04 - if: needs.info.outputs.requirements == 'true' || github.event.inputs.full == 'true' needs: - info - base From 3b3766fbe0b7ab2ae2816a970414cfa431b4f903 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 8 Jul 2022 12:54:09 +0200 Subject: [PATCH 2287/3516] Bump deconz dependency to fix #74523 (#74710) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_sensor.py | 31 +++++++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index c19d75ec054..06f8b6c0376 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==96"], + "requirements": ["pydeconz==97"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 3019833e03d..ea7c9fe5900 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1444,7 +1444,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==96 +pydeconz==97 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4e0136bcc15..16fd3833b4d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -977,7 +977,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==96 +pydeconz==97 # homeassistant.components.dexcom pydexcom==0.2.3 diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 658e11da906..5f11a4d7b0b 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -816,6 +816,37 @@ async def test_dont_add_sensor_if_state_is_none( assert len(hass.states.async_all()) == 0 +async def test_air_quality_sensor_without_ppb(hass, aioclient_mock): + """Test sensor with scaled data is not created if state is None.""" + data = { + "sensors": { + "1": { + "config": { + "on": True, + "reachable": True, + }, + "ep": 2, + "etag": "c2d2e42396f7c78e11e46c66e2ec0200", + "lastseen": "2020-11-20T22:48Z", + "manufacturername": "BOSCH", + "modelid": "AIR", + "name": "BOSCH Air quality sensor", + "state": { + "airquality": "poor", + "lastupdated": "2020-11-20T22:48:00.209", + }, + "swversion": "20200402", + "type": "ZHAAirQuality", + "uniqueid": "00:00:00:00:00:00:00:00-02-fdef", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + await setup_deconz_integration(hass, aioclient_mock) + + assert len(hass.states.async_all()) == 1 + + async def test_add_battery_later(hass, aioclient_mock, mock_deconz_websocket): """Test that a sensor without an initial battery state creates a battery sensor once state exist.""" data = { From 540ffe116ec1b0a2e11463854819338869001900 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 8 Jul 2022 13:59:20 +0200 Subject: [PATCH 2288/3516] Update debugpy to 1.6.2 (#74692) --- 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 3f31c5de758..3cc6c16e831 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -2,7 +2,7 @@ "domain": "debugpy", "name": "Remote Python Debugger", "documentation": "https://www.home-assistant.io/integrations/debugpy", - "requirements": ["debugpy==1.6.1"], + "requirements": ["debugpy==1.6.2"], "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index ea7c9fe5900..bdb840ea88a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -518,7 +518,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.6.1 +debugpy==1.6.2 # homeassistant.components.decora # decora==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 16fd3833b4d..d37ff39e79d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -388,7 +388,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.6.1 +debugpy==1.6.2 # homeassistant.components.ihc # homeassistant.components.namecheapdns From e55a4dcab1f00da4d50e0f76364cbbd453f0c440 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Fri, 8 Jul 2022 15:16:13 +0200 Subject: [PATCH 2289/3516] Add missing strings for here_travel_time (#74641) * Add missing strings for here_travel_time * script.translations develop * Correct origin_menu option --- homeassistant/components/here_travel_time/strings.json | 7 +++++++ .../components/here_travel_time/translations/en.json | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/homeassistant/components/here_travel_time/strings.json b/homeassistant/components/here_travel_time/strings.json index e4a20a38d6b..dab135efc82 100644 --- a/homeassistant/components/here_travel_time/strings.json +++ b/homeassistant/components/here_travel_time/strings.json @@ -8,6 +8,13 @@ "mode": "Travel Mode" } }, + "origin_menu": { + "title": "Choose Origin", + "menu_options": { + "origin_coordinates": "Using a map location", + "origin_entity": "Using an entity" + } + }, "origin_coordinates": { "title": "Choose Origin", "data": { diff --git a/homeassistant/components/here_travel_time/translations/en.json b/homeassistant/components/here_travel_time/translations/en.json index d4f9984d945..f31d5a3783d 100644 --- a/homeassistant/components/here_travel_time/translations/en.json +++ b/homeassistant/components/here_travel_time/translations/en.json @@ -39,6 +39,13 @@ }, "title": "Choose Origin" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Using a map location", + "origin_entity": "Using an entity" + }, + "title": "Choose Origin" + }, "user": { "data": { "api_key": "API Key", From b2a30716586c4b662733287c55cc5c40ef9736f3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 8 Jul 2022 16:19:32 +0200 Subject: [PATCH 2290/3516] Cleanup generic in NextDNS (#74705) Cleanup generic in nextdns --- homeassistant/components/nextdns/sensor.py | 64 +++++++--------------- 1 file changed, 21 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index 71d71ff287b..54d10ba66b9 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -43,7 +43,6 @@ PARALLEL_UPDATES = 1 class NextDnsSensorRequiredKeysMixin(Generic[TCoordinatorData]): """Class for NextDNS entity required keys.""" - coordinator_class: type[TCoordinatorData] coordinator_type: str value: Callable[[TCoordinatorData], StateType] @@ -57,9 +56,8 @@ class NextDnsSensorEntityDescription( SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsStatus]( key="all_queries", - coordinator_class=AnalyticsStatus, coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", @@ -68,9 +66,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.all_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsStatus]( key="blocked_queries", - coordinator_class=AnalyticsStatus, coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", @@ -79,9 +76,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.blocked_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsStatus]( key="relayed_queries", - coordinator_class=AnalyticsStatus, coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", @@ -90,9 +86,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.relayed_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsStatus]( key="blocked_queries_ratio", - coordinator_class=AnalyticsStatus, coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", @@ -101,9 +96,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.blocked_queries_ratio, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsProtocols]( key="doh_queries", - coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -113,9 +107,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doh_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsProtocols]( key="dot_queries", - coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -125,9 +118,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.dot_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsProtocols]( key="doq_queries", - coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -137,9 +129,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doq_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsProtocols]( key="udp_queries", - coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -149,9 +140,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.udp_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsProtocols]( key="doh_queries_ratio", - coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_registry_enabled_default=False, icon="mdi:dns", @@ -161,9 +151,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doh_queries_ratio, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsProtocols]( key="dot_queries_ratio", - coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -173,9 +162,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.dot_queries_ratio, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsProtocols]( key="doq_queries_ratio", - coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_registry_enabled_default=False, icon="mdi:dns", @@ -185,9 +173,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doq_queries_ratio, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsProtocols]( key="udp_queries_ratio", - coordinator_class=AnalyticsProtocols, coordinator_type=ATTR_PROTOCOLS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -197,9 +184,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.udp_queries_ratio, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsEncryption]( key="encrypted_queries", - coordinator_class=AnalyticsEncryption, coordinator_type=ATTR_ENCRYPTION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -209,9 +195,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.encrypted_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsEncryption]( key="unencrypted_queries", - coordinator_class=AnalyticsEncryption, coordinator_type=ATTR_ENCRYPTION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -221,9 +206,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.unencrypted_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsEncryption]( key="encrypted_queries_ratio", - coordinator_class=AnalyticsEncryption, coordinator_type=ATTR_ENCRYPTION, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -233,9 +217,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.encrypted_queries_ratio, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsIpVersions]( key="ipv4_queries", - coordinator_class=AnalyticsIpVersions, coordinator_type=ATTR_IP_VERSIONS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -245,9 +228,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.ipv4_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsIpVersions]( key="ipv6_queries", - coordinator_class=AnalyticsIpVersions, coordinator_type=ATTR_IP_VERSIONS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -257,9 +239,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.ipv6_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsIpVersions]( key="ipv6_queries_ratio", - coordinator_class=AnalyticsIpVersions, coordinator_type=ATTR_IP_VERSIONS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -269,9 +250,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.ipv6_queries_ratio, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsDnssec]( key="validated_queries", - coordinator_class=AnalyticsDnssec, coordinator_type=ATTR_DNSSEC, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -281,9 +261,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.validated_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsDnssec]( key="not_validated_queries", - coordinator_class=AnalyticsDnssec, coordinator_type=ATTR_DNSSEC, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -293,9 +272,8 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.not_validated_queries, ), - NextDnsSensorEntityDescription( + NextDnsSensorEntityDescription[AnalyticsDnssec]( key="validated_queries_ratio", - coordinator_class=AnalyticsDnssec, coordinator_type=ATTR_DNSSEC, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, From 08d8304a529074a0b39b6ca83355ba799bf21afc Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 8 Jul 2022 07:59:50 -0700 Subject: [PATCH 2291/3516] Migrate google calendar to new entity naming (#74727) * Migrate google calendar to new entity naming * Update homeassistant/components/google/calendar.py Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/google/calendar.py | 9 +++------ tests/components/google/conftest.py | 4 ++-- tests/components/google/test_init.py | 6 +++--- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index a86cdf55e3a..534b1cbdef3 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -216,6 +216,8 @@ async def async_setup_entry( class GoogleCalendarEntity(CalendarEntity): """A calendar event device.""" + _attr_has_entity_name = True + def __init__( self, calendar_service: GoogleCalendarService, @@ -231,7 +233,7 @@ class GoogleCalendarEntity(CalendarEntity): self._search: str | None = data.get(CONF_SEARCH) self._ignore_availability: bool = data.get(CONF_IGNORE_AVAILABILITY, False) self._event: CalendarEvent | None = None - self._name: str = data[CONF_NAME] + self._attr_name = data[CONF_NAME].capitalize() self._offset = data.get(CONF_OFFSET, DEFAULT_CONF_OFFSET) self._offset_value: timedelta | None = None self.entity_id = entity_id @@ -257,11 +259,6 @@ class GoogleCalendarEntity(CalendarEntity): """Return the next upcoming event.""" return self._event - @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - def _event_filter(self, event: Event) -> bool: """Return True if the event is visible.""" if self._ignore_availability: diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index a871722c2e9..f47d1232582 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -34,10 +34,10 @@ EMAIL_ADDRESS = "user@gmail.com" # the yaml config that overrides the entity name and other settings. A test # can use a fixture to exercise either case. TEST_API_ENTITY = "calendar.we_are_we_are_a_test_calendar" -TEST_API_ENTITY_NAME = "We are, we are, a... Test Calendar" +TEST_API_ENTITY_NAME = "We are, we are, a... test calendar" # Name of the entity when using yaml configuration overrides TEST_YAML_ENTITY = "calendar.backyard_light" -TEST_YAML_ENTITY_NAME = "Backyard Light" +TEST_YAML_ENTITY_NAME = "Backyard light" # A calendar object returned from the API TEST_API_CALENDAR = { diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index a40b499f769..f6c1f8c611f 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -20,7 +20,7 @@ from homeassistant.components.google import DOMAIN, SERVICE_ADD_EVENT from homeassistant.components.google.calendar import SERVICE_CREATE_EVENT from homeassistant.components.google.const import CONF_CALENDAR_ACCESS from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import STATE_OFF +from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_OFF from homeassistant.core import HomeAssistant, State from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component @@ -307,8 +307,8 @@ async def test_multiple_config_entries( state = hass.states.get("calendar.example_calendar_1") assert state - assert state.name == "Example Calendar 1" assert state.state == STATE_OFF + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Example calendar 1" config_entry2 = MockConfigEntry( domain=DOMAIN, data=config_entry.data, unique_id="other-address@example.com" @@ -327,7 +327,7 @@ async def test_multiple_config_entries( state = hass.states.get("calendar.example_calendar_2") assert state - assert state.name == "Example Calendar 2" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Example calendar 2" @pytest.mark.parametrize( From 1d69e631b53b60f03add725aac664b70e060a6c5 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Fri, 8 Jul 2022 18:47:59 +0200 Subject: [PATCH 2292/3516] Fix ZHA group not setting the correct color mode (#74687) * Fix ZHA group not setting the correct color mode * Changed to use _attr_color_mode --- homeassistant/components/zha/light.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 309fdf2699b..e1c85b39d8e 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -141,7 +141,7 @@ class BaseLight(LogMixin, light.LightEntity): self._color_channel = None self._identify_channel = None self._default_transition = None - self._color_mode = ColorMode.UNKNOWN # Set by sub classes + self._attr_color_mode = ColorMode.UNKNOWN # Set by sub classes @property def extra_state_attributes(self) -> dict[str, Any]: @@ -159,11 +159,6 @@ class BaseLight(LogMixin, light.LightEntity): return False return self._state - @property - def color_mode(self): - """Return the color mode of this light.""" - return self._color_mode - @property def brightness(self): """Return the brightness of this light.""" @@ -309,7 +304,7 @@ class BaseLight(LogMixin, light.LightEntity): if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return - self._color_mode = ColorMode.COLOR_TEMP + self._attr_color_mode = ColorMode.COLOR_TEMP self._color_temp = temperature self._hs_color = None @@ -323,7 +318,7 @@ class BaseLight(LogMixin, light.LightEntity): if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return - self._color_mode = ColorMode.HS + self._attr_color_mode = ColorMode.HS self._hs_color = hs_color self._color_temp = None @@ -451,13 +446,13 @@ class Light(BaseLight, ZhaEntity): self._attr_supported_color_modes ) if len(self._attr_supported_color_modes) == 1: - self._color_mode = next(iter(self._attr_supported_color_modes)) + self._attr_color_mode = next(iter(self._attr_supported_color_modes)) else: # Light supports color_temp + hs, determine which mode the light is in assert self._color_channel if self._color_channel.color_mode == Color.ColorMode.Color_temperature: - self._color_mode = ColorMode.COLOR_TEMP + self._attr_color_mode = ColorMode.COLOR_TEMP else: - self._color_mode = ColorMode.HS + self._attr_color_mode = ColorMode.HS if self._identify_channel: self._supported_features |= light.LightEntityFeature.FLASH @@ -518,7 +513,7 @@ class Light(BaseLight, ZhaEntity): if "off_brightness" in last_state.attributes: self._off_brightness = last_state.attributes["off_brightness"] if "color_mode" in last_state.attributes: - self._color_mode = ColorMode(last_state.attributes["color_mode"]) + self._attr_color_mode = ColorMode(last_state.attributes["color_mode"]) if "color_temp" in last_state.attributes: self._color_temp = last_state.attributes["color_temp"] if "hs_color" in last_state.attributes: @@ -558,13 +553,13 @@ class Light(BaseLight, ZhaEntity): if (color_mode := results.get("color_mode")) is not None: if color_mode == Color.ColorMode.Color_temperature: - self._color_mode = ColorMode.COLOR_TEMP + self._attr_color_mode = ColorMode.COLOR_TEMP color_temp = results.get("color_temperature") if color_temp is not None and color_mode: self._color_temp = color_temp self._hs_color = None else: - self._color_mode = ColorMode.HS + self._attr_color_mode = ColorMode.HS color_x = results.get("current_x") color_y = results.get("current_y") if color_x is not None and color_y is not None: @@ -650,7 +645,7 @@ class LightGroup(BaseLight, ZhaGroupEntity): CONF_DEFAULT_LIGHT_TRANSITION, 0, ) - self._color_mode = None + self._attr_color_mode = None async def async_added_to_hass(self): """Run when about to be added to hass.""" From ff324ab2fb0a95f49f98bbda0c9fcb6646da4d7b Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 8 Jul 2022 09:50:56 -0700 Subject: [PATCH 2293/3516] Attempt to fix flaky test by waiting for setup to complete (#74734) --- tests/components/system_log/test_init.py | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 121c29d2eed..d558d690536 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -89,6 +89,7 @@ def get_frame(name): async def test_normal_logs(hass, simple_queue, hass_ws_client): """Test that debug and info are not logged.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() _LOGGER.debug("debug") _LOGGER.info("info") @@ -102,6 +103,8 @@ async def test_normal_logs(hass, simple_queue, hass_ws_client): async def test_exception(hass, simple_queue, hass_ws_client): """Test that exceptions are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + _generate_and_log_exception("exception message", "log message") await _async_block_until_queue_empty(hass, simple_queue) log = find_log(await get_error_log(hass_ws_client), "ERROR") @@ -112,6 +115,8 @@ async def test_exception(hass, simple_queue, hass_ws_client): async def test_warning(hass, simple_queue, hass_ws_client): """Test that warning are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + _LOGGER.warning("warning message") await _async_block_until_queue_empty(hass, simple_queue) @@ -122,6 +127,8 @@ async def test_warning(hass, simple_queue, hass_ws_client): async def test_error(hass, simple_queue, hass_ws_client): """Test that errors are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + _LOGGER.error("error message") await _async_block_until_queue_empty(hass, simple_queue) @@ -132,6 +139,8 @@ async def test_error(hass, simple_queue, hass_ws_client): async def test_config_not_fire_event(hass, simple_queue): """Test that errors are not posted as events with default config.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + events = [] @callback @@ -152,6 +161,8 @@ async def test_error_posted_as_event(hass, simple_queue): await async_setup_component( hass, system_log.DOMAIN, {"system_log": {"max_entries": 2, "fire_event": True}} ) + await hass.async_block_till_done() + events = async_capture_events(hass, system_log.EVENT_SYSTEM_LOG) _LOGGER.error("error message") @@ -164,6 +175,8 @@ async def test_error_posted_as_event(hass, simple_queue): async def test_critical(hass, simple_queue, hass_ws_client): """Test that critical are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + _LOGGER.critical("critical message") await _async_block_until_queue_empty(hass, simple_queue) @@ -174,6 +187,8 @@ async def test_critical(hass, simple_queue, hass_ws_client): async def test_remove_older_logs(hass, simple_queue, hass_ws_client): """Test that older logs are rotated out.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + _LOGGER.error("error message 1") _LOGGER.error("error message 2") _LOGGER.error("error message 3") @@ -192,6 +207,8 @@ def log_msg(nr=2): async def test_dedupe_logs(hass, simple_queue, hass_ws_client): """Test that duplicate log entries are dedupe.""" await async_setup_component(hass, system_log.DOMAIN, {}) + await hass.async_block_till_done() + _LOGGER.error("error message 1") log_msg() log_msg("2-2") @@ -234,6 +251,8 @@ async def test_dedupe_logs(hass, simple_queue, hass_ws_client): async def test_clear_logs(hass, simple_queue, hass_ws_client): """Test that the log can be cleared via a service call.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + _LOGGER.error("error message") await _async_block_until_queue_empty(hass, simple_queue) @@ -247,6 +266,8 @@ async def test_clear_logs(hass, simple_queue, hass_ws_client): async def test_write_log(hass): """Test that error propagates to logger.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + logger = MagicMock() with patch("logging.getLogger", return_value=logger) as mock_logging: await hass.services.async_call( @@ -260,6 +281,8 @@ async def test_write_log(hass): async def test_write_choose_logger(hass): """Test that correct logger is chosen.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + with patch("logging.getLogger") as mock_logging: await hass.services.async_call( system_log.DOMAIN, @@ -273,6 +296,8 @@ async def test_write_choose_logger(hass): async def test_write_choose_level(hass): """Test that correct logger is chosen.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + logger = MagicMock() with patch("logging.getLogger", return_value=logger): await hass.services.async_call( @@ -287,6 +312,8 @@ async def test_write_choose_level(hass): async def test_unknown_path(hass, simple_queue, hass_ws_client): """Test error logged from unknown path.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + _LOGGER.findCaller = MagicMock(return_value=("unknown_path", 0, None, None)) _LOGGER.error("error message") await _async_block_until_queue_empty(hass, simple_queue) @@ -317,6 +344,8 @@ async def async_log_error_from_test_path(hass, path, sq): async def test_homeassistant_path(hass, simple_queue, hass_ws_client): """Test error logged from Home Assistant path.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + with patch( "homeassistant.components.system_log.HOMEASSISTANT_PATH", new=["venv_path/homeassistant"], @@ -331,6 +360,8 @@ async def test_homeassistant_path(hass, simple_queue, hass_ws_client): async def test_config_path(hass, simple_queue, hass_ws_client): """Test error logged from config path.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) + await hass.async_block_till_done() + with patch.object(hass.config, "config_dir", new="config"): await async_log_error_from_test_path( hass, "config/custom_component/test.py", simple_queue From 4209d7733b3983cc45f61b4eb6c758f5edb314b0 Mon Sep 17 00:00:00 2001 From: Antonino Piazza Date: Fri, 8 Jul 2022 22:09:03 +0200 Subject: [PATCH 2294/3516] Add huawei_lte wifi guest network switch (#71035) --- .../components/huawei_lte/__init__.py | 13 ++ homeassistant/components/huawei_lte/const.py | 3 +- homeassistant/components/huawei_lte/switch.py | 47 +++++- tests/components/huawei_lte/test_switches.py | 138 ++++++++++++++++++ 4 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 tests/components/huawei_lte/test_switches.py diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index f4e2cb209db..c0337095a9c 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -74,6 +74,7 @@ from .const import ( KEY_SMS_SMS_COUNT, KEY_WLAN_HOST_LIST, KEY_WLAN_WIFI_FEATURE_SWITCH, + KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH, NOTIFY_SUPPRESS_TIMEOUT, SERVICE_CLEAR_TRAFFIC_STATISTICS, SERVICE_REBOOT, @@ -275,6 +276,18 @@ class Router: self._get_data( KEY_WLAN_WIFI_FEATURE_SWITCH, self.client.wlan.wifi_feature_switch ) + self._get_data( + KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH, + lambda: next( + filter( + lambda ssid: ssid.get("wifiisguestnetwork") == "1", + self.client.wlan.multi_basic_settings() + .get("Ssids", {}) + .get("Ssid", []), + ), + {}, + ), + ) dispatcher_send(self.hass, UPDATE_SIGNAL, self.config_entry.unique_id) diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index b9cbf546087..754be6bf2f3 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -43,6 +43,7 @@ KEY_NET_NET_MODE = "net_net_mode" KEY_SMS_SMS_COUNT = "sms_sms_count" KEY_WLAN_HOST_LIST = "wlan_host_list" KEY_WLAN_WIFI_FEATURE_SWITCH = "wlan_wifi_feature_switch" +KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH = "wlan_wifi_guest_network_switch" BINARY_SENSOR_KEYS = { KEY_MONITORING_CHECK_NOTIFICATIONS, @@ -67,7 +68,7 @@ SENSOR_KEYS = { KEY_SMS_SMS_COUNT, } -SWITCH_KEYS = {KEY_DIALUP_MOBILE_DATASWITCH} +SWITCH_KEYS = {KEY_DIALUP_MOBILE_DATASWITCH, KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH} ALL_KEYS = ( BINARY_SENSOR_KEYS diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index cc5e8e446c5..611f7212d9a 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -16,7 +16,11 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HuaweiLteBaseEntityWithDevice -from .const import DOMAIN, KEY_DIALUP_MOBILE_DATASWITCH +from .const import ( + DOMAIN, + KEY_DIALUP_MOBILE_DATASWITCH, + KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH, +) _LOGGER = logging.getLogger(__name__) @@ -33,6 +37,9 @@ async def async_setup_entry( if router.data.get(KEY_DIALUP_MOBILE_DATASWITCH): switches.append(HuaweiLteMobileDataSwitch(router)) + if router.data.get(KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH).get("WifiEnable"): + switches.append(HuaweiLteWifiGuestNetworkSwitch(router)) + async_add_entities(switches, True) @@ -113,3 +120,41 @@ class HuaweiLteMobileDataSwitch(HuaweiLteBaseSwitch): def icon(self) -> str: """Return switch icon.""" return "mdi:signal" if self.is_on else "mdi:signal-off" + + +@dataclass +class HuaweiLteWifiGuestNetworkSwitch(HuaweiLteBaseSwitch): + """Huawei LTE WiFi guest network switch device.""" + + def __post_init__(self) -> None: + """Initialize identifiers.""" + self.key = KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH + self.item = "WifiEnable" + + @property + def _entity_name(self) -> str: + return "WiFi guest network" + + @property + def _device_unique_id(self) -> str: + return f"{self.key}.{self.item}" + + @property + def is_on(self) -> bool: + """Return whether the switch is on.""" + return self._raw_state == "1" + + def _turn(self, state: bool) -> None: + self.router.client.wlan.wifi_guest_network_switch(state) + self._raw_state = "1" if state else "0" + self.schedule_update_ha_state() + + @property + def icon(self) -> str: + """Return switch icon.""" + return "mdi:wifi" if self.is_on else "mdi:wifi-off" + + @property + def extra_state_attributes(self) -> dict[str, str]: + """Return the state attributes.""" + return {"ssid": self.router.data[self.key].get("WifiSsid")} diff --git a/tests/components/huawei_lte/test_switches.py b/tests/components/huawei_lte/test_switches.py new file mode 100644 index 00000000000..c5006add923 --- /dev/null +++ b/tests/components/huawei_lte/test_switches.py @@ -0,0 +1,138 @@ +"""Tests for the Huawei LTE switches.""" +from unittest.mock import MagicMock, patch + +from huawei_lte_api.enums.cradle import ConnectionStatusEnum +from pytest import fixture + +from homeassistant.components.huawei_lte.const import DOMAIN +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, CONF_URL, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_registry import EntityRegistry + +from tests.common import MockConfigEntry + +SWITCH_WIFI_GUEST_NETWORK = "switch.huawei_lte_wifi_guest_network" + + +@fixture +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch( + "homeassistant.components.huawei_lte.Client", + return_value=MagicMock( + device=MagicMock( + information=MagicMock(return_value={"SerialNumber": "test-serial-number"}) + ), + monitoring=MagicMock( + check_notifications=MagicMock(return_value={"SmsStorageFull": 0}), + status=MagicMock( + return_value={"ConnectionStatus": ConnectionStatusEnum.CONNECTED.value} + ), + ), + wlan=MagicMock( + multi_basic_settings=MagicMock( + return_value={ + "Ssids": {"Ssid": [{"wifiisguestnetwork": "1", "WifiEnable": "0"}]} + } + ), + wifi_feature_switch=MagicMock(return_value={"wifi24g_switch_enable": 1}), + ), + ), +) +async def setup_component_with_wifi_guest_network( + client: MagicMock, hass: HomeAssistant +) -> None: + """Initialize huawei_lte components.""" + assert client + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + assert await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() + + +@fixture +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch( + "homeassistant.components.huawei_lte.Client", + return_value=MagicMock( + device=MagicMock( + information=MagicMock(return_value={"SerialNumber": "test-serial-number"}) + ), + monitoring=MagicMock( + check_notifications=MagicMock(return_value={"SmsStorageFull": 0}), + status=MagicMock( + return_value={"ConnectionStatus": ConnectionStatusEnum.CONNECTED.value} + ), + ), + wlan=MagicMock( + multi_basic_settings=MagicMock(return_value={}), + wifi_feature_switch=MagicMock(return_value={"wifi24g_switch_enable": 1}), + ), + ), +) +async def setup_component_without_wifi_guest_network( + client: MagicMock, hass: HomeAssistant +) -> None: + """Initialize huawei_lte components.""" + assert client + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + assert await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() + + +def test_huawei_lte_wifi_guest_network_config_entry_when_network_is_not_present( + hass: HomeAssistant, + setup_component_without_wifi_guest_network, +) -> None: + """Test switch wifi guest network config entry when network is not present.""" + entity_registry: EntityRegistry = er.async_get(hass) + assert not entity_registry.async_is_registered(SWITCH_WIFI_GUEST_NETWORK) + + +def test_huawei_lte_wifi_guest_network_config_entry_when_network_is_present( + hass: HomeAssistant, + setup_component_with_wifi_guest_network, +) -> None: + """Test switch wifi guest network config entry when network is present.""" + entity_registry: EntityRegistry = er.async_get(hass) + assert entity_registry.async_is_registered(SWITCH_WIFI_GUEST_NETWORK) + + +async def test_turn_on_switch_wifi_guest_network( + hass: HomeAssistant, setup_component_with_wifi_guest_network +) -> None: + """Test switch wifi guest network turn on method.""" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: SWITCH_WIFI_GUEST_NETWORK}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.is_state(SWITCH_WIFI_GUEST_NETWORK, STATE_ON) + hass.data[DOMAIN].routers[ + "test-serial-number" + ].client.wlan.wifi_guest_network_switch.assert_called_once_with(True) + + +async def test_turn_off_switch_wifi_guest_network( + hass: HomeAssistant, setup_component_with_wifi_guest_network +) -> None: + """Test switch wifi guest network turn off method.""" + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: SWITCH_WIFI_GUEST_NETWORK}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.is_state(SWITCH_WIFI_GUEST_NETWORK, STATE_OFF) + hass.data[DOMAIN].routers[ + "test-serial-number" + ].client.wlan.wifi_guest_network_switch.assert_called_with(False) From 010b18be34849baec7d956e96ae4bcb386d87025 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 8 Jul 2022 14:17:56 -0700 Subject: [PATCH 2295/3516] Bump atomicwrites (#74758) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 4 ++-- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index d5d0c2c0370..4987169d280 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.54.0"], + "requirements": ["hass-nabucasa==0.54.1"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a388f178df3..8f0e2087c14 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiohttp_cors==0.7.0 astral==2.2 async-upnp-client==0.31.2 async_timeout==4.0.2 -atomicwrites==1.4.0 +atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 @@ -14,7 +14,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 -hass-nabucasa==0.54.0 +hass-nabucasa==0.54.1 home-assistant-frontend==20220707.0 httpx==0.23.0 ifaddr==0.1.7 diff --git a/pyproject.toml b/pyproject.toml index 60e0865fab9..621bbf68999 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "astral==2.2", "async_timeout==4.0.2", "attrs==21.2.0", - "atomicwrites==1.4.0", + "atomicwrites-homeassistant==1.4.1", "awesomeversion==22.6.0", "bcrypt==3.1.7", "certifi>=2021.5.30", diff --git a/requirements.txt b/requirements.txt index 7826a9a0f26..fefa0f33ecb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ aiohttp==3.8.1 astral==2.2 async_timeout==4.0.2 attrs==21.2.0 -atomicwrites==1.4.0 +atomicwrites-homeassistant==1.4.1 awesomeversion==22.6.0 bcrypt==3.1.7 certifi>=2021.5.30 diff --git a/requirements_all.txt b/requirements_all.txt index bdb840ea88a..7c5512c6c00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -795,7 +795,7 @@ habitipy==0.2.0 hangups==0.4.18 # homeassistant.components.cloud -hass-nabucasa==0.54.0 +hass-nabucasa==0.54.1 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d37ff39e79d..6c16b073632 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -577,7 +577,7 @@ habitipy==0.2.0 hangups==0.4.18 # homeassistant.components.cloud -hass-nabucasa==0.54.0 +hass-nabucasa==0.54.1 # homeassistant.components.tasmota hatasmota==0.5.1 From 7ffc60fb2c40b4c125f246bc8c74bcb223e8fd4b Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Fri, 8 Jul 2022 15:16:13 +0200 Subject: [PATCH 2296/3516] Add missing strings for here_travel_time (#74641) * Add missing strings for here_travel_time * script.translations develop * Correct origin_menu option --- homeassistant/components/here_travel_time/strings.json | 7 +++++++ .../components/here_travel_time/translations/en.json | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/homeassistant/components/here_travel_time/strings.json b/homeassistant/components/here_travel_time/strings.json index e4a20a38d6b..dab135efc82 100644 --- a/homeassistant/components/here_travel_time/strings.json +++ b/homeassistant/components/here_travel_time/strings.json @@ -8,6 +8,13 @@ "mode": "Travel Mode" } }, + "origin_menu": { + "title": "Choose Origin", + "menu_options": { + "origin_coordinates": "Using a map location", + "origin_entity": "Using an entity" + } + }, "origin_coordinates": { "title": "Choose Origin", "data": { diff --git a/homeassistant/components/here_travel_time/translations/en.json b/homeassistant/components/here_travel_time/translations/en.json index d4f9984d945..f31d5a3783d 100644 --- a/homeassistant/components/here_travel_time/translations/en.json +++ b/homeassistant/components/here_travel_time/translations/en.json @@ -39,6 +39,13 @@ }, "title": "Choose Origin" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Using a map location", + "origin_entity": "Using an entity" + }, + "title": "Choose Origin" + }, "user": { "data": { "api_key": "API Key", From dc33d5db827a9c42b594c136c04a5253231e8065 Mon Sep 17 00:00:00 2001 From: siyuan-nz <91467287+siyuan-nz@users.noreply.github.com> Date: Fri, 8 Jul 2022 19:51:10 +1200 Subject: [PATCH 2297/3516] Add ssh-rsa as acceptable an host key algorithm (#74684) --- homeassistant/components/unifi_direct/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/unifi_direct/device_tracker.py b/homeassistant/components/unifi_direct/device_tracker.py index f71498faa11..7a81975c0ba 100644 --- a/homeassistant/components/unifi_direct/device_tracker.py +++ b/homeassistant/components/unifi_direct/device_tracker.py @@ -80,7 +80,7 @@ class UnifiDeviceScanner(DeviceScanner): def _connect(self): """Connect to the Unifi AP SSH server.""" - self.ssh = pxssh.pxssh() + self.ssh = pxssh.pxssh(options={"HostKeyAlgorithms": "ssh-rsa"}) try: self.ssh.login( self.host, self.username, password=self.password, port=self.port From 88d723736f3a7430b7d76d0339f36dba0458270b Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Fri, 8 Jul 2022 18:47:59 +0200 Subject: [PATCH 2298/3516] Fix ZHA group not setting the correct color mode (#74687) * Fix ZHA group not setting the correct color mode * Changed to use _attr_color_mode --- homeassistant/components/zha/light.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 309fdf2699b..e1c85b39d8e 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -141,7 +141,7 @@ class BaseLight(LogMixin, light.LightEntity): self._color_channel = None self._identify_channel = None self._default_transition = None - self._color_mode = ColorMode.UNKNOWN # Set by sub classes + self._attr_color_mode = ColorMode.UNKNOWN # Set by sub classes @property def extra_state_attributes(self) -> dict[str, Any]: @@ -159,11 +159,6 @@ class BaseLight(LogMixin, light.LightEntity): return False return self._state - @property - def color_mode(self): - """Return the color mode of this light.""" - return self._color_mode - @property def brightness(self): """Return the brightness of this light.""" @@ -309,7 +304,7 @@ class BaseLight(LogMixin, light.LightEntity): if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return - self._color_mode = ColorMode.COLOR_TEMP + self._attr_color_mode = ColorMode.COLOR_TEMP self._color_temp = temperature self._hs_color = None @@ -323,7 +318,7 @@ class BaseLight(LogMixin, light.LightEntity): if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return - self._color_mode = ColorMode.HS + self._attr_color_mode = ColorMode.HS self._hs_color = hs_color self._color_temp = None @@ -451,13 +446,13 @@ class Light(BaseLight, ZhaEntity): self._attr_supported_color_modes ) if len(self._attr_supported_color_modes) == 1: - self._color_mode = next(iter(self._attr_supported_color_modes)) + self._attr_color_mode = next(iter(self._attr_supported_color_modes)) else: # Light supports color_temp + hs, determine which mode the light is in assert self._color_channel if self._color_channel.color_mode == Color.ColorMode.Color_temperature: - self._color_mode = ColorMode.COLOR_TEMP + self._attr_color_mode = ColorMode.COLOR_TEMP else: - self._color_mode = ColorMode.HS + self._attr_color_mode = ColorMode.HS if self._identify_channel: self._supported_features |= light.LightEntityFeature.FLASH @@ -518,7 +513,7 @@ class Light(BaseLight, ZhaEntity): if "off_brightness" in last_state.attributes: self._off_brightness = last_state.attributes["off_brightness"] if "color_mode" in last_state.attributes: - self._color_mode = ColorMode(last_state.attributes["color_mode"]) + self._attr_color_mode = ColorMode(last_state.attributes["color_mode"]) if "color_temp" in last_state.attributes: self._color_temp = last_state.attributes["color_temp"] if "hs_color" in last_state.attributes: @@ -558,13 +553,13 @@ class Light(BaseLight, ZhaEntity): if (color_mode := results.get("color_mode")) is not None: if color_mode == Color.ColorMode.Color_temperature: - self._color_mode = ColorMode.COLOR_TEMP + self._attr_color_mode = ColorMode.COLOR_TEMP color_temp = results.get("color_temperature") if color_temp is not None and color_mode: self._color_temp = color_temp self._hs_color = None else: - self._color_mode = ColorMode.HS + self._attr_color_mode = ColorMode.HS color_x = results.get("current_x") color_y = results.get("current_y") if color_x is not None and color_y is not None: @@ -650,7 +645,7 @@ class LightGroup(BaseLight, ZhaGroupEntity): CONF_DEFAULT_LIGHT_TRANSITION, 0, ) - self._color_mode = None + self._attr_color_mode = None async def async_added_to_hass(self): """Run when about to be added to hass.""" From e80fd4fc783e7928337b007849fe26d6c59d1e84 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 8 Jul 2022 12:54:09 +0200 Subject: [PATCH 2299/3516] Bump deconz dependency to fix #74523 (#74710) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_sensor.py | 31 +++++++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index c19d75ec054..06f8b6c0376 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==96"], + "requirements": ["pydeconz==97"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 4a35ee93e08..48d08ed0b89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1444,7 +1444,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==96 +pydeconz==97 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 89b53d2b583..31a657d9888 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -974,7 +974,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==96 +pydeconz==97 # homeassistant.components.dexcom pydexcom==0.2.3 diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 658e11da906..5f11a4d7b0b 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -816,6 +816,37 @@ async def test_dont_add_sensor_if_state_is_none( assert len(hass.states.async_all()) == 0 +async def test_air_quality_sensor_without_ppb(hass, aioclient_mock): + """Test sensor with scaled data is not created if state is None.""" + data = { + "sensors": { + "1": { + "config": { + "on": True, + "reachable": True, + }, + "ep": 2, + "etag": "c2d2e42396f7c78e11e46c66e2ec0200", + "lastseen": "2020-11-20T22:48Z", + "manufacturername": "BOSCH", + "modelid": "AIR", + "name": "BOSCH Air quality sensor", + "state": { + "airquality": "poor", + "lastupdated": "2020-11-20T22:48:00.209", + }, + "swversion": "20200402", + "type": "ZHAAirQuality", + "uniqueid": "00:00:00:00:00:00:00:00-02-fdef", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + await setup_deconz_integration(hass, aioclient_mock) + + assert len(hass.states.async_all()) == 1 + + async def test_add_battery_later(hass, aioclient_mock, mock_deconz_websocket): """Test that a sensor without an initial battery state creates a battery sensor once state exist.""" data = { From 7b1cad223d7d05c87b507d93cef652bd91800103 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 8 Jul 2022 14:17:56 -0700 Subject: [PATCH 2300/3516] Bump atomicwrites (#74758) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 4 ++-- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index d5d0c2c0370..4987169d280 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.54.0"], + "requirements": ["hass-nabucasa==0.54.1"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index de9df889eba..ad03978c6ef 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ aiohttp_cors==0.7.0 astral==2.2 async-upnp-client==0.31.2 async_timeout==4.0.2 -atomicwrites==1.4.0 +atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 @@ -14,7 +14,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 -hass-nabucasa==0.54.0 +hass-nabucasa==0.54.1 home-assistant-frontend==20220707.0 httpx==0.23.0 ifaddr==0.1.7 diff --git a/pyproject.toml b/pyproject.toml index 48e3ea452ff..3ed9ca223d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "astral==2.2", "async_timeout==4.0.2", "attrs==21.2.0", - "atomicwrites==1.4.0", + "atomicwrites-homeassistant==1.4.1", "awesomeversion==22.6.0", "bcrypt==3.1.7", "certifi>=2021.5.30", diff --git a/requirements.txt b/requirements.txt index 98b148fa923..c345cac25c9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ aiohttp==3.8.1 astral==2.2 async_timeout==4.0.2 attrs==21.2.0 -atomicwrites==1.4.0 +atomicwrites-homeassistant==1.4.1 awesomeversion==22.6.0 bcrypt==3.1.7 certifi>=2021.5.30 diff --git a/requirements_all.txt b/requirements_all.txt index 48d08ed0b89..daa4ca8ce3d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -795,7 +795,7 @@ habitipy==0.2.0 hangups==0.4.18 # homeassistant.components.cloud -hass-nabucasa==0.54.0 +hass-nabucasa==0.54.1 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 31a657d9888..cf5ae0519b9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -574,7 +574,7 @@ habitipy==0.2.0 hangups==0.4.18 # homeassistant.components.cloud -hass-nabucasa==0.54.0 +hass-nabucasa==0.54.1 # homeassistant.components.tasmota hatasmota==0.5.1 From 0cca086aab84d63db4364622bfd4a79bc75479a5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 8 Jul 2022 15:18:40 -0600 Subject: [PATCH 2301/3516] Bump regenmaschine to 2022.07.0 (#74680) --- homeassistant/components/rainmachine/__init__.py | 2 +- homeassistant/components/rainmachine/config_flow.py | 2 +- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 6d51be9d921..c285bf89e57 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -183,7 +183,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD], port=entry.data[CONF_PORT], - ssl=entry.data.get(CONF_SSL, DEFAULT_SSL), + use_ssl=entry.data.get(CONF_SSL, DEFAULT_SSL), ) except RainMachineError as err: raise ConfigEntryNotReady from err diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index d24dae46c2b..c12362591e7 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -32,7 +32,7 @@ async def async_get_controller( websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) try: - await client.load_local(ip_address, password, port=port, ssl=ssl) + await client.load_local(ip_address, password, port=port, use_ssl=ssl) except RainMachineError: return None else: diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index e9df60e4697..4f06ed0d71b 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.06.1"], + "requirements": ["regenmaschine==2022.07.0"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 7c5512c6c00..d4e792ee233 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2065,7 +2065,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.06.1 +regenmaschine==2022.07.0 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6c16b073632..4bc006c0b54 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1379,7 +1379,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rainmachine -regenmaschine==2022.06.1 +regenmaschine==2022.07.0 # homeassistant.components.renault renault-api==0.1.11 From 766523cf8cda9f3cae68a25d48388a62aa4919f0 Mon Sep 17 00:00:00 2001 From: Benoit Anastay <45088785+BenoitAnastay@users.noreply.github.com> Date: Fri, 8 Jul 2022 23:22:31 +0200 Subject: [PATCH 2302/3516] Fix error with HDD temperature report in Freebox integration (#74718) * Fix error whith HDD temperature report There was a non handled error case, documented in issue https://github.com/home-assistant/core/issues/43812 back in 2020 and the fix wasn't applied * Use get method instead of ignoring the sensor * Update test values Add idle state drive with unkown temp * update Tests for system sensors api * Fix booleans values * Fix disk unique_id There was a typo in the code --- homeassistant/components/freebox/router.py | 2 +- homeassistant/components/freebox/sensor.py | 2 +- tests/components/freebox/const.py | 38 +++++++++++++++++++++- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 70fc7b86a40..ce4d03aae7a 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -113,7 +113,7 @@ class FreeboxRouter: # According to the doc `syst_datas["sensors"]` is temperature sensors in celsius degree. # Name and id of sensors may vary under Freebox devices. for sensor in syst_datas["sensors"]: - self.sensors_temperature[sensor["name"]] = sensor["value"] + self.sensors_temperature[sensor["name"]] = sensor.get("value") # Connection sensors connection_datas: dict[str, Any] = await self._api.connection.get_status() diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 46aa9ee8aa0..450456b9146 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -159,7 +159,7 @@ class FreeboxDiskSensor(FreeboxSensor): self._disk = disk self._partition = partition self._attr_name = f"{partition['label']} {description.name}" - self._unique_id = f"{self._router.mac} {description.key} {self._disk['id']} {self._partition['id']}" + self._attr_unique_id = f"{self._router.mac} {description.key} {self._disk['id']} {self._partition['id']}" @property def device_info(self) -> DeviceInfo: diff --git a/tests/components/freebox/const.py b/tests/components/freebox/const.py index cc3d720d7ef..25402cbcdef 100644 --- a/tests/components/freebox/const.py +++ b/tests/components/freebox/const.py @@ -22,6 +22,7 @@ DATA_SYSTEM_GET_CONFIG = { "fans": [{"id": "fan0_speed", "name": "Ventilateur 1", "value": 2130}], "sensors": [ {"id": "temp_hdd", "name": "Disque dur", "value": 40}, + {"id": "temp_hdd2", "name": "Disque dur 2"}, {"id": "temp_sw", "name": "Température Switch", "value": 50}, {"id": "temp_cpum", "name": "Température CPU M", "value": 60}, {"id": "temp_cpub", "name": "Température CPU B", "value": 56}, @@ -123,7 +124,42 @@ DATA_STORAGE_GET_DISKS = [ "path": "L0Rpc3F1ZSBkdXI=", } ], - } + }, + { + "idle_duration": 8290, + "read_error_requests": 0, + "read_requests": 2326826, + "spinning": False, + "table_type": "gpt", + "firmware": "0001", + "type": "sata", + "idle": True, + "connector": 0, + "id": 2000, + "write_error_requests": 0, + "state": "enabled", + "write_requests": 122733632, + "total_bytes": 2000000000000, + "model": "ST2000LM015-2E8174", + "active_duration": 0, + "temp": 0, + "serial": "WDZYJ27Q", + "partitions": [ + { + "fstype": "ext4", + "total_bytes": 1960000000000, + "label": "Disque 2", + "id": 2001, + "internal": False, + "fsck_result": "no_run_yet", + "state": "mounted", + "disk_id": 2000, + "free_bytes": 1880000000000, + "used_bytes": 85410000000, + "path": "L0Rpc3F1ZSAy", + } + ], + }, ] # switch From cb5658d7dc7ac9368b506aa15fd2a5bcd8c9c59b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 8 Jul 2022 15:18:40 -0600 Subject: [PATCH 2303/3516] Bump regenmaschine to 2022.07.0 (#74680) --- homeassistant/components/rainmachine/__init__.py | 2 +- homeassistant/components/rainmachine/config_flow.py | 2 +- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 6d51be9d921..c285bf89e57 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -183,7 +183,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD], port=entry.data[CONF_PORT], - ssl=entry.data.get(CONF_SSL, DEFAULT_SSL), + use_ssl=entry.data.get(CONF_SSL, DEFAULT_SSL), ) except RainMachineError as err: raise ConfigEntryNotReady from err diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index d24dae46c2b..c12362591e7 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -32,7 +32,7 @@ async def async_get_controller( websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) try: - await client.load_local(ip_address, password, port=port, ssl=ssl) + await client.load_local(ip_address, password, port=port, use_ssl=ssl) except RainMachineError: return None else: diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index e9df60e4697..4f06ed0d71b 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.06.1"], + "requirements": ["regenmaschine==2022.07.0"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index daa4ca8ce3d..d7c0b8124f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2065,7 +2065,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.06.1 +regenmaschine==2022.07.0 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf5ae0519b9..83301dc1c4c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1376,7 +1376,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rainmachine -regenmaschine==2022.06.1 +regenmaschine==2022.07.0 # homeassistant.components.renault renault-api==0.1.11 From ea709912d493309a3b87baed9749b591a83ee3bd Mon Sep 17 00:00:00 2001 From: Benoit Anastay <45088785+BenoitAnastay@users.noreply.github.com> Date: Fri, 8 Jul 2022 23:22:31 +0200 Subject: [PATCH 2304/3516] Fix error with HDD temperature report in Freebox integration (#74718) * Fix error whith HDD temperature report There was a non handled error case, documented in issue https://github.com/home-assistant/core/issues/43812 back in 2020 and the fix wasn't applied * Use get method instead of ignoring the sensor * Update test values Add idle state drive with unkown temp * update Tests for system sensors api * Fix booleans values * Fix disk unique_id There was a typo in the code --- homeassistant/components/freebox/router.py | 2 +- homeassistant/components/freebox/sensor.py | 2 +- tests/components/freebox/const.py | 38 +++++++++++++++++++++- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 70fc7b86a40..ce4d03aae7a 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -113,7 +113,7 @@ class FreeboxRouter: # According to the doc `syst_datas["sensors"]` is temperature sensors in celsius degree. # Name and id of sensors may vary under Freebox devices. for sensor in syst_datas["sensors"]: - self.sensors_temperature[sensor["name"]] = sensor["value"] + self.sensors_temperature[sensor["name"]] = sensor.get("value") # Connection sensors connection_datas: dict[str, Any] = await self._api.connection.get_status() diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 46aa9ee8aa0..450456b9146 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -159,7 +159,7 @@ class FreeboxDiskSensor(FreeboxSensor): self._disk = disk self._partition = partition self._attr_name = f"{partition['label']} {description.name}" - self._unique_id = f"{self._router.mac} {description.key} {self._disk['id']} {self._partition['id']}" + self._attr_unique_id = f"{self._router.mac} {description.key} {self._disk['id']} {self._partition['id']}" @property def device_info(self) -> DeviceInfo: diff --git a/tests/components/freebox/const.py b/tests/components/freebox/const.py index cc3d720d7ef..25402cbcdef 100644 --- a/tests/components/freebox/const.py +++ b/tests/components/freebox/const.py @@ -22,6 +22,7 @@ DATA_SYSTEM_GET_CONFIG = { "fans": [{"id": "fan0_speed", "name": "Ventilateur 1", "value": 2130}], "sensors": [ {"id": "temp_hdd", "name": "Disque dur", "value": 40}, + {"id": "temp_hdd2", "name": "Disque dur 2"}, {"id": "temp_sw", "name": "Température Switch", "value": 50}, {"id": "temp_cpum", "name": "Température CPU M", "value": 60}, {"id": "temp_cpub", "name": "Température CPU B", "value": 56}, @@ -123,7 +124,42 @@ DATA_STORAGE_GET_DISKS = [ "path": "L0Rpc3F1ZSBkdXI=", } ], - } + }, + { + "idle_duration": 8290, + "read_error_requests": 0, + "read_requests": 2326826, + "spinning": False, + "table_type": "gpt", + "firmware": "0001", + "type": "sata", + "idle": True, + "connector": 0, + "id": 2000, + "write_error_requests": 0, + "state": "enabled", + "write_requests": 122733632, + "total_bytes": 2000000000000, + "model": "ST2000LM015-2E8174", + "active_duration": 0, + "temp": 0, + "serial": "WDZYJ27Q", + "partitions": [ + { + "fstype": "ext4", + "total_bytes": 1960000000000, + "label": "Disque 2", + "id": 2001, + "internal": False, + "fsck_result": "no_run_yet", + "state": "mounted", + "disk_id": 2000, + "free_bytes": 1880000000000, + "used_bytes": 85410000000, + "path": "L0Rpc3F1ZSAy", + } + ], + }, ] # switch From 14c6b8d41f651d53d9df14c7ccaa8f3d81d8d314 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 8 Jul 2022 14:22:45 -0700 Subject: [PATCH 2305/3516] Bumped version to 2022.7.2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 6902a728ea7..796ae18f58d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 3ed9ca223d4..68b4758a688 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.1" +version = "2022.7.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From a3abe7456e3399d0e903fd03869bc6f68c27a8e0 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 2 Jul 2022 21:20:40 +0300 Subject: [PATCH 2306/3516] Fix CI failure due to integrations leaving dirty known_devices.yaml (#74329) --- tests/components/demo/test_init.py | 13 +++++-------- tests/components/device_tracker/test_init.py | 13 ++++++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index b00028e34d9..fa0aff8223b 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -1,12 +1,10 @@ """The tests for the Demo component.""" -from contextlib import suppress import json -import os +from unittest.mock import patch import pytest from homeassistant.components.demo import DOMAIN -from homeassistant.components.device_tracker.legacy import YAML_DEVICES from homeassistant.components.recorder import get_instance from homeassistant.components.recorder.statistics import list_statistic_ids from homeassistant.helpers.json import JSONEncoder @@ -22,11 +20,10 @@ def mock_history(hass): @pytest.fixture(autouse=True) -def demo_cleanup(hass): - """Clean up device tracker demo file.""" - yield - with suppress(FileNotFoundError): - os.remove(hass.config.path(YAML_DEVICES)) +def mock_device_tracker_update_config(hass): + """Prevent device tracker from creating known devices file.""" + with patch("homeassistant.components.device_tracker.legacy.update_config"): + yield async def test_setting_up_demo(hass): diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 0953fc67b0a..d8914032f36 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -28,6 +28,8 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from . import common + from tests.common import ( assert_setup_component, async_fire_time_changed, @@ -35,7 +37,6 @@ from tests.common import ( mock_restore_cache, patch_yaml_files, ) -from tests.components.device_tracker import common TEST_PLATFORM = {device_tracker.DOMAIN: {CONF_PLATFORM: "test"}} @@ -165,6 +166,7 @@ async def test_setup_without_yaml_file(hass, enable_custom_integrations): """Test with no YAML file.""" with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) + await hass.async_block_till_done() async def test_gravatar(hass): @@ -210,10 +212,11 @@ async def test_gravatar_and_picture(hass): @patch("homeassistant.components.demo.device_tracker.setup_scanner", autospec=True) async def test_discover_platform(mock_demo_setup_scanner, mock_see, hass): """Test discovery of device_tracker demo platform.""" - await discovery.async_load_platform( - hass, device_tracker.DOMAIN, "demo", {"test_key": "test_val"}, {"bla": {}} - ) - await hass.async_block_till_done() + with patch("homeassistant.components.device_tracker.legacy.update_config"): + await discovery.async_load_platform( + hass, device_tracker.DOMAIN, "demo", {"test_key": "test_val"}, {"bla": {}} + ) + await hass.async_block_till_done() assert device_tracker.DOMAIN in hass.config.components assert mock_demo_setup_scanner.called assert mock_demo_setup_scanner.call_args[0] == ( From c27fbce7d09a8a067c8915cee56efd53a5aa2323 Mon Sep 17 00:00:00 2001 From: kpine Date: Fri, 8 Jul 2022 15:20:44 -0700 Subject: [PATCH 2307/3516] Fix KeyError from zwave_js diagnostics (#74579) --- .../components/zwave_js/diagnostics.py | 29 ++++--- tests/components/zwave_js/test_diagnostics.py | 80 ++++++++++++++++++- 2 files changed, 92 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 3372b0eeec0..078bd761b71 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -94,20 +94,23 @@ def get_device_entities( # If the value ID returns as None, we don't need to include this entity if (value_id := get_value_id_from_unique_id(entry.unique_id)) is None: continue - state_key = get_state_key_from_unique_id(entry.unique_id) - zwave_value = node.values[value_id] - primary_value_data = { - "command_class": zwave_value.command_class, - "command_class_name": zwave_value.command_class_name, - "endpoint": zwave_value.endpoint, - "property": zwave_value.property_, - "property_name": zwave_value.property_name, - "property_key": zwave_value.property_key, - "property_key_name": zwave_value.property_key_name, - } - if state_key is not None: - primary_value_data["state_key"] = state_key + primary_value_data = None + if (zwave_value := node.values.get(value_id)) is not None: + primary_value_data = { + "command_class": zwave_value.command_class, + "command_class_name": zwave_value.command_class_name, + "endpoint": zwave_value.endpoint, + "property": zwave_value.property_, + "property_name": zwave_value.property_name, + "property_key": zwave_value.property_key, + "property_key_name": zwave_value.property_key_name, + } + + state_key = get_state_key_from_unique_id(entry.unique_id) + if state_key is not None: + primary_value_data["state_key"] = state_key + entity = { "domain": entry.domain, "entity_id": entry.entity_id, diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index 3ac3f32b45a..9f3a7b0884c 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -10,8 +10,12 @@ from homeassistant.components.zwave_js.diagnostics import ( async_get_device_diagnostics, ) from homeassistant.components.zwave_js.discovery import async_discover_node_values -from homeassistant.components.zwave_js.helpers import get_device_id -from homeassistant.helpers.device_registry import async_get +from homeassistant.components.zwave_js.helpers import ( + get_device_id, + get_value_id_from_unique_id, +) +from homeassistant.helpers.device_registry import async_get as async_get_dev_reg +from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg from .common import PROPERTY_ULTRAVIOLET @@ -53,7 +57,7 @@ async def test_device_diagnostics( version_state, ): """Test the device level diagnostics data dump.""" - dev_reg = async_get(hass) + dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device({get_device_id(client.driver, multisensor_6)}) assert device @@ -106,7 +110,7 @@ async def test_device_diagnostics( async def test_device_diagnostics_error(hass, integration): """Test the device diagnostics raises exception when an invalid device is used.""" - dev_reg = async_get(hass) + dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_or_create( config_entry_id=integration.entry_id, identifiers={("test", "test")} ) @@ -118,3 +122,71 @@ async def test_empty_zwave_value_matcher(): """Test empty ZwaveValueMatcher is invalid.""" with pytest.raises(ValueError): ZwaveValueMatcher() + + +async def test_device_diagnostics_missing_primary_value( + hass, + client, + multisensor_6, + integration, + hass_client, +): + """Test that the device diagnostics handles an entity with a missing primary value.""" + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get_device({get_device_id(client.driver, multisensor_6)}) + assert device + + entity_id = "sensor.multisensor_6_air_temperature" + ent_reg = async_get_ent_reg(hass) + entry = ent_reg.async_get(entity_id) + + # check that the primary value for the entity exists in the diagnostics + diagnostics_data = await get_diagnostics_for_device( + hass, hass_client, integration, device + ) + + value = multisensor_6.values.get(get_value_id_from_unique_id(entry.unique_id)) + assert value + + air_entity = next( + x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id + ) + + assert air_entity["primary_value"] == { + "command_class": value.command_class, + "command_class_name": value.command_class_name, + "endpoint": value.endpoint, + "property": value.property_, + "property_name": value.property_name, + "property_key": value.property_key, + "property_key_name": value.property_key_name, + } + + # make the entity's primary value go missing + event = Event( + type="value removed", + data={ + "source": "node", + "event": "value removed", + "nodeId": multisensor_6.node_id, + "args": { + "commandClassName": value.command_class_name, + "commandClass": value.command_class, + "endpoint": value.endpoint, + "property": value.property_, + "prevValue": 0, + "propertyName": value.property_name, + }, + }, + ) + multisensor_6.receive_event(event) + + diagnostics_data = await get_diagnostics_for_device( + hass, hass_client, integration, device + ) + + air_entity = next( + x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id + ) + + assert air_entity["primary_value"] is None From a697672944b8bbdd32e3dad8eca93bd9ae8b40a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 8 Jul 2022 18:55:31 -0500 Subject: [PATCH 2308/3516] Add bluetooth integration (#74653) Co-authored-by: Paulus Schoutsen --- .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/bootstrap.py | 2 +- .../components/bluetooth/__init__.py | 297 ++++++++++++ homeassistant/components/bluetooth/const.py | 3 + .../components/bluetooth/manifest.json | 10 + homeassistant/components/bluetooth/models.py | 142 ++++++ homeassistant/components/bluetooth/usage.py | 13 + .../components/switchbot/manifest.json | 1 + homeassistant/config_entries.py | 9 + homeassistant/generated/bluetooth.py | 14 + homeassistant/helpers/config_entry_flow.py | 13 +- homeassistant/loader.py | 42 ++ mypy.ini | 11 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/hassfest/__main__.py | 2 + script/hassfest/bluetooth.py | 65 +++ script/hassfest/config_flow.py | 1 + script/hassfest/manifest.py | 10 + tests/components/bluetooth/__init__.py | 1 + tests/components/bluetooth/conftest.py | 25 + tests/components/bluetooth/test_init.py | 440 ++++++++++++++++++ tests/components/bluetooth/test_usage.py | 22 + tests/helpers/test_config_entry_flow.py | 3 + tests/test_config_entries.py | 1 + tests/test_loader.py | 45 ++ 27 files changed, 1179 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/bluetooth/__init__.py create mode 100644 homeassistant/components/bluetooth/const.py create mode 100644 homeassistant/components/bluetooth/manifest.json create mode 100644 homeassistant/components/bluetooth/models.py create mode 100644 homeassistant/components/bluetooth/usage.py create mode 100644 homeassistant/generated/bluetooth.py create mode 100644 script/hassfest/bluetooth.py create mode 100644 tests/components/bluetooth/__init__.py create mode 100644 tests/components/bluetooth/conftest.py create mode 100644 tests/components/bluetooth/test_init.py create mode 100644 tests/components/bluetooth/test_usage.py diff --git a/.strict-typing b/.strict-typing index c5ed000a7ce..becd5cdda9a 100644 --- a/.strict-typing +++ b/.strict-typing @@ -57,6 +57,7 @@ homeassistant.components.automation.* homeassistant.components.backup.* homeassistant.components.baf.* homeassistant.components.binary_sensor.* +homeassistant.components.bluetooth.* homeassistant.components.bluetooth_tracker.* homeassistant.components.bmw_connected_drive.* homeassistant.components.bond.* diff --git a/CODEOWNERS b/CODEOWNERS index fda94805214..4e5d07f54d8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -138,6 +138,8 @@ build.json @home-assistant/supervisor /homeassistant/components/blueprint/ @home-assistant/core /tests/components/blueprint/ @home-assistant/core /homeassistant/components/bluesound/ @thrawnarn +/homeassistant/components/bluetooth/ @bdraco +/tests/components/bluetooth/ @bdraco /homeassistant/components/bmw_connected_drive/ @gerard33 @rikroe /tests/components/bmw_connected_drive/ @gerard33 @rikroe /homeassistant/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 939d3073f57..5d19249e37b 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -70,7 +70,7 @@ LOGGING_INTEGRATIONS = { # To record data "recorder", } -DISCOVERY_INTEGRATIONS = ("dhcp", "ssdp", "usb", "zeroconf") +DISCOVERY_INTEGRATIONS = ("bluetooth", "dhcp", "ssdp", "usb", "zeroconf") STAGE_1_INTEGRATIONS = { # We need to make sure discovery integrations # update their deps before stage 2 integrations diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py new file mode 100644 index 00000000000..cf7f1884869 --- /dev/null +++ b/homeassistant/components/bluetooth/__init__.py @@ -0,0 +1,297 @@ +"""The bluetooth integration.""" +from __future__ import annotations + +from collections.abc import Callable +import dataclasses +from enum import Enum +import fnmatch +from functools import cached_property +import logging +import platform +from typing import Final + +from bleak import BleakError +from bleak.backends.device import MANUFACTURERS, BLEDevice +from bleak.backends.scanner import AdvertisementData +from lru import LRU # pylint: disable=no-name-in-module + +from homeassistant import config_entries +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HomeAssistant, + callback as hass_callback, +) +from homeassistant.data_entry_flow import BaseServiceInfo +from homeassistant.helpers import discovery_flow +from homeassistant.helpers.typing import ConfigType +from homeassistant.loader import BluetoothMatcher, async_get_bluetooth + +from . import models +from .const import DOMAIN +from .models import HaBleakScanner +from .usage import install_multiple_bleak_catcher + +_LOGGER = logging.getLogger(__name__) + +MAX_REMEMBER_ADDRESSES: Final = 2048 + + +class BluetoothScanningMode(Enum): + """The mode of scanning for bluetooth devices.""" + + PASSIVE = "passive" + ACTIVE = "active" + + +SCANNING_MODE_TO_BLEAK = { + BluetoothScanningMode.ACTIVE: "active", + BluetoothScanningMode.PASSIVE: "passive", +} + +LOCAL_NAME: Final = "local_name" +SERVICE_UUID: Final = "service_uuid" +MANUFACTURER_ID: Final = "manufacturer_id" +MANUFACTURER_DATA_FIRST_BYTE: Final = "manufacturer_data_first_byte" + + +@dataclasses.dataclass +class BluetoothServiceInfo(BaseServiceInfo): + """Prepared info from bluetooth entries.""" + + name: str + address: str + rssi: int + manufacturer_data: dict[int, bytes] + service_data: dict[str, bytes] + service_uuids: list[str] + + @classmethod + def from_advertisement( + cls, device: BLEDevice, advertisement_data: AdvertisementData + ) -> BluetoothServiceInfo: + """Create a BluetoothServiceInfo from an advertisement.""" + return cls( + name=advertisement_data.local_name or device.name or device.address, + address=device.address, + rssi=device.rssi, + manufacturer_data=advertisement_data.manufacturer_data, + service_data=advertisement_data.service_data, + service_uuids=advertisement_data.service_uuids, + ) + + @cached_property + def manufacturer(self) -> str | None: + """Convert manufacturer data to a string.""" + for manufacturer in self.manufacturer_data: + if manufacturer in MANUFACTURERS: + name: str = MANUFACTURERS[manufacturer] + return name + return None + + @cached_property + def manufacturer_id(self) -> int | None: + """Get the first manufacturer id.""" + for manufacturer in self.manufacturer_data: + return manufacturer + return None + + +BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") +BluetoothCallback = Callable[[BluetoothServiceInfo, BluetoothChange], None] + + +@hass_callback +def async_register_callback( + hass: HomeAssistant, + callback: BluetoothCallback, + match_dict: BluetoothMatcher | None, +) -> Callable[[], None]: + """Register to receive a callback on bluetooth change. + + Returns a callback that can be used to cancel the registration. + """ + manager: BluetoothManager = hass.data[DOMAIN] + return manager.async_register_callback(callback, match_dict) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the bluetooth integration.""" + integration_matchers = await async_get_bluetooth(hass) + bluetooth_discovery = BluetoothManager( + hass, integration_matchers, BluetoothScanningMode.PASSIVE + ) + await bluetooth_discovery.async_setup() + hass.data[DOMAIN] = bluetooth_discovery + return True + + +def _ble_device_matches( + matcher: BluetoothMatcher, device: BLEDevice, advertisement_data: AdvertisementData +) -> bool: + """Check if a ble device and advertisement_data matches the matcher.""" + if ( + matcher_local_name := matcher.get(LOCAL_NAME) + ) is not None and not fnmatch.fnmatch( + advertisement_data.local_name or device.name or device.address, + matcher_local_name, + ): + return False + + if ( + matcher_service_uuid := matcher.get(SERVICE_UUID) + ) is not None and matcher_service_uuid not in advertisement_data.service_uuids: + return False + + if ( + (matcher_manfacturer_id := matcher.get(MANUFACTURER_ID)) is not None + and matcher_manfacturer_id not in advertisement_data.manufacturer_data + ): + return False + + if ( + matcher_manufacturer_data_first_byte := matcher.get( + MANUFACTURER_DATA_FIRST_BYTE + ) + ) is not None and not any( + matcher_manufacturer_data_first_byte == manufacturer_data[0] + for manufacturer_data in advertisement_data.manufacturer_data.values() + ): + return False + + return True + + +@hass_callback +def async_enable_rssi_updates() -> None: + """Bleak filters out RSSI updates by default on linux only.""" + # We want RSSI updates + if platform.system() == "Linux": + from bleak.backends.bluezdbus import ( # pylint: disable=import-outside-toplevel + scanner, + ) + + scanner._ADVERTISING_DATA_PROPERTIES.add( # pylint: disable=protected-access + "RSSI" + ) + + +class BluetoothManager: + """Manage Bluetooth.""" + + def __init__( + self, + hass: HomeAssistant, + integration_matchers: list[BluetoothMatcher], + scanning_mode: BluetoothScanningMode, + ) -> None: + """Init bluetooth discovery.""" + self.hass = hass + self.scanning_mode = scanning_mode + self._integration_matchers = integration_matchers + self.scanner: HaBleakScanner | None = None + self._cancel_device_detected: CALLBACK_TYPE | None = None + self._callbacks: list[tuple[BluetoothCallback, BluetoothMatcher | None]] = [] + # Some devices use a random address so we need to use + # an LRU to avoid memory issues. + self._matched: LRU = LRU(MAX_REMEMBER_ADDRESSES) + + async def async_setup(self) -> None: + """Set up BT Discovery.""" + try: + self.scanner = HaBleakScanner( + scanning_mode=SCANNING_MODE_TO_BLEAK[self.scanning_mode] + ) + except (FileNotFoundError, BleakError) as ex: + _LOGGER.warning( + "Could not create bluetooth scanner (is bluetooth present and enabled?): %s", + ex, + ) + return + async_enable_rssi_updates() + install_multiple_bleak_catcher(self.scanner) + # We have to start it right away as some integrations might + # need it straight away. + _LOGGER.debug("Starting bluetooth scanner") + self.scanner.register_detection_callback(self.scanner.async_callback_dispatcher) + self._cancel_device_detected = self.scanner.async_register_callback( + self._device_detected, {} + ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) + await self.scanner.start() + + @hass_callback + def _device_detected( + self, device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + matched_domains: set[str] | None = None + if device.address not in self._matched: + matched_domains = { + matcher["domain"] + for matcher in self._integration_matchers + if _ble_device_matches(matcher, device, advertisement_data) + } + if matched_domains: + self._matched[device.address] = True + _LOGGER.debug( + "Device detected: %s with advertisement_data: %s matched domains: %s", + device, + advertisement_data, + matched_domains, + ) + + if not matched_domains and not self._callbacks: + return + + service_info: BluetoothServiceInfo | None = None + for callback, matcher in self._callbacks: + if matcher is None or _ble_device_matches( + matcher, device, advertisement_data + ): + if service_info is None: + service_info = BluetoothServiceInfo.from_advertisement( + device, advertisement_data + ) + try: + callback(service_info, BluetoothChange.ADVERTISEMENT) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in bluetooth callback") + + if not matched_domains: + return + if service_info is None: + service_info = BluetoothServiceInfo.from_advertisement( + device, advertisement_data + ) + for domain in matched_domains: + discovery_flow.async_create_flow( + self.hass, + domain, + {"source": config_entries.SOURCE_BLUETOOTH}, + service_info, + ) + + @hass_callback + def async_register_callback( + self, callback: BluetoothCallback, match_dict: BluetoothMatcher | None = None + ) -> Callable[[], None]: + """Register a callback.""" + callback_entry = (callback, match_dict) + self._callbacks.append(callback_entry) + + @hass_callback + def _async_remove_callback() -> None: + self._callbacks.remove(callback_entry) + + return _async_remove_callback + + async def async_stop(self, event: Event) -> None: + """Stop bluetooth discovery.""" + if self._cancel_device_detected: + self._cancel_device_detected() + self._cancel_device_detected = None + if self.scanner: + await self.scanner.stop() + models.HA_BLEAK_SCANNER = None diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py new file mode 100644 index 00000000000..ca5777ccdc2 --- /dev/null +++ b/homeassistant/components/bluetooth/const.py @@ -0,0 +1,3 @@ +"""Constants for the Bluetooth integration.""" + +DOMAIN = "bluetooth" diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json new file mode 100644 index 00000000000..0cc11ee14b3 --- /dev/null +++ b/homeassistant/components/bluetooth/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "bluetooth", + "name": "Bluetooth", + "documentation": "https://www.home-assistant.io/integrations/bluetooth", + "dependencies": ["websocket_api"], + "quality_scale": "internal", + "requirements": ["bleak==0.14.3"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py new file mode 100644 index 00000000000..a2651c587f7 --- /dev/null +++ b/homeassistant/components/bluetooth/models.py @@ -0,0 +1,142 @@ +"""Models for bluetooth.""" +from __future__ import annotations + +import asyncio +import contextlib +import logging +from typing import Any, Final, cast + +from bleak import BleakScanner +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData, AdvertisementDataCallback +from lru import LRU # pylint: disable=no-name-in-module + +from homeassistant.core import CALLBACK_TYPE, callback as hass_callback + +_LOGGER = logging.getLogger(__name__) + +FILTER_UUIDS: Final = "UUIDs" + +HA_BLEAK_SCANNER: HaBleakScanner | None = None + +MAX_HISTORY_SIZE: Final = 512 + + +def _dispatch_callback( + callback: AdvertisementDataCallback, + filters: dict[str, set[str]], + device: BLEDevice, + advertisement_data: AdvertisementData, +) -> None: + """Dispatch the callback.""" + if not callback: + # Callback destroyed right before being called, ignore + return + + if (uuids := filters.get(FILTER_UUIDS)) and not uuids.intersection( + advertisement_data.service_uuids + ): + return + + try: + callback(device, advertisement_data) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in callback: %s", callback) + + +class HaBleakScanner(BleakScanner): # type: ignore[misc] + """BleakScanner that cannot be stopped.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the BleakScanner.""" + self._callbacks: list[ + tuple[AdvertisementDataCallback, dict[str, set[str]]] + ] = [] + self._history: LRU = LRU(MAX_HISTORY_SIZE) + super().__init__(*args, **kwargs) + + @hass_callback + def async_register_callback( + self, callback: AdvertisementDataCallback, filters: dict[str, set[str]] + ) -> CALLBACK_TYPE: + """Register a callback.""" + callback_entry = (callback, filters) + self._callbacks.append(callback_entry) + + @hass_callback + def _remove_callback() -> None: + self._callbacks.remove(callback_entry) + + # Replay the history since otherwise we miss devices + # that were already discovered before the callback was registered + # or we are in passive mode + for device, advertisement_data in self._history.values(): + _dispatch_callback(callback, filters, device, advertisement_data) + + return _remove_callback + + def async_callback_dispatcher( + self, device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Dispatch the callback. + + Here we get the actual callback from bleak and dispatch + it to all the wrapped HaBleakScannerWrapper classes + """ + self._history[device.address] = (device, advertisement_data) + for callback_filters in self._callbacks: + _dispatch_callback(*callback_filters, device, advertisement_data) + + +class HaBleakScannerWrapper(BleakScanner): # type: ignore[misc] + """A wrapper that uses the single instance.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the BleakScanner.""" + self._detection_cancel: CALLBACK_TYPE | None = None + self._mapped_filters: dict[str, set[str]] = {} + if "filters" in kwargs: + self._mapped_filters = {k: set(v) for k, v in kwargs["filters"].items()} + if "service_uuids" in kwargs: + self._mapped_filters[FILTER_UUIDS] = set(kwargs["service_uuids"]) + super().__init__(*args, **kwargs) + + async def stop(self, *args: Any, **kwargs: Any) -> None: + """Stop scanning for devices.""" + return + + async def start(self, *args: Any, **kwargs: Any) -> None: + """Start scanning for devices.""" + return + + def _cancel_callback(self) -> None: + """Cancel callback.""" + if self._detection_cancel: + self._detection_cancel() + self._detection_cancel = None + + @property + def discovered_devices(self) -> list[BLEDevice]: + """Return a list of discovered devices.""" + assert HA_BLEAK_SCANNER is not None + return cast(list[BLEDevice], HA_BLEAK_SCANNER.discovered_devices) + + def register_detection_callback(self, callback: AdvertisementDataCallback) -> None: + """Register a callback that is called when a device is discovered or has a property changed. + + This method takes the callback and registers it with the long running + scanner. + """ + self._cancel_callback() + super().register_detection_callback(callback) + assert HA_BLEAK_SCANNER is not None + self._detection_cancel = HA_BLEAK_SCANNER.async_register_callback( + self._callback, self._mapped_filters + ) + + def __del__(self) -> None: + """Delete the BleakScanner.""" + if self._detection_cancel: + # Nothing to do if event loop is already closed + with contextlib.suppress(RuntimeError): + asyncio.get_running_loop().call_soon_threadsafe(self._detection_cancel) diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py new file mode 100644 index 00000000000..e305576f97f --- /dev/null +++ b/homeassistant/components/bluetooth/usage.py @@ -0,0 +1,13 @@ +"""bluetooth usage utility to handle multiple instances.""" +from __future__ import annotations + +import bleak + +from . import models +from .models import HaBleakScanner, HaBleakScannerWrapper + + +def install_multiple_bleak_catcher(hass_bleak_scanner: HaBleakScanner) -> None: + """Wrap the bleak classes to return the shared instance if multiple instances are detected.""" + models.HA_BLEAK_SCANNER = hass_bleak_scanner + bleak.BleakScanner = HaBleakScannerWrapper diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index cb485ffd8a5..91ee8881a07 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -5,6 +5,7 @@ "requirements": ["PySwitchbot==0.14.0"], "config_flow": true, "codeowners": ["@danielhiversen", "@RenierM26"], + "bluetooth": [{ "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" }], "iot_class": "local_polling", "loggers": ["switchbot"] } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index c832bab7eb4..e7f65c38ec1 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -28,6 +28,7 @@ from .util import uuid as uuid_util from .util.decorator import Registry if TYPE_CHECKING: + from .components.bluetooth import BluetoothServiceInfo from .components.dhcp import DhcpServiceInfo from .components.hassio import HassioServiceInfo from .components.mqtt import MqttServiceInfo @@ -37,6 +38,7 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) +SOURCE_BLUETOOTH = "bluetooth" SOURCE_DHCP = "dhcp" SOURCE_DISCOVERY = "discovery" SOURCE_HASSIO = "hassio" @@ -116,6 +118,7 @@ class ConfigEntryState(Enum): DEFAULT_DISCOVERY_UNIQUE_ID = "default_discovery_unique_id" DISCOVERY_NOTIFICATION_ID = "config_entry_discovery" DISCOVERY_SOURCES = { + SOURCE_BLUETOOTH, SOURCE_DHCP, SOURCE_DISCOVERY, SOURCE_HOMEKIT, @@ -1460,6 +1463,12 @@ class ConfigFlow(data_entry_flow.FlowHandler): reason=reason, description_placeholders=description_placeholders ) + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> data_entry_flow.FlowResult: + """Handle a flow initialized by Bluetooth discovery.""" + return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + async def async_step_dhcp( self, discovery_info: DhcpServiceInfo ) -> data_entry_flow.FlowResult: diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py new file mode 100644 index 00000000000..49596c4773c --- /dev/null +++ b/homeassistant/generated/bluetooth.py @@ -0,0 +1,14 @@ +"""Automatically generated by hassfest. + +To update, run python3 -m script.hassfest +""" +from __future__ import annotations + +# fmt: off + +BLUETOOTH: list[dict[str, str | int]] = [ + { + "domain": "switchbot", + "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + } +] diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 3617c0b1f29..75a4dcd20f4 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -6,7 +6,7 @@ import logging from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, cast from homeassistant import config_entries -from homeassistant.components import dhcp, onboarding, ssdp, zeroconf +from homeassistant.components import bluetooth, dhcp, onboarding, ssdp, zeroconf from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult @@ -92,6 +92,17 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() + async def async_step_bluetooth( + self, discovery_info: bluetooth.BluetoothServiceInfo + ) -> FlowResult: + """Handle a flow initialized by bluetooth discovery.""" + if self._async_in_progress() or self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + await self.async_set_unique_id(self._domain) + + return await self.async_step_confirm() + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: """Handle a flow initialized by dhcp discovery.""" if self._async_in_progress() or self._async_current_entries(): diff --git a/homeassistant/loader.py b/homeassistant/loader.py index ab681d7c42d..0a65928701b 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -24,6 +24,7 @@ from awesomeversion import ( ) from .generated.application_credentials import APPLICATION_CREDENTIALS +from .generated.bluetooth import BLUETOOTH from .generated.dhcp import DHCP from .generated.mqtt import MQTT from .generated.ssdp import SSDP @@ -77,6 +78,25 @@ class DHCPMatcher(DHCPMatcherRequired, DHCPMatcherOptional): """Matcher for the dhcp integration.""" +class BluetoothMatcherRequired(TypedDict, total=True): + """Matcher for the bluetooth integration for required fields.""" + + domain: str + + +class BluetoothMatcherOptional(TypedDict, total=False): + """Matcher for the bluetooth integration for optional fields.""" + + local_name: str + service_uuid: str + manufacturer_id: int + manufacturer_data_first_byte: int + + +class BluetoothMatcher(BluetoothMatcherRequired, BluetoothMatcherOptional): + """Matcher for the bluetooth integration.""" + + class Manifest(TypedDict, total=False): """ Integration manifest. @@ -97,6 +117,7 @@ class Manifest(TypedDict, total=False): issue_tracker: str quality_scale: str iot_class: str + bluetooth: list[dict[str, int | str]] mqtt: list[str] ssdp: list[dict[str, str]] zeroconf: list[str | dict[str, str]] @@ -269,6 +290,22 @@ async def async_get_zeroconf( return zeroconf +async def async_get_bluetooth(hass: HomeAssistant) -> list[BluetoothMatcher]: + """Return cached list of bluetooth types.""" + bluetooth = cast(list[BluetoothMatcher], BLUETOOTH.copy()) + + integrations = await async_get_custom_components(hass) + for integration in integrations.values(): + if not integration.bluetooth: + continue + for entry in integration.bluetooth: + bluetooth.append( + cast(BluetoothMatcher, {"domain": integration.domain, **entry}) + ) + + return bluetooth + + async def async_get_dhcp(hass: HomeAssistant) -> list[DHCPMatcher]: """Return cached list of dhcp types.""" dhcp = cast(list[DHCPMatcher], DHCP.copy()) @@ -519,6 +556,11 @@ class Integration: """Return Integration zeroconf entries.""" return self.manifest.get("zeroconf") + @property + def bluetooth(self) -> list[dict[str, str | int]] | None: + """Return Integration bluetooth entries.""" + return self.manifest.get("bluetooth") + @property def dhcp(self) -> list[dict[str, str | bool]] | None: """Return Integration dhcp entries.""" diff --git a/mypy.ini b/mypy.ini index c47413b4af8..a883007de0d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -390,6 +390,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.bluetooth.*] +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.bluetooth_tracker.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index d4e792ee233..766ceea6e07 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -401,6 +401,9 @@ bimmer_connected==0.9.6 # homeassistant.components.bizkaibus bizkaibus==0.1.1 +# homeassistant.components.bluetooth +bleak==0.14.3 + # homeassistant.components.blebox blebox_uniapi==2.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4bc006c0b54..30cbba02ff4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -316,6 +316,9 @@ bellows==0.31.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.9.6 +# homeassistant.components.bluetooth +bleak==0.14.3 + # homeassistant.components.blebox blebox_uniapi==2.0.1 diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 889cad2a497..4bc30583d47 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -6,6 +6,7 @@ from time import monotonic from . import ( application_credentials, + bluetooth, codeowners, config_flow, coverage, @@ -27,6 +28,7 @@ from .model import Config, Integration INTEGRATION_PLUGINS = [ application_credentials, + bluetooth, codeowners, config_flow, dependencies, diff --git a/script/hassfest/bluetooth.py b/script/hassfest/bluetooth.py new file mode 100644 index 00000000000..77a8779efbd --- /dev/null +++ b/script/hassfest/bluetooth.py @@ -0,0 +1,65 @@ +"""Generate bluetooth file.""" +from __future__ import annotations + +import json + +from .model import Config, Integration + +BASE = """ +\"\"\"Automatically generated by hassfest. + +To update, run python3 -m script.hassfest +\"\"\" +from __future__ import annotations + +# fmt: off + +BLUETOOTH: list[dict[str, str | int]] = {} +""".strip() + + +def generate_and_validate(integrations: list[dict[str, str]]): + """Validate and generate bluetooth data.""" + match_list = [] + + for domain in sorted(integrations): + integration = integrations[domain] + + if not integration.manifest or not integration.config_flow: + continue + + match_types = integration.manifest.get("bluetooth", []) + + if not match_types: + continue + + for entry in match_types: + match_list.append({"domain": domain, **entry}) + + return BASE.format(json.dumps(match_list, indent=4)) + + +def validate(integrations: dict[str, Integration], config: Config): + """Validate bluetooth file.""" + bluetooth_path = config.root / "homeassistant/generated/bluetooth.py" + config.cache["bluetooth"] = content = generate_and_validate(integrations) + + if config.specific_integrations: + return + + with open(str(bluetooth_path)) as fp: + current = fp.read().strip() + if current != content: + config.add_error( + "bluetooth", + "File bluetooth.py is not up to date. Run python3 -m script.hassfest", + fixable=True, + ) + return + + +def generate(integrations: dict[str, Integration], config: Config): + """Generate bluetooth file.""" + bluetooth_path = config.root / "homeassistant/generated/bluetooth.py" + with open(str(bluetooth_path), "w") as fp: + fp.write(f"{config.cache['bluetooth']}\n") diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index 169ccedf4a1..ad4a1d79229 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -35,6 +35,7 @@ def validate_integration(config: Config, integration: Integration): needs_unique_id = integration.domain not in UNIQUE_ID_IGNORE and ( "async_step_discovery" in config_flow + or "async_step_bluetooth" in config_flow or "async_step_hassio" in config_flow or "async_step_homekit" in config_flow or "async_step_mqtt" in config_flow diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index b847d384361..4f76fb9ed1e 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -190,6 +190,16 @@ MANIFEST_SCHEMA = vol.Schema( vol.Optional("ssdp"): vol.Schema( vol.All([vol.All(vol.Schema({}, extra=vol.ALLOW_EXTRA), vol.Length(min=1))]) ), + vol.Optional("bluetooth"): [ + vol.Schema( + { + vol.Optional("service_uuid"): vol.All(str, verify_lowercase), + vol.Optional("local_name"): vol.All(str), + vol.Optional("manufacturer_id"): int, + vol.Optional("manufacturer_data_first_byte"): int, + } + ) + ], vol.Optional("homekit"): vol.Schema({vol.Optional("models"): [str]}), vol.Optional("dhcp"): [ vol.Schema( diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py new file mode 100644 index 00000000000..6bf53afcd1e --- /dev/null +++ b/tests/components/bluetooth/__init__.py @@ -0,0 +1 @@ +"""Tests for the Bluetooth integration.""" diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py new file mode 100644 index 00000000000..fc0bd85b795 --- /dev/null +++ b/tests/components/bluetooth/conftest.py @@ -0,0 +1,25 @@ +"""Tests for the bluetooth component.""" + +import threading + +import pytest + +from tests.common import INSTANCES + + +@pytest.fixture(autouse=True) +def verify_cleanup(): + """Verify that the test has cleaned up resources correctly.""" + threads_before = frozenset(threading.enumerate()) + + yield + + if len(INSTANCES) >= 2: + count = len(INSTANCES) + for inst in INSTANCES: + inst.stop() + pytest.exit(f"Detected non stopped instances ({count}), aborting test run") + + threads = frozenset(threading.enumerate()) - threads_before + for thread in threads: + assert isinstance(thread, threading._DummyThread) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py new file mode 100644 index 00000000000..1e0647df01c --- /dev/null +++ b/tests/components/bluetooth/test_init.py @@ -0,0 +1,440 @@ +"""Tests for the Bluetooth integration.""" +from unittest.mock import AsyncMock, MagicMock, patch + +import bleak +from bleak import BleakError +from bleak.backends.scanner import AdvertisementData, BLEDevice +import pytest + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth import ( + BluetoothChange, + BluetoothServiceInfo, + models, +) +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP +from homeassistant.setup import async_setup_component + + +@pytest.fixture() +def mock_bleak_scanner_start(): + """Fixture to mock starting the bleak scanner.""" + scanner = bleak.BleakScanner + models.HA_BLEAK_SCANNER = None + + with patch("homeassistant.components.bluetooth.HaBleakScanner.stop"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + ) as mock_bleak_scanner_start: + yield mock_bleak_scanner_start + + # We need to drop the stop method from the object since we patched + # out start and this fixture will expire before the stop method is called + # when EVENT_HOMEASSISTANT_STOP is fired. + if models.HA_BLEAK_SCANNER: + models.HA_BLEAK_SCANNER.stop = AsyncMock() + bleak.BleakScanner = scanner + + +async def test_setup_and_stop(hass, mock_bleak_scanner_start): + """Test we and setup and stop the scanner.""" + mock_bt = [ + {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + +async def test_setup_and_stop_no_bluetooth(hass, caplog): + """Test we fail gracefully when bluetooth is not available.""" + mock_bt = [ + {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} + ] + with patch( + "homeassistant.components.bluetooth.HaBleakScanner", side_effect=BleakError + ) as mock_ha_bleak_scanner, patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object( + hass.config_entries.flow, "async_init" + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert len(mock_ha_bleak_scanner.mock_calls) == 1 + assert "Could not create bluetooth scanner" in caplog.text + + +async def test_discovery_match_by_service_uuid(hass, mock_bleak_scanner_start): + """Test bluetooth discovery match by service_uuid.""" + mock_bt = [ + {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") + wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) + + models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 0 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "switchbot" + + +async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): + """Test bluetooth discovery match by local_name.""" + mock_bt = [{"domain": "switchbot", "local_name": "wohand"}] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") + wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) + + models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 0 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) + + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "switchbot" + + +async def test_discovery_match_by_manufacturer_id_and_first_byte( + hass, mock_bleak_scanner_start +): + """Test bluetooth discovery match by manufacturer_id and manufacturer_data_first_byte.""" + mock_bt = [ + { + "domain": "homekit_controller", + "manufacturer_id": 76, + "manufacturer_data_first_byte": 0x06, + } + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + hkc_device = BLEDevice("44:44:33:11:23:45", "lock") + hkc_adv = AdvertisementData( + local_name="lock", service_uuids=[], manufacturer_data={76: b"\x06"} + ) + + models.HA_BLEAK_SCANNER._callback(hkc_device, hkc_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "homekit_controller" + mock_config_flow.reset_mock() + + # 2nd discovery should not generate another flow + models.HA_BLEAK_SCANNER._callback(hkc_device, hkc_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 0 + + mock_config_flow.reset_mock() + not_hkc_device = BLEDevice("44:44:33:11:23:21", "lock") + not_hkc_adv = AdvertisementData( + local_name="lock", service_uuids=[], manufacturer_data={76: b"\x02"} + ) + + models.HA_BLEAK_SCANNER._callback(not_hkc_device, not_hkc_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 0 + not_apple_device = BLEDevice("44:44:33:11:23:23", "lock") + not_apple_adv = AdvertisementData( + local_name="lock", service_uuids=[], manufacturer_data={21: b"\x02"} + ) + + models.HA_BLEAK_SCANNER._callback(not_apple_device, not_apple_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 0 + + +async def test_register_callbacks(hass, mock_bleak_scanner_start): + """Test configured options for a device are loaded via config entry.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + if len(callbacks) >= 3: + raise ValueError + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {"service_uuids": {"cba20d00-224d-11e6-9fb8-0002a5d5c51b"}}, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + await hass.async_block_till_done() + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + # 3rd callback raises ValueError but is still tracked + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + await hass.async_block_till_done() + + cancel() + + # 4th callback should not be tracked since we canceled + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + await hass.async_block_till_done() + + assert len(callbacks) == 3 + + service_info: BluetoothServiceInfo = callbacks[0][0] + assert service_info.name == "wohand" + assert service_info.manufacturer == "Nordic Semiconductor ASA" + assert service_info.manufacturer_id == 89 + + service_info: BluetoothServiceInfo = callbacks[1][0] + assert service_info.name == "empty" + assert service_info.manufacturer is None + assert service_info.manufacturer_id is None + + service_info: BluetoothServiceInfo = callbacks[2][0] + assert service_info.name == "empty" + assert service_info.manufacturer is None + assert service_info.manufacturer_id is None + + +async def test_wrapped_instance_with_filter(hass, mock_bleak_scanner_start): + """Test consumers can use the wrapped instance with a filter as if it was normal BleakScanner.""" + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + detected = [] + + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + assert models.HA_BLEAK_SCANNER is not None + scanner = models.HaBleakScannerWrapper( + filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} + ) + scanner.register_detection_callback(_device_detected) + + mock_discovered = [MagicMock()] + type(models.HA_BLEAK_SCANNER).discovered_devices = mock_discovered + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + discovered = await scanner.discover(timeout=0) + assert len(discovered) == 1 + assert discovered == mock_discovered + assert len(detected) == 1 + + scanner.register_detection_callback(_device_detected) + # We should get a reply from the history when we register again + assert len(detected) == 2 + scanner.register_detection_callback(_device_detected) + # We should get a reply from the history when we register again + assert len(detected) == 3 + + type(models.HA_BLEAK_SCANNER).discovered_devices = [] + discovered = await scanner.discover(timeout=0) + assert len(discovered) == 0 + assert discovered == [] + + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + assert len(detected) == 4 + + # The filter we created in the wrapped scanner with should be respected + # and we should not get another callback + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + assert len(detected) == 4 + + +async def test_wrapped_instance_with_service_uuids(hass, mock_bleak_scanner_start): + """Test consumers can use the wrapped instance with a service_uuids list as if it was normal BleakScanner.""" + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + detected = [] + + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + assert models.HA_BLEAK_SCANNER is not None + scanner = models.HaBleakScannerWrapper( + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + scanner.register_detection_callback(_device_detected) + + type(models.HA_BLEAK_SCANNER).discovered_devices = [MagicMock()] + for _ in range(2): + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(detected) == 2 + + # The UUIDs list we created in the wrapped scanner with should be respected + # and we should not get another callback + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + assert len(detected) == 2 + + +async def test_wrapped_instance_with_broken_callbacks(hass, mock_bleak_scanner_start): + """Test broken callbacks do not cause the scanner to fail.""" + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + detected = [] + + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + if detected: + raise ValueError + detected.append((device, advertisement_data)) + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + + assert models.HA_BLEAK_SCANNER is not None + scanner = models.HaBleakScannerWrapper( + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + scanner.register_detection_callback(_device_detected) + + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + assert len(detected) == 1 diff --git a/tests/components/bluetooth/test_usage.py b/tests/components/bluetooth/test_usage.py new file mode 100644 index 00000000000..92339735340 --- /dev/null +++ b/tests/components/bluetooth/test_usage.py @@ -0,0 +1,22 @@ +"""Tests for the Bluetooth integration.""" + +from unittest.mock import MagicMock + +import bleak + +from homeassistant.components.bluetooth import models +from homeassistant.components.bluetooth.models import HaBleakScannerWrapper +from homeassistant.components.bluetooth.usage import install_multiple_bleak_catcher + + +async def test_multiple_bleak_scanner_instances(hass): + """Test creating multiple zeroconf throws without an integration.""" + assert models.HA_BLEAK_SCANNER is None + mock_scanner = MagicMock() + + install_multiple_bleak_catcher(mock_scanner) + + instance = bleak.BleakScanner() + + assert isinstance(instance, HaBleakScannerWrapper) + assert models.HA_BLEAK_SCANNER is mock_scanner diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index dcb0ee7abfd..63fa58851e3 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -94,6 +94,7 @@ async def test_user_has_confirmation(hass, discovery_flow_conf): @pytest.mark.parametrize( "source", [ + config_entries.SOURCE_BLUETOOTH, config_entries.SOURCE_DISCOVERY, config_entries.SOURCE_MQTT, config_entries.SOURCE_SSDP, @@ -117,6 +118,7 @@ async def test_discovery_single_instance(hass, discovery_flow_conf, source): @pytest.mark.parametrize( "source", [ + config_entries.SOURCE_BLUETOOTH, config_entries.SOURCE_DISCOVERY, config_entries.SOURCE_MQTT, config_entries.SOURCE_SSDP, @@ -142,6 +144,7 @@ async def test_discovery_confirmation(hass, discovery_flow_conf, source): @pytest.mark.parametrize( "source", [ + config_entries.SOURCE_BLUETOOTH, config_entries.SOURCE_DISCOVERY, config_entries.SOURCE_MQTT, config_entries.SOURCE_SSDP, diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 1970d883efc..3e7245ed73a 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -2497,6 +2497,7 @@ async def test_async_setup_update_entry(hass): @pytest.mark.parametrize( "discovery_source", ( + (config_entries.SOURCE_BLUETOOTH, BaseServiceInfo()), (config_entries.SOURCE_DISCOVERY, {}), (config_entries.SOURCE_SSDP, BaseServiceInfo()), (config_entries.SOURCE_USB, BaseServiceInfo()), diff --git a/tests/test_loader.py b/tests/test_loader.py index 96694c43c7f..b8a469f80aa 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -205,6 +205,7 @@ def test_integration_properties(hass): {"hostname": "tesla_*", "macaddress": "98ED5C*"}, {"registered_devices": True}, ], + "bluetooth": [{"manufacturer_id": 76, "manufacturer_data_first_byte": 6}], "usb": [ {"vid": "10C4", "pid": "EA60"}, {"vid": "1CF1", "pid": "0030"}, @@ -242,6 +243,9 @@ def test_integration_properties(hass): {"vid": "1A86", "pid": "7523"}, {"vid": "10C4", "pid": "8A2A"}, ] + assert integration.bluetooth == [ + {"manufacturer_id": 76, "manufacturer_data_first_byte": 6} + ] assert integration.ssdp == [ { "manufacturer": "Royal Philips Electronics", @@ -274,6 +278,7 @@ def test_integration_properties(hass): assert integration.homekit is None assert integration.zeroconf is None assert integration.dhcp is None + assert integration.bluetooth is None assert integration.usb is None assert integration.ssdp is None assert integration.mqtt is None @@ -296,6 +301,7 @@ def test_integration_properties(hass): assert integration.zeroconf == [{"type": "_hue._tcp.local.", "name": "hue*"}] assert integration.dhcp is None assert integration.usb is None + assert integration.bluetooth is None assert integration.ssdp is None @@ -417,6 +423,25 @@ def _get_test_integration_with_dhcp_matcher(hass, name, config_flow): ) +def _get_test_integration_with_bluetooth_matcher(hass, name, config_flow): + """Return a generated test integration with a bluetooth matcher.""" + return loader.Integration( + hass, + f"homeassistant.components.{name}", + None, + { + "name": name, + "domain": name, + "config_flow": config_flow, + "bluetooth": [ + { + "local_name": "Prodigio_*", + }, + ], + }, + ) + + def _get_test_integration_with_usb_matcher(hass, name, config_flow): """Return a generated test integration with a usb matcher.""" return loader.Integration( @@ -543,6 +568,26 @@ async def test_get_zeroconf_back_compat(hass): ] +async def test_get_bluetooth(hass): + """Verify that custom components with bluetooth are found.""" + test_1_integration = _get_test_integration_with_bluetooth_matcher( + hass, "test_1", True + ) + test_2_integration = _get_test_integration_with_dhcp_matcher(hass, "test_2", True) + with patch("homeassistant.loader.async_get_custom_components") as mock_get: + mock_get.return_value = { + "test_1": test_1_integration, + "test_2": test_2_integration, + } + bluetooth = await loader.async_get_bluetooth(hass) + bluetooth_for_domain = [ + entry for entry in bluetooth if entry["domain"] == "test_1" + ] + assert bluetooth_for_domain == [ + {"domain": "test_1", "local_name": "Prodigio_*"}, + ] + + async def test_get_dhcp(hass): """Verify that custom components with dhcp are found.""" test_1_integration = _get_test_integration_with_dhcp_matcher(hass, "test_1", True) From cdaefc8fdaa260d03e6b6a6580858171a20a4b52 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 9 Jul 2022 00:20:23 +0000 Subject: [PATCH 2309/3516] [ci skip] Translation update --- .../components/airly/translations/ja.json | 2 +- .../components/anthemav/translations/id.json | 7 ++++++- .../application_credentials/translations/cs.json | 3 +++ .../components/awair/translations/id.json | 4 ++++ .../components/binary_sensor/translations/cs.json | 4 ++++ .../components/bosch_shc/translations/cs.json | 1 + .../components/deconz/translations/cs.json | 1 + .../components/esphome/translations/id.json | 4 ++-- .../components/esphome/translations/ja.json | 2 +- .../components/fibaro/translations/cs.json | 7 +++++++ .../components/fritz/translations/cs.json | 1 + .../garages_amsterdam/translations/cs.json | 1 + .../components/generic/translations/cs.json | 3 +++ .../components/generic/translations/id.json | 2 ++ .../google_travel_time/translations/cs.json | 1 + .../here_travel_time/translations/ca.json | 7 +++++++ .../here_travel_time/translations/de.json | 9 ++++++++- .../here_travel_time/translations/el.json | 7 +++++++ .../here_travel_time/translations/et.json | 7 +++++++ .../here_travel_time/translations/fr.json | 7 +++++++ .../here_travel_time/translations/id.json | 7 +++++++ .../here_travel_time/translations/pt-BR.json | 7 +++++++ homeassistant/components/hive/translations/ja.json | 2 +- homeassistant/components/hue/translations/bg.json | 1 + .../components/insteon/translations/bg.json | 5 +++++ .../components/isy994/translations/cs.json | 1 + .../components/lg_soundbar/translations/id.json | 6 +++++- .../components/life360/translations/id.json | 6 ++++++ .../components/media_player/translations/cs.json | 4 ++++ .../components/mjpeg/translations/cs.json | 3 +++ .../components/motioneye/translations/bg.json | 3 ++- homeassistant/components/nam/translations/cs.json | 1 + .../components/nextdns/translations/id.json | 4 +++- homeassistant/components/nina/translations/id.json | 9 +++++++-- .../components/onewire/translations/ja.json | 2 +- .../components/qnap_qsw/translations/cs.json | 7 +++++++ .../components/qnap_qsw/translations/id.json | 6 ++++++ .../components/ridwell/translations/cs.json | 1 + .../components/select/translations/cs.json | 8 ++++++++ .../components/sensor/translations/cs.json | 2 ++ .../components/skybell/translations/cs.json | 7 +++++++ .../components/solax/translations/cs.json | 1 + .../components/soundtouch/translations/id.json | 5 +++++ .../components/spotify/translations/ja.json | 2 +- .../components/steam_online/translations/cs.json | 14 ++++++++++++++ .../components/syncthing/translations/cs.json | 1 + .../components/ukraine_alarm/translations/ja.json | 2 +- .../components/version/translations/cs.json | 5 +++++ 48 files changed, 187 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/application_credentials/translations/cs.json create mode 100644 homeassistant/components/fibaro/translations/cs.json create mode 100644 homeassistant/components/qnap_qsw/translations/cs.json create mode 100644 homeassistant/components/select/translations/cs.json create mode 100644 homeassistant/components/skybell/translations/cs.json create mode 100644 homeassistant/components/steam_online/translations/cs.json diff --git a/homeassistant/components/airly/translations/ja.json b/homeassistant/components/airly/translations/ja.json index 5116cac487a..fbc64fa69ea 100644 --- a/homeassistant/components/airly/translations/ja.json +++ b/homeassistant/components/airly/translations/ja.json @@ -15,7 +15,7 @@ "longitude": "\u7d4c\u5ea6", "name": "\u540d\u524d" }, - "description": "Airly\u306e\u7a7a\u6c17\u54c1\u8cea\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002 API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.airly.eu/register \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001https://developer.airly.eu/register \u306b\u30a2\u30af\u30bb\u30b9\u3057\u3066\u304f\u3060\u3055\u3044" } } }, diff --git a/homeassistant/components/anthemav/translations/id.json b/homeassistant/components/anthemav/translations/id.json index d6bac04f480..1eb2ba0b5a1 100644 --- a/homeassistant/components/anthemav/translations/id.json +++ b/homeassistant/components/anthemav/translations/id.json @@ -1,11 +1,16 @@ { "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, "error": { - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "cannot_receive_deviceinfo": "Gagal mengambil alamat MAC. Pastikan perangkat nyala" }, "step": { "user": { "data": { + "host": "Host", "port": "Port" } } diff --git a/homeassistant/components/application_credentials/translations/cs.json b/homeassistant/components/application_credentials/translations/cs.json new file mode 100644 index 00000000000..6699bb0a908 --- /dev/null +++ b/homeassistant/components/application_credentials/translations/cs.json @@ -0,0 +1,3 @@ +{ + "title": "P\u0159ihla\u0161ovac\u00ed \u00fadaje aplikace" +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/id.json b/homeassistant/components/awair/translations/id.json index ab9431a6a46..863b7982b2a 100644 --- a/homeassistant/components/awair/translations/id.json +++ b/homeassistant/components/awair/translations/id.json @@ -18,6 +18,10 @@ "description": "Masukkan kembali token akses pengembang Awair Anda." }, "reauth_confirm": { + "data": { + "access_token": "Token Akses", + "email": "Email" + }, "description": "Masukkan kembali token akses pengembang Awair Anda." }, "user": { diff --git a/homeassistant/components/binary_sensor/translations/cs.json b/homeassistant/components/binary_sensor/translations/cs.json index b8793a2c087..9e5c5b9d7c2 100644 --- a/homeassistant/components/binary_sensor/translations/cs.json +++ b/homeassistant/components/binary_sensor/translations/cs.json @@ -97,6 +97,10 @@ "vibration": "{entity_name} za\u010dalo detekovat vibrace" } }, + "device_class": { + "gas": "plyn", + "sound": "zvuk" + }, "state": { "_": { "off": "Neaktivn\u00ed", diff --git a/homeassistant/components/bosch_shc/translations/cs.json b/homeassistant/components/bosch_shc/translations/cs.json index 72df4a96818..45e02001105 100644 --- a/homeassistant/components/bosch_shc/translations/cs.json +++ b/homeassistant/components/bosch_shc/translations/cs.json @@ -4,6 +4,7 @@ "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" } diff --git a/homeassistant/components/deconz/translations/cs.json b/homeassistant/components/deconz/translations/cs.json index 8e6c6a71a44..b8afd28926c 100644 --- a/homeassistant/components/deconz/translations/cs.json +++ b/homeassistant/components/deconz/translations/cs.json @@ -68,6 +68,7 @@ "remote_button_long_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\" po dlouh\u00e9m stisku", "remote_button_quadruple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto \u010dty\u0159ikr\u00e1t", "remote_button_quintuple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto p\u011btkr\u00e1t", + "remote_button_rotated": "Tla\u010d\u00edtko \"{subtype}\" se oto\u010dilo", "remote_button_rotation_stopped": "Oto\u010den\u00ed tla\u010d\u00edtka \"{subtype}\" bylo zastaveno", "remote_button_short_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto", "remote_button_short_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\"", diff --git a/homeassistant/components/esphome/translations/id.json b/homeassistant/components/esphome/translations/id.json index e099405bf0d..155bf33d986 100644 --- a/homeassistant/components/esphome/translations/id.json +++ b/homeassistant/components/esphome/translations/id.json @@ -9,7 +9,7 @@ "connection_error": "Tidak dapat terhubung ke ESP. Pastikan file YAML Anda mengandung baris 'api:'.", "invalid_auth": "Autentikasi tidak valid", "invalid_psk": "Kunci enkripsi transport tidak valid. Pastikan kuncinya sesuai dengan yang ada pada konfigurasi Anda", - "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" + "resolve_error": "Tidak dapat menemukan alamat ESP. Jika kesalahan ini terus terjadi, atur alamat IP statis" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Host", "port": "Port" }, - "description": "Masukkan pengaturan koneksi node [ESPHome](https://esphomelib.com/)." + "description": "Masukkan pengaturan koneksi node [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/esphome/translations/ja.json b/homeassistant/components/esphome/translations/ja.json index 0779d072b94..a4bfbc327dd 100644 --- a/homeassistant/components/esphome/translations/ja.json +++ b/homeassistant/components/esphome/translations/ja.json @@ -9,7 +9,7 @@ "connection_error": "ESP\u306b\u63a5\u7d9a\u3067\u304d\u307e\u305b\u3093\u3002YAML\u30d5\u30a1\u30a4\u30eb\u306b 'api:' \u306e\u884c\u304c\u542b\u307e\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "invalid_psk": "\u30c8\u30e9\u30f3\u30b9\u30dd\u30fc\u30c8\u306e\u6697\u53f7\u5316\u30ad\u30fc\u304c\u7121\u52b9\u3067\u3059\u3002\u8a2d\u5b9a\u3068\u4e00\u81f4\u3057\u3066\u3044\u308b\u304b\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "resolve_error": "ESP\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u89e3\u6c7a\u3067\u304d\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u304c\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u306f\u3001IP\u30a2\u30c9\u30ec\u30b9\u3092\u9759\u7684\u306b\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "ESP\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u89e3\u6c7a\u3067\u304d\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u304c\u89e3\u6c7a\u3057\u306a\u3044\u5834\u5408\u306f\u3001\u56fa\u5b9aIP\u30a2\u30c9\u30ec\u30b9\u306b\u8a2d\u5b9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/fibaro/translations/cs.json b/homeassistant/components/fibaro/translations/cs.json new file mode 100644 index 00000000000..e1bf8e7f45f --- /dev/null +++ b/homeassistant/components/fibaro/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/cs.json b/homeassistant/components/fritz/translations/cs.json index 55326bb1803..8f114045caf 100644 --- a/homeassistant/components/fritz/translations/cs.json +++ b/homeassistant/components/fritz/translations/cs.json @@ -10,6 +10,7 @@ "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/garages_amsterdam/translations/cs.json b/homeassistant/components/garages_amsterdam/translations/cs.json index 3b814303e69..5073c9248e0 100644 --- a/homeassistant/components/garages_amsterdam/translations/cs.json +++ b/homeassistant/components/garages_amsterdam/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" } } diff --git a/homeassistant/components/generic/translations/cs.json b/homeassistant/components/generic/translations/cs.json index 520e0743edd..8ef4333b872 100644 --- a/homeassistant/components/generic/translations/cs.json +++ b/homeassistant/components/generic/translations/cs.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, "step": { "content_type": { "data": { diff --git a/homeassistant/components/generic/translations/id.json b/homeassistant/components/generic/translations/id.json index 77ea5f01f0b..5222c111c58 100644 --- a/homeassistant/components/generic/translations/id.json +++ b/homeassistant/components/generic/translations/id.json @@ -9,6 +9,7 @@ "invalid_still_image": "URL tidak mengembalikan gambar diam yang valid", "malformed_url": "URL salah format", "no_still_image_or_stream_url": "Anda harus menentukan setidaknya gambar diam atau URL streaming", + "relative_url": "URL relatif tidak diizinkan", "stream_file_not_found": "File tidak ditemukan saat mencoba menyambung ke streaming (sudahkah ffmpeg diinstal?)", "stream_http_not_found": "HTTP 404 Tidak ditemukan saat mencoba menyambung ke streaming", "stream_io_error": "Kesalahan Input/Output saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", @@ -53,6 +54,7 @@ "invalid_still_image": "URL tidak mengembalikan gambar diam yang valid", "malformed_url": "URL salah format", "no_still_image_or_stream_url": "Anda harus menentukan setidaknya gambar diam atau URL streaming", + "relative_url": "URL relatif tidak diizinkan", "stream_file_not_found": "File tidak ditemukan saat mencoba menyambung ke streaming (sudahkah ffmpeg diinstal?)", "stream_http_not_found": "HTTP 404 Tidak ditemukan saat mencoba menyambung ke streaming", "stream_io_error": "Kesalahan Input/Output saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", diff --git a/homeassistant/components/google_travel_time/translations/cs.json b/homeassistant/components/google_travel_time/translations/cs.json index a6c3b361960..19da83d1596 100644 --- a/homeassistant/components/google_travel_time/translations/cs.json +++ b/homeassistant/components/google_travel_time/translations/cs.json @@ -19,6 +19,7 @@ "step": { "init": { "data": { + "language": "Jazyk", "time": "\u010cas", "units": "Jednotky" } diff --git a/homeassistant/components/here_travel_time/translations/ca.json b/homeassistant/components/here_travel_time/translations/ca.json index 08dfb0d970d..586e5bd5442 100644 --- a/homeassistant/components/here_travel_time/translations/ca.json +++ b/homeassistant/components/here_travel_time/translations/ca.json @@ -39,6 +39,13 @@ }, "title": "Tria origen" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Utilitzant una ubicaci\u00f3 de mapa", + "origin_entity": "Utilitzant una entitat" + }, + "title": "Tria l'origen" + }, "user": { "data": { "api_key": "Clau API", diff --git a/homeassistant/components/here_travel_time/translations/de.json b/homeassistant/components/here_travel_time/translations/de.json index b50ef028089..db93b79fd2f 100644 --- a/homeassistant/components/here_travel_time/translations/de.json +++ b/homeassistant/components/here_travel_time/translations/de.json @@ -23,7 +23,7 @@ "destination_menu": { "menu_options": { "destination_coordinates": "Verwendung einer Kartenposition", - "destination_entity": "Verwenden einer Entit\u00e4t" + "destination_entity": "Verwendung einer Entit\u00e4t" }, "title": "Ziel w\u00e4hlen" }, @@ -39,6 +39,13 @@ }, "title": "Herkunft w\u00e4hlen" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Verwendung einer Kartenposition", + "origin_entity": "Verwendung einer Entit\u00e4t" + }, + "title": "Herkunft w\u00e4hlen" + }, "user": { "data": { "api_key": "API-Schl\u00fcssel", diff --git a/homeassistant/components/here_travel_time/translations/el.json b/homeassistant/components/here_travel_time/translations/el.json index cd4d986ed3f..062cf89ea0f 100644 --- a/homeassistant/components/here_travel_time/translations/el.json +++ b/homeassistant/components/here_travel_time/translations/el.json @@ -39,6 +39,13 @@ }, "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c0\u03c1\u03bf\u03ad\u03bb\u03b5\u03c5\u03c3\u03b7\u03c2" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b8\u03ad\u03c3\u03b7\u03c2 \u03c7\u03ac\u03c1\u03c4\u03b7", + "origin_entity": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c0\u03c1\u03bf\u03ad\u03bb\u03b5\u03c5\u03c3\u03b7\u03c2" + }, "user": { "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", diff --git a/homeassistant/components/here_travel_time/translations/et.json b/homeassistant/components/here_travel_time/translations/et.json index c9646af7544..a017971a33f 100644 --- a/homeassistant/components/here_travel_time/translations/et.json +++ b/homeassistant/components/here_travel_time/translations/et.json @@ -39,6 +39,13 @@ }, "title": "Vali l\u00e4htekoht" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Kasuta asukohta kaardil", + "origin_entity": "Kasuta olemi andmeid" + }, + "title": "Vali l\u00e4htepunkt" + }, "user": { "data": { "api_key": "API v\u00f5ti", diff --git a/homeassistant/components/here_travel_time/translations/fr.json b/homeassistant/components/here_travel_time/translations/fr.json index 063a4008779..c44c2337af9 100644 --- a/homeassistant/components/here_travel_time/translations/fr.json +++ b/homeassistant/components/here_travel_time/translations/fr.json @@ -39,6 +39,13 @@ }, "title": "Choix de l'origine" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Utilisation d'un emplacement sur la carte", + "origin_entity": "Utilisation d'une entit\u00e9" + }, + "title": "Choix de l'origine" + }, "user": { "data": { "api_key": "Cl\u00e9 d'API", diff --git a/homeassistant/components/here_travel_time/translations/id.json b/homeassistant/components/here_travel_time/translations/id.json index f03910d9adf..57d4ec5ee80 100644 --- a/homeassistant/components/here_travel_time/translations/id.json +++ b/homeassistant/components/here_travel_time/translations/id.json @@ -39,6 +39,13 @@ }, "title": "Pilih Asal" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Menggunakan lokasi pada peta", + "origin_entity": "Menggunakan entitas" + }, + "title": "Pilih Asal" + }, "user": { "data": { "api_key": "Kunci API", diff --git a/homeassistant/components/here_travel_time/translations/pt-BR.json b/homeassistant/components/here_travel_time/translations/pt-BR.json index 34f862f0029..9b524df7539 100644 --- a/homeassistant/components/here_travel_time/translations/pt-BR.json +++ b/homeassistant/components/here_travel_time/translations/pt-BR.json @@ -39,6 +39,13 @@ }, "title": "Escolha a Origem" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Usando uma localiza\u00e7\u00e3o no mapa", + "origin_entity": "Usando uma entidade" + }, + "title": "Escolha a Origem" + }, "user": { "data": { "api_key": "Chave da API", diff --git a/homeassistant/components/hive/translations/ja.json b/homeassistant/components/hive/translations/ja.json index ed11bbd8b7e..981c65ba540 100644 --- a/homeassistant/components/hive/translations/ja.json +++ b/homeassistant/components/hive/translations/ja.json @@ -41,7 +41,7 @@ "scan_interval": "\u30b9\u30ad\u30e3\u30f3\u30a4\u30f3\u30bf\u30fc\u30d0\u30eb(\u79d2)", "username": "\u30e6\u30fc\u30b6\u30fc\u540d" }, - "description": "Hive\u306e\u30ed\u30b0\u30a4\u30f3\u60c5\u5831\u3068\u8a2d\u5b9a\u3092\u5165\u529b\u3057\u307e\u3059\u3002", + "description": "Hive\u306e\u30ed\u30b0\u30a4\u30f3\u60c5\u5831\u3092\u5165\u529b\u3057\u307e\u3059\u3002", "title": "Hive\u30ed\u30b0\u30a4\u30f3" } } diff --git a/homeassistant/components/hue/translations/bg.json b/homeassistant/components/hue/translations/bg.json index 68da5279508..93617d8c0e5 100644 --- a/homeassistant/components/hue/translations/bg.json +++ b/homeassistant/components/hue/translations/bg.json @@ -39,6 +39,7 @@ "2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", "3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "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" } diff --git a/homeassistant/components/insteon/translations/bg.json b/homeassistant/components/insteon/translations/bg.json index e1adb8657da..f3fb9df607c 100644 --- a/homeassistant/components/insteon/translations/bg.json +++ b/homeassistant/components/insteon/translations/bg.json @@ -34,6 +34,11 @@ "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { + "add_x10": { + "data": { + "platform": "\u041f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430" + } + }, "change_hub_config": { "data": { "host": "IP \u0430\u0434\u0440\u0435\u0441", diff --git a/homeassistant/components/isy994/translations/cs.json b/homeassistant/components/isy994/translations/cs.json index e2d3dc4c883..ac6773f09e1 100644 --- a/homeassistant/components/isy994/translations/cs.json +++ b/homeassistant/components/isy994/translations/cs.json @@ -7,6 +7,7 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "invalid_host": "Z\u00e1znam hostitele nebyl v \u00fapln\u00e9m form\u00e1tu URL, nap\u0159. http://192.168.10.100:80", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { diff --git a/homeassistant/components/lg_soundbar/translations/id.json b/homeassistant/components/lg_soundbar/translations/id.json index 70ce3eb75e9..74d380f3a1a 100644 --- a/homeassistant/components/lg_soundbar/translations/id.json +++ b/homeassistant/components/lg_soundbar/translations/id.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Layanan sudah dikonfigurasi" + "already_configured": "Layanan sudah dikonfigurasi", + "existing_instance_updated": "Memperbarui konfigurasi yang ada." + }, + "error": { + "cannot_connect": "Gagal terhubung" }, "step": { "user": { diff --git a/homeassistant/components/life360/translations/id.json b/homeassistant/components/life360/translations/id.json index b219745572d..dfcdf97f46f 100644 --- a/homeassistant/components/life360/translations/id.json +++ b/homeassistant/components/life360/translations/id.json @@ -17,6 +17,12 @@ "unknown": "Kesalahan yang tidak diharapkan" }, "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "title": "Autentikasi Ulang Integrasi" + }, "user": { "data": { "password": "Kata Sandi", diff --git a/homeassistant/components/media_player/translations/cs.json b/homeassistant/components/media_player/translations/cs.json index e20ec750f79..19e88635f8b 100644 --- a/homeassistant/components/media_player/translations/cs.json +++ b/homeassistant/components/media_player/translations/cs.json @@ -6,6 +6,10 @@ "is_on": "{entity_name} je zapnuto", "is_paused": "{entity_name} je pozastaven", "is_playing": "{entity_name} p\u0159ehr\u00e1v\u00e1" + }, + "trigger_type": { + "paused": "{entity_name} je pozastaveno", + "turned_on": "{entity_name} bylo zapnuto" } }, "state": { diff --git a/homeassistant/components/mjpeg/translations/cs.json b/homeassistant/components/mjpeg/translations/cs.json index 00616fbdd50..3fc5acfe8dd 100644 --- a/homeassistant/components/mjpeg/translations/cs.json +++ b/homeassistant/components/mjpeg/translations/cs.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, diff --git a/homeassistant/components/motioneye/translations/bg.json b/homeassistant/components/motioneye/translations/bg.json index ef56ca3f2df..d2db5257b51 100644 --- a/homeassistant/components/motioneye/translations/bg.json +++ b/homeassistant/components/motioneye/translations/bg.json @@ -1,7 +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" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_url": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d URL" }, "step": { "user": { diff --git a/homeassistant/components/nam/translations/cs.json b/homeassistant/components/nam/translations/cs.json index 72df4a96818..1b979ab1412 100644 --- a/homeassistant/components/nam/translations/cs.json +++ b/homeassistant/components/nam/translations/cs.json @@ -1,6 +1,7 @@ { "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": { diff --git a/homeassistant/components/nextdns/translations/id.json b/homeassistant/components/nextdns/translations/id.json index 9ebb40ae2d8..39f87faf813 100644 --- a/homeassistant/components/nextdns/translations/id.json +++ b/homeassistant/components/nextdns/translations/id.json @@ -4,7 +4,9 @@ "already_configured": "Profil NextDNS ini sudah dikonfigurasi." }, "error": { - "invalid_api_key": "Kunci API tidak valid" + "cannot_connect": "Gagal terhubung", + "invalid_api_key": "Kunci API tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" }, "step": { "profiles": { diff --git a/homeassistant/components/nina/translations/id.json b/homeassistant/components/nina/translations/id.json index 7e3642a04ef..f827ef935f7 100644 --- a/homeassistant/components/nina/translations/id.json +++ b/homeassistant/components/nina/translations/id.json @@ -26,7 +26,9 @@ }, "options": { "error": { - "no_selection": "Pilih setidaknya satu kota/kabupaten" + "cannot_connect": "Gagal terhubung", + "no_selection": "Pilih setidaknya satu kota/kabupaten", + "unknown": "Kesalahan yang tidak diharapkan" }, "step": { "init": { @@ -35,7 +37,10 @@ "_e_to_h": "Kota/kabupaten (E-H)", "_i_to_l": "Kota/kabupaten (I-L)", "_m_to_q": "Kota/kabupaten (M-Q)", - "_r_to_u": "Kota/kabupaten (R-U)" + "_r_to_u": "Kota/kabupaten (R-U)", + "_v_to_z": "Kota/kabupaten (V-Z)", + "corona_filter": "Hapus Peringatan Corona", + "slots": "Peringatan maksimum per kota/kabupaten" }, "title": "Opsi" } diff --git a/homeassistant/components/onewire/translations/ja.json b/homeassistant/components/onewire/translations/ja.json index 75b01a0cbc9..04e6bbac0cf 100644 --- a/homeassistant/components/onewire/translations/ja.json +++ b/homeassistant/components/onewire/translations/ja.json @@ -12,7 +12,7 @@ "host": "\u30db\u30b9\u30c8", "port": "\u30dd\u30fc\u30c8" }, - "title": "1-Wire\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" + "title": "\u30b5\u30fc\u30d0\u30fc\u306e\u8a73\u7d30\u3092\u8a2d\u5b9a\u3059\u308b" } } }, diff --git a/homeassistant/components/qnap_qsw/translations/cs.json b/homeassistant/components/qnap_qsw/translations/cs.json new file mode 100644 index 00000000000..33006d6761b --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/id.json b/homeassistant/components/qnap_qsw/translations/id.json index b34bcd046d9..b9f124a765c 100644 --- a/homeassistant/components/qnap_qsw/translations/id.json +++ b/homeassistant/components/qnap_qsw/translations/id.json @@ -9,6 +9,12 @@ "invalid_auth": "Autentikasi tidak valid" }, "step": { + "discovered_connection": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + }, "user": { "data": { "password": "Kata Sandi", diff --git a/homeassistant/components/ridwell/translations/cs.json b/homeassistant/components/ridwell/translations/cs.json index 72df4a96818..5d43feee500 100644 --- a/homeassistant/components/ridwell/translations/cs.json +++ b/homeassistant/components/ridwell/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { diff --git a/homeassistant/components/select/translations/cs.json b/homeassistant/components/select/translations/cs.json new file mode 100644 index 00000000000..32d6cadee9e --- /dev/null +++ b/homeassistant/components/select/translations/cs.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "select_option": "Zm\u011bnit mo\u017enost {entity_name}" + } + }, + "title": "V\u00fdb\u011br" +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/cs.json b/homeassistant/components/sensor/translations/cs.json index f493f4134ed..f6f2e97fdd4 100644 --- a/homeassistant/components/sensor/translations/cs.json +++ b/homeassistant/components/sensor/translations/cs.json @@ -11,6 +11,7 @@ "is_power_factor": "Aktu\u00e1ln\u00ed \u00fa\u010din\u00edk {entity_name}", "is_pressure": "Aktu\u00e1ln\u00ed tlak {entity_name}", "is_signal_strength": "Aktu\u00e1ln\u00ed s\u00edla sign\u00e1lu {entity_name}", + "is_sulphur_dioxide": "Aktu\u00e1ln\u00ed \u00farove\u0148 koncentrace oxidu si\u0159i\u010dit\u00e9ho {entity_name}", "is_temperature": "Aktu\u00e1ln\u00ed teplota {entity_name}", "is_value": "Aktu\u00e1ln\u00ed hodnota {entity_name}", "is_voltage": "Aktu\u00e1ln\u00ed nap\u011bt\u00ed {entity_name}" @@ -22,6 +23,7 @@ "gas": "P\u0159i zm\u011bn\u011b mno\u017estv\u00ed plynu {entity_name}", "humidity": "P\u0159i zm\u011bn\u011b vlhkosti {entity_name}", "illuminance": "P\u0159i zm\u011bn\u011b osv\u011btlen\u00ed {entity_name}", + "nitrogen_monoxide": "Zm\u011bna koncentrace oxidu dusnat\u00e9ho {entity_name}", "power": "P\u0159i zm\u011bn\u011b el. v\u00fdkonu {entity_name}", "power_factor": "P\u0159i zm\u011bn\u011b \u00fa\u010din\u00edku {entity_name}", "pressure": "P\u0159i zm\u011bn\u011b tlaku {entity_name}", diff --git a/homeassistant/components/skybell/translations/cs.json b/homeassistant/components/skybell/translations/cs.json new file mode 100644 index 00000000000..08830492748 --- /dev/null +++ b/homeassistant/components/skybell/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/translations/cs.json b/homeassistant/components/solax/translations/cs.json index 7940c6378fe..2bf3fbfa3fb 100644 --- a/homeassistant/components/solax/translations/cs.json +++ b/homeassistant/components/solax/translations/cs.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "ip_address": "IP adresa", "port": "Port" } } diff --git a/homeassistant/components/soundtouch/translations/id.json b/homeassistant/components/soundtouch/translations/id.json index 71b7e84f1e2..ce2c8f4e1a3 100644 --- a/homeassistant/components/soundtouch/translations/id.json +++ b/homeassistant/components/soundtouch/translations/id.json @@ -7,6 +7,11 @@ "cannot_connect": "Gagal terhubung" }, "step": { + "user": { + "data": { + "host": "Host" + } + }, "zeroconf_confirm": { "description": "Anda akan menambahkan perangkat SoundTouch bernama `{name}` ke Home Assistant.", "title": "Konfirmasi penambahan perangkat Bose SoundTouch" diff --git a/homeassistant/components/spotify/translations/ja.json b/homeassistant/components/spotify/translations/ja.json index b0b94ebe5ab..4a65f5037dd 100644 --- a/homeassistant/components/spotify/translations/ja.json +++ b/homeassistant/components/spotify/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", - "missing_configuration": "Spotify\u7d71\u5408\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "missing_configuration": "Spotify\u7d71\u5408\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", "reauth_account_mismatch": "\u8a8d\u8a3c\u3055\u308c\u305fSpotify\u30a2\u30ab\u30a6\u30f3\u30c8\u304c\u3001\u518d\u8a8d\u8a3c\u304c\u5fc5\u8981\u306a\u30a2\u30ab\u30a6\u30f3\u30c8\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" }, diff --git a/homeassistant/components/steam_online/translations/cs.json b/homeassistant/components/steam_online/translations/cs.json new file mode 100644 index 00000000000..ffd864a8f1d --- /dev/null +++ b/homeassistant/components/steam_online/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nastavena" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/cs.json b/homeassistant/components/syncthing/translations/cs.json index a679dc35fe3..df806ffaf1c 100644 --- a/homeassistant/components/syncthing/translations/cs.json +++ b/homeassistant/components/syncthing/translations/cs.json @@ -4,6 +4,7 @@ "already_configured": "Slu\u017eba je ji\u017e nastavena" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" } } diff --git a/homeassistant/components/ukraine_alarm/translations/ja.json b/homeassistant/components/ukraine_alarm/translations/ja.json index babf5963357..ae57ebe0d17 100644 --- a/homeassistant/components/ukraine_alarm/translations/ja.json +++ b/homeassistant/components/ukraine_alarm/translations/ja.json @@ -25,7 +25,7 @@ "data": { "region": "\u30ea\u30fc\u30b8\u30e7\u30f3" }, - "description": "\u30a6\u30af\u30e9\u30a4\u30ca\u306e\u8b66\u5831\u7d71\u5408\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u3002API\u30ad\u30fc\u3092\u751f\u6210\u3059\u308b\u306b\u306f\u3001 {api_url} \u306b\u79fb\u52d5\u3057\u3066\u304f\u3060\u3055\u3044" + "description": "\u30e2\u30cb\u30bf\u30fc\u3059\u308b\u72b6\u614b\u3092\u9078\u629e" } } } diff --git a/homeassistant/components/version/translations/cs.json b/homeassistant/components/version/translations/cs.json index 33006d6761b..16fbcb2c8d4 100644 --- a/homeassistant/components/version/translations/cs.json +++ b/homeassistant/components/version/translations/cs.json @@ -2,6 +2,11 @@ "config": { "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "step": { + "user": { + "title": "Vyberte typ instalace" + } } } } \ No newline at end of file From 36357fe45da4a2a28cf25e59723bba4d57c9167d Mon Sep 17 00:00:00 2001 From: Regev Brody Date: Sat, 9 Jul 2022 14:22:29 +0300 Subject: [PATCH 2310/3516] Bump pyezviz to 0.2.0.9 (#74755) * Bump ezviz dependency to fix #74618 * Bump ezviz dependency to fix #74618 Co-authored-by: J. Nick Koston --- homeassistant/components/ezviz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json index cfdfd6e441d..47e2ec44e8a 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.2.0.8"], + "requirements": ["pyezviz==0.2.0.9"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["paho_mqtt", "pyezviz"] diff --git a/requirements_all.txt b/requirements_all.txt index 766ceea6e07..d34799e4f6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1492,7 +1492,7 @@ pyeverlights==0.1.0 pyevilgenius==2.0.0 # homeassistant.components.ezviz -pyezviz==0.2.0.8 +pyezviz==0.2.0.9 # homeassistant.components.fido pyfido==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 30cbba02ff4..894ca0f2ab4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1001,7 +1001,7 @@ pyeverlights==0.1.0 pyevilgenius==2.0.0 # homeassistant.components.ezviz -pyezviz==0.2.0.8 +pyezviz==0.2.0.9 # homeassistant.components.fido pyfido==2.1.1 From f4c333626eff73aa992db61ce249aa9d094fc359 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 9 Jul 2022 08:29:18 -0500 Subject: [PATCH 2311/3516] Add coverage for lutron caseta bridges to hkc (#74765) --- .../fixtures/lutron_caseta_bridge.json | 178 ++++++++++++++++++ .../test_lutron_caseta_bridge.py | 55 ++++++ 2 files changed, 233 insertions(+) create mode 100644 tests/components/homekit_controller/fixtures/lutron_caseta_bridge.json create mode 100644 tests/components/homekit_controller/specific_devices/test_lutron_caseta_bridge.py diff --git a/tests/components/homekit_controller/fixtures/lutron_caseta_bridge.json b/tests/components/homekit_controller/fixtures/lutron_caseta_bridge.json new file mode 100644 index 00000000000..40366ad32c6 --- /dev/null +++ b/tests/components/homekit_controller/fixtures/lutron_caseta_bridge.json @@ -0,0 +1,178 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 85899345921, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 137438953473, + "perms": ["pr"], + "format": "string", + "value": "Lutron Electronics Co., Inc", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 141733920769, + "perms": ["pr"], + "format": "string", + "value": "L-BDG2-WH", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 150323855361, + "perms": ["pr"], + "format": "string", + "value": "Smart Bridge 2", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 206158430209, + "perms": ["pr"], + "format": "string", + "value": "12344331", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 352187318273, + "perms": ["pr"], + "format": "string", + "value": "08.08", + "description": "Firmware Revision", + "maxLen": 64 + } + ] + }, + { + "iid": 2, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 236223201282, + "perms": ["pr"], + "format": "string", + "value": "1.1.0", + "description": "Version", + "maxLen": 64 + } + ] + } + ] + }, + { + "aid": 21474836482, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 85899345921, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 137438953473, + "perms": ["pr"], + "format": "string", + "value": "Lutron Electronics Co., Inc", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 141733920769, + "perms": ["pr"], + "format": "string", + "value": "PD-FSQN-XX", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 150323855361, + "perms": ["pr"], + "format": "string", + "value": "Cas\u00e9ta\u00ae Wireless Fan Speed Control", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 206158430209, + "perms": ["pr"], + "format": "string", + "value": "39024290", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 352187318273, + "perms": ["pr"], + "format": "string", + "value": "001.005", + "description": "Firmware Revision", + "maxLen": 64 + } + ] + }, + { + "iid": 2, + "type": "00000040-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000029-0000-1000-8000-0026BB765291", + "iid": 176093659138, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 0, + "description": "Rotation Speed", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 25 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 150323855362, + "perms": ["pr"], + "format": "string", + "value": "Cas\u00e9ta\u00ae Wireless Fan Speed Control", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 158913789954, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": false, + "description": "On" + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/specific_devices/test_lutron_caseta_bridge.py b/tests/components/homekit_controller/specific_devices/test_lutron_caseta_bridge.py new file mode 100644 index 00000000000..8961eb414fa --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_lutron_caseta_bridge.py @@ -0,0 +1,55 @@ +"""Tests for handling accessories on a Lutron Caseta bridge via HomeKit.""" + +from homeassistant.const import STATE_OFF + +from tests.components.homekit_controller.common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_lutron_caseta_bridge_setup(hass): + """Test that a Lutron Caseta bridge can be correctly setup in HA via HomeKit.""" + accessories = await setup_accessories_from_file(hass, "lutron_caseta_bridge.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="Smart Bridge 2", + model="L-BDG2-WH", + manufacturer="Lutron Electronics Co., Inc", + sw_version="08.08", + hw_version="", + serial_number="12344331", + devices=[ + DeviceTestInfo( + name="Cas\u00e9ta\u00ae Wireless Fan Speed Control", + model="PD-FSQN-XX", + manufacturer="Lutron Electronics Co., Inc", + sw_version="001.005", + hw_version="", + serial_number="39024290", + unique_id="00:00:00:00:00:00:aid:21474836482", + devices=[], + entities=[ + EntityTestInfo( + entity_id="fan.caseta_r_wireless_fan_speed_control", + friendly_name="Caséta® Wireless Fan Speed Control", + unique_id="homekit-39024290-2", + unit_of_measurement=None, + supported_features=1, + state=STATE_OFF, + capabilities={"preset_modes": None}, + ) + ], + ), + ], + entities=[], + ), + ) From 825e696d2688398f2b9c8c53db1ed3eff0d1d91c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 9 Jul 2022 08:42:32 -0600 Subject: [PATCH 2312/3516] Migrate Guardian to new entity naming style (#74745) --- homeassistant/components/guardian/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 25e0df913d8..ba4bc5e6ad6 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -373,11 +373,12 @@ class PairedSensorManager: class GuardianEntity(CoordinatorEntity): """Define a base Guardian entity.""" + _attr_has_entity_name = True + def __init__( # pylint: disable=super-init-not-called self, entry: ConfigEntry, description: EntityDescription ) -> None: """Initialize.""" - self._attr_device_info = DeviceInfo(manufacturer="Elexa") self._attr_extra_state_attributes = {} self._entry = entry self.entity_description = description @@ -406,12 +407,11 @@ class PairedSensorEntity(GuardianEntity): paired_sensor_uid = coordinator.data["uid"] self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, paired_sensor_uid)}, + manufacturer="Elexa", + model=coordinator.data["codename"], 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}" - ) self._attr_unique_id = f"{paired_sensor_uid}_{description.key}" self.coordinator = coordinator @@ -434,10 +434,10 @@ class ValveControllerEntity(GuardianEntity): self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, entry.data[CONF_UID])}, + manufacturer="Elexa", 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 e17db1fd0c8935e61e6d871789e4e24810d6802d Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 9 Jul 2022 10:43:50 -0400 Subject: [PATCH 2313/3516] Remove deprecated yaml config from Steam (#74805) --- .../components/steam_online/config_flow.py | 53 +++---------------- .../components/steam_online/sensor.py | 36 ++----------- tests/components/steam_online/__init__.py | 9 ---- .../steam_online/test_config_flow.py | 32 +---------- 4 files changed, 12 insertions(+), 118 deletions(-) diff --git a/homeassistant/components/steam_online/config_flow.py b/homeassistant/components/steam_online/config_flow.py index 7ed1f0a3610..8356ad8bbc6 100644 --- a/homeassistant/components/steam_online/config_flow.py +++ b/homeassistant/components/steam_online/config_flow.py @@ -12,29 +12,16 @@ from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv, entity_registry as er -from homeassistant.helpers.typing import ConfigType -from .const import ( - CONF_ACCOUNT, - CONF_ACCOUNTS, - DEFAULT_NAME, - DOMAIN, - LOGGER, - PLACEHOLDERS, -) +from .const import CONF_ACCOUNT, CONF_ACCOUNTS, DOMAIN, LOGGER, PLACEHOLDERS -def validate_input( - user_input: dict[str, str | int], multi: bool = False -) -> list[dict[str, str | int]]: +def validate_input(user_input: dict[str, str]) -> dict[str, str | int]: """Handle common flow input validation.""" steam.api.key.set(user_input[CONF_API_KEY]) interface = steam.api.interface("ISteamUser") - if multi: - names = interface.GetPlayerSummaries(steamids=user_input[CONF_ACCOUNTS]) - else: - names = interface.GetPlayerSummaries(steamids=user_input[CONF_ACCOUNT]) - return names["response"]["players"]["player"] + names = interface.GetPlayerSummaries(steamids=user_input[CONF_ACCOUNT]) + return names["response"]["players"]["player"][0] class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -62,8 +49,8 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): elif user_input is not None: try: res = await self.hass.async_add_executor_job(validate_input, user_input) - if res[0] is not None: - name = str(res[0]["personaname"]) + if res is not None: + name = str(res["personaname"]) else: errors["base"] = "invalid_account" except (steam.api.HTTPError, steam.api.HTTPTimeoutError) as ex: @@ -80,22 +67,10 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.hass.config_entries.async_reload(entry.entry_id) return self.async_abort(reason="reauth_successful") self._abort_if_unique_id_configured() - if self.source == config_entries.SOURCE_IMPORT: - res = await self.hass.async_add_executor_job( - validate_input, user_input, True - ) - accounts_data = { - CONF_ACCOUNTS: { - acc["steamid"]: acc["personaname"] for acc in res - } - } - user_input.pop(CONF_ACCOUNTS) - else: - accounts_data = {CONF_ACCOUNTS: {user_input[CONF_ACCOUNT]: name}} return self.async_create_entry( - title=name or DEFAULT_NAME, + title=name, data=user_input, - options=accounts_data, + options={CONF_ACCOUNTS: {user_input[CONF_ACCOUNT]: name}}, ) user_input = user_input or {} return self.async_show_form( @@ -114,18 +89,6 @@ class SteamFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders=PLACEHOLDERS, ) - 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]: - return self.async_abort(reason="already_configured") - LOGGER.warning( - "Steam yaml config is now deprecated and has been imported. " - "Please remove it from your config" - ) - import_config[CONF_ACCOUNT] = import_config[CONF_ACCOUNTS][0] - return await self.async_step_user(import_config) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle a reauthorization flow request.""" self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py index be175b41b66..307dfac0542 100644 --- a/homeassistant/components/steam_online/sensor.py +++ b/homeassistant/components/steam_online/sensor.py @@ -4,19 +4,11 @@ from __future__ import annotations from datetime import datetime from time import localtime, mktime -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_API_KEY +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +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, StateType +from homeassistant.helpers.typing import StateType from homeassistant.util.dt import utc_from_timestamp from . import SteamEntity @@ -31,31 +23,9 @@ from .const import ( ) from .coordinator import SteamDataUpdateCoordinator -# Deprecated in Home Assistant 2022.5 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_ACCOUNTS, default=[]): vol.All(cv.ensure_list, [cv.string]), - } -) - PARALLEL_UPDATES = 1 -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the Twitch sensor from yaml.""" - 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, diff --git a/tests/components/steam_online/__init__.py b/tests/components/steam_online/__init__.py index 4c8c398502f..d41554e4d04 100644 --- a/tests/components/steam_online/__init__.py +++ b/tests/components/steam_online/__init__.py @@ -30,15 +30,6 @@ CONF_OPTIONS_2 = { } } -CONF_IMPORT_OPTIONS = { - CONF_ACCOUNTS: { - ACCOUNT_1: ACCOUNT_NAME_1, - ACCOUNT_2: ACCOUNT_NAME_2, - } -} - -CONF_IMPORT_DATA = {CONF_API_KEY: API_KEY, CONF_ACCOUNTS: [ACCOUNT_1, ACCOUNT_2]} - def create_entry(hass: HomeAssistant) -> MockConfigEntry: """Add config entry in Home Assistant.""" diff --git a/tests/components/steam_online/test_config_flow.py b/tests/components/steam_online/test_config_flow.py index 51ecd77c508..1844611530d 100644 --- a/tests/components/steam_online/test_config_flow.py +++ b/tests/components/steam_online/test_config_flow.py @@ -5,7 +5,7 @@ import steam from homeassistant import data_entry_flow from homeassistant.components.steam_online.const import CONF_ACCOUNTS, DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -15,8 +15,6 @@ from . import ( ACCOUNT_2, ACCOUNT_NAME_1, CONF_DATA, - CONF_IMPORT_DATA, - CONF_IMPORT_OPTIONS, CONF_OPTIONS, CONF_OPTIONS_2, create_entry, @@ -137,34 +135,6 @@ async def test_flow_reauth(hass: HomeAssistant) -> None: assert entry.data == new_conf -async def test_flow_import(hass: HomeAssistant) -> None: - """Test import step.""" - with patch_interface(): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=CONF_IMPORT_DATA, - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == ACCOUNT_NAME_1 - assert result["data"] == CONF_DATA - assert result["options"] == CONF_IMPORT_OPTIONS - assert result["result"].unique_id == ACCOUNT_1 - - -async def test_flow_import_already_configured(hass: HomeAssistant) -> None: - """Test import step already configured.""" - create_entry(hass) - with patch_interface(): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=CONF_IMPORT_DATA, - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "already_configured" - - async def test_options_flow(hass: HomeAssistant) -> None: """Test updating options.""" entry = create_entry(hass) From 79b34090e88d3610da7d93173b2232d682144494 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sat, 9 Jul 2022 10:54:15 -0400 Subject: [PATCH 2314/3516] Bump aiopyarr to 22.7.0 (#74749) --- homeassistant/components/sonarr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonarr/manifest.json b/homeassistant/components/sonarr/manifest.json index 6b34b077888..6b9b45b75ec 100644 --- a/homeassistant/components/sonarr/manifest.json +++ b/homeassistant/components/sonarr/manifest.json @@ -3,7 +3,7 @@ "name": "Sonarr", "documentation": "https://www.home-assistant.io/integrations/sonarr", "codeowners": ["@ctalkington"], - "requirements": ["aiopyarr==22.6.0"], + "requirements": ["aiopyarr==22.7.0"], "config_flow": true, "quality_scale": "silver", "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index d34799e4f6f..8f0fcb9a608 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -226,7 +226,7 @@ aiopvapi==1.6.19 aiopvpc==3.0.0 # homeassistant.components.sonarr -aiopyarr==22.6.0 +aiopyarr==22.7.0 # homeassistant.components.qnap_qsw aioqsw==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 894ca0f2ab4..6d792218e83 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -195,7 +195,7 @@ aiopvapi==1.6.19 aiopvpc==3.0.0 # homeassistant.components.sonarr -aiopyarr==22.6.0 +aiopyarr==22.7.0 # homeassistant.components.qnap_qsw aioqsw==0.1.0 From 430d3e4604e6929e6ef671282b267a4fc8622b74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 9 Jul 2022 18:07:46 +0300 Subject: [PATCH 2315/3516] Look for huawei_lte device MACs in a few more device info attributes (#74795) --- homeassistant/components/huawei_lte/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/huawei_lte/utils.py b/homeassistant/components/huawei_lte/utils.py index 37f9f1a5542..bbcf29e552c 100644 --- a/homeassistant/components/huawei_lte/utils.py +++ b/homeassistant/components/huawei_lte/utils.py @@ -14,7 +14,10 @@ def get_device_macs( :param device_info: the device.information structure for the device :param wlan_settings: the wlan.multi_basic_settings structure for the device """ - macs = [device_info.get("MacAddress1"), device_info.get("MacAddress2")] + macs = [ + device_info.get(x) + for x in ("MacAddress1", "MacAddress2", "WifiMacAddrWl0", "WifiMacAddrWl1") + ] try: macs.extend(x.get("WifiMac") for x in wlan_settings["Ssids"]["Ssid"]) except Exception: # pylint: disable=broad-except From a9c97e5d3a10763132ca045ba32da4265adbcfe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 9 Jul 2022 18:08:40 +0300 Subject: [PATCH 2316/3516] Sort huawei_lte sensor meta dict, add section separators (#74782) --- homeassistant/components/huawei_lte/sensor.py | 217 ++++++++++-------- 1 file changed, 116 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 85f63ed2bc6..642d8368207 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -62,9 +62,18 @@ class SensorMeta(NamedTuple): SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { + # + # Device information + # KEY_DEVICE_INFORMATION: SensorMeta( include=re.compile(r"^(WanIP.*Address|uptime)$", re.IGNORECASE) ), + (KEY_DEVICE_INFORMATION, "uptime"): SensorMeta( + name="Uptime", + icon="mdi:timer-outline", + native_unit_of_measurement=TIME_SECONDS, + entity_category=EntityCategory.DIAGNOSTIC, + ), (KEY_DEVICE_INFORMATION, "WanIPAddress"): SensorMeta( name="WAN IP address", icon="mdi:ip", @@ -76,12 +85,9 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_INFORMATION, "uptime"): SensorMeta( - name="Uptime", - icon="mdi:timer-outline", - native_unit_of_measurement=TIME_SECONDS, - entity_category=EntityCategory.DIAGNOSTIC, - ), + # + # Signal + # (KEY_DEVICE_SIGNAL, "band"): SensorMeta( name="Band", entity_category=EntityCategory.DIAGNOSTIC, @@ -91,6 +97,15 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { icon="mdi:transmission-tower", entity_category=EntityCategory.DIAGNOSTIC, ), + (KEY_DEVICE_SIGNAL, "cqi0"): SensorMeta( + name="CQI 0", + icon="mdi:speedometer", + entity_category=EntityCategory.DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "cqi1"): SensorMeta( + name="CQI 1", + icon="mdi:speedometer", + ), (KEY_DEVICE_SIGNAL, "dl_mcs"): SensorMeta( name="Downlink MCS", entity_category=EntityCategory.DIAGNOSTIC, @@ -108,49 +123,42 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { name="EARFCN", entity_category=EntityCategory.DIAGNOSTIC, ), + (KEY_DEVICE_SIGNAL, "ecio"): SensorMeta( + name="EC/IO", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + # https://wiki.teltonika.lt/view/EC/IO + icon=lambda x: ( + "mdi:signal-cellular-outline", + "mdi:signal-cellular-1", + "mdi:signal-cellular-2", + "mdi:signal-cellular-3", + )[bisect((-20, -10, -6), x if x is not None else -1000)], + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "enodeb_id"): SensorMeta( + name="eNodeB ID", + entity_category=EntityCategory.DIAGNOSTIC, + ), (KEY_DEVICE_SIGNAL, "lac"): SensorMeta( name="LAC", icon="mdi:map-marker", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "plmn"): SensorMeta( - name="PLMN", + (KEY_DEVICE_SIGNAL, "ltedlfreq"): SensorMeta( + name="Downlink frequency", + formatter=lambda x: ( + round(int(x) / 10) if x is not None else None, + FREQUENCY_MEGAHERTZ, + ), entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "rac"): SensorMeta( - name="RAC", - icon="mdi:map-marker", - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "rrc_status"): SensorMeta( - name="RRC status", - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "tac"): SensorMeta( - name="TAC", - icon="mdi:map-marker", - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "tdd"): SensorMeta( - name="TDD", - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "txpower"): SensorMeta( - name="Transmit power", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "ul_mcs"): SensorMeta( - name="Uplink MCS", - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "ulbandwidth"): SensorMeta( - name="Uplink bandwidth", - icon=lambda x: ( - "mdi:speedometer-slow", - "mdi:speedometer-medium", - "mdi:speedometer", - )[bisect((8, 15), x if x is not None else -1000)], + (KEY_DEVICE_SIGNAL, "lteulfreq"): SensorMeta( + name="Uplink frequency", + formatter=lambda x: ( + round(int(x) / 10) if x is not None else None, + FREQUENCY_MEGAHERTZ, + ), entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "mode"): SensorMeta( @@ -168,19 +176,31 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { icon="mdi:transmission-tower", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "rsrq"): SensorMeta( - name="RSRQ", + (KEY_DEVICE_SIGNAL, "plmn"): SensorMeta( + name="PLMN", + entity_category=EntityCategory.DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "rac"): SensorMeta( + name="RAC", + icon="mdi:map-marker", + entity_category=EntityCategory.DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "rrc_status"): SensorMeta( + name="RRC status", + entity_category=EntityCategory.DIAGNOSTIC, + ), + (KEY_DEVICE_SIGNAL, "rscp"): SensorMeta( + name="RSCP", device_class=SensorDeviceClass.SIGNAL_STRENGTH, - # http://www.lte-anbieter.info/technik/rsrq.php + # https://wiki.teltonika.lt/view/RSCP icon=lambda x: ( "mdi:signal-cellular-outline", "mdi:signal-cellular-1", "mdi:signal-cellular-2", "mdi:signal-cellular-3", - )[bisect((-11, -8, -5), x if x is not None else -1000)], + )[bisect((-95, -85, -75), x if x is not None else -1000)], state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=True, ), (KEY_DEVICE_SIGNAL, "rsrp"): SensorMeta( name="RSRP", @@ -196,6 +216,20 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, ), + (KEY_DEVICE_SIGNAL, "rsrq"): SensorMeta( + name="RSRQ", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + # http://www.lte-anbieter.info/technik/rsrq.php + icon=lambda x: ( + "mdi:signal-cellular-outline", + "mdi:signal-cellular-1", + "mdi:signal-cellular-2", + "mdi:signal-cellular-3", + )[bisect((-11, -8, -5), x if x is not None else -1000)], + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=True, + ), (KEY_DEVICE_SIGNAL, "rssi"): SensorMeta( name="RSSI", device_class=SensorDeviceClass.SIGNAL_STRENGTH, @@ -224,65 +258,40 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, ), - (KEY_DEVICE_SIGNAL, "rscp"): SensorMeta( - name="RSCP", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - # https://wiki.teltonika.lt/view/RSCP - icon=lambda x: ( - "mdi:signal-cellular-outline", - "mdi:signal-cellular-1", - "mdi:signal-cellular-2", - "mdi:signal-cellular-3", - )[bisect((-95, -85, -75), x if x is not None else -1000)], - state_class=SensorStateClass.MEASUREMENT, + (KEY_DEVICE_SIGNAL, "tac"): SensorMeta( + name="TAC", + icon="mdi:map-marker", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "ecio"): SensorMeta( - name="EC/IO", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - # https://wiki.teltonika.lt/view/EC/IO - icon=lambda x: ( - "mdi:signal-cellular-outline", - "mdi:signal-cellular-1", - "mdi:signal-cellular-2", - "mdi:signal-cellular-3", - )[bisect((-20, -10, -6), x if x is not None else -1000)], - state_class=SensorStateClass.MEASUREMENT, + (KEY_DEVICE_SIGNAL, "tdd"): SensorMeta( + name="TDD", entity_category=EntityCategory.DIAGNOSTIC, ), (KEY_DEVICE_SIGNAL, "transmode"): SensorMeta( name="Transmission mode", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "cqi0"): SensorMeta( - name="CQI 0", - icon="mdi:speedometer", + (KEY_DEVICE_SIGNAL, "txpower"): SensorMeta( + name="Transmit power", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "cqi1"): SensorMeta( - name="CQI 1", - icon="mdi:speedometer", - ), - (KEY_DEVICE_SIGNAL, "enodeb_id"): SensorMeta( - name="eNodeB ID", + (KEY_DEVICE_SIGNAL, "ul_mcs"): SensorMeta( + name="Uplink MCS", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_DEVICE_SIGNAL, "ltedlfreq"): SensorMeta( - name="Downlink frequency", - formatter=lambda x: ( - round(int(x) / 10) if x is not None else None, - FREQUENCY_MEGAHERTZ, - ), - entity_category=EntityCategory.DIAGNOSTIC, - ), - (KEY_DEVICE_SIGNAL, "lteulfreq"): SensorMeta( - name="Uplink frequency", - formatter=lambda x: ( - round(int(x) / 10) if x is not None else None, - FREQUENCY_MEGAHERTZ, - ), + (KEY_DEVICE_SIGNAL, "ulbandwidth"): SensorMeta( + name="Uplink bandwidth", + icon=lambda x: ( + "mdi:speedometer-slow", + "mdi:speedometer-medium", + "mdi:speedometer", + )[bisect((8, 15), x if x is not None else -1000)], entity_category=EntityCategory.DIAGNOSTIC, ), + # + # Monitoring + # KEY_MONITORING_CHECK_NOTIFICATIONS: SensorMeta( exclude=re.compile( r"^(onlineupdatestatus|smsstoragefull)$", @@ -331,13 +340,13 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_MONITORING_STATUS, "SecondaryDns"): SensorMeta( - name="Secondary DNS server", + (KEY_MONITORING_STATUS, "PrimaryIPv6Dns"): SensorMeta( + name="Primary IPv6 DNS server", icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), - (KEY_MONITORING_STATUS, "PrimaryIPv6Dns"): SensorMeta( - name="Primary IPv6 DNS server", + (KEY_MONITORING_STATUS, "SecondaryDns"): SensorMeta( + name="Secondary DNS server", icon="mdi:ip", entity_category=EntityCategory.DIAGNOSTIC, ), @@ -396,14 +405,12 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { icon="mdi:upload", state_class=SensorStateClass.TOTAL_INCREASING, ), + # + # Network + # KEY_NET_CURRENT_PLMN: SensorMeta( exclude=re.compile(r"^(Rat|ShortName|Spn)$", re.IGNORECASE) ), - (KEY_NET_CURRENT_PLMN, "State"): SensorMeta( - name="Operator search mode", - formatter=lambda x: ({"0": "Auto", "1": "Manual"}.get(x, "Unknown"), None), - entity_category=EntityCategory.CONFIG, - ), (KEY_NET_CURRENT_PLMN, "FullName"): SensorMeta( name="Operator name", entity_category=EntityCategory.DIAGNOSTIC, @@ -412,6 +419,11 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { name="Operator code", entity_category=EntityCategory.DIAGNOSTIC, ), + (KEY_NET_CURRENT_PLMN, "State"): SensorMeta( + name="Operator search mode", + formatter=lambda x: ({"0": "Auto", "1": "Manual"}.get(x, "Unknown"), None), + entity_category=EntityCategory.CONFIG, + ), KEY_NET_NET_MODE: SensorMeta(include=re.compile(r"^NetworkMode$", re.IGNORECASE)), (KEY_NET_NET_MODE, "NetworkMode"): SensorMeta( name="Preferred mode", @@ -429,6 +441,9 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { ), entity_category=EntityCategory.CONFIG, ), + # + # SMS + # (KEY_SMS_SMS_COUNT, "LocalDeleted"): SensorMeta( name="SMS deleted (device)", icon="mdi:email-minus", From 8bf692d046373238601b0012712de7cd3e6a63df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 9 Jul 2022 17:13:27 +0200 Subject: [PATCH 2317/3516] Update aioqsw to v0.1.1 (#74784) --- homeassistant/components/qnap_qsw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qnap_qsw/manifest.json b/homeassistant/components/qnap_qsw/manifest.json index be565f2a07e..83c9423f0f4 100644 --- a/homeassistant/components/qnap_qsw/manifest.json +++ b/homeassistant/components/qnap_qsw/manifest.json @@ -3,7 +3,7 @@ "name": "QNAP QSW", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/qnap_qsw", - "requirements": ["aioqsw==0.1.0"], + "requirements": ["aioqsw==0.1.1"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioqsw"], diff --git a/requirements_all.txt b/requirements_all.txt index 8f0fcb9a608..3f96f8eb711 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -229,7 +229,7 @@ aiopvpc==3.0.0 aiopyarr==22.7.0 # homeassistant.components.qnap_qsw -aioqsw==0.1.0 +aioqsw==0.1.1 # homeassistant.components.recollect_waste aiorecollect==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d792218e83..ef1b8f5941e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -198,7 +198,7 @@ aiopvpc==3.0.0 aiopyarr==22.7.0 # homeassistant.components.qnap_qsw -aioqsw==0.1.0 +aioqsw==0.1.1 # homeassistant.components.recollect_waste aiorecollect==1.0.8 From 3a5cca3ff2c9e649eac1d81842160f56e78eee15 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 9 Jul 2022 17:15:08 +0200 Subject: [PATCH 2318/3516] Convert rfxtrx to entity naming (#74720) --- homeassistant/components/rfxtrx/__init__.py | 19 ++--------- homeassistant/components/rfxtrx/sensor.py | 28 +++++++++++++++- tests/components/rfxtrx/test_config_flow.py | 24 +++++++------- tests/components/rfxtrx/test_sensor.py | 36 ++++++++++----------- 4 files changed, 60 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index b517abafc86..60ca25ed6d7 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -456,6 +456,9 @@ class RfxtrxEntity(RestoreEntity): Contains the common logic for Rfxtrx lights and switches. """ + _attr_assumed_state = True + _attr_has_entity_name = True + _attr_should_poll = False _device: rfxtrxmod.RFXtrxDevice _event: rfxtrxmod.RFXtrxEvent | None @@ -466,7 +469,6 @@ class RfxtrxEntity(RestoreEntity): event: rfxtrxmod.RFXtrxEvent | None = None, ) -> None: """Initialize the device.""" - self._name = f"{device.type_string} {device.id_string}" self._device = device self._event = event self._device_id = device_id @@ -484,16 +486,6 @@ class RfxtrxEntity(RestoreEntity): async_dispatcher_connect(self.hass, SIGNAL_EVENT, self._handle_event) ) - @property - def should_poll(self): - """No polling needed for a RFXtrx switch.""" - return False - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - @property def extra_state_attributes(self): """Return the device state attributes.""" @@ -501,11 +493,6 @@ class RfxtrxEntity(RestoreEntity): return None return {ATTR_EVENT: "".join(f"{x:02x}" for x in self._event.data)} - @property - def assumed_state(self): - """Return true if unable to access real state of entity.""" - return True - @property def unique_id(self): """Return unique identifier of remote device.""" diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 3111f0eabd7..6ff3073b900 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -63,12 +63,14 @@ class RfxtrxSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES = ( RfxtrxSensorEntityDescription( key="Barometer", + name="Barometer", device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PRESSURE_HPA, ), RfxtrxSensorEntityDescription( key="Battery numeric", + name="Battery", device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, @@ -77,42 +79,49 @@ SENSOR_TYPES = ( ), RfxtrxSensorEntityDescription( key="Current", + name="Current", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Current Ch. 1", + name="Current Ch. 1", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Current Ch. 2", + name="Current Ch. 2", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Current Ch. 3", + name="Current Ch. 3", device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, ), RfxtrxSensorEntityDescription( key="Energy usage", + name="Instantaneous power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_WATT, ), RfxtrxSensorEntityDescription( key="Humidity", + name="Humidity", device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, ), RfxtrxSensorEntityDescription( key="Rssi numeric", + name="Signal strength", device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -121,86 +130,104 @@ SENSOR_TYPES = ( ), RfxtrxSensorEntityDescription( key="Temperature", + name="Temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), RfxtrxSensorEntityDescription( key="Temperature2", + name="Temperature 2", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), RfxtrxSensorEntityDescription( key="Total usage", + name="Total energy usage", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), RfxtrxSensorEntityDescription( key="Voltage", + name="Voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, ), RfxtrxSensorEntityDescription( key="Wind direction", + name="Wind direction", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=DEGREE, ), RfxtrxSensorEntityDescription( key="Rain rate", + name="Rain rate", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, ), RfxtrxSensorEntityDescription( key="Sound", + name="Sound", ), RfxtrxSensorEntityDescription( key="Sensor Status", + name="Sensor status", ), RfxtrxSensorEntityDescription( key="Count", + name="Count", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement="count", ), RfxtrxSensorEntityDescription( key="Counter value", + name="Counter value", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement="count", ), RfxtrxSensorEntityDescription( key="Chill", + name="Chill", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), RfxtrxSensorEntityDescription( key="Wind average speed", + name="Wind average speed", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SPEED_METERS_PER_SECOND, ), RfxtrxSensorEntityDescription( key="Wind gust", + name="Wind gust", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=SPEED_METERS_PER_SECOND, ), RfxtrxSensorEntityDescription( key="Rain total", + name="Rain total", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=LENGTH_MILLIMETERS, ), RfxtrxSensorEntityDescription( key="Forecast", + name="Forecast status", ), RfxtrxSensorEntityDescription( key="Forecast numeric", + name="Forecast", ), RfxtrxSensorEntityDescription( key="Humidity status", + name="Humidity status", ), RfxtrxSensorEntityDescription( key="UV", + name="UV index", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UV_INDEX, ), @@ -252,7 +279,6 @@ class RfxtrxSensor(RfxtrxEntity, SensorEntity): """Initialize the sensor.""" super().__init__(device, device_id, event=event) self.entity_description = entity_description - self._name = f"{device.type_string} {device.id_string} {entity_description.key}" self._unique_id = "_".join( x for x in (*self._device_id, entity_description.key) ) diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index 23ce5d1393f..555d780fe92 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -475,11 +475,11 @@ async def test_options_replace_sensor_device(hass): await start_options_flow(hass, entry) state = hass.states.get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_rssi_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_signal_strength" ) assert state state = hass.states.get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_battery_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_battery" ) assert state state = hass.states.get( @@ -495,11 +495,11 @@ async def test_options_replace_sensor_device(hass): ) assert state state = hass.states.get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_rssi_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_signal_strength" ) assert state state = hass.states.get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_battery_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_battery" ) assert state state = hass.states.get( @@ -565,7 +565,7 @@ async def test_options_replace_sensor_device(hass): entity_registry = er.async_get(hass) entry = entity_registry.async_get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_rssi_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_signal_strength" ) assert entry assert entry.device_id == new_device @@ -580,7 +580,7 @@ async def test_options_replace_sensor_device(hass): assert entry assert entry.device_id == new_device entry = entity_registry.async_get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_battery_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_f0_04_battery" ) assert entry assert entry.device_id == new_device @@ -591,11 +591,11 @@ async def test_options_replace_sensor_device(hass): assert entry.device_id == new_device state = hass.states.get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_rssi_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_signal_strength" ) assert not state state = hass.states.get( - "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_battery_numeric" + "sensor.thgn122_123_thgn132_thgr122_228_238_268_23_04_battery" ) assert not state state = hass.states.get( @@ -637,13 +637,13 @@ async def test_options_replace_control_device(hass): state = hass.states.get("binary_sensor.ac_118cdea_2") assert state - state = hass.states.get("sensor.ac_118cdea_2_rssi_numeric") + state = hass.states.get("sensor.ac_118cdea_2_signal_strength") assert state state = hass.states.get("switch.ac_118cdea_2") assert state state = hass.states.get("binary_sensor.ac_1118cdea_2") assert state - state = hass.states.get("sensor.ac_1118cdea_2_rssi_numeric") + state = hass.states.get("sensor.ac_1118cdea_2_signal_strength") assert state state = hass.states.get("switch.ac_1118cdea_2") assert state @@ -700,7 +700,7 @@ async def test_options_replace_control_device(hass): entry = entity_registry.async_get("binary_sensor.ac_118cdea_2") assert entry assert entry.device_id == new_device - entry = entity_registry.async_get("sensor.ac_118cdea_2_rssi_numeric") + entry = entity_registry.async_get("sensor.ac_118cdea_2_signal_strength") assert entry assert entry.device_id == new_device entry = entity_registry.async_get("switch.ac_118cdea_2") @@ -709,7 +709,7 @@ async def test_options_replace_control_device(hass): state = hass.states.get("binary_sensor.ac_1118cdea_2") assert not state - state = hass.states.get("sensor.ac_1118cdea_2_rssi_numeric") + state = hass.states.get("sensor.ac_1118cdea_2_signal_strength") assert not state state = hass.states.get("switch.ac_1118cdea_2") assert not state diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py index 77f9960de49..8ba0104cf50 100644 --- a/tests/components/rfxtrx/test_sensor.py +++ b/tests/components/rfxtrx/test_sensor.py @@ -101,19 +101,19 @@ async def test_one_sensor_no_datatype(hass, rfxtrx): assert state.attributes.get("friendly_name") == f"{base_name} Humidity status" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - state = hass.states.get(f"{base_id}_rssi_numeric") + state = hass.states.get(f"{base_id}_signal_strength") assert state assert state.state == "unknown" - assert state.attributes.get("friendly_name") == f"{base_name} Rssi numeric" + assert state.attributes.get("friendly_name") == f"{base_name} Signal strength" assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS_MILLIWATT ) - state = hass.states.get(f"{base_id}_battery_numeric") + state = hass.states.get(f"{base_id}_battery") assert state assert state.state == "unknown" - assert state.attributes.get("friendly_name") == f"{base_name} Battery numeric" + assert state.attributes.get("friendly_name") == f"{base_name} Battery" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -179,7 +179,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state.state == "normal" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - state = hass.states.get(f"{base_id}_rssi_numeric") + state = hass.states.get(f"{base_id}_signal_strength") assert state assert state.state == "-64" assert ( @@ -192,7 +192,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state.state == "18.4" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - state = hass.states.get(f"{base_id}_battery_numeric") + state = hass.states.get(f"{base_id}_battery") assert state assert state.state == "100" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -211,7 +211,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state.state == "normal" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - state = hass.states.get(f"{base_id}_rssi_numeric") + state = hass.states.get(f"{base_id}_signal_strength") assert state assert state.state == "-64" assert ( @@ -224,7 +224,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state.state == "14.9" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - state = hass.states.get(f"{base_id}_battery_numeric") + state = hass.states.get(f"{base_id}_battery") assert state assert state.state == "100" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -243,7 +243,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state.state == "normal" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None - state = hass.states.get(f"{base_id}_rssi_numeric") + state = hass.states.get(f"{base_id}_signal_strength") assert state assert state.state == "-64" assert ( @@ -256,7 +256,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): assert state.state == "17.9" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS - state = hass.states.get(f"{base_id}_battery_numeric") + state = hass.states.get(f"{base_id}_battery") assert state assert state.state == "100" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -328,19 +328,19 @@ async def test_rssi_sensor(hass, rfxtrx): await hass.async_block_till_done() await hass.async_start() - state = hass.states.get("sensor.pt2262_22670e_rssi_numeric") + state = hass.states.get("sensor.pt2262_22670e_signal_strength") assert state assert state.state == "unknown" - assert state.attributes.get("friendly_name") == "PT2262 22670e Rssi numeric" + assert state.attributes.get("friendly_name") == "PT2262 22670e Signal strength" assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS_MILLIWATT ) - state = hass.states.get("sensor.ac_213c7f2_48_rssi_numeric") + state = hass.states.get("sensor.ac_213c7f2_48_signal_strength") assert state assert state.state == "unknown" - assert state.attributes.get("friendly_name") == "AC 213c7f2:48 Rssi numeric" + assert state.attributes.get("friendly_name") == "AC 213c7f2:48 Signal strength" assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS_MILLIWATT @@ -349,21 +349,21 @@ async def test_rssi_sensor(hass, rfxtrx): await rfxtrx.signal("0913000022670e013b70") await rfxtrx.signal("0b1100cd0213c7f230010f71") - state = hass.states.get("sensor.pt2262_22670e_rssi_numeric") + state = hass.states.get("sensor.pt2262_22670e_signal_strength") assert state assert state.state == "-64" - state = hass.states.get("sensor.ac_213c7f2_48_rssi_numeric") + state = hass.states.get("sensor.ac_213c7f2_48_signal_strength") assert state assert state.state == "-64" await rfxtrx.signal("0913000022670e013b60") await rfxtrx.signal("0b1100cd0213c7f230010f61") - state = hass.states.get("sensor.pt2262_22670e_rssi_numeric") + state = hass.states.get("sensor.pt2262_22670e_signal_strength") assert state assert state.state == "-72" - state = hass.states.get("sensor.ac_213c7f2_48_rssi_numeric") + state = hass.states.get("sensor.ac_213c7f2_48_signal_strength") assert state assert state.state == "-72" From cd03c49fc27d3b2c8c33ad032a9207acdd71b9ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 9 Jul 2022 10:27:42 -0500 Subject: [PATCH 2319/3516] Wait for config entry platform forwards (#73806) --- homeassistant/components/abode/__init__.py | 2 +- .../components/accuweather/__init__.py | 2 +- homeassistant/components/acmeda/__init__.py | 2 +- homeassistant/components/adax/__init__.py | 2 +- homeassistant/components/adguard/__init__.py | 2 +- .../components/advantage_air/__init__.py | 2 +- homeassistant/components/aemet/__init__.py | 2 +- .../components/agent_dvr/__init__.py | 2 +- homeassistant/components/airly/__init__.py | 2 +- homeassistant/components/airnow/__init__.py | 2 +- .../components/airthings/__init__.py | 2 +- .../components/airtouch4/__init__.py | 2 +- .../components/airvisual/__init__.py | 2 +- homeassistant/components/airzone/__init__.py | 2 +- .../components/aladdin_connect/__init__.py | 2 +- .../components/alarmdecoder/__init__.py | 2 +- homeassistant/components/ambee/__init__.py | 2 +- .../components/amberelectric/__init__.py | 2 +- .../components/ambiclimate/__init__.py | 2 +- .../components/androidtv/__init__.py | 2 +- homeassistant/components/apple_tv/__init__.py | 13 ++----- .../components/arcam_fmj/__init__.py | 2 +- .../components/aseko_pool_live/__init__.py | 2 +- homeassistant/components/asuswrt/__init__.py | 2 +- homeassistant/components/atag/__init__.py | 2 +- homeassistant/components/august/__init__.py | 2 +- homeassistant/components/aurora/__init__.py | 2 +- .../aurora_abb_powerone/__init__.py | 2 +- .../components/aussie_broadband/__init__.py | 2 +- homeassistant/components/awair/__init__.py | 2 +- .../components/azure_devops/__init__.py | 2 +- homeassistant/components/baf/__init__.py | 2 +- homeassistant/components/balboa/__init__.py | 2 +- homeassistant/components/blebox/__init__.py | 3 +- homeassistant/components/blink/__init__.py | 2 +- .../bmw_connected_drive/__init__.py | 2 +- homeassistant/components/bond/__init__.py | 2 +- .../components/bosch_shc/__init__.py | 2 +- homeassistant/components/braviatv/__init__.py | 2 +- homeassistant/components/broadlink/device.py | 2 +- homeassistant/components/brother/__init__.py | 2 +- homeassistant/components/brunt/__init__.py | 2 +- homeassistant/components/bsblan/__init__.py | 2 +- .../components/buienradar/__init__.py | 2 +- homeassistant/components/canary/__init__.py | 2 +- .../components/cert_expiry/__init__.py | 2 +- .../components/climacell/__init__.py | 2 +- .../components/co2signal/__init__.py | 2 +- homeassistant/components/coinbase/__init__.py | 2 +- homeassistant/components/control4/__init__.py | 2 +- .../components/coolmaster/__init__.py | 2 +- .../components/coronavirus/__init__.py | 2 +- homeassistant/components/cpuspeed/__init__.py | 2 +- .../components/crownstone/entry_manager.py | 4 ++- homeassistant/components/daikin/__init__.py | 2 +- homeassistant/components/deluge/__init__.py | 2 +- homeassistant/components/demo/__init__.py | 7 ++-- homeassistant/components/denonavr/__init__.py | 2 +- .../components/derivative/__init__.py | 2 +- .../components/device_tracker/__init__.py | 8 +++++ .../devolo_home_control/__init__.py | 2 +- .../devolo_home_network/__init__.py | 2 +- homeassistant/components/dexcom/__init__.py | 2 +- homeassistant/components/directv/__init__.py | 2 +- homeassistant/components/dlna_dmr/__init__.py | 2 +- homeassistant/components/dnsip/__init__.py | 2 +- homeassistant/components/doorbird/__init__.py | 2 +- homeassistant/components/dsmr/__init__.py | 2 +- homeassistant/components/dunehd/__init__.py | 2 +- homeassistant/components/dynalite/__init__.py | 2 +- homeassistant/components/eafm/__init__.py | 2 +- homeassistant/components/ecobee/__init__.py | 2 +- homeassistant/components/econet/__init__.py | 2 +- homeassistant/components/efergy/__init__.py | 2 +- .../components/eight_sleep/__init__.py | 2 +- homeassistant/components/elgato/__init__.py | 2 +- homeassistant/components/elkm1/__init__.py | 2 +- homeassistant/components/elmax/__init__.py | 2 +- homeassistant/components/emonitor/__init__.py | 2 +- .../components/enphase_envoy/__init__.py | 2 +- .../components/environment_canada/__init__.py | 2 +- homeassistant/components/epson/__init__.py | 2 +- homeassistant/components/esphome/__init__.py | 19 +++++------ .../components/esphome/entry_data.py | 9 ++--- .../components/evil_genius_labs/__init__.py | 2 +- homeassistant/components/ezviz/__init__.py | 2 +- .../components/faa_delays/__init__.py | 2 +- homeassistant/components/fibaro/__init__.py | 2 +- homeassistant/components/filesize/__init__.py | 2 +- .../components/fireservicerota/__init__.py | 2 +- homeassistant/components/firmata/__init__.py | 13 ++++--- homeassistant/components/fivem/__init__.py | 2 +- .../components/flick_electric/__init__.py | 2 +- homeassistant/components/flipr/__init__.py | 2 +- homeassistant/components/flo/__init__.py | 2 +- homeassistant/components/flume/__init__.py | 2 +- .../components/flunearyou/__init__.py | 2 +- homeassistant/components/flux_led/__init__.py | 2 +- .../components/forecast_solar/__init__.py | 2 +- .../components/forked_daapd/__init__.py | 2 +- homeassistant/components/foscam/__init__.py | 2 +- homeassistant/components/freebox/__init__.py | 2 +- .../components/freedompro/__init__.py | 2 +- homeassistant/components/fritz/__init__.py | 2 +- homeassistant/components/fritzbox/__init__.py | 2 +- .../fritzbox_callmonitor/__init__.py | 2 +- homeassistant/components/fronius/__init__.py | 2 +- .../components/garages_amsterdam/__init__.py | 2 +- homeassistant/components/gdacs/__init__.py | 4 ++- homeassistant/components/generic/__init__.py | 2 +- .../components/geocaching/__init__.py | 2 +- homeassistant/components/geofency/__init__.py | 2 +- .../components/geonetnz_quakes/__init__.py | 4 ++- .../components/geonetnz_volcano/__init__.py | 4 ++- homeassistant/components/gios/__init__.py | 2 +- homeassistant/components/github/__init__.py | 2 +- homeassistant/components/glances/__init__.py | 4 ++- homeassistant/components/goalzero/__init__.py | 2 +- .../components/gogogate2/__init__.py | 2 +- homeassistant/components/goodwe/__init__.py | 2 +- homeassistant/components/google/__init__.py | 2 +- .../components/google_travel_time/__init__.py | 2 +- .../components/gpslogger/__init__.py | 2 +- homeassistant/components/gree/__init__.py | 2 +- homeassistant/components/group/__init__.py | 4 ++- .../components/growatt_server/__init__.py | 2 +- homeassistant/components/guardian/__init__.py | 2 +- homeassistant/components/habitica/__init__.py | 2 +- homeassistant/components/harmony/__init__.py | 2 +- homeassistant/components/hassio/__init__.py | 2 +- .../components/here_travel_time/__init__.py | 2 +- .../components/hisense_aehw4a1/__init__.py | 2 +- homeassistant/components/hive/__init__.py | 14 ++++---- homeassistant/components/hlk_sw16/__init__.py | 34 ++++++++----------- .../components/home_connect/__init__.py | 2 +- .../components/home_plus_control/__init__.py | 15 ++------ .../components/homewizard/__init__.py | 2 +- .../components/honeywell/__init__.py | 2 +- .../components/huawei_lte/__init__.py | 2 +- homeassistant/components/hue/bridge.py | 4 +-- .../components/huisbaasje/__init__.py | 2 +- .../hunterdouglas_powerview/__init__.py | 2 +- .../components/hvv_departures/__init__.py | 2 +- homeassistant/components/hyperion/__init__.py | 21 ++++-------- homeassistant/components/ialarm/__init__.py | 2 +- .../components/iaqualink/__init__.py | 14 ++++---- homeassistant/components/icloud/__init__.py | 2 +- homeassistant/components/insteon/__init__.py | 5 +-- .../components/integration/__init__.py | 2 +- .../components/intellifire/__init__.py | 2 +- homeassistant/components/ios/__init__.py | 2 +- homeassistant/components/iotawatt/__init__.py | 2 +- homeassistant/components/ipma/__init__.py | 2 +- homeassistant/components/ipp/__init__.py | 2 +- homeassistant/components/iqvia/__init__.py | 2 +- homeassistant/components/iss/__init__.py | 2 +- homeassistant/components/isy994/__init__.py | 2 +- homeassistant/components/izone/__init__.py | 2 +- homeassistant/components/juicenet/__init__.py | 2 +- .../components/kaleidescape/__init__.py | 2 +- .../components/keenetic_ndms2/__init__.py | 2 +- homeassistant/components/kmtronic/__init__.py | 2 +- homeassistant/components/kodi/__init__.py | 2 +- .../components/kostal_plenticore/__init__.py | 2 +- homeassistant/components/kraken/__init__.py | 2 +- homeassistant/components/kulersky/__init__.py | 2 +- .../components/launch_library/__init__.py | 2 +- .../components/laundrify/__init__.py | 2 +- homeassistant/components/lcn/__init__.py | 2 +- homeassistant/components/lifx/__init__.py | 2 +- homeassistant/components/litejet/__init__.py | 2 +- .../components/litterrobot/__init__.py | 2 +- homeassistant/components/local_ip/__init__.py | 2 +- homeassistant/components/locative/__init__.py | 2 +- .../components/logi_circle/__init__.py | 2 +- homeassistant/components/lookin/__init__.py | 2 +- .../components/luftdaten/__init__.py | 2 +- .../components/lutron_caseta/__init__.py | 2 +- homeassistant/components/lyric/__init__.py | 2 +- homeassistant/components/mazda/__init__.py | 2 +- homeassistant/components/meater/__init__.py | 2 +- homeassistant/components/melcloud/__init__.py | 2 +- homeassistant/components/met/__init__.py | 2 +- .../components/met_eireann/__init__.py | 2 +- .../components/meteo_france/__init__.py | 2 +- .../components/meteoclimatic/__init__.py | 2 +- .../components/metoffice/__init__.py | 2 +- homeassistant/components/mill/__init__.py | 2 +- homeassistant/components/min_max/__init__.py | 2 +- .../components/minecraft_server/__init__.py | 2 +- homeassistant/components/mjpeg/__init__.py | 2 +- .../components/mobile_app/__init__.py | 2 +- .../components/modem_callerid/__init__.py | 2 +- .../components/modern_forms/__init__.py | 2 +- .../components/moehlenhoff_alpha2/__init__.py | 2 +- .../components/monoprice/__init__.py | 2 +- homeassistant/components/moon/__init__.py | 2 +- .../components/motion_blinds/__init__.py | 2 +- .../components/motioneye/__init__.py | 21 ++++-------- homeassistant/components/mqtt/__init__.py | 2 +- homeassistant/components/mullvad/__init__.py | 2 +- homeassistant/components/mutesync/__init__.py | 2 +- homeassistant/components/myq/__init__.py | 2 +- .../components/mysensors/__init__.py | 15 +++----- homeassistant/components/nam/__init__.py | 2 +- homeassistant/components/nanoleaf/__init__.py | 2 +- homeassistant/components/neato/__init__.py | 2 +- homeassistant/components/nest/__init__.py | 2 +- homeassistant/components/netatmo/__init__.py | 2 +- homeassistant/components/netgear/__init__.py | 2 +- homeassistant/components/nexia/__init__.py | 2 +- .../components/nightscout/__init__.py | 2 +- homeassistant/components/nina/__init__.py | 2 +- .../components/nmap_tracker/__init__.py | 2 +- homeassistant/components/notify/__init__.py | 7 ++++ homeassistant/components/notion/__init__.py | 2 +- homeassistant/components/nuheat/__init__.py | 2 +- homeassistant/components/nuki/__init__.py | 2 +- homeassistant/components/nut/__init__.py | 2 +- homeassistant/components/nws/__init__.py | 2 +- homeassistant/components/nzbget/__init__.py | 2 +- .../components/octoprint/__init__.py | 2 +- .../components/omnilogic/__init__.py | 2 +- homeassistant/components/oncue/__init__.py | 2 +- .../components/ondilo_ico/__init__.py | 2 +- homeassistant/components/onewire/__init__.py | 2 +- homeassistant/components/onvif/__init__.py | 2 +- .../components/open_meteo/__init__.py | 2 +- .../components/opengarage/__init__.py | 2 +- .../components/opentherm_gw/__init__.py | 2 +- homeassistant/components/openuv/__init__.py | 2 +- .../components/openweathermap/__init__.py | 2 +- homeassistant/components/overkiz/__init__.py | 2 +- .../components/ovo_energy/__init__.py | 2 +- .../components/owntracks/__init__.py | 2 +- .../components/p1_monitor/__init__.py | 2 +- .../components/panasonic_viera/__init__.py | 2 +- homeassistant/components/peco/__init__.py | 2 +- .../components/philips_js/__init__.py | 2 +- homeassistant/components/pi_hole/__init__.py | 2 +- homeassistant/components/picnic/__init__.py | 2 +- homeassistant/components/plaato/__init__.py | 2 +- homeassistant/components/plex/__init__.py | 9 +++-- homeassistant/components/plugwise/gateway.py | 2 +- .../components/plum_lightpad/__init__.py | 2 +- .../components/poolsense/__init__.py | 2 +- .../components/powerwall/__init__.py | 2 +- .../components/progettihwsw/__init__.py | 2 +- homeassistant/components/prosegur/__init__.py | 2 +- homeassistant/components/ps4/__init__.py | 2 +- .../components/pure_energie/__init__.py | 2 +- homeassistant/components/pvoutput/__init__.py | 2 +- .../pvpc_hourly_pricing/__init__.py | 2 +- homeassistant/components/qnap_qsw/__init__.py | 2 +- homeassistant/components/rachio/__init__.py | 2 +- .../components/radiotherm/__init__.py | 2 +- .../components/rainforest_eagle/__init__.py | 2 +- .../components/rainmachine/__init__.py | 2 +- homeassistant/components/rdw/__init__.py | 2 +- .../components/recollect_waste/__init__.py | 2 +- homeassistant/components/renault/__init__.py | 2 +- homeassistant/components/rfxtrx/__init__.py | 2 +- homeassistant/components/ridwell/__init__.py | 2 +- homeassistant/components/ring/__init__.py | 2 +- homeassistant/components/risco/__init__.py | 13 ++----- .../rituals_perfume_genie/__init__.py | 2 +- homeassistant/components/roku/__init__.py | 2 +- homeassistant/components/roomba/__init__.py | 2 +- homeassistant/components/roon/server.py | 4 ++- .../components/rpi_power/__init__.py | 2 +- .../components/ruckus_unleashed/__init__.py | 2 +- homeassistant/components/sabnzbd/__init__.py | 2 +- .../components/screenlogic/__init__.py | 2 +- homeassistant/components/season/__init__.py | 2 +- homeassistant/components/sense/__init__.py | 2 +- homeassistant/components/senseme/__init__.py | 2 +- homeassistant/components/sensibo/__init__.py | 2 +- homeassistant/components/senz/__init__.py | 2 +- homeassistant/components/sharkiq/__init__.py | 2 +- homeassistant/components/sia/__init__.py | 2 +- homeassistant/components/sia/hub.py | 2 +- .../components/simplisafe/__init__.py | 2 +- homeassistant/components/skybell/__init__.py | 2 +- homeassistant/components/sleepiq/__init__.py | 2 +- .../components/slimproto/__init__.py | 2 +- homeassistant/components/sma/__init__.py | 2 +- homeassistant/components/smappee/__init__.py | 2 +- .../components/smart_meter_texas/__init__.py | 2 +- .../components/smartthings/__init__.py | 2 +- homeassistant/components/smarttub/__init__.py | 2 +- homeassistant/components/smhi/__init__.py | 2 +- homeassistant/components/sms/__init__.py | 2 +- .../components/solaredge/__init__.py | 2 +- homeassistant/components/solarlog/__init__.py | 2 +- homeassistant/components/solax/__init__.py | 2 +- homeassistant/components/soma/__init__.py | 2 +- .../components/somfy_mylink/__init__.py | 2 +- homeassistant/components/sonarr/__init__.py | 2 +- homeassistant/components/songpal/__init__.py | 2 +- homeassistant/components/sonos/__init__.py | 9 ++--- .../components/speedtestdotnet/__init__.py | 2 +- homeassistant/components/spider/__init__.py | 2 +- homeassistant/components/spotify/__init__.py | 2 +- homeassistant/components/sql/__init__.py | 2 +- .../components/squeezebox/__init__.py | 2 +- .../components/srp_energy/__init__.py | 2 +- homeassistant/components/starline/__init__.py | 2 +- homeassistant/components/steamist/__init__.py | 2 +- .../components/stookalert/__init__.py | 2 +- homeassistant/components/subaru/__init__.py | 2 +- .../components/surepetcare/__init__.py | 2 +- .../components/switch_as_x/__init__.py | 2 +- .../components/switchbot/__init__.py | 4 ++- .../components/switcher_kis/__init__.py | 25 +++++--------- .../components/syncthing/__init__.py | 2 +- homeassistant/components/syncthru/__init__.py | 2 +- .../components/synology_dsm/__init__.py | 2 +- .../components/system_bridge/__init__.py | 2 +- homeassistant/components/tado/__init__.py | 2 +- .../components/tailscale/__init__.py | 2 +- .../components/tankerkoenig/__init__.py | 2 +- homeassistant/components/tasmota/__init__.py | 21 ++++-------- homeassistant/components/tautulli/__init__.py | 2 +- .../tesla_wall_connector/__init__.py | 2 +- .../components/threshold/__init__.py | 4 ++- homeassistant/components/tibber/__init__.py | 2 +- homeassistant/components/tile/__init__.py | 2 +- homeassistant/components/tod/__init__.py | 4 ++- homeassistant/components/tolo/__init__.py | 2 +- homeassistant/components/toon/__init__.py | 2 +- homeassistant/components/tplink/__init__.py | 2 +- homeassistant/components/traccar/__init__.py | 2 +- homeassistant/components/tractive/__init__.py | 2 +- homeassistant/components/tradfri/__init__.py | 2 +- .../components/trafikverket_ferry/__init__.py | 2 +- .../components/trafikverket_train/__init__.py | 2 +- .../trafikverket_weatherstation/__init__.py | 2 +- .../components/transmission/__init__.py | 4 ++- homeassistant/components/tuya/__init__.py | 2 +- .../components/twentemilieu/__init__.py | 2 +- homeassistant/components/twinkly/__init__.py | 2 +- .../components/ukraine_alarm/__init__.py | 2 +- .../components/unifiprotect/__init__.py | 2 +- homeassistant/components/upb/__init__.py | 2 +- homeassistant/components/upcloud/__init__.py | 2 +- homeassistant/components/upnp/__init__.py | 2 +- homeassistant/components/uptime/__init__.py | 2 +- .../components/uptimerobot/__init__.py | 2 +- .../components/utility_meter/__init__.py | 4 +-- homeassistant/components/vallox/__init__.py | 2 +- homeassistant/components/velbus/__init__.py | 2 +- homeassistant/components/venstar/__init__.py | 2 +- homeassistant/components/vera/__init__.py | 2 +- homeassistant/components/verisure/__init__.py | 2 +- homeassistant/components/version/__init__.py | 2 +- homeassistant/components/vesync/__init__.py | 11 +++--- homeassistant/components/vicare/__init__.py | 2 +- homeassistant/components/vilfo/__init__.py | 2 +- homeassistant/components/vizio/__init__.py | 2 +- .../components/vlc_telnet/__init__.py | 2 +- homeassistant/components/volumio/__init__.py | 2 +- homeassistant/components/vulcan/__init__.py | 2 +- homeassistant/components/wallbox/__init__.py | 2 +- homeassistant/components/watttime/__init__.py | 2 +- .../components/waze_travel_time/__init__.py | 2 +- homeassistant/components/webostv/__init__.py | 2 +- .../components/whirlpool/__init__.py | 2 +- homeassistant/components/whois/__init__.py | 2 +- homeassistant/components/wiffi/__init__.py | 2 +- homeassistant/components/wilight/__init__.py | 2 +- homeassistant/components/withings/__init__.py | 2 +- homeassistant/components/wiz/__init__.py | 2 +- homeassistant/components/wled/__init__.py | 2 +- homeassistant/components/wolflink/__init__.py | 2 +- homeassistant/components/ws66i/__init__.py | 2 +- homeassistant/components/xbox/__init__.py | 2 +- .../components/xiaomi_aqara/__init__.py | 2 +- .../components/xiaomi_miio/__init__.py | 7 ++-- .../components/yale_smart_alarm/__init__.py | 2 +- .../components/yamaha_musiccast/__init__.py | 2 +- homeassistant/components/yeelight/__init__.py | 2 +- homeassistant/components/yolink/__init__.py | 2 +- homeassistant/components/youless/__init__.py | 2 +- homeassistant/components/zerproc/__init__.py | 2 +- homeassistant/config_entries.py | 18 ++++++++-- homeassistant/setup.py | 6 ++-- 386 files changed, 528 insertions(+), 557 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index edd799d31f7..bd66aa66c9c 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -103,7 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = AbodeSystem(abode, polling) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await setup_hass_events(hass) await hass.async_add_executor_job(setup_hass_services, hass) diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index 70db587bd49..d36ea4e8466 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/acmeda/__init__.py b/homeassistant/components/acmeda/__init__.py index f9599a240e8..0bb7cbdc177 100644 --- a/homeassistant/components/acmeda/__init__.py +++ b/homeassistant/components/acmeda/__init__.py @@ -19,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return False hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = hub - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/adax/__init__.py b/homeassistant/components/adax/__init__.py index dc24e741c89..438f1fc74ad 100644 --- a/homeassistant/components/adax/__init__.py +++ b/homeassistant/components/adax/__init__.py @@ -10,7 +10,7 @@ PLATFORMS = [Platform.CLIMATE] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Adax from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 2a244a5fe80..2bb15d19223 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except AdGuardHomeConnectionError as exception: raise ConfigEntryNotReady from exception - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def add_url(call: ServiceCall) -> None: """Service call to add a new filter subscription to AdGuard Home.""" diff --git a/homeassistant/components/advantage_air/__init__.py b/homeassistant/components/advantage_air/__init__.py index 12c5a4593c5..d50224698b8 100644 --- a/homeassistant/components/advantage_air/__init__.py +++ b/homeassistant/components/advantage_air/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "async_change": async_change, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/aemet/__init__.py b/homeassistant/components/aemet/__init__.py index a914a23a0da..032e0a3a9f6 100644 --- a/homeassistant/components/aemet/__init__.py +++ b/homeassistant/components/aemet/__init__.py @@ -40,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ENTRY_WEATHER_COORDINATOR: weather_coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) diff --git a/homeassistant/components/agent_dvr/__init__.py b/homeassistant/components/agent_dvr/__init__.py index a2831fe301e..6723d62e9e0 100644 --- a/homeassistant/components/agent_dvr/__init__.py +++ b/homeassistant/components/agent_dvr/__init__.py @@ -48,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b sw_version=agent_client.version, ) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 31396ecf51b..a3feab1e6f8 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -110,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Remove air_quality entities from registry if they exist ent_reg = er.async_get(hass) diff --git a/homeassistant/components/airnow/__init__.py b/homeassistant/components/airnow/__init__.py index 9c6babe1136..7c26cded4de 100644 --- a/homeassistant/components/airnow/__init__.py +++ b/homeassistant/components/airnow/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/airthings/__init__.py b/homeassistant/components/airthings/__init__.py index 352c0249637..423e890a855 100644 --- a/homeassistant/components/airthings/__init__.py +++ b/homeassistant/components/airthings/__init__.py @@ -48,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/airtouch4/__init__.py b/homeassistant/components/airtouch4/__init__.py index 7b0673ecfe4..a2c3f716ab1 100644 --- a/homeassistant/components/airtouch4/__init__.py +++ b/homeassistant/components/airtouch4/__init__.py @@ -31,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index d0e1a741f2e..c1bb6020b3c 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -274,7 +274,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index cf57a28a5e1..65ce9193e07 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -79,7 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index af996c9f5b2..1a28a03cf05 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -31,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady("Can not connect to host") from ex hass.data.setdefault(DOMAIN, {})[entry.entry_id] = acc - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index d5c1b88e08e..7206b24632b 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -131,7 +131,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await open_connection() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ambee/__init__.py b/homeassistant/components/ambee/__init__.py index 4481afb09ca..ee311df75fa 100644 --- a/homeassistant/components/ambee/__init__.py +++ b/homeassistant/components/ambee/__init__.py @@ -58,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await pollen.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id][SERVICE_POLLEN] = pollen - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/amberelectric/__init__.py b/homeassistant/components/amberelectric/__init__.py index 0d39077f2f1..b6901e1b81b 100644 --- a/homeassistant/components/amberelectric/__init__.py +++ b/homeassistant/components/amberelectric/__init__.py @@ -19,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = AmberUpdateCoordinator(hass, api_instance, site_id) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ambiclimate/__init__.py b/homeassistant/components/ambiclimate/__init__.py index acab01e30d1..240c9780cee 100644 --- a/homeassistant/components/ambiclimate/__init__.py +++ b/homeassistant/components/ambiclimate/__init__.py @@ -41,5 +41,5 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ambiclimate from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py index 4b203fc3757..6942ff7ffd0 100644 --- a/homeassistant/components/androidtv/__init__.py +++ b/homeassistant/components/androidtv/__init__.py @@ -149,7 +149,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ANDROID_DEV_OPT: entry.options.copy(), } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 5177c6f3486..5d9c1cde785 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -67,17 +67,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) ) - async def setup_platforms(): - """Set up platforms and initiate connection.""" - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) - await manager.init() - - hass.async_create_task(setup_platforms()) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await manager.init() return True diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index a82e0239842..c2c6be0db30 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -63,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: task = asyncio.create_task(_run_client(hass, client, DEFAULT_SCAN_INTERVAL)) tasks[entry.entry_id] = task - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/aseko_pool_live/__init__.py b/homeassistant/components/aseko_pool_live/__init__.py index 213d0dabc91..70a66251bdc 100644 --- a/homeassistant/components/aseko_pool_live/__init__.py +++ b/homeassistant/components/aseko_pool_live/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id].append((unit, coordinator)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index 0532dd9c7cc..f3d12c3bd39 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -28,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_ASUSWRT: router} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/atag/__init__.py b/homeassistant/components/atag/__init__.py index 6eb17fded3f..56b2c1e969e 100644 --- a/homeassistant/components/atag/__init__.py +++ b/homeassistant/components/atag/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=atag.id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index e8df7e1072d..81842f995e8 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -88,7 +88,7 @@ async def async_setup_august( data = hass.data[DOMAIN][config_entry.entry_id] = AugustData(hass, august_gateway) await data.async_setup() - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index 01d6092a4f2..b8d0589d007 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: AURORA_API: api, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/aurora_abb_powerone/__init__.py b/homeassistant/components/aurora_abb_powerone/__init__.py index c988121b6bd..305a42d4dcc 100644 --- a/homeassistant/components/aurora_abb_powerone/__init__.py +++ b/homeassistant/components/aurora_abb_powerone/__init__.py @@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.data[CONF_ADDRESS] ser_client = AuroraSerialClient(address, comport, parity="N", timeout=1) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ser_client - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/aussie_broadband/__init__.py b/homeassistant/components/aussie_broadband/__init__.py index 6136ff6f8f3..b32e11f27c0 100644 --- a/homeassistant/components/aussie_broadband/__init__.py +++ b/homeassistant/components/aussie_broadband/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "client": client, "services": services, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index 2364b85a63e..ef39e488001 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index c6de8515979..645932f8b73 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -81,7 +81,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator, project - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 601215d4c61..9bb056ece4f 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -39,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady(f"Timed out connecting to {ip_address}") from ex hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BAFData(device, run_future) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/balboa/__init__.py b/homeassistant/components/balboa/__init__.py index 60989ecc6d6..6be1d741137 100644 --- a/homeassistant/components/balboa/__init__.py +++ b/homeassistant/components/balboa/__init__.py @@ -61,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(stop_monitoring) # At this point we have a configured spa. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def keep_alive(now: datetime) -> None: """Keep alive task.""" diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index a8f93dd0122..383e5a95f4e 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -48,7 +48,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: domain_entry = domain.setdefault(entry.entry_id, {}) product = domain_entry.setdefault(PRODUCT, product) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index 7321e2392df..0c18950da66 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -86,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not hass.data[DOMAIN][entry.entry_id].available: raise ConfigEntryNotReady - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def blink_refresh(event_time=None): """Call blink to refresh info.""" diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 7023dd7481a..959fbe04240 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -127,7 +127,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator # Set up all platforms except notify - hass.config_entries.async_setup_platforms( + await hass.config_entries.async_forward_entry_setups( entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY] ) diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py index 7dca4db507d..46066d9f55e 100644 --- a/homeassistant/components/bond/__init__.py +++ b/homeassistant/components/bond/__init__.py @@ -92,7 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _async_remove_old_device_identifiers(config_entry_id, device_registry, hub) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/bosch_shc/__init__.py b/homeassistant/components/bosch_shc/__init__.py index 4d076a784d1..a8b2a389f9f 100644 --- a/homeassistant/components/bosch_shc/__init__.py +++ b/homeassistant/components/bosch_shc/__init__.py @@ -68,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=shc_info.version, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def stop_polling(event): """Stop polling service.""" diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index 3962e953520..e1d90681d2a 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -39,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index 46582334e2d..c279e7860b6 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -126,7 +126,7 @@ class BroadlinkDevice: self.reset_jobs.append(config.add_update_listener(self.async_update)) # Forward entry setup to related domains. - self.hass.config_entries.async_setup_platforms( + await self.hass.config_entries.async_forward_entry_setups( config, get_domains(self.api.type) ) diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index 96e2ad069ce..7c3d3003ea6 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -40,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = coordinator hass.data[DOMAIN][SNMP] = snmp_engine - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/brunt/__init__.py b/homeassistant/components/brunt/__init__.py index 988a96ce08e..f189be63920 100644 --- a/homeassistant/components/brunt/__init__.py +++ b/homeassistant/components/brunt/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {DATA_BAPI: bapi, DATA_COOR: coordinator} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/bsblan/__init__.py b/homeassistant/components/bsblan/__init__.py index 7ada5b01e46..c15324825ba 100644 --- a/homeassistant/components/bsblan/__init__.py +++ b/homeassistant/components/bsblan/__init__.py @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {DATA_BSBLAN_CLIENT: bsblan} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/buienradar/__init__.py b/homeassistant/components/buienradar/__init__.py index 64b94cfbaa8..9055220b7ee 100644 --- a/homeassistant/components/buienradar/__init__.py +++ b/homeassistant/components/buienradar/__init__.py @@ -13,7 +13,7 @@ PLATFORMS = [Platform.CAMERA, Platform.SENSOR, Platform.WEATHER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up buienradar from a config entry.""" hass.data.setdefault(DOMAIN, {}) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) return True diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index 9b020a2f09d..e6ea20e0768 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -116,7 +116,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 95f1b660520..85f73532fed 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_finish_startup(_): await coordinator.async_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) if hass.state == CoreState.running: await async_finish_startup(None) diff --git a/homeassistant/components/climacell/__init__.py b/homeassistant/components/climacell/__init__.py index cf80b83fc36..4f2ad7c5889 100644 --- a/homeassistant/components/climacell/__init__.py +++ b/homeassistant/components/climacell/__init__.py @@ -159,7 +159,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/co2signal/__init__.py b/homeassistant/components/co2signal/__init__.py index ba613ea6b18..cc7738741bb 100644 --- a/homeassistant/components/co2signal/__init__.py +++ b/homeassistant/components/co2signal/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 4ef26a11130..36ce65517db 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -76,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = instance - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/control4/__init__.py b/homeassistant/components/control4/__init__.py index 48a639e56f6..4253a4eca02 100644 --- a/homeassistant/components/control4/__init__.py +++ b/homeassistant/components/control4/__init__.py @@ -110,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry_data[CONF_CONFIG_LISTENER] = entry.add_update_listener(update_listener) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index 0f98f8d3afc..ef2b5328f96 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_INFO: info, DATA_COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/coronavirus/__init__.py b/homeassistant/components/coronavirus/__init__.py index 27085c88ef2..a1c4f876f66 100644 --- a/homeassistant/components/coronavirus/__init__.py +++ b/homeassistant/components/coronavirus/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not coordinator.last_update_success: await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/cpuspeed/__init__.py b/homeassistant/components/cpuspeed/__init__.py index a5d6a35459f..da1e0129117 100644 --- a/homeassistant/components/cpuspeed/__init__.py +++ b/homeassistant/components/cpuspeed/__init__.py @@ -16,7 +16,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/crownstone/entry_manager.py b/homeassistant/components/crownstone/entry_manager.py index b1963462adc..dcae7ef4705 100644 --- a/homeassistant/components/crownstone/entry_manager.py +++ b/homeassistant/components/crownstone/entry_manager.py @@ -97,7 +97,9 @@ class CrownstoneEntryManager: # Makes HA aware of the Crownstone environment HA is placed in, a user can have multiple self.usb_sphere_id = self.config_entry.options[CONF_USB_SPHERE] - self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) + await self.hass.config_entries.async_forward_entry_setups( + self.config_entry, PLATFORMS + ) # HA specific listeners self.config_entry.async_on_unload( diff --git a/homeassistant/components/daikin/__init__.py b/homeassistant/components/daikin/__init__.py index 5f05355374c..536e2fa48d1 100644 --- a/homeassistant/components/daikin/__init__.py +++ b/homeassistant/components/daikin/__init__.py @@ -47,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: daikin_api}) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/deluge/__init__.py b/homeassistant/components/deluge/__init__.py index 2253eee43d5..2958b9e1df6 100644 --- a/homeassistant/components/deluge/__init__.py +++ b/homeassistant/components/deluge/__init__.py @@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = DelugeDataUpdateCoordinator(hass, api, entry) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index d6c5a5d3afc..2602d674005 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -259,10 +259,9 @@ async def _insert_statistics(hass): async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set the config entry up.""" # Set up demo platforms with config entry - for platform in COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + await hass.config_entries.async_forward_entry_setups( + config_entry, COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM + ) if "recorder" in hass.config.components: await _insert_statistics(hass) return True diff --git a/homeassistant/components/denonavr/__init__.py b/homeassistant/components/denonavr/__init__.py index 27594703fd2..0f58f5f5218 100644 --- a/homeassistant/components/denonavr/__init__.py +++ b/homeassistant/components/denonavr/__init__.py @@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/derivative/__init__.py b/homeassistant/components/derivative/__init__.py index e3fe9d85f41..c5b1c8e31e9 100644 --- a/homeassistant/components/derivative/__init__.py +++ b/homeassistant/components/derivative/__init__.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Derivative from a config entry.""" - hass.config_entries.async_setup_platforms(entry, (Platform.SENSOR,)) + await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 24075ee1a7d..e9222156b00 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -47,6 +47,14 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the device tracker.""" + + # We need to add the component here break the deadlock + # when setting up integrations from config entries as + # they would otherwise wait for the device tracker to be + # setup and thus the config entries would not be able to + # setup their platforms. + hass.config.components.add(DOMAIN) + await async_setup_legacy_integration(hass, config) return True diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index b79d0a5c8fe..ec0d5a3a666 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -63,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except GatewayOfflineError as err: raise ConfigEntryNotReady from err - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def shutdown(event: Event) -> None: for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]: diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index f427e5acbfc..5cf91325d70 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -103,7 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for coordinator in coordinators.values(): await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect) diff --git a/homeassistant/components/dexcom/__init__.py b/homeassistant/components/dexcom/__init__.py index 7b5ae85bdff..137a884d201 100644 --- a/homeassistant/components/dexcom/__init__.py +++ b/homeassistant/components/dexcom/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: COORDINATOR ].async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index 1068ec4ccc4..3dfb5708b98 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -32,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = dtv - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/dlna_dmr/__init__.py b/homeassistant/components/dlna_dmr/__init__.py index d34d8550355..e9dd60c5896 100644 --- a/homeassistant/components/dlna_dmr/__init__.py +++ b/homeassistant/components/dlna_dmr/__init__.py @@ -17,7 +17,7 @@ async def async_setup_entry( LOGGER.debug("Setting up config entry: %s", entry.unique_id) # Forward setup to the appropriate platform - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/dnsip/__init__.py b/homeassistant/components/dnsip/__init__.py index f679fb4ad30..13783a1b07f 100644 --- a/homeassistant/components/dnsip/__init__.py +++ b/homeassistant/components/dnsip/__init__.py @@ -10,7 +10,7 @@ from .const import PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up DNS IP from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) return True diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 133c7612a89..62554f9662c 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -146,7 +146,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/dsmr/__init__.py b/homeassistant/components/dsmr/__init__.py index 0e238363fc0..044cfd14e64 100644 --- a/homeassistant/components/dsmr/__init__.py +++ b/homeassistant/components/dsmr/__init__.py @@ -13,7 +13,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) return True diff --git a/homeassistant/components/dunehd/__init__.py b/homeassistant/components/dunehd/__init__.py index 839f79bc3f4..c97e27f4017 100644 --- a/homeassistant/components/dunehd/__init__.py +++ b/homeassistant/components/dunehd/__init__.py @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = player - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index 35f76354eab..fe1872e1fe3 100644 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -275,7 +275,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = None raise ConfigEntryNotReady - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/eafm/__init__.py b/homeassistant/components/eafm/__init__.py index e7b6cd88092..a57558ff1cc 100644 --- a/homeassistant/components/eafm/__init__.py +++ b/homeassistant/components/eafm/__init__.py @@ -11,7 +11,7 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up flood monitoring sensors for this config entry.""" hass.data.setdefault(DOMAIN, {}) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index ceab670fa6e..7204dbf8de2 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -61,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = data - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/econet/__init__.py b/homeassistant/components/econet/__init__.py index a706ceb8e7e..728222dcda7 100644 --- a/homeassistant/components/econet/__init__.py +++ b/homeassistant/components/econet/__init__.py @@ -68,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data[DOMAIN][API_CLIENT][config_entry.entry_id] = api hass.data[DOMAIN][EQUIPMENT][config_entry.entry_id] = equipment - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) api.subscribe() diff --git a/homeassistant/components/efergy/__init__.py b/homeassistant/components/efergy/__init__.py index 915eb0daf46..ce8483672a2 100644 --- a/homeassistant/components/efergy/__init__.py +++ b/homeassistant/components/efergy/__init__.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) from ex hass.data.setdefault(DOMAIN, {})[entry.entry_id] = api - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 5cd7bec9244..67ff6c59a54 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -158,7 +158,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: eight, heat_coordinator, user_coordinator ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/elgato/__init__.py b/homeassistant/components/elgato/__init__.py index f15ccc0a03d..c2d70c69735 100644 --- a/homeassistant/components/elgato/__init__.py +++ b/homeassistant/components/elgato/__init__.py @@ -54,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: info=info, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index cf1cac3bdb3..00fcadfe57a 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -303,7 +303,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "keypads": {}, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/elmax/__init__.py b/homeassistant/components/elmax/__init__.py index af123efae9a..0c0a80b4958 100644 --- a/homeassistant/components/elmax/__init__.py +++ b/homeassistant/components/elmax/__init__.py @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator # Perform platform initialization. - hass.config_entries.async_setup_platforms(entry, ELMAX_PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, ELMAX_PLATFORMS) return True diff --git a/homeassistant/components/emonitor/__init__.py b/homeassistant/components/emonitor/__init__.py index 3d03c7b8fe6..ea19808cd37 100644 --- a/homeassistant/components/emonitor/__init__.py +++ b/homeassistant/components/emonitor/__init__.py @@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 0c6c893df64..61c2fd86c77 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -86,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: NAME: name, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/environment_canada/__init__.py b/homeassistant/components/environment_canada/__init__.py index 4ed1f5d0fbb..e12d12b87d0 100644 --- a/homeassistant/components/environment_canada/__init__.py +++ b/homeassistant/components/environment_canada/__init__.py @@ -71,7 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinators - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/epson/__init__.py b/homeassistant/components/epson/__init__.py index 9710cb8d96a..5a8544c7bf1 100644 --- a/homeassistant/components/epson/__init__.py +++ b/homeassistant/components/epson/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = projector - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index ddedaf11ceb..bddb56a38d3 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -321,20 +321,17 @@ async def async_setup_entry( # noqa: C901 on_connect_error=on_connect_error, ) - async def complete_setup() -> None: - """Complete the config entry setup.""" - infos, services = await entry_data.async_load_from_store() - await entry_data.async_update_static_infos(hass, entry, infos) - await _setup_services(hass, entry_data, services) + infos, services = await entry_data.async_load_from_store() + await entry_data.async_update_static_infos(hass, entry, infos) + await _setup_services(hass, entry_data, services) - if entry_data.device_info is not None and entry_data.device_info.name: - cli.expected_name = entry_data.device_info.name - reconnect_logic.name = entry_data.device_info.name + if entry_data.device_info is not None and entry_data.device_info.name: + cli.expected_name = entry_data.device_info.name + reconnect_logic.name = entry_data.device_info.name - await reconnect_logic.start() - entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback) + await reconnect_logic.start() + entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback) - hass.async_create_task(complete_setup()) return True diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 41a0e89245e..80fd855379e 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -101,13 +101,8 @@ class RuntimeEntryData: ) -> None: async with self.platform_load_lock: needed = platforms - self.loaded_platforms - tasks = [] - for platform in needed: - tasks.append( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) - if tasks: - await asyncio.wait(tasks) + if needed: + await hass.config_entries.async_forward_entry_setups(entry, needed) self.loaded_platforms |= needed async def async_update_static_infos( diff --git a/homeassistant/components/evil_genius_labs/__init__.py b/homeassistant/components/evil_genius_labs/__init__.py index da3cbb51717..d7083715394 100644 --- a/homeassistant/components/evil_genius_labs/__init__.py +++ b/homeassistant/components/evil_genius_labs/__init__.py @@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py index 9d6f7864b84..51931f4b104 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -83,7 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_COORDINATOR: coordinator, DATA_UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/faa_delays/__init__.py b/homeassistant/components/faa_delays/__init__.py index 8bfcf60f30a..10ddb13c228 100644 --- a/homeassistant/components/faa_delays/__init__.py +++ b/homeassistant/components/faa_delays/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 3b5eece1a14..9431cd162bc 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -519,7 +519,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: configuration_url=controller.hub_api_url.removesuffix("/api/"), ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) controller.enable_state_handler() diff --git a/homeassistant/components/filesize/__init__.py b/homeassistant/components/filesize/__init__.py index 61ef26f0a66..9e08615d4ab 100644 --- a/homeassistant/components/filesize/__init__.py +++ b/homeassistant/components/filesize/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not hass.config.is_allowed_path(path): raise ConfigEntryNotReady(f"Filepath {path} is not valid or allowed") - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py index ffd82307940..a9a4323fe12 100644 --- a/homeassistant/components/fireservicerota/__init__.py +++ b/homeassistant/components/fireservicerota/__init__.py @@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/firmata/__init__.py b/homeassistant/components/firmata/__init__.py index ffc4d39a89b..a58cd0591d1 100644 --- a/homeassistant/components/firmata/__init__.py +++ b/homeassistant/components/firmata/__init__.py @@ -197,11 +197,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b sw_version=board.firmware_version, ) - for (conf, platform) in CONF_PLATFORM_MAP.items(): - if conf in config_entry.data: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, platform) - ) + await hass.config_entries.async_forward_entry_setups( + config_entry, + [ + platform + for conf, platform in CONF_PLATFORM_MAP.items() + if conf in config_entry.data + ], + ) return True diff --git a/homeassistant/components/fivem/__init__.py b/homeassistant/components/fivem/__init__.py index 1fe5ccf0b8f..7b0ae2e2758 100644 --- a/homeassistant/components/fivem/__init__.py +++ b/homeassistant/components/fivem/__init__.py @@ -56,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/flick_electric/__init__.py b/homeassistant/components/flick_electric/__init__.py index 54eaf5a6917..a963d199c5a 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: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = FlickAPI(auth) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/flipr/__init__.py b/homeassistant/components/flipr/__init__.py index 3281410ec2d..3f9f70e294c 100644 --- a/homeassistant/components/flipr/__init__.py +++ b/homeassistant/components/flipr/__init__.py @@ -33,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py index 2dcca979acc..b30e31de361 100644 --- a/homeassistant/components/flo/__init__.py +++ b/homeassistant/components/flo/__init__.py @@ -44,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: tasks = [device.async_refresh() for device in devices] await asyncio.gather(*tasks) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/flume/__init__.py b/homeassistant/components/flume/__init__.py index 3ca99a335f2..294f50c50e2 100644 --- a/homeassistant/components/flume/__init__.py +++ b/homeassistant/components/flume/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: FLUME_HTTP_SESSION: http_session, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 9a2ef2d4465..5e48e1561b5 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinators - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index e6c1393154a..0284bf90ba0 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -187,7 +187,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = FluxLedUpdateCoordinator(hass, device, entry) hass.data[DOMAIN][entry.entry_id] = coordinator platforms = PLATFORMS_BY_TYPE[device.device_type] - hass.config_entries.async_setup_platforms(entry, platforms) + await hass.config_entries.async_forward_entry_setups(entry, platforms) async def _async_sync_time(*args: Any) -> None: """Set the time every morning at 02:40:30.""" diff --git a/homeassistant/components/forecast_solar/__init__.py b/homeassistant/components/forecast_solar/__init__.py index 18d542c1d3b..ece451a9b0a 100644 --- a/homeassistant/components/forecast_solar/__init__.py +++ b/homeassistant/components/forecast_solar/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) diff --git a/homeassistant/components/forked_daapd/__init__.py b/homeassistant/components/forked_daapd/__init__.py index 903a56ce559..14f40db2057 100644 --- a/homeassistant/components/forked_daapd/__init__.py +++ b/homeassistant/components/forked_daapd/__init__.py @@ -10,7 +10,7 @@ PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up forked-daapd from a config entry by forwarding to platform.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index 2adac19c1de..ef88d0f671a 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -21,7 +21,7 @@ PLATFORMS = [Platform.CAMERA] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up foscam from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry.data diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 7d7bc7695cd..12463934adb 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.unique_id] = router - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Services async def async_reboot(call: ServiceCall) -> None: diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index ec0085c9d32..6bd4d03bde0 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/fritz/__init__.py b/homeassistant/components/fritz/__init__.py index 55253fe3d1e..28036ef37e7 100644 --- a/homeassistant/components/fritz/__init__.py +++ b/homeassistant/components/fritz/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await avm_wrapper.async_config_entry_first_refresh() # Load the other platforms like switch - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await async_setup_services(hass) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 7bb71e52560..4bb2eee45b1 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -68,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_migrate_entries(hass, entry.entry_id, _update_unique_id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def logout_fritzbox(event: Event) -> None: """Close connections to this fritzbox.""" diff --git a/homeassistant/components/fritzbox_callmonitor/__init__.py b/homeassistant/components/fritzbox_callmonitor/__init__.py index 812d7a7db59..a47c3c24755 100644 --- a/homeassistant/components/fritzbox_callmonitor/__init__.py +++ b/homeassistant/components/fritzbox_callmonitor/__init__.py @@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py index f6607aed11f..c4d764f4c71 100644 --- a/homeassistant/components/fronius/__init__.py +++ b/homeassistant/components/fronius/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await solar_net.init_devices() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = solar_net - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/garages_amsterdam/__init__.py b/homeassistant/components/garages_amsterdam/__init__.py index 01dc6b17545..5f8f3e36671 100644 --- a/homeassistant/components/garages_amsterdam/__init__.py +++ b/homeassistant/components/garages_amsterdam/__init__.py @@ -19,7 +19,7 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Garages Amsterdam from a config entry.""" await get_coordinator(hass) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/gdacs/__init__.py b/homeassistant/components/gdacs/__init__.py index b585658befb..56f17adc992 100644 --- a/homeassistant/components/gdacs/__init__.py +++ b/homeassistant/components/gdacs/__init__.py @@ -136,7 +136,9 @@ class GdacsFeedEntityManager: async def async_init(self): """Schedule initial and regular updates based on configured time interval.""" - self._hass.config_entries.async_setup_platforms(self._config_entry, PLATFORMS) + await self._hass.config_entries.async_forward_entry_setups( + self._config_entry, PLATFORMS + ) async def update(event_time): """Update.""" diff --git a/homeassistant/components/generic/__init__.py b/homeassistant/components/generic/__init__.py index cb669d8b906..280b468add8 100644 --- a/homeassistant/components/generic/__init__.py +++ b/homeassistant/components/generic/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up generic IP camera from a config entry.""" await _async_migrate_unique_ids(hass, entry) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/homeassistant/components/geocaching/__init__.py b/homeassistant/components/geocaching/__init__.py index 430cbc9a8d0..aa2926df949 100644 --- a/homeassistant/components/geocaching/__init__.py +++ b/homeassistant/components/geocaching/__init__.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index d3b0fcbe81a..2e6ed8429dd 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -144,7 +144,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, DOMAIN, "Geofency", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/geonetnz_quakes/__init__.py b/homeassistant/components/geonetnz_quakes/__init__.py index d9c27e9dff8..6c091e71f05 100644 --- a/homeassistant/components/geonetnz_quakes/__init__.py +++ b/homeassistant/components/geonetnz_quakes/__init__.py @@ -144,7 +144,9 @@ class GeonetnzQuakesFeedEntityManager: async def async_init(self): """Schedule initial and regular updates based on configured time interval.""" - self._hass.config_entries.async_setup_platforms(self._config_entry, PLATFORMS) + await self._hass.config_entries.async_forward_entry_setups( + self._config_entry, PLATFORMS + ) async def update(event_time): """Update.""" diff --git a/homeassistant/components/geonetnz_volcano/__init__.py b/homeassistant/components/geonetnz_volcano/__init__.py index 8d0733aacd1..a1b6368c8ef 100644 --- a/homeassistant/components/geonetnz_volcano/__init__.py +++ b/homeassistant/components/geonetnz_volcano/__init__.py @@ -130,7 +130,9 @@ class GeonetnzVolcanoFeedEntityManager: async def async_init(self): """Schedule initial and regular updates based on configured time interval.""" - self._hass.config_entries.async_setup_platforms(self._config_entry, PLATFORMS) + await self._hass.config_entries.async_forward_entry_setups( + self._config_entry, PLATFORMS + ) async def update(event_time): """Update.""" diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index 73d773561f5..adef70e5864 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Remove air_quality entities from registry if they exist ent_reg = er.async_get(hass) diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 404aeae11b5..53b8cd67871 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -44,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_cleanup_device_registry(hass=hass, entry=entry) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) return True diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index 571214deb20..2b52cdeef8b 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -91,7 +91,9 @@ class GlancesData: self.config_entry.add_update_listener(self.async_options_updated) ) - self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) + await self.hass.config_entries.async_forward_entry_setups( + self.config_entry, PLATFORMS + ) return True diff --git a/homeassistant/components/goalzero/__init__.py b/homeassistant/components/goalzero/__init__.py index ea292a651c2..f32cad5a488 100644 --- a/homeassistant/components/goalzero/__init__.py +++ b/homeassistant/components/goalzero/__init__.py @@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = GoalZeroDataUpdateCoordinator(hass, api) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/gogogate2/__init__.py b/homeassistant/components/gogogate2/__init__.py index 7dccd5551c7..ece2f6bbbc8 100644 --- a/homeassistant/components/gogogate2/__init__.py +++ b/homeassistant/components/gogogate2/__init__.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data_update_coordinator = get_data_update_coordinator(hass, entry) await data_update_coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/goodwe/__init__.py b/homeassistant/components/goodwe/__init__.py index e48590931ba..5dcc66e90d4 100644 --- a/homeassistant/components/goodwe/__init__.py +++ b/homeassistant/components/goodwe/__init__.py @@ -94,7 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(update_listener)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 261c61d1a88..e05de4f5b79 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -272,7 +272,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if get_feature_access(hass, entry) is FeatureAccess.read_write: await async_setup_add_event_service(hass, calendar_service) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/google_travel_time/__init__.py b/homeassistant/components/google_travel_time/__init__.py index 2012e38e0a2..7d125be5025 100644 --- a/homeassistant/components/google_travel_time/__init__.py +++ b/homeassistant/components/google_travel_time/__init__.py @@ -19,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for entity in async_entries_for_config_entry(ent_reg, entry.entry_id): ent_reg.async_update_entity(entity.entity_id, new_unique_id=entry.entry_id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 3df0bac51e9..5331f6e7029 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -99,7 +99,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, DOMAIN, "GPSLogger", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index fa51a48bb4f..d4a929f1642 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery hass.data[DOMAIN].setdefault(DISPATCHERS, []) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def _async_scan_update(_=None): await gree_discovery.discovery.scan() diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 1f8fba21e78..943b15dd870 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -231,7 +231,9 @@ def groups_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - hass.config_entries.async_setup_platforms(entry, (entry.options["group_type"],)) + await hass.config_entries.async_forward_entry_setups( + entry, (entry.options["group_type"],) + ) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True diff --git a/homeassistant/components/growatt_server/__init__.py b/homeassistant/components/growatt_server/__init__.py index f77323bf536..177d0957883 100644 --- a/homeassistant/components/growatt_server/__init__.py +++ b/homeassistant/components/growatt_server/__init__.py @@ -11,7 +11,7 @@ async def async_setup_entry( ) -> bool: """Load the saved entities.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index ba4bc5e6ad6..b07d8ec5b3f 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -161,7 +161,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Set up all of the Guardian entity platforms: - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @callback def extract_client(func: Callable) -> Callable: diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 3cadd6897d2..25738893689 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -155,7 +155,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) data[entry.entry_id] = api - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) if not hass.services.has_service(DOMAIN, SERVICE_API_CALL): hass.services.async_register( diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index 4e109ae95a7..259ea660317 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: CANCEL_STOP: cancel_stop, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 5b5cc48eed8..d580847646d 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -758,7 +758,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[ADDONS_COORDINATOR] = coordinator await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py index 24da0bc2673..549cc08356c 100644 --- a/homeassistant/components/here_travel_time/__init__.py +++ b/homeassistant/components/here_travel_time/__init__.py @@ -92,7 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b here_travel_time_config, ) hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/hisense_aehw4a1/__init__.py b/homeassistant/components/hisense_aehw4a1/__init__.py index 74eb4371614..cc599aa31fc 100644 --- a/homeassistant/components/hisense_aehw4a1/__init__.py +++ b/homeassistant/components/hisense_aehw4a1/__init__.py @@ -75,7 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry for Hisense AEH-W4A1.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 3693f0183ce..a1a784162e8 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -93,12 +93,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except HiveReauthRequired as err: raise ConfigEntryAuthFailed from err - for ha_type, hive_type in PLATFORM_LOOKUP.items(): - device_list = devices.get(hive_type) - if device_list: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, ha_type) - ) + await hass.config_entries.async_forward_entry_setups( + entry, + [ + ha_type + for ha_type, hive_type in PLATFORM_LOOKUP.items() + if devices.get(hive_type) + ], + ) return True diff --git a/homeassistant/components/hlk_sw16/__init__.py b/homeassistant/components/hlk_sw16/__init__.py index ddd9c5da359..c695f5524c3 100644 --- a/homeassistant/components/hlk_sw16/__init__.py +++ b/homeassistant/components/hlk_sw16/__init__.py @@ -96,29 +96,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.warning("HLK-SW16 %s connected", address) async_dispatcher_send(hass, f"hlk_sw16_device_available_{entry.entry_id}", True) - async def connect(): - """Set up connection and hook it into HA for reconnect/shutdown.""" - _LOGGER.info("Initiating HLK-SW16 connection to %s", address) + _LOGGER.debug("Initiating HLK-SW16 connection to %s", address) - client = await create_hlk_sw16_connection( - host=host, - port=port, - disconnect_callback=disconnected, - reconnect_callback=reconnected, - loop=hass.loop, - timeout=CONNECTION_TIMEOUT, - reconnect_interval=DEFAULT_RECONNECT_INTERVAL, - keep_alive_interval=DEFAULT_KEEP_ALIVE_INTERVAL, - ) + client = await create_hlk_sw16_connection( + host=host, + port=port, + disconnect_callback=disconnected, + reconnect_callback=reconnected, + loop=hass.loop, + timeout=CONNECTION_TIMEOUT, + reconnect_interval=DEFAULT_RECONNECT_INTERVAL, + keep_alive_interval=DEFAULT_KEEP_ALIVE_INTERVAL, + ) - hass.data[DOMAIN][entry.entry_id][DATA_DEVICE_REGISTER] = client + hass.data[DOMAIN][entry.entry_id][DATA_DEVICE_REGISTER] = client - # Load entities - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + # Load entities + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - _LOGGER.info("Connected to HLK-SW16 device: %s", address) - - hass.loop.create_task(connect()) + _LOGGER.debug("Connected to HLK-SW16 device: %s", address) return True diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index 50e51b4ae1e..a2876dd86f5 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -263,7 +263,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await update_all_devices(hass, entry) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/home_plus_control/__init__.py b/homeassistant/components/home_plus_control/__init__.py index fbae1ae970a..d58086e59ec 100644 --- a/homeassistant/components/home_plus_control/__init__.py +++ b/homeassistant/components/home_plus_control/__init__.py @@ -1,5 +1,4 @@ """The Legrand Home+ Control integration.""" -import asyncio from datetime import timedelta import logging @@ -145,18 +144,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(coordinator.async_add_listener(_async_update_entities)) - async def start_platforms(): - """Continue setting up the platforms.""" - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) - # Only refresh the coordinator after all platforms are loaded. - await coordinator.async_refresh() + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - hass.async_create_task(start_platforms()) + # Only refresh the coordinator after all platforms are loaded. + await coordinator.async_refresh() return True diff --git a/homeassistant/components/homewizard/__init__.py b/homeassistant/components/homewizard/__init__.py index 4a087988b61..ec43cdfdd2e 100644 --- a/homeassistant/components/homewizard/__init__.py +++ b/homeassistant/components/homewizard/__init__.py @@ -75,7 +75,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index bad0ed96e01..146b8e3c035 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -77,7 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await data.async_update() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = data - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index c0337095a9c..3ca3daff523 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -434,7 +434,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Forward config entry setup to platforms - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Notify doesn't support config entry setup yet, load with discovery for now await discovery.async_load_platform( diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index e15da5c8489..625a623105f 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -100,7 +100,7 @@ class HueBridge: if self.api_version == 1: if self.api.sensors is not None: self.sensor_manager = SensorManager(self) - self.hass.config_entries.async_setup_platforms( + await self.hass.config_entries.async_forward_entry_setups( self.config_entry, PLATFORMS_v1 ) @@ -108,7 +108,7 @@ class HueBridge: else: await async_setup_devices(self) await async_setup_hue_events(self) - self.hass.config_entries.async_setup_platforms( + await self.hass.config_entries.async_forward_entry_setups( self.config_entry, PLATFORMS_v2 ) diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index f2b4ef8d4ef..fa810d823ca 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -63,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_COORDINATOR: coordinator} # Offload the loading of entities to the platform - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index 4a22bc4ed81..f7c8cd1eb4b 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -96,7 +96,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_info=device_info, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/hvv_departures/__init__.py b/homeassistant/components/hvv_departures/__init__.py index 3c090249bc0..1104359111c 100644 --- a/homeassistant/components/hvv_departures/__init__.py +++ b/homeassistant/components/hvv_departures/__init__.py @@ -24,7 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = hub - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index 292c426ba19..ec478883e8f 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -269,21 +269,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } ) - async def setup_then_listen() -> None: - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) - assert hyperion_client - if hyperion_client.instances is not None: - await async_instances_to_clients_raw(hyperion_client.instances) - hass.data[DOMAIN][entry.entry_id][CONF_ON_UNLOAD].append( - entry.add_update_listener(_async_entry_updated) - ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + assert hyperion_client + if hyperion_client.instances is not None: + await async_instances_to_clients_raw(hyperion_client.instances) + hass.data[DOMAIN][entry.entry_id][CONF_ON_UNLOAD].append( + entry.add_update_listener(_async_entry_updated) + ) - hass.async_create_task(setup_then_listen()) return True diff --git a/homeassistant/components/ialarm/__init__.py b/homeassistant/components/ialarm/__init__.py index 254bf6f685f..374ba29dccf 100644 --- a/homeassistant/components/ialarm/__init__.py +++ b/homeassistant/components/ialarm/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 02e27d8e82f..844313a4aed 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -117,22 +117,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: elif isinstance(dev, AqualinkToggle): switches += [dev] - forward_setup = hass.config_entries.async_forward_entry_setup + platforms = [] if binary_sensors: _LOGGER.debug("Got %s binary sensors: %s", len(binary_sensors), binary_sensors) - hass.async_create_task(forward_setup(entry, Platform.BINARY_SENSOR)) + platforms.append(Platform.BINARY_SENSOR) if climates: _LOGGER.debug("Got %s climates: %s", len(climates), climates) - hass.async_create_task(forward_setup(entry, Platform.CLIMATE)) + platforms.append(Platform.CLIMATE) if lights: _LOGGER.debug("Got %s lights: %s", len(lights), lights) - hass.async_create_task(forward_setup(entry, Platform.LIGHT)) + platforms.append(Platform.LIGHT) if sensors: _LOGGER.debug("Got %s sensors: %s", len(sensors), sensors) - hass.async_create_task(forward_setup(entry, Platform.SENSOR)) + platforms.append(Platform.SENSOR) if switches: _LOGGER.debug("Got %s switches: %s", len(switches), switches) - hass.async_create_task(forward_setup(entry, Platform.SWITCH)) + platforms.append(Platform.SWITCH) + + await hass.config_entries.async_forward_entry_setups(entry, platforms) async def _async_systems_update(now): """Refresh internal state for all systems.""" diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 8175cf43f27..06028ebce6c 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -98,7 +98,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.unique_id] = account - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def play_sound(service: ServiceCall) -> None: """Play sound on the device.""" diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index e2bebf0bb7f..82b910215c4 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -154,10 +154,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) device = devices.add_x10_device(housecode, unitcode, x10_type, steps) - for platform in INSTEON_PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + await hass.config_entries.async_forward_entry_setups(entry, INSTEON_PLATFORMS) for address in devices: device = devices[address] diff --git a/homeassistant/components/integration/__init__.py b/homeassistant/components/integration/__init__.py index 84bc28e3d2f..f482f4e41e8 100644 --- a/homeassistant/components/integration/__init__.py +++ b/homeassistant/components/integration/__init__.py @@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Integration from a config entry.""" - hass.config_entries.async_setup_platforms(entry, (Platform.SENSOR,)) + await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) return True diff --git a/homeassistant/components/intellifire/__init__.py b/homeassistant/components/intellifire/__init__.py index 5fb8ac92a72..f5b6085781b 100644 --- a/homeassistant/components/intellifire/__init__.py +++ b/homeassistant/components/intellifire/__init__.py @@ -91,7 +91,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ios/__init__.py b/homeassistant/components/ios/__init__.py index a70a9908fd8..717effa2bb1 100644 --- a/homeassistant/components/ios/__init__.py +++ b/homeassistant/components/ios/__init__.py @@ -287,7 +287,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: """Set up an iOS entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) hass.http.register_view(iOSIdentifyDeviceView(hass.config.path(CONFIGURATION_FILE))) hass.http.register_view(iOSPushConfigView(hass.data[DOMAIN][CONF_USER][CONF_PUSH])) diff --git a/homeassistant/components/iotawatt/__init__.py b/homeassistant/components/iotawatt/__init__.py index 55fc701cffd..9f51382f98e 100644 --- a/homeassistant/components/iotawatt/__init__.py +++ b/homeassistant/components/iotawatt/__init__.py @@ -14,7 +14,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = IotawattUpdater(hass, entry) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 1bd948ffa5e..4d675e8cc1d 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -13,7 +13,7 @@ PLATFORMS = [Platform.WEATHER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up IPMA station as config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index 23ee5adc0e4..42dc2b8d93b 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 42da8aadb6d..686f5b57f05 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -101,7 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinators - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/iss/__init__.py b/homeassistant/components/iss/__init__.py index 997c3fff2a3..476a944be81 100644 --- a/homeassistant/components/iss/__init__.py +++ b/homeassistant/components/iss/__init__.py @@ -16,7 +16,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(update_listener)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 3cee445b587..301c86827e9 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -204,7 +204,7 @@ async def async_setup_entry( _async_get_or_create_isy_device_in_registry(hass, entry, isy) # Load platforms for the devices in the ISY controller that we support. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @callback def _async_stop_auto_update(event: Event) -> None: diff --git a/homeassistant/components/izone/__init__.py b/homeassistant/components/izone/__init__.py index 0aef8360bdd..3f2565bd8f4 100644 --- a/homeassistant/components/izone/__init__.py +++ b/homeassistant/components/izone/__init__.py @@ -47,7 +47,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" await async_start_discovery_service(hass) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index 60377d7e85b..483782c948a 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -94,7 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/kaleidescape/__init__.py b/homeassistant/components/kaleidescape/__init__.py index a66ae25d436..f074ac640d8 100644 --- a/homeassistant/components/kaleidescape/__init__.py +++ b/homeassistant/components/kaleidescape/__init__.py @@ -45,7 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, disconnect) ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/keenetic_ndms2/__init__.py b/homeassistant/components/keenetic_ndms2/__init__.py index ecd9ece1bd0..68465c26c45 100644 --- a/homeassistant/components/keenetic_ndms2/__init__.py +++ b/homeassistant/components/keenetic_ndms2/__init__.py @@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/kmtronic/__init__.py b/homeassistant/components/kmtronic/__init__.py index a7637879fc1..ef4e8ebb303 100644 --- a/homeassistant/components/kmtronic/__init__.py +++ b/homeassistant/components/kmtronic/__init__.py @@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) update_listener = entry.add_update_listener(async_update_options) hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index f5e58f3c6a1..d3c7d4da724 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -67,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_REMOVE_LISTENER: remove_stop_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/kostal_plenticore/__init__.py b/homeassistant/components/kostal_plenticore/__init__.py index b431960caef..6c7a7cde523 100644 --- a/homeassistant/components/kostal_plenticore/__init__.py +++ b/homeassistant/components/kostal_plenticore/__init__.py @@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = plenticore - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/kraken/__init__.py b/homeassistant/components/kraken/__init__.py index bf01f27673d..db2baf56f97 100644 --- a/homeassistant/components/kraken/__init__.py +++ b/homeassistant/components/kraken/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await kraken_data.async_setup() hass.data[DOMAIN] = kraken_data entry.async_on_unload(entry.add_update_listener(async_options_updated)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/kulersky/__init__.py b/homeassistant/components/kulersky/__init__.py index 39c0d0a5b84..6c8037bdafc 100644 --- a/homeassistant/components/kulersky/__init__.py +++ b/homeassistant/components/kulersky/__init__.py @@ -16,7 +16,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if DATA_ADDRESSES not in hass.data[DOMAIN]: hass.data[DOMAIN][DATA_ADDRESSES] = set() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/launch_library/__init__.py b/homeassistant/components/launch_library/__init__.py index 14eac4fd6a6..34ee7441351 100644 --- a/homeassistant/components/launch_library/__init__.py +++ b/homeassistant/components/launch_library/__init__.py @@ -58,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/laundrify/__init__.py b/homeassistant/components/laundrify/__init__.py index 27fc412abab..4bfb84082d5 100644 --- a/homeassistant/components/laundrify/__init__.py +++ b/homeassistant/components/laundrify/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "coordinator": coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index d1486fe0d32..8df579fddd7 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -120,7 +120,7 @@ async def async_setup_entry( register_lcn_address_devices(hass, config_entry) # forward config_entry to components - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) # register for LCN bus messages device_registry = dr.async_get(hass) diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index df672c07143..b6710064a74 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -50,7 +50,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up LIFX from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py index d854e922ebd..5131ee52e67 100644 --- a/homeassistant/components/litejet/__init__.py +++ b/homeassistant/components/litejet/__init__.py @@ -59,7 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = system - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index 90c432010d7..a2612966a98 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -31,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from ex if hub.account.robots: - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/local_ip/__init__.py b/homeassistant/components/local_ip/__init__.py index cc122187f47..b5ed762ef5d 100644 --- a/homeassistant/components/local_ip/__init__.py +++ b/homeassistant/components/local_ip/__init__.py @@ -10,7 +10,7 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up local_ip from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index fa4d510d648..5a796b976ff 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -121,7 +121,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, DOMAIN, "Locative", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index fff086b6c4e..cabf6342fac 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -190,7 +190,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_LOGI] = logi_circle - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def service_handler(service: ServiceCall) -> None: """Dispatch service calls to target entities.""" diff --git a/homeassistant/components/lookin/__init__.py b/homeassistant/components/lookin/__init__.py index 9b0a5b05f1f..5ab4078eb20 100644 --- a/homeassistant/components/lookin/__init__.py +++ b/homeassistant/components/lookin/__init__.py @@ -164,7 +164,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_coordinators=device_coordinators, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index d29a358fb61..d842fdc1a89 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -61,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 7faf70f0d7a..cea069a1556 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -188,7 +188,7 @@ async def async_setup_entry( bridge, bridge_device, button_devices ) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index 2eaee440ae7..e7fe6789268 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -148,7 +148,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() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index 85b9700a624..f5685155587 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -192,7 +192,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() # Setup components - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Register services hass.services.async_register( diff --git a/homeassistant/components/meater/__init__.py b/homeassistant/components/meater/__init__.py index 904d9e412c0..6db3093567d 100644 --- a/homeassistant/components/meater/__init__.py +++ b/homeassistant/components/meater/__init__.py @@ -79,7 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "coordinator": coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/melcloud/__init__.py b/homeassistant/components/melcloud/__init__.py index 1bb65a943f8..d8a044d23f6 100644 --- a/homeassistant/components/melcloud/__init__.py +++ b/homeassistant/components/melcloud/__init__.py @@ -69,7 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: conf = entry.data mel_devices = await mel_devices_setup(hass, conf[CONF_TOKEN]) hass.data.setdefault(DOMAIN, {}).update({entry.entry_id: mel_devices}) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 2f663df458e..b4889a62411 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -67,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/met_eireann/__init__.py b/homeassistant/components/met_eireann/__init__.py index 399e72d4924..a5b096b5554 100644 --- a/homeassistant/components/met_eireann/__init__.py +++ b/homeassistant/components/met_eireann/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data[DOMAIN][config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index bbc3b16875d..1d36746413e 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -158,7 +158,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/meteoclimatic/__init__.py b/homeassistant/components/meteoclimatic/__init__.py index 58e51d0490a..7510c4bec4c 100644 --- a/homeassistant/components/meteoclimatic/__init__.py +++ b/homeassistant/components/meteoclimatic/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/metoffice/__init__.py b/homeassistant/components/metoffice/__init__.py index 3d10fdef378..e71c417da43 100644 --- a/homeassistant/components/metoffice/__init__.py +++ b/homeassistant/components/metoffice/__init__.py @@ -93,7 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: metoffice_daily_coordinator.async_config_entry_first_refresh(), ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/mill/__init__.py b/homeassistant/components/mill/__init__.py index 6131ebc7fd4..8fd1d1a3e22 100644 --- a/homeassistant/components/mill/__init__.py +++ b/homeassistant/components/mill/__init__.py @@ -76,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][conn_type][key] = data_coordinator await data_coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/min_max/__init__.py b/homeassistant/components/min_max/__init__.py index db80473f90a..a027a029ec2 100644 --- a/homeassistant/components/min_max/__init__.py +++ b/homeassistant/components/min_max/__init__.py @@ -9,7 +9,7 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Min/Max from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index b45cc0fb2e3..1b4a71e8ab8 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: server.start_periodic_update() # Set up platforms. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/mjpeg/__init__.py b/homeassistant/components/mjpeg/__init__.py index 632156b7adc..605c8b6c9d5 100644 --- a/homeassistant/components/mjpeg/__init__.py +++ b/homeassistant/components/mjpeg/__init__.py @@ -24,7 +24,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Reload entry when its updated. entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 1b308705624..7aac961042b 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -96,7 +96,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: registration_name = f"Mobile App: {registration[ATTR_DEVICE_NAME]}" webhook_register(hass, DOMAIN, registration_name, webhook_id, handle_webhook) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass_notify.async_reload(hass, DOMAIN) diff --git a/homeassistant/components/modem_callerid/__init__.py b/homeassistant/components/modem_callerid/__init__.py index 8f62cf4beb5..bbdd1b05383 100644 --- a/homeassistant/components/modem_callerid/__init__.py +++ b/homeassistant/components/modem_callerid/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady(f"Unable to open port: {device}") from ex hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_KEY_API: api} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/modern_forms/__init__.py b/homeassistant/components/modern_forms/__init__.py index ed4212d9444..9b425f61ad0 100644 --- a/homeassistant/components/modern_forms/__init__.py +++ b/homeassistant/components/modern_forms/__init__.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator # Set up all platforms for this device/entry. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/moehlenhoff_alpha2/__init__.py b/homeassistant/components/moehlenhoff_alpha2/__init__.py index 64bdfeb4e6d..b2d3438250f 100644 --- a/homeassistant/components/moehlenhoff_alpha2/__init__.py +++ b/homeassistant/components/moehlenhoff_alpha2/__init__.py @@ -32,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index 91fd353f2e0..1c9a2fa7868 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -48,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: FIRST_RUN: first_run, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/moon/__init__.py b/homeassistant/components/moon/__init__.py index 0b36ba59198..e2eaaf89948 100644 --- a/homeassistant/components/moon/__init__.py +++ b/homeassistant/components/moon/__init__.py @@ -7,7 +7,7 @@ from .const import PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index 95ac1c5fd44..184e721aeed 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -204,7 +204,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=version, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 279cc8bde70..462f5ef03d2 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -1,7 +1,6 @@ """The motionEye integration.""" from __future__ import annotations -import asyncio from collections.abc import Callable import contextlib from http import HTTPStatus @@ -392,20 +391,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: else: device_registry.async_remove_device(device_entry.id) - async def setup_then_listen() -> None: - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) - entry.async_on_unload( - coordinator.async_add_listener(_async_process_motioneye_cameras) - ) - await coordinator.async_refresh() - entry.async_on_unload(entry.add_update_listener(_async_entry_updated)) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + entry.async_on_unload( + coordinator.async_add_listener(_async_process_motioneye_cameras) + ) + await coordinator.async_refresh() + entry.async_on_unload(entry.add_update_listener(_async_entry_updated)) - hass.async_create_task(setup_then_listen()) return True diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index a099e7b580c..143d97b928f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -438,7 +438,7 @@ async def async_setup_entry( # noqa: C901 blocking=False, ) - hass.async_create_task(async_forward_entry_setup_and_setup_discovery(entry)) + await async_forward_entry_setup_and_setup_discovery(entry) return True diff --git a/homeassistant/components/mullvad/__init__.py b/homeassistant/components/mullvad/__init__.py index 5d558ee0ade..7934220cf91 100644 --- a/homeassistant/components/mullvad/__init__.py +++ b/homeassistant/components/mullvad/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/mutesync/__init__.py b/homeassistant/components/mutesync/__init__.py index 4cdd97f446b..aa5e0d70fe9 100644 --- a/homeassistant/components/mutesync/__init__.py +++ b/homeassistant/components/mutesync/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index 8bdf07dad75..d5b4730c2de 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = {MYQ_GATEWAY: myq, MYQ_COORDINATOR: coordinator} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 3313a70808c..cc0d918ec05 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -1,7 +1,6 @@ """Connect to a MySensors gateway via pymysensors API.""" from __future__ import annotations -import asyncio from collections.abc import Callable from functools import partial import logging @@ -86,16 +85,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ), ) - async def finish() -> None: - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS_WITH_ENTRY_SUPPORT - ) - ) - await finish_setup(hass, entry, gateway) - - hass.async_create_task(finish()) + await hass.config_entries.async_forward_entry_setups( + entry, PLATFORMS_WITH_ENTRY_SUPPORT + ) + await finish_setup(hass, entry, gateway) return True diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 021b46e2f38..302c0af76d4 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Remove air_quality entities from registry if they exist ent_reg = entity_registry.async_get(hass) diff --git a/homeassistant/components/nanoleaf/__init__.py b/homeassistant/components/nanoleaf/__init__.py index f6fb2f8112b..1d18e0078ec 100644 --- a/homeassistant/components/nanoleaf/__init__.py +++ b/homeassistant/components/nanoleaf/__init__.py @@ -107,7 +107,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: nanoleaf, coordinator, event_listener ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index cbfd860a0b1..e7b402aed36 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -103,7 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[NEATO_LOGIN] = hub - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index b31354b598c..72759ac0f52 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -227,7 +227,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_DEVICE_MANAGER: device_manager, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index a3a9d22e017..36847c85515 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -153,7 +153,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await data_handler.async_setup() hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] = data_handler - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def unregister_webhook( call_or_event_or_dt: ServiceCall | Event | datetime | None, diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index a996699ab9e..ade5a8df6bd 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -160,7 +160,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: KEY_COORDINATOR_LINK: coordinator_link, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index 355c17a2ed1..b221f440ff8 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -61,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nightscout/__init__.py b/homeassistant/components/nightscout/__init__.py index 8e0c6c2b791..88f12ffa4bc 100644 --- a/homeassistant/components/nightscout/__init__.py +++ b/homeassistant/components/nightscout/__init__.py @@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry_type=dr.DeviceEntryType.SERVICE, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nina/__init__.py b/homeassistant/components/nina/__init__.py index c5375c96785..17e0280ca50 100644 --- a/homeassistant/components/nina/__init__.py +++ b/homeassistant/components/nina/__init__.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nmap_tracker/__init__.py b/homeassistant/components/nmap_tracker/__init__.py index 21469f197f4..5f3333ec750 100644 --- a/homeassistant/components/nmap_tracker/__init__.py +++ b/homeassistant/components/nmap_tracker/__init__.py @@ -91,7 +91,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: scanner = domain_data[entry.entry_id] = NmapDeviceScanner(hass, entry, devices) await scanner.async_setup() entry.async_on_unload(entry.add_update_listener(_async_update_listener)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index bc8a7bffa95..788c698c0ca 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -39,6 +39,13 @@ PLATFORM_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the notify services.""" + + # We need to add the component here break the deadlock + # when setting up integrations from config entries as + # they would otherwise wait for notify to be + # setup and thus the config entries would not be able to + # setup their platforms. + hass.config.components.add(DOMAIN) await async_setup_legacy(hass, config) async def persistent_notification(service: ServiceCall) -> None: diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index e8bd2c725d5..49277740f56 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -99,7 +99,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index 06796e836b4..86574920cd0 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = (thermostat, coordinator) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index a59b0a62f70..dcb359f32d2 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -147,7 +147,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Fetch initial data so we have data when entities subscribe await coordinator.async_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index 28c41ccda3a..27332e50b18 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -103,7 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=data.device_info.firmware, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 3667acf975e..fe0d0b97c69 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -159,7 +159,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator_forecast.async_refresh() await coordinator_forecast_hourly.async_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nzbget/__init__.py b/homeassistant/components/nzbget/__init__.py index a29ea829bbc..c5512076172 100644 --- a/homeassistant/components/nzbget/__init__.py +++ b/homeassistant/components/nzbget/__init__.py @@ -56,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) _async_register_services(hass, coordinator) diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index eded3bec16a..1d1c1958420 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -182,7 +182,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "client": client, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/omnilogic/__init__.py b/homeassistant/components/omnilogic/__init__.py index 8a55eff6bb0..27f145f82b6 100644 --- a/homeassistant/components/omnilogic/__init__.py +++ b/homeassistant/components/omnilogic/__init__.py @@ -61,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: OMNI_API: api, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/oncue/__init__.py b/homeassistant/components/oncue/__init__.py index 24a2ec24c7d..eb9ac37db18 100644 --- a/homeassistant/components/oncue/__init__.py +++ b/homeassistant/components/oncue/__init__.py @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ondilo_ico/__init__.py b/homeassistant/components/ondilo_ico/__init__.py index e827b32f48a..5dccca54772 100644 --- a/homeassistant/components/ondilo_ico/__init__.py +++ b/homeassistant/components/ondilo_ico/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = api.OndiloClient(hass, entry, implementation) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/onewire/__init__.py b/homeassistant/components/onewire/__init__.py index b836d7e3298..e3454a5eb5c 100644 --- a/homeassistant/components/onewire/__init__.py +++ b/homeassistant/components/onewire/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = onewire_hub - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(options_update_listener)) diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index 7922d59ca53..ac20a564c8a 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -44,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if device.capabilities.events: platforms += [Platform.BINARY_SENSOR, Platform.SENSOR] - hass.config_entries.async_setup_platforms(entry, platforms) + await hass.config_entries.async_forward_entry_setups(entry, platforms) entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.async_stop) diff --git a/homeassistant/components/open_meteo/__init__.py b/homeassistant/components/open_meteo/__init__.py index de42d19d8c9..4dc6a45e16c 100644 --- a/homeassistant/components/open_meteo/__init__.py +++ b/homeassistant/components/open_meteo/__init__.py @@ -62,7 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/opengarage/__init__.py b/homeassistant/components/opengarage/__init__.py index eb1b50db5b6..6b97e88df0b 100644 --- a/homeassistant/components/opengarage/__init__.py +++ b/homeassistant/components/opengarage/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 5ec8c6420d5..76f8341734c 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -110,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b # Schedule directly on the loop to avoid blocking HA startup. hass.loop.create_task(gateway.connect_and_subscribe()) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) register_services(hass) return True diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 774cd05fd9f..be003507b81 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -80,7 +80,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = openuv - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @_verify_domain_control async def update_data(_: ServiceCall) -> None: diff --git a/homeassistant/components/openweathermap/__init__.py b/homeassistant/components/openweathermap/__init__.py index 8fd7aaae7ad..d462e34cd84 100644 --- a/homeassistant/components/openweathermap/__init__.py +++ b/homeassistant/components/openweathermap/__init__.py @@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ENTRY_WEATHER_COORDINATOR: weather_coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) update_listener = entry.add_update_listener(async_update_options) hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener diff --git a/homeassistant/components/overkiz/__init__.py b/homeassistant/components/overkiz/__init__.py index 1132e269d04..9acdbfb9ec9 100644 --- a/homeassistant/components/overkiz/__init__.py +++ b/homeassistant/components/overkiz/__init__.py @@ -111,7 +111,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) or OVERKIZ_DEVICE_TO_PLATFORM.get(device.ui_class): platforms[platform].append(device) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) device_registry = dr.async_get(hass) diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 9d2623af0f9..cb2ded6fcef 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -77,7 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() # Setup components - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 5a21862c767..6086ee1efd8 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -103,7 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: webhook.async_register(hass, DOMAIN, "OwnTracks", webhook_id, handle_webhook) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) hass.data[DOMAIN]["unsub"] = async_dispatcher_connect( hass, DOMAIN, async_handle_message diff --git a/homeassistant/components/p1_monitor/__init__.py b/homeassistant/components/p1_monitor/__init__.py index 44a3c855c8c..03055013345 100644 --- a/homeassistant/components/p1_monitor/__init__.py +++ b/homeassistant/components/p1_monitor/__init__.py @@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index e03dca74fb0..79504653a39 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -109,7 +109,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b data={**config, ATTR_DEVICE_INFO: device_info}, ) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/peco/__init__.py b/homeassistant/components/peco/__init__.py index 86f69213a1c..ad74200dace 100644 --- a/homeassistant/components/peco/__init__.py +++ b/homeassistant/components/peco/__init__.py @@ -60,7 +60,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 8ff20d8b104..40e803e4b35 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_entry)) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index df156353b88..ffb3352e282 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -135,7 +135,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_KEY_COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, _async_platforms(entry)) + await hass.config_entries.async_forward_entry_setups(entry, _async_platforms(entry)) return True diff --git a/homeassistant/components/picnic/__init__.py b/homeassistant/components/picnic/__init__.py index e34223a4799..a7d26ceb5c6 100644 --- a/homeassistant/components/picnic/__init__.py +++ b/homeassistant/components/picnic/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: CONF_COORDINATOR: picnic_coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 19c93533867..914d92040d2 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -92,7 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: else: await async_setup_coordinator(hass, entry) - hass.config_entries.async_setup_platforms( + await hass.config_entries.async_forward_entry_setups( entry, [platform for platform in PLATFORMS if entry.options.get(platform, True)] ) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index c8745213f90..148e6a906bd 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -215,7 +215,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket - def start_websocket_session(platform, _): + def start_websocket_session(platform): hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id].add(platform) if hass.data[PLEX_DOMAIN][PLATFORMS_COMPLETED][server_id] == PLATFORMS: hass.loop.create_task(websocket.listen()) @@ -228,11 +228,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + for platform in PLATFORMS: - task = hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) - task.add_done_callback(partial(start_websocket_session, platform)) + start_websocket_session(platform) async_cleanup_plex_devices(hass, entry) diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index afa7451021e..4fde6a54a4a 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -77,7 +77,7 @@ async def async_setup_entry_gw(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=api.smile_version[0], ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS_GATEWAY) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS_GATEWAY) return True diff --git a/homeassistant/components/plum_lightpad/__init__.py b/homeassistant/components/plum_lightpad/__init__.py index 83aec34a185..aa82d5662c6 100644 --- a/homeassistant/components/plum_lightpad/__init__.py +++ b/homeassistant/components/plum_lightpad/__init__.py @@ -76,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = plum - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def cleanup(event): """Clean up resources.""" diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py index 72bfee387eb..a3e9a93da37 100644 --- a/homeassistant/components/poolsense/__init__.py +++ b/homeassistant/components/poolsense/__init__.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index 31e249ec806..2fdf3d61d20 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -174,7 +174,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = runtime_data - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/progettihwsw/__init__.py b/homeassistant/components/progettihwsw/__init__.py index 1ebabd5bb08..bce25c07b17 100644 --- a/homeassistant/components/progettihwsw/__init__.py +++ b/homeassistant/components/progettihwsw/__init__.py @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Check board validation again to load new values to API. await hass.data[DOMAIN][entry.entry_id].check_board() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/prosegur/__init__.py b/homeassistant/components/prosegur/__init__.py index b8023c7fadd..04f353e96b8 100644 --- a/homeassistant/components/prosegur/__init__.py +++ b/homeassistant/components/prosegur/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Could not connect with Prosegur backend: %s", error) raise ConfigEntryNotReady from error - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 7e215060d73..e480396e6a2 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -75,7 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up PS4 from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/pure_energie/__init__.py b/homeassistant/components/pure_energie/__init__.py index 4e86726ccc8..4a64e5abb84 100644 --- a/homeassistant/components/pure_energie/__init__.py +++ b/homeassistant/components/pure_energie/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/pvoutput/__init__.py b/homeassistant/components/pvoutput/__init__.py index 6457a4d25c2..bca5a23e62b 100644 --- a/homeassistant/components/pvoutput/__init__.py +++ b/homeassistant/components/pvoutput/__init__.py @@ -14,7 +14,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index 7ecc89020c0..f10d3b995a8 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -108,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_update_options)) return True diff --git a/homeassistant/components/qnap_qsw/__init__.py b/homeassistant/components/qnap_qsw/__init__.py index 040f8eb52f2..b3bcc1705de 100644 --- a/homeassistant/components/qnap_qsw/__init__.py +++ b/homeassistant/components/qnap_qsw/__init__.py @@ -48,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: QSW_COORD_FW: coord_fw, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index e0ac98b7546..6723678012f 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -95,6 +95,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = person async_register_webhook(hass, entry) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/radiotherm/__init__.py b/homeassistant/components/radiotherm/__init__.py index 865e75257ec..787570eaeb4 100644 --- a/homeassistant/components/radiotherm/__init__.py +++ b/homeassistant/components/radiotherm/__init__.py @@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await _async_call_or_raise_not_ready(time_coro, host) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/homeassistant/components/rainforest_eagle/__init__.py b/homeassistant/components/rainforest_eagle/__init__.py index 862c9850cb9..7cf540de1e6 100644 --- a/homeassistant/components/rainforest_eagle/__init__.py +++ b/homeassistant/components/rainforest_eagle/__init__.py @@ -16,7 +16,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = data.EagleDataCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index c285bf89e57..7647a330a30 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -255,7 +255,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_COORDINATOR: coordinators, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/rdw/__init__.py b/homeassistant/components/rdw/__init__.py index ac2bed06310..601d21ee889 100644 --- a/homeassistant/components/rdw/__init__.py +++ b/homeassistant/components/rdw/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index a977ce18f3f..c4c3f9a1e35 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/renault/__init__.py b/homeassistant/components/renault/__init__.py index 17e4d3dd82b..21ce5118401 100644 --- a/homeassistant/components/renault/__init__.py +++ b/homeassistant/components/renault/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data[DOMAIN][config_entry.entry_id] = renault_hub - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) if not hass.services.has_service(DOMAIN, SERVICE_AC_START): setup_services(hass) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 60ca25ed6d7..efb9b634d62 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -96,7 +96,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ridwell/__init__.py b/homeassistant/components/ridwell/__init__.py index 5f3933a09c5..6b615719bd2 100644 --- a/homeassistant/components/ridwell/__init__.py +++ b/homeassistant/components/ridwell/__init__.py @@ -82,7 +82,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index 0d8f87eef3c..fb037eca05d 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -112,7 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ), } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) if hass.services.has_service(DOMAIN, "update"): return True diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 362fd615700..1932548a907 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -1,5 +1,4 @@ """The Risco integration.""" -import asyncio from datetime import timedelta import logging @@ -56,16 +55,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: EVENTS_COORDINATOR: events_coordinator, } - async def start_platforms(): - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) - await events_coordinator.async_refresh() - - hass.async_create_task(start_platforms()) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await events_coordinator.async_refresh() return True diff --git a/homeassistant/components/rituals_perfume_genie/__init__.py b/homeassistant/components/rituals_perfume_genie/__init__.py index c45a762ca9a..441eb04cbe8 100644 --- a/homeassistant/components/rituals_perfume_genie/__init__.py +++ b/homeassistant/components/rituals_perfume_genie/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id][DEVICES][hublot] = device hass.data[DOMAIN][entry.entry_id][COORDINATORS][hublot] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index f24d08909b8..7f922c2eea5 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index 31b5187a195..641c814d122 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -74,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b CANCEL_STOP: cancel_stop, } - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) if not config_entry.update_listeners: config_entry.add_update_listener(async_update_options) diff --git a/homeassistant/components/roon/server.py b/homeassistant/components/roon/server.py index d5e4cded08d..df9dec3d9af 100644 --- a/homeassistant/components/roon/server.py +++ b/homeassistant/components/roon/server.py @@ -68,7 +68,9 @@ class RoonServer: ) # initialize media_player platform - hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups( + self.config_entry, PLATFORMS + ) # Initialize Roon background polling asyncio.create_task(self.async_do_loop()) diff --git a/homeassistant/components/rpi_power/__init__.py b/homeassistant/components/rpi_power/__init__.py index 8647ab38a78..79504f17d65 100644 --- a/homeassistant/components/rpi_power/__init__.py +++ b/homeassistant/components/rpi_power/__init__.py @@ -8,7 +8,7 @@ PLATFORMS = [Platform.BINARY_SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Raspberry Pi Power Supply Checker from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ruckus_unleashed/__init__.py b/homeassistant/components/ruckus_unleashed/__init__.py index 2c8c8bb108d..258a5968ab1 100644 --- a/homeassistant/components/ruckus_unleashed/__init__.py +++ b/homeassistant/components/ruckus_unleashed/__init__.py @@ -63,7 +63,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENERS: [], } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index c03d27fefe5..aca8d1cd9f4 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -242,7 +242,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_track_time_interval(hass, async_update_sabnzbd, UPDATE_INTERVAL) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index fe7d8a79afd..41e0638c634 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -74,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/season/__init__.py b/homeassistant/components/season/__init__.py index 6d4a2974522..f67abee3bea 100644 --- a/homeassistant/components/season/__init__.py +++ b/homeassistant/components/season/__init__.py @@ -7,7 +7,7 @@ from .const import PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index e938f7132e0..82b25802b02 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -130,7 +130,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: SENSE_DISCOVERED_DEVICES_DATA: sense_discovered_devices, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def async_sense_update(_): """Retrieve latest state.""" diff --git a/homeassistant/components/senseme/__init__.py b/homeassistant/components/senseme/__init__.py index 7a64a23002f..a744dd0d0b8 100644 --- a/homeassistant/components/senseme/__init__.py +++ b/homeassistant/components/senseme/__init__.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await device.async_update(not status) hass.data[DOMAIN][entry.entry_id] = device - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sensibo/__init__.py b/homeassistant/components/sensibo/__init__.py index dc02e9ee686..ee3cba7d001 100644 --- a/homeassistant/components/sensibo/__init__.py +++ b/homeassistant/components/sensibo/__init__.py @@ -19,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/senz/__init__.py b/homeassistant/components/senz/__init__.py index 08aa26fa3c5..6f68189e8bb 100644 --- a/homeassistant/components/senz/__init__.py +++ b/homeassistant/components/senz/__init__.py @@ -113,7 +113,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sharkiq/__init__.py b/homeassistant/components/sharkiq/__init__.py index aad05652b0b..0c4f7bb0bfc 100644 --- a/homeassistant/components/sharkiq/__init__.py +++ b/homeassistant/components/sharkiq/__init__.py @@ -65,7 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/sia/__init__.py b/homeassistant/components/sia/__init__.py index dbbb12f29ce..31ae36f793d 100644 --- a/homeassistant/components/sia/__init__.py +++ b/homeassistant/components/sia/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady( f"SIA Server at port {entry.data[CONF_PORT]} could not start." ) from exc - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sia/hub.py b/homeassistant/components/sia/hub.py index 4f46e88162c..399da14c2ad 100644 --- a/homeassistant/components/sia/hub.py +++ b/homeassistant/components/sia/hub.py @@ -141,4 +141,4 @@ class SIAHub: return hub.update_accounts() await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index c74efab61ac..14a0d6dd8e9 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -327,7 +327,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = simplisafe - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @callback def extract_system(func: Callable) -> Callable: diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py index 00c7a533590..9f032327d62 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -105,7 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ] ) hass.data[DOMAIN][entry.entry_id] = device_coordinators - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index b98be3dcfe1..f70bed1333e 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -107,7 +107,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client=gateway, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/slimproto/__init__.py b/homeassistant/components/slimproto/__init__.py index 96932e1e81f..a96ff7ae925 100644 --- a/homeassistant/components/slimproto/__init__.py +++ b/homeassistant/components/slimproto/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = slimserver # initialize platform(s) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # setup event listeners async def on_hass_stop(event: Event) -> None: diff --git a/homeassistant/components/sma/__init__.py b/homeassistant/components/sma/__init__.py index 556c2a16b6c..13f402b53c3 100644 --- a/homeassistant/components/sma/__init__.py +++ b/homeassistant/components/sma/__init__.py @@ -120,7 +120,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: PYSMA_DEVICE_INFO: device_info, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index 43c7aca9a34..c7edd46c7e2 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -105,7 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = SmappeeBase(hass, smappee) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/smart_meter_texas/__init__.py b/homeassistant/components/smart_meter_texas/__init__.py index cd83a1e51c1..0fcbedca874 100644 --- a/homeassistant/components/smart_meter_texas/__init__.py +++ b/homeassistant/components/smart_meter_texas/__init__.py @@ -80,7 +80,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: asyncio.create_task(coordinator.async_refresh()) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 3f10758076f..c45702773b2 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -183,7 +183,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return False - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/smarttub/__init__.py b/homeassistant/components/smarttub/__init__.py index f6e36a847e5..f98dbac86a1 100644 --- a/homeassistant/components/smarttub/__init__.py +++ b/homeassistant/components/smarttub/__init__.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not await controller.async_setup_entry(entry): return False - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/smhi/__init__.py b/homeassistant/components/smhi/__init__.py index 98c8de87032..5b3f60f4b08 100644 --- a/homeassistant/components/smhi/__init__.py +++ b/homeassistant/components/smhi/__init__.py @@ -20,7 +20,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unique_id = f"{entry.data[CONF_LOCATION][CONF_LATITUDE]}-{entry.data[CONF_LOCATION][CONF_LONGITUDE]}" hass.config_entries.async_update_entry(entry, unique_id=unique_id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index 0b63a3d0366..83d4bbc31f3 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -91,7 +91,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: GATEWAY: gateway, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index 148e684589f..dca129c7a70 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_API_CLIENT: api} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/solarlog/__init__.py b/homeassistant/components/solarlog/__init__.py index 0ebcc3782c9..22c0e3d87d7 100644 --- a/homeassistant/components/solarlog/__init__.py +++ b/homeassistant/components/solarlog/__init__.py @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = SolarlogData(hass, entry) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/solax/__init__.py b/homeassistant/components/solax/__init__.py index 3915f414d0e..6ede5b5df02 100644 --- a/homeassistant/components/solax/__init__.py +++ b/homeassistant/components/solax/__init__.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from err hass.data.setdefault(DOMAIN, {})[entry.entry_id] = api - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index 034a6a0b782..1b3478b2897 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -58,7 +58,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: devices = await hass.async_add_executor_job(hass.data[DOMAIN][API].list_devices) hass.data[DOMAIN][DEVICES] = devices["shades"] - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index a7fb40eafd4..8ac6c4672fd 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UNDO_UPDATE_LISTENER: undo_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index 9934cc8f481..4447425f42a 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -81,7 +81,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_SYSTEM_STATUS: system_status, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/songpal/__init__.py b/homeassistant/components/songpal/__init__.py index 64e71af3e6a..8ab1cb18bdd 100644 --- a/homeassistant/components/songpal/__init__.py +++ b/homeassistant/components/songpal/__init__.py @@ -38,7 +38,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up songpal media player.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index e3f65d754dd..837b6c0f749 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -143,7 +143,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: manager = hass.data[DATA_SONOS_DISCOVERY_MANAGER] = SonosDiscoveryManager( hass, entry, data, hosts ) - hass.async_create_task(manager.setup_platforms_and_discovery()) + await manager.setup_platforms_and_discovery() return True @@ -377,12 +377,7 @@ class SonosDiscoveryManager: async def setup_platforms_and_discovery(self): """Set up platforms and discovery.""" - await asyncio.gather( - *( - self.hass.config_entries.async_forward_entry_setup(self.entry, platform) - for platform in PLATFORMS - ) - ) + await self.hass.config_entries.async_forward_entry_setups(self.entry, PLATFORMS) self.entry.async_on_unload( self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, self._async_stop_event_listener diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 2cbb5e405fb..00221c39a42 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data[DOMAIN] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/spider/__init__.py b/homeassistant/components/spider/__init__.py index b3c1918aad4..d4122a91d62 100644 --- a/homeassistant/components/spider/__init__.py +++ b/homeassistant/components/spider/__init__.py @@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = api - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 560620beed4..7ee4e9649a5 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -112,7 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not set(session.token["scope"].split(" ")).issuperset(SPOTIFY_SCOPES): raise ConfigEntryAuthFailed - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/sql/__init__.py b/homeassistant/components/sql/__init__.py index 2917056b5d4..63f81c784be 100644 --- a/homeassistant/components/sql/__init__.py +++ b/homeassistant/components/sql/__init__.py @@ -16,7 +16,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SQL from a config entry.""" entry.async_on_unload(entry.add_update_listener(async_update_listener)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/squeezebox/__init__.py b/homeassistant/components/squeezebox/__init__.py index 89b3300fc5a..b3e2717d075 100644 --- a/homeassistant/components/squeezebox/__init__.py +++ b/homeassistant/components/squeezebox/__init__.py @@ -15,7 +15,7 @@ PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Logitech Squeezebox from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/srp_energy/__init__.py b/homeassistant/components/srp_energy/__init__.py index ac9cf693c10..bed6780e523 100644 --- a/homeassistant/components/srp_energy/__init__.py +++ b/homeassistant/components/srp_energy/__init__.py @@ -30,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Unable to connect to Srp Energy: %s", str(ex)) raise ConfigEntryNotReady from ex - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/starline/__init__.py b/homeassistant/components/starline/__init__.py index 886a583ae93..2af7b4a75f4 100644 --- a/homeassistant/components/starline/__init__.py +++ b/homeassistant/components/starline/__init__.py @@ -40,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: config_entry_id=entry.entry_id, **account.device_info(device) ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def async_set_scan_interval(call: ServiceCall) -> None: """Set scan interval.""" diff --git a/homeassistant/components/steamist/__init__.py b/homeassistant/components/steamist/__init__.py index edd805e89d3..0a363f77e82 100644 --- a/homeassistant/components/steamist/__init__.py +++ b/homeassistant/components/steamist/__init__.py @@ -56,7 +56,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if discovery := await async_discover_device(hass, host): async_update_entry_from_discovery(hass, entry, discovery) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/stookalert/__init__.py b/homeassistant/components/stookalert/__init__.py index ad800c20e95..63458a2f78a 100644 --- a/homeassistant/components/stookalert/__init__.py +++ b/homeassistant/components/stookalert/__init__.py @@ -16,7 +16,7 @@ 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) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/subaru/__init__.py b/homeassistant/components/subaru/__init__.py index 6e05586706f..83a5a1c1c9d 100644 --- a/homeassistant/components/subaru/__init__.py +++ b/homeassistant/components/subaru/__init__.py @@ -90,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ENTRY_VEHICLES: vehicle_info, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index 75ea1306ccf..2e6125bc502 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -108,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) lock_state_service_schema = vol.Schema( { diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index 1adeace7a96..102319cec93 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -90,7 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_id = async_add_to_device(hass, entry, entity_id) - hass.config_entries.async_setup_platforms( + await hass.config_entries.async_forward_entry_setups( entry, (entry.options[CONF_TARGET_DOMAIN],) ) return True diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 68fbd4bd584..0a3d01f3382 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -78,7 +78,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sensor_type = entry.data[CONF_SENSOR_TYPE] - hass.config_entries.async_setup_platforms(entry, PLATFORMS_BY_TYPE[sensor_type]) + await hass.config_entries.async_forward_entry_setups( + entry, PLATFORMS_BY_TYPE[sensor_type] + ) return True diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index d4779e8e748..e0f328e9312 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -1,7 +1,6 @@ """The Switcher integration.""" from __future__ import annotations -import asyncio from datetime import timedelta import logging @@ -96,24 +95,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ] = SwitcherDataUpdateCoordinator(hass, entry, device) coordinator.async_setup() - async def platforms_setup_task() -> None: - # Must be ready before dispatcher is called - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) + # Must be ready before dispatcher is called + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - discovery_task = hass.data[DOMAIN].pop(DATA_DISCOVERY, None) - if discovery_task is not None: - discovered_devices = await discovery_task - for device in discovered_devices.values(): - on_device_data_callback(device) + discovery_task = hass.data[DOMAIN].pop(DATA_DISCOVERY, None) + if discovery_task is not None: + discovered_devices = await discovery_task + for device in discovered_devices.values(): + on_device_data_callback(device) - await async_start_bridge(hass, on_device_data_callback) - - hass.async_create_task(platforms_setup_task()) + await async_start_bridge(hass, on_device_data_callback) @callback async def stop_bridge(event: Event) -> None: diff --git a/homeassistant/components/syncthing/__init__.py b/homeassistant/components/syncthing/__init__.py index ac9d5d32e92..3950f908e85 100644 --- a/homeassistant/components/syncthing/__init__.py +++ b/homeassistant/components/syncthing/__init__.py @@ -54,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: syncthing.subscribe() hass.data[DOMAIN][entry.entry_id] = syncthing - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def cancel_listen_task(_): await syncthing.unsubscribe() diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index 792267791eb..988c92de593 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -74,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: name=printer.hostname(), ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index e6868491eae..c37540b14b7 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -121,7 +121,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator_switches=coordinator_switches, ) hass.data.setdefault(DOMAIN, {})[entry.unique_id] = synology_data - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index 1bee974a4c4..19bcc224a66 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -123,7 +123,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) if hass.services.has_service(DOMAIN, SERVICE_OPEN_URL): return True diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 0029dbf8c89..7417ac49c96 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -93,7 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: UPDATE_LISTENER: update_listener, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tailscale/__init__.py b/homeassistant/components/tailscale/__init__.py index 7388eec22df..2a5fca43ef1 100644 --- a/homeassistant/components/tailscale/__init__.py +++ b/homeassistant/components/tailscale/__init__.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index b7a15e82ea8..3db67b4c8be 100644 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -136,7 +136,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(_async_update_listener)) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tasmota/__init__.py b/homeassistant/components/tasmota/__init__.py index 7e37c6ea7f2..da9d7e0d53b 100644 --- a/homeassistant/components/tasmota/__init__.py +++ b/homeassistant/components/tasmota/__init__.py @@ -1,7 +1,6 @@ """The Tasmota integration.""" from __future__ import annotations -import asyncio import logging from hatasmota.const import ( @@ -75,21 +74,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, mac, config, entry, tasmota_mqtt, device_registry ) - async def start_platforms() -> None: - await device_automation.async_setup_entry(hass, entry) - await asyncio.gather( - *( - hass.config_entries.async_forward_entry_setup(entry, platform) - for platform in PLATFORMS - ) - ) + await device_automation.async_setup_entry(hass, entry) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + discovery_prefix = entry.data[CONF_DISCOVERY_PREFIX] + await discovery.async_start( + hass, discovery_prefix, entry, tasmota_mqtt, async_discover_device + ) - discovery_prefix = entry.data[CONF_DISCOVERY_PREFIX] - await discovery.async_start( - hass, discovery_prefix, entry, tasmota_mqtt, async_discover_device - ) - - hass.async_create_task(start_platforms()) return True diff --git a/homeassistant/components/tautulli/__init__.py b/homeassistant/components/tautulli/__init__.py index 339ec6eb895..af7939a45f5 100644 --- a/homeassistant/components/tautulli/__init__.py +++ b/homeassistant/components/tautulli/__init__.py @@ -31,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = TautulliDataUpdateCoordinator(hass, host_configuration, api_client) await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tesla_wall_connector/__init__.py b/homeassistant/components/tesla_wall_connector/__init__.py index c4c7c551375..b1d5811c610 100644 --- a/homeassistant/components/tesla_wall_connector/__init__.py +++ b/homeassistant/components/tesla_wall_connector/__init__.py @@ -93,7 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_coordinator=coordinator, ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/threshold/__init__.py b/homeassistant/components/threshold/__init__.py index 75f4c5d1abc..2ca1410a890 100644 --- a/homeassistant/components/threshold/__init__.py +++ b/homeassistant/components/threshold/__init__.py @@ -7,7 +7,9 @@ from homeassistant.core import HomeAssistant async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Min/Max from a config entry.""" - hass.config_entries.async_setup_platforms(entry, (Platform.BINARY_SENSOR,)) + await hass.config_entries.async_forward_entry_setups( + entry, (Platform.BINARY_SENSOR,) + ) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index a0fda8823c4..34f5843412e 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -62,7 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Failed to login. %s", exp) return False - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # set up notify platform, no entry support for notify component yet, # have to use discovery to load platform. diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 0787e00a489..cca0706954f 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -105,7 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_TILE: tiles, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tod/__init__.py b/homeassistant/components/tod/__init__.py index 09038836e2e..e404826534e 100644 --- a/homeassistant/components/tod/__init__.py +++ b/homeassistant/components/tod/__init__.py @@ -8,7 +8,9 @@ from homeassistant.core import HomeAssistant async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Times of the Day from a config entry.""" - hass.config_entries.async_setup_platforms(entry, (Platform.BINARY_SENSOR,)) + await hass.config_entries.async_forward_entry_setups( + entry, (Platform.BINARY_SENSOR,) + ) entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) diff --git a/homeassistant/components/tolo/__init__.py b/homeassistant/components/tolo/__init__.py index 3b78adacbac..a75cb7ce298 100644 --- a/homeassistant/components/tolo/__init__.py +++ b/homeassistant/components/tolo/__init__.py @@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 3d00d6216a0..6af3c0e066b 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -110,7 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) # Spin up the platforms - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # If Home Assistant is already in a running state, register the webhook # immediately, else trigger it after Home Assistant has finished starting. diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 10e621e6668..50c18000baa 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -85,7 +85,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from ex hass.data[DOMAIN][entry.entry_id] = TPLinkDataUpdateCoordinator(hass, device) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py index 7e103730c6d..1679965b070 100644 --- a/homeassistant/components/traccar/__init__.py +++ b/homeassistant/components/traccar/__init__.py @@ -98,7 +98,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, DOMAIN, "Traccar", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index ff2ba9c34bf..aa5048b89c0 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -102,7 +102,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id][CLIENT] = tractive hass.data[DOMAIN][entry.entry_id][TRACKABLES] = trackables - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def cancel_listen_task(_: Event) -> None: await tractive.unsubscribe() diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index e79cd8396e1..aa61be1e782 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -135,7 +135,7 @@ async def async_setup_entry( async_track_time_interval(hass, async_keep_alive, timedelta(seconds=60)) ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/trafikverket_ferry/__init__.py b/homeassistant/components/trafikverket_ferry/__init__.py index 5042e7c5167..c522acb6d12 100644 --- a/homeassistant/components/trafikverket_ferry/__init__.py +++ b/homeassistant/components/trafikverket_ferry/__init__.py @@ -15,7 +15,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/trafikverket_train/__init__.py b/homeassistant/components/trafikverket_train/__init__.py index ee026371b04..c1c756be9ed 100644 --- a/homeassistant/components/trafikverket_train/__init__.py +++ b/homeassistant/components/trafikverket_train/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "train_api": train_api, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/trafikverket_weatherstation/__init__.py b/homeassistant/components/trafikverket_weatherstation/__init__.py index eab54f8f45b..13b88918133 100644 --- a/homeassistant/components/trafikverket_weatherstation/__init__.py +++ b/homeassistant/components/trafikverket_weatherstation/__init__.py @@ -15,7 +15,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index ae5d2dacf61..48d4a0e5163 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -168,7 +168,9 @@ class TransmissionClient: self.add_options() self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL]) - self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) + await self.hass.config_entries.async_forward_entry_setups( + self.config_entry, PLATFORMS + ) def add_torrent(service: ServiceCall) -> None: """Add new torrent to download.""" diff --git a/homeassistant/components/tuya/__init__.py b/homeassistant/components/tuya/__init__.py index 97422179960..9f25cfe7773 100644 --- a/homeassistant/components/tuya/__init__.py +++ b/homeassistant/components/tuya/__init__.py @@ -123,7 +123,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) device_ids.add(device.id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/twentemilieu/__init__.py b/homeassistant/components/twentemilieu/__init__.py index bbe306392ba..c4fe53c67f0 100644 --- a/homeassistant/components/twentemilieu/__init__.py +++ b/homeassistant/components/twentemilieu/__init__.py @@ -51,7 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data.setdefault(DOMAIN, {})[entry.data[CONF_ID]] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/twinkly/__init__.py b/homeassistant/components/twinkly/__init__.py index a8347889895..3b0228e64b0 100644 --- a/homeassistant/components/twinkly/__init__.py +++ b/homeassistant/components/twinkly/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] = client hass.data[DOMAIN][entry.entry_id][DATA_DEVICE_INFO] = device_info - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ukraine_alarm/__init__.py b/homeassistant/components/ukraine_alarm/__init__.py index 587854a3a7e..eb24e5d9a78 100644 --- a/homeassistant/components/ukraine_alarm/__init__.py +++ b/homeassistant/components/ukraine_alarm/__init__.py @@ -33,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index 5b4059ee1d4..30b1d1ad56d 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -91,7 +91,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_service - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async_setup_services(hass) hass.http.register_view(ThumbnailProxyView(hass)) hass.http.register_view(VideoProxyView(hass)) diff --git a/homeassistant/components/upb/__init__.py b/homeassistant/components/upb/__init__.py index e4e1900d8f2..b2980ace4e1 100644 --- a/homeassistant/components/upb/__init__.py +++ b/homeassistant/components/upb/__init__.py @@ -28,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = {"upb": upb} - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) def _element_changed(element, changeset): if (change := changeset.get("last_change")) is None: diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index e42659948d8..cd356925de1 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -158,7 +158,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_UPCLOUD].coordinators[entry.data[CONF_USERNAME]] = coordinator # Forward entry setup - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 27d69f9c509..a45e58f28bc 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -167,7 +167,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator # Setup platforms, creating sensors/binary_sensors. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/uptime/__init__.py b/homeassistant/components/uptime/__init__.py index 1c36fea3b32..b2f912751f7 100644 --- a/homeassistant/components/uptime/__init__.py +++ b/homeassistant/components/uptime/__init__.py @@ -7,7 +7,7 @@ from .const import PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/uptimerobot/__init__.py b/homeassistant/components/uptimerobot/__init__.py index a4c975ff58e..14221463c0e 100644 --- a/homeassistant/components/uptimerobot/__init__.py +++ b/homeassistant/components/uptimerobot/__init__.py @@ -39,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 05ed01f8208..b17592cdf0b 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -200,7 +200,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not entry.options.get(CONF_TARIFFS): # Only a single meter sensor is required hass.data[DATA_UTILITY][entry.entry_id][CONF_TARIFF_ENTITY] = None - hass.config_entries.async_setup_platforms(entry, (Platform.SENSOR,)) + await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,)) else: # Create tariff selection + one meter sensor for each tariff entity_entry = entity_registry.async_get_or_create( @@ -209,7 +209,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DATA_UTILITY][entry.entry_id][ CONF_TARIFF_ENTITY ] = entity_entry.entity_id - hass.config_entries.async_setup_platforms( + await hass.config_entries.async_forward_entry_setups( entry, (Platform.SELECT, Platform.SENSOR) ) diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index a69542596c8..6a2234af9e6 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -219,7 +219,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "name": name, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 9b5a52306d8..eeeee2f9716 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -76,7 +76,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _migrate_device_identifiers(hass, entry.entry_id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) if hass.services.has_service(DOMAIN, SERVICE_SCAN): return True diff --git a/homeassistant/components/venstar/__init__.py b/homeassistant/components/venstar/__init__.py index 759908c87e4..b543f6d947a 100644 --- a/homeassistant/components/venstar/__init__.py +++ b/homeassistant/components/venstar/__init__.py @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, config: ConfigEntry) -> bool: await venstar_data_coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[config.entry_id] = venstar_data_coordinator - hass.config_entries.async_setup_platforms(config, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config, PLATFORMS) return True diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 2dda8fdb7c4..a63e63d74c0 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -142,7 +142,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: set_controller_data(hass, entry, controller_data) # Forward the config data to the necessary platforms. - hass.config_entries.async_setup_platforms( + await hass.config_entries.async_forward_entry_setups( entry, platforms=get_configured_platforms(controller_data) ) diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index d42a6ad004b..9a64061554d 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = coordinator # Set up all platforms for this device/entry. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/version/__init__.py b/homeassistant/components/version/__init__.py index 22dbad36d7b..878ed3d0138 100644 --- a/homeassistant/components/version/__init__.py +++ b/homeassistant/components/version/__init__.py @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index eb18a1229ca..10aa49514e5 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -54,22 +54,25 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b fans = hass.data[DOMAIN][VS_FANS] = [] lights = hass.data[DOMAIN][VS_LIGHTS] = [] sensors = hass.data[DOMAIN][VS_SENSORS] = [] + platforms = [] if device_dict[VS_SWITCHES]: switches.extend(device_dict[VS_SWITCHES]) - hass.async_create_task(forward_setup(config_entry, Platform.SWITCH)) + platforms.append(Platform.SWITCH) if device_dict[VS_FANS]: fans.extend(device_dict[VS_FANS]) - hass.async_create_task(forward_setup(config_entry, Platform.FAN)) + platforms.append(Platform.FAN) if device_dict[VS_LIGHTS]: lights.extend(device_dict[VS_LIGHTS]) - hass.async_create_task(forward_setup(config_entry, Platform.LIGHT)) + platforms.append(Platform.LIGHT) if device_dict[VS_SENSORS]: sensors.extend(device_dict[VS_SENSORS]) - hass.async_create_task(forward_setup(config_entry, Platform.SENSOR)) + platforms.append(Platform.SENSOR) + + await hass.config_entries.async_forward_entry_setups(config_entry, platforms) async def async_new_device_discovery(service: ServiceCall) -> None: """Discover if new devices should be added.""" diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index 221f3a4374a..20d237ee4e4 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.async_add_executor_job(setup_vicare_api, hass, entry) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/vilfo/__init__.py b/homeassistant/components/vilfo/__init__.py index ac3b8b87f3f..82fe4c7bb70 100644 --- a/homeassistant/components/vilfo/__init__.py +++ b/homeassistant/components/vilfo/__init__.py @@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = vilfo_router - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index 1e0d5a322fb..38cf916a5e6 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -69,7 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_refresh() hass.data[DOMAIN][CONF_APPS] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/vlc_telnet/__init__.py b/homeassistant/components/vlc_telnet/__init__.py index 9fe3b97ab31..a8a0c16be8e 100644 --- a/homeassistant/components/vlc_telnet/__init__.py +++ b/homeassistant/components/vlc_telnet/__init__.py @@ -40,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/volumio/__init__.py b/homeassistant/components/volumio/__init__.py index 8189aeef1f4..77119b9a65e 100644 --- a/homeassistant/components/volumio/__init__.py +++ b/homeassistant/components/volumio/__init__.py @@ -29,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DATA_INFO: info, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/vulcan/__init__.py b/homeassistant/components/vulcan/__init__.py index 50f525d9a68..8e66075935b 100644 --- a/homeassistant/components/vulcan/__init__.py +++ b/homeassistant/components/vulcan/__init__.py @@ -34,7 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) from err hass.data[DOMAIN][entry.entry_id] = client - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py index ae003d84a9c..f0392f808ae 100644 --- a/homeassistant/components/wallbox/__init__.py +++ b/homeassistant/components/wallbox/__init__.py @@ -208,7 +208,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = wallbox_coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/watttime/__init__.py b/homeassistant/components/watttime/__init__.py index 2f07658b923..cac73f597f6 100644 --- a/homeassistant/components/watttime/__init__.py +++ b/homeassistant/components/watttime/__init__.py @@ -66,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/waze_travel_time/__init__.py b/homeassistant/components/waze_travel_time/__init__.py index 3b193d6c06b..4e82af5119c 100644 --- a/homeassistant/components/waze_travel_time/__init__.py +++ b/homeassistant/components/waze_travel_time/__init__.py @@ -19,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for entity in async_entries_for_config_entry(ent_reg, entry.entry_id): ent_reg.async_update_entity(entity.entity_id, new_unique_id=entry.entry_id) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index e759077ad3e..d9b2acb1836 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -101,7 +101,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = wrapper - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # set up notify platform, no entry support for notify component yet, # have to use discovery to load platform. diff --git a/homeassistant/components/whirlpool/__init__.py b/homeassistant/components/whirlpool/__init__.py index 659b4602f0d..1991ec3806c 100644 --- a/homeassistant/components/whirlpool/__init__.py +++ b/homeassistant/components/whirlpool/__init__.py @@ -32,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = {AUTH_INSTANCE_KEY: auth} - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/whois/__init__.py b/homeassistant/components/whois/__init__.py index aa64ebecf83..746e83a2677 100644 --- a/homeassistant/components/whois/__init__.py +++ b/homeassistant/components/whois/__init__.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/wiffi/__init__.py b/homeassistant/components/wiffi/__init__.py index f2d11d53862..0472a0bc3b3 100644 --- a/homeassistant/components/wiffi/__init__.py +++ b/homeassistant/components/wiffi/__init__.py @@ -53,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Port %s already in use", entry.data[CONF_PORT]) raise ConfigEntryNotReady from exc - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index 2cdcf20c1ea..fefde1644ad 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: hass.data[DOMAIN][entry.entry_id] = parent # Set up all platforms for this device/entry. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 6e8dee9a774..1b77faa7a61 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -153,7 +153,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Start subscription check in the background, outside this component's setup. async_call_later(hass, 1, async_call_later_callback) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/wiz/__init__.py b/homeassistant/components/wiz/__init__.py index b47db32d90f..396893b04b1 100644 --- a/homeassistant/components/wiz/__init__.py +++ b/homeassistant/components/wiz/__init__.py @@ -133,7 +133,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = WizData( coordinator=coordinator, bulb=bulb, scenes=scenes ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index bcfb98b9916..809afdfb3c7 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator # Set up all platforms for this device/entry. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Reload entry when its updated. entry.async_on_unload(entry.add_update_listener(async_reload_entry)) diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py index 19cafa89a13..9d76c61806b 100644 --- a/homeassistant/components/wolflink/__init__.py +++ b/homeassistant/components/wolflink/__init__.py @@ -106,7 +106,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id][COORDINATOR] = coordinator hass.data[DOMAIN][entry.entry_id][DEVICE_ID] = device_id - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/ws66i/__init__.py b/homeassistant/components/ws66i/__init__.py index dea1b470b9e..0b40ce84816 100644 --- a/homeassistant/components/ws66i/__init__.py +++ b/homeassistant/components/ws66i/__init__.py @@ -104,7 +104,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index 19bbec8bbf5..bee773d2094 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -114,7 +114,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "coordinator": coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index 0c9696a42ef..da3303494e5 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -196,7 +196,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: else: platforms = GATEWAY_PLATFORMS_NO_KEY - hass.config_entries.async_setup_platforms(entry, platforms) + await hass.config_entries.async_forward_entry_setups(entry, platforms) return True diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index e2dc36ff568..17211474f97 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -436,10 +436,7 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> KEY_COORDINATOR: coordinator_dict, } - for platform in GATEWAY_PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) + await hass.config_entries.async_forward_entry_setups(entry, GATEWAY_PLATFORMS) async def async_setup_device_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -452,7 +449,7 @@ async def async_setup_device_entry(hass: HomeAssistant, entry: ConfigEntry) -> b entry.async_on_unload(entry.add_update_listener(update_listener)) - hass.config_entries.async_setup_platforms(entry, platforms) + await hass.config_entries.async_forward_entry_setups(entry, platforms) return True diff --git a/homeassistant/components/yale_smart_alarm/__init__.py b/homeassistant/components/yale_smart_alarm/__init__.py index 8712241a2aa..763742cce70 100644 --- a/homeassistant/components/yale_smart_alarm/__init__.py +++ b/homeassistant/components/yale_smart_alarm/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: COORDINATOR: coordinator, } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) return True diff --git a/homeassistant/components/yamaha_musiccast/__init__.py b/homeassistant/components/yamaha_musiccast/__init__.py index a3362e0558a..b8117df056a 100644 --- a/homeassistant/components/yamaha_musiccast/__init__.py +++ b/homeassistant/components/yamaha_musiccast/__init__.py @@ -80,7 +80,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.musiccast.device.enable_polling() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(async_reload_entry)) return True diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 4fd742e24f5..c07852629a9 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -216,7 +216,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except (asyncio.TimeoutError, OSError, BulbException) as ex: raise ConfigEntryNotReady from ex - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Wait to install the reload listener until everything was successfully initialized entry.async_on_unload(entry.add_update_listener(_async_update_listener)) diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 92068d1e26e..714b0b25070 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -108,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_coordinator.data = {} device_coordinators[device.device_id] = device_coordinator hass.data[DOMAIN][entry.entry_id][ATTR_COORDINATORS] = device_coordinators - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/youless/__init__.py b/homeassistant/components/youless/__init__.py index 3339cdccd36..0026d2ec484 100644 --- a/homeassistant/components/youless/__init__.py +++ b/homeassistant/components/youless/__init__.py @@ -44,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/zerproc/__init__.py b/homeassistant/components/zerproc/__init__.py index e8cc6962a0b..43a768c3844 100644 --- a/homeassistant/components/zerproc/__init__.py +++ b/homeassistant/components/zerproc/__init__.py @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if DATA_ADDRESSES not in hass.data[DOMAIN]: hass.data[DOMAIN][DATA_ADDRESSES] = set() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index e7f65c38ec1..b25b62aa6e0 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1158,9 +1158,24 @@ class ConfigEntries: self, entry: ConfigEntry, platforms: Iterable[Platform | str] ) -> None: """Forward the setup of an entry to platforms.""" + report( + "called async_setup_platforms instead of awaiting async_forward_entry_setups; " + "this will fail in version 2022.12", + # Raise this to warning once all core integrations have been migrated + level=logging.DEBUG, + error_if_core=False, + ) for platform in platforms: self.hass.async_create_task(self.async_forward_entry_setup(entry, platform)) + async def async_forward_entry_setups( + self, entry: ConfigEntry, platforms: Iterable[Platform | str] + ) -> None: + """Forward the setup of an entry to platforms.""" + await asyncio.gather( + *(self.async_forward_entry_setup(entry, platform) for platform in platforms) + ) + async def async_forward_entry_setup( self, entry: ConfigEntry, domain: Platform | str ) -> bool: @@ -1169,9 +1184,6 @@ class ConfigEntries: By default an entry is setup with the component it belongs to. If that component also has related platforms, the component will have to forward the entry to be setup by that component. - - You don't want to await this coroutine if it is called as part of the - setup of a component, because it can cause a deadlock. """ # Setup Component if not set up yet if domain not in self.hass.config.components: diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 2599b4b3c85..8d5f9e52025 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -265,6 +265,10 @@ async def _async_setup_component( await asyncio.sleep(0) await hass.config_entries.flow.async_wait_init_flow_finish(domain) + # Add to components before the async_setup + # call to avoid a deadlock when forwarding platforms + hass.config.components.add(domain) + await asyncio.gather( *( entry.async_setup(hass, integration=integration) @@ -272,8 +276,6 @@ async def _async_setup_component( ) ) - hass.config.components.add(domain) - # Cleanup if domain in hass.data[DATA_SETUP]: hass.data[DATA_SETUP].pop(domain) From 85148b343db053a742f8a6cc296eb488319ab695 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 9 Jul 2022 10:32:15 -0600 Subject: [PATCH 2320/3516] Bump regenmaschine to 2022.07.1 (#74815) --- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 4f06ed0d71b..b318ef7f295 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.07.0"], + "requirements": ["regenmaschine==2022.07.1"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 3f96f8eb711..69733bda770 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2068,7 +2068,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.07.0 +regenmaschine==2022.07.1 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef1b8f5941e..27541bcc27e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1382,7 +1382,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rainmachine -regenmaschine==2022.07.0 +regenmaschine==2022.07.1 # homeassistant.components.renault renault-api==0.1.11 From 124c8e8f7334386fae2ba1f77f4f826036f67fca Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Sat, 9 Jul 2022 18:39:30 +0200 Subject: [PATCH 2321/3516] Add sensors for HomeWizard Watermeter (#74756) Co-authored-by: Franck Nijhof --- .../components/homewizard/manifest.json | 2 +- homeassistant/components/homewizard/sensor.py | 14 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/homewizard/fixtures/data.json | 4 +- .../components/homewizard/test_diagnostics.py | 2 + tests/components/homewizard/test_sensor.py | 82 +++++++++++++++++++ 7 files changed, 104 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index e426234b449..97b3c80b50d 100644 --- a/homeassistant/components/homewizard/manifest.json +++ b/homeassistant/components/homewizard/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/homewizard", "codeowners": ["@DCSBL"], "dependencies": [], - "requirements": ["python-homewizard-energy==1.0.3"], + "requirements": ["python-homewizard-energy==1.1.0"], "zeroconf": ["_hwenergy._tcp.local."], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index b7f759f6a0e..de39ce6f242 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -119,6 +119,20 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL_INCREASING, ), + SensorEntityDescription( + key="active_liter_lpm", + name="Active Water Usage", + native_unit_of_measurement="l/min", + icon="mdi:water", + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="total_liter_m3", + name="Total Water Usage", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + icon="mdi:gauge", + state_class=SensorStateClass.TOTAL_INCREASING, + ), ) diff --git a/requirements_all.txt b/requirements_all.txt index 69733bda770..2ab6ccf679e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1907,7 +1907,7 @@ python-gc100==1.0.3a0 python-gitlab==1.6.0 # homeassistant.components.homewizard -python-homewizard-energy==1.0.3 +python-homewizard-energy==1.1.0 # homeassistant.components.hp_ilo python-hpilo==4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27541bcc27e..727724e1e1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1281,7 +1281,7 @@ python-ecobee-api==0.2.14 python-forecastio==1.4.0 # homeassistant.components.homewizard -python-homewizard-energy==1.0.3 +python-homewizard-energy==1.1.0 # homeassistant.components.izone python-izone==1.2.3 diff --git a/tests/components/homewizard/fixtures/data.json b/tests/components/homewizard/fixtures/data.json index b6eada38038..35cfd2197a1 100644 --- a/tests/components/homewizard/fixtures/data.json +++ b/tests/components/homewizard/fixtures/data.json @@ -12,5 +12,7 @@ "active_power_l2_w": 456, "active_power_l3_w": 123.456, "total_gas_m3": 1122.333, - "gas_timestamp": 210314112233 + "gas_timestamp": 210314112233, + "active_liter_lpm": 12.345, + "total_liter_m3": 1234.567 } diff --git a/tests/components/homewizard/test_diagnostics.py b/tests/components/homewizard/test_diagnostics.py index e477c94d914..899bfb5fb2f 100644 --- a/tests/components/homewizard/test_diagnostics.py +++ b/tests/components/homewizard/test_diagnostics.py @@ -41,6 +41,8 @@ async def test_diagnostics( "active_power_l3_w": 123.456, "total_gas_m3": 1122.333, "gas_timestamp": "2021-03-14T11:22:33", + "active_liter_lpm": 12.345, + "total_liter_m3": 1234.567, }, "state": {"power_on": True, "switch_lock": False, "brightness": 255}, }, diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index 8195aa11708..a81291861a2 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -534,6 +534,88 @@ async def test_sensor_entity_total_gas(hass, mock_config_entry_data, mock_config assert ATTR_ICON not in state.attributes +async def test_sensor_entity_active_liters( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active liters (watermeter).""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"active_liter_lpm": 12.345})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get("sensor.product_name_aabbccddeeff_active_water_usage") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_active_water_usage" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_active_liter_lpm" + assert not entry.disabled + assert state.state == "12.345" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active Water Usage" + ) + + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "l/min" + assert ATTR_DEVICE_CLASS not in state.attributes + assert state.attributes.get(ATTR_ICON) == "mdi:water" + + +async def test_sensor_entity_total_liters( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads total liters (watermeter).""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"total_liter_m3": 1234.567})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get("sensor.product_name_aabbccddeeff_total_water_usage") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_total_water_usage" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_total_liter_m3" + assert not entry.disabled + assert state.state == "1234.567" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Total Water Usage" + ) + + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + assert ATTR_DEVICE_CLASS not in state.attributes + assert state.attributes.get(ATTR_ICON) == "mdi:gauge" + + async def test_sensor_entity_disabled_when_null( hass, mock_config_entry_data, mock_config_entry ): From 72d134be52b5f6abf3f09e738abf4d4da1886d01 Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Sat, 9 Jul 2022 19:05:49 +0200 Subject: [PATCH 2322/3516] Migrate devolo Home Network to new entity naming (#74741) --- .../components/devolo_home_network/entity.py | 2 ++ .../devolo_home_network/test_binary_sensor.py | 14 +++++++-- .../devolo_home_network/test_sensor.py | 29 ++++++++++++++----- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/devolo_home_network/entity.py b/homeassistant/components/devolo_home_network/entity.py index dd26324bc2c..b7b2275109f 100644 --- a/homeassistant/components/devolo_home_network/entity.py +++ b/homeassistant/components/devolo_home_network/entity.py @@ -15,6 +15,8 @@ from .const import DOMAIN class DevoloEntity(CoordinatorEntity): """Representation of a devolo home network device.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, device: Device, device_name: str ) -> None: diff --git a/tests/components/devolo_home_network/test_binary_sensor.py b/tests/components/devolo_home_network/test_binary_sensor.py index 564e6e5ade6..8f9936be5bb 100644 --- a/tests/components/devolo_home_network/test_binary_sensor.py +++ b/tests/components/devolo_home_network/test_binary_sensor.py @@ -9,7 +9,12 @@ from homeassistant.components.devolo_home_network.const import ( CONNECTED_TO_ROUTER, LONG_UPDATE_INTERVAL, ) -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.helpers.entity import EntityCategory @@ -25,10 +30,11 @@ from tests.common import async_fire_time_changed async def test_binary_sensor_setup(hass: HomeAssistant): """Test default setup of the binary sensor component.""" entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(f"{DOMAIN}.{CONNECTED_TO_ROUTER}") is None + assert hass.states.get(f"{DOMAIN}.{device_name}_{CONNECTED_TO_ROUTER}") is None await hass.config_entries.async_unload(entry.entry_id) @@ -36,8 +42,9 @@ async def test_binary_sensor_setup(hass: HomeAssistant): @pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_device") async def test_update_attached_to_router(hass: HomeAssistant): """Test state change of a attached_to_router binary sensor device.""" - state_key = f"{DOMAIN}.{CONNECTED_TO_ROUTER}" entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{DOMAIN}.{device_name}_{CONNECTED_TO_ROUTER}" er = entity_registry.async_get(hass) @@ -47,6 +54,7 @@ async def test_update_attached_to_router(hass: HomeAssistant): state = hass.states.get(state_key) assert state is not None assert state.state == STATE_OFF + assert state.attributes[ATTR_FRIENDLY_NAME] == f"{entry.title} Connected to router" assert er.async_get(state_key).entity_category == EntityCategory.DIAGNOSTIC diff --git a/tests/components/devolo_home_network/test_sensor.py b/tests/components/devolo_home_network/test_sensor.py index 582d6533802..33499f512fa 100644 --- a/tests/components/devolo_home_network/test_sensor.py +++ b/tests/components/devolo_home_network/test_sensor.py @@ -9,7 +9,7 @@ from homeassistant.components.devolo_home_network.const import ( SHORT_UPDATE_INTERVAL, ) from homeassistant.components.sensor import DOMAIN, SensorStateClass -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry from homeassistant.helpers.entity import EntityCategory @@ -24,12 +24,13 @@ from tests.common import async_fire_time_changed async def test_sensor_setup(hass: HomeAssistant): """Test default setup of the sensor component.""" entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(f"{DOMAIN}.connected_wifi_clients") is not None - assert hass.states.get(f"{DOMAIN}.connected_plc_devices") is None - assert hass.states.get(f"{DOMAIN}.neighboring_wifi_networks") is None + assert hass.states.get(f"{DOMAIN}.{device_name}_connected_wifi_clients") is not None + assert hass.states.get(f"{DOMAIN}.{device_name}_connected_plc_devices") is None + assert hass.states.get(f"{DOMAIN}.{device_name}_neighboring_wifi_networks") is None await hass.config_entries.async_unload(entry.entry_id) @@ -37,15 +38,18 @@ async def test_sensor_setup(hass: HomeAssistant): @pytest.mark.usefixtures("mock_device") async def test_update_connected_wifi_clients(hass: HomeAssistant): """Test state change of a connected_wifi_clients sensor device.""" - state_key = f"{DOMAIN}.connected_wifi_clients" - entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{DOMAIN}.{device_name}_connected_wifi_clients" await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() state = hass.states.get(state_key) assert state is not None assert state.state == "1" + assert ( + state.attributes[ATTR_FRIENDLY_NAME] == f"{entry.title} Connected Wifi clients" + ) assert state.attributes["state_class"] == SensorStateClass.MEASUREMENT # Emulate device failure @@ -74,8 +78,9 @@ async def test_update_connected_wifi_clients(hass: HomeAssistant): @pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_device") async def test_update_neighboring_wifi_networks(hass: HomeAssistant): """Test state change of a neighboring_wifi_networks sensor device.""" - state_key = f"{DOMAIN}.neighboring_wifi_networks" entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{DOMAIN}.{device_name}_neighboring_wifi_networks" er = entity_registry.async_get(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -83,6 +88,10 @@ async def test_update_neighboring_wifi_networks(hass: HomeAssistant): state = hass.states.get(state_key) assert state is not None assert state.state == "1" + assert ( + state.attributes[ATTR_FRIENDLY_NAME] + == f"{entry.title} Neighboring Wifi networks" + ) assert er.async_get(state_key).entity_category is EntityCategory.DIAGNOSTIC # Emulate device failure @@ -111,8 +120,9 @@ async def test_update_neighboring_wifi_networks(hass: HomeAssistant): @pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_device") async def test_update_connected_plc_devices(hass: HomeAssistant): """Test state change of a connected_plc_devices sensor device.""" - state_key = f"{DOMAIN}.connected_plc_devices" entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{DOMAIN}.{device_name}_connected_plc_devices" er = entity_registry.async_get(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -120,6 +130,9 @@ async def test_update_connected_plc_devices(hass: HomeAssistant): state = hass.states.get(state_key) assert state is not None assert state.state == "1" + assert ( + state.attributes[ATTR_FRIENDLY_NAME] == f"{entry.title} Connected PLC devices" + ) assert er.async_get(state_key).entity_category is EntityCategory.DIAGNOSTIC # Emulate device failure From 3aff5fd2e634bf178b616b552c763ef470d7a59b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 9 Jul 2022 19:08:53 +0200 Subject: [PATCH 2323/3516] Migrate Open-Meteo to new entity naming style (#74695) --- homeassistant/components/open_meteo/weather.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/open_meteo/weather.py b/homeassistant/components/open_meteo/weather.py index d1f0adf2e87..6af9900ec15 100644 --- a/homeassistant/components/open_meteo/weather.py +++ b/homeassistant/components/open_meteo/weather.py @@ -37,6 +37,7 @@ class OpenMeteoWeatherEntity( ): """Defines an Open-Meteo weather entity.""" + _attr_has_entity_name = True _attr_native_precipitation_unit = LENGTH_MILLIMETERS _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR @@ -50,7 +51,6 @@ class OpenMeteoWeatherEntity( """Initialize Open-Meteo weather entity.""" super().__init__(coordinator=coordinator) self._attr_unique_id = entry.entry_id - self._attr_name = entry.title self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, From 1cf8b761247139f46bc5ddb10e34b1fb24965133 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 9 Jul 2022 19:11:14 +0200 Subject: [PATCH 2324/3516] Migrate CO2 Signal to new entity naming style (#74696) --- .../components/co2signal/__init__.py | 5 --- homeassistant/components/co2signal/sensor.py | 40 +++++++++---------- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/co2signal/__init__.py b/homeassistant/components/co2signal/__init__.py index cc7738741bb..a5bae23332d 100644 --- a/homeassistant/components/co2signal/__init__.py +++ b/homeassistant/components/co2signal/__init__.py @@ -15,7 +15,6 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_COUNTRY_CODE, DOMAIN -from .util import get_extra_name PLATFORMS = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) @@ -73,10 +72,6 @@ class CO2SignalCoordinator(DataUpdateCoordinator[CO2SignalResponse]): """Return entry ID.""" return self._entry.entry_id - def get_extra_name(self) -> str | None: - """Return the extra name describing the location if not home.""" - return get_extra_name(self._entry.data) - async def _async_update_data(self) -> CO2SignalResponse: """Fetch the latest data from the source.""" try: diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 23303474e3b..f7514664698 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -5,14 +5,17 @@ from dataclasses import dataclass from datetime import timedelta from typing import cast -from homeassistant.components.sensor import SensorEntity, SensorStateClass +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import CO2SignalCoordinator @@ -22,12 +25,9 @@ SCAN_INTERVAL = timedelta(minutes=3) @dataclass -class CO2SensorEntityDescription: +class CO2SensorEntityDescription(SensorEntityDescription): """Provide a description of a CO2 sensor.""" - key: str - name: str - unit_of_measurement: str | None = None # For backwards compat, allow description to override unique ID key to use unique_id: str | None = None @@ -42,7 +42,7 @@ SENSORS = ( CO2SensorEntityDescription( key="fossilFuelPercentage", name="Grid fossil fuel percentage", - unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=PERCENTAGE, ), ) @@ -58,21 +58,18 @@ async def async_setup_entry( class CO2Sensor(CoordinatorEntity[CO2SignalCoordinator], SensorEntity): """Implementation of the CO2Signal sensor.""" - _attr_state_class = SensorStateClass.MEASUREMENT + entity_description: CO2SensorEntityDescription + _attr_has_entity_name = True _attr_icon = "mdi:molecule-co2" + _attr_state_class = SensorStateClass.MEASUREMENT def __init__( self, coordinator: CO2SignalCoordinator, description: CO2SensorEntityDescription ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._description = description + self.entity_description = description - name = description.name - if extra_name := coordinator.get_extra_name(): - name = f"{extra_name} - {name}" - - self._attr_name = name self._attr_extra_state_attributes = { "country_code": coordinator.data["countryCode"], ATTR_ATTRIBUTION: ATTRIBUTION, @@ -92,19 +89,22 @@ class CO2Sensor(CoordinatorEntity[CO2SignalCoordinator], SensorEntity): def available(self) -> bool: """Return True if entity is available.""" return ( - super().available and self._description.key in self.coordinator.data["data"] + super().available + and self.entity_description.key in self.coordinator.data["data"] ) @property - def native_value(self) -> StateType: + def native_value(self) -> float | None: """Return sensor state.""" - if (value := self.coordinator.data["data"][self._description.key]) is None: # type: ignore[literal-required] + if (value := self.coordinator.data["data"][self.entity_description.key]) is None: # type: ignore[literal-required] return None return round(value, 2) @property def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" - if self._description.unit_of_measurement: - return self._description.unit_of_measurement - return cast(str, self.coordinator.data["units"].get(self._description.key)) + if self.entity_description.unit_of_measurement: + return self.entity_description.unit_of_measurement + return cast( + str, self.coordinator.data["units"].get(self.entity_description.key) + ) From d208bd461ddc07fbdb2c0199a4f75a04a173cb1c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 19:12:19 +0200 Subject: [PATCH 2325/3516] Migrate Renault to new entity naming style (#74709) --- .../components/renault/binary_sensor.py | 4 +-- homeassistant/components/renault/button.py | 4 +-- .../components/renault/renault_entities.py | 10 +----- homeassistant/components/renault/select.py | 2 +- homeassistant/components/renault/sensor.py | 36 +++++++++---------- 5 files changed, 24 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/renault/binary_sensor.py b/homeassistant/components/renault/binary_sensor.py index a24c9be4e6d..f309a8f188a 100644 --- a/homeassistant/components/renault/binary_sensor.py +++ b/homeassistant/components/renault/binary_sensor.py @@ -85,7 +85,7 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple( key="plugged_in", coordinator="battery", device_class=BinarySensorDeviceClass.PLUG, - name="Plugged In", + name="Plugged in", on_key="plugStatus", on_value=PlugState.PLUGGED.value, ), @@ -130,7 +130,7 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple( coordinator="lock_status", # On means open, Off means closed device_class=BinarySensorDeviceClass.DOOR, - name=f"{door} Door", + name=f"{door.capitalize()} door", on_key=f"doorStatus{door.replace(' ','')}", on_value="open", ) diff --git a/homeassistant/components/renault/button.py b/homeassistant/components/renault/button.py index e62bdf083ae..71d197ac335 100644 --- a/homeassistant/components/renault/button.py +++ b/homeassistant/components/renault/button.py @@ -71,13 +71,13 @@ BUTTON_TYPES: tuple[RenaultButtonEntityDescription, ...] = ( async_press=_start_air_conditioner, key="start_air_conditioner", icon="mdi:air-conditioner", - name="Start Air Conditioner", + name="Start air conditioner", ), RenaultButtonEntityDescription( async_press=_start_charge, key="start_charge", icon="mdi:ev-station", - name="Start Charge", + name="Start charge", requires_electricity=True, ), ) diff --git a/homeassistant/components/renault/renault_entities.py b/homeassistant/components/renault/renault_entities.py index 82f071decae..188d429016a 100644 --- a/homeassistant/components/renault/renault_entities.py +++ b/homeassistant/components/renault/renault_entities.py @@ -4,7 +4,6 @@ from __future__ import annotations from dataclasses import dataclass from typing import 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 @@ -28,6 +27,7 @@ class RenaultDataEntityDescription(EntityDescription, RenaultDataRequiredKeysMix class RenaultEntity(Entity): """Implementation of a Renault entity with a data coordinator.""" + _attr_has_entity_name = True entity_description: EntityDescription def __init__( @@ -41,14 +41,6 @@ class RenaultEntity(Entity): self._attr_device_info = self.vehicle.device_info self._attr_unique_id = f"{self.vehicle.details.vin}_{description.key}".lower() - @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}" - class RenaultDataEntity( CoordinatorEntity[RenaultDataUpdateCoordinator[T]], RenaultEntity diff --git a/homeassistant/components/renault/select.py b/homeassistant/components/renault/select.py index e7ec97b3927..9af47206e3c 100644 --- a/homeassistant/components/renault/select.py +++ b/homeassistant/components/renault/select.py @@ -98,7 +98,7 @@ SENSOR_TYPES: tuple[RenaultSelectEntityDescription, ...] = ( data_key="chargeMode", device_class=DEVICE_CLASS_CHARGE_MODE, icon_lambda=_get_charge_mode_icon, - name="Charge Mode", + name="Charge mode", options=["always", "always_charging", "schedule_mode"], ), ) diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index 3b486af175b..7e692182ff9 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -164,7 +164,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="batteryLevel", device_class=SensorDeviceClass.BATTERY, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - name="Battery Level", + name="Battery level", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), @@ -175,7 +175,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( device_class=DEVICE_CLASS_CHARGE_STATE, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], icon_lambda=_get_charge_state_icon, - name="Charge State", + name="Charge state", value_lambda=_get_charge_state_formatted, ), RenaultSensorEntityDescription( @@ -184,7 +184,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="chargingRemainingTime", entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], icon="mdi:timer", - name="Charging Remaining Time", + name="Charging remaining time", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, ), @@ -195,7 +195,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="chargingInstantaneousPower", device_class=SensorDeviceClass.CURRENT, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - name="Charging Power", + name="Charging power", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, ), @@ -206,7 +206,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="chargingInstantaneousPower", device_class=SensorDeviceClass.POWER, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - name="Charging Power", + name="Charging power", native_unit_of_measurement=POWER_KILO_WATT, state_class=SensorStateClass.MEASUREMENT, value_lambda=_get_charging_power, @@ -218,7 +218,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( device_class=DEVICE_CLASS_PLUG_STATE, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], icon_lambda=_get_plug_state_icon, - name="Plug State", + name="Plug state", value_lambda=_get_plug_state_formatted, ), RenaultSensorEntityDescription( @@ -227,7 +227,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="batteryAutonomy", entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], icon="mdi:ev-station", - name="Battery Autonomy", + name="Battery autonomy", native_unit_of_measurement=LENGTH_KILOMETERS, state_class=SensorStateClass.MEASUREMENT, ), @@ -237,7 +237,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="batteryAvailableEnergy", entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], device_class=SensorDeviceClass.ENERGY, - name="Battery Available Energy", + name="Battery available energy", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, state_class=SensorStateClass.MEASUREMENT, ), @@ -247,7 +247,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="batteryTemperature", device_class=SensorDeviceClass.TEMPERATURE, entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], - name="Battery Temperature", + name="Battery temperature", native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), @@ -258,7 +258,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="timestamp", entity_class=RenaultSensor[KamereonVehicleBatteryStatusData], entity_registry_enabled_default=False, - name="Battery Last Activity", + name="Battery last activity", value_lambda=_get_utc_value, ), RenaultSensorEntityDescription( @@ -278,7 +278,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="fuelAutonomy", entity_class=RenaultSensor[KamereonVehicleCockpitData], icon="mdi:gas-station", - name="Fuel Autonomy", + name="Fuel autonomy", native_unit_of_measurement=LENGTH_KILOMETERS, state_class=SensorStateClass.MEASUREMENT, requires_fuel=True, @@ -290,7 +290,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="fuelQuantity", entity_class=RenaultSensor[KamereonVehicleCockpitData], icon="mdi:fuel", - name="Fuel Quantity", + name="Fuel quantity", native_unit_of_measurement=VOLUME_LITERS, state_class=SensorStateClass.MEASUREMENT, requires_fuel=True, @@ -302,7 +302,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( device_class=SensorDeviceClass.TEMPERATURE, data_key="externalTemperature", entity_class=RenaultSensor[KamereonVehicleHvacStatusData], - name="Outside Temperature", + name="Outside temperature", native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, ), @@ -311,7 +311,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( coordinator="hvac_status", data_key="socThreshold", entity_class=RenaultSensor[KamereonVehicleHvacStatusData], - name="HVAC SOC Threshold", + name="HVAC SoC threshold", native_unit_of_measurement=PERCENTAGE, ), RenaultSensorEntityDescription( @@ -321,7 +321,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="lastUpdateTime", entity_class=RenaultSensor[KamereonVehicleHvacStatusData], entity_registry_enabled_default=False, - name="HVAC Last Activity", + name="HVAC last activity", value_lambda=_get_utc_value, ), RenaultSensorEntityDescription( @@ -331,7 +331,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="lastUpdateTime", entity_class=RenaultSensor[KamereonVehicleLocationData], entity_registry_enabled_default=False, - name="Location Last Activity", + name="Location last activity", value_lambda=_get_utc_value, ), RenaultSensorEntityDescription( @@ -339,7 +339,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( coordinator="res_state", data_key="details", entity_class=RenaultSensor[KamereonVehicleResStateData], - name="Remote Engine Start", + name="Remote engine start", ), RenaultSensorEntityDescription( key="res_state_code", @@ -347,6 +347,6 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = ( data_key="code", entity_class=RenaultSensor[KamereonVehicleResStateData], entity_registry_enabled_default=False, - name="Remote Engine Start Code", + name="Remote engine start code", ), ) From 04bb2d1e5d4c3d0b97e5d260fc9ddcdb9a59b8b7 Mon Sep 17 00:00:00 2001 From: Pieter Mulder Date: Sat, 9 Jul 2022 19:13:46 +0200 Subject: [PATCH 2326/3516] Update pyCEC to version 0.5.2 (#74742) --- homeassistant/components/hdmi_cec/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hdmi_cec/manifest.json b/homeassistant/components/hdmi_cec/manifest.json index ff2411db35a..8ea56a51fa9 100644 --- a/homeassistant/components/hdmi_cec/manifest.json +++ b/homeassistant/components/hdmi_cec/manifest.json @@ -2,7 +2,7 @@ "domain": "hdmi_cec", "name": "HDMI-CEC", "documentation": "https://www.home-assistant.io/integrations/hdmi_cec", - "requirements": ["pyCEC==0.5.1"], + "requirements": ["pyCEC==0.5.2"], "codeowners": [], "iot_class": "local_push", "loggers": ["pycec"] diff --git a/requirements_all.txt b/requirements_all.txt index 2ab6ccf679e..fad4f243350 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1326,7 +1326,7 @@ py-zabbix==1.1.7 py17track==2021.12.2 # homeassistant.components.hdmi_cec -pyCEC==0.5.1 +pyCEC==0.5.2 # homeassistant.components.control4 pyControl4==0.0.6 From 24ca656372f907f43393374f8fe9ccc57c5340b4 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 9 Jul 2022 19:17:50 +0200 Subject: [PATCH 2327/3516] Migrate deCONZ Group and Scenes to new entity naming style (#74761) --- homeassistant/components/deconz/deconz_device.py | 16 +++++++++++++--- homeassistant/components/deconz/light.py | 4 ++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 94051cbf0ac..e9be4658fb5 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -120,6 +120,8 @@ class DeconzDevice(DeconzBase, Entity): class DeconzSceneMixin(DeconzDevice): """Representation of a deCONZ scene.""" + _attr_has_entity_name = True + _device: PydeconzScene def __init__( @@ -130,7 +132,9 @@ class DeconzSceneMixin(DeconzDevice): """Set up a scene.""" super().__init__(device, gateway) - self._attr_name = device.full_name + self.group = self.gateway.api.groups[device.group_id] + + self._attr_name = device.name self._group_identifier = self.get_parent_identifier() def get_device_identifier(self) -> str: @@ -139,7 +143,7 @@ class DeconzSceneMixin(DeconzDevice): def get_parent_identifier(self) -> str: """Describe a unique identifier for group this scene belongs to.""" - return f"{self.gateway.bridgeid}-{self._device.group_deconz_id}" + return f"{self.gateway.bridgeid}-{self.group.deconz_id}" @property def unique_id(self) -> str: @@ -149,4 +153,10 @@ class DeconzSceneMixin(DeconzDevice): @property def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - return DeviceInfo(identifiers={(DECONZ_DOMAIN, self._group_identifier)}) + return DeviceInfo( + identifiers={(DECONZ_DOMAIN, self._group_identifier)}, + manufacturer="Dresden Elektronik", + model="deCONZ group", + name=self.group.name, + via_device=(DECONZ_DOMAIN, self.gateway.api.config.bridge_id), + ) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 7f3a47d719d..e73314aa477 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -289,6 +289,8 @@ class DeconzLight(DeconzBaseLight[Light]): class DeconzGroup(DeconzBaseLight[Group]): """Representation of a deCONZ group.""" + _attr_has_entity_name = True + _device: Group def __init__(self, device: Group, gateway: DeconzGateway) -> None: @@ -296,6 +298,8 @@ class DeconzGroup(DeconzBaseLight[Group]): self._unique_id = f"{gateway.bridgeid}-{device.deconz_id}" super().__init__(device, gateway) + self._attr_name = None + @property def unique_id(self) -> str: """Return a unique identifier for this device.""" From 1c4fee65c0f1e6e2c1ed1d03ec9e3303b870ad1e Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 9 Jul 2022 10:21:53 -0700 Subject: [PATCH 2328/3516] Migrate nest to new entity naming style (#74724) --- homeassistant/components/nest/camera_sdm.py | 7 ++----- homeassistant/components/nest/climate_sdm.py | 6 +----- homeassistant/components/nest/sensor_sdm.py | 13 +++---------- tests/components/nest/test_camera_sdm.py | 3 ++- tests/components/nest/test_events.py | 1 - tests/components/nest/test_sensor_sdm.py | 7 ++++--- 6 files changed, 12 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index 4e38338aee8..c26b216b21f 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -61,6 +61,8 @@ async def async_setup_sdm_entry( class NestCamera(Camera): """Devices that support cameras.""" + _attr_has_entity_name = True + def __init__(self, device: Device) -> None: """Initialize the camera.""" super().__init__() @@ -83,11 +85,6 @@ class NestCamera(Camera): # The API "name" field is a unique device identifier. return f"{self._device.name}-camera" - @property - def name(self) -> str | None: - """Return the name of the camera.""" - return self._device_info.device_name - @property def device_info(self) -> DeviceInfo: """Return device specific attributes.""" diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 452c30073da..c13370776ad 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -97,6 +97,7 @@ class ThermostatEntity(ClimateEntity): _attr_min_temp = MIN_TEMP _attr_max_temp = MAX_TEMP + _attr_has_entity_name = True def __init__(self, device: Device) -> None: """Initialize ThermostatEntity.""" @@ -115,11 +116,6 @@ class ThermostatEntity(ClimateEntity): # The API "name" field is a unique device identifier. return self._device.name - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._device_info.device_name - @property def device_info(self) -> DeviceInfo: """Return device specific attributes.""" diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index c6d1c8b2b30..11edc9f3506 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -53,6 +53,7 @@ class SensorBase(SensorEntity): _attr_should_poll = False _attr_state_class = SensorStateClass.MEASUREMENT + _attr_has_entity_name = True def __init__(self, device: Device) -> None: """Initialize the sensor.""" @@ -73,11 +74,7 @@ class TemperatureSensor(SensorBase): _attr_device_class = SensorDeviceClass.TEMPERATURE _attr_native_unit_of_measurement = TEMP_CELSIUS - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"{self._device_info.device_name} Temperature" + _attr_name = "Temperature" @property def native_value(self) -> float: @@ -94,11 +91,7 @@ class HumiditySensor(SensorBase): _attr_device_class = SensorDeviceClass.HUMIDITY _attr_native_unit_of_measurement = PERCENTAGE - - @property - def name(self) -> str: - """Return the name of the sensor.""" - return f"{self._device_info.device_name} Humidity" + _attr_name = "Humidity" @property def native_value(self) -> int: diff --git a/tests/components/nest/test_camera_sdm.py b/tests/components/nest/test_camera_sdm.py index 5c4194f46f6..86f05c613ed 100644 --- a/tests/components/nest/test_camera_sdm.py +++ b/tests/components/nest/test_camera_sdm.py @@ -17,6 +17,7 @@ from homeassistant.components import camera from homeassistant.components.camera import STATE_IDLE, STATE_STREAMING, StreamType from homeassistant.components.nest.const import DOMAIN from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component @@ -210,11 +211,11 @@ async def test_camera_device( camera = hass.states.get("camera.my_camera") assert camera is not None assert camera.state == STATE_STREAMING + assert camera.attributes.get(ATTR_FRIENDLY_NAME) == "My Camera" registry = er.async_get(hass) entry = registry.async_get("camera.my_camera") assert entry.unique_id == f"{DEVICE_ID}-camera" - assert entry.original_name == "My Camera" assert entry.domain == "camera" device_registry = dr.async_get(hass) diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index 83845586764..1fffcc9042b 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -160,7 +160,6 @@ async def test_event( entry = registry.async_get("camera.front") assert entry is not None assert entry.unique_id == "some-device-id-camera" - assert entry.original_name == "Front" assert entry.domain == "camera" device_registry = dr.async_get(hass) diff --git a/tests/components/nest/test_sensor_sdm.py b/tests/components/nest/test_sensor_sdm.py index bb49f13b3eb..0f98d0c05b4 100644 --- a/tests/components/nest/test_sensor_sdm.py +++ b/tests/components/nest/test_sensor_sdm.py @@ -13,6 +13,7 @@ import pytest from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, @@ -59,6 +60,7 @@ async def test_thermostat_device( 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 + assert temperature.attributes.get(ATTR_FRIENDLY_NAME) == "My Sensor Temperature" humidity = hass.states.get("sensor.my_sensor_humidity") assert humidity is not None @@ -66,16 +68,15 @@ async def test_thermostat_device( 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 + assert humidity.attributes.get(ATTR_FRIENDLY_NAME) == "My Sensor Humidity" registry = er.async_get(hass) entry = registry.async_get("sensor.my_sensor_temperature") assert entry.unique_id == f"{DEVICE_ID}-temperature" - assert entry.original_name == "My Sensor Temperature" assert entry.domain == "sensor" entry = registry.async_get("sensor.my_sensor_humidity") assert entry.unique_id == f"{DEVICE_ID}-humidity" - assert entry.original_name == "My Sensor Humidity" assert entry.domain == "sensor" device_registry = dr.async_get(hass) @@ -195,11 +196,11 @@ async def test_device_with_unknown_type( temperature = hass.states.get("sensor.my_sensor_temperature") assert temperature is not None assert temperature.state == "25.1" + assert temperature.attributes.get(ATTR_FRIENDLY_NAME) == "My Sensor Temperature" registry = er.async_get(hass) entry = registry.async_get("sensor.my_sensor_temperature") assert entry.unique_id == f"{DEVICE_ID}-temperature" - assert entry.original_name == "My Sensor Temperature" assert entry.domain == "sensor" device_registry = dr.async_get(hass) From cdcc73a414a9f2e66589c838ebc27baaedb5e728 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 9 Jul 2022 19:22:23 +0200 Subject: [PATCH 2329/3516] Migrate Stookalert to new entity naming style (#74693) --- homeassistant/components/stookalert/binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py index 974635e2efd..70a25c2bfdf 100644 --- a/homeassistant/components/stookalert/binary_sensor.py +++ b/homeassistant/components/stookalert/binary_sensor.py @@ -35,15 +35,15 @@ class StookalertBinarySensor(BinarySensorEntity): _attr_attribution = "Data provided by rivm.nl" _attr_device_class = BinarySensorDeviceClass.SAFETY + _attr_has_entity_name = True def __init__(self, client: stookalert.stookalert, entry: ConfigEntry) -> None: """Initialize a Stookalert device.""" self._client = client - self._attr_name = f"Stookalert {entry.data[CONF_PROVINCE]}" self._attr_unique_id = entry.unique_id self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"{entry.entry_id}")}, - name=entry.data[CONF_PROVINCE], + name=f"Stookalert {entry.data[CONF_PROVINCE]}", manufacturer="RIVM", model="Stookalert", entry_type=DeviceEntryType.SERVICE, From 4080d2b0dabeae322f42b34b8eb553f5d2a300b9 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 9 Jul 2022 19:34:51 +0200 Subject: [PATCH 2330/3516] Convert philips_js to entity naming (#74721) Co-authored-by: Franck Nijhof --- homeassistant/components/philips_js/light.py | 4 +++- homeassistant/components/philips_js/media_player.py | 2 +- homeassistant/components/philips_js/remote.py | 4 +++- homeassistant/components/philips_js/switch.py | 4 +++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index ab4f04a8ddc..4fa3b066214 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -137,6 +137,8 @@ class PhilipsTVLightEntity( ): """Representation of a Philips TV exposing the JointSpace API.""" + _attr_has_entity_name = True + def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, @@ -151,7 +153,7 @@ class PhilipsTVLightEntity( self._attr_supported_color_modes = {ColorMode.HS, ColorMode.ONOFF} self._attr_supported_features = LightEntityFeature.EFFECT - self._attr_name = f"{coordinator.system['name']} Ambilight" + self._attr_name = "Ambilight" self._attr_unique_id = coordinator.unique_id self._attr_icon = "mdi:television-ambient-light" self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 3d63865fe31..8d864436ac5 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -73,6 +73,7 @@ class PhilipsTVMediaPlayer( """Representation of a Philips TV exposing the JointSpace API.""" _attr_device_class = MediaPlayerDeviceClass.TV + _attr_has_entity_name = True def __init__( self, @@ -83,7 +84,6 @@ class PhilipsTVMediaPlayer( self._sources: dict[str, str] = {} self._supports = SUPPORT_PHILIPS_JS self._system = coordinator.system - self._attr_name = coordinator.system["name"] self._attr_unique_id = coordinator.unique_id self._attr_device_info = DeviceInfo( identifiers={ diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py index 38851964427..7e8c5448cca 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -30,6 +30,8 @@ async def async_setup_entry( class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteEntity): """Device that sends commands.""" + _attr_has_entity_name = True + def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, @@ -37,7 +39,7 @@ class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteE """Initialize the Philips TV.""" super().__init__(coordinator) self._tv = coordinator.api - self._attr_name = f"{coordinator.system['name']} Remote" + self._attr_name = "Remote" self._attr_unique_id = coordinator.unique_id self._attr_device_info = DeviceInfo( identifiers={ diff --git a/homeassistant/components/philips_js/switch.py b/homeassistant/components/philips_js/switch.py index bf1e83b0ec1..b66fd3296d9 100644 --- a/homeassistant/components/philips_js/switch.py +++ b/homeassistant/components/philips_js/switch.py @@ -38,6 +38,8 @@ class PhilipsTVScreenSwitch( ): """A Philips TV screen state switch.""" + _attr_has_entity_name = True + def __init__( self, coordinator: PhilipsTVDataUpdateCoordinator, @@ -46,7 +48,7 @@ class PhilipsTVScreenSwitch( super().__init__(coordinator) - self._attr_name = f"{coordinator.system['name']} Screen State" + self._attr_name = "Screen state" self._attr_icon = "mdi:television-shimmer" self._attr_unique_id = f"{coordinator.unique_id}_screenstate" self._attr_device_info = DeviceInfo( From 8ce897f46281d6daee628c00007228d349f0eabe Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 9 Jul 2022 19:46:53 +0200 Subject: [PATCH 2331/3516] Convert fjaraskupan to entity naming (#74723) --- homeassistant/components/fjaraskupan/binary_sensor.py | 6 +++--- homeassistant/components/fjaraskupan/fan.py | 2 +- homeassistant/components/fjaraskupan/light.py | 3 ++- homeassistant/components/fjaraskupan/number.py | 4 +++- homeassistant/components/fjaraskupan/sensor.py | 4 +++- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fjaraskupan/binary_sensor.py b/homeassistant/components/fjaraskupan/binary_sensor.py index ef4f64c5ecd..0ea5c1669db 100644 --- a/homeassistant/components/fjaraskupan/binary_sensor.py +++ b/homeassistant/components/fjaraskupan/binary_sensor.py @@ -30,13 +30,13 @@ class EntityDescription(BinarySensorEntityDescription): SENSORS = ( EntityDescription( key="grease-filter", - name="Grease Filter", + name="Grease filter", device_class=BinarySensorDeviceClass.PROBLEM, is_on=lambda state: state.grease_filter_full, ), EntityDescription( key="carbon-filter", - name="Carbon Filter", + name="Carbon filter", device_class=BinarySensorDeviceClass.PROBLEM, is_on=lambda state: state.carbon_filter_full, ), @@ -68,6 +68,7 @@ class BinarySensor(CoordinatorEntity[Coordinator], BinarySensorEntity): """Grease filter sensor.""" entity_description: EntityDescription + _attr_has_entity_name = True def __init__( self, @@ -82,7 +83,6 @@ class BinarySensor(CoordinatorEntity[Coordinator], BinarySensorEntity): self._attr_unique_id = f"{device.address}-{entity_description.key}" self._attr_device_info = device_info - self._attr_name = f"{device_info['name']} {entity_description.name}" @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index e372d540f54..3642438d5d6 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -67,6 +67,7 @@ class Fan(CoordinatorEntity[Coordinator], FanEntity): """Fan entity.""" _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE + _attr_has_entity_name = True def __init__( self, @@ -78,7 +79,6 @@ class Fan(CoordinatorEntity[Coordinator], FanEntity): super().__init__(coordinator) self._device = device self._default_on_speed = 25 - self._attr_name = device_info["name"] self._attr_unique_id = device.address self._attr_device_info = device_info self._percentage = 0 diff --git a/homeassistant/components/fjaraskupan/light.py b/homeassistant/components/fjaraskupan/light.py index 9d52c5cac82..f492f628a3a 100644 --- a/homeassistant/components/fjaraskupan/light.py +++ b/homeassistant/components/fjaraskupan/light.py @@ -29,6 +29,8 @@ async def async_setup_entry( class Light(CoordinatorEntity[Coordinator], LightEntity): """Light device.""" + _attr_has_entity_name = True + def __init__( self, coordinator: Coordinator, @@ -42,7 +44,6 @@ class Light(CoordinatorEntity[Coordinator], LightEntity): self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} self._attr_unique_id = device.address self._attr_device_info = device_info - self._attr_name = device_info["name"] async def async_turn_on(self, **kwargs): """Turn the light on.""" diff --git a/homeassistant/components/fjaraskupan/number.py b/homeassistant/components/fjaraskupan/number.py index 511d97cbed8..fb793d2328e 100644 --- a/homeassistant/components/fjaraskupan/number.py +++ b/homeassistant/components/fjaraskupan/number.py @@ -34,6 +34,8 @@ async def async_setup_entry( class PeriodicVentingTime(CoordinatorEntity[Coordinator], NumberEntity): """Periodic Venting.""" + _attr_has_entity_name = True + _attr_native_max_value: float = 59 _attr_native_min_value: float = 0 _attr_native_step: float = 1 @@ -51,7 +53,7 @@ class PeriodicVentingTime(CoordinatorEntity[Coordinator], NumberEntity): 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" + self._attr_name = "Periodic venting" @property def native_value(self) -> float | None: diff --git a/homeassistant/components/fjaraskupan/sensor.py b/homeassistant/components/fjaraskupan/sensor.py index e4dbffab38e..81b499661b1 100644 --- a/homeassistant/components/fjaraskupan/sensor.py +++ b/homeassistant/components/fjaraskupan/sensor.py @@ -35,6 +35,8 @@ async def async_setup_entry( class RssiSensor(CoordinatorEntity[Coordinator], SensorEntity): """Sensor device.""" + _attr_has_entity_name = True + def __init__( self, coordinator: Coordinator, @@ -45,7 +47,7 @@ class RssiSensor(CoordinatorEntity[Coordinator], SensorEntity): super().__init__(coordinator) self._attr_unique_id = f"{device.address}-signal-strength" self._attr_device_info = device_info - self._attr_name = f"{device_info['name']} Signal Strength" + self._attr_name = "Signal strength" self._attr_device_class = SensorDeviceClass.SIGNAL_STRENGTH self._attr_state_class = SensorStateClass.MEASUREMENT self._attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT From 5defe67269a679cad7858009a45c75ee9b7907af Mon Sep 17 00:00:00 2001 From: Ethan Madden Date: Sat, 9 Jul 2022 10:51:47 -0700 Subject: [PATCH 2332/3516] `air_quality` and `filter_life` fixes for Pur131S (#74740) --- homeassistant/components/vesync/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/vesync/sensor.py b/homeassistant/components/vesync/sensor.py index 2da6d8ea6b7..45018c79ca0 100644 --- a/homeassistant/components/vesync/sensor.py +++ b/homeassistant/components/vesync/sensor.py @@ -83,13 +83,12 @@ SENSORS: tuple[VeSyncSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda device: device.details["filter_life"], + value_fn=lambda device: device.filter_life, exists_fn=lambda device: sku_supported(device, FILTER_LIFE_SUPPORTED), ), VeSyncSensorEntityDescription( key="air-quality", name="Air Quality", - state_class=SensorStateClass.MEASUREMENT, value_fn=lambda device: device.details["air_quality"], exists_fn=lambda device: sku_supported(device, AIR_QUALITY_SUPPORTED), ), From 0f33c08dca80fc39eb93e9b7ef0db5dabe22a41a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 19:52:26 +0200 Subject: [PATCH 2333/3516] Remove telegram_bot from mypy ignore list (#74661) --- homeassistant/components/telegram_bot/polling.py | 5 +++-- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 4c0de6ade1e..762ba08ab26 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -22,10 +22,11 @@ async def async_setup_platform(hass, bot, config): return True -def process_error(update: Update, context: CallbackContext): +def process_error(update: Update, context: CallbackContext) -> None: """Telegram bot error handler.""" try: - raise context.error + if context.error: + raise context.error except (TimedOut, NetworkError, RetryAfter): # Long polling timeout or connection problem. Nothing serious. pass diff --git a/mypy.ini b/mypy.ini index a883007de0d..57544f298ab 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2819,9 +2819,6 @@ ignore_errors = true [mypy-homeassistant.components.sonos.statistics] ignore_errors = true -[mypy-homeassistant.components.telegram_bot.polling] -ignore_errors = true - [mypy-homeassistant.components.template.number] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 4e11028d574..74a921c8351 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -74,7 +74,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.sonos.sensor", "homeassistant.components.sonos.speaker", "homeassistant.components.sonos.statistics", - "homeassistant.components.telegram_bot.polling", "homeassistant.components.template.number", "homeassistant.components.template.sensor", "homeassistant.components.toon", From e74f7d13ab26563085b56970bdccf03d2fb191b1 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 9 Jul 2022 18:55:33 +0100 Subject: [PATCH 2334/3516] Update systembridgeconnector to 3.3.2 (#74701) --- homeassistant/components/system_bridge/coordinator.py | 7 ++++--- homeassistant/components/system_bridge/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py index 6088967aa33..1719d951cf0 100644 --- a/homeassistant/components/system_bridge/coordinator.py +++ b/homeassistant/components/system_bridge/coordinator.py @@ -93,7 +93,7 @@ class SystemBridgeDataUpdateCoordinator( if not self.websocket_client.connected: await self._setup_websocket() - await self.websocket_client.get_data(modules) + self.hass.async_create_task(self.websocket_client.get_data(modules)) async def async_handle_module( self, @@ -107,9 +107,7 @@ class SystemBridgeDataUpdateCoordinator( async def _listen_for_data(self) -> None: """Listen for events from the WebSocket.""" - try: - await self.websocket_client.register_data_listener(MODULES) await self.websocket_client.listen(callback=self.async_handle_module) except AuthenticationException as exception: self.last_update_success = False @@ -175,6 +173,9 @@ class SystemBridgeDataUpdateCoordinator( self.async_update_listeners() self.hass.async_create_task(self._listen_for_data()) + + await self.websocket_client.register_data_listener(MODULES) + self.last_update_success = True self.async_update_listeners() diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 087613413d8..4fb2201e2c7 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -3,7 +3,7 @@ "name": "System Bridge", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/system_bridge", - "requirements": ["systembridgeconnector==3.1.5"], + "requirements": ["systembridgeconnector==3.3.2"], "codeowners": ["@timmo001"], "zeroconf": ["_system-bridge._tcp.local."], "after_dependencies": ["zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index fad4f243350..4cdf5deb9d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2271,7 +2271,7 @@ swisshydrodata==0.1.0 synology-srm==0.2.0 # homeassistant.components.system_bridge -systembridgeconnector==3.1.5 +systembridgeconnector==3.3.2 # homeassistant.components.tailscale tailscale==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 727724e1e1d..732bf76c8df 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1519,7 +1519,7 @@ sunwatcher==0.2.1 surepy==0.7.2 # homeassistant.components.system_bridge -systembridgeconnector==3.1.5 +systembridgeconnector==3.3.2 # homeassistant.components.tailscale tailscale==0.2.0 From 5360e56d09b87870f06100cbabb6a663faa85f5f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 19:59:11 +0200 Subject: [PATCH 2335/3516] Remove xiaomi_miio from mypy ignore list (#74669) --- .../components/xiaomi_miio/__init__.py | 16 ++++++---- .../components/xiaomi_miio/air_quality.py | 3 +- .../components/xiaomi_miio/binary_sensor.py | 4 +-- .../components/xiaomi_miio/device.py | 4 +-- .../components/xiaomi_miio/device_tracker.py | 4 +-- homeassistant/components/xiaomi_miio/fan.py | 5 ++-- .../components/xiaomi_miio/humidifier.py | 6 +++- homeassistant/components/xiaomi_miio/light.py | 23 ++++++++++---- .../components/xiaomi_miio/sensor.py | 7 +++-- .../components/xiaomi_miio/switch.py | 20 +++++++++---- mypy.ini | 30 ------------------- script/hassfest/mypy_config.py | 10 ------- 12 files changed, 63 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 17211474f97..9f0be1da528 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -18,6 +18,7 @@ from miio import ( CleaningDetails, CleaningSummary, ConsumableStatus, + Device as MiioDevice, DeviceException, DNDStatus, Fan, @@ -283,10 +284,10 @@ async def async_create_miio_device_and_coordinator( host = entry.data[CONF_HOST] token = entry.data[CONF_TOKEN] name = entry.title - device = None + device: MiioDevice | None = None migrate = False update_method = _async_update_data_default - coordinator_class = DataUpdateCoordinator + coordinator_class: type[DataUpdateCoordinator] = DataUpdateCoordinator if ( model not in MODELS_HUMIDIFIER @@ -345,10 +346,13 @@ async def async_create_miio_device_and_coordinator( if migrate: # Removing fan platform entity for humidifiers and migrate the name to the config entry for migration entity_registry = er.async_get(hass) + assert entry.unique_id entity_id = entity_registry.async_get_entity_id("fan", DOMAIN, entry.unique_id) if entity_id: # This check is entities that have a platform migration only and should be removed in the future - if migrate_entity_name := entity_registry.async_get(entity_id).name: + if (entity := entity_registry.async_get(entity_id)) and ( + migrate_entity_name := entity.name + ): hass.config_entries.async_update_entry(entry, title=migrate_entity_name) entity_registry.async_remove(entity_id) @@ -377,8 +381,10 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> name = entry.title gateway_id = entry.unique_id + assert gateway_id + # For backwards compat - if entry.unique_id.endswith("-gateway"): + if gateway_id.endswith("-gateway"): hass.config_entries.async_update_entry(entry, unique_id=entry.data["mac"]) entry.async_on_unload(entry.add_update_listener(update_listener)) @@ -419,7 +425,7 @@ async def async_setup_gateway_entry(hass: HomeAssistant, entry: ConfigEntry) -> return async_update_data - coordinator_dict = {} + coordinator_dict: dict[str, DataUpdateCoordinator] = {} for sub_device in gateway.gateway_device.devices.values(): # Create update coordinator coordinator_dict[sub_device.sid] = DataUpdateCoordinator( diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index 10d6c6129d3..30fcaa5152a 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -1,4 +1,5 @@ """Support for Xiaomi Mi Air Quality Monitor (PM2.5).""" +from collections.abc import Callable import logging from miio import AirQualityMonitor, AirQualityMonitorCGDN1, DeviceException @@ -218,7 +219,7 @@ class AirMonitorCGDN1(XiaomiMiioEntity, AirQualityEntity): return self._particulate_matter_10 -DEVICE_MAP = { +DEVICE_MAP: dict[str, dict[str, Callable]] = { MODEL_AIRQUALITYMONITOR_S1: { "device_class": AirQualityMonitor, "entity_class": AirMonitorS1, diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index d2f00e9805c..a1e833f42de 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Xiaomi Miio binary sensors.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Iterable from dataclasses import dataclass import logging @@ -180,7 +180,7 @@ async def async_setup_entry( if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: model = config_entry.data[CONF_MODEL] - sensors = [] + sensors: Iterable[str] = [] if model in MODEL_AIRFRESH_A1 or model in MODEL_AIRFRESH_T2017: sensors = AIRFRESH_A1_BINARY_SENSORS elif model in MODEL_FAN_ZA5: diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index dc1cf86f7df..f8b5e6bc45a 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -184,9 +184,9 @@ class XiaomiCoordinatedMiioEntity(CoordinatorEntity[_T]): return int(timedelta.total_seconds()) @staticmethod - def _parse_datetime_time(time: datetime.time) -> str: + def _parse_datetime_time(initial_time: datetime.time) -> str: time = datetime.datetime.now().replace( - hour=time.hour, minute=time.minute, second=0, microsecond=0 + hour=initial_time.hour, minute=initial_time.minute, second=0, microsecond=0 ) if time < datetime.datetime.now(): diff --git a/homeassistant/components/xiaomi_miio/device_tracker.py b/homeassistant/components/xiaomi_miio/device_tracker.py index 3f819d7ab7d..1ec914fc647 100644 --- a/homeassistant/components/xiaomi_miio/device_tracker.py +++ b/homeassistant/components/xiaomi_miio/device_tracker.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_TOKEN @@ -18,7 +18,7 @@ from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_TOKEN): vol.All(cv.string, vol.Length(min=32, max=32)), diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index ac85955b347..8ce93933022 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -183,7 +183,8 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Fan from a config entry.""" - entities = [] + entities: list[FanEntity] = [] + entity: FanEntity if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: return @@ -299,7 +300,7 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): return self._preset_modes @property - def percentage(self) -> None: + def percentage(self) -> int | None: """Return the percentage based speed of the fan.""" return None diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index d5c829754d6..05c1eaf35bf 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -68,7 +68,8 @@ async def async_setup_entry( if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: return - entities = [] + entities: list[HumidifierEntity] = [] + entity: HumidifierEntity model = config_entry.data[CONF_MODEL] unique_id = config_entry.unique_id coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] @@ -112,6 +113,7 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): _attr_device_class = HumidifierDeviceClass.HUMIDIFIER _attr_supported_features = HumidifierEntityFeature.MODES + supported_features: int def __init__(self, name, device, entry, unique_id, coordinator): """Initialize the generic Xiaomi device.""" @@ -169,6 +171,8 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): """Representation of a Xiaomi Air Humidifier.""" + available_modes: list[str] + def __init__(self, name, device, entry, unique_id, coordinator): """Initialize the plug switch.""" super().__init__(name, device, entry, unique_id, coordinator) diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 28feb23d93e..fd6af0d9560 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -1,4 +1,6 @@ """Support for Xiaomi Philips Lights.""" +from __future__ import annotations + import asyncio import datetime from datetime import timedelta @@ -6,7 +8,14 @@ from functools import partial import logging from math import ceil -from miio import Ceil, DeviceException, PhilipsBulb, PhilipsEyecare, PhilipsMoonlight +from miio import ( + Ceil, + Device as MiioDevice, + DeviceException, + PhilipsBulb, + PhilipsEyecare, + PhilipsMoonlight, +) from miio.gateway.gateway import ( GATEWAY_MODEL_AC_V1, GATEWAY_MODEL_AC_V2, @@ -115,7 +124,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Xiaomi light from a config entry.""" - entities = [] + entities: list[LightEntity] = [] + entity: LightEntity + light: MiioDevice if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY] @@ -195,7 +206,7 @@ async def async_setup_entry( async def async_service_handler(service: ServiceCall) -> None: """Map services to methods on Xiaomi Philips Lights.""" - method = SERVICE_TO_METHOD.get(service.service) + method = SERVICE_TO_METHOD[service.service] params = { key: value for key, value in service.data.items() @@ -372,7 +383,7 @@ class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): @staticmethod def delayed_turn_off_timestamp( - countdown: int, current: datetime, previous: datetime + countdown: int, current: datetime.datetime, previous: datetime.datetime ): """Update the turn off timestamp only if necessary.""" if countdown is not None and countdown > 0: @@ -671,7 +682,7 @@ class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): @staticmethod def delayed_turn_off_timestamp( - countdown: int, current: datetime, previous: datetime + countdown: int, current: datetime.datetime, previous: datetime.datetime ): """Update the turn off timestamp only if necessary.""" if countdown is not None and countdown > 0: @@ -783,7 +794,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): return 588 @property - def hs_color(self) -> tuple: + def hs_color(self) -> tuple[float, float] | None: """Return the hs color value.""" return self._hs_color diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index df0c953d62a..50feacf5da7 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -1,6 +1,7 @@ """Support for Xiaomi Mi Air Quality Monitor (PM2.5) and Humidifier.""" from __future__ import annotations +from collections.abc import Iterable from dataclasses import dataclass import logging @@ -470,7 +471,7 @@ FAN_V2_V3_SENSORS = ( FAN_ZA5_SENSORS = (ATTR_HUMIDITY, ATTR_TEMPERATURE) -MODEL_TO_SENSORS_MAP = { +MODEL_TO_SENSORS_MAP: dict[str, tuple[str, ...]] = { MODEL_AIRFRESH_A1: AIRFRESH_SENSORS_A1, MODEL_AIRFRESH_VA2: AIRFRESH_SENSORS, MODEL_AIRFRESH_T2017: AIRFRESH_SENSORS_T2017, @@ -661,7 +662,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Xiaomi sensor from a config entry.""" - entities = [] + entities: list[SensorEntity] = [] if config_entry.data[CONF_FLOW_TYPE] == CONF_GATEWAY: gateway = hass.data[DOMAIN][config_entry.entry_id][CONF_GATEWAY] @@ -715,7 +716,7 @@ async def async_setup_entry( ) else: device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] - sensors = [] + sensors: Iterable[str] = [] if model in MODEL_TO_SENSORS_MAP: sensors = MODEL_TO_SENSORS_MAP[model] elif model in MODELS_HUMIDIFIER_MIOT: diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 05d6543e93d..e3d34e8c512 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -201,12 +201,20 @@ MODEL_TO_FEATURES_MAP = { @dataclass -class XiaomiMiioSwitchDescription(SwitchEntityDescription): +class XiaomiMiioSwitchRequiredKeyMixin: + """A class that describes switch entities.""" + + feature: int + method_on: str + method_off: str + + +@dataclass +class XiaomiMiioSwitchDescription( + SwitchEntityDescription, XiaomiMiioSwitchRequiredKeyMixin +): """A class that describes switch entities.""" - feature: int | None = None - method_on: str | None = None - method_off: str | None = None available_with_device_off: bool = True @@ -443,7 +451,7 @@ async def async_setup_other_entry(hass, config_entry, async_add_entities): async def async_service_handler(service: ServiceCall) -> None: """Map services to methods on XiaomiPlugGenericSwitch.""" - method = SERVICE_TO_METHOD.get(service.service) + method = SERVICE_TO_METHOD[service.service] params = { key: value for key, value in service.data.items() @@ -480,6 +488,8 @@ async def async_setup_other_entry(hass, config_entry, async_add_entities): class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): """Representation of a Xiaomi Plug Generic.""" + entity_description: XiaomiMiioSwitchDescription + def __init__(self, name, device, entry, unique_id, coordinator, description): """Initialize the plug switch.""" super().__init__(name, device, entry, unique_id, coordinator) diff --git a/mypy.ini b/mypy.ini index 57544f298ab..53ebc8c74ce 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2863,33 +2863,3 @@ ignore_errors = true [mypy-homeassistant.components.xbox.sensor] ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.air_quality] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.device] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.device_tracker] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.fan] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.humidifier] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.light] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.sensor] -ignore_errors = true - -[mypy-homeassistant.components.xiaomi_miio.switch] -ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 74a921c8351..fc3e26778a4 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -89,16 +89,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.xbox.browse_media", "homeassistant.components.xbox.media_source", "homeassistant.components.xbox.sensor", - "homeassistant.components.xiaomi_miio", - "homeassistant.components.xiaomi_miio.air_quality", - "homeassistant.components.xiaomi_miio.binary_sensor", - "homeassistant.components.xiaomi_miio.device", - "homeassistant.components.xiaomi_miio.device_tracker", - "homeassistant.components.xiaomi_miio.fan", - "homeassistant.components.xiaomi_miio.humidifier", - "homeassistant.components.xiaomi_miio.light", - "homeassistant.components.xiaomi_miio.sensor", - "homeassistant.components.xiaomi_miio.switch", ] # Component modules which should set no_implicit_reexport = true. From ea1fc6b5d3fbaed043ec2ad576a9e91120ca271c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 9 Jul 2022 11:59:36 -0600 Subject: [PATCH 2336/3516] Migrate SimpliSafe to new entity naming style (#74763) --- homeassistant/components/simplisafe/__init__.py | 13 ++++++------- .../components/simplisafe/binary_sensor.py | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 14a0d6dd8e9..716163c4957 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -113,8 +113,7 @@ ATTR_SYSTEM_ID = "system_id" ATTR_TIMESTAMP = "timestamp" DEFAULT_CONFIG_URL = "https://webapp.simplisafe.com/new/#/dashboard" -DEFAULT_ENTITY_MODEL = "alarm_control_panel" -DEFAULT_ENTITY_NAME = "Alarm Control Panel" +DEFAULT_ENTITY_MODEL = "Alarm control panel" DEFAULT_ERROR_THRESHOLD = 2 DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) DEFAULT_SOCKET_MIN_RETRY = 15 @@ -660,6 +659,8 @@ class SimpliSafe: class SimpliSafeEntity(CoordinatorEntity): """Define a base SimpliSafe entity.""" + _attr_has_entity_name = True + def __init__( self, simplisafe: SimpliSafe, @@ -679,12 +680,11 @@ class SimpliSafeEntity(CoordinatorEntity): self._error_count = 0 if device: - model = device.type.name - device_name = device.name + model = device.type.name.capitalize().replace("_", " ") + device_name = f"{device.name.capitalize()} {model}" serial = device.serial else: - model = DEFAULT_ENTITY_MODEL - device_name = DEFAULT_ENTITY_NAME + model = device_name = DEFAULT_ENTITY_MODEL serial = system.serial event = simplisafe.initial_event_to_use[system.system_id] @@ -714,7 +714,6 @@ class SimpliSafeEntity(CoordinatorEntity): 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 diff --git a/homeassistant/components/simplisafe/binary_sensor.py b/homeassistant/components/simplisafe/binary_sensor.py index 3e4c64e8658..239f5468cb3 100644 --- a/homeassistant/components/simplisafe/binary_sensor.py +++ b/homeassistant/components/simplisafe/binary_sensor.py @@ -103,7 +103,7 @@ class BatteryBinarySensor(SimpliSafeEntity, BinarySensorEntity): """Initialize.""" super().__init__(simplisafe, system, device=sensor) - self._attr_name = f"{super().name} Battery" + self._attr_name = "Battery" self._attr_unique_id = f"{super().unique_id}-battery" self._device: SensorV3 From ac21dc929f473107573d7163ecf953e4c4095c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 9 Jul 2022 20:00:24 +0200 Subject: [PATCH 2337/3516] Update aioairzone to v0.4.6 (#74810) --- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index e189ae741ad..e4f94327708 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -3,7 +3,7 @@ "name": "Airzone", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airzone", - "requirements": ["aioairzone==0.4.5"], + "requirements": ["aioairzone==0.4.6"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioairzone"] diff --git a/requirements_all.txt b/requirements_all.txt index 4cdf5deb9d8..902df711bd6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -113,7 +113,7 @@ aio_geojson_usgs_earthquakes==0.1 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.5 +aioairzone==0.4.6 # homeassistant.components.ambient_station aioambient==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 732bf76c8df..b9370052c10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,7 +100,7 @@ aio_geojson_usgs_earthquakes==0.1 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.5 +aioairzone==0.4.6 # homeassistant.components.ambient_station aioambient==2021.11.0 From 0b1f29b2b9945223a6b0ce7441eb9934dfcbf66e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 20:01:39 +0200 Subject: [PATCH 2338/3516] Remove nilu from mypy ignore list (#74412) --- homeassistant/components/nilu/air_quality.py | 35 ++++++++++---------- mypy.ini | 3 -- script/hassfest/mypy_config.py | 1 - 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/nilu/air_quality.py b/homeassistant/components/nilu/air_quality.py index dd60afe2ff6..400c9f0ab8e 100644 --- a/homeassistant/components/nilu/air_quality.py +++ b/homeassistant/components/nilu/air_quality.py @@ -121,21 +121,22 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the NILU air quality sensor.""" - name = config.get(CONF_NAME) - area = config.get(CONF_AREA) - stations = config.get(CONF_STATION) - show_on_map = config.get(CONF_SHOW_ON_MAP) + name: str = config[CONF_NAME] + area: str | None = config.get(CONF_AREA) + stations: list[str] | None = config.get(CONF_STATION) + show_on_map: bool = config[CONF_SHOW_ON_MAP] sensors = [] if area: stations = lookup_stations_in_area(area) - elif not area and not stations: + elif not stations: latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) location_client = create_location_client(latitude, longitude) stations = location_client.station_names + assert stations is not None for station in stations: client = NiluData(create_station_client(station)) client.update() @@ -195,61 +196,61 @@ class NiluSensor(AirQualityEntity): return self._name @property - def air_quality_index(self) -> str: + def air_quality_index(self) -> str | None: """Return the Air Quality Index (AQI).""" return self._max_aqi @property - def carbon_monoxide(self) -> str: + def carbon_monoxide(self) -> str | None: """Return the CO (carbon monoxide) level.""" return self.get_component_state(CO) @property - def carbon_dioxide(self) -> str: + def carbon_dioxide(self) -> str | None: """Return the CO2 (carbon dioxide) level.""" return self.get_component_state(CO2) @property - def nitrogen_oxide(self) -> str: + def nitrogen_oxide(self) -> str | None: """Return the N2O (nitrogen oxide) level.""" return self.get_component_state(NOX) @property - def nitrogen_monoxide(self) -> str: + def nitrogen_monoxide(self) -> str | None: """Return the NO (nitrogen monoxide) level.""" return self.get_component_state(NO) @property - def nitrogen_dioxide(self) -> str: + def nitrogen_dioxide(self) -> str | None: """Return the NO2 (nitrogen dioxide) level.""" return self.get_component_state(NO2) @property - def ozone(self) -> str: + def ozone(self) -> str | None: """Return the O3 (ozone) level.""" return self.get_component_state(OZONE) @property - def particulate_matter_2_5(self) -> str: + def particulate_matter_2_5(self) -> str | None: """Return the particulate matter 2.5 level.""" return self.get_component_state(PM25) @property - def particulate_matter_10(self) -> str: + def particulate_matter_10(self) -> str | None: """Return the particulate matter 10 level.""" return self.get_component_state(PM10) @property - def particulate_matter_0_1(self) -> str: + def particulate_matter_0_1(self) -> str | None: """Return the particulate matter 0.1 level.""" return self.get_component_state(PM1) @property - def sulphur_dioxide(self) -> str: + def sulphur_dioxide(self) -> str | None: """Return the SO2 (sulphur dioxide) level.""" return self.get_component_state(SO2) - def get_component_state(self, component_name: str) -> str: + def get_component_state(self, component_name: str) -> str | None: """Return formatted value of specified component.""" if component_name in self._api.data.sensors: sensor = self._api.data.sensors[component_name] diff --git a/mypy.ini b/mypy.ini index 53ebc8c74ce..a8af4b574f4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2729,9 +2729,6 @@ ignore_errors = true [mypy-homeassistant.components.minecraft_server.sensor] ignore_errors = true -[mypy-homeassistant.components.nilu.air_quality] -ignore_errors = true - [mypy-homeassistant.components.nzbget] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index fc3e26778a4..e49f16455f7 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -44,7 +44,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.minecraft_server", "homeassistant.components.minecraft_server.helpers", "homeassistant.components.minecraft_server.sensor", - "homeassistant.components.nilu.air_quality", "homeassistant.components.nzbget", "homeassistant.components.nzbget.config_flow", "homeassistant.components.nzbget.coordinator", From 7cac1ae6a34a86ade3ba827757158aa615906d92 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 20:03:31 +0200 Subject: [PATCH 2339/3516] Use instance attributes in ign_sismologia (#74399) --- .../components/ign_sismologia/geo_location.py | 112 +++++++----------- 1 file changed, 43 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index cca3e61a5a3..1194c58d2ca 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -1,15 +1,19 @@ """Support for IGN Sismologia (Earthquakes) Feeds.""" from __future__ import annotations +from collections.abc import Callable from datetime import timedelta import logging +from typing import Any -from georss_ign_sismologia_client import IgnSismologiaFeedManager +from georss_ign_sismologia_client import ( + IgnSismologiaFeedEntry, + IgnSismologiaFeedManager, +) import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, @@ -17,7 +21,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, LENGTH_KILOMETERS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -61,19 +65,19 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the IGN Sismologia Feed platform.""" - scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - coordinates = ( + scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + coordinates: tuple[float, float] = ( config.get(CONF_LATITUDE, hass.config.latitude), config.get(CONF_LONGITUDE, hass.config.longitude), ) - radius_in_km = config[CONF_RADIUS] - minimum_magnitude = config[CONF_MINIMUM_MAGNITUDE] + radius_in_km: float = config[CONF_RADIUS] + minimum_magnitude: float = config[CONF_MINIMUM_MAGNITUDE] # Initialize the entity manager. feed = IgnSismologiaFeedEntityManager( hass, add_entities, scan_interval, coordinates, radius_in_km, minimum_magnitude ) - def start_feed_manager(event): + def start_feed_manager(event: Event) -> None: """Start feed manager.""" feed.startup() @@ -85,13 +89,13 @@ class IgnSismologiaFeedEntityManager: def __init__( self, - hass, - add_entities, - scan_interval, - coordinates, - radius_in_km, - minimum_magnitude, - ): + hass: HomeAssistant, + add_entities: AddEntitiesCallback, + scan_interval: timedelta, + coordinates: tuple[float, float], + radius_in_km: float, + minimum_magnitude: float, + ) -> None: """Initialize the Feed Entity Manager.""" self._hass = hass @@ -106,32 +110,32 @@ class IgnSismologiaFeedEntityManager: self._add_entities = add_entities self._scan_interval = scan_interval - def startup(self): + def startup(self) -> None: """Start up this manager.""" self._feed_manager.update() self._init_regular_updates() - def _init_regular_updates(self): + def _init_regular_updates(self) -> None: """Schedule regular updates at the specified interval.""" track_time_interval( self._hass, lambda now: self._feed_manager.update(), self._scan_interval ) - def get_entry(self, external_id): + def get_entry(self, external_id: str) -> IgnSismologiaFeedEntry | None: """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - def _generate_entity(self, external_id): + def _generate_entity(self, external_id: str) -> None: """Generate new entity.""" new_entity = IgnSismologiaLocationEvent(self, external_id) # Add new entities to HA. self._add_entities([new_entity], True) - def _update_entity(self, external_id): + def _update_entity(self, external_id: str) -> None: """Update entity.""" dispatcher_send(self._hass, f"ign_sismologia_update_{external_id}") - def _remove_entity(self, external_id): + def _remove_entity(self, external_id: str) -> None: """Remove entity.""" dispatcher_send(self._hass, f"ign_sismologia_delete_{external_id}") @@ -139,25 +143,26 @@ class IgnSismologiaFeedEntityManager: class IgnSismologiaLocationEvent(GeolocationEvent): """This represents an external event with IGN Sismologia feed data.""" + _attr_icon = "mdi:pulse" + _attr_should_poll = False + _attr_source = SOURCE _attr_unit_of_measurement = LENGTH_KILOMETERS - def __init__(self, feed_manager, external_id): + def __init__( + self, feed_manager: IgnSismologiaFeedEntityManager, external_id: str + ) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager self._external_id = external_id self._title = None - self._distance = None - self._latitude = None - self._longitude = None - self._attribution = None self._region = None self._magnitude = None self._publication_date = None self._image_url = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self._remove_signal_delete = async_dispatcher_connect( self.hass, @@ -171,51 +176,36 @@ class IgnSismologiaLocationEvent(GeolocationEvent): ) @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for IGN Sismologia feed location events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed(self, feed_entry: IgnSismologiaFeedEntry) -> None: """Update the internal state from the provided feed entry.""" self._title = feed_entry.title - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] - self._attribution = feed_entry.attribution + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] + self._attr_attribution = feed_entry.attribution self._region = feed_entry.region self._magnitude = feed_entry.magnitude self._publication_date = feed_entry.published self._image_url = feed_entry.image_url - @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:pulse" - - @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - @property def name(self) -> str | None: """Return the name of the entity.""" @@ -228,22 +218,7 @@ class IgnSismologiaLocationEvent(GeolocationEvent): return self._title @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = {} for key, value in ( @@ -251,7 +226,6 @@ class IgnSismologiaLocationEvent(GeolocationEvent): (ATTR_TITLE, self._title), (ATTR_REGION, self._region), (ATTR_MAGNITUDE, self._magnitude), - (ATTR_ATTRIBUTION, self._attribution), (ATTR_PUBLICATION_DATE, self._publication_date), (ATTR_IMAGE_URL, self._image_url), ): From 2cc9db5468d2ae41084b4d177152c200bc0f6ffe Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 9 Jul 2022 20:30:48 +0200 Subject: [PATCH 2340/3516] Make deCONZ utilise forward_entry_setups (#74823) --- homeassistant/components/deconz/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 112f29db333..cb4715f3c28 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -46,12 +46,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b gateway = hass.data[DOMAIN][config_entry.entry_id] = DeconzGateway( hass, config_entry, api ) + await gateway.async_update_device_registry() config_entry.add_update_listener(gateway.async_config_entry_updated) - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) await async_setup_events(gateway) - await gateway.async_update_device_registry() + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) if len(hass.data[DOMAIN]) == 1: async_setup_services(hass) From 5b32eea3d04d223b01efddb5c13a88e540df8741 Mon Sep 17 00:00:00 2001 From: simeon-simsoft <61541002+simeon-simsoft@users.noreply.github.com> Date: Sat, 9 Jul 2022 19:41:39 +0100 Subject: [PATCH 2341/3516] Add support for bidirectional chargers to Wallbox integration (#74313) * Add support for the Quasar bidirectional charger to the Wallbox integration, including ability to control charger while discharging, set a negative charge rate and monitor discharged amount * Make code more generic in order to support other bidirectional models in the future * Updates to files to comply with HA formatting rules * Change const file to fix black check failure * Remove unnecessay loop in number entity --- homeassistant/components/wallbox/const.py | 3 +++ homeassistant/components/wallbox/number.py | 22 ++++++++++++++++------ homeassistant/components/wallbox/sensor.py | 9 +++++++++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py index 0e4e1477911..9d1db637879 100644 --- a/homeassistant/components/wallbox/const.py +++ b/homeassistant/components/wallbox/const.py @@ -3,7 +3,10 @@ from homeassistant.backports.enum import StrEnum DOMAIN = "wallbox" +BIDIRECTIONAL_MODEL_PREFIXES = ["QSX"] + CONF_STATION = "station" +CHARGER_ADDED_DISCHARGED_ENERGY_KEY = "added_discharged_energy" CHARGER_ADDED_ENERGY_KEY = "added_energy" CHARGER_ADDED_RANGE_KEY = "added_range" CHARGER_CHARGING_POWER_KEY = "charging_power" diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index 1db791fd389..5470ec11532 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -1,4 +1,4 @@ -"""Home Assistant component for accessing the Wallbox Portal API. The sensor component creates multiple sensors regarding wallbox performance.""" +"""Home Assistant component for accessing the Wallbox Portal API. The number component allows control of charging current.""" from __future__ import annotations from dataclasses import dataclass @@ -11,9 +11,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import InvalidAuth, WallboxCoordinator, WallboxEntity from .const import ( + BIDIRECTIONAL_MODEL_PREFIXES, CHARGER_DATA_KEY, CHARGER_MAX_AVAILABLE_POWER_KEY, CHARGER_MAX_CHARGING_CURRENT_KEY, + CHARGER_PART_NUMBER_KEY, CHARGER_SERIAL_NUMBER_KEY, DOMAIN, ) @@ -21,14 +23,13 @@ from .const import ( @dataclass class WallboxNumberEntityDescription(NumberEntityDescription): - """Describes Wallbox sensor entity.""" + """Describes Wallbox number entity.""" NUMBER_TYPES: dict[str, WallboxNumberEntityDescription] = { CHARGER_MAX_CHARGING_CURRENT_KEY: WallboxNumberEntityDescription( key=CHARGER_MAX_CHARGING_CURRENT_KEY, name="Max. Charging Current", - native_min_value=6, ), } @@ -36,7 +37,7 @@ NUMBER_TYPES: dict[str, WallboxNumberEntityDescription] = { async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Create wallbox sensor entities in HASS.""" + """Create wallbox number entities in HASS.""" coordinator: WallboxCoordinator = hass.data[DOMAIN][entry.entry_id] # Check if the user is authorized to change current, if so, add number component: try: @@ -66,21 +67,30 @@ class WallboxNumber(WallboxEntity, NumberEntity): entry: ConfigEntry, description: WallboxNumberEntityDescription, ) -> None: - """Initialize a Wallbox sensor.""" + """Initialize a Wallbox number entity.""" super().__init__(coordinator) self.entity_description = description self._coordinator = coordinator self._attr_name = f"{entry.title} {description.name}" self._attr_unique_id = f"{description.key}-{coordinator.data[CHARGER_DATA_KEY][CHARGER_SERIAL_NUMBER_KEY]}" + self._is_bidirectional = ( + coordinator.data[CHARGER_DATA_KEY][CHARGER_PART_NUMBER_KEY][0:3] + in BIDIRECTIONAL_MODEL_PREFIXES + ) @property def native_max_value(self) -> float: """Return the maximum available current.""" return cast(float, self._coordinator.data[CHARGER_MAX_AVAILABLE_POWER_KEY]) + @property + def native_min_value(self) -> float: + """Return the minimum available current based on charger type - some chargers can discharge.""" + return (self.max_value * -1) if self._is_bidirectional else 6 + @property def native_value(self) -> float | None: - """Return the state of the sensor.""" + """Return the value of the entity.""" return cast( Optional[float], self._coordinator.data[CHARGER_MAX_CHARGING_CURRENT_KEY] ) diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py index e3598ca7e07..2c4a8c67bed 100644 --- a/homeassistant/components/wallbox/sensor.py +++ b/homeassistant/components/wallbox/sensor.py @@ -25,6 +25,7 @@ from homeassistant.helpers.typing import StateType from . import WallboxCoordinator, WallboxEntity from .const import ( + CHARGER_ADDED_DISCHARGED_ENERGY_KEY, CHARGER_ADDED_ENERGY_KEY, CHARGER_ADDED_RANGE_KEY, CHARGER_CHARGING_POWER_KEY, @@ -94,6 +95,14 @@ SENSOR_TYPES: dict[str, WallboxSensorEntityDescription] = { device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), + CHARGER_ADDED_DISCHARGED_ENERGY_KEY: WallboxSensorEntityDescription( + key=CHARGER_ADDED_DISCHARGED_ENERGY_KEY, + name="Discharged Energy", + precision=2, + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), CHARGER_COST_KEY: WallboxSensorEntityDescription( key=CHARGER_COST_KEY, icon="mdi:ev-station", From cdcfd1149e7ee77686eb2a2f3f8a6d0c8c33f4af Mon Sep 17 00:00:00 2001 From: Stephan Uhle Date: Sat, 9 Jul 2022 22:22:30 +0200 Subject: [PATCH 2342/3516] Fixed unit of measurement. #70121 (#74838) --- homeassistant/components/edl21/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index f7a79d727a0..65603b0c8c4 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -17,6 +17,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( CONF_NAME, + DEGREE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, @@ -250,6 +251,7 @@ SENSOR_UNIT_MAPPING = { "W": POWER_WATT, "A": ELECTRIC_CURRENT_AMPERE, "V": ELECTRIC_POTENTIAL_VOLT, + "°": DEGREE, } @@ -449,7 +451,7 @@ class EDL21Entity(SensorEntity): @property def native_unit_of_measurement(self): """Return the unit of measurement.""" - if (unit := self._telegram.get("unit")) is None: + if (unit := self._telegram.get("unit")) is None or unit == 0: return None return SENSOR_UNIT_MAPPING[unit] From d37ad2089412006d2a47d4ab078cf15dd3fde7b8 Mon Sep 17 00:00:00 2001 From: Michel van de Wetering Date: Sat, 9 Jul 2022 22:29:50 +0200 Subject: [PATCH 2343/3516] Fix mediaplayer join service groupmembers definition (#74807) --- homeassistant/components/media_player/services.yaml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index b698b87aec6..5a513e4f3a0 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -257,11 +257,14 @@ join: group_members: name: Group members description: The players which will be synced with the target player. - example: - - "media_player.multiroom_player2" - - "media_player.multiroom_player3" + required: true + example: | + - media_player.multiroom_player2 + - media_player.multiroom_player3 selector: - object: + entity: + multiple: true + domain: media_player unjoin: description: From 16900dcef15bdb9016feabd12bfec94d61ed4df6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 22:32:57 +0200 Subject: [PATCH 2344/3516] Make Store a generic class (#74617) --- homeassistant/auth/auth_store.py | 5 +++-- homeassistant/auth/mfa_modules/notify.py | 10 ++++------ homeassistant/auth/mfa_modules/totp.py | 12 +++++------- homeassistant/auth/providers/homeassistant.py | 13 ++++++------- homeassistant/components/almond/__init__.py | 6 +++--- .../components/ambiclimate/climate.py | 2 +- .../components/analytics/analytics.py | 8 ++++---- homeassistant/components/camera/prefs.py | 8 ++++---- homeassistant/components/energy/data.py | 10 ++++++---- homeassistant/components/hassio/__init__.py | 4 +--- .../components/homekit_controller/storage.py | 13 +++++++------ homeassistant/components/http/__init__.py | 12 +++++++----- homeassistant/components/http/auth.py | 6 +++--- .../components/mobile_app/__init__.py | 3 ++- homeassistant/components/nest/media_source.py | 15 +++++---------- homeassistant/components/network/network.py | 10 ++++++---- .../resolution_center/issue_registry.py | 7 +++++-- .../components/smartthings/smartapp.py | 7 ++++--- homeassistant/components/trace/__init__.py | 4 +++- homeassistant/components/zha/core/store.py | 4 ++-- homeassistant/config_entries.py | 4 +++- homeassistant/core.py | 6 +++--- homeassistant/helpers/area_registry.py | 9 ++++++--- homeassistant/helpers/device_registry.py | 3 +-- homeassistant/helpers/instance_id.py | 2 +- homeassistant/helpers/restore_state.py | 2 +- homeassistant/helpers/storage.py | 18 ++++++++++-------- 27 files changed, 106 insertions(+), 97 deletions(-) diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index baf5a8bf3b3..2597781dc60 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -46,7 +46,7 @@ class AuthStore: self._users: dict[str, models.User] | None = None self._groups: dict[str, models.Group] | None = None self._perm_lookup: PermissionLookup | None = None - self._store = Store( + self._store = Store[dict[str, list[dict[str, Any]]]]( hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) self._lock = asyncio.Lock() @@ -483,9 +483,10 @@ class AuthStore: jwt_key=rt_dict["jwt_key"], last_used_at=last_used_at, last_used_ip=rt_dict.get("last_used_ip"), - credential=credentials.get(rt_dict.get("credential_id")), version=rt_dict.get("version"), ) + if "credential_id" in rt_dict: + token.credential = credentials.get(rt_dict["credential_id"]) users[rt_dict["user_id"]].refresh_tokens[token.id] = token self._groups = groups diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 3872257a205..464ce495050 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -7,7 +7,7 @@ from __future__ import annotations import asyncio from collections import OrderedDict import logging -from typing import Any +from typing import Any, cast import attr import voluptuous as vol @@ -100,7 +100,7 @@ class NotifyAuthModule(MultiFactorAuthModule): """Initialize the user data store.""" super().__init__(hass, config) self._user_settings: _UsersDict | None = None - self._user_store = Store( + self._user_store = Store[dict[str, dict[str, Any]]]( hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) self._include = config.get(CONF_INCLUDE, []) @@ -119,10 +119,8 @@ class NotifyAuthModule(MultiFactorAuthModule): if self._user_settings is not None: return - if (data := await self._user_store.async_load()) is None or not isinstance( - data, dict - ): - data = {STORAGE_USERS: {}} + if (data := await self._user_store.async_load()) is None: + data = cast(dict[str, dict[str, Any]], {STORAGE_USERS: {}}) self._user_settings = { user_id: NotifySetting(**setting) diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index e503198f08b..397a7fcd386 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from io import BytesIO -from typing import Any +from typing import Any, cast import voluptuous as vol @@ -77,7 +77,7 @@ class TotpAuthModule(MultiFactorAuthModule): """Initialize the user data store.""" super().__init__(hass, config) self._users: dict[str, str] | None = None - self._user_store = Store( + self._user_store = Store[dict[str, dict[str, str]]]( hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) self._init_lock = asyncio.Lock() @@ -93,16 +93,14 @@ class TotpAuthModule(MultiFactorAuthModule): if self._users is not None: return - if (data := await self._user_store.async_load()) is None or not isinstance( - data, dict - ): - data = {STORAGE_USERS: {}} + if (data := await self._user_store.async_load()) is None: + data = cast(dict[str, dict[str, str]], {STORAGE_USERS: {}}) self._users = data.get(STORAGE_USERS, {}) async def _async_save(self) -> None: """Save data.""" - await self._user_store.async_save({STORAGE_USERS: self._users}) + await self._user_store.async_save({STORAGE_USERS: self._users or {}}) def _add_ota_secret(self, user_id: str, secret: str | None = None) -> str: """Create a ota_secret for user.""" diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index cb95907c9b2..d190a618596 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -61,10 +61,10 @@ class Data: def __init__(self, hass: HomeAssistant) -> None: """Initialize the user data store.""" self.hass = hass - self._store = Store( + self._store = Store[dict[str, list[dict[str, str]]]]( hass, STORAGE_VERSION, STORAGE_KEY, private=True, atomic_writes=True ) - self._data: dict[str, Any] | None = None + self._data: dict[str, list[dict[str, str]]] | None = None # Legacy mode will allow usernames to start/end with whitespace # and will compare usernames case-insensitive. # Remove in 2020 or when we launch 1.0. @@ -80,10 +80,8 @@ class Data: async def async_load(self) -> None: """Load stored data.""" - if (data := await self._store.async_load()) is None or not isinstance( - data, dict - ): - data = {"users": []} + if (data := await self._store.async_load()) is None: + data = cast(dict[str, list[dict[str, str]]], {"users": []}) seen: set[str] = set() @@ -123,7 +121,8 @@ class Data: @property def users(self) -> list[dict[str, str]]: """Return users.""" - return self._data["users"] # type: ignore[index,no-any-return] + assert self._data is not None + return self._data["users"] def validate_login(self, username: str, password: str) -> None: """Validate a username and password. diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 15c280d9c1e..09ff85491ba 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -5,7 +5,7 @@ import asyncio from datetime import timedelta import logging import time -from typing import Optional, cast +from typing import Any from aiohttp import ClientError, ClientSession import async_timeout @@ -167,8 +167,8 @@ async def _configure_almond_for_ha( return _LOGGER.debug("Configuring Almond to connect to Home Assistant at %s", hass_url) - store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) - data = cast(Optional[dict], await store.async_load()) + store = storage.Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) + data = await store.async_load() if data is None: data = {} diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index 93d9348655f..50135693ff4 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -64,7 +64,7 @@ async def async_setup_entry( """Set up the Ambiclimate device from config entry.""" config = entry.data websession = async_get_clientsession(hass) - store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) token_info = await store.async_load() oauth = ambiclimate.AmbiclimateOAuth( diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 802aa33585a..5bb0368b021 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -1,6 +1,6 @@ """Analytics helper class for the analytics integration.""" import asyncio -from typing import cast +from typing import Any import uuid import aiohttp @@ -66,12 +66,12 @@ class Analytics: """Initialize the Analytics class.""" self.hass: HomeAssistant = hass self.session = async_get_clientsession(hass) - self._data: dict = { + self._data: dict[str, Any] = { ATTR_PREFERENCES: {}, ATTR_ONBOARDED: False, ATTR_UUID: None, } - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + self._store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) @property def preferences(self) -> dict: @@ -109,7 +109,7 @@ class Analytics: async def load(self) -> None: """Load preferences.""" - stored = cast(dict, await self._store.async_load()) + stored = await self._store.async_load() if stored: self._data = stored diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index 3d54c10d09a..08c57631a1b 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -36,14 +36,14 @@ class CameraPreferences: def __init__(self, hass: HomeAssistant) -> None: """Initialize camera prefs.""" self._hass = hass - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + self._store = Store[dict[str, dict[str, bool]]]( + hass, STORAGE_VERSION, STORAGE_KEY + ) self._prefs: dict[str, dict[str, bool]] | None = None async def async_initialize(self) -> None: """Finish initializing the preferences.""" - if (prefs := await self._store.async_load()) is None or not isinstance( - prefs, dict - ): + if (prefs := await self._store.async_load()) is None: prefs = {} self._prefs = prefs diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py index d33f915628d..e8c62da0c3c 100644 --- a/homeassistant/components/energy/data.py +++ b/homeassistant/components/energy/data.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections import Counter from collections.abc import Awaitable, Callable -from typing import Literal, Optional, TypedDict, Union, cast +from typing import Literal, TypedDict, Union import voluptuous as vol @@ -263,13 +263,15 @@ class EnergyManager: def __init__(self, hass: HomeAssistant) -> None: """Initialize energy manager.""" self._hass = hass - self._store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) + self._store = storage.Store[EnergyPreferences]( + hass, STORAGE_VERSION, STORAGE_KEY + ) self.data: EnergyPreferences | None = None self._update_listeners: list[Callable[[], Awaitable]] = [] async def async_initialize(self) -> None: """Initialize the energy integration.""" - self.data = cast(Optional[EnergyPreferences], await self._store.async_load()) + self.data = await self._store.async_load() @staticmethod def default_preferences() -> EnergyPreferences: @@ -294,7 +296,7 @@ class EnergyManager: data[key] = update[key] # type: ignore[literal-required] self.data = data - self._store.async_delay_save(lambda: cast(dict, self.data), 60) + self._store.async_delay_save(lambda: data, 60) if not self._update_listeners: return diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index d580847646d..46592cbc20c 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -533,12 +533,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: if not await hassio.is_connected(): _LOGGER.warning("Not connected with the supervisor / system too busy!") - store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + store = Store[dict[str, str]](hass, STORAGE_VERSION, STORAGE_KEY) if (data := await store.async_load()) is None: data = {} - assert isinstance(data, dict) - refresh_token = None if "hassio_user" in data: user = await hass.auth.async_get_user(data["hassio_user"]) diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index 9372764a88a..ff39c52627e 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, TypedDict, cast +from typing import Any, TypedDict from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.storage import Store @@ -46,7 +46,9 @@ class EntityMapStorage: def __init__(self, hass: HomeAssistant) -> None: """Create a new entity map store.""" self.hass = hass - self.store = Store(hass, ENTITY_MAP_STORAGE_VERSION, ENTITY_MAP_STORAGE_KEY) + self.store = Store[StorageLayout]( + hass, ENTITY_MAP_STORAGE_VERSION, ENTITY_MAP_STORAGE_KEY + ) self.storage_data: dict[str, Pairing] = {} async def async_initialize(self) -> None: @@ -55,8 +57,7 @@ class EntityMapStorage: # There is no cached data about HomeKit devices yet return - storage = cast(StorageLayout, raw_storage) - self.storage_data = storage.get("pairings", {}) + self.storage_data = raw_storage.get("pairings", {}) def get_map(self, homekit_id: str) -> Pairing | None: """Get a pairing cache item.""" @@ -87,6 +88,6 @@ class EntityMapStorage: self.store.async_delay_save(self._data_to_save, ENTITY_MAP_SAVE_DELAY) @callback - def _data_to_save(self) -> dict[str, Any]: + def _data_to_save(self) -> StorageLayout: """Return data of entity map to store in a file.""" - return {"pairings": self.storage_data} + return StorageLayout(pairings=self.storage_data) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 374e69975ce..7c8594bdd90 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -7,7 +7,7 @@ import logging import os import ssl from tempfile import NamedTemporaryFile -from typing import Any, Final, Optional, TypedDict, Union, cast +from typing import Any, Final, TypedDict, Union, cast from aiohttp import web from aiohttp.typedefs import StrOrURL @@ -125,10 +125,10 @@ class ConfData(TypedDict, total=False): @bind_hass -async def async_get_last_config(hass: HomeAssistant) -> dict | None: +async def async_get_last_config(hass: HomeAssistant) -> dict[str, Any] | None: """Return the last known working config.""" - store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) - return cast(Optional[dict], await store.async_load()) + store = storage.Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) + return await store.async_load() class ApiConfig: @@ -475,7 +475,9 @@ async def start_http_server_and_save_config( await server.start() # If we are set up successful, we store the HTTP settings for safe mode. - store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) + store: storage.Store[dict[str, Any]] = storage.Store( + hass, STORAGE_VERSION, STORAGE_KEY + ) if CONF_TRUSTED_PROXIES in conf: conf[CONF_TRUSTED_PROXIES] = [ diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 18f68cc386f..7c6f445ce80 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -6,7 +6,7 @@ from datetime import timedelta from ipaddress import ip_address import logging import secrets -from typing import Final +from typing import Any, Final from aiohttp import hdrs from aiohttp.web import Application, Request, StreamResponse, middleware @@ -118,8 +118,8 @@ def async_user_not_allowed_do_auth( async def async_setup_auth(hass: HomeAssistant, app: Application) -> None: """Create auth middleware for the app.""" - store = Store(hass, STORAGE_VERSION, STORAGE_KEY) - if (data := await store.async_load()) is None or not isinstance(data, dict): + store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) + if (data := await store.async_load()) is None: data = {} refresh_token = None diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 7aac961042b..70c23da66e2 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -1,5 +1,6 @@ """Integrates Native Apps to Home Assistant.""" from contextlib import suppress +from typing import Any from homeassistant.components import cloud, notify as hass_notify from homeassistant.components.webhook import ( @@ -38,7 +39,7 @@ PLATFORMS = [Platform.SENSOR, Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the mobile app component.""" - store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) if (app_config := await store.async_load()) is None or not isinstance( app_config, dict ): diff --git a/homeassistant/components/nest/media_source.py b/homeassistant/components/nest/media_source.py index 4614d4b1ed4..7b5b96b7145 100644 --- a/homeassistant/components/nest/media_source.py +++ b/homeassistant/components/nest/media_source.py @@ -22,6 +22,7 @@ from collections.abc import Mapping from dataclasses import dataclass import logging import os +from typing import Any from google_nest_sdm.camera_traits import CameraClipPreviewTrait, CameraEventImageTrait from google_nest_sdm.device import Device @@ -89,7 +90,7 @@ async def async_get_media_event_store( os.makedirs(media_path, exist_ok=True) await hass.async_add_executor_job(mkdir) - store = Store(hass, STORAGE_VERSION, STORAGE_KEY, private=True) + store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY, private=True) return NestEventMediaStore(hass, subscriber, store, media_path) @@ -119,7 +120,7 @@ class NestEventMediaStore(EventMediaStore): self, hass: HomeAssistant, subscriber: GoogleNestSubscriber, - store: Store, + store: Store[dict[str, Any]], media_path: str, ) -> None: """Initialize NestEventMediaStore.""" @@ -127,7 +128,7 @@ class NestEventMediaStore(EventMediaStore): self._subscriber = subscriber self._store = store self._media_path = media_path - self._data: dict | None = None + self._data: dict[str, Any] | None = None self._devices: Mapping[str, str] | None = {} async def async_load(self) -> dict | None: @@ -137,15 +138,9 @@ class NestEventMediaStore(EventMediaStore): if (data := await self._store.async_load()) is None: _LOGGER.debug("Loaded empty event store") self._data = {} - elif isinstance(data, dict): + else: _LOGGER.debug("Loaded event store with %d records", len(data)) self._data = data - else: - raise ValueError( - "Unexpected data in storage version={}, key={}".format( - STORAGE_VERSION, STORAGE_KEY - ) - ) return self._data async def async_save(self, data: dict) -> None: diff --git a/homeassistant/components/network/network.py b/homeassistant/components/network/network.py index b2caf6438bd..e9542ec2d54 100644 --- a/homeassistant/components/network/network.py +++ b/homeassistant/components/network/network.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import Any from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.singleton import singleton @@ -38,8 +38,10 @@ class Network: def __init__(self, hass: HomeAssistant) -> None: """Initialize the Network class.""" - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True) - self._data: dict[str, Any] = {} + self._store = Store[dict[str, list[str]]]( + hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True + ) + self._data: dict[str, list[str]] = {} self.adapters: list[Adapter] = [] @property @@ -67,7 +69,7 @@ class Network: async def async_load(self) -> None: """Load config.""" if stored := await self._store.async_load(): - self._data = cast(dict, stored) + self._data = stored async def _async_save(self) -> None: """Save preferences.""" diff --git a/homeassistant/components/resolution_center/issue_registry.py b/homeassistant/components/resolution_center/issue_registry.py index d97ad73bbac..7d5bbb482ba 100644 --- a/homeassistant/components/resolution_center/issue_registry.py +++ b/homeassistant/components/resolution_center/issue_registry.py @@ -2,7 +2,7 @@ from __future__ import annotations import dataclasses -from typing import cast +from typing import Optional, cast from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant, callback @@ -39,7 +39,9 @@ class IssueRegistry: """Initialize the issue registry.""" self.hass = hass self.issues: dict[tuple[str, str], IssueEntry] = {} - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True) + self._store = Store[dict[str, list[dict[str, Optional[str]]]]]( + hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True + ) @callback def async_get_issue(self, domain: str, issue_id: str) -> IssueEntry | None: @@ -119,6 +121,7 @@ class IssueRegistry: if isinstance(data, dict): for issue in data["issues"]: + assert issue["domain"] and issue["issue_id"] issues[(issue["domain"], issue["issue_id"])] = IssueEntry( active=False, breaks_in_ha_version=None, diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index fbd63d41373..adf0426e9a2 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -3,6 +3,7 @@ import asyncio import functools import logging import secrets +from typing import Any from urllib.parse import urlparse from uuid import uuid4 @@ -211,8 +212,8 @@ async def setup_smartapp_endpoint(hass: HomeAssistant): return # Get/create config to store a unique id for this hass instance. - store = Store(hass, STORAGE_VERSION, STORAGE_KEY) - if not (config := await store.async_load()) or not isinstance(config, dict): + store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) + if not (config := await store.async_load()): # Create config config = { CONF_INSTANCE_ID: str(uuid4()), @@ -283,7 +284,7 @@ async def unload_smartapp_endpoint(hass: HomeAssistant): if cloudhook_url and cloud.async_is_logged_in(hass): await cloud.async_delete_cloudhook(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]) # Remove cloudhook from storage - store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) await store.async_save( { CONF_INSTANCE_ID: hass.data[DOMAIN][CONF_INSTANCE_ID], diff --git a/homeassistant/components/trace/__init__.py b/homeassistant/components/trace/__init__.py index 14783fd3f84..3761ff155b4 100644 --- a/homeassistant/components/trace/__init__.py +++ b/homeassistant/components/trace/__init__.py @@ -52,7 +52,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Initialize the trace integration.""" hass.data[DATA_TRACE] = {} websocket_api.async_setup(hass) - store = Store(hass, STORAGE_VERSION, STORAGE_KEY, encoder=ExtendedJSONEncoder) + store = Store[dict[str, list]]( + hass, STORAGE_VERSION, STORAGE_KEY, encoder=ExtendedJSONEncoder + ) hass.data[DATA_TRACE_STORE] = store async def _async_store_traces_at_stop(*_) -> None: diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index e58dcd46dba..0b7564fe815 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -40,7 +40,7 @@ class ZhaStorage: """Initialize the zha device storage.""" self.hass: HomeAssistant = hass self.devices: MutableMapping[str, ZhaDeviceEntry] = {} - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY) + self._store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) @callback def async_create_device(self, device: ZHADevice) -> ZhaDeviceEntry: @@ -94,7 +94,7 @@ class ZhaStorage: async def async_load(self) -> None: """Load the registry of zha device entries.""" - data = cast(dict[str, Any], await self._store.async_load()) + data = await self._store.async_load() devices: OrderedDict[str, ZhaDeviceEntry] = OrderedDict() diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b25b62aa6e0..4b76f63681a 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -845,7 +845,9 @@ class ConfigEntries: self._hass_config = hass_config self._entries: dict[str, ConfigEntry] = {} self._domain_index: dict[str, list[str]] = {} - self._store = storage.Store(hass, STORAGE_VERSION, STORAGE_KEY) + self._store = storage.Store[dict[str, list[dict[str, Any]]]]( + hass, STORAGE_VERSION, STORAGE_KEY + ) EntityRegistryDisabledHandler(hass).async_setup() @callback diff --git a/homeassistant/core.py b/homeassistant/core.py index b568ee72689..7b41fe476aa 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -1942,7 +1942,7 @@ class Config: # pylint: disable=import-outside-toplevel from .helpers.storage import Store - store = Store( + store = Store[dict[str, Any]]( self.hass, CORE_STORAGE_VERSION, CORE_STORAGE_KEY, @@ -1950,7 +1950,7 @@ class Config: atomic_writes=True, ) - if not (data := await store.async_load()) or not isinstance(data, dict): + if not (data := await store.async_load()): return # In 2021.9 we fixed validation to disallow a path (because that's never correct) @@ -1998,7 +1998,7 @@ class Config: "currency": self.currency, } - store = Store( + store: Store[dict[str, Any]] = Store( self.hass, CORE_STORAGE_VERSION, CORE_STORAGE_KEY, diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index e5d35ccbf44..aeb52e8faed 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections import OrderedDict from collections.abc import Container, Iterable, MutableMapping -from typing import cast +from typing import Optional, cast import attr @@ -49,7 +49,9 @@ class AreaRegistry: """Initialize the area registry.""" self.hass = hass self.areas: MutableMapping[str, AreaEntry] = {} - self._store = Store(hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True) + self._store = Store[dict[str, list[dict[str, Optional[str]]]]]( + hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True + ) self._normalized_name_area_idx: dict[str, str] = {} @callback @@ -176,8 +178,9 @@ class AreaRegistry: areas: MutableMapping[str, AreaEntry] = OrderedDict() - if isinstance(data, dict): + if data is not None: for area in data["areas"]: + assert area["name"] is not None and area["id"] is not None normalized_name = normalize_area_name(area["name"]) areas[area["id"]] = AreaEntry( name=area["name"], diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index ed3d5a7b06f..ca5e6e1aefa 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -164,7 +164,7 @@ def _async_get_device_id_from_index( return None -class DeviceRegistryStore(storage.Store): +class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): """Store entity registry data.""" async def _async_migrate_func( @@ -569,7 +569,6 @@ class DeviceRegistry: deleted_devices = OrderedDict() if data is not None: - data = cast("dict[str, Any]", data) for device in data["devices"]: devices[device["id"]] = DeviceEntry( area_id=device["area_id"], diff --git a/homeassistant/helpers/instance_id.py b/homeassistant/helpers/instance_id.py index 59a4cf39498..8561d10794c 100644 --- a/homeassistant/helpers/instance_id.py +++ b/homeassistant/helpers/instance_id.py @@ -16,7 +16,7 @@ LEGACY_UUID_FILE = ".uuid" @singleton.singleton(DATA_KEY) async def async_get(hass: HomeAssistant) -> str: """Get unique ID for the hass instance.""" - store = storage.Store(hass, DATA_VERSION, DATA_KEY, True) + store = storage.Store[dict[str, str]](hass, DATA_VERSION, DATA_KEY, True) data: dict[str, str] | None = await storage.async_migrator( hass, diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index b8262d3a533..4f2d1dd0503 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -139,7 +139,7 @@ class RestoreStateData: def __init__(self, hass: HomeAssistant) -> None: """Initialize the restore state data class.""" self.hass: HomeAssistant = hass - self.store: Store = Store( + self.store = Store[list[dict[str, Any]]]( hass, STORAGE_VERSION, STORAGE_KEY, encoder=JSONEncoder ) self.last_states: dict[str, StoredState] = {} diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 554a88f4ad5..6819a1eb48b 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -2,14 +2,14 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Mapping, Sequence from contextlib import suppress from copy import deepcopy import inspect from json import JSONEncoder import logging import os -from typing import Any +from typing import Any, Generic, TypeVar, Union from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback @@ -24,6 +24,8 @@ _LOGGER = logging.getLogger(__name__) STORAGE_SEMAPHORE = "storage_semaphore" +_T = TypeVar("_T", bound=Union[Mapping[str, Any], Sequence[Any]]) + @bind_hass async def async_migrator( @@ -66,7 +68,7 @@ async def async_migrator( @bind_hass -class Store: +class Store(Generic[_T]): """Class to help storing data.""" def __init__( @@ -90,7 +92,7 @@ class Store: self._unsub_delay_listener: CALLBACK_TYPE | None = None self._unsub_final_write_listener: CALLBACK_TYPE | None = None self._write_lock = asyncio.Lock() - self._load_task: asyncio.Future | None = None + self._load_task: asyncio.Future[_T | None] | None = None self._encoder = encoder self._atomic_writes = atomic_writes @@ -99,7 +101,7 @@ class Store: """Return the config path.""" return self.hass.config.path(STORAGE_DIR, self.key) - async def async_load(self) -> dict | list | None: + async def async_load(self) -> _T | None: """Load data. If the expected version and minor version do not match the given versions, the @@ -113,7 +115,7 @@ class Store: return await self._load_task - async def _async_load(self): + async def _async_load(self) -> _T | None: """Load the data and ensure the task is removed.""" if STORAGE_SEMAPHORE not in self.hass.data: self.hass.data[STORAGE_SEMAPHORE] = asyncio.Semaphore(MAX_LOAD_CONCURRENTLY) @@ -178,7 +180,7 @@ class Store: return stored - async def async_save(self, data: dict | list) -> None: + async def async_save(self, data: _T) -> None: """Save data.""" self._data = { "version": self.version, @@ -196,7 +198,7 @@ class Store: @callback def async_delay_save( self, - data_func: Callable[[], dict | list], + data_func: Callable[[], _T], delay: float = 0, ) -> None: """Save data with an optional delay.""" From 81cdbf4f9b578b5c827af6bca793d9ee6d5aa12a Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Sat, 9 Jul 2022 23:53:47 +0300 Subject: [PATCH 2345/3516] Bump python-gammu to 3.2.4 with Python 3.10 support (#74797) --- homeassistant/components/sms/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sms/manifest.json b/homeassistant/components/sms/manifest.json index d98304ebf23..b3426c01422 100644 --- a/homeassistant/components/sms/manifest.json +++ b/homeassistant/components/sms/manifest.json @@ -3,7 +3,7 @@ "name": "SMS notifications via GSM-modem", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sms", - "requirements": ["python-gammu==3.2.3"], + "requirements": ["python-gammu==3.2.4"], "codeowners": ["@ocalvo"], "iot_class": "local_polling", "loggers": ["gammu"] diff --git a/requirements_all.txt b/requirements_all.txt index 902df711bd6..2ac21604734 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1898,7 +1898,7 @@ python-family-hub-local==0.0.2 python-forecastio==1.4.0 # homeassistant.components.sms -# python-gammu==3.2.3 +# python-gammu==3.2.4 # homeassistant.components.gc100 python-gc100==1.0.3a0 From f53bf1127f81aa919b1b157ffa4ff0dce73636b6 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 9 Jul 2022 22:07:32 +0100 Subject: [PATCH 2346/3516] Hide homekit_controller implementation that doesn't apply to BLE (#74836) --- homeassistant/components/homekit_controller/config_flow.py | 2 +- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index b4bd66aa626..24a04874f20 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -240,7 +240,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # that the device is available and we should not wait # to retry connecting any longer. reconnect_soon # will do nothing if the device is already connected - await conn.pairing.connection.reconnect_soon() + await conn.pairing.reconnect_soon() if conn.config_num != config_num: _LOGGER.debug( "HomeKit info %s: c# incremented, refreshing entities", hkid diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 955f5e37177..6517b078454 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.7.20"], + "requirements": ["aiohomekit==0.7.21"], "zeroconf": ["_hap._tcp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 2ac21604734..6f78c0206c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.20 +aiohomekit==0.7.21 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b9370052c10..0a2ad4fcb05 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.20 +aiohomekit==0.7.21 # homeassistant.components.emulated_hue # homeassistant.components.http From da133a7f05b53d4cd1fb9547f90b1addd0604c4f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 23:09:15 +0200 Subject: [PATCH 2347/3516] Remove xbox from mypy ignore list (#74504) --- homeassistant/components/xbox/__init__.py | 6 +++--- homeassistant/components/xbox/base_sensor.py | 4 ++-- homeassistant/components/xbox/binary_sensor.py | 4 ++-- homeassistant/components/xbox/browse_media.py | 8 +++++--- homeassistant/components/xbox/media_source.py | 4 ++-- homeassistant/components/xbox/sensor.py | 4 ++-- mypy.ini | 18 ------------------ script/hassfest/mypy_config.py | 6 ------ 8 files changed, 16 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index bee773d2094..6e8492e45ca 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -180,7 +180,7 @@ class XboxUpdateCoordinator(DataUpdateCoordinator): name=DOMAIN, update_interval=timedelta(seconds=10), ) - self.data: XboxData = XboxData({}, []) + self.data: XboxData = XboxData({}, {}) self.client: XboxLiveClient = client self.consoles: SmartglassConsoleList = consoles @@ -230,7 +230,7 @@ class XboxUpdateCoordinator(DataUpdateCoordinator): ) # Update user presence - presence_data = {} + presence_data: dict[str, PresenceData] = {} batch: PeopleResponse = await self.client.people.get_friends_own_batch( [self.client.xuid] ) @@ -262,7 +262,7 @@ def _build_presence_data(person: Person) -> PresenceData: online=person.presence_state == "Online", status=person.presence_text, in_party=person.multiplayer_summary.in_party > 0, - in_game=active_app and active_app.is_game, + in_game=active_app is not None and active_app.is_game, in_multiplayer=person.multiplayer_summary.in_multiplayer_session, gamer_score=person.gamer_score, gold_tenure=person.detail.tenure, diff --git a/homeassistant/components/xbox/base_sensor.py b/homeassistant/components/xbox/base_sensor.py index 024feb294b5..5d0f3f92434 100644 --- a/homeassistant/components/xbox/base_sensor.py +++ b/homeassistant/components/xbox/base_sensor.py @@ -33,7 +33,7 @@ class XboxBaseSensorEntity(CoordinatorEntity[XboxUpdateCoordinator]): return self.coordinator.data.presence.get(self.xuid) @property - def name(self) -> str: + def name(self) -> str | None: """Return the name of the sensor.""" if not self.data: return None @@ -45,7 +45,7 @@ class XboxBaseSensorEntity(CoordinatorEntity[XboxUpdateCoordinator]): return f"{self.data.gamertag} {attr_name}" @property - def entity_picture(self) -> str: + def entity_picture(self) -> str | None: """Return the gamer pic.""" if not self.data: return None diff --git a/homeassistant/components/xbox/binary_sensor.py b/homeassistant/components/xbox/binary_sensor.py index 7cf7ca6a6a5..ac97d502c55 100644 --- a/homeassistant/components/xbox/binary_sensor.py +++ b/homeassistant/components/xbox/binary_sensor.py @@ -54,7 +54,7 @@ def async_update_friends( current_ids = set(current) # Process new favorites, add them to Home Assistant - new_entities = [] + new_entities: list[XboxBinarySensorEntity] = [] for xuid in new_ids - current_ids: current[xuid] = [ XboxBinarySensorEntity(coordinator, xuid, attribute) @@ -75,7 +75,7 @@ def async_update_friends( async def async_remove_entities( xuid: str, coordinator: XboxUpdateCoordinator, - current: dict[str, XboxBinarySensorEntity], + current: dict[str, list[XboxBinarySensorEntity]], ) -> None: """Remove friend sensors from Home Assistant.""" registry = er.async_get(coordinator.hass) diff --git a/homeassistant/components/xbox/browse_media.py b/homeassistant/components/xbox/browse_media.py index b6e5a89efb3..ee1eabf1e00 100644 --- a/homeassistant/components/xbox/browse_media.py +++ b/homeassistant/components/xbox/browse_media.py @@ -1,7 +1,7 @@ """Support for media browsing.""" from __future__ import annotations -from typing import NamedTuple +from typing import TYPE_CHECKING, NamedTuple from xbox.webapi.api.client import XboxLiveClient from xbox.webapi.api.provider.catalog.const import HOME_APP_IDS, SYSTEM_PFN_ID_MAP @@ -65,6 +65,8 @@ async def build_item_response( can_expand=True, children=[], ) + if TYPE_CHECKING: + assert library_info.children is not None # Add Home id_type = AlternateIdType.LEGACY_XBOX_PRODUCT_ID @@ -84,7 +86,7 @@ async def build_item_response( title="Home", can_play=True, can_expand=False, - thumbnail=home_thumb.uri, + thumbnail=None if home_thumb is None else home_thumb.uri, ) ) @@ -107,7 +109,7 @@ async def build_item_response( title="Live TV", can_play=True, can_expand=False, - thumbnail=tv_thumb.uri, + thumbnail=None if tv_thumb is None else tv_thumb.uri, ) ) diff --git a/homeassistant/components/xbox/media_source.py b/homeassistant/components/xbox/media_source.py index c6dae46a955..21b9b25ce2d 100644 --- a/homeassistant/components/xbox/media_source.py +++ b/homeassistant/components/xbox/media_source.py @@ -55,7 +55,7 @@ def async_parse_identifier( identifier = item.identifier or "" start = ["", "", ""] items = identifier.lstrip("/").split("~~", 2) - return tuple(items + start[len(items) :]) + return tuple(items + start[len(items) :]) # type: ignore[return-value] @dataclass @@ -201,7 +201,7 @@ class XboxSource(MediaSource): ) -def _build_game_item(item: InstalledPackage, images: list[Image]): +def _build_game_item(item: InstalledPackage, images: dict[str, list[Image]]): """Build individual game.""" thumbnail = "" image = _find_media_image(images.get(item.one_store_product_id, [])) diff --git a/homeassistant/components/xbox/sensor.py b/homeassistant/components/xbox/sensor.py index 02b4f8b84a4..9cba49d1dcb 100644 --- a/homeassistant/components/xbox/sensor.py +++ b/homeassistant/components/xbox/sensor.py @@ -56,7 +56,7 @@ def async_update_friends( current_ids = set(current) # Process new favorites, add them to Home Assistant - new_entities = [] + new_entities: list[XboxSensorEntity] = [] for xuid in new_ids - current_ids: current[xuid] = [ XboxSensorEntity(coordinator, xuid, attribute) @@ -77,7 +77,7 @@ def async_update_friends( async def async_remove_entities( xuid: str, coordinator: XboxUpdateCoordinator, - current: dict[str, XboxSensorEntity], + current: dict[str, list[XboxSensorEntity]], ) -> None: """Remove friend sensors from Home Assistant.""" registry = er.async_get(coordinator.hass) diff --git a/mypy.ini b/mypy.ini index a8af4b574f4..13be5d712f4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2842,21 +2842,3 @@ ignore_errors = true [mypy-homeassistant.components.withings.config_flow] ignore_errors = true - -[mypy-homeassistant.components.xbox] -ignore_errors = true - -[mypy-homeassistant.components.xbox.base_sensor] -ignore_errors = true - -[mypy-homeassistant.components.xbox.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.xbox.browse_media] -ignore_errors = true - -[mypy-homeassistant.components.xbox.media_source] -ignore_errors = true - -[mypy-homeassistant.components.xbox.sensor] -ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index e49f16455f7..199d6d00a7d 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -82,12 +82,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.withings.binary_sensor", "homeassistant.components.withings.common", "homeassistant.components.withings.config_flow", - "homeassistant.components.xbox", - "homeassistant.components.xbox.base_sensor", - "homeassistant.components.xbox.binary_sensor", - "homeassistant.components.xbox.browse_media", - "homeassistant.components.xbox.media_source", - "homeassistant.components.xbox.sensor", ] # Component modules which should set no_implicit_reexport = true. From 3922141f5c24473c670d41dfe400cc1d8a02c53d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 23:11:55 +0200 Subject: [PATCH 2348/3516] Remove omnilogic from mypy ignore list (#74452) --- homeassistant/components/omnilogic/common.py | 3 ++- homeassistant/components/omnilogic/sensor.py | 7 +++++-- homeassistant/components/omnilogic/switch.py | 6 ++++-- mypy.ini | 9 --------- script/hassfest/mypy_config.py | 3 --- 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/omnilogic/common.py b/homeassistant/components/omnilogic/common.py index 4c92420972b..e28fd53d6fe 100644 --- a/homeassistant/components/omnilogic/common.py +++ b/homeassistant/components/omnilogic/common.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from typing import Any from omnilogic import OmniLogic, OmniLogicException @@ -122,7 +123,7 @@ class OmniLogicEntity(CoordinatorEntity[OmniLogicUpdateCoordinator]): self._unique_id = unique_id self._item_id = item_id self._icon = icon - self._attrs = {} + self._attrs: dict[str, Any] = {} self._msp_system_id = msp_system_id self._backyard_name = coordinator.data[backyard_id]["BackyardName"] diff --git a/homeassistant/components/omnilogic/sensor.py b/homeassistant/components/omnilogic/sensor.py index ce9b29ef1d4..04bb1abf3e8 100644 --- a/homeassistant/components/omnilogic/sensor.py +++ b/homeassistant/components/omnilogic/sensor.py @@ -1,4 +1,6 @@ """Definition and setup of the Omnilogic Sensors for Home Assistant.""" +from typing import Any + from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -34,7 +36,8 @@ async def async_setup_entry( continue for entity_setting in entity_settings: - for state_key, entity_class in entity_setting["entity_classes"].items(): + entity_classes: dict[str, type] = entity_setting["entity_classes"] + for state_key, entity_class in entity_classes.items(): if check_guard(state_key, item, entity_setting): continue @@ -248,7 +251,7 @@ class OmniLogicORPSensor(OmnilogicSensor): return orp_state -SENSOR_TYPES = { +SENSOR_TYPES: dict[tuple[int, str], list[dict[str, Any]]] = { (2, "Backyard"): [ { "entity_classes": {"airTemp": OmniLogicTemperatureSensor}, diff --git a/homeassistant/components/omnilogic/switch.py b/homeassistant/components/omnilogic/switch.py index 3edb4a20d33..2d2ad08d38a 100644 --- a/homeassistant/components/omnilogic/switch.py +++ b/homeassistant/components/omnilogic/switch.py @@ -1,5 +1,6 @@ """Platform for Omnilogic switch integration.""" import time +from typing import Any from omnilogic import OmniLogicException import voluptuous as vol @@ -34,7 +35,8 @@ async def async_setup_entry( continue for entity_setting in entity_settings: - for state_key, entity_class in entity_setting["entity_classes"].items(): + entity_classes: dict[str, type] = entity_setting["entity_classes"] + for state_key, entity_class in entity_classes.items(): if check_guard(state_key, item, entity_setting): continue @@ -229,7 +231,7 @@ class OmniLogicPumpControl(OmniLogicSwitch): raise OmniLogicException("Cannot set speed on a non-variable speed pump.") -SWITCH_TYPES = { +SWITCH_TYPES: dict[tuple[int, str], list[dict[str, Any]]] = { (4, "Relays"): [ { "entity_classes": {"switchState": OmniLogicRelayControl}, diff --git a/mypy.ini b/mypy.ini index 13be5d712f4..06dec7d4897 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2741,15 +2741,6 @@ ignore_errors = true [mypy-homeassistant.components.nzbget.switch] ignore_errors = true -[mypy-homeassistant.components.omnilogic.common] -ignore_errors = true - -[mypy-homeassistant.components.omnilogic.sensor] -ignore_errors = true - -[mypy-homeassistant.components.omnilogic.switch] -ignore_errors = true - [mypy-homeassistant.components.onvif.base] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 199d6d00a7d..4ca5b3509d4 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -48,9 +48,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.nzbget.config_flow", "homeassistant.components.nzbget.coordinator", "homeassistant.components.nzbget.switch", - "homeassistant.components.omnilogic.common", - "homeassistant.components.omnilogic.sensor", - "homeassistant.components.omnilogic.switch", "homeassistant.components.onvif.base", "homeassistant.components.onvif.binary_sensor", "homeassistant.components.onvif.camera", From d49c58cf8777ad23570289676f30596214f51ff7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 23:14:07 +0200 Subject: [PATCH 2349/3516] Use instance attributes in geonetnz_quakes (#74401) --- .../geonetnz_quakes/geo_location.py | 111 ++++++------------ 1 file changed, 39 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py index 515fce56439..26ad780d098 100644 --- a/homeassistant/components/geonetnz_quakes/geo_location.py +++ b/homeassistant/components/geonetnz_quakes/geo_location.py @@ -1,12 +1,15 @@ """Geolocation support for GeoNet NZ Quakes Feeds.""" from __future__ import annotations +from collections.abc import Callable import logging +from typing import Any + +from aio_geojson_geonetnz_quakes.feed_entry import GeonetnzQuakesFeedEntry from homeassistant.components.geo_location import GeolocationEvent from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_TIME, CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, @@ -18,6 +21,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from . import GeonetnzQuakesFeedEntityManager from .const import DOMAIN, FEED _LOGGER = logging.getLogger(__name__) @@ -40,10 +44,14 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the GeoNet NZ Quakes Feed platform.""" - manager = hass.data[DOMAIN][FEED][entry.entry_id] + manager: GeonetnzQuakesFeedEntityManager = hass.data[DOMAIN][FEED][entry.entry_id] @callback - def async_add_geolocation(feed_manager, integration_id, external_id): + def async_add_geolocation( + feed_manager: GeonetnzQuakesFeedEntityManager, + integration_id: str, + external_id: str, + ) -> None: """Add geolocation entity from feed.""" new_entity = GeonetnzQuakesEvent(feed_manager, integration_id, external_id) _LOGGER.debug("Adding geolocation %s", new_entity) @@ -63,27 +71,34 @@ async def async_setup_entry( class GeonetnzQuakesEvent(GeolocationEvent): """This represents an external event with GeoNet NZ Quakes feed data.""" - def __init__(self, feed_manager, integration_id, external_id): + _attr_icon = "mdi:pulse" + _attr_should_poll = False + _attr_source = SOURCE + + def __init__( + self, + feed_manager: GeonetnzQuakesFeedEntityManager, + integration_id: str, + external_id: str, + ) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager - self._integration_id = integration_id self._external_id = external_id - self._title = None - self._distance = None - self._latitude = None - self._longitude = None - self._attribution = None + self._attr_unique_id = f"{integration_id}_{external_id}" + self._attr_unit_of_measurement = LENGTH_KILOMETERS self._depth = None self._locality = None self._magnitude = None self._mmi = None self._quality = None self._time = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" + if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + self._attr_unit_of_measurement = LENGTH_MILES self._remove_signal_delete = async_dispatcher_connect( self.hass, f"geonetnz_quakes_delete_{self._external_id}", @@ -105,40 +120,35 @@ class GeonetnzQuakesEvent(GeolocationEvent): entity_registry.async_remove(self.entity_id) @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for GeoNet NZ Quakes feed location events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed(self, feed_entry: GeonetnzQuakesFeedEntry) -> None: """Update the internal state from the provided feed entry.""" - self._title = feed_entry.title + self._attr_name = feed_entry.title # Convert distance if not metric system. if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - self._distance = IMPERIAL_SYSTEM.length( + self._attr_distance = IMPERIAL_SYSTEM.length( feed_entry.distance_to_home, LENGTH_KILOMETERS ) else: - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] - self._attribution = feed_entry.attribution + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] + self._attr_attribution = feed_entry.attribution self._depth = feed_entry.depth self._locality = feed_entry.locality self._magnitude = feed_entry.magnitude @@ -147,54 +157,11 @@ class GeonetnzQuakesEvent(GeolocationEvent): self._time = feed_entry.time @property - def unique_id(self) -> str | None: - """Return a unique ID containing latitude/longitude and external id.""" - return f"{self._integration_id}_{self._external_id}" - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return "mdi:pulse" - - @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._title - - @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - return LENGTH_MILES - return LENGTH_KILOMETERS - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = {} for key, value in ( (ATTR_EXTERNAL_ID, self._external_id), - (ATTR_ATTRIBUTION, self._attribution), (ATTR_DEPTH, self._depth), (ATTR_LOCALITY, self._locality), (ATTR_MAGNITUDE, self._magnitude), From d04e77ef7f1f7e0ac677f444728552d3a2adbb48 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 23:17:14 +0200 Subject: [PATCH 2350/3516] Use instance attributes in geo_json_events (#74397) --- .../geo_json_events/geo_location.py | 110 +++++++----------- 1 file changed, 42 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/geo_json_events/geo_location.py b/homeassistant/components/geo_json_events/geo_location.py index 896fb7d36da..c0d6abe694f 100644 --- a/homeassistant/components/geo_json_events/geo_location.py +++ b/homeassistant/components/geo_json_events/geo_location.py @@ -1,10 +1,13 @@ """Support for generic GeoJSON events.""" from __future__ import annotations -from datetime import timedelta +from collections.abc import Callable +from datetime import datetime, timedelta import logging +from typing import Any from aio_geojson_generic_client import GenericFeedManager +from aio_geojson_generic_client.feed_entry import GenericFeedEntry import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent @@ -17,7 +20,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, LENGTH_KILOMETERS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import aiohttp_client import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -55,20 +58,20 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the GeoJSON Events platform.""" - url = config[CONF_URL] - scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - coordinates = ( + url: str = config[CONF_URL] + scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + coordinates: tuple[float, float] = ( config.get(CONF_LATITUDE, hass.config.latitude), config.get(CONF_LONGITUDE, hass.config.longitude), ) - radius_in_km = config[CONF_RADIUS] + radius_in_km: float = config[CONF_RADIUS] # Initialize the entity manager. manager = GeoJsonFeedEntityManager( hass, async_add_entities, scan_interval, coordinates, url, radius_in_km ) await manager.async_init() - async def start_feed_manager(event=None): + async def start_feed_manager(event: Event) -> None: """Start feed manager.""" await manager.async_update() @@ -79,8 +82,14 @@ class GeoJsonFeedEntityManager: """Feed Entity Manager for GeoJSON feeds.""" def __init__( - self, hass, async_add_entities, scan_interval, coordinates, url, radius_in_km - ): + self, + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + scan_interval: timedelta, + coordinates: tuple[float, float], + url: str, + radius_in_km: float, + ) -> None: """Initialize the GeoJSON Feed Manager.""" self._hass = hass @@ -97,10 +106,10 @@ class GeoJsonFeedEntityManager: self._async_add_entities = async_add_entities self._scan_interval = scan_interval - async def async_init(self): + async def async_init(self) -> None: """Schedule initial and regular updates based on configured time interval.""" - async def update(event_time): + async def update(event_time: datetime) -> None: """Update.""" await self.async_update() @@ -108,26 +117,26 @@ class GeoJsonFeedEntityManager: async_track_time_interval(self._hass, update, self._scan_interval) _LOGGER.debug("Feed entity manager initialized") - async def async_update(self): + async def async_update(self) -> None: """Refresh data.""" await self._feed_manager.update() _LOGGER.debug("Feed entity manager updated") - def get_entry(self, external_id): + def get_entry(self, external_id: str) -> GenericFeedEntry | None: """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - async def _generate_entity(self, external_id): + async def _generate_entity(self, external_id: str) -> None: """Generate new entity.""" new_entity = GeoJsonLocationEvent(self, external_id) # Add new entities to HA. self._async_add_entities([new_entity], True) - async def _update_entity(self, external_id): + async def _update_entity(self, external_id: str) -> None: """Update entity.""" async_dispatcher_send(self._hass, f"geo_json_events_update_{external_id}") - async def _remove_entity(self, external_id): + async def _remove_entity(self, external_id: str) -> None: """Remove entity.""" async_dispatcher_send(self._hass, f"geo_json_events_delete_{external_id}") @@ -135,18 +144,18 @@ class GeoJsonFeedEntityManager: class GeoJsonLocationEvent(GeolocationEvent): """This represents an external event with GeoJSON data.""" - def __init__(self, feed_manager, external_id): + _attr_should_poll = False + _attr_source = SOURCE + _attr_unit_of_measurement = LENGTH_KILOMETERS + + def __init__(self, feed_manager: GenericFeedManager, external_id: str) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager self._external_id = external_id - self._name = None - self._distance = None - self._latitude = None - self._longitude = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self._remove_signal_delete = async_dispatcher_connect( self.hass, @@ -160,68 +169,33 @@ class GeoJsonLocationEvent(GeolocationEvent): ) @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for GeoJSON location events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed(self, feed_entry: GenericFeedEntry) -> None: """Update the internal state from the provided feed entry.""" - self._name = feed_entry.title - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] + self._attr_name = feed_entry.title + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._name - - @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return LENGTH_KILOMETERS - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" if not self._external_id: return {} From 36bb34f39117ce7beab2d1cfffa3b9043d9b2c29 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 23:18:53 +0200 Subject: [PATCH 2351/3516] Remove kostal_plenticore from mypy ignore list (#74433) --- .../components/kostal_plenticore/helper.py | 44 +++++++++++-------- .../components/kostal_plenticore/sensor.py | 15 ++++++- mypy.ini | 12 ----- script/hassfest/mypy_config.py | 4 -- 4 files changed, 38 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index c87d96161a4..ee684b68974 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from collections import defaultdict -from collections.abc import Callable, Iterable +from collections.abc import Callable from datetime import datetime, timedelta import logging from typing import Any @@ -122,17 +122,20 @@ class Plenticore: class DataUpdateCoordinatorMixin: """Base implementation for read and write data.""" - async def async_read_data(self, module_id: str, data_id: str) -> list[str, bool]: + _plenticore: Plenticore + name: str + + async def async_read_data( + self, module_id: str, data_id: str + ) -> dict[str, dict[str, str]] | None: """Read data from Plenticore.""" if (client := self._plenticore.client) is None: - return False + return None try: - val = await client.get_setting_values(module_id, data_id) + return await client.get_setting_values(module_id, data_id) except PlenticoreApiException: - return False - else: - return val + return None async def async_write_data(self, module_id: str, value: dict[str, str]) -> bool: """Write settings back to Plenticore.""" @@ -170,7 +173,7 @@ class PlenticoreUpdateCoordinator(DataUpdateCoordinator): update_interval=update_inverval, ) # data ids to poll - self._fetch = defaultdict(list) + self._fetch: dict[str, list[str]] = defaultdict(list) self._plenticore = plenticore def start_fetch_data(self, module_id: str, data_id: str) -> None: @@ -246,7 +249,7 @@ class PlenticoreSelectUpdateCoordinator(DataUpdateCoordinator): update_interval=update_inverval, ) # data ids to poll - self._fetch = defaultdict(list) + self._fetch: dict[str, list[str]] = defaultdict(list) self._plenticore = plenticore def start_fetch_data(self, module_id: str, data_id: str, all_options: str) -> None: @@ -284,20 +287,23 @@ class SelectDataUpdateCoordinator( async def _async_get_current_option( self, - module_id: str | dict[str, Iterable[str]], + module_id: dict[str, list[str]], ) -> dict[str, dict[str, str]]: """Get current option.""" for mid, pids in module_id.items(): all_options = pids[1] for all_option in all_options: - if all_option != "None": - val = await self.async_read_data(mid, all_option) - for option in val.values(): - if option[all_option] == "1": - fetched = {mid: {pids[0]: all_option}} - return fetched + if all_option == "None" or not ( + val := await self.async_read_data(mid, all_option) + ): + continue + for option in val.values(): + if option[all_option] == "1": + fetched = {mid: {pids[0]: all_option}} + return fetched return {mid: {pids[0]: "None"}} + return {} class PlenticoreDataFormatter: @@ -361,7 +367,7 @@ class PlenticoreDataFormatter: return "" @staticmethod - def format_float(state: str) -> int | str: + def format_float(state: str) -> float | str: """Return the given state value as float rounded to three decimal places.""" try: return round(float(state), 3) @@ -377,7 +383,7 @@ class PlenticoreDataFormatter: return state @staticmethod - def format_inverter_state(state: str) -> str: + def format_inverter_state(state: str) -> str | None: """Return a readable string of the inverter state.""" try: value = int(state) @@ -387,7 +393,7 @@ class PlenticoreDataFormatter: return PlenticoreDataFormatter.INVERTER_STATES.get(value) @staticmethod - def format_em_manager_state(state: str) -> str: + def format_em_manager_state(state: str) -> str | None: """Return a readable state of the energy manager.""" try: value = int(state) diff --git a/homeassistant/components/kostal_plenticore/sensor.py b/homeassistant/components/kostal_plenticore/sensor.py index 5f8fb47e85a..f66264e1d7a 100644 --- a/homeassistant/components/kostal_plenticore/sensor.py +++ b/homeassistant/components/kostal_plenticore/sensor.py @@ -36,7 +36,18 @@ async def async_setup_entry( timedelta(seconds=10), plenticore, ) - for module_id, data_id, name, sensor_data, fmt in SENSOR_PROCESS_DATA: + module_id: str + data_id: str + name: str + sensor_data: dict[str, Any] + fmt: str + for ( # type: ignore[assignment] + module_id, + data_id, + name, + sensor_data, + fmt, + ) in SENSOR_PROCESS_DATA: if ( module_id not in available_process_data or data_id not in available_process_data[module_id] @@ -78,7 +89,7 @@ class PlenticoreDataSensor(CoordinatorEntity, SensorEntity): sensor_data: dict[str, Any], formatter: Callable[[str], Any], device_info: DeviceInfo, - entity_category: EntityCategory, + entity_category: EntityCategory | None, ): """Create a new Sensor Entity for Plenticore process data.""" super().__init__(coordinator) diff --git a/mypy.ini b/mypy.ini index 06dec7d4897..e6f82ba957e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2696,18 +2696,6 @@ ignore_errors = true [mypy-homeassistant.components.konnected.config_flow] ignore_errors = true -[mypy-homeassistant.components.kostal_plenticore.helper] -ignore_errors = true - -[mypy-homeassistant.components.kostal_plenticore.select] -ignore_errors = true - -[mypy-homeassistant.components.kostal_plenticore.sensor] -ignore_errors = true - -[mypy-homeassistant.components.kostal_plenticore.switch] -ignore_errors = true - [mypy-homeassistant.components.lovelace] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 4ca5b3509d4..f86fd447722 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -33,10 +33,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.izone.climate", "homeassistant.components.konnected", "homeassistant.components.konnected.config_flow", - "homeassistant.components.kostal_plenticore.helper", - "homeassistant.components.kostal_plenticore.select", - "homeassistant.components.kostal_plenticore.sensor", - "homeassistant.components.kostal_plenticore.switch", "homeassistant.components.lovelace", "homeassistant.components.lovelace.dashboard", "homeassistant.components.lovelace.resources", From bb974ebf7e04b7c369793fbf3744131cddb00088 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 23:21:58 +0200 Subject: [PATCH 2352/3516] Use instance attributes in gdacs (#74400) --- .../components/gdacs/geo_location.py | 103 ++++++------------ 1 file changed, 35 insertions(+), 68 deletions(-) diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py index 5bfbd915b6c..715ac779668 100644 --- a/homeassistant/components/gdacs/geo_location.py +++ b/homeassistant/components/gdacs/geo_location.py @@ -1,12 +1,16 @@ """Geolocation support for GDACS Feed.""" from __future__ import annotations +from collections.abc import Callable import logging +from typing import Any + +from aio_georss_gdacs import GdacsFeedManager +from aio_georss_gdacs.feed_entry import GdacsFeedEntry from homeassistant.components.geo_location import GeolocationEvent from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, LENGTH_MILES, @@ -17,6 +21,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from . import GdacsFeedEntityManager from .const import DEFAULT_ICON, DOMAIN, FEED _LOGGER = logging.getLogger(__name__) @@ -52,10 +57,12 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the GDACS Feed platform.""" - manager = hass.data[DOMAIN][FEED][entry.entry_id] + manager: GdacsFeedEntityManager = hass.data[DOMAIN][FEED][entry.entry_id] @callback - def async_add_geolocation(feed_manager, integration_id, external_id): + def async_add_geolocation( + feed_manager: GdacsFeedManager, integration_id: str, external_id: str + ) -> None: """Add geolocation entity from feed.""" new_entity = GdacsEvent(feed_manager, integration_id, external_id) _LOGGER.debug("Adding geolocation %s", new_entity) @@ -75,16 +82,17 @@ async def async_setup_entry( class GdacsEvent(GeolocationEvent): """This represents an external event with GDACS feed data.""" - def __init__(self, feed_manager, integration_id, external_id): + _attr_should_poll = False + _attr_source = SOURCE + + def __init__( + self, feed_manager: GdacsFeedManager, integration_id: str, external_id: str + ) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager - self._integration_id = integration_id self._external_id = external_id - self._title = None - self._distance = None - self._latitude = None - self._longitude = None - self._attribution = None + self._attr_unique_id = f"{integration_id}_{external_id}" + self._attr_unit_of_measurement = LENGTH_KILOMETERS self._alert_level = None self._country = None self._description = None @@ -97,11 +105,13 @@ class GdacsEvent(GeolocationEvent): self._severity = None self._vulnerability = None self._version = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" + if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + self._attr_unit_of_measurement = LENGTH_MILES self._remove_signal_delete = async_dispatcher_connect( self.hass, f"gdacs_delete_{self._external_id}", self._delete_callback ) @@ -119,43 +129,38 @@ class GdacsEvent(GeolocationEvent): entity_registry.async_remove(self.entity_id) @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for GDACS feed location events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed(self, feed_entry: GdacsFeedEntry) -> None: """Update the internal state from the provided feed entry.""" 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}" + self._attr_name = f"{feed_entry.event_type}: {event_name}" # Convert distance if not metric system. if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - self._distance = IMPERIAL_SYSTEM.length( + self._attr_distance = IMPERIAL_SYSTEM.length( feed_entry.distance_to_home, LENGTH_KILOMETERS ) else: - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] - self._attribution = feed_entry.attribution + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] + self._attr_attribution = feed_entry.attribution self._alert_level = feed_entry.alert_level self._country = feed_entry.country self._description = feed_entry.title @@ -173,57 +178,19 @@ class GdacsEvent(GeolocationEvent): self._version = feed_entry.version @property - def unique_id(self) -> str | None: - """Return a unique ID containing latitude/longitude and external id.""" - return f"{self._integration_id}_{self._external_id}" - - @property - def icon(self): + def icon(self) -> str: """Return the icon to use in the frontend, if any.""" if self._event_type_short and self._event_type_short in ICONS: return ICONS[self._event_type_short] return DEFAULT_ICON @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._title - - @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: - return LENGTH_MILES - return LENGTH_KILOMETERS - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = {} for key, value in ( (ATTR_EXTERNAL_ID, self._external_id), (ATTR_DESCRIPTION, self._description), - (ATTR_ATTRIBUTION, self._attribution), (ATTR_EVENT_TYPE, self._event_type), (ATTR_ALERT_LEVEL, self._alert_level), (ATTR_COUNTRY, self._country), From e529ef7b0e93c769772a971b9c52456c4e6c80f9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 23:29:00 +0200 Subject: [PATCH 2353/3516] Use instance attributes in qld_bushfire (#74402) --- .../components/qld_bushfire/geo_location.py | 123 +++++++----------- 1 file changed, 47 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/qld_bushfire/geo_location.py b/homeassistant/components/qld_bushfire/geo_location.py index 6d8f2b83bc3..e754de466d1 100644 --- a/homeassistant/components/qld_bushfire/geo_location.py +++ b/homeassistant/components/qld_bushfire/geo_location.py @@ -1,15 +1,19 @@ """Support for Queensland Bushfire Alert Feeds.""" from __future__ import annotations +from collections.abc import Callable from datetime import timedelta import logging +from typing import Any -from georss_qld_bushfire_alert_client import QldBushfireAlertFeedManager +from georss_qld_bushfire_alert_client import ( + QldBushfireAlertFeedEntry, + QldBushfireAlertFeedManager, +) import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, @@ -17,7 +21,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_START, LENGTH_KILOMETERS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -70,19 +74,19 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Queensland Bushfire Alert Feed platform.""" - scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - coordinates = ( + scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + coordinates: tuple[float, float] = ( config.get(CONF_LATITUDE, hass.config.latitude), config.get(CONF_LONGITUDE, hass.config.longitude), ) - radius_in_km = config[CONF_RADIUS] - categories = config[CONF_CATEGORIES] + radius_in_km: float = config[CONF_RADIUS] + categories: list[str] = config[CONF_CATEGORIES] # Initialize the entity manager. feed = QldBushfireFeedEntityManager( hass, add_entities, scan_interval, coordinates, radius_in_km, categories ) - def start_feed_manager(event): + def start_feed_manager(event: Event) -> None: """Start feed manager.""" feed.startup() @@ -93,8 +97,14 @@ class QldBushfireFeedEntityManager: """Feed Entity Manager for Qld Bushfire Alert GeoRSS feed.""" def __init__( - self, hass, add_entities, scan_interval, coordinates, radius_in_km, categories - ): + self, + hass: HomeAssistant, + add_entities: AddEntitiesCallback, + scan_interval: timedelta, + coordinates: tuple[float, float], + radius_in_km: float, + categories: list[str], + ) -> None: """Initialize the Feed Entity Manager.""" self._hass = hass self._feed_manager = QldBushfireAlertFeedManager( @@ -108,32 +118,32 @@ class QldBushfireFeedEntityManager: self._add_entities = add_entities self._scan_interval = scan_interval - def startup(self): + def startup(self) -> None: """Start up this manager.""" self._feed_manager.update() self._init_regular_updates() - def _init_regular_updates(self): + def _init_regular_updates(self) -> None: """Schedule regular updates at the specified interval.""" track_time_interval( self._hass, lambda now: self._feed_manager.update(), self._scan_interval ) - def get_entry(self, external_id): + def get_entry(self, external_id: str) -> QldBushfireAlertFeedEntry | None: """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - def _generate_entity(self, external_id): + def _generate_entity(self, external_id: str) -> None: """Generate new entity.""" new_entity = QldBushfireLocationEvent(self, external_id) # Add new entities to HA. self._add_entities([new_entity], True) - def _update_entity(self, external_id): + def _update_entity(self, external_id: str) -> None: """Update entity.""" dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) - def _remove_entity(self, external_id): + def _remove_entity(self, external_id: str) -> None: """Remove entity.""" dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) @@ -141,23 +151,25 @@ class QldBushfireFeedEntityManager: class QldBushfireLocationEvent(GeolocationEvent): """This represents an external event with Qld Bushfire feed data.""" - def __init__(self, feed_manager, external_id): + _attr_icon = "mdi:fire" + _attr_should_poll = False + _attr_source = SOURCE + _attr_unit_of_measurement = LENGTH_KILOMETERS + + def __init__( + self, feed_manager: QldBushfireFeedEntityManager, external_id: str + ) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager self._external_id = external_id - self._name = None - self._distance = None - self._latitude = None - self._longitude = None - self._attribution = None self._category = None self._publication_date = None self._updated_date = None self._status = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self._remove_signal_delete = async_dispatcher_connect( self.hass, @@ -171,84 +183,43 @@ class QldBushfireLocationEvent(GeolocationEvent): ) @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self._remove_signal_delete() self._remove_signal_update() self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for Qld Bushfire Alert feed location events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed(self, feed_entry: QldBushfireAlertFeedEntry) -> None: """Update the internal state from the provided feed entry.""" - self._name = feed_entry.title - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] - self._attribution = feed_entry.attribution + self._attr_name = feed_entry.title + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] + self._attr_attribution = feed_entry.attribution self._category = feed_entry.category self._publication_date = feed_entry.published self._updated_date = feed_entry.updated self._status = feed_entry.status @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:fire" - - @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._name - - @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return LENGTH_KILOMETERS - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = {} for key, value in ( (ATTR_EXTERNAL_ID, self._external_id), (ATTR_CATEGORY, self._category), - (ATTR_ATTRIBUTION, self._attribution), (ATTR_PUBLICATION_DATE, self._publication_date), (ATTR_UPDATED_DATE, self._updated_date), (ATTR_STATUS, self._status), From d33779d3a05acbc97b7d94a347d3323303dbf828 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Sun, 10 Jul 2022 00:33:10 +0300 Subject: [PATCH 2354/3516] Cleanup mikrotik device extra_attributes (#74491) --- homeassistant/components/mikrotik/const.py | 1 - .../components/mikrotik/device_tracker.py | 21 +++---------------- homeassistant/components/mikrotik/hub.py | 5 ++--- 3 files changed, 5 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/mikrotik/const.py b/homeassistant/components/mikrotik/const.py index 2942e6981fa..bbe129c4a00 100644 --- a/homeassistant/components/mikrotik/const.py +++ b/homeassistant/components/mikrotik/const.py @@ -44,7 +44,6 @@ PLATFORMS: Final = [Platform.DEVICE_TRACKER] ATTR_DEVICE_TRACKER: Final = [ "comment", - "mac-address", "ssid", "interface", "signal-strength", diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 4e002cdbcfe..158d95dd683 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -18,10 +18,6 @@ import homeassistant.util.dt as dt_util from .const import DOMAIN from .hub import Device, MikrotikDataUpdateCoordinator -# These are normalized to ATTR_IP and ATTR_MAC to conform -# to device_tracker -FILTER_ATTRS = ("ip_address", "mac_address") - async def async_setup_entry( hass: HomeAssistant, @@ -90,6 +86,8 @@ class MikrotikDataUpdateCoordinatorTracker( """Initialize the tracked device.""" super().__init__(coordinator) self.device = device + self._attr_name = str(device.name) + self._attr_unique_id = device.mac @property def is_connected(self) -> bool: @@ -107,12 +105,6 @@ class MikrotikDataUpdateCoordinatorTracker( """Return the source type of the client.""" return SOURCE_TYPE_ROUTER - @property - def name(self) -> str: - """Return the name of the client.""" - # Stringify to ensure we return a string - return str(self.device.name) - @property def hostname(self) -> str: """Return the hostname of the client.""" @@ -128,14 +120,7 @@ class MikrotikDataUpdateCoordinatorTracker( """Return the mac address of the client.""" return self.device.ip_address - @property - def unique_id(self) -> str: - """Return a unique identifier for this device.""" - return self.device.mac - @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return the device state attributes.""" - if self.is_connected: - return {k: v for k, v in self.device.attrs.items() if k not in FILTER_ATTRS} - return None + return self.device.attrs if self.is_connected else None diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 9219159ca74..66fe7226d9b 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -77,11 +77,10 @@ class Device: @property def attrs(self) -> dict[str, Any]: """Return device attributes.""" - attr_data = self._wireless_params if self._wireless_params else self._params + attr_data = self._wireless_params | self._params for attr in ATTR_DEVICE_TRACKER: if attr in attr_data: self._attrs[slugify(attr)] = attr_data[attr] - self._attrs["ip_address"] = self._params.get("active-address") return self._attrs def update( @@ -250,7 +249,7 @@ class MikrotikData: ) -> list[dict[str, Any]]: """Retrieve data from Mikrotik API.""" try: - _LOGGER.info("Running command %s", cmd) + _LOGGER.debug("Running command %s", cmd) if params: return list(self.api(cmd=cmd, **params)) return list(self.api(cmd=cmd)) From e3242d8d16b4f1d20d4b707d8979b538b353b0e2 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 9 Jul 2022 23:35:06 +0200 Subject: [PATCH 2355/3516] Move add/remove logic of deCONZ clip sensors to gateway class (#74481) --- .../components/deconz/binary_sensor.py | 19 ----- homeassistant/components/deconz/climate.py | 18 ----- .../components/deconz/deconz_event.py | 3 - homeassistant/components/deconz/gateway.py | 77 ++++++++++++++----- homeassistant/components/deconz/lock.py | 1 + homeassistant/components/deconz/number.py | 4 +- homeassistant/components/deconz/sensor.py | 19 ----- 7 files changed, 60 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 0d090751edd..814dec443e0 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -24,7 +24,6 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.helpers.entity_registry as er @@ -215,9 +214,6 @@ async def async_setup_entry( """Add sensor from deCONZ.""" sensor = gateway.api.sensors[sensor_id] - if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): - return - for description in ( ENTITY_DESCRIPTIONS.get(type(sensor), []) + BINARY_SENSOR_DESCRIPTIONS ): @@ -236,21 +232,6 @@ async def async_setup_entry( gateway.api.sensors, ) - @callback - def async_reload_clip_sensors() -> None: - """Load clip sensor sensors from deCONZ.""" - for sensor_id, sensor in gateway.api.sensors.items(): - if sensor.type.startswith("CLIP"): - async_add_sensor(EventType.ADDED, sensor_id) - - config_entry.async_on_unload( - async_dispatcher_connect( - hass, - gateway.signal_reload_clip_sensors, - async_reload_clip_sensors, - ) - ) - class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): """Representation of a deCONZ binary sensor.""" diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 880e11f080b..75b37db2c13 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -28,7 +28,6 @@ from homeassistant.components.climate.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_LOCKED, ATTR_OFFSET, ATTR_VALVE @@ -85,8 +84,6 @@ async def async_setup_entry( def async_add_climate(_: EventType, climate_id: str) -> None: """Add climate from deCONZ.""" climate = gateway.api.sensors.thermostat[climate_id] - if not gateway.option_allow_clip_sensor and climate.type.startswith("CLIP"): - return async_add_entities([DeconzThermostat(climate, gateway)]) gateway.register_platform_add_device_callback( @@ -94,21 +91,6 @@ async def async_setup_entry( gateway.api.sensors.thermostat, ) - @callback - def async_reload_clip_sensors() -> None: - """Load clip sensors from deCONZ.""" - for climate_id, climate in gateway.api.sensors.thermostat.items(): - if climate.type.startswith("CLIP"): - async_add_climate(EventType.ADDED, climate_id) - - config_entry.async_on_unload( - async_dispatcher_connect( - hass, - gateway.signal_reload_clip_sensors, - async_reload_clip_sensors, - ) - ) - class DeconzThermostat(DeconzDevice, ClimateEntity): """Representation of a deCONZ thermostat.""" diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 270e66bf91d..6f7b6f9038a 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -46,9 +46,6 @@ async def async_setup_events(gateway: DeconzGateway) -> None: new_event: DeconzAlarmEvent | DeconzEvent sensor = gateway.api.sensors[sensor_id] - if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): - return None - if isinstance(sensor, Switch): new_event = DeconzEvent(sensor, gateway) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 25471d1448a..0ee967f88d9 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any, cast import async_timeout from pydeconz import DeconzSession, errors +from pydeconz.interfaces import sensors from pydeconz.interfaces.api import APIItems, GroupedAPIItems from pydeconz.interfaces.groups import Groups from pydeconz.models.event import EventType @@ -42,6 +43,35 @@ from .errors import AuthenticationRequired, CannotConnect if TYPE_CHECKING: from .deconz_event import DeconzAlarmEvent, DeconzEvent +SENSORS = ( + sensors.SensorResourceManager, + sensors.AirPurifierHandler, + sensors.AirQualityHandler, + sensors.AlarmHandler, + sensors.AncillaryControlHandler, + sensors.BatteryHandler, + sensors.CarbonMonoxideHandler, + sensors.ConsumptionHandler, + sensors.DaylightHandler, + sensors.DoorLockHandler, + sensors.FireHandler, + sensors.GenericFlagHandler, + sensors.GenericStatusHandler, + sensors.HumidityHandler, + sensors.LightLevelHandler, + sensors.OpenCloseHandler, + sensors.PowerHandler, + sensors.PresenceHandler, + sensors.PressureHandler, + sensors.RelativeRotaryHandler, + sensors.SwitchHandler, + sensors.TemperatureHandler, + sensors.ThermostatHandler, + sensors.TimeHandler, + sensors.VibrationHandler, + sensors.WaterHandler, +) + class DeconzGateway: """Manages a single deCONZ gateway.""" @@ -60,14 +90,17 @@ class DeconzGateway: self.ignore_state_updates = False self.signal_reachable = f"deconz-reachable-{config_entry.entry_id}" - self.signal_reload_clip_sensors = f"deconz_reload_clip_{config_entry.entry_id}" self.deconz_ids: dict[str, str] = {} self.entities: dict[str, set[str]] = {} self.events: list[DeconzAlarmEvent | DeconzEvent] = [] - self.ignored_devices: set[tuple[Callable[[EventType, str], None], str]] = set() + self.clip_sensors: set[tuple[Callable[[EventType, str], None], str]] = set() self.deconz_groups: set[tuple[Callable[[EventType, str], None], str]] = set() + self.ignored_devices: set[tuple[Callable[[EventType, str], None], str]] = set() + self.option_allow_clip_sensor = self.config_entry.options.get( + CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR + ) self.option_allow_deconz_groups = config_entry.options.get( CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS ) @@ -90,20 +123,12 @@ class DeconzGateway: """Gateway which is used with deCONZ services without defining id.""" return cast(bool, self.config_entry.options[CONF_MASTER_GATEWAY]) - # Options - - @property - def option_allow_clip_sensor(self) -> bool: - """Allow loading clip sensor from gateway.""" - return self.config_entry.options.get( - CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR - ) - @callback def register_platform_add_device_callback( self, add_device_callback: Callable[[EventType, str], None], deconz_device_interface: APIItems | GroupedAPIItems, + always_ignore_clip_sensors: bool = False, ) -> None: """Wrap add_device_callback to check allow_new_devices option.""" @@ -128,6 +153,13 @@ class DeconzGateway: if not self.option_allow_deconz_groups: return + if isinstance(deconz_device_interface, SENSORS): + device = deconz_device_interface[device_id] + if device.type.startswith("CLIP") and not always_ignore_clip_sensors: + self.clip_sensors.add((async_add_device, device_id)) + if not self.option_allow_clip_sensor: + return + add_device_callback(EventType.ADDED, device_id) self.config_entry.async_on_unload( @@ -212,15 +244,20 @@ class DeconzGateway: # Allow CLIP sensors - if self.option_allow_clip_sensor: - async_dispatcher_send(self.hass, self.signal_reload_clip_sensors) - - else: - deconz_ids += [ - sensor.deconz_id - for sensor in self.api.sensors.values() - if sensor.type.startswith("CLIP") - ] + option_allow_clip_sensor = self.config_entry.options.get( + CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR + ) + if option_allow_clip_sensor != self.option_allow_clip_sensor: + self.option_allow_clip_sensor = option_allow_clip_sensor + if option_allow_clip_sensor: + for add_device, device_id in self.clip_sensors: + add_device(EventType.ADDED, device_id) + else: + deconz_ids += [ + sensor.deconz_id + for sensor in self.api.sensors.values() + if sensor.type.startswith("CLIP") + ] # Allow Groups diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index cf4bd7f14f5..d6f9d670c01 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -46,6 +46,7 @@ async def async_setup_entry( gateway.register_platform_add_device_callback( async_add_lock_from_sensor, gateway.api.sensors.door_lock, + always_ignore_clip_sensors=True, ) diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index 81f3e434007..acd890d9c90 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -65,8 +65,7 @@ async def async_setup_entry( def async_add_sensor(_: EventType, sensor_id: str) -> None: """Add sensor from deCONZ.""" sensor = gateway.api.sensors.presence[sensor_id] - if sensor.type.startswith("CLIP"): - return + for description in ENTITY_DESCRIPTIONS.get(type(sensor), []): if ( not hasattr(sensor, description.key) @@ -78,6 +77,7 @@ async def async_setup_entry( gateway.register_platform_add_device_callback( async_add_sensor, gateway.api.sensors.presence, + always_ignore_clip_sensors=True, ) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 2aff2b12448..15af1b3dd8f 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -39,7 +39,6 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -248,9 +247,6 @@ async def async_setup_entry( sensor = gateway.api.sensors[sensor_id] entities: list[DeconzSensor] = [] - if not gateway.option_allow_clip_sensor and sensor.type.startswith("CLIP"): - return - if sensor.battery is None and not sensor.type.startswith("CLIP"): DeconzBatteryTracker(sensor_id, gateway, async_add_entities) @@ -276,21 +272,6 @@ async def async_setup_entry( gateway.api.sensors, ) - @callback - def async_reload_clip_sensors() -> None: - """Load clip sensor sensors from deCONZ.""" - for sensor_id, sensor in gateway.api.sensors.items(): - if sensor.type.startswith("CLIP"): - async_add_sensor(EventType.ADDED, sensor_id) - - config_entry.async_on_unload( - async_dispatcher_connect( - hass, - gateway.signal_reload_clip_sensors, - async_reload_clip_sensors, - ) - ) - class DeconzSensor(DeconzDevice, SensorEntity): """Representation of a deCONZ sensor.""" From db111645c2270d7a595ad14c6e280538ac72afa3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 9 Jul 2022 23:38:53 +0200 Subject: [PATCH 2356/3516] Use instance attributes in nsw_rural_fire_service_feed (#74398) --- .../geo_location.py | 135 +++++++----------- 1 file changed, 54 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py index fe5d27feee2..553f73a128e 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py +++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py @@ -1,15 +1,19 @@ """Support for NSW Rural Fire Service Feeds.""" from __future__ import annotations -from datetime import timedelta +from collections.abc import Callable +from datetime import datetime, timedelta import logging +from typing import Any from aio_geojson_nsw_rfs_incidents import NswRuralFireServiceIncidentsFeedManager +from aio_geojson_nsw_rfs_incidents.feed_entry import ( + NswRuralFireServiceIncidentsFeedEntry, +) import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_LOCATION, CONF_LATITUDE, CONF_LONGITUDE, @@ -19,7 +23,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, LENGTH_KILOMETERS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -73,23 +77,23 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the NSW Rural Fire Service Feed platform.""" - scan_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - coordinates = ( + scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + coordinates: tuple[float, float] = ( config.get(CONF_LATITUDE, hass.config.latitude), config.get(CONF_LONGITUDE, hass.config.longitude), ) - radius_in_km = config[CONF_RADIUS] - categories = config.get(CONF_CATEGORIES) + radius_in_km: float = config[CONF_RADIUS] + categories: list[str] = config[CONF_CATEGORIES] # Initialize the entity manager. manager = NswRuralFireServiceFeedEntityManager( hass, async_add_entities, scan_interval, coordinates, radius_in_km, categories ) - async def start_feed_manager(event): + async def start_feed_manager(event: Event) -> None: """Start feed manager.""" await manager.async_init() - async def stop_feed_manager(event): + async def stop_feed_manager(event: Event) -> None: """Stop feed manager.""" await manager.async_stop() @@ -103,13 +107,13 @@ class NswRuralFireServiceFeedEntityManager: def __init__( self, - hass, - async_add_entities, - scan_interval, - coordinates, - radius_in_km, - categories, - ): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + scan_interval: timedelta, + coordinates: tuple[float, float], + radius_in_km: float, + categories: list[str], + ) -> None: """Initialize the Feed Entity Manager.""" self._hass = hass websession = aiohttp_client.async_get_clientsession(hass) @@ -124,12 +128,12 @@ class NswRuralFireServiceFeedEntityManager: ) self._async_add_entities = async_add_entities self._scan_interval = scan_interval - self._track_time_remove_callback = None + self._track_time_remove_callback: Callable[[], None] | None = None - async def async_init(self): + async def async_init(self) -> None: """Schedule initial and regular updates based on configured time interval.""" - async def update(event_time): + async def update(event_time: datetime) -> None: """Update.""" await self.async_update() @@ -140,32 +144,34 @@ class NswRuralFireServiceFeedEntityManager: _LOGGER.debug("Feed entity manager initialized") - async def async_update(self): + async def async_update(self) -> None: """Refresh data.""" await self._feed_manager.update() _LOGGER.debug("Feed entity manager updated") - async def async_stop(self): + async def async_stop(self) -> None: """Stop this feed entity manager from refreshing.""" if self._track_time_remove_callback: self._track_time_remove_callback() _LOGGER.debug("Feed entity manager stopped") - def get_entry(self, external_id): + def get_entry( + self, external_id: str + ) -> NswRuralFireServiceIncidentsFeedEntry | None: """Get feed entry by external id.""" return self._feed_manager.feed_entries.get(external_id) - async def _generate_entity(self, external_id): + async def _generate_entity(self, external_id: str) -> None: """Generate new entity.""" new_entity = NswRuralFireServiceLocationEvent(self, external_id) # Add new entities to HA. self._async_add_entities([new_entity], True) - async def _update_entity(self, external_id): + async def _update_entity(self, external_id: str) -> None: """Update entity.""" async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) - async def _remove_entity(self, external_id): + async def _remove_entity(self, external_id: str) -> None: """Remove entity.""" async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) @@ -173,15 +179,16 @@ class NswRuralFireServiceFeedEntityManager: class NswRuralFireServiceLocationEvent(GeolocationEvent): """This represents an external event with NSW Rural Fire Service data.""" - def __init__(self, feed_manager, external_id): + _attr_should_poll = False + _attr_source = SOURCE + _attr_unit_of_measurement = LENGTH_KILOMETERS + + def __init__( + self, feed_manager: NswRuralFireServiceFeedEntityManager, external_id: str + ) -> None: """Initialize entity with data from feed entry.""" self._feed_manager = feed_manager self._external_id = external_id - self._name = None - self._distance = None - self._latitude = None - self._longitude = None - self._attribution = None self._category = None self._publication_date = None self._location = None @@ -191,10 +198,10 @@ class NswRuralFireServiceLocationEvent(GeolocationEvent): self._fire = None self._size = None self._responsible_agency = None - self._remove_signal_delete = None - self._remove_signal_update = None + self._remove_signal_delete: Callable[[], None] + self._remove_signal_update: Callable[[], None] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" self._remove_signal_delete = async_dispatcher_connect( self.hass, @@ -213,34 +220,31 @@ class NswRuralFireServiceLocationEvent(GeolocationEvent): self._remove_signal_update() @callback - def _delete_callback(self): + def _delete_callback(self) -> None: """Remove this entity.""" self.hass.async_create_task(self.async_remove(force_remove=True)) @callback - def _update_callback(self): + def _update_callback(self) -> None: """Call update method.""" self.async_schedule_update_ha_state(True) - @property - def should_poll(self): - """No polling needed for NSW Rural Fire Service location events.""" - return False - - async def async_update(self): + async def async_update(self) -> None: """Update this entity from the data held in the feed manager.""" _LOGGER.debug("Updating %s", self._external_id) feed_entry = self._feed_manager.get_entry(self._external_id) if feed_entry: self._update_from_feed(feed_entry) - def _update_from_feed(self, feed_entry): + def _update_from_feed( + self, feed_entry: NswRuralFireServiceIncidentsFeedEntry + ) -> None: """Update the internal state from the provided feed entry.""" - self._name = feed_entry.title - self._distance = feed_entry.distance_to_home - self._latitude = feed_entry.coordinates[0] - self._longitude = feed_entry.coordinates[1] - self._attribution = feed_entry.attribution + self._attr_name = feed_entry.title + self._attr_distance = feed_entry.distance_to_home + self._attr_latitude = feed_entry.coordinates[0] + self._attr_longitude = feed_entry.coordinates[1] + self._attr_attribution = feed_entry.attribution self._category = feed_entry.category self._publication_date = feed_entry.publication_date self._location = feed_entry.location @@ -252,51 +256,20 @@ class NswRuralFireServiceLocationEvent(GeolocationEvent): self._responsible_agency = feed_entry.responsible_agency @property - def icon(self): + def icon(self) -> str: """Return the icon to use in the frontend.""" if self._fire: return "mdi:fire" return "mdi:alarm-light" @property - def source(self) -> str: - """Return source value of this external event.""" - return SOURCE - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._name - - @property - def distance(self) -> float | None: - """Return distance value of this external event.""" - return self._distance - - @property - def latitude(self) -> float | None: - """Return latitude value of this external event.""" - return self._latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of this external event.""" - return self._longitude - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return LENGTH_KILOMETERS - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device state attributes.""" attributes = {} for key, value in ( (ATTR_EXTERNAL_ID, self._external_id), (ATTR_CATEGORY, self._category), (ATTR_LOCATION, self._location), - (ATTR_ATTRIBUTION, self._attribution), (ATTR_PUBLICATION_DATE, self._publication_date), (ATTR_COUNCIL_AREA, self._council_area), (ATTR_STATUS, self._status), From 63582c3f98303481fd0cb14b54ba20126d0adf0c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 9 Jul 2022 23:40:15 +0200 Subject: [PATCH 2357/3516] Bump deCONZ dependency to fix #74791 (#74804) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 06f8b6c0376..2ae400bbe19 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==97"], + "requirements": ["pydeconz==98"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 6f78c0206c8..1842c31497f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1447,7 +1447,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==97 +pydeconz==98 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a2ad4fcb05..269ce43c0a8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -980,7 +980,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==97 +pydeconz==98 # homeassistant.components.dexcom pydexcom==0.2.3 From 81e7eb623bfae512b4d71acb58b551533daf3320 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 9 Jul 2022 23:40:55 +0200 Subject: [PATCH 2358/3516] Migrate Twente Milieu to new entity naming style (#74593) --- .../components/twentemilieu/calendar.py | 2 +- .../components/twentemilieu/const.py | 10 ++--- .../components/twentemilieu/entity.py | 2 + .../components/twentemilieu/test_calendar.py | 4 +- tests/components/twentemilieu/test_sensor.py | 45 ++++++++++++------- 5 files changed, 40 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/twentemilieu/calendar.py b/homeassistant/components/twentemilieu/calendar.py index 1c7cfcbd4fc..8bcf1b1d390 100644 --- a/homeassistant/components/twentemilieu/calendar.py +++ b/homeassistant/components/twentemilieu/calendar.py @@ -28,7 +28,7 @@ async def async_setup_entry( class TwenteMilieuCalendar(TwenteMilieuEntity, CalendarEntity): """Defines a Twente Milieu calendar.""" - _attr_name = "Twente Milieu" + _attr_has_entity_name = True _attr_icon = "mdi:delete-empty" def __init__( diff --git a/homeassistant/components/twentemilieu/const.py b/homeassistant/components/twentemilieu/const.py index c9f2f935772..ac7354a42f2 100644 --- a/homeassistant/components/twentemilieu/const.py +++ b/homeassistant/components/twentemilieu/const.py @@ -15,9 +15,9 @@ CONF_HOUSE_NUMBER = "house_number" CONF_HOUSE_LETTER = "house_letter" WASTE_TYPE_TO_DESCRIPTION = { - WasteType.NON_RECYCLABLE: "Non-recyclable Waste Pickup", - WasteType.ORGANIC: "Organic Waste Pickup", - WasteType.PACKAGES: "Packages Waste Pickup", - WasteType.PAPER: "Paper Waste Pickup", - WasteType.TREE: "Christmas Tree Pickup", + WasteType.NON_RECYCLABLE: "Non-recyclable waste pickup", + WasteType.ORGANIC: "Organic waste pickup", + WasteType.PACKAGES: "Packages waste pickup", + WasteType.PAPER: "Paper waste pickup", + WasteType.TREE: "Christmas tree pickup", } diff --git a/homeassistant/components/twentemilieu/entity.py b/homeassistant/components/twentemilieu/entity.py index 008c0fa441e..5a1a1758d3e 100644 --- a/homeassistant/components/twentemilieu/entity.py +++ b/homeassistant/components/twentemilieu/entity.py @@ -22,6 +22,8 @@ class TwenteMilieuEntity( ): """Defines a Twente Milieu entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator[dict[WasteType, list[date]]], diff --git a/tests/components/twentemilieu/test_calendar.py b/tests/components/twentemilieu/test_calendar.py index b9f1dd9247d..11f8a1abd75 100644 --- a/tests/components/twentemilieu/test_calendar.py +++ b/tests/components/twentemilieu/test_calendar.py @@ -27,7 +27,7 @@ async def test_waste_pickup_calendar( assert entry.unique_id == "12345" assert state.attributes[ATTR_ICON] == "mdi:delete-empty" assert state.attributes["all_day"] is True - assert state.attributes["message"] == "Christmas Tree Pickup" + assert state.attributes["message"] == "Christmas tree pickup" assert not state.attributes["location"] assert not state.attributes["description"] assert state.state == STATE_OFF @@ -78,7 +78,7 @@ async def test_api_events( assert events[0] == { "start": {"date": "2022-01-06"}, "end": {"date": "2022-01-06"}, - "summary": "Christmas Tree Pickup", + "summary": "Christmas tree pickup", "description": None, "location": None, } diff --git a/tests/components/twentemilieu/test_sensor.py b/tests/components/twentemilieu/test_sensor.py index 5f09018358e..6e20fd4d141 100644 --- a/tests/components/twentemilieu/test_sensor.py +++ b/tests/components/twentemilieu/test_sensor.py @@ -22,57 +22,72 @@ async def test_waste_pickup_sensors( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - state = hass.states.get("sensor.christmas_tree_pickup") - entry = entity_registry.async_get("sensor.christmas_tree_pickup") + state = hass.states.get("sensor.twente_milieu_christmas_tree_pickup") + entry = entity_registry.async_get("sensor.twente_milieu_christmas_tree_pickup") assert entry assert state assert entry.unique_id == "twentemilieu_12345_tree" assert state.state == "2022-01-06" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Christmas Tree Pickup" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Twente Milieu Christmas tree pickup" + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert state.attributes.get(ATTR_ICON) == "mdi:pine-tree" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - state = hass.states.get("sensor.non_recyclable_waste_pickup") - entry = entity_registry.async_get("sensor.non_recyclable_waste_pickup") + state = hass.states.get("sensor.twente_milieu_non_recyclable_waste_pickup") + entry = entity_registry.async_get( + "sensor.twente_milieu_non_recyclable_waste_pickup" + ) assert entry assert state assert entry.unique_id == "twentemilieu_12345_Non-recyclable" assert state.state == "2021-11-01" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Non-recyclable Waste Pickup" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Twente Milieu Non-recyclable waste pickup" + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - state = hass.states.get("sensor.organic_waste_pickup") - entry = entity_registry.async_get("sensor.organic_waste_pickup") + state = hass.states.get("sensor.twente_milieu_organic_waste_pickup") + entry = entity_registry.async_get("sensor.twente_milieu_organic_waste_pickup") assert entry assert state assert entry.unique_id == "twentemilieu_12345_Organic" assert state.state == "2021-11-02" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Organic Waste Pickup" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Twente Milieu Organic waste pickup" + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - state = hass.states.get("sensor.packages_waste_pickup") - entry = entity_registry.async_get("sensor.packages_waste_pickup") + state = hass.states.get("sensor.twente_milieu_packages_waste_pickup") + entry = entity_registry.async_get("sensor.twente_milieu_packages_waste_pickup") assert entry assert state assert entry.unique_id == "twentemilieu_12345_Plastic" assert state.state == "2021-11-03" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Packages Waste Pickup" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Twente Milieu Packages waste pickup" + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - state = hass.states.get("sensor.paper_waste_pickup") - entry = entity_registry.async_get("sensor.paper_waste_pickup") + state = hass.states.get("sensor.twente_milieu_paper_waste_pickup") + entry = entity_registry.async_get("sensor.twente_milieu_paper_waste_pickup") assert entry assert state assert entry.unique_id == "twentemilieu_12345_Paper" assert state.state == STATE_UNKNOWN - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Paper Waste Pickup" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Twente Milieu Paper waste pickup" + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert state.attributes.get(ATTR_ICON) == "mdi:delete-empty" assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes From 0f813b61c376046a9169eb47e5fc3676246992c6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 9 Jul 2022 23:41:04 +0200 Subject: [PATCH 2359/3516] Migrate Elgato to new entity naming style (#74590) --- homeassistant/components/elgato/entity.py | 2 ++ homeassistant/components/elgato/light.py | 1 - tests/components/elgato/test_button.py | 10 +++++----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/elgato/entity.py b/homeassistant/components/elgato/entity.py index 8927f8d71c0..16d3ac78e81 100644 --- a/homeassistant/components/elgato/entity.py +++ b/homeassistant/components/elgato/entity.py @@ -12,6 +12,8 @@ from .const import DOMAIN class ElgatoEntity(Entity): """Defines an Elgato entity.""" + _attr_has_entity_name = True + def __init__(self, client: Elgato, info: Info, mac: str | None) -> None: """Initialize an Elgato entity.""" self.client = client diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py index e13119c2887..2a9f63a83d7 100644 --- a/homeassistant/components/elgato/light.py +++ b/homeassistant/components/elgato/light.py @@ -80,7 +80,6 @@ class ElgatoLight( self._attr_min_mireds = 143 self._attr_max_mireds = 344 - self._attr_name = info.display_name or info.product_name self._attr_supported_color_modes = {ColorMode.COLOR_TEMP} self._attr_unique_id = info.serial_number diff --git a/tests/components/elgato/test_button.py b/tests/components/elgato/test_button.py index 2ab3d7ee7c4..1f673bc6f44 100644 --- a/tests/components/elgato/test_button.py +++ b/tests/components/elgato/test_button.py @@ -25,12 +25,12 @@ async def test_button_identify( device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) - state = hass.states.get("button.identify") + state = hass.states.get("button.frenck_identify") assert state assert state.attributes.get(ATTR_ICON) == "mdi:help" assert state.state == STATE_UNKNOWN - entry = entity_registry.async_get("button.identify") + entry = entity_registry.async_get("button.frenck_identify") assert entry assert entry.unique_id == "CN11A1A00001_identify" assert entry.entity_category == EntityCategory.CONFIG @@ -53,14 +53,14 @@ async def test_button_identify( await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.identify"}, + {ATTR_ENTITY_ID: "button.frenck_identify"}, blocking=True, ) assert len(mock_elgato.identify.mock_calls) == 1 mock_elgato.identify.assert_called_with() - state = hass.states.get("button.identify") + state = hass.states.get("button.frenck_identify") assert state assert state.state == "2021-11-13T11:48:00+00:00" @@ -79,7 +79,7 @@ async def test_button_identify_error( await hass.services.async_call( BUTTON_DOMAIN, SERVICE_PRESS, - {ATTR_ENTITY_ID: "button.identify"}, + {ATTR_ENTITY_ID: "button.frenck_identify"}, blocking=True, ) await hass.async_block_till_done() From 157d6dc83f5c45d0ca050e53c373e0f714f52226 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 9 Jul 2022 16:45:49 -0500 Subject: [PATCH 2360/3516] Add missing Start Dimmer mapping for bond buttons (#74555) --- homeassistant/components/bond/button.py | 7 +++++++ tests/components/bond/test_button.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/bond/button.py b/homeassistant/components/bond/button.py index 9a82309e347..6f57a23b079 100644 --- a/homeassistant/components/bond/button.py +++ b/homeassistant/components/bond/button.py @@ -88,6 +88,13 @@ BUTTONS: tuple[BondButtonEntityDescription, ...] = ( mutually_exclusive=Action.TURN_DOWN_LIGHT_ON, argument=None, ), + BondButtonEntityDescription( + key=Action.START_DIMMER, + name="Start Dimmer", + icon="mdi:brightness-percent", + mutually_exclusive=Action.SET_BRIGHTNESS, + argument=None, + ), BondButtonEntityDescription( key=Action.START_UP_LIGHT_DIMMER, name="Start Up Light Dimmer", diff --git a/tests/components/bond/test_button.py b/tests/components/bond/test_button.py index 4411b25657b..24ea4730d6c 100644 --- a/tests/components/bond/test_button.py +++ b/tests/components/bond/test_button.py @@ -30,6 +30,7 @@ def light_brightness_increase_decrease_only(name: str): "actions": [ Action.TURN_LIGHT_ON, Action.TURN_LIGHT_OFF, + Action.START_DIMMER, Action.START_INCREASING_BRIGHTNESS, Action.START_DECREASING_BRIGHTNESS, Action.STOP, @@ -75,6 +76,8 @@ async def test_entity_registry(hass: core.HomeAssistant): assert entity.unique_id == "test-hub-id_test-device-id_startincreasingbrightness" entity = registry.entities["button.name_1_start_decreasing_brightness"] assert entity.unique_id == "test-hub-id_test-device-id_startdecreasingbrightness" + entity = registry.entities["button.name_1_start_dimmer"] + assert entity.unique_id == "test-hub-id_test-device-id_startdimmer" async def test_mutually_exclusive_actions(hass: core.HomeAssistant): From d80d16aaa45e3d6dfe49c558e18247a67b2d45f8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 9 Jul 2022 23:46:24 +0200 Subject: [PATCH 2361/3516] Use pydeconz interface controls for number platform (#74666) --- homeassistant/components/deconz/number.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index acd890d9c90..4c0959f950d 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -113,8 +113,10 @@ class DeconzNumber(DeconzDevice, NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set sensor config.""" - data = {self.entity_description.key: int(value)} - await self._device.set_config(**data) + await self.gateway.api.sensors.presence.set_config( + id=self._device.resource_id, + delay=int(value), + ) @property def unique_id(self) -> str: From 40ee7bab8f8e027744097c32f876856768289cc1 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 10 Jul 2022 00:31:09 +0200 Subject: [PATCH 2362/3516] Fix false-positive in pylint plugin (#74244) --- pylint/plugins/hass_enforce_type_hints.py | 67 +++++---- tests/pylint/test_enforce_type_hints.py | 159 ++++++++++++++++------ 2 files changed, 159 insertions(+), 67 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 7996afdc545..680d25414aa 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -53,13 +53,18 @@ class ClassTypeHintMatch: _TYPE_HINT_MATCHERS: dict[str, re.Pattern[str]] = { # a_or_b matches items such as "DiscoveryInfoType | None" "a_or_b": re.compile(r"^(\w+) \| (\w+)$"), - # x_of_y matches items such as "Awaitable[None]" - "x_of_y": re.compile(r"^(\w+)\[(.*?]*)\]$"), - # x_of_y_comma_z matches items such as "Callable[..., Awaitable[None]]" - "x_of_y_comma_z": re.compile(r"^(\w+)\[(.*?]*), (.*?]*)\]$"), - # x_of_y_of_z_comma_a matches items such as "list[dict[str, Any]]" - "x_of_y_of_z_comma_a": re.compile(r"^(\w+)\[(\w+)\[(.*?]*), (.*?]*)\]\]$"), } +_INNER_MATCH = r"((?:\w+)|(?:\.{3})|(?:\w+\[.+\]))" +_INNER_MATCH_POSSIBILITIES = [i + 1 for i in range(5)] +_TYPE_HINT_MATCHERS.update( + { + f"x_of_y_{i}": re.compile( + rf"^(\w+)\[{_INNER_MATCH}" + f", {_INNER_MATCH}" * (i - 1) + r"\]$" + ) + for i in _INNER_MATCH_POSSIBILITIES + } +) + _MODULE_REGEX: re.Pattern[str] = re.compile(r"^homeassistant\.components\.\w+(\.\w+)?$") @@ -1438,25 +1443,26 @@ def _is_valid_type( and _is_valid_type(match.group(2), node.right) ) - # Special case for xxx[yyy[zzz, aaa]]` - if match := _TYPE_HINT_MATCHERS["x_of_y_of_z_comma_a"].match(expected_type): - return ( - isinstance(node, nodes.Subscript) - and _is_valid_type(match.group(1), node.value) - and isinstance(subnode := node.slice, nodes.Subscript) - and _is_valid_type(match.group(2), subnode.value) - and isinstance(subnode.slice, nodes.Tuple) - and _is_valid_type(match.group(3), subnode.slice.elts[0]) - and _is_valid_type(match.group(4), subnode.slice.elts[1]) + # Special case for `xxx[aaa, bbb, ccc, ...] + if ( + isinstance(node, nodes.Subscript) + and isinstance(node.slice, nodes.Tuple) + and ( + match := _TYPE_HINT_MATCHERS[f"x_of_y_{len(node.slice.elts)}"].match( + expected_type + ) ) - - # Special case for xxx[yyy, zzz]` - if match := _TYPE_HINT_MATCHERS["x_of_y_comma_z"].match(expected_type): - # Handle special case of Mapping[xxx, Any] - if in_return and match.group(1) == "Mapping" and match.group(3) == "Any": + ): + # This special case is separate because we want Mapping[str, Any] + # to also match dict[str, int] and similar + if ( + len(node.slice.elts) == 2 + and in_return + and match.group(1) == "Mapping" + and match.group(3) == "Any" + ): return ( - isinstance(node, nodes.Subscript) - and isinstance(node.value, nodes.Name) + isinstance(node.value, nodes.Name) # We accept dict when Mapping is needed and node.value.name in ("Mapping", "dict") and isinstance(node.slice, nodes.Tuple) @@ -1464,16 +1470,19 @@ def _is_valid_type( # Ignore second item # and _is_valid_type(match.group(3), node.slice.elts[1]) ) + + # This is the default case return ( - isinstance(node, nodes.Subscript) - and _is_valid_type(match.group(1), node.value) + _is_valid_type(match.group(1), node.value) and isinstance(node.slice, nodes.Tuple) - and _is_valid_type(match.group(2), node.slice.elts[0]) - and _is_valid_type(match.group(3), node.slice.elts[1]) + and all( + _is_valid_type(match.group(n + 2), node.slice.elts[n]) + for n in range(len(node.slice.elts)) + ) ) - # Special case for xxx[yyy]` - if match := _TYPE_HINT_MATCHERS["x_of_y"].match(expected_type): + # Special case for xxx[yyy] + if match := _TYPE_HINT_MATCHERS["x_of_y_1"].match(expected_type): return ( isinstance(node, nodes.Subscript) and _is_valid_type(match.group(1), node.value) diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index df10c7514c6..e549d21fe0f 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -40,53 +40,34 @@ def test_regex_get_module_platform( @pytest.mark.parametrize( - ("string", "expected_x", "expected_y", "expected_z", "expected_a"), + ("string", "expected_count", "expected_items"), [ - ("list[dict[str, str]]", "list", "dict", "str", "str"), - ("list[dict[str, Any]]", "list", "dict", "str", "Any"), + ("Callable[..., None]", 2, ("Callable", "...", "None")), + ("Callable[..., Awaitable[None]]", 2, ("Callable", "...", "Awaitable[None]")), + ("tuple[int, int, int, int]", 4, ("tuple", "int", "int", "int", "int")), + ( + "tuple[int, int, int, int, int]", + 5, + ("tuple", "int", "int", "int", "int", "int"), + ), + ("Awaitable[None]", 1, ("Awaitable", "None")), + ("list[dict[str, str]]", 1, ("list", "dict[str, str]")), + ("list[dict[str, Any]]", 1, ("list", "dict[str, Any]")), ], ) -def test_regex_x_of_y_of_z_comma_a( +def test_regex_x_of_y_i( hass_enforce_type_hints: ModuleType, string: str, - expected_x: str, - expected_y: str, - expected_z: str, - expected_a: str, + expected_count: int, + expected_items: tuple[str, ...], ) -> None: - """Test x_of_y_of_z_comma_a regexes.""" + """Test x_of_y_i regexes.""" matchers: dict[str, re.Pattern] = hass_enforce_type_hints._TYPE_HINT_MATCHERS - assert (match := matchers["x_of_y_of_z_comma_a"].match(string)) + assert (match := matchers[f"x_of_y_{expected_count}"].match(string)) assert match.group(0) == string - assert match.group(1) == expected_x - assert match.group(2) == expected_y - assert match.group(3) == expected_z - assert match.group(4) == expected_a - - -@pytest.mark.parametrize( - ("string", "expected_x", "expected_y", "expected_z"), - [ - ("Callable[..., None]", "Callable", "...", "None"), - ("Callable[..., Awaitable[None]]", "Callable", "...", "Awaitable[None]"), - ], -) -def test_regex_x_of_y_comma_z( - hass_enforce_type_hints: ModuleType, - string: str, - expected_x: str, - expected_y: str, - expected_z: str, -) -> None: - """Test x_of_y_comma_z regexes.""" - matchers: dict[str, re.Pattern] = hass_enforce_type_hints._TYPE_HINT_MATCHERS - - assert (match := matchers["x_of_y_comma_z"].match(string)) - assert match.group(0) == string - assert match.group(1) == expected_x - assert match.group(2) == expected_y - assert match.group(3) == expected_z + for index in range(expected_count): + assert match.group(index + 1) == expected_items[index] @pytest.mark.parametrize( @@ -743,3 +724,105 @@ def test_valid_mapping_return_type( with assert_no_messages(linter): type_hint_checker.visit_classdef(class_node) + + +def test_valid_long_tuple( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Check invalid entity properties are ignored by default.""" + # Set ignore option + type_hint_checker.config.ignore_missing_annotations = False + + class_node, _, _ = astroid.extract_node( + """ + class Entity(): + pass + + class ToggleEntity(Entity): + pass + + class LightEntity(ToggleEntity): + pass + + class TestLight( #@ + LightEntity + ): + @property + def rgbw_color( #@ + self + ) -> tuple[int, int, int, int]: + pass + + @property + def rgbww_color( #@ + self + ) -> tuple[int, int, int, int, int]: + pass + """, + "homeassistant.components.pylint_test.light", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_no_messages(linter): + type_hint_checker.visit_classdef(class_node) + + +def test_invalid_long_tuple( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Check invalid entity properties are ignored by default.""" + # Set ignore option + type_hint_checker.config.ignore_missing_annotations = False + + class_node, rgbw_node, rgbww_node = astroid.extract_node( + """ + class Entity(): + pass + + class ToggleEntity(Entity): + pass + + class LightEntity(ToggleEntity): + pass + + class TestLight( #@ + LightEntity + ): + @property + def rgbw_color( #@ + self + ) -> tuple[int, int, int, int, int]: + pass + + @property + def rgbww_color( #@ + self + ) -> tuple[int, int, int, int, float]: + pass + """, + "homeassistant.components.pylint_test.light", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_adds_messages( + linter, + pylint.testutils.MessageTest( + msg_id="hass-return-type", + node=rgbw_node, + args=["tuple[int, int, int, int]", None], + line=15, + col_offset=4, + end_line=15, + end_col_offset=18, + ), + pylint.testutils.MessageTest( + msg_id="hass-return-type", + node=rgbww_node, + args=["tuple[int, int, int, int, int]", None], + line=21, + col_offset=4, + end_line=21, + end_col_offset=19, + ), + ): + type_hint_checker.visit_classdef(class_node) From 2f375004439208b74fb53b0936ea2f15296110ae Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 01:51:46 +0200 Subject: [PATCH 2363/3516] Update shodan to 1.28.0 (#74850) --- homeassistant/components/shodan/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index 7d609a5e9c3..98af966c496 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -2,7 +2,7 @@ "domain": "shodan", "name": "Shodan", "documentation": "https://www.home-assistant.io/integrations/shodan", - "requirements": ["shodan==1.27.0"], + "requirements": ["shodan==1.28.0"], "codeowners": ["@fabaff"], "iot_class": "cloud_polling", "loggers": ["shodan"] diff --git a/requirements_all.txt b/requirements_all.txt index 1842c31497f..b8ff50bd853 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2162,7 +2162,7 @@ sharkiq==0.0.1 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.27.0 +shodan==1.28.0 # homeassistant.components.sighthound simplehound==0.3 From d40ad9691685504b30cf02bb4d21b25198c42bd9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 10 Jul 2022 00:27:48 +0000 Subject: [PATCH 2364/3516] [ci skip] Translation update --- .../components/anthemav/translations/ar.json | 18 ++++++++++++++ .../components/awair/translations/ar.json | 11 +++++++++ .../here_travel_time/translations/ar.json | 12 ++++++++++ .../translations/zh-Hant.json | 7 ++++++ .../components/hive/translations/ar.json | 11 +++++++++ .../lg_soundbar/translations/ar.json | 18 ++++++++++++++ .../components/life360/translations/ar.json | 18 ++++++++++++++ .../components/nextdns/translations/ar.json | 20 ++++++++++++++++ .../components/nina/translations/ar.json | 24 +++++++++++++++++++ .../overkiz/translations/sensor.ar.json | 7 ++++++ .../components/qnap_qsw/translations/ar.json | 12 ++++++++++ .../radiotherm/translations/ar.json | 11 +++++++++ .../sensibo/translations/sensor.ar.json | 7 ++++++ .../simplepush/translations/ar.json | 18 ++++++++++++++ .../soundtouch/translations/ar.json | 21 ++++++++++++++++ 15 files changed, 215 insertions(+) create mode 100644 homeassistant/components/anthemav/translations/ar.json create mode 100644 homeassistant/components/awair/translations/ar.json create mode 100644 homeassistant/components/here_travel_time/translations/ar.json create mode 100644 homeassistant/components/hive/translations/ar.json create mode 100644 homeassistant/components/lg_soundbar/translations/ar.json create mode 100644 homeassistant/components/life360/translations/ar.json create mode 100644 homeassistant/components/nextdns/translations/ar.json create mode 100644 homeassistant/components/nina/translations/ar.json create mode 100644 homeassistant/components/overkiz/translations/sensor.ar.json create mode 100644 homeassistant/components/qnap_qsw/translations/ar.json create mode 100644 homeassistant/components/radiotherm/translations/ar.json create mode 100644 homeassistant/components/sensibo/translations/sensor.ar.json create mode 100644 homeassistant/components/simplepush/translations/ar.json create mode 100644 homeassistant/components/soundtouch/translations/ar.json diff --git a/homeassistant/components/anthemav/translations/ar.json b/homeassistant/components/anthemav/translations/ar.json new file mode 100644 index 00000000000..558784a02e4 --- /dev/null +++ b/homeassistant/components/anthemav/translations/ar.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0627\u0644\u062c\u0647\u0627\u0632 \u062a\u0645 \u062a\u0647\u064a\u0623\u062a\u0647 \u0645\u0633\u0628\u0642\u0627" + }, + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644" + }, + "step": { + "user": { + "data": { + "host": "\u0645\u0636\u064a\u0641", + "port": "\u0645\u0646\u0641\u0630" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/ar.json b/homeassistant/components/awair/translations/ar.json new file mode 100644 index 00000000000..3f25b63e7b9 --- /dev/null +++ b/homeassistant/components/awair/translations/ar.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "email": "\u0627\u0644\u0628\u0631\u064a\u062f \u0627\u0644\u0625\u0644\u0643\u062a\u0631\u0648\u0646\u064a" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/ar.json b/homeassistant/components/here_travel_time/translations/ar.json new file mode 100644 index 00000000000..adea555a76a --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/ar.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "origin_menu": { + "menu_options": { + "origin_coordinates": "\u0627\u0633\u062a\u062e\u062f\u0645 \u0645\u0648\u0642\u0639 \u0627\u0644\u062e\u0631\u064a\u0637\u0629", + "origin_entity": "\u0627\u0633\u062a\u062e\u062f\u0645 \u0643\u064a\u0627\u0646" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/zh-Hant.json b/homeassistant/components/here_travel_time/translations/zh-Hant.json index 53d5eae18fd..4476294b661 100644 --- a/homeassistant/components/here_travel_time/translations/zh-Hant.json +++ b/homeassistant/components/here_travel_time/translations/zh-Hant.json @@ -39,6 +39,13 @@ }, "title": "\u9078\u64c7\u51fa\u767c\u5730" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "\u4f7f\u7528\u5730\u5716\u5ea7\u6a19", + "origin_entity": "\u4f7f\u7528\u70ba\u5be6\u9ad4" + }, + "title": "\u9078\u64c7\u51fa\u767c\u5730" + }, "user": { "data": { "api_key": "API \u91d1\u9470", diff --git a/homeassistant/components/hive/translations/ar.json b/homeassistant/components/hive/translations/ar.json new file mode 100644 index 00000000000..fd198262e7e --- /dev/null +++ b/homeassistant/components/hive/translations/ar.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "configuration": { + "data": { + "device_name": "\u0627\u0633\u0645 \u0627\u0644\u062c\u0647\u0627\u0632" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/ar.json b/homeassistant/components/lg_soundbar/translations/ar.json new file mode 100644 index 00000000000..3fc833f41f1 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/ar.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0627\u0644\u062e\u062f\u0645\u0629 \u062a\u0645 \u062a\u0647\u064a\u0623\u062a\u0647\u0627 \u0645\u0633\u0628\u0642\u0627", + "existing_instance_updated": "\u062a\u062d\u062f\u064a\u062b \u0627\u0644\u062a\u0643\u0648\u064a\u0646 \u0627\u0644\u062d\u0627\u0644\u064a" + }, + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644" + }, + "step": { + "user": { + "data": { + "host": "\u0627\u0644\u0645\u0636\u064a\u0641" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/ar.json b/homeassistant/components/life360/translations/ar.json new file mode 100644 index 00000000000..32dbd371473 --- /dev/null +++ b/homeassistant/components/life360/translations/ar.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0627\u0644\u062d\u0633\u0627\u0628 \u062a\u0645 \u0625\u0639\u062f\u0627\u062f\u0647 \u0645\u0633\u0628\u0642\u0627", + "reauth_successful": "\u0625\u0639\u0627\u062f\u0629 \u0627\u0644\u062a\u0648\u062b\u064a\u0642 \u062a\u0645\u062a \u0628\u0646\u062c\u0627\u062d" + }, + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644" + } + }, + "options": { + "step": { + "init": { + "title": "\u062e\u064a\u0627\u0631\u0627\u062a \u0627\u0644\u062d\u0633\u0627\u0628" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/ar.json b/homeassistant/components/nextdns/translations/ar.json new file mode 100644 index 00000000000..965e46a92e7 --- /dev/null +++ b/homeassistant/components/nextdns/translations/ar.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644", + "unknown": "\u062e\u0637\u0623 \u063a\u064a\u0631 \u0645\u062a\u0648\u0642\u0639" + }, + "step": { + "profiles": { + "data": { + "profile": "\u0627\u0644\u0645\u0644\u0641 \u0627\u0644\u0634\u062e\u0635\u064a" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u062c\u0627\u0631\u064a \u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u062e\u0627\u062f\u0645" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/ar.json b/homeassistant/components/nina/translations/ar.json new file mode 100644 index 00000000000..d0c2b4eccbc --- /dev/null +++ b/homeassistant/components/nina/translations/ar.json @@ -0,0 +1,24 @@ +{ + "options": { + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644", + "no_selection": "\u0645\u0646 \u0641\u0636\u0644\u0643 \u0627\u062e\u062a\u0631 \u0639\u0644\u0649 \u0627\u0644\u0623\u0642\u0644 \u0645\u062f\u064a\u0646\u0629/\u0645\u0642\u0627\u0637\u0639\u0629 \u0648\u0627\u062d\u062f\u0629", + "unknown": "\u062e\u0637\u0623 \u063a\u064a\u0631 \u0645\u062a\u0648\u0642\u0639" + }, + "step": { + "init": { + "data": { + "_a_to_d": "\u0627\u0644\u0645\u062f\u064a\u0646\u0629/\u0627\u0644\u0645\u0642\u0627\u0637\u0639\u0629 (A-D)", + "_e_to_h": "\u0627\u0644\u0645\u062f\u064a\u0646\u0629/\u0627\u0644\u0645\u0642\u0627\u0637\u0639\u0629 (E-H)", + "_i_to_l": "\u0627\u0644\u0645\u062f\u064a\u0646\u0629/\u0627\u0644\u0645\u0642\u0627\u0637\u0639\u0629 (I-L)", + "_m_to_q": "\u0627\u0644\u0645\u062f\u064a\u0646\u0629 / \u0627\u0644\u0645\u0642\u0627\u0637\u0639\u0629 (MQ)", + "_r_to_u": "\u0627\u0644\u0645\u062f\u064a\u0646\u0629 / \u0627\u0644\u0645\u0642\u0627\u0637\u0639\u0629 (RU)", + "_v_to_z": "\u0627\u0644\u0645\u062f\u064a\u0646\u0629 / \u0627\u0644\u0645\u0642\u0627\u0637\u0639\u0629 (VZ)", + "corona_filter": "\u0623\u0632\u0644 \u062a\u062d\u0630\u064a\u0631\u0627\u062a \u0643\u0648\u0631\u0648\u0646\u0627", + "slots": "\u0627\u0644\u062a\u062d\u0630\u064a\u0631\u0627\u062a \u0627\u0644\u0642\u0635\u0648\u0649 \u0644\u0643\u0644 \u0645\u062f\u064a\u0646\u0629/\u0645\u062d\u0627\u0641\u0638\u0629" + }, + "title": "\u0627\u0644\u062e\u064a\u0627\u0631\u0627\u062a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.ar.json b/homeassistant/components/overkiz/translations/sensor.ar.json new file mode 100644 index 00000000000..a68781b47cd --- /dev/null +++ b/homeassistant/components/overkiz/translations/sensor.ar.json @@ -0,0 +1,7 @@ +{ + "state": { + "overkiz__three_way_handle_direction": { + "tilt": "\u0625\u0645\u0627\u0644\u0629" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/ar.json b/homeassistant/components/qnap_qsw/translations/ar.json new file mode 100644 index 00000000000..07980293874 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/ar.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "discovered_connection": { + "data": { + "password": "\u0627\u0644\u0631\u0642\u0645 \u0627\u0644\u0633\u0631\u064a", + "username": "\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/ar.json b/homeassistant/components/radiotherm/translations/ar.json new file mode 100644 index 00000000000..217c499dd98 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/ar.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u0627\u0644\u0645\u0636\u064a\u0641" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.ar.json b/homeassistant/components/sensibo/translations/sensor.ar.json new file mode 100644 index 00000000000..b8d510999fd --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.ar.json @@ -0,0 +1,7 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "\u0637\u0628\u064a\u0639\u064a\u0627" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/ar.json b/homeassistant/components/simplepush/translations/ar.json new file mode 100644 index 00000000000..81b5a92d394 --- /dev/null +++ b/homeassistant/components/simplepush/translations/ar.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0627\u0644\u062c\u0647\u0627\u0632 \u062a\u0645 \u062a\u0647\u064a\u0626\u062a\u0647 \u0645\u0646 \u0642\u0628\u0644 " + }, + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644" + }, + "step": { + "user": { + "data": { + "device_key": "\u0627\u0644\u0645\u0641\u062a\u0627\u062d \u0627\u0644\u062e\u0627\u0635 \u0628\u062c\u0647\u0627\u0632\u0643", + "name": "\u0627\u0644\u0627\u0633\u0645" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ar.json b/homeassistant/components/soundtouch/translations/ar.json new file mode 100644 index 00000000000..22fdd032123 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/ar.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0627\u0644\u062c\u0647\u0627\u0632 \u062a\u0645 \u0625\u0639\u062f\u0627\u062f\u0647 \u0645\u0633\u0628\u0642\u0627" + }, + "error": { + "cannot_connect": "\u0641\u0634\u0644 \u0641\u064a \u0627\u0644\u0627\u062a\u0635\u0627\u0644" + }, + "step": { + "user": { + "data": { + "host": "\u0627\u0644\u0645\u0636\u064a\u0641" + } + }, + "zeroconf_confirm": { + "description": "\u0623\u0646\u062a \u0639\u0644\u0649 \u0648\u0634\u0643 \u0627\u0636\u0627\u0641\u0629 \u062c\u0647\u0627\u0632 SoundTouch \u0630\u0627\u062a \u0627\u0644\u0627\u0633\u0645 {name} \u0625\u0644\u0649 Home Assistant", + "title": "\u062c\u0627\u0631\u064a \u0625\u0636\u0627\u0641\u0629 \u062c\u0647\u0627\u0632 Bose SoundTouch" + } + } + } +} \ No newline at end of file From 80727ff952b929fbd30781ee0e2af8d7b5705a63 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 03:30:22 +0200 Subject: [PATCH 2365/3516] Update pyudev to 0.23.2 (#74859) --- homeassistant/components/usb/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/usb/manifest.json b/homeassistant/components/usb/manifest.json index 87b81965c25..22dca558379 100644 --- a/homeassistant/components/usb/manifest.json +++ b/homeassistant/components/usb/manifest.json @@ -2,7 +2,7 @@ "domain": "usb", "name": "USB Discovery", "documentation": "https://www.home-assistant.io/integrations/usb", - "requirements": ["pyudev==0.22.0", "pyserial==3.5"], + "requirements": ["pyudev==0.23.2", "pyserial==3.5"], "codeowners": ["@bdraco"], "dependencies": ["websocket_api"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8f0e2087c14..d53db9a7464 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -26,7 +26,7 @@ pillow==9.2.0 pip>=21.0,<22.2 pyserial==3.5 python-slugify==4.0.1 -pyudev==0.22.0 +pyudev==0.23.2 pyyaml==6.0 requests==2.28.1 scapy==2.4.5 diff --git a/requirements_all.txt b/requirements_all.txt index b8ff50bd853..493a5fa97d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1993,7 +1993,7 @@ pytradfri[async]==9.0.0 pytrafikverket==0.2.0.1 # homeassistant.components.usb -pyudev==0.22.0 +pyudev==0.23.2 # homeassistant.components.unifiprotect pyunifiprotect==4.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 269ce43c0a8..aa0bb68a8b1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1334,7 +1334,7 @@ pytradfri[async]==9.0.0 pytrafikverket==0.2.0.1 # homeassistant.components.usb -pyudev==0.22.0 +pyudev==0.23.2 # homeassistant.components.unifiprotect pyunifiprotect==4.0.9 From f7aea76e73744b02519469e4c0c2609f23ccdddd Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Sun, 10 Jul 2022 06:33:54 -0400 Subject: [PATCH 2366/3516] Bump pymazda to 0.3.6 (#74863) --- 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 a18b4406355..acf5282689f 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.3.3"], + "requirements": ["pymazda==0.3.6"], "codeowners": ["@bdr99"], "quality_scale": "platinum", "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 493a5fa97d9..334ea849a0f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1643,7 +1643,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.3 +pymazda==0.3.6 # homeassistant.components.mediaroom pymediaroom==0.6.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index aa0bb68a8b1..ac5f3f7148d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1119,7 +1119,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.3 +pymazda==0.3.6 # homeassistant.components.melcloud pymelcloud==2.5.6 From cb4d2d1b268cbfa6bafd547671c621d625b88336 Mon Sep 17 00:00:00 2001 From: David Straub Date: Sun, 10 Jul 2022 12:49:18 +0200 Subject: [PATCH 2367/3516] Bump pysml to 0.0.8 (fixes #74382) (#74875) --- homeassistant/components/edl21/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/edl21/manifest.json b/homeassistant/components/edl21/manifest.json index 4cffabe87fc..cac35a10152 100644 --- a/homeassistant/components/edl21/manifest.json +++ b/homeassistant/components/edl21/manifest.json @@ -2,7 +2,7 @@ "domain": "edl21", "name": "EDL21", "documentation": "https://www.home-assistant.io/integrations/edl21", - "requirements": ["pysml==0.0.7"], + "requirements": ["pysml==0.0.8"], "codeowners": ["@mtdcr"], "iot_class": "local_push", "loggers": ["sml"] diff --git a/requirements_all.txt b/requirements_all.txt index 334ea849a0f..723a6c4ed5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1838,7 +1838,7 @@ pysmartthings==0.7.6 pysmarty==0.8 # homeassistant.components.edl21 -pysml==0.0.7 +pysml==0.0.8 # homeassistant.components.snmp pysnmplib==5.0.15 From 3429a75cc5d9be8560dfef5c2619d21273e2bffe Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 10 Jul 2022 05:51:40 -0500 Subject: [PATCH 2368/3516] Bump rokuecp to 0.17.0 (#74862) --- homeassistant/components/roku/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 05fe0e1b260..910516b93e8 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -2,7 +2,7 @@ "domain": "roku", "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", - "requirements": ["rokuecp==0.16.0"], + "requirements": ["rokuecp==0.17.0"], "homekit": { "models": ["3820X", "3810X", "4660X", "7820X", "C105X", "C135X"] }, diff --git a/requirements_all.txt b/requirements_all.txt index 723a6c4ed5b..c25a6dd40eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2095,7 +2095,7 @@ rjpl==0.3.6 rocketchat-API==0.6.1 # homeassistant.components.roku -rokuecp==0.16.0 +rokuecp==0.17.0 # homeassistant.components.roomba roombapy==1.6.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac5f3f7148d..832bfa1bdee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1397,7 +1397,7 @@ rflink==0.0.63 ring_doorbell==0.7.2 # homeassistant.components.roku -rokuecp==0.16.0 +rokuecp==0.17.0 # homeassistant.components.roomba roombapy==1.6.5 From 70ceccb06a545711445ac33b62b8a6c84d44b146 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 14:21:14 +0200 Subject: [PATCH 2369/3516] Update respx to 0.19.2 (#74878) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 113794bda1c..822fb02c6e0 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -25,7 +25,7 @@ pytest-timeout==2.1.0 pytest-xdist==2.5.0 pytest==7.1.2 requests_mock==1.9.2 -respx==0.19.0 +respx==0.19.2 stdlib-list==0.7.0 tomli==2.0.1;python_version<"3.11" tqdm==4.49.0 From 52130b227e349d5cef221ee101f85d4368bee7c6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 14:55:43 +0200 Subject: [PATCH 2370/3516] Update flake8-comprehensions to 3.10.0 (#74882) --- .pre-commit-config.yaml | 2 +- homeassistant/util/color.py | 14 ++++++-------- requirements_test_pre_commit.txt | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5da3a6b21ff..fd2d5ac7e20 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: - pyflakes==2.4.0 - flake8-docstrings==1.6.0 - pydocstyle==6.1.1 - - flake8-comprehensions==3.8.0 + - flake8-comprehensions==3.10.0 - flake8-noqa==1.2.1 - mccabe==0.6.1 files: ^(homeassistant|script|tests)/.+\.py$ diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 448b417cc97..494ee04546c 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -294,22 +294,20 @@ def color_xy_brightness_to_RGB( b = X * 0.051713 - Y * 0.121364 + Z * 1.011530 # Apply reverse gamma correction. - r, g, b = map( - lambda x: (12.92 * x) # type: ignore[no-any-return] - if (x <= 0.0031308) - else ((1.0 + 0.055) * pow(x, (1.0 / 2.4)) - 0.055), - [r, g, b], + r, g, b = ( + 12.92 * x if (x <= 0.0031308) else ((1.0 + 0.055) * pow(x, (1.0 / 2.4)) - 0.055) + for x in (r, g, b) ) # Bring all negative components to zero. - r, g, b = map(lambda x: max(0, x), [r, g, b]) + r, g, b = (max(0, x) for x in (r, g, b)) # If one component is greater than 1, weight components by that value. max_component = max(r, g, b) if max_component > 1: - r, g, b = map(lambda x: x / max_component, [r, g, b]) + r, g, b = (x / max_component for x in (r, g, b)) - ir, ig, ib = map(lambda x: int(x * 255), [r, g, b]) + ir, ig, ib = (int(x * 255) for x in (r, g, b)) return (ir, ig, ib) diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 23d675afaf1..c229bc51731 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -3,7 +3,7 @@ bandit==1.7.4 black==22.6.0 codespell==2.1.0 -flake8-comprehensions==3.8.0 +flake8-comprehensions==3.10.0 flake8-docstrings==1.6.0 flake8-noqa==1.2.1 flake8==4.0.1 From 6f9fcdff99fa26c3254a5d5e27f1cb0c7e049d01 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 10 Jul 2022 10:04:57 -0700 Subject: [PATCH 2371/3516] Improve calendar error handling to match best practices (#74891) --- homeassistant/components/calendar/__init__.py | 12 +++++-- homeassistant/components/google/calendar.py | 3 +- tests/components/calendar/test_init.py | 34 +++++++++++++++++-- tests/components/google/test_calendar.py | 5 +-- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 4623f490302..54da2a1cb02 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -14,6 +14,7 @@ from homeassistant.components import frontend, http from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -336,9 +337,14 @@ class CalendarEventView(http.HomeAssistantView): if not isinstance(entity, CalendarEntity): return web.Response(status=HTTPStatus.BAD_REQUEST) - calendar_event_list = await entity.async_get_events( - request.app["hass"], start_date, end_date - ) + try: + calendar_event_list = await entity.async_get_events( + request.app["hass"], start_date, end_date + ) + except HomeAssistantError as err: + return self.json_message( + f"Error reading events: {err}", HTTPStatus.INTERNAL_SERVER_ERROR + ) return self.json( [ { diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 534b1cbdef3..79e4b2da114 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -282,8 +282,7 @@ class GoogleCalendarEntity(CalendarEntity): async for result_page in result: result_items.extend(result_page.items) except ApiException as err: - _LOGGER.error("Unable to connect to Google: %s", err) - return [] + raise HomeAssistantError(str(err)) from err return [ _get_calendar_event(event) for event in filter(self._event_filter, result_items) diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 0a91f58b0b2..97bfd89f465 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -1,8 +1,10 @@ """The tests for the calendar component.""" from datetime import timedelta from http import HTTPStatus +from unittest.mock import patch from homeassistant.bootstrap import async_setup_component +from homeassistant.exceptions import HomeAssistantError import homeassistant.util.dt as dt_util @@ -11,8 +13,6 @@ async def test_events_http_api(hass, hass_client): await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) await hass.async_block_till_done() client = await hass_client() - response = await client.get("/api/calendars/calendar.calendar_2") - assert response.status == HTTPStatus.BAD_REQUEST start = dt_util.now() end = start + timedelta(days=1) response = await client.get( @@ -25,6 +25,36 @@ async def test_events_http_api(hass, hass_client): assert events[0]["summary"] == "Future Event" +async def test_events_http_api_missing_fields(hass, hass_client): + """Test the calendar demo view.""" + await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) + await hass.async_block_till_done() + client = await hass_client() + response = await client.get("/api/calendars/calendar.calendar_2") + assert response.status == HTTPStatus.BAD_REQUEST + + +async def test_events_http_api_error(hass, hass_client): + """Test the calendar demo view.""" + await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) + await hass.async_block_till_done() + client = await hass_client() + start = dt_util.now() + end = start + timedelta(days=1) + + with patch( + "homeassistant.components.demo.calendar.DemoCalendar.async_get_events", + side_effect=HomeAssistantError("Failure"), + ): + response = await client.get( + "/api/calendars/calendar.calendar_1?start={}&end={}".format( + start.isoformat(), end.isoformat() + ) + ) + assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR + assert await response.json() == {"message": "Error reading events: Failure"} + + async def test_calendars_http_api(hass, hass_client): """Test the calendar demo view.""" await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index f92519e2553..e49eb0c2e01 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -423,10 +423,7 @@ async def test_http_event_api_failure( mock_events_list({}, exc=ClientError()) response = await client.get(upcoming_event_url()) - assert response.status == HTTPStatus.OK - # A failure to talk to the server results in an empty list of events - events = await response.json() - assert events == [] + assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR @pytest.mark.freeze_time("2022-03-27 12:05:00+00:00") From 240a83239a0f2aaaf14372d52699ca28457cbf9c Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 10 Jul 2022 13:35:45 -0400 Subject: [PATCH 2372/3516] Correctly handle device triggers for missing ZHA devices (#74894) --- homeassistant/components/zha/core/helpers.py | 2 +- tests/components/zha/test_device_trigger.py | 38 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 390ef290dc2..b60f61b1e8e 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -170,7 +170,7 @@ def async_get_zha_device(hass: HomeAssistant, device_id: str) -> ZHADevice: device_registry = dr.async_get(hass) registry_device = device_registry.async_get(device_id) if not registry_device: - raise ValueError(f"Device id `{device_id}` not found in registry.") + raise KeyError(f"Device id `{device_id}` not found in registry.") zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee_address = list(list(registry_device.identifiers)[0])[1] ieee = zigpy.types.EUI64.convert(ieee_address) diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 1f5fa467a93..8e19fe5b637 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -370,3 +370,41 @@ async def test_exception_bad_trigger(hass, mock_devices, calls, caplog): ) await hass.async_block_till_done() assert "Invalid config for [automation]" in caplog.text + + +async def test_exception_no_device(hass, mock_devices, calls, caplog): + """Test for exception on event triggers firing.""" + + zigpy_device, zha_device = mock_devices + + zigpy_device.device_automation_triggers = { + (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, + (DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE}, + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}, + (LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD}, + (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, + } + + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": "no_such_device_id", + "domain": "zha", + "platform": "device", + "type": "junk", + "subtype": "junk", + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert "Invalid config for [automation]" in caplog.text From edaafadde0d31c400524276fabe8bf78ec314911 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 10 Jul 2022 13:46:22 -0400 Subject: [PATCH 2373/3516] Remove ZHA device storage (#74837) * Remove ZHA device storage * remove storage file if it exists --- homeassistant/components/zha/__init__.py | 12 +- homeassistant/components/zha/core/device.py | 8 - homeassistant/components/zha/core/gateway.py | 27 +--- homeassistant/components/zha/core/store.py | 146 ------------------- tests/components/zha/conftest.py | 17 +-- tests/components/zha/test_gateway.py | 64 +------- 6 files changed, 15 insertions(+), 259 deletions(-) delete mode 100644 homeassistant/components/zha/core/store.py diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 0e11d992a25..80956117ff9 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -1,6 +1,7 @@ """Support for Zigbee Home Automation devices.""" import asyncio import logging +import os import voluptuous as vol from zhaquirks import setup as setup_quirks @@ -12,6 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import ConfigType from . import api @@ -98,6 +100,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b if config.get(CONF_ENABLE_QUIRKS, True): setup_quirks(config) + # temporary code to remove the zha storage file from disk. this will be removed in 2022.10.0 + storage_path = hass.config.path(STORAGE_DIR, "zha.storage") + if os.path.isfile(storage_path): + _LOGGER.debug("removing ZHA storage file") + await hass.async_add_executor_job(os.remove, storage_path) + else: + _LOGGER.debug("ZHA storage file does not exist or was already removed") + zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() @@ -124,7 +134,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b """Handle shutdown tasks.""" zha_gateway: ZHAGateway = zha_data[DATA_ZHA_GATEWAY] await zha_gateway.shutdown() - await zha_gateway.async_update_device_storage() zha_data[DATA_ZHA_SHUTDOWN_TASK] = hass.bus.async_listen_once( ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown @@ -137,7 +146,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> """Unload ZHA config entry.""" zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] await zha_gateway.shutdown() - await zha_gateway.async_update_device_storage() GROUP_PROBE.cleanup() api.async_unload_api(hass) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index e83c0afbceb..afd4f647ecb 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -470,8 +470,6 @@ class ZHADevice(LogMixin): self.debug("started configuration") await self._channels.async_configure() self.debug("completed configuration") - entry = self.gateway.zha_storage.async_create_or_update_device(self) - self.debug("stored in registry: %s", entry) if ( should_identify @@ -496,12 +494,6 @@ class ZHADevice(LogMixin): for unsubscribe in self.unsubs: unsubscribe() - @callback - def async_update_last_seen(self, last_seen: float | None) -> None: - """Set last seen on the zigpy device.""" - if self._zigpy_device.last_seen is None and last_seen is not None: - self._zigpy_device.last_seen = last_seen - @property def zha_device_info(self) -> dict[str, Any]: """Get ZHA device information.""" diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 099abfe5e88..7782d8ef7fd 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -27,7 +27,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType from . import discovery @@ -81,14 +80,12 @@ from .const import ( from .device import DeviceStatus, ZHADevice from .group import GroupMember, ZHAGroup from .registries import GROUP_ENTITY_DOMAINS -from .store import async_get_registry if TYPE_CHECKING: from logging import Filter, LogRecord from ..entity import ZhaEntity from .channels.base import ZigbeeChannel - from .store import ZhaStorage _LogFilterType = Union[Filter, Callable[[LogRecord], int]] @@ -118,7 +115,6 @@ class ZHAGateway: """Gateway that handles events that happen on the ZHA Zigbee network.""" # -- Set in async_initialize -- - zha_storage: ZhaStorage ha_device_registry: dr.DeviceRegistry ha_entity_registry: er.EntityRegistry application_controller: ControllerApplication @@ -150,7 +146,6 @@ class ZHAGateway: discovery.PROBE.initialize(self._hass) discovery.GROUP_PROBE.initialize(self._hass) - self.zha_storage = await async_get_registry(self._hass) self.ha_device_registry = dr.async_get(self._hass) self.ha_entity_registry = er.async_get(self._hass) @@ -196,10 +191,9 @@ class ZHAGateway: zha_device = self._async_get_or_create_device(zigpy_device, restored=True) if zha_device.ieee == self.application_controller.ieee: self.coordinator_zha_device = zha_device - zha_dev_entry = self.zha_storage.devices.get(str(zigpy_device.ieee)) delta_msg = "not known" - if zha_dev_entry and zha_dev_entry.last_seen is not None: - delta = round(time.time() - zha_dev_entry.last_seen) + if zha_device.last_seen is not None: + delta = round(time.time() - zha_device.last_seen) zha_device.available = delta < zha_device.consider_unavailable_time delta_msg = f"{str(timedelta(seconds=delta))} ago" _LOGGER.debug( @@ -210,13 +204,6 @@ class ZHAGateway: delta_msg, zha_device.consider_unavailable_time, ) - # update the last seen time for devices every 10 minutes to avoid thrashing - # writes and shutdown issues where storage isn't updated - self._unsubs.append( - async_track_time_interval( - self._hass, self.async_update_device_storage, timedelta(minutes=10) - ) - ) @callback def async_load_groups(self) -> None: @@ -526,8 +513,6 @@ class ZHAGateway: model=zha_device.model, ) zha_device.set_device_id(device_registry_device.id) - entry = self.zha_storage.async_get_or_create_device(zha_device) - zha_device.async_update_last_seen(entry.last_seen) return zha_device @callback @@ -550,17 +535,9 @@ class ZHAGateway: if device.status is DeviceStatus.INITIALIZED: device.update_available(available) - async def async_update_device_storage(self, *_: Any) -> None: - """Update the devices in the store.""" - for device in self.devices.values(): - self.zha_storage.async_update_device(device) - async def async_device_initialized(self, device: zigpy.device.Device) -> None: """Handle device joined and basic information discovered (async).""" zha_device = self._async_get_or_create_device(device) - # This is an active device so set a last seen if it is none - if zha_device.last_seen is None: - zha_device.async_update_last_seen(time.time()) _LOGGER.debug( "device - %s:%s entering async_device_initialized - is_new_join: %s", device.nwk, diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py deleted file mode 100644 index 0b7564fe815..00000000000 --- a/homeassistant/components/zha/core/store.py +++ /dev/null @@ -1,146 +0,0 @@ -"""Data storage helper for ZHA.""" -from __future__ import annotations - -from collections import OrderedDict -from collections.abc import MutableMapping -import datetime -import time -from typing import TYPE_CHECKING, Any, cast - -import attr - -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.storage import Store -from homeassistant.loader import bind_hass - -if TYPE_CHECKING: - from .device import ZHADevice - -DATA_REGISTRY = "zha_storage" - -STORAGE_KEY = "zha.storage" -STORAGE_VERSION = 1 -SAVE_DELAY = 10 -TOMBSTONE_LIFETIME = datetime.timedelta(days=60).total_seconds() - - -@attr.s(slots=True, frozen=True) -class ZhaDeviceEntry: - """Zha Device storage Entry.""" - - name: str | None = attr.ib(default=None) - ieee: str | None = attr.ib(default=None) - last_seen: float | None = attr.ib(default=None) - - -class ZhaStorage: - """Class to hold a registry of zha devices.""" - - def __init__(self, hass: HomeAssistant) -> None: - """Initialize the zha device storage.""" - self.hass: HomeAssistant = hass - self.devices: MutableMapping[str, ZhaDeviceEntry] = {} - self._store = Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) - - @callback - def async_create_device(self, device: ZHADevice) -> ZhaDeviceEntry: - """Create a new ZhaDeviceEntry.""" - ieee_str: str = str(device.ieee) - device_entry: ZhaDeviceEntry = ZhaDeviceEntry( - name=device.name, ieee=ieee_str, last_seen=device.last_seen - ) - self.devices[ieee_str] = device_entry - self.async_schedule_save() - return device_entry - - @callback - def async_get_or_create_device(self, device: ZHADevice) -> ZhaDeviceEntry: - """Create a new ZhaDeviceEntry.""" - ieee_str: str = str(device.ieee) - if ieee_str in self.devices: - return self.devices[ieee_str] - return self.async_create_device(device) - - @callback - def async_create_or_update_device(self, device: ZHADevice) -> ZhaDeviceEntry: - """Create or update a ZhaDeviceEntry.""" - if str(device.ieee) in self.devices: - return self.async_update_device(device) - return self.async_create_device(device) - - @callback - def async_delete_device(self, device: ZHADevice) -> None: - """Delete ZhaDeviceEntry.""" - ieee_str: str = str(device.ieee) - if ieee_str in self.devices: - del self.devices[ieee_str] - self.async_schedule_save() - - @callback - def async_update_device(self, device: ZHADevice) -> ZhaDeviceEntry: - """Update name of ZhaDeviceEntry.""" - ieee_str: str = str(device.ieee) - old = self.devices[ieee_str] - - if device.last_seen is None: - return old - - changes = {} - changes["last_seen"] = device.last_seen - - new = self.devices[ieee_str] = attr.evolve(old, **changes) - self.async_schedule_save() - return new - - async def async_load(self) -> None: - """Load the registry of zha device entries.""" - data = await self._store.async_load() - - devices: OrderedDict[str, ZhaDeviceEntry] = OrderedDict() - - if data is not None: - for device in data["devices"]: - devices[device["ieee"]] = ZhaDeviceEntry( - name=device["name"], - ieee=device["ieee"], - last_seen=device.get("last_seen"), - ) - - self.devices = devices - - @callback - def async_schedule_save(self) -> None: - """Schedule saving the registry of zha devices.""" - self._store.async_delay_save(self._data_to_save, SAVE_DELAY) - - async def async_save(self) -> None: - """Save the registry of zha devices.""" - await self._store.async_save(self._data_to_save()) - - @callback - def _data_to_save(self) -> dict: - """Return data for the registry of zha devices to store in a file.""" - data = {} - - data["devices"] = [ - {"name": entry.name, "ieee": entry.ieee, "last_seen": entry.last_seen} - for entry in self.devices.values() - if entry.last_seen and (time.time() - entry.last_seen) < TOMBSTONE_LIFETIME - ] - - return data - - -@bind_hass -async def async_get_registry(hass: HomeAssistant) -> ZhaStorage: - """Return zha device storage instance.""" - if (task := hass.data.get(DATA_REGISTRY)) is None: - - async def _load_reg() -> ZhaStorage: - registry = ZhaStorage(hass) - await registry.async_load() - return registry - - task = hass.data[DATA_REGISTRY] = hass.async_create_task(_load_reg()) - - return cast(ZhaStorage, await task) diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 482a11b95de..d9223027668 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -15,7 +15,6 @@ from zigpy.state import State import zigpy.types import zigpy.zdo.types as zdo_t -from homeassistant.components.zha import DOMAIN import homeassistant.components.zha.core.const as zha_const import homeassistant.components.zha.core.device as zha_core_device from homeassistant.setup import async_setup_component @@ -188,26 +187,14 @@ def zha_device_joined(hass, setup_zha): @pytest.fixture -def zha_device_restored(hass, zigpy_app_controller, setup_zha, hass_storage): +def zha_device_restored(hass, zigpy_app_controller, setup_zha): """Return a restored ZHA device.""" async def _zha_device(zigpy_dev, last_seen=None): zigpy_app_controller.devices[zigpy_dev.ieee] = zigpy_dev if last_seen is not None: - hass_storage[f"{DOMAIN}.storage"] = { - "key": f"{DOMAIN}.storage", - "version": 1, - "data": { - "devices": [ - { - "ieee": str(zigpy_dev.ieee), - "last_seen": last_seen, - "name": f"{zigpy_dev.manufacturer} {zigpy_dev.model}", - } - ], - }, - } + zigpy_dev.last_seen = last_seen await setup_zha() zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index b19c98548ce..3c8c3e78c0e 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,7 +1,5 @@ """Test ZHA Gateway.""" import asyncio -import math -import time from unittest.mock import patch import pytest @@ -10,10 +8,9 @@ import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting from homeassistant.components.zha.core.group import GroupMember -from homeassistant.components.zha.core.store import TOMBSTONE_LIFETIME from homeassistant.const import Platform -from .common import async_enable_traffic, async_find_group_entity_id, get_zha_gateway +from .common import async_find_group_entity_id, get_zha_gateway from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" @@ -214,62 +211,3 @@ async def test_gateway_create_group_with_id(hass, device_light_1, coordinator): assert len(zha_group.members) == 1 assert zha_group.members[0].device is device_light_1 assert zha_group.group_id == 0x1234 - - -async def test_updating_device_store(hass, zigpy_dev_basic, zha_dev_basic): - """Test saving data after a delay.""" - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None - await async_enable_traffic(hass, [zha_dev_basic]) - - assert zha_dev_basic.last_seen is not None - entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) - assert math.isclose(entry.last_seen, zha_dev_basic.last_seen, rel_tol=1e-06) - - assert zha_dev_basic.last_seen is not None - last_seen = zha_dev_basic.last_seen - - # test that we can't set None as last seen any more - zha_dev_basic.async_update_last_seen(None) - assert math.isclose(last_seen, zha_dev_basic.last_seen, rel_tol=1e-06) - - # test that we won't put None in storage - zigpy_dev_basic.last_seen = None - assert zha_dev_basic.last_seen is None - await zha_gateway.async_update_device_storage() - await hass.async_block_till_done() - entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) - assert math.isclose(entry.last_seen, last_seen, rel_tol=1e-06) - - # test that we can still set a good last_seen - last_seen = time.time() - zha_dev_basic.async_update_last_seen(last_seen) - assert math.isclose(last_seen, zha_dev_basic.last_seen, rel_tol=1e-06) - - # test that we still put good values in storage - await zha_gateway.async_update_device_storage() - await hass.async_block_till_done() - entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) - assert math.isclose(entry.last_seen, last_seen, rel_tol=1e-06) - - -async def test_cleaning_up_storage(hass, zigpy_dev_basic, zha_dev_basic, hass_storage): - """Test cleaning up zha storage and remove stale devices.""" - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None - await async_enable_traffic(hass, [zha_dev_basic]) - - assert zha_dev_basic.last_seen is not None - await zha_gateway.zha_storage.async_save() - await hass.async_block_till_done() - - assert hass_storage["zha.storage"]["data"]["devices"] - device = hass_storage["zha.storage"]["data"]["devices"][0] - assert device["ieee"] == str(zha_dev_basic.ieee) - - zha_dev_basic.device.last_seen = time.time() - TOMBSTONE_LIFETIME - 1 - await zha_gateway.async_update_device_storage() - await hass.async_block_till_done() - await zha_gateway.zha_storage.async_save() - await hass.async_block_till_done() - assert not hass_storage["zha.storage"]["data"]["devices"] From c9aa3c112aef05a147ee36f1df576bf424cfdf17 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 20:05:24 +0200 Subject: [PATCH 2374/3516] Migrate GitHub to new entity naming style (#74903) --- homeassistant/components/github/sensor.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 8dff4b04b01..d17f762fedd 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -89,7 +89,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="pulls_count", - name="Pull Requests", + name="Pull requests", native_unit_of_measurement="Pull Requests", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.MEASUREMENT, @@ -97,7 +97,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="latest_commit", - name="Latest Commit", + name="Latest commit", value_fn=lambda data: data["default_branch_ref"]["commit"]["message"][:255], attr_fn=lambda data: { "sha": data["default_branch_ref"]["commit"]["sha"], @@ -106,7 +106,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="latest_discussion", - name="Latest Discussion", + name="Latest discussion", avabl_fn=lambda data: data["discussion"]["discussions"], value_fn=lambda data: data["discussion"]["discussions"][0]["title"][:255], attr_fn=lambda data: { @@ -116,7 +116,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="latest_release", - name="Latest Release", + name="Latest release", avabl_fn=lambda data: data["release"] is not None, value_fn=lambda data: data["release"]["name"][:255], attr_fn=lambda data: { @@ -126,7 +126,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="latest_issue", - name="Latest Issue", + name="Latest issue", avabl_fn=lambda data: data["issue"]["issues"], value_fn=lambda data: data["issue"]["issues"][0]["title"][:255], attr_fn=lambda data: { @@ -136,7 +136,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="latest_pull_request", - name="Latest Pull Request", + name="Latest pull request", avabl_fn=lambda data: data["pull_request"]["pull_requests"], value_fn=lambda data: data["pull_request"]["pull_requests"][0]["title"][:255], attr_fn=lambda data: { @@ -146,7 +146,7 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = ( ), GitHubSensorEntityDescription( key="latest_tag", - name="Latest Tag", + name="Latest tag", avabl_fn=lambda data: data["refs"]["tags"], value_fn=lambda data: data["refs"]["tags"][0]["name"][:255], attr_fn=lambda data: { @@ -176,6 +176,7 @@ class GitHubSensorEntity(CoordinatorEntity[GitHubDataUpdateCoordinator], SensorE """Defines a GitHub sensor entity.""" _attr_attribution = "Data provided by the GitHub API" + _attr_has_entity_name = True entity_description: GitHubSensorEntityDescription @@ -188,9 +189,6 @@ class GitHubSensorEntity(CoordinatorEntity[GitHubDataUpdateCoordinator], SensorE super().__init__(coordinator=coordinator) self.entity_description = entity_description - self._attr_name = ( - f"{coordinator.data.get('full_name')} {entity_description.name}" - ) self._attr_unique_id = f"{coordinator.data.get('id')}_{entity_description.key}" self._attr_device_info = DeviceInfo( From 59170d3c54bec9ff8ca06dcbe43c5f58d3c77910 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sun, 10 Jul 2022 19:50:54 +0100 Subject: [PATCH 2375/3516] Prepare homekit_controller for _hap._udp.local. (#74857) * Prepare homekit_controller for _hap._udp.local. --- .../homekit_controller/config_flow.py | 7 ++++++- .../homekit_controller/manifest.json | 4 ++-- homeassistant/generated/zeroconf.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit_controller/common.py | 2 +- .../homekit_controller/test_config_flow.py | 19 ++++++++++++++++++- 7 files changed, 34 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 24a04874f20..493dd05a8b2 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -8,6 +8,7 @@ from typing import Any import aiohomekit from aiohomekit.exceptions import AuthenticationError from aiohomekit.model import Accessories, CharacteristicsTypes, ServicesTypes +from aiohomekit.utils import domain_supported, domain_to_name import voluptuous as vol from homeassistant import config_entries @@ -203,8 +204,12 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): hkid = properties[zeroconf.ATTR_PROPERTIES_ID] normalized_hkid = normalize_hkid(hkid) + # If this aiohomekit doesn't support this particular device, ignore it. + if not domain_supported(discovery_info.name): + return self.async_abort(reason="ignored_model") + model = properties["md"] - name = discovery_info.name.replace("._hap._tcp.local.", "") + name = domain_to_name(discovery_info.name) status_flags = int(properties["sf"]) paired = not status_flags & 0x01 diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 6517b078454..a83d6264603 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,8 +3,8 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.7.21"], - "zeroconf": ["_hap._tcp.local."], + "requirements": ["aiohomekit==0.7.22"], + "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], "iot_class": "local_push", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 3c9d21d1d95..5284eef02a7 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -167,6 +167,11 @@ ZEROCONF = { "name": "*z.wave-me*" } ], + "_hap._udp.local.": [ + { + "domain": "homekit_controller" + } + ], "_homekit._tcp.local.": [ { "domain": "homekit" diff --git a/requirements_all.txt b/requirements_all.txt index c25a6dd40eb..6141bb5f450 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.21 +aiohomekit==0.7.22 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 832bfa1bdee..0c879ca216a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.21 +aiohomekit==0.7.22 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 749bd4b0f07..4cad585d135 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -224,7 +224,7 @@ async def device_config_changed(hass, accessories): host="127.0.0.1", addresses=["127.0.0.1"], hostname="mock_hostname", - name="TestDevice", + name="TestDevice._hap._tcp.local.", port=8080, properties={ "md": "TestDevice", diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 170a2a797d0..7b62c1e9d6d 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -141,7 +141,7 @@ def get_device_discovery_info( result = zeroconf.ZeroconfServiceInfo( host="127.0.0.1", hostname=device.description.name, - name=device.description.name, + name=device.description.name + "._hap._tcp.local.", addresses=["127.0.0.1"], port=8080, properties={ @@ -265,6 +265,23 @@ async def test_pair_already_paired_1(hass, controller): assert result["reason"] == "already_paired" +async def test_unknown_domain_type(hass, controller): + """Test that aiohomekit can reject discoveries it doesn't support.""" + device = setup_mock_accessory(controller) + # Flag device as already paired + discovery_info = get_device_discovery_info(device) + discovery_info.name = "TestDevice._music._tap.local." + + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_ZEROCONF}, + data=discovery_info, + ) + assert result["type"] == "abort" + assert result["reason"] == "ignored_model" + + async def test_id_missing(hass, controller): """Test id is missing.""" device = setup_mock_accessory(controller) From c241e876efd6c90162aba7d241acb73f74d0759b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 20:55:54 +0200 Subject: [PATCH 2376/3516] Update url-normalize to 1.4.3 (#74897) --- homeassistant/components/huawei_lte/manifest.json | 2 +- homeassistant/components/syncthru/manifest.json | 2 +- homeassistant/components/zwave_me/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index dd0382d5f55..656c80e5a89 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "huawei-lte-api==1.6.0", "stringcase==1.2.0", - "url-normalize==1.4.1" + "url-normalize==1.4.3" ], "ssdp": [ { diff --git a/homeassistant/components/syncthru/manifest.json b/homeassistant/components/syncthru/manifest.json index 4536e703ce9..f0ffe081faf 100644 --- a/homeassistant/components/syncthru/manifest.json +++ b/homeassistant/components/syncthru/manifest.json @@ -3,7 +3,7 @@ "name": "Samsung SyncThru Printer", "documentation": "https://www.home-assistant.io/integrations/syncthru", "config_flow": true, - "requirements": ["pysyncthru==0.7.10", "url-normalize==1.4.1"], + "requirements": ["pysyncthru==0.7.10", "url-normalize==1.4.3"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:Printer:1", diff --git a/homeassistant/components/zwave_me/manifest.json b/homeassistant/components/zwave_me/manifest.json index f69bbc1eea1..04627583d0e 100644 --- a/homeassistant/components/zwave_me/manifest.json +++ b/homeassistant/components/zwave_me/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave.Me", "documentation": "https://www.home-assistant.io/integrations/zwave_me", "iot_class": "local_push", - "requirements": ["zwave_me_ws==0.2.4", "url-normalize==1.4.1"], + "requirements": ["zwave_me_ws==0.2.4", "url-normalize==1.4.3"], "after_dependencies": ["zeroconf"], "zeroconf": [{ "type": "_hap._tcp.local.", "name": "*z.wave-me*" }], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 6141bb5f450..2f1c35c8e83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2372,7 +2372,7 @@ upcloud-api==2.0.0 # homeassistant.components.huawei_lte # homeassistant.components.syncthru # homeassistant.components.zwave_me -url-normalize==1.4.1 +url-normalize==1.4.3 # homeassistant.components.uscis uscisstatus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c879ca216a..7b24be8441f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1581,7 +1581,7 @@ upcloud-api==2.0.0 # homeassistant.components.huawei_lte # homeassistant.components.syncthru # homeassistant.components.zwave_me -url-normalize==1.4.1 +url-normalize==1.4.3 # homeassistant.components.uvc uvcclient==0.11.0 From c92936cc7b0f56f483e0c7c82fae58729556aec8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 13:21:21 -0600 Subject: [PATCH 2377/3516] Migrate Ambient PWS to new entity naming style (#74743) --- .../components/ambient_station/__init__.py | 4 +- .../ambient_station/binary_sensor.py | 32 ++--- .../components/ambient_station/sensor.py | 112 +++++++++--------- 3 files changed, 74 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index fb4a865a72e..7242c0ba53b 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -198,6 +198,7 @@ class AmbientStation: class AmbientWeatherEntity(Entity): """Define a base Ambient PWS entity.""" + _attr_has_entity_name = True _attr_should_poll = False def __init__( @@ -215,10 +216,9 @@ class AmbientWeatherEntity(Entity): configuration_url=f"https://ambientweather.net/dashboard/{public_device_id}", identifiers={(DOMAIN, mac_address)}, manufacturer="Ambient Weather", - name=station_name, + name=station_name.capitalize(), ) - self._attr_name = f"{station_name}_{description.name}" self._attr_unique_id = f"{mac_address}_{description.key}" self._mac_address = mac_address self.entity_description = description diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index 5fecbce3ded..4380e1839f2 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -143,112 +143,112 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), AmbientBinarySensorDescription( key=TYPE_BATTIN, - name="Interior Battery", + name="Interior battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT10, - name="Soil Monitor Battery 10", + name="Battery 10", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM1, - name="Soil Monitor Battery 1", + name="Soil monitor battery 1", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM2, - name="Soil Monitor Battery 2", + name="Soil monitor battery 2", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM3, - name="Soil Monitor Battery 3", + name="Soil monitor battery 3", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM4, - name="Soil Monitor Battery 4", + name="Soil monitor battery 4", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM5, - name="Soil Monitor Battery 5", + name="Soil monitor battery 5", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM6, - name="Soil Monitor Battery 6", + name="Soil monitor battery 6", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM7, - name="Soil Monitor Battery 7", + name="Soil monitor battery 7", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM8, - name="Soil Monitor Battery 8", + name="Soil monitor battery 8", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM9, - name="Soil Monitor Battery 9", + name="Soil monitor battery 9", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_SM10, - name="Soil Monitor Battery 10", + name="Soil monitor battery 10", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_CO2, - name="CO2 Battery", + name="CO2 battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_BATT_LIGHTNING, - name="Lightning Detector Battery", + name="Lightning detector battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_PM25IN_BATT, - name="PM25 Indoor Battery", + name="PM25 indoor battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, ), AmbientBinarySensorDescription( key=TYPE_PM25_BATT, - name="PM25 Battery", + name="PM25 battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state=0, diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index c837ef6fdec..2944f33938d 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -113,7 +113,7 @@ TYPE_YEARLYRAININ = "yearlyrainin" SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=TYPE_24HOURRAININ, - name="24 Hr Rain", + name="24 hr rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.TOTAL_INCREASING, @@ -126,74 +126,74 @@ SENSOR_DESCRIPTIONS = ( ), SensorEntityDescription( key=TYPE_AQI_PM25_24H, - name="AQI PM2.5 24h Avg", + name="AQI PM2.5 24h avg", device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_AQI_PM25_IN, - name="AQI PM2.5 Indoor", + name="AQI PM2.5 indoor", device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_AQI_PM25_IN_24H, - name="AQI PM2.5 Indoor 24h Avg", + name="AQI PM2.5 indoor 24h avg", device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_BAROMABSIN, - name="Abs Pressure", + name="Abs pressure", native_unit_of_measurement=PRESSURE_INHG, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_BAROMRELIN, - name="Rel Pressure", + name="Rel pressure", native_unit_of_measurement=PRESSURE_INHG, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_CO2, - name="co2", + name="CO2", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_DAILYRAININ, - name="Daily Rain", + name="Daily rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_DEWPOINT, - name="Dew Point", + name="Dew point", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_EVENTRAININ, - name="Event Rain", + name="Event rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_FEELSLIKE, - name="Feels Like", + name="Feels like", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HOURLYRAININ, - name="Hourly Rain Rate", + name="Hourly rain rate", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, @@ -266,60 +266,60 @@ SENSOR_DESCRIPTIONS = ( ), SensorEntityDescription( key=TYPE_HUMIDITYIN, - name="Humidity In", + name="Humidity in", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_LASTRAIN, - name="Last Rain", + name="Last rain", icon="mdi:water", device_class=SensorDeviceClass.TIMESTAMP, ), SensorEntityDescription( key=TYPE_LIGHTNING_PER_DAY, - name="Lightning Strikes Per Day", + name="Lightning strikes per day", icon="mdi:lightning-bolt", native_unit_of_measurement="strikes", state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_LIGHTNING_PER_HOUR, - name="Lightning Strikes Per Hour", + name="Lightning strikes per hour", icon="mdi:lightning-bolt", native_unit_of_measurement="strikes", state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_MAXDAILYGUST, - name="Max Gust", + name="Max gust", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_MONTHLYRAININ, - name="Monthly Rain", + name="Monthly rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_PM25_24H, - name="PM25 24h Avg", + name="PM25 24h avg", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM25, ), SensorEntityDescription( key=TYPE_PM25_IN, - name="PM25 Indoor", + name="PM25 indoor", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_PM25_IN_24H, - name="PM25 Indoor 24h Avg", + name="PM25 indoor 24h avg", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM25, ), @@ -332,144 +332,144 @@ SENSOR_DESCRIPTIONS = ( ), SensorEntityDescription( key=TYPE_SOILHUM10, - name="Soil Humidity 10", + name="Soil humidity 10", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM1, - name="Soil Humidity 1", + name="Soil humidity 1", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM2, - name="Soil Humidity 2", + name="Soil humidity 2", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM3, - name="Soil Humidity 3", + name="Soil humidity 3", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM4, - name="Soil Humidity 4", + name="Soil humidity 4", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM5, - name="Soil Humidity 5", + name="Soil humidity 5", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM6, - name="Soil Humidity 6", + name="Soil humidity 6", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM7, - name="Soil Humidity 7", + name="Soil humidity 7", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM8, - name="Soil Humidity 8", + name="Soil humidity 8", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILHUM9, - name="Soil Humidity 9", + name="Soil humidity 9", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, ), SensorEntityDescription( key=TYPE_SOILTEMP10F, - name="Soil Temp 10", + name="Soil temp 10", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP1F, - name="Soil Temp 1", + name="Soil temp 1", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP2F, - name="Soil Temp 2", + name="Soil temp 2", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP3F, - name="Soil Temp 3", + name="Soil temp 3", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP4F, - name="Soil Temp 4", + name="Soil temp 4", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP5F, - name="Soil Temp 5", + name="Soil temp 5", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP6F, - name="Soil Temp 6", + name="Soil temp 6", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP7F, - name="Soil Temp 7", + name="Soil temp 7", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP8F, - name="Soil Temp 8", + name="Soil temp 8", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP9F, - name="Soil Temp 9", + name="Soil temp 9", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOLARRADIATION, - name="Solar Rad", + name="Solar rad", native_unit_of_measurement=IRRADIATION_WATTS_PER_SQUARE_METER, device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOLARRADIATION_LX, - name="Solar Rad", + name="Solar rad", native_unit_of_measurement=LIGHT_LUX, device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, @@ -553,85 +553,85 @@ SENSOR_DESCRIPTIONS = ( ), SensorEntityDescription( key=TYPE_TEMPINF, - name="Inside Temp", + name="Inside temp", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_TOTALRAININ, - name="Lifetime Rain", + name="Lifetime rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_UV, - name="UV Index", + name="UV index", native_unit_of_measurement="Index", device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_WEEKLYRAININ, - name="Weekly Rain", + name="Weekly rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_WINDDIR, - name="Wind Dir", + name="Wind dir", icon="mdi:weather-windy", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( key=TYPE_WINDDIR_AVG10M, - name="Wind Dir Avg 10m", + name="Wind dir avg 10m", icon="mdi:weather-windy", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( key=TYPE_WINDDIR_AVG2M, - name="Wind Dir Avg 2m", + name="Wind dir avg 2m", icon="mdi:weather-windy", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( key=TYPE_WINDGUSTDIR, - name="Gust Dir", + name="Gust dir", icon="mdi:weather-windy", native_unit_of_measurement=DEGREE, ), SensorEntityDescription( key=TYPE_WINDGUSTMPH, - name="Wind Gust", + name="Wind gust", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_WINDSPDMPH_AVG10M, - name="Wind Avg 10m", + name="Wind avg 10m", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, ), SensorEntityDescription( key=TYPE_WINDSPDMPH_AVG2M, - name="Wind Avg 2m", + name="Wind avg 2m", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, ), SensorEntityDescription( key=TYPE_WINDSPEEDMPH, - name="Wind Speed", + name="Wind speed", icon="mdi:weather-windy", native_unit_of_measurement=SPEED_MILES_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_YEARLYRAININ, - name="Yearly Rain", + name="Yearly rain", icon="mdi:water", native_unit_of_measurement=PRECIPITATION_INCHES, state_class=SensorStateClass.TOTAL_INCREASING, From a4d5ecb8ec4553b44bfa6fdfa2d93feb3244aa7b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 13:27:01 -0600 Subject: [PATCH 2378/3516] Migrate RainMachine to new entity naming style (#74754) --- .../components/rainmachine/__init__.py | 5 +++-- .../components/rainmachine/binary_sensor.py | 18 +++++++++--------- homeassistant/components/rainmachine/sensor.py | 10 +++++----- homeassistant/components/rainmachine/switch.py | 6 ++++-- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 7647a330a30..3feeac7a827 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -401,6 +401,8 @@ async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: class RainMachineEntity(CoordinatorEntity): """Define a generic RainMachine entity.""" + _attr_has_entity_name = True + def __init__( self, entry: ConfigEntry, @@ -415,7 +417,7 @@ class RainMachineEntity(CoordinatorEntity): 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), + name=str(controller.name).capitalize(), manufacturer="RainMachine", model=( f"Version {controller.hardware_version} " @@ -424,7 +426,6 @@ class RainMachineEntity(CoordinatorEntity): sw_version=controller.software_version, ) self._attr_extra_state_attributes = {} - self._attr_name = f"{controller.name} {description.name}" self._attr_unique_id = f"{controller.mac}_{description.key}" self._controller = controller self.entity_description = description diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 7a13515db3b..6ba374a28ba 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -43,14 +43,14 @@ class RainMachineBinarySensorDescription( BINARY_SENSOR_DESCRIPTIONS = ( RainMachineBinarySensorDescription( key=TYPE_FLOW_SENSOR, - name="Flow Sensor", + name="Flow sensor", icon="mdi:water-pump", api_category=DATA_PROVISION_SETTINGS, data_key="useFlowSensor", ), RainMachineBinarySensorDescription( key=TYPE_FREEZE, - name="Freeze Restrictions", + name="Freeze restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_CURRENT, @@ -58,7 +58,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_FREEZE_PROTECTION, - name="Freeze Protection", + name="Freeze protection", icon="mdi:weather-snowy", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_UNIVERSAL, @@ -66,7 +66,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_HOT_DAYS, - name="Extra Water on Hot Days", + name="Extra water on hot days", icon="mdi:thermometer-lines", entity_category=EntityCategory.DIAGNOSTIC, api_category=DATA_RESTRICTIONS_UNIVERSAL, @@ -74,7 +74,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_HOURLY, - name="Hourly Restrictions", + name="Hourly restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -83,7 +83,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_MONTH, - name="Month Restrictions", + name="Month restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -92,7 +92,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_RAINDELAY, - name="Rain Delay Restrictions", + name="Rain delay restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -101,7 +101,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_RAINSENSOR, - name="Rain Sensor Restrictions", + name="Rain sensor restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -110,7 +110,7 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), RainMachineBinarySensorDescription( key=TYPE_WEEKDAY, - name="Weekday Restrictions", + name="Weekday restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 7550756f8c4..5a5329ad1fa 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -68,7 +68,7 @@ class RainMachineSensorDescriptionUid( SENSOR_DESCRIPTIONS = ( RainMachineSensorDescriptionApiCategory( key=TYPE_FLOW_SENSOR_CLICK_M3, - name="Flow Sensor Clicks per Cubic Meter", + name="Flow sensor clicks per cubic meter", icon="mdi:water-pump", native_unit_of_measurement=f"clicks/{VOLUME_CUBIC_METERS}", entity_category=EntityCategory.DIAGNOSTIC, @@ -79,7 +79,7 @@ SENSOR_DESCRIPTIONS = ( ), RainMachineSensorDescriptionApiCategory( key=TYPE_FLOW_SENSOR_CONSUMED_LITERS, - name="Flow Sensor Consumed Liters", + name="Flow sensor consumed liters", icon="mdi:water-pump", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="liter", @@ -90,7 +90,7 @@ SENSOR_DESCRIPTIONS = ( ), RainMachineSensorDescriptionApiCategory( key=TYPE_FLOW_SENSOR_START_INDEX, - name="Flow Sensor Start Index", + name="Flow sensor start index", icon="mdi:water-pump", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="index", @@ -100,7 +100,7 @@ SENSOR_DESCRIPTIONS = ( ), RainMachineSensorDescriptionApiCategory( key=TYPE_FLOW_SENSOR_WATERING_CLICKS, - name="Flow Sensor Clicks", + name="Flow sensor clicks", icon="mdi:water-pump", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="clicks", @@ -111,7 +111,7 @@ SENSOR_DESCRIPTIONS = ( ), RainMachineSensorDescriptionApiCategory( key=TYPE_FREEZE_TEMP, - name="Freeze Protect Temperature", + name="Freeze protect temperature", icon="mdi:thermometer", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=TEMP_CELSIUS, diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 8d339682305..aa91f529b5b 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -149,6 +149,8 @@ async def async_setup_entry( ("zone", zone_coordinator, RainMachineZone, RainMachineZoneEnabled), ): for uid, data in coordinator.data.items(): + name = data["name"].capitalize() + # Add a switch to start/stop the program or zone: entities.append( switch_class( @@ -157,7 +159,7 @@ async def async_setup_entry( controller, RainMachineSwitchDescription( key=f"{kind}_{uid}", - name=data["name"], + name=name, icon="mdi:water", uid=uid, ), @@ -172,7 +174,7 @@ async def async_setup_entry( controller, RainMachineSwitchDescription( key=f"{kind}_{uid}_enabled", - name=f"{data['name']} Enabled", + name=f"{name} enabled", entity_category=EntityCategory.CONFIG, icon="mdi:cog", uid=uid, From ae4f2a0e3481aadddedc0f5d8b529dd84099f454 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 13:37:14 -0600 Subject: [PATCH 2379/3516] Fix incorrect new entity naming for Guardian (#74912) --- homeassistant/components/guardian/__init__.py | 4 ++-- homeassistant/components/guardian/binary_sensor.py | 6 +++--- homeassistant/components/guardian/switch.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index b07d8ec5b3f..3707bf9d2eb 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -409,7 +409,7 @@ class PairedSensorEntity(GuardianEntity): identifiers={(DOMAIN, paired_sensor_uid)}, manufacturer="Elexa", model=coordinator.data["codename"], - name=f"Guardian Paired Sensor {paired_sensor_uid}", + name=f"Guardian paired sensor {paired_sensor_uid}", via_device=(DOMAIN, entry.data[CONF_UID]), ) self._attr_unique_id = f"{paired_sensor_uid}_{description.key}" @@ -436,7 +436,7 @@ class ValveControllerEntity(GuardianEntity): identifiers={(DOMAIN, entry.data[CONF_UID])}, manufacturer="Elexa", model=coordinators[API_SYSTEM_DIAGNOSTICS].data["firmware"], - name=f"Guardian Valve Controller {entry.data[CONF_UID]}", + name=f"Guardian valve controller {entry.data[CONF_UID]}", ) self._attr_unique_id = f"{entry.data[CONF_UID]}_{description.key}" self.coordinators = coordinators diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index 1d7195a8f17..dc9febcfa91 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -32,18 +32,18 @@ SENSOR_KIND_MOVED = "moved" SENSOR_DESCRIPTION_AP_ENABLED = BinarySensorEntityDescription( key=SENSOR_KIND_AP_INFO, - name="Onboard AP Enabled", + name="Onboard AP enabled", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, ) SENSOR_DESCRIPTION_LEAK_DETECTED = BinarySensorEntityDescription( key=SENSOR_KIND_LEAK_DETECTED, - name="Leak Detected", + name="Leak detected", device_class=BinarySensorDeviceClass.MOISTURE, ) SENSOR_DESCRIPTION_MOVED = BinarySensorEntityDescription( key=SENSOR_KIND_MOVED, - name="Recently Moved", + name="Recently moved", device_class=BinarySensorDeviceClass.MOVING, entity_category=EntityCategory.DIAGNOSTIC, ) diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 9a4f70fd3d2..485b0a3ffbc 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -25,7 +25,7 @@ SWITCH_KIND_VALVE = "valve" SWITCH_DESCRIPTION_VALVE = SwitchEntityDescription( key=SWITCH_KIND_VALVE, - name="Valve Controller", + name="Valve controller", icon="mdi:water", ) From f95c9d0f02ca9a0f740d212e1d6d6969c65964ad Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 13:38:00 -0600 Subject: [PATCH 2380/3516] Migrate AirVisual to new entity naming style (#74753) --- homeassistant/components/airvisual/sensor.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 52d046cef42..e28a11666da 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -62,20 +62,20 @@ SENSOR_KIND_VOC = "voc" GEOGRAPHY_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_KIND_LEVEL, - name="Air Pollution Level", + name="Air pollution level", device_class=DEVICE_CLASS_POLLUTANT_LEVEL, icon="mdi:gauge", ), SensorEntityDescription( key=SENSOR_KIND_AQI, - name="Air Quality Index", + name="Air quality index", device_class=SensorDeviceClass.AQI, native_unit_of_measurement="AQI", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_KIND_POLLUTANT, - name="Main Pollutant", + name="Main pollutant", device_class=DEVICE_CLASS_POLLUTANT_LABEL, icon="mdi:chemical-weapon", ), @@ -85,7 +85,7 @@ GEOGRAPHY_SENSOR_LOCALES = {"cn": "Chinese", "us": "U.S."} NODE_PRO_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_KIND_AQI, - name="Air Quality Index", + name="Air quality index", device_class=SensorDeviceClass.AQI, native_unit_of_measurement="AQI", state_class=SensorStateClass.MEASUREMENT, @@ -292,6 +292,8 @@ class AirVisualGeographySensor(AirVisualEntity, SensorEntity): class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): """Define an AirVisual sensor related to a Node/Pro unit.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, @@ -301,9 +303,6 @@ class AirVisualNodeProSensor(AirVisualEntity, SensorEntity): """Initialize.""" super().__init__(coordinator, entry, description) - self._attr_name = ( - f"{coordinator.data['settings']['node_name']} Node/Pro: {description.name}" - ) self._attr_unique_id = f"{coordinator.data['serial_number']}_{description.key}" @property From 5971ab65493fa3c5d00061359c469ece3f915988 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 13:40:06 -0600 Subject: [PATCH 2381/3516] Migrate Flo to new entity naming style (#74744) --- homeassistant/components/flo/binary_sensor.py | 4 +- homeassistant/components/flo/entity.py | 3 +- homeassistant/components/flo/sensor.py | 10 ++-- homeassistant/components/flo/switch.py | 2 +- tests/components/flo/test_binary_sensor.py | 11 +++-- tests/components/flo/test_sensor.py | 49 ++++++++++++------- tests/components/flo/test_services.py | 2 +- tests/components/flo/test_switch.py | 2 +- 8 files changed, 52 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/flo/binary_sensor.py b/homeassistant/components/flo/binary_sensor.py index c395e6136fe..f5c051d1a70 100644 --- a/homeassistant/components/flo/binary_sensor.py +++ b/homeassistant/components/flo/binary_sensor.py @@ -48,7 +48,7 @@ class FloPendingAlertsBinarySensor(FloEntity, BinarySensorEntity): def __init__(self, device): """Initialize the pending alerts binary sensor.""" - super().__init__("pending_system_alerts", "Pending System Alerts", device) + super().__init__("pending_system_alerts", "Pending system alerts", device) @property def is_on(self): @@ -74,7 +74,7 @@ class FloWaterDetectedBinarySensor(FloEntity, BinarySensorEntity): def __init__(self, device): """Initialize the pending alerts binary sensor.""" - super().__init__("water_detected", "Water Detected", device) + super().__init__("water_detected", "Water detected", device) @property def is_on(self): diff --git a/homeassistant/components/flo/entity.py b/homeassistant/components/flo/entity.py index 280f19dc57e..39ad57f5c03 100644 --- a/homeassistant/components/flo/entity.py +++ b/homeassistant/components/flo/entity.py @@ -14,6 +14,7 @@ class FloEntity(Entity): """A base class for Flo entities.""" _attr_force_update = False + _attr_has_entity_name = True _attr_should_poll = False def __init__( @@ -38,7 +39,7 @@ class FloEntity(Entity): identifiers={(FLO_DOMAIN, self._device.id)}, manufacturer=self._device.manufacturer, model=self._device.model, - name=self._device.device_name, + name=self._device.device_name.capitalize(), sw_version=self._device.firmware_version, ) diff --git a/homeassistant/components/flo/sensor.py b/homeassistant/components/flo/sensor.py index a5d54386633..e7fbd293bd1 100644 --- a/homeassistant/components/flo/sensor.py +++ b/homeassistant/components/flo/sensor.py @@ -22,12 +22,12 @@ from .entity import FloEntity WATER_ICON = "mdi:water" GAUGE_ICON = "mdi:gauge" -NAME_DAILY_USAGE = "Today's Water Usage" -NAME_CURRENT_SYSTEM_MODE = "Current System Mode" -NAME_FLOW_RATE = "Water Flow Rate" -NAME_WATER_TEMPERATURE = "Water Temperature" +NAME_DAILY_USAGE = "Today's water usage" +NAME_CURRENT_SYSTEM_MODE = "Current system mode" +NAME_FLOW_RATE = "Water flow rate" +NAME_WATER_TEMPERATURE = "Water temperature" NAME_AIR_TEMPERATURE = "Temperature" -NAME_WATER_PRESSURE = "Water Pressure" +NAME_WATER_PRESSURE = "Water pressure" NAME_HUMIDITY = "Humidity" NAME_BATTERY = "Battery" diff --git a/homeassistant/components/flo/switch.py b/homeassistant/components/flo/switch.py index 01ab2c9259e..884b76fc64e 100644 --- a/homeassistant/components/flo/switch.py +++ b/homeassistant/components/flo/switch.py @@ -68,7 +68,7 @@ class FloSwitch(FloEntity, SwitchEntity): def __init__(self, device: FloDeviceDataUpdateCoordinator) -> None: """Initialize the Flo switch.""" - super().__init__("shutoff_valve", "Shutoff Valve", device) + super().__init__("shutoff_valve", "Shutoff valve", device) self._state = self._device.last_known_valve_state == "open" @property diff --git a/tests/components/flo/test_binary_sensor.py b/tests/components/flo/test_binary_sensor.py index b6a8abf727c..36901584cf6 100644 --- a/tests/components/flo/test_binary_sensor.py +++ b/tests/components/flo/test_binary_sensor.py @@ -22,12 +22,17 @@ async def test_binary_sensors(hass, config_entry, aioclient_mock_fixture): assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2 - valve_state = hass.states.get("binary_sensor.pending_system_alerts") + valve_state = hass.states.get( + "binary_sensor.smart_water_shutoff_pending_system_alerts" + ) assert valve_state.state == STATE_ON assert valve_state.attributes.get("info") == 0 assert valve_state.attributes.get("warning") == 2 assert valve_state.attributes.get("critical") == 0 - assert valve_state.attributes.get(ATTR_FRIENDLY_NAME) == "Pending System Alerts" + assert ( + valve_state.attributes.get(ATTR_FRIENDLY_NAME) + == "Smart water shutoff Pending system alerts" + ) - detector_state = hass.states.get("binary_sensor.water_detected") + detector_state = hass.states.get("binary_sensor.kitchen_sink_water_detected") assert detector_state.state == STATE_OFF diff --git a/tests/components/flo/test_sensor.py b/tests/components/flo/test_sensor.py index 0a7cf16ecf5..b5439241d33 100644 --- a/tests/components/flo/test_sensor.py +++ b/tests/components/flo/test_sensor.py @@ -18,48 +18,63 @@ async def test_sensors(hass, config_entry, aioclient_mock_fixture): assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2 # we should have 5 entities for the valve - assert hass.states.get("sensor.current_system_mode").state == "home" - - assert hass.states.get("sensor.today_s_water_usage").state == "3.7" + print(hass.states) assert ( - hass.states.get("sensor.today_s_water_usage").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.smart_water_shutoff_current_system_mode").state + == "home" + ) + + assert ( + hass.states.get("sensor.smart_water_shutoff_today_s_water_usage").state == "3.7" + ) + assert ( + hass.states.get("sensor.smart_water_shutoff_today_s_water_usage").attributes[ + ATTR_STATE_CLASS + ] == SensorStateClass.TOTAL_INCREASING ) - assert hass.states.get("sensor.water_flow_rate").state == "0" + assert hass.states.get("sensor.smart_water_shutoff_water_flow_rate").state == "0" assert ( - hass.states.get("sensor.water_flow_rate").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.smart_water_shutoff_water_flow_rate").attributes[ + ATTR_STATE_CLASS + ] == SensorStateClass.MEASUREMENT ) - assert hass.states.get("sensor.water_pressure").state == "54.2" + assert hass.states.get("sensor.smart_water_shutoff_water_pressure").state == "54.2" assert ( - hass.states.get("sensor.water_pressure").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.smart_water_shutoff_water_pressure").attributes[ + ATTR_STATE_CLASS + ] == SensorStateClass.MEASUREMENT ) - assert hass.states.get("sensor.water_temperature").state == "21" + assert hass.states.get("sensor.smart_water_shutoff_water_temperature").state == "21" assert ( - hass.states.get("sensor.water_temperature").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.smart_water_shutoff_water_temperature").attributes[ + ATTR_STATE_CLASS + ] == SensorStateClass.MEASUREMENT ) # and 3 entities for the detector - assert hass.states.get("sensor.temperature").state == "16" + print(hass.states) + assert hass.states.get("sensor.kitchen_sink_temperature").state == "16" assert ( - hass.states.get("sensor.temperature").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.kitchen_sink_temperature").attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT ) - assert hass.states.get("sensor.humidity").state == "43" + assert hass.states.get("sensor.kitchen_sink_humidity").state == "43" assert ( - hass.states.get("sensor.humidity").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.kitchen_sink_humidity").attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT ) - assert hass.states.get("sensor.battery").state == "100" + assert hass.states.get("sensor.kitchen_sink_battery").state == "100" assert ( - hass.states.get("sensor.battery").attributes[ATTR_STATE_CLASS] + hass.states.get("sensor.kitchen_sink_battery").attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT ) @@ -82,7 +97,7 @@ async def test_manual_update_entity( await hass.services.async_call( "homeassistant", "update_entity", - {ATTR_ENTITY_ID: ["sensor.current_system_mode"]}, + {ATTR_ENTITY_ID: ["sensor.smart_water_shutoff_current_system_mode"]}, blocking=True, ) assert aioclient_mock.call_count == call_count + 3 diff --git a/tests/components/flo/test_services.py b/tests/components/flo/test_services.py index 720d0596b22..c7e2e070745 100644 --- a/tests/components/flo/test_services.py +++ b/tests/components/flo/test_services.py @@ -17,7 +17,7 @@ from homeassistant.setup import async_setup_component from .common import TEST_PASSWORD, TEST_USER_ID -SWITCH_ENTITY_ID = "switch.shutoff_valve" +SWITCH_ENTITY_ID = "switch.smart_water_shutoff_shutoff_valve" async def test_services(hass, config_entry, aioclient_mock_fixture, aioclient_mock): diff --git a/tests/components/flo/test_switch.py b/tests/components/flo/test_switch.py index cc6ab7e3a7e..5f97489fcde 100644 --- a/tests/components/flo/test_switch.py +++ b/tests/components/flo/test_switch.py @@ -17,7 +17,7 @@ async def test_valve_switches(hass, config_entry, aioclient_mock_fixture): assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2 - entity_id = "switch.shutoff_valve" + entity_id = "switch.smart_water_shutoff_shutoff_valve" assert hass.states.get(entity_id).state == STATE_ON await hass.services.async_call( From edf304718cefc0f218c55f1170f038f82801ac1b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 13:41:12 -0600 Subject: [PATCH 2382/3516] Migrate Notion to new entity naming style (#74746) --- homeassistant/components/notion/__init__.py | 12 +++++++++--- homeassistant/components/notion/binary_sensor.py | 14 +++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 49277740f56..2a73d12d946 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -118,13 +118,18 @@ def _async_register_new_bridge( hass: HomeAssistant, bridge: dict, entry: ConfigEntry ) -> None: """Register a new bridge.""" + if name := bridge["name"]: + bridge_name = name.capitalize() + else: + bridge_name = bridge["id"] + device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, bridge["hardware_id"])}, manufacturer="Silicon Labs", model=bridge["hardware_revision"], - name=bridge["name"] or bridge["id"], + name=bridge_name, sw_version=bridge["firmware_version"]["wifi"], ) @@ -132,6 +137,8 @@ def _async_register_new_bridge( class NotionEntity(CoordinatorEntity): """Define a base Notion entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, @@ -150,13 +157,12 @@ class NotionEntity(CoordinatorEntity): identifiers={(DOMAIN, sensor["hardware_id"])}, manufacturer="Silicon Labs", model=sensor["hardware_revision"], - name=str(sensor["name"]), + name=str(sensor["name"]).capitalize(), sw_version=sensor["firmware_version"], via_device=(DOMAIN, bridge.get("hardware_id")), ) self._attr_extra_state_attributes = {} - self._attr_name = f'{sensor["name"]}: {description.name}' self._attr_unique_id = ( f'{sensor_id}_{coordinator.data["tasks"][task_id]["task_type"]}' ) diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index 57c70849a9a..2a34724837d 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -48,7 +48,7 @@ class NotionBinarySensorDescription( BINARY_SENSOR_DESCRIPTIONS = ( NotionBinarySensorDescription( key=SENSOR_BATTERY, - name="Low Battery", + name="Low battery", device_class=BinarySensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, on_state="critical", @@ -61,13 +61,13 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), NotionBinarySensorDescription( key=SENSOR_GARAGE_DOOR, - name="Garage Door", + name="Garage door", device_class=BinarySensorDeviceClass.GARAGE_DOOR, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_LEAK, - name="Leak Detector", + name="Leak detector", device_class=BinarySensorDeviceClass.MOISTURE, on_state="leak", ), @@ -86,25 +86,25 @@ BINARY_SENSOR_DESCRIPTIONS = ( ), NotionBinarySensorDescription( key=SENSOR_SLIDING, - name="Sliding Door/Window", + name="Sliding door/window", device_class=BinarySensorDeviceClass.DOOR, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_SMOKE_CO, - name="Smoke/Carbon Monoxide Detector", + name="Smoke/Carbon monoxide detector", device_class=BinarySensorDeviceClass.SMOKE, on_state="alarm", ), NotionBinarySensorDescription( key=SENSOR_WINDOW_HINGED_HORIZONTAL, - name="Hinged Window", + name="Hinged window", device_class=BinarySensorDeviceClass.WINDOW, on_state="open", ), NotionBinarySensorDescription( key=SENSOR_WINDOW_HINGED_VERTICAL, - name="Hinged Window", + name="Hinged window", device_class=BinarySensorDeviceClass.WINDOW, on_state="open", ), From 98a27ed3ed6e0b62aac8e5e0e1ebb3ad2139d028 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 13:48:58 -0600 Subject: [PATCH 2383/3516] Remove old RainMachine service descriptions (#74920) --- .../components/rainmachine/services.yaml | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/homeassistant/components/rainmachine/services.yaml b/homeassistant/components/rainmachine/services.yaml index 7dcbe2cd8d7..198aec94a22 100644 --- a/homeassistant/components/rainmachine/services.yaml +++ b/homeassistant/components/rainmachine/services.yaml @@ -1,32 +1,4 @@ # Describes the format for available RainMachine services -disable_program: - name: Disable Program - description: Disable a program - target: - entity: - integration: rainmachine - domain: switch -disable_zone: - name: Disable Zone - description: Disable a zone - target: - entity: - integration: rainmachine - domain: switch -enable_program: - name: Enable Program - description: Enable a program - target: - entity: - integration: rainmachine - domain: switch -enable_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 From 1aeb15050eefa56e6f7ddf74ae2186fd8f66b0d1 Mon Sep 17 00:00:00 2001 From: Thijs W Date: Sun, 10 Jul 2022 21:52:49 +0200 Subject: [PATCH 2384/3516] Bump afsapi to 0.2.5 (#74907) --- homeassistant/components/frontier_silicon/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/frontier_silicon/manifest.json b/homeassistant/components/frontier_silicon/manifest.json index 20092b941a9..12fb5145aa0 100644 --- a/homeassistant/components/frontier_silicon/manifest.json +++ b/homeassistant/components/frontier_silicon/manifest.json @@ -2,7 +2,7 @@ "domain": "frontier_silicon", "name": "Frontier Silicon", "documentation": "https://www.home-assistant.io/integrations/frontier_silicon", - "requirements": ["afsapi==0.2.4"], + "requirements": ["afsapi==0.2.5"], "codeowners": ["@wlcrs"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 2f1c35c8e83..9915f03e8f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -89,7 +89,7 @@ adguardhome==0.5.1 advantage_air==0.3.1 # homeassistant.components.frontier_silicon -afsapi==0.2.4 +afsapi==0.2.5 # homeassistant.components.agent_dvr agent-py==0.0.23 From 23f2e9014e989ffa3c56ec4cf37b9647a39a925b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 21:59:38 +0200 Subject: [PATCH 2385/3516] Update feedparser to 6.0.10 (#74913) --- homeassistant/components/feedreader/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/feedreader/manifest.json b/homeassistant/components/feedreader/manifest.json index 1a9bb05e140..9a50a905922 100644 --- a/homeassistant/components/feedreader/manifest.json +++ b/homeassistant/components/feedreader/manifest.json @@ -2,7 +2,7 @@ "domain": "feedreader", "name": "Feedreader", "documentation": "https://www.home-assistant.io/integrations/feedreader", - "requirements": ["feedparser==6.0.2"], + "requirements": ["feedparser==6.0.10"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["feedparser", "sgmllib3k"] diff --git a/requirements_all.txt b/requirements_all.txt index 9915f03e8f8..c136010056a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -638,7 +638,7 @@ faadelays==0.0.7 fastdotcom==0.0.3 # homeassistant.components.feedreader -feedparser==6.0.2 +feedparser==6.0.10 # homeassistant.components.fibaro fiblary3==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7b24be8441f..dd29bcded25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -459,7 +459,7 @@ epson-projector==0.4.2 faadelays==0.0.7 # homeassistant.components.feedreader -feedparser==6.0.2 +feedparser==6.0.10 # homeassistant.components.fibaro fiblary3==0.1.8 From bb4b2014fc3bd8b1ce7f294ccf6fcc069ef00da5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 22:03:19 +0200 Subject: [PATCH 2386/3516] Migrate Wiz to new entity naming style (#74911) --- homeassistant/components/wiz/binary_sensor.py | 2 +- homeassistant/components/wiz/entity.py | 3 ++- homeassistant/components/wiz/number.py | 5 ++--- homeassistant/components/wiz/sensor.py | 5 ++--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/wiz/binary_sensor.py b/homeassistant/components/wiz/binary_sensor.py index 1ecb3125215..538bd3a741d 100644 --- a/homeassistant/components/wiz/binary_sensor.py +++ b/homeassistant/components/wiz/binary_sensor.py @@ -66,12 +66,12 @@ class WizOccupancyEntity(WizEntity, BinarySensorEntity): """Representation of WiZ Occupancy sensor.""" _attr_device_class = BinarySensorDeviceClass.OCCUPANCY + _attr_name = "Occupancy" def __init__(self, wiz_data: WizData, name: str) -> None: """Initialize an WiZ device.""" super().__init__(wiz_data, name) self._attr_unique_id = OCCUPANCY_UNIQUE_ID.format(self._device.mac) - self._attr_name = f"{name} Occupancy" self._async_update_attrs() @callback diff --git a/homeassistant/components/wiz/entity.py b/homeassistant/components/wiz/entity.py index c78f3e3b37b..633fb71f165 100644 --- a/homeassistant/components/wiz/entity.py +++ b/homeassistant/components/wiz/entity.py @@ -21,13 +21,14 @@ from .models import WizData class WizEntity(CoordinatorEntity[DataUpdateCoordinator[Optional[float]]], Entity): """Representation of WiZ entity.""" + _attr_has_entity_name = True + def __init__(self, wiz_data: WizData, name: str) -> None: """Initialize a WiZ entity.""" super().__init__(wiz_data.coordinator) self._device = wiz_data.bulb bulb_type: BulbType = self._device.bulbtype self._attr_unique_id = self._device.mac - self._attr_name = name self._attr_device_info = DeviceInfo( connections={(CONNECTION_NETWORK_MAC, self._device.mac)}, name=name, diff --git a/homeassistant/components/wiz/number.py b/homeassistant/components/wiz/number.py index d2f68fcf7c3..9fd700f8f9c 100644 --- a/homeassistant/components/wiz/number.py +++ b/homeassistant/components/wiz/number.py @@ -52,7 +52,7 @@ NUMBERS: tuple[WizNumberEntityDescription, ...] = ( native_max_value=200, native_step=1, icon="mdi:speedometer", - name="Effect Speed", + name="Effect speed", value_fn=lambda device: cast(Optional[int], device.state.get_speed()), set_value_fn=_async_set_speed, required_feature="effect", @@ -63,7 +63,7 @@ NUMBERS: tuple[WizNumberEntityDescription, ...] = ( native_max_value=100, native_step=1, icon="mdi:floor-lamp-dual", - name="Dual Head Ratio", + name="Dual head ratio", value_fn=lambda device: cast(Optional[int], device.state.get_ratio()), set_value_fn=_async_set_ratio, required_feature="dual_head", @@ -98,7 +98,6 @@ class WizSpeedNumber(WizEntity, NumberEntity): super().__init__(wiz_data, name) self.entity_description = description self._attr_unique_id = f"{self._device.mac}_{description.key}" - self._attr_name = f"{name} {description.name}" self._async_update_attrs() @property diff --git a/homeassistant/components/wiz/sensor.py b/homeassistant/components/wiz/sensor.py index 11f3933fd16..d2042d6ea9c 100644 --- a/homeassistant/components/wiz/sensor.py +++ b/homeassistant/components/wiz/sensor.py @@ -20,7 +20,7 @@ from .models import WizData SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="rssi", - name="Signal Strength", + name="Signal strength", entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, @@ -33,7 +33,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( POWER_SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="power", - name="Current Power", + name="Current power", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, @@ -73,7 +73,6 @@ class WizSensor(WizEntity, SensorEntity): super().__init__(wiz_data, name) self.entity_description = description self._attr_unique_id = f"{self._device.mac}_{description.key}" - self._attr_name = f"{name} {description.name}" self._async_update_attrs() @callback From c5253d3da039713c204a702228e44c8dff7bc449 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 22:05:00 +0200 Subject: [PATCH 2387/3516] Migrate Geocaching to new entity naming style (#74899) --- homeassistant/components/geocaching/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/geocaching/sensor.py b/homeassistant/components/geocaching/sensor.py index 0c719c463a4..134877d7509 100644 --- a/homeassistant/components/geocaching/sensor.py +++ b/homeassistant/components/geocaching/sensor.py @@ -91,6 +91,7 @@ class GeocachingSensor( """Representation of a Sensor.""" entity_description: GeocachingSensorEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -100,9 +101,6 @@ class GeocachingSensor( """Initialize the Geocaching sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = ( - f"Geocaching {coordinator.data.user.username} {description.name}" - ) self._attr_unique_id = ( f"{coordinator.data.user.reference_code}_{description.key}" ) From b070bb8ef097cc221ae16c85722e6e0f8cf3e4b0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 22:05:54 +0200 Subject: [PATCH 2388/3516] Migrate Supervisor integration to new entity naming style (#74906) --- homeassistant/components/hassio/binary_sensor.py | 2 +- homeassistant/components/hassio/entity.py | 13 ++++++++----- homeassistant/components/hassio/sensor.py | 6 +++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/hassio/binary_sensor.py b/homeassistant/components/hassio/binary_sensor.py index 85cb402b0ca..6ddc15e7725 100644 --- a/homeassistant/components/hassio/binary_sensor.py +++ b/homeassistant/components/hassio/binary_sensor.py @@ -36,7 +36,7 @@ COMMON_ENTITY_DESCRIPTIONS = ( device_class=BinarySensorDeviceClass.UPDATE, entity_registry_enabled_default=False, key=ATTR_UPDATE_AVAILABLE, - name="Update Available", + name="Update available", ), ) diff --git a/homeassistant/components/hassio/entity.py b/homeassistant/components/hassio/entity.py index c6bec04123c..dfa89ae911a 100644 --- a/homeassistant/components/hassio/entity.py +++ b/homeassistant/components/hassio/entity.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import Any -from homeassistant.const import ATTR_NAME from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -20,6 +19,8 @@ from .const import ( class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Base entity for a Hass.io add-on.""" + _attr_has_entity_name = True + def __init__( self, coordinator: HassioDataUpdateCoordinator, @@ -30,7 +31,6 @@ class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): super().__init__(coordinator) self.entity_description = entity_description 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 = DeviceInfo(identifiers={(DOMAIN, addon[ATTR_SLUG])}) @@ -48,6 +48,8 @@ class HassioAddonEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): class HassioOSEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Base Entity for Hass.io OS.""" + _attr_has_entity_name = True + def __init__( self, coordinator: HassioDataUpdateCoordinator, @@ -56,7 +58,6 @@ class HassioOSEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Initialize base entity.""" super().__init__(coordinator) 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 = DeviceInfo(identifiers={(DOMAIN, "OS")}) @@ -73,6 +74,8 @@ class HassioOSEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Base Entity for Supervisor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: HassioDataUpdateCoordinator, @@ -81,7 +84,6 @@ class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Initialize base entity.""" super().__init__(coordinator) self.entity_description = entity_description - self._attr_name = f"Home Assistant Supervisor: {entity_description.name}" self._attr_unique_id = f"home_assistant_supervisor_{entity_description.key}" self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, "supervisor")}) @@ -99,6 +101,8 @@ class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): class HassioCoreEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Base Entity for Core.""" + _attr_has_entity_name = True + def __init__( self, coordinator: HassioDataUpdateCoordinator, @@ -107,7 +111,6 @@ class HassioCoreEntity(CoordinatorEntity[HassioDataUpdateCoordinator]): """Initialize base entity.""" super().__init__(coordinator) self.entity_description = entity_description - self._attr_name = f"Home Assistant Core: {entity_description.name}" self._attr_unique_id = f"home_assistant_core_{entity_description.key}" self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, "core")}) diff --git a/homeassistant/components/hassio/sensor.py b/homeassistant/components/hassio/sensor.py index 55fcb0bcd28..31e728a9736 100644 --- a/homeassistant/components/hassio/sensor.py +++ b/homeassistant/components/hassio/sensor.py @@ -31,7 +31,7 @@ COMMON_ENTITY_DESCRIPTIONS = ( SensorEntityDescription( entity_registry_enabled_default=False, key=ATTR_VERSION_LATEST, - name="Newest Version", + name="Newest version", ), ) @@ -39,7 +39,7 @@ ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + ( SensorEntityDescription( entity_registry_enabled_default=False, key=ATTR_CPU_PERCENT, - name="CPU Percent", + name="CPU percent", icon="mdi:cpu-64-bit", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -47,7 +47,7 @@ ADDON_ENTITY_DESCRIPTIONS = COMMON_ENTITY_DESCRIPTIONS + ( SensorEntityDescription( entity_registry_enabled_default=False, key=ATTR_MEMORY_PERCENT, - name="Memory Percent", + name="Memory percent", icon="mdi:memory", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, From 4433065438b6d6dff1db0da771b70f9026a49cf0 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 14:09:42 -0600 Subject: [PATCH 2389/3516] Migrate Ridwell to new entity naming style (#74915) --- homeassistant/components/ridwell/__init__.py | 2 ++ homeassistant/components/ridwell/sensor.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ridwell/__init__.py b/homeassistant/components/ridwell/__init__.py index 6b615719bd2..5f3656a8b5a 100644 --- a/homeassistant/components/ridwell/__init__.py +++ b/homeassistant/components/ridwell/__init__.py @@ -124,6 +124,8 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class RidwellEntity(CoordinatorEntity): """Define a base Ridwell entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, diff --git a/homeassistant/components/ridwell/sensor.py b/homeassistant/components/ridwell/sensor.py index 44bab16691a..f44cac134d8 100644 --- a/homeassistant/components/ridwell/sensor.py +++ b/homeassistant/components/ridwell/sensor.py @@ -28,7 +28,7 @@ ATTR_QUANTITY = "quantity" SENSOR_DESCRIPTION = SensorEntityDescription( key=SENSOR_TYPE_NEXT_PICKUP, - name="Ridwell Pickup", + name="Ridwell pickup", device_class=SensorDeviceClass.DATE, ) From 07444dba2a053be8c191dcc522bcaf3321acd5d2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 14:11:02 -0600 Subject: [PATCH 2390/3516] Migrate ReCollect Waste to new entity naming style (#74914) --- homeassistant/components/recollect_waste/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index fab482a00e6..7d527ac56c6 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -28,11 +28,11 @@ SENSOR_TYPE_NEXT_PICKUP = "next_pickup" SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_TYPE_CURRENT_PICKUP, - name="Current Pickup", + name="Current pickup", ), SensorEntityDescription( key=SENSOR_TYPE_NEXT_PICKUP, - name="Next Pickup", + name="Next pickup", ), ) @@ -68,6 +68,7 @@ class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): """ReCollect Waste Sensor.""" _attr_device_class = SensorDeviceClass.DATE + _attr_has_entity_name = True def __init__( self, From 4a38be2924b107e55653046560940205982a5ede Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 14:12:56 -0600 Subject: [PATCH 2391/3516] Migrate WattTime to new entity naming style (#74916) --- homeassistant/components/watttime/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/watttime/sensor.py b/homeassistant/components/watttime/sensor.py index 41842c6ed70..040753d97e8 100644 --- a/homeassistant/components/watttime/sensor.py +++ b/homeassistant/components/watttime/sensor.py @@ -35,14 +35,14 @@ SENSOR_TYPE_REALTIME_EMISSIONS_PERCENT = "percent" REALTIME_EMISSIONS_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_TYPE_REALTIME_EMISSIONS_MOER, - name="Marginal Operating Emissions Rate", + name="Marginal operating emissions rate", icon="mdi:blur", native_unit_of_measurement=f"{MASS_POUNDS} CO2/MWh", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_REALTIME_EMISSIONS_PERCENT, - name="Relative Marginal Emissions Intensity", + name="Relative marginal emissions intensity", icon="mdi:blur", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -67,6 +67,8 @@ async def async_setup_entry( class RealtimeEmissionsSensor(CoordinatorEntity, SensorEntity): """Define a realtime emissions sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, From c81d63e070029b1a86799778f2f8789c78fcc0a8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 22:13:09 +0200 Subject: [PATCH 2392/3516] Migrate Cast to new entity naming style (#74901) --- homeassistant/components/cast/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index c8a6a82571e..da32dfd6ae7 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -274,6 +274,7 @@ class CastDevice: class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): """Representation of a Cast device on the network.""" + _attr_has_entity_name = True _attr_should_poll = False _attr_media_image_remotely_accessible = True _mz_only = False @@ -293,7 +294,6 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): self._cast_view_remove_handler = None self._attr_unique_id = str(cast_info.uuid) - self._attr_name = cast_info.friendly_name self._attr_device_info = DeviceInfo( identifiers={(CAST_DOMAIN, str(cast_info.uuid).replace("-", ""))}, manufacturer=str(cast_info.cast_info.manufacturer), From d6ceebbb68a15de8a55674f18b8e92827e33e6d2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 22:13:22 +0200 Subject: [PATCH 2393/3516] Migrate Met.no to new entity naming style (#74908) --- homeassistant/components/met/weather.py | 3 ++- tests/components/met/test_weather.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 0ff0a60bfa1..c843be73fe7 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -71,6 +71,7 @@ def format_condition(condition: str) -> str: class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): """Implementation of a Met.no weather condition.""" + _attr_has_entity_name = True _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_precipitation_unit = LENGTH_MILLIMETERS _attr_native_pressure_unit = PRESSURE_HPA @@ -111,7 +112,7 @@ class MetWeather(CoordinatorEntity[MetDataUpdateCoordinator], WeatherEntity): name = self._config.get(CONF_NAME) name_appendix = "" if self._hourly: - name_appendix = " Hourly" + name_appendix = " hourly" if name is not None: return f"{name}{name_appendix}" diff --git a/tests/components/met/test_weather.py b/tests/components/met/test_weather.py index 3025356d8fb..c9fde5c8ff7 100644 --- a/tests/components/met/test_weather.py +++ b/tests/components/met/test_weather.py @@ -16,10 +16,10 @@ async def test_tracking_home(hass, mock_weather): # Test the hourly sensor is disabled by default registry = er.async_get(hass) - state = hass.states.get("weather.test_home_hourly") + state = hass.states.get("weather.forecast_test_home_hourly") assert state is None - entry = registry.async_get("weather.test_home_hourly") + entry = registry.async_get("weather.forecast_test_home_hourly") assert entry assert entry.disabled assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION @@ -50,7 +50,7 @@ async def test_not_tracking_home(hass, mock_weather): WEATHER_DOMAIN, DOMAIN, "10-20-hourly", - suggested_object_id="somewhere_hourly", + suggested_object_id="forecast_somewhere_hourly", disabled_by=None, ) From 267057c989f3b741ab60c5dfab831710feac6105 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Sun, 10 Jul 2022 22:15:43 +0200 Subject: [PATCH 2394/3516] Fix Vicare One Time Charge (#74872) --- homeassistant/components/vicare/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 575ca35729b..db0ce9cddaa 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -3,7 +3,7 @@ "name": "Viessmann ViCare", "documentation": "https://www.home-assistant.io/integrations/vicare", "codeowners": ["@oischinger"], - "requirements": ["PyViCare==2.16.2"], + "requirements": ["PyViCare==2.16.4"], "iot_class": "cloud_polling", "config_flow": true, "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index d7c0b8124f0..7bb2b07636c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -47,7 +47,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.6.6 # homeassistant.components.vicare -PyViCare==2.16.2 +PyViCare==2.16.4 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83301dc1c4c..bd56b01d799 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -43,7 +43,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.6.6 # homeassistant.components.vicare -PyViCare==2.16.2 +PyViCare==2.16.4 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 From 8285f42d267cdd799be00a0cafed6bea5454f932 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 14:16:16 -0600 Subject: [PATCH 2395/3516] Migrate IQVIA to new entity naming style (#74917) --- homeassistant/components/iqvia/__init__.py | 2 ++ homeassistant/components/iqvia/sensor.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 686f5b57f05..aad505e23c4 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -118,6 +118,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class IQVIAEntity(CoordinatorEntity): """Define a base IQVIA entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index d8c7ea317c8..033ed4e3031 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -79,17 +79,17 @@ TREND_SUBSIDING = "Subsiding" FORECAST_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=TYPE_ALLERGY_FORECAST, - name="Allergy Index: Forecasted Average", + name="Allergy index: forecasted average", icon="mdi:flower", ), SensorEntityDescription( key=TYPE_ASTHMA_FORECAST, - name="Asthma Index: Forecasted Average", + name="Asthma index: forecasted average", icon="mdi:flower", ), SensorEntityDescription( key=TYPE_DISEASE_FORECAST, - name="Cold & Flu: Forecasted Average", + name="Cold & flu: forecasted average", icon="mdi:snowflake", ), ) @@ -97,29 +97,29 @@ FORECAST_SENSOR_DESCRIPTIONS = ( INDEX_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=TYPE_ALLERGY_TODAY, - name="Allergy Index: Today", + name="Allergy index: today", icon="mdi:flower", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_ALLERGY_TOMORROW, - name="Allergy Index: Tomorrow", + name="Allergy index: tomorrow", icon="mdi:flower", ), SensorEntityDescription( key=TYPE_ASTHMA_TODAY, - name="Asthma Index: Today", + name="Asthma index: today", icon="mdi:flower", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_ASTHMA_TOMORROW, - name="Asthma Index: Tomorrow", + name="Asthma index: tomorrow", icon="mdi:flower", ), SensorEntityDescription( key=TYPE_DISEASE_TODAY, - name="Cold & Flu Index: Today", + name="Cold & flu index: today", icon="mdi:pill", state_class=SensorStateClass.MEASUREMENT, ), From 6f28e4bfee341013e802947c955d66e429a34d7f Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 10 Jul 2022 16:17:59 -0400 Subject: [PATCH 2396/3516] Migrate ZHA to new entity naming standard (#74846) --- homeassistant/components/zha/entity.py | 17 +- tests/components/zha/common.py | 26 +- tests/components/zha/test_cover.py | 2 +- tests/components/zha/test_device.py | 2 +- tests/components/zha/test_device_action.py | 8 +- tests/components/zha/test_discover.py | 4 +- tests/components/zha/test_number.py | 4 +- tests/components/zha/test_select.py | 8 +- tests/components/zha/test_sensor.py | 34 +- tests/components/zha/zha_devices_list.py | 2312 ++++++++++---------- 10 files changed, 1210 insertions(+), 1207 deletions(-) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index f70948eb04a..8b3627df9de 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -45,6 +45,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): """A base class for ZHA entities.""" unique_id_suffix: str | None = None + _attr_has_entity_name = True def __init__(self, unique_id: str, zha_device: ZHADevice, **kwargs: Any) -> None: """Init ZHA entity.""" @@ -173,11 +174,13 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): ) -> None: """Init ZHA entity.""" super().__init__(unique_id, zha_device, **kwargs) - ieeetail = "".join([f"{o:02x}" for o in zha_device.ieee[:4]]) - ch_names = ", ".join(sorted(ch.name for ch in channels)) - self._name: str = f"{zha_device.name} {ieeetail} {ch_names}" - if self.unique_id_suffix: - self._name += f" {self.unique_id_suffix}" + self._name: str = ( + self.__class__.__name__.lower() + .replace("zha", "") + .replace("entity", "") + .replace("sensor", "") + .capitalize() + ) self.cluster_channels: dict[str, ZigbeeChannel] = {} for channel in channels: self.cluster_channels[channel.name] = channel @@ -260,7 +263,9 @@ class ZhaGroupEntity(BaseZhaEntity): super().__init__(unique_id, zha_device, **kwargs) self._available = False self._group = zha_device.gateway.groups.get(group_id) - self._name = f"{self._group.name}_zha_group_0x{group_id:04x}" + self._name = ( + f"{self._group.name}_zha_group_0x{group_id:04x}".lower().capitalize() + ) self._group_id: int = group_id self._entity_ids: list[str] = entity_ids self._async_unsub_state_changed: CALLBACK_TYPE | None = None diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 757587071fd..6a51f441a78 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -7,7 +7,7 @@ import zigpy.zcl import zigpy.zcl.foundation as zcl_f import homeassistant.components.zha.core.const as zha_const -from homeassistant.util import slugify +from homeassistant.helpers import entity_registry def patch_cluster(cluster): @@ -133,7 +133,7 @@ async def find_entity_id(domain, zha_device, hass, qualifier=None): This is used to get the entity id in order to get the state from the state machine so that we can test state changes. """ - entities = await find_entity_ids(domain, zha_device, hass) + entities = find_entity_ids(domain, zha_device, hass) if not entities: return None if qualifier: @@ -144,28 +144,26 @@ async def find_entity_id(domain, zha_device, hass, qualifier=None): return entities[0] -async def find_entity_ids(domain, zha_device, hass): +def find_entity_ids(domain, zha_device, hass): """Find the entity ids under the testing. This is used to get the entity id in order to get the state from the state machine so that we can test state changes. """ - ieeetail = "".join([f"{o:02x}" for o in zha_device.ieee[:4]]) - head = f"{domain}.{slugify(f'{zha_device.name} {ieeetail}')}" - enitiy_ids = hass.states.async_entity_ids(domain) - await hass.async_block_till_done() - - res = [] - for entity_id in enitiy_ids: - if entity_id.startswith(head): - res.append(entity_id) - return res + registry = entity_registry.async_get(hass) + return [ + entity.entity_id + for entity in entity_registry.async_entries_for_device( + registry, zha_device.device_id + ) + if entity.domain == domain + ] def async_find_group_entity_id(hass, domain, group): """Find the group entity id under test.""" - entity_id = f"{domain}.{group.name.lower().replace(' ','_')}_zha_group_0x{group.group_id:04x}" + entity_id = f"{domain}.fakemanufacturer_fakemodel_{group.name.lower().replace(' ','_')}_zha_group_0x{group.group_id:04x}" entity_ids = hass.states.async_entity_ids(domain) diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 3dab405151d..0f55735ecb2 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -345,7 +345,7 @@ async def test_restore_state(hass, zha_device_restored, zigpy_shade_device): hass, ( State( - "cover.fakemanufacturer_fakemodel_e769900a_level_on_off_shade", + "cover.fakemanufacturer_fakemodel_shade", STATE_OPEN, {ATTR_CURRENT_POSITION: 50}, ), diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index 8b718635b6a..f05a5cd1872 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -311,7 +311,7 @@ async def test_device_restore_availability( zha_device = await zha_device_restored( zigpy_device, last_seen=time.time() - last_seen_delta ) - entity_id = "switch.fakemanufacturer_fakemodel_e769900a_on_off" + entity_id = "switch.fakemanufacturer_fakemodel_switch" await hass.async_block_till_done() # ensure the switch entity was created diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index fffb79fe0f2..f24ec054c0b 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -86,28 +86,28 @@ async def test_get_actions(hass, device_ias): "domain": Platform.SELECT, "type": "select_option", "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_e769900a_ias_wd_warningmode", + "entity_id": "select.fakemanufacturer_fakemodel_defaulttoneselect", "metadata": {"secondary": True}, }, { "domain": Platform.SELECT, "type": "select_option", "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_e769900a_ias_wd_sirenlevel", + "entity_id": "select.fakemanufacturer_fakemodel_defaultsirenlevelselect", "metadata": {"secondary": True}, }, { "domain": Platform.SELECT, "type": "select_option", "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_e769900a_ias_wd_strobelevel", + "entity_id": "select.fakemanufacturer_fakemodel_defaultstrobelevelselect", "metadata": {"secondary": True}, }, { "domain": Platform.SELECT, "type": "select_option", "device_id": reg_device.id, - "entity_id": "select.fakemanufacturer_fakemodel_e769900a_ias_wd_strobe", + "entity_id": "select.fakemanufacturer_fakemodel_defaultstrobeselect", "metadata": {"secondary": True}, }, ] diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index 4de251fda8b..0f51141ec5d 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -439,8 +439,8 @@ def test_single_input_cluster_device_class_by_cluster_class(): @pytest.mark.parametrize( "override, entity_id", [ - (None, "light.manufacturer_model_77665544_level_light_color_on_off"), - ("switch", "switch.manufacturer_model_77665544_on_off"), + (None, "light.manufacturer_model_light"), + ("switch", "switch.manufacturer_model_switch"), ], ) async def test_device_override( diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index f6b606ccbbf..808649b7f55 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -133,7 +133,7 @@ async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_devi assert hass.states.get(entity_id).attributes.get("unit_of_measurement") == "%" assert ( hass.states.get(entity_id).attributes.get("friendly_name") - == "FakeManufacturer FakeModel e769900a analog_output PWM1" + == "FakeManufacturer FakeModel Number PWM1" ) # change value from device @@ -210,7 +210,7 @@ async def test_level_control_number( Platform.NUMBER, zha_device, hass, - qualifier=attr, + qualifier=attr.replace("_", ""), ) assert entity_id is not None diff --git a/tests/components/zha/test_select.py b/tests/components/zha/test_select.py index c883d648e8e..1c714def54b 100644 --- a/tests/components/zha/test_select.py +++ b/tests/components/zha/test_select.py @@ -113,12 +113,11 @@ async def test_select(hass, siren): entity_registry = er.async_get(hass) zha_device, cluster = siren assert cluster is not None - select_name = security.IasWd.Warning.WarningMode.__name__ entity_id = await find_entity_id( Platform.SELECT, zha_device, hass, - qualifier=select_name.lower(), + qualifier="tone", ) assert entity_id is not None @@ -163,7 +162,7 @@ async def test_select_restore_state( ): """Test zha select entity restore state.""" - entity_id = "select.fakemanufacturer_fakemodel_e769900a_ias_wd_warningmode" + entity_id = "select.fakemanufacturer_fakemodel_defaulttoneselect" core_rs(entity_id, state="Burglar") zigpy_device = zigpy_device_mock( @@ -180,12 +179,11 @@ async def test_select_restore_state( zha_device = await zha_device_restored(zigpy_device) cluster = zigpy_device.endpoints[1].ias_wd assert cluster is not None - select_name = security.IasWd.Warning.WarningMode.__name__ entity_id = await find_entity_id( Platform.SELECT, zha_device, hass, - qualifier=select_name.lower(), + qualifier="tone", ) assert entity_id is not None diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index c638bdd8c48..d2fc7c3ca73 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -48,7 +48,7 @@ from .common import ( ) from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE -ENTITY_ID_PREFIX = "sensor.fakemanufacturer_fakemodel_e769900a_{}" +ENTITY_ID_PREFIX = "sensor.fakemanufacturer_fakemodel_{}" @pytest.fixture(autouse=True) @@ -312,7 +312,7 @@ async def async_test_device_temperature(hass, cluster, entity_id): ), ( smartenergy.Metering.cluster_id, - "smartenergy_metering_summation_delivered", + "smartenergy_summation", async_test_smart_energy_summation, 1, { @@ -360,7 +360,7 @@ async def async_test_device_temperature(hass, cluster, entity_id): ), ( general.PowerConfiguration.cluster_id, - "power", + "battery", async_test_powerconfiguration, 2, { @@ -414,7 +414,7 @@ async def test_sensor( zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100 cluster.PLUGGED_ATTR_READS = read_plug zha_device = await zha_device_joined_restored(zigpy_device) - entity_id = ENTITY_ID_PREFIX.format(entity_suffix) + entity_id = ENTITY_ID_PREFIX.format(entity_suffix.replace("_", "")) await async_enable_traffic(hass, [zha_device], enabled=False) await hass.async_block_till_done() @@ -620,7 +620,7 @@ async def test_electrical_measurement_init( {"apparent_power", "rms_voltage", "rms_current"}, { "electrical_measurement", - "electrical_measurement_ac_frequency", + "electrical_measurement_frequency", "electrical_measurement_power_factor", }, { @@ -636,7 +636,7 @@ async def test_electrical_measurement_init( { "electrical_measurement_apparent_power", "electrical_measurement_rms_current", - "electrical_measurement_ac_frequency", + "electrical_measurement_frequency", "electrical_measurement_power_factor", }, ), @@ -648,7 +648,7 @@ async def test_electrical_measurement_init( "electrical_measurement", "electrical_measurement_apparent_power", "electrical_measurement_rms_current", - "electrical_measurement_ac_frequency", + "electrical_measurement_frequency", "electrical_measurement_power_factor", }, set(), @@ -659,7 +659,7 @@ async def test_electrical_measurement_init( "instantaneous_demand", }, { - "smartenergy_metering_summation_delivered", + "smartenergy_summation", }, { "smartenergy_metering", @@ -670,7 +670,7 @@ async def test_electrical_measurement_init( {"instantaneous_demand", "current_summ_delivered"}, {}, { - "smartenergy_metering_summation_delivered", + "smartenergy_summation", "smartenergy_metering", }, ), @@ -678,7 +678,7 @@ async def test_electrical_measurement_init( smartenergy.Metering.cluster_id, {}, { - "smartenergy_metering_summation_delivered", + "smartenergy_summation", "smartenergy_metering", }, {}, @@ -696,8 +696,10 @@ async def test_unsupported_attributes_sensor( ): """Test zha sensor platform.""" - entity_ids = {ENTITY_ID_PREFIX.format(e) for e in entity_ids} - missing_entity_ids = {ENTITY_ID_PREFIX.format(e) for e in missing_entity_ids} + entity_ids = {ENTITY_ID_PREFIX.format(e.replace("_", "")) for e in entity_ids} + missing_entity_ids = { + ENTITY_ID_PREFIX.format(e.replace("_", "")) for e in missing_entity_ids + } zigpy_device = zigpy_device_mock( { @@ -718,7 +720,7 @@ async def test_unsupported_attributes_sensor( await async_enable_traffic(hass, [zha_device], enabled=False) await hass.async_block_till_done() - present_entity_ids = set(await find_entity_ids(Platform.SENSOR, zha_device, hass)) + present_entity_ids = set(find_entity_ids(Platform.SENSOR, zha_device, hass)) assert present_entity_ids == entity_ids assert missing_entity_ids not in present_entity_ids @@ -811,7 +813,7 @@ async def test_se_summation_uom( ): """Test zha smart energy summation.""" - entity_id = ENTITY_ID_PREFIX.format("smartenergy_metering_summation_delivered") + entity_id = ENTITY_ID_PREFIX.format("smartenergysummation") zigpy_device = zigpy_device_mock( { 1: { @@ -865,7 +867,7 @@ async def test_elec_measurement_sensor_type( ): """Test zha electrical measurement sensor type.""" - entity_id = ENTITY_ID_PREFIX.format("electrical_measurement") + entity_id = ENTITY_ID_PREFIX.format("electricalmeasurement") zigpy_dev = elec_measurement_zigpy_dev zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS[ "measurement_type" @@ -914,7 +916,7 @@ async def test_elec_measurement_skip_unsupported_attribute( ): """Test zha electrical measurement skipping update of unsupported attributes.""" - entity_id = ENTITY_ID_PREFIX.format("electrical_measurement") + entity_id = ENTITY_ID_PREFIX.format("electricalmeasurement") zha_dev = elec_measurement_zha_dev cluster = zha_dev.device.endpoints[1].electrical_measurement diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 9a32a1670b6..4cea8eb8f66 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -38,25 +38,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008"], DEV_SIG_ENTITIES: [ - "button.adurolight_adurolight_ncc_77665544_identify", - "sensor.adurolight_adurolight_ncc_77665544_basic_rssi", - "sensor.adurolight_adurolight_ncc_77665544_basic_lqi", + "button.adurolight_adurolight_ncc_identifybutton", + "sensor.adurolight_adurolight_ncc_rssi", + "sensor.adurolight_adurolight_ncc_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.adurolight_adurolight_ncc_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.adurolight_adurolight_ncc_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.adurolight_adurolight_ncc_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.adurolight_adurolight_ncc_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.adurolight_adurolight_ncc_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.adurolight_adurolight_ncc_lqi", }, }, }, @@ -76,43 +76,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["5:0x0019"], DEV_SIG_ENTITIES: [ - "button.bosch_isw_zpr1_wp13_77665544_identify", - "sensor.bosch_isw_zpr1_wp13_77665544_power", - "sensor.bosch_isw_zpr1_wp13_77665544_temperature", - "binary_sensor.bosch_isw_zpr1_wp13_77665544_ias_zone", - "sensor.bosch_isw_zpr1_wp13_77665544_basic_rssi", - "sensor.bosch_isw_zpr1_wp13_77665544_basic_lqi", + "button.bosch_isw_zpr1_wp13_identifybutton", + "sensor.bosch_isw_zpr1_wp13_battery", + "sensor.bosch_isw_zpr1_wp13_temperature", + "binary_sensor.bosch_isw_zpr1_wp13_iaszone", + "sensor.bosch_isw_zpr1_wp13_rssi", + "sensor.bosch_isw_zpr1_wp13_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-5-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.bosch_isw_zpr1_wp13_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.bosch_isw_zpr1_wp13_iaszone", }, ("button", "00:11:22:33:44:55:66:77-5-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.bosch_isw_zpr1_wp13_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.bosch_isw_zpr1_wp13_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-5-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_battery", }, ("sensor", "00:11:22:33:44:55:66:77-5-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-5-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-5-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_lqi", }, }, }, @@ -132,31 +132,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_3130_77665544_identify", - "sensor.centralite_3130_77665544_power", - "sensor.centralite_3130_77665544_basic_rssi", - "sensor.centralite_3130_77665544_basic_lqi", + "button.centralite_3130_identifybutton", + "sensor.centralite_3130_battery", + "sensor.centralite_3130_rssi", + "sensor.centralite_3130_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3130_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_3130_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_lqi", }, }, }, @@ -176,79 +176,79 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_3210_l_77665544_identify", - "sensor.centralite_3210_l_77665544_electrical_measurement", - "sensor.centralite_3210_l_77665544_electrical_measurement_apparent_power", - "sensor.centralite_3210_l_77665544_electrical_measurement_rms_current", - "sensor.centralite_3210_l_77665544_electrical_measurement_rms_voltage", - "sensor.centralite_3210_l_77665544_electrical_measurement_ac_frequency", - "sensor.centralite_3210_l_77665544_electrical_measurement_power_factor", - "sensor.centralite_3210_l_77665544_smartenergy_metering", - "sensor.centralite_3210_l_77665544_smartenergy_metering_summation_delivered", - "switch.centralite_3210_l_77665544_on_off", - "sensor.centralite_3210_l_77665544_basic_rssi", - "sensor.centralite_3210_l_77665544_basic_lqi", + "button.centralite_3210_l_identifybutton", + "sensor.centralite_3210_l_electricalmeasurement", + "sensor.centralite_3210_l_electricalmeasurementapparentpower", + "sensor.centralite_3210_l_electricalmeasurementrmscurrent", + "sensor.centralite_3210_l_electricalmeasurementrmsvoltage", + "sensor.centralite_3210_l_electricalmeasurementfrequency", + "sensor.centralite_3210_l_electricalmeasurementpowerfactor", + "sensor.centralite_3210_l_smartenergymetering", + "sensor.centralite_3210_l_smartenergysummation", + "switch.centralite_3210_l_switch", + "sensor.centralite_3210_l_rssi", + "sensor.centralite_3210_l_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.centralite_3210_l_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.centralite_3210_l_switch", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3210_l_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_3210_l_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementapparentpower", }, ("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", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementrmscurrent", }, ("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_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_lqi", }, }, }, @@ -268,43 +268,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_3310_s_77665544_identify", - "sensor.centralite_3310_s_77665544_power", - "sensor.centralite_3310_s_77665544_temperature", - "sensor.centralite_3310_s_77665544_manufacturer_specific", - "sensor.centralite_3310_s_77665544_basic_rssi", - "sensor.centralite_3310_s_77665544_basic_lqi", + "button.centralite_3310_s_identifybutton", + "sensor.centralite_3310_s_battery", + "sensor.centralite_3310_s_temperature", + "sensor.centralite_3310_s_humidity", + "sensor.centralite_3310_s_rssi", + "sensor.centralite_3310_s_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3310_s_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_3310_s_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-1-64581"): { DEV_SIG_CHANNELS: ["manufacturer_specific"], DEV_SIG_ENT_MAP_CLASS: "Humidity", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_77665544_manufacturer_specific", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_humidity", }, }, }, @@ -331,43 +331,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_3315_s_77665544_identify", - "sensor.centralite_3315_s_77665544_power", - "sensor.centralite_3315_s_77665544_temperature", - "binary_sensor.centralite_3315_s_77665544_ias_zone", - "sensor.centralite_3315_s_77665544_basic_rssi", - "sensor.centralite_3315_s_77665544_basic_lqi", + "button.centralite_3315_s_identifybutton", + "sensor.centralite_3315_s_battery", + "sensor.centralite_3315_s_temperature", + "binary_sensor.centralite_3315_s_iaszone", + "sensor.centralite_3315_s_rssi", + "sensor.centralite_3315_s_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3315_s_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3315_s_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3315_s_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_3315_s_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_lqi", }, }, }, @@ -394,43 +394,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_3320_l_77665544_identify", - "sensor.centralite_3320_l_77665544_power", - "sensor.centralite_3320_l_77665544_temperature", - "binary_sensor.centralite_3320_l_77665544_ias_zone", - "sensor.centralite_3320_l_77665544_basic_rssi", - "sensor.centralite_3320_l_77665544_basic_lqi", + "button.centralite_3320_l_identifybutton", + "sensor.centralite_3320_l_battery", + "sensor.centralite_3320_l_temperature", + "binary_sensor.centralite_3320_l_iaszone", + "sensor.centralite_3320_l_rssi", + "sensor.centralite_3320_l_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3320_l_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3320_l_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3320_l_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_3320_l_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_lqi", }, }, }, @@ -457,43 +457,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_3326_l_77665544_identify", - "sensor.centralite_3326_l_77665544_power", - "sensor.centralite_3326_l_77665544_temperature", - "binary_sensor.centralite_3326_l_77665544_ias_zone", - "sensor.centralite_3326_l_77665544_basic_rssi", - "sensor.centralite_3326_l_77665544_basic_lqi", + "button.centralite_3326_l_identifybutton", + "sensor.centralite_3326_l_battery", + "sensor.centralite_3326_l_temperature", + "binary_sensor.centralite_3326_l_iaszone", + "sensor.centralite_3326_l_rssi", + "sensor.centralite_3326_l_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3326_l_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3326_l_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3326_l_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_3326_l_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_lqi", }, }, }, @@ -520,49 +520,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.centralite_motion_sensor_a_77665544_identify", - "sensor.centralite_motion_sensor_a_77665544_power", - "sensor.centralite_motion_sensor_a_77665544_temperature", - "binary_sensor.centralite_motion_sensor_a_77665544_ias_zone", - "binary_sensor.centralite_motion_sensor_a_77665544_occupancy", - "sensor.centralite_motion_sensor_a_77665544_basic_rssi", - "sensor.centralite_motion_sensor_a_77665544_basic_lqi", + "button.centralite_motion_sensor_a_identifybutton", + "sensor.centralite_motion_sensor_a_battery", + "sensor.centralite_motion_sensor_a_temperature", + "binary_sensor.centralite_motion_sensor_a_iaszone", + "binary_sensor.centralite_motion_sensor_a_occupancy", + "sensor.centralite_motion_sensor_a_rssi", + "sensor.centralite_motion_sensor_a_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_motion_sensor_a_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_motion_sensor_a_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_motion_sensor_a_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.centralite_motion_sensor_a_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-2-1030"): { DEV_SIG_CHANNELS: ["occupancy"], DEV_SIG_ENT_MAP_CLASS: "Occupancy", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_motion_sensor_a_77665544_occupancy", + DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_motion_sensor_a_occupancy", }, }, }, @@ -589,43 +589,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["4:0x0019"], DEV_SIG_ENTITIES: [ - "button.climaxtechnology_psmp5_00_00_02_02tc_77665544_identify", - "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering", - "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering_summation_delivered", - "switch.climaxtechnology_psmp5_00_00_02_02tc_77665544_on_off", - "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_basic_rssi", - "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_basic_lqi", + "button.climaxtechnology_psmp5_00_00_02_02tc_identifybutton", + "sensor.climaxtechnology_psmp5_00_00_02_02tc_smartenergymetering", + "sensor.climaxtechnology_psmp5_00_00_02_02tc_smartenergysummation", + "switch.climaxtechnology_psmp5_00_00_02_02tc_switch", + "sensor.climaxtechnology_psmp5_00_00_02_02tc_rssi", + "sensor.climaxtechnology_psmp5_00_00_02_02tc_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.climaxtechnology_psmp5_00_00_02_02tc_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.climaxtechnology_psmp5_00_00_02_02tc_switch", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_psmp5_00_00_02_02tc_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_psmp5_00_00_02_02tc_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_lqi", }, }, }, @@ -645,61 +645,61 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.climaxtechnology_sd8sc_00_00_03_12tc_77665544_identify", - "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_zone", - "sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_basic_rssi", - "sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_basic_lqi", - "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_warningmode", - "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_sirenlevel", - "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_strobelevel", - "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_strobe", - "siren.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd", + "button.climaxtechnology_sd8sc_00_00_03_12tc_identifybutton", + "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_iaszone", + "sensor.climaxtechnology_sd8sc_00_00_03_12tc_rssi", + "sensor.climaxtechnology_sd8sc_00_00_03_12tc_lqi", + "select.climaxtechnology_sd8sc_00_00_03_12tc_defaulttoneselect", + "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultsirenlevelselect", + "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultstrobelevelselect", + "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultstrobeselect", + "siren.climaxtechnology_sd8sc_00_00_03_12tc_siren", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_sd8sc_00_00_03_12tc_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_sd8sc_00_00_03_12tc_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_sd8sc_00_00_03_12tc_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_sd8sc_00_00_03_12tc_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_sd8sc_00_00_03_12tc_lqi", }, ("select", "00:11:22:33:44:55:66:77-1-1282-WarningMode"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultToneSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_warningmode", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_defaulttoneselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-SirenLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultSirenLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_sirenlevel", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultsirenlevelselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-StrobeLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_strobelevel", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultstrobelevelselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-Strobe"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd_strobe", + DEV_SIG_ENT_MAP_ID: "select.climaxtechnology_sd8sc_00_00_03_12tc_defaultstrobeselect", }, ("siren", "00:11:22:33:44:55:66:77-1-1282"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHASiren", - DEV_SIG_ENT_MAP_ID: "siren.climaxtechnology_sd8sc_00_00_03_12tc_77665544_ias_wd", + DEV_SIG_ENT_MAP_ID: "siren.climaxtechnology_sd8sc_00_00_03_12tc_siren", }, }, }, @@ -719,31 +719,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.climaxtechnology_ws15_00_00_03_03tc_77665544_identify", - "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_ias_zone", - "sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_basic_rssi", - "sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_basic_lqi", + "button.climaxtechnology_ws15_00_00_03_03tc_identifybutton", + "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_iaszone", + "sensor.climaxtechnology_ws15_00_00_03_03tc_rssi", + "sensor.climaxtechnology_ws15_00_00_03_03tc_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_ws15_00_00_03_03tc_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.climaxtechnology_ws15_00_00_03_03tc_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_ws15_00_00_03_03tc_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_ws15_00_00_03_03tc_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_ws15_00_00_03_03tc_lqi", }, }, }, @@ -770,31 +770,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.feibit_inc_co_fb56_zcw08ku1_1_77665544_identify", - "light.feibit_inc_co_fb56_zcw08ku1_1_77665544_level_light_color_on_off", - "sensor.feibit_inc_co_fb56_zcw08ku1_1_77665544_basic_rssi", - "sensor.feibit_inc_co_fb56_zcw08ku1_1_77665544_basic_lqi", + "button.feibit_inc_co_fb56_zcw08ku1_1_identifybutton", + "light.feibit_inc_co_fb56_zcw08ku1_1_light", + "sensor.feibit_inc_co_fb56_zcw08ku1_1_rssi", + "sensor.feibit_inc_co_fb56_zcw08ku1_1_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-11"): { DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.feibit_inc_co_fb56_zcw08ku1_1_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.feibit_inc_co_fb56_zcw08ku1_1_light", }, ("button", "00:11:22:33:44:55:66:77-11-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.feibit_inc_co_fb56_zcw08ku1_1_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.feibit_inc_co_fb56_zcw08ku1_1_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-11-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.feibit_inc_co_fb56_zcw08ku1_1_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.feibit_inc_co_fb56_zcw08ku1_1_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-11-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.feibit_inc_co_fb56_zcw08ku1_1_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.feibit_inc_co_fb56_zcw08ku1_1_lqi", }, }, }, @@ -814,67 +814,67 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.heiman_smokesensor_em_77665544_identify", - "sensor.heiman_smokesensor_em_77665544_power", - "binary_sensor.heiman_smokesensor_em_77665544_ias_zone", - "sensor.heiman_smokesensor_em_77665544_basic_rssi", - "sensor.heiman_smokesensor_em_77665544_basic_lqi", - "select.heiman_smokesensor_em_77665544_ias_wd_warningmode", - "select.heiman_smokesensor_em_77665544_ias_wd_sirenlevel", - "select.heiman_smokesensor_em_77665544_ias_wd_strobelevel", - "select.heiman_smokesensor_em_77665544_ias_wd_strobe", - "siren.heiman_smokesensor_em_77665544_ias_wd", + "button.heiman_smokesensor_em_identifybutton", + "sensor.heiman_smokesensor_em_battery", + "binary_sensor.heiman_smokesensor_em_iaszone", + "sensor.heiman_smokesensor_em_rssi", + "sensor.heiman_smokesensor_em_lqi", + "select.heiman_smokesensor_em_defaulttoneselect", + "select.heiman_smokesensor_em_defaultsirenlevelselect", + "select.heiman_smokesensor_em_defaultstrobelevelselect", + "select.heiman_smokesensor_em_defaultstrobeselect", + "siren.heiman_smokesensor_em_siren", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_smokesensor_em_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_smokesensor_em_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.heiman_smokesensor_em_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.heiman_smokesensor_em_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_lqi", }, ("select", "00:11:22:33:44:55:66:77-1-1282-WarningMode"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultToneSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_77665544_ias_wd_warningmode", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_defaulttoneselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-SirenLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultSirenLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_77665544_ias_wd_sirenlevel", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_defaultsirenlevelselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-StrobeLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_77665544_ias_wd_strobelevel", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_defaultstrobelevelselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-Strobe"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_77665544_ias_wd_strobe", + DEV_SIG_ENT_MAP_ID: "select.heiman_smokesensor_em_defaultstrobeselect", }, ("siren", "00:11:22:33:44:55:66:77-1-1282"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHASiren", - DEV_SIG_ENT_MAP_ID: "siren.heiman_smokesensor_em_77665544_ias_wd", + DEV_SIG_ENT_MAP_ID: "siren.heiman_smokesensor_em_siren", }, }, }, @@ -894,31 +894,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.heiman_co_v16_77665544_identify", - "binary_sensor.heiman_co_v16_77665544_ias_zone", - "sensor.heiman_co_v16_77665544_basic_rssi", - "sensor.heiman_co_v16_77665544_basic_lqi", + "button.heiman_co_v16_identifybutton", + "binary_sensor.heiman_co_v16_iaszone", + "sensor.heiman_co_v16_rssi", + "sensor.heiman_co_v16_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_co_v16_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_co_v16_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.heiman_co_v16_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.heiman_co_v16_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_co_v16_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_co_v16_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_co_v16_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_co_v16_lqi", }, }, }, @@ -938,61 +938,61 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.heiman_warningdevice_77665544_identify", - "binary_sensor.heiman_warningdevice_77665544_ias_zone", - "sensor.heiman_warningdevice_77665544_basic_rssi", - "sensor.heiman_warningdevice_77665544_basic_lqi", - "select.heiman_warningdevice_77665544_ias_wd_warningmode", - "select.heiman_warningdevice_77665544_ias_wd_sirenlevel", - "select.heiman_warningdevice_77665544_ias_wd_strobelevel", - "select.heiman_warningdevice_77665544_ias_wd_strobe", - "siren.heiman_warningdevice_77665544_ias_wd", + "button.heiman_warningdevice_identifybutton", + "binary_sensor.heiman_warningdevice_iaszone", + "sensor.heiman_warningdevice_rssi", + "sensor.heiman_warningdevice_lqi", + "select.heiman_warningdevice_defaulttoneselect", + "select.heiman_warningdevice_defaultsirenlevelselect", + "select.heiman_warningdevice_defaultstrobelevelselect", + "select.heiman_warningdevice_defaultstrobeselect", + "siren.heiman_warningdevice_siren", ], DEV_SIG_ENT_MAP: { ("select", "00:11:22:33:44:55:66:77-1-1282-WarningMode"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultToneSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_77665544_ias_wd_warningmode", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_defaulttoneselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-SirenLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultSirenLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_77665544_ias_wd_sirenlevel", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_defaultsirenlevelselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-StrobeLevel"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_77665544_ias_wd_strobelevel", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_defaultstrobelevelselect", }, ("select", "00:11:22:33:44:55:66:77-1-1282-Strobe"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_77665544_ias_wd_strobe", + DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_defaultstrobeselect", }, ("siren", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["ias_wd"], DEV_SIG_ENT_MAP_CLASS: "ZHASiren", - DEV_SIG_ENT_MAP_ID: "siren.heiman_warningdevice_77665544_ias_wd", + DEV_SIG_ENT_MAP_ID: "siren.heiman_warningdevice_siren", }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_warningdevice_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_warningdevice_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.heiman_warningdevice_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.heiman_warningdevice_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_warningdevice_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_warningdevice_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_warningdevice_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.heiman_warningdevice_lqi", }, }, }, @@ -1012,49 +1012,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["6:0x0019"], DEV_SIG_ENTITIES: [ - "button.hivehome_com_mot003_77665544_identify", - "sensor.hivehome_com_mot003_77665544_power", - "sensor.hivehome_com_mot003_77665544_illuminance", - "sensor.hivehome_com_mot003_77665544_temperature", - "binary_sensor.hivehome_com_mot003_77665544_ias_zone", - "sensor.hivehome_com_mot003_77665544_basic_rssi", - "sensor.hivehome_com_mot003_77665544_basic_lqi", + "button.hivehome_com_mot003_identifybutton", + "sensor.hivehome_com_mot003_battery", + "sensor.hivehome_com_mot003_illuminance", + "sensor.hivehome_com_mot003_temperature", + "binary_sensor.hivehome_com_mot003_iaszone", + "sensor.hivehome_com_mot003_rssi", + "sensor.hivehome_com_mot003_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-6-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.hivehome_com_mot003_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.hivehome_com_mot003_iaszone", }, ("button", "00:11:22:33:44:55:66:77-6-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.hivehome_com_mot003_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.hivehome_com_mot003_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-6-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_battery", }, ("sensor", "00:11:22:33:44:55:66:77-6-1024"): { DEV_SIG_CHANNELS: ["illuminance"], DEV_SIG_ENT_MAP_CLASS: "Illuminance", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_77665544_illuminance", + DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_illuminance", }, ("sensor", "00:11:22:33:44:55:66:77-6-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-6-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-6-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_lqi", }, }, }, @@ -1081,31 +1081,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_identify", - "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_level_light_color_on_off", - "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_identifybutton", + "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_light", + "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_lqi", }, }, }, @@ -1125,31 +1125,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_identify", - "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_level_light_color_on_off", - "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_identifybutton", + "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_light", + "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_lqi", }, }, }, @@ -1169,31 +1169,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_identify", - "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_level_on_off", - "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_identifybutton", + "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_light", + "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_lqi", }, }, }, @@ -1213,31 +1213,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_identify", - "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_level_light_color_on_off", - "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_identifybutton", + "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_light", + "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_lqi", }, }, }, @@ -1257,31 +1257,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_identify", - "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_level_on_off", - "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_identifybutton", + "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_light", + "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_rssi", + "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_lqi", }, }, }, @@ -1301,31 +1301,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_control_outlet_77665544_identify", - "switch.ikea_of_sweden_tradfri_control_outlet_77665544_on_off", - "sensor.ikea_of_sweden_tradfri_control_outlet_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_control_outlet_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_control_outlet_identifybutton", + "switch.ikea_of_sweden_tradfri_control_outlet_switch", + "sensor.ikea_of_sweden_tradfri_control_outlet_rssi", + "sensor.ikea_of_sweden_tradfri_control_outlet_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.ikea_of_sweden_tradfri_control_outlet_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.ikea_of_sweden_tradfri_control_outlet_switch", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_control_outlet_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_control_outlet_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_control_outlet_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_control_outlet_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_control_outlet_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_control_outlet_lqi", }, }, }, @@ -1345,37 +1345,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_motion_sensor_77665544_identify", - "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_power", - "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_on_off", - "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_motion_sensor_identifybutton", + "sensor.ikea_of_sweden_tradfri_motion_sensor_battery", + "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_motion", + "sensor.ikea_of_sweden_tradfri_motion_sensor_rssi", + "sensor.ikea_of_sweden_tradfri_motion_sensor_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_motion_sensor_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_motion_sensor_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Motion", - DEV_SIG_ENT_MAP_ID: "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_motion", }, }, }, @@ -1395,31 +1395,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019", "1:0x0102"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_on_off_switch_77665544_identify", - "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_power", - "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_on_off_switch_identifybutton", + "sensor.ikea_of_sweden_tradfri_on_off_switch_battery", + "sensor.ikea_of_sweden_tradfri_on_off_switch_rssi", + "sensor.ikea_of_sweden_tradfri_on_off_switch_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_on_off_switch_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_on_off_switch_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_lqi", }, }, }, @@ -1439,31 +1439,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_remote_control_77665544_identify", - "sensor.ikea_of_sweden_tradfri_remote_control_77665544_power", - "sensor.ikea_of_sweden_tradfri_remote_control_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_remote_control_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_remote_control_identifybutton", + "sensor.ikea_of_sweden_tradfri_remote_control_battery", + "sensor.ikea_of_sweden_tradfri_remote_control_rssi", + "sensor.ikea_of_sweden_tradfri_remote_control_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_remote_control_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_remote_control_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_lqi", }, }, }, @@ -1490,25 +1490,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_signal_repeater_77665544_identify", - "sensor.ikea_of_sweden_tradfri_signal_repeater_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_signal_repeater_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_signal_repeater_identifybutton", + "sensor.ikea_of_sweden_tradfri_signal_repeater_rssi", + "sensor.ikea_of_sweden_tradfri_signal_repeater_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_signal_repeater_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_signal_repeater_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_signal_repeater_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_signal_repeater_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_signal_repeater_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_signal_repeater_lqi", }, }, }, @@ -1528,31 +1528,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ikea_of_sweden_tradfri_wireless_dimmer_77665544_identify", - "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_power", - "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_basic_rssi", - "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_basic_lqi", + "button.ikea_of_sweden_tradfri_wireless_dimmer_identifybutton", + "sensor.ikea_of_sweden_tradfri_wireless_dimmer_battery", + "sensor.ikea_of_sweden_tradfri_wireless_dimmer_rssi", + "sensor.ikea_of_sweden_tradfri_wireless_dimmer_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_wireless_dimmer_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ikea_of_sweden_tradfri_wireless_dimmer_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_lqi", }, }, }, @@ -1579,43 +1579,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006", "2:0x0008"], DEV_SIG_ENTITIES: [ - "button.jasco_products_45852_77665544_identify", - "sensor.jasco_products_45852_77665544_smartenergy_metering", - "sensor.jasco_products_45852_77665544_smartenergy_metering_summation_delivered", - "light.jasco_products_45852_77665544_level_on_off", - "sensor.jasco_products_45852_77665544_basic_rssi", - "sensor.jasco_products_45852_77665544_basic_lqi", + "button.jasco_products_45852_identifybutton", + "sensor.jasco_products_45852_smartenergymetering", + "sensor.jasco_products_45852_smartenergysummation", + "light.jasco_products_45852_light", + "sensor.jasco_products_45852_rssi", + "sensor.jasco_products_45852_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.jasco_products_45852_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.jasco_products_45852_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.jasco_products_45852_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.jasco_products_45852_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_lqi", }, }, }, @@ -1642,43 +1642,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006"], DEV_SIG_ENTITIES: [ - "button.jasco_products_45856_77665544_identify", - "light.jasco_products_45856_77665544_on_off", - "sensor.jasco_products_45856_77665544_smartenergy_metering", - "sensor.jasco_products_45856_77665544_smartenergy_metering_summation_delivered", - "sensor.jasco_products_45856_77665544_basic_rssi", - "sensor.jasco_products_45856_77665544_basic_lqi", + "button.jasco_products_45856_identifybutton", + "light.jasco_products_45856_light", + "sensor.jasco_products_45856_smartenergymetering", + "sensor.jasco_products_45856_smartenergysummation", + "sensor.jasco_products_45856_rssi", + "sensor.jasco_products_45856_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.jasco_products_45856_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.jasco_products_45856_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.jasco_products_45856_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.jasco_products_45856_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_lqi", }, }, }, @@ -1705,43 +1705,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006", "2:0x0008"], DEV_SIG_ENTITIES: [ - "button.jasco_products_45857_77665544_identify", - "light.jasco_products_45857_77665544_level_on_off", - "sensor.jasco_products_45857_77665544_smartenergy_metering", - "sensor.jasco_products_45857_77665544_smartenergy_metering_summation_delivered", - "sensor.jasco_products_45857_77665544_basic_rssi", - "sensor.jasco_products_45857_77665544_basic_lqi", + "button.jasco_products_45857_identifybutton", + "light.jasco_products_45857_light", + "sensor.jasco_products_45857_smartenergymetering", + "sensor.jasco_products_45857_smartenergysummation", + "sensor.jasco_products_45857_rssi", + "sensor.jasco_products_45857_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.jasco_products_45857_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.jasco_products_45857_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.jasco_products_45857_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.jasco_products_45857_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_lqi", }, }, }, @@ -1761,49 +1761,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.keen_home_inc_sv02_610_mp_1_3_77665544_identify", - "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_power", - "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_pressure", - "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_temperature", - "cover.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", - "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_basic_rssi", - "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_basic_lqi", + "button.keen_home_inc_sv02_610_mp_1_3_identifybutton", + "sensor.keen_home_inc_sv02_610_mp_1_3_battery", + "sensor.keen_home_inc_sv02_610_mp_1_3_pressure", + "sensor.keen_home_inc_sv02_610_mp_1_3_temperature", + "cover.keen_home_inc_sv02_610_mp_1_3_keenvent", + "sensor.keen_home_inc_sv02_610_mp_1_3_rssi", + "sensor.keen_home_inc_sv02_610_mp_1_3_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_610_mp_1_3_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_610_mp_1_3_identifybutton", }, ("cover", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["level", "on_off"], DEV_SIG_ENT_MAP_CLASS: "KeenVent", - DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_610_mp_1_3_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_610_mp_1_3_keenvent", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { DEV_SIG_CHANNELS: ["pressure"], DEV_SIG_ENT_MAP_CLASS: "Pressure", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_pressure", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_pressure", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_lqi", }, }, }, @@ -1823,49 +1823,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.keen_home_inc_sv02_612_mp_1_2_77665544_identify", - "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_power", - "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_pressure", - "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_temperature", - "cover.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", - "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_basic_rssi", - "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_basic_lqi", + "button.keen_home_inc_sv02_612_mp_1_2_identifybutton", + "sensor.keen_home_inc_sv02_612_mp_1_2_battery", + "sensor.keen_home_inc_sv02_612_mp_1_2_pressure", + "sensor.keen_home_inc_sv02_612_mp_1_2_temperature", + "cover.keen_home_inc_sv02_612_mp_1_2_keenvent", + "sensor.keen_home_inc_sv02_612_mp_1_2_rssi", + "sensor.keen_home_inc_sv02_612_mp_1_2_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_2_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_2_identifybutton", }, ("cover", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["level", "on_off"], DEV_SIG_ENT_MAP_CLASS: "KeenVent", - DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_612_mp_1_2_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_612_mp_1_2_keenvent", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { DEV_SIG_CHANNELS: ["pressure"], DEV_SIG_ENT_MAP_CLASS: "Pressure", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_pressure", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_pressure", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_lqi", }, }, }, @@ -1885,49 +1885,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.keen_home_inc_sv02_612_mp_1_3_77665544_identify", - "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_power", - "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_pressure", - "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_temperature", - "cover.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", - "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_basic_rssi", - "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_basic_lqi", + "button.keen_home_inc_sv02_612_mp_1_3_identifybutton", + "sensor.keen_home_inc_sv02_612_mp_1_3_battery", + "sensor.keen_home_inc_sv02_612_mp_1_3_pressure", + "sensor.keen_home_inc_sv02_612_mp_1_3_temperature", + "cover.keen_home_inc_sv02_612_mp_1_3_keenvent", + "sensor.keen_home_inc_sv02_612_mp_1_3_rssi", + "sensor.keen_home_inc_sv02_612_mp_1_3_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_3_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_3_identifybutton", }, ("cover", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["level", "on_off"], DEV_SIG_ENT_MAP_CLASS: "KeenVent", - DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_612_mp_1_3_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_612_mp_1_3_keenvent", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { DEV_SIG_CHANNELS: ["pressure"], DEV_SIG_ENT_MAP_CLASS: "Pressure", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_pressure", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_pressure", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_lqi", }, }, }, @@ -1947,37 +1947,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.king_of_fans_inc_hbuniversalcfremote_77665544_identify", - "light.king_of_fans_inc_hbuniversalcfremote_77665544_level_on_off", - "fan.king_of_fans_inc_hbuniversalcfremote_77665544_fan", - "sensor.king_of_fans_inc_hbuniversalcfremote_77665544_basic_rssi", - "sensor.king_of_fans_inc_hbuniversalcfremote_77665544_basic_lqi", + "button.king_of_fans_inc_hbuniversalcfremote_identifybutton", + "light.king_of_fans_inc_hbuniversalcfremote_light", + "fan.king_of_fans_inc_hbuniversalcfremote_fan", + "sensor.king_of_fans_inc_hbuniversalcfremote_rssi", + "sensor.king_of_fans_inc_hbuniversalcfremote_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.king_of_fans_inc_hbuniversalcfremote_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.king_of_fans_inc_hbuniversalcfremote_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.king_of_fans_inc_hbuniversalcfremote_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.king_of_fans_inc_hbuniversalcfremote_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.king_of_fans_inc_hbuniversalcfremote_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.king_of_fans_inc_hbuniversalcfremote_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.king_of_fans_inc_hbuniversalcfremote_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.king_of_fans_inc_hbuniversalcfremote_lqi", }, ("fan", "00:11:22:33:44:55:66:77-1-514"): { DEV_SIG_CHANNELS: ["fan"], DEV_SIG_ENT_MAP_CLASS: "ZhaFan", - DEV_SIG_ENT_MAP_ID: "fan.king_of_fans_inc_hbuniversalcfremote_77665544_fan", + DEV_SIG_ENT_MAP_ID: "fan.king_of_fans_inc_hbuniversalcfremote_fan", }, }, }, @@ -1997,31 +1997,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019", "1:0x0300"], DEV_SIG_ENTITIES: [ - "button.lds_zbt_cctswitch_d0001_77665544_identify", - "sensor.lds_zbt_cctswitch_d0001_77665544_power", - "sensor.lds_zbt_cctswitch_d0001_77665544_basic_rssi", - "sensor.lds_zbt_cctswitch_d0001_77665544_basic_lqi", + "button.lds_zbt_cctswitch_d0001_identifybutton", + "sensor.lds_zbt_cctswitch_d0001_battery", + "sensor.lds_zbt_cctswitch_d0001_rssi", + "sensor.lds_zbt_cctswitch_d0001_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lds_zbt_cctswitch_d0001_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lds_zbt_cctswitch_d0001_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_lqi", }, }, }, @@ -2041,31 +2041,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ledvance_a19_rgbw_77665544_identify", - "light.ledvance_a19_rgbw_77665544_level_light_color_on_off", - "sensor.ledvance_a19_rgbw_77665544_basic_rssi", - "sensor.ledvance_a19_rgbw_77665544_basic_lqi", + "button.ledvance_a19_rgbw_identifybutton", + "light.ledvance_a19_rgbw_light", + "sensor.ledvance_a19_rgbw_rssi", + "sensor.ledvance_a19_rgbw_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ledvance_a19_rgbw_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.ledvance_a19_rgbw_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ledvance_a19_rgbw_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ledvance_a19_rgbw_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_a19_rgbw_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_a19_rgbw_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_a19_rgbw_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_a19_rgbw_lqi", }, }, }, @@ -2085,31 +2085,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ledvance_flex_rgbw_77665544_identify", - "light.ledvance_flex_rgbw_77665544_level_light_color_on_off", - "sensor.ledvance_flex_rgbw_77665544_basic_rssi", - "sensor.ledvance_flex_rgbw_77665544_basic_lqi", + "button.ledvance_flex_rgbw_identifybutton", + "light.ledvance_flex_rgbw_light", + "sensor.ledvance_flex_rgbw_rssi", + "sensor.ledvance_flex_rgbw_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ledvance_flex_rgbw_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.ledvance_flex_rgbw_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ledvance_flex_rgbw_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ledvance_flex_rgbw_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_flex_rgbw_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_flex_rgbw_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_flex_rgbw_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_flex_rgbw_lqi", }, }, }, @@ -2129,31 +2129,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ledvance_plug_77665544_identify", - "switch.ledvance_plug_77665544_on_off", - "sensor.ledvance_plug_77665544_basic_rssi", - "sensor.ledvance_plug_77665544_basic_lqi", + "button.ledvance_plug_identifybutton", + "switch.ledvance_plug_switch", + "sensor.ledvance_plug_rssi", + "sensor.ledvance_plug_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.ledvance_plug_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.ledvance_plug_switch", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ledvance_plug_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ledvance_plug_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_plug_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_plug_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_plug_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_plug_lqi", }, }, }, @@ -2173,31 +2173,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.ledvance_rt_rgbw_77665544_identify", - "light.ledvance_rt_rgbw_77665544_level_light_color_on_off", - "sensor.ledvance_rt_rgbw_77665544_basic_rssi", - "sensor.ledvance_rt_rgbw_77665544_basic_lqi", + "button.ledvance_rt_rgbw_identifybutton", + "light.ledvance_rt_rgbw_light", + "sensor.ledvance_rt_rgbw_rssi", + "sensor.ledvance_rt_rgbw_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ledvance_rt_rgbw_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.ledvance_rt_rgbw_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ledvance_rt_rgbw_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.ledvance_rt_rgbw_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_rt_rgbw_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_rt_rgbw_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_rt_rgbw_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.ledvance_rt_rgbw_lqi", }, }, }, @@ -2238,91 +2238,91 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_plug_maus01_77665544_identify", - "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement", - "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_apparent_power", - "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_current", - "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_rms_voltage", - "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_ac_frequency", - "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_power_factor", - "sensor.lumi_lumi_plug_maus01_77665544_analog_input", - "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", - "binary_sensor.lumi_lumi_plug_maus01_77665544_binary_input", - "switch.lumi_lumi_plug_maus01_77665544_on_off", - "sensor.lumi_lumi_plug_maus01_77665544_basic_rssi", - "sensor.lumi_lumi_plug_maus01_77665544_basic_lqi", - "sensor.lumi_lumi_plug_maus01_77665544_device_temperature", + "button.lumi_lumi_plug_maus01_identifybutton", + "sensor.lumi_lumi_plug_maus01_electricalmeasurement", + "sensor.lumi_lumi_plug_maus01_electricalmeasurementapparentpower", + "sensor.lumi_lumi_plug_maus01_electricalmeasurementrmscurrent", + "sensor.lumi_lumi_plug_maus01_electricalmeasurementrmsvoltage", + "sensor.lumi_lumi_plug_maus01_electricalmeasurementfrequency", + "sensor.lumi_lumi_plug_maus01_electricalmeasurementpowerfactor", + "sensor.lumi_lumi_plug_maus01_analoginput", + "sensor.lumi_lumi_plug_maus01_analoginput_2", + "binary_sensor.lumi_lumi_plug_maus01_binaryinput", + "switch.lumi_lumi_plug_maus01_switch", + "sensor.lumi_lumi_plug_maus01_rssi", + "sensor.lumi_lumi_plug_maus01_lqi", + "sensor.lumi_lumi_plug_maus01_devicetemperature", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.lumi_lumi_plug_maus01_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.lumi_lumi_plug_maus01_switch", }, ("sensor", "00:11:22:33:44:55:66:77-1-2"): { DEV_SIG_CHANNELS: ["device_temperature"], DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_device_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_devicetemperature", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_plug_maus01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_plug_maus01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementapparentpower", }, ("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", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementrmscurrent", }, ("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", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-2-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_analog_input", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_analoginput", }, ("sensor", "00:11:22:33:44:55:66:77-3-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_analoginput_2", }, ("binary_sensor", "00:11:22:33:44:55:66:77-100-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_plug_maus01_77665544_binary_input", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_plug_maus01_binaryinput", }, }, }, @@ -2349,79 +2349,79 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_relay_c2acn01_77665544_identify", - "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_apparent_power", - "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_current", - "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_rms_voltage", - "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_ac_frequency", - "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_power_factor", - "sensor.lumi_lumi_relay_c2acn01_77665544_basic_rssi", - "sensor.lumi_lumi_relay_c2acn01_77665544_basic_lqi", - "sensor.lumi_lumi_relay_c2acn01_77665544_device_temperature", + "button.lumi_lumi_relay_c2acn01_identifybutton", + "light.lumi_lumi_relay_c2acn01_light", + "light.lumi_lumi_relay_c2acn01_light_2", + "sensor.lumi_lumi_relay_c2acn01_electricalmeasurement", + "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementapparentpower", + "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementrmscurrent", + "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementrmsvoltage", + "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementfrequency", + "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementpowerfactor", + "sensor.lumi_lumi_relay_c2acn01_rssi", + "sensor.lumi_lumi_relay_c2acn01_lqi", + "sensor.lumi_lumi_relay_c2acn01_devicetemperature", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_relay_c2acn01_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_relay_c2acn01_light", }, ("sensor", "00:11:22:33:44:55:66:77-1-2"): { DEV_SIG_CHANNELS: ["device_temperature"], DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_device_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_devicetemperature", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_relay_c2acn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_relay_c2acn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementapparentpower", }, ("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", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementrmscurrent", }, ("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", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_lqi", }, ("light", "00:11:22:33:44:55:66:77-2"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_relay_c2acn01_77665544_on_off_2", + DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_relay_c2acn01_light_2", }, }, }, @@ -2455,31 +2455,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_remote_b186acn01_77665544_identify", - "sensor.lumi_lumi_remote_b186acn01_77665544_power", - "sensor.lumi_lumi_remote_b186acn01_77665544_basic_rssi", - "sensor.lumi_lumi_remote_b186acn01_77665544_basic_lqi", + "button.lumi_lumi_remote_b186acn01_identifybutton", + "sensor.lumi_lumi_remote_b186acn01_battery", + "sensor.lumi_lumi_remote_b186acn01_rssi", + "sensor.lumi_lumi_remote_b186acn01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b186acn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b186acn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_lqi", }, }, }, @@ -2513,31 +2513,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_remote_b286acn01_77665544_identify", - "sensor.lumi_lumi_remote_b286acn01_77665544_power", - "sensor.lumi_lumi_remote_b286acn01_77665544_basic_rssi", - "sensor.lumi_lumi_remote_b286acn01_77665544_basic_lqi", + "button.lumi_lumi_remote_b286acn01_identifybutton", + "sensor.lumi_lumi_remote_b286acn01_battery", + "sensor.lumi_lumi_remote_b286acn01_rssi", + "sensor.lumi_lumi_remote_b286acn01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286acn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286acn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_lqi", }, }, }, @@ -2592,25 +2592,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_remote_b286opcn01_77665544_identify", - "sensor.lumi_lumi_remote_b286opcn01_77665544_basic_rssi", - "sensor.lumi_lumi_remote_b286opcn01_77665544_basic_lqi", + "button.lumi_lumi_remote_b286opcn01_identifybutton", + "sensor.lumi_lumi_remote_b286opcn01_rssi", + "sensor.lumi_lumi_remote_b286opcn01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286opcn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286opcn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286opcn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286opcn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286opcn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286opcn01_lqi", }, }, }, @@ -2665,25 +2665,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300", "2:0x0006"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_remote_b486opcn01_77665544_identify", - "sensor.lumi_lumi_remote_b486opcn01_77665544_basic_rssi", - "sensor.lumi_lumi_remote_b486opcn01_77665544_basic_lqi", + "button.lumi_lumi_remote_b486opcn01_identifybutton", + "sensor.lumi_lumi_remote_b486opcn01_rssi", + "sensor.lumi_lumi_remote_b486opcn01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b486opcn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b486opcn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b486opcn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b486opcn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b486opcn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b486opcn01_lqi", }, }, }, @@ -2703,25 +2703,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_remote_b686opcn01_77665544_identify", - "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_rssi", - "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_lqi", + "button.lumi_lumi_remote_b686opcn01_identifybutton", + "sensor.lumi_lumi_remote_b686opcn01_rssi", + "sensor.lumi_lumi_remote_b686opcn01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_lqi", }, }, }, @@ -2776,25 +2776,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0300", "2:0x0006"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_remote_b686opcn01_77665544_identify", - "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_rssi", - "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_lqi", + "button.lumi_lumi_remote_b686opcn01_identifybutton", + "sensor.lumi_lumi_remote_b686opcn01_rssi", + "sensor.lumi_lumi_remote_b686opcn01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_lqi", }, }, }, @@ -2814,31 +2814,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["8:0x0006"], DEV_SIG_ENTITIES: [ - "light.lumi_lumi_router_77665544_on_off", - "binary_sensor.lumi_lumi_router_77665544_on_off", - "sensor.lumi_lumi_router_77665544_basic_rssi", - "sensor.lumi_lumi_router_77665544_basic_lqi", + "light.lumi_lumi_router_light", + "binary_sensor.lumi_lumi_router_opening", + "sensor.lumi_lumi_router_rssi", + "sensor.lumi_lumi_router_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-8"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_light", }, ("sensor", "00:11:22:33:44:55:66:77-8-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-8-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_opening", }, }, }, @@ -2858,31 +2858,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["8:0x0006"], DEV_SIG_ENTITIES: [ - "light.lumi_lumi_router_77665544_on_off", - "binary_sensor.lumi_lumi_router_77665544_on_off", - "sensor.lumi_lumi_router_77665544_basic_rssi", - "sensor.lumi_lumi_router_77665544_basic_lqi", + "light.lumi_lumi_router_light", + "binary_sensor.lumi_lumi_router_opening", + "sensor.lumi_lumi_router_rssi", + "sensor.lumi_lumi_router_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-8"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_light", }, ("sensor", "00:11:22:33:44:55:66:77-8-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-8-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_opening", }, }, }, @@ -2902,31 +2902,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["8:0x0006"], DEV_SIG_ENTITIES: [ - "light.lumi_lumi_router_77665544_on_off", - "binary_sensor.lumi_lumi_router_77665544_on_off", - "sensor.lumi_lumi_router_77665544_basic_rssi", - "sensor.lumi_lumi_router_77665544_basic_lqi", + "light.lumi_lumi_router_light", + "binary_sensor.lumi_lumi_router_opening", + "sensor.lumi_lumi_router_rssi", + "sensor.lumi_lumi_router_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-8"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_light", }, ("sensor", "00:11:22:33:44:55:66:77-8-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-8-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_opening", }, }, }, @@ -2946,31 +2946,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sen_ill_mgl01_77665544_identify", - "sensor.lumi_lumi_sen_ill_mgl01_77665544_illuminance", - "sensor.lumi_lumi_sen_ill_mgl01_77665544_basic_rssi", - "sensor.lumi_lumi_sen_ill_mgl01_77665544_basic_lqi", + "button.lumi_lumi_sen_ill_mgl01_identifybutton", + "sensor.lumi_lumi_sen_ill_mgl01_illuminance", + "sensor.lumi_lumi_sen_ill_mgl01_rssi", + "sensor.lumi_lumi_sen_ill_mgl01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sen_ill_mgl01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sen_ill_mgl01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1024"): { DEV_SIG_CHANNELS: ["illuminance"], DEV_SIG_ENT_MAP_CLASS: "Illuminance", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_77665544_illuminance", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_illuminance", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_lqi", }, }, }, @@ -3004,31 +3004,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_86sw1_77665544_identify", - "sensor.lumi_lumi_sensor_86sw1_77665544_power", - "sensor.lumi_lumi_sensor_86sw1_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_86sw1_77665544_basic_lqi", + "button.lumi_lumi_sensor_86sw1_identifybutton", + "sensor.lumi_lumi_sensor_86sw1_battery", + "sensor.lumi_lumi_sensor_86sw1_rssi", + "sensor.lumi_lumi_sensor_86sw1_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_86sw1_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_86sw1_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_lqi", }, }, }, @@ -3062,31 +3062,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_cube_aqgl01_77665544_identify", - "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_power", - "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_basic_lqi", + "button.lumi_lumi_sensor_cube_aqgl01_identifybutton", + "sensor.lumi_lumi_sensor_cube_aqgl01_battery", + "sensor.lumi_lumi_sensor_cube_aqgl01_rssi", + "sensor.lumi_lumi_sensor_cube_aqgl01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_cube_aqgl01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_cube_aqgl01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_lqi", }, }, }, @@ -3120,43 +3120,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_ht_77665544_identify", - "sensor.lumi_lumi_sensor_ht_77665544_power", - "sensor.lumi_lumi_sensor_ht_77665544_temperature", - "sensor.lumi_lumi_sensor_ht_77665544_humidity", - "sensor.lumi_lumi_sensor_ht_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_ht_77665544_basic_lqi", + "button.lumi_lumi_sensor_ht_identifybutton", + "sensor.lumi_lumi_sensor_ht_battery", + "sensor.lumi_lumi_sensor_ht_temperature", + "sensor.lumi_lumi_sensor_ht_humidity", + "sensor.lumi_lumi_sensor_ht_rssi", + "sensor.lumi_lumi_sensor_ht_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_ht_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_ht_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-1-1029"): { DEV_SIG_CHANNELS: ["humidity"], DEV_SIG_ENT_MAP_CLASS: "Humidity", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_77665544_humidity", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_humidity", }, }, }, @@ -3176,37 +3176,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_magnet_77665544_identify", - "sensor.lumi_lumi_sensor_magnet_77665544_power", - "binary_sensor.lumi_lumi_sensor_magnet_77665544_on_off", - "sensor.lumi_lumi_sensor_magnet_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_magnet_77665544_basic_lqi", + "button.lumi_lumi_sensor_magnet_identifybutton", + "sensor.lumi_lumi_sensor_magnet_battery", + "binary_sensor.lumi_lumi_sensor_magnet_opening", + "sensor.lumi_lumi_sensor_magnet_rssi", + "sensor.lumi_lumi_sensor_magnet_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_magnet_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_magnet_opening", }, }, }, @@ -3226,37 +3226,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_magnet_aq2_77665544_identify", - "sensor.lumi_lumi_sensor_magnet_aq2_77665544_power", - "binary_sensor.lumi_lumi_sensor_magnet_aq2_77665544_on_off", - "sensor.lumi_lumi_sensor_magnet_aq2_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_magnet_aq2_77665544_basic_lqi", + "button.lumi_lumi_sensor_magnet_aq2_identifybutton", + "sensor.lumi_lumi_sensor_magnet_aq2_battery", + "binary_sensor.lumi_lumi_sensor_magnet_aq2_opening", + "sensor.lumi_lumi_sensor_magnet_aq2_rssi", + "sensor.lumi_lumi_sensor_magnet_aq2_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_aq2_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_aq2_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_magnet_aq2_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_magnet_aq2_opening", }, }, }, @@ -3276,49 +3276,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_motion_aq2_77665544_identify", - "sensor.lumi_lumi_sensor_motion_aq2_77665544_power", - "sensor.lumi_lumi_sensor_motion_aq2_77665544_illuminance", - "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_occupancy", - "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_ias_zone", - "sensor.lumi_lumi_sensor_motion_aq2_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_motion_aq2_77665544_basic_lqi", + "button.lumi_lumi_sensor_motion_aq2_identifybutton", + "sensor.lumi_lumi_sensor_motion_aq2_battery", + "sensor.lumi_lumi_sensor_motion_aq2_illuminance", + "binary_sensor.lumi_lumi_sensor_motion_aq2_occupancy", + "binary_sensor.lumi_lumi_sensor_motion_aq2_iaszone", + "sensor.lumi_lumi_sensor_motion_aq2_rssi", + "sensor.lumi_lumi_sensor_motion_aq2_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1030"): { DEV_SIG_CHANNELS: ["occupancy"], DEV_SIG_ENT_MAP_CLASS: "Occupancy", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_occupancy", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_motion_aq2_occupancy", }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_motion_aq2_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_motion_aq2_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_motion_aq2_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1024"): { DEV_SIG_CHANNELS: ["illuminance"], DEV_SIG_ENT_MAP_CLASS: "Illuminance", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_77665544_illuminance", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_illuminance", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_lqi", }, }, }, @@ -3338,37 +3338,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_smoke_77665544_identify", - "sensor.lumi_lumi_sensor_smoke_77665544_power", - "binary_sensor.lumi_lumi_sensor_smoke_77665544_ias_zone", - "sensor.lumi_lumi_sensor_smoke_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_smoke_77665544_basic_lqi", + "button.lumi_lumi_sensor_smoke_identifybutton", + "sensor.lumi_lumi_sensor_smoke_battery", + "binary_sensor.lumi_lumi_sensor_smoke_iaszone", + "sensor.lumi_lumi_sensor_smoke_rssi", + "sensor.lumi_lumi_sensor_smoke_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_smoke_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_smoke_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_smoke_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_smoke_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_lqi", }, }, }, @@ -3388,31 +3388,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_switch_77665544_identify", - "sensor.lumi_lumi_sensor_switch_77665544_power", - "sensor.lumi_lumi_sensor_switch_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_switch_77665544_basic_lqi", + "button.lumi_lumi_sensor_switch_identifybutton", + "sensor.lumi_lumi_sensor_switch_battery", + "sensor.lumi_lumi_sensor_switch_rssi", + "sensor.lumi_lumi_sensor_switch_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_switch_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_switch_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_lqi", }, }, }, @@ -3432,25 +3432,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006"], DEV_SIG_ENTITIES: [ - "sensor.lumi_lumi_sensor_switch_aq2_77665544_power", - "sensor.lumi_lumi_sensor_switch_aq2_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_switch_aq2_77665544_basic_lqi", + "sensor.lumi_lumi_sensor_switch_aq2_battery", + "sensor.lumi_lumi_sensor_switch_aq2_rssi", + "sensor.lumi_lumi_sensor_switch_aq2_lqi", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_lqi", }, }, }, @@ -3470,25 +3470,25 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006"], DEV_SIG_ENTITIES: [ - "sensor.lumi_lumi_sensor_switch_aq3_77665544_power", - "sensor.lumi_lumi_sensor_switch_aq3_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_switch_aq3_77665544_basic_lqi", + "sensor.lumi_lumi_sensor_switch_aq3_battery", + "sensor.lumi_lumi_sensor_switch_aq3_rssi", + "sensor.lumi_lumi_sensor_switch_aq3_lqi", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_lqi", }, }, }, @@ -3508,43 +3508,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_sensor_wleak_aq1_77665544_identify", - "sensor.lumi_lumi_sensor_wleak_aq1_77665544_power", - "binary_sensor.lumi_lumi_sensor_wleak_aq1_77665544_ias_zone", - "sensor.lumi_lumi_sensor_wleak_aq1_77665544_basic_rssi", - "sensor.lumi_lumi_sensor_wleak_aq1_77665544_basic_lqi", - "sensor.lumi_lumi_sensor_wleak_aq1_77665544_device_temperature", + "button.lumi_lumi_sensor_wleak_aq1_identifybutton", + "sensor.lumi_lumi_sensor_wleak_aq1_battery", + "binary_sensor.lumi_lumi_sensor_wleak_aq1_iaszone", + "sensor.lumi_lumi_sensor_wleak_aq1_rssi", + "sensor.lumi_lumi_sensor_wleak_aq1_lqi", + "sensor.lumi_lumi_sensor_wleak_aq1_devicetemperature", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_wleak_aq1_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_wleak_aq1_iaszone", }, ("sensor", "00:11:22:33:44:55:66:77-1-2"): { DEV_SIG_CHANNELS: ["device_temperature"], DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_77665544_device_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_devicetemperature", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_wleak_aq1_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_wleak_aq1_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_lqi", }, }, }, @@ -3571,43 +3571,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0019", "2:0x0005"], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_vibration_aq1_77665544_identify", - "sensor.lumi_lumi_vibration_aq1_77665544_power", - "binary_sensor.lumi_lumi_vibration_aq1_77665544_ias_zone", - "lock.lumi_lumi_vibration_aq1_77665544_door_lock", - "sensor.lumi_lumi_vibration_aq1_77665544_basic_rssi", - "sensor.lumi_lumi_vibration_aq1_77665544_basic_lqi", + "button.lumi_lumi_vibration_aq1_identifybutton", + "sensor.lumi_lumi_vibration_aq1_battery", + "binary_sensor.lumi_lumi_vibration_aq1_iaszone", + "lock.lumi_lumi_vibration_aq1_doorlock", + "sensor.lumi_lumi_vibration_aq1_rssi", + "sensor.lumi_lumi_vibration_aq1_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_vibration_aq1_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_vibration_aq1_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_vibration_aq1_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_vibration_aq1_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_lqi", }, ("lock", "00:11:22:33:44:55:66:77-1-257"): { DEV_SIG_CHANNELS: ["door_lock"], DEV_SIG_ENT_MAP_CLASS: "ZhaDoorLock", - DEV_SIG_ENT_MAP_ID: "lock.lumi_lumi_vibration_aq1_77665544_door_lock", + DEV_SIG_ENT_MAP_ID: "lock.lumi_lumi_vibration_aq1_doorlock", }, }, }, @@ -3627,49 +3627,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.lumi_lumi_weather_77665544_identify", - "sensor.lumi_lumi_weather_77665544_power", - "sensor.lumi_lumi_weather_77665544_pressure", - "sensor.lumi_lumi_weather_77665544_temperature", - "sensor.lumi_lumi_weather_77665544_humidity", - "sensor.lumi_lumi_weather_77665544_basic_rssi", - "sensor.lumi_lumi_weather_77665544_basic_lqi", + "button.lumi_lumi_weather_identifybutton", + "sensor.lumi_lumi_weather_battery", + "sensor.lumi_lumi_weather_pressure", + "sensor.lumi_lumi_weather_temperature", + "sensor.lumi_lumi_weather_humidity", + "sensor.lumi_lumi_weather_rssi", + "sensor.lumi_lumi_weather_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_weather_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_weather_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { DEV_SIG_CHANNELS: ["pressure"], DEV_SIG_ENT_MAP_CLASS: "Pressure", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_pressure", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_pressure", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-1-1029"): { DEV_SIG_CHANNELS: ["humidity"], DEV_SIG_ENT_MAP_CLASS: "Humidity", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_77665544_humidity", + DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_humidity", }, }, }, @@ -3689,37 +3689,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.nyce_3010_77665544_identify", - "sensor.nyce_3010_77665544_power", - "binary_sensor.nyce_3010_77665544_ias_zone", - "sensor.nyce_3010_77665544_basic_rssi", - "sensor.nyce_3010_77665544_basic_lqi", + "button.nyce_3010_identifybutton", + "sensor.nyce_3010_battery", + "binary_sensor.nyce_3010_iaszone", + "sensor.nyce_3010_rssi", + "sensor.nyce_3010_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.nyce_3010_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.nyce_3010_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.nyce_3010_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.nyce_3010_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_lqi", }, }, }, @@ -3739,37 +3739,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.nyce_3014_77665544_identify", - "sensor.nyce_3014_77665544_power", - "binary_sensor.nyce_3014_77665544_ias_zone", - "sensor.nyce_3014_77665544_basic_rssi", - "sensor.nyce_3014_77665544_basic_lqi", + "button.nyce_3014_identifybutton", + "sensor.nyce_3014_battery", + "binary_sensor.nyce_3014_iaszone", + "sensor.nyce_3014_rssi", + "sensor.nyce_3014_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.nyce_3014_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.nyce_3014_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.nyce_3014_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.nyce_3014_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_lqi", }, }, }, @@ -3832,31 +3832,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["3:0x0019"], DEV_SIG_ENTITIES: [ - "button.osram_lightify_a19_rgbw_77665544_identify", - "light.osram_lightify_a19_rgbw_77665544_level_light_color_on_off", - "sensor.osram_lightify_a19_rgbw_77665544_basic_rssi", - "sensor.osram_lightify_a19_rgbw_77665544_basic_lqi", + "button.osram_lightify_a19_rgbw_identifybutton", + "light.osram_lightify_a19_rgbw_light", + "sensor.osram_lightify_a19_rgbw_rssi", + "sensor.osram_lightify_a19_rgbw_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-3"): { DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.osram_lightify_a19_rgbw_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.osram_lightify_a19_rgbw_light", }, ("button", "00:11:22:33:44:55:66:77-3-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_lightify_a19_rgbw_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.osram_lightify_a19_rgbw_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_a19_rgbw_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_a19_rgbw_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_a19_rgbw_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_a19_rgbw_lqi", }, }, }, @@ -3876,31 +3876,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0006", "1:0x0008", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.osram_lightify_dimming_switch_77665544_identify", - "sensor.osram_lightify_dimming_switch_77665544_power", - "sensor.osram_lightify_dimming_switch_77665544_basic_rssi", - "sensor.osram_lightify_dimming_switch_77665544_basic_lqi", + "button.osram_lightify_dimming_switch_identifybutton", + "sensor.osram_lightify_dimming_switch_battery", + "sensor.osram_lightify_dimming_switch_rssi", + "sensor.osram_lightify_dimming_switch_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_lightify_dimming_switch_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.osram_lightify_dimming_switch_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_lqi", }, }, }, @@ -3920,31 +3920,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["3:0x0019"], DEV_SIG_ENTITIES: [ - "button.osram_lightify_flex_rgbw_77665544_identify", - "light.osram_lightify_flex_rgbw_77665544_level_light_color_on_off", - "sensor.osram_lightify_flex_rgbw_77665544_basic_rssi", - "sensor.osram_lightify_flex_rgbw_77665544_basic_lqi", + "button.osram_lightify_flex_rgbw_identifybutton", + "light.osram_lightify_flex_rgbw_light", + "sensor.osram_lightify_flex_rgbw_rssi", + "sensor.osram_lightify_flex_rgbw_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-3"): { DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.osram_lightify_flex_rgbw_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.osram_lightify_flex_rgbw_light", }, ("button", "00:11:22:33:44:55:66:77-3-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_lightify_flex_rgbw_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.osram_lightify_flex_rgbw_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_flex_rgbw_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_flex_rgbw_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_flex_rgbw_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_flex_rgbw_lqi", }, }, }, @@ -3964,67 +3964,67 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["3:0x0019"], DEV_SIG_ENTITIES: [ - "button.osram_lightify_rt_tunable_white_77665544_identify", - "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_apparent_power", - "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_current", - "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_rms_voltage", - "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_ac_frequency", - "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_power_factor", - "sensor.osram_lightify_rt_tunable_white_77665544_basic_rssi", - "sensor.osram_lightify_rt_tunable_white_77665544_basic_lqi", + "button.osram_lightify_rt_tunable_white_identifybutton", + "light.osram_lightify_rt_tunable_white_light", + "sensor.osram_lightify_rt_tunable_white_electricalmeasurement", + "sensor.osram_lightify_rt_tunable_white_electricalmeasurementapparentpower", + "sensor.osram_lightify_rt_tunable_white_electricalmeasurementrmscurrent", + "sensor.osram_lightify_rt_tunable_white_electricalmeasurementrmsvoltage", + "sensor.osram_lightify_rt_tunable_white_electricalmeasurementfrequency", + "sensor.osram_lightify_rt_tunable_white_electricalmeasurementpowerfactor", + "sensor.osram_lightify_rt_tunable_white_rssi", + "sensor.osram_lightify_rt_tunable_white_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-3"): { DEV_SIG_CHANNELS: ["on_off", "light_color", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.osram_lightify_rt_tunable_white_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.osram_lightify_rt_tunable_white_light", }, ("button", "00:11:22:33:44:55:66:77-3-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_lightify_rt_tunable_white_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.osram_lightify_rt_tunable_white_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementapparentpower", }, ("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", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementrmscurrent", }, ("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_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_lqi", }, }, }, @@ -4044,67 +4044,67 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["3:0x0019"], DEV_SIG_ENTITIES: [ - "button.osram_plug_01_77665544_identify", - "sensor.osram_plug_01_77665544_electrical_measurement", - "sensor.osram_plug_01_77665544_electrical_measurement_apparent_power", - "sensor.osram_plug_01_77665544_electrical_measurement_rms_current", - "sensor.osram_plug_01_77665544_electrical_measurement_rms_voltage", - "sensor.osram_plug_01_77665544_electrical_measurement_ac_frequency", - "sensor.osram_plug_01_77665544_electrical_measurement_power_factor", - "switch.osram_plug_01_77665544_on_off", - "sensor.osram_plug_01_77665544_basic_rssi", - "sensor.osram_plug_01_77665544_basic_lqi", + "button.osram_plug_01_identifybutton", + "sensor.osram_plug_01_electricalmeasurement", + "sensor.osram_plug_01_electricalmeasurementapparentpower", + "sensor.osram_plug_01_electricalmeasurementrmscurrent", + "sensor.osram_plug_01_electricalmeasurementrmsvoltage", + "sensor.osram_plug_01_electricalmeasurementfrequency", + "sensor.osram_plug_01_electricalmeasurementpowerfactor", + "switch.osram_plug_01_switch", + "sensor.osram_plug_01_rssi", + "sensor.osram_plug_01_lqi", ], DEV_SIG_ENT_MAP: { ("switch", "00:11:22:33:44:55:66:77-3"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.osram_plug_01_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.osram_plug_01_switch", }, ("button", "00:11:22:33:44:55:66:77-3-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_plug_01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.osram_plug_01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementapparentpower", }, ("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", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementrmscurrent", }, ("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_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-3-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_lqi", }, }, }, @@ -4185,25 +4185,25 @@ DEVICES = [ "6:0x0300", ], DEV_SIG_ENTITIES: [ - "sensor.osram_switch_4x_lightify_77665544_power", - "sensor.osram_switch_4x_lightify_77665544_basic_rssi", - "sensor.osram_switch_4x_lightify_77665544_basic_lqi", + "sensor.osram_switch_4x_lightify_battery", + "sensor.osram_switch_4x_lightify_rssi", + "sensor.osram_switch_4x_lightify_lqi", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_lqi", }, }, }, @@ -4230,37 +4230,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0008", "2:0x0019"], DEV_SIG_ENTITIES: [ - "button.philips_rwl020_77665544_identify", - "sensor.philips_rwl020_77665544_power", - "binary_sensor.philips_rwl020_77665544_binary_input", - "sensor.philips_rwl020_77665544_basic_rssi", - "sensor.philips_rwl020_77665544_basic_lqi", + "button.philips_rwl020_identifybutton", + "sensor.philips_rwl020_battery", + "binary_sensor.philips_rwl020_binaryinput", + "sensor.philips_rwl020_rssi", + "sensor.philips_rwl020_lqi", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_lqi", }, ("binary_sensor", "00:11:22:33:44:55:66:77-2-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput", - DEV_SIG_ENT_MAP_ID: "binary_sensor.philips_rwl020_77665544_binary_input", + DEV_SIG_ENT_MAP_ID: "binary_sensor.philips_rwl020_binaryinput", }, ("button", "00:11:22:33:44:55:66:77-2-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.philips_rwl020_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.philips_rwl020_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-2-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_battery", }, }, }, @@ -4280,43 +4280,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.samjin_button_77665544_identify", - "sensor.samjin_button_77665544_power", - "sensor.samjin_button_77665544_temperature", - "binary_sensor.samjin_button_77665544_ias_zone", - "sensor.samjin_button_77665544_basic_rssi", - "sensor.samjin_button_77665544_basic_lqi", + "button.samjin_button_identifybutton", + "sensor.samjin_button_battery", + "sensor.samjin_button_temperature", + "binary_sensor.samjin_button_iaszone", + "sensor.samjin_button_rssi", + "sensor.samjin_button_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_button_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_button_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.samjin_button_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.samjin_button_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_lqi", }, }, }, @@ -4336,43 +4336,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.samjin_multi_77665544_identify", - "sensor.samjin_multi_77665544_power", - "sensor.samjin_multi_77665544_temperature", - "binary_sensor.samjin_multi_77665544_ias_zone", - "sensor.samjin_multi_77665544_basic_rssi", - "sensor.samjin_multi_77665544_basic_lqi", + "button.samjin_multi_identifybutton", + "sensor.samjin_multi_battery", + "sensor.samjin_multi_temperature", + "binary_sensor.samjin_multi_iaszone", + "sensor.samjin_multi_rssi", + "sensor.samjin_multi_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_multi_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_multi_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.samjin_multi_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.samjin_multi_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_lqi", }, }, }, @@ -4392,43 +4392,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.samjin_water_77665544_identify", - "sensor.samjin_water_77665544_power", - "sensor.samjin_water_77665544_temperature", - "binary_sensor.samjin_water_77665544_ias_zone", - "sensor.samjin_water_77665544_basic_rssi", - "sensor.samjin_water_77665544_basic_lqi", + "button.samjin_water_identifybutton", + "sensor.samjin_water_battery", + "sensor.samjin_water_temperature", + "binary_sensor.samjin_water_iaszone", + "sensor.samjin_water_rssi", + "sensor.samjin_water_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_water_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_water_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.samjin_water_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.samjin_water_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_lqi", }, }, }, @@ -4448,67 +4448,67 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0005", "1:0x0006", "1:0x0019"], DEV_SIG_ENTITIES: [ - "button.securifi_ltd_unk_model_77665544_identify", - "sensor.securifi_ltd_unk_model_77665544_electrical_measurement", - "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_apparent_power", - "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_current", - "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_rms_voltage", - "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_ac_frequency", - "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_power_factor", - "switch.securifi_ltd_unk_model_77665544_on_off", - "sensor.securifi_ltd_unk_model_77665544_basic_rssi", - "sensor.securifi_ltd_unk_model_77665544_basic_lqi", + "button.securifi_ltd_unk_model_identifybutton", + "sensor.securifi_ltd_unk_model_electricalmeasurement", + "sensor.securifi_ltd_unk_model_electricalmeasurementapparentpower", + "sensor.securifi_ltd_unk_model_electricalmeasurementrmscurrent", + "sensor.securifi_ltd_unk_model_electricalmeasurementrmsvoltage", + "sensor.securifi_ltd_unk_model_electricalmeasurementfrequency", + "sensor.securifi_ltd_unk_model_electricalmeasurementpowerfactor", + "switch.securifi_ltd_unk_model_switch", + "sensor.securifi_ltd_unk_model_rssi", + "sensor.securifi_ltd_unk_model_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.securifi_ltd_unk_model_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.securifi_ltd_unk_model_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementapparentpower", }, ("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", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementrmscurrent", }, ("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_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_lqi", }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.securifi_ltd_unk_model_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.securifi_ltd_unk_model_switch", }, }, }, @@ -4528,43 +4528,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sercomm_corp_sz_dws04n_sf_77665544_identify", - "sensor.sercomm_corp_sz_dws04n_sf_77665544_power", - "sensor.sercomm_corp_sz_dws04n_sf_77665544_temperature", - "binary_sensor.sercomm_corp_sz_dws04n_sf_77665544_ias_zone", - "sensor.sercomm_corp_sz_dws04n_sf_77665544_basic_rssi", - "sensor.sercomm_corp_sz_dws04n_sf_77665544_basic_lqi", + "button.sercomm_corp_sz_dws04n_sf_identifybutton", + "sensor.sercomm_corp_sz_dws04n_sf_battery", + "sensor.sercomm_corp_sz_dws04n_sf_temperature", + "binary_sensor.sercomm_corp_sz_dws04n_sf_iaszone", + "sensor.sercomm_corp_sz_dws04n_sf_rssi", + "sensor.sercomm_corp_sz_dws04n_sf_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.sercomm_corp_sz_dws04n_sf_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.sercomm_corp_sz_dws04n_sf_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_dws04n_sf_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_dws04n_sf_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_lqi", }, }, }, @@ -4591,79 +4591,79 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019", "2:0x0006"], DEV_SIG_ENTITIES: [ - "button.sercomm_corp_sz_esw01_77665544_identify", - "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", - "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_apparent_power", - "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_electrical_measurement_ac_frequency", - "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_power_factor", - "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering", - "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering_summation_delivered", - "light.sercomm_corp_sz_esw01_77665544_on_off", - "sensor.sercomm_corp_sz_esw01_77665544_basic_rssi", - "sensor.sercomm_corp_sz_esw01_77665544_basic_lqi", + "button.sercomm_corp_sz_esw01_identifybutton", + "sensor.sercomm_corp_sz_esw01_electricalmeasurement", + "sensor.sercomm_corp_sz_esw01_electricalmeasurementapparentpower", + "sensor.sercomm_corp_sz_esw01_electricalmeasurementrmscurrent", + "sensor.sercomm_corp_sz_esw01_electricalmeasurementrmsvoltage", + "sensor.sercomm_corp_sz_esw01_electricalmeasurementfrequency", + "sensor.sercomm_corp_sz_esw01_electricalmeasurementpowerfactor", + "sensor.sercomm_corp_sz_esw01_smartenergymetering", + "sensor.sercomm_corp_sz_esw01_smartenergysummation", + "light.sercomm_corp_sz_esw01_light", + "sensor.sercomm_corp_sz_esw01_rssi", + "sensor.sercomm_corp_sz_esw01_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.sercomm_corp_sz_esw01_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.sercomm_corp_sz_esw01_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_esw01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_esw01_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementapparentpower", }, ("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", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementrmscurrent", }, ("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_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_lqi", }, }, }, @@ -4683,49 +4683,49 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sercomm_corp_sz_pir04_77665544_identify", - "sensor.sercomm_corp_sz_pir04_77665544_power", - "sensor.sercomm_corp_sz_pir04_77665544_illuminance", - "sensor.sercomm_corp_sz_pir04_77665544_temperature", - "binary_sensor.sercomm_corp_sz_pir04_77665544_ias_zone", - "sensor.sercomm_corp_sz_pir04_77665544_basic_rssi", - "sensor.sercomm_corp_sz_pir04_77665544_basic_lqi", + "button.sercomm_corp_sz_pir04_identifybutton", + "sensor.sercomm_corp_sz_pir04_battery", + "sensor.sercomm_corp_sz_pir04_illuminance", + "sensor.sercomm_corp_sz_pir04_temperature", + "binary_sensor.sercomm_corp_sz_pir04_iaszone", + "sensor.sercomm_corp_sz_pir04_rssi", + "sensor.sercomm_corp_sz_pir04_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.sercomm_corp_sz_pir04_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.sercomm_corp_sz_pir04_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_pir04_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_pir04_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1024"): { DEV_SIG_CHANNELS: ["illuminance"], DEV_SIG_ENT_MAP_CLASS: "Illuminance", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_77665544_illuminance", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_illuminance", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_lqi", }, }, }, @@ -4745,67 +4745,67 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sinope_technologies_rm3250zb_77665544_identify", - "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement", - "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_apparent_power", - "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_current", - "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_rms_voltage", - "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_ac_frequency", - "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_power_factor", - "switch.sinope_technologies_rm3250zb_77665544_on_off", - "sensor.sinope_technologies_rm3250zb_77665544_basic_rssi", - "sensor.sinope_technologies_rm3250zb_77665544_basic_lqi", + "button.sinope_technologies_rm3250zb_identifybutton", + "sensor.sinope_technologies_rm3250zb_electricalmeasurement", + "sensor.sinope_technologies_rm3250zb_electricalmeasurementapparentpower", + "sensor.sinope_technologies_rm3250zb_electricalmeasurementrmscurrent", + "sensor.sinope_technologies_rm3250zb_electricalmeasurementrmsvoltage", + "sensor.sinope_technologies_rm3250zb_electricalmeasurementfrequency", + "sensor.sinope_technologies_rm3250zb_electricalmeasurementpowerfactor", + "switch.sinope_technologies_rm3250zb_switch", + "sensor.sinope_technologies_rm3250zb_rssi", + "sensor.sinope_technologies_rm3250zb_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_rm3250zb_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_rm3250zb_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementapparentpower", }, ("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", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementrmscurrent", }, ("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_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_lqi", }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.sinope_technologies_rm3250zb_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.sinope_technologies_rm3250zb_switch", }, }, }, @@ -4832,79 +4832,79 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sinope_technologies_th1123zb_77665544_identify", - "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement", - "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_apparent_power", - "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_current", - "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_rms_voltage", - "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_ac_frequency", - "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_power_factor", - "sensor.sinope_technologies_th1123zb_77665544_temperature", - "sensor.sinope_technologies_th1123zb_77665544_thermostat_hvac_action", - "climate.sinope_technologies_th1123zb_77665544_thermostat", - "sensor.sinope_technologies_th1123zb_77665544_basic_rssi", - "sensor.sinope_technologies_th1123zb_77665544_basic_lqi", + "button.sinope_technologies_th1123zb_identifybutton", + "sensor.sinope_technologies_th1123zb_electricalmeasurement", + "sensor.sinope_technologies_th1123zb_electricalmeasurementapparentpower", + "sensor.sinope_technologies_th1123zb_electricalmeasurementrmscurrent", + "sensor.sinope_technologies_th1123zb_electricalmeasurementrmsvoltage", + "sensor.sinope_technologies_th1123zb_electricalmeasurementfrequency", + "sensor.sinope_technologies_th1123zb_electricalmeasurementpowerfactor", + "sensor.sinope_technologies_th1123zb_temperature", + "sensor.sinope_technologies_th1123zb_sinopehvacaction", + "climate.sinope_technologies_th1123zb_thermostat", + "sensor.sinope_technologies_th1123zb_rssi", + "sensor.sinope_technologies_th1123zb_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1123zb_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1123zb_identifybutton", }, ("climate", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "Thermostat", - DEV_SIG_ENT_MAP_ID: "climate.sinope_technologies_th1123zb_77665544_thermostat", + DEV_SIG_ENT_MAP_ID: "climate.sinope_technologies_th1123zb_thermostat", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementapparentpower", }, ("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", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementrmscurrent", }, ("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_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_77665544_thermostat_hvac_action", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_sinopehvacaction", }, }, }, @@ -4931,79 +4931,79 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sinope_technologies_th1124zb_77665544_identify", - "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", - "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_apparent_power", - "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_current", - "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_rms_voltage", - "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_ac_frequency", - "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_power_factor", - "sensor.sinope_technologies_th1124zb_77665544_temperature", - "sensor.sinope_technologies_th1124zb_77665544_thermostat_hvac_action", - "climate.sinope_technologies_th1124zb_77665544_thermostat", - "sensor.sinope_technologies_th1124zb_77665544_basic_rssi", - "sensor.sinope_technologies_th1124zb_77665544_basic_lqi", + "button.sinope_technologies_th1124zb_identifybutton", + "sensor.sinope_technologies_th1124zb_electricalmeasurement", + "sensor.sinope_technologies_th1124zb_electricalmeasurementapparentpower", + "sensor.sinope_technologies_th1124zb_electricalmeasurementrmscurrent", + "sensor.sinope_technologies_th1124zb_electricalmeasurementrmsvoltage", + "sensor.sinope_technologies_th1124zb_electricalmeasurementfrequency", + "sensor.sinope_technologies_th1124zb_electricalmeasurementpowerfactor", + "sensor.sinope_technologies_th1124zb_temperature", + "sensor.sinope_technologies_th1124zb_sinopehvacaction", + "climate.sinope_technologies_th1124zb_thermostat", + "sensor.sinope_technologies_th1124zb_rssi", + "sensor.sinope_technologies_th1124zb_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1124zb_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1124zb_identifybutton", }, ("climate", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "Thermostat", - DEV_SIG_ENT_MAP_ID: "climate.sinope_technologies_th1124zb_77665544_thermostat", + DEV_SIG_ENT_MAP_ID: "climate.sinope_technologies_th1124zb_thermostat", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementapparentpower", }, ("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", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementrmscurrent", }, ("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_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_lqi", }, ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { DEV_SIG_CHANNELS: ["thermostat"], DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_77665544_thermostat_hvac_action", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_sinopehvacaction", }, }, }, @@ -5023,73 +5023,73 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.smartthings_outletv4_77665544_identify", - "sensor.smartthings_outletv4_77665544_electrical_measurement", - "sensor.smartthings_outletv4_77665544_electrical_measurement_apparent_power", - "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_current", - "sensor.smartthings_outletv4_77665544_electrical_measurement_rms_voltage", - "sensor.smartthings_outletv4_77665544_electrical_measurement_ac_frequency", - "sensor.smartthings_outletv4_77665544_electrical_measurement_power_factor", - "binary_sensor.smartthings_outletv4_77665544_binary_input", - "switch.smartthings_outletv4_77665544_on_off", - "sensor.smartthings_outletv4_77665544_basic_rssi", - "sensor.smartthings_outletv4_77665544_basic_lqi", + "button.smartthings_outletv4_identifybutton", + "sensor.smartthings_outletv4_electricalmeasurement", + "sensor.smartthings_outletv4_electricalmeasurementapparentpower", + "sensor.smartthings_outletv4_electricalmeasurementrmscurrent", + "sensor.smartthings_outletv4_electricalmeasurementrmsvoltage", + "sensor.smartthings_outletv4_electricalmeasurementfrequency", + "sensor.smartthings_outletv4_electricalmeasurementpowerfactor", + "binary_sensor.smartthings_outletv4_binaryinput", + "switch.smartthings_outletv4_switch", + "sensor.smartthings_outletv4_rssi", + "sensor.smartthings_outletv4_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput", - DEV_SIG_ENT_MAP_ID: "binary_sensor.smartthings_outletv4_77665544_binary_input", + DEV_SIG_ENT_MAP_ID: "binary_sensor.smartthings_outletv4_binaryinput", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.smartthings_outletv4_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.smartthings_outletv4_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurement", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_apparent_power", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementapparentpower", }, ("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", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementrmscurrent", }, ("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", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementrmsvoltage", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_ac_frequency", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementfrequency", }, ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { DEV_SIG_CHANNELS: ["electrical_measurement"], DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_electrical_measurement_power_factor", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_electricalmeasurementpowerfactor", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_lqi", }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.smartthings_outletv4_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.smartthings_outletv4_switch", }, }, }, @@ -5109,37 +5109,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.smartthings_tagv4_77665544_identify", - "device_tracker.smartthings_tagv4_77665544_power", - "binary_sensor.smartthings_tagv4_77665544_binary_input", - "sensor.smartthings_tagv4_77665544_basic_rssi", - "sensor.smartthings_tagv4_77665544_basic_lqi", + "button.smartthings_tagv4_identifybutton", + "device_tracker.smartthings_tagv4_devicescanner", + "binary_sensor.smartthings_tagv4_binaryinput", + "sensor.smartthings_tagv4_rssi", + "sensor.smartthings_tagv4_lqi", ], DEV_SIG_ENT_MAP: { ("device_tracker", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "ZHADeviceScannerEntity", - DEV_SIG_ENT_MAP_ID: "device_tracker.smartthings_tagv4_77665544_power", + DEV_SIG_ENT_MAP_ID: "device_tracker.smartthings_tagv4_devicescanner", }, ("binary_sensor", "00:11:22:33:44:55:66:77-1-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput", - DEV_SIG_ENT_MAP_ID: "binary_sensor.smartthings_tagv4_77665544_binary_input", + DEV_SIG_ENT_MAP_ID: "binary_sensor.smartthings_tagv4_binaryinput", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.smartthings_tagv4_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.smartthings_tagv4_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_tagv4_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_tagv4_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_tagv4_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.smartthings_tagv4_lqi", }, }, }, @@ -5159,31 +5159,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.third_reality_inc_3rss007z_77665544_identify", - "switch.third_reality_inc_3rss007z_77665544_on_off", - "sensor.third_reality_inc_3rss007z_77665544_basic_rssi", - "sensor.third_reality_inc_3rss007z_77665544_basic_lqi", + "button.third_reality_inc_3rss007z_identifybutton", + "switch.third_reality_inc_3rss007z_switch", + "sensor.third_reality_inc_3rss007z_rssi", + "sensor.third_reality_inc_3rss007z_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss007z_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss007z_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss007z_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss007z_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss007z_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss007z_lqi", }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.third_reality_inc_3rss007z_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.third_reality_inc_3rss007z_switch", }, }, }, @@ -5203,37 +5203,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.third_reality_inc_3rss008z_77665544_identify", - "sensor.third_reality_inc_3rss008z_77665544_power", - "switch.third_reality_inc_3rss008z_77665544_on_off", - "sensor.third_reality_inc_3rss008z_77665544_basic_rssi", - "sensor.third_reality_inc_3rss008z_77665544_basic_lqi", + "button.third_reality_inc_3rss008z_identifybutton", + "sensor.third_reality_inc_3rss008z_battery", + "switch.third_reality_inc_3rss008z_switch", + "sensor.third_reality_inc_3rss008z_rssi", + "sensor.third_reality_inc_3rss008z_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss008z_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss008z_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_lqi", }, ("switch", "00:11:22:33:44:55:66:77-1-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.third_reality_inc_3rss008z_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.third_reality_inc_3rss008z_switch", }, }, }, @@ -5253,43 +5253,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.visonic_mct_340_e_77665544_identify", - "sensor.visonic_mct_340_e_77665544_power", - "sensor.visonic_mct_340_e_77665544_temperature", - "binary_sensor.visonic_mct_340_e_77665544_ias_zone", - "sensor.visonic_mct_340_e_77665544_basic_rssi", - "sensor.visonic_mct_340_e_77665544_basic_lqi", + "button.visonic_mct_340_e_identifybutton", + "sensor.visonic_mct_340_e_battery", + "sensor.visonic_mct_340_e_temperature", + "binary_sensor.visonic_mct_340_e_iaszone", + "sensor.visonic_mct_340_e_rssi", + "sensor.visonic_mct_340_e_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.visonic_mct_340_e_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.visonic_mct_340_e_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.visonic_mct_340_e_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.visonic_mct_340_e_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_lqi", }, }, }, @@ -5309,43 +5309,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.zen_within_zen_01_77665544_identify", - "sensor.zen_within_zen_01_77665544_power", - "sensor.zen_within_zen_01_77665544_thermostat_hvac_action", - "climate.zen_within_zen_01_77665544_fan_thermostat", - "sensor.zen_within_zen_01_77665544_basic_rssi", - "sensor.zen_within_zen_01_77665544_basic_lqi", + "button.zen_within_zen_01_identifybutton", + "sensor.zen_within_zen_01_battery", + "sensor.zen_within_zen_01_thermostathvacaction", + "climate.zen_within_zen_01_zenwithinthermostat", + "sensor.zen_within_zen_01_rssi", + "sensor.zen_within_zen_01_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.zen_within_zen_01_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.zen_within_zen_01_identifybutton", }, ("climate", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["thermostat", "fan"], DEV_SIG_ENT_MAP_CLASS: "ZenWithinThermostat", - DEV_SIG_ENT_MAP_ID: "climate.zen_within_zen_01_77665544_fan_thermostat", + DEV_SIG_ENT_MAP_ID: "climate.zen_within_zen_01_zenwithinthermostat", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_lqi", }, ("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.zen_within_zen_01_77665544_thermostat_hvac_action", + DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_thermostathvacaction", }, }, }, @@ -5386,43 +5386,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "light.tyzb01_ns1ndbww_ts0004_77665544_on_off", - "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_2", - "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_3", - "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_4", - "sensor.tyzb01_ns1ndbww_ts0004_77665544_basic_rssi", - "sensor.tyzb01_ns1ndbww_ts0004_77665544_basic_lqi", + "light.tyzb01_ns1ndbww_ts0004_light", + "light.tyzb01_ns1ndbww_ts0004_light_2", + "light.tyzb01_ns1ndbww_ts0004_light_3", + "light.tyzb01_ns1ndbww_ts0004_light_4", + "sensor.tyzb01_ns1ndbww_ts0004_rssi", + "sensor.tyzb01_ns1ndbww_ts0004_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_light", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.tyzb01_ns1ndbww_ts0004_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.tyzb01_ns1ndbww_ts0004_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.tyzb01_ns1ndbww_ts0004_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.tyzb01_ns1ndbww_ts0004_lqi", }, ("light", "00:11:22:33:44:55:66:77-2"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_2", + DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_light_2", }, ("light", "00:11:22:33:44:55:66:77-3"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_3", + DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_light_3", }, ("light", "00:11:22:33:44:55:66:77-4"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_4", + DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_light_4", }, }, }, @@ -5442,37 +5442,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.netvox_z308e3ed_77665544_identify", - "sensor.netvox_z308e3ed_77665544_power", - "binary_sensor.netvox_z308e3ed_77665544_ias_zone", - "sensor.netvox_z308e3ed_77665544_basic_rssi", - "sensor.netvox_z308e3ed_77665544_basic_lqi", + "button.netvox_z308e3ed_identifybutton", + "sensor.netvox_z308e3ed_battery", + "binary_sensor.netvox_z308e3ed_iaszone", + "sensor.netvox_z308e3ed_rssi", + "sensor.netvox_z308e3ed_lqi", ], DEV_SIG_ENT_MAP: { ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { DEV_SIG_CHANNELS: ["ias_zone"], DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.netvox_z308e3ed_77665544_ias_zone", + DEV_SIG_ENT_MAP_ID: "binary_sensor.netvox_z308e3ed_iaszone", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.netvox_z308e3ed_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.netvox_z308e3ed_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_lqi", }, }, }, @@ -5492,43 +5492,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sengled_e11_g13_77665544_identify", - "light.sengled_e11_g13_77665544_level_on_off", - "sensor.sengled_e11_g13_77665544_smartenergy_metering", - "sensor.sengled_e11_g13_77665544_smartenergy_metering_summation_delivered", - "sensor.sengled_e11_g13_77665544_basic_rssi", - "sensor.sengled_e11_g13_77665544_basic_lqi", + "button.sengled_e11_g13_identifybutton", + "light.sengled_e11_g13_light", + "sensor.sengled_e11_g13_smartenergymetering", + "sensor.sengled_e11_g13_smartenergysummation", + "sensor.sengled_e11_g13_rssi", + "sensor.sengled_e11_g13_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.sengled_e11_g13_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.sengled_e11_g13_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sengled_e11_g13_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sengled_e11_g13_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_lqi", }, }, }, @@ -5548,43 +5548,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sengled_e12_n14_77665544_identify", - "light.sengled_e12_n14_77665544_level_on_off", - "sensor.sengled_e12_n14_77665544_smartenergy_metering", - "sensor.sengled_e12_n14_77665544_smartenergy_metering_summation_delivered", - "sensor.sengled_e12_n14_77665544_basic_rssi", - "sensor.sengled_e12_n14_77665544_basic_lqi", + "button.sengled_e12_n14_identifybutton", + "light.sengled_e12_n14_light", + "sensor.sengled_e12_n14_smartenergymetering", + "sensor.sengled_e12_n14_smartenergysummation", + "sensor.sengled_e12_n14_rssi", + "sensor.sengled_e12_n14_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.sengled_e12_n14_77665544_level_on_off", + DEV_SIG_ENT_MAP_ID: "light.sengled_e12_n14_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sengled_e12_n14_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sengled_e12_n14_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_lqi", }, }, }, @@ -5604,43 +5604,43 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["1:0x0019"], DEV_SIG_ENTITIES: [ - "button.sengled_z01_a19nae26_77665544_identify", - "light.sengled_z01_a19nae26_77665544_level_light_color_on_off", - "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering", - "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering_summation_delivered", - "sensor.sengled_z01_a19nae26_77665544_basic_rssi", - "sensor.sengled_z01_a19nae26_77665544_basic_lqi", + "button.sengled_z01_a19nae26_identifybutton", + "light.sengled_z01_a19nae26_light", + "sensor.sengled_z01_a19nae26_smartenergymetering", + "sensor.sengled_z01_a19nae26_smartenergysummation", + "sensor.sengled_z01_a19nae26_rssi", + "sensor.sengled_z01_a19nae26_lqi", ], DEV_SIG_ENT_MAP: { ("light", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["on_off", "level", "light_color"], DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.sengled_z01_a19nae26_77665544_level_light_color_on_off", + DEV_SIG_ENT_MAP_ID: "light.sengled_z01_a19nae26_light", }, ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sengled_z01_a19nae26_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.sengled_z01_a19nae26_identifybutton", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_smartenergymetering", }, ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { DEV_SIG_CHANNELS: ["smartenergy_metering"], DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering_summation_delivered", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_smartenergysummation", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_lqi", }, }, }, @@ -5660,31 +5660,31 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "button.unk_manufacturer_unk_model_77665544_identify", - "cover.unk_manufacturer_unk_model_77665544_level_on_off_shade", - "sensor.unk_manufacturer_unk_model_77665544_basic_rssi", - "sensor.unk_manufacturer_unk_model_77665544_basic_lqi", + "button.unk_manufacturer_unk_model_identifybutton", + "cover.unk_manufacturer_unk_model_shade", + "sensor.unk_manufacturer_unk_model_rssi", + "sensor.unk_manufacturer_unk_model_lqi", ], DEV_SIG_ENT_MAP: { ("button", "00:11:22:33:44:55:66:77-1-3"): { DEV_SIG_CHANNELS: ["identify"], DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.unk_manufacturer_unk_model_77665544_identify", + DEV_SIG_ENT_MAP_ID: "button.unk_manufacturer_unk_model_identifybutton", }, ("cover", "00:11:22:33:44:55:66:77-1"): { DEV_SIG_CHANNELS: ["level", "on_off", "shade"], DEV_SIG_ENT_MAP_CLASS: "Shade", - DEV_SIG_ENT_MAP_ID: "cover.unk_manufacturer_unk_model_77665544_level_on_off_shade", + DEV_SIG_ENT_MAP_ID: "cover.unk_manufacturer_unk_model_shade", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.unk_manufacturer_unk_model_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.unk_manufacturer_unk_model_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.unk_manufacturer_unk_model_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.unk_manufacturer_unk_model_lqi", }, }, }, @@ -5809,139 +5809,139 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: ["232:0x0008"], DEV_SIG_ENTITIES: [ - "number.digi_xbee3_77665544_analog_output", - "number.digi_xbee3_77665544_analog_output_2", - "sensor.digi_xbee3_77665544_analog_input", - "sensor.digi_xbee3_77665544_analog_input_2", - "sensor.digi_xbee3_77665544_analog_input_3", - "sensor.digi_xbee3_77665544_analog_input_4", - "sensor.digi_xbee3_77665544_analog_input_5", - "switch.digi_xbee3_77665544_on_off", - "switch.digi_xbee3_77665544_on_off_2", - "switch.digi_xbee3_77665544_on_off_3", - "switch.digi_xbee3_77665544_on_off_4", - "switch.digi_xbee3_77665544_on_off_5", - "switch.digi_xbee3_77665544_on_off_6", - "switch.digi_xbee3_77665544_on_off_7", - "switch.digi_xbee3_77665544_on_off_8", - "switch.digi_xbee3_77665544_on_off_9", - "switch.digi_xbee3_77665544_on_off_10", - "switch.digi_xbee3_77665544_on_off_11", - "switch.digi_xbee3_77665544_on_off_12", - "switch.digi_xbee3_77665544_on_off_13", - "switch.digi_xbee3_77665544_on_off_14", - "switch.digi_xbee3_77665544_on_off_15", + "number.digi_xbee3_number", + "number.digi_xbee3_number_2", + "sensor.digi_xbee3_analoginput", + "sensor.digi_xbee3_analoginput_2", + "sensor.digi_xbee3_analoginput_3", + "sensor.digi_xbee3_analoginput_4", + "sensor.digi_xbee3_analoginput_5", + "switch.digi_xbee3_switch", + "switch.digi_xbee3_switch_2", + "switch.digi_xbee3_switch_3", + "switch.digi_xbee3_switch_4", + "switch.digi_xbee3_switch_5", + "switch.digi_xbee3_switch_6", + "switch.digi_xbee3_switch_7", + "switch.digi_xbee3_switch_8", + "switch.digi_xbee3_switch_9", + "switch.digi_xbee3_switch_10", + "switch.digi_xbee3_switch_11", + "switch.digi_xbee3_switch_12", + "switch.digi_xbee3_switch_13", + "switch.digi_xbee3_switch_14", + "switch.digi_xbee3_switch_15", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-208-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analoginput", }, ("switch", "00:11:22:33:44:55:66:77-208-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch", }, ("sensor", "00:11:22:33:44:55:66:77-209-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_2", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analoginput_2", }, ("switch", "00:11:22:33:44:55:66:77-209-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_2", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_2", }, ("sensor", "00:11:22:33:44:55:66:77-210-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_3", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analoginput_3", }, ("switch", "00:11:22:33:44:55:66:77-210-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_3", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_3", }, ("sensor", "00:11:22:33:44:55:66:77-211-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_4", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analoginput_4", }, ("switch", "00:11:22:33:44:55:66:77-211-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_4", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_4", }, ("switch", "00:11:22:33:44:55:66:77-212-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_5", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_5", }, ("switch", "00:11:22:33:44:55:66:77-213-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_6", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_6", }, ("switch", "00:11:22:33:44:55:66:77-214-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_7", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_7", }, ("sensor", "00:11:22:33:44:55:66:77-215-12"): { DEV_SIG_CHANNELS: ["analog_input"], DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_77665544_analog_input_5", + DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analoginput_5", }, ("switch", "00:11:22:33:44:55:66:77-215-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_8", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_8", }, ("switch", "00:11:22:33:44:55:66:77-216-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_9", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_9", }, ("switch", "00:11:22:33:44:55:66:77-217-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_10", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_10", }, ("number", "00:11:22:33:44:55:66:77-218-13"): { DEV_SIG_CHANNELS: ["analog_output"], DEV_SIG_ENT_MAP_CLASS: "ZhaNumber", - DEV_SIG_ENT_MAP_ID: "number.digi_xbee3_77665544_analog_output", + DEV_SIG_ENT_MAP_ID: "number.digi_xbee3_number", }, ("switch", "00:11:22:33:44:55:66:77-218-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_11", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_11", }, ("switch", "00:11:22:33:44:55:66:77-219-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_12", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_12", }, ("number", "00:11:22:33:44:55:66:77-219-13"): { DEV_SIG_CHANNELS: ["analog_output"], DEV_SIG_ENT_MAP_CLASS: "ZhaNumber", - DEV_SIG_ENT_MAP_ID: "number.digi_xbee3_77665544_analog_output_2", + DEV_SIG_ENT_MAP_ID: "number.digi_xbee3_number_2", }, ("switch", "00:11:22:33:44:55:66:77-220-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_13", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_13", }, ("switch", "00:11:22:33:44:55:66:77-221-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_14", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_14", }, ("switch", "00:11:22:33:44:55:66:77-222-6"): { DEV_SIG_CHANNELS: ["on_off"], DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_77665544_on_off_15", + DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_15", }, }, }, @@ -5961,37 +5961,37 @@ DEVICES = [ }, DEV_SIG_EVT_CHANNELS: [], DEV_SIG_ENTITIES: [ - "sensor.efektalab_ru_efekta_pws_77665544_power", - "sensor.efektalab_ru_efekta_pws_77665544_soil_moisture", - "sensor.efektalab_ru_efekta_pws_77665544_temperature", - "sensor.efektalab_ru_efekta_pws_77665544_basic_rssi", - "sensor.efektalab_ru_efekta_pws_77665544_basic_lqi", + "sensor.efektalab_ru_efekta_pws_battery", + "sensor.efektalab_ru_efekta_pws_soilmoisture", + "sensor.efektalab_ru_efekta_pws_temperature", + "sensor.efektalab_ru_efekta_pws_rssi", + "sensor.efektalab_ru_efekta_pws_lqi", ], DEV_SIG_ENT_MAP: { ("sensor", "00:11:22:33:44:55:66:77-1-1"): { DEV_SIG_CHANNELS: ["power"], DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_power", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_battery", }, ("sensor", "00:11:22:33:44:55:66:77-1-1032"): { DEV_SIG_CHANNELS: ["soil_moisture"], DEV_SIG_ENT_MAP_CLASS: "SoilMoisture", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_soil_moisture", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_soilmoisture", }, ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { DEV_SIG_CHANNELS: ["temperature"], DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_temperature", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_temperature", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_basic_rssi", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_rssi", }, ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { DEV_SIG_CHANNELS: ["basic"], DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_77665544_basic_lqi", + DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_lqi", }, }, }, From cdab725bf482b8d90211f16170317045c313bf63 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 22:18:15 +0200 Subject: [PATCH 2397/3516] Migrate Slimproto to new entity naming style (#74910) --- homeassistant/components/slimproto/media_player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/slimproto/media_player.py b/homeassistant/components/slimproto/media_player.py index 2f85aa4b9df..bcd538f57e8 100644 --- a/homeassistant/components/slimproto/media_player.py +++ b/homeassistant/components/slimproto/media_player.py @@ -74,6 +74,7 @@ async def async_setup_entry( class SlimProtoPlayer(MediaPlayerEntity): """Representation of MediaPlayerEntity from SlimProto Player.""" + _attr_has_entity_name = True _attr_should_poll = False _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE @@ -139,7 +140,6 @@ class SlimProtoPlayer(MediaPlayerEntity): @callback def update_attributes(self) -> None: """Handle player updates.""" - self._attr_name = self.player.name self._attr_volume_level = self.player.volume_level / 100 self._attr_media_position = self.player.elapsed_seconds self._attr_media_position_updated_at = utcnow() From 5f728b955e416332d98bb9f87550d84e9f1f979a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 22:19:13 +0200 Subject: [PATCH 2398/3516] Migrate Sonos to new entity naming style (#74909) --- .../components/sonos/binary_sensor.py | 4 ++-- homeassistant/components/sonos/entity.py | 1 + .../components/sonos/media_player.py | 1 - homeassistant/components/sonos/number.py | 3 +-- homeassistant/components/sonos/sensor.py | 6 +++--- homeassistant/components/sonos/switch.py | 20 ++++++++----------- 6 files changed, 15 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/sonos/binary_sensor.py b/homeassistant/components/sonos/binary_sensor.py index 4eaa75f92ae..e890c1c64a8 100644 --- a/homeassistant/components/sonos/binary_sensor.py +++ b/homeassistant/components/sonos/binary_sensor.py @@ -58,12 +58,12 @@ class SonosPowerEntity(SonosEntity, BinarySensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING + _attr_name = "Power" def __init__(self, speaker: SonosSpeaker) -> None: """Initialize the power entity binary sensor.""" super().__init__(speaker) self._attr_unique_id = f"{self.soco.uid}-power" - self._attr_name = f"{self.speaker.zone_name} Power" async def _async_fallback_poll(self) -> None: """Poll the device for the current state.""" @@ -92,12 +92,12 @@ class SonosMicrophoneSensorEntity(SonosEntity, BinarySensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_icon = "mdi:microphone" + _attr_name = "Microphone" def __init__(self, speaker: SonosSpeaker) -> None: """Initialize the microphone binary sensor entity.""" super().__init__(speaker) self._attr_unique_id = f"{self.soco.uid}-microphone" - self._attr_name = f"{self.speaker.zone_name} Microphone" async def _async_fallback_poll(self) -> None: """Handle polling when subscription fails.""" diff --git a/homeassistant/components/sonos/entity.py b/homeassistant/components/sonos/entity.py index ba1f72cd56b..0955fb0e82d 100644 --- a/homeassistant/components/sonos/entity.py +++ b/homeassistant/components/sonos/entity.py @@ -26,6 +26,7 @@ class SonosEntity(Entity): """Representation of a Sonos entity.""" _attr_should_poll = False + _attr_has_entity_name = True def __init__(self, speaker: SonosSpeaker) -> None: """Initialize a SonosEntity.""" diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 4e7998c8e4e..1f57cafbf09 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -216,7 +216,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): """Initialize the media player entity.""" super().__init__(speaker) self._attr_unique_id = self.soco.uid - self._attr_name = self.speaker.zone_name async def async_added_to_hass(self) -> None: """Handle common setup when added to hass.""" diff --git a/homeassistant/components/sonos/number.py b/homeassistant/components/sonos/number.py index 3b034423471..ccbcbc3c339 100644 --- a/homeassistant/components/sonos/number.py +++ b/homeassistant/components/sonos/number.py @@ -72,8 +72,7 @@ class SonosLevelEntity(SonosEntity, NumberEntity): """Initialize the level entity.""" super().__init__(speaker) self._attr_unique_id = f"{self.soco.uid}-{level_type}" - name_suffix = level_type.replace("_", " ").title() - self._attr_name = f"{self.speaker.zone_name} {name_suffix}" + self._attr_name = level_type.replace("_", " ").capitalize() self.level_type = level_type self._attr_native_min_value, self._attr_native_max_value = valid_range diff --git a/homeassistant/components/sonos/sensor.py b/homeassistant/components/sonos/sensor.py index 380d1a3b9b6..8477e523a40 100644 --- a/homeassistant/components/sonos/sensor.py +++ b/homeassistant/components/sonos/sensor.py @@ -80,13 +80,13 @@ class SonosBatteryEntity(SonosEntity, SensorEntity): _attr_device_class = SensorDeviceClass.BATTERY _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_name = "Battery" _attr_native_unit_of_measurement = PERCENTAGE def __init__(self, speaker: SonosSpeaker) -> None: """Initialize the battery sensor.""" super().__init__(speaker) self._attr_unique_id = f"{self.soco.uid}-battery" - self._attr_name = f"{self.speaker.zone_name} Battery" async def _async_fallback_poll(self) -> None: """Poll the device for the current state.""" @@ -108,13 +108,13 @@ class SonosAudioInputFormatSensorEntity(SonosPollingEntity, SensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_icon = "mdi:import" + _attr_name = "Audio input format" _attr_should_poll = True def __init__(self, speaker: SonosSpeaker, audio_format: str) -> None: """Initialize the audio input format sensor.""" super().__init__(speaker) self._attr_unique_id = f"{self.soco.uid}-audio-format" - self._attr_name = f"{self.speaker.zone_name} Audio Input Format" self._attr_native_value = audio_format def poll_state(self) -> None: @@ -137,7 +137,7 @@ class SonosFavoritesEntity(SensorEntity): _attr_entity_registry_enabled_default = False _attr_icon = "mdi:star" - _attr_name = "Sonos Favorites" + _attr_name = "Sonos favorites" _attr_native_unit_of_measurement = "items" _attr_should_poll = False diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 53911d85d3e..8dcf47fd0a2 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -69,13 +69,13 @@ POLL_REQUIRED = ( FRIENDLY_NAMES = { ATTR_CROSSFADE: "Crossfade", ATTR_LOUDNESS: "Loudness", - ATTR_MUSIC_PLAYBACK_FULL_VOLUME: "Surround Music Full Volume", - ATTR_NIGHT_SOUND: "Night Sound", - ATTR_SPEECH_ENHANCEMENT: "Speech Enhancement", + ATTR_MUSIC_PLAYBACK_FULL_VOLUME: "Surround music full volume", + ATTR_NIGHT_SOUND: "Night sound", + ATTR_SPEECH_ENHANCEMENT: "Speech enhancement", ATTR_STATUS_LIGHT: "Status Light", - ATTR_SUB_ENABLED: "Subwoofer Enabled", - ATTR_SURROUND_ENABLED: "Surround Enabled", - ATTR_TOUCH_CONTROLS: "Touch Controls", + ATTR_SUB_ENABLED: "Subwoofer enabled", + ATTR_SURROUND_ENABLED: "Surround enabled", + ATTR_TOUCH_CONTROLS: "Touch controls", } FEATURE_ICONS = { @@ -160,7 +160,7 @@ class SonosSwitchEntity(SonosPollingEntity, SwitchEntity): self.feature_type = feature_type self.needs_coordinator = feature_type in COORDINATOR_FEATURES self._attr_entity_category = EntityCategory.CONFIG - self._attr_name = f"{speaker.zone_name} {FRIENDLY_NAMES[feature_type]}" + self._attr_name = FRIENDLY_NAMES[feature_type] self._attr_unique_id = f"{speaker.soco.uid}-{feature_type}" self._attr_icon = FEATURE_ICONS.get(feature_type) @@ -240,11 +240,7 @@ class SonosAlarmEntity(SonosEntity, SwitchEntity): @property def name(self) -> str: """Return the name of the sensor.""" - return "{} {} Alarm {}".format( - self.speaker.zone_name, - self.alarm.recurrence.title(), - str(self.alarm.start_time)[0:5], - ) + return f"{self.alarm.recurrence.capitalize()} alarm {str(self.alarm.start_time)[:5]}" async def _async_fallback_poll(self) -> None: """Call the central alarm polling method.""" From f5d18108d001ffec7147c4f93932581dc73898ca Mon Sep 17 00:00:00 2001 From: kpine Date: Fri, 8 Jul 2022 15:20:44 -0700 Subject: [PATCH 2399/3516] Fix KeyError from zwave_js diagnostics (#74579) --- .../components/zwave_js/diagnostics.py | 29 ++++--- tests/components/zwave_js/test_diagnostics.py | 80 ++++++++++++++++++- 2 files changed, 92 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 3372b0eeec0..078bd761b71 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -94,20 +94,23 @@ def get_device_entities( # If the value ID returns as None, we don't need to include this entity if (value_id := get_value_id_from_unique_id(entry.unique_id)) is None: continue - state_key = get_state_key_from_unique_id(entry.unique_id) - zwave_value = node.values[value_id] - primary_value_data = { - "command_class": zwave_value.command_class, - "command_class_name": zwave_value.command_class_name, - "endpoint": zwave_value.endpoint, - "property": zwave_value.property_, - "property_name": zwave_value.property_name, - "property_key": zwave_value.property_key, - "property_key_name": zwave_value.property_key_name, - } - if state_key is not None: - primary_value_data["state_key"] = state_key + primary_value_data = None + if (zwave_value := node.values.get(value_id)) is not None: + primary_value_data = { + "command_class": zwave_value.command_class, + "command_class_name": zwave_value.command_class_name, + "endpoint": zwave_value.endpoint, + "property": zwave_value.property_, + "property_name": zwave_value.property_name, + "property_key": zwave_value.property_key, + "property_key_name": zwave_value.property_key_name, + } + + state_key = get_state_key_from_unique_id(entry.unique_id) + if state_key is not None: + primary_value_data["state_key"] = state_key + entity = { "domain": entry.domain, "entity_id": entry.entity_id, diff --git a/tests/components/zwave_js/test_diagnostics.py b/tests/components/zwave_js/test_diagnostics.py index 3ac3f32b45a..9f3a7b0884c 100644 --- a/tests/components/zwave_js/test_diagnostics.py +++ b/tests/components/zwave_js/test_diagnostics.py @@ -10,8 +10,12 @@ from homeassistant.components.zwave_js.diagnostics import ( async_get_device_diagnostics, ) from homeassistant.components.zwave_js.discovery import async_discover_node_values -from homeassistant.components.zwave_js.helpers import get_device_id -from homeassistant.helpers.device_registry import async_get +from homeassistant.components.zwave_js.helpers import ( + get_device_id, + get_value_id_from_unique_id, +) +from homeassistant.helpers.device_registry import async_get as async_get_dev_reg +from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg from .common import PROPERTY_ULTRAVIOLET @@ -53,7 +57,7 @@ async def test_device_diagnostics( version_state, ): """Test the device level diagnostics data dump.""" - dev_reg = async_get(hass) + dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_device({get_device_id(client.driver, multisensor_6)}) assert device @@ -106,7 +110,7 @@ async def test_device_diagnostics( async def test_device_diagnostics_error(hass, integration): """Test the device diagnostics raises exception when an invalid device is used.""" - dev_reg = async_get(hass) + dev_reg = async_get_dev_reg(hass) device = dev_reg.async_get_or_create( config_entry_id=integration.entry_id, identifiers={("test", "test")} ) @@ -118,3 +122,71 @@ async def test_empty_zwave_value_matcher(): """Test empty ZwaveValueMatcher is invalid.""" with pytest.raises(ValueError): ZwaveValueMatcher() + + +async def test_device_diagnostics_missing_primary_value( + hass, + client, + multisensor_6, + integration, + hass_client, +): + """Test that the device diagnostics handles an entity with a missing primary value.""" + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get_device({get_device_id(client.driver, multisensor_6)}) + assert device + + entity_id = "sensor.multisensor_6_air_temperature" + ent_reg = async_get_ent_reg(hass) + entry = ent_reg.async_get(entity_id) + + # check that the primary value for the entity exists in the diagnostics + diagnostics_data = await get_diagnostics_for_device( + hass, hass_client, integration, device + ) + + value = multisensor_6.values.get(get_value_id_from_unique_id(entry.unique_id)) + assert value + + air_entity = next( + x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id + ) + + assert air_entity["primary_value"] == { + "command_class": value.command_class, + "command_class_name": value.command_class_name, + "endpoint": value.endpoint, + "property": value.property_, + "property_name": value.property_name, + "property_key": value.property_key, + "property_key_name": value.property_key_name, + } + + # make the entity's primary value go missing + event = Event( + type="value removed", + data={ + "source": "node", + "event": "value removed", + "nodeId": multisensor_6.node_id, + "args": { + "commandClassName": value.command_class_name, + "commandClass": value.command_class, + "endpoint": value.endpoint, + "property": value.property_, + "prevValue": 0, + "propertyName": value.property_name, + }, + }, + ) + multisensor_6.receive_event(event) + + diagnostics_data = await get_diagnostics_for_device( + hass, hass_client, integration, device + ) + + air_entity = next( + x for x in diagnostics_data["entities"] if x["entity_id"] == entity_id + ) + + assert air_entity["primary_value"] is None From 59471a6fbd039133af2b7cc31df0f9046b3c3c51 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sat, 9 Jul 2022 18:55:33 +0100 Subject: [PATCH 2400/3516] Update systembridgeconnector to 3.3.2 (#74701) --- homeassistant/components/system_bridge/coordinator.py | 7 ++++--- homeassistant/components/system_bridge/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py index 6088967aa33..1719d951cf0 100644 --- a/homeassistant/components/system_bridge/coordinator.py +++ b/homeassistant/components/system_bridge/coordinator.py @@ -93,7 +93,7 @@ class SystemBridgeDataUpdateCoordinator( if not self.websocket_client.connected: await self._setup_websocket() - await self.websocket_client.get_data(modules) + self.hass.async_create_task(self.websocket_client.get_data(modules)) async def async_handle_module( self, @@ -107,9 +107,7 @@ class SystemBridgeDataUpdateCoordinator( async def _listen_for_data(self) -> None: """Listen for events from the WebSocket.""" - try: - await self.websocket_client.register_data_listener(MODULES) await self.websocket_client.listen(callback=self.async_handle_module) except AuthenticationException as exception: self.last_update_success = False @@ -175,6 +173,9 @@ class SystemBridgeDataUpdateCoordinator( self.async_update_listeners() self.hass.async_create_task(self._listen_for_data()) + + await self.websocket_client.register_data_listener(MODULES) + self.last_update_success = True self.async_update_listeners() diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 087613413d8..4fb2201e2c7 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -3,7 +3,7 @@ "name": "System Bridge", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/system_bridge", - "requirements": ["systembridgeconnector==3.1.5"], + "requirements": ["systembridgeconnector==3.3.2"], "codeowners": ["@timmo001"], "zeroconf": ["_system-bridge._tcp.local."], "after_dependencies": ["zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index d7c0b8124f0..ce409af3b0a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2268,7 +2268,7 @@ swisshydrodata==0.1.0 synology-srm==0.2.0 # homeassistant.components.system_bridge -systembridgeconnector==3.1.5 +systembridgeconnector==3.3.2 # homeassistant.components.tailscale tailscale==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83301dc1c4c..387e646e83d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1513,7 +1513,7 @@ sunwatcher==0.2.1 surepy==0.7.2 # homeassistant.components.system_bridge -systembridgeconnector==3.1.5 +systembridgeconnector==3.3.2 # homeassistant.components.tailscale tailscale==0.2.0 From 43527d8d19bdbf8b5f4973fe1e62ee0aad76ec46 Mon Sep 17 00:00:00 2001 From: Ethan Madden Date: Sat, 9 Jul 2022 10:51:47 -0700 Subject: [PATCH 2401/3516] `air_quality` and `filter_life` fixes for Pur131S (#74740) --- homeassistant/components/vesync/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/vesync/sensor.py b/homeassistant/components/vesync/sensor.py index 2da6d8ea6b7..45018c79ca0 100644 --- a/homeassistant/components/vesync/sensor.py +++ b/homeassistant/components/vesync/sensor.py @@ -83,13 +83,12 @@ SENSORS: tuple[VeSyncSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda device: device.details["filter_life"], + value_fn=lambda device: device.filter_life, exists_fn=lambda device: sku_supported(device, FILTER_LIFE_SUPPORTED), ), VeSyncSensorEntityDescription( key="air-quality", name="Air Quality", - state_class=SensorStateClass.MEASUREMENT, value_fn=lambda device: device.details["air_quality"], exists_fn=lambda device: sku_supported(device, AIR_QUALITY_SUPPORTED), ), From e233024533f142b26ad274766602038f9096f49f Mon Sep 17 00:00:00 2001 From: Pieter Mulder Date: Sat, 9 Jul 2022 19:13:46 +0200 Subject: [PATCH 2402/3516] Update pyCEC to version 0.5.2 (#74742) --- homeassistant/components/hdmi_cec/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hdmi_cec/manifest.json b/homeassistant/components/hdmi_cec/manifest.json index ff2411db35a..8ea56a51fa9 100644 --- a/homeassistant/components/hdmi_cec/manifest.json +++ b/homeassistant/components/hdmi_cec/manifest.json @@ -2,7 +2,7 @@ "domain": "hdmi_cec", "name": "HDMI-CEC", "documentation": "https://www.home-assistant.io/integrations/hdmi_cec", - "requirements": ["pyCEC==0.5.1"], + "requirements": ["pyCEC==0.5.2"], "codeowners": [], "iot_class": "local_push", "loggers": ["pycec"] diff --git a/requirements_all.txt b/requirements_all.txt index ce409af3b0a..42858b8f445 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1323,7 +1323,7 @@ py-zabbix==1.1.7 py17track==2021.12.2 # homeassistant.components.hdmi_cec -pyCEC==0.5.1 +pyCEC==0.5.2 # homeassistant.components.control4 pyControl4==0.0.6 From 79b4f8ce0c64c97c1380e40616e3b7de1e63295d Mon Sep 17 00:00:00 2001 From: Regev Brody Date: Sat, 9 Jul 2022 14:22:29 +0300 Subject: [PATCH 2403/3516] Bump pyezviz to 0.2.0.9 (#74755) * Bump ezviz dependency to fix #74618 * Bump ezviz dependency to fix #74618 Co-authored-by: J. Nick Koston --- homeassistant/components/ezviz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json index cfdfd6e441d..47e2ec44e8a 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.2.0.8"], + "requirements": ["pyezviz==0.2.0.9"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["paho_mqtt", "pyezviz"] diff --git a/requirements_all.txt b/requirements_all.txt index 42858b8f445..2edf124752a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1489,7 +1489,7 @@ pyeverlights==0.1.0 pyevilgenius==2.0.0 # homeassistant.components.ezviz -pyezviz==0.2.0.8 +pyezviz==0.2.0.9 # homeassistant.components.fido pyfido==2.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 387e646e83d..4a5fc9ec897 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -995,7 +995,7 @@ pyeverlights==0.1.0 pyevilgenius==2.0.0 # homeassistant.components.ezviz -pyezviz==0.2.0.8 +pyezviz==0.2.0.9 # homeassistant.components.fido pyfido==2.1.1 From 2accc4c07da9f245a7d033530c00da9c0533cd5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sat, 9 Jul 2022 17:13:27 +0200 Subject: [PATCH 2404/3516] Update aioqsw to v0.1.1 (#74784) --- homeassistant/components/qnap_qsw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qnap_qsw/manifest.json b/homeassistant/components/qnap_qsw/manifest.json index be565f2a07e..83c9423f0f4 100644 --- a/homeassistant/components/qnap_qsw/manifest.json +++ b/homeassistant/components/qnap_qsw/manifest.json @@ -3,7 +3,7 @@ "name": "QNAP QSW", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/qnap_qsw", - "requirements": ["aioqsw==0.1.0"], + "requirements": ["aioqsw==0.1.1"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioqsw"], diff --git a/requirements_all.txt b/requirements_all.txt index 2edf124752a..36a6f9de35e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -229,7 +229,7 @@ aiopvpc==3.0.0 aiopyarr==22.6.0 # homeassistant.components.qnap_qsw -aioqsw==0.1.0 +aioqsw==0.1.1 # homeassistant.components.recollect_waste aiorecollect==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a5fc9ec897..dae7af38692 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -198,7 +198,7 @@ aiopvpc==3.0.0 aiopyarr==22.6.0 # homeassistant.components.qnap_qsw -aioqsw==0.1.0 +aioqsw==0.1.1 # homeassistant.components.recollect_waste aiorecollect==1.0.8 From 357fe2a722494da07ddf37490a6ecd20171dc4be Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Sat, 9 Jul 2022 23:53:47 +0300 Subject: [PATCH 2405/3516] Bump python-gammu to 3.2.4 with Python 3.10 support (#74797) --- homeassistant/components/sms/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sms/manifest.json b/homeassistant/components/sms/manifest.json index d98304ebf23..b3426c01422 100644 --- a/homeassistant/components/sms/manifest.json +++ b/homeassistant/components/sms/manifest.json @@ -3,7 +3,7 @@ "name": "SMS notifications via GSM-modem", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sms", - "requirements": ["python-gammu==3.2.3"], + "requirements": ["python-gammu==3.2.4"], "codeowners": ["@ocalvo"], "iot_class": "local_polling", "loggers": ["gammu"] diff --git a/requirements_all.txt b/requirements_all.txt index 36a6f9de35e..0e564ea62f8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1895,7 +1895,7 @@ python-family-hub-local==0.0.2 python-forecastio==1.4.0 # homeassistant.components.sms -# python-gammu==3.2.3 +# python-gammu==3.2.4 # homeassistant.components.gc100 python-gc100==1.0.3a0 From 07f4efcd835ae8200b2c35a509857b95f5f583dc Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 9 Jul 2022 23:40:15 +0200 Subject: [PATCH 2406/3516] Bump deCONZ dependency to fix #74791 (#74804) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 06f8b6c0376..2ae400bbe19 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==97"], + "requirements": ["pydeconz==98"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 0e564ea62f8..a9f7a7cdf6d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1444,7 +1444,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==97 +pydeconz==98 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dae7af38692..3fdbdc7244e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -974,7 +974,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==97 +pydeconz==98 # homeassistant.components.dexcom pydexcom==0.2.3 From 2ba285b8e501e14c48b2628cc81ecf4a9b930aad Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 9 Jul 2022 10:32:15 -0600 Subject: [PATCH 2407/3516] Bump regenmaschine to 2022.07.1 (#74815) --- homeassistant/components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 4f06ed0d71b..b318ef7f295 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.07.0"], + "requirements": ["regenmaschine==2022.07.1"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index a9f7a7cdf6d..4578574a921 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2065,7 +2065,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.07.0 +regenmaschine==2022.07.1 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3fdbdc7244e..57c33617e0c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1376,7 +1376,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rainmachine -regenmaschine==2022.07.0 +regenmaschine==2022.07.1 # homeassistant.components.renault renault-api==0.1.11 From 2f570fa715b557e0150f2b44e50dde2da13b8435 Mon Sep 17 00:00:00 2001 From: Stephan Uhle Date: Sat, 9 Jul 2022 22:22:30 +0200 Subject: [PATCH 2408/3516] Fixed unit of measurement. #70121 (#74838) --- homeassistant/components/edl21/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index f7a79d727a0..65603b0c8c4 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -17,6 +17,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( CONF_NAME, + DEGREE, ELECTRIC_CURRENT_AMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, @@ -250,6 +251,7 @@ SENSOR_UNIT_MAPPING = { "W": POWER_WATT, "A": ELECTRIC_CURRENT_AMPERE, "V": ELECTRIC_POTENTIAL_VOLT, + "°": DEGREE, } @@ -449,7 +451,7 @@ class EDL21Entity(SensorEntity): @property def native_unit_of_measurement(self): """Return the unit of measurement.""" - if (unit := self._telegram.get("unit")) is None: + if (unit := self._telegram.get("unit")) is None or unit == 0: return None return SENSOR_UNIT_MAPPING[unit] From 01eae3687aaa1aecab780fb8a8aff4f4302062de Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 10 Jul 2022 05:51:40 -0500 Subject: [PATCH 2409/3516] Bump rokuecp to 0.17.0 (#74862) --- homeassistant/components/roku/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 05fe0e1b260..910516b93e8 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -2,7 +2,7 @@ "domain": "roku", "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", - "requirements": ["rokuecp==0.16.0"], + "requirements": ["rokuecp==0.17.0"], "homekit": { "models": ["3820X", "3810X", "4660X", "7820X", "C105X", "C135X"] }, diff --git a/requirements_all.txt b/requirements_all.txt index 4578574a921..67aa76635dd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2092,7 +2092,7 @@ rjpl==0.3.6 rocketchat-API==0.6.1 # homeassistant.components.roku -rokuecp==0.16.0 +rokuecp==0.17.0 # homeassistant.components.roomba roombapy==1.6.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 57c33617e0c..6654bf8b1bd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1391,7 +1391,7 @@ rflink==0.0.63 ring_doorbell==0.7.2 # homeassistant.components.roku -rokuecp==0.16.0 +rokuecp==0.17.0 # homeassistant.components.roomba roombapy==1.6.5 From 986a86ebed921e76073b9402aad12f04e347d8cb Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Sun, 10 Jul 2022 06:33:54 -0400 Subject: [PATCH 2410/3516] Bump pymazda to 0.3.6 (#74863) --- 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 a18b4406355..acf5282689f 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.3.3"], + "requirements": ["pymazda==0.3.6"], "codeowners": ["@bdr99"], "quality_scale": "platinum", "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 67aa76635dd..e1e5ea7b65e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1640,7 +1640,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.3 +pymazda==0.3.6 # homeassistant.components.mediaroom pymediaroom==0.6.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6654bf8b1bd..d7471a9df92 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1113,7 +1113,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.3 +pymazda==0.3.6 # homeassistant.components.melcloud pymelcloud==2.5.6 From bd069966f21a4d87f6d6001579b64ad652d0eb1d Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Sun, 10 Jul 2022 22:15:43 +0200 Subject: [PATCH 2411/3516] Fix Vicare One Time Charge (#74872) --- homeassistant/components/vicare/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 575ca35729b..db0ce9cddaa 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -3,7 +3,7 @@ "name": "Viessmann ViCare", "documentation": "https://www.home-assistant.io/integrations/vicare", "codeowners": ["@oischinger"], - "requirements": ["PyViCare==2.16.2"], + "requirements": ["PyViCare==2.16.4"], "iot_class": "cloud_polling", "config_flow": true, "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index e1e5ea7b65e..0e66461ff7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -47,7 +47,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.6.6 # homeassistant.components.vicare -PyViCare==2.16.2 +PyViCare==2.16.4 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d7471a9df92..64087216db2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -43,7 +43,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.6.6 # homeassistant.components.vicare -PyViCare==2.16.2 +PyViCare==2.16.4 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 From adbcd8adb495814d190bacaf23e50167d53794b2 Mon Sep 17 00:00:00 2001 From: David Straub Date: Sun, 10 Jul 2022 12:49:18 +0200 Subject: [PATCH 2412/3516] Bump pysml to 0.0.8 (fixes #74382) (#74875) --- homeassistant/components/edl21/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/edl21/manifest.json b/homeassistant/components/edl21/manifest.json index 4cffabe87fc..cac35a10152 100644 --- a/homeassistant/components/edl21/manifest.json +++ b/homeassistant/components/edl21/manifest.json @@ -2,7 +2,7 @@ "domain": "edl21", "name": "EDL21", "documentation": "https://www.home-assistant.io/integrations/edl21", - "requirements": ["pysml==0.0.7"], + "requirements": ["pysml==0.0.8"], "codeowners": ["@mtdcr"], "iot_class": "local_push", "loggers": ["sml"] diff --git a/requirements_all.txt b/requirements_all.txt index 0e66461ff7c..cf39a40868e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1835,7 +1835,7 @@ pysmartthings==0.7.6 pysmarty==0.8 # homeassistant.components.edl21 -pysml==0.0.7 +pysml==0.0.8 # homeassistant.components.snmp pysnmplib==5.0.15 From 20f77ef832369ad556a2870f0f182bdb014a800a Mon Sep 17 00:00:00 2001 From: Thijs W Date: Sun, 10 Jul 2022 21:52:49 +0200 Subject: [PATCH 2413/3516] Bump afsapi to 0.2.5 (#74907) --- homeassistant/components/frontier_silicon/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/frontier_silicon/manifest.json b/homeassistant/components/frontier_silicon/manifest.json index 20092b941a9..12fb5145aa0 100644 --- a/homeassistant/components/frontier_silicon/manifest.json +++ b/homeassistant/components/frontier_silicon/manifest.json @@ -2,7 +2,7 @@ "domain": "frontier_silicon", "name": "Frontier Silicon", "documentation": "https://www.home-assistant.io/integrations/frontier_silicon", - "requirements": ["afsapi==0.2.4"], + "requirements": ["afsapi==0.2.5"], "codeowners": ["@wlcrs"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index cf39a40868e..deb6ce80558 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -89,7 +89,7 @@ adguardhome==0.5.1 advantage_air==0.3.1 # homeassistant.components.frontier_silicon -afsapi==0.2.4 +afsapi==0.2.5 # homeassistant.components.agent_dvr agent-py==0.0.23 From 02452c7632d68010d0ffa97dd64094982d4f1200 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 10 Jul 2022 13:25:47 -0700 Subject: [PATCH 2414/3516] Bumped version to 2022.7.3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 796ae18f58d..7f5cf999b96 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 68b4758a688..c86fc6b26f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.2" +version = "2022.7.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From a19ab389fc47c1cb19081fff34bd0dd172dc9b97 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 14:29:50 -0600 Subject: [PATCH 2415/3516] Migrate Flu Near You to new entity naming style (#74918) --- homeassistant/components/flunearyou/sensor.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index 8a232d3d103..f666e7412eb 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -42,12 +42,12 @@ SENSOR_TYPE_USER_TOTAL = "total" CDC_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_TYPE_CDC_LEVEL, - name="CDC Level", + name="CDC level", icon="mdi:biohazard", ), SensorEntityDescription( key=SENSOR_TYPE_CDC_LEVEL2, - name="CDC Level 2", + name="CDC level 2", icon="mdi:biohazard", ), ) @@ -55,49 +55,49 @@ CDC_SENSOR_DESCRIPTIONS = ( USER_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_TYPE_USER_CHICK, - name="Avian Flu Symptoms", + name="Avian flu symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_DENGUE, - name="Dengue Fever Symptoms", + name="Dengue fever symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_FLU, - name="Flu Symptoms", + name="Flu symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_LEPTO, - name="Leptospirosis Symptoms", + name="Leptospirosis symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_NO_SYMPTOMS, - name="No Symptoms", + name="No symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_SYMPTOMS, - name="Flu-like Symptoms", + name="Flu-like symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=SENSOR_TYPE_USER_TOTAL, - name="Total Symptoms", + name="Total symptoms", icon="mdi:alert", native_unit_of_measurement="reports", state_class=SensorStateClass.MEASUREMENT, @@ -133,6 +133,8 @@ async def async_setup_entry( class FluNearYouSensor(CoordinatorEntity, SensorEntity): """Define a base Flu Near You sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: DataUpdateCoordinator, From b4e5c95e0343dfe839fbfca7a3ed176cb28151e3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jul 2022 14:45:09 -0600 Subject: [PATCH 2416/3516] Migrate OpenUV to new entity naming style (#74919) --- homeassistant/components/openuv/__init__.py | 2 ++ .../components/openuv/binary_sensor.py | 2 +- homeassistant/components/openuv/sensor.py | 20 +++++++++---------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index be003507b81..29b13ff5258 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -183,6 +183,8 @@ class OpenUV: class OpenUvEntity(Entity): """Define a generic OpenUV entity.""" + _attr_has_entity_name = True + def __init__(self, openuv: OpenUV, description: EntityDescription) -> None: """Initialize.""" self._attr_extra_state_attributes = {} diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 503d82d32f2..757f0479e01 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -18,7 +18,7 @@ ATTR_PROTECTION_WINDOW_STARTING_UV = "start_uv" BINARY_SENSOR_DESCRIPTION_PROTECTION_WINDOW = BinarySensorEntityDescription( key=TYPE_PROTECTION_WINDOW, - name="Protection Window", + name="Protection window", icon="mdi:sunglasses", ) diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index f654ed63a6d..3a5bd3c2a47 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -49,68 +49,68 @@ UV_LEVEL_LOW = "Low" SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=TYPE_CURRENT_OZONE_LEVEL, - name="Current Ozone Level", + name="Current ozone level", device_class=SensorDeviceClass.OZONE, native_unit_of_measurement="du", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_CURRENT_UV_INDEX, - name="Current UV Index", + name="Current UV index", icon="mdi:weather-sunny", native_unit_of_measurement=UV_INDEX, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_CURRENT_UV_LEVEL, - name="Current UV Level", + name="Current UV level", icon="mdi:weather-sunny", ), SensorEntityDescription( key=TYPE_MAX_UV_INDEX, - name="Max UV Index", + name="Max UV index", icon="mdi:weather-sunny", native_unit_of_measurement=UV_INDEX, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_1, - name="Skin Type 1 Safe Exposure Time", + name="Skin type 1 safe exposure time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_2, - name="Skin Type 2 Safe Exposure Time", + name="Skin type 2 safe exposure time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_3, - name="Skin Type 3 Safe Exposure Time", + name="Skin type 3 safe exposure time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_4, - name="Skin Type 4 Safe Exposure Time", + name="Skin type 4 safe exposure time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_5, - name="Skin Type 5 Safe Exposure Time", + name="Skin type 5 safe exposure time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SAFE_EXPOSURE_TIME_6, - name="Skin Type 6 Safe Exposure Time", + name="Skin type 6 safe exposure time", icon="mdi:timer-outline", native_unit_of_measurement=TIME_MINUTES, state_class=SensorStateClass.MEASUREMENT, From c2d8335cc50d086167b391f4508299f21cd760da Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 10 Jul 2022 23:06:27 +0200 Subject: [PATCH 2417/3516] Add "Home Assistant (skip pip)" to VS Code launch.json (#74887) --- .vscode/launch.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index e8bf893e0c9..3cec89cc7e6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,6 +12,14 @@ "justMyCode": false, "args": ["--debug", "-c", "config"] }, + { + "name": "Home Assistant (skip pip)", + "type": "python", + "request": "launch", + "module": "homeassistant", + "justMyCode": false, + "args": ["--debug", "-c", "config", "--skip-pip"] + }, { // Debug by attaching to local Home Asistant server using Remote Python Debugger. // See https://www.home-assistant.io/integrations/debugpy/ From c9330841a111a27e84f8adb7ec7103632eb0cfc0 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 10 Jul 2022 23:08:33 +0200 Subject: [PATCH 2418/3516] Add AirQ sensors to Sensibo (#74868) --- homeassistant/components/sensibo/sensor.py | 58 +++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index fad22fdd677..8a53a0febd4 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -17,10 +17,13 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_BILLION, + CONCENTRATION_PARTS_PER_MILLION, ELECTRIC_POTENTIAL_VOLT, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory @@ -106,7 +109,6 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( SensiboMotionSensorEntityDescription( key="temperature", device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, name="Temperature", icon="mdi:thermometer", @@ -143,9 +145,39 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( value_fn=lambda data: data.timer_time, extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on}, ), + SensiboDeviceSensorEntityDescription( + key="feels_like", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + name="Temperature Feels Like", + value_fn=lambda data: data.feelslike, + extra_fn=None, + entity_registry_enabled_default=False, + ), FILTER_LAST_RESET_DESCRIPTION, ) +AIRQ_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( + SensiboDeviceSensorEntityDescription( + key="airq_tvoc", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + state_class=SensorStateClass.MEASUREMENT, + icon="mdi:air-filter", + name="AirQ TVOC", + value_fn=lambda data: data.tvoc, + extra_fn=None, + ), + SensiboDeviceSensorEntityDescription( + key="airq_co2", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + name="AirQ CO2", + value_fn=lambda data: data.co2, + extra_fn=None, + ), +) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -177,6 +209,12 @@ async def async_setup_entry( for description in DEVICE_SENSOR_TYPES if device_data.model != "pure" ) + entities.extend( + SensiboDeviceSensor(coordinator, device_id, description) + for device_id, device_data in coordinator.data.parsed.items() + for description in AIRQ_SENSOR_TYPES + if device_data.model == "airq" + ) async_add_entities(entities) @@ -207,6 +245,15 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, SensorEntity): f"{self.device_data.name} Motion Sensor {entity_description.name}" ) + @property + def native_unit_of_measurement(self) -> str | None: + """Add native unit of measurement.""" + if self.entity_description.device_class == SensorDeviceClass.TEMPERATURE: + return ( + TEMP_CELSIUS if self.device_data.temp_unit == "C" else TEMP_FAHRENHEIT + ) + return self.entity_description.native_unit_of_measurement + @property def native_value(self) -> StateType: """Return value of sensor.""" @@ -235,6 +282,15 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, SensorEntity): self._attr_unique_id = f"{device_id}-{entity_description.key}" self._attr_name = f"{self.device_data.name} {entity_description.name}" + @property + def native_unit_of_measurement(self) -> str | None: + """Add native unit of measurement.""" + if self.entity_description.device_class == SensorDeviceClass.TEMPERATURE: + return ( + TEMP_CELSIUS if self.device_data.temp_unit == "C" else TEMP_FAHRENHEIT + ) + return self.entity_description.native_unit_of_measurement + @property def native_value(self) -> StateType | datetime: """Return value of sensor.""" From 8054e309b346558e07dc3f9c729d592148432bc1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 23:23:40 +0200 Subject: [PATCH 2419/3516] Update flake8-noqa to 1.2.5 (#74896) --- .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 fd2d5ac7e20..7206de9ee83 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: - flake8-docstrings==1.6.0 - pydocstyle==6.1.1 - flake8-comprehensions==3.10.0 - - flake8-noqa==1.2.1 + - flake8-noqa==1.2.5 - 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 c229bc51731..dd5077cc00c 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -5,7 +5,7 @@ black==22.6.0 codespell==2.1.0 flake8-comprehensions==3.10.0 flake8-docstrings==1.6.0 -flake8-noqa==1.2.1 +flake8-noqa==1.2.5 flake8==4.0.1 isort==5.10.1 mccabe==0.6.1 From 176e2754ec9752df12180ae3a102b23b037451ce Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 23:56:04 +0200 Subject: [PATCH 2420/3516] Update adb-shell to 0.4.3 (#74855) --- homeassistant/components/androidtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 7bddd13c833..92d4f806b39 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,7 +3,7 @@ "name": "Android TV", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell[async]==0.4.2", + "adb-shell[async]==0.4.3", "androidtv[async]==0.0.67", "pure-python-adb[async]==0.3.0.dev0" ], diff --git a/requirements_all.txt b/requirements_all.txt index c136010056a..62ee16e610a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -77,7 +77,7 @@ accuweather==0.3.0 adax==0.2.0 # homeassistant.components.androidtv -adb-shell[async]==0.4.2 +adb-shell[async]==0.4.3 # homeassistant.components.alarmdecoder adext==0.4.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd29bcded25..e30e0d5b5c3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -67,7 +67,7 @@ accuweather==0.3.0 adax==0.2.0 # homeassistant.components.androidtv -adb-shell[async]==0.4.2 +adb-shell[async]==0.4.3 # homeassistant.components.alarmdecoder adext==0.4.2 From f15d3fc5dba45e4e503d7cd17e30f6f29a5329b0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 23:56:48 +0200 Subject: [PATCH 2421/3516] Migrate WLED to new entity naming style (#74860) --- homeassistant/components/wled/binary_sensor.py | 2 +- homeassistant/components/wled/button.py | 2 +- homeassistant/components/wled/light.py | 7 +++---- homeassistant/components/wled/models.py | 2 ++ homeassistant/components/wled/number.py | 7 ++----- homeassistant/components/wled/select.py | 14 ++++++-------- homeassistant/components/wled/sensor.py | 13 ++++++------- homeassistant/components/wled/switch.py | 12 ++++++------ homeassistant/components/wled/update.py | 2 +- 9 files changed, 28 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/wled/binary_sensor.py b/homeassistant/components/wled/binary_sensor.py index d2262798d50..61f8dc45f7b 100644 --- a/homeassistant/components/wled/binary_sensor.py +++ b/homeassistant/components/wled/binary_sensor.py @@ -34,6 +34,7 @@ class WLEDUpdateBinarySensor(WLEDEntity, BinarySensorEntity): _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_device_class = BinarySensorDeviceClass.UPDATE + _attr_name = "Firmware" # Disabled by default, as this entity is deprecated. _attr_entity_registry_enabled_default = False @@ -41,7 +42,6 @@ class WLEDUpdateBinarySensor(WLEDEntity, BinarySensorEntity): def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize the button entity.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Firmware" self._attr_unique_id = f"{coordinator.data.info.mac_address}_update" @property diff --git a/homeassistant/components/wled/button.py b/homeassistant/components/wled/button.py index 97877053163..b08ee396c70 100644 --- a/homeassistant/components/wled/button.py +++ b/homeassistant/components/wled/button.py @@ -28,11 +28,11 @@ class WLEDRestartButton(WLEDEntity, ButtonEntity): _attr_device_class = ButtonDeviceClass.RESTART _attr_entity_category = EntityCategory.CONFIG + _attr_name = "Restart" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize the button entity.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Restart" self._attr_unique_id = f"{coordinator.data.info.mac_address}_restart" @wled_exception_handler diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 4f5c758dfff..98be359628e 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -52,13 +52,13 @@ class WLEDMasterLight(WLEDEntity, LightEntity): _attr_color_mode = ColorMode.BRIGHTNESS _attr_icon = "mdi:led-strip-variant" + _attr_name = "Master" _attr_supported_features = LightEntityFeature.TRANSITION _attr_supported_color_modes = {ColorMode.BRIGHTNESS} def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED master light.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Master" self._attr_unique_id = coordinator.data.info.mac_address @property @@ -118,9 +118,8 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): # 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}" - if segment == 0: - self._attr_name = coordinator.data.info.name + if segment != 0: + self._attr_name = f"Segment {segment}" self._attr_unique_id = ( f"{self.coordinator.data.info.mac_address}_{self._segment}" diff --git a/homeassistant/components/wled/models.py b/homeassistant/components/wled/models.py index b5fc0855e04..2bdd2e46e2c 100644 --- a/homeassistant/components/wled/models.py +++ b/homeassistant/components/wled/models.py @@ -10,6 +10,8 @@ from .coordinator import WLEDDataUpdateCoordinator class WLEDEntity(CoordinatorEntity[WLEDDataUpdateCoordinator]): """Defines a base WLED entity.""" + _attr_has_entity_name = True + @property def device_info(self) -> DeviceInfo: """Return device information about this WLED device.""" diff --git a/homeassistant/components/wled/number.py b/homeassistant/components/wled/number.py index 6c426cc44c5..d6032791e5b 100644 --- a/homeassistant/components/wled/number.py +++ b/homeassistant/components/wled/number.py @@ -71,11 +71,8 @@ class WLEDNumber(WLEDEntity, NumberEntity): # 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}" + if segment != 0: + self._attr_name = f"Segment {segment} {description.name}" self._attr_unique_id = ( f"{coordinator.data.info.mac_address}_{description.key}_{segment}" diff --git a/homeassistant/components/wled/select.py b/homeassistant/components/wled/select.py index e555b3422ce..c3980f9f9c7 100644 --- a/homeassistant/components/wled/select.py +++ b/homeassistant/components/wled/select.py @@ -51,12 +51,12 @@ class WLEDLiveOverrideSelect(WLEDEntity, SelectEntity): _attr_device_class = DEVICE_CLASS_WLED_LIVE_OVERRIDE _attr_entity_category = EntityCategory.CONFIG _attr_icon = "mdi:theater" + _attr_name = "Live override" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED .""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Live Override" self._attr_unique_id = f"{coordinator.data.info.mac_address}_live_override" self._attr_options = [str(live.value) for live in Live] @@ -75,12 +75,12 @@ class WLEDPresetSelect(WLEDEntity, SelectEntity): """Defined a WLED Preset select.""" _attr_icon = "mdi:playlist-play" + _attr_name = "Preset" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED .""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Preset" self._attr_unique_id = f"{coordinator.data.info.mac_address}_preset" self._attr_options = [preset.name for preset in self.coordinator.data.presets] @@ -106,12 +106,12 @@ class WLEDPlaylistSelect(WLEDEntity, SelectEntity): """Define a WLED Playlist select.""" _attr_icon = "mdi:play-speed" + _attr_name = "Playlist" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED playlist.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Playlist" self._attr_unique_id = f"{coordinator.data.info.mac_address}_playlist" self._attr_options = [ playlist.name for playlist in self.coordinator.data.playlists @@ -140,6 +140,7 @@ class WLEDPaletteSelect(WLEDEntity, SelectEntity): _attr_entity_category = EntityCategory.CONFIG _attr_icon = "mdi:palette-outline" + _attr_name = "Color palette" _segment: int def __init__(self, coordinator: WLEDDataUpdateCoordinator, segment: int) -> None: @@ -148,11 +149,8 @@ class WLEDPaletteSelect(WLEDEntity, SelectEntity): # 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} Color Palette" - ) - if segment == 0: - self._attr_name = f"{coordinator.data.info.name} Color Palette" + if segment != 0: + self._attr_name = f"Segment {segment} color palette" self._attr_unique_id = f"{coordinator.data.info.mac_address}_palette_{segment}" self._attr_options = [ diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 720158938c7..4a677910273 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -50,7 +50,7 @@ class WLEDSensorEntityDescription( SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( WLEDSensorEntityDescription( key="estimated_current", - name="Estimated Current", + name="Estimated current", native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, @@ -60,13 +60,13 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( ), WLEDSensorEntityDescription( key="info_leds_count", - name="LED Count", + name="LED count", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.info.leds.count, ), WLEDSensorEntityDescription( key="info_leds_max_power", - name="Max Current", + name="Max current", native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.CURRENT, @@ -83,7 +83,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( ), WLEDSensorEntityDescription( key="free_heap", - name="Free Memory", + name="Free memory", icon="mdi:memory", native_unit_of_measurement=DATA_BYTES, state_class=SensorStateClass.MEASUREMENT, @@ -93,7 +93,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( ), WLEDSensorEntityDescription( key="wifi_signal", - name="Wi-Fi Signal", + name="Wi-Fi signal", icon="mdi:wifi", native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, @@ -111,7 +111,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( ), WLEDSensorEntityDescription( key="wifi_channel", - name="Wi-Fi Channel", + name="Wi-Fi channel", icon="mdi:wifi", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, @@ -155,7 +155,6 @@ class WLEDSensorEntity(WLEDEntity, SensorEntity): """Initialize a WLED sensor entity.""" super().__init__(coordinator=coordinator) self.entity_description = description - self._attr_name = f"{coordinator.data.info.name} {description.name}" self._attr_unique_id = f"{coordinator.data.info.mac_address}_{description.key}" @property diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index e98b3494ad6..7d0d9ee24fb 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -55,11 +55,11 @@ class WLEDNightlightSwitch(WLEDEntity, SwitchEntity): _attr_icon = "mdi:weather-night" _attr_entity_category = EntityCategory.CONFIG + _attr_name = "Nightlight" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED nightlight switch.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Nightlight" self._attr_unique_id = f"{coordinator.data.info.mac_address}_nightlight" @property @@ -92,11 +92,11 @@ class WLEDSyncSendSwitch(WLEDEntity, SwitchEntity): _attr_icon = "mdi:upload-network-outline" _attr_entity_category = EntityCategory.CONFIG + _attr_name = "Sync send" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED sync send switch.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Sync Send" self._attr_unique_id = f"{coordinator.data.info.mac_address}_sync_send" @property @@ -125,11 +125,11 @@ class WLEDSyncReceiveSwitch(WLEDEntity, SwitchEntity): _attr_icon = "mdi:download-network-outline" _attr_entity_category = EntityCategory.CONFIG + _attr_name = "Sync receive" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED sync receive switch.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Sync Receive" self._attr_unique_id = f"{coordinator.data.info.mac_address}_sync_receive" @property @@ -158,6 +158,7 @@ class WLEDReverseSwitch(WLEDEntity, SwitchEntity): _attr_icon = "mdi:swap-horizontal-bold" _attr_entity_category = EntityCategory.CONFIG + _attr_name = "Reverse" _segment: int def __init__(self, coordinator: WLEDDataUpdateCoordinator, segment: int) -> None: @@ -166,9 +167,8 @@ class WLEDReverseSwitch(WLEDEntity, SwitchEntity): # 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} Reverse" - if segment == 0: - self._attr_name = f"{coordinator.data.info.name} Reverse" + if segment != 0: + self._attr_name = f"Segment {segment} reverse" self._attr_unique_id = f"{coordinator.data.info.mac_address}_reverse_{segment}" self._segment = segment diff --git a/homeassistant/components/wled/update.py b/homeassistant/components/wled/update.py index f0fc532b3b3..75546fdac1a 100644 --- a/homeassistant/components/wled/update.py +++ b/homeassistant/components/wled/update.py @@ -36,11 +36,11 @@ class WLEDUpdateEntity(WLEDEntity, UpdateEntity): UpdateEntityFeature.INSTALL | UpdateEntityFeature.SPECIFIC_VERSION ) _attr_title = "WLED" + _attr_name = "Firmware" def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize the update entity.""" super().__init__(coordinator=coordinator) - self._attr_name = f"{coordinator.data.info.name} Firmware" self._attr_unique_id = coordinator.data.info.mac_address @property From 08887a6faa19cc56d1c13a536f236b0ae404c202 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 23:57:46 +0200 Subject: [PATCH 2422/3516] Update yamllint to 1.27.1 (#74853) --- .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 7206de9ee83..91bda72e616 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,7 +61,7 @@ repos: - --branch=master - --branch=rc - repo: https://github.com/adrienverge/yamllint.git - rev: v1.26.3 + rev: v1.27.1 hooks: - id: yamllint - repo: https://github.com/pre-commit/mirrors-prettier diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index dd5077cc00c..caed3668db7 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -13,4 +13,4 @@ pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 pyupgrade==2.34.0 -yamllint==1.26.3 +yamllint==1.27.1 From 792c825699f0c03569bd0da01c88efdfed9d6446 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 23:58:05 +0200 Subject: [PATCH 2423/3516] Update numpy to 1.23.1 (#74851) --- homeassistant/components/compensation/manifest.json | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/gen_requirements_all.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index 509d5740b22..5b1ff9715d1 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,7 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.23.0"], + "requirements": ["numpy==1.23.1"], "codeowners": ["@Petro31"], "iot_class": "calculated" } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 7485ff9d608..561ebdb6e89 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.23.0", "pyiqvia==2022.04.0"], + "requirements": ["numpy==1.23.1", "pyiqvia==2022.04.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyiqvia"] diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 0272feb0f9e..cada1199084 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.23.0", "opencv-python-headless==4.6.0.66"], + "requirements": ["numpy==1.23.1", "opencv-python-headless==4.6.0.66"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index dd88fd7e277..c8a62791429 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.5.0", "tf-models-official==2.5.0", "pycocotools==2.0.1", - "numpy==1.23.0", + "numpy==1.23.1", "pillow==9.2.0" ], "codeowners": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index b579cc036bb..e70056f207a 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.23.0"], + "requirements": ["numpy==1.23.1"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d53db9a7464..d443a1f66aa 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -88,7 +88,7 @@ httpcore==0.15.0 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy==1.23.0 +numpy==1.23.1 # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 62ee16e610a..c78dee5d185 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1129,7 +1129,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.23.0 +numpy==1.23.1 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e30e0d5b5c3..f82b2d1a25e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -785,7 +785,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.23.0 +numpy==1.23.1 # homeassistant.components.google oauth2client==4.1.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 11e88976e83..10951bcc7e8 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -106,7 +106,7 @@ httpcore==0.15.0 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy==1.23.0 +numpy==1.23.1 # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 From a4517a4c1d9dc9149b7fc4a916b7950804e9881f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 10 Jul 2022 23:59:08 +0200 Subject: [PATCH 2424/3516] Trigger full CI on Bluetooth integration changes (#74929) --- .core_files.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.core_files.yaml b/.core_files.yaml index 55b543a333e..29390865611 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -52,6 +52,7 @@ components: &components - homeassistant/components/auth/** - homeassistant/components/automation/** - homeassistant/components/backup/** + - homeassistant/components/bluetooth/** - homeassistant/components/cloud/** - homeassistant/components/config/** - homeassistant/components/configurator/** From 9ff77e0fa1af036b4b0abf8c56468b7760d83e74 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 00:06:45 +0200 Subject: [PATCH 2425/3516] Update pytest-sugar is 0.9.5 (#74931) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 822fb02c6e0..dbb7a76dac4 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -20,7 +20,7 @@ pytest-cov==3.0.0 pytest-freezegun==0.4.2 pytest-socket==0.5.1 pytest-test-groups==1.0.3 -pytest-sugar==0.9.4 +pytest-sugar==0.9.5 pytest-timeout==2.1.0 pytest-xdist==2.5.0 pytest==7.1.2 From 261c52e26087189e45ff1eac2a70da8919b6eaef Mon Sep 17 00:00:00 2001 From: Thomas Hollstegge Date: Mon, 11 Jul 2022 00:10:55 +0200 Subject: [PATCH 2426/3516] Alexa: Fix duplicate proactive reports (#74930) --- homeassistant/components/alexa/config.py | 4 ++-- homeassistant/components/alexa/smart_home_http.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index b6cbe6ba74b..9f51d92a229 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -65,6 +65,7 @@ class AbstractConfig(ABC): async def async_enable_proactive_mode(self): """Enable proactive mode.""" + _LOGGER.debug("Enable proactive mode") if self._unsub_proactive_report is None: self._unsub_proactive_report = self.hass.async_create_task( async_enable_proactive_mode(self.hass, self) @@ -77,6 +78,7 @@ class AbstractConfig(ABC): async def async_disable_proactive_mode(self): """Disable proactive mode.""" + _LOGGER.debug("Disable proactive mode") if unsub_func := await self._unsub_proactive_report: unsub_func() self._unsub_proactive_report = None @@ -113,7 +115,6 @@ class AbstractConfig(ABC): self._store.set_authorized(authorized) if self.should_report_state != self.is_reporting_states: if self.should_report_state: - _LOGGER.debug("Enable proactive mode") try: await self.async_enable_proactive_mode() except Exception: @@ -121,7 +122,6 @@ class AbstractConfig(ABC): self._store.set_authorized(False) raise else: - _LOGGER.debug("Disable proactive mode") await self.async_disable_proactive_mode() diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index 6a953a9f9d4..9be7381adb6 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -12,7 +12,6 @@ from .auth import Auth from .config import AbstractConfig from .const import CONF_ENDPOINT, CONF_ENTITY_CONFIG, CONF_FILTER, CONF_LOCALE from .smart_home import async_handle_message -from .state_report import async_enable_proactive_mode _LOGGER = logging.getLogger(__name__) SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home" @@ -104,7 +103,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> None: hass.http.register_view(SmartHomeView(smart_home_config)) if smart_home_config.should_report_state: - await async_enable_proactive_mode(hass, smart_home_config) + await smart_home_config.async_enable_proactive_mode() class SmartHomeView(HomeAssistantView): From d697bb53c5a32ebd4b58b4298f63d894fe9d4aaa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 00:11:43 +0200 Subject: [PATCH 2427/3516] Update lru-dict to 1.1.8 (#74932) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d443a1f66aa..c92bf170f45 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ home-assistant-frontend==20220707.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 -lru-dict==1.1.7 +lru-dict==1.1.8 orjson==3.7.7 paho-mqtt==1.6.1 pillow==9.2.0 diff --git a/pyproject.toml b/pyproject.toml index 621bbf68999..41059773977 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ "httpx==0.23.0", "ifaddr==0.1.7", "jinja2==3.1.2", - "lru-dict==1.1.7", + "lru-dict==1.1.8", "PyJWT==2.4.0", # PyJWT has loose dependency. We want the latest one. "cryptography==36.0.2", diff --git a/requirements.txt b/requirements.txt index fefa0f33ecb..84d8711753e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ ciso8601==2.2.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 -lru-dict==1.1.7 +lru-dict==1.1.8 PyJWT==2.4.0 cryptography==36.0.2 orjson==3.7.7 From abb11009b4ce1455434d54f6d44488da506e40bb Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 11 Jul 2022 00:23:28 +0000 Subject: [PATCH 2428/3516] [ci skip] Translation update --- .../here_travel_time/translations/ja.json | 7 +++++++ .../here_travel_time/translations/pt.json | 13 +++++++++++++ .../season/translations/sensor.pt.json | 6 ++++++ .../zodiac/translations/sensor.pt.json | 18 ++++++++++++++++++ 4 files changed, 44 insertions(+) create mode 100644 homeassistant/components/here_travel_time/translations/pt.json create mode 100644 homeassistant/components/zodiac/translations/sensor.pt.json diff --git a/homeassistant/components/here_travel_time/translations/ja.json b/homeassistant/components/here_travel_time/translations/ja.json index 6db262d829a..ae3eb1d4f73 100644 --- a/homeassistant/components/here_travel_time/translations/ja.json +++ b/homeassistant/components/here_travel_time/translations/ja.json @@ -39,6 +39,13 @@ }, "title": "\u539f\u70b9(Origin)\u3092\u9078\u629e" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "\u5730\u56f3\u4e0a\u306e\u5834\u6240\u3092\u4f7f\u7528\u3059\u308b", + "origin_entity": "\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u4f7f\u7528\u3059\u308b" + }, + "title": "\u539f\u70b9(Origin)\u3092\u9078\u629e" + }, "user": { "data": { "api_key": "API\u30ad\u30fc", diff --git a/homeassistant/components/here_travel_time/translations/pt.json b/homeassistant/components/here_travel_time/translations/pt.json new file mode 100644 index 00000000000..f412c0033f6 --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "origin_menu": { + "menu_options": { + "origin_coordinates": "Usando uma localiza\u00e7\u00e3o no mapa", + "origin_entity": "Usando uma entidade" + }, + "title": "Escolha a Origem" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.pt.json b/homeassistant/components/season/translations/sensor.pt.json index 4c81e432350..e30461da7d2 100644 --- a/homeassistant/components/season/translations/sensor.pt.json +++ b/homeassistant/components/season/translations/sensor.pt.json @@ -1,5 +1,11 @@ { "state": { + "season__season": { + "autumn": "Outono ", + "spring": "Primavera", + "summer": "Ver\u00e3o ", + "winter": "Inverno" + }, "season__season__": { "autumn": "Outono", "spring": "Primavera", diff --git a/homeassistant/components/zodiac/translations/sensor.pt.json b/homeassistant/components/zodiac/translations/sensor.pt.json new file mode 100644 index 00000000000..b37a6886df0 --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.pt.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Aqu\u00e1rio ", + "aries": "Carneiro ", + "cancer": "Caranguejo ", + "capricorn": "Capric\u00f3rnio", + "gemini": "G\u00e9meos ", + "leo": "Le\u00e3o ", + "libra": "Balan\u00e7a", + "pisces": "Peixes", + "sagittarius": "Sagit\u00e1rio", + "scorpio": "Escorpi\u00e3o ", + "taurus": "Touro", + "virgo": "Virgem" + } + } +} \ No newline at end of file From 7a729aed54c4f61be993097a758a57b8878acbb4 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 11 Jul 2022 14:03:48 +1000 Subject: [PATCH 2429/3516] Migrate Advantage Air to new entity naming style (#74940) --- .../components/advantage_air/binary_sensor.py | 10 +++++----- homeassistant/components/advantage_air/cover.py | 2 +- homeassistant/components/advantage_air/entity.py | 2 ++ homeassistant/components/advantage_air/select.py | 2 +- homeassistant/components/advantage_air/sensor.py | 8 ++++---- .../components/advantage_air/test_binary_sensor.py | 12 ++++++------ tests/components/advantage_air/test_climate.py | 6 +++--- tests/components/advantage_air/test_cover.py | 10 +++++----- tests/components/advantage_air/test_select.py | 2 +- tests/components/advantage_air/test_sensor.py | 14 +++++++------- tests/components/advantage_air/test_switch.py | 2 +- 11 files changed, 36 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index 73b10b158b0..bc302e5d4a6 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -27,7 +27,7 @@ async def async_setup_entry( entities: list[BinarySensorEntity] = [] for ac_key, ac_device in instance["coordinator"].data["aircons"].items(): - entities.append(AdvantageAirZoneFilter(instance, ac_key)) + entities.append(AdvantageAirFilter(instance, ac_key)) for zone_key, zone in ac_device["zones"].items(): # Only add motion sensor when motion is enabled if zone["motionConfig"] >= 2: @@ -38,7 +38,7 @@ async def async_setup_entry( async_add_entities(entities) -class AdvantageAirZoneFilter(AdvantageAirEntity, BinarySensorEntity): +class AdvantageAirFilter(AdvantageAirEntity, BinarySensorEntity): """Advantage Air Filter.""" _attr_device_class = BinarySensorDeviceClass.PROBLEM @@ -47,7 +47,7 @@ class AdvantageAirZoneFilter(AdvantageAirEntity, BinarySensorEntity): def __init__(self, instance, ac_key): """Initialize an Advantage Air Filter.""" super().__init__(instance, ac_key) - self._attr_name = f'{self._ac["name"]} Filter' + self._attr_name = f'{self._ac["name"]} filter' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-filter' ) @@ -66,7 +66,7 @@ class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone Motion.""" super().__init__(instance, ac_key, zone_key) - self._attr_name = f'{self._zone["name"]} Motion' + self._attr_name = f'{self._zone["name"]} motion' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-motion' ) @@ -86,7 +86,7 @@ class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone MyZone.""" super().__init__(instance, ac_key, zone_key) - self._attr_name = f'{self._zone["name"]} MyZone' + self._attr_name = f'{self._zone["name"]} myZone' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-myzone' ) diff --git a/homeassistant/components/advantage_air/cover.py b/homeassistant/components/advantage_air/cover.py index 36ae2c7fff0..f8240126476 100644 --- a/homeassistant/components/advantage_air/cover.py +++ b/homeassistant/components/advantage_air/cover.py @@ -52,7 +52,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Cover Class.""" super().__init__(instance, ac_key, zone_key) - self._attr_name = f'{self._zone["name"]}' + self._attr_name = self._zone["name"] self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}' ) diff --git a/homeassistant/components/advantage_air/entity.py b/homeassistant/components/advantage_air/entity.py index 9514cc7915b..b0ff1bfb8c8 100644 --- a/homeassistant/components/advantage_air/entity.py +++ b/homeassistant/components/advantage_air/entity.py @@ -9,6 +9,8 @@ from .const import DOMAIN class AdvantageAirEntity(CoordinatorEntity): """Parent class for Advantage Air Entities.""" + _attr_has_entity_name = True + def __init__(self, instance, ac_key, zone_key=None): """Initialize common aspects of an Advantage Air sensor.""" super().__init__(instance["coordinator"]) diff --git a/homeassistant/components/advantage_air/select.py b/homeassistant/components/advantage_air/select.py index ecc612ae1ed..ff2a555bc80 100644 --- a/homeassistant/components/advantage_air/select.py +++ b/homeassistant/components/advantage_air/select.py @@ -36,7 +36,7 @@ class AdvantageAirMyZone(AdvantageAirEntity, SelectEntity): def __init__(self, instance, ac_key): """Initialize an Advantage Air MyZone control.""" super().__init__(instance, ac_key) - self._attr_name = f'{self._ac["name"]} MyZone' + self._attr_name = f'{self._ac["name"]} myZone' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-myzone' ) diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index 8055c37a571..855a0e6d15f 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -67,7 +67,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): super().__init__(instance, ac_key) self.action = action self._time_key = f"countDownTo{action}" - self._attr_name = f'{self._ac["name"]} Time To {action}' + self._attr_name = f'{self._ac["name"]} time to {action}' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-timeto{action}' ) @@ -100,7 +100,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone Vent Sensor.""" super().__init__(instance, ac_key, zone_key=zone_key) - self._attr_name = f'{self._zone["name"]} Vent' + self._attr_name = f'{self._zone["name"]} vent' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-vent' ) @@ -130,7 +130,7 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone wireless signal sensor.""" super().__init__(instance, ac_key, zone_key) - self._attr_name = f'{self._zone["name"]} Signal' + self._attr_name = f'{self._zone["name"]} signal' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-signal' ) @@ -166,7 +166,7 @@ class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity): def __init__(self, instance, ac_key, zone_key): """Initialize an Advantage Air Zone Temp Sensor.""" super().__init__(instance, ac_key, zone_key) - self._attr_name = f'{self._zone["name"]} Temperature' + self._attr_name = f'{self._zone["name"]} temperature' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-temp' ) diff --git a/tests/components/advantage_air/test_binary_sensor.py b/tests/components/advantage_air/test_binary_sensor.py index 275b5fc4e52..9aa092a8679 100644 --- a/tests/components/advantage_air/test_binary_sensor.py +++ b/tests/components/advantage_air/test_binary_sensor.py @@ -34,7 +34,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test First Air Filter - entity_id = "binary_sensor.ac_one_filter" + entity_id = "binary_sensor.testname_ac_one_filter" state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF @@ -44,7 +44,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-filter" # Test Second Air Filter - entity_id = "binary_sensor.ac_two_filter" + entity_id = "binary_sensor.testname_ac_two_filter" state = hass.states.get(entity_id) assert state assert state.state == STATE_ON @@ -54,7 +54,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac2-filter" # Test First Motion Sensor - entity_id = "binary_sensor.zone_open_with_sensor_motion" + entity_id = "binary_sensor.testname_zone_open_with_sensor_motion" state = hass.states.get(entity_id) assert state assert state.state == STATE_ON @@ -64,7 +64,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-motion" # Test Second Motion Sensor - entity_id = "binary_sensor.zone_closed_with_sensor_motion" + entity_id = "binary_sensor.testname_zone_closed_with_sensor_motion" state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF @@ -74,7 +74,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z02-motion" # Test First MyZone Sensor (disabled by default) - entity_id = "binary_sensor.zone_open_with_sensor_myzone" + entity_id = "binary_sensor.testname_zone_open_with_sensor_myzone" assert not hass.states.get(entity_id) @@ -96,7 +96,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-myzone" # Test Second Motion Sensor (disabled by default) - entity_id = "binary_sensor.zone_closed_with_sensor_myzone" + entity_id = "binary_sensor.testname_zone_closed_with_sensor_myzone" assert not hass.states.get(entity_id) diff --git a/tests/components/advantage_air/test_climate.py b/tests/components/advantage_air/test_climate.py index 4d075a36151..27f89d1df3e 100644 --- a/tests/components/advantage_air/test_climate.py +++ b/tests/components/advantage_air/test_climate.py @@ -51,7 +51,7 @@ async def test_climate_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test Main Climate Entity - entity_id = "climate.ac_one" + entity_id = "climate.testname_ac_one" state = hass.states.get(entity_id) assert state assert state.state == HVACMode.FAN_ONLY @@ -122,7 +122,7 @@ async def test_climate_async_setup_entry(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" # Test Climate Zone Entity - entity_id = "climate.zone_open_with_sensor" + entity_id = "climate.testname_zone_open_with_sensor" state = hass.states.get(entity_id) assert state assert state.attributes.get("min_temp") == 16 @@ -204,7 +204,7 @@ async def test_climate_async_failed_update(hass, aioclient_mock): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: ["climate.ac_one"], ATTR_TEMPERATURE: 25}, + {ATTR_ENTITY_ID: ["climate.testname_ac_one"], ATTR_TEMPERATURE: 25}, blocking=True, ) assert len(aioclient_mock.mock_calls) == 2 diff --git a/tests/components/advantage_air/test_cover.py b/tests/components/advantage_air/test_cover.py index 2868179b3ee..fa3abda5138 100644 --- a/tests/components/advantage_air/test_cover.py +++ b/tests/components/advantage_air/test_cover.py @@ -45,7 +45,7 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test Cover Zone Entity - entity_id = "cover.zone_open_without_sensor" + entity_id = "cover.testname_zone_open_without_sensor" state = hass.states.get(entity_id) assert state assert state.state == STATE_OPEN @@ -119,8 +119,8 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): SERVICE_CLOSE_COVER, { ATTR_ENTITY_ID: [ - "cover.zone_open_without_sensor", - "cover.zone_closed_without_sensor", + "cover.testname_zone_open_without_sensor", + "cover.testname_zone_closed_without_sensor", ] }, blocking=True, @@ -134,8 +134,8 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): SERVICE_OPEN_COVER, { ATTR_ENTITY_ID: [ - "cover.zone_open_without_sensor", - "cover.zone_closed_without_sensor", + "cover.testname_zone_open_without_sensor", + "cover.testname_zone_closed_without_sensor", ] }, blocking=True, diff --git a/tests/components/advantage_air/test_select.py b/tests/components/advantage_air/test_select.py index 3d246f566e5..41ed9c407fc 100644 --- a/tests/components/advantage_air/test_select.py +++ b/tests/components/advantage_air/test_select.py @@ -37,7 +37,7 @@ async def test_select_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test Select Entity - entity_id = "select.ac_one_myzone" + entity_id = "select.testname_ac_one_myzone" state = hass.states.get(entity_id) assert state assert state.state == "Zone open with Sensor" diff --git a/tests/components/advantage_air/test_sensor.py b/tests/components/advantage_air/test_sensor.py index 997f11dea91..70322f4c9df 100644 --- a/tests/components/advantage_air/test_sensor.py +++ b/tests/components/advantage_air/test_sensor.py @@ -41,7 +41,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test First TimeToOn Sensor - entity_id = "sensor.ac_one_time_to_on" + entity_id = "sensor.testname_ac_one_time_to_on" state = hass.states.get(entity_id) assert state assert int(state.state) == 0 @@ -66,7 +66,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" # Test First TimeToOff Sensor - entity_id = "sensor.ac_one_time_to_off" + entity_id = "sensor.testname_ac_one_time_to_off" state = hass.states.get(entity_id) assert state assert int(state.state) == 10 @@ -91,7 +91,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" # Test First Zone Vent Sensor - entity_id = "sensor.zone_open_with_sensor_vent" + entity_id = "sensor.testname_zone_open_with_sensor_vent" state = hass.states.get(entity_id) assert state assert int(state.state) == 100 @@ -101,7 +101,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-vent" # Test Second Zone Vent Sensor - entity_id = "sensor.zone_closed_with_sensor_vent" + entity_id = "sensor.testname_zone_closed_with_sensor_vent" state = hass.states.get(entity_id) assert state assert int(state.state) == 0 @@ -111,7 +111,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z02-vent" # Test First Zone Signal Sensor - entity_id = "sensor.zone_open_with_sensor_signal" + entity_id = "sensor.testname_zone_open_with_sensor_signal" state = hass.states.get(entity_id) assert state assert int(state.state) == 40 @@ -121,7 +121,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-signal" # Test Second Zone Signal Sensor - entity_id = "sensor.zone_closed_with_sensor_signal" + entity_id = "sensor.testname_zone_closed_with_sensor_signal" state = hass.states.get(entity_id) assert state assert int(state.state) == 10 @@ -131,7 +131,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z02-signal" # Test First Zone Temp Sensor (disabled by default) - entity_id = "sensor.zone_open_with_sensor_temperature" + entity_id = "sensor.testname_zone_open_with_sensor_temperature" assert not hass.states.get(entity_id) diff --git a/tests/components/advantage_air/test_switch.py b/tests/components/advantage_air/test_switch.py index 1a78025df70..1d99d7f29c1 100644 --- a/tests/components/advantage_air/test_switch.py +++ b/tests/components/advantage_air/test_switch.py @@ -41,7 +41,7 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test Switch Entity - entity_id = "switch.ac_one_fresh_air" + entity_id = "switch.testname_ac_one_fresh_air" state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF From d0f71d2e53ce98ec35757d742b438661f888efa1 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 11 Jul 2022 14:04:18 +1000 Subject: [PATCH 2430/3516] Migrate Aussie Broadband to new entity naming style (#74937) --- .../components/aussie_broadband/sensor.py | 22 +++++----- .../aussie_broadband/test_sensor.py | 44 +++++++++++++------ 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/aussie_broadband/sensor.py b/homeassistant/components/aussie_broadband/sensor.py index 09946cef03d..ecc891deba9 100644 --- a/homeassistant/components/aussie_broadband/sensor.py +++ b/homeassistant/components/aussie_broadband/sensor.py @@ -33,7 +33,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = ( # Internet Services sensors SensorValueEntityDescription( key="usedMb", - name="Data Used", + name="Data used", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=DATA_MEGABYTES, icon="mdi:network", @@ -55,35 +55,35 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = ( # Mobile Phone Services sensors SensorValueEntityDescription( key="national", - name="National Calls", + name="National calls", state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone", value=lambda x: x.get("calls"), ), SensorValueEntityDescription( key="mobile", - name="Mobile Calls", + name="Mobile calls", state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone", value=lambda x: x.get("calls"), ), SensorValueEntityDescription( key="international", - name="International Calls", + name="International calls", entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone-plus", ), SensorValueEntityDescription( key="sms", - name="SMS Sent", + name="SMS sent", state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:message-processing", value=lambda x: x.get("calls"), ), SensorValueEntityDescription( key="internet", - name="Data Used", + name="Data used", state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement=DATA_KILOBYTES, icon="mdi:network", @@ -91,14 +91,14 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = ( ), SensorValueEntityDescription( key="voicemail", - name="Voicemail Calls", + name="Voicemail calls", entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone", ), SensorValueEntityDescription( key="other", - name="Other Calls", + name="Other calls", entity_registry_enabled_default=False, state_class=SensorStateClass.TOTAL_INCREASING, icon="mdi:phone", @@ -106,13 +106,13 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = ( # Generic sensors SensorValueEntityDescription( key="daysTotal", - name="Billing Cycle Length", + name="Billing cycle length", native_unit_of_measurement=TIME_DAYS, icon="mdi:calendar-range", ), SensorValueEntityDescription( key="daysRemaining", - name="Billing Cycle Remaining", + name="Billing cycle remaining", native_unit_of_measurement=TIME_DAYS, icon="mdi:calendar-clock", ), @@ -137,6 +137,7 @@ async def async_setup_entry( class AussieBroadandSensorEntity(CoordinatorEntity, SensorEntity): """Base class for Aussie Broadband metric sensors.""" + _attr_has_entity_name = True entity_description: SensorValueEntityDescription def __init__( @@ -146,7 +147,6 @@ class AussieBroadandSensorEntity(CoordinatorEntity, SensorEntity): super().__init__(service["coordinator"]) self.entity_description = description self._attr_unique_id = f"{service[SERVICE_ID]}:{description.key}" - self._attr_name = f"{service['name']} {description.name}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, service[SERVICE_ID])}, diff --git a/tests/components/aussie_broadband/test_sensor.py b/tests/components/aussie_broadband/test_sensor.py index c99c52d5c86..2db4b79dbe9 100644 --- a/tests/components/aussie_broadband/test_sensor.py +++ b/tests/components/aussie_broadband/test_sensor.py @@ -44,11 +44,17 @@ async def test_nbn_sensor_states(hass): await setup_platform(hass, [SENSOR_DOMAIN], usage=MOCK_NBN_USAGE) - assert hass.states.get("sensor.nbn_data_used").state == "54321" - assert hass.states.get("sensor.nbn_downloaded").state == "50000" - assert hass.states.get("sensor.nbn_uploaded").state == "4321" - assert hass.states.get("sensor.nbn_billing_cycle_length").state == "28" - assert hass.states.get("sensor.nbn_billing_cycle_remaining").state == "25" + assert hass.states.get("sensor.fake_abb_nbn_service_data_used").state == "54321" + assert hass.states.get("sensor.fake_abb_nbn_service_downloaded").state == "50000" + assert hass.states.get("sensor.fake_abb_nbn_service_uploaded").state == "4321" + assert ( + hass.states.get("sensor.fake_abb_nbn_service_billing_cycle_length").state + == "28" + ) + assert ( + hass.states.get("sensor.fake_abb_nbn_service_billing_cycle_remaining").state + == "25" + ) async def test_phone_sensor_states(hass): @@ -56,12 +62,18 @@ async def test_phone_sensor_states(hass): await setup_platform(hass, [SENSOR_DOMAIN], usage=MOCK_MOBILE_USAGE) - assert hass.states.get("sensor.mobile_national_calls").state == "1" - assert hass.states.get("sensor.mobile_mobile_calls").state == "2" - assert hass.states.get("sensor.mobile_sms_sent").state == "4" - assert hass.states.get("sensor.mobile_data_used").state == "512" - assert hass.states.get("sensor.mobile_billing_cycle_length").state == "31" - assert hass.states.get("sensor.mobile_billing_cycle_remaining").state == "30" + assert hass.states.get("sensor.fake_abb_mobile_service_national_calls").state == "1" + assert hass.states.get("sensor.fake_abb_mobile_service_mobile_calls").state == "2" + assert hass.states.get("sensor.fake_abb_mobile_service_sms_sent").state == "4" + assert hass.states.get("sensor.fake_abb_mobile_service_data_used").state == "512" + assert ( + hass.states.get("sensor.fake_abb_mobile_service_billing_cycle_length").state + == "31" + ) + assert ( + hass.states.get("sensor.fake_abb_mobile_service_billing_cycle_remaining").state + == "30" + ) async def test_voip_sensor_states(hass): @@ -69,6 +81,10 @@ async def test_voip_sensor_states(hass): await setup_platform(hass, [SENSOR_DOMAIN], usage=MOCK_VOIP_USAGE) - assert hass.states.get("sensor.mobile_national_calls").state == "1" - assert hass.states.get("sensor.mobile_sms_sent").state == STATE_UNKNOWN - assert hass.states.get("sensor.mobile_data_used").state == STATE_UNKNOWN + assert hass.states.get("sensor.fake_abb_voip_service_national_calls").state == "1" + assert ( + hass.states.get("sensor.fake_abb_voip_service_sms_sent").state == STATE_UNKNOWN + ) + assert ( + hass.states.get("sensor.fake_abb_voip_service_data_used").state == STATE_UNKNOWN + ) From 53502eb6625eb167ca41247b42978359e375e04c Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Mon, 11 Jul 2022 00:04:54 -0400 Subject: [PATCH 2431/3516] Migrate Mazda to new entity naming style (#74939) --- homeassistant/components/mazda/__init__.py | 2 ++ .../components/mazda/binary_sensor.py | 18 ++++++--------- homeassistant/components/mazda/button.py | 23 +++++-------------- .../components/mazda/device_tracker.py | 2 +- homeassistant/components/mazda/lock.py | 3 ++- homeassistant/components/mazda/sensor.py | 22 ++++++++---------- homeassistant/components/mazda/switch.py | 2 +- tests/components/mazda/test_binary_sensor.py | 10 ++++---- tests/components/mazda/test_button.py | 18 +++++++-------- tests/components/mazda/test_device_tracker.py | 2 +- tests/components/mazda/test_sensor.py | 16 ++++++------- 11 files changed, 51 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/mazda/__init__.py b/homeassistant/components/mazda/__init__.py index f5685155587..b62725e5b1b 100644 --- a/homeassistant/components/mazda/__init__.py +++ b/homeassistant/components/mazda/__init__.py @@ -222,6 +222,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class MazdaEntity(CoordinatorEntity): """Defines a base Mazda entity.""" + _attr_has_entity_name = True + def __init__(self, client, coordinator, index): """Initialize the Mazda entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/mazda/binary_sensor.py b/homeassistant/components/mazda/binary_sensor.py index cc60d6318c7..c2727654525 100644 --- a/homeassistant/components/mazda/binary_sensor.py +++ b/homeassistant/components/mazda/binary_sensor.py @@ -22,9 +22,6 @@ from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN class MazdaBinarySensorRequiredKeysMixin: """Mixin for required keys.""" - # Suffix to be appended to the vehicle name to obtain the binary sensor name - name_suffix: str - # Function to determine the value for this binary sensor, given the coordinator data value_fn: Callable[[dict[str, Any]], bool] @@ -49,49 +46,49 @@ def _plugged_in_supported(data): BINARY_SENSOR_ENTITIES = [ MazdaBinarySensorEntityDescription( key="driver_door", - name_suffix="Driver Door", + name="Driver door", icon="mdi:car-door", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["driverDoorOpen"], ), MazdaBinarySensorEntityDescription( key="passenger_door", - name_suffix="Passenger Door", + name="Passenger door", icon="mdi:car-door", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["passengerDoorOpen"], ), MazdaBinarySensorEntityDescription( key="rear_left_door", - name_suffix="Rear Left Door", + name="Rear left door", icon="mdi:car-door", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["rearLeftDoorOpen"], ), MazdaBinarySensorEntityDescription( key="rear_right_door", - name_suffix="Rear Right Door", + name="Rear right door", icon="mdi:car-door", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["rearRightDoorOpen"], ), MazdaBinarySensorEntityDescription( key="trunk", - name_suffix="Trunk", + name="Trunk", icon="mdi:car-back", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["trunkOpen"], ), MazdaBinarySensorEntityDescription( key="hood", - name_suffix="Hood", + name="Hood", icon="mdi:car", device_class=BinarySensorDeviceClass.DOOR, value_fn=lambda data: data["status"]["doors"]["hoodOpen"], ), MazdaBinarySensorEntityDescription( key="ev_plugged_in", - name_suffix="Plugged In", + name="Plugged in", device_class=BinarySensorDeviceClass.PLUG, is_supported=_plugged_in_supported, value_fn=lambda data: data["evStatus"]["chargeInfo"]["pluggedIn"], @@ -126,7 +123,6 @@ class MazdaBinarySensorEntity(MazdaEntity, BinarySensorEntity): super().__init__(client, coordinator, index) self.entity_description = description - self._attr_name = f"{self.vehicle_name} {description.name_suffix}" self._attr_unique_id = f"{self.vin}_{description.key}" @property diff --git a/homeassistant/components/mazda/button.py b/homeassistant/components/mazda/button.py index e747cb33dc2..d9b21df5156 100644 --- a/homeassistant/components/mazda/button.py +++ b/homeassistant/components/mazda/button.py @@ -61,17 +61,7 @@ async def handle_refresh_vehicle_status( @dataclass -class MazdaButtonRequiredKeysMixin: - """Mixin for required keys.""" - - # Suffix to be appended to the vehicle name to obtain the button name - name_suffix: str - - -@dataclass -class MazdaButtonEntityDescription( - ButtonEntityDescription, MazdaButtonRequiredKeysMixin -): +class MazdaButtonEntityDescription(ButtonEntityDescription): """Describes a Mazda button entity.""" # Function to determine whether the vehicle supports this button, given the coordinator data @@ -85,27 +75,27 @@ class MazdaButtonEntityDescription( BUTTON_ENTITIES = [ MazdaButtonEntityDescription( key="start_engine", - name_suffix="Start Engine", + name="Start engine", icon="mdi:engine", ), MazdaButtonEntityDescription( key="stop_engine", - name_suffix="Stop Engine", + name="Stop engine", icon="mdi:engine-off", ), MazdaButtonEntityDescription( key="turn_on_hazard_lights", - name_suffix="Turn On Hazard Lights", + name="Turn on hazard lights", icon="mdi:hazard-lights", ), MazdaButtonEntityDescription( key="turn_off_hazard_lights", - name_suffix="Turn Off Hazard Lights", + name="Turn off hazard lights", icon="mdi:hazard-lights", ), MazdaButtonEntityDescription( key="refresh_vehicle_status", - name_suffix="Refresh Status", + name="Refresh status", icon="mdi:refresh", async_press=handle_refresh_vehicle_status, is_supported=lambda data: data["isElectric"], @@ -146,7 +136,6 @@ class MazdaButtonEntity(MazdaEntity, ButtonEntity): super().__init__(client, coordinator, index) self.entity_description = description - self._attr_name = f"{self.vehicle_name} {description.name_suffix}" self._attr_unique_id = f"{self.vin}_{description.key}" async def async_press(self) -> None: diff --git a/homeassistant/components/mazda/device_tracker.py b/homeassistant/components/mazda/device_tracker.py index 13266cd64d7..79a9ef90b9b 100644 --- a/homeassistant/components/mazda/device_tracker.py +++ b/homeassistant/components/mazda/device_tracker.py @@ -29,6 +29,7 @@ async def async_setup_entry( class MazdaDeviceTracker(MazdaEntity, TrackerEntity): """Class for the device tracker.""" + _attr_name = "Device tracker" _attr_icon = "mdi:car" _attr_force_update = False @@ -36,7 +37,6 @@ class MazdaDeviceTracker(MazdaEntity, TrackerEntity): """Initialize Mazda device tracker.""" super().__init__(client, coordinator, index) - self._attr_name = f"{self.vehicle_name} Device Tracker" self._attr_unique_id = self.vin @property diff --git a/homeassistant/components/mazda/lock.py b/homeassistant/components/mazda/lock.py index bcd409d2faf..1f42c5dce48 100644 --- a/homeassistant/components/mazda/lock.py +++ b/homeassistant/components/mazda/lock.py @@ -32,11 +32,12 @@ async def async_setup_entry( class MazdaLock(MazdaEntity, LockEntity): """Class for the lock.""" + _attr_name = "Lock" + def __init__(self, client, coordinator, index) -> None: """Initialize Mazda lock.""" super().__init__(client, coordinator, index) - self._attr_name = f"{self.vehicle_name} Lock" self._attr_unique_id = self.vin @property diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py index 7e8b45e0ca1..c688ac62637 100644 --- a/homeassistant/components/mazda/sensor.py +++ b/homeassistant/components/mazda/sensor.py @@ -32,9 +32,6 @@ from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN class MazdaSensorRequiredKeysMixin: """Mixin for required keys.""" - # Suffix to be appended to the vehicle name to obtain the sensor name - name_suffix: str - # Function to determine the value for this sensor, given the coordinator data and the configured unit system value: Callable[[dict[str, Any], UnitSystem], StateType] @@ -159,7 +156,7 @@ def _ev_remaining_range_value(data, unit_system): SENSOR_ENTITIES = [ MazdaSensorEntityDescription( key="fuel_remaining_percentage", - name_suffix="Fuel Remaining Percentage", + name="Fuel remaining percentage", icon="mdi:gas-station", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -168,7 +165,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="fuel_distance_remaining", - name_suffix="Fuel Distance Remaining", + name="Fuel distance remaining", icon="mdi:gas-station", unit=_get_distance_unit, state_class=SensorStateClass.MEASUREMENT, @@ -177,7 +174,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="odometer", - name_suffix="Odometer", + name="Odometer", icon="mdi:speedometer", unit=_get_distance_unit, state_class=SensorStateClass.TOTAL_INCREASING, @@ -186,7 +183,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="front_left_tire_pressure", - name_suffix="Front Left Tire Pressure", + name="Front left tire pressure", icon="mdi:car-tire-alert", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_PSI, @@ -196,7 +193,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="front_right_tire_pressure", - name_suffix="Front Right Tire Pressure", + name="Front right tire pressure", icon="mdi:car-tire-alert", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_PSI, @@ -206,7 +203,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="rear_left_tire_pressure", - name_suffix="Rear Left Tire Pressure", + name="Rear left tire pressure", icon="mdi:car-tire-alert", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_PSI, @@ -216,7 +213,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="rear_right_tire_pressure", - name_suffix="Rear Right Tire Pressure", + name="Rear right tire pressure", icon="mdi:car-tire-alert", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_PSI, @@ -226,7 +223,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="ev_charge_level", - name_suffix="Charge Level", + name="Charge level", device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -235,7 +232,7 @@ SENSOR_ENTITIES = [ ), MazdaSensorEntityDescription( key="ev_remaining_range", - name_suffix="Remaining Range", + name="Remaining range", icon="mdi:ev-station", unit=_get_distance_unit, state_class=SensorStateClass.MEASUREMENT, @@ -276,7 +273,6 @@ class MazdaSensorEntity(MazdaEntity, SensorEntity): super().__init__(client, coordinator, index) self.entity_description = description - self._attr_name = f"{self.vehicle_name} {description.name_suffix}" self._attr_unique_id = f"{self.vin}_{description.key}" @property diff --git a/homeassistant/components/mazda/switch.py b/homeassistant/components/mazda/switch.py index 3ab3028425f..844585720d7 100644 --- a/homeassistant/components/mazda/switch.py +++ b/homeassistant/components/mazda/switch.py @@ -30,6 +30,7 @@ async def async_setup_entry( class MazdaChargingSwitch(MazdaEntity, SwitchEntity): """Class for the charging switch.""" + _attr_name = "Charging" _attr_icon = "mdi:ev-station" def __init__( @@ -41,7 +42,6 @@ class MazdaChargingSwitch(MazdaEntity, SwitchEntity): """Initialize Mazda charging switch.""" super().__init__(client, coordinator, index) - self._attr_name = f"{self.vehicle_name} Charging" self._attr_unique_id = self.vin @property diff --git a/tests/components/mazda/test_binary_sensor.py b/tests/components/mazda/test_binary_sensor.py index f2b272109c9..aa7cc306708 100644 --- a/tests/components/mazda/test_binary_sensor.py +++ b/tests/components/mazda/test_binary_sensor.py @@ -16,7 +16,7 @@ async def test_binary_sensors(hass): # Driver Door state = hass.states.get("binary_sensor.my_mazda3_driver_door") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Driver Door" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Driver door" assert state.attributes.get(ATTR_ICON) == "mdi:car-door" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR assert state.state == "off" @@ -27,7 +27,7 @@ async def test_binary_sensors(hass): # Passenger Door state = hass.states.get("binary_sensor.my_mazda3_passenger_door") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Passenger Door" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Passenger door" assert state.attributes.get(ATTR_ICON) == "mdi:car-door" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR assert state.state == "on" @@ -38,7 +38,7 @@ async def test_binary_sensors(hass): # Rear Left Door state = hass.states.get("binary_sensor.my_mazda3_rear_left_door") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Left Door" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear left door" assert state.attributes.get(ATTR_ICON) == "mdi:car-door" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR assert state.state == "off" @@ -49,7 +49,7 @@ async def test_binary_sensors(hass): # Rear Right Door state = hass.states.get("binary_sensor.my_mazda3_rear_right_door") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Right Door" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear right door" assert state.attributes.get(ATTR_ICON) == "mdi:car-door" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.DOOR assert state.state == "off" @@ -90,7 +90,7 @@ async def test_electric_vehicle_binary_sensors(hass): # Plugged In state = hass.states.get("binary_sensor.my_mazda3_plugged_in") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Plugged In" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Plugged in" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.PLUG assert state.state == "on" entry = entity_registry.async_get("binary_sensor.my_mazda3_plugged_in") diff --git a/tests/components/mazda/test_button.py b/tests/components/mazda/test_button.py index 71b16434ee3..81ad175020e 100644 --- a/tests/components/mazda/test_button.py +++ b/tests/components/mazda/test_button.py @@ -22,7 +22,7 @@ async def test_button_setup_non_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_start_engine" state = hass.states.get("button.my_mazda3_start_engine") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start Engine" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start engine" assert state.attributes.get(ATTR_ICON) == "mdi:engine" entry = entity_registry.async_get("button.my_mazda3_stop_engine") @@ -30,7 +30,7 @@ async def test_button_setup_non_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_stop_engine" state = hass.states.get("button.my_mazda3_stop_engine") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop Engine" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop engine" assert state.attributes.get(ATTR_ICON) == "mdi:engine-off" entry = entity_registry.async_get("button.my_mazda3_turn_on_hazard_lights") @@ -38,7 +38,7 @@ async def test_button_setup_non_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_turn_on_hazard_lights" state = hass.states.get("button.my_mazda3_turn_on_hazard_lights") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn On Hazard Lights" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn on hazard lights" assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" entry = entity_registry.async_get("button.my_mazda3_turn_off_hazard_lights") @@ -47,7 +47,7 @@ async def test_button_setup_non_electric_vehicle(hass) -> None: state = hass.states.get("button.my_mazda3_turn_off_hazard_lights") assert state assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn Off Hazard Lights" + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn off hazard lights" ) assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" @@ -69,7 +69,7 @@ async def test_button_setup_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_start_engine" state = hass.states.get("button.my_mazda3_start_engine") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start Engine" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Start engine" assert state.attributes.get(ATTR_ICON) == "mdi:engine" entry = entity_registry.async_get("button.my_mazda3_stop_engine") @@ -77,7 +77,7 @@ async def test_button_setup_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_stop_engine" state = hass.states.get("button.my_mazda3_stop_engine") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop Engine" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Stop engine" assert state.attributes.get(ATTR_ICON) == "mdi:engine-off" entry = entity_registry.async_get("button.my_mazda3_turn_on_hazard_lights") @@ -85,7 +85,7 @@ async def test_button_setup_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_turn_on_hazard_lights" state = hass.states.get("button.my_mazda3_turn_on_hazard_lights") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn On Hazard Lights" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn on hazard lights" assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" entry = entity_registry.async_get("button.my_mazda3_turn_off_hazard_lights") @@ -94,7 +94,7 @@ async def test_button_setup_electric_vehicle(hass) -> None: state = hass.states.get("button.my_mazda3_turn_off_hazard_lights") assert state assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn Off Hazard Lights" + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Turn off hazard lights" ) assert state.attributes.get(ATTR_ICON) == "mdi:hazard-lights" @@ -103,7 +103,7 @@ async def test_button_setup_electric_vehicle(hass) -> None: assert entry.unique_id == "JM000000000000000_refresh_vehicle_status" state = hass.states.get("button.my_mazda3_refresh_status") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Refresh Status" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Refresh status" assert state.attributes.get(ATTR_ICON) == "mdi:refresh" diff --git a/tests/components/mazda/test_device_tracker.py b/tests/components/mazda/test_device_tracker.py index 4af367c1c04..42a70fff1d4 100644 --- a/tests/components/mazda/test_device_tracker.py +++ b/tests/components/mazda/test_device_tracker.py @@ -20,7 +20,7 @@ async def test_device_tracker(hass): state = hass.states.get("device_tracker.my_mazda3_device_tracker") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Device Tracker" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Device tracker" assert state.attributes.get(ATTR_ICON) == "mdi:car" assert state.attributes.get(ATTR_LATITUDE) == 1.234567 assert state.attributes.get(ATTR_LONGITUDE) == -2.345678 diff --git a/tests/components/mazda/test_sensor.py b/tests/components/mazda/test_sensor.py index f2e9039397b..763e1490e89 100644 --- a/tests/components/mazda/test_sensor.py +++ b/tests/components/mazda/test_sensor.py @@ -32,7 +32,7 @@ async def test_sensors(hass): assert state assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "My Mazda3 Fuel Remaining Percentage" + == "My Mazda3 Fuel remaining percentage" ) assert state.attributes.get(ATTR_ICON) == "mdi:gas-station" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -46,7 +46,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.my_mazda3_fuel_distance_remaining") assert state assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Fuel Distance Remaining" + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Fuel distance remaining" ) assert state.attributes.get(ATTR_ICON) == "mdi:gas-station" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS @@ -72,7 +72,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.my_mazda3_front_left_tire_pressure") assert state assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Front Left Tire Pressure" + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Front left tire pressure" ) assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE @@ -88,7 +88,7 @@ async def test_sensors(hass): assert state assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "My Mazda3 Front Right Tire Pressure" + == "My Mazda3 Front right tire pressure" ) assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE @@ -103,7 +103,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.my_mazda3_rear_left_tire_pressure") assert state assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Left Tire Pressure" + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear left tire pressure" ) assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE @@ -118,7 +118,7 @@ async def test_sensors(hass): state = hass.states.get("sensor.my_mazda3_rear_right_tire_pressure") assert state assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear Right Tire Pressure" + state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Rear right tire pressure" ) assert state.attributes.get(ATTR_ICON) == "mdi:car-tire-alert" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE @@ -167,7 +167,7 @@ async def test_electric_vehicle_sensors(hass): # Charge Level state = hass.states.get("sensor.my_mazda3_charge_level") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Charge Level" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Charge level" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.BATTERY assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -179,7 +179,7 @@ async def test_electric_vehicle_sensors(hass): # Remaining Range state = hass.states.get("sensor.my_mazda3_remaining_range") assert state - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Remaining Range" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Remaining range" assert state.attributes.get(ATTR_ICON) == "mdi:ev-station" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT From 5451ccd2b562acb67e2f97e509612362ab992e0b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 06:05:09 +0200 Subject: [PATCH 2432/3516] Update wakeonlan to 2.1.0 (#74856) --- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/wake_on_lan/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 9cd068ef409..8523231e084 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -6,7 +6,7 @@ "getmac==0.8.2", "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.5.0", - "wakeonlan==2.0.1", + "wakeonlan==2.1.0", "async-upnp-client==0.31.2" ], "ssdp": [ diff --git a/homeassistant/components/wake_on_lan/manifest.json b/homeassistant/components/wake_on_lan/manifest.json index e959f4b33f3..fc2c36eed80 100644 --- a/homeassistant/components/wake_on_lan/manifest.json +++ b/homeassistant/components/wake_on_lan/manifest.json @@ -2,7 +2,7 @@ "domain": "wake_on_lan", "name": "Wake on LAN", "documentation": "https://www.home-assistant.io/integrations/wake_on_lan", - "requirements": ["wakeonlan==2.0.1"], + "requirements": ["wakeonlan==2.1.0"], "codeowners": ["@ntilley905"], "iot_class": "local_push" } diff --git a/requirements_all.txt b/requirements_all.txt index c78dee5d185..55f70e17a2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2415,7 +2415,7 @@ vultr==0.1.2 # homeassistant.components.samsungtv # homeassistant.components.wake_on_lan -wakeonlan==2.0.1 +wakeonlan==2.1.0 # homeassistant.components.wallbox wallbox==0.4.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f82b2d1a25e..d95ea62c203 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1612,7 +1612,7 @@ vultr==0.1.2 # homeassistant.components.samsungtv # homeassistant.components.wake_on_lan -wakeonlan==2.0.1 +wakeonlan==2.1.0 # homeassistant.components.wallbox wallbox==0.4.9 From f4e61eff18a3473bd745ccc13041f5329c570c57 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 10 Jul 2022 21:24:52 -0700 Subject: [PATCH 2433/3516] Add update coordinator for google calendar (#74690) Co-authored-by: Martin Hjelmare --- homeassistant/components/google/calendar.py | 154 ++++++++++++++------ homeassistant/helpers/update_coordinator.py | 9 ++ tests/components/google/test_calendar.py | 16 +- tests/helpers/test_update_coordinator.py | 29 ++++ 4 files changed, 160 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 79e4b2da114..ca98b3da087 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -2,7 +2,6 @@ from __future__ import annotations -import copy from datetime import datetime, timedelta import logging from typing import Any @@ -21,7 +20,7 @@ from homeassistant.components.calendar import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers import ( config_validation as cv, @@ -30,7 +29,11 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util import Throttle +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) from . import ( CONF_IGNORE_AVAILABILITY, @@ -182,9 +185,17 @@ async def async_setup_entry( entity_registry.async_remove( entity_entry.entity_id, ) + coordinator = CalendarUpdateCoordinator( + hass, + calendar_service, + data[CONF_NAME], + calendar_id, + data.get(CONF_SEARCH), + ) + await coordinator.async_config_entry_first_refresh() entities.append( GoogleCalendarEntity( - calendar_service, + coordinator, calendar_id, data, generate_entity_id(ENTITY_ID_FORMAT, entity_name, hass=hass), @@ -213,14 +224,66 @@ async def async_setup_entry( ) -class GoogleCalendarEntity(CalendarEntity): - """A calendar event device.""" +class CalendarUpdateCoordinator(DataUpdateCoordinator): + """Coordinator for calendar RPC calls.""" + + def __init__( + self, + hass: HomeAssistant, + calendar_service: GoogleCalendarService, + name: str, + calendar_id: str, + search: str | None, + ) -> None: + """Create the Calendar event device.""" + super().__init__( + hass, + _LOGGER, + name=name, + update_interval=MIN_TIME_BETWEEN_UPDATES, + ) + self.calendar_service = calendar_service + self.calendar_id = calendar_id + self._search = search + + async def async_get_events( + self, start_date: datetime, end_date: datetime + ) -> list[Event]: + """Get all events in a specific time frame.""" + request = ListEventsRequest( + calendar_id=self.calendar_id, + start_time=start_date, + end_time=end_date, + search=self._search, + ) + result_items = [] + try: + result = await self.calendar_service.async_list_events(request) + async for result_page in result: + result_items.extend(result_page.items) + except ApiException as err: + self.async_set_update_error(err) + raise HomeAssistantError(str(err)) from err + return result_items + + async def _async_update_data(self) -> list[Event]: + """Fetch data from API endpoint.""" + request = ListEventsRequest(calendar_id=self.calendar_id, search=self._search) + try: + result = await self.calendar_service.async_list_events(request) + except ApiException as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + return result.items + + +class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity): + """A calendar event entity.""" _attr_has_entity_name = True def __init__( self, - calendar_service: GoogleCalendarService, + coordinator: CalendarUpdateCoordinator, calendar_id: str, data: dict[str, Any], entity_id: str, @@ -228,9 +291,9 @@ class GoogleCalendarEntity(CalendarEntity): entity_enabled: bool, ) -> None: """Create the Calendar event device.""" - self.calendar_service = calendar_service + super().__init__(coordinator) + self.coordinator = coordinator self.calendar_id = calendar_id - self._search: str | None = data.get(CONF_SEARCH) self._ignore_availability: bool = data.get(CONF_IGNORE_AVAILABILITY, False) self._event: CalendarEvent | None = None self._attr_name = data[CONF_NAME].capitalize() @@ -240,6 +303,17 @@ class GoogleCalendarEntity(CalendarEntity): self._attr_unique_id = unique_id self._attr_entity_registry_enabled_default = entity_enabled + @property + def should_poll(self) -> bool: + """Enable polling for the entity. + + The coordinator is not used by multiple entities, but instead + is used to poll the calendar API at a separate interval from the + entity state updates itself which happen more frequently (e.g. to + fire an alarm when the next event starts). + """ + return True + @property def extra_state_attributes(self) -> dict[str, bool]: """Return the device state attributes.""" @@ -265,48 +339,44 @@ class GoogleCalendarEntity(CalendarEntity): return True return event.transparency == OPAQUE + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self._apply_coordinator_update() + async def async_get_events( self, hass: HomeAssistant, start_date: datetime, end_date: datetime ) -> list[CalendarEvent]: """Get all events in a specific time frame.""" - - request = ListEventsRequest( - calendar_id=self.calendar_id, - start_time=start_date, - end_time=end_date, - search=self._search, - ) - result_items = [] - try: - result = await self.calendar_service.async_list_events(request) - async for result_page in result: - result_items.extend(result_page.items) - except ApiException as err: - raise HomeAssistantError(str(err)) from err + result_items = await self.coordinator.async_get_events(start_date, end_date) return [ _get_calendar_event(event) for event in filter(self._event_filter, result_items) ] - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self) -> None: - """Get the latest data.""" - request = ListEventsRequest(calendar_id=self.calendar_id, search=self._search) - try: - result = await self.calendar_service.async_list_events(request) - except ApiException as err: - _LOGGER.error("Unable to connect to Google: %s", err) - return + def _apply_coordinator_update(self) -> None: + """Copy state from the coordinator to this entity.""" + events = self.coordinator.data + self._event = _get_calendar_event(next(iter(events))) if events else None + if self._event: + (self._event.summary, self._offset_value) = extract_offset( + self._event.summary, self._offset + ) - # Pick the first visible event and apply offset calculations. - valid_items = filter(self._event_filter, result.items) - event = copy.deepcopy(next(valid_items, None)) - if event: - (event.summary, offset) = extract_offset(event.summary, self._offset) - self._event = _get_calendar_event(event) - self._offset_value = offset - else: - self._event = None + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._apply_coordinator_update() + super()._handle_coordinator_update() + + async def async_update(self) -> None: + """Disable update behavior. + + This relies on the coordinator callback update to write home assistant + state with the next calendar event. This update is a no-op as no new data + fetch is needed to evaluate the state to determine if the next event has + started, handled by CalendarEntity parent class. + """ def _get_calendar_event(event: Event) -> CalendarEvent: @@ -359,7 +429,7 @@ async def async_create_event(entity: GoogleCalendarEntity, call: ServiceCall) -> raise ValueError("Missing required fields to set start or end date/datetime") try: - await entity.calendar_service.async_create_event( + await entity.coordinator.calendar_service.async_create_event( entity.calendar_id, Event( summary=call.data[EVENT_SUMMARY], diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index fc619469500..30da847642b 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -282,6 +282,15 @@ class DataUpdateCoordinator(Generic[_T]): self.async_update_listeners() + @callback + def async_set_update_error(self, err: Exception) -> None: + """Manually set an error, log the message and notify listeners.""" + self.last_exception = err + if self.last_update_success: + self.logger.error("Error requesting %s data: %s", self.name, err) + self.last_update_success = False + self.async_update_listeners() + @callback def async_set_updated_data(self, data: _T) -> None: """Manually update data, notify listeners and reset refresh interval.""" diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index e49eb0c2e01..f4129eb0926 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -341,7 +341,7 @@ async def test_update_error( assert state.name == TEST_ENTITY_NAME assert state.state == "on" - # Advance time to avoid throttling + # Advance time to next data update interval now += datetime.timedelta(minutes=30) aioclient_mock.clear_requests() @@ -351,12 +351,12 @@ async def test_update_error( async_fire_time_changed(hass, now) await hass.async_block_till_done() - # No change + # Entity is marked uanvailable due to API failure state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME - assert state.state == "on" + assert state.state == "unavailable" - # Advance time beyond update/throttle point + # Advance time past next coordinator update now += datetime.timedelta(minutes=30) aioclient_mock.clear_requests() @@ -380,7 +380,7 @@ async def test_update_error( async_fire_time_changed(hass, now) await hass.async_block_till_done() - # State updated + # State updated with new API response state = hass.states.get(TEST_ENTITY) assert state.name == TEST_ENTITY_NAME assert state.state == "off" @@ -425,6 +425,10 @@ async def test_http_event_api_failure( response = await client.get(upcoming_event_url()) assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR + state = hass.states.get(TEST_ENTITY) + assert state.name == TEST_ENTITY_NAME + assert state.state == "unavailable" + @pytest.mark.freeze_time("2022-03-27 12:05:00+00:00") async def test_http_api_event( @@ -613,7 +617,7 @@ async def test_future_event_update_behavior( # Advance time until event has started now += datetime.timedelta(minutes=60) - now_utc += datetime.timedelta(minutes=30) + now_utc += datetime.timedelta(minutes=60) with patch("homeassistant.util.dt.utcnow", return_value=now_utc), patch( "homeassistant.util.dt.now", return_value=now ): diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 0d0970a4756..4e5f07a2232 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -402,3 +402,32 @@ async def test_not_schedule_refresh_if_system_option_disable_polling(hass): crd = get_crd(hass, DEFAULT_UPDATE_INTERVAL) crd.async_add_listener(lambda: None) assert crd._unsub_refresh is None + + +async def test_async_set_update_error(crd, caplog): + """Test manually setting an update failure.""" + update_callback = Mock() + crd.async_add_listener(update_callback) + + crd.async_set_update_error(aiohttp.ClientError("Client Failure #1")) + assert crd.last_update_success is False + assert "Client Failure #1" in caplog.text + update_callback.assert_called_once() + update_callback.reset_mock() + + # Additional failure does not log or change state + crd.async_set_update_error(aiohttp.ClientError("Client Failure #2")) + assert crd.last_update_success is False + assert "Client Failure #2" not in caplog.text + update_callback.assert_not_called() + update_callback.reset_mock() + + crd.async_set_updated_data(200) + assert crd.last_update_success is True + update_callback.assert_called_once() + update_callback.reset_mock() + + crd.async_set_update_error(aiohttp.ClientError("Client Failure #3")) + assert crd.last_update_success is False + assert "Client Failure #2" not in caplog.text + update_callback.assert_called_once() From a3d0719c49c9aeea33eef7d538f82db92c7c83ed Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Mon, 11 Jul 2022 03:11:37 -0400 Subject: [PATCH 2434/3516] Add binary_sensor to ElkM1 integration (#74485) * Add binary_sensor to ElkM1 integration * Update for review comments. * Fix black. * Fix pylint error. Co-authored-by: J. Nick Koston --- .coveragerc | 1 + homeassistant/components/elkm1/__init__.py | 1 + .../components/elkm1/alarm_control_panel.py | 2 +- .../components/elkm1/binary_sensor.py | 57 +++++++++++++++++++ homeassistant/components/elkm1/climate.py | 2 +- homeassistant/components/elkm1/light.py | 2 +- homeassistant/components/elkm1/scene.py | 2 +- homeassistant/components/elkm1/sensor.py | 2 +- homeassistant/components/elkm1/switch.py | 2 +- 9 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/elkm1/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 97f26d8adc5..4e252fb9c92 100644 --- a/.coveragerc +++ b/.coveragerc @@ -266,6 +266,7 @@ omit = homeassistant/components/eliqonline/sensor.py homeassistant/components/elkm1/__init__.py homeassistant/components/elkm1/alarm_control_panel.py + homeassistant/components/elkm1/binary_sensor.py homeassistant/components/elkm1/climate.py homeassistant/components/elkm1/discovery.py homeassistant/components/elkm1/light.py diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 00fcadfe57a..472d31ccb93 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -74,6 +74,7 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [ Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.LIGHT, Platform.SCENE, diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index 6b6a5b44d55..3f5163a849d 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -69,7 +69,7 @@ async def async_setup_entry( elk = elk_data["elk"] entities: list[ElkEntity] = [] create_elk_entities(elk_data, elk.areas, "area", ElkArea, entities) - async_add_entities(entities, True) + async_add_entities(entities) platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/elkm1/binary_sensor.py b/homeassistant/components/elkm1/binary_sensor.py new file mode 100644 index 00000000000..38a72796482 --- /dev/null +++ b/homeassistant/components/elkm1/binary_sensor.py @@ -0,0 +1,57 @@ +"""Support for control of ElkM1 binary sensors.""" +from __future__ import annotations + +from typing import Any + +from elkm1_lib.const import ZoneLogicalStatus, ZoneType +from elkm1_lib.elements import Element +from elkm1_lib.zones import Zone + +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 . import ElkAttachedEntity, ElkEntity +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Create the Elk-M1 sensor platform.""" + + elk_data = hass.data[DOMAIN][config_entry.entry_id] + auto_configure = elk_data["auto_configure"] + elk = elk_data["elk"] + + entities: list[ElkEntity] = [] + for element in elk.zones: + # Don't create binary sensors for zones that are analog + if element.definition in {ZoneType.TEMPERATURE, ZoneType.ANALOG_ZONE}: + continue + + if auto_configure: + if not element.configured: + continue + elif not elk_data["config"]["zone"]["included"][element.index]: + continue + + entities.append(ElkBinarySensor(element, elk, elk_data)) + + async_add_entities(entities) + + +class ElkBinarySensor(ElkAttachedEntity, BinarySensorEntity): + """Representation of ElkM1 binary sensor.""" + + _element: Zone + _attr_entity_registry_enabled_default = False + + def _element_changed(self, _: Element, changeset: Any) -> None: + # Zone in NORMAL state is OFF; any other state is ON + self._attr_is_on = bool( + self._element.logical_status != ZoneLogicalStatus.NORMAL + ) diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index 9f6dc359f6f..8bbf776c475 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -68,7 +68,7 @@ async def async_setup_entry( create_elk_entities( elk_data, elk.thermostats, "thermostat", ElkThermostat, entities ) - async_add_entities(entities, True) + async_add_entities(entities) class ElkThermostat(ElkEntity, ClimateEntity): diff --git a/homeassistant/components/elkm1/light.py b/homeassistant/components/elkm1/light.py index 9e008359e8c..3db457761aa 100644 --- a/homeassistant/components/elkm1/light.py +++ b/homeassistant/components/elkm1/light.py @@ -26,7 +26,7 @@ async def async_setup_entry( entities: list[ElkEntity] = [] elk = elk_data["elk"] create_elk_entities(elk_data, elk.lights, "plc", ElkLight, entities) - async_add_entities(entities, True) + async_add_entities(entities) class ElkLight(ElkEntity, LightEntity): diff --git a/homeassistant/components/elkm1/scene.py b/homeassistant/components/elkm1/scene.py index d8100c5bcb1..1869e5ba0f3 100644 --- a/homeassistant/components/elkm1/scene.py +++ b/homeassistant/components/elkm1/scene.py @@ -24,7 +24,7 @@ async def async_setup_entry( entities: list[ElkEntity] = [] elk = elk_data["elk"] create_elk_entities(elk_data, elk.tasks, "task", ElkTask, entities) - async_add_entities(entities, True) + async_add_entities(entities) class ElkTask(ElkAttachedEntity, Scene): diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index 57f989d5cb5..1d84af259ee 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -51,7 +51,7 @@ async def async_setup_entry( create_elk_entities(elk_data, [elk.panel], "panel", ElkPanel, entities) create_elk_entities(elk_data, elk.settings, "setting", ElkSetting, entities) create_elk_entities(elk_data, elk.zones, "zone", ElkZone, entities) - async_add_entities(entities, True) + async_add_entities(entities) platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/elkm1/switch.py b/homeassistant/components/elkm1/switch.py index 54588958e61..a17557b1507 100644 --- a/homeassistant/components/elkm1/switch.py +++ b/homeassistant/components/elkm1/switch.py @@ -24,7 +24,7 @@ async def async_setup_entry( entities: list[ElkEntity] = [] elk = elk_data["elk"] create_elk_entities(elk_data, elk.outputs, "output", ElkOutput, entities) - async_add_entities(entities, True) + async_add_entities(entities) class ElkOutput(ElkAttachedEntity, SwitchEntity): From eb92f0e16c7d462b83d9a2bde6ebd266e9c63c79 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 09:44:56 +0200 Subject: [PATCH 2435/3516] Migrate Forecast.Solar to new entity naming style (#74898) --- .../components/forecast_solar/const.py | 20 ++++++------ .../components/forecast_solar/sensor.py | 3 +- .../components/forecast_solar/test_sensor.py | 31 ++++++++++++------- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/forecast_solar/const.py b/homeassistant/components/forecast_solar/const.py index d9742cf5dfc..185025fb5ce 100644 --- a/homeassistant/components/forecast_solar/const.py +++ b/homeassistant/components/forecast_solar/const.py @@ -19,31 +19,31 @@ CONF_INVERTER_SIZE = "inverter_size" SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( ForecastSolarSensorEntityDescription( key="energy_production_today", - name="Estimated Energy Production - Today", + name="Estimated energy production - today", state=lambda estimate: estimate.energy_production_today / 1000, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), ForecastSolarSensorEntityDescription( key="energy_production_tomorrow", - name="Estimated Energy Production - Tomorrow", + name="Estimated energy production - tomorrow", state=lambda estimate: estimate.energy_production_tomorrow / 1000, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), ForecastSolarSensorEntityDescription( key="power_highest_peak_time_today", - name="Highest Power Peak Time - Today", + name="Highest power peak time - today", device_class=SensorDeviceClass.TIMESTAMP, ), ForecastSolarSensorEntityDescription( key="power_highest_peak_time_tomorrow", - name="Highest Power Peak Time - Tomorrow", + name="Highest power peak time - tomorrow", device_class=SensorDeviceClass.TIMESTAMP, ), ForecastSolarSensorEntityDescription( key="power_production_now", - name="Estimated Power Production - Now", + name="Estimated power production - now", device_class=SensorDeviceClass.POWER, state=lambda estimate: estimate.power_production_now, state_class=SensorStateClass.MEASUREMENT, @@ -54,7 +54,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=1) ), - name="Estimated Power Production - Next Hour", + name="Estimated power production - next hour", device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, native_unit_of_measurement=POWER_WATT, @@ -64,7 +64,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=12) ), - name="Estimated Power Production - Next 12 Hours", + name="Estimated power production - next 12 hours", device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, native_unit_of_measurement=POWER_WATT, @@ -74,14 +74,14 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( state=lambda estimate: estimate.power_production_at_time( estimate.now() + timedelta(hours=24) ), - name="Estimated Power Production - Next 24 Hours", + name="Estimated power production - next 24 hours", device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, native_unit_of_measurement=POWER_WATT, ), ForecastSolarSensorEntityDescription( key="energy_current_hour", - name="Estimated Energy Production - This Hour", + name="Estimated energy production - this hour", state=lambda estimate: estimate.energy_current_hour / 1000, device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, @@ -89,7 +89,7 @@ SENSORS: tuple[ForecastSolarSensorEntityDescription, ...] = ( ForecastSolarSensorEntityDescription( key="energy_next_hour", state=lambda estimate: estimate.sum_energy_production(1) / 1000, - name="Estimated Energy Production - Next Hour", + name="Estimated energy production - next hour", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, ), diff --git a/homeassistant/components/forecast_solar/sensor.py b/homeassistant/components/forecast_solar/sensor.py index 78335292a78..7bac69b1b6e 100644 --- a/homeassistant/components/forecast_solar/sensor.py +++ b/homeassistant/components/forecast_solar/sensor.py @@ -39,6 +39,7 @@ class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity): """Defines a Forecast.Solar sensor.""" entity_description: ForecastSolarSensorEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -58,7 +59,7 @@ class ForecastSolarSensorEntity(CoordinatorEntity, SensorEntity): identifiers={(DOMAIN, entry_id)}, manufacturer="Forecast.Solar", model=coordinator.data.account_type.value, - name="Solar Production Forecast", + name="Solar production forecast", configuration_url="https://forecast.solar", ) diff --git a/tests/components/forecast_solar/test_sensor.py b/tests/components/forecast_solar/test_sensor.py index a05acb5bc16..893730c722e 100644 --- a/tests/components/forecast_solar/test_sensor.py +++ b/tests/components/forecast_solar/test_sensor.py @@ -41,7 +41,7 @@ async def test_sensors( assert state.state == "100.0" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Estimated Energy Production - Today" + == "Solar production forecast Estimated energy production - today" ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -56,7 +56,7 @@ async def test_sensors( assert state.state == "200.0" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Estimated Energy Production - Tomorrow" + == "Solar production forecast Estimated energy production - tomorrow" ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -69,7 +69,10 @@ async def test_sensors( assert state assert entry.unique_id == f"{entry_id}_power_highest_peak_time_today" assert state.state == "2021-06-27T20:00:00+00:00" # Timestamp sensor is UTC - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Highest Power Peak Time - Today" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Solar production forecast Highest power peak time - today" + ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -82,7 +85,8 @@ async def test_sensors( assert entry.unique_id == f"{entry_id}_power_highest_peak_time_tomorrow" assert state.state == "2021-06-27T21:00:00+00:00" # Timestamp sensor is UTC assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "Highest Power Peak Time - Tomorrow" + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Solar production forecast Highest power peak time - tomorrow" ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP @@ -96,7 +100,8 @@ async def test_sensors( assert entry.unique_id == f"{entry_id}_power_production_now" assert state.state == "300000" assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == "Estimated Power Production - Now" + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Solar production forecast Estimated power production - now" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT @@ -111,7 +116,7 @@ async def test_sensors( assert state.state == "800.0" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Estimated Energy Production - This Hour" + == "Solar production forecast Estimated energy production - this hour" ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -126,7 +131,7 @@ async def test_sensors( assert state.state == "900.0" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Estimated Energy Production - Next Hour" + == "Solar production forecast Estimated energy production - next hour" ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -138,7 +143,7 @@ async def test_sensors( assert device_entry assert device_entry.identifiers == {(DOMAIN, f"{entry_id}")} assert device_entry.manufacturer == "Forecast.Solar" - assert device_entry.name == "Solar Production Forecast" + assert device_entry.name == "Solar production forecast" assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert device_entry.model == "public" assert not device_entry.sw_version @@ -172,17 +177,17 @@ async def test_disabled_by_default( [ ( "power_production_next_12hours", - "Estimated Power Production - Next 12 Hours", + "Estimated power production - next 12 hours", "600000", ), ( "power_production_next_24hours", - "Estimated Power Production - Next 24 Hours", + "Estimated power production - next 24 hours", "700000", ), ( "power_production_next_hour", - "Estimated Power Production - Next Hour", + "Estimated power production - next hour", "400000", ), ], @@ -219,7 +224,9 @@ async def test_enabling_disable_by_default( assert state assert entry.unique_id == f"{entry_id}_{key}" assert state.state == value - assert state.attributes.get(ATTR_FRIENDLY_NAME) == name + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == f"Solar production forecast {name}" + ) assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER From 34b8f2b28379a774851e5e919646cf952a062b2e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 09:51:01 +0200 Subject: [PATCH 2436/3516] Migrate Ambee to new entity naming style (#74877) --- homeassistant/components/ambee/const.py | 54 +++++++++--------- homeassistant/components/ambee/sensor.py | 2 + tests/components/ambee/test_sensor.py | 73 ++++++++++++++---------- 3 files changed, 73 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/ambee/const.py b/homeassistant/components/ambee/const.py index 8f8f2237654..83abb841629 100644 --- a/homeassistant/components/ambee/const.py +++ b/homeassistant/components/ambee/const.py @@ -27,7 +27,7 @@ SERVICE_AIR_QUALITY: Final = "air_quality" SERVICE_POLLEN: Final = "pollen" SERVICES: dict[str, str] = { - SERVICE_AIR_QUALITY: "Air Quality", + SERVICE_AIR_QUALITY: "Air quality", SERVICE_POLLEN: "Pollen", } @@ -35,25 +35,25 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { SERVICE_AIR_QUALITY: [ SensorEntityDescription( key="particulate_matter_2_5", - name="Particulate Matter < 2.5 μm", + name="Particulate matter < 2.5 μm", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="particulate_matter_10", - name="Particulate Matter < 10 μm", + name="Particulate matter < 10 μm", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="sulphur_dioxide", - name="Sulphur Dioxide (SO2)", + name="Sulphur dioxide (SO2)", native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="nitrogen_dioxide", - name="Nitrogen Dioxide (NO2)", + name="Nitrogen dioxide (NO2)", native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, state_class=SensorStateClass.MEASUREMENT, ), @@ -65,60 +65,60 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="carbon_monoxide", - name="Carbon Monoxide (CO)", + name="Carbon monoxide (CO)", device_class=SensorDeviceClass.CO, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="air_quality_index", - name="Air Quality Index (AQI)", + name="Air quality index (AQI)", state_class=SensorStateClass.MEASUREMENT, ), ], SERVICE_POLLEN: [ SensorEntityDescription( key="grass", - name="Grass Pollen", + name="Grass", icon="mdi:grass", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, ), SensorEntityDescription( key="tree", - name="Tree Pollen", + name="Tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, ), SensorEntityDescription( key="weed", - name="Weed Pollen", + name="Weed", icon="mdi:sprout", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, ), SensorEntityDescription( key="grass_risk", - name="Grass Pollen Risk", + name="Grass risk", icon="mdi:grass", device_class=DEVICE_CLASS_AMBEE_RISK, ), SensorEntityDescription( key="tree_risk", - name="Tree Pollen Risk", + name="Tree risk", icon="mdi:tree", device_class=DEVICE_CLASS_AMBEE_RISK, ), SensorEntityDescription( key="weed_risk", - name="Weed Pollen Risk", + name="Weed risk", icon="mdi:sprout", device_class=DEVICE_CLASS_AMBEE_RISK, ), SensorEntityDescription( key="grass_poaceae", - name="Poaceae Grass Pollen", + name="Poaceae grass", icon="mdi:grass", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -126,7 +126,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_alder", - name="Alder Tree Pollen", + name="Alder tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -134,7 +134,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_birch", - name="Birch Tree Pollen", + name="Birch tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -142,7 +142,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_cypress", - name="Cypress Tree Pollen", + name="Cypress tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -150,7 +150,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_elm", - name="Elm Tree Pollen", + name="Elm tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -158,7 +158,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_hazel", - name="Hazel Tree Pollen", + name="Hazel tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -166,7 +166,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_oak", - name="Oak Tree Pollen", + name="Oak tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -174,7 +174,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_pine", - name="Pine Tree Pollen", + name="Pine tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -182,7 +182,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_plane", - name="Plane Tree Pollen", + name="Plane tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -190,7 +190,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="tree_poplar", - name="Poplar Tree Pollen", + name="Poplar tree", icon="mdi:tree", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -198,7 +198,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="weed_chenopod", - name="Chenopod Weed Pollen", + name="Chenopod weed", icon="mdi:sprout", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -206,7 +206,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="weed_mugwort", - name="Mugwort Weed Pollen", + name="Mugwort weed", icon="mdi:sprout", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -214,7 +214,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="weed_nettle", - name="Nettle Weed Pollen", + name="Nettle weed", icon="mdi:sprout", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -222,7 +222,7 @@ SENSORS: dict[str, list[SensorEntityDescription]] = { ), SensorEntityDescription( key="weed_ragweed", - name="Ragweed Weed Pollen", + name="Ragweed weed", icon="mdi:sprout", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, diff --git a/homeassistant/components/ambee/sensor.py b/homeassistant/components/ambee/sensor.py index bf9cfe74f31..8fb6c9f2a61 100644 --- a/homeassistant/components/ambee/sensor.py +++ b/homeassistant/components/ambee/sensor.py @@ -42,6 +42,8 @@ async def async_setup_entry( class AmbeeSensorEntity(CoordinatorEntity, SensorEntity): """Defines an Ambee sensor.""" + _attr_has_entity_name = True + def __init__( self, *, diff --git a/tests/components/ambee/test_sensor.py b/tests/components/ambee/test_sensor.py index a32398139d0..d143aea8f7c 100644 --- a/tests/components/ambee/test_sensor.py +++ b/tests/components/ambee/test_sensor.py @@ -41,7 +41,10 @@ async def test_air_quality( assert state assert entry.unique_id == f"{entry_id}_air_quality_particulate_matter_2_5" assert state.state == "3.14" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 2.5 μm" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Air quality Particulate matter < 2.5 μm" + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -56,7 +59,10 @@ async def test_air_quality( assert state assert entry.unique_id == f"{entry_id}_air_quality_particulate_matter_10" assert state.state == "5.24" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Particulate Matter < 10 μm" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Air quality Particulate matter < 10 μm" + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -71,7 +77,9 @@ async def test_air_quality( assert state assert entry.unique_id == f"{entry_id}_air_quality_sulphur_dioxide" assert state.state == "0.031" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sulphur Dioxide (SO2)" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Air quality Sulphur dioxide (SO2)" + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -86,7 +94,9 @@ async def test_air_quality( assert state assert entry.unique_id == f"{entry_id}_air_quality_nitrogen_dioxide" assert state.state == "0.66" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Nitrogen Dioxide (NO2)" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Air quality Nitrogen dioxide (NO2)" + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -101,7 +111,7 @@ async def test_air_quality( assert state assert entry.unique_id == f"{entry_id}_air_quality_ozone" assert state.state == "17.067" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Ozone" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Air quality Ozone" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -117,7 +127,9 @@ async def test_air_quality( assert entry.unique_id == f"{entry_id}_air_quality_carbon_monoxide" assert state.state == "0.105" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CO - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Carbon Monoxide (CO)" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Air quality Carbon monoxide (CO)" + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -131,7 +143,10 @@ async def test_air_quality( assert state assert entry.unique_id == f"{entry_id}_air_quality_air_quality_index" assert state.state == "13" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Air Quality Index (AQI)" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Air quality Air quality index (AQI)" + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -142,7 +157,7 @@ async def test_air_quality( assert device_entry assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_air_quality")} assert device_entry.manufacturer == "Ambee" - assert device_entry.name == "Air Quality" + assert device_entry.name == "Air quality" assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert not device_entry.model assert not device_entry.sw_version @@ -163,7 +178,7 @@ async def test_pollen( assert state assert entry.unique_id == f"{entry_id}_pollen_grass" assert state.state == "190" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Grass Pollen" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Grass" assert state.attributes.get(ATTR_ICON) == "mdi:grass" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( @@ -178,7 +193,7 @@ async def test_pollen( assert state assert entry.unique_id == f"{entry_id}_pollen_tree" assert state.state == "127" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Tree Pollen" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Tree" assert state.attributes.get(ATTR_ICON) == "mdi:tree" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( @@ -193,7 +208,7 @@ async def test_pollen( assert state assert entry.unique_id == f"{entry_id}_pollen_weed" assert state.state == "95" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Weed Pollen" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Weed" assert state.attributes.get(ATTR_ICON) == "mdi:sprout" assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( @@ -209,7 +224,7 @@ async def test_pollen( assert entry.unique_id == f"{entry_id}_pollen_grass_risk" assert state.state == "high" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Grass Pollen Risk" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Grass risk" assert state.attributes.get(ATTR_ICON) == "mdi:grass" assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -221,7 +236,7 @@ async def test_pollen( assert entry.unique_id == f"{entry_id}_pollen_tree_risk" assert state.state == "moderate" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Tree Pollen Risk" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Tree risk" assert state.attributes.get(ATTR_ICON) == "mdi:tree" assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -233,7 +248,7 @@ async def test_pollen( assert entry.unique_id == f"{entry_id}_pollen_weed_risk" assert state.state == "high" assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_AMBEE_RISK - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Weed Pollen Risk" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pollen Weed risk" assert state.attributes.get(ATTR_ICON) == "mdi:sprout" assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -286,20 +301,20 @@ async def test_pollen_disabled_by_default( @pytest.mark.parametrize( "key,icon,name,value", [ - ("grass_poaceae", "mdi:grass", "Poaceae Grass Pollen", "190"), - ("tree_alder", "mdi:tree", "Alder Tree Pollen", "0"), - ("tree_birch", "mdi:tree", "Birch Tree Pollen", "35"), - ("tree_cypress", "mdi:tree", "Cypress Tree Pollen", "0"), - ("tree_elm", "mdi:tree", "Elm Tree Pollen", "0"), - ("tree_hazel", "mdi:tree", "Hazel Tree Pollen", "0"), - ("tree_oak", "mdi:tree", "Oak Tree Pollen", "55"), - ("tree_pine", "mdi:tree", "Pine Tree Pollen", "30"), - ("tree_plane", "mdi:tree", "Plane Tree Pollen", "5"), - ("tree_poplar", "mdi:tree", "Poplar Tree Pollen", "0"), - ("weed_chenopod", "mdi:sprout", "Chenopod Weed Pollen", "0"), - ("weed_mugwort", "mdi:sprout", "Mugwort Weed Pollen", "1"), - ("weed_nettle", "mdi:sprout", "Nettle Weed Pollen", "88"), - ("weed_ragweed", "mdi:sprout", "Ragweed Weed Pollen", "3"), + ("grass_poaceae", "mdi:grass", "Poaceae grass", "190"), + ("tree_alder", "mdi:tree", "Alder tree", "0"), + ("tree_birch", "mdi:tree", "Birch tree", "35"), + ("tree_cypress", "mdi:tree", "Cypress tree", "0"), + ("tree_elm", "mdi:tree", "Elm tree", "0"), + ("tree_hazel", "mdi:tree", "Hazel tree", "0"), + ("tree_oak", "mdi:tree", "Oak tree", "55"), + ("tree_pine", "mdi:tree", "Pine tree", "30"), + ("tree_plane", "mdi:tree", "Plane tree", "5"), + ("tree_poplar", "mdi:tree", "Poplar tree", "0"), + ("weed_chenopod", "mdi:sprout", "Chenopod weed", "0"), + ("weed_mugwort", "mdi:sprout", "Mugwort weed", "1"), + ("weed_nettle", "mdi:sprout", "Nettle weed", "88"), + ("weed_ragweed", "mdi:sprout", "Ragweed weed", "3"), ], ) async def test_pollen_enable_disable_by_defaults( @@ -335,7 +350,7 @@ async def test_pollen_enable_disable_by_defaults( assert state assert entry.unique_id == f"{entry_id}_pollen_{key}" assert state.state == value - assert state.attributes.get(ATTR_FRIENDLY_NAME) == name + assert state.attributes.get(ATTR_FRIENDLY_NAME) == f"Pollen {name}" assert state.attributes.get(ATTR_ICON) == icon assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert ( From 81f74d205340ed10a1b690dc7f8ecf40e9590e74 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 10:42:23 +0200 Subject: [PATCH 2437/3516] Update pre-commit to 2.20.0 (#74955) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index dbb7a76dac4..fa0dcb68a09 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,7 +12,7 @@ coverage==6.4.1 freezegun==1.2.1 mock-open==1.4.0 mypy==0.961 -pre-commit==2.19.0 +pre-commit==2.20.0 pylint==2.14.4 pipdeptree==2.2.1 pytest-aiohttp==0.3.0 From 6e1cd4c48a62458a2adce1a1b07932b99cb4e52d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Jul 2022 10:49:06 +0200 Subject: [PATCH 2438/3516] Migrate ecobee to native_* (#74043) --- homeassistant/components/ecobee/weather.py | 43 +++++++++++----------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 1c330ceb4e2..aca4dcdf2f5 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -7,20 +7,24 @@ from pyecobee.const import ECOBEE_STATE_UNKNOWN from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRESSURE_HPA, PRESSURE_INHG, TEMP_FAHRENHEIT +from homeassistant.const import ( + LENGTH_METERS, + PRESSURE_HPA, + SPEED_METERS_PER_SECOND, + TEMP_FAHRENHEIT, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util -from homeassistant.util.pressure import convert as pressure_convert from .const import ( DOMAIN, @@ -49,6 +53,11 @@ async def async_setup_entry( class EcobeeWeather(WeatherEntity): """Representation of Ecobee weather data.""" + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_FAHRENHEIT + _attr_native_visibility_unit = LENGTH_METERS + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + def __init__(self, data, name, index): """Initialize the Ecobee weather platform.""" self.data = data @@ -101,7 +110,7 @@ class EcobeeWeather(WeatherEntity): return None @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" try: return float(self.get_forecast(0, "temperature")) / 10 @@ -109,18 +118,10 @@ class EcobeeWeather(WeatherEntity): return None @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_FAHRENHEIT - - @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" try: pressure = self.get_forecast(0, "pressure") - if not self.hass.config.units.is_metric: - pressure = pressure_convert(pressure, PRESSURE_HPA, PRESSURE_INHG) - return round(pressure, 2) return round(pressure) except ValueError: return None @@ -134,15 +135,15 @@ class EcobeeWeather(WeatherEntity): return None @property - def visibility(self): + def native_visibility(self): """Return the visibility.""" try: - return int(self.get_forecast(0, "visibility")) / 1000 + return int(self.get_forecast(0, "visibility")) except ValueError: return None @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" try: return int(self.get_forecast(0, "windSpeed")) @@ -202,13 +203,13 @@ def _process_forecast(json): json["weatherSymbol"] ] if json["tempHigh"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_TEMP] = float(json["tempHigh"]) / 10 + forecast[ATTR_FORECAST_NATIVE_TEMP] = float(json["tempHigh"]) / 10 if json["tempLow"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_TEMP_LOW] = float(json["tempLow"]) / 10 + forecast[ATTR_FORECAST_NATIVE_TEMP_LOW] = float(json["tempLow"]) / 10 if json["windBearing"] != ECOBEE_STATE_UNKNOWN: forecast[ATTR_FORECAST_WIND_BEARING] = int(json["windBearing"]) if json["windSpeed"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_WIND_SPEED] = int(json["windSpeed"]) + forecast[ATTR_FORECAST_NATIVE_WIND_SPEED] = int(json["windSpeed"]) except (ValueError, IndexError, KeyError): return None From ed1545fab06708503c990ece79c3b085478fc3b1 Mon Sep 17 00:00:00 2001 From: Markus Date: Mon, 11 Jul 2022 10:49:31 +0200 Subject: [PATCH 2439/3516] Fix Pyload request content type headers (#74957) --- homeassistant/components/pyload/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index e4bbf7df3d2..81e1a02408a 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from datetime import timedelta import logging -from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol @@ -144,7 +143,7 @@ class PyLoadAPI: """Initialize pyLoad API and set headers needed later.""" self.api_url = api_url self.status = None - self.headers = {CONTENT_TYPE: CONTENT_TYPE_JSON} + self.headers = {"Content-Type": CONTENT_TYPE_JSON} if username is not None and password is not None: self.payload = {"username": username, "password": password} From f6fd7d115e6813d7980a6521919e0021f6415b94 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Jul 2022 10:58:57 +0200 Subject: [PATCH 2440/3516] Migrate homematicip_cloud to native_* (#74385) --- .../components/homematicip_cloud/weather.py | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 985754a8417..3acbae95441 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -22,7 +22,7 @@ from homeassistant.components.weather import ( WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -71,6 +71,9 @@ async def async_setup_entry( class HomematicipWeatherSensor(HomematicipGenericEntity, WeatherEntity): """Representation of the HomematicIP weather sensor plus & basic.""" + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the weather sensor.""" super().__init__(hap, device) @@ -81,22 +84,17 @@ class HomematicipWeatherSensor(HomematicipGenericEntity, WeatherEntity): return self._device.label @property - def temperature(self) -> float: + def native_temperature(self) -> float: """Return the platform temperature.""" return self._device.actualTemperature - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def humidity(self) -> int: """Return the humidity.""" return self._device.humidity @property - def wind_speed(self) -> float: + def native_wind_speed(self) -> float: """Return the wind speed.""" return self._device.windSpeed @@ -129,6 +127,9 @@ class HomematicipWeatherSensorPro(HomematicipWeatherSensor): class HomematicipHomeWeather(HomematicipGenericEntity, WeatherEntity): """Representation of the HomematicIP home weather.""" + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__(self, hap: HomematicipHAP) -> None: """Initialize the home weather.""" hap.home.modelType = "HmIP-Home-Weather" @@ -145,22 +146,17 @@ class HomematicipHomeWeather(HomematicipGenericEntity, WeatherEntity): return f"Weather {self._home.location.city}" @property - def temperature(self) -> float: + def native_temperature(self) -> float: """Return the temperature.""" return self._device.weather.temperature - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def humidity(self) -> int: """Return the humidity.""" return self._device.weather.humidity @property - def wind_speed(self) -> float: + def native_wind_speed(self) -> float: """Return the wind speed.""" return round(self._device.weather.windSpeed, 1) From 7d27dad1902336e959fab97db2e6e5bad33520d3 Mon Sep 17 00:00:00 2001 From: henryptung Date: Mon, 11 Jul 2022 02:33:28 -0700 Subject: [PATCH 2441/3516] Remove pip --prefix workaround (#74922) Remove --prefix workaround See discussion in https://github.com/home-assistant/core/issues/74405. This workaround is no longer needed on pip >= 21.0 and actively causes problems for pip >= 21.3. --- homeassistant/util/package.py | 3 --- tests/util/test_package.py | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index aad93e37542..18ab43967ec 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -89,9 +89,6 @@ def install_package( # This only works if not running in venv args += ["--user"] env["PYTHONUSERBASE"] = os.path.abspath(target) - # Workaround for incompatible prefix setting - # See http://stackoverflow.com/a/4495175 - args += ["--prefix="] _LOGGER.debug("Running pip command: args=%s", args) with Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) as process: _, stderr = process.communicate() diff --git a/tests/util/test_package.py b/tests/util/test_package.py index d6b7402a5b6..7ab087f1463 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -137,7 +137,6 @@ def test_install_target(mock_sys, mock_popen, mock_env_copy, mock_venv): "--quiet", TEST_NEW_REQ, "--user", - "--prefix=", ] assert package.install_package(TEST_NEW_REQ, False, target=target) @@ -156,7 +155,7 @@ def test_install_target_venv(mock_sys, mock_popen, mock_env_copy, mock_venv): def test_install_error(caplog, mock_sys, mock_popen, mock_venv): - """Test an install with a target.""" + """Test an install that errors out.""" caplog.set_level(logging.WARNING) mock_popen.return_value.returncode = 1 assert not package.install_package(TEST_NEW_REQ) From 0d6bf08ff69a0b75ffe797f49c201d1b7c4f3a72 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Jul 2022 11:59:06 +0200 Subject: [PATCH 2442/3516] Don't allow using deprecated features of WeatherEntity (#74394) --- homeassistant/components/weather/__init__.py | 37 ++++++++++++-------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 1fdb9173646..3e0233917d7 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -171,16 +171,16 @@ class Forecast(TypedDict, total=False): datetime: str precipitation_probability: int | None native_precipitation: float | None - precipitation: float | None + precipitation: None native_pressure: float | None - pressure: float | None + pressure: None native_temperature: float | None - temperature: float | None + temperature: None native_templow: float | None - templow: float | None + templow: None wind_bearing: float | str | None native_wind_speed: float | None - wind_speed: float | None + wind_speed: None async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -218,33 +218,33 @@ class WeatherEntity(Entity): _attr_humidity: float | None = None _attr_ozone: float | None = None _attr_precision: float - _attr_pressure: float | None = ( + _attr_pressure: None = ( None # Provide backwards compatibility. Use _attr_native_pressure ) - _attr_pressure_unit: str | None = ( + _attr_pressure_unit: None = ( None # Provide backwards compatibility. Use _attr_native_pressure_unit ) _attr_state: None = None - _attr_temperature: float | None = ( + _attr_temperature: None = ( None # Provide backwards compatibility. Use _attr_native_temperature ) - _attr_temperature_unit: str | None = ( + _attr_temperature_unit: None = ( None # Provide backwards compatibility. Use _attr_native_temperature_unit ) - _attr_visibility: float | None = ( + _attr_visibility: None = ( None # Provide backwards compatibility. Use _attr_native_visibility ) - _attr_visibility_unit: str | None = ( + _attr_visibility_unit: None = ( None # Provide backwards compatibility. Use _attr_native_visibility_unit ) - _attr_precipitation_unit: str | None = ( + _attr_precipitation_unit: None = ( None # Provide backwards compatibility. Use _attr_native_precipitation_unit ) _attr_wind_bearing: float | str | None = None - _attr_wind_speed: float | None = ( + _attr_wind_speed: None = ( None # Provide backwards compatibility. Use _attr_native_wind_speed ) - _attr_wind_speed_unit: str | None = ( + _attr_wind_speed_unit: None = ( None # Provide backwards compatibility. Use _attr_native_wind_speed_unit ) @@ -321,6 +321,7 @@ class WeatherEntity(Entity): return self.async_registry_entry_updated() + @final @property def temperature(self) -> float | None: """Return the temperature for backward compatibility. @@ -345,6 +346,7 @@ class WeatherEntity(Entity): return self._attr_native_temperature_unit + @final @property def temperature_unit(self) -> str | None: """Return the temperature unit for backward compatibility. @@ -376,6 +378,7 @@ class WeatherEntity(Entity): return self._default_temperature_unit + @final @property def pressure(self) -> float | None: """Return the pressure for backward compatibility. @@ -400,6 +403,7 @@ class WeatherEntity(Entity): return self._attr_native_pressure_unit + @final @property def pressure_unit(self) -> str | None: """Return the pressure unit for backward compatibility. @@ -436,6 +440,7 @@ class WeatherEntity(Entity): """Return the humidity in native units.""" return self._attr_humidity + @final @property def wind_speed(self) -> float | None: """Return the wind_speed for backward compatibility. @@ -460,6 +465,7 @@ class WeatherEntity(Entity): return self._attr_native_wind_speed_unit + @final @property def wind_speed_unit(self) -> str | None: """Return the wind_speed unit for backward compatibility. @@ -505,6 +511,7 @@ class WeatherEntity(Entity): """Return the ozone level.""" return self._attr_ozone + @final @property def visibility(self) -> float | None: """Return the visibility for backward compatibility. @@ -529,6 +536,7 @@ class WeatherEntity(Entity): return self._attr_native_visibility_unit + @final @property def visibility_unit(self) -> str | None: """Return the visibility unit for backward compatibility. @@ -573,6 +581,7 @@ class WeatherEntity(Entity): return self._attr_native_precipitation_unit + @final @property def precipitation_unit(self) -> str | None: """Return the precipitation unit for backward compatibility. From c80066072cc487e4aba20caf674cab0231a41105 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 12:18:28 +0200 Subject: [PATCH 2443/3516] Update psutil to 5.9.1 (#74963) --- homeassistant/components/systemmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index b9fe86cbfa8..398614815a2 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -2,7 +2,7 @@ "domain": "systemmonitor", "name": "System Monitor", "documentation": "https://www.home-assistant.io/integrations/systemmonitor", - "requirements": ["psutil==5.9.0"], + "requirements": ["psutil==5.9.1"], "codeowners": [], "iot_class": "local_push", "loggers": ["psutil"] diff --git a/requirements_all.txt b/requirements_all.txt index 55f70e17a2d..75da41fcdb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1284,7 +1284,7 @@ prometheus_client==0.7.1 proxmoxer==1.3.1 # homeassistant.components.systemmonitor -psutil==5.9.0 +psutil==5.9.1 # homeassistant.components.pulseaudio_loopback pulsectl==20.2.4 From ca93aacc5751e51363b32be0925e5859626f48bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huryn?= Date: Mon, 11 Jul 2022 12:24:37 +0200 Subject: [PATCH 2444/3516] Add blebox tvLiftBox support (#74395) * Added tvLiftBox support. * Changes after @epenet code review. * After @epenet code review, dictionaries moved to relevant modules. * Import path changed to full path. * Removed redundant code in BLEBOX_TO__DEVICE_CLASSES for switch and button platforms. * Post isort on covers. * Added tests, required version bump. As property was added inside dependency. --- homeassistant/components/blebox/__init__.py | 7 +- homeassistant/components/blebox/button.py | 48 +++++++++++++ homeassistant/components/blebox/const.py | 14 ---- homeassistant/components/blebox/cover.py | 11 ++- homeassistant/components/blebox/manifest.json | 2 +- homeassistant/components/blebox/sensor.py | 10 ++- homeassistant/components/blebox/switch.py | 5 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/blebox/test_button.py | 68 +++++++++++++++++++ 10 files changed, 141 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/blebox/button.py create mode 100644 tests/components/blebox/test_button.py diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index 383e5a95f4e..ff907d728b7 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -17,12 +17,13 @@ from .const import DEFAULT_SETUP_TIMEOUT, DOMAIN, PRODUCT _LOGGER = logging.getLogger(__name__) PLATFORMS = [ + Platform.AIR_QUALITY, + Platform.BUTTON, + Platform.CLIMATE, Platform.COVER, + Platform.LIGHT, Platform.SENSOR, Platform.SWITCH, - Platform.AIR_QUALITY, - Platform.LIGHT, - Platform.CLIMATE, ] PARALLEL_UPDATES = 0 diff --git a/homeassistant/components/blebox/button.py b/homeassistant/components/blebox/button.py new file mode 100644 index 00000000000..01de5fa56b7 --- /dev/null +++ b/homeassistant/components/blebox/button.py @@ -0,0 +1,48 @@ +"""BleBox button entities implementation.""" +from __future__ import annotations + +from homeassistant.components.button import ButtonDeviceClass, ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import BleBoxEntity, create_blebox_entities + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a BleBox button entry.""" + create_blebox_entities( + hass, config_entry, async_add_entities, BleBoxButtonEntity, "buttons" + ) + + +class BleBoxButtonEntity(BleBoxEntity, ButtonEntity): + """Representation of BleBox buttons.""" + + def __init__(self, feature): + """Initialize a BleBox button feature.""" + super().__init__(feature) + self._attr_device_class = ButtonDeviceClass.UPDATE + self._attr_icon = self.get_icon() + + def get_icon(self): + """Return icon for endpoint.""" + if "up" in self._feature.query_string: + return "mdi:arrow-up-circle" + if "down" in self._feature.query_string: + return "mdi:arrow-down-circle" + if "fav" in self._feature.query_string: + return "mdi:heart-circle" + if "open" in self._feature.query_string: + return "mdi:arrow-up-circle" + if "close" in self._feature.query_string: + return "mdi:arrow-down-circle" + return "" + + async def async_press(self) -> None: + """Handle the button press.""" + await self._feature.set() diff --git a/homeassistant/components/blebox/const.py b/homeassistant/components/blebox/const.py index 013a6501068..533c33f37bb 100644 --- a/homeassistant/components/blebox/const.py +++ b/homeassistant/components/blebox/const.py @@ -1,9 +1,5 @@ """Constants for the BleBox devices integration.""" -from homeassistant.components.cover import CoverDeviceClass -from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.components.switch import SwitchDeviceClass -from homeassistant.const import TEMP_CELSIUS DOMAIN = "blebox" PRODUCT = "product" @@ -16,16 +12,6 @@ CANNOT_CONNECT = "cannot_connect" UNSUPPORTED_VERSION = "unsupported_version" UNKNOWN = "unknown" -BLEBOX_TO_HASS_DEVICE_CLASSES = { - "shutter": CoverDeviceClass.SHUTTER, - "gatebox": CoverDeviceClass.DOOR, - "gate": CoverDeviceClass.GATE, - "relay": SwitchDeviceClass.SWITCH, - "temperature": SensorDeviceClass.TEMPERATURE, -} - - -BLEBOX_TO_UNIT_MAP = {"celsius": TEMP_CELSIUS} DEFAULT_HOST = "192.168.0.2" DEFAULT_PORT = 80 diff --git a/homeassistant/components/blebox/cover.py b/homeassistant/components/blebox/cover.py index 8763ec34d34..882356f1a77 100644 --- a/homeassistant/components/blebox/cover.py +++ b/homeassistant/components/blebox/cover.py @@ -5,6 +5,7 @@ from typing import Any from homeassistant.components.cover import ( ATTR_POSITION, + CoverDeviceClass, CoverEntity, CoverEntityFeature, ) @@ -14,7 +15,13 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BleBoxEntity, create_blebox_entities -from .const import BLEBOX_TO_HASS_DEVICE_CLASSES + +BLEBOX_TO_COVER_DEVICE_CLASSES = { + "gate": CoverDeviceClass.GATE, + "gatebox": CoverDeviceClass.DOOR, + "shutter": CoverDeviceClass.SHUTTER, +} + BLEBOX_TO_HASS_COVER_STATES = { None: None, @@ -49,7 +56,7 @@ class BleBoxCoverEntity(BleBoxEntity, CoverEntity): def __init__(self, feature): """Initialize a BleBox cover feature.""" super().__init__(feature) - self._attr_device_class = BLEBOX_TO_HASS_DEVICE_CLASSES[feature.device_class] + self._attr_device_class = BLEBOX_TO_COVER_DEVICE_CLASSES[feature.device_class] position = CoverEntityFeature.SET_POSITION if feature.is_slider else 0 stop = CoverEntityFeature.STOP if feature.has_stop else 0 self._attr_supported_features = ( diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 08554695316..49d44db8f01 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -3,7 +3,7 @@ "name": "BleBox devices", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blebox", - "requirements": ["blebox_uniapi==2.0.1"], + "requirements": ["blebox_uniapi==2.0.2"], "codeowners": ["@bbx-a", "@riokuu"], "iot_class": "local_polling", "loggers": ["blebox_uniapi"] diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index 2c317acada8..663af970e3e 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -1,11 +1,15 @@ """BleBox sensor entities.""" -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BleBoxEntity, create_blebox_entities -from .const import BLEBOX_TO_HASS_DEVICE_CLASSES, BLEBOX_TO_UNIT_MAP + +BLEBOX_TO_UNIT_MAP = {"celsius": TEMP_CELSIUS} + +BLEBOX_TO_SENSOR_DEVICE_CLASS = {"temperature": SensorDeviceClass.TEMPERATURE} async def async_setup_entry( @@ -27,7 +31,7 @@ class BleBoxSensorEntity(BleBoxEntity, SensorEntity): """Initialize a BleBox sensor feature.""" super().__init__(feature) self._attr_native_unit_of_measurement = BLEBOX_TO_UNIT_MAP[feature.unit] - self._attr_device_class = BLEBOX_TO_HASS_DEVICE_CLASSES[feature.device_class] + self._attr_device_class = BLEBOX_TO_SENSOR_DEVICE_CLASS[feature.device_class] @property def native_value(self): diff --git a/homeassistant/components/blebox/switch.py b/homeassistant/components/blebox/switch.py index 50eba1d2c4a..f9c866244c7 100644 --- a/homeassistant/components/blebox/switch.py +++ b/homeassistant/components/blebox/switch.py @@ -1,13 +1,12 @@ """BleBox switch implementation.""" from datetime import timedelta -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BleBoxEntity, create_blebox_entities -from .const import BLEBOX_TO_HASS_DEVICE_CLASSES SCAN_INTERVAL = timedelta(seconds=5) @@ -29,7 +28,7 @@ class BleBoxSwitchEntity(BleBoxEntity, SwitchEntity): def __init__(self, feature): """Initialize a BleBox switch feature.""" super().__init__(feature) - self._attr_device_class = BLEBOX_TO_HASS_DEVICE_CLASSES[feature.device_class] + self._attr_device_class = SwitchDeviceClass.SWITCH @property def is_on(self): diff --git a/requirements_all.txt b/requirements_all.txt index 75da41fcdb9..7a9c482ea84 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -405,7 +405,7 @@ bizkaibus==0.1.1 bleak==0.14.3 # homeassistant.components.blebox -blebox_uniapi==2.0.1 +blebox_uniapi==2.0.2 # homeassistant.components.blink blinkpy==0.19.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d95ea62c203..5b7077c8ebd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -320,7 +320,7 @@ bimmer_connected==0.9.6 bleak==0.14.3 # homeassistant.components.blebox -blebox_uniapi==2.0.1 +blebox_uniapi==2.0.2 # homeassistant.components.blink blinkpy==0.19.0 diff --git a/tests/components/blebox/test_button.py b/tests/components/blebox/test_button.py new file mode 100644 index 00000000000..22e1b8cb734 --- /dev/null +++ b/tests/components/blebox/test_button.py @@ -0,0 +1,68 @@ +"""Blebox button entities tests.""" +import logging +from unittest.mock import PropertyMock + +import blebox_uniapi +import pytest + +from homeassistant.components.button import ButtonDeviceClass +from homeassistant.const import ATTR_ICON + +from .conftest import async_setup_entity, mock_feature + +query_icon_matching = [ + ("up", "mdi:arrow-up-circle"), + ("down", "mdi:arrow-down-circle"), + ("fav", "mdi:heart-circle"), + ("open", "mdi:arrow-up-circle"), + ("close", "mdi:arrow-down-circle"), +] + + +@pytest.fixture(name="tvliftbox") +def tv_lift_box_fixture(caplog): + """Return simple button entity mock.""" + caplog.set_level(logging.ERROR) + + feature = mock_feature( + "buttons", + blebox_uniapi.button.Button, + unique_id="BleBox-tvLiftBox-4a3fdaad90aa-open_or_stop", + full_name="tvLiftBox-open_or_stop", + control_type=blebox_uniapi.button.ControlType.OPEN, + ) + + product = feature.product + type(product).name = PropertyMock(return_value="My tvLiftBox") + type(product).model = PropertyMock(return_value="tvLiftBox") + type(product)._query_string = PropertyMock(return_value="open_or_stop") + + return (feature, "button.tvliftbox_open_or_stop") + + +async def test_tvliftbox_init(tvliftbox, hass, config, caplog): + """Test tvLiftBox initialisation.""" + caplog.set_level(logging.ERROR) + + _, entity_id = tvliftbox + entry = await async_setup_entity(hass, config, entity_id) + state = hass.states.get(entity_id) + + assert entry.unique_id == "BleBox-tvLiftBox-4a3fdaad90aa-open_or_stop" + + assert state.attributes["device_class"] == ButtonDeviceClass.UPDATE + + assert state.name == "tvLiftBox-open_or_stop" + + +@pytest.mark.parametrize("input", query_icon_matching) +async def test_get_icon(input, tvliftbox, hass, config, caplog): + """Test if proper icon is returned.""" + caplog.set_level(logging.ERROR) + + feature_mock, entity_id = tvliftbox + feature_mock.query_string = input[0] + _ = await async_setup_entity(hass, config, entity_id) + state = hass.states.get(entity_id) + + assert state.attributes[ATTR_ICON] == input[1] From ba3f287c01571284319207d8aa65fe9e490f09ee Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 12:39:07 +0200 Subject: [PATCH 2445/3516] Update sentry-sdk to 1.7.0 (#74967) --- 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 3f01b7bc5f0..44bcd8025bb 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.6.0"], + "requirements": ["sentry-sdk==1.7.0"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 7a9c482ea84..08d9d6542ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2153,7 +2153,7 @@ sendgrid==6.8.2 sense_energy==0.10.4 # homeassistant.components.sentry -sentry-sdk==1.6.0 +sentry-sdk==1.7.0 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b7077c8ebd..a09e50bdf1e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1434,7 +1434,7 @@ securetar==2022.2.0 sense_energy==0.10.4 # homeassistant.components.sentry -sentry-sdk==1.6.0 +sentry-sdk==1.7.0 # homeassistant.components.sharkiq sharkiq==0.0.1 From d244d06711ebb647cd2bda97db2d4c2a0fc8d286 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 13:21:37 +0200 Subject: [PATCH 2446/3516] Update PyTurboJPEG to 1.6.7 (#74965) --- homeassistant/components/camera/manifest.json | 2 +- homeassistant/components/stream/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index aa4ca61e1d5..b1ab479f3a5 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -3,7 +3,7 @@ "name": "Camera", "documentation": "https://www.home-assistant.io/integrations/camera", "dependencies": ["http"], - "requirements": ["PyTurboJPEG==1.6.6"], + "requirements": ["PyTurboJPEG==1.6.7"], "after_dependencies": ["media_player"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index e9411e53224..6c090b93ac2 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -2,7 +2,7 @@ "domain": "stream", "name": "Stream", "documentation": "https://www.home-assistant.io/integrations/stream", - "requirements": ["PyTurboJPEG==1.6.6", "ha-av==10.0.0b4"], + "requirements": ["PyTurboJPEG==1.6.7", "ha-av==10.0.0b4"], "dependencies": ["http"], "codeowners": ["@hunterjm", "@uvjustin", "@allenporter"], "quality_scale": "internal", diff --git a/requirements_all.txt b/requirements_all.txt index 08d9d6542ca..74d53ca2c7b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -44,7 +44,7 @@ PyTransportNSW==0.1.1 # homeassistant.components.camera # homeassistant.components.stream -PyTurboJPEG==1.6.6 +PyTurboJPEG==1.6.7 # homeassistant.components.vicare PyViCare==2.16.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a09e50bdf1e..6069c63dcf2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -40,7 +40,7 @@ PyTransportNSW==0.1.1 # homeassistant.components.camera # homeassistant.components.stream -PyTurboJPEG==1.6.6 +PyTurboJPEG==1.6.7 # homeassistant.components.vicare PyViCare==2.16.2 From ab9950621b54ddd0d5e955a57db4724a5b222760 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 13:40:54 +0200 Subject: [PATCH 2447/3516] Remove toon from mypy ignore list (#74968) --- homeassistant/components/toon/__init__.py | 6 +++++- homeassistant/components/toon/config_flow.py | 4 ++-- homeassistant/components/toon/models.py | 22 ++++++++++---------- mypy.ini | 9 -------- script/hassfest/mypy_config.py | 3 --- 5 files changed, 18 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 6af3c0e066b..59174cff260 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -102,7 +102,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={ - (DOMAIN, coordinator.data.agreement.agreement_id, "meter_adapter") + ( + DOMAIN, + coordinator.data.agreement.agreement_id, + "meter_adapter", + ) # type: ignore[arg-type] }, manufacturer="Eneco", name="Meter Adapter", diff --git a/homeassistant/components/toon/config_flow.py b/homeassistant/components/toon/config_flow.py index 9afba2bc2ae..e86d951069c 100644 --- a/homeassistant/components/toon/config_flow.py +++ b/homeassistant/components/toon/config_flow.py @@ -20,8 +20,8 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): DOMAIN = DOMAIN VERSION = 2 - agreements: list[Agreement] | None = None - data: dict[str, Any] | None = None + agreements: list[Agreement] + data: dict[str, Any] @property def logger(self) -> logging.Logger: diff --git a/homeassistant/components/toon/models.py b/homeassistant/components/toon/models.py index e39faa1efc6..301e3c06233 100644 --- a/homeassistant/components/toon/models.py +++ b/homeassistant/components/toon/models.py @@ -39,8 +39,8 @@ class ToonElectricityMeterDeviceEntity(ToonEntity): agreement_id = self.coordinator.data.agreement.agreement_id return DeviceInfo( name="Electricity Meter", - identifiers={(DOMAIN, agreement_id, "electricity")}, - via_device=(DOMAIN, agreement_id, "meter_adapter"), + identifiers={(DOMAIN, agreement_id, "electricity")}, # type: ignore[arg-type] + via_device=(DOMAIN, agreement_id, "meter_adapter"), # type: ignore[typeddict-item] ) @@ -53,8 +53,8 @@ class ToonGasMeterDeviceEntity(ToonEntity): agreement_id = self.coordinator.data.agreement.agreement_id return DeviceInfo( name="Gas Meter", - identifiers={(DOMAIN, agreement_id, "gas")}, - via_device=(DOMAIN, agreement_id, "electricity"), + identifiers={(DOMAIN, agreement_id, "gas")}, # type: ignore[arg-type] + via_device=(DOMAIN, agreement_id, "electricity"), # type: ignore[typeddict-item] ) @@ -67,8 +67,8 @@ class ToonWaterMeterDeviceEntity(ToonEntity): agreement_id = self.coordinator.data.agreement.agreement_id return DeviceInfo( name="Water Meter", - identifiers={(DOMAIN, agreement_id, "water")}, - via_device=(DOMAIN, agreement_id, "electricity"), + identifiers={(DOMAIN, agreement_id, "water")}, # type: ignore[arg-type] + via_device=(DOMAIN, agreement_id, "electricity"), # type: ignore[typeddict-item] ) @@ -81,8 +81,8 @@ class ToonSolarDeviceEntity(ToonEntity): agreement_id = self.coordinator.data.agreement.agreement_id return DeviceInfo( name="Solar Panels", - identifiers={(DOMAIN, agreement_id, "solar")}, - via_device=(DOMAIN, agreement_id, "meter_adapter"), + identifiers={(DOMAIN, agreement_id, "solar")}, # type: ignore[arg-type] + via_device=(DOMAIN, agreement_id, "meter_adapter"), # type: ignore[typeddict-item] ) @@ -96,7 +96,7 @@ class ToonBoilerModuleDeviceEntity(ToonEntity): return DeviceInfo( name="Boiler Module", manufacturer="Eneco", - identifiers={(DOMAIN, agreement_id, "boiler_module")}, + identifiers={(DOMAIN, agreement_id, "boiler_module")}, # type: ignore[arg-type] via_device=(DOMAIN, agreement_id), ) @@ -110,8 +110,8 @@ class ToonBoilerDeviceEntity(ToonEntity): agreement_id = self.coordinator.data.agreement.agreement_id return DeviceInfo( name="Boiler", - identifiers={(DOMAIN, agreement_id, "boiler")}, - via_device=(DOMAIN, agreement_id, "boiler_module"), + identifiers={(DOMAIN, agreement_id, "boiler")}, # type: ignore[arg-type] + via_device=(DOMAIN, agreement_id, "boiler_module"), # type: ignore[typeddict-item] ) diff --git a/mypy.ini b/mypy.ini index e6f82ba957e..04c5b6f05e2 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2801,15 +2801,6 @@ ignore_errors = true [mypy-homeassistant.components.template.sensor] ignore_errors = true -[mypy-homeassistant.components.toon] -ignore_errors = true - -[mypy-homeassistant.components.toon.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.toon.models] -ignore_errors = true - [mypy-homeassistant.components.withings] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index f86fd447722..9984eeeb405 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -68,9 +68,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.sonos.statistics", "homeassistant.components.template.number", "homeassistant.components.template.sensor", - "homeassistant.components.toon", - "homeassistant.components.toon.config_flow", - "homeassistant.components.toon.models", "homeassistant.components.withings", "homeassistant.components.withings.binary_sensor", "homeassistant.components.withings.common", From ce353460b32ce89b7587109fde5adf85bb20ecaa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 14:27:54 +0200 Subject: [PATCH 2448/3516] Fix Withings re-authentication flow (#74961) --- homeassistant/components/withings/common.py | 37 ++++--------------- .../components/withings/config_flow.py | 22 ++++++----- .../components/withings/strings.json | 2 +- .../components/withings/translations/en.json | 2 +- tests/components/withings/common.py | 14 +++---- tests/components/withings/test_config_flow.py | 10 ++++- tests/components/withings/test_init.py | 14 ++----- 7 files changed, 38 insertions(+), 63 deletions(-) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index ad40378eafb..90f60cd4112 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -32,7 +32,7 @@ from homeassistant.components.application_credentials import AuthImplementation from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.http import HomeAssistantView from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_WEBHOOK_ID, MASS_KILOGRAMS, @@ -57,6 +57,7 @@ from . import const from .const import Measurement _LOGGER = logging.getLogger(const.LOG_NAMESPACE) +_RETRY_COEFFICIENT = 0.5 NOT_AUTHENTICATED_ERROR = re.compile( f"^{HTTPStatus.UNAUTHORIZED},.*", re.IGNORECASE, @@ -484,7 +485,7 @@ class ConfigEntryWithingsApi(AbstractWithingsApi): ) -> None: """Initialize object.""" self._hass = hass - self._config_entry = config_entry + self.config_entry = config_entry self._implementation = implementation self.session = OAuth2Session(hass, config_entry, implementation) @@ -496,7 +497,7 @@ class ConfigEntryWithingsApi(AbstractWithingsApi): self.session.async_ensure_token_valid(), self._hass.loop ).result() - access_token = self._config_entry.data["token"]["access_token"] + access_token = self.config_entry.data["token"]["access_token"] response = requests.request( method, f"{self.URL}/{path}", @@ -651,7 +652,7 @@ class DataManager: "Failed attempt %s of %s (%s)", attempt, attempts, exception1 ) # Make each backoff pause a little bit longer - await asyncio.sleep(0.5 * attempt) + await asyncio.sleep(_RETRY_COEFFICIENT * attempt) exception = exception1 continue @@ -738,32 +739,8 @@ class DataManager: if isinstance( exception, (UnauthorizedException, AuthFailedException) ) or NOT_AUTHENTICATED_ERROR.match(str(exception)): - context = { - const.PROFILE: self._profile, - "userid": self._user_id, - "source": SOURCE_REAUTH, - } - - # Check if reauth flow already exists. - flow = next( - iter( - flow - for flow in self._hass.config_entries.flow.async_progress_by_handler( - const.DOMAIN - ) - if flow.context == context - ), - None, - ) - if flow: - return - - # Start a reauth flow. - await self._hass.config_entries.flow.async_init( - const.DOMAIN, - context=context, - ) - return + self._api.config_entry.async_start_reauth(self._hass) + return None raise exception diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index a4ac6597248..b0fa1876d92 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -8,7 +8,6 @@ from typing import Any import voluptuous as vol from withings_api.common import AuthScope -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util import slugify @@ -25,6 +24,7 @@ class WithingsFlowHandler( # Temporarily holds authorization data during the profile step. _current_data: dict[str, None | str | int] = {} + _reauth_profile: str | None = None @property def logger(self) -> logging.Logger: @@ -53,12 +53,7 @@ class WithingsFlowHandler( async def async_step_profile(self, data: dict[str, Any]) -> FlowResult: """Prompt the user to select a user profile.""" errors = {} - reauth_profile = ( - self.context.get(const.PROFILE) - if self.context.get("source") == SOURCE_REAUTH - else None - ) - profile = data.get(const.PROFILE) or reauth_profile + profile = data.get(const.PROFILE) or self._reauth_profile if profile: existing_entries = [ @@ -67,7 +62,7 @@ class WithingsFlowHandler( if slugify(config_entry.data.get(const.PROFILE)) == slugify(profile) ] - if reauth_profile or not existing_entries: + if self._reauth_profile or not existing_entries: new_data = {**self._current_data, **data, const.PROFILE: profile} self._current_data = {} return await self.async_step_finish(new_data) @@ -81,16 +76,23 @@ class WithingsFlowHandler( ) async def async_step_reauth(self, data: Mapping[str, Any]) -> FlowResult: + """Prompt user to re-authenticate.""" + self._reauth_profile = data.get(const.PROFILE) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, data: dict[str, Any] | None = None + ) -> FlowResult: """Prompt user to re-authenticate.""" if data is not None: return await self.async_step_user() - placeholders = {const.PROFILE: self.context["profile"]} + placeholders = {const.PROFILE: self._reauth_profile} self.context.update({"title_placeholders": placeholders}) return self.async_show_form( - step_id="reauth", + step_id="reauth_confirm", description_placeholders=placeholders, ) diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index 433888d9ebf..8f8a32c95e7 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -10,7 +10,7 @@ "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" }, - "reauth": { + "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", "description": "The \"{profile}\" profile needs to be re-authenticated in order to continue receiving Withings data." } diff --git a/homeassistant/components/withings/translations/en.json b/homeassistant/components/withings/translations/en.json index e8acc8c3440..ca969626510 100644 --- a/homeassistant/components/withings/translations/en.json +++ b/homeassistant/components/withings/translations/en.json @@ -24,7 +24,7 @@ "description": "Provide a unique profile name for this data. Typically this is the name of the profile you selected in the previous step.", "title": "User Profile." }, - "reauth": { + "reauth_confirm": { "description": "The \"{profile}\" profile needs to be re-authenticated in order to continue receiving Withings data.", "title": "Reauthenticate Integration" } diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index 66e7d6b7055..f3855ae96f0 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -42,6 +42,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import AUTH_CALLBACK_PATH from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker @@ -167,6 +168,10 @@ class ComponentFactory: ) api_mock: ConfigEntryWithingsApi = MagicMock(spec=ConfigEntryWithingsApi) + api_mock.config_entry = MockConfigEntry( + domain=const.DOMAIN, + data={"profile": profile_config.profile}, + ) ComponentFactory._setup_api_method( api_mock.user_get_device, profile_config.api_response_user_get_device ) @@ -301,15 +306,6 @@ def get_config_entries_for_user_id( ) -def async_get_flow_for_user_id(hass: HomeAssistant, user_id: int) -> list[dict]: - """Get a flow for a user id.""" - return [ - flow - for flow in hass.config_entries.flow.async_progress() - if flow["handler"] == const.DOMAIN and flow["context"].get("userid") == user_id - ] - - def get_data_manager_by_user_id( hass: HomeAssistant, user_id: int ) -> DataManager | None: diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index 2643ac18c24..9fcc84dbe83 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -62,11 +62,17 @@ async def test_config_reauth_profile( result = await hass.config_entries.flow.async_init( const.DOMAIN, - context={"source": config_entries.SOURCE_REAUTH, "profile": "person0"}, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + "title_placeholders": {"name": config_entry.title}, + "unique_id": config_entry.unique_id, + }, + data={"profile": "person0"}, ) assert result assert result["type"] == "form" - assert result["step_id"] == "reauth" + assert result["step_id"] == "reauth_confirm" assert result["description_placeholders"] == {const.PROFILE: "person0"} result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index db26f7328ce..83e8a622e78 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -20,12 +20,7 @@ from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.setup import async_setup_component -from .common import ( - ComponentFactory, - async_get_flow_for_user_id, - get_data_manager_by_user_id, - new_profile_config, -) +from .common import ComponentFactory, get_data_manager_by_user_id, new_profile_config from tests.common import MockConfigEntry @@ -122,6 +117,7 @@ async def test_async_setup_no_config(hass: HomeAssistant) -> None: [Exception("401, this is the message")], ], ) +@patch("homeassistant.components.withings.common._RETRY_COEFFICIENT", 0) async def test_auth_failure( hass: HomeAssistant, component_factory: ComponentFactory, @@ -138,20 +134,18 @@ async def test_auth_failure( ) await component_factory.configure_component(profile_configs=(person0,)) - assert not async_get_flow_for_user_id(hass, person0.user_id) + assert not hass.config_entries.flow.async_progress() await component_factory.setup_profile(person0.user_id) data_manager = get_data_manager_by_user_id(hass, person0.user_id) await data_manager.poll_data_update_coordinator.async_refresh() - flows = async_get_flow_for_user_id(hass, person0.user_id) + flows = hass.config_entries.flow.async_progress() assert flows assert len(flows) == 1 flow = flows[0] assert flow["handler"] == const.DOMAIN - assert flow["context"]["profile"] == person0.profile - assert flow["context"]["userid"] == person0.user_id result = await hass.config_entries.flow.async_configure( flow["flow_id"], user_input={} From 66e27945ac9a03ccb9c36bb32b0dec81f79de2e4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 14:38:08 +0200 Subject: [PATCH 2449/3516] Fix mix of aiohttp and requests in ClickSend TTS (#74985) --- homeassistant/components/clicksend_tts/notify.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/clicksend_tts/notify.py b/homeassistant/components/clicksend_tts/notify.py index 712787c34e6..8026c8e150b 100644 --- a/homeassistant/components/clicksend_tts/notify.py +++ b/homeassistant/components/clicksend_tts/notify.py @@ -3,7 +3,6 @@ from http import HTTPStatus import json import logging -from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol @@ -20,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) BASE_API_URL = "https://rest.clicksend.com/v3" -HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON} +HEADERS = {"Content-Type": CONTENT_TYPE_JSON} CONF_LANGUAGE = "language" CONF_VOICE = "voice" From 9d2c2139037800308c1a988f29be218829f41008 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Jul 2022 14:49:36 +0200 Subject: [PATCH 2450/3516] Support overriding unit of temperature number entities (#74977) --- homeassistant/components/number/__init__.py | 40 ++++- tests/components/number/test_init.py | 159 ++++++++++++++++++++ 2 files changed, 197 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index fe438ea6aea..1820e28bc4c 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -14,8 +14,13 @@ import voluptuous as vol from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_MODE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.const import ( + ATTR_MODE, + CONF_UNIT_OF_MEASUREMENT, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -69,6 +74,10 @@ UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = { NumberDeviceClass.TEMPERATURE: temperature_util.convert, } +VALID_UNITS: dict[str, tuple[str, ...]] = { + NumberDeviceClass.TEMPERATURE: temperature_util.VALID_UNITS, +} + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Number entities.""" @@ -193,6 +202,7 @@ class NumberEntity(Entity): _attr_native_value: float _attr_native_unit_of_measurement: str | None _deprecated_number_entity_reported = False + _number_option_unit_of_measurement: str | None = None def __init_subclass__(cls, **kwargs: Any) -> None: """Post initialisation processing.""" @@ -226,6 +236,13 @@ class NumberEntity(Entity): report_issue, ) + async def async_internal_added_to_hass(self) -> None: + """Call when the number entity is added to hass.""" + await super().async_internal_added_to_hass() + if not self.registry_entry: + return + self.async_registry_entry_updated() + @property def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" @@ -348,6 +365,9 @@ class NumberEntity(Entity): @final def unit_of_measurement(self) -> str | None: """Return the unit of measurement of the entity, after unit conversion.""" + if self._number_option_unit_of_measurement: + return self._number_option_unit_of_measurement + if hasattr(self, "_attr_unit_of_measurement"): return self._attr_unit_of_measurement if ( @@ -467,6 +487,22 @@ class NumberEntity(Entity): report_issue, ) + @callback + def async_registry_entry_updated(self) -> None: + """Run when the entity registry entry has been updated.""" + assert self.registry_entry + if ( + (number_options := self.registry_entry.options.get(DOMAIN)) + and (custom_unit := number_options.get(CONF_UNIT_OF_MEASUREMENT)) + and (device_class := self.device_class) in UNIT_CONVERSIONS + and self.native_unit_of_measurement in VALID_UNITS[device_class] + and custom_unit in VALID_UNITS[device_class] + ): + self._number_option_unit_of_measurement = custom_unit + return + + self._number_option_unit_of_measurement = None + @dataclasses.dataclass class NumberExtraStoredData(ExtraStoredData): diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 9921d2a639e..8d7f8a91ae8 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -22,6 +22,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant, State +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.restore_state import STORAGE_KEY as RESTORE_STATE_KEY from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM @@ -689,3 +690,161 @@ async def test_restore_number_restore_state( assert entity0.native_value == native_value assert type(entity0.native_value) == native_value_type assert entity0.native_unit_of_measurement == uom + + +@pytest.mark.parametrize( + "device_class,native_unit,custom_unit,state_unit,native_value,custom_value", + [ + # Not a supported temperature unit + ( + NumberDeviceClass.TEMPERATURE, + TEMP_CELSIUS, + "my_temperature_unit", + TEMP_CELSIUS, + 1000, + 1000, + ), + ( + NumberDeviceClass.TEMPERATURE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TEMP_FAHRENHEIT, + 37.5, + 99.5, + ), + ( + NumberDeviceClass.TEMPERATURE, + TEMP_FAHRENHEIT, + TEMP_CELSIUS, + TEMP_CELSIUS, + 100, + 38.0, + ), + ], +) +async def test_custom_unit( + hass, + enable_custom_integrations, + device_class, + native_unit, + custom_unit, + state_unit, + native_value, + custom_value, +): + """Test custom unit.""" + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get_or_create("number", "test", "very_unique") + entity_registry.async_update_entity_options( + entry.entity_id, "number", {"unit_of_measurement": custom_unit} + ) + await hass.async_block_till_done() + + platform = getattr(hass.components, "test.number") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockNumberEntity( + name="Test", + native_value=native_value, + native_unit_of_measurement=native_unit, + device_class=device_class, + unique_id="very_unique", + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component(hass, "number", {"number": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert float(state.state) == pytest.approx(float(custom_value)) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit + + +@pytest.mark.parametrize( + "native_unit, custom_unit, used_custom_unit, default_unit, native_value, custom_value, default_value", + [ + ( + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TEMP_FAHRENHEIT, + TEMP_CELSIUS, + 37.5, + 99.5, + 37.5, + ), + ( + TEMP_FAHRENHEIT, + TEMP_FAHRENHEIT, + TEMP_FAHRENHEIT, + TEMP_CELSIUS, + 100, + 100, + 38.0, + ), + # Not a supported temperature unit + (TEMP_CELSIUS, "no_unit", TEMP_CELSIUS, TEMP_CELSIUS, 1000, 1000, 1000), + ], +) +async def test_custom_unit_change( + hass, + enable_custom_integrations, + native_unit, + custom_unit, + used_custom_unit, + default_unit, + native_value, + custom_value, + default_value, +): + """Test custom unit changes are picked up.""" + entity_registry = er.async_get(hass) + platform = getattr(hass.components, "test.number") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockNumberEntity( + name="Test", + native_value=native_value, + native_unit_of_measurement=native_unit, + device_class=NumberDeviceClass.TEMPERATURE, + unique_id="very_unique", + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component(hass, "number", {"number": {"platform": "test"}}) + await hass.async_block_till_done() + + # Default unit conversion according to unit system + state = hass.states.get(entity0.entity_id) + assert float(state.state) == pytest.approx(float(default_value)) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == default_unit + + entity_registry.async_update_entity_options( + "number.test", "number", {"unit_of_measurement": custom_unit} + ) + await hass.async_block_till_done() + + # Unit conversion to the custom unit + state = hass.states.get(entity0.entity_id) + assert float(state.state) == pytest.approx(float(custom_value)) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == used_custom_unit + + entity_registry.async_update_entity_options( + "number.test", "number", {"unit_of_measurement": native_unit} + ) + await hass.async_block_till_done() + + # Unit conversion to another custom unit + state = hass.states.get(entity0.entity_id) + assert float(state.state) == pytest.approx(float(native_value)) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit + + entity_registry.async_update_entity_options("number.test", "number", None) + await hass.async_block_till_done() + + # Default unit conversion according to unit system + state = hass.states.get(entity0.entity_id) + assert float(state.state) == pytest.approx(float(default_value)) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == default_unit From 5f4713a200cc145d7d408c4f842e12a4613dc02e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 15:07:54 +0200 Subject: [PATCH 2451/3516] Remove solaredge from mypy ignore list (#74983) --- .../components/solaredge/config_flow.py | 2 +- homeassistant/components/solaredge/const.py | 1 + .../components/solaredge/coordinator.py | 24 +++++++++---------- homeassistant/components/solaredge/models.py | 13 +++++++--- homeassistant/components/solaredge/sensor.py | 19 +++++++++------ mypy.ini | 9 ------- script/hassfest/mypy_config.py | 3 --- 7 files changed, 36 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py index 58e5577ccfa..8bdf6a4b4aa 100644 --- a/homeassistant/components/solaredge/config_flow.py +++ b/homeassistant/components/solaredge/config_flow.py @@ -23,7 +23,7 @@ class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._errors = {} + self._errors: dict[str, str] = {} @callback def _async_current_site_ids(self) -> set[str]: diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py index 916a01de9e9..fd989fc8a2a 100644 --- a/homeassistant/components/solaredge/const.py +++ b/homeassistant/components/solaredge/const.py @@ -75,6 +75,7 @@ SENSOR_TYPES = [ ), SolarEdgeSensorEntityDescription( key="site_details", + json_key="status", name="Site details", entity_registry_enabled_default=False, ), diff --git a/homeassistant/components/solaredge/coordinator.py b/homeassistant/components/solaredge/coordinator.py index fe8f2f86a8e..7d3e949fc10 100644 --- a/homeassistant/components/solaredge/coordinator.py +++ b/homeassistant/components/solaredge/coordinator.py @@ -3,6 +3,7 @@ from __future__ import annotations from abc import abstractmethod from datetime import date, datetime, timedelta +from typing import Any from solaredge import Solaredge from stringcase import snakecase @@ -23,16 +24,17 @@ from .const import ( class SolarEdgeDataService: """Get and update the latest data.""" + coordinator: DataUpdateCoordinator + def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: """Initialize the data object.""" self.api = api self.site_id = site_id - self.data = {} - self.attributes = {} + self.data: dict[str, Any] = {} + self.attributes: dict[str, Any] = {} self.hass = hass - self.coordinator = None @callback def async_setup(self) -> None: @@ -105,12 +107,6 @@ class SolarEdgeOverviewDataService(SolarEdgeDataService): class SolarEdgeDetailsDataService(SolarEdgeDataService): """Get and update the latest details data.""" - def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: - """Initialize the details data service.""" - super().__init__(hass, api, site_id) - - self.data = None - @property def update_interval(self) -> timedelta: """Update interval.""" @@ -125,7 +121,7 @@ class SolarEdgeDetailsDataService(SolarEdgeDataService): except KeyError as ex: raise UpdateFailed("Missing details data, skipping update") from ex - self.data = None + self.data = {} self.attributes = {} for key, value in details.items(): @@ -143,9 +139,13 @@ class SolarEdgeDetailsDataService(SolarEdgeDataService): ]: self.attributes[key] = value elif key == "status": - self.data = value + self.data["status"] = value - LOGGER.debug("Updated SolarEdge details: %s, %s", self.data, self.attributes) + LOGGER.debug( + "Updated SolarEdge details: %s, %s", + self.data.get("status"), + self.attributes, + ) class SolarEdgeInventoryDataService(SolarEdgeDataService): diff --git a/homeassistant/components/solaredge/models.py b/homeassistant/components/solaredge/models.py index ce24d854aac..57efb88023c 100644 --- a/homeassistant/components/solaredge/models.py +++ b/homeassistant/components/solaredge/models.py @@ -7,7 +7,14 @@ from homeassistant.components.sensor import SensorEntityDescription @dataclass -class SolarEdgeSensorEntityDescription(SensorEntityDescription): - """Sensor entity description for SolarEdge.""" +class SolarEdgeSensorEntityRequiredKeyMixin: + """Sensor entity description with json_key for SolarEdge.""" - json_key: str | None = None + json_key: str + + +@dataclass +class SolarEdgeSensorEntityDescription( + SensorEntityDescription, SolarEdgeSensorEntityRequiredKeyMixin +): + """Sensor entity description for SolarEdge.""" diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index a769a043442..a27c180e0b9 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -101,7 +101,7 @@ class SolarEdgeSensorFactory: def create_sensor( self, sensor_type: SolarEdgeSensorEntityDescription - ) -> SolarEdgeSensorEntityDescription: + ) -> SolarEdgeSensorEntity: """Create and return a sensor based on the sensor_key.""" sensor_class, service = self.services[sensor_type.key] @@ -155,7 +155,7 @@ class SolarEdgeDetailsSensor(SolarEdgeSensorEntity): @property def native_value(self) -> str | None: """Return the state of the sensor.""" - return self.data_service.data + return self.data_service.data.get(self.entity_description.json_key) @property def unique_id(self) -> str | None: @@ -169,7 +169,7 @@ class SolarEdgeInventorySensor(SolarEdgeSensorEntity): """Representation of an SolarEdge Monitoring API inventory sensor.""" @property - def extra_state_attributes(self) -> dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" return self.data_service.attributes.get(self.entity_description.json_key) @@ -182,14 +182,19 @@ class SolarEdgeInventorySensor(SolarEdgeSensorEntity): class SolarEdgeEnergyDetailsSensor(SolarEdgeSensorEntity): """Representation of an SolarEdge Monitoring API power flow sensor.""" - def __init__(self, platform_name, sensor_type, data_service): + def __init__( + self, + platform_name: str, + sensor_type: SolarEdgeSensorEntityDescription, + data_service: SolarEdgeEnergyDetailsService, + ) -> None: """Initialize the power flow sensor.""" super().__init__(platform_name, sensor_type, data_service) self._attr_native_unit_of_measurement = data_service.unit @property - def extra_state_attributes(self) -> dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" return self.data_service.attributes.get(self.entity_description.json_key) @@ -208,7 +213,7 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity): self, platform_name: str, description: SolarEdgeSensorEntityDescription, - data_service: SolarEdgeDataService, + data_service: SolarEdgePowerFlowDataService, ) -> None: """Initialize the power flow sensor.""" super().__init__(platform_name, description, data_service) @@ -216,7 +221,7 @@ class SolarEdgePowerFlowSensor(SolarEdgeSensorEntity): self._attr_native_unit_of_measurement = data_service.unit @property - def extra_state_attributes(self) -> dict[str, Any]: + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes.""" return self.data_service.attributes.get(self.entity_description.json_key) diff --git a/mypy.ini b/mypy.ini index 04c5b6f05e2..4975e893a38 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2750,15 +2750,6 @@ ignore_errors = true [mypy-homeassistant.components.profiler] ignore_errors = true -[mypy-homeassistant.components.solaredge.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.solaredge.coordinator] -ignore_errors = true - -[mypy-homeassistant.components.solaredge.sensor] -ignore_errors = true - [mypy-homeassistant.components.sonos] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 9984eeeb405..d9a63aeb11f 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -51,9 +51,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.onvif.sensor", "homeassistant.components.plex.media_player", "homeassistant.components.profiler", - "homeassistant.components.solaredge.config_flow", - "homeassistant.components.solaredge.coordinator", - "homeassistant.components.solaredge.sensor", "homeassistant.components.sonos", "homeassistant.components.sonos.alarms", "homeassistant.components.sonos.binary_sensor", From 8820ce0bddad2276840b4e3b3477b9fc5bf2ec4d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 11 Jul 2022 15:59:46 +0200 Subject: [PATCH 2452/3516] Migrate NextDNS to new entity naming style (#74951) * Use new entity naming style * Remove unnecessary code * Only the first word should be capitalised --- homeassistant/components/nextdns/button.py | 9 ++-- homeassistant/components/nextdns/sensor.py | 49 +++++++++++----------- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/nextdns/button.py b/homeassistant/components/nextdns/button.py index efe742a6113..1f761e6955c 100644 --- a/homeassistant/components/nextdns/button.py +++ b/homeassistant/components/nextdns/button.py @@ -1,8 +1,6 @@ """Support for the NextDNS service.""" from __future__ import annotations -from typing import cast - from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -17,7 +15,7 @@ PARALLEL_UPDATES = 1 CLEAR_LOGS_BUTTON = ButtonEntityDescription( key="clear_logs", - name="{profile_name} Clear Logs", + name="Clear logs", entity_category=EntityCategory.CONFIG, ) @@ -39,6 +37,8 @@ async def async_setup_entry( class NextDnsButton(CoordinatorEntity[NextDnsStatusUpdateCoordinator], ButtonEntity): """Define an NextDNS button.""" + _attr_has_entity_name = True + def __init__( self, coordinator: NextDnsStatusUpdateCoordinator, @@ -48,9 +48,6 @@ class NextDnsButton(CoordinatorEntity[NextDnsStatusUpdateCoordinator], ButtonEnt super().__init__(coordinator) self._attr_device_info = coordinator.device_info self._attr_unique_id = f"{coordinator.profile_id}_{description.key}" - self._attr_name = cast(str, description.name).format( - profile_name=coordinator.profile_name - ) self.entity_description = description async def async_press(self) -> None: diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index 54d10ba66b9..174357864b3 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Generic, cast +from typing import Generic from nextdns import ( AnalyticsDnssec, @@ -61,7 +61,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", - name="{profile_name} DNS Queries", + name="DNS queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.all_queries, @@ -71,7 +71,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", - name="{profile_name} DNS Queries Blocked", + name="DNS queries blocked", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.blocked_queries, @@ -81,7 +81,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", - name="{profile_name} DNS Queries Relayed", + name="DNS queries relayed", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.relayed_queries, @@ -91,7 +91,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( coordinator_type=ATTR_STATUS, entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:dns", - name="{profile_name} DNS Queries Blocked Ratio", + name="DNS queries blocked ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.blocked_queries_ratio, @@ -102,7 +102,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:dns", - name="{profile_name} DNS-over-HTTPS Queries", + name="DNS-over-HTTPS queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doh_queries, @@ -113,7 +113,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:dns", - name="{profile_name} DNS-over-TLS Queries", + name="DNS-over-TLS queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.dot_queries, @@ -124,7 +124,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:dns", - name="{profile_name} DNS-over-QUIC Queries", + name="DNS-over-QUIC queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doq_queries, @@ -135,7 +135,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:dns", - name="{profile_name} UDP Queries", + name="UDP queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.udp_queries, @@ -146,7 +146,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, icon="mdi:dns", entity_category=EntityCategory.DIAGNOSTIC, - name="{profile_name} DNS-over-HTTPS Queries Ratio", + name="DNS-over-HTTPS queries ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doh_queries_ratio, @@ -157,7 +157,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:dns", - name="{profile_name} DNS-over-TLS Queries Ratio", + name="DNS-over-TLS queries ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.dot_queries_ratio, @@ -168,7 +168,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_registry_enabled_default=False, icon="mdi:dns", entity_category=EntityCategory.DIAGNOSTIC, - name="{profile_name} DNS-over-QUIC Queries Ratio", + name="DNS-over-QUIC queries ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doq_queries_ratio, @@ -179,7 +179,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:dns", - name="{profile_name} UDP Queries Ratio", + name="UDP queries ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.udp_queries_ratio, @@ -190,7 +190,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:lock", - name="{profile_name} Encrypted Queries", + name="Encrypted queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.encrypted_queries, @@ -201,7 +201,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:lock-open", - name="{profile_name} Unencrypted Queries", + name="Unencrypted queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.unencrypted_queries, @@ -212,7 +212,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:lock", - name="{profile_name} Encrypted Queries Ratio", + name="Encrypted queries ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.encrypted_queries_ratio, @@ -223,7 +223,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:ip", - name="{profile_name} IPv4 Queries", + name="IPv4 queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.ipv4_queries, @@ -234,7 +234,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:ip", - name="{profile_name} IPv6 Queries", + name="IPv6 queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.ipv6_queries, @@ -245,7 +245,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:ip", - name="{profile_name} IPv6 Queries Ratio", + name="IPv6 queries ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.ipv6_queries_ratio, @@ -256,7 +256,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:lock-check", - name="{profile_name} DNSSEC Validated Queries", + name="DNSSEC validated queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.validated_queries, @@ -267,7 +267,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:lock-alert", - name="{profile_name} DNSSEC Not Validated Queries", + name="DNSSEC not validated queries", native_unit_of_measurement="queries", state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.not_validated_queries, @@ -278,7 +278,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:lock-check", - name="{profile_name} DNSSEC Validated Queries Ratio", + name="DNSSEC validated queries ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.validated_queries_ratio, @@ -308,6 +308,8 @@ class NextDnsSensor( ): """Define an NextDNS sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: NextDnsUpdateCoordinator[TCoordinatorData], @@ -317,9 +319,6 @@ class NextDnsSensor( super().__init__(coordinator) self._attr_device_info = coordinator.device_info self._attr_unique_id = f"{coordinator.profile_id}_{description.key}" - self._attr_name = cast(str, description.name).format( - profile_name=coordinator.profile_name - ) self._attr_native_value = description.value(coordinator.data) self.entity_description: NextDnsSensorEntityDescription = description From c1a4dc2f229179243acd00c2231e3070e92db2af Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 11 Jul 2022 16:00:13 +0200 Subject: [PATCH 2453/3516] Add NextDNS switch platform (#74512) * Add switch platform * Use lambda to get state * Use async with timeout * Add tests * Use correct type * Use Generic for coordinator * Use TCoordinatorData * Cleanup generic * Simplify coordinator data update methods * Use new entity naming style * Remove unnecessary code * Only the first word should be capitalised * Suggested change * improve typing in tests * Improve typing intests * Update tests/components/nextdns/__init__.py * black Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/nextdns/__init__.py | 62 ++-- homeassistant/components/nextdns/const.py | 2 + homeassistant/components/nextdns/switch.py | 247 +++++++++++++++ tests/components/nextdns/__init__.py | 34 ++- tests/components/nextdns/test_switch.py | 306 +++++++++++++++++++ 5 files changed, 619 insertions(+), 32 deletions(-) create mode 100644 homeassistant/components/nextdns/switch.py create mode 100644 tests/components/nextdns/test_switch.py diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index 7e7f5ff2dd8..2f68abee847 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -17,6 +17,7 @@ from nextdns import ( ApiError, InvalidApiKeyError, NextDns, + Settings, ) from nextdns.model import NextDnsData @@ -34,10 +35,12 @@ from .const import ( ATTR_ENCRYPTION, ATTR_IP_VERSIONS, ATTR_PROTOCOLS, + ATTR_SETTINGS, ATTR_STATUS, CONF_PROFILE_ID, DOMAIN, UPDATE_INTERVAL_ANALYTICS, + UPDATE_INTERVAL_SETTINGS, ) TCoordinatorData = TypeVar("TCoordinatorData", bound=NextDnsData) @@ -68,6 +71,14 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator[TCoordinatorData]): super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) async def _async_update_data(self) -> TCoordinatorData: + """Update data via internal method.""" + try: + async with timeout(10): + return await self._async_update_data_internal() + except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: + raise UpdateFailed(err) from err + + async def _async_update_data_internal(self) -> TCoordinatorData: """Update data via library.""" raise NotImplementedError("Update method not implemented") @@ -75,71 +86,60 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator[TCoordinatorData]): class NextDnsStatusUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsStatus]): """Class to manage fetching NextDNS analytics status data from API.""" - async def _async_update_data(self) -> AnalyticsStatus: + async def _async_update_data_internal(self) -> AnalyticsStatus: """Update data via library.""" - try: - async with timeout(10): - return await self.nextdns.get_analytics_status(self.profile_id) - except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: - raise UpdateFailed(err) from err + return await self.nextdns.get_analytics_status(self.profile_id) class NextDnsDnssecUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsDnssec]): """Class to manage fetching NextDNS analytics Dnssec data from API.""" - async def _async_update_data(self) -> AnalyticsDnssec: + async def _async_update_data_internal(self) -> AnalyticsDnssec: """Update data via library.""" - try: - async with timeout(10): - return await self.nextdns.get_analytics_dnssec(self.profile_id) - except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: - raise UpdateFailed(err) from err + return await self.nextdns.get_analytics_dnssec(self.profile_id) class NextDnsEncryptionUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsEncryption]): """Class to manage fetching NextDNS analytics encryption data from API.""" - async def _async_update_data(self) -> AnalyticsEncryption: + async def _async_update_data_internal(self) -> AnalyticsEncryption: """Update data via library.""" - try: - async with timeout(10): - return await self.nextdns.get_analytics_encryption(self.profile_id) - except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: - raise UpdateFailed(err) from err + return await self.nextdns.get_analytics_encryption(self.profile_id) class NextDnsIpVersionsUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsIpVersions]): """Class to manage fetching NextDNS analytics IP versions data from API.""" - async def _async_update_data(self) -> AnalyticsIpVersions: + async def _async_update_data_internal(self) -> AnalyticsIpVersions: """Update data via library.""" - try: - async with timeout(10): - return await self.nextdns.get_analytics_ip_versions(self.profile_id) - except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: - raise UpdateFailed(err) from err + return await self.nextdns.get_analytics_ip_versions(self.profile_id) class NextDnsProtocolsUpdateCoordinator(NextDnsUpdateCoordinator[AnalyticsProtocols]): """Class to manage fetching NextDNS analytics protocols data from API.""" - async def _async_update_data(self) -> AnalyticsProtocols: + async def _async_update_data_internal(self) -> AnalyticsProtocols: """Update data via library.""" - try: - async with timeout(10): - return await self.nextdns.get_analytics_protocols(self.profile_id) - except (ApiError, ClientConnectorError, InvalidApiKeyError) as err: - raise UpdateFailed(err) from err + return await self.nextdns.get_analytics_protocols(self.profile_id) + + +class NextDnsSettingsUpdateCoordinator(NextDnsUpdateCoordinator[Settings]): + """Class to manage fetching NextDNS connection data from API.""" + + async def _async_update_data_internal(self) -> Settings: + """Update data via library.""" + return await self.nextdns.get_settings(self.profile_id) _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BUTTON, Platform.SENSOR] +PLATFORMS = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] COORDINATORS = [ (ATTR_DNSSEC, NextDnsDnssecUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), (ATTR_ENCRYPTION, NextDnsEncryptionUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), (ATTR_IP_VERSIONS, NextDnsIpVersionsUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), (ATTR_PROTOCOLS, NextDnsProtocolsUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), + (ATTR_SETTINGS, NextDnsSettingsUpdateCoordinator, UPDATE_INTERVAL_SETTINGS), (ATTR_STATUS, NextDnsStatusUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), ] diff --git a/homeassistant/components/nextdns/const.py b/homeassistant/components/nextdns/const.py index 04bab44354b..d455dd79635 100644 --- a/homeassistant/components/nextdns/const.py +++ b/homeassistant/components/nextdns/const.py @@ -5,11 +5,13 @@ ATTR_DNSSEC = "dnssec" ATTR_ENCRYPTION = "encryption" ATTR_IP_VERSIONS = "ip_versions" ATTR_PROTOCOLS = "protocols" +ATTR_SETTINGS = "settings" ATTR_STATUS = "status" CONF_PROFILE_ID = "profile_id" CONF_PROFILE_NAME = "profile_name" UPDATE_INTERVAL_ANALYTICS = timedelta(minutes=10) +UPDATE_INTERVAL_SETTINGS = timedelta(minutes=1) DOMAIN = "nextdns" diff --git a/homeassistant/components/nextdns/switch.py b/homeassistant/components/nextdns/switch.py new file mode 100644 index 00000000000..4bd3c14c20f --- /dev/null +++ b/homeassistant/components/nextdns/switch.py @@ -0,0 +1,247 @@ +"""Support for the NextDNS service.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any, Generic + +from nextdns import Settings + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import NextDnsSettingsUpdateCoordinator, TCoordinatorData +from .const import ATTR_SETTINGS, DOMAIN + +PARALLEL_UPDATES = 1 + + +@dataclass +class NextDnsSwitchRequiredKeysMixin(Generic[TCoordinatorData]): + """Class for NextDNS entity required keys.""" + + state: Callable[[TCoordinatorData], bool] + + +@dataclass +class NextDnsSwitchEntityDescription( + SwitchEntityDescription, NextDnsSwitchRequiredKeysMixin[TCoordinatorData] +): + """NextDNS switch entity description.""" + + +SWITCHES = ( + NextDnsSwitchEntityDescription[Settings]( + key="block_page", + name="Block page", + entity_category=EntityCategory.CONFIG, + icon="mdi:web-cancel", + state=lambda data: data.block_page, + ), + NextDnsSwitchEntityDescription[Settings]( + key="cache_boost", + name="Cache boost", + entity_category=EntityCategory.CONFIG, + icon="mdi:memory", + state=lambda data: data.cache_boost, + ), + NextDnsSwitchEntityDescription[Settings]( + key="cname_flattening", + name="CNAME flattening", + entity_category=EntityCategory.CONFIG, + icon="mdi:tournament", + state=lambda data: data.cname_flattening, + ), + NextDnsSwitchEntityDescription[Settings]( + key="anonymized_ecs", + name="Anonymized EDNS client subnet", + entity_category=EntityCategory.CONFIG, + icon="mdi:incognito", + state=lambda data: data.anonymized_ecs, + ), + NextDnsSwitchEntityDescription[Settings]( + key="logs", + name="Logs", + entity_category=EntityCategory.CONFIG, + icon="mdi:file-document-outline", + state=lambda data: data.logs, + ), + NextDnsSwitchEntityDescription[Settings]( + key="web3", + name="Web3", + entity_category=EntityCategory.CONFIG, + icon="mdi:web", + state=lambda data: data.web3, + ), + NextDnsSwitchEntityDescription[Settings]( + key="allow_affiliate", + name="Allow affiliate & tracking links", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.allow_affiliate, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_disguised_trackers", + name="Block disguised third-party trackers", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.block_disguised_trackers, + ), + NextDnsSwitchEntityDescription[Settings]( + key="ai_threat_detection", + name="AI-Driven threat detection", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.ai_threat_detection, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_csam", + name="Block child sexual abuse material", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.block_csam, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_ddns", + name="Block dynamic DNS hostnames", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.block_ddns, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_nrd", + name="Block newly registered domains", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.block_nrd, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_parked_domains", + name="Block parked domains", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.block_parked_domains, + ), + NextDnsSwitchEntityDescription[Settings]( + key="cryptojacking_protection", + name="Cryptojacking protection", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.cryptojacking_protection, + ), + NextDnsSwitchEntityDescription[Settings]( + key="dga_protection", + name="Domain generation algorithms protection", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.dga_protection, + ), + NextDnsSwitchEntityDescription[Settings]( + key="dns_rebinding_protection", + name="DNS rebinding protection", + entity_category=EntityCategory.CONFIG, + icon="mdi:dns", + state=lambda data: data.dns_rebinding_protection, + ), + NextDnsSwitchEntityDescription[Settings]( + key="google_safe_browsing", + name="Google safe browsing", + entity_category=EntityCategory.CONFIG, + icon="mdi:google", + state=lambda data: data.google_safe_browsing, + ), + NextDnsSwitchEntityDescription[Settings]( + key="idn_homograph_attacks_protection", + name="IDN homograph attacks protection", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.idn_homograph_attacks_protection, + ), + NextDnsSwitchEntityDescription[Settings]( + key="threat_intelligence_feeds", + name="Threat intelligence feeds", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.threat_intelligence_feeds, + ), + NextDnsSwitchEntityDescription[Settings]( + key="typosquatting_protection", + name="Typosquatting protection", + entity_category=EntityCategory.CONFIG, + icon="mdi:keyboard-outline", + state=lambda data: data.typosquatting_protection, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_bypass_methods", + name="Block bypass methods", + entity_category=EntityCategory.CONFIG, + state=lambda data: data.block_bypass_methods, + ), + NextDnsSwitchEntityDescription[Settings]( + key="safesearch", + name="Force SafeSearch", + entity_category=EntityCategory.CONFIG, + icon="mdi:search-web", + state=lambda data: data.safesearch, + ), + NextDnsSwitchEntityDescription[Settings]( + key="youtube_restricted_mode", + name="Force YouTube restricted mode", + entity_category=EntityCategory.CONFIG, + icon="mdi:youtube", + state=lambda data: data.youtube_restricted_mode, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add NextDNS entities from a config_entry.""" + coordinator: NextDnsSettingsUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + ATTR_SETTINGS + ] + + switches: list[NextDnsSwitch] = [] + for description in SWITCHES: + switches.append(NextDnsSwitch(coordinator, description)) + + async_add_entities(switches) + + +class NextDnsSwitch(CoordinatorEntity[NextDnsSettingsUpdateCoordinator], SwitchEntity): + """Define an NextDNS switch.""" + + _attr_has_entity_name = True + entity_description: NextDnsSwitchEntityDescription + + def __init__( + self, + coordinator: NextDnsSettingsUpdateCoordinator, + description: NextDnsSwitchEntityDescription, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + self._attr_device_info = coordinator.device_info + self._attr_unique_id = f"{coordinator.profile_id}_{description.key}" + self._attr_is_on = description.state(coordinator.data) + self.entity_description = description + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_is_on = self.entity_description.state(self.coordinator.data) + self.async_write_ha_state() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on switch.""" + result = await self.coordinator.nextdns.set_setting( + self.coordinator.profile_id, self.entity_description.key, True + ) + + if result: + self._attr_is_on = True + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off switch.""" + result = await self.coordinator.nextdns.set_setting( + self.coordinator.profile_id, self.entity_description.key, False + ) + + if result: + self._attr_is_on = False + self.async_write_ha_state() diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py index 24063d957d4..82c55f56bbb 100644 --- a/tests/components/nextdns/__init__.py +++ b/tests/components/nextdns/__init__.py @@ -7,10 +7,12 @@ from nextdns import ( AnalyticsIpVersions, AnalyticsProtocols, AnalyticsStatus, + Settings, ) from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -24,9 +26,36 @@ IP_VERSIONS = AnalyticsIpVersions(ipv4_queries=90, ipv6_queries=10) PROTOCOLS = AnalyticsProtocols( doh_queries=20, doq_queries=10, dot_queries=30, udp_queries=40 ) +SETTINGS = Settings( + ai_threat_detection=True, + allow_affiliate=True, + anonymized_ecs=True, + block_bypass_methods=True, + block_csam=True, + block_ddns=True, + block_disguised_trackers=True, + block_nrd=True, + block_page=False, + block_parked_domains=True, + cache_boost=True, + cname_flattening=True, + cryptojacking_protection=True, + dga_protection=True, + dns_rebinding_protection=True, + google_safe_browsing=False, + idn_homograph_attacks_protection=True, + logs=True, + safesearch=False, + threat_intelligence_feeds=True, + typosquatting_protection=True, + web3=True, + youtube_restricted_mode=False, +) -async def init_integration(hass, add_to_hass=True) -> MockConfigEntry: +async def init_integration( + hass: HomeAssistant, add_to_hass: bool = True +) -> MockConfigEntry: """Set up the NextDNS integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, @@ -55,6 +84,9 @@ async def init_integration(hass, add_to_hass=True) -> MockConfigEntry: ), patch( "homeassistant.components.nextdns.NextDns.get_analytics_protocols", return_value=PROTOCOLS, + ), patch( + "homeassistant.components.nextdns.NextDns.get_settings", + return_value=SETTINGS, ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/nextdns/test_switch.py b/tests/components/nextdns/test_switch.py new file mode 100644 index 00000000000..3e07a2633d1 --- /dev/null +++ b/tests/components/nextdns/test_switch.py @@ -0,0 +1,306 @@ +"""Test switch of NextDNS integration.""" +from datetime import timedelta +from unittest.mock import patch + +from nextdns import ApiError + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow + +from . import SETTINGS, init_integration + +from tests.common import async_fire_time_changed + + +async def test_switch(hass: HomeAssistant) -> None: + """Test states of the switches.""" + registry = er.async_get(hass) + + await init_integration(hass) + + state = hass.states.get("switch.fake_profile_ai_driven_threat_detection") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_ai_driven_threat_detection") + assert entry + assert entry.unique_id == "xyz12_ai_threat_detection" + + state = hass.states.get("switch.fake_profile_allow_affiliate_tracking_links") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_allow_affiliate_tracking_links") + assert entry + assert entry.unique_id == "xyz12_allow_affiliate" + + state = hass.states.get("switch.fake_profile_anonymized_edns_client_subnet") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_anonymized_edns_client_subnet") + assert entry + assert entry.unique_id == "xyz12_anonymized_ecs" + + state = hass.states.get("switch.fake_profile_block_bypass_methods") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_bypass_methods") + assert entry + assert entry.unique_id == "xyz12_block_bypass_methods" + + state = hass.states.get("switch.fake_profile_block_child_sexual_abuse_material") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_child_sexual_abuse_material") + assert entry + assert entry.unique_id == "xyz12_block_csam" + + state = hass.states.get("switch.fake_profile_block_disguised_third_party_trackers") + assert state + assert state.state == STATE_ON + + entry = registry.async_get( + "switch.fake_profile_block_disguised_third_party_trackers" + ) + assert entry + assert entry.unique_id == "xyz12_block_disguised_trackers" + + state = hass.states.get("switch.fake_profile_block_dynamic_dns_hostnames") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_dynamic_dns_hostnames") + assert entry + assert entry.unique_id == "xyz12_block_ddns" + + state = hass.states.get("switch.fake_profile_block_newly_registered_domains") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_newly_registered_domains") + assert entry + assert entry.unique_id == "xyz12_block_nrd" + + state = hass.states.get("switch.fake_profile_block_page") + assert state + assert state.state == STATE_OFF + + entry = registry.async_get("switch.fake_profile_block_page") + assert entry + assert entry.unique_id == "xyz12_block_page" + + state = hass.states.get("switch.fake_profile_block_parked_domains") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_parked_domains") + assert entry + assert entry.unique_id == "xyz12_block_parked_domains" + + state = hass.states.get("switch.fake_profile_cname_flattening") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_cname_flattening") + assert entry + assert entry.unique_id == "xyz12_cname_flattening" + + state = hass.states.get("switch.fake_profile_cache_boost") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_cache_boost") + assert entry + assert entry.unique_id == "xyz12_cache_boost" + + state = hass.states.get("switch.fake_profile_cryptojacking_protection") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_cryptojacking_protection") + assert entry + assert entry.unique_id == "xyz12_cryptojacking_protection" + + state = hass.states.get("switch.fake_profile_dns_rebinding_protection") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_dns_rebinding_protection") + assert entry + assert entry.unique_id == "xyz12_dns_rebinding_protection" + + state = hass.states.get( + "switch.fake_profile_domain_generation_algorithms_protection" + ) + assert state + assert state.state == STATE_ON + + entry = registry.async_get( + "switch.fake_profile_domain_generation_algorithms_protection" + ) + assert entry + assert entry.unique_id == "xyz12_dga_protection" + + state = hass.states.get("switch.fake_profile_force_safesearch") + assert state + assert state.state == STATE_OFF + + entry = registry.async_get("switch.fake_profile_force_safesearch") + assert entry + assert entry.unique_id == "xyz12_safesearch" + + state = hass.states.get("switch.fake_profile_force_youtube_restricted_mode") + assert state + assert state.state == STATE_OFF + + entry = registry.async_get("switch.fake_profile_force_youtube_restricted_mode") + assert entry + assert entry.unique_id == "xyz12_youtube_restricted_mode" + + state = hass.states.get("switch.fake_profile_google_safe_browsing") + assert state + assert state.state == STATE_OFF + + entry = registry.async_get("switch.fake_profile_google_safe_browsing") + assert entry + assert entry.unique_id == "xyz12_google_safe_browsing" + + state = hass.states.get("switch.fake_profile_idn_homograph_attacks_protection") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_idn_homograph_attacks_protection") + assert entry + assert entry.unique_id == "xyz12_idn_homograph_attacks_protection" + + state = hass.states.get("switch.fake_profile_logs") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_logs") + assert entry + assert entry.unique_id == "xyz12_logs" + + state = hass.states.get("switch.fake_profile_threat_intelligence_feeds") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_threat_intelligence_feeds") + assert entry + assert entry.unique_id == "xyz12_threat_intelligence_feeds" + + state = hass.states.get("switch.fake_profile_typosquatting_protection") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_typosquatting_protection") + assert entry + assert entry.unique_id == "xyz12_typosquatting_protection" + + state = hass.states.get("switch.fake_profile_web3") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_web3") + assert entry + assert entry.unique_id == "xyz12_web3" + + +async def test_switch_on(hass: HomeAssistant) -> None: + """Test the switch can be turned on.""" + await init_integration(hass) + + state = hass.states.get("switch.fake_profile_block_page") + assert state + assert state.state == STATE_OFF + + with patch( + "homeassistant.components.nextdns.NextDns.set_setting", return_value=True + ) as mock_switch_on: + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.fake_profile_block_page"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.fake_profile_block_page") + assert state + assert state.state == STATE_ON + + mock_switch_on.assert_called_once() + + +async def test_switch_off(hass: HomeAssistant) -> None: + """Test the switch can be turned on.""" + await init_integration(hass) + + state = hass.states.get("switch.fake_profile_web3") + assert state + assert state.state == STATE_ON + + with patch( + "homeassistant.components.nextdns.NextDns.set_setting", return_value=True + ) as mock_switch_on: + assert await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.fake_profile_web3"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.fake_profile_web3") + assert state + assert state.state == STATE_OFF + + mock_switch_on.assert_called_once() + + +async def test_availability(hass: HomeAssistant) -> None: + """Ensure that we mark the entities unavailable correctly when service causes an error.""" + await init_integration(hass) + + state = hass.states.get("switch.fake_profile_web3") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == STATE_ON + + future = utcnow() + timedelta(minutes=10) + with patch( + "homeassistant.components.nextdns.NextDns.get_settings", + side_effect=ApiError("API Error"), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("switch.fake_profile_web3") + assert state + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=20) + with patch( + "homeassistant.components.nextdns.NextDns.get_settings", + return_value=SETTINGS, + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("switch.fake_profile_web3") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == STATE_ON From ee4749b207d45b06ef1d5be7127327684389434f Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 11 Jul 2022 16:21:26 +0200 Subject: [PATCH 2454/3516] Change more properties to attributes for rfxtrx (#74880) * Move more properties to attributes * Apply suggestions from code review Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> * Convert states to attributes * Adjustments from review Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/rfxtrx/__init__.py | 30 +++++------ .../components/rfxtrx/binary_sensor.py | 36 +++++-------- homeassistant/components/rfxtrx/cover.py | 53 ++++++++----------- homeassistant/components/rfxtrx/light.py | 36 +++++-------- homeassistant/components/rfxtrx/sensor.py | 17 ++---- homeassistant/components/rfxtrx/switch.py | 21 +++----- 6 files changed, 76 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index efb9b634d62..3257a482c0c 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -440,6 +440,14 @@ def get_device_tuple_from_identifiers( return DeviceTuple(identifier2[1], identifier2[2], identifier2[3]) +def get_identifiers_from_device_tuple( + device_tuple: DeviceTuple, +) -> set[tuple[str, str]]: + """Calculate the device identifier from a device tuple.""" + # work around legacy identifier, being a multi tuple value + return {(DOMAIN, *device_tuple)} # type: ignore[arg-type] + + async def async_remove_config_entry_device( hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry ) -> bool: @@ -469,10 +477,15 @@ class RfxtrxEntity(RestoreEntity): event: rfxtrxmod.RFXtrxEvent | None = None, ) -> None: """Initialize the device.""" + self._attr_device_info = DeviceInfo( + identifiers=get_identifiers_from_device_tuple(device_id), + model=device.type_string, + name=f"{device.type_string} {device.id_string}", + ) + self._attr_unique_id = "_".join(x for x in device_id) self._device = device self._event = event self._device_id = device_id - self._unique_id = "_".join(x for x in self._device_id) # If id_string is 213c7f2:1, the group_id is 213c7f2, and the device will respond to # group events regardless of their group indices. (self._group_id, _, _) = device.id_string.partition(":") @@ -493,20 +506,6 @@ class RfxtrxEntity(RestoreEntity): return None return {ATTR_EVENT: "".join(f"{x:02x}" for x in self._event.data)} - @property - def unique_id(self): - """Return unique identifier of remote device.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info.""" - 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: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): """Check if event applies to me.""" if isinstance(event, rfxtrxmod.ControlEvent): @@ -546,7 +545,6 @@ class RfxtrxCommandEntity(RfxtrxEntity): ) -> None: """Initialzie a switch or light device.""" super().__init__(device, device_id, event=event) - self._state: bool | None = None async def _async_send(self, fun, *args): rfx_object = self.hass.data[DOMAIN][DATA_RFXOBJECT] diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index ef315c7b974..1a0fe698dcf 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -124,6 +124,9 @@ async def async_setup_entry( class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): """A representation of a RFXtrx binary sensor.""" + _attr_force_update = True + """We should force updates. Repeated states have meaning.""" + def __init__( self, device: rfxtrxmod.RFXtrxDevice, @@ -140,7 +143,6 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): self.entity_description = entity_description self._data_bits = data_bits self._off_delay = off_delay - self._state: bool | None = None self._delay_listener: CALLBACK_TYPE | None = None self._cmd_on = cmd_on self._cmd_off = cmd_off @@ -152,20 +154,10 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): if self._event is None: old_state = await self.async_get_last_state() if old_state is not None: - self._state = old_state.state == STATE_ON + self._attr_is_on = old_state.state == STATE_ON - if self._state and self._off_delay is not None: - self._state = False - - @property - def force_update(self) -> bool: - """We should force updates. Repeated states have meaning.""" - return True - - @property - def is_on(self): - """Return true if the sensor state is True.""" - return self._state + if self.is_on and self._off_delay is not None: + self._attr_is_on = False def _apply_event_lighting4(self, event: rfxtrxmod.RFXtrxEvent): """Apply event for a lighting 4 device.""" @@ -174,22 +166,22 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): assert cmdstr cmd = int(cmdstr, 16) if cmd == self._cmd_on: - self._state = True + self._attr_is_on = True elif cmd == self._cmd_off: - self._state = False + self._attr_is_on = False else: - self._state = True + self._attr_is_on = True def _apply_event_standard(self, event: rfxtrxmod.RFXtrxEvent): assert isinstance(event, (rfxtrxmod.SensorEvent, rfxtrxmod.ControlEvent)) if event.values.get("Command") in COMMAND_ON_LIST: - self._state = True + self._attr_is_on = True elif event.values.get("Command") in COMMAND_OFF_LIST: - self._state = False + self._attr_is_on = False elif event.values.get("Sensor Status") in SENSOR_STATUS_ON: - self._state = True + self._attr_is_on = True elif event.values.get("Sensor Status") in SENSOR_STATUS_OFF: - self._state = False + self._attr_is_on = False def _apply_event(self, event: rfxtrxmod.RFXtrxEvent): """Apply command from rfxtrx.""" @@ -226,7 +218,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): def off_delay_listener(now): """Switch device off after a delay.""" self._delay_listener = None - self._state = False + self._attr_is_on = False self.async_write_ha_state() self._delay_listener = evt.async_call_later( diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 6bf49beb89a..5e1788194f5 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -71,6 +71,21 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): """Initialize the RFXtrx cover device.""" super().__init__(device, device_id, event) self._venetian_blind_mode = venetian_blind_mode + self._attr_is_closed: bool | None = True + + self._attr_supported_features = ( + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP + ) + + if venetian_blind_mode in ( + CONST_VENETIAN_BLIND_MODE_US, + CONST_VENETIAN_BLIND_MODE_EU, + ): + self._attr_supported_features |= ( + CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT + ) async def async_added_to_hass(self) -> None: """Restore device state.""" @@ -79,31 +94,7 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): if self._event is None: old_state = await self.async_get_last_state() if old_state is not None: - self._state = old_state.state == STATE_OPEN - - @property - def supported_features(self) -> int: - """Flag supported features.""" - supported_features = ( - CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE | CoverEntityFeature.STOP - ) - - if self._venetian_blind_mode in ( - CONST_VENETIAN_BLIND_MODE_US, - CONST_VENETIAN_BLIND_MODE_EU, - ): - supported_features |= ( - CoverEntityFeature.OPEN_TILT - | CoverEntityFeature.CLOSE_TILT - | CoverEntityFeature.STOP_TILT - ) - - return supported_features - - @property - def is_closed(self) -> bool: - """Return if the cover is closed.""" - return not self._state + self._attr_is_closed = old_state.state != STATE_OPEN async def async_open_cover(self, **kwargs: Any) -> None: """Move the cover up.""" @@ -113,7 +104,7 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): await self._async_send(self._device.send_up2sec) else: await self._async_send(self._device.send_open) - self._state = True + self._attr_is_closed = False self.async_write_ha_state() async def async_close_cover(self, **kwargs: Any) -> None: @@ -124,13 +115,13 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): await self._async_send(self._device.send_down2sec) else: await self._async_send(self._device.send_close) - self._state = False + self._attr_is_closed = True self.async_write_ha_state() async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the cover.""" await self._async_send(self._device.send_stop) - self._state = True + self._attr_is_closed = False self.async_write_ha_state() async def async_open_cover_tilt(self, **kwargs: Any) -> None: @@ -150,7 +141,7 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the cover tilt.""" await self._async_send(self._device.send_stop) - self._state = True + self._attr_is_closed = False self.async_write_ha_state() def _apply_event(self, event: rfxtrxmod.RFXtrxEvent): @@ -158,9 +149,9 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): assert isinstance(event, rfxtrxmod.ControlEvent) super()._apply_event(event) if event.values["Command"] in COMMAND_ON_LIST: - self._state = True + self._attr_is_closed = False elif event.values["Command"] in COMMAND_OFF_LIST: - self._state = False + self._attr_is_closed = True @callback def _handle_event(self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple): diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index 87f8c68aaac..198d0ffd3f1 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -56,7 +56,7 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} - _brightness = 0 + _attr_brightness: int = 0 _device: rfxtrxmod.LightingDevice async def async_added_to_hass(self): @@ -66,37 +66,28 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): if self._event is None: old_state = await self.async_get_last_state() if old_state is not None: - self._state = old_state.state == STATE_ON - self._brightness = old_state.attributes.get(ATTR_BRIGHTNESS) - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def is_on(self): - """Return true if device is on.""" - return self._state + self._attr_is_on = old_state.state == STATE_ON + if brightness := old_state.attributes.get(ATTR_BRIGHTNESS): + self._attr_brightness = int(brightness) async def async_turn_on(self, **kwargs): """Turn the device on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) - self._state = True + self._attr_is_on = True if brightness is None: await self._async_send(self._device.send_on) - self._brightness = 255 + self._attr_brightness = 255 else: await self._async_send(self._device.send_dim, brightness * 100 // 255) - self._brightness = brightness + self._attr_brightness = brightness self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the device off.""" await self._async_send(self._device.send_off) - self._state = False - self._brightness = 0 + self._attr_is_on = False + self._attr_brightness = 0 self.async_write_ha_state() def _apply_event(self, event: rfxtrxmod.RFXtrxEvent): @@ -104,12 +95,13 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): assert isinstance(event, rfxtrxmod.ControlEvent) super()._apply_event(event) if event.values["Command"] in COMMAND_ON_LIST: - self._state = True + self._attr_is_on = True elif event.values["Command"] in COMMAND_OFF_LIST: - self._state = False + self._attr_is_on = False elif event.values["Command"] == "Set level": - self._brightness = event.values["Dim level"] * 255 // 100 - self._state = self._brightness > 0 + brightness = event.values["Dim level"] * 255 // 100 + self._attr_brightness = brightness + self._attr_is_on = brightness > 0 @callback def _handle_event(self, event, device_id): diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 6ff3073b900..86c3eabc922 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -273,15 +273,16 @@ async def async_setup_entry( class RfxtrxSensor(RfxtrxEntity, SensorEntity): """Representation of a RFXtrx sensor.""" + _attr_force_update = True + """We should force updates. Repeated states have meaning.""" + entity_description: RfxtrxSensorEntityDescription def __init__(self, device, device_id, entity_description, event=None): """Initialize the sensor.""" super().__init__(device, device_id, event=event) self.entity_description = entity_description - self._unique_id = "_".join( - x for x in (*self._device_id, entity_description.key) - ) + self._attr_unique_id = "_".join(x for x in (*device_id, entity_description.key)) async def async_added_to_hass(self): """Restore device state.""" @@ -302,16 +303,6 @@ class RfxtrxSensor(RfxtrxEntity, SensorEntity): value = self._event.values.get(self.entity_description.key) return self.entity_description.convert(value) - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def force_update(self) -> bool: - """We should force updates. Repeated states have meaning.""" - return True - @callback def _handle_event(self, event, device_id): """Check if event applies to me and update.""" diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index fc826b4ebe6..f164e54b212 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -94,7 +94,7 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): if self._event is None: old_state = await self.async_get_last_state() if old_state is not None: - self._state = old_state.state == STATE_ON + self._attr_is_on = old_state.state == STATE_ON def _apply_event_lighting4(self, event: rfxtrxmod.RFXtrxEvent): """Apply event for a lighting 4 device.""" @@ -103,18 +103,18 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): assert cmdstr cmd = int(cmdstr, 16) if cmd == self._cmd_on: - self._state = True + self._attr_is_on = True elif cmd == self._cmd_off: - self._state = False + self._attr_is_on = False else: - self._state = True + self._attr_is_on = True def _apply_event_standard(self, event: rfxtrxmod.RFXtrxEvent) -> None: assert isinstance(event, rfxtrxmod.ControlEvent) if event.values["Command"] in COMMAND_ON_LIST: - self._state = True + self._attr_is_on = True elif event.values["Command"] in COMMAND_OFF_LIST: - self._state = False + self._attr_is_on = False def _apply_event(self, event: rfxtrxmod.RFXtrxEvent) -> None: """Apply command from rfxtrx.""" @@ -134,18 +134,13 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): self.async_write_ha_state() - @property - def is_on(self): - """Return true if device is on.""" - return self._state - async def async_turn_on(self, **kwargs): """Turn the device on.""" if self._cmd_on is not None: await self._async_send(self._device.send_command, self._cmd_on) else: await self._async_send(self._device.send_on) - self._state = True + self._attr_is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs): @@ -154,5 +149,5 @@ class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): await self._async_send(self._device.send_command, self._cmd_off) else: await self._async_send(self._device.send_off) - self._state = False + self._attr_is_on = False self.async_write_ha_state() From 06a4c226fdc180e59e66d0d4fe33894e7d6c7802 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 16:44:44 +0200 Subject: [PATCH 2455/3516] Remove konnected from mypy ignore list (#75003) --- homeassistant/components/konnected/__init__.py | 2 +- homeassistant/components/konnected/config_flow.py | 12 +++++++----- mypy.ini | 6 ------ script/hassfest/mypy_config.py | 2 -- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 98440766334..620ed12ac54 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -419,7 +419,7 @@ class KonnectedView(HomeAssistantView): resp = {} if request.query.get(CONF_ZONE): resp[CONF_ZONE] = zone_num - else: + elif zone_num: resp[CONF_PIN] = ZONE_TO_PIN[zone_num] # Make sure entity is setup diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index 94a58227c56..fcf94a38c18 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -6,6 +6,7 @@ import copy import logging import random import string +from typing import Any from urllib.parse import urlparse import voluptuous as vol @@ -171,11 +172,11 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 # class variable to store/share discovered host information - discovered_hosts = {} + discovered_hosts: dict[str, dict[str, Any]] = {} def __init__(self) -> None: """Initialize the Konnected flow.""" - self.data = {} + self.data: dict[str, Any] = {} self.options = OPTIONS_SCHEMA({CONF_IO: {}}) async def async_gen_config(self, host, port): @@ -271,6 +272,7 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.error("Malformed Konnected SSDP info") else: # extract host/port from ssdp_location + assert discovery_info.ssdp_location netloc = urlparse(discovery_info.ssdp_location).netloc.split(":") self._async_abort_entries_match( {CONF_HOST: netloc[0], CONF_PORT: int(netloc[1])} @@ -392,10 +394,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self.current_opt = self.entry.options or self.entry.data[CONF_DEFAULT_OPTIONS] # as config proceeds we'll build up new options and then replace what's in the config entry - self.new_opt = {CONF_IO: {}} + self.new_opt: dict[str, dict[str, Any]] = {CONF_IO: {}} self.active_cfg = None - self.io_cfg = {} - self.current_states = [] + self.io_cfg: dict[str, Any] = {} + self.current_states: list[dict[str, Any]] = [] self.current_state = 1 @callback diff --git a/mypy.ini b/mypy.ini index 4975e893a38..ca7fd4a15c2 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2690,12 +2690,6 @@ ignore_errors = true [mypy-homeassistant.components.izone.climate] ignore_errors = true -[mypy-homeassistant.components.konnected] -ignore_errors = true - -[mypy-homeassistant.components.konnected.config_flow] -ignore_errors = true - [mypy-homeassistant.components.lovelace] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index d9a63aeb11f..21309560eb3 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -31,8 +31,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.icloud.device_tracker", "homeassistant.components.icloud.sensor", "homeassistant.components.izone.climate", - "homeassistant.components.konnected", - "homeassistant.components.konnected.config_flow", "homeassistant.components.lovelace", "homeassistant.components.lovelace.dashboard", "homeassistant.components.lovelace.resources", From fe1c23321e615fee486e1a1a527c20e8cd4ca0a3 Mon Sep 17 00:00:00 2001 From: Ludovico de Nittis Date: Mon, 11 Jul 2022 16:51:10 +0200 Subject: [PATCH 2456/3516] Update pyialarm to 2.2.0 (#74874) --- homeassistant/components/ialarm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ialarm/manifest.json b/homeassistant/components/ialarm/manifest.json index 60ecb9da74a..00a0cba9a27 100644 --- a/homeassistant/components/ialarm/manifest.json +++ b/homeassistant/components/ialarm/manifest.json @@ -2,7 +2,7 @@ "domain": "ialarm", "name": "Antifurto365 iAlarm", "documentation": "https://www.home-assistant.io/integrations/ialarm", - "requirements": ["pyialarm==1.9.0"], + "requirements": ["pyialarm==2.2.0"], "codeowners": ["@RyuzakiKK"], "config_flow": true, "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 74d53ca2c7b..5376d91ed2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1556,7 +1556,7 @@ pyhomematic==0.1.77 pyhomeworks==0.0.6 # homeassistant.components.ialarm -pyialarm==1.9.0 +pyialarm==2.2.0 # homeassistant.components.icloud pyicloud==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6069c63dcf2..8ad2dd3957f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1053,7 +1053,7 @@ pyhiveapi==0.5.13 pyhomematic==0.1.77 # homeassistant.components.ialarm -pyialarm==1.9.0 +pyialarm==2.2.0 # homeassistant.components.icloud pyicloud==1.0.0 From 73a8ae35c296f4d2cfb9c1b0773f8f672826993f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 16:53:14 +0200 Subject: [PATCH 2457/3516] Remove izone from mypy ignore list (#75005) --- homeassistant/components/izone/climate.py | 26 +++++++++++------------ mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index 419e1709b45..a26490e78c8 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -76,7 +76,7 @@ async def async_setup_entry( @callback def init_controller(ctrl: Controller): """Register the controller device and the containing zones.""" - conf: ConfigType = hass.data.get(DATA_CONFIG) + conf: ConfigType | None = hass.data.get(DATA_CONFIG) # Filter out any entities excluded in the config file if conf and ctrl.device_uid in conf[CONF_EXCLUDE]: @@ -107,8 +107,6 @@ async def async_setup_entry( "async_set_airflow_max", ) - return True - def _return_on_connection_error(ret=None): def wrap(func): @@ -310,7 +308,7 @@ class ControllerDevice(ClimateEntity): return key assert False, "Should be unreachable" - @property + @property # type: ignore[misc] @_return_on_connection_error([]) def hvac_modes(self) -> list[HVACMode]: """Return the list of available operation modes.""" @@ -318,13 +316,13 @@ class ControllerDevice(ClimateEntity): return [HVACMode.OFF, HVACMode.FAN_ONLY] return [HVACMode.OFF, *self._state_to_pizone] - @property + @property # type: ignore[misc] @_return_on_connection_error(PRESET_NONE) def preset_mode(self): """Eco mode is external air.""" return PRESET_ECO if self._controller.free_air else PRESET_NONE - @property + @property # type: ignore[misc] @_return_on_connection_error([PRESET_NONE]) def preset_modes(self): """Available preset modes, normal or eco.""" @@ -332,7 +330,7 @@ class ControllerDevice(ClimateEntity): return [PRESET_NONE, PRESET_ECO] return [PRESET_NONE] - @property + @property # type: ignore[misc] @_return_on_connection_error() def current_temperature(self) -> float | None: """Return the current temperature.""" @@ -362,7 +360,7 @@ class ControllerDevice(ClimateEntity): return None return zone.target_temperature - @property + @property # type: ignore[misc] @_return_on_connection_error() def target_temperature(self) -> float | None: """Return the temperature we try to reach (either from control zone or master unit).""" @@ -390,13 +388,13 @@ class ControllerDevice(ClimateEntity): """Return the list of available fan modes.""" return list(self._fan_to_pizone) - @property + @property # type: ignore[misc] @_return_on_connection_error(0.0) def min_temp(self) -> float: """Return the minimum temperature.""" return self._controller.temp_min - @property + @property # type: ignore[misc] @_return_on_connection_error(50.0) def max_temp(self) -> float: """Return the maximum temperature.""" @@ -471,7 +469,9 @@ class ZoneDevice(ClimateEntity): self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE self._attr_device_info = DeviceInfo( - identifiers={(IZONE, controller.unique_id, zone.index)}, + identifiers={ + (IZONE, controller.unique_id, zone.index) # type:ignore[arg-type] + }, manufacturer="IZone", model=zone.type.name.title(), name=self.name, @@ -533,7 +533,7 @@ class ZoneDevice(ClimateEntity): """ return False - @property + @property # type: ignore[misc] @_return_on_connection_error(0) def supported_features(self): """Return the list of supported features.""" @@ -552,7 +552,7 @@ class ZoneDevice(ClimateEntity): return PRECISION_TENTHS @property - def hvac_mode(self) -> HVACMode: + def hvac_mode(self) -> HVACMode | None: """Return current operation ie. heat, cool, idle.""" mode = self._zone.mode for (key, value) in self._state_to_pizone.items(): diff --git a/mypy.ini b/mypy.ini index ca7fd4a15c2..ce02b366ad0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2687,9 +2687,6 @@ ignore_errors = true [mypy-homeassistant.components.icloud.sensor] ignore_errors = true -[mypy-homeassistant.components.izone.climate] -ignore_errors = true - [mypy-homeassistant.components.lovelace] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 21309560eb3..8d36184290b 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -30,7 +30,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.icloud.account", "homeassistant.components.icloud.device_tracker", "homeassistant.components.icloud.sensor", - "homeassistant.components.izone.climate", "homeassistant.components.lovelace", "homeassistant.components.lovelace.dashboard", "homeassistant.components.lovelace.resources", From af2feb3d40f6cc6801477caab3419dc98afce443 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 16:54:01 +0200 Subject: [PATCH 2458/3516] Update pyupgrade to v2.37.1 (#74989) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- tests/components/august/mocks.py | 3 ++- tests/components/calendar/test_trigger.py | 4 ++-- tests/components/google/conftest.py | 4 ++-- tests/components/nest/common.py | 4 ++-- tests/components/stream/conftest.py | 2 +- tests/components/unifiprotect/utils.py | 3 ++- 8 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 91bda72e616..bbf7295be99 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.34.0 + rev: v2.37.1 hooks: - id: pyupgrade args: [--py39-plus] diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index caed3668db7..b1a9a0a1a5b 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -12,5 +12,5 @@ mccabe==0.6.1 pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 -pyupgrade==2.34.0 +pyupgrade==2.37.1 yamllint==1.27.1 diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index c93e6429f1c..932065f37da 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -1,10 +1,11 @@ """Mocks for the august component.""" from __future__ import annotations +from collections.abc import Iterable import json import os import time -from typing import Any, Iterable +from typing import Any from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from yalexs.activity import ( diff --git a/tests/components/calendar/test_trigger.py b/tests/components/calendar/test_trigger.py index a3b5bacca59..ebe5e9185e4 100644 --- a/tests/components/calendar/test_trigger.py +++ b/tests/components/calendar/test_trigger.py @@ -8,11 +8,11 @@ forward exercising the triggers. """ from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Generator import datetime import logging import secrets -from typing import Any, Generator +from typing import Any from unittest.mock import patch import pytest diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index f47d1232582..7e668de2891 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -1,10 +1,10 @@ """Test configuration and mocks for the google integration.""" from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Generator import datetime import http -from typing import Any, Generator, TypeVar +from typing import Any, TypeVar from unittest.mock import Mock, mock_open, patch from aiohttp.client_exceptions import ClientError diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index f86112ada75..fbdc2665879 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -2,11 +2,11 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Generator import copy from dataclasses import dataclass, field import time -from typing import Any, Generator, TypeVar +from typing import Any, TypeVar from google_nest_sdm.auth import AbstractAuth from google_nest_sdm.device import Device diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py index 91b4106c1f4..ff1a66a1215 100644 --- a/tests/components/stream/conftest.py +++ b/tests/components/stream/conftest.py @@ -12,10 +12,10 @@ so that it can inspect the output. from __future__ import annotations import asyncio +from collections.abc import Generator from http import HTTPStatus import logging import threading -from typing import Generator from unittest.mock import Mock, patch from aiohttp import web diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index 260c6996128..8d54cbddea6 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -2,9 +2,10 @@ # pylint: disable=protected-access from __future__ import annotations +from collections.abc import Sequence from dataclasses import dataclass from datetime import timedelta -from typing import Any, Callable, Sequence +from typing import Any, Callable from unittest.mock import Mock from pyunifiprotect import ProtectApiClient From eb922b2a1f6d8bb3e6f5701602042102e401a9fa Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Jul 2022 16:55:06 +0200 Subject: [PATCH 2459/3516] Add temperature number to demo integration (#74986) --- homeassistant/components/demo/number.py | 54 ++++++++++++++++--------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py index 02ab1a2a989..17382ab9962 100644 --- a/homeassistant/components/demo/number.py +++ b/homeassistant/components/demo/number.py @@ -1,9 +1,9 @@ """Demo platform that offers a fake Number entity.""" from __future__ import annotations -from homeassistant.components.number import NumberEntity, NumberMode +from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.const import DEVICE_DEFAULT_NAME, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -35,10 +35,10 @@ async def async_setup_platform( 0.42, "mdi:square-wave", False, - 0.0, - 1.0, - 0.01, - NumberMode.BOX, + native_min_value=0.0, + native_max_value=1.0, + native_step=0.01, + mode=NumberMode.BOX, ), DemoNumber( "large_range", @@ -46,9 +46,9 @@ async def async_setup_platform( 500, "mdi:square-wave", False, - 1, - 1000, - 1, + native_min_value=1, + native_max_value=1000, + native_step=1, ), DemoNumber( "small_range", @@ -56,9 +56,22 @@ async def async_setup_platform( 128, "mdi:square-wave", False, - 1, - 255, - 1, + native_min_value=1, + native_max_value=255, + native_step=1, + ), + DemoNumber( + "temp1", + "Temperature setting", + 22, + "mdi:thermometer", + False, + device_class=NumberDeviceClass.TEMPERATURE, + native_min_value=15.0, + native_max_value=35.0, + native_step=1, + mode=NumberMode.BOX, + unit_of_measurement=TEMP_CELSIUS, ), ] ) @@ -84,19 +97,24 @@ class DemoNumber(NumberEntity): name: str, state: float, icon: str, - assumed: bool, + assumed_state: bool, + *, + device_class: NumberDeviceClass | None = None, + mode: NumberMode = NumberMode.AUTO, native_min_value: float | None = None, native_max_value: float | None = None, native_step: float | None = None, - mode: NumberMode = NumberMode.AUTO, + unit_of_measurement: str | None = None, ) -> None: """Initialize the Demo Number entity.""" - self._attr_assumed_state = assumed + self._attr_assumed_state = assumed_state + self._attr_device_class = device_class self._attr_icon = icon - self._attr_name = name or DEVICE_DEFAULT_NAME - self._attr_unique_id = unique_id - self._attr_native_value = state self._attr_mode = mode + self._attr_name = name or DEVICE_DEFAULT_NAME + self._attr_native_unit_of_measurement = unit_of_measurement + self._attr_native_value = state + self._attr_unique_id = unique_id if native_min_value is not None: self._attr_native_min_value = native_min_value From c2fefe03b2dc800f42de695f0b73a8f26621d882 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Jul 2022 17:14:00 +0200 Subject: [PATCH 2460/3516] Add support for subscribing to bluetooth callbacks by address (#74773) --- .../components/bluetooth/__init__.py | 119 +++++++- homeassistant/components/bluetooth/models.py | 50 +++- tests/components/bluetooth/test_init.py | 268 +++++++++++++++++- 3 files changed, 415 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index cf7f1884869..26c1af3fb92 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -8,7 +8,7 @@ import fnmatch from functools import cached_property import logging import platform -from typing import Final +from typing import Final, TypedDict from bleak import BleakError from bleak.backends.device import MANUFACTURERS, BLEDevice @@ -26,7 +26,11 @@ from homeassistant.core import ( from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import discovery_flow from homeassistant.helpers.typing import ConfigType -from homeassistant.loader import BluetoothMatcher, async_get_bluetooth +from homeassistant.loader import ( + BluetoothMatcher, + BluetoothMatcherOptional, + async_get_bluetooth, +) from . import models from .const import DOMAIN @@ -38,6 +42,19 @@ _LOGGER = logging.getLogger(__name__) MAX_REMEMBER_ADDRESSES: Final = 2048 +class BluetoothCallbackMatcherOptional(TypedDict, total=False): + """Matcher for the bluetooth integration for callback optional fields.""" + + address: str + + +class BluetoothCallbackMatcher( + BluetoothMatcherOptional, + BluetoothCallbackMatcherOptional, +): + """Callback matcher for the bluetooth integration.""" + + class BluetoothScanningMode(Enum): """The mode of scanning for bluetooth devices.""" @@ -50,6 +67,7 @@ SCANNING_MODE_TO_BLEAK = { BluetoothScanningMode.PASSIVE: "passive", } +ADDRESS: Final = "address" LOCAL_NAME: Final = "local_name" SERVICE_UUID: Final = "service_uuid" MANUFACTURER_ID: Final = "manufacturer_id" @@ -102,11 +120,34 @@ BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") BluetoothCallback = Callable[[BluetoothServiceInfo, BluetoothChange], None] +@hass_callback +def async_discovered_service_info( + hass: HomeAssistant, +) -> list[BluetoothServiceInfo]: + """Return the discovered devices list.""" + if DOMAIN not in hass.data: + return [] + manager: BluetoothManager = hass.data[DOMAIN] + return manager.async_discovered_service_info() + + +@hass_callback +def async_address_present( + hass: HomeAssistant, + address: str, +) -> bool: + """Check if an address is present in the bluetooth device list.""" + if DOMAIN not in hass.data: + return False + manager: BluetoothManager = hass.data[DOMAIN] + return manager.async_address_present(address) + + @hass_callback def async_register_callback( hass: HomeAssistant, callback: BluetoothCallback, - match_dict: BluetoothMatcher | None, + match_dict: BluetoothCallbackMatcher | None, ) -> Callable[[], None]: """Register to receive a callback on bluetooth change. @@ -128,9 +169,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: def _ble_device_matches( - matcher: BluetoothMatcher, device: BLEDevice, advertisement_data: AdvertisementData + matcher: BluetoothCallbackMatcher | BluetoothMatcher, + device: BLEDevice, + advertisement_data: AdvertisementData, ) -> bool: """Check if a ble device and advertisement_data matches the matcher.""" + if ( + matcher_address := matcher.get(ADDRESS) + ) is not None and device.address != matcher_address: + return False + if ( matcher_local_name := matcher.get(LOCAL_NAME) ) is not None and not fnmatch.fnmatch( @@ -192,7 +240,9 @@ class BluetoothManager: self._integration_matchers = integration_matchers self.scanner: HaBleakScanner | None = None self._cancel_device_detected: CALLBACK_TYPE | None = None - self._callbacks: list[tuple[BluetoothCallback, BluetoothMatcher | None]] = [] + self._callbacks: list[ + tuple[BluetoothCallback, BluetoothCallbackMatcher | None] + ] = [] # Some devices use a random address so we need to use # an LRU to avoid memory issues. self._matched: LRU = LRU(MAX_REMEMBER_ADDRESSES) @@ -227,14 +277,22 @@ class BluetoothManager: ) -> None: """Handle a detected device.""" matched_domains: set[str] | None = None - if device.address not in self._matched: + match_key = (device.address, bool(advertisement_data.manufacturer_data)) + match_key_has_mfr_data = (device.address, True) + + # If we matched without manufacturer_data, we need to do it again + # since we may think the device is unsupported otherwise + if ( + match_key_has_mfr_data not in self._matched + and match_key not in self._matched + ): matched_domains = { matcher["domain"] for matcher in self._integration_matchers if _ble_device_matches(matcher, device, advertisement_data) } if matched_domains: - self._matched[device.address] = True + self._matched[match_key] = True _LOGGER.debug( "Device detected: %s with advertisement_data: %s matched domains: %s", device, @@ -275,18 +333,61 @@ class BluetoothManager: @hass_callback def async_register_callback( - self, callback: BluetoothCallback, match_dict: BluetoothMatcher | None = None + self, + callback: BluetoothCallback, + matcher: BluetoothCallbackMatcher | None = None, ) -> Callable[[], None]: """Register a callback.""" - callback_entry = (callback, match_dict) + callback_entry = (callback, matcher) self._callbacks.append(callback_entry) @hass_callback def _async_remove_callback() -> None: self._callbacks.remove(callback_entry) + # If we have history for the subscriber, we can trigger the callback + # immediately with the last packet so the subscriber can see the + # device. + if ( + matcher + and (address := matcher.get(ADDRESS)) + and models.HA_BLEAK_SCANNER + and (device_adv_data := models.HA_BLEAK_SCANNER.history.get(address)) + ): + try: + callback( + BluetoothServiceInfo.from_advertisement(*device_adv_data), + BluetoothChange.ADVERTISEMENT, + ) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in bluetooth callback") + return _async_remove_callback + @hass_callback + def async_address_present(self, address: str) -> bool: + """Return if the address is present.""" + return bool( + models.HA_BLEAK_SCANNER + and any( + device.address == address + for device in models.HA_BLEAK_SCANNER.discovered_devices + ) + ) + + @hass_callback + def async_discovered_service_info(self) -> list[BluetoothServiceInfo]: + """Return if the address is present.""" + if models.HA_BLEAK_SCANNER: + discovered = models.HA_BLEAK_SCANNER.discovered_devices + history = models.HA_BLEAK_SCANNER.history + return [ + BluetoothServiceInfo.from_advertisement(*history[device.address]) + for device in discovered + if device.address in history + ] + return [] + async def async_stop(self, event: Event) -> None: """Stop bluetooth discovery.""" if self._cancel_device_detected: diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index a2651c587f7..43d4d0cb923 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -8,7 +8,11 @@ from typing import Any, Final, cast from bleak import BleakScanner from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData, AdvertisementDataCallback +from bleak.backends.scanner import ( + AdvertisementData, + AdvertisementDataCallback, + BaseBleakScanner, +) from lru import LRU # pylint: disable=no-name-in-module from homeassistant.core import CALLBACK_TYPE, callback as hass_callback @@ -52,7 +56,7 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] self._callbacks: list[ tuple[AdvertisementDataCallback, dict[str, set[str]]] ] = [] - self._history: LRU = LRU(MAX_HISTORY_SIZE) + self.history: LRU = LRU(MAX_HISTORY_SIZE) super().__init__(*args, **kwargs) @hass_callback @@ -70,7 +74,7 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] # Replay the history since otherwise we miss devices # that were already discovered before the callback was registered # or we are in passive mode - for device, advertisement_data in self._history.values(): + for device, advertisement_data in self.history.values(): _dispatch_callback(callback, filters, device, advertisement_data) return _remove_callback @@ -83,31 +87,46 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] Here we get the actual callback from bleak and dispatch it to all the wrapped HaBleakScannerWrapper classes """ - self._history[device.address] = (device, advertisement_data) + self.history[device.address] = (device, advertisement_data) for callback_filters in self._callbacks: _dispatch_callback(*callback_filters, device, advertisement_data) -class HaBleakScannerWrapper(BleakScanner): # type: ignore[misc] +class HaBleakScannerWrapper(BaseBleakScanner): # type: ignore[misc] """A wrapper that uses the single instance.""" def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize the BleakScanner.""" self._detection_cancel: CALLBACK_TYPE | None = None self._mapped_filters: dict[str, set[str]] = {} - if "filters" in kwargs: - self._mapped_filters = {k: set(v) for k, v in kwargs["filters"].items()} - if "service_uuids" in kwargs: - self._mapped_filters[FILTER_UUIDS] = set(kwargs["service_uuids"]) + self._adv_data_callback: AdvertisementDataCallback | None = None + self._map_filters(*args, **kwargs) super().__init__(*args, **kwargs) async def stop(self, *args: Any, **kwargs: Any) -> None: """Stop scanning for devices.""" - return async def start(self, *args: Any, **kwargs: Any) -> None: """Start scanning for devices.""" - return + + def _map_filters(self, *args: Any, **kwargs: Any) -> bool: + """Map the filters.""" + mapped_filters = {} + if filters := kwargs.get("filters"): + if FILTER_UUIDS not in filters: + _LOGGER.warning("Only %s filters are supported", FILTER_UUIDS) + mapped_filters = {k: set(v) for k, v in filters.items()} + if service_uuids := kwargs.get("service_uuids"): + mapped_filters[FILTER_UUIDS] = set(service_uuids) + if mapped_filters == self._mapped_filters: + return False + self._mapped_filters = mapped_filters + return True + + def set_scanning_filter(self, *args: Any, **kwargs: Any) -> None: + """Set the filters to use.""" + if self._map_filters(*args, **kwargs): + self._setup_detection_callback() def _cancel_callback(self) -> None: """Cancel callback.""" @@ -127,8 +146,15 @@ class HaBleakScannerWrapper(BleakScanner): # type: ignore[misc] This method takes the callback and registers it with the long running scanner. """ + self._adv_data_callback = callback + self._setup_detection_callback() + + def _setup_detection_callback(self) -> None: + """Set up the detection callback.""" + if self._adv_data_callback is None: + return self._cancel_callback() - super().register_detection_callback(callback) + super().register_detection_callback(self._adv_data_callback) assert HA_BLEAK_SCANNER is not None self._detection_cancel = HA_BLEAK_SCANNER.async_register_callback( self._callback, self._mapped_filters diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 1e0647df01c..f43ef4737f6 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -78,6 +78,30 @@ async def test_setup_and_stop_no_bluetooth(hass, caplog): assert "Could not create bluetooth scanner" in caplog.text +async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): + """Test we fail gracefully when asking for discovered devices and there is no blueooth.""" + mock_bt = [] + with patch( + "homeassistant.components.bluetooth.HaBleakScanner", side_effect=BleakError + ) as mock_ha_bleak_scanner, patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object( + hass.config_entries.flow, "async_init" + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert len(mock_ha_bleak_scanner.mock_calls) == 1 + assert "Could not create bluetooth scanner" in caplog.text + assert not bluetooth.async_discovered_service_info(hass) + assert not bluetooth.async_address_present(hass, "aa:bb:bb:dd:ee:ff") + + async def test_discovery_match_by_service_uuid(hass, mock_bleak_scanner_start): """Test bluetooth discovery match by service_uuid.""" mock_bt = [ @@ -207,8 +231,47 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( assert len(mock_config_flow.mock_calls) == 0 +async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): + """Test the async_discovered_device_api.""" + mock_bt = [] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch( + "bleak.BleakScanner.discovered_devices", # Must patch before we setup + [MagicMock(address="44:44:33:11:23:45")], + ): + assert not bluetooth.async_discovered_service_info(hass) + assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22") + + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + assert not bluetooth.async_discovered_service_info(hass) + + wrong_device = BLEDevice("44:44:33:11:23:42", "wrong_name") + wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) + models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv) + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + service_infos = bluetooth.async_discovered_service_info(hass) + assert len(service_infos) == 1 + # wrong_name should not appear because bleak no longer sees it + assert service_infos[0].name == "wohand" + + assert bluetooth.async_address_present(hass, "44:44:33:11:23:42") is False + assert bluetooth.async_address_present(hass, "44:44:33:11:23:45") is True + + async def test_register_callbacks(hass, mock_bleak_scanner_start): - """Test configured options for a device are loaded via config entry.""" + """Test registering a callback.""" mock_bt = [] callbacks = [] @@ -284,6 +347,92 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start): assert service_info.manufacturer_id is None +async def test_register_callback_by_address(hass, mock_bleak_scanner_start): + """Test registering a callback by address.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + if len(callbacks) >= 3: + raise ValueError + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {"address": "44:44:33:11:23:45"}, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + await hass.async_block_till_done() + + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + # 3rd callback raises ValueError but is still tracked + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + await hass.async_block_till_done() + + cancel() + + # 4th callback should not be tracked since we canceled + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + await hass.async_block_till_done() + + # Now register again with a callback that fails to + # make sure we do not perm fail + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {"address": "44:44:33:11:23:45"}, + ) + cancel() + + # Now register again, since the 3rd callback + # should fail but we should still record it + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {"address": "44:44:33:11:23:45"}, + ) + cancel() + + assert len(callbacks) == 3 + + for idx in range(3): + service_info: BluetoothServiceInfo = callbacks[idx][0] + assert service_info.name == "wohand" + assert service_info.manufacturer == "Nordic Semiconductor ASA" + assert service_info.manufacturer_id == 89 + + async def test_wrapped_instance_with_filter(hass, mock_bleak_scanner_start): """Test consumers can use the wrapped instance with a filter as if it was normal BleakScanner.""" with patch( @@ -438,3 +587,120 @@ async def test_wrapped_instance_with_broken_callbacks(hass, mock_bleak_scanner_s models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 1 + + +async def test_wrapped_instance_changes_uuids(hass, mock_bleak_scanner_start): + """Test consumers can use the wrapped instance can change the uuids later.""" + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + detected = [] + + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") + + assert models.HA_BLEAK_SCANNER is not None + scanner = models.HaBleakScannerWrapper() + scanner.set_scanning_filter(service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]) + scanner.register_detection_callback(_device_detected) + + type(models.HA_BLEAK_SCANNER).discovered_devices = [MagicMock()] + for _ in range(2): + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(detected) == 2 + + # The UUIDs list we created in the wrapped scanner with should be respected + # and we should not get another callback + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + assert len(detected) == 2 + + +async def test_wrapped_instance_changes_filters(hass, mock_bleak_scanner_start): + """Test consumers can use the wrapped instance can change the filter later.""" + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + detected = [] + + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) + + switchbot_device = BLEDevice("44:44:33:11:23:42", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:62", "empty") + empty_adv = AdvertisementData(local_name="empty") + + assert models.HA_BLEAK_SCANNER is not None + scanner = models.HaBleakScannerWrapper() + scanner.set_scanning_filter( + filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} + ) + scanner.register_detection_callback(_device_detected) + + type(models.HA_BLEAK_SCANNER).discovered_devices = [MagicMock()] + for _ in range(2): + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(detected) == 2 + + # The UUIDs list we created in the wrapped scanner with should be respected + # and we should not get another callback + models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + assert len(detected) == 2 + + +async def test_wrapped_instance_unsupported_filter( + hass, mock_bleak_scanner_start, caplog +): + """Test we want when their filter is ineffective.""" + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] + ), patch.object(hass.config_entries.flow, "async_init"): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert models.HA_BLEAK_SCANNER is not None + scanner = models.HaBleakScannerWrapper() + scanner.set_scanning_filter( + filters={"unsupported": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} + ) + assert "Only UUIDs filters are supported" in caplog.text From fa51a39f1dcf96dadc69bd8cda5a7cffedad4560 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:24:35 +0200 Subject: [PATCH 2461/3516] Use instance attributes in evohome (#74996) --- homeassistant/components/evohome/__init__.py | 36 ++----------------- homeassistant/components/evohome/climate.py | 31 ++++++++-------- .../components/evohome/water_heater.py | 26 ++++++++------ mypy.ini | 3 -- script/hassfest/mypy_config.py | 1 - 5 files changed, 35 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 908dff48aef..1bb84dfc40a 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -20,7 +20,6 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME, - TEMP_CELSIUS, Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback @@ -519,13 +518,14 @@ class EvoDevice(Entity): DHW controller. """ + _attr_should_poll = False + def __init__(self, evo_broker, evo_device) -> None: """Initialize the evohome entity.""" self._evo_device = evo_device self._evo_broker = evo_broker self._evo_tcs = evo_broker.tcs - self._unique_id = self._name = self._icon = self._precision = None self._device_state_attrs = {} async def async_refresh(self, payload: dict | None = None) -> None: @@ -533,7 +533,7 @@ class EvoDevice(Entity): if payload is None: self.async_schedule_update_ha_state(force_refresh=True) return - if payload["unique_id"] != self._unique_id: + if payload["unique_id"] != self._attr_unique_id: return if payload["service"] in (SVC_SET_ZONE_OVERRIDE, SVC_RESET_ZONE_OVERRIDE): await self.async_zone_svc_request(payload["service"], payload["data"]) @@ -548,21 +548,6 @@ class EvoDevice(Entity): """Process a service request (setpoint override) for a zone.""" raise NotImplementedError - @property - def should_poll(self) -> bool: - """Evohome entities should not be polled.""" - return False - - @property - def unique_id(self) -> str | None: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> str: - """Return the name of the evohome entity.""" - return self._name - @property def extra_state_attributes(self) -> dict[str, Any]: """Return the evohome-specific state attributes.""" @@ -576,25 +561,10 @@ class EvoDevice(Entity): return {"status": convert_dict(status)} - @property - def icon(self) -> str: - """Return the icon to use in the frontend UI.""" - return self._icon - async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" async_dispatcher_connect(self.hass, DOMAIN, self.async_refresh) - @property - def precision(self) -> float: - """Return the temperature precision to use in the frontend UI.""" - return self._precision - - @property - def temperature_unit(self) -> str: - """Return the temperature unit to use in the frontend UI.""" - return TEMP_CELSIUS - class EvoChild(EvoDevice): """Base for any evohome child. diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 676bc88f470..700fa6ab078 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -13,7 +13,7 @@ from homeassistant.components.climate.const import ( ClimateEntityFeature, HVACMode, ) -from homeassistant.const import PRECISION_TENTHS +from homeassistant.const import PRECISION_TENTHS, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -151,19 +151,19 @@ class EvoZone(EvoChild, EvoClimateEntity): if evo_device.modelType.startswith("VisionProWifi"): # this system does not have a distinct ID for the zone - self._unique_id = f"{evo_device.zoneId}z" + self._attr_unique_id = f"{evo_device.zoneId}z" else: - self._unique_id = evo_device.zoneId + self._attr_unique_id = evo_device.zoneId - self._name = evo_device.name - self._icon = "mdi:radiator" + self._attr_name = evo_device.name if evo_broker.client_v1: - self._precision = PRECISION_TENTHS + self._attr_precision = PRECISION_TENTHS else: - self._precision = self._evo_device.setpointCapabilities["valueResolution"] + self._attr_precision = self._evo_device.setpointCapabilities[ + "valueResolution" + ] - self._preset_modes = list(HA_PRESET_TO_EVO) self._attr_supported_features = ( ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE ) @@ -313,22 +313,23 @@ class EvoController(EvoClimateEntity): It is assumed there is only one TCS per location, and they are thus synonymous. """ + _attr_icon = "mdi:thermostat" + _attr_precision = PRECISION_TENTHS + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, evo_broker, evo_device) -> None: """Initialize a Honeywell TCC Controller/Location.""" super().__init__(evo_broker, evo_device) - self._unique_id = evo_device.systemId - self._name = evo_device.location.name - self._icon = "mdi:thermostat" - - self._precision = PRECISION_TENTHS + self._attr_unique_id = evo_device.systemId + self._attr_name = evo_device.location.name modes = [m["systemMode"] for m in evo_broker.config["allowedSystemModes"]] - self._preset_modes = [ + self._attr_preset_modes = [ TCS_PRESET_TO_HA[m] for m in modes if m in list(TCS_PRESET_TO_HA) ] self._attr_supported_features = ( - ClimateEntityFeature.PRESET_MODE if self._preset_modes else 0 + ClimateEntityFeature.PRESET_MODE if self._attr_preset_modes else 0 ) async def async_tcs_svc_request(self, service: dict, data: dict) -> None: diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index f216862b232..ff54cfbe4a6 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -7,7 +7,13 @@ from homeassistant.components.water_heater import ( WaterHeaterEntity, WaterHeaterEntityFeature, ) -from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, STATE_OFF, STATE_ON +from homeassistant.const import ( + PRECISION_TENTHS, + PRECISION_WHOLE, + STATE_OFF, + STATE_ON, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -51,15 +57,20 @@ async def async_setup_platform( class EvoDHW(EvoChild, WaterHeaterEntity): """Base for a Honeywell TCC DHW controller (aka boiler).""" + _attr_name = "DHW controller" + _attr_icon = "mdi:thermometer-lines" + _attr_operation_list = list(HA_STATE_TO_EVO) + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, evo_broker, evo_device) -> None: """Initialize an evohome DHW controller.""" super().__init__(evo_broker, evo_device) - self._unique_id = evo_device.dhwId - self._name = "DHW controller" - self._icon = "mdi:thermometer-lines" + self._attr_unique_id = evo_device.dhwId - self._precision = PRECISION_TENTHS if evo_broker.client_v1 else PRECISION_WHOLE + self._attr_precision = ( + PRECISION_TENTHS if evo_broker.client_v1 else PRECISION_WHOLE + ) self._attr_supported_features = ( WaterHeaterEntityFeature.AWAY_MODE | WaterHeaterEntityFeature.OPERATION_MODE ) @@ -71,11 +82,6 @@ class EvoDHW(EvoChild, WaterHeaterEntity): return STATE_AUTO return EVO_STATE_TO_HA[self._evo_device.stateStatus["state"]] - @property - def operation_list(self) -> list[str]: - """Return the list of available operations.""" - return list(HA_STATE_TO_EVO) - @property def is_away_mode_on(self): """Return True if away mode is on.""" diff --git a/mypy.ini b/mypy.ini index ce02b366ad0..8335af1ed77 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2672,9 +2672,6 @@ ignore_errors = true [mypy-homeassistant.components.evohome.climate] ignore_errors = true -[mypy-homeassistant.components.evohome.water_heater] -ignore_errors = true - [mypy-homeassistant.components.icloud] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 8d36184290b..b492a70c8a9 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -25,7 +25,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.conversation.default_agent", "homeassistant.components.evohome", "homeassistant.components.evohome.climate", - "homeassistant.components.evohome.water_heater", "homeassistant.components.icloud", "homeassistant.components.icloud.account", "homeassistant.components.icloud.device_tracker", From e1e85caf189193cf048f401ecca8453cb13afbd8 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 11 Jul 2022 17:26:07 +0200 Subject: [PATCH 2462/3516] Migrate Nettigo Air Monitor to new entity naming style (#74993) --- homeassistant/components/nam/__init__.py | 3 +- homeassistant/components/nam/button.py | 6 +- homeassistant/components/nam/const.py | 175 -------------------- homeassistant/components/nam/sensor.py | 199 ++++++++++++++++++++++- 4 files changed, 203 insertions(+), 180 deletions(-) diff --git a/homeassistant/components/nam/__init__.py b/homeassistant/components/nam/__init__.py index 302c0af76d4..25615db6eed 100644 --- a/homeassistant/components/nam/__init__.py +++ b/homeassistant/components/nam/__init__.py @@ -30,7 +30,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import ( ATTR_SDS011, ATTR_SPS30, - DEFAULT_NAME, DEFAULT_UPDATE_INTERVAL, DOMAIN, MANUFACTURER, @@ -130,7 +129,7 @@ class NAMDataUpdateCoordinator(DataUpdateCoordinator): """Return the device info.""" return DeviceInfo( connections={(CONNECTION_NETWORK_MAC, cast(str, self._unique_id))}, - name=DEFAULT_NAME, + name="Nettigo Air Monitor", sw_version=self.nam.software_version, manufacturer=MANUFACTURER, configuration_url=f"http://{self.nam.host}/", diff --git a/homeassistant/components/nam/button.py b/homeassistant/components/nam/button.py index db5474ec925..6725ef3292d 100644 --- a/homeassistant/components/nam/button.py +++ b/homeassistant/components/nam/button.py @@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import NAMDataUpdateCoordinator -from .const import DEFAULT_NAME, DOMAIN +from .const import DOMAIN PARALLEL_UPDATES = 1 @@ -23,7 +23,7 @@ _LOGGER = logging.getLogger(__name__) RESTART_BUTTON: ButtonEntityDescription = ButtonEntityDescription( key="restart", - name=f"{DEFAULT_NAME} Restart", + name="Restart", device_class=ButtonDeviceClass.RESTART, entity_category=EntityCategory.CONFIG, ) @@ -44,6 +44,8 @@ async def async_setup_entry( class NAMButton(CoordinatorEntity[NAMDataUpdateCoordinator], ButtonEntity): """Define an Nettigo Air Monitor button.""" + _attr_has_entity_name = True + def __init__( self, coordinator: NAMDataUpdateCoordinator, diff --git a/homeassistant/components/nam/const.py b/homeassistant/components/nam/const.py index b81e7337a9f..e7c6f2532ef 100644 --- a/homeassistant/components/nam/const.py +++ b/homeassistant/components/nam/const.py @@ -4,21 +4,6 @@ from __future__ import annotations from datetime import timedelta from typing import Final -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import ( - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_MILLION, - PERCENTAGE, - PRESSURE_HPA, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - TEMP_CELSIUS, -) -from homeassistant.helpers.entity import EntityCategory - SUFFIX_P0: Final = "_p0" SUFFIX_P1: Final = "_p1" SUFFIX_P2: Final = "_p2" @@ -49,7 +34,6 @@ ATTR_SPS30_P2: Final = f"{ATTR_SPS30}{SUFFIX_P2}" ATTR_SPS30_P4: Final = f"{ATTR_SPS30}{SUFFIX_P4}" ATTR_UPTIME: Final = "uptime" -DEFAULT_NAME: Final = "Nettigo Air Monitor" DEFAULT_UPDATE_INTERVAL: Final = timedelta(minutes=6) DOMAIN: Final = "nam" MANUFACTURER: Final = "Nettigo" @@ -58,162 +42,3 @@ MIGRATION_SENSORS: Final = [ ("temperature", ATTR_DHT22_TEMPERATURE), ("humidity", ATTR_DHT22_HUMIDITY), ] - -SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( - SensorEntityDescription( - key=ATTR_BME280_HUMIDITY, - name=f"{DEFAULT_NAME} BME280 Humidity", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_BME280_PRESSURE, - name=f"{DEFAULT_NAME} BME280 Pressure", - native_unit_of_measurement=PRESSURE_HPA, - device_class=SensorDeviceClass.PRESSURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_BME280_TEMPERATURE, - name=f"{DEFAULT_NAME} BME280 Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_BMP180_PRESSURE, - name=f"{DEFAULT_NAME} BMP180 Pressure", - native_unit_of_measurement=PRESSURE_HPA, - device_class=SensorDeviceClass.PRESSURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_BMP180_TEMPERATURE, - name=f"{DEFAULT_NAME} BMP180 Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_BMP280_PRESSURE, - name=f"{DEFAULT_NAME} BMP280 Pressure", - native_unit_of_measurement=PRESSURE_HPA, - device_class=SensorDeviceClass.PRESSURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_BMP280_TEMPERATURE, - name=f"{DEFAULT_NAME} BMP280 Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_HECA_HUMIDITY, - name=f"{DEFAULT_NAME} HECA Humidity", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_HECA_TEMPERATURE, - name=f"{DEFAULT_NAME} HECA Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_MHZ14A_CARBON_DIOXIDE, - name=f"{DEFAULT_NAME} MH-Z14A Carbon Dioxide", - native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - device_class=SensorDeviceClass.CO2, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SDS011_P1, - name=f"{DEFAULT_NAME} SDS011 Particulate Matter 10", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=SensorDeviceClass.PM10, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SDS011_P2, - name=f"{DEFAULT_NAME} SDS011 Particulate Matter 2.5", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=SensorDeviceClass.PM25, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SHT3X_HUMIDITY, - name=f"{DEFAULT_NAME} SHT3X Humidity", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SHT3X_TEMPERATURE, - name=f"{DEFAULT_NAME} SHT3X Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SPS30_P0, - name=f"{DEFAULT_NAME} SPS30 Particulate Matter 1.0", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=SensorDeviceClass.PM1, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SPS30_P1, - name=f"{DEFAULT_NAME} SPS30 Particulate Matter 10", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=SensorDeviceClass.PM10, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SPS30_P2, - name=f"{DEFAULT_NAME} SPS30 Particulate Matter 2.5", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - device_class=SensorDeviceClass.PM25, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SPS30_P4, - name=f"{DEFAULT_NAME} SPS30 Particulate Matter 4.0", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - icon="mdi:molecule", - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_DHT22_HUMIDITY, - name=f"{DEFAULT_NAME} DHT22 Humidity", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_DHT22_TEMPERATURE, - name=f"{DEFAULT_NAME} DHT22 Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=ATTR_SIGNAL_STRENGTH, - name=f"{DEFAULT_NAME} Signal Strength", - native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - entity_category=EntityCategory.DIAGNOSTIC, - ), - SensorEntityDescription( - key=ATTR_UPTIME, - name=f"{DEFAULT_NAME} Uptime", - device_class=SensorDeviceClass.TIMESTAMP, - entity_registry_enabled_default=False, - entity_category=EntityCategory.DIAGNOSTIC, - ), -) diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index af729cf9066..6229102035e 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -7,24 +7,219 @@ from typing import cast from homeassistant.components.sensor import ( DOMAIN as PLATFORM, + SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + PERCENTAGE, + PRESSURE_HPA, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import utcnow from . import NAMDataUpdateCoordinator -from .const import ATTR_UPTIME, DOMAIN, MIGRATION_SENSORS, SENSORS +from .const import ( + ATTR_BME280_HUMIDITY, + ATTR_BME280_PRESSURE, + ATTR_BME280_TEMPERATURE, + ATTR_BMP180_PRESSURE, + ATTR_BMP180_TEMPERATURE, + ATTR_BMP280_PRESSURE, + ATTR_BMP280_TEMPERATURE, + ATTR_DHT22_HUMIDITY, + ATTR_DHT22_TEMPERATURE, + ATTR_HECA_HUMIDITY, + ATTR_HECA_TEMPERATURE, + ATTR_MHZ14A_CARBON_DIOXIDE, + ATTR_SDS011_P1, + ATTR_SDS011_P2, + ATTR_SHT3X_HUMIDITY, + ATTR_SHT3X_TEMPERATURE, + ATTR_SIGNAL_STRENGTH, + ATTR_SPS30_P0, + ATTR_SPS30_P1, + ATTR_SPS30_P2, + ATTR_SPS30_P4, + ATTR_UPTIME, + DOMAIN, + MIGRATION_SENSORS, +) PARALLEL_UPDATES = 1 _LOGGER = logging.getLogger(__name__) +SENSORS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key=ATTR_BME280_HUMIDITY, + name="BME280 humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BME280_PRESSURE, + name="BME280 pressure", + native_unit_of_measurement=PRESSURE_HPA, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BME280_TEMPERATURE, + name="BME280 temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BMP180_PRESSURE, + name="BMP180 pressure", + native_unit_of_measurement=PRESSURE_HPA, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BMP180_TEMPERATURE, + name="BMP180 temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BMP280_PRESSURE, + name="BMP280 pressure", + native_unit_of_measurement=PRESSURE_HPA, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_BMP280_TEMPERATURE, + name="BMP280 temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_HECA_HUMIDITY, + name="HECA humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_HECA_TEMPERATURE, + name="HECA temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_MHZ14A_CARBON_DIOXIDE, + name="MH-Z14A carbon dioxide", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SDS011_P1, + name="SDS011 particulate matter 10", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM10, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SDS011_P2, + name="SDS011 particulate matter 2.5", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SHT3X_HUMIDITY, + name="SHT3X humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SHT3X_TEMPERATURE, + name="SHT3X temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SPS30_P0, + name="SPS30 particulate matter 1.0", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM1, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SPS30_P1, + name="SPS30 particulate matter 10", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM10, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SPS30_P2, + name="SPS30 particulate matter 2.5", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM25, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SPS30_P4, + name="SPS30 particulate matter 4.0", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + icon="mdi:molecule", + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_DHT22_HUMIDITY, + name="DHT22 humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_DHT22_TEMPERATURE, + name="DHT22 temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key=ATTR_SIGNAL_STRENGTH, + name="Signal strength", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key=ATTR_UPTIME, + name="Uptime", + device_class=SensorDeviceClass.TIMESTAMP, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback @@ -61,6 +256,8 @@ async def async_setup_entry( class NAMSensor(CoordinatorEntity[NAMDataUpdateCoordinator], SensorEntity): """Define an Nettigo Air Monitor sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: NAMDataUpdateCoordinator, From 7b5cf63a4689199195b358a5d70a34dd0a4ee4dc Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 11 Jul 2022 17:30:22 +0200 Subject: [PATCH 2463/3516] Migrate Airly to new entity naming style (#74995) --- homeassistant/components/airly/const.py | 1 - homeassistant/components/airly/sensor.py | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index 801bca58412..8fddaea8ec2 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -25,7 +25,6 @@ SUFFIX_LIMIT: Final = "LIMIT" ATTRIBUTION: Final = "Data provided by Airly" CONF_USE_NEAREST: Final = "use_nearest" -DEFAULT_NAME: Final = "Airly" DOMAIN: Final = "airly" LABEL_ADVICE: Final = "advice" MANUFACTURER: Final = "Airly sp. z o.o." diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 9b647b93afa..eeb0037c814 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -45,7 +45,6 @@ from .const import ( ATTR_LIMIT, ATTR_PERCENT, ATTRIBUTION, - DEFAULT_NAME, DOMAIN, MANUFACTURER, SUFFIX_LIMIT, @@ -137,6 +136,7 @@ async def async_setup_entry( class AirlySensor(CoordinatorEntity[AirlyDataUpdateCoordinator], SensorEntity): """Define an Airly sensor.""" + _attr_has_entity_name = True entity_description: AirlySensorEntityDescription def __init__( @@ -151,12 +151,11 @@ class AirlySensor(CoordinatorEntity[AirlyDataUpdateCoordinator], SensorEntity): entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, f"{coordinator.latitude}-{coordinator.longitude}")}, manufacturer=MANUFACTURER, - name=DEFAULT_NAME, + name=name, configuration_url=URL.format( latitude=coordinator.latitude, longitude=coordinator.longitude ), ) - self._attr_name = f"{name} {description.name}" self._attr_unique_id = ( f"{coordinator.latitude}-{coordinator.longitude}-{description.key}".lower() ) From 6ac05784a63f7490f875959139ef903034bc45b0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:33:20 +0200 Subject: [PATCH 2464/3516] Remove icloud from mypy ignore list (#75007) --- homeassistant/components/icloud/__init__.py | 16 ++++--- homeassistant/components/icloud/account.py | 45 +++++++++---------- .../components/icloud/device_tracker.py | 6 +-- homeassistant/components/icloud/sensor.py | 4 +- mypy.ini | 12 ----- script/hassfest/mypy_config.py | 4 -- 6 files changed, 37 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 06028ebce6c..63802804f4d 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -1,4 +1,8 @@ """The iCloud component.""" +from __future__ import annotations + +from typing import Any + import voluptuous as vol from homeassistant.config_entries import ConfigEntry @@ -82,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry.unique_id is None: hass.config_entries.async_update_entry(entry, unique_id=username) - icloud_dir = Store(hass, STORAGE_VERSION, STORAGE_KEY) + icloud_dir = Store[Any](hass, STORAGE_VERSION, STORAGE_KEY) account = IcloudAccount( hass, @@ -103,7 +107,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def play_sound(service: ServiceCall) -> None: """Play sound on the device.""" account = service.data[ATTR_ACCOUNT] - device_name = service.data.get(ATTR_DEVICE_NAME) + device_name: str = service.data[ATTR_DEVICE_NAME] device_name = slugify(device_name.replace(" ", "", 99)) for device in _get_account(account).get_devices_with_name(device_name): @@ -112,7 +116,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def display_message(service: ServiceCall) -> None: """Display a message on the device.""" account = service.data[ATTR_ACCOUNT] - device_name = service.data.get(ATTR_DEVICE_NAME) + device_name: str = service.data[ATTR_DEVICE_NAME] device_name = slugify(device_name.replace(" ", "", 99)) message = service.data.get(ATTR_LOST_DEVICE_MESSAGE) sound = service.data.get(ATTR_LOST_DEVICE_SOUND, False) @@ -123,7 +127,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def lost_device(service: ServiceCall) -> None: """Make the device in lost state.""" account = service.data[ATTR_ACCOUNT] - device_name = service.data.get(ATTR_DEVICE_NAME) + device_name: str = service.data[ATTR_DEVICE_NAME] device_name = slugify(device_name.replace(" ", "", 99)) number = service.data.get(ATTR_LOST_DEVICE_NUMBER) message = service.data.get(ATTR_LOST_DEVICE_MESSAGE) @@ -139,11 +143,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: else: _get_account(account).keep_alive() - def _get_account(account_identifier: str) -> any: + def _get_account(account_identifier: str) -> IcloudAccount: if account_identifier is None: return None - icloud_account = hass.data[DOMAIN].get(account_identifier) + icloud_account: IcloudAccount | None = hass.data[DOMAIN].get(account_identifier) if icloud_account is None: for account in hass.data[DOMAIN].values(): if account.username == account_identifier: diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 95b90791165..4dc3c07aba7 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -91,21 +91,19 @@ class IcloudAccount: self._username = username self._password = password self._with_family = with_family - self._fetch_interval = max_interval + self._fetch_interval: float = max_interval self._max_interval = max_interval self._gps_accuracy_threshold = gps_accuracy_threshold self._icloud_dir = icloud_dir self.api: PyiCloudService | None = None - self._owner_fullname = None - self._family_members_fullname = {} - self._devices = {} + self._owner_fullname: str | None = None + self._family_members_fullname: dict[str, str] = {} + self._devices: dict[str, IcloudDevice] = {} self._retried_fetch = False self._config_entry = config_entry - self.listeners = [] - def setup(self) -> None: """Set up an iCloud account.""" try: @@ -271,6 +269,8 @@ class IcloudAccount: distances = [] for zone_state in zones: + if zone_state is None: + continue zone_state_lat = zone_state.attributes[DEVICE_LOCATION_LATITUDE] zone_state_long = zone_state.attributes[DEVICE_LOCATION_LONGITUDE] zone_distance = distance( @@ -279,7 +279,8 @@ class IcloudAccount: zone_state_lat, zone_state_long, ) - distances.append(round(zone_distance / 1000, 1)) + if zone_distance is not None: + distances.append(round(zone_distance / 1000, 1)) # Max interval if no zone if not distances: @@ -288,7 +289,7 @@ class IcloudAccount: # Calculate out how long it would take for the device to drive # to the nearest zone at 120 km/h: - interval = round(mindistance / 2, 0) + interval = round(mindistance / 2) # Never poll more than once per minute interval = max(interval, 1) @@ -324,7 +325,7 @@ class IcloudAccount: self.api.authenticate() self.update_devices() - def get_devices_with_name(self, name: str) -> [any]: + def get_devices_with_name(self, name: str) -> list[Any]: """Get devices by name.""" result = [] name_slug = slugify(name.replace(" ", "", 99)) @@ -341,7 +342,7 @@ class IcloudAccount: return self._username @property - def owner_fullname(self) -> str: + def owner_fullname(self) -> str | None: """Return the account owner fullname.""" return self._owner_fullname @@ -351,7 +352,7 @@ class IcloudAccount: return self._family_members_fullname @property - def fetch_interval(self) -> int: + def fetch_interval(self) -> float: """Return the account fetch interval.""" return self._fetch_interval @@ -386,14 +387,7 @@ class IcloudDevice: self._device_class = self._status[DEVICE_CLASS] self._device_model = self._status[DEVICE_DISPLAY_NAME] - if self._status[DEVICE_PERSON_ID]: - owner_fullname = account.family_members_fullname[ - self._status[DEVICE_PERSON_ID] - ] - else: - owner_fullname = account.owner_fullname - - self._battery_level = None + self._battery_level: int | None = None self._battery_status = None self._location = None @@ -402,8 +396,13 @@ class IcloudDevice: ATTR_ACCOUNT_FETCH_INTERVAL: self._account.fetch_interval, ATTR_DEVICE_NAME: self._device_model, ATTR_DEVICE_STATUS: None, - ATTR_OWNER_NAME: owner_fullname, } + if self._status[DEVICE_PERSON_ID]: + self._attrs[ATTR_OWNER_NAME] = account.family_members_fullname[ + self._status[DEVICE_PERSON_ID] + ] + elif account.owner_fullname is not None: + self._attrs[ATTR_OWNER_NAME] = account.owner_fullname def update(self, status) -> None: """Update the iCloud device.""" @@ -487,17 +486,17 @@ class IcloudDevice: return self._device_model @property - def battery_level(self) -> int: + def battery_level(self) -> int | None: """Return the Apple device battery level.""" return self._battery_level @property - def battery_status(self) -> str: + def battery_status(self) -> str | None: """Return the Apple device battery status.""" return self._battery_status @property - def location(self) -> dict[str, Any]: + def location(self) -> dict[str, Any] | None: """Return the Apple device location.""" return self._location diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index c9d251b06c7..6886d500a84 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -36,7 +36,7 @@ async def async_setup_entry( ) -> None: """Set up device tracker for iCloud component.""" account = hass.data[DOMAIN][entry.unique_id] - tracked = set() + tracked = set[str]() @callback def update_account(): @@ -51,7 +51,7 @@ async def async_setup_entry( @callback -def add_entities(account, async_add_entities, tracked): +def add_entities(account: IcloudAccount, async_add_entities, tracked): """Add new tracker entities from the account.""" new_tracked = [] @@ -101,7 +101,7 @@ class IcloudTrackerEntity(TrackerEntity): return self._device.location[DEVICE_LOCATION_LONGITUDE] @property - def battery_level(self) -> int: + def battery_level(self) -> int | None: """Return the battery level of the device.""" return self._device.battery_level diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 38ea3af62b6..6e415aa3350 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -21,7 +21,7 @@ async def async_setup_entry( ) -> None: """Set up device tracker for iCloud component.""" account = hass.data[DOMAIN][entry.unique_id] - tracked = set() + tracked = set[str]() @callback def update_account(): @@ -74,7 +74,7 @@ class IcloudDeviceBatterySensor(SensorEntity): return f"{self._device.name} battery state" @property - def native_value(self) -> int: + def native_value(self) -> int | None: """Battery state percentage.""" return self._device.battery_level diff --git a/mypy.ini b/mypy.ini index 8335af1ed77..65b51e9d1c9 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2672,18 +2672,6 @@ ignore_errors = true [mypy-homeassistant.components.evohome.climate] ignore_errors = true -[mypy-homeassistant.components.icloud] -ignore_errors = true - -[mypy-homeassistant.components.icloud.account] -ignore_errors = true - -[mypy-homeassistant.components.icloud.device_tracker] -ignore_errors = true - -[mypy-homeassistant.components.icloud.sensor] -ignore_errors = true - [mypy-homeassistant.components.lovelace] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index b492a70c8a9..17ef8edc529 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -25,10 +25,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.conversation.default_agent", "homeassistant.components.evohome", "homeassistant.components.evohome.climate", - "homeassistant.components.icloud", - "homeassistant.components.icloud.account", - "homeassistant.components.icloud.device_tracker", - "homeassistant.components.icloud.sensor", "homeassistant.components.lovelace", "homeassistant.components.lovelace.dashboard", "homeassistant.components.lovelace.resources", From 6fd47d035ea658aa93f28b4cd741ed7cfb399a3b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 11 Jul 2022 08:40:52 -0700 Subject: [PATCH 2465/3516] Add basic Rhasspy integration (#74942) Co-authored-by: Franck Nijhof --- .strict-typing | 1 + CODEOWNERS | 2 + homeassistant/components/rhasspy/__init__.py | 15 +++++++ .../components/rhasspy/config_flow.py | 29 ++++++++++++ homeassistant/components/rhasspy/const.py | 3 ++ .../components/rhasspy/manifest.json | 9 ++++ homeassistant/components/rhasspy/strings.json | 12 +++++ .../components/rhasspy/translations/en.json | 12 +++++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 +++++ tests/components/rhasspy/__init__.py | 1 + tests/components/rhasspy/test_config_flow.py | 44 +++++++++++++++++++ tests/components/rhasspy/test_init.py | 26 +++++++++++ 13 files changed, 166 insertions(+) create mode 100644 homeassistant/components/rhasspy/__init__.py create mode 100644 homeassistant/components/rhasspy/config_flow.py create mode 100644 homeassistant/components/rhasspy/const.py create mode 100644 homeassistant/components/rhasspy/manifest.json create mode 100644 homeassistant/components/rhasspy/strings.json create mode 100644 homeassistant/components/rhasspy/translations/en.json create mode 100644 tests/components/rhasspy/__init__.py create mode 100644 tests/components/rhasspy/test_config_flow.py create mode 100644 tests/components/rhasspy/test_init.py diff --git a/.strict-typing b/.strict-typing index becd5cdda9a..9792f401ac4 100644 --- a/.strict-typing +++ b/.strict-typing @@ -193,6 +193,7 @@ homeassistant.components.recorder.* homeassistant.components.remote.* homeassistant.components.renault.* homeassistant.components.resolution_center.* +homeassistant.components.rhasspy.* homeassistant.components.ridwell.* homeassistant.components.rituals_perfume_genie.* homeassistant.components.roku.* diff --git a/CODEOWNERS b/CODEOWNERS index 4e5d07f54d8..a473d07af9d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -865,6 +865,8 @@ build.json @home-assistant/supervisor /tests/components/rflink/ @javicalle /homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221 /tests/components/rfxtrx/ @danielhiversen @elupus @RobBie1221 +/homeassistant/components/rhasspy/ @balloob @synesthesiam +/tests/components/rhasspy/ @balloob @synesthesiam /homeassistant/components/ridwell/ @bachya /tests/components/ridwell/ @bachya /homeassistant/components/ring/ @balloob diff --git a/homeassistant/components/rhasspy/__init__.py b/homeassistant/components/rhasspy/__init__.py new file mode 100644 index 00000000000..669d81952d4 --- /dev/null +++ b/homeassistant/components/rhasspy/__init__.py @@ -0,0 +1,15 @@ +"""The Rhasspy integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Rhasspy from a config entry.""" + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return True diff --git a/homeassistant/components/rhasspy/config_flow.py b/homeassistant/components/rhasspy/config_flow.py new file mode 100644 index 00000000000..69ed802f817 --- /dev/null +++ b/homeassistant/components/rhasspy/config_flow.py @@ -0,0 +1,29 @@ +"""Config flow for Rhasspy integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Rhasspy.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if user_input is None: + return self.async_show_form(step_id="user", data_schema=vol.Schema({})) + + return self.async_create_entry(title="Rhasspy", data={}) diff --git a/homeassistant/components/rhasspy/const.py b/homeassistant/components/rhasspy/const.py new file mode 100644 index 00000000000..127f20032ac --- /dev/null +++ b/homeassistant/components/rhasspy/const.py @@ -0,0 +1,3 @@ +"""Constants for the Rhasspy integration.""" + +DOMAIN = "rhasspy" diff --git a/homeassistant/components/rhasspy/manifest.json b/homeassistant/components/rhasspy/manifest.json new file mode 100644 index 00000000000..8b11e231b8c --- /dev/null +++ b/homeassistant/components/rhasspy/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "rhasspy", + "name": "Rhasspy", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/rhasspy", + "dependencies": ["intent"], + "codeowners": ["@balloob", "@synesthesiam"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/rhasspy/strings.json b/homeassistant/components/rhasspy/strings.json new file mode 100644 index 00000000000..4d2111ebd8a --- /dev/null +++ b/homeassistant/components/rhasspy/strings.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "description": "Do you want to enable Rhasspy support?" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + } +} diff --git a/homeassistant/components/rhasspy/translations/en.json b/homeassistant/components/rhasspy/translations/en.json new file mode 100644 index 00000000000..af826fbf27d --- /dev/null +++ b/homeassistant/components/rhasspy/translations/en.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "user": { + "description": "Do you want to enable Rhasspy support?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d3be2c5e674..3c02842e856 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -290,6 +290,7 @@ FLOWS = { "recollect_waste", "renault", "rfxtrx", + "rhasspy", "ridwell", "ring", "risco", diff --git a/mypy.ini b/mypy.ini index 65b51e9d1c9..1fb12c3a47e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1886,6 +1886,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.rhasspy.*] +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.ridwell.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/rhasspy/__init__.py b/tests/components/rhasspy/__init__.py new file mode 100644 index 00000000000..521c5166040 --- /dev/null +++ b/tests/components/rhasspy/__init__.py @@ -0,0 +1 @@ +"""Tests for the Rhasspy integration.""" diff --git a/tests/components/rhasspy/test_config_flow.py b/tests/components/rhasspy/test_config_flow.py new file mode 100644 index 00000000000..53c82c0cecd --- /dev/null +++ b/tests/components/rhasspy/test_config_flow.py @@ -0,0 +1,44 @@ +"""Test the Rhasspy config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.rhasspy.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.rhasspy.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Rhasspy" + assert result2["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_single_entry(hass: HomeAssistant) -> None: + """Test we only allow single entry.""" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/rhasspy/test_init.py b/tests/components/rhasspy/test_init.py new file mode 100644 index 00000000000..e4f0b346347 --- /dev/null +++ b/tests/components/rhasspy/test_init.py @@ -0,0 +1,26 @@ +"""Tests for the Rhasspy integration.""" +from homeassistant.components.rhasspy.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry(hass: HomeAssistant) -> None: + """Test the Rhasspy configuration entry loading/unloading.""" + mock_config_entry = MockConfigEntry( + title="Rhasspy", + domain=DOMAIN, + data={}, + ) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED From 01ca7f657ca16f2a6b5c8818f55fbc84a0f6ad3a Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 12 Jul 2022 01:43:32 +1000 Subject: [PATCH 2466/3516] Shorten Entity Name in Aussie Broadband (#74946) --- homeassistant/components/aussie_broadband/sensor.py | 3 ++- tests/components/aussie_broadband/common.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aussie_broadband/sensor.py b/homeassistant/components/aussie_broadband/sensor.py index ecc891deba9..648abaccb0c 100644 --- a/homeassistant/components/aussie_broadband/sensor.py +++ b/homeassistant/components/aussie_broadband/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +import re from typing import Any, cast from homeassistant.components.sensor import ( @@ -152,7 +153,7 @@ class AussieBroadandSensorEntity(CoordinatorEntity, SensorEntity): identifiers={(DOMAIN, service[SERVICE_ID])}, manufacturer="Aussie Broadband", configuration_url=f"https://my.aussiebroadband.com.au/#/{service['name'].lower()}/{service[SERVICE_ID]}/", - name=service["description"], + name=re.sub(r" - AVC\d+$", "", service["description"]), model=service["name"], ) diff --git a/tests/components/aussie_broadband/common.py b/tests/components/aussie_broadband/common.py index abb99355ef3..5d050388b38 100644 --- a/tests/components/aussie_broadband/common.py +++ b/tests/components/aussie_broadband/common.py @@ -12,7 +12,7 @@ from tests.common import MockConfigEntry FAKE_SERVICES = [ { "service_id": "12345678", - "description": "Fake ABB NBN Service", + "description": "Fake ABB NBN Service - AVC123456789", "type": "NBN", "name": "NBN", }, From 924dce1b86c271513fc03985ddba7eab677a8a6b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Jul 2022 17:44:07 +0200 Subject: [PATCH 2467/3516] Log warning if number entities set _attr_unit_of_measurement (#74987) --- homeassistant/components/number/__init__.py | 4 +++- homeassistant/components/zha/number.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 1820e28bc4c..8c1800697a3 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -192,9 +192,10 @@ class NumberEntity(Entity): entity_description: NumberEntityDescription _attr_max_value: None _attr_min_value: None + _attr_mode: NumberMode = NumberMode.AUTO _attr_state: None = None _attr_step: None - _attr_mode: NumberMode = NumberMode.AUTO + _attr_unit_of_measurement: None # Subclasses of NumberEntity should not set this _attr_value: None _attr_native_max_value: float _attr_native_min_value: float @@ -369,6 +370,7 @@ class NumberEntity(Entity): return self._number_option_unit_of_measurement if hasattr(self, "_attr_unit_of_measurement"): + self._report_deprecated_number_entity() return self._attr_unit_of_measurement if ( hasattr(self, "entity_description") diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 36fc5267bd9..76b1121c2f0 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -521,7 +521,7 @@ class TimerDurationMinutes(ZHANumberConfigurationEntity, id_suffix="timer_durati _attr_icon: str = ICONS[14] _attr_native_min_value: float = 0x00 _attr_native_max_value: float = 0x257 - _attr_unit_of_measurement: str | None = UNITS[72] + _attr_native_unit_of_measurement: str | None = UNITS[72] _zcl_attribute: str = "timer_duration" @@ -533,5 +533,5 @@ class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time") _attr_icon: str = ICONS[14] _attr_native_min_value: float = 0x00 _attr_native_max_value: float = 0xFFFFFFFF - _attr_unit_of_measurement: str | None = UNITS[72] + _attr_native_unit_of_measurement: str | None = UNITS[72] _zcl_attribute: str = "filter_life_time" From 63706d2f67f47de2fa5fe62fc2be0555f2d4658b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:46:32 +0200 Subject: [PATCH 2468/3516] Remove blueprint from mypy ignore list (#74990) --- homeassistant/components/blueprint/errors.py | 10 ++++++---- homeassistant/components/blueprint/importer.py | 11 +++++++---- homeassistant/components/blueprint/models.py | 13 +++++++------ homeassistant/components/blueprint/websocket_api.py | 8 ++++---- mypy.ini | 9 --------- script/hassfest/mypy_config.py | 3 --- 6 files changed, 24 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/blueprint/errors.py b/homeassistant/components/blueprint/errors.py index 4b14201652f..aceca533d23 100644 --- a/homeassistant/components/blueprint/errors.py +++ b/homeassistant/components/blueprint/errors.py @@ -13,7 +13,7 @@ from homeassistant.exceptions import HomeAssistantError class BlueprintException(HomeAssistantError): """Base exception for blueprint errors.""" - def __init__(self, domain: str, msg: str) -> None: + def __init__(self, domain: str | None, msg: str) -> None: """Initialize a blueprint exception.""" super().__init__(msg) self.domain = domain @@ -22,7 +22,9 @@ class BlueprintException(HomeAssistantError): class BlueprintWithNameException(BlueprintException): """Base exception for blueprint errors.""" - def __init__(self, domain: str, blueprint_name: str, msg: str) -> None: + def __init__( + self, domain: str | None, blueprint_name: str | None, msg: str + ) -> None: """Initialize blueprint exception.""" super().__init__(domain, msg) self.blueprint_name = blueprint_name @@ -41,8 +43,8 @@ class InvalidBlueprint(BlueprintWithNameException): def __init__( self, - domain: str, - blueprint_name: str, + domain: str | None, + blueprint_name: str | None, blueprint_data: Any, msg_or_exc: vol.Invalid, ) -> None: diff --git a/homeassistant/components/blueprint/importer.py b/homeassistant/components/blueprint/importer.py index de39741d8ed..f8b37a97c31 100644 --- a/homeassistant/components/blueprint/importer.py +++ b/homeassistant/components/blueprint/importer.py @@ -91,12 +91,12 @@ def _get_community_post_import_url(url: str) -> str: def _extract_blueprint_from_community_topic( url: str, topic: dict, -) -> ImportedBlueprint | None: +) -> ImportedBlueprint: """Extract a blueprint from a community post JSON. Async friendly. """ - block_content = None + block_content: str blueprint = None post = topic["post_stream"]["posts"][0] @@ -118,6 +118,7 @@ def _extract_blueprint_from_community_topic( if not is_blueprint_config(data): continue + assert isinstance(data, dict) blueprint = Blueprint(data) break @@ -134,7 +135,7 @@ def _extract_blueprint_from_community_topic( async def fetch_blueprint_from_community_post( hass: HomeAssistant, url: str -) -> ImportedBlueprint | None: +) -> ImportedBlueprint: """Get blueprints from a community post url. Method can raise aiohttp client exceptions, vol.Invalid. @@ -160,6 +161,7 @@ async def fetch_blueprint_from_github_url( resp = await session.get(import_url, raise_for_status=True) raw_yaml = await resp.text() data = yaml.parse_yaml(raw_yaml) + assert isinstance(data, dict) blueprint = Blueprint(data) parsed_import_url = yarl.URL(import_url) @@ -189,7 +191,7 @@ async def fetch_blueprint_from_github_gist_url( blueprint = None filename = None - content = None + content: str for filename, info in gist["files"].items(): if not filename.endswith(".yaml"): @@ -200,6 +202,7 @@ async def fetch_blueprint_from_github_gist_url( if not is_blueprint_config(data): continue + assert isinstance(data, dict) blueprint = Blueprint(data) break diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py index a8146764710..0d90c663b4f 100644 --- a/homeassistant/components/blueprint/models.py +++ b/homeassistant/components/blueprint/models.py @@ -188,7 +188,7 @@ class DomainBlueprints: self.hass = hass self.domain = domain self.logger = logger - self._blueprints = {} + self._blueprints: dict[str, Blueprint | None] = {} self._load_lock = asyncio.Lock() hass.data.setdefault(DOMAIN, {})[domain] = self @@ -216,19 +216,20 @@ class DomainBlueprints: except HomeAssistantError as err: raise FailedToLoad(self.domain, blueprint_path, err) from err + assert isinstance(blueprint_data, dict) return Blueprint( blueprint_data, expected_domain=self.domain, path=blueprint_path ) - def _load_blueprints(self) -> dict[str, Blueprint | BlueprintException]: + def _load_blueprints(self) -> dict[str, Blueprint | BlueprintException | None]: """Load all the blueprints.""" blueprint_folder = pathlib.Path( self.hass.config.path(BLUEPRINT_FOLDER, self.domain) ) - results = {} + results: dict[str, Blueprint | BlueprintException | None] = {} - for blueprint_path in blueprint_folder.glob("**/*.yaml"): - blueprint_path = str(blueprint_path.relative_to(blueprint_folder)) + for path in blueprint_folder.glob("**/*.yaml"): + blueprint_path = str(path.relative_to(blueprint_folder)) if self._blueprints.get(blueprint_path) is None: try: self._blueprints[blueprint_path] = self._load_blueprint( @@ -245,7 +246,7 @@ class DomainBlueprints: async def async_get_blueprints( self, - ) -> dict[str, Blueprint | BlueprintException]: + ) -> dict[str, Blueprint | BlueprintException | None]: """Get all the blueprints.""" async with self._load_lock: return await self.hass.async_add_executor_job(self._load_blueprints) diff --git a/homeassistant/components/blueprint/websocket_api.py b/homeassistant/components/blueprint/websocket_api.py index b8a4c214a2e..0b84d1d08c2 100644 --- a/homeassistant/components/blueprint/websocket_api.py +++ b/homeassistant/components/blueprint/websocket_api.py @@ -24,13 +24,13 @@ def async_setup(hass: HomeAssistant): websocket_api.async_register_command(hass, ws_delete_blueprint) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "blueprint/list", vol.Required("domain"): cv.string, } ) +@websocket_api.async_response async def ws_list_blueprints(hass, connection, msg): """List available blueprints.""" domain_blueprints: dict[str, models.DomainBlueprints] | None = hass.data.get( @@ -55,13 +55,13 @@ async def ws_list_blueprints(hass, connection, msg): connection.send_result(msg["id"], results) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "blueprint/import", vol.Required("url"): cv.url, } ) +@websocket_api.async_response async def ws_import_blueprint(hass, connection, msg): """Import a blueprint.""" async with async_timeout.timeout(10): @@ -86,7 +86,6 @@ async def ws_import_blueprint(hass, connection, msg): ) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "blueprint/save", @@ -96,6 +95,7 @@ async def ws_import_blueprint(hass, connection, msg): vol.Optional("source_url"): cv.url, } ) +@websocket_api.async_response async def ws_save_blueprint(hass, connection, msg): """Save a blueprint.""" @@ -135,7 +135,6 @@ async def ws_save_blueprint(hass, connection, msg): ) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "blueprint/delete", @@ -143,6 +142,7 @@ async def ws_save_blueprint(hass, connection, msg): vol.Required("path"): cv.path, } ) +@websocket_api.async_response async def ws_delete_blueprint(hass, connection, msg): """Delete a blueprint.""" diff --git a/mypy.ini b/mypy.ini index 1fb12c3a47e..4b74049f084 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2656,15 +2656,6 @@ no_implicit_optional = false warn_return_any = false warn_unreachable = false -[mypy-homeassistant.components.blueprint.importer] -ignore_errors = true - -[mypy-homeassistant.components.blueprint.models] -ignore_errors = true - -[mypy-homeassistant.components.blueprint.websocket_api] -ignore_errors = true - [mypy-homeassistant.components.cloud.client] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 17ef8edc529..a2c0fb37629 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -16,9 +16,6 @@ from .model import Config, Integration # remove your component from this list to enable type checks. # Do your best to not add anything new here. IGNORED_MODULES: Final[list[str]] = [ - "homeassistant.components.blueprint.importer", - "homeassistant.components.blueprint.models", - "homeassistant.components.blueprint.websocket_api", "homeassistant.components.cloud.client", "homeassistant.components.cloud.http_api", "homeassistant.components.conversation", From 7e0515b11931f5957c4fa2c03f3185710dfc96a8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:47:33 +0200 Subject: [PATCH 2469/3516] Remove conversation from mypy ignore list (#74991) --- homeassistant/components/conversation/__init__.py | 6 +++--- homeassistant/components/conversation/agent.py | 2 +- homeassistant/components/conversation/default_agent.py | 4 +++- mypy.ini | 6 ------ script/hassfest/mypy_config.py | 2 -- 5 files changed, 7 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index d04878cae4e..9fd6d1ad3e2 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -80,10 +80,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -@websocket_api.async_response @websocket_api.websocket_command( {"type": "conversation/process", "text": str, vol.Optional("conversation_id"): str} ) +@websocket_api.async_response async def websocket_process(hass, connection, msg): """Process text.""" connection.send_result( @@ -94,8 +94,8 @@ async def websocket_process(hass, connection, msg): ) -@websocket_api.async_response @websocket_api.websocket_command({"type": "conversation/agent/info"}) +@websocket_api.async_response async def websocket_get_agent_info(hass, connection, msg): """Do we need onboarding.""" agent = await _get_agent(hass) @@ -109,8 +109,8 @@ async def websocket_get_agent_info(hass, connection, msg): ) -@websocket_api.async_response @websocket_api.websocket_command({"type": "conversation/onboarding/set", "shown": bool}) +@websocket_api.async_response async def websocket_set_onboarding(hass, connection, msg): """Set onboarding status.""" agent = await _get_agent(hass) diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py index 56cf4aecdea..a19ae6d697b 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/agent.py @@ -26,5 +26,5 @@ class AbstractConversationAgent(ABC): @abstractmethod async def async_process( self, text: str, context: Context, conversation_id: str | None = None - ) -> intent.IntentResponse: + ) -> intent.IntentResponse | None: """Process a sentence.""" diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 6d8d29ab086..9079f7893ec 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -112,7 +112,7 @@ class DefaultAgent(AbstractConversationAgent): async def async_process( self, text: str, context: core.Context, conversation_id: str | None = None - ) -> intent.IntentResponse: + ) -> intent.IntentResponse | None: """Process a sentence.""" intents = self.hass.data[DOMAIN] @@ -129,3 +129,5 @@ class DefaultAgent(AbstractConversationAgent): text, context, ) + + return None diff --git a/mypy.ini b/mypy.ini index 4b74049f084..cccfdc55091 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2662,12 +2662,6 @@ ignore_errors = true [mypy-homeassistant.components.cloud.http_api] ignore_errors = true -[mypy-homeassistant.components.conversation] -ignore_errors = true - -[mypy-homeassistant.components.conversation.default_agent] -ignore_errors = true - [mypy-homeassistant.components.evohome] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index a2c0fb37629..8ddfa2b53a4 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -18,8 +18,6 @@ from .model import Config, Integration IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.cloud.client", "homeassistant.components.cloud.http_api", - "homeassistant.components.conversation", - "homeassistant.components.conversation.default_agent", "homeassistant.components.evohome", "homeassistant.components.evohome.climate", "homeassistant.components.lovelace", From 964bb63da66c4e59eb2e88bb34070643a1935881 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 18:12:56 +0200 Subject: [PATCH 2470/3516] Migrate Spotify to new entity naming style (#74992) --- homeassistant/components/spotify/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 7d24f1deee8..04f523c2d4b 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -107,10 +107,11 @@ def spotify_exception_handler(func): class SpotifyMediaPlayer(MediaPlayerEntity): """Representation of a Spotify controller.""" + _attr_entity_registry_enabled_default = False + _attr_has_entity_name = True _attr_icon = "mdi:spotify" _attr_media_content_type = MEDIA_TYPE_MUSIC _attr_media_image_remotely_accessible = False - _attr_entity_registry_enabled_default = False def __init__( self, @@ -122,7 +123,6 @@ class SpotifyMediaPlayer(MediaPlayerEntity): self._id = user_id self.data = data - self._attr_name = f"Spotify {name}" self._attr_unique_id = user_id if self.data.current_user["product"] == "premium": From a30d7e51044eeef99a3e4cb368a626fee098116c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 18:13:09 +0200 Subject: [PATCH 2471/3516] Migrate Speedtest.net to new entity naming style (#75004) --- homeassistant/components/speedtestdotnet/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index 4f16c012fa6..44b018e1e19 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -49,6 +49,7 @@ class SpeedtestSensor( """Implementation of a speedtest.net sensor.""" entity_description: SpeedtestSensorEntityDescription + _attr_has_entity_name = True _attr_icon = ICON def __init__( @@ -59,7 +60,6 @@ class SpeedtestSensor( """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = f"{DEFAULT_NAME} {description.name}" self._attr_unique_id = description.key self._state: StateType = None self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} From 4a39087fe7be56b463ff9d67360531554bc2a1c4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 18:14:17 +0200 Subject: [PATCH 2472/3516] Migrate Verisure to new entity naming style (#74997) --- .../verisure/alarm_control_panel.py | 2 +- .../components/verisure/binary_sensor.py | 5 ++-- homeassistant/components/verisure/camera.py | 3 ++- homeassistant/components/verisure/lock.py | 4 ++-- homeassistant/components/verisure/sensor.py | 24 +++++-------------- homeassistant/components/verisure/switch.py | 4 ++-- 6 files changed, 16 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 8f92ee0a5dd..5030e01c8b1 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -33,7 +33,7 @@ class VerisureAlarm( """Representation of a Verisure alarm status.""" _attr_code_format = CodeFormat.NUMBER - _attr_name = "Verisure Alarm" + _attr_has_entity_name = True _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index 217890b8a01..45e29cfa5f1 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -39,13 +39,13 @@ class VerisureDoorWindowSensor( """Representation of a Verisure door window sensor.""" _attr_device_class = BinarySensorDeviceClass.OPENING + _attr_has_entity_name = True def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: """Initialize the Verisure door window sensor.""" super().__init__(coordinator) - self._attr_name = coordinator.data["door_window"][serial_number]["area"] self._attr_unique_id = f"{serial_number}_door_window" self.serial_number = serial_number @@ -84,9 +84,10 @@ class VerisureEthernetStatus( ): """Representation of a Verisure VBOX internet status.""" - _attr_name = "Verisure Ethernet status" _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_has_entity_name = True + _attr_name = "Ethernet status" @property def unique_id(self) -> str: diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index c753bf2c5dc..98ed41c5b9f 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -46,6 +46,8 @@ async def async_setup_entry( class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], Camera): """Representation of a Verisure camera.""" + _attr_has_entity_name = True + def __init__( self, coordinator: VerisureDataUpdateCoordinator, @@ -56,7 +58,6 @@ class VerisureSmartcam(CoordinatorEntity[VerisureDataUpdateCoordinator], Camera) super().__init__(coordinator) Camera.__init__(self) - self._attr_name = coordinator.data["cameras"][serial_number]["area"] self._attr_unique_id = serial_number self.serial_number = serial_number diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 8074cf28f32..02cdad158ca 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -59,13 +59,13 @@ async def async_setup_entry( class VerisureDoorlock(CoordinatorEntity[VerisureDataUpdateCoordinator], LockEntity): """Representation of a Verisure doorlock.""" + _attr_has_entity_name = True + def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: """Initialize the Verisure lock.""" super().__init__(coordinator) - - self._attr_name = coordinator.data["locks"][serial_number]["area"] self._attr_unique_id = serial_number self.serial_number = serial_number diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index 3b8f722c6f7..676082e4cda 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -51,6 +51,8 @@ class VerisureThermometer( """Representation of a Verisure thermometer.""" _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_has_entity_name = True + _attr_name = "Temperature" _attr_native_unit_of_measurement = TEMP_CELSIUS _attr_state_class = SensorStateClass.MEASUREMENT @@ -62,12 +64,6 @@ class VerisureThermometer( self._attr_unique_id = f"{serial_number}_temperature" self.serial_number = serial_number - @property - def name(self) -> str: - """Return the name of the entity.""" - name = self.coordinator.data["climate"][self.serial_number]["deviceArea"] - return f"{name} Temperature" - @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" @@ -106,6 +102,8 @@ class VerisureHygrometer( """Representation of a Verisure hygrometer.""" _attr_device_class = SensorDeviceClass.HUMIDITY + _attr_has_entity_name = True + _attr_name = "Humidity" _attr_native_unit_of_measurement = PERCENTAGE _attr_state_class = SensorStateClass.MEASUREMENT @@ -117,12 +115,6 @@ class VerisureHygrometer( self._attr_unique_id = f"{serial_number}_humidity" self.serial_number = serial_number - @property - def name(self) -> str: - """Return the name of the entity.""" - name = self.coordinator.data["climate"][self.serial_number]["deviceArea"] - return f"{name} Humidity" - @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" @@ -160,6 +152,8 @@ class VerisureMouseDetection( ): """Representation of a Verisure mouse detector.""" + _attr_name = "Mouse" + _attr_has_entity_name = True _attr_native_unit_of_measurement = "Mice" def __init__( @@ -170,12 +164,6 @@ class VerisureMouseDetection( self._attr_unique_id = f"{serial_number}_mice" self.serial_number = serial_number - @property - def name(self) -> str: - """Return the name of the entity.""" - name = self.coordinator.data["mice"][self.serial_number]["area"] - return f"{name} Mouse" - @property def device_info(self) -> DeviceInfo: """Return device information about this entity.""" diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 5d1fd728f4a..177beb4272b 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -30,13 +30,13 @@ async def async_setup_entry( class VerisureSmartplug(CoordinatorEntity[VerisureDataUpdateCoordinator], SwitchEntity): """Representation of a Verisure smartplug.""" + _attr_has_entity_name = True + def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: """Initialize the Verisure device.""" super().__init__(coordinator) - - self._attr_name = coordinator.data["smart_plugs"][serial_number]["area"] self._attr_unique_id = serial_number self.serial_number = serial_number From 75abf87611d8ad5627126a3fe09fdddc8402237c Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Mon, 11 Jul 2022 18:16:29 +0200 Subject: [PATCH 2473/3516] Migrate Fronius to new entity naming style (#74974) --- homeassistant/components/fronius/sensor.py | 32 +- tests/components/fronius/test_sensor.py | 541 +++++++++------------ 2 files changed, 234 insertions(+), 339 deletions(-) diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index c3b219c4b22..35881225b68 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any, Final from homeassistant.components.sensor import ( - DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -109,14 +108,14 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="current_ac", - name="AC current", + name="Current AC", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="current_dc", - name="DC current", + name="Current DC", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, @@ -124,7 +123,7 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="current_dc_2", - name="DC current 2", + name="Current DC 2", native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, @@ -132,14 +131,14 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="power_ac", - name="AC power", + name="Power AC", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="voltage_ac", - name="AC voltage", + name="Voltage AC", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -147,7 +146,7 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="voltage_dc", - name="DC voltage", + name="Voltage DC", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -155,7 +154,7 @@ INVERTER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="voltage_dc_2", - name="DC voltage 2", + name="Voltage DC 2", native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -492,7 +491,7 @@ OHMPILOT_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="temperature_channel_1", - name="Temperature Channel 1", + name="Temperature channel 1", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -541,7 +540,7 @@ POWER_FLOW_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ ), SensorEntityDescription( key="meter_mode", - name="Mode", + name="Meter mode", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( @@ -656,7 +655,8 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn """Defines a Fronius coordinator entity.""" entity_descriptions: list[SensorEntityDescription] - _entity_id_prefix: str + + _attr_has_entity_name = True def __init__( self, @@ -669,10 +669,6 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn self.entity_description = next( desc for desc in self.entity_descriptions if desc.key == key ) - # default entity_id added 2021.12 - # used for migration from non-unique_id entities of previous integration implementation - # when removed after migration period `_entity_id_prefix` will also no longer be needed - self.entity_id = f"{SENSOR_DOMAIN}.{key}_{DOMAIN}_{self._entity_id_prefix}_{coordinator.solar_net.host}" self.solar_net_id = solar_net_id self._attr_native_value = self._get_entity_value() @@ -709,7 +705,6 @@ class InverterSensor(_FroniusSensorEntity): solar_net_id: str, ) -> None: """Set up an individual Fronius inverter sensor.""" - self._entity_id_prefix = f"inverter_{solar_net_id}" super().__init__(coordinator, key, solar_net_id) # device_info created in __init__ from a `GetInverterInfo` request self._attr_device_info = coordinator.inverter_info.device_info @@ -720,7 +715,6 @@ class LoggerSensor(_FroniusSensorEntity): """Defines a Fronius logger device sensor entity.""" entity_descriptions = LOGGER_ENTITY_DESCRIPTIONS - _entity_id_prefix = "logger_info_0" def __init__( self, @@ -749,7 +743,6 @@ class MeterSensor(_FroniusSensorEntity): solar_net_id: str, ) -> None: """Set up an individual Fronius meter sensor.""" - self._entity_id_prefix = f"meter_{solar_net_id}" super().__init__(coordinator, key, solar_net_id) meter_data = self._device_data() # S0 meters connected directly to inverters respond "n.a." as serial number @@ -782,7 +775,6 @@ class OhmpilotSensor(_FroniusSensorEntity): solar_net_id: str, ) -> None: """Set up an individual Fronius meter sensor.""" - self._entity_id_prefix = f"ohmpilot_{solar_net_id}" super().__init__(coordinator, key, solar_net_id) device_data = self._device_data() @@ -801,7 +793,6 @@ class PowerFlowSensor(_FroniusSensorEntity): """Defines a Fronius power flow sensor entity.""" entity_descriptions = POWER_FLOW_ENTITY_DESCRIPTIONS - _entity_id_prefix = "power_flow_0" def __init__( self, @@ -830,7 +821,6 @@ class StorageSensor(_FroniusSensorEntity): solar_net_id: str, ) -> None: """Set up an individual Fronius storage sensor.""" - self._entity_id_prefix = f"storage_{solar_net_id}" super().__init__(coordinator, key, solar_net_id) storage_data = self._device_data() diff --git a/tests/components/fronius/test_sensor.py b/tests/components/fronius/test_sensor.py index 0f3e8f28a56..3ed1e37505c 100644 --- a/tests/components/fronius/test_sensor.py +++ b/tests/components/fronius/test_sensor.py @@ -30,11 +30,11 @@ async def test_symo_inverter(hass, aioclient_mock): hass, config_entry.entry_id, FroniusInverterUpdateCoordinator.default_interval ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52 - assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 0) - assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 10828) - assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 44186900) - assert_state("sensor.energy_year_fronius_inverter_1_http_fronius", 25507686) - assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 16) + assert_state("sensor.symo_20_current_dc", 0) + assert_state("sensor.symo_20_energy_day", 10828) + assert_state("sensor.symo_20_energy_total", 44186900) + assert_state("sensor.symo_20_energy_year", 25507686) + assert_state("sensor.symo_20_voltage_dc", 16) # Second test at daytime when inverter is producing mock_responses(aioclient_mock, night=False) @@ -48,15 +48,15 @@ async def test_symo_inverter(hass, aioclient_mock): ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58 # 4 additional AC entities - assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 2.19) - assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 1113) - assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 44188000) - assert_state("sensor.energy_year_fronius_inverter_1_http_fronius", 25508798) - assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 518) - assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 5.19) - assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 49.94) - assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 1190) - assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 227.90) + assert_state("sensor.symo_20_current_dc", 2.19) + assert_state("sensor.symo_20_energy_day", 1113) + assert_state("sensor.symo_20_energy_total", 44188000) + assert_state("sensor.symo_20_energy_year", 25508798) + assert_state("sensor.symo_20_voltage_dc", 518) + assert_state("sensor.symo_20_current_ac", 5.19) + assert_state("sensor.symo_20_frequency_ac", 49.94) + assert_state("sensor.symo_20_power_ac", 1190) + assert_state("sensor.symo_20_voltage_ac", 227.90) # Third test at nighttime - additional AC entities aren't changed mock_responses(aioclient_mock, night=True) @@ -64,10 +64,10 @@ async def test_symo_inverter(hass, aioclient_mock): hass, dt.utcnow() + FroniusInverterUpdateCoordinator.default_interval ) await hass.async_block_till_done() - assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 5.19) - assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 49.94) - assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 1190) - assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 227.90) + assert_state("sensor.symo_20_current_ac", 5.19) + assert_state("sensor.symo_20_frequency_ac", 49.94) + assert_state("sensor.symo_20_power_ac", 1190) + assert_state("sensor.symo_20_voltage_ac", 227.90) async def test_symo_logger(hass, aioclient_mock): @@ -82,18 +82,9 @@ async def test_symo_logger(hass, aioclient_mock): await setup_fronius_integration(hass) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 24 # states are rounded to 4 decimals - assert_state( - "sensor.cash_factor_fronius_logger_info_0_http_fronius", - 0.078, - ) - assert_state( - "sensor.co2_factor_fronius_logger_info_0_http_fronius", - 0.53, - ) - assert_state( - "sensor.delivery_factor_fronius_logger_info_0_http_fronius", - 0.15, - ) + assert_state("sensor.solarnet_grid_export_tariff", 0.078) + assert_state("sensor.solarnet_co2_factor", 0.53) + assert_state("sensor.solarnet_grid_import_tariff", 0.15) async def test_symo_meter(hass, aioclient_mock): @@ -113,48 +104,38 @@ async def test_symo_meter(hass, aioclient_mock): ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 58 # states are rounded to 4 decimals - assert_state("sensor.current_ac_phase_1_fronius_meter_0_http_fronius", 7.755) - assert_state("sensor.current_ac_phase_2_fronius_meter_0_http_fronius", 6.68) - assert_state("sensor.current_ac_phase_3_fronius_meter_0_http_fronius", 10.102) - assert_state( - "sensor.energy_reactive_ac_consumed_fronius_meter_0_http_fronius", 59960790 - ) - assert_state( - "sensor.energy_reactive_ac_produced_fronius_meter_0_http_fronius", 723160 - ) - assert_state("sensor.energy_real_ac_minus_fronius_meter_0_http_fronius", 35623065) - assert_state("sensor.energy_real_ac_plus_fronius_meter_0_http_fronius", 15303334) - assert_state("sensor.energy_real_consumed_fronius_meter_0_http_fronius", 15303334) - assert_state("sensor.energy_real_produced_fronius_meter_0_http_fronius", 35623065) - assert_state("sensor.frequency_phase_average_fronius_meter_0_http_fronius", 50) - assert_state("sensor.power_apparent_phase_1_fronius_meter_0_http_fronius", 1772.793) - assert_state("sensor.power_apparent_phase_2_fronius_meter_0_http_fronius", 1527.048) - assert_state("sensor.power_apparent_phase_3_fronius_meter_0_http_fronius", 2333.562) - assert_state("sensor.power_apparent_fronius_meter_0_http_fronius", 5592.57) - assert_state("sensor.power_factor_phase_1_fronius_meter_0_http_fronius", -0.99) - assert_state("sensor.power_factor_phase_2_fronius_meter_0_http_fronius", -0.99) - assert_state("sensor.power_factor_phase_3_fronius_meter_0_http_fronius", 0.99) - assert_state("sensor.power_factor_fronius_meter_0_http_fronius", 1) - assert_state("sensor.power_reactive_phase_1_fronius_meter_0_http_fronius", 51.48) - assert_state("sensor.power_reactive_phase_2_fronius_meter_0_http_fronius", 115.63) - assert_state("sensor.power_reactive_phase_3_fronius_meter_0_http_fronius", -164.24) - assert_state("sensor.power_reactive_fronius_meter_0_http_fronius", 2.87) - assert_state("sensor.power_real_phase_1_fronius_meter_0_http_fronius", 1765.55) - assert_state("sensor.power_real_phase_2_fronius_meter_0_http_fronius", 1515.8) - assert_state("sensor.power_real_phase_3_fronius_meter_0_http_fronius", 2311.22) - assert_state("sensor.power_real_fronius_meter_0_http_fronius", 5592.57) - assert_state("sensor.voltage_ac_phase_1_fronius_meter_0_http_fronius", 228.6) - assert_state("sensor.voltage_ac_phase_2_fronius_meter_0_http_fronius", 228.6) - assert_state("sensor.voltage_ac_phase_3_fronius_meter_0_http_fronius", 231) - assert_state( - "sensor.voltage_ac_phase_to_phase_12_fronius_meter_0_http_fronius", 395.9 - ) - assert_state( - "sensor.voltage_ac_phase_to_phase_23_fronius_meter_0_http_fronius", 398 - ) - assert_state( - "sensor.voltage_ac_phase_to_phase_31_fronius_meter_0_http_fronius", 398 - ) + assert_state("sensor.smart_meter_63a_current_ac_phase_1", 7.755) + assert_state("sensor.smart_meter_63a_current_ac_phase_2", 6.68) + assert_state("sensor.smart_meter_63a_current_ac_phase_3", 10.102) + assert_state("sensor.smart_meter_63a_energy_reactive_ac_consumed", 59960790) + assert_state("sensor.smart_meter_63a_energy_reactive_ac_produced", 723160) + assert_state("sensor.smart_meter_63a_energy_real_ac_minus", 35623065) + assert_state("sensor.smart_meter_63a_energy_real_ac_plus", 15303334) + assert_state("sensor.smart_meter_63a_energy_real_consumed", 15303334) + assert_state("sensor.smart_meter_63a_energy_real_produced", 35623065) + assert_state("sensor.smart_meter_63a_frequency_phase_average", 50) + assert_state("sensor.smart_meter_63a_power_apparent_phase_1", 1772.793) + assert_state("sensor.smart_meter_63a_power_apparent_phase_2", 1527.048) + assert_state("sensor.smart_meter_63a_power_apparent_phase_3", 2333.562) + assert_state("sensor.smart_meter_63a_power_apparent", 5592.57) + assert_state("sensor.smart_meter_63a_power_factor_phase_1", -0.99) + assert_state("sensor.smart_meter_63a_power_factor_phase_2", -0.99) + assert_state("sensor.smart_meter_63a_power_factor_phase_3", 0.99) + assert_state("sensor.smart_meter_63a_power_factor", 1) + assert_state("sensor.smart_meter_63a_power_reactive_phase_1", 51.48) + assert_state("sensor.smart_meter_63a_power_reactive_phase_2", 115.63) + assert_state("sensor.smart_meter_63a_power_reactive_phase_3", -164.24) + assert_state("sensor.smart_meter_63a_power_reactive", 2.87) + assert_state("sensor.smart_meter_63a_power_real_phase_1", 1765.55) + assert_state("sensor.smart_meter_63a_power_real_phase_2", 1515.8) + assert_state("sensor.smart_meter_63a_power_real_phase_3", 2311.22) + assert_state("sensor.smart_meter_63a_power_real", 5592.57) + assert_state("sensor.smart_meter_63a_voltage_ac_phase_1", 228.6) + assert_state("sensor.smart_meter_63a_voltage_ac_phase_2", 228.6) + assert_state("sensor.smart_meter_63a_voltage_ac_phase_3", 231) + assert_state("sensor.smart_meter_63a_voltage_ac_phase_1_2", 395.9) + assert_state("sensor.smart_meter_63a_voltage_ac_phase_2_3", 398) + assert_state("sensor.smart_meter_63a_voltage_ac_phase_3_1", 398) async def test_symo_power_flow(hass, aioclient_mock): @@ -175,30 +156,12 @@ async def test_symo_power_flow(hass, aioclient_mock): ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52 # states are rounded to 4 decimals - assert_state( - "sensor.energy_day_fronius_power_flow_0_http_fronius", - 10828, - ) - assert_state( - "sensor.energy_total_fronius_power_flow_0_http_fronius", - 44186900, - ) - assert_state( - "sensor.energy_year_fronius_power_flow_0_http_fronius", - 25507686, - ) - assert_state( - "sensor.power_grid_fronius_power_flow_0_http_fronius", - 975.31, - ) - assert_state( - "sensor.power_load_fronius_power_flow_0_http_fronius", - -975.31, - ) - assert_state( - "sensor.relative_autonomy_fronius_power_flow_0_http_fronius", - 0, - ) + assert_state("sensor.solarnet_energy_day", 10828) + assert_state("sensor.solarnet_energy_total", 44186900) + assert_state("sensor.solarnet_energy_year", 25507686) + assert_state("sensor.solarnet_power_grid", 975.31) + assert_state("sensor.solarnet_power_load", -975.31) + assert_state("sensor.solarnet_relative_autonomy", 0) # Second test at daytime when inverter is producing mock_responses(aioclient_mock, night=False) @@ -208,38 +171,14 @@ async def test_symo_power_flow(hass, aioclient_mock): await hass.async_block_till_done() # 54 because power_flow `rel_SelfConsumption` and `P_PV` is not `null` anymore assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 54 - assert_state( - "sensor.energy_day_fronius_power_flow_0_http_fronius", - 1101.7001, - ) - assert_state( - "sensor.energy_total_fronius_power_flow_0_http_fronius", - 44188000, - ) - assert_state( - "sensor.energy_year_fronius_power_flow_0_http_fronius", - 25508788, - ) - assert_state( - "sensor.power_grid_fronius_power_flow_0_http_fronius", - 1703.74, - ) - assert_state( - "sensor.power_load_fronius_power_flow_0_http_fronius", - -2814.74, - ) - assert_state( - "sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", - 1111, - ) - assert_state( - "sensor.relative_autonomy_fronius_power_flow_0_http_fronius", - 39.4708, - ) - assert_state( - "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", - 100, - ) + assert_state("sensor.solarnet_energy_day", 1101.7001) + assert_state("sensor.solarnet_energy_total", 44188000) + assert_state("sensor.solarnet_energy_year", 25508788) + assert_state("sensor.solarnet_power_grid", 1703.74) + assert_state("sensor.solarnet_power_load", -2814.74) + assert_state("sensor.solarnet_power_photovoltaics", 1111) + assert_state("sensor.solarnet_relative_autonomy", 39.4708) + assert_state("sensor.solarnet_relative_self_consumption", 100) async def test_gen24(hass, aioclient_mock): @@ -259,74 +198,60 @@ async def test_gen24(hass, aioclient_mock): ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 52 # inverter 1 - assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 0.1589) - assert_state("sensor.current_dc_2_fronius_inverter_1_http_fronius", 0.0754) - assert_state("sensor.status_code_fronius_inverter_1_http_fronius", 7) - assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 0.0783) - assert_state("sensor.voltage_dc_2_fronius_inverter_1_http_fronius", 403.4312) - assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 37.3204) - assert_state("sensor.error_code_fronius_inverter_1_http_fronius", 0) - assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 411.3811) - assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 1530193.42) - assert_state("sensor.inverter_state_fronius_inverter_1_http_fronius", "Running") - assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 234.9168) - assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 49.9917) + assert_state("sensor.inverter_name_current_ac", 0.1589) + assert_state("sensor.inverter_name_current_dc_2", 0.0754) + assert_state("sensor.inverter_name_status_code", 7) + assert_state("sensor.inverter_name_current_dc", 0.0783) + assert_state("sensor.inverter_name_voltage_dc_2", 403.4312) + assert_state("sensor.inverter_name_power_ac", 37.3204) + assert_state("sensor.inverter_name_error_code", 0) + assert_state("sensor.inverter_name_voltage_dc", 411.3811) + assert_state("sensor.inverter_name_energy_total", 1530193.42) + assert_state("sensor.inverter_name_inverter_state", "Running") + assert_state("sensor.inverter_name_voltage_ac", 234.9168) + assert_state("sensor.inverter_name_frequency_ac", 49.9917) # meter - assert_state("sensor.energy_real_produced_fronius_meter_0_http_fronius", 3863340.0) - assert_state("sensor.energy_real_consumed_fronius_meter_0_http_fronius", 2013105.0) - assert_state("sensor.power_real_fronius_meter_0_http_fronius", 653.1) - assert_state("sensor.frequency_phase_average_fronius_meter_0_http_fronius", 49.9) - assert_state("sensor.meter_location_fronius_meter_0_http_fronius", 0.0) - assert_state("sensor.power_factor_fronius_meter_0_http_fronius", 0.828) - assert_state( - "sensor.energy_reactive_ac_consumed_fronius_meter_0_http_fronius", 88221.0 - ) - assert_state("sensor.energy_real_ac_minus_fronius_meter_0_http_fronius", 3863340.0) - assert_state("sensor.current_ac_phase_2_fronius_meter_0_http_fronius", 2.33) - assert_state("sensor.voltage_ac_phase_1_fronius_meter_0_http_fronius", 235.9) - assert_state( - "sensor.voltage_ac_phase_to_phase_12_fronius_meter_0_http_fronius", 408.7 - ) - assert_state("sensor.power_real_phase_2_fronius_meter_0_http_fronius", 294.9) - assert_state("sensor.energy_real_ac_plus_fronius_meter_0_http_fronius", 2013105.0) - assert_state("sensor.voltage_ac_phase_2_fronius_meter_0_http_fronius", 236.1) - assert_state( - "sensor.energy_reactive_ac_produced_fronius_meter_0_http_fronius", 1989125.0 - ) - assert_state("sensor.voltage_ac_phase_3_fronius_meter_0_http_fronius", 236.9) - assert_state("sensor.power_factor_phase_1_fronius_meter_0_http_fronius", 0.441) - assert_state( - "sensor.voltage_ac_phase_to_phase_23_fronius_meter_0_http_fronius", 409.6 - ) - assert_state("sensor.current_ac_phase_3_fronius_meter_0_http_fronius", 1.825) - assert_state("sensor.power_factor_phase_3_fronius_meter_0_http_fronius", 0.832) - assert_state("sensor.power_apparent_phase_1_fronius_meter_0_http_fronius", 243.3) - assert_state( - "sensor.voltage_ac_phase_to_phase_31_fronius_meter_0_http_fronius", 409.4 - ) - assert_state("sensor.power_apparent_phase_2_fronius_meter_0_http_fronius", 323.4) - assert_state("sensor.power_apparent_phase_3_fronius_meter_0_http_fronius", 301.2) - assert_state("sensor.power_real_phase_1_fronius_meter_0_http_fronius", 106.8) - assert_state("sensor.power_factor_phase_2_fronius_meter_0_http_fronius", 0.934) - assert_state("sensor.power_real_phase_3_fronius_meter_0_http_fronius", 251.3) - assert_state("sensor.power_reactive_phase_1_fronius_meter_0_http_fronius", -218.6) - assert_state("sensor.power_reactive_phase_2_fronius_meter_0_http_fronius", -132.8) - assert_state("sensor.power_reactive_phase_3_fronius_meter_0_http_fronius", -166.0) - assert_state("sensor.power_apparent_fronius_meter_0_http_fronius", 868.0) - assert_state("sensor.power_reactive_fronius_meter_0_http_fronius", -517.4) - assert_state("sensor.current_ac_phase_1_fronius_meter_0_http_fronius", 1.145) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_produced", 3863340.0) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_consumed", 2013105.0) + assert_state("sensor.smart_meter_ts_65a_3_power_real", 653.1) + assert_state("sensor.smart_meter_ts_65a_3_frequency_phase_average", 49.9) + assert_state("sensor.smart_meter_ts_65a_3_meter_location", 0.0) + assert_state("sensor.smart_meter_ts_65a_3_power_factor", 0.828) + assert_state("sensor.smart_meter_ts_65a_3_energy_reactive_ac_consumed", 88221.0) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_ac_minus", 3863340.0) + assert_state("sensor.smart_meter_ts_65a_3_current_ac_phase_2", 2.33) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_1", 235.9) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_1_2", 408.7) + assert_state("sensor.smart_meter_ts_65a_3_power_real_phase_2", 294.9) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_ac_plus", 2013105.0) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_2", 236.1) + assert_state("sensor.smart_meter_ts_65a_3_energy_reactive_ac_produced", 1989125.0) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_3", 236.9) + assert_state("sensor.smart_meter_ts_65a_3_power_factor_phase_1", 0.441) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_2_3", 409.6) + assert_state("sensor.smart_meter_ts_65a_3_current_ac_phase_3", 1.825) + assert_state("sensor.smart_meter_ts_65a_3_power_factor_phase_3", 0.832) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent_phase_1", 243.3) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_3_1", 409.4) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent_phase_2", 323.4) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent_phase_3", 301.2) + assert_state("sensor.smart_meter_ts_65a_3_power_real_phase_1", 106.8) + assert_state("sensor.smart_meter_ts_65a_3_power_factor_phase_2", 0.934) + assert_state("sensor.smart_meter_ts_65a_3_power_real_phase_3", 251.3) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive_phase_1", -218.6) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive_phase_2", -132.8) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive_phase_3", -166.0) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent", 868.0) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive", -517.4) + assert_state("sensor.smart_meter_ts_65a_3_current_ac_phase_1", 1.145) # power_flow - assert_state("sensor.power_grid_fronius_power_flow_0_http_fronius", 658.4) - assert_state( - "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", 100.0 - ) - assert_state( - "sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", 62.9481 - ) - assert_state("sensor.power_load_fronius_power_flow_0_http_fronius", -695.6827) - assert_state("sensor.meter_mode_fronius_power_flow_0_http_fronius", "meter") - assert_state("sensor.relative_autonomy_fronius_power_flow_0_http_fronius", 5.3592) - assert_state("sensor.energy_total_fronius_power_flow_0_http_fronius", 1530193.42) + assert_state("sensor.solarnet_power_grid", 658.4) + assert_state("sensor.solarnet_relative_self_consumption", 100.0) + assert_state("sensor.solarnet_power_photovoltaics", 62.9481) + assert_state("sensor.solarnet_power_load", -695.6827) + assert_state("sensor.solarnet_meter_mode", "meter") + assert_state("sensor.solarnet_relative_autonomy", 5.3592) + assert_state("sensor.solarnet_energy_total", 1530193.42) async def test_gen24_storage(hass, aioclient_mock): @@ -348,92 +273,74 @@ async def test_gen24_storage(hass, aioclient_mock): ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 64 # inverter 1 - assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 0.3952) - assert_state("sensor.voltage_dc_2_fronius_inverter_1_http_fronius", 318.8103) - assert_state("sensor.current_dc_2_fronius_inverter_1_http_fronius", 0.3564) - assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 1.1087) - assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 250.9093) - assert_state("sensor.error_code_fronius_inverter_1_http_fronius", 0) - assert_state("sensor.status_code_fronius_inverter_1_http_fronius", 7) - assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 7512794.0117) - assert_state("sensor.inverter_state_fronius_inverter_1_http_fronius", "Running") - assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 419.1009) - assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 227.354) - assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 49.9816) + assert_state("sensor.gen24_storage_current_dc", 0.3952) + assert_state("sensor.gen24_storage_voltage_dc_2", 318.8103) + assert_state("sensor.gen24_storage_current_dc_2", 0.3564) + assert_state("sensor.gen24_storage_current_ac", 1.1087) + assert_state("sensor.gen24_storage_power_ac", 250.9093) + assert_state("sensor.gen24_storage_error_code", 0) + assert_state("sensor.gen24_storage_status_code", 7) + assert_state("sensor.gen24_storage_energy_total", 7512794.0117) + assert_state("sensor.gen24_storage_inverter_state", "Running") + assert_state("sensor.gen24_storage_voltage_dc", 419.1009) + assert_state("sensor.gen24_storage_voltage_ac", 227.354) + assert_state("sensor.gen24_storage_frequency_ac", 49.9816) # meter - assert_state("sensor.energy_real_produced_fronius_meter_0_http_fronius", 1705128.0) - assert_state("sensor.power_real_fronius_meter_0_http_fronius", 487.7) - assert_state("sensor.power_factor_fronius_meter_0_http_fronius", 0.698) - assert_state("sensor.energy_real_consumed_fronius_meter_0_http_fronius", 1247204.0) - assert_state("sensor.frequency_phase_average_fronius_meter_0_http_fronius", 49.9) - assert_state("sensor.meter_location_fronius_meter_0_http_fronius", 0.0) - assert_state("sensor.power_reactive_fronius_meter_0_http_fronius", -501.5) - assert_state( - "sensor.energy_reactive_ac_produced_fronius_meter_0_http_fronius", 3266105.0 - ) - assert_state("sensor.power_real_phase_3_fronius_meter_0_http_fronius", 19.6) - assert_state("sensor.current_ac_phase_3_fronius_meter_0_http_fronius", 0.645) - assert_state("sensor.energy_real_ac_minus_fronius_meter_0_http_fronius", 1705128.0) - assert_state("sensor.power_apparent_phase_2_fronius_meter_0_http_fronius", 383.9) - assert_state("sensor.current_ac_phase_1_fronius_meter_0_http_fronius", 1.701) - assert_state("sensor.current_ac_phase_2_fronius_meter_0_http_fronius", 1.832) - assert_state("sensor.power_apparent_phase_1_fronius_meter_0_http_fronius", 319.5) - assert_state("sensor.voltage_ac_phase_1_fronius_meter_0_http_fronius", 229.4) - assert_state("sensor.power_real_phase_2_fronius_meter_0_http_fronius", 150.0) - assert_state( - "sensor.voltage_ac_phase_to_phase_31_fronius_meter_0_http_fronius", 394.3 - ) - assert_state("sensor.voltage_ac_phase_2_fronius_meter_0_http_fronius", 225.6) - assert_state( - "sensor.energy_reactive_ac_consumed_fronius_meter_0_http_fronius", 5482.0 - ) - assert_state("sensor.energy_real_ac_plus_fronius_meter_0_http_fronius", 1247204.0) - assert_state("sensor.power_factor_phase_1_fronius_meter_0_http_fronius", 0.995) - assert_state("sensor.power_factor_phase_3_fronius_meter_0_http_fronius", 0.163) - assert_state("sensor.power_factor_phase_2_fronius_meter_0_http_fronius", 0.389) - assert_state("sensor.power_reactive_phase_1_fronius_meter_0_http_fronius", -31.3) - assert_state("sensor.power_reactive_phase_3_fronius_meter_0_http_fronius", -116.7) - assert_state( - "sensor.voltage_ac_phase_to_phase_12_fronius_meter_0_http_fronius", 396.0 - ) - assert_state( - "sensor.voltage_ac_phase_to_phase_23_fronius_meter_0_http_fronius", 393.0 - ) - assert_state("sensor.power_reactive_phase_2_fronius_meter_0_http_fronius", -353.4) - assert_state("sensor.power_real_phase_1_fronius_meter_0_http_fronius", 317.9) - assert_state("sensor.voltage_ac_phase_3_fronius_meter_0_http_fronius", 228.3) - assert_state("sensor.power_apparent_fronius_meter_0_http_fronius", 821.9) - assert_state("sensor.power_apparent_phase_3_fronius_meter_0_http_fronius", 118.4) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_produced", 1705128.0) + assert_state("sensor.smart_meter_ts_65a_3_power_real", 487.7) + assert_state("sensor.smart_meter_ts_65a_3_power_factor", 0.698) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_consumed", 1247204.0) + assert_state("sensor.smart_meter_ts_65a_3_frequency_phase_average", 49.9) + assert_state("sensor.smart_meter_ts_65a_3_meter_location", 0.0) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive", -501.5) + assert_state("sensor.smart_meter_ts_65a_3_energy_reactive_ac_produced", 3266105.0) + assert_state("sensor.smart_meter_ts_65a_3_power_real_phase_3", 19.6) + assert_state("sensor.smart_meter_ts_65a_3_current_ac_phase_3", 0.645) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_ac_minus", 1705128.0) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent_phase_2", 383.9) + assert_state("sensor.smart_meter_ts_65a_3_current_ac_phase_1", 1.701) + assert_state("sensor.smart_meter_ts_65a_3_current_ac_phase_2", 1.832) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent_phase_1", 319.5) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_1", 229.4) + assert_state("sensor.smart_meter_ts_65a_3_power_real_phase_2", 150.0) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_3_1", 394.3) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_2", 225.6) + assert_state("sensor.smart_meter_ts_65a_3_energy_reactive_ac_consumed", 5482.0) + assert_state("sensor.smart_meter_ts_65a_3_energy_real_ac_plus", 1247204.0) + assert_state("sensor.smart_meter_ts_65a_3_power_factor_phase_1", 0.995) + assert_state("sensor.smart_meter_ts_65a_3_power_factor_phase_3", 0.163) + assert_state("sensor.smart_meter_ts_65a_3_power_factor_phase_2", 0.389) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive_phase_1", -31.3) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive_phase_3", -116.7) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_1_2", 396.0) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_2_3", 393.0) + assert_state("sensor.smart_meter_ts_65a_3_power_reactive_phase_2", -353.4) + assert_state("sensor.smart_meter_ts_65a_3_power_real_phase_1", 317.9) + assert_state("sensor.smart_meter_ts_65a_3_voltage_ac_phase_3", 228.3) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent", 821.9) + assert_state("sensor.smart_meter_ts_65a_3_power_apparent_phase_3", 118.4) # ohmpilot - assert_state( - "sensor.energy_real_ac_consumed_fronius_ohmpilot_0_http_fronius", 1233295.0 - ) - assert_state("sensor.power_real_ac_fronius_ohmpilot_0_http_fronius", 0.0) - assert_state("sensor.temperature_channel_1_fronius_ohmpilot_0_http_fronius", 38.9) - assert_state("sensor.state_code_fronius_ohmpilot_0_http_fronius", 0.0) - assert_state( - "sensor.state_message_fronius_ohmpilot_0_http_fronius", "Up and running" - ) + assert_state("sensor.ohmpilot_energy_consumed", 1233295.0) + assert_state("sensor.ohmpilot_power", 0.0) + assert_state("sensor.ohmpilot_temperature_channel_1", 38.9) + assert_state("sensor.ohmpilot_state_code", 0.0) + assert_state("sensor.ohmpilot_state_message", "Up and running") # power_flow - assert_state("sensor.power_grid_fronius_power_flow_0_http_fronius", 2274.9) - assert_state("sensor.power_battery_fronius_power_flow_0_http_fronius", 0.1591) - assert_state("sensor.power_load_fronius_power_flow_0_http_fronius", -2459.3092) - assert_state( - "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", 100.0 - ) - assert_state( - "sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", 216.4328 - ) - assert_state("sensor.relative_autonomy_fronius_power_flow_0_http_fronius", 7.4984) - assert_state("sensor.meter_mode_fronius_power_flow_0_http_fronius", "bidirectional") - assert_state("sensor.energy_total_fronius_power_flow_0_http_fronius", 7512664.4042) + assert_state("sensor.solarnet_power_grid", 2274.9) + assert_state("sensor.solarnet_power_battery", 0.1591) + assert_state("sensor.solarnet_power_load", -2459.3092) + assert_state("sensor.solarnet_relative_self_consumption", 100.0) + assert_state("sensor.solarnet_power_photovoltaics", 216.4328) + assert_state("sensor.solarnet_relative_autonomy", 7.4984) + assert_state("sensor.solarnet_meter_mode", "bidirectional") + assert_state("sensor.solarnet_energy_total", 7512664.4042) # storage - assert_state("sensor.current_dc_fronius_storage_0_http_fronius", 0.0) - assert_state("sensor.state_of_charge_fronius_storage_0_http_fronius", 4.6) - assert_state("sensor.capacity_maximum_fronius_storage_0_http_fronius", 16588) - assert_state("sensor.temperature_cell_fronius_storage_0_http_fronius", 21.5) - assert_state("sensor.capacity_designed_fronius_storage_0_http_fronius", 16588) - assert_state("sensor.voltage_dc_fronius_storage_0_http_fronius", 0.0) + assert_state("sensor.byd_battery_box_premium_hv_current_dc", 0.0) + assert_state("sensor.byd_battery_box_premium_hv_state_of_charge", 4.6) + assert_state("sensor.byd_battery_box_premium_hv_capacity_maximum", 16588) + assert_state("sensor.byd_battery_box_premium_hv_temperature_cell", 21.5) + assert_state("sensor.byd_battery_box_premium_hv_capacity_designed", 16588) + assert_state("sensor.byd_battery_box_premium_hv_voltage_dc", 0.0) # Devices device_registry = dr.async_get(hass) @@ -486,52 +393,50 @@ async def test_primo_s0(hass, aioclient_mock): ) assert len(hass.states.async_all(domain_filter=SENSOR_DOMAIN)) == 40 # logger - assert_state("sensor.cash_factor_fronius_logger_info_0_http_fronius", 1) - assert_state("sensor.co2_factor_fronius_logger_info_0_http_fronius", 0.53) - assert_state("sensor.delivery_factor_fronius_logger_info_0_http_fronius", 1) + assert_state("sensor.solarnet_grid_export_tariff", 1) + assert_state("sensor.solarnet_co2_factor", 0.53) + assert_state("sensor.solarnet_grid_import_tariff", 1) # inverter 1 - assert_state("sensor.energy_total_fronius_inverter_1_http_fronius", 17114940) - assert_state("sensor.energy_day_fronius_inverter_1_http_fronius", 22504) - assert_state("sensor.voltage_dc_fronius_inverter_1_http_fronius", 452.3) - assert_state("sensor.power_ac_fronius_inverter_1_http_fronius", 862) - assert_state("sensor.error_code_fronius_inverter_1_http_fronius", 0) - assert_state("sensor.current_dc_fronius_inverter_1_http_fronius", 4.23) - assert_state("sensor.status_code_fronius_inverter_1_http_fronius", 7) - assert_state("sensor.energy_year_fronius_inverter_1_http_fronius", 7532755.5) - assert_state("sensor.current_ac_fronius_inverter_1_http_fronius", 3.85) - assert_state("sensor.voltage_ac_fronius_inverter_1_http_fronius", 223.9) - assert_state("sensor.frequency_ac_fronius_inverter_1_http_fronius", 60) - assert_state("sensor.led_color_fronius_inverter_1_http_fronius", 2) - assert_state("sensor.led_state_fronius_inverter_1_http_fronius", 0) + assert_state("sensor.primo_5_0_1_energy_total", 17114940) + assert_state("sensor.primo_5_0_1_energy_day", 22504) + assert_state("sensor.primo_5_0_1_voltage_dc", 452.3) + assert_state("sensor.primo_5_0_1_power_ac", 862) + assert_state("sensor.primo_5_0_1_error_code", 0) + assert_state("sensor.primo_5_0_1_current_dc", 4.23) + assert_state("sensor.primo_5_0_1_status_code", 7) + assert_state("sensor.primo_5_0_1_energy_year", 7532755.5) + assert_state("sensor.primo_5_0_1_current_ac", 3.85) + assert_state("sensor.primo_5_0_1_voltage_ac", 223.9) + assert_state("sensor.primo_5_0_1_frequency_ac", 60) + assert_state("sensor.primo_5_0_1_led_color", 2) + assert_state("sensor.primo_5_0_1_led_state", 0) # inverter 2 - assert_state("sensor.energy_total_fronius_inverter_2_http_fronius", 5796010) - assert_state("sensor.energy_day_fronius_inverter_2_http_fronius", 14237) - assert_state("sensor.voltage_dc_fronius_inverter_2_http_fronius", 329.5) - assert_state("sensor.power_ac_fronius_inverter_2_http_fronius", 296) - assert_state("sensor.error_code_fronius_inverter_2_http_fronius", 0) - assert_state("sensor.current_dc_fronius_inverter_2_http_fronius", 0.97) - assert_state("sensor.status_code_fronius_inverter_2_http_fronius", 7) - assert_state("sensor.energy_year_fronius_inverter_2_http_fronius", 3596193.25) - assert_state("sensor.current_ac_fronius_inverter_2_http_fronius", 1.32) - assert_state("sensor.voltage_ac_fronius_inverter_2_http_fronius", 223.6) - assert_state("sensor.frequency_ac_fronius_inverter_2_http_fronius", 60.01) - assert_state("sensor.led_color_fronius_inverter_2_http_fronius", 2) - assert_state("sensor.led_state_fronius_inverter_2_http_fronius", 0) + assert_state("sensor.primo_3_0_1_energy_total", 5796010) + assert_state("sensor.primo_3_0_1_energy_day", 14237) + assert_state("sensor.primo_3_0_1_voltage_dc", 329.5) + assert_state("sensor.primo_3_0_1_power_ac", 296) + assert_state("sensor.primo_3_0_1_error_code", 0) + assert_state("sensor.primo_3_0_1_current_dc", 0.97) + assert_state("sensor.primo_3_0_1_status_code", 7) + assert_state("sensor.primo_3_0_1_energy_year", 3596193.25) + assert_state("sensor.primo_3_0_1_current_ac", 1.32) + assert_state("sensor.primo_3_0_1_voltage_ac", 223.6) + assert_state("sensor.primo_3_0_1_frequency_ac", 60.01) + assert_state("sensor.primo_3_0_1_led_color", 2) + assert_state("sensor.primo_3_0_1_led_state", 0) # meter - assert_state("sensor.meter_location_fronius_meter_0_http_fronius", 1) - assert_state("sensor.power_real_fronius_meter_0_http_fronius", -2216.7487) + assert_state("sensor.s0_meter_at_inverter_1_meter_location", 1) + assert_state("sensor.s0_meter_at_inverter_1_power_real", -2216.7487) # power_flow - assert_state("sensor.power_load_fronius_power_flow_0_http_fronius", -2218.9349) - assert_state("sensor.meter_mode_fronius_power_flow_0_http_fronius", "vague-meter") - assert_state("sensor.power_photovoltaics_fronius_power_flow_0_http_fronius", 1834) - assert_state("sensor.power_grid_fronius_power_flow_0_http_fronius", 384.9349) - assert_state( - "sensor.relative_self_consumption_fronius_power_flow_0_http_fronius", 100 - ) - assert_state("sensor.relative_autonomy_fronius_power_flow_0_http_fronius", 82.6523) - assert_state("sensor.energy_total_fronius_power_flow_0_http_fronius", 22910919.5) - assert_state("sensor.energy_day_fronius_power_flow_0_http_fronius", 36724) - assert_state("sensor.energy_year_fronius_power_flow_0_http_fronius", 11128933.25) + assert_state("sensor.solarnet_power_load", -2218.9349) + assert_state("sensor.solarnet_meter_mode", "vague-meter") + assert_state("sensor.solarnet_power_photovoltaics", 1834) + assert_state("sensor.solarnet_power_grid", 384.9349) + assert_state("sensor.solarnet_relative_self_consumption", 100) + assert_state("sensor.solarnet_relative_autonomy", 82.6523) + assert_state("sensor.solarnet_energy_total", 22910919.5) + assert_state("sensor.solarnet_energy_day", 36724) + assert_state("sensor.solarnet_energy_year", 11128933.25) # Devices device_registry = dr.async_get(hass) From 8d6925b3ab535c252ebce33f948d46d0c0927572 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 18:17:54 +0200 Subject: [PATCH 2474/3516] Migrate Tuya to new entity naming style (#74975) --- homeassistant/components/tuya/base.py | 11 +- .../components/tuya/binary_sensor.py | 10 +- homeassistant/components/tuya/button.py | 10 +- homeassistant/components/tuya/light.py | 2 +- homeassistant/components/tuya/number.py | 48 +++---- homeassistant/components/tuya/select.py | 48 +++---- homeassistant/components/tuya/sensor.py | 130 +++++++++--------- 7 files changed, 125 insertions(+), 134 deletions(-) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 33b571ea848..624bfd80cd4 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -131,6 +131,7 @@ class ElectricityTypeData: class TuyaEntity(Entity): """Tuya base device.""" + _attr_has_entity_name = True _attr_should_poll = False def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None: @@ -139,16 +140,6 @@ class TuyaEntity(Entity): self.device = device self.device_manager = device_manager - @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.device.name} {self.entity_description.name}" - return self.device.name - @property def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index d5e4a9b22b0..5641a18022e 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -64,19 +64,19 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { ), TuyaBinarySensorEntityDescription( key=DPCode.VOC_STATE, - name="Volatile Organic Compound", + name="Volatile organic compound", device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), TuyaBinarySensorEntityDescription( key=DPCode.PM25_STATE, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), TuyaBinarySensorEntityDescription( key=DPCode.CO_STATE, - name="Carbon Monoxide", + name="Carbon monoxide", icon="mdi:molecule-co", device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", @@ -84,7 +84,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { TuyaBinarySensorEntityDescription( key=DPCode.CO2_STATE, icon="mdi:molecule-co2", - name="Carbon Dioxide", + name="Carbon dioxide", device_class=BinarySensorDeviceClass.SAFETY, on_value="alarm", ), @@ -101,7 +101,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = { ), TuyaBinarySensorEntityDescription( key=DPCode.WATERSENSOR_STATE, - name="Water Leak", + name="Water leak", device_class=BinarySensorDeviceClass.MOISTURE, on_value="alarm", ), diff --git a/homeassistant/components/tuya/button.py b/homeassistant/components/tuya/button.py index 26014e53b74..9eef7b03452 100644 --- a/homeassistant/components/tuya/button.py +++ b/homeassistant/components/tuya/button.py @@ -22,31 +22,31 @@ BUTTONS: dict[str, tuple[ButtonEntityDescription, ...]] = { "sd": ( ButtonEntityDescription( key=DPCode.RESET_DUSTER_CLOTH, - name="Reset Duster Cloth", + name="Reset duster cloth", icon="mdi:restart", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_EDGE_BRUSH, - name="Reset Edge Brush", + name="Reset edge brush", icon="mdi:restart", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_FILTER, - name="Reset Filter", + name="Reset filter", icon="mdi:air-filter", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_MAP, - name="Reset Map", + name="Reset map", icon="mdi:map-marker-remove", entity_category=EntityCategory.CONFIG, ), ButtonEntityDescription( key=DPCode.RESET_ROLL_BRUSH, - name="Reset Roll Brush", + name="Reset roll brush", icon="mdi:restart", entity_category=EntityCategory.CONFIG, ), diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 7ba8acdc0fe..9b78008af55 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -299,7 +299,7 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = { ), TuyaLightEntityDescription( key=DPCode.SWITCH_NIGHT_LIGHT, - name="Night Light", + name="Night light", ), ), # Remote Control diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index e7712dcf630..39874d0ae8d 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -51,21 +51,21 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { ), NumberEntityDescription( key=DPCode.TEMP_BOILING_C, - name="Temperature After Boiling", + name="Temperature after boiling", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.TEMP_BOILING_F, - name="Temperature After Boiling", + name="Temperature after boiling", device_class=NumberDeviceClass.TEMPERATURE, icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.WARM_TIME, - name="Heat Preservation Time", + name="Heat preservation time", icon="mdi:timer", entity_category=EntityCategory.CONFIG, ), @@ -80,7 +80,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { ), NumberEntityDescription( key=DPCode.VOICE_TIMES, - name="Voice Times", + name="Voice times", icon="mdi:microphone", ), ), @@ -94,13 +94,13 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { ), NumberEntityDescription( key=DPCode.NEAR_DETECTION, - name="Near Detection", + name="Near detection", icon="mdi:signal-distance-variant", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.FAR_DETECTION, - name="Far Detection", + name="Far detection", icon="mdi:signal-distance-variant", entity_category=EntityCategory.CONFIG, ), @@ -110,7 +110,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "kfj": ( NumberEntityDescription( key=DPCode.WATER_SET, - name="Water Level", + name="Water level", icon="mdi:cup-water", entity_category=EntityCategory.CONFIG, ), @@ -123,7 +123,7 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { ), NumberEntityDescription( key=DPCode.WARM_TIME, - name="Heat Preservation Time", + name="Heat preservation time", icon="mdi:timer", entity_category=EntityCategory.CONFIG, ), @@ -138,20 +138,20 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "mzj": ( NumberEntityDescription( key=DPCode.COOK_TEMPERATURE, - name="Cook Temperature", + name="Cook temperature", icon="mdi:thermometer", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.COOK_TIME, - name="Cook Time", + name="Cook time", icon="mdi:timer", native_unit_of_measurement=TIME_MINUTES, entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.CLOUD_RECIPE_NUMBER, - name="Cloud Recipe", + name="Cloud recipe", entity_category=EntityCategory.CONFIG, ), ), @@ -189,37 +189,37 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "tgkg": ( NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_1, - name="Minimum Brightness", + name="Minimum brightness", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_1, - name="Maximum Brightness", + name="Maximum brightness", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_2, - name="Minimum Brightness 2", + name="Minimum brightness 2", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_2, - name="Maximum Brightness 2", + name="Maximum brightness 2", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_3, - name="Minimum Brightness 3", + name="Minimum brightness 3", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_3, - name="Maximum Brightness 3", + name="Maximum brightness 3", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), @@ -229,25 +229,25 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "tgq": ( NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_1, - name="Minimum Brightness", + name="Minimum brightness", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_1, - name="Maximum Brightness", + name="Maximum brightness", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MIN_2, - name="Minimum Brightness 2", + name="Minimum brightness 2", icon="mdi:lightbulb-outline", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.BRIGHTNESS_MAX_2, - name="Maximum Brightness 2", + name="Maximum brightness 2", icon="mdi:lightbulb-on-outline", entity_category=EntityCategory.CONFIG, ), @@ -265,19 +265,19 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = { "szjqr": ( NumberEntityDescription( key=DPCode.ARM_DOWN_PERCENT, - name="Move Down %", + name="Move down %", icon="mdi:arrow-down-bold", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.ARM_UP_PERCENT, - name="Move Up %", + name="Move up %", icon="mdi:arrow-up-bold", entity_category=EntityCategory.CONFIG, ), NumberEntityDescription( key=DPCode.CLICK_SUSTAIN_TIME, - name="Down Delay", + name="Down delay", icon="mdi:timer", entity_category=EntityCategory.CONFIG, ), diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 974268c109f..6ec636b078d 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -57,13 +57,13 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "kg": ( SelectEntityDescription( key=DPCode.RELAY_STATUS, - name="Power on Behavior", + name="Power on behavior", device_class=TuyaDeviceClass.RELAY_STATUS, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LIGHT_MODE, - name="Indicator Light Mode", + name="Indicator light mode", device_class=TuyaDeviceClass.LIGHT_MODE, entity_category=EntityCategory.CONFIG, ), @@ -73,7 +73,7 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "qn": ( SelectEntityDescription( key=DPCode.LEVEL, - name="Temperature Level", + name="Temperature level", icon="mdi:thermometer-lines", ), ), @@ -96,27 +96,27 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "sp": ( SelectEntityDescription( key=DPCode.IPC_WORK_MODE, - name="IPC Mode", + name="IPC mode", device_class=TuyaDeviceClass.IPC_WORK_MODE, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.DECIBEL_SENSITIVITY, - name="Sound Detection Sensitivity", + name="Sound detection densitivity", icon="mdi:volume-vibrate", device_class=TuyaDeviceClass.DECIBEL_SENSITIVITY, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.RECORD_MODE, - name="Record Mode", + name="Record mode", icon="mdi:record-rec", device_class=TuyaDeviceClass.RECORD_MODE, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.BASIC_NIGHTVISION, - name="Night Vision", + name="Night vision", icon="mdi:theme-light-dark", device_class=TuyaDeviceClass.BASIC_NIGHTVISION, entity_category=EntityCategory.CONFIG, @@ -130,7 +130,7 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { ), SelectEntityDescription( key=DPCode.MOTION_SENSITIVITY, - name="Motion Detection Sensitivity", + name="Motion detection sensitivity", icon="mdi:motion-sensor", device_class=TuyaDeviceClass.MOTION_SENSITIVITY, entity_category=EntityCategory.CONFIG, @@ -141,13 +141,13 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "tdq": ( SelectEntityDescription( key=DPCode.RELAY_STATUS, - name="Power on Behavior", + name="Power on behavior", device_class=TuyaDeviceClass.RELAY_STATUS, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LIGHT_MODE, - name="Indicator Light Mode", + name="Indicator light mode", device_class=TuyaDeviceClass.LIGHT_MODE, entity_category=EntityCategory.CONFIG, ), @@ -157,31 +157,31 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "tgkg": ( SelectEntityDescription( key=DPCode.RELAY_STATUS, - name="Power on Behavior", + name="Power on behavior", device_class=TuyaDeviceClass.RELAY_STATUS, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LIGHT_MODE, - name="Indicator Light Mode", + name="Indicator light mode", device_class=TuyaDeviceClass.LIGHT_MODE, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LED_TYPE_1, - name="Light Source Type", + name="Light source type", device_class=TuyaDeviceClass.LED_TYPE, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LED_TYPE_2, - name="Light 2 Source Type", + name="Light 2 source type", device_class=TuyaDeviceClass.LED_TYPE, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LED_TYPE_3, - name="Light 3 Source Type", + name="Light 3 source type", device_class=TuyaDeviceClass.LED_TYPE, entity_category=EntityCategory.CONFIG, ), @@ -191,13 +191,13 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "tgq": ( SelectEntityDescription( key=DPCode.LED_TYPE_1, - name="Light Source Type", + name="Light source type", device_class=TuyaDeviceClass.LED_TYPE, entity_category=EntityCategory.CONFIG, ), SelectEntityDescription( key=DPCode.LED_TYPE_2, - name="Light 2 Source Type", + name="Light 2 source type", device_class=TuyaDeviceClass.LED_TYPE, entity_category=EntityCategory.CONFIG, ), @@ -216,14 +216,14 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "sd": ( SelectEntityDescription( key=DPCode.CISTERN, - name="Water Tank Adjustment", + name="Water tank adjustment", entity_category=EntityCategory.CONFIG, device_class=TuyaDeviceClass.VACUUM_CISTERN, icon="mdi:water-opacity", ), SelectEntityDescription( key=DPCode.COLLECTION_MODE, - name="Dust Collection Mode", + name="Dust collection mode", entity_category=EntityCategory.CONFIG, device_class=TuyaDeviceClass.VACUUM_COLLECTION, icon="mdi:air-filter", @@ -241,14 +241,14 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "fs": ( SelectEntityDescription( key=DPCode.FAN_VERTICAL, - name="Vertical Swing Flap Angle", + name="Vertical swing flap angle", device_class=TuyaDeviceClass.FAN_ANGLE, entity_category=EntityCategory.CONFIG, icon="mdi:format-vertical-align-center", ), SelectEntityDescription( key=DPCode.FAN_HORIZONTAL, - name="Horizontal Swing Flap Angle", + name="Horizontal swing flap angle", device_class=TuyaDeviceClass.FAN_ANGLE, entity_category=EntityCategory.CONFIG, icon="mdi:format-horizontal-align-center", @@ -273,7 +273,7 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "cl": ( SelectEntityDescription( key=DPCode.CONTROL_BACK_MODE, - name="Motor Mode", + name="Motor mode", device_class=TuyaDeviceClass.CURTAIN_MOTOR_MODE, entity_category=EntityCategory.CONFIG, icon="mdi:swap-horizontal", @@ -290,14 +290,14 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { "jsq": ( SelectEntityDescription( key=DPCode.SPRAY_MODE, - name="Spray Mode", + name="Spray mode", device_class=TuyaDeviceClass.HUMIDIFIER_SPRAY_MODE, entity_category=EntityCategory.CONFIG, icon="mdi:spray", ), SelectEntityDescription( key=DPCode.LEVEL, - name="Spraying Level", + name="Spraying level", device_class=TuyaDeviceClass.HUMIDIFIER_LEVEL, entity_category=EntityCategory.CONFIG, icon="mdi:spray", diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index dd2996f61ba..266ee951530 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -58,7 +58,7 @@ BATTERY_SENSORS: tuple[TuyaSensorEntityDescription, ...] = ( ), TuyaSensorEntityDescription( key=DPCode.BATTERY_STATE, - name="Battery State", + name="Battery state", icon="mdi:battery", entity_category=EntityCategory.DIAGNOSTIC, ), @@ -99,26 +99,26 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile Organic Compound", + name="Volatile organic compound", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CO_VALUE, - name="Carbon Monoxide", + name="Carbon monoxide", icon="mdi:molecule-co", device_class=SensorDeviceClass.CO, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", icon="mdi:molecule-co2", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, @@ -154,7 +154,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.SMOKE_SENSOR_VALUE, - name="Smoke Amount", + name="Smoke amount", icon="mdi:smoke-detector", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorStateClass.MEASUREMENT, @@ -166,13 +166,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "bh": ( TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Current Temperature", + name="Current temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT_F, - name="Current Temperature", + name="Current temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), @@ -199,7 +199,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), @@ -210,7 +210,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "cobj": ( TuyaSensorEntityDescription( key=DPCode.CO_VALUE, - name="Carbon Monoxide", + name="Carbon monoxide", device_class=SensorDeviceClass.CO, state_class=SensorStateClass.MEASUREMENT, ), @@ -221,7 +221,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "cwwsq": ( TuyaSensorEntityDescription( key=DPCode.FEED_REPORT, - name="Last Amount", + name="Last amount", icon="mdi:counter", state_class=SensorStateClass.MEASUREMENT, ), @@ -243,7 +243,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), @@ -254,13 +254,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile Organic Compound", + name="Volatile organic compound", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), @@ -270,19 +270,19 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "jqbj": ( TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile Organic Compound", + name="Volatile organic compound", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), @@ -368,7 +368,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), @@ -385,7 +385,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "mzj": ( TuyaSensorEntityDescription( key=DPCode.TEMP_CURRENT, - name="Current Temperature", + name="Current temperature", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), @@ -396,7 +396,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.REMAIN_TIME, - name="Remaining Time", + name="Remaining time", native_unit_of_measurement=TIME_MINUTES, icon="mdi:timer", ), @@ -409,7 +409,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "pm2.5": ( TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), @@ -420,7 +420,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile Organic Compound", + name="Volatile organic compound", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), @@ -432,7 +432,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), @@ -444,13 +444,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PM1, - name="Particulate Matter 1.0 µm", + name="Particulate matter 1.0 µm", device_class=SensorDeviceClass.PM1, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM10, - name="Particulate Matter 10.0 µm", + name="Particulate matter 10.0 µm", device_class=SensorDeviceClass.PM10, state_class=SensorStateClass.MEASUREMENT, ), @@ -515,13 +515,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "voc": ( TuyaSensorEntityDescription( key=DPCode.CO2_VALUE, - name="Carbon Dioxide", + name="Carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.PM25_VALUE, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), @@ -544,7 +544,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.VOC_VALUE, - name="Volatile Organic Compound", + name="Volatile organic compound", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), @@ -603,7 +603,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "ywbj": ( TuyaSensorEntityDescription( key=DPCode.SMOKE_SENSOR_VALUE, - name="Smoke Amount", + name="Smoke amount", icon="mdi:smoke-detector", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorStateClass.MEASUREMENT, @@ -618,13 +618,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "zndb": ( TuyaSensorEntityDescription( key=DPCode.FORWARD_ENERGY_TOTAL, - name="Total Energy", + name="Total energy", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A Current", + name="Phase A current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -632,7 +632,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A Power", + name="Phase A power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, @@ -640,7 +640,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A Voltage", + name="Phase A voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -648,7 +648,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B Current", + name="Phase B current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -656,7 +656,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B Power", + name="Phase B power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, @@ -664,7 +664,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B Voltage", + name="Phase B voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -672,7 +672,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C Current", + name="Phase C current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -680,7 +680,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C Power", + name="Phase C power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, @@ -688,7 +688,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C Voltage", + name="Phase C voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -700,13 +700,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "dlq": ( TuyaSensorEntityDescription( key=DPCode.TOTAL_FORWARD_ENERGY, - name="Total Energy", + name="Total energy", device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A Current", + name="Phase A current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -714,7 +714,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A Power", + name="Phase A power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, @@ -722,7 +722,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_A, - name="Phase A Voltage", + name="Phase A voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -730,7 +730,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B Current", + name="Phase B current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -738,7 +738,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B Power", + name="Phase B power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, @@ -746,7 +746,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_B, - name="Phase B Voltage", + name="Phase B voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -754,7 +754,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C Current", + name="Phase C current", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -762,7 +762,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C Power", + name="Phase C power", device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=POWER_KILO_WATT, @@ -770,7 +770,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.PHASE_C, - name="Phase C Voltage", + name="Phase C voltage", device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, @@ -782,13 +782,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "sd": ( TuyaSensorEntityDescription( key=DPCode.CLEAN_AREA, - name="Cleaning Area", + name="Cleaning area", icon="mdi:texture-box", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.CLEAN_TIME, - name="Cleaning Time", + name="Cleaning time", icon="mdi:progress-clock", state_class=SensorStateClass.MEASUREMENT, ), @@ -800,37 +800,37 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.TOTAL_CLEAN_TIME, - name="Total Cleaning Time", + name="Total cleaning time", icon="mdi:history", state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.TOTAL_CLEAN_COUNT, - name="Total Cleaning Times", + name="Total cleaning times", icon="mdi:counter", state_class=SensorStateClass.TOTAL_INCREASING, ), TuyaSensorEntityDescription( key=DPCode.DUSTER_CLOTH, - name="Duster Cloth Life", + name="Duster cloth life", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.EDGE_BRUSH, - name="Side Brush Life", + name="Side brush life", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.FILTER_LIFE, - name="Filter Life", + name="Filter life", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.ROLL_BRUSH, - name="Rolling Brush Life", + name="Rolling brush life", icon="mdi:ticket-percent-outline", state_class=SensorStateClass.MEASUREMENT, ), @@ -840,7 +840,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "cl": ( TuyaSensorEntityDescription( key=DPCode.TIME_TOTAL, - name="Last Operation Duration", + name="Last operation duration", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:progress-clock", ), @@ -868,7 +868,7 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.LEVEL_CURRENT, - name="Water Level", + name="Water level", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:waves-arrow-up", ), @@ -878,13 +878,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { "kj": ( TuyaSensorEntityDescription( key=DPCode.FILTER, - name="Filter Utilization", + name="Filter utilization", entity_category=EntityCategory.DIAGNOSTIC, icon="mdi:ticket-percent-outline", ), TuyaSensorEntityDescription( key=DPCode.PM25, - name="Particulate Matter 2.5 µm", + name="Particulate matter 2.5 µm", device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, icon="mdi:molecule", @@ -903,26 +903,26 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { ), TuyaSensorEntityDescription( key=DPCode.TVOC, - name="Total Volatile Organic Compound", + name="Total volatile organic compound", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.ECO2, - name="Concentration of Carbon Dioxide", + name="Concentration of carbon dioxide", device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), TuyaSensorEntityDescription( key=DPCode.TOTAL_TIME, - name="Total Operating Time", + name="Total operating time", icon="mdi:history", state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, ), TuyaSensorEntityDescription( key=DPCode.TOTAL_PM, - name="Total Absorption of Particles", + name="Total absorption of particles", icon="mdi:texture-box", state_class=SensorStateClass.TOTAL_INCREASING, entity_category=EntityCategory.DIAGNOSTIC, From ba18e11308b11e337ea62b1ff9982796ff640da7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 18:34:27 +0200 Subject: [PATCH 2475/3516] Remove profiler from mypy ignore list (#74453) --- homeassistant/components/profiler/__init__.py | 12 +++++++----- mypy.ini | 3 --- script/hassfest/mypy_config.py | 1 - 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/profiler/__init__.py b/homeassistant/components/profiler/__init__.py index 978c19dc2ab..69bbd39a77a 100644 --- a/homeassistant/components/profiler/__init__.py +++ b/homeassistant/components/profiler/__init__.py @@ -7,7 +7,7 @@ import sys import threading import time import traceback -from typing import Any +from typing import Any, cast import voluptuous as vol @@ -123,10 +123,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for thread in threading.enumerate(): if thread == main_thread: continue + ident = cast(int, thread.ident) _LOGGER.critical( "Thread [%s]: %s", thread.name, - "".join(traceback.format_stack(frames.get(thread.ident))).strip(), + "".join(traceback.format_stack(frames.get(ident))).strip(), ) async def _async_dump_scheduled(call: ServiceCall) -> None: @@ -136,13 +137,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: original_maxother = arepr.maxother arepr.maxstring = 300 arepr.maxother = 300 + handle: asyncio.Handle try: - for handle in hass.loop._scheduled: # pylint: disable=protected-access + for handle in getattr(hass.loop, "_scheduled"): if not handle.cancelled(): _LOGGER.critical("Scheduled: %s", handle) finally: - arepr.max_string = original_maxstring - arepr.max_other = original_maxother + arepr.maxstring = original_maxstring + arepr.maxother = original_maxother async_register_admin_service( hass, diff --git a/mypy.ini b/mypy.ini index cccfdc55091..3cdd8b2880f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2719,9 +2719,6 @@ ignore_errors = true [mypy-homeassistant.components.plex.media_player] ignore_errors = true -[mypy-homeassistant.components.profiler] -ignore_errors = true - [mypy-homeassistant.components.sonos] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 8ddfa2b53a4..76304419c6c 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -37,7 +37,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.onvif.device", "homeassistant.components.onvif.sensor", "homeassistant.components.plex.media_player", - "homeassistant.components.profiler", "homeassistant.components.sonos", "homeassistant.components.sonos.alarms", "homeassistant.components.sonos.binary_sensor", From 7c2bd319f1773004d59129ea9a21ca23a24f61df Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 18:59:21 +0200 Subject: [PATCH 2476/3516] Migrate AdGuard Home to new entity naming style (#74999) * Migrate AdGuard Home to new entity naming style * sentence-casing --- homeassistant/components/adguard/__init__.py | 2 ++ homeassistant/components/adguard/sensor.py | 16 ++++++++-------- homeassistant/components/adguard/switch.py | 16 ++++++---------- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 2bb15d19223..0c43f84bdfc 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -132,6 +132,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class AdGuardHomeEntity(Entity): """Defines a base AdGuard Home entity.""" + _attr_has_entity_name = True + def __init__( self, adguard: AdGuardHome, diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 8134d2c4d43..2d3226aba59 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -100,7 +100,7 @@ class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor): super().__init__( adguard, entry, - "AdGuard DNS Queries", + "DNS queries", "mdi:magnify", "dns_queries", "queries", @@ -119,7 +119,7 @@ class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor): super().__init__( adguard, entry, - "AdGuard DNS Queries Blocked", + "DNS queries blocked", "mdi:magnify-close", "blocked_filtering", "queries", @@ -139,7 +139,7 @@ class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor): super().__init__( adguard, entry, - "AdGuard DNS Queries Blocked Ratio", + "DNS queries blocked ratio", "mdi:magnify-close", "blocked_percentage", PERCENTAGE, @@ -159,7 +159,7 @@ class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor): super().__init__( adguard, entry, - "AdGuard Parental Control Blocked", + "Parental control blocked", "mdi:human-male-girl", "blocked_parental", "requests", @@ -178,7 +178,7 @@ class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor): super().__init__( adguard, entry, - "AdGuard Safe Browsing Blocked", + "Safe browsing blocked", "mdi:shield-half-full", "blocked_safebrowsing", "requests", @@ -197,7 +197,7 @@ class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor): super().__init__( adguard, entry, - "AdGuard Safe Searches Enforced", + "Safe searches enforced", "mdi:shield-search", "enforced_safesearch", "requests", @@ -216,7 +216,7 @@ class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor): super().__init__( adguard, entry, - "AdGuard Average Processing Speed", + "Average processing speed", "mdi:speedometer", "average_speed", TIME_MILLISECONDS, @@ -236,7 +236,7 @@ class AdGuardHomeRulesCountSensor(AdGuardHomeSensor): super().__init__( adguard, entry, - "AdGuard Rules Count", + "Rules count", "mdi:counter", "rules_count", "rules", diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index 0cb7a48fee6..5b4017d4054 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -107,9 +107,7 @@ class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch): def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" - super().__init__( - adguard, entry, "AdGuard Protection", "mdi:shield-check", "protection" - ) + super().__init__(adguard, entry, "Protection", "mdi:shield-check", "protection") async def _adguard_turn_off(self) -> None: """Turn off the switch.""" @@ -130,7 +128,7 @@ class AdGuardHomeParentalSwitch(AdGuardHomeSwitch): def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" super().__init__( - adguard, entry, "AdGuard Parental Control", "mdi:shield-check", "parental" + adguard, entry, "Parental control", "mdi:shield-check", "parental" ) async def _adguard_turn_off(self) -> None: @@ -152,7 +150,7 @@ class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch): def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" super().__init__( - adguard, entry, "AdGuard Safe Search", "mdi:shield-check", "safesearch" + adguard, entry, "Safe search", "mdi:shield-check", "safesearch" ) async def _adguard_turn_off(self) -> None: @@ -174,7 +172,7 @@ class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch): def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" super().__init__( - adguard, entry, "AdGuard Safe Browsing", "mdi:shield-check", "safebrowsing" + adguard, entry, "Safe browsing", "mdi:shield-check", "safebrowsing" ) async def _adguard_turn_off(self) -> None: @@ -195,9 +193,7 @@ class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch): def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: """Initialize AdGuard Home switch.""" - super().__init__( - adguard, entry, "AdGuard Filtering", "mdi:shield-check", "filtering" - ) + super().__init__(adguard, entry, "Filtering", "mdi:shield-check", "filtering") async def _adguard_turn_off(self) -> None: """Turn off the switch.""" @@ -220,7 +216,7 @@ class AdGuardHomeQueryLogSwitch(AdGuardHomeSwitch): super().__init__( adguard, entry, - "AdGuard Query Log", + "Query log", "mdi:shield-check", "querylog", enabled_default=False, From 28a34a1f8975a41241060c5fffef4b9a7618a3cc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 19:14:23 +0200 Subject: [PATCH 2477/3516] Remove withings from mypy ignore list (#74966) --- homeassistant/components/withings/__init__.py | 5 +++-- homeassistant/components/withings/binary_sensor.py | 2 +- homeassistant/components/withings/common.py | 8 ++++---- mypy.ini | 12 ------------ script/hassfest/mypy_config.py | 4 ---- 5 files changed, 8 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 1b77faa7a61..da2174c3822 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -6,6 +6,7 @@ For more details about this platform, please refer to the documentation at from __future__ import annotations import asyncio +from typing import Any from aiohttp.web import Request, Response import voluptuous as vol @@ -103,7 +104,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Withings from a config entry.""" - config_updates = {} + config_updates: dict[str, Any] = {} # Add a unique id if it's an older config entry. if entry.unique_id != entry.data["token"]["userid"] or not isinstance( @@ -197,7 +198,7 @@ async def async_webhook_handler( return json_message_response("Parameter appli not provided", message_code=20) try: - appli = NotifyAppli(int(params.getone("appli"))) + appli = NotifyAppli(int(params.getone("appli"))) # type: ignore[arg-type] except ValueError: return json_message_response("Invalid appli provided", message_code=21) diff --git a/homeassistant/components/withings/binary_sensor.py b/homeassistant/components/withings/binary_sensor.py index b0af5051124..ff98e6e0d45 100644 --- a/homeassistant/components/withings/binary_sensor.py +++ b/homeassistant/components/withings/binary_sensor.py @@ -32,6 +32,6 @@ class WithingsHealthBinarySensor(BaseWithingsSensor, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.OCCUPANCY @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if the binary sensor is on.""" return self._state_data diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 90f60cd4112..93c3800e42f 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -10,7 +10,7 @@ from enum import Enum, IntEnum from http import HTTPStatus import logging import re -from typing import Any +from typing import Any, Union from aiohttp.web import Response import requests @@ -589,7 +589,7 @@ class DataManager: update_method=self.async_subscribe_webhook, ) self.poll_data_update_coordinator = DataUpdateCoordinator[ - dict[MeasureType, Any] + Union[dict[MeasureType, Any], None] ]( hass, _LOGGER, @@ -951,7 +951,7 @@ class BaseWithingsSensor(Entity): return self._unique_id @property - def icon(self) -> str: + def icon(self) -> str | None: """Icon to use in the frontend, if any.""" return self._attribute.icon @@ -1005,7 +1005,7 @@ async def async_get_data_manager( config_entry_data = hass.data[const.DOMAIN][config_entry.entry_id] if const.DATA_MANAGER not in config_entry_data: - profile = config_entry.data.get(const.PROFILE) + profile: str = config_entry.data[const.PROFILE] _LOGGER.debug("Creating withings data manager for profile: %s", profile) config_entry_data[const.DATA_MANAGER] = DataManager( diff --git a/mypy.ini b/mypy.ini index 3cdd8b2880f..17887f7c78c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2760,15 +2760,3 @@ ignore_errors = true [mypy-homeassistant.components.template.sensor] ignore_errors = true - -[mypy-homeassistant.components.withings] -ignore_errors = true - -[mypy-homeassistant.components.withings.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.withings.common] -ignore_errors = true - -[mypy-homeassistant.components.withings.config_flow] -ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 76304419c6c..e1b2d60ddd1 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -51,10 +51,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.sonos.statistics", "homeassistant.components.template.number", "homeassistant.components.template.sensor", - "homeassistant.components.withings", - "homeassistant.components.withings.binary_sensor", - "homeassistant.components.withings.common", - "homeassistant.components.withings.config_flow", ] # Component modules which should set no_implicit_reexport = true. From 14baaf4b67beb188a8d7e091c20aef274d7610a2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 11 Jul 2022 20:02:44 +0200 Subject: [PATCH 2478/3516] Remove plex from mypy ignore list (#74984) --- homeassistant/components/plex/media_player.py | 40 +++++++++---------- mypy.ini | 3 -- script/hassfest/mypy_config.py | 1 - 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index ce76c4be3ff..7d21fff3afe 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -248,7 +248,7 @@ class PlexMediaPlayer(MediaPlayerEntity): else: self._attr_state = STATE_IDLE - @property + @property # type: ignore[misc] @needs_session def username(self): """Return the username of the client owner.""" @@ -279,109 +279,109 @@ class PlexMediaPlayer(MediaPlayerEntity): return "video" - @property + @property # type: ignore[misc] @needs_session def session_key(self): """Return current session key.""" return self.session.sessionKey - @property + @property # type: ignore[misc] @needs_session def media_library_title(self): """Return the library name of playing media.""" return self.session.media_library_title - @property + @property # type: ignore[misc] @needs_session def media_content_id(self): """Return the content ID of current playing media.""" return self.session.media_content_id - @property + @property # type: ignore[misc] @needs_session def media_content_type(self): """Return the content type of current playing media.""" return self.session.media_content_type - @property + @property # type: ignore[misc] @needs_session def media_content_rating(self): """Return the content rating of current playing media.""" return self.session.media_content_rating - @property + @property # type: ignore[misc] @needs_session def media_artist(self): """Return the artist of current playing media, music track only.""" return self.session.media_artist - @property + @property # type: ignore[misc] @needs_session def media_album_name(self): """Return the album name of current playing media, music track only.""" return self.session.media_album_name - @property + @property # type: ignore[misc] @needs_session def media_album_artist(self): """Return the album artist of current playing media, music only.""" return self.session.media_album_artist - @property + @property # type: ignore[misc] @needs_session def media_track(self): """Return the track number of current playing media, music only.""" return self.session.media_track - @property + @property # type: ignore[misc] @needs_session def media_duration(self): """Return the duration of current playing media in seconds.""" return self.session.media_duration - @property + @property # type: ignore[misc] @needs_session def media_position(self): """Return the duration of current playing media in seconds.""" return self.session.media_position - @property + @property # type: ignore[misc] @needs_session def media_position_updated_at(self): """When was the position of the current playing media valid.""" return self.session.media_position_updated_at - @property + @property # type: ignore[misc] @needs_session def media_image_url(self): """Return the image URL of current playing media.""" return self.session.media_image_url - @property + @property # type: ignore[misc] @needs_session def media_summary(self): """Return the summary of current playing media.""" return self.session.media_summary - @property + @property # type: ignore[misc] @needs_session def media_title(self): """Return the title of current playing media.""" return self.session.media_title - @property + @property # type: ignore[misc] @needs_session def media_season(self): """Return the season of current playing media (TV Show only).""" return self.session.media_season - @property + @property # type: ignore[misc] @needs_session def media_series_title(self): """Return the title of the series of current playing media.""" return self.session.media_series_title - @property + @property # type: ignore[misc] @needs_session def media_episode(self): """Return the episode of current playing media (TV Show only).""" @@ -515,7 +515,7 @@ class PlexMediaPlayer(MediaPlayerEntity): return attributes @property - def device_info(self) -> DeviceInfo: + def device_info(self) -> DeviceInfo | None: """Return a device description for device registry.""" if self.machine_identifier is None: return None diff --git a/mypy.ini b/mypy.ini index 17887f7c78c..b973b959213 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2716,9 +2716,6 @@ ignore_errors = true [mypy-homeassistant.components.onvif.sensor] ignore_errors = true -[mypy-homeassistant.components.plex.media_player] -ignore_errors = true - [mypy-homeassistant.components.sonos] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index e1b2d60ddd1..87ace51a3d6 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -36,7 +36,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.onvif.camera", "homeassistant.components.onvif.device", "homeassistant.components.onvif.sensor", - "homeassistant.components.plex.media_player", "homeassistant.components.sonos", "homeassistant.components.sonos.alarms", "homeassistant.components.sonos.binary_sensor", From 2986a2f01b6dba6ac67c005bb7e07d753007842e Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 11 Jul 2022 14:19:30 -0400 Subject: [PATCH 2479/3516] Identify the active ZHA coordinator device in API responses (#74739) * Remove deprecated zigpy properties * Create a `ZHADevice.is_active_coordinator` property * Add `@puddly` to the ZHA code owners * Create a `ZHAGateway.coordinator_ieee` shortcut property --- CODEOWNERS | 4 +-- homeassistant/components/zha/__init__.py | 6 ++-- homeassistant/components/zha/api.py | 5 +-- homeassistant/components/zha/core/const.py | 1 + homeassistant/components/zha/core/device.py | 12 ++++++- homeassistant/components/zha/core/gateway.py | 11 ++++--- homeassistant/components/zha/manifest.json | 2 +- tests/components/zha/test_device.py | 34 ++++++++++++++++++-- 8 files changed, 57 insertions(+), 18 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index a473d07af9d..d2ad6ef2307 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1238,8 +1238,8 @@ build.json @home-assistant/supervisor /tests/components/zeroconf/ @bdraco /homeassistant/components/zerproc/ @emlove /tests/components/zerproc/ @emlove -/homeassistant/components/zha/ @dmulcahey @adminiuga -/tests/components/zha/ @dmulcahey @adminiuga +/homeassistant/components/zha/ @dmulcahey @adminiuga @puddly +/tests/components/zha/ @dmulcahey @adminiuga @puddly /homeassistant/components/zodiac/ @JulienTant /tests/components/zodiac/ @JulienTant /homeassistant/components/zone/ @home-assistant/core diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 80956117ff9..7fd9495635f 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -119,10 +119,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={ - (dr.CONNECTION_ZIGBEE, str(zha_gateway.application_controller.ieee)) - }, - identifiers={(DOMAIN, str(zha_gateway.application_controller.ieee))}, + connections={(dr.CONNECTION_ZIGBEE, str(zha_gateway.coordinator_ieee))}, + identifiers={(DOMAIN, str(zha_gateway.coordinator_ieee))}, name="Zigbee Coordinator", manufacturer="ZHA", model=zha_gateway.radio_description, diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index cc4dd45689e..89d360577d4 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -1091,10 +1091,7 @@ def async_load_api(hass: HomeAssistant) -> None: zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee: EUI64 = service.data[ATTR_IEEE] zha_device: ZHADevice | None = zha_gateway.get_device(ieee) - if zha_device is not None and ( - zha_device.is_coordinator - and zha_device.ieee == zha_gateway.application_controller.ieee - ): + if zha_device is not None and zha_device.is_active_coordinator: _LOGGER.info("Removing the coordinator (%s) is not allowed", ieee) return _LOGGER.info("Removing node %s", ieee) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index c2d9e926453..0d6dec2d816 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -17,6 +17,7 @@ import zigpy_znp.zigbee.application from homeassistant.const import Platform import homeassistant.helpers.config_validation as cv +ATTR_ACTIVE_COORDINATOR = "active_coordinator" ATTR_ARGS = "args" ATTR_ATTRIBUTE = "attribute" ATTR_ATTRIBUTE_ID = "attribute_id" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index afd4f647ecb..4719b6bf585 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -30,6 +30,7 @@ from homeassistant.helpers.event import async_track_time_interval from . import channels from .const import ( + ATTR_ACTIVE_COORDINATOR, ATTR_ARGS, ATTR_ATTRIBUTE, ATTR_AVAILABLE, @@ -252,12 +253,20 @@ class ZHADevice(LogMixin): @property def is_coordinator(self) -> bool | None: - """Return true if this device represents the coordinator.""" + """Return true if this device represents a coordinator.""" if self._zigpy_device.node_desc is None: return None return self._zigpy_device.node_desc.is_coordinator + @property + def is_active_coordinator(self) -> bool: + """Return true if this device is the active coordinator.""" + if not self.is_coordinator: + return False + + return self.ieee == self.gateway.coordinator_ieee + @property def is_end_device(self) -> bool | None: """Return true if this device is an end device.""" @@ -499,6 +508,7 @@ class ZHADevice(LogMixin): """Get ZHA device information.""" device_info: dict[str, Any] = {} device_info.update(self.device_info) + device_info[ATTR_ACTIVE_COORDINATOR] = self.is_active_coordinator device_info["entities"] = [ { "entity_id": entity_ref.reference_id, diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 7782d8ef7fd..9efb6e99550 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -178,9 +178,7 @@ class ZHAGateway: self.application_controller.add_listener(self) self.application_controller.groups.add_listener(self) self._hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self - self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str( - self.application_controller.ieee - ) + self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(self.coordinator_ieee) self.async_load_devices() self.async_load_groups() @@ -189,7 +187,7 @@ class ZHAGateway: """Restore ZHA devices from zigpy application state.""" for zigpy_device in self.application_controller.devices.values(): zha_device = self._async_get_or_create_device(zigpy_device, restored=True) - if zha_device.ieee == self.application_controller.ieee: + if zha_device.ieee == self.coordinator_ieee: self.coordinator_zha_device = zha_device delta_msg = "not known" if zha_device.last_seen is not None: @@ -435,6 +433,11 @@ class ZHAGateway: ) self.ha_entity_registry.async_remove(entry.entity_id) + @property + def coordinator_ieee(self) -> EUI64: + """Return the active coordinator's IEEE address.""" + return self.application_controller.state.node_info.ieee + @property def devices(self) -> dict[EUI64, ZHADevice]: """Return devices.""" diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 78aea7c9fb6..c97cca8deb3 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -76,7 +76,7 @@ "known_devices": ["Bitron Video AV2010/10"] } ], - "codeowners": ["@dmulcahey", "@adminiuga"], + "codeowners": ["@dmulcahey", "@adminiuga", "@puddly"], "zeroconf": [ { "type": "_esphomelib._tcp.local.", diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index f05a5cd1872..733a8e99e4b 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -6,7 +6,9 @@ from unittest.mock import patch import pytest import zigpy.profiles.zha +import zigpy.types import zigpy.zcl.clusters.general as general +import zigpy.zdo.types as zdo_t from homeassistant.components.zha.core.const import ( CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, @@ -42,7 +44,7 @@ def required_platforms_only(): def zigpy_device(zigpy_device_mock): """Device tracker zigpy device.""" - def _dev(with_basic_channel: bool = True): + def _dev(with_basic_channel: bool = True, **kwargs): in_clusters = [general.OnOff.cluster_id] if with_basic_channel: in_clusters.append(general.Basic.cluster_id) @@ -54,7 +56,7 @@ def zigpy_device(zigpy_device_mock): SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH, } } - return zigpy_device_mock(endpoints) + return zigpy_device_mock(endpoints, **kwargs) return _dev @@ -321,3 +323,31 @@ async def test_device_restore_availability( assert hass.states.get(entity_id).state == STATE_OFF else: assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + +async def test_device_is_active_coordinator(hass, zha_device_joined, zigpy_device): + """Test that the current coordinator is uniquely detected.""" + + current_coord_dev = zigpy_device(ieee="aa:bb:cc:dd:ee:ff:00:11", nwk=0x0000) + current_coord_dev.node_desc = current_coord_dev.node_desc.replace( + logical_type=zdo_t.LogicalType.Coordinator + ) + + old_coord_dev = zigpy_device(ieee="aa:bb:cc:dd:ee:ff:00:12", nwk=0x0000) + old_coord_dev.node_desc = old_coord_dev.node_desc.replace( + logical_type=zdo_t.LogicalType.Coordinator + ) + + # The two coordinators have different IEEE addresses + assert current_coord_dev.ieee != old_coord_dev.ieee + + current_coordinator = await zha_device_joined(current_coord_dev) + stale_coordinator = await zha_device_joined(old_coord_dev) + + # Ensure the current ApplicationController's IEEE matches our coordinator's + current_coordinator.gateway.application_controller.state.node_info.ieee = ( + current_coord_dev.ieee + ) + + assert current_coordinator.is_active_coordinator + assert not stale_coordinator.is_active_coordinator From 5774f2e7b96e8280efd36bf4d20bed87a05cc8c3 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 11 Jul 2022 14:29:42 -0400 Subject: [PATCH 2480/3516] Use forward_entry_setups in ZHA (#74834) --- homeassistant/components/zha/__init__.py | 23 ++++------------------ homeassistant/components/zha/core/const.py | 1 - 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 7fd9495635f..70b9dfd9b46 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -31,7 +31,6 @@ from .core.const import ( DATA_ZHA, DATA_ZHA_CONFIG, DATA_ZHA_GATEWAY, - DATA_ZHA_PLATFORM_LOADED, DATA_ZHA_SHUTDOWN_TASK, DOMAIN, PLATFORMS, @@ -111,11 +110,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() - zha_data[DATA_ZHA_PLATFORM_LOADED] = [] - for platform in PLATFORMS: - coro = hass.config_entries.async_forward_entry_setup(config_entry, platform) - zha_data[DATA_ZHA_PLATFORM_LOADED].append(hass.async_create_task(coro)) - device_registry = dr.async_get(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -136,7 +130,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b zha_data[DATA_ZHA_SHUTDOWN_TASK] = hass.bus.async_listen_once( ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown ) - asyncio.create_task(async_load_entities(hass)) + + await zha_gateway.async_initialize_devices_and_entities() + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + async_dispatcher_send(hass, SIGNAL_ADD_ENTITIES) return True @@ -161,18 +158,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return True -async def async_load_entities(hass: HomeAssistant) -> None: - """Load entities after integration was setup.""" - zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - await zha_gateway.async_initialize_devices_and_entities() - to_setup = hass.data[DATA_ZHA][DATA_ZHA_PLATFORM_LOADED] - results = await asyncio.gather(*to_setup, return_exceptions=True) - for res in results: - if isinstance(res, Exception): - _LOGGER.warning("Couldn't setup zha platform: %s", res) - async_dispatcher_send(hass, SIGNAL_ADD_ENTITIES) - - async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Migrate old entry.""" _LOGGER.debug("Migrating from version %s", config_entry.version) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 0d6dec2d816..de373215e52 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -171,7 +171,6 @@ DATA_ZHA_CONFIG = "config" DATA_ZHA_BRIDGE_ID = "zha_bridge_id" DATA_ZHA_CORE_EVENTS = "zha_core_events" DATA_ZHA_GATEWAY = "zha_gateway" -DATA_ZHA_PLATFORM_LOADED = "platform_loaded" DATA_ZHA_SHUTDOWN_TASK = "zha_shutdown_task" DEBUG_COMP_BELLOWS = "bellows" From 2d2fd3e48fd626290e0f98956623bd0e483b4742 Mon Sep 17 00:00:00 2001 From: 0bmay <57501269+0bmay@users.noreply.github.com> Date: Mon, 11 Jul 2022 12:53:33 -0700 Subject: [PATCH 2481/3516] Cache Canary camera image (#73923) * Cache camera image Cache camera image so a new image isn't generated each call. Adds debug logging * Apply suggestions from code review code compression with walrus operator Co-authored-by: Erik Montnemery * fix after walrus operator suggested tweak fully use the live_stream_session variable in async_camera_image * Invalidate cached image after 15 minutes requested code change; invalidate cached image * Removed unnecessary if statement based on code review * Image capture flow updates now sets the image expiration upon getting an updated image updates the cache image only when a new image is captured Co-authored-by: Erik Montnemery --- homeassistant/components/canary/camera.py | 57 +++++++++++++++++------ 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 44bac9e3bde..04d8d159541 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +import logging from typing import Final from aiohttp.web import Request, StreamResponse @@ -23,7 +24,7 @@ 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 +from homeassistant.util import dt as dt_util from .const import ( CONF_FFMPEG_ARGUMENTS, @@ -34,7 +35,7 @@ from .const import ( ) from .coordinator import CanaryDataUpdateCoordinator -MIN_TIME_BETWEEN_SESSION_RENEW: Final = timedelta(seconds=90) +FORCE_CAMERA_REFRESH_INTERVAL: Final = timedelta(minutes=15) PLATFORM_SCHEMA: Final = vol.All( cv.deprecated(CONF_FFMPEG_ARGUMENTS), @@ -47,6 +48,8 @@ PLATFORM_SCHEMA: Final = vol.All( ), ) +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistant, @@ -105,6 +108,11 @@ class CanaryCamera(CoordinatorEntity[CanaryDataUpdateCoordinator], Camera): model=device.device_type["name"], name=device.name, ) + self._image: bytes | None = None + self._expires_at = dt_util.utcnow() + _LOGGER.debug( + "%s %s has been initialized", self.name, device.device_type["name"] + ) @property def location(self) -> Location: @@ -125,17 +133,33 @@ class CanaryCamera(CoordinatorEntity[CanaryDataUpdateCoordinator], Camera): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a still image response from the camera.""" - await self.hass.async_add_executor_job(self.renew_live_stream_session) - live_stream_url = await self.hass.async_add_executor_job( - getattr, self._live_stream_session, "live_stream_url" - ) - return await ffmpeg.async_get_image( - self.hass, - live_stream_url, - extra_cmd=self._ffmpeg_arguments, - width=width, - height=height, - ) + utcnow = dt_util.utcnow() + if self._expires_at <= utcnow: + _LOGGER.debug("Grabbing a live view image from %s", self.name) + await self.hass.async_add_executor_job(self.renew_live_stream_session) + + if (live_stream_session := self._live_stream_session) is None: + return None + + if not (live_stream_url := live_stream_session.live_stream_url): + return None + + image = await ffmpeg.async_get_image( + self.hass, + live_stream_url, + extra_cmd=self._ffmpeg_arguments, + width=width, + height=height, + ) + + if image: + self._image = image + self._expires_at = FORCE_CAMERA_REFRESH_INTERVAL + utcnow + _LOGGER.debug("Grabbed a live view image from %s", self.name) + await self.hass.async_add_executor_job(live_stream_session.stop_session) + _LOGGER.debug("Stopped live session from %s", self.name) + + return self._image async def handle_async_mjpeg_stream( self, request: Request @@ -161,9 +185,14 @@ class CanaryCamera(CoordinatorEntity[CanaryDataUpdateCoordinator], Camera): finally: await stream.close() - @Throttle(MIN_TIME_BETWEEN_SESSION_RENEW) def renew_live_stream_session(self) -> None: """Renew live stream session.""" self._live_stream_session = self.coordinator.canary.get_live_stream_session( self._device ) + + _LOGGER.debug( + "Live Stream URL for %s is %s", + self.name, + self._live_stream_session.live_stream_url, + ) From ef025bccc02584e59620404d73d0ce5251d753d5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 23:30:48 +0200 Subject: [PATCH 2482/3516] Update tqdm to 4.64.0 (#75010) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index fa0dcb68a09..1ca1fc05819 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -28,7 +28,7 @@ requests_mock==1.9.2 respx==0.19.2 stdlib-list==0.7.0 tomli==2.0.1;python_version<"3.11" -tqdm==4.49.0 +tqdm==4.64.0 types-atomicwrites==1.4.1 types-croniter==1.0.0 types-backports==0.1.3 From da027fa3905d36b91beefd456cb546f891617eab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Jul 2022 23:46:55 +0200 Subject: [PATCH 2483/3516] JSON serialize NamedTuple subclasses with aiohttp (#74971) --- homeassistant/helpers/aiohttp_client.py | 4 ++-- tests/helpers/test_aiohttp_client.py | 11 +++++++++++ tests/helpers/test_json.py | 22 ++++++++++++++++++++++ tests/test_util/aiohttp.py | 2 +- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 2e56698db41..2ef96091d15 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -14,7 +14,6 @@ from aiohttp import web from aiohttp.hdrs import CONTENT_TYPE, USER_AGENT from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout import async_timeout -import orjson from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ @@ -23,6 +22,7 @@ from homeassistant.loader import bind_hass from homeassistant.util import ssl as ssl_util from .frame import warn_use +from .json import json_dumps DATA_CONNECTOR = "aiohttp_connector" DATA_CONNECTOR_NOTVERIFY = "aiohttp_connector_notverify" @@ -98,7 +98,7 @@ def _async_create_clientsession( """Create a new ClientSession with kwargs, i.e. for cookies.""" clientsession = aiohttp.ClientSession( connector=_async_get_connector(hass, verify_ssl), - json_serialize=lambda x: orjson.dumps(x).decode("utf-8"), + json_serialize=json_dumps, **kwargs, ) # Prevent packages accidentally overriding our default headers diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 599b2c6984e..1ffb4267167 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -19,6 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE import homeassistant.helpers.aiohttp_client as client +from homeassistant.util.color import RGBColor from tests.common import MockConfigEntry @@ -215,6 +216,16 @@ async def test_async_aiohttp_proxy_stream_client_err(aioclient_mock, camera_clie assert resp.status == 502 +async def test_sending_named_tuple(hass, aioclient_mock): + """Test sending a named tuple in json.""" + resp = aioclient_mock.post("http://127.0.0.1/rgb", json={"rgb": RGBColor(4, 3, 2)}) + session = client.async_create_clientsession(hass) + resp = await session.post("http://127.0.0.1/rgb", json={"rgb": RGBColor(4, 3, 2)}) + assert resp.status == 200 + await resp.json() == {"rgb": RGBColor(4, 3, 2)} + aioclient_mock.mock_calls[0][2]["rgb"] == RGBColor(4, 3, 2) + + async def test_client_session_immutable_headers(hass): """Test we can't mutate headers.""" session = client.async_get_clientsession(hass) diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py index 54c488690fa..1e85338f152 100644 --- a/tests/helpers/test_json.py +++ b/tests/helpers/test_json.py @@ -2,6 +2,7 @@ import datetime import json import time +from typing import NamedTuple import pytest @@ -13,6 +14,7 @@ from homeassistant.helpers.json import ( json_dumps_sorted, ) from homeassistant.util import dt as dt_util +from homeassistant.util.color import RGBColor @pytest.mark.parametrize("encoder", (JSONEncoder, ExtendedJSONEncoder)) @@ -96,3 +98,23 @@ def test_json_dumps_tuple_subclass(): tt = time.struct_time((1999, 3, 17, 32, 44, 55, 2, 76, 0)) assert json_dumps(tt) == "[1999,3,17,32,44,55,2,76,0]" + + +def test_json_dumps_named_tuple_subclass(): + """Test the json dumps a tuple subclass.""" + + class NamedTupleSubclass(NamedTuple): + """A NamedTuple subclass.""" + + name: str + + nts = NamedTupleSubclass("a") + + assert json_dumps(nts) == '["a"]' + + +def test_json_dumps_rgb_color_subclass(): + """Test the json dumps of RGBColor.""" + rgb = RGBColor(4, 2, 1) + + assert json_dumps(rgb) == "[4,2,1]" diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 9ed47109210..4ed81a3a577 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -111,7 +111,7 @@ class AiohttpClientMocker: def create_session(self, loop): """Create a ClientSession that is bound to this mocker.""" - session = ClientSession(loop=loop) + session = ClientSession(loop=loop, json_serialize=json_dumps) # Setting directly on `session` will raise deprecation warning object.__setattr__(session, "_request", self.match_request) return session From 2e228b2608c8e6c5ed2da932f85aa48fcdfef8dd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 12 Jul 2022 00:10:53 +0200 Subject: [PATCH 2484/3516] Tweak handling of entities with `has_entity_name` set (#74948) Co-authored-by: Paulus Schoutsen --- .../components/config/entity_registry.py | 10 +++-- homeassistant/helpers/entity_platform.py | 1 + .../components/config/test_entity_registry.py | 42 +++++++++++-------- tests/helpers/test_entity_platform.py | 2 + 4 files changed, 33 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 445ca96c8b0..6d022aa2d14 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -1,6 +1,8 @@ """HTTP views to interact with the entity registry.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant import config_entries @@ -223,32 +225,32 @@ def websocket_remove_entity(hass, connection, msg): @callback -def _entry_dict(entry): +def _entry_dict(entry: er.RegistryEntry) -> dict[str, Any]: """Convert entry to API format.""" return { "area_id": entry.area_id, "config_entry_id": entry.config_entry_id, "device_id": entry.device_id, "disabled_by": entry.disabled_by, + "has_entity_name": entry.has_entity_name, "entity_category": entry.entity_category, "entity_id": entry.entity_id, "hidden_by": entry.hidden_by, "icon": entry.icon, "name": entry.name, + "original_name": entry.original_name, "platform": entry.platform, } @callback -def _entry_ext_dict(entry): +def _entry_ext_dict(entry: er.RegistryEntry) -> dict[str, Any]: """Convert entry to API format.""" data = _entry_dict(entry) data["capabilities"] = entry.capabilities data["device_class"] = entry.device_class - data["has_entity_name"] = entry.has_entity_name data["options"] = entry.options data["original_device_class"] = entry.original_device_class data["original_icon"] = entry.original_icon - data["original_name"] = entry.original_name data["unique_id"] = entry.unique_id return data diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 6253b939bed..2049565e859 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -544,6 +544,7 @@ class EntityPlatform: entity_category=entity.entity_category, hidden_by=hidden_by, known_object_ids=self.entities.keys(), + has_entity_name=entity.has_entity_name, original_device_class=entity.device_class, original_icon=entity.icon, original_name=entity.name, diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 9c5984f751e..e472736ee6c 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -57,28 +57,32 @@ async def test_list_entities(hass, client): assert msg["result"] == [ { + "area_id": None, "config_entry_id": None, "device_id": None, - "area_id": None, "disabled_by": None, - "entity_id": "test_domain.name", - "hidden_by": None, - "name": "Hello World", - "icon": None, - "platform": "test_platform", "entity_category": None, + "entity_id": "test_domain.name", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "name": "Hello World", + "original_name": None, + "platform": "test_platform", }, { + "area_id": None, "config_entry_id": None, "device_id": None, - "area_id": None, "disabled_by": None, - "entity_id": "test_domain.no_name", - "hidden_by": None, - "name": None, - "icon": None, - "platform": "test_platform", "entity_category": None, + "entity_id": "test_domain.no_name", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "name": None, + "original_name": None, + "platform": "test_platform", }, ] @@ -103,16 +107,18 @@ async def test_list_entities(hass, client): assert msg["result"] == [ { + "area_id": None, "config_entry_id": None, "device_id": None, - "area_id": None, "disabled_by": None, - "entity_id": "test_domain.name", - "hidden_by": None, - "name": "Hello World", - "icon": None, - "platform": "test_platform", "entity_category": None, + "entity_id": "test_domain.name", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "name": "Hello World", + "original_name": None, + "platform": "test_platform", }, ] diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 80a37f9f2fd..d7f77eeacda 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1194,6 +1194,7 @@ async def test_entity_info_added_to_entity_registry(hass): capability_attributes={"max": 100}, device_class="mock-device-class", entity_category=EntityCategory.CONFIG, + has_entity_name=True, icon="nice:icon", name="best name", supported_features=5, @@ -1213,6 +1214,7 @@ async def test_entity_info_added_to_entity_registry(hass): capabilities={"max": 100}, device_class=None, entity_category=EntityCategory.CONFIG, + has_entity_name=True, icon=None, id=ANY, name=None, From 7b9a0eed22f01348df5a6c87bbddc4091f513746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 12 Jul 2022 01:35:45 +0300 Subject: [PATCH 2485/3516] Upgrade huawei-lte-api to 1.6.1 (#75030) --- homeassistant/components/huawei_lte/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 656c80e5a89..910e0e132f1 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ - "huawei-lte-api==1.6.0", + "huawei-lte-api==1.6.1", "stringcase==1.2.0", "url-normalize==1.4.3" ], diff --git a/requirements_all.txt b/requirements_all.txt index 5376d91ed2e..933ca47b086 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -849,7 +849,7 @@ horimote==0.4.1 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.6.0 +huawei-lte-api==1.6.1 # homeassistant.components.huisbaasje huisbaasje-client==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ad2dd3957f..0d94346b384 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,7 +616,7 @@ homepluscontrol==0.0.5 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.6.0 +huawei-lte-api==1.6.1 # homeassistant.components.huisbaasje huisbaasje-client==0.1.0 From 5d8e1b8387a9d17f9b55dcea2a816c303904701a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 12 Jul 2022 00:24:26 +0000 Subject: [PATCH 2486/3516] [ci skip] Translation update --- .../components/ambee/translations/pt.json | 11 +++++++++++ .../components/binary_sensor/translations/pt.json | 6 ++++++ .../climacell/translations/sensor.pt.json | 7 +++++++ .../components/deconz/translations/pt.json | 1 + homeassistant/components/demo/translations/pt.json | 1 + .../components/derivative/translations/pt.json | 11 +++++++++++ .../components/econet/translations/pt.json | 7 +++++++ .../evil_genius_labs/translations/pt.json | 11 +++++++++++ .../components/ezviz/translations/pt.json | 11 +++++++++++ .../components/flipr/translations/pt.json | 7 +++++++ .../components/foscam/translations/pt.json | 3 +++ .../components/fritz/translations/pt.json | 8 ++++++++ .../components/goodwe/translations/pt.json | 7 +++++++ .../components/group/translations/pt.json | 9 +++++++++ .../components/harmony/translations/pt.json | 9 +++++++++ .../here_travel_time/translations/pl.json | 7 +++++++ .../here_travel_time/translations/pt.json | 9 +++++++++ homeassistant/components/hue/translations/pt.json | 6 ++++++ .../components/insteon/translations/pt.json | 6 ++++++ .../components/integration/translations/pt.json | 11 +++++++++++ .../components/iotawatt/translations/pt.json | 11 +++++++++++ .../components/isy994/translations/pt.json | 5 +++++ homeassistant/components/knx/translations/pt.json | 11 +++++++++++ .../components/meteoclimatic/translations/pt.json | 7 +++++++ .../components/nextdns/translations/pt.json | 11 +++++++++++ homeassistant/components/nina/translations/pt.json | 9 +++++++++ .../components/nmap_tracker/translations/pt.json | 11 +++++++++++ .../components/nuheat/translations/pt.json | 1 + .../components/overkiz/translations/sensor.pt.json | 7 +++++++ homeassistant/components/peco/translations/pt.json | 11 +++++++++++ .../components/plaato/translations/pt.json | 1 + .../components/rachio/translations/pt.json | 9 +++++++++ .../rainforest_eagle/translations/pt.json | 11 +++++++++++ .../components/rhasspy/translations/et.json | 12 ++++++++++++ .../components/rhasspy/translations/fr.json | 12 ++++++++++++ .../components/rhasspy/translations/pt-BR.json | 12 ++++++++++++ .../components/roomba/translations/pt.json | 5 +++++ .../components/select/translations/pt.json | 7 +++++++ .../components/shelly/translations/pt.json | 5 +++++ .../components/squeezebox/translations/pt.json | 3 ++- .../components/tankerkoenig/translations/pt.json | 11 +++++++++++ .../tomorrowio/translations/sensor.pt.json | 7 +++++++ .../components/tuya/translations/select.pt.json | 14 ++++++++++++++ .../components/tuya/translations/sensor.pt.json | 7 +++++++ homeassistant/components/upb/translations/pt.json | 7 +++++++ .../components/watttime/translations/pt.json | 11 +++++++++++ .../components/withings/translations/af.json | 9 +++++++++ .../components/withings/translations/ar.json | 9 +++++++++ .../components/withings/translations/bg.json | 3 +++ .../components/withings/translations/bn.json | 9 +++++++++ .../components/withings/translations/bs.json | 9 +++++++++ .../components/withings/translations/ca.json | 3 +++ .../components/withings/translations/de.json | 4 ++++ .../components/withings/translations/en.json | 4 ++++ .../components/withings/translations/et.json | 4 ++++ .../components/withings/translations/eu.json | 9 +++++++++ .../components/withings/translations/fr.json | 6 +++++- .../components/withings/translations/hy.json | 9 +++++++++ .../components/withings/translations/pl.json | 4 ++++ .../components/withings/translations/pt-BR.json | 4 ++++ .../components/withings/translations/zh-Hans.json | 9 +++++++++ .../components/withings/translations/zh-Hant.json | 4 ++++ homeassistant/components/wled/translations/pt.json | 3 +++ .../xiaomi_miio/translations/select.pt.json | 7 +++++++ 64 files changed, 473 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/ambee/translations/pt.json create mode 100644 homeassistant/components/climacell/translations/sensor.pt.json create mode 100644 homeassistant/components/derivative/translations/pt.json create mode 100644 homeassistant/components/econet/translations/pt.json create mode 100644 homeassistant/components/evil_genius_labs/translations/pt.json create mode 100644 homeassistant/components/ezviz/translations/pt.json create mode 100644 homeassistant/components/flipr/translations/pt.json create mode 100644 homeassistant/components/fritz/translations/pt.json create mode 100644 homeassistant/components/goodwe/translations/pt.json create mode 100644 homeassistant/components/integration/translations/pt.json create mode 100644 homeassistant/components/iotawatt/translations/pt.json create mode 100644 homeassistant/components/knx/translations/pt.json create mode 100644 homeassistant/components/meteoclimatic/translations/pt.json create mode 100644 homeassistant/components/nextdns/translations/pt.json create mode 100644 homeassistant/components/nina/translations/pt.json create mode 100644 homeassistant/components/nmap_tracker/translations/pt.json create mode 100644 homeassistant/components/overkiz/translations/sensor.pt.json create mode 100644 homeassistant/components/peco/translations/pt.json create mode 100644 homeassistant/components/rainforest_eagle/translations/pt.json create mode 100644 homeassistant/components/rhasspy/translations/et.json create mode 100644 homeassistant/components/rhasspy/translations/fr.json create mode 100644 homeassistant/components/rhasspy/translations/pt-BR.json create mode 100644 homeassistant/components/select/translations/pt.json create mode 100644 homeassistant/components/tankerkoenig/translations/pt.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.pt.json create mode 100644 homeassistant/components/tuya/translations/select.pt.json create mode 100644 homeassistant/components/tuya/translations/sensor.pt.json create mode 100644 homeassistant/components/watttime/translations/pt.json create mode 100644 homeassistant/components/withings/translations/af.json create mode 100644 homeassistant/components/withings/translations/ar.json create mode 100644 homeassistant/components/withings/translations/bn.json create mode 100644 homeassistant/components/withings/translations/bs.json create mode 100644 homeassistant/components/withings/translations/eu.json create mode 100644 homeassistant/components/withings/translations/hy.json create mode 100644 homeassistant/components/withings/translations/zh-Hans.json create mode 100644 homeassistant/components/xiaomi_miio/translations/select.pt.json diff --git a/homeassistant/components/ambee/translations/pt.json b/homeassistant/components/ambee/translations/pt.json new file mode 100644 index 00000000000..286cd58dd89 --- /dev/null +++ b/homeassistant/components/ambee/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/pt.json b/homeassistant/components/binary_sensor/translations/pt.json index 9d7fdda1006..d204347bd02 100644 --- a/homeassistant/components/binary_sensor/translations/pt.json +++ b/homeassistant/components/binary_sensor/translations/pt.json @@ -89,6 +89,9 @@ "vibration": "foram detectadas vibra\u00e7\u00f5es em {entity_name}" } }, + "device_class": { + "moisture": "humidade" + }, "state": { "_": { "off": "Desligado", @@ -178,6 +181,9 @@ "off": "Limpo", "on": "Detectado" }, + "update": { + "off": "Actualizado" + }, "vibration": { "off": "Limpo", "on": "Detetado" diff --git a/homeassistant/components/climacell/translations/sensor.pt.json b/homeassistant/components/climacell/translations/sensor.pt.json new file mode 100644 index 00000000000..30ba0f75808 --- /dev/null +++ b/homeassistant/components/climacell/translations/sensor.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "climacell__health_concern": { + "unhealthy_for_sensitive_groups": "Pouco saud\u00e1vel para grupos sens\u00edveis" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/pt.json b/homeassistant/components/deconz/translations/pt.json index 94efb5c68ca..53dfd5b29bf 100644 --- a/homeassistant/components/deconz/translations/pt.json +++ b/homeassistant/components/deconz/translations/pt.json @@ -54,6 +54,7 @@ "remote_button_double_press": "Bot\u00e3o \"{subtype}\" clicado duas vezes", "remote_button_long_press": "Bot\u00e3o \"{subtype}\" pressionado continuamente", "remote_falling": "Dispositivo em queda livre", + "remote_flip_180_degrees": "Dispositivo virado 180 graus", "remote_gyro_activated": "Dispositivo agitado" } }, diff --git a/homeassistant/components/demo/translations/pt.json b/homeassistant/components/demo/translations/pt.json index db34017d6e2..7d9ee992b39 100644 --- a/homeassistant/components/demo/translations/pt.json +++ b/homeassistant/components/demo/translations/pt.json @@ -3,6 +3,7 @@ "step": { "options_1": { "data": { + "bool": "Booleano opcional", "constant": "Constante" } } diff --git a/homeassistant/components/derivative/translations/pt.json b/homeassistant/components/derivative/translations/pt.json new file mode 100644 index 00000000000..d6c0f4acd0b --- /dev/null +++ b/homeassistant/components/derivative/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "round": "Precis\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/pt.json b/homeassistant/components/econet/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/econet/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/evil_genius_labs/translations/pt.json b/homeassistant/components/evil_genius_labs/translations/pt.json new file mode 100644 index 00000000000..4e8578a0a28 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Anfitri\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/pt.json b/homeassistant/components/ezviz/translations/pt.json new file mode 100644 index 00000000000..cd669c3fd29 --- /dev/null +++ b/homeassistant/components/ezviz/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/pt.json b/homeassistant/components/flipr/translations/pt.json new file mode 100644 index 00000000000..ce1bf4bb4b8 --- /dev/null +++ b/homeassistant/components/flipr/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/pt.json b/homeassistant/components/foscam/translations/pt.json index b8a454fbaba..65a6a1558db 100644 --- a/homeassistant/components/foscam/translations/pt.json +++ b/homeassistant/components/foscam/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/fritz/translations/pt.json b/homeassistant/components/fritz/translations/pt.json new file mode 100644 index 00000000000..9eeb4c35b69 --- /dev/null +++ b/homeassistant/components/fritz/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/goodwe/translations/pt.json b/homeassistant/components/goodwe/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/goodwe/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/pt.json b/homeassistant/components/group/translations/pt.json index 940aa088ced..728b1dcbd95 100644 --- a/homeassistant/components/group/translations/pt.json +++ b/homeassistant/components/group/translations/pt.json @@ -9,6 +9,15 @@ } } }, + "options": { + "step": { + "fan": { + "data": { + "hide_members": "Esconder membros" + } + } + } + }, "state": { "_": { "closed": "Fechada", diff --git a/homeassistant/components/harmony/translations/pt.json b/homeassistant/components/harmony/translations/pt.json index 04374af8e82..3b58778917d 100644 --- a/homeassistant/components/harmony/translations/pt.json +++ b/homeassistant/components/harmony/translations/pt.json @@ -14,5 +14,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "activity": "A actividade por defeito a executar quando nenhuma \u00e9 especificada." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/pl.json b/homeassistant/components/here_travel_time/translations/pl.json index 3e4f41212a2..17b91417007 100644 --- a/homeassistant/components/here_travel_time/translations/pl.json +++ b/homeassistant/components/here_travel_time/translations/pl.json @@ -39,6 +39,13 @@ }, "title": "Wybierz punkt pocz\u0105tkowy" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Lokalizacja na mapie", + "origin_entity": "Encja" + }, + "title": "Wybierz punkt pocz\u0105tkowy" + }, "user": { "data": { "api_key": "Klucz API", diff --git a/homeassistant/components/here_travel_time/translations/pt.json b/homeassistant/components/here_travel_time/translations/pt.json index f412c0033f6..a36091fc30c 100644 --- a/homeassistant/components/here_travel_time/translations/pt.json +++ b/homeassistant/components/here_travel_time/translations/pt.json @@ -9,5 +9,14 @@ "title": "Escolha a Origem" } } + }, + "options": { + "step": { + "departure_time": { + "data": { + "departure_time": "Hora de partida" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/pt.json b/homeassistant/components/hue/translations/pt.json index 09d839cbd5c..8f51f8d74e9 100644 --- a/homeassistant/components/hue/translations/pt.json +++ b/homeassistant/components/hue/translations/pt.json @@ -32,6 +32,12 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button_1": "Primeiro bot\u00e3o", + "button_4": "Quarto bot\u00e3o" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/insteon/translations/pt.json b/homeassistant/components/insteon/translations/pt.json index 1a281774ffe..1475c2d00d3 100644 --- a/homeassistant/components/insteon/translations/pt.json +++ b/homeassistant/components/insteon/translations/pt.json @@ -60,8 +60,14 @@ }, "init": { "data": { + "add_x10": "Adicionar um dispositivo X10.", "remove_x10": "Remova um dispositivo X10." } + }, + "remove_override": { + "data": { + "address": "Seleccione um endere\u00e7o de dispositivo para remover" + } } } } diff --git a/homeassistant/components/integration/translations/pt.json b/homeassistant/components/integration/translations/pt.json new file mode 100644 index 00000000000..286cd58dd89 --- /dev/null +++ b/homeassistant/components/integration/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iotawatt/translations/pt.json b/homeassistant/components/iotawatt/translations/pt.json new file mode 100644 index 00000000000..85b7c4b704a --- /dev/null +++ b/homeassistant/components/iotawatt/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "auth": { + "data": { + "username": "Nome de utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/pt.json b/homeassistant/components/isy994/translations/pt.json index 36962100519..9f8734b5a6d 100644 --- a/homeassistant/components/isy994/translations/pt.json +++ b/homeassistant/components/isy994/translations/pt.json @@ -9,6 +9,11 @@ "unknown": "Erro inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + }, "user": { "data": { "host": "", diff --git a/homeassistant/components/knx/translations/pt.json b/homeassistant/components/knx/translations/pt.json new file mode 100644 index 00000000000..ba21368f797 --- /dev/null +++ b/homeassistant/components/knx/translations/pt.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "tunnel": { + "data": { + "host": "Anfitri\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/pt.json b/homeassistant/components/meteoclimatic/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/pt.json b/homeassistant/components/nextdns/translations/pt.json new file mode 100644 index 00000000000..92f429b6a45 --- /dev/null +++ b/homeassistant/components/nextdns/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Chave API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/pt.json b/homeassistant/components/nina/translations/pt.json new file mode 100644 index 00000000000..052bde4e432 --- /dev/null +++ b/homeassistant/components/nina/translations/pt.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Selecione a cidade/distrito" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/pt.json b/homeassistant/components/nmap_tracker/translations/pt.json new file mode 100644 index 00000000000..8215a0f7a01 --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/pt.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "interval_seconds": "Intervalo de varrimento" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/pt.json b/homeassistant/components/nuheat/translations/pt.json index 7953cf5625c..16a664b4225 100644 --- a/homeassistant/components/nuheat/translations/pt.json +++ b/homeassistant/components/nuheat/translations/pt.json @@ -12,6 +12,7 @@ "user": { "data": { "password": "Palavra-passe", + "serial_number": "N\u00famero de s\u00e9rie do termostato.", "username": "Nome de Utilizador" } } diff --git a/homeassistant/components/overkiz/translations/sensor.pt.json b/homeassistant/components/overkiz/translations/sensor.pt.json new file mode 100644 index 00000000000..fd6f8f53478 --- /dev/null +++ b/homeassistant/components/overkiz/translations/sensor.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "overkiz__discrete_rssi_level": { + "normal": "Normal" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/peco/translations/pt.json b/homeassistant/components/peco/translations/pt.json new file mode 100644 index 00000000000..9be1378d3e6 --- /dev/null +++ b/homeassistant/components/peco/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "county": "Distrito" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/pt.json b/homeassistant/components/plaato/translations/pt.json index e9890abba2f..2eeb965754a 100644 --- a/homeassistant/components/plaato/translations/pt.json +++ b/homeassistant/components/plaato/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "A Conta j\u00e1 est\u00e1 configurada", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." }, diff --git a/homeassistant/components/rachio/translations/pt.json b/homeassistant/components/rachio/translations/pt.json index 8a4fdd22177..d6320473ac0 100644 --- a/homeassistant/components/rachio/translations/pt.json +++ b/homeassistant/components/rachio/translations/pt.json @@ -15,5 +15,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Dura\u00e7\u00e3o em minutos para funcionar ao activar um interruptor de zona" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rainforest_eagle/translations/pt.json b/homeassistant/components/rainforest_eagle/translations/pt.json new file mode 100644 index 00000000000..4e8578a0a28 --- /dev/null +++ b/homeassistant/components/rainforest_eagle/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Anfitri\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/et.json b/homeassistant/components/rhasspy/translations/et.json new file mode 100644 index 00000000000..8182211cdef --- /dev/null +++ b/homeassistant/components/rhasspy/translations/et.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks sidumine." + }, + "step": { + "user": { + "description": "Kas soovid Rhaspy toe lubada?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/fr.json b/homeassistant/components/rhasspy/translations/fr.json new file mode 100644 index 00000000000..6fce2e23902 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/fr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "step": { + "user": { + "description": "Voulez-vous activer la prise en charge de Rhasspy\u00a0?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/pt-BR.json b/homeassistant/components/rhasspy/translations/pt-BR.json new file mode 100644 index 00000000000..0e62dceb57d --- /dev/null +++ b/homeassistant/components/rhasspy/translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 foi configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Deseja habilitar o suporte ao Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/pt.json b/homeassistant/components/roomba/translations/pt.json index 84fe0402f15..3afb1cc22e3 100644 --- a/homeassistant/components/roomba/translations/pt.json +++ b/homeassistant/components/roomba/translations/pt.json @@ -11,6 +11,11 @@ "link": { "title": "Recuperar Palavra-passe" }, + "manual": { + "data": { + "host": "Anfitri\u00e3o" + } + }, "user": { "data": { "host": "Servidor" diff --git a/homeassistant/components/select/translations/pt.json b/homeassistant/components/select/translations/pt.json new file mode 100644 index 00000000000..aa92ddaf4c3 --- /dev/null +++ b/homeassistant/components/select/translations/pt.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "select_option": "Alterar op\u00e7\u00e3o {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/pt.json b/homeassistant/components/shelly/translations/pt.json index d66cc0e5dd9..43f18a5c9d6 100644 --- a/homeassistant/components/shelly/translations/pt.json +++ b/homeassistant/components/shelly/translations/pt.json @@ -25,5 +25,10 @@ } } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Bot\u00e3o" + } } } \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/pt.json b/homeassistant/components/squeezebox/translations/pt.json index 97ced548ed5..86aa270e47d 100644 --- a/homeassistant/components/squeezebox/translations/pt.json +++ b/homeassistant/components/squeezebox/translations/pt.json @@ -17,7 +17,8 @@ "password": "Palavra-passe", "port": "Porta", "username": "Utilizador" - } + }, + "title": "Editar informa\u00e7\u00e3o sobre a liga\u00e7\u00e3o" }, "user": { "data": { diff --git a/homeassistant/components/tankerkoenig/translations/pt.json b/homeassistant/components/tankerkoenig/translations/pt.json new file mode 100644 index 00000000000..19646bbce9c --- /dev/null +++ b/homeassistant/components/tankerkoenig/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome da regi\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.pt.json b/homeassistant/components/tomorrowio/translations/sensor.pt.json new file mode 100644 index 00000000000..db42eacabaa --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "tomorrowio__health_concern": { + "moderate": "Moderado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.pt.json b/homeassistant/components/tuya/translations/select.pt.json new file mode 100644 index 00000000000..77604504f4d --- /dev/null +++ b/homeassistant/components/tuya/translations/select.pt.json @@ -0,0 +1,14 @@ +{ + "state": { + "tuya__record_mode": { + "2": "Grava\u00e7\u00e3o cont\u00ednua" + }, + "tuya__relay_status": { + "on": "Ligado" + }, + "tuya__vacuum_mode": { + "pick_zone": "Escolha a Zona", + "right_spiral": "Espiral Direita" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sensor.pt.json b/homeassistant/components/tuya/translations/sensor.pt.json new file mode 100644 index 00000000000..9547600f35f --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "tuya__air_quality": { + "good": "Bom" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/pt.json b/homeassistant/components/upb/translations/pt.json index 657ce03e544..3bd5d8358a6 100644 --- a/homeassistant/components/upb/translations/pt.json +++ b/homeassistant/components/upb/translations/pt.json @@ -6,6 +6,13 @@ "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "protocol": "Protocolo" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/pt.json b/homeassistant/components/watttime/translations/pt.json new file mode 100644 index 00000000000..d64652ed815 --- /dev/null +++ b/homeassistant/components/watttime/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Nome de utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/af.json b/homeassistant/components/withings/translations/af.json new file mode 100644 index 00000000000..3a1c3f97dbf --- /dev/null +++ b/homeassistant/components/withings/translations/af.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "Herverifieer integrasie" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/ar.json b/homeassistant/components/withings/translations/ar.json new file mode 100644 index 00000000000..8db98ed45e5 --- /dev/null +++ b/homeassistant/components/withings/translations/ar.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "\u0625\u0639\u0627\u062f\u0629 \u0645\u0635\u0627\u062f\u0642\u0629 \u0627\u0644\u062a\u0643\u0627\u0645\u0644" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/bg.json b/homeassistant/components/withings/translations/bg.json index 9d5313ee391..ad1284c7a48 100644 --- a/homeassistant/components/withings/translations/bg.json +++ b/homeassistant/components/withings/translations/bg.json @@ -20,6 +20,9 @@ }, "reauth": { "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" + }, + "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/withings/translations/bn.json b/homeassistant/components/withings/translations/bn.json new file mode 100644 index 00000000000..97f6140ff26 --- /dev/null +++ b/homeassistant/components/withings/translations/bn.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "\u0987\u09a8\u09cd\u099f\u09bf\u0997\u09cd\u09b0\u09c7\u09b6\u09a8 \u09aa\u09c1\u09a8\u09b0\u09be\u09af\u09bc \u09aa\u09cd\u09b0\u09ae\u09be\u09a3\u09c0\u0995\u09b0\u09a3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/bs.json b/homeassistant/components/withings/translations/bs.json new file mode 100644 index 00000000000..14583e8ae80 --- /dev/null +++ b/homeassistant/components/withings/translations/bs.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "Ponovo potvrdite integraciju" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/ca.json b/homeassistant/components/withings/translations/ca.json index c75e08e0234..c7ec9fcd8ca 100644 --- a/homeassistant/components/withings/translations/ca.json +++ b/homeassistant/components/withings/translations/ca.json @@ -27,6 +27,9 @@ "reauth": { "description": "El perfil \"{profile}\" s'ha de tornar a autenticar per poder continuar rebent dades de Withings.", "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, + "reauth_confirm": { + "title": "Reautenticar la integraci\u00f3" } } } diff --git a/homeassistant/components/withings/translations/de.json b/homeassistant/components/withings/translations/de.json index 31d5ad2f6e5..672ced9ca5c 100644 --- a/homeassistant/components/withings/translations/de.json +++ b/homeassistant/components/withings/translations/de.json @@ -27,6 +27,10 @@ "reauth": { "description": "Das Profil \"{profile}\" muss neu authentifiziert werden, um weiterhin Withings-Daten zu empfangen.", "title": "Integration erneut authentifizieren" + }, + "reauth_confirm": { + "description": "Das Profil \"{profile}\" muss neu authentifiziert werden, um weiterhin Withings-Daten zu empfangen.", + "title": "Integration erneut authentifizieren" } } } diff --git a/homeassistant/components/withings/translations/en.json b/homeassistant/components/withings/translations/en.json index ca969626510..490e60512f9 100644 --- a/homeassistant/components/withings/translations/en.json +++ b/homeassistant/components/withings/translations/en.json @@ -24,6 +24,10 @@ "description": "Provide a unique profile name for this data. Typically this is the name of the profile you selected in the previous step.", "title": "User Profile." }, + "reauth": { + "description": "The \"{profile}\" profile needs to be re-authenticated in order to continue receiving Withings data.", + "title": "Reauthenticate Integration" + }, "reauth_confirm": { "description": "The \"{profile}\" profile needs to be re-authenticated in order to continue receiving Withings data.", "title": "Reauthenticate Integration" diff --git a/homeassistant/components/withings/translations/et.json b/homeassistant/components/withings/translations/et.json index 5395069522f..a336b6b0bdf 100644 --- a/homeassistant/components/withings/translations/et.json +++ b/homeassistant/components/withings/translations/et.json @@ -27,6 +27,10 @@ "reauth": { "description": "Withingi andmete jsaamiseks tuleb kasutaja {profile} taastuvastada.", "title": "Taastuvasta sidumine" + }, + "reauth_confirm": { + "description": "Profiil \"{profile}\" tuleb uuesti tuvastada, et j\u00e4tkata Withingsi andmete saamist.", + "title": "Taastuvasta sidumine" } } } diff --git a/homeassistant/components/withings/translations/eu.json b/homeassistant/components/withings/translations/eu.json new file mode 100644 index 00000000000..fa2d5af97c2 --- /dev/null +++ b/homeassistant/components/withings/translations/eu.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "Berriro autentifikatu Integrazioa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/fr.json b/homeassistant/components/withings/translations/fr.json index f59b8e2e714..e0d9e8db08d 100644 --- a/homeassistant/components/withings/translations/fr.json +++ b/homeassistant/components/withings/translations/fr.json @@ -25,7 +25,11 @@ "title": "Profil utilisateur" }, "reauth": { - "description": "Le profile \" {profile} \" doit \u00eatre r\u00e9-authentifi\u00e9 afin de continuer \u00e0 recevoir les donn\u00e9es Withings.", + "description": "Le profile \u00ab\u00a0{profile}\u00a0\u00bb doit \u00eatre r\u00e9-authentifi\u00e9 afin de continuer \u00e0 recevoir les donn\u00e9es Withings.", + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, + "reauth_confirm": { + "description": "Le profile \u00ab\u00a0{profile}\u00a0\u00bb doit \u00eatre r\u00e9-authentifi\u00e9 afin de continuer \u00e0 recevoir les donn\u00e9es Withings.", "title": "R\u00e9-authentifier l'int\u00e9gration" } } diff --git a/homeassistant/components/withings/translations/hy.json b/homeassistant/components/withings/translations/hy.json new file mode 100644 index 00000000000..92c4ce24080 --- /dev/null +++ b/homeassistant/components/withings/translations/hy.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "\u054e\u0565\u0580\u0561\u0570\u0561\u057d\u057f\u0561\u057f\u0565\u056c \u056b\u0576\u057f\u0565\u0563\u0580\u0578\u0582\u0574\u0568" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/pl.json b/homeassistant/components/withings/translations/pl.json index 638171af846..27544b29fa4 100644 --- a/homeassistant/components/withings/translations/pl.json +++ b/homeassistant/components/withings/translations/pl.json @@ -27,6 +27,10 @@ "reauth": { "description": "Profil \"{profile}\" musi zosta\u0107 ponownie uwierzytelniony, aby nadal otrzymywa\u0107 dane Withings.", "title": "Ponownie uwierzytelnij integracj\u0119" + }, + "reauth_confirm": { + "description": "Profil \"{profile}\" musi zosta\u0107 ponownie uwierzytelniony, aby nadal otrzymywa\u0107 dane Withings.", + "title": "Ponownie uwierzytelnij integracj\u0119" } } } diff --git a/homeassistant/components/withings/translations/pt-BR.json b/homeassistant/components/withings/translations/pt-BR.json index 6a067498f1e..4ea2fd4a92c 100644 --- a/homeassistant/components/withings/translations/pt-BR.json +++ b/homeassistant/components/withings/translations/pt-BR.json @@ -27,6 +27,10 @@ "reauth": { "description": "O perfil \"{profile}\" precisa ser autenticado novamente para continuar recebendo dados do Withings", "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "reauth_confirm": { + "description": "O perfil \"{profile}\" precisa ser autenticado novamente para continuar recebendo dados do Withings.", + "title": "Reautenticar Integra\u00e7\u00e3o" } } } diff --git a/homeassistant/components/withings/translations/zh-Hans.json b/homeassistant/components/withings/translations/zh-Hans.json new file mode 100644 index 00000000000..83a6258ba49 --- /dev/null +++ b/homeassistant/components/withings/translations/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "\u91cd\u65b0\u9a8c\u8bc1\u96c6\u6210" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/zh-Hant.json b/homeassistant/components/withings/translations/zh-Hant.json index 2ee7ce3d3da..35328ea9353 100644 --- a/homeassistant/components/withings/translations/zh-Hant.json +++ b/homeassistant/components/withings/translations/zh-Hant.json @@ -27,6 +27,10 @@ "reauth": { "description": "\"{profile}\" \u8a2d\u5b9a\u6a94\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u4ee5\u4fdd\u6301\u63a5\u6536 Withings \u8cc7\u6599\u3002", "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, + "reauth_confirm": { + "description": "\"{profile}\" \u8a2d\u5b9a\u6a94\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u4ee5\u4fdd\u6301\u63a5\u6536 Withings \u8cc7\u6599\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" } } } diff --git a/homeassistant/components/wled/translations/pt.json b/homeassistant/components/wled/translations/pt.json index 313c9057da0..cc9f0af829a 100644 --- a/homeassistant/components/wled/translations/pt.json +++ b/homeassistant/components/wled/translations/pt.json @@ -12,6 +12,9 @@ "data": { "host": "Nome servidor ou endere\u00e7o IP" } + }, + "zeroconf_confirm": { + "title": "Dispositivo WLED descoberto" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/select.pt.json b/homeassistant/components/xiaomi_miio/translations/select.pt.json new file mode 100644 index 00000000000..24ed8a3e752 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "dim": "Escurecer" + } + } +} \ No newline at end of file From f4953e6e9b0708f6f05aa8723c101966d16cfb84 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Tue, 12 Jul 2022 01:42:33 -0500 Subject: [PATCH 2487/3516] Do not spam log when Life360 member location is missing (#75029) --- homeassistant/components/life360/__init__.py | 6 +- .../components/life360/coordinator.py | 62 +++++++++++++++---- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/life360/__init__.py b/homeassistant/components/life360/__init__.py index 4527f6ac298..d05a9ec400b 100644 --- a/homeassistant/components/life360/__init__.py +++ b/homeassistant/components/life360/__init__.py @@ -37,7 +37,7 @@ from .const import ( SHOW_DRIVING, SHOW_MOVING, ) -from .coordinator import Life360DataUpdateCoordinator +from .coordinator import Life360DataUpdateCoordinator, MissingLocReason PLATFORMS = [Platform.DEVICE_TRACKER] @@ -128,6 +128,10 @@ class IntegData: coordinators: dict[str, Life360DataUpdateCoordinator] = field( init=False, default_factory=dict ) + # member_id: missing location reason + missing_loc_reason: dict[str, MissingLocReason] = field( + init=False, default_factory=dict + ) # member_id: ConfigEntry.entry_id tracked_members: dict[str, str] = field(init=False, default_factory=dict) logged_circles: list[str] = field(init=False, default_factory=list) diff --git a/homeassistant/components/life360/coordinator.py b/homeassistant/components/life360/coordinator.py index dc7fdb73a8c..05eecd43cdc 100644 --- a/homeassistant/components/life360/coordinator.py +++ b/homeassistant/components/life360/coordinator.py @@ -2,8 +2,10 @@ from __future__ import annotations +from contextlib import suppress from dataclasses import dataclass, field from datetime import datetime +from enum import Enum from typing import Any from life360 import Life360, Life360Error, LoginError @@ -33,6 +35,13 @@ from .const import ( ) +class MissingLocReason(Enum): + """Reason member location information is missing.""" + + VAGUE_ERROR_REASON = "vague error reason" + EXPLICIT_ERROR_REASON = "explicit error reason" + + @dataclass class Life360Place: """Life360 Place data.""" @@ -99,6 +108,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator): max_retries=COMM_MAX_RETRIES, authorization=entry.data[CONF_AUTHORIZATION], ) + self._missing_loc_reason = hass.data[DOMAIN].missing_loc_reason async def _retrieve_data(self, func: str, *args: Any) -> list[dict[str, Any]]: """Get data from Life360.""" @@ -141,10 +151,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator): if not int(member["features"]["shareLocation"]): continue - # Note that member may be in more than one circle. If that's the case just - # go ahead and process the newly retrieved data (overwriting the older - # data), since it might be slightly newer than what was retrieved while - # processing another circle. + member_id = member["id"] first = member["firstName"] last = member["lastName"] @@ -153,16 +160,45 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator): else: name = first or last - loc = member["location"] - if not loc: - if err_msg := member["issues"]["title"]: - if member["issues"]["dialog"]: - err_msg += f": {member['issues']['dialog']}" - else: - err_msg = "Location information missing" - LOGGER.error("%s: %s", name, err_msg) + cur_missing_reason = self._missing_loc_reason.get(member_id) + + # Check if location information is missing. This can happen if server + # has not heard from member's device in a long time (e.g., has been off + # for a long time, or has lost service, etc.) + if loc := member["location"]: + with suppress(KeyError): + del self._missing_loc_reason[member_id] + else: + if explicit_reason := member["issues"]["title"]: + if extended_reason := member["issues"]["dialog"]: + explicit_reason += f": {extended_reason}" + # Note that different Circles can report missing location in + # different ways. E.g., one might report an explicit reason and + # another does not. If a vague reason has already been logged but a + # more explicit reason is now available, log that, too. + if ( + cur_missing_reason is None + or cur_missing_reason == MissingLocReason.VAGUE_ERROR_REASON + and explicit_reason + ): + if explicit_reason: + self._missing_loc_reason[ + member_id + ] = MissingLocReason.EXPLICIT_ERROR_REASON + err_msg = explicit_reason + else: + self._missing_loc_reason[ + member_id + ] = MissingLocReason.VAGUE_ERROR_REASON + err_msg = "Location information missing" + LOGGER.error("%s: %s", name, err_msg) continue + # Note that member may be in more than one circle. If that's the case + # just go ahead and process the newly retrieved data (overwriting the + # older data), since it might be slightly newer than what was retrieved + # while processing another circle. + place = loc["name"] or None if place: @@ -179,7 +215,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator): if self._hass.config.units.is_metric: speed = convert(speed, LENGTH_MILES, LENGTH_KILOMETERS) - data.members[member["id"]] = Life360Member( + data.members[member_id] = Life360Member( address, dt_util.utc_from_timestamp(int(loc["since"])), bool(int(loc["charge"])), From dfe840c04599d475e31f3d9a3c76fb61d4bedbd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Jul 2022 08:51:24 +0200 Subject: [PATCH 2488/3516] Bump actions/setup-python from 4.0.0 to 4.1.0 (#75040) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 6 +++--- .github/workflows/ci.yaml | 24 ++++++++++++------------ .github/workflows/translations.yaml | 4 ++-- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index d49f1e3a9c7..ac30becb128 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -29,7 +29,7 @@ jobs: fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -70,7 +70,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -115,7 +115,7 @@ jobs: - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5320b9fe474..a5f3da97ac4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -166,7 +166,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} cache: "pip" @@ -205,7 +205,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -254,7 +254,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -306,7 +306,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -347,7 +347,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -470,7 +470,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ matrix.python-version }} - name: Generate partial pip restore key @@ -533,7 +533,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment @@ -565,7 +565,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment @@ -598,7 +598,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment @@ -642,7 +642,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment @@ -690,7 +690,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment @@ -744,7 +744,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 0d3ddc4ca18..bc9fa63c86c 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -24,7 +24,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v3.0.2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.0.0 + uses: actions/setup-python@v4.1.0 with: python-version: ${{ env.DEFAULT_PYTHON }} From 6a376009364935af7524751c8bff10097a0ce3e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huryn?= Date: Tue, 12 Jul 2022 10:01:53 +0200 Subject: [PATCH 2489/3516] Remove incorrect device class from blebox button (#75042) * Removed redundant attr device class from button. * Removed irrelevant test for checking buttons device_class. --- homeassistant/components/blebox/button.py | 3 +-- tests/components/blebox/test_button.py | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/components/blebox/button.py b/homeassistant/components/blebox/button.py index 01de5fa56b7..e9ceaac2dc7 100644 --- a/homeassistant/components/blebox/button.py +++ b/homeassistant/components/blebox/button.py @@ -1,7 +1,7 @@ """BleBox button entities implementation.""" from __future__ import annotations -from homeassistant.components.button import ButtonDeviceClass, ButtonEntity +from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -26,7 +26,6 @@ class BleBoxButtonEntity(BleBoxEntity, ButtonEntity): def __init__(self, feature): """Initialize a BleBox button feature.""" super().__init__(feature) - self._attr_device_class = ButtonDeviceClass.UPDATE self._attr_icon = self.get_icon() def get_icon(self): diff --git a/tests/components/blebox/test_button.py b/tests/components/blebox/test_button.py index 22e1b8cb734..8d89f31d260 100644 --- a/tests/components/blebox/test_button.py +++ b/tests/components/blebox/test_button.py @@ -5,7 +5,6 @@ from unittest.mock import PropertyMock import blebox_uniapi import pytest -from homeassistant.components.button import ButtonDeviceClass from homeassistant.const import ATTR_ICON from .conftest import async_setup_entity, mock_feature @@ -50,8 +49,6 @@ async def test_tvliftbox_init(tvliftbox, hass, config, caplog): assert entry.unique_id == "BleBox-tvLiftBox-4a3fdaad90aa-open_or_stop" - assert state.attributes["device_class"] == ButtonDeviceClass.UPDATE - assert state.name == "tvLiftBox-open_or_stop" From 5930f056a8245d8f27a7d54cb2c126a64eb13d98 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 12 Jul 2022 11:07:18 +0200 Subject: [PATCH 2490/3516] Mqtt support config_entry unload (#70149) * squashed commits for rebase * Flake * Fix reloading issue manual legacy items * Improve ACS sync for unsubscribe at disconnect * Processed review comments * Update homeassistant/components/mqtt/client.py Co-authored-by: Erik Montnemery * No need to await entry setup * Remove complication is_connected * Update homeassistant/components/mqtt/__init__.py Co-authored-by: Erik Montnemery --- homeassistant/components/mqtt/__init__.py | 126 +++++--- .../components/mqtt/alarm_control_panel.py | 8 +- .../components/mqtt/binary_sensor.py | 8 +- homeassistant/components/mqtt/button.py | 8 +- homeassistant/components/mqtt/camera.py | 8 +- homeassistant/components/mqtt/client.py | 59 +++- homeassistant/components/mqtt/climate.py | 8 +- homeassistant/components/mqtt/const.py | 4 +- homeassistant/components/mqtt/cover.py | 8 +- .../mqtt/device_tracker/__init__.py | 21 +- .../mqtt/device_tracker/schema_discovery.py | 23 +- .../mqtt/device_tracker/schema_yaml.py | 44 ++- homeassistant/components/mqtt/discovery.py | 3 +- homeassistant/components/mqtt/fan.py | 8 +- homeassistant/components/mqtt/humidifier.py | 8 +- .../components/mqtt/light/__init__.py | 11 +- homeassistant/components/mqtt/lock.py | 8 +- homeassistant/components/mqtt/mixins.py | 56 +++- homeassistant/components/mqtt/number.py | 8 +- homeassistant/components/mqtt/scene.py | 8 +- homeassistant/components/mqtt/select.py | 8 +- homeassistant/components/mqtt/sensor.py | 8 +- homeassistant/components/mqtt/siren.py | 8 +- homeassistant/components/mqtt/switch.py | 8 +- homeassistant/components/mqtt/util.py | 12 + .../components/mqtt/vacuum/__init__.py | 13 +- .../mqtt/test_alarm_control_panel.py | 10 + tests/components/mqtt/test_binary_sensor.py | 10 + tests/components/mqtt/test_button.py | 10 + tests/components/mqtt/test_camera.py | 10 + tests/components/mqtt/test_climate.py | 10 + tests/components/mqtt/test_common.py | 95 ++++++- tests/components/mqtt/test_config_flow.py | 7 +- tests/components/mqtt/test_cover.py | 10 + tests/components/mqtt/test_device_tracker.py | 120 +++++++- tests/components/mqtt/test_device_trigger.py | 52 ++++ tests/components/mqtt/test_fan.py | 10 + tests/components/mqtt/test_humidifier.py | 10 + tests/components/mqtt/test_init.py | 268 +++++++++++++++++- tests/components/mqtt/test_light.py | 10 + tests/components/mqtt/test_light_template.py | 10 + tests/components/mqtt/test_lock.py | 10 + tests/components/mqtt/test_number.py | 10 + tests/components/mqtt/test_scene.py | 10 + tests/components/mqtt/test_select.py | 10 + tests/components/mqtt/test_sensor.py | 10 + tests/components/mqtt/test_siren.py | 10 + tests/components/mqtt/test_switch.py | 10 + tests/components/mqtt/test_tag.py | 27 ++ 49 files changed, 1107 insertions(+), 124 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 143d97b928f..906923138b9 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -11,7 +11,7 @@ from typing import Any, cast import jinja2 import voluptuous as vol -from homeassistant import config_entries +from homeassistant import config as conf_util, config_entries from homeassistant.components import websocket_api from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -20,10 +20,9 @@ from homeassistant.const import ( CONF_PAYLOAD, CONF_PORT, CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, SERVICE_RELOAD, ) -from homeassistant.core import Event, HassJob, HomeAssistant, ServiceCall, callback +from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template @@ -65,9 +64,10 @@ from .const import ( # noqa: F401 CONF_TOPIC, CONF_WILL_MESSAGE, CONFIG_ENTRY_IS_SETUP, - DATA_CONFIG_ENTRY_LOCK, DATA_MQTT, DATA_MQTT_CONFIG, + DATA_MQTT_RELOAD_DISPATCHERS, + DATA_MQTT_RELOAD_ENTRY, DATA_MQTT_RELOAD_NEEDED, DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, @@ -87,7 +87,12 @@ from .models import ( # noqa: F401 ReceiveMessage, ReceivePayloadType, ) -from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic +from .util import ( + _VALID_QOS_SCHEMA, + mqtt_config_entry_enabled, + valid_publish_topic, + valid_subscribe_topic, +) _LOGGER = logging.getLogger(__name__) @@ -174,7 +179,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: conf = dict(conf) hass.data[DATA_MQTT_CONFIG] = conf - if not bool(hass.config_entries.async_entries(DOMAIN)): + if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is None: # Create an import flow if the user has yaml configured entities etc. # but no broker configuration. Note: The intention is not for this to # import broker configuration from YAML because that has been deprecated. @@ -185,6 +190,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: data={}, ) ) + hass.data[DATA_MQTT_RELOAD_NEEDED] = True + elif mqtt_entry_status is False: + _LOGGER.info( + "MQTT will be not available until the config entry is enabled", + ) + hass.data[DATA_MQTT_RELOAD_NEEDED] = True + return True @@ -239,17 +251,18 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) - await _async_setup_discovery(hass, mqtt_client.conf, entry) -async def async_setup_entry( # noqa: C901 - hass: HomeAssistant, entry: ConfigEntry -) -> bool: - """Load a config entry.""" - # Merge basic configuration, and add missing defaults for basic options - _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) +async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | None: + """Fetch fresh MQTT yaml config from the hass config when (re)loading the entry.""" + if DATA_MQTT_RELOAD_ENTRY in hass.data: + hass_config = await conf_util.async_hass_config_yaml(hass) + mqtt_config = CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {})) + hass.data[DATA_MQTT_CONFIG] = mqtt_config + _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) # Bail out if broker setting is missing if CONF_BROKER not in entry.data: _LOGGER.error("MQTT broker is not configured, please configure it") - return False + return None # If user doesn't have configuration.yaml config, generate default values # for options not in config entry data @@ -271,22 +284,21 @@ async def async_setup_entry( # noqa: C901 # Merge advanced configuration values from configuration.yaml conf = _merge_extended_config(entry, conf) + return conf - hass.data[DATA_MQTT] = MQTT( - hass, - entry, - conf, - ) + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Load a config entry.""" + # Merge basic configuration, and add missing defaults for basic options + if (conf := await async_fetch_config(hass, entry)) is None: + # Bail out + return False + + hass.data[DATA_MQTT] = MQTT(hass, entry, conf) entry.add_update_listener(_async_config_entry_updated) await hass.data[DATA_MQTT].async_connect() - async def async_stop_mqtt(_event: Event): - """Stop MQTT component.""" - await hass.data[DATA_MQTT].async_disconnect() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt) - async def async_publish_service(call: ServiceCall) -> None: """Handle MQTT publish service calls.""" msg_topic = call.data.get(ATTR_TOPIC) @@ -375,7 +387,6 @@ async def async_setup_entry( # noqa: C901 ) # setup platforms and discovery - hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[CONFIG_ENTRY_IS_SETUP] = set() async def async_setup_reload_service() -> None: @@ -411,6 +422,7 @@ async def async_setup_entry( # noqa: C901 # pylint: disable-next=import-outside-toplevel from . import device_automation, tag + # Forward the entry setup to the MQTT platforms await asyncio.gather( *( [ @@ -428,21 +440,25 @@ async def async_setup_entry( # noqa: C901 await _async_setup_discovery(hass, conf, entry) # Setup reload service after all platforms have loaded await async_setup_reload_service() - if DATA_MQTT_RELOAD_NEEDED in hass.data: hass.data.pop(DATA_MQTT_RELOAD_NEEDED) - await hass.services.async_call( - DOMAIN, - SERVICE_RELOAD, - {}, - blocking=False, - ) + await async_reload_manual_mqtt_items(hass) await async_forward_entry_setup_and_setup_discovery(entry) return True +async def async_reload_manual_mqtt_items(hass: HomeAssistant) -> None: + """Reload manual configured MQTT items.""" + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + + @websocket_api.websocket_command( {vol.Required("type"): "mqtt/device/debug_info", vol.Required("device_id"): str} ) @@ -544,3 +560,49 @@ async def async_remove_config_entry_device( await device_automation.async_removed_from_device(hass, device_entry.id) return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload MQTT dump and publish service when the config entry is unloaded.""" + # Unload publish and dump services. + hass.services.async_remove( + DOMAIN, + SERVICE_PUBLISH, + ) + hass.services.async_remove( + DOMAIN, + SERVICE_DUMP, + ) + + # Stop the discovery + await discovery.async_stop(hass) + mqtt_client: MQTT = hass.data[DATA_MQTT] + # Unload the platforms + await asyncio.gather( + *( + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ) + ) + await hass.async_block_till_done() + # Unsubscribe reload dispatchers + while reload_dispatchers := hass.data.setdefault(DATA_MQTT_RELOAD_DISPATCHERS, []): + reload_dispatchers.pop()() + hass.data[CONFIG_ENTRY_IS_SETUP] = set() + # Cleanup listeners + mqtt_client.cleanup() + + # Trigger reload manual MQTT items at entry setup + # Reload the legacy yaml platform + await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS) + if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is False: + # The entry is disabled reload legacy manual items when the entry is enabled again + hass.data[DATA_MQTT_RELOAD_NEEDED] = True + elif mqtt_entry_status is True: + # The entry is reloaded: + # Trigger re-fetching the yaml config at entry setup + hass.data[DATA_MQTT_RELOAD_ENTRY] = True + # Stop the loop + await mqtt_client.async_disconnect() + + return True diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index b6f2f8f236e..cf7262f9468 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -156,8 +156,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT Alarm Control Panel platform.""" async_add_entities([MqttAlarm(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 012c6e81ac8..cffb2fd8300 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -112,8 +112,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT binary sensor.""" async_add_entities([MqttBinarySensor(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index b75fbe4b97f..0881b963b04 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -91,8 +91,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT button.""" async_add_entities([MqttButton(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 69af7992229..f213bec9bb6 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -89,8 +89,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT Camera.""" async_add_entities([MqttCamera(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index d676c128260..9eeed426d17 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -23,8 +23,9 @@ from homeassistant.const import ( CONF_PROTOCOL, CONF_USERNAME, EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import CoreState, HassJob, HomeAssistant, callback +from homeassistant.core import CoreState, Event, HassJob, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.typing import ConfigType @@ -59,6 +60,7 @@ from .models import ( ReceiveMessage, ReceivePayloadType, ) +from .util import mqtt_config_entry_enabled if TYPE_CHECKING: # Only import for paho-mqtt type checking here, imports are done locally @@ -95,6 +97,10 @@ async def async_publish( ) -> None: """Publish message to a MQTT topic.""" + if DATA_MQTT not in hass.data or not mqtt_config_entry_enabled(hass): + raise HomeAssistantError( + f"Cannot publish to topic '{topic}', MQTT is not enabled" + ) outgoing_payload = payload if not isinstance(payload, bytes): if not encoding: @@ -174,6 +180,10 @@ async def async_subscribe( Call the return value to unsubscribe. """ + if DATA_MQTT not in hass.data or not mqtt_config_entry_enabled(hass): + raise HomeAssistantError( + f"Cannot subscribe to topic '{topic}', MQTT is not enabled" + ) # Count callback parameters which don't have a default value non_default = 0 if msg_callback: @@ -316,6 +326,8 @@ class MQTT: self._last_subscribe = time.time() self._mqttc: mqtt.Client = None self._paho_lock = asyncio.Lock() + self._pending_acks: set[int] = set() + self._cleanup_on_unload: list[Callable] = [] self._pending_operations: dict[str, asyncio.Event] = {} @@ -331,6 +343,20 @@ class MQTT: self.init_client() + @callback + async def async_stop_mqtt(_event: Event): + """Stop MQTT component.""" + await self.async_disconnect() + + self._cleanup_on_unload.append( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_mqtt) + ) + + def cleanup(self): + """Clean up listeners.""" + while self._cleanup_on_unload: + self._cleanup_on_unload.pop()() + def init_client(self): """Initialize paho client.""" self._mqttc = MqttClientSetup(self.conf).client @@ -405,6 +431,15 @@ class MQTT: # Do not disconnect, we want the broker to always publish will self._mqttc.loop_stop() + # wait for ACK-s to be processes (unsubscribe only) + async with self._paho_lock: + tasks = [ + self.hass.async_create_task(self._wait_for_mid(mid)) + for mid in self._pending_acks + ] + await asyncio.gather(*tasks) + + # stop the MQTT loop await self.hass.async_add_executor_job(stop) async def async_subscribe( @@ -440,7 +475,7 @@ class MQTT: self.subscriptions.remove(subscription) self._matching_subscriptions.cache_clear() - # Only unsubscribe if currently connected. + # Only unsubscribe if currently connected if self.connected: self.hass.async_create_task(self._async_unsubscribe(topic)) @@ -451,18 +486,20 @@ class MQTT: This method is a coroutine. """ + + def _client_unsubscribe(topic: str) -> None: + result: int | None = None + result, mid = self._mqttc.unsubscribe(topic) + _LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid) + _raise_on_error(result) + self._pending_acks.add(mid) + if any(other.topic == topic for other in self.subscriptions): # Other subscriptions on topic remaining - don't unsubscribe. return async with self._paho_lock: - result: int | None = None - result, mid = await self.hass.async_add_executor_job( - self._mqttc.unsubscribe, topic - ) - _LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid) - _raise_on_error(result) - await self._wait_for_mid(mid) + await self.hass.async_add_executor_job(_client_unsubscribe, topic) async def _async_perform_subscriptions( self, subscriptions: Iterable[tuple[str, int]] @@ -643,6 +680,10 @@ class MQTT: ) finally: del self._pending_operations[mid] + # Cleanup ACK sync buffer + async with self._paho_lock: + if mid in self._pending_acks: + self._pending_acks.remove(mid) async def _discovery_cooldown(self): now = time.time() diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 80ff33309ba..30263798740 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -401,8 +401,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT climate devices.""" async_add_entities([MqttClimate(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 6ac77021337..6a5cb912fce 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -31,9 +31,11 @@ CONF_TLS_INSECURE = "tls_insecure" CONF_TLS_VERSION = "tls_version" CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" -DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" DATA_MQTT = "mqtt" DATA_MQTT_CONFIG = "mqtt_config" +MQTT_DATA_DEVICE_TRACKER_LEGACY = "mqtt_device_tracker_legacy" +DATA_MQTT_RELOAD_DISPATCHERS = "mqtt_reload_dispatchers" +DATA_MQTT_RELOAD_ENTRY = "mqtt_reload_entry" DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed" DATA_MQTT_UPDATED_CONFIG = "mqtt_updated_config" diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 14746329250..b0fbacd10fc 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -251,8 +251,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT Cover.""" async_add_entities([MqttCover(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/device_tracker/__init__.py b/homeassistant/components/mqtt/device_tracker/__init__.py index 1b6c2b25ff3..99e0c87044b 100644 --- a/homeassistant/components/mqtt/device_tracker/__init__.py +++ b/homeassistant/components/mqtt/device_tracker/__init__.py @@ -2,7 +2,11 @@ import voluptuous as vol from homeassistant.components import device_tracker +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from ..const import MQTT_DATA_DEVICE_TRACKER_LEGACY from ..mixins import warn_for_legacy_schema from .schema_discovery import PLATFORM_SCHEMA_MODERN # noqa: F401 from .schema_discovery import async_setup_entry_from_discovery @@ -12,5 +16,20 @@ from .schema_yaml import PLATFORM_SCHEMA_YAML, async_setup_scanner_from_yaml PLATFORM_SCHEMA = vol.All( PLATFORM_SCHEMA_YAML, warn_for_legacy_schema(device_tracker.DOMAIN) ) + +# Legacy setup async_setup_scanner = async_setup_scanner_from_yaml -async_setup_entry = async_setup_entry_from_discovery + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up MQTT device_tracker through configuration.yaml and dynamically through MQTT discovery.""" + await async_setup_entry_from_discovery(hass, config_entry, async_add_entities) + # (re)load legacy service + if MQTT_DATA_DEVICE_TRACKER_LEGACY in hass.data: + await async_setup_scanner_from_yaml( + hass, **hass.data[MQTT_DATA_DEVICE_TRACKER_LEGACY] + ) diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 1ba540c8243..105442b176e 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -1,4 +1,6 @@ -"""Support for tracking MQTT enabled devices.""" +"""Support for tracking MQTT enabled devices identified through discovery.""" +from __future__ import annotations + import asyncio import functools @@ -7,6 +9,7 @@ import voluptuous as vol from homeassistant.components import device_tracker from homeassistant.components.device_tracker import SOURCE_TYPES from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, @@ -16,8 +19,10 @@ from homeassistant.const import ( STATE_HOME, STATE_NOT_HOME, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType from .. import subscription from ..config import MQTT_RO_SCHEMA @@ -47,7 +52,11 @@ PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) -async def async_setup_entry_from_discovery(hass, config_entry, async_add_entities): +async def async_setup_entry_from_discovery( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: """Set up MQTT device tracker configuration.yaml and dynamically through MQTT discovery.""" # load and initialize platform config from configuration.yaml await asyncio.gather( @@ -66,8 +75,12 @@ async def async_setup_entry_from_discovery(hass, config_entry, async_add_entitie async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT Device Tracker entity.""" async_add_entities([MqttDeviceTracker(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/device_tracker/schema_yaml.py b/homeassistant/components/mqtt/device_tracker/schema_yaml.py index 2dfa5b7134c..1990b380dcb 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_yaml.py +++ b/homeassistant/components/mqtt/device_tracker/schema_yaml.py @@ -1,16 +1,23 @@ """Support for tracking MQTT enabled devices defined in YAML.""" +from collections.abc import Callable +import logging +from typing import Any + import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPES from homeassistant.const import CONF_DEVICES, STATE_HOME, STATE_NOT_HOME -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv +from ... import mqtt from ..client import async_subscribe from ..config import SCHEMA_BASE -from ..const import CONF_QOS -from ..util import valid_subscribe_topic +from ..const import CONF_QOS, MQTT_DATA_DEVICE_TRACKER_LEGACY +from ..util import mqtt_config_entry_enabled, valid_subscribe_topic + +_LOGGER = logging.getLogger(__name__) CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" @@ -26,13 +33,34 @@ PLATFORM_SCHEMA_YAML = PLATFORM_SCHEMA.extend(SCHEMA_BASE).extend( ) -async def async_setup_scanner_from_yaml(hass, config, async_see, discovery_info=None): +async def async_setup_scanner_from_yaml( + hass: HomeAssistant, config, async_see, discovery_info=None +): """Set up the MQTT tracker.""" devices = config[CONF_DEVICES] qos = config[CONF_QOS] payload_home = config[CONF_PAYLOAD_HOME] payload_not_home = config[CONF_PAYLOAD_NOT_HOME] source_type = config.get(CONF_SOURCE_TYPE) + config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + subscriptions: list[Callable] = [] + + hass.data[MQTT_DATA_DEVICE_TRACKER_LEGACY] = { + "async_see": async_see, + "config": config, + } + if not mqtt_config_entry_enabled(hass): + _LOGGER.info( + "MQTT device trackers will be not available until the config entry is enabled", + ) + return + + @callback + def _entry_unload(*_: Any) -> None: + """Handle the unload of the config entry.""" + # Unsubscribe from mqtt + for unsubscribe in subscriptions: + unsubscribe() for dev_id, topic in devices.items(): @@ -52,6 +80,10 @@ async def async_setup_scanner_from_yaml(hass, config, async_see, discovery_info= hass.async_create_task(async_see(**see_args)) - await async_subscribe(hass, topic, async_message_received, qos) + subscriptions.append( + await async_subscribe(hass, topic, async_message_received, qos) + ) + + config_entry.async_on_unload(_entry_unload) return True diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 5b39e8fa1b5..a480cdd5680 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -234,8 +234,7 @@ async def async_start( # noqa: C901 hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None ) - hass.data[DATA_CONFIG_FLOW_LOCK] = asyncio.Lock() - + hass.data.setdefault(DATA_CONFIG_FLOW_LOCK, asyncio.Lock()) hass.data[ALREADY_DISCOVERED] = {} hass.data[PENDING_DISCOVERED] = {} diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 15e4a80f3e7..20c4936ab38 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -241,8 +241,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT fan.""" async_add_entities([MqttFan(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 43ff2af65d4..3a1271ea2c9 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -197,8 +197,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT humidifier.""" async_add_entities([MqttHumidifier(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index c7f3395ba4e..76c2980e63b 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -1,6 +1,7 @@ """Support for MQTT lights.""" from __future__ import annotations +from collections.abc import Callable import functools import voluptuous as vol @@ -120,10 +121,14 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up a MQTT Light.""" - setup_entity = { + setup_entity: dict[str, Callable] = { "basic": async_setup_entity_basic, "json": async_setup_entity_json, "template": async_setup_entity_template, diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index b4788f1db0c..4910eafae75 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -112,8 +112,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT Lock platform.""" async_add_entities([MqttLock(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 8fe9ee564de..af0660b4c93 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -3,7 +3,8 @@ from __future__ import annotations from abc import abstractmethod import asyncio -from collections.abc import Callable +from collections.abc import Callable, Coroutine +from functools import partial import logging from typing import Any, Protocol, cast, final @@ -61,7 +62,7 @@ from .const import ( CONF_TOPIC, DATA_MQTT, DATA_MQTT_CONFIG, - DATA_MQTT_RELOAD_NEEDED, + DATA_MQTT_RELOAD_DISPATCHERS, DATA_MQTT_UPDATED_CONFIG, DEFAULT_ENCODING, DEFAULT_PAYLOAD_AVAILABLE, @@ -84,7 +85,7 @@ from .subscription import ( async_subscribe_topics, async_unsubscribe_topics, ) -from .util import valid_subscribe_topic +from .util import mqtt_config_entry_enabled, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -299,11 +300,24 @@ async def async_get_platform_config_from_yaml( return platform_configs -async def async_setup_entry_helper(hass, domain, async_setup, schema): +async def async_setup_entry_helper( + hass: HomeAssistant, + domain: str, + async_setup: partial[Coroutine[HomeAssistant, str, None]], + schema: vol.Schema, +) -> None: """Set up entity, automation or tag creation dynamically through MQTT discovery.""" async def async_discover(discovery_payload): """Discover and add an MQTT entity, automation or tag.""" + if not mqtt_config_entry_enabled(hass): + _LOGGER.warning( + "MQTT integration is disabled, skipping setup of discovered item " + "MQTT %s, payload %s", + domain, + discovery_payload, + ) + return discovery_data = discovery_payload.discovery_data try: config = schema(discovery_payload) @@ -316,8 +330,10 @@ async def async_setup_entry_helper(hass, domain, async_setup, schema): ) raise - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(domain, "mqtt"), async_discover + hass.data.setdefault(DATA_MQTT_RELOAD_DISPATCHERS, []).append( + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(domain, "mqtt"), async_discover + ) ) @@ -328,16 +344,17 @@ async def async_setup_platform_helper( async_add_entities: AddEntitiesCallback, async_setup_entities: SetupEntity, ) -> None: - """Return true if platform setup should be aborted.""" - if not bool(hass.config_entries.async_entries(DOMAIN)): - hass.data[DATA_MQTT_RELOAD_NEEDED] = None + """Help to set up the platform for manual configured MQTT entities.""" + if not (entry_status := mqtt_config_entry_enabled(hass)): _LOGGER.warning( - "MQTT integration is not setup, skipping setup of manually configured " - "MQTT %s", + "MQTT integration is %s, skipping setup of manually configured MQTT %s", + "not setup" if entry_status is None else "disabled", platform_domain, ) return - await async_setup_entities(hass, async_add_entities, config) + # Ensure we set config_entry when entries are set up to enable clean up + config_entry: ConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] + await async_setup_entities(hass, async_add_entities, config, config_entry) def init_entity_id_from_config(hass, entity, config, entity_id_format): @@ -640,6 +657,7 @@ class MqttDiscoveryDeviceUpdate: MQTT_DISCOVERY_UPDATED.format(discovery_hash), self.async_discovery_update, ) + config_entry.async_on_unload(self._entry_unload) if device_id is not None: self._remove_device_updated = hass.bus.async_listen( EVENT_DEVICE_REGISTRY_UPDATED, self._async_device_removed @@ -650,6 +668,14 @@ class MqttDiscoveryDeviceUpdate: discovery_hash, ) + @callback + def _entry_unload(self, *_: Any) -> None: + """Handle cleanup when the config entry is unloaded.""" + stop_discovery_updates( + self.hass, self._discovery_data, self._remove_discovery_updated + ) + self.hass.async_add_job(self.async_tear_down()) + async def async_discovery_update( self, discovery_payload: DiscoveryInfoType | None, @@ -734,7 +760,11 @@ class MqttDiscoveryDeviceUpdate: class MqttDiscoveryUpdate(Entity): """Mixin used to handle updated discovery message for entity based platforms.""" - def __init__(self, discovery_data, discovery_update=None) -> None: + def __init__( + self, + discovery_data: dict, + discovery_update: Callable | None = None, + ) -> None: """Initialize the discovery update mixin.""" self._discovery_data = discovery_data self._discovery_update = discovery_update diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 9df9dbf818c..fbadd653df7 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -145,8 +145,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT number.""" async_add_entities([MqttNumber(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 8b654f7cca0..62de54505eb 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -88,8 +88,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT scene.""" async_add_entities([MqttScene(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index bdf55f895f3..ec88b1732d4 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -103,8 +103,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT select.""" async_add_entities([MqttSelect(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 0dda382bec4..4c04d6176f1 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -156,8 +156,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config: ConfigType, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up MQTT sensor.""" async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index f1de0d27de7..5ed76fd6330 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -152,8 +152,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT siren.""" async_add_entities([MqttSiren(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 34009695257..b5c7ab13dfc 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -111,8 +111,12 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT switch.""" async_add_entities([MqttSwitch(hass, config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index 66eec1bdfe8..9ef30da7f3b 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -1,9 +1,13 @@ """Utility functions for the MQTT integration.""" + +from __future__ import annotations + from typing import Any import voluptuous as vol from homeassistant.const import CONF_PAYLOAD +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, template from .const import ( @@ -13,9 +17,17 @@ from .const import ( ATTR_TOPIC, DEFAULT_QOS, DEFAULT_RETAIN, + DOMAIN, ) +def mqtt_config_entry_enabled(hass: HomeAssistant) -> bool | None: + """Return true when the MQTT config entry is enabled.""" + if not bool(hass.config_entries.async_entries(DOMAIN)): + return None + return not bool(hass.config_entries.async_entries(DOMAIN)[0].disabled_by) + + def valid_topic(value: Any) -> str: """Validate that this is a valid topic name/filter.""" value = cv.string(value) diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index c49b8cfa012..cdd14e6d8e3 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -100,10 +100,17 @@ async def async_setup_entry( async def _async_setup_entity( - hass, async_add_entities, config, config_entry=None, discovery_data=None -): + hass: HomeAssistant, + async_add_entities: AddEntitiesCallback, + config: ConfigType, + config_entry: ConfigEntry | None = None, + discovery_data: dict | None = None, +) -> None: """Set up the MQTT vacuum.""" - setup_entity = {LEGACY: async_setup_entity_legacy, STATE: async_setup_entity_state} + setup_entity = { + LEGACY: async_setup_entity_legacy, + STATE: async_setup_entity_state, + } await setup_entity[config[CONF_SCHEMA]]( hass, config, async_add_entities, config_entry, discovery_data ) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index f4a72829046..7d127902f3d 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -60,6 +60,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -971,3 +972,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = alarm_control_panel.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 20037a88d1c..658af79f20f 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -44,6 +44,7 @@ from .test_common import ( help_test_setting_attribute_with_template, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -1079,3 +1080,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = binary_sensor.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index 8748ef3be4d..68db846c91c 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -37,6 +37,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -482,3 +483,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = button.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 84bf4181a2c..c6d116b6a74 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -36,6 +36,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -346,3 +347,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = camera.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index c633f267e76..aec83a85227 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -55,6 +55,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -1881,3 +1882,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = climate.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 92feaa3c109..fe1f4003f0b 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -2,7 +2,7 @@ import copy from datetime import datetime import json -from unittest.mock import ANY, patch +from unittest.mock import ANY, MagicMock, patch import yaml @@ -11,6 +11,7 @@ from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info from homeassistant.components.mqtt.const import MQTT_DISCONNECTED from homeassistant.components.mqtt.mixins import MQTT_ATTRIBUTES_BLOCKED +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, @@ -1670,6 +1671,25 @@ async def help_test_reload_with_config(hass, caplog, tmp_path, config): assert "" in caplog.text +async def help_test_entry_reload_with_new_config(hass, tmp_path, new_config): + """Test reloading with supplied config.""" + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + assert mqtt_config_entry.state is ConfigEntryState.LOADED + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump(new_config) + new_yaml_config_file.write_text(new_yaml_config) + assert new_yaml_config_file.read_text() == new_yaml_config + + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file), patch( + "paho.mqtt.client.Client" + ) as mock_client: + mock_client().connect = lambda *args: 0 + # reload the config entry + assert await hass.config_entries.async_reload(mqtt_config_entry.entry_id) + assert mqtt_config_entry.state is ConfigEntryState.LOADED + await hass.async_block_till_done() + + async def help_test_reloadable( hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path, domain, config ): @@ -1782,6 +1802,7 @@ async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): domain: [new_config_1, new_config_2, new_config_3], } await help_test_reload_with_config(hass, caplog, tmp_path, new_config) + await hass.async_block_till_done() assert len(hass.states.async_all(domain)) == 3 @@ -1792,6 +1813,12 @@ async def help_test_reloadable_late(hass, caplog, tmp_path, domain, config): async def help_test_setup_manual_entity_from_yaml(hass, platform, config): """Help to test setup from yaml through configuration entry.""" + calls = MagicMock() + + async def mock_reload(hass): + """Mock reload.""" + calls() + config_structure = {mqtt.DOMAIN: {platform: config}} await async_setup_component(hass, mqtt.DOMAIN, config_structure) @@ -1799,7 +1826,71 @@ async def help_test_setup_manual_entity_from_yaml(hass, platform, config): entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) entry.add_to_hass(hass) - with patch("paho.mqtt.client.Client") as mock_client: + with patch( + "homeassistant.components.mqtt.async_reload_manual_mqtt_items", + side_effect=mock_reload, + ), patch("paho.mqtt.client.Client") as mock_client: mock_client().connect = lambda *args: 0 assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + calls.assert_called_once() + + +async def help_test_unload_config_entry(hass, tmp_path, newconfig): + """Test unloading the MQTT config entry.""" + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + assert mqtt_config_entry.state is ConfigEntryState.LOADED + + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump(newconfig) + new_yaml_config_file.write_text(new_yaml_config) + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): + assert await hass.config_entries.async_unload(mqtt_config_entry.entry_id) + assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED + await hass.async_block_till_done() + + +async def help_test_unload_config_entry_with_platform( + hass, + mqtt_mock_entry_with_yaml_config, + tmp_path, + domain, + config, +): + """Test unloading the MQTT config entry with a specific platform domain.""" + # prepare setup through configuration.yaml + config_setup = copy.deepcopy(config) + config_setup["name"] = "config_setup" + config_name = config_setup + assert await async_setup_component(hass, domain, {domain: [config_setup]}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + # prepare setup through discovery + discovery_setup = copy.deepcopy(config) + discovery_setup["name"] = "discovery_setup" + async_fire_mqtt_message( + hass, f"homeassistant/{domain}/bla/config", json.dumps(discovery_setup) + ) + await hass.async_block_till_done() + + # check if both entities were setup correctly + config_setup_entity = hass.states.get(f"{domain}.config_setup") + assert config_setup_entity + + discovery_setup_entity = hass.states.get(f"{domain}.discovery_setup") + assert discovery_setup_entity + + await help_test_unload_config_entry(hass, tmp_path, config_setup) + + async_fire_mqtt_message( + hass, f"homeassistant/{domain}/bla/config", json.dumps(discovery_setup) + ) + await hass.async_block_till_done() + + # check if both entities were unloaded correctly + config_setup_entity = hass.states.get(f"{domain}.{config_name}") + assert config_setup_entity is None + + discovery_setup_entity = hass.states.get(f"{domain}.discovery_setup") + assert discovery_setup_entity is None diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 208a6ce2d61..e40397fd1d4 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -145,12 +145,17 @@ async def test_manual_config_starts_discovery_flow( async def test_manual_config_set( - hass, mock_try_connection, mock_finish_setup, mqtt_client_mock + hass, + mock_try_connection, + mock_finish_setup, + mqtt_client_mock, ): """Test manual config does not create an entry, and entry can be setup late.""" # MQTT config present in yaml config assert await async_setup_component(hass, "mqtt", {"mqtt": {"broker": "bla"}}) await hass.async_block_till_done() + # do not try to reload + del hass.data["mqtt_reload_needed"] assert len(mock_finish_setup.mock_calls) == 0 mock_try_connection.return_value = True diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index c0d63cec1b4..3f85d4e89b1 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -73,6 +73,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -3364,3 +3365,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = cover.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index a9eb9b20825..6708703ddbb 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -1,13 +1,20 @@ """The tests for the MQTT device tracker platform using configuration.yaml.""" +import json from unittest.mock import patch import pytest from homeassistant.components.device_tracker.const import DOMAIN, SOURCE_TYPE_BLUETOOTH +from homeassistant.config_entries import ConfigEntryDisabler from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME, Platform from homeassistant.setup import async_setup_component -from .test_common import help_test_setup_manual_entity_from_yaml +from .test_common import ( + MockConfigEntry, + help_test_entry_reload_with_new_config, + help_test_setup_manual_entity_from_yaml, + help_test_unload_config_entry, +) from tests.common import async_fire_mqtt_message @@ -265,3 +272,114 @@ async def test_setup_with_modern_schema(hass, mock_device_tracker_conf): await help_test_setup_manual_entity_from_yaml(hass, DOMAIN, config) assert hass.states.get(entity_id) is not None + + +async def test_unload_entry( + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config, tmp_path +): + """Test unloading the config entry.""" + # setup through configuration.yaml + await mqtt_mock_entry_no_yaml_config() + dev_id = "jan" + entity_id = f"{DOMAIN}.{dev_id}" + topic = "/location/jan" + location = "home" + + hass.config.components = {"mqtt", "zone"} + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {dev_id: topic}}} + ) + async_fire_mqtt_message(hass, topic, location) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == location + + # setup through discovery + dev_id = "piet" + subscription = "/location/#" + domain = DOMAIN + discovery_config = { + "devices": {dev_id: subscription}, + "state_topic": "some-state", + "name": "piet", + } + async_fire_mqtt_message( + hass, f"homeassistant/{domain}/bla/config", json.dumps(discovery_config) + ) + await hass.async_block_till_done() + + # check that both entities were created + config_setup_entity = hass.states.get(f"{domain}.jan") + assert config_setup_entity + + discovery_setup_entity = hass.states.get(f"{domain}.piet") + assert discovery_setup_entity + + await help_test_unload_config_entry(hass, tmp_path, {}) + await hass.async_block_till_done() + + # check that both entities were unsubscribed and that the location was not processed + async_fire_mqtt_message(hass, "some-state", "not_home") + async_fire_mqtt_message(hass, "location/jan", "not_home") + await hass.async_block_till_done() + + config_setup_entity = hass.states.get(f"{domain}.jan") + assert config_setup_entity.state == location + + # the discovered tracker is an entity which state is removed at unload + discovery_setup_entity = hass.states.get(f"{domain}.piet") + assert discovery_setup_entity is None + + +async def test_reload_entry_legacy( + hass, mock_device_tracker_conf, mqtt_mock_entry_no_yaml_config, tmp_path +): + """Test reloading the config entry with manual MQTT items.""" + # setup through configuration.yaml + await mqtt_mock_entry_no_yaml_config() + entity_id = f"{DOMAIN}.jan" + topic = "location/jan" + location = "home" + + config = { + DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {"jan": topic}}, + } + hass.config.components = {"mqtt", "zone"} + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, topic, location) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == location + + await help_test_entry_reload_with_new_config(hass, tmp_path, config) + await hass.async_block_till_done() + + location = "not_home" + async_fire_mqtt_message(hass, topic, location) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == location + + +async def test_setup_with_disabled_entry( + hass, mock_device_tracker_conf, caplog +) -> None: + """Test setting up the platform with a disabled config entry.""" + # Try to setup the platform with a disabled config entry + config_entry = MockConfigEntry( + domain="mqtt", data={}, disabled_by=ConfigEntryDisabler.USER + ) + config_entry.add_to_hass(hass) + topic = "location/jan" + + config = { + DOMAIN: {CONF_PLATFORM: "mqtt", "devices": {"jan": topic}}, + } + hass.config.components = {"mqtt", "zone"} + + await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + assert ( + "MQTT device trackers will be not available until the config entry is enabled" + in caplog.text + ) diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 842e1dc4106..37a59ef6b53 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -12,6 +12,8 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.trigger import async_initialize_triggers from homeassistant.setup import async_setup_component +from .test_common import help_test_unload_config_entry + from tests.common import ( assert_lists_same, async_fire_mqtt_message, @@ -1372,3 +1374,53 @@ async def test_trigger_debug_info(hass, mqtt_mock_entry_no_yaml_config): == "homeassistant/device_automation/bla2/config" ) assert debug_info_data["triggers"][0]["discovery_data"]["payload"] == config2 + + +async def test_unload_entry(hass, calls, device_reg, mqtt_mock, tmp_path) -> None: + """Test unloading the MQTT entry.""" + + data1 = ( + '{ "automation_type":"trigger",' + ' "device":{"identifiers":["0AFFD2"]},' + ' "topic": "foobar/triggers/button1",' + ' "type": "button_short_press",' + ' "subtype": "button_1" }' + ) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) + await hass.async_block_till_done() + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": device_entry.id, + "discovery_id": "bla1", + "type": "button_short_press", + "subtype": "button_1", + }, + "action": { + "service": "test.automation", + "data_template": {"some": ("short_press")}, + }, + }, + ] + }, + ) + + # Fake short press 1 + async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") + await hass.async_block_till_done() + assert len(calls) == 1 + + await help_test_unload_config_entry(hass, tmp_path, {}) + + # Fake short press 2 + async_fire_mqtt_message(hass, "foobar/triggers/button1", "short_press") + await hass.async_block_till_done() + assert len(calls) == 1 diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index b9ca5e3888d..37dcefc9d3f 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -58,6 +58,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -1910,3 +1911,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = fan.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 0301e9e0481..38dc634578f 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -60,6 +60,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -1296,3 +1297,12 @@ async def test_config_schema_validation(hass): CONFIG_SCHEMA({DOMAIN: {platform: [config]}}) with pytest.raises(MultipleInvalid): CONFIG_SCHEMA({"mqtt": {"humidifier": [{"bla": "bla"}]}}) + + +async def test_unload_config_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = humidifier.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index b435798c241..de63528a08b 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -17,6 +17,7 @@ from homeassistant.components import mqtt from homeassistant.components.mqtt import CONFIG_SCHEMA, debug_info from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA from homeassistant.components.mqtt.models import ReceiveMessage +from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.const import ( ATTR_ASSUMED_STATE, EVENT_HOMEASSISTANT_STARTED, @@ -32,7 +33,10 @@ from homeassistant.helpers.entity import Entity from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from .test_common import help_test_setup_manual_entity_from_yaml +from .test_common import ( + help_test_entry_reload_with_new_config, + help_test_setup_manual_entity_from_yaml, +) from tests.common import ( MockConfigEntry, @@ -106,6 +110,18 @@ def record_calls(calls): return record_calls +@pytest.fixture +def empty_mqtt_config(hass, tmp_path): + """Fixture to provide an empty config from yaml.""" + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config_file.write_text("") + + with patch.object( + hass_config, "YAML_CONFIG_FILE", new_yaml_config_file + ) as empty_config: + yield empty_config + + async def test_mqtt_connects_on_home_assistant_mqtt_setup( hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config ): @@ -115,14 +131,14 @@ async def test_mqtt_connects_on_home_assistant_mqtt_setup( async def test_mqtt_disconnects_on_home_assistant_stop( - hass, mqtt_mock_entry_no_yaml_config + hass, mqtt_mock_entry_no_yaml_config, mqtt_client_mock ): """Test if client stops on HA stop.""" - mqtt_mock = await mqtt_mock_entry_no_yaml_config() + await mqtt_mock_entry_no_yaml_config() hass.bus.fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() await hass.async_block_till_done() - assert mqtt_mock.async_disconnect.called + assert mqtt_client_mock.loop_stop.call_count == 1 async def test_publish(hass, mqtt_mock_entry_no_yaml_config): @@ -521,8 +537,11 @@ async def test_service_call_with_ascii_qos_retain_flags( assert not mqtt_mock.async_publish.call_args[0][3] -async def test_publish_function_with_bad_encoding_conditions(hass, caplog): - """Test internal publish function with bas use cases.""" +async def test_publish_function_with_bad_encoding_conditions( + hass, caplog, mqtt_mock_entry_no_yaml_config +): + """Test internal publish function with basic use cases.""" + await mqtt_mock_entry_no_yaml_config() await mqtt.async_publish( hass, "some-topic", "test-payload", qos=0, retain=False, encoding=None ) @@ -1249,13 +1268,18 @@ async def test_restore_all_active_subscriptions_on_reconnect( assert mqtt_client_mock.subscribe.mock_calls == expected -async def test_initial_setup_logs_error(hass, caplog, mqtt_client_mock): +async def test_initial_setup_logs_error( + hass, caplog, mqtt_client_mock, empty_mqtt_config +): """Test for setup failure if initial client connection fails.""" entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) - + entry.add_to_hass(hass) mqtt_client_mock.connect.return_value = 1 - assert await mqtt.async_setup_entry(hass, entry) - await hass.async_block_till_done() + try: + assert await mqtt.async_setup_entry(hass, entry) + await hass.async_block_till_done() + except HomeAssistantError: + assert True assert "Failed to connect to MQTT server:" in caplog.text @@ -1298,6 +1322,7 @@ async def test_handle_mqtt_on_callback( async def test_publish_error(hass, caplog): """Test publish error.""" entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry.add_to_hass(hass) # simulate an Out of memory error with patch("paho.mqtt.client.Client") as mock_client: @@ -1365,6 +1390,7 @@ async def test_setup_override_configuration(hass, caplog, tmp_path): domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker", "password": "somepassword"}, ) + entry.add_to_hass(hass) with patch("paho.mqtt.client.Client") as mock_client: mock_client().username_pw_set = mock_usename_password_set @@ -1429,9 +1455,11 @@ async def test_setup_mqtt_client_protocol(hass): mqtt.config_integration.CONF_PROTOCOL: "3.1", }, ) + entry.add_to_hass(hass) with patch("paho.mqtt.client.Client") as mock_client: mock_client.on_connect(return_value=0) assert await mqtt.async_setup_entry(hass, entry) + await hass.async_block_till_done() # check if protocol setup was correctly assert mock_client.call_args[1]["protocol"] == 3 @@ -1467,15 +1495,18 @@ async def test_handle_mqtt_timeout_on_callback(hass, caplog): entry = MockConfigEntry( domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"} ) - # Set up the integration - assert await mqtt.async_setup_entry(hass, entry) + entry.add_to_hass(hass) + # Make sure we are connected correctly mock_client.on_connect(mock_client, None, None, 0) + # Set up the integration + assert await mqtt.async_setup_entry(hass, entry) + await hass.async_block_till_done() # Now call we publish without simulating and ACK callback await mqtt.async_publish(hass, "no_callback/test-topic", "test-payload") await hass.async_block_till_done() - # The is no ACK so we should see a timeout in the log after publishing + # There is no ACK so we should see a timeout in the log after publishing assert len(mock_client.publish.mock_calls) == 1 assert "No ACK from MQTT server" in caplog.text @@ -1483,10 +1514,12 @@ async def test_handle_mqtt_timeout_on_callback(hass, caplog): async def test_setup_raises_ConfigEntryNotReady_if_no_connect_broker(hass, caplog): """Test for setup failure if connection to broker is missing.""" entry = MockConfigEntry(domain=mqtt.DOMAIN, data={mqtt.CONF_BROKER: "test-broker"}) + entry.add_to_hass(hass) with patch("paho.mqtt.client.Client") as mock_client: mock_client().connect = MagicMock(side_effect=OSError("Connection error")) assert await mqtt.async_setup_entry(hass, entry) + await hass.async_block_till_done() assert "Failed to connect to MQTT server due to exception:" in caplog.text @@ -1514,8 +1547,9 @@ async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( domain=mqtt.DOMAIN, data=config_item_data, ) - + entry.add_to_hass(hass) assert await mqtt.async_setup_entry(hass, entry) + await hass.async_block_till_done() assert calls @@ -1546,8 +1580,9 @@ async def test_setup_without_tls_config_uses_tlsv1_under_python36(hass): domain=mqtt.DOMAIN, data={"certificate": "auto", mqtt.CONF_BROKER: "test-broker"}, ) - + entry.add_to_hass(hass) assert await mqtt.async_setup_entry(hass, entry) + await hass.async_block_till_done() assert calls @@ -2644,3 +2679,206 @@ async def test_config_schema_validation(hass): config = {"mqtt": {"sensor": [{"some_illegal_topic": "mystate/topic/path"}]}} with pytest.raises(vol.MultipleInvalid): CONFIG_SCHEMA(config) + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) +async def test_unload_config_entry( + hass, mqtt_mock, mqtt_client_mock, tmp_path, caplog +) -> None: + """Test unloading the MQTT entry.""" + assert hass.services.has_service(mqtt.DOMAIN, "dump") + assert hass.services.has_service(mqtt.DOMAIN, "publish") + + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + assert mqtt_config_entry.state is ConfigEntryState.LOADED + + # Publish just before unloading to test await cleanup + mqtt_client_mock.reset_mock() + mqtt.publish(hass, "just_in_time", "published", qos=0, retain=False) + + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump({}) + new_yaml_config_file.write_text(new_yaml_config) + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file): + assert await hass.config_entries.async_unload(mqtt_config_entry.entry_id) + mqtt_client_mock.publish.assert_any_call("just_in_time", "published", 0, False) + assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED + await hass.async_block_till_done() + assert not hass.services.has_service(mqtt.DOMAIN, "dump") + assert not hass.services.has_service(mqtt.DOMAIN, "publish") + assert "No ACK from MQTT server" not in caplog.text + + +@patch("homeassistant.components.mqtt.PLATFORMS", []) +async def test_setup_with_disabled_entry(hass, caplog) -> None: + """Test setting up the platform with a disabled config entry.""" + # Try to setup the platform with a disabled config entry + config_entry = MockConfigEntry( + domain=mqtt.DOMAIN, data={}, disabled_by=ConfigEntryDisabler.USER + ) + config_entry.add_to_hass(hass) + + config = {mqtt.DOMAIN: {}} + await async_setup_component(hass, mqtt.DOMAIN, config) + await hass.async_block_till_done() + + assert "MQTT will be not available until the config entry is enabled" in caplog.text + + +@patch("homeassistant.components.mqtt.PLATFORMS", []) +async def test_publish_or_subscribe_without_valid_config_entry(hass, caplog): + """Test internal publish function with bas use cases.""" + with pytest.raises(HomeAssistantError): + await mqtt.async_publish( + hass, "some-topic", "test-payload", qos=0, retain=False, encoding=None + ) + with pytest.raises(HomeAssistantError): + await mqtt.async_subscribe(hass, "some-topic", lambda: None, qos=0) + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) +async def test_reload_entry_with_new_config(hass, tmp_path): + """Test reloading the config entry with a new yaml config.""" + config_old = [{"name": "test_old1", "command_topic": "test-topic_old"}] + config_yaml_new = { + "mqtt": { + "light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}] + }, + "light": [ + { + "platform": "mqtt", + "name": "test_new_legacy", + "command_topic": "test-topic_new", + } + ], + } + await help_test_setup_manual_entity_from_yaml(hass, "light", config_old) + assert hass.states.get("light.test_old1") is not None + + await help_test_entry_reload_with_new_config(hass, tmp_path, config_yaml_new) + assert hass.states.get("light.test_old1") is None + assert hass.states.get("light.test_new_modern") is not None + assert hass.states.get("light.test_new_legacy") is not None + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) +async def test_disabling_and_enabling_entry(hass, tmp_path, caplog): + """Test disabling and enabling the config entry.""" + config_old = [{"name": "test_old1", "command_topic": "test-topic_old"}] + config_yaml_new = { + "mqtt": { + "light": [{"name": "test_new_modern", "command_topic": "test-topic_new"}] + }, + "light": [ + { + "platform": "mqtt", + "name": "test_new_legacy", + "command_topic": "test-topic_new", + } + ], + } + await help_test_setup_manual_entity_from_yaml(hass, "light", config_old) + assert hass.states.get("light.test_old1") is not None + + mqtt_config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + + assert mqtt_config_entry.state is ConfigEntryState.LOADED + new_yaml_config_file = tmp_path / "configuration.yaml" + new_yaml_config = yaml.dump(config_yaml_new) + new_yaml_config_file.write_text(new_yaml_config) + assert new_yaml_config_file.read_text() == new_yaml_config + + with patch.object(hass_config, "YAML_CONFIG_FILE", new_yaml_config_file), patch( + "paho.mqtt.client.Client" + ) as mock_client: + mock_client().connect = lambda *args: 0 + + # Late discovery of a light + config = '{"name": "abc", "command_topic": "test-topic"}' + async_fire_mqtt_message(hass, "homeassistant/light/abc/config", config) + + # Disable MQTT config entry + await hass.config_entries.async_set_disabled_by( + mqtt_config_entry.entry_id, ConfigEntryDisabler.USER + ) + + await hass.async_block_till_done() + await hass.async_block_till_done() + # Assert that the discovery was still received + # but kipped the setup + assert ( + "MQTT integration is disabled, skipping setup of manually configured MQTT light" + in caplog.text + ) + + assert mqtt_config_entry.state is ConfigEntryState.NOT_LOADED + assert hass.states.get("light.test_old1") is None + + # Enable the entry again + await hass.config_entries.async_set_disabled_by( + mqtt_config_entry.entry_id, None + ) + await hass.async_block_till_done() + await hass.async_block_till_done() + assert mqtt_config_entry.state is ConfigEntryState.LOADED + + assert hass.states.get("light.test_old1") is None + assert hass.states.get("light.test_new_modern") is not None + assert hass.states.get("light.test_new_legacy") is not None + + +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) +@pytest.mark.parametrize( + "config, unique", + [ + ( + [ + { + "name": "test1", + "unique_id": "very_not_unique_deadbeef", + "command_topic": "test-topic_unique", + }, + { + "name": "test2", + "unique_id": "very_not_unique_deadbeef", + "command_topic": "test-topic_unique", + }, + ], + False, + ), + ( + [ + { + "name": "test1", + "unique_id": "very_unique_deadbeef1", + "command_topic": "test-topic_unique", + }, + { + "name": "test2", + "unique_id": "very_unique_deadbeef2", + "command_topic": "test-topic_unique", + }, + ], + True, + ), + ], +) +async def test_setup_manual_items_with_unique_ids( + hass, tmp_path, caplog, config, unique +): + """Test setup manual items is generating unique id's.""" + await help_test_setup_manual_entity_from_yaml(hass, "light", config) + + assert hass.states.get("light.test1") is not None + assert (hass.states.get("light.test2") is not None) == unique + assert bool("Platform mqtt does not generate unique IDs." in caplog.text) != unique + + # reload and assert again + caplog.clear() + await help_test_entry_reload_with_new_config( + hass, tmp_path, {"mqtt": {"light": config}} + ) + + assert hass.states.get("light.test1") is not None + assert (hass.states.get("light.test2") is not None) == unique + assert bool("Platform mqtt does not generate unique IDs." in caplog.text) != unique diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 4d8d8f24a3c..bfafc99a9e2 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -240,6 +240,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -3803,3 +3804,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = light.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 6e271d08651..2c96468057f 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -72,6 +72,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -1266,3 +1267,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = light.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 1bf4183e60f..f6dc4a0ed6d 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -48,6 +48,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -748,3 +749,12 @@ async def test_setup_manual_entity_from_yaml(hass, caplog, tmp_path): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = LOCK_DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 1db7c5e3463..458f1f740e1 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -57,6 +57,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -853,3 +854,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = number.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index 3036565dad5..713410059fe 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -22,6 +22,7 @@ from .test_common import ( help_test_reloadable_late, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, ) DEFAULT_CONFIG = { @@ -237,3 +238,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = scene.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_select.py b/tests/components/mqtt/test_select.py index c22bd43b86f..4c3a0523951 100644 --- a/tests/components/mqtt/test_select.py +++ b/tests/components/mqtt/test_select.py @@ -48,6 +48,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -687,3 +688,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = select.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index f30bcf43392..ab094c20b6f 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -58,6 +58,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -1213,3 +1214,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = sensor.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index 6da9682c1c7..13648f1c486 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -45,6 +45,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -975,3 +976,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = siren.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index ba23efc859c..af6c0f99f50 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -42,6 +42,7 @@ from .test_common import ( help_test_setting_blocked_attribute_via_mqtt_json_message, help_test_setup_manual_entity_from_yaml, help_test_unique_id, + help_test_unload_config_entry_with_platform, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, ) @@ -664,3 +665,12 @@ async def test_setup_manual_entity_from_yaml(hass): del config["platform"] await help_test_setup_manual_entity_from_yaml(hass, platform, config) assert hass.states.get(f"{platform}.test") is not None + + +async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path): + """Test unloading the config entry.""" + domain = switch.DOMAIN + config = DEFAULT_CONFIG[domain] + await help_test_unload_config_entry_with_platform( + hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config + ) diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index f06dd6f5244..507c6d99bed 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -11,6 +11,8 @@ from homeassistant.const import Platform from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component +from .test_common import help_test_unload_config_entry + from tests.common import ( MockConfigEntry, async_fire_mqtt_message, @@ -797,3 +799,28 @@ async def test_cleanup_device_with_entity2( # Verify device registry entry is cleared device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None + + +async def test_unload_entry(hass, device_reg, mqtt_mock, tag_mock, tmp_path) -> None: + """Test unloading the MQTT entry.""" + + config = copy.deepcopy(DEFAULT_CONFIG_DEVICE) + + async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) + await hass.async_block_till_done() + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) + + # Fake tag scan, should be processed + async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) + await hass.async_block_till_done() + tag_mock.assert_called_once_with(ANY, DEFAULT_TAG_ID, device_entry.id) + + tag_mock.reset_mock() + + await help_test_unload_config_entry(hass, tmp_path, {}) + await hass.async_block_till_done() + + # Fake tag scan, should not be processed + async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) + await hass.async_block_till_done() + tag_mock.assert_not_called() From ff1cdb4de7522e04ddb8607c1da11386dee228e6 Mon Sep 17 00:00:00 2001 From: Thijs W Date: Tue, 12 Jul 2022 12:03:26 +0200 Subject: [PATCH 2491/3516] Bump afsapi to 0.2.6 (#75041) --- homeassistant/components/frontier_silicon/manifest.json | 2 +- homeassistant/components/frontier_silicon/media_player.py | 7 +++++-- requirements_all.txt | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontier_silicon/manifest.json b/homeassistant/components/frontier_silicon/manifest.json index 12fb5145aa0..b04d68e672d 100644 --- a/homeassistant/components/frontier_silicon/manifest.json +++ b/homeassistant/components/frontier_silicon/manifest.json @@ -2,7 +2,7 @@ "domain": "frontier_silicon", "name": "Frontier Silicon", "documentation": "https://www.home-assistant.io/integrations/frontier_silicon", - "requirements": ["afsapi==0.2.5"], + "requirements": ["afsapi==0.2.6"], "codeowners": ["@wlcrs"], "iot_class": "local_polling" } diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 61dc6e69726..b16374c0bc0 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -179,11 +179,14 @@ class AFSAPIDevice(MediaPlayerEntity): self._attr_media_artist = await afsapi.get_play_artist() self._attr_media_album_name = await afsapi.get_play_album() - self._attr_source = (await afsapi.get_mode()).label + radio_mode = await afsapi.get_mode() + self._attr_source = radio_mode.label if radio_mode is not None else None self._attr_is_volume_muted = await afsapi.get_mute() self._attr_media_image_url = await afsapi.get_play_graphic() - self._attr_sound_mode = (await afsapi.get_eq_preset()).label + + eq_preset = await afsapi.get_eq_preset() + self._attr_sound_mode = eq_preset.label if eq_preset is not None else None volume = await self.fs_device.get_volume() diff --git a/requirements_all.txt b/requirements_all.txt index 933ca47b086..597efbc799b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -89,7 +89,7 @@ adguardhome==0.5.1 advantage_air==0.3.1 # homeassistant.components.frontier_silicon -afsapi==0.2.5 +afsapi==0.2.6 # homeassistant.components.agent_dvr agent-py==0.0.23 From cd25bc19013e9b981d4afeaefef85d4a4da00c5e Mon Sep 17 00:00:00 2001 From: hahn-th Date: Tue, 12 Jul 2022 15:31:04 +0200 Subject: [PATCH 2492/3516] Bump homematicip to 1.0.4 (#75053) --- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 40f7e67fd07..f6fb9f3e739 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==1.0.3"], + "requirements": ["homematicip==1.0.4"], "codeowners": [], "quality_scale": "platinum", "iot_class": "cloud_push", diff --git a/requirements_all.txt b/requirements_all.txt index 597efbc799b..53823fbc756 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -837,7 +837,7 @@ home-assistant-frontend==20220707.0 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.3 +homematicip==1.0.4 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d94346b384..193daabeabc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -607,7 +607,7 @@ home-assistant-frontend==20220707.0 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.3 +homematicip==1.0.4 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 From 7283d1b7fbb715119a9ef94c9c0c4378e5a266e0 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 12 Jul 2022 15:35:12 +0200 Subject: [PATCH 2493/3516] Migrate Brother to new entity naming style (#75000) --- homeassistant/components/brother/sensor.py | 51 +++++++++++----------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index a589ea0bd77..b6af96087af 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -111,6 +111,8 @@ async def async_setup_entry( class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): """Define an Brother Printer sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: BrotherDataUpdateCoordinator, @@ -121,7 +123,6 @@ class BrotherPrinterSensor(CoordinatorEntity, SensorEntity): super().__init__(coordinator) self._attrs: dict[str, Any] = {} self._attr_device_info = device_info - self._attr_name = f"{coordinator.data.model} {description.name}" self._attr_unique_id = f"{coordinator.data.serial.lower()}_{description.key}" self.entity_description = description @@ -168,13 +169,13 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_STATUS, icon="mdi:printer", - name=ATTR_STATUS.title(), + name="Status", entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_PAGE_COUNTER, icon="mdi:file-document-outline", - name=ATTR_PAGE_COUNTER.replace("_", " ").title(), + name="Page counter", native_unit_of_measurement=UNIT_PAGES, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -182,7 +183,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_BW_COUNTER, icon="mdi:file-document-outline", - name=ATTR_BW_COUNTER.replace("_", " ").title(), + name="B/W counter", native_unit_of_measurement=UNIT_PAGES, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -190,7 +191,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_COLOR_COUNTER, icon="mdi:file-document-outline", - name=ATTR_COLOR_COUNTER.replace("_", " ").title(), + name="Color counter", native_unit_of_measurement=UNIT_PAGES, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -198,7 +199,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_DUPLEX_COUNTER, icon="mdi:file-document-outline", - name=ATTR_DUPLEX_COUNTER.replace("_", " ").title(), + name="Duplex unit pages counter", native_unit_of_measurement=UNIT_PAGES, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -206,7 +207,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - name=ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name="Drum remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -214,7 +215,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_BLACK_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - name=ATTR_BLACK_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name="Black drum remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -222,7 +223,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_CYAN_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - name=ATTR_CYAN_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name="Cyan drum remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -230,7 +231,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_MAGENTA_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - name=ATTR_MAGENTA_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name="Magenta drum remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -238,7 +239,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_YELLOW_DRUM_REMAINING_LIFE, icon="mdi:chart-donut", - name=ATTR_YELLOW_DRUM_REMAINING_LIFE.replace("_", " ").title(), + name="Yellow drum remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -246,7 +247,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_BELT_UNIT_REMAINING_LIFE, icon="mdi:current-ac", - name=ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), + name="Belt unit remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -254,7 +255,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_FUSER_REMAINING_LIFE, icon="mdi:water-outline", - name=ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), + name="Fuser remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -262,7 +263,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_LASER_REMAINING_LIFE, icon="mdi:spotlight-beam", - name=ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), + name="Laser remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -270,7 +271,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_PF_KIT_1_REMAINING_LIFE, icon="mdi:printer-3d", - name=ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), + name="PF Kit 1 remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -278,7 +279,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_PF_KIT_MP_REMAINING_LIFE, icon="mdi:printer-3d", - name=ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), + name="PF Kit MP remaining life", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -286,7 +287,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_BLACK_TONER_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), + name="Black toner remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -294,7 +295,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_CYAN_TONER_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), + name="Cyan toner remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -302,7 +303,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_MAGENTA_TONER_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), + name="Magenta toner remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -310,7 +311,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_YELLOW_TONER_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), + name="Yellow toner remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -318,7 +319,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_BLACK_INK_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), + name="Black ink remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -326,7 +327,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_CYAN_INK_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), + name="Cyan ink remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -334,7 +335,7 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_MAGENTA_INK_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), + name="Magenta ink remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, @@ -342,14 +343,14 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = ( BrotherSensorEntityDescription( key=ATTR_YELLOW_INK_REMAINING, icon="mdi:printer-3d-nozzle", - name=ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), + name="Yellow ink remaining", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), BrotherSensorEntityDescription( key=ATTR_UPTIME, - name=ATTR_UPTIME.title(), + name="Uptime", entity_registry_enabled_default=False, device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, From 5fdae0fc5bbf574bc8391e738a9b0285f3c9a095 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Tue, 12 Jul 2022 15:56:16 +0200 Subject: [PATCH 2494/3516] Migrate HomeWizard to new entity naming style (#74958) --- homeassistant/components/homewizard/sensor.py | 33 +++++++-------- homeassistant/components/homewizard/switch.py | 9 ++--- tests/components/homewizard/test_sensor.py | 34 ++++++++-------- tests/components/homewizard/test_switch.py | 40 ++++++------------- 4 files changed, 50 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index de39ce6f242..a66a2664ae1 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -31,25 +31,25 @@ _LOGGER = logging.getLogger(__name__) SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( SensorEntityDescription( key="smr_version", - name="DSMR Version", + name="DSMR version", icon="mdi:counter", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="meter_model", - name="Smart Meter Model", + name="Smart meter model", icon="mdi:gauge", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="wifi_ssid", - name="Wifi SSID", + name="Wi-Fi SSID", icon="mdi:wifi", entity_category=EntityCategory.DIAGNOSTIC, ), SensorEntityDescription( key="wifi_strength", - name="Wifi Strength", + name="Wi-Fi strength", icon="mdi:wifi", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, @@ -58,77 +58,77 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( ), SensorEntityDescription( key="total_power_import_t1_kwh", - name="Total Power Import T1", + name="Total power import T1", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="total_power_import_t2_kwh", - name="Total Power Import T2", + name="Total power import T2", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="total_power_export_t1_kwh", - name="Total Power Export T1", + name="Total power export T1", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="total_power_export_t2_kwh", - name="Total Power Export T2", + name="Total power export T2", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="active_power_w", - name="Active Power", + name="Active power", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="active_power_l1_w", - name="Active Power L1", + name="Active power L1", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="active_power_l2_w", - name="Active Power L2", + name="Active power L2", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="active_power_l3_w", - name="Active Power L3", + name="Active power L3", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="total_gas_m3", - name="Total Gas", + name="Total gas", native_unit_of_measurement=VOLUME_CUBIC_METERS, device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="active_liter_lpm", - name="Active Water Usage", + name="Active water usage", native_unit_of_measurement="l/min", icon="mdi:water", state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="total_liter_m3", - name="Total Water Usage", + name="Total water usage", native_unit_of_measurement=VOLUME_CUBIC_METERS, icon="mdi:gauge", state_class=SensorStateClass.TOTAL_INCREASING, @@ -153,6 +153,8 @@ async def async_setup_entry( class HWEnergySensor(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SensorEntity): """Representation of a HomeWizard Sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: HWEnergyDeviceUpdateCoordinator, @@ -166,7 +168,6 @@ class HWEnergySensor(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SensorE self.entry = entry # Config attributes. - self._attr_name = f"{entry.title} {description.name}" self.data_type = description.key self._attr_unique_id = f"{entry.unique_id}_{description.key}" diff --git a/homeassistant/components/homewizard/switch.py b/homeassistant/components/homewizard/switch.py index eb2e9c49afe..eca8a7670be 100644 --- a/homeassistant/components/homewizard/switch.py +++ b/homeassistant/components/homewizard/switch.py @@ -36,6 +36,8 @@ class HWEnergySwitchEntity( ): """Representation switchable entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: HWEnergyDeviceUpdateCoordinator, @@ -65,9 +67,6 @@ class HWEnergyMainSwitchEntity(HWEnergySwitchEntity): """Initialize the switch.""" super().__init__(coordinator, entry, "power_on") - # Config attributes - self._attr_name = f"{entry.title} Switch" - async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self.coordinator.api.state_set(power_on=True) @@ -101,6 +100,7 @@ class HWEnergySwitchLockEntity(HWEnergySwitchEntity): It disables any method that can turn of the relay. """ + _attr_name = "Switch lock" _attr_device_class = SwitchDeviceClass.SWITCH _attr_entity_category = EntityCategory.CONFIG @@ -110,9 +110,6 @@ class HWEnergySwitchLockEntity(HWEnergySwitchEntity): """Initialize the switch.""" super().__init__(coordinator, entry, "switch_lock") - # Config attributes - self._attr_name = f"{entry.title} Switch Lock" - async def async_turn_on(self, **kwargs: Any) -> None: """Turn switch-lock on.""" await self.coordinator.api.state_set(switch_lock=True) diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index a81291861a2..85b6dc58235 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -61,7 +61,7 @@ async def test_sensor_entity_smr_version( assert state.state == "50" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) DSMR Version" + == "Product Name (aabbccddeeff) DSMR version" ) assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -101,7 +101,7 @@ async def test_sensor_entity_meter_model( assert state.state == "Model X" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Smart Meter Model" + == "Product Name (aabbccddeeff) Smart meter model" ) assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -128,8 +128,8 @@ async def test_sensor_entity_wifi_ssid(hass, mock_config_entry_data, mock_config entity_registry = er.async_get(hass) - state = hass.states.get("sensor.product_name_aabbccddeeff_wifi_ssid") - entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_wifi_ssid") + state = hass.states.get("sensor.product_name_aabbccddeeff_wi_fi_ssid") + entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_wi_fi_ssid") assert entry assert state assert entry.unique_id == "aabbccddeeff_wifi_ssid" @@ -137,7 +137,7 @@ async def test_sensor_entity_wifi_ssid(hass, mock_config_entry_data, mock_config assert state.state == "My Wifi" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Wifi SSID" + == "Product Name (aabbccddeeff) Wi-Fi SSID" ) assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes @@ -166,7 +166,7 @@ async def test_sensor_entity_wifi_strength( entity_registry = er.async_get(hass) - entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_wifi_strength") + entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_wi_fi_strength") assert entry assert entry.unique_id == "aabbccddeeff_wifi_strength" assert entry.disabled @@ -206,7 +206,7 @@ async def test_sensor_entity_total_power_import_t1_kwh( assert state.state == "1234.123" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Total Power Import T1" + == "Product Name (aabbccddeeff) Total power import T1" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -248,7 +248,7 @@ async def test_sensor_entity_total_power_import_t2_kwh( assert state.state == "1234.123" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Total Power Import T2" + == "Product Name (aabbccddeeff) Total power import T2" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -290,7 +290,7 @@ async def test_sensor_entity_total_power_export_t1_kwh( assert state.state == "1234.123" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Total Power Export T1" + == "Product Name (aabbccddeeff) Total power export T1" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -332,7 +332,7 @@ async def test_sensor_entity_total_power_export_t2_kwh( assert state.state == "1234.123" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Total Power Export T2" + == "Product Name (aabbccddeeff) Total power export T2" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR @@ -370,7 +370,7 @@ async def test_sensor_entity_active_power( assert state.state == "123.123" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Active Power" + == "Product Name (aabbccddeeff) Active power" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT @@ -410,7 +410,7 @@ async def test_sensor_entity_active_power_l1( assert state.state == "123.123" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Active Power L1" + == "Product Name (aabbccddeeff) Active power L1" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT @@ -450,7 +450,7 @@ async def test_sensor_entity_active_power_l2( assert state.state == "456.456" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Active Power L2" + == "Product Name (aabbccddeeff) Active power L2" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT @@ -490,7 +490,7 @@ async def test_sensor_entity_active_power_l3( assert state.state == "789.789" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Active Power L3" + == "Product Name (aabbccddeeff) Active power L3" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT @@ -526,7 +526,7 @@ async def test_sensor_entity_total_gas(hass, mock_config_entry_data, mock_config assert state.state == "50" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Total Gas" + == "Product Name (aabbccddeeff) Total gas" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS @@ -566,7 +566,7 @@ async def test_sensor_entity_active_liters( assert state.state == "12.345" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Active Water Usage" + == "Product Name (aabbccddeeff) Active water usage" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT @@ -607,7 +607,7 @@ async def test_sensor_entity_total_liters( assert state.state == "1234.567" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Total Water Usage" + == "Product Name (aabbccddeeff) Total water usage" ) assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_TOTAL_INCREASING diff --git a/tests/components/homewizard/test_switch.py b/tests/components/homewizard/test_switch.py index 118f0774a47..224d32e1b5c 100644 --- a/tests/components/homewizard/test_switch.py +++ b/tests/components/homewizard/test_switch.py @@ -39,7 +39,7 @@ async def test_switch_entity_not_loaded_when_not_available( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - state_power_on = hass.states.get("sensor.product_name_aabbccddeeff_switch") + state_power_on = hass.states.get("sensor.product_name_aabbccddeeff") state_switch_lock = hass.states.get("sensor.product_name_aabbccddeeff_switch_lock") assert state_power_on is None @@ -67,10 +67,8 @@ async def test_switch_loads_entities(hass, mock_config_entry_data, mock_config_e entity_registry = er.async_get(hass) - state_power_on = hass.states.get("switch.product_name_aabbccddeeff_switch") - entry_power_on = entity_registry.async_get( - "switch.product_name_aabbccddeeff_switch" - ) + state_power_on = hass.states.get("switch.product_name_aabbccddeeff") + entry_power_on = entity_registry.async_get("switch.product_name_aabbccddeeff") assert state_power_on assert entry_power_on assert entry_power_on.unique_id == "aabbccddeeff_power_on" @@ -78,7 +76,7 @@ async def test_switch_loads_entities(hass, mock_config_entry_data, mock_config_e assert state_power_on.state == STATE_OFF assert ( state_power_on.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Switch" + == "Product Name (aabbccddeeff)" ) assert state_power_on.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_OUTLET assert ATTR_ICON not in state_power_on.attributes @@ -95,7 +93,7 @@ async def test_switch_loads_entities(hass, mock_config_entry_data, mock_config_e assert state_switch_lock.state == STATE_OFF assert ( state_switch_lock.attributes.get(ATTR_FRIENDLY_NAME) - == "Product Name (aabbccddeeff) Switch Lock" + == "Product Name (aabbccddeeff) Switch lock" ) assert state_switch_lock.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SWITCH assert ATTR_ICON not in state_switch_lock.attributes @@ -127,38 +125,30 @@ async def test_switch_power_on_off(hass, mock_config_entry_data, mock_config_ent await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert ( - hass.states.get("switch.product_name_aabbccddeeff_switch").state - == STATE_OFF - ) + assert hass.states.get("switch.product_name_aabbccddeeff").state == STATE_OFF # Turn power_on on await hass.services.async_call( switch.DOMAIN, SERVICE_TURN_ON, - {"entity_id": "switch.product_name_aabbccddeeff_switch"}, + {"entity_id": "switch.product_name_aabbccddeeff"}, blocking=True, ) await hass.async_block_till_done() assert len(api.state_set.mock_calls) == 1 - assert ( - hass.states.get("switch.product_name_aabbccddeeff_switch").state == STATE_ON - ) + assert hass.states.get("switch.product_name_aabbccddeeff").state == STATE_ON # Turn power_on off await hass.services.async_call( switch.DOMAIN, SERVICE_TURN_OFF, - {"entity_id": "switch.product_name_aabbccddeeff_switch"}, + {"entity_id": "switch.product_name_aabbccddeeff"}, blocking=True, ) await hass.async_block_till_done() - assert ( - hass.states.get("switch.product_name_aabbccddeeff_switch").state - == STATE_OFF - ) + assert hass.states.get("switch.product_name_aabbccddeeff").state == STATE_OFF assert len(api.state_set.mock_calls) == 2 @@ -254,9 +244,7 @@ async def test_switch_lock_sets_power_on_unavailable( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert ( - hass.states.get("switch.product_name_aabbccddeeff_switch").state == STATE_ON - ) + assert hass.states.get("switch.product_name_aabbccddeeff").state == STATE_ON assert ( hass.states.get("switch.product_name_aabbccddeeff_switch_lock").state == STATE_OFF @@ -273,7 +261,7 @@ async def test_switch_lock_sets_power_on_unavailable( await hass.async_block_till_done() assert len(api.state_set.mock_calls) == 1 assert ( - hass.states.get("switch.product_name_aabbccddeeff_switch").state + hass.states.get("switch.product_name_aabbccddeeff").state == STATE_UNAVAILABLE ) assert ( @@ -290,9 +278,7 @@ async def test_switch_lock_sets_power_on_unavailable( ) await hass.async_block_till_done() - assert ( - hass.states.get("switch.product_name_aabbccddeeff_switch").state == STATE_ON - ) + assert hass.states.get("switch.product_name_aabbccddeeff").state == STATE_ON assert ( hass.states.get("switch.product_name_aabbccddeeff_switch_lock").state == STATE_OFF From 5489b2111a21738155edc42168029584c8b28f6a Mon Sep 17 00:00:00 2001 From: Gabe Cook Date: Tue, 12 Jul 2022 09:06:38 -0500 Subject: [PATCH 2495/3516] Fix Ruckus Unleashed SSH connection failures (#75032) --- .../components/ruckus_unleashed/__init__.py | 7 +++---- .../components/ruckus_unleashed/config_flow.py | 14 +++++++------- .../components/ruckus_unleashed/coordinator.py | 4 +--- .../components/ruckus_unleashed/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ruckus_unleashed/test_init.py | 4 ++-- 7 files changed, 16 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/ruckus_unleashed/__init__.py b/homeassistant/components/ruckus_unleashed/__init__.py index 258a5968ab1..5861486457f 100644 --- a/homeassistant/components/ruckus_unleashed/__init__.py +++ b/homeassistant/components/ruckus_unleashed/__init__.py @@ -29,8 +29,7 @@ from .coordinator import RuckusUnleashedDataUpdateCoordinator async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ruckus Unleashed from a config entry.""" try: - ruckus = await hass.async_add_executor_job( - Ruckus, + ruckus = await Ruckus.create( entry.data[CONF_HOST], entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], @@ -42,10 +41,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - system_info = await hass.async_add_executor_job(ruckus.system_info) + system_info = await ruckus.system_info() registry = device_registry.async_get(hass) - ap_info = await hass.async_add_executor_job(ruckus.ap_info) + ap_info = await ruckus.ap_info() for device in ap_info[API_AP][API_ID].values(): registry.async_get_or_create( config_entry_id=entry.entry_id, diff --git a/homeassistant/components/ruckus_unleashed/config_flow.py b/homeassistant/components/ruckus_unleashed/config_flow.py index 7d34e620a13..4adf245c3de 100644 --- a/homeassistant/components/ruckus_unleashed/config_flow.py +++ b/homeassistant/components/ruckus_unleashed/config_flow.py @@ -21,22 +21,24 @@ DATA_SCHEMA = vol.Schema( ) -def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. """ try: - ruckus = Ruckus(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD]) + ruckus = await Ruckus.create( + data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD] + ) except AuthenticationError as error: raise InvalidAuth from error except ConnectionError as error: raise CannotConnect from error - mesh_name = ruckus.mesh_name() + mesh_name = await ruckus.mesh_name() - system_info = ruckus.system_info() + system_info = await ruckus.system_info() try: host_serial = system_info[API_SYSTEM_OVERVIEW][API_SERIAL] except KeyError as error: @@ -58,9 +60,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: try: - info = await self.hass.async_add_executor_job( - validate_input, self.hass, user_input - ) + info = await validate_input(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: diff --git a/homeassistant/components/ruckus_unleashed/coordinator.py b/homeassistant/components/ruckus_unleashed/coordinator.py index 8b80eaae0da..e84b79ef843 100644 --- a/homeassistant/components/ruckus_unleashed/coordinator.py +++ b/homeassistant/components/ruckus_unleashed/coordinator.py @@ -37,9 +37,7 @@ class RuckusUnleashedDataUpdateCoordinator(DataUpdateCoordinator): async def _fetch_clients(self) -> dict: """Fetch clients from the API and format them.""" - clients = await self.hass.async_add_executor_job( - self.ruckus.current_active_clients - ) + clients = await self.ruckus.current_active_clients() return {e[API_MAC]: e for e in clients[API_CURRENT_ACTIVE_CLIENTS][API_CLIENTS]} async def _async_update_data(self) -> dict: diff --git a/homeassistant/components/ruckus_unleashed/manifest.json b/homeassistant/components/ruckus_unleashed/manifest.json index f010d340147..a6b2ad0271c 100644 --- a/homeassistant/components/ruckus_unleashed/manifest.json +++ b/homeassistant/components/ruckus_unleashed/manifest.json @@ -3,7 +3,7 @@ "name": "Ruckus Unleashed", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ruckus_unleashed", - "requirements": ["pyruckus==0.12"], + "requirements": ["pyruckus==0.16"], "codeowners": ["@gabe565"], "iot_class": "local_polling", "loggers": ["pexpect", "pyruckus"] diff --git a/requirements_all.txt b/requirements_all.txt index 53823fbc756..36b3ba5cb62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1783,7 +1783,7 @@ pyrisco==0.3.1 pyrituals==0.0.6 # homeassistant.components.ruckus_unleashed -pyruckus==0.12 +pyruckus==0.16 # homeassistant.components.sabnzbd pysabnzbd==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 193daabeabc..e4e8b4262f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1217,7 +1217,7 @@ pyrisco==0.3.1 pyrituals==0.0.6 # homeassistant.components.ruckus_unleashed -pyruckus==0.12 +pyruckus==0.16 # homeassistant.components.sabnzbd pysabnzbd==1.1.1 diff --git a/tests/components/ruckus_unleashed/test_init.py b/tests/components/ruckus_unleashed/test_init.py index e9ac9ec7cd8..d72856aa542 100644 --- a/tests/components/ruckus_unleashed/test_init.py +++ b/tests/components/ruckus_unleashed/test_init.py @@ -31,7 +31,7 @@ async def test_setup_entry_login_error(hass): """Test entry setup failed due to login error.""" entry = mock_config_entry() with patch( - "homeassistant.components.ruckus_unleashed.Ruckus", + "homeassistant.components.ruckus_unleashed.Ruckus.connect", side_effect=AuthenticationError, ): entry.add_to_hass(hass) @@ -45,7 +45,7 @@ async def test_setup_entry_connection_error(hass): """Test entry setup failed due to connection error.""" entry = mock_config_entry() with patch( - "homeassistant.components.ruckus_unleashed.Ruckus", + "homeassistant.components.ruckus_unleashed.Ruckus.connect", side_effect=ConnectionError, ): entry.add_to_hass(hass) From cf612c4bec584f6627eba17e551879e0df67457f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20L=C3=B6vdahl?= Date: Tue, 12 Jul 2022 17:32:37 +0300 Subject: [PATCH 2496/3516] Migrate Vallox to new entity naming style (#75025) --- .../components/vallox/binary_sensor.py | 4 ++-- homeassistant/components/vallox/fan.py | 2 +- homeassistant/components/vallox/sensor.py | 18 +++++++++--------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/vallox/binary_sensor.py b/homeassistant/components/vallox/binary_sensor.py index 762b63c0c1d..9f1b3018186 100644 --- a/homeassistant/components/vallox/binary_sensor.py +++ b/homeassistant/components/vallox/binary_sensor.py @@ -21,6 +21,7 @@ class ValloxBinarySensor(ValloxEntity, BinarySensorEntity): entity_description: ValloxBinarySensorEntityDescription _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_has_entity_name = True def __init__( self, @@ -33,7 +34,6 @@ class ValloxBinarySensor(ValloxEntity, BinarySensorEntity): self.entity_description = description - self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{self._device_uuid}-{description.key}" @property @@ -59,7 +59,7 @@ class ValloxBinarySensorEntityDescription( SENSORS: tuple[ValloxBinarySensorEntityDescription, ...] = ( ValloxBinarySensorEntityDescription( key="post_heater", - name="Post Heater", + name="Post heater", icon="mdi:radiator", metric_key="A_CYC_IO_HEATER", ), diff --git a/homeassistant/components/vallox/fan.py b/homeassistant/components/vallox/fan.py index 4ba7d2d88fd..be496bbf899 100644 --- a/homeassistant/components/vallox/fan.py +++ b/homeassistant/components/vallox/fan.py @@ -83,6 +83,7 @@ class ValloxFan(ValloxEntity, FanEntity): """Representation of the fan.""" _attr_supported_features = FanEntityFeature.PRESET_MODE + _attr_has_entity_name = True def __init__( self, @@ -95,7 +96,6 @@ class ValloxFan(ValloxEntity, FanEntity): self._client = client - self._attr_name = name self._attr_unique_id = str(self._device_uuid) @property diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 54a010c3e3d..48b44dd97fe 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -37,6 +37,7 @@ class ValloxSensor(ValloxEntity, SensorEntity): entity_description: ValloxSensorEntityDescription _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_has_entity_name = True def __init__( self, @@ -49,7 +50,6 @@ class ValloxSensor(ValloxEntity, SensorEntity): self.entity_description = description - self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{self._device_uuid}-{description.key}" @property @@ -129,13 +129,13 @@ class ValloxSensorEntityDescription(SensorEntityDescription): SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( ValloxSensorEntityDescription( key="current_profile", - name="Current Profile", + name="Current profile", icon="mdi:gauge", sensor_type=ValloxProfileSensor, ), ValloxSensorEntityDescription( key="fan_speed", - name="Fan Speed", + name="Fan speed", metric_key="A_CYC_FAN_SPEED", icon="mdi:fan", state_class=SensorStateClass.MEASUREMENT, @@ -144,20 +144,20 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( ), ValloxSensorEntityDescription( key="remaining_time_for_filter", - name="Remaining Time For Filter", + name="Remaining time for filter", device_class=SensorDeviceClass.TIMESTAMP, sensor_type=ValloxFilterRemainingSensor, ), ValloxSensorEntityDescription( key="cell_state", - name="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", + name="Extract air", metric_key="A_CYC_TEMP_EXTRACT_AIR", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -165,7 +165,7 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( ), ValloxSensorEntityDescription( key="exhaust_air", - name="Exhaust Air", + name="Exhaust air", metric_key="A_CYC_TEMP_EXHAUST_AIR", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -173,7 +173,7 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( ), ValloxSensorEntityDescription( key="outdoor_air", - name="Outdoor Air", + name="Outdoor air", metric_key="A_CYC_TEMP_OUTDOOR_AIR", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, @@ -181,7 +181,7 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( ), ValloxSensorEntityDescription( key="supply_air", - name="Supply Air", + name="Supply air", metric_key="A_CYC_TEMP_SUPPLY_AIR", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, From b0fde206b8b25f9fb7e7e24d07849c2b6b76ee83 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 12 Jul 2022 19:26:06 +0200 Subject: [PATCH 2497/3516] Teach resolution center about fixing issues (#74694) --- .../components/resolution_center/__init__.py | 9 +- .../resolution_center/issue_handler.py | 77 ++++- .../resolution_center/issue_registry.py | 5 + .../resolution_center/manifest.json | 3 +- .../components/resolution_center/models.py | 17 ++ .../resolution_center/websocket_api.py | 78 +++++ .../components/resolution_center/test_init.py | 42 ++- .../resolution_center/test_issue_registry.py | 3 + .../resolution_center/test_websocket_api.py | 268 +++++++++++++++++- 9 files changed, 485 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/resolution_center/__init__.py b/homeassistant/components/resolution_center/__init__.py index 1446aa68bba..7d0cd8416c3 100644 --- a/homeassistant/components/resolution_center/__init__.py +++ b/homeassistant/components/resolution_center/__init__.py @@ -4,16 +4,19 @@ from __future__ import annotations from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType -from . import websocket_api +from . import issue_handler, websocket_api from .const import DOMAIN -from .issue_handler import async_create_issue, async_delete_issue +from .issue_handler import ResolutionCenterFlow, async_create_issue, async_delete_issue from .issue_registry import async_load as async_load_issue_registry -__all__ = ["DOMAIN", "async_create_issue", "async_delete_issue"] +__all__ = ["DOMAIN", "ResolutionCenterFlow", "async_create_issue", "async_delete_issue"] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Resolution Center.""" + hass.data[DOMAIN] = {} + + issue_handler.async_setup(hass) websocket_api.async_setup(hass) await async_load_issue_registry(hass) diff --git a/homeassistant/components/resolution_center/issue_handler.py b/homeassistant/components/resolution_center/issue_handler.py index 245895fa2db..210a998ce14 100644 --- a/homeassistant/components/resolution_center/issue_handler.py +++ b/homeassistant/components/resolution_center/issue_handler.py @@ -1,12 +1,85 @@ """The resolution center integration.""" from __future__ import annotations +from typing import Any + from awesomeversion import AwesomeVersion, AwesomeVersionStrategy +from homeassistant import data_entry_flow from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.integration_platform import ( + async_process_integration_platforms, +) +from .const import DOMAIN from .issue_registry import async_get as async_get_issue_registry -from .models import IssueSeverity +from .models import IssueSeverity, ResolutionCenterFlow, ResolutionCenterProtocol + + +class ResolutionCenterFlowManager(data_entry_flow.FlowManager): + """Manage resolution center flows.""" + + async def async_create_flow( + self, + handler_key: Any, + *, + context: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, + ) -> ResolutionCenterFlow: + """Create a flow. platform is a resolution center module.""" + if "platforms" not in self.hass.data[DOMAIN]: + await async_process_resolution_center_platforms(self.hass) + + platforms: dict[str, ResolutionCenterProtocol] = self.hass.data[DOMAIN][ + "platforms" + ] + if handler_key not in platforms: + raise data_entry_flow.UnknownHandler + platform = platforms[handler_key] + + assert data and "issue_id" in data + issue_id = data["issue_id"] + + issue_registry = async_get_issue_registry(self.hass) + issue = issue_registry.async_get_issue(handler_key, issue_id) + if issue is None or not issue.is_fixable: + raise data_entry_flow.UnknownStep + + return await platform.async_create_fix_flow(self.hass, issue_id) + + async def async_finish_flow( + self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult + ) -> data_entry_flow.FlowResult: + """Complete a fix flow.""" + async_delete_issue(self.hass, flow.handler, flow.init_data["issue_id"]) + if "result" not in result: + result["result"] = None + return result + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Initialize resolution center.""" + hass.data[DOMAIN]["flow_manager"] = ResolutionCenterFlowManager(hass) + + +async def async_process_resolution_center_platforms(hass: HomeAssistant) -> None: + """Start processing resolution center platforms.""" + hass.data[DOMAIN]["platforms"] = {} + + await async_process_integration_platforms( + hass, DOMAIN, _register_resolution_center_platform + ) + + +async def _register_resolution_center_platform( + hass: HomeAssistant, integration_domain: str, platform: ResolutionCenterProtocol +) -> None: + """Register a resolution center platform.""" + if not hasattr(platform, "async_create_fix_flow"): + raise HomeAssistantError(f"Invalid resolution center platform {platform}") + hass.data[DOMAIN]["platforms"][integration_domain] = platform @callback @@ -16,6 +89,7 @@ def async_create_issue( issue_id: str, *, breaks_in_ha_version: str | None = None, + is_fixable: bool, learn_more_url: str | None = None, severity: IssueSeverity, translation_key: str, @@ -35,6 +109,7 @@ def async_create_issue( domain, issue_id, breaks_in_ha_version=breaks_in_ha_version, + is_fixable=is_fixable, learn_more_url=learn_more_url, severity=severity, translation_key=translation_key, diff --git a/homeassistant/components/resolution_center/issue_registry.py b/homeassistant/components/resolution_center/issue_registry.py index 7d5bbb482ba..d9042d891dd 100644 --- a/homeassistant/components/resolution_center/issue_registry.py +++ b/homeassistant/components/resolution_center/issue_registry.py @@ -25,6 +25,7 @@ class IssueEntry: breaks_in_ha_version: str | None dismissed_version: str | None domain: str + is_fixable: bool | None issue_id: str learn_more_url: str | None severity: IssueSeverity | None @@ -55,6 +56,7 @@ class IssueRegistry: issue_id: str, *, breaks_in_ha_version: str | None = None, + is_fixable: bool, learn_more_url: str | None = None, severity: IssueSeverity, translation_key: str, @@ -68,6 +70,7 @@ class IssueRegistry: breaks_in_ha_version=breaks_in_ha_version, dismissed_version=None, domain=domain, + is_fixable=is_fixable, issue_id=issue_id, learn_more_url=learn_more_url, severity=severity, @@ -81,6 +84,7 @@ class IssueRegistry: issue, active=True, breaks_in_ha_version=breaks_in_ha_version, + is_fixable=is_fixable, learn_more_url=learn_more_url, severity=severity, translation_key=translation_key, @@ -127,6 +131,7 @@ class IssueRegistry: breaks_in_ha_version=None, dismissed_version=issue["dismissed_version"], domain=issue["domain"], + is_fixable=None, issue_id=issue["issue_id"], learn_more_url=None, severity=None, diff --git a/homeassistant/components/resolution_center/manifest.json b/homeassistant/components/resolution_center/manifest.json index 87cd309ad3d..4b8c1bb2506 100644 --- a/homeassistant/components/resolution_center/manifest.json +++ b/homeassistant/components/resolution_center/manifest.json @@ -3,5 +3,6 @@ "name": "Resolution Center", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/resolution_center", - "codeowners": ["@home-assistant/core"] + "codeowners": ["@home-assistant/core"], + "dependencies": ["http"] } diff --git a/homeassistant/components/resolution_center/models.py b/homeassistant/components/resolution_center/models.py index eabfd98cef3..12e2f4e73f3 100644 --- a/homeassistant/components/resolution_center/models.py +++ b/homeassistant/components/resolution_center/models.py @@ -1,7 +1,11 @@ """Models for Resolution Center.""" from __future__ import annotations +from typing import Protocol + +from homeassistant import data_entry_flow from homeassistant.backports.enum import StrEnum +from homeassistant.core import HomeAssistant class IssueSeverity(StrEnum): @@ -10,3 +14,16 @@ class IssueSeverity(StrEnum): CRITICAL = "critical" ERROR = "error" WARNING = "warning" + + +class ResolutionCenterFlow(data_entry_flow.FlowHandler): + """Handle a flow for fixing an issue.""" + + +class ResolutionCenterProtocol(Protocol): + """Define the format of resolution center platforms.""" + + async def async_create_fix_flow( + self, hass: HomeAssistant, issue_id: str + ) -> ResolutionCenterFlow: + """Create a flow to fix a fixable issue.""" diff --git a/homeassistant/components/resolution_center/websocket_api.py b/homeassistant/components/resolution_center/websocket_api.py index 14793f0bd2d..dfa4f1903d9 100644 --- a/homeassistant/components/resolution_center/websocket_api.py +++ b/homeassistant/components/resolution_center/websocket_api.py @@ -2,13 +2,24 @@ from __future__ import annotations import dataclasses +from http import HTTPStatus from typing import Any +from aiohttp import web import voluptuous as vol +from homeassistant import data_entry_flow +from homeassistant.auth.permissions.const import POLICY_EDIT from homeassistant.components import websocket_api +from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import Unauthorized +from homeassistant.helpers.data_entry_flow import ( + FlowManagerIndexView, + FlowManagerResourceView, +) +from .const import DOMAIN from .issue_handler import async_dismiss_issue from .issue_registry import async_get as async_get_issue_registry @@ -19,6 +30,13 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_dismiss_issue) websocket_api.async_register_command(hass, ws_list_issues) + hass.http.register_view( + ResolutionCenterFlowIndexView(hass.data[DOMAIN]["flow_manager"]) + ) + hass.http.register_view( + ResolutionCenterFlowResourceView(hass.data[DOMAIN]["flow_manager"]) + ) + @callback @websocket_api.websocket_command( @@ -60,3 +78,63 @@ def ws_list_issues( ] connection.send_result(msg["id"], {"issues": issues}) + + +class ResolutionCenterFlowIndexView(FlowManagerIndexView): + """View to create issue fix flows.""" + + url = "/api/resolution_center/issues/fix" + name = "api:resolution_center:issues:fix" + + @RequestDataValidator( + vol.Schema( + { + vol.Required("handler"): str, + vol.Required("issue_id"): str, + }, + extra=vol.ALLOW_EXTRA, + ) + ) + async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: + """Handle a POST request.""" + if not request["hass_user"].is_admin: + raise Unauthorized(permission=POLICY_EDIT) + + try: + result = await self._flow_mgr.async_init( + data["handler"], + data={"issue_id": data["issue_id"]}, + ) + except data_entry_flow.UnknownHandler: + return self.json_message("Invalid handler specified", HTTPStatus.NOT_FOUND) + except data_entry_flow.UnknownStep: + return self.json_message( + "Handler does not support user", HTTPStatus.BAD_REQUEST + ) + + result = self._prepare_result_json(result) + + return self.json(result) # pylint: disable=arguments-differ + + +class ResolutionCenterFlowResourceView(FlowManagerResourceView): + """View to interact with the option flow manager.""" + + url = "/api/resolution_center/issues/fix/{flow_id}" + name = "api:resolution_center:issues:fix:resource" + + async def get(self, request: web.Request, flow_id: str) -> web.Response: + """Get the current state of a data_entry_flow.""" + if not request["hass_user"].is_admin: + raise Unauthorized(permission=POLICY_EDIT) + + return await super().get(request, flow_id) + + # pylint: disable=arguments-differ + async def post(self, request: web.Request, flow_id: str) -> web.Response: + """Handle a POST request.""" + if not request["hass_user"].is_admin: + raise Unauthorized(permission=POLICY_EDIT) + + # pylint: disable=no-value-for-parameter + return await super().post(request, flow_id) # type: ignore[no-any-return] diff --git a/tests/components/resolution_center/test_init.py b/tests/components/resolution_center/test_init.py index 869c5d6c485..66f9bc42935 100644 --- a/tests/components/resolution_center/test_init.py +++ b/tests/components/resolution_center/test_init.py @@ -1,4 +1,6 @@ """Test the resolution center websocket API.""" +from unittest.mock import AsyncMock, Mock + import pytest from homeassistant.components.resolution_center import ( @@ -6,11 +8,16 @@ from homeassistant.components.resolution_center import ( async_delete_issue, ) from homeassistant.components.resolution_center.const import DOMAIN -from homeassistant.components.resolution_center.issue_handler import async_dismiss_issue +from homeassistant.components.resolution_center.issue_handler import ( + async_dismiss_issue, + async_process_resolution_center_platforms, +) from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import mock_platform + async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: """Test creating and updating issues.""" @@ -29,6 +36,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: "breaks_in_ha_version": "2022.9.0dev0", "domain": "test", "issue_id": "issue_1", + "is_fixable": True, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -38,6 +46,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: "breaks_in_ha_version": "2022.8", "domain": "test", "issue_id": "issue_2", + "is_fixable": False, "learn_more_url": "https://theuselessweb.com/abc", "severity": "other", "translation_key": "even_worse", @@ -51,6 +60,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -78,6 +88,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: issues[0]["domain"], issues[0]["issue_id"], breaks_in_ha_version=issues[0]["breaks_in_ha_version"], + is_fixable=issues[0]["is_fixable"], learn_more_url="blablabla", severity=issues[0]["severity"], translation_key=issues[0]["translation_key"], @@ -109,6 +120,7 @@ async def test_create_issue_invalid_version( "breaks_in_ha_version": ha_version, "domain": "test", "issue_id": "issue_1", + "is_fixable": True, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -121,6 +133,7 @@ async def test_create_issue_invalid_version( issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -150,6 +163,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: { "breaks_in_ha_version": "2022.9", "domain": "test", + "is_fixable": True, "issue_id": "issue_1", "learn_more_url": "https://theuselessweb.com", "severity": "error", @@ -164,6 +178,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -246,6 +261,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: issues[0]["domain"], issues[0]["issue_id"], breaks_in_ha_version=issues[0]["breaks_in_ha_version"], + is_fixable=issues[0]["is_fixable"], learn_more_url="blablabla", severity=issues[0]["severity"], translation_key=issues[0]["translation_key"], @@ -275,6 +291,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None: "breaks_in_ha_version": "2022.9", "domain": "fake_integration", "issue_id": "issue_1", + "is_fixable": True, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -288,6 +305,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None: issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -344,3 +362,25 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None: assert msg["success"] assert msg["result"] == {"issues": []} + + +async def test_non_compliant_platform(hass: HomeAssistant, hass_ws_client) -> None: + """Test non-compliant platforms are not registered.""" + + hass.config.components.add("fake_integration") + hass.config.components.add("integration_without_diagnostics") + mock_platform( + hass, + "fake_integration.resolution_center", + Mock(async_create_fix_flow=AsyncMock(return_value=True)), + ) + mock_platform( + hass, + "integration_without_diagnostics.resolution_center", + Mock(spec=[]), + ) + assert await async_setup_component(hass, DOMAIN, {}) + + await async_process_resolution_center_platforms(hass) + + assert list(hass.data[DOMAIN]["platforms"].keys()) == ["fake_integration"] diff --git a/tests/components/resolution_center/test_issue_registry.py b/tests/components/resolution_center/test_issue_registry.py index c236e96adb2..a0dffaacc8f 100644 --- a/tests/components/resolution_center/test_issue_registry.py +++ b/tests/components/resolution_center/test_issue_registry.py @@ -20,6 +20,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: "breaks_in_ha_version": "2022.9", "domain": "test", "issue_id": "issue_1", + "is_fixable": True, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -29,6 +30,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: "breaks_in_ha_version": "2022.8", "domain": "test", "issue_id": "issue_2", + "is_fixable": True, "learn_more_url": "https://theuselessweb.com/abc", "severity": "other", "translation_key": "even_worse", @@ -42,6 +44,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], diff --git a/tests/components/resolution_center/test_websocket_api.py b/tests/components/resolution_center/test_websocket_api.py index 9258a06f904..42899065121 100644 --- a/tests/components/resolution_center/test_websocket_api.py +++ b/tests/components/resolution_center/test_websocket_api.py @@ -1,22 +1,33 @@ """Test the resolution center websocket API.""" -from homeassistant.components.resolution_center import async_create_issue +from __future__ import annotations + +from http import HTTPStatus +from unittest.mock import ANY, AsyncMock, Mock + +import pytest +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components.resolution_center import ( + ResolutionCenterFlow, + async_create_issue, +) from homeassistant.components.resolution_center.const import DOMAIN from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import mock_platform -async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: - """Test we can dismiss an issue.""" - assert await async_setup_component(hass, DOMAIN, {}) - - client = await hass_ws_client(hass) +async def create_issues(hass, ws_client): + """Create issues.""" issues = [ { "breaks_in_ha_version": "2022.9", - "domain": "test", + "domain": "fake_integration", "issue_id": "issue_1", + "is_fixable": True, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -30,14 +41,15 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], translation_placeholders=issue["translation_placeholders"], ) - await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) - msg = await client.receive_json() + await ws_client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + msg = await ws_client.receive_json() assert msg["success"] assert msg["result"] == { @@ -51,11 +63,63 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: ] } + return issues + + +class MockFixFlow(ResolutionCenterFlow): + """Handler for an issue fixing flow.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + + return await (self.async_step_confirm()) + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + return self.async_create_entry(title=None, data=None) + + return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) + + +@pytest.fixture(autouse=True) +async def mock_resolution_center_integration(hass): + """Mock a resolution_center integration.""" + hass.config.components.add("fake_integration") + hass.config.components.add("integration_without_diagnostics") + + def async_create_fix_flow(hass, issue_id): + return MockFixFlow() + + mock_platform( + hass, + "fake_integration.resolution_center", + Mock(async_create_fix_flow=AsyncMock(wraps=async_create_fix_flow)), + ) + mock_platform( + hass, + "integration_without_diagnostics.resolution_center", + Mock(spec=[]), + ) + + +async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can dismiss an issue.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + issues = await create_issues(hass, client) + await client.send_json( { "id": 2, "type": "resolution_center/dismiss_issue", - "domain": "test", + "domain": "fake_integration", "issue_id": "no_such_issue", } ) @@ -66,7 +130,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: { "id": 3, "type": "resolution_center/dismiss_issue", - "domain": "test", + "domain": "fake_integration", "issue_id": "issue_1", } ) @@ -90,6 +154,185 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: } +async def test_fix_non_existing_issue( + hass: HomeAssistant, hass_client, hass_ws_client +) -> None: + """Test trying to fix an issue that doesn't exist.""" + assert await async_setup_component(hass, "http", {}) + assert await async_setup_component(hass, DOMAIN, {}) + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + issues = await create_issues(hass, ws_client) + + url = "/api/resolution_center/issues/fix" + resp = await client.post( + url, json={"handler": "no_such_integration", "issue_id": "no_such_issue"} + ) + + assert resp.status != HTTPStatus.OK + + url = "/api/resolution_center/issues/fix" + resp = await client.post( + url, json={"handler": "fake_integration", "issue_id": "no_such_issue"} + ) + + assert resp.status != HTTPStatus.OK + + await ws_client.send_json({"id": 3, "type": "resolution_center/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + + +async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> None: + """Test we can fix an issue.""" + assert await async_setup_component(hass, "http", {}) + assert await async_setup_component(hass, DOMAIN, {}) + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await create_issues(hass, ws_client) + + url = "/api/resolution_center/issues/fix" + resp = await client.post( + url, json={"handler": "fake_integration", "issue_id": "issue_1"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "data_schema": [], + "description_placeholders": None, + "errors": None, + "flow_id": ANY, + "handler": "fake_integration", + "last_step": None, + "step_id": "confirm", + "type": "form", + } + + url = f"/api/resolution_center/issues/fix/{flow_id}" + # Test we can get the status of the flow + resp2 = await client.get(url) + + assert resp2.status == HTTPStatus.OK + data2 = await resp2.json() + + assert data == data2 + + resp = await client.post(url) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "description": None, + "description_placeholders": None, + "flow_id": flow_id, + "handler": "fake_integration", + "title": None, + "type": "create_entry", + "version": 1, + } + + await ws_client.send_json({"id": 4, "type": "resolution_center/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + +async def test_fix_issue_unauth( + hass: HomeAssistant, hass_client, hass_admin_user +) -> None: + """Test we can't query the result if not authorized.""" + assert await async_setup_component(hass, "http", {}) + assert await async_setup_component(hass, DOMAIN, {}) + + hass_admin_user.groups = [] + + client = await hass_client() + + url = "/api/resolution_center/issues/fix" + resp = await client.post( + url, json={"handler": "fake_integration", "issue_id": "issue_1"} + ) + + assert resp.status == HTTPStatus.UNAUTHORIZED + + +async def test_get_progress_unauth( + hass: HomeAssistant, hass_client, hass_ws_client, hass_admin_user +) -> None: + """Test we can't fix an issue if not authorized.""" + assert await async_setup_component(hass, "http", {}) + assert await async_setup_component(hass, DOMAIN, {}) + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await create_issues(hass, ws_client) + + url = "/api/resolution_center/issues/fix" + resp = await client.post( + url, json={"handler": "fake_integration", "issue_id": "issue_1"} + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + flow_id = data["flow_id"] + + hass_admin_user.groups = [] + + url = f"/api/resolution_center/issues/fix/{flow_id}" + # Test we can't get the status of the flow + resp = await client.get(url) + assert resp.status == HTTPStatus.UNAUTHORIZED + + +async def test_step_unauth( + hass: HomeAssistant, hass_client, hass_ws_client, hass_admin_user +) -> None: + """Test we can't fix an issue if not authorized.""" + assert await async_setup_component(hass, "http", {}) + assert await async_setup_component(hass, DOMAIN, {}) + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await create_issues(hass, ws_client) + + url = "/api/resolution_center/issues/fix" + resp = await client.post( + url, json={"handler": "fake_integration", "issue_id": "issue_1"} + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + flow_id = data["flow_id"] + + hass_admin_user.groups = [] + + url = f"/api/resolution_center/issues/fix/{flow_id}" + # Test we can't get the status of the flow + resp = await client.post(url) + assert resp.status == HTTPStatus.UNAUTHORIZED + + async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: """Test we can list issues.""" assert await async_setup_component(hass, DOMAIN, {}) @@ -106,6 +349,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: { "breaks_in_ha_version": "2022.9", "domain": "test", + "is_fixable": True, "issue_id": "issue_1", "learn_more_url": "https://theuselessweb.com", "severity": "error", @@ -115,6 +359,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: { "breaks_in_ha_version": "2022.8", "domain": "test", + "is_fixable": False, "issue_id": "issue_2", "learn_more_url": "https://theuselessweb.com/abc", "severity": "other", @@ -129,6 +374,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], From 397f94ee5023184f977a86bc8537bbe126b963b2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Jul 2022 20:06:13 +0200 Subject: [PATCH 2498/3516] Migrate DSMR to use keys for entity unique ID (#74895) --- homeassistant/components/dsmr/__init__.py | 91 ++++++++++++++- homeassistant/components/dsmr/sensor.py | 128 ++++++++++++++------- tests/components/dsmr/test_init.py | 133 ++++++++++++++++++++++ tests/components/dsmr/test_sensor.py | 6 +- 4 files changed, 311 insertions(+), 47 deletions(-) create mode 100644 tests/components/dsmr/test_init.py diff --git a/homeassistant/components/dsmr/__init__.py b/homeassistant/components/dsmr/__init__.py index 044cfd14e64..f3546fc0a00 100644 --- a/homeassistant/components/dsmr/__init__.py +++ b/homeassistant/components/dsmr/__init__.py @@ -1,15 +1,29 @@ """The dsmr component.""" +from __future__ import annotations + from asyncio import CancelledError from contextlib import suppress +from typing import Any from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er -from .const import DATA_TASK, DOMAIN, PLATFORMS +from .const import CONF_DSMR_VERSION, DATA_TASK, DOMAIN, PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up DSMR from a config entry.""" + + @callback + def _async_migrate_entity_entry( + entity_entry: er.RegistryEntry, + ) -> dict[str, Any] | None: + """Migrate DSMR entity entry.""" + return async_migrate_entity_entry(entry, entity_entry) + + await er.async_migrate_entries(hass, entry.entry_id, _async_migrate_entity_entry) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {} @@ -38,3 +52,76 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update options.""" await hass.config_entries.async_reload(entry.entry_id) + + +@callback +def async_migrate_entity_entry( + config_entry: ConfigEntry, entity_entry: er.RegistryEntry +) -> dict[str, Any] | None: + """Migrate DSMR entity entries. + + - Migrates unique ID for sensors based on entity description name to key. + """ + + # Replace names with keys in unique ID + for old, new in ( + ("Power_Consumption", "current_electricity_usage"), + ("Power_Production", "current_electricity_delivery"), + ("Power_Tariff", "electricity_active_tariff"), + ("Energy_Consumption_(tarif_1)", "electricity_used_tariff_1"), + ("Energy_Consumption_(tarif_2)", "electricity_used_tariff_2"), + ("Energy_Production_(tarif_1)", "electricity_delivered_tariff_1"), + ("Energy_Production_(tarif_2)", "electricity_delivered_tariff_2"), + ("Power_Consumption_Phase_L1", "instantaneous_active_power_l1_positive"), + ("Power_Consumption_Phase_L3", "instantaneous_active_power_l3_positive"), + ("Power_Consumption_Phase_L2", "instantaneous_active_power_l2_positive"), + ("Power_Production_Phase_L1", "instantaneous_active_power_l1_negative"), + ("Power_Production_Phase_L2", "instantaneous_active_power_l2_negative"), + ("Power_Production_Phase_L3", "instantaneous_active_power_l3_negative"), + ("Short_Power_Failure_Count", "short_power_failure_count"), + ("Long_Power_Failure_Count", "long_power_failure_count"), + ("Voltage_Sags_Phase_L1", "voltage_sag_l1_count"), + ("Voltage_Sags_Phase_L2", "voltage_sag_l2_count"), + ("Voltage_Sags_Phase_L3", "voltage_sag_l3_count"), + ("Voltage_Swells_Phase_L1", "voltage_swell_l1_count"), + ("Voltage_Swells_Phase_L2", "voltage_swell_l2_count"), + ("Voltage_Swells_Phase_L3", "voltage_swell_l3_count"), + ("Voltage_Phase_L1", "instantaneous_voltage_l1"), + ("Voltage_Phase_L2", "instantaneous_voltage_l2"), + ("Voltage_Phase_L3", "instantaneous_voltage_l3"), + ("Current_Phase_L1", "instantaneous_current_l1"), + ("Current_Phase_L2", "instantaneous_current_l2"), + ("Current_Phase_L3", "instantaneous_current_l3"), + ("Max_power_per_phase", "belgium_max_power_per_phase"), + ("Max_current_per_phase", "belgium_max_current_per_phase"), + ("Energy_Consumption_(total)", "electricity_imported_total"), + ("Energy_Production_(total)", "electricity_exported_total"), + ): + if entity_entry.unique_id.endswith(old): + return {"new_unique_id": entity_entry.unique_id.replace(old, new)} + + # Replace unique ID for gas sensors, based on DSMR version + old = "Gas_Consumption" + if entity_entry.unique_id.endswith(old): + dsmr_version = config_entry.data[CONF_DSMR_VERSION] + if dsmr_version in {"4", "5", "5L"}: + return { + "new_unique_id": entity_entry.unique_id.replace( + old, "hourly_gas_meter_reading" + ) + } + if dsmr_version == "5B": + return { + "new_unique_id": entity_entry.unique_id.replace( + old, "belgium_5min_gas_meter_reading" + ) + } + if dsmr_version == "2.2": + return { + "new_unique_id": entity_entry.unique_id.replace( + old, "gas_meter_reading" + ) + } + + # No migration needed + return None diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 1337f209d5d..6acb652c6e5 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -59,7 +59,16 @@ UNIT_CONVERSION = {"m3": VOLUME_CUBIC_METERS} @dataclass -class DSMRSensorEntityDescription(SensorEntityDescription): +class DSMRSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + obis_reference: str + + +@dataclass +class DSMRSensorEntityDescription( + SensorEntityDescription, DSMRSensorEntityDescriptionMixin +): """Represents an DSMR Sensor.""" dsmr_versions: set[str] | None = None @@ -68,211 +77,239 @@ class DSMRSensorEntityDescription(SensorEntityDescription): SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( - key=obis_references.CURRENT_ELECTRICITY_USAGE, + key="current_electricity_usage", name="Power Consumption", + obis_reference=obis_references.CURRENT_ELECTRICITY_USAGE, device_class=SensorDeviceClass.POWER, force_update=True, state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key=obis_references.CURRENT_ELECTRICITY_DELIVERY, + key="electricity_delivery", name="Power Production", + obis_reference=obis_references.CURRENT_ELECTRICITY_DELIVERY, device_class=SensorDeviceClass.POWER, force_update=True, state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_ACTIVE_TARIFF, + key="electricity_active_tariff", name="Power Tariff", + obis_reference=obis_references.ELECTRICITY_ACTIVE_TARIFF, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, icon="mdi:flash", ), DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_USED_TARIFF_1, + key="electricity_used_tariff_1", name="Energy Consumption (tarif 1)", + obis_reference=obis_references.ELECTRICITY_USED_TARIFF_1, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, device_class=SensorDeviceClass.ENERGY, force_update=True, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_USED_TARIFF_2, + key="electricity_used_tariff_2", name="Energy Consumption (tarif 2)", + obis_reference=obis_references.ELECTRICITY_USED_TARIFF_2, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, force_update=True, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, + key="electricity_delivered_tariff_1", name="Energy Production (tarif 1)", + obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, force_update=True, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, + key="electricity_delivered_tariff_2", name="Energy Production (tarif 2)", + obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, force_update=True, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, + key="instantaneous_active_power_l1_positive", name="Power Consumption Phase L1", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, + key="instantaneous_active_power_l2_positive", name="Power Consumption Phase L2", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, + key="instantaneous_active_power_l3_positive", name="Power Consumption Phase L3", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, + key="instantaneous_active_power_l1_negative", name="Power Production Phase L1", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, + key="instantaneous_active_power_l2_negative", name="Power Production Phase L2", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, + key="instantaneous_active_power_l3_negative", name="Power Production Phase L3", + obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key=obis_references.SHORT_POWER_FAILURE_COUNT, + key="short_power_failure_count", name="Short Power Failure Count", + obis_reference=obis_references.SHORT_POWER_FAILURE_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:flash-off", entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.LONG_POWER_FAILURE_COUNT, + key="long_power_failure_count", name="Long Power Failure Count", + obis_reference=obis_references.LONG_POWER_FAILURE_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:flash-off", entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SAG_L1_COUNT, + key="voltage_sag_l1_count", name="Voltage Sags Phase L1", + obis_reference=obis_references.VOLTAGE_SAG_L1_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SAG_L2_COUNT, + key="voltage_sag_l2_count", name="Voltage Sags Phase L2", + obis_reference=obis_references.VOLTAGE_SAG_L2_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SAG_L3_COUNT, + key="voltage_sag_l3_count", name="Voltage Sags Phase L3", + obis_reference=obis_references.VOLTAGE_SAG_L3_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SWELL_L1_COUNT, + key="voltage_swell_l1_count", name="Voltage Swells Phase L1", + obis_reference=obis_references.VOLTAGE_SWELL_L1_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:pulse", entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SWELL_L2_COUNT, + key="voltage_swell_l2_count", name="Voltage Swells Phase L2", + obis_reference=obis_references.VOLTAGE_SWELL_L2_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:pulse", entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.VOLTAGE_SWELL_L3_COUNT, + key="voltage_swell_l3_count", name="Voltage Swells Phase L3", + obis_reference=obis_references.VOLTAGE_SWELL_L3_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, icon="mdi:pulse", entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_VOLTAGE_L1, + key="instantaneous_voltage_l1", name="Voltage Phase L1", + obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L1, device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_VOLTAGE_L2, + key="instantaneous_voltage_l2", name="Voltage Phase L2", + obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L2, device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_VOLTAGE_L3, + key="instantaneous_voltage_l3", name="Voltage Phase L3", + obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L3, device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_CURRENT_L1, + key="instantaneous_current_l1", name="Current Phase L1", + obis_reference=obis_references.INSTANTANEOUS_CURRENT_L1, device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_CURRENT_L2, + key="instantaneous_current_l2", name="Current Phase L2", + obis_reference=obis_references.INSTANTANEOUS_CURRENT_L2, device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.INSTANTANEOUS_CURRENT_L3, + key="instantaneous_current_l3", name="Current Phase L3", + obis_reference=obis_references.INSTANTANEOUS_CURRENT_L3, device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.BELGIUM_MAX_POWER_PER_PHASE, + key="belgium_max_power_per_phase", name="Max power per phase", + obis_reference=obis_references.BELGIUM_MAX_POWER_PER_PHASE, dsmr_versions={"5B"}, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, @@ -280,8 +317,9 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.BELGIUM_MAX_CURRENT_PER_PHASE, + key="belgium_max_current_per_phase", name="Max current per phase", + obis_reference=obis_references.BELGIUM_MAX_CURRENT_PER_PHASE, dsmr_versions={"5B"}, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, @@ -289,24 +327,27 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, ), DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_IMPORTED_TOTAL, + key="electricity_imported_total", name="Energy Consumption (total)", + obis_reference=obis_references.ELECTRICITY_IMPORTED_TOTAL, dsmr_versions={"5L", "5S", "Q3D"}, force_update=True, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( - key=obis_references.ELECTRICITY_EXPORTED_TOTAL, + key="electricity_exported_total", name="Energy Production (total)", + obis_reference=obis_references.ELECTRICITY_EXPORTED_TOTAL, dsmr_versions={"5L", "5S", "Q3D"}, force_update=True, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( - key=obis_references.HOURLY_GAS_METER_READING, + key="hourly_gas_meter_reading", name="Gas Consumption", + obis_reference=obis_references.HOURLY_GAS_METER_READING, dsmr_versions={"4", "5", "5L"}, is_gas=True, force_update=True, @@ -314,8 +355,9 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( - key=obis_references.BELGIUM_5MIN_GAS_METER_READING, + key="belgium_5min_gas_meter_reading", name="Gas Consumption", + obis_reference=obis_references.BELGIUM_5MIN_GAS_METER_READING, dsmr_versions={"5B"}, is_gas=True, force_update=True, @@ -323,8 +365,9 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRSensorEntityDescription( - key=obis_references.GAS_METER_READING, + key="gas_meter_reading", name="Gas Consumption", + obis_reference=obis_references.GAS_METER_READING, dsmr_versions={"2.2"}, is_gas=True, force_update=True, @@ -492,25 +535,23 @@ class DSMREntity(SensorEntity): identifiers={(DOMAIN, device_serial)}, name=device_name, ) - self._attr_unique_id = f"{device_serial}_{entity_description.name}".replace( - " ", "_" - ) + self._attr_unique_id = f"{device_serial}_{entity_description.key}" @callback def update_data(self, telegram: dict[str, DSMRObject]) -> None: """Update data.""" self.telegram = telegram - if self.hass and self.entity_description.key in self.telegram: + if self.hass and self.entity_description.obis_reference in self.telegram: self.async_write_ha_state() def get_dsmr_object_attr(self, attribute: str) -> str | None: """Read attribute from last received telegram for this DSMR object.""" # Make sure telegram contains an object for this entities obis - if self.entity_description.key not in self.telegram: + if self.entity_description.obis_reference not in self.telegram: return None # Get the attribute value if the object has it - dsmr_object = self.telegram[self.entity_description.key] + dsmr_object = self.telegram[self.entity_description.obis_reference] attr: str | None = getattr(dsmr_object, attribute) return attr @@ -520,7 +561,10 @@ class DSMREntity(SensorEntity): if (value := self.get_dsmr_object_attr("value")) is None: return None - if self.entity_description.key == obis_references.ELECTRICITY_ACTIVE_TARIFF: + if ( + self.entity_description.obis_reference + == obis_references.ELECTRICITY_ACTIVE_TARIFF + ): return self.translate_tariff(value, self._entry.data[CONF_DSMR_VERSION]) with suppress(TypeError): diff --git a/tests/components/dsmr/test_init.py b/tests/components/dsmr/test_init.py new file mode 100644 index 00000000000..914a9f6bdaf --- /dev/null +++ b/tests/components/dsmr/test_init.py @@ -0,0 +1,133 @@ +"""Tests for the DSMR integration.""" +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.dsmr.const import DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "dsmr_version,old_unique_id,new_unique_id", + [ + ("5", "1234_Power_Consumption", "1234_current_electricity_usage"), + ("5", "1234_Power_Production", "1234_current_electricity_delivery"), + ("5", "1234_Power_Tariff", "1234_electricity_active_tariff"), + ("5", "1234_Energy_Consumption_(tarif_1)", "1234_electricity_used_tariff_1"), + ("5", "1234_Energy_Consumption_(tarif_2)", "1234_electricity_used_tariff_2"), + ( + "5", + "1234_Energy_Production_(tarif_1)", + "1234_electricity_delivered_tariff_1", + ), + ( + "5", + "1234_Energy_Production_(tarif_2)", + "1234_electricity_delivered_tariff_2", + ), + ( + "5", + "1234_Power_Consumption_Phase_L1", + "1234_instantaneous_active_power_l1_positive", + ), + ( + "5", + "1234_Power_Consumption_Phase_L2", + "1234_instantaneous_active_power_l2_positive", + ), + ( + "5", + "1234_Power_Consumption_Phase_L3", + "1234_instantaneous_active_power_l3_positive", + ), + ( + "5", + "1234_Power_Production_Phase_L1", + "1234_instantaneous_active_power_l1_negative", + ), + ( + "5", + "1234_Power_Production_Phase_L2", + "1234_instantaneous_active_power_l2_negative", + ), + ( + "5", + "1234_Power_Production_Phase_L3", + "1234_instantaneous_active_power_l3_negative", + ), + ("5", "1234_Short_Power_Failure_Count", "1234_short_power_failure_count"), + ("5", "1234_Long_Power_Failure_Count", "1234_long_power_failure_count"), + ("5", "1234_Voltage_Sags_Phase_L1", "1234_voltage_sag_l1_count"), + ("5", "1234_Voltage_Sags_Phase_L2", "1234_voltage_sag_l2_count"), + ("5", "1234_Voltage_Sags_Phase_L3", "1234_voltage_sag_l3_count"), + ("5", "1234_Voltage_Swells_Phase_L1", "1234_voltage_swell_l1_count"), + ("5", "1234_Voltage_Swells_Phase_L2", "1234_voltage_swell_l2_count"), + ("5", "1234_Voltage_Swells_Phase_L3", "1234_voltage_swell_l3_count"), + ("5", "1234_Voltage_Phase_L1", "1234_instantaneous_voltage_l1"), + ("5", "1234_Voltage_Phase_L2", "1234_instantaneous_voltage_l2"), + ("5", "1234_Voltage_Phase_L3", "1234_instantaneous_voltage_l3"), + ("5", "1234_Current_Phase_L1", "1234_instantaneous_current_l1"), + ("5", "1234_Current_Phase_L2", "1234_instantaneous_current_l2"), + ("5", "1234_Current_Phase_L3", "1234_instantaneous_current_l3"), + ("5B", "1234_Max_power_per_phase", "1234_belgium_max_power_per_phase"), + ("5B", "1234_Max_current_per_phase", "1234_belgium_max_current_per_phase"), + ("5L", "1234_Energy_Consumption_(total)", "1234_electricity_imported_total"), + ("5L", "1234_Energy_Production_(total)", "1234_electricity_exported_total"), + ("5L", "1234_Energy_Production_(total)", "1234_electricity_exported_total"), + ("5", "1234_Gas_Consumption", "1234_hourly_gas_meter_reading"), + ("5B", "1234_Gas_Consumption", "1234_belgium_5min_gas_meter_reading"), + ("2.2", "1234_Gas_Consumption", "1234_gas_meter_reading"), + ], +) +async def test_migrate_unique_id( + hass: HomeAssistant, + dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock], + dsmr_version: str, + old_unique_id: str, + new_unique_id: str, +) -> None: + """Test migration of unique_id.""" + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="/dev/ttyUSB0", + data={ + "port": "/dev/ttyUSB0", + "dsmr_version": dsmr_version, + "precision": 4, + "reconnect_interval": 30, + "serial_id": "1234", + "serial_id_gas": "5678", + }, + options={ + "time_between_update": 0, + }, + ) + + mock_entry.add_to_hass(hass) + + entity_registry = er.async_get(hass) + entity: er.RegistryEntry = entity_registry.async_get_or_create( + suggested_object_id="my_sensor", + disabled_by=None, + domain=SENSOR_DOMAIN, + platform=DOMAIN, + unique_id=old_unique_id, + config_entry=mock_entry, + ) + assert entity.unique_id == old_unique_id + + assert await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + assert ( + entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, old_unique_id) + is None + ) + assert ( + entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, new_unique_id) + == "sensor.my_sensor" + ) diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 4502f61586a..b006765bde7 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -79,11 +79,11 @@ async def test_default_setup(hass, dsmr_connection_fixture): entry = registry.async_get("sensor.power_consumption") assert entry - assert entry.unique_id == "1234_Power_Consumption" + assert entry.unique_id == "1234_current_electricity_usage" entry = registry.async_get("sensor.gas_consumption") assert entry - assert entry.unique_id == "5678_Gas_Consumption" + assert entry.unique_id == "5678_gas_meter_reading" telegram_callback = connection_factory.call_args_list[0][0][2] @@ -157,7 +157,7 @@ async def test_setup_only_energy(hass, dsmr_connection_fixture): entry = registry.async_get("sensor.power_consumption") assert entry - assert entry.unique_id == "1234_Power_Consumption" + assert entry.unique_id == "1234_current_electricity_usage" entry = registry.async_get("sensor.gas_consumption") assert not entry From 96ecbe4388e5baac497b5cf7f4378b6360c0a1c6 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Tue, 12 Jul 2022 14:45:38 -0400 Subject: [PATCH 2499/3516] Migrate Environment Canada to new entity naming style (#75024) Co-authored-by: Franck Nijhof --- .../components/environment_canada/__init__.py | 13 +++++++ .../components/environment_canada/camera.py | 6 ++- .../components/environment_canada/sensor.py | 38 ++++++++++--------- .../components/environment_canada/weather.py | 7 ++-- 4 files changed, 42 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/environment_canada/__init__.py b/homeassistant/components/environment_canada/__init__.py index e12d12b87d0..a8548429d50 100644 --- a/homeassistant/components/environment_canada/__init__.py +++ b/homeassistant/components/environment_canada/__init__.py @@ -9,6 +9,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_LANGUAGE, CONF_STATION, DOMAIN @@ -87,6 +89,17 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok +def device_info(config_entry: ConfigEntry) -> DeviceInfo: + """Build and return the device info for EC.""" + return DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, config_entry.entry_id)}, + manufacturer="Environment Canada", + name=config_entry.title, + configuration_url="https://weather.gc.ca/", + ) + + class ECDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching EC data.""" diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index e415bab977b..7b93f0b28f4 100644 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -12,6 +12,7 @@ from homeassistant.helpers.entity_platform import ( ) from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import device_info from .const import ATTR_OBSERVATION_TIME, DOMAIN SERVICE_SET_RADAR_TYPE = "set_radar_type" @@ -40,16 +41,19 @@ async def async_setup_entry( class ECCamera(CoordinatorEntity, Camera): """Implementation of an Environment Canada radar camera.""" + _attr_has_entity_name = True + _attr_name = "Radar" + def __init__(self, coordinator): """Initialize the camera.""" super().__init__(coordinator) Camera.__init__(self) self.radar_object = coordinator.ec_data - self._attr_name = f"{coordinator.config_entry.title} Radar" self._attr_unique_id = f"{coordinator.config_entry.unique_id}-radar" self._attr_attribution = self.radar_object.metadata["attribution"] self._attr_entity_registry_enabled_default = False + self._attr_device_info = device_info(coordinator.config_entry) self.content_type = "image/gif" diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 2e124d1ec7c..08da60fe01f 100644 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -27,6 +27,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import device_info from .const import ATTR_STATION, DOMAIN ATTR_TIME = "alert time" @@ -51,12 +52,12 @@ class ECSensorEntityDescription( SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ECSensorEntityDescription( key="condition", - name="Current Condition", + name="Current condition", value_fn=lambda data: data.conditions.get("condition", {}).get("value"), ), ECSensorEntityDescription( key="dewpoint", - name="Dew Point", + name="Dew point", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -64,7 +65,7 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ), ECSensorEntityDescription( key="high_temp", - name="High Temperature", + name="High temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -88,12 +89,12 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ), ECSensorEntityDescription( key="icon_code", - name="Icon Code", + name="Icon code", value_fn=lambda data: data.conditions.get("icon_code", {}).get("value"), ), ECSensorEntityDescription( key="low_temp", - name="Low Temperature", + name="Low temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -101,34 +102,34 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ), ECSensorEntityDescription( key="normal_high", - name="Normal High Temperature", + name="Normal high temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, value_fn=lambda data: data.conditions.get("normal_high", {}).get("value"), ), ECSensorEntityDescription( key="normal_low", - name="Normal Low Temperature", + name="Normal low temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, value_fn=lambda data: data.conditions.get("normal_low", {}).get("value"), ), ECSensorEntityDescription( key="pop", - name="Chance of Precipitation", + name="Chance of precipitation", native_unit_of_measurement=PERCENTAGE, value_fn=lambda data: data.conditions.get("pop", {}).get("value"), ), ECSensorEntityDescription( key="precip_yesterday", - name="Precipitation Yesterday", + name="Precipitation yesterday", native_unit_of_measurement=LENGTH_MILLIMETERS, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("precip_yesterday", {}).get("value"), ), ECSensorEntityDescription( key="pressure", - name="Barometric Pressure", + name="Barometric pressure", device_class=SensorDeviceClass.PRESSURE, native_unit_of_measurement=PRESSURE_KPA, state_class=SensorStateClass.MEASUREMENT, @@ -156,13 +157,13 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ), ECSensorEntityDescription( key="timestamp", - name="Observation Time", + name="Observation time", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: data.metadata.get("timestamp"), ), ECSensorEntityDescription( key="uv_index", - name="UV Index", + name="UV index", native_unit_of_measurement=UV_INDEX, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("uv_index", {}).get("value"), @@ -176,13 +177,13 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ), ECSensorEntityDescription( key="wind_bearing", - name="Wind Bearing", + name="Wind bearing", native_unit_of_measurement=DEGREE, value_fn=lambda data: data.conditions.get("wind_bearing", {}).get("value"), ), ECSensorEntityDescription( key="wind_chill", - name="Wind Chill", + name="Wind chill", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, @@ -190,19 +191,19 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = ( ), ECSensorEntityDescription( key="wind_dir", - name="Wind Direction", + name="Wind direction", value_fn=lambda data: data.conditions.get("wind_dir", {}).get("value"), ), ECSensorEntityDescription( key="wind_gust", - name="Wind Gust", + name="Wind gust", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("wind_gust", {}).get("value"), ), ECSensorEntityDescription( key="wind_speed", - name="Wind Speed", + name="Wind speed", native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, state_class=SensorStateClass.MEASUREMENT, value_fn=lambda data: data.conditions.get("wind_speed", {}).get("value"), @@ -285,6 +286,7 @@ class ECBaseSensor(CoordinatorEntity, SensorEntity): """Environment Canada sensor base.""" entity_description: ECSensorEntityDescription + _attr_has_entity_name = True def __init__(self, coordinator, description): """Initialize the base sensor.""" @@ -292,8 +294,8 @@ class ECBaseSensor(CoordinatorEntity, SensorEntity): self.entity_description = description self._ec_data = coordinator.ec_data self._attr_attribution = self._ec_data.metadata["attribution"] - self._attr_name = f"{coordinator.config_entry.title} {description.name}" self._attr_unique_id = f"{coordinator.config_entry.title}-{description.key}" + self._attr_device_info = device_info(coordinator.config_entry) @property def native_value(self): diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 40706ffb6c1..8dbf8c15731 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -35,6 +35,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt +from . import device_info from .const import DOMAIN # Icon codes from http://dd.weatheroffice.ec.gc.ca/citypage_weather/ @@ -68,6 +69,7 @@ async def async_setup_entry( class ECWeather(CoordinatorEntity, WeatherEntity): """Representation of a weather condition.""" + _attr_has_entity_name = True _attr_native_pressure_unit = PRESSURE_KPA _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_visibility_unit = LENGTH_KILOMETERS @@ -78,14 +80,13 @@ class ECWeather(CoordinatorEntity, WeatherEntity): super().__init__(coordinator) self.ec_data = coordinator.ec_data self._attr_attribution = self.ec_data.metadata["attribution"] - self._attr_name = ( - f"{coordinator.config_entry.title}{' Hourly' if hourly else ''}" - ) + self._attr_name = "Hourly forecast" if hourly else "Forecast" self._attr_unique_id = ( f"{coordinator.config_entry.unique_id}{'-hourly' if hourly else '-daily'}" ) self._attr_entity_registry_enabled_default = not hourly self._hourly = hourly + self._attr_device_info = device_info(coordinator.config_entry) @property def native_temperature(self): From d40978742cf3d095b9fd980abb5dcf39a8194b75 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 12 Jul 2022 20:46:04 +0200 Subject: [PATCH 2500/3516] Update coverage to 6.4.2 (#75072) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 1ca1fc05819..adecd327631 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt codecov==2.1.12 -coverage==6.4.1 +coverage==6.4.2 freezegun==1.2.1 mock-open==1.4.0 mypy==0.961 From b54fe14a10a7a0ee3762c217c32cc6582ace406c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 12 Jul 2022 12:53:21 -0600 Subject: [PATCH 2501/3516] Replace Guardian `reboot` and `reset_valve_diagnostics` services with buttons (#75028) --- .coveragerc | 1 + homeassistant/components/guardian/__init__.py | 96 +++++++++++---- homeassistant/components/guardian/button.py | 113 ++++++++++++++++++ 3 files changed, 185 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/guardian/button.py diff --git a/.coveragerc b/.coveragerc index 4e252fb9c92..94b9eceb11b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -450,6 +450,7 @@ omit = homeassistant/components/gtfs/sensor.py homeassistant/components/guardian/__init__.py homeassistant/components/guardian/binary_sensor.py + homeassistant/components/guardian/button.py homeassistant/components/guardian/sensor.py homeassistant/components/guardian/switch.py homeassistant/components/guardian/util.py diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 3707bf9d2eb..f13ca1a7ff5 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -88,8 +88,7 @@ SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( }, ) - -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] @callback @@ -106,6 +105,25 @@ def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) raise ValueError(f"No client for device ID: {device_id}") +@callback +def async_log_deprecated_service_call( + hass: HomeAssistant, + call: ServiceCall, + alternate_service: str, + alternate_target: str, +) -> None: + """Log a warning about a deprecated service call.""" + LOGGER.warning( + ( + 'The "%s" service is deprecated and will be removed in a future version; ' + 'use the "%s" service and pass it a target entity ID of "%s"' + ), + f"{call.domain}.{call.service}", + alternate_service, + alternate_target, + ) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elexa Guardian from a config entry.""" client = Client(entry.data[CONF_IP_ADDRESS], port=entry.data[CONF_PORT]) @@ -164,17 +182,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @callback - def extract_client(func: Callable) -> Callable: - """Define a decorator to get the correct client for a service call.""" + def hydrate_with_entry_and_client(func: Callable) -> Callable: + """Define a decorator to hydrate a method with args based on service call.""" async def wrapper(call: ServiceCall) -> None: """Wrap the service function.""" entry_id = async_get_entry_id_for_service_call(hass, call) client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + entry = hass.config_entries.async_get_entry(entry_id) + assert entry try: async with client: - await func(call, client) + await func(call, entry, client) except GuardianError as err: raise HomeAssistantError( f"Error while executing {func.__name__}: {err}" @@ -182,48 +202,76 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return wrapper - @extract_client - async def async_disable_ap(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_disable_ap( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Disable the onboard AP.""" await client.wifi.disable_ap() - @extract_client - async def async_enable_ap(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_enable_ap( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Enable the onboard AP.""" await client.wifi.enable_ap() - @extract_client - async def async_pair_sensor(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_pair_sensor( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Add a new paired sensor.""" - entry_id = async_get_entry_id_for_service_call(hass, call) - paired_sensor_manager = hass.data[DOMAIN][entry_id][DATA_PAIRED_SENSOR_MANAGER] + paired_sensor_manager = hass.data[DOMAIN][entry.entry_id][ + DATA_PAIRED_SENSOR_MANAGER + ] uid = call.data[CONF_UID] await client.sensor.pair_sensor(uid) await paired_sensor_manager.async_pair_sensor(uid) - @extract_client - async def async_reboot(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_reboot( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Reboot the valve controller.""" + async_log_deprecated_service_call( + hass, + call, + "button.press", + f"button.guardian_valve_controller_{entry.data[CONF_UID]}_reboot", + ) await client.system.reboot() - @extract_client - async def async_reset_valve_diagnostics(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_reset_valve_diagnostics( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Fully reset system motor diagnostics.""" + async_log_deprecated_service_call( + hass, + call, + "button.press", + f"button.guardian_valve_controller_{entry.data[CONF_UID]}_reset_valve_diagnostics", + ) await client.valve.reset() - @extract_client - async def async_unpair_sensor(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_unpair_sensor( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Remove a paired sensor.""" - entry_id = async_get_entry_id_for_service_call(hass, call) - paired_sensor_manager = hass.data[DOMAIN][entry_id][DATA_PAIRED_SENSOR_MANAGER] + paired_sensor_manager = hass.data[DOMAIN][entry.entry_id][ + DATA_PAIRED_SENSOR_MANAGER + ] uid = call.data[CONF_UID] await client.sensor.unpair_sensor(uid) await paired_sensor_manager.async_unpair_sensor(uid) - @extract_client - async def async_upgrade_firmware(call: ServiceCall, client: Client) -> None: + @hydrate_with_entry_and_client + async def async_upgrade_firmware( + call: ServiceCall, entry: ConfigEntry, client: Client + ) -> None: """Upgrade the device firmware.""" await client.system.upgrade_firmware( url=call.data[CONF_URL], @@ -389,7 +437,6 @@ class GuardianEntity(CoordinatorEntity): This should be extended by Guardian platforms. """ - raise NotImplementedError class PairedSensorEntity(GuardianEntity): @@ -454,7 +501,6 @@ class ValveControllerEntity(GuardianEntity): This should be extended by Guardian platforms. """ - raise NotImplementedError @callback def async_add_coordinator_update_listener(self, api: str) -> None: diff --git a/homeassistant/components/guardian/button.py b/homeassistant/components/guardian/button.py new file mode 100644 index 00000000000..e7cc757d367 --- /dev/null +++ b/homeassistant/components/guardian/button.py @@ -0,0 +1,113 @@ +"""Buttons for the Elexa Guardian integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from aioguardian import Client +from aioguardian.errors import GuardianError + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import ValveControllerEntity +from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN + + +@dataclass +class GuardianButtonDescriptionMixin: + """Define an entity description mixin for Guardian buttons.""" + + push_action: Callable[[Client], Awaitable] + + +@dataclass +class GuardianButtonDescription( + ButtonEntityDescription, GuardianButtonDescriptionMixin +): + """Describe a Guardian button description.""" + + +BUTTON_KIND_REBOOT = "reboot" +BUTTON_KIND_RESET_VALVE_DIAGNOSTICS = "reset_valve_diagnostics" + + +async def _async_reboot(client: Client) -> None: + """Reboot the Guardian.""" + await client.system.reboot() + + +async def _async_valve_reset(client: Client) -> None: + """Reset the valve diagnostics on the Guardian.""" + await client.valve.reset() + + +BUTTON_DESCRIPTIONS = ( + GuardianButtonDescription( + key=BUTTON_KIND_REBOOT, + name="Reboot", + push_action=_async_reboot, + ), + GuardianButtonDescription( + key=BUTTON_KIND_RESET_VALVE_DIAGNOSTICS, + name="Reset valve diagnostics", + push_action=_async_valve_reset, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Guardian buttons based on a config entry.""" + async_add_entities( + [ + GuardianButton( + entry, + hass.data[DOMAIN][entry.entry_id][DATA_CLIENT], + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], + description, + ) + for description in BUTTON_DESCRIPTIONS + ] + ) + + +class GuardianButton(ValveControllerEntity, ButtonEntity): + """Define a Guardian button.""" + + _attr_device_class = ButtonDeviceClass.RESTART + _attr_entity_category = EntityCategory.CONFIG + + entity_description: GuardianButtonDescription + + def __init__( + self, + entry: ConfigEntry, + client: Client, + coordinators: dict[str, DataUpdateCoordinator], + description: GuardianButtonDescription, + ) -> None: + """Initialize.""" + super().__init__(entry, coordinators, description) + + self._client = client + + async def async_press(self) -> None: + """Send out a restart command.""" + try: + async with self._client: + await self.entity_description.push_action(self._client) + except GuardianError as err: + raise HomeAssistantError( + f'Error while pressing button "{self.entity_id}": {err}' + ) from err From 41ec8cd3549cd649184409c0d4bf6c4eb149411b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 12 Jul 2022 13:49:54 -0700 Subject: [PATCH 2502/3516] Expose supported brands via API (#75074) --- .pre-commit-config.yaml | 2 +- .../components/websocket_api/commands.py | 24 ++++++++ homeassistant/generated/supported_brands.py | 15 +++++ script/hassfest/__main__.py | 2 + script/hassfest/model.py | 5 ++ script/hassfest/supported_brands.py | 55 +++++++++++++++++++ .../components/websocket_api/test_commands.py | 41 +++++++++++++- 7 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 homeassistant/generated/supported_brands.py create mode 100644 script/hassfest/supported_brands.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bbf7295be99..51429bdf94b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] - exclude: ^tests/fixtures/ + exclude: ^tests/fixtures/|homeassistant/generated/ - repo: https://github.com/PyCQA/flake8 rev: 4.0.1 hooks: diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index bea08722eb0..b7e7a353633 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -22,6 +22,7 @@ from homeassistant.exceptions import ( TemplateError, Unauthorized, ) +from homeassistant.generated import supported_brands from homeassistant.helpers import config_validation as cv, entity, template from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import ( @@ -68,6 +69,7 @@ def async_register_commands( async_reg(hass, handle_unsubscribe_events) async_reg(hass, handle_validate_config) async_reg(hass, handle_subscribe_entities) + async_reg(hass, handle_supported_brands) def pong_message(iden: int) -> dict[str, Any]: @@ -691,3 +693,25 @@ async def handle_validate_config( result[key] = {"valid": True, "error": None} connection.send_result(msg["id"], result) + + +@decorators.websocket_command( + { + vol.Required("type"): "supported_brands", + } +) +@decorators.async_response +async def handle_supported_brands( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle supported brands command.""" + data = {} + for integration in await asyncio.gather( + *[ + async_get_integration(hass, integration) + for integration in supported_brands.HAS_SUPPORTED_BRANDS + ] + ): + data[integration.domain] = integration.manifest["supported_brands"] + + connection.send_result(msg["id"], data) diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py new file mode 100644 index 00000000000..589e0462cf7 --- /dev/null +++ b/homeassistant/generated/supported_brands.py @@ -0,0 +1,15 @@ +"""Automatically generated by hassfest. + +To update, run python3 -m script.hassfest +""" + +# fmt: off + +HAS_SUPPORTED_BRANDS = ( + "denonavr", + "hunterdouglas_powerview", + "motion_blinds", + "overkiz", + "renault", + "wemo" +) diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 4bc30583d47..233abda4ed8 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -20,6 +20,7 @@ from . import ( requirements, services, ssdp, + supported_brands, translations, usb, zeroconf, @@ -39,6 +40,7 @@ INTEGRATION_PLUGINS = [ requirements, services, ssdp, + supported_brands, translations, usb, zeroconf, diff --git a/script/hassfest/model.py b/script/hassfest/model.py index fc38e1db592..d4e1fbf806a 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -112,6 +112,11 @@ class Integration: """List of dependencies.""" return self.manifest.get("dependencies", []) + @property + def supported_brands(self) -> dict[str]: + """Return dict of supported brands.""" + return self.manifest.get("supported_brands", {}) + @property def integration_type(self) -> str: """Get integration_type.""" diff --git a/script/hassfest/supported_brands.py b/script/hassfest/supported_brands.py new file mode 100644 index 00000000000..6740260a04c --- /dev/null +++ b/script/hassfest/supported_brands.py @@ -0,0 +1,55 @@ +"""Generate supported_brands data.""" +from __future__ import annotations + +import json + +from .model import Config, Integration + +BASE = """ +\"\"\"Automatically generated by hassfest. + +To update, run python3 -m script.hassfest +\"\"\" + +# fmt: off + +HAS_SUPPORTED_BRANDS = ({}) +""".strip() + + +def generate_and_validate(integrations: dict[str, Integration], config: Config) -> str: + """Validate and generate supported_brands data.""" + + brands = [ + domain + for domain, integration in sorted(integrations.items()) + if integration.supported_brands + ] + + return BASE.format(json.dumps(brands, indent=4)[1:-1]) + + +def validate(integrations: dict[str, Integration], config: Config) -> None: + """Validate supported_brands data.""" + supported_brands_path = config.root / "homeassistant/generated/supported_brands.py" + config.cache["supported_brands"] = content = generate_and_validate( + integrations, config + ) + + if config.specific_integrations: + return + + if supported_brands_path.read_text(encoding="utf-8").strip() != content: + config.add_error( + "supported_brands", + "File supported_brands.py is not up to date. Run python3 -m script.hassfest", + fixable=True, + ) + + +def generate(integrations: dict[str, Integration], config: Config): + """Generate supported_brands data.""" + supported_brands_path = config.root / "homeassistant/generated/supported_brands.py" + supported_brands_path.write_text( + f"{config.cache['supported_brands']}\n", encoding="utf-8" + ) diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 0f4695596fc..f1065061c73 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -22,7 +22,13 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.loader import async_get_integration from homeassistant.setup import DATA_SETUP_TIME, async_setup_component -from tests.common import MockEntity, MockEntityPlatform, async_mock_service +from tests.common import ( + MockEntity, + MockEntityPlatform, + MockModule, + async_mock_service, + mock_integration, +) STATE_KEY_SHORT_NAMES = { "entity_id": "e", @@ -1749,3 +1755,36 @@ async def test_validate_config_invalid(websocket_client, key, config, error): assert msg["type"] == const.TYPE_RESULT assert msg["success"] assert msg["result"] == {key: {"valid": False, "error": error}} + + +async def test_supported_brands(hass, websocket_client): + """Test supported brands.""" + mock_integration( + hass, + MockModule("test", partial_manifest={"supported_brands": {"hello": "World"}}), + ) + mock_integration( + hass, + MockModule( + "abcd", partial_manifest={"supported_brands": {"something": "Something"}} + ), + ) + + with patch( + "homeassistant.generated.supported_brands.HAS_SUPPORTED_BRANDS", + ("abcd", "test"), + ): + await websocket_client.send_json({"id": 7, "type": "supported_brands"}) + msg = await websocket_client.receive_json() + + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert msg["result"] == { + "abcd": { + "something": "Something", + }, + "test": { + "hello": "World", + }, + } From a3fd5acf3f189126ebe7eb8ad7241679184157fe Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 13 Jul 2022 00:27:34 +0000 Subject: [PATCH 2503/3516] [ci skip] Translation update --- .../components/abode/translations/ja.json | 2 +- .../components/abode/translations/pt.json | 2 +- .../accuweather/translations/ja.json | 2 +- .../components/adax/translations/pt.json | 17 +++++++++++++ .../components/adguard/translations/pt.json | 5 +++- .../components/aemet/translations/pt.json | 7 ++++++ .../components/airthings/translations/pt.json | 7 ++++++ .../components/airvisual/translations/pt.json | 7 +++++- .../components/airzone/translations/pt.json | 12 +++++++++ .../aladdin_connect/translations/pt.json | 17 +++++++++++++ .../components/almond/translations/ja.json | 2 +- .../components/ambee/translations/pt.json | 6 +++++ .../ambient_station/translations/pt.json | 2 +- .../components/androidtv/translations/pt.json | 8 ++++++ .../components/anthemav/translations/pt.json | 18 +++++++++++++ .../aseko_pool_live/translations/pt.json | 12 +++++++++ .../components/asuswrt/translations/pt.json | 12 +++++++++ .../aurora_abb_powerone/translations/pt.json | 7 ++++++ .../aussie_broadband/translations/pt.json | 20 +++++++++++++++ .../components/awair/translations/pt.json | 6 +++++ .../azure_event_hub/translations/ja.json | 2 +- .../azure_event_hub/translations/pt.json | 7 ++++++ .../components/baf/translations/pt.json | 7 ++++++ .../components/balboa/translations/pt.json | 14 +++++++++++ .../binary_sensor/translations/pt.json | 5 +++- .../components/blink/translations/pt.json | 3 ++- .../components/bosch_shc/translations/pt.json | 17 +++++++++++++ .../components/braviatv/translations/pt.json | 2 +- .../components/brunt/translations/pt.json | 21 ++++++++++++++++ .../components/bsblan/translations/pt.json | 3 ++- .../buienradar/translations/pt.json | 11 ++++++++ .../components/canary/translations/ja.json | 2 +- .../components/cast/translations/ja.json | 2 +- .../components/cast/translations/pt.json | 2 +- .../cloudflare/translations/ja.json | 2 +- .../cloudflare/translations/pt.json | 1 + .../components/co2signal/translations/pt.json | 14 +++++++++++ .../components/coinbase/translations/pt.json | 8 ++++++ .../components/cpuspeed/translations/ja.json | 2 +- .../components/cpuspeed/translations/pt.json | 7 ++++++ .../crownstone/translations/pt.json | 16 ++++++++++++ .../components/deluge/translations/pt.json | 18 +++++++++++++ .../derivative/translations/pt.json | 9 +++++++ .../devolo_home_control/translations/pt.json | 5 ++++ .../dialogflow/translations/ja.json | 2 +- .../components/discord/translations/pt.json | 23 +++++++++++++++++ .../components/dlna_dmr/translations/pt.json | 14 +++++++++++ .../components/dlna_dms/translations/pt.json | 15 +++++++++++ .../components/dsmr/translations/pt.json | 16 ++++++++++++ .../components/ecobee/translations/ja.json | 2 +- .../components/econet/translations/pt.json | 10 +++++++- .../components/efergy/translations/pt.json | 10 ++++++++ .../eight_sleep/translations/pt.json | 15 +++++++++++ .../components/elkm1/translations/pt.json | 13 ++++++++++ .../components/elmax/translations/pt.json | 15 +++++++++++ .../components/emonitor/translations/pt.json | 14 +++++++++++ .../components/enocean/translations/ja.json | 2 +- .../enphase_envoy/translations/pt.json | 17 +++++++++++++ .../environment_canada/translations/pt.json | 12 +++++++++ .../components/esphome/translations/pt.json | 3 ++- .../evil_genius_labs/translations/pt.json | 5 +++- .../components/ezviz/translations/pt.json | 5 ++++ .../components/fibaro/translations/pt.json | 7 ++++++ .../components/firmata/translations/pt.json | 2 +- .../components/fivem/translations/pt.json | 15 +++++++++++ .../fjaraskupan/translations/ja.json | 2 +- .../components/flipr/translations/pt.json | 8 ++++++ .../components/flux_led/translations/pt.json | 15 +++++++++++ .../forecast_solar/translations/pt.json | 11 ++++++++ .../components/foscam/translations/pt.json | 5 ++++ .../components/fritz/translations/pt.json | 9 ++++++- .../fritzbox_callmonitor/translations/pt.json | 11 ++++++++ .../components/fronius/translations/pt.json | 14 +++++++++++ .../components/generic/translations/ja.json | 2 +- .../components/generic/translations/pt.json | 22 ++++++++++++++++ .../geocaching/translations/pt.json | 20 +++++++++++++++ .../components/geofency/translations/ja.json | 2 +- .../components/github/translations/pt.json | 7 ++++++ .../components/goodwe/translations/pt.json | 7 ++++++ .../components/google/translations/pt.json | 19 ++++++++++++++ .../google_travel_time/translations/pt.json | 11 ++++++++ .../components/gpslogger/translations/ja.json | 2 +- .../components/gree/translations/ja.json | 2 +- .../components/group/translations/pt.json | 3 +-- .../growatt_server/translations/pt.json | 12 +++++++++ .../components/hangouts/translations/pt.json | 2 +- .../components/heos/translations/ja.json | 2 +- .../hisense_aehw4a1/translations/ja.json | 2 +- .../home_plus_control/translations/ja.json | 2 +- .../components/homekit/translations/pt.json | 1 + .../homematicip_cloud/translations/pt.json | 2 +- .../components/honeywell/translations/pt.json | 11 ++++++++ .../huawei_lte/translations/pt.json | 2 +- .../huisbaasje/translations/pt.json | 11 +++++++- .../components/ialarm/translations/pt.json | 7 ++++++ .../components/iaqualink/translations/ja.json | 2 +- .../components/iaqualink/translations/pt.json | 2 +- .../components/ifttt/translations/ja.json | 2 +- .../components/insteon/translations/ja.json | 2 +- .../components/insteon/translations/pt.json | 2 +- .../intellifire/translations/pt.json | 16 ++++++++++++ .../components/ios/translations/ja.json | 2 +- .../components/ios/translations/pt.json | 2 +- .../components/iotawatt/translations/pt.json | 10 +++++++- .../islamic_prayer_times/translations/ja.json | 2 +- .../components/iss/translations/ja.json | 2 +- .../components/izone/translations/ja.json | 2 +- .../components/jellyfin/translations/ja.json | 2 +- .../components/jellyfin/translations/pt.json | 2 +- .../kaleidescape/translations/pt.json | 11 ++++++++ .../keenetic_ndms2/translations/pt.json | 15 +++++++++++ .../components/knx/translations/ja.json | 2 +- .../components/knx/translations/pt.json | 3 ++- .../components/kodi/translations/pt.json | 2 +- .../components/konnected/translations/pt.json | 4 +-- .../kostal_plenticore/translations/pt.json | 15 +++++++++++ .../components/kraken/translations/ja.json | 2 +- .../components/kraken/translations/pt.json | 12 +++++++++ .../components/kulersky/translations/ja.json | 2 +- .../launch_library/translations/ja.json | 2 +- .../launch_library/translations/pt.json | 7 ++++++ .../components/laundrify/translations/ja.json | 2 +- .../lg_soundbar/translations/pt.json | 17 +++++++++++++ .../components/life360/translations/pt.json | 9 +++++++ .../components/lifx/translations/ja.json | 2 +- .../components/lifx/translations/pt.json | 2 +- .../components/litejet/translations/ja.json | 2 +- .../litterrobot/translations/pt.json | 2 +- .../components/local_ip/translations/ja.json | 2 +- .../components/locative/translations/ja.json | 2 +- .../components/lookin/translations/pt.json | 11 ++++++++ .../components/luftdaten/translations/pt.json | 2 +- .../lutron_caseta/translations/pt.json | 7 ++++++ .../components/lyric/translations/pt.json | 13 ++++++++++ .../components/mailgun/translations/ja.json | 2 +- .../components/mazda/translations/pt.json | 10 ++++++++ .../components/meater/translations/pt.json | 19 ++++++++++++++ .../met_eireann/translations/pt.json | 13 ++++++++++ .../meteoclimatic/translations/pt.json | 3 ++- .../components/mjpeg/translations/pt.json | 23 +++++++++++++++++ .../modem_callerid/translations/pt.json | 7 ++++++ .../modern_forms/translations/pt.json | 11 ++++++++ .../moehlenhoff_alpha2/translations/pt.json | 7 ++++++ .../components/moon/translations/ja.json | 2 +- .../motion_blinds/translations/pt.json | 2 +- .../components/motioneye/translations/pt.json | 14 +++++++++++ .../components/mqtt/translations/ja.json | 2 +- .../components/mqtt/translations/pt.json | 4 +-- .../components/mysensors/translations/pt.json | 8 ++++++ .../components/nam/translations/pt.json | 25 +++++++++++++++++++ .../components/nanoleaf/translations/pt.json | 7 ++++++ .../components/nest/translations/ja.json | 2 +- .../components/nest/translations/pt.json | 12 ++++++++- .../components/netatmo/translations/ja.json | 2 +- .../components/netatmo/translations/pt.json | 4 +++ .../components/netgear/translations/pt.json | 7 ++++++ .../components/nextdns/translations/pt.json | 7 +++++- .../nfandroidtv/translations/pt.json | 15 +++++++++++ .../components/nina/translations/ja.json | 2 +- .../components/nina/translations/pt.json | 13 ++++++++++ .../components/notion/translations/pt.json | 6 +++++ .../components/nuki/translations/pt.json | 11 ++++++++ .../components/nws/translations/pt.json | 2 +- .../components/nzbget/translations/ja.json | 2 +- .../components/nzbget/translations/pt.json | 2 +- .../components/octoprint/translations/pt.json | 10 ++++++++ .../components/omnilogic/translations/ja.json | 2 +- .../components/oncue/translations/pt.json | 7 ++++++ .../ondilo_ico/translations/pt.json | 16 ++++++++++++ .../components/onewire/translations/pt.json | 7 ++++++ .../components/onvif/translations/pt.json | 7 +++++- .../opengarage/translations/pt.json | 15 +++++++++++ .../components/openuv/translations/pt.json | 2 +- .../components/overkiz/translations/pt.json | 14 +++++++++++ .../components/owntracks/translations/ja.json | 2 +- .../p1_monitor/translations/pt.json | 15 +++++++++++ .../philips_js/translations/pt.json | 9 ++++++- .../components/pi_hole/translations/pt.json | 5 ++++ .../components/picnic/translations/pt.json | 18 +++++++++++++ .../components/plaato/translations/ja.json | 2 +- .../components/plaato/translations/pt.json | 2 +- .../components/plex/translations/pt.json | 2 +- .../components/point/translations/ja.json | 2 +- .../components/point/translations/pt.json | 4 +-- .../components/powerwall/translations/pt.json | 8 +++++- .../components/profiler/translations/ja.json | 2 +- .../components/prosegur/translations/pt.json | 5 ++++ .../components/ps4/translations/pt.json | 2 +- .../pure_energie/translations/pt.json | 11 ++++++++ .../components/pvoutput/translations/pt.json | 7 ++++++ .../components/qnap_qsw/translations/pt.json | 21 ++++++++++++++++ .../radio_browser/translations/ja.json | 2 +- .../radio_browser/translations/pt.json | 7 ++++++ .../radiotherm/translations/pt.json | 10 ++++++++ .../rainforest_eagle/translations/pt.json | 8 +++++- .../components/rdw/translations/pt.json | 7 ++++++ .../components/renault/translations/pt.json | 17 +++++++++++++ .../components/rfxtrx/translations/ja.json | 2 +- .../components/rhasspy/translations/ca.json | 7 ++++++ .../components/rhasspy/translations/de.json | 12 +++++++++ .../components/rhasspy/translations/el.json | 12 +++++++++ .../components/rhasspy/translations/ja.json | 12 +++++++++ .../components/rhasspy/translations/pt.json | 7 ++++++ .../rhasspy/translations/zh-Hant.json | 12 +++++++++ .../components/ridwell/translations/pt.json | 23 +++++++++++++++++ .../components/roomba/translations/pt.json | 7 +++++- .../components/roon/translations/pt.json | 7 ++++++ .../components/rpi_power/translations/ja.json | 2 +- .../rtsp_to_webrtc/translations/ja.json | 2 +- .../rtsp_to_webrtc/translations/pt.json | 7 ++++++ .../components/sabnzbd/translations/pt.json | 11 ++++++++ .../components/samsungtv/translations/pt.json | 2 +- .../components/scrape/translations/pt.json | 14 +++++++++++ .../screenlogic/translations/pt.json | 14 +++++++++++ .../components/sense/translations/pt.json | 3 +++ .../components/senseme/translations/pt.json | 14 +++++++++++ .../components/sensibo/translations/pt.json | 19 ++++++++++++++ .../components/sentry/translations/ja.json | 2 +- .../shopping_list/translations/pt.json | 2 +- .../components/sia/translations/pt.json | 11 ++++++++ .../simplepush/translations/pt.json | 17 +++++++++++++ .../components/skybell/translations/pt.json | 15 +++++++++++ .../components/slack/translations/pt.json | 14 +++++++++++ .../components/sleepiq/translations/pt.json | 20 +++++++++++++++ .../components/slimproto/translations/ja.json | 2 +- .../components/slimproto/translations/pt.json | 7 ++++++ .../smartthings/translations/pt.json | 2 +- .../components/smarttub/translations/pt.json | 2 +- .../components/smhi/translations/pt.json | 3 +++ .../components/sms/translations/ja.json | 2 +- .../components/solax/translations/pt.json | 14 +++++++++++ .../components/soma/translations/ja.json | 2 +- .../components/soma/translations/pt.json | 7 ++++++ .../somfy_mylink/translations/pt.json | 17 +++++++++++++ .../components/sonos/translations/ja.json | 2 +- .../components/sonos/translations/pt.json | 2 +- .../soundtouch/translations/pt.json | 17 +++++++++++++ .../speedtestdotnet/translations/ja.json | 2 +- .../components/spider/translations/ja.json | 2 +- .../srp_energy/translations/ja.json | 2 +- .../steam_online/translations/pt.json | 10 ++++++++ .../components/steamist/translations/pt.json | 17 +++++++++++++ .../components/sun/translations/ja.json | 2 +- .../components/sun/translations/pt.json | 7 ++++++ .../surepetcare/translations/pt.json | 16 ++++++++++++ .../components/switchbot/translations/pt.json | 11 ++++++++ .../switcher_kis/translations/ja.json | 2 +- .../components/syncthing/translations/pt.json | 17 +++++++++++++ .../synology_dsm/translations/pt.json | 6 +++++ .../system_bridge/translations/pt.json | 10 ++++++++ .../tankerkoenig/translations/pt.json | 8 ++++++ .../components/tasmota/translations/ja.json | 2 +- .../components/tautulli/translations/ja.json | 2 +- .../components/tautulli/translations/pt.json | 20 +++++++++++++++ .../tellduslive/translations/pt.json | 2 +- .../tesla_wall_connector/translations/pt.json | 14 +++++++++++ .../components/tile/translations/pt.json | 7 +++++- .../tomorrowio/translations/pt.json | 15 +++++++++++ .../components/tplink/translations/pt.json | 3 +++ .../components/traccar/translations/ja.json | 2 +- .../trafikverket_ferry/translations/pt.json | 14 +++++++++++ .../translations/pt.json | 14 +++++++++++ .../transmission/translations/pt.json | 9 ++++++- .../tuya/translations/select.pt.json | 10 +++++++- .../components/twilio/translations/ja.json | 2 +- .../components/unifi/translations/pt.json | 2 +- .../unifiprotect/translations/pt.json | 25 +++++++++++++++++++ .../components/upnp/translations/pt.json | 2 +- .../components/uptime/translations/ja.json | 2 +- .../uptimerobot/translations/pt.json | 15 ++++++++++- .../components/vallox/translations/pt.json | 3 +++ .../components/venstar/translations/pt.json | 18 +++++++++++++ .../components/vesync/translations/ja.json | 2 +- .../components/vicare/translations/ja.json | 2 +- .../components/vicare/translations/pt.json | 18 +++++++++++++ .../vlc_telnet/translations/pt.json | 19 ++++++++++++++ .../components/wallbox/translations/pt.json | 7 ++++++ .../components/watttime/translations/pt.json | 5 +++- .../waze_travel_time/translations/pt.json | 7 ++++++ .../components/weather/translations/pt.json | 4 +-- .../components/webostv/translations/pt.json | 11 ++++++++ .../components/wemo/translations/ja.json | 2 +- .../components/whirlpool/translations/pt.json | 7 ++++++ .../components/whois/translations/pt.json | 7 ++++++ .../components/withings/translations/el.json | 4 +++ .../components/withings/translations/ja.json | 4 +++ .../components/withings/translations/pt.json | 3 +++ .../components/wiz/translations/pt.json | 11 ++++++++ .../wled/translations/select.pt.json | 8 ++++++ .../components/ws66i/translations/pt.json | 7 ++++++ .../components/xbox/translations/ja.json | 2 +- .../xiaomi_miio/translations/pt.json | 8 ++++++ .../yale_smart_alarm/translations/pt.json | 18 +++++++++++++ .../components/yolink/translations/pt.json | 16 ++++++++++++ .../components/youless/translations/pt.json | 7 ++++++ .../components/zerproc/translations/ja.json | 2 +- .../components/zha/translations/ja.json | 2 +- .../components/zha/translations/ru.json | 8 +++--- .../components/zwave_js/translations/pt.json | 22 ++++++++++++++++ .../components/zwave_me/translations/pt.json | 15 +++++++++++ 300 files changed, 2217 insertions(+), 143 deletions(-) create mode 100644 homeassistant/components/adax/translations/pt.json create mode 100644 homeassistant/components/aemet/translations/pt.json create mode 100644 homeassistant/components/airthings/translations/pt.json create mode 100644 homeassistant/components/airzone/translations/pt.json create mode 100644 homeassistant/components/aladdin_connect/translations/pt.json create mode 100644 homeassistant/components/androidtv/translations/pt.json create mode 100644 homeassistant/components/anthemav/translations/pt.json create mode 100644 homeassistant/components/aseko_pool_live/translations/pt.json create mode 100644 homeassistant/components/asuswrt/translations/pt.json create mode 100644 homeassistant/components/aurora_abb_powerone/translations/pt.json create mode 100644 homeassistant/components/aussie_broadband/translations/pt.json create mode 100644 homeassistant/components/azure_event_hub/translations/pt.json create mode 100644 homeassistant/components/baf/translations/pt.json create mode 100644 homeassistant/components/balboa/translations/pt.json create mode 100644 homeassistant/components/bosch_shc/translations/pt.json create mode 100644 homeassistant/components/brunt/translations/pt.json create mode 100644 homeassistant/components/buienradar/translations/pt.json create mode 100644 homeassistant/components/co2signal/translations/pt.json create mode 100644 homeassistant/components/coinbase/translations/pt.json create mode 100644 homeassistant/components/cpuspeed/translations/pt.json create mode 100644 homeassistant/components/crownstone/translations/pt.json create mode 100644 homeassistant/components/deluge/translations/pt.json create mode 100644 homeassistant/components/discord/translations/pt.json create mode 100644 homeassistant/components/dlna_dmr/translations/pt.json create mode 100644 homeassistant/components/dlna_dms/translations/pt.json create mode 100644 homeassistant/components/efergy/translations/pt.json create mode 100644 homeassistant/components/eight_sleep/translations/pt.json create mode 100644 homeassistant/components/elmax/translations/pt.json create mode 100644 homeassistant/components/emonitor/translations/pt.json create mode 100644 homeassistant/components/enphase_envoy/translations/pt.json create mode 100644 homeassistant/components/environment_canada/translations/pt.json create mode 100644 homeassistant/components/fibaro/translations/pt.json create mode 100644 homeassistant/components/fivem/translations/pt.json create mode 100644 homeassistant/components/flux_led/translations/pt.json create mode 100644 homeassistant/components/forecast_solar/translations/pt.json create mode 100644 homeassistant/components/fritzbox_callmonitor/translations/pt.json create mode 100644 homeassistant/components/fronius/translations/pt.json create mode 100644 homeassistant/components/generic/translations/pt.json create mode 100644 homeassistant/components/geocaching/translations/pt.json create mode 100644 homeassistant/components/github/translations/pt.json create mode 100644 homeassistant/components/google/translations/pt.json create mode 100644 homeassistant/components/google_travel_time/translations/pt.json create mode 100644 homeassistant/components/growatt_server/translations/pt.json create mode 100644 homeassistant/components/honeywell/translations/pt.json create mode 100644 homeassistant/components/ialarm/translations/pt.json create mode 100644 homeassistant/components/intellifire/translations/pt.json create mode 100644 homeassistant/components/kaleidescape/translations/pt.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/pt.json create mode 100644 homeassistant/components/kostal_plenticore/translations/pt.json create mode 100644 homeassistant/components/kraken/translations/pt.json create mode 100644 homeassistant/components/launch_library/translations/pt.json create mode 100644 homeassistant/components/lg_soundbar/translations/pt.json create mode 100644 homeassistant/components/lookin/translations/pt.json create mode 100644 homeassistant/components/lyric/translations/pt.json create mode 100644 homeassistant/components/mazda/translations/pt.json create mode 100644 homeassistant/components/meater/translations/pt.json create mode 100644 homeassistant/components/met_eireann/translations/pt.json create mode 100644 homeassistant/components/mjpeg/translations/pt.json create mode 100644 homeassistant/components/modem_callerid/translations/pt.json create mode 100644 homeassistant/components/modern_forms/translations/pt.json create mode 100644 homeassistant/components/moehlenhoff_alpha2/translations/pt.json create mode 100644 homeassistant/components/motioneye/translations/pt.json create mode 100644 homeassistant/components/mysensors/translations/pt.json create mode 100644 homeassistant/components/nam/translations/pt.json create mode 100644 homeassistant/components/nanoleaf/translations/pt.json create mode 100644 homeassistant/components/netgear/translations/pt.json create mode 100644 homeassistant/components/nfandroidtv/translations/pt.json create mode 100644 homeassistant/components/nuki/translations/pt.json create mode 100644 homeassistant/components/octoprint/translations/pt.json create mode 100644 homeassistant/components/oncue/translations/pt.json create mode 100644 homeassistant/components/ondilo_ico/translations/pt.json create mode 100644 homeassistant/components/opengarage/translations/pt.json create mode 100644 homeassistant/components/overkiz/translations/pt.json create mode 100644 homeassistant/components/p1_monitor/translations/pt.json create mode 100644 homeassistant/components/picnic/translations/pt.json create mode 100644 homeassistant/components/pure_energie/translations/pt.json create mode 100644 homeassistant/components/pvoutput/translations/pt.json create mode 100644 homeassistant/components/qnap_qsw/translations/pt.json create mode 100644 homeassistant/components/radio_browser/translations/pt.json create mode 100644 homeassistant/components/radiotherm/translations/pt.json create mode 100644 homeassistant/components/rdw/translations/pt.json create mode 100644 homeassistant/components/renault/translations/pt.json create mode 100644 homeassistant/components/rhasspy/translations/ca.json create mode 100644 homeassistant/components/rhasspy/translations/de.json create mode 100644 homeassistant/components/rhasspy/translations/el.json create mode 100644 homeassistant/components/rhasspy/translations/ja.json create mode 100644 homeassistant/components/rhasspy/translations/pt.json create mode 100644 homeassistant/components/rhasspy/translations/zh-Hant.json create mode 100644 homeassistant/components/ridwell/translations/pt.json create mode 100644 homeassistant/components/rtsp_to_webrtc/translations/pt.json create mode 100644 homeassistant/components/sabnzbd/translations/pt.json create mode 100644 homeassistant/components/scrape/translations/pt.json create mode 100644 homeassistant/components/screenlogic/translations/pt.json create mode 100644 homeassistant/components/senseme/translations/pt.json create mode 100644 homeassistant/components/sensibo/translations/pt.json create mode 100644 homeassistant/components/sia/translations/pt.json create mode 100644 homeassistant/components/simplepush/translations/pt.json create mode 100644 homeassistant/components/skybell/translations/pt.json create mode 100644 homeassistant/components/slack/translations/pt.json create mode 100644 homeassistant/components/sleepiq/translations/pt.json create mode 100644 homeassistant/components/slimproto/translations/pt.json create mode 100644 homeassistant/components/solax/translations/pt.json create mode 100644 homeassistant/components/somfy_mylink/translations/pt.json create mode 100644 homeassistant/components/soundtouch/translations/pt.json create mode 100644 homeassistant/components/steam_online/translations/pt.json create mode 100644 homeassistant/components/steamist/translations/pt.json create mode 100644 homeassistant/components/surepetcare/translations/pt.json create mode 100644 homeassistant/components/switchbot/translations/pt.json create mode 100644 homeassistant/components/syncthing/translations/pt.json create mode 100644 homeassistant/components/system_bridge/translations/pt.json create mode 100644 homeassistant/components/tautulli/translations/pt.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/pt.json create mode 100644 homeassistant/components/tomorrowio/translations/pt.json create mode 100644 homeassistant/components/trafikverket_ferry/translations/pt.json create mode 100644 homeassistant/components/trafikverket_weatherstation/translations/pt.json create mode 100644 homeassistant/components/unifiprotect/translations/pt.json create mode 100644 homeassistant/components/venstar/translations/pt.json create mode 100644 homeassistant/components/vicare/translations/pt.json create mode 100644 homeassistant/components/vlc_telnet/translations/pt.json create mode 100644 homeassistant/components/wallbox/translations/pt.json create mode 100644 homeassistant/components/waze_travel_time/translations/pt.json create mode 100644 homeassistant/components/webostv/translations/pt.json create mode 100644 homeassistant/components/whirlpool/translations/pt.json create mode 100644 homeassistant/components/whois/translations/pt.json create mode 100644 homeassistant/components/wiz/translations/pt.json create mode 100644 homeassistant/components/wled/translations/select.pt.json create mode 100644 homeassistant/components/ws66i/translations/pt.json create mode 100644 homeassistant/components/yale_smart_alarm/translations/pt.json create mode 100644 homeassistant/components/yolink/translations/pt.json create mode 100644 homeassistant/components/youless/translations/pt.json create mode 100644 homeassistant/components/zwave_js/translations/pt.json create mode 100644 homeassistant/components/zwave_me/translations/pt.json diff --git a/homeassistant/components/abode/translations/ja.json b/homeassistant/components/abode/translations/ja.json index cd498691f4b..1eb5f1cc499 100644 --- a/homeassistant/components/abode/translations/ja.json +++ b/homeassistant/components/abode/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/abode/translations/pt.json b/homeassistant/components/abode/translations/pt.json index 95a51741222..3d6b007b471 100644 --- a/homeassistant/components/abode/translations/pt.json +++ b/homeassistant/components/abode/translations/pt.json @@ -18,7 +18,7 @@ "user": { "data": { "password": "Palavra-passe", - "username": "Endere\u00e7o de e-mail" + "username": "Email" } } } diff --git a/homeassistant/components/accuweather/translations/ja.json b/homeassistant/components/accuweather/translations/ja.json index c7ea9f1d264..b9c7819e78a 100644 --- a/homeassistant/components/accuweather/translations/ja.json +++ b/homeassistant/components/accuweather/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "create_entry": { "default": "\u4e00\u90e8\u306e\u30bb\u30f3\u30b5\u30fc\u306f\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u7d71\u5408\u306e\u69cb\u6210\u5f8c\u3001\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30ec\u30b8\u30b9\u30c8\u30ea\u3067\u305d\u308c\u3089\u3092\u6709\u52b9\u306b\u3067\u304d\u307e\u3059\u3002\n\u5929\u6c17\u4e88\u5831\u306f\u30c7\u30d5\u30a9\u30eb\u30c8\u3067\u306f\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002\u7d71\u5408\u306e\u30aa\u30d7\u30b7\u30e7\u30f3\u3067\u6709\u52b9\u306b\u3067\u304d\u307e\u3059\u3002" diff --git a/homeassistant/components/adax/translations/pt.json b/homeassistant/components/adax/translations/pt.json new file mode 100644 index 00000000000..b83b23758af --- /dev/null +++ b/homeassistant/components/adax/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "cloud": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/pt.json b/homeassistant/components/adguard/translations/pt.json index df9b6c03bc5..e389261748d 100644 --- a/homeassistant/components/adguard/translations/pt.json +++ b/homeassistant/components/adguard/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" }, @@ -14,7 +17,7 @@ "port": "Porta", "ssl": "Utiliza um certificado SSL", "username": "Nome de Utilizador", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar o certificado SSL" } } } diff --git a/homeassistant/components/aemet/translations/pt.json b/homeassistant/components/aemet/translations/pt.json new file mode 100644 index 00000000000..cc227afe3a3 --- /dev/null +++ b/homeassistant/components/aemet/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_api_key": "Chave de API inv\u00e1lida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings/translations/pt.json b/homeassistant/components/airthings/translations/pt.json new file mode 100644 index 00000000000..3b5850222d9 --- /dev/null +++ b/homeassistant/components/airthings/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/pt.json b/homeassistant/components/airvisual/translations/pt.json index cc1c500946d..75869241f0d 100644 --- a/homeassistant/components/airvisual/translations/pt.json +++ b/homeassistant/components/airvisual/translations/pt.json @@ -10,6 +10,11 @@ "invalid_api_key": "Chave de API inv\u00e1lida" }, "step": { + "geography_by_coords": { + "data": { + "latitude": "Latitude" + } + }, "node_pro": { "data": { "ip_address": "Servidor", @@ -18,7 +23,7 @@ }, "reauth_confirm": { "data": { - "api_key": "" + "api_key": "Chave da API" } } } diff --git a/homeassistant/components/airzone/translations/pt.json b/homeassistant/components/airzone/translations/pt.json new file mode 100644 index 00000000000..f681da4210f --- /dev/null +++ b/homeassistant/components/airzone/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/pt.json b/homeassistant/components/aladdin_connect/translations/pt.json new file mode 100644 index 00000000000..6c09ed1c852 --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/ja.json b/homeassistant/components/almond/translations/ja.json index 1d41aa41f87..898bc4bc1c0 100644 --- a/homeassistant/components/almond/translations/ja.json +++ b/homeassistant/components/almond/translations/ja.json @@ -4,7 +4,7 @@ "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/ambee/translations/pt.json b/homeassistant/components/ambee/translations/pt.json index 286cd58dd89..4a6d267473b 100644 --- a/homeassistant/components/ambee/translations/pt.json +++ b/homeassistant/components/ambee/translations/pt.json @@ -1,8 +1,14 @@ { "config": { "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + } + }, "user": { "data": { + "latitude": "Latitude", "name": "Nome" } } diff --git a/homeassistant/components/ambient_station/translations/pt.json b/homeassistant/components/ambient_station/translations/pt.json index c67faa25f0b..f40c2b211a9 100644 --- a/homeassistant/components/ambient_station/translations/pt.json +++ b/homeassistant/components/ambient_station/translations/pt.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "api_key": "Chave de API", + "api_key": "Chave da API", "app_key": "Chave de aplica\u00e7\u00e3o" }, "title": "Preencha as suas informa\u00e7\u00f5es" diff --git a/homeassistant/components/androidtv/translations/pt.json b/homeassistant/components/androidtv/translations/pt.json new file mode 100644 index 00000000000..09a78c773cc --- /dev/null +++ b/homeassistant/components/androidtv/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/pt.json b/homeassistant/components/anthemav/translations/pt.json new file mode 100644 index 00000000000..fa5aa3de317 --- /dev/null +++ b/homeassistant/components/anthemav/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aseko_pool_live/translations/pt.json b/homeassistant/components/aseko_pool_live/translations/pt.json new file mode 100644 index 00000000000..2933743c867 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "Email", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/pt.json b/homeassistant/components/asuswrt/translations/pt.json new file mode 100644 index 00000000000..54c86ef332a --- /dev/null +++ b/homeassistant/components/asuswrt/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta (leave empty for protocol default)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora_abb_powerone/translations/pt.json b/homeassistant/components/aurora_abb_powerone/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aussie_broadband/translations/pt.json b/homeassistant/components/aussie_broadband/translations/pt.json new file mode 100644 index 00000000000..e831e2ce397 --- /dev/null +++ b/homeassistant/components/aussie_broadband/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "username": "Nome de Utilizador" + } + } + } + }, + "options": { + "abort": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/pt.json b/homeassistant/components/awair/translations/pt.json index ea99bbf0167..c906e6f380e 100644 --- a/homeassistant/components/awair/translations/pt.json +++ b/homeassistant/components/awair/translations/pt.json @@ -16,6 +16,12 @@ "email": "Email" } }, + "reauth_confirm": { + "data": { + "access_token": "Token de Acesso", + "email": "Email" + } + }, "user": { "data": { "access_token": "Token de Acesso", diff --git a/homeassistant/components/azure_event_hub/translations/ja.json b/homeassistant/components/azure_event_hub/translations/ja.json index b504f86a3bd..720e57d8066 100644 --- a/homeassistant/components/azure_event_hub/translations/ja.json +++ b/homeassistant/components/azure_event_hub/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u8a8d\u8a3c\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002yaml\u3092\u524a\u9664\u3057\u3066\u69cb\u6210\u30d5\u30ed\u30fc(config flow)\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "unknown": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u8a8d\u8a3c\u63a5\u7d9a\u304c\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u3067\u5931\u6557\u3057\u307e\u3057\u305f\u3002yaml\u3092\u524a\u9664\u3057\u3066\u69cb\u6210\u30d5\u30ed\u30fc(config flow)\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "error": { diff --git a/homeassistant/components/azure_event_hub/translations/pt.json b/homeassistant/components/azure_event_hub/translations/pt.json new file mode 100644 index 00000000000..d252c078a2c --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/pt.json b/homeassistant/components/baf/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/baf/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/pt.json b/homeassistant/components/balboa/translations/pt.json new file mode 100644 index 00000000000..f13cad90edc --- /dev/null +++ b/homeassistant/components/balboa/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/pt.json b/homeassistant/components/binary_sensor/translations/pt.json index d204347bd02..a91c4987abb 100644 --- a/homeassistant/components/binary_sensor/translations/pt.json +++ b/homeassistant/components/binary_sensor/translations/pt.json @@ -105,6 +105,9 @@ "off": "Sem carregar", "on": "A carregar" }, + "carbon_monoxide": { + "on": "Detectado" + }, "cold": { "off": "Normal", "on": "Frio" @@ -186,7 +189,7 @@ }, "vibration": { "off": "Limpo", - "on": "Detetado" + "on": "Detectado" }, "window": { "off": "Fechada", diff --git a/homeassistant/components/blink/translations/pt.json b/homeassistant/components/blink/translations/pt.json index 76c420a584c..1f71ecf3f22 100644 --- a/homeassistant/components/blink/translations/pt.json +++ b/homeassistant/components/blink/translations/pt.json @@ -6,7 +6,8 @@ "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_access_token": "Token de acesso inv\u00e1lido", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" }, "step": { "2fa": { diff --git a/homeassistant/components/bosch_shc/translations/pt.json b/homeassistant/components/bosch_shc/translations/pt.json new file mode 100644 index 00000000000..1286d95f2df --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + }, + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/pt.json b/homeassistant/components/braviatv/translations/pt.json index e113d74d6fc..0838bb5d632 100644 --- a/homeassistant/components/braviatv/translations/pt.json +++ b/homeassistant/components/braviatv/translations/pt.json @@ -4,7 +4,7 @@ "already_configured": "Esta TV j\u00e1 est\u00e1 configurada." }, "error": { - "cannot_connect": "Falha na conex\u00e3o, nome de servidor inv\u00e1lido ou c\u00f3digo PIN.", + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido.", "unsupported_model": "O seu modelo de TV n\u00e3o \u00e9 suportado." }, diff --git a/homeassistant/components/brunt/translations/pt.json b/homeassistant/components/brunt/translations/pt.json new file mode 100644 index 00000000000..a27df267700 --- /dev/null +++ b/homeassistant/components/brunt/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + }, + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/pt.json b/homeassistant/components/bsblan/translations/pt.json index 5461f207375..3cb7a7d5891 100644 --- a/homeassistant/components/bsblan/translations/pt.json +++ b/homeassistant/components/bsblan/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" diff --git a/homeassistant/components/buienradar/translations/pt.json b/homeassistant/components/buienradar/translations/pt.json new file mode 100644 index 00000000000..2e6515edd09 --- /dev/null +++ b/homeassistant/components/buienradar/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "longitude": "Longitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/ja.json b/homeassistant/components/canary/translations/ja.json index 9f9903b86e4..1fcbcde47d1 100644 --- a/homeassistant/components/canary/translations/ja.json +++ b/homeassistant/components/canary/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { diff --git a/homeassistant/components/cast/translations/ja.json b/homeassistant/components/cast/translations/ja.json index 626ef56cba1..1dab27d9e54 100644 --- a/homeassistant/components/cast/translations/ja.json +++ b/homeassistant/components/cast/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "invalid_known_hosts": "\u65e2\u77e5\u306e\u30db\u30b9\u30c8\u306f\u3001\u30b3\u30f3\u30de\u3067\u533a\u5207\u3089\u308c\u305f\u30db\u30b9\u30c8\u306e\u30ea\u30b9\u30c8\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002" diff --git a/homeassistant/components/cast/translations/pt.json b/homeassistant/components/cast/translations/pt.json index bb29c923128..34770733822 100644 --- a/homeassistant/components/cast/translations/pt.json +++ b/homeassistant/components/cast/translations/pt.json @@ -8,7 +8,7 @@ "title": "Google Cast" }, "confirm": { - "description": "Deseja configurar o Google Cast?" + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" } } } diff --git a/homeassistant/components/cloudflare/translations/ja.json b/homeassistant/components/cloudflare/translations/ja.json index 1057d4e7bc5..08ffdd5c7a5 100644 --- a/homeassistant/components/cloudflare/translations/ja.json +++ b/homeassistant/components/cloudflare/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { diff --git a/homeassistant/components/cloudflare/translations/pt.json b/homeassistant/components/cloudflare/translations/pt.json index 158cd3f3f74..650823693d6 100644 --- a/homeassistant/components/cloudflare/translations/pt.json +++ b/homeassistant/components/cloudflare/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "unknown": "Erro inesperado" }, diff --git a/homeassistant/components/co2signal/translations/pt.json b/homeassistant/components/co2signal/translations/pt.json new file mode 100644 index 00000000000..cf6bc7f5bc4 --- /dev/null +++ b/homeassistant/components/co2signal/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Latitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/pt.json b/homeassistant/components/coinbase/translations/pt.json new file mode 100644 index 00000000000..49cb628dd85 --- /dev/null +++ b/homeassistant/components/coinbase/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cpuspeed/translations/ja.json b/homeassistant/components/cpuspeed/translations/ja.json index 12cec097b11..95e92eafa76 100644 --- a/homeassistant/components/cpuspeed/translations/ja.json +++ b/homeassistant/components/cpuspeed/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "not_compatible": "CPU\u60c5\u5831\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3002\u3053\u306e\u7d71\u5408\u306f\u3001\u304a\u4f7f\u3044\u306e\u30b7\u30b9\u30c6\u30e0\u3068\u4e92\u63db\u6027\u304c\u3042\u308a\u307e\u305b\u3093\u3002" }, "step": { diff --git a/homeassistant/components/cpuspeed/translations/pt.json b/homeassistant/components/cpuspeed/translations/pt.json new file mode 100644 index 00000000000..cf03f249d96 --- /dev/null +++ b/homeassistant/components/cpuspeed/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/crownstone/translations/pt.json b/homeassistant/components/crownstone/translations/pt.json new file mode 100644 index 00000000000..97ea705b32b --- /dev/null +++ b/homeassistant/components/crownstone/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "usb_manual_config": { + "data": { + "usb_manual_path": "Caminho do Dispositivo USB" + } + }, + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/pt.json b/homeassistant/components/deluge/translations/pt.json new file mode 100644 index 00000000000..fb1af357526 --- /dev/null +++ b/homeassistant/components/deluge/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/pt.json b/homeassistant/components/derivative/translations/pt.json index d6c0f4acd0b..6801ab6b6d4 100644 --- a/homeassistant/components/derivative/translations/pt.json +++ b/homeassistant/components/derivative/translations/pt.json @@ -7,5 +7,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data_description": { + "unit_prefix": "." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/pt.json b/homeassistant/components/devolo_home_control/translations/pt.json index 2215d148b7b..d60cc81f541 100644 --- a/homeassistant/components/devolo_home_control/translations/pt.json +++ b/homeassistant/components/devolo_home_control/translations/pt.json @@ -13,6 +13,11 @@ "password": "Palavra-passe", "username": "Email / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "password": "Palavra-passe" + } } } } diff --git a/homeassistant/components/dialogflow/translations/ja.json b/homeassistant/components/dialogflow/translations/ja.json index 27e5d35e35e..8ba9292c8cd 100644 --- a/homeassistant/components/dialogflow/translations/ja.json +++ b/homeassistant/components/dialogflow/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/discord/translations/pt.json b/homeassistant/components/discord/translations/pt.json new file mode 100644 index 00000000000..9925c2a7416 --- /dev/null +++ b/homeassistant/components/discord/translations/pt.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "API Token" + } + }, + "user": { + "data": { + "api_token": "API Token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/pt.json b/homeassistant/components/dlna_dmr/translations/pt.json new file mode 100644 index 00000000000..f6f3f84e497 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "manual": { + "data": { + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/pt.json b/homeassistant/components/dlna_dms/translations/pt.json new file mode 100644 index 00000000000..f67e36f3487 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/pt.json b/homeassistant/components/dsmr/translations/pt.json index ce8a9287272..c9fff00bd53 100644 --- a/homeassistant/components/dsmr/translations/pt.json +++ b/homeassistant/components/dsmr/translations/pt.json @@ -1,7 +1,23 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "setup_network": { + "data": { + "host": "Servidor" + } + }, + "setup_serial_manual_path": { + "data": { + "port": "Caminho do Dispositivo USB" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/ecobee/translations/ja.json b/homeassistant/components/ecobee/translations/ja.json index 73ac4cd1611..79f8add6a4d 100644 --- a/homeassistant/components/ecobee/translations/ja.json +++ b/homeassistant/components/ecobee/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "pin_request_failed": "ecobee\u304b\u3089\u306ePIN\u30ea\u30af\u30a8\u30b9\u30c8\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f; API\u30ad\u30fc\u304c\u6b63\u3057\u3044\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", diff --git a/homeassistant/components/econet/translations/pt.json b/homeassistant/components/econet/translations/pt.json index ce8a9287272..6c333b17fc9 100644 --- a/homeassistant/components/econet/translations/pt.json +++ b/homeassistant/components/econet/translations/pt.json @@ -1,7 +1,15 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "email": "Email" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/efergy/translations/pt.json b/homeassistant/components/efergy/translations/pt.json new file mode 100644 index 00000000000..8f319572c97 --- /dev/null +++ b/homeassistant/components/efergy/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/translations/pt.json b/homeassistant/components/eight_sleep/translations/pt.json new file mode 100644 index 00000000000..bcb163d8c10 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/pt.json b/homeassistant/components/elkm1/translations/pt.json index 2e669c21f1e..6b770832a24 100644 --- a/homeassistant/components/elkm1/translations/pt.json +++ b/homeassistant/components/elkm1/translations/pt.json @@ -4,6 +4,19 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" + }, + "step": { + "discovered_connection": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + }, + "manual_connection": { + "data": { + "username": "Nome de Utilizador" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/elmax/translations/pt.json b/homeassistant/components/elmax/translations/pt.json new file mode 100644 index 00000000000..bcb163d8c10 --- /dev/null +++ b/homeassistant/components/elmax/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/pt.json b/homeassistant/components/emonitor/translations/pt.json new file mode 100644 index 00000000000..8578f969852 --- /dev/null +++ b/homeassistant/components/emonitor/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/ja.json b/homeassistant/components/enocean/translations/ja.json index e0ec74d778f..bc71afe3a27 100644 --- a/homeassistant/components/enocean/translations/ja.json +++ b/homeassistant/components/enocean/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_dongle_path": "\u30c9\u30f3\u30b0\u30eb\u30d1\u30b9\u304c\u7121\u52b9", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "invalid_dongle_path": "\u3053\u306e\u30d1\u30b9\u306b\u6709\u52b9\u306a\u30c9\u30f3\u30b0\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" diff --git a/homeassistant/components/enphase_envoy/translations/pt.json b/homeassistant/components/enphase_envoy/translations/pt.json new file mode 100644 index 00000000000..1aa61659a93 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/translations/pt.json b/homeassistant/components/environment_canada/translations/pt.json new file mode 100644 index 00000000000..c7081cd694a --- /dev/null +++ b/homeassistant/components/environment_canada/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/pt.json b/homeassistant/components/esphome/translations/pt.json index 60eeaa3f4b2..da3216186ea 100644 --- a/homeassistant/components/esphome/translations/pt.json +++ b/homeassistant/components/esphome/translations/pt.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer" + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { "connection_error": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao ESP. Por favor, verifique se o seu arquivo YAML cont\u00e9m uma linha 'api:'.", diff --git a/homeassistant/components/evil_genius_labs/translations/pt.json b/homeassistant/components/evil_genius_labs/translations/pt.json index 4e8578a0a28..f13cad90edc 100644 --- a/homeassistant/components/evil_genius_labs/translations/pt.json +++ b/homeassistant/components/evil_genius_labs/translations/pt.json @@ -1,9 +1,12 @@ { "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { - "host": "Anfitri\u00e3o" + "host": "Servidor" } } } diff --git a/homeassistant/components/ezviz/translations/pt.json b/homeassistant/components/ezviz/translations/pt.json index cd669c3fd29..b7e8b2d8b81 100644 --- a/homeassistant/components/ezviz/translations/pt.json +++ b/homeassistant/components/ezviz/translations/pt.json @@ -5,6 +5,11 @@ "data": { "password": "Palavra-passe" } + }, + "user_custom_url": { + "data": { + "username": "Nome de Utilizador" + } } } } diff --git a/homeassistant/components/fibaro/translations/pt.json b/homeassistant/components/fibaro/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/fibaro/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/firmata/translations/pt.json b/homeassistant/components/firmata/translations/pt.json index 785f8887678..32a5bb07569 100644 --- a/homeassistant/components/firmata/translations/pt.json +++ b/homeassistant/components/firmata/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cannot_connect": "N\u0101o foi poss\u00edvel conectar ao board do Firmata durante a configura\u00e7\u00e3o" + "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { "one": "Um", diff --git a/homeassistant/components/fivem/translations/pt.json b/homeassistant/components/fivem/translations/pt.json new file mode 100644 index 00000000000..cf73866f4a0 --- /dev/null +++ b/homeassistant/components/fivem/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "unknown_error": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "name": "Nome", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fjaraskupan/translations/ja.json b/homeassistant/components/fjaraskupan/translations/ja.json index f22b3c04c84..e7c39f8d142 100644 --- a/homeassistant/components/fjaraskupan/translations/ja.json +++ b/homeassistant/components/fjaraskupan/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/flipr/translations/pt.json b/homeassistant/components/flipr/translations/pt.json index ce1bf4bb4b8..12a3977fdde 100644 --- a/homeassistant/components/flipr/translations/pt.json +++ b/homeassistant/components/flipr/translations/pt.json @@ -1,7 +1,15 @@ { "config": { "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/pt.json b/homeassistant/components/flux_led/translations/pt.json new file mode 100644 index 00000000000..7f2f103180a --- /dev/null +++ b/homeassistant/components/flux_led/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "flow_title": "", + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/pt.json b/homeassistant/components/forecast_solar/translations/pt.json new file mode 100644 index 00000000000..2e6515edd09 --- /dev/null +++ b/homeassistant/components/forecast_solar/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "longitude": "Longitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/pt.json b/homeassistant/components/foscam/translations/pt.json index 65a6a1558db..f1afd77d051 100644 --- a/homeassistant/components/foscam/translations/pt.json +++ b/homeassistant/components/foscam/translations/pt.json @@ -1,11 +1,16 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { "user": { "data": { + "host": "Servidor", "password": "Palavra-passe" } } diff --git a/homeassistant/components/fritz/translations/pt.json b/homeassistant/components/fritz/translations/pt.json index 9eeb4c35b69..263485b4587 100644 --- a/homeassistant/components/fritz/translations/pt.json +++ b/homeassistant/components/fritz/translations/pt.json @@ -3,6 +3,13 @@ "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" }, - "flow_title": "{name}" + "flow_title": "{name}", + "step": { + "user": { + "data": { + "port": "Porta" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/pt.json b/homeassistant/components/fritzbox_callmonitor/translations/pt.json new file mode 100644 index 00000000000..0077ceddd46 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/translations/pt.json b/homeassistant/components/fronius/translations/pt.json new file mode 100644 index 00000000000..f13cad90edc --- /dev/null +++ b/homeassistant/components/fronius/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/ja.json b/homeassistant/components/generic/translations/ja.json index 499de6409e6..8e1ba58b4df 100644 --- a/homeassistant/components/generic/translations/ja.json +++ b/homeassistant/components/generic/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "already_exists": "\u3053\u306eURL\u8a2d\u5b9a\u306e\u30ab\u30e1\u30e9\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059\u3002", diff --git a/homeassistant/components/generic/translations/pt.json b/homeassistant/components/generic/translations/pt.json new file mode 100644 index 00000000000..b59623f6607 --- /dev/null +++ b/homeassistant/components/generic/translations/pt.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "confirm": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geocaching/translations/pt.json b/homeassistant/components/geocaching/translations/pt.json new file mode 100644 index 00000000000..8685cf2d3c2 --- /dev/null +++ b/homeassistant/components/geocaching/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})" + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/ja.json b/homeassistant/components/geofency/translations/ja.json index e653cb99d43..812ba39b940 100644 --- a/homeassistant/components/geofency/translations/ja.json +++ b/homeassistant/components/geofency/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/github/translations/pt.json b/homeassistant/components/github/translations/pt.json new file mode 100644 index 00000000000..d252c078a2c --- /dev/null +++ b/homeassistant/components/github/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goodwe/translations/pt.json b/homeassistant/components/goodwe/translations/pt.json index ce8a9287272..410be6f924f 100644 --- a/homeassistant/components/goodwe/translations/pt.json +++ b/homeassistant/components/goodwe/translations/pt.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "host": "Endere\u00e7o IP" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/pt.json b/homeassistant/components/google/translations/pt.json new file mode 100644 index 00000000000..518809302e3 --- /dev/null +++ b/homeassistant/components/google/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_travel_time/translations/pt.json b/homeassistant/components/google_travel_time/translations/pt.json new file mode 100644 index 00000000000..286cd58dd89 --- /dev/null +++ b/homeassistant/components/google_travel_time/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/translations/ja.json b/homeassistant/components/gpslogger/translations/ja.json index 4674c074763..6b05591da9b 100644 --- a/homeassistant/components/gpslogger/translations/ja.json +++ b/homeassistant/components/gpslogger/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/gree/translations/ja.json b/homeassistant/components/gree/translations/ja.json index d1234b69652..981d3c1f285 100644 --- a/homeassistant/components/gree/translations/ja.json +++ b/homeassistant/components/gree/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/group/translations/pt.json b/homeassistant/components/group/translations/pt.json index 728b1dcbd95..b05b2868744 100644 --- a/homeassistant/components/group/translations/pt.json +++ b/homeassistant/components/group/translations/pt.json @@ -3,8 +3,7 @@ "step": { "media_player": { "data": { - "entities": "Membros", - "name": "Nome" + "entities": "Membros" } } } diff --git a/homeassistant/components/growatt_server/translations/pt.json b/homeassistant/components/growatt_server/translations/pt.json new file mode 100644 index 00000000000..a814cc1772d --- /dev/null +++ b/homeassistant/components/growatt_server/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome", + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/pt.json b/homeassistant/components/hangouts/translations/pt.json index 093deaecc15..b4feb91c76d 100644 --- a/homeassistant/components/hangouts/translations/pt.json +++ b/homeassistant/components/hangouts/translations/pt.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Google Hangouts j\u00e1 est\u00e1 configurado", - "unknown": "Ocorreu um erro desconhecido." + "unknown": "Erro inesperado" }, "error": { "invalid_2fa": "Autentica\u00e7\u00e3o por 2 fatores inv\u00e1lida, por favor, tente novamente.", diff --git a/homeassistant/components/heos/translations/ja.json b/homeassistant/components/heos/translations/ja.json index 55e075a548a..9a6e9513ecd 100644 --- a/homeassistant/components/heos/translations/ja.json +++ b/homeassistant/components/heos/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" diff --git a/homeassistant/components/hisense_aehw4a1/translations/ja.json b/homeassistant/components/hisense_aehw4a1/translations/ja.json index 75107c4c0fc..edace613685 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/ja.json +++ b/homeassistant/components/hisense_aehw4a1/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/home_plus_control/translations/ja.json b/homeassistant/components/home_plus_control/translations/ja.json index 3cef1cd5fa1..e1d0ade87b9 100644 --- a/homeassistant/components/home_plus_control/translations/ja.json +++ b/homeassistant/components/home_plus_control/translations/ja.json @@ -6,7 +6,7 @@ "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "create_entry": { "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" diff --git a/homeassistant/components/homekit/translations/pt.json b/homeassistant/components/homekit/translations/pt.json index f122a97b19c..4e25e3b691c 100644 --- a/homeassistant/components/homekit/translations/pt.json +++ b/homeassistant/components/homekit/translations/pt.json @@ -22,6 +22,7 @@ }, "init": { "data": { + "domains": "Dom\u00ednios a incluir", "mode": "Modo" }, "title": "Selecione os dom\u00ednios a serem expostos." diff --git a/homeassistant/components/homematicip_cloud/translations/pt.json b/homeassistant/components/homematicip_cloud/translations/pt.json index 645ba242561..2016dc9e13c 100644 --- a/homeassistant/components/homematicip_cloud/translations/pt.json +++ b/homeassistant/components/homematicip_cloud/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O ponto de acesso j\u00e1 se encontra configurado", + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "connection_aborted": "N\u00e3o foi poss\u00edvel ligar ao servidor HMIP", "unknown": "Ocorreu um erro desconhecido." }, diff --git a/homeassistant/components/honeywell/translations/pt.json b/homeassistant/components/honeywell/translations/pt.json new file mode 100644 index 00000000000..ebb712d0682 --- /dev/null +++ b/homeassistant/components/honeywell/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/pt.json b/homeassistant/components/huawei_lte/translations/pt.json index be10fe829f7..c8856eb2f79 100644 --- a/homeassistant/components/huawei_lte/translations/pt.json +++ b/homeassistant/components/huawei_lte/translations/pt.json @@ -13,7 +13,7 @@ "data": { "password": "Palavra-passe", "url": "", - "username": "Nome do utilizador" + "username": "Nome de Utilizador" }, "title": "Configurar o Huawei LTE" } diff --git a/homeassistant/components/huisbaasje/translations/pt.json b/homeassistant/components/huisbaasje/translations/pt.json index 3b5850222d9..a2f32087684 100644 --- a/homeassistant/components/huisbaasje/translations/pt.json +++ b/homeassistant/components/huisbaasje/translations/pt.json @@ -1,7 +1,16 @@ { "config": { "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/pt.json b/homeassistant/components/ialarm/translations/pt.json new file mode 100644 index 00000000000..0c5c7760566 --- /dev/null +++ b/homeassistant/components/ialarm/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/ja.json b/homeassistant/components/iaqualink/translations/ja.json index 20c3551ff80..968f4023522 100644 --- a/homeassistant/components/iaqualink/translations/ja.json +++ b/homeassistant/components/iaqualink/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/iaqualink/translations/pt.json b/homeassistant/components/iaqualink/translations/pt.json index 3b466866334..f73c9be5561 100644 --- a/homeassistant/components/iaqualink/translations/pt.json +++ b/homeassistant/components/iaqualink/translations/pt.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Palavra-passe", - "username": "Nome de utilizador / Endere\u00e7o de e-mail" + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/ifttt/translations/ja.json b/homeassistant/components/ifttt/translations/ja.json index 87844be51d4..05353ba44a1 100644 --- a/homeassistant/components/ifttt/translations/ja.json +++ b/homeassistant/components/ifttt/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/insteon/translations/ja.json b/homeassistant/components/insteon/translations/ja.json index 812f9c0bfd6..f5b41709d71 100644 --- a/homeassistant/components/insteon/translations/ja.json +++ b/homeassistant/components/insteon/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "not_insteon_device": "\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306f\u3001Insteon\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u306a\u3044", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/insteon/translations/pt.json b/homeassistant/components/insteon/translations/pt.json index 1475c2d00d3..4654d2c4de1 100644 --- a/homeassistant/components/insteon/translations/pt.json +++ b/homeassistant/components/insteon/translations/pt.json @@ -2,7 +2,7 @@ "config": { "abort": { "cannot_connect": "Falha na liga\u00e7\u00e3o", - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed" + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" diff --git a/homeassistant/components/intellifire/translations/pt.json b/homeassistant/components/intellifire/translations/pt.json new file mode 100644 index 00000000000..b86378c5e24 --- /dev/null +++ b/homeassistant/components/intellifire/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "flow_title": "{serial} ({host})", + "step": { + "api_config": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/translations/ja.json b/homeassistant/components/ios/translations/ja.json index 60fbfbea06f..d973d38e176 100644 --- a/homeassistant/components/ios/translations/ja.json +++ b/homeassistant/components/ios/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/ios/translations/pt.json b/homeassistant/components/ios/translations/pt.json index 319ba1e3759..096d42a6503 100644 --- a/homeassistant/components/ios/translations/pt.json +++ b/homeassistant/components/ios/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do componente iOS do Home Assistante \u00e9 necess\u00e1ria." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "confirm": { diff --git a/homeassistant/components/iotawatt/translations/pt.json b/homeassistant/components/iotawatt/translations/pt.json index 85b7c4b704a..6e210edaef7 100644 --- a/homeassistant/components/iotawatt/translations/pt.json +++ b/homeassistant/components/iotawatt/translations/pt.json @@ -1,9 +1,17 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { "auth": { "data": { - "username": "Nome de utilizador" + "username": "Nome de Utilizador" + } + }, + "user": { + "data": { + "host": "Servidor" } } } diff --git a/homeassistant/components/islamic_prayer_times/translations/ja.json b/homeassistant/components/islamic_prayer_times/translations/ja.json index ea6ad0c6522..9c38d5a4aab 100644 --- a/homeassistant/components/islamic_prayer_times/translations/ja.json +++ b/homeassistant/components/islamic_prayer_times/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/iss/translations/ja.json b/homeassistant/components/iss/translations/ja.json index bf5deeaa716..d53b9f8fecb 100644 --- a/homeassistant/components/iss/translations/ja.json +++ b/homeassistant/components/iss/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "latitude_longitude_not_defined": "Home Assistant\u3067\u7def\u5ea6\u3068\u7d4c\u5ea6\u304c\u5b9a\u7fa9\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/izone/translations/ja.json b/homeassistant/components/izone/translations/ja.json index bd5ae39dec4..1699170cbd9 100644 --- a/homeassistant/components/izone/translations/ja.json +++ b/homeassistant/components/izone/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/jellyfin/translations/ja.json b/homeassistant/components/jellyfin/translations/ja.json index 69cc9a5279d..fe3ae792913 100644 --- a/homeassistant/components/jellyfin/translations/ja.json +++ b/homeassistant/components/jellyfin/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/jellyfin/translations/pt.json b/homeassistant/components/jellyfin/translations/pt.json index d1f8622ba3f..8e3e0f68479 100644 --- a/homeassistant/components/jellyfin/translations/pt.json +++ b/homeassistant/components/jellyfin/translations/pt.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "password": "Palavra Passe", + "password": "Palavra-passe", "url": "Endere\u00e7o", "username": "Nome de utilizador" } diff --git a/homeassistant/components/kaleidescape/translations/pt.json b/homeassistant/components/kaleidescape/translations/pt.json new file mode 100644 index 00000000000..ce7cbc3f548 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/pt.json b/homeassistant/components/keenetic_ndms2/translations/pt.json new file mode 100644 index 00000000000..df69b1c4ff3 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/ja.json b/homeassistant/components/knx/translations/ja.json index dcd44d5838f..bbac3566bca 100644 --- a/homeassistant/components/knx/translations/ja.json +++ b/homeassistant/components/knx/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/knx/translations/pt.json b/homeassistant/components/knx/translations/pt.json index ba21368f797..7220ef495c9 100644 --- a/homeassistant/components/knx/translations/pt.json +++ b/homeassistant/components/knx/translations/pt.json @@ -3,7 +3,8 @@ "step": { "tunnel": { "data": { - "host": "Anfitri\u00e3o" + "host": "Anfitri\u00e3o", + "port": "Porta" } } } diff --git a/homeassistant/components/kodi/translations/pt.json b/homeassistant/components/kodi/translations/pt.json index 441d052867f..a20906321aa 100644 --- a/homeassistant/components/kodi/translations/pt.json +++ b/homeassistant/components/kodi/translations/pt.json @@ -25,7 +25,7 @@ "data": { "host": "Servidor", "port": "Porta", - "ssl": "Conecte-se por SSL" + "ssl": "Utiliza um certificado SSL" }, "description": "Informa\u00e7\u00f5es de conex\u00e3o Kodi. Certifique-se de habilitar \"Permitir controle do Kodi via HTTP\" em Sistema / Configura\u00e7\u00f5es / Rede / Servi\u00e7os." }, diff --git a/homeassistant/components/konnected/translations/pt.json b/homeassistant/components/konnected/translations/pt.json index 64aaf6cbf4a..2ba94342093 100644 --- a/homeassistant/components/konnected/translations/pt.json +++ b/homeassistant/components/konnected/translations/pt.json @@ -25,7 +25,7 @@ "step": { "options_binary": { "data": { - "name": "Nome (opcional)" + "name": "Nome" } }, "options_digital": { @@ -55,7 +55,7 @@ }, "options_switch": { "data": { - "name": "Nome (opcional)" + "name": "Nome" } } } diff --git a/homeassistant/components/kostal_plenticore/translations/pt.json b/homeassistant/components/kostal_plenticore/translations/pt.json new file mode 100644 index 00000000000..df69b1c4ff3 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/translations/ja.json b/homeassistant/components/kraken/translations/ja.json index 1d581131252..40a1ac53232 100644 --- a/homeassistant/components/kraken/translations/ja.json +++ b/homeassistant/components/kraken/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/kraken/translations/pt.json b/homeassistant/components/kraken/translations/pt.json new file mode 100644 index 00000000000..66ccff7f372 --- /dev/null +++ b/homeassistant/components/kraken/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/ja.json b/homeassistant/components/kulersky/translations/ja.json index d1234b69652..981d3c1f285 100644 --- a/homeassistant/components/kulersky/translations/ja.json +++ b/homeassistant/components/kulersky/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/launch_library/translations/ja.json b/homeassistant/components/launch_library/translations/ja.json index f0e85fa9a4a..c824577578f 100644 --- a/homeassistant/components/launch_library/translations/ja.json +++ b/homeassistant/components/launch_library/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/launch_library/translations/pt.json b/homeassistant/components/launch_library/translations/pt.json new file mode 100644 index 00000000000..25538aa0036 --- /dev/null +++ b/homeassistant/components/launch_library/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/ja.json b/homeassistant/components/laundrify/translations/ja.json index 020b7578e80..f80f610ac9a 100644 --- a/homeassistant/components/laundrify/translations/ja.json +++ b/homeassistant/components/laundrify/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/lg_soundbar/translations/pt.json b/homeassistant/components/lg_soundbar/translations/pt.json new file mode 100644 index 00000000000..91ff56e73f8 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/pt.json b/homeassistant/components/life360/translations/pt.json index 71370e40068..cc3b190458f 100644 --- a/homeassistant/components/life360/translations/pt.json +++ b/homeassistant/components/life360/translations/pt.json @@ -1,16 +1,25 @@ { "config": { "abort": { + "already_configured": "Conta j\u00e1 configurada", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", "unknown": "Erro inesperado" }, "error": { "already_configured": "Conta j\u00e1 configurada", + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_username": "Nome de utilizador incorreto", "unknown": "Erro inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + }, + "title": "Reautenticar integra\u00e7\u00e3o" + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/lifx/translations/ja.json b/homeassistant/components/lifx/translations/ja.json index 6cfa33a7ace..1945c3112f0 100644 --- a/homeassistant/components/lifx/translations/ja.json +++ b/homeassistant/components/lifx/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/lifx/translations/pt.json b/homeassistant/components/lifx/translations/pt.json index 56064a70e0d..594ac7dacc4 100644 --- a/homeassistant/components/lifx/translations/pt.json +++ b/homeassistant/components/lifx/translations/pt.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Nenhum dispositivo LIFX encontrado na rede.", - "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do LIFX \u00e9 permitida." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "confirm": { diff --git a/homeassistant/components/litejet/translations/ja.json b/homeassistant/components/litejet/translations/ja.json index c26dc073113..8a7f367f66a 100644 --- a/homeassistant/components/litejet/translations/ja.json +++ b/homeassistant/components/litejet/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "open_failed": "\u6307\u5b9a\u3055\u308c\u305f\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8\u3092\u958b\u304f\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3002" diff --git a/homeassistant/components/litterrobot/translations/pt.json b/homeassistant/components/litterrobot/translations/pt.json index 7953cf5625c..c2bf0536ccf 100644 --- a/homeassistant/components/litterrobot/translations/pt.json +++ b/homeassistant/components/litterrobot/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Conta j\u00e1 configurada" }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", diff --git a/homeassistant/components/local_ip/translations/ja.json b/homeassistant/components/local_ip/translations/ja.json index f5d2efd6613..6ba538c36ce 100644 --- a/homeassistant/components/local_ip/translations/ja.json +++ b/homeassistant/components/local_ip/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/locative/translations/ja.json b/homeassistant/components/locative/translations/ja.json index a4f03bde29f..ee9e6f5f1a1 100644 --- a/homeassistant/components/locative/translations/ja.json +++ b/homeassistant/components/locative/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/lookin/translations/pt.json b/homeassistant/components/lookin/translations/pt.json new file mode 100644 index 00000000000..28ad8b6c8f3 --- /dev/null +++ b/homeassistant/components/lookin/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/translations/pt.json b/homeassistant/components/luftdaten/translations/pt.json index 709ed6af0a1..12e4c078d8c 100644 --- a/homeassistant/components/luftdaten/translations/pt.json +++ b/homeassistant/components/luftdaten/translations/pt.json @@ -9,7 +9,7 @@ "user": { "data": { "show_on_map": "Mostrar no mapa", - "station_id": "Luftdaten Sensor ID" + "station_id": "Sensor ID" } } } diff --git a/homeassistant/components/lutron_caseta/translations/pt.json b/homeassistant/components/lutron_caseta/translations/pt.json index a04f550a71a..f4fdf676dbd 100644 --- a/homeassistant/components/lutron_caseta/translations/pt.json +++ b/homeassistant/components/lutron_caseta/translations/pt.json @@ -6,6 +6,13 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/pt.json b/homeassistant/components/lyric/translations/pt.json new file mode 100644 index 00000000000..002029ae6f7 --- /dev/null +++ b/homeassistant/components/lyric/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." + }, + "step": { + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/ja.json b/homeassistant/components/mailgun/translations/ja.json index 34c6ced3f38..9c0f731f8fb 100644 --- a/homeassistant/components/mailgun/translations/ja.json +++ b/homeassistant/components/mailgun/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/mazda/translations/pt.json b/homeassistant/components/mazda/translations/pt.json new file mode 100644 index 00000000000..70ac9ae7bf8 --- /dev/null +++ b/homeassistant/components/mazda/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/pt.json b/homeassistant/components/meater/translations/pt.json new file mode 100644 index 00000000000..69bade43888 --- /dev/null +++ b/homeassistant/components/meater/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "unknown_auth_error": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + }, + "user": { + "data": { + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met_eireann/translations/pt.json b/homeassistant/components/met_eireann/translations/pt.json new file mode 100644 index 00000000000..785a126c56b --- /dev/null +++ b/homeassistant/components/met_eireann/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Latitude", + "name": "Nome" + }, + "title": "Localiza\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteoclimatic/translations/pt.json b/homeassistant/components/meteoclimatic/translations/pt.json index ce8a9287272..245d9637441 100644 --- a/homeassistant/components/meteoclimatic/translations/pt.json +++ b/homeassistant/components/meteoclimatic/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "unknown": "Erro inesperado" } } } \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/pt.json b/homeassistant/components/mjpeg/translations/pt.json new file mode 100644 index 00000000000..1967a9e4a50 --- /dev/null +++ b/homeassistant/components/mjpeg/translations/pt.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome", + "password": "Palavra-passe" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador", + "verify_ssl": "Verificar o certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/pt.json b/homeassistant/components/modem_callerid/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/pt.json b/homeassistant/components/modern_forms/translations/pt.json new file mode 100644 index 00000000000..ce7cbc3f548 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/pt.json b/homeassistant/components/moehlenhoff_alpha2/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/ja.json b/homeassistant/components/moon/translations/ja.json index 6544580781f..f7678a63278 100644 --- a/homeassistant/components/moon/translations/ja.json +++ b/homeassistant/components/moon/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/motion_blinds/translations/pt.json b/homeassistant/components/motion_blinds/translations/pt.json index 7538043f1ce..ccf03b80e43 100644 --- a/homeassistant/components/motion_blinds/translations/pt.json +++ b/homeassistant/components/motion_blinds/translations/pt.json @@ -5,7 +5,7 @@ "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "connection_error": "Falha na liga\u00e7\u00e3o" }, - "flow_title": "Cortinas Motion", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/motioneye/translations/pt.json b/homeassistant/components/motioneye/translations/pt.json new file mode 100644 index 00000000000..848cfe1ac2d --- /dev/null +++ b/homeassistant/components/motioneye/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index 59dff554676..07af6230aa6 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" diff --git a/homeassistant/components/mqtt/translations/pt.json b/homeassistant/components/mqtt/translations/pt.json index 209c33cf165..6ff10cf515c 100644 --- a/homeassistant/components/mqtt/translations/pt.json +++ b/homeassistant/components/mqtt/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Apenas uma configura\u00e7\u00e3o do MQTT \u00e9 permitida." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel ligar ao broker." @@ -57,7 +57,7 @@ "will_retain": "Reter mensagem testamental", "will_topic": "T\u00f3pico da mensagem testamental" }, - "description": "Por favor, selecione as op\u00e7\u00f5es do MQTT." + "description": "Discovery - If discovery is enabled (recommended), Home Assistant will automatically discover devices and entities which publish their configuration on the MQTT broker. If discovery is disabled, all configuration must be done manually.\nBirth message - The birth message will be sent each time Home Assistant (re)connects to the MQTT broker.\nWill message - The will message will be sent each time Home Assistant loses its connection to the broker, both in case of a clean (e.g. Home Assistant shutting down) and in case of an unclean (e.g. Home Assistant crashing or losing its network connection) disconnect." } } } diff --git a/homeassistant/components/mysensors/translations/pt.json b/homeassistant/components/mysensors/translations/pt.json new file mode 100644 index 00000000000..3ace45dd942 --- /dev/null +++ b/homeassistant/components/mysensors/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/pt.json b/homeassistant/components/nam/translations/pt.json new file mode 100644 index 00000000000..0aa6df94840 --- /dev/null +++ b/homeassistant/components/nam/translations/pt.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "credentials": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + }, + "reauth_confirm": { + "data": { + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/pt.json b/homeassistant/components/nanoleaf/translations/pt.json new file mode 100644 index 00000000000..7293cf1c3c3 --- /dev/null +++ b/homeassistant/components/nanoleaf/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index 398b09f92c1..55d9b9a0348 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -7,7 +7,7 @@ "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "unknown_authorize_url_generation": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u4e2d\u306b\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002" }, "create_entry": { diff --git a/homeassistant/components/nest/translations/pt.json b/homeassistant/components/nest/translations/pt.json index 13a7439b93d..40b70a2c67f 100644 --- a/homeassistant/components/nest/translations/pt.json +++ b/homeassistant/components/nest/translations/pt.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "Conta j\u00e1 configurada", "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "invalid_access_token": "Token de acesso inv\u00e1lido", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", @@ -14,9 +16,14 @@ "internal_error": "Erro interno ao validar o c\u00f3digo", "invalid_pin": "C\u00f3digo PIN inv\u00e1lido", "timeout": "Limite temporal ultrapassado ao validar c\u00f3digo", - "unknown": "Erro desconhecido ao validar o c\u00f3digo" + "unknown": "Erro inesperado" }, "step": { + "auth": { + "data": { + "code": "Token de Acesso" + } + }, "init": { "data": { "flow_impl": "Fornecedor" @@ -33,6 +40,9 @@ }, "pick_implementation": { "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" } } }, diff --git a/homeassistant/components/netatmo/translations/ja.json b/homeassistant/components/netatmo/translations/ja.json index 016411aef41..c60eb3dcd5e 100644 --- a/homeassistant/components/netatmo/translations/ja.json +++ b/homeassistant/components/netatmo/translations/ja.json @@ -5,7 +5,7 @@ "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "create_entry": { "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" diff --git a/homeassistant/components/netatmo/translations/pt.json b/homeassistant/components/netatmo/translations/pt.json index e39ecffa8a7..191337f9812 100644 --- a/homeassistant/components/netatmo/translations/pt.json +++ b/homeassistant/components/netatmo/translations/pt.json @@ -4,6 +4,7 @@ "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "create_entry": { @@ -12,6 +13,9 @@ "step": { "pick_implementation": { "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" } } }, diff --git a/homeassistant/components/netgear/translations/pt.json b/homeassistant/components/netgear/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/netgear/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/pt.json b/homeassistant/components/nextdns/translations/pt.json index 92f429b6a45..cd774c0c6cd 100644 --- a/homeassistant/components/nextdns/translations/pt.json +++ b/homeassistant/components/nextdns/translations/pt.json @@ -1,9 +1,14 @@ { "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_api_key": "Chave de API inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { - "api_key": "Chave API" + "api_key": "Chave da API" } } } diff --git a/homeassistant/components/nfandroidtv/translations/pt.json b/homeassistant/components/nfandroidtv/translations/pt.json new file mode 100644 index 00000000000..e3233d5779f --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/ja.json b/homeassistant/components/nina/translations/ja.json index fa3fa6f4995..5591b2b0986 100644 --- a/homeassistant/components/nina/translations/ja.json +++ b/homeassistant/components/nina/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/nina/translations/pt.json b/homeassistant/components/nina/translations/pt.json index 052bde4e432..f86b761e487 100644 --- a/homeassistant/components/nina/translations/pt.json +++ b/homeassistant/components/nina/translations/pt.json @@ -1,9 +1,22 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, "step": { "user": { "title": "Selecione a cidade/distrito" } } + }, + "options": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + } } } \ No newline at end of file diff --git a/homeassistant/components/notion/translations/pt.json b/homeassistant/components/notion/translations/pt.json index e92d51b2058..4ce3e317e32 100644 --- a/homeassistant/components/notion/translations/pt.json +++ b/homeassistant/components/notion/translations/pt.json @@ -7,6 +7,12 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + }, + "title": "Reautenticar integra\u00e7\u00e3o" + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/nuki/translations/pt.json b/homeassistant/components/nuki/translations/pt.json new file mode 100644 index 00000000000..ce7cbc3f548 --- /dev/null +++ b/homeassistant/components/nuki/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/pt.json b/homeassistant/components/nws/translations/pt.json index 2447be7ee67..3d9fdf5a2d3 100644 --- a/homeassistant/components/nws/translations/pt.json +++ b/homeassistant/components/nws/translations/pt.json @@ -4,7 +4,7 @@ "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falha ao conectar, tente novamente", + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/nzbget/translations/ja.json b/homeassistant/components/nzbget/translations/ja.json index c6b485976a7..43a741977f7 100644 --- a/homeassistant/components/nzbget/translations/ja.json +++ b/homeassistant/components/nzbget/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { diff --git a/homeassistant/components/nzbget/translations/pt.json b/homeassistant/components/nzbget/translations/pt.json index ba038c72c68..4bbc3a839fd 100644 --- a/homeassistant/components/nzbget/translations/pt.json +++ b/homeassistant/components/nzbget/translations/pt.json @@ -15,7 +15,7 @@ "name": "Nome", "password": "Palavra-passe", "port": "Porta", - "ssl": "NZBGet usa um certificado SSL", + "ssl": "Utiliza um certificado SSL", "username": "Nome de Utilizador", "verify_ssl": "NZBGet usa um certificado adequado" }, diff --git a/homeassistant/components/octoprint/translations/pt.json b/homeassistant/components/octoprint/translations/pt.json new file mode 100644 index 00000000000..7fef03088cb --- /dev/null +++ b/homeassistant/components/octoprint/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "unknown": "Erro inesperado" + }, + "error": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/ja.json b/homeassistant/components/omnilogic/translations/ja.json index c8f97ac3a40..f66f0a4878f 100644 --- a/homeassistant/components/omnilogic/translations/ja.json +++ b/homeassistant/components/omnilogic/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/oncue/translations/pt.json b/homeassistant/components/oncue/translations/pt.json new file mode 100644 index 00000000000..3b5850222d9 --- /dev/null +++ b/homeassistant/components/oncue/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/pt.json b/homeassistant/components/ondilo_ico/translations/pt.json new file mode 100644 index 00000000000..b7b721c012c --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/pt.json b/homeassistant/components/onewire/translations/pt.json index db0e0c2a137..91786f4b324 100644 --- a/homeassistant/components/onewire/translations/pt.json +++ b/homeassistant/components/onewire/translations/pt.json @@ -5,6 +5,13 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/pt.json b/homeassistant/components/onvif/translations/pt.json index f79bbec3201..4240578ca47 100644 --- a/homeassistant/components/onvif/translations/pt.json +++ b/homeassistant/components/onvif/translations/pt.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "O dispositivo ONVIF j\u00e1 est\u00e1 configurado.", - "already_in_progress": "O fluxo de configura\u00e7\u00e3o para o dispositivo ONVIF j\u00e1 est\u00e1 em andamento.", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "no_h264": "N\u00e3o existem fluxos H264 dispon\u00edveis. Verifique a configura\u00e7\u00e3o de perfil no seu dispositivo.", "no_mac": "N\u00e3o foi poss\u00edvel configurar o ID unico para o dispositivo ONVIF.", "onvif_error": "Erro ao configurar o dispositivo ONVIF. Verifique os logs para obter mais informa\u00e7\u00f5es." @@ -11,6 +11,11 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { + "configure": { + "data": { + "host": "Servidor" + } + }, "configure_profile": { "data": { "include": "Criar entidade da c\u00e2mara" diff --git a/homeassistant/components/opengarage/translations/pt.json b/homeassistant/components/opengarage/translations/pt.json new file mode 100644 index 00000000000..1ebfd763b2c --- /dev/null +++ b/homeassistant/components/opengarage/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "port": "Porta", + "verify_ssl": "Verificar o certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/pt.json b/homeassistant/components/openuv/translations/pt.json index 6433111fe81..64d5de8785a 100644 --- a/homeassistant/components/openuv/translations/pt.json +++ b/homeassistant/components/openuv/translations/pt.json @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "api_key": "Chave de API do OpenUV", + "api_key": "Chave da API", "elevation": "Eleva\u00e7\u00e3o", "latitude": "Latitude", "longitude": "Longitude" diff --git a/homeassistant/components/overkiz/translations/pt.json b/homeassistant/components/overkiz/translations/pt.json new file mode 100644 index 00000000000..1e3d9138c84 --- /dev/null +++ b/homeassistant/components/overkiz/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/translations/ja.json b/homeassistant/components/owntracks/translations/ja.json index 998478a9cc8..ca30df2b5f0 100644 --- a/homeassistant/components/owntracks/translations/ja.json +++ b/homeassistant/components/owntracks/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "create_entry": { "default": "\n\nAndroid\u306e\u5834\u5408\u3001[OwnTracks app]({android_url})\u3092\u958b\u304d\u3001\u74b0\u5883\u8a2d\u5b9a -> \u63a5\u7d9a \u306b\u79fb\u52d5\u3057\u3066\u3001\u6b21\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification(\u8b58\u5225\u60c5\u5831):\n - Username: `''`\n - Device ID: `''`\n\nOS\u306e\u5834\u5408\u3001[OwnTracks app]({ios_url})\u3092\u958b\u304d\u3001\u5de6\u4e0a\u306e(i)\u30a2\u30a4\u30b3\u30f3\u3092\u30bf\u30c3\u30d7\u3057\u3066 -> \u8a2d\u5b9a\u3002\u6b21\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3057\u307e\u3059:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication(\u8a8d\u8a3c\u3092\u30aa\u30f3\u306b\u3059\u308b)\n - UserID: `''`\n\n{secret}\n\n\u8a73\u7d30\u306f[\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8]({docs_url})\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044\u3002" diff --git a/homeassistant/components/p1_monitor/translations/pt.json b/homeassistant/components/p1_monitor/translations/pt.json new file mode 100644 index 00000000000..38336a1d5de --- /dev/null +++ b/homeassistant/components/p1_monitor/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/pt.json b/homeassistant/components/philips_js/translations/pt.json index 4646fcae7dc..f6815d0a16a 100644 --- a/homeassistant/components/philips_js/translations/pt.json +++ b/homeassistant/components/philips_js/translations/pt.json @@ -1,13 +1,20 @@ { "config": { "error": { - "invalid_pin": "PIN inv\u00e1lido" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_pin": "PIN inv\u00e1lido", + "unknown": "Erro inesperado" }, "step": { "pair": { "data": { "pin": "C\u00f3digo PIN" } + }, + "user": { + "data": { + "host": "Servidor" + } } } } diff --git a/homeassistant/components/pi_hole/translations/pt.json b/homeassistant/components/pi_hole/translations/pt.json index ce1b6a07d2d..e75629c42e0 100644 --- a/homeassistant/components/pi_hole/translations/pt.json +++ b/homeassistant/components/pi_hole/translations/pt.json @@ -7,6 +7,11 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { + "api_key": { + "data": { + "api_key": "Chave da API" + } + }, "user": { "data": { "api_key": "Chave da API", diff --git a/homeassistant/components/picnic/translations/pt.json b/homeassistant/components/picnic/translations/pt.json new file mode 100644 index 00000000000..79bc83a3f69 --- /dev/null +++ b/homeassistant/components/picnic/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/ja.json b/homeassistant/components/plaato/translations/ja.json index 0842ca6da74..1d7dfc19010 100644 --- a/homeassistant/components/plaato/translations/ja.json +++ b/homeassistant/components/plaato/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/plaato/translations/pt.json b/homeassistant/components/plaato/translations/pt.json index 2eeb965754a..3039abbcafb 100644 --- a/homeassistant/components/plaato/translations/pt.json +++ b/homeassistant/components/plaato/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "A Conta j\u00e1 est\u00e1 configurada", + "already_configured": "Conta j\u00e1 configurada", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." }, diff --git a/homeassistant/components/plex/translations/pt.json b/homeassistant/components/plex/translations/pt.json index 3b63ab169e2..6daae889e80 100644 --- a/homeassistant/components/plex/translations/pt.json +++ b/homeassistant/components/plex/translations/pt.json @@ -4,7 +4,7 @@ "already_configured": "Este servidor Plex j\u00e1 est\u00e1 configurado", "already_in_progress": "Plex est\u00e1 a ser configurado", "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", - "unknown": "Falha por motivo desconhecido" + "unknown": "Erro inesperado" }, "error": { "faulty_credentials": "A autoriza\u00e7\u00e3o falhou, verifique o token" diff --git a/homeassistant/components/point/translations/ja.json b/homeassistant/components/point/translations/ja.json index 6d573895877..884d6d9a1db 100644 --- a/homeassistant/components/point/translations/ja.json +++ b/homeassistant/components/point/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "already_setup": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "external_setup": "\u5225\u306e\u30d5\u30ed\u30fc\u304b\u3089\u30dd\u30a4\u30f3\u30c8\u304c\u6b63\u5e38\u306b\u69cb\u6210\u3055\u308c\u307e\u3057\u305f\u3002", "no_flows": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", diff --git a/homeassistant/components/point/translations/pt.json b/homeassistant/components/point/translations/pt.json index 3af92508762..fdb8b0c2c8f 100644 --- a/homeassistant/components/point/translations/pt.json +++ b/homeassistant/components/point/translations/pt.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_setup": "S\u00f3 pode configurar uma \u00fanica conta Point.", + "already_setup": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", "external_setup": "Point configurado com \u00eaxito a partir de outro fluxo.", "no_flows": "\u00c9 necess\u00e1rio configurar o Point antes de poder autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es] (https://www.home-assistant.io/components/point/).", "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." }, "create_entry": { - "default": "Autenticado com sucesso com Minut para o(s) seu(s) dispositivo (s) Point" + "default": "Autenticado com sucesso" }, "error": { "follow_link": "Por favor, siga o link e autentique antes de pressionar Enviar", diff --git a/homeassistant/components/powerwall/translations/pt.json b/homeassistant/components/powerwall/translations/pt.json index c748619963b..1ded97b1b27 100644 --- a/homeassistant/components/powerwall/translations/pt.json +++ b/homeassistant/components/powerwall/translations/pt.json @@ -8,9 +8,15 @@ "unknown": "Erro inesperado" }, "step": { + "reauth_confim": { + "data": { + "password": "Palavra-passe" + } + }, "user": { "data": { - "ip_address": "Endere\u00e7o IP" + "ip_address": "Endere\u00e7o IP", + "password": "Palavra-passe" } } } diff --git a/homeassistant/components/profiler/translations/ja.json b/homeassistant/components/profiler/translations/ja.json index c9c3cc04633..acdaf12a90c 100644 --- a/homeassistant/components/profiler/translations/ja.json +++ b/homeassistant/components/profiler/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/prosegur/translations/pt.json b/homeassistant/components/prosegur/translations/pt.json index d479d880d7f..a4b08e68352 100644 --- a/homeassistant/components/prosegur/translations/pt.json +++ b/homeassistant/components/prosegur/translations/pt.json @@ -8,6 +8,11 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/ps4/translations/pt.json b/homeassistant/components/ps4/translations/pt.json index cf428838b0b..ac585a23994 100644 --- a/homeassistant/components/ps4/translations/pt.json +++ b/homeassistant/components/ps4/translations/pt.json @@ -15,7 +15,7 @@ "step": { "link": { "data": { - "code": "PIN", + "code": "C\u00f3digo PIN", "ip_address": "Endere\u00e7o de IP", "name": "Nome", "region": "Regi\u00e3o" diff --git a/homeassistant/components/pure_energie/translations/pt.json b/homeassistant/components/pure_energie/translations/pt.json new file mode 100644 index 00000000000..ce7cbc3f548 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvoutput/translations/pt.json b/homeassistant/components/pvoutput/translations/pt.json new file mode 100644 index 00000000000..3b5850222d9 --- /dev/null +++ b/homeassistant/components/pvoutput/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/pt.json b/homeassistant/components/qnap_qsw/translations/pt.json new file mode 100644 index 00000000000..9f02a9e2519 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "discovered_connection": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + }, + "user": { + "data": { + "password": "Palavra-passe", + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radio_browser/translations/ja.json b/homeassistant/components/radio_browser/translations/ja.json index 24b32e6e30a..0143961d6ce 100644 --- a/homeassistant/components/radio_browser/translations/ja.json +++ b/homeassistant/components/radio_browser/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/radio_browser/translations/pt.json b/homeassistant/components/radio_browser/translations/pt.json new file mode 100644 index 00000000000..25538aa0036 --- /dev/null +++ b/homeassistant/components/radio_browser/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/pt.json b/homeassistant/components/radiotherm/translations/pt.json new file mode 100644 index 00000000000..ae100e45845 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainforest_eagle/translations/pt.json b/homeassistant/components/rainforest_eagle/translations/pt.json index 4e8578a0a28..91786f4b324 100644 --- a/homeassistant/components/rainforest_eagle/translations/pt.json +++ b/homeassistant/components/rainforest_eagle/translations/pt.json @@ -1,9 +1,15 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { - "host": "Anfitri\u00e3o" + "host": "Servidor" } } } diff --git a/homeassistant/components/rdw/translations/pt.json b/homeassistant/components/rdw/translations/pt.json new file mode 100644 index 00000000000..3b5850222d9 --- /dev/null +++ b/homeassistant/components/rdw/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/pt.json b/homeassistant/components/renault/translations/pt.json new file mode 100644 index 00000000000..f2223dc3827 --- /dev/null +++ b/homeassistant/components/renault/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "invalid_credentials": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/ja.json b/homeassistant/components/rfxtrx/translations/ja.json index 9b22d34af58..755621839c8 100644 --- a/homeassistant/components/rfxtrx/translations/ja.json +++ b/homeassistant/components/rfxtrx/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "already_configured": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { diff --git a/homeassistant/components/rhasspy/translations/ca.json b/homeassistant/components/rhasspy/translations/ca.json new file mode 100644 index 00000000000..cc92e3ec9f1 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/ca.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/de.json b/homeassistant/components/rhasspy/translations/de.json new file mode 100644 index 00000000000..953cb89400a --- /dev/null +++ b/homeassistant/components/rhasspy/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "user": { + "description": "M\u00f6chtest du die Rhasspy-Unterst\u00fctzung aktivieren?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/el.json b/homeassistant/components/rhasspy/translations/el.json new file mode 100644 index 00000000000..fe181ecdb04 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/el.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + }, + "step": { + "user": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 Rhasspy;" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/ja.json b/homeassistant/components/rhasspy/translations/ja.json new file mode 100644 index 00000000000..d6f1b06b228 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" + }, + "step": { + "user": { + "description": "Rhasspy\u306e\u30b5\u30dd\u30fc\u30c8\u3092\u6709\u52b9\u306b\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/pt.json b/homeassistant/components/rhasspy/translations/pt.json new file mode 100644 index 00000000000..25538aa0036 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/zh-Hant.json b/homeassistant/components/rhasspy/translations/zh-Hant.json new file mode 100644 index 00000000000..ce30b5189a6 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/zh-Hant.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u958b\u555f Rhasspy \u652f\u63f4\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ridwell/translations/pt.json b/homeassistant/components/ridwell/translations/pt.json new file mode 100644 index 00000000000..3d5061419fc --- /dev/null +++ b/homeassistant/components/ridwell/translations/pt.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + }, + "title": "Reautenticar integra\u00e7\u00e3o" + }, + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/pt.json b/homeassistant/components/roomba/translations/pt.json index 3afb1cc22e3..5e40221cec6 100644 --- a/homeassistant/components/roomba/translations/pt.json +++ b/homeassistant/components/roomba/translations/pt.json @@ -4,13 +4,18 @@ "not_irobot_device": "O dispositivo descoberto n\u00e3o \u00e9 um dispositivo iRobot" }, "error": { - "cannot_connect": "Falha ao conectar, tente novamente" + "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "flow_title": "iRobot {name} ({host})", "step": { "link": { "title": "Recuperar Palavra-passe" }, + "link_manual": { + "data": { + "password": "Palavra-passe" + } + }, "manual": { "data": { "host": "Anfitri\u00e3o" diff --git a/homeassistant/components/roon/translations/pt.json b/homeassistant/components/roon/translations/pt.json index 1e12fdcfcba..105f0b64676 100644 --- a/homeassistant/components/roon/translations/pt.json +++ b/homeassistant/components/roon/translations/pt.json @@ -6,6 +6,13 @@ "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" + }, + "step": { + "fallback": { + "data": { + "host": "Servidor" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/ja.json b/homeassistant/components/rpi_power/translations/ja.json index 26aee66af5b..2c2dff7e51e 100644 --- a/homeassistant/components/rpi_power/translations/ja.json +++ b/homeassistant/components/rpi_power/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u3053\u306e\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306b\u5fc5\u8981\u306a\u30b7\u30b9\u30c6\u30e0\u30af\u30e9\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002\u30ab\u30fc\u30cd\u30eb\u304c\u6700\u65b0\u3067\u3001\u30cf\u30fc\u30c9\u30a6\u30a7\u30a2\u304c\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/rtsp_to_webrtc/translations/ja.json b/homeassistant/components/rtsp_to_webrtc/translations/ja.json index c904164a599..2f6c9eb0f80 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/ja.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "server_failure": "RTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u304c\u30a8\u30e9\u30fc\u3092\u8fd4\u3057\u307e\u3057\u305f\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "server_unreachable": "RTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u3068\u306e\u901a\u4fe1\u304c\u3067\u304d\u307e\u305b\u3093\u3002\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "invalid_url": "\u6709\u52b9\u306aRTSPtoWebRTC\u30b5\u30fc\u30d0\u30fc\u306eURL\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u4f8b: https://example.com", diff --git a/homeassistant/components/rtsp_to_webrtc/translations/pt.json b/homeassistant/components/rtsp_to_webrtc/translations/pt.json new file mode 100644 index 00000000000..25538aa0036 --- /dev/null +++ b/homeassistant/components/rtsp_to_webrtc/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/pt.json b/homeassistant/components/sabnzbd/translations/pt.json new file mode 100644 index 00000000000..20bba0ede4b --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/pt.json b/homeassistant/components/samsungtv/translations/pt.json index b2cd242c7da..c0c3fb735c1 100644 --- a/homeassistant/components/samsungtv/translations/pt.json +++ b/homeassistant/components/samsungtv/translations/pt.json @@ -5,7 +5,7 @@ "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "cannot_connect": "Falha na liga\u00e7\u00e3o" }, - "flow_title": "TV Samsung: {model}", + "flow_title": "", "step": { "user": { "data": { diff --git a/homeassistant/components/scrape/translations/pt.json b/homeassistant/components/scrape/translations/pt.json new file mode 100644 index 00000000000..00c65c09cdf --- /dev/null +++ b/homeassistant/components/scrape/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "step": { + "user": { + "data": { + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/pt.json b/homeassistant/components/screenlogic/translations/pt.json new file mode 100644 index 00000000000..218e55c6d5f --- /dev/null +++ b/homeassistant/components/screenlogic/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "gateway_entry": { + "data": { + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/pt.json b/homeassistant/components/sense/translations/pt.json index e3b78cd8e42..d429d0af1e8 100644 --- a/homeassistant/components/sense/translations/pt.json +++ b/homeassistant/components/sense/translations/pt.json @@ -9,6 +9,9 @@ "unknown": "Erro inesperado" }, "step": { + "reauth_validate": { + "title": "Reautenticar integra\u00e7\u00e3o" + }, "user": { "data": { "email": "Email", diff --git a/homeassistant/components/senseme/translations/pt.json b/homeassistant/components/senseme/translations/pt.json new file mode 100644 index 00000000000..4c3266a6022 --- /dev/null +++ b/homeassistant/components/senseme/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido." + }, + "step": { + "manual": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/pt.json b/homeassistant/components/sensibo/translations/pt.json new file mode 100644 index 00000000000..80f65d0a06d --- /dev/null +++ b/homeassistant/components/sensibo/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + } + }, + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/ja.json b/homeassistant/components/sentry/translations/ja.json index 8ac8ebf58a1..56312cecda3 100644 --- a/homeassistant/components/sentry/translations/ja.json +++ b/homeassistant/components/sentry/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "bad_dsn": "\u7121\u52b9\u306aDSN", diff --git a/homeassistant/components/shopping_list/translations/pt.json b/homeassistant/components/shopping_list/translations/pt.json index 9e8b24efa29..bdb2d4041ef 100644 --- a/homeassistant/components/shopping_list/translations/pt.json +++ b/homeassistant/components/shopping_list/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "A lista de compras j\u00e1 est\u00e1 configurada." + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" }, "step": { "user": { diff --git a/homeassistant/components/sia/translations/pt.json b/homeassistant/components/sia/translations/pt.json new file mode 100644 index 00000000000..0077ceddd46 --- /dev/null +++ b/homeassistant/components/sia/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/pt.json b/homeassistant/components/simplepush/translations/pt.json new file mode 100644 index 00000000000..d7e598b33e4 --- /dev/null +++ b/homeassistant/components/simplepush/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/pt.json b/homeassistant/components/skybell/translations/pt.json new file mode 100644 index 00000000000..8487c8869b6 --- /dev/null +++ b/homeassistant/components/skybell/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/pt.json b/homeassistant/components/slack/translations/pt.json new file mode 100644 index 00000000000..cb9139874f2 --- /dev/null +++ b/homeassistant/components/slack/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/translations/pt.json b/homeassistant/components/sleepiq/translations/pt.json new file mode 100644 index 00000000000..5602f898fc3 --- /dev/null +++ b/homeassistant/components/sleepiq/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/ja.json b/homeassistant/components/slimproto/translations/ja.json index 4b8d0691b68..cf3ac93acad 100644 --- a/homeassistant/components/slimproto/translations/ja.json +++ b/homeassistant/components/slimproto/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" } } } \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/pt.json b/homeassistant/components/slimproto/translations/pt.json new file mode 100644 index 00000000000..25538aa0036 --- /dev/null +++ b/homeassistant/components/slimproto/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/pt.json b/homeassistant/components/smartthings/translations/pt.json index 9f2ed5a4b90..a6297502b0e 100644 --- a/homeassistant/components/smartthings/translations/pt.json +++ b/homeassistant/components/smartthings/translations/pt.json @@ -28,7 +28,7 @@ "title": "Selecionar Localiza\u00e7\u00e3o" }, "user": { - "description": "Por favor, insira um SmartThings [Personal Access Token]({token_url} ) que foi criado de acordo com as [instru\u00e7\u00f5es]({component_url}).", + "description": "SmartThings will be configured to send push updates to Home Assistant at:\n> {webhook_url}\n\nIf this is not correct, please update your configuration, restart Home Assistant, and try again.", "title": "Insira o Token de acesso pessoal" } } diff --git a/homeassistant/components/smarttub/translations/pt.json b/homeassistant/components/smarttub/translations/pt.json index 5d4e3e1faed..c6325f3dba3 100644 --- a/homeassistant/components/smarttub/translations/pt.json +++ b/homeassistant/components/smarttub/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_configured": "Conta j\u00e1 configurada", "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { diff --git a/homeassistant/components/smhi/translations/pt.json b/homeassistant/components/smhi/translations/pt.json index d5cd5e83a13..acdfb6606bd 100644 --- a/homeassistant/components/smhi/translations/pt.json +++ b/homeassistant/components/smhi/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, "error": { "wrong_location": "Localiza\u00e7\u00e3o apenas na Su\u00e9cia" }, diff --git a/homeassistant/components/sms/translations/ja.json b/homeassistant/components/sms/translations/ja.json index ddfb644d90f..88780e7aef0 100644 --- a/homeassistant/components/sms/translations/ja.json +++ b/homeassistant/components/sms/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/solax/translations/pt.json b/homeassistant/components/solax/translations/pt.json new file mode 100644 index 00000000000..916bd3daced --- /dev/null +++ b/homeassistant/components/solax/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/translations/ja.json b/homeassistant/components/soma/translations/ja.json index 026499458d8..d3415c1d197 100644 --- a/homeassistant/components/soma/translations/ja.json +++ b/homeassistant/components/soma/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "already_setup": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "connection_error": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "missing_configuration": "SOMA Connect\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", diff --git a/homeassistant/components/soma/translations/pt.json b/homeassistant/components/soma/translations/pt.json index f681da4210f..9199c825f4a 100644 --- a/homeassistant/components/soma/translations/pt.json +++ b/homeassistant/components/soma/translations/pt.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_setup": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o" + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/somfy_mylink/translations/pt.json b/homeassistant/components/somfy_mylink/translations/pt.json new file mode 100644 index 00000000000..fe98366e28e --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "flow_title": "" + }, + "options": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/translations/ja.json b/homeassistant/components/sonos/translations/ja.json index 7aa5823c7c6..d4b696cbb48 100644 --- a/homeassistant/components/sonos/translations/ja.json +++ b/homeassistant/components/sonos/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "not_sonos_device": "\u691c\u51fa\u3055\u308c\u305f\u30c7\u30d0\u30a4\u30b9\u306f\u3001Sonos\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/sonos/translations/pt.json b/homeassistant/components/sonos/translations/pt.json index 51fbd16a20d..5b0b34a8eb7 100644 --- a/homeassistant/components/sonos/translations/pt.json +++ b/homeassistant/components/sonos/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Nenhum dispositivo Sonos encontrado na rede.", + "no_devices_found": "Nenhum dispositivo encontrado na rede", "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do Sonos \u00e9 necess\u00e1ria." }, "step": { diff --git a/homeassistant/components/soundtouch/translations/pt.json b/homeassistant/components/soundtouch/translations/pt.json new file mode 100644 index 00000000000..91786f4b324 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/ja.json b/homeassistant/components/speedtestdotnet/translations/ja.json index 5712139b6b9..40f592b2c46 100644 --- a/homeassistant/components/speedtestdotnet/translations/ja.json +++ b/homeassistant/components/speedtestdotnet/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/spider/translations/ja.json b/homeassistant/components/spider/translations/ja.json index 9277adceeee..f45ec1ebb98 100644 --- a/homeassistant/components/spider/translations/ja.json +++ b/homeassistant/components/spider/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", diff --git a/homeassistant/components/srp_energy/translations/ja.json b/homeassistant/components/srp_energy/translations/ja.json index 805a500502b..2e33e78e8d8 100644 --- a/homeassistant/components/srp_energy/translations/ja.json +++ b/homeassistant/components/srp_energy/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/steam_online/translations/pt.json b/homeassistant/components/steam_online/translations/pt.json new file mode 100644 index 00000000000..18ac9fb4b40 --- /dev/null +++ b/homeassistant/components/steam_online/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steamist/translations/pt.json b/homeassistant/components/steamist/translations/pt.json new file mode 100644 index 00000000000..6ead5b65917 --- /dev/null +++ b/homeassistant/components/steamist/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sun/translations/ja.json b/homeassistant/components/sun/translations/ja.json index 8188e950389..ecb23ee53d6 100644 --- a/homeassistant/components/sun/translations/ja.json +++ b/homeassistant/components/sun/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/sun/translations/pt.json b/homeassistant/components/sun/translations/pt.json index 2f060112a0c..d050eb7c5f5 100644 --- a/homeassistant/components/sun/translations/pt.json +++ b/homeassistant/components/sun/translations/pt.json @@ -1,4 +1,11 @@ { + "config": { + "step": { + "user": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + }, "state": { "_": { "above_horizon": "Acima do horizonte", diff --git a/homeassistant/components/surepetcare/translations/pt.json b/homeassistant/components/surepetcare/translations/pt.json new file mode 100644 index 00000000000..0e89c76d047 --- /dev/null +++ b/homeassistant/components/surepetcare/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/pt.json b/homeassistant/components/switchbot/translations/pt.json new file mode 100644 index 00000000000..b8a454fbaba --- /dev/null +++ b/homeassistant/components/switchbot/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switcher_kis/translations/ja.json b/homeassistant/components/switcher_kis/translations/ja.json index d1234b69652..981d3c1f285 100644 --- a/homeassistant/components/switcher_kis/translations/ja.json +++ b/homeassistant/components/switcher_kis/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/syncthing/translations/pt.json b/homeassistant/components/syncthing/translations/pt.json new file mode 100644 index 00000000000..51baddfeab3 --- /dev/null +++ b/homeassistant/components/syncthing/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/pt.json b/homeassistant/components/synology_dsm/translations/pt.json index 9745f897e05..66df18026ea 100644 --- a/homeassistant/components/synology_dsm/translations/pt.json +++ b/homeassistant/components/synology_dsm/translations/pt.json @@ -23,6 +23,12 @@ "verify_ssl": "Verificar o certificado SSL" } }, + "reauth_confirm": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + }, "user": { "data": { "host": "Servidor", diff --git a/homeassistant/components/system_bridge/translations/pt.json b/homeassistant/components/system_bridge/translations/pt.json new file mode 100644 index 00000000000..8f319572c97 --- /dev/null +++ b/homeassistant/components/system_bridge/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/pt.json b/homeassistant/components/tankerkoenig/translations/pt.json index 19646bbce9c..7af02efc468 100644 --- a/homeassistant/components/tankerkoenig/translations/pt.json +++ b/homeassistant/components/tankerkoenig/translations/pt.json @@ -1,6 +1,14 @@ { "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + } + }, "user": { "data": { "name": "Nome da regi\u00e3o" diff --git a/homeassistant/components/tasmota/translations/ja.json b/homeassistant/components/tasmota/translations/ja.json index 8aad41de32f..34c56d4d413 100644 --- a/homeassistant/components/tasmota/translations/ja.json +++ b/homeassistant/components/tasmota/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "invalid_discovery_topic": "(\u4e0d\u6b63\u306a)Invalid discovery topic prefix." diff --git a/homeassistant/components/tautulli/translations/ja.json b/homeassistant/components/tautulli/translations/ja.json index 6f733e1cad4..2407bb4b984 100644 --- a/homeassistant/components/tautulli/translations/ja.json +++ b/homeassistant/components/tautulli/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/tautulli/translations/pt.json b/homeassistant/components/tautulli/translations/pt.json new file mode 100644 index 00000000000..43d522f0ab1 --- /dev/null +++ b/homeassistant/components/tautulli/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + } + }, + "user": { + "data": { + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/pt.json b/homeassistant/components/tellduslive/translations/pt.json index 1f06d33d356..e8fda8a7647 100644 --- a/homeassistant/components/tellduslive/translations/pt.json +++ b/homeassistant/components/tellduslive/translations/pt.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.", - "unknown": "Ocorreu um erro desconhecido", + "unknown": "Erro inesperado", "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." }, "error": { diff --git a/homeassistant/components/tesla_wall_connector/translations/pt.json b/homeassistant/components/tesla_wall_connector/translations/pt.json new file mode 100644 index 00000000000..8578f969852 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tile/translations/pt.json b/homeassistant/components/tile/translations/pt.json index bfafaa77b42..e883bd875e1 100644 --- a/homeassistant/components/tile/translations/pt.json +++ b/homeassistant/components/tile/translations/pt.json @@ -7,10 +7,15 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + }, "user": { "data": { "password": "Palavra-passe", - "username": "E-mail" + "username": "Email" }, "title": "Configurar Tile" } diff --git a/homeassistant/components/tomorrowio/translations/pt.json b/homeassistant/components/tomorrowio/translations/pt.json new file mode 100644 index 00000000000..1e8c651e5e8 --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "location": "Localiza\u00e7\u00e3o", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/pt.json b/homeassistant/components/tplink/translations/pt.json index 90afdfcf10a..d4507db3a55 100644 --- a/homeassistant/components/tplink/translations/pt.json +++ b/homeassistant/components/tplink/translations/pt.json @@ -2,6 +2,9 @@ "config": { "abort": { "no_devices_found": "Nenhum dispositivo TP-Link encontrado na rede." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" } } } \ No newline at end of file diff --git a/homeassistant/components/traccar/translations/ja.json b/homeassistant/components/traccar/translations/ja.json index c635e23fbe0..73c7de74ced 100644 --- a/homeassistant/components/traccar/translations/ja.json +++ b/homeassistant/components/traccar/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/trafikverket_ferry/translations/pt.json b/homeassistant/components/trafikverket_ferry/translations/pt.json new file mode 100644 index 00000000000..8cd0d6c0842 --- /dev/null +++ b/homeassistant/components/trafikverket_ferry/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_weatherstation/translations/pt.json b/homeassistant/components/trafikverket_weatherstation/translations/pt.json new file mode 100644 index 00000000000..8cd0d6c0842 --- /dev/null +++ b/homeassistant/components/trafikverket_weatherstation/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/pt.json b/homeassistant/components/transmission/translations/pt.json index c3d4131d995..399596eff8b 100644 --- a/homeassistant/components/transmission/translations/pt.json +++ b/homeassistant/components/transmission/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", @@ -9,6 +10,12 @@ "name_exists": "Nome j\u00e1 existe" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + }, + "title": "Reautenticar integra\u00e7\u00e3o" + }, "user": { "data": { "host": "Servidor", diff --git a/homeassistant/components/tuya/translations/select.pt.json b/homeassistant/components/tuya/translations/select.pt.json index 77604504f4d..fac54570cd6 100644 --- a/homeassistant/components/tuya/translations/select.pt.json +++ b/homeassistant/components/tuya/translations/select.pt.json @@ -1,10 +1,18 @@ { "state": { + "tuya__basic_nightvision": { + "1": "Desligado" + }, + "tuya__light_mode": { + "none": "Desligado" + }, "tuya__record_mode": { "2": "Grava\u00e7\u00e3o cont\u00ednua" }, "tuya__relay_status": { - "on": "Ligado" + "off": "Desligado", + "on": "Ligado", + "power_off": "Desligado" }, "tuya__vacuum_mode": { "pick_zone": "Escolha a Zona", diff --git a/homeassistant/components/twilio/translations/ja.json b/homeassistant/components/twilio/translations/ja.json index 521fee184f2..4ebff4fde6e 100644 --- a/homeassistant/components/twilio/translations/ja.json +++ b/homeassistant/components/twilio/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "cloud_not_connected": "Home Assistant Cloud\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "webhook_not_internet_accessible": "Webhook\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u53d7\u4fe1\u3059\u308b\u306b\u306f\u3001Home Assistant\u306e\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u3001\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u304b\u3089\u30a2\u30af\u30bb\u30b9\u3067\u304d\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002" }, "create_entry": { diff --git a/homeassistant/components/unifi/translations/pt.json b/homeassistant/components/unifi/translations/pt.json index 7a0a8e1a1fd..b62d5f4104e 100644 --- a/homeassistant/components/unifi/translations/pt.json +++ b/homeassistant/components/unifi/translations/pt.json @@ -15,7 +15,7 @@ "port": "Porto", "site": "Site ID", "username": "Nome do utilizador", - "verify_ssl": "Controlador com certificados adequados" + "verify_ssl": "Verificar o certificado SSL" }, "title": "Configurar o controlador UniFi" } diff --git a/homeassistant/components/unifiprotect/translations/pt.json b/homeassistant/components/unifiprotect/translations/pt.json new file mode 100644 index 00000000000..253a0f414df --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/pt.json @@ -0,0 +1,25 @@ +{ + "config": { + "step": { + "discovery_confirm": { + "data": { + "username": "Nome de Utilizador" + }, + "description": "Do you want to setup {name} ({ip_address})? " + }, + "reauth_confirm": { + "data": { + "port": "Porta" + } + }, + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe", + "port": "Porta", + "verify_ssl": "Verificar o certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/pt.json b/homeassistant/components/upnp/translations/pt.json index 022d1c823c1..6fa823925e1 100644 --- a/homeassistant/components/upnp/translations/pt.json +++ b/homeassistant/components/upnp/translations/pt.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "UPnP/IGD j\u00e1 est\u00e1 configurado", - "no_devices_found": "Nenhum dispositivo UPnP / IGD encontrado na rede." + "no_devices_found": "Nenhum dispositivo encontrado na rede" }, "error": { "one": "um", diff --git a/homeassistant/components/uptime/translations/ja.json b/homeassistant/components/uptime/translations/ja.json index 99dc644a0e5..8614c751069 100644 --- a/homeassistant/components/uptime/translations/ja.json +++ b/homeassistant/components/uptime/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "user": { diff --git a/homeassistant/components/uptimerobot/translations/pt.json b/homeassistant/components/uptimerobot/translations/pt.json index 10c16aafa0f..826cdc5aa59 100644 --- a/homeassistant/components/uptimerobot/translations/pt.json +++ b/homeassistant/components/uptimerobot/translations/pt.json @@ -4,7 +4,20 @@ "unknown": "Erro inesperado" }, "error": { - "invalid_api_key": "Chave de API inv\u00e1lida" + "invalid_api_key": "Chave de API inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + } + }, + "user": { + "data": { + "api_key": "Chave da API" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/vallox/translations/pt.json b/homeassistant/components/vallox/translations/pt.json index 0c5c7760566..f5c3b1c7b61 100644 --- a/homeassistant/components/vallox/translations/pt.json +++ b/homeassistant/components/vallox/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido." + }, "error": { "unknown": "Erro inesperado" } diff --git a/homeassistant/components/venstar/translations/pt.json b/homeassistant/components/venstar/translations/pt.json new file mode 100644 index 00000000000..04374af8e82 --- /dev/null +++ b/homeassistant/components/venstar/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/translations/ja.json b/homeassistant/components/vesync/translations/ja.json index 66d7cf26ca9..c6717072946 100644 --- a/homeassistant/components/vesync/translations/ja.json +++ b/homeassistant/components/vesync/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" diff --git a/homeassistant/components/vicare/translations/ja.json b/homeassistant/components/vicare/translations/ja.json index 10ffe077aca..8d50ef180d1 100644 --- a/homeassistant/components/vicare/translations/ja.json +++ b/homeassistant/components/vicare/translations/ja.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "error": { diff --git a/homeassistant/components/vicare/translations/pt.json b/homeassistant/components/vicare/translations/pt.json new file mode 100644 index 00000000000..6a34ef9530f --- /dev/null +++ b/homeassistant/components/vicare/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "client_id": "Chave da API", + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/pt.json b/homeassistant/components/vlc_telnet/translations/pt.json new file mode 100644 index 00000000000..55ccd56b497 --- /dev/null +++ b/homeassistant/components/vlc_telnet/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/pt.json b/homeassistant/components/wallbox/translations/pt.json new file mode 100644 index 00000000000..ce8a9287272 --- /dev/null +++ b/homeassistant/components/wallbox/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/pt.json b/homeassistant/components/watttime/translations/pt.json index d64652ed815..859d8de1627 100644 --- a/homeassistant/components/watttime/translations/pt.json +++ b/homeassistant/components/watttime/translations/pt.json @@ -1,9 +1,12 @@ { "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { - "username": "Nome de utilizador" + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/waze_travel_time/translations/pt.json b/homeassistant/components/waze_travel_time/translations/pt.json new file mode 100644 index 00000000000..3d986f4533d --- /dev/null +++ b/homeassistant/components/waze_travel_time/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/weather/translations/pt.json b/homeassistant/components/weather/translations/pt.json index 5875b8a7192..13c5273632c 100644 --- a/homeassistant/components/weather/translations/pt.json +++ b/homeassistant/components/weather/translations/pt.json @@ -1,13 +1,13 @@ { "state": { "_": { - "clear-night": "Limpo, Noite", + "clear-night": "C\u00e9u limpo, Noite", "cloudy": "Nublado", "exceptional": "Excecional", "fog": "Nevoeiro", "hail": "Granizo", "lightning": "Rel\u00e2mpago", - "lightning-rainy": "Rel\u00e2mpagos, chuva", + "lightning-rainy": "Trovoada, chuva", "partlycloudy": "Parcialmente nublado", "pouring": "Chuva forte", "rainy": "Chuva", diff --git a/homeassistant/components/webostv/translations/pt.json b/homeassistant/components/webostv/translations/pt.json new file mode 100644 index 00000000000..ce7cbc3f548 --- /dev/null +++ b/homeassistant/components/webostv/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/ja.json b/homeassistant/components/wemo/translations/ja.json index f86e1e80520..7ab4a0bfc1b 100644 --- a/homeassistant/components/wemo/translations/ja.json +++ b/homeassistant/components/wemo/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/whirlpool/translations/pt.json b/homeassistant/components/whirlpool/translations/pt.json new file mode 100644 index 00000000000..ce1bf4bb4b8 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whois/translations/pt.json b/homeassistant/components/whois/translations/pt.json new file mode 100644 index 00000000000..d252c078a2c --- /dev/null +++ b/homeassistant/components/whois/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/el.json b/homeassistant/components/withings/translations/el.json index dc284985ee2..068d347467a 100644 --- a/homeassistant/components/withings/translations/el.json +++ b/homeassistant/components/withings/translations/el.json @@ -27,6 +27,10 @@ "reauth": { "description": "\u03a4\u03bf \u03c0\u03c1\u03bf\u03c6\u03af\u03bb \"{profile}\" \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03b9 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 Withings.", "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + }, + "reauth_confirm": { + "description": "\u03a4\u03bf \u03c0\u03c1\u03bf\u03c6\u03af\u03bb \"{profile}\" \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03b9 \u03bd\u03b1 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 Withings.", + "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" } } } diff --git a/homeassistant/components/withings/translations/ja.json b/homeassistant/components/withings/translations/ja.json index 20f097e973c..3fdcffdb918 100644 --- a/homeassistant/components/withings/translations/ja.json +++ b/homeassistant/components/withings/translations/ja.json @@ -27,6 +27,10 @@ "reauth": { "description": "Withings data\u306e\u53d7\u4fe1\u3092\u7d99\u7d9a\u3059\u308b\u306b\u306f\u3001\"{profile}\" \u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" + }, + "reauth_confirm": { + "description": "Withings data\u306e\u53d7\u4fe1\u3092\u7d99\u7d9a\u3059\u308b\u306b\u306f\u3001\"{profile}\" \u306e\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u3092\u518d\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } } diff --git a/homeassistant/components/withings/translations/pt.json b/homeassistant/components/withings/translations/pt.json index 1fe7083ecfd..673627c8036 100644 --- a/homeassistant/components/withings/translations/pt.json +++ b/homeassistant/components/withings/translations/pt.json @@ -19,6 +19,9 @@ }, "reauth": { "title": "Re-autenticar Perfil" + }, + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" } } } diff --git a/homeassistant/components/wiz/translations/pt.json b/homeassistant/components/wiz/translations/pt.json new file mode 100644 index 00000000000..b686fee56b5 --- /dev/null +++ b/homeassistant/components/wiz/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/select.pt.json b/homeassistant/components/wled/translations/select.pt.json new file mode 100644 index 00000000000..d33123843d4 --- /dev/null +++ b/homeassistant/components/wled/translations/select.pt.json @@ -0,0 +1,8 @@ +{ + "state": { + "wled__live_override": { + "0": "Desligado", + "1": "Ligado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ws66i/translations/pt.json b/homeassistant/components/ws66i/translations/pt.json new file mode 100644 index 00000000000..3b5850222d9 --- /dev/null +++ b/homeassistant/components/ws66i/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/ja.json b/homeassistant/components/xbox/translations/ja.json index 7a9337c9332..2d1e95019b7 100644 --- a/homeassistant/components/xbox/translations/ja.json +++ b/homeassistant/components/xbox/translations/ja.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "create_entry": { "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" diff --git a/homeassistant/components/xiaomi_miio/translations/pt.json b/homeassistant/components/xiaomi_miio/translations/pt.json index db0e0c2a137..41cb2e55e91 100644 --- a/homeassistant/components/xiaomi_miio/translations/pt.json +++ b/homeassistant/components/xiaomi_miio/translations/pt.json @@ -5,6 +5,14 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "manual": { + "data": { + "host": "Endere\u00e7o IP", + "token": "API Token" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/pt.json b/homeassistant/components/yale_smart_alarm/translations/pt.json new file mode 100644 index 00000000000..0b1a6c82cf3 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "name": "Nome", + "username": "Nome de Utilizador" + } + }, + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/pt.json b/homeassistant/components/yolink/translations/pt.json new file mode 100644 index 00000000000..1ffa7cb5245 --- /dev/null +++ b/homeassistant/components/yolink/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/youless/translations/pt.json b/homeassistant/components/youless/translations/pt.json new file mode 100644 index 00000000000..3b5850222d9 --- /dev/null +++ b/homeassistant/components/youless/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/ja.json b/homeassistant/components/zerproc/translations/ja.json index d1234b69652..981d3c1f285 100644 --- a/homeassistant/components/zerproc/translations/ja.json +++ b/homeassistant/components/zerproc/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 636df6f047e..9e8289960f8 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -2,7 +2,7 @@ "config": { "abort": { "not_zha_device": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306fzha\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002", "usb_probe_failed": "USB\u30c7\u30d0\u30a4\u30b9\u3092\u63a2\u3057\u51fa\u3059\u3053\u3068\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index f9d67cc2d35..be7fb30dbec 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -40,14 +40,14 @@ }, "config_panel": { "zha_alarm_options": { - "alarm_arm_requires_code": "\u041a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443", + "alarm_arm_requires_code": "\u0422\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c \u043a\u043e\u0434 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443", "alarm_failed_tries": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043d\u0435\u0443\u0434\u0430\u0447\u043d\u044b\u0445 \u0432\u0432\u043e\u0434\u043e\u0432 \u043a\u043e\u0434\u0430, \u0434\u043b\u044f \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u043d\u0438\u044f \u0442\u0440\u0435\u0432\u043e\u0433\u0438", "alarm_master_code": "\u041c\u0430\u0441\u0442\u0435\u0440-\u043a\u043e\u0434 \u0434\u043b\u044f \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044c\u043d\u044b\u0445 \u043f\u0430\u043d\u0435\u043b\u0435\u0439", - "title": "\u041e\u043f\u0446\u0438\u0438 \u043f\u0430\u043d\u0435\u043b\u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439" + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0430\u043d\u0435\u043b\u0435\u0439 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439" }, "zha_options": { - "consider_unavailable_battery": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0430\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u044b\u043c \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0441\u0435\u043a\u0443\u043d\u0434)", - "consider_unavailable_mains": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043e\u0442 \u0441\u0435\u0442\u0438 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0441\u0435\u043a\u0443\u043d\u0434)", + "consider_unavailable_battery": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0430\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u044b\u043c \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "consider_unavailable_mains": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043e\u0442 \u0441\u0435\u0442\u0438 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "default_light_transition": "\u0412\u0440\u0435\u043c\u044f \u043f\u043b\u0430\u0432\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u0441\u0432\u0435\u0442\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "enable_identify_on_join": "\u042d\u0444\u0444\u0435\u043a\u0442 \u0434\u043b\u044f \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043a \u0441\u0435\u0442\u0438", "title": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" diff --git a/homeassistant/components/zwave_js/translations/pt.json b/homeassistant/components/zwave_js/translations/pt.json new file mode 100644 index 00000000000..086d4c19ebb --- /dev/null +++ b/homeassistant/components/zwave_js/translations/pt.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + }, + "options": { + "error": { + "unknown": "Erro inesperado" + }, + "step": { + "manual": { + "data": { + "url": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_me/translations/pt.json b/homeassistant/components/zwave_me/translations/pt.json new file mode 100644 index 00000000000..a142354a462 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "token": "API Token", + "url": "" + } + } + } + } +} \ No newline at end of file From 3ca04aa33b80759acd9f2a660b1035adab184eb3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Jul 2022 08:40:43 +0200 Subject: [PATCH 2504/3516] Bump actions/cache from 3.0.4 to 3.0.5 (#75104) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a5f3da97ac4..573445a4d2e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -172,7 +172,7 @@ jobs: cache: "pip" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -185,7 +185,7 @@ jobs: pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -211,7 +211,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -222,7 +222,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -260,7 +260,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -271,7 +271,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -312,7 +312,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -323,7 +323,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -353,7 +353,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -364,7 +364,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -480,7 +480,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: >- @@ -488,7 +488,7 @@ jobs: needs.info.outputs.python_cache_key }} - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: ${{ env.PIP_CACHE }} key: >- @@ -538,7 +538,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: >- @@ -570,7 +570,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: >- @@ -603,7 +603,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: >- @@ -647,7 +647,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: >- @@ -695,7 +695,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: >- @@ -749,7 +749,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.4 + uses: actions/cache@v3.0.5 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ From ad82352daeb280c14738627bf577195cd8dfcf71 Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 13 Jul 2022 02:46:32 -0400 Subject: [PATCH 2505/3516] Bump AIOAladdinConnect to 0.1.23 (#75065) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 3a9e295a08f..bdf906ad200 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.21"], + "requirements": ["AIOAladdinConnect==0.1.23"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 36b3ba5cb62..ca2a76ec5f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.21 +AIOAladdinConnect==0.1.23 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e4e8b4262f6..cfda178b48a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.21 +AIOAladdinConnect==0.1.23 # homeassistant.components.adax Adax-local==0.1.4 From 1e5ada0f9db8a5e95c6cd2a4229c7f9be59716be Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Wed, 13 Jul 2022 02:56:08 -0400 Subject: [PATCH 2506/3516] Fix Insteon thermostat issues (#75079) --- homeassistant/components/insteon/climate.py | 2 +- homeassistant/components/insteon/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/insteon/climate.py b/homeassistant/components/insteon/climate.py index 97e21a02f6f..29c127ba0c7 100644 --- a/homeassistant/components/insteon/climate.py +++ b/homeassistant/components/insteon/climate.py @@ -86,7 +86,7 @@ class InsteonClimateEntity(InsteonEntity, ClimateEntity): @property def temperature_unit(self) -> str: """Return the unit of measurement.""" - if self._insteon_device.properties[CELSIUS].value: + if self._insteon_device.configuration[CELSIUS].value: return TEMP_CELSIUS return TEMP_FAHRENHEIT diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 1be077a6b38..c48d502c16e 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -4,8 +4,8 @@ "documentation": "https://www.home-assistant.io/integrations/insteon", "dependencies": ["http", "websocket_api"], "requirements": [ - "pyinsteon==1.1.1", - "insteon-frontend-home-assistant==0.1.1" + "pyinsteon==1.1.3", + "insteon-frontend-home-assistant==0.2.0" ], "codeowners": ["@teharris1"], "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index ca2a76ec5f1..d84fa688c87 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -894,7 +894,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.insteon -insteon-frontend-home-assistant==0.1.1 +insteon-frontend-home-assistant==0.2.0 # homeassistant.components.intellifire intellifire4py==2.0.1 @@ -1562,7 +1562,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.1 +pyinsteon==1.1.3 # homeassistant.components.intesishome pyintesishome==1.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfda178b48a..57b73ced72e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -640,7 +640,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.insteon -insteon-frontend-home-assistant==0.1.1 +insteon-frontend-home-assistant==0.2.0 # homeassistant.components.intellifire intellifire4py==2.0.1 @@ -1059,7 +1059,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.1 +pyinsteon==1.1.3 # homeassistant.components.ipma pyipma==2.0.5 From b7a6f4e22036dfdde69fede8840ecc41d9398c4d Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Wed, 13 Jul 2022 10:12:50 +0300 Subject: [PATCH 2507/3516] Fix missing ordered states in universal media player (#75099) --- homeassistant/components/universal/media_player.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index f33db3827af..352703f8f9b 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -69,11 +69,13 @@ from homeassistant.const import ( SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, + STATE_BUFFERING, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING, + STATE_STANDBY, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -101,8 +103,10 @@ STATES_ORDER = [ STATE_UNAVAILABLE, STATE_OFF, STATE_IDLE, + STATE_STANDBY, STATE_ON, STATE_PAUSED, + STATE_BUFFERING, STATE_PLAYING, ] ATTRS_SCHEMA = cv.schema_with_slug_keys(cv.string) From 34f1d5e094745e0331463904b5fbc49761234dea Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Wed, 13 Jul 2022 09:21:58 +0200 Subject: [PATCH 2508/3516] Add Plugwise number platform (#74655) --- homeassistant/components/plugwise/const.py | 3 +- .../components/plugwise/manifest.json | 2 +- homeassistant/components/plugwise/number.py | 115 ++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../fixtures/anna_heatpump/all_data.json | 3 + tests/components/plugwise/test_number.py | 40 ++++++ 7 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/plugwise/number.py create mode 100644 tests/components/plugwise/test_number.py diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index 63bd2a6d8f1..d56d9c06ff5 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -25,8 +25,9 @@ UNIT_LUMEN: Final = "lm" PLATFORMS_GATEWAY: Final[list[str]] = [ Platform.BINARY_SENSOR, Platform.CLIMATE, - Platform.SENSOR, + Platform.NUMBER, Platform.SELECT, + Platform.SENSOR, Platform.SWITCH, ] ZEROCONF_MAP: Final[dict[str, str]] = { diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index a311151f645..f0e7f98d4b8 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.18.5"], + "requirements": ["plugwise==0.18.6"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py new file mode 100644 index 00000000000..6718c59c3c0 --- /dev/null +++ b/homeassistant/components/plugwise/number.py @@ -0,0 +1,115 @@ +"""Number platform for Plugwise integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from plugwise import Smile + +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import PlugwiseDataUpdateCoordinator +from .entity import PlugwiseEntity + + +@dataclass +class PlugwiseEntityDescriptionMixin: + """Mixin values for Plugwse entities.""" + + command: Callable[[Smile, float], Awaitable[None]] + + +@dataclass +class PlugwiseNumberEntityDescription( + NumberEntityDescription, PlugwiseEntityDescriptionMixin +): + """Class describing Plugwise Number entities.""" + + +NUMBER_TYPES = ( + PlugwiseNumberEntityDescription( + key="maximum_boiler_temperature", + command=lambda api, value: api.set_max_boiler_temperature(value), + device_class=NumberDeviceClass.TEMPERATURE, + name="Maximum Boiler Temperature Setpoint", + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=TEMP_CELSIUS, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Plugwise number platform.""" + + coordinator: PlugwiseDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + entities: list[PlugwiseNumberEntity] = [] + for device_id, device in coordinator.data.devices.items(): + for description in NUMBER_TYPES: + if description.key in device: + entities.append( + PlugwiseNumberEntity(coordinator, device_id, description) + ) + + async_add_entities(entities) + + +class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity): + """Representation of a Plugwise number.""" + + entity_description: PlugwiseNumberEntityDescription + + def __init__( + self, + coordinator: PlugwiseDataUpdateCoordinator, + device_id: str, + description: PlugwiseNumberEntityDescription, + ) -> None: + """Initiate Plugwise Number.""" + super().__init__(coordinator, device_id) + self.entity_description = description + self._attr_unique_id = f"{device_id}-{description.key}" + self._attr_name = (f"{self.device['name']} {description.name}").lstrip() + self._attr_mode = NumberMode.BOX + + @property + def native_step(self) -> float: + """Return the setpoint step value.""" + return max(self.device["resolution"], 1) + + @property + def native_value(self) -> float: + """Return the present setpoint value.""" + return self.device[self.entity_description.key] + + @property + def native_min_value(self) -> float: + """Return the setpoint min. value.""" + return self.device["lower_bound"] + + @property + def native_max_value(self) -> float: + """Return the setpoint max. value.""" + return self.device["upper_bound"] + + async def async_set_native_value(self, value: float) -> None: + """Change to the new setpoint value.""" + await self.entity_description.command(self.coordinator.api, value) + await self.coordinator.async_request_refresh() diff --git a/requirements_all.txt b/requirements_all.txt index d84fa688c87..5dd7d0f871f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1254,7 +1254,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.18.5 +plugwise==0.18.6 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 57b73ced72e..d3bb3335510 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -862,7 +862,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.18.5 +plugwise==0.18.6 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/tests/components/plugwise/fixtures/anna_heatpump/all_data.json b/tests/components/plugwise/fixtures/anna_heatpump/all_data.json index 60bc4c35668..6fcb841cf3e 100644 --- a/tests/components/plugwise/fixtures/anna_heatpump/all_data.json +++ b/tests/components/plugwise/fixtures/anna_heatpump/all_data.json @@ -13,6 +13,9 @@ "model": "Generic heater", "name": "OpenTherm", "vendor": "Techneco", + "lower_bound": 0.0, + "upper_bound": 100.0, + "resolution": 1.0, "maximum_boiler_temperature": 60.0, "binary_sensors": { "dhw_state": false, diff --git a/tests/components/plugwise/test_number.py b/tests/components/plugwise/test_number.py new file mode 100644 index 00000000000..a4e084e5d3a --- /dev/null +++ b/tests/components/plugwise/test_number.py @@ -0,0 +1,40 @@ +"""Tests for the Plugwise Number integration.""" + +from unittest.mock import MagicMock + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_anna_number_entities( + hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test creation of a number.""" + state = hass.states.get("number.opentherm_maximum_boiler_temperature_setpoint") + assert state + assert float(state.state) == 60.0 + + +async def test_anna_max_boiler_temp_change( + hass: HomeAssistant, mock_smile_anna: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test changing of number entities.""" + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.opentherm_maximum_boiler_temperature_setpoint", + ATTR_VALUE: 65, + }, + blocking=True, + ) + + assert mock_smile_anna.set_max_boiler_temperature.call_count == 1 + mock_smile_anna.set_max_boiler_temperature.assert_called_with(65) From 2169d839ce24c6e2b9fa215c5cabbd48b8a4022f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 13 Jul 2022 03:08:23 -0600 Subject: [PATCH 2509/3516] Remove service descriptions for deprecated Guardian services (#75084) --- .../components/guardian/services.yaml | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/homeassistant/components/guardian/services.yaml b/homeassistant/components/guardian/services.yaml index 4d48783c955..61cf709a31c 100644 --- a/homeassistant/components/guardian/services.yaml +++ b/homeassistant/components/guardian/services.yaml @@ -39,28 +39,6 @@ pair_sensor: example: 5410EC688BCF selector: text: -reboot: - name: Reboot - description: Reboot the device. - fields: - device_id: - name: Valve Controller - description: The valve controller to reboot - required: true - selector: - device: - integration: guardian -reset_valve_diagnostics: - name: Reset Valve Diagnostics - description: Fully (and irrecoverably) reset all valve diagnostics. - fields: - device_id: - name: Valve Controller - description: The valve controller whose diagnostics should be reset - required: true - selector: - device: - integration: guardian unpair_sensor: name: Unpair Sensor description: Remove a paired sensor from the valve controller. From 4a36318d56673b22f56630eb04919463444aec19 Mon Sep 17 00:00:00 2001 From: Everything Smart Home <53482654+EverythingSmartHome@users.noreply.github.com> Date: Wed, 13 Jul 2022 18:58:36 +0100 Subject: [PATCH 2510/3516] Add Aqara E1 curtain motor direction select entity to ZHA (#75132) * Add direction change select option for Aqara Curtain * Add direction change select option for Aqara Curtain --- homeassistant/components/zha/select.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 79349273e38..e2835d4acd4 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -264,3 +264,20 @@ class AqaraApproachDistance(ZCLEnumSelectEntity, id_suffix="approach_distance"): _select_attr = "approach_distance" _enum = AqaraApproachDistances + + +class AqaraE1ReverseDirection(types.enum8): + """Aqara curtain reversal.""" + + Normal = 0x00 + Inverted = 0x01 + + +@CONFIG_DIAGNOSTIC_MATCH( + channel_names="window_covering", models={"lumi.curtain.agl001"} +) +class AqaraCurtainMode(ZCLEnumSelectEntity, id_suffix="window_covering_mode"): + """Representation of a ZHA curtain mode configuration entity.""" + + _select_attr = "window_covering_mode" + _enum = AqaraE1ReverseDirection From ffeac9714f69ebeb4248fa38e6ac6c8e6cf12a5e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 13 Jul 2022 12:52:13 -0600 Subject: [PATCH 2511/3516] Ensure SimpliSafe diagnostics redact the `code` option (#75137) --- homeassistant/components/simplisafe/diagnostics.py | 3 ++- tests/components/simplisafe/conftest.py | 4 +++- tests/components/simplisafe/test_diagnostics.py | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/diagnostics.py b/homeassistant/components/simplisafe/diagnostics.py index dac89715c10..cd6e4ca52be 100644 --- a/homeassistant/components/simplisafe/diagnostics.py +++ b/homeassistant/components/simplisafe/diagnostics.py @@ -5,7 +5,7 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ADDRESS, CONF_LOCATION +from homeassistant.const import CONF_ADDRESS, CONF_CODE, CONF_LOCATION from homeassistant.core import HomeAssistant from . import SimpliSafe @@ -23,6 +23,7 @@ CONF_WIFI_SSID = "wifi_ssid" TO_REDACT = { CONF_ADDRESS, + CONF_CODE, CONF_CREDIT_CARD, CONF_EXPIRES, CONF_LOCATION, diff --git a/tests/components/simplisafe/conftest.py b/tests/components/simplisafe/conftest.py index 56967ac24c5..82bd04a7349 100644 --- a/tests/components/simplisafe/conftest.py +++ b/tests/components/simplisafe/conftest.py @@ -43,7 +43,9 @@ def api_fixture(api_auth_state, data_subscription, system_v3, websocket): @pytest.fixture(name="config_entry") def config_entry_fixture(hass, config, unique_id): """Define a config entry.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=unique_id, data=config) + entry = MockConfigEntry( + domain=DOMAIN, unique_id=unique_id, data=config, options={CONF_CODE: "1234"} + ) entry.add_to_hass(hass) return entry diff --git a/tests/components/simplisafe/test_diagnostics.py b/tests/components/simplisafe/test_diagnostics.py index 13d5c778e89..446d9d5e9e3 100644 --- a/tests/components/simplisafe/test_diagnostics.py +++ b/tests/components/simplisafe/test_diagnostics.py @@ -8,7 +8,9 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_simplisa """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { - "options": {}, + "options": { + "code": REDACTED, + }, }, "subscription_data": { "system_123": { From 755abbe2d0bf7e757eeae8c770b85afddcf83f7c Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 13 Jul 2022 22:05:43 +0200 Subject: [PATCH 2512/3516] Make sure device tuple is a list on save (#75103) --- homeassistant/components/rfxtrx/config_flow.py | 2 +- tests/components/rfxtrx/test_config_flow.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 61d01b8d533..508ca9a7037 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -199,7 +199,7 @@ class OptionsFlow(config_entries.OptionsFlow): if not errors: devices = {} device = { - CONF_DEVICE_ID: device_id, + CONF_DEVICE_ID: list(device_id), } devices[self._selected_device_event_code] = device diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index 555d780fe92..7679dbbf37e 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -867,6 +867,9 @@ async def test_options_configure_rfy_cover_device(hass): entry.data["devices"]["0C1a0000010203010000000000"]["venetian_blind_mode"] == "EU" ) + assert isinstance( + entry.data["devices"]["0C1a0000010203010000000000"]["device_id"], list + ) device_registry = dr.async_get(hass) device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) @@ -904,6 +907,9 @@ async def test_options_configure_rfy_cover_device(hass): entry.data["devices"]["0C1a0000010203010000000000"]["venetian_blind_mode"] == "EU" ) + assert isinstance( + entry.data["devices"]["0C1a0000010203010000000000"]["device_id"], list + ) def test_get_serial_by_id_no_dir(): From d1ffc7e9e312552800b2adcfffcccbda11b4d2d1 Mon Sep 17 00:00:00 2001 From: kingy444 Date: Thu, 14 Jul 2022 06:06:32 +1000 Subject: [PATCH 2513/3516] Fix Powerview top shade open position (#75110) --- .../hunterdouglas_powerview/cover.py | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 3f7d04f5b87..7c4c90a2132 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -455,14 +455,6 @@ class PowerViewShadeTDBUTop(PowerViewShadeTDBU): super().__init__(coordinator, device_info, room_name, shade, name) self._attr_unique_id = f"{self._shade.id}_top" self._attr_name = f"{self._shade_name} Top" - # these shades share a class in parent API - # override open position for top shade - self._shade.open_position = { - ATTR_POSITION1: MIN_POSITION, - ATTR_POSITION2: MAX_POSITION, - ATTR_POSKIND1: POS_KIND_PRIMARY, - ATTR_POSKIND2: POS_KIND_SECONDARY, - } @property def should_poll(self) -> bool: @@ -485,6 +477,21 @@ class PowerViewShadeTDBUTop(PowerViewShadeTDBU): # these need to be inverted to report state correctly in HA return hd_position_to_hass(self.positions.secondary, MAX_POSITION) + @property + def open_position(self) -> PowerviewShadeMove: + """Return the open position and required additional positions.""" + # these shades share a class in parent API + # override open position for top shade + return PowerviewShadeMove( + { + ATTR_POSITION1: MIN_POSITION, + ATTR_POSITION2: MAX_POSITION, + ATTR_POSKIND1: POS_KIND_PRIMARY, + ATTR_POSKIND2: POS_KIND_SECONDARY, + }, + {}, + ) + @callback def _clamp_cover_limit(self, target_hass_position: int) -> int: """Dont allow a cover to go into an impossbile position.""" From 1768315c500681f874f7d95c64cb041802769925 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 13 Jul 2022 14:12:53 -0700 Subject: [PATCH 2514/3516] Block bad pubnub version (#75138) --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c92bf170f45..72d88ab7cf5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -114,3 +114,7 @@ backoff<2.0 # Breaking change in version # https://github.com/samuelcolvin/pydantic/issues/4092 pydantic!=1.9.1 + +# Breaks asyncio +# https://github.com/pubnub/python/issues/130 +pubnub!=6.4.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 10951bcc7e8..7bdecd9e0c2 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -132,6 +132,10 @@ backoff<2.0 # Breaking change in version # https://github.com/samuelcolvin/pydantic/issues/4092 pydantic!=1.9.1 + +# Breaks asyncio +# https://github.com/pubnub/python/issues/130 +pubnub!=6.4.0 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From 3166d4895ffcbc3c26703dc65d84feec6e83f96c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 13 Jul 2022 17:13:09 -0400 Subject: [PATCH 2515/3516] Bump ZHA dependencies (#75133) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index c97cca8deb3..ce1da9071b4 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.31.0", + "bellows==0.31.1", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.77", @@ -12,7 +12,7 @@ "zigpy==0.47.2", "zigpy-xbee==0.15.0", "zigpy-zigate==0.9.0", - "zigpy-znp==0.8.0" + "zigpy-znp==0.8.1" ], "usb": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 5dd7d0f871f..380a8d24f21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -393,7 +393,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.31.0 +bellows==0.31.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.9.6 @@ -2519,7 +2519,7 @@ zigpy-xbee==0.15.0 zigpy-zigate==0.9.0 # homeassistant.components.zha -zigpy-znp==0.8.0 +zigpy-znp==0.8.1 # homeassistant.components.zha zigpy==0.47.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3bb3335510..5469672ee54 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -311,7 +311,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.31.0 +bellows==0.31.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.9.6 @@ -1683,7 +1683,7 @@ zigpy-xbee==0.15.0 zigpy-zigate==0.9.0 # homeassistant.components.zha -zigpy-znp==0.8.0 +zigpy-znp==0.8.1 # homeassistant.components.zha zigpy==0.47.2 From 8259ce986858d4135a1f024bbd63a5e1f45d6ad2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Jul 2022 10:49:06 +0200 Subject: [PATCH 2516/3516] Migrate ecobee to native_* (#74043) --- homeassistant/components/ecobee/weather.py | 43 +++++++++++----------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 1c330ceb4e2..aca4dcdf2f5 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -7,20 +7,24 @@ from pyecobee.const import ECOBEE_STATE_UNKNOWN from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRESSURE_HPA, PRESSURE_INHG, TEMP_FAHRENHEIT +from homeassistant.const import ( + LENGTH_METERS, + PRESSURE_HPA, + SPEED_METERS_PER_SECOND, + TEMP_FAHRENHEIT, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import dt as dt_util -from homeassistant.util.pressure import convert as pressure_convert from .const import ( DOMAIN, @@ -49,6 +53,11 @@ async def async_setup_entry( class EcobeeWeather(WeatherEntity): """Representation of Ecobee weather data.""" + _attr_native_pressure_unit = PRESSURE_HPA + _attr_native_temperature_unit = TEMP_FAHRENHEIT + _attr_native_visibility_unit = LENGTH_METERS + _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + def __init__(self, data, name, index): """Initialize the Ecobee weather platform.""" self.data = data @@ -101,7 +110,7 @@ class EcobeeWeather(WeatherEntity): return None @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" try: return float(self.get_forecast(0, "temperature")) / 10 @@ -109,18 +118,10 @@ class EcobeeWeather(WeatherEntity): return None @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_FAHRENHEIT - - @property - def pressure(self): + def native_pressure(self): """Return the pressure.""" try: pressure = self.get_forecast(0, "pressure") - if not self.hass.config.units.is_metric: - pressure = pressure_convert(pressure, PRESSURE_HPA, PRESSURE_INHG) - return round(pressure, 2) return round(pressure) except ValueError: return None @@ -134,15 +135,15 @@ class EcobeeWeather(WeatherEntity): return None @property - def visibility(self): + def native_visibility(self): """Return the visibility.""" try: - return int(self.get_forecast(0, "visibility")) / 1000 + return int(self.get_forecast(0, "visibility")) except ValueError: return None @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" try: return int(self.get_forecast(0, "windSpeed")) @@ -202,13 +203,13 @@ def _process_forecast(json): json["weatherSymbol"] ] if json["tempHigh"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_TEMP] = float(json["tempHigh"]) / 10 + forecast[ATTR_FORECAST_NATIVE_TEMP] = float(json["tempHigh"]) / 10 if json["tempLow"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_TEMP_LOW] = float(json["tempLow"]) / 10 + forecast[ATTR_FORECAST_NATIVE_TEMP_LOW] = float(json["tempLow"]) / 10 if json["windBearing"] != ECOBEE_STATE_UNKNOWN: forecast[ATTR_FORECAST_WIND_BEARING] = int(json["windBearing"]) if json["windSpeed"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_WIND_SPEED] = int(json["windSpeed"]) + forecast[ATTR_FORECAST_NATIVE_WIND_SPEED] = int(json["windSpeed"]) except (ValueError, IndexError, KeyError): return None From 5e7174a5f4949a8ca8fd7a76920cb56386930de3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 11 Jul 2022 10:58:57 +0200 Subject: [PATCH 2517/3516] Migrate homematicip_cloud to native_* (#74385) --- .../components/homematicip_cloud/weather.py | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 985754a8417..3acbae95441 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -22,7 +22,7 @@ from homeassistant.components.weather import ( WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -71,6 +71,9 @@ async def async_setup_entry( class HomematicipWeatherSensor(HomematicipGenericEntity, WeatherEntity): """Representation of the HomematicIP weather sensor plus & basic.""" + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the weather sensor.""" super().__init__(hap, device) @@ -81,22 +84,17 @@ class HomematicipWeatherSensor(HomematicipGenericEntity, WeatherEntity): return self._device.label @property - def temperature(self) -> float: + def native_temperature(self) -> float: """Return the platform temperature.""" return self._device.actualTemperature - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def humidity(self) -> int: """Return the humidity.""" return self._device.humidity @property - def wind_speed(self) -> float: + def native_wind_speed(self) -> float: """Return the wind speed.""" return self._device.windSpeed @@ -129,6 +127,9 @@ class HomematicipWeatherSensorPro(HomematicipWeatherSensor): class HomematicipHomeWeather(HomematicipGenericEntity, WeatherEntity): """Representation of the HomematicIP home weather.""" + _attr_native_temperature_unit = TEMP_CELSIUS + _attr_native_wind_speed_unit = SPEED_KILOMETERS_PER_HOUR + def __init__(self, hap: HomematicipHAP) -> None: """Initialize the home weather.""" hap.home.modelType = "HmIP-Home-Weather" @@ -145,22 +146,17 @@ class HomematicipHomeWeather(HomematicipGenericEntity, WeatherEntity): return f"Weather {self._home.location.city}" @property - def temperature(self) -> float: + def native_temperature(self) -> float: """Return the temperature.""" return self._device.weather.temperature - @property - def temperature_unit(self) -> str: - """Return the unit of measurement.""" - return TEMP_CELSIUS - @property def humidity(self) -> int: """Return the humidity.""" return self._device.weather.humidity @property - def wind_speed(self) -> float: + def native_wind_speed(self) -> float: """Return the wind speed.""" return round(self._device.weather.windSpeed, 1) From cc79c3d6e126d07210c95bb48aaed64be5689a98 Mon Sep 17 00:00:00 2001 From: Ludovico de Nittis Date: Mon, 11 Jul 2022 16:51:10 +0200 Subject: [PATCH 2518/3516] Update pyialarm to 2.2.0 (#74874) --- homeassistant/components/ialarm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ialarm/manifest.json b/homeassistant/components/ialarm/manifest.json index 60ecb9da74a..00a0cba9a27 100644 --- a/homeassistant/components/ialarm/manifest.json +++ b/homeassistant/components/ialarm/manifest.json @@ -2,7 +2,7 @@ "domain": "ialarm", "name": "Antifurto365 iAlarm", "documentation": "https://www.home-assistant.io/integrations/ialarm", - "requirements": ["pyialarm==1.9.0"], + "requirements": ["pyialarm==2.2.0"], "codeowners": ["@RyuzakiKK"], "config_flow": true, "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index deb6ce80558..6bb80d139ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1553,7 +1553,7 @@ pyhomematic==0.1.77 pyhomeworks==0.0.6 # homeassistant.components.ialarm -pyialarm==1.9.0 +pyialarm==2.2.0 # homeassistant.components.icloud pyicloud==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64087216db2..819b03b2229 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1047,7 +1047,7 @@ pyhiveapi==0.5.13 pyhomematic==0.1.77 # homeassistant.components.ialarm -pyialarm==1.9.0 +pyialarm==2.2.0 # homeassistant.components.icloud pyicloud==1.0.0 From d31a0c8dcadde568e5518ced19e7cf51a568b0c5 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sun, 10 Jul 2022 13:35:45 -0400 Subject: [PATCH 2519/3516] Correctly handle device triggers for missing ZHA devices (#74894) --- homeassistant/components/zha/core/helpers.py | 2 +- tests/components/zha/test_device_trigger.py | 38 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 390ef290dc2..b60f61b1e8e 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -170,7 +170,7 @@ def async_get_zha_device(hass: HomeAssistant, device_id: str) -> ZHADevice: device_registry = dr.async_get(hass) registry_device = device_registry.async_get(device_id) if not registry_device: - raise ValueError(f"Device id `{device_id}` not found in registry.") + raise KeyError(f"Device id `{device_id}` not found in registry.") zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee_address = list(list(registry_device.identifiers)[0])[1] ieee = zigpy.types.EUI64.convert(ieee_address) diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 1f5fa467a93..8e19fe5b637 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -370,3 +370,41 @@ async def test_exception_bad_trigger(hass, mock_devices, calls, caplog): ) await hass.async_block_till_done() assert "Invalid config for [automation]" in caplog.text + + +async def test_exception_no_device(hass, mock_devices, calls, caplog): + """Test for exception on event triggers firing.""" + + zigpy_device, zha_device = mock_devices + + zigpy_device.device_automation_triggers = { + (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, + (DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE}, + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}, + (LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD}, + (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, + } + + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": "no_such_device_id", + "domain": "zha", + "platform": "device", + "type": "junk", + "subtype": "junk", + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert "Invalid config for [automation]" in caplog.text From 3f53022b50db4171fbfca3f97f49ee393f2dbadd Mon Sep 17 00:00:00 2001 From: henryptung Date: Mon, 11 Jul 2022 02:33:28 -0700 Subject: [PATCH 2520/3516] Remove pip --prefix workaround (#74922) Remove --prefix workaround See discussion in https://github.com/home-assistant/core/issues/74405. This workaround is no longer needed on pip >= 21.0 and actively causes problems for pip >= 21.3. --- homeassistant/util/package.py | 3 --- tests/util/test_package.py | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index aad93e37542..18ab43967ec 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -89,9 +89,6 @@ def install_package( # This only works if not running in venv args += ["--user"] env["PYTHONUSERBASE"] = os.path.abspath(target) - # Workaround for incompatible prefix setting - # See http://stackoverflow.com/a/4495175 - args += ["--prefix="] _LOGGER.debug("Running pip command: args=%s", args) with Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env) as process: _, stderr = process.communicate() diff --git a/tests/util/test_package.py b/tests/util/test_package.py index d6b7402a5b6..7ab087f1463 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -137,7 +137,6 @@ def test_install_target(mock_sys, mock_popen, mock_env_copy, mock_venv): "--quiet", TEST_NEW_REQ, "--user", - "--prefix=", ] assert package.install_package(TEST_NEW_REQ, False, target=target) @@ -156,7 +155,7 @@ def test_install_target_venv(mock_sys, mock_popen, mock_env_copy, mock_venv): def test_install_error(caplog, mock_sys, mock_popen, mock_venv): - """Test an install with a target.""" + """Test an install that errors out.""" caplog.set_level(logging.WARNING) mock_popen.return_value.returncode = 1 assert not package.install_package(TEST_NEW_REQ) From 4d81d056da889247407c896b8bc220f187e12a8d Mon Sep 17 00:00:00 2001 From: Markus Date: Mon, 11 Jul 2022 10:49:31 +0200 Subject: [PATCH 2521/3516] Fix Pyload request content type headers (#74957) --- homeassistant/components/pyload/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index e4bbf7df3d2..81e1a02408a 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from datetime import timedelta import logging -from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol @@ -144,7 +143,7 @@ class PyLoadAPI: """Initialize pyLoad API and set headers needed later.""" self.api_url = api_url self.status = None - self.headers = {CONTENT_TYPE: CONTENT_TYPE_JSON} + self.headers = {"Content-Type": CONTENT_TYPE_JSON} if username is not None and password is not None: self.payload = {"username": username, "password": password} From c45313e9dec151f951f2d2f5aec9bb49518a4cf7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Jul 2022 23:46:55 +0200 Subject: [PATCH 2522/3516] JSON serialize NamedTuple subclasses with aiohttp (#74971) --- homeassistant/helpers/aiohttp_client.py | 4 ++-- tests/helpers/test_aiohttp_client.py | 11 +++++++++++ tests/helpers/test_json.py | 22 ++++++++++++++++++++++ tests/test_util/aiohttp.py | 2 +- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 2e56698db41..2ef96091d15 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -14,7 +14,6 @@ from aiohttp import web from aiohttp.hdrs import CONTENT_TYPE, USER_AGENT from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout import async_timeout -import orjson from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ @@ -23,6 +22,7 @@ from homeassistant.loader import bind_hass from homeassistant.util import ssl as ssl_util from .frame import warn_use +from .json import json_dumps DATA_CONNECTOR = "aiohttp_connector" DATA_CONNECTOR_NOTVERIFY = "aiohttp_connector_notverify" @@ -98,7 +98,7 @@ def _async_create_clientsession( """Create a new ClientSession with kwargs, i.e. for cookies.""" clientsession = aiohttp.ClientSession( connector=_async_get_connector(hass, verify_ssl), - json_serialize=lambda x: orjson.dumps(x).decode("utf-8"), + json_serialize=json_dumps, **kwargs, ) # Prevent packages accidentally overriding our default headers diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 599b2c6984e..1ffb4267167 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -19,6 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE import homeassistant.helpers.aiohttp_client as client +from homeassistant.util.color import RGBColor from tests.common import MockConfigEntry @@ -215,6 +216,16 @@ async def test_async_aiohttp_proxy_stream_client_err(aioclient_mock, camera_clie assert resp.status == 502 +async def test_sending_named_tuple(hass, aioclient_mock): + """Test sending a named tuple in json.""" + resp = aioclient_mock.post("http://127.0.0.1/rgb", json={"rgb": RGBColor(4, 3, 2)}) + session = client.async_create_clientsession(hass) + resp = await session.post("http://127.0.0.1/rgb", json={"rgb": RGBColor(4, 3, 2)}) + assert resp.status == 200 + await resp.json() == {"rgb": RGBColor(4, 3, 2)} + aioclient_mock.mock_calls[0][2]["rgb"] == RGBColor(4, 3, 2) + + async def test_client_session_immutable_headers(hass): """Test we can't mutate headers.""" session = client.async_get_clientsession(hass) diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py index 54c488690fa..1e85338f152 100644 --- a/tests/helpers/test_json.py +++ b/tests/helpers/test_json.py @@ -2,6 +2,7 @@ import datetime import json import time +from typing import NamedTuple import pytest @@ -13,6 +14,7 @@ from homeassistant.helpers.json import ( json_dumps_sorted, ) from homeassistant.util import dt as dt_util +from homeassistant.util.color import RGBColor @pytest.mark.parametrize("encoder", (JSONEncoder, ExtendedJSONEncoder)) @@ -96,3 +98,23 @@ def test_json_dumps_tuple_subclass(): tt = time.struct_time((1999, 3, 17, 32, 44, 55, 2, 76, 0)) assert json_dumps(tt) == "[1999,3,17,32,44,55,2,76,0]" + + +def test_json_dumps_named_tuple_subclass(): + """Test the json dumps a tuple subclass.""" + + class NamedTupleSubclass(NamedTuple): + """A NamedTuple subclass.""" + + name: str + + nts = NamedTupleSubclass("a") + + assert json_dumps(nts) == '["a"]' + + +def test_json_dumps_rgb_color_subclass(): + """Test the json dumps of RGBColor.""" + rgb = RGBColor(4, 2, 1) + + assert json_dumps(rgb) == "[4,2,1]" diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 9ed47109210..4ed81a3a577 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -111,7 +111,7 @@ class AiohttpClientMocker: def create_session(self, loop): """Create a ClientSession that is bound to this mocker.""" - session = ClientSession(loop=loop) + session = ClientSession(loop=loop, json_serialize=json_dumps) # Setting directly on `session` will raise deprecation warning object.__setattr__(session, "_request", self.match_request) return session From a34e72f3a185e27a2af31da75106cdd374aaad15 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 11 Jul 2022 14:38:08 +0200 Subject: [PATCH 2523/3516] Fix mix of aiohttp and requests in ClickSend TTS (#74985) --- homeassistant/components/clicksend_tts/notify.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/clicksend_tts/notify.py b/homeassistant/components/clicksend_tts/notify.py index 712787c34e6..8026c8e150b 100644 --- a/homeassistant/components/clicksend_tts/notify.py +++ b/homeassistant/components/clicksend_tts/notify.py @@ -3,7 +3,6 @@ from http import HTTPStatus import json import logging -from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol @@ -20,7 +19,7 @@ _LOGGER = logging.getLogger(__name__) BASE_API_URL = "https://rest.clicksend.com/v3" -HEADERS = {CONTENT_TYPE: CONTENT_TYPE_JSON} +HEADERS = {"Content-Type": CONTENT_TYPE_JSON} CONF_LANGUAGE = "language" CONF_VOICE = "voice" From f738d39ad9f491581d8116f7d0f6fce49bdac8dc Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Tue, 12 Jul 2022 01:42:33 -0500 Subject: [PATCH 2524/3516] Do not spam log when Life360 member location is missing (#75029) --- homeassistant/components/life360/__init__.py | 6 +- .../components/life360/coordinator.py | 62 +++++++++++++++---- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/life360/__init__.py b/homeassistant/components/life360/__init__.py index 4527f6ac298..d05a9ec400b 100644 --- a/homeassistant/components/life360/__init__.py +++ b/homeassistant/components/life360/__init__.py @@ -37,7 +37,7 @@ from .const import ( SHOW_DRIVING, SHOW_MOVING, ) -from .coordinator import Life360DataUpdateCoordinator +from .coordinator import Life360DataUpdateCoordinator, MissingLocReason PLATFORMS = [Platform.DEVICE_TRACKER] @@ -128,6 +128,10 @@ class IntegData: coordinators: dict[str, Life360DataUpdateCoordinator] = field( init=False, default_factory=dict ) + # member_id: missing location reason + missing_loc_reason: dict[str, MissingLocReason] = field( + init=False, default_factory=dict + ) # member_id: ConfigEntry.entry_id tracked_members: dict[str, str] = field(init=False, default_factory=dict) logged_circles: list[str] = field(init=False, default_factory=list) diff --git a/homeassistant/components/life360/coordinator.py b/homeassistant/components/life360/coordinator.py index dc7fdb73a8c..05eecd43cdc 100644 --- a/homeassistant/components/life360/coordinator.py +++ b/homeassistant/components/life360/coordinator.py @@ -2,8 +2,10 @@ from __future__ import annotations +from contextlib import suppress from dataclasses import dataclass, field from datetime import datetime +from enum import Enum from typing import Any from life360 import Life360, Life360Error, LoginError @@ -33,6 +35,13 @@ from .const import ( ) +class MissingLocReason(Enum): + """Reason member location information is missing.""" + + VAGUE_ERROR_REASON = "vague error reason" + EXPLICIT_ERROR_REASON = "explicit error reason" + + @dataclass class Life360Place: """Life360 Place data.""" @@ -99,6 +108,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator): max_retries=COMM_MAX_RETRIES, authorization=entry.data[CONF_AUTHORIZATION], ) + self._missing_loc_reason = hass.data[DOMAIN].missing_loc_reason async def _retrieve_data(self, func: str, *args: Any) -> list[dict[str, Any]]: """Get data from Life360.""" @@ -141,10 +151,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator): if not int(member["features"]["shareLocation"]): continue - # Note that member may be in more than one circle. If that's the case just - # go ahead and process the newly retrieved data (overwriting the older - # data), since it might be slightly newer than what was retrieved while - # processing another circle. + member_id = member["id"] first = member["firstName"] last = member["lastName"] @@ -153,16 +160,45 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator): else: name = first or last - loc = member["location"] - if not loc: - if err_msg := member["issues"]["title"]: - if member["issues"]["dialog"]: - err_msg += f": {member['issues']['dialog']}" - else: - err_msg = "Location information missing" - LOGGER.error("%s: %s", name, err_msg) + cur_missing_reason = self._missing_loc_reason.get(member_id) + + # Check if location information is missing. This can happen if server + # has not heard from member's device in a long time (e.g., has been off + # for a long time, or has lost service, etc.) + if loc := member["location"]: + with suppress(KeyError): + del self._missing_loc_reason[member_id] + else: + if explicit_reason := member["issues"]["title"]: + if extended_reason := member["issues"]["dialog"]: + explicit_reason += f": {extended_reason}" + # Note that different Circles can report missing location in + # different ways. E.g., one might report an explicit reason and + # another does not. If a vague reason has already been logged but a + # more explicit reason is now available, log that, too. + if ( + cur_missing_reason is None + or cur_missing_reason == MissingLocReason.VAGUE_ERROR_REASON + and explicit_reason + ): + if explicit_reason: + self._missing_loc_reason[ + member_id + ] = MissingLocReason.EXPLICIT_ERROR_REASON + err_msg = explicit_reason + else: + self._missing_loc_reason[ + member_id + ] = MissingLocReason.VAGUE_ERROR_REASON + err_msg = "Location information missing" + LOGGER.error("%s: %s", name, err_msg) continue + # Note that member may be in more than one circle. If that's the case + # just go ahead and process the newly retrieved data (overwriting the + # older data), since it might be slightly newer than what was retrieved + # while processing another circle. + place = loc["name"] or None if place: @@ -179,7 +215,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator): if self._hass.config.units.is_metric: speed = convert(speed, LENGTH_MILES, LENGTH_KILOMETERS) - data.members[member["id"]] = Life360Member( + data.members[member_id] = Life360Member( address, dt_util.utc_from_timestamp(int(loc["since"])), bool(int(loc["charge"])), From bdc4171e3795244f89501b6243a357f89422c3f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 12 Jul 2022 01:35:45 +0300 Subject: [PATCH 2525/3516] Upgrade huawei-lte-api to 1.6.1 (#75030) --- homeassistant/components/huawei_lte/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index dd0382d5f55..c1b15d57620 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ - "huawei-lte-api==1.6.0", + "huawei-lte-api==1.6.1", "stringcase==1.2.0", "url-normalize==1.4.1" ], diff --git a/requirements_all.txt b/requirements_all.txt index 6bb80d139ae..e20a7fa830e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -846,7 +846,7 @@ horimote==0.4.1 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.6.0 +huawei-lte-api==1.6.1 # homeassistant.components.huisbaasje huisbaasje-client==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 819b03b2229..8eebc524bef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -610,7 +610,7 @@ homepluscontrol==0.0.5 httplib2==0.20.4 # homeassistant.components.huawei_lte -huawei-lte-api==1.6.0 +huawei-lte-api==1.6.1 # homeassistant.components.huisbaasje huisbaasje-client==0.1.0 From 620d2ed8fd19f1baf2db09571ad731f7790e1d73 Mon Sep 17 00:00:00 2001 From: Gabe Cook Date: Tue, 12 Jul 2022 09:06:38 -0500 Subject: [PATCH 2526/3516] Fix Ruckus Unleashed SSH connection failures (#75032) --- .../components/ruckus_unleashed/__init__.py | 7 +++---- .../components/ruckus_unleashed/config_flow.py | 14 +++++++------- .../components/ruckus_unleashed/coordinator.py | 4 +--- .../components/ruckus_unleashed/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ruckus_unleashed/test_init.py | 4 ++-- 7 files changed, 16 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/ruckus_unleashed/__init__.py b/homeassistant/components/ruckus_unleashed/__init__.py index 2c8c8bb108d..62ed4949e05 100644 --- a/homeassistant/components/ruckus_unleashed/__init__.py +++ b/homeassistant/components/ruckus_unleashed/__init__.py @@ -29,8 +29,7 @@ from .coordinator import RuckusUnleashedDataUpdateCoordinator async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ruckus Unleashed from a config entry.""" try: - ruckus = await hass.async_add_executor_job( - Ruckus, + ruckus = await Ruckus.create( entry.data[CONF_HOST], entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], @@ -42,10 +41,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - system_info = await hass.async_add_executor_job(ruckus.system_info) + system_info = await ruckus.system_info() registry = device_registry.async_get(hass) - ap_info = await hass.async_add_executor_job(ruckus.ap_info) + ap_info = await ruckus.ap_info() for device in ap_info[API_AP][API_ID].values(): registry.async_get_or_create( config_entry_id=entry.entry_id, diff --git a/homeassistant/components/ruckus_unleashed/config_flow.py b/homeassistant/components/ruckus_unleashed/config_flow.py index 7d34e620a13..4adf245c3de 100644 --- a/homeassistant/components/ruckus_unleashed/config_flow.py +++ b/homeassistant/components/ruckus_unleashed/config_flow.py @@ -21,22 +21,24 @@ DATA_SCHEMA = vol.Schema( ) -def validate_input(hass: core.HomeAssistant, data): +async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. """ try: - ruckus = Ruckus(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD]) + ruckus = await Ruckus.create( + data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD] + ) except AuthenticationError as error: raise InvalidAuth from error except ConnectionError as error: raise CannotConnect from error - mesh_name = ruckus.mesh_name() + mesh_name = await ruckus.mesh_name() - system_info = ruckus.system_info() + system_info = await ruckus.system_info() try: host_serial = system_info[API_SYSTEM_OVERVIEW][API_SERIAL] except KeyError as error: @@ -58,9 +60,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: try: - info = await self.hass.async_add_executor_job( - validate_input, self.hass, user_input - ) + info = await validate_input(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: diff --git a/homeassistant/components/ruckus_unleashed/coordinator.py b/homeassistant/components/ruckus_unleashed/coordinator.py index 8b80eaae0da..e84b79ef843 100644 --- a/homeassistant/components/ruckus_unleashed/coordinator.py +++ b/homeassistant/components/ruckus_unleashed/coordinator.py @@ -37,9 +37,7 @@ class RuckusUnleashedDataUpdateCoordinator(DataUpdateCoordinator): async def _fetch_clients(self) -> dict: """Fetch clients from the API and format them.""" - clients = await self.hass.async_add_executor_job( - self.ruckus.current_active_clients - ) + clients = await self.ruckus.current_active_clients() return {e[API_MAC]: e for e in clients[API_CURRENT_ACTIVE_CLIENTS][API_CLIENTS]} async def _async_update_data(self) -> dict: diff --git a/homeassistant/components/ruckus_unleashed/manifest.json b/homeassistant/components/ruckus_unleashed/manifest.json index f010d340147..a6b2ad0271c 100644 --- a/homeassistant/components/ruckus_unleashed/manifest.json +++ b/homeassistant/components/ruckus_unleashed/manifest.json @@ -3,7 +3,7 @@ "name": "Ruckus Unleashed", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ruckus_unleashed", - "requirements": ["pyruckus==0.12"], + "requirements": ["pyruckus==0.16"], "codeowners": ["@gabe565"], "iot_class": "local_polling", "loggers": ["pexpect", "pyruckus"] diff --git a/requirements_all.txt b/requirements_all.txt index e20a7fa830e..df753662c8a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1780,7 +1780,7 @@ pyrisco==0.3.1 pyrituals==0.0.6 # homeassistant.components.ruckus_unleashed -pyruckus==0.12 +pyruckus==0.16 # homeassistant.components.sabnzbd pysabnzbd==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8eebc524bef..7c0eaa25b14 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1211,7 +1211,7 @@ pyrisco==0.3.1 pyrituals==0.0.6 # homeassistant.components.ruckus_unleashed -pyruckus==0.12 +pyruckus==0.16 # homeassistant.components.sabnzbd pysabnzbd==1.1.1 diff --git a/tests/components/ruckus_unleashed/test_init.py b/tests/components/ruckus_unleashed/test_init.py index e9ac9ec7cd8..d72856aa542 100644 --- a/tests/components/ruckus_unleashed/test_init.py +++ b/tests/components/ruckus_unleashed/test_init.py @@ -31,7 +31,7 @@ async def test_setup_entry_login_error(hass): """Test entry setup failed due to login error.""" entry = mock_config_entry() with patch( - "homeassistant.components.ruckus_unleashed.Ruckus", + "homeassistant.components.ruckus_unleashed.Ruckus.connect", side_effect=AuthenticationError, ): entry.add_to_hass(hass) @@ -45,7 +45,7 @@ async def test_setup_entry_connection_error(hass): """Test entry setup failed due to connection error.""" entry = mock_config_entry() with patch( - "homeassistant.components.ruckus_unleashed.Ruckus", + "homeassistant.components.ruckus_unleashed.Ruckus.connect", side_effect=ConnectionError, ): entry.add_to_hass(hass) From 51a4c9856219746bf6257dd9675a115d1f80410e Mon Sep 17 00:00:00 2001 From: Thijs W Date: Tue, 12 Jul 2022 12:03:26 +0200 Subject: [PATCH 2527/3516] Bump afsapi to 0.2.6 (#75041) --- homeassistant/components/frontier_silicon/manifest.json | 2 +- homeassistant/components/frontier_silicon/media_player.py | 7 +++++-- requirements_all.txt | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontier_silicon/manifest.json b/homeassistant/components/frontier_silicon/manifest.json index 12fb5145aa0..b04d68e672d 100644 --- a/homeassistant/components/frontier_silicon/manifest.json +++ b/homeassistant/components/frontier_silicon/manifest.json @@ -2,7 +2,7 @@ "domain": "frontier_silicon", "name": "Frontier Silicon", "documentation": "https://www.home-assistant.io/integrations/frontier_silicon", - "requirements": ["afsapi==0.2.5"], + "requirements": ["afsapi==0.2.6"], "codeowners": ["@wlcrs"], "iot_class": "local_polling" } diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 61dc6e69726..b16374c0bc0 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -179,11 +179,14 @@ class AFSAPIDevice(MediaPlayerEntity): self._attr_media_artist = await afsapi.get_play_artist() self._attr_media_album_name = await afsapi.get_play_album() - self._attr_source = (await afsapi.get_mode()).label + radio_mode = await afsapi.get_mode() + self._attr_source = radio_mode.label if radio_mode is not None else None self._attr_is_volume_muted = await afsapi.get_mute() self._attr_media_image_url = await afsapi.get_play_graphic() - self._attr_sound_mode = (await afsapi.get_eq_preset()).label + + eq_preset = await afsapi.get_eq_preset() + self._attr_sound_mode = eq_preset.label if eq_preset is not None else None volume = await self.fs_device.get_volume() diff --git a/requirements_all.txt b/requirements_all.txt index df753662c8a..bc734b22445 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -89,7 +89,7 @@ adguardhome==0.5.1 advantage_air==0.3.1 # homeassistant.components.frontier_silicon -afsapi==0.2.5 +afsapi==0.2.6 # homeassistant.components.agent_dvr agent-py==0.0.23 From 5c882429d48874f0e24a0f1670cb286e16ce7257 Mon Sep 17 00:00:00 2001 From: hahn-th Date: Tue, 12 Jul 2022 15:31:04 +0200 Subject: [PATCH 2528/3516] Bump homematicip to 1.0.4 (#75053) --- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 40f7e67fd07..f6fb9f3e739 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==1.0.3"], + "requirements": ["homematicip==1.0.4"], "codeowners": [], "quality_scale": "platinum", "iot_class": "cloud_push", diff --git a/requirements_all.txt b/requirements_all.txt index bc734b22445..5d99a060bea 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -834,7 +834,7 @@ home-assistant-frontend==20220707.0 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.3 +homematicip==1.0.4 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7c0eaa25b14..790814dfe0f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -601,7 +601,7 @@ home-assistant-frontend==20220707.0 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.3 +homematicip==1.0.4 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 From 55ae0228a93be4e5e3c1ee803988aea78947c5f8 Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 13 Jul 2022 02:46:32 -0400 Subject: [PATCH 2529/3516] Bump AIOAladdinConnect to 0.1.23 (#75065) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 3a9e295a08f..bdf906ad200 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.21"], + "requirements": ["AIOAladdinConnect==0.1.23"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 5d99a060bea..89a25bd31d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.21 +AIOAladdinConnect==0.1.23 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 790814dfe0f..ba4e19393dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.21 +AIOAladdinConnect==0.1.23 # homeassistant.components.adax Adax-local==0.1.4 From 06e7f718913b483d012257435038b3f6819ea082 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Wed, 13 Jul 2022 02:56:08 -0400 Subject: [PATCH 2530/3516] Fix Insteon thermostat issues (#75079) --- homeassistant/components/insteon/climate.py | 2 +- homeassistant/components/insteon/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/insteon/climate.py b/homeassistant/components/insteon/climate.py index 97e21a02f6f..29c127ba0c7 100644 --- a/homeassistant/components/insteon/climate.py +++ b/homeassistant/components/insteon/climate.py @@ -86,7 +86,7 @@ class InsteonClimateEntity(InsteonEntity, ClimateEntity): @property def temperature_unit(self) -> str: """Return the unit of measurement.""" - if self._insteon_device.properties[CELSIUS].value: + if self._insteon_device.configuration[CELSIUS].value: return TEMP_CELSIUS return TEMP_FAHRENHEIT diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 1be077a6b38..c48d502c16e 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -4,8 +4,8 @@ "documentation": "https://www.home-assistant.io/integrations/insteon", "dependencies": ["http", "websocket_api"], "requirements": [ - "pyinsteon==1.1.1", - "insteon-frontend-home-assistant==0.1.1" + "pyinsteon==1.1.3", + "insteon-frontend-home-assistant==0.2.0" ], "codeowners": ["@teharris1"], "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 89a25bd31d8..c980c6ca7db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -891,7 +891,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.insteon -insteon-frontend-home-assistant==0.1.1 +insteon-frontend-home-assistant==0.2.0 # homeassistant.components.intellifire intellifire4py==2.0.1 @@ -1559,7 +1559,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.1 +pyinsteon==1.1.3 # homeassistant.components.intesishome pyintesishome==1.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba4e19393dd..04de268790f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -634,7 +634,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.insteon -insteon-frontend-home-assistant==0.1.1 +insteon-frontend-home-assistant==0.2.0 # homeassistant.components.intellifire intellifire4py==2.0.1 @@ -1053,7 +1053,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.1 +pyinsteon==1.1.3 # homeassistant.components.ipma pyipma==2.0.5 From 98c3bc56b50fea9e30bae5e7955785d51a819fff Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Wed, 13 Jul 2022 10:12:50 +0300 Subject: [PATCH 2531/3516] Fix missing ordered states in universal media player (#75099) --- homeassistant/components/universal/media_player.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index f33db3827af..352703f8f9b 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -69,11 +69,13 @@ from homeassistant.const import ( SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, + STATE_BUFFERING, STATE_IDLE, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING, + STATE_STANDBY, STATE_UNAVAILABLE, STATE_UNKNOWN, ) @@ -101,8 +103,10 @@ STATES_ORDER = [ STATE_UNAVAILABLE, STATE_OFF, STATE_IDLE, + STATE_STANDBY, STATE_ON, STATE_PAUSED, + STATE_BUFFERING, STATE_PLAYING, ] ATTRS_SCHEMA = cv.schema_with_slug_keys(cv.string) From b105b0fbcbe9f5ac32fbcfa4c70570b7812e128e Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 13 Jul 2022 22:05:43 +0200 Subject: [PATCH 2532/3516] Make sure device tuple is a list on save (#75103) --- homeassistant/components/rfxtrx/config_flow.py | 2 +- tests/components/rfxtrx/test_config_flow.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index 61d01b8d533..508ca9a7037 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -199,7 +199,7 @@ class OptionsFlow(config_entries.OptionsFlow): if not errors: devices = {} device = { - CONF_DEVICE_ID: device_id, + CONF_DEVICE_ID: list(device_id), } devices[self._selected_device_event_code] = device diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index 2c695d71d2e..eaa7f1c79a1 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -867,6 +867,9 @@ async def test_options_configure_rfy_cover_device(hass): entry.data["devices"]["0C1a0000010203010000000000"]["venetian_blind_mode"] == "EU" ) + assert isinstance( + entry.data["devices"]["0C1a0000010203010000000000"]["device_id"], list + ) device_registry = dr.async_get(hass) device_entries = dr.async_entries_for_config_entry(device_registry, entry.entry_id) @@ -904,6 +907,9 @@ async def test_options_configure_rfy_cover_device(hass): entry.data["devices"]["0C1a0000010203010000000000"]["venetian_blind_mode"] == "EU" ) + assert isinstance( + entry.data["devices"]["0C1a0000010203010000000000"]["device_id"], list + ) def test_get_serial_by_id_no_dir(): From 9e99ea68fbf9eb7bc4dd5cafc51e72eb6fc8a57b Mon Sep 17 00:00:00 2001 From: kingy444 Date: Thu, 14 Jul 2022 06:06:32 +1000 Subject: [PATCH 2533/3516] Fix Powerview top shade open position (#75110) --- .../hunterdouglas_powerview/cover.py | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 3f7d04f5b87..7c4c90a2132 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -455,14 +455,6 @@ class PowerViewShadeTDBUTop(PowerViewShadeTDBU): super().__init__(coordinator, device_info, room_name, shade, name) self._attr_unique_id = f"{self._shade.id}_top" self._attr_name = f"{self._shade_name} Top" - # these shades share a class in parent API - # override open position for top shade - self._shade.open_position = { - ATTR_POSITION1: MIN_POSITION, - ATTR_POSITION2: MAX_POSITION, - ATTR_POSKIND1: POS_KIND_PRIMARY, - ATTR_POSKIND2: POS_KIND_SECONDARY, - } @property def should_poll(self) -> bool: @@ -485,6 +477,21 @@ class PowerViewShadeTDBUTop(PowerViewShadeTDBU): # these need to be inverted to report state correctly in HA return hd_position_to_hass(self.positions.secondary, MAX_POSITION) + @property + def open_position(self) -> PowerviewShadeMove: + """Return the open position and required additional positions.""" + # these shades share a class in parent API + # override open position for top shade + return PowerviewShadeMove( + { + ATTR_POSITION1: MIN_POSITION, + ATTR_POSITION2: MAX_POSITION, + ATTR_POSKIND1: POS_KIND_PRIMARY, + ATTR_POSKIND2: POS_KIND_SECONDARY, + }, + {}, + ) + @callback def _clamp_cover_limit(self, target_hass_position: int) -> int: """Dont allow a cover to go into an impossbile position.""" From 89e87119f27f9175801584011de733ce2a57cdf7 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 13 Jul 2022 17:13:09 -0400 Subject: [PATCH 2534/3516] Bump ZHA dependencies (#75133) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 78aea7c9fb6..769254511a7 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.31.0", + "bellows==0.31.1", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.77", @@ -12,7 +12,7 @@ "zigpy==0.47.2", "zigpy-xbee==0.15.0", "zigpy-zigate==0.9.0", - "zigpy-znp==0.8.0" + "zigpy-znp==0.8.1" ], "usb": [ { diff --git a/requirements_all.txt b/requirements_all.txt index c980c6ca7db..01e309e7b6a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -393,7 +393,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.31.0 +bellows==0.31.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.9.6 @@ -2516,7 +2516,7 @@ zigpy-xbee==0.15.0 zigpy-zigate==0.9.0 # homeassistant.components.zha -zigpy-znp==0.8.0 +zigpy-znp==0.8.1 # homeassistant.components.zha zigpy==0.47.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04de268790f..9900ef878b4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -308,7 +308,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.31.0 +bellows==0.31.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.9.6 @@ -1677,7 +1677,7 @@ zigpy-xbee==0.15.0 zigpy-zigate==0.9.0 # homeassistant.components.zha -zigpy-znp==0.8.0 +zigpy-znp==0.8.1 # homeassistant.components.zha zigpy==0.47.2 From f052c3ca7469d94e21881b96aac75300fa4bc915 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 13 Jul 2022 12:52:13 -0600 Subject: [PATCH 2535/3516] Ensure SimpliSafe diagnostics redact the `code` option (#75137) --- homeassistant/components/simplisafe/diagnostics.py | 3 ++- tests/components/simplisafe/conftest.py | 4 +++- tests/components/simplisafe/test_diagnostics.py | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/simplisafe/diagnostics.py b/homeassistant/components/simplisafe/diagnostics.py index dac89715c10..cd6e4ca52be 100644 --- a/homeassistant/components/simplisafe/diagnostics.py +++ b/homeassistant/components/simplisafe/diagnostics.py @@ -5,7 +5,7 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ADDRESS, CONF_LOCATION +from homeassistant.const import CONF_ADDRESS, CONF_CODE, CONF_LOCATION from homeassistant.core import HomeAssistant from . import SimpliSafe @@ -23,6 +23,7 @@ CONF_WIFI_SSID = "wifi_ssid" TO_REDACT = { CONF_ADDRESS, + CONF_CODE, CONF_CREDIT_CARD, CONF_EXPIRES, CONF_LOCATION, diff --git a/tests/components/simplisafe/conftest.py b/tests/components/simplisafe/conftest.py index 56967ac24c5..82bd04a7349 100644 --- a/tests/components/simplisafe/conftest.py +++ b/tests/components/simplisafe/conftest.py @@ -43,7 +43,9 @@ def api_fixture(api_auth_state, data_subscription, system_v3, websocket): @pytest.fixture(name="config_entry") def config_entry_fixture(hass, config, unique_id): """Define a config entry.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=unique_id, data=config) + entry = MockConfigEntry( + domain=DOMAIN, unique_id=unique_id, data=config, options={CONF_CODE: "1234"} + ) entry.add_to_hass(hass) return entry diff --git a/tests/components/simplisafe/test_diagnostics.py b/tests/components/simplisafe/test_diagnostics.py index 13d5c778e89..446d9d5e9e3 100644 --- a/tests/components/simplisafe/test_diagnostics.py +++ b/tests/components/simplisafe/test_diagnostics.py @@ -8,7 +8,9 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_simplisa """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { - "options": {}, + "options": { + "code": REDACTED, + }, }, "subscription_data": { "system_123": { From 1555f706e5a12e17c25c7a9dee637073af0e389e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 13 Jul 2022 14:12:53 -0700 Subject: [PATCH 2536/3516] Block bad pubnub version (#75138) --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ad03978c6ef..167fe546b35 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -114,3 +114,7 @@ backoff<2.0 # Breaking change in version # https://github.com/samuelcolvin/pydantic/issues/4092 pydantic!=1.9.1 + +# Breaks asyncio +# https://github.com/pubnub/python/issues/130 +pubnub!=6.4.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 11e88976e83..7b000a30be2 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -132,6 +132,10 @@ backoff<2.0 # Breaking change in version # https://github.com/samuelcolvin/pydantic/issues/4092 pydantic!=1.9.1 + +# Breaks asyncio +# https://github.com/pubnub/python/issues/130 +pubnub!=6.4.0 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From 533ae85a4984468cb66a0630ab227e141b91bb5c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 13 Jul 2022 14:14:13 -0700 Subject: [PATCH 2537/3516] Bumped version to 2022.7.4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7f5cf999b96..9db438feeb3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index c86fc6b26f0..b7f75f9a008 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.3" +version = "2022.7.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 1d2e64e3dc724184d56916143c5b2d453021fc22 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 14 Jul 2022 00:27:41 +0000 Subject: [PATCH 2538/3516] [ci skip] Translation update --- .../airvisual/translations/sensor.pt.json | 7 +++++ .../components/anthemav/translations/ru.json | 19 ++++++++++++ .../aussie_broadband/translations/pt.json | 5 ++++ .../binary_sensor/translations/pt.json | 3 +- .../components/bosch_shc/translations/pt.json | 3 +- .../components/brunt/translations/pt.json | 1 + .../demo/translations/select.pt.json | 7 +++++ .../derivative/translations/sv.json | 29 +++++++++++++++++-- .../components/dlna_dmr/translations/pt.json | 3 ++ .../components/dlna_dms/translations/pt.json | 1 + .../components/dsmr/translations/pt.json | 3 +- .../components/econet/translations/pt.json | 3 +- .../components/efergy/translations/pt.json | 14 +++++++-- .../components/emonitor/translations/pt.json | 4 +++ .../components/energy/translations/pt.json | 3 ++ .../components/fibaro/translations/pt.json | 3 ++ .../components/fritz/translations/pt.json | 3 ++ .../components/generic/translations/no.json | 3 ++ .../components/generic/translations/ru.json | 4 +++ .../components/group/translations/pt.json | 5 ++++ .../here_travel_time/translations/no.json | 5 ++++ .../here_travel_time/translations/ru.json | 7 +++++ .../homeassistant/translations/ru.json | 1 + .../components/homekit/translations/it.json | 4 +-- .../homekit_controller/translations/pt.json | 3 +- .../components/hyperion/translations/pt.json | 5 ++++ .../components/isy994/translations/pt.json | 1 + .../lg_soundbar/translations/ru.json | 18 ++++++++++++ .../components/life360/translations/ru.json | 23 +++++++++++++++ .../components/lookin/translations/pt.json | 3 ++ .../lutron_caseta/translations/pt.json | 6 ++++ .../components/nextdns/translations/no.json | 19 ++++++++++++ .../components/nextdns/translations/ru.json | 29 +++++++++++++++++++ .../components/nuki/translations/pt.json | 3 +- .../components/onewire/translations/pt.json | 3 +- .../ovo_energy/translations/pt.json | 4 ++- .../components/pvoutput/translations/pt.json | 3 +- .../pvpc_hourly_pricing/translations/pt.json | 7 +++++ .../components/qnap_qsw/translations/ru.json | 6 ++++ .../components/rhasspy/translations/ca.json | 5 ++++ .../components/rhasspy/translations/no.json | 7 +++++ .../components/rhasspy/translations/ru.json | 12 ++++++++ .../components/sensor/translations/pt.json | 2 ++ .../components/sleepiq/translations/pt.json | 5 ++++ .../components/sma/translations/pt.json | 8 +++++ .../soundtouch/translations/ru.json | 21 ++++++++++++++ .../srp_energy/translations/pt.json | 2 ++ .../surepetcare/translations/pt.json | 1 + .../components/switchbot/translations/pt.json | 9 ++++++ .../components/tod/translations/pt.json | 11 +++++++ .../tuya/translations/select.pt.json | 6 +++- .../unifiprotect/translations/pt.json | 1 + .../utility_meter/translations/pt.json | 13 +++++++++ .../components/wallbox/translations/pt.json | 7 +++++ .../components/withings/translations/ca.json | 1 + .../components/withings/translations/no.json | 3 ++ .../components/withings/translations/pt.json | 3 +- .../components/withings/translations/ru.json | 4 +++ .../components/zha/translations/pt.json | 5 ++++ .../zoneminder/translations/pt.json | 1 + .../components/zwave_js/translations/pt.json | 5 ++++ 61 files changed, 388 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/airvisual/translations/sensor.pt.json create mode 100644 homeassistant/components/anthemav/translations/ru.json create mode 100644 homeassistant/components/demo/translations/select.pt.json create mode 100644 homeassistant/components/energy/translations/pt.json create mode 100644 homeassistant/components/lg_soundbar/translations/ru.json create mode 100644 homeassistant/components/nextdns/translations/no.json create mode 100644 homeassistant/components/nextdns/translations/ru.json create mode 100644 homeassistant/components/rhasspy/translations/no.json create mode 100644 homeassistant/components/rhasspy/translations/ru.json create mode 100644 homeassistant/components/sma/translations/pt.json create mode 100644 homeassistant/components/soundtouch/translations/ru.json create mode 100644 homeassistant/components/tod/translations/pt.json create mode 100644 homeassistant/components/utility_meter/translations/pt.json diff --git a/homeassistant/components/airvisual/translations/sensor.pt.json b/homeassistant/components/airvisual/translations/sensor.pt.json new file mode 100644 index 00000000000..08a2e0c018e --- /dev/null +++ b/homeassistant/components/airvisual/translations/sensor.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "airvisual__pollutant_level": { + "moderate": "Moderado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/ru.json b/homeassistant/components/anthemav/translations/ru.json new file mode 100644 index 00000000000..0f343609e4c --- /dev/null +++ b/homeassistant/components/anthemav/translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "cannot_receive_deviceinfo": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c MAC-\u0430\u0434\u0440\u0435\u0441. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aussie_broadband/translations/pt.json b/homeassistant/components/aussie_broadband/translations/pt.json index e831e2ce397..5a8312a5ac1 100644 --- a/homeassistant/components/aussie_broadband/translations/pt.json +++ b/homeassistant/components/aussie_broadband/translations/pt.json @@ -5,6 +5,11 @@ "unknown": "Erro inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + }, "user": { "data": { "username": "Nome de Utilizador" diff --git a/homeassistant/components/binary_sensor/translations/pt.json b/homeassistant/components/binary_sensor/translations/pt.json index a91c4987abb..470acfff5db 100644 --- a/homeassistant/components/binary_sensor/translations/pt.json +++ b/homeassistant/components/binary_sensor/translations/pt.json @@ -90,7 +90,8 @@ } }, "device_class": { - "moisture": "humidade" + "moisture": "humidade", + "problem": "problema" }, "state": { "_": { diff --git a/homeassistant/components/bosch_shc/translations/pt.json b/homeassistant/components/bosch_shc/translations/pt.json index 1286d95f2df..e229572938d 100644 --- a/homeassistant/components/bosch_shc/translations/pt.json +++ b/homeassistant/components/bosch_shc/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/brunt/translations/pt.json b/homeassistant/components/brunt/translations/pt.json index a27df267700..6f18afa4df3 100644 --- a/homeassistant/components/brunt/translations/pt.json +++ b/homeassistant/components/brunt/translations/pt.json @@ -4,6 +4,7 @@ "already_configured": "Conta j\u00e1 configurada" }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, diff --git a/homeassistant/components/demo/translations/select.pt.json b/homeassistant/components/demo/translations/select.pt.json new file mode 100644 index 00000000000..438f02fa47f --- /dev/null +++ b/homeassistant/components/demo/translations/select.pt.json @@ -0,0 +1,7 @@ +{ + "state": { + "demo__speed": { + "light_speed": "Velocidade da Luz" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/sv.json b/homeassistant/components/derivative/translations/sv.json index 66dcd34b1d7..ac5b766d124 100644 --- a/homeassistant/components/derivative/translations/sv.json +++ b/homeassistant/components/derivative/translations/sv.json @@ -1,12 +1,37 @@ { + "config": { + "step": { + "user": { + "data": { + "name": "Namn", + "round": "Precision", + "time_window": "Tidsf\u00f6nster", + "unit_prefix": "Metriskt prefix", + "unit_time": "Tidsenhet" + }, + "data_description": { + "round": "Anger antal decimaler i resultatet." + }, + "description": "Skapa en sensor som ber\u00e4knar derivatan av en sensor", + "title": "L\u00e4gg till derivatasensor" + } + } + }, "options": { "step": { "init": { "data": { "name": "Namn", - "round": "Precision" + "round": "Precision", + "time_window": "Tidsf\u00f6nster", + "unit_prefix": "Metriskt prefix", + "unit_time": "Tidsenhet" + }, + "data_description": { + "round": "Anger antal decimaler i resultatet." } } } - } + }, + "title": "Derivatasensor" } \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/pt.json b/homeassistant/components/dlna_dmr/translations/pt.json index f6f3f84e497..49f47abb540 100644 --- a/homeassistant/components/dlna_dmr/translations/pt.json +++ b/homeassistant/components/dlna_dmr/translations/pt.json @@ -4,6 +4,9 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o" }, "step": { + "confirm": { + "description": "Autentica\u00e7\u00e3o inv\u00e1lid" + }, "manual": { "data": { "url": "" diff --git a/homeassistant/components/dlna_dms/translations/pt.json b/homeassistant/components/dlna_dms/translations/pt.json index f67e36f3487..1a5e788da4b 100644 --- a/homeassistant/components/dlna_dms/translations/pt.json +++ b/homeassistant/components/dlna_dms/translations/pt.json @@ -4,6 +4,7 @@ "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "no_devices_found": "Nenhum dispositivo encontrado na rede" }, + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/dsmr/translations/pt.json b/homeassistant/components/dsmr/translations/pt.json index c9fff00bd53..311a677d6de 100644 --- a/homeassistant/components/dsmr/translations/pt.json +++ b/homeassistant/components/dsmr/translations/pt.json @@ -11,7 +11,8 @@ "setup_network": { "data": { "host": "Servidor" - } + }, + "title": "Seleccione o endere\u00e7o de liga\u00e7\u00e3o" }, "setup_serial_manual_path": { "data": { diff --git a/homeassistant/components/econet/translations/pt.json b/homeassistant/components/econet/translations/pt.json index 6c333b17fc9..25aaf514180 100644 --- a/homeassistant/components/econet/translations/pt.json +++ b/homeassistant/components/econet/translations/pt.json @@ -7,7 +7,8 @@ "step": { "user": { "data": { - "email": "Email" + "email": "Email", + "password": "Palavra-passe" } } } diff --git a/homeassistant/components/efergy/translations/pt.json b/homeassistant/components/efergy/translations/pt.json index 8f319572c97..db89dfe29e6 100644 --- a/homeassistant/components/efergy/translations/pt.json +++ b/homeassistant/components/efergy/translations/pt.json @@ -1,10 +1,20 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" }, "error": { - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave API" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/pt.json b/homeassistant/components/emonitor/translations/pt.json index 8578f969852..04374af8e82 100644 --- a/homeassistant/components/emonitor/translations/pt.json +++ b/homeassistant/components/emonitor/translations/pt.json @@ -3,6 +3,10 @@ "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/energy/translations/pt.json b/homeassistant/components/energy/translations/pt.json new file mode 100644 index 00000000000..c8d85790fdd --- /dev/null +++ b/homeassistant/components/energy/translations/pt.json @@ -0,0 +1,3 @@ +{ + "title": "Energia" +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/pt.json b/homeassistant/components/fibaro/translations/pt.json index ce8a9287272..db0e0c2a137 100644 --- a/homeassistant/components/fibaro/translations/pt.json +++ b/homeassistant/components/fibaro/translations/pt.json @@ -2,6 +2,9 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" } } } \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/pt.json b/homeassistant/components/fritz/translations/pt.json index 263485b4587..b191e607a42 100644 --- a/homeassistant/components/fritz/translations/pt.json +++ b/homeassistant/components/fritz/translations/pt.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "flow_title": "{name}", "step": { "user": { diff --git a/homeassistant/components/generic/translations/no.json b/homeassistant/components/generic/translations/no.json index 72355d002ed..5c718dc0f47 100644 --- a/homeassistant/components/generic/translations/no.json +++ b/homeassistant/components/generic/translations/no.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Et kamera med disse URL-innstillingene finnes allerede.", "invalid_still_image": "URL returnerte ikke et gyldig stillbilde", + "malformed_url": "Feil utforming p\u00e5 URL", "no_still_image_or_stream_url": "Du m\u00e5 angi minst en URL-adresse for stillbilde eller dataflyt", + "relative_url": "Relative URL-adresser ikke tillatt", "stream_file_not_found": "Filen ble ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m (er ffmpeg installert?)", "stream_http_not_found": "HTTP 404 Ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m", "stream_io_error": "Inn-/utdatafeil under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m. Feil RTSP-transportprotokoll?", @@ -50,6 +52,7 @@ "error": { "already_exists": "Et kamera med disse URL-innstillingene finnes allerede.", "invalid_still_image": "URL returnerte ikke et gyldig stillbilde", + "malformed_url": "Feil utforming p\u00e5 URL", "no_still_image_or_stream_url": "Du m\u00e5 angi minst en URL-adresse for stillbilde eller dataflyt", "stream_file_not_found": "Filen ble ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m (er ffmpeg installert?)", "stream_http_not_found": "HTTP 404 Ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m", diff --git a/homeassistant/components/generic/translations/ru.json b/homeassistant/components/generic/translations/ru.json index f9fed6b5831..7314a172867 100644 --- a/homeassistant/components/generic/translations/ru.json +++ b/homeassistant/components/generic/translations/ru.json @@ -7,7 +7,9 @@ "error": { "already_exists": "\u041a\u0430\u043c\u0435\u0440\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c URL-\u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.", "invalid_still_image": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435.", + "malformed_url": "\u041d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", "no_still_image_or_stream_url": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u043f\u043e\u0442\u043e\u043a\u0430.", + "relative_url": "\u041e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u0434\u043e\u043f\u0443\u0441\u043a\u0430\u044e\u0442\u0441\u044f.", "stream_file_not_found": "\u0424\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u043b\u0438 ffmpeg?", "stream_http_not_found": "HTTP 404 \u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", "stream_io_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0432\u043e\u0434\u0430/\u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", @@ -49,7 +51,9 @@ "error": { "already_exists": "\u041a\u0430\u043c\u0435\u0440\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c URL-\u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.", "invalid_still_image": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435.", + "malformed_url": "\u041d\u0435\u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", "no_still_image_or_stream_url": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u043a\u0430\u0437\u0430\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u043f\u043e\u0442\u043e\u043a\u0430.", + "relative_url": "\u041e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u0434\u043e\u043f\u0443\u0441\u043a\u0430\u044e\u0442\u0441\u044f.", "stream_file_not_found": "\u0424\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u043b\u0438 ffmpeg?", "stream_http_not_found": "HTTP 404 \u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", "stream_io_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0432\u043e\u0434\u0430/\u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", diff --git a/homeassistant/components/group/translations/pt.json b/homeassistant/components/group/translations/pt.json index b05b2868744..57c8a41e630 100644 --- a/homeassistant/components/group/translations/pt.json +++ b/homeassistant/components/group/translations/pt.json @@ -14,6 +14,11 @@ "data": { "hide_members": "Esconder membros" } + }, + "switch": { + "data": { + "hide_members": "Esconder membros" + } } } }, diff --git a/homeassistant/components/here_travel_time/translations/no.json b/homeassistant/components/here_travel_time/translations/no.json index 52d4477f379..e4282933051 100644 --- a/homeassistant/components/here_travel_time/translations/no.json +++ b/homeassistant/components/here_travel_time/translations/no.json @@ -39,6 +39,11 @@ }, "title": "Velg Opprinnelse" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Bruk kartplassering" + } + }, "user": { "data": { "api_key": "API-n\u00f8kkel", diff --git a/homeassistant/components/here_travel_time/translations/ru.json b/homeassistant/components/here_travel_time/translations/ru.json index fc649d920df..a89608530d8 100644 --- a/homeassistant/components/here_travel_time/translations/ru.json +++ b/homeassistant/components/here_travel_time/translations/ru.json @@ -39,6 +39,13 @@ }, "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0443\u043d\u043a\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "\u0423\u043a\u0430\u0437\u0430\u0442\u044c \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u043a\u0430\u0440\u0442\u0435", + "origin_entity": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442" + }, + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0443\u043d\u043a\u0442 \u043e\u0442\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f" + }, "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", diff --git a/homeassistant/components/homeassistant/translations/ru.json b/homeassistant/components/homeassistant/translations/ru.json index f8932f1ea7d..b0e27b70861 100644 --- a/homeassistant/components/homeassistant/translations/ru.json +++ b/homeassistant/components/homeassistant/translations/ru.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "\u0410\u0440\u0445\u0438\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0426\u041f", + "config_dir": "\u041a\u0430\u0442\u0430\u043b\u043e\u0433 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438", "dev": "\u0421\u0440\u0435\u0434\u0430 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index 4086db071c2..c46acd8965d 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -44,14 +44,14 @@ "data": { "entities": "Entit\u00e0" }, - "description": "Tutte le entit\u00e0 \"{domini}\" saranno incluse a eccezione delle entit\u00e0 escluse e delle entit\u00e0 categorizzate.", + "description": "Tutte le entit\u00e0 \"{domains}\" saranno incluse a eccezione delle entit\u00e0 escluse e delle entit\u00e0 categorizzate.", "title": "Seleziona le entit\u00e0 da escludere" }, "include": { "data": { "entities": "Entit\u00e0" }, - "description": "Tutte le entit\u00e0 \"{domini}\" saranno incluse a eccezione delle entit\u00e0 escluse e delle entit\u00e0 categorizzate.", + "description": "Tutte le entit\u00e0 \"{domains}\" saranno incluse a eccezione delle entit\u00e0 escluse e delle entit\u00e0 categorizzate.", "title": "Seleziona le entit\u00e0 da includere" }, "init": { diff --git a/homeassistant/components/homekit_controller/translations/pt.json b/homeassistant/components/homekit_controller/translations/pt.json index c4ab5c1e636..febadec444c 100644 --- a/homeassistant/components/homekit_controller/translations/pt.json +++ b/homeassistant/components/homekit_controller/translations/pt.json @@ -48,7 +48,8 @@ "button6": "Bot\u00e3o 6", "button7": "Bot\u00e3o 7", "button8": "Bot\u00e3o 8", - "button9": "Bot\u00e3o 9" + "button9": "Bot\u00e3o 9", + "doorbell": "Campainha" }, "trigger_type": { "single_press": "\"{subtype}\" pressionado" diff --git a/homeassistant/components/hyperion/translations/pt.json b/homeassistant/components/hyperion/translations/pt.json index ac9710c6b9b..0a402a87037 100644 --- a/homeassistant/components/hyperion/translations/pt.json +++ b/homeassistant/components/hyperion/translations/pt.json @@ -10,6 +10,11 @@ "invalid_access_token": "Token de acesso inv\u00e1lido" }, "step": { + "auth": { + "data": { + "create_token": "Criar novo token automaticamente" + } + }, "user": { "data": { "host": "Servidor", diff --git a/homeassistant/components/isy994/translations/pt.json b/homeassistant/components/isy994/translations/pt.json index 9f8734b5a6d..aa4c5f614e6 100644 --- a/homeassistant/components/isy994/translations/pt.json +++ b/homeassistant/components/isy994/translations/pt.json @@ -8,6 +8,7 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, + "flow_title": "{name} ({host})", "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/lg_soundbar/translations/ru.json b/homeassistant/components/lg_soundbar/translations/ru.json new file mode 100644 index 00000000000..f7961eb2e7e --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/ru.json @@ -0,0 +1,18 @@ +{ + "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.", + "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/ru.json b/homeassistant/components/life360/translations/ru.json index 4b3fbceb5d6..1f1f92977ed 100644 --- a/homeassistant/components/life360/translations/ru.json +++ b/homeassistant/components/life360/translations/ru.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "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." }, "create_entry": { @@ -9,11 +11,18 @@ }, "error": { "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.", "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" + }, + "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", @@ -23,5 +32,19 @@ "title": "Life360" } } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0432\u043e\u0436\u0434\u0435\u043d\u0438\u0435 \u043a\u0430\u043a \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435", + "driving_speed": "\u0421\u043a\u043e\u0440\u043e\u0441\u0442\u044c \u0432\u043e\u0436\u0434\u0435\u043d\u0438\u044f", + "limit_gps_acc": "\u041e\u0433\u0440\u0430\u043d\u0438\u0447\u0438\u0442\u044c \u0442\u043e\u0447\u043d\u043e\u0441\u0442\u044c GPS", + "max_gps_accuracy": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f \u0442\u043e\u0447\u043d\u043e\u0441\u0442\u044c GPS (\u0432 \u043c\u0435\u0442\u0440\u0430\u0445)", + "set_drive_speed": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043f\u043e\u0440\u043e\u0433 \u0441\u043a\u043e\u0440\u043e\u0441\u0442\u0438 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/pt.json b/homeassistant/components/lookin/translations/pt.json index 28ad8b6c8f3..03fd6fa7632 100644 --- a/homeassistant/components/lookin/translations/pt.json +++ b/homeassistant/components/lookin/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/lutron_caseta/translations/pt.json b/homeassistant/components/lutron_caseta/translations/pt.json index f4fdf676dbd..4ae57417d6f 100644 --- a/homeassistant/components/lutron_caseta/translations/pt.json +++ b/homeassistant/components/lutron_caseta/translations/pt.json @@ -14,5 +14,11 @@ } } } + }, + "device_automation": { + "trigger_subtype": { + "on": "Ligado", + "raise_3": "Aumentar 3" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/no.json b/homeassistant/components/nextdns/translations/no.json new file mode 100644 index 00000000000..fb4d6616587 --- /dev/null +++ b/homeassistant/components/nextdns/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "unknown": "Totalt uventet feil" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/ru.json b/homeassistant/components/nextdns/translations/ru.json new file mode 100644 index 00000000000..952058cf5f6 --- /dev/null +++ b/homeassistant/components/nextdns/translations/ru.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e\u0442 \u043f\u0440\u043e\u0444\u0438\u043b\u044c NextDNS \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "profiles": { + "data": { + "profile": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c" + } + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/pt.json b/homeassistant/components/nuki/translations/pt.json index ce7cbc3f548..f681da4210f 100644 --- a/homeassistant/components/nuki/translations/pt.json +++ b/homeassistant/components/nuki/translations/pt.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "Servidor" + "host": "Servidor", + "port": "Porta" } } } diff --git a/homeassistant/components/onewire/translations/pt.json b/homeassistant/components/onewire/translations/pt.json index 91786f4b324..fa5aa3de317 100644 --- a/homeassistant/components/onewire/translations/pt.json +++ b/homeassistant/components/onewire/translations/pt.json @@ -9,7 +9,8 @@ "step": { "user": { "data": { - "host": "Servidor" + "host": "Servidor", + "port": "Porta" } } } diff --git a/homeassistant/components/ovo_energy/translations/pt.json b/homeassistant/components/ovo_energy/translations/pt.json index 7015a44b5f9..15241a5fb65 100644 --- a/homeassistant/components/ovo_energy/translations/pt.json +++ b/homeassistant/components/ovo_energy/translations/pt.json @@ -5,11 +5,13 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, + "flow_title": "{username}", "step": { "reauth": { "data": { "password": "Palavra-passe" - } + }, + "title": "Reautentica\u00e7\u00e3o" }, "user": { "data": { diff --git a/homeassistant/components/pvoutput/translations/pt.json b/homeassistant/components/pvoutput/translations/pt.json index 3b5850222d9..98fe3611480 100644 --- a/homeassistant/components/pvoutput/translations/pt.json +++ b/homeassistant/components/pvoutput/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" } } } \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/pt.json b/homeassistant/components/pvpc_hourly_pricing/translations/pt.json index d252c078a2c..79ce20e5033 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/pt.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/pt.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "power": "Pot\u00eancia contratada (kW)" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/ru.json b/homeassistant/components/qnap_qsw/translations/ru.json index ae3869552d0..b45585ca3a4 100644 --- a/homeassistant/components/qnap_qsw/translations/ru.json +++ b/homeassistant/components/qnap_qsw/translations/ru.json @@ -9,6 +9,12 @@ "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { + "discovered_connection": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/rhasspy/translations/ca.json b/homeassistant/components/rhasspy/translations/ca.json index cc92e3ec9f1..b016b21a2e2 100644 --- a/homeassistant/components/rhasspy/translations/ca.json +++ b/homeassistant/components/rhasspy/translations/ca.json @@ -2,6 +2,11 @@ "config": { "abort": { "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "user": { + "description": "Vols activar el suport de Rhasspy?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/no.json b/homeassistant/components/rhasspy/translations/no.json new file mode 100644 index 00000000000..4dc3393a982 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/no.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Kun \u00e9n konfigurert instans i gangen." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/ru.json b/homeassistant/components/rhasspy/translations/ru.json new file mode 100644 index 00000000000..ce319f9537e --- /dev/null +++ b/homeassistant/components/rhasspy/translations/ru.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "step": { + "user": { + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/pt.json b/homeassistant/components/sensor/translations/pt.json index eaef8ef5755..0e162b9c7ca 100644 --- a/homeassistant/components/sensor/translations/pt.json +++ b/homeassistant/components/sensor/translations/pt.json @@ -2,6 +2,7 @@ "device_automation": { "condition_type": { "is_battery_level": "N\u00edvel de bateria atual de {entity_name}", + "is_energy": "Energia atual de {entity_name}", "is_humidity": "humidade {entity_name}", "is_illuminance": "Luminancia atual de {entity_name}", "is_power": "Pot\u00eancia atual de {entity_name}", @@ -12,6 +13,7 @@ }, "trigger_type": { "battery_level": "n\u00edvel da bateria {entity_name}", + "energy": "Mudan\u00e7as de energia de {entity_name}", "humidity": "humidade {entity_name}", "illuminance": "ilumin\u00e2ncia {entity_name}", "power": "pot\u00eancia {entity_name}", diff --git a/homeassistant/components/sleepiq/translations/pt.json b/homeassistant/components/sleepiq/translations/pt.json index 5602f898fc3..cf42abdb666 100644 --- a/homeassistant/components/sleepiq/translations/pt.json +++ b/homeassistant/components/sleepiq/translations/pt.json @@ -9,6 +9,11 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe" + } + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/sma/translations/pt.json b/homeassistant/components/sma/translations/pt.json new file mode 100644 index 00000000000..a37e6656da7 --- /dev/null +++ b/homeassistant/components/sma/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ru.json b/homeassistant/components/soundtouch/translations/ru.json new file mode 100644 index 00000000000..d987f817a85 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + }, + "zeroconf_confirm": { + "description": "\u0412\u044b \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442\u0435\u0441\u044c \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432 Home Assistant \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e SoundTouch \u0441 \u0438\u043c\u0435\u043d\u0435\u043c `{name}`.", + "title": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Bose SoundTouch" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/pt.json b/homeassistant/components/srp_energy/translations/pt.json index 3e10b977773..15c3b188dec 100644 --- a/homeassistant/components/srp_energy/translations/pt.json +++ b/homeassistant/components/srp_energy/translations/pt.json @@ -5,12 +5,14 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_account": "O ID da conta deve ser um n\u00famero de 9 d\u00edgitos", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { "user": { "data": { + "id": "ID da conta", "password": "Palavra-passe", "username": "Nome de Utilizador" } diff --git a/homeassistant/components/surepetcare/translations/pt.json b/homeassistant/components/surepetcare/translations/pt.json index 0e89c76d047..565b9f6c0e8 100644 --- a/homeassistant/components/surepetcare/translations/pt.json +++ b/homeassistant/components/surepetcare/translations/pt.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/switchbot/translations/pt.json b/homeassistant/components/switchbot/translations/pt.json index b8a454fbaba..e2dc9fee9b4 100644 --- a/homeassistant/components/switchbot/translations/pt.json +++ b/homeassistant/components/switchbot/translations/pt.json @@ -7,5 +7,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "update_time": "Tempo entre actualiza\u00e7\u00f5es (segundos)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tod/translations/pt.json b/homeassistant/components/tod/translations/pt.json new file mode 100644 index 00000000000..286cd58dd89 --- /dev/null +++ b/homeassistant/components/tod/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.pt.json b/homeassistant/components/tuya/translations/select.pt.json index fac54570cd6..3a66717f219 100644 --- a/homeassistant/components/tuya/translations/select.pt.json +++ b/homeassistant/components/tuya/translations/select.pt.json @@ -3,6 +3,9 @@ "tuya__basic_nightvision": { "1": "Desligado" }, + "tuya__countdown": { + "cancel": "Cancelar" + }, "tuya__light_mode": { "none": "Desligado" }, @@ -16,7 +19,8 @@ }, "tuya__vacuum_mode": { "pick_zone": "Escolha a Zona", - "right_spiral": "Espiral Direita" + "right_spiral": "Espiral Direita", + "spiral": "Espiral" } } } \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/pt.json b/homeassistant/components/unifiprotect/translations/pt.json index 253a0f414df..9f781c2de55 100644 --- a/homeassistant/components/unifiprotect/translations/pt.json +++ b/homeassistant/components/unifiprotect/translations/pt.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { diff --git a/homeassistant/components/utility_meter/translations/pt.json b/homeassistant/components/utility_meter/translations/pt.json new file mode 100644 index 00000000000..1ad401c42e6 --- /dev/null +++ b/homeassistant/components/utility_meter/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nome", + "tariffs": "Tarifas suportadas" + }, + "description": "Criar um sensor que monitorize o consumo de v\u00e1rias utilidades (por exemplo, energia, g\u00e1s, \u00e1gua, aquecimento) durante um per\u00edodo configurado, tipicamente mensal. O sensor de contador de utilidades (utility_meter) suporta opcionalmente a divis\u00e3o do consumo por tarifas; nesse caso \u00e9 criado um sensor para cada tarifa, bem como uma entidade selecionada para escolher a tarifa atual." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/pt.json b/homeassistant/components/wallbox/translations/pt.json index ce8a9287272..ece60e5e010 100644 --- a/homeassistant/components/wallbox/translations/pt.json +++ b/homeassistant/components/wallbox/translations/pt.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/withings/translations/ca.json b/homeassistant/components/withings/translations/ca.json index c7ec9fcd8ca..91be6ffabbe 100644 --- a/homeassistant/components/withings/translations/ca.json +++ b/homeassistant/components/withings/translations/ca.json @@ -29,6 +29,7 @@ "title": "Reautenticaci\u00f3 de la integraci\u00f3" }, "reauth_confirm": { + "description": "El perfil \"{profile}\" s'ha de tornar a autenticar per poder continuar rebent dades de Withings.", "title": "Reautenticar la integraci\u00f3" } } diff --git a/homeassistant/components/withings/translations/no.json b/homeassistant/components/withings/translations/no.json index ff8d18eec64..40ba24111bf 100644 --- a/homeassistant/components/withings/translations/no.json +++ b/homeassistant/components/withings/translations/no.json @@ -27,6 +27,9 @@ "reauth": { "description": "Profilen {profile} m\u00e5 godkjennes p\u00e5 nytt for \u00e5 kunne fortsette \u00e5 motta Withings-data.", "title": "Godkjenne integrering p\u00e5 nytt" + }, + "reauth_confirm": { + "title": "Re-autentiser integrasjon" } } } diff --git a/homeassistant/components/withings/translations/pt.json b/homeassistant/components/withings/translations/pt.json index 673627c8036..fa97013c5c0 100644 --- a/homeassistant/components/withings/translations/pt.json +++ b/homeassistant/components/withings/translations/pt.json @@ -15,7 +15,8 @@ "profile": { "data": { "profile": "Perfil" - } + }, + "description": "Fornecer um nome de perfil \u00fanico para estes dados. Normalmente, este \u00e9 o nome do perfil que seleccionou na etapa anterior." }, "reauth": { "title": "Re-autenticar Perfil" diff --git a/homeassistant/components/withings/translations/ru.json b/homeassistant/components/withings/translations/ru.json index 7127f9545fa..78f8f3ec5e4 100644 --- a/homeassistant/components/withings/translations/ru.json +++ b/homeassistant/components/withings/translations/ru.json @@ -27,6 +27,10 @@ "reauth": { "description": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c \"{profile}\" \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 Withings.", "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "reauth_confirm": { + "description": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c \"{profile}\" \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445 Withings.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" } } } diff --git a/homeassistant/components/zha/translations/pt.json b/homeassistant/components/zha/translations/pt.json index 42a5c292ecb..435e80b8b76 100644 --- a/homeassistant/components/zha/translations/pt.json +++ b/homeassistant/components/zha/translations/pt.json @@ -12,6 +12,11 @@ } } }, + "config_panel": { + "zha_options": { + "default_light_transition": "Tempo de transi\u00e7\u00e3o de luz predefinido (segundos)" + } + }, "device_automation": { "action_type": { "warn": "Avisar" diff --git a/homeassistant/components/zoneminder/translations/pt.json b/homeassistant/components/zoneminder/translations/pt.json index f8fa0efe967..b85c7c2e7a7 100644 --- a/homeassistant/components/zoneminder/translations/pt.json +++ b/homeassistant/components/zoneminder/translations/pt.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "auth_fail": "O nome de utilizador ou palavra-passe est\u00e1 incorrecto.", "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, diff --git a/homeassistant/components/zwave_js/translations/pt.json b/homeassistant/components/zwave_js/translations/pt.json index 086d4c19ebb..dc59810f31e 100644 --- a/homeassistant/components/zwave_js/translations/pt.json +++ b/homeassistant/components/zwave_js/translations/pt.json @@ -12,6 +12,11 @@ "unknown": "Erro inesperado" }, "step": { + "configure_addon": { + "data": { + "emulate_hardware": "Emular Hardware" + } + }, "manual": { "data": { "url": "" From 2aa98da6246769571af0e6cd8b2fdd2bc8ab04a4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jul 2022 10:56:01 +0200 Subject: [PATCH 2539/3516] Migrate Whois to new entity naming style (#75019) --- homeassistant/components/whois/sensor.py | 7 ++++--- tests/components/whois/test_sensor.py | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 48efaf7630d..68e77162a4f 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -79,7 +79,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( ), WhoisSensorEntityDescription( key="days_until_expiration", - name="Days Until Expiration", + name="Days until expiration", icon="mdi:calendar-clock", native_unit_of_measurement=TIME_DAYS, value_fn=_days_until_expiration, @@ -93,7 +93,7 @@ SENSORS: tuple[WhoisSensorEntityDescription, ...] = ( ), WhoisSensorEntityDescription( key="last_updated", - name="Last Updated", + name="Last updated", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda domain: _ensure_timezone(domain.last_updated), @@ -156,6 +156,7 @@ class WhoisSensorEntity(CoordinatorEntity, SensorEntity): """Implementation of a WHOIS sensor.""" entity_description: WhoisSensorEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -166,10 +167,10 @@ class WhoisSensorEntity(CoordinatorEntity, SensorEntity): """Initialize the sensor.""" super().__init__(coordinator=coordinator) self.entity_description = description - self._attr_name = f"{domain} {description.name}" self._attr_unique_id = f"{domain}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, domain)}, + name=domain, entry_type=DeviceEntryType.SERVICE, ) self._domain = domain diff --git a/tests/components/whois/test_sensor.py b/tests/components/whois/test_sensor.py index 89b84dc5849..828a9242621 100644 --- a/tests/components/whois/test_sensor.py +++ b/tests/components/whois/test_sensor.py @@ -60,7 +60,7 @@ async def test_whois_sensors( assert state.state == "364" assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "home-assistant.io Days Until Expiration" + == "home-assistant.io Days until expiration" ) assert state.attributes.get(ATTR_ICON) == "mdi:calendar-clock" assert ATTR_DEVICE_CLASS not in state.attributes @@ -83,7 +83,7 @@ async def test_whois_sensors( assert entry.unique_id == "home-assistant.io_last_updated" assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == "2021-12-31T23:00:00+00:00" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Last Updated" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Last updated" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert ATTR_ICON not in state.attributes @@ -137,9 +137,9 @@ async def test_whois_sensors( assert device_entry.configuration_url is None assert device_entry.entry_type == dr.DeviceEntryType.SERVICE assert device_entry.identifiers == {(DOMAIN, "home-assistant.io")} + assert device_entry.name == "home-assistant.io" assert device_entry.manufacturer is None assert device_entry.model is None - assert device_entry.name is None assert device_entry.sw_version is None @@ -159,7 +159,7 @@ async def test_whois_sensors_missing_some_attrs( assert entry.unique_id == "home-assistant.io_last_updated" assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == "2021-12-31T23:00:00+00:00" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Last Updated" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "home-assistant.io Last updated" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert ATTR_ICON not in state.attributes From a720b2989aab7886af64fc2be8083857a544614e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jul 2022 10:56:14 +0200 Subject: [PATCH 2540/3516] Migrate RDW to new entity naming style (#75017) --- homeassistant/components/rdw/binary_sensor.py | 7 ++++--- homeassistant/components/rdw/sensor.py | 7 ++++--- tests/components/rdw/test_binary_sensor.py | 14 +++++++------- tests/components/rdw/test_sensor.py | 14 +++++++------- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/rdw/binary_sensor.py b/homeassistant/components/rdw/binary_sensor.py index 81d3e448b78..25ef6b27f1d 100644 --- a/homeassistant/components/rdw/binary_sensor.py +++ b/homeassistant/components/rdw/binary_sensor.py @@ -41,13 +41,13 @@ class RDWBinarySensorEntityDescription( BINARY_SENSORS: tuple[RDWBinarySensorEntityDescription, ...] = ( RDWBinarySensorEntityDescription( key="liability_insured", - name="Liability Insured", + name="Liability insured", icon="mdi:shield-car", is_on_fn=lambda vehicle: vehicle.liability_insured, ), RDWBinarySensorEntityDescription( key="pending_recall", - name="Pending Recall", + name="Pending recall", device_class=BinarySensorDeviceClass.PROBLEM, is_on_fn=lambda vehicle: vehicle.pending_recall, ), @@ -75,6 +75,7 @@ class RDWBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): """Defines an RDW binary sensor.""" entity_description: RDWBinarySensorEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -91,7 +92,7 @@ class RDWBinarySensorEntity(CoordinatorEntity, BinarySensorEntity): entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, coordinator.data.license_plate)}, manufacturer=coordinator.data.brand, - name=f"{coordinator.data.brand}: {coordinator.data.license_plate}", + name=f"{coordinator.data.brand} {coordinator.data.license_plate}", model=coordinator.data.model, configuration_url=f"https://ovi.rdw.nl/default.aspx?kenteken={coordinator.data.license_plate}", ) diff --git a/homeassistant/components/rdw/sensor.py b/homeassistant/components/rdw/sensor.py index 04f525c61b8..d4cb97005a8 100644 --- a/homeassistant/components/rdw/sensor.py +++ b/homeassistant/components/rdw/sensor.py @@ -42,13 +42,13 @@ class RDWSensorEntityDescription( SENSORS: tuple[RDWSensorEntityDescription, ...] = ( RDWSensorEntityDescription( key="apk_expiration", - name="APK Expiration", + name="APK expiration", device_class=SensorDeviceClass.DATE, value_fn=lambda vehicle: vehicle.apk_expiration, ), RDWSensorEntityDescription( key="ascription_date", - name="Ascription Date", + name="Ascription date", device_class=SensorDeviceClass.DATE, value_fn=lambda vehicle: vehicle.ascription_date, ), @@ -76,6 +76,7 @@ class RDWSensorEntity(CoordinatorEntity, SensorEntity): """Defines an RDW sensor.""" entity_description: RDWSensorEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -93,7 +94,7 @@ class RDWSensorEntity(CoordinatorEntity, SensorEntity): entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, f"{license_plate}")}, manufacturer=coordinator.data.brand, - name=f"{coordinator.data.brand}: {coordinator.data.license_plate}", + name=f"{coordinator.data.brand} {coordinator.data.license_plate}", model=coordinator.data.model, configuration_url=f"https://ovi.rdw.nl/default.aspx?kenteken={coordinator.data.license_plate}", ) diff --git a/tests/components/rdw/test_binary_sensor.py b/tests/components/rdw/test_binary_sensor.py index abf15d869ce..aea188db773 100644 --- a/tests/components/rdw/test_binary_sensor.py +++ b/tests/components/rdw/test_binary_sensor.py @@ -16,23 +16,23 @@ async def test_vehicle_binary_sensors( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - state = hass.states.get("binary_sensor.liability_insured") - entry = entity_registry.async_get("binary_sensor.liability_insured") + state = hass.states.get("binary_sensor.skoda_11zkz3_liability_insured") + entry = entity_registry.async_get("binary_sensor.skoda_11zkz3_liability_insured") assert entry assert state assert entry.unique_id == "11ZKZ3_liability_insured" assert state.state == "off" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Liability Insured" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Skoda 11ZKZ3 Liability insured" assert state.attributes.get(ATTR_ICON) == "mdi:shield-car" assert ATTR_DEVICE_CLASS not in state.attributes - state = hass.states.get("binary_sensor.pending_recall") - entry = entity_registry.async_get("binary_sensor.pending_recall") + state = hass.states.get("binary_sensor.skoda_11zkz3_pending_recall") + entry = entity_registry.async_get("binary_sensor.skoda_11zkz3_pending_recall") assert entry assert state assert entry.unique_id == "11ZKZ3_pending_recall" assert state.state == "off" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pending Recall" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Skoda 11ZKZ3 Pending recall" assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.PROBLEM assert ATTR_ICON not in state.attributes @@ -41,7 +41,7 @@ async def test_vehicle_binary_sensors( assert device_entry assert device_entry.identifiers == {(DOMAIN, "11ZKZ3")} assert device_entry.manufacturer == "Skoda" - assert device_entry.name == "Skoda: 11ZKZ3" + assert device_entry.name == "Skoda 11ZKZ3" assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert device_entry.model == "Citigo" assert ( diff --git a/tests/components/rdw/test_sensor.py b/tests/components/rdw/test_sensor.py index 32d4c368d73..3e7ad7ab89e 100644 --- a/tests/components/rdw/test_sensor.py +++ b/tests/components/rdw/test_sensor.py @@ -21,25 +21,25 @@ async def test_vehicle_sensors( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - state = hass.states.get("sensor.apk_expiration") - entry = entity_registry.async_get("sensor.apk_expiration") + state = hass.states.get("sensor.skoda_11zkz3_apk_expiration") + entry = entity_registry.async_get("sensor.skoda_11zkz3_apk_expiration") assert entry assert state assert entry.unique_id == "11ZKZ3_apk_expiration" assert state.state == "2022-01-04" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "APK Expiration" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Skoda 11ZKZ3 APK expiration" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert ATTR_ICON not in state.attributes assert ATTR_STATE_CLASS not in state.attributes assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes - state = hass.states.get("sensor.ascription_date") - entry = entity_registry.async_get("sensor.ascription_date") + state = hass.states.get("sensor.skoda_11zkz3_ascription_date") + entry = entity_registry.async_get("sensor.skoda_11zkz3_ascription_date") assert entry assert state assert entry.unique_id == "11ZKZ3_ascription_date" assert state.state == "2021-11-04" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Ascription Date" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Skoda 11ZKZ3 Ascription date" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DATE assert ATTR_ICON not in state.attributes assert ATTR_STATE_CLASS not in state.attributes @@ -50,7 +50,7 @@ async def test_vehicle_sensors( assert device_entry assert device_entry.identifiers == {(DOMAIN, "11ZKZ3")} assert device_entry.manufacturer == "Skoda" - assert device_entry.name == "Skoda: 11ZKZ3" + assert device_entry.name == "Skoda 11ZKZ3" assert device_entry.entry_type is dr.DeviceEntryType.SERVICE assert device_entry.model == "Citigo" assert ( From 08ff1b89862594077c68e16e3010b908c7880ad7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 11:05:52 +0200 Subject: [PATCH 2541/3516] Fix flapping system log test (#75111) --- .../components/system_log/__init__.py | 65 +++----- homeassistant/components/zha/core/gateway.py | 12 +- tests/components/network/conftest.py | 6 +- tests/components/system_log/test_init.py | 153 +++++++++--------- tests/conftest.py | 20 +++ 5 files changed, 135 insertions(+), 121 deletions(-) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 343830fe690..b0d538a4ff8 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -1,7 +1,6 @@ """Support for system log.""" from collections import OrderedDict, deque import logging -import queue import re import traceback @@ -9,7 +8,7 @@ import voluptuous as vol from homeassistant import __path__ as HOMEASSISTANT_PATH from homeassistant.components import websocket_api -from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.core import HomeAssistant, ServiceCall, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -56,8 +55,7 @@ SERVICE_WRITE_SCHEMA = vol.Schema( ) -def _figure_out_source(record, call_stack, hass): - paths = [HOMEASSISTANT_PATH[0], hass.config.config_dir] +def _figure_out_source(record, call_stack, paths_re): # If a stack trace exists, extract file names from the entire call stack. # The other case is when a regular "log" is made (without an attached @@ -78,11 +76,10 @@ def _figure_out_source(record, call_stack, hass): # Iterate through the stack call (in reverse) and find the last call from # a file in Home Assistant. Try to figure out where error happened. - paths_re = r"(?:{})/(.*)".format("|".join([re.escape(x) for x in paths])) for pathname in reversed(stack): # Try to match with a file within Home Assistant - if match := re.match(paths_re, pathname[0]): + if match := paths_re.match(pathname[0]): return [match.group(1), pathname[1]] # Ok, we don't know what this is return (record.pathname, record.lineno) @@ -157,26 +154,16 @@ class DedupStore(OrderedDict): return [value.to_dict() for value in reversed(self.values())] -class LogErrorQueueHandler(logging.handlers.QueueHandler): - """Process the log in another thread.""" - - def emit(self, record): - """Emit a log record.""" - try: - self.enqueue(record) - except Exception: # pylint: disable=broad-except - self.handleError(record) - - class LogErrorHandler(logging.Handler): """Log handler for error messages.""" - def __init__(self, hass, maxlen, fire_event): + def __init__(self, hass, maxlen, fire_event, paths_re): """Initialize a new LogErrorHandler.""" super().__init__() self.hass = hass self.records = DedupStore(maxlen=maxlen) self.fire_event = fire_event + self.paths_re = paths_re def emit(self, record): """Save error and warning logs. @@ -189,7 +176,9 @@ class LogErrorHandler(logging.Handler): if not record.exc_info: stack = [(f[0], f[1]) for f in traceback.extract_stack()] - entry = LogEntry(record, stack, _figure_out_source(record, stack, self.hass)) + entry = LogEntry( + record, stack, _figure_out_source(record, stack, self.paths_re) + ) self.records.add_entry(entry) if self.fire_event: self.hass.bus.fire(EVENT_SYSTEM_LOG, entry.to_dict()) @@ -200,29 +189,28 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if (conf := config.get(DOMAIN)) is None: conf = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] - simple_queue: queue.SimpleQueue = queue.SimpleQueue() - queue_handler = LogErrorQueueHandler(simple_queue) - queue_handler.setLevel(logging.WARN) - logging.root.addHandler(queue_handler) - - handler = LogErrorHandler(hass, conf[CONF_MAX_ENTRIES], conf[CONF_FIRE_EVENT]) + hass_path: str = HOMEASSISTANT_PATH[0] + config_dir = hass.config.config_dir + assert config_dir is not None + paths_re = re.compile( + r"(?:{})/(.*)".format("|".join([re.escape(x) for x in (hass_path, config_dir)])) + ) + handler = LogErrorHandler( + hass, conf[CONF_MAX_ENTRIES], conf[CONF_FIRE_EVENT], paths_re + ) + handler.setLevel(logging.WARN) hass.data[DOMAIN] = handler - listener = logging.handlers.QueueListener( - simple_queue, handler, respect_handler_level=True - ) - - listener.start() - @callback - def _async_stop_queue_handler(_) -> None: + def _async_stop_handler(_) -> None: """Cleanup handler.""" - logging.root.removeHandler(queue_handler) - listener.stop() + logging.root.removeHandler(handler) del hass.data[DOMAIN] - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_stop_queue_handler) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_stop_handler) + + logging.root.addHandler(handler) websocket_api.async_register_command(hass, list_errors) @@ -238,13 +226,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: level = service.data[CONF_LEVEL] getattr(logger, level)(service.data[CONF_MESSAGE]) - async def async_shutdown_handler(event): - """Remove logging handler when Home Assistant is shutdown.""" - # This is needed as older logger instances will remain - logging.getLogger().removeHandler(handler) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_shutdown_handler) - hass.services.async_register( DOMAIN, SERVICE_CLEAR, async_service_handler, schema=SERVICE_CLEAR_SCHEMA ) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 9efb6e99550..f2fd226249b 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -8,6 +8,7 @@ from datetime import timedelta from enum import Enum import itertools import logging +import re import time import traceback from typing import TYPE_CHECKING, Any, NamedTuple, Union @@ -20,6 +21,7 @@ import zigpy.endpoint import zigpy.group from zigpy.types.named import EUI64 +from homeassistant import __path__ as HOMEASSISTANT_PATH from homeassistant.components.system_log import LogEntry, _figure_out_source from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -732,7 +734,15 @@ class LogRelayHandler(logging.Handler): if record.levelno >= logging.WARN and not record.exc_info: stack = [f for f, _, _, _ in traceback.extract_stack()] - entry = LogEntry(record, stack, _figure_out_source(record, stack, self.hass)) + hass_path: str = HOMEASSISTANT_PATH[0] + config_dir = self.hass.config.config_dir + assert config_dir is not None + paths_re = re.compile( + r"(?:{})/(.*)".format( + "|".join([re.escape(x) for x in (hass_path, config_dir)]) + ) + ) + entry = LogEntry(record, stack, _figure_out_source(record, stack, paths_re)) async_dispatcher_send( self.hass, ZHA_GW_MSG, diff --git a/tests/components/network/conftest.py b/tests/components/network/conftest.py index 9c1bf232d7b..8b1b383ae42 100644 --- a/tests/components/network/conftest.py +++ b/tests/components/network/conftest.py @@ -6,4 +6,8 @@ import pytest @pytest.fixture(autouse=True) def mock_get_source_ip(): """Override mock of network util's async_get_source_ip.""" - return + + +@pytest.fixture(autouse=True) +def mock_network(): + """Override mock of network util's async_get_adapters.""" diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index d558d690536..6304e0ea7cf 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -1,10 +1,11 @@ """Test system log component.""" -import asyncio -import logging -import queue -from unittest.mock import MagicMock, patch +from __future__ import annotations -import pytest +import asyncio +from collections.abc import Awaitable +import logging +from typing import Any +from unittest.mock import MagicMock, patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import system_log @@ -16,28 +17,6 @@ _LOGGER = logging.getLogger("test_logger") BASIC_CONFIG = {"system_log": {"max_entries": 2}} -@pytest.fixture -def simple_queue(): - """Fixture that get the queue.""" - simple_queue_fixed = queue.SimpleQueue() - with patch( - "homeassistant.components.system_log.queue.SimpleQueue", - return_value=simple_queue_fixed, - ): - yield simple_queue_fixed - - -async def _async_block_until_queue_empty(hass, sq): - # Unfortunately we are stuck with polling - await hass.async_block_till_done() - while not sq.empty(): - await asyncio.sleep(0.01) - hass.data[system_log.DOMAIN].acquire() - hass.data[system_log.DOMAIN].release() - await hass.async_block_till_done() - await hass.async_block_till_done() - - async def get_error_log(hass_ws_client): """Fetch all entries from system_log via the API.""" client = await hass_ws_client() @@ -81,66 +60,97 @@ def assert_log(log, exception, message, level): assert "timestamp" in log +class WatchLogErrorHandler(system_log.LogErrorHandler): + """WatchLogErrorHandler that watches for a message.""" + + instances: list[WatchLogErrorHandler] = [] + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize HASSQueueListener.""" + super().__init__(*args, **kwargs) + self.watch_message: str | None = None + self.watch_event: asyncio.Event | None = asyncio.Event() + WatchLogErrorHandler.instances.append(self) + + def add_watcher(self, match: str) -> Awaitable: + """Add a watcher.""" + self.watch_event = asyncio.Event() + self.watch_message = match + return self.watch_event.wait() + + def handle(self, record: logging.LogRecord) -> None: + """Handle a logging record.""" + super().handle(record) + if record.message in self.watch_message: + self.watch_event.set() + + def get_frame(name): """Get log stack frame.""" return (name, 5, None, None) -async def test_normal_logs(hass, simple_queue, hass_ws_client): +async def async_setup_system_log(hass, config) -> WatchLogErrorHandler: + """Set up the system_log component.""" + WatchLogErrorHandler.instances = [] + with patch( + "homeassistant.components.system_log.LogErrorHandler", WatchLogErrorHandler + ): + await async_setup_component(hass, system_log.DOMAIN, config) + await hass.async_block_till_done() + + assert len(WatchLogErrorHandler.instances) == 1 + return WatchLogErrorHandler.instances.pop() + + +async def test_normal_logs(hass, hass_ws_client): """Test that debug and info are not logged.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.debug("debug") _LOGGER.info("info") - await _async_block_until_queue_empty(hass, simple_queue) # Assert done by get_error_log logs = await get_error_log(hass_ws_client) assert len([msg for msg in logs if msg["level"] in ("DEBUG", "INFO")]) == 0 -async def test_exception(hass, simple_queue, hass_ws_client): +async def test_exception(hass, hass_ws_client): """Test that exceptions are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() _generate_and_log_exception("exception message", "log message") - await _async_block_until_queue_empty(hass, simple_queue) log = find_log(await get_error_log(hass_ws_client), "ERROR") assert log is not None assert_log(log, "exception message", "log message", "ERROR") -async def test_warning(hass, simple_queue, hass_ws_client): +async def test_warning(hass, hass_ws_client): """Test that warning are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.warning("warning message") - await _async_block_until_queue_empty(hass, simple_queue) log = find_log(await get_error_log(hass_ws_client), "WARNING") assert_log(log, "", "warning message", "WARNING") -async def test_error(hass, simple_queue, hass_ws_client): +async def test_error(hass, hass_ws_client): """Test that errors are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() _LOGGER.error("error message") - await _async_block_until_queue_empty(hass, simple_queue) log = find_log(await get_error_log(hass_ws_client), "ERROR") assert_log(log, "", "error message", "ERROR") -async def test_config_not_fire_event(hass, simple_queue): +async def test_config_not_fire_event(hass): """Test that errors are not posted as events with default config.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - events = [] @callback @@ -150,50 +160,49 @@ async def test_config_not_fire_event(hass, simple_queue): hass.bus.async_listen(system_log.EVENT_SYSTEM_LOG, event_listener) - _LOGGER.error("error message") - await _async_block_until_queue_empty(hass, simple_queue) + await hass.async_block_till_done() + await hass.async_block_till_done() assert len(events) == 0 -async def test_error_posted_as_event(hass, simple_queue): +async def test_error_posted_as_event(hass): """Test that error are posted as events.""" - await async_setup_component( - hass, system_log.DOMAIN, {"system_log": {"max_entries": 2, "fire_event": True}} + watcher = await async_setup_system_log( + hass, {"system_log": {"max_entries": 2, "fire_event": True}} ) - await hass.async_block_till_done() + wait_empty = watcher.add_watcher("error message") events = async_capture_events(hass, system_log.EVENT_SYSTEM_LOG) _LOGGER.error("error message") - await _async_block_until_queue_empty(hass, simple_queue) + await wait_empty + await hass.async_block_till_done() + await hass.async_block_till_done() assert len(events) == 1 assert_log(events[0].data, "", "error message", "ERROR") -async def test_critical(hass, simple_queue, hass_ws_client): +async def test_critical(hass, hass_ws_client): """Test that critical are logged and retrieved correctly.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() _LOGGER.critical("critical message") - await _async_block_until_queue_empty(hass, simple_queue) log = find_log(await get_error_log(hass_ws_client), "CRITICAL") assert_log(log, "", "critical message", "CRITICAL") -async def test_remove_older_logs(hass, simple_queue, hass_ws_client): +async def test_remove_older_logs(hass, hass_ws_client): """Test that older logs are rotated out.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.error("error message 1") _LOGGER.error("error message 2") _LOGGER.error("error message 3") - await _async_block_until_queue_empty(hass, simple_queue) - + await hass.async_block_till_done() log = await get_error_log(hass_ws_client) assert_log(log[0], "", "error message 3", "ERROR") assert_log(log[1], "", "error message 2", "ERROR") @@ -204,16 +213,14 @@ def log_msg(nr=2): _LOGGER.error("error message %s", nr) -async def test_dedupe_logs(hass, simple_queue, hass_ws_client): +async def test_dedupe_logs(hass, hass_ws_client): """Test that duplicate log entries are dedupe.""" - await async_setup_component(hass, system_log.DOMAIN, {}) + await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.error("error message 1") log_msg() log_msg("2-2") _LOGGER.error("error message 3") - await _async_block_until_queue_empty(hass, simple_queue) log = await get_error_log(hass_ws_client) assert_log(log[0], "", "error message 3", "ERROR") @@ -221,8 +228,6 @@ async def test_dedupe_logs(hass, simple_queue, hass_ws_client): assert_log(log[1], "", ["error message 2", "error message 2-2"], "ERROR") log_msg() - await _async_block_until_queue_empty(hass, simple_queue) - log = await get_error_log(hass_ws_client) assert_log(log[0], "", ["error message 2", "error message 2-2"], "ERROR") assert log[0]["timestamp"] > log[0]["first_occurred"] @@ -231,7 +236,6 @@ async def test_dedupe_logs(hass, simple_queue, hass_ws_client): log_msg("2-4") log_msg("2-5") log_msg("2-6") - await _async_block_until_queue_empty(hass, simple_queue) log = await get_error_log(hass_ws_client) assert_log( @@ -248,17 +252,14 @@ async def test_dedupe_logs(hass, simple_queue, hass_ws_client): ) -async def test_clear_logs(hass, simple_queue, hass_ws_client): +async def test_clear_logs(hass, hass_ws_client): """Test that the log can be cleared via a service call.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.error("error message") - await _async_block_until_queue_empty(hass, simple_queue) await hass.services.async_call(system_log.DOMAIN, system_log.SERVICE_CLEAR, {}) - await _async_block_until_queue_empty(hass, simple_queue) - + await hass.async_block_till_done() # Assert done by get_error_log await get_error_log(hass_ws_client) @@ -309,19 +310,17 @@ async def test_write_choose_level(hass): assert logger.method_calls[0] == ("debug", ("test_message",)) -async def test_unknown_path(hass, simple_queue, hass_ws_client): +async def test_unknown_path(hass, hass_ws_client): """Test error logged from unknown path.""" await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) await hass.async_block_till_done() - _LOGGER.findCaller = MagicMock(return_value=("unknown_path", 0, None, None)) _LOGGER.error("error message") - await _async_block_until_queue_empty(hass, simple_queue) log = (await get_error_log(hass_ws_client))[0] assert log["source"] == ["unknown_path", 0] -async def async_log_error_from_test_path(hass, path, sq): +async def async_log_error_from_test_path(hass, path, watcher): """Log error while mocking the path.""" call_path = "internal_path.py" with patch.object( @@ -337,34 +336,34 @@ async def async_log_error_from_test_path(hass, path, sq): ] ), ): + wait_empty = watcher.add_watcher("error message") _LOGGER.error("error message") - await _async_block_until_queue_empty(hass, sq) + await wait_empty -async def test_homeassistant_path(hass, simple_queue, hass_ws_client): +async def test_homeassistant_path(hass, hass_ws_client): """Test error logged from Home Assistant path.""" - await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) - await hass.async_block_till_done() with patch( "homeassistant.components.system_log.HOMEASSISTANT_PATH", new=["venv_path/homeassistant"], ): + watcher = await async_setup_system_log(hass, BASIC_CONFIG) await async_log_error_from_test_path( - hass, "venv_path/homeassistant/component/component.py", simple_queue + hass, "venv_path/homeassistant/component/component.py", watcher ) log = (await get_error_log(hass_ws_client))[0] assert log["source"] == ["component/component.py", 5] -async def test_config_path(hass, simple_queue, hass_ws_client): +async def test_config_path(hass, hass_ws_client): """Test error logged from config path.""" - await async_setup_component(hass, system_log.DOMAIN, BASIC_CONFIG) - await hass.async_block_till_done() with patch.object(hass.config, "config_dir", new="config"): + watcher = await async_setup_system_log(hass, BASIC_CONFIG) + await async_log_error_from_test_path( - hass, "config/custom_component/test.py", simple_queue + hass, "config/custom_component/test.py", watcher ) log = (await get_error_log(hass_ws_client))[0] assert log["source"] == ["custom_component/test.py", 5] diff --git a/tests/conftest.py b/tests/conftest.py index 97b1a959d2c..4b2852e94fc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,6 +22,7 @@ from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY from homeassistant.auth.models import Credentials from homeassistant.auth.providers import homeassistant, legacy_api_password from homeassistant.components import mqtt, recorder +from homeassistant.components.network.models import Adapter, IPv4ConfiguredAddress from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, TYPE_AUTH_OK, @@ -644,6 +645,25 @@ async def mqtt_mock_entry_with_yaml_config(hass, mqtt_client_mock, mqtt_config): yield _setup_mqtt_entry +@pytest.fixture(autouse=True) +def mock_network(): + """Mock network.""" + mock_adapter = Adapter( + name="eth0", + index=0, + enabled=True, + auto=True, + default=True, + ipv4=[IPv4ConfiguredAddress(address="10.10.10.10", network_prefix=24)], + ipv6=[], + ) + with patch( + "homeassistant.components.network.network.async_load_adapters", + return_value=[mock_adapter], + ): + yield + + @pytest.fixture(autouse=True) def mock_get_source_ip(): """Mock network util's async_get_source_ip.""" From c9df5888c2daab6d0287bf5f31cdf56dde3173b7 Mon Sep 17 00:00:00 2001 From: mkmer Date: Thu, 14 Jul 2022 05:09:27 -0400 Subject: [PATCH 2542/3516] Add Aladdin Connect wifi_rssi and battery_level sensors (#74258) --- .../components/aladdin_connect/__init__.py | 2 +- .../components/aladdin_connect/cover.py | 13 +- .../components/aladdin_connect/sensor.py | 115 ++++++++++++++++++ 3 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/aladdin_connect/sensor.py diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index 1a28a03cf05..036f5364ef5 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -16,7 +16,7 @@ from .const import DOMAIN _LOGGER: Final = logging.getLogger(__name__) -PLATFORMS: list[Platform] = [Platform.COVER] +PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 9c03cd322b6..d06ebebd076 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -24,6 +24,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady 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 @@ -87,8 +88,18 @@ class AladdinDevice(CoverEntity): self._device_id = device["device_id"] self._number = device["door_number"] - self._attr_name = device["name"] + self._name = device["name"] self._attr_unique_id = f"{self._device_id}-{self._number}" + self._attr_has_entity_name = True + + @property + def device_info(self) -> DeviceInfo | None: + """Device information for Aladdin Connect cover.""" + return DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + name=self._name, + manufacturer="Overhead Door", + ) async def async_added_to_hass(self) -> None: """Connect Aladdin Connect to the cloud.""" diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py new file mode 100644 index 00000000000..d9783d0f61d --- /dev/null +++ b/homeassistant/components/aladdin_connect/sensor.py @@ -0,0 +1,115 @@ +"""Support for Aladdin Connect Garage Door sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import cast + +from AIOAladdinConnect import AladdinConnectClient + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .model import DoorDevice + + +@dataclass +class AccSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable + + +@dataclass +class AccSensorEntityDescription( + SensorEntityDescription, AccSensorEntityDescriptionMixin +): + """Describes AladdinConnect sensor entity.""" + + +SENSORS: tuple[AccSensorEntityDescription, ...] = ( + AccSensorEntityDescription( + key="battery_level", + name="Battery level", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=AladdinConnectClient.get_battery_status, + ), + AccSensorEntityDescription( + key="rssi", + name="Wi-Fi RSSI", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_registry_enabled_default=False, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + value_fn=AladdinConnectClient.get_rssi_status, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Aladdin Connect sensor devices.""" + + acc: AladdinConnectClient = hass.data[DOMAIN][entry.entry_id] + + entities = [] + doors = await acc.get_doors() + + for door in doors: + entities.extend( + [AladdinConnectSensor(acc, door, description) for description in SENSORS] + ) + + async_add_entities(entities) + + +class AladdinConnectSensor(SensorEntity): + """A sensor implementation for Aladdin Connect devices.""" + + _device: AladdinConnectSensor + entity_description: AccSensorEntityDescription + + def __init__( + self, + acc: AladdinConnectClient, + device: DoorDevice, + description: AccSensorEntityDescription, + ) -> None: + """Initialize a sensor for an Abode device.""" + self._device_id = device["device_id"] + self._number = device["door_number"] + self._name = device["name"] + self._acc = acc + self.entity_description = description + self._attr_unique_id = f"{self._device_id}-{self._number}-{description.key}" + self._attr_has_entity_name = True + + @property + def device_info(self) -> DeviceInfo | None: + """Device information for Aladdin Connect sensors.""" + return DeviceInfo( + identifiers={(DOMAIN, self._device_id)}, + name=self._name, + manufacturer="Overhead Door", + ) + + @property + def native_value(self) -> float | None: + """Return the state of the sensor.""" + return cast( + float, + self.entity_description.value_fn(self._acc, self._device_id, self._number), + ) From 169264db66b97209c8b811d0d8fa70ae8f9cef7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huryn?= Date: Thu, 14 Jul 2022 11:21:01 +0200 Subject: [PATCH 2543/3516] Fix Blebox light scenes (#75106) * Bug fix for light platform, when async_turn_on recieves multiple keys. * Changes according to @MartinHjelmare suggestion. * Moved effect set call in BleBoxLightEntity.async_turn_on method. * Added tests for effect in light platform. Added ValueError raise if effect not in effect list. * Removed duplicated line from test as @MartinHjelmare suggested. --- homeassistant/components/blebox/light.py | 15 +++++--- tests/components/blebox/test_light.py | 46 ++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index 2bb6bc91762..a2ad51cfc9c 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -159,15 +159,20 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity): else: value = feature.apply_brightness(value, brightness) + try: + await self._feature.async_on(value) + except ValueError as exc: + raise ValueError( + f"Turning on '{self.name}' failed: Bad value {value}" + ) from exc + if effect is not None: - effect_value = self.effect_list.index(effect) - await self._feature.async_api_command("effect", effect_value) - else: try: - await self._feature.async_on(value) + effect_value = self.effect_list.index(effect) + await self._feature.async_api_command("effect", effect_value) except ValueError as exc: raise ValueError( - f"Turning on '{self.name}' failed: Bad value {value}" + f"Turning on with effect '{self.name}' failed: {effect} not in effect list." ) from exc async def async_turn_off(self, **kwargs): diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index f61496714fb..7afb78e5b03 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -7,6 +7,7 @@ import pytest from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_EFFECT, ATTR_RGBW_COLOR, ATTR_SUPPORTED_COLOR_MODES, ColorMode, @@ -524,3 +525,48 @@ async def test_turn_on_failure(feature, hass, config, caplog): assert f"Turning on '{feature_mock.full_name}' failed: Bad value 123" in str( info.value ) + + +async def test_wlightbox_on_effect(wlightbox, hass, config): + """Test light on.""" + + feature_mock, entity_id = wlightbox + + def initial_update(): + feature_mock.is_on = False + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + def turn_on(value): + feature_mock.is_on = True + feature_mock.effect = "POLICE" + + feature_mock.async_on = AsyncMock(side_effect=turn_on) + + with pytest.raises(ValueError) as info: + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {"entity_id": entity_id, ATTR_EFFECT: "NOT IN LIST"}, + blocking=True, + ) + + assert ( + f"Turning on with effect '{feature_mock.full_name}' failed: NOT IN LIST not in effect list." + in str(info.value) + ) + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {"entity_id": entity_id, ATTR_EFFECT: "POLICE"}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_EFFECT] == "POLICE" From 3bccac9949eadc4bcb088e0b12df2d7c88f2e7fd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jul 2022 11:37:59 +0200 Subject: [PATCH 2544/3516] Verisure config flow cleanups (#75144) --- .../components/verisure/config_flow.py | 37 +- tests/components/verisure/conftest.py | 50 +++ tests/components/verisure/test_config_flow.py | 411 +++++++++--------- 3 files changed, 263 insertions(+), 235 deletions(-) create mode 100644 tests/components/verisure/conftest.py diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py index 41687dbc6a4..119a9250736 100644 --- a/homeassistant/components/verisure/config_flow.py +++ b/homeassistant/components/verisure/config_flow.py @@ -34,8 +34,8 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): email: str entry: ConfigEntry - installations: dict[str, str] password: str + verisure: Verisure @staticmethod @callback @@ -50,11 +50,13 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: - verisure = Verisure( + self.email = user_input[CONF_EMAIL] + self.password = user_input[CONF_PASSWORD] + self.verisure = Verisure( username=user_input[CONF_EMAIL], password=user_input[CONF_PASSWORD] ) try: - await self.hass.async_add_executor_job(verisure.login) + await self.hass.async_add_executor_job(self.verisure.login) except VerisureLoginError as ex: LOGGER.debug("Could not log in to Verisure, %s", ex) errors["base"] = "invalid_auth" @@ -62,13 +64,6 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): LOGGER.debug("Unexpected response from Verisure, %s", ex) errors["base"] = "unknown" else: - self.email = user_input[CONF_EMAIL] - self.password = user_input[CONF_PASSWORD] - self.installations = { - inst["giid"]: f"{inst['alias']} ({inst['street']})" - for inst in verisure.installations - } - return await self.async_step_installation() return self.async_show_form( @@ -86,22 +81,26 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Select Verisure installation to add.""" - if len(self.installations) == 1: - user_input = {CONF_GIID: list(self.installations)[0]} + installations = { + inst["giid"]: f"{inst['alias']} ({inst['street']})" + for inst in self.verisure.installations or [] + } if user_input is None: - return self.async_show_form( - step_id="installation", - data_schema=vol.Schema( - {vol.Required(CONF_GIID): vol.In(self.installations)} - ), - ) + if len(installations) != 1: + return self.async_show_form( + step_id="installation", + data_schema=vol.Schema( + {vol.Required(CONF_GIID): vol.In(installations)} + ), + ) + user_input = {CONF_GIID: list(installations)[0]} await self.async_set_unique_id(user_input[CONF_GIID]) self._abort_if_unique_id_configured() return self.async_create_entry( - title=self.installations[user_input[CONF_GIID]], + title=installations[user_input[CONF_GIID]], data={ CONF_EMAIL: self.email, CONF_PASSWORD: self.password, diff --git a/tests/components/verisure/conftest.py b/tests/components/verisure/conftest.py new file mode 100644 index 00000000000..f91215866d8 --- /dev/null +++ b/tests/components/verisure/conftest.py @@ -0,0 +1,50 @@ +"""Fixtures for Verisure integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant.components.verisure.const import CONF_GIID, DOMAIN +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + domain=DOMAIN, + unique_id="12345", + data={ + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_GIID: "12345", + CONF_PASSWORD: "SuperS3cr3t!", + }, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.verisure.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup + + +@pytest.fixture +def mock_verisure_config_flow() -> Generator[None, MagicMock, None]: + """Return a mocked Tailscale client.""" + with patch( + "homeassistant.components.verisure.config_flow.Verisure", autospec=True + ) as verisure_mock: + verisure = verisure_mock.return_value + verisure.login.return_value = True + verisure.installations = [ + {"giid": "12345", "alias": "ascending", "street": "12345th street"}, + {"giid": "54321", "alias": "descending", "street": "54321th street"}, + ] + yield verisure diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index 03fa0fa82cc..d957709c878 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Verisure config flow.""" from __future__ import annotations -from unittest.mock import PropertyMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import pytest from verisure import Error as VerisureError, LoginError as VerisureLoginError @@ -21,151 +21,151 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -TEST_INSTALLATIONS = [ - {"giid": "12345", "alias": "ascending", "street": "12345th street"}, - {"giid": "54321", "alias": "descending", "street": "54321th street"}, -] -TEST_INSTALLATION = [TEST_INSTALLATIONS[0]] - -async def test_full_user_flow_single_installation(hass: HomeAssistant) -> None: +async def test_full_user_flow_single_installation( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, +) -> None: """Test a full user initiated configuration flow with a single installation.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM - assert result["errors"] == {} + assert result.get("step_id") == "user" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result - with patch( - "homeassistant.components.verisure.config_flow.Verisure", - ) as mock_verisure, patch( - "homeassistant.components.verisure.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - type(mock_verisure.return_value).installations = PropertyMock( - return_value=TEST_INSTALLATION - ) - mock_verisure.login.return_value = True + mock_verisure_config_flow.installations = [ + mock_verisure_config_flow.installations[0] + ] - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "SuperS3cr3t!", - }, - ) - await hass.async_block_till_done() + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "ascending (12345th street)" - assert result2["data"] == { + assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("title") == "ascending (12345th street)" + assert result2.get("data") == { CONF_GIID: "12345", CONF_EMAIL: "verisure_my_pages@example.com", CONF_PASSWORD: "SuperS3cr3t!", } - assert len(mock_verisure.mock_calls) == 2 + assert len(mock_verisure_config_flow.login.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_full_user_flow_multiple_installations(hass: HomeAssistant) -> None: +async def test_full_user_flow_multiple_installations( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, +) -> None: """Test a full user initiated configuration flow with multiple installations.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["step_id"] == "user" - assert result["type"] == FlowResultType.FORM - assert result["errors"] == {} + assert result.get("step_id") == "user" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result - with patch( - "homeassistant.components.verisure.config_flow.Verisure", - ) as mock_verisure: - type(mock_verisure.return_value).installations = PropertyMock( - return_value=TEST_INSTALLATIONS - ) - mock_verisure.login.return_value = True + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "SuperS3cr3t!", - }, - ) - await hass.async_block_till_done() + assert result2.get("step_id") == "installation" + assert result2.get("type") == FlowResultType.FORM + assert result2.get("errors") is None + assert "flow_id" in result2 - assert result2["step_id"] == "installation" - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] is None + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], {"giid": "54321"} + ) + await hass.async_block_till_done() - with patch( - "homeassistant.components.verisure.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], {"giid": "54321"} - ) - await hass.async_block_till_done() - - assert result3["type"] == FlowResultType.CREATE_ENTRY - assert result3["title"] == "descending (54321th street)" - assert result3["data"] == { + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "descending (54321th street)" + assert result3.get("data") == { CONF_GIID: "54321", CONF_EMAIL: "verisure_my_pages@example.com", CONF_PASSWORD: "SuperS3cr3t!", } - assert len(mock_verisure.mock_calls) == 2 + assert len(mock_verisure_config_flow.login.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_invalid_login(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "side_effect,error", + [ + (VerisureLoginError, "invalid_auth"), + (VerisureError, "unknown"), + ], +) +async def test_verisure_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, + side_effect: Exception, + error: str, +) -> None: """Test a flow with an invalid Verisure My Pages login.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch( - "homeassistant.components.verisure.config_flow.Verisure.login", - side_effect=VerisureLoginError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "SuperS3cr3t!", - }, - ) - await hass.async_block_till_done() + assert "flow_id" in result - assert result2["type"] == FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "invalid_auth"} - - -async def test_unknown_error(hass: HomeAssistant) -> None: - """Test a flow with an invalid Verisure My Pages login.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + mock_verisure_config_flow.login.side_effect = side_effect + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, ) + await hass.async_block_till_done() - with patch( - "homeassistant.components.verisure.config_flow.Verisure.login", - side_effect=VerisureError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "SuperS3cr3t!", - }, - ) - await hass.async_block_till_done() + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "user" + assert result2.get("errors") == {"base": error} + assert "flow_id" in result2 - assert result2["type"] == FlowResultType.FORM - assert result2["step_id"] == "user" - assert result2["errors"] == {"base": "unknown"} + mock_verisure_config_flow.login.side_effect = None + mock_verisure_config_flow.installations = [ + mock_verisure_config_flow.installations[0] + ] + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "ascending (12345th street)" + assert result3.get("data") == { + CONF_GIID: "12345", + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_PASSWORD: "SuperS3cr3t!", + } + + assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_setup_entry.mock_calls) == 1 async def test_dhcp(hass: HomeAssistant) -> None: @@ -178,144 +178,121 @@ async def test_dhcp(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_DHCP}, ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "user" -async def test_reauth_flow(hass: HomeAssistant) -> None: +async def test_reauth_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: """Test a reauthentication flow.""" - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="12345", - data={ - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_GIID: "12345", - CONF_PASSWORD: "SuperS3cr3t!", - }, - ) - entry.add_to_hass(hass) + mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={ "source": config_entries.SOURCE_REAUTH, - "unique_id": entry.unique_id, - "entry_id": entry.entry_id, + "unique_id": mock_config_entry.unique_id, + "entry_id": mock_config_entry.entry_id, }, - data=entry.data, + data=mock_config_entry.data, ) - assert result["step_id"] == "reauth_confirm" - assert result["type"] == FlowResultType.FORM - assert result["errors"] == {} + assert result.get("step_id") == "reauth_confirm" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result - with patch( - "homeassistant.components.verisure.config_flow.Verisure.login", - return_value=True, - ) as mock_verisure, patch( - "homeassistant.components.verisure.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "correct horse battery staple", - }, - ) - await hass.async_block_till_done() + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "correct horse battery staple", + }, + ) + await hass.async_block_till_done() - assert result2["type"] == FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" - assert entry.data == { + assert result2.get("type") == FlowResultType.ABORT + assert result2.get("reason") == "reauth_successful" + assert mock_config_entry.data == { CONF_GIID: "12345", CONF_EMAIL: "verisure_my_pages@example.com", CONF_PASSWORD: "correct horse battery staple", } - assert len(mock_verisure.mock_calls) == 1 + assert len(mock_verisure_config_flow.login.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_reauth_flow_invalid_login(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "side_effect,error", + [ + (VerisureLoginError, "invalid_auth"), + (VerisureError, "unknown"), + ], +) +async def test_reauth_flow_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, + mock_config_entry: MockConfigEntry, + side_effect: Exception, + error: str, +) -> None: """Test a reauthentication flow.""" - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="12345", - data={ - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_GIID: "12345", - CONF_PASSWORD: "SuperS3cr3t!", - }, - ) - entry.add_to_hass(hass) + mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={ "source": config_entries.SOURCE_REAUTH, - "unique_id": entry.unique_id, - "entry_id": entry.entry_id, + "unique_id": mock_config_entry.unique_id, + "entry_id": mock_config_entry.entry_id, }, - data=entry.data, + data=mock_config_entry.data, ) - with patch( - "homeassistant.components.verisure.config_flow.Verisure.login", - side_effect=VerisureLoginError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "WrOngP4ssw0rd!", - }, - ) - await hass.async_block_till_done() + assert "flow_id" in result - assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == {"base": "invalid_auth"} - - -async def test_reauth_flow_unknown_error(hass: HomeAssistant) -> None: - """Test a reauthentication flow, with an unknown error happening.""" - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="12345", - data={ - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_GIID: "12345", - CONF_PASSWORD: "SuperS3cr3t!", + mock_verisure_config_flow.login.side_effect = side_effect + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "WrOngP4ssw0rd!", }, ) - entry.add_to_hass(hass) + await hass.async_block_till_done() - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "unique_id": entry.unique_id, - "entry_id": entry.entry_id, + assert result2.get("step_id") == "reauth_confirm" + assert result2.get("type") == FlowResultType.FORM + assert result2.get("errors") == {"base": error} + assert "flow_id" in result2 + + mock_verisure_config_flow.login.side_effect = None + mock_verisure_config_flow.installations = [ + mock_verisure_config_flow.installations[0] + ] + + await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "correct horse battery staple", }, - data=entry.data, ) + await hass.async_block_till_done() - with patch( - "homeassistant.components.verisure.config_flow.Verisure.login", - side_effect=VerisureError, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "WrOngP4ssw0rd!", - }, - ) - await hass.async_block_till_done() + assert mock_config_entry.data == { + CONF_GIID: "12345", + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_PASSWORD: "correct horse battery staple", + } - assert result2["step_id"] == "reauth_confirm" - assert result2["type"] == FlowResultType.FORM - assert result2["errors"] == {"base": "unknown"} + assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_setup_entry.mock_calls) == 1 @pytest.mark.parametrize( @@ -362,16 +339,17 @@ async def test_options_flow( result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "init" + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "init" + assert "flow_id" in result result = await hass.config_entries.options.async_configure( result["flow_id"], user_input=input, ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"] == output + assert result.get("type") == FlowResultType.CREATE_ENTRY + assert result.get("data") == output async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: @@ -392,9 +370,10 @@ async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "init" - assert result["errors"] == {} + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "init" + assert result.get("errors") == {} + assert "flow_id" in result result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -404,6 +383,6 @@ async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: }, ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "init" - assert result["errors"] == {"base": "code_format_mismatch"} + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "init" + assert result.get("errors") == {"base": "code_format_mismatch"} From 51c17197c53ac633de4cfd9569e17bde7acc1f2c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Jul 2022 11:40:10 +0200 Subject: [PATCH 2545/3516] Remove nzbget from mypy ignore list (#75158) --- homeassistant/components/nzbget/coordinator.py | 12 ++++++++++-- mypy.ini | 12 ------------ script/hassfest/mypy_config.py | 4 ---- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/nzbget/coordinator.py b/homeassistant/components/nzbget/coordinator.py index 5851bb21b41..c037619d31b 100644 --- a/homeassistant/components/nzbget/coordinator.py +++ b/homeassistant/components/nzbget/coordinator.py @@ -1,6 +1,8 @@ """Provides the NZBGet DataUpdateCoordinator.""" +from collections.abc import Mapping from datetime import timedelta import logging +from typing import Any from async_timeout import timeout from pynzbgetapi import NZBGetAPI, NZBGetAPIException @@ -25,7 +27,13 @@ _LOGGER = logging.getLogger(__name__) class NZBGetDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching NZBGet data.""" - def __init__(self, hass: HomeAssistant, *, config: dict, options: dict) -> None: + def __init__( + self, + hass: HomeAssistant, + *, + config: Mapping[str, Any], + options: Mapping[str, Any], + ) -> None: """Initialize global NZBGet data updater.""" self.nzbget = NZBGetAPI( config[CONF_HOST], @@ -37,7 +45,7 @@ class NZBGetDataUpdateCoordinator(DataUpdateCoordinator): ) self._completed_downloads_init = False - self._completed_downloads = {} + self._completed_downloads = set[tuple]() update_interval = timedelta(seconds=options[CONF_SCAN_INTERVAL]) diff --git a/mypy.ini b/mypy.ini index b973b959213..1e6741eccd1 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2689,18 +2689,6 @@ ignore_errors = true [mypy-homeassistant.components.minecraft_server.sensor] ignore_errors = true -[mypy-homeassistant.components.nzbget] -ignore_errors = true - -[mypy-homeassistant.components.nzbget.config_flow] -ignore_errors = true - -[mypy-homeassistant.components.nzbget.coordinator] -ignore_errors = true - -[mypy-homeassistant.components.nzbget.switch] -ignore_errors = true - [mypy-homeassistant.components.onvif.base] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 87ace51a3d6..64260b90033 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -27,10 +27,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.minecraft_server", "homeassistant.components.minecraft_server.helpers", "homeassistant.components.minecraft_server.sensor", - "homeassistant.components.nzbget", - "homeassistant.components.nzbget.config_flow", - "homeassistant.components.nzbget.coordinator", - "homeassistant.components.nzbget.switch", "homeassistant.components.onvif.base", "homeassistant.components.onvif.binary_sensor", "homeassistant.components.onvif.camera", From b60f6c7cdd4d51ca9f695667be6b9bac70dbebc0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jul 2022 11:50:21 +0200 Subject: [PATCH 2546/3516] Bump pip_check (#75164) --- .github/workflows/ci.yaml | 4 ++-- script/pip_check | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 573445a4d2e..29527383bab 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,8 +20,8 @@ on: type: boolean env: - CACHE_VERSION: 0 - PIP_CACHE_VERSION: 0 + CACHE_VERSION: 1 + PIP_CACHE_VERSION: 1 HA_SHORT_VERSION: 2022.8 DEFAULT_PYTHON: 3.9 PRE_COMMIT_CACHE: ~/.cache/pre-commit diff --git a/script/pip_check b/script/pip_check index f29ea5a5dd0..6f48ce15bb1 100755 --- a/script/pip_check +++ b/script/pip_check @@ -3,7 +3,7 @@ PIP_CACHE=$1 # Number of existing dependency conflicts # Update if a PR resolve one! -DEPENDENCY_CONFLICTS=6 +DEPENDENCY_CONFLICTS=7 PIP_CHECK=$(pip check --cache-dir=$PIP_CACHE) LINE_COUNT=$(echo "$PIP_CHECK" | wc -l) From debd475a6dca085535f758369a3e4e3cb6efa7c2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Jul 2022 11:55:43 +0200 Subject: [PATCH 2547/3516] Remove onvif from mypy ignore list (#75162) --- homeassistant/components/onvif/base.py | 34 ++++++++++++------- .../components/onvif/binary_sensor.py | 8 +++-- homeassistant/components/onvif/button.py | 6 ++-- homeassistant/components/onvif/camera.py | 17 ++++++---- homeassistant/components/onvif/device.py | 24 ++++++++----- homeassistant/components/onvif/sensor.py | 6 ++-- mypy.ini | 15 -------- script/hassfest/mypy_config.py | 5 --- 8 files changed, 56 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/onvif/base.py b/homeassistant/components/onvif/base.py index 93fd6a26b35..f40b6173a3b 100644 --- a/homeassistant/components/onvif/base.py +++ b/homeassistant/components/onvif/base.py @@ -1,42 +1,50 @@ """Base classes for ONVIF entities.""" +from __future__ import annotations + from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN from .device import ONVIFDevice -from .models import Profile class ONVIFBaseEntity(Entity): """Base class common to all ONVIF entities.""" - def __init__(self, device: ONVIFDevice, profile: Profile = None) -> None: + def __init__(self, device: ONVIFDevice) -> None: """Initialize the ONVIF entity.""" self.device: ONVIFDevice = device - self.profile: Profile = profile @property def available(self): """Return True if device is available.""" return self.device.available + @property + def mac_or_serial(self) -> str: + """Return MAC or serial, for unique_id generation. + + 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 + adopted an either/or approach in the config entry setup, and can + guarantee that one or the other will be populated. + See: https://github.com/home-assistant/core/issues/35883 + """ + return ( + self.device.info.mac + or self.device.info.serial_number # type:ignore[return-value] + ) + @property def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - connections = None + connections: set[tuple[str, str]] = set() 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 - # adopted an either/or approach in the config entry setup, and can - # guarantee that one or the other will be populated. - # See: https://github.com/home-assistant/core/issues/35883 - (DOMAIN, self.device.info.mac or self.device.info.serial_number) - }, + identifiers={(DOMAIN, self.mac_or_serial)}, manufacturer=self.device.info.manufacturer, model=self.device.info.model, name=self.device.name, diff --git a/homeassistant/components/onvif/binary_sensor.py b/homeassistant/components/onvif/binary_sensor.py index 2e72d331d3d..cd9af1d83b5 100644 --- a/homeassistant/components/onvif/binary_sensor.py +++ b/homeassistant/components/onvif/binary_sensor.py @@ -48,15 +48,16 @@ async def async_setup_entry( device.events.async_add_listener(async_check_entities) - return True - class ONVIFBinarySensor(ONVIFBaseEntity, RestoreEntity, BinarySensorEntity): """Representation of a binary ONVIF event.""" _attr_should_poll = False + _attr_unique_id: str - def __init__(self, uid, device: ONVIFDevice, entry: er.RegistryEntry | None = None): + def __init__( + self, uid: str, device: ONVIFDevice, entry: er.RegistryEntry | None = None + ) -> None: """Initialize the ONVIF binary sensor.""" self._attr_unique_id = uid if entry is not None: @@ -65,6 +66,7 @@ class ONVIFBinarySensor(ONVIFBaseEntity, RestoreEntity, BinarySensorEntity): self._attr_name = entry.name else: event = device.events.get_uid(uid) + assert event self._attr_device_class = event.device_class self._attr_entity_category = event.entity_category self._attr_entity_registry_enabled_default = event.entity_enabled diff --git a/homeassistant/components/onvif/button.py b/homeassistant/components/onvif/button.py index 0af4a16d269..2732c672ad9 100644 --- a/homeassistant/components/onvif/button.py +++ b/homeassistant/components/onvif/button.py @@ -30,9 +30,7 @@ class RebootButton(ONVIFBaseEntity, ButtonEntity): """Initialize the button entity.""" super().__init__(device) self._attr_name = f"{self.device.name} Reboot" - self._attr_unique_id = ( - f"{self.device.info.mac or self.device.info.serial_number}_reboot" - ) + self._attr_unique_id = f"{self.mac_or_serial}_reboot" async def async_press(self) -> None: """Send out a SystemReboot command.""" @@ -49,7 +47,7 @@ class SetSystemDateAndTimeButton(ONVIFBaseEntity, ButtonEntity): """Initialize the button entity.""" super().__init__(device) self._attr_name = f"{self.device.name} Set System Date and Time" - self._attr_unique_id = f"{self.device.info.mac or self.device.info.serial_number}_setsystemdatetime" + self._attr_unique_id = f"{self.mac_or_serial}_setsystemdatetime" async def async_press(self) -> None: """Send out a SetSystemDateAndTime command.""" diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 6aa7ae42767..5aa49f68aa6 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -13,6 +13,7 @@ from homeassistant.components.stream import ( CONF_RTSP_TRANSPORT, CONF_USE_WALLCLOCK_AS_TIMESTAMPS, ) +from homeassistant.components.stream.const import RTSP_TRANSPORTS from homeassistant.config_entries import ConfigEntry from homeassistant.const import HTTP_BASIC_AUTHENTICATION from homeassistant.core import HomeAssistant @@ -46,6 +47,8 @@ from .const import ( ZOOM_IN, ZOOM_OUT, ) +from .device import ONVIFDevice +from .models import Profile async def async_setup_entry( @@ -85,20 +88,19 @@ async def async_setup_entry( [ONVIFCameraEntity(device, profile) for profile in device.profiles] ) - return True - class ONVIFCameraEntity(ONVIFBaseEntity, Camera): """Representation of an ONVIF camera.""" _attr_supported_features = CameraEntityFeature.STREAM - def __init__(self, device, profile): + def __init__(self, device: ONVIFDevice, profile: Profile) -> None: """Initialize ONVIF camera entity.""" - ONVIFBaseEntity.__init__(self, device, profile) + ONVIFBaseEntity.__init__(self, device) Camera.__init__(self) + self.profile = profile self.stream_options[CONF_RTSP_TRANSPORT] = device.config_entry.options.get( - CONF_RTSP_TRANSPORT + CONF_RTSP_TRANSPORT, next(iter(RTSP_TRANSPORTS)) ) self.stream_options[ CONF_USE_WALLCLOCK_AS_TIMESTAMPS @@ -118,8 +120,8 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): def unique_id(self) -> str: """Return a unique ID.""" if self.profile.index: - return f"{self.device.info.mac or self.device.info.serial_number}_{self.profile.index}" - return self.device.info.mac or self.device.info.serial_number + return f"{self.mac_or_serial}_{self.profile.index}" + return self.mac_or_serial @property def entity_registry_enabled_default(self) -> bool: @@ -149,6 +151,7 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): ) if image is None: + assert self._stream_uri return await ffmpeg.async_get_image( self.hass, self._stream_uri, diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 5907ea90124..dda28e07a2a 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -42,21 +42,21 @@ from .models import PTZ, Capabilities, DeviceInfo, Profile, Resolution, Video class ONVIFDevice: """Manages an ONVIF device.""" - def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry = None) -> None: + device: ONVIFCamera + events: EventManager + + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Initialize the device.""" self.hass: HomeAssistant = hass self.config_entry: ConfigEntry = config_entry self.available: bool = True - self.device: ONVIFCamera = None - self.events: EventManager = None - self.info: DeviceInfo = DeviceInfo() self.capabilities: Capabilities = Capabilities() self.profiles: list[Profile] = [] self.max_resolution: int = 0 - self._dt_diff_seconds: int = 0 + self._dt_diff_seconds: float = 0 @property def name(self) -> str: @@ -99,6 +99,7 @@ class ONVIFDevice: await self.async_check_date_and_time() # Create event manager + assert self.config_entry.unique_id self.events = EventManager( self.hass, self.device, self.config_entry.unique_id ) @@ -297,7 +298,7 @@ class ONVIFDevice: """Obtain media profiles for this device.""" media_service = self.device.create_media_service() result = await media_service.GetProfiles() - profiles = [] + profiles: list[Profile] = [] if not isinstance(result, list): return profiles @@ -396,7 +397,7 @@ class ONVIFDevice: req.ProfileToken = profile.token if move_mode == CONTINUOUS_MOVE: # Guard against unsupported operation - if not profile.ptz.continuous: + if not profile.ptz or not profile.ptz.continuous: LOGGER.warning( "ContinuousMove not supported on device '%s'", self.name ) @@ -419,7 +420,7 @@ class ONVIFDevice: ) elif move_mode == RELATIVE_MOVE: # Guard against unsupported operation - if not profile.ptz.relative: + if not profile.ptz or not profile.ptz.relative: LOGGER.warning( "RelativeMove not supported on device '%s'", self.name ) @@ -436,7 +437,7 @@ class ONVIFDevice: await ptz_service.RelativeMove(req) elif move_mode == ABSOLUTE_MOVE: # Guard against unsupported operation - if not profile.ptz.absolute: + if not profile.ptz or not profile.ptz.absolute: LOGGER.warning( "AbsoluteMove not supported on device '%s'", self.name ) @@ -453,6 +454,11 @@ class ONVIFDevice: await ptz_service.AbsoluteMove(req) elif move_mode == GOTOPRESET_MOVE: # Guard against unsupported operation + if not profile.ptz or not profile.ptz.presets: + LOGGER.warning( + "Absolute Presets not supported on device '%s'", self.name + ) + return if preset_val not in profile.ptz.presets: LOGGER.warning( "PTZ preset '%s' does not exist on device '%s'. Available Presets: %s", diff --git a/homeassistant/components/onvif/sensor.py b/homeassistant/components/onvif/sensor.py index 67c0ee3da3f..b662dca1d5d 100644 --- a/homeassistant/components/onvif/sensor.py +++ b/homeassistant/components/onvif/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import date, datetime +from decimal import Decimal from homeassistant.components.sensor import RestoreSensor from homeassistant.config_entries import ConfigEntry @@ -47,8 +48,6 @@ async def async_setup_entry( device.events.async_add_listener(async_check_entities) - return True - class ONVIFSensor(ONVIFBaseEntity, RestoreSensor): """Representation of a ONVIF sensor event.""" @@ -65,6 +64,7 @@ class ONVIFSensor(ONVIFBaseEntity, RestoreSensor): self._attr_native_unit_of_measurement = entry.unit_of_measurement else: event = device.events.get_uid(uid) + assert event self._attr_device_class = event.device_class self._attr_entity_category = event.entity_category self._attr_entity_registry_enabled_default = event.entity_enabled @@ -75,7 +75,7 @@ class ONVIFSensor(ONVIFBaseEntity, RestoreSensor): super().__init__(device) @property - def native_value(self) -> StateType | date | datetime: + def native_value(self) -> StateType | date | datetime | Decimal: """Return the value reported by the sensor.""" if (event := self.device.events.get_uid(self._attr_unique_id)) is not None: return event.value diff --git a/mypy.ini b/mypy.ini index 1e6741eccd1..8588209b87a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2689,21 +2689,6 @@ ignore_errors = true [mypy-homeassistant.components.minecraft_server.sensor] ignore_errors = true -[mypy-homeassistant.components.onvif.base] -ignore_errors = true - -[mypy-homeassistant.components.onvif.binary_sensor] -ignore_errors = true - -[mypy-homeassistant.components.onvif.camera] -ignore_errors = true - -[mypy-homeassistant.components.onvif.device] -ignore_errors = true - -[mypy-homeassistant.components.onvif.sensor] -ignore_errors = true - [mypy-homeassistant.components.sonos] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 64260b90033..992d8836916 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -27,11 +27,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.minecraft_server", "homeassistant.components.minecraft_server.helpers", "homeassistant.components.minecraft_server.sensor", - "homeassistant.components.onvif.base", - "homeassistant.components.onvif.binary_sensor", - "homeassistant.components.onvif.camera", - "homeassistant.components.onvif.device", - "homeassistant.components.onvif.sensor", "homeassistant.components.sonos", "homeassistant.components.sonos.alarms", "homeassistant.components.sonos.binary_sensor", From 28c082a0804710e24a92ae009a371eb11336fb8e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jul 2022 12:42:18 +0200 Subject: [PATCH 2548/3516] Update sentry-sdk to 1.7.1 (#75154) --- 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 44bcd8025bb..29b897e4a3c 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.7.0"], + "requirements": ["sentry-sdk==1.7.1"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 380a8d24f21..f93afa0debd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2153,7 +2153,7 @@ sendgrid==6.8.2 sense_energy==0.10.4 # homeassistant.components.sentry -sentry-sdk==1.7.0 +sentry-sdk==1.7.1 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5469672ee54..a1959df8456 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1434,7 +1434,7 @@ securetar==2022.2.0 sense_energy==0.10.4 # homeassistant.components.sentry -sentry-sdk==1.7.0 +sentry-sdk==1.7.1 # homeassistant.components.sharkiq sharkiq==0.0.1 From c260413e2a344256e8328e498ab45fc179c29cd6 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:08:50 +0200 Subject: [PATCH 2549/3516] Remove lovelace from mypy ignore list (#75167) * Remove lovelace from mypy ignore list * Raise error on failed config --- homeassistant/components/lovelace/__init__.py | 8 ++++++-- homeassistant/components/lovelace/resources.py | 4 ++-- homeassistant/components/lovelace/websocket.py | 8 ++++---- mypy.ini | 12 ------------ script/hassfest/mypy_config.py | 4 ---- 5 files changed, 12 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 9b268dfdfcd..cf74a3a588c 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -86,11 +86,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: config = await async_process_component_config(hass, conf, integration) + if config is None: + raise HomeAssistantError("Config validation failed") + resource_collection = await create_yaml_resource_col( hass, config[DOMAIN].get(CONF_RESOURCES) ) hass.data[DOMAIN]["resources"] = resource_collection + default_config: dashboard.LovelaceConfig if mode == MODE_YAML: default_config = dashboard.LovelaceYAML(hass, None, None) resource_collection = await create_yaml_resource_col(hass, yaml_resources) @@ -179,8 +183,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Process YAML dashboards for url_path, dashboard_conf in hass.data[DOMAIN]["yaml_dashboards"].items(): # For now always mode=yaml - config = dashboard.LovelaceYAML(hass, url_path, dashboard_conf) - hass.data[DOMAIN]["dashboards"][url_path] = config + lovelace_config = dashboard.LovelaceYAML(hass, url_path, dashboard_conf) + hass.data[DOMAIN]["dashboards"][url_path] = lovelace_config try: _register_panel(hass, url_path, MODE_YAML, dashboard_conf, False) diff --git a/homeassistant/components/lovelace/resources.py b/homeassistant/components/lovelace/resources.py index 22297c54d6c..7e7c670baf5 100644 --- a/homeassistant/components/lovelace/resources.py +++ b/homeassistant/components/lovelace/resources.py @@ -7,7 +7,7 @@ import uuid import voluptuous as vol -from homeassistant.const import CONF_RESOURCES, CONF_TYPE +from homeassistant.const import CONF_ID, CONF_RESOURCES, CONF_TYPE from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, storage @@ -94,7 +94,7 @@ class ResourceStorageCollection(collection.StorageCollection): conf.pop(CONF_RESOURCES) for item in data: - item[collection.CONF_ID] = uuid.uuid4().hex + item[CONF_ID] = uuid.uuid4().hex data = {"items": data} diff --git a/homeassistant/components/lovelace/websocket.py b/homeassistant/components/lovelace/websocket.py index 45a042c1f2e..cb45d9cfbc6 100644 --- a/homeassistant/components/lovelace/websocket.py +++ b/homeassistant/components/lovelace/websocket.py @@ -45,8 +45,8 @@ def _handle_errors(func): return send_with_error_handling -@websocket_api.async_response @websocket_api.websocket_command({"type": "lovelace/resources"}) +@websocket_api.async_response async def websocket_lovelace_resources(hass, connection, msg): """Send Lovelace UI resources over WebSocket configuration.""" resources = hass.data[DOMAIN]["resources"] @@ -58,7 +58,6 @@ async def websocket_lovelace_resources(hass, connection, msg): connection.send_result(msg["id"], resources.async_items()) -@websocket_api.async_response @websocket_api.websocket_command( { "type": "lovelace/config", @@ -66,6 +65,7 @@ async def websocket_lovelace_resources(hass, connection, msg): vol.Optional(CONF_URL_PATH): vol.Any(None, cv.string), } ) +@websocket_api.async_response @_handle_errors async def websocket_lovelace_config(hass, connection, msg, config): """Send Lovelace UI config over WebSocket configuration.""" @@ -73,7 +73,6 @@ async def websocket_lovelace_config(hass, connection, msg, config): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { "type": "lovelace/config/save", @@ -81,6 +80,7 @@ async def websocket_lovelace_config(hass, connection, msg, config): vol.Optional(CONF_URL_PATH): vol.Any(None, cv.string), } ) +@websocket_api.async_response @_handle_errors async def websocket_lovelace_save_config(hass, connection, msg, config): """Save Lovelace UI configuration.""" @@ -88,13 +88,13 @@ async def websocket_lovelace_save_config(hass, connection, msg, config): @websocket_api.require_admin -@websocket_api.async_response @websocket_api.websocket_command( { "type": "lovelace/config/delete", vol.Optional(CONF_URL_PATH): vol.Any(None, cv.string), } ) +@websocket_api.async_response @_handle_errors async def websocket_lovelace_delete_config(hass, connection, msg, config): """Delete Lovelace UI configuration.""" diff --git a/mypy.ini b/mypy.ini index 8588209b87a..93ae6c2f78c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2668,18 +2668,6 @@ ignore_errors = true [mypy-homeassistant.components.evohome.climate] ignore_errors = true -[mypy-homeassistant.components.lovelace] -ignore_errors = true - -[mypy-homeassistant.components.lovelace.dashboard] -ignore_errors = true - -[mypy-homeassistant.components.lovelace.resources] -ignore_errors = true - -[mypy-homeassistant.components.lovelace.websocket] -ignore_errors = true - [mypy-homeassistant.components.minecraft_server] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 992d8836916..3b8e80490b6 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -20,10 +20,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.cloud.http_api", "homeassistant.components.evohome", "homeassistant.components.evohome.climate", - "homeassistant.components.lovelace", - "homeassistant.components.lovelace.dashboard", - "homeassistant.components.lovelace.resources", - "homeassistant.components.lovelace.websocket", "homeassistant.components.minecraft_server", "homeassistant.components.minecraft_server.helpers", "homeassistant.components.minecraft_server.sensor", From e16bd1e47167f705177feb22f8ac2072682f621d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:46:37 +0200 Subject: [PATCH 2550/3516] Remove evohome climate from mypy ignore list (#75169) * Remove evohome climate from mypy ignore list * Raise error --- homeassistant/components/evohome/__init__.py | 4 +-- homeassistant/components/evohome/climate.py | 33 ++++++++------------ mypy.ini | 3 -- script/hassfest/mypy_config.py | 1 - 4 files changed, 15 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 1bb84dfc40a..27c3698143b 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -540,11 +540,11 @@ class EvoDevice(Entity): return await self.async_tcs_svc_request(payload["service"], payload["data"]) - async def async_tcs_svc_request(self, service: dict, data: dict) -> None: + async def async_tcs_svc_request(self, service: str, data: dict[str, Any]) -> None: """Process a service request (system mode) for a controller.""" raise NotImplementedError - async def async_zone_svc_request(self, service: dict, data: dict) -> None: + async def async_zone_svc_request(self, service: str, data: dict[str, Any]) -> None: """Process a service request (setpoint override) for a zone.""" raise NotImplementedError diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 700fa6ab078..841619af6f1 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import datetime as dt import logging +from typing import Any from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -15,6 +16,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import PRECISION_TENTHS, TEMP_CELSIUS from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -93,9 +95,8 @@ async def async_setup_platform( broker.params[CONF_LOCATION_IDX], ) - controller = EvoController(broker, broker.tcs) + entities: list[EvoClimateEntity] = [EvoController(broker, broker.tcs)] - zones = [] for zone in broker.tcs.zones.values(): if zone.modelType == "HeatingZone" or zone.zoneType == "Thermostat": _LOGGER.debug( @@ -107,7 +108,7 @@ async def async_setup_platform( ) new_entity = EvoZone(broker, zone) - zones.append(new_entity) + entities.append(new_entity) else: _LOGGER.warning( @@ -119,7 +120,7 @@ async def async_setup_platform( zone.name, ) - async_add_entities([controller] + zones, update_before_add=True) + async_add_entities(entities, update_before_add=True) class EvoClimateEntity(EvoDevice, ClimateEntity): @@ -168,7 +169,7 @@ class EvoZone(EvoChild, EvoClimateEntity): ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE ) - async def async_zone_svc_request(self, service: dict, data: dict) -> None: + async def async_zone_svc_request(self, service: str, data: dict[str, Any]) -> None: """Process a service request (setpoint override) for a zone.""" if service == SVC_RESET_ZONE_OVERRIDE: await self._evo_broker.call_client_api( @@ -272,7 +273,7 @@ class EvoZone(EvoChild, EvoClimateEntity): self._evo_device.cancel_temp_override() ) - async def async_set_preset_mode(self, preset_mode: str | None) -> None: + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode; if None, then revert to following the schedule.""" evo_preset_mode = HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW) @@ -332,7 +333,7 @@ class EvoController(EvoClimateEntity): ClimateEntityFeature.PRESET_MODE if self._attr_preset_modes else 0 ) - async def async_tcs_svc_request(self, service: dict, data: dict) -> None: + async def async_tcs_svc_request(self, service: str, data: dict[str, Any]) -> None: """Process a service request (system mode) for a controller. Data validation is not required, it will have been done upstream. @@ -385,25 +386,17 @@ class EvoController(EvoClimateEntity): """Return the current preset mode, e.g., home, away, temp.""" return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"]) - @property - def min_temp(self) -> float: - """Return None as Controllers don't have a target temperature.""" - return None - - @property - def max_temp(self) -> float: - """Return None as Controllers don't have a target temperature.""" - return None - async def async_set_temperature(self, **kwargs) -> None: """Raise exception as Controllers don't have a target temperature.""" raise NotImplementedError("Evohome Controllers don't have target temperatures.") - async def async_set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set an operating mode for a Controller.""" - await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) + if not (tcs_mode := HA_HVAC_TO_TCS.get(hvac_mode)): + raise HomeAssistantError(f"Invalid hvac_mode: {hvac_mode}") + await self._set_tcs_mode(tcs_mode) - async def async_set_preset_mode(self, preset_mode: str | None) -> None: + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode; if None, then revert to 'Auto' mode.""" await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EVO_AUTO)) diff --git a/mypy.ini b/mypy.ini index 93ae6c2f78c..2297869dbb0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2665,9 +2665,6 @@ ignore_errors = true [mypy-homeassistant.components.evohome] ignore_errors = true -[mypy-homeassistant.components.evohome.climate] -ignore_errors = true - [mypy-homeassistant.components.minecraft_server] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 3b8e80490b6..f53d6a6e6b0 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -19,7 +19,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.cloud.client", "homeassistant.components.cloud.http_api", "homeassistant.components.evohome", - "homeassistant.components.evohome.climate", "homeassistant.components.minecraft_server", "homeassistant.components.minecraft_server.helpers", "homeassistant.components.minecraft_server.sensor", From 20432ccc7601f46ce0f84f5b539ada2d9b5b3381 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 14 Jul 2022 07:02:13 -0500 Subject: [PATCH 2551/3516] Migrate roku to new entity naming (#74819) * migrate roku to new entity naming * Update binary_sensor.py * Update sensor.py * Update test_binary_sensor.py * Update sensor.py * Update entity.py * Update media_player.py * Update remote.py * Update media_player.py * Update remote.py * Update entity.py * Update entity.py * Update entity.py * Update entity.py --- .../components/roku/binary_sensor.py | 6 +-- homeassistant/components/roku/entity.py | 52 ++++++++++--------- homeassistant/components/roku/media_player.py | 22 ++++---- homeassistant/components/roku/remote.py | 22 ++++---- tests/components/roku/test_binary_sensor.py | 12 ++--- 5 files changed, 55 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/roku/binary_sensor.py b/homeassistant/components/roku/binary_sensor.py index 5b6da073dd1..243ea994dfa 100644 --- a/homeassistant/components/roku/binary_sensor.py +++ b/homeassistant/components/roku/binary_sensor.py @@ -36,7 +36,7 @@ class RokuBinarySensorEntityDescription( BINARY_SENSORS: tuple[RokuBinarySensorEntityDescription, ...] = ( RokuBinarySensorEntityDescription( key="headphones_connected", - name="Headphones Connected", + name="Headphones connected", icon="mdi:headphones", value_fn=lambda device: device.info.headphones_connected, ), @@ -49,14 +49,14 @@ BINARY_SENSORS: tuple[RokuBinarySensorEntityDescription, ...] = ( ), RokuBinarySensorEntityDescription( key="supports_ethernet", - name="Supports Ethernet", + name="Supports ethernet", icon="mdi:ethernet", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.info.ethernet_support, ), RokuBinarySensorEntityDescription( key="supports_find_remote", - name="Supports Find Remote", + name="Supports find remote", icon="mdi:remote", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.info.supports_find_remote, diff --git a/homeassistant/components/roku/entity.py b/homeassistant/components/roku/entity.py index 39373c96c6a..a85024f8220 100644 --- a/homeassistant/components/roku/entity.py +++ b/homeassistant/components/roku/entity.py @@ -25,30 +25,32 @@ class RokuEntity(CoordinatorEntity[RokuDataUpdateCoordinator]): if description is not None: self.entity_description = description - self._attr_name = f"{coordinator.data.info.name} {description.name}" - if device_id is not None: + + if device_id is None: + self._attr_name = f"{coordinator.data.info.name} {description.name}" + + if device_id is not None: + self._attr_has_entity_name = True + + if description is not None: self._attr_unique_id = f"{device_id}_{description.key}" + else: + self._attr_unique_id = device_id - @property - def device_info(self) -> DeviceInfo | None: - """Return device information about this Roku device.""" - if self._device_id is None: - return None - - return DeviceInfo( - identifiers={(DOMAIN, self._device_id)}, - connections={ - (CONNECTION_NETWORK_MAC, mac_address) - for mac_address in ( - self.coordinator.data.info.wifi_mac, - self.coordinator.data.info.ethernet_mac, - ) - if mac_address is not None - }, - name=self.coordinator.data.info.name, - manufacturer=self.coordinator.data.info.brand, - model=self.coordinator.data.info.model_name, - hw_version=self.coordinator.data.info.model_number, - sw_version=self.coordinator.data.info.version, - suggested_area=self.coordinator.data.info.device_location, - ) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_id)}, + connections={ + (CONNECTION_NETWORK_MAC, mac_address) + for mac_address in ( + self.coordinator.data.info.wifi_mac, + self.coordinator.data.info.ethernet_mac, + ) + if mac_address is not None + }, + name=self.coordinator.data.info.name, + manufacturer=self.coordinator.data.info.brand, + model=self.coordinator.data.info.model_name, + hw_version=self.coordinator.data.info.model_number, + sw_version=self.coordinator.data.info.version, + suggested_area=self.coordinator.data.info.device_location, + ) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index a47432694dd..d7b9a3489c9 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -99,7 +99,15 @@ async def async_setup_entry( """Set up the Roku config entry.""" coordinator: RokuDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] unique_id = coordinator.data.info.serial_number - async_add_entities([RokuMediaPlayer(unique_id, coordinator)], True) + async_add_entities( + [ + RokuMediaPlayer( + device_id=unique_id, + coordinator=coordinator, + ) + ], + True, + ) platform = entity_platform.async_get_current_platform() @@ -127,18 +135,6 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): | MediaPlayerEntityFeature.BROWSE_MEDIA ) - def __init__( - self, unique_id: str | None, coordinator: RokuDataUpdateCoordinator - ) -> None: - """Initialize the Roku device.""" - super().__init__( - coordinator=coordinator, - device_id=unique_id, - ) - - self._attr_name = coordinator.data.info.name - self._attr_unique_id = unique_id - def _media_playback_trackable(self) -> bool: """Detect if we have enough media data to track playback.""" if self.coordinator.data.media is None or self.coordinator.data.media.live: diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index 6d1312c0b03..fceac67a477 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -21,24 +21,22 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Load Roku remote based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: RokuDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] unique_id = coordinator.data.info.serial_number - async_add_entities([RokuRemote(unique_id, coordinator)], True) + async_add_entities( + [ + RokuRemote( + device_id=unique_id, + coordinator=coordinator, + ) + ], + True, + ) class RokuRemote(RokuEntity, RemoteEntity): """Device that sends commands to an Roku.""" - def __init__(self, unique_id: str, coordinator: RokuDataUpdateCoordinator) -> None: - """Initialize the Roku device.""" - super().__init__( - device_id=unique_id, - coordinator=coordinator, - ) - - self._attr_name = coordinator.data.info.name - self._attr_unique_id = unique_id - @property def is_on(self) -> bool: """Return true if device is on.""" diff --git a/tests/components/roku/test_binary_sensor.py b/tests/components/roku/test_binary_sensor.py index 24f92b0b11b..706b1eceddb 100644 --- a/tests/components/roku/test_binary_sensor.py +++ b/tests/components/roku/test_binary_sensor.py @@ -29,7 +29,7 @@ async def test_roku_binary_sensors( assert entry.unique_id == f"{UPNP_SERIAL}_headphones_connected" assert entry.entity_category is None assert state.state == STATE_OFF - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Headphones Connected" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Headphones connected" assert state.attributes.get(ATTR_ICON) == "mdi:headphones" assert ATTR_DEVICE_CLASS not in state.attributes @@ -51,7 +51,7 @@ async def test_roku_binary_sensors( assert entry.unique_id == f"{UPNP_SERIAL}_supports_ethernet" assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == STATE_ON - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Supports Ethernet" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Supports ethernet" assert state.attributes.get(ATTR_ICON) == "mdi:ethernet" assert ATTR_DEVICE_CLASS not in state.attributes @@ -62,7 +62,7 @@ async def test_roku_binary_sensors( assert entry.unique_id == f"{UPNP_SERIAL}_supports_find_remote" assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == STATE_OFF - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Supports Find Remote" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Supports find remote" assert state.attributes.get(ATTR_ICON) == "mdi:remote" assert ATTR_DEVICE_CLASS not in state.attributes @@ -105,7 +105,7 @@ async def test_rokutv_binary_sensors( assert state.state == STATE_OFF assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == '58" Onn Roku TV Headphones Connected' + == '58" Onn Roku TV Headphones connected' ) assert state.attributes.get(ATTR_ICON) == "mdi:headphones" assert ATTR_DEVICE_CLASS not in state.attributes @@ -131,7 +131,7 @@ async def test_rokutv_binary_sensors( assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == STATE_ON assert ( - state.attributes.get(ATTR_FRIENDLY_NAME) == '58" Onn Roku TV Supports Ethernet' + state.attributes.get(ATTR_FRIENDLY_NAME) == '58" Onn Roku TV Supports ethernet' ) assert state.attributes.get(ATTR_ICON) == "mdi:ethernet" assert ATTR_DEVICE_CLASS not in state.attributes @@ -147,7 +147,7 @@ async def test_rokutv_binary_sensors( assert state.state == STATE_ON assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == '58" Onn Roku TV Supports Find Remote' + == '58" Onn Roku TV Supports find remote' ) assert state.attributes.get(ATTR_ICON) == "mdi:remote" assert ATTR_DEVICE_CLASS not in state.attributes From a3c1926da57b5c89bf77c15622d75d231653f5d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 14:40:17 +0200 Subject: [PATCH 2552/3516] Add mock_bluetooth fixture (#75075) --- tests/components/bluetooth/conftest.py | 24 ----------------- tests/components/bluetooth/test_init.py | 23 +--------------- tests/conftest.py | 36 ++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 47 deletions(-) diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index fc0bd85b795..760500fe7a1 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -1,25 +1 @@ """Tests for the bluetooth component.""" - -import threading - -import pytest - -from tests.common import INSTANCES - - -@pytest.fixture(autouse=True) -def verify_cleanup(): - """Verify that the test has cleaned up resources correctly.""" - threads_before = frozenset(threading.enumerate()) - - yield - - if len(INSTANCES) >= 2: - count = len(INSTANCES) - for inst in INSTANCES: - inst.stop() - pytest.exit(f"Detected non stopped instances ({count}), aborting test run") - - threads = frozenset(threading.enumerate()) - threads_before - for thread in threads: - assert isinstance(thread, threading._DummyThread) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index f43ef4737f6..2bbe4ce7dcb 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1,10 +1,8 @@ """Tests for the Bluetooth integration.""" -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import MagicMock, patch -import bleak from bleak import BleakError from bleak.backends.scanner import AdvertisementData, BLEDevice -import pytest from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( @@ -16,25 +14,6 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT from homeassistant.setup import async_setup_component -@pytest.fixture() -def mock_bleak_scanner_start(): - """Fixture to mock starting the bleak scanner.""" - scanner = bleak.BleakScanner - models.HA_BLEAK_SCANNER = None - - with patch("homeassistant.components.bluetooth.HaBleakScanner.stop"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", - ) as mock_bleak_scanner_start: - yield mock_bleak_scanner_start - - # We need to drop the stop method from the object since we patched - # out start and this fixture will expire before the stop method is called - # when EVENT_HOMEASSISTANT_STOP is fired. - if models.HA_BLEAK_SCANNER: - models.HA_BLEAK_SCANNER.stop = AsyncMock() - bleak.BleakScanner = scanner - - async def test_setup_and_stop(hass, mock_bleak_scanner_start): """Test we and setup and stop the scanner.""" mock_bt = [ diff --git a/tests/conftest.py b/tests/conftest.py index 4b2852e94fc..ee2e903e88d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -167,7 +167,8 @@ def verify_cleanup(): pytest.exit(f"Detected non stopped instances ({count}), aborting test run") threads = frozenset(threading.enumerate()) - threads_before - assert not threads + for thread in threads: + assert isinstance(thread, threading._DummyThread) @pytest.fixture(autouse=True) @@ -855,3 +856,36 @@ def mock_integration_frame(): ], ): yield correct_frame + + +@pytest.fixture(name="mock_bleak_scanner_start") +def mock_bleak_scanner_start(): + """Fixture to mock starting the bleak scanner.""" + + # Late imports to avoid loading bleak unless we need it + + import bleak # pylint: disable=import-outside-toplevel + + from homeassistant.components.bluetooth import ( # pylint: disable=import-outside-toplevel + models as bluetooth_models, + ) + + scanner = bleak.BleakScanner + bluetooth_models.HA_BLEAK_SCANNER = None + + with patch("homeassistant.components.bluetooth.HaBleakScanner.stop"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + ) as mock_bleak_scanner_start: + yield mock_bleak_scanner_start + + # We need to drop the stop method from the object since we patched + # out start and this fixture will expire before the stop method is called + # when EVENT_HOMEASSISTANT_STOP is fired. + if bluetooth_models.HA_BLEAK_SCANNER: + bluetooth_models.HA_BLEAK_SCANNER.stop = AsyncMock() + bleak.BleakScanner = scanner + + +@pytest.fixture(name="mock_bluetooth") +def mock_bluetooth(mock_bleak_scanner_start): + """Mock out bluetooth from starting.""" From a31dde9cb427104f1b22fb12d3013baf20a1dc76 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 14:44:27 +0200 Subject: [PATCH 2553/3516] Await startup in homekit controller (#75021) --- .../components/homekit_controller/__init__.py | 6 ++ .../components/homekit_controller/camera.py | 2 +- .../homekit_controller/config_flow.py | 9 +- .../homekit_controller/connection.py | 53 +++++++----- tests/components/homekit_controller/common.py | 7 ++ .../homekit_controller/test_config_flow.py | 2 +- .../homekit_controller/test_init.py | 84 ++++++++++++++++++- 7 files changed, 133 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 67da7a1068f..bc911b991ca 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -227,6 +227,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not await conn.async_setup(): del hass.data[KNOWN_DEVICES][conn.unique_id] + if (connection := getattr(conn.pairing, "connection")) and hasattr( + connection, "host" + ): + raise ConfigEntryNotReady( + f"Cannot connect to {connection.host}:{connection.port}" + ) raise ConfigEntryNotReady return True diff --git a/homeassistant/components/homekit_controller/camera.py b/homeassistant/components/homekit_controller/camera.py index 0ffa0a22f4d..0f0dd4f9050 100644 --- a/homeassistant/components/homekit_controller/camera.py +++ b/homeassistant/components/homekit_controller/camera.py @@ -25,7 +25,7 @@ class HomeKitCamera(AccessoryEntity, Camera): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a jpeg with the current camera snapshot.""" - return await self._accessory.pairing.image( + return await self._accessory.pairing.image( # type: ignore[attr-defined] self._aid, width or 640, height or 480, diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 493dd05a8b2..ac840eb0689 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -17,6 +17,7 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import device_registry as dr +from .connection import HKDevice from .const import DOMAIN, KNOWN_DEVICES from .utils import async_get_controller @@ -240,17 +241,19 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_update_entry( existing_entry, data={**existing_entry.data, **updated_ip_port} ) - conn = self.hass.data[KNOWN_DEVICES][hkid] + conn: HKDevice = self.hass.data[KNOWN_DEVICES][hkid] # When we rediscover the device, let aiohomekit know # that the device is available and we should not wait # to retry connecting any longer. reconnect_soon # will do nothing if the device is already connected await conn.pairing.reconnect_soon() - if conn.config_num != config_num: + if config_num and conn.config_num != config_num: _LOGGER.debug( "HomeKit info %s: c# incremented, refreshing entities", hkid ) - self.hass.async_create_task(conn.async_refresh_entity_map(config_num)) + self.hass.async_create_task( + conn.async_refresh_entity_map_and_entities(config_num) + ) return self.async_abort(reason="already_configured") _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 7a85e234807..35f4c6bdb31 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -7,6 +7,7 @@ import datetime import logging from typing import Any +from aiohomekit import Controller from aiohomekit.exceptions import ( AccessoryDisconnectedError, AccessoryNotFoundError, @@ -35,6 +36,7 @@ from .const import ( IDENTIFIER_SERIAL_NUMBER, ) from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry +from .storage import EntityMapStorage DEFAULT_SCAN_INTERVAL = datetime.timedelta(seconds=60) RETRY_INTERVAL = 60 # seconds @@ -70,11 +72,13 @@ class HKDevice: # don't want to mutate a dict owned by a config entry. self.pairing_data = pairing_data.copy() - self.pairing = hass.data[CONTROLLER].load_pairing( + connection: Controller = hass.data[CONTROLLER] + + self.pairing = connection.load_pairing( self.pairing_data["AccessoryPairingID"], self.pairing_data ) - self.accessories = None + self.accessories: list[Any] | None = None self.config_num = 0 self.entity_map = Accessories() @@ -167,26 +171,23 @@ class HKDevice: async def async_setup(self) -> bool: """Prepare to use a paired HomeKit device in Home Assistant.""" - cache = self.hass.data[ENTITY_MAP].get_map(self.unique_id) - if not cache: - if await self.async_refresh_entity_map(self.config_num): - self._polling_interval_remover = async_track_time_interval( - self.hass, self.async_update, DEFAULT_SCAN_INTERVAL - ) - return True + entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP] + if cache := entity_storage.get_map(self.unique_id): + self.accessories = cache["accessories"] + self.config_num = cache["config_num"] + self.entity_map = Accessories.from_list(self.accessories) + elif not await self.async_refresh_entity_map(self.config_num): return False - self.accessories = cache["accessories"] - self.config_num = cache["config_num"] - - self.entity_map = Accessories.from_list(self.accessories) - + await self.async_process_entity_map() + if not self.pairing.is_connected: + return False + # If everything is up to date, we can create the entities + # since we know the data is not stale. + self.add_entities() self._polling_interval_remover = async_track_time_interval( self.hass, self.async_update, DEFAULT_SCAN_INTERVAL ) - - self.hass.async_create_task(self.async_process_entity_map()) - return True def device_info_for_accessory(self, accessory: Accessory) -> DeviceInfo: @@ -361,7 +362,7 @@ class HKDevice: # is especially important for BLE, as the Pairing instance relies on the entity map # to map aid/iid to GATT characteristics. So push it to there as well. - self.pairing.pairing_data["accessories"] = self.accessories + self.pairing.pairing_data["accessories"] = self.accessories # type: ignore[attr-defined] self.async_detect_workarounds() @@ -375,8 +376,6 @@ class HKDevice: # Load any triggers for this config entry await async_setup_triggers_for_entry(self.hass, self.config_entry) - self.add_entities() - if self.watchable_characteristics: await self.pairing.subscribe(self.watchable_characteristics) if not self.pairing.is_connected: @@ -395,6 +394,12 @@ class HKDevice: self.config_entry, self.platforms ) + async def async_refresh_entity_map_and_entities(self, config_num: int) -> None: + """Refresh the entity map and entities for this pairing.""" + await self.async_refresh_entity_map(config_num) + await self.async_process_entity_map() + self.add_entities() + async def async_refresh_entity_map(self, config_num: int) -> bool: """Handle setup of a HomeKit accessory.""" try: @@ -404,15 +409,17 @@ class HKDevice: # later when Bonjour spots c# is still not up to date. return False + assert self.accessories is not None + self.entity_map = Accessories.from_list(self.accessories) - self.hass.data[ENTITY_MAP].async_create_or_update_map( + entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP] + + entity_storage.async_create_or_update_map( self.unique_id, config_num, self.accessories ) self.config_num = config_num - self.hass.async_create_task(self.async_process_entity_map()) - return True def add_accessory_factory(self, add_entities_cb) -> None: diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 4cad585d135..e773b2ffc66 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -186,6 +186,13 @@ async def setup_platform(hass): async def setup_test_accessories(hass, accessories): """Load a fake homekit device based on captured JSON profile.""" fake_controller = await setup_platform(hass) + return await setup_test_accessories_with_controller( + hass, accessories, fake_controller + ) + + +async def setup_test_accessories_with_controller(hass, accessories, fake_controller): + """Load a fake homekit device based on captured JSON profile.""" pairing_id = "00:00:00:00:00:00" diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 7b62c1e9d6d..9535b7d0cd5 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -515,7 +515,7 @@ async def test_discovery_already_configured_update_csharp(hass, controller): assert entry.data["AccessoryIP"] == discovery_info.host assert entry.data["AccessoryPort"] == discovery_info.port - assert connection_mock.async_refresh_entity_map.await_count == 1 + assert connection_mock.async_refresh_entity_map_and_entities.await_count == 1 @pytest.mark.parametrize("exception,expected", PAIRING_START_ABORT_ERRORS) diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 84a6c8b86bf..5b4d5c74ff6 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -1,19 +1,26 @@ """Tests for homekit_controller init.""" +from datetime import timedelta from unittest.mock import patch +from aiohomekit import exceptions +from aiohomekit.model import Accessory from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes +from aiohomekit.testing import FakeController, FakeDiscovery, FakePairing -from homeassistant.components.homekit_controller.const import ENTITY_MAP +from homeassistant.components.homekit_controller.const import DOMAIN, ENTITY_MAP +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow -from .common import Helper, remove_device +from .common import Helper, remove_device, setup_test_accessories_with_controller +from tests.common import async_fire_time_changed from tests.components.homekit_controller.common import setup_test_component ALIVE_DEVICE_NAME = "testdevice" @@ -89,3 +96,76 @@ async def test_device_remove_devices(hass, hass_ws_client): await remove_device(await hass_ws_client(hass), dead_device_entry.id, entry_id) is True ) + + +async def test_offline_device_raises(hass): + """Test an offline device raises ConfigEntryNotReady.""" + + is_connected = False + + class OfflineFakePairing(FakePairing): + """Fake pairing that always returns False for is_connected.""" + + @property + def is_connected(self): + nonlocal is_connected + return is_connected + + class OfflineFakeDiscovery(FakeDiscovery): + """Fake discovery that returns an offline pairing.""" + + async def start_pairing(self, alias: str): + if self.description.id in self.controller.pairings: + raise exceptions.AlreadyPairedError( + f"{self.description.id} already paired" + ) + + async def finish_pairing(pairing_code): + if pairing_code != self.pairing_code: + raise exceptions.AuthenticationError("M4") + pairing_data = {} + pairing_data["AccessoryIP"] = self.info["address"] + pairing_data["AccessoryPort"] = self.info["port"] + pairing_data["Connection"] = "IP" + + obj = self.controller.pairings[alias] = OfflineFakePairing( + self.controller, pairing_data, self.accessories + ) + return obj + + return finish_pairing + + class OfflineFakeController(FakeController): + """Fake controller that always returns a discovery with a pairing that always returns False for is_connected.""" + + def add_device(self, accessories): + device_id = "00:00:00:00:00:00" + discovery = self.discoveries[device_id] = OfflineFakeDiscovery( + self, + device_id, + accessories=accessories, + ) + return discovery + + with patch( + "homeassistant.components.homekit_controller.utils.Controller" + ) as controller: + fake_controller = controller.return_value = OfflineFakeController() + await async_setup_component(hass, DOMAIN, {}) + + accessory = Accessory.create_with_info( + "TestDevice", "example.com", "Test", "0001", "0.1" + ) + create_alive_service(accessory) + + config_entry, _ = await setup_test_accessories_with_controller( + hass, [accessory], fake_controller + ) + + assert config_entry.state == ConfigEntryState.SETUP_RETRY + + is_connected = True + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED From 2286dea636fda001f03433ba14d7adbda43979e5 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 14 Jul 2022 10:55:24 -0400 Subject: [PATCH 2554/3516] Bump version of pyunifiprotect to 4.0.10 (#75180) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 9aeb8b48050..35dafc96927 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.9", "unifi-discovery==1.1.4"], + "requirements": ["pyunifiprotect==4.0.10", "unifi-discovery==1.1.4"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index f93afa0debd..2904f51d6c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1996,7 +1996,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.9 +pyunifiprotect==4.0.10 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a1959df8456..8a3f0532550 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1337,7 +1337,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.9 +pyunifiprotect==4.0.10 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 89985b93fbf9335ba0380db23126b3f20b220a65 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 17:17:16 +0200 Subject: [PATCH 2555/3516] Avoid loading platforms in HKC if we are going to raise ConfigEntryNotReady (#75177) --- .../homekit_controller/connection.py | 39 ++++++++++++++++--- .../homekit_controller/test_init.py | 5 ++- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 35f4c6bdb31..e1cbedb01e8 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -14,7 +14,7 @@ from aiohomekit.exceptions import ( EncryptionError, ) from aiohomekit.model import Accessories, Accessory -from aiohomekit.model.characteristics import Characteristic +from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes from aiohomekit.model.services import Service from homeassistant.const import ATTR_VIA_DEVICE @@ -169,6 +169,29 @@ class HKDevice: self.available = available async_dispatcher_send(self.hass, self.signal_state_updated) + async def async_ensure_available(self) -> bool: + """Verify the accessory is available after processing the entity map.""" + if self.available: + return True + if self.watchable_characteristics and self.pollable_characteristics: + # We already tried, no need to try again + return False + # We there are no watchable and not pollable characteristics, + # we need to force a connection to the device to verify its alive. + # + # This is similar to iOS's behavior for keeping alive connections + # to cameras. + # + primary = self.entity_map.accessories[0] + aid = primary.aid + iid = primary.accessory_information[CharacteristicsTypes.SERIAL_NUMBER].iid + try: + await self.pairing.get_characteristics([(aid, iid)]) + except (AccessoryDisconnectedError, EncryptionError, AccessoryNotFoundError): + return False + self.async_set_available_state(True) + return True + async def async_setup(self) -> bool: """Prepare to use a paired HomeKit device in Home Assistant.""" entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP] @@ -180,16 +203,22 @@ class HKDevice: return False await self.async_process_entity_map() - if not self.pairing.is_connected: + + if not await self.async_ensure_available(): return False # If everything is up to date, we can create the entities # since we know the data is not stale. - self.add_entities() + await self.async_add_new_entities() self._polling_interval_remover = async_track_time_interval( self.hass, self.async_update, DEFAULT_SCAN_INTERVAL ) return True + async def async_add_new_entities(self) -> None: + """Add new entities to Home Assistant.""" + await self.async_load_platforms() + self.add_entities() + def device_info_for_accessory(self, accessory: Accessory) -> DeviceInfo: """Build a DeviceInfo for a given accessory.""" identifiers = { @@ -369,8 +398,6 @@ class HKDevice: # Migrate to new device ids self.async_migrate_devices() - await self.async_load_platforms() - self.async_create_devices() # Load any triggers for this config entry @@ -398,7 +425,7 @@ class HKDevice: """Refresh the entity map and entities for this pairing.""" await self.async_refresh_entity_map(config_num) await self.async_process_entity_map() - self.add_entities() + await self.async_add_new_entities() async def async_refresh_entity_map(self, config_num: int) -> bool: """Handle setup of a HomeKit accessory.""" diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 5b4d5c74ff6..841b726ac0e 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -3,7 +3,7 @@ from datetime import timedelta from unittest.mock import patch -from aiohomekit import exceptions +from aiohomekit import AccessoryDisconnectedError, exceptions from aiohomekit.model import Accessory from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes @@ -111,6 +111,9 @@ async def test_offline_device_raises(hass): nonlocal is_connected return is_connected + def get_characteristics(self, chars, *args, **kwargs): + raise AccessoryDisconnectedError("any") + class OfflineFakeDiscovery(FakeDiscovery): """Fake discovery that returns an offline pairing.""" From 1725948d4a527f71eb6681dd03899a7d128c4b50 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Jul 2022 18:06:33 +0200 Subject: [PATCH 2556/3516] Use instance attributes in minecraft_server (#75157) * Remove minecraft_server from mypy ignore list * Use new entity naming style --- .../components/minecraft_server/__init__.py | 62 +++++++------------ .../minecraft_server/binary_sensor.py | 9 +-- .../components/minecraft_server/helpers.py | 4 +- .../components/minecraft_server/sensor.py | 44 ++++--------- mypy.ini | 9 --- script/hassfest/mypy_config.py | 3 - 6 files changed, 42 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index 1b4a71e8ab8..4abfbca9a2f 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -1,21 +1,22 @@ """The Minecraft Server integration.""" from __future__ import annotations +from collections.abc import Mapping from datetime import datetime, timedelta import logging +from typing import Any from mcstatus.server import MinecraftServer as MCStatus from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ConfigType from . import helpers from .const import DOMAIN, MANUFACTURER, SCAN_INTERVAL, SIGNAL_NAME_PREFIX @@ -30,6 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: domain_data = hass.data.setdefault(DOMAIN, {}) # Create and store server instance. + assert entry.unique_id unique_id = entry.unique_id _LOGGER.debug( "Creating server instance for '%s' (%s)", @@ -71,7 +73,7 @@ class MinecraftServer: _MAX_RETRIES_STATUS = 3 def __init__( - self, hass: HomeAssistant, unique_id: str, config_data: ConfigType + self, hass: HomeAssistant, unique_id: str, config_data: Mapping[str, Any] ) -> None: """Initialize server instance.""" self._hass = hass @@ -94,14 +96,14 @@ class MinecraftServer: self.latency_time = None self.players_online = None self.players_max = None - self.players_list = None + self.players_list: list[str] | None = None self.motd = None # Dispatcher signal name self.signal_name = f"{SIGNAL_NAME_PREFIX}_{self.unique_id}" # Callback for stopping periodic update. - self._stop_periodic_update = None + self._stop_periodic_update: CALLBACK_TYPE | None = None def start_periodic_update(self) -> None: """Start periodic execution of update method.""" @@ -111,7 +113,8 @@ class MinecraftServer: def stop_periodic_update(self) -> None: """Stop periodic execution of update method.""" - self._stop_periodic_update() + if self._stop_periodic_update: + self._stop_periodic_update() async def async_check_connection(self) -> None: """Check server connection using a 'status' request and store connection status.""" @@ -219,14 +222,21 @@ class MinecraftServer: class MinecraftServerEntity(Entity): """Representation of a Minecraft Server base entity.""" + _attr_has_entity_name = True + _attr_should_poll = False + def __init__( - self, server: MinecraftServer, type_name: str, icon: str, device_class: str + self, + server: MinecraftServer, + type_name: str, + icon: str, + device_class: str | None, ) -> None: """Initialize base entity.""" self._server = server - self._name = f"{server.name} {type_name}" - self._icon = icon - self._unique_id = f"{self._server.unique_id}-{type_name}" + self._attr_name = type_name + self._attr_icon = icon + self._attr_unique_id = f"{self._server.unique_id}-{type_name}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._server.unique_id)}, manufacturer=MANUFACTURER, @@ -234,34 +244,9 @@ class MinecraftServerEntity(Entity): name=self._server.name, sw_version=self._server.protocol_version, ) - self._device_class = device_class + self._attr_device_class = device_class self._extra_state_attributes = None - self._disconnect_dispatcher = None - - @property - def name(self) -> str: - """Return name.""" - return self._name - - @property - def unique_id(self) -> str: - """Return unique ID.""" - return self._unique_id - - @property - def device_class(self) -> str: - """Return device class.""" - return self._device_class - - @property - def icon(self) -> str: - """Return icon.""" - return self._icon - - @property - def should_poll(self) -> bool: - """Disable polling.""" - return False + self._disconnect_dispatcher: CALLBACK_TYPE | None = None async def async_update(self) -> None: """Fetch data from the server.""" @@ -275,7 +260,8 @@ class MinecraftServerEntity(Entity): async def async_will_remove_from_hass(self) -> None: """Disconnect dispatcher before removal.""" - self._disconnect_dispatcher() + if self._disconnect_dispatcher: + self._disconnect_dispatcher() @callback def _update_callback(self) -> None: diff --git a/homeassistant/components/minecraft_server/binary_sensor.py b/homeassistant/components/minecraft_server/binary_sensor.py index 75c572f366f..0bf4cdab859 100644 --- a/homeassistant/components/minecraft_server/binary_sensor.py +++ b/homeassistant/components/minecraft_server/binary_sensor.py @@ -37,13 +37,8 @@ class MinecraftServerStatusBinarySensor(MinecraftServerEntity, BinarySensorEntit icon=ICON_STATUS, device_class=BinarySensorDeviceClass.CONNECTIVITY, ) - self._is_on = False - - @property - def is_on(self) -> bool: - """Return binary state.""" - return self._is_on + self._attr_is_on = False async def async_update(self) -> None: """Update status.""" - self._is_on = self._server.online + self._attr_is_on = self._server.online diff --git a/homeassistant/components/minecraft_server/helpers.py b/homeassistant/components/minecraft_server/helpers.py index 13ec4cd1afb..7153c170a6a 100644 --- a/homeassistant/components/minecraft_server/helpers.py +++ b/homeassistant/components/minecraft_server/helpers.py @@ -11,7 +11,9 @@ from homeassistant.core import HomeAssistant from .const import SRV_RECORD_PREFIX -async def async_check_srv_record(hass: HomeAssistant, host: str) -> dict[str, Any]: +async def async_check_srv_record( + hass: HomeAssistant, host: str +) -> dict[str, Any] | None: """Check if the given host is a valid Minecraft SRV record.""" # Check if 'host' is a valid SRV record. return_value = None diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index d7ca73d1411..f10a359eca0 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -1,8 +1,6 @@ """The Minecraft Server sensor platform.""" from __future__ import annotations -from typing import Any - from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import TIME_MILLISECONDS @@ -62,30 +60,19 @@ class MinecraftServerSensorEntity(MinecraftServerEntity, SensorEntity): self, server: MinecraftServer, type_name: str, - icon: str = None, - unit: str = None, - device_class: str = None, + icon: str, + unit: str | None, + device_class: str | None = None, ) -> None: """Initialize sensor base entity.""" super().__init__(server, type_name, icon, device_class) - self._state = None - self._unit = unit + self._attr_native_unit_of_measurement = unit @property def available(self) -> bool: """Return sensor availability.""" return self._server.online - @property - def native_value(self) -> Any: - """Return sensor state.""" - return self._state - - @property - def native_unit_of_measurement(self) -> str: - """Return sensor measurement unit.""" - return self._unit - class MinecraftServerVersionSensor(MinecraftServerSensorEntity): """Representation of a Minecraft Server version sensor.""" @@ -98,7 +85,7 @@ class MinecraftServerVersionSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update version.""" - self._state = self._server.version + self._attr_native_value = self._server.version class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity): @@ -115,7 +102,7 @@ class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update protocol version.""" - self._state = self._server.protocol_version + self._attr_native_value = self._server.protocol_version class MinecraftServerLatencyTimeSensor(MinecraftServerSensorEntity): @@ -132,7 +119,7 @@ class MinecraftServerLatencyTimeSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update latency time.""" - self._state = self._server.latency_time + self._attr_native_value = self._server.latency_time class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity): @@ -149,20 +136,15 @@ class MinecraftServerPlayersOnlineSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update online players state and device state attributes.""" - self._state = self._server.players_online + self._attr_native_value = self._server.players_online - extra_state_attributes = None + extra_state_attributes = {} players_list = self._server.players_list if players_list is not None and len(players_list) != 0: - extra_state_attributes = {ATTR_PLAYERS_LIST: self._server.players_list} + extra_state_attributes[ATTR_PLAYERS_LIST] = self._server.players_list - self._extra_state_attributes = extra_state_attributes - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return players list in device state attributes.""" - return self._extra_state_attributes + self._attr_extra_state_attributes = extra_state_attributes class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity): @@ -179,7 +161,7 @@ class MinecraftServerPlayersMaxSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update maximum number of players.""" - self._state = self._server.players_max + self._attr_native_value = self._server.players_max class MinecraftServerMOTDSensor(MinecraftServerSensorEntity): @@ -196,4 +178,4 @@ class MinecraftServerMOTDSensor(MinecraftServerSensorEntity): async def async_update(self) -> None: """Update MOTD.""" - self._state = self._server.motd + self._attr_native_value = self._server.motd diff --git a/mypy.ini b/mypy.ini index 2297869dbb0..b936eab1fd0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2665,15 +2665,6 @@ ignore_errors = true [mypy-homeassistant.components.evohome] ignore_errors = true -[mypy-homeassistant.components.minecraft_server] -ignore_errors = true - -[mypy-homeassistant.components.minecraft_server.helpers] -ignore_errors = true - -[mypy-homeassistant.components.minecraft_server.sensor] -ignore_errors = true - [mypy-homeassistant.components.sonos] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index f53d6a6e6b0..c7bdd8e6c2b 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -19,9 +19,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.cloud.client", "homeassistant.components.cloud.http_api", "homeassistant.components.evohome", - "homeassistant.components.minecraft_server", - "homeassistant.components.minecraft_server.helpers", - "homeassistant.components.minecraft_server.sensor", "homeassistant.components.sonos", "homeassistant.components.sonos.alarms", "homeassistant.components.sonos.binary_sensor", From 666f715e76faf1061aed7cb4ec59611b7b2f4455 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 18:09:09 +0200 Subject: [PATCH 2557/3516] Avoid importing MQTT into core for ServiceInfo dataclass (#74418) * Avoid importing MQTT into core for discovery dataclass Likely fixes #73863 * relo * adjust * rename * rename * rename * adjust missed imports * drop compat * fix conflict correctly * Update homeassistant/helpers/config_entry_flow.py * fix black from trying to fix the conflict in github --- homeassistant/components/mqtt/__init__.py | 15 -------------- homeassistant/components/mqtt/discovery.py | 3 ++- homeassistant/components/mqtt/models.py | 2 +- .../components/tasmota/config_flow.py | 3 ++- homeassistant/config_entries.py | 2 +- homeassistant/helpers/config_entry_flow.py | 20 +++++++++++-------- homeassistant/helpers/service_info/mqtt.py | 20 +++++++++++++++++++ tests/components/tasmota/test_config_flow.py | 10 +++++----- 8 files changed, 43 insertions(+), 32 deletions(-) create mode 100644 homeassistant/helpers/service_info/mqtt.py diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 906923138b9..394e4af3e2e 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -3,8 +3,6 @@ from __future__ import annotations import asyncio from collections.abc import Callable -from dataclasses import dataclass -import datetime as dt import logging from typing import Any, cast @@ -23,7 +21,6 @@ from homeassistant.const import ( SERVICE_RELOAD, ) from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback -from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.device_registry import DeviceEntry @@ -145,18 +142,6 @@ MQTT_PUBLISH_SCHEMA = vol.All( ) -@dataclass -class MqttServiceInfo(BaseServiceInfo): - """Prepared info from mqtt entries.""" - - topic: str - payload: ReceivePayloadType - qos: int - retain: bool - subscribed_topic: str - timestamp: dt.datetime - - async def _async_setup_discovery( hass: HomeAssistant, conf: ConfigType, config_entry ) -> None: diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index a480cdd5680..ebc3a170aeb 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -17,6 +17,7 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_send, ) from homeassistant.helpers.json import json_loads +from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from homeassistant.loader import async_get_mqtt from .. import mqtt @@ -267,7 +268,7 @@ async def async_start( # noqa: C901 if key not in hass.data[INTEGRATION_UNSUBSCRIBE]: return - data = mqtt.MqttServiceInfo( + data = MqttServiceInfo( topic=msg.topic, payload=msg.payload, qos=msg.qos, diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 9bce6baab8b..d5560f6954e 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -12,12 +12,12 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import template from homeassistant.helpers.entity import Entity +from homeassistant.helpers.service_info.mqtt import ReceivePayloadType from homeassistant.helpers.typing import TemplateVarsType _SENTINEL = object() PublishPayloadType = Union[str, bytes, int, float, None] -ReceivePayloadType = Union[str, bytes] @attr.s(slots=True, frozen=True) diff --git a/homeassistant/components/tasmota/config_flow.py b/homeassistant/components/tasmota/config_flow.py index e2109a16afe..d8981090d58 100644 --- a/homeassistant/components/tasmota/config_flow.py +++ b/homeassistant/components/tasmota/config_flow.py @@ -6,8 +6,9 @@ from typing import Any import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.mqtt import MqttServiceInfo, valid_subscribe_topic +from homeassistant.components.mqtt import valid_subscribe_topic from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from .const import CONF_DISCOVERY_PREFIX, DEFAULT_PREFIX, DOMAIN diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 4b76f63681a..6e2b70be7cc 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -31,10 +31,10 @@ if TYPE_CHECKING: from .components.bluetooth import BluetoothServiceInfo from .components.dhcp import DhcpServiceInfo from .components.hassio import HassioServiceInfo - from .components.mqtt import MqttServiceInfo from .components.ssdp import SsdpServiceInfo from .components.usb import UsbServiceInfo from .components.zeroconf import ZeroconfServiceInfo + from .helpers.service_info.mqtt import MqttServiceInfo _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 75a4dcd20f4..6ee04ea6d9b 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -6,7 +6,7 @@ import logging from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, cast from homeassistant import config_entries -from homeassistant.components import bluetooth, dhcp, onboarding, ssdp, zeroconf +from homeassistant.components import onboarding from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult @@ -15,8 +15,12 @@ from .typing import UNDEFINED, DiscoveryInfoType, UndefinedType if TYPE_CHECKING: import asyncio - from homeassistant.components import mqtt + from homeassistant.components.bluetooth import BluetoothServiceInfo + from homeassistant.components.dhcp import DhcpServiceInfo + from homeassistant.components.ssdp import SsdpServiceInfo + from homeassistant.components.zeroconf import ZeroconfServiceInfo + from .service_info.mqtt import MqttServiceInfo _R = TypeVar("_R", bound="Awaitable[bool] | bool") DiscoveryFunctionType = Callable[[HomeAssistant], _R] @@ -93,7 +97,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() async def async_step_bluetooth( - self, discovery_info: bluetooth.BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfo ) -> FlowResult: """Handle a flow initialized by bluetooth discovery.""" if self._async_in_progress() or self._async_current_entries(): @@ -103,7 +107,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() - async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: """Handle a flow initialized by dhcp discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -113,7 +117,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() async def async_step_homekit( - self, discovery_info: zeroconf.ZeroconfServiceInfo + self, discovery_info: ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by Homekit discovery.""" if self._async_in_progress() or self._async_current_entries(): @@ -123,7 +127,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() - async def async_step_mqtt(self, discovery_info: mqtt.MqttServiceInfo) -> FlowResult: + async def async_step_mqtt(self, discovery_info: MqttServiceInfo) -> FlowResult: """Handle a flow initialized by mqtt discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -133,7 +137,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() async def async_step_zeroconf( - self, discovery_info: zeroconf.ZeroconfServiceInfo + self, discovery_info: ZeroconfServiceInfo ) -> FlowResult: """Handle a flow initialized by Zeroconf discovery.""" if self._async_in_progress() or self._async_current_entries(): @@ -143,7 +147,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + async def async_step_ssdp(self, discovery_info: SsdpServiceInfo) -> FlowResult: """Handle a flow initialized by Ssdp discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/helpers/service_info/mqtt.py b/homeassistant/helpers/service_info/mqtt.py new file mode 100644 index 00000000000..fcf5d4744f1 --- /dev/null +++ b/homeassistant/helpers/service_info/mqtt.py @@ -0,0 +1,20 @@ +"""MQTT Discovery data.""" +from dataclasses import dataclass +import datetime as dt +from typing import Union + +from homeassistant.data_entry_flow import BaseServiceInfo + +ReceivePayloadType = Union[str, bytes] + + +@dataclass +class MqttServiceInfo(BaseServiceInfo): + """Prepared info from mqtt entries.""" + + topic: str + payload: ReceivePayloadType + qos: int + retain: bool + subscribed_topic: str + timestamp: dt.datetime diff --git a/tests/components/tasmota/test_config_flow.py b/tests/components/tasmota/test_config_flow.py index 3413817892b..77a2787c0d5 100644 --- a/tests/components/tasmota/test_config_flow.py +++ b/tests/components/tasmota/test_config_flow.py @@ -1,6 +1,6 @@ """Test config flow.""" from homeassistant import config_entries -from homeassistant.components import mqtt +from homeassistant.helpers.service_info.mqtt import MqttServiceInfo from tests.common import MockConfigEntry @@ -19,7 +19,7 @@ async def test_mqtt_abort_if_existing_entry(hass, mqtt_mock): async def test_mqtt_abort_invalid_topic(hass, mqtt_mock): """Check MQTT flow aborts if discovery topic is invalid.""" - discovery_info = mqtt.MqttServiceInfo( + discovery_info = MqttServiceInfo( topic="tasmota/discovery/DC4F220848A2/bla", payload=( '{"ip":"192.168.0.136","dn":"Tasmota","fn":["Tasmota",null,null,null,null,' @@ -42,7 +42,7 @@ async def test_mqtt_abort_invalid_topic(hass, mqtt_mock): assert result["type"] == "abort" assert result["reason"] == "invalid_discovery_info" - discovery_info = mqtt.MqttServiceInfo( + discovery_info = MqttServiceInfo( topic="tasmota/discovery/DC4F220848A2/config", payload="", qos=0, @@ -56,7 +56,7 @@ async def test_mqtt_abort_invalid_topic(hass, mqtt_mock): assert result["type"] == "abort" assert result["reason"] == "invalid_discovery_info" - discovery_info = mqtt.MqttServiceInfo( + discovery_info = MqttServiceInfo( topic="tasmota/discovery/DC4F220848A2/config", payload=( '{"ip":"192.168.0.136","dn":"Tasmota","fn":["Tasmota",null,null,null,null,' @@ -81,7 +81,7 @@ async def test_mqtt_abort_invalid_topic(hass, mqtt_mock): async def test_mqtt_setup(hass, mqtt_mock) -> None: """Test we can finish a config flow through MQTT with custom prefix.""" - discovery_info = mqtt.MqttServiceInfo( + discovery_info = MqttServiceInfo( topic="tasmota/discovery/DC4F220848A2/config", payload=( '{"ip":"192.168.0.136","dn":"Tasmota","fn":["Tasmota",null,null,null,null,' From bb14f83b94fbe8e1eea486094f8320117dc4bcf5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 18:30:20 +0200 Subject: [PATCH 2558/3516] Bump unifi-discovery to 1.1.5 (#75189) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/pip_check | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 35dafc96927..2410ea91399 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.10", "unifi-discovery==1.1.4"], + "requirements": ["pyunifiprotect==4.0.10", "unifi-discovery==1.1.5"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 2904f51d6c3..04ca43b481d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2358,7 +2358,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.unifiprotect -unifi-discovery==1.1.4 +unifi-discovery==1.1.5 # homeassistant.components.unifiled unifiled==0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a3f0532550..e91630a0610 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1570,7 +1570,7 @@ twitchAPI==2.5.2 uasiren==0.0.1 # homeassistant.components.unifiprotect -unifi-discovery==1.1.4 +unifi-discovery==1.1.5 # homeassistant.components.upb upb_lib==0.4.12 diff --git a/script/pip_check b/script/pip_check index 6f48ce15bb1..f29ea5a5dd0 100755 --- a/script/pip_check +++ b/script/pip_check @@ -3,7 +3,7 @@ PIP_CACHE=$1 # Number of existing dependency conflicts # Update if a PR resolve one! -DEPENDENCY_CONFLICTS=7 +DEPENDENCY_CONFLICTS=6 PIP_CHECK=$(pip check --cache-dir=$PIP_CACHE) LINE_COUNT=$(echo "$PIP_CHECK" | wc -l) From 56da7d0ad06fdeef8e409ba85d114c84b883a8f8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jul 2022 19:00:06 +0200 Subject: [PATCH 2559/3516] Allow Mjpeg camera name to be None (#75002) --- homeassistant/components/mjpeg/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index 91b2271aafa..5c42d392142 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -85,7 +85,7 @@ class MjpegCamera(Camera): def __init__( self, *, - name: str, + name: str | None = None, mjpeg_url: str, still_image_url: str | None, authentication: str | None = None, From 5e46fa6f8ba4c2f8f21421bfefe9fb422b713437 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:38:22 -0400 Subject: [PATCH 2560/3516] Skip `iso4217` version 1.10, which includes a broken `__init__.pyi` file (#75200) --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 72d88ab7cf5..09eb66c5f34 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -118,3 +118,7 @@ pydantic!=1.9.1 # Breaks asyncio # https://github.com/pubnub/python/issues/130 pubnub!=6.4.0 + +# Package's __init__.pyi stub has invalid syntax and breaks mypy +# https://github.com/dahlia/iso4217/issues/16 +iso4217!=1.10.20220401 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 7bdecd9e0c2..dcc53a73df0 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -136,6 +136,10 @@ pydantic!=1.9.1 # Breaks asyncio # https://github.com/pubnub/python/issues/130 pubnub!=6.4.0 + +# Package's __init__.pyi stub has invalid syntax and breaks mypy +# https://github.com/dahlia/iso4217/issues/16 +iso4217!=1.10.20220401 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From fde3489e86bc22eb00a21e59e3ef13406b9a573f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 20:36:54 +0200 Subject: [PATCH 2561/3516] Relocate BluetoothServiceInfo to helpers.service_info (#75195) --- .../components/bluetooth/__init__.py | 48 +-------------- .../bluetooth_tracker/device_tracker.py | 6 +- homeassistant/config_entries.py | 2 +- homeassistant/helpers/config_entry_flow.py | 2 +- .../helpers/service_info/bluetooth.py | 58 +++++++++++++++++++ 5 files changed, 65 insertions(+), 51 deletions(-) create mode 100644 homeassistant/helpers/service_info/bluetooth.py diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 26c1af3fb92..01f55048c6d 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -2,16 +2,14 @@ from __future__ import annotations from collections.abc import Callable -import dataclasses from enum import Enum import fnmatch -from functools import cached_property import logging import platform from typing import Final, TypedDict from bleak import BleakError -from bleak.backends.device import MANUFACTURERS, BLEDevice +from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData from lru import LRU # pylint: disable=no-name-in-module @@ -23,8 +21,8 @@ from homeassistant.core import ( HomeAssistant, callback as hass_callback, ) -from homeassistant.data_entry_flow import BaseServiceInfo from homeassistant.helpers import discovery_flow +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.helpers.typing import ConfigType from homeassistant.loader import ( BluetoothMatcher, @@ -74,48 +72,6 @@ MANUFACTURER_ID: Final = "manufacturer_id" MANUFACTURER_DATA_FIRST_BYTE: Final = "manufacturer_data_first_byte" -@dataclasses.dataclass -class BluetoothServiceInfo(BaseServiceInfo): - """Prepared info from bluetooth entries.""" - - name: str - address: str - rssi: int - manufacturer_data: dict[int, bytes] - service_data: dict[str, bytes] - service_uuids: list[str] - - @classmethod - def from_advertisement( - cls, device: BLEDevice, advertisement_data: AdvertisementData - ) -> BluetoothServiceInfo: - """Create a BluetoothServiceInfo from an advertisement.""" - return cls( - name=advertisement_data.local_name or device.name or device.address, - address=device.address, - rssi=device.rssi, - manufacturer_data=advertisement_data.manufacturer_data, - service_data=advertisement_data.service_data, - service_uuids=advertisement_data.service_uuids, - ) - - @cached_property - def manufacturer(self) -> str | None: - """Convert manufacturer data to a string.""" - for manufacturer in self.manufacturer_data: - if manufacturer in MANUFACTURERS: - name: str = MANUFACTURERS[manufacturer] - return name - return None - - @cached_property - def manufacturer_id(self) -> int | None: - """Get the first manufacturer id.""" - for manufacturer in self.manufacturer_data: - return manufacturer - return None - - BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") BluetoothCallback = Callable[[BluetoothServiceInfo, BluetoothChange], None] diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index b62333c0489..5cf173d356e 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -61,7 +61,7 @@ def is_bluetooth_device(device: Device) -> bool: def discover_devices(device_id: int) -> list[tuple[str, str]]: """Discover Bluetooth devices.""" try: - result = bluetooth.discover_devices( + result = bluetooth.discover_devices( # type: ignore[attr-defined] duration=8, lookup_names=True, flush_cache=True, @@ -124,7 +124,7 @@ async def get_tracking_devices(hass: HomeAssistant) -> tuple[set[str], set[str]] def lookup_name(mac: str) -> str | None: """Lookup a Bluetooth device name.""" _LOGGER.debug("Scanning %s", mac) - return bluetooth.lookup_name(mac, timeout=5) # type: ignore[no-any-return] + return bluetooth.lookup_name(mac, timeout=5) # type: ignore[attr-defined,no-any-return] async def async_setup_scanner( @@ -180,7 +180,7 @@ async def async_setup_scanner( if tasks: await asyncio.wait(tasks) - except bluetooth.BluetoothError: + except bluetooth.BluetoothError: # type: ignore[attr-defined] _LOGGER.exception("Error looking up Bluetooth device") async def update_bluetooth(now: datetime | None = None) -> None: diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 6e2b70be7cc..e5a36b980ed 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -28,12 +28,12 @@ from .util import uuid as uuid_util from .util.decorator import Registry if TYPE_CHECKING: - from .components.bluetooth import BluetoothServiceInfo from .components.dhcp import DhcpServiceInfo from .components.hassio import HassioServiceInfo from .components.ssdp import SsdpServiceInfo from .components.usb import UsbServiceInfo from .components.zeroconf import ZeroconfServiceInfo + from .helpers.service_info.bluetooth import BluetoothServiceInfo from .helpers.service_info.mqtt import MqttServiceInfo _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 6ee04ea6d9b..847a90a0d1b 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -15,11 +15,11 @@ from .typing import UNDEFINED, DiscoveryInfoType, UndefinedType if TYPE_CHECKING: import asyncio - from homeassistant.components.bluetooth import BluetoothServiceInfo from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.ssdp import SsdpServiceInfo from homeassistant.components.zeroconf import ZeroconfServiceInfo + from .service_info.bluetooth import BluetoothServiceInfo from .service_info.mqtt import MqttServiceInfo _R = TypeVar("_R", bound="Awaitable[bool] | bool") diff --git a/homeassistant/helpers/service_info/bluetooth.py b/homeassistant/helpers/service_info/bluetooth.py new file mode 100644 index 00000000000..9dff2782da0 --- /dev/null +++ b/homeassistant/helpers/service_info/bluetooth.py @@ -0,0 +1,58 @@ +"""The bluetooth integration service info.""" +from __future__ import annotations + +import dataclasses +from functools import cached_property +from typing import TYPE_CHECKING + +from homeassistant.data_entry_flow import BaseServiceInfo + +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + from bleak.backends.scanner import AdvertisementData + + +@dataclasses.dataclass +class BluetoothServiceInfo(BaseServiceInfo): + """Prepared info from bluetooth entries.""" + + name: str + address: str + rssi: int + manufacturer_data: dict[int, bytes] + service_data: dict[str, bytes] + service_uuids: list[str] + + @classmethod + def from_advertisement( + cls, device: BLEDevice, advertisement_data: AdvertisementData + ) -> BluetoothServiceInfo: + """Create a BluetoothServiceInfo from an advertisement.""" + return cls( + name=advertisement_data.local_name or device.name or device.address, + address=device.address, + rssi=device.rssi, + manufacturer_data=advertisement_data.manufacturer_data, + service_data=advertisement_data.service_data, + service_uuids=advertisement_data.service_uuids, + ) + + @cached_property + def manufacturer(self) -> str | None: + """Convert manufacturer data to a string.""" + from bleak.backends.device import ( # pylint: disable=import-outside-toplevel + MANUFACTURERS, + ) + + for manufacturer in self.manufacturer_data: + if manufacturer in MANUFACTURERS: + name: str = MANUFACTURERS[manufacturer] + return name + return None + + @cached_property + def manufacturer_id(self) -> int | None: + """Get the first manufacturer id.""" + for manufacturer in self.manufacturer_data: + return manufacturer + return None From 5f08052f40b1d1eca574ec546b5d8acb33a148fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 20:48:30 +0200 Subject: [PATCH 2562/3516] Move lutron_caseta migration to happen after successful setup (#75204) --- homeassistant/components/lutron_caseta/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index cea069a1556..d235f294b3a 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -143,8 +143,6 @@ async def async_setup_entry( ca_certs = hass.config.path(config_entry.data[CONF_CA_CERTS]) bridge = None - await _async_migrate_unique_ids(hass, config_entry) - try: bridge = Smartbridge.create_tls( hostname=host, keyfile=keyfile, certfile=certfile, ca_certs=ca_certs @@ -167,6 +165,7 @@ async def async_setup_entry( raise ConfigEntryNotReady(f"Cannot connect to {host}") _LOGGER.debug("Connected to Lutron Caseta bridge via LEAP at %s", host) + await _async_migrate_unique_ids(hass, config_entry) devices = bridge.get_devices() bridge_device = devices[BRIDGE_DEVICE_ID] From 124bfe1629618cb3d6f0ae9d1db054431e4e349a Mon Sep 17 00:00:00 2001 From: Khole Date: Thu, 14 Jul 2022 19:50:35 +0100 Subject: [PATCH 2563/3516] Fix Hive power unit of measurement (#75210) --- homeassistant/components/hive/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index d224ec085f3..809feb19d5e 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -8,7 +8,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, POWER_KILO_WATT +from homeassistant.const import PERCENTAGE, POWER_WATT from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -28,7 +28,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="Power", - native_unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, From 874043f5962f03c5f54b4f27462fbb7a7dc0126b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 14 Jul 2022 21:01:57 +0200 Subject: [PATCH 2564/3516] Migrate Axis to new entity naming style (#74735) --- homeassistant/components/axis/axis_base.py | 4 +++- homeassistant/components/axis/binary_sensor.py | 6 ++---- homeassistant/components/axis/camera.py | 1 - homeassistant/components/axis/light.py | 2 +- homeassistant/components/axis/switch.py | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py index 791764dc605..3d887478528 100644 --- a/homeassistant/components/axis/axis_base.py +++ b/homeassistant/components/axis/axis_base.py @@ -10,6 +10,8 @@ from .const import DOMAIN as AXIS_DOMAIN class AxisEntityBase(Entity): """Base common to all Axis entities.""" + _attr_has_entity_name = True + def __init__(self, device): """Initialize the Axis event.""" self.device = device @@ -47,7 +49,7 @@ class AxisEventBase(AxisEntityBase): super().__init__(device) self.event = event - self._attr_name = f"{device.name} {event.TYPE} {event.id}" + self._attr_name = f"{event.TYPE} {event.id}" self._attr_unique_id = f"{device.unique_id}-{event.topic}-{event.id}" self._attr_device_class = event.CLASS diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index d2b6a9f7dd7..fba7e8d6248 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -110,9 +110,7 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): and self.event.id in self.device.api.vapix.ports and self.device.api.vapix.ports[self.event.id].name ): - return ( - f"{self.device.name} {self.device.api.vapix.ports[self.event.id].name}" - ) + return self.device.api.vapix.ports[self.event.id].name if self.event.CLASS == CLASS_MOTION: @@ -128,6 +126,6 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): and event_data and self.event.id in event_data ): - return f"{self.device.name} {self.event.TYPE} {event_data[self.event.id].name}" + return f"{self.event.TYPE} {event_data[self.event.id].name}" return self._attr_name diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index bd46ae54f81..4df9a5e2141 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -40,7 +40,6 @@ class AxisCamera(AxisEntityBase, MjpegCamera): MjpegCamera.__init__( self, - name=device.name, username=device.username, password=device.password, mjpeg_url=self.mjpeg_source, diff --git a/homeassistant/components/axis/light.py b/homeassistant/components/axis/light.py index 30f1cee9340..e34c0d4a2d6 100644 --- a/homeassistant/components/axis/light.py +++ b/homeassistant/components/axis/light.py @@ -53,7 +53,7 @@ class AxisLight(AxisEventBase, LightEntity): self.max_intensity = 0 light_type = device.api.vapix.light_control[self.light_id].light_type - self._attr_name = f"{device.name} {light_type} {event.TYPE} {event.id}" + self._attr_name = f"{light_type} {event.TYPE} {event.id}" self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} self._attr_color_mode = ColorMode.BRIGHTNESS diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index a778c974737..61f16cfc789 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -40,7 +40,7 @@ class AxisSwitch(AxisEventBase, SwitchEntity): super().__init__(event, device) if event.id and device.api.vapix.ports[event.id].name: - self._attr_name = f"{device.name} {device.api.vapix.ports[event.id].name}" + self._attr_name = device.api.vapix.ports[event.id].name @property def is_on(self): From 3f6e9304898431b7073842e94690417a3f32dec6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 21:03:12 +0200 Subject: [PATCH 2565/3516] Bump nexia to 2.0.2 (#75209) --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index cc5e6de8641..e381dc95897 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,7 +1,7 @@ { "domain": "nexia", "name": "Nexia/American Standard/Trane", - "requirements": ["nexia==2.0.1"], + "requirements": ["nexia==2.0.2"], "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 04ca43b481d..ad537da36ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1086,7 +1086,7 @@ nettigo-air-monitor==1.3.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==2.0.1 +nexia==2.0.2 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e91630a0610..a0bf0ab1d8a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -757,7 +757,7 @@ netmap==0.7.0.2 nettigo-air-monitor==1.3.0 # homeassistant.components.nexia -nexia==2.0.1 +nexia==2.0.2 # homeassistant.components.discord nextcord==2.0.0a8 From 54a939e22332a45bfd60b551268348ad71c3f0d4 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 14 Jul 2022 21:04:41 +0200 Subject: [PATCH 2566/3516] Migrate Filesize to new entity naming style (#75199) --- homeassistant/components/filesize/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index 5f66aefaab5..06e567f14cb 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -119,6 +119,7 @@ class FilesizeEntity(CoordinatorEntity[FileSizeCoordinator], SensorEntity): """Filesize sensor.""" entity_description: SensorEntityDescription + _attr_has_entity_name = True def __init__( self, @@ -130,7 +131,7 @@ class FilesizeEntity(CoordinatorEntity[FileSizeCoordinator], SensorEntity): """Initialize the Filesize sensor.""" super().__init__(coordinator) base_name = path.split("/")[-1] - self._attr_name = f"{base_name} {description.name}" + self._attr_name = description.name self._attr_unique_id = ( entry_id if description.key == "file" else f"{entry_id}-{description.key}" ) From 3d2101cac53a08644d6d4bf1e14b9f99fd874e7c Mon Sep 17 00:00:00 2001 From: Peter Galantha Date: Thu, 14 Jul 2022 12:14:25 -0700 Subject: [PATCH 2567/3516] Add total state_class for esphome (#75015) --- homeassistant/components/esphome/manifest.json | 2 +- homeassistant/components/esphome/sensor.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index a8a76c2b0c8..cf748b27170 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==10.10.0"], + "requirements": ["aioesphomeapi==10.11.0"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 45a73a5e5af..897ab86b18a 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -67,6 +67,7 @@ _STATE_CLASSES: EsphomeEnumMapper[ EsphomeSensorStateClass.NONE: None, EsphomeSensorStateClass.MEASUREMENT: SensorStateClass.MEASUREMENT, EsphomeSensorStateClass.TOTAL_INCREASING: SensorStateClass.TOTAL_INCREASING, + EsphomeSensorStateClass.TOTAL: SensorStateClass.TOTAL, } ) diff --git a/requirements_all.txt b/requirements_all.txt index ad537da36ff..24850836455 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -150,7 +150,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.10.0 +aioesphomeapi==10.11.0 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a0bf0ab1d8a..22a93dacae8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,7 +137,7 @@ aioeagle==1.1.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==10.10.0 +aioesphomeapi==10.11.0 # homeassistant.components.flo aioflo==2021.11.0 From b7cdf5412b8b30b85c1668eda1d9f7b294fdd60c Mon Sep 17 00:00:00 2001 From: mkmer Date: Thu, 14 Jul 2022 15:14:41 -0400 Subject: [PATCH 2568/3516] Bumped AIOAladdin Connect to 0.1.24 (#75182) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index bdf906ad200..8ef86a76bf8 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.23"], + "requirements": ["AIOAladdinConnect==0.1.24"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 24850836455..e0f109c4a14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.23 +AIOAladdinConnect==0.1.24 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 22a93dacae8..6f1b457ade5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.23 +AIOAladdinConnect==0.1.24 # homeassistant.components.adax Adax-local==0.1.4 From 5287980f48279d4add33252b82cb3a63ff67d989 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Jul 2022 21:15:37 +0200 Subject: [PATCH 2569/3516] Remove template from mypy ignore list (#74426) --- homeassistant/components/number/__init__.py | 2 +- homeassistant/components/template/number.py | 8 ++++---- homeassistant/components/template/sensor.py | 2 +- mypy.ini | 5 ----- script/hassfest/mypy_config.py | 2 -- 5 files changed, 6 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 8c1800697a3..5ab1eaa7be4 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -200,7 +200,7 @@ class NumberEntity(Entity): _attr_native_max_value: float _attr_native_min_value: float _attr_native_step: float - _attr_native_value: float + _attr_native_value: float | None = None _attr_native_unit_of_measurement: str | None _deprecated_number_entity_reported = False _number_option_unit_of_measurement: str | None = None diff --git a/homeassistant/components/template/number.py b/homeassistant/components/template/number.py index d41bdee597b..89f4d22b957 100644 --- a/homeassistant/components/template/number.py +++ b/homeassistant/components/template/number.py @@ -14,6 +14,7 @@ from homeassistant.components.number.const import ( ATTR_VALUE, DEFAULT_MAX_VALUE, DEFAULT_MIN_VALUE, + DEFAULT_STEP, DOMAIN as NUMBER_DOMAIN, ) from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_STATE, CONF_UNIQUE_ID @@ -119,10 +120,9 @@ class TemplateNumber(TemplateEntity, NumberEntity): self._min_value_template = config[ATTR_MIN] self._max_value_template = config[ATTR_MAX] self._attr_assumed_state = self._optimistic = config[CONF_OPTIMISTIC] - self._attr_native_value = None - self._attr_native_step = None - self._attr_native_min_value = None - self._attr_native_max_value = None + self._attr_native_step = DEFAULT_STEP + self._attr_native_min_value = DEFAULT_MIN_VALUE + self._attr_native_max_value = DEFAULT_MAX_VALUE async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 8dcda988e9f..b52953c1a8a 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -205,7 +205,7 @@ class SensorTemplate(TemplateSensor): ) -> None: """Initialize the sensor.""" super().__init__(hass, config=config, fallback_name=None, unique_id=unique_id) - self._template = config.get(CONF_STATE) + self._template: template.Template = config[CONF_STATE] if (object_id := config.get(CONF_OBJECT_ID)) is not None: self.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, object_id, hass=hass diff --git a/mypy.ini b/mypy.ini index b936eab1fd0..d92cca692a4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2701,8 +2701,3 @@ ignore_errors = true [mypy-homeassistant.components.sonos.statistics] ignore_errors = true -[mypy-homeassistant.components.template.number] -ignore_errors = true - -[mypy-homeassistant.components.template.sensor] -ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index c7bdd8e6c2b..d2c11d4744d 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -31,8 +31,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.sonos.sensor", "homeassistant.components.sonos.speaker", "homeassistant.components.sonos.statistics", - "homeassistant.components.template.number", - "homeassistant.components.template.sensor", ] # Component modules which should set no_implicit_reexport = true. From bdc63b692bb39f9f13a3db18ea1758a63da43df1 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 14 Jul 2022 15:15:51 -0400 Subject: [PATCH 2570/3516] Bump zigpy from 0.47.2 to 0.47.3 (#75194) --- 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 ce1da9071b4..5d5ce7fef12 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.77", "zigpy-deconz==0.18.0", - "zigpy==0.47.2", + "zigpy==0.47.3", "zigpy-xbee==0.15.0", "zigpy-zigate==0.9.0", "zigpy-znp==0.8.1" diff --git a/requirements_all.txt b/requirements_all.txt index e0f109c4a14..790ecd54b81 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2522,7 +2522,7 @@ zigpy-zigate==0.9.0 zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.47.2 +zigpy==0.47.3 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f1b457ade5..a24335f074c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1686,7 +1686,7 @@ zigpy-zigate==0.9.0 zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.47.2 +zigpy==0.47.3 # homeassistant.components.zwave_js zwave-js-server-python==0.39.0 From 6184f0557d82b43f1649de2bd55f39228bcf4ada Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 21:18:55 +0200 Subject: [PATCH 2571/3516] Bump pyunifiprotect to 4.0.11 (#75215) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 2410ea91399..815b5250e1d 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.10", "unifi-discovery==1.1.5"], + "requirements": ["pyunifiprotect==4.0.11", "unifi-discovery==1.1.5"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 790ecd54b81..aa20e5270fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1996,7 +1996,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.10 +pyunifiprotect==4.0.11 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a24335f074c..719af4677fa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1337,7 +1337,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.10 +pyunifiprotect==4.0.11 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 75892385bb656347f6e9b4ea4474eb43b5a8dcf9 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 15 Jul 2022 03:24:24 +0800 Subject: [PATCH 2572/3516] Fix playback of hls cameras in stream (#75166) --- homeassistant/components/stream/__init__.py | 19 +++-- homeassistant/components/stream/core.py | 39 ++++++++-- homeassistant/components/stream/fmp4utils.py | 12 +++ homeassistant/components/stream/worker.py | 78 ++++++++++++++------ tests/components/stream/test_worker.py | 4 +- 5 files changed, 111 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index b842eb7fb78..ef68ea7bcae 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -57,9 +57,15 @@ from .const import ( SOURCE_TIMEOUT, STREAM_RESTART_INCREMENT, STREAM_RESTART_RESET_TIME, - TARGET_SEGMENT_DURATION_NON_LL_HLS, ) -from .core import PROVIDERS, IdleTimer, KeyFrameConverter, StreamOutput, StreamSettings +from .core import ( + PROVIDERS, + STREAM_SETTINGS_NON_LL_HLS, + IdleTimer, + KeyFrameConverter, + StreamOutput, + StreamSettings, +) from .diagnostics import Diagnostics from .hls import HlsStreamOutput, async_setup_hls @@ -224,14 +230,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hls_part_timeout=2 * conf[CONF_PART_DURATION], ) else: - hass.data[DOMAIN][ATTR_SETTINGS] = StreamSettings( - ll_hls=False, - min_segment_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS - - SEGMENT_DURATION_ADJUSTER, - part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS, - hls_advance_part_limit=3, - hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS, - ) + hass.data[DOMAIN][ATTR_SETTINGS] = STREAM_SETTINGS_NON_LL_HLS # Setup HLS hls_endpoint = async_setup_hls(hass) diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 09d9a9d5031..8c456af91aa 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -5,6 +5,7 @@ import asyncio from collections import deque from collections.abc import Callable, Coroutine, Iterable import datetime +import logging from typing import TYPE_CHECKING, Any from aiohttp import web @@ -16,13 +17,20 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.event import async_call_later from homeassistant.util.decorator import Registry -from .const import ATTR_STREAMS, DOMAIN +from .const import ( + ATTR_STREAMS, + DOMAIN, + SEGMENT_DURATION_ADJUSTER, + TARGET_SEGMENT_DURATION_NON_LL_HLS, +) if TYPE_CHECKING: from av import CodecContext, Packet from . import Stream +_LOGGER = logging.getLogger(__name__) + PROVIDERS: Registry[str, type[StreamOutput]] = Registry() @@ -37,6 +45,15 @@ class StreamSettings: hls_part_timeout: float = attr.ib() +STREAM_SETTINGS_NON_LL_HLS = StreamSettings( + ll_hls=False, + min_segment_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS - SEGMENT_DURATION_ADJUSTER, + part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS, + hls_advance_part_limit=3, + hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS, +) + + @attr.s(slots=True) class Part: """Represent a segment part.""" @@ -426,12 +443,22 @@ class KeyFrameConverter: return packet = self.packet self.packet = None - # decode packet (flush afterwards) - frames = self._codec_context.decode(packet) - for _i in range(2): - if frames: + for _ in range(2): # Retry once if codec context needs to be flushed + try: + # decode packet (flush afterwards) + frames = self._codec_context.decode(packet) + for _i in range(2): + if frames: + break + frames = self._codec_context.decode(None) break - frames = self._codec_context.decode(None) + except EOFError: + _LOGGER.debug("Codec context needs flushing, attempting to reopen") + self._codec_context.close() + self._codec_context.open() + else: + _LOGGER.debug("Unable to decode keyframe") + return if frames: frame = frames[0] if width and height: diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index f136784cf87..313f5632841 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -2,6 +2,10 @@ from __future__ import annotations from collections.abc import Generator +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from io import BytesIO def find_box( @@ -135,3 +139,11 @@ def get_codec_string(mp4_bytes: bytes) -> str: codecs.append(codec) return ",".join(codecs) + + +def read_init(bytes_io: BytesIO) -> bytes: + """Read the init from a mp4 file.""" + bytes_io.seek(24) + moov_len = int.from_bytes(bytes_io.read(4), byteorder="big") + bytes_io.seek(0) + return bytes_io.read(24 + moov_len) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index e46d83542f7..aa49a6de7ae 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -5,11 +5,12 @@ from collections import defaultdict, deque from collections.abc import Callable, Generator, Iterator, Mapping import contextlib import datetime -from io import BytesIO +from io import SEEK_END, BytesIO import logging from threading import Event from typing import Any, cast +import attr import av from homeassistant.core import HomeAssistant @@ -24,8 +25,16 @@ from .const import ( SEGMENT_CONTAINER_FORMAT, SOURCE_TIMEOUT, ) -from .core import KeyFrameConverter, Part, Segment, StreamOutput, StreamSettings +from .core import ( + STREAM_SETTINGS_NON_LL_HLS, + KeyFrameConverter, + Part, + Segment, + StreamOutput, + StreamSettings, +) from .diagnostics import Diagnostics +from .fmp4utils import read_init from .hls import HlsStreamOutput _LOGGER = logging.getLogger(__name__) @@ -108,7 +117,7 @@ class StreamMuxer: hass: HomeAssistant, video_stream: av.video.VideoStream, audio_stream: av.audio.stream.AudioStream | None, - audio_bsf: av.BitStreamFilterContext | None, + audio_bsf: av.BitStreamFilter | None, stream_state: StreamState, stream_settings: StreamSettings, ) -> None: @@ -120,6 +129,7 @@ class StreamMuxer: self._input_video_stream: av.video.VideoStream = video_stream self._input_audio_stream: av.audio.stream.AudioStream | None = audio_stream self._audio_bsf = audio_bsf + self._audio_bsf_context: av.BitStreamFilterContext = None self._output_video_stream: av.video.VideoStream = None self._output_audio_stream: av.audio.stream.AudioStream | None = None self._segment: Segment | None = None @@ -151,7 +161,7 @@ class StreamMuxer: **{ # Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970 # "cmaf" flag replaces several of the movflags used, but too recent to use for now - "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer", + "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer+delay_moov", # Sometimes the first segment begins with negative timestamps, and this setting just # adjusts the timestamps in the output from that segment to start from 0. Helps from # having to make some adjustments in test_durations @@ -164,7 +174,7 @@ class StreamMuxer: # Fragment durations may exceed the 15% allowed variance but it seems ok **( { - "movflags": "empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer", + "movflags": "empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer+delay_moov", # 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". @@ -194,8 +204,11 @@ class StreamMuxer: # Check if audio is requested output_astream = None if input_astream: + if self._audio_bsf: + self._audio_bsf_context = self._audio_bsf.create() + self._audio_bsf_context.set_input_stream(input_astream) output_astream = container.add_stream( - template=self._audio_bsf or input_astream + template=self._audio_bsf_context or input_astream ) return container, output_vstream, output_astream @@ -238,15 +251,29 @@ class StreamMuxer: self._part_has_keyframe |= packet.is_keyframe elif packet.stream == self._input_audio_stream: - if self._audio_bsf: - self._audio_bsf.send(packet) - while packet := self._audio_bsf.recv(): + if self._audio_bsf_context: + self._audio_bsf_context.send(packet) + while packet := self._audio_bsf_context.recv(): packet.stream = self._output_audio_stream self._av_output.mux(packet) return packet.stream = self._output_audio_stream self._av_output.mux(packet) + def create_segment(self) -> None: + """Create a segment when the moov is ready.""" + self._segment = Segment( + sequence=self._stream_state.sequence, + stream_id=self._stream_state.stream_id, + init=read_init(self._memory_file), + # Fetch the latest StreamOutputs, which may have changed since the + # worker started. + stream_outputs=self._stream_state.outputs, + start_time=self._start_time, + ) + self._memory_file_pos = self._memory_file.tell() + self._memory_file.seek(0, SEEK_END) + def check_flush_part(self, packet: av.Packet) -> None: """Check for and mark a part segment boundary and record its duration.""" if self._memory_file_pos == self._memory_file.tell(): @@ -254,16 +281,10 @@ class StreamMuxer: if self._segment is None: # We have our first non-zero byte position. This means the init has just # been written. Create a Segment and put it to the queue of each output. - self._segment = Segment( - sequence=self._stream_state.sequence, - stream_id=self._stream_state.stream_id, - init=self._memory_file.getvalue(), - # Fetch the latest StreamOutputs, which may have changed since the - # worker started. - stream_outputs=self._stream_state.outputs, - start_time=self._start_time, - ) - self._memory_file_pos = self._memory_file.tell() + self.create_segment() + # When using delay_moov, the moov is not written until a moof is also ready + # Flush the moof + self.flush(packet, last_part=False) else: # These are the ends of the part segments self.flush(packet, last_part=False) @@ -297,6 +318,10 @@ class StreamMuxer: # Closing the av_output will write the remaining buffered data to the # memory_file as a new moof/mdat. self._av_output.close() + # With delay_moov, this may be the first time the file pointer has + # moved, so the segment may not yet have been created + if not self._segment: + self.create_segment() elif not self._part_has_keyframe: # Parts which are not the last part or an independent part should # not have durations below 0.85 of the part target duration. @@ -305,6 +330,9 @@ class StreamMuxer: self._part_start_dts + 0.85 * self._stream_settings.part_target_duration / packet.time_base, ) + # Undo dts adjustments if we don't have ll_hls + if not self._stream_settings.ll_hls: + adjusted_dts = packet.dts assert self._segment self._memory_file.seek(self._memory_file_pos) self._hass.loop.call_soon_threadsafe( @@ -445,10 +473,7 @@ def get_audio_bitstream_filter( _LOGGER.debug( "ADTS AAC detected. Adding aac_adtstoaac bitstream filter" ) - bsf = av.BitStreamFilter("aac_adtstoasc") - bsf_context = bsf.create() - bsf_context.set_input_stream(audio_stream) - return bsf_context + return av.BitStreamFilter("aac_adtstoasc") break return None @@ -489,7 +514,12 @@ def stream_worker( audio_stream = None # Disable ll-hls for hls inputs if container.format.name == "hls": - stream_settings.ll_hls = False + for field in attr.fields(StreamSettings): + setattr( + stream_settings, + field.name, + getattr(STREAM_SETTINGS_NON_LL_HLS, field.name), + ) stream_state.diagnostics.set_value("container_format", container.format.name) stream_state.diagnostics.set_value("video_codec", video_stream.name) if audio_stream: diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index d887a165b44..94d77e7657e 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -755,7 +755,9 @@ async def test_durations(hass, worker_finished_stream): }, ) - source = generate_h264_video(duration=SEGMENT_DURATION + 1) + source = generate_h264_video( + duration=round(SEGMENT_DURATION + target_part_duration + 1) + ) worker_finished, mock_stream = worker_finished_stream with patch("homeassistant.components.stream.Stream", wraps=mock_stream): From e7ae2fada76a43ead67f3e2317d737ee76d4816c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 14 Jul 2022 21:40:39 +0200 Subject: [PATCH 2573/3516] Remove evohome from mypy ignore list (#75175) --- homeassistant/components/evohome/__init__.py | 57 +++++++++++++------- mypy.ini | 3 -- script/hassfest/mypy_config.py | 1 - 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 27c3698143b..9c46ff43032 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -113,8 +113,9 @@ def _dt_aware_to_naive(dt_aware: dt) -> dt: def convert_until(status_dict: dict, until_key: str) -> None: """Reformat a dt str from "%Y-%m-%dT%H:%M:%SZ" as local/aware/isoformat.""" - if until_key in status_dict: # only present for certain modes - dt_utc_naive = dt_util.parse_datetime(status_dict[until_key]) + if until_key in status_dict and ( # only present for certain modes + dt_utc_naive := dt_util.parse_datetime(status_dict[until_key]) + ): status_dict[until_key] = dt_util.as_local(dt_utc_naive).isoformat() @@ -125,7 +126,9 @@ def convert_dict(dictionary: dict[str, Any]) -> dict[str, Any]: """Convert a string to snake_case.""" string = re.sub(r"[\-\.\s]", "_", str(key)) return (string[0]).lower() + re.sub( - r"[A-Z]", lambda matched: f"_{matched.group(0).lower()}", string[1:] + r"[A-Z]", + lambda matched: f"_{matched.group(0).lower()}", # type:ignore[str-bytes-safe] + string[1:], ) return { @@ -136,7 +139,7 @@ def convert_dict(dictionary: dict[str, Any]) -> dict[str, Any]: } -def _handle_exception(err) -> bool: +def _handle_exception(err) -> None: """Return False if the exception can't be ignored.""" try: raise err @@ -190,15 +193,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return ({}, None) # evohomeasync2 requires naive/local datetimes as strings - if tokens.get(ACCESS_TOKEN_EXPIRES) is not None: - tokens[ACCESS_TOKEN_EXPIRES] = _dt_aware_to_naive( - dt_util.parse_datetime(tokens[ACCESS_TOKEN_EXPIRES]) - ) + if tokens.get(ACCESS_TOKEN_EXPIRES) is not None and ( + expires := dt_util.parse_datetime(tokens[ACCESS_TOKEN_EXPIRES]) + ): + tokens[ACCESS_TOKEN_EXPIRES] = _dt_aware_to_naive(expires) user_data = tokens.pop(USER_DATA, None) return (tokens, user_data) - store = Store(hass, STORAGE_VER, STORAGE_KEY) + store = Store[dict[str, Any]](hass, STORAGE_VER, STORAGE_KEY) tokens, user_data = await load_auth_tokens(store) client_v2 = evohomeasync2.EvohomeClient( @@ -230,7 +233,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return False if _LOGGER.isEnabledFor(logging.DEBUG): - _config = {"locationInfo": {"timeZone": None}, GWS: [{TCS: None}]} + _config: dict[str, Any] = { + "locationInfo": {"timeZone": None}, + GWS: [{TCS: None}], + } _config["locationInfo"]["timeZone"] = loc_config["locationInfo"]["timeZone"] _config[GWS][0][TCS] = loc_config[GWS][0][TCS] _LOGGER.debug("Config = %s", _config) @@ -389,7 +395,14 @@ def setup_service_functions(hass: HomeAssistant, broker): class EvoBroker: """Container for evohome client and data.""" - def __init__(self, hass, client, client_v1, store, params) -> None: + def __init__( + self, + hass, + client: evohomeasync2.EvohomeClient, + client_v1: evohomeasync.EvohomeClient | None, + store: Store[dict[str, Any]], + params, + ) -> None: """Initialize the evohome client and its data structure.""" self.hass = hass self.client = client @@ -403,7 +416,7 @@ class EvoBroker: self.tcs_utc_offset = timedelta( minutes=client.locations[loc_idx].timeZone[UTC_OFFSET] ) - self.temps = {} + self.temps: dict[str, Any] | None = {} async def save_auth_tokens(self) -> None: """Save access tokens and session IDs to the store for later use.""" @@ -443,6 +456,8 @@ class EvoBroker: async def _update_v1_api_temps(self, *args, **kwargs) -> None: """Get the latest high-precision temperatures of the default Location.""" + assert self.client_v1 + def get_session_id(client_v1) -> str | None: user_data = client_v1.user_data if client_v1 else None return user_data.get("sessionId") if user_data else None @@ -526,7 +541,7 @@ class EvoDevice(Entity): self._evo_broker = evo_broker self._evo_tcs = evo_broker.tcs - self._device_state_attrs = {} + self._device_state_attrs: dict[str, Any] = {} async def async_refresh(self, payload: dict | None = None) -> None: """Process any signals.""" @@ -575,8 +590,8 @@ class EvoChild(EvoDevice): def __init__(self, evo_broker, evo_device) -> None: """Initialize a evohome Controller (hub).""" super().__init__(evo_broker, evo_device) - self._schedule = {} - self._setpoints = {} + self._schedule: dict[str, Any] = {} + self._setpoints: dict[str, Any] = {} @property def current_temperature(self) -> float | None: @@ -590,6 +605,8 @@ class EvoChild(EvoDevice): if self._evo_device.temperatureStatus["isAvailable"]: return self._evo_device.temperatureStatus["temperature"] + return None + @property def setpoints(self) -> dict[str, Any]: """Return the current/next setpoints from the schedule. @@ -630,9 +647,12 @@ class EvoChild(EvoDevice): day = self._schedule["DailySchedules"][(day_of_week + offset) % 7] switchpoint = day["Switchpoints"][idx] + switchpoint_time_of_day = dt_util.parse_datetime( + f"{sp_date}T{switchpoint['TimeOfDay']}" + ) + assert switchpoint_time_of_day dt_aware = _dt_evo_to_aware( - dt_util.parse_datetime(f"{sp_date}T{switchpoint['TimeOfDay']}"), - self._evo_broker.tcs_utc_offset, + switchpoint_time_of_day, self._evo_broker.tcs_utc_offset ) self._setpoints[f"{key}_sp_from"] = dt_aware.isoformat() @@ -661,7 +681,8 @@ class EvoChild(EvoDevice): async def async_update(self) -> None: """Get the latest state data.""" next_sp_from = self._setpoints.get("next_sp_from", "2000-01-01T00:00:00+00:00") - if dt_util.now() >= dt_util.parse_datetime(next_sp_from): + next_sp_from_dt = dt_util.parse_datetime(next_sp_from) + if next_sp_from_dt is None or dt_util.now() >= next_sp_from_dt: await self._update_schedule() # no schedule, or it's out-of-date self._device_state_attrs = {"setpoints": self.setpoints} diff --git a/mypy.ini b/mypy.ini index d92cca692a4..78117ef2b1e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2662,9 +2662,6 @@ ignore_errors = true [mypy-homeassistant.components.cloud.http_api] ignore_errors = true -[mypy-homeassistant.components.evohome] -ignore_errors = true - [mypy-homeassistant.components.sonos] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index d2c11d4744d..2e3e80ebc50 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -18,7 +18,6 @@ from .model import Config, Integration IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.cloud.client", "homeassistant.components.cloud.http_api", - "homeassistant.components.evohome", "homeassistant.components.sonos", "homeassistant.components.sonos.alarms", "homeassistant.components.sonos.binary_sensor", From f0cc565f6c53744db744b88fada0e8f7366e1bc0 Mon Sep 17 00:00:00 2001 From: Thomas Hollstegge Date: Thu, 14 Jul 2022 21:40:53 +0200 Subject: [PATCH 2574/3516] Fix Alexa: Only trigger doorbell event on actual state change to "ON" (#74924) * Alexa: Only trigger doorbell event on actual state change to "ON" * Remove unnecessary check for new_state Co-authored-by: Jan Bouwhuis * First check state is `on` before checking the old state Co-authored-by: Jan Bouwhuis --- .../components/alexa/state_report.py | 4 +- tests/components/alexa/test_state_report.py | 50 ++++++++++++++++++- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index d3e476c2e8c..783397ca047 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -86,7 +86,9 @@ async def async_enable_proactive_mode(hass, smart_home_config): return if should_doorbell: - if new_state.state == STATE_ON: + if new_state.state == STATE_ON and ( + old_state is None or old_state.state != STATE_ON + ): await async_send_doorbell_event_message( hass, smart_home_config, alexa_changed_entity ) diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index b2d36b9d179..bb61cea2413 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -346,7 +346,11 @@ async def test_doorbell_event(hass, aioclient_mock): hass.states.async_set( "binary_sensor.test_doorbell", "off", - {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, + { + "friendly_name": "Test Doorbell Sensor", + "device_class": "occupancy", + "linkquality": 42, + }, ) await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) @@ -354,7 +358,21 @@ async def test_doorbell_event(hass, aioclient_mock): hass.states.async_set( "binary_sensor.test_doorbell", "on", - {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, + { + "friendly_name": "Test Doorbell Sensor", + "device_class": "occupancy", + "linkquality": 42, + }, + ) + + hass.states.async_set( + "binary_sensor.test_doorbell", + "on", + { + "friendly_name": "Test Doorbell Sensor", + "device_class": "occupancy", + "linkquality": 99, + }, ) # To trigger event listener @@ -386,6 +404,34 @@ async def test_doorbell_event(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 2 +async def test_doorbell_event_from_unknown(hass, aioclient_mock): + """Test doorbell press reports.""" + aioclient_mock.post(TEST_URL, text="", status=202) + + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) + + hass.states.async_set( + "binary_sensor.test_doorbell", + "on", + { + "friendly_name": "Test Doorbell Sensor", + "device_class": "occupancy", + }, + ) + + # To trigger event listener + await hass.async_block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + call = aioclient_mock.mock_calls + + call_json = call[0][2] + assert call_json["event"]["header"]["namespace"] == "Alexa.DoorbellEventSource" + assert call_json["event"]["header"]["name"] == "DoorbellPress" + assert call_json["event"]["payload"]["cause"]["type"] == "PHYSICAL_INTERACTION" + assert call_json["event"]["endpoint"]["endpointId"] == "binary_sensor#test_doorbell" + + async def test_doorbell_event_fail(hass, aioclient_mock, caplog): """Test proactive state retries once.""" aioclient_mock.post( From 9a4a7e2f4dd2d440df91ac34a0abdd844fbd86f4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jul 2022 21:43:14 +0200 Subject: [PATCH 2575/3516] Extend failed login message with the request URL (#75218) --- homeassistant/components/http/ban.py | 2 +- tests/components/http/test_ban.py | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 81349fe95a1..ee8324b2791 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -117,7 +117,7 @@ async def process_wrong_login(request: Request) -> None: # The user-agent is unsanitized input so we only include it in the log user_agent = request.headers.get("user-agent") - log_msg = f"{base_msg} ({user_agent})" + log_msg = f"{base_msg} Requested URL: '{request.rel_url}'. ({user_agent})" notification_msg = f"{base_msg} See the log for details." diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index 05a6493c9c2..7a4202c1a67 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -234,7 +234,7 @@ async def test_ban_middleware_loaded_by_default(hass): assert len(mock_setup.mock_calls) == 1 -async def test_ip_bans_file_creation(hass, aiohttp_client): +async def test_ip_bans_file_creation(hass, aiohttp_client, caplog): """Testing if banned IP file created.""" app = web.Application() app["hass"] = hass @@ -243,7 +243,7 @@ async def test_ip_bans_file_creation(hass, aiohttp_client): """Return a mock web response.""" raise HTTPUnauthorized - app.router.add_get("/", unauth_handler) + app.router.add_get("/example", unauth_handler) setup_bans(hass, app, 2) mock_real_ip(app)("200.201.202.204") @@ -259,19 +259,19 @@ async def test_ip_bans_file_creation(hass, aiohttp_client): m_open = mock_open() with patch("homeassistant.components.http.ban.open", m_open, create=True): - resp = await client.get("/") + resp = await client.get("/example") assert resp.status == HTTPStatus.UNAUTHORIZED assert len(manager.ip_bans_lookup) == len(BANNED_IPS) assert m_open.call_count == 0 - resp = await client.get("/") + resp = await client.get("/example") assert resp.status == HTTPStatus.UNAUTHORIZED assert len(manager.ip_bans_lookup) == len(BANNED_IPS) + 1 m_open.assert_called_once_with( hass.config.path(IP_BANS_FILE), "a", encoding="utf8" ) - resp = await client.get("/") + resp = await client.get("/example") assert resp.status == HTTPStatus.FORBIDDEN assert m_open.call_count == 1 @@ -283,6 +283,11 @@ async def test_ip_bans_file_creation(hass, aiohttp_client): == "Login attempt or request with invalid authentication from example.com (200.201.202.204). See the log for details." ) + assert ( + "Login attempt or request with invalid authentication from example.com (200.201.202.204). Requested URL: '/example'." + in caplog.text + ) + async def test_failed_login_attempts_counter(hass, aiohttp_client): """Testing if failed login attempts counter increased.""" From 09f37fc522891aa94d110b4a2990bb299870b4c2 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 14 Jul 2022 21:46:26 +0200 Subject: [PATCH 2576/3516] Migrate SQL to new entity naming style (#75203) --- homeassistant/components/sql/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 33ddafe2c0a..dfb1e15f052 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -145,6 +145,7 @@ class SQLSensor(SensorEntity): """Representation of an SQL sensor.""" _attr_icon = "mdi:database-search" + _attr_has_entity_name = True def __init__( self, @@ -157,7 +158,6 @@ class SQLSensor(SensorEntity): entry_id: str, ) -> None: """Initialize the SQL sensor.""" - self._attr_name = name self._query = query self._attr_native_unit_of_measurement = unit self._template = value_template From 72906bf1547b48f2f81aafd418c1c8880046d649 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Thu, 14 Jul 2022 15:56:36 -0400 Subject: [PATCH 2577/3516] Migrate UPB to new entity naming style (#75096) Co-authored-by: Franck Nijhof --- homeassistant/components/upb/__init__.py | 5 ----- homeassistant/components/upb/light.py | 2 ++ homeassistant/components/upb/scene.py | 5 +++++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/upb/__init__.py b/homeassistant/components/upb/__init__.py index b2980ace4e1..5e03df71750 100644 --- a/homeassistant/components/upb/__init__.py +++ b/homeassistant/components/upb/__init__.py @@ -75,11 +75,6 @@ class UpbEntity(Entity): element_type = "link" if element.addr.is_link else "device" self._unique_id = f"{unique_id}_{element_type}_{element.addr}" - @property - def name(self): - """Name of the element.""" - return self._element.name - @property def unique_id(self): """Return unique id of the element.""" diff --git a/homeassistant/components/upb/light.py b/homeassistant/components/upb/light.py index 13f680d9e5a..98a775c18ab 100644 --- a/homeassistant/components/upb/light.py +++ b/homeassistant/components/upb/light.py @@ -49,6 +49,8 @@ async def async_setup_entry( class UpbLight(UpbAttachedEntity, LightEntity): """Representation of an UPB Light.""" + _attr_has_entity_name = True + def __init__(self, element, unique_id, upb): """Initialize an UpbLight.""" super().__init__(element, unique_id, upb) diff --git a/homeassistant/components/upb/scene.py b/homeassistant/components/upb/scene.py index 2334e87aeaa..fe6f07199c4 100644 --- a/homeassistant/components/upb/scene.py +++ b/homeassistant/components/upb/scene.py @@ -49,6 +49,11 @@ async def async_setup_entry( class UpbLink(UpbEntity, Scene): """Representation of an UPB Link.""" + def __init__(self, element, unique_id, upb): + """Initialize the base of all UPB devices.""" + super().__init__(element, unique_id, upb) + self._attr_name = element.name + async def async_activate(self, **kwargs: Any) -> None: """Activate the task.""" self._element.activate() From d004adf8335f0d6118fd80e250119e24bd6aa254 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jul 2022 22:04:19 +0200 Subject: [PATCH 2578/3516] Add entity descriptions in AdGuard Home sensors (#75179) --- homeassistant/components/adguard/__init__.py | 8 +- homeassistant/components/adguard/sensor.py | 286 +++++++------------ 2 files changed, 102 insertions(+), 192 deletions(-) diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 0c43f84bdfc..e27aef6389d 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -138,8 +138,8 @@ class AdGuardHomeEntity(Entity): self, adguard: AdGuardHome, entry: ConfigEntry, - name: str, - icon: str, + name: str | None, + icon: str | None, enabled_default: bool = True, ) -> None: """Initialize the AdGuard Home entity.""" @@ -151,12 +151,12 @@ class AdGuardHomeEntity(Entity): self.adguard = adguard @property - def name(self) -> str: + def name(self) -> str | None: """Return the name of the entity.""" return self._name @property - def icon(self) -> str: + def icon(self) -> str | None: """Return the mdi icon of the entity.""" return self._icon diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 2d3226aba59..19ed0b96801 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -1,11 +1,14 @@ """Support for AdGuard Home sensors.""" from __future__ import annotations +from collections.abc import Callable, Coroutine +from dataclasses import dataclass from datetime import timedelta +from typing import Any from adguardhome import AdGuardHome, AdGuardHomeConnectionError -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, TIME_MILLISECONDS from homeassistant.core import HomeAssistant @@ -19,6 +22,81 @@ SCAN_INTERVAL = timedelta(seconds=300) PARALLEL_UPDATES = 4 +@dataclass +class AdGuardHomeEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[AdGuardHome], Callable[[], Coroutine[Any, Any, int | float]]] + + +@dataclass +class AdGuardHomeEntityDescription( + SensorEntityDescription, AdGuardHomeEntityDescriptionMixin +): + """Describes AdGuard Home sensor entity.""" + + +SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( + AdGuardHomeEntityDescription( + key="dns_queries", + name="DNS queries", + icon="mdi:magnify", + native_unit_of_measurement="queries", + value_fn=lambda adguard: adguard.stats.dns_queries, + ), + AdGuardHomeEntityDescription( + key="blocked_filtering", + name="DNS queries blocked", + icon="mdi:magnify-close", + native_unit_of_measurement="queries", + value_fn=lambda adguard: adguard.stats.blocked_filtering, + ), + AdGuardHomeEntityDescription( + key="blocked_percentage", + name="DNS queries blocked ratio", + icon="mdi:magnify-close", + native_unit_of_measurement=PERCENTAGE, + value_fn=lambda adguard: adguard.stats.blocked_percentage, + ), + AdGuardHomeEntityDescription( + key="blocked_parental", + name="Parental control blocked", + icon="mdi:human-male-girl", + native_unit_of_measurement="requests", + value_fn=lambda adguard: adguard.stats.replaced_parental, + ), + AdGuardHomeEntityDescription( + key="blocked_safebrowsing", + name="Safe browsing blocked", + icon="mdi:shield-half-full", + native_unit_of_measurement="requests", + value_fn=lambda adguard: adguard.stats.replaced_safebrowsing, + ), + AdGuardHomeEntityDescription( + key="enforced_safesearch", + name="Safe searches enforced", + icon="mdi:shield-search", + native_unit_of_measurement="requests", + value_fn=lambda adguard: adguard.stats.replaced_safesearch, + ), + AdGuardHomeEntityDescription( + key="average_speed", + name="Average processing speed", + icon="mdi:speedometer", + native_unit_of_measurement=TIME_MILLISECONDS, + value_fn=lambda adguard: adguard.stats.avg_processing_time, + ), + AdGuardHomeEntityDescription( + key="rules_count", + name="Rules count", + icon="mdi:counter", + native_unit_of_measurement="rules", + value_fn=lambda adguard: adguard.stats.avg_processing_time, + entity_registry_enabled_default=False, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -34,215 +112,47 @@ async def async_setup_entry( hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_VERSION] = version - sensors = [ - AdGuardHomeDNSQueriesSensor(adguard, entry), - AdGuardHomeBlockedFilteringSensor(adguard, entry), - AdGuardHomePercentageBlockedSensor(adguard, entry), - AdGuardHomeReplacedParentalSensor(adguard, entry), - AdGuardHomeReplacedSafeBrowsingSensor(adguard, entry), - AdGuardHomeReplacedSafeSearchSensor(adguard, entry), - AdGuardHomeAverageProcessingTimeSensor(adguard, entry), - AdGuardHomeRulesCountSensor(adguard, entry), - ] - - async_add_entities(sensors, True) + async_add_entities( + [AdGuardHomeSensor(adguard, entry, description) for description in SENSORS], + True, + ) class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity): """Defines a AdGuard Home sensor.""" + entity_description: AdGuardHomeEntityDescription + def __init__( self, adguard: AdGuardHome, entry: ConfigEntry, - name: str, - icon: str, - measurement: str, - unit_of_measurement: str, - enabled_default: bool = True, + description: AdGuardHomeEntityDescription, ) -> None: """Initialize AdGuard Home sensor.""" - self._state: int | str | None = None - self._unit_of_measurement = unit_of_measurement - self.measurement = measurement + self.entity_description = description - super().__init__(adguard, entry, name, icon, enabled_default) - - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return "_".join( + self._attr_unique_id = "_".join( [ DOMAIN, - self.adguard.host, - str(self.adguard.port), + adguard.host, + str(adguard.port), "sensor", - self.measurement, + description.key, ] ) - @property - def native_value(self) -> int | str | None: - """Return the state of the sensor.""" - return self._state - - @property - def native_unit_of_measurement(self) -> str | None: - """Return the unit this state is expressed in.""" - return self._unit_of_measurement - - -class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor): - """Defines a AdGuard Home DNS Queries sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" super().__init__( adguard, entry, - "DNS queries", - "mdi:magnify", - "dns_queries", - "queries", + description.name, + description.icon, + description.entity_registry_enabled_default, ) async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" - self._state = await self.adguard.stats.dns_queries() - - -class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor): - """Defines a AdGuard Home blocked by filtering sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "DNS queries blocked", - "mdi:magnify-close", - "blocked_filtering", - "queries", - enabled_default=False, - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.stats.blocked_filtering() - - -class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor): - """Defines a AdGuard Home blocked percentage sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "DNS queries blocked ratio", - "mdi:magnify-close", - "blocked_percentage", - PERCENTAGE, - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - percentage = await self.adguard.stats.blocked_percentage() - self._state = f"{percentage:.2f}" - - -class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor): - """Defines a AdGuard Home replaced by parental control sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "Parental control blocked", - "mdi:human-male-girl", - "blocked_parental", - "requests", - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.stats.replaced_parental() - - -class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor): - """Defines a AdGuard Home replaced by safe browsing sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "Safe browsing blocked", - "mdi:shield-half-full", - "blocked_safebrowsing", - "requests", - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.stats.replaced_safebrowsing() - - -class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor): - """Defines a AdGuard Home replaced by safe search sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "Safe searches enforced", - "mdi:shield-search", - "enforced_safesearch", - "requests", - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.stats.replaced_safesearch() - - -class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor): - """Defines a AdGuard Home average processing time sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "Average processing speed", - "mdi:speedometer", - "average_speed", - TIME_MILLISECONDS, - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - average = await self.adguard.stats.avg_processing_time() - self._state = f"{average:.2f}" - - -class AdGuardHomeRulesCountSensor(AdGuardHomeSensor): - """Defines a AdGuard Home rules count sensor.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home sensor.""" - super().__init__( - adguard, - entry, - "Rules count", - "mdi:counter", - "rules_count", - "rules", - enabled_default=False, - ) - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.filtering.rules_count(allowlist=False) + value = await self.entity_description.value_fn(self.adguard)() + self._attr_native_value = value + if isinstance(value, float): + self._attr_native_value = f"{value:.2f}" From fef1b842ce9ef96303bfe0fdf3e9d10d356ca082 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 14 Jul 2022 22:04:46 +0200 Subject: [PATCH 2579/3516] Update wled to 0.14.1 (#75174) --- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wled/test_diagnostics.py | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index 668950b9326..2fc00131fac 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,7 +3,7 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.13.2"], + "requirements": ["wled==0.14.1"], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index aa20e5270fd..1a41d7b544c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2448,7 +2448,7 @@ wirelesstagpy==0.8.1 withings-api==2.4.0 # homeassistant.components.wled -wled==0.13.2 +wled==0.14.1 # homeassistant.components.wolflink wolf_smartset==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 719af4677fa..4949ab91bbf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1633,7 +1633,7 @@ wiffi==1.1.0 withings-api==2.4.0 # homeassistant.components.wled -wled==0.13.2 +wled==0.14.1 # homeassistant.components.wolflink wolf_smartset==0.1.11 diff --git a/tests/components/wled/test_diagnostics.py b/tests/components/wled/test_diagnostics.py index d8782848c92..8f086331f8f 100644 --- a/tests/components/wled/test_diagnostics.py +++ b/tests/components/wled/test_diagnostics.py @@ -26,7 +26,9 @@ async def test_diagnostics( "free_heap": 14600, "leds": { "__type": "", - "repr": "Leds(cct=False, count=30, fps=None, max_power=850, max_segments=10, power=470, rgbw=False, wv=True)", + "repr": "Leds(cct=False, count=30, fps=None, light_capabilities=None, " + "max_power=850, max_segments=10, power=470, rgbw=False, wv=True, " + "segment_light_capabilities=None)", }, "live_ip": "Unknown", "live_mode": "Unknown", From 61cc9f5288f378529daa122062b6120d50d1dffd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 22:06:08 +0200 Subject: [PATCH 2580/3516] Consolidate executor jobs when loading integration manifests (#75176) --- homeassistant/bootstrap.py | 12 +- .../components/analytics/analytics.py | 14 +- .../components/websocket_api/commands.py | 32 ++-- homeassistant/helpers/service.py | 18 +-- homeassistant/helpers/translation.py | 19 +-- homeassistant/loader.py | 146 +++++++++++------- tests/components/analytics/test_analytics.py | 8 +- tests/helpers/test_translation.py | 12 +- 8 files changed, 143 insertions(+), 118 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 5d19249e37b..b8ec5987142 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -35,7 +35,6 @@ from .setup import ( async_setup_component, ) from .util import dt as dt_util -from .util.async_ import gather_with_concurrency from .util.logging import async_activate_log_queue_handler from .util.package import async_get_user_site, is_virtual_env @@ -479,14 +478,9 @@ async def _async_set_up_integrations( integrations_to_process = [ int_or_exc - for int_or_exc in await gather_with_concurrency( - loader.MAX_LOAD_CONCURRENTLY, - *( - loader.async_get_integration(hass, domain) - for domain in old_to_resolve - ), - return_exceptions=True, - ) + for int_or_exc in ( + await loader.async_get_integrations(hass, old_to_resolve) + ).values() if isinstance(int_or_exc, loader.Integration) ] resolve_dependencies_tasks = [ diff --git a/homeassistant/components/analytics/analytics.py b/homeassistant/components/analytics/analytics.py index 5bb0368b021..1a696b0c206 100644 --- a/homeassistant/components/analytics/analytics.py +++ b/homeassistant/components/analytics/analytics.py @@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.storage import Store from homeassistant.helpers.system_info import async_get_system_info -from homeassistant.loader import IntegrationNotFound, async_get_integration +from homeassistant.loader import IntegrationNotFound, async_get_integrations from homeassistant.setup import async_get_loaded_integrations from .const import ( @@ -182,15 +182,9 @@ class Analytics: if self.preferences.get(ATTR_USAGE, False) or self.preferences.get( ATTR_STATISTICS, False ): - configured_integrations = await asyncio.gather( - *( - async_get_integration(self.hass, domain) - for domain in async_get_loaded_integrations(self.hass) - ), - return_exceptions=True, - ) - - for integration in configured_integrations: + domains = async_get_loaded_integrations(self.hass) + configured_integrations = await async_get_integrations(self.hass, domains) + for integration in configured_integrations.values(): if isinstance(integration, IntegrationNotFound): continue diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index b7e7a353633..6c18fd96627 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -1,7 +1,6 @@ """Commands part of Websocket API.""" from __future__ import annotations -import asyncio from collections.abc import Callable import datetime as dt import json @@ -32,7 +31,12 @@ from homeassistant.helpers.event import ( ) from homeassistant.helpers.json import JSON_DUMP, ExtendedJSONEncoder from homeassistant.helpers.service import async_get_all_descriptions -from homeassistant.loader import IntegrationNotFound, async_get_integration +from homeassistant.loader import ( + Integration, + IntegrationNotFound, + async_get_integration, + async_get_integrations, +) from homeassistant.setup import DATA_SETUP_TIME, async_get_loaded_integrations from homeassistant.util.json import ( find_paths_unserializable_data, @@ -372,9 +376,13 @@ async def handle_manifest_list( wanted_integrations = msg.get("integrations") if wanted_integrations is None: wanted_integrations = async_get_loaded_integrations(hass) - integrations = await asyncio.gather( - *(async_get_integration(hass, domain) for domain in wanted_integrations) - ) + + ints_or_excs = await async_get_integrations(hass, wanted_integrations) + integrations: list[Integration] = [] + for int_or_exc in ints_or_excs.values(): + if isinstance(int_or_exc, Exception): + raise int_or_exc + integrations.append(int_or_exc) connection.send_result( msg["id"], [integration.manifest for integration in integrations] ) @@ -706,12 +714,12 @@ async def handle_supported_brands( ) -> None: """Handle supported brands command.""" data = {} - for integration in await asyncio.gather( - *[ - async_get_integration(hass, integration) - for integration in supported_brands.HAS_SUPPORTED_BRANDS - ] - ): - data[integration.domain] = integration.manifest["supported_brands"] + ints_or_excs = await async_get_integrations( + hass, supported_brands.HAS_SUPPORTED_BRANDS + ) + for int_or_exc in ints_or_excs.values(): + if isinstance(int_or_exc, Exception): + raise int_or_exc + data[int_or_exc.domain] = int_or_exc.manifest["supported_brands"] connection.send_result(msg["id"], data) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index bc3451c24c0..cf7fb3b2304 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -31,13 +31,7 @@ from homeassistant.exceptions import ( Unauthorized, UnknownUser, ) -from homeassistant.loader import ( - MAX_LOAD_CONCURRENTLY, - Integration, - async_get_integration, - bind_hass, -) -from homeassistant.util.async_ import gather_with_concurrency +from homeassistant.loader import Integration, async_get_integrations, bind_hass from homeassistant.util.yaml import load_yaml from homeassistant.util.yaml.loader import JSON_TYPE @@ -467,10 +461,12 @@ async def async_get_all_descriptions( loaded = {} if missing: - integrations = await gather_with_concurrency( - MAX_LOAD_CONCURRENTLY, - *(async_get_integration(hass, domain) for domain in missing), - ) + ints_or_excs = await async_get_integrations(hass, missing) + integrations = [ + int_or_exc + for int_or_exc in ints_or_excs.values() + if isinstance(int_or_exc, Integration) + ] contents = await hass.async_add_executor_job( _load_services_files, hass, integrations diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index cda50de535b..616baeeea92 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -9,13 +9,11 @@ from typing import Any from homeassistant.core import HomeAssistant, callback from homeassistant.loader import ( - MAX_LOAD_CONCURRENTLY, Integration, async_get_config_flows, - async_get_integration, + async_get_integrations, bind_hass, ) -from homeassistant.util.async_ import gather_with_concurrency from homeassistant.util.json import load_json _LOGGER = logging.getLogger(__name__) @@ -151,16 +149,13 @@ async def async_get_component_strings( ) -> dict[str, Any]: """Load translations.""" domains = list({loaded.split(".")[-1] for loaded in components}) - integrations = dict( - zip( - domains, - await gather_with_concurrency( - MAX_LOAD_CONCURRENTLY, - *(async_get_integration(hass, domain) for domain in domains), - ), - ) - ) + integrations: dict[str, Integration] = {} + ints_or_excs = await async_get_integrations(hass, domains) + for domain, int_or_exc in ints_or_excs.items(): + if isinstance(int_or_exc, Exception): + raise int_or_exc + integrations[domain] = int_or_exc translations: dict[str, Any] = {} # Determine paths of missing components/platforms diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 0a65928701b..d0e6189ef96 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -7,7 +7,7 @@ documentation as possible to keep it understandable. from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Callable, Iterable from contextlib import suppress import functools as ft import importlib @@ -31,7 +31,6 @@ from .generated.ssdp import SSDP from .generated.usb import USB from .generated.zeroconf import HOMEKIT, ZEROCONF from .helpers.json import JSON_DECODE_EXCEPTIONS, json_loads -from .util.async_ import gather_with_concurrency # Typing imports that create a circular dependency if TYPE_CHECKING: @@ -128,6 +127,7 @@ class Manifest(TypedDict, total=False): version: str codeowners: list[str] loggers: list[str] + supported_brands: dict[str, str] def manifest_from_legacy_module(domain: str, module: ModuleType) -> Manifest: @@ -166,19 +166,15 @@ async def _async_get_custom_components( get_sub_directories, custom_components.__path__ ) - integrations = await gather_with_concurrency( - MAX_LOAD_CONCURRENTLY, - *( - hass.async_add_executor_job( - Integration.resolve_from_root, hass, custom_components, comp.name - ) - for comp in dirs - ), + integrations = await hass.async_add_executor_job( + _resolve_integrations_from_root, + hass, + custom_components, + [comp.name for comp in dirs], ) - return { integration.domain: integration - for integration in integrations + for integration in integrations.values() if integration is not None } @@ -681,59 +677,101 @@ class Integration: return f"" +def _resolve_integrations_from_root( + hass: HomeAssistant, root_module: ModuleType, domains: list[str] +) -> dict[str, Integration]: + """Resolve multiple integrations from root.""" + integrations: dict[str, Integration] = {} + for domain in domains: + try: + integration = Integration.resolve_from_root(hass, root_module, domain) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error loading integration: %s", domain) + else: + if integration: + integrations[domain] = integration + return integrations + + async def async_get_integration(hass: HomeAssistant, domain: str) -> Integration: - """Get an integration.""" + """Get integration.""" + integrations_or_excs = await async_get_integrations(hass, [domain]) + int_or_exc = integrations_or_excs[domain] + if isinstance(int_or_exc, Integration): + return int_or_exc + raise int_or_exc + + +async def async_get_integrations( + hass: HomeAssistant, domains: Iterable[str] +) -> dict[str, Integration | Exception]: + """Get integrations.""" if (cache := hass.data.get(DATA_INTEGRATIONS)) is None: if not _async_mount_config_dir(hass): - raise IntegrationNotFound(domain) + return {domain: IntegrationNotFound(domain) for domain in domains} cache = hass.data[DATA_INTEGRATIONS] = {} - int_or_evt: Integration | asyncio.Event | None = cache.get(domain, _UNDEF) + results: dict[str, Integration | Exception] = {} + needed: dict[str, asyncio.Event] = {} + in_progress: dict[str, asyncio.Event] = {} + for domain in domains: + int_or_evt: Integration | asyncio.Event | None = cache.get(domain, _UNDEF) + if isinstance(int_or_evt, asyncio.Event): + in_progress[domain] = int_or_evt + elif int_or_evt is not _UNDEF: + results[domain] = cast(Integration, int_or_evt) + elif "." in domain: + results[domain] = ValueError(f"Invalid domain {domain}") + else: + needed[domain] = cache[domain] = asyncio.Event() - if isinstance(int_or_evt, asyncio.Event): - await int_or_evt.wait() + if in_progress: + await asyncio.gather(*[event.wait() for event in in_progress.values()]) + for domain in in_progress: + # 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 := cache.get(domain, _UNDEF)) is _UNDEF: + results[domain] = IntegrationNotFound(domain) + else: + results[domain] = cast(Integration, int_or_evt) - # 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 := cache.get(domain, _UNDEF)) is _UNDEF: - raise IntegrationNotFound(domain) + # First we look for custom components + if needed: + # Instead of using resolve_from_root we use the cache of custom + # components to find the integration. + custom = await async_get_custom_components(hass) + for domain, event in needed.items(): + if integration := custom.get(domain): + results[domain] = cache[domain] = integration + event.set() - if int_or_evt is not _UNDEF: - return cast(Integration, int_or_evt) + for domain in results: + if domain in needed: + del needed[domain] - event = cache[domain] = asyncio.Event() + # Now the rest use resolve_from_root + if needed: + from . import components # pylint: disable=import-outside-toplevel - try: - integration = await _async_get_integration(hass, domain) - except Exception: - # Remove event from cache. - cache.pop(domain) - event.set() - raise + integrations = await hass.async_add_executor_job( + _resolve_integrations_from_root, hass, components, list(needed) + ) + for domain, event in needed.items(): + int_or_exc = integrations.get(domain) + if not int_or_exc: + cache.pop(domain) + results[domain] = IntegrationNotFound(domain) + elif isinstance(int_or_exc, Exception): + cache.pop(domain) + exc = IntegrationNotFound(domain) + exc.__cause__ = int_or_exc + results[domain] = exc + else: + results[domain] = cache[domain] = int_or_exc + event.set() - cache[domain] = integration - event.set() - return 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): - return integration - - from . import components # pylint: disable=import-outside-toplevel - - if integration := await hass.async_add_executor_job( - Integration.resolve_from_root, hass, components, domain - ): - return integration - - raise IntegrationNotFound(domain) + return results class LoaderError(Exception): diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index a18c59f171f..82a61126432 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -269,8 +269,8 @@ async def test_send_statistics_one_integration_fails(hass, caplog, aioclient_moc hass.config.components = ["default_config"] with patch( - "homeassistant.components.analytics.analytics.async_get_integration", - side_effect=IntegrationNotFound("any"), + "homeassistant.components.analytics.analytics.async_get_integrations", + return_value={"any": IntegrationNotFound("any")}, ), patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION): await analytics.send_analytics() @@ -291,8 +291,8 @@ async def test_send_statistics_async_get_integration_unknown_exception( hass.config.components = ["default_config"] with pytest.raises(ValueError), patch( - "homeassistant.components.analytics.analytics.async_get_integration", - side_effect=ValueError, + "homeassistant.components.analytics.analytics.async_get_integrations", + return_value={"any": ValueError()}, ), patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION): await analytics.send_analytics() diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 2e30a649a7b..d993233ac5d 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -135,8 +135,8 @@ async def test_get_translations_loads_config_flows(hass, mock_config_flows): "homeassistant.helpers.translation.load_translations_files", return_value={"component1": {"title": "world"}}, ), patch( - "homeassistant.helpers.translation.async_get_integration", - return_value=integration, + "homeassistant.helpers.translation.async_get_integrations", + return_value={"component1": integration}, ): translations = await translation.async_get_translations( hass, "en", "title", config_flow=True @@ -164,8 +164,8 @@ async def test_get_translations_loads_config_flows(hass, mock_config_flows): "homeassistant.helpers.translation.load_translations_files", return_value={"component2": {"title": "world"}}, ), patch( - "homeassistant.helpers.translation.async_get_integration", - return_value=integration, + "homeassistant.helpers.translation.async_get_integrations", + return_value={"component2": integration}, ): translations = await translation.async_get_translations( hass, "en", "title", config_flow=True @@ -212,8 +212,8 @@ async def test_get_translations_while_loading_components(hass): "homeassistant.helpers.translation.load_translations_files", mock_load_translation_files, ), patch( - "homeassistant.helpers.translation.async_get_integration", - return_value=integration, + "homeassistant.helpers.translation.async_get_integrations", + return_value={"component1": integration}, ): tasks = [ translation.async_get_translations(hass, "en", "title") for _ in range(5) From 03e3ebb2380f34943e9ff3feae57855f23863ef3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Jul 2022 22:37:12 +0200 Subject: [PATCH 2581/3516] Use json_loads by default for the aiohttp helper (#75214) --- homeassistant/helpers/aiohttp_client.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 2ef96091d15..f44b59ff077 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -7,7 +7,7 @@ from contextlib import suppress from ssl import SSLContext import sys from types import MappingProxyType -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast import aiohttp from aiohttp import web @@ -22,7 +22,11 @@ from homeassistant.loader import bind_hass from homeassistant.util import ssl as ssl_util from .frame import warn_use -from .json import json_dumps +from .json import json_dumps, json_loads + +if TYPE_CHECKING: + from aiohttp.typedefs import JSONDecoder + DATA_CONNECTOR = "aiohttp_connector" DATA_CONNECTOR_NOTVERIFY = "aiohttp_connector_notverify" @@ -35,6 +39,19 @@ SERVER_SOFTWARE = "HomeAssistant/{0} aiohttp/{1} Python/{2[0]}.{2[1]}".format( WARN_CLOSE_MSG = "closes the Home Assistant aiohttp session" +class HassClientResponse(aiohttp.ClientResponse): + """aiohttp.ClientResponse with a json method that uses json_loads by default.""" + + async def json( + self, + *args: Any, + loads: JSONDecoder = json_loads, + **kwargs: Any, + ) -> Any: + """Send a json request and parse the json response.""" + return await super().json(*args, loads=loads, **kwargs) + + @callback @bind_hass def async_get_clientsession( @@ -99,6 +116,7 @@ def _async_create_clientsession( clientsession = aiohttp.ClientSession( connector=_async_get_connector(hass, verify_ssl), json_serialize=json_dumps, + response_class=HassClientResponse, **kwargs, ) # Prevent packages accidentally overriding our default headers From ff297cb90279b942e949f85cead0b60dc725e591 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 14 Jul 2022 22:51:48 +0100 Subject: [PATCH 2582/3516] Bump aiohomekit to 1.0.0 (#75198) * Bump to 1.0.0rc1 * 1.0.0rc2 * fix one of the tests * simplify test * 1.0.0 Co-authored-by: J. Nick Koston --- .../components/homekit_controller/__init__.py | 2 +- .../homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../homekit_controller/test_init.py | 63 ++++--------------- 5 files changed, 16 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index bc911b991ca..461d46eed1d 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -227,7 +227,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not await conn.async_setup(): del hass.data[KNOWN_DEVICES][conn.unique_id] - if (connection := getattr(conn.pairing, "connection")) and hasattr( + if (connection := getattr(conn.pairing, "connection", None)) and hasattr( connection, "host" ): raise ConfigEntryNotReady( diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index a83d6264603..0275afa07fe 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==0.7.22"], + "requirements": ["aiohomekit==1.0.0"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/requirements_all.txt b/requirements_all.txt index 1a41d7b544c..74f8e7fb0c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.22 +aiohomekit==1.0.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4949ab91bbf..1d4cb752271 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==0.7.22 +aiohomekit==1.0.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 841b726ac0e..57b39076aa6 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -3,11 +3,11 @@ from datetime import timedelta from unittest.mock import patch -from aiohomekit import AccessoryDisconnectedError, exceptions +from aiohomekit import AccessoryDisconnectedError from aiohomekit.model import Accessory from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes -from aiohomekit.testing import FakeController, FakeDiscovery, FakePairing +from aiohomekit.testing import FakePairing from homeassistant.components.homekit_controller.const import DOMAIN, ENTITY_MAP from homeassistant.config_entries import ConfigEntryState @@ -98,7 +98,7 @@ async def test_device_remove_devices(hass, hass_ws_client): ) -async def test_offline_device_raises(hass): +async def test_offline_device_raises(hass, controller): """Test an offline device raises ConfigEntryNotReady.""" is_connected = False @@ -114,56 +114,17 @@ async def test_offline_device_raises(hass): def get_characteristics(self, chars, *args, **kwargs): raise AccessoryDisconnectedError("any") - class OfflineFakeDiscovery(FakeDiscovery): - """Fake discovery that returns an offline pairing.""" - - async def start_pairing(self, alias: str): - if self.description.id in self.controller.pairings: - raise exceptions.AlreadyPairedError( - f"{self.description.id} already paired" - ) - - async def finish_pairing(pairing_code): - if pairing_code != self.pairing_code: - raise exceptions.AuthenticationError("M4") - pairing_data = {} - pairing_data["AccessoryIP"] = self.info["address"] - pairing_data["AccessoryPort"] = self.info["port"] - pairing_data["Connection"] = "IP" - - obj = self.controller.pairings[alias] = OfflineFakePairing( - self.controller, pairing_data, self.accessories - ) - return obj - - return finish_pairing - - class OfflineFakeController(FakeController): - """Fake controller that always returns a discovery with a pairing that always returns False for is_connected.""" - - def add_device(self, accessories): - device_id = "00:00:00:00:00:00" - discovery = self.discoveries[device_id] = OfflineFakeDiscovery( - self, - device_id, - accessories=accessories, - ) - return discovery - - with patch( - "homeassistant.components.homekit_controller.utils.Controller" - ) as controller: - fake_controller = controller.return_value = OfflineFakeController() + with patch("aiohomekit.testing.FakePairing", OfflineFakePairing): await async_setup_component(hass, DOMAIN, {}) + accessory = Accessory.create_with_info( + "TestDevice", "example.com", "Test", "0001", "0.1" + ) + create_alive_service(accessory) - accessory = Accessory.create_with_info( - "TestDevice", "example.com", "Test", "0001", "0.1" - ) - create_alive_service(accessory) - - config_entry, _ = await setup_test_accessories_with_controller( - hass, [accessory], fake_controller - ) + config_entry, _ = await setup_test_accessories_with_controller( + hass, [accessory], controller + ) + await hass.async_block_till_done() assert config_entry.state == ConfigEntryState.SETUP_RETRY From 4a3d047dffa9bbb49ddeb4038f3592e96183ccf1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 14 Jul 2022 23:53:09 +0200 Subject: [PATCH 2583/3516] Use pydeconz interface controls for fans (#75156) --- homeassistant/components/deconz/fan.py | 67 ++++++++++--------- homeassistant/components/deconz/light.py | 8 +-- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 39 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index ab9a1ba6f4a..6002e61d326 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -1,17 +1,10 @@ """Support for deCONZ fans.""" from __future__ import annotations -from typing import Any, Literal +from typing import Any from pydeconz.models.event import EventType -from pydeconz.models.light.fan import ( - FAN_SPEED_25_PERCENT, - FAN_SPEED_50_PERCENT, - FAN_SPEED_75_PERCENT, - FAN_SPEED_100_PERCENT, - FAN_SPEED_OFF, - Fan, -) +from pydeconz.models.light.light import Light, LightFanSpeed from homeassistant.components.fan import DOMAIN, FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry @@ -25,11 +18,11 @@ from homeassistant.util.percentage import ( from .deconz_device import DeconzDevice from .gateway import DeconzGateway, get_gateway_from_config_entry -ORDERED_NAMED_FAN_SPEEDS: list[Literal[0, 1, 2, 3, 4, 5, 6]] = [ - FAN_SPEED_25_PERCENT, - FAN_SPEED_50_PERCENT, - FAN_SPEED_75_PERCENT, - FAN_SPEED_100_PERCENT, +ORDERED_NAMED_FAN_SPEEDS: list[LightFanSpeed] = [ + LightFanSpeed.PERCENT_25, + LightFanSpeed.PERCENT_50, + LightFanSpeed.PERCENT_75, + LightFanSpeed.PERCENT_100, ] @@ -45,12 +38,14 @@ async def async_setup_entry( @callback def async_add_fan(_: EventType, fan_id: str) -> None: """Add fan from deCONZ.""" - fan = gateway.api.lights.fans[fan_id] + fan = gateway.api.lights.lights[fan_id] + if not fan.supports_fan_speed: + return async_add_entities([DeconzFan(fan, gateway)]) gateway.register_platform_add_device_callback( async_add_fan, - gateway.api.lights.fans, + gateway.api.lights.lights, ) @@ -58,33 +53,32 @@ class DeconzFan(DeconzDevice, FanEntity): """Representation of a deCONZ fan.""" TYPE = DOMAIN - _device: Fan - _default_on_speed: Literal[0, 1, 2, 3, 4, 5, 6] + _device: Light + _default_on_speed = LightFanSpeed.PERCENT_50 _attr_supported_features = FanEntityFeature.SET_SPEED - def __init__(self, device: Fan, gateway: DeconzGateway) -> None: + def __init__(self, device: Light, gateway: DeconzGateway) -> None: """Set up fan.""" super().__init__(device, gateway) - self._default_on_speed = FAN_SPEED_50_PERCENT - if self._device.speed in ORDERED_NAMED_FAN_SPEEDS: - self._default_on_speed = self._device.speed + if device.fan_speed in ORDERED_NAMED_FAN_SPEEDS: + self._default_on_speed = device.fan_speed @property def is_on(self) -> bool: """Return true if fan is on.""" - return self._device.speed != FAN_SPEED_OFF + return self._device.fan_speed != LightFanSpeed.OFF @property def percentage(self) -> int | None: """Return the current speed percentage.""" - if self._device.speed == FAN_SPEED_OFF: + if self._device.fan_speed == LightFanSpeed.OFF: return 0 - if self._device.speed not in ORDERED_NAMED_FAN_SPEEDS: + if self._device.fan_speed not in ORDERED_NAMED_FAN_SPEEDS: return None return ordered_list_item_to_percentage( - ORDERED_NAMED_FAN_SPEEDS, self._device.speed + ORDERED_NAMED_FAN_SPEEDS, self._device.fan_speed ) @property @@ -95,16 +89,19 @@ class DeconzFan(DeconzDevice, FanEntity): @callback 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 + if self._device.fan_speed in ORDERED_NAMED_FAN_SPEEDS: + self._default_on_speed = self._device.fan_speed super().async_update_callback() async def async_set_percentage(self, percentage: int) -> None: """Set the speed percentage of the fan.""" if percentage == 0: return await self.async_turn_off() - await self._device.set_speed( - percentage_to_ordered_list_item(ORDERED_NAMED_FAN_SPEEDS, percentage) + await self.gateway.api.lights.lights.set_state( + id=self._device.resource_id, + fan_speed=percentage_to_ordered_list_item( + ORDERED_NAMED_FAN_SPEEDS, percentage + ), ) async def async_turn_on( @@ -117,8 +114,14 @@ class DeconzFan(DeconzDevice, FanEntity): if percentage is not None: await self.async_set_percentage(percentage) return - await self._device.set_speed(self._default_on_speed) + await self.gateway.api.lights.lights.set_state( + id=self._device.resource_id, + fan_speed=self._default_on_speed, + ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off fan.""" - await self._device.set_speed(FAN_SPEED_OFF) + await self.gateway.api.lights.lights.set_state( + id=self._device.resource_id, + fan_speed=LightFanSpeed.OFF, + ) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index e73314aa477..7be6551cccc 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -85,8 +85,7 @@ async def async_setup_entry( @callback def async_add_light(_: EventType, light_id: str) -> None: """Add light from deCONZ.""" - light = gateway.api.lights[light_id] - assert isinstance(light, Light) + light = gateway.api.lights.lights[light_id] if light.type in POWER_PLUGS: return @@ -97,11 +96,6 @@ async def async_setup_entry( gateway.api.lights.lights, ) - gateway.register_platform_add_device_callback( - async_add_light, - gateway.api.lights.fans, - ) - @callback def async_add_group(_: EventType, group_id: str) -> None: """Add group from deCONZ. diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 2ae400bbe19..c1b0b07de02 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==98"], + "requirements": ["pydeconz==99"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 74f8e7fb0c0..98af8acf5e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1447,7 +1447,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==98 +pydeconz==99 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d4cb752271..0a1afc49dce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -980,7 +980,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==98 +pydeconz==99 # homeassistant.components.dexcom pydexcom==0.2.3 From ea6bb370a64953dbf29db6c9b4fce902e619e2da Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 14 Jul 2022 18:12:48 -0500 Subject: [PATCH 2584/3516] Bump frontend to 20220707.1 (#75232) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- mypy.ini | 1 - requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 5c7fd1a20be..288b4796e0e 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220707.0"], + "requirements": ["home-assistant-frontend==20220707.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 09eb66c5f34..d19b95fa2d3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.1 -home-assistant-frontend==20220707.0 +home-assistant-frontend==20220707.1 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/mypy.ini b/mypy.ini index 78117ef2b1e..ab83c443d5a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2697,4 +2697,3 @@ ignore_errors = true [mypy-homeassistant.components.sonos.statistics] ignore_errors = true - diff --git a/requirements_all.txt b/requirements_all.txt index 98af8acf5e4..a695d12c975 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -831,7 +831,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220707.0 +home-assistant-frontend==20220707.1 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a1afc49dce..5124f58a11c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -601,7 +601,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220707.0 +home-assistant-frontend==20220707.1 # homeassistant.components.home_connect homeconnect==0.7.1 From 08a361dab9303501e796a679a4bc93b6c342a38b Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 15 Jul 2022 00:28:00 +0000 Subject: [PATCH 2585/3516] [ci skip] Translation update --- .../components/here_travel_time/translations/hu.json | 7 +++++++ .../components/rhasspy/translations/hu.json | 12 ++++++++++++ .../components/rhasspy/translations/id.json | 12 ++++++++++++ .../components/rhasspy/translations/pl.json | 12 ++++++++++++ .../components/withings/translations/hu.json | 4 ++++ .../components/withings/translations/id.json | 4 ++++ 6 files changed, 51 insertions(+) create mode 100644 homeassistant/components/rhasspy/translations/hu.json create mode 100644 homeassistant/components/rhasspy/translations/id.json create mode 100644 homeassistant/components/rhasspy/translations/pl.json diff --git a/homeassistant/components/here_travel_time/translations/hu.json b/homeassistant/components/here_travel_time/translations/hu.json index cdd40f139d7..b3cee83f662 100644 --- a/homeassistant/components/here_travel_time/translations/hu.json +++ b/homeassistant/components/here_travel_time/translations/hu.json @@ -39,6 +39,13 @@ }, "title": "Eredet kiv\u00e1laszt\u00e1sa" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "T\u00e9rk\u00e9pes hely haszn\u00e1lata", + "origin_entity": "Egy entit\u00e1s haszn\u00e1lata" + }, + "title": "V\u00e1lassza az eredetit" + }, "user": { "data": { "api_key": "API kulcs", diff --git a/homeassistant/components/rhasspy/translations/hu.json b/homeassistant/components/rhasspy/translations/hu.json new file mode 100644 index 00000000000..8297d6af059 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva. Csak egyetlen konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "user": { + "description": "Szeretn\u00e9 enged\u00e9lyezni a Rhasspy t\u00e1mogat\u00e1st?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/id.json b/homeassistant/components/rhasspy/translations/id.json new file mode 100644 index 00000000000..7a0b5445bcd --- /dev/null +++ b/homeassistant/components/rhasspy/translations/id.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "user": { + "description": "Ingin mengaktifkan dukungan Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/pl.json b/homeassistant/components/rhasspy/translations/pl.json new file mode 100644 index 00000000000..79db07a077f --- /dev/null +++ b/homeassistant/components/rhasspy/translations/pl.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "user": { + "description": "Czy chcesz w\u0142\u0105czy\u0107 obs\u0142ug\u0119 Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/hu.json b/homeassistant/components/withings/translations/hu.json index 7504c36c58e..a157d5e3688 100644 --- a/homeassistant/components/withings/translations/hu.json +++ b/homeassistant/components/withings/translations/hu.json @@ -27,6 +27,10 @@ "reauth": { "description": "A \u201e{profile}\u201d profilt \u00fajra hiteles\u00edteni kell, hogy tov\u00e1bbra is fogadni tudja a Withings adatokat.", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, + "reauth_confirm": { + "description": "A \u201e{profile}\u201d profilt \u00fajra kell hiteles\u00edteni, hogy tov\u00e1bbra is megkaphassa a Withings-adatokat.", + "title": "Az integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" } } } diff --git a/homeassistant/components/withings/translations/id.json b/homeassistant/components/withings/translations/id.json index eb21a0d3352..4b4490a617b 100644 --- a/homeassistant/components/withings/translations/id.json +++ b/homeassistant/components/withings/translations/id.json @@ -27,6 +27,10 @@ "reauth": { "description": "Profil \"{profile}\" perlu diautentikasi ulang untuk terus menerima data Withings.", "title": "Autentikasi Ulang Integrasi" + }, + "reauth_confirm": { + "description": "Profil \"{profile}\" perlu diautentikasi ulang untuk terus menerima data Withings.", + "title": "Autentikasi Ulang Integrasi" } } } From 700081e160306c57c1315fc612ee9b4aaa7a07e4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Jul 2022 06:09:11 +0200 Subject: [PATCH 2586/3516] Add entity descriptions in AdGuard Home switches (#75229) --- homeassistant/components/adguard/switch.py | 257 ++++++++------------- 1 file changed, 91 insertions(+), 166 deletions(-) diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index 5b4017d4054..efbd5c7fa38 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -1,13 +1,15 @@ """Support for AdGuard Home switches.""" from __future__ import annotations +from collections.abc import Callable, Coroutine +from dataclasses import dataclass from datetime import timedelta import logging from typing import Any from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -22,6 +24,74 @@ SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 1 +@dataclass +class AdGuardHomeSwitchEntityDescriptionMixin: + """Mixin for required keys.""" + + is_on_fn: Callable[[AdGuardHome], Callable[[], Coroutine[Any, Any, bool]]] + turn_on_fn: Callable[[AdGuardHome], Callable[[], Coroutine[Any, Any, None]]] + turn_off_fn: Callable[[AdGuardHome], Callable[[], Coroutine[Any, Any, None]]] + + +@dataclass +class AdGuardHomeSwitchEntityDescription( + SwitchEntityDescription, AdGuardHomeSwitchEntityDescriptionMixin +): + """Describes AdGuard Home switch entity.""" + + +SWITCHES: tuple[AdGuardHomeSwitchEntityDescription, ...] = ( + AdGuardHomeSwitchEntityDescription( + key="protection", + name="Protection", + icon="mdi:shield-check", + is_on_fn=lambda adguard: adguard.protection_enabled, + turn_on_fn=lambda adguard: adguard.enable_protection, + turn_off_fn=lambda adguard: adguard.disable_protection, + ), + AdGuardHomeSwitchEntityDescription( + key="parental", + name="Parental control", + icon="mdi:shield-check", + is_on_fn=lambda adguard: adguard.parental.enabled, + turn_on_fn=lambda adguard: adguard.parental.enable, + turn_off_fn=lambda adguard: adguard.parental.disable, + ), + AdGuardHomeSwitchEntityDescription( + key="safesearch", + name="Safe search", + icon="mdi:shield-check", + is_on_fn=lambda adguard: adguard.safesearch.enabled, + turn_on_fn=lambda adguard: adguard.safesearch.enable, + turn_off_fn=lambda adguard: adguard.safesearch.disable, + ), + AdGuardHomeSwitchEntityDescription( + key="safebrowsing", + name="Safe browsing", + icon="mdi:shield-check", + is_on_fn=lambda adguard: adguard.safebrowsing.enabled, + turn_on_fn=lambda adguard: adguard.safebrowsing.enable, + turn_off_fn=lambda adguard: adguard.safebrowsing.disable, + ), + AdGuardHomeSwitchEntityDescription( + key="filtering", + name="Filtering", + icon="mdi:shield-check", + is_on_fn=lambda adguard: adguard.filtering.enabled, + turn_on_fn=lambda adguard: adguard.filtering.enable, + turn_off_fn=lambda adguard: adguard.filtering.disable, + ), + AdGuardHomeSwitchEntityDescription( + key="querylog", + name="Query log", + icon="mdi:shield-check", + is_on_fn=lambda adguard: adguard.querylog.enabled, + turn_on_fn=lambda adguard: adguard.querylog.enable, + turn_off_fn=lambda adguard: adguard.querylog.disable, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, @@ -37,199 +107,54 @@ async def async_setup_entry( hass.data[DOMAIN][entry.entry_id][DATA_ADGUARD_VERSION] = version - switches = [ - AdGuardHomeProtectionSwitch(adguard, entry), - AdGuardHomeFilteringSwitch(adguard, entry), - AdGuardHomeParentalSwitch(adguard, entry), - AdGuardHomeSafeBrowsingSwitch(adguard, entry), - AdGuardHomeSafeSearchSwitch(adguard, entry), - AdGuardHomeQueryLogSwitch(adguard, entry), - ] - async_add_entities(switches, True) + async_add_entities( + [AdGuardHomeSwitch(adguard, entry, description) for description in SWITCHES], + True, + ) class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): """Defines a AdGuard Home switch.""" + entity_description: AdGuardHomeSwitchEntityDescription + def __init__( self, adguard: AdGuardHome, entry: ConfigEntry, - name: str, - icon: str, - key: str, - enabled_default: bool = True, + description: AdGuardHomeSwitchEntityDescription, ) -> None: """Initialize AdGuard Home switch.""" - self._state = False - self._key = key - super().__init__(adguard, entry, name, icon, enabled_default) + self.entity_description = description - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return "_".join( - [DOMAIN, self.adguard.host, str(self.adguard.port), "switch", self._key] + self._attr_unique_id = "_".join( + [DOMAIN, adguard.host, str(adguard.port), "switch", description.key] ) - @property - def is_on(self) -> bool: - """Return the state of the switch.""" - return self._state + super().__init__( + adguard, + entry, + description.name, + description.icon, + description.entity_registry_enabled_default, + ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" try: - await self._adguard_turn_off() + await self.entity_description.turn_off_fn(self.adguard)() except AdGuardHomeError: _LOGGER.error("An error occurred while turning off AdGuard Home switch") self._available = False - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - raise NotImplementedError() - async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" try: - await self._adguard_turn_on() + await self.entity_description.turn_on_fn(self.adguard)() except AdGuardHomeError: _LOGGER.error("An error occurred while turning on AdGuard Home switch") self._available = False - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - raise NotImplementedError() - - -class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch): - """Defines a AdGuard Home protection switch.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home switch.""" - super().__init__(adguard, entry, "Protection", "mdi:shield-check", "protection") - - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - await self.adguard.disable_protection() - - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - await self.adguard.enable_protection() - async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" - self._state = await self.adguard.protection_enabled() - - -class AdGuardHomeParentalSwitch(AdGuardHomeSwitch): - """Defines a AdGuard Home parental control switch.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home switch.""" - super().__init__( - adguard, entry, "Parental control", "mdi:shield-check", "parental" - ) - - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - await self.adguard.parental.disable() - - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - await self.adguard.parental.enable() - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.parental.enabled() - - -class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch): - """Defines a AdGuard Home safe search switch.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home switch.""" - super().__init__( - adguard, entry, "Safe search", "mdi:shield-check", "safesearch" - ) - - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - await self.adguard.safesearch.disable() - - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - await self.adguard.safesearch.enable() - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.safesearch.enabled() - - -class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch): - """Defines a AdGuard Home safe search switch.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home switch.""" - super().__init__( - adguard, entry, "Safe browsing", "mdi:shield-check", "safebrowsing" - ) - - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - await self.adguard.safebrowsing.disable() - - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - await self.adguard.safebrowsing.enable() - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.safebrowsing.enabled() - - -class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch): - """Defines a AdGuard Home filtering switch.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home switch.""" - super().__init__(adguard, entry, "Filtering", "mdi:shield-check", "filtering") - - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - await self.adguard.filtering.disable() - - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - await self.adguard.filtering.enable() - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.filtering.enabled() - - -class AdGuardHomeQueryLogSwitch(AdGuardHomeSwitch): - """Defines a AdGuard Home query log switch.""" - - def __init__(self, adguard: AdGuardHome, entry: ConfigEntry) -> None: - """Initialize AdGuard Home switch.""" - super().__init__( - adguard, - entry, - "Query log", - "mdi:shield-check", - "querylog", - enabled_default=False, - ) - - async def _adguard_turn_off(self) -> None: - """Turn off the switch.""" - await self.adguard.querylog.disable() - - async def _adguard_turn_on(self) -> None: - """Turn on the switch.""" - await self.adguard.querylog.enable() - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - self._state = await self.adguard.querylog.enabled() + self._attr_is_on = await self.entity_description.is_on_fn(self.adguard)() From 98807f7efcf7c9327bb46b7628fd657fe4447c6b Mon Sep 17 00:00:00 2001 From: mkmer Date: Fri, 15 Jul 2022 00:28:00 -0400 Subject: [PATCH 2587/3516] Bump AIOAladdinConnect to 0.1.25 (#75235) Co-authored-by: Paulus Schoutsen --- homeassistant/components/aladdin_connect/__init__.py | 6 ++++-- homeassistant/components/aladdin_connect/config_flow.py | 7 +++++-- homeassistant/components/aladdin_connect/const.py | 1 + homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index 036f5364ef5..40dd0dd981a 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN +from .const import CLIENT_ID, DOMAIN _LOGGER: Final = logging.getLogger(__name__) @@ -23,7 +23,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up platform from a ConfigEntry.""" username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] - acc = AladdinConnectClient(username, password, async_get_clientsession(hass)) + acc = AladdinConnectClient( + username, password, async_get_clientsession(hass), CLIENT_ID + ) try: if not await acc.login(): raise ConfigEntryAuthFailed("Incorrect Password") diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index 0d45ea9a8ef..44d3f01a9ec 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -18,7 +18,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN +from .const import CLIENT_ID, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,10 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ acc = AladdinConnectClient( - data[CONF_USERNAME], data[CONF_PASSWORD], async_get_clientsession(hass) + data[CONF_USERNAME], + data[CONF_PASSWORD], + async_get_clientsession(hass), + CLIENT_ID, ) login = await acc.login() await acc.close() diff --git a/homeassistant/components/aladdin_connect/const.py b/homeassistant/components/aladdin_connect/const.py index 7a11cf63a9e..46d5d468f71 100644 --- a/homeassistant/components/aladdin_connect/const.py +++ b/homeassistant/components/aladdin_connect/const.py @@ -18,3 +18,4 @@ STATES_MAP: Final[dict[str, str]] = { DOMAIN = "aladdin_connect" SUPPORTED_FEATURES: Final = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE +CLIENT_ID = "1000" diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 8ef86a76bf8..5baeba33971 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.24"], + "requirements": ["AIOAladdinConnect==0.1.25"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index a695d12c975..26a27816bc4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.24 +AIOAladdinConnect==0.1.25 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5124f58a11c..df934644fed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.24 +AIOAladdinConnect==0.1.25 # homeassistant.components.adax Adax-local==0.1.4 From 2dde3d02cc88b0f71a95519d9ec722ef620b372c Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Thu, 14 Jul 2022 22:29:11 -0600 Subject: [PATCH 2588/3516] Bump pylitterbot to 2022.7.0 (#75241) --- 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 a07f13a47b5..104a06afc8f 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,7 +3,7 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2022.3.0"], + "requirements": ["pylitterbot==2022.7.0"], "codeowners": ["@natekspencer"], "iot_class": "cloud_polling", "loggers": ["pylitterbot"] diff --git a/requirements_all.txt b/requirements_all.txt index 26a27816bc4..cf89a5622bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1628,7 +1628,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.3.0 +pylitterbot==2022.7.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df934644fed..009ccb04343 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1107,7 +1107,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.3.0 +pylitterbot==2022.7.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 From a5693c083f63ba52250bbd973e20e8bd6c3442a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huryn?= Date: Sun, 3 Jul 2022 17:06:38 +0200 Subject: [PATCH 2589/3516] Address Blebox uniapi review sidenotes (#74298) * Changes accordingly to sidenotes given by @MartinHjelmare in pull #73834. * Mini version bump according to notes in pull #73834. * Error message fix, test adjustment. --- CODEOWNERS | 4 ++-- homeassistant/components/blebox/const.py | 21 +------------------ homeassistant/components/blebox/cover.py | 18 ++++++++++++++-- homeassistant/components/blebox/light.py | 9 ++++---- homeassistant/components/blebox/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/blebox/test_light.py | 19 +++++++++-------- 8 files changed, 37 insertions(+), 42 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 5f5588c0b91..572ffc11081 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -129,8 +129,8 @@ build.json @home-assistant/supervisor /homeassistant/components/binary_sensor/ @home-assistant/core /tests/components/binary_sensor/ @home-assistant/core /homeassistant/components/bizkaibus/ @UgaitzEtxebarria -/homeassistant/components/blebox/ @bbx-a @bbx-jp @riokuu -/tests/components/blebox/ @bbx-a @bbx-jp @riokuu +/homeassistant/components/blebox/ @bbx-a @riokuu +/tests/components/blebox/ @bbx-a @riokuu /homeassistant/components/blink/ @fronzbot /tests/components/blink/ @fronzbot /homeassistant/components/blueprint/ @home-assistant/core diff --git a/homeassistant/components/blebox/const.py b/homeassistant/components/blebox/const.py index f0c6a2d7586..013a6501068 100644 --- a/homeassistant/components/blebox/const.py +++ b/homeassistant/components/blebox/const.py @@ -3,13 +3,7 @@ from homeassistant.components.cover import CoverDeviceClass from homeassistant.components.sensor import SensorDeviceClass from homeassistant.components.switch import SwitchDeviceClass -from homeassistant.const import ( - STATE_CLOSED, - STATE_CLOSING, - STATE_OPEN, - STATE_OPENING, - TEMP_CELSIUS, -) +from homeassistant.const import TEMP_CELSIUS DOMAIN = "blebox" PRODUCT = "product" @@ -30,19 +24,6 @@ BLEBOX_TO_HASS_DEVICE_CLASSES = { "temperature": SensorDeviceClass.TEMPERATURE, } -BLEBOX_TO_HASS_COVER_STATES = { - None: None, - 0: STATE_CLOSING, # moving down - 1: STATE_OPENING, # moving up - 2: STATE_OPEN, # manually stopped - 3: STATE_CLOSED, # lower limit - 4: STATE_OPEN, # upper limit / open - # gateController - 5: STATE_OPEN, # overload - 6: STATE_OPEN, # motor failure - # 7 is not used - 8: STATE_OPEN, # safety stop -} BLEBOX_TO_UNIT_MAP = {"celsius": TEMP_CELSIUS} diff --git a/homeassistant/components/blebox/cover.py b/homeassistant/components/blebox/cover.py index 368443988a4..8763ec34d34 100644 --- a/homeassistant/components/blebox/cover.py +++ b/homeassistant/components/blebox/cover.py @@ -9,12 +9,26 @@ from homeassistant.components.cover import ( CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING +from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import BleBoxEntity, create_blebox_entities -from .const import BLEBOX_TO_HASS_COVER_STATES, BLEBOX_TO_HASS_DEVICE_CLASSES +from .const import BLEBOX_TO_HASS_DEVICE_CLASSES + +BLEBOX_TO_HASS_COVER_STATES = { + None: None, + 0: STATE_CLOSING, # moving down + 1: STATE_OPENING, # moving up + 2: STATE_OPEN, # manually stopped + 3: STATE_CLOSED, # lower limit + 4: STATE_OPEN, # upper limit / open + # gateController + 5: STATE_OPEN, # overload + 6: STATE_OPEN, # motor failure + # 7 is not used + 8: STATE_OPEN, # safety stop +} async def async_setup_entry( diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index 32cc6360db1..2bb6bc91762 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -4,7 +4,6 @@ from __future__ import annotations from datetime import timedelta import logging -from blebox_uniapi.error import BadOnValueError import blebox_uniapi.light from blebox_uniapi.light import BleboxColorMode @@ -166,10 +165,10 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity): else: try: await self._feature.async_on(value) - except BadOnValueError as ex: - _LOGGER.error( - "Turning on '%s' failed: Bad value %s (%s)", self.name, value, ex - ) + except ValueError as exc: + raise ValueError( + f"Turning on '{self.name}' failed: Bad value {value}" + ) from exc async def async_turn_off(self, **kwargs): """Turn the light off.""" diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 5c57d5f6b9f..08554695316 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -3,8 +3,8 @@ "name": "BleBox devices", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blebox", - "requirements": ["blebox_uniapi==2.0.0"], - "codeowners": ["@bbx-a", "@bbx-jp", "@riokuu"], + "requirements": ["blebox_uniapi==2.0.1"], + "codeowners": ["@bbx-a", "@riokuu"], "iot_class": "local_polling", "loggers": ["blebox_uniapi"] } diff --git a/requirements_all.txt b/requirements_all.txt index 01e309e7b6a..b2de6fd2584 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -402,7 +402,7 @@ bimmer_connected==0.9.6 bizkaibus==0.1.1 # homeassistant.components.blebox -blebox_uniapi==2.0.0 +blebox_uniapi==2.0.1 # homeassistant.components.blink blinkpy==0.19.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9900ef878b4..5442f40d743 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -314,7 +314,7 @@ bellows==0.31.1 bimmer_connected==0.9.6 # homeassistant.components.blebox -blebox_uniapi==2.0.0 +blebox_uniapi==2.0.1 # homeassistant.components.blink blinkpy==0.19.0 diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index 4663c216136..f61496714fb 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -509,17 +509,18 @@ async def test_turn_on_failure(feature, hass, config, caplog): caplog.set_level(logging.ERROR) feature_mock, entity_id = feature - feature_mock.async_on = AsyncMock(side_effect=blebox_uniapi.error.BadOnValueError) + feature_mock.async_on = AsyncMock(side_effect=ValueError) await async_setup_entity(hass, config, entity_id) feature_mock.sensible_on_value = 123 - await hass.services.async_call( - "light", - SERVICE_TURN_ON, - {"entity_id": entity_id}, - blocking=True, - ) + with pytest.raises(ValueError) as info: + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {"entity_id": entity_id}, + blocking=True, + ) - assert ( - f"Turning on '{feature_mock.full_name}' failed: Bad value 123 ()" in caplog.text + assert f"Turning on '{feature_mock.full_name}' failed: Bad value 123" in str( + info.value ) From b1c07ac17a80a3026e4ea57414c375b388f155ab Mon Sep 17 00:00:00 2001 From: Thomas Hollstegge Date: Thu, 14 Jul 2022 21:40:53 +0200 Subject: [PATCH 2590/3516] Fix Alexa: Only trigger doorbell event on actual state change to "ON" (#74924) * Alexa: Only trigger doorbell event on actual state change to "ON" * Remove unnecessary check for new_state Co-authored-by: Jan Bouwhuis * First check state is `on` before checking the old state Co-authored-by: Jan Bouwhuis --- .../components/alexa/state_report.py | 4 +- tests/components/alexa/test_state_report.py | 50 ++++++++++++++++++- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index d3e476c2e8c..783397ca047 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -86,7 +86,9 @@ async def async_enable_proactive_mode(hass, smart_home_config): return if should_doorbell: - if new_state.state == STATE_ON: + if new_state.state == STATE_ON and ( + old_state is None or old_state.state != STATE_ON + ): await async_send_doorbell_event_message( hass, smart_home_config, alexa_changed_entity ) diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index b2d36b9d179..bb61cea2413 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -346,7 +346,11 @@ async def test_doorbell_event(hass, aioclient_mock): hass.states.async_set( "binary_sensor.test_doorbell", "off", - {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, + { + "friendly_name": "Test Doorbell Sensor", + "device_class": "occupancy", + "linkquality": 42, + }, ) await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) @@ -354,7 +358,21 @@ async def test_doorbell_event(hass, aioclient_mock): hass.states.async_set( "binary_sensor.test_doorbell", "on", - {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, + { + "friendly_name": "Test Doorbell Sensor", + "device_class": "occupancy", + "linkquality": 42, + }, + ) + + hass.states.async_set( + "binary_sensor.test_doorbell", + "on", + { + "friendly_name": "Test Doorbell Sensor", + "device_class": "occupancy", + "linkquality": 99, + }, ) # To trigger event listener @@ -386,6 +404,34 @@ async def test_doorbell_event(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 2 +async def test_doorbell_event_from_unknown(hass, aioclient_mock): + """Test doorbell press reports.""" + aioclient_mock.post(TEST_URL, text="", status=202) + + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) + + hass.states.async_set( + "binary_sensor.test_doorbell", + "on", + { + "friendly_name": "Test Doorbell Sensor", + "device_class": "occupancy", + }, + ) + + # To trigger event listener + await hass.async_block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + call = aioclient_mock.mock_calls + + call_json = call[0][2] + assert call_json["event"]["header"]["namespace"] == "Alexa.DoorbellEventSource" + assert call_json["event"]["header"]["name"] == "DoorbellPress" + assert call_json["event"]["payload"]["cause"]["type"] == "PHYSICAL_INTERACTION" + assert call_json["event"]["endpoint"]["endpointId"] == "binary_sensor#test_doorbell" + + async def test_doorbell_event_fail(hass, aioclient_mock, caplog): """Test proactive state retries once.""" aioclient_mock.post( From b5e24048dbf9ccc4720b19b36353bbccf2aa13a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Huryn?= Date: Thu, 14 Jul 2022 11:21:01 +0200 Subject: [PATCH 2591/3516] Fix Blebox light scenes (#75106) * Bug fix for light platform, when async_turn_on recieves multiple keys. * Changes according to @MartinHjelmare suggestion. * Moved effect set call in BleBoxLightEntity.async_turn_on method. * Added tests for effect in light platform. Added ValueError raise if effect not in effect list. * Removed duplicated line from test as @MartinHjelmare suggested. --- homeassistant/components/blebox/light.py | 15 +++++--- tests/components/blebox/test_light.py | 46 ++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index 2bb6bc91762..a2ad51cfc9c 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -159,15 +159,20 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity): else: value = feature.apply_brightness(value, brightness) + try: + await self._feature.async_on(value) + except ValueError as exc: + raise ValueError( + f"Turning on '{self.name}' failed: Bad value {value}" + ) from exc + if effect is not None: - effect_value = self.effect_list.index(effect) - await self._feature.async_api_command("effect", effect_value) - else: try: - await self._feature.async_on(value) + effect_value = self.effect_list.index(effect) + await self._feature.async_api_command("effect", effect_value) except ValueError as exc: raise ValueError( - f"Turning on '{self.name}' failed: Bad value {value}" + f"Turning on with effect '{self.name}' failed: {effect} not in effect list." ) from exc async def async_turn_off(self, **kwargs): diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index f61496714fb..7afb78e5b03 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -7,6 +7,7 @@ import pytest from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_EFFECT, ATTR_RGBW_COLOR, ATTR_SUPPORTED_COLOR_MODES, ColorMode, @@ -524,3 +525,48 @@ async def test_turn_on_failure(feature, hass, config, caplog): assert f"Turning on '{feature_mock.full_name}' failed: Bad value 123" in str( info.value ) + + +async def test_wlightbox_on_effect(wlightbox, hass, config): + """Test light on.""" + + feature_mock, entity_id = wlightbox + + def initial_update(): + feature_mock.is_on = False + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + await async_setup_entity(hass, config, entity_id) + feature_mock.async_update = AsyncMock() + + state = hass.states.get(entity_id) + assert state.state == STATE_OFF + + def turn_on(value): + feature_mock.is_on = True + feature_mock.effect = "POLICE" + + feature_mock.async_on = AsyncMock(side_effect=turn_on) + + with pytest.raises(ValueError) as info: + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {"entity_id": entity_id, ATTR_EFFECT: "NOT IN LIST"}, + blocking=True, + ) + + assert ( + f"Turning on with effect '{feature_mock.full_name}' failed: NOT IN LIST not in effect list." + in str(info.value) + ) + + await hass.services.async_call( + "light", + SERVICE_TURN_ON, + {"entity_id": entity_id, ATTR_EFFECT: "POLICE"}, + blocking=True, + ) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_EFFECT] == "POLICE" From 326ffdcd494cce725e1fd82f3125d028e2a21a93 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 15 Jul 2022 03:24:24 +0800 Subject: [PATCH 2592/3516] Fix playback of hls cameras in stream (#75166) --- homeassistant/components/stream/__init__.py | 19 +++-- homeassistant/components/stream/core.py | 39 ++++++++-- homeassistant/components/stream/fmp4utils.py | 12 +++ homeassistant/components/stream/worker.py | 78 ++++++++++++++------ tests/components/stream/test_worker.py | 4 +- 5 files changed, 111 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index b842eb7fb78..ef68ea7bcae 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -57,9 +57,15 @@ from .const import ( SOURCE_TIMEOUT, STREAM_RESTART_INCREMENT, STREAM_RESTART_RESET_TIME, - TARGET_SEGMENT_DURATION_NON_LL_HLS, ) -from .core import PROVIDERS, IdleTimer, KeyFrameConverter, StreamOutput, StreamSettings +from .core import ( + PROVIDERS, + STREAM_SETTINGS_NON_LL_HLS, + IdleTimer, + KeyFrameConverter, + StreamOutput, + StreamSettings, +) from .diagnostics import Diagnostics from .hls import HlsStreamOutput, async_setup_hls @@ -224,14 +230,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hls_part_timeout=2 * conf[CONF_PART_DURATION], ) else: - hass.data[DOMAIN][ATTR_SETTINGS] = StreamSettings( - ll_hls=False, - min_segment_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS - - SEGMENT_DURATION_ADJUSTER, - part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS, - hls_advance_part_limit=3, - hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS, - ) + hass.data[DOMAIN][ATTR_SETTINGS] = STREAM_SETTINGS_NON_LL_HLS # Setup HLS hls_endpoint = async_setup_hls(hass) diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 09d9a9d5031..8c456af91aa 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -5,6 +5,7 @@ import asyncio from collections import deque from collections.abc import Callable, Coroutine, Iterable import datetime +import logging from typing import TYPE_CHECKING, Any from aiohttp import web @@ -16,13 +17,20 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.event import async_call_later from homeassistant.util.decorator import Registry -from .const import ATTR_STREAMS, DOMAIN +from .const import ( + ATTR_STREAMS, + DOMAIN, + SEGMENT_DURATION_ADJUSTER, + TARGET_SEGMENT_DURATION_NON_LL_HLS, +) if TYPE_CHECKING: from av import CodecContext, Packet from . import Stream +_LOGGER = logging.getLogger(__name__) + PROVIDERS: Registry[str, type[StreamOutput]] = Registry() @@ -37,6 +45,15 @@ class StreamSettings: hls_part_timeout: float = attr.ib() +STREAM_SETTINGS_NON_LL_HLS = StreamSettings( + ll_hls=False, + min_segment_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS - SEGMENT_DURATION_ADJUSTER, + part_target_duration=TARGET_SEGMENT_DURATION_NON_LL_HLS, + hls_advance_part_limit=3, + hls_part_timeout=TARGET_SEGMENT_DURATION_NON_LL_HLS, +) + + @attr.s(slots=True) class Part: """Represent a segment part.""" @@ -426,12 +443,22 @@ class KeyFrameConverter: return packet = self.packet self.packet = None - # decode packet (flush afterwards) - frames = self._codec_context.decode(packet) - for _i in range(2): - if frames: + for _ in range(2): # Retry once if codec context needs to be flushed + try: + # decode packet (flush afterwards) + frames = self._codec_context.decode(packet) + for _i in range(2): + if frames: + break + frames = self._codec_context.decode(None) break - frames = self._codec_context.decode(None) + except EOFError: + _LOGGER.debug("Codec context needs flushing, attempting to reopen") + self._codec_context.close() + self._codec_context.open() + else: + _LOGGER.debug("Unable to decode keyframe") + return if frames: frame = frames[0] if width and height: diff --git a/homeassistant/components/stream/fmp4utils.py b/homeassistant/components/stream/fmp4utils.py index f136784cf87..313f5632841 100644 --- a/homeassistant/components/stream/fmp4utils.py +++ b/homeassistant/components/stream/fmp4utils.py @@ -2,6 +2,10 @@ from __future__ import annotations from collections.abc import Generator +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from io import BytesIO def find_box( @@ -135,3 +139,11 @@ def get_codec_string(mp4_bytes: bytes) -> str: codecs.append(codec) return ",".join(codecs) + + +def read_init(bytes_io: BytesIO) -> bytes: + """Read the init from a mp4 file.""" + bytes_io.seek(24) + moov_len = int.from_bytes(bytes_io.read(4), byteorder="big") + bytes_io.seek(0) + return bytes_io.read(24 + moov_len) diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index e46d83542f7..aa49a6de7ae 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -5,11 +5,12 @@ from collections import defaultdict, deque from collections.abc import Callable, Generator, Iterator, Mapping import contextlib import datetime -from io import BytesIO +from io import SEEK_END, BytesIO import logging from threading import Event from typing import Any, cast +import attr import av from homeassistant.core import HomeAssistant @@ -24,8 +25,16 @@ from .const import ( SEGMENT_CONTAINER_FORMAT, SOURCE_TIMEOUT, ) -from .core import KeyFrameConverter, Part, Segment, StreamOutput, StreamSettings +from .core import ( + STREAM_SETTINGS_NON_LL_HLS, + KeyFrameConverter, + Part, + Segment, + StreamOutput, + StreamSettings, +) from .diagnostics import Diagnostics +from .fmp4utils import read_init from .hls import HlsStreamOutput _LOGGER = logging.getLogger(__name__) @@ -108,7 +117,7 @@ class StreamMuxer: hass: HomeAssistant, video_stream: av.video.VideoStream, audio_stream: av.audio.stream.AudioStream | None, - audio_bsf: av.BitStreamFilterContext | None, + audio_bsf: av.BitStreamFilter | None, stream_state: StreamState, stream_settings: StreamSettings, ) -> None: @@ -120,6 +129,7 @@ class StreamMuxer: self._input_video_stream: av.video.VideoStream = video_stream self._input_audio_stream: av.audio.stream.AudioStream | None = audio_stream self._audio_bsf = audio_bsf + self._audio_bsf_context: av.BitStreamFilterContext = None self._output_video_stream: av.video.VideoStream = None self._output_audio_stream: av.audio.stream.AudioStream | None = None self._segment: Segment | None = None @@ -151,7 +161,7 @@ class StreamMuxer: **{ # Removed skip_sidx - see https://github.com/home-assistant/core/pull/39970 # "cmaf" flag replaces several of the movflags used, but too recent to use for now - "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer", + "movflags": "frag_custom+empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer+delay_moov", # Sometimes the first segment begins with negative timestamps, and this setting just # adjusts the timestamps in the output from that segment to start from 0. Helps from # having to make some adjustments in test_durations @@ -164,7 +174,7 @@ class StreamMuxer: # Fragment durations may exceed the 15% allowed variance but it seems ok **( { - "movflags": "empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer", + "movflags": "empty_moov+default_base_moof+frag_discont+negative_cts_offsets+skip_trailer+delay_moov", # 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". @@ -194,8 +204,11 @@ class StreamMuxer: # Check if audio is requested output_astream = None if input_astream: + if self._audio_bsf: + self._audio_bsf_context = self._audio_bsf.create() + self._audio_bsf_context.set_input_stream(input_astream) output_astream = container.add_stream( - template=self._audio_bsf or input_astream + template=self._audio_bsf_context or input_astream ) return container, output_vstream, output_astream @@ -238,15 +251,29 @@ class StreamMuxer: self._part_has_keyframe |= packet.is_keyframe elif packet.stream == self._input_audio_stream: - if self._audio_bsf: - self._audio_bsf.send(packet) - while packet := self._audio_bsf.recv(): + if self._audio_bsf_context: + self._audio_bsf_context.send(packet) + while packet := self._audio_bsf_context.recv(): packet.stream = self._output_audio_stream self._av_output.mux(packet) return packet.stream = self._output_audio_stream self._av_output.mux(packet) + def create_segment(self) -> None: + """Create a segment when the moov is ready.""" + self._segment = Segment( + sequence=self._stream_state.sequence, + stream_id=self._stream_state.stream_id, + init=read_init(self._memory_file), + # Fetch the latest StreamOutputs, which may have changed since the + # worker started. + stream_outputs=self._stream_state.outputs, + start_time=self._start_time, + ) + self._memory_file_pos = self._memory_file.tell() + self._memory_file.seek(0, SEEK_END) + def check_flush_part(self, packet: av.Packet) -> None: """Check for and mark a part segment boundary and record its duration.""" if self._memory_file_pos == self._memory_file.tell(): @@ -254,16 +281,10 @@ class StreamMuxer: if self._segment is None: # We have our first non-zero byte position. This means the init has just # been written. Create a Segment and put it to the queue of each output. - self._segment = Segment( - sequence=self._stream_state.sequence, - stream_id=self._stream_state.stream_id, - init=self._memory_file.getvalue(), - # Fetch the latest StreamOutputs, which may have changed since the - # worker started. - stream_outputs=self._stream_state.outputs, - start_time=self._start_time, - ) - self._memory_file_pos = self._memory_file.tell() + self.create_segment() + # When using delay_moov, the moov is not written until a moof is also ready + # Flush the moof + self.flush(packet, last_part=False) else: # These are the ends of the part segments self.flush(packet, last_part=False) @@ -297,6 +318,10 @@ class StreamMuxer: # Closing the av_output will write the remaining buffered data to the # memory_file as a new moof/mdat. self._av_output.close() + # With delay_moov, this may be the first time the file pointer has + # moved, so the segment may not yet have been created + if not self._segment: + self.create_segment() elif not self._part_has_keyframe: # Parts which are not the last part or an independent part should # not have durations below 0.85 of the part target duration. @@ -305,6 +330,9 @@ class StreamMuxer: self._part_start_dts + 0.85 * self._stream_settings.part_target_duration / packet.time_base, ) + # Undo dts adjustments if we don't have ll_hls + if not self._stream_settings.ll_hls: + adjusted_dts = packet.dts assert self._segment self._memory_file.seek(self._memory_file_pos) self._hass.loop.call_soon_threadsafe( @@ -445,10 +473,7 @@ def get_audio_bitstream_filter( _LOGGER.debug( "ADTS AAC detected. Adding aac_adtstoaac bitstream filter" ) - bsf = av.BitStreamFilter("aac_adtstoasc") - bsf_context = bsf.create() - bsf_context.set_input_stream(audio_stream) - return bsf_context + return av.BitStreamFilter("aac_adtstoasc") break return None @@ -489,7 +514,12 @@ def stream_worker( audio_stream = None # Disable ll-hls for hls inputs if container.format.name == "hls": - stream_settings.ll_hls = False + for field in attr.fields(StreamSettings): + setattr( + stream_settings, + field.name, + getattr(STREAM_SETTINGS_NON_LL_HLS, field.name), + ) stream_state.diagnostics.set_value("container_format", container.format.name) stream_state.diagnostics.set_value("video_codec", video_stream.name) if audio_stream: diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 8717e23a476..65a01051606 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -749,7 +749,9 @@ async def test_durations(hass, worker_finished_stream): }, ) - source = generate_h264_video(duration=SEGMENT_DURATION + 1) + source = generate_h264_video( + duration=round(SEGMENT_DURATION + target_part_duration + 1) + ) worker_finished, mock_stream = worker_finished_stream with patch("homeassistant.components.stream.Stream", wraps=mock_stream): From 674e02914bf035ae0b9053f6c1eabd992f615460 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 14 Jul 2022 10:55:24 -0400 Subject: [PATCH 2593/3516] Bump version of pyunifiprotect to 4.0.10 (#75180) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 9aeb8b48050..35dafc96927 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.9", "unifi-discovery==1.1.4"], + "requirements": ["pyunifiprotect==4.0.10", "unifi-discovery==1.1.4"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index b2de6fd2584..e4d2125bc36 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1993,7 +1993,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.9 +pyunifiprotect==4.0.10 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5442f40d743..341409a28a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1331,7 +1331,7 @@ pytrafikverket==0.2.0.1 pyudev==0.22.0 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.9 +pyunifiprotect==4.0.10 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From d401faac7c1e55067153fc8f74395a14c939671d Mon Sep 17 00:00:00 2001 From: mkmer Date: Thu, 14 Jul 2022 15:14:41 -0400 Subject: [PATCH 2594/3516] Bumped AIOAladdin Connect to 0.1.24 (#75182) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index bdf906ad200..8ef86a76bf8 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.23"], + "requirements": ["AIOAladdinConnect==0.1.24"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index e4d2125bc36..aec20c76564 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.23 +AIOAladdinConnect==0.1.24 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 341409a28a7..eb71ac5d06f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.23 +AIOAladdinConnect==0.1.24 # homeassistant.components.adax Adax-local==0.1.4 From 5eaa15138c75b950e1fb85415d5207026c0c5cce Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 14 Jul 2022 15:15:51 -0400 Subject: [PATCH 2595/3516] Bump zigpy from 0.47.2 to 0.47.3 (#75194) --- 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 769254511a7..c43958f7e46 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "pyserial-asyncio==0.6", "zha-quirks==0.0.77", "zigpy-deconz==0.18.0", - "zigpy==0.47.2", + "zigpy==0.47.3", "zigpy-xbee==0.15.0", "zigpy-zigate==0.9.0", "zigpy-znp==0.8.1" diff --git a/requirements_all.txt b/requirements_all.txt index aec20c76564..e7f719510a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2519,7 +2519,7 @@ zigpy-zigate==0.9.0 zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.47.2 +zigpy==0.47.3 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eb71ac5d06f..1c26f68f499 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1680,7 +1680,7 @@ zigpy-zigate==0.9.0 zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.47.2 +zigpy==0.47.3 # homeassistant.components.zwave_js zwave-js-server-python==0.39.0 From b231eea0c6e97d96ba2dc1a26e702bdb5d705465 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:38:22 -0400 Subject: [PATCH 2596/3516] Skip `iso4217` version 1.10, which includes a broken `__init__.pyi` file (#75200) --- homeassistant/package_constraints.txt | 4 ++++ script/gen_requirements_all.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 167fe546b35..207268404a5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -118,3 +118,7 @@ pydantic!=1.9.1 # Breaks asyncio # https://github.com/pubnub/python/issues/130 pubnub!=6.4.0 + +# Package's __init__.pyi stub has invalid syntax and breaks mypy +# https://github.com/dahlia/iso4217/issues/16 +iso4217!=1.10.20220401 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 7b000a30be2..7f77f4ae106 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -136,6 +136,10 @@ pydantic!=1.9.1 # Breaks asyncio # https://github.com/pubnub/python/issues/130 pubnub!=6.4.0 + +# Package's __init__.pyi stub has invalid syntax and breaks mypy +# https://github.com/dahlia/iso4217/issues/16 +iso4217!=1.10.20220401 """ IGNORE_PRE_COMMIT_HOOK_ID = ( From 62d77a135b4f89caf59684fe57de4e57b66d454b Mon Sep 17 00:00:00 2001 From: Khole Date: Thu, 14 Jul 2022 19:50:35 +0100 Subject: [PATCH 2597/3516] Fix Hive power unit of measurement (#75210) --- homeassistant/components/hive/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index 5bac23fdb3d..d985f1a83bb 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -8,7 +8,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, POWER_KILO_WATT +from homeassistant.const import PERCENTAGE, POWER_WATT from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -26,7 +26,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="Power", - native_unit_of_measurement=POWER_KILO_WATT, + native_unit_of_measurement=POWER_WATT, state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.POWER, ), From c56caca1828c60943ff6936a47cc6deca5ca7cd5 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 14 Jul 2022 18:12:48 -0500 Subject: [PATCH 2598/3516] Bump frontend to 20220707.1 (#75232) --- 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 5c7fd1a20be..288b4796e0e 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220707.0"], + "requirements": ["home-assistant-frontend==20220707.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 207268404a5..d27d9382c37 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,7 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.1 -home-assistant-frontend==20220707.0 +home-assistant-frontend==20220707.1 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index e7f719510a6..965c6bfd1e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -828,7 +828,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220707.0 +home-assistant-frontend==20220707.1 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c26f68f499..a02857ab353 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -595,7 +595,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220707.0 +home-assistant-frontend==20220707.1 # homeassistant.components.home_connect homeconnect==0.7.1 From e4f1fa48d315e2fce6516fbeccfd073e87e581a0 Mon Sep 17 00:00:00 2001 From: mkmer Date: Fri, 15 Jul 2022 00:28:00 -0400 Subject: [PATCH 2599/3516] Bump AIOAladdinConnect to 0.1.25 (#75235) Co-authored-by: Paulus Schoutsen --- homeassistant/components/aladdin_connect/__init__.py | 6 ++++-- homeassistant/components/aladdin_connect/config_flow.py | 7 +++++-- homeassistant/components/aladdin_connect/const.py | 1 + homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index af996c9f5b2..82b0c8ec692 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN +from .const import CLIENT_ID, DOMAIN _LOGGER: Final = logging.getLogger(__name__) @@ -23,7 +23,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up platform from a ConfigEntry.""" username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] - acc = AladdinConnectClient(username, password, async_get_clientsession(hass)) + acc = AladdinConnectClient( + username, password, async_get_clientsession(hass), CLIENT_ID + ) try: if not await acc.login(): raise ConfigEntryAuthFailed("Incorrect Password") diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index 0d45ea9a8ef..44d3f01a9ec 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -18,7 +18,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN +from .const import CLIENT_ID, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,10 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ acc = AladdinConnectClient( - data[CONF_USERNAME], data[CONF_PASSWORD], async_get_clientsession(hass) + data[CONF_USERNAME], + data[CONF_PASSWORD], + async_get_clientsession(hass), + CLIENT_ID, ) login = await acc.login() await acc.close() diff --git a/homeassistant/components/aladdin_connect/const.py b/homeassistant/components/aladdin_connect/const.py index 7a11cf63a9e..46d5d468f71 100644 --- a/homeassistant/components/aladdin_connect/const.py +++ b/homeassistant/components/aladdin_connect/const.py @@ -18,3 +18,4 @@ STATES_MAP: Final[dict[str, str]] = { DOMAIN = "aladdin_connect" SUPPORTED_FEATURES: Final = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE +CLIENT_ID = "1000" diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 8ef86a76bf8..5baeba33971 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.24"], + "requirements": ["AIOAladdinConnect==0.1.25"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 965c6bfd1e2..ea539b9339f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.24 +AIOAladdinConnect==0.1.25 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a02857ab353..02b6d9308ad 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.24 +AIOAladdinConnect==0.1.25 # homeassistant.components.adax Adax-local==0.1.4 From 99d39b203f831732d631d7baad0208dda1e4621c Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Thu, 14 Jul 2022 22:29:11 -0600 Subject: [PATCH 2600/3516] Bump pylitterbot to 2022.7.0 (#75241) --- 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 a07f13a47b5..104a06afc8f 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -3,7 +3,7 @@ "name": "Litter-Robot", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/litterrobot", - "requirements": ["pylitterbot==2022.3.0"], + "requirements": ["pylitterbot==2022.7.0"], "codeowners": ["@natekspencer"], "iot_class": "cloud_polling", "loggers": ["pylitterbot"] diff --git a/requirements_all.txt b/requirements_all.txt index ea539b9339f..bb4bad5d959 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1625,7 +1625,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.3.0 +pylitterbot==2022.7.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02b6d9308ad..79318dc1789 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1101,7 +1101,7 @@ pylibrespot-java==0.1.0 pylitejet==0.3.0 # homeassistant.components.litterrobot -pylitterbot==2022.3.0 +pylitterbot==2022.7.0 # homeassistant.components.lutron_caseta pylutron-caseta==0.13.1 From c185e636ed7ef3c8d47fbc02238489cc576a3793 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 14 Jul 2022 21:31:07 -0700 Subject: [PATCH 2601/3516] Bumped version to 2022.7.5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9db438feeb3..a2d71ea6d71 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "4" +PATCH_VERSION: Final = "5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index b7f75f9a008..c68cd921da2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.4" +version = "2022.7.5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From b4003713b67d3e83397382f4af5d10e8d9a94660 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 14 Jul 2022 22:01:18 -0700 Subject: [PATCH 2602/3516] Remove nest mac prefix that matches cast devices (#75108) --- homeassistant/components/nest/manifest.json | 3 +-- homeassistant/generated/dhcp.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index d0588d46f06..72e0aed8420 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -11,8 +11,7 @@ "dhcp": [ { "macaddress": "18B430*" }, { "macaddress": "641666*" }, - { "macaddress": "D8EB46*" }, - { "macaddress": "1C53F9*" } + { "macaddress": "D8EB46*" } ], "iot_class": "cloud_push", "loggers": ["google_nest_sdm", "nest"] diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 57be9a62138..c062870f3e3 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -69,7 +69,6 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'nest', 'macaddress': '18B430*'}, {'domain': 'nest', 'macaddress': '641666*'}, {'domain': 'nest', 'macaddress': 'D8EB46*'}, - {'domain': 'nest', 'macaddress': '1C53F9*'}, {'domain': 'nexia', 'hostname': 'xl857-*', 'macaddress': '000231*'}, {'domain': 'nuheat', 'hostname': 'nuheat', 'macaddress': '002338*'}, {'domain': 'nuki', 'hostname': 'nuki_bridge_*'}, From a23b427025579a8be7d1cb5db13eb58ebdd0f963 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 14 Jul 2022 22:01:18 -0700 Subject: [PATCH 2603/3516] Remove nest mac prefix that matches cast devices (#75108) --- homeassistant/components/nest/manifest.json | 3 +-- homeassistant/generated/dhcp.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index d0588d46f06..72e0aed8420 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -11,8 +11,7 @@ "dhcp": [ { "macaddress": "18B430*" }, { "macaddress": "641666*" }, - { "macaddress": "D8EB46*" }, - { "macaddress": "1C53F9*" } + { "macaddress": "D8EB46*" } ], "iot_class": "cloud_push", "loggers": ["google_nest_sdm", "nest"] diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index e9cf6ca4c06..71c8dd530c2 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -69,7 +69,6 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'nest', 'macaddress': '18B430*'}, {'domain': 'nest', 'macaddress': '641666*'}, {'domain': 'nest', 'macaddress': 'D8EB46*'}, - {'domain': 'nest', 'macaddress': '1C53F9*'}, {'domain': 'nexia', 'hostname': 'xl857-*', 'macaddress': '000231*'}, {'domain': 'nuheat', 'hostname': 'nuheat', 'macaddress': '002338*'}, {'domain': 'nuki', 'hostname': 'nuki_bridge_*'}, From 3e98ac180c4342eaddfa3c3b0226b4d17cb5df94 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 15 Jul 2022 10:44:29 +0200 Subject: [PATCH 2604/3516] Migrate Trafikverket Ferry to new entity naming style (#75206) --- .../components/trafikverket_ferry/sensor.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/trafikverket_ferry/sensor.py b/homeassistant/components/trafikverket_ferry/sensor.py index 256341a7132..b02c673f698 100644 --- a/homeassistant/components/trafikverket_ferry/sensor.py +++ b/homeassistant/components/trafikverket_ferry/sensor.py @@ -51,7 +51,7 @@ class TrafikverketSensorEntityDescription( SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( TrafikverketSensorEntityDescription( key="departure_time", - name="Departure Time", + name="Departure time", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_time"]), @@ -59,21 +59,21 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( ), TrafikverketSensorEntityDescription( key="departure_from", - name="Departure From", + name="Departure from", icon="mdi:ferry", value_fn=lambda data: cast(str, data["departure_from"]), info_fn=lambda data: cast(list[str], data["departure_information"]), ), TrafikverketSensorEntityDescription( key="departure_to", - name="Departure To", + name="Departure to", icon="mdi:ferry", value_fn=lambda data: cast(str, data["departure_to"]), info_fn=lambda data: cast(list[str], data["departure_information"]), ), TrafikverketSensorEntityDescription( key="departure_modified", - name="Departure Modified", + name="Departure modified", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_modified"]), @@ -82,7 +82,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( ), TrafikverketSensorEntityDescription( key="departure_time_next", - name="Departure Time Next", + name="Departure time next", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_time_next"]), @@ -91,7 +91,7 @@ SENSOR_TYPES: tuple[TrafikverketSensorEntityDescription, ...] = ( ), TrafikverketSensorEntityDescription( key="departure_time_next_next", - name="Departure Time Next After", + name="Departure time next after", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda data: as_utc(data["departure_time_next_next"]), @@ -121,6 +121,7 @@ class FerrySensor(CoordinatorEntity[TVDataUpdateCoordinator], SensorEntity): entity_description: TrafikverketSensorEntityDescription _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True def __init__( self, @@ -131,7 +132,6 @@ class FerrySensor(CoordinatorEntity[TVDataUpdateCoordinator], SensorEntity): ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._attr_name = f"{name} {entity_description.name}" self._attr_unique_id = f"{entry_id}-{entity_description.key}" self.entity_description = entity_description self._attr_device_info = DeviceInfo( From 911402e7472c1a8ca6cde60b3039d21c2e64cec9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 15 Jul 2022 10:47:30 +0200 Subject: [PATCH 2605/3516] Remove cloud from mypy ignore list (#74449) --- homeassistant/components/cloud/http_api.py | 1 + homeassistant/components/webhook/__init__.py | 18 +++++++++++++----- mypy.ini | 6 ------ script/hassfest/mypy_config.py | 2 -- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 6086cef703a..8e5c214b388 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -419,6 +419,7 @@ async def websocket_hook_delete(hass, connection, msg): async def _account_data(hass: HomeAssistant, cloud: Cloud): """Generate the auth data JSON response.""" + assert hass.config.api if not cloud.is_logged_in: return { "logged_in": False, diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index fb9927f1b37..449de006bf9 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -6,7 +6,9 @@ from http import HTTPStatus from ipaddress import ip_address import logging import secrets +from typing import TYPE_CHECKING, Any +from aiohttp import StreamReader from aiohttp.web import Request, Response import voluptuous as vol @@ -17,7 +19,7 @@ from homeassistant.helpers.network import get_url from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util import network -from homeassistant.util.aiohttp import MockRequest, serialize_response +from homeassistant.util.aiohttp import MockRequest, MockStreamReader, serialize_response _LOGGER = logging.getLogger(__name__) @@ -83,17 +85,20 @@ def async_generate_path(webhook_id: str) -> str: @bind_hass async def async_handle_webhook( - hass: HomeAssistant, webhook_id: str, request: Request + hass: HomeAssistant, webhook_id: str, request: Request | MockRequest ) -> Response: """Handle a webhook.""" - handlers = hass.data.setdefault(DOMAIN, {}) + handlers: dict[str, dict[str, Any]] = hass.data.setdefault(DOMAIN, {}) # Always respond successfully to not give away if a hook exists or not. if (webhook := handlers.get(webhook_id)) is None: + content_stream: StreamReader | MockStreamReader if isinstance(request, MockRequest): received_from = request.mock_source + content_stream = request.content else: received_from = request.remote + content_stream = request.content _LOGGER.info( "Received message for unregistered webhook %s from %s", @@ -102,13 +107,16 @@ async def async_handle_webhook( ) # Look at content to provide some context for received webhook # Limit to 64 chars to avoid flooding the log - content = await request.content.read(64) + content = await content_stream.read(64) _LOGGER.debug("%s", content) return Response(status=HTTPStatus.OK) if webhook["local_only"]: + if TYPE_CHECKING: + assert isinstance(request, Request) + assert request.remote is not None try: - remote = ip_address(request.remote) # type: ignore[arg-type] + remote = ip_address(request.remote) except ValueError: _LOGGER.debug("Unable to parse remote ip %s", request.remote) return Response(status=HTTPStatus.OK) diff --git a/mypy.ini b/mypy.ini index ab83c443d5a..5a3cbd7a05f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2656,12 +2656,6 @@ no_implicit_optional = false warn_return_any = false warn_unreachable = false -[mypy-homeassistant.components.cloud.client] -ignore_errors = true - -[mypy-homeassistant.components.cloud.http_api] -ignore_errors = true - [mypy-homeassistant.components.sonos] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 2e3e80ebc50..4bf8cfd68cf 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -16,8 +16,6 @@ from .model import Config, Integration # remove your component from this list to enable type checks. # Do your best to not add anything new here. IGNORED_MODULES: Final[list[str]] = [ - "homeassistant.components.cloud.client", - "homeassistant.components.cloud.http_api", "homeassistant.components.sonos", "homeassistant.components.sonos.alarms", "homeassistant.components.sonos.binary_sensor", From cba3c8cf65f984849e93e866826f7188e0bd7221 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 15 Jul 2022 10:51:18 +0200 Subject: [PATCH 2606/3516] Migrate Sensibo to new entity naming style (#75212) --- .../components/sensibo/binary_sensor.py | 17 ++++++----------- homeassistant/components/sensibo/button.py | 3 +-- homeassistant/components/sensibo/climate.py | 1 - homeassistant/components/sensibo/entity.py | 8 +++++--- homeassistant/components/sensibo/number.py | 1 - homeassistant/components/sensibo/select.py | 3 +-- homeassistant/components/sensibo/sensor.py | 16 ++++++---------- homeassistant/components/sensibo/switch.py | 1 - homeassistant/components/sensibo/update.py | 3 +-- 9 files changed, 20 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/sensibo/binary_sensor.py b/homeassistant/components/sensibo/binary_sensor.py index ed280aab4fe..3a61570701d 100644 --- a/homeassistant/components/sensibo/binary_sensor.py +++ b/homeassistant/components/sensibo/binary_sensor.py @@ -55,7 +55,7 @@ class SensiboDeviceBinarySensorEntityDescription( FILTER_CLEAN_REQUIRED_DESCRIPTION = SensiboDeviceBinarySensorEntityDescription( key="filter_clean", device_class=BinarySensorDeviceClass.PROBLEM, - name="Filter Clean Required", + name="Filter clean required", value_fn=lambda data: data.filter_clean, ) @@ -71,7 +71,7 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionBinarySensorEntityDescription, ...] = ( SensiboMotionBinarySensorEntityDescription( key="is_main_sensor", entity_category=EntityCategory.DIAGNOSTIC, - name="Main Sensor", + name="Main sensor", icon="mdi:connection", value_fn=lambda data: data.is_main_sensor, ), @@ -88,7 +88,7 @@ MOTION_DEVICE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, .. SensiboDeviceBinarySensorEntityDescription( key="room_occupied", device_class=BinarySensorDeviceClass.MOTION, - name="Room Occupied", + name="Room occupied", icon="mdi:motion-sensor", value_fn=lambda data: data.room_occupied, ), @@ -111,7 +111,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( key="pure_geo_integration", entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, - name="Pure Boost linked with Presence", + name="Pure Boost linked with presence", icon="mdi:connection", value_fn=lambda data: data.pure_geo_integration, ), @@ -119,7 +119,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( key="pure_measure_integration", entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, - name="Pure Boost linked with Indoor Air Quality", + name="Pure Boost linked with indoor air quality", icon="mdi:connection", value_fn=lambda data: data.pure_measure_integration, ), @@ -127,7 +127,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceBinarySensorEntityDescription, ...] = ( key="pure_prime_integration", entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.CONNECTIVITY, - name="Pure Boost linked with Outdoor Air Quality", + name="Pure Boost linked with outdoor air quality", icon="mdi:connection", value_fn=lambda data: data.pure_prime_integration, ), @@ -194,13 +194,9 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, BinarySensorEntity): device_id, sensor_id, sensor_data, - entity_description.name, ) self.entity_description = entity_description self._attr_unique_id = f"{sensor_id}-{entity_description.key}" - self._attr_name = ( - f"{self.device_data.name} Motion Sensor {entity_description.name}" - ) @property def is_on(self) -> bool | None: @@ -228,7 +224,6 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, BinarySensorEntity): ) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/sensibo/button.py b/homeassistant/components/sensibo/button.py index 97ae6321f7e..ad8a525aebb 100644 --- a/homeassistant/components/sensibo/button.py +++ b/homeassistant/components/sensibo/button.py @@ -16,7 +16,7 @@ PARALLEL_UPDATES = 0 DEVICE_BUTTON_TYPES: ButtonEntityDescription = ButtonEntityDescription( key="reset_filter", - name="Reset Filter", + name="Reset filter", icon="mdi:air-filter", entity_category=EntityCategory.CONFIG, ) @@ -57,7 +57,6 @@ class SensiboDeviceButton(SensiboDeviceBaseEntity, ButtonEntity): ) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" async def async_press(self) -> None: """Press the button.""" diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index b4af38ab69c..25ac73bfd4f 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -126,7 +126,6 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): """Initiate SensiboClimate.""" super().__init__(coordinator, device_id) self._attr_unique_id = device_id - self._attr_name = self.device_data.name self._attr_temperature_unit = ( TEMP_CELSIUS if self.device_data.temp_unit == "C" else TEMP_FAHRENHEIT ) diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index ac2ec24fac1..41d8b8b5070 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -37,6 +37,8 @@ class SensiboBaseEntity(CoordinatorEntity[SensiboDataUpdateCoordinator]): class SensiboDeviceBaseEntity(SensiboBaseEntity): """Representation of a Sensibo device.""" + _attr_has_entity_name = True + def __init__( self, coordinator: SensiboDataUpdateCoordinator, @@ -114,21 +116,21 @@ class SensiboDeviceBaseEntity(SensiboBaseEntity): class SensiboMotionBaseEntity(SensiboBaseEntity): """Representation of a Sensibo motion entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: SensiboDataUpdateCoordinator, device_id: str, sensor_id: str, sensor_data: MotionSensor, - name: str | None, ) -> None: """Initiate Sensibo Number.""" super().__init__(coordinator, device_id) self._sensor_id = sensor_id - self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, sensor_id)}, - name=f"{self.device_data.name} Motion Sensor {name}", + name=f"{self.device_data.name} Motion Sensor", via_device=(DOMAIN, device_id), manufacturer="Sensibo", configuration_url="https://home.sensibo.com/", diff --git a/homeassistant/components/sensibo/number.py b/homeassistant/components/sensibo/number.py index 183c4db4b87..bcad658c700 100644 --- a/homeassistant/components/sensibo/number.py +++ b/homeassistant/components/sensibo/number.py @@ -86,7 +86,6 @@ class SensiboNumber(SensiboDeviceBaseEntity, NumberEntity): super().__init__(coordinator, device_id) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" @property def native_value(self) -> float | None: diff --git a/homeassistant/components/sensibo/select.py b/homeassistant/components/sensibo/select.py index f64411ff4dc..a8cfc527704 100644 --- a/homeassistant/components/sensibo/select.py +++ b/homeassistant/components/sensibo/select.py @@ -36,7 +36,7 @@ DEVICE_SELECT_TYPES = ( key="horizontalSwing", remote_key="horizontal_swing_mode", remote_options="horizontal_swing_modes", - name="Horizontal Swing", + name="Horizontal swing", icon="mdi:air-conditioner", ), SensiboSelectEntityDescription( @@ -79,7 +79,6 @@ class SensiboSelect(SensiboDeviceBaseEntity, SelectEntity): super().__init__(coordinator, device_id) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" @property def current_option(self) -> str | None: diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 8a53a0febd4..7a17db85a5b 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -69,7 +69,7 @@ class SensiboDeviceSensorEntityDescription( FILTER_LAST_RESET_DESCRIPTION = SensiboDeviceSensorEntityDescription( key="filter_last_reset", device_class=SensorDeviceClass.TIMESTAMP, - name="Filter Last Reset", + name="Filter last reset", icon="mdi:timer", value_fn=lambda data: data.filter_last_reset, extra_fn=None, @@ -93,7 +93,7 @@ MOTION_SENSOR_TYPES: tuple[SensiboMotionSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, state_class=SensorStateClass.MEASUREMENT, - name="Battery Voltage", + name="Battery voltage", icon="mdi:battery", value_fn=lambda data: data.battery_voltage, ), @@ -128,7 +128,7 @@ PURE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( ), SensiboDeviceSensorEntityDescription( key="pure_sensitivity", - name="Pure Sensitivity", + name="Pure sensitivity", icon="mdi:air-filter", value_fn=lambda data: data.pure_sensitivity, extra_fn=None, @@ -140,7 +140,7 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( SensiboDeviceSensorEntityDescription( key="timer_time", device_class=SensorDeviceClass.TIMESTAMP, - name="Timer End Time", + name="Timer end time", icon="mdi:timer", value_fn=lambda data: data.timer_time, extra_fn=lambda data: {"id": data.timer_id, "turn_on": data.timer_state_on}, @@ -149,7 +149,7 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceSensorEntityDescription, ...] = ( key="feels_like", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - name="Temperature Feels Like", + name="Temperature feels like", value_fn=lambda data: data.feelslike, extra_fn=None, entity_registry_enabled_default=False, @@ -237,13 +237,10 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, SensorEntity): device_id, sensor_id, sensor_data, - entity_description.name, ) self.entity_description = entity_description self._attr_unique_id = f"{sensor_id}-{entity_description.key}" - self._attr_name = ( - f"{self.device_data.name} Motion Sensor {entity_description.name}" - ) + self._attr_name = entity_description.name @property def native_unit_of_measurement(self) -> str | None: @@ -280,7 +277,6 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, SensorEntity): ) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" @property def native_unit_of_measurement(self) -> str | None: diff --git a/homeassistant/components/sensibo/switch.py b/homeassistant/components/sensibo/switch.py index d9cf9417504..14cfaac73ae 100644 --- a/homeassistant/components/sensibo/switch.py +++ b/homeassistant/components/sensibo/switch.py @@ -135,7 +135,6 @@ class SensiboDeviceSwitch(SensiboDeviceBaseEntity, SwitchEntity): ) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/sensibo/update.py b/homeassistant/components/sensibo/update.py index 48304cbd3c5..67e0cbd6e65 100644 --- a/homeassistant/components/sensibo/update.py +++ b/homeassistant/components/sensibo/update.py @@ -43,7 +43,7 @@ DEVICE_SENSOR_TYPES: tuple[SensiboDeviceUpdateEntityDescription, ...] = ( key="fw_ver_available", device_class=UpdateDeviceClass.FIRMWARE, entity_category=EntityCategory.DIAGNOSTIC, - name="Update Available", + name="Update available", icon="mdi:rocket-launch", value_version=lambda data: data.fw_ver, value_available=lambda data: data.fw_ver_available, @@ -81,7 +81,6 @@ class SensiboDeviceUpdate(SensiboDeviceBaseEntity, UpdateEntity): super().__init__(coordinator, device_id) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = f"{self.device_data.name} {entity_description.name}" self._attr_title = self.device_data.model @property From d2e5d01acad3d60b2bb057dd65b50118f6cd1831 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 15 Jul 2022 10:52:07 +0200 Subject: [PATCH 2607/3516] Migrate Yale Smart Alarm to new entity naming style (#75202) --- .../components/yale_smart_alarm/alarm_control_panel.py | 2 -- .../components/yale_smart_alarm/binary_sensor.py | 6 +----- homeassistant/components/yale_smart_alarm/button.py | 8 +++++--- homeassistant/components/yale_smart_alarm/entity.py | 7 +++++-- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index fbd3f945aa2..e2df1b09ebe 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -14,7 +14,6 @@ from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -47,7 +46,6 @@ class YaleAlarmDevice(YaleAlarmEntity, AlarmControlPanelEntity): def __init__(self, coordinator: YaleDataUpdateCoordinator) -> None: """Initialize the Yale Alarm Device.""" super().__init__(coordinator) - self._attr_name = coordinator.entry.data[CONF_NAME] self._attr_unique_id = coordinator.entry.entry_id async def async_alarm_disarm(self, code: str | None = None) -> None: diff --git a/homeassistant/components/yale_smart_alarm/binary_sensor.py b/homeassistant/components/yale_smart_alarm/binary_sensor.py index 566dbed8c33..635fb3fad60 100644 --- a/homeassistant/components/yale_smart_alarm/binary_sensor.py +++ b/homeassistant/components/yale_smart_alarm/binary_sensor.py @@ -7,7 +7,6 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -21,7 +20,7 @@ SENSOR_TYPES = ( key="acfail", device_class=BinarySensorDeviceClass.PROBLEM, entity_category=EntityCategory.DIAGNOSTIC, - name="Power Loss", + name="Power loss", ), BinarySensorEntityDescription( key="battery", @@ -85,9 +84,6 @@ class YaleProblemSensor(YaleAlarmEntity, BinarySensorEntity): """Initiate Yale Problem Sensor.""" super().__init__(coordinator) self.entity_description = entity_description - self._attr_name = ( - f"{coordinator.entry.data[CONF_NAME]} {entity_description.name}" - ) self._attr_unique_id = f"{coordinator.entry.entry_id}-{entity_description.key}" @property diff --git a/homeassistant/components/yale_smart_alarm/button.py b/homeassistant/components/yale_smart_alarm/button.py index cd312e79ceb..d8601ec85f9 100644 --- a/homeassistant/components/yale_smart_alarm/button.py +++ b/homeassistant/components/yale_smart_alarm/button.py @@ -5,7 +5,6 @@ from typing import TYPE_CHECKING from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -14,7 +13,11 @@ from .coordinator import YaleDataUpdateCoordinator from .entity import YaleAlarmEntity BUTTON_TYPES = ( - ButtonEntityDescription(key="panic", name="Panic Button", icon="mdi:alarm-light"), + ButtonEntityDescription( + key="panic", + name="Panic button", + icon="mdi:alarm-light", + ), ) @@ -47,7 +50,6 @@ class YalePanicButton(YaleAlarmEntity, ButtonEntity): """Initialize the plug switch.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = f"{coordinator.entry.data[CONF_NAME]} {description.name}" self._attr_unique_id = f"yale_smart_alarm-{description.key}" async def async_press(self) -> None: diff --git a/homeassistant/components/yale_smart_alarm/entity.py b/homeassistant/components/yale_smart_alarm/entity.py index b9a832f88e8..86b5839b51f 100644 --- a/homeassistant/components/yale_smart_alarm/entity.py +++ b/homeassistant/components/yale_smart_alarm/entity.py @@ -12,13 +12,14 @@ from .coordinator import YaleDataUpdateCoordinator class YaleEntity(CoordinatorEntity[YaleDataUpdateCoordinator], Entity): """Base implementation for Yale device.""" + _attr_has_entity_name = True + def __init__(self, coordinator: YaleDataUpdateCoordinator, data: dict) -> None: """Initialize an Yale device.""" super().__init__(coordinator) - self._attr_name: str = data["name"] self._attr_unique_id: str = data["address"] self._attr_device_info: DeviceInfo = DeviceInfo( - name=self._attr_name, + name=data["name"], manufacturer=MANUFACTURER, model=MODEL, identifiers={(DOMAIN, data["address"])}, @@ -29,6 +30,8 @@ class YaleEntity(CoordinatorEntity[YaleDataUpdateCoordinator], Entity): class YaleAlarmEntity(CoordinatorEntity[YaleDataUpdateCoordinator], Entity): """Base implementation for Yale Alarm device.""" + _attr_has_entity_name = True + def __init__(self, coordinator: YaleDataUpdateCoordinator) -> None: """Initialize an Yale device.""" super().__init__(coordinator) From 3f3ed3a2c5927a5b1230f1333eac80cec2437a06 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Jul 2022 10:52:40 +0200 Subject: [PATCH 2608/3516] Add multi-factor authentication support to Verisure (#75113) Co-authored-by: Paulus Schoutsen --- homeassistant/components/verisure/__init__.py | 14 +- .../components/verisure/config_flow.py | 142 +++++++- .../components/verisure/coordinator.py | 4 +- .../components/verisure/manifest.json | 2 +- .../components/verisure/strings.json | 15 +- .../components/verisure/translations/en.json | 15 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/verisure/test_config_flow.py | 312 +++++++++++++++++- 9 files changed, 479 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 9a64061554d..9ad8db08d59 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -3,9 +3,10 @@ from __future__ import annotations from contextlib import suppress import os +from pathlib import Path from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.const import CONF_EMAIL, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed import homeassistant.helpers.config_validation as cv @@ -28,6 +29,8 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Verisure from a config entry.""" + await hass.async_add_executor_job(migrate_cookie_files, hass, entry) + coordinator = VerisureDataUpdateCoordinator(hass, entry=entry) if not await coordinator.async_login(): @@ -64,3 +67,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: del hass.data[DOMAIN] return True + + +def migrate_cookie_files(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Migrate old cookie file to new location.""" + cookie_file = Path(hass.config.path(STORAGE_DIR, f"verisure_{entry.unique_id}")) + if cookie_file.exists(): + cookie_file.rename( + hass.config.path(STORAGE_DIR, f"verisure_{entry.data[CONF_EMAIL]}") + ) diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py index 119a9250736..d53c7c9ed66 100644 --- a/homeassistant/components/verisure/config_flow.py +++ b/homeassistant/components/verisure/config_flow.py @@ -13,9 +13,10 @@ from verisure import ( import voluptuous as vol from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.const import CONF_CODE, CONF_EMAIL, CONF_PASSWORD from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.storage import STORAGE_DIR from .const import ( CONF_GIID, @@ -53,13 +54,34 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): self.email = user_input[CONF_EMAIL] self.password = user_input[CONF_PASSWORD] self.verisure = Verisure( - username=user_input[CONF_EMAIL], password=user_input[CONF_PASSWORD] + username=self.email, + password=self.password, + cookieFileName=self.hass.config.path( + STORAGE_DIR, f"verisure_{user_input[CONF_EMAIL]}" + ), ) + try: await self.hass.async_add_executor_job(self.verisure.login) except VerisureLoginError as ex: - LOGGER.debug("Could not log in to Verisure, %s", ex) - errors["base"] = "invalid_auth" + if "Multifactor authentication enabled" in str(ex): + try: + await self.hass.async_add_executor_job(self.verisure.login_mfa) + except ( + VerisureLoginError, + VerisureError, + VerisureResponseError, + ) as mfa_ex: + LOGGER.debug( + "Unexpected response from Verisure during MFA set up, %s", + mfa_ex, + ) + errors["base"] = "unknown_mfa" + else: + return await self.async_step_mfa() + else: + LOGGER.debug("Could not log in to Verisure, %s", ex) + errors["base"] = "invalid_auth" except (VerisureError, VerisureResponseError) as ex: LOGGER.debug("Unexpected response from Verisure, %s", ex) errors["base"] = "unknown" @@ -77,6 +99,39 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_mfa( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle multifactor authentication step.""" + errors: dict[str, str] = {} + + if user_input is not None: + try: + await self.hass.async_add_executor_job( + self.verisure.mfa_validate, user_input[CONF_CODE], True + ) + await self.hass.async_add_executor_job(self.verisure.login) + except VerisureLoginError as ex: + LOGGER.debug("Could not log in to Verisure, %s", ex) + errors["base"] = "invalid_auth" + except (VerisureError, VerisureResponseError) as ex: + LOGGER.debug("Unexpected response from Verisure, %s", ex) + errors["base"] = "unknown" + else: + return await self.async_step_installation() + + return self.async_show_form( + step_id="mfa", + data_schema=vol.Schema( + { + vol.Required(CONF_CODE): vol.All( + vol.Coerce(str), vol.Length(min=6, max=6) + ) + } + ), + errors=errors, + ) + async def async_step_installation( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -123,14 +178,38 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: - verisure = Verisure( - username=user_input[CONF_EMAIL], password=user_input[CONF_PASSWORD] + self.email = user_input[CONF_EMAIL] + self.password = user_input[CONF_PASSWORD] + + self.verisure = Verisure( + username=self.email, + password=self.password, + cookieFileName=self.hass.config.path( + STORAGE_DIR, f"verisure-{user_input[CONF_EMAIL]}" + ), ) + try: - await self.hass.async_add_executor_job(verisure.login) + await self.hass.async_add_executor_job(self.verisure.login) except VerisureLoginError as ex: - LOGGER.debug("Could not log in to Verisure, %s", ex) - errors["base"] = "invalid_auth" + if "Multifactor authentication enabled" in str(ex): + try: + await self.hass.async_add_executor_job(self.verisure.login_mfa) + except ( + VerisureLoginError, + VerisureError, + VerisureResponseError, + ) as mfa_ex: + LOGGER.debug( + "Unexpected response from Verisure during MFA set up, %s", + mfa_ex, + ) + errors["base"] = "unknown_mfa" + else: + return await self.async_step_reauth_mfa() + else: + LOGGER.debug("Could not log in to Verisure, %s", ex) + errors["base"] = "invalid_auth" except (VerisureError, VerisureResponseError) as ex: LOGGER.debug("Unexpected response from Verisure, %s", ex) errors["base"] = "unknown" @@ -160,6 +239,51 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) + async def async_step_reauth_mfa( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle multifactor authentication step during re-authentication.""" + errors: dict[str, str] = {} + + if user_input is not None: + try: + await self.hass.async_add_executor_job( + self.verisure.mfa_validate, user_input[CONF_CODE], True + ) + await self.hass.async_add_executor_job(self.verisure.login) + except VerisureLoginError as ex: + LOGGER.debug("Could not log in to Verisure, %s", ex) + errors["base"] = "invalid_auth" + except (VerisureError, VerisureResponseError) as ex: + LOGGER.debug("Unexpected response from Verisure, %s", ex) + errors["base"] = "unknown" + else: + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_EMAIL: self.email, + CONF_PASSWORD: self.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_mfa", + data_schema=vol.Schema( + { + vol.Required(CONF_CODE): vol.All( + vol.Coerce(str), + vol.Length(min=6, max=6), + ) + } + ), + errors=errors, + ) + class VerisureOptionsFlowHandler(OptionsFlow): """Handle Verisure options.""" diff --git a/homeassistant/components/verisure/coordinator.py b/homeassistant/components/verisure/coordinator.py index 821e2830339..17cadb9598f 100644 --- a/homeassistant/components/verisure/coordinator.py +++ b/homeassistant/components/verisure/coordinator.py @@ -31,7 +31,9 @@ class VerisureDataUpdateCoordinator(DataUpdateCoordinator): self.verisure = Verisure( username=entry.data[CONF_EMAIL], password=entry.data[CONF_PASSWORD], - cookieFileName=hass.config.path(STORAGE_DIR, f"verisure_{entry.entry_id}"), + cookieFileName=hass.config.path( + STORAGE_DIR, f"verisure_{entry.data[CONF_EMAIL]}" + ), ) super().__init__( diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index c71be7ee4fc..820b8a20f14 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -2,7 +2,7 @@ "domain": "verisure", "name": "Verisure", "documentation": "https://www.home-assistant.io/integrations/verisure", - "requirements": ["vsure==1.7.3"], + "requirements": ["vsure==1.8.1"], "codeowners": ["@frenck"], "config_flow": true, "dhcp": [ diff --git a/homeassistant/components/verisure/strings.json b/homeassistant/components/verisure/strings.json index 5170bff5faa..c8326d73756 100644 --- a/homeassistant/components/verisure/strings.json +++ b/homeassistant/components/verisure/strings.json @@ -8,6 +8,12 @@ "password": "[%key:common::config_flow::data::password%]" } }, + "mfa": { + "data": { + "description": "Your account has 2-step verification enabled. Please enter the verification code Verisure sends to you.", + "code": "Verification Code" + } + }, "installation": { "description": "Home Assistant found multiple Verisure installations in your My Pages account. Please, select the installation to add to Home Assistant.", "data": { @@ -20,11 +26,18 @@ "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_mfa": { + "data": { + "description": "Your account has 2-step verification enabled. Please enter the verification code Verisure sends to you.", + "code": "Verification Code" + } } }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "unknown_mfa": "Unknown error occurred during MFA set up" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", diff --git a/homeassistant/components/verisure/translations/en.json b/homeassistant/components/verisure/translations/en.json index 57f73c3772b..34193ce0d09 100644 --- a/homeassistant/components/verisure/translations/en.json +++ b/homeassistant/components/verisure/translations/en.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "unknown_mfa": "Unknown error occurred during MFA set up" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant found multiple Verisure installations in your My Pages account. Please, select the installation to add to Home Assistant." }, + "mfa": { + "data": { + "code": "Verification Code", + "description": "Your account has 2-step verification enabled. Please enter the verification code Verisure sends to you." + } + }, "reauth_confirm": { "data": { "description": "Re-authenticate with your Verisure My Pages account.", @@ -22,6 +29,12 @@ "password": "Password" } }, + "reauth_mfa": { + "data": { + "code": "Verification Code", + "description": "Your account has 2-step verification enabled. Please enter the verification code Verisure sends to you." + } + }, "user": { "data": { "description": "Sign-in with your Verisure My Pages account.", diff --git a/requirements_all.txt b/requirements_all.txt index cf89a5622bd..0ef7f82524e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2402,7 +2402,7 @@ volkszaehler==0.3.2 volvooncall==0.10.0 # homeassistant.components.verisure -vsure==1.7.3 +vsure==1.8.1 # homeassistant.components.vasttrafik vtjp==0.1.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 009ccb04343..926e89c8f2a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1602,7 +1602,7 @@ venstarcolortouch==0.17 vilfo-api-client==0.3.2 # homeassistant.components.verisure -vsure==1.7.3 +vsure==1.8.1 # homeassistant.components.vulcan vulcan-api==2.1.1 diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index d957709c878..43adc91c38c 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -106,6 +106,129 @@ async def test_full_user_flow_multiple_installations( assert len(mock_setup_entry.mock_calls) == 1 +async def test_full_user_flow_single_installation_with_mfa( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, +) -> None: + """Test a full user initiated flow with a single installation and mfa.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("step_id") == "user" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result + + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "mfa" + assert "flow_id" in result2 + + mock_verisure_config_flow.login.side_effect = None + mock_verisure_config_flow.installations = [ + mock_verisure_config_flow.installations[0] + ] + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "code": "123456", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "ascending (12345th street)" + assert result3.get("data") == { + CONF_GIID: "12345", + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_PASSWORD: "SuperS3cr3t!", + } + + assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 1 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_full_user_flow_multiple_installations_with_mfa( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, +) -> None: + """Test a full user initiated configuration flow with a single installation.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("step_id") == "user" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result + + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "mfa" + assert "flow_id" in result2 + + mock_verisure_config_flow.login.side_effect = None + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "code": "123456", + }, + ) + await hass.async_block_till_done() + + assert result3.get("step_id") == "installation" + assert result3.get("type") == FlowResultType.FORM + assert result3.get("errors") is None + assert "flow_id" in result2 + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], {"giid": "54321"} + ) + await hass.async_block_till_done() + + assert result4.get("type") == FlowResultType.CREATE_ENTRY + assert result4.get("title") == "descending (54321th street)" + assert result4.get("data") == { + CONF_GIID: "54321", + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_PASSWORD: "SuperS3cr3t!", + } + + assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 1 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + @pytest.mark.parametrize( "side_effect,error", [ @@ -142,10 +265,10 @@ async def test_verisure_errors( assert result2.get("errors") == {"base": error} assert "flow_id" in result2 - mock_verisure_config_flow.login.side_effect = None - mock_verisure_config_flow.installations = [ - mock_verisure_config_flow.installations[0] - ] + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" + ) + mock_verisure_config_flow.login_mfa.side_effect = side_effect result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], @@ -156,15 +279,65 @@ async def test_verisure_errors( ) await hass.async_block_till_done() - assert result3.get("type") == FlowResultType.CREATE_ENTRY - assert result3.get("title") == "ascending (12345th street)" - assert result3.get("data") == { + mock_verisure_config_flow.login_mfa.side_effect = None + + assert result3.get("type") == FlowResultType.FORM + assert result3.get("step_id") == "user" + assert result3.get("errors") == {"base": "unknown_mfa"} + assert "flow_id" in result3 + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result4.get("type") == FlowResultType.FORM + assert result4.get("step_id") == "mfa" + assert "flow_id" in result4 + + mock_verisure_config_flow.mfa_validate.side_effect = side_effect + + result5 = await hass.config_entries.flow.async_configure( + result4["flow_id"], + { + "code": "123456", + }, + ) + assert result5.get("type") == FlowResultType.FORM + assert result5.get("step_id") == "mfa" + assert result5.get("errors") == {"base": error} + assert "flow_id" in result5 + + mock_verisure_config_flow.installations = [ + mock_verisure_config_flow.installations[0] + ] + + mock_verisure_config_flow.mfa_validate.side_effect = None + mock_verisure_config_flow.login.side_effect = None + + result6 = await hass.config_entries.flow.async_configure( + result5["flow_id"], + { + "code": "654321", + }, + ) + await hass.async_block_till_done() + + assert result6.get("type") == FlowResultType.CREATE_ENTRY + assert result6.get("title") == "ascending (12345th street)" + assert result6.get("data") == { CONF_GIID: "12345", CONF_EMAIL: "verisure_my_pages@example.com", CONF_PASSWORD: "SuperS3cr3t!", } - assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_verisure_config_flow.login.mock_calls) == 4 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 2 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 2 assert len(mock_setup_entry.mock_calls) == 1 @@ -226,6 +399,70 @@ async def test_reauth_flow( assert len(mock_setup_entry.mock_calls) == 1 +async def test_reauth_flow_with_mfa( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_verisure_config_flow: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test a reauthentication flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": mock_config_entry.unique_id, + "entry_id": mock_config_entry.entry_id, + }, + data=mock_config_entry.data, + ) + assert result.get("step_id") == "reauth_confirm" + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") == {} + assert "flow_id" in result + + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "correct horse battery staple!", + }, + ) + await hass.async_block_till_done() + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "reauth_mfa" + assert "flow_id" in result2 + + mock_verisure_config_flow.login.side_effect = None + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "code": "123456", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == FlowResultType.ABORT + assert result3.get("reason") == "reauth_successful" + assert mock_config_entry.data == { + CONF_GIID: "12345", + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_PASSWORD: "correct horse battery staple!", + } + + assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 1 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + @pytest.mark.parametrize( "side_effect,error", [ @@ -271,16 +508,63 @@ async def test_reauth_flow_errors( assert result2.get("errors") == {"base": error} assert "flow_id" in result2 + mock_verisure_config_flow.login.side_effect = VerisureLoginError( + "Multifactor authentication enabled, disable or create MFA cookie" + ) + mock_verisure_config_flow.login_mfa.side_effect = side_effect + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == FlowResultType.FORM + assert result3.get("step_id") == "reauth_confirm" + assert result3.get("errors") == {"base": "unknown_mfa"} + assert "flow_id" in result3 + + mock_verisure_config_flow.login_mfa.side_effect = None + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "SuperS3cr3t!", + }, + ) + await hass.async_block_till_done() + + assert result4.get("type") == FlowResultType.FORM + assert result4.get("step_id") == "reauth_mfa" + assert "flow_id" in result4 + + mock_verisure_config_flow.mfa_validate.side_effect = side_effect + + result5 = await hass.config_entries.flow.async_configure( + result4["flow_id"], + { + "code": "123456", + }, + ) + assert result5.get("type") == FlowResultType.FORM + assert result5.get("step_id") == "reauth_mfa" + assert result5.get("errors") == {"base": error} + assert "flow_id" in result5 + + mock_verisure_config_flow.mfa_validate.side_effect = None mock_verisure_config_flow.login.side_effect = None mock_verisure_config_flow.installations = [ mock_verisure_config_flow.installations[0] ] await hass.config_entries.flow.async_configure( - result2["flow_id"], + result5["flow_id"], { - "email": "verisure_my_pages@example.com", - "password": "correct horse battery staple", + "code": "654321", }, ) await hass.async_block_till_done() @@ -288,10 +572,12 @@ async def test_reauth_flow_errors( assert mock_config_entry.data == { CONF_GIID: "12345", CONF_EMAIL: "verisure_my_pages@example.com", - CONF_PASSWORD: "correct horse battery staple", + CONF_PASSWORD: "SuperS3cr3t!", } - assert len(mock_verisure_config_flow.login.mock_calls) == 2 + assert len(mock_verisure_config_flow.login.mock_calls) == 4 + assert len(mock_verisure_config_flow.login_mfa.mock_calls) == 2 + assert len(mock_verisure_config_flow.mfa_validate.mock_calls) == 2 assert len(mock_setup_entry.mock_calls) == 1 From 06e905054eefa2de3fc49bb8b44dbed2fe9eb598 Mon Sep 17 00:00:00 2001 From: apaperclip <67401560+apaperclip@users.noreply.github.com> Date: Fri, 15 Jul 2022 04:57:23 -0400 Subject: [PATCH 2609/3516] Fix aruba ssh host key algorithm (#75224) --- homeassistant/components/aruba/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/aruba/device_tracker.py b/homeassistant/components/aruba/device_tracker.py index ecdcc5f70f2..dc2d2fee8e9 100644 --- a/homeassistant/components/aruba/device_tracker.py +++ b/homeassistant/components/aruba/device_tracker.py @@ -87,7 +87,7 @@ class ArubaDeviceScanner(DeviceScanner): def get_aruba_data(self): """Retrieve data from Aruba Access Point and return parsed result.""" - connect = f"ssh {self.username}@{self.host}" + connect = f"ssh {self.username}@{self.host} -o HostKeyAlgorithms=ssh-rsa" ssh = pexpect.spawn(connect) query = ssh.expect( [ From 48f4b51a1d985b7429bd812991104bcb27f89828 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 15 Jul 2022 11:07:39 +0200 Subject: [PATCH 2610/3516] Migrate DNSIP to new entity naming style (#75197) --- homeassistant/components/dnsip/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index a770afe388d..93bf73f1b9d 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -52,6 +52,7 @@ class WanIpSensor(SensorEntity): """Implementation of a DNS IP sensor.""" _attr_icon = "mdi:web" + _attr_has_entity_name = True def __init__( self, @@ -61,7 +62,7 @@ class WanIpSensor(SensorEntity): ipv6: bool, ) -> None: """Initialize the DNS IP sensor.""" - self._attr_name = f"{name} IPv6" if ipv6 else name + self._attr_name = "IPv6" if ipv6 else None self._attr_unique_id = f"{hostname}_{ipv6}" self.hostname = hostname self.resolver = aiodns.DNSResolver() @@ -76,7 +77,7 @@ class WanIpSensor(SensorEntity): identifiers={(DOMAIN, f"{hostname}_{ipv6}")}, manufacturer="DNS", model=aiodns.__version__, - name=hostname, + name=name, ) async def async_update(self) -> None: From c6c063e8c52612a524e387400e6f4a919ac77a57 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 15 Jul 2022 12:38:20 +0200 Subject: [PATCH 2611/3516] Various cleanups in AdGuard Home (#75250) Co-authored-by: Martin Hjelmare --- .coveragerc | 1 + homeassistant/components/adguard/__init__.py | 101 +------------------ homeassistant/components/adguard/const.py | 3 + homeassistant/components/adguard/entity.py | 69 +++++++++++++ homeassistant/components/adguard/sensor.py | 14 +-- homeassistant/components/adguard/switch.py | 27 ++--- 6 files changed, 86 insertions(+), 129 deletions(-) create mode 100644 homeassistant/components/adguard/entity.py diff --git a/.coveragerc b/.coveragerc index 94b9eceb11b..cfc51dc700d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -23,6 +23,7 @@ omit = homeassistant/components/adax/climate.py homeassistant/components/adguard/__init__.py homeassistant/components/adguard/const.py + homeassistant/components/adguard/entity.py homeassistant/components/adguard/sensor.py homeassistant/components/adguard/switch.py homeassistant/components/ads/* diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index e27aef6389d..fbcbea61316 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -1,12 +1,10 @@ """Support for AdGuard Home.""" from __future__ import annotations -import logging - -from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError +from adguardhome import AdGuardHome, AdGuardHomeConnectionError import voluptuous as vol -from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -22,13 +20,10 @@ from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo, Entity from .const import ( CONF_FORCE, DATA_ADGUARD_CLIENT, - DATA_ADGUARD_VERSION, DOMAIN, SERVICE_ADD_URL, SERVICE_DISABLE_URL, @@ -37,8 +32,6 @@ from .const import ( SERVICE_REMOVE_URL, ) -_LOGGER = logging.getLogger(__name__) - SERVICE_URL_SCHEMA = vol.Schema({vol.Required(CONF_URL): cv.url}) SERVICE_ADD_URL_SCHEMA = vol.Schema( {vol.Required(CONF_NAME): cv.string, vol.Required(CONF_URL): cv.url} @@ -127,93 +120,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: del hass.data[DOMAIN] return unload_ok - - -class AdGuardHomeEntity(Entity): - """Defines a base AdGuard Home entity.""" - - _attr_has_entity_name = True - - def __init__( - self, - adguard: AdGuardHome, - entry: ConfigEntry, - name: str | None, - icon: str | None, - enabled_default: bool = True, - ) -> None: - """Initialize the AdGuard Home entity.""" - self._available = True - self._enabled_default = enabled_default - self._icon = icon - self._name = name - self._entry = entry - self.adguard = adguard - - @property - def name(self) -> str | None: - """Return the name of the entity.""" - return self._name - - @property - def icon(self) -> str | None: - """Return the mdi icon of the entity.""" - return self._icon - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._enabled_default - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - - async def async_update(self) -> None: - """Update AdGuard Home entity.""" - if not self.enabled: - return - - try: - await self._adguard_update() - self._available = True - except AdGuardHomeError: - if self._available: - _LOGGER.debug( - "An error occurred while updating AdGuard Home sensor", - exc_info=True, - ) - self._available = False - - async def _adguard_update(self) -> None: - """Update AdGuard Home entity.""" - raise NotImplementedError() - - -class AdGuardHomeDeviceEntity(AdGuardHomeEntity): - """Defines a AdGuard Home device entity.""" - - @property - def device_info(self) -> DeviceInfo: - """Return device information about this AdGuard Home instance.""" - if self._entry.source == SOURCE_HASSIO: - config_url = "homeassistant://hassio/ingress/a0d7b954_adguard" - else: - if self.adguard.tls: - config_url = f"https://{self.adguard.host}:{self.adguard.port}" - else: - config_url = f"http://{self.adguard.host}:{self.adguard.port}" - - return DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={ - (DOMAIN, self.adguard.host, self.adguard.port, self.adguard.base_path) # type: ignore[arg-type] - }, - manufacturer="AdGuard Team", - name="AdGuard Home", - sw_version=self.hass.data[DOMAIN][self._entry.entry_id].get( - DATA_ADGUARD_VERSION - ), - configuration_url=config_url, - ) diff --git a/homeassistant/components/adguard/const.py b/homeassistant/components/adguard/const.py index 8bfa5b49fc6..a4ccde68539 100644 --- a/homeassistant/components/adguard/const.py +++ b/homeassistant/components/adguard/const.py @@ -1,7 +1,10 @@ """Constants for the AdGuard Home integration.""" +import logging DOMAIN = "adguard" +LOGGER = logging.getLogger(__package__) + DATA_ADGUARD_CLIENT = "adguard_client" DATA_ADGUARD_VERSION = "adguard_version" diff --git a/homeassistant/components/adguard/entity.py b/homeassistant/components/adguard/entity.py new file mode 100644 index 00000000000..7d6bf099366 --- /dev/null +++ b/homeassistant/components/adguard/entity.py @@ -0,0 +1,69 @@ +"""AdGuard Home base entity.""" +from __future__ import annotations + +from adguardhome import AdGuardHome, AdGuardHomeError + +from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import DATA_ADGUARD_VERSION, DOMAIN, LOGGER + + +class AdGuardHomeEntity(Entity): + """Defines a base AdGuard Home entity.""" + + _attr_has_entity_name = True + _attr_available = True + + def __init__( + self, + adguard: AdGuardHome, + entry: ConfigEntry, + ) -> None: + """Initialize the AdGuard Home entity.""" + self._entry = entry + self.adguard = adguard + + async def async_update(self) -> None: + """Update AdGuard Home entity.""" + if not self.enabled: + return + + try: + await self._adguard_update() + self._attr_available = True + except AdGuardHomeError: + if self._attr_available: + LOGGER.debug( + "An error occurred while updating AdGuard Home sensor", + exc_info=True, + ) + self._attr_available = False + + async def _adguard_update(self) -> None: + """Update AdGuard Home entity.""" + raise NotImplementedError() + + @property + def device_info(self) -> DeviceInfo: + """Return device information about this AdGuard Home instance.""" + if self._entry.source == SOURCE_HASSIO: + config_url = "homeassistant://hassio/ingress/a0d7b954_adguard" + elif self.adguard.tls: + config_url = f"https://{self.adguard.host}:{self.adguard.port}" + else: + config_url = f"http://{self.adguard.host}:{self.adguard.port}" + + return DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={ + (DOMAIN, self.adguard.host, self.adguard.port, self.adguard.base_path) # type: ignore[arg-type] + }, + manufacturer="AdGuard Team", + name="AdGuard Home", + sw_version=self.hass.data[DOMAIN][self._entry.entry_id].get( + DATA_ADGUARD_VERSION + ), + configuration_url=config_url, + ) diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 19ed0b96801..07a483f03c4 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -15,8 +15,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AdGuardHomeDeviceEntity from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN +from .entity import AdGuardHomeEntity SCAN_INTERVAL = timedelta(seconds=300) PARALLEL_UPDATES = 4 @@ -118,7 +118,7 @@ async def async_setup_entry( ) -class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity): +class AdGuardHomeSensor(AdGuardHomeEntity, SensorEntity): """Defines a AdGuard Home sensor.""" entity_description: AdGuardHomeEntityDescription @@ -130,8 +130,8 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity): description: AdGuardHomeEntityDescription, ) -> None: """Initialize AdGuard Home sensor.""" + super().__init__(adguard, entry) self.entity_description = description - self._attr_unique_id = "_".join( [ DOMAIN, @@ -142,14 +142,6 @@ class AdGuardHomeSensor(AdGuardHomeDeviceEntity, SensorEntity): ] ) - super().__init__( - adguard, - entry, - description.name, - description.icon, - description.entity_registry_enabled_default, - ) - async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" value = await self.entity_description.value_fn(self.adguard)() diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index efbd5c7fa38..a359bf86c2d 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass from datetime import timedelta -import logging from typing import Any from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError @@ -15,10 +14,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import AdGuardHomeDeviceEntity -from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERSION, DOMAIN, LOGGER +from .entity import AdGuardHomeEntity SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 1 @@ -113,7 +110,7 @@ async def async_setup_entry( ) -class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): +class AdGuardHomeSwitch(AdGuardHomeEntity, SwitchEntity): """Defines a AdGuard Home switch.""" entity_description: AdGuardHomeSwitchEntityDescription @@ -125,35 +122,27 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): description: AdGuardHomeSwitchEntityDescription, ) -> None: """Initialize AdGuard Home switch.""" + super().__init__(adguard, entry) self.entity_description = description - self._attr_unique_id = "_".join( [DOMAIN, adguard.host, str(adguard.port), "switch", description.key] ) - super().__init__( - adguard, - entry, - description.name, - description.icon, - description.entity_registry_enabled_default, - ) - async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" try: await self.entity_description.turn_off_fn(self.adguard)() except AdGuardHomeError: - _LOGGER.error("An error occurred while turning off AdGuard Home switch") - self._available = False + LOGGER.error("An error occurred while turning off AdGuard Home switch") + self._attr_available = False async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" try: await self.entity_description.turn_on_fn(self.adguard)() except AdGuardHomeError: - _LOGGER.error("An error occurred while turning on AdGuard Home switch") - self._available = False + LOGGER.error("An error occurred while turning on AdGuard Home switch") + self._attr_available = False async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" From 97fd6699249115ee23b4ad4cadd9e1ec8d95ac49 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 15 Jul 2022 17:44:25 +0200 Subject: [PATCH 2612/3516] HomeKit Controller BLE Fixes (#75271) --- homeassistant/components/homekit_controller/config_flow.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index ac840eb0689..41094a65e00 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -6,6 +6,7 @@ import re from typing import Any import aiohomekit +from aiohomekit.controller.abstract import AbstractPairing from aiohomekit.exceptions import AuthenticationError from aiohomekit.model import Accessories, CharacteristicsTypes, ServicesTypes from aiohomekit.utils import domain_supported, domain_to_name @@ -468,13 +469,13 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): data_schema=vol.Schema(schema), ) - async def _entry_from_accessory(self, pairing): + async def _entry_from_accessory(self, pairing: AbstractPairing) -> FlowResult: """Return a config entry from an initialized bridge.""" # The bulk of the pairing record is stored on the config entry. # A specific exception is the 'accessories' key. This is more # volatile. We do cache it, but not against the config entry. # So copy the pairing data and mutate the copy. - pairing_data = pairing.pairing_data.copy() + pairing_data = pairing.pairing_data.copy() # type: ignore[attr-defined] # Use the accessories data from the pairing operation if it is # available. Otherwise request a fresh copy from the API. @@ -489,6 +490,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) name = accessory_info.value(CharacteristicsTypes.NAME, "") + await pairing.close() + return self.async_create_entry(title=name, data=pairing_data) From 2106c9f24723c422fd2d95b5d17e78c707e74e27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 15 Jul 2022 18:21:09 +0200 Subject: [PATCH 2613/3516] Fix delay adding entities in HKC (#75273) --- .../components/homekit_controller/__init__.py | 4 +++- .../components/homekit_controller/connection.py | 4 ++-- .../components/homekit_controller/device_trigger.py | 12 +++++++----- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 461d46eed1d..9175dca50bc 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -76,7 +76,9 @@ class HomeKitEntity(Entity): ) self._accessory.add_pollable_characteristics(self.pollable_characteristics) - self._accessory.add_watchable_characteristics(self.watchable_characteristics) + await self._accessory.add_watchable_characteristics( + self.watchable_characteristics + ) async def async_will_remove_from_hass(self) -> None: """Prepare to be removed from hass.""" diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index e1cbedb01e8..85e8e3987bd 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -145,12 +145,12 @@ class HKDevice: char for char in self.pollable_characteristics if char[0] != accessory_id ] - def add_watchable_characteristics( + async def add_watchable_characteristics( self, characteristics: list[tuple[int, int]] ) -> None: """Add (aid, iid) pairs that we need to poll.""" self.watchable_characteristics.extend(characteristics) - self.hass.async_create_task(self.pairing.subscribe(characteristics)) + await self.pairing.subscribe(characteristics) def remove_watchable_characteristics(self, accessory_id: int) -> None: """Remove all pollable characteristics by accessory id.""" diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index dcac7238c8e..49924b30c57 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -1,7 +1,7 @@ """Provides device automations for homekit devices.""" from __future__ import annotations -from collections.abc import Generator +from collections.abc import Callable, Generator from typing import TYPE_CHECKING, Any from aiohomekit.model.characteristics import CharacteristicsTypes @@ -59,7 +59,9 @@ HK_TO_HA_INPUT_EVENT_VALUES = { class TriggerSource: """Represents a stateless source of event data from HomeKit.""" - def __init__(self, connection, aid, triggers): + def __init__( + self, connection: HKDevice, aid: int, triggers: list[dict[str, Any]] + ) -> None: """Initialize a set of triggers for a device.""" self._hass = connection.hass self._connection = connection @@ -67,7 +69,7 @@ class TriggerSource: self._triggers: dict[tuple[str, str], dict[str, Any]] = {} for trigger in triggers: self._triggers[(trigger["type"], trigger["subtype"])] = trigger - self._callbacks = {} + self._callbacks: dict[int, list[Callable[[Any], None]]] = {} def fire(self, iid, value): """Process events that have been received from a HomeKit accessory.""" @@ -97,7 +99,7 @@ class TriggerSource: trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]] iid = trigger["characteristic"] - self._connection.add_watchable_characteristics([(self._aid, iid)]) + await self._connection.add_watchable_characteristics([(self._aid, iid)]) self._callbacks.setdefault(iid, []).append(event_handler) def async_remove_handler(): @@ -196,7 +198,7 @@ TRIGGER_FINDERS = { async def async_setup_triggers_for_entry(hass: HomeAssistant, config_entry): """Triggers aren't entities as they have no state, but we still need to set them up for a config entry.""" hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] + conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] @callback def async_add_service(service): From dbcd98d0294750c7ef946d7df0379ecd3f204caf Mon Sep 17 00:00:00 2001 From: Khole Date: Fri, 15 Jul 2022 22:03:22 +0100 Subject: [PATCH 2614/3516] Add fixes for hive light (#75286) --- homeassistant/components/hive/light.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index c06237f3709..69345c430c7 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -44,13 +44,15 @@ class HiveDeviceLight(HiveEntity, LightEntity): super().__init__(hive, hive_device) if self.device["hiveType"] == "warmwhitelight": self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} + self._attr_color_mode = ColorMode.BRIGHTNESS elif self.device["hiveType"] == "tuneablelight": self._attr_supported_color_modes = {ColorMode.COLOR_TEMP} + self._attr_color_mode = ColorMode.COLOR_TEMP elif self.device["hiveType"] == "colourtuneablelight": self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} - self._attr_min_mireds = self.device.get("min_mireds") - self._attr_max_mireds = self.device.get("max_mireds") + self._attr_min_mireds = 153 + self._attr_max_mireds = 370 @refresh_system async def async_turn_on(self, **kwargs): @@ -94,6 +96,13 @@ class HiveDeviceLight(HiveEntity, LightEntity): if self._attr_available: self._attr_is_on = self.device["status"]["state"] self._attr_brightness = self.device["status"]["brightness"] + if self.device["hiveType"] == "tuneablelight": + self._attr_color_temp = self.device["status"].get("color_temp") if self.device["hiveType"] == "colourtuneablelight": - rgb = self.device["status"]["hs_color"] - self._attr_hs_color = color_util.color_RGB_to_hs(*rgb) + if self.device["status"]["mode"] == "COLOUR": + rgb = self.device["status"]["hs_color"] + self._attr_hs_color = color_util.color_RGB_to_hs(*rgb) + self._attr_color_mode = ColorMode.HS + else: + self._attr_color_temp = self.device["status"].get("color_temp") + self._attr_color_mode = ColorMode.COLOR_TEMP From 1ce47147226da8e6bc392a4113e2b3a6d46c2839 Mon Sep 17 00:00:00 2001 From: clayton craft Date: Fri, 15 Jul 2022 15:38:06 -0700 Subject: [PATCH 2615/3516] Bump venstarcolortouch to 0.18 (#75237) venstarcolortouch: bump to 0.18 --- homeassistant/components/venstar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index 2f3331af6e2..4a6eea28e24 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -3,7 +3,7 @@ "name": "Venstar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/venstar", - "requirements": ["venstarcolortouch==0.17"], + "requirements": ["venstarcolortouch==0.18"], "codeowners": ["@garbled1"], "iot_class": "local_polling", "loggers": ["venstarcolortouch"] diff --git a/requirements_all.txt b/requirements_all.txt index 0ef7f82524e..94759b214d4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2390,7 +2390,7 @@ vehicle==0.4.0 velbus-aio==2022.6.2 # homeassistant.components.venstar -venstarcolortouch==0.17 +venstarcolortouch==0.18 # homeassistant.components.vilfo vilfo-api-client==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 926e89c8f2a..397cc15e05c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1596,7 +1596,7 @@ vehicle==0.4.0 velbus-aio==2022.6.2 # homeassistant.components.venstar -venstarcolortouch==0.17 +venstarcolortouch==0.18 # homeassistant.components.vilfo vilfo-api-client==0.3.2 From d0c4d39ae20841b441b497cdd42e6b48f6162c2c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 16 Jul 2022 00:26:06 +0000 Subject: [PATCH 2616/3516] [ci skip] Translation update --- .../components/derivative/translations/ru.json | 4 ++-- .../here_travel_time/translations/nl.json | 6 ++++++ .../components/nina/translations/nl.json | 6 ++++++ .../components/rhasspy/translations/nl.json | 12 ++++++++++++ .../components/verisure/translations/ca.json | 15 ++++++++++++++- .../components/verisure/translations/de.json | 15 ++++++++++++++- .../components/verisure/translations/et.json | 15 ++++++++++++++- .../components/verisure/translations/fr.json | 15 ++++++++++++++- .../components/verisure/translations/ja.json | 15 ++++++++++++++- .../components/verisure/translations/nl.json | 10 ++++++++++ .../components/verisure/translations/pt-BR.json | 15 ++++++++++++++- .../components/verisure/translations/zh-Hant.json | 15 ++++++++++++++- .../components/withings/translations/nl.json | 3 +++ 13 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/rhasspy/translations/nl.json diff --git a/homeassistant/components/derivative/translations/ru.json b/homeassistant/components/derivative/translations/ru.json index 6155d64301a..bef5b20efdd 100644 --- a/homeassistant/components/derivative/translations/ru.json +++ b/homeassistant/components/derivative/translations/ru.json @@ -12,7 +12,7 @@ }, "data_description": { "round": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439.", - "time_window": "\u0415\u0441\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e, \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0432\u043d\u043e \u0432\u0437\u0432\u0435\u0448\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043a\u043e\u043b\u044c\u0437\u044f\u0449\u0435\u043c\u0443 \u0441\u0440\u0435\u0434\u043d\u0435\u043c\u0443 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u044b\u0445 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u044d\u0442\u043e\u0433\u043e \u043e\u043a\u043d\u0430.", + "time_window": "\u0415\u0441\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e, \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0432\u043d\u043e \u0432\u0437\u0432\u0435\u0448\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043a\u043e\u043b\u044c\u0437\u044f\u0449\u0435\u043c\u0443 \u0441\u0440\u0435\u0434\u043d\u0435\u043c\u0443 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u044b\u0445 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u044d\u0442\u043e\u0433\u043e \u043e\u043a\u043d\u0430.", "unit_prefix": "\u0414\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0438 \u0441 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u043c \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \u043c\u0435\u0442\u0440\u0438\u043a\u0438 \u0438 \u0435\u0434\u0438\u043d\u0438\u0446\u0435\u0439 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u043e\u0439." }, "description": "\u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u0441\u0435\u043d\u0441\u043e\u0440, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u0447\u0438\u0442\u0430\u0435\u0442 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u0443\u044e \u0441\u0435\u043d\u0441\u043e\u0440\u0430.", @@ -33,7 +33,7 @@ }, "data_description": { "round": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439.", - "time_window": "\u0415\u0441\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e, \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0432\u043d\u043e \u0432\u0437\u0432\u0435\u0448\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043a\u043e\u043b\u044c\u0437\u044f\u0449\u0435\u043c\u0443 \u0441\u0440\u0435\u0434\u043d\u0435\u043c\u0443 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u044b\u0445 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u044d\u0442\u043e\u0433\u043e \u043e\u043a\u043d\u0430.", + "time_window": "\u0415\u0441\u043b\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e, \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0432\u043d\u043e \u0432\u0437\u0432\u0435\u0448\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u0441\u043a\u043e\u043b\u044c\u0437\u044f\u0449\u0435\u043c\u0443 \u0441\u0440\u0435\u0434\u043d\u0435\u043c\u0443 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u044b\u0445 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u044d\u0442\u043e\u0433\u043e \u043e\u043a\u043d\u0430.", "unit_prefix": "\u0414\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043c\u0430\u0441\u0448\u0442\u0430\u0431\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0438 \u0441 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u043c \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \u043c\u0435\u0442\u0440\u0438\u043a\u0438 \u0438 \u0435\u0434\u0438\u043d\u0438\u0446\u0435\u0439 \u0432\u0440\u0435\u043c\u0435\u043d\u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u043e\u0439.." } } diff --git a/homeassistant/components/here_travel_time/translations/nl.json b/homeassistant/components/here_travel_time/translations/nl.json index cbec21776e5..7d3438901cc 100644 --- a/homeassistant/components/here_travel_time/translations/nl.json +++ b/homeassistant/components/here_travel_time/translations/nl.json @@ -39,6 +39,12 @@ }, "title": "Herkomst kiezen" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Een kaartlocatie gebruiken", + "origin_entity": "Een entiteit gebruiken" + } + }, "user": { "data": { "api_key": "API-sleutel", diff --git a/homeassistant/components/nina/translations/nl.json b/homeassistant/components/nina/translations/nl.json index 1b407576bbd..7c18eb1e6b6 100644 --- a/homeassistant/components/nina/translations/nl.json +++ b/homeassistant/components/nina/translations/nl.json @@ -31,6 +31,12 @@ }, "step": { "init": { + "data": { + "_i_to_l": "Stad/provincie (I-L)", + "_m_to_q": "Stad/provincie (M-Q)", + "_r_to_u": "Stad/provincie (R-U)", + "_v_to_z": "Stad/provincie (V-Z)" + }, "title": "Opties" } } diff --git a/homeassistant/components/rhasspy/translations/nl.json b/homeassistant/components/rhasspy/translations/nl.json new file mode 100644 index 00000000000..7089a156d1b --- /dev/null +++ b/homeassistant/components/rhasspy/translations/nl.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." + }, + "step": { + "user": { + "description": "Wilt u Rhasspy-ondersteuning inschakelen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/ca.json b/homeassistant/components/verisure/translations/ca.json index 87e441fd937..36c76a15dcd 100644 --- a/homeassistant/components/verisure/translations/ca.json +++ b/homeassistant/components/verisure/translations/ca.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "unknown": "Error inesperat" + "unknown": "Error inesperat", + "unknown_mfa": "S'ha produ\u00eft un error desconegut durant la configuraci\u00f3 de l'MFA" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant ha trobat diverses instal\u00b7lacions Verisure al compte de My Pages. Selecciona la instal\u00b7laci\u00f3 a afegir a Home Assistant." }, + "mfa": { + "data": { + "code": "Codi de verificaci\u00f3", + "description": "El teu compte t\u00e9 activada la verificaci\u00f3 en dos passos. Introdueix el codi de verificaci\u00f3 que t'ha enviat Verisure." + } + }, "reauth_confirm": { "data": { "description": "Torna a autenticar-te amb el compte de Verisure My Pages.", @@ -22,6 +29,12 @@ "password": "Contrasenya" } }, + "reauth_mfa": { + "data": { + "code": "Codi de verificaci\u00f3", + "description": "El teu compte t\u00e9 activada la verificaci\u00f3 en dos passos. Introdueix el codi de verificaci\u00f3 que t'ha enviat Verisure." + } + }, "user": { "data": { "description": "Inicia sessi\u00f3 amb el compte de Verisure My Pages.", diff --git a/homeassistant/components/verisure/translations/de.json b/homeassistant/components/verisure/translations/de.json index 3eaf6ff04f6..b5e544e579b 100644 --- a/homeassistant/components/verisure/translations/de.json +++ b/homeassistant/components/verisure/translations/de.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung", - "unknown": "Unerwarteter Fehler" + "unknown": "Unerwarteter Fehler", + "unknown_mfa": "Beim MFA-Setup ist ein unbekannter Fehler aufgetreten" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant hat mehrere Verisure-Installationen in deinen My Pages-Konto gefunden. Bitte w\u00e4hle die Installation aus, die du zu Home Assistant hinzuf\u00fcgen m\u00f6chtest." }, + "mfa": { + "data": { + "code": "Verifizierungs-Code", + "description": "Dein Konto hat die 2-Schritt-Verifizierung aktiviert. Bitte gib den Verifizierungscode ein, den du von Verisure erhalten hast." + } + }, "reauth_confirm": { "data": { "description": "Authentifiziere dich erneut mit deinem Verisure My Pages-Konto.", @@ -22,6 +29,12 @@ "password": "Passwort" } }, + "reauth_mfa": { + "data": { + "code": "Verifizierungs-Code", + "description": "Dein Konto hat die 2-Schritt-Verifizierung aktiviert. Bitte gib den Verifizierungscode ein, den du von Verisure erhalten hast." + } + }, "user": { "data": { "description": "Melde dich mit deinen Verisure My Pages-Konto an.", diff --git a/homeassistant/components/verisure/translations/et.json b/homeassistant/components/verisure/translations/et.json index 78a2c987ef2..25fb59a8aad 100644 --- a/homeassistant/components/verisure/translations/et.json +++ b/homeassistant/components/verisure/translations/et.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Vigane autentimine", - "unknown": "Ootamatu t\u00f5rge" + "unknown": "Ootamatu t\u00f5rge", + "unknown_mfa": "MFA h\u00e4\u00e4lestamisel ilmnes tundmatu t\u00f5rge" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant leidis kontolt Minu lehed mitu Verisure paigaldust. Vali Home Assistantile lisatav paigaldus." }, + "mfa": { + "data": { + "code": "Kinnituskood", + "description": "Kontol on lubatud 2-astmeline kontroll. Palun sisesta Verisure'i poolt saadetud kinnituskood." + } + }, "reauth_confirm": { "data": { "description": "Taastuvasta oma Verisure My Pages'i kontoga.", @@ -22,6 +29,12 @@ "password": "Salas\u00f5na" } }, + "reauth_mfa": { + "data": { + "code": "Kinnituskood", + "description": "Kontol on lubatud 2-astmeline kontroll. Palun sisesta Verisure'i poolt saadetud kinnituskood." + } + }, "user": { "data": { "description": "Logi sisse oma Verisure My Pages kontoga.", diff --git a/homeassistant/components/verisure/translations/fr.json b/homeassistant/components/verisure/translations/fr.json index f3c26abb74a..0848eb6022b 100644 --- a/homeassistant/components/verisure/translations/fr.json +++ b/homeassistant/components/verisure/translations/fr.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Authentification non valide", - "unknown": "Erreur inattendue" + "unknown": "Erreur inattendue", + "unknown_mfa": "Une erreur inconnue est survenue lors de la configuration de l'authentification multifacteur" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant a trouv\u00e9 plusieurs installations Verisure dans votre compte My Pages. Veuillez s\u00e9lectionner l'installation \u00e0 ajouter \u00e0 Home Assistant." }, + "mfa": { + "data": { + "code": "Code de v\u00e9rification", + "description": "La v\u00e9rification en deux \u00e9tapes est activ\u00e9e sur votre compte. Veuillez saisir le code de v\u00e9rification envoy\u00e9 par Verisure." + } + }, "reauth_confirm": { "data": { "description": "R\u00e9-authentifiez-vous avec votre compte Verisure My Pages.", @@ -22,6 +29,12 @@ "password": "Mot de passe" } }, + "reauth_mfa": { + "data": { + "code": "Code de v\u00e9rification", + "description": "La v\u00e9rification en deux \u00e9tapes est activ\u00e9e sur votre compte. Veuillez saisir le code de v\u00e9rification envoy\u00e9 par Verisure." + } + }, "user": { "data": { "description": "Connectez-vous avec votre compte Verisure My Pages.", diff --git a/homeassistant/components/verisure/translations/ja.json b/homeassistant/components/verisure/translations/ja.json index 5bd85b67e34..9aa412a0214 100644 --- a/homeassistant/components/verisure/translations/ja.json +++ b/homeassistant/components/verisure/translations/ja.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", + "unknown_mfa": "MFA\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u4e2d\u306b\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant\u306f\u3001\u30de\u30a4\u30da\u30fc\u30b8\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u8907\u6570\u306eVerisure\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u691c\u51fa\u3057\u307e\u3057\u305f\u3002Home Assistant\u306b\u8ffd\u52a0\u3059\u308b\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, + "mfa": { + "data": { + "code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9", + "description": "\u30a2\u30ab\u30a6\u30f3\u30c8\u30672\u6bb5\u968e\u8a8d\u8a3c\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u3059\u3002Verisure\u304b\u3089\u9001\u4fe1\u3055\u308c\u305f\u78ba\u8a8d\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + }, "reauth_confirm": { "data": { "description": "Verisure MyPages\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u518d\u8a8d\u8a3c\u3057\u307e\u3059\u3002", @@ -22,6 +29,12 @@ "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" } }, + "reauth_mfa": { + "data": { + "code": "\u78ba\u8a8d\u30b3\u30fc\u30c9", + "description": "\u30a2\u30ab\u30a6\u30f3\u30c8\u30672\u6bb5\u968e\u8a8d\u8a3c\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u3059\u3002Verisure\u304b\u3089\u9001\u4fe1\u3055\u308c\u305f\u78ba\u8a8d\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + }, "user": { "data": { "description": "Verisure My Pages\u30a2\u30ab\u30a6\u30f3\u30c8\u3067\u30b5\u30a4\u30f3\u30a4\u30f3\u3057\u307e\u3059\u3002", diff --git a/homeassistant/components/verisure/translations/nl.json b/homeassistant/components/verisure/translations/nl.json index 1a23da57319..89dc4860e95 100644 --- a/homeassistant/components/verisure/translations/nl.json +++ b/homeassistant/components/verisure/translations/nl.json @@ -15,6 +15,11 @@ }, "description": "Home Assistant heeft meerdere Verisure-installaties gevonden in uw My Pages-account. Selecteer de installatie om toe te voegen aan Home Assistant." }, + "mfa": { + "data": { + "code": "Verificatiecode" + } + }, "reauth_confirm": { "data": { "description": "Verifieer opnieuw met uw Verisure My Pages-account.", @@ -22,6 +27,11 @@ "password": "Wachtwoord" } }, + "reauth_mfa": { + "data": { + "code": "Verificatiecode" + } + }, "user": { "data": { "description": "Aanmelden met Verisure My Pages-account.", diff --git a/homeassistant/components/verisure/translations/pt-BR.json b/homeassistant/components/verisure/translations/pt-BR.json index 1fc733bfd55..9d42afd0e30 100644 --- a/homeassistant/components/verisure/translations/pt-BR.json +++ b/homeassistant/components/verisure/translations/pt-BR.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "unknown": "Erro inesperado" + "unknown": "Erro inesperado", + "unknown_mfa": "Ocorreu um erro desconhecido durante a configura\u00e7\u00e3o da MFA" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "O Home Assistant encontrou v\u00e1rias instala\u00e7\u00f5es da Verisure na sua conta do My Pages. Por favor, selecione a instala\u00e7\u00e3o para adicionar ao Home Assistant." }, + "mfa": { + "data": { + "code": "C\u00f3digo de verifica\u00e7\u00e3o", + "description": "Sua conta tem a verifica\u00e7\u00e3o em duas etapas ativada. Insira o c\u00f3digo de verifica\u00e7\u00e3o que a Verisure envia para voc\u00ea." + } + }, "reauth_confirm": { "data": { "description": "Re-autentique com sua conta Verisure My Pages.", @@ -22,6 +29,12 @@ "password": "Senha" } }, + "reauth_mfa": { + "data": { + "code": "C\u00f3digo de verifica\u00e7\u00e3o", + "description": "Sua conta tem a verifica\u00e7\u00e3o em duas etapas ativada. Insira o c\u00f3digo de verifica\u00e7\u00e3o que a Verisure envia para voc\u00ea." + } + }, "user": { "data": { "description": "Fa\u00e7a login com sua conta Verisure My Pages.", diff --git a/homeassistant/components/verisure/translations/zh-Hant.json b/homeassistant/components/verisure/translations/zh-Hant.json index 29410e390fe..53eb4ab1eb7 100644 --- a/homeassistant/components/verisure/translations/zh-Hant.json +++ b/homeassistant/components/verisure/translations/zh-Hant.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "unknown_mfa": "\u65bc\u591a\u6b65\u9a5f\u8a8d\u8b49\u8a2d\u5b9a\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant \u65bc My Pages \u5e33\u865f\u4e2d\u627e\u5230\u591a\u500b Verisure \u5b89\u88dd\u3002\u8acb\u9078\u64c7\u6240\u8981\u65b0\u589e\u81f3 Home Assistant \u7684\u9805\u76ee\u3002" }, + "mfa": { + "data": { + "code": "\u9a57\u8b49\u78bc", + "description": "\u5e33\u865f\u5177\u6709\u5169\u6b65\u9a5f\u9a57\u8b49\u3001\u8acb\u8f38\u5165\u6240\u6536\u5230\u7684 Verisure \u9a57\u8b49\u78bc\u3002" + } + }, "reauth_confirm": { "data": { "description": "\u91cd\u65b0\u8a8d\u8b49 Verisure My Pages \u5e33\u865f\u3002", @@ -22,6 +29,12 @@ "password": "\u5bc6\u78bc" } }, + "reauth_mfa": { + "data": { + "code": "\u9a57\u8b49\u78bc", + "description": "\u5e33\u865f\u5177\u6709\u5169\u6b65\u9a5f\u9a57\u8b49\u3001\u8acb\u8f38\u5165\u6240\u6536\u5230\u7684 Verisure \u9a57\u8b49\u78bc\u3002" + } + }, "user": { "data": { "description": "\u4ee5 Verisure My Pages \u5e33\u865f\u767b\u5165", diff --git a/homeassistant/components/withings/translations/nl.json b/homeassistant/components/withings/translations/nl.json index cca755effbb..8e40180b75e 100644 --- a/homeassistant/components/withings/translations/nl.json +++ b/homeassistant/components/withings/translations/nl.json @@ -27,6 +27,9 @@ "reauth": { "description": "Het {profile} \" moet opnieuw worden geverifieerd om Withings-gegevens te blijven ontvangen.", "title": "Integratie herauthenticeren" + }, + "reauth_confirm": { + "title": "Integratie herauthenticeren" } } } From 027cdbdb3893f3341629b7394e8bd0e24c6cd325 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 16 Jul 2022 14:33:18 +0200 Subject: [PATCH 2617/3516] Fix Sensibo new entity naming style (#75307) --- homeassistant/components/sensibo/sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 7a17db85a5b..f21366c7aa6 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -240,7 +240,6 @@ class SensiboMotionSensor(SensiboMotionBaseEntity, SensorEntity): ) self.entity_description = entity_description self._attr_unique_id = f"{sensor_id}-{entity_description.key}" - self._attr_name = entity_description.name @property def native_unit_of_measurement(self) -> str | None: From 13cea26e7496ba671ad2bc56137fe59e8a463e20 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 16 Jul 2022 15:21:19 +0200 Subject: [PATCH 2618/3516] Migrate Tailscale to new entity naming style (#75018) --- homeassistant/components/tailscale/__init__.py | 6 +++--- homeassistant/components/tailscale/binary_sensor.py | 2 +- homeassistant/components/tailscale/sensor.py | 4 ++-- tests/components/tailscale/test_binary_sensor.py | 2 +- tests/components/tailscale/test_sensor.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/tailscale/__init__.py b/homeassistant/components/tailscale/__init__.py index 2a5fca43ef1..abc4c4ca399 100644 --- a/homeassistant/components/tailscale/__init__.py +++ b/homeassistant/components/tailscale/__init__.py @@ -41,6 +41,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class TailscaleEntity(CoordinatorEntity): """Defines a Tailscale base entity.""" + _attr_has_entity_name = True + def __init__( self, *, @@ -52,8 +54,6 @@ class TailscaleEntity(CoordinatorEntity): super().__init__(coordinator=coordinator) self.entity_description = description self.device_id = device.device_id - self.friendly_name = device.name.split(".")[0] - self._attr_name = f"{self.friendly_name} {description.name}" self._attr_unique_id = f"{device.device_id}_{description.key}" @property @@ -71,6 +71,6 @@ class TailscaleEntity(CoordinatorEntity): identifiers={(DOMAIN, device.device_id)}, manufacturer="Tailscale Inc.", model=device.os, - name=self.friendly_name, + name=device.name.split(".")[0], sw_version=device.client_version, ) diff --git a/homeassistant/components/tailscale/binary_sensor.py b/homeassistant/components/tailscale/binary_sensor.py index 2f97d307b15..94176916dec 100644 --- a/homeassistant/components/tailscale/binary_sensor.py +++ b/homeassistant/components/tailscale/binary_sensor.py @@ -44,7 +44,7 @@ BINARY_SENSORS: tuple[TailscaleBinarySensorEntityDescription, ...] = ( ), TailscaleBinarySensorEntityDescription( key="client_supports_hair_pinning", - name="Supports Hairpinning", + name="Supports hairpinning", icon="mdi:wan", entity_category=EntityCategory.DIAGNOSTIC, is_on_fn=lambda device: device.client_connectivity.client_supports.hair_pinning, diff --git a/homeassistant/components/tailscale/sensor.py b/homeassistant/components/tailscale/sensor.py index 07f7dbe91cc..13d8a6db0cf 100644 --- a/homeassistant/components/tailscale/sensor.py +++ b/homeassistant/components/tailscale/sensor.py @@ -45,14 +45,14 @@ SENSORS: tuple[TailscaleSensorEntityDescription, ...] = ( ), TailscaleSensorEntityDescription( key="ip", - name="IP Address", + name="IP address", icon="mdi:ip-network", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.addresses[0] if device.addresses else None, ), TailscaleSensorEntityDescription( key="last_seen", - name="Last Seen", + name="Last seen", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda device: device.last_seen, ), diff --git a/tests/components/tailscale/test_binary_sensor.py b/tests/components/tailscale/test_binary_sensor.py index feb34c6d8a3..864fb497134 100644 --- a/tests/components/tailscale/test_binary_sensor.py +++ b/tests/components/tailscale/test_binary_sensor.py @@ -43,7 +43,7 @@ async def test_tailscale_binary_sensors( assert state.state == STATE_OFF assert ( state.attributes.get(ATTR_FRIENDLY_NAME) - == "frencks-iphone Supports Hairpinning" + == "frencks-iphone Supports hairpinning" ) assert state.attributes.get(ATTR_ICON) == "mdi:wan" assert ATTR_DEVICE_CLASS not in state.attributes diff --git a/tests/components/tailscale/test_sensor.py b/tests/components/tailscale/test_sensor.py index 911de0eb64a..9f55bba2c70 100644 --- a/tests/components/tailscale/test_sensor.py +++ b/tests/components/tailscale/test_sensor.py @@ -35,7 +35,7 @@ async def test_tailscale_sensors( assert entry.unique_id == "123457_last_seen" assert entry.entity_category is None assert state.state == "2021-11-15T20:37:03+00:00" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router Last Seen" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router Last seen" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP assert ATTR_ICON not in state.attributes @@ -46,7 +46,7 @@ async def test_tailscale_sensors( assert entry.unique_id == "123457_ip" assert entry.entity_category == EntityCategory.DIAGNOSTIC assert state.state == "100.11.11.112" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router IP Address" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "router IP address" assert state.attributes.get(ATTR_ICON) == "mdi:ip-network" assert ATTR_DEVICE_CLASS not in state.attributes From 0f3cc4a4aa2ac7965f5a563b86d03c8eeb6125d2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 16 Jul 2022 15:25:07 +0200 Subject: [PATCH 2619/3516] Migrate GIOS to new entity naming style (#75051) Use new entity naming style --- homeassistant/components/gios/const.py | 64 ----------------- homeassistant/components/gios/model.py | 14 ---- homeassistant/components/gios/sensor.py | 96 +++++++++++++++++++++++-- 3 files changed, 89 insertions(+), 85 deletions(-) delete mode 100644 homeassistant/components/gios/model.py diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py index 858a756e3e3..895775495f9 100644 --- a/homeassistant/components/gios/const.py +++ b/homeassistant/components/gios/const.py @@ -4,15 +4,9 @@ from __future__ import annotations from datetime import timedelta from typing import Final -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass -from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - -from .model import GiosSensorEntityDescription - ATTRIBUTION: Final = "Data provided by GIOŚ" CONF_STATION_ID: Final = "station_id" -DEFAULT_NAME: Final = "GIOŚ" # Term of service GIOŚ allow downloading data no more than twice an hour. SCAN_INTERVAL: Final = timedelta(minutes=30) DOMAIN: Final = "gios" @@ -33,61 +27,3 @@ ATTR_PM10: Final = "pm10" ATTR_PM25: Final = "pm25" ATTR_SO2: Final = "so2" ATTR_AQI: Final = "aqi" - -SENSOR_TYPES: Final[tuple[GiosSensorEntityDescription, ...]] = ( - GiosSensorEntityDescription( - key=ATTR_AQI, - name="AQI", - device_class=SensorDeviceClass.AQI, - value=None, - ), - GiosSensorEntityDescription( - key=ATTR_C6H6, - name="C6H6", - icon="mdi:molecule", - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - GiosSensorEntityDescription( - key=ATTR_CO, - name="CO", - device_class=SensorDeviceClass.CO, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - GiosSensorEntityDescription( - key=ATTR_NO2, - name="NO2", - device_class=SensorDeviceClass.NITROGEN_DIOXIDE, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - GiosSensorEntityDescription( - key=ATTR_O3, - name="O3", - device_class=SensorDeviceClass.OZONE, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - GiosSensorEntityDescription( - key=ATTR_PM10, - name="PM10", - device_class=SensorDeviceClass.PM10, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - GiosSensorEntityDescription( - key=ATTR_PM25, - name="PM2.5", - device_class=SensorDeviceClass.PM25, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - GiosSensorEntityDescription( - key=ATTR_SO2, - name="SO2", - device_class=SensorDeviceClass.SULPHUR_DIOXIDE, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), -) diff --git a/homeassistant/components/gios/model.py b/homeassistant/components/gios/model.py deleted file mode 100644 index 0f5d992590b..00000000000 --- a/homeassistant/components/gios/model.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Type definitions for GIOS integration.""" -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass - -from homeassistant.components.sensor import SensorEntityDescription - - -@dataclass -class GiosSensorEntityDescription(SensorEntityDescription): - """Class describing GIOS sensor entities.""" - - value: Callable | None = round diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index 391976ad793..2d32b8261f3 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -1,12 +1,25 @@ """Support for the GIOS service.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass import logging from typing import Any, cast -from homeassistant.components.sensor import DOMAIN as PLATFORM, SensorEntity +from homeassistant.components.sensor import ( + DOMAIN as PLATFORM, + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_NAME, CONF_NAME +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_NAME, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONF_NAME, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import DeviceEntryType @@ -18,21 +31,90 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import GiosDataUpdateCoordinator from .const import ( ATTR_AQI, + ATTR_C6H6, + ATTR_CO, ATTR_INDEX, + ATTR_NO2, + ATTR_O3, + ATTR_PM10, ATTR_PM25, + ATTR_SO2, ATTR_STATION, ATTRIBUTION, - DEFAULT_NAME, DOMAIN, MANUFACTURER, - SENSOR_TYPES, URL, ) -from .model import GiosSensorEntityDescription _LOGGER = logging.getLogger(__name__) +@dataclass +class GiosSensorEntityDescription(SensorEntityDescription): + """Class describing GIOS sensor entities.""" + + value: Callable | None = round + + +SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = ( + GiosSensorEntityDescription( + key=ATTR_AQI, + name="AQI", + device_class=SensorDeviceClass.AQI, + value=None, + ), + GiosSensorEntityDescription( + key=ATTR_C6H6, + name="C6H6", + icon="mdi:molecule", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_CO, + name="CO", + device_class=SensorDeviceClass.CO, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_NO2, + name="NO2", + device_class=SensorDeviceClass.NITROGEN_DIOXIDE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_O3, + name="O3", + device_class=SensorDeviceClass.OZONE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_PM10, + name="PM10", + device_class=SensorDeviceClass.PM10, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_PM25, + name="PM2.5", + device_class=SensorDeviceClass.PM25, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + GiosSensorEntityDescription( + key=ATTR_SO2, + name="SO2", + device_class=SensorDeviceClass.SULPHUR_DIOXIDE, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -72,6 +154,7 @@ async def async_setup_entry( class GiosSensor(CoordinatorEntity[GiosDataUpdateCoordinator], SensorEntity): """Define an GIOS sensor.""" + _attr_has_entity_name = True entity_description: GiosSensorEntityDescription def __init__( @@ -86,10 +169,9 @@ class GiosSensor(CoordinatorEntity[GiosDataUpdateCoordinator], SensorEntity): entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, str(coordinator.gios.station_id))}, manufacturer=MANUFACTURER, - name=DEFAULT_NAME, + name=name, configuration_url=URL.format(station_id=coordinator.gios.station_id), ) - self._attr_name = f"{name} {description.name}" self._attr_unique_id = f"{coordinator.gios.station_id}-{description.key}" self._attrs: dict[str, Any] = { ATTR_ATTRIBUTION: ATTRIBUTION, From 8d88562d4025dd55309360d74a71ce23a6bdc052 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 16 Jul 2022 15:27:20 +0200 Subject: [PATCH 2620/3516] Migrate Uptime to new entity naming style (#75090) --- homeassistant/components/uptime/sensor.py | 9 ++++++++- tests/components/uptime/test_sensor.py | 11 ++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index 944f9b77de8..3f7b7f5da25 100644 --- a/homeassistant/components/uptime/sensor.py +++ b/homeassistant/components/uptime/sensor.py @@ -12,6 +12,8 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -58,10 +60,15 @@ class UptimeSensor(SensorEntity): """Representation of an uptime sensor.""" _attr_device_class = SensorDeviceClass.TIMESTAMP + _attr_has_entity_name = True _attr_should_poll = False def __init__(self, entry: ConfigEntry) -> None: """Initialize the uptime sensor.""" - self._attr_name = entry.title self._attr_native_value = dt_util.utcnow() self._attr_unique_id = entry.entry_id + self._attr_device_info = DeviceInfo( + name=entry.title, + identifiers={(DOMAIN, entry.entry_id)}, + entry_type=DeviceEntryType.SERVICE, + ) diff --git a/tests/components/uptime/test_sensor.py b/tests/components/uptime/test_sensor.py index e8d0306246f..053224c3b4f 100644 --- a/tests/components/uptime/test_sensor.py +++ b/tests/components/uptime/test_sensor.py @@ -2,9 +2,10 @@ import pytest from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.uptime.const import DOMAIN from homeassistant.const import ATTR_DEVICE_CLASS 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 tests.common import MockConfigEntry @@ -25,3 +26,11 @@ async def test_uptime_sensor( entry = entity_registry.async_get("sensor.uptime") assert entry assert entry.unique_id == init_integration.entry_id + + device_registry = dr.async_get(hass) + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, init_integration.entry_id)} + assert device_entry.name == init_integration.title + assert device_entry.entry_type == dr.DeviceEntryType.SERVICE From 393610c5348094ff571fecd50433d6f6efd300f0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 16 Jul 2022 15:28:13 +0200 Subject: [PATCH 2621/3516] Migrate Season to new entity naming style (#75088) --- homeassistant/components/season/sensor.py | 9 ++++++++- tests/components/season/test_sensor.py | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index 216475f0cdf..5fc161062bb 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -14,6 +14,8 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.dt import utcnow @@ -121,13 +123,18 @@ class SeasonSensorEntity(SensorEntity): """Representation of the current season.""" _attr_device_class = "season__season" + _attr_has_entity_name = True def __init__(self, entry: ConfigEntry, hemisphere: str) -> None: """Initialize the season.""" - self._attr_name = entry.title self._attr_unique_id = entry.entry_id self.hemisphere = hemisphere self.type = entry.data[CONF_TYPE] + self._attr_device_info = DeviceInfo( + name=entry.title, + identifiers={(DOMAIN, entry.entry_id)}, + entry_type=DeviceEntryType.SERVICE, + ) def update(self) -> None: """Update season.""" diff --git a/tests/components/season/test_sensor.py b/tests/components/season/test_sensor.py index 90d01106bba..0c2470edb7b 100644 --- a/tests/components/season/test_sensor.py +++ b/tests/components/season/test_sensor.py @@ -4,7 +4,11 @@ from datetime import datetime from freezegun import freeze_time import pytest -from homeassistant.components.season.const import TYPE_ASTRONOMICAL, TYPE_METEOROLOGICAL +from homeassistant.components.season.const import ( + DOMAIN, + TYPE_ASTRONOMICAL, + TYPE_METEOROLOGICAL, +) from homeassistant.components.season.sensor import ( STATE_AUTUMN, STATE_SPRING, @@ -13,7 +17,7 @@ from homeassistant.components.season.sensor import ( ) from homeassistant.const import CONF_TYPE, STATE_UNKNOWN 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 tests.common import MockConfigEntry @@ -123,6 +127,14 @@ async def test_season_southern_hemisphere( assert entry assert entry.unique_id == mock_config_entry.entry_id + device_registry = dr.async_get(hass) + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, mock_config_entry.entry_id)} + assert device_entry.name == "Season" + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE + async def test_season_equator( hass: HomeAssistant, From 952c90efcd5511c13936526259298e532aab1aa5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 16 Jul 2022 08:45:09 -0600 Subject: [PATCH 2622/3516] Bump simplisafe-python to 2022.07.0 (#75294) --- 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 a09c273076c..b6a139fba80 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==2022.06.1"], + "requirements": ["simplisafe-python==2022.07.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 94759b214d4..363cfb2a733 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2171,7 +2171,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.06.1 +simplisafe-python==2022.07.0 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 397cc15e05c..21072433cc8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1446,7 +1446,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.06.1 +simplisafe-python==2022.07.0 # homeassistant.components.slack slackclient==2.5.0 From c52d4c64bf2a65f13955e8a8e779e2fe6b058d34 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Sat, 16 Jul 2022 16:57:17 +0200 Subject: [PATCH 2623/3516] Bump bimmer_connected to 0.10.1 (#75287) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index b10d4842163..0381035a63e 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.9.6"], + "requirements": ["bimmer_connected==0.10.1"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 363cfb2a733..63fd2862faa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -396,7 +396,7 @@ beautifulsoup4==4.11.1 bellows==0.31.1 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.6 +bimmer_connected==0.10.1 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 21072433cc8..034ead5c35e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -314,7 +314,7 @@ beautifulsoup4==4.11.1 bellows==0.31.1 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.6 +bimmer_connected==0.10.1 # homeassistant.components.bluetooth bleak==0.14.3 From 686449cef605eabfa00a34c1c25defdd6d54c284 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Sat, 16 Jul 2022 17:00:42 +0200 Subject: [PATCH 2624/3516] Force `_attr_native_value` to metric in bmw_connected_drive (#75225) Co-authored-by: rikroe --- .../components/bmw_connected_drive/coordinator.py | 3 ++- homeassistant/components/bmw_connected_drive/sensor.py | 7 ++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/coordinator.py b/homeassistant/components/bmw_connected_drive/coordinator.py index e5a968b47fd..08e90d3c4e0 100644 --- a/homeassistant/components/bmw_connected_drive/coordinator.py +++ b/homeassistant/components/bmw_connected_drive/coordinator.py @@ -33,7 +33,8 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator): entry.data[CONF_PASSWORD], get_region_from_name(entry.data[CONF_REGION]), observer_position=GPSPosition(hass.config.latitude, hass.config.longitude), - use_metric_units=hass.config.units.is_metric, + # Force metric system as BMW API apparently only returns metric values now + use_metric_units=True, ) self.read_only = entry.options[CONF_READ_ONLY] self._entry = entry diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 9f19673c398..f1046881ed3 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -16,7 +16,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, LENGTH_MILES, PERCENTAGE, @@ -183,10 +182,8 @@ class BMWSensor(BMWBaseEntity, SensorEntity): 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 = description.unit_metric + # Force metric system as BMW API apparently only returns metric values now + self._attr_native_unit_of_measurement = description.unit_metric @callback def _handle_coordinator_update(self) -> None: From 20d70337d51c0608d46cdfe17e5d8bd79be33947 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 16 Jul 2022 17:07:16 +0200 Subject: [PATCH 2625/3516] Migrate Trafikverket Weatherstation to new entity naming style (#75211) --- homeassistant/components/trafikverket_weatherstation/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index c54c9f67388..68c47e9320c 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -165,6 +165,7 @@ class TrafikverketWeatherStation( entity_description: TrafikverketSensorEntityDescription _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True def __init__( self, @@ -176,7 +177,6 @@ class TrafikverketWeatherStation( """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = f"{sensor_station} {description.name}" self._attr_unique_id = f"{entry_id}_{description.key}" self._attr_device_info = DeviceInfo( entry_type=DeviceEntryType.SERVICE, From 859189421beb13bc787b61bc4b588f548ac3dbf8 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 16 Jul 2022 17:43:47 +0200 Subject: [PATCH 2626/3516] Migrate BraviaTV to new entity naming style (#75253) --- .coveragerc | 1 + homeassistant/components/braviatv/const.py | 1 - homeassistant/components/braviatv/entity.py | 29 ++++++++++++++++ .../components/braviatv/media_player.py | 31 +++-------------- homeassistant/components/braviatv/remote.py | 33 +++---------------- 5 files changed, 38 insertions(+), 57 deletions(-) create mode 100644 homeassistant/components/braviatv/entity.py diff --git a/.coveragerc b/.coveragerc index cfc51dc700d..0a2e8e57fa8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -137,6 +137,7 @@ omit = homeassistant/components/bosch_shc/switch.py homeassistant/components/braviatv/__init__.py homeassistant/components/braviatv/const.py + homeassistant/components/braviatv/entity.py homeassistant/components/braviatv/media_player.py homeassistant/components/braviatv/remote.py homeassistant/components/broadlink/__init__.py diff --git a/homeassistant/components/braviatv/const.py b/homeassistant/components/braviatv/const.py index 01746cbe963..4aa44992cbf 100644 --- a/homeassistant/components/braviatv/const.py +++ b/homeassistant/components/braviatv/const.py @@ -12,6 +12,5 @@ CONF_IGNORED_SOURCES: Final = "ignored_sources" BRAVIA_CONFIG_FILE: Final = "bravia.conf" CLIENTID_PREFIX: Final = "HomeAssistant" -DEFAULT_NAME: Final = f"{ATTR_MANUFACTURER} Bravia TV" DOMAIN: Final = "braviatv" NICKNAME: Final = "Home Assistant" diff --git a/homeassistant/components/braviatv/entity.py b/homeassistant/components/braviatv/entity.py new file mode 100644 index 00000000000..ad896ae8c5a --- /dev/null +++ b/homeassistant/components/braviatv/entity.py @@ -0,0 +1,29 @@ +"""A entity class for BraviaTV integration.""" +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import BraviaTVCoordinator +from .const import ATTR_MANUFACTURER, DOMAIN + + +class BraviaTVEntity(CoordinatorEntity[BraviaTVCoordinator]): + """BraviaTV entity class.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: BraviaTVCoordinator, + unique_id: str, + model: str, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + + self._attr_unique_id = unique_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer=ATTR_MANUFACTURER, + model=model, + name=f"{ATTR_MANUFACTURER} {model}", + ) diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 745325a4c39..5d812788563 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -9,12 +9,10 @@ from homeassistant.components.media_player import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING 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 . import BraviaTVCoordinator -from .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN +from .const import DOMAIN +from .entity import BraviaTVEntity async def async_setup_entry( @@ -27,19 +25,13 @@ 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)}, - manufacturer=ATTR_MANUFACTURER, - model=config_entry.title, - name=DEFAULT_NAME, - ) async_add_entities( - [BraviaTVMediaPlayer(coordinator, DEFAULT_NAME, unique_id, device_info)] + [BraviaTVMediaPlayer(coordinator, unique_id, config_entry.title)] ) -class BraviaTVMediaPlayer(CoordinatorEntity[BraviaTVCoordinator], MediaPlayerEntity): +class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): """Representation of a Bravia TV Media Player.""" _attr_device_class = MediaPlayerDeviceClass.TV @@ -57,21 +49,6 @@ class BraviaTVMediaPlayer(CoordinatorEntity[BraviaTVCoordinator], MediaPlayerEnt | MediaPlayerEntityFeature.STOP ) - def __init__( - self, - coordinator: BraviaTVCoordinator, - name: str, - unique_id: str, - device_info: DeviceInfo, - ) -> None: - """Initialize the entity.""" - - self._attr_device_info = device_info - self._attr_name = name - self._attr_unique_id = unique_id - - super().__init__(coordinator) - @property def state(self) -> str | None: """Return the state of the device.""" diff --git a/homeassistant/components/braviatv/remote.py b/homeassistant/components/braviatv/remote.py index 016f8363b09..f45b2d74004 100644 --- a/homeassistant/components/braviatv/remote.py +++ b/homeassistant/components/braviatv/remote.py @@ -7,12 +7,10 @@ from typing import Any from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity 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 . import BraviaTVCoordinator -from .const import ATTR_MANUFACTURER, DEFAULT_NAME, DOMAIN +from .const import DOMAIN +from .entity import BraviaTVEntity async def async_setup_entry( @@ -25,36 +23,13 @@ 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)}, - manufacturer=ATTR_MANUFACTURER, - model=config_entry.title, - name=DEFAULT_NAME, - ) - async_add_entities( - [BraviaTVRemote(coordinator, DEFAULT_NAME, unique_id, device_info)] - ) + async_add_entities([BraviaTVRemote(coordinator, unique_id, config_entry.title)]) -class BraviaTVRemote(CoordinatorEntity[BraviaTVCoordinator], RemoteEntity): +class BraviaTVRemote(BraviaTVEntity, RemoteEntity): """Representation of a Bravia TV Remote.""" - def __init__( - self, - coordinator: BraviaTVCoordinator, - name: str, - unique_id: str, - device_info: DeviceInfo, - ) -> None: - """Initialize the entity.""" - - self._attr_device_info = device_info - self._attr_name = name - self._attr_unique_id = unique_id - - super().__init__(coordinator) - @property def is_on(self) -> bool: """Return true if device is on.""" From ecc219fbc14158f5925627c83bb6d63d4a3a0ca9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 16 Jul 2022 11:02:08 -0500 Subject: [PATCH 2627/3516] Include the source in the bluetooth service info (#75112) --- homeassistant/components/bluetooth/__init__.py | 14 ++++++++++---- homeassistant/helpers/service_info/bluetooth.py | 4 +++- tests/components/bluetooth/test_init.py | 8 +++++++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 01f55048c6d..b6058767669 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -39,6 +39,8 @@ _LOGGER = logging.getLogger(__name__) MAX_REMEMBER_ADDRESSES: Final = 2048 +SOURCE_LOCAL: Final = "local" + class BluetoothCallbackMatcherOptional(TypedDict, total=False): """Matcher for the bluetooth integration for callback optional fields.""" @@ -266,7 +268,7 @@ class BluetoothManager: ): if service_info is None: service_info = BluetoothServiceInfo.from_advertisement( - device, advertisement_data + device, advertisement_data, SOURCE_LOCAL ) try: callback(service_info, BluetoothChange.ADVERTISEMENT) @@ -277,7 +279,7 @@ class BluetoothManager: return if service_info is None: service_info = BluetoothServiceInfo.from_advertisement( - device, advertisement_data + device, advertisement_data, SOURCE_LOCAL ) for domain in matched_domains: discovery_flow.async_create_flow( @@ -312,7 +314,9 @@ class BluetoothManager: ): try: callback( - BluetoothServiceInfo.from_advertisement(*device_adv_data), + BluetoothServiceInfo.from_advertisement( + *device_adv_data, SOURCE_LOCAL + ), BluetoothChange.ADVERTISEMENT, ) except Exception: # pylint: disable=broad-except @@ -338,7 +342,9 @@ class BluetoothManager: discovered = models.HA_BLEAK_SCANNER.discovered_devices history = models.HA_BLEAK_SCANNER.history return [ - BluetoothServiceInfo.from_advertisement(*history[device.address]) + BluetoothServiceInfo.from_advertisement( + *history[device.address], SOURCE_LOCAL + ) for device in discovered if device.address in history ] diff --git a/homeassistant/helpers/service_info/bluetooth.py b/homeassistant/helpers/service_info/bluetooth.py index 9dff2782da0..003a4228d80 100644 --- a/homeassistant/helpers/service_info/bluetooth.py +++ b/homeassistant/helpers/service_info/bluetooth.py @@ -22,10 +22,11 @@ class BluetoothServiceInfo(BaseServiceInfo): manufacturer_data: dict[int, bytes] service_data: dict[str, bytes] service_uuids: list[str] + source: str @classmethod def from_advertisement( - cls, device: BLEDevice, advertisement_data: AdvertisementData + cls, device: BLEDevice, advertisement_data: AdvertisementData, source: str ) -> BluetoothServiceInfo: """Create a BluetoothServiceInfo from an advertisement.""" return cls( @@ -35,6 +36,7 @@ class BluetoothServiceInfo(BaseServiceInfo): manufacturer_data=advertisement_data.manufacturer_data, service_data=advertisement_data.service_data, service_uuids=advertisement_data.service_uuids, + source=source, ) @cached_property diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 2bbe4ce7dcb..1d348f42f0e 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -6,6 +6,7 @@ from bleak.backends.scanner import AdvertisementData, BLEDevice from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( + SOURCE_LOCAL, BluetoothChange, BluetoothServiceInfo, models, @@ -244,6 +245,7 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): assert len(service_infos) == 1 # wrong_name should not appear because bleak no longer sees it assert service_infos[0].name == "wohand" + assert service_infos[0].source == SOURCE_LOCAL assert bluetooth.async_address_present(hass, "44:44:33:11:23:42") is False assert bluetooth.async_address_present(hass, "44:44:33:11:23:45") is True @@ -255,7 +257,8 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start): callbacks = [] def _fake_subscriber( - service_info: BluetoothServiceInfo, change: BluetoothChange + service_info: BluetoothServiceInfo, + change: BluetoothChange, ) -> None: """Fake subscriber for the BleakScanner.""" callbacks.append((service_info, change)) @@ -312,16 +315,19 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start): service_info: BluetoothServiceInfo = callbacks[0][0] assert service_info.name == "wohand" + assert service_info.source == SOURCE_LOCAL assert service_info.manufacturer == "Nordic Semiconductor ASA" assert service_info.manufacturer_id == 89 service_info: BluetoothServiceInfo = callbacks[1][0] assert service_info.name == "empty" + assert service_info.source == SOURCE_LOCAL assert service_info.manufacturer is None assert service_info.manufacturer_id is None service_info: BluetoothServiceInfo = callbacks[2][0] assert service_info.name == "empty" + assert service_info.source == SOURCE_LOCAL assert service_info.manufacturer is None assert service_info.manufacturer_id is None From b9c8d65940ec47a82332b8b1a67301da018ccadf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 16 Jul 2022 12:49:15 -0500 Subject: [PATCH 2628/3516] Restore accessory state into pairing using new HKC methods (#75276) --- .../components/homekit_controller/__init__.py | 23 ++-- .../homekit_controller/config_flow.py | 27 ++-- .../homekit_controller/connection.py | 118 ++++++++++-------- .../homekit_controller/device_trigger.py | 5 +- .../homekit_controller/diagnostics.py | 1 + .../homekit_controller/manifest.json | 2 +- .../components/homekit_controller/storage.py | 12 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit_controller/common.py | 6 +- .../homekit_controller/test_config_flow.py | 6 +- .../homekit_controller/test_diagnostics.py | 2 + .../homekit_controller/test_init.py | 12 +- .../homekit_controller/test_storage.py | 3 +- 14 files changed, 130 insertions(+), 91 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 9175dca50bc..37dd648dedb 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -6,6 +6,11 @@ import logging from typing import Any import aiohomekit +from aiohomekit.exceptions import ( + AccessoryDisconnectedError, + AccessoryNotFoundError, + EncryptionError, +) from aiohomekit.model import Accessory from aiohomekit.model.characteristics import ( Characteristic, @@ -26,7 +31,7 @@ from homeassistant.helpers.typing import ConfigType from .config_flow import normalize_hkid from .connection import HKDevice, valid_serial_number from .const import ENTITY_MAP, KNOWN_DEVICES, TRIGGERS -from .storage import EntityMapStorage +from .storage import async_get_entity_storage from .utils import async_get_controller, folded_name _LOGGER = logging.getLogger(__name__) @@ -227,23 +232,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, unique_id=normalize_hkid(conn.unique_id) ) - if not await conn.async_setup(): + try: + await conn.async_setup() + except (AccessoryNotFoundError, EncryptionError, AccessoryDisconnectedError) as ex: del hass.data[KNOWN_DEVICES][conn.unique_id] - if (connection := getattr(conn.pairing, "connection", None)) and hasattr( - connection, "host" - ): - raise ConfigEntryNotReady( - f"Cannot connect to {connection.host}:{connection.port}" - ) - raise ConfigEntryNotReady + await conn.pairing.close() + raise ConfigEntryNotReady from ex return True async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up for Homekit devices.""" - map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass) - await map_storage.async_initialize() + await async_get_entity_storage(hass) await async_get_controller(hass) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 41094a65e00..9b8b759f80e 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -8,7 +8,6 @@ from typing import Any import aiohomekit from aiohomekit.controller.abstract import AbstractPairing from aiohomekit.exceptions import AuthenticationError -from aiohomekit.model import Accessories, CharacteristicsTypes, ServicesTypes from aiohomekit.utils import domain_supported, domain_to_name import voluptuous as vol @@ -20,6 +19,7 @@ from homeassistant.helpers import device_registry as dr from .connection import HKDevice from .const import DOMAIN, KNOWN_DEVICES +from .storage import async_get_entity_storage from .utils import async_get_controller HOMEKIT_DIR = ".homekit" @@ -252,9 +252,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.debug( "HomeKit info %s: c# incremented, refreshing entities", hkid ) - self.hass.async_create_task( - conn.async_refresh_entity_map_and_entities(config_num) - ) + conn.async_notify_config_changed(config_num) return self.async_abort(reason="already_configured") _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) @@ -481,17 +479,22 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # available. Otherwise request a fresh copy from the API. # This removes the 'accessories' key from pairing_data at # the same time. - if not (accessories := pairing_data.pop("accessories", None)): - accessories = await pairing.list_accessories_and_characteristics() - - parsed = Accessories.from_list(accessories) - accessory_info = parsed.aid(1).services.first( - service_type=ServicesTypes.ACCESSORY_INFORMATION - ) - name = accessory_info.value(CharacteristicsTypes.NAME, "") + name = await pairing.get_primary_name() await pairing.close() + # Save the state of the accessories so we do not + # have to request them again when we setup the + # config entry. + accessories_state = pairing.accessories_state + entity_storage = await async_get_entity_storage(self.hass) + assert self.unique_id is not None + entity_storage.async_create_or_update_map( + self.unique_id, + accessories_state.config_num, + accessories_state.accessories.serialize(), + ) + return self.async_create_entry(title=name, data=pairing_data) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 85e8e3987bd..7e4dcc59be0 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -5,6 +5,7 @@ import asyncio from collections.abc import Callable import datetime import logging +from types import MappingProxyType from typing import Any from aiohomekit import Controller @@ -17,8 +18,9 @@ from aiohomekit.model import Accessories, Accessory from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes from aiohomekit.model.services import Service +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_VIA_DEVICE -from homeassistant.core import CALLBACK_TYPE, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo @@ -62,7 +64,12 @@ def valid_serial_number(serial: str) -> bool: class HKDevice: """HomeKit device.""" - def __init__(self, hass, config_entry, pairing_data) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + pairing_data: MappingProxyType[str, Any], + ) -> None: """Initialise a generic HomeKit device.""" self.hass = hass @@ -78,11 +85,6 @@ class HKDevice: self.pairing_data["AccessoryPairingID"], self.pairing_data ) - self.accessories: list[Any] | None = None - self.config_num = 0 - - self.entity_map = Accessories() - # A list of callbacks that turn HK accessories into entities self.accessory_factories: list[AddAccessoryCb] = [] @@ -132,6 +134,17 @@ class HKDevice: self.watchable_characteristics: list[tuple[int, int]] = [] self.pairing.dispatcher_connect(self.process_new_events) + self.pairing.dispatcher_connect_config_changed(self.process_config_changed) + + @property + def entity_map(self) -> Accessories: + """Return the accessories from the pairing.""" + return self.pairing.accessories_state.accessories + + @property + def config_num(self) -> int: + """Return the config num from the pairing.""" + return self.pairing.accessories_state.config_num def add_pollable_characteristics( self, characteristics: list[tuple[int, int]] @@ -169,13 +182,13 @@ class HKDevice: self.available = available async_dispatcher_send(self.hass, self.signal_state_updated) - async def async_ensure_available(self) -> bool: + async def async_ensure_available(self) -> None: """Verify the accessory is available after processing the entity map.""" if self.available: - return True + return if self.watchable_characteristics and self.pollable_characteristics: # We already tried, no need to try again - return False + return # We there are no watchable and not pollable characteristics, # we need to force a connection to the device to verify its alive. # @@ -185,34 +198,42 @@ class HKDevice: primary = self.entity_map.accessories[0] aid = primary.aid iid = primary.accessory_information[CharacteristicsTypes.SERIAL_NUMBER].iid - try: - await self.pairing.get_characteristics([(aid, iid)]) - except (AccessoryDisconnectedError, EncryptionError, AccessoryNotFoundError): - return False + await self.pairing.get_characteristics([(aid, iid)]) self.async_set_available_state(True) - return True - async def async_setup(self) -> bool: + async def async_setup(self) -> None: """Prepare to use a paired HomeKit device in Home Assistant.""" entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP] if cache := entity_storage.get_map(self.unique_id): - self.accessories = cache["accessories"] - self.config_num = cache["config_num"] - self.entity_map = Accessories.from_list(self.accessories) - elif not await self.async_refresh_entity_map(self.config_num): - return False + self.pairing.restore_accessories_state( + cache["accessories"], cache["config_num"] + ) + + # We need to force an update here to make sure we have + # the latest values since the async_update we do in + # async_process_entity_map will no values to poll yet + # since entities are added via dispatching and then + # they add the chars they are concerned about in + # async_added_to_hass which is too late. + # + # Ideally we would know which entities we are about to add + # so we only poll those chars but that is not possible + # yet. + await self.pairing.async_populate_accessories_state(force_update=True) await self.async_process_entity_map() - if not await self.async_ensure_available(): - return False + await self.async_ensure_available() + + if not cache: + # If its missing from the cache, make sure we save it + self.async_save_entity_map() # If everything is up to date, we can create the entities # since we know the data is not stale. await self.async_add_new_entities() self._polling_interval_remover = async_track_time_interval( self.hass, self.async_update, DEFAULT_SCAN_INTERVAL ) - return True async def async_add_new_entities(self) -> None: """Add new entities to Home Assistant.""" @@ -390,9 +411,6 @@ class HKDevice: # Ensure the Pairing object has access to the latest version of the entity map. This # is especially important for BLE, as the Pairing instance relies on the entity map # to map aid/iid to GATT characteristics. So push it to there as well. - - self.pairing.pairing_data["accessories"] = self.accessories # type: ignore[attr-defined] - self.async_detect_workarounds() # Migrate to new device ids @@ -403,13 +421,6 @@ class HKDevice: # Load any triggers for this config entry await async_setup_triggers_for_entry(self.hass, self.config_entry) - if self.watchable_characteristics: - await self.pairing.subscribe(self.watchable_characteristics) - if not self.pairing.is_connected: - return - - await self.async_update() - async def async_unload(self) -> None: """Stop interacting with device and prepare for removal from hass.""" if self._polling_interval_remover: @@ -421,34 +432,31 @@ class HKDevice: self.config_entry, self.platforms ) - async def async_refresh_entity_map_and_entities(self, config_num: int) -> None: - """Refresh the entity map and entities for this pairing.""" - await self.async_refresh_entity_map(config_num) + def async_notify_config_changed(self, config_num: int) -> None: + """Notify the pairing of a config change.""" + self.pairing.notify_config_changed(config_num) + + def process_config_changed(self, config_num: int) -> None: + """Handle a config change notification from the pairing.""" + self.hass.async_create_task(self.async_update_new_accessories_state()) + + async def async_update_new_accessories_state(self) -> None: + """Process a change in the pairings accessories state.""" + self.async_save_entity_map() await self.async_process_entity_map() + if self.watchable_characteristics: + await self.pairing.subscribe(self.watchable_characteristics) + await self.async_update() await self.async_add_new_entities() - async def async_refresh_entity_map(self, config_num: int) -> bool: - """Handle setup of a HomeKit accessory.""" - try: - self.accessories = await self.pairing.list_accessories_and_characteristics() - except AccessoryDisconnectedError: - # If we fail to refresh this data then we will naturally retry - # later when Bonjour spots c# is still not up to date. - return False - - assert self.accessories is not None - - self.entity_map = Accessories.from_list(self.accessories) - + @callback + def async_save_entity_map(self) -> None: + """Save the entity map.""" entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP] - entity_storage.async_create_or_update_map( - self.unique_id, config_num, self.accessories + self.unique_id, self.config_num, self.entity_map.serialize() ) - self.config_num = config_num - return True - def add_accessory_factory(self, add_entities_cb) -> None: """Add a callback to run when discovering new entities for accessories.""" self.accessory_factories.append(add_entities_cb) diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index 49924b30c57..d7828753d98 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -15,6 +15,7 @@ from homeassistant.components.automation import ( AutomationTriggerInfo, ) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.typing import ConfigType @@ -195,7 +196,9 @@ TRIGGER_FINDERS = { } -async def async_setup_triggers_for_entry(hass: HomeAssistant, config_entry): +async def async_setup_triggers_for_entry( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Triggers aren't entities as they have no state, but we still need to set them up for a config entry.""" hkid = config_entry.data["AccessoryPairingID"] conn: HKDevice = hass.data[KNOWN_DEVICES][hkid] diff --git a/homeassistant/components/homekit_controller/diagnostics.py b/homeassistant/components/homekit_controller/diagnostics.py index f83ce7604cf..9b17c0c2fe7 100644 --- a/homeassistant/components/homekit_controller/diagnostics.py +++ b/homeassistant/components/homekit_controller/diagnostics.py @@ -107,6 +107,7 @@ def _async_get_diagnostics( # It is roughly equivalent to what is in .storage/homekit_controller-entity-map # But it also has the latest values seen by the polling or events data["entity-map"] = accessories = connection.entity_map.serialize() + data["config-num"] = connection.config_num # It contains serial numbers, which we should strip out for accessory in accessories: diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 0275afa07fe..a5ca76fdc1e 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.0.0"], + "requirements": ["aiohomekit==1.1.1"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index ff39c52627e..8c0628c97f6 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -7,7 +7,7 @@ from typing import Any, TypedDict from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.storage import Store -from .const import DOMAIN +from .const import DOMAIN, ENTITY_MAP ENTITY_MAP_STORAGE_KEY = f"{DOMAIN}-entity-map" ENTITY_MAP_STORAGE_VERSION = 1 @@ -91,3 +91,13 @@ class EntityMapStorage: def _data_to_save(self) -> StorageLayout: """Return data of entity map to store in a file.""" return StorageLayout(pairings=self.storage_data) + + +async def async_get_entity_storage(hass: HomeAssistant) -> EntityMapStorage: + """Get entity storage.""" + if ENTITY_MAP in hass.data: + map_storage: EntityMapStorage = hass.data[ENTITY_MAP] + return map_storage + map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass) + await map_storage.async_initialize() + return map_storage diff --git a/requirements_all.txt b/requirements_all.txt index 63fd2862faa..4fc5e180e95 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.0.0 +aiohomekit==1.1.1 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 034ead5c35e..ff34193cee8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.0.0 +aiohomekit==1.1.1 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index e773b2ffc66..18367d28f63 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -9,7 +9,7 @@ import os from typing import Any, Final from unittest import mock -from aiohomekit.model import Accessories, Accessory +from aiohomekit.model import Accessories, AccessoriesState, Accessory from aiohomekit.testing import FakeController, FakePairing from homeassistant.components import zeroconf @@ -225,7 +225,9 @@ async def device_config_changed(hass, accessories): accessories_obj = Accessories() for accessory in accessories: accessories_obj.add_accessory(accessory) - pairing.accessories = accessories_obj + pairing._accessories_state = AccessoriesState( + accessories_obj, pairing.config_num + 1 + ) discovery_info = zeroconf.ZeroconfServiceInfo( host="127.0.0.1", diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 9535b7d0cd5..73bd159fd73 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -2,7 +2,7 @@ import asyncio from unittest import mock import unittest.mock -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import aiohomekit from aiohomekit.exceptions import AuthenticationError @@ -492,7 +492,7 @@ async def test_discovery_already_configured_update_csharp(hass, controller): connection_mock = AsyncMock() connection_mock.pairing.connect.reconnect_soon = AsyncMock() - connection_mock.async_refresh_entity_map = AsyncMock() + connection_mock.async_notify_config_changed = MagicMock() hass.data[KNOWN_DEVICES] = {"AA:BB:CC:DD:EE:FF": connection_mock} device = setup_mock_accessory(controller) @@ -515,7 +515,7 @@ async def test_discovery_already_configured_update_csharp(hass, controller): assert entry.data["AccessoryIP"] == discovery_info.host assert entry.data["AccessoryPort"] == discovery_info.port - assert connection_mock.async_refresh_entity_map_and_entities.await_count == 1 + assert connection_mock.async_notify_config_changed.call_count == 1 @pytest.mark.parametrize("exception,expected", PAIRING_START_ABORT_ERRORS) diff --git a/tests/components/homekit_controller/test_diagnostics.py b/tests/components/homekit_controller/test_diagnostics.py index e770279752e..dd0e35b0d1e 100644 --- a/tests/components/homekit_controller/test_diagnostics.py +++ b/tests/components/homekit_controller/test_diagnostics.py @@ -28,6 +28,7 @@ async def test_config_entry(hass: HomeAssistant, hass_client: ClientSession, utc "version": 1, "data": {"AccessoryPairingID": "00:00:00:00:00:00"}, }, + "config-num": 0, "entity-map": [ { "aid": 1, @@ -299,6 +300,7 @@ async def test_device(hass: HomeAssistant, hass_client: ClientSession, utcnow): "version": 1, "data": {"AccessoryPairingID": "00:00:00:00:00:00"}, }, + "config-num": 0, "entity-map": [ { "aid": 1, diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 57b39076aa6..df14ad325b6 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -111,8 +111,16 @@ async def test_offline_device_raises(hass, controller): nonlocal is_connected return is_connected - def get_characteristics(self, chars, *args, **kwargs): - raise AccessoryDisconnectedError("any") + async def async_populate_accessories_state(self, *args, **kwargs): + nonlocal is_connected + if not is_connected: + raise AccessoryDisconnectedError("any") + + async def get_characteristics(self, chars, *args, **kwargs): + nonlocal is_connected + if not is_connected: + raise AccessoryDisconnectedError("any") + return {} with patch("aiohomekit.testing.FakePairing", OfflineFakePairing): await async_setup_component(hass, DOMAIN, {}) diff --git a/tests/components/homekit_controller/test_storage.py b/tests/components/homekit_controller/test_storage.py index b4ed617f901..13d613e3916 100644 --- a/tests/components/homekit_controller/test_storage.py +++ b/tests/components/homekit_controller/test_storage.py @@ -3,6 +3,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes from homeassistant.components.homekit_controller.const import ENTITY_MAP +from homeassistant.components.homekit_controller.storage import EntityMapStorage from tests.common import flush_store from tests.components.homekit_controller.common import ( @@ -68,7 +69,7 @@ async def test_storage_is_updated_on_add(hass, hass_storage, utcnow): """Test entity map storage is cleaned up on adding an accessory.""" await setup_test_component(hass, create_lightbulb_service) - entity_map = hass.data[ENTITY_MAP] + entity_map: EntityMapStorage = hass.data[ENTITY_MAP] hkid = "00:00:00:00:00:00" # Is in memory store updated? From 514e826fed5bd601223022920a3bde38a09b8b6c Mon Sep 17 00:00:00 2001 From: Jelte Zeilstra Date: Sat, 16 Jul 2022 20:39:11 +0200 Subject: [PATCH 2629/3516] Add install UniFi device update feature (#75302) * Add install UniFi device update feature * Add tests for install UniFi device update feature * Fix type error * Process review feedback * Process review feedback --- homeassistant/components/unifi/update.py | 13 +- tests/components/unifi/test_update.py | 165 +++++++++++++++-------- 2 files changed, 118 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/unifi/update.py b/homeassistant/components/unifi/update.py index 09720f15f84..24967e043d9 100644 --- a/homeassistant/components/unifi/update.py +++ b/homeassistant/components/unifi/update.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.update import ( DOMAIN, @@ -71,7 +72,6 @@ class UniFiDeviceUpdateEntity(UniFiBase, UpdateEntity): DOMAIN = DOMAIN TYPE = DEVICE_UPDATE _attr_device_class = UpdateDeviceClass.FIRMWARE - _attr_supported_features = UpdateEntityFeature.PROGRESS def __init__(self, device, controller): """Set up device update entity.""" @@ -79,6 +79,11 @@ class UniFiDeviceUpdateEntity(UniFiBase, UpdateEntity): self.device = self._item + self._attr_supported_features = UpdateEntityFeature.PROGRESS + + if self.controller.site_role == "admin": + self._attr_supported_features |= UpdateEntityFeature.INSTALL + @property def name(self) -> str: """Return the name of the device.""" @@ -126,3 +131,9 @@ class UniFiDeviceUpdateEntity(UniFiBase, UpdateEntity): async def options_updated(self) -> None: """No action needed.""" + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update.""" + await self.controller.api.devices.upgrade(self.device.mac) diff --git a/tests/components/unifi/test_update.py b/tests/components/unifi/test_update.py index b5eb9c1d02e..7bbeb65497e 100644 --- a/tests/components/unifi/test_update.py +++ b/tests/components/unifi/test_update.py @@ -1,23 +1,59 @@ """The tests for the UniFi Network update platform.""" +from copy import deepcopy from aiounifi.controller import MESSAGE_DEVICE from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING +from yarl import URL +from homeassistant.components.unifi.const import DOMAIN as UNIFI_DOMAIN from homeassistant.components.update import ( ATTR_IN_PROGRESS, ATTR_INSTALLED_VERSION, ATTR_LATEST_VERSION, DOMAIN as UPDATE_DOMAIN, + SERVICE_INSTALL, UpdateDeviceClass, + UpdateEntityFeature, ) from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) -from .test_controller import setup_unifi_integration +from .test_controller import DESCRIPTION, setup_unifi_integration + +DEVICE_1 = { + "board_rev": 3, + "device_id": "mock-id", + "ip": "10.0.1.1", + "last_seen": 1562600145, + "mac": "00:00:00:00:01:01", + "model": "US16P150", + "name": "Device 1", + "next_interval": 20, + "state": 1, + "type": "usw", + "upgradable": True, + "version": "4.0.42.10433", + "upgrade_to_firmware": "4.3.17.11279", +} + +DEVICE_2 = { + "board_rev": 3, + "device_id": "mock-id", + "ip": "10.0.1.2", + "mac": "00:00:00:00:01:02", + "model": "US16P150", + "name": "Device 2", + "next_interval": 20, + "state": 0, + "type": "usw", + "version": "4.0.42.10433", +} async def test_no_entities(hass, aioclient_mock): @@ -31,41 +67,11 @@ async def test_device_updates( hass, aioclient_mock, mock_unifi_websocket, mock_device_registry ): """Test the update_items function with some devices.""" - device_1 = { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "fan_level": 0, - "ip": "10.0.1.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device 1", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "version": "4.0.42.10433", - "upgrade_to_firmware": "4.3.17.11279", - } - device_2 = { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "ip": "10.0.1.2", - "mac": "00:00:00:00:01:02", - "model": "US16P150", - "name": "Device 2", - "next_interval": 20, - "state": 0, - "type": "usw", - "version": "4.0.42.10433", - } + device_1 = deepcopy(DEVICE_1) await setup_unifi_integration( hass, aioclient_mock, - devices_response=[device_1, device_2], + devices_response=[device_1, DEVICE_2], ) assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 2 @@ -76,6 +82,10 @@ async def test_device_updates( assert device_1_state.attributes[ATTR_LATEST_VERSION] == "4.3.17.11279" assert device_1_state.attributes[ATTR_IN_PROGRESS] is False assert device_1_state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + assert ( + device_1_state.attributes[ATTR_SUPPORTED_FEATURES] + == UpdateEntityFeature.PROGRESS | UpdateEntityFeature.INSTALL + ) device_2_state = hass.states.get("update.device_2") assert device_2_state.state == STATE_OFF @@ -83,6 +93,10 @@ async def test_device_updates( assert device_2_state.attributes[ATTR_LATEST_VERSION] == "4.0.42.10433" assert device_2_state.attributes[ATTR_IN_PROGRESS] is False assert device_2_state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE + assert ( + device_2_state.attributes[ATTR_SUPPORTED_FEATURES] + == UpdateEntityFeature.PROGRESS | UpdateEntityFeature.INSTALL + ) # Simulate start of update @@ -122,46 +136,79 @@ async def test_device_updates( assert device_1_state.attributes[ATTR_IN_PROGRESS] is False -async def test_controller_state_change( - hass, aioclient_mock, mock_unifi_websocket, mock_device_registry -): - """Verify entities state reflect on controller becoming unavailable.""" - device = { - "board_rev": 3, - "device_id": "mock-id", - "has_fan": True, - "fan_level": 0, - "ip": "10.0.1.1", - "last_seen": 1562600145, - "mac": "00:00:00:00:01:01", - "model": "US16P150", - "name": "Device", - "next_interval": 20, - "overheating": True, - "state": 1, - "type": "usw", - "upgradable": True, - "version": "4.0.42.10433", - "upgrade_to_firmware": "4.3.17.11279", - } +async def test_not_admin(hass, aioclient_mock): + """Test that the INSTALL feature is not available on a non-admin account.""" + description = deepcopy(DESCRIPTION) + description[0]["site_role"] = "not admin" await setup_unifi_integration( hass, aioclient_mock, - devices_response=[device], + site_description=description, + devices_response=[DEVICE_1], ) assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 - assert hass.states.get("update.device").state == STATE_ON + device_state = hass.states.get("update.device_1") + assert device_state.state == STATE_ON + assert ( + device_state.attributes[ATTR_SUPPORTED_FEATURES] == UpdateEntityFeature.PROGRESS + ) + + +async def test_install(hass, aioclient_mock): + """Test the device update install call.""" + config_entry = await setup_unifi_integration( + hass, aioclient_mock, devices_response=[DEVICE_1] + ) + + assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 + device_state = hass.states.get("update.device_1") + assert device_state.state == STATE_ON + + controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + url = f"https://{controller.host}:1234/api/s/{controller.site}/cmd/devmgr" + aioclient_mock.clear_requests() + aioclient_mock.post(url) + + await hass.services.async_call( + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.device_1"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert aioclient_mock.call_count == 1 + assert aioclient_mock.mock_calls[0] == ( + "post", + URL(url), + {"cmd": "upgrade", "mac": "00:00:00:00:01:01"}, + {}, + ) + + +async def test_controller_state_change( + hass, aioclient_mock, mock_unifi_websocket, mock_device_registry +): + """Verify entities state reflect on controller becoming unavailable.""" + await setup_unifi_integration( + hass, + aioclient_mock, + devices_response=[DEVICE_1], + ) + + assert len(hass.states.async_entity_ids(UPDATE_DOMAIN)) == 1 + assert hass.states.get("update.device_1").state == STATE_ON # Controller unavailable mock_unifi_websocket(state=STATE_DISCONNECTED) await hass.async_block_till_done() - assert hass.states.get("update.device").state == STATE_UNAVAILABLE + assert hass.states.get("update.device_1").state == STATE_UNAVAILABLE # Controller available mock_unifi_websocket(state=STATE_RUNNING) await hass.async_block_till_done() - assert hass.states.get("update.device").state == STATE_ON + assert hass.states.get("update.device_1").state == STATE_ON From ae4b1967a31decc87a71301d93d3d65bc6ef9300 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 16 Jul 2022 20:56:48 +0200 Subject: [PATCH 2630/3516] Use pydeconz interface controls for lights (#75261) --- homeassistant/components/deconz/gateway.py | 8 ++-- homeassistant/components/deconz/light.py | 44 ++++++++++--------- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 31 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 0ee967f88d9..f8e4548cf91 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -10,8 +10,8 @@ from typing import TYPE_CHECKING, Any, cast import async_timeout from pydeconz import DeconzSession, errors from pydeconz.interfaces import sensors -from pydeconz.interfaces.api import APIItems, GroupedAPIItems -from pydeconz.interfaces.groups import Groups +from pydeconz.interfaces.api_handlers import APIHandler, GroupedAPIHandler +from pydeconz.interfaces.groups import GroupHandler from pydeconz.models.event import EventType from homeassistant.config_entries import SOURCE_HASSIO, ConfigEntry @@ -127,7 +127,7 @@ class DeconzGateway: def register_platform_add_device_callback( self, add_device_callback: Callable[[EventType, str], None], - deconz_device_interface: APIItems | GroupedAPIItems, + deconz_device_interface: APIHandler | GroupedAPIHandler, always_ignore_clip_sensors: bool = False, ) -> None: """Wrap add_device_callback to check allow_new_devices option.""" @@ -148,7 +148,7 @@ class DeconzGateway: self.ignored_devices.add((async_add_device, device_id)) return - if isinstance(deconz_device_interface, Groups): + if isinstance(deconz_device_interface, GroupHandler): self.deconz_groups.add((async_add_device, device_id)) if not self.option_allow_deconz_groups: return diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 7be6551cccc..7c6f1a0e362 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -3,16 +3,12 @@ from __future__ import annotations from typing import Any, Generic, TypedDict, TypeVar +from pydeconz.interfaces.groups import GroupHandler +from pydeconz.interfaces.lights import LightHandler from pydeconz.models import ResourceType from pydeconz.models.event import EventType from pydeconz.models.group import Group -from pydeconz.models.light import ( - ALERT_LONG, - ALERT_SHORT, - EFFECT_COLOR_LOOP, - EFFECT_NONE, -) -from pydeconz.models.light.light import Light +from pydeconz.models.light.light import Light, LightAlert, LightColorMode, LightEffect from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -42,8 +38,14 @@ from .deconz_device import DeconzDevice from .gateway import DeconzGateway, 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} +EFFECT_TO_DECONZ = {EFFECT_COLORLOOP: LightEffect.COLOR_LOOP, "None": LightEffect.NONE} +FLASH_TO_DECONZ = {FLASH_SHORT: LightAlert.SHORT, FLASH_LONG: LightAlert.LONG} + +DECONZ_TO_COLOR_MODE = { + LightColorMode.CT: ColorMode.COLOR_TEMP, + LightColorMode.HS: ColorMode.HS, + LightColorMode.XY: ColorMode.XY, +} _L = TypeVar("_L", Group, Light) @@ -51,10 +53,10 @@ _L = TypeVar("_L", Group, Light) class SetStateAttributes(TypedDict, total=False): """Attributes available with set state call.""" - alert: str + alert: LightAlert brightness: int color_temperature: int - effect: str + effect: LightEffect hue: int on: bool saturation: int @@ -130,7 +132,13 @@ class DeconzBaseLight(Generic[_L], DeconzDevice, LightEntity): """Set up light.""" super().__init__(device, gateway) - self._attr_supported_color_modes: set[str] = set() + self.api: GroupHandler | LightHandler + if isinstance(self._device, Light): + self.api = self.gateway.api.lights.lights + elif isinstance(self._device, Group): + self.api = self.gateway.api.groups + + self._attr_supported_color_modes: set[ColorMode] = set() if device.color_temp is not None: self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP) @@ -158,12 +166,8 @@ class DeconzBaseLight(Generic[_L], DeconzDevice, LightEntity): @property def color_mode(self) -> str | None: """Return the color mode of the light.""" - if self._device.color_mode == "ct": - color_mode = ColorMode.COLOR_TEMP - elif self._device.color_mode == "hs": - color_mode = ColorMode.HS - elif self._device.color_mode == "xy": - color_mode = ColorMode.XY + if self._device.color_mode in DECONZ_TO_COLOR_MODE: + color_mode = DECONZ_TO_COLOR_MODE[self._device.color_mode] elif self._device.brightness is not None: color_mode = ColorMode.BRIGHTNESS else: @@ -229,7 +233,7 @@ class DeconzBaseLight(Generic[_L], DeconzDevice, LightEntity): if ATTR_EFFECT in kwargs and kwargs[ATTR_EFFECT] in EFFECT_TO_DECONZ: data["effect"] = EFFECT_TO_DECONZ[kwargs[ATTR_EFFECT]] - await self._device.set_state(**data) + await self.api.set_state(id=self._device.resource_id, **data) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off light.""" @@ -246,7 +250,7 @@ class DeconzBaseLight(Generic[_L], DeconzDevice, LightEntity): data["alert"] = FLASH_TO_DECONZ[kwargs[ATTR_FLASH]] del data["on"] - await self._device.set_state(**data) + await self.api.set_state(id=self._device.resource_id, **data) @property def extra_state_attributes(self) -> dict[str, bool]: diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index c1b0b07de02..8019d0df2df 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==99"], + "requirements": ["pydeconz==100"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 4fc5e180e95..ff2640568d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1447,7 +1447,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==99 +pydeconz==100 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff34193cee8..fc576e28612 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -980,7 +980,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==99 +pydeconz==100 # homeassistant.components.dexcom pydexcom==0.2.3 From cb12f77e330d40cd39e640e649ed9d246b6654c4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 16 Jul 2022 22:00:58 +0200 Subject: [PATCH 2631/3516] Update sentry-sdk to 1.7.2 (#75331) --- 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 29b897e4a3c..c1163ea1791 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.7.1"], + "requirements": ["sentry-sdk==1.7.2"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index ff2640568d8..706cf2b1a6b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2153,7 +2153,7 @@ sendgrid==6.8.2 sense_energy==0.10.4 # homeassistant.components.sentry -sentry-sdk==1.7.1 +sentry-sdk==1.7.2 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fc576e28612..b0a4d4acff5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1434,7 +1434,7 @@ securetar==2022.2.0 sense_energy==0.10.4 # homeassistant.components.sentry -sentry-sdk==1.7.1 +sentry-sdk==1.7.2 # homeassistant.components.sharkiq sharkiq==0.0.1 From 08d648799710b3b5696b86bbc42ec256d8b727fa Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 16 Jul 2022 14:04:44 -0600 Subject: [PATCH 2632/3516] Handle (and better log) more AirVisual cloud API errors (#75332) --- .../components/airvisual/__init__.py | 11 ++++----- .../components/airvisual/config_flow.py | 4 +++- .../components/airvisual/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/airvisual/test_config_flow.py | 24 +++++++++++++++++++ 6 files changed, 35 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index c1bb6020b3c..a2a3d76c3db 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, cast +from typing import Any from pyairvisual import CloudAPI, NodeSamba from pyairvisual.errors import ( @@ -12,6 +12,7 @@ from pyairvisual.errors import ( InvalidKeyError, KeyExpiredError, NodeProError, + UnauthorizedError, ) from homeassistant.config_entries import ConfigEntry @@ -210,9 +211,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: - data = await api_coro - return cast(dict[str, Any], data) - except (InvalidKeyError, KeyExpiredError) as ex: + return await api_coro + except (InvalidKeyError, KeyExpiredError, UnauthorizedError) as ex: raise ConfigEntryAuthFailed from ex except AirVisualError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err @@ -253,8 +253,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async with NodeSamba( entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD] ) as node: - data = await node.async_get_latest_measurements() - return cast(dict[str, Any], data) + return await node.async_get_latest_measurements() except NodeProError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index f97616c38fc..385c9f55753 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -9,8 +9,10 @@ from pyairvisual import CloudAPI, NodeSamba from pyairvisual.errors import ( AirVisualError, InvalidKeyError, + KeyExpiredError, NodeProError, NotFoundError, + UnauthorizedError, ) import voluptuous as vol @@ -119,7 +121,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input[CONF_API_KEY] not in valid_keys: try: await coro - except InvalidKeyError: + except (InvalidKeyError, KeyExpiredError, UnauthorizedError): errors[CONF_API_KEY] = "invalid_api_key" except NotFoundError: errors[CONF_CITY] = "location_not_found" diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index ed803a3e6a1..9a6279f34a6 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -3,7 +3,7 @@ "name": "AirVisual", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airvisual", - "requirements": ["pyairvisual==5.0.9"], + "requirements": ["pyairvisual==2022.07.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyairvisual", "pysmb"] diff --git a/requirements_all.txt b/requirements_all.txt index 706cf2b1a6b..ef293dcdcae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1369,7 +1369,7 @@ pyaftership==21.11.0 pyairnow==1.1.0 # homeassistant.components.airvisual -pyairvisual==5.0.9 +pyairvisual==2022.07.0 # homeassistant.components.almond pyalmond==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0a4d4acff5..2b1ae82fd16 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -935,7 +935,7 @@ pyaehw4a1==0.3.9 pyairnow==1.1.0 # homeassistant.components.airvisual -pyairvisual==5.0.9 +pyairvisual==2022.07.0 # homeassistant.components.almond pyalmond==0.0.2 diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index 12a9e67122f..f97ee845dba 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -4,8 +4,10 @@ from unittest.mock import patch from pyairvisual.errors import ( AirVisualError, InvalidKeyError, + KeyExpiredError, NodeProError, NotFoundError, + UnauthorizedError, ) import pytest @@ -84,6 +86,28 @@ async def test_duplicate_error(hass, config, config_entry, data): {CONF_API_KEY: "invalid_api_key"}, INTEGRATION_TYPE_GEOGRAPHY_NAME, ), + ( + { + CONF_API_KEY: "abcde12345", + CONF_CITY: "Beijing", + CONF_STATE: "Beijing", + CONF_COUNTRY: "China", + }, + KeyExpiredError, + {CONF_API_KEY: "invalid_api_key"}, + INTEGRATION_TYPE_GEOGRAPHY_NAME, + ), + ( + { + CONF_API_KEY: "abcde12345", + CONF_CITY: "Beijing", + CONF_STATE: "Beijing", + CONF_COUNTRY: "China", + }, + UnauthorizedError, + {CONF_API_KEY: "invalid_api_key"}, + INTEGRATION_TYPE_GEOGRAPHY_NAME, + ), ( { CONF_API_KEY: "abcde12345", From 4ceda65889ff0cd8e3959f4d3df386cab8f87838 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sat, 16 Jul 2022 22:26:22 +0200 Subject: [PATCH 2633/3516] Update pyotgw to 2.0.0 (#75285) * Update pyotgw to 2.0.0 * Include updated tests --- .../components/opentherm_gw/__init__.py | 4 +- .../components/opentherm_gw/config_flow.py | 4 +- .../components/opentherm_gw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../opentherm_gw/test_config_flow.py | 42 +++++++++++++------ tests/components/opentherm_gw/test_init.py | 4 +- 7 files changed, 38 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 76f8341734c..7c27eeceede 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -416,7 +416,7 @@ class OpenThermGatewayDevice: self.status = {} self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update" self.options_update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_options_update" - self.gateway = pyotgw.pyotgw() + self.gateway = pyotgw.OpenThermGateway() self.gw_version = None async def cleanup(self, event=None): @@ -427,7 +427,7 @@ class OpenThermGatewayDevice: async def connect_and_subscribe(self): """Connect to serial device and subscribe report handler.""" - self.status = await self.gateway.connect(self.hass.loop, self.device_path) + self.status = await self.gateway.connect(self.device_path) version_string = self.status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) self.gw_version = version_string[18:] if version_string else None _LOGGER.debug( diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 1d66d6e2069..3f91496adab 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -59,8 +59,8 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def test_connection(): """Try to connect to the OpenTherm Gateway.""" - otgw = pyotgw.pyotgw() - status = await otgw.connect(self.hass.loop, device) + otgw = pyotgw.OpenThermGateway() + status = await otgw.connect(device) await otgw.disconnect() return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 7aa19224020..dfb60413721 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -2,7 +2,7 @@ "domain": "opentherm_gw", "name": "OpenTherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", - "requirements": ["pyotgw==1.1b1"], + "requirements": ["pyotgw==2.0.0"], "codeowners": ["@mvn23"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index ef293dcdcae..42360e81184 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1718,7 +1718,7 @@ pyopnsense==0.2.0 pyoppleio==1.0.5 # homeassistant.components.opentherm_gw -pyotgw==1.1b1 +pyotgw==2.0.0 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b1ae82fd16..8d6fa41d253 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1173,7 +1173,7 @@ pyopenuv==2022.04.0 pyopnsense==0.2.0 # homeassistant.components.opentherm_gw -pyotgw==1.1b1 +pyotgw==2.0.0 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index 8244ece6b9f..46d53bc54b5 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -43,10 +43,12 @@ async def test_form_user(hass): "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS + "pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS ) as mock_pyotgw_connect, patch( - "pyotgw.pyotgw.disconnect", return_value=None - ) as mock_pyotgw_disconnect: + "pyotgw.OpenThermGateway.disconnect", return_value=None + ) as mock_pyotgw_disconnect, patch( + "pyotgw.status.StatusManager._process_updates", return_value=None + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} ) @@ -75,10 +77,12 @@ async def test_form_import(hass): "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS + "pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS ) as mock_pyotgw_connect, patch( - "pyotgw.pyotgw.disconnect", return_value=None - ) as mock_pyotgw_disconnect: + "pyotgw.OpenThermGateway.disconnect", return_value=None + ) as mock_pyotgw_disconnect, patch( + "pyotgw.status.StatusManager._process_updates", return_value=None + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -117,10 +121,12 @@ async def test_form_duplicate_entries(hass): "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS + "pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS ) as mock_pyotgw_connect, patch( - "pyotgw.pyotgw.disconnect", return_value=None - ) as mock_pyotgw_disconnect: + "pyotgw.OpenThermGateway.disconnect", return_value=None + ) as mock_pyotgw_disconnect, patch( + "pyotgw.status.StatusManager._process_updates", return_value=None + ): result1 = await hass.config_entries.flow.async_configure( flow1["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} ) @@ -148,8 +154,10 @@ async def test_form_connection_timeout(hass): ) with patch( - "pyotgw.pyotgw.connect", side_effect=(asyncio.TimeoutError) - ) as mock_connect: + "pyotgw.OpenThermGateway.connect", side_effect=(asyncio.TimeoutError) + ) as mock_connect, patch( + "pyotgw.status.StatusManager._process_updates", return_value=None + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "socket://192.0.2.254:1234"}, @@ -166,7 +174,11 @@ async def test_form_connection_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("pyotgw.pyotgw.connect", side_effect=(SerialException)) as mock_connect: + with patch( + "pyotgw.OpenThermGateway.connect", side_effect=(SerialException) + ) as mock_connect, patch( + "pyotgw.status.StatusManager._process_updates", return_value=None + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} ) @@ -196,7 +208,11 @@ async def test_options_migration(hass): with patch( "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.connect_and_subscribe", return_value=True, - ), patch("homeassistant.components.opentherm_gw.async_setup", return_value=True): + ), patch( + "homeassistant.components.opentherm_gw.async_setup", return_value=True + ), patch( + "pyotgw.status.StatusManager._process_updates", return_value=None + ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/opentherm_gw/test_init.py b/tests/components/opentherm_gw/test_init.py index 554f58fd81b..7e16805c683 100644 --- a/tests/components/opentherm_gw/test_init.py +++ b/tests/components/opentherm_gw/test_init.py @@ -34,7 +34,7 @@ async def test_device_registry_insert(hass): with patch( "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup", return_value=None, - ), patch("pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS): + ), patch("pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS): await setup.async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() @@ -62,7 +62,7 @@ async def test_device_registry_update(hass): with patch( "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup", return_value=None, - ), patch("pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS_UPD): + ), patch("pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS_UPD): await setup.async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() From 2f92c47fe368329d0905b9cb5f0288f7de442eb9 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sun, 17 Jul 2022 05:07:47 +0800 Subject: [PATCH 2634/3516] Apply filter to libav.hls logging namespace (#75330) --- homeassistant/components/stream/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index ef68ea7bcae..f0b4ed99654 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -187,14 +187,15 @@ def filter_libav_logging() -> None: return logging.getLogger(__name__).isEnabledFor(logging.DEBUG) for logging_namespace in ( - "libav.mp4", + "libav.NULL", "libav.h264", "libav.hevc", + "libav.hls", + "libav.mp4", + "libav.mpegts", "libav.rtsp", "libav.tcp", "libav.tls", - "libav.mpegts", - "libav.NULL", ): logging.getLogger(logging_namespace).addFilter(libav_filter) From 59c99e0d60d21b8bc110a436038227c399282671 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sat, 16 Jul 2022 18:28:17 -0400 Subject: [PATCH 2635/3516] Improve UniFi Protect unauth handling (#75269) --- homeassistant/components/unifiprotect/data.py | 12 ++++++--- tests/components/unifiprotect/test_init.py | 27 ++++++++++++++----- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 9e0783a99b1..d4140759a7b 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -72,6 +72,7 @@ class ProtectData: self._pending_camera_ids: set[str] = set() self._unsub_interval: CALLBACK_TYPE | None = None self._unsub_websocket: CALLBACK_TYPE | None = None + self._auth_failures = 0 self.last_update_success = False self.api = protect @@ -117,9 +118,13 @@ class ProtectData: try: updates = await self.api.update(force=force) except NotAuthorized: - await self.async_stop() - _LOGGER.exception("Reauthentication required") - self._entry.async_start_reauth(self._hass) + if self._auth_failures < 10: + _LOGGER.exception("Auth error while updating") + self._auth_failures += 1 + else: + await self.async_stop() + _LOGGER.exception("Reauthentication required") + self._entry.async_start_reauth(self._hass) self.last_update_success = False except ClientError: if self.last_update_success: @@ -129,6 +134,7 @@ class ProtectData: self._async_process_updates(self.api.bootstrap) else: self.last_update_success = True + self._auth_failures = 0 self._async_process_updates(updates) @callback diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index f6f0645df18..9392caa30ac 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -9,14 +9,18 @@ import aiohttp from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient from pyunifiprotect.data import NVR, Bootstrap, Light -from homeassistant.components.unifiprotect.const import CONF_DISABLE_RTSP, DOMAIN +from homeassistant.components.unifiprotect.const import ( + CONF_DISABLE_RTSP, + DEFAULT_SCAN_INTERVAL, + DOMAIN, +) from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from . import _patch_discovery -from .utils import MockUFPFixture, init_entry +from .utils import MockUFPFixture, init_entry, time_changed from tests.common import MockConfigEntry @@ -145,12 +149,23 @@ async def test_setup_failed_update(hass: HomeAssistant, ufp: MockUFPFixture): async def test_setup_failed_update_reauth(hass: HomeAssistant, ufp: MockUFPFixture): """Test setup of unifiprotect entry with update that gives unauthroized error.""" - ufp.api.update = AsyncMock(side_effect=NotAuthorized) - await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() - assert ufp.entry.state == ConfigEntryState.SETUP_RETRY - assert ufp.api.update.called + assert ufp.entry.state == ConfigEntryState.LOADED + + # reauth should not be triggered until there are 10 auth failures in a row + # to verify it is not transient + ufp.api.update = AsyncMock(side_effect=NotAuthorized) + for _ in range(10): + await time_changed(hass, DEFAULT_SCAN_INTERVAL) + assert len(hass.config_entries.flow._progress) == 0 + + assert ufp.api.update.call_count == 10 + assert ufp.entry.state == ConfigEntryState.LOADED + + await time_changed(hass, DEFAULT_SCAN_INTERVAL) + assert ufp.api.update.call_count == 11 + assert len(hass.config_entries.flow._progress) == 1 async def test_setup_failed_error(hass: HomeAssistant, ufp: MockUFPFixture): From 79a09409321308da522df9e0a652dfd7b5fca1b6 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 17 Jul 2022 00:25:56 +0000 Subject: [PATCH 2636/3516] [ci skip] Translation update --- .../accuweather/translations/pt.json | 3 ++ .../components/airtouch4/translations/pt.json | 11 +++++++ .../airvisual/translations/sensor.pt.json | 6 ++++ .../alarm_control_panel/translations/pt.json | 1 + .../components/apple_tv/translations/pt.json | 2 +- .../components/axis/translations/pt.json | 1 + .../azure_devops/translations/pt.json | 3 +- .../binary_sensor/translations/pt.json | 3 +- .../components/blebox/translations/pt.json | 1 + .../components/bond/translations/pt.json | 1 + .../components/co2signal/translations/pt.json | 15 +++++++++- .../components/elkm1/translations/pt.json | 6 ++-- .../evil_genius_labs/translations/pt.json | 3 +- .../components/generic/translations/pt.json | 1 + .../components/group/translations/pt.json | 15 +++++++++- .../homeassistant/translations/pt.json | 1 + .../components/homekit/translations/pt.json | 5 ++++ .../components/hue/translations/pt.json | 3 ++ .../components/meater/translations/pt.json | 1 + .../components/netgear/translations/pt.json | 7 +++++ .../overkiz/translations/sensor.id.json | 3 +- .../components/recorder/translations/pt.json | 8 +++++ .../components/scrape/translations/pt.json | 12 +++++++- .../components/season/translations/pt.json | 9 +++++- .../season/translations/sensor.pt.json | 4 +-- .../components/sense/translations/pt.json | 3 ++ .../components/sensor/translations/he.json | 2 ++ .../components/sensor/translations/pt.json | 2 +- .../components/sia/translations/pt.json | 10 +++++++ .../components/sql/translations/pt.json | 30 +++++++++++++++++++ .../components/timer/translations/pt.json | 2 +- .../ukraine_alarm/translations/pt.json | 7 +++++ .../components/verisure/translations/id.json | 13 +++++++- .../components/verisure/translations/pl.json | 15 +++++++++- .../xiaomi_aqara/translations/pt.json | 2 +- .../zodiac/translations/sensor.pt.json | 12 ++++---- 36 files changed, 199 insertions(+), 24 deletions(-) create mode 100644 homeassistant/components/airtouch4/translations/pt.json create mode 100644 homeassistant/components/recorder/translations/pt.json create mode 100644 homeassistant/components/sql/translations/pt.json create mode 100644 homeassistant/components/ukraine_alarm/translations/pt.json diff --git a/homeassistant/components/accuweather/translations/pt.json b/homeassistant/components/accuweather/translations/pt.json index 08d419ec9b2..8b5d307e722 100644 --- a/homeassistant/components/accuweather/translations/pt.json +++ b/homeassistant/components/accuweather/translations/pt.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, + "create_entry": { + "default": "Alguns sensores n\u00e3o s\u00e3o ativados por defeito. Podem ser ativados no registo da entidade ap\u00f3s a configura\u00e7\u00e3o da integra\u00e7\u00e3o.\nA previs\u00e3o do tempo n\u00e3o est\u00e1 ativada por defeito. Pode ativ\u00e1-la nas op\u00e7\u00f5es de integra\u00e7\u00e3o." + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_api_key": "Chave de API inv\u00e1lida" diff --git a/homeassistant/components/airtouch4/translations/pt.json b/homeassistant/components/airtouch4/translations/pt.json new file mode 100644 index 00000000000..4e8578a0a28 --- /dev/null +++ b/homeassistant/components/airtouch4/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Anfitri\u00e3o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.pt.json b/homeassistant/components/airvisual/translations/sensor.pt.json index 08a2e0c018e..f7a976b51ed 100644 --- a/homeassistant/components/airvisual/translations/sensor.pt.json +++ b/homeassistant/components/airvisual/translations/sensor.pt.json @@ -1,5 +1,11 @@ { "state": { + "airvisual__pollutant_label": { + "co": "Mon\u00f3xido de carbono", + "n2": "Di\u00f3xido de nitrog\u00e9nio", + "o3": "Ozono", + "p1": "PM10" + }, "airvisual__pollutant_level": { "moderate": "Moderado" } diff --git a/homeassistant/components/alarm_control_panel/translations/pt.json b/homeassistant/components/alarm_control_panel/translations/pt.json index e4293b81731..fb8cce10c93 100644 --- a/homeassistant/components/alarm_control_panel/translations/pt.json +++ b/homeassistant/components/alarm_control_panel/translations/pt.json @@ -13,6 +13,7 @@ "armed_custom_bypass": "Armado com desvio personalizado", "armed_home": "Armado Casa", "armed_night": "Armado noite", + "armed_vacation": "Armado f\u00e9rias", "arming": "A armar", "disarmed": "Desarmado", "disarming": "A desarmar", diff --git a/homeassistant/components/apple_tv/translations/pt.json b/homeassistant/components/apple_tv/translations/pt.json index deec60a19ea..e54e421caa5 100644 --- a/homeassistant/components/apple_tv/translations/pt.json +++ b/homeassistant/components/apple_tv/translations/pt.json @@ -40,7 +40,7 @@ "data": { "device_input": "Dispositivo" }, - "description": "Comece por introduzir o nome do dispositivo (por exemplo, Cozinha ou Quarto) ou o endere\u00e7o IP da Apple TV que pretende adicionar. Se algum dispositivo foi automaticamente encontrado na sua rede, ele \u00e9 mostrado abaixo.\n\nSe n\u00e3o conseguir ver o seu dispositivo ou se tiver algum problema, tente especificar o endere\u00e7o IP do dispositivo.\n\n{devices}", + "description": "Comece por introduzir o nome do dispositivo (por exemplo, Cozinha ou Quarto) ou o endere\u00e7o IP da Apple TV que pretende adicionar. Se algum dispositivo foi automaticamente encontrado na sua rede, ele \u00e9 mostrado abaixo.\n\nSe n\u00e3o conseguir ver o seu dispositivo ou se tiver algum problema, tente especificar o endere\u00e7o IP do dispositivo.", "title": "Configure uma nova Apple TV" } } diff --git a/homeassistant/components/axis/translations/pt.json b/homeassistant/components/axis/translations/pt.json index 8ba642263a4..b74ecb2dc44 100644 --- a/homeassistant/components/axis/translations/pt.json +++ b/homeassistant/components/axis/translations/pt.json @@ -10,6 +10,7 @@ "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/azure_devops/translations/pt.json b/homeassistant/components/azure_devops/translations/pt.json index 2af1f548447..b09f2cceda7 100644 --- a/homeassistant/components/azure_devops/translations/pt.json +++ b/homeassistant/components/azure_devops/translations/pt.json @@ -7,6 +7,7 @@ "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" - } + }, + "flow_title": "{project_url}" } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/pt.json b/homeassistant/components/binary_sensor/translations/pt.json index 470acfff5db..9eba64372d4 100644 --- a/homeassistant/components/binary_sensor/translations/pt.json +++ b/homeassistant/components/binary_sensor/translations/pt.json @@ -186,7 +186,8 @@ "on": "Detectado" }, "update": { - "off": "Actualizado" + "off": "Actualizado", + "on": "Atualiza\u00e7\u00e3o dispon\u00edvel" }, "vibration": { "off": "Limpo", diff --git a/homeassistant/components/blebox/translations/pt.json b/homeassistant/components/blebox/translations/pt.json index 9c2be6fd04b..8b581a984e7 100644 --- a/homeassistant/components/blebox/translations/pt.json +++ b/homeassistant/components/blebox/translations/pt.json @@ -8,6 +8,7 @@ "unknown": "Erro inesperado", "unsupported_version": "O dispositivo BleBox possui firmware desatualizado. Atualize-o primeiro." }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/bond/translations/pt.json b/homeassistant/components/bond/translations/pt.json index 2173d698932..828e7c55baf 100644 --- a/homeassistant/components/bond/translations/pt.json +++ b/homeassistant/components/bond/translations/pt.json @@ -8,6 +8,7 @@ "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, + "flow_title": "{name} ({host})", "step": { "confirm": { "data": { diff --git a/homeassistant/components/co2signal/translations/pt.json b/homeassistant/components/co2signal/translations/pt.json index cf6bc7f5bc4..6d105e40d36 100644 --- a/homeassistant/components/co2signal/translations/pt.json +++ b/homeassistant/components/co2signal/translations/pt.json @@ -6,8 +6,21 @@ "step": { "coordinates": { "data": { - "latitude": "Latitude" + "latitude": "Latitude", + "longitude": "Longitude" } + }, + "country": { + "data": { + "country_code": "C\u00f3digo do Pa\u00eds" + } + }, + "user": { + "data": { + "api_key": "Token de Acesso", + "location": "Obter dados para" + }, + "description": "Visite https://co2signal.com/ para solicitar um token." } } } diff --git a/homeassistant/components/elkm1/translations/pt.json b/homeassistant/components/elkm1/translations/pt.json index 6b770832a24..08fe97d2354 100644 --- a/homeassistant/components/elkm1/translations/pt.json +++ b/homeassistant/components/elkm1/translations/pt.json @@ -10,12 +10,14 @@ "data": { "password": "Palavra-passe", "username": "Nome de Utilizador" - } + }, + "title": "Ligar ao Controlo Elk-M1" }, "manual_connection": { "data": { "username": "Nome de Utilizador" - } + }, + "title": "Ligar ao Controlo Elk-M1" } } } diff --git a/homeassistant/components/evil_genius_labs/translations/pt.json b/homeassistant/components/evil_genius_labs/translations/pt.json index f13cad90edc..bf245f20e6e 100644 --- a/homeassistant/components/evil_genius_labs/translations/pt.json +++ b/homeassistant/components/evil_genius_labs/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "Falha na liga\u00e7\u00e3o" + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "timeout": "Tempo limite para estabelecer liga\u00e7\u00e3o" }, "step": { "user": { diff --git a/homeassistant/components/generic/translations/pt.json b/homeassistant/components/generic/translations/pt.json index b59623f6607..06abd0a7dfa 100644 --- a/homeassistant/components/generic/translations/pt.json +++ b/homeassistant/components/generic/translations/pt.json @@ -13,6 +13,7 @@ "step": { "init": { "data": { + "authentication": "Autentica\u00e7\u00e3o", "password": "Palavra-passe", "username": "Nome de Utilizador" } diff --git a/homeassistant/components/group/translations/pt.json b/homeassistant/components/group/translations/pt.json index 57c8a41e630..0d0e11d7ffe 100644 --- a/homeassistant/components/group/translations/pt.json +++ b/homeassistant/components/group/translations/pt.json @@ -1,10 +1,23 @@ { "config": { "step": { + "binary_sensor": { + "title": "Adicionar Grupo" + }, + "fan": { + "title": "Adicionar Grupo" + }, + "light": { + "title": "Adicionar Grupo" + }, "media_player": { "data": { "entities": "Membros" - } + }, + "title": "Adicionar Grupo" + }, + "switch": { + "title": "Adicionar Grupo" } } }, diff --git a/homeassistant/components/homeassistant/translations/pt.json b/homeassistant/components/homeassistant/translations/pt.json index 13fd384d6a2..30f84823c6b 100644 --- a/homeassistant/components/homeassistant/translations/pt.json +++ b/homeassistant/components/homeassistant/translations/pt.json @@ -10,6 +10,7 @@ "os_version": "Vers\u00e3o do Sistema Operativo", "python_version": "Vers\u00e3o Python", "timezone": "Fuso hor\u00e1rio", + "user": "Utilizador", "version": "Vers\u00e3o", "virtualenv": "Ambiente Virtual" } diff --git a/homeassistant/components/homekit/translations/pt.json b/homeassistant/components/homekit/translations/pt.json index 4e25e3b691c..931dfcdd1c9 100644 --- a/homeassistant/components/homekit/translations/pt.json +++ b/homeassistant/components/homekit/translations/pt.json @@ -20,6 +20,11 @@ "cameras": { "title": "Selecione o codec de v\u00eddeo da c\u00e2mera." }, + "exclude": { + "data": { + "entities": "Entidades" + } + }, "init": { "data": { "domains": "Dom\u00ednios a incluir", diff --git a/homeassistant/components/hue/translations/pt.json b/homeassistant/components/hue/translations/pt.json index 8f51f8d74e9..9b982d6c84e 100644 --- a/homeassistant/components/hue/translations/pt.json +++ b/homeassistant/components/hue/translations/pt.json @@ -36,6 +36,9 @@ "trigger_subtype": { "button_1": "Primeiro bot\u00e3o", "button_4": "Quarto bot\u00e3o" + }, + "trigger_type": { + "short_release": "Bot\u00e3o \"{subtype}\" solto ap\u00f3s press\u00e3o curta" } }, "options": { diff --git a/homeassistant/components/meater/translations/pt.json b/homeassistant/components/meater/translations/pt.json index 69bade43888..bc859189b94 100644 --- a/homeassistant/components/meater/translations/pt.json +++ b/homeassistant/components/meater/translations/pt.json @@ -11,6 +11,7 @@ }, "user": { "data": { + "password": "Palavra-passe", "username": "Nome de Utilizador" } } diff --git a/homeassistant/components/netgear/translations/pt.json b/homeassistant/components/netgear/translations/pt.json index ce8a9287272..ece60e5e010 100644 --- a/homeassistant/components/netgear/translations/pt.json +++ b/homeassistant/components/netgear/translations/pt.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.id.json b/homeassistant/components/overkiz/translations/sensor.id.json index bf4703507f8..0d0cc6ab7ed 100644 --- a/homeassistant/components/overkiz/translations/sensor.id.json +++ b/homeassistant/components/overkiz/translations/sensor.id.json @@ -39,7 +39,8 @@ }, "overkiz__three_way_handle_direction": { "closed": "Tutup", - "open": "Buka" + "open": "Buka", + "tilt": "Miring" } } } \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/pt.json b/homeassistant/components/recorder/translations/pt.json new file mode 100644 index 00000000000..b06f3d16e60 --- /dev/null +++ b/homeassistant/components/recorder/translations/pt.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "database_engine": "Motor de Base de Dados", + "database_version": "Vers\u00e3o de Base de Dados" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/pt.json b/homeassistant/components/scrape/translations/pt.json index 00c65c09cdf..003da0eed66 100644 --- a/homeassistant/components/scrape/translations/pt.json +++ b/homeassistant/components/scrape/translations/pt.json @@ -6,7 +6,17 @@ "step": { "user": { "data": { - "name": "Nome" + "name": "Nome", + "unit_of_measurement": "Unidade de Medida" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Unidade de Medida" } } } diff --git a/homeassistant/components/season/translations/pt.json b/homeassistant/components/season/translations/pt.json index b7bb07e9522..c8d0ddf9f70 100644 --- a/homeassistant/components/season/translations/pt.json +++ b/homeassistant/components/season/translations/pt.json @@ -1,7 +1,14 @@ { "config": { "abort": { - "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado." + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "type": "Defini\u00e7\u00e3o do tipo de esta\u00e7\u00e3o" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.pt.json b/homeassistant/components/season/translations/sensor.pt.json index e30461da7d2..3f157f43c79 100644 --- a/homeassistant/components/season/translations/sensor.pt.json +++ b/homeassistant/components/season/translations/sensor.pt.json @@ -1,9 +1,9 @@ { "state": { "season__season": { - "autumn": "Outono ", + "autumn": "Outono", "spring": "Primavera", - "summer": "Ver\u00e3o ", + "summer": "Ver\u00e3o", "winter": "Inverno" }, "season__season__": { diff --git a/homeassistant/components/sense/translations/pt.json b/homeassistant/components/sense/translations/pt.json index d429d0af1e8..1b8b4e2dc18 100644 --- a/homeassistant/components/sense/translations/pt.json +++ b/homeassistant/components/sense/translations/pt.json @@ -10,6 +10,9 @@ }, "step": { "reauth_validate": { + "data": { + "password": "Palavra-passe" + }, "title": "Reautenticar integra\u00e7\u00e3o" }, "user": { diff --git a/homeassistant/components/sensor/translations/he.json b/homeassistant/components/sensor/translations/he.json index 819da5aad0c..7f2dd33a023 100644 --- a/homeassistant/components/sensor/translations/he.json +++ b/homeassistant/components/sensor/translations/he.json @@ -2,10 +2,12 @@ "device_automation": { "condition_type": { "is_apparent_power": "\u05d4\u05e2\u05d5\u05e6\u05de\u05d4 \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name} \u05de\u05e1\u05ea\u05de\u05e0\u05ea", + "is_battery_level": "\u05e8\u05de\u05ea \u05d4\u05e1\u05d5\u05dc\u05dc\u05d4 \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea \u05e9\u05dc {entity_name}", "is_reactive_power": "\u05d4\u05e1\u05e4\u05e7 \u05ea\u05d2\u05d5\u05d1\u05ea\u05d9 \u05e0\u05d5\u05db\u05d7\u05d9 {entity_name}" }, "trigger_type": { "apparent_power": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d4\u05e1\u05e4\u05e7 \u05dc\u05db\u05d0\u05d5\u05e8\u05d4", + "battery_level": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05de\u05ea \u05d4\u05e1\u05d5\u05dc\u05dc\u05d4", "reactive_power": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d4\u05e1\u05e4\u05e7 \u05ea\u05d2\u05d5\u05d1\u05ea\u05d9" } }, diff --git a/homeassistant/components/sensor/translations/pt.json b/homeassistant/components/sensor/translations/pt.json index 0e162b9c7ca..864d49f2373 100644 --- a/homeassistant/components/sensor/translations/pt.json +++ b/homeassistant/components/sensor/translations/pt.json @@ -18,7 +18,7 @@ "illuminance": "ilumin\u00e2ncia {entity_name}", "power": "pot\u00eancia {entity_name}", "pressure": "press\u00e3o {entity_name}", - "signal_strength": "for\u00e7a do sinal de {entity_name}", + "signal_strength": "Altera\u00e7\u00e3o da intensidade do sinal de {entity_name}", "temperature": "temperatura de {entity_name}", "value": "valor {entity_name}" } diff --git a/homeassistant/components/sia/translations/pt.json b/homeassistant/components/sia/translations/pt.json index 0077ceddd46..21f8130bfc5 100644 --- a/homeassistant/components/sia/translations/pt.json +++ b/homeassistant/components/sia/translations/pt.json @@ -7,5 +7,15 @@ } } } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Ignorar a verifica\u00e7\u00e3o de carimbo de data/hora dos eventos SIA", + "zones": "N\u00famero de zonas da conta" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sql/translations/pt.json b/homeassistant/components/sql/translations/pt.json new file mode 100644 index 00000000000..f2cf791e9fe --- /dev/null +++ b/homeassistant/components/sql/translations/pt.json @@ -0,0 +1,30 @@ +{ + "config": { + "step": { + "user": { + "data": { + "unit_of_measurement": "Unidade de Medida" + }, + "data_description": { + "unit_of_measurement": "Unidade de Medida (opcional)" + } + } + } + }, + "options": { + "error": { + "query_invalid": "Busca SQL In\u00e1lida" + }, + "step": { + "init": { + "data": { + "column": "Coluna", + "unit_of_measurement": "Unidade de Medida" + }, + "data_description": { + "unit_of_measurement": "Unidade de Medida (opcional)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/timer/translations/pt.json b/homeassistant/components/timer/translations/pt.json index a49163aed8c..1f506228973 100644 --- a/homeassistant/components/timer/translations/pt.json +++ b/homeassistant/components/timer/translations/pt.json @@ -1,7 +1,7 @@ { "state": { "_": { - "active": "ativo", + "active": "Ativo", "idle": "Em espera", "paused": "Em pausa" } diff --git a/homeassistant/components/ukraine_alarm/translations/pt.json b/homeassistant/components/ukraine_alarm/translations/pt.json new file mode 100644 index 00000000000..e6c831ab07c --- /dev/null +++ b/homeassistant/components/ukraine_alarm/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "timeout": "Tempo limite para estabelecer liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/id.json b/homeassistant/components/verisure/translations/id.json index 5c9badda341..c8b78a7282a 100644 --- a/homeassistant/components/verisure/translations/id.json +++ b/homeassistant/components/verisure/translations/id.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Autentikasi tidak valid", - "unknown": "Kesalahan yang tidak diharapkan" + "unknown": "Kesalahan yang tidak diharapkan", + "unknown_mfa": "Terjadi kesalahan yang tidak diketahui dalam pengaturan MFA" }, "step": { "installation": { @@ -15,6 +16,11 @@ }, "description": "Home Assistant menemukan beberapa instalasi Verisure di akun My Pages. Pilih instalasi untuk ditambahkan ke Home Assistant." }, + "mfa": { + "data": { + "code": "Kode Verifikasi" + } + }, "reauth_confirm": { "data": { "description": "Autentikasi ulang dengan akun Verisure My Pages Anda.", @@ -22,6 +28,11 @@ "password": "Kata Sandi" } }, + "reauth_mfa": { + "data": { + "code": "Kode Verifikasi" + } + }, "user": { "data": { "description": "Masuk dengan akun Verisure My Pages Anda.", diff --git a/homeassistant/components/verisure/translations/pl.json b/homeassistant/components/verisure/translations/pl.json index baaf61f60c3..dcdf566f9d4 100644 --- a/homeassistant/components/verisure/translations/pl.json +++ b/homeassistant/components/verisure/translations/pl.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Niepoprawne uwierzytelnienie", - "unknown": "Nieoczekiwany b\u0142\u0105d" + "unknown": "Nieoczekiwany b\u0142\u0105d", + "unknown_mfa": "Podczas konfigurowania us\u0142ugi MFA wyst\u0105pi\u0142 nieznany b\u0142\u0105d" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant znalaz\u0142 wiele instalacji Verisure na Twoim koncie. Wybierz instalacj\u0119, kt\u00f3r\u0105 chcesz doda\u0107 do Home Assistanta." }, + "mfa": { + "data": { + "code": "Kod weryfikacyjny", + "description": "Twoje konto ma w\u0142\u0105czon\u0105 weryfikacj\u0119 dwuetapow\u0105. Wprowad\u017a kod weryfikacyjny wys\u0142any przez Verisure." + } + }, "reauth_confirm": { "data": { "description": "Ponownie uwierzytelnij za pomoc\u0105 konta Verisure.", @@ -22,6 +29,12 @@ "password": "Has\u0142o" } }, + "reauth_mfa": { + "data": { + "code": "Kod weryfikacyjny", + "description": "Twoje konto ma w\u0142\u0105czon\u0105 weryfikacj\u0119 dwuetapow\u0105. Wprowad\u017a kod weryfikacyjny wys\u0142any przez Verisure." + } + }, "user": { "data": { "description": "Zaloguj si\u0119 na swoje konto Verisure.", diff --git a/homeassistant/components/xiaomi_aqara/translations/pt.json b/homeassistant/components/xiaomi_aqara/translations/pt.json index a800e4d57c6..1b7f8e1c0b9 100644 --- a/homeassistant/components/xiaomi_aqara/translations/pt.json +++ b/homeassistant/components/xiaomi_aqara/translations/pt.json @@ -17,7 +17,7 @@ "data": { "name": "Nome da Gateway" }, - "description": "A chave (palavra-passe) pode ser recuperada usando este tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Se a chave n\u00e3o for fornecida, apenas os sensores estar\u00e3o acess\u00edveis" + "description": "A chave (palavra-passe) pode ser obtida usando este tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Se a chave n\u00e3o for fornecida, apenas os sensores estar\u00e3o acess\u00edveis" }, "user": { "data": { diff --git a/homeassistant/components/zodiac/translations/sensor.pt.json b/homeassistant/components/zodiac/translations/sensor.pt.json index b37a6886df0..a5e943a5daf 100644 --- a/homeassistant/components/zodiac/translations/sensor.pt.json +++ b/homeassistant/components/zodiac/translations/sensor.pt.json @@ -1,16 +1,16 @@ { "state": { "zodiac__sign": { - "aquarius": "Aqu\u00e1rio ", - "aries": "Carneiro ", - "cancer": "Caranguejo ", + "aquarius": "Aqu\u00e1rio", + "aries": "Carneiro", + "cancer": "Caranguejo", "capricorn": "Capric\u00f3rnio", - "gemini": "G\u00e9meos ", - "leo": "Le\u00e3o ", + "gemini": "G\u00e9meos", + "leo": "Le\u00e3o", "libra": "Balan\u00e7a", "pisces": "Peixes", "sagittarius": "Sagit\u00e1rio", - "scorpio": "Escorpi\u00e3o ", + "scorpio": "Escorpi\u00e3o", "taurus": "Touro", "virgo": "Virgem" } From ba8a530d19dc3e93602275961de46d700611f4a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 16 Jul 2022 23:14:23 -0500 Subject: [PATCH 2637/3516] Use shared bluetooth models for BluetoothServiceInfo (#75322) --- .../helpers/service_info/bluetooth.py | 62 +------------------ homeassistant/package_constraints.txt | 1 + pyproject.toml | 1 + requirements.txt | 1 + 4 files changed, 6 insertions(+), 59 deletions(-) diff --git a/homeassistant/helpers/service_info/bluetooth.py b/homeassistant/helpers/service_info/bluetooth.py index 003a4228d80..d4d74a45be4 100644 --- a/homeassistant/helpers/service_info/bluetooth.py +++ b/homeassistant/helpers/service_info/bluetooth.py @@ -1,60 +1,4 @@ """The bluetooth integration service info.""" -from __future__ import annotations - -import dataclasses -from functools import cached_property -from typing import TYPE_CHECKING - -from homeassistant.data_entry_flow import BaseServiceInfo - -if TYPE_CHECKING: - from bleak.backends.device import BLEDevice - from bleak.backends.scanner import AdvertisementData - - -@dataclasses.dataclass -class BluetoothServiceInfo(BaseServiceInfo): - """Prepared info from bluetooth entries.""" - - name: str - address: str - rssi: int - manufacturer_data: dict[int, bytes] - service_data: dict[str, bytes] - service_uuids: list[str] - source: str - - @classmethod - def from_advertisement( - cls, device: BLEDevice, advertisement_data: AdvertisementData, source: str - ) -> BluetoothServiceInfo: - """Create a BluetoothServiceInfo from an advertisement.""" - return cls( - name=advertisement_data.local_name or device.name or device.address, - address=device.address, - rssi=device.rssi, - manufacturer_data=advertisement_data.manufacturer_data, - service_data=advertisement_data.service_data, - service_uuids=advertisement_data.service_uuids, - source=source, - ) - - @cached_property - def manufacturer(self) -> str | None: - """Convert manufacturer data to a string.""" - from bleak.backends.device import ( # pylint: disable=import-outside-toplevel - MANUFACTURERS, - ) - - for manufacturer in self.manufacturer_data: - if manufacturer in MANUFACTURERS: - name: str = MANUFACTURERS[manufacturer] - return name - return None - - @cached_property - def manufacturer_id(self) -> int | None: - """Get the first manufacturer id.""" - for manufacturer in self.manufacturer_data: - return manufacturer - return None +from home_assistant_bluetooth import ( # pylint: disable=unused-import # noqa: F401 + BluetoothServiceInfo, +) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d19b95fa2d3..0d81d485829 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -15,6 +15,7 @@ ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.1 +home-assistant-bluetooth==1.3.0 home-assistant-frontend==20220707.1 httpx==0.23.0 ifaddr==0.1.7 diff --git a/pyproject.toml b/pyproject.toml index 41059773977..9b4c99c1152 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.23.0", + "home-assistant-bluetooth==1.3.0", "ifaddr==0.1.7", "jinja2==3.1.2", "lru-dict==1.1.8", diff --git a/requirements.txt b/requirements.txt index 84d8711753e..ea29bc59435 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ bcrypt==3.1.7 certifi>=2021.5.30 ciso8601==2.2.0 httpx==0.23.0 +home-assistant-bluetooth==1.3.0 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 From 2eebda63fdde17072b126c0d4b2de3d9c86b7cfa Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sun, 17 Jul 2022 22:00:01 +1000 Subject: [PATCH 2638/3516] Correct docstrings in Advantage Air (#75344) --- .../components/advantage_air/binary_sensor.py | 14 +++++++------- homeassistant/components/advantage_air/cover.py | 4 ++-- homeassistant/components/advantage_air/select.py | 4 ++-- homeassistant/components/advantage_air/switch.py | 2 +- tests/components/advantage_air/test_climate.py | 2 +- tests/components/advantage_air/test_cover.py | 2 +- tests/components/advantage_air/test_select.py | 4 ++-- tests/components/advantage_air/test_switch.py | 2 +- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index bc302e5d4a6..c87bf37ca92 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -21,7 +21,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up AdvantageAir motion platform.""" + """Set up AdvantageAir Binary Sensor platform.""" instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] @@ -39,13 +39,13 @@ async def async_setup_entry( class AdvantageAirFilter(AdvantageAirEntity, BinarySensorEntity): - """Advantage Air Filter.""" + """Advantage Air Filter sensor.""" _attr_device_class = BinarySensorDeviceClass.PROBLEM _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__(self, instance, ac_key): - """Initialize an Advantage Air Filter.""" + """Initialize an Advantage Air Filter sensor.""" super().__init__(instance, ac_key) self._attr_name = f'{self._ac["name"]} filter' self._attr_unique_id = ( @@ -59,12 +59,12 @@ class AdvantageAirFilter(AdvantageAirEntity, BinarySensorEntity): class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): - """Advantage Air Zone Motion.""" + """Advantage Air Zone Motion sensor.""" _attr_device_class = BinarySensorDeviceClass.MOTION def __init__(self, instance, ac_key, zone_key): - """Initialize an Advantage Air Zone Motion.""" + """Initialize an Advantage Air Zone Motion sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} motion' self._attr_unique_id = ( @@ -78,13 +78,13 @@ class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): - """Advantage Air Zone MyZone.""" + """Advantage Air Zone MyZone sensor.""" _attr_entity_registry_enabled_default = False _attr_entity_category = EntityCategory.DIAGNOSTIC def __init__(self, instance, ac_key, zone_key): - """Initialize an Advantage Air Zone MyZone.""" + """Initialize an Advantage Air Zone MyZone sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} myZone' self._attr_unique_id = ( diff --git a/homeassistant/components/advantage_air/cover.py b/homeassistant/components/advantage_air/cover.py index f8240126476..391f39953d2 100644 --- a/homeassistant/components/advantage_air/cover.py +++ b/homeassistant/components/advantage_air/cover.py @@ -40,7 +40,7 @@ async def async_setup_entry( class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): - """Advantage Air Cover Class.""" + """Advantage Air Zone Vent.""" _attr_device_class = CoverDeviceClass.DAMPER _attr_supported_features = ( @@ -50,7 +50,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): ) def __init__(self, instance, ac_key, zone_key): - """Initialize an Advantage Air Cover Class.""" + """Initialize an Advantage Air Zone Vent.""" super().__init__(instance, ac_key, zone_key) self._attr_name = self._zone["name"] self._attr_unique_id = ( diff --git a/homeassistant/components/advantage_air/select.py b/homeassistant/components/advantage_air/select.py index ff2a555bc80..0edae258279 100644 --- a/homeassistant/components/advantage_air/select.py +++ b/homeassistant/components/advantage_air/select.py @@ -15,7 +15,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up AdvantageAir toggle platform.""" + """Set up AdvantageAir select platform.""" instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] @@ -49,7 +49,7 @@ class AdvantageAirMyZone(AdvantageAirEntity, SelectEntity): @property def current_option(self): - """Return the fresh air status.""" + """Return the current MyZone.""" return self._number_to_name[self._ac["myZone"]] async def async_select_option(self, option): diff --git a/homeassistant/components/advantage_air/switch.py b/homeassistant/components/advantage_air/switch.py index 44be859aa63..3c0060c65b3 100644 --- a/homeassistant/components/advantage_air/switch.py +++ b/homeassistant/components/advantage_air/switch.py @@ -17,7 +17,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up AdvantageAir toggle platform.""" + """Set up AdvantageAir switch platform.""" instance = hass.data[ADVANTAGE_AIR_DOMAIN][config_entry.entry_id] diff --git a/tests/components/advantage_air/test_climate.py b/tests/components/advantage_air/test_climate.py index 27f89d1df3e..47073f27fc1 100644 --- a/tests/components/advantage_air/test_climate.py +++ b/tests/components/advantage_air/test_climate.py @@ -34,7 +34,7 @@ from tests.components.advantage_air import ( async def test_climate_async_setup_entry(hass, aioclient_mock): - """Test climate setup.""" + """Test climate platform.""" aioclient_mock.get( TEST_SYSTEM_URL, diff --git a/tests/components/advantage_air/test_cover.py b/tests/components/advantage_air/test_cover.py index fa3abda5138..c638c6a3c87 100644 --- a/tests/components/advantage_air/test_cover.py +++ b/tests/components/advantage_air/test_cover.py @@ -27,7 +27,7 @@ from tests.components.advantage_air import ( async def test_cover_async_setup_entry(hass, aioclient_mock): - """Test climate setup without sensors.""" + """Test cover platform.""" aioclient_mock.get( TEST_SYSTEM_URL, diff --git a/tests/components/advantage_air/test_select.py b/tests/components/advantage_air/test_select.py index 41ed9c407fc..2ba982fc384 100644 --- a/tests/components/advantage_air/test_select.py +++ b/tests/components/advantage_air/test_select.py @@ -19,7 +19,7 @@ from tests.components.advantage_air import ( async def test_select_async_setup_entry(hass, aioclient_mock): - """Test climate setup without sensors.""" + """Test select platform.""" aioclient_mock.get( TEST_SYSTEM_URL, @@ -36,7 +36,7 @@ async def test_select_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 - # Test Select Entity + # Test MyZone Select Entity entity_id = "select.testname_ac_one_myzone" state = hass.states.get(entity_id) assert state diff --git a/tests/components/advantage_air/test_switch.py b/tests/components/advantage_air/test_switch.py index 1d99d7f29c1..41af9e8ff80 100644 --- a/tests/components/advantage_air/test_switch.py +++ b/tests/components/advantage_air/test_switch.py @@ -23,7 +23,7 @@ from tests.components.advantage_air import ( async def test_cover_async_setup_entry(hass, aioclient_mock): - """Test climate setup without sensors.""" + """Test switch platform.""" aioclient_mock.get( TEST_SYSTEM_URL, From 9a27f1437d84e1ab87a8089f910fc14e384581c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Jul 2022 07:25:19 -0500 Subject: [PATCH 2639/3516] Use default encoder when saving storage (#75319) --- homeassistant/util/json.py | 27 ++++++-------------- tests/helpers/test_storage.py | 48 ++++++++++++++++++++++++++++++++++- tests/util/test_json.py | 23 +++++------------ 3 files changed, 62 insertions(+), 36 deletions(-) diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 68273c89743..1413f6d9b15 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -49,13 +49,6 @@ def load_json(filename: str, default: list | dict | None = None) -> list | dict: return {} if default is None else default -def _orjson_encoder(data: Any) -> str: - """JSON encoder that uses orjson.""" - return orjson.dumps( - data, option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS - ).decode("utf-8") - - def _orjson_default_encoder(data: Any) -> str: """JSON encoder that uses orjson with hass defaults.""" return orjson.dumps( @@ -79,21 +72,17 @@ def save_json( """ dump: Callable[[Any], Any] try: - if encoder: - # For backwards compatibility, if they pass in the - # default json encoder we use _orjson_default_encoder - # which is the orjson equivalent to the default encoder. - if encoder is DefaultHASSJSONEncoder: - dump = _orjson_default_encoder - json_data = _orjson_default_encoder(data) + # For backwards compatibility, if they pass in the + # default json encoder we use _orjson_default_encoder + # which is the orjson equivalent to the default encoder. + if encoder and encoder is not DefaultHASSJSONEncoder: # If they pass a custom encoder that is not the # DefaultHASSJSONEncoder, we use the slow path of json.dumps - else: - dump = json.dumps - json_data = json.dumps(data, indent=2, cls=encoder) + dump = json.dumps + json_data = json.dumps(data, indent=2, cls=encoder) else: - dump = _orjson_encoder - json_data = _orjson_encoder(data) + dump = _orjson_default_encoder + json_data = _orjson_default_encoder(data) except TypeError as error: msg = f"Failed to serialize to JSON: {filename}. Bad data at {format_unserializable_data(find_paths_unserializable_data(data, dump=dump))}" _LOGGER.error(msg) diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 53c1b8a4677..ca5cb92bfd5 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta import json +from typing import NamedTuple from unittest.mock import Mock, patch import pytest @@ -13,8 +14,9 @@ from homeassistant.const import ( from homeassistant.core import CoreState from homeassistant.helpers import storage from homeassistant.util import dt +from homeassistant.util.color import RGBColor -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, async_test_home_assistant MOCK_VERSION = 1 MOCK_VERSION_2 = 2 @@ -460,3 +462,47 @@ async def test_changing_delayed_written_data(hass, store, hass_storage): "key": MOCK_KEY, "data": {"hello": "world"}, } + + +async def test_saving_load_round_trip(tmpdir): + """Test saving and loading round trip.""" + loop = asyncio.get_running_loop() + hass = await async_test_home_assistant(loop) + + hass.config.config_dir = await hass.async_add_executor_job( + tmpdir.mkdir, "temp_storage" + ) + + class NamedTupleSubclass(NamedTuple): + """A NamedTuple subclass.""" + + name: str + + nts = NamedTupleSubclass("a") + + data = { + "named_tuple_subclass": nts, + "rgb_color": RGBColor(255, 255, 0), + "set": {1, 2, 3}, + "list": [1, 2, 3], + "tuple": (1, 2, 3), + "dict_with_int": {1: 1, 2: 2}, + "dict_with_named_tuple": {1: nts, 2: nts}, + } + + store = storage.Store( + hass, MOCK_VERSION_2, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1 + ) + await store.async_save(data) + load = await store.async_load() + assert load == { + "dict_with_int": {"1": 1, "2": 2}, + "dict_with_named_tuple": {"1": ["a"], "2": ["a"]}, + "list": [1, 2, 3], + "named_tuple_subclass": ["a"], + "rgb_color": [255, 255, 0], + "set": [1, 2, 3], + "tuple": [1, 2, 3], + } + + await hass.async_stop(force=True) diff --git a/tests/util/test_json.py b/tests/util/test_json.py index 28d321036c5..509c0376fae 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -12,7 +12,6 @@ import pytest from homeassistant.core import Event, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.json import JSONEncoder as DefaultHASSJSONEncoder -from homeassistant.helpers.template import TupleWrapper from homeassistant.util.json import ( SerializationError, find_paths_unserializable_data, @@ -83,23 +82,15 @@ def test_overwrite_and_reload(atomic_writes): def test_save_bad_data(): """Test error from trying to save unserializable data.""" + + class CannotSerializeMe: + """Cannot serialize this.""" + with pytest.raises(SerializationError) as excinfo: - save_json("test4", {"hello": set()}) + save_json("test4", {"hello": CannotSerializeMe()}) - assert ( - "Failed to serialize to JSON: test4. Bad data at $.hello=set()(" - in str(excinfo.value) - ) - - -def test_save_bad_data_tuple_wrapper(): - """Test error from trying to save unserializable data.""" - with pytest.raises(SerializationError) as excinfo: - save_json("test4", {"hello": TupleWrapper(("4", "5"))}) - - assert ( - "Failed to serialize to JSON: test4. Bad data at $.hello=('4', '5')(" - in str(excinfo.value) + assert "Failed to serialize to JSON: test4. Bad data at $.hello=" in str( + excinfo.value ) From d8f3044ffa9cc7825b1493b28c260b1be434fb21 Mon Sep 17 00:00:00 2001 From: hahn-th Date: Sun, 17 Jul 2022 14:30:43 +0200 Subject: [PATCH 2640/3516] Bump homematicip 1.0.5 (#75334) --- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index f6fb9f3e739..db0833f8114 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==1.0.4"], + "requirements": ["homematicip==1.0.5"], "codeowners": [], "quality_scale": "platinum", "iot_class": "cloud_push", diff --git a/requirements_all.txt b/requirements_all.txt index 42360e81184..6a4b30192fe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -837,7 +837,7 @@ home-assistant-frontend==20220707.1 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.4 +homematicip==1.0.5 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d6fa41d253..d82b023b4f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -607,7 +607,7 @@ home-assistant-frontend==20220707.1 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.4 +homematicip==1.0.5 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 From cd223d91bb457d937e52de8384eda4e1b41749f1 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 17 Jul 2022 15:01:26 +0200 Subject: [PATCH 2641/3516] Migrate Tractive to new entity naming style (#75184) --- homeassistant/components/tractive/binary_sensor.py | 5 +++-- homeassistant/components/tractive/device_tracker.py | 4 ++-- homeassistant/components/tractive/entity.py | 2 +- homeassistant/components/tractive/sensor.py | 9 +++++---- homeassistant/components/tractive/switch.py | 6 +++--- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/tractive/binary_sensor.py b/homeassistant/components/tractive/binary_sensor.py index 001eb013a35..36aa2370a59 100644 --- a/homeassistant/components/tractive/binary_sensor.py +++ b/homeassistant/components/tractive/binary_sensor.py @@ -31,13 +31,14 @@ TRACKERS_WITH_BUILTIN_BATTERY = ("TRNJA4", "TRAXL1") class TractiveBinarySensor(TractiveEntity, BinarySensorEntity): """Tractive sensor.""" + _attr_has_entity_name = True + def __init__( self, user_id: str, item: Trackables, description: BinarySensorEntityDescription ) -> None: """Initialize sensor entity.""" super().__init__(user_id, item.trackable, item.tracker_details) - self._attr_name = f"{item.trackable['details']['name']} {description.name}" self._attr_unique_id = f"{item.trackable['_id']}_{description.key}" self.entity_description = description @@ -76,7 +77,7 @@ class TractiveBinarySensor(TractiveEntity, BinarySensorEntity): SENSOR_TYPE = BinarySensorEntityDescription( key=ATTR_BATTERY_CHARGING, - name="Battery Charging", + name="Tracker battery charging", device_class=BinarySensorDeviceClass.BATTERY_CHARGING, entity_category=EntityCategory.DIAGNOSTIC, ) diff --git a/homeassistant/components/tractive/device_tracker.py b/homeassistant/components/tractive/device_tracker.py index 218151ae769..4b08defbf97 100644 --- a/homeassistant/components/tractive/device_tracker.py +++ b/homeassistant/components/tractive/device_tracker.py @@ -40,7 +40,9 @@ async def async_setup_entry( class TractiveDeviceTracker(TractiveEntity, TrackerEntity): """Tractive device tracker.""" + _attr_has_entity_name = True _attr_icon = "mdi:paw" + _attr_name = "Tracker" def __init__(self, user_id: str, item: Trackables) -> None: """Initialize tracker entity.""" @@ -51,8 +53,6 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity): self._longitude: float = item.pos_report["latlong"][1] self._accuracy: int = item.pos_report["pos_uncertainty"] self._source_type: str = item.pos_report["sensor_used"] - - self._attr_name = f"{self._tracker_id} {item.trackable['details']['name']}" self._attr_unique_id = item.trackable["_id"] @property diff --git a/homeassistant/components/tractive/entity.py b/homeassistant/components/tractive/entity.py index fd29c6c6c6f..def321d928f 100644 --- a/homeassistant/components/tractive/entity.py +++ b/homeassistant/components/tractive/entity.py @@ -18,7 +18,7 @@ class TractiveEntity(Entity): self._attr_device_info = DeviceInfo( configuration_url="https://my.tractive.com/", identifiers={(DOMAIN, tracker_details["_id"])}, - name=f"Tractive ({tracker_details['_id']})", + name=trackable["details"]["name"], manufacturer="Tractive GmbH", sw_version=tracker_details["fw_version"], model=tracker_details["model_number"], diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index e19e10f6b44..c412502d8d9 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -48,6 +48,8 @@ class TractiveSensorEntityDescription( class TractiveSensor(TractiveEntity, SensorEntity): """Tractive sensor.""" + _attr_has_entity_name = True + def __init__( self, user_id: str, @@ -57,7 +59,6 @@ class TractiveSensor(TractiveEntity, SensorEntity): """Initialize sensor entity.""" super().__init__(user_id, item.trackable, item.tracker_details) - self._attr_name = f"{item.trackable['details']['name']} {description.name}" self._attr_unique_id = f"{item.trackable['_id']}_{description.key}" self.entity_description = description @@ -133,7 +134,7 @@ class TractiveActivitySensor(TractiveSensor): SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( TractiveSensorEntityDescription( key=ATTR_BATTERY_LEVEL, - name="Battery Level", + name="Tracker battery level", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, entity_class=TractiveHardwareSensor, @@ -149,14 +150,14 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( ), TractiveSensorEntityDescription( key=ATTR_MINUTES_ACTIVE, - name="Minutes Active", + name="Minutes active", icon="mdi:clock-time-eight-outline", native_unit_of_measurement=TIME_MINUTES, entity_class=TractiveActivitySensor, ), TractiveSensorEntityDescription( key=ATTR_DAILY_GOAL, - name="Daily Goal", + name="Daily goal", icon="mdi:flag-checkered", native_unit_of_measurement=TIME_MINUTES, entity_class=TractiveActivitySensor, diff --git a/homeassistant/components/tractive/switch.py b/homeassistant/components/tractive/switch.py index 2a425a8f2ac..87cf27b36b1 100644 --- a/homeassistant/components/tractive/switch.py +++ b/homeassistant/components/tractive/switch.py @@ -47,7 +47,7 @@ class TractiveSwitchEntityDescription( SWITCH_TYPES: tuple[TractiveSwitchEntityDescription, ...] = ( TractiveSwitchEntityDescription( key=ATTR_BUZZER, - name="Tracker Buzzer", + name="Tracker buzzer", icon="mdi:volume-high", method="async_set_buzzer", entity_category=EntityCategory.CONFIG, @@ -61,7 +61,7 @@ SWITCH_TYPES: tuple[TractiveSwitchEntityDescription, ...] = ( ), TractiveSwitchEntityDescription( key=ATTR_LIVE_TRACKING, - name="Live Tracking", + name="Live tracking", icon="mdi:map-marker-path", method="async_set_live_tracking", entity_category=EntityCategory.CONFIG, @@ -88,6 +88,7 @@ async def async_setup_entry( class TractiveSwitch(TractiveEntity, SwitchEntity): """Tractive switch.""" + _attr_has_entity_name = True entity_description: TractiveSwitchEntityDescription def __init__( @@ -99,7 +100,6 @@ class TractiveSwitch(TractiveEntity, SwitchEntity): """Initialize switch entity.""" super().__init__(user_id, item.trackable, item.tracker_details) - self._attr_name = f"{item.trackable['details']['name']} {description.name}" self._attr_unique_id = f"{item.trackable['_id']}_{description.key}" self._attr_available = False self._tracker = item.tracker From 503b31fb1576f043759e84e8f519f4a52dce9df5 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 17 Jul 2022 15:15:24 +0200 Subject: [PATCH 2642/3516] Migrate Xiaomi Miio to new entity naming style - part 1 (#75350) --- .../components/xiaomi_miio/binary_sensor.py | 22 +++---- .../components/xiaomi_miio/button.py | 9 ++- .../components/xiaomi_miio/device.py | 10 +-- homeassistant/components/xiaomi_miio/fan.py | 59 +++++++++--------- .../components/xiaomi_miio/humidifier.py | 12 ++-- .../components/xiaomi_miio/number.py | 21 +++---- .../components/xiaomi_miio/select.py | 9 ++- .../components/xiaomi_miio/sensor.py | 62 +++++++++---------- .../components/xiaomi_miio/switch.py | 19 +++--- .../components/xiaomi_miio/vacuum.py | 5 +- 10 files changed, 103 insertions(+), 125 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/binary_sensor.py b/homeassistant/components/xiaomi_miio/binary_sensor.py index a1e833f42de..2b3dfde6190 100644 --- a/homeassistant/components/xiaomi_miio/binary_sensor.py +++ b/homeassistant/components/xiaomi_miio/binary_sensor.py @@ -57,13 +57,13 @@ class XiaomiMiioBinarySensorDescription(BinarySensorEntityDescription): BINARY_SENSOR_TYPES = ( XiaomiMiioBinarySensorDescription( key=ATTR_NO_WATER, - name="Water Tank Empty", + name="Water tank empty", icon="mdi:water-off-outline", entity_category=EntityCategory.DIAGNOSTIC, ), XiaomiMiioBinarySensorDescription( key=ATTR_WATER_TANK_DETACHED, - name="Water Tank", + name="Water tank", icon="mdi:car-coolant-level", device_class=BinarySensorDeviceClass.CONNECTIVITY, value=lambda value: not value, @@ -71,13 +71,13 @@ BINARY_SENSOR_TYPES = ( ), XiaomiMiioBinarySensorDescription( key=ATTR_PTC_STATUS, - name="Auxiliary Heat Status", + name="Auxiliary heat status", device_class=BinarySensorDeviceClass.POWER, entity_category=EntityCategory.DIAGNOSTIC, ), XiaomiMiioBinarySensorDescription( key=ATTR_POWERSUPPLY_ATTACHED, - name="Power Supply", + name="Power supply", device_class=BinarySensorDeviceClass.PLUG, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -89,7 +89,7 @@ FAN_ZA5_BINARY_SENSORS = (ATTR_POWERSUPPLY_ATTACHED,) VACUUM_SENSORS = { ATTR_MOP_ATTACHED: XiaomiMiioBinarySensorDescription( key=ATTR_WATER_BOX_ATTACHED, - name="Mop Attached", + name="Mop attached", icon="mdi:square-rounded", parent_key=VacuumCoordinatorDataAttributes.status, entity_registry_enabled_default=True, @@ -98,7 +98,7 @@ VACUUM_SENSORS = { ), ATTR_WATER_BOX_ATTACHED: XiaomiMiioBinarySensorDescription( key=ATTR_WATER_BOX_ATTACHED, - name="Water Box Attached", + name="Water box attached", icon="mdi:water", parent_key=VacuumCoordinatorDataAttributes.status, entity_registry_enabled_default=True, @@ -107,7 +107,7 @@ VACUUM_SENSORS = { ), ATTR_WATER_SHORTAGE: XiaomiMiioBinarySensorDescription( key=ATTR_WATER_SHORTAGE, - name="Water Shortage", + name="Water shortage", icon="mdi:water", parent_key=VacuumCoordinatorDataAttributes.status, entity_registry_enabled_default=True, @@ -120,7 +120,7 @@ VACUUM_SENSORS_SEPARATE_MOP = { **VACUUM_SENSORS, ATTR_MOP_ATTACHED: XiaomiMiioBinarySensorDescription( key=ATTR_MOP_ATTACHED, - name="Mop Attached", + name="Mop attached", icon="mdi:square-rounded", parent_key=VacuumCoordinatorDataAttributes.status, entity_registry_enabled_default=True, @@ -158,7 +158,6 @@ def _setup_vacuum_sensors(hass, config_entry, async_add_entities): continue entities.append( XiaomiGenericBinarySensor( - f"{config_entry.title} {description.name}", device, config_entry, f"{sensor}_{config_entry.unique_id}", @@ -199,7 +198,6 @@ async def async_setup_entry( continue entities.append( XiaomiGenericBinarySensor( - f"{config_entry.title} {description.name}", hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE], config_entry, f"{description.key}_{config_entry.unique_id}", @@ -216,9 +214,9 @@ class XiaomiGenericBinarySensor(XiaomiCoordinatedMiioEntity, BinarySensorEntity) entity_description: XiaomiMiioBinarySensorDescription - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the entity.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self.entity_description = description self._attr_entity_registry_enabled_default = ( diff --git a/homeassistant/components/xiaomi_miio/button.py b/homeassistant/components/xiaomi_miio/button.py index 0f5b59a262d..e02e6ad81bf 100644 --- a/homeassistant/components/xiaomi_miio/button.py +++ b/homeassistant/components/xiaomi_miio/button.py @@ -38,7 +38,7 @@ class XiaomiMiioButtonDescription(ButtonEntityDescription): BUTTON_TYPES = ( XiaomiMiioButtonDescription( key=ATTR_RESET_DUST_FILTER, - name="Reset Dust Filter", + name="Reset dust filter", icon="mdi:air-filter", method_press="reset_dust_filter", method_press_error_message="Resetting the dust filter lifetime failed", @@ -46,7 +46,7 @@ BUTTON_TYPES = ( ), XiaomiMiioButtonDescription( key=ATTR_RESET_UPPER_FILTER, - name="Reset Upper Filter", + name="Reset upper filter", icon="mdi:air-filter", method_press="reset_upper_filter", method_press_error_message="Resetting the upper filter lifetime failed.", @@ -86,7 +86,6 @@ async def async_setup_entry( entities.append( XiaomiGenericCoordinatedButton( - f"{config_entry.title} {description.name}", device, config_entry, f"{description.key}_{unique_id}", @@ -105,9 +104,9 @@ class XiaomiGenericCoordinatedButton(XiaomiCoordinatedMiioEntity, ButtonEntity): _attr_device_class = ButtonDeviceClass.RESTART - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self.entity_description = description async def async_press(self) -> None: diff --git a/homeassistant/components/xiaomi_miio/device.py b/homeassistant/components/xiaomi_miio/device.py index f8b5e6bc45a..81ca71d6b68 100644 --- a/homeassistant/components/xiaomi_miio/device.py +++ b/homeassistant/components/xiaomi_miio/device.py @@ -110,7 +110,9 @@ class XiaomiMiioEntity(Entity): class XiaomiCoordinatedMiioEntity(CoordinatorEntity[_T]): """Representation of a base a coordinated Xiaomi Miio Entity.""" - def __init__(self, name, device, entry, unique_id, coordinator): + _attr_has_entity_name = True + + def __init__(self, device, entry, unique_id, coordinator): """Initialize the coordinated Xiaomi Miio Device.""" super().__init__(coordinator) self._device = device @@ -119,18 +121,12 @@ class XiaomiCoordinatedMiioEntity(CoordinatorEntity[_T]): self._device_id = entry.unique_id self._device_name = entry.title self._unique_id = unique_id - self._name = name @property def unique_id(self): """Return an unique ID.""" return self._unique_id - @property - def name(self): - """Return the name of this entity, if any.""" - return self._name - @property def device_info(self) -> DeviceInfo: """Return the device info.""" diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 8ce93933022..177f84679ee 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -191,7 +191,6 @@ async def async_setup_entry( hass.data.setdefault(DATA_KEY, {}) - name = config_entry.title model = config_entry.data[CONF_MODEL] unique_id = config_entry.unique_id coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] @@ -199,7 +198,6 @@ async def async_setup_entry( if model == MODEL_AIRPURIFIER_3C: entity = XiaomiAirPurifierMB4( - name, device, config_entry, unique_id, @@ -207,28 +205,27 @@ async def async_setup_entry( ) elif model in MODELS_PURIFIER_MIOT: entity = XiaomiAirPurifierMiot( - name, device, config_entry, unique_id, coordinator, ) elif model.startswith("zhimi.airpurifier."): - entity = XiaomiAirPurifier(name, device, config_entry, unique_id, coordinator) + entity = XiaomiAirPurifier(device, config_entry, unique_id, coordinator) elif model.startswith("zhimi.airfresh."): - entity = XiaomiAirFresh(name, device, config_entry, unique_id, coordinator) + entity = XiaomiAirFresh(device, config_entry, unique_id, coordinator) elif model == MODEL_AIRFRESH_A1: - entity = XiaomiAirFreshA1(name, device, config_entry, unique_id, coordinator) + entity = XiaomiAirFreshA1(device, config_entry, unique_id, coordinator) elif model == MODEL_AIRFRESH_T2017: - entity = XiaomiAirFreshT2017(name, device, config_entry, unique_id, coordinator) + entity = XiaomiAirFreshT2017(device, config_entry, unique_id, coordinator) elif model == MODEL_FAN_P5: - entity = XiaomiFanP5(name, device, config_entry, unique_id, coordinator) + entity = XiaomiFanP5(device, config_entry, unique_id, coordinator) elif model in MODELS_FAN_MIIO: - entity = XiaomiFan(name, device, config_entry, unique_id, coordinator) + entity = XiaomiFan(device, config_entry, unique_id, coordinator) elif model == MODEL_FAN_ZA5: - entity = XiaomiFanZA5(name, device, config_entry, unique_id, coordinator) + entity = XiaomiFanZA5(device, config_entry, unique_id, coordinator) elif model in MODELS_FAN_MIOT: - entity = XiaomiFanMiot(name, device, config_entry, unique_id, coordinator) + entity = XiaomiFanMiot(device, config_entry, unique_id, coordinator) else: return @@ -277,9 +274,9 @@ async def async_setup_entry( class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): """Representation of a generic Xiaomi device.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the generic Xiaomi device.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._available_attributes = {} self._state = None @@ -349,9 +346,9 @@ class XiaomiGenericDevice(XiaomiCoordinatedMiioEntity, FanEntity): class XiaomiGenericAirPurifier(XiaomiGenericDevice): """Representation of a generic AirPurifier device.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the generic AirPurifier device.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._speed_count = 100 @@ -396,9 +393,9 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): REVERSE_SPEED_MODE_MAPPING = {v: k for k, v in SPEED_MODE_MAPPING.items()} - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) if self._model == MODEL_AIRPURIFIER_PRO: self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO @@ -565,9 +562,9 @@ class XiaomiAirPurifierMiot(XiaomiAirPurifier): class XiaomiAirPurifierMB4(XiaomiGenericAirPurifier): """Representation of a Xiaomi Air Purifier MB4.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize Air Purifier MB4.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._device_features = FEATURE_FLAGS_AIRPURIFIER_3C self._preset_modes = PRESET_MODES_AIRPURIFIER_3C @@ -619,9 +616,9 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): "Interval": AirfreshOperationMode.Interval, } - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the miio device.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._device_features = FEATURE_FLAGS_AIRFRESH self._available_attributes = AVAILABLE_ATTRIBUTES_AIRFRESH @@ -717,9 +714,9 @@ class XiaomiAirFresh(XiaomiGenericAirPurifier): class XiaomiAirFreshA1(XiaomiGenericAirPurifier): """Representation of a Xiaomi Air Fresh A1.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the miio device.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._favorite_speed = None self._device_features = FEATURE_FLAGS_AIRFRESH_A1 self._preset_modes = PRESET_MODES_AIRFRESH_A1 @@ -792,9 +789,9 @@ class XiaomiAirFreshA1(XiaomiGenericAirPurifier): class XiaomiAirFreshT2017(XiaomiAirFreshA1): """Representation of a Xiaomi Air Fresh T2017.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the miio device.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._device_features = FEATURE_FLAGS_AIRFRESH_T2017 self._speed_range = (60, 300) @@ -802,9 +799,9 @@ class XiaomiAirFreshT2017(XiaomiAirFreshA1): class XiaomiGenericFan(XiaomiGenericDevice): """Representation of a generic Xiaomi Fan.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the fan.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) if self._model == MODEL_FAN_P5: self._device_features = FEATURE_FLAGS_FAN_P5 @@ -877,9 +874,9 @@ class XiaomiGenericFan(XiaomiGenericDevice): class XiaomiFan(XiaomiGenericFan): """Representation of a Xiaomi Fan.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the fan.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._state = self.coordinator.data.is_on self._oscillating = self.coordinator.data.oscillate @@ -968,9 +965,9 @@ class XiaomiFan(XiaomiGenericFan): class XiaomiFanP5(XiaomiGenericFan): """Representation of a Xiaomi Fan P5.""" - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the fan.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._state = self.coordinator.data.is_on self._preset_mode = self.coordinator.data.mode.name diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index 05c1eaf35bf..0a9543ac604 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -73,12 +73,10 @@ async def async_setup_entry( model = config_entry.data[CONF_MODEL] unique_id = config_entry.unique_id coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] - name = config_entry.title if model in MODELS_HUMIDIFIER_MIOT: air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] entity = XiaomiAirHumidifierMiot( - name, air_humidifier, config_entry, unique_id, @@ -87,7 +85,6 @@ async def async_setup_entry( elif model in MODELS_HUMIDIFIER_MJJSQ: air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] entity = XiaomiAirHumidifierMjjsq( - name, air_humidifier, config_entry, unique_id, @@ -96,7 +93,6 @@ async def async_setup_entry( else: air_humidifier = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] entity = XiaomiAirHumidifier( - name, air_humidifier, config_entry, unique_id, @@ -115,9 +111,9 @@ class XiaomiGenericHumidifier(XiaomiCoordinatedMiioEntity, HumidifierEntity): _attr_supported_features = HumidifierEntityFeature.MODES supported_features: int - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the generic Xiaomi device.""" - super().__init__(name, device, entry, unique_id, coordinator=coordinator) + super().__init__(device, entry, unique_id, coordinator=coordinator) self._state = None self._attributes = {} @@ -173,9 +169,9 @@ class XiaomiAirHumidifier(XiaomiGenericHumidifier, HumidifierEntity): available_modes: list[str] - def __init__(self, name, device, entry, unique_id, coordinator): + def __init__(self, device, entry, unique_id, coordinator): """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._attr_min_humidity = 30 self._attr_max_humidity = 80 diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 7fd5347f432..e7c61044e25 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -107,7 +107,7 @@ class OscillationAngleValues: NUMBER_TYPES = { FEATURE_SET_MOTOR_SPEED: XiaomiMiioNumberDescription( key=ATTR_MOTOR_SPEED, - name="Motor Speed", + name="Motor speed", icon="mdi:fast-forward-outline", native_unit_of_measurement="rpm", native_min_value=200, @@ -119,7 +119,7 @@ NUMBER_TYPES = { ), FEATURE_SET_FAVORITE_LEVEL: XiaomiMiioNumberDescription( key=ATTR_FAVORITE_LEVEL, - name="Favorite Level", + name="Favorite level", icon="mdi:star-cog", native_min_value=0, native_max_value=17, @@ -129,7 +129,7 @@ NUMBER_TYPES = { ), FEATURE_SET_FAN_LEVEL: XiaomiMiioNumberDescription( key=ATTR_FAN_LEVEL, - name="Fan Level", + name="Fan level", icon="mdi:fan", native_min_value=1, native_max_value=3, @@ -149,7 +149,7 @@ NUMBER_TYPES = { ), FEATURE_SET_OSCILLATION_ANGLE: XiaomiMiioNumberDescription( key=ATTR_OSCILLATION_ANGLE, - name="Oscillation Angle", + name="Oscillation angle", icon="mdi:angle-acute", native_unit_of_measurement=DEGREE, native_min_value=1, @@ -160,7 +160,7 @@ NUMBER_TYPES = { ), FEATURE_SET_DELAY_OFF_COUNTDOWN: XiaomiMiioNumberDescription( key=ATTR_DELAY_OFF_COUNTDOWN, - name="Delay Off Countdown", + name="Delay off countdown", icon="mdi:fan-off", native_unit_of_measurement=TIME_MINUTES, native_min_value=0, @@ -171,7 +171,7 @@ NUMBER_TYPES = { ), FEATURE_SET_LED_BRIGHTNESS: XiaomiMiioNumberDescription( key=ATTR_LED_BRIGHTNESS, - name="Led Brightness", + name="LED brightness", icon="mdi:brightness-6", native_min_value=0, native_max_value=100, @@ -181,7 +181,7 @@ NUMBER_TYPES = { ), FEATURE_SET_LED_BRIGHTNESS_LEVEL: XiaomiMiioNumberDescription( key=ATTR_LED_BRIGHTNESS_LEVEL, - name="Led Brightness", + name="LED brightness", icon="mdi:brightness-6", native_min_value=0, native_max_value=8, @@ -191,7 +191,7 @@ NUMBER_TYPES = { ), FEATURE_SET_FAVORITE_RPM: XiaomiMiioNumberDescription( key=ATTR_FAVORITE_RPM, - name="Favorite Motor Speed", + name="Favorite motor speed", icon="mdi:star-cog", native_unit_of_measurement="rpm", native_min_value=300, @@ -283,7 +283,6 @@ async def async_setup_entry( entities.append( XiaomiNumberEntity( - f"{config_entry.title} {description.name}", device, config_entry, f"{description.key}_{config_entry.unique_id}", @@ -298,9 +297,9 @@ async def async_setup_entry( class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): """Representation of a generic Xiaomi attribute selector.""" - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the generic Xiaomi attribute selector.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._attr_native_value = self._extract_value_from_attribute( coordinator.data, description.key diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index f2fc736ed82..5f8fe8df591 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -110,7 +110,6 @@ async def async_setup_entry( description = SELECTOR_TYPES[FEATURE_SET_LED_BRIGHTNESS] entities.append( entity_class( - f"{config_entry.title} {description.name}", device, config_entry, f"{description.key}_{config_entry.unique_id}", @@ -125,9 +124,9 @@ async def async_setup_entry( class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity): """Representation of a generic Xiaomi attribute selector.""" - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the generic Xiaomi attribute selector.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._attr_options = list(description.options) self.entity_description = description @@ -135,9 +134,9 @@ class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity): class XiaomiAirHumidifierSelector(XiaomiSelector): """Representation of a Xiaomi Air Humidifier selector.""" - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id, coordinator, description) + super().__init__(device, entry, unique_id, coordinator, description) self._current_led_brightness = self._extract_value_from_attribute( self.coordinator.data, self.entity_description.key ) diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 50feacf5da7..235d103b53d 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -172,13 +172,13 @@ SENSOR_TYPES = { ), ATTR_LOAD_POWER: XiaomiMiioSensorDescription( key=ATTR_LOAD_POWER, - name="Load Power", + name="Load power", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, ), ATTR_WATER_LEVEL: XiaomiMiioSensorDescription( key=ATTR_WATER_LEVEL, - name="Water Level", + name="Water level", native_unit_of_measurement=PERCENTAGE, icon="mdi:water-check", state_class=SensorStateClass.MEASUREMENT, @@ -186,7 +186,7 @@ SENSOR_TYPES = { ), ATTR_ACTUAL_SPEED: XiaomiMiioSensorDescription( key=ATTR_ACTUAL_SPEED, - name="Actual Speed", + name="Actual speed", native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, @@ -194,7 +194,7 @@ SENSOR_TYPES = { ), ATTR_CONTROL_SPEED: XiaomiMiioSensorDescription( key=ATTR_CONTROL_SPEED, - name="Control Speed", + name="Control speed", native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, @@ -202,7 +202,7 @@ SENSOR_TYPES = { ), ATTR_FAVORITE_SPEED: XiaomiMiioSensorDescription( key=ATTR_FAVORITE_SPEED, - name="Favorite Speed", + name="Favorite speed", native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, @@ -210,7 +210,7 @@ SENSOR_TYPES = { ), ATTR_MOTOR_SPEED: XiaomiMiioSensorDescription( key=ATTR_MOTOR_SPEED, - name="Motor Speed", + name="Motor speed", native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, @@ -218,7 +218,7 @@ SENSOR_TYPES = { ), ATTR_MOTOR2_SPEED: XiaomiMiioSensorDescription( key=ATTR_MOTOR2_SPEED, - name="Second Motor Speed", + name="Second motor speed", native_unit_of_measurement="rpm", icon="mdi:fast-forward", state_class=SensorStateClass.MEASUREMENT, @@ -226,7 +226,7 @@ SENSOR_TYPES = { ), ATTR_USE_TIME: XiaomiMiioSensorDescription( key=ATTR_USE_TIME, - name="Use Time", + name="Use time", native_unit_of_measurement=TIME_SECONDS, icon="mdi:progress-clock", state_class=SensorStateClass.TOTAL_INCREASING, @@ -269,7 +269,7 @@ SENSOR_TYPES = { ), ATTR_FILTER_LIFE_REMAINING: XiaomiMiioSensorDescription( key=ATTR_FILTER_LIFE_REMAINING, - name="Filter Life Remaining", + name="Filter life remaining", native_unit_of_measurement=PERCENTAGE, icon="mdi:air-filter", state_class=SensorStateClass.MEASUREMENT, @@ -278,7 +278,7 @@ SENSOR_TYPES = { ), ATTR_FILTER_USE: XiaomiMiioSensorDescription( key=ATTR_FILTER_HOURS_USED, - name="Filter Use", + name="Filter use", native_unit_of_measurement=TIME_HOURS, icon="mdi:clock-outline", state_class=SensorStateClass.MEASUREMENT, @@ -320,14 +320,14 @@ SENSOR_TYPES = { ), ATTR_CARBON_DIOXIDE: XiaomiMiioSensorDescription( key=ATTR_CARBON_DIOXIDE, - name="Carbon Dioxide", + name="Carbon dioxide", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), ATTR_PURIFY_VOLUME: XiaomiMiioSensorDescription( key=ATTR_PURIFY_VOLUME, - name="Purify Volume", + name="Purify volume", native_unit_of_measurement=VOLUME_CUBIC_METERS, device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL_INCREASING, @@ -491,7 +491,7 @@ VACUUM_SENSORS = { f"dnd_{ATTR_DND_START}": XiaomiMiioSensorDescription( key=ATTR_DND_START, icon="mdi:minus-circle-off", - name="DnD Start", + name="DnD start", device_class=SensorDeviceClass.TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.dnd_status, entity_registry_enabled_default=False, @@ -500,7 +500,7 @@ VACUUM_SENSORS = { f"dnd_{ATTR_DND_END}": XiaomiMiioSensorDescription( key=ATTR_DND_END, icon="mdi:minus-circle-off", - name="DnD End", + name="DnD end", device_class=SensorDeviceClass.TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.dnd_status, entity_registry_enabled_default=False, @@ -509,7 +509,7 @@ VACUUM_SENSORS = { f"last_clean_{ATTR_LAST_CLEAN_START}": XiaomiMiioSensorDescription( key=ATTR_LAST_CLEAN_START, icon="mdi:clock-time-twelve", - name="Last Clean Start", + name="Last clean start", device_class=SensorDeviceClass.TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, entity_category=EntityCategory.DIAGNOSTIC, @@ -519,7 +519,7 @@ VACUUM_SENSORS = { icon="mdi:clock-time-twelve", device_class=SensorDeviceClass.TIMESTAMP, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, - name="Last Clean End", + name="Last clean end", entity_category=EntityCategory.DIAGNOSTIC, ), f"last_clean_{ATTR_LAST_CLEAN_TIME}": XiaomiMiioSensorDescription( @@ -527,7 +527,7 @@ VACUUM_SENSORS = { icon="mdi:timer-sand", key=ATTR_LAST_CLEAN_TIME, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, - name="Last Clean Duration", + name="Last clean duration", entity_category=EntityCategory.DIAGNOSTIC, ), f"last_clean_{ATTR_LAST_CLEAN_AREA}": XiaomiMiioSensorDescription( @@ -535,7 +535,7 @@ VACUUM_SENSORS = { icon="mdi:texture-box", key=ATTR_LAST_CLEAN_AREA, parent_key=VacuumCoordinatorDataAttributes.last_clean_details, - name="Last Clean Area", + name="Last clean area", entity_category=EntityCategory.DIAGNOSTIC, ), f"current_{ATTR_STATUS_CLEAN_TIME}": XiaomiMiioSensorDescription( @@ -543,7 +543,7 @@ VACUUM_SENSORS = { icon="mdi:timer-sand", key=ATTR_STATUS_CLEAN_TIME, parent_key=VacuumCoordinatorDataAttributes.status, - name="Current Clean Duration", + name="Current clean duration", entity_category=EntityCategory.DIAGNOSTIC, ), f"current_{ATTR_LAST_CLEAN_AREA}": XiaomiMiioSensorDescription( @@ -552,7 +552,7 @@ VACUUM_SENSORS = { key=ATTR_STATUS_CLEAN_AREA, parent_key=VacuumCoordinatorDataAttributes.status, entity_category=EntityCategory.DIAGNOSTIC, - name="Current Clean Area", + name="Current clean area", ), f"clean_history_{ATTR_CLEAN_HISTORY_TOTAL_DURATION}": XiaomiMiioSensorDescription( native_unit_of_measurement=TIME_SECONDS, @@ -568,7 +568,7 @@ VACUUM_SENSORS = { icon="mdi:texture-box", key=ATTR_CLEAN_HISTORY_TOTAL_AREA, parent_key=VacuumCoordinatorDataAttributes.clean_history_status, - name="Total Clean Area", + name="Total clean area", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -578,17 +578,17 @@ VACUUM_SENSORS = { state_class=SensorStateClass.TOTAL_INCREASING, key=ATTR_CLEAN_HISTORY_COUNT, parent_key=VacuumCoordinatorDataAttributes.clean_history_status, - name="Total Clean Count", + name="Total clean count", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), f"clean_history_{ATTR_CLEAN_HISTORY_DUST_COLLECTION_COUNT}": XiaomiMiioSensorDescription( native_unit_of_measurement="", icon="mdi:counter", - state_class="total_increasing", + state_class=SensorStateClass.TOTAL_INCREASING, key=ATTR_CLEAN_HISTORY_DUST_COLLECTION_COUNT, parent_key=VacuumCoordinatorDataAttributes.clean_history_status, - name="Total Dust Collection Count", + name="Total dust collection count", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -597,7 +597,7 @@ VACUUM_SENSORS = { icon="mdi:brush", key=ATTR_CONSUMABLE_STATUS_MAIN_BRUSH_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, - name="Main Brush Left", + name="Main brush left", entity_category=EntityCategory.DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_SIDE_BRUSH_LEFT}": XiaomiMiioSensorDescription( @@ -605,7 +605,7 @@ VACUUM_SENSORS = { icon="mdi:brush", key=ATTR_CONSUMABLE_STATUS_SIDE_BRUSH_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, - name="Side Brush Left", + name="Side brush left", entity_category=EntityCategory.DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_FILTER_LEFT}": XiaomiMiioSensorDescription( @@ -613,7 +613,7 @@ VACUUM_SENSORS = { icon="mdi:air-filter", key=ATTR_CONSUMABLE_STATUS_FILTER_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, - name="Filter Left", + name="Filter left", entity_category=EntityCategory.DIAGNOSTIC, ), f"consumable_{ATTR_CONSUMABLE_STATUS_SENSOR_DIRTY_LEFT}": XiaomiMiioSensorDescription( @@ -621,7 +621,7 @@ VACUUM_SENSORS = { icon="mdi:eye-outline", key=ATTR_CONSUMABLE_STATUS_SENSOR_DIRTY_LEFT, parent_key=VacuumCoordinatorDataAttributes.consumable_status, - name="Sensor Dirty Left", + name="Sensor dirty left", entity_category=EntityCategory.DIAGNOSTIC, ), } @@ -644,7 +644,6 @@ def _setup_vacuum_sensors(hass, config_entry, async_add_entities): continue entities.append( XiaomiGenericSensor( - f"{config_entry.title} {description.name}", device, config_entry, f"{sensor}_{config_entry.unique_id}", @@ -741,7 +740,6 @@ async def async_setup_entry( continue entities.append( XiaomiGenericSensor( - f"{config_entry.title} {description.name}", device, config_entry, f"{sensor}_{config_entry.unique_id}", @@ -758,9 +756,9 @@ class XiaomiGenericSensor(XiaomiCoordinatedMiioEntity, SensorEntity): entity_description: XiaomiMiioSensorDescription - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the entity.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self.entity_description = description self._attr_unique_id = unique_id self._attr_native_value = self._determine_native_value() diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index e3d34e8c512..f80b6343d09 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -231,7 +231,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_CHILD_LOCK, feature=FEATURE_SET_CHILD_LOCK, - name="Child Lock", + name="Child lock", icon="mdi:lock", method_on="async_set_child_lock_on", method_off="async_set_child_lock_off", @@ -249,7 +249,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_DRY, feature=FEATURE_SET_DRY, - name="Dry Mode", + name="Dry mode", icon="mdi:hair-dryer", method_on="async_set_dry_on", method_off="async_set_dry_off", @@ -258,7 +258,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_CLEAN, feature=FEATURE_SET_CLEAN, - name="Clean Mode", + name="Clean mode", icon="mdi:shimmer", method_on="async_set_clean_on", method_off="async_set_clean_off", @@ -268,7 +268,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_LED, feature=FEATURE_SET_LED, - name="Led", + name="LED", icon="mdi:led-outline", method_on="async_set_led_on", method_off="async_set_led_off", @@ -277,7 +277,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_LEARN_MODE, feature=FEATURE_SET_LEARN_MODE, - name="Learn Mode", + name="Learn mode", icon="mdi:school-outline", method_on="async_set_learn_mode_on", method_off="async_set_learn_mode_off", @@ -286,7 +286,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_AUTO_DETECT, feature=FEATURE_SET_AUTO_DETECT, - name="Auto Detect", + name="Auto detect", method_on="async_set_auto_detect_on", method_off="async_set_auto_detect_off", entity_category=EntityCategory.CONFIG, @@ -303,7 +303,7 @@ SWITCH_TYPES = ( XiaomiMiioSwitchDescription( key=ATTR_PTC, feature=FEATURE_SET_PTC, - name="Auxiliary Heat", + name="Auxiliary heat", icon="mdi:radiator", method_on="async_set_ptc_on", method_off="async_set_ptc_off", @@ -353,7 +353,6 @@ async def async_setup_coordinated_entry(hass, config_entry, async_add_entities): if description.feature & device_features: entities.append( XiaomiGenericCoordinatedSwitch( - f"{config_entry.title} {description.name}", device, config_entry, f"{description.key}_{unique_id}", @@ -490,9 +489,9 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): entity_description: XiaomiMiioSwitchDescription - def __init__(self, name, device, entry, unique_id, coordinator, description): + def __init__(self, device, entry, unique_id, coordinator, description): """Initialize the plug switch.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._attr_is_on = self._extract_value_from_attribute( self.coordinator.data, description.key diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 6d398ff40b9..e8f4b334544 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -86,11 +86,9 @@ async def async_setup_entry( entities = [] if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: - name = config_entry.title unique_id = config_entry.unique_id mirobo = MiroboVacuum( - name, hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE], config_entry, unique_id, @@ -201,14 +199,13 @@ class MiroboVacuum( def __init__( self, - name, device, entry, unique_id, coordinator: DataUpdateCoordinator[VacuumCoordinatorData], ): """Initialize the Xiaomi vacuum cleaner robot handler.""" - super().__init__(name, device, entry, unique_id, coordinator) + super().__init__(device, entry, unique_id, coordinator) self._state: str | None = None async def async_added_to_hass(self) -> None: From 8d63f81821e726a44a5aa1ebe6bb9283cd45b037 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Jul 2022 08:19:05 -0500 Subject: [PATCH 2643/3516] Add bluetooth discovery to HomeKit Controller (#75333) Co-authored-by: Jc2k --- .../homekit_controller/config_flow.py | 114 +++++++++-- .../homekit_controller/manifest.json | 3 +- .../homekit_controller/strings.json | 4 +- .../homekit_controller/translations/en.json | 4 +- homeassistant/generated/bluetooth.py | 5 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../homekit_controller/test_config_flow.py | 189 +++++++++++++++--- 8 files changed, 268 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 9b8b759f80e..d8b3fda2d06 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -1,13 +1,17 @@ """Config flow to configure homekit_controller.""" from __future__ import annotations +from collections.abc import Awaitable import logging import re -from typing import Any +from typing import TYPE_CHECKING, Any, cast import aiohomekit -from aiohomekit.controller.abstract import AbstractPairing +from aiohomekit import Controller, const as aiohomekit_const +from aiohomekit.controller.abstract import AbstractDiscovery, AbstractPairing from aiohomekit.exceptions import AuthenticationError +from aiohomekit.model.categories import Categories +from aiohomekit.model.status_flags import StatusFlags from aiohomekit.utils import domain_supported, domain_to_name import voluptuous as vol @@ -16,6 +20,7 @@ from homeassistant.components import zeroconf from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.service_info import bluetooth from .connection import HKDevice from .const import DOMAIN, KNOWN_DEVICES @@ -41,6 +46,8 @@ PIN_FORMAT = re.compile(r"^(\d{3})-{0,1}(\d{2})-{0,1}(\d{3})$") _LOGGER = logging.getLogger(__name__) +BLE_DEFAULT_NAME = "Bluetooth device" + INSECURE_CODES = { "00000000", "11111111", @@ -62,6 +69,11 @@ def normalize_hkid(hkid: str) -> str: return hkid.lower() +def formatted_category(category: Categories) -> str: + """Return a human readable category name.""" + return str(category.name).replace("_", " ").title() + + @callback def find_existing_host(hass, serial: str) -> config_entries.ConfigEntry | None: """Return a set of the configured hosts.""" @@ -92,14 +104,15 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize the homekit_controller flow.""" - self.model = None - self.hkid = None - self.name = None - self.devices = {} - self.controller = None - self.finish_pairing = None + self.model: str | None = None + self.hkid: str | None = None + self.name: str | None = None + self.category: Categories | None = None + self.devices: dict[str, AbstractDiscovery] = {} + self.controller: Controller | None = None + self.finish_pairing: Awaitable[AbstractPairing] | None = None async def _async_setup_controller(self): """Create the controller.""" @@ -111,9 +124,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: key = user_input["device"] - self.hkid = self.devices[key].description.id - self.model = self.devices[key].description.model - self.name = self.devices[key].description.name + discovery = self.devices[key] + self.category = discovery.description.category + self.hkid = discovery.description.id + self.model = getattr(discovery.description, "model", BLE_DEFAULT_NAME) + self.name = discovery.description.name or BLE_DEFAULT_NAME await self.async_set_unique_id( normalize_hkid(self.hkid), raise_on_progress=False @@ -138,7 +153,14 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", errors=errors, data_schema=vol.Schema( - {vol.Required("device"): vol.In(self.devices.keys())} + { + vol.Required("device"): vol.In( + { + key: f"{key} ({formatted_category(discovery.description.category)})" + for key, discovery in self.devices.items() + } + ) + } ), ) @@ -151,13 +173,14 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self._async_setup_controller() try: - device = await self.controller.async_find(unique_id) + discovery = await self.controller.async_find(unique_id) except aiohomekit.AccessoryNotFoundError: return self.async_abort(reason="accessory_not_found_error") - self.name = device.description.name - self.model = device.description.model - self.hkid = device.description.id + self.name = discovery.description.name + self.model = discovery.description.model + self.category = discovery.description.category + self.hkid = discovery.description.id return self._async_step_pair_show_form() @@ -213,6 +236,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): model = properties["md"] name = domain_to_name(discovery_info.name) status_flags = int(properties["sf"]) + category = Categories(int(properties.get("ci", 0))) paired = not status_flags & 0x01 # The configuration number increases every time the characteristic map @@ -326,6 +350,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.name = name self.model = model + self.category = category self.hkid = hkid # We want to show the pairing form - but don't call async_step_pair @@ -333,6 +358,55 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # pairing code) return self._async_step_pair_show_form() + async def async_step_bluetooth( + self, discovery_info: bluetooth.BluetoothServiceInfo + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + if not aiohomekit_const.BLE_TRANSPORT_SUPPORTED: + return self.async_abort(reason="ignored_model") + + # Late imports in case BLE is not available + from aiohomekit.controller.ble.discovery import ( # pylint: disable=import-outside-toplevel + BleDiscovery, + ) + from aiohomekit.controller.ble.manufacturer_data import ( # pylint: disable=import-outside-toplevel + HomeKitAdvertisement, + ) + + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + + mfr_data = discovery_info.manufacturer_data + + try: + device = HomeKitAdvertisement.from_manufacturer_data( + discovery_info.name, discovery_info.address, mfr_data + ) + except ValueError: + return self.async_abort(reason="ignored_model") + + if not (device.status_flags & StatusFlags.UNPAIRED): + return self.async_abort(reason="already_paired") + + if self.controller is None: + await self._async_setup_controller() + assert self.controller is not None + + try: + discovery = await self.controller.async_find(device.id) + except aiohomekit.AccessoryNotFoundError: + return self.async_abort(reason="accessory_not_found_error") + + if TYPE_CHECKING: + discovery = cast(BleDiscovery, discovery) + + self.name = discovery.description.name + self.model = BLE_DEFAULT_NAME + self.category = discovery.description.category + self.hkid = discovery.description.id + + return self._async_step_pair_show_form() + async def async_step_pair(self, pair_info=None): """Pair with a new HomeKit accessory.""" # If async_step_pair is called with no pairing code then we do the M1 @@ -453,8 +527,10 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @callback def _async_step_pair_show_form(self, errors=None): - placeholders = {"name": self.name} - self.context["title_placeholders"] = {"name": self.name} + placeholders = self.context["title_placeholders"] = { + "name": self.name, + "category": formatted_category(self.category), + } schema = {vol.Required("pairing_code"): vol.All(str, vol.Strip)} if errors and errors.get("pairing_code") == "insecure_setup_code": diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index a5ca76fdc1e..a4f5350a167 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,8 +3,9 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.1.1"], + "requirements": ["aiohomekit==1.1.4"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], + "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_first_byte": 6 }], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], "iot_class": "local_push", diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index 7ad868db3fc..2831dabc38d 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -1,7 +1,7 @@ { "title": "HomeKit Controller", "config": { - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "user": { "title": "Device selection", @@ -12,7 +12,7 @@ }, "pair": { "title": "Pair with a device via HomeKit Accessory Protocol", - "description": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.", + "description": "HomeKit Controller communicates with {name} ({category}) over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.", "data": { "pairing_code": "Pairing Code", "allow_insecure_setup_codes": "Allow pairing with insecure setup codes." diff --git a/homeassistant/components/homekit_controller/translations/en.json b/homeassistant/components/homekit_controller/translations/en.json index 5de3a6c5334..2686e71d252 100644 --- a/homeassistant/components/homekit_controller/translations/en.json +++ b/homeassistant/components/homekit_controller/translations/en.json @@ -18,7 +18,7 @@ "unable_to_pair": "Unable to pair, please try again.", "unknown_error": "Device reported an unknown error. Pairing failed." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Abort pairing on all controllers, or try restarting the device, then continue to resume pairing.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Allow pairing with insecure setup codes.", "pairing_code": "Pairing Code" }, - "description": "HomeKit Controller communicates with {name} over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.", + "description": "HomeKit Controller communicates with {name} ({category}) over the local area network using a secure encrypted connection without a separate HomeKit controller or iCloud. Enter your HomeKit pairing code (in the format XXX-XX-XXX) to use this accessory. This code is usually found on the device itself or in the packaging.", "title": "Pair with a device via HomeKit Accessory Protocol" }, "protocol_error": { diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 49596c4773c..feac27af1f1 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -7,6 +7,11 @@ from __future__ import annotations # fmt: off BLUETOOTH: list[dict[str, str | int]] = [ + { + "domain": "homekit_controller", + "manufacturer_id": 76, + "manufacturer_data_first_byte": 6 + }, { "domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" diff --git a/requirements_all.txt b/requirements_all.txt index 6a4b30192fe..47ff7005c66 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.1 +aiohomekit==1.1.4 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d82b023b4f6..a9d015738f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.1 +aiohomekit==1.1.4 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 73bd159fd73..78d3c609a9c 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -1,6 +1,5 @@ """Tests for homekit_controller config flow.""" import asyncio -from unittest import mock import unittest.mock from unittest.mock import AsyncMock, MagicMock, patch @@ -15,8 +14,13 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import KNOWN_DEVICES -from homeassistant.data_entry_flow import FlowResultType +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_FORM, + FlowResultType, +) from homeassistant.helpers import device_registry +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from tests.common import MockConfigEntry, mock_device_registry @@ -78,23 +82,55 @@ VALID_PAIRING_CODES = [ " 98765432 ", ] +NOT_HK_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( + name="FakeAccessory", + address="AA:BB:CC:DD:EE:FF", + rssi=-81, + manufacturer_data={12: b"\x06\x12\x34"}, + service_data={}, + service_uuids=[], + source="local", +) -def _setup_flow_handler(hass, pairing=None): - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass - flow.context = {} +HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED = BluetoothServiceInfo( + name="Eve Energy Not Found", + address="AA:BB:CC:DD:EE:FF", + rssi=-81, + # ID is '9b:86:af:01:af:db' + manufacturer_data={ + 76: b"\x061\x01\x9b\x86\xaf\x01\xaf\xdb\x07\x00\x06\x00\x02\x02X\x19\xb1Q" + }, + service_data={}, + service_uuids=[], + source="local", +) - finish_pairing = unittest.mock.AsyncMock(return_value=pairing) +HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_UNPAIRED = BluetoothServiceInfo( + name="Eve Energy Found Unpaired", + address="AA:BB:CC:DD:EE:FF", + rssi=-81, + # ID is '00:00:00:00:00:00', pairing flag is byte 3 + manufacturer_data={ + 76: b"\x061\x01\x00\x00\x00\x00\x00\x00\x07\x00\x06\x00\x02\x02X\x19\xb1Q" + }, + service_data={}, + service_uuids=[], + source="local", +) - discovery = mock.Mock() - discovery.description.id = "00:00:00:00:00:00" - discovery.async_start_pairing = unittest.mock.AsyncMock(return_value=finish_pairing) - flow.controller = mock.Mock() - flow.controller.pairings = {} - flow.controller.async_find = unittest.mock.AsyncMock(return_value=discovery) - - return flow +HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_PAIRED = BluetoothServiceInfo( + name="Eve Energy Found Paired", + address="AA:BB:CC:DD:EE:FF", + rssi=-81, + # ID is '00:00:00:00:00:00', pairing flag is byte 3 + manufacturer_data={ + 76: b"\x061\x00\x00\x00\x00\x00\x00\x00\x07\x00\x06\x00\x02\x02X\x19\xb1Q" + }, + service_data={}, + service_uuids=[], + source="local", +) @pytest.mark.parametrize("pairing_code", INVALID_PAIRING_CODES) @@ -151,7 +187,7 @@ def get_device_discovery_info( "c#": device.description.config_num, "s#": device.description.state_num, "ff": "0", - "ci": "0", + "ci": "7", "sf": "0" if paired else "1", "sh": "", }, @@ -208,7 +244,7 @@ async def test_discovery_works(hass, controller, upper_case_props, missing_cshar assert result["step_id"] == "pair" assert get_flow_context(hass, result) == { "source": config_entries.SOURCE_ZEROCONF, - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", } @@ -592,7 +628,7 @@ async def test_pair_form_errors_on_start(hass, controller, exception, expected): ) assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -607,7 +643,7 @@ async def test_pair_form_errors_on_start(hass, controller, exception, expected): assert result["errors"]["pairing_code"] == expected assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -640,7 +676,7 @@ async def test_pair_abort_errors_on_finish(hass, controller, exception, expected ) assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -653,7 +689,7 @@ async def test_pair_abort_errors_on_finish(hass, controller, exception, expected assert result["type"] == "form" assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -680,7 +716,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) ) assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -693,7 +729,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) assert result["type"] == "form" assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -706,7 +742,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) assert result["errors"]["pairing_code"] == expected assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, "pairing": True, @@ -737,7 +773,7 @@ async def test_user_works(hass, controller): assert get_flow_context(hass, result) == { "source": config_entries.SOURCE_USER, "unique_id": "00:00:00:00:00:00", - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Other"}, } result = await hass.config_entries.flow.async_configure( @@ -772,7 +808,7 @@ async def test_user_pairing_with_insecure_setup_code(hass, controller): assert get_flow_context(hass, result) == { "source": config_entries.SOURCE_USER, "unique_id": "00:00:00:00:00:00", - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Other"}, } result = await hass.config_entries.flow.async_configure( @@ -829,7 +865,7 @@ async def test_unignore_works(hass, controller): assert result["type"] == "form" assert result["step_id"] == "pair" assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Other"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_UNIGNORE, } @@ -917,7 +953,7 @@ async def test_mdns_update_to_paired_during_pairing(hass, controller): ) assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -942,7 +978,7 @@ async def test_mdns_update_to_paired_during_pairing(hass, controller): assert result["type"] == "form" assert get_flow_context(hass, result) == { - "title_placeholders": {"name": "TestDevice"}, + "title_placeholders": {"name": "TestDevice", "category": "Outlet"}, "unique_id": "00:00:00:00:00:00", "source": config_entries.SOURCE_ZEROCONF, } @@ -967,3 +1003,98 @@ async def test_mdns_update_to_paired_during_pairing(hass, controller): assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "Koogeek-LS1-20833F" assert result["data"] == {} + + +async def test_discovery_no_bluetooth_support(hass, controller): + """Test discovery with bluetooth support not available.""" + with patch( + "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED", + False, + ): + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "ignored_model" + + +async def test_bluetooth_not_homekit(hass, controller): + """Test bluetooth discovery with a non-homekit device.""" + with patch( + "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED", + True, + ): + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_HK_BLUETOOTH_SERVICE_INFO, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "ignored_model" + + +async def test_bluetooth_valid_device_no_discovery(hass, controller): + """Test bluetooth discovery with a homekit device and discovery fails.""" + with patch( + "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED", + True, + ): + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "accessory_not_found_error" + + +async def test_bluetooth_valid_device_discovery_paired(hass, controller): + """Test bluetooth discovery with a homekit device and discovery works.""" + setup_mock_accessory(controller) + + with patch( + "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED", + True, + ): + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_PAIRED, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_paired" + + +async def test_bluetooth_valid_device_discovery_unpaired(hass, controller): + """Test bluetooth discovery with a homekit device and discovery works.""" + setup_mock_accessory(controller) + with patch( + "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED", + True, + ): + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_UNPAIRED, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "pair" + + assert get_flow_context(hass, result) == { + "source": config_entries.SOURCE_BLUETOOTH, + "unique_id": "AA:BB:CC:DD:EE:FF", + "title_placeholders": {"name": "TestDevice", "category": "Other"}, + } + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result2["type"] == RESULT_TYPE_FORM + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], user_input={"pairing_code": "111-22-333"} + ) + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "Koogeek-LS1-20833F" + assert result3["data"] == {} From 460f522d6dfff84540513dac46d1daaa4da6c861 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 17 Jul 2022 10:59:13 -0400 Subject: [PATCH 2644/3516] Migrate Deluge to new entity naming style (#75359) --- homeassistant/components/deluge/__init__.py | 2 ++ homeassistant/components/deluge/sensor.py | 5 ++--- homeassistant/components/deluge/switch.py | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/deluge/__init__.py b/homeassistant/components/deluge/__init__.py index 2958b9e1df6..566b97b5b04 100644 --- a/homeassistant/components/deluge/__init__.py +++ b/homeassistant/components/deluge/__init__.py @@ -73,6 +73,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class DelugeEntity(CoordinatorEntity[DelugeDataUpdateCoordinator]): """Representation of a Deluge entity.""" + _attr_has_entity_name = True + def __init__(self, coordinator: DelugeDataUpdateCoordinator) -> None: """Initialize a Deluge entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 8a8e8f64657..bcdca8b3d92 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -52,14 +52,14 @@ SENSOR_TYPES: tuple[DelugeSensorEntityDescription, ...] = ( ), DelugeSensorEntityDescription( key=DOWNLOAD_SPEED, - name="Down Speed", + name="Down speed", native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, value=lambda data: get_state(data, DOWNLOAD_SPEED), ), DelugeSensorEntityDescription( key=UPLOAD_SPEED, - name="Up Speed", + name="Up speed", native_unit_of_measurement=DATA_RATE_KILOBYTES_PER_SECOND, state_class=SensorStateClass.MEASUREMENT, value=lambda data: get_state(data, UPLOAD_SPEED), @@ -92,7 +92,6 @@ class DelugeSensor(DelugeEntity, SensorEntity): """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = f"{coordinator.config_entry.title} {description.name}" self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" @property diff --git a/homeassistant/components/deluge/switch.py b/homeassistant/components/deluge/switch.py index cc11fcaf86c..c25c1752ee4 100644 --- a/homeassistant/components/deluge/switch.py +++ b/homeassistant/components/deluge/switch.py @@ -29,7 +29,6 @@ class DelugeSwitch(DelugeEntity, SwitchEntity): def __init__(self, coordinator: DelugeDataUpdateCoordinator) -> None: """Initialize the Deluge switch.""" super().__init__(coordinator) - self._attr_name = coordinator.config_entry.title self._attr_unique_id = f"{coordinator.config_entry.entry_id}_enabled" def turn_on(self, **kwargs: Any) -> None: From 27e3ff9c694abbcad74f1da1779539b04aa2da71 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Sun, 17 Jul 2022 11:16:14 -0400 Subject: [PATCH 2645/3516] Migrate Skybell to new entity naming style (#75341) --- homeassistant/components/skybell/camera.py | 2 +- homeassistant/components/skybell/entity.py | 5 ++--- homeassistant/components/skybell/light.py | 8 +------- homeassistant/components/skybell/sensor.py | 14 +++++++------- homeassistant/components/skybell/switch.py | 6 +++--- 5 files changed, 14 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/skybell/camera.py b/homeassistant/components/skybell/camera.py index 499f1f3bfca..5bbcea833c2 100644 --- a/homeassistant/components/skybell/camera.py +++ b/homeassistant/components/skybell/camera.py @@ -41,7 +41,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) CAMERA_TYPES: tuple[CameraEntityDescription, ...] = ( - CameraEntityDescription(key="activity", name="Last Activity"), + CameraEntityDescription(key="activity", name="Last activity"), CameraEntityDescription(key="avatar", name="Camera"), ) diff --git a/homeassistant/components/skybell/entity.py b/homeassistant/components/skybell/entity.py index 0e5c246a8ed..29c7167b02b 100644 --- a/homeassistant/components/skybell/entity.py +++ b/homeassistant/components/skybell/entity.py @@ -16,6 +16,7 @@ class SkybellEntity(CoordinatorEntity[SkybellDataUpdateCoordinator]): """An HA implementation for Skybell entity.""" _attr_attribution = "Data provided by Skybell.com" + _attr_has_entity_name = True def __init__( self, coordinator: SkybellDataUpdateCoordinator, description: EntityDescription @@ -23,14 +24,12 @@ class SkybellEntity(CoordinatorEntity[SkybellDataUpdateCoordinator]): """Initialize a SkyBell entity.""" super().__init__(coordinator) self.entity_description = description - if description.name != coordinator.device.name: - self._attr_name = f"{self._device.name} {description.name}" self._attr_unique_id = f"{self._device.device_id}_{description.key}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._device.device_id)}, manufacturer=DEFAULT_NAME, model=self._device.type, - name=self._device.name, + name=self._device.name.capitalize(), sw_version=self._device.firmware_ver, ) if self._device.mac: diff --git a/homeassistant/components/skybell/light.py b/homeassistant/components/skybell/light.py index 845be44a34b..2b3066a8827 100644 --- a/homeassistant/components/skybell/light.py +++ b/homeassistant/components/skybell/light.py @@ -23,13 +23,7 @@ async def async_setup_entry( ) -> None: """Set up Skybell switch.""" async_add_entities( - SkybellLight( - coordinator, - LightEntityDescription( - key=coordinator.device.name, - name=coordinator.device.name, - ), - ) + SkybellLight(coordinator, LightEntityDescription(key="light")) for coordinator in hass.data[DOMAIN][entry.entry_id] ) diff --git a/homeassistant/components/skybell/sensor.py b/homeassistant/components/skybell/sensor.py index eeb81e07aaf..352d29bd793 100644 --- a/homeassistant/components/skybell/sensor.py +++ b/homeassistant/components/skybell/sensor.py @@ -35,27 +35,27 @@ class SkybellSensorEntityDescription(SensorEntityDescription): SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( SkybellSensorEntityDescription( key="chime_level", - name="Chime Level", + name="Chime level", icon="mdi:bell-ring", value_fn=lambda device: device.outdoor_chime_level, ), SkybellSensorEntityDescription( key="last_button_event", - name="Last Button Event", + name="Last button event", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda device: device.latest("button").get(CONST.CREATED_AT), ), SkybellSensorEntityDescription( key="last_motion_event", - name="Last Motion Event", + name="Last motion event", icon="mdi:clock", device_class=SensorDeviceClass.TIMESTAMP, value_fn=lambda device: device.latest("motion").get(CONST.CREATED_AT), ), SkybellSensorEntityDescription( key=CONST.ATTR_LAST_CHECK_IN, - name="Last Check in", + name="Last check in", icon="mdi:clock", entity_registry_enabled_default=False, device_class=SensorDeviceClass.TIMESTAMP, @@ -64,7 +64,7 @@ SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( ), SkybellSensorEntityDescription( key="motion_threshold", - name="Motion Threshold", + name="Motion threshold", icon="mdi:walk", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -72,7 +72,7 @@ SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( ), SkybellSensorEntityDescription( key="video_profile", - name="Video Profile", + name="Video profile", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda device: device.video_profile, @@ -87,7 +87,7 @@ SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( ), SkybellSensorEntityDescription( key=CONST.ATTR_WIFI_STATUS, - name="Wifi Status", + name="Wifi status", icon="mdi:wifi-strength-3", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, diff --git a/homeassistant/components/skybell/switch.py b/homeassistant/components/skybell/switch.py index d4f2817141c..529be94f1ac 100644 --- a/homeassistant/components/skybell/switch.py +++ b/homeassistant/components/skybell/switch.py @@ -22,15 +22,15 @@ from .entity import SkybellEntity SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SwitchEntityDescription( key="do_not_disturb", - name="Do Not Disturb", + name="Do not disturb", ), SwitchEntityDescription( key="do_not_ring", - name="Do Not Ring", + name="Do not ring", ), SwitchEntityDescription( key="motion_sensor", - name="Motion Sensor", + name="Motion sensor", ), ) From 98dae902a13849518b6ee49ffc8bf1ee689e7c97 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 17 Jul 2022 17:45:38 +0200 Subject: [PATCH 2646/3516] Migrate PVOutput to new entity naming style (#75016) --- homeassistant/components/pvoutput/sensor.py | 9 ++-- tests/components/pvoutput/test_sensor.py | 53 +++++++++++++-------- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 7bd4b8789eb..a0afee0f3eb 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -47,7 +47,7 @@ class PVOutputSensorEntityDescription( SENSORS: tuple[PVOutputSensorEntityDescription, ...] = ( PVOutputSensorEntityDescription( key="energy_consumption", - name="Energy Consumed", + name="Energy consumed", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -55,7 +55,7 @@ SENSORS: tuple[PVOutputSensorEntityDescription, ...] = ( ), PVOutputSensorEntityDescription( key="energy_generation", - name="Energy Generated", + name="Energy generated", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, @@ -70,7 +70,7 @@ SENSORS: tuple[PVOutputSensorEntityDescription, ...] = ( ), PVOutputSensorEntityDescription( key="power_consumption", - name="Power Consumed", + name="Power consumed", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, @@ -78,7 +78,7 @@ SENSORS: tuple[PVOutputSensorEntityDescription, ...] = ( ), PVOutputSensorEntityDescription( key="power_generation", - name="Power Generated", + name="Power generated", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, @@ -129,6 +129,7 @@ class PVOutputSensorEntity( """Representation of a PVOutput sensor.""" entity_description: PVOutputSensorEntityDescription + _attr_has_entity_name = True def __init__( self, diff --git a/tests/components/pvoutput/test_sensor.py b/tests/components/pvoutput/test_sensor.py index a194326cd9d..54d1d3e641f 100644 --- a/tests/components/pvoutput/test_sensor.py +++ b/tests/components/pvoutput/test_sensor.py @@ -31,40 +31,46 @@ async def test_sensors( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - state = hass.states.get("sensor.energy_consumed") - entry = entity_registry.async_get("sensor.energy_consumed") + state = hass.states.get("sensor.frenck_s_solar_farm_energy_consumed") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_energy_consumed") assert entry assert state assert entry.unique_id == "12345_energy_consumption" assert entry.entity_category is None assert state.state == "1000" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Consumed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Frenck's Solar Farm Energy consumed" + ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.energy_generated") - entry = entity_registry.async_get("sensor.energy_generated") + state = hass.states.get("sensor.frenck_s_solar_farm_energy_generated") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_energy_generated") assert entry assert state assert entry.unique_id == "12345_energy_generation" assert entry.entity_category is None assert state.state == "500" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Generated" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Frenck's Solar Farm Energy generated" + ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.efficiency") - entry = entity_registry.async_get("sensor.efficiency") + state = hass.states.get("sensor.frenck_s_solar_farm_efficiency") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_efficiency") assert entry assert state assert entry.unique_id == "12345_normalized_output" assert entry.entity_category is None assert state.state == "0.5" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Efficiency" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's Solar Farm Efficiency" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) @@ -73,54 +79,59 @@ async def test_sensors( assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.power_consumed") - entry = entity_registry.async_get("sensor.power_consumed") + state = hass.states.get("sensor.frenck_s_solar_farm_power_consumed") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_power_consumed") assert entry assert state assert entry.unique_id == "12345_power_consumption" assert entry.entity_category is None assert state.state == "2500" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Consumed" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's Solar Farm Power consumed" + ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.power_generated") - entry = entity_registry.async_get("sensor.power_generated") + state = hass.states.get("sensor.frenck_s_solar_farm_power_generated") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_power_generated") assert entry assert state assert entry.unique_id == "12345_power_generation" assert entry.entity_category is None assert state.state == "1500" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Generated" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Frenck's Solar Farm Power generated" + ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.temperature") - entry = entity_registry.async_get("sensor.temperature") + state = hass.states.get("sensor.frenck_s_solar_farm_temperature") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_temperature") assert entry assert state assert entry.unique_id == "12345_temperature" assert entry.entity_category is None assert state.state == "20.2" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Temperature" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's Solar Farm Temperature" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.voltage") - entry = entity_registry.async_get("sensor.voltage") + state = hass.states.get("sensor.frenck_s_solar_farm_voltage") + entry = entity_registry.async_get("sensor.frenck_s_solar_farm_voltage") assert entry assert state assert entry.unique_id == "12345_voltage" assert entry.entity_category is None assert state.state == "220.5" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Voltage" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's Solar Farm Voltage" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_POTENTIAL_VOLT assert ATTR_ICON not in state.attributes From 5beddb13c0239215ac4324ceb0a9e600aeb8efbc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Jul 2022 12:56:07 -0500 Subject: [PATCH 2647/3516] Fix HKC device triggers (#75371) --- .../components/homekit_controller/device_trigger.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index d7828753d98..700ab60c47f 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -17,7 +17,7 @@ from homeassistant.components.automation import ( from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, KNOWN_DEVICES, TRIGGERS @@ -89,13 +89,13 @@ class TriggerSource: ) -> CALLBACK_TYPE: """Attach a trigger.""" trigger_data = automation_info["trigger_data"] + job = HassJob(action) + @callback def event_handler(char): if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]: return - self._hass.async_create_task( - action({"trigger": {**trigger_data, **config}}) - ) + self._hass.async_run_hass_job(job, {"trigger": {**trigger_data, **config}}) trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]] iid = trigger["characteristic"] @@ -236,11 +236,11 @@ async def async_setup_triggers_for_entry( def async_fire_triggers(conn: HKDevice, events: dict[tuple[int, int], Any]): """Process events generated by a HomeKit accessory into automation triggers.""" + trigger_sources: dict[str, TriggerSource] = conn.hass.data[TRIGGERS] for (aid, iid), ev in events.items(): if aid in conn.devices: device_id = conn.devices[aid] - if device_id in conn.hass.data[TRIGGERS]: - source = conn.hass.data[TRIGGERS][device_id] + if source := trigger_sources.get(device_id): source.fire(iid, ev) From 939c33b1dc1ab8a1c32fb39f718adc003fbace85 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 17 Jul 2022 13:31:14 -0700 Subject: [PATCH 2648/3516] Exclude calendar description from recorder (#75375) --- homeassistant/components/calendar/recorder.py | 10 +++++ tests/components/calendar/test_recorder.py | 44 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 homeassistant/components/calendar/recorder.py create mode 100644 tests/components/calendar/test_recorder.py diff --git a/homeassistant/components/calendar/recorder.py b/homeassistant/components/calendar/recorder.py new file mode 100644 index 00000000000..4aba7b409cc --- /dev/null +++ b/homeassistant/components/calendar/recorder.py @@ -0,0 +1,10 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant, callback + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude potentially large attributes from being recorded in the database.""" + return {"description"} diff --git a/tests/components/calendar/test_recorder.py b/tests/components/calendar/test_recorder.py new file mode 100644 index 00000000000..0fbcaf38432 --- /dev/null +++ b/tests/components/calendar/test_recorder.py @@ -0,0 +1,44 @@ +"""The tests for calendar recorder.""" + +from datetime import timedelta + +from homeassistant.components.recorder.db_schema import StateAttributes, States +from homeassistant.components.recorder.util import session_scope +from homeassistant.const import ATTR_FRIENDLY_NAME +from homeassistant.core import State +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed +from tests.components.recorder.common import async_wait_recording_done + + +async def test_events_http_api(hass, recorder_mock): + """Test the calendar demo view.""" + await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) + await hass.async_block_till_done() + + state = hass.states.get("calendar.calendar_1") + assert state + assert ATTR_FRIENDLY_NAME in state.attributes + assert "description" in state.attributes + + # calendar.calendar_1 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + await async_wait_recording_done(hass) + + def _fetch_states() -> list[State]: + with session_scope(hass=hass) as session: + native_states = [] + for db_state, db_state_attributes in session.query(States, StateAttributes): + state = db_state.to_native() + state.attributes = db_state_attributes.to_native() + native_states.append(state) + return native_states + + states: list[State] = await hass.async_add_executor_job(_fetch_states) + assert len(states) > 1 + for state in states: + assert ATTR_FRIENDLY_NAME in state.attributes + assert "description" not in state.attributes From a95c2c7850462478f7d2cbe1d5b36bb3c2cd7d90 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Jul 2022 16:13:12 -0500 Subject: [PATCH 2649/3516] Avoid throwing on unsupported bleak client filter (#75378) * Avoid throwing on unsupported bleak client filter * Avoid throwing on unsupported bleak client filter --- homeassistant/components/bluetooth/models.py | 5 +++-- tests/components/bluetooth/test_init.py | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 43d4d0cb923..eda94305aa9 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -113,9 +113,10 @@ class HaBleakScannerWrapper(BaseBleakScanner): # type: ignore[misc] """Map the filters.""" mapped_filters = {} if filters := kwargs.get("filters"): - if FILTER_UUIDS not in filters: + if filter_uuids := filters.get(FILTER_UUIDS): + mapped_filters[FILTER_UUIDS] = set(filter_uuids) + else: _LOGGER.warning("Only %s filters are supported", FILTER_UUIDS) - mapped_filters = {k: set(v) for k, v in filters.items()} if service_uuids := kwargs.get("service_uuids"): mapped_filters[FILTER_UUIDS] = set(service_uuids) if mapped_filters == self._mapped_filters: diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 1d348f42f0e..ec737cef593 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -686,6 +686,9 @@ async def test_wrapped_instance_unsupported_filter( assert models.HA_BLEAK_SCANNER is not None scanner = models.HaBleakScannerWrapper() scanner.set_scanning_filter( - filters={"unsupported": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} + filters={ + "unsupported": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + "DuplicateData": True, + } ) assert "Only UUIDs filters are supported" in caplog.text From 91f2550bc3f8d0fc852d8e7a1fbf201a0279aebc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Jul 2022 17:25:45 -0500 Subject: [PATCH 2650/3516] Change manufacturer_data_first_byte to manufacturer_data_start (#75379) --- homeassistant/components/bluetooth/__init__.py | 18 ++++++++++-------- .../homekit_controller/manifest.json | 2 +- homeassistant/generated/bluetooth.py | 6 ++++-- homeassistant/loader.py | 2 +- script/hassfest/bluetooth.py | 2 +- script/hassfest/manifest.py | 2 +- tests/components/bluetooth/test_init.py | 8 +++++--- tests/test_loader.py | 4 ++-- 8 files changed, 25 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index b6058767669..ea058a72759 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -71,7 +71,7 @@ ADDRESS: Final = "address" LOCAL_NAME: Final = "local_name" SERVICE_UUID: Final = "service_uuid" MANUFACTURER_ID: Final = "manufacturer_id" -MANUFACTURER_DATA_FIRST_BYTE: Final = "manufacturer_data_first_byte" +MANUFACTURER_DATA_START: Final = "manufacturer_data_start" BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") @@ -157,14 +157,16 @@ def _ble_device_matches( return False if ( - matcher_manufacturer_data_first_byte := matcher.get( - MANUFACTURER_DATA_FIRST_BYTE + matcher_manufacturer_data_start := matcher.get(MANUFACTURER_DATA_START) + ) is not None: + matcher_manufacturer_data_start_bytes = bytearray( + matcher_manufacturer_data_start ) - ) is not None and not any( - matcher_manufacturer_data_first_byte == manufacturer_data[0] - for manufacturer_data in advertisement_data.manufacturer_data.values() - ): - return False + if not any( + manufacturer_data.startswith(matcher_manufacturer_data_start_bytes) + for manufacturer_data in advertisement_data.manufacturer_data.values() + ): + return False return True diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index a4f5350a167..17692476834 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/homekit_controller", "requirements": ["aiohomekit==1.1.4"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], - "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_first_byte": 6 }], + "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], "iot_class": "local_push", diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index feac27af1f1..03486b6043c 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -6,11 +6,13 @@ from __future__ import annotations # fmt: off -BLUETOOTH: list[dict[str, str | int]] = [ +BLUETOOTH: list[dict[str, str | int | list[int]]] = [ { "domain": "homekit_controller", "manufacturer_id": 76, - "manufacturer_data_first_byte": 6 + "manufacturer_data_start": [ + 6 + ] }, { "domain": "switchbot", diff --git a/homeassistant/loader.py b/homeassistant/loader.py index d0e6189ef96..9de06c48786 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -89,7 +89,7 @@ class BluetoothMatcherOptional(TypedDict, total=False): local_name: str service_uuid: str manufacturer_id: int - manufacturer_data_first_byte: int + manufacturer_data_start: list[int] class BluetoothMatcher(BluetoothMatcherRequired, BluetoothMatcherOptional): diff --git a/script/hassfest/bluetooth.py b/script/hassfest/bluetooth.py index 77a8779efbd..d8277213f27 100644 --- a/script/hassfest/bluetooth.py +++ b/script/hassfest/bluetooth.py @@ -14,7 +14,7 @@ from __future__ import annotations # fmt: off -BLUETOOTH: list[dict[str, str | int]] = {} +BLUETOOTH: list[dict[str, str | int | list[int]]] = {} """.strip() diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 4f76fb9ed1e..9c1bd0a63f3 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -196,7 +196,7 @@ MANIFEST_SCHEMA = vol.Schema( vol.Optional("service_uuid"): vol.All(str, verify_lowercase), vol.Optional("local_name"): vol.All(str), vol.Optional("manufacturer_id"): int, - vol.Optional("manufacturer_data_first_byte"): int, + vol.Optional("manufacturer_data_start"): [int], } ) ], diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index ec737cef593..a31f911f6a7 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -153,12 +153,12 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): async def test_discovery_match_by_manufacturer_id_and_first_byte( hass, mock_bleak_scanner_start ): - """Test bluetooth discovery match by manufacturer_id and manufacturer_data_first_byte.""" + """Test bluetooth discovery match by manufacturer_id and manufacturer_data_start.""" mock_bt = [ { "domain": "homekit_controller", "manufacturer_id": 76, - "manufacturer_data_first_byte": 0x06, + "manufacturer_data_start": [0x06, 0x02, 0x03], } ] with patch( @@ -174,7 +174,9 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( hkc_device = BLEDevice("44:44:33:11:23:45", "lock") hkc_adv = AdvertisementData( - local_name="lock", service_uuids=[], manufacturer_data={76: b"\x06"} + local_name="lock", + service_uuids=[], + manufacturer_data={76: b"\x06\x02\x03\x99"}, ) models.HA_BLEAK_SCANNER._callback(hkc_device, hkc_adv) diff --git a/tests/test_loader.py b/tests/test_loader.py index b8a469f80aa..da788e0db75 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -205,7 +205,7 @@ def test_integration_properties(hass): {"hostname": "tesla_*", "macaddress": "98ED5C*"}, {"registered_devices": True}, ], - "bluetooth": [{"manufacturer_id": 76, "manufacturer_data_first_byte": 6}], + "bluetooth": [{"manufacturer_id": 76, "manufacturer_data_start": [0x06]}], "usb": [ {"vid": "10C4", "pid": "EA60"}, {"vid": "1CF1", "pid": "0030"}, @@ -244,7 +244,7 @@ def test_integration_properties(hass): {"vid": "10C4", "pid": "8A2A"}, ] assert integration.bluetooth == [ - {"manufacturer_id": 76, "manufacturer_data_first_byte": 6} + {"manufacturer_id": 76, "manufacturer_data_start": [0x06]} ] assert integration.ssdp == [ { From a8bb00f30581968eda231feda740790c5e64503a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Jul 2022 17:45:04 -0500 Subject: [PATCH 2651/3516] Fix availability in HKC for sleeping bluetooth devices (#75357) --- .../homekit_controller/connection.py | 79 +++++++++------- .../homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../homekit_controller/test_init.py | 89 ++++++++++++++++--- 5 files changed, 126 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 7e4dcc59be0..b2f3b3ae8e0 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable -import datetime +from datetime import timedelta import logging from types import MappingProxyType from typing import Any @@ -14,8 +14,8 @@ from aiohomekit.exceptions import ( AccessoryNotFoundError, EncryptionError, ) -from aiohomekit.model import Accessories, Accessory -from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes +from aiohomekit.model import Accessories, Accessory, Transport +from aiohomekit.model.characteristics import Characteristic from aiohomekit.model.services import Service from homeassistant.config_entries import ConfigEntry @@ -40,9 +40,9 @@ from .const import ( from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry from .storage import EntityMapStorage -DEFAULT_SCAN_INTERVAL = datetime.timedelta(seconds=60) RETRY_INTERVAL = 60 # seconds MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3 +BLE_AVAILABILITY_CHECK_INTERVAL = 1800 # seconds _LOGGER = logging.getLogger(__name__) @@ -122,6 +122,7 @@ class HKDevice: # If this is set polling is active and can be disabled by calling # this method. self._polling_interval_remover: CALLBACK_TYPE | None = None + self._ble_available_interval_remover: CALLBACK_TYPE | None = None # Never allow concurrent polling of the same accessory or bridge self._polling_lock = asyncio.Lock() @@ -133,9 +134,6 @@ class HKDevice: self.watchable_characteristics: list[tuple[int, int]] = [] - self.pairing.dispatcher_connect(self.process_new_events) - self.pairing.dispatcher_connect_config_changed(self.process_config_changed) - @property def entity_map(self) -> Accessories: """Return the accessories from the pairing.""" @@ -182,32 +180,15 @@ class HKDevice: self.available = available async_dispatcher_send(self.hass, self.signal_state_updated) - async def async_ensure_available(self) -> None: - """Verify the accessory is available after processing the entity map.""" - if self.available: - return - if self.watchable_characteristics and self.pollable_characteristics: - # We already tried, no need to try again - return - # We there are no watchable and not pollable characteristics, - # we need to force a connection to the device to verify its alive. - # - # This is similar to iOS's behavior for keeping alive connections - # to cameras. - # - primary = self.entity_map.accessories[0] - aid = primary.aid - iid = primary.accessory_information[CharacteristicsTypes.SERIAL_NUMBER].iid - await self.pairing.get_characteristics([(aid, iid)]) - self.async_set_available_state(True) - async def async_setup(self) -> None: """Prepare to use a paired HomeKit device in Home Assistant.""" entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP] + pairing = self.pairing + transport = pairing.transport + entry = self.config_entry + if cache := entity_storage.get_map(self.unique_id): - self.pairing.restore_accessories_state( - cache["accessories"], cache["config_num"] - ) + pairing.restore_accessories_state(cache["accessories"], cache["config_num"]) # We need to force an update here to make sure we have # the latest values since the async_update we do in @@ -219,22 +200,47 @@ class HKDevice: # Ideally we would know which entities we are about to add # so we only poll those chars but that is not possible # yet. - await self.pairing.async_populate_accessories_state(force_update=True) + try: + await self.pairing.async_populate_accessories_state(force_update=True) + except AccessoryNotFoundError: + if transport != Transport.BLE or not cache: + # BLE devices may sleep and we can't force a connection + raise + + entry.async_on_unload(pairing.dispatcher_connect(self.process_new_events)) + entry.async_on_unload( + pairing.dispatcher_connect_config_changed(self.process_config_changed) + ) + entry.async_on_unload( + pairing.dispatcher_availability_changed(self.async_set_available_state) + ) await self.async_process_entity_map() - await self.async_ensure_available() - if not cache: # If its missing from the cache, make sure we save it self.async_save_entity_map() # If everything is up to date, we can create the entities # since we know the data is not stale. await self.async_add_new_entities() + + self.async_set_available_state(self.pairing.is_available) + self._polling_interval_remover = async_track_time_interval( - self.hass, self.async_update, DEFAULT_SCAN_INTERVAL + self.hass, self.async_update, self.pairing.poll_interval ) + if transport == Transport.BLE: + # If we are using BLE, we need to periodically check of the + # BLE device is available since we won't get callbacks + # when it goes away since we HomeKit supports disconnected + # notifications and we cannot treat a disconnect as unavailability. + self._ble_available_interval_remover = async_track_time_interval( + self.hass, + self.async_update_available_state, + timedelta(seconds=BLE_AVAILABILITY_CHECK_INTERVAL), + ) + async def async_add_new_entities(self) -> None: """Add new entities to Home Assistant.""" await self.async_load_platforms() @@ -546,10 +552,15 @@ class HKDevice: if tasks: await asyncio.gather(*tasks) + @callback + def async_update_available_state(self, *_: Any) -> None: + """Update the available state of the device.""" + self.async_set_available_state(self.pairing.is_available) + async def async_update(self, now=None): """Poll state of all entities attached to this bridge/accessory.""" if not self.pollable_characteristics: - self.async_set_available_state(self.pairing.is_connected) + self.async_update_available_state() _LOGGER.debug( "HomeKit connection not polling any characteristics: %s", self.unique_id ) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 17692476834..29268f3b6e9 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.1.4"], + "requirements": ["aiohomekit==1.1.5"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "after_dependencies": ["zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 47ff7005c66..a8d87c8a557 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.4 +aiohomekit==1.1.5 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a9d015738f5..ebfaabd16c3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.4 +aiohomekit==1.1.5 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index df14ad325b6..37d41fcf372 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -3,15 +3,15 @@ from datetime import timedelta from unittest.mock import patch -from aiohomekit import AccessoryDisconnectedError -from aiohomekit.model import Accessory +from aiohomekit import AccessoryNotFoundError +from aiohomekit.model import Accessory, Transport from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes from aiohomekit.testing import FakePairing from homeassistant.components.homekit_controller.const import DOMAIN, ENTITY_MAP from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import EntityRegistry @@ -104,31 +104,35 @@ async def test_offline_device_raises(hass, controller): is_connected = False class OfflineFakePairing(FakePairing): - """Fake pairing that always returns False for is_connected.""" + """Fake pairing that can flip is_connected.""" @property def is_connected(self): nonlocal is_connected return is_connected + @property + def is_available(self): + return self.is_connected + async def async_populate_accessories_state(self, *args, **kwargs): nonlocal is_connected if not is_connected: - raise AccessoryDisconnectedError("any") + raise AccessoryNotFoundError("any") async def get_characteristics(self, chars, *args, **kwargs): nonlocal is_connected if not is_connected: - raise AccessoryDisconnectedError("any") + raise AccessoryNotFoundError("any") return {} + accessory = Accessory.create_with_info( + "TestDevice", "example.com", "Test", "0001", "0.1" + ) + create_alive_service(accessory) + with patch("aiohomekit.testing.FakePairing", OfflineFakePairing): await async_setup_component(hass, DOMAIN, {}) - accessory = Accessory.create_with_info( - "TestDevice", "example.com", "Test", "0001", "0.1" - ) - create_alive_service(accessory) - config_entry, _ = await setup_test_accessories_with_controller( hass, [accessory], controller ) @@ -141,3 +145,66 @@ async def test_offline_device_raises(hass, controller): async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) await hass.async_block_till_done() assert config_entry.state == ConfigEntryState.LOADED + assert hass.states.get("light.testdevice").state == STATE_OFF + + +async def test_ble_device_only_checks_is_available(hass, controller): + """Test a BLE device only checks is_available.""" + + is_available = False + + class FakeBLEPairing(FakePairing): + """Fake BLE pairing that can flip is_available.""" + + @property + def transport(self): + return Transport.BLE + + @property + def is_connected(self): + return False + + @property + def is_available(self): + nonlocal is_available + return is_available + + async def async_populate_accessories_state(self, *args, **kwargs): + nonlocal is_available + if not is_available: + raise AccessoryNotFoundError("any") + + async def get_characteristics(self, chars, *args, **kwargs): + nonlocal is_available + if not is_available: + raise AccessoryNotFoundError("any") + return {} + + accessory = Accessory.create_with_info( + "TestDevice", "example.com", "Test", "0001", "0.1" + ) + create_alive_service(accessory) + + with patch("aiohomekit.testing.FakePairing", FakeBLEPairing): + await async_setup_component(hass, DOMAIN, {}) + config_entry, _ = await setup_test_accessories_with_controller( + hass, [accessory], controller + ) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.SETUP_RETRY + + is_available = True + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert config_entry.state == ConfigEntryState.LOADED + assert hass.states.get("light.testdevice").state == STATE_OFF + + is_available = False + async_fire_time_changed(hass, utcnow() + timedelta(hours=1)) + assert hass.states.get("light.testdevice").state == STATE_UNAVAILABLE + + is_available = True + async_fire_time_changed(hass, utcnow() + timedelta(hours=1)) + assert hass.states.get("light.testdevice").state == STATE_OFF From 4d5673013b7a9e1e7f58506f960cfa60e428db85 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 18 Jul 2022 00:24:34 +0000 Subject: [PATCH 2652/3516] [ci skip] Translation update --- .../components/generic/translations/tr.json | 4 +++ .../here_travel_time/translations/tr.json | 7 +++++ .../homeassistant/translations/tr.json | 1 + .../homekit_controller/translations/ca.json | 4 +-- .../homekit_controller/translations/et.json | 4 +-- .../translations/zh-Hant.json | 4 +-- .../components/nextdns/translations/tr.json | 29 +++++++++++++++++++ .../components/rhasspy/translations/tr.json | 12 ++++++++ .../components/verisure/translations/tr.json | 15 +++++++++- .../components/withings/translations/tr.json | 4 +++ 10 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/nextdns/translations/tr.json create mode 100644 homeassistant/components/rhasspy/translations/tr.json diff --git a/homeassistant/components/generic/translations/tr.json b/homeassistant/components/generic/translations/tr.json index d439c559aa5..efce9d014b1 100644 --- a/homeassistant/components/generic/translations/tr.json +++ b/homeassistant/components/generic/translations/tr.json @@ -7,7 +7,9 @@ "error": { "already_exists": "Bu URL ayarlar\u0131na sahip bir kamera zaten var.", "invalid_still_image": "URL ge\u00e7erli bir hareketsiz resim d\u00f6nd\u00fcrmedi", + "malformed_url": "Hatal\u0131 bi\u00e7imlendirilmi\u015f URL", "no_still_image_or_stream_url": "En az\u0131ndan bir dura\u011fan resim veya ak\u0131\u015f URL'si belirtmelisiniz", + "relative_url": "G\u00f6receli URL'lere izin verilmez", "stream_file_not_found": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken dosya bulunamad\u0131 (ffmpeg y\u00fckl\u00fc m\u00fc?)", "stream_http_not_found": "HTTP 404 Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken bulunamad\u0131", "stream_io_error": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken Giri\u015f/\u00c7\u0131k\u0131\u015f hatas\u0131. Yanl\u0131\u015f RTSP aktar\u0131m protokol\u00fc?", @@ -50,7 +52,9 @@ "error": { "already_exists": "Bu URL ayarlar\u0131na sahip bir kamera zaten var.", "invalid_still_image": "URL ge\u00e7erli bir hareketsiz resim d\u00f6nd\u00fcrmedi", + "malformed_url": "Hatal\u0131 bi\u00e7imlendirilmi\u015f URL", "no_still_image_or_stream_url": "En az\u0131ndan bir dura\u011fan resim veya ak\u0131\u015f URL'si belirtmelisiniz", + "relative_url": "G\u00f6receli URL'lere izin verilmez", "stream_file_not_found": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken dosya bulunamad\u0131 (ffmpeg y\u00fckl\u00fc m\u00fc?)", "stream_http_not_found": "HTTP 404 Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken bulunamad\u0131", "stream_io_error": "Ak\u0131\u015fa ba\u011flanmaya \u00e7al\u0131\u015f\u0131rken Giri\u015f/\u00c7\u0131k\u0131\u015f hatas\u0131. Yanl\u0131\u015f RTSP aktar\u0131m protokol\u00fc?", diff --git a/homeassistant/components/here_travel_time/translations/tr.json b/homeassistant/components/here_travel_time/translations/tr.json index 181588ba54a..10be792e0ee 100644 --- a/homeassistant/components/here_travel_time/translations/tr.json +++ b/homeassistant/components/here_travel_time/translations/tr.json @@ -39,6 +39,13 @@ }, "title": "Kalk\u0131\u015f Se\u00e7in" }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Bir harita konumu kullan\u0131n", + "origin_entity": "Bir varl\u0131\u011f\u0131 kullan\u0131n" + }, + "title": "Kalk\u0131\u015f Se\u00e7in" + }, "user": { "data": { "api_key": "API Anahtar\u0131", diff --git a/homeassistant/components/homeassistant/translations/tr.json b/homeassistant/components/homeassistant/translations/tr.json index cffbee33e74..7ec8074702b 100644 --- a/homeassistant/components/homeassistant/translations/tr.json +++ b/homeassistant/components/homeassistant/translations/tr.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU Mimarisi", + "config_dir": "Yap\u0131land\u0131rma Dizini", "dev": "Geli\u015ftirme", "docker": "Docker", "hassio": "S\u00fcperviz\u00f6r", diff --git a/homeassistant/components/homekit_controller/translations/ca.json b/homeassistant/components/homekit_controller/translations/ca.json index ff9f180c943..5cb98f7287d 100644 --- a/homeassistant/components/homekit_controller/translations/ca.json +++ b/homeassistant/components/homekit_controller/translations/ca.json @@ -18,7 +18,7 @@ "unable_to_pair": "No s'ha pogut vincular, torna-ho a provar.", "unknown_error": "El dispositiu ha em\u00e8s un error desconegut. Vinculaci\u00f3 fallida." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Atura la vinculaci\u00f3 a tots els controladors o prova de reiniciar el dispositiu, despr\u00e9s, segueix amb la vinculaci\u00f3.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Permet la vinculaci\u00f3 amb codis de configuraci\u00f3 insegurs.", "pairing_code": "Codi de vinculaci\u00f3" }, - "description": "El controlador HomeKit es comunica amb {name} a trav\u00e9s de la xarxa d'\u00e0rea local utilitzant una connexi\u00f3 segura encriptada sense un HomeKit o iCloud separats. Introdueix el codi de vinculaci\u00f3 de HomeKit (en format XXX-XX-XXX) per utilitzar aquest accessori. Aquest codi es troba normalment en el propi dispositiu o en la seva caixa.", + "description": "El controlador HomeKit es comunica amb {name} ({category}) a trav\u00e9s de la xarxa d'\u00e0rea local utilitzant una connexi\u00f3 segura xifrada sense un HomeKit o iCloud separats. Introdueix el codi de vinculaci\u00f3 de HomeKit (en format XXX-XX-XXX) per utilitzar aquest accessori. Aquest codi es troba normalment en el mateix dispositiu o en la seva caixa.", "title": "Vinculaci\u00f3 amb un dispositiu a trav\u00e9s de HomeKit Accessory Protocol" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/et.json b/homeassistant/components/homekit_controller/translations/et.json index c658213eea0..8db1df29803 100644 --- a/homeassistant/components/homekit_controller/translations/et.json +++ b/homeassistant/components/homekit_controller/translations/et.json @@ -18,7 +18,7 @@ "unable_to_pair": "Ei saa siduda, proovi uuesti.", "unknown_error": "Seade teatas tundmatust t\u00f5rkest. Sidumine nurjus." }, - "flow_title": "{name}", + "flow_title": "{name} ( {category} )", "step": { "busy_error": { "description": "Katkesta sidumine k\u00f5igis kontrollerites v\u00f5i proovi seade taask\u00e4ivitada ja j\u00e4tka sidumist.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Luba sidumist ebaturvalise salas\u00f5naga.", "pairing_code": "Sidumiskood" }, - "description": "HomeKiti kontroller suhtleb seadmega {name} kohtv\u00f5rgu kaudu, kasutades turvalist kr\u00fcpteeritud \u00fchendust ilma eraldi HomeKiti kontrolleri v\u00f5i iCloudita. Selle lisaseadme kasutamiseks sisesta oma HomeKiti sidumiskood (vormingus XXX-XX-XXX). See kood on tavaliselt seadmel v\u00f5i pakendil.", + "description": "HomeKiti kontroller suhtleb seadmega {name}({category}) kohtv\u00f5rgu kaudu, kasutades turvalist kr\u00fcpteeritud \u00fchendust ilma eraldi HomeKiti kontrolleri v\u00f5i iCloudita. Selle lisaseadme kasutamiseks sisesta oma HomeKiti sidumiskood (vormingus XXX-XX-XXX). See kood on tavaliselt seadmel v\u00f5i pakendil.", "title": "Seadme sidumine HomeKiti tarvikuprotokolli kaudu" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/zh-Hant.json b/homeassistant/components/homekit_controller/translations/zh-Hant.json index e13b69bd0fd..63b4b45f520 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hant.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hant.json @@ -18,7 +18,7 @@ "unable_to_pair": "\u7121\u6cd5\u914d\u5c0d\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002", "unknown_error": "\u88dd\u7f6e\u56de\u5831\u672a\u77e5\u932f\u8aa4\u3002\u914d\u5c0d\u5931\u6557\u3002" }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "\u53d6\u6d88\u6240\u6709\u63a7\u5236\u5668\u914d\u5c0d\uff0c\u6216\u8005\u91cd\u555f\u88dd\u7f6e\u3001\u7136\u5f8c\u518d\u7e7c\u7e8c\u914d\u5c0d\u3002", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "\u5141\u8a31\u8207\u4e0d\u5b89\u5168\u8a2d\u5b9a\u4ee3\u78bc\u9032\u884c\u914d\u5c0d\u3002", "pairing_code": "\u8a2d\u5b9a\u4ee3\u78bc" }, - "description": "\u4f7f\u7528 {name} \u4e4b HomeKit \u63a7\u5236\u5668\u901a\u8a0a\u4f7f\u7528\u52a0\u5bc6\u9023\u7dda\uff0c\u4e26\u4e0d\u9700\u8981\u984d\u5916\u7684 HomeKit \u63a7\u5236\u5668\u6216 iCloud \u9023\u7dda\u3002\u8f38\u5165\u914d\u4ef6 Homekit \u8a2d\u5b9a\u914d\u5c0d\u4ee3\u78bc\uff08\u683c\u5f0f\uff1aXXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6\u3002\u4ee3\u78bc\u901a\u5e38\u53ef\u4ee5\u65bc\u88dd\u7f6e\u6216\u8005\u5305\u88dd\u4e0a\u627e\u5230\u3002", + "description": "\u4f7f\u7528 {name} ({category}) \u4e4b HomeKit \u63a7\u5236\u5668\u672c\u5730\u7aef\u901a\u8a0a\u4f7f\u7528\u52a0\u5bc6\u9023\u7dda\uff0c\u4e26\u4e0d\u9700\u8981\u984d\u5916\u7684 HomeKit \u63a7\u5236\u5668\u6216 iCloud \u9023\u7dda\u3002\u8f38\u5165\u914d\u4ef6 Homekit \u8a2d\u5b9a\u914d\u5c0d\u4ee3\u78bc\uff08\u683c\u5f0f\uff1aXXX-XX-XXX\uff09\u4ee5\u4f7f\u7528\u6b64\u914d\u4ef6\u3002\u4ee3\u78bc\u901a\u5e38\u53ef\u4ee5\u65bc\u88dd\u7f6e\u6216\u8005\u5305\u88dd\u4e0a\u627e\u5230\u3002", "title": "\u900f\u904e HomeKit \u914d\u4ef6\u901a\u8a0a\u5354\u5b9a\u6240\u914d\u5c0d\u88dd\u7f6e" }, "protocol_error": { diff --git a/homeassistant/components/nextdns/translations/tr.json b/homeassistant/components/nextdns/translations/tr.json new file mode 100644 index 00000000000..5cf9dc9b2c2 --- /dev/null +++ b/homeassistant/components/nextdns/translations/tr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Bu NextDNS profili zaten yap\u0131land\u0131r\u0131lm\u0131\u015f." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "API Anahtar\u0131" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Eri\u015fim sunucusu" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rhasspy/translations/tr.json b/homeassistant/components/rhasspy/translations/tr.json new file mode 100644 index 00000000000..a8f9bf1204c --- /dev/null +++ b/homeassistant/components/rhasspy/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "user": { + "description": "Rhasspy deste\u011fini etkinle\u015ftirmek istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/tr.json b/homeassistant/components/verisure/translations/tr.json index 88a0d943872..e339b3d6997 100644 --- a/homeassistant/components/verisure/translations/tr.json +++ b/homeassistant/components/verisure/translations/tr.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "unknown": "Beklenmeyen hata" + "unknown": "Beklenmeyen hata", + "unknown_mfa": "MFA kurulumu s\u0131ras\u0131nda bilinmeyen bir hata olu\u015ftu" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant, Sayfalar\u0131m hesab\u0131n\u0131zda birden fazla Verisure y\u00fcklemesi buldu. L\u00fctfen Home Assistant'a eklemek i\u00e7in kurulumu se\u00e7in." }, + "mfa": { + "data": { + "code": "Do\u011frulama kodu", + "description": "Hesab\u0131n\u0131zda 2 ad\u0131ml\u0131 do\u011frulama etkinle\u015ftirildi. L\u00fctfen Verisure'un size g\u00f6nderdi\u011fi do\u011frulama kodunu girin." + } + }, "reauth_confirm": { "data": { "description": "Verisure My Pages hesab\u0131n\u0131zla yeniden kimlik do\u011frulamas\u0131 yap\u0131n.", @@ -22,6 +29,12 @@ "password": "Parola" } }, + "reauth_mfa": { + "data": { + "code": "Do\u011frulama kodu", + "description": "Hesab\u0131n\u0131zda 2 ad\u0131ml\u0131 do\u011frulama etkinle\u015ftirildi. L\u00fctfen Verisure'un size g\u00f6nderdi\u011fi do\u011frulama kodunu girin." + } + }, "user": { "data": { "description": "Verisure My Pages hesab\u0131n\u0131zla oturum a\u00e7\u0131n.", diff --git a/homeassistant/components/withings/translations/tr.json b/homeassistant/components/withings/translations/tr.json index 31b2a424071..698fe41988c 100644 --- a/homeassistant/components/withings/translations/tr.json +++ b/homeassistant/components/withings/translations/tr.json @@ -27,6 +27,10 @@ "reauth": { "description": "Withings verilerini almaya devam etmek i\u00e7in \" {profile}", "title": "Entegrasyonu Yeniden Do\u011frula" + }, + "reauth_confirm": { + "description": "Withings verilerini almaya devam etmek i\u00e7in \" {profile} \" profilinin yeniden do\u011frulanmas\u0131 gerekiyor.", + "title": "Entegrasyonu Yeniden Do\u011frula" } } } From e522b6e3b898767a383f3918cf9afc4518fe810d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Jul 2022 01:36:01 -0500 Subject: [PATCH 2653/3516] Bump aiohomekit to 1.1.7 (#75384) BLEDevice is now passed around instead of the address internally to avoid bleak creating a new scanner each time a client needs to resolve the address to os details --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 29268f3b6e9..192a5cb701a 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.1.5"], + "requirements": ["aiohomekit==1.1.7"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "after_dependencies": ["zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index a8d87c8a557..1f549530318 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.5 +aiohomekit==1.1.7 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ebfaabd16c3..ba7307a7cc3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.5 +aiohomekit==1.1.7 # homeassistant.components.emulated_hue # homeassistant.components.http From 943e0b9cf7b9b99cf2e9c156da068e0ff4fd140f Mon Sep 17 00:00:00 2001 From: Matrix Date: Mon, 18 Jul 2022 17:40:18 +0800 Subject: [PATCH 2654/3516] Yolink feature garage door (#75120) --- .coveragerc | 1 + homeassistant/components/yolink/__init__.py | 1 + .../components/yolink/binary_sensor.py | 9 +- homeassistant/components/yolink/cover.py | 86 +++++++++++++++++++ homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/yolink/cover.py diff --git a/.coveragerc b/.coveragerc index 0a2e8e57fa8..0fb2210b3cd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1498,6 +1498,7 @@ omit = homeassistant/components/yolink/climate.py homeassistant/components/yolink/const.py homeassistant/components/yolink/coordinator.py + homeassistant/components/yolink/cover.py homeassistant/components/yolink/entity.py homeassistant/components/yolink/lock.py homeassistant/components/yolink/sensor.py diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 714b0b25070..3257d64d265 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -27,6 +27,7 @@ SCAN_INTERVAL = timedelta(minutes=5) PLATFORMS = [ Platform.BINARY_SENSOR, Platform.CLIMATE, + Platform.COVER, Platform.LOCK, Platform.SENSOR, Platform.SIREN, diff --git a/homeassistant/components/yolink/binary_sensor.py b/homeassistant/components/yolink/binary_sensor.py index b296e01fa56..6e29bccf437 100644 --- a/homeassistant/components/yolink/binary_sensor.py +++ b/homeassistant/components/yolink/binary_sensor.py @@ -47,6 +47,13 @@ SENSOR_DEVICE_TYPE = [ ] +def is_door_sensor(device: YoLinkDevice) -> bool: + """Check Door Sensor type.""" + return device.device_type == ATTR_DEVICE_DOOR_SENSOR and ( + device.parent_id is None or device.parent_id == "null" + ) + + SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( YoLinkBinarySensorEntityDescription( key="door_state", @@ -54,7 +61,7 @@ SENSOR_TYPES: tuple[YoLinkBinarySensorEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.DOOR, name="State", value=lambda value: value == "open" if value is not None else None, - exists_fn=lambda device: device.device_type == ATTR_DEVICE_DOOR_SENSOR, + exists_fn=is_door_sensor, ), YoLinkBinarySensorEntityDescription( key="motion_state", diff --git a/homeassistant/components/yolink/cover.py b/homeassistant/components/yolink/cover.py new file mode 100644 index 00000000000..e7e32764ca8 --- /dev/null +++ b/homeassistant/components/yolink/cover.py @@ -0,0 +1,86 @@ +"""YoLink Garage Door.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.cover import ( + CoverDeviceClass, + CoverEntity, + CoverEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ATTR_COORDINATORS, ATTR_DEVICE_DOOR_SENSOR, DOMAIN +from .coordinator import YoLinkCoordinator +from .entity import YoLinkEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up YoLink garage door from a config entry.""" + device_coordinators = hass.data[DOMAIN][config_entry.entry_id][ATTR_COORDINATORS] + entities = [ + YoLinkCoverEntity(config_entry, device_coordinator) + for device_coordinator in device_coordinators.values() + if device_coordinator.device.device_type == ATTR_DEVICE_DOOR_SENSOR + and device_coordinator.device.parent_id is not None + and device_coordinator.device.parent_id != "null" + ] + async_add_entities(entities) + + +class YoLinkCoverEntity(YoLinkEntity, CoverEntity): + """YoLink Cover Entity.""" + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: YoLinkCoordinator, + ) -> None: + """Init YoLink garage door entity.""" + super().__init__(config_entry, coordinator) + self._attr_unique_id = f"{coordinator.device.device_id}_door_state" + self._attr_name = f"{coordinator.device.device_name} (State)" + self._attr_device_class = CoverDeviceClass.GARAGE + self._attr_supported_features = ( + CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE + ) + + @callback + def update_entity_state(self, state: dict[str, Any]) -> None: + """Update HA Entity State.""" + self._attr_is_closed = state.get("state") == "closed" + self.async_write_ha_state() + + async def toggle_garage_state(self, state: str) -> None: + """Toggle Garage door state.""" + # make sure current state is correct + await self.coordinator.async_refresh() + if state == "open" and self.is_closed is False: + return + if state == "close" and self.is_closed is True: + return + # get paired controller + door_controller_coordinator = self.hass.data[DOMAIN][ + self.config_entry.entry_id + ][ATTR_COORDINATORS].get(self.coordinator.device.parent_id) + if door_controller_coordinator is None: + raise ValueError( + "This device has not been paired with a garage door controller" + ) + # call controller api open/close garage door + await door_controller_coordinator.device.call_device_http_api("toggle", None) + await self.coordinator.async_refresh() + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open garage door.""" + await self.toggle_garage_state("open") + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close garage door.""" + await self.toggle_garage_state("close") diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index d2a02a44c42..0db736938f7 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -3,7 +3,7 @@ "name": "YoLink", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yolink", - "requirements": ["yolink-api==0.0.8"], + "requirements": ["yolink-api==0.0.9"], "dependencies": ["auth", "application_credentials"], "codeowners": ["@matrixd2"], "iot_class": "cloud_push" diff --git a/requirements_all.txt b/requirements_all.txt index 1f549530318..ad2c15cf77b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2486,7 +2486,7 @@ yeelight==0.7.10 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.0.8 +yolink-api==0.0.9 # homeassistant.components.youless youless-api==0.16 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba7307a7cc3..784b740d459 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1662,7 +1662,7 @@ yalexs==1.1.25 yeelight==0.7.10 # homeassistant.components.yolink -yolink-api==0.0.8 +yolink-api==0.0.9 # homeassistant.components.youless youless-api==0.16 From ca5065a62734fef757ad05f55caa012aeb5e9bde Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 18 Jul 2022 02:44:51 -0700 Subject: [PATCH 2655/3516] Improve google calendar config flow timeout error messages (#75364) --- .../components/google/config_flow.py | 3 +++ homeassistant/components/google/strings.json | 1 + tests/components/google/test_config_flow.py | 21 ++++++++++++++++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google/config_flow.py b/homeassistant/components/google/config_flow.py index 22b62094e76..e320212ca1b 100644 --- a/homeassistant/components/google/config_flow.py +++ b/homeassistant/components/google/config_flow.py @@ -91,6 +91,9 @@ class OAuth2FlowHandler( self.flow_impl.client_secret, calendar_access, ) + except TimeoutError as err: + _LOGGER.error("Timeout initializing device flow: %s", str(err)) + return self.async_abort(reason="timeout_connect") except OAuthError as err: _LOGGER.error("Error initializing device flow: %s", str(err)) return self.async_abort(reason="oauth_error") diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json index 3ff75047f70..b4c5270e003 100644 --- a/homeassistant/components/google/strings.json +++ b/homeassistant/components/google/strings.json @@ -16,6 +16,7 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]", "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "code_expired": "Authentication code expired or credential setup is invalid, please try again.", diff --git a/tests/components/google/test_config_flow.py b/tests/components/google/test_config_flow.py index 24ad8a7b769..5c373fb2219 100644 --- a/tests/components/google/test_config_flow.py +++ b/tests/components/google/test_config_flow.py @@ -242,7 +242,7 @@ async def test_code_error( mock_code_flow: Mock, component_setup: ComponentSetup, ) -> None: - """Test successful creds setup.""" + """Test server error setting up the oauth flow.""" assert await component_setup() with patch( @@ -256,6 +256,25 @@ async def test_code_error( assert result.get("reason") == "oauth_error" +async def test_timeout_error( + hass: HomeAssistant, + mock_code_flow: Mock, + component_setup: ComponentSetup, +) -> None: + """Test timeout error setting up the oauth flow.""" + assert await component_setup() + + with patch( + "homeassistant.components.google.api.OAuth2WebServerFlow.step1_get_device_and_user_codes", + side_effect=TimeoutError(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == "abort" + assert result.get("reason") == "timeout_connect" + + @pytest.mark.parametrize("code_expiration_delta", [datetime.timedelta(seconds=50)]) async def test_expired_after_exchange( hass: HomeAssistant, From 6fdb414b58ec11a809d7c2ce9566797824faa62c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 18 Jul 2022 06:12:13 -0400 Subject: [PATCH 2656/3516] Migrate Goalzero to new entity naming style (#75358) --- .../components/goalzero/binary_sensor.py | 4 ++-- homeassistant/components/goalzero/entity.py | 6 ++--- homeassistant/components/goalzero/sensor.py | 22 +++++++++---------- homeassistant/components/goalzero/switch.py | 6 ++--- tests/components/goalzero/test_sensor.py | 2 +- 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/goalzero/binary_sensor.py b/homeassistant/components/goalzero/binary_sensor.py index c4219b51e6c..db8a1b788f2 100644 --- a/homeassistant/components/goalzero/binary_sensor.py +++ b/homeassistant/components/goalzero/binary_sensor.py @@ -26,7 +26,7 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( ), BinarySensorEntityDescription( key="app_online", - name="App Online", + name="App online", device_class=BinarySensorDeviceClass.CONNECTIVITY, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -37,7 +37,7 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( ), BinarySensorEntityDescription( key="inputDetected", - name="Input Detected", + name="Input detected", device_class=BinarySensorDeviceClass.POWER, ), ) diff --git a/homeassistant/components/goalzero/entity.py b/homeassistant/components/goalzero/entity.py index 8c696ce1377..eef6ea43d9c 100644 --- a/homeassistant/components/goalzero/entity.py +++ b/homeassistant/components/goalzero/entity.py @@ -15,6 +15,7 @@ class GoalZeroEntity(CoordinatorEntity[GoalZeroDataUpdateCoordinator]): """Representation of a Goal Zero Yeti entity.""" _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True def __init__( self, @@ -24,9 +25,6 @@ class GoalZeroEntity(CoordinatorEntity[GoalZeroDataUpdateCoordinator]): """Initialize a Goal Zero Yeti entity.""" super().__init__(coordinator) self.entity_description = description - self._attr_name = ( - f"{coordinator.config_entry.data[CONF_NAME]} {description.name}" - ) self._attr_unique_id = f"{coordinator.config_entry.entry_id}/{description.key}" @property @@ -37,7 +35,7 @@ class GoalZeroEntity(CoordinatorEntity[GoalZeroDataUpdateCoordinator]): identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)}, manufacturer=MANUFACTURER, model=self._api.sysdata[ATTR_MODEL], - name=self.coordinator.config_entry.data[CONF_NAME], + name=self.coordinator.config_entry.data[CONF_NAME].capitalize(), sw_version=self._api.data["firmwareVersion"], ) diff --git a/homeassistant/components/goalzero/sensor.py b/homeassistant/components/goalzero/sensor.py index ef95578820d..345c3b41f7d 100644 --- a/homeassistant/components/goalzero/sensor.py +++ b/homeassistant/components/goalzero/sensor.py @@ -32,14 +32,14 @@ from .entity import GoalZeroEntity SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="wattsIn", - name="Watts In", + name="Watts in", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="ampsIn", - name="Amps In", + name="Amps in", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -47,14 +47,14 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="wattsOut", - name="Watts Out", + name="Watts out", device_class=SensorDeviceClass.POWER, native_unit_of_measurement=POWER_WATT, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="ampsOut", - name="Amps Out", + name="Amps out", device_class=SensorDeviceClass.CURRENT, native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, state_class=SensorStateClass.MEASUREMENT, @@ -62,7 +62,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="whOut", - name="WH Out", + name="Wh out", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, state_class=SensorStateClass.TOTAL_INCREASING, @@ -70,7 +70,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="whStored", - name="WH Stored", + name="Wh stored", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_WATT_HOUR, state_class=SensorStateClass.MEASUREMENT, @@ -84,13 +84,13 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="socPercent", - name="State of Charge Percent", + name="State of charge percent", device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, ), SensorEntityDescription( key="timeToEmptyFull", - name="Time to Empty/Full", + name="Time to empty/full", device_class=TIME_MINUTES, native_unit_of_measurement=TIME_MINUTES, ), @@ -103,7 +103,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="wifiStrength", - name="Wifi Strength", + name="Wi-Fi strength", device_class=SensorDeviceClass.SIGNAL_STRENGTH, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, entity_registry_enabled_default=False, @@ -111,7 +111,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="timestamp", - name="Total Run Time", + name="Total run time", native_unit_of_measurement=TIME_SECONDS, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, @@ -124,7 +124,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="ipAddr", - name="IP Address", + name="IP address", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), diff --git a/homeassistant/components/goalzero/switch.py b/homeassistant/components/goalzero/switch.py index 9a58cb385b6..25fdeb52114 100644 --- a/homeassistant/components/goalzero/switch.py +++ b/homeassistant/components/goalzero/switch.py @@ -14,15 +14,15 @@ from .entity import GoalZeroEntity SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( SwitchEntityDescription( key="v12PortStatus", - name="12V Port Status", + name="12V port status", ), SwitchEntityDescription( key="usbPortStatus", - name="USB Port Status", + name="USB port status", ), SwitchEntityDescription( key="acPortStatus", - name="AC Port Status", + name="AC port status", ), ) diff --git a/tests/components/goalzero/test_sensor.py b/tests/components/goalzero/test_sensor.py index ce67351c978..e61015a4925 100644 --- a/tests/components/goalzero/test_sensor.py +++ b/tests/components/goalzero/test_sensor.py @@ -85,7 +85,7 @@ async def test_sensors( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert state.attributes.get(ATTR_STATE_CLASS) is None - state = hass.states.get(f"sensor.{DEFAULT_NAME}_wifi_strength") + state = hass.states.get(f"sensor.{DEFAULT_NAME}_wi_fi_strength") assert state.state == "-62" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.SIGNAL_STRENGTH assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS From 11f80762eabf9e0a3c21395e7ae9cd8d939016ea Mon Sep 17 00:00:00 2001 From: Nick Whyte Date: Mon, 18 Jul 2022 11:17:25 +0100 Subject: [PATCH 2657/3516] Upgrade ness_alarm dependencies (#75298) * Upgrade ness alarm dependencies to fix #74571 * Update requirements --- homeassistant/components/ness_alarm/__init__.py | 1 - homeassistant/components/ness_alarm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ness_alarm/__init__.py b/homeassistant/components/ness_alarm/__init__.py index d082f77d837..c81c3e0c7f7 100644 --- a/homeassistant/components/ness_alarm/__init__.py +++ b/homeassistant/components/ness_alarm/__init__.py @@ -99,7 +99,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: client = Client( host=host, port=port, - loop=hass.loop, update_interval=scan_interval.total_seconds(), infer_arming_state=infer_arming_state, ) diff --git a/homeassistant/components/ness_alarm/manifest.json b/homeassistant/components/ness_alarm/manifest.json index 4aa01428d27..0b20ad7e6a9 100644 --- a/homeassistant/components/ness_alarm/manifest.json +++ b/homeassistant/components/ness_alarm/manifest.json @@ -2,7 +2,7 @@ "domain": "ness_alarm", "name": "Ness Alarm", "documentation": "https://www.home-assistant.io/integrations/ness_alarm", - "requirements": ["nessclient==0.9.15"], + "requirements": ["nessclient==0.10.0"], "codeowners": ["@nickw444"], "iot_class": "local_push", "loggers": ["nessclient"] diff --git a/requirements_all.txt b/requirements_all.txt index ad2c15cf77b..359b1541de8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1068,7 +1068,7 @@ nad_receiver==0.3.0 ndms2_client==0.1.1 # homeassistant.components.ness_alarm -nessclient==0.9.15 +nessclient==0.10.0 # homeassistant.components.netdata netdata==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 784b740d459..450b4be10e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -745,7 +745,7 @@ mutesync==0.0.1 ndms2_client==0.1.1 # homeassistant.components.ness_alarm -nessclient==0.9.15 +nessclient==0.10.0 # homeassistant.components.discovery netdisco==3.0.0 From bfe34adf6ef1d5376d1b32dae2bdd70d61fba993 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 18 Jul 2022 15:09:43 +0200 Subject: [PATCH 2658/3516] Update Home Assistant base image to 2022.07.0 (#75396) --- build.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build.yaml b/build.yaml index 7bd4abcfc20..9cf66e2621a 100644 --- a/build.yaml +++ b/build.yaml @@ -1,11 +1,11 @@ image: homeassistant/{arch}-homeassistant shadow_repository: ghcr.io/home-assistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.06.2 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.06.2 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.06.2 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.06.2 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.06.2 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.07.0 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.07.0 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.07.0 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.07.0 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.07.0 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io From 9d0c91d6484c73b5390a0b9549847d0c11024583 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 18 Jul 2022 15:13:46 +0200 Subject: [PATCH 2659/3516] Migrate Sensor.Community to new entity naming style (#75014) --- homeassistant/components/luftdaten/sensor.py | 1 + tests/components/luftdaten/test_sensor.py | 38 ++++++++++---------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py index 055820de3a0..5333d86a708 100644 --- a/homeassistant/components/luftdaten/sensor.py +++ b/homeassistant/components/luftdaten/sensor.py @@ -97,6 +97,7 @@ class SensorCommunitySensor(CoordinatorEntity, SensorEntity): """Implementation of a Sensor.Community sensor.""" _attr_attribution = "Data provided by Sensor.Community" + _attr_has_entity_name = True _attr_should_poll = False def __init__( diff --git a/tests/components/luftdaten/test_sensor.py b/tests/components/luftdaten/test_sensor.py index 3cf4426d500..f3d0f1c0b1f 100644 --- a/tests/components/luftdaten/test_sensor.py +++ b/tests/components/luftdaten/test_sensor.py @@ -29,71 +29,73 @@ async def test_luftdaten_sensors( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - entry = entity_registry.async_get("sensor.temperature") + entry = entity_registry.async_get("sensor.sensor_12345_temperature") assert entry assert entry.device_id assert entry.unique_id == "12345_temperature" - state = hass.states.get("sensor.temperature") + state = hass.states.get("sensor.sensor_12345_temperature") assert state assert state.state == "22.3" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Temperature" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 Temperature" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS assert ATTR_ICON not in state.attributes - entry = entity_registry.async_get("sensor.humidity") + entry = entity_registry.async_get("sensor.sensor_12345_humidity") assert entry assert entry.device_id assert entry.unique_id == "12345_humidity" - state = hass.states.get("sensor.humidity") + state = hass.states.get("sensor.sensor_12345_humidity") assert state assert state.state == "34.7" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Humidity" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 Humidity" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE assert ATTR_ICON not in state.attributes - entry = entity_registry.async_get("sensor.pressure") + entry = entity_registry.async_get("sensor.sensor_12345_pressure") assert entry assert entry.device_id assert entry.unique_id == "12345_pressure" - state = hass.states.get("sensor.pressure") + state = hass.states.get("sensor.sensor_12345_pressure") assert state assert state.state == "98545.0" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pressure" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 Pressure" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PA assert ATTR_ICON not in state.attributes - entry = entity_registry.async_get("sensor.pressure_at_sealevel") + entry = entity_registry.async_get("sensor.sensor_12345_pressure_at_sealevel") assert entry assert entry.device_id assert entry.unique_id == "12345_pressure_at_sealevel" - state = hass.states.get("sensor.pressure_at_sealevel") + state = hass.states.get("sensor.sensor_12345_pressure_at_sealevel") assert state assert state.state == "103102.13" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Pressure at sealevel" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 Pressure at sealevel" + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PA assert ATTR_ICON not in state.attributes - entry = entity_registry.async_get("sensor.pm10") + entry = entity_registry.async_get("sensor.sensor_12345_pm10") assert entry assert entry.device_id assert entry.unique_id == "12345_P1" - state = hass.states.get("sensor.pm10") + state = hass.states.get("sensor.sensor_12345_pm10") assert state assert state.state == "8.5" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "PM10" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 PM10" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM10 assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( @@ -102,15 +104,15 @@ async def test_luftdaten_sensors( ) assert ATTR_ICON not in state.attributes - entry = entity_registry.async_get("sensor.pm2_5") + entry = entity_registry.async_get("sensor.sensor_12345_pm2_5") assert entry assert entry.device_id assert entry.unique_id == "12345_P2" - state = hass.states.get("sensor.pm2_5") + state = hass.states.get("sensor.sensor_12345_pm2_5") assert state assert state.state == "4.07" - assert state.attributes.get(ATTR_FRIENDLY_NAME) == "PM2.5" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 PM2.5" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PM25 assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( From 6f5e4ca50354b4e9f1a02fbca70217ad14c3546d Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 18 Jul 2022 10:20:49 -0400 Subject: [PATCH 2660/3516] Fix ZHA light turn on issues (#75220) * rename variable * default transition is for color commands not level * no extra command for groups * don't transition color change when light off -> on * clean up * update condition * fix condition again... * simplify * simplify * missed one * rename * simplify * rename * tests * color_provided_while_off with no changes * fix missing flag clear * more tests for transition scenarios * add to comment * fix comment * don't transition when force on is set * stale comment * dont transition when colors don't change * remove extra line * remove debug print :) * fix colors * restore color to 65535 until investigated --- homeassistant/components/zha/light.py | 98 ++-- tests/components/zha/test_light.py | 811 +++++++++++++++++++++++++- 2 files changed, 868 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index e1c85b39d8e..cc8dc475c43 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -73,7 +73,6 @@ CAPABILITIES_COLOR_LOOP = 0x4 CAPABILITIES_COLOR_XY = 0x08 CAPABILITIES_COLOR_TEMP = 0x10 -DEFAULT_TRANSITION = 1 DEFAULT_MIN_BRIGHTNESS = 2 UPDATE_COLORLOOP_ACTION = 0x1 @@ -119,7 +118,7 @@ class BaseLight(LogMixin, light.LightEntity): """Operations common to all light entities.""" _FORCE_ON = False - _DEFAULT_COLOR_FROM_OFF_TRANSITION = 0 + _DEFAULT_MIN_TRANSITION_TIME = 0 def __init__(self, *args, **kwargs): """Initialize the light.""" @@ -140,7 +139,7 @@ class BaseLight(LogMixin, light.LightEntity): self._level_channel = None self._color_channel = None self._identify_channel = None - self._default_transition = None + self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME self._attr_color_mode = ColorMode.UNKNOWN # Set by sub classes @property @@ -216,33 +215,49 @@ class BaseLight(LogMixin, light.LightEntity): transition = kwargs.get(light.ATTR_TRANSITION) duration = ( transition * 10 - if transition - else self._default_transition * 10 - if self._default_transition - else DEFAULT_TRANSITION - ) + if transition is not None + else self._zha_config_transition * 10 + ) or self._DEFAULT_MIN_TRANSITION_TIME # if 0 is passed in some devices still need the minimum default brightness = kwargs.get(light.ATTR_BRIGHTNESS) effect = kwargs.get(light.ATTR_EFFECT) flash = kwargs.get(light.ATTR_FLASH) + temperature = kwargs.get(light.ATTR_COLOR_TEMP) + hs_color = kwargs.get(light.ATTR_HS_COLOR) # If the light is currently off but a turn_on call with a color/temperature is sent, # the light needs to be turned on first at a low brightness level where the light is immediately transitioned # to the correct color. Afterwards, the transition is only from the low brightness to the new brightness. # Otherwise, the transition is from the color the light had before being turned on to the new color. - # This can look especially bad with transitions longer than a second. - color_provided_from_off = ( - not self._state + # This can look especially bad with transitions longer than a second. We do not want to do this for + # devices that need to be forced to use the on command because we would end up with 4 commands sent: + # move to level, on, color, move to level... We also will not set this if the bulb is already in the + # desired color mode with the desired color or color temperature. + new_color_provided_while_off = ( + not isinstance(self, LightGroup) + and not self._FORCE_ON + and not self._state + and ( + ( + temperature is not None + and ( + self._color_temp != temperature + or self._attr_color_mode != ColorMode.COLOR_TEMP + ) + ) + or ( + hs_color is not None + and ( + self.hs_color != hs_color + or self._attr_color_mode != ColorMode.HS + ) + ) + ) and brightness_supported(self._attr_supported_color_modes) - and (light.ATTR_COLOR_TEMP in kwargs or light.ATTR_HS_COLOR in kwargs) ) - final_duration = duration - if color_provided_from_off: - # Set the duration for the color changing commands to 0. - duration = 0 if ( brightness is None - and (self._off_with_transition or color_provided_from_off) + and (self._off_with_transition or new_color_provided_while_off) and self._off_brightness is not None ): brightness = self._off_brightness @@ -254,11 +269,11 @@ class BaseLight(LogMixin, light.LightEntity): t_log = {} - if color_provided_from_off: + if new_color_provided_while_off: # If the light is currently off, we first need to turn it on at a low brightness level with no transition. # After that, we set it to the desired color/temperature with no transition. result = await self._level_channel.move_to_level_with_on_off( - DEFAULT_MIN_BRIGHTNESS, self._DEFAULT_COLOR_FROM_OFF_TRANSITION + DEFAULT_MIN_BRIGHTNESS, self._DEFAULT_MIN_TRANSITION_TIME ) t_log["move_to_level_with_on_off"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: @@ -269,7 +284,7 @@ class BaseLight(LogMixin, light.LightEntity): if ( (brightness is not None or transition) - and not color_provided_from_off + and not new_color_provided_while_off and brightness_supported(self._attr_supported_color_modes) ): result = await self._level_channel.move_to_level_with_on_off( @@ -285,7 +300,7 @@ class BaseLight(LogMixin, light.LightEntity): if ( brightness is None - and not color_provided_from_off + and not new_color_provided_while_off or (self._FORCE_ON and brightness) ): # since some lights don't always turn on with move_to_level_with_on_off, @@ -297,9 +312,13 @@ class BaseLight(LogMixin, light.LightEntity): return self._state = True - if light.ATTR_COLOR_TEMP in kwargs: - temperature = kwargs[light.ATTR_COLOR_TEMP] - result = await self._color_channel.move_to_color_temp(temperature, duration) + if temperature is not None: + result = await self._color_channel.move_to_color_temp( + temperature, + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, + ) t_log["move_to_color_temp"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) @@ -308,11 +327,14 @@ class BaseLight(LogMixin, light.LightEntity): self._color_temp = temperature self._hs_color = None - if light.ATTR_HS_COLOR in kwargs: - hs_color = kwargs[light.ATTR_HS_COLOR] + if hs_color is not None: xy_color = color_util.color_hs_to_xy(*hs_color) result = await self._color_channel.move_to_color( - int(xy_color[0] * 65535), int(xy_color[1] * 65535), duration + int(xy_color[0] * 65535), + int(xy_color[1] * 65535), + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, ) t_log["move_to_color"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: @@ -322,9 +344,9 @@ class BaseLight(LogMixin, light.LightEntity): self._hs_color = hs_color self._color_temp = None - if color_provided_from_off: + if new_color_provided_while_off: # The light is has the correct color, so we can now transition it to the correct brightness level. - result = await self._level_channel.move_to_level(level, final_duration) + result = await self._level_channel.move_to_level(level, duration) t_log["move_to_level_if_color"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) @@ -371,12 +393,13 @@ class BaseLight(LogMixin, light.LightEntity): async def async_turn_off(self, **kwargs): """Turn the entity off.""" - duration = kwargs.get(light.ATTR_TRANSITION) + transition = kwargs.get(light.ATTR_TRANSITION) supports_level = brightness_supported(self._attr_supported_color_modes) - if duration and supports_level: + # is not none looks odd here but it will override built in bulb transition times if we pass 0 in here + if transition is not None and supports_level: result = await self._level_channel.move_to_level_with_on_off( - 0, duration * 10 + 0, transition * 10 ) else: result = await self._on_off_channel.off() @@ -387,7 +410,7 @@ class BaseLight(LogMixin, light.LightEntity): if supports_level: # store current brightness so that the next turn_on uses it. - self._off_with_transition = bool(duration) + self._off_with_transition = transition is not None self._off_brightness = self._brightness self.async_write_ha_state() @@ -460,7 +483,7 @@ class Light(BaseLight, ZhaEntity): if effect_list: self._effect_list = effect_list - self._default_transition = async_get_zha_config_value( + self._zha_config_transition = async_get_zha_config_value( zha_device.gateway.config_entry, ZHA_OPTIONS, CONF_DEFAULT_LIGHT_TRANSITION, @@ -472,6 +495,7 @@ class Light(BaseLight, ZhaEntity): """Set the state.""" self._state = bool(value) if value: + self._off_with_transition = False self._off_brightness = None self.async_write_ha_state() @@ -605,7 +629,7 @@ class HueLight(Light): @STRICT_MATCH( channel_names=CHANNEL_ON_OFF, aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}, - manufacturers={"Jasco", "Quotra-Vision"}, + manufacturers={"Jasco", "Quotra-Vision", "eWeLight", "eWeLink"}, ) class ForceOnLight(Light): """Representation of a light which does not respect move_to_level_with_on_off.""" @@ -621,7 +645,7 @@ class ForceOnLight(Light): class SengledLight(Light): """Representation of a Sengled light which does not react to move_to_color_temp with 0 as a transition.""" - _DEFAULT_COLOR_FROM_OFF_TRANSITION = 1 + _DEFAULT_MIN_TRANSITION_TIME = 1 @GROUP_MATCH() @@ -639,7 +663,7 @@ class LightGroup(BaseLight, ZhaGroupEntity): self._color_channel = group.endpoint[Color.cluster_id] self._identify_channel = group.endpoint[Identify.cluster_id] self._debounced_member_refresh = None - self._default_transition = async_get_zha_config_value( + self._zha_config_transition = async_get_zha_config_value( zha_device.gateway.config_entry, ZHA_OPTIONS, CONF_DEFAULT_LIGHT_TRANSITION, diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index dd6df0dff19..982ff622341 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -15,7 +15,11 @@ from homeassistant.components.light import ( ColorMode, ) from homeassistant.components.zha.core.group import GroupMember -from homeassistant.components.zha.light import FLASH_EFFECTS +from homeassistant.components.zha.light import ( + CAPABILITIES_COLOR_TEMP, + CAPABILITIES_COLOR_XY, + FLASH_EFFECTS, +) from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform import homeassistant.util.dt as dt_util @@ -142,6 +146,10 @@ async def device_light_1(hass, zigpy_device_mock, zha_device_joined): ieee=IEEE_GROUPABLE_DEVICE, nwk=0xB79D, ) + color_cluster = zigpy_device.endpoints[1].light_color + color_cluster.PLUGGED_ATTR_READS = { + "color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY + } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True return zha_device @@ -167,8 +175,13 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined): } }, ieee=IEEE_GROUPABLE_DEVICE2, + manufacturer="Sengled", nwk=0xC79E, ) + color_cluster = zigpy_device.endpoints[1].light_color + color_cluster.PLUGGED_ATTR_READS = { + "color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY + } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True return zha_device @@ -201,6 +214,38 @@ async def device_light_3(hass, zigpy_device_mock, zha_device_joined): return zha_device +@pytest.fixture +async def eWeLink_light(hass, zigpy_device_mock, zha_device_joined): + """Mock eWeLink light.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [ + general.OnOff.cluster_id, + general.LevelControl.cluster_id, + lighting.Color.cluster_id, + general.Groups.cluster_id, + general.Identify.cluster_id, + ], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + }, + ieee="03:2d:6f:00:0a:90:69:e3", + manufacturer="eWeLink", + nwk=0xB79D, + ) + color_cluster = zigpy_device.endpoints[1].light_color + color_cluster.PLUGGED_ATTR_READS = { + "color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY + } + zha_device = await zha_device_joined(zigpy_device) + zha_device.available = True + return zha_device + + async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored): """Test zha light platform refresh.""" @@ -323,6 +368,758 @@ async def test_light( await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_LONG) +@patch( + "zigpy.zcl.clusters.lighting.Color.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@patch( + "zigpy.zcl.clusters.general.Identify.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@patch( + "zigpy.zcl.clusters.general.LevelControl.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@patch( + "zigpy.zcl.clusters.general.OnOff.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +async def test_transitions( + hass, device_light_1, device_light_2, eWeLink_light, coordinator +): + """Test ZHA light transition code.""" + zha_gateway = get_zha_gateway(hass) + assert zha_gateway is not None + zha_gateway.coordinator_zha_device = coordinator + coordinator._zha_gateway = zha_gateway + device_light_1._zha_gateway = zha_gateway + device_light_2._zha_gateway = zha_gateway + member_ieee_addresses = [device_light_1.ieee, device_light_2.ieee] + members = [GroupMember(device_light_1.ieee, 1), GroupMember(device_light_2.ieee, 1)] + + assert coordinator.is_coordinator + + # test creating a group with 2 members + zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members) + await hass.async_block_till_done() + + assert zha_group is not None + assert len(zha_group.members) == 2 + for member in zha_group.members: + assert member.device.ieee in member_ieee_addresses + assert member.group == zha_group + assert member.endpoint is not None + + device_1_entity_id = await find_entity_id(Platform.LIGHT, device_light_1, hass) + device_2_entity_id = await find_entity_id(Platform.LIGHT, device_light_2, hass) + eWeLink_light_entity_id = await find_entity_id(Platform.LIGHT, eWeLink_light, hass) + assert device_1_entity_id != device_2_entity_id + + group_entity_id = async_find_group_entity_id(hass, Platform.LIGHT, zha_group) + assert hass.states.get(group_entity_id) is not None + + assert device_1_entity_id in zha_group.member_entity_ids + assert device_2_entity_id in zha_group.member_entity_ids + + dev1_cluster_on_off = device_light_1.device.endpoints[1].on_off + dev2_cluster_on_off = device_light_2.device.endpoints[1].on_off + eWeLink_cluster_on_off = eWeLink_light.device.endpoints[1].on_off + + dev1_cluster_level = device_light_1.device.endpoints[1].level + dev2_cluster_level = device_light_2.device.endpoints[1].level + eWeLink_cluster_level = eWeLink_light.device.endpoints[1].level + + dev1_cluster_color = device_light_1.device.endpoints[1].light_color + dev2_cluster_color = device_light_2.device.endpoints[1].light_color + eWeLink_cluster_color = eWeLink_light.device.endpoints[1].light_color + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, [device_light_1, device_light_2]) + await async_wait_for_updates(hass) + + # test that the lights were created and are off + group_state = hass.states.get(group_entity_id) + assert group_state.state == STATE_OFF + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_OFF + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_OFF + + # first test 0 length transition with no color provided + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_level.request.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {"entity_id": device_1_entity_id, "transition": 0, "brightness": 50}, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 0 + assert dev1_cluster_on_off.request.await_count == 0 + assert dev1_cluster_color.request.call_count == 0 + assert dev1_cluster_color.request.await_count == 0 + assert dev1_cluster_level.request.call_count == 1 + assert dev1_cluster_level.request.await_count == 1 + assert dev1_cluster_level.request.call_args == call( + False, + 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 50, # brightness (level in ZCL) + 0, # transition time + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_ON + assert light1_state.attributes["brightness"] == 50 + + dev1_cluster_level.request.reset_mock() + + # test non 0 length transition with color provided while light is on + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_1_entity_id, + "transition": 3, + "brightness": 18, + "color_temp": 432, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 0 + assert dev1_cluster_on_off.request.await_count == 0 + assert dev1_cluster_color.request.call_count == 1 + assert dev1_cluster_color.request.await_count == 1 + assert dev1_cluster_level.request.call_count == 1 + assert dev1_cluster_level.request.await_count == 1 + assert dev1_cluster_level.request.call_args == call( + False, + 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 18, # brightness (level in ZCL) + 30, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_color.request.call_args == call( + False, + 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, + 432, # color temp mireds + 30.0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_ON + assert light1_state.attributes["brightness"] == 18 + assert light1_state.attributes["color_temp"] == 432 + assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + dev1_cluster_level.request.reset_mock() + dev1_cluster_color.request.reset_mock() + + # test 0 length transition to turn light off + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + "entity_id": device_1_entity_id, + "transition": 0, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 0 + assert dev1_cluster_on_off.request.await_count == 0 + assert dev1_cluster_color.request.call_count == 0 + assert dev1_cluster_color.request.await_count == 0 + assert dev1_cluster_level.request.call_count == 1 + assert dev1_cluster_level.request.await_count == 1 + assert dev1_cluster_level.request.call_args == call( + False, + 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 0, # brightness (level in ZCL) + 0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_OFF + + dev1_cluster_level.request.reset_mock() + + # test non 0 length transition and color temp while turning light on (color_provided_while_off) + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_1_entity_id, + "transition": 1, + "brightness": 25, + "color_temp": 235, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 0 + assert dev1_cluster_on_off.request.await_count == 0 + assert dev1_cluster_color.request.call_count == 1 + assert dev1_cluster_color.request.await_count == 1 + assert dev1_cluster_level.request.call_count == 2 + assert dev1_cluster_level.request.await_count == 2 + + # first it comes on with no transition at 2 brightness + assert dev1_cluster_level.request.call_args_list[0] == call( + False, + 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 2, # brightness (level in ZCL) + 0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_color.request.call_args == call( + False, + 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, + 235, # color temp mireds + 0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_level.request.call_args_list[1] == call( + False, + 0, + dev1_cluster_level.commands_by_name["move_to_level"].schema, + 25, # brightness (level in ZCL) + 10.0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_ON + assert light1_state.attributes["brightness"] == 25 + assert light1_state.attributes["color_temp"] == 235 + assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + dev1_cluster_level.request.reset_mock() + dev1_cluster_color.request.reset_mock() + + # turn light 1 back off + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + "entity_id": device_1_entity_id, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 1 + assert dev1_cluster_on_off.request.await_count == 1 + assert dev1_cluster_color.request.call_count == 0 + assert dev1_cluster_color.request.await_count == 0 + assert dev1_cluster_level.request.call_count == 0 + assert dev1_cluster_level.request.await_count == 0 + group_state = hass.states.get(group_entity_id) + assert group_state.state == STATE_OFF + + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_color.request.reset_mock() + dev1_cluster_level.request.reset_mock() + + # test no transition provided and color temp while turning light on (color_provided_while_off) + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_1_entity_id, + "brightness": 25, + "color_temp": 236, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 0 + assert dev1_cluster_on_off.request.await_count == 0 + assert dev1_cluster_color.request.call_count == 1 + assert dev1_cluster_color.request.await_count == 1 + assert dev1_cluster_level.request.call_count == 2 + assert dev1_cluster_level.request.await_count == 2 + + # first it comes on with no transition at 2 brightness + assert dev1_cluster_level.request.call_args_list[0] == call( + False, + 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 2, # brightness (level in ZCL) + 0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_color.request.call_args == call( + False, + 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, + 236, # color temp mireds + 0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_level.request.call_args_list[1] == call( + False, + 0, + dev1_cluster_level.commands_by_name["move_to_level"].schema, + 25, # brightness (level in ZCL) + 0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_ON + assert light1_state.attributes["brightness"] == 25 + assert light1_state.attributes["color_temp"] == 236 + assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + dev1_cluster_level.request.reset_mock() + dev1_cluster_color.request.reset_mock() + + # turn light 1 back off to setup group test + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + "entity_id": device_1_entity_id, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 1 + assert dev1_cluster_on_off.request.await_count == 1 + assert dev1_cluster_color.request.call_count == 0 + assert dev1_cluster_color.request.await_count == 0 + assert dev1_cluster_level.request.call_count == 0 + assert dev1_cluster_level.request.await_count == 0 + group_state = hass.states.get(group_entity_id) + assert group_state.state == STATE_OFF + + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_color.request.reset_mock() + dev1_cluster_level.request.reset_mock() + + # test no transition when the same color temp is provided from off + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_1_entity_id, + "color_temp": 236, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 1 + assert dev1_cluster_on_off.request.await_count == 1 + assert dev1_cluster_color.request.call_count == 1 + assert dev1_cluster_color.request.await_count == 1 + assert dev1_cluster_level.request.call_count == 0 + assert dev1_cluster_level.request.await_count == 0 + + assert dev1_cluster_on_off.request.call_args == call( + False, + 1, + dev1_cluster_on_off.commands_by_name["on"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + assert dev1_cluster_color.request.call_args == call( + False, + 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, + 236, # color temp mireds + 0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_ON + assert light1_state.attributes["brightness"] == 25 + assert light1_state.attributes["color_temp"] == 236 + assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_color.request.reset_mock() + + # turn light 1 back off to setup group test + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + "entity_id": device_1_entity_id, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 1 + assert dev1_cluster_on_off.request.await_count == 1 + assert dev1_cluster_color.request.call_count == 0 + assert dev1_cluster_color.request.await_count == 0 + assert dev1_cluster_level.request.call_count == 0 + assert dev1_cluster_level.request.await_count == 0 + group_state = hass.states.get(group_entity_id) + assert group_state.state == STATE_OFF + + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_color.request.reset_mock() + dev1_cluster_level.request.reset_mock() + + # test sengled light uses default minimum transition time + dev2_cluster_on_off.request.reset_mock() + dev2_cluster_color.request.reset_mock() + dev2_cluster_level.request.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {"entity_id": device_2_entity_id, "transition": 0, "brightness": 100}, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 0 + assert dev2_cluster_on_off.request.await_count == 0 + assert dev2_cluster_color.request.call_count == 0 + assert dev2_cluster_color.request.await_count == 0 + assert dev2_cluster_level.request.call_count == 1 + assert dev2_cluster_level.request.await_count == 1 + assert dev2_cluster_level.request.call_args == call( + False, + 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 100, # brightness (level in ZCL) + 1, # transition time - sengled light uses default minimum + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_ON + assert light2_state.attributes["brightness"] == 100 + + dev2_cluster_level.request.reset_mock() + + # turn the sengled light back off + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + "entity_id": device_2_entity_id, + }, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 1 + assert dev2_cluster_on_off.request.await_count == 1 + assert dev2_cluster_color.request.call_count == 0 + assert dev2_cluster_color.request.await_count == 0 + assert dev2_cluster_level.request.call_count == 0 + assert dev2_cluster_level.request.await_count == 0 + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_OFF + + dev2_cluster_on_off.request.reset_mock() + + # test non 0 length transition and color temp while turning light on and sengled (color_provided_while_off) + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_2_entity_id, + "transition": 1, + "brightness": 25, + "color_temp": 235, + }, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 0 + assert dev2_cluster_on_off.request.await_count == 0 + assert dev2_cluster_color.request.call_count == 1 + assert dev2_cluster_color.request.await_count == 1 + assert dev2_cluster_level.request.call_count == 2 + assert dev2_cluster_level.request.await_count == 2 + + # first it comes on with no transition at 2 brightness + assert dev2_cluster_level.request.call_args_list[0] == call( + False, + 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 2, # brightness (level in ZCL) + 1, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev2_cluster_color.request.call_args == call( + False, + 10, + dev2_cluster_color.commands_by_name["move_to_color_temp"].schema, + 235, # color temp mireds + 1, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when color_provided_while_off + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev2_cluster_level.request.call_args_list[1] == call( + False, + 0, + dev2_cluster_level.commands_by_name["move_to_level"].schema, + 25, # brightness (level in ZCL) + 10.0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_ON + assert light2_state.attributes["brightness"] == 25 + assert light2_state.attributes["color_temp"] == 235 + assert light2_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + dev2_cluster_level.request.reset_mock() + dev2_cluster_color.request.reset_mock() + + # turn the sengled light back off + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + "entity_id": device_2_entity_id, + }, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 1 + assert dev2_cluster_on_off.request.await_count == 1 + assert dev2_cluster_color.request.call_count == 0 + assert dev2_cluster_color.request.await_count == 0 + assert dev2_cluster_level.request.call_count == 0 + assert dev2_cluster_level.request.await_count == 0 + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_OFF + + dev2_cluster_on_off.request.reset_mock() + + # test non 0 length transition and color temp while turning group light on (color_provided_while_off) + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": group_entity_id, + "transition": 1, + "brightness": 25, + "color_temp": 235, + }, + blocking=True, + ) + + group_on_off_channel = zha_group.endpoint[general.OnOff.cluster_id] + group_level_channel = zha_group.endpoint[general.LevelControl.cluster_id] + group_color_channel = zha_group.endpoint[lighting.Color.cluster_id] + assert group_on_off_channel.request.call_count == 0 + assert group_on_off_channel.request.await_count == 0 + assert group_color_channel.request.call_count == 1 + assert group_color_channel.request.await_count == 1 + assert group_level_channel.request.call_count == 1 + assert group_level_channel.request.await_count == 1 + + # groups are omitted from the 3 call dance for color_provided_while_off + assert group_color_channel.request.call_args == call( + False, + 10, + dev2_cluster_color.commands_by_name["move_to_color_temp"].schema, + 235, # color temp mireds + 10.0, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when color_provided_while_off + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert group_level_channel.request.call_args == call( + False, + 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 25, # brightness (level in ZCL) + 10.0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + group_state = hass.states.get(group_entity_id) + assert group_state.state == STATE_ON + assert group_state.attributes["brightness"] == 25 + assert group_state.attributes["color_temp"] == 235 + assert group_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + group_on_off_channel.request.reset_mock() + group_color_channel.request.reset_mock() + group_level_channel.request.reset_mock() + + # turn the sengled light back on + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_2_entity_id, + }, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 1 + assert dev2_cluster_on_off.request.await_count == 1 + assert dev2_cluster_color.request.call_count == 0 + assert dev2_cluster_color.request.await_count == 0 + assert dev2_cluster_level.request.call_count == 0 + assert dev2_cluster_level.request.await_count == 0 + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_ON + + dev2_cluster_on_off.request.reset_mock() + + # turn the light off with a transition + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + {"entity_id": device_2_entity_id, "transition": 2}, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 0 + assert dev2_cluster_on_off.request.await_count == 0 + assert dev2_cluster_color.request.call_count == 0 + assert dev2_cluster_color.request.await_count == 0 + assert dev2_cluster_level.request.call_count == 1 + assert dev2_cluster_level.request.await_count == 1 + assert dev2_cluster_level.request.call_args == call( + False, + 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 0, # brightness (level in ZCL) + 20, # transition time + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_OFF + + dev2_cluster_level.request.reset_mock() + + # turn the light back on with no args should use a transition and last known brightness + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {"entity_id": device_2_entity_id}, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 0 + assert dev2_cluster_on_off.request.await_count == 0 + assert dev2_cluster_color.request.call_count == 0 + assert dev2_cluster_color.request.await_count == 0 + assert dev2_cluster_level.request.call_count == 1 + assert dev2_cluster_level.request.await_count == 1 + assert dev2_cluster_level.request.call_args == call( + False, + 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 25, # brightness (level in ZCL) - this is the last brightness we set a few tests above + 1, # transition time - sengled light uses default minimum + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_ON + + dev2_cluster_level.request.reset_mock() + + # test eWeLink color temp while turning light on from off (color_provided_while_off) + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": eWeLink_light_entity_id, + "color_temp": 235, + }, + blocking=True, + ) + assert eWeLink_cluster_on_off.request.call_count == 1 + assert eWeLink_cluster_on_off.request.await_count == 1 + assert eWeLink_cluster_color.request.call_count == 1 + assert eWeLink_cluster_color.request.await_count == 1 + assert eWeLink_cluster_level.request.call_count == 0 + assert eWeLink_cluster_level.request.await_count == 0 + + # first it comes on + assert eWeLink_cluster_on_off.request.call_args_list[0] == call( + False, + 1, + eWeLink_cluster_on_off.commands_by_name["on"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_color.request.call_args == call( + False, + 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, + 235, # color temp mireds + 0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + eWeLink_state = hass.states.get(eWeLink_light_entity_id) + assert eWeLink_state.state == STATE_ON + assert eWeLink_state.attributes["color_temp"] == 235 + assert eWeLink_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + async def async_test_on_off_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light @@ -463,7 +1260,7 @@ async def async_test_level_on_off_from_hass( 4, level_cluster.commands_by_name["move_to_level_with_on_off"].schema, 10, - 1, + 0, expect_reply=True, manufacturer=None, tries=1, @@ -601,7 +1398,10 @@ async def test_zha_group_light_entity( # test that the lights were created and are off group_state = hass.states.get(group_entity_id) assert group_state.state == STATE_OFF - assert group_state.attributes["supported_color_modes"] == [ColorMode.HS] + assert group_state.attributes["supported_color_modes"] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] # Light which is off has no color mode assert "color_mode" not in group_state.attributes @@ -629,7 +1429,10 @@ async def test_zha_group_light_entity( # Check state group_state = hass.states.get(group_entity_id) assert group_state.state == STATE_ON - assert group_state.attributes["supported_color_modes"] == [ColorMode.HS] + assert group_state.attributes["supported_color_modes"] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] assert group_state.attributes["color_mode"] == ColorMode.HS # test long flashing the lights from the HA From b3ef6f4d047358048ecf12a4b4e07379cc90bcba Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 18 Jul 2022 09:18:07 -0600 Subject: [PATCH 2661/3516] Simplify Guardian entity inheritance hierarchy (#75274) --- homeassistant/components/guardian/__init__.py | 107 ++++++--------- .../components/guardian/binary_sensor.py | 127 +++++++++--------- homeassistant/components/guardian/button.py | 49 ++++--- homeassistant/components/guardian/sensor.py | 112 +++++++-------- homeassistant/components/guardian/switch.py | 78 ++++++----- 5 files changed, 225 insertions(+), 248 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index f13ca1a7ff5..d15b7d57aea 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable +from dataclasses import dataclass from typing import cast from aioguardian import Client @@ -24,10 +25,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo, EntityDescription -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( API_SENSOR_PAIR_DUMP, @@ -132,7 +130,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # so we use a lock to ensure that only one API request is reaching it at a time: api_lock = asyncio.Lock() - # Set up DataUpdateCoordinators for the valve controller: + # Set up GuardianDataUpdateCoordinators for the valve controller: coordinators: dict[str, GuardianDataUpdateCoordinator] = {} init_valve_controller_tasks = [] for api, api_coro in ( @@ -418,17 +416,18 @@ class PairedSensorManager: dev_reg.async_remove_device(device.id) -class GuardianEntity(CoordinatorEntity): +class GuardianEntity(CoordinatorEntity[GuardianDataUpdateCoordinator]): """Define a base Guardian entity.""" _attr_has_entity_name = True - def __init__( # pylint: disable=super-init-not-called - self, entry: ConfigEntry, description: EntityDescription + def __init__( + self, coordinator: GuardianDataUpdateCoordinator, description: EntityDescription ) -> None: """Initialize.""" + super().__init__(coordinator) + self._attr_extra_state_attributes = {} - self._entry = entry self.entity_description = description @callback @@ -438,6 +437,17 @@ class GuardianEntity(CoordinatorEntity): This should be extended by Guardian platforms. """ + @callback + def _handle_coordinator_update(self) -> None: + """Respond to a DataUpdateCoordinator update.""" + self._async_update_from_latest_data() + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + self._async_update_from_latest_data() + class PairedSensorEntity(GuardianEntity): """Define a Guardian paired sensor entity.""" @@ -445,11 +455,11 @@ class PairedSensorEntity(GuardianEntity): def __init__( self, entry: ConfigEntry, - coordinator: DataUpdateCoordinator, + coordinator: GuardianDataUpdateCoordinator, description: EntityDescription, ) -> None: """Initialize.""" - super().__init__(entry, description) + super().__init__(coordinator, description) paired_sensor_uid = coordinator.data["uid"] self._attr_device_info = DeviceInfo( @@ -460,11 +470,20 @@ class PairedSensorEntity(GuardianEntity): via_device=(DOMAIN, entry.data[CONF_UID]), ) self._attr_unique_id = f"{paired_sensor_uid}_{description.key}" - self.coordinator = coordinator - async def async_added_to_hass(self) -> None: - """Perform tasks when the entity is added.""" - self._async_update_from_latest_data() + +@dataclass +class ValveControllerEntityDescriptionMixin: + """Define an entity description mixin for valve controller entities.""" + + api_category: str + + +@dataclass +class ValveControllerEntityDescription( + EntityDescription, ValveControllerEntityDescriptionMixin +): + """Describe a Guardian valve controller entity.""" class ValveControllerEntity(GuardianEntity): @@ -473,64 +492,18 @@ class ValveControllerEntity(GuardianEntity): def __init__( self, entry: ConfigEntry, - coordinators: dict[str, DataUpdateCoordinator], - description: EntityDescription, + coordinators: dict[str, GuardianDataUpdateCoordinator], + description: ValveControllerEntityDescription, ) -> None: """Initialize.""" - super().__init__(entry, description) + super().__init__(coordinators[description.api_category], description) + + self._diagnostics_coordinator = coordinators[API_SYSTEM_DIAGNOSTICS] self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, entry.data[CONF_UID])}, manufacturer="Elexa", - model=coordinators[API_SYSTEM_DIAGNOSTICS].data["firmware"], + model=self._diagnostics_coordinator.data["firmware"], name=f"Guardian valve controller {entry.data[CONF_UID]}", ) self._attr_unique_id = f"{entry.data[CONF_UID]}_{description.key}" - self.coordinators = coordinators - - @property - def available(self) -> bool: - """Return if entity is available.""" - return any( - coordinator.last_update_success - for coordinator in self.coordinators.values() - ) - - async def _async_continue_entity_setup(self) -> None: - """Perform additional, internal tasks when the entity is about to be added. - - This should be extended by Guardian platforms. - """ - - @callback - def async_add_coordinator_update_listener(self, api: str) -> None: - """Add a listener to a DataUpdateCoordinator based on the API referenced.""" - - @callback - def update() -> None: - """Update the entity's state.""" - self._async_update_from_latest_data() - self.async_write_ha_state() - - self.async_on_remove(self.coordinators[api].async_add_listener(update)) - - async def async_added_to_hass(self) -> None: - """Perform tasks when the entity is added.""" - await self._async_continue_entity_setup() - self.async_add_coordinator_update_listener(API_SYSTEM_DIAGNOSTICS) - self._async_update_from_latest_data() - - async def async_update(self) -> None: - """Update the entity. - - Only used by the generic entity update service. - """ - # Ignore manual update requests if the entity is disabled - if not self.enabled: - return - - refresh_tasks = [ - coordinator.async_request_refresh() - for coordinator in self.coordinators.values() - ] - await asyncio.gather(*refresh_tasks) diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index dc9febcfa91..9b824ab589f 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -1,6 +1,8 @@ """Binary sensors for the Elexa Guardian integration.""" from __future__ import annotations +from dataclasses import dataclass + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, @@ -11,9 +13,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import PairedSensorEntity, ValveControllerEntity +from . import ( + PairedSensorEntity, + ValveControllerEntity, + ValveControllerEntityDescription, +) from .const import ( API_SYSTEM_ONBOARD_SENSOR_STATUS, API_WIFI_STATUS, @@ -23,6 +28,7 @@ from .const import ( DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, ) +from .util import GuardianDataUpdateCoordinator ATTR_CONNECTED_CLIENTS = "connected_clients" @@ -30,31 +36,42 @@ SENSOR_KIND_AP_INFO = "ap_enabled" SENSOR_KIND_LEAK_DETECTED = "leak_detected" SENSOR_KIND_MOVED = "moved" -SENSOR_DESCRIPTION_AP_ENABLED = BinarySensorEntityDescription( - key=SENSOR_KIND_AP_INFO, - name="Onboard AP enabled", - device_class=BinarySensorDeviceClass.CONNECTIVITY, - entity_category=EntityCategory.DIAGNOSTIC, -) -SENSOR_DESCRIPTION_LEAK_DETECTED = BinarySensorEntityDescription( - key=SENSOR_KIND_LEAK_DETECTED, - name="Leak detected", - device_class=BinarySensorDeviceClass.MOISTURE, -) -SENSOR_DESCRIPTION_MOVED = BinarySensorEntityDescription( - key=SENSOR_KIND_MOVED, - name="Recently moved", - device_class=BinarySensorDeviceClass.MOVING, - entity_category=EntityCategory.DIAGNOSTIC, -) + +@dataclass +class ValveControllerBinarySensorDescription( + BinarySensorEntityDescription, ValveControllerEntityDescription +): + """Describe a Guardian valve controller binary sensor.""" + PAIRED_SENSOR_DESCRIPTIONS = ( - SENSOR_DESCRIPTION_LEAK_DETECTED, - SENSOR_DESCRIPTION_MOVED, + BinarySensorEntityDescription( + key=SENSOR_KIND_LEAK_DETECTED, + name="Leak detected", + device_class=BinarySensorDeviceClass.MOISTURE, + ), + BinarySensorEntityDescription( + key=SENSOR_KIND_MOVED, + name="Recently moved", + device_class=BinarySensorDeviceClass.MOVING, + entity_category=EntityCategory.DIAGNOSTIC, + ), ) + VALVE_CONTROLLER_DESCRIPTIONS = ( - SENSOR_DESCRIPTION_AP_ENABLED, - SENSOR_DESCRIPTION_LEAK_DETECTED, + ValveControllerBinarySensorDescription( + key=SENSOR_KIND_LEAK_DETECTED, + name="Leak detected", + device_class=BinarySensorDeviceClass.MOISTURE, + api_category=API_SYSTEM_ONBOARD_SENSOR_STATUS, + ), + ValveControllerBinarySensorDescription( + key=SENSOR_KIND_AP_INFO, + name="Onboard AP enabled", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + api_category=API_WIFI_STATUS, + ), ) @@ -62,19 +79,18 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" + entry_data = hass.data[DOMAIN][entry.entry_id] + paired_sensor_coordinators = entry_data[DATA_COORDINATOR_PAIRED_SENSOR] + valve_controller_coordinators = entry_data[DATA_COORDINATOR] @callback def add_new_paired_sensor(uid: str) -> None: """Add a new paired sensor.""" - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR_PAIRED_SENSOR][ - uid - ] - async_add_entities( - [ - PairedSensorBinarySensor(entry, coordinator, description) - for description in PAIRED_SENSOR_DESCRIPTIONS - ] + PairedSensorBinarySensor( + entry, paired_sensor_coordinators[uid], description + ) + for description in PAIRED_SENSOR_DESCRIPTIONS ) # Handle adding paired sensors after HASS startup: @@ -88,9 +104,7 @@ async def async_setup_entry( # Add all valve controller-specific binary sensors: sensors: list[PairedSensorBinarySensor | ValveControllerBinarySensor] = [ - ValveControllerBinarySensor( - entry, hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], description - ) + ValveControllerBinarySensor(entry, valve_controller_coordinators, description) for description in VALVE_CONTROLLER_DESCRIPTIONS ] @@ -98,9 +112,7 @@ async def async_setup_entry( sensors.extend( [ PairedSensorBinarySensor(entry, coordinator, description) - for coordinator in hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR_PAIRED_SENSOR - ].values() + for coordinator in paired_sensor_coordinators.values() for description in PAIRED_SENSOR_DESCRIPTIONS ] ) @@ -111,10 +123,12 @@ async def async_setup_entry( class PairedSensorBinarySensor(PairedSensorEntity, BinarySensorEntity): """Define a binary sensor related to a Guardian valve controller.""" + entity_description: BinarySensorEntityDescription + def __init__( self, entry: ConfigEntry, - coordinator: DataUpdateCoordinator, + coordinator: GuardianDataUpdateCoordinator, description: BinarySensorEntityDescription, ) -> None: """Initialize.""" @@ -134,45 +148,26 @@ class PairedSensorBinarySensor(PairedSensorEntity, BinarySensorEntity): class ValveControllerBinarySensor(ValveControllerEntity, BinarySensorEntity): """Define a binary sensor related to a Guardian valve controller.""" + entity_description: ValveControllerBinarySensorDescription + def __init__( self, entry: ConfigEntry, - coordinators: dict[str, DataUpdateCoordinator], - description: BinarySensorEntityDescription, + coordinators: dict[str, GuardianDataUpdateCoordinator], + description: ValveControllerBinarySensorDescription, ) -> None: """Initialize.""" super().__init__(entry, coordinators, description) self._attr_is_on = True - async def _async_continue_entity_setup(self) -> None: - """Add an API listener.""" - if self.entity_description.key == SENSOR_KIND_AP_INFO: - self.async_add_coordinator_update_listener(API_WIFI_STATUS) - elif self.entity_description.key == SENSOR_KIND_LEAK_DETECTED: - self.async_add_coordinator_update_listener(API_SYSTEM_ONBOARD_SENSOR_STATUS) - @callback def _async_update_from_latest_data(self) -> None: """Update the entity.""" if self.entity_description.key == SENSOR_KIND_AP_INFO: - self._attr_available = self.coordinators[ - API_WIFI_STATUS - ].last_update_success - self._attr_is_on = self.coordinators[API_WIFI_STATUS].data[ - "station_connected" - ] - self._attr_extra_state_attributes.update( - { - ATTR_CONNECTED_CLIENTS: self.coordinators[API_WIFI_STATUS].data.get( - "ap_clients" - ) - } - ) + self._attr_is_on = self.coordinator.data["station_connected"] + self._attr_extra_state_attributes[ + ATTR_CONNECTED_CLIENTS + ] = self.coordinator.data.get("ap_clients") elif self.entity_description.key == SENSOR_KIND_LEAK_DETECTED: - self._attr_available = self.coordinators[ - API_SYSTEM_ONBOARD_SENSOR_STATUS - ].last_update_success - self._attr_is_on = self.coordinators[API_SYSTEM_ONBOARD_SENSOR_STATUS].data[ - "wet" - ] + self._attr_is_on = self.coordinator.data["wet"] diff --git a/homeassistant/components/guardian/button.py b/homeassistant/components/guardian/button.py index e7cc757d367..e013cde85d6 100644 --- a/homeassistant/components/guardian/button.py +++ b/homeassistant/components/guardian/button.py @@ -17,24 +17,26 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import ValveControllerEntity -from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN +from . import ValveControllerEntity, ValveControllerEntityDescription +from .const import API_SYSTEM_DIAGNOSTICS, DATA_CLIENT, DATA_COORDINATOR, DOMAIN +from .util import GuardianDataUpdateCoordinator @dataclass -class GuardianButtonDescriptionMixin: - """Define an entity description mixin for Guardian buttons.""" +class GuardianButtonEntityDescriptionMixin: + """Define an mixin for button entities.""" push_action: Callable[[Client], Awaitable] @dataclass -class GuardianButtonDescription( - ButtonEntityDescription, GuardianButtonDescriptionMixin +class ValveControllerButtonDescription( + ButtonEntityDescription, + ValveControllerEntityDescription, + GuardianButtonEntityDescriptionMixin, ): - """Describe a Guardian button description.""" + """Describe a Guardian valve controller button.""" BUTTON_KIND_REBOOT = "reboot" @@ -52,15 +54,21 @@ async def _async_valve_reset(client: Client) -> None: BUTTON_DESCRIPTIONS = ( - GuardianButtonDescription( + ValveControllerButtonDescription( key=BUTTON_KIND_REBOOT, name="Reboot", push_action=_async_reboot, + # Buttons don't actually need a coordinator; we give them one so they can + # properly inherit from GuardianEntity: + api_category=API_SYSTEM_DIAGNOSTICS, ), - GuardianButtonDescription( + ValveControllerButtonDescription( key=BUTTON_KIND_RESET_VALVE_DIAGNOSTICS, name="Reset valve diagnostics", push_action=_async_valve_reset, + # Buttons don't actually need a coordinator; we give them one so they can + # properly inherit from GuardianEntity: + api_category=API_SYSTEM_DIAGNOSTICS, ), ) @@ -69,16 +77,13 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian buttons based on a config entry.""" + entry_data = hass.data[DOMAIN][entry.entry_id] + client = entry_data[DATA_CLIENT] + valve_controller_coordinators = entry_data[DATA_COORDINATOR] + async_add_entities( - [ - GuardianButton( - entry, - hass.data[DOMAIN][entry.entry_id][DATA_CLIENT], - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], - description, - ) - for description in BUTTON_DESCRIPTIONS - ] + GuardianButton(entry, valve_controller_coordinators, description, client) + for description in BUTTON_DESCRIPTIONS ) @@ -88,14 +93,14 @@ class GuardianButton(ValveControllerEntity, ButtonEntity): _attr_device_class = ButtonDeviceClass.RESTART _attr_entity_category = EntityCategory.CONFIG - entity_description: GuardianButtonDescription + entity_description: ValveControllerButtonDescription def __init__( self, entry: ConfigEntry, + coordinators: dict[str, GuardianDataUpdateCoordinator], + description: ValveControllerButtonDescription, client: Client, - coordinators: dict[str, DataUpdateCoordinator], - description: GuardianButtonDescription, ) -> None: """Initialize.""" super().__init__(entry, coordinators, description) diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 895452e0fda..c4fd1e110fa 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -1,6 +1,8 @@ """Sensors for the Elexa Guardian integration.""" from __future__ import annotations +from dataclasses import dataclass + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -14,7 +16,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import PairedSensorEntity, ValveControllerEntity +from . import ( + PairedSensorEntity, + ValveControllerEntity, + ValveControllerEntityDescription, +) from .const import ( API_SYSTEM_DIAGNOSTICS, API_SYSTEM_ONBOARD_SENSOR_STATUS, @@ -29,35 +35,47 @@ SENSOR_KIND_BATTERY = "battery" SENSOR_KIND_TEMPERATURE = "temperature" SENSOR_KIND_UPTIME = "uptime" -SENSOR_DESCRIPTION_BATTERY = SensorEntityDescription( - key=SENSOR_KIND_BATTERY, - name="Battery", - device_class=SensorDeviceClass.BATTERY, - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=PERCENTAGE, -) -SENSOR_DESCRIPTION_TEMPERATURE = SensorEntityDescription( - key=SENSOR_KIND_TEMPERATURE, - name="Temperature", - device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement=TEMP_FAHRENHEIT, - state_class=SensorStateClass.MEASUREMENT, -) -SENSOR_DESCRIPTION_UPTIME = SensorEntityDescription( - key=SENSOR_KIND_UPTIME, - name="Uptime", - icon="mdi:timer", - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=TIME_MINUTES, -) + +@dataclass +class ValveControllerSensorDescription( + SensorEntityDescription, ValveControllerEntityDescription +): + """Describe a Guardian valve controller sensor.""" + PAIRED_SENSOR_DESCRIPTIONS = ( - SENSOR_DESCRIPTION_BATTERY, - SENSOR_DESCRIPTION_TEMPERATURE, + SensorEntityDescription( + key=SENSOR_KIND_BATTERY, + name="Battery", + device_class=SensorDeviceClass.BATTERY, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + ), + SensorEntityDescription( + key=SENSOR_KIND_TEMPERATURE, + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_FAHRENHEIT, + state_class=SensorStateClass.MEASUREMENT, + ), ) VALVE_CONTROLLER_DESCRIPTIONS = ( - SENSOR_DESCRIPTION_TEMPERATURE, - SENSOR_DESCRIPTION_UPTIME, + ValveControllerSensorDescription( + key=SENSOR_KIND_TEMPERATURE, + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_FAHRENHEIT, + state_class=SensorStateClass.MEASUREMENT, + api_category=API_SYSTEM_ONBOARD_SENSOR_STATUS, + ), + ValveControllerSensorDescription( + key=SENSOR_KIND_UPTIME, + name="Uptime", + icon="mdi:timer", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=TIME_MINUTES, + api_category=API_SYSTEM_DIAGNOSTICS, + ), ) @@ -65,19 +83,16 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" + entry_data = hass.data[DOMAIN][entry.entry_id] + paired_sensor_coordinators = entry_data[DATA_COORDINATOR_PAIRED_SENSOR] + valve_controller_coordinators = entry_data[DATA_COORDINATOR] @callback def add_new_paired_sensor(uid: str) -> None: """Add a new paired sensor.""" - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR_PAIRED_SENSOR][ - uid - ] - async_add_entities( - [ - PairedSensorSensor(entry, coordinator, description) - for description in PAIRED_SENSOR_DESCRIPTIONS - ] + PairedSensorSensor(entry, paired_sensor_coordinators[uid], description) + for description in PAIRED_SENSOR_DESCRIPTIONS ) # Handle adding paired sensors after HASS startup: @@ -91,9 +106,7 @@ async def async_setup_entry( # Add all valve controller-specific binary sensors: sensors: list[PairedSensorSensor | ValveControllerSensor] = [ - ValveControllerSensor( - entry, hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], description - ) + ValveControllerSensor(entry, valve_controller_coordinators, description) for description in VALVE_CONTROLLER_DESCRIPTIONS ] @@ -101,9 +114,7 @@ async def async_setup_entry( sensors.extend( [ PairedSensorSensor(entry, coordinator, description) - for coordinator in hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR_PAIRED_SENSOR - ].values() + for coordinator in paired_sensor_coordinators.values() for description in PAIRED_SENSOR_DESCRIPTIONS ] ) @@ -114,6 +125,8 @@ async def async_setup_entry( class PairedSensorSensor(PairedSensorEntity, SensorEntity): """Define a binary sensor related to a Guardian valve controller.""" + entity_description: SensorEntityDescription + @callback def _async_update_from_latest_data(self) -> None: """Update the entity.""" @@ -126,25 +139,12 @@ class PairedSensorSensor(PairedSensorEntity, SensorEntity): class ValveControllerSensor(ValveControllerEntity, SensorEntity): """Define a generic Guardian sensor.""" - async def _async_continue_entity_setup(self) -> None: - """Register API interest (and related tasks) when the entity is added.""" - if self.entity_description.key == SENSOR_KIND_TEMPERATURE: - self.async_add_coordinator_update_listener(API_SYSTEM_ONBOARD_SENSOR_STATUS) + entity_description: ValveControllerSensorDescription @callback def _async_update_from_latest_data(self) -> None: """Update the entity.""" if self.entity_description.key == SENSOR_KIND_TEMPERATURE: - self._attr_available = self.coordinators[ - API_SYSTEM_ONBOARD_SENSOR_STATUS - ].last_update_success - self._attr_native_value = self.coordinators[ - API_SYSTEM_ONBOARD_SENSOR_STATUS - ].data["temperature"] + self._attr_native_value = self.coordinator.data["temperature"] elif self.entity_description.key == SENSOR_KIND_UPTIME: - self._attr_available = self.coordinators[ - API_SYSTEM_DIAGNOSTICS - ].last_update_success - self._attr_native_value = self.coordinators[API_SYSTEM_DIAGNOSTICS].data[ - "uptime" - ] + self._attr_native_value = self.coordinator.data["uptime"] diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 485b0a3ffbc..c58f4548a87 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -1,6 +1,7 @@ """Switches for the Elexa Guardian integration.""" from __future__ import annotations +from dataclasses import dataclass from typing import Any from aioguardian import Client @@ -11,10 +12,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import ValveControllerEntity +from . import ValveControllerEntity, ValveControllerEntityDescription from .const import API_VALVE_STATUS, DATA_CLIENT, DATA_COORDINATOR, DOMAIN +from .util import GuardianDataUpdateCoordinator ATTR_AVG_CURRENT = "average_current" ATTR_INST_CURRENT = "instantaneous_current" @@ -23,10 +24,21 @@ ATTR_TRAVEL_COUNT = "travel_count" SWITCH_KIND_VALVE = "valve" -SWITCH_DESCRIPTION_VALVE = SwitchEntityDescription( - key=SWITCH_KIND_VALVE, - name="Valve controller", - icon="mdi:water", + +@dataclass +class ValveControllerSwitchDescription( + SwitchEntityDescription, ValveControllerEntityDescription +): + """Describe a Guardian valve controller switch.""" + + +VALVE_CONTROLLER_DESCRIPTIONS = ( + ValveControllerSwitchDescription( + key=SWITCH_KIND_VALVE, + name="Valve controller", + icon="mdi:water", + api_category=API_VALVE_STATUS, + ), ) @@ -34,61 +46,53 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" + entry_data = hass.data[DOMAIN][entry.entry_id] + client = entry_data[DATA_CLIENT] + valve_controller_coordinators = entry_data[DATA_COORDINATOR] + async_add_entities( - [ - ValveControllerSwitch( - entry, - hass.data[DOMAIN][entry.entry_id][DATA_CLIENT], - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], - ) - ] + ValveControllerSwitch(entry, valve_controller_coordinators, description, client) + for description in VALVE_CONTROLLER_DESCRIPTIONS ) class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): """Define a switch to open/close the Guardian valve.""" + entity_description: ValveControllerSwitchDescription + + ON_STATES = { + "start_opening", + "opening", + "finish_opening", + "opened", + } + def __init__( self, entry: ConfigEntry, + coordinators: dict[str, GuardianDataUpdateCoordinator], + description: ValveControllerSwitchDescription, client: Client, - coordinators: dict[str, DataUpdateCoordinator], ) -> None: """Initialize.""" - super().__init__(entry, coordinators, SWITCH_DESCRIPTION_VALVE) + super().__init__(entry, coordinators, description) self._attr_is_on = True self._client = client - async def _async_continue_entity_setup(self) -> None: - """Register API interest (and related tasks) when the entity is added.""" - self.async_add_coordinator_update_listener(API_VALVE_STATUS) - @callback def _async_update_from_latest_data(self) -> None: """Update the entity.""" - self._attr_available = self.coordinators[API_VALVE_STATUS].last_update_success - self._attr_is_on = self.coordinators[API_VALVE_STATUS].data["state"] in ( - "start_opening", - "opening", - "finish_opening", - "opened", - ) - + self._attr_is_on = self.coordinator.data["state"] in self.ON_STATES self._attr_extra_state_attributes.update( { - ATTR_AVG_CURRENT: self.coordinators[API_VALVE_STATUS].data[ - "average_current" - ], - ATTR_INST_CURRENT: self.coordinators[API_VALVE_STATUS].data[ - "instantaneous_current" - ], - ATTR_INST_CURRENT_DDT: self.coordinators[API_VALVE_STATUS].data[ + ATTR_AVG_CURRENT: self.coordinator.data["average_current"], + ATTR_INST_CURRENT: self.coordinator.data["instantaneous_current"], + ATTR_INST_CURRENT_DDT: self.coordinator.data[ "instantaneous_current_ddt" ], - ATTR_TRAVEL_COUNT: self.coordinators[API_VALVE_STATUS].data[ - "travel_count" - ], + ATTR_TRAVEL_COUNT: self.coordinator.data["travel_count"], } ) From 3144d179e0073f780c8d6738457f9e455c641339 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 18 Jul 2022 17:39:38 +0200 Subject: [PATCH 2662/3516] Make UniFi utilise forward_entry_setups (#74835) --- homeassistant/components/unifi/__init__.py | 50 ++++------ homeassistant/components/unifi/config_flow.py | 14 +-- homeassistant/components/unifi/controller.py | 94 ++++++++++--------- tests/components/unifi/test_controller.py | 24 ++--- tests/components/unifi/test_init.py | 47 ++++------ 5 files changed, 103 insertions(+), 126 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 1369bb69e1b..086bae8d8cf 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -5,19 +5,13 @@ from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType -from .const import ( - ATTR_MANUFACTURER, - CONF_CONTROLLER, - DOMAIN as UNIFI_DOMAIN, - LOGGER, - UNIFI_WIRELESS_CLIENTS, -) -from .controller import UniFiController +from .const import CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN, UNIFI_WIRELESS_CLIENTS +from .controller import PLATFORMS, UniFiController, get_unifi_controller +from .errors import AuthenticationRequired, CannotConnect from .services import async_setup_services, async_unload_services SAVE_DELAY = 10 @@ -40,9 +34,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b # Flat configuration was introduced with 2021.3 await async_flatten_entry_data(hass, config_entry) - controller = UniFiController(hass, config_entry) - if not await controller.async_setup(): - return False + try: + api = await get_unifi_controller(hass, config_entry.data) + controller = UniFiController(hass, config_entry, api) + await controller.initialize() + + except CannotConnect as err: + raise ConfigEntryNotReady from err + + except AuthenticationRequired as err: + raise ConfigEntryAuthFailed from err # Unique ID was introduced with 2021.3 if config_entry.unique_id is None: @@ -50,30 +51,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b config_entry, unique_id=controller.site_id ) - if not hass.data[UNIFI_DOMAIN]: + hass.data[UNIFI_DOMAIN][config_entry.entry_id] = controller + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + await controller.async_update_device_registry() + + if len(hass.data[UNIFI_DOMAIN]) == 1: async_setup_services(hass) - hass.data[UNIFI_DOMAIN][config_entry.entry_id] = controller + api.start_websocket() config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, controller.shutdown) ) - LOGGER.debug("UniFi Network config options %s", config_entry.options) - - if controller.mac is None: - return True - - 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 Network", - default_name="UniFi Network", - ) - return True diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 2f49c15e4d8..4944dd91296 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -9,6 +9,7 @@ from __future__ import annotations from collections.abc import Mapping import socket +from types import MappingProxyType from typing import Any from urllib.parse import urlparse @@ -46,7 +47,7 @@ from .const import ( DEFAULT_POE_CLIENTS, DOMAIN as UNIFI_DOMAIN, ) -from .controller import UniFiController, get_controller +from .controller import UniFiController, get_unifi_controller from .errors import AuthenticationRequired, CannotConnect DEFAULT_PORT = 443 @@ -99,16 +100,9 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): } try: - controller = await get_controller( - self.hass, - host=self.config[CONF_HOST], - username=self.config[CONF_USERNAME], - password=self.config[CONF_PASSWORD], - port=self.config[CONF_PORT], - site=self.config[CONF_SITE_ID], - verify_ssl=self.config[CONF_VERIFY_SSL], + controller = await get_unifi_controller( + self.hass, MappingProxyType(self.config) ) - sites = await controller.sites() except AuthenticationRequired: diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index fa92568b477..7446d6abbff 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -4,6 +4,8 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta import ssl +from types import MappingProxyType +from typing import Any from aiohttp import CookieJar import aiounifi @@ -36,14 +38,19 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client, entity_registry as er +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 from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.helpers.event import async_track_time_interval import homeassistant.util.dt as dt_util from .const import ( + ATTR_MANUFACTURER, CONF_ALLOW_BANDWIDTH_SENSORS, CONF_ALLOW_UPTIME_SENSORS, CONF_BLOCK_CLIENT, @@ -91,12 +98,15 @@ DEVICE_CONNECTED = ( class UniFiController: """Manages a single UniFi Network instance.""" - def __init__(self, hass, config_entry): + def __init__(self, hass, config_entry, api): """Initialize the system.""" self.hass = hass self.config_entry = config_entry + self.api = api + + api.callback = self.async_unifi_signalling_callback + self.available = True - self.api = None self.progress = None self.wireless_clients = None @@ -295,36 +305,18 @@ class UniFiController: unifi_wireless_clients = self.hass.data[UNIFI_WIRELESS_CLIENTS] unifi_wireless_clients.update_data(self.wireless_clients, self.config_entry) - async def async_setup(self): + async def initialize(self): """Set up a UniFi Network instance.""" - try: - self.api = await get_controller( - self.hass, - host=self.config_entry.data[CONF_HOST], - username=self.config_entry.data[CONF_USERNAME], - password=self.config_entry.data[CONF_PASSWORD], - port=self.config_entry.data[CONF_PORT], - site=self.config_entry.data[CONF_SITE_ID], - verify_ssl=self.config_entry.data[CONF_VERIFY_SSL], - async_callback=self.async_unifi_signalling_callback, - ) - await self.api.initialize() - - sites = await self.api.sites() - description = await self.api.site_description() - - except CannotConnect as err: - raise ConfigEntryNotReady from err - - except AuthenticationRequired as err: - raise ConfigEntryAuthFailed from err + await self.api.initialize() + sites = await self.api.sites() for site in sites.values(): if self.site == site["name"]: self.site_id = site["_id"] self._site_name = site["desc"] break + description = await self.api.site_description() self._site_role = description[0]["site_role"] # Restore clients that are not a part of active clients list. @@ -357,18 +349,12 @@ class UniFiController: self.wireless_clients = wireless_clients.get_data(self.config_entry) self.update_wireless_clients() - self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) - - self.api.start_websocket() - self.config_entry.add_update_listener(self.async_config_entry_updated) self._cancel_heartbeat_check = async_track_time_interval( self.hass, self._async_check_for_stale, CHECK_HEARTBEAT_INTERVAL ) - return True - @callback def async_heartbeat( self, unique_id: str, heartbeat_expire_time: datetime | None = None @@ -397,6 +383,22 @@ class UniFiController: for unique_id in unique_ids_to_remove: del self._heartbeat_time[unique_id] + async def async_update_device_registry(self) -> None: + """Update device registry.""" + if self.mac is None: + return + + 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.url, + connections={(CONNECTION_NETWORK_MAC, self.mac)}, + default_manufacturer=ATTR_MANUFACTURER, + default_model="UniFi Network", + default_name="UniFi Network", + ) + @staticmethod async def async_config_entry_updated( hass: HomeAssistant, config_entry: ConfigEntry @@ -463,13 +465,14 @@ class UniFiController: return True -async def get_controller( - hass, host, username, password, port, site, verify_ssl, async_callback=None -): +async def get_unifi_controller( + hass: HomeAssistant, + config: MappingProxyType[str, Any], +) -> aiounifi.Controller: """Create a controller object and verify authentication.""" sslcontext = None - if verify_ssl: + if verify_ssl := bool(config.get(CONF_VERIFY_SSL)): session = aiohttp_client.async_get_clientsession(hass) if isinstance(verify_ssl, str): sslcontext = ssl.create_default_context(cafile=verify_ssl) @@ -479,14 +482,13 @@ async def get_controller( ) controller = aiounifi.Controller( - host, - username=username, - password=password, - port=port, - site=site, + host=config[CONF_HOST], + username=config[CONF_USERNAME], + password=config[CONF_PASSWORD], + port=config[CONF_PORT], + site=config[CONF_SITE_ID], websession=session, sslcontext=sslcontext, - callback=async_callback, ) try: @@ -498,7 +500,7 @@ async def get_controller( except aiounifi.Unauthorized as err: LOGGER.warning( "Connected to UniFi Network at %s but not registered: %s", - host, + config[CONF_HOST], err, ) raise AuthenticationRequired from err @@ -510,13 +512,15 @@ async def get_controller( aiounifi.RequestError, aiounifi.ResponseError, ) as err: - LOGGER.error("Error connecting to the UniFi Network at %s: %s", host, err) + LOGGER.error( + "Error connecting to the UniFi Network at %s: %s", config[CONF_HOST], err + ) raise CannotConnect from err except aiounifi.LoginRequired as err: LOGGER.warning( "Connected to UniFi Network at %s but login required: %s", - host, + config[CONF_HOST], err, ) raise AuthenticationRequired from err diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 625afbb4ec6..e420d031f46 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -30,7 +30,7 @@ from homeassistant.components.unifi.const import ( from homeassistant.components.unifi.controller import ( PLATFORMS, RETRY_TIMER, - get_controller, + get_unifi_controller, ) from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect from homeassistant.const import ( @@ -271,7 +271,7 @@ async def test_controller_mac(hass, aioclient_mock): async def test_controller_not_accessible(hass): """Retry to login gets scheduled when connection fails.""" with patch( - "homeassistant.components.unifi.controller.get_controller", + "homeassistant.components.unifi.controller.get_unifi_controller", side_effect=CannotConnect, ): await setup_unifi_integration(hass) @@ -281,7 +281,7 @@ async def test_controller_not_accessible(hass): async def test_controller_trigger_reauth_flow(hass): """Failed authentication trigger a reauthentication flow.""" with patch( - "homeassistant.components.unifi.controller.get_controller", + "homeassistant.components.unifi.get_unifi_controller", side_effect=AuthenticationRequired, ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: await setup_unifi_integration(hass) @@ -292,7 +292,7 @@ async def test_controller_trigger_reauth_flow(hass): async def test_controller_unknown_error(hass): """Unknown errors are handled.""" with patch( - "homeassistant.components.unifi.controller.get_controller", + "homeassistant.components.unifi.controller.get_unifi_controller", side_effect=Exception, ): await setup_unifi_integration(hass) @@ -470,22 +470,22 @@ async def test_reconnect_mechanism_exceptions( mock_reconnect.assert_called_once() -async def test_get_controller(hass): +async def test_get_unifi_controller(hass): """Successful call.""" with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", return_value=True ): - assert await get_controller(hass, **CONTROLLER_DATA) + assert await get_unifi_controller(hass, CONTROLLER_DATA) -async def test_get_controller_verify_ssl_false(hass): +async def test_get_unifi_controller_verify_ssl_false(hass): """Successful call with verify ssl set to false.""" controller_data = dict(CONTROLLER_DATA) controller_data[CONF_VERIFY_SSL] = False with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", return_value=True ): - assert await get_controller(hass, **controller_data) + assert await get_unifi_controller(hass, controller_data) @pytest.mark.parametrize( @@ -501,9 +501,11 @@ async def test_get_controller_verify_ssl_false(hass): (aiounifi.AiounifiException, AuthenticationRequired), ], ) -async def test_get_controller_fails_to_connect(hass, side_effect, raised_exception): - """Check that get_controller can handle controller being unavailable.""" +async def test_get_unifi_controller_fails_to_connect( + hass, side_effect, raised_exception +): + """Check that get_unifi_controller can handle controller being unavailable.""" with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", side_effect=side_effect ), pytest.raises(raised_exception): - await get_controller(hass, **CONTROLLER_DATA) + await get_unifi_controller(hass, CONTROLLER_DATA) diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index f183e1c22ff..03ea89097c5 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -1,10 +1,10 @@ """Test UniFi Network integration setup process.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import patch from homeassistant.components import unifi from homeassistant.components.unifi import async_flatten_entry_data from homeassistant.components.unifi.const import CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN -from homeassistant.helpers import device_registry as dr +from homeassistant.components.unifi.errors import AuthenticationRequired, CannotConnect from homeassistant.setup import async_setup_component from .test_controller import ( @@ -29,40 +29,27 @@ async def test_successful_config_entry(hass, aioclient_mock): assert hass.data[UNIFI_DOMAIN] -async def test_controller_fail_setup(hass): - """Test that a failed setup still stores controller.""" - with patch("homeassistant.components.unifi.UniFiController") as mock_controller: - mock_controller.return_value.async_setup = AsyncMock(return_value=False) +async def test_setup_entry_fails_config_entry_not_ready(hass): + """Failed authentication trigger a reauthentication flow.""" + with patch( + "homeassistant.components.unifi.get_unifi_controller", + side_effect=CannotConnect, + ): await setup_unifi_integration(hass) assert hass.data[UNIFI_DOMAIN] == {} -async def test_controller_mac(hass): - """Test that configured options for a host are loaded via config entry.""" - entry = MockConfigEntry( - domain=UNIFI_DOMAIN, data=ENTRY_CONFIG, unique_id="1", entry_id=1 - ) - entry.add_to_hass(hass) +async def test_setup_entry_fails_trigger_reauth_flow(hass): + """Failed authentication trigger a reauthentication flow.""" + with patch( + "homeassistant.components.unifi.get_unifi_controller", + side_effect=AuthenticationRequired, + ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + await setup_unifi_integration(hass) + mock_flow_init.assert_called_once() - 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 - - device_registry = dr.async_get(hass) - device = device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "mac1")}, - ) - assert device.configuration_url == "https://123:443" - assert device.manufacturer == "Ubiquiti Networks" - assert device.model == "UniFi Network" - assert device.name == "UniFi Network" - assert device.sw_version is None + assert hass.data[UNIFI_DOMAIN] == {} async def test_flatten_entry_data(hass): From 7adb0f0ef5432e79c445f714e6783841acd3b9a4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 18 Jul 2022 22:10:22 +0200 Subject: [PATCH 2663/3516] Custom component -> Custom integration (#75404) --- homeassistant/components/number/__init__.py | 4 ++-- homeassistant/components/sensor/recorder.py | 2 +- homeassistant/components/weather/__init__.py | 2 +- homeassistant/helpers/entity.py | 2 +- homeassistant/helpers/frame.py | 2 +- homeassistant/util/async_.py | 2 +- tests/components/sensor/test_init.py | 2 +- tests/components/sensor/test_recorder.py | 2 +- tests/helpers/test_aiohttp_client.py | 2 +- tests/helpers/test_entity.py | 2 +- tests/helpers/test_httpx_client.py | 2 +- tests/util/test_async.py | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 5ab1eaa7be4..4990fbfa7f8 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -152,7 +152,7 @@ class NumberEntityDescription(EntityDescription): else: module = inspect.getmodule(self) if module and module.__file__ and "custom_components" in module.__file__: - report_issue = "report it to the custom component author." + report_issue = "report it to the custom integration author." else: report_issue = ( "create a bug report at " @@ -222,7 +222,7 @@ class NumberEntity(Entity): ): module = inspect.getmodule(cls) if module and module.__file__ and "custom_components" in module.__file__: - report_issue = "report it to the custom component author." + report_issue = "report it to the custom integration author." else: report_issue = ( "create a bug report at " diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 3fc5cbec7ee..ea7d129a9c3 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -275,7 +275,7 @@ def _suggest_report_issue(hass: HomeAssistant, entity_id: str) -> str: custom_component = entity_sources(hass).get(entity_id, {}).get("custom_component") report_issue = "" if custom_component: - report_issue = "report it to the custom component author." + report_issue = "report it to the custom integration author." else: report_issue = ( "create a bug report at " diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 3e0233917d7..c55ae043622 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -299,7 +299,7 @@ class WeatherEntity(Entity): and module.__file__ and "custom_components" in module.__file__ ): - report_issue = "report it to the custom component author." + report_issue = "report it to the custom integration author." else: report_issue = ( "create a bug report at " diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index f00f7d85e76..cb71cfd9edf 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -936,7 +936,7 @@ class Entity(ABC): """Suggest to report an issue.""" report_issue = "" if "custom_components" in type(self).__module__: - report_issue = "report it to the custom component author." + report_issue = "report it to the custom integration author." else: report_issue = ( "create a bug report at " diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index b81f5f29432..ca5cf759d8e 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -96,7 +96,7 @@ def report_integration( index = found_frame.filename.index(path) if path == "custom_components/": - extra = " to the custom component author" + extra = " to the custom integration author" else: extra = "" diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 8b96e85664d..e9c5a41062e 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -150,7 +150,7 @@ def check_loop( integration = found_frame.filename[start:end] if path == "custom_components/": - extra = " to the custom component author" + extra = " to the custom integration author" else: extra = "" diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 0bae8235ff9..3a593b0e6cc 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -107,7 +107,7 @@ async def test_deprecated_last_reset( f"with state_class {state_class} has set last_reset. Setting last_reset for " "entities with state_class other than 'total' is not supported. Please update " "your configuration if state_class is manually configured, otherwise report it " - "to the custom component author." + "to the custom integration author." ) in caplog.text state = hass.states.get("sensor.test") diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index c62d1309c7a..4be59e4c82c 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -775,7 +775,7 @@ def test_compile_hourly_sum_statistics_nan_inf_state( ( "sensor.custom_sensor", "from integration test ", - "report it to the custom component author", + "report it to the custom integration author", ), ], ) diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 1ffb4267167..7bac31e8e19 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -183,7 +183,7 @@ async def test_warning_close_session_custom(hass, caplog): await session.close() assert ( "Detected integration that closes the Home Assistant aiohttp session. " - "Please report issue to the custom component author for hue using this method at " + "Please report issue to the custom integration author for hue using this method at " "custom_components/hue/light.py, line 23: await session.close()" in caplog.text ) diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index b9067a3db1c..698d3cfe98a 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -715,7 +715,7 @@ async def test_warn_slow_write_state_custom_component(hass, caplog): assert ( "Updating state for comp_test.test_entity " "(.CustomComponentEntity'>) " - "took 10.000 seconds. Please report it to the custom component author." + "took 10.000 seconds. Please report it to the custom integration author." ) in caplog.text diff --git a/tests/helpers/test_httpx_client.py b/tests/helpers/test_httpx_client.py index cdb650f7686..068c0fe1470 100644 --- a/tests/helpers/test_httpx_client.py +++ b/tests/helpers/test_httpx_client.py @@ -153,6 +153,6 @@ async def test_warning_close_session_custom(hass, caplog): await httpx_session.aclose() assert ( "Detected integration that closes the Home Assistant httpx client. " - "Please report issue to the custom component author for hue using this method at " + "Please report issue to the custom integration author for hue using this method at " "custom_components/hue/light.py, line 23: await session.aclose()" in caplog.text ) diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 9bae6f5ebea..10861767de1 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -168,7 +168,7 @@ async def test_check_loop_async_custom(caplog): hasync.check_loop(banned_function) assert ( "Detected blocking call to banned_function inside the event loop. This is " - "causing stability issues. Please report issue to the custom component author " + "causing stability issues. Please report issue to the custom integration author " "for hue doing blocking calls at custom_components/hue/light.py, line 23: " "self.light.is_on" in caplog.text ) From 8b912d1d91c6376f79f213769e5c0ab72570d1b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Jul 2022 15:28:14 -0500 Subject: [PATCH 2664/3516] Significantly improve BLE reliablity with linux/dbus for HKC (#75410) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 192a5cb701a..10ca50db820 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.1.7"], + "requirements": ["aiohomekit==1.1.8"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "after_dependencies": ["zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 359b1541de8..30af50ccc15 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.7 +aiohomekit==1.1.8 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 450b4be10e1..e679fb6827a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.7 +aiohomekit==1.1.8 # homeassistant.components.emulated_hue # homeassistant.components.http From 5928a7d494cd3ba91c92c4ffc598bb0f5d07077d Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 19 Jul 2022 07:02:58 +1000 Subject: [PATCH 2665/3516] Correct devices in Advantage Air (#75395) --- .../components/advantage_air/binary_sensor.py | 2 +- homeassistant/components/advantage_air/climate.py | 1 - homeassistant/components/advantage_air/entity.py | 8 +++++--- homeassistant/components/advantage_air/select.py | 2 +- homeassistant/components/advantage_air/sensor.py | 2 +- homeassistant/components/advantage_air/switch.py | 2 +- .../components/advantage_air/test_binary_sensor.py | 12 ++++++------ tests/components/advantage_air/test_climate.py | 6 +++--- tests/components/advantage_air/test_cover.py | 10 +++++----- tests/components/advantage_air/test_select.py | 2 +- tests/components/advantage_air/test_sensor.py | 14 +++++++------- tests/components/advantage_air/test_switch.py | 2 +- 12 files changed, 32 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index c87bf37ca92..9fc53d7e1dc 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -43,11 +43,11 @@ class AdvantageAirFilter(AdvantageAirEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.PROBLEM _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_name = "Filter" def __init__(self, instance, ac_key): """Initialize an Advantage Air Filter sensor.""" super().__init__(instance, ac_key) - self._attr_name = f'{self._ac["name"]} filter' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-filter' ) diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index 192e1987902..1d89c313579 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -108,7 +108,6 @@ class AdvantageAirAC(AdvantageAirClimateEntity): def __init__(self, instance, ac_key): """Initialize an AdvantageAir AC unit.""" super().__init__(instance, ac_key) - self._attr_name = self._ac["name"] self._attr_unique_id = f'{self.coordinator.data["system"]["rid"]}-{ac_key}' if self._ac.get("myAutoModeEnabled"): self._attr_hvac_modes = AC_HVAC_MODES + [HVACMode.AUTO] diff --git a/homeassistant/components/advantage_air/entity.py b/homeassistant/components/advantage_air/entity.py index b0ff1bfb8c8..6c434518656 100644 --- a/homeassistant/components/advantage_air/entity.py +++ b/homeassistant/components/advantage_air/entity.py @@ -18,11 +18,13 @@ class AdvantageAirEntity(CoordinatorEntity): self.ac_key = ac_key self.zone_key = zone_key self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self.coordinator.data["system"]["rid"])}, + via_device=(DOMAIN, self.coordinator.data["system"]["rid"]), + identifiers={ + (DOMAIN, f"{self.coordinator.data['system']['rid']}_{ac_key}") + }, manufacturer="Advantage Air", model=self.coordinator.data["system"]["sysType"], - name=self.coordinator.data["system"]["name"], - sw_version=self.coordinator.data["system"]["myAppRev"], + name=self._ac["name"], ) @property diff --git a/homeassistant/components/advantage_air/select.py b/homeassistant/components/advantage_air/select.py index 0edae258279..5dfa92c10ad 100644 --- a/homeassistant/components/advantage_air/select.py +++ b/homeassistant/components/advantage_air/select.py @@ -32,11 +32,11 @@ class AdvantageAirMyZone(AdvantageAirEntity, SelectEntity): _attr_options = [ADVANTAGE_AIR_INACTIVE] _number_to_name = {0: ADVANTAGE_AIR_INACTIVE} _name_to_number = {ADVANTAGE_AIR_INACTIVE: 0} + _attr_name = "MyZone" def __init__(self, instance, ac_key): """Initialize an Advantage Air MyZone control.""" super().__init__(instance, ac_key) - self._attr_name = f'{self._ac["name"]} myZone' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-myzone' ) diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index 855a0e6d15f..370aab7b292 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -67,7 +67,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): super().__init__(instance, ac_key) self.action = action self._time_key = f"countDownTo{action}" - self._attr_name = f'{self._ac["name"]} time to {action}' + self._attr_name = f"Time to {action}" self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-timeto{action}' ) diff --git a/homeassistant/components/advantage_air/switch.py b/homeassistant/components/advantage_air/switch.py index 3c0060c65b3..504578c72e2 100644 --- a/homeassistant/components/advantage_air/switch.py +++ b/homeassistant/components/advantage_air/switch.py @@ -32,11 +32,11 @@ class AdvantageAirFreshAir(AdvantageAirEntity, SwitchEntity): """Representation of Advantage Air fresh air control.""" _attr_icon = "mdi:air-filter" + _attr_name = "Fresh air" def __init__(self, instance, ac_key): """Initialize an Advantage Air fresh air control.""" super().__init__(instance, ac_key) - self._attr_name = f'{self._ac["name"]} Fresh Air' self._attr_unique_id = ( f'{self.coordinator.data["system"]["rid"]}-{ac_key}-freshair' ) diff --git a/tests/components/advantage_air/test_binary_sensor.py b/tests/components/advantage_air/test_binary_sensor.py index 9aa092a8679..4bd792808fb 100644 --- a/tests/components/advantage_air/test_binary_sensor.py +++ b/tests/components/advantage_air/test_binary_sensor.py @@ -34,7 +34,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test First Air Filter - entity_id = "binary_sensor.testname_ac_one_filter" + entity_id = "binary_sensor.ac_one_filter" state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF @@ -44,7 +44,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-filter" # Test Second Air Filter - entity_id = "binary_sensor.testname_ac_two_filter" + entity_id = "binary_sensor.ac_two_filter" state = hass.states.get(entity_id) assert state assert state.state == STATE_ON @@ -54,7 +54,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac2-filter" # Test First Motion Sensor - entity_id = "binary_sensor.testname_zone_open_with_sensor_motion" + entity_id = "binary_sensor.ac_one_zone_open_with_sensor_motion" state = hass.states.get(entity_id) assert state assert state.state == STATE_ON @@ -64,7 +64,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-motion" # Test Second Motion Sensor - entity_id = "binary_sensor.testname_zone_closed_with_sensor_motion" + entity_id = "binary_sensor.ac_one_zone_closed_with_sensor_motion" state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF @@ -74,7 +74,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z02-motion" # Test First MyZone Sensor (disabled by default) - entity_id = "binary_sensor.testname_zone_open_with_sensor_myzone" + entity_id = "binary_sensor.ac_one_zone_open_with_sensor_myzone" assert not hass.states.get(entity_id) @@ -96,7 +96,7 @@ async def test_binary_sensor_async_setup_entry(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-myzone" # Test Second Motion Sensor (disabled by default) - entity_id = "binary_sensor.testname_zone_closed_with_sensor_myzone" + entity_id = "binary_sensor.ac_one_zone_closed_with_sensor_myzone" assert not hass.states.get(entity_id) diff --git a/tests/components/advantage_air/test_climate.py b/tests/components/advantage_air/test_climate.py index 47073f27fc1..6ee0a614b09 100644 --- a/tests/components/advantage_air/test_climate.py +++ b/tests/components/advantage_air/test_climate.py @@ -51,7 +51,7 @@ async def test_climate_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test Main Climate Entity - entity_id = "climate.testname_ac_one" + entity_id = "climate.ac_one" state = hass.states.get(entity_id) assert state assert state.state == HVACMode.FAN_ONLY @@ -122,7 +122,7 @@ async def test_climate_async_setup_entry(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" # Test Climate Zone Entity - entity_id = "climate.testname_zone_open_with_sensor" + entity_id = "climate.ac_one_zone_open_with_sensor" state = hass.states.get(entity_id) assert state assert state.attributes.get("min_temp") == 16 @@ -204,7 +204,7 @@ async def test_climate_async_failed_update(hass, aioclient_mock): await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: ["climate.testname_ac_one"], ATTR_TEMPERATURE: 25}, + {ATTR_ENTITY_ID: ["climate.ac_one"], ATTR_TEMPERATURE: 25}, blocking=True, ) assert len(aioclient_mock.mock_calls) == 2 diff --git a/tests/components/advantage_air/test_cover.py b/tests/components/advantage_air/test_cover.py index c638c6a3c87..90cdf9a2168 100644 --- a/tests/components/advantage_air/test_cover.py +++ b/tests/components/advantage_air/test_cover.py @@ -45,7 +45,7 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test Cover Zone Entity - entity_id = "cover.testname_zone_open_without_sensor" + entity_id = "cover.ac_two_zone_open_without_sensor" state = hass.states.get(entity_id) assert state assert state.state == STATE_OPEN @@ -119,8 +119,8 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): SERVICE_CLOSE_COVER, { ATTR_ENTITY_ID: [ - "cover.testname_zone_open_without_sensor", - "cover.testname_zone_closed_without_sensor", + "cover.ac_two_zone_open_without_sensor", + "cover.ac_two_zone_closed_without_sensor", ] }, blocking=True, @@ -134,8 +134,8 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): SERVICE_OPEN_COVER, { ATTR_ENTITY_ID: [ - "cover.testname_zone_open_without_sensor", - "cover.testname_zone_closed_without_sensor", + "cover.ac_two_zone_open_without_sensor", + "cover.ac_two_zone_closed_without_sensor", ] }, blocking=True, diff --git a/tests/components/advantage_air/test_select.py b/tests/components/advantage_air/test_select.py index 2ba982fc384..20d42fdcffc 100644 --- a/tests/components/advantage_air/test_select.py +++ b/tests/components/advantage_air/test_select.py @@ -37,7 +37,7 @@ async def test_select_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test MyZone Select Entity - entity_id = "select.testname_ac_one_myzone" + entity_id = "select.ac_one_myzone" state = hass.states.get(entity_id) assert state assert state.state == "Zone open with Sensor" diff --git a/tests/components/advantage_air/test_sensor.py b/tests/components/advantage_air/test_sensor.py index 70322f4c9df..4dc2f1baaff 100644 --- a/tests/components/advantage_air/test_sensor.py +++ b/tests/components/advantage_air/test_sensor.py @@ -41,7 +41,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test First TimeToOn Sensor - entity_id = "sensor.testname_ac_one_time_to_on" + entity_id = "sensor.ac_one_time_to_on" state = hass.states.get(entity_id) assert state assert int(state.state) == 0 @@ -66,7 +66,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" # Test First TimeToOff Sensor - entity_id = "sensor.testname_ac_one_time_to_off" + entity_id = "sensor.ac_one_time_to_off" state = hass.states.get(entity_id) assert state assert int(state.state) == 10 @@ -91,7 +91,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" # Test First Zone Vent Sensor - entity_id = "sensor.testname_zone_open_with_sensor_vent" + entity_id = "sensor.ac_one_zone_open_with_sensor_vent" state = hass.states.get(entity_id) assert state assert int(state.state) == 100 @@ -101,7 +101,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-vent" # Test Second Zone Vent Sensor - entity_id = "sensor.testname_zone_closed_with_sensor_vent" + entity_id = "sensor.ac_one_zone_closed_with_sensor_vent" state = hass.states.get(entity_id) assert state assert int(state.state) == 0 @@ -111,7 +111,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z02-vent" # Test First Zone Signal Sensor - entity_id = "sensor.testname_zone_open_with_sensor_signal" + entity_id = "sensor.ac_one_zone_open_with_sensor_signal" state = hass.states.get(entity_id) assert state assert int(state.state) == 40 @@ -121,7 +121,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z01-signal" # Test Second Zone Signal Sensor - entity_id = "sensor.testname_zone_closed_with_sensor_signal" + entity_id = "sensor.ac_one_zone_closed_with_sensor_signal" state = hass.states.get(entity_id) assert state assert int(state.state) == 10 @@ -131,7 +131,7 @@ async def test_sensor_platform(hass, aioclient_mock): assert entry.unique_id == "uniqueid-ac1-z02-signal" # Test First Zone Temp Sensor (disabled by default) - entity_id = "sensor.testname_zone_open_with_sensor_temperature" + entity_id = "sensor.ac_one_zone_open_with_sensor_temperature" assert not hass.states.get(entity_id) diff --git a/tests/components/advantage_air/test_switch.py b/tests/components/advantage_air/test_switch.py index 41af9e8ff80..be78edf8ffe 100644 --- a/tests/components/advantage_air/test_switch.py +++ b/tests/components/advantage_air/test_switch.py @@ -41,7 +41,7 @@ async def test_cover_async_setup_entry(hass, aioclient_mock): assert len(aioclient_mock.mock_calls) == 1 # Test Switch Entity - entity_id = "switch.testname_ac_one_fresh_air" + entity_id = "switch.ac_one_fresh_air" state = hass.states.get(entity_id) assert state assert state.state == STATE_OFF From 45d1f8bc55e72c7bbe5f8e011b05e8a1d13f8718 Mon Sep 17 00:00:00 2001 From: stegm Date: Mon, 18 Jul 2022 23:08:18 +0200 Subject: [PATCH 2666/3516] Address late review of kostal plenticore (#75297) * Changtes from review #64927 * Fix unit tests for number. * Changes from review. --- .../components/kostal_plenticore/__init__.py | 2 +- .../components/kostal_plenticore/const.py | 54 ---- .../components/kostal_plenticore/number.py | 72 ++++- .../kostal_plenticore/test_number.py | 265 +++++++++--------- 4 files changed, 201 insertions(+), 192 deletions(-) diff --git a/homeassistant/components/kostal_plenticore/__init__.py b/homeassistant/components/kostal_plenticore/__init__.py index 6c7a7cde523..24e8ab9f0d3 100644 --- a/homeassistant/components/kostal_plenticore/__init__.py +++ b/homeassistant/components/kostal_plenticore/__init__.py @@ -12,7 +12,7 @@ from .helper import Plenticore _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.SELECT, Platform.SENSOR, Platform.SWITCH, Platform.NUMBER] +PLATFORMS = [Platform.NUMBER, Platform.SELECT, Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/kostal_plenticore/const.py b/homeassistant/components/kostal_plenticore/const.py index 11bb794f799..7ae0b13f0e8 100644 --- a/homeassistant/components/kostal_plenticore/const.py +++ b/homeassistant/components/kostal_plenticore/const.py @@ -1,8 +1,6 @@ """Constants for the Kostal Plenticore Solar Inverter integration.""" -from dataclasses import dataclass from typing import NamedTuple -from homeassistant.components.number import NumberEntityDescription from homeassistant.components.sensor import ( ATTR_STATE_CLASS, SensorDeviceClass, @@ -18,7 +16,6 @@ from homeassistant.const import ( PERCENTAGE, POWER_WATT, ) -from homeassistant.helpers.entity import EntityCategory DOMAIN = "kostal_plenticore" @@ -794,57 +791,6 @@ SENSOR_PROCESS_DATA = [ ] -@dataclass -class PlenticoreNumberEntityDescriptionMixin: - """Define an entity description mixin for number entities.""" - - module_id: str - data_id: str - fmt_from: str - fmt_to: str - - -@dataclass -class PlenticoreNumberEntityDescription( - NumberEntityDescription, PlenticoreNumberEntityDescriptionMixin -): - """Describes a Plenticore number entity.""" - - -NUMBER_SETTINGS_DATA = [ - PlenticoreNumberEntityDescription( - key="battery_min_soc", - entity_category=EntityCategory.CONFIG, - entity_registry_enabled_default=False, - icon="mdi:battery-negative", - name="Battery min SoC", - native_unit_of_measurement=PERCENTAGE, - native_max_value=100, - native_min_value=5, - native_step=5, - module_id="devices:local", - data_id="Battery:MinSoc", - fmt_from="format_round", - fmt_to="format_round_back", - ), - PlenticoreNumberEntityDescription( - key="battery_min_home_consumption", - device_class=SensorDeviceClass.POWER, - entity_category=EntityCategory.CONFIG, - entity_registry_enabled_default=False, - name="Battery min Home Consumption", - native_unit_of_measurement=POWER_WATT, - native_max_value=38000, - native_min_value=50, - native_step=1, - module_id="devices:local", - data_id="Battery:MinHomeComsumption", - fmt_from="format_round", - fmt_to="format_round_back", - ), -] - - class SwitchData(NamedTuple): """Representation of a SelectData tuple.""" diff --git a/homeassistant/components/kostal_plenticore/number.py b/homeassistant/components/kostal_plenticore/number.py index 1ad911f6d15..69fba631b34 100644 --- a/homeassistant/components/kostal_plenticore/number.py +++ b/homeassistant/components/kostal_plenticore/number.py @@ -2,25 +2,82 @@ from __future__ import annotations from abc import ABC +from dataclasses import dataclass from datetime import timedelta -from functools import partial import logging from kostal.plenticore import SettingsData -from homeassistant.components.number import NumberEntity, NumberMode +from homeassistant.components.number import ( + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE, POWER_WATT from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, NUMBER_SETTINGS_DATA, PlenticoreNumberEntityDescription +from .const import DOMAIN from .helper import PlenticoreDataFormatter, SettingDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) +@dataclass +class PlenticoreNumberEntityDescriptionMixin: + """Define an entity description mixin for number entities.""" + + module_id: str + data_id: str + fmt_from: str + fmt_to: str + + +@dataclass +class PlenticoreNumberEntityDescription( + NumberEntityDescription, PlenticoreNumberEntityDescriptionMixin +): + """Describes a Plenticore number entity.""" + + +NUMBER_SETTINGS_DATA = [ + PlenticoreNumberEntityDescription( + key="battery_min_soc", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:battery-negative", + name="Battery min SoC", + native_unit_of_measurement=PERCENTAGE, + native_max_value=100, + native_min_value=5, + native_step=5, + module_id="devices:local", + data_id="Battery:MinSoc", + fmt_from="format_round", + fmt_to="format_round_back", + ), + PlenticoreNumberEntityDescription( + key="battery_min_home_consumption", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + name="Battery min Home Consumption", + native_unit_of_measurement=POWER_WATT, + native_max_value=38000, + native_min_value=50, + native_step=1, + module_id="devices:local", + data_id="Battery:MinHomeComsumption", + fmt_from="format_round", + fmt_to="format_round_back", + ), +] + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: @@ -54,10 +111,9 @@ async def async_setup_entry( continue setting_data = next( - filter( - partial(lambda id, sd: id == sd.id, description.data_id), - available_settings_data[description.module_id], - ) + sd + for sd in available_settings_data[description.module_id] + if description.data_id == sd.id ) entities.append( diff --git a/tests/components/kostal_plenticore/test_number.py b/tests/components/kostal_plenticore/test_number.py index f0fb42f6e78..f6978612cff 100644 --- a/tests/components/kostal_plenticore/test_number.py +++ b/tests/components/kostal_plenticore/test_number.py @@ -1,72 +1,98 @@ """Test Kostal Plenticore number.""" -from unittest.mock import AsyncMock, MagicMock +from collections.abc import Generator +from datetime import timedelta +from unittest.mock import patch -from kostal.plenticore import SettingsData +from kostal.plenticore import PlenticoreApiClient, SettingsData import pytest -from homeassistant.components.kostal_plenticore.const import ( - PlenticoreNumberEntityDescription, +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, ) -from homeassistant.components.kostal_plenticore.number import PlenticoreDataNumber +from homeassistant.components.number.const import ATTR_MAX, ATTR_MIN +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import async_get +from homeassistant.util import dt -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture -def mock_coordinator() -> MagicMock: - """Return a mocked coordinator for tests.""" - coordinator = MagicMock() - coordinator.async_write_data = AsyncMock() - coordinator.async_refresh = AsyncMock() - return coordinator +def mock_plenticore_client() -> Generator[PlenticoreApiClient, None, None]: + """Return a patched PlenticoreApiClient.""" + with patch( + "homeassistant.components.kostal_plenticore.helper.PlenticoreApiClient", + autospec=True, + ) as plenticore_client_class: + yield plenticore_client_class.return_value @pytest.fixture -def mock_number_description() -> PlenticoreNumberEntityDescription: - """Return a PlenticoreNumberEntityDescription for tests.""" - return PlenticoreNumberEntityDescription( - key="mock key", - module_id="moduleid", - data_id="dataid", - native_min_value=0, - native_max_value=1000, - fmt_from="format_round", - fmt_to="format_round_back", - ) +def mock_get_setting_values(mock_plenticore_client: PlenticoreApiClient) -> list: + """Add a setting value to the given Plenticore client. + Returns a list with setting values which can be extended by test cases. + """ -@pytest.fixture -def mock_setting_data() -> SettingsData: - """Return a default SettingsData for tests.""" - return SettingsData( - { - "default": None, - "min": None, - "access": None, - "max": None, - "unit": None, - "type": None, - "id": "data_id", - } - ) - - -async def test_setup_all_entries( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_plenticore: MagicMock -): - """Test if all available entries are setup up.""" - mock_plenticore.client.get_settings.return_value = { + mock_plenticore_client.get_settings.return_value = { "devices:local": [ - SettingsData({"id": "Battery:MinSoc", "min": None, "max": None}), SettingsData( - {"id": "Battery:MinHomeComsumption", "min": None, "max": None} + { + "default": None, + "min": 5, + "max": 100, + "access": "readwrite", + "unit": "%", + "type": "byte", + "id": "Battery:MinSoc", + } + ), + SettingsData( + { + "default": None, + "min": 50, + "max": 38000, + "access": "readwrite", + "unit": "W", + "type": "byte", + "id": "Battery:MinHomeComsumption", + } ), ] } + # this values are always retrieved by the integration on startup + setting_values = [ + { + "devices:local": { + "Properties:SerialNo": "42", + "Branding:ProductName1": "PLENTICORE", + "Branding:ProductName2": "plus 10", + "Properties:VersionIOC": "01.45", + "Properties:VersionMC": " 01.46", + }, + "scb:network": {"Hostname": "scb"}, + } + ] + + mock_plenticore_client.get_setting_values.side_effect = setting_values + + return setting_values + + +async def test_setup_all_entries( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_plenticore_client: PlenticoreApiClient, + mock_get_setting_values: list, + entity_registry_enabled_by_default, +): + """Test if all available entries are setup.""" + mock_config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -78,10 +104,16 @@ async def test_setup_all_entries( async def test_setup_no_entries( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_plenticore: MagicMock + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_plenticore_client: PlenticoreApiClient, + mock_get_setting_values: list, + entity_registry_enabled_by_default, ): - """Test that no entries are setup up.""" - mock_plenticore.client.get_settings.return_value = [] + """Test that no entries are setup if Plenticore does not provide data.""" + + mock_plenticore_client.get_settings.return_value = [] + mock_config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -92,106 +124,81 @@ async def test_setup_no_entries( assert ent_reg.async_get("number.scb_battery_min_home_consumption") is None -def test_number_returns_value_if_available( - mock_coordinator: MagicMock, - mock_number_description: PlenticoreNumberEntityDescription, - mock_setting_data: SettingsData, +async def test_number_has_value( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_plenticore_client: PlenticoreApiClient, + mock_get_setting_values: list, + entity_registry_enabled_by_default, ): - """Test if value property on PlenticoreDataNumber returns an int if available.""" + """Test if number has a value if data is provided on update.""" - mock_coordinator.data = {"moduleid": {"dataid": "42"}} + mock_get_setting_values.append({"devices:local": {"Battery:MinSoc": "42"}}) - entity = PlenticoreDataNumber( - mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data - ) + mock_config_entry.add_to_hass(hass) - assert entity.value == 42 - assert type(entity.value) == int + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=3)) + await hass.async_block_till_done() + + state = hass.states.get("number.scb_battery_min_soc") + assert state.state == "42" + assert state.attributes[ATTR_MIN] == 5 + assert state.attributes[ATTR_MAX] == 100 -def test_number_returns_none_if_unavailable( - mock_coordinator: MagicMock, - mock_number_description: PlenticoreNumberEntityDescription, - mock_setting_data: SettingsData, +async def test_number_is_unavailable( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_plenticore_client: PlenticoreApiClient, + mock_get_setting_values: list, + entity_registry_enabled_by_default, ): - """Test if value property on PlenticoreDataNumber returns none if unavailable.""" + """Test if number is unavailable if no data is provided on update.""" - mock_coordinator.data = {} # makes entity not available + mock_config_entry.add_to_hass(hass) - entity = PlenticoreDataNumber( - mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data - ) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() - assert entity.value is None + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=3)) + await hass.async_block_till_done() + + state = hass.states.get("number.scb_battery_min_soc") + assert state.state == STATE_UNAVAILABLE async def test_set_value( - mock_coordinator: MagicMock, - mock_number_description: PlenticoreNumberEntityDescription, - mock_setting_data: SettingsData, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_plenticore_client: PlenticoreApiClient, + mock_get_setting_values: list, + entity_registry_enabled_by_default, ): - """Test if set value calls coordinator with new value.""" + """Test if a new value could be set.""" - entity = PlenticoreDataNumber( - mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data - ) + mock_get_setting_values.append({"devices:local": {"Battery:MinSoc": "42"}}) - await entity.async_set_native_value(42) + mock_config_entry.add_to_hass(hass) - mock_coordinator.async_write_data.assert_called_once_with( - "moduleid", {"dataid": "42"} - ) - mock_coordinator.async_refresh.assert_called_once() + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=3)) + await hass.async_block_till_done() -async def test_minmax_overwrite( - mock_coordinator: MagicMock, - mock_number_description: PlenticoreNumberEntityDescription, -): - """Test if min/max value is overwritten from retrieved settings data.""" - - setting_data = SettingsData( + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, { - "min": "5", - "max": "100", - } + ATTR_ENTITY_ID: "number.scb_battery_min_soc", + ATTR_VALUE: 80, + }, + blocking=True, ) - entity = PlenticoreDataNumber( - mock_coordinator, "42", "scb", None, mock_number_description, setting_data + mock_plenticore_client.set_setting_values.assert_called_once_with( + "devices:local", {"Battery:MinSoc": "80"} ) - - assert entity.min_value == 5 - assert entity.max_value == 100 - - -async def test_added_to_hass( - mock_coordinator: MagicMock, - mock_number_description: PlenticoreNumberEntityDescription, - mock_setting_data: SettingsData, -): - """Test if coordinator starts fetching after added to hass.""" - - entity = PlenticoreDataNumber( - mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data - ) - - await entity.async_added_to_hass() - - mock_coordinator.start_fetch_data.assert_called_once_with("moduleid", "dataid") - - -async def test_remove_from_hass( - mock_coordinator: MagicMock, - mock_number_description: PlenticoreNumberEntityDescription, - mock_setting_data: SettingsData, -): - """Test if coordinator stops fetching after remove from hass.""" - - entity = PlenticoreDataNumber( - mock_coordinator, "42", "scb", None, mock_number_description, mock_setting_data - ) - - await entity.async_will_remove_from_hass() - - mock_coordinator.stop_fetch_data.assert_called_once_with("moduleid", "dataid") From 9f33a0d6dd3c645dbfb3e6cb292d3c6669c55823 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Mon, 18 Jul 2022 17:22:38 -0400 Subject: [PATCH 2667/3516] Migrate Tautulli to new entity naming style (#75382) --- homeassistant/components/tautulli/__init__.py | 2 ++ homeassistant/components/tautulli/sensor.py | 29 +++++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/tautulli/__init__.py b/homeassistant/components/tautulli/__init__.py index af7939a45f5..bd5032014bc 100644 --- a/homeassistant/components/tautulli/__init__.py +++ b/homeassistant/components/tautulli/__init__.py @@ -46,6 +46,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: class TautulliEntity(CoordinatorEntity[TautulliDataUpdateCoordinator]): """Defines a base Tautulli entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: TautulliDataUpdateCoordinator, diff --git a/homeassistant/components/tautulli/sensor.py b/homeassistant/components/tautulli/sensor.py index 1d5efde7cc7..5653aa5dc57 100644 --- a/homeassistant/components/tautulli/sensor.py +++ b/homeassistant/components/tautulli/sensor.py @@ -60,14 +60,14 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( TautulliSensorEntityDescription( icon="mdi:plex", key="watching_count", - name="Tautulli", + name="Watching", native_unit_of_measurement="Watching", value_fn=lambda home_stats, activity, _: cast(int, activity.stream_count), ), TautulliSensorEntityDescription( icon="mdi:plex", key="stream_count_direct_play", - name="Direct Plays", + name="Direct plays", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="Streams", entity_registry_enabled_default=False, @@ -78,7 +78,7 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( TautulliSensorEntityDescription( icon="mdi:plex", key="stream_count_direct_stream", - name="Direct Streams", + name="Direct streams", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement="Streams", entity_registry_enabled_default=False, @@ -99,7 +99,7 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( ), TautulliSensorEntityDescription( key="total_bandwidth", - name="Total Bandwidth", + name="Total bandwidth", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=DATA_KILOBITS, state_class=SensorStateClass.MEASUREMENT, @@ -107,7 +107,7 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( ), TautulliSensorEntityDescription( key="lan_bandwidth", - name="LAN Bandwidth", + name="LAN bandwidth", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=DATA_KILOBITS, entity_registry_enabled_default=False, @@ -116,7 +116,7 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( ), TautulliSensorEntityDescription( key="wan_bandwidth", - name="WAN Bandwidth", + name="WAN bandwidth", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=DATA_KILOBITS, entity_registry_enabled_default=False, @@ -126,21 +126,21 @@ SENSOR_TYPES: tuple[TautulliSensorEntityDescription, ...] = ( TautulliSensorEntityDescription( icon="mdi:movie-open", key="top_movies", - name="Top Movie", + name="Top movie", entity_registry_enabled_default=False, value_fn=get_top_stats, ), TautulliSensorEntityDescription( icon="mdi:television", key="top_tv", - name="Top TV Show", + name="Top TV show", entity_registry_enabled_default=False, value_fn=get_top_stats, ), TautulliSensorEntityDescription( icon="mdi:walk", key=ATTR_TOP_USER, - name="Top User", + name="Top user", entity_registry_enabled_default=False, value_fn=get_top_stats, ), @@ -170,7 +170,7 @@ SESSION_SENSOR_TYPES: tuple[TautulliSessionSensorEntityDescription, ...] = ( ), TautulliSessionSensorEntityDescription( key="full_title", - name="Full Title", + name="Full title", entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.full_title), ), @@ -184,7 +184,7 @@ SESSION_SENSOR_TYPES: tuple[TautulliSessionSensorEntityDescription, ...] = ( ), TautulliSessionSensorEntityDescription( key="stream_resolution", - name="Stream Resolution", + name="Stream resolution", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.stream_video_resolution), @@ -192,21 +192,21 @@ SESSION_SENSOR_TYPES: tuple[TautulliSessionSensorEntityDescription, ...] = ( TautulliSessionSensorEntityDescription( icon="mdi:plex", key="transcode_decision", - name="Transcode Decision", + name="Transcode decision", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.transcode_decision), ), TautulliSessionSensorEntityDescription( key="session_thumb", - name="session Thumbnail", + name="session thumbnail", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.user_thumb), ), TautulliSessionSensorEntityDescription( key="video_resolution", - name="Video Resolution", + name="Video resolution", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda session: cast(str, session.video_resolution), @@ -284,7 +284,6 @@ class TautulliSessionSensor(TautulliEntity, SensorEntity): super().__init__(coordinator, description, user) entry_id = coordinator.config_entry.entry_id self._attr_unique_id = f"{entry_id}_{user.user_id}_{description.key}" - self._attr_name = f"{user.username} {description.name}" @property def native_value(self) -> StateType: From e75d7dfb7515d3d17c4df0aa08336f50d97d6d2b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 18 Jul 2022 23:34:53 +0200 Subject: [PATCH 2668/3516] Update google-cloud-texttospeech to 2.12.0 (#75401) --- homeassistant/components/google_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_cloud/manifest.json b/homeassistant/components/google_cloud/manifest.json index 633c5edc453..d48571c55bd 100644 --- a/homeassistant/components/google_cloud/manifest.json +++ b/homeassistant/components/google_cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "google_cloud", "name": "Google Cloud Platform", "documentation": "https://www.home-assistant.io/integrations/google_cloud", - "requirements": ["google-cloud-texttospeech==2.11.1"], + "requirements": ["google-cloud-texttospeech==2.12.0"], "codeowners": ["@lufton"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 30af50ccc15..4b7743aa53e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -743,7 +743,7 @@ goodwe==0.2.15 google-cloud-pubsub==2.11.0 # homeassistant.components.google_cloud -google-cloud-texttospeech==2.11.1 +google-cloud-texttospeech==2.12.0 # homeassistant.components.nest google-nest-sdm==2.0.0 From 983bcfa9355fb86462bdfde7611dfbf7642aaf82 Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 18 Jul 2022 17:41:06 -0400 Subject: [PATCH 2669/3516] Bump AIOAladdinConnect to 0.1.27 (#75400) --- homeassistant/components/aladdin_connect/cover.py | 7 ++++--- homeassistant/components/aladdin_connect/manifest.json | 2 +- homeassistant/components/aladdin_connect/model.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/aladdin_connect/conftest.py | 1 + tests/components/aladdin_connect/test_cover.py | 5 +++++ 7 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index d06ebebd076..f032fcecbe0 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -89,6 +89,7 @@ class AladdinDevice(CoverEntity): self._device_id = device["device_id"] self._number = device["door_number"] self._name = device["name"] + self._serial = device["serial"] self._attr_unique_id = f"{self._device_id}-{self._number}" self._attr_has_entity_name = True @@ -108,8 +109,8 @@ class AladdinDevice(CoverEntity): """Schedule a state update.""" self.async_write_ha_state() - self._acc.register_callback(update_callback, self._number) - await self._acc.get_doors(self._number) + self._acc.register_callback(update_callback, self._serial) + await self._acc.get_doors(self._serial) async def async_will_remove_from_hass(self) -> None: """Close Aladdin Connect before removing.""" @@ -125,7 +126,7 @@ class AladdinDevice(CoverEntity): async def async_update(self) -> None: """Update status of cover.""" - await self._acc.get_doors(self._number) + await self._acc.get_doors(self._serial) @property def is_closed(self) -> bool | None: diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 5baeba33971..4a04bf69aed 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.25"], + "requirements": ["AIOAladdinConnect==0.1.27"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/homeassistant/components/aladdin_connect/model.py b/homeassistant/components/aladdin_connect/model.py index 4248f3504fe..63624b223a9 100644 --- a/homeassistant/components/aladdin_connect/model.py +++ b/homeassistant/components/aladdin_connect/model.py @@ -11,3 +11,4 @@ class DoorDevice(TypedDict): door_number: int name: str status: str + serial: str diff --git a/requirements_all.txt b/requirements_all.txt index 4b7743aa53e..92e8fa4f9e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.25 +AIOAladdinConnect==0.1.27 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e679fb6827a..5153abbf447 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.25 +AIOAladdinConnect==0.1.27 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py index ee68d207361..c8f7d240ba5 100644 --- a/tests/components/aladdin_connect/conftest.py +++ b/tests/components/aladdin_connect/conftest.py @@ -10,6 +10,7 @@ DEVICE_CONFIG_OPEN = { "name": "home", "status": "open", "link_status": "Connected", + "serial": "12345", } diff --git a/tests/components/aladdin_connect/test_cover.py b/tests/components/aladdin_connect/test_cover.py index 54ec4ee5de1..4e65607fa9d 100644 --- a/tests/components/aladdin_connect/test_cover.py +++ b/tests/components/aladdin_connect/test_cover.py @@ -33,6 +33,7 @@ DEVICE_CONFIG_OPEN = { "name": "home", "status": "open", "link_status": "Connected", + "serial": "12345", } DEVICE_CONFIG_OPENING = { @@ -41,6 +42,7 @@ DEVICE_CONFIG_OPENING = { "name": "home", "status": "opening", "link_status": "Connected", + "serial": "12345", } DEVICE_CONFIG_CLOSED = { @@ -49,6 +51,7 @@ DEVICE_CONFIG_CLOSED = { "name": "home", "status": "closed", "link_status": "Connected", + "serial": "12345", } DEVICE_CONFIG_CLOSING = { @@ -57,6 +60,7 @@ DEVICE_CONFIG_CLOSING = { "name": "home", "status": "closing", "link_status": "Connected", + "serial": "12345", } DEVICE_CONFIG_DISCONNECTED = { @@ -65,6 +69,7 @@ DEVICE_CONFIG_DISCONNECTED = { "name": "home", "status": "open", "link_status": "Disconnected", + "serial": "12345", } DEVICE_CONFIG_BAD = { From 135495297774dbe114ede3989b612b949f160dd3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Jul 2022 17:56:34 -0500 Subject: [PATCH 2670/3516] Migrate LIFX to config entry per device (#74316) --- .coveragerc | 3 - .strict-typing | 1 + CODEOWNERS | 3 +- homeassistant/components/lifx/__init__.py | 195 +++- homeassistant/components/lifx/config_flow.py | 242 ++++- homeassistant/components/lifx/const.py | 16 + homeassistant/components/lifx/coordinator.py | 158 +++ homeassistant/components/lifx/discovery.py | 58 + homeassistant/components/lifx/light.py | 946 ++++------------- homeassistant/components/lifx/manager.py | 216 ++++ homeassistant/components/lifx/manifest.json | 10 +- homeassistant/components/lifx/migration.py | 74 ++ homeassistant/components/lifx/strings.json | 22 +- .../components/lifx/translations/en.json | 24 +- homeassistant/components/lifx/util.py | 161 +++ homeassistant/generated/dhcp.py | 2 + mypy.ini | 11 + requirements_all.txt | 3 + requirements_test_all.txt | 9 + tests/components/lifx/__init__.py | 217 ++++ tests/components/lifx/conftest.py | 57 + tests/components/lifx/test_config_flow.py | 508 +++++++++ tests/components/lifx/test_init.py | 150 +++ tests/components/lifx/test_light.py | 993 ++++++++++++++++++ tests/components/lifx/test_migration.py | 281 +++++ 25 files changed, 3601 insertions(+), 759 deletions(-) create mode 100644 homeassistant/components/lifx/coordinator.py create mode 100644 homeassistant/components/lifx/discovery.py create mode 100644 homeassistant/components/lifx/manager.py create mode 100644 homeassistant/components/lifx/migration.py create mode 100644 homeassistant/components/lifx/util.py create mode 100644 tests/components/lifx/__init__.py create mode 100644 tests/components/lifx/conftest.py create mode 100644 tests/components/lifx/test_config_flow.py create mode 100644 tests/components/lifx/test_init.py create mode 100644 tests/components/lifx/test_light.py create mode 100644 tests/components/lifx/test_migration.py diff --git a/.coveragerc b/.coveragerc index 0fb2210b3cd..3645286980b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -647,9 +647,6 @@ omit = homeassistant/components/life360/const.py homeassistant/components/life360/coordinator.py homeassistant/components/life360/device_tracker.py - homeassistant/components/lifx/__init__.py - homeassistant/components/lifx/const.py - homeassistant/components/lifx/light.py homeassistant/components/lifx_cloud/scene.py homeassistant/components/lightwave/* homeassistant/components/limitlessled/light.py diff --git a/.strict-typing b/.strict-typing index 9792f401ac4..aa911dd81d9 100644 --- a/.strict-typing +++ b/.strict-typing @@ -146,6 +146,7 @@ homeassistant.components.lametric.* homeassistant.components.laundrify.* homeassistant.components.lcn.* homeassistant.components.light.* +homeassistant.components.lifx.* homeassistant.components.local_ip.* homeassistant.components.lock.* homeassistant.components.logbook.* diff --git a/CODEOWNERS b/CODEOWNERS index d2ad6ef2307..fd271e6adc6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -577,7 +577,8 @@ build.json @home-assistant/supervisor /homeassistant/components/lg_netcast/ @Drafteed /homeassistant/components/life360/ @pnbruckner /tests/components/life360/ @pnbruckner -/homeassistant/components/lifx/ @Djelibeybi +/homeassistant/components/lifx/ @bdraco @Djelibeybi +/tests/components/lifx/ @bdraco @Djelibeybi /homeassistant/components/light/ @home-assistant/core /tests/components/light/ @home-assistant/core /homeassistant/components/linux_battery/ @fabaff diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index b6710064a74..8816226ff84 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -1,19 +1,41 @@ """Support for LIFX.""" +from __future__ import annotations + +import asyncio +from collections.abc import Iterable +from datetime import datetime, timedelta +import socket +from typing import Any + +from aiolifx.aiolifx import Light +from aiolifx_connection import LIFXConnection import voluptuous as vol -from homeassistant import config_entries from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PORT, Platform -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_STARTED, + Platform, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_call_later, async_track_time_interval from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import _LOGGER, DATA_LIFX_MANAGER, DOMAIN, TARGET_ANY +from .coordinator import LIFXUpdateCoordinator +from .discovery import async_discover_devices, async_trigger_discovery +from .manager import LIFXManager +from .migration import async_migrate_entities_devices, async_migrate_legacy_entries +from .util import async_entry_is_legacy, async_get_legacy_entry CONF_SERVER = "server" CONF_BROADCAST = "broadcast" + INTERFACE_SCHEMA = vol.Schema( { vol.Optional(CONF_SERVER): cv.string, @@ -22,39 +44,176 @@ INTERFACE_SCHEMA = vol.Schema( } ) -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: {LIGHT_DOMAIN: vol.Schema(vol.All(cv.ensure_list, [INTERFACE_SCHEMA]))}}, - extra=vol.ALLOW_EXTRA, +CONFIG_SCHEMA = vol.All( + cv.deprecated(DOMAIN), + vol.Schema( + { + DOMAIN: { + LIGHT_DOMAIN: vol.Schema(vol.All(cv.ensure_list, [INTERFACE_SCHEMA])) + } + }, + extra=vol.ALLOW_EXTRA, + ), ) -DATA_LIFX_MANAGER = "lifx_manager" PLATFORMS = [Platform.LIGHT] +DISCOVERY_INTERVAL = timedelta(minutes=15) +MIGRATION_INTERVAL = timedelta(minutes=5) + +DISCOVERY_COOLDOWN = 5 + + +async def async_legacy_migration( + hass: HomeAssistant, + legacy_entry: ConfigEntry, + discovered_devices: Iterable[Light], +) -> bool: + """Migrate config entries.""" + existing_serials = { + entry.unique_id + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.unique_id and not async_entry_is_legacy(entry) + } + # device.mac_addr is not the mac_address, its the serial number + hosts_by_serial = {device.mac_addr: device.ip_addr for device in discovered_devices} + missing_discovery_count = await async_migrate_legacy_entries( + hass, hosts_by_serial, existing_serials, legacy_entry + ) + if missing_discovery_count: + _LOGGER.info( + "Migration in progress, waiting to discover %s device(s)", + missing_discovery_count, + ) + return False + + _LOGGER.debug( + "Migration successful, removing legacy entry %s", legacy_entry.entry_id + ) + await hass.config_entries.async_remove(legacy_entry.entry_id) + return True + + +class LIFXDiscoveryManager: + """Manage discovery and migration.""" + + def __init__(self, hass: HomeAssistant, migrating: bool) -> None: + """Init the manager.""" + self.hass = hass + self.lock = asyncio.Lock() + self.migrating = migrating + self._cancel_discovery: CALLBACK_TYPE | None = None + + @callback + def async_setup_discovery_interval(self) -> None: + """Set up discovery at an interval.""" + if self._cancel_discovery: + self._cancel_discovery() + self._cancel_discovery = None + discovery_interval = ( + MIGRATION_INTERVAL if self.migrating else DISCOVERY_INTERVAL + ) + _LOGGER.debug( + "LIFX starting discovery with interval: %s and migrating: %s", + discovery_interval, + self.migrating, + ) + self._cancel_discovery = async_track_time_interval( + self.hass, self.async_discovery, discovery_interval + ) + + async def async_discovery(self, *_: Any) -> None: + """Discovery and migrate LIFX devics.""" + migrating_was_in_progress = self.migrating + + async with self.lock: + discovered = await async_discover_devices(self.hass) + + if legacy_entry := async_get_legacy_entry(self.hass): + migration_complete = await async_legacy_migration( + self.hass, legacy_entry, discovered + ) + if migration_complete and migrating_was_in_progress: + self.migrating = False + _LOGGER.debug( + "LIFX migration complete, switching to normal discovery interval: %s", + DISCOVERY_INTERVAL, + ) + self.async_setup_discovery_interval() + + if discovered: + async_trigger_discovery(self.hass, discovered) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the LIFX component.""" - conf = config.get(DOMAIN) + hass.data[DOMAIN] = {} + migrating = bool(async_get_legacy_entry(hass)) + discovery_manager = LIFXDiscoveryManager(hass, migrating) - hass.data[DOMAIN] = conf or {} + @callback + def _async_delayed_discovery(now: datetime) -> None: + """Start an untracked task to discover devices. - if conf is not None: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} - ) - ) + We do not want the discovery task to block startup. + """ + asyncio.create_task(discovery_manager.async_discovery()) + + # Let the system settle a bit before starting discovery + # to reduce the risk we miss devices because the event + # loop is blocked at startup. + discovery_manager.async_setup_discovery_interval() + async_call_later(hass, DISCOVERY_COOLDOWN, _async_delayed_discovery) + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STARTED, discovery_manager.async_discovery + ) return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up LIFX from a config entry.""" + + if async_entry_is_legacy(entry): + return True + + if legacy_entry := async_get_legacy_entry(hass): + # If the legacy entry still exists, harvest the entities + # that are moving to this config entry. + await async_migrate_entities_devices(hass, legacy_entry.entry_id, entry) + + assert entry.unique_id is not None + domain_data = hass.data[DOMAIN] + if DATA_LIFX_MANAGER not in domain_data: + manager = LIFXManager(hass) + domain_data[DATA_LIFX_MANAGER] = manager + manager.async_setup() + + host = entry.data[CONF_HOST] + connection = LIFXConnection(host, TARGET_ANY) + try: + await connection.async_setup() + except socket.gaierror as ex: + raise ConfigEntryNotReady(f"Could not resolve {host}: {ex}") from ex + coordinator = LIFXUpdateCoordinator(hass, connection, entry.title) + coordinator.async_setup() + await coordinator.async_config_entry_first_refresh() + + domain_data[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - hass.data.pop(DATA_LIFX_MANAGER).cleanup() - return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if async_entry_is_legacy(entry): + return True + domain_data = hass.data[DOMAIN] + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + coordinator: LIFXUpdateCoordinator = domain_data.pop(entry.entry_id) + coordinator.connection.async_stop() + # Only the DATA_LIFX_MANAGER left, remove it. + if len(domain_data) == 1: + manager: LIFXManager = domain_data.pop(DATA_LIFX_MANAGER) + manager.async_unload() + return unload_ok diff --git a/homeassistant/components/lifx/config_flow.py b/homeassistant/components/lifx/config_flow.py index c48bee9e4e7..30b42e640f8 100644 --- a/homeassistant/components/lifx/config_flow.py +++ b/homeassistant/components/lifx/config_flow.py @@ -1,16 +1,240 @@ """Config flow flow LIFX.""" -import aiolifx +from __future__ import annotations -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_entry_flow +import asyncio +import socket +from typing import Any -from .const import DOMAIN +from aiolifx.aiolifx import Light +from aiolifx_connection import LIFXConnection +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import zeroconf +from homeassistant.components.dhcp import DhcpServiceInfo +from homeassistant.const import CONF_DEVICE, CONF_HOST +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import DiscoveryInfoType + +from .const import _LOGGER, CONF_SERIAL, DOMAIN, TARGET_ANY +from .discovery import async_discover_devices +from .util import ( + async_entry_is_legacy, + async_execute_lifx, + async_get_legacy_entry, + formatted_serial, + lifx_features, + mac_matches_serial_number, +) -async def _async_has_devices(hass: HomeAssistant) -> bool: - """Return if there are devices that can be discovered.""" - lifx_ip_addresses = await aiolifx.LifxScan(hass.loop).scan() - return len(lifx_ip_addresses) > 0 +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for tplink.""" + VERSION = 1 -config_entry_flow.register_discovery_flow(DOMAIN, "LIFX", _async_has_devices) + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovered_devices: dict[str, Light] = {} + self._discovered_device: Light | None = None + + async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + """Handle discovery via dhcp.""" + mac = discovery_info.macaddress + host = discovery_info.ip + hass = self.hass + for entry in self._async_current_entries(): + if ( + entry.unique_id + and not async_entry_is_legacy(entry) + and mac_matches_serial_number(mac, entry.unique_id) + ): + if entry.data[CONF_HOST] != host: + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_HOST: host} + ) + hass.async_create_task( + hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") + return await self._async_handle_discovery(host) + + async def async_step_homekit( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: + """Handle HomeKit discovery.""" + return await self._async_handle_discovery(host=discovery_info.host) + + async def async_step_integration_discovery( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: + """Handle discovery.""" + _LOGGER.debug("async_step_integration_discovery %s", discovery_info) + serial = discovery_info[CONF_SERIAL] + host = discovery_info[CONF_HOST] + await self.async_set_unique_id(formatted_serial(serial)) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + return await self._async_handle_discovery(host, serial) + + async def _async_handle_discovery( + self, host: str, serial: str | None = None + ) -> FlowResult: + """Handle any discovery.""" + _LOGGER.debug("Discovery %s %s", host, serial) + self._async_abort_entries_match({CONF_HOST: host}) + self.context[CONF_HOST] = host + if any( + progress.get("context", {}).get(CONF_HOST) == host + for progress in self._async_in_progress() + ): + return self.async_abort(reason="already_in_progress") + if not ( + device := await self._async_try_connect( + host, serial=serial, raise_on_progress=True + ) + ): + return self.async_abort(reason="cannot_connect") + self._discovered_device = device + return await self.async_step_discovery_confirm() + + @callback + def _async_discovered_pending_migration(self) -> bool: + """Check if a discovered device is pending migration.""" + assert self.unique_id is not None + if not (legacy_entry := async_get_legacy_entry(self.hass)): + return False + device_registry = dr.async_get(self.hass) + existing_device = device_registry.async_get_device( + identifiers={(DOMAIN, self.unique_id)} + ) + return bool( + existing_device is not None + and legacy_entry.entry_id in existing_device.config_entries + ) + + async def async_step_discovery_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + _LOGGER.debug( + "Confirming discovery: %s with serial %s", + self._discovered_device.label, + self.unique_id, + ) + if user_input is not None or self._async_discovered_pending_migration(): + return self._async_create_entry_from_device(self._discovered_device) + + self._set_confirm_only() + placeholders = { + "label": self._discovered_device.label, + "host": self._discovered_device.ip_addr, + "serial": self.unique_id, + } + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="discovery_confirm", description_placeholders=placeholders + ) + + 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: + host = user_input[CONF_HOST] + if not host: + return await self.async_step_pick_device() + if ( + device := await self._async_try_connect(host, raise_on_progress=False) + ) is None: + errors["base"] = "cannot_connect" + else: + return self._async_create_entry_from_device(device) + + 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: + serial = user_input[CONF_DEVICE] + await self.async_set_unique_id(serial, raise_on_progress=False) + device_without_label = self._discovered_devices[serial] + device = await self._async_try_connect( + device_without_label.ip_addr, raise_on_progress=False + ) + if not device: + return self.async_abort(reason="cannot_connect") + return self._async_create_entry_from_device(device) + + configured_serials: set[str] = set() + configured_hosts: set[str] = set() + for entry in self._async_current_entries(): + if entry.unique_id and not async_entry_is_legacy(entry): + configured_serials.add(entry.unique_id) + configured_hosts.add(entry.data[CONF_HOST]) + self._discovered_devices = { + # device.mac_addr is not the mac_address, its the serial number + device.mac_addr: device + for device in await async_discover_devices(self.hass) + } + devices_name = { + serial: f"{serial} ({device.ip_addr})" + for serial, device in self._discovered_devices.items() + if serial not in configured_serials + and device.ip_addr not in configured_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)}), + ) + + @callback + def _async_create_entry_from_device(self, device: Light) -> FlowResult: + """Create a config entry from a smart device.""" + self._abort_if_unique_id_configured(updates={CONF_HOST: device.ip_addr}) + return self.async_create_entry( + title=device.label, + data={CONF_HOST: device.ip_addr}, + ) + + async def _async_try_connect( + self, host: str, serial: str | None = None, raise_on_progress: bool = True + ) -> Light | None: + """Try to connect.""" + self._async_abort_entries_match({CONF_HOST: host}) + connection = LIFXConnection(host, TARGET_ANY) + try: + await connection.async_setup() + except socket.gaierror: + return None + device: Light = connection.device + device.get_hostfirmware() + try: + message = await async_execute_lifx(device.get_color) + except asyncio.TimeoutError: + return None + finally: + connection.async_stop() + if ( + lifx_features(device)["relays"] is True + or device.host_firmware_version is None + ): + return None # relays not supported + # device.mac_addr is not the mac_address, its the serial number + device.mac_addr = serial or message.target_addr + await self.async_set_unique_id( + formatted_serial(device.mac_addr), raise_on_progress=raise_on_progress + ) + return device diff --git a/homeassistant/components/lifx/const.py b/homeassistant/components/lifx/const.py index 8628527c428..ec756c2091f 100644 --- a/homeassistant/components/lifx/const.py +++ b/homeassistant/components/lifx/const.py @@ -1,3 +1,19 @@ """Const for LIFX.""" +import logging + DOMAIN = "lifx" + +TARGET_ANY = "00:00:00:00:00:00" + +DISCOVERY_INTERVAL = 10 +MESSAGE_TIMEOUT = 1.65 +MESSAGE_RETRIES = 5 +OVERALL_TIMEOUT = 9 +UNAVAILABLE_GRACE = 90 + +CONF_SERIAL = "serial" + +DATA_LIFX_MANAGER = "lifx_manager" + +_LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py new file mode 100644 index 00000000000..87ba46e94d1 --- /dev/null +++ b/homeassistant/components/lifx/coordinator.py @@ -0,0 +1,158 @@ +"""Coordinator for lifx.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +from functools import partial +from typing import cast + +from aiolifx.aiolifx import Light +from aiolifx_connection import LIFXConnection + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + _LOGGER, + MESSAGE_RETRIES, + MESSAGE_TIMEOUT, + TARGET_ANY, + UNAVAILABLE_GRACE, +) +from .util import async_execute_lifx, get_real_mac_addr, lifx_features + +REQUEST_REFRESH_DELAY = 0.35 + + +class LIFXUpdateCoordinator(DataUpdateCoordinator): + """DataUpdateCoordinator to gather data for a specific lifx device.""" + + def __init__( + self, + hass: HomeAssistant, + connection: LIFXConnection, + title: str, + ) -> None: + """Initialize DataUpdateCoordinator.""" + assert connection.device is not None + self.connection = connection + self.device: Light = connection.device + self.lock = asyncio.Lock() + update_interval = timedelta(seconds=10) + super().__init__( + hass, + _LOGGER, + name=f"{title} ({self.device.ip_addr})", + 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 + ), + ) + + @callback + def async_setup(self) -> None: + """Change timeouts.""" + self.device.timeout = MESSAGE_TIMEOUT + self.device.retry_count = MESSAGE_RETRIES + self.device.unregister_timeout = UNAVAILABLE_GRACE + + @property + def serial_number(self) -> str: + """Return the internal mac address.""" + return cast( + str, self.device.mac_addr + ) # device.mac_addr is not the mac_address, its the serial number + + @property + def mac_address(self) -> str: + """Return the physical mac address.""" + return get_real_mac_addr( + # device.mac_addr is not the mac_address, its the serial number + self.device.mac_addr, + self.device.host_firmware_version, + ) + + async def _async_update_data(self) -> None: + """Fetch all device data from the api.""" + async with self.lock: + if self.device.host_firmware_version is None: + self.device.get_hostfirmware() + if self.device.product is None: + self.device.get_version() + try: + response = await async_execute_lifx(self.device.get_color) + except asyncio.TimeoutError as ex: + raise UpdateFailed( + f"Failed to fetch state from device: {self.device.ip_addr}" + ) from ex + if self.device.product is None: + raise UpdateFailed( + f"Failed to fetch get version from device: {self.device.ip_addr}" + ) + # device.mac_addr is not the mac_address, its the serial number + if self.device.mac_addr == TARGET_ANY: + self.device.mac_addr = response.target_addr + if lifx_features(self.device)["multizone"]: + try: + await self.async_update_color_zones() + except asyncio.TimeoutError as ex: + raise UpdateFailed( + f"Failed to fetch zones from device: {self.device.ip_addr}" + ) from ex + + async def async_update_color_zones(self) -> None: + """Get updated color information for each zone.""" + zone = 0 + top = 1 + while zone < top: + # Each get_color_zones can update 8 zones at once + resp = await async_execute_lifx( + partial(self.device.get_color_zones, start_index=zone) + ) + zone += 8 + top = resp.count + + # We only await multizone responses so don't ask for just one + if zone == top - 1: + zone -= 1 + + async def async_get_color(self) -> None: + """Send a get color message to the device.""" + await async_execute_lifx(self.device.get_color) + + async def async_set_power(self, state: bool, duration: int | None) -> None: + """Send a set power message to the device.""" + await async_execute_lifx( + partial(self.device.set_power, state, duration=duration) + ) + + async def async_set_color( + self, hsbk: list[float | int | None], duration: int | None + ) -> None: + """Send a set color message to the device.""" + await async_execute_lifx( + partial(self.device.set_color, hsbk, duration=duration) + ) + + async def async_set_color_zones( + self, + start_index: int, + end_index: int, + hsbk: list[float | int | None], + duration: int | None, + apply: int, + ) -> None: + """Send a set color zones message to the device.""" + await async_execute_lifx( + partial( + self.device.set_color_zones, + start_index=start_index, + end_index=end_index, + color=hsbk, + duration=duration, + apply=apply, + ) + ) diff --git a/homeassistant/components/lifx/discovery.py b/homeassistant/components/lifx/discovery.py new file mode 100644 index 00000000000..1c6e9ab3060 --- /dev/null +++ b/homeassistant/components/lifx/discovery.py @@ -0,0 +1,58 @@ +"""The lifx integration discovery.""" +from __future__ import annotations + +import asyncio +from collections.abc import Iterable + +from aiolifx.aiolifx import LifxDiscovery, Light, ScanManager + +from homeassistant import config_entries +from homeassistant.components import network +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant, callback + +from .const import CONF_SERIAL, DOMAIN + +DEFAULT_TIMEOUT = 8.5 + + +async def async_discover_devices(hass: HomeAssistant) -> Iterable[Light]: + """Discover lifx devices.""" + all_lights: dict[str, Light] = {} + broadcast_addrs = await network.async_get_ipv4_broadcast_addresses(hass) + discoveries = [] + for address in broadcast_addrs: + manager = ScanManager(str(address)) + lifx_discovery = LifxDiscovery(hass.loop, manager, broadcast_ip=str(address)) + discoveries.append(lifx_discovery) + lifx_discovery.start() + + await asyncio.sleep(DEFAULT_TIMEOUT) + for discovery in discoveries: + all_lights.update(discovery.lights) + discovery.cleanup() + + return all_lights.values() + + +@callback +def async_init_discovery_flow(hass: HomeAssistant, host: str, serial: str) -> None: + """Start discovery of devices.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={CONF_HOST: host, CONF_SERIAL: serial}, + ) + ) + + +@callback +def async_trigger_discovery( + hass: HomeAssistant, + discovered_devices: Iterable[Light], +) -> None: + """Trigger config flows for discovered devices.""" + for device in discovered_devices: + # device.mac_addr is not the mac_address, its the serial number + async_init_discovery_flow(hass, device.ip_addr, device.mac_addr) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 28390e5c02a..28a678d5e8f 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -2,86 +2,56 @@ from __future__ import annotations import asyncio -from dataclasses import dataclass -from datetime import timedelta -from functools import partial -from ipaddress import IPv4Address -import logging +from datetime import datetime, timedelta import math +from typing import Any -import aiolifx as aiolifx_module -from aiolifx.aiolifx import LifxDiscovery, Light +from aiolifx import products import aiolifx_effects as aiolifx_effects_module -from awesomeversion import AwesomeVersion import voluptuous as vol from homeassistant import util -from homeassistant.components import network from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT, - ATTR_COLOR_NAME, - ATTR_COLOR_TEMP, ATTR_EFFECT, - ATTR_HS_COLOR, - ATTR_KELVIN, - ATTR_RGB_COLOR, ATTR_TRANSITION, - ATTR_XY_COLOR, - COLOR_GROUP, - DOMAIN, LIGHT_TURN_ON_SCHEMA, - VALID_BRIGHTNESS, - VALID_BRIGHTNESS_PCT, ColorMode, LightEntity, LightEntityFeature, - preprocess_turn_on_alternatives, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_MODE, - ATTR_MODEL, - ATTR_SW_VERSION, - EVENT_HOMEASSISTANT_STOP, -) -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODEL, ATTR_SW_VERSION +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.entity_platform import AddEntitiesCallback, EntityPlatform -import homeassistant.helpers.entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import CoordinatorEntity import homeassistant.util.color as color_util -from . import ( - CONF_BROADCAST, - CONF_PORT, - CONF_SERVER, - DATA_LIFX_MANAGER, - DOMAIN as LIFX_DOMAIN, +from .const import DATA_LIFX_MANAGER, DOMAIN +from .coordinator import LIFXUpdateCoordinator +from .manager import ( + SERVICE_EFFECT_COLORLOOP, + SERVICE_EFFECT_PULSE, + SERVICE_EFFECT_STOP, + LIFXManager, ) - -_LOGGER = logging.getLogger(__name__) - -SCAN_INTERVAL = timedelta(seconds=10) - -DISCOVERY_INTERVAL = 10 -MESSAGE_TIMEOUT = 1 -MESSAGE_RETRIES = 8 -UNAVAILABLE_GRACE = 90 - -FIX_MAC_FW = AwesomeVersion("3.70") +from .util import convert_8_to_16, convert_16_to_8, find_hsbk, lifx_features, merge_hsbk SERVICE_LIFX_SET_STATE = "set_state" +COLOR_ZONE_POPULATE_DELAY = 0.3 + ATTR_INFRARED = "infrared" ATTR_ZONES = "zones" ATTR_POWER = "power" +SERVICE_LIFX_SET_STATE = "set_state" + LIFX_SET_STATE_SCHEMA = cv.make_entity_service_schema( { **LIGHT_TURN_ON_SCHEMA, @@ -91,645 +61,152 @@ LIFX_SET_STATE_SCHEMA = cv.make_entity_service_schema( } ) -SERVICE_EFFECT_PULSE = "effect_pulse" -SERVICE_EFFECT_COLORLOOP = "effect_colorloop" -SERVICE_EFFECT_STOP = "effect_stop" - -ATTR_POWER_ON = "power_on" -ATTR_PERIOD = "period" -ATTR_CYCLES = "cycles" -ATTR_SPREAD = "spread" -ATTR_CHANGE = "change" - -PULSE_MODE_BLINK = "blink" -PULSE_MODE_BREATHE = "breathe" -PULSE_MODE_PING = "ping" -PULSE_MODE_STROBE = "strobe" -PULSE_MODE_SOLID = "solid" - -PULSE_MODES = [ - PULSE_MODE_BLINK, - PULSE_MODE_BREATHE, - PULSE_MODE_PING, - PULSE_MODE_STROBE, - PULSE_MODE_SOLID, -] - -LIFX_EFFECT_SCHEMA = { - vol.Optional(ATTR_POWER_ON, default=True): cv.boolean, -} - -LIFX_EFFECT_PULSE_SCHEMA = cv.make_entity_service_schema( - { - **LIFX_EFFECT_SCHEMA, - ATTR_BRIGHTNESS: VALID_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, - vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, - vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( - vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)) - ), - vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( - vol.Coerce(tuple), vol.ExactSequence((cv.small_float, cv.small_float)) - ), - vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( - vol.Coerce(tuple), - vol.ExactSequence( - ( - vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), - vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), - ) - ), - ), - vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All( - vol.Coerce(int), vol.Range(min=1) - ), - vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): cv.positive_int, - ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)), - ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)), - ATTR_MODE: vol.In(PULSE_MODES), - } -) - -LIFX_EFFECT_COLORLOOP_SCHEMA = cv.make_entity_service_schema( - { - **LIFX_EFFECT_SCHEMA, - ATTR_BRIGHTNESS: VALID_BRIGHTNESS, - ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, - ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Clamp(min=0.05)), - ATTR_CHANGE: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), - ATTR_SPREAD: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), - ATTR_TRANSITION: cv.positive_float, - } -) - -LIFX_EFFECT_STOP_SCHEMA = cv.make_entity_service_schema({}) - - -def aiolifx(): - """Return the aiolifx module.""" - return aiolifx_module - - -def aiolifx_effects(): - """Return the aiolifx_effects module.""" - return aiolifx_effects_module - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the LIFX light platform. Obsolete.""" - _LOGGER.warning("LIFX no longer works with light platform configuration") +HSBK_HUE = 0 +HSBK_SATURATION = 1 +HSBK_BRIGHTNESS = 2 +HSBK_KELVIN = 3 async def async_setup_entry( hass: HomeAssistant, - config_entry: ConfigEntry, + entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: """Set up LIFX from a config entry.""" - # Priority 1: manual config - if not (interfaces := hass.data[LIFX_DOMAIN].get(DOMAIN)): - # Priority 2: Home Assistant enabled interfaces - ip_addresses = ( - source_ip - for source_ip in await network.async_get_enabled_source_ips(hass) - if isinstance(source_ip, IPv4Address) and not source_ip.is_loopback - ) - interfaces = [{CONF_SERVER: str(ip)} for ip in ip_addresses] - + domain_data = hass.data[DOMAIN] + coordinator: LIFXUpdateCoordinator = domain_data[entry.entry_id] + manager: LIFXManager = domain_data[DATA_LIFX_MANAGER] + device = coordinator.device platform = entity_platform.async_get_current_platform() - lifx_manager = LIFXManager(hass, platform, config_entry, async_add_entities) - hass.data[DATA_LIFX_MANAGER] = lifx_manager - - for interface in interfaces: - lifx_manager.start_discovery(interface) - - -def lifx_features(bulb): - """Return a feature map for this bulb, or a default map if unknown.""" - return aiolifx().products.features_map.get( - bulb.product - ) or aiolifx().products.features_map.get(1) - - -def find_hsbk(hass, **kwargs): - """Find the desired color from a number of possible inputs.""" - hue, saturation, brightness, kelvin = [None] * 4 - - preprocess_turn_on_alternatives(hass, kwargs) - - if ATTR_HS_COLOR in kwargs: - hue, saturation = kwargs[ATTR_HS_COLOR] - elif ATTR_RGB_COLOR in kwargs: - hue, saturation = color_util.color_RGB_to_hs(*kwargs[ATTR_RGB_COLOR]) - elif ATTR_XY_COLOR in kwargs: - hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR]) - - if hue is not None: - hue = int(hue / 360 * 65535) - saturation = int(saturation / 100 * 65535) - kelvin = 3500 - - if ATTR_COLOR_TEMP in kwargs: - kelvin = int( - color_util.color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) - ) - saturation = 0 - - if ATTR_BRIGHTNESS in kwargs: - brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) - - hsbk = [hue, saturation, brightness, kelvin] - return None if hsbk == [None] * 4 else hsbk - - -def merge_hsbk(base, change): - """Copy change on top of base, except when None.""" - if change is None: - return None - return [b if c is None else c for b, c in zip(base, change)] - - -@dataclass -class InFlightDiscovery: - """Represent a LIFX device that is being discovered.""" - - device: Light - lock: asyncio.Lock - - -class LIFXManager: - """Representation of all known LIFX entities.""" - - def __init__( - self, - hass: HomeAssistant, - platform: EntityPlatform, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, - ) -> None: - """Initialize the light.""" - self.entities: dict[str, LIFXLight] = {} - self.switch_devices: list[str] = [] - self.hass = hass - self.platform = platform - self.config_entry = config_entry - self.async_add_entities = async_add_entities - self.effects_conductor = aiolifx_effects().Conductor(hass.loop) - self.discoveries: list[LifxDiscovery] = [] - self.discoveries_inflight: dict[str, InFlightDiscovery] = {} - self.cleanup_unsub = self.hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, self.cleanup - ) - self.entity_registry_updated_unsub = self.hass.bus.async_listen( - er.EVENT_ENTITY_REGISTRY_UPDATED, self.entity_registry_updated - ) - - self.register_set_state() - self.register_effects() - - def start_discovery(self, interface): - """Start discovery on a network interface.""" - kwargs = {"discovery_interval": DISCOVERY_INTERVAL} - if broadcast_ip := interface.get(CONF_BROADCAST): - kwargs["broadcast_ip"] = broadcast_ip - lifx_discovery = aiolifx().LifxDiscovery(self.hass.loop, self, **kwargs) - - kwargs = {} - if listen_ip := interface.get(CONF_SERVER): - kwargs["listen_ip"] = listen_ip - if listen_port := interface.get(CONF_PORT): - kwargs["listen_port"] = listen_port - lifx_discovery.start(**kwargs) - - self.discoveries.append(lifx_discovery) - - @callback - def cleanup(self, event=None): - """Release resources.""" - self.cleanup_unsub() - self.entity_registry_updated_unsub() - - for discovery in self.discoveries: - discovery.cleanup() - - for service in ( - SERVICE_LIFX_SET_STATE, - SERVICE_EFFECT_STOP, - SERVICE_EFFECT_PULSE, - SERVICE_EFFECT_COLORLOOP, - ): - self.hass.services.async_remove(LIFX_DOMAIN, service) - - def register_set_state(self): - """Register the LIFX set_state service call.""" - self.platform.async_register_entity_service( - SERVICE_LIFX_SET_STATE, LIFX_SET_STATE_SCHEMA, "set_state" - ) - - def register_effects(self): - """Register the LIFX effects as hass service calls.""" - - async def service_handler(service: ServiceCall) -> None: - """Apply a service, i.e. start an effect.""" - entities = await self.platform.async_extract_from_service(service) - if entities: - await self.start_effect(entities, service.service, **service.data) - - self.hass.services.async_register( - LIFX_DOMAIN, - SERVICE_EFFECT_PULSE, - service_handler, - schema=LIFX_EFFECT_PULSE_SCHEMA, - ) - - self.hass.services.async_register( - LIFX_DOMAIN, - SERVICE_EFFECT_COLORLOOP, - service_handler, - schema=LIFX_EFFECT_COLORLOOP_SCHEMA, - ) - - self.hass.services.async_register( - LIFX_DOMAIN, - SERVICE_EFFECT_STOP, - service_handler, - schema=LIFX_EFFECT_STOP_SCHEMA, - ) - - async def start_effect(self, entities, service, **kwargs): - """Start a light effect on entities.""" - bulbs = [light.bulb for light in entities] - - if service == SERVICE_EFFECT_PULSE: - effect = aiolifx_effects().EffectPulse( - power_on=kwargs.get(ATTR_POWER_ON), - period=kwargs.get(ATTR_PERIOD), - cycles=kwargs.get(ATTR_CYCLES), - mode=kwargs.get(ATTR_MODE), - hsbk=find_hsbk(self.hass, **kwargs), - ) - await self.effects_conductor.start(effect, bulbs) - elif service == SERVICE_EFFECT_COLORLOOP: - preprocess_turn_on_alternatives(self.hass, kwargs) - - brightness = None - if ATTR_BRIGHTNESS in kwargs: - brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) - - effect = aiolifx_effects().EffectColorloop( - power_on=kwargs.get(ATTR_POWER_ON), - period=kwargs.get(ATTR_PERIOD), - change=kwargs.get(ATTR_CHANGE), - spread=kwargs.get(ATTR_SPREAD), - transition=kwargs.get(ATTR_TRANSITION), - brightness=brightness, - ) - await self.effects_conductor.start(effect, bulbs) - elif service == SERVICE_EFFECT_STOP: - await self.effects_conductor.stop(bulbs) - - def clear_inflight_discovery(self, inflight: InFlightDiscovery) -> None: - """Clear in-flight discovery.""" - self.discoveries_inflight.pop(inflight.device.mac_addr, None) - - @callback - def register(self, bulb: Light) -> None: - """Allow a single in-flight discovery per bulb.""" - if bulb.mac_addr in self.switch_devices: - _LOGGER.debug( - "Skipping discovered LIFX Switch at %s (%s)", - bulb.ip_addr, - bulb.mac_addr, - ) - return - - # Try to bail out of discovery as early as possible - if bulb.mac_addr in self.entities: - entity = self.entities[bulb.mac_addr] - entity.registered = True - _LOGGER.debug("Reconnected to %s", entity.who) - return - - if bulb.mac_addr not in self.discoveries_inflight: - inflight = InFlightDiscovery(bulb, asyncio.Lock()) - self.discoveries_inflight[bulb.mac_addr] = inflight - _LOGGER.debug( - "First discovery response received from %s (%s)", - bulb.ip_addr, - bulb.mac_addr, - ) - else: - _LOGGER.debug( - "Duplicate discovery response received from %s (%s)", - bulb.ip_addr, - bulb.mac_addr, - ) - - self.hass.async_create_task( - self._async_handle_discovery(self.discoveries_inflight[bulb.mac_addr]) - ) - - async def _async_handle_discovery(self, inflight: InFlightDiscovery) -> None: - """Handle LIFX bulb registration lifecycle.""" - - # only allow a single discovery process per discovered device - async with inflight.lock: - - # Bail out if an entity was created by a previous discovery while - # this discovery was waiting for the asyncio lock to release. - if inflight.device.mac_addr in self.entities: - self.clear_inflight_discovery(inflight) - entity: LIFXLight = self.entities[inflight.device.mac_addr] - entity.registered = True - _LOGGER.debug("Reconnected to %s", entity.who) - return - - # Determine the product info so that LIFX Switches - # can be skipped. - ack = AwaitAioLIFX().wait - - if inflight.device.product is None: - if await ack(inflight.device.get_version) is None: - _LOGGER.debug( - "Failed to discover product information for %s (%s)", - inflight.device.ip_addr, - inflight.device.mac_addr, - ) - self.clear_inflight_discovery(inflight) - return - - if lifx_features(inflight.device)["relays"] is True: - _LOGGER.debug( - "Skipping discovered LIFX Switch at %s (%s)", - inflight.device.ip_addr, - inflight.device.mac_addr, - ) - self.switch_devices.append(inflight.device.mac_addr) - self.clear_inflight_discovery(inflight) - return - - await self._async_process_discovery(inflight=inflight) - - async def _async_process_discovery(self, inflight: InFlightDiscovery) -> None: - """Process discovery of a device.""" - bulb = inflight.device - ack = AwaitAioLIFX().wait - - bulb.timeout = MESSAGE_TIMEOUT - bulb.retry_count = MESSAGE_RETRIES - bulb.unregister_timeout = UNAVAILABLE_GRACE - - # Read initial state - if bulb.color is None: - if await ack(bulb.get_color) is None: - _LOGGER.debug( - "Failed to determine current state of %s (%s)", - bulb.ip_addr, - bulb.mac_addr, - ) - self.clear_inflight_discovery(inflight) - return - - if lifx_features(bulb)["multizone"]: - entity: LIFXLight = LIFXStrip(bulb.mac_addr, bulb, self.effects_conductor) - elif lifx_features(bulb)["color"]: - entity = LIFXColor(bulb.mac_addr, bulb, self.effects_conductor) - else: - entity = LIFXWhite(bulb.mac_addr, bulb, self.effects_conductor) - - self.entities[bulb.mac_addr] = entity - self.async_add_entities([entity], True) - _LOGGER.debug("Entity created for %s", entity.who) - self.clear_inflight_discovery(inflight) - - @callback - def unregister(self, bulb: Light) -> None: - """Mark unresponsive bulbs as unavailable in Home Assistant.""" - if bulb.mac_addr in self.entities: - entity = self.entities[bulb.mac_addr] - entity.registered = False - entity.async_write_ha_state() - _LOGGER.debug("Disconnected from %s", entity.who) - - @callback - def entity_registry_updated(self, event): - """Handle entity registry updated.""" - if event.data["action"] == "remove": - self.remove_empty_devices() - - def remove_empty_devices(self): - """Remove devices with no entities.""" - entity_reg = er.async_get(self.hass) - device_reg = dr.async_get(self.hass) - device_list = dr.async_entries_for_config_entry( - device_reg, self.config_entry.entry_id - ) - for device_entry in device_list: - if not er.async_entries_for_device( - entity_reg, - device_entry.id, - include_disabled_entities=True, - ): - device_reg.async_update_device( - device_entry.id, remove_config_entry_id=self.config_entry.entry_id - ) - - -class AwaitAioLIFX: - """Wait for an aiolifx callback and return the message.""" - - def __init__(self): - """Initialize the wrapper.""" - self.message = None - self.event = asyncio.Event() - - @callback - def callback(self, bulb, message): - """Handle responses.""" - self.message = message - self.event.set() - - async def wait(self, method): - """Call an aiolifx method and wait for its response.""" - self.message = None - self.event.clear() - method(callb=self.callback) - - await self.event.wait() - return self.message - - -def convert_8_to_16(value): - """Scale an 8 bit level into 16 bits.""" - return (value << 8) | value - - -def convert_16_to_8(value): - """Scale a 16 bit level into 8 bits.""" - return value >> 8 - - -class LIFXLight(LightEntity): + platform.async_register_entity_service( + SERVICE_LIFX_SET_STATE, + LIFX_SET_STATE_SCHEMA, + "set_state", + ) + if lifx_features(device)["multizone"]: + entity: LIFXLight = LIFXStrip(coordinator, manager, entry) + elif lifx_features(device)["color"]: + entity = LIFXColor(coordinator, manager, entry) + else: + entity = LIFXWhite(coordinator, manager, entry) + async_add_entities([entity]) + + +class LIFXLight(CoordinatorEntity[LIFXUpdateCoordinator], LightEntity): """Representation of a LIFX light.""" _attr_supported_features = LightEntityFeature.TRANSITION | LightEntityFeature.EFFECT def __init__( self, - mac_addr: str, - bulb: Light, - effects_conductor: aiolifx_effects_module.Conductor, + coordinator: LIFXUpdateCoordinator, + manager: LIFXManager, + entry: ConfigEntry, ) -> None: """Initialize the light.""" - self.mac_addr = mac_addr + super().__init__(coordinator) + bulb = coordinator.device + self.mac_addr = bulb.mac_addr self.bulb = bulb - self.effects_conductor = effects_conductor - self.registered = True - self.postponed_update = None - self.lock = asyncio.Lock() - - def get_mac_addr(self): - """Increment the last byte of the mac address by one for FW>3.70.""" - if ( - self.bulb.host_firmware_version - and AwesomeVersion(self.bulb.host_firmware_version) >= FIX_MAC_FW - ): - octets = [int(octet, 16) for octet in self.mac_addr.split(":")] - octets[5] = (octets[5] + 1) % 256 - return ":".join(f"{octet:02x}" for octet in octets) - return self.mac_addr - - @property - def device_info(self) -> DeviceInfo: - """Return information about the device.""" - _map = aiolifx().products.product_map - + bulb_features = lifx_features(bulb) + self.manager = manager + self.effects_conductor: aiolifx_effects_module.Conductor = ( + manager.effects_conductor + ) + self.postponed_update: CALLBACK_TYPE | None = None + self.entry = entry + self._attr_unique_id = self.coordinator.serial_number + self._attr_name = bulb.label + self._attr_min_mireds = math.floor( + color_util.color_temperature_kelvin_to_mired(bulb_features["max_kelvin"]) + ) + self._attr_max_mireds = math.ceil( + color_util.color_temperature_kelvin_to_mired(bulb_features["min_kelvin"]) + ) info = DeviceInfo( - identifiers={(LIFX_DOMAIN, self.unique_id)}, - connections={(dr.CONNECTION_NETWORK_MAC, self.get_mac_addr())}, + identifiers={(DOMAIN, coordinator.serial_number)}, + connections={(dr.CONNECTION_NETWORK_MAC, coordinator.mac_address)}, manufacturer="LIFX", name=self.name, ) - - if (model := (_map.get(self.bulb.product) or self.bulb.product)) is not None: + _map = products.product_map + if (model := (_map.get(bulb.product) or bulb.product)) is not None: info[ATTR_MODEL] = str(model) - if (version := self.bulb.host_firmware_version) is not None: + if (version := bulb.host_firmware_version) is not None: info[ATTR_SW_VERSION] = version - - return info - - @property - def available(self): - """Return the availability of the bulb.""" - return self.registered - - @property - def unique_id(self): - """Return a unique ID.""" - return self.mac_addr - - @property - def name(self): - """Return the name of the bulb.""" - return self.bulb.label - - @property - def who(self): - """Return a string identifying the bulb by name and mac.""" - return f"{self.name} ({self.mac_addr})" - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - kelvin = lifx_features(self.bulb)["max_kelvin"] - return math.floor(color_util.color_temperature_kelvin_to_mired(kelvin)) - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - kelvin = lifx_features(self.bulb)["min_kelvin"] - return math.ceil(color_util.color_temperature_kelvin_to_mired(kelvin)) - - @property - def color_mode(self) -> ColorMode: - """Return the color mode of the light.""" - bulb_features = lifx_features(self.bulb) + self._attr_device_info = info if bulb_features["min_kelvin"] != bulb_features["max_kelvin"]: - return ColorMode.COLOR_TEMP - return ColorMode.BRIGHTNESS + color_mode = ColorMode.COLOR_TEMP + else: + color_mode = ColorMode.BRIGHTNESS + self._attr_color_mode = color_mode + self._attr_supported_color_modes = {color_mode} @property - def supported_color_modes(self) -> set[ColorMode]: - """Flag supported color modes.""" - return {self.color_mode} - - @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of this light between 0..255.""" fade = self.bulb.power_level / 65535 - return convert_16_to_8(int(fade * self.bulb.color[2])) + return convert_16_to_8(int(fade * self.bulb.color[HSBK_BRIGHTNESS])) @property - def color_temp(self): + def color_temp(self) -> int | None: """Return the color temperature.""" - _, sat, _, kelvin = self.bulb.color - if sat: - return None - return color_util.color_temperature_kelvin_to_mired(kelvin) + return color_util.color_temperature_kelvin_to_mired( + self.bulb.color[HSBK_KELVIN] + ) @property - def is_on(self): + def is_on(self) -> bool: """Return true if light is on.""" - return self.bulb.power_level != 0 + return bool(self.bulb.power_level != 0) @property - def effect(self): + def effect(self) -> str | None: """Return the name of the currently running effect.""" - effect = self.effects_conductor.effect(self.bulb) - if effect: - return f"lifx_effect_{effect.name}" + if effect := self.effects_conductor.effect(self.bulb): + return f"effect_{effect.name}" return None - async def update_hass(self, now=None): - """Request new status and push it to hass.""" - self.postponed_update = None - await self.async_update() - self.async_write_ha_state() - - async def update_during_transition(self, when): + async def update_during_transition(self, when: int) -> None: """Update state at the start and end of a transition.""" if self.postponed_update: self.postponed_update() + self.postponed_update = None # Transition has started - await self.update_hass() + self.async_write_ha_state() + + # The state reply we get back may be stale so we also request + # a refresh to get a fresh state + # https://lan.developer.lifx.com/docs/changing-a-device + await self.coordinator.async_request_refresh() # Transition has ended if when > 0: + + async def _async_refresh(now: datetime) -> None: + """Refresh the state.""" + await self.coordinator.async_refresh() + self.postponed_update = async_track_point_in_utc_time( self.hass, - self.update_hass, + _async_refresh, util.dt.utcnow() + timedelta(milliseconds=when), ) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" - kwargs[ATTR_POWER] = True - self.hass.async_create_task(self.set_state(**kwargs)) + await self.set_state(**{**kwargs, ATTR_POWER: True}) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" - kwargs[ATTR_POWER] = False - self.hass.async_create_task(self.set_state(**kwargs)) + await self.set_state(**{**kwargs, ATTR_POWER: False}) - async def set_state(self, **kwargs): + async def set_state(self, **kwargs: Any) -> None: """Set a color on the light and turn it on/off.""" - async with self.lock: + self.coordinator.async_set_updated_data(None) + async with self.coordinator.lock: + # Cancel any pending refreshes bulb = self.bulb await self.effects_conductor.stop([bulb]) @@ -752,89 +229,113 @@ class LIFXLight(LightEntity): hsbk = find_hsbk(self.hass, **kwargs) - # Send messages, waiting for ACK each time - ack = AwaitAioLIFX().wait - if not self.is_on: if power_off: - await self.set_power(ack, False) + await self.set_power(False) # If fading on with color, set color immediately if hsbk and power_on: - await self.set_color(ack, hsbk, kwargs) - await self.set_power(ack, True, duration=fade) + await self.set_color(hsbk, kwargs) + await self.set_power(True, duration=fade) elif hsbk: - await self.set_color(ack, hsbk, kwargs, duration=fade) + await self.set_color(hsbk, kwargs, duration=fade) elif power_on: - await self.set_power(ack, True, duration=fade) + await self.set_power(True, duration=fade) else: - if power_on: - await self.set_power(ack, True) if hsbk: - await self.set_color(ack, hsbk, kwargs, duration=fade) + await self.set_color(hsbk, kwargs, duration=fade) + # The response from set_color will tell us if the + # bulb is actually on or not, so we don't need to + # call power_on if its already on + if power_on and self.bulb.power_level == 0: + await self.set_power(True) + elif power_on: + await self.set_power(True) if power_off: - await self.set_power(ack, False, duration=fade) - - # Avoid state ping-pong by holding off updates as the state settles - await asyncio.sleep(0.3) + await self.set_power(False, duration=fade) # Update when the transition starts and ends await self.update_during_transition(fade) - async def set_power(self, ack, pwr, duration=0): + async def set_power( + self, + pwr: bool, + duration: int = 0, + ) -> None: """Send a power change to the bulb.""" - await ack(partial(self.bulb.set_power, pwr, duration=duration)) + try: + await self.coordinator.async_set_power(pwr, duration) + except asyncio.TimeoutError as ex: + raise HomeAssistantError(f"Timeout setting power for {self.name}") from ex - async def set_color(self, ack, hsbk, kwargs, duration=0): + async def set_color( + self, + hsbk: list[float | int | None], + kwargs: dict[str, Any], + duration: int = 0, + ) -> None: """Send a color change to the bulb.""" - hsbk = merge_hsbk(self.bulb.color, hsbk) - await ack(partial(self.bulb.set_color, hsbk, duration=duration)) + merged_hsbk = merge_hsbk(self.bulb.color, hsbk) + try: + await self.coordinator.async_set_color(merged_hsbk, duration) + except asyncio.TimeoutError as ex: + raise HomeAssistantError(f"Timeout setting color for {self.name}") from ex - async def default_effect(self, **kwargs): + async def get_color( + self, + ) -> None: + """Send a get color message to the bulb.""" + try: + await self.coordinator.async_get_color() + except asyncio.TimeoutError as ex: + raise HomeAssistantError( + f"Timeout setting getting color for {self.name}" + ) from ex + + async def default_effect(self, **kwargs: Any) -> None: """Start an effect with default parameters.""" - service = kwargs[ATTR_EFFECT] - data = {ATTR_ENTITY_ID: self.entity_id} await self.hass.services.async_call( - LIFX_DOMAIN, service, data, context=self._context + DOMAIN, + kwargs[ATTR_EFFECT], + {ATTR_ENTITY_ID: self.entity_id}, + context=self._context, ) - async def async_update(self): - """Update bulb status.""" - if self.available and not self.lock.locked(): - await AwaitAioLIFX().wait(self.bulb.get_color) + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self.async_on_remove( + self.manager.async_register_entity(self.entity_id, self.entry.entry_id) + ) + return await super().async_added_to_hass() class LIFXWhite(LIFXLight): """Representation of a white-only LIFX light.""" - @property - def effect_list(self): - """Return the list of supported effects for this light.""" - return [SERVICE_EFFECT_PULSE, SERVICE_EFFECT_STOP] + _attr_effect_list = [SERVICE_EFFECT_PULSE, SERVICE_EFFECT_STOP] class LIFXColor(LIFXLight): """Representation of a color LIFX light.""" - @property - def color_mode(self) -> ColorMode: - """Return the color mode of the light.""" - sat = self.bulb.color[1] - if sat: - return ColorMode.HS - return ColorMode.COLOR_TEMP + _attr_effect_list = [ + SERVICE_EFFECT_COLORLOOP, + SERVICE_EFFECT_PULSE, + SERVICE_EFFECT_STOP, + ] @property def supported_color_modes(self) -> set[ColorMode]: - """Flag supported color modes.""" + """Return the supported color modes.""" return {ColorMode.COLOR_TEMP, ColorMode.HS} @property - def effect_list(self): - """Return the list of supported effects for this light.""" - return [SERVICE_EFFECT_COLORLOOP, SERVICE_EFFECT_PULSE, SERVICE_EFFECT_STOP] + def color_mode(self) -> ColorMode: + """Return the color mode of the light.""" + has_sat = self.bulb.color[HSBK_SATURATION] + return ColorMode.HS if has_sat else ColorMode.COLOR_TEMP @property - def hs_color(self): + def hs_color(self) -> tuple[float, float] | None: """Return the hs value.""" hue, sat, _, _ = self.bulb.color hue = hue / 65535 * 360 @@ -845,63 +346,70 @@ class LIFXColor(LIFXLight): class LIFXStrip(LIFXColor): """Representation of a LIFX light strip with multiple zones.""" - async def set_color(self, ack, hsbk, kwargs, duration=0): + async def set_color( + self, + hsbk: list[float | int | None], + kwargs: dict[str, Any], + duration: int = 0, + ) -> None: """Send a color change to the bulb.""" bulb = self.bulb - num_zones = len(bulb.color_zones) + color_zones = bulb.color_zones + num_zones = len(color_zones) + + # Zone brightness is not reported when powered off + if not self.is_on and hsbk[HSBK_BRIGHTNESS] is None: + await self.set_power(True) + await asyncio.sleep(COLOR_ZONE_POPULATE_DELAY) + await self.update_color_zones() + await self.set_power(False) 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: - await super().set_color(ack, hsbk, kwargs, duration) + first_zone = color_zones[0] + first_zone_brightness = first_zone[HSBK_BRIGHTNESS] + all_zones_have_same_brightness = all( + color_zones[zone][HSBK_BRIGHTNESS] == first_zone_brightness + for zone in range(num_zones) + ) + all_zones_are_the_same = all( + color_zones[zone] == first_zone for zone in range(num_zones) + ) + if ( + all_zones_have_same_brightness or hsbk[HSBK_BRIGHTNESS] is not None + ) and (all_zones_are_the_same or hsbk[HSBK_KELVIN] is not None): + await super().set_color(hsbk, kwargs, duration) return zones = list(range(0, num_zones)) else: zones = [x for x in set(zones) if x < num_zones] - # Zone brightness is not reported when powered off - if not self.is_on and hsbk[2] is None: - await self.set_power(ack, True) - await asyncio.sleep(0.3) - await self.update_color_zones() - await self.set_power(ack, False) - await asyncio.sleep(0.3) - # Send new color to each zone for index, zone in enumerate(zones): - zone_hsbk = merge_hsbk(bulb.color_zones[zone], hsbk) + zone_hsbk = merge_hsbk(color_zones[zone], hsbk) apply = 1 if (index == len(zones) - 1) else 0 - set_zone = partial( - bulb.set_color_zones, - start_index=zone, - end_index=zone, - color=zone_hsbk, - duration=duration, - apply=apply, - ) - await ack(set_zone) + try: + await self.coordinator.async_set_color_zones( + zone, zone, zone_hsbk, duration, apply + ) + except asyncio.TimeoutError as ex: + raise HomeAssistantError( + f"Timeout setting color zones for {self.name}" + ) from ex - async def async_update(self): - """Update strip status.""" - if self.available and not self.lock.locked(): - await super().async_update() - await self.update_color_zones() + # set_color_zones does not update the + # state of the bulb, so we need to do that + await self.get_color() - async def update_color_zones(self): - """Get updated color information for each zone.""" - zone = 0 - top = 1 - while self.available and zone < top: - # Each get_color_zones can update 8 zones at once - resp = await AwaitAioLIFX().wait( - partial(self.bulb.get_color_zones, start_index=zone) - ) - if resp: - zone += 8 - top = resp.count - - # We only await multizone responses so don't ask for just one - if zone == top - 1: - zone -= 1 + async def update_color_zones( + self, + ) -> None: + """Send a get color zones message to the bulb.""" + try: + await self.coordinator.async_update_color_zones() + except asyncio.TimeoutError as ex: + raise HomeAssistantError( + f"Timeout setting updating color zones for {self.name}" + ) from ex diff --git a/homeassistant/components/lifx/manager.py b/homeassistant/components/lifx/manager.py new file mode 100644 index 00000000000..ee5428e36a8 --- /dev/null +++ b/homeassistant/components/lifx/manager.py @@ -0,0 +1,216 @@ +"""Support for LIFX lights.""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import timedelta +from typing import Any + +import aiolifx_effects +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_NAME, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + ATTR_KELVIN, + ATTR_RGB_COLOR, + ATTR_TRANSITION, + ATTR_XY_COLOR, + COLOR_GROUP, + VALID_BRIGHTNESS, + VALID_BRIGHTNESS_PCT, + preprocess_turn_on_alternatives, +) +from homeassistant.const import ATTR_MODE +from homeassistant.core import HomeAssistant, ServiceCall, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.service import async_extract_referenced_entity_ids + +from .const import _LOGGER, DATA_LIFX_MANAGER, DOMAIN +from .util import convert_8_to_16, find_hsbk + +SCAN_INTERVAL = timedelta(seconds=10) + + +SERVICE_EFFECT_PULSE = "effect_pulse" +SERVICE_EFFECT_COLORLOOP = "effect_colorloop" +SERVICE_EFFECT_STOP = "effect_stop" + +ATTR_POWER_ON = "power_on" +ATTR_PERIOD = "period" +ATTR_CYCLES = "cycles" +ATTR_SPREAD = "spread" +ATTR_CHANGE = "change" + +PULSE_MODE_BLINK = "blink" +PULSE_MODE_BREATHE = "breathe" +PULSE_MODE_PING = "ping" +PULSE_MODE_STROBE = "strobe" +PULSE_MODE_SOLID = "solid" + +PULSE_MODES = [ + PULSE_MODE_BLINK, + PULSE_MODE_BREATHE, + PULSE_MODE_PING, + PULSE_MODE_STROBE, + PULSE_MODE_SOLID, +] + +LIFX_EFFECT_SCHEMA = { + vol.Optional(ATTR_POWER_ON, default=True): cv.boolean, +} + +LIFX_EFFECT_PULSE_SCHEMA = cv.make_entity_service_schema( + { + **LIFX_EFFECT_SCHEMA, + ATTR_BRIGHTNESS: VALID_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, + vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, + vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( + vol.Coerce(tuple), vol.ExactSequence((cv.byte, cv.byte, cv.byte)) + ), + vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( + vol.Coerce(tuple), vol.ExactSequence((cv.small_float, cv.small_float)) + ), + vol.Exclusive(ATTR_HS_COLOR, COLOR_GROUP): vol.All( + vol.Coerce(tuple), + vol.ExactSequence( + ( + vol.All(vol.Coerce(float), vol.Range(min=0, max=360)), + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), + ) + ), + ), + vol.Exclusive(ATTR_COLOR_TEMP, COLOR_GROUP): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + vol.Exclusive(ATTR_KELVIN, COLOR_GROUP): cv.positive_int, + ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Range(min=0.05)), + ATTR_CYCLES: vol.All(vol.Coerce(float), vol.Range(min=1)), + ATTR_MODE: vol.In(PULSE_MODES), + } +) + +LIFX_EFFECT_COLORLOOP_SCHEMA = cv.make_entity_service_schema( + { + **LIFX_EFFECT_SCHEMA, + ATTR_BRIGHTNESS: VALID_BRIGHTNESS, + ATTR_BRIGHTNESS_PCT: VALID_BRIGHTNESS_PCT, + ATTR_PERIOD: vol.All(vol.Coerce(float), vol.Clamp(min=0.05)), + ATTR_CHANGE: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), + ATTR_SPREAD: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=360)), + ATTR_TRANSITION: cv.positive_float, + } +) + +LIFX_EFFECT_STOP_SCHEMA = cv.make_entity_service_schema({}) + +SERVICES = ( + SERVICE_EFFECT_STOP, + SERVICE_EFFECT_PULSE, + SERVICE_EFFECT_COLORLOOP, +) + + +class LIFXManager: + """Representation of all known LIFX entities.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the manager.""" + self.hass = hass + self.effects_conductor = aiolifx_effects.Conductor(hass.loop) + self.entry_id_to_entity_id: dict[str, str] = {} + + @callback + def async_unload(self) -> None: + """Release resources.""" + for service in SERVICES: + self.hass.services.async_remove(DOMAIN, service) + + @callback + def async_register_entity( + self, entity_id: str, entry_id: str + ) -> Callable[[], None]: + """Register an entity to the config entry id.""" + self.entry_id_to_entity_id[entry_id] = entity_id + + @callback + def unregister_entity() -> None: + """Unregister entity when it is being destroyed.""" + self.entry_id_to_entity_id.pop(entry_id) + + return unregister_entity + + @callback + def async_setup(self) -> None: + """Register the LIFX effects as hass service calls.""" + + async def service_handler(service: ServiceCall) -> None: + """Apply a service, i.e. start an effect.""" + referenced = async_extract_referenced_entity_ids(self.hass, service) + all_referenced = referenced.referenced | referenced.indirectly_referenced + if all_referenced: + await self.start_effect(all_referenced, service.service, **service.data) + + self.hass.services.async_register( + DOMAIN, + SERVICE_EFFECT_PULSE, + service_handler, + schema=LIFX_EFFECT_PULSE_SCHEMA, + ) + + self.hass.services.async_register( + DOMAIN, + SERVICE_EFFECT_COLORLOOP, + service_handler, + schema=LIFX_EFFECT_COLORLOOP_SCHEMA, + ) + + self.hass.services.async_register( + DOMAIN, + SERVICE_EFFECT_STOP, + service_handler, + schema=LIFX_EFFECT_STOP_SCHEMA, + ) + + async def start_effect( + self, entity_ids: set[str], service: str, **kwargs: Any + ) -> None: + """Start a light effect on entities.""" + bulbs = [ + coordinator.device + for entry_id, coordinator in self.hass.data[DOMAIN].items() + if entry_id != DATA_LIFX_MANAGER + and self.entry_id_to_entity_id[entry_id] in entity_ids + ] + _LOGGER.debug("Starting effect %s on %s", service, bulbs) + + if service == SERVICE_EFFECT_PULSE: + effect = aiolifx_effects.EffectPulse( + power_on=kwargs.get(ATTR_POWER_ON), + period=kwargs.get(ATTR_PERIOD), + cycles=kwargs.get(ATTR_CYCLES), + mode=kwargs.get(ATTR_MODE), + hsbk=find_hsbk(self.hass, **kwargs), + ) + await self.effects_conductor.start(effect, bulbs) + elif service == SERVICE_EFFECT_COLORLOOP: + preprocess_turn_on_alternatives(self.hass, kwargs) # type: ignore[no-untyped-call] + + brightness = None + if ATTR_BRIGHTNESS in kwargs: + brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) + + effect = aiolifx_effects.EffectColorloop( + power_on=kwargs.get(ATTR_POWER_ON), + period=kwargs.get(ATTR_PERIOD), + change=kwargs.get(ATTR_CHANGE), + spread=kwargs.get(ATTR_SPREAD), + transition=kwargs.get(ATTR_TRANSITION), + brightness=brightness, + ) + await self.effects_conductor.start(effect, bulbs) + elif service == SERVICE_EFFECT_STOP: + await self.effects_conductor.stop(bulbs) diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 06e7b292ac6..ebc4d73ce5d 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -3,7 +3,12 @@ "name": "LIFX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lifx", - "requirements": ["aiolifx==0.8.1", "aiolifx_effects==0.2.2"], + "requirements": [ + "aiolifx==0.8.1", + "aiolifx_effects==0.2.2", + "aiolifx-connection==1.0.0" + ], + "quality_scale": "platinum", "dependencies": ["network"], "homekit": { "models": [ @@ -29,7 +34,8 @@ "LIFX Z" ] }, - "codeowners": ["@Djelibeybi"], + "dhcp": [{ "macaddress": "D073D5*" }, { "registered_devices": true }], + "codeowners": ["@bdraco", "@Djelibeybi"], "iot_class": "local_polling", "loggers": ["aiolifx", "aiolifx_effects", "bitstring"] } diff --git a/homeassistant/components/lifx/migration.py b/homeassistant/components/lifx/migration.py new file mode 100644 index 00000000000..1ff94daa92f --- /dev/null +++ b/homeassistant/components/lifx/migration.py @@ -0,0 +1,74 @@ +"""Migrate lifx devices to their own config entry.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .const import _LOGGER, DOMAIN +from .discovery import async_init_discovery_flow + + +async def async_migrate_legacy_entries( + hass: HomeAssistant, + discovered_hosts_by_serial: dict[str, str], + existing_serials: set[str], + legacy_entry: ConfigEntry, +) -> int: + """Migrate the legacy config entries to have an entry per device.""" + _LOGGER.debug( + "Migrating legacy entries: discovered_hosts_by_serial=%s, existing_serials=%s", + discovered_hosts_by_serial, + existing_serials, + ) + + device_registry = dr.async_get(hass) + for dev_entry in dr.async_entries_for_config_entry( + device_registry, legacy_entry.entry_id + ): + for domain, serial in dev_entry.identifiers: + if ( + domain == DOMAIN + and serial not in existing_serials + and (host := discovered_hosts_by_serial.get(serial)) + ): + async_init_discovery_flow(hass, host, serial) + + remaining_devices = dr.async_entries_for_config_entry( + dr.async_get(hass), legacy_entry.entry_id + ) + _LOGGER.debug("The following devices remain: %s", remaining_devices) + return len(remaining_devices) + + +async def async_migrate_entities_devices( + hass: HomeAssistant, legacy_entry_id: str, new_entry: ConfigEntry +) -> None: + """Move entities and devices to the new config entry.""" + migrated_devices = [] + device_registry = dr.async_get(hass) + for dev_entry in dr.async_entries_for_config_entry( + device_registry, legacy_entry_id + ): + for domain, value in dev_entry.identifiers: + if domain == DOMAIN and value == new_entry.unique_id: + _LOGGER.debug( + "Migrating device with %s to %s", + dev_entry.identifiers, + new_entry.unique_id, + ) + migrated_devices.append(dev_entry.id) + device_registry.async_update_device( + dev_entry.id, + add_config_entry_id=new_entry.entry_id, + remove_config_entry_id=legacy_entry_id, + ) + + entity_registry = er.async_get(hass) + for reg_entity in er.async_entries_for_config_entry( + entity_registry, legacy_entry_id + ): + if reg_entity.device_id in migrated_devices: + entity_registry.async_update_entity( + reg_entity.entity_id, config_entry_id=new_entry.entry_id + ) diff --git a/homeassistant/components/lifx/strings.json b/homeassistant/components/lifx/strings.json index ebb8b39a8bc..b83ae9c1609 100644 --- a/homeassistant/components/lifx/strings.json +++ b/homeassistant/components/lifx/strings.json @@ -1,12 +1,28 @@ { "config": { + "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "Do you want to set up LIFX?" + "user": { + "description": "If you leave the host empty, discovery will be used to find devices.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "pick_device": { + "data": { + "device": "Device" + } + }, + "discovery_confirm": { + "description": "Do you want to setup {label} ({host}) {serial}?" } }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "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%]" } } diff --git a/homeassistant/components/lifx/translations/en.json b/homeassistant/components/lifx/translations/en.json index 154101995ac..119259457a7 100644 --- a/homeassistant/components/lifx/translations/en.json +++ b/homeassistant/components/lifx/translations/en.json @@ -1,12 +1,28 @@ { "config": { "abort": { - "no_devices_found": "No devices found on the network", - "single_instance_allowed": "Already configured. Only a single configuration possible." + "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": { + "cannot_connect": "Failed to connect" + }, + "flow_title": "{label} ({host}) {serial}", "step": { - "confirm": { - "description": "Do you want to set up LIFX?" + "discovery_confirm": { + "description": "Do you want to setup {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Device" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "If you leave the host empty, discovery will be used to find devices." } } } diff --git a/homeassistant/components/lifx/util.py b/homeassistant/components/lifx/util.py new file mode 100644 index 00000000000..1de8bdae76a --- /dev/null +++ b/homeassistant/components/lifx/util.py @@ -0,0 +1,161 @@ +"""Support for LIFX.""" + +from __future__ import annotations + +import asyncio +from collections.abc import Callable +from typing import Any + +from aiolifx import products +from aiolifx.aiolifx import Light +from aiolifx.message import Message +import async_timeout +from awesomeversion import AwesomeVersion + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_XY_COLOR, + preprocess_turn_on_alternatives, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +import homeassistant.util.color as color_util + +from .const import _LOGGER, DOMAIN, OVERALL_TIMEOUT + +FIX_MAC_FW = AwesomeVersion("3.70") + + +@callback +def async_entry_is_legacy(entry: ConfigEntry) -> bool: + """Check if a config entry is the legacy shared one.""" + return entry.unique_id is None or entry.unique_id == DOMAIN + + +@callback +def async_get_legacy_entry(hass: HomeAssistant) -> ConfigEntry | None: + """Get the legacy config entry.""" + for entry in hass.config_entries.async_entries(DOMAIN): + if async_entry_is_legacy(entry): + return entry + return None + + +def convert_8_to_16(value: int) -> int: + """Scale an 8 bit level into 16 bits.""" + return (value << 8) | value + + +def convert_16_to_8(value: int) -> int: + """Scale a 16 bit level into 8 bits.""" + return value >> 8 + + +def lifx_features(bulb: Light) -> dict[str, Any]: + """Return a feature map for this bulb, or a default map if unknown.""" + features: dict[str, Any] = ( + products.features_map.get(bulb.product) or products.features_map[1] + ) + return features + + +def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] | None: + """Find the desired color from a number of possible inputs. + + Hue, Saturation, Brightness, Kelvin + """ + hue, saturation, brightness, kelvin = [None] * 4 + + preprocess_turn_on_alternatives(hass, kwargs) # type: ignore[no-untyped-call] + + if ATTR_HS_COLOR in kwargs: + hue, saturation = kwargs[ATTR_HS_COLOR] + elif ATTR_RGB_COLOR in kwargs: + hue, saturation = color_util.color_RGB_to_hs(*kwargs[ATTR_RGB_COLOR]) + elif ATTR_XY_COLOR in kwargs: + hue, saturation = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR]) + + if hue is not None: + assert saturation is not None + hue = int(hue / 360 * 65535) + saturation = int(saturation / 100 * 65535) + kelvin = 3500 + + if ATTR_COLOR_TEMP in kwargs: + kelvin = int( + color_util.color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) + ) + saturation = 0 + + if ATTR_BRIGHTNESS in kwargs: + brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS]) + + hsbk = [hue, saturation, brightness, kelvin] + return None if hsbk == [None] * 4 else hsbk + + +def merge_hsbk( + base: list[float | int | None], change: list[float | int | None] +) -> list[float | int | None]: + """Copy change on top of base, except when None. + + Hue, Saturation, Brightness, Kelvin + """ + return [b if c is None else c for b, c in zip(base, change)] + + +def _get_mac_offset(mac_addr: str, offset: int) -> str: + octets = [int(octet, 16) for octet in mac_addr.split(":")] + octets[5] = (octets[5] + offset) % 256 + return ":".join(f"{octet:02x}" for octet in octets) + + +def _off_by_one_mac(firmware: str) -> bool: + """Check if the firmware version has the off by one mac.""" + return bool(firmware and AwesomeVersion(firmware) >= FIX_MAC_FW) + + +def get_real_mac_addr(mac_addr: str, firmware: str) -> str: + """Increment the last byte of the mac address by one for FW>3.70.""" + return _get_mac_offset(mac_addr, 1) if _off_by_one_mac(firmware) else mac_addr + + +def formatted_serial(serial_number: str) -> str: + """Format the serial number to match the HA device registry.""" + return dr.format_mac(serial_number) + + +def mac_matches_serial_number(mac_addr: str, serial_number: str) -> bool: + """Check if a mac address matches the serial number.""" + formatted_mac = dr.format_mac(mac_addr) + return bool( + formatted_serial(serial_number) == formatted_mac + or _get_mac_offset(serial_number, 1) == formatted_mac + ) + + +async def async_execute_lifx(method: Callable) -> Message: + """Execute a lifx coroutine and wait for a response.""" + future: asyncio.Future[Message] = asyncio.Future() + + def _callback(bulb: Light, message: Message) -> None: + if not future.done(): + # The future will get canceled out from under + # us by async_timeout when we hit the OVERALL_TIMEOUT + future.set_result(message) + + _LOGGER.debug("Sending LIFX command: %s", method) + + method(callb=_callback) + result = None + + async with async_timeout.timeout(OVERALL_TIMEOUT): + result = await future + + if result is None: + raise asyncio.TimeoutError("No response from LIFX bulb") + return result diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index c062870f3e3..fb8000f8393 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -58,6 +58,8 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'isy994', 'registered_devices': True}, {'domain': 'isy994', 'hostname': 'isy*', 'macaddress': '0021B9*'}, {'domain': 'isy994', 'hostname': 'polisy*', 'macaddress': '000DB9*'}, + {'domain': 'lifx', 'macaddress': 'D073D5*'}, + {'domain': 'lifx', 'registered_devices': True}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '48A2E6*'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': 'B82CA0*'}, {'domain': 'lyric', 'hostname': 'lyric-*', 'macaddress': '00D02D*'}, diff --git a/mypy.ini b/mypy.ini index 5a3cbd7a05f..2333c20c4d8 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1369,6 +1369,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.lifx.*] +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.local_ip.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 92e8fa4f9e9..230e6b3a7a6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -186,6 +186,9 @@ aiokafka==0.7.2 # homeassistant.components.kef aiokef==0.2.16 +# homeassistant.components.lifx +aiolifx-connection==1.0.0 + # homeassistant.components.lifx aiolifx==0.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5153abbf447..9578e14fc6d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -164,6 +164,15 @@ aiohue==4.4.2 # homeassistant.components.apache_kafka aiokafka==0.7.2 +# homeassistant.components.lifx +aiolifx-connection==1.0.0 + +# homeassistant.components.lifx +aiolifx==0.8.1 + +# homeassistant.components.lifx +aiolifx_effects==0.2.2 + # homeassistant.components.lookin aiolookin==0.1.1 diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py new file mode 100644 index 00000000000..fdea992c87d --- /dev/null +++ b/tests/components/lifx/__init__.py @@ -0,0 +1,217 @@ +"""Tests for the lifx integration.""" +from __future__ import annotations + +import asyncio +from contextlib import contextmanager +from unittest.mock import AsyncMock, MagicMock, patch + +from aiolifx.aiolifx import Light + +from homeassistant.components.lifx import discovery +from homeassistant.components.lifx.const import TARGET_ANY + +MODULE = "homeassistant.components.lifx" +MODULE_CONFIG_FLOW = "homeassistant.components.lifx.config_flow" +IP_ADDRESS = "127.0.0.1" +LABEL = "My Bulb" +SERIAL = "aa:bb:cc:dd:ee:cc" +MAC_ADDRESS = "aa:bb:cc:dd:ee:cd" +DEFAULT_ENTRY_TITLE = LABEL + + +class MockMessage: + """Mock a lifx message.""" + + def __init__(self): + """Init message.""" + self.target_addr = SERIAL + self.count = 9 + + +class MockFailingLifxCommand: + """Mock a lifx command that fails.""" + + def __init__(self, bulb, **kwargs): + """Init command.""" + self.bulb = bulb + self.calls = [] + + def __call__(self, *args, **kwargs): + """Call command.""" + if callb := kwargs.get("callb"): + callb(self.bulb, None) + self.calls.append([args, kwargs]) + + def reset_mock(self): + """Reset mock.""" + self.calls = [] + + +class MockLifxCommand: + """Mock a lifx command.""" + + def __init__(self, bulb, **kwargs): + """Init command.""" + self.bulb = bulb + self.calls = [] + + def __call__(self, *args, **kwargs): + """Call command.""" + if callb := kwargs.get("callb"): + callb(self.bulb, MockMessage()) + self.calls.append([args, kwargs]) + + def reset_mock(self): + """Reset mock.""" + self.calls = [] + + +def _mocked_bulb() -> Light: + bulb = Light(asyncio.get_running_loop(), SERIAL, IP_ADDRESS) + bulb.host_firmware_version = "3.00" + bulb.label = LABEL + bulb.color = [1, 2, 3, 4] + bulb.power_level = 0 + bulb.try_sending = AsyncMock() + bulb.set_infrared = MockLifxCommand(bulb) + bulb.get_color = MockLifxCommand(bulb) + bulb.set_power = MockLifxCommand(bulb) + bulb.set_color = MockLifxCommand(bulb) + bulb.get_hostfirmware = MockLifxCommand(bulb) + bulb.get_version = MockLifxCommand(bulb) + bulb.product = 1 # LIFX Original 1000 + return bulb + + +def _mocked_failing_bulb() -> Light: + bulb = _mocked_bulb() + bulb.get_color = MockFailingLifxCommand(bulb) + bulb.set_power = MockFailingLifxCommand(bulb) + bulb.set_color = MockFailingLifxCommand(bulb) + bulb.get_hostfirmware = MockFailingLifxCommand(bulb) + bulb.get_version = MockFailingLifxCommand(bulb) + return bulb + + +def _mocked_white_bulb() -> Light: + bulb = _mocked_bulb() + bulb.product = 19 # LIFX White 900 BR30 (High Voltage) + return bulb + + +def _mocked_brightness_bulb() -> Light: + bulb = _mocked_bulb() + bulb.product = 51 # LIFX Mini White + return bulb + + +def _mocked_light_strip() -> Light: + bulb = _mocked_bulb() + bulb.product = 31 # LIFX Z + bulb.get_color_zones = MockLifxCommand(bulb) + bulb.set_color_zones = MockLifxCommand(bulb) + bulb.color_zones = [MagicMock(), MagicMock()] + return bulb + + +def _mocked_bulb_new_firmware() -> Light: + bulb = _mocked_bulb() + bulb.host_firmware_version = "3.90" + return bulb + + +def _mocked_relay() -> Light: + bulb = _mocked_bulb() + bulb.product = 70 # LIFX Switch + return bulb + + +def _patch_device(device: Light | None = None, no_device: bool = False): + """Patch out discovery.""" + + class MockLifxConnecton: + """Mock lifx discovery.""" + + def __init__(self, *args, **kwargs): + """Init connection.""" + if no_device: + self.device = _mocked_failing_bulb() + else: + self.device = device or _mocked_bulb() + self.device.mac_addr = TARGET_ANY + + async def async_setup(self): + """Mock setup.""" + + def async_stop(self): + """Mock teardown.""" + + @contextmanager + def _patcher(): + with patch("homeassistant.components.lifx.LIFXConnection", MockLifxConnecton): + yield + + return _patcher() + + +def _patch_discovery(device: Light | None = None, no_device: bool = False): + """Patch out discovery.""" + + class MockLifxDiscovery: + """Mock lifx discovery.""" + + def __init__(self, *args, **kwargs): + """Init discovery.""" + if no_device: + self.lights = {} + return + discovered = device or _mocked_bulb() + self.lights = {discovered.mac_addr: discovered} + + def start(self): + """Mock start.""" + + def cleanup(self): + """Mock cleanup.""" + + @contextmanager + def _patcher(): + with patch.object(discovery, "DEFAULT_TIMEOUT", 0), patch( + "homeassistant.components.lifx.discovery.LifxDiscovery", MockLifxDiscovery + ): + yield + + return _patcher() + + +def _patch_config_flow_try_connect( + device: Light | None = None, no_device: bool = False +): + """Patch out discovery.""" + + class MockLifxConnecton: + """Mock lifx discovery.""" + + def __init__(self, *args, **kwargs): + """Init connection.""" + if no_device: + self.device = _mocked_failing_bulb() + else: + self.device = device or _mocked_bulb() + self.device.mac_addr = TARGET_ANY + + async def async_setup(self): + """Mock setup.""" + + def async_stop(self): + """Mock teardown.""" + + @contextmanager + def _patcher(): + with patch( + "homeassistant.components.lifx.config_flow.LIFXConnection", + MockLifxConnecton, + ): + yield + + return _patcher() diff --git a/tests/components/lifx/conftest.py b/tests/components/lifx/conftest.py new file mode 100644 index 00000000000..326c4f75413 --- /dev/null +++ b/tests/components/lifx/conftest.py @@ -0,0 +1,57 @@ +"""Tests for the lifx integration.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from tests.common import mock_device_registry, mock_registry + + +@pytest.fixture +def mock_effect_conductor(): + """Mock the effect conductor.""" + + class MockConductor: + def __init__(self, *args, **kwargs) -> None: + """Mock the conductor.""" + self.start = AsyncMock() + self.stop = AsyncMock() + + def effect(self, bulb): + """Mock effect.""" + return MagicMock() + + mock_conductor = MockConductor() + + with patch( + "homeassistant.components.lifx.manager.aiolifx_effects.Conductor", + return_value=mock_conductor, + ): + yield mock_conductor + + +@pytest.fixture(autouse=True) +def lifx_mock_get_source_ip(mock_get_source_ip): + """Mock network util's async_get_source_ip.""" + + +@pytest.fixture(autouse=True) +def lifx_mock_async_get_ipv4_broadcast_addresses(): + """Mock network util's async_get_ipv4_broadcast_addresses.""" + with patch( + "homeassistant.components.network.async_get_ipv4_broadcast_addresses", + return_value=["255.255.255.255"], + ): + yield + + +@pytest.fixture(name="device_reg") +def device_reg_fixture(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture(name="entity_reg") +def entity_reg_fixture(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) diff --git a/tests/components/lifx/test_config_flow.py b/tests/components/lifx/test_config_flow.py new file mode 100644 index 00000000000..f007e9ee0e8 --- /dev/null +++ b/tests/components/lifx/test_config_flow.py @@ -0,0 +1,508 @@ +"""Tests for the lifx integration config flow.""" +import socket +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components import dhcp, zeroconf +from homeassistant.components.lifx import DOMAIN +from homeassistant.components.lifx.const import CONF_SERIAL +from homeassistant.const import CONF_DEVICE, CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM + +from . import ( + DEFAULT_ENTRY_TITLE, + IP_ADDRESS, + LABEL, + MAC_ADDRESS, + MODULE, + SERIAL, + _mocked_failing_bulb, + _mocked_relay, + _patch_config_flow_try_connect, + _patch_discovery, +) + +from tests.common import MockConfigEntry + + +async def test_discovery(hass: HomeAssistant): + """Test setting up discovery.""" + with _patch_discovery(), _patch_config_flow_try_connect(): + 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_config_flow_try_connect(), 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: SERIAL}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == DEFAULT_ENTRY_TITLE + assert result3["data"] == {CONF_HOST: IP_ADDRESS} + 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_config_flow_try_connect(): + 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_but_cannot_connect(hass: HomeAssistant): + """Test we can discover the device but we cannot connect.""" + with _patch_discovery(), _patch_config_flow_try_connect(no_device=True): + 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"] + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_DEVICE: SERIAL}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "abort" + assert result3["reason"] == "cannot_connect" + + +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_config_flow_try_connect(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_config_flow_try_connect(): + 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_config_flow_try_connect(): + 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_config_flow_try_connect(), 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: SERIAL} + ) + assert result3["type"] == "create_entry" + assert result3["title"] == DEFAULT_ENTRY_TITLE + assert result3["data"] == { + CONF_HOST: IP_ADDRESS, + } + 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_config_flow_try_connect(): + 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_config_flow_try_connect( + no_device=True + ): + 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_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_config_flow_try_connect( + 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_config_flow_try_connect(), 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"] == DEFAULT_ENTRY_TITLE + assert result4["data"] == { + CONF_HOST: 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_config_flow_try_connect( + 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_dns_error(hass: HomeAssistant): + """Test manually setup with unresolving host.""" + 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"] + + class MockLifxConnectonDnsError: + """Mock lifx discovery.""" + + def __init__(self, *args, **kwargs): + """Init connection.""" + self.device = _mocked_failing_bulb() + + async def async_setup(self): + """Mock setup.""" + raise socket.gaierror() + + def async_stop(self): + """Mock teardown.""" + + # Cannot connect due to dns error + with _patch_discovery(no_device=True), patch( + "homeassistant.components.lifx.config_flow.LIFXConnection", + MockLifxConnectonDnsError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_HOST: "does.not.resolve"} + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_manual_no_capabilities(hass: HomeAssistant): + """Test manually setup without successful get_capabilities.""" + 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_config_flow_try_connect(), 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, + } + + +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.""" + + with _patch_discovery(), _patch_config_flow_try_connect(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={CONF_HOST: IP_ADDRESS, CONF_SERIAL: SERIAL}, + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with _patch_discovery(), _patch_config_flow_try_connect(): + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=LABEL + ), + ) + await hass.async_block_till_done() + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_in_progress" + + with _patch_discovery(), _patch_config_flow_try_connect(): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress="00:00:00:00:00:00", hostname="mock_hostname" + ), + ) + await hass.async_block_till_done() + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "already_in_progress" + + with _patch_discovery(no_device=True), _patch_config_flow_try_connect( + no_device=True + ): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.2.3.5", macaddress="00:00:00:00:00:01", hostname="mock_hostname" + ), + ) + await hass.async_block_till_done() + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "cannot_connect" + + +@pytest.mark.parametrize( + "source, data", + [ + ( + config_entries.SOURCE_DHCP, + dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=LABEL), + ), + ( + config_entries.SOURCE_HOMEKIT, + zeroconf.ZeroconfServiceInfo( + host=IP_ADDRESS, + addresses=[IP_ADDRESS], + hostname=LABEL, + name=LABEL, + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "any"}, + type="mock_type", + ), + ), + ( + config_entries.SOURCE_INTEGRATION_DISCOVERY, + {CONF_HOST: IP_ADDRESS, CONF_SERIAL: SERIAL}, + ), + ], +) +async def test_discovered_by_dhcp_or_discovery(hass, source, data): + """Test we can setup when discovered from dhcp or discovery.""" + + with _patch_discovery(), _patch_config_flow_try_connect(): + 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_config_flow_try_connect(), 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, + } + assert mock_async_setup.called + assert mock_async_setup_entry.called + + +@pytest.mark.parametrize( + "source, data", + [ + ( + config_entries.SOURCE_DHCP, + dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=LABEL), + ), + ( + config_entries.SOURCE_HOMEKIT, + zeroconf.ZeroconfServiceInfo( + host=IP_ADDRESS, + addresses=[IP_ADDRESS], + hostname=LABEL, + name=LABEL, + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "any"}, + type="mock_type", + ), + ), + ( + config_entries.SOURCE_INTEGRATION_DISCOVERY, + {CONF_HOST: IP_ADDRESS, CONF_SERIAL: SERIAL}, + ), + ], +) +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.""" + + with _patch_discovery(no_device=True), _patch_config_flow_try_connect( + no_device=True + ): + 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"] == "cannot_connect" + + +async def test_discovered_by_dhcp_updates_ip(hass): + """Update host from dhcp.""" + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.2"}, unique_id=SERIAL + ) + config_entry.add_to_hass(hass) + with _patch_discovery(no_device=True), _patch_config_flow_try_connect( + no_device=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=LABEL + ), + ) + await hass.async_block_till_done() + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert config_entry.data[CONF_HOST] == IP_ADDRESS + + +async def test_refuse_relays(hass: HomeAssistant): + """Test we refuse to setup relays.""" + 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(device=_mocked_relay()), _patch_config_flow_try_connect( + device=_mocked_relay() + ): + 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["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/lifx/test_init.py b/tests/components/lifx/test_init.py new file mode 100644 index 00000000000..5424ae3c3fc --- /dev/null +++ b/tests/components/lifx/test_init.py @@ -0,0 +1,150 @@ +"""Tests for the lifx component.""" +from __future__ import annotations + +from datetime import timedelta +import socket +from unittest.mock import patch + +from homeassistant.components import lifx +from homeassistant.components.lifx import DOMAIN, discovery +from homeassistant.config_entries import ConfigEntryState +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, + SERIAL, + MockFailingLifxCommand, + _mocked_bulb, + _mocked_failing_bulb, + _patch_config_flow_try_connect, + _patch_device, + _patch_discovery, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_configuring_lifx_causes_discovery(hass): + """Test that specifying empty config does discovery.""" + start_calls = 0 + + class MockLifxDiscovery: + """Mock lifx discovery.""" + + def __init__(self, *args, **kwargs): + """Init discovery.""" + discovered = _mocked_bulb() + self.lights = {discovered.mac_addr: discovered} + + def start(self): + """Mock start.""" + nonlocal start_calls + start_calls += 1 + + def cleanup(self): + """Mock cleanup.""" + + with _patch_config_flow_try_connect(), patch.object( + discovery, "DEFAULT_TIMEOUT", 0 + ), patch( + "homeassistant.components.lifx.discovery.LifxDiscovery", MockLifxDiscovery + ): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + assert start_calls == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert start_calls == 1 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + assert start_calls == 2 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=15)) + await hass.async_block_till_done() + assert start_calls == 3 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=30)) + await hass.async_block_till_done() + assert start_calls == 4 + + +async def test_config_entry_reload(hass): + """Test that a config entry can be reloaded.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + with _patch_discovery(), _patch_config_flow_try_connect(), _patch_device(): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + assert already_migrated_config_entry.state == ConfigEntryState.LOADED + await hass.config_entries.async_unload(already_migrated_config_entry.entry_id) + await hass.async_block_till_done() + assert already_migrated_config_entry.state == ConfigEntryState.NOT_LOADED + + +async def test_config_entry_retry(hass): + """Test that a config entry can be retried.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + with _patch_discovery(no_device=True), _patch_config_flow_try_connect( + no_device=True + ), _patch_device(no_device=True): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_get_version_fails(hass): + """Test we handle get version failing.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.product = None + bulb.host_firmware_version = None + bulb.get_version = MockFailingLifxCommand(bulb) + + with _patch_discovery(device=bulb), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_dns_error_at_startup(hass): + """Test we handle get version failing.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_failing_bulb() + + class MockLifxConnectonDnsError: + """Mock lifx connection with a dns error.""" + + def __init__(self, *args, **kwargs): + """Init connection.""" + self.device = bulb + + async def async_setup(self): + """Mock setup.""" + raise socket.gaierror() + + def async_stop(self): + """Mock teardown.""" + + # Cannot connect due to dns error + with _patch_discovery(device=bulb), patch( + "homeassistant.components.lifx.LIFXConnection", + MockLifxConnectonDnsError, + ): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + assert already_migrated_config_entry.state == ConfigEntryState.SETUP_RETRY diff --git a/tests/components/lifx/test_light.py b/tests/components/lifx/test_light.py new file mode 100644 index 00000000000..5b641e850f2 --- /dev/null +++ b/tests/components/lifx/test_light.py @@ -0,0 +1,993 @@ +"""Tests for the lifx integration light platform.""" + +from datetime import timedelta +from unittest.mock import patch + +import aiolifx_effects +import pytest + +from homeassistant.components import lifx +from homeassistant.components.lifx import DOMAIN +from homeassistant.components.lifx.light import ATTR_INFRARED, ATTR_ZONES +from homeassistant.components.lifx.manager import SERVICE_EFFECT_COLORLOOP +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_SUPPORTED_COLOR_MODES, + ATTR_TRANSITION, + ATTR_XY_COLOR, + DOMAIN as LIGHT_DOMAIN, + ColorMode, +) +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, STATE_OFF, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import ( + IP_ADDRESS, + MAC_ADDRESS, + SERIAL, + MockFailingLifxCommand, + MockLifxCommand, + MockMessage, + _mocked_brightness_bulb, + _mocked_bulb, + _mocked_bulb_new_firmware, + _mocked_light_strip, + _mocked_white_bulb, + _patch_config_flow_try_connect, + _patch_device, + _patch_discovery, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_light_unique_id(hass: HomeAssistant) -> None: + """Test a light unique id.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "1.2.3.4"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == SERIAL + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device( + identifiers=set(), connections={(dr.CONNECTION_NETWORK_MAC, SERIAL)} + ) + assert device.identifiers == {(DOMAIN, SERIAL)} + + +async def test_light_unique_id_new_firmware(hass: HomeAssistant) -> None: + """Test a light unique id with newer firmware.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "1.2.3.4"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb_new_firmware() + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == SERIAL + device_registry = dr.async_get(hass) + device = device_registry.async_get_device( + identifiers=set(), + connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, + ) + assert device.identifiers == {(DOMAIN, SERIAL)} + + +@patch("homeassistant.components.lifx.light.COLOR_ZONE_POPULATE_DELAY", 0) +async def test_light_strip(hass: HomeAssistant) -> None: + """Test a light strip.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_light_strip() + bulb.power_level = 65535 + bulb.color = [65535, 65535, 65535, 65535] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 255 + assert attributes[ATTR_COLOR_MODE] == ColorMode.HS + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] + assert attributes[ATTR_HS_COLOR] == (360.0, 100.0) + assert attributes[ATTR_RGB_COLOR] == (255, 0, 0) + assert attributes[ATTR_XY_COLOR] == (0.701, 0.299) + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is True + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + call_dict = bulb.set_color_zones.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 0, + "color": [], + "duration": 0, + "end_index": 0, + "start_index": 0, + } + bulb.set_color_zones.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + call_dict = bulb.set_color_zones.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 0, + "color": [], + "duration": 0, + "end_index": 0, + "start_index": 0, + } + bulb.set_color_zones.reset_mock() + + bulb.color_zones = [ + (0, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ] + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + # Single color uses the fast path + assert bulb.set_color.calls[0][0][0] == [1820, 19660, 65535, 3500] + bulb.set_color.reset_mock() + assert len(bulb.set_color_zones.calls) == 0 + + bulb.color_zones = [ + (0, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ] + + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (255, 10, 30)}, + blocking=True, + ) + # Single color uses the fast path + assert bulb.set_color.calls[0][0][0] == [64643, 62964, 65535, 3500] + bulb.set_color.reset_mock() + assert len(bulb.set_color_zones.calls) == 0 + + bulb.color_zones = [ + (0, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ] + + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_XY_COLOR: (0.3, 0.7)}, + blocking=True, + ) + # Single color uses the fast path + assert bulb.set_color.calls[0][0][0] == [15848, 65535, 65535, 3500] + bulb.set_color.reset_mock() + assert len(bulb.set_color_zones.calls) == 0 + + bulb.color_zones = [ + (0, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (54612, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + (46420, 65535, 65535, 3500), + ] + + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + # multiple zones in effect and we are changing the brightness + # we need to do each zone individually + assert len(bulb.set_color.calls) == 0 + call_dict = bulb.set_color_zones.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 0, + "color": [0, 65535, 32896, 3500], + "duration": 0, + "end_index": 0, + "start_index": 0, + } + call_dict = bulb.set_color_zones.calls[1][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 0, + "color": [54612, 65535, 32896, 3500], + "duration": 0, + "end_index": 1, + "start_index": 1, + } + call_dict = bulb.set_color_zones.calls[7][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 1, + "color": [46420, 65535, 32896, 3500], + "duration": 0, + "end_index": 7, + "start_index": 7, + } + bulb.set_color_zones.reset_mock() + + await hass.services.async_call( + DOMAIN, + "set_state", + { + ATTR_ENTITY_ID: entity_id, + ATTR_RGB_COLOR: (255, 255, 255), + ATTR_ZONES: [0, 2], + }, + blocking=True, + ) + # set a two zones + assert len(bulb.set_color.calls) == 0 + call_dict = bulb.set_color_zones.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 0, + "color": [0, 0, 65535, 3500], + "duration": 0, + "end_index": 0, + "start_index": 0, + } + call_dict = bulb.set_color_zones.calls[1][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 1, + "color": [0, 0, 65535, 3500], + "duration": 0, + "end_index": 2, + "start_index": 2, + } + bulb.set_color_zones.reset_mock() + + bulb.get_color_zones.reset_mock() + bulb.set_power.reset_mock() + + bulb.power_level = 0 + await hass.services.async_call( + DOMAIN, + "set_state", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (255, 255, 255), ATTR_ZONES: [3]}, + blocking=True, + ) + # set a one zone + assert len(bulb.set_power.calls) == 2 + assert len(bulb.get_color_zones.calls) == 2 + assert len(bulb.set_color.calls) == 0 + call_dict = bulb.set_color_zones.calls[0][1] + call_dict.pop("callb") + assert call_dict == { + "apply": 1, + "color": [0, 0, 65535, 3500], + "duration": 0, + "end_index": 3, + "start_index": 3, + } + bulb.get_color_zones.reset_mock() + bulb.set_power.reset_mock() + bulb.set_color_zones.reset_mock() + + bulb.set_color_zones = MockFailingLifxCommand(bulb) + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + "set_state", + { + ATTR_ENTITY_ID: entity_id, + ATTR_RGB_COLOR: (255, 255, 255), + ATTR_ZONES: [3], + }, + blocking=True, + ) + + bulb.set_color_zones = MockLifxCommand(bulb) + bulb.get_color_zones = MockFailingLifxCommand(bulb) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + "set_state", + { + ATTR_ENTITY_ID: entity_id, + ATTR_RGB_COLOR: (255, 255, 255), + ATTR_ZONES: [3], + }, + blocking=True, + ) + + bulb.get_color_zones = MockLifxCommand(bulb) + bulb.get_color = MockFailingLifxCommand(bulb) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + DOMAIN, + "set_state", + { + ATTR_ENTITY_ID: entity_id, + ATTR_RGB_COLOR: (255, 255, 255), + ATTR_ZONES: [3], + }, + blocking=True, + ) + + +async def test_color_light_with_temp( + hass: HomeAssistant, mock_effect_conductor +) -> None: + """Test a color light with temp.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + bulb.power_level = 65535 + bulb.color = [65535, 65535, 65535, 65535] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 255 + assert attributes[ATTR_COLOR_MODE] == ColorMode.HS + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] + assert attributes[ATTR_HS_COLOR] == (360.0, 100.0) + assert attributes[ATTR_RGB_COLOR] == (255, 0, 0) + assert attributes[ATTR_XY_COLOR] == (0.701, 0.299) + + bulb.color = [32000, None, 32000, 6000] + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is True + bulb.set_power.reset_mock() + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] + assert attributes[ATTR_HS_COLOR] == (31.007, 6.862) + assert attributes[ATTR_RGB_COLOR] == (255, 246, 237) + assert attributes[ATTR_XY_COLOR] == (0.339, 0.338) + bulb.color = [65535, 65535, 65535, 65535] + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is True + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [65535, 65535, 25700, 65535] + bulb.set_color.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_HS_COLOR: (10, 30)}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [1820, 19660, 65535, 3500] + bulb.set_color.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_RGB_COLOR: (255, 30, 80)}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [63107, 57824, 65535, 3500] + bulb.set_color.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_XY_COLOR: (0.46, 0.376)}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [4956, 30583, 65535, 3500] + bulb.set_color.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "effect_colorloop"}, + blocking=True, + ) + start_call = mock_effect_conductor.start.mock_calls + first_call = start_call[0][1] + assert isinstance(first_call[0], aiolifx_effects.EffectColorloop) + assert first_call[1][0] == bulb + mock_effect_conductor.start.reset_mock() + mock_effect_conductor.stop.reset_mock() + + await hass.services.async_call( + DOMAIN, + SERVICE_EFFECT_COLORLOOP, + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128}, + blocking=True, + ) + start_call = mock_effect_conductor.start.mock_calls + first_call = start_call[0][1] + assert isinstance(first_call[0], aiolifx_effects.EffectColorloop) + assert first_call[1][0] == bulb + mock_effect_conductor.start.reset_mock() + mock_effect_conductor.stop.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "effect_pulse"}, + blocking=True, + ) + assert len(mock_effect_conductor.stop.mock_calls) == 1 + start_call = mock_effect_conductor.start.mock_calls + first_call = start_call[0][1] + assert isinstance(first_call[0], aiolifx_effects.EffectPulse) + assert first_call[1][0] == bulb + mock_effect_conductor.start.reset_mock() + mock_effect_conductor.stop.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_EFFECT: "effect_stop"}, + blocking=True, + ) + assert len(mock_effect_conductor.stop.mock_calls) == 2 + + +async def test_white_bulb(hass: HomeAssistant) -> None: + """Test a white bulb.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_white_bulb() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.COLOR_TEMP, + ] + assert attributes[ATTR_COLOR_TEMP] == 166 + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is True + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, None, 25700, 6000] + bulb.set_color.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 400}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, 0, 32000, 2500] + bulb.set_color.reset_mock() + + +async def test_config_zoned_light_strip_fails(hass): + """Test we handle failure to update zones.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + light_strip = _mocked_light_strip() + entity_id = "light.my_bulb" + + class MockFailingLifxCommand: + """Mock a lifx command that fails on the 3rd try.""" + + def __init__(self, bulb, **kwargs): + """Init command.""" + self.bulb = bulb + self.call_count = 0 + + def __call__(self, callb=None, *args, **kwargs): + """Call command.""" + self.call_count += 1 + response = None if self.call_count >= 3 else MockMessage() + if callb: + callb(self.bulb, response) + + light_strip.get_color_zones = MockFailingLifxCommand(light_strip) + + with _patch_discovery(device=light_strip), _patch_device(device=light_strip): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == SERIAL + assert hass.states.get(entity_id).state == STATE_OFF + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=30)) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + +async def test_white_light_fails(hass): + """Test we handle failure to power on off.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_white_bulb() + entity_id = "light.my_bulb" + + bulb.set_power = MockFailingLifxCommand(bulb) + + with _patch_discovery(device=bulb), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + entity_registry = er.async_get(hass) + assert entity_registry.async_get(entity_id).unique_id == SERIAL + assert hass.states.get(entity_id).state == STATE_OFF + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is True + bulb.set_power.reset_mock() + + bulb.set_power = MockLifxCommand(bulb) + bulb.set_color = MockFailingLifxCommand(bulb) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_COLOR_TEMP: 153}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [1, 0, 3, 6535] + bulb.set_color.reset_mock() + + +async def test_brightness_bulb(hass: HomeAssistant) -> None: + """Test a brightness only bulb.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_brightness_bulb() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.BRIGHTNESS, + ] + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is True + bulb.set_power.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [32000, None, 25700, 6000] + bulb.set_color.reset_mock() + + +async def test_transitions_brightness_only(hass: HomeAssistant) -> None: + """Test transitions with a brightness only device.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_brightness_bulb() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.BRIGHTNESS + assert attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + ColorMode.BRIGHTNESS, + ] + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + bulb.power_level = 0 + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_TRANSITION: 5, ATTR_BRIGHTNESS: 100}, + blocking=True, + ) + assert bulb.set_power.calls[0][0][0] is True + call_dict = bulb.set_power.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 5000} + bulb.set_power.reset_mock() + + bulb.power_level = 0 + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {ATTR_ENTITY_ID: entity_id, ATTR_TRANSITION: 5, ATTR_BRIGHTNESS: 200}, + blocking=True, + ) + assert bulb.set_power.calls[0][0][0] is True + call_dict = bulb.set_power.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 5000} + bulb.set_power.reset_mock() + + await hass.async_block_till_done() + bulb.get_color.reset_mock() + + # Ensure we force an update after the transition + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + assert len(bulb.get_color.calls) == 2 + + +async def test_transitions_color_bulb(hass: HomeAssistant) -> None: + """Test transitions with a color bulb.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb_new_firmware() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + bulb.power_level = 0 + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + ATTR_ENTITY_ID: entity_id, + ATTR_TRANSITION: 5, + }, + blocking=True, + ) + assert bulb.set_power.calls[0][0][0] is False + call_dict = bulb.set_power.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 0} # already off + bulb.set_power.reset_mock() + bulb.set_color.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + ATTR_RGB_COLOR: (255, 5, 10), + ATTR_ENTITY_ID: entity_id, + ATTR_TRANSITION: 5, + ATTR_BRIGHTNESS: 100, + }, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [65316, 64249, 25700, 3500] + assert bulb.set_power.calls[0][0][0] is True + call_dict = bulb.set_power.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 5000} + bulb.set_power.reset_mock() + bulb.set_color.reset_mock() + + bulb.power_level = 12800 + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + ATTR_RGB_COLOR: (5, 5, 10), + ATTR_ENTITY_ID: entity_id, + ATTR_TRANSITION: 5, + ATTR_BRIGHTNESS: 200, + }, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [43690, 32767, 51400, 3500] + call_dict = bulb.set_color.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 5000} + bulb.set_power.reset_mock() + bulb.set_color.reset_mock() + + await hass.async_block_till_done() + bulb.get_color.reset_mock() + + # Ensure we force an update after the transition + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + assert len(bulb.get_color.calls) == 2 + + bulb.set_power.reset_mock() + bulb.set_color.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + ATTR_ENTITY_ID: entity_id, + ATTR_TRANSITION: 5, + }, + blocking=True, + ) + assert bulb.set_power.calls[0][0][0] is False + call_dict = bulb.set_power.calls[0][1] + call_dict.pop("callb") + assert call_dict == {"duration": 5000} + bulb.set_power.reset_mock() + bulb.set_color.reset_mock() + + +async def test_infrared_color_bulb(hass: HomeAssistant) -> None: + """Test setting infrared with a color bulb.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb_new_firmware() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + attributes = state.attributes + assert attributes[ATTR_BRIGHTNESS] == 125 + assert attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + assert bulb.set_power.calls[0][0][0] is False + bulb.set_power.reset_mock() + + await hass.services.async_call( + DOMAIN, + "set_state", + { + ATTR_INFRARED: 100, + ATTR_ENTITY_ID: entity_id, + ATTR_BRIGHTNESS: 100, + }, + blocking=True, + ) + assert bulb.set_infrared.calls[0][0][0] == 25700 + + +async def test_color_bulb_is_actually_off(hass: HomeAssistant) -> None: + """Test setting a color when we think a bulb is on but its actually off.""" + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "127.0.0.1"}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + bulb = _mocked_bulb_new_firmware() + bulb.power_level = 65535 + bulb.color = [32000, None, 32000, 6000] + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + entity_id = "light.my_bulb" + + state = hass.states.get(entity_id) + assert state.state == "on" + + class MockLifxCommandActuallyOff: + """Mock a lifx command that will update our power level state.""" + + def __init__(self, bulb, **kwargs): + """Init command.""" + self.bulb = bulb + self.calls = [] + + def __call__(self, *args, **kwargs): + """Call command.""" + bulb.power_level = 0 + if callb := kwargs.get("callb"): + callb(self.bulb, MockMessage()) + self.calls.append([args, kwargs]) + + bulb.set_color = MockLifxCommandActuallyOff(bulb) + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + ATTR_RGB_COLOR: (100, 100, 100), + ATTR_ENTITY_ID: entity_id, + ATTR_BRIGHTNESS: 100, + }, + blocking=True, + ) + assert bulb.set_color.calls[0][0][0] == [0, 0, 25700, 3500] + assert len(bulb.set_power.calls) == 1 diff --git a/tests/components/lifx/test_migration.py b/tests/components/lifx/test_migration.py new file mode 100644 index 00000000000..0f00034590b --- /dev/null +++ b/tests/components/lifx/test_migration.py @@ -0,0 +1,281 @@ +"""Tests the lifx migration.""" +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import patch + +from homeassistant import setup +from homeassistant.components import lifx +from homeassistant.components.lifx import DOMAIN, discovery +from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.device_registry import DeviceRegistry +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, + LABEL, + MAC_ADDRESS, + SERIAL, + _mocked_bulb, + _patch_config_flow_try_connect, + _patch_device, + _patch_discovery, +) + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_migration_device_online_end_to_end( + hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry +): + """Test migration from single config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, title="LEGACY", data={}, unique_id=DOMAIN + ) + config_entry.add_to_hass(hass) + device = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, SERIAL)}, + connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, + name=LABEL, + ) + light_entity_reg = entity_reg.async_get_or_create( + config_entry=config_entry, + platform=DOMAIN, + domain="light", + unique_id=dr.format_mac(SERIAL), + original_name=LABEL, + device_id=device.id, + ) + + with _patch_discovery(), _patch_config_flow_try_connect(), _patch_device(): + 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 == DOMAIN: + migrated_entry = entry + break + + assert migrated_entry is not None + + assert device.config_entries == {migrated_entry.entry_id} + assert light_entity_reg.config_entry_id == migrated_entry.entry_id + assert er.async_entries_for_config_entry(entity_reg, config_entry) == [] + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20)) + await hass.async_block_till_done() + + legacy_entry = None + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.unique_id == DOMAIN: + legacy_entry = entry + break + + assert legacy_entry is None + + +async def test_discovery_is_more_frequent_during_migration( + hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry +): + """Test that discovery is more frequent during migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, title="LEGACY", data={}, unique_id=DOMAIN + ) + config_entry.add_to_hass(hass) + device = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, SERIAL)}, + connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, + name=LABEL, + ) + entity_reg.async_get_or_create( + config_entry=config_entry, + platform=DOMAIN, + domain="light", + unique_id=dr.format_mac(SERIAL), + original_name=LABEL, + device_id=device.id, + ) + + bulb = _mocked_bulb() + start_calls = 0 + + class MockLifxDiscovery: + """Mock lifx discovery.""" + + def __init__(self, *args, **kwargs): + """Init discovery.""" + self.bulb = bulb + self.lights = {} + + def start(self): + """Mock start.""" + nonlocal start_calls + start_calls += 1 + # Discover the bulb so we can complete migration + # and verify we switch back to normal discovery + # interval + if start_calls == 4: + self.lights = {self.bulb.mac_addr: self.bulb} + + def cleanup(self): + """Mock cleanup.""" + + with _patch_device(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), patch.object(discovery, "DEFAULT_TIMEOUT", 0), patch( + "homeassistant.components.lifx.discovery.LifxDiscovery", MockLifxDiscovery + ): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + assert start_calls == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert start_calls == 1 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + assert start_calls == 3 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) + await hass.async_block_till_done() + assert start_calls == 4 + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=15)) + await hass.async_block_till_done() + assert start_calls == 5 + + +async def test_migration_device_online_end_to_end_after_downgrade( + hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry +): + """Test migration from single config entry can happen again after a downgrade.""" + config_entry = MockConfigEntry( + domain=DOMAIN, title="LEGACY", data={}, unique_id=DOMAIN + ) + config_entry.add_to_hass(hass) + + already_migrated_config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=SERIAL + ) + already_migrated_config_entry.add_to_hass(hass) + device = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, SERIAL)}, + connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, + name=LABEL, + ) + light_entity_reg = entity_reg.async_get_or_create( + config_entry=config_entry, + platform=DOMAIN, + domain="light", + unique_id=SERIAL, + original_name=LABEL, + device_id=device.id, + ) + + with _patch_discovery(), _patch_config_flow_try_connect(), _patch_device(): + await setup.async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20)) + await hass.async_block_till_done() + + assert device.config_entries == {config_entry.entry_id} + assert light_entity_reg.config_entry_id == config_entry.entry_id + assert er.async_entries_for_config_entry(entity_reg, config_entry) == [] + + legacy_entry = None + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.unique_id == DOMAIN: + legacy_entry = entry + break + + assert legacy_entry is None + + +async def test_migration_device_online_end_to_end_ignores_other_devices( + hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry +): + """Test migration from single config entry.""" + legacy_config_entry = MockConfigEntry( + domain=DOMAIN, title="LEGACY", data={}, unique_id=DOMAIN + ) + legacy_config_entry.add_to_hass(hass) + + other_domain_config_entry = MockConfigEntry( + domain="other_domain", data={}, unique_id="other_domain" + ) + other_domain_config_entry.add_to_hass(hass) + device = device_reg.async_get_or_create( + config_entry_id=legacy_config_entry.entry_id, + identifiers={(DOMAIN, SERIAL)}, + connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, + name=LABEL, + ) + other_device = device_reg.async_get_or_create( + config_entry_id=other_domain_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "556655665566")}, + name=LABEL, + ) + light_entity_reg = entity_reg.async_get_or_create( + config_entry=legacy_config_entry, + platform=DOMAIN, + domain="light", + unique_id=SERIAL, + original_name=LABEL, + device_id=device.id, + ) + ignored_entity_reg = entity_reg.async_get_or_create( + config_entry=other_domain_config_entry, + platform=DOMAIN, + domain="sensor", + unique_id="00:00:00:00:00:00_sensor", + original_name=LABEL, + device_id=device.id, + ) + garbage_entity_reg = entity_reg.async_get_or_create( + config_entry=legacy_config_entry, + platform=DOMAIN, + domain="sensor", + unique_id="garbage", + original_name=LABEL, + device_id=other_device.id, + ) + + with _patch_discovery(), _patch_config_flow_try_connect(), _patch_device(): + await setup.async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20)) + await hass.async_block_till_done() + + new_entry = None + legacy_entry = None + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.unique_id == DOMAIN: + legacy_entry = entry + else: + new_entry = entry + + assert new_entry is not None + assert legacy_entry is None + + assert device.config_entries == {legacy_config_entry.entry_id} + assert light_entity_reg.config_entry_id == legacy_config_entry.entry_id + assert ignored_entity_reg.config_entry_id == other_domain_config_entry.entry_id + assert garbage_entity_reg.config_entry_id == legacy_config_entry.entry_id + + assert er.async_entries_for_config_entry(entity_reg, legacy_config_entry) == [] + assert dr.async_entries_for_config_entry(device_reg, legacy_config_entry) == [] From 41e4b38c3a37d580aa103aae520aee2f32102484 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Jul 2022 17:58:08 -0500 Subject: [PATCH 2671/3516] Add device and advertisement to BluetoothServiceInfoBleak (#75381) --- .../components/bluetooth/__init__.py | 71 ++++++++++++++++--- homeassistant/components/bluetooth/models.py | 7 +- tests/components/bluetooth/test_init.py | 42 +++++++++++ 3 files changed, 110 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index ea058a72759..8a1897c5ec2 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -2,11 +2,12 @@ from __future__ import annotations from collections.abc import Callable +from dataclasses import dataclass from enum import Enum import fnmatch import logging import platform -from typing import Final, TypedDict +from typing import Final, TypedDict, Union from bleak import BleakError from bleak.backends.device import BLEDevice @@ -42,6 +43,37 @@ MAX_REMEMBER_ADDRESSES: Final = 2048 SOURCE_LOCAL: Final = "local" +@dataclass +class BluetoothServiceInfoBleak(BluetoothServiceInfo): # type: ignore[misc] + """BluetoothServiceInfo with bleak data. + + Integrations may need BLEDevice and AdvertisementData + to connect to the device without having bleak trigger + another scan to translate the address to the system's + internal details. + """ + + device: BLEDevice + advertisement: AdvertisementData + + @classmethod + def from_advertisement( + cls, device: BLEDevice, advertisement_data: AdvertisementData, source: str + ) -> BluetoothServiceInfo: + """Create a BluetoothServiceInfoBleak from an advertisement.""" + return cls( + name=advertisement_data.local_name or device.name or device.address, + address=device.address, + rssi=device.rssi, + manufacturer_data=advertisement_data.manufacturer_data, + service_data=advertisement_data.service_data, + service_uuids=advertisement_data.service_uuids, + source=source, + device=device, + advertisement=advertisement_data, + ) + + class BluetoothCallbackMatcherOptional(TypedDict, total=False): """Matcher for the bluetooth integration for callback optional fields.""" @@ -75,13 +107,15 @@ MANUFACTURER_DATA_START: Final = "manufacturer_data_start" BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") -BluetoothCallback = Callable[[BluetoothServiceInfo, BluetoothChange], None] +BluetoothCallback = Callable[ + [Union[BluetoothServiceInfoBleak, BluetoothServiceInfo], BluetoothChange], None +] @hass_callback def async_discovered_service_info( hass: HomeAssistant, -) -> list[BluetoothServiceInfo]: +) -> list[BluetoothServiceInfoBleak]: """Return the discovered devices list.""" if DOMAIN not in hass.data: return [] @@ -89,6 +123,18 @@ def async_discovered_service_info( return manager.async_discovered_service_info() +@hass_callback +def async_ble_device_from_address( + hass: HomeAssistant, + address: str, +) -> BLEDevice | None: + """Return BLEDevice for an address if its present.""" + if DOMAIN not in hass.data: + return None + manager: BluetoothManager = hass.data[DOMAIN] + return manager.async_ble_device_from_address(address) + + @hass_callback def async_address_present( hass: HomeAssistant, @@ -263,13 +309,13 @@ class BluetoothManager: if not matched_domains and not self._callbacks: return - service_info: BluetoothServiceInfo | None = None + service_info: BluetoothServiceInfoBleak | None = None for callback, matcher in self._callbacks: if matcher is None or _ble_device_matches( matcher, device, advertisement_data ): if service_info is None: - service_info = BluetoothServiceInfo.from_advertisement( + service_info = BluetoothServiceInfoBleak.from_advertisement( device, advertisement_data, SOURCE_LOCAL ) try: @@ -280,7 +326,7 @@ class BluetoothManager: if not matched_domains: return if service_info is None: - service_info = BluetoothServiceInfo.from_advertisement( + service_info = BluetoothServiceInfoBleak.from_advertisement( device, advertisement_data, SOURCE_LOCAL ) for domain in matched_domains: @@ -316,7 +362,7 @@ class BluetoothManager: ): try: callback( - BluetoothServiceInfo.from_advertisement( + BluetoothServiceInfoBleak.from_advertisement( *device_adv_data, SOURCE_LOCAL ), BluetoothChange.ADVERTISEMENT, @@ -326,6 +372,15 @@ class BluetoothManager: return _async_remove_callback + @hass_callback + def async_ble_device_from_address(self, address: str) -> BLEDevice | None: + """Return the BLEDevice if present.""" + if models.HA_BLEAK_SCANNER and ( + ble_adv := models.HA_BLEAK_SCANNER.history.get(address) + ): + return ble_adv[0] + return None + @hass_callback def async_address_present(self, address: str) -> bool: """Return if the address is present.""" @@ -344,7 +399,7 @@ class BluetoothManager: discovered = models.HA_BLEAK_SCANNER.discovered_devices history = models.HA_BLEAK_SCANNER.history return [ - BluetoothServiceInfo.from_advertisement( + BluetoothServiceInfoBleak.from_advertisement( *history[device.address], SOURCE_LOCAL ) for device in discovered diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index eda94305aa9..fd7559aeefa 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping import contextlib import logging from typing import Any, Final, cast @@ -56,7 +57,9 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] self._callbacks: list[ tuple[AdvertisementDataCallback, dict[str, set[str]]] ] = [] - self.history: LRU = LRU(MAX_HISTORY_SIZE) + self.history: Mapping[str, tuple[BLEDevice, AdvertisementData]] = LRU( + MAX_HISTORY_SIZE + ) super().__init__(*args, **kwargs) @hass_callback @@ -87,7 +90,7 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] Here we get the actual callback from bleak and dispatch it to all the wrapped HaBleakScannerWrapper classes """ - self.history[device.address] = (device, advertisement_data) + self.history[device.address] = (device, advertisement_data) # type: ignore[index] for callback_filters in self._callbacks: _dispatch_callback(*callback_filters, device, advertisement_data) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index a31f911f6a7..1a002e5e354 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -248,6 +248,8 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): # wrong_name should not appear because bleak no longer sees it assert service_infos[0].name == "wohand" assert service_infos[0].source == SOURCE_LOCAL + assert isinstance(service_infos[0].device, BLEDevice) + assert isinstance(service_infos[0].advertisement, AdvertisementData) assert bluetooth.async_address_present(hass, "44:44:33:11:23:42") is False assert bluetooth.async_address_present(hass, "44:44:33:11:23:45") is True @@ -694,3 +696,43 @@ async def test_wrapped_instance_unsupported_filter( } ) assert "Only UUIDs filters are supported" in caplog.text + + +async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): + """Test the async_ble_device_from_address api.""" + mock_bt = [] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch( + "bleak.BleakScanner.discovered_devices", # Must patch before we setup + [MagicMock(address="44:44:33:11:23:45")], + ): + assert not bluetooth.async_discovered_service_info(hass) + assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22") + assert ( + bluetooth.async_ble_device_from_address(hass, "44:44:33:11:23:45") is None + ) + + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + assert not bluetooth.async_discovered_service_info(hass) + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert ( + bluetooth.async_ble_device_from_address(hass, "44:44:33:11:23:45") + is switchbot_device + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, "00:66:33:22:11:22") is None + ) From b37f15b1d572c5215636cf3f90f609b25941541d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Jul 2022 19:16:24 -0500 Subject: [PATCH 2672/3516] Update bluetooth_le_tracker to use Bleak (#75013) Co-authored-by: Martin Hjelmare --- .../bluetooth_le_tracker/device_tracker.py | 185 ++++++------ .../bluetooth_le_tracker/manifest.json | 6 +- requirements_all.txt | 1 - requirements_test_all.txt | 4 - .../bluetooth_le_tracker/conftest.py | 7 + .../test_device_tracker.py | 273 +++++++++++++++++- 6 files changed, 370 insertions(+), 106 deletions(-) create mode 100644 tests/components/bluetooth_le_tracker/conftest.py diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index b44a180988d..f85cc2bad0a 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -2,19 +2,20 @@ from __future__ import annotations import asyncio -from collections.abc import Callable +from collections.abc import Awaitable, Callable from datetime import datetime, timedelta import logging from uuid import UUID -import pygatt +from bleak import BleakClient, BleakError +from bleak.backends.device import BLEDevice import voluptuous as vol +from homeassistant.components import bluetooth from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, ) from homeassistant.components.device_tracker.const import ( - CONF_SCAN_INTERVAL, CONF_TRACK_NEW, SCAN_INTERVAL, SOURCE_TYPE_BLUETOOTH_LE, @@ -23,10 +24,10 @@ from homeassistant.components.device_tracker.legacy import ( YAML_DEVICES, async_load_config, ) -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -53,33 +54,31 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def setup_scanner( # noqa: C901 +async def async_setup_scanner( # noqa: C901 hass: HomeAssistant, config: ConfigType, - see: Callable[..., None], + async_see: Callable[..., Awaitable[None]], discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the Bluetooth LE Scanner.""" new_devices: dict[str, dict] = {} - hass.data.setdefault(DATA_BLE, {DATA_BLE_ADAPTER: None}) - - def handle_stop(event): - """Try to shut down the bluetooth child process nicely.""" - # These should never be unset at the point this runs, but just for - # safety's sake, use `get`. - adapter = hass.data.get(DATA_BLE, {}).get(DATA_BLE_ADAPTER) - if adapter is not None: - adapter.kill() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop) if config[CONF_TRACK_BATTERY]: battery_track_interval = config[CONF_TRACK_BATTERY_INTERVAL] else: battery_track_interval = timedelta(0) - def see_device(address, name, new_device=False, battery=None): + yaml_path = hass.config.path(YAML_DEVICES) + devs_to_track: set[str] = set() + devs_no_track: set[str] = set() + devs_track_battery = {} + interval: timedelta = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) + # if track new devices is true discover new devices + # on every scan. + track_new = config.get(CONF_TRACK_NEW) + + async def async_see_device(address, name, new_device=False, battery=None): """Mark a device as seen.""" if name is not None: name = name.strip("\x00") @@ -95,7 +94,7 @@ def setup_scanner( # noqa: C901 if new_devices[address]["seen"] < MIN_SEEN_NEW: return _LOGGER.debug("Adding %s to tracked devices", address) - devs_to_track.append(address) + devs_to_track.add(address) if battery_track_interval > timedelta(0): devs_track_battery[address] = dt_util.as_utc( datetime.fromtimestamp(0) @@ -105,109 +104,113 @@ def setup_scanner( # noqa: C901 new_devices[address] = {"seen": 1, "name": name} return - see( + await async_see( mac=BLE_PREFIX + address, host_name=name, source_type=SOURCE_TYPE_BLUETOOTH_LE, battery=battery, ) - def discover_ble_devices(): - """Discover Bluetooth LE devices.""" - _LOGGER.debug("Discovering Bluetooth LE devices") - try: - adapter = pygatt.GATTToolBackend() - hass.data[DATA_BLE][DATA_BLE_ADAPTER] = adapter - devs = adapter.scan() - - devices = {x["address"]: x["name"] for x in devs} - _LOGGER.debug("Bluetooth LE devices discovered = %s", devices) - except (RuntimeError, pygatt.exceptions.BLEError) as error: - _LOGGER.error("Error during Bluetooth LE scan: %s", error) - return {} - return devices - - yaml_path = hass.config.path(YAML_DEVICES) - devs_to_track = [] - devs_donot_track = [] - devs_track_battery = {} - # Load all known devices. # We just need the devices so set consider_home and home range # to 0 - for device in asyncio.run_coroutine_threadsafe( - async_load_config(yaml_path, hass, timedelta(0)), hass.loop - ).result(): + for device in await async_load_config(yaml_path, hass, timedelta(0)): # check if device is a valid bluetooth device if device.mac and device.mac[:4].upper() == BLE_PREFIX: address = device.mac[4:] if device.track: _LOGGER.debug("Adding %s to BLE tracker", device.mac) - devs_to_track.append(address) + devs_to_track.add(address) if battery_track_interval > timedelta(0): devs_track_battery[address] = dt_util.as_utc( datetime.fromtimestamp(0) ) else: _LOGGER.debug("Adding %s to BLE do not track", device.mac) - devs_donot_track.append(address) - - # if track new devices is true discover new devices - # on every scan. - track_new = config.get(CONF_TRACK_NEW) + devs_no_track.add(address) if not devs_to_track and not track_new: _LOGGER.warning("No Bluetooth LE devices to track!") return False - interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) - - def update_ble(now): + async def _async_see_update_ble_battery( + mac: str, + now: datetime, + service_info: bluetooth.BluetoothServiceInfo, + ) -> None: """Lookup Bluetooth LE devices and update status.""" - devs = discover_ble_devices() - if devs_track_battery: - adapter = hass.data[DATA_BLE][DATA_BLE_ADAPTER] - for mac in devs_to_track: - if mac not in devs: - continue + battery = None + ble_device: BLEDevice | str = ( + bluetooth.async_ble_device_from_address(hass, mac) or mac + ) + try: + async with BleakClient(ble_device) as client: + bat_char = await client.read_gatt_char(BATTERY_CHARACTERISTIC_UUID) + battery = ord(bat_char) + except asyncio.TimeoutError: + _LOGGER.warning( + "Timeout when trying to get battery status for %s", service_info.name + ) + # Bleak currently has a few places where checking dbus attributes + # can raise when there is another error. We need to trap AttributeError + # until bleak releases v0.15+ which resolves these. + except (AttributeError, BleakError) as err: + _LOGGER.debug("Could not read battery status: %s", err) + # If the device does not offer battery information, there is no point in asking again later on. + # Remove the device from the battery-tracked devices, so that their battery is not wasted + # trying to get an unavailable information. + del devs_track_battery[mac] + if battery: + await async_see_device(mac, service_info.name, battery=battery) - if devs[mac] is None: - devs[mac] = mac - - battery = None + @callback + def _async_update_ble( + service_info: bluetooth.BluetoothServiceInfo, change: bluetooth.BluetoothChange + ) -> None: + """Update from a ble callback.""" + mac = service_info.address + if mac in devs_to_track: + now = dt_util.utcnow() + hass.async_create_task(async_see_device(mac, service_info.name)) if ( mac in devs_track_battery and now > devs_track_battery[mac] + battery_track_interval ): - handle = None - try: - adapter.start(reset_on_start=False) - _LOGGER.debug("Reading battery for Bluetooth LE device %s", mac) - bt_device = adapter.connect(mac) - # Try to get the handle; it will raise a BLEError exception if not available - handle = bt_device.get_handle(BATTERY_CHARACTERISTIC_UUID) - battery = ord(bt_device.char_read(BATTERY_CHARACTERISTIC_UUID)) - devs_track_battery[mac] = now - except pygatt.exceptions.NotificationTimeout: - _LOGGER.warning("Timeout when trying to get battery status") - except pygatt.exceptions.BLEError as err: - _LOGGER.warning("Could not read battery status: %s", err) - if handle is not None: - # If the device does not offer battery information, there is no point in asking again later on. - # Remove the device from the battery-tracked devices, so that their battery is not wasted - # trying to get an unavailable information. - del devs_track_battery[mac] - finally: - adapter.stop() - see_device(mac, devs[mac], battery=battery) + devs_track_battery[mac] = now + asyncio.create_task( + _async_see_update_ble_battery(mac, now, service_info) + ) if track_new: - for address in devs: - if address not in devs_to_track and address not in devs_donot_track: - _LOGGER.info("Discovered Bluetooth LE device %s", address) - see_device(address, devs[address], new_device=True) + if mac not in devs_to_track and mac not in devs_no_track: + _LOGGER.info("Discovered Bluetooth LE device %s", mac) + hass.async_create_task( + async_see_device(mac, service_info.name, new_device=True) + ) - track_point_in_utc_time(hass, update_ble, dt_util.utcnow() + interval) + @callback + def _async_refresh_ble(now: datetime) -> None: + """Refresh BLE devices from the discovered service info.""" + # Make sure devices are seen again at the scheduled + # interval so they do not get set to not_home when + # there have been no callbacks because the RSSI or + # other properties have not changed. + for service_info in bluetooth.async_discovered_service_info(hass): + _async_update_ble(service_info, bluetooth.BluetoothChange.ADVERTISEMENT) + + cancels = [ + bluetooth.async_register_callback(hass, _async_update_ble, None), + async_track_time_interval(hass, _async_refresh_ble, interval), + ] + + @callback + def _async_handle_stop(event: Event) -> None: + """Cancel the callback.""" + for cancel in cancels: + cancel() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_handle_stop) + + _async_refresh_ble(dt_util.now()) - update_ble(dt_util.utcnow()) return True diff --git a/homeassistant/components/bluetooth_le_tracker/manifest.json b/homeassistant/components/bluetooth_le_tracker/manifest.json index 7552c024d62..6d1d4ba2d4a 100644 --- a/homeassistant/components/bluetooth_le_tracker/manifest.json +++ b/homeassistant/components/bluetooth_le_tracker/manifest.json @@ -2,8 +2,8 @@ "domain": "bluetooth_le_tracker", "name": "Bluetooth LE Tracker", "documentation": "https://www.home-assistant.io/integrations/bluetooth_le_tracker", - "requirements": ["pygatt[GATTTOOL]==4.0.5"], + "dependencies": ["bluetooth"], "codeowners": [], - "iot_class": "local_polling", - "loggers": ["pygatt"] + "iot_class": "local_push", + "loggers": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 230e6b3a7a6..d43ee87da12 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1530,7 +1530,6 @@ pyfronius==0.7.1 # homeassistant.components.ifttt pyfttt==0.3 -# homeassistant.components.bluetooth_le_tracker # homeassistant.components.skybeacon pygatt[GATTTOOL]==4.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9578e14fc6d..ee02d6f05a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1042,10 +1042,6 @@ pyfronius==0.7.1 # homeassistant.components.ifttt pyfttt==0.3 -# homeassistant.components.bluetooth_le_tracker -# homeassistant.components.skybeacon -pygatt[GATTTOOL]==4.0.5 - # homeassistant.components.hvv_departures pygti==0.9.2 diff --git a/tests/components/bluetooth_le_tracker/conftest.py b/tests/components/bluetooth_le_tracker/conftest.py new file mode 100644 index 00000000000..30b2d5a44fb --- /dev/null +++ b/tests/components/bluetooth_le_tracker/conftest.py @@ -0,0 +1,7 @@ +"""Tests for the bluetooth_le_tracker component.""" +import pytest + + +@pytest.fixture(autouse=True) +def bluetooth_le_tracker_auto_mock_bluetooth(mock_bluetooth): + """Mock the bluetooth integration scanner.""" diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index 308371c9aaa..dfe47b38b33 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -1,9 +1,17 @@ """Test Bluetooth LE device tracker.""" +import asyncio from datetime import timedelta from unittest.mock import patch +from bleak import BleakError + +from homeassistant.components.bluetooth import BluetoothServiceInfo from homeassistant.components.bluetooth_le_tracker import device_tracker +from homeassistant.components.bluetooth_le_tracker.device_tracker import ( + CONF_TRACK_BATTERY, + CONF_TRACK_BATTERY_INTERVAL, +) from homeassistant.components.device_tracker.const import ( CONF_SCAN_INTERVAL, CONF_TRACK_NEW, @@ -16,7 +24,49 @@ from homeassistant.util import dt as dt_util, slugify from tests.common import async_fire_time_changed -async def test_preserve_new_tracked_device_name(hass, mock_device_tracker_conf): +class MockBleakClient: + """Mock BleakClient.""" + + def __init__(self, *args, **kwargs): + """Mock BleakClient.""" + pass + + async def __aenter__(self, *args, **kwargs): + """Mock BleakClient.__aenter__.""" + return self + + async def __aexit__(self, *args, **kwargs): + """Mock BleakClient.__aexit__.""" + pass + + +class MockBleakClientTimesOut(MockBleakClient): + """Mock BleakClient that times out.""" + + async def read_gatt_char(self, *args, **kwargs): + """Mock BleakClient.read_gatt_char.""" + raise asyncio.TimeoutError + + +class MockBleakClientFailing(MockBleakClient): + """Mock BleakClient that fails.""" + + async def read_gatt_char(self, *args, **kwargs): + """Mock BleakClient.read_gatt_char.""" + raise BleakError("Failed") + + +class MockBleakClientBattery5(MockBleakClient): + """Mock BleakClient that returns a battery level of 5.""" + + async def read_gatt_char(self, *args, **kwargs): + """Mock BleakClient.read_gatt_char.""" + return b"\x05" + + +async def test_preserve_new_tracked_device_name( + hass, mock_bluetooth, mock_device_tracker_conf +): """Test preserving tracked device name across new seens.""" address = "DE:AD:BE:EF:13:37" @@ -24,13 +74,22 @@ async def test_preserve_new_tracked_device_name(hass, mock_device_tracker_conf): entity_id = f"{DOMAIN}.{slugify(name)}" with patch( - "homeassistant.components." - "bluetooth_le_tracker.device_tracker.pygatt.GATTToolBackend" - ) as mock_backend, patch.object(device_tracker, "MIN_SEEN_NEW", 3): + "homeassistant.components.bluetooth.async_discovered_service_info" + ) as mock_async_discovered_service_info, patch.object( + device_tracker, "MIN_SEEN_NEW", 3 + ): + device = BluetoothServiceInfo( + name=name, + address=address, + rssi=-19, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + ) # Return with name when seen first time - device = {"address": address, "name": name} - mock_backend.return_value.scan.return_value = [device] + mock_async_discovered_service_info.return_value = [device] config = { CONF_PLATFORM: "bluetooth_le_tracker", @@ -41,7 +100,17 @@ async def test_preserve_new_tracked_device_name(hass, mock_device_tracker_conf): assert result # Seen once here; return without name when seen subsequent times - device["name"] = None + device = BluetoothServiceInfo( + name=None, + address=address, + rssi=-19, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + ) + # Return with name when seen first time + mock_async_discovered_service_info.return_value = [device] # Tick until device seen enough times for to be registered for tracking for _ in range(device_tracker.MIN_SEEN_NEW - 1): @@ -54,3 +123,193 @@ async def test_preserve_new_tracked_device_name(hass, mock_device_tracker_conf): state = hass.states.get(entity_id) assert state assert state.name == name + + +async def test_tracking_battery_times_out( + hass, mock_bluetooth, mock_device_tracker_conf +): + """Test tracking the battery times out.""" + + address = "DE:AD:BE:EF:13:37" + name = "Mock device name" + entity_id = f"{DOMAIN}.{slugify(name)}" + + with patch( + "homeassistant.components.bluetooth.async_discovered_service_info" + ) as mock_async_discovered_service_info, patch.object( + device_tracker, "MIN_SEEN_NEW", 3 + ): + + device = BluetoothServiceInfo( + name=name, + address=address, + rssi=-19, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + ) + # Return with name when seen first time + mock_async_discovered_service_info.return_value = [device] + + config = { + CONF_PLATFORM: "bluetooth_le_tracker", + CONF_SCAN_INTERVAL: timedelta(minutes=1), + CONF_TRACK_BATTERY: True, + CONF_TRACK_BATTERY_INTERVAL: timedelta(minutes=2), + CONF_TRACK_NEW: True, + } + result = await async_setup_component(hass, DOMAIN, {DOMAIN: config}) + assert result + + # Tick until device seen enough times for to be registered for tracking + for _ in range(device_tracker.MIN_SEEN_NEW - 1): + async_fire_time_changed( + hass, + dt_util.utcnow() + config[CONF_SCAN_INTERVAL] + timedelta(seconds=1), + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.bluetooth_le_tracker.device_tracker.BleakClient", + MockBleakClientTimesOut, + ): + # Wait for the battery scan + async_fire_time_changed( + hass, + dt_util.utcnow() + + config[CONF_SCAN_INTERVAL] + + timedelta(seconds=1) + + timedelta(minutes=2), + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.name == name + assert "battery" not in state.attributes + + +async def test_tracking_battery_fails(hass, mock_bluetooth, mock_device_tracker_conf): + """Test tracking the battery fails.""" + + address = "DE:AD:BE:EF:13:37" + name = "Mock device name" + entity_id = f"{DOMAIN}.{slugify(name)}" + + with patch( + "homeassistant.components.bluetooth.async_discovered_service_info" + ) as mock_async_discovered_service_info, patch.object( + device_tracker, "MIN_SEEN_NEW", 3 + ): + + device = BluetoothServiceInfo( + name=name, + address=address, + rssi=-19, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + ) + # Return with name when seen first time + mock_async_discovered_service_info.return_value = [device] + + config = { + CONF_PLATFORM: "bluetooth_le_tracker", + CONF_SCAN_INTERVAL: timedelta(minutes=1), + CONF_TRACK_BATTERY: True, + CONF_TRACK_BATTERY_INTERVAL: timedelta(minutes=2), + CONF_TRACK_NEW: True, + } + result = await async_setup_component(hass, DOMAIN, {DOMAIN: config}) + assert result + + # Tick until device seen enough times for to be registered for tracking + for _ in range(device_tracker.MIN_SEEN_NEW - 1): + async_fire_time_changed( + hass, + dt_util.utcnow() + config[CONF_SCAN_INTERVAL] + timedelta(seconds=1), + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.bluetooth_le_tracker.device_tracker.BleakClient", + MockBleakClientFailing, + ): + # Wait for the battery scan + async_fire_time_changed( + hass, + dt_util.utcnow() + + config[CONF_SCAN_INTERVAL] + + timedelta(seconds=1) + + timedelta(minutes=2), + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.name == name + assert "battery" not in state.attributes + + +async def test_tracking_battery_successful( + hass, mock_bluetooth, mock_device_tracker_conf +): + """Test tracking the battery gets a value.""" + + address = "DE:AD:BE:EF:13:37" + name = "Mock device name" + entity_id = f"{DOMAIN}.{slugify(name)}" + + with patch( + "homeassistant.components.bluetooth.async_discovered_service_info" + ) as mock_async_discovered_service_info, patch.object( + device_tracker, "MIN_SEEN_NEW", 3 + ): + + device = BluetoothServiceInfo( + name=name, + address=address, + rssi=-19, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", + ) + # Return with name when seen first time + mock_async_discovered_service_info.return_value = [device] + + config = { + CONF_PLATFORM: "bluetooth_le_tracker", + CONF_SCAN_INTERVAL: timedelta(minutes=1), + CONF_TRACK_BATTERY: True, + CONF_TRACK_BATTERY_INTERVAL: timedelta(minutes=2), + CONF_TRACK_NEW: True, + } + result = await async_setup_component(hass, DOMAIN, {DOMAIN: config}) + assert result + + # Tick until device seen enough times for to be registered for tracking + for _ in range(device_tracker.MIN_SEEN_NEW - 1): + async_fire_time_changed( + hass, + dt_util.utcnow() + config[CONF_SCAN_INTERVAL] + timedelta(seconds=1), + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.bluetooth_le_tracker.device_tracker.BleakClient", + MockBleakClientBattery5, + ): + # Wait for the battery scan + async_fire_time_changed( + hass, + dt_util.utcnow() + + config[CONF_SCAN_INTERVAL] + + timedelta(seconds=1) + + timedelta(minutes=2), + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state.name == name + assert state.attributes["battery"] == 5 From ebabaeb3648d7c35453f07be2a0f67378a733955 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 19 Jul 2022 00:29:06 +0000 Subject: [PATCH 2673/3516] [ci skip] Translation update --- .../components/awair/translations/ru.json | 7 ++++++ .../evil_genius_labs/translations/ca.json | 2 +- .../components/google/translations/ca.json | 3 ++- .../components/google/translations/de.json | 3 ++- .../components/google/translations/en.json | 3 ++- .../components/google/translations/et.json | 3 ++- .../components/google/translations/id.json | 3 ++- .../components/google/translations/ja.json | 3 ++- .../components/google/translations/pl.json | 3 ++- .../components/google/translations/pt-BR.json | 3 ++- .../components/google/translations/ru.json | 1 + .../components/hive/translations/ru.json | 7 ++++++ .../homekit_controller/translations/de.json | 4 ++-- .../homekit_controller/translations/id.json | 4 ++-- .../homekit_controller/translations/pl.json | 4 ++-- .../translations/pt-BR.json | 4 ++-- .../components/lcn/translations/ru.json | 1 + .../components/lifx/translations/en.json | 6 ++++- .../components/lifx/translations/pt-BR.json | 20 +++++++++++++++++ .../components/nest/translations/ru.json | 4 ++++ .../components/nina/translations/ru.json | 22 +++++++++++++++++++ .../overkiz/translations/sensor.ru.json | 5 +++++ .../simplepush/translations/ru.json | 21 ++++++++++++++++++ .../transmission/translations/ru.json | 10 ++++++++- .../ukraine_alarm/translations/ca.json | 2 +- .../components/verisure/translations/id.json | 6 +++-- .../components/verisure/translations/ru.json | 15 ++++++++++++- 27 files changed, 146 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/simplepush/translations/ru.json diff --git a/homeassistant/components/awair/translations/ru.json b/homeassistant/components/awair/translations/ru.json index 05a14ce7857..23424091565 100644 --- a/homeassistant/components/awair/translations/ru.json +++ b/homeassistant/components/awair/translations/ru.json @@ -17,6 +17,13 @@ }, "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430." }, + "reauth_confirm": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430." + }, "user": { "data": { "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", diff --git a/homeassistant/components/evil_genius_labs/translations/ca.json b/homeassistant/components/evil_genius_labs/translations/ca.json index aa6d7355314..21810a50f65 100644 --- a/homeassistant/components/evil_genius_labs/translations/ca.json +++ b/homeassistant/components/evil_genius_labs/translations/ca.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "timeout": "Temps m\u00e0xim d'espera per establir la connexi\u00f3 esgotat", + "timeout": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/google/translations/ca.json b/homeassistant/components/google/translations/ca.json index 004fcb67f46..066630df50d 100644 --- a/homeassistant/components/google/translations/ca.json +++ b/homeassistant/components/google/translations/ca.json @@ -11,7 +11,8 @@ "invalid_access_token": "Token d'acc\u00e9s inv\u00e0lid", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", "oauth_error": "S'han rebut dades token inv\u00e0lides.", - "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "timeout_connect": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3" }, "create_entry": { "default": "Autenticaci\u00f3 exitosa" diff --git a/homeassistant/components/google/translations/de.json b/homeassistant/components/google/translations/de.json index 433324147dd..9900d3eb6d1 100644 --- a/homeassistant/components/google/translations/de.json +++ b/homeassistant/components/google/translations/de.json @@ -11,7 +11,8 @@ "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", "oauth_error": "Ung\u00fcltige Token-Daten empfangen.", - "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "timeout_connect": "Zeit\u00fcberschreitung beim Verbindungsaufbau" }, "create_entry": { "default": "Erfolgreich authentifiziert" diff --git a/homeassistant/components/google/translations/en.json b/homeassistant/components/google/translations/en.json index 2ef34ccc84b..1720e8c1454 100644 --- a/homeassistant/components/google/translations/en.json +++ b/homeassistant/components/google/translations/en.json @@ -11,7 +11,8 @@ "invalid_access_token": "Invalid access token", "missing_configuration": "The component is not configured. Please follow the documentation.", "oauth_error": "Received invalid token data.", - "reauth_successful": "Re-authentication was successful" + "reauth_successful": "Re-authentication was successful", + "timeout_connect": "Timeout establishing connection" }, "create_entry": { "default": "Successfully authenticated" diff --git a/homeassistant/components/google/translations/et.json b/homeassistant/components/google/translations/et.json index 1b5aff5774b..c516c9201e2 100644 --- a/homeassistant/components/google/translations/et.json +++ b/homeassistant/components/google/translations/et.json @@ -11,7 +11,8 @@ "invalid_access_token": "Vigane juurdep\u00e4\u00e4sut\u00f5end", "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.", "oauth_error": "Saadi sobimatud loaandmed.", - "reauth_successful": "Taastuvastamine \u00f5nnestus" + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "timeout_connect": "\u00dchenduse loomise ajal\u00f6pp" }, "create_entry": { "default": "Tuvastamine \u00f5nnestus" diff --git a/homeassistant/components/google/translations/id.json b/homeassistant/components/google/translations/id.json index ea13f27fce5..20ed21a56be 100644 --- a/homeassistant/components/google/translations/id.json +++ b/homeassistant/components/google/translations/id.json @@ -11,7 +11,8 @@ "invalid_access_token": "Token akses tidak valid", "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", "oauth_error": "Menerima respons token yang tidak valid.", - "reauth_successful": "Autentikasi ulang berhasil" + "reauth_successful": "Autentikasi ulang berhasil", + "timeout_connect": "Tenggang waktu membuat koneksi habis" }, "create_entry": { "default": "Berhasil diautentikasi" diff --git a/homeassistant/components/google/translations/ja.json b/homeassistant/components/google/translations/ja.json index e18ee9c4a02..6e2aac00c5d 100644 --- a/homeassistant/components/google/translations/ja.json +++ b/homeassistant/components/google/translations/ja.json @@ -11,7 +11,8 @@ "invalid_access_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", "oauth_error": "\u7121\u52b9\u306a\u30c8\u30fc\u30af\u30f3\u30c7\u30fc\u30bf\u3092\u53d7\u4fe1\u3057\u307e\u3057\u305f\u3002", - "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "timeout_connect": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8" }, "create_entry": { "default": "\u6b63\u5e38\u306b\u8a8d\u8a3c\u3055\u308c\u307e\u3057\u305f" diff --git a/homeassistant/components/google/translations/pl.json b/homeassistant/components/google/translations/pl.json index 3f78e3e9d6f..ff7b8af3bfc 100644 --- a/homeassistant/components/google/translations/pl.json +++ b/homeassistant/components/google/translations/pl.json @@ -11,7 +11,8 @@ "invalid_access_token": "Niepoprawny token dost\u0119pu", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", "oauth_error": "Otrzymano nieprawid\u0142owe dane tokena.", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "timeout_connect": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia" }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono" diff --git a/homeassistant/components/google/translations/pt-BR.json b/homeassistant/components/google/translations/pt-BR.json index e934155c9fa..83d4bb54afa 100644 --- a/homeassistant/components/google/translations/pt-BR.json +++ b/homeassistant/components/google/translations/pt-BR.json @@ -11,7 +11,8 @@ "invalid_access_token": "Token de acesso inv\u00e1lido", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "oauth_error": "Dados de token recebidos inv\u00e1lidos.", - "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "timeout_connect": "Tempo limite estabelecendo conex\u00e3o" }, "create_entry": { "default": "Autenticado com sucesso" diff --git a/homeassistant/components/google/translations/ru.json b/homeassistant/components/google/translations/ru.json index 51badc7226d..bb47301624f 100644 --- a/homeassistant/components/google/translations/ru.json +++ b/homeassistant/components/google/translations/ru.json @@ -3,6 +3,7 @@ "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_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.", "code_expired": "\u0421\u0440\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043a\u043e\u0434\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0438\u0441\u0442\u0435\u043a \u0438\u043b\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u044b \u043d\u0435\u0432\u0435\u0440\u043d\u043e, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", diff --git a/homeassistant/components/hive/translations/ru.json b/homeassistant/components/hive/translations/ru.json index 02736871d24..160f948b0a3 100644 --- a/homeassistant/components/hive/translations/ru.json +++ b/homeassistant/components/hive/translations/ru.json @@ -20,6 +20,13 @@ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 Hive \u0438\u043b\u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 0000, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u043f\u0440\u043e\u0441\u0438\u0442\u044c \u0434\u0440\u0443\u0433\u043e\u0439 \u043a\u043e\u0434.", "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" }, + "configuration": { + "data": { + "device_name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u043b\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 Hive.", + "title": "Hive" + }, "reauth": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/homekit_controller/translations/de.json b/homeassistant/components/homekit_controller/translations/de.json index 248d3871b3e..2b61efe6892 100644 --- a/homeassistant/components/homekit_controller/translations/de.json +++ b/homeassistant/components/homekit_controller/translations/de.json @@ -18,7 +18,7 @@ "unable_to_pair": "Koppeln fehltgeschlagen, bitte versuche es erneut", "unknown_error": "Das Ger\u00e4t meldete einen unbekannten Fehler. Die Kopplung ist fehlgeschlagen." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Breche das Pairing auf allen Controllern ab oder versuche, das Ger\u00e4t neu zu starten, und fahre dann fort, das Pairing fortzusetzen.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Pairing mit unsicheren Setup-Codes zulassen.", "pairing_code": "Kopplungscode" }, - "description": "HomeKit Controller kommuniziert mit {name} \u00fcber das lokale Netzwerk mit einer sicheren verschl\u00fcsselten Verbindung ohne separaten HomeKit Controller oder iCloud. Gib deinen HomeKit-Kopplungscode (im Format XXX-XX-XXX) ein, um dieses Zubeh\u00f6r zu verwenden. Dieser Code befindet sich in der Regel auf dem Ger\u00e4t selbst oder in der Verpackung.", + "description": "HomeKit Controller kommuniziert mit {name} ( {category} ) \u00fcber das lokale Netzwerk unter Verwendung einer sicheren verschl\u00fcsselten Verbindung ohne separaten HomeKit Controller oder iCloud. Gib deinen HomeKit-Kopplungscode (im Format XXX-XX-XXX) ein, um dieses Zubeh\u00f6r zu verwenden. Dieser Code befindet sich normalerweise auf dem Ger\u00e4t selbst oder in der Verpackung.", "title": "Mit HomeKit Zubeh\u00f6r koppeln" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/id.json b/homeassistant/components/homekit_controller/translations/id.json index 57754bea50b..0514fcca4ed 100644 --- a/homeassistant/components/homekit_controller/translations/id.json +++ b/homeassistant/components/homekit_controller/translations/id.json @@ -18,7 +18,7 @@ "unable_to_pair": "Gagal memasangkan, coba lagi.", "unknown_error": "Perangkat melaporkan kesalahan yang tidak diketahui. Pemasangan gagal." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Batalkan pemasangan di semua pengontrol, atau coba mulai ulang perangkat, lalu lanjutkan untuk melanjutkan pemasangan.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Izinkan pemasangan dengan kode penyiapan yang tidak aman.", "pairing_code": "Kode Pemasangan" }, - "description": "Pengontrol HomeKit berkomunikasi dengan {name} melalui jaringan area lokal menggunakan koneksi terenkripsi yang aman tanpa pengontrol HomeKit atau iCloud terpisah. Masukkan kode pemasangan HomeKit Anda (dalam format XXX-XX-XXX) untuk menggunakan aksesori ini. Kode ini biasanya ditemukan pada perangkat itu sendiri atau dalam kemasan.", + "description": "Pengontrol HomeKit berkomunikasi dengan {name} ({category}) melalui jaringan area lokal menggunakan koneksi terenkripsi yang aman tanpa pengontrol HomeKit atau iCloud terpisah. Masukkan kode pemasangan HomeKit Anda (dalam format XXX-XX-XXX) untuk menggunakan aksesori ini. Kode ini biasanya ditemukan pada perangkat itu sendiri atau dalam kemasan.", "title": "Pasangkan dengan perangkat melalui HomeKit Accessory Protocol" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/pl.json b/homeassistant/components/homekit_controller/translations/pl.json index d7b5cc69cf3..d7aba3a17c7 100644 --- a/homeassistant/components/homekit_controller/translations/pl.json +++ b/homeassistant/components/homekit_controller/translations/pl.json @@ -18,7 +18,7 @@ "unable_to_pair": "Nie mo\u017cna sparowa\u0107, spr\u00f3buj ponownie", "unknown_error": "Urz\u0105dzenie zg\u0142osi\u0142o nieznany b\u0142\u0105d. Parowanie nie powiod\u0142o si\u0119." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Przerwij parowanie we wszystkich kontrolerach lub spr\u00f3buj ponownie uruchomi\u0107 urz\u0105dzenie, a nast\u0119pnie wzn\u00f3w parowanie", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Zezwalaj na parowanie z niezabezpieczonymi kodami konfiguracji.", "pairing_code": "Kod parowania" }, - "description": "Kontroler HomeKit komunikuje si\u0119 z {name} poprzez sie\u0107 lokaln\u0105 za pomoc\u0105 bezpiecznego, szyfrowanego po\u0142\u0105czenia bez oddzielnego kontrolera HomeKit lub iCloud. Wprowad\u017a kod parowania (w formacie XXX-XX-XXX), aby u\u017cy\u0107 tego akcesorium. Ten kod zazwyczaj znajduje si\u0119 na samym urz\u0105dzeniu lub w jego opakowaniu.", + "description": "Kontroler HomeKit komunikuje si\u0119 z {name} ({category}) poprzez sie\u0107 lokaln\u0105 za pomoc\u0105 bezpiecznego, szyfrowanego po\u0142\u0105czenia bez oddzielnego kontrolera HomeKit lub iCloud. Wprowad\u017a kod parowania (w formacie XXX-XX-XXX), aby u\u017cy\u0107 tego akcesorium. Ten kod zazwyczaj znajduje si\u0119 na samym urz\u0105dzeniu lub w jego opakowaniu.", "title": "Sparuj z urz\u0105dzeniem poprzez akcesorium HomeKit" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/pt-BR.json b/homeassistant/components/homekit_controller/translations/pt-BR.json index 67b89e51b08..f84ff6bac37 100644 --- a/homeassistant/components/homekit_controller/translations/pt-BR.json +++ b/homeassistant/components/homekit_controller/translations/pt-BR.json @@ -18,7 +18,7 @@ "unable_to_pair": "N\u00e3o \u00e9 poss\u00edvel parear, tente novamente.", "unknown_error": "O dispositivo relatou um erro desconhecido. O pareamento falhou." }, - "flow_title": "{name}", + "flow_title": "{name} ( {category} )", "step": { "busy_error": { "description": "Abortar o emparelhamento em todos os controladores, ou tentar reiniciar o dispositivo, em seguida, continuar a retomar o emparelhamento.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Permitir o emparelhamento com c\u00f3digos de configura\u00e7\u00e3o inseguros.", "pairing_code": "C\u00f3digo de pareamento" }, - "description": "O HomeKit Controller se comunica com {name} sobre a rede local usando uma conex\u00e3o criptografada segura sem um controlador HomeKit separado ou iCloud. Digite seu c\u00f3digo de emparelhamento HomeKit (no formato XXX-XX-XXX) para usar este acess\u00f3rio. Este c\u00f3digo geralmente \u00e9 encontrado no pr\u00f3prio dispositivo ou na embalagem.", + "description": "O Controlador HomeKit se comunica com {name} ( {category} ) pela rede local usando uma conex\u00e3o criptografada segura sem um controlador HomeKit separado ou iCloud. Insira o c\u00f3digo de pareamento do HomeKit (no formato XXX-XX-XXX) para usar este acess\u00f3rio. Esse c\u00f3digo geralmente \u00e9 encontrado no pr\u00f3prio dispositivo ou na embalagem.", "title": "Emparelhar com um dispositivo atrav\u00e9s do protocolo `HomeKit Accessory`" }, "protocol_error": { diff --git a/homeassistant/components/lcn/translations/ru.json b/homeassistant/components/lcn/translations/ru.json index 0953ee96ef7..7f198d6571f 100644 --- a/homeassistant/components/lcn/translations/ru.json +++ b/homeassistant/components/lcn/translations/ru.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "\u043f\u043e\u043b\u0443\u0447\u0435\u043d \u043a\u043e\u0434 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438", "fingerprint": "\u043f\u043e\u043b\u0443\u0447\u0435\u043d \u043a\u043e\u0434 \u043e\u0442\u043f\u0435\u0447\u0430\u0442\u043a\u0430 \u043f\u0430\u043b\u044c\u0446\u0430", "send_keys": "\u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0430 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0445 \u043a\u043b\u044e\u0447\u0435\u0439", "transmitter": "\u043f\u043e\u043b\u0443\u0447\u0435\u043d \u043a\u043e\u0434 \u043e\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u0447\u0438\u043a\u0430", diff --git a/homeassistant/components/lifx/translations/en.json b/homeassistant/components/lifx/translations/en.json index 119259457a7..1f7cf981f5d 100644 --- a/homeassistant/components/lifx/translations/en.json +++ b/homeassistant/components/lifx/translations/en.json @@ -3,13 +3,17 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", - "no_devices_found": "No devices found on the network" + "no_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": "{label} ({host}) {serial}", "step": { + "confirm": { + "description": "Do you want to set up LIFX?" + }, "discovery_confirm": { "description": "Do you want to setup {label} ({host}) {serial}?" }, diff --git a/homeassistant/components/lifx/translations/pt-BR.json b/homeassistant/components/lifx/translations/pt-BR.json index f67284d8b5d..ee340af857e 100644 --- a/homeassistant/components/lifx/translations/pt-BR.json +++ b/homeassistant/components/lifx/translations/pt-BR.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede", "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, + "error": { + "cannot_connect": "Falhou ao conectar" + }, + "flow_title": "{label} ( {host} ) {serial}", "step": { "confirm": { "description": "Voc\u00ea quer configurar o LIFX?" + }, + "discovery_confirm": { + "description": "Deseja configurar {label} ( {host} ) {serial}?" + }, + "pick_device": { + "data": { + "device": "Dispositivo" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Se voc\u00ea deixar o host vazio, a descoberta ser\u00e1 usada para localizar dispositivos." } } } diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index f17f48b4560..9662d4175fd 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -1,6 +1,7 @@ { "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.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", @@ -29,6 +30,9 @@ "description": "[\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Google. \n\n\u041f\u043e\u0441\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u0442\u043e\u043a\u0435\u043d.", "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u043a\u0430 \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 Google" }, + "device_project_upgrade": { + "title": "Nest: \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c" + }, "init": { "data": { "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440" diff --git a/homeassistant/components/nina/translations/ru.json b/homeassistant/components/nina/translations/ru.json index 114f9d44040..461637eabfc 100644 --- a/homeassistant/components/nina/translations/ru.json +++ b/homeassistant/components/nina/translations/ru.json @@ -23,5 +23,27 @@ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0433\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433" } } + }, + "options": { + "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_selection": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0445\u043e\u0442\u044f \u0431\u044b \u043e\u0434\u0438\u043d \u0433\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "init": { + "data": { + "_a_to_d": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (A-D)", + "_e_to_h": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (E-H)", + "_i_to_l": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (I-L)", + "_m_to_q": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (M-Q)", + "_r_to_u": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (R-U)", + "_v_to_z": "\u0413\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433 (V-Z)", + "corona_filter": "\u0418\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u044f \u043e \u043a\u043e\u0440\u043e\u043d\u0430\u0432\u0438\u0440\u0443\u0441\u0435", + "slots": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0439 \u043d\u0430 \u0433\u043e\u0440\u043e\u0434/\u043e\u043a\u0440\u0443\u0433" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.ru.json b/homeassistant/components/overkiz/translations/sensor.ru.json index afd43899b93..94adceab671 100644 --- a/homeassistant/components/overkiz/translations/sensor.ru.json +++ b/homeassistant/components/overkiz/translations/sensor.ru.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "\u0427\u0438\u0441\u0442\u043e", "dirty": "\u0413\u0440\u044f\u0437\u043d\u043e" + }, + "overkiz__three_way_handle_direction": { + "closed": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e", + "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e", + "tilt": "\u041d\u0430\u043a\u043b\u043e\u043d" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/ru.json b/homeassistant/components/simplepush/translations/ru.json new file mode 100644 index 00000000000..4844f358c82 --- /dev/null +++ b/homeassistant/components/simplepush/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "device_key": "\u041a\u043b\u044e\u0447 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0412\u0430\u0448\u0435\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "event": "\u0421\u043e\u0431\u044b\u0442\u0438\u0435 \u0434\u043b\u044f \u0441\u043e\u0431\u044b\u0442\u0438\u0439", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u0412\u0430\u0448\u0438\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c", + "salt": "\u0421\u043e\u043b\u044c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u0430\u044f \u0432\u0430\u0448\u0438\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/ru.json b/homeassistant/components/transmission/translations/ru.json index a83e19da400..a01c71898f8 100644 --- a/homeassistant/components/transmission/translations/ru.json +++ b/homeassistant/components/transmission/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": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -9,6 +10,13 @@ "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\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": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/ukraine_alarm/translations/ca.json b/homeassistant/components/ukraine_alarm/translations/ca.json index ea03b64dd3b..0c5a5e3dedb 100644 --- a/homeassistant/components/ukraine_alarm/translations/ca.json +++ b/homeassistant/components/ukraine_alarm/translations/ca.json @@ -5,7 +5,7 @@ "cannot_connect": "Ha fallat la connexi\u00f3", "max_regions": "Es poden configurar un m\u00e0xim de 5 regions", "rate_limit": "Massa peticions", - "timeout": "Temps m\u00e0xim d'espera per establir la connexi\u00f3 esgotat", + "timeout": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/verisure/translations/id.json b/homeassistant/components/verisure/translations/id.json index c8b78a7282a..7f8dbe66817 100644 --- a/homeassistant/components/verisure/translations/id.json +++ b/homeassistant/components/verisure/translations/id.json @@ -18,7 +18,8 @@ }, "mfa": { "data": { - "code": "Kode Verifikasi" + "code": "Kode Verifikasi", + "description": "Verifikasi 2 langka tetap diaktifkan pada akun Anda. Masukkan kode verifikasi yang dikirimkan Verisure kepada Anda." } }, "reauth_confirm": { @@ -30,7 +31,8 @@ }, "reauth_mfa": { "data": { - "code": "Kode Verifikasi" + "code": "Kode Verifikasi", + "description": "Verifikasi 2 langka tetap diaktifkan pada akun Anda. Masukkan kode verifikasi yang dikirimkan Verisure kepada Anda." } }, "user": { diff --git a/homeassistant/components/verisure/translations/ru.json b/homeassistant/components/verisure/translations/ru.json index 430b8d773d0..c3380b48f9f 100644 --- a/homeassistant/components/verisure/translations/ru.json +++ b/homeassistant/components/verisure/translations/ru.json @@ -6,7 +6,8 @@ }, "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": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "unknown_mfa": "\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 MFA \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u043b \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043e\u043a Verisure \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u00ab\u041c\u043e\u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b\u00bb \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432 Home Assistant." }, + "mfa": { + "data": { + "code": "\u041a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f", + "description": "\u0412 \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u0434\u0432\u0443\u0445\u044d\u0442\u0430\u043f\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 Verisure \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u0442 \u0412\u0430\u043c." + } + }, "reauth_confirm": { "data": { "description": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Verisure.", @@ -22,6 +29,12 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c" } }, + "reauth_mfa": { + "data": { + "code": "\u041a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f", + "description": "\u0412 \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u0434\u0432\u0443\u0445\u044d\u0442\u0430\u043f\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 Verisure \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u0442 \u0412\u0430\u043c." + } + }, "user": { "data": { "description": "\u0412\u0445\u043e\u0434 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Verisure.", From a9e9d7b112b3555484e7a948bc995ab748a6e28d Mon Sep 17 00:00:00 2001 From: R0nd Date: Tue, 19 Jul 2022 07:13:12 +0300 Subject: [PATCH 2674/3516] Pass context to shopping list events (#75377) --- .../components/shopping_list/__init__.py | 44 +++++++++++++++---- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 5f6a13e8e13..2af54722739 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -335,7 +335,11 @@ class ClearCompletedItemsView(http.HomeAssistantView): @callback -def websocket_handle_items(hass, connection, msg): +def websocket_handle_items( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: """Handle get shopping_list items.""" connection.send_message( websocket_api.result_message(msg["id"], hass.data[DOMAIN].items) @@ -343,15 +347,25 @@ def websocket_handle_items(hass, connection, msg): @websocket_api.async_response -async def websocket_handle_add(hass, connection, msg): +async def websocket_handle_add( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: """Handle add item to shopping_list.""" item = await hass.data[DOMAIN].async_add(msg["name"]) - hass.bus.async_fire(EVENT, {"action": "add", "item": item}) + hass.bus.async_fire( + EVENT, {"action": "add", "item": item}, context=connection.context(msg) + ) connection.send_message(websocket_api.result_message(msg["id"], item)) @websocket_api.async_response -async def websocket_handle_update(hass, connection, msg): +async def websocket_handle_update( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: """Handle update shopping_list item.""" msg_id = msg.pop("id") item_id = msg.pop("item_id") @@ -360,7 +374,9 @@ async def websocket_handle_update(hass, connection, msg): try: item = await hass.data[DOMAIN].async_update(item_id, data) - hass.bus.async_fire(EVENT, {"action": "update", "item": item}) + hass.bus.async_fire( + EVENT, {"action": "update", "item": item}, context=connection.context(msg) + ) connection.send_message(websocket_api.result_message(msg_id, item)) except KeyError: connection.send_message( @@ -369,10 +385,14 @@ async def websocket_handle_update(hass, connection, msg): @websocket_api.async_response -async def websocket_handle_clear(hass, connection, msg): +async def websocket_handle_clear( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: """Handle clearing shopping_list items.""" await hass.data[DOMAIN].async_clear_completed() - hass.bus.async_fire(EVENT, {"action": "clear"}) + hass.bus.async_fire(EVENT, {"action": "clear"}, context=connection.context(msg)) connection.send_message(websocket_api.result_message(msg["id"])) @@ -382,12 +402,18 @@ async def websocket_handle_clear(hass, connection, msg): vol.Required("item_ids"): [str], } ) -def websocket_handle_reorder(hass, connection, msg): +def websocket_handle_reorder( + hass: HomeAssistant, + connection: websocket_api.connection.ActiveConnection, + msg: dict, +) -> None: """Handle reordering shopping_list items.""" msg_id = msg.pop("id") try: hass.data[DOMAIN].async_reorder(msg.pop("item_ids")) - hass.bus.async_fire(EVENT, {"action": "reorder"}) + hass.bus.async_fire( + EVENT, {"action": "reorder"}, context=connection.context(msg) + ) connection.send_result(msg_id) except KeyError: connection.send_error( From d05160a40227fc4b9b91094b6d455265201c36ec Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Jul 2022 06:13:53 +0200 Subject: [PATCH 2675/3516] Remove deprecated Lyric YAML configuration (#75418) --- homeassistant/components/lyric/__init__.py | 53 +-------------- tests/components/lyric/test_config_flow.py | 75 ++++------------------ 2 files changed, 16 insertions(+), 112 deletions(-) diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index e7fe6789268..4e4eec85899 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -11,14 +11,9 @@ from aiolyric.exceptions import LyricAuthenticationException, LyricException from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation import async_timeout -import voluptuous as vol -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import ( @@ -28,7 +23,6 @@ from homeassistant.helpers import ( device_registry as dr, ) from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -42,53 +36,13 @@ from .api import ( ) from .const import DOMAIN -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.CLIMATE, Platform.SENSOR] -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Honeywell Lyric component.""" - hass.data[DOMAIN] = {} - - if DOMAIN not in config: - return True - - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential( - config[DOMAIN][CONF_CLIENT_ID], - config[DOMAIN][CONF_CLIENT_SECRET], - ), - ) - - _LOGGER.warning( - "Configuration of Honeywell Lyric integration in YAML is deprecated " - "and will be removed in a future release; Your existing OAuth " - "Application Credentials have been imported into the UI " - "automatically and can be safely removed from your " - "configuration.yaml file" - ) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Honeywell Lyric from a config entry.""" implementation = ( @@ -143,10 +97,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_interval=timedelta(seconds=300), ) - hass.data[DOMAIN][entry.entry_id] = coordinator - # Fetch initial data so we have data when entities subscribe await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index 6eb97f03f89..7faa63c2b1b 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -1,16 +1,17 @@ """Test the Honeywell Lyric config flow.""" -import asyncio from http import HTTPStatus from unittest.mock import patch import pytest -from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.http import CONF_BASE_URL, DOMAIN as DOMAIN_HTTP -from homeassistant.components.lyric import config_flow +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.components.lyric.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -21,18 +22,12 @@ CLIENT_SECRET = "5678" @pytest.fixture() async def mock_impl(hass): """Mock implementation.""" - await setup.async_setup_component(hass, "http", {}) + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() - impl = config_entry_oauth2_flow.LocalOAuth2Implementation( - hass, - DOMAIN, - CLIENT_ID, - CLIENT_SECRET, - OAUTH2_AUTHORIZE, - OAUTH2_TOKEN, + await async_import_client_credential( + hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "cred" ) - config_flow.OAuth2FlowHandler.async_register_implementation(hass, impl) - return impl async def test_abort_if_no_configuration(hass): @@ -45,21 +40,9 @@ async def test_abort_if_no_configuration(hass): async def test_full_flow( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host + hass, hass_client_no_auth, aioclient_mock, current_request_with_host, mock_impl ): """Check full flow.""" - assert await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - CONF_CLIENT_ID: CLIENT_ID, - CONF_CLIENT_SECRET: CLIENT_SECRET, - }, - DOMAIN_HTTP: {CONF_BASE_URL: "https://example.com"}, - }, - ) - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -98,7 +81,7 @@ async def test_full_flow( ) as mock_setup: result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["data"]["auth_implementation"] == DOMAIN + assert result["data"]["auth_implementation"] == "cred" result["data"]["token"].pop("expires_at") assert result["data"]["token"] == { @@ -116,42 +99,10 @@ async def test_full_flow( assert len(mock_setup.mock_calls) == 1 -async def test_abort_if_authorization_timeout( - hass, mock_impl, current_request_with_host -): - """Check Somfy authorization timeout.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - flow = config_flow.OAuth2FlowHandler() - flow.hass = hass - - with patch.object( - mock_impl, "async_generate_authorize_url", side_effect=asyncio.TimeoutError - ): - result = await flow.async_step_user() - - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "authorize_url_timeout" - - async def test_reauthentication_flow( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host + hass, hass_client_no_auth, aioclient_mock, current_request_with_host, mock_impl ): """Test reauthentication flow.""" - await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - CONF_CLIENT_ID: CLIENT_ID, - CONF_CLIENT_SECRET: CLIENT_SECRET, - }, - DOMAIN_HTTP: {CONF_BASE_URL: "https://example.com"}, - }, - ) - old_entry = MockConfigEntry( domain=DOMAIN, unique_id=DOMAIN, From e65018fb851764dd29963459b6cdeef67d30efbe Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Jul 2022 06:14:41 +0200 Subject: [PATCH 2676/3516] Clean up deprecated connection class remainders (#75421) --- homeassistant/helpers/config_entry_flow.py | 17 +---------------- tests/helpers/test_config_entry_flow.py | 11 ----------- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 847a90a0d1b..bf2f95c12c6 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -10,7 +10,7 @@ from homeassistant.components import onboarding from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult -from .typing import UNDEFINED, DiscoveryInfoType, UndefinedType +from .typing import DiscoveryInfoType if TYPE_CHECKING: import asyncio @@ -173,23 +173,8 @@ def register_discovery_flow( domain: str, title: str, discovery_function: DiscoveryFunctionType[Awaitable[bool] | bool], - connection_class: str | UndefinedType = UNDEFINED, ) -> None: """Register flow for discovered integrations that not require auth.""" - if connection_class is not UNDEFINED: - _LOGGER.warning( - ( - "The %s (%s) integration is setting a connection_class" - " when calling the 'register_discovery_flow()' method in its" - " config flow. The connection class has been deprecated and will" - " be removed in a future release of Home Assistant." - " If '%s' is a custom integration, please contact the author" - " of that integration about this warning.", - ), - title, - domain, - domain, - ) class DiscoveryFlow(DiscoveryFlowHandler[Union[Awaitable[bool], bool]]): """Discovery flow handler.""" diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 63fa58851e3..a4a3e2b27e7 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -418,14 +418,3 @@ async def test_webhook_create_cloudhook_aborts_not_connected(hass, webhook_flow_ assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "cloud_not_connected" - - -async def test_warning_deprecated_connection_class(hass, caplog): - """Test that we log a warning when the connection_class is used.""" - discovery_function = Mock() - with patch.dict(config_entries.HANDLERS): - config_entry_flow.register_discovery_flow( - "test", "Test", discovery_function, connection_class="local_polling" - ) - - assert "integration is setting a connection_class" in caplog.text From 24b3b5fc46fea2fa2f3caab2c6b593369cec72fd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Jul 2022 06:14:55 +0200 Subject: [PATCH 2677/3516] Remove deprecated Senz YAML configuration (#75419) --- homeassistant/components/senz/__init__.py | 51 ++--------------------- tests/components/senz/test_config_flow.py | 18 ++++---- 2 files changed, 14 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/senz/__init__.py b/homeassistant/components/senz/__init__.py index 6f68189e8bb..012b40f5def 100644 --- a/homeassistant/components/senz/__init__.py +++ b/homeassistant/components/senz/__init__.py @@ -6,14 +6,9 @@ import logging from aiosenz import SENZAPI, Thermostat from httpx import RequestError -import voluptuous as vol -from homeassistant.components.application_credentials import ( - ClientCredential, - async_import_client_credential, -) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( @@ -21,7 +16,6 @@ from homeassistant.helpers import ( config_validation as cv, httpx_client, ) -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .api import SENZConfigEntryAuth @@ -31,52 +25,13 @@ UPDATE_INTERVAL = timedelta(seconds=30) _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [Platform.CLIMATE] SENZDataUpdateCoordinator = DataUpdateCoordinator[dict[str, Thermostat]] -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the SENZ OAuth2 configuration.""" - hass.data[DOMAIN] = {} - - if DOMAIN not in config: - return True - - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential( - config[DOMAIN][CONF_CLIENT_ID], - config[DOMAIN][CONF_CLIENT_SECRET], - ), - ) - _LOGGER.warning( - "Configuration of SENZ integration in YAML is deprecated " - "and will be removed in a future release; Your existing OAuth " - "Application Credentials have been imported into the UI " - "automatically and can be safely removed from your " - "configuration.yaml file" - ) - - return True - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SENZ from a config entry.""" implementation = ( @@ -111,7 +66,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id] = coordinator + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/tests/components/senz/test_config_flow.py b/tests/components/senz/test_config_flow.py index 3906f2c0320..f0ed54ba932 100644 --- a/tests/components/senz/test_config_flow.py +++ b/tests/components/senz/test_config_flow.py @@ -3,10 +3,15 @@ from unittest.mock import patch from aiosenz import AUTHORIZATION_ENDPOINT, TOKEN_ENDPOINT -from homeassistant import config_entries, setup +from homeassistant import config_entries +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) from homeassistant.components.senz.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.setup import async_setup_component CLIENT_ID = "1234" CLIENT_SECRET = "5678" @@ -19,12 +24,11 @@ async def test_full_flow( current_request_with_host, ) -> None: """Check full flow.""" - assert await setup.async_setup_component( - hass, - "senz", - { - "senz": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, - }, + await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + await async_import_client_credential( + hass, DOMAIN, ClientCredential(CLIENT_ID, CLIENT_SECRET), "cred" ) result = await hass.config_entries.flow.async_init( From 8e8c6e23945fa0d66619d6488fa359714bf406d7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 19 Jul 2022 06:15:22 +0200 Subject: [PATCH 2678/3516] Remove unused ignore file (#75416) --- .ignore | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .ignore diff --git a/.ignore b/.ignore deleted file mode 100644 index 45c6dc5561f..00000000000 --- a/.ignore +++ /dev/null @@ -1,6 +0,0 @@ -# Patterns matched in this file will be ignored by supported search utilities - -# Ignore generated html and javascript files -/homeassistant/components/frontend/www_static/*.html -/homeassistant/components/frontend/www_static/*.js -/homeassistant/components/frontend/www_static/panels/*.html From c3d536b25520bb784b8874275d2e7a91d1954169 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 19 Jul 2022 13:01:39 +0200 Subject: [PATCH 2679/3516] Store creation timestamps for resolution center issues (#75430) --- .../resolution_center/issue_registry.py | 15 ++++-- .../resolution_center/websocket_api.py | 3 +- .../components/resolution_center/test_init.py | 47 ++++++++++++++++++- .../resolution_center/test_issue_registry.py | 4 ++ .../resolution_center/test_websocket_api.py | 6 +++ 5 files changed, 70 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/resolution_center/issue_registry.py b/homeassistant/components/resolution_center/issue_registry.py index d9042d891dd..aed1cd51b10 100644 --- a/homeassistant/components/resolution_center/issue_registry.py +++ b/homeassistant/components/resolution_center/issue_registry.py @@ -2,11 +2,13 @@ from __future__ import annotations import dataclasses +from datetime import datetime from typing import Optional, cast from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.storage import Store +import homeassistant.util.dt as dt_util from .models import IssueSeverity @@ -14,7 +16,6 @@ DATA_REGISTRY = "issue_registry" STORAGE_KEY = "resolution_center.issue_registry" STORAGE_VERSION = 1 SAVE_DELAY = 10 -SAVED_FIELDS = ("dismissed_version", "domain", "issue_id") @dataclasses.dataclass(frozen=True) @@ -23,6 +24,7 @@ class IssueEntry: active: bool breaks_in_ha_version: str | None + created: datetime dismissed_version: str | None domain: str is_fixable: bool | None @@ -68,6 +70,7 @@ class IssueRegistry: issue = IssueEntry( active=True, breaks_in_ha_version=breaks_in_ha_version, + created=dt_util.utcnow(), dismissed_version=None, domain=domain, is_fixable=is_fixable, @@ -125,10 +128,11 @@ class IssueRegistry: if isinstance(data, dict): for issue in data["issues"]: - assert issue["domain"] and issue["issue_id"] + assert issue["created"] and issue["domain"] and issue["issue_id"] issues[(issue["domain"], issue["issue_id"])] = IssueEntry( active=False, breaks_in_ha_version=None, + created=cast(datetime, dt_util.parse_datetime(issue["created"])), dismissed_version=issue["dismissed_version"], domain=issue["domain"], is_fixable=None, @@ -152,7 +156,12 @@ class IssueRegistry: data = {} data["issues"] = [ - {field: getattr(entry, field) for field in SAVED_FIELDS} + { + "created": entry.created.isoformat(), + "dismissed_version": entry.dismissed_version, + "domain": entry.domain, + "issue_id": entry.issue_id, + } for entry in self.issues.values() ] diff --git a/homeassistant/components/resolution_center/websocket_api.py b/homeassistant/components/resolution_center/websocket_api.py index dfa4f1903d9..314df009118 100644 --- a/homeassistant/components/resolution_center/websocket_api.py +++ b/homeassistant/components/resolution_center/websocket_api.py @@ -67,8 +67,9 @@ def ws_list_issues( """Return a list of issues.""" def ws_dict(kv_pairs: list[tuple[Any, Any]]) -> dict[Any, Any]: - result = {k: v for k, v in kv_pairs if k != "active"} + result = {k: v for k, v in kv_pairs if k not in ("active")} result["dismissed"] = result["dismissed_version"] is not None + result["created"] = result["created"].isoformat() return result issue_registry = async_get_issue_registry(hass) diff --git a/tests/components/resolution_center/test_init.py b/tests/components/resolution_center/test_init.py index 66f9bc42935..a707c3845fe 100644 --- a/tests/components/resolution_center/test_init.py +++ b/tests/components/resolution_center/test_init.py @@ -1,6 +1,7 @@ """Test the resolution center websocket API.""" from unittest.mock import AsyncMock, Mock +from freezegun import freeze_time import pytest from homeassistant.components.resolution_center import ( @@ -19,6 +20,7 @@ from homeassistant.setup import async_setup_component from tests.common import mock_platform +@freeze_time("2022-07-19 07:53:05") async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: """Test creating and updating issues.""" assert await async_setup_component(hass, DOMAIN, {}) @@ -75,6 +77,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: "issues": [ dict( issue, + created="2022-07-19T07:53:05+00:00", dismissed=False, dismissed_version=None, ) @@ -101,6 +104,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: assert msg["success"] assert msg["result"]["issues"][0] == dict( issues[0], + created="2022-07-19T07:53:05+00:00", dismissed=False, dismissed_version=None, learn_more_url="blablabla", @@ -147,6 +151,7 @@ async def test_create_issue_invalid_version( assert msg["result"] == {"issues": []} +@freeze_time("2022-07-19 07:53:05") async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: """Test dismissing issues.""" assert await async_setup_component(hass, DOMAIN, {}) @@ -193,6 +198,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: "issues": [ dict( issue, + created="2022-07-19T07:53:05+00:00", dismissed=False, dismissed_version=None, ) @@ -212,6 +218,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: "issues": [ dict( issue, + created="2022-07-19T07:53:05+00:00", dismissed=False, dismissed_version=None, ) @@ -230,6 +237,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: "issues": [ dict( issue, + created="2022-07-19T07:53:05+00:00", dismissed=True, dismissed_version=ha_version, ) @@ -248,6 +256,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: "issues": [ dict( issue, + created="2022-07-19T07:53:05+00:00", dismissed=True, dismissed_version=ha_version, ) @@ -274,14 +283,16 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: assert msg["success"] assert msg["result"]["issues"][0] == dict( issues[0], + created="2022-07-19T07:53:05+00:00", dismissed=True, dismissed_version=ha_version, learn_more_url="blablabla", ) -async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None: +async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> None: """Test we can delete an issue.""" + freezer.move_to("2022-07-19 07:53:05") assert await async_setup_component(hass, DOMAIN, {}) client = await hass_ws_client(hass) @@ -320,6 +331,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None: "issues": [ dict( issue, + created="2022-07-19T07:53:05+00:00", dismissed=False, dismissed_version=None, ) @@ -338,6 +350,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None: "issues": [ dict( issue, + created="2022-07-19T07:53:05+00:00", dismissed=False, dismissed_version=None, ) @@ -363,6 +376,38 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client) -> None: assert msg["success"] assert msg["result"] == {"issues": []} + # Create the same issues again created timestamp should change + freezer.move_to("2022-07-19 08:53:05") + + for issue in issues: + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + + await client.send_json({"id": 5, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created="2022-07-19T08:53:05+00:00", + dismissed=False, + dismissed_version=None, + ) + for issue in issues + ] + } + async def test_non_compliant_platform(hass: HomeAssistant, hass_ws_client) -> None: """Test non-compliant platforms are not registered.""" diff --git a/tests/components/resolution_center/test_issue_registry.py b/tests/components/resolution_center/test_issue_registry.py index a0dffaacc8f..3f5cc235a19 100644 --- a/tests/components/resolution_center/test_issue_registry.py +++ b/tests/components/resolution_center/test_issue_registry.py @@ -64,8 +64,10 @@ async def test_load_issues(hass: HomeAssistant) -> None: assert list(registry.issues) == list(registry2.issues) issue1_registry2 = registry2.async_get_issue("test", "issue_1") + assert issue1_registry2.created == issue1.created assert issue1_registry2.dismissed_version == issue1.dismissed_version issue2_registry2 = registry2.async_get_issue("test", "issue_2") + assert issue2_registry2.created == issue2.created assert issue2_registry2.dismissed_version == issue2.dismissed_version @@ -76,11 +78,13 @@ async def test_loading_issues_from_storage(hass: HomeAssistant, hass_storage) -> "data": { "issues": [ { + "created": "2022-07-19T09:41:13.746514+00:00", "dismissed_version": "2022.7.0.dev0", "domain": "test", "issue_id": "issue_1", }, { + "created": "2022-07-19T19:41:13.746514+00:00", "dismissed_version": None, "domain": "test", "issue_id": "issue_2", diff --git a/tests/components/resolution_center/test_websocket_api.py b/tests/components/resolution_center/test_websocket_api.py index 42899065121..8701996a535 100644 --- a/tests/components/resolution_center/test_websocket_api.py +++ b/tests/components/resolution_center/test_websocket_api.py @@ -4,6 +4,7 @@ from __future__ import annotations from http import HTTPStatus from unittest.mock import ANY, AsyncMock, Mock +from freezegun import freeze_time import pytest import voluptuous as vol @@ -56,6 +57,7 @@ async def create_issues(hass, ws_client): "issues": [ dict( issue, + created=ANY, dismissed=False, dismissed_version=None, ) @@ -146,6 +148,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: "issues": [ dict( issue, + created=ANY, dismissed=True, dismissed_version=ha_version, ) @@ -188,6 +191,7 @@ async def test_fix_non_existing_issue( "issues": [ dict( issue, + created=ANY, dismissed=False, dismissed_version=None, ) @@ -333,6 +337,7 @@ async def test_step_unauth( assert resp.status == HTTPStatus.UNAUTHORIZED +@freeze_time("2022-07-19 07:53:05") async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: """Test we can list issues.""" assert await async_setup_component(hass, DOMAIN, {}) @@ -389,6 +394,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: "issues": [ dict( issue, + created="2022-07-19T07:53:05+00:00", dismissed=False, dismissed_version=None, ) From 403bbda9592a19ef1f2490de0cc55799256eaf31 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 19 Jul 2022 13:58:39 +0200 Subject: [PATCH 2680/3516] Rename resolution_center dismiss to ignore (#75432) --- .../resolution_center/issue_handler.py | 8 ++- .../resolution_center/issue_registry.py | 9 +-- .../resolution_center/websocket_api.py | 13 ++-- .../components/resolution_center/test_init.py | 60 ++++++++++++------- .../resolution_center/test_issue_registry.py | 4 +- .../resolution_center/test_websocket_api.py | 43 +++++++++++-- 6 files changed, 96 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/resolution_center/issue_handler.py b/homeassistant/components/resolution_center/issue_handler.py index 210a998ce14..78085e2cec6 100644 --- a/homeassistant/components/resolution_center/issue_handler.py +++ b/homeassistant/components/resolution_center/issue_handler.py @@ -128,10 +128,12 @@ def async_delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: @callback -def async_dismiss_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: - """Dismiss an issue. +def async_ignore_issue( + hass: HomeAssistant, domain: str, issue_id: str, ignore: bool +) -> None: + """Ignore an issue. Will raise if the issue does not exist. """ issue_registry = async_get_issue_registry(hass) - issue_registry.async_dismiss(domain, issue_id) + issue_registry.async_ignore(domain, issue_id, ignore) diff --git a/homeassistant/components/resolution_center/issue_registry.py b/homeassistant/components/resolution_center/issue_registry.py index aed1cd51b10..e398d58b3e0 100644 --- a/homeassistant/components/resolution_center/issue_registry.py +++ b/homeassistant/components/resolution_center/issue_registry.py @@ -105,15 +105,16 @@ class IssueRegistry: self.async_schedule_save() @callback - def async_dismiss(self, domain: str, issue_id: str) -> IssueEntry: - """Dismiss issue.""" + def async_ignore(self, domain: str, issue_id: str, ignore: bool) -> IssueEntry: + """Ignore issue.""" old = self.issues[(domain, issue_id)] - if old.dismissed_version == ha_version: + dismissed_version = ha_version if ignore else None + if old.dismissed_version == dismissed_version: return old issue = self.issues[(domain, issue_id)] = dataclasses.replace( old, - dismissed_version=ha_version, + dismissed_version=dismissed_version, ) self.async_schedule_save() diff --git a/homeassistant/components/resolution_center/websocket_api.py b/homeassistant/components/resolution_center/websocket_api.py index 314df009118..e111c2b3e50 100644 --- a/homeassistant/components/resolution_center/websocket_api.py +++ b/homeassistant/components/resolution_center/websocket_api.py @@ -20,14 +20,14 @@ from homeassistant.helpers.data_entry_flow import ( ) from .const import DOMAIN -from .issue_handler import async_dismiss_issue +from .issue_handler import async_ignore_issue from .issue_registry import async_get as async_get_issue_registry @callback def async_setup(hass: HomeAssistant) -> None: """Set up the resolution center websocket API.""" - websocket_api.async_register_command(hass, ws_dismiss_issue) + websocket_api.async_register_command(hass, ws_ignore_issue) websocket_api.async_register_command(hass, ws_list_issues) hass.http.register_view( @@ -41,16 +41,17 @@ def async_setup(hass: HomeAssistant) -> None: @callback @websocket_api.websocket_command( { - vol.Required("type"): "resolution_center/dismiss_issue", + vol.Required("type"): "resolution_center/ignore_issue", vol.Required("domain"): str, vol.Required("issue_id"): str, + vol.Required("ignore"): bool, } ) -def ws_dismiss_issue( +def ws_ignore_issue( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Fix an issue.""" - async_dismiss_issue(hass, msg["domain"], msg["issue_id"]) + async_ignore_issue(hass, msg["domain"], msg["issue_id"], msg["ignore"]) connection.send_result(msg["id"]) @@ -68,7 +69,7 @@ def ws_list_issues( def ws_dict(kv_pairs: list[tuple[Any, Any]]) -> dict[Any, Any]: result = {k: v for k, v in kv_pairs if k not in ("active")} - result["dismissed"] = result["dismissed_version"] is not None + result["ignored"] = result["dismissed_version"] is not None result["created"] = result["created"].isoformat() return result diff --git a/tests/components/resolution_center/test_init.py b/tests/components/resolution_center/test_init.py index a707c3845fe..478c2d51f7f 100644 --- a/tests/components/resolution_center/test_init.py +++ b/tests/components/resolution_center/test_init.py @@ -10,7 +10,7 @@ from homeassistant.components.resolution_center import ( ) from homeassistant.components.resolution_center.const import DOMAIN from homeassistant.components.resolution_center.issue_handler import ( - async_dismiss_issue, + async_ignore_issue, async_process_resolution_center_platforms, ) from homeassistant.const import __version__ as ha_version @@ -78,8 +78,8 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: dict( issue, created="2022-07-19T07:53:05+00:00", - dismissed=False, dismissed_version=None, + ignored=False, ) for issue in issues ] @@ -105,8 +105,8 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: assert msg["result"]["issues"][0] == dict( issues[0], created="2022-07-19T07:53:05+00:00", - dismissed=False, dismissed_version=None, + ignored=False, learn_more_url="blablabla", ) @@ -152,8 +152,8 @@ async def test_create_issue_invalid_version( @freeze_time("2022-07-19 07:53:05") -async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: - """Test dismissing issues.""" +async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: + """Test ignoring issues.""" assert await async_setup_component(hass, DOMAIN, {}) client = await hass_ws_client(hass) @@ -199,16 +199,16 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: dict( issue, created="2022-07-19T07:53:05+00:00", - dismissed=False, dismissed_version=None, + ignored=False, ) for issue in issues ] } - # Dismiss a non-existing issue + # Ignore a non-existing issue with pytest.raises(KeyError): - async_dismiss_issue(hass, issues[0]["domain"], "no_such_issue") + async_ignore_issue(hass, issues[0]["domain"], "no_such_issue", True) await client.send_json({"id": 3, "type": "resolution_center/list_issues"}) msg = await client.receive_json() @@ -219,15 +219,15 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: dict( issue, created="2022-07-19T07:53:05+00:00", - dismissed=False, dismissed_version=None, + ignored=False, ) for issue in issues ] } - # Dismiss an existing issue - async_dismiss_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + # Ignore an existing issue + async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], True) await client.send_json({"id": 4, "type": "resolution_center/list_issues"}) msg = await client.receive_json() @@ -238,15 +238,15 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: dict( issue, created="2022-07-19T07:53:05+00:00", - dismissed=True, dismissed_version=ha_version, + ignored=True, ) for issue in issues ] } - # Dismiss the same issue again - async_dismiss_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + # Ignore the same issue again + async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], True) await client.send_json({"id": 5, "type": "resolution_center/list_issues"}) msg = await client.receive_json() @@ -257,14 +257,14 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: dict( issue, created="2022-07-19T07:53:05+00:00", - dismissed=True, dismissed_version=ha_version, + ignored=True, ) for issue in issues ] } - # Update a dismissed issue + # Update an ignored issue async_create_issue( hass, issues[0]["domain"], @@ -284,11 +284,31 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: assert msg["result"]["issues"][0] == dict( issues[0], created="2022-07-19T07:53:05+00:00", - dismissed=True, dismissed_version=ha_version, + ignored=True, learn_more_url="blablabla", ) + # Unignore the same issue + async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], False) + + await client.send_json({"id": 7, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created="2022-07-19T07:53:05+00:00", + dismissed_version=None, + ignored=False, + learn_more_url="blablabla", + ) + for issue in issues + ] + } + async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> None: """Test we can delete an issue.""" @@ -332,8 +352,8 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non dict( issue, created="2022-07-19T07:53:05+00:00", - dismissed=False, dismissed_version=None, + ignored=False, ) for issue in issues ] @@ -351,8 +371,8 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non dict( issue, created="2022-07-19T07:53:05+00:00", - dismissed=False, dismissed_version=None, + ignored=False, ) for issue in issues ] @@ -401,8 +421,8 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non dict( issue, created="2022-07-19T08:53:05+00:00", - dismissed=False, dismissed_version=None, + ignored=False, ) for issue in issues ] diff --git a/tests/components/resolution_center/test_issue_registry.py b/tests/components/resolution_center/test_issue_registry.py index 3f5cc235a19..854a14fc84e 100644 --- a/tests/components/resolution_center/test_issue_registry.py +++ b/tests/components/resolution_center/test_issue_registry.py @@ -4,7 +4,7 @@ from homeassistant.components.resolution_center import ( issue_registry, ) from homeassistant.components.resolution_center.const import DOMAIN -from homeassistant.components.resolution_center.issue_handler import async_dismiss_issue +from homeassistant.components.resolution_center.issue_handler import async_ignore_issue from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -50,7 +50,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: translation_key=issue["translation_key"], translation_placeholders=issue["translation_placeholders"], ) - async_dismiss_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) + async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], True) registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] assert len(registry.issues) == 2 diff --git a/tests/components/resolution_center/test_websocket_api.py b/tests/components/resolution_center/test_websocket_api.py index 8701996a535..044b0e9832b 100644 --- a/tests/components/resolution_center/test_websocket_api.py +++ b/tests/components/resolution_center/test_websocket_api.py @@ -58,8 +58,8 @@ async def create_issues(hass, ws_client): dict( issue, created=ANY, - dismissed=False, dismissed_version=None, + ignored=False, ) for issue in issues ] @@ -120,9 +120,10 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: await client.send_json( { "id": 2, - "type": "resolution_center/dismiss_issue", + "type": "resolution_center/ignore_issue", "domain": "fake_integration", "issue_id": "no_such_issue", + "ignore": True, } ) msg = await client.receive_json() @@ -131,9 +132,10 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: await client.send_json( { "id": 3, - "type": "resolution_center/dismiss_issue", + "type": "resolution_center/ignore_issue", "domain": "fake_integration", "issue_id": "issue_1", + "ignore": True, } ) msg = await client.receive_json() @@ -149,8 +151,37 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: dict( issue, created=ANY, - dismissed=True, dismissed_version=ha_version, + ignored=True, + ) + for issue in issues + ] + } + + await client.send_json( + { + "id": 5, + "type": "resolution_center/ignore_issue", + "domain": "fake_integration", + "issue_id": "issue_1", + "ignore": False, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] is None + + await client.send_json({"id": 6, "type": "resolution_center/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + dict( + issue, + created=ANY, + dismissed_version=None, + ignored=False, ) for issue in issues ] @@ -192,8 +223,8 @@ async def test_fix_non_existing_issue( dict( issue, created=ANY, - dismissed=False, dismissed_version=None, + ignored=False, ) for issue in issues ] @@ -395,8 +426,8 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: dict( issue, created="2022-07-19T07:53:05+00:00", - dismissed=False, dismissed_version=None, + ignored=False, ) for issue in issues ] From 25b874a6092f463694a12b8de805f4aacc7c5d9e Mon Sep 17 00:00:00 2001 From: Matrix Date: Tue, 19 Jul 2022 20:05:28 +0800 Subject: [PATCH 2681/3516] Fix yolink leak sensor battery expose (#75423) --- homeassistant/components/yolink/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 4679c3e670b..6a7c7ea4cff 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -52,6 +52,7 @@ class YoLinkSensorEntityDescription( SENSOR_DEVICE_TYPE = [ ATTR_DEVICE_DOOR_SENSOR, + ATTR_DEVICE_LEAK_SENSOR, ATTR_DEVICE_MOTION_SENSOR, ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_VIBRATION_SENSOR, From b6d235c0c2c9755f1c54d35808da2307faeeb18d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 19 Jul 2022 15:21:17 +0200 Subject: [PATCH 2682/3516] Improve tradfri decorator typing (#75439) --- homeassistant/components/tradfri/base_class.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index bd1968dfd15..a2b7304cc3e 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -2,7 +2,7 @@ from __future__ import annotations from abc import abstractmethod -from collections.abc import Callable +from collections.abc import Callable, Coroutine from functools import wraps from typing import Any, cast @@ -20,7 +20,7 @@ from .coordinator import TradfriDeviceDataUpdateCoordinator def handle_error( func: Callable[[Command | list[Command]], Any] -) -> Callable[[str], Any]: +) -> Callable[[Command | list[Command]], Coroutine[Any, Any, None]]: """Handle tradfri api call error.""" @wraps(func) From 6b60fb954197003c05789a6f34e91d2989e091eb Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 19 Jul 2022 21:40:23 +0800 Subject: [PATCH 2683/3516] Don't use executor in send_big_result (#75427) --- homeassistant/components/camera/__init__.py | 2 +- homeassistant/components/lovelace/websocket.py | 2 +- homeassistant/components/media_player/__init__.py | 2 +- homeassistant/components/websocket_api/connection.py | 7 ++----- tests/components/websocket_api/test_connection.py | 4 ++-- 5 files changed, 7 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 45b77ec1bd6..35fa6fef1d6 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -804,7 +804,7 @@ async def websocket_camera_thumbnail( _LOGGER.warning("The websocket command 'camera_thumbnail' has been deprecated") try: image = await async_get_image(hass, msg["entity_id"]) - await connection.send_big_result( + connection.send_big_result( msg["id"], { "content_type": image.content_type, diff --git a/homeassistant/components/lovelace/websocket.py b/homeassistant/components/lovelace/websocket.py index cb45d9cfbc6..7e04201291d 100644 --- a/homeassistant/components/lovelace/websocket.py +++ b/homeassistant/components/lovelace/websocket.py @@ -38,7 +38,7 @@ def _handle_errors(func): return if msg is not None: - await connection.send_big_result(msg["id"], result) + connection.send_big_result(msg["id"], result) else: connection.send_result(msg["id"], result) diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 14546a36ec8..5e5347e6806 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -1164,7 +1164,7 @@ async def websocket_handle_thumbnail(hass, connection, msg): ) return - await connection.send_big_result( + connection.send_big_result( msg["id"], { "content_type": content_type, diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 26c4c6f8321..1c26d958969 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -54,12 +54,9 @@ class ActiveConnection: """Send a result message.""" self.send_message(messages.result_message(msg_id, result)) - async def send_big_result(self, msg_id: int, result: Any) -> None: + def send_big_result(self, msg_id: int, result: Any) -> None: """Send a result message that would be expensive to JSON serialize.""" - content = await self.hass.async_add_executor_job( - JSON_DUMP, messages.result_message(msg_id, result) - ) - self.send_message(content) + self.send_message(JSON_DUMP(messages.result_message(msg_id, result))) @callback def send_error(self, msg_id: int, code: str, message: str) -> None: diff --git a/tests/components/websocket_api/test_connection.py b/tests/components/websocket_api/test_connection.py index 3a54d5912e0..da31f0ee8a3 100644 --- a/tests/components/websocket_api/test_connection.py +++ b/tests/components/websocket_api/test_connection.py @@ -17,8 +17,8 @@ async def test_send_big_result(hass, websocket_client): @websocket_api.websocket_command({"type": "big_result"}) @websocket_api.async_response - async def send_big_result(hass, connection, msg): - await connection.send_big_result(msg["id"], {"big": "result"}) + def send_big_result(hass, connection, msg): + connection.send_big_result(msg["id"], {"big": "result"}) websocket_api.async_register_command(hass, send_big_result) From 4b036cbad91c4e6a8eba6952af4a01d39a22a4e2 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 19 Jul 2022 18:33:53 +0200 Subject: [PATCH 2684/3516] Add typing to pilight Throttle decorator (#75443) --- homeassistant/components/pilight/__init__.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py index 38d7a3e18ea..0386d267f04 100644 --- a/homeassistant/components/pilight/__init__.py +++ b/homeassistant/components/pilight/__init__.py @@ -1,11 +1,16 @@ """Component to create an interface to a Pilight daemon.""" +from __future__ import annotations + +from collections.abc import Callable from datetime import timedelta import functools import logging import socket import threading +from typing import Any from pilight import pilight +from typing_extensions import ParamSpec import voluptuous as vol from homeassistant.const import ( @@ -22,6 +27,8 @@ from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util +_P = ParamSpec("_P") + _LOGGER = logging.getLogger(__name__) CONF_SEND_DELAY = "send_delay" @@ -138,23 +145,23 @@ class CallRateDelayThrottle: def __init__(self, hass, delay_seconds: float) -> None: """Initialize the delay handler.""" self._delay = timedelta(seconds=max(0.0, delay_seconds)) - self._queue: list = [] + self._queue: list[Callable[[Any], None]] = [] self._active = False self._lock = threading.Lock() self._next_ts = dt_util.utcnow() self._schedule = functools.partial(track_point_in_utc_time, hass) - def limited(self, method): + def limited(self, method: Callable[_P, Any]) -> Callable[_P, None]: """Decorate to delay calls on a certain method.""" @functools.wraps(method) - def decorated(*args, **kwargs): + def decorated(*args: _P.args, **kwargs: _P.kwargs) -> None: """Delay a call.""" if self._delay.total_seconds() == 0.0: method(*args, **kwargs) return - def action(event): + def action(event: Any) -> None: """Wrap an action that gets scheduled.""" method(*args, **kwargs) From 5ae5ae5392729b4c94a8004bd02e147d60227341 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 19 Jul 2022 18:35:04 +0200 Subject: [PATCH 2685/3516] Improve debouncer typing (#75436) --- .strict-typing | 1 + homeassistant/components/flux_led/number.py | 5 +++-- homeassistant/components/plex/__init__.py | 2 +- homeassistant/components/samsungtv/__init__.py | 4 ++-- homeassistant/components/shelly/__init__.py | 5 +++-- .../components/sonos/household_coordinator.py | 3 ++- homeassistant/components/usb/__init__.py | 5 +++-- homeassistant/helpers/debounce.py | 18 +++++++++++------- homeassistant/helpers/device_registry.py | 3 ++- homeassistant/helpers/update_coordinator.py | 4 ++-- mypy.ini | 3 +++ 11 files changed, 33 insertions(+), 20 deletions(-) diff --git a/.strict-typing b/.strict-typing index aa911dd81d9..f133199a753 100644 --- a/.strict-typing +++ b/.strict-typing @@ -15,6 +15,7 @@ homeassistant.auth.auth_store homeassistant.auth.providers.* homeassistant.helpers.area_registry homeassistant.helpers.condition +homeassistant.helpers.debounce homeassistant.helpers.discovery homeassistant.helpers.entity homeassistant.helpers.entity_values diff --git a/homeassistant/components/flux_led/number.py b/homeassistant/components/flux_led/number.py index 65c8a955dcf..18237c97e94 100644 --- a/homeassistant/components/flux_led/number.py +++ b/homeassistant/components/flux_led/number.py @@ -2,8 +2,9 @@ from __future__ import annotations from abc import abstractmethod +from collections.abc import Coroutine import logging -from typing import cast +from typing import Any, cast from flux_led.protocol import ( MUSIC_PIXELS_MAX, @@ -143,7 +144,7 @@ class FluxConfigNumber( ) -> None: """Initialize the flux number.""" super().__init__(coordinator, base_unique_id, name, key) - self._debouncer: Debouncer | None = None + self._debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None self._pending_value: int | None = None async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 148e6a906bd..9ff8bcf7b54 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -95,7 +95,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.debug("Scanning for GDM clients") gdm.scan(scan_for_clients=True) - hass.data[PLEX_DOMAIN][GDM_DEBOUNCER] = Debouncer( + hass.data[PLEX_DOMAIN][GDM_DEBOUNCER] = Debouncer[None]( hass, _LOGGER, cooldown=10, diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index b870aab62d4..0a7fd4b3378 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -1,7 +1,7 @@ """The Samsung TV integration.""" from __future__ import annotations -from collections.abc import Mapping +from collections.abc import Coroutine, Mapping from functools import partial import socket from typing import Any @@ -131,7 +131,7 @@ class DebouncedEntryReloader: self.hass = hass self.entry = entry self.token = self.entry.data.get(CONF_TOKEN) - self._debounced_reload = Debouncer( + self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer( hass, LOGGER, cooldown=ENTRY_RELOAD_COOLDOWN, diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 012f692c579..125e63449ef 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Coroutine from datetime import timedelta from typing import Any, Final, cast @@ -296,7 +297,7 @@ class BlockDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.entry = entry self.device = device - self._debounced_reload = Debouncer( + self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer( hass, LOGGER, cooldown=ENTRY_RELOAD_COOLDOWN, @@ -636,7 +637,7 @@ class RpcDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.entry = entry self.device = device - self._debounced_reload = Debouncer( + self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer( hass, LOGGER, cooldown=ENTRY_RELOAD_COOLDOWN, diff --git a/homeassistant/components/sonos/household_coordinator.py b/homeassistant/components/sonos/household_coordinator.py index 0d76feae461..51d7e9cec8c 100644 --- a/homeassistant/components/sonos/household_coordinator.py +++ b/homeassistant/components/sonos/household_coordinator.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Coroutine import logging +from typing import Any from soco import SoCo @@ -35,7 +36,7 @@ class SonosHouseholdCoordinator: async def _async_setup(self) -> None: """Finish setup in async context.""" self.cache_update_lock = asyncio.Lock() - self.async_poll = Debouncer( + self.async_poll = Debouncer[Coroutine[Any, Any, None]]( self.hass, _LOGGER, cooldown=3, diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 7a56a659d07..5783401df13 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -1,12 +1,13 @@ """The USB Discovery integration.""" from __future__ import annotations +from collections.abc import Coroutine import dataclasses import fnmatch import logging import os import sys -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from serial.tools.list_ports import comports from serial.tools.list_ports_common import ListPortInfo @@ -109,7 +110,7 @@ class USBDiscovery: self.usb = usb self.seen: set[tuple[str, ...]] = set() self.observer_active = False - self._request_debouncer: Debouncer | None = None + self._request_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None async def async_setup(self) -> None: """Set up USB Discovery.""" diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py index 7937459b50c..2fbdefd7ec0 100644 --- a/homeassistant/helpers/debounce.py +++ b/homeassistant/helpers/debounce.py @@ -2,14 +2,16 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Callable from logging import Logger -from typing import Any +from typing import Generic, TypeVar from homeassistant.core import HassJob, HomeAssistant, callback +_R_co = TypeVar("_R_co", covariant=True) -class Debouncer: + +class Debouncer(Generic[_R_co]): """Class to rate limit calls to a specific command.""" def __init__( @@ -19,7 +21,7 @@ class Debouncer: *, cooldown: float, immediate: bool, - function: Callable[..., Awaitable[Any]] | None = None, + function: Callable[[], _R_co] | None = None, ) -> None: """Initialize debounce. @@ -35,15 +37,17 @@ class Debouncer: self._timer_task: asyncio.TimerHandle | None = None self._execute_at_end_of_timer: bool = False self._execute_lock = asyncio.Lock() - self._job: HassJob | None = None if function is None else HassJob(function) + self._job: HassJob[[], _R_co] | None = ( + None if function is None else HassJob(function) + ) @property - def function(self) -> Callable[..., Awaitable[Any]] | None: + def function(self) -> Callable[[], _R_co] | None: """Return the function being wrapped by the Debouncer.""" return self._function @function.setter - def function(self, function: Callable[..., Awaitable[Any]]) -> None: + def function(self, function: Callable[[], _R_co]) -> None: """Update the function being wrapped by the Debouncer.""" self._function = function if self._job is None or function != self._job.target: diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index ca5e6e1aefa..a19f476495a 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections import OrderedDict +from collections.abc import Coroutine import logging import time from typing import TYPE_CHECKING, Any, NamedTuple, cast @@ -832,7 +833,7 @@ def async_setup_cleanup(hass: HomeAssistant, dev_reg: DeviceRegistry) -> None: ent_reg = entity_registry.async_get(hass) async_cleanup(hass, dev_reg, ent_reg) - debounced_cleanup = Debouncer( + debounced_cleanup: Debouncer[Coroutine[Any, Any, None]] = Debouncer( hass, _LOGGER, cooldown=CLEANUP_DELAY, immediate=False, function=cleanup ) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 30da847642b..768b8040729 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable, Generator +from collections.abc import Awaitable, Callable, Coroutine, Generator from datetime import datetime, timedelta import logging from time import monotonic @@ -44,7 +44,7 @@ class DataUpdateCoordinator(Generic[_T]): name: str, update_interval: timedelta | None = None, update_method: Callable[[], Awaitable[_T]] | None = None, - request_refresh_debouncer: Debouncer | None = None, + request_refresh_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None, ) -> None: """Initialize global data updater.""" self.hass = hass diff --git a/mypy.ini b/mypy.ini index 2333c20c4d8..3355680069e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -57,6 +57,9 @@ disallow_any_generics = true [mypy-homeassistant.helpers.condition] disallow_any_generics = true +[mypy-homeassistant.helpers.debounce] +disallow_any_generics = true + [mypy-homeassistant.helpers.discovery] disallow_any_generics = true From 32311f240b20548d9c619f681833f54cb833557f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Jul 2022 11:50:30 -0500 Subject: [PATCH 2686/3516] Avoid converting discovery_info dataclasses to dict that will be thrown away in config flows (#75451) * Avoid converting BluetoothServiceInfo to a dict for default discovery Fixes ``` 2022-07-19 09:46:48.303 ERROR (MainThread) [homeassistant] Error doing job: Task exception was never retrieved Traceback (most recent call last): File "/Users/bdraco/home-assistant/homeassistant/helpers/discovery_flow.py", line 74, in _async_process_pending_flows await gather_with_concurrency( File "/Users/bdraco/home-assistant/homeassistant/util/async_.py", line 201, in gather_with_concurrency return await gather( File "/Users/bdraco/home-assistant/homeassistant/util/async_.py", line 199, in sem_task return await task File "/Users/bdraco/home-assistant/homeassistant/data_entry_flow.py", line 222, in async_init flow, result = await task File "/Users/bdraco/home-assistant/homeassistant/data_entry_flow.py", line 249, in _async_init result = await self._async_handle_step(flow, flow.init_step, data, init_done) File "/Users/bdraco/home-assistant/homeassistant/data_entry_flow.py", line 359, in _async_handle_step result: FlowResult = await getattr(flow, method)(user_input) File "/Users/bdraco/home-assistant/homeassistant/config_entries.py", line 1484, in async_step_bluetooth return await self.async_step_discovery(dataclasses.asdict(discovery_info)) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/dataclasses.py", line 1239, in asdict return _asdict_inner(obj, dict_factory) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/dataclasses.py", line 1246, in _asdict_inner value = _asdict_inner(getattr(obj, f.name), dict_factory) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/dataclasses.py", line 1280, in _asdict_inner return copy.deepcopy(obj) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 172, in deepcopy y = _reconstruct(x, memo, *rv) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 271, in _reconstruct state = deepcopy(state, memo) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 146, in deepcopy y = copier(x, memo) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 231, in _deepcopy_dict y[deepcopy(key, memo)] = deepcopy(value, memo) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 161, in deepcopy rv = reductor(4) TypeError: Cannot pickle Objective-C objects ``` * Avoid converting BluetoothServiceInfo to a dict for default discovery Fixes ``` 2022-07-19 09:46:48.303 ERROR (MainThread) [homeassistant] Error doing job: Task exception was never retrieved Traceback (most recent call last): File "/Users/bdraco/home-assistant/homeassistant/helpers/discovery_flow.py", line 74, in _async_process_pending_flows await gather_with_concurrency( File "/Users/bdraco/home-assistant/homeassistant/util/async_.py", line 201, in gather_with_concurrency return await gather( File "/Users/bdraco/home-assistant/homeassistant/util/async_.py", line 199, in sem_task return await task File "/Users/bdraco/home-assistant/homeassistant/data_entry_flow.py", line 222, in async_init flow, result = await task File "/Users/bdraco/home-assistant/homeassistant/data_entry_flow.py", line 249, in _async_init result = await self._async_handle_step(flow, flow.init_step, data, init_done) File "/Users/bdraco/home-assistant/homeassistant/data_entry_flow.py", line 359, in _async_handle_step result: FlowResult = await getattr(flow, method)(user_input) File "/Users/bdraco/home-assistant/homeassistant/config_entries.py", line 1484, in async_step_bluetooth return await self.async_step_discovery(dataclasses.asdict(discovery_info)) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/dataclasses.py", line 1239, in asdict return _asdict_inner(obj, dict_factory) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/dataclasses.py", line 1246, in _asdict_inner value = _asdict_inner(getattr(obj, f.name), dict_factory) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/dataclasses.py", line 1280, in _asdict_inner return copy.deepcopy(obj) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 172, in deepcopy y = _reconstruct(x, memo, *rv) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 271, in _reconstruct state = deepcopy(state, memo) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 146, in deepcopy y = copier(x, memo) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 231, in _deepcopy_dict y[deepcopy(key, memo)] = deepcopy(value, memo) File "/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/lib/python3.10/copy.py", line 161, in deepcopy rv = reductor(4) TypeError: Cannot pickle Objective-C objects ``` --- homeassistant/config_entries.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index e5a36b980ed..232d3e5cbf1 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -5,7 +5,6 @@ import asyncio from collections import ChainMap from collections.abc import Callable, Coroutine, Iterable, Mapping from contextvars import ContextVar -import dataclasses from enum import Enum import functools import logging @@ -1446,13 +1445,19 @@ class ConfigFlow(data_entry_flow.FlowHandler): if self._async_in_progress(include_uninitialized=True): raise data_entry_flow.AbortFlow("already_in_progress") - async def async_step_discovery( - self, discovery_info: DiscoveryInfoType + async def _async_step_discovery_without_unique_id( + self, ) -> data_entry_flow.FlowResult: """Handle a flow initialized by discovery.""" await self._async_handle_discovery_without_unique_id() return await self.async_step_user() + async def async_step_discovery( + self, discovery_info: DiscoveryInfoType + ) -> data_entry_flow.FlowResult: + """Handle a flow initialized by discovery.""" + return await self._async_step_discovery_without_unique_id() + @callback def async_abort( self, @@ -1481,55 +1486,55 @@ class ConfigFlow(data_entry_flow.FlowHandler): self, discovery_info: BluetoothServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by Bluetooth discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() async def async_step_dhcp( self, discovery_info: DhcpServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by DHCP discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() async def async_step_hassio( self, discovery_info: HassioServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by HASS IO discovery.""" - return await self.async_step_discovery(discovery_info.config) + return await self._async_step_discovery_without_unique_id() async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType ) -> data_entry_flow.FlowResult: """Handle a flow initialized by integration specific discovery.""" - return await self.async_step_discovery(discovery_info) + return await self._async_step_discovery_without_unique_id() async def async_step_homekit( self, discovery_info: ZeroconfServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by Homekit discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() async def async_step_mqtt( self, discovery_info: MqttServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by MQTT discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() async def async_step_ssdp( self, discovery_info: SsdpServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by SSDP discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() async def async_step_usb( self, discovery_info: UsbServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by USB discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() async def async_step_zeroconf( self, discovery_info: ZeroconfServiceInfo ) -> data_entry_flow.FlowResult: """Handle a flow initialized by Zeroconf discovery.""" - return await self.async_step_discovery(dataclasses.asdict(discovery_info)) + return await self._async_step_discovery_without_unique_id() @callback def async_create_entry( From e02a24529fa01075f0f3daa5906fac1aca71c575 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 19 Jul 2022 18:53:19 +0200 Subject: [PATCH 2687/3516] Update mypy to 0.971 (#75450) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index adecd327631..2331e971711 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -11,7 +11,7 @@ codecov==2.1.12 coverage==6.4.2 freezegun==1.2.1 mock-open==1.4.0 -mypy==0.961 +mypy==0.971 pre-commit==2.20.0 pylint==2.14.4 pipdeptree==2.2.1 From e4f6f738e83b2b3d5e786d42c53dea867a435b29 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 19 Jul 2022 18:56:41 +0200 Subject: [PATCH 2688/3516] Bump python-miio to 0.5.12 (#75415) * Bump python-miio to 0.5.12 * Fix imports --- homeassistant/components/xiaomi_miio/fan.py | 16 +++++++++++---- .../components/xiaomi_miio/humidifier.py | 12 ++++++++--- .../components/xiaomi_miio/manifest.json | 2 +- .../components/xiaomi_miio/select.py | 20 ++++++++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 39 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 177f84679ee..aa4b8a8a1bc 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -7,14 +7,22 @@ import logging import math from typing import Any -from miio.airfresh import OperationMode as AirfreshOperationMode -from miio.airfresh_t2017 import OperationMode as AirfreshOperationModeT2017 -from miio.airpurifier import OperationMode as AirpurifierOperationMode -from miio.airpurifier_miot import OperationMode as AirpurifierMiotOperationMode from miio.fan_common import ( MoveDirection as FanMoveDirection, OperationMode as FanOperationMode, ) +from miio.integrations.airpurifier.dmaker.airfresh_t2017 import ( + OperationMode as AirfreshOperationModeT2017, +) +from miio.integrations.airpurifier.zhimi.airfresh import ( + OperationMode as AirfreshOperationMode, +) +from miio.integrations.airpurifier.zhimi.airpurifier import ( + OperationMode as AirpurifierOperationMode, +) +from miio.integrations.airpurifier.zhimi.airpurifier_miot import ( + OperationMode as AirpurifierMiotOperationMode, +) from miio.integrations.fan.zhimi.zhimi_miot import ( OperationModeFanZA5 as FanZA5OperationMode, ) diff --git a/homeassistant/components/xiaomi_miio/humidifier.py b/homeassistant/components/xiaomi_miio/humidifier.py index 0a9543ac604..b5a5e738ea0 100644 --- a/homeassistant/components/xiaomi_miio/humidifier.py +++ b/homeassistant/components/xiaomi_miio/humidifier.py @@ -2,9 +2,15 @@ import logging import math -from miio.airhumidifier import OperationMode as AirhumidifierOperationMode -from miio.airhumidifier_miot import OperationMode as AirhumidifierMiotOperationMode -from miio.airhumidifier_mjjsq import OperationMode as AirhumidifierMjjsqOperationMode +from miio.integrations.humidifier.deerma.airhumidifier_mjjsq import ( + OperationMode as AirhumidifierMjjsqOperationMode, +) +from miio.integrations.humidifier.zhimi.airhumidifier import ( + OperationMode as AirhumidifierOperationMode, +) +from miio.integrations.humidifier.zhimi.airhumidifier_miot import ( + OperationMode as AirhumidifierMiotOperationMode, +) from homeassistant.components.humidifier import ( HumidifierDeviceClass, diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 7157e32299a..0f1a9dd92aa 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.5", "python-miio==0.5.11"], + "requirements": ["construct==2.10.56", "micloud==0.5", "python-miio==0.5.12"], "codeowners": ["@rytilahti", "@syssi", "@starkillerOG", "@bieniu"], "zeroconf": ["_miio._udp.local."], "iot_class": "local_polling", diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 5f8fe8df591..b7e6a65775e 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -3,12 +3,22 @@ from __future__ import annotations from dataclasses import dataclass -from miio.airfresh import LedBrightness as AirfreshLedBrightness -from miio.airhumidifier import LedBrightness as AirhumidifierLedBrightness -from miio.airhumidifier_miot import LedBrightness as AirhumidifierMiotLedBrightness -from miio.airpurifier import LedBrightness as AirpurifierLedBrightness -from miio.airpurifier_miot import LedBrightness as AirpurifierMiotLedBrightness from miio.fan_common import LedBrightness as FanLedBrightness +from miio.integrations.airpurifier.zhimi.airfresh import ( + LedBrightness as AirfreshLedBrightness, +) +from miio.integrations.airpurifier.zhimi.airpurifier import ( + LedBrightness as AirpurifierLedBrightness, +) +from miio.integrations.airpurifier.zhimi.airpurifier_miot import ( + LedBrightness as AirpurifierMiotLedBrightness, +) +from miio.integrations.humidifier.zhimi.airhumidifier import ( + LedBrightness as AirhumidifierLedBrightness, +) +from miio.integrations.humidifier.zhimi.airhumidifier_miot import ( + LedBrightness as AirhumidifierMiotLedBrightness, +) from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry diff --git a/requirements_all.txt b/requirements_all.txt index d43ee87da12..0606ec11933 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1930,7 +1930,7 @@ python-kasa==0.5.0 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.5.11 +python-miio==0.5.12 # homeassistant.components.mpd python-mpd2==3.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ee02d6f05a2..8148e114d6b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1298,7 +1298,7 @@ python-juicenet==1.1.0 python-kasa==0.5.0 # homeassistant.components.xiaomi_miio -python-miio==0.5.11 +python-miio==0.5.12 # homeassistant.components.nest python-nest==4.2.0 From 503e88642e10281a463e681b278b2ea77d250f94 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 19 Jul 2022 21:06:18 +0200 Subject: [PATCH 2689/3516] Update pyupgrade to 2.37.2 (#75456) --- .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 51429bdf94b..8e259c1d063 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.37.1 + rev: v2.37.2 hooks: - id: pyupgrade args: [--py39-plus] diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index b1a9a0a1a5b..c7f5d559c38 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -12,5 +12,5 @@ mccabe==0.6.1 pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 -pyupgrade==2.37.1 +pyupgrade==2.37.2 yamllint==1.27.1 From c29bd483735aa81c9ef572b0815c8a293f4f9046 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 20 Jul 2022 00:22:51 +0000 Subject: [PATCH 2690/3516] [ci skip] Translation update --- .../components/google/translations/fr.json | 3 ++- .../components/google/translations/ru.json | 3 ++- .../homekit_controller/translations/fr.json | 4 ++-- .../components/lifx/translations/ca.json | 20 +++++++++++++++++++ .../components/lifx/translations/de.json | 20 +++++++++++++++++++ .../components/lifx/translations/et.json | 20 +++++++++++++++++++ .../components/lifx/translations/fr.json | 20 +++++++++++++++++++ .../components/lifx/translations/ru.json | 20 +++++++++++++++++++ 8 files changed, 106 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/google/translations/fr.json b/homeassistant/components/google/translations/fr.json index 7c5e4927787..389f769cdb9 100644 --- a/homeassistant/components/google/translations/fr.json +++ b/homeassistant/components/google/translations/fr.json @@ -11,7 +11,8 @@ "invalid_access_token": "Jeton d'acc\u00e8s non valide", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", "oauth_error": "Des donn\u00e9es de jeton non valides ont \u00e9t\u00e9 re\u00e7ues.", - "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "timeout_connect": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9" }, "create_entry": { "default": "Authentification r\u00e9ussie" diff --git a/homeassistant/components/google/translations/ru.json b/homeassistant/components/google/translations/ru.json index bb47301624f..5d9f51fe14e 100644 --- a/homeassistant/components/google/translations/ru.json +++ b/homeassistant/components/google/translations/ru.json @@ -8,7 +8,8 @@ "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", "oauth_error": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u044b \u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0442\u043e\u043a\u0435\u043d\u0430.", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", + "timeout_connect": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." }, "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." diff --git a/homeassistant/components/homekit_controller/translations/fr.json b/homeassistant/components/homekit_controller/translations/fr.json index 01286ceb991..b46069de2b8 100644 --- a/homeassistant/components/homekit_controller/translations/fr.json +++ b/homeassistant/components/homekit_controller/translations/fr.json @@ -18,7 +18,7 @@ "unable_to_pair": "Impossible d'appairer, veuillez r\u00e9essayer.", "unknown_error": "L'appareil a signal\u00e9 une erreur inconnue. L'appairage a \u00e9chou\u00e9." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Annulez l'association sur tous les contr\u00f4leurs ou essayez de red\u00e9marrer l'appareil, puis continuez \u00e0 reprendre l'association.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Autoriser le jumelage avec des codes de configuration non s\u00e9curis\u00e9s.", "pairing_code": "Code d\u2019appairage" }, - "description": "Le contr\u00f4leur HomeKit communique avec {name} sur le r\u00e9seau local en utilisant une connexion crypt\u00e9e s\u00e9curis\u00e9e sans contr\u00f4leur HomeKit s\u00e9par\u00e9 ou iCloud. Entrez votre code d'appariement HomeKit (au format XXX-XX-XXX) pour utiliser cet accessoire. Ce code se trouve g\u00e9n\u00e9ralement sur l'appareil lui-m\u00eame ou dans l'emballage.", + "description": "Le contr\u00f4leur HomeKit communique avec {name} ({category}) sur le r\u00e9seau local en utilisant une connexion chiffr\u00e9e s\u00e9curis\u00e9e sans contr\u00f4leur HomeKit s\u00e9par\u00e9 ni iCloud. Saisissez votre code d'appairage HomeKit (au format XXX-XX-XXX) pour utiliser cet accessoire. Ce code se trouve g\u00e9n\u00e9ralement sur l'appareil lui-m\u00eame ou dans l'emballage.", "title": "Couplage avec un appareil via le protocole accessoire HomeKit" }, "protocol_error": { diff --git a/homeassistant/components/lifx/translations/ca.json b/homeassistant/components/lifx/translations/ca.json index 28c25cde70a..8d0efc4de8a 100644 --- a/homeassistant/components/lifx/translations/ca.json +++ b/homeassistant/components/lifx/translations/ca.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "no_devices_found": "No s'han trobat dispositius a la xarxa", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Vols configurar LIFX?" + }, + "discovery_confirm": { + "description": "Vols configurar {label} ({host}) {serial}?" + }, + "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/lifx/translations/de.json b/homeassistant/components/lifx/translations/de.json index 0c619ea4062..82e37b39c8b 100644 --- a/homeassistant/components/lifx/translations/de.json +++ b/homeassistant/components/lifx/translations/de.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "M\u00f6chtest du LIFX einrichten?" + }, + "discovery_confirm": { + "description": "M\u00f6chtest du {label} ({host}) {serial} 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/lifx/translations/et.json b/homeassistant/components/lifx/translations/et.json index ba833f79f8b..6d06cbb17ba 100644 --- a/homeassistant/components/lifx/translations/et.json +++ b/homeassistant/components/lifx/translations/et.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine juba k\u00e4ib", "no_devices_found": "V\u00f5rgust ei leitud seadmeid", "single_instance_allowed": "Juba seadistatud, lubatud on ainult \u00fcks sidumine." }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Kas soovid seadistada LIFX-i?" + }, + "discovery_confirm": { + "description": "Kas seadistada {label} ( {host} ) {serial} ?" + }, + "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/lifx/translations/fr.json b/homeassistant/components/lifx/translations/fr.json index f8cc0a9dddd..c3f0561b085 100644 --- a/homeassistant/components/lifx/translations/fr.json +++ b/homeassistant/components/lifx/translations/fr.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Voulez-vous configurer LIFX?" + }, + "discovery_confirm": { + "description": "Voulez-vous configurer {label} ({host}) {serial}\u00a0?" + }, + "pick_device": { + "data": { + "device": "Appareil" + } + }, + "user": { + "data": { + "host": "H\u00f4te" + }, + "description": "Si vous laissez l'h\u00f4te vide, la d\u00e9couverte sera utilis\u00e9e pour trouver des appareils." } } } diff --git a/homeassistant/components/lifx/translations/ru.json b/homeassistant/components/lifx/translations/ru.json index 0d50dec498b..9e9a9460e19 100644 --- a/homeassistant/components/lifx/translations/ru.json +++ b/homeassistant/components/lifx/translations/ru.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", "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": "{label} ({host}) {serial}", "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 LIFX?" + }, + "discovery_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {label} ({host}) {serial}?" + }, + "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." } } } From 672883e19d1bdd2c26aa1f2f515a8fbc702bb518 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Jul 2022 02:50:37 +0200 Subject: [PATCH 2691/3516] Remove old type casting in esphome (#75475) --- homeassistant/components/esphome/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index bddb56a38d3..3d8455d3150 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -562,13 +562,11 @@ async def platform_async_setup_entry( """Update entities of this platform when entities are listed.""" old_infos = entry_data.info[component_key] new_infos: dict[int, EntityInfo] = {} - add_entities = [] + add_entities: list[_EntityT] = [] for info in infos: if not isinstance(info, info_type): # Filter out infos that don't belong to this platform. continue - # cast back to upper type, otherwise mypy gets confused - info = cast(EntityInfo, info) if info.key in old_infos: # Update existing entity From b04c3e9adc0e4eda2aa530eb116b6dc5f56d8822 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Jul 2022 02:54:46 +0200 Subject: [PATCH 2692/3516] Improve deprecation helper typing (#75453) --- .strict-typing | 1 + homeassistant/helpers/deprecation.py | 32 +++++++++++++++++++--------- mypy.ini | 3 +++ 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/.strict-typing b/.strict-typing index f133199a753..c184990884f 100644 --- a/.strict-typing +++ b/.strict-typing @@ -16,6 +16,7 @@ homeassistant.auth.providers.* homeassistant.helpers.area_registry homeassistant.helpers.condition homeassistant.helpers.debounce +homeassistant.helpers.deprecation homeassistant.helpers.discovery homeassistant.helpers.entity homeassistant.helpers.entity_values diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 4bf57c1a4e1..8d961d7008b 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -5,12 +5,20 @@ from collections.abc import Callable import functools import inspect import logging -from typing import Any +from typing import Any, TypeVar + +from typing_extensions import ParamSpec from ..helpers.frame import MissingIntegrationFrame, get_integration_frame +_ObjectT = TypeVar("_ObjectT", bound=object) +_R = TypeVar("_R") +_P = ParamSpec("_P") -def deprecated_substitute(substitute_name: str) -> Callable[..., Callable]: + +def deprecated_substitute( + substitute_name: str, +) -> Callable[[Callable[[_ObjectT], Any]], Callable[[_ObjectT], Any]]: """Help migrate properties to new names. When a property is added to replace an older property, this decorator can @@ -19,10 +27,10 @@ def deprecated_substitute(substitute_name: str) -> Callable[..., Callable]: warning will be issued alerting the user of the impending change. """ - def decorator(func: Callable) -> Callable: + def decorator(func: Callable[[_ObjectT], Any]) -> Callable[[_ObjectT], Any]: """Decorate function as deprecated.""" - def func_wrapper(self: Callable) -> Any: + def func_wrapper(self: _ObjectT) -> Any: """Wrap for the original function.""" if hasattr(self, substitute_name): # If this platform is still using the old property, issue @@ -81,14 +89,16 @@ def get_deprecated( return config.get(new_name, default) -def deprecated_class(replacement: str) -> Any: +def deprecated_class( + replacement: str, +) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: """Mark class as deprecated and provide a replacement class to be used instead.""" - def deprecated_decorator(cls: Any) -> Any: + def deprecated_decorator(cls: Callable[_P, _R]) -> Callable[_P, _R]: """Decorate class as deprecated.""" @functools.wraps(cls) - def deprecated_cls(*args: Any, **kwargs: Any) -> Any: + def deprecated_cls(*args: _P.args, **kwargs: _P.kwargs) -> _R: """Wrap for the original class.""" _print_deprecation_warning(cls, replacement, "class") return cls(*args, **kwargs) @@ -98,14 +108,16 @@ def deprecated_class(replacement: str) -> Any: return deprecated_decorator -def deprecated_function(replacement: str) -> Callable[..., Callable]: +def deprecated_function( + replacement: str, +) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: """Mark function as deprecated and provide a replacement function to be used instead.""" - def deprecated_decorator(func: Callable) -> Callable: + def deprecated_decorator(func: Callable[_P, _R]) -> Callable[_P, _R]: """Decorate function as deprecated.""" @functools.wraps(func) - def deprecated_func(*args: Any, **kwargs: Any) -> Any: + def deprecated_func(*args: _P.args, **kwargs: _P.kwargs) -> _R: """Wrap for the original function.""" _print_deprecation_warning(func, replacement, "function") return func(*args, **kwargs) diff --git a/mypy.ini b/mypy.ini index 3355680069e..6321ee04b98 100644 --- a/mypy.ini +++ b/mypy.ini @@ -60,6 +60,9 @@ disallow_any_generics = true [mypy-homeassistant.helpers.debounce] disallow_any_generics = true +[mypy-homeassistant.helpers.deprecation] +disallow_any_generics = true + [mypy-homeassistant.helpers.discovery] disallow_any_generics = true From d09fff595cf2ca3e7735309909f41501d83a75a0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Jul 2022 03:03:22 +0200 Subject: [PATCH 2693/3516] Rename existing TypeVars referencing Self type (#75473) --- homeassistant/backports/enum.py | 6 +++--- homeassistant/components/esphome/__init__.py | 6 +++--- homeassistant/components/zha/core/channels/__init__.py | 10 ++++++---- homeassistant/config_entries.py | 6 ++++-- homeassistant/helpers/restore_state.py | 4 ++-- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/homeassistant/backports/enum.py b/homeassistant/backports/enum.py index 9a96704a836..939d6e7669b 100644 --- a/homeassistant/backports/enum.py +++ b/homeassistant/backports/enum.py @@ -4,15 +4,15 @@ from __future__ import annotations from enum import Enum from typing import Any, TypeVar -_StrEnumT = TypeVar("_StrEnumT", bound="StrEnum") +_StrEnumSelfT = TypeVar("_StrEnumSelfT", bound="StrEnum") class StrEnum(str, Enum): """Partial backport of Python 3.11's StrEnum for our basic use cases.""" def __new__( - cls: type[_StrEnumT], value: str, *args: Any, **kwargs: Any - ) -> _StrEnumT: + cls: type[_StrEnumSelfT], value: str, *args: Any, **kwargs: Any + ) -> _StrEnumSelfT: """Create a new StrEnum instance.""" if not isinstance(value, str): raise TypeError(f"{value!r} is not a string") diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 3d8455d3150..5d7b0efc18d 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -58,7 +58,7 @@ from .entry_data import RuntimeEntryData DOMAIN = "esphome" CONF_NOISE_PSK = "noise_psk" _LOGGER = logging.getLogger(__name__) -_T = TypeVar("_T") +_DomainDataSelfT = TypeVar("_DomainDataSelfT", bound="DomainData") STORAGE_VERSION = 1 @@ -101,11 +101,11 @@ class DomainData: ) @classmethod - def get(cls: type[_T], hass: HomeAssistant) -> _T: + def get(cls: type[_DomainDataSelfT], hass: HomeAssistant) -> _DomainDataSelfT: """Get the global DomainData instance stored in hass.data.""" # Don't use setdefault - this is a hot code path if DOMAIN in hass.data: - return cast(_T, hass.data[DOMAIN]) + return cast(_DomainDataSelfT, hass.data[DOMAIN]) ret = hass.data[DOMAIN] = cls() return ret diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 9042856b456..849cdc29e47 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -36,8 +36,8 @@ if TYPE_CHECKING: from ...entity import ZhaEntity from ..device import ZHADevice -_ChannelsT = TypeVar("_ChannelsT", bound="Channels") -_ChannelPoolT = TypeVar("_ChannelPoolT", bound="ChannelPool") +_ChannelsSelfT = TypeVar("_ChannelsSelfT", bound="Channels") +_ChannelPoolSelfT = TypeVar("_ChannelPoolSelfT", bound="ChannelPool") _ChannelsDictType = dict[str, base.ZigbeeChannel] @@ -104,7 +104,7 @@ class Channels: } @classmethod - def new(cls: type[_ChannelsT], zha_device: ZHADevice) -> _ChannelsT: + def new(cls: type[_ChannelsSelfT], zha_device: ZHADevice) -> _ChannelsSelfT: """Create new instance.""" channels = cls(zha_device) for ep_id in sorted(zha_device.device.endpoints): @@ -272,7 +272,9 @@ class ChannelPool: ) @classmethod - def new(cls: type[_ChannelPoolT], channels: Channels, ep_id: int) -> _ChannelPoolT: + def new( + cls: type[_ChannelPoolSelfT], channels: Channels, ep_id: int + ) -> _ChannelPoolSelfT: """Create new channels for an endpoint.""" pool = cls(channels, ep_id) pool.add_all_channels() diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 232d3e5cbf1..b5d5804f100 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -73,7 +73,7 @@ PATH_CONFIG = ".config_entries.json" SAVE_DELAY = 1 -_T = TypeVar("_T", bound="ConfigEntryState") +_ConfigEntryStateSelfT = TypeVar("_ConfigEntryStateSelfT", bound="ConfigEntryState") _R = TypeVar("_R") @@ -97,7 +97,9 @@ class ConfigEntryState(Enum): _recoverable: bool - def __new__(cls: type[_T], value: str, recoverable: bool) -> _T: + def __new__( + cls: type[_ConfigEntryStateSelfT], value: str, recoverable: bool + ) -> _ConfigEntryStateSelfT: """Create new ConfigEntryState.""" obj = object.__new__(cls) obj._value_ = value diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 4f2d1dd0503..314daecde48 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -32,7 +32,7 @@ STATE_DUMP_INTERVAL = timedelta(minutes=15) # How long should a saved state be preserved if the entity no longer exists STATE_EXPIRATION = timedelta(days=7) -_StoredStateT = TypeVar("_StoredStateT", bound="StoredState") +_StoredStateSelfT = TypeVar("_StoredStateSelfT", bound="StoredState") class ExtraStoredData: @@ -82,7 +82,7 @@ class StoredState: return result @classmethod - def from_dict(cls: type[_StoredStateT], json_dict: dict) -> _StoredStateT: + def from_dict(cls: type[_StoredStateSelfT], json_dict: dict) -> _StoredStateSelfT: """Initialize a stored state from a dict.""" extra_data_dict = json_dict.get("extra_data") extra_data = RestoredExtraData(extra_data_dict) if extra_data_dict else None From 07b4d48e7c1cadb84a70026bf20d46024387b0d1 Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 19 Jul 2022 21:23:14 -0400 Subject: [PATCH 2694/3516] Disable Aladdin Connect battery_level by default (#75441) * Disable battery_level by default * Removed async_setup_compnent, renamed constant. --- .../components/aladdin_connect/sensor.py | 1 + tests/components/aladdin_connect/test_init.py | 12 +-- .../components/aladdin_connect/test_sensor.py | 85 +++++++++++++++++++ 3 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 tests/components/aladdin_connect/test_sensor.py diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py index d9783d0f61d..68631c57fc8 100644 --- a/homeassistant/components/aladdin_connect/sensor.py +++ b/homeassistant/components/aladdin_connect/sensor.py @@ -42,6 +42,7 @@ SENSORS: tuple[AccSensorEntityDescription, ...] = ( key="battery_level", name="Battery level", device_class=SensorDeviceClass.BATTERY, + entity_registry_enabled_default=False, native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value_fn=AladdinConnectClient.get_battery_status, diff --git a/tests/components/aladdin_connect/test_init.py b/tests/components/aladdin_connect/test_init.py index 4c422ae29ba..4cbb2333cc5 100644 --- a/tests/components/aladdin_connect/test_init.py +++ b/tests/components/aladdin_connect/test_init.py @@ -9,14 +9,14 @@ from homeassistant.core import HomeAssistant from tests.common import AsyncMock, MockConfigEntry -YAML_CONFIG = {"username": "test-user", "password": "test-password"} +CONFIG = {"username": "test-user", "password": "test-password"} async def test_setup_get_doors_errors(hass: HomeAssistant) -> None: """Test component setup Get Doors Errors.""" config_entry = MockConfigEntry( domain=DOMAIN, - data=YAML_CONFIG, + data=CONFIG, unique_id="test-id", ) config_entry.add_to_hass(hass) @@ -38,7 +38,7 @@ async def test_setup_login_error( """Test component setup Login Errors.""" config_entry = MockConfigEntry( domain=DOMAIN, - data=YAML_CONFIG, + data=CONFIG, unique_id="test-id", ) config_entry.add_to_hass(hass) @@ -57,7 +57,7 @@ async def test_setup_connection_error( """Test component setup Login Errors.""" config_entry = MockConfigEntry( domain=DOMAIN, - data=YAML_CONFIG, + data=CONFIG, unique_id="test-id", ) config_entry.add_to_hass(hass) @@ -74,7 +74,7 @@ async def test_setup_component_no_error(hass: HomeAssistant) -> None: """Test component setup No Error.""" config_entry = MockConfigEntry( domain=DOMAIN, - data=YAML_CONFIG, + data=CONFIG, unique_id="test-id", ) config_entry.add_to_hass(hass) @@ -116,7 +116,7 @@ async def test_load_and_unload( """Test loading and unloading Aladdin Connect entry.""" config_entry = MockConfigEntry( domain=DOMAIN, - data=YAML_CONFIG, + data=CONFIG, unique_id="test-id", ) config_entry.add_to_hass(hass) diff --git a/tests/components/aladdin_connect/test_sensor.py b/tests/components/aladdin_connect/test_sensor.py new file mode 100644 index 00000000000..3702bcd9efa --- /dev/null +++ b/tests/components/aladdin_connect/test_sensor.py @@ -0,0 +1,85 @@ +"""Test the Aladdin Connect Sensors.""" +from datetime import timedelta +from unittest.mock import MagicMock, patch + +from homeassistant.components.aladdin_connect.const import DOMAIN +from homeassistant.components.aladdin_connect.cover import SCAN_INTERVAL +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry +from homeassistant.util.dt import utcnow + +from tests.common import MockConfigEntry, async_fire_time_changed + +CONFIG = {"username": "test-user", "password": "test-password"} +RELOAD_AFTER_UPDATE_DELAY = timedelta(seconds=31) + + +async def test_sensors( + hass: HomeAssistant, + mock_aladdinconnect_api: MagicMock, +) -> None: + """Test Sensors for AladdinConnect.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + + await hass.async_block_till_done() + + with patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + registry = entity_registry.async_get(hass) + entry = registry.async_get("sensor.home_battery_level") + assert entry + assert entry.disabled + assert entry.disabled_by is entity_registry.RegistryEntryDisabler.INTEGRATION + update_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert update_entry != entry + assert update_entry.disabled is False + state = hass.states.get("sensor.home_battery_level") + assert state is None + + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + state = hass.states.get("sensor.home_battery_level") + assert state + + entry = registry.async_get("sensor.home_wi_fi_rssi") + await hass.async_block_till_done() + assert entry + assert entry.disabled + assert entry.disabled_by is entity_registry.RegistryEntryDisabler.INTEGRATION + update_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert update_entry != entry + assert update_entry.disabled is False + state = hass.states.get("sensor.home_wi_fi_rssi") + assert state is None + + update_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.home_wi_fi_rssi") + assert state From 3193ea3359573f35e6929015f39c15b9f7170e59 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Jul 2022 04:00:14 +0200 Subject: [PATCH 2695/3516] Fix type narrowing in energy integration (#75462) --- homeassistant/components/energy/data.py | 2 +- homeassistant/components/energy/validate.py | 23 +++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py index e8c62da0c3c..3b3952136be 100644 --- a/homeassistant/components/energy/data.py +++ b/homeassistant/components/energy/data.py @@ -54,7 +54,7 @@ class FlowToGridSourceType(TypedDict): stat_compensation: str | None # Used to generate costs if stat_compensation is set to None - entity_energy_from: str | None # entity_id of an energy meter (kWh), entity_id of the energy meter for stat_energy_from + entity_energy_to: str | None # entity_id of an energy meter (kWh), entity_id of the energy meter for stat_energy_to entity_energy_price: str | None # entity_id of an entity providing price ($/kWh) number_energy_price: float | None # Price for energy ($/kWh) diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index 3baae348770..02549ddfe96 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -263,10 +263,10 @@ def _async_validate_auto_generated_cost_entity( async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: """Validate the energy configuration.""" - manager = await data.async_get_manager(hass) + manager: data.EnergyManager = await data.async_get_manager(hass) statistics_metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]] = {} validate_calls = [] - wanted_statistics_metadata = set() + wanted_statistics_metadata: set[str] = set() result = EnergyPreferencesValidation() @@ -279,6 +279,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: result.energy_sources.append(source_result) if source["type"] == "grid": + flow: data.FlowFromGridSourceType | data.FlowToGridSourceType for flow in source["flow_from"]: wanted_statistics_metadata.add(flow["stat_energy_from"]) validate_calls.append( @@ -294,14 +295,14 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) ) - if flow.get("stat_cost") is not None: - wanted_statistics_metadata.add(flow["stat_cost"]) + if (stat_cost := flow.get("stat_cost")) is not None: + wanted_statistics_metadata.add(stat_cost) validate_calls.append( functools.partial( _async_validate_cost_stat, hass, statistics_metadata, - flow["stat_cost"], + stat_cost, source_result, ) ) @@ -345,14 +346,14 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) ) - if flow.get("stat_compensation") is not None: - wanted_statistics_metadata.add(flow["stat_compensation"]) + if (stat_compensation := flow.get("stat_compensation")) is not None: + wanted_statistics_metadata.add(stat_compensation) validate_calls.append( functools.partial( _async_validate_cost_stat, hass, statistics_metadata, - flow["stat_compensation"], + stat_compensation, source_result, ) ) @@ -396,14 +397,14 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) ) - if source.get("stat_cost") is not None: - wanted_statistics_metadata.add(source["stat_cost"]) + if (stat_cost := source.get("stat_cost")) is not None: + wanted_statistics_metadata.add(stat_cost) validate_calls.append( functools.partial( _async_validate_cost_stat, hass, statistics_metadata, - source["stat_cost"], + stat_cost, source_result, ) ) From 1a1eeb2274903cb18f4fb99a4ea3af94f4a04059 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Jul 2022 04:02:03 +0200 Subject: [PATCH 2696/3516] Allow for subclass typing with StatisticsBase (#75476) --- homeassistant/components/recorder/db_schema.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 36487353e25..4777eeb500e 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from datetime import datetime, timedelta import logging -from typing import Any, cast +from typing import Any, TypeVar, cast import ciso8601 from fnvhash import fnv1a_32 @@ -55,6 +55,8 @@ Base = declarative_base() SCHEMA_VERSION = 29 +_StatisticsBaseSelfT = TypeVar("_StatisticsBaseSelfT", bound="StatisticsBase") + _LOGGER = logging.getLogger(__name__) TABLE_EVENTS = "events" @@ -443,7 +445,9 @@ class StatisticsBase: sum = Column(DOUBLE_TYPE) @classmethod - def from_stats(cls, metadata_id: int, stats: StatisticData) -> StatisticsBase: + def from_stats( + cls: type[_StatisticsBaseSelfT], metadata_id: int, stats: StatisticData + ) -> _StatisticsBaseSelfT: """Create object from a statistics.""" return cls( # type: ignore[call-arg,misc] metadata_id=metadata_id, From 1626c53c13866cc1fb0b19db2175dcde6af5f097 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Jul 2022 04:11:46 +0200 Subject: [PATCH 2697/3516] Improve dispatcher helper typing (#75455) * Improve dispatcher helper typing * Code review --- .strict-typing | 1 + homeassistant/helpers/dispatcher.py | 14 +++++++++----- mypy.ini | 3 +++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.strict-typing b/.strict-typing index c184990884f..9757a5f6c03 100644 --- a/.strict-typing +++ b/.strict-typing @@ -18,6 +18,7 @@ homeassistant.helpers.condition homeassistant.helpers.debounce homeassistant.helpers.deprecation homeassistant.helpers.discovery +homeassistant.helpers.dispatcher homeassistant.helpers.entity homeassistant.helpers.entity_values homeassistant.helpers.event diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index 07a3e3b3b28..c7ad4fb1adf 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -1,7 +1,7 @@ """Helpers for Home Assistant dispatcher & internal component/platform.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Coroutine import logging from typing import Any @@ -62,7 +62,9 @@ def dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: hass.loop.call_soon_threadsafe(async_dispatcher_send, hass, signal, *args) -def _generate_job(signal: str, target: Callable[..., Any]) -> HassJob: +def _generate_job( + signal: str, target: Callable[..., Any] +) -> HassJob[..., None | Coroutine[Any, Any, None]]: """Generate a HassJob for a signal and target.""" return HassJob( catch_log_exception( @@ -84,16 +86,18 @@ def async_dispatcher_send(hass: HomeAssistant, signal: str, *args: Any) -> None: This method must be run in the event loop. """ - target_list = hass.data.get(DATA_DISPATCHER, {}).get(signal, {}) + target_list: dict[ + Callable[..., Any], HassJob[..., None | Coroutine[Any, Any, None]] | None + ] = hass.data.get(DATA_DISPATCHER, {}).get(signal, {}) - run: list[HassJob] = [] + run: list[HassJob[..., None | Coroutine[Any, Any, None]]] = [] for target, job in target_list.items(): if job is None: job = _generate_job(signal, target) target_list[target] = job # Run the jobs all at the end - # to ensure no jobs add more disptachers + # to ensure no jobs add more dispatchers # which can result in the target_list # changing size during iteration run.append(job) diff --git a/mypy.ini b/mypy.ini index 6321ee04b98..d7f3db26d55 100644 --- a/mypy.ini +++ b/mypy.ini @@ -66,6 +66,9 @@ disallow_any_generics = true [mypy-homeassistant.helpers.discovery] disallow_any_generics = true +[mypy-homeassistant.helpers.dispatcher] +disallow_any_generics = true + [mypy-homeassistant.helpers.entity] disallow_any_generics = true From 0f81d1d14ad92dfaa012dee173e664d4cae4fd6f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Jul 2022 21:14:31 -0500 Subject: [PATCH 2698/3516] Drop RSSI update workaround from bluetooth on linux (#75467) It turns out we do not need these are we can check the discovered device list to see if bluez is still seeing the device --- homeassistant/components/bluetooth/__init__.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 8a1897c5ec2..33b2da7523e 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -6,7 +6,6 @@ from dataclasses import dataclass from enum import Enum import fnmatch import logging -import platform from typing import Final, TypedDict, Union from bleak import BleakError @@ -217,20 +216,6 @@ def _ble_device_matches( return True -@hass_callback -def async_enable_rssi_updates() -> None: - """Bleak filters out RSSI updates by default on linux only.""" - # We want RSSI updates - if platform.system() == "Linux": - from bleak.backends.bluezdbus import ( # pylint: disable=import-outside-toplevel - scanner, - ) - - scanner._ADVERTISING_DATA_PROPERTIES.add( # pylint: disable=protected-access - "RSSI" - ) - - class BluetoothManager: """Manage Bluetooth.""" @@ -265,7 +250,6 @@ class BluetoothManager: ex, ) return - async_enable_rssi_updates() install_multiple_bleak_catcher(self.scanner) # We have to start it right away as some integrations might # need it straight away. From 8a48d549513539ffc9d3928b4b15c124f5ce6e5f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Jul 2022 05:45:57 +0200 Subject: [PATCH 2699/3516] Improve entity_platform helper typing (#75464) * Improve entity_platform helper typing * Add protocol class * Apply suggestions from code review Co-authored-by: Martin Hjelmare Co-authored-by: Martin Hjelmare --- .strict-typing | 1 + homeassistant/helpers/entity_platform.py | 55 ++++++++++++++++++------ mypy.ini | 3 ++ 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/.strict-typing b/.strict-typing index 9757a5f6c03..46d85aa1ec2 100644 --- a/.strict-typing +++ b/.strict-typing @@ -20,6 +20,7 @@ homeassistant.helpers.deprecation homeassistant.helpers.discovery homeassistant.helpers.dispatcher homeassistant.helpers.entity +homeassistant.helpers.entity_platform homeassistant.helpers.entity_values homeassistant.helpers.event homeassistant.helpers.reload diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 2049565e859..c5c61cc1b0d 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -2,11 +2,10 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Coroutine, Iterable +from collections.abc import Awaitable, Callable, Coroutine, Iterable from contextvars import ContextVar 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 @@ -71,6 +70,36 @@ class AddEntitiesCallback(Protocol): """Define add_entities type.""" +class EntityPlatformModule(Protocol): + """Protocol type for entity platform modules.""" + + async def async_setup_platform( + self, + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, + ) -> None: + """Set up an integration platform async.""" + + def setup_platform( + self, + hass: HomeAssistant, + config: ConfigType, + add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, + ) -> None: + """Set up an integration platform.""" + + async def async_setup_entry( + self, + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up an integration platform from a config entry.""" + + class EntityPlatform: """Manage the entities for a single platform.""" @@ -81,7 +110,7 @@ class EntityPlatform: logger: Logger, domain: str, platform_name: str, - platform: ModuleType | None, + platform: EntityPlatformModule | None, scan_interval: timedelta, entity_namespace: str | None, ) -> None: @@ -95,7 +124,7 @@ class EntityPlatform: self.entity_namespace = entity_namespace self.config_entry: config_entries.ConfigEntry | None = None self.entities: dict[str, Entity] = {} - self._tasks: list[asyncio.Future] = [] + self._tasks: list[asyncio.Task[None]] = [] # Stop tracking tasks after setup is completed self._setup_complete = False # Method to cancel the state change listener @@ -169,10 +198,12 @@ class EntityPlatform: return @callback - def async_create_setup_task() -> Coroutine: + def async_create_setup_task() -> Coroutine[ + Any, Any, None + ] | asyncio.Future[None]: """Get task to set up platform.""" if getattr(platform, "async_setup_platform", None): - return platform.async_setup_platform( # type: ignore[no-any-return,union-attr] + return platform.async_setup_platform( # type: ignore[union-attr] hass, platform_config, self._async_schedule_add_entities, @@ -181,7 +212,7 @@ class EntityPlatform: # This should not be replaced with hass.async_add_job because # we don't want to track this task in case it blocks startup. - return hass.loop.run_in_executor( # type: ignore[return-value] + return hass.loop.run_in_executor( None, platform.setup_platform, # type: ignore[union-attr] hass, @@ -211,18 +242,18 @@ class EntityPlatform: platform = self.platform @callback - def async_create_setup_task() -> Coroutine: + def async_create_setup_task() -> Coroutine[Any, Any, None]: """Get task to set up platform.""" config_entries.current_entry.set(config_entry) - return platform.async_setup_entry( # type: ignore[no-any-return,union-attr] + return platform.async_setup_entry( # type: ignore[union-attr] self.hass, config_entry, self._async_schedule_add_entities_for_entry ) return await self._async_setup_platform(async_create_setup_task) async def _async_setup_platform( - self, async_create_setup_task: Callable[[], Coroutine], tries: int = 0 + self, async_create_setup_task: Callable[[], Awaitable[None]], tries: int = 0 ) -> bool: """Set up a platform via config file or config entry. @@ -701,7 +732,7 @@ class EntityPlatform: def async_register_entity_service( self, name: str, - schema: dict | vol.Schema, + schema: dict[str, Any] | vol.Schema, func: str | Callable[..., Any], required_features: Iterable[int] | None = None, ) -> None: @@ -753,7 +784,7 @@ class EntityPlatform: return async with self._process_updates: - tasks = [] + tasks: list[Coroutine[Any, Any, None]] = [] for entity in self.entities.values(): if not entity.should_poll: continue diff --git a/mypy.ini b/mypy.ini index d7f3db26d55..04b76dfea0f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -72,6 +72,9 @@ disallow_any_generics = true [mypy-homeassistant.helpers.entity] disallow_any_generics = true +[mypy-homeassistant.helpers.entity_platform] +disallow_any_generics = true + [mypy-homeassistant.helpers.entity_values] disallow_any_generics = true From 51ed9ee59d21e2a065a4b1799075258a950e1e4a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Jul 2022 05:46:18 +0200 Subject: [PATCH 2700/3516] Fix bluetooth service_info typing (#75477) * Fix bluetooth service_info typing * Remove additional type ignores * Remove pylint disable --- homeassistant/components/bluetooth/__init__.py | 6 +++--- .../components/bluetooth_tracker/device_tracker.py | 6 +++--- homeassistant/helpers/service_info/__init__.py | 1 + homeassistant/helpers/service_info/bluetooth.py | 6 +++--- 4 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 homeassistant/helpers/service_info/__init__.py diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 33b2da7523e..5edc0053203 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -43,7 +43,7 @@ SOURCE_LOCAL: Final = "local" @dataclass -class BluetoothServiceInfoBleak(BluetoothServiceInfo): # type: ignore[misc] +class BluetoothServiceInfoBleak(BluetoothServiceInfo): """BluetoothServiceInfo with bleak data. Integrations may need BLEDevice and AdvertisementData @@ -58,7 +58,7 @@ class BluetoothServiceInfoBleak(BluetoothServiceInfo): # type: ignore[misc] @classmethod def from_advertisement( cls, device: BLEDevice, advertisement_data: AdvertisementData, source: str - ) -> BluetoothServiceInfo: + ) -> BluetoothServiceInfoBleak: """Create a BluetoothServiceInfoBleak from an advertisement.""" return cls( name=advertisement_data.local_name or device.name or device.address, @@ -377,7 +377,7 @@ class BluetoothManager: ) @hass_callback - def async_discovered_service_info(self) -> list[BluetoothServiceInfo]: + def async_discovered_service_info(self) -> list[BluetoothServiceInfoBleak]: """Return if the address is present.""" if models.HA_BLEAK_SCANNER: discovered = models.HA_BLEAK_SCANNER.discovered_devices diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 5cf173d356e..b62333c0489 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -61,7 +61,7 @@ def is_bluetooth_device(device: Device) -> bool: def discover_devices(device_id: int) -> list[tuple[str, str]]: """Discover Bluetooth devices.""" try: - result = bluetooth.discover_devices( # type: ignore[attr-defined] + result = bluetooth.discover_devices( duration=8, lookup_names=True, flush_cache=True, @@ -124,7 +124,7 @@ async def get_tracking_devices(hass: HomeAssistant) -> tuple[set[str], set[str]] def lookup_name(mac: str) -> str | None: """Lookup a Bluetooth device name.""" _LOGGER.debug("Scanning %s", mac) - return bluetooth.lookup_name(mac, timeout=5) # type: ignore[attr-defined,no-any-return] + return bluetooth.lookup_name(mac, timeout=5) # type: ignore[no-any-return] async def async_setup_scanner( @@ -180,7 +180,7 @@ async def async_setup_scanner( if tasks: await asyncio.wait(tasks) - except bluetooth.BluetoothError: # type: ignore[attr-defined] + except bluetooth.BluetoothError: _LOGGER.exception("Error looking up Bluetooth device") async def update_bluetooth(now: datetime | None = None) -> None: diff --git a/homeassistant/helpers/service_info/__init__.py b/homeassistant/helpers/service_info/__init__.py new file mode 100644 index 00000000000..b907fd9bbd1 --- /dev/null +++ b/homeassistant/helpers/service_info/__init__.py @@ -0,0 +1 @@ +"""Service info helpers.""" diff --git a/homeassistant/helpers/service_info/bluetooth.py b/homeassistant/helpers/service_info/bluetooth.py index d4d74a45be4..968d1dde95f 100644 --- a/homeassistant/helpers/service_info/bluetooth.py +++ b/homeassistant/helpers/service_info/bluetooth.py @@ -1,4 +1,4 @@ """The bluetooth integration service info.""" -from home_assistant_bluetooth import ( # pylint: disable=unused-import # noqa: F401 - BluetoothServiceInfo, -) +from home_assistant_bluetooth import BluetoothServiceInfo + +__all__ = ["BluetoothServiceInfo"] From d989e4373d576c403790c9a7e5eb7a29d08e3c47 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 20 Jul 2022 12:08:11 +0800 Subject: [PATCH 2701/3516] Remove websocket_api send_big_result (#75452) --- homeassistant/components/camera/__init__.py | 34 +--------- .../components/lovelace/websocket.py | 5 +- .../components/media_player/__init__.py | 46 ------------- .../components/websocket_api/connection.py | 5 -- .../components/androidtv/test_media_player.py | 65 +++++++------------ tests/components/camera/test_init.py | 19 ------ tests/components/media_player/test_init.py | 34 ---------- .../websocket_api/test_connection.py | 20 ------ 8 files changed, 27 insertions(+), 201 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 35fa6fef1d6..dcee6c6c5ed 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -import base64 import collections from collections.abc import Awaitable, Callable, Iterable from contextlib import suppress @@ -362,12 +361,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.http.register_view(CameraImageView(component)) hass.http.register_view(CameraMjpegStream(component)) - websocket_api.async_register_command( - hass, - WS_TYPE_CAMERA_THUMBNAIL, - websocket_camera_thumbnail, - SCHEMA_WS_CAMERA_THUMBNAIL, - ) + websocket_api.async_register_command(hass, ws_camera_stream) websocket_api.async_register_command(hass, ws_camera_web_rtc_offer) websocket_api.async_register_command(hass, websocket_get_prefs) @@ -793,32 +787,6 @@ class CameraMjpegStream(CameraView): raise web.HTTPBadRequest() from err -@websocket_api.async_response -async def websocket_camera_thumbnail( - hass: HomeAssistant, connection: ActiveConnection, msg: dict -) -> None: - """Handle get camera thumbnail websocket command. - - Async friendly. - """ - _LOGGER.warning("The websocket command 'camera_thumbnail' has been deprecated") - try: - image = await async_get_image(hass, msg["entity_id"]) - connection.send_big_result( - msg["id"], - { - "content_type": image.content_type, - "content": base64.b64encode(image.content).decode("utf-8"), - }, - ) - except HomeAssistantError: - connection.send_message( - websocket_api.error_message( - msg["id"], "image_fetch_failed", "Unable to fetch image" - ) - ) - - @websocket_api.websocket_command( { vol.Required("type"): "camera/stream", diff --git a/homeassistant/components/lovelace/websocket.py b/homeassistant/components/lovelace/websocket.py index 7e04201291d..66ec7f22b09 100644 --- a/homeassistant/components/lovelace/websocket.py +++ b/homeassistant/components/lovelace/websocket.py @@ -37,10 +37,7 @@ def _handle_errors(func): connection.send_error(msg["id"], *error) return - if msg is not None: - connection.send_big_result(msg["id"], result) - else: - connection.send_result(msg["id"], result) + connection.send_result(msg["id"], result) return send_with_error_handling diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 5e5347e6806..9ca9613278d 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -import base64 import collections from collections.abc import Callable from contextlib import suppress @@ -27,7 +26,6 @@ from homeassistant.backports.enum import StrEnum from homeassistant.components import websocket_api from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView from homeassistant.components.websocket_api.const import ( - ERR_NOT_FOUND, ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR, ) @@ -254,7 +252,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL ) - websocket_api.async_register_command(hass, websocket_handle_thumbnail) websocket_api.async_register_command(hass, websocket_browse_media) hass.http.register_view(MediaPlayerImageView(component)) @@ -1130,49 +1127,6 @@ class MediaPlayerImageView(HomeAssistantView): return web.Response(body=data, content_type=content_type, headers=headers) -@websocket_api.websocket_command( - { - vol.Required("type"): "media_player_thumbnail", - vol.Required("entity_id"): cv.entity_id, - } -) -@websocket_api.async_response -async def websocket_handle_thumbnail(hass, connection, msg): - """Handle get media player cover command. - - Async friendly. - """ - component = hass.data[DOMAIN] - - if (player := component.get_entity(msg["entity_id"])) is None: - connection.send_message( - websocket_api.error_message(msg["id"], ERR_NOT_FOUND, "Entity not found") - ) - return - - _LOGGER.warning( - "The websocket command media_player_thumbnail is deprecated. Use /api/media_player_proxy instead" - ) - - data, content_type = await player.async_get_media_image() - - if data is None: - connection.send_message( - websocket_api.error_message( - msg["id"], "thumbnail_fetch_failed", "Failed to fetch thumbnail" - ) - ) - return - - connection.send_big_result( - msg["id"], - { - "content_type": content_type, - "content": base64.b64encode(data).decode("utf-8"), - }, - ) - - @websocket_api.websocket_command( { vol.Required("type"): "media_player/browse_media", diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 1c26d958969..87c52288bcc 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -11,7 +11,6 @@ import voluptuous as vol from homeassistant.auth.models import RefreshToken, User from homeassistant.core import Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized -from homeassistant.helpers.json import JSON_DUMP from . import const, messages @@ -54,10 +53,6 @@ class ActiveConnection: """Send a result message.""" self.send_message(messages.result_message(msg_id, result)) - def send_big_result(self, msg_id: int, result: Any) -> None: - """Send a result message that would be expensive to JSON serialize.""" - self.send_message(JSON_DUMP(messages.result_message(msg_id, result))) - @callback def send_error(self, msg_id: int, code: str, message: str) -> None: """Send a error message.""" diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index 73f8de55cc9..f5487c78425 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -1,5 +1,4 @@ """The tests for the androidtv platform.""" -import base64 import logging from unittest.mock import Mock, patch @@ -52,7 +51,6 @@ from homeassistant.components.media_player import ( SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, ) -from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_COMMAND, @@ -851,10 +849,10 @@ async def test_androidtv_volume_set(hass): patch_set_volume_level.assert_called_with(0.5) -async def test_get_image(hass, hass_ws_client): +async def test_get_image_http(hass, hass_client_no_auth): """Test taking a screen capture. - This is based on `test_get_image` in tests/components/media_player/test_init.py. + This is based on `test_get_image_http` in tests/components/media_player/test_init.py. """ patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) @@ -868,44 +866,36 @@ async def test_get_image(hass, hass_ws_client): with patchers.patch_shell("11")[patch_key]: await async_update_entity(hass, entity_id) - client = await hass_ws_client(hass) + media_player_name = "media_player." + slugify( + CONFIG_ANDROIDTV_DEFAULT[TEST_ENTITY_NAME] + ) + state = hass.states.get(media_player_name) + assert "entity_picture_local" not in state.attributes + + client = await hass_client_no_auth() with patch( "androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap", return_value=b"image" ): - await client.send_json( - {"id": 5, "type": "media_player_thumbnail", "entity_id": entity_id} - ) + resp = await client.get(state.attributes["entity_picture"]) + content = await resp.read() - msg = await client.receive_json() - - assert msg["id"] == 5 - assert msg["type"] == TYPE_RESULT - assert msg["success"] - assert msg["result"]["content_type"] == "image/png" - assert msg["result"]["content"] == base64.b64encode(b"image").decode("utf-8") + assert content == b"image" with patch( "androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap", side_effect=ConnectionResetError, ): - await client.send_json( - {"id": 6, "type": "media_player_thumbnail", "entity_id": entity_id} - ) + resp = await client.get(state.attributes["entity_picture"]) - msg = await client.receive_json() - - # The device is unavailable, but getting the media image did not cause an exception - state = hass.states.get(entity_id) - assert state is not None - assert state.state == STATE_UNAVAILABLE + # The device is unavailable, but getting the media image did not cause an exception + state = hass.states.get(media_player_name) + assert state is not None + assert state.state == STATE_UNAVAILABLE -async def test_get_image_disabled(hass, hass_ws_client): - """Test taking a screen capture with screencap option disabled. - - This is based on `test_get_image` in tests/components/media_player/test_init.py. - """ +async def test_get_image_disabled(hass): + """Test that the screencap option can disable entity_picture.""" patch_key, entity_id, config_entry = _setup(CONFIG_ANDROIDTV_DEFAULT) config_entry.add_to_hass(hass) hass.config_entries.async_update_entry( @@ -921,17 +911,12 @@ async def test_get_image_disabled(hass, hass_ws_client): with patchers.patch_shell("11")[patch_key]: await async_update_entity(hass, entity_id) - client = await hass_ws_client(hass) - - with patch( - "androidtv.basetv.basetv_async.BaseTVAsync.adb_screencap", return_value=b"image" - ) as screen_cap: - await client.send_json( - {"id": 5, "type": "media_player_thumbnail", "entity_id": entity_id} - ) - - await client.receive_json() - assert not screen_cap.called + media_player_name = "media_player." + slugify( + CONFIG_ANDROIDTV_DEFAULT[TEST_ENTITY_NAME] + ) + state = hass.states.get(media_player_name) + assert "entity_picture_local" not in state.attributes + assert "entity_picture" not in state.attributes async def _test_service( diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index ba13bbd6c52..f800a4ac2bd 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -1,6 +1,5 @@ """The tests for the camera component.""" import asyncio -import base64 from http import HTTPStatus import io from unittest.mock import AsyncMock, Mock, PropertyMock, mock_open, patch @@ -235,24 +234,6 @@ async def test_snapshot_service(hass, mock_camera): assert mock_write.mock_calls[0][1][0] == b"Test" -async def test_websocket_camera_thumbnail(hass, hass_ws_client, mock_camera): - """Test camera_thumbnail websocket command.""" - await async_setup_component(hass, "camera", {}) - - client = await hass_ws_client(hass) - await client.send_json( - {"id": 5, "type": "camera_thumbnail", "entity_id": "camera.demo_camera"} - ) - - msg = await client.receive_json() - - assert msg["id"] == 5 - assert msg["type"] == TYPE_RESULT - assert msg["success"] - assert msg["result"]["content_type"] == "image/jpg" - assert msg["result"]["content"] == base64.b64encode(b"Test").decode("utf-8") - - async def test_websocket_stream_no_source( hass, hass_ws_client, mock_camera, mock_stream ): diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index eceb7e9ec4f..f0666a11545 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -1,6 +1,5 @@ """Test the base functions of the media player.""" import asyncio -import base64 from http import HTTPStatus from unittest.mock import patch @@ -14,39 +13,6 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF from homeassistant.setup import async_setup_component -async def test_get_image(hass, hass_ws_client, caplog): - """Test get image via WS command.""" - await async_setup_component( - hass, "media_player", {"media_player": {"platform": "demo"}} - ) - await hass.async_block_till_done() - - client = await hass_ws_client(hass) - - with patch( - "homeassistant.components.media_player.MediaPlayerEntity." - "async_get_media_image", - return_value=(b"image", "image/jpeg"), - ): - await client.send_json( - { - "id": 5, - "type": "media_player_thumbnail", - "entity_id": "media_player.bedroom", - } - ) - - msg = await client.receive_json() - - assert msg["id"] == 5 - assert msg["type"] == TYPE_RESULT - assert msg["success"] - assert msg["result"]["content_type"] == "image/jpeg" - assert msg["result"]["content"] == base64.b64encode(b"image").decode("utf-8") - - assert "media_player_thumbnail is deprecated" in caplog.text - - async def test_get_image_http(hass, hass_client_no_auth): """Test get image via http command.""" await async_setup_component( diff --git a/tests/components/websocket_api/test_connection.py b/tests/components/websocket_api/test_connection.py index da31f0ee8a3..fd9af99c1a4 100644 --- a/tests/components/websocket_api/test_connection.py +++ b/tests/components/websocket_api/test_connection.py @@ -7,30 +7,10 @@ import voluptuous as vol from homeassistant import exceptions from homeassistant.components import websocket_api -from homeassistant.components.websocket_api import const from tests.common import MockUser -async def test_send_big_result(hass, websocket_client): - """Test sending big results over the WS.""" - - @websocket_api.websocket_command({"type": "big_result"}) - @websocket_api.async_response - def send_big_result(hass, connection, msg): - connection.send_big_result(msg["id"], {"big": "result"}) - - websocket_api.async_register_command(hass, send_big_result) - - await websocket_client.send_json({"id": 5, "type": "big_result"}) - - msg = await websocket_client.receive_json() - assert msg["id"] == 5 - assert msg["type"] == const.TYPE_RESULT - assert msg["success"] - assert msg["result"] == {"big": "result"} - - async def test_exception_handling(): """Test handling of exceptions.""" send_messages = [] From 8c7e329754a2ef2d07810ccd30b8499d0d4fe0f7 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 20 Jul 2022 03:15:36 -0400 Subject: [PATCH 2702/3516] Bump pytomorrowio to 0.3.4 (#75478) --- homeassistant/components/tomorrowio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tomorrowio/manifest.json b/homeassistant/components/tomorrowio/manifest.json index 5447b90d1ce..0823b5ac185 100644 --- a/homeassistant/components/tomorrowio/manifest.json +++ b/homeassistant/components/tomorrowio/manifest.json @@ -3,7 +3,7 @@ "name": "Tomorrow.io", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tomorrowio", - "requirements": ["pytomorrowio==0.3.3"], + "requirements": ["pytomorrowio==0.3.4"], "codeowners": ["@raman325", "@lymanepp"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 0606ec11933..4fb9282d0ce 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1978,7 +1978,7 @@ pythonegardia==1.0.40 pytile==2022.02.0 # homeassistant.components.tomorrowio -pytomorrowio==0.3.3 +pytomorrowio==0.3.4 # homeassistant.components.touchline pytouchline==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8148e114d6b..6ea366315f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1325,7 +1325,7 @@ python_awair==0.2.3 pytile==2022.02.0 # homeassistant.components.tomorrowio -pytomorrowio==0.3.3 +pytomorrowio==0.3.4 # homeassistant.components.traccar pytraccar==0.10.0 From 2b752355d63f6fb906ab47db80865d84797a99bb Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 20 Jul 2022 03:15:30 -0600 Subject: [PATCH 2703/3516] Modify Tile to store a single dataclass in `hass.data` (#75459) --- homeassistant/components/tile/__init__.py | 16 +++++++++++----- homeassistant/components/tile/const.py | 3 --- homeassistant/components/tile/device_tracker.py | 13 ++++++------- homeassistant/components/tile/diagnostics.py | 9 ++++----- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index cca0706954f..38f52a067fa 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -1,6 +1,7 @@ """The Tile component.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta from functools import partial @@ -17,7 +18,7 @@ from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_e from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.async_ import gather_with_concurrency -from .const import DATA_COORDINATOR, DATA_TILE, DOMAIN, LOGGER +from .const import DOMAIN, LOGGER PLATFORMS = [Platform.DEVICE_TRACKER] DEVICE_TYPES = ["PHONE", "TILE"] @@ -28,6 +29,14 @@ DEFAULT_UPDATE_INTERVAL = timedelta(minutes=2) CONF_SHOW_INACTIVE = "show_inactive" +@dataclass +class TileData: + """Define an object to be stored in `hass.data`.""" + + coordinators: dict[str, DataUpdateCoordinator] + tiles: dict[str, Tile] + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Tile as config entry.""" @@ -100,10 +109,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await gather_with_concurrency(DEFAULT_INIT_TASK_LIMIT, *coordinator_init_tasks) hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_COORDINATOR: coordinators, - DATA_TILE: tiles, - } + hass.data[DOMAIN][entry.entry_id] = TileData(coordinators=coordinators, tiles=tiles) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/tile/const.py b/homeassistant/components/tile/const.py index 0f6f0dabb5c..eed3eb698ef 100644 --- a/homeassistant/components/tile/const.py +++ b/homeassistant/components/tile/const.py @@ -3,7 +3,4 @@ import logging DOMAIN = "tile" -DATA_COORDINATOR = "coordinator" -DATA_TILE = "tile" - LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index df5f035a849..61e9a1bdcd9 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -18,7 +18,8 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import DATA_COORDINATOR, DATA_TILE, DOMAIN +from . import TileData +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -38,14 +39,12 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Tile device trackers.""" + data: TileData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( [ - TileDeviceTracker( - entry, - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][tile_uuid], - tile, - ) - for tile_uuid, tile in hass.data[DOMAIN][entry.entry_id][DATA_TILE].items() + TileDeviceTracker(entry, data.coordinators[tile_uuid], tile) + for tile_uuid, tile in data.tiles.items() ] ) diff --git a/homeassistant/components/tile/diagnostics.py b/homeassistant/components/tile/diagnostics.py index 7e85ef25047..8654bf44680 100644 --- a/homeassistant/components/tile/diagnostics.py +++ b/homeassistant/components/tile/diagnostics.py @@ -3,14 +3,13 @@ from __future__ import annotations from typing import Any -from pytile.tile import Tile - from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import HomeAssistant -from .const import DATA_TILE, DOMAIN +from . import TileData +from .const import DOMAIN CONF_ALTITUDE = "altitude" CONF_UUID = "uuid" @@ -27,8 +26,8 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - tiles: dict[str, Tile] = hass.data[DOMAIN][entry.entry_id][DATA_TILE] + data: TileData = hass.data[DOMAIN][entry.entry_id] return async_redact_data( - {"tiles": [tile.as_dict() for tile in tiles.values()]}, TO_REDACT + {"tiles": [tile.as_dict() for tile in data.tiles.values()]}, TO_REDACT ) From 3d31e626831f2df7c039df54bd1710d0d2c01fce Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 20 Jul 2022 03:18:26 -0600 Subject: [PATCH 2704/3516] Modify Ridwell to store a single dataclass in `hass.data` (#75457) --- homeassistant/components/ridwell/__init__.py | 24 ++++++++++--------- homeassistant/components/ridwell/const.py | 3 --- .../components/ridwell/diagnostics.py | 10 ++++---- homeassistant/components/ridwell/sensor.py | 11 ++++----- homeassistant/components/ridwell/switch.py | 11 ++++----- 5 files changed, 27 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/ridwell/__init__.py b/homeassistant/components/ridwell/__init__.py index 5f3656a8b5a..5a9c19ed36f 100644 --- a/homeassistant/components/ridwell/__init__.py +++ b/homeassistant/components/ridwell/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass from datetime import timedelta from typing import Any @@ -21,19 +22,21 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) -from .const import ( - DATA_ACCOUNT, - DATA_COORDINATOR, - DOMAIN, - LOGGER, - SENSOR_TYPE_NEXT_PICKUP, -) +from .const import DOMAIN, LOGGER, SENSOR_TYPE_NEXT_PICKUP DEFAULT_UPDATE_INTERVAL = timedelta(hours=1) PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.SWITCH] +@dataclass +class RidwellData: + """Define an object to be stored in `hass.data`.""" + + accounts: dict[str, RidwellAccount] + coordinator: DataUpdateCoordinator + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ridwell from a config entry.""" session = aiohttp_client.async_get_clientsession(hass) @@ -77,10 +80,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_ACCOUNT: accounts, - DATA_COORDINATOR: coordinator, - } + hass.data[DOMAIN][entry.entry_id] = RidwellData( + accounts=accounts, coordinator=coordinator + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/ridwell/const.py b/homeassistant/components/ridwell/const.py index bc1e78e2d93..69c5ead5277 100644 --- a/homeassistant/components/ridwell/const.py +++ b/homeassistant/components/ridwell/const.py @@ -5,7 +5,4 @@ DOMAIN = "ridwell" LOGGER = logging.getLogger(__package__) -DATA_ACCOUNT = "account" -DATA_COORDINATOR = "coordinator" - SENSOR_TYPE_NEXT_PICKUP = "next_pickup" diff --git a/homeassistant/components/ridwell/diagnostics.py b/homeassistant/components/ridwell/diagnostics.py index fce89a639ad..3f29165842f 100644 --- a/homeassistant/components/ridwell/diagnostics.py +++ b/homeassistant/components/ridwell/diagnostics.py @@ -6,19 +6,17 @@ from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DATA_COORDINATOR, DOMAIN +from . import RidwellData +from .const import DOMAIN async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR - ] + data: RidwellData = hass.data[DOMAIN][entry.entry_id] return { - "data": [dataclasses.asdict(event) for event in coordinator.data.values()], + "data": [dataclasses.asdict(event) for event in data.coordinator.data.values()] } diff --git a/homeassistant/components/ridwell/sensor.py b/homeassistant/components/ridwell/sensor.py index f44cac134d8..9b7a4ab6954 100644 --- a/homeassistant/components/ridwell/sensor.py +++ b/homeassistant/components/ridwell/sensor.py @@ -18,8 +18,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import RidwellEntity -from .const import DATA_ACCOUNT, DATA_COORDINATOR, DOMAIN, SENSOR_TYPE_NEXT_PICKUP +from . import RidwellData, RidwellEntity +from .const import DOMAIN, SENSOR_TYPE_NEXT_PICKUP ATTR_CATEGORY = "category" ATTR_PICKUP_STATE = "pickup_state" @@ -37,13 +37,12 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Ridwell sensors based on a config entry.""" - accounts = hass.data[DOMAIN][entry.entry_id][DATA_ACCOUNT] - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + data: RidwellData = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ - RidwellSensor(coordinator, account, SENSOR_DESCRIPTION) - for account in accounts.values() + RidwellSensor(data.coordinator, account, SENSOR_DESCRIPTION) + for account in data.accounts.values() ] ) diff --git a/homeassistant/components/ridwell/switch.py b/homeassistant/components/ridwell/switch.py index 7bdea622507..d8e228be7db 100644 --- a/homeassistant/components/ridwell/switch.py +++ b/homeassistant/components/ridwell/switch.py @@ -12,8 +12,8 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import RidwellEntity -from .const import DATA_ACCOUNT, DATA_COORDINATOR, DOMAIN +from . import RidwellData, RidwellEntity +from .const import DOMAIN SWITCH_TYPE_OPT_IN = "opt_in" @@ -28,13 +28,12 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Ridwell sensors based on a config entry.""" - accounts = hass.data[DOMAIN][entry.entry_id][DATA_ACCOUNT] - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + data: RidwellData = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ - RidwellSwitch(coordinator, account, SWITCH_DESCRIPTION) - for account in accounts.values() + RidwellSwitch(data.coordinator, account, SWITCH_DESCRIPTION) + for account in data.accounts.values() ] ) From 0e59e8b9258bc50029e922085d8a1692eda841d5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 11:24:15 +0200 Subject: [PATCH 2705/3516] Migrate Moon to new entity naming style (#75085) --- homeassistant/components/moon/sensor.py | 10 +++++++++- tests/components/moon/test_sensor.py | 16 ++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index c5078771af8..f3a3b3f48fa 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -12,6 +12,8 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util @@ -72,11 +74,17 @@ class MoonSensorEntity(SensorEntity): """Representation of a Moon sensor.""" _attr_device_class = "moon__phase" + _attr_has_entity_name = True + _attr_name = "Phase" def __init__(self, entry: ConfigEntry) -> None: """Initialize the moon sensor.""" - self._attr_name = entry.title self._attr_unique_id = entry.entry_id + self._attr_device_info = DeviceInfo( + name="Moon", + identifiers={(DOMAIN, entry.entry_id)}, + entry_type=DeviceEntryType.SERVICE, + ) async def async_update(self) -> None: """Get the time and updates the states.""" diff --git a/tests/components/moon/test_sensor.py b/tests/components/moon/test_sensor.py index bb9e5dcc157..6f13c97a3be 100644 --- a/tests/components/moon/test_sensor.py +++ b/tests/components/moon/test_sensor.py @@ -16,9 +16,9 @@ from homeassistant.components.moon.sensor import ( STATE_WAXING_CRESCENT, STATE_WAXING_GIBBOUS, ) -from homeassistant.const import ATTR_ICON +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_ICON 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 tests.common import MockConfigEntry @@ -52,12 +52,20 @@ async def test_moon_day( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("sensor.moon") + state = hass.states.get("sensor.moon_phase") assert state assert state.state == native_value assert state.attributes[ATTR_ICON] == icon + assert state.attributes[ATTR_FRIENDLY_NAME] == "Moon Phase" entity_registry = er.async_get(hass) - entry = entity_registry.async_get("sensor.moon") + entry = entity_registry.async_get("sensor.moon_phase") assert entry assert entry.unique_id == mock_config_entry.entry_id + + device_registry = dr.async_get(hass) + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.name == "Moon" + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE From b62ebbe97431444cdd60edc0532d54f91466f0e5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 11:30:08 +0200 Subject: [PATCH 2706/3516] Migrate DSMR to new entity naming style (#75077) --- homeassistant/components/dsmr/sensor.py | 65 +++++------ tests/components/dsmr/test_sensor.py | 140 ++++++++++++------------ 2 files changed, 103 insertions(+), 102 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 6acb652c6e5..7ab1a3bb45b 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -78,7 +78,7 @@ class DSMRSensorEntityDescription( SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( DSMRSensorEntityDescription( key="current_electricity_usage", - name="Power Consumption", + name="Power consumption", obis_reference=obis_references.CURRENT_ELECTRICITY_USAGE, device_class=SensorDeviceClass.POWER, force_update=True, @@ -86,7 +86,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="electricity_delivery", - name="Power Production", + name="Power production", obis_reference=obis_references.CURRENT_ELECTRICITY_DELIVERY, device_class=SensorDeviceClass.POWER, force_update=True, @@ -94,14 +94,14 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="electricity_active_tariff", - name="Power Tariff", + name="Active tariff", obis_reference=obis_references.ELECTRICITY_ACTIVE_TARIFF, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, icon="mdi:flash", ), DSMRSensorEntityDescription( key="electricity_used_tariff_1", - name="Energy Consumption (tarif 1)", + name="Energy consumption (tarif 1)", obis_reference=obis_references.ELECTRICITY_USED_TARIFF_1, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, device_class=SensorDeviceClass.ENERGY, @@ -110,7 +110,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="electricity_used_tariff_2", - name="Energy Consumption (tarif 2)", + name="Energy consumption (tarif 2)", obis_reference=obis_references.ELECTRICITY_USED_TARIFF_2, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, force_update=True, @@ -119,7 +119,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="electricity_delivered_tariff_1", - name="Energy Production (tarif 1)", + name="Energy production (tarif 1)", obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_1, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, force_update=True, @@ -128,7 +128,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="electricity_delivered_tariff_2", - name="Energy Production (tarif 2)", + name="Energy production (tarif 2)", obis_reference=obis_references.ELECTRICITY_DELIVERED_TARIFF_2, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, force_update=True, @@ -137,7 +137,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_active_power_l1_positive", - name="Power Consumption Phase L1", + name="Power consumption phase L1", obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_POSITIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, @@ -145,7 +145,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_active_power_l2_positive", - name="Power Consumption Phase L2", + name="Power consumption phase L2", obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_POSITIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, @@ -153,7 +153,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_active_power_l3_positive", - name="Power Consumption Phase L3", + name="Power consumption phase L3", obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_POSITIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, @@ -161,7 +161,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_active_power_l1_negative", - name="Power Production Phase L1", + name="Power production phase L1", obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L1_NEGATIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, @@ -169,7 +169,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_active_power_l2_negative", - name="Power Production Phase L2", + name="Power production phase L2", obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L2_NEGATIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, @@ -177,7 +177,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_active_power_l3_negative", - name="Power Production Phase L3", + name="Power production phase L3", obis_reference=obis_references.INSTANTANEOUS_ACTIVE_POWER_L3_NEGATIVE, device_class=SensorDeviceClass.POWER, entity_registry_enabled_default=False, @@ -185,7 +185,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="short_power_failure_count", - name="Short Power Failure Count", + name="Short power failure count", obis_reference=obis_references.SHORT_POWER_FAILURE_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, @@ -194,7 +194,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="long_power_failure_count", - name="Long Power Failure Count", + name="Long power failure count", obis_reference=obis_references.LONG_POWER_FAILURE_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, @@ -203,7 +203,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="voltage_sag_l1_count", - name="Voltage Sags Phase L1", + name="Voltage sags phase L1", obis_reference=obis_references.VOLTAGE_SAG_L1_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, @@ -211,7 +211,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="voltage_sag_l2_count", - name="Voltage Sags Phase L2", + name="Voltage sags phase L2", obis_reference=obis_references.VOLTAGE_SAG_L2_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, @@ -219,7 +219,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="voltage_sag_l3_count", - name="Voltage Sags Phase L3", + name="Voltage sags phase L3", obis_reference=obis_references.VOLTAGE_SAG_L3_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, @@ -227,7 +227,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="voltage_swell_l1_count", - name="Voltage Swells Phase L1", + name="Voltage swells phase L1", obis_reference=obis_references.VOLTAGE_SWELL_L1_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, @@ -236,7 +236,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="voltage_swell_l2_count", - name="Voltage Swells Phase L2", + name="Voltage swells phase L2", obis_reference=obis_references.VOLTAGE_SWELL_L2_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, @@ -245,7 +245,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="voltage_swell_l3_count", - name="Voltage Swells Phase L3", + name="Voltage swells phase L3", obis_reference=obis_references.VOLTAGE_SWELL_L3_COUNT, dsmr_versions={"2.2", "4", "5", "5B", "5L"}, entity_registry_enabled_default=False, @@ -254,7 +254,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_voltage_l1", - name="Voltage Phase L1", + name="Voltage phase L1", obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L1, device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, @@ -263,7 +263,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_voltage_l2", - name="Voltage Phase L2", + name="Voltage phase L2", obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L2, device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, @@ -272,7 +272,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_voltage_l3", - name="Voltage Phase L3", + name="Voltage phase L3", obis_reference=obis_references.INSTANTANEOUS_VOLTAGE_L3, device_class=SensorDeviceClass.VOLTAGE, entity_registry_enabled_default=False, @@ -281,7 +281,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_current_l1", - name="Current Phase L1", + name="Current phase L1", obis_reference=obis_references.INSTANTANEOUS_CURRENT_L1, device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, @@ -290,7 +290,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_current_l2", - name="Current Phase L2", + name="Current phase L2", obis_reference=obis_references.INSTANTANEOUS_CURRENT_L2, device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, @@ -299,7 +299,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="instantaneous_current_l3", - name="Current Phase L3", + name="Current phase L3", obis_reference=obis_references.INSTANTANEOUS_CURRENT_L3, device_class=SensorDeviceClass.CURRENT, entity_registry_enabled_default=False, @@ -328,7 +328,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="electricity_imported_total", - name="Energy Consumption (total)", + name="Energy consumption (total)", obis_reference=obis_references.ELECTRICITY_IMPORTED_TOTAL, dsmr_versions={"5L", "5S", "Q3D"}, force_update=True, @@ -337,7 +337,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="electricity_exported_total", - name="Energy Production (total)", + name="Energy production (total)", obis_reference=obis_references.ELECTRICITY_EXPORTED_TOTAL, dsmr_versions={"5L", "5S", "Q3D"}, force_update=True, @@ -346,7 +346,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="hourly_gas_meter_reading", - name="Gas Consumption", + name="Gas consumption", obis_reference=obis_references.HOURLY_GAS_METER_READING, dsmr_versions={"4", "5", "5L"}, is_gas=True, @@ -356,7 +356,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="belgium_5min_gas_meter_reading", - name="Gas Consumption", + name="Gas consumption", obis_reference=obis_references.BELGIUM_5MIN_GAS_METER_READING, dsmr_versions={"5B"}, is_gas=True, @@ -366,7 +366,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( ), DSMRSensorEntityDescription( key="gas_meter_reading", - name="Gas Consumption", + name="Gas consumption", obis_reference=obis_references.GAS_METER_READING, dsmr_versions={"2.2"}, is_gas=True, @@ -513,6 +513,7 @@ class DSMREntity(SensorEntity): """Entity reading values from DSMR telegram.""" entity_description: DSMRSensorEntityDescription + _attr_has_entity_name = True _attr_should_poll = False def __init__( diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index b006765bde7..e8cf763f98e 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -77,18 +77,18 @@ async def test_default_setup(hass, dsmr_connection_fixture): registry = er.async_get(hass) - entry = registry.async_get("sensor.power_consumption") + entry = registry.async_get("sensor.electricity_meter_power_consumption") assert entry assert entry.unique_id == "1234_current_electricity_usage" - entry = registry.async_get("sensor.gas_consumption") + entry = registry.async_get("sensor.gas_meter_gas_consumption") assert entry assert entry.unique_id == "5678_gas_meter_reading" telegram_callback = connection_factory.call_args_list[0][0][2] # make sure entities have been created and return 'unknown' state - power_consumption = hass.states.get("sensor.power_consumption") + power_consumption = hass.states.get("sensor.electricity_meter_power_consumption") assert power_consumption.state == STATE_UNKNOWN assert ( power_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER @@ -107,22 +107,22 @@ async def test_default_setup(hass, dsmr_connection_fixture): await asyncio.sleep(0) # ensure entities have new state value after incoming telegram - power_consumption = hass.states.get("sensor.power_consumption") + power_consumption = hass.states.get("sensor.electricity_meter_power_consumption") assert power_consumption.state == "0.0" assert ( power_consumption.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR ) # tariff should be translated in human readable and have no unit - power_tariff = hass.states.get("sensor.power_tariff") - assert power_tariff.state == "low" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None - assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None - assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" + active_tariff = hass.states.get("sensor.electricity_meter_active_tariff") + assert active_tariff.state == "low" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert active_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert active_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly - gas_consumption = hass.states.get("sensor.gas_consumption") + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( @@ -155,11 +155,11 @@ async def test_setup_only_energy(hass, dsmr_connection_fixture): registry = er.async_get(hass) - entry = registry.async_get("sensor.power_consumption") + entry = registry.async_get("sensor.electricity_meter_power_consumption") assert entry assert entry.unique_id == "1234_current_electricity_usage" - entry = registry.async_get("sensor.gas_consumption") + entry = registry.async_get("sensor.gas_meter_gas_consumption") assert not entry @@ -213,15 +213,15 @@ async def test_v4_meter(hass, dsmr_connection_fixture): await asyncio.sleep(0) # tariff should be translated in human readable and have no unit - power_tariff = hass.states.get("sensor.power_tariff") - assert power_tariff.state == "low" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None - assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None - assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" + active_tariff = hass.states.get("sensor.electricity_meter_active_tariff") + assert active_tariff.state == "low" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert active_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert active_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly - gas_consumption = hass.states.get("sensor.gas_consumption") + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS @@ -284,15 +284,15 @@ async def test_v5_meter(hass, dsmr_connection_fixture): await asyncio.sleep(0) # tariff should be translated in human readable and have no unit - power_tariff = hass.states.get("sensor.power_tariff") - assert power_tariff.state == "low" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None - assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None - assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" + active_tariff = hass.states.get("sensor.electricity_meter_active_tariff") + assert active_tariff.state == "low" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert active_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert active_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly - gas_consumption = hass.states.get("sensor.gas_consumption") + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( @@ -359,24 +359,24 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): # after receiving telegram entities need to have the chance to update await asyncio.sleep(0) - power_tariff = hass.states.get("sensor.energy_consumption_total") - assert power_tariff.state == "123.456" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert power_tariff.attributes.get(ATTR_ICON) is None + active_tariff = hass.states.get("sensor.electricity_meter_energy_consumption_total") + assert active_tariff.state == "123.456" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + assert active_tariff.attributes.get(ATTR_ICON) is None assert ( - power_tariff.attributes.get(ATTR_STATE_CLASS) + active_tariff.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING ) assert ( - power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR ) - power_tariff = hass.states.get("sensor.energy_production_total") - assert power_tariff.state == "654.321" - assert power_tariff.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + active_tariff = hass.states.get("sensor.electricity_meter_energy_production_total") + assert active_tariff.state == "654.321" + assert active_tariff.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR # check if gas consumption is parsed correctly - gas_consumption = hass.states.get("sensor.gas_consumption") + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( @@ -438,15 +438,15 @@ async def test_belgian_meter(hass, dsmr_connection_fixture): await asyncio.sleep(0) # tariff should be translated in human readable and have no unit - power_tariff = hass.states.get("sensor.power_tariff") - assert power_tariff.state == "normal" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None - assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None - assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" + active_tariff = hass.states.get("sensor.electricity_meter_active_tariff") + assert active_tariff.state == "normal" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert active_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert active_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" # check if gas consumption is parsed correctly - gas_consumption = hass.states.get("sensor.gas_consumption") + gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ( @@ -497,12 +497,12 @@ async def test_belgian_meter_low(hass, dsmr_connection_fixture): await asyncio.sleep(0) # tariff should be translated in human readable and have no unit - power_tariff = hass.states.get("sensor.power_tariff") - assert power_tariff.state == "low" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) is None - assert power_tariff.attributes.get(ATTR_ICON) == "mdi:flash" - assert power_tariff.attributes.get(ATTR_STATE_CLASS) is None - assert power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" + active_tariff = hass.states.get("sensor.electricity_meter_active_tariff") + assert active_tariff.state == "low" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) is None + assert active_tariff.attributes.get(ATTR_ICON) == "mdi:flash" + assert active_tariff.attributes.get(ATTR_STATE_CLASS) is None + assert active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "" async def test_swedish_meter(hass, dsmr_connection_fixture): @@ -553,26 +553,26 @@ async def test_swedish_meter(hass, dsmr_connection_fixture): # after receiving telegram entities need to have the chance to update await asyncio.sleep(0) - power_tariff = hass.states.get("sensor.energy_consumption_total") - assert power_tariff.state == "123.456" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert power_tariff.attributes.get(ATTR_ICON) is None + active_tariff = hass.states.get("sensor.electricity_meter_energy_consumption_total") + assert active_tariff.state == "123.456" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + assert active_tariff.attributes.get(ATTR_ICON) is None assert ( - power_tariff.attributes.get(ATTR_STATE_CLASS) + active_tariff.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING ) assert ( - power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR ) - power_tariff = hass.states.get("sensor.energy_production_total") - assert power_tariff.state == "654.321" + active_tariff = hass.states.get("sensor.electricity_meter_energy_production_total") + assert active_tariff.state == "654.321" assert ( - power_tariff.attributes.get(ATTR_STATE_CLASS) + active_tariff.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING ) assert ( - power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR ) @@ -627,26 +627,26 @@ async def test_easymeter(hass, dsmr_connection_fixture): # after receiving telegram entities need to have the chance to update await asyncio.sleep(0) - power_tariff = hass.states.get("sensor.energy_consumption_total") - assert power_tariff.state == "54184.6316" - assert power_tariff.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert power_tariff.attributes.get(ATTR_ICON) is None + active_tariff = hass.states.get("sensor.electricity_meter_energy_consumption_total") + assert active_tariff.state == "54184.6316" + assert active_tariff.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + assert active_tariff.attributes.get(ATTR_ICON) is None assert ( - power_tariff.attributes.get(ATTR_STATE_CLASS) + active_tariff.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING ) assert ( - power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR ) - power_tariff = hass.states.get("sensor.energy_production_total") - assert power_tariff.state == "19981.1069" + active_tariff = hass.states.get("sensor.electricity_meter_energy_production_total") + assert active_tariff.state == "19981.1069" assert ( - power_tariff.attributes.get(ATTR_STATE_CLASS) + active_tariff.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING ) assert ( - power_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR ) From 05d2b955eedb82113710736c1684e89001ea7eae Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 11:31:17 +0200 Subject: [PATCH 2707/3516] Migrate CPUSpeed to new entity naming style (#75080) --- homeassistant/components/cpuspeed/sensor.py | 9 ++++++++- tests/components/cpuspeed/test_sensor.py | 10 +++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index 2c9db7e7300..6b64b8709a3 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -7,8 +7,11 @@ from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import FREQUENCY_GIGAHERTZ from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from .const import DOMAIN + ATTR_BRAND = "brand" ATTR_HZ = "ghz_advertised" ATTR_ARCH = "arch" @@ -30,12 +33,16 @@ class CPUSpeedSensor(SensorEntity): """Representation of a CPU sensor.""" _attr_icon = "mdi:pulse" - _attr_name = "CPU Speed" + _attr_has_entity_name = True _attr_native_unit_of_measurement = FREQUENCY_GIGAHERTZ def __init__(self, entry: ConfigEntry) -> None: """Initialize the CPU sensor.""" self._attr_unique_id = entry.entry_id + self._attr_device_info = DeviceInfo( + name="CPU Speed", + identifiers={(DOMAIN, entry.entry_id)}, + ) def update(self) -> None: """Get the latest data and updates the state.""" diff --git a/tests/components/cpuspeed/test_sensor.py b/tests/components/cpuspeed/test_sensor.py index ebf9f0111bd..068d24b6f3c 100644 --- a/tests/components/cpuspeed/test_sensor.py +++ b/tests/components/cpuspeed/test_sensor.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock +from homeassistant.components.cpuspeed.const import DOMAIN from homeassistant.components.cpuspeed.sensor import ATTR_ARCH, ATTR_BRAND, ATTR_HZ from homeassistant.components.homeassistant import ( DOMAIN as HOME_ASSISTANT_DOMAIN, @@ -15,7 +16,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) 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 tests.common import MockConfigEntry @@ -29,6 +30,7 @@ async def test_sensor( """Test the CPU Speed sensor.""" await async_setup_component(hass, "homeassistant", {}) entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) entry = entity_registry.async_get("sensor.cpu_speed") assert entry @@ -62,6 +64,12 @@ async def test_sensor( assert state.attributes.get(ATTR_BRAND) == "Intel Ryzen 7" assert state.attributes.get(ATTR_HZ) == 3.6 + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, entry.config_entry_id)} + assert device_entry.name == "CPU Speed" + async def test_sensor_partial_info( hass: HomeAssistant, From 93425b0e4d7e9d13ccb69859e3ae9e6a1ba6219e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 11:38:00 +0200 Subject: [PATCH 2708/3516] Migrate Plugwise to new entity naming style (#75109) --- .../components/plugwise/binary_sensor.py | 9 ++- homeassistant/components/plugwise/climate.py | 2 +- homeassistant/components/plugwise/entity.py | 4 +- .../components/plugwise/manifest.json | 2 +- homeassistant/components/plugwise/number.py | 3 +- homeassistant/components/plugwise/select.py | 5 +- homeassistant/components/plugwise/sensor.py | 61 +++++++++---------- homeassistant/components/plugwise/switch.py | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 10 files changed, 45 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/plugwise/binary_sensor.py b/homeassistant/components/plugwise/binary_sensor.py index 129f4faef92..6f751b82b35 100644 --- a/homeassistant/components/plugwise/binary_sensor.py +++ b/homeassistant/components/plugwise/binary_sensor.py @@ -31,14 +31,14 @@ class PlugwiseBinarySensorEntityDescription(BinarySensorEntityDescription): BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = ( PlugwiseBinarySensorEntityDescription( key="dhw_state", - name="DHW State", + name="DHW state", icon="mdi:water-pump", icon_off="mdi:water-pump-off", entity_category=EntityCategory.DIAGNOSTIC, ), PlugwiseBinarySensorEntityDescription( key="flame_state", - name="Flame State", + name="Flame state", icon="mdi:fire", icon_off="mdi:fire-off", entity_category=EntityCategory.DIAGNOSTIC, @@ -59,14 +59,14 @@ BINARY_SENSORS: tuple[PlugwiseBinarySensorEntityDescription, ...] = ( ), PlugwiseBinarySensorEntityDescription( key="slave_boiler_state", - name="Secondary Boiler State", + name="Secondary boiler state", icon="mdi:fire", icon_off="mdi:circle-off-outline", entity_category=EntityCategory.DIAGNOSTIC, ), PlugwiseBinarySensorEntityDescription( key="plugwise_notification", - name="Plugwise Notification", + name="Plugwise notification", icon="mdi:mailbox-up-outline", icon_off="mdi:mailbox-outline", entity_category=EntityCategory.DIAGNOSTIC, @@ -118,7 +118,6 @@ class PlugwiseBinarySensorEntity(PlugwiseEntity, BinarySensorEntity): super().__init__(coordinator, device_id) self.entity_description = description self._attr_unique_id = f"{device_id}-{description.key}" - self._attr_name = (f"{self.device.get('name', '')} {description.name}").lstrip() @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index c74a1baa9be..9729ad745fd 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -39,6 +39,7 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): """Representation of an Plugwise thermostat.""" _attr_temperature_unit = TEMP_CELSIUS + _attr_has_entity_name = True def __init__( self, @@ -49,7 +50,6 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): super().__init__(coordinator, device_id) self._attr_extra_state_attributes = {} self._attr_unique_id = f"{device_id}-climate" - self._attr_name = self.device.get("name") # Determine preset modes self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE diff --git a/homeassistant/components/plugwise/entity.py b/homeassistant/components/plugwise/entity.py index 491eb7c7db8..694f6e5817c 100644 --- a/homeassistant/components/plugwise/entity.py +++ b/homeassistant/components/plugwise/entity.py @@ -18,6 +18,8 @@ from .coordinator import PlugwiseDataUpdateCoordinator class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]): """Represent a PlugWise Entity.""" + _attr_has_entity_name = True + def __init__( self, coordinator: PlugwiseDataUpdateCoordinator, @@ -44,7 +46,7 @@ class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]): connections=connections, manufacturer=data.get("vendor"), model=data.get("model"), - name=f"Smile {coordinator.data.gateway['smile_name']}", + name=coordinator.data.gateway["smile_name"], sw_version=data.get("firmware"), hw_version=data.get("hardware"), ) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index f0e7f98d4b8..bf7fc453f89 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.18.6"], + "requirements": ["plugwise==0.18.7"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/homeassistant/components/plugwise/number.py b/homeassistant/components/plugwise/number.py index 6718c59c3c0..380be9111ba 100644 --- a/homeassistant/components/plugwise/number.py +++ b/homeassistant/components/plugwise/number.py @@ -42,7 +42,7 @@ NUMBER_TYPES = ( key="maximum_boiler_temperature", command=lambda api, value: api.set_max_boiler_temperature(value), device_class=NumberDeviceClass.TEMPERATURE, - name="Maximum Boiler Temperature Setpoint", + name="Maximum boiler temperature setpoint", entity_category=EntityCategory.CONFIG, native_unit_of_measurement=TEMP_CELSIUS, ), @@ -86,7 +86,6 @@ class PlugwiseNumberEntity(PlugwiseEntity, NumberEntity): super().__init__(coordinator, device_id) self.entity_description = description self._attr_unique_id = f"{device_id}-{description.key}" - self._attr_name = (f"{self.device['name']} {description.name}").lstrip() self._attr_mode = NumberMode.BOX @property diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index cac9c3e5637..7afe76e1a8a 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -38,7 +38,7 @@ class PlugwiseSelectEntityDescription( SELECT_TYPES = ( PlugwiseSelectEntityDescription( key="select_schedule", - name="Thermostat Schedule", + name="Thermostat schedule", icon="mdi:calendar-clock", command=lambda api, loc, opt: api.set_schedule_state(loc, opt, STATE_ON), current_option="selected_schedule", @@ -46,7 +46,7 @@ SELECT_TYPES = ( ), PlugwiseSelectEntityDescription( key="select_regulation_mode", - name="Regulation Mode", + name="Regulation mode", icon="mdi:hvac", entity_category=EntityCategory.CONFIG, command=lambda api, loc, opt: api.set_regulation_mode(opt), @@ -92,7 +92,6 @@ class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity): super().__init__(coordinator, device_id) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_name = (f"{self.device['name']} {entity_description.name}").lstrip() @property def current_option(self) -> str: diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 87c81699d10..3ee0437e8dd 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -41,182 +41,182 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="intended_boiler_temperature", - name="Intended Boiler Temperature", + name="Intended boiler temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="temperature_difference", - name="Temperature Difference", + name="Temperature difference", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="outdoor_temperature", - name="Outdoor Temperature", + name="Outdoor temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="outdoor_air_temperature", - name="Outdoor Air Temperature", + name="Outdoor air temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="water_temperature", - name="Water Temperature", + name="Water temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="return_temperature", - name="Return Temperature", + name="Return temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_consumed", - name="Electricity Consumed", + name="Electricity consumed", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_produced", - name="Electricity Produced", + name="Electricity produced", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_consumed_interval", - name="Electricity Consumed Interval", + name="Electricity consumed interval", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_consumed_peak_interval", - name="Electricity Consumed Peak Interval", + name="Electricity consumed peak interval", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_consumed_off_peak_interval", - name="Electricity Consumed Off Peak Interval", + name="Electricity consumed off peak interval", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_produced_interval", - name="Electricity Produced Interval", + name="Electricity produced interval", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_produced_peak_interval", - name="Electricity Produced Peak Interval", + name="Electricity produced peak interval", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_produced_off_peak_interval", - name="Electricity Produced Off Peak Interval", + name="Electricity produced off peak interval", native_unit_of_measurement=ENERGY_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="electricity_consumed_off_peak_point", - name="Electricity Consumed Off Peak Point", + name="Electricity consumed off peak point", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_consumed_peak_point", - name="Electricity Consumed Peak Point", + name="Electricity consumed peak point", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_consumed_off_peak_cumulative", - name="Electricity Consumed Off Peak Cumulative", + name="Electricity consumed off peak cumulative", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="electricity_consumed_peak_cumulative", - name="Electricity Consumed Peak Cumulative", + name="Electricity consumed peak cumulative", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="electricity_produced_off_peak_point", - name="Electricity Produced Off Peak Point", + name="Electricity produced off peak point", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_produced_peak_point", - name="Electricity Produced Peak Point", + name="Electricity produced peak point", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="electricity_produced_off_peak_cumulative", - name="Electricity Produced Off Peak Cumulative", + name="Electricity produced off peak cumulative", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="electricity_produced_peak_cumulative", - name="Electricity Produced Peak Cumulative", + name="Electricity produced peak cumulative", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key="gas_consumed_interval", - name="Gas Consumed Interval", + name="Gas consumed interval", native_unit_of_measurement=VOLUME_CUBIC_METERS, device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="gas_consumed_cumulative", - name="Gas Consumed Cumulative", + name="Gas consumed cumulative", native_unit_of_measurement=VOLUME_CUBIC_METERS, device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key="net_electricity_point", - name="Net Electricity Point", + name="Net electricity point", native_unit_of_measurement=POWER_WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="net_electricity_cumulative", - name="Net Electricity Cumulative", + name="Net electricity cumulative", native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, @@ -237,28 +237,28 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( ), SensorEntityDescription( key="modulation_level", - name="Modulation Level", + name="Modulation level", icon="mdi:percent", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="valve_position", - name="Valve Position", + name="Valve position", icon="mdi:valve", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="water_pressure", - name="Water Pressure", + name="Water pressure", native_unit_of_measurement=PRESSURE_BAR, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key="humidity", - name="Relative Humidity", + name="Relative humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, @@ -307,7 +307,6 @@ class PlugwiseSensorEntity(PlugwiseEntity, SensorEntity): super().__init__(coordinator, device_id) self.entity_description = description self._attr_unique_id = f"{device_id}-{description.key}" - self._attr_name = (f"{self.device.get('name', '')} {description.name}").lstrip() @property def native_value(self) -> int | float | None: diff --git a/homeassistant/components/plugwise/switch.py b/homeassistant/components/plugwise/switch.py index 45a10297ed5..c2942308b75 100644 --- a/homeassistant/components/plugwise/switch.py +++ b/homeassistant/components/plugwise/switch.py @@ -21,7 +21,7 @@ from .util import plugwise_command SWITCHES: tuple[SwitchEntityDescription, ...] = ( SwitchEntityDescription( key="dhw_cm_switch", - name="DHW Comfort Mode", + name="DHW comfort mode", icon="mdi:water-plus", entity_category=EntityCategory.CONFIG, ), @@ -68,7 +68,6 @@ class PlugwiseSwitchEntity(PlugwiseEntity, SwitchEntity): super().__init__(coordinator, device_id) self.entity_description = description self._attr_unique_id = f"{device_id}-{description.key}" - self._attr_name = (f"{self.device.get('name', '')} {description.name}").lstrip() @property def is_on(self) -> bool | None: diff --git a/requirements_all.txt b/requirements_all.txt index 4fb9282d0ce..cee83671b41 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1257,7 +1257,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.18.6 +plugwise==0.18.7 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ea366315f7..7bab6fbb9d6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -871,7 +871,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.18.6 +plugwise==0.18.7 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 From 2db8b154c9ff5b3f8ff935500fdad5c76f287fe4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 11:39:07 +0200 Subject: [PATCH 2709/3516] Update orjson to 3.7.8 (#75484) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0d81d485829..d787cec62f2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 -orjson==3.7.7 +orjson==3.7.8 paho-mqtt==1.6.1 pillow==9.2.0 pip>=21.0,<22.2 diff --git a/pyproject.toml b/pyproject.toml index 9b4c99c1152..eb1209234be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dependencies = [ "PyJWT==2.4.0", # PyJWT has loose dependency. We want the latest one. "cryptography==36.0.2", - "orjson==3.7.7", + "orjson==3.7.8", "pip>=21.0,<22.2", "python-slugify==4.0.1", "pyyaml==6.0", diff --git a/requirements.txt b/requirements.txt index ea29bc59435..4785d344e64 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ jinja2==3.1.2 lru-dict==1.1.8 PyJWT==2.4.0 cryptography==36.0.2 -orjson==3.7.7 +orjson==3.7.8 pip>=21.0,<22.2 python-slugify==4.0.1 pyyaml==6.0 From fe97f6791d938190aebe086e5499153530bd0e02 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 11:40:34 +0200 Subject: [PATCH 2710/3516] Map % RH unit in Tuya sensors (#75483) --- 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 727e505200b..792036e49ff 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -433,7 +433,7 @@ UNITS = ( ), UnitOfMeasurement( unit=PERCENTAGE, - aliases={"pct", "percent"}, + aliases={"pct", "percent", "% RH"}, device_classes={ SensorDeviceClass.BATTERY, SensorDeviceClass.HUMIDITY, From 460837e453bfb873dc40357d3b870934f48e85e1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 11:43:46 +0200 Subject: [PATCH 2711/3516] Allow account linking to phase out services (#75447) --- .../components/cloud/account_link.py | 15 +++++- tests/components/cloud/test_account_link.py | 47 ++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index 5df16cb1724..19819307cf0 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -33,7 +33,20 @@ async def async_provide_implementation(hass: HomeAssistant, domain: str): services = await _get_services(hass) for service in services: - if service["service"] == domain and CURRENT_VERSION >= service["min_version"]: + if ( + service["service"] == domain + and CURRENT_VERSION >= service["min_version"] + and ( + service.get("accepts_new_authorizations", True) + or ( + (entries := hass.config_entries.async_entries(domain)) + and any( + entry.data.get("auth_implementation") == DOMAIN + for entry in entries + ) + ) + ) + ): return [CloudOAuth2Implementation(hass, domain)] return [] diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index 16b43d8a3c8..ad914436c07 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -11,7 +11,7 @@ from homeassistant.components.cloud import account_link from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed, mock_platform +from tests.common import MockConfigEntry, async_fire_time_changed, mock_platform TEST_DOMAIN = "oauth2_test" @@ -38,6 +38,18 @@ def flow_handler(hass): async def test_setup_provide_implementation(hass): """Test that we provide implementations.""" + legacy_entry = MockConfigEntry( + domain="legacy", + version=1, + data={"auth_implementation": "cloud"}, + ) + none_cloud_entry = MockConfigEntry( + domain="no_cloud", + version=1, + data={"auth_implementation": "somethingelse"}, + ) + none_cloud_entry.add_to_hass(hass) + legacy_entry.add_to_hass(hass) account_link.async_setup(hass) with patch( @@ -45,6 +57,21 @@ async def test_setup_provide_implementation(hass): return_value=[ {"service": "test", "min_version": "0.1.0"}, {"service": "too_new", "min_version": "1000000.0.0"}, + { + "service": "deprecated", + "min_version": "0.1.0", + "accepts_new_authorizations": False, + }, + { + "service": "legacy", + "min_version": "0.1.0", + "accepts_new_authorizations": False, + }, + { + "service": "no_cloud", + "min_version": "0.1.0", + "accepts_new_authorizations": False, + }, ], ): assert ( @@ -57,15 +84,33 @@ async def test_setup_provide_implementation(hass): await config_entry_oauth2_flow.async_get_implementations(hass, "too_new") == {} ) + assert ( + await config_entry_oauth2_flow.async_get_implementations(hass, "deprecated") + == {} + ) + assert ( + await config_entry_oauth2_flow.async_get_implementations(hass, "no_cloud") + == {} + ) + implementations = await config_entry_oauth2_flow.async_get_implementations( hass, "test" ) + legacy_implementations = ( + await config_entry_oauth2_flow.async_get_implementations(hass, "legacy") + ) + assert "cloud" in implementations assert implementations["cloud"].domain == "cloud" assert implementations["cloud"].service == "test" assert implementations["cloud"].hass is hass + assert "cloud" in legacy_implementations + assert legacy_implementations["cloud"].domain == "cloud" + assert legacy_implementations["cloud"].service == "legacy" + assert legacy_implementations["cloud"].hass is hass + async def test_get_services_cached(hass): """Test that we cache services.""" From 11e7ddaa7141b9ae08e7f15130aea8bc02b4d9df Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 11:58:17 +0200 Subject: [PATCH 2712/3516] Plugwise prefer use of Adam instead of Anna (#75161) Co-authored-by: Martin Hjelmare --- .../components/plugwise/config_flow.py | 27 ++++++ .../components/plugwise/strings.json | 3 +- tests/components/plugwise/test_config_flow.py | 86 +++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index fbfdca00b41..3fee1445758 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -119,6 +119,32 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN): _version = _properties.get("version", "n/a") _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}" + # This is an Anna, but we already have config entries. + # Assuming that the user has already configured Adam, aborting discovery. + if self._async_current_entries() and _product == "smile_thermo": + return self.async_abort(reason="anna_with_adam") + + # If we have discovered an Adam or Anna, both might be on the network. + # In that case, we need to cancel the Anna flow, as the Adam should + # be added. + for flow in self._async_in_progress(): + # This is an Anna, and there is already an Adam flow in progress + if ( + _product == "smile_thermo" + and "context" in flow + and flow["context"].get("product") == "smile_open_therm" + ): + return self.async_abort(reason="anna_with_adam") + + # This is an Adam, and there is already an Anna flow in progress + if ( + _product == "smile_open_therm" + and "context" in flow + and flow["context"].get("product") == "smile_thermo" + and "flow_id" in flow + ): + self.hass.config_entries.flow.async_abort(flow["flow_id"]) + self.context.update( { "title_placeholders": { @@ -128,6 +154,7 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN): CONF_USERNAME: self._username, }, "configuration_url": f"http://{discovery_info.host}:{discovery_info.port}", + "product": _product, } ) return await self.async_step_user() diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index 42dcee96196..7278f6c4414 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -19,7 +19,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna" } } } diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 66f0986682e..4dbe1d2615f 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -61,6 +61,34 @@ TEST_DISCOVERY2 = ZeroconfServiceInfo( type="mock_type", ) +TEST_DISCOVERY_ANNA = ZeroconfServiceInfo( + host=TEST_HOST, + addresses=[TEST_HOST], + hostname=f"{TEST_HOSTNAME}.local.", + name="mock_name", + port=DEFAULT_PORT, + properties={ + "product": "smile_thermo", + "version": "1.2.3", + "hostname": f"{TEST_HOSTNAME}.local.", + }, + type="mock_type", +) + +TEST_DISCOVERY_ADAM = ZeroconfServiceInfo( + host=TEST_HOST, + addresses=[TEST_HOST], + hostname=f"{TEST_HOSTNAME2}.local.", + name="mock_name", + port=DEFAULT_PORT, + properties={ + "product": "smile_open_therm", + "version": "1.2.3", + "hostname": f"{TEST_HOSTNAME2}.local.", + }, + type="mock_type", +) + @pytest.fixture(name="mock_smile") def mock_smile(): @@ -294,3 +322,61 @@ async def test_flow_errors( assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_smile_config_flow.connect.mock_calls) == 2 + + +async def test_zeroconf_abort_anna_with_existing_config_entries( + hass: HomeAssistant, + mock_smile_adam: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test we abort Anna discovery with existing config entries.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data=TEST_DISCOVERY_ANNA, + ) + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "anna_with_adam" + + +async def test_zeroconf_abort_anna_with_adam(hass: HomeAssistant) -> None: + """Test we abort Anna discovery when an Adam is also discovered.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data=TEST_DISCOVERY_ANNA, + ) + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "user" + + flows_in_progress = hass.config_entries.flow.async_progress() + assert len(flows_in_progress) == 1 + assert flows_in_progress[0]["context"]["product"] == "smile_thermo" + + # Discover Adam, Anna should be aborted and no longer present + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data=TEST_DISCOVERY_ADAM, + ) + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "user" + + flows_in_progress = hass.config_entries.flow.async_progress() + assert len(flows_in_progress) == 1 + assert flows_in_progress[0]["context"]["product"] == "smile_open_therm" + + # Discover Anna again, Anna should be aborted directly + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_ZEROCONF}, + data=TEST_DISCOVERY_ANNA, + ) + assert result3.get("type") == FlowResultType.ABORT + assert result3.get("reason") == "anna_with_adam" + + # Adam should still be there + flows_in_progress = hass.config_entries.flow.async_progress() + assert len(flows_in_progress) == 1 + assert flows_in_progress[0]["context"]["product"] == "smile_open_therm" From 5ef92e5e956de4aada3fbe990f371116f219e7fa Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 20 Jul 2022 11:58:54 +0200 Subject: [PATCH 2713/3516] Fix MQTT race awaiting an ACK when disconnecting (#75117) Co-authored-by: Erik --- homeassistant/components/mqtt/client.py | 54 +++++++++++++------------ tests/components/mqtt/test_init.py | 46 +++++++++++++++++++-- 2 files changed, 72 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 9eeed426d17..6e6f67e4e7a 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -325,11 +325,11 @@ class MQTT: self._ha_started = asyncio.Event() self._last_subscribe = time.time() self._mqttc: mqtt.Client = None - self._paho_lock = asyncio.Lock() - self._pending_acks: set[int] = set() self._cleanup_on_unload: list[Callable] = [] - self._pending_operations: dict[str, asyncio.Event] = {} + self._paho_lock = asyncio.Lock() # Prevents parallel calls to the MQTT client + self._pending_operations: dict[int, asyncio.Event] = {} + self._pending_operations_condition = asyncio.Condition() if self.hass.state == CoreState.running: self._ha_started.set() @@ -431,13 +431,13 @@ class MQTT: # Do not disconnect, we want the broker to always publish will self._mqttc.loop_stop() - # wait for ACK-s to be processes (unsubscribe only) - async with self._paho_lock: - tasks = [ - self.hass.async_create_task(self._wait_for_mid(mid)) - for mid in self._pending_acks - ] - await asyncio.gather(*tasks) + def no_more_acks() -> bool: + """Return False if there are unprocessed ACKs.""" + return not bool(self._pending_operations) + + # wait for ACK-s to be processesed (unsubscribe only) + async with self._pending_operations_condition: + await self._pending_operations_condition.wait_for(no_more_acks) # stop the MQTT loop await self.hass.async_add_executor_job(stop) @@ -487,19 +487,21 @@ class MQTT: This method is a coroutine. """ - def _client_unsubscribe(topic: str) -> None: + def _client_unsubscribe(topic: str) -> int: result: int | None = None result, mid = self._mqttc.unsubscribe(topic) _LOGGER.debug("Unsubscribing from %s, mid: %s", topic, mid) _raise_on_error(result) - self._pending_acks.add(mid) + return mid if any(other.topic == topic for other in self.subscriptions): # Other subscriptions on topic remaining - don't unsubscribe. return async with self._paho_lock: - await self.hass.async_add_executor_job(_client_unsubscribe, topic) + mid = await self.hass.async_add_executor_job(_client_unsubscribe, topic) + await self._register_mid(mid) + self.hass.async_create_task(self._wait_for_mid(mid)) async def _async_perform_subscriptions( self, subscriptions: Iterable[tuple[str, int]] @@ -647,14 +649,18 @@ class MQTT: """Publish / Subscribe / Unsubscribe callback.""" self.hass.add_job(self._mqtt_handle_mid, mid) - @callback - def _mqtt_handle_mid(self, mid) -> None: + async def _mqtt_handle_mid(self, mid: int) -> None: # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid # may be executed first. - if mid not in self._pending_operations: - self._pending_operations[mid] = asyncio.Event() + await self._register_mid(mid) self._pending_operations[mid].set() + async def _register_mid(self, mid: int) -> None: + """Create Event for an expected ACK.""" + async with self._pending_operations_condition: + if mid not in self._pending_operations: + self._pending_operations[mid] = asyncio.Event() + def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None: """Disconnected callback.""" self.connected = False @@ -666,12 +672,11 @@ class MQTT: result_code, ) - async def _wait_for_mid(self, mid): + async def _wait_for_mid(self, mid: int) -> None: """Wait for ACK from broker.""" # Create the mid event if not created, either _mqtt_handle_mid or _wait_for_mid # may be executed first. - if mid not in self._pending_operations: - self._pending_operations[mid] = asyncio.Event() + await self._register_mid(mid) try: await asyncio.wait_for(self._pending_operations[mid].wait(), TIMEOUT_ACK) except asyncio.TimeoutError: @@ -679,11 +684,10 @@ class MQTT: "No ACK from MQTT server in %s seconds (mid: %s)", TIMEOUT_ACK, mid ) finally: - del self._pending_operations[mid] - # Cleanup ACK sync buffer - async with self._paho_lock: - if mid in self._pending_acks: - self._pending_acks.remove(mid) + async with self._pending_operations_condition: + # Cleanup ACK sync buffer + del self._pending_operations[mid] + self._pending_operations_condition.notify_all() async def _discovery_cooldown(self): now = time.time() diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index de63528a08b..cec6038ae04 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -4,7 +4,6 @@ import copy from datetime import datetime, timedelta from functools import partial import json -import logging import ssl from unittest.mock import ANY, AsyncMock, MagicMock, call, mock_open, patch @@ -47,8 +46,6 @@ from tests.common import ( ) from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES -_LOGGER = logging.getLogger(__name__) - class RecordCallsPartial(partial): """Wrapper class for partial.""" @@ -141,6 +138,49 @@ async def test_mqtt_disconnects_on_home_assistant_stop( assert mqtt_client_mock.loop_stop.call_count == 1 +@patch("homeassistant.components.mqtt.PLATFORMS", []) +async def test_mqtt_await_ack_at_disconnect( + hass, +): + """Test if ACK is awaited correctly when disconnecting.""" + + class FakeInfo: + """Returns a simulated client publish response.""" + + mid = 100 + rc = 0 + + with patch("paho.mqtt.client.Client") as mock_client: + mock_client().connect = MagicMock(return_value=0) + mock_client().publish = MagicMock(return_value=FakeInfo()) + entry = MockConfigEntry( + domain=mqtt.DOMAIN, + data={"certificate": "auto", mqtt.CONF_BROKER: "test-broker"}, + ) + entry.add_to_hass(hass) + assert await mqtt.async_setup_entry(hass, entry) + mqtt_client = mock_client.return_value + + # publish from MQTT client without awaiting + hass.async_create_task( + mqtt.async_publish(hass, "test-topic", "some-payload", 0, False) + ) + await asyncio.sleep(0) + # Simulate late ACK callback from client with mid 100 + mqtt_client.on_publish(0, 0, 100) + # disconnect the MQTT client + await hass.async_stop() + await hass.async_block_till_done() + # assert the payload was sent through the client + assert mqtt_client.publish.called + assert mqtt_client.publish.call_args[0] == ( + "test-topic", + "some-payload", + 0, + False, + ) + + async def test_publish(hass, mqtt_mock_entry_no_yaml_config): """Test the publish function.""" mqtt_mock = await mqtt_mock_entry_no_yaml_config() From a3b2b5c328a5e17929b2bcd54c9ed3c5acb44a36 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 20 Jul 2022 12:03:30 +0200 Subject: [PATCH 2714/3516] Add zha typing [classmethods] (#75472) --- homeassistant/components/zha/button.py | 14 +++++---- homeassistant/components/zha/core/device.py | 8 +++-- homeassistant/components/zha/entity.py | 29 ++++++++++++----- homeassistant/components/zha/number.py | 14 ++++++--- homeassistant/components/zha/select.py | 16 ++++++---- homeassistant/components/zha/sensor.py | 35 ++++++++++++--------- homeassistant/components/zha/switch.py | 10 ++++-- 7 files changed, 81 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py index 0f98bfaad51..fcc040cbde2 100644 --- a/homeassistant/components/zha/button.py +++ b/homeassistant/components/zha/button.py @@ -4,7 +4,7 @@ from __future__ import annotations import abc import functools import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar import zigpy.exceptions from zigpy.zcl.foundation import Status @@ -27,6 +27,8 @@ if TYPE_CHECKING: from .core.device import ZHADevice +_ZHAIdentifyButtonSelfT = TypeVar("_ZHAIdentifyButtonSelfT", bound="ZHAIdentifyButton") + MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.BUTTON) CONFIG_DIAGNOSTIC_MATCH = functools.partial( ZHA_ENTITIES.config_diagnostic_match, Platform.BUTTON @@ -66,7 +68,7 @@ class ZHAButton(ZhaEntity, ButtonEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this button.""" super().__init__(unique_id, zha_device, channels, **kwargs) @@ -89,12 +91,12 @@ class ZHAIdentifyButton(ZHAButton): @classmethod def create_entity( - cls, + cls: type[_ZHAIdentifyButtonSelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _ZHAIdentifyButtonSelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None @@ -126,7 +128,7 @@ class ZHAAttributeButton(ZhaEntity, ButtonEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this button.""" super().__init__(unique_id, zha_device, channels, **kwargs) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 4719b6bf585..150241a091b 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -9,7 +9,7 @@ from functools import cached_property import logging import random import time -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar from zigpy import types import zigpy.device @@ -86,6 +86,8 @@ _LOGGER = logging.getLogger(__name__) _UPDATE_ALIVE_INTERVAL = (60, 90) _CHECKIN_GRACE_PERIODS = 2 +_ZHADeviceSelfT = TypeVar("_ZHADeviceSelfT", bound="ZHADevice") + class DeviceStatus(Enum): """Status of a device.""" @@ -340,12 +342,12 @@ class ZHADevice(LogMixin): @classmethod def new( - cls, + cls: type[_ZHADeviceSelfT], hass: HomeAssistant, zigpy_dev: zigpy.device.Device, gateway: ZHAGateway, restored: bool = False, - ): + ) -> _ZHADeviceSelfT: """Create new device.""" zha_dev = cls(hass, zigpy_dev, gateway) zha_dev.channels = channels.Channels.new(zha_dev) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 8b3627df9de..2f609555c79 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -5,7 +5,7 @@ import asyncio from collections.abc import Callable import functools import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar from homeassistant.const import ATTR_NAME from homeassistant.core import CALLBACK_TYPE, Event, callback @@ -35,6 +35,9 @@ if TYPE_CHECKING: from .core.channels.base import ZigbeeChannel from .core.device import ZHADevice +_ZhaEntitySelfT = TypeVar("_ZhaEntitySelfT", bound="ZhaEntity") +_ZhaGroupEntitySelfT = TypeVar("_ZhaGroupEntitySelfT", bound="ZhaGroupEntity") + _LOGGER = logging.getLogger(__name__) ENTITY_SUFFIX = "entity_suffix" @@ -155,7 +158,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): class ZhaEntity(BaseZhaEntity, RestoreEntity): """A base class for non group ZHA entities.""" - def __init_subclass__(cls, id_suffix: str | None = None, **kwargs) -> None: + def __init_subclass__(cls, id_suffix: str | None = None, **kwargs: Any) -> None: """Initialize subclass. :param id_suffix: suffix to add to the unique_id of the entity. Used for multi @@ -187,12 +190,12 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): @classmethod def create_entity( - cls, + cls: type[_ZhaEntitySelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _ZhaEntitySelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None @@ -257,7 +260,12 @@ class ZhaGroupEntity(BaseZhaEntity): """A base class for ZHA group entities.""" def __init__( - self, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs + self, + entity_ids: list[str], + unique_id: str, + group_id: int, + zha_device: ZHADevice, + **kwargs: Any, ) -> None: """Initialize a light group.""" super().__init__(unique_id, zha_device, **kwargs) @@ -279,8 +287,13 @@ class ZhaGroupEntity(BaseZhaEntity): @classmethod def create_entity( - cls, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs - ) -> ZhaGroupEntity | None: + cls: type[_ZhaGroupEntitySelfT], + entity_ids: list[str], + unique_id: str, + group_id: int, + zha_device: ZHADevice, + **kwargs: Any, + ) -> _ZhaGroupEntitySelfT | None: """Group Entity Factory. Return entity if it is a supported configuration, otherwise return None diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 76b1121c2f0..4252bf0e14c 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -3,7 +3,7 @@ from __future__ import annotations import functools import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, TypeVar import zigpy.exceptions from zigpy.zcl.foundation import Status @@ -33,6 +33,10 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) +_ZHANumberConfigurationEntitySelfT = TypeVar( + "_ZHANumberConfigurationEntitySelfT", bound="ZHANumberConfigurationEntity" +) + STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.NUMBER) CONFIG_DIAGNOSTIC_MATCH = functools.partial( ZHA_ENTITIES.config_diagnostic_match, Platform.NUMBER @@ -368,12 +372,12 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity): @classmethod def create_entity( - cls, + cls: type[_ZHANumberConfigurationEntitySelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _ZHANumberConfigurationEntitySelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None @@ -397,7 +401,7 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this number configuration entity.""" self._channel: ZigbeeChannel = channels[0] diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index e2835d4acd4..503c5a013a8 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -4,7 +4,7 @@ from __future__ import annotations from enum import Enum import functools import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, TypeVar from zigpy import types from zigpy.zcl.clusters.general import OnOff @@ -34,6 +34,10 @@ if TYPE_CHECKING: from .core.device import ZHADevice +_ZCLEnumSelectEntitySelfT = TypeVar( + "_ZCLEnumSelectEntitySelfT", bound="ZCLEnumSelectEntity" +) + CONFIG_DIAGNOSTIC_MATCH = functools.partial( ZHA_ENTITIES.config_diagnostic_match, Platform.SELECT ) @@ -72,7 +76,7 @@ class ZHAEnumSelectEntity(ZhaEntity, SelectEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this select entity.""" self._attr_name = self._enum.__name__ @@ -154,12 +158,12 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity): @classmethod def create_entity( - cls, + cls: type[_ZCLEnumSelectEntitySelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _ZCLEnumSelectEntitySelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None @@ -183,7 +187,7 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this select entity.""" self._attr_options = [entry.name.replace("_", " ") for entry in self._enum] diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 513ba5510b5..f42e88041ef 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import functools import numbers -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar from homeassistant.components.climate.const import HVACAction from homeassistant.components.sensor import ( @@ -69,6 +69,13 @@ if TYPE_CHECKING: from .core.channels.base import ZigbeeChannel from .core.device import ZHADevice +_SensorSelfT = TypeVar("_SensorSelfT", bound="Sensor") +_BatterySelfT = TypeVar("_BatterySelfT", bound="Battery") +_ThermostatHVACActionSelfT = TypeVar( + "_ThermostatHVACActionSelfT", bound="ThermostatHVACAction" +) +_RSSISensorSelfT = TypeVar("_RSSISensorSelfT", bound="RSSISensor") + PARALLEL_UPDATES = 5 BATTERY_SIZES = { @@ -126,7 +133,7 @@ class Sensor(ZhaEntity, SensorEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) @@ -134,12 +141,12 @@ class Sensor(ZhaEntity, SensorEntity): @classmethod def create_entity( - cls, + cls: type[_SensorSelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _SensorSelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None @@ -214,12 +221,12 @@ class Battery(Sensor): @classmethod def create_entity( - cls, + cls: type[_BatterySelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _BatterySelfT | None: """Entity Factory. Unlike any other entity, PowerConfiguration cluster may not support @@ -641,12 +648,12 @@ class ThermostatHVACAction(Sensor, id_suffix="hvac_action"): @classmethod def create_entity( - cls, + cls: type[_ThermostatHVACActionSelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _ThermostatHVACActionSelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None @@ -767,12 +774,12 @@ class RSSISensor(Sensor, id_suffix="rssi"): @classmethod def create_entity( - cls, + cls: type[_RSSISensorSelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, - ) -> ZhaEntity | None: + **kwargs: Any, + ) -> _RSSISensorSelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 3b044bb7646..881401f31da 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -3,7 +3,7 @@ from __future__ import annotations import functools import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar import zigpy.exceptions from zigpy.zcl.clusters.general import OnOff @@ -31,6 +31,10 @@ if TYPE_CHECKING: from .core.channels.base import ZigbeeChannel from .core.device import ZHADevice +_ZHASwitchConfigurationEntitySelfT = TypeVar( + "_ZHASwitchConfigurationEntitySelfT", bound="ZHASwitchConfigurationEntity" +) + STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SWITCH) GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.SWITCH) CONFIG_DIAGNOSTIC_MATCH = functools.partial( @@ -172,12 +176,12 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): @classmethod def create_entity( - cls, + cls: type[_ZHASwitchConfigurationEntitySelfT], unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], **kwargs: Any, - ) -> ZhaEntity | None: + ) -> _ZHASwitchConfigurationEntitySelfT | None: """Entity Factory. Return entity if it is a supported configuration, otherwise return None From 39dc9aa1795c25599cc794faf216d0b4ca950fd4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 12:06:52 +0200 Subject: [PATCH 2715/3516] Rename Resolution Center -> Repairs (#75486) --- .strict-typing | 2 +- CODEOWNERS | 4 +- .../__init__.py | 8 +-- homeassistant/components/repairs/const.py | 3 + .../issue_handler.py | 38 ++++++------- .../issue_registry.py | 2 +- .../manifest.json | 6 +- .../{resolution_center => repairs}/models.py | 10 ++-- .../websocket_api.py | 28 ++++----- .../components/resolution_center/const.py | 3 - mypy.ini | 2 +- script/hassfest/manifest.py | 2 +- tests/components/repairs/__init__.py | 1 + .../test_init.py | 51 ++++++++--------- .../test_issue_registry.py | 11 ++-- .../test_websocket_api.py | 57 +++++++++---------- .../components/resolution_center/__init__.py | 1 - 17 files changed, 106 insertions(+), 123 deletions(-) rename homeassistant/components/{resolution_center => repairs}/__init__.py (66%) create mode 100644 homeassistant/components/repairs/const.py rename homeassistant/components/{resolution_center => repairs}/issue_handler.py (75%) rename homeassistant/components/{resolution_center => repairs}/issue_registry.py (99%) rename homeassistant/components/{resolution_center => repairs}/manifest.json (65%) rename homeassistant/components/{resolution_center => repairs}/models.py (69%) rename homeassistant/components/{resolution_center => repairs}/websocket_api.py (83%) delete mode 100644 homeassistant/components/resolution_center/const.py create mode 100644 tests/components/repairs/__init__.py rename tests/components/{resolution_center => repairs}/test_init.py (87%) rename tests/components/{resolution_center => repairs}/test_issue_registry.py (91%) rename tests/components/{resolution_center => repairs}/test_websocket_api.py (86%) delete mode 100644 tests/components/resolution_center/__init__.py diff --git a/.strict-typing b/.strict-typing index 46d85aa1ec2..7616589a7b5 100644 --- a/.strict-typing +++ b/.strict-typing @@ -197,7 +197,7 @@ homeassistant.components.recollect_waste.* homeassistant.components.recorder.* homeassistant.components.remote.* homeassistant.components.renault.* -homeassistant.components.resolution_center.* +homeassistant.components.repairs.* homeassistant.components.rhasspy.* homeassistant.components.ridwell.* homeassistant.components.rituals_perfume_genie.* diff --git a/CODEOWNERS b/CODEOWNERS index fd271e6adc6..dbbb21f0d6c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -859,9 +859,9 @@ build.json @home-assistant/supervisor /tests/components/remote/ @home-assistant/core /homeassistant/components/renault/ @epenet /tests/components/renault/ @epenet +/homeassistant/components/repairs/ @home-assistant/core +/tests/components/repairs/ @home-assistant/core /homeassistant/components/repetier/ @MTrab @ShadowBr0ther -/homeassistant/components/resolution_center/ @home-assistant/core -/tests/components/resolution_center/ @home-assistant/core /homeassistant/components/rflink/ @javicalle /tests/components/rflink/ @javicalle /homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221 diff --git a/homeassistant/components/resolution_center/__init__.py b/homeassistant/components/repairs/__init__.py similarity index 66% rename from homeassistant/components/resolution_center/__init__.py rename to homeassistant/components/repairs/__init__.py index 7d0cd8416c3..5a85d1999d0 100644 --- a/homeassistant/components/resolution_center/__init__.py +++ b/homeassistant/components/repairs/__init__.py @@ -1,4 +1,4 @@ -"""The resolution center integration.""" +"""The repairs integration.""" from __future__ import annotations from homeassistant.core import HomeAssistant @@ -6,14 +6,14 @@ from homeassistant.helpers.typing import ConfigType from . import issue_handler, websocket_api from .const import DOMAIN -from .issue_handler import ResolutionCenterFlow, async_create_issue, async_delete_issue +from .issue_handler import RepairsFlow, async_create_issue, async_delete_issue from .issue_registry import async_load as async_load_issue_registry -__all__ = ["DOMAIN", "ResolutionCenterFlow", "async_create_issue", "async_delete_issue"] +__all__ = ["DOMAIN", "RepairsFlow", "async_create_issue", "async_delete_issue"] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up Resolution Center.""" + """Set up Repairs.""" hass.data[DOMAIN] = {} issue_handler.async_setup(hass) diff --git a/homeassistant/components/repairs/const.py b/homeassistant/components/repairs/const.py new file mode 100644 index 00000000000..cddc5edcffd --- /dev/null +++ b/homeassistant/components/repairs/const.py @@ -0,0 +1,3 @@ +"""Constants for the Repairs integration.""" + +DOMAIN = "repairs" diff --git a/homeassistant/components/resolution_center/issue_handler.py b/homeassistant/components/repairs/issue_handler.py similarity index 75% rename from homeassistant/components/resolution_center/issue_handler.py rename to homeassistant/components/repairs/issue_handler.py index 78085e2cec6..ff23c1c70a4 100644 --- a/homeassistant/components/resolution_center/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -1,4 +1,4 @@ -"""The resolution center integration.""" +"""The repairs integration.""" from __future__ import annotations from typing import Any @@ -14,11 +14,11 @@ from homeassistant.helpers.integration_platform import ( from .const import DOMAIN from .issue_registry import async_get as async_get_issue_registry -from .models import IssueSeverity, ResolutionCenterFlow, ResolutionCenterProtocol +from .models import IssueSeverity, RepairsFlow, RepairsProtocol -class ResolutionCenterFlowManager(data_entry_flow.FlowManager): - """Manage resolution center flows.""" +class RepairsFlowManager(data_entry_flow.FlowManager): + """Manage repairs flows.""" async def async_create_flow( self, @@ -26,14 +26,12 @@ class ResolutionCenterFlowManager(data_entry_flow.FlowManager): *, context: dict[str, Any] | None = None, data: dict[str, Any] | None = None, - ) -> ResolutionCenterFlow: - """Create a flow. platform is a resolution center module.""" + ) -> RepairsFlow: + """Create a flow. platform is a repairs module.""" if "platforms" not in self.hass.data[DOMAIN]: - await async_process_resolution_center_platforms(self.hass) + await async_process_repairs_platforms(self.hass) - platforms: dict[str, ResolutionCenterProtocol] = self.hass.data[DOMAIN][ - "platforms" - ] + platforms: dict[str, RepairsProtocol] = self.hass.data[DOMAIN]["platforms"] if handler_key not in platforms: raise data_entry_flow.UnknownHandler platform = platforms[handler_key] @@ -60,25 +58,23 @@ class ResolutionCenterFlowManager(data_entry_flow.FlowManager): @callback def async_setup(hass: HomeAssistant) -> None: - """Initialize resolution center.""" - hass.data[DOMAIN]["flow_manager"] = ResolutionCenterFlowManager(hass) + """Initialize repairs.""" + hass.data[DOMAIN]["flow_manager"] = RepairsFlowManager(hass) -async def async_process_resolution_center_platforms(hass: HomeAssistant) -> None: - """Start processing resolution center platforms.""" +async def async_process_repairs_platforms(hass: HomeAssistant) -> None: + """Start processing repairs platforms.""" hass.data[DOMAIN]["platforms"] = {} - await async_process_integration_platforms( - hass, DOMAIN, _register_resolution_center_platform - ) + await async_process_integration_platforms(hass, DOMAIN, _register_repairs_platform) -async def _register_resolution_center_platform( - hass: HomeAssistant, integration_domain: str, platform: ResolutionCenterProtocol +async def _register_repairs_platform( + hass: HomeAssistant, integration_domain: str, platform: RepairsProtocol ) -> None: - """Register a resolution center platform.""" + """Register a repairs platform.""" if not hasattr(platform, "async_create_fix_flow"): - raise HomeAssistantError(f"Invalid resolution center platform {platform}") + raise HomeAssistantError(f"Invalid repairs platform {platform}") hass.data[DOMAIN]["platforms"][integration_domain] = platform diff --git a/homeassistant/components/resolution_center/issue_registry.py b/homeassistant/components/repairs/issue_registry.py similarity index 99% rename from homeassistant/components/resolution_center/issue_registry.py rename to homeassistant/components/repairs/issue_registry.py index e398d58b3e0..c1eda0d53be 100644 --- a/homeassistant/components/resolution_center/issue_registry.py +++ b/homeassistant/components/repairs/issue_registry.py @@ -13,7 +13,7 @@ import homeassistant.util.dt as dt_util from .models import IssueSeverity DATA_REGISTRY = "issue_registry" -STORAGE_KEY = "resolution_center.issue_registry" +STORAGE_KEY = "repairs.issue_registry" STORAGE_VERSION = 1 SAVE_DELAY = 10 diff --git a/homeassistant/components/resolution_center/manifest.json b/homeassistant/components/repairs/manifest.json similarity index 65% rename from homeassistant/components/resolution_center/manifest.json rename to homeassistant/components/repairs/manifest.json index 4b8c1bb2506..f2013743a05 100644 --- a/homeassistant/components/resolution_center/manifest.json +++ b/homeassistant/components/repairs/manifest.json @@ -1,8 +1,8 @@ { - "domain": "resolution_center", - "name": "Resolution Center", + "domain": "repairs", + "name": "Repairs", "config_flow": false, - "documentation": "https://www.home-assistant.io/integrations/resolution_center", + "documentation": "https://www.home-assistant.io/integrations/repairs", "codeowners": ["@home-assistant/core"], "dependencies": ["http"] } diff --git a/homeassistant/components/resolution_center/models.py b/homeassistant/components/repairs/models.py similarity index 69% rename from homeassistant/components/resolution_center/models.py rename to homeassistant/components/repairs/models.py index 12e2f4e73f3..2a6eeb15269 100644 --- a/homeassistant/components/resolution_center/models.py +++ b/homeassistant/components/repairs/models.py @@ -1,4 +1,4 @@ -"""Models for Resolution Center.""" +"""Models for Repairs.""" from __future__ import annotations from typing import Protocol @@ -16,14 +16,14 @@ class IssueSeverity(StrEnum): WARNING = "warning" -class ResolutionCenterFlow(data_entry_flow.FlowHandler): +class RepairsFlow(data_entry_flow.FlowHandler): """Handle a flow for fixing an issue.""" -class ResolutionCenterProtocol(Protocol): - """Define the format of resolution center platforms.""" +class RepairsProtocol(Protocol): + """Define the format of repairs platforms.""" async def async_create_fix_flow( self, hass: HomeAssistant, issue_id: str - ) -> ResolutionCenterFlow: + ) -> RepairsFlow: """Create a flow to fix a fixable issue.""" diff --git a/homeassistant/components/resolution_center/websocket_api.py b/homeassistant/components/repairs/websocket_api.py similarity index 83% rename from homeassistant/components/resolution_center/websocket_api.py rename to homeassistant/components/repairs/websocket_api.py index e111c2b3e50..2e4e166f3e9 100644 --- a/homeassistant/components/resolution_center/websocket_api.py +++ b/homeassistant/components/repairs/websocket_api.py @@ -1,4 +1,4 @@ -"""The resolution center websocket API.""" +"""The repairs websocket API.""" from __future__ import annotations import dataclasses @@ -26,22 +26,18 @@ from .issue_registry import async_get as async_get_issue_registry @callback def async_setup(hass: HomeAssistant) -> None: - """Set up the resolution center websocket API.""" + """Set up the repairs websocket API.""" websocket_api.async_register_command(hass, ws_ignore_issue) websocket_api.async_register_command(hass, ws_list_issues) - hass.http.register_view( - ResolutionCenterFlowIndexView(hass.data[DOMAIN]["flow_manager"]) - ) - hass.http.register_view( - ResolutionCenterFlowResourceView(hass.data[DOMAIN]["flow_manager"]) - ) + hass.http.register_view(RepairsFlowIndexView(hass.data[DOMAIN]["flow_manager"])) + hass.http.register_view(RepairsFlowResourceView(hass.data[DOMAIN]["flow_manager"])) @callback @websocket_api.websocket_command( { - vol.Required("type"): "resolution_center/ignore_issue", + vol.Required("type"): "repairs/ignore_issue", vol.Required("domain"): str, vol.Required("issue_id"): str, vol.Required("ignore"): bool, @@ -58,7 +54,7 @@ def ws_ignore_issue( @websocket_api.websocket_command( { - vol.Required("type"): "resolution_center/list_issues", + vol.Required("type"): "repairs/list_issues", } ) @callback @@ -82,11 +78,11 @@ def ws_list_issues( connection.send_result(msg["id"], {"issues": issues}) -class ResolutionCenterFlowIndexView(FlowManagerIndexView): +class RepairsFlowIndexView(FlowManagerIndexView): """View to create issue fix flows.""" - url = "/api/resolution_center/issues/fix" - name = "api:resolution_center:issues:fix" + url = "/api/repairs/issues/fix" + name = "api:repairs:issues:fix" @RequestDataValidator( vol.Schema( @@ -119,11 +115,11 @@ class ResolutionCenterFlowIndexView(FlowManagerIndexView): return self.json(result) # pylint: disable=arguments-differ -class ResolutionCenterFlowResourceView(FlowManagerResourceView): +class RepairsFlowResourceView(FlowManagerResourceView): """View to interact with the option flow manager.""" - url = "/api/resolution_center/issues/fix/{flow_id}" - name = "api:resolution_center:issues:fix:resource" + url = "/api/repairs/issues/fix/{flow_id}" + name = "api:repairs:issues:fix:resource" async def get(self, request: web.Request, flow_id: str) -> web.Response: """Get the current state of a data_entry_flow.""" diff --git a/homeassistant/components/resolution_center/const.py b/homeassistant/components/resolution_center/const.py deleted file mode 100644 index 46d020e5118..00000000000 --- a/homeassistant/components/resolution_center/const.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Constants for the Resolution Center integration.""" - -DOMAIN = "resolution_center" diff --git a/mypy.ini b/mypy.ini index 04b76dfea0f..84bc6ae00b0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1898,7 +1898,7 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true -[mypy-homeassistant.components.resolution_center.*] +[mypy-homeassistant.components.repairs.*] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 9c1bd0a63f3..129899cff11 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -80,7 +80,7 @@ NO_IOT_CLASS = [ "proxy", "python_script", "raspberry_pi", - "resolution_center", + "repairs", "safe_mode", "script", "search", diff --git a/tests/components/repairs/__init__.py b/tests/components/repairs/__init__.py new file mode 100644 index 00000000000..b0d26f49ee4 --- /dev/null +++ b/tests/components/repairs/__init__.py @@ -0,0 +1 @@ +"""Tests for the repairs integration.""" diff --git a/tests/components/resolution_center/test_init.py b/tests/components/repairs/test_init.py similarity index 87% rename from tests/components/resolution_center/test_init.py rename to tests/components/repairs/test_init.py index 478c2d51f7f..fbf240e740d 100644 --- a/tests/components/resolution_center/test_init.py +++ b/tests/components/repairs/test_init.py @@ -1,17 +1,14 @@ -"""Test the resolution center websocket API.""" +"""Test the repairs websocket API.""" from unittest.mock import AsyncMock, Mock from freezegun import freeze_time import pytest -from homeassistant.components.resolution_center import ( - async_create_issue, - async_delete_issue, -) -from homeassistant.components.resolution_center.const import DOMAIN -from homeassistant.components.resolution_center.issue_handler import ( +from homeassistant.components.repairs import async_create_issue, async_delete_issue +from homeassistant.components.repairs.const import DOMAIN +from homeassistant.components.repairs.issue_handler import ( async_ignore_issue, - async_process_resolution_center_platforms, + async_process_repairs_platforms, ) from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant @@ -27,7 +24,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: client = await hass_ws_client(hass) - await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 1, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -69,7 +66,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: translation_placeholders=issue["translation_placeholders"], ) - await client.send_json({"id": 2, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 2, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -98,7 +95,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: translation_placeholders=issues[0]["translation_placeholders"], ) - await client.send_json({"id": 3, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 3, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -144,7 +141,7 @@ async def test_create_issue_invalid_version( translation_placeholders=issue["translation_placeholders"], ) - await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 1, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -158,7 +155,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: client = await hass_ws_client(hass) - await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 1, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -190,7 +187,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: translation_placeholders=issue["translation_placeholders"], ) - await client.send_json({"id": 2, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 2, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -210,7 +207,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: with pytest.raises(KeyError): async_ignore_issue(hass, issues[0]["domain"], "no_such_issue", True) - await client.send_json({"id": 3, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 3, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -229,7 +226,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: # Ignore an existing issue async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], True) - await client.send_json({"id": 4, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 4, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -248,7 +245,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: # Ignore the same issue again async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], True) - await client.send_json({"id": 5, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 5, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -277,7 +274,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: translation_placeholders=issues[0]["translation_placeholders"], ) - await client.send_json({"id": 6, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 6, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -292,7 +289,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: # Unignore the same issue async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], False) - await client.send_json({"id": 7, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 7, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -343,7 +340,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non translation_placeholders=issue["translation_placeholders"], ) - await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 1, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -362,7 +359,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non # Delete a non-existing issue async_delete_issue(hass, issues[0]["domain"], "no_such_issue") - await client.send_json({"id": 2, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 2, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -381,7 +378,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non # Delete an existing issue async_delete_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) - await client.send_json({"id": 3, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 3, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -390,7 +387,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non # Delete the same issue again async_delete_issue(hass, issues[0]["domain"], issues[0]["issue_id"]) - await client.send_json({"id": 4, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 4, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -412,7 +409,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non translation_placeholders=issue["translation_placeholders"], ) - await client.send_json({"id": 5, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 5, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -436,16 +433,16 @@ async def test_non_compliant_platform(hass: HomeAssistant, hass_ws_client) -> No hass.config.components.add("integration_without_diagnostics") mock_platform( hass, - "fake_integration.resolution_center", + "fake_integration.repairs", Mock(async_create_fix_flow=AsyncMock(return_value=True)), ) mock_platform( hass, - "integration_without_diagnostics.resolution_center", + "integration_without_diagnostics.repairs", Mock(spec=[]), ) assert await async_setup_component(hass, DOMAIN, {}) - await async_process_resolution_center_platforms(hass) + await async_process_repairs_platforms(hass) assert list(hass.data[DOMAIN]["platforms"].keys()) == ["fake_integration"] diff --git a/tests/components/resolution_center/test_issue_registry.py b/tests/components/repairs/test_issue_registry.py similarity index 91% rename from tests/components/resolution_center/test_issue_registry.py rename to tests/components/repairs/test_issue_registry.py index 854a14fc84e..1af67601581 100644 --- a/tests/components/resolution_center/test_issue_registry.py +++ b/tests/components/repairs/test_issue_registry.py @@ -1,10 +1,7 @@ -"""Test the resolution center websocket API.""" -from homeassistant.components.resolution_center import ( - async_create_issue, - issue_registry, -) -from homeassistant.components.resolution_center.const import DOMAIN -from homeassistant.components.resolution_center.issue_handler import async_ignore_issue +"""Test the repairs websocket API.""" +from homeassistant.components.repairs import async_create_issue, issue_registry +from homeassistant.components.repairs.const import DOMAIN +from homeassistant.components.repairs.issue_handler import async_ignore_issue from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component diff --git a/tests/components/resolution_center/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py similarity index 86% rename from tests/components/resolution_center/test_websocket_api.py rename to tests/components/repairs/test_websocket_api.py index 044b0e9832b..388912e0adc 100644 --- a/tests/components/resolution_center/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -1,4 +1,4 @@ -"""Test the resolution center websocket API.""" +"""Test the repairs websocket API.""" from __future__ import annotations from http import HTTPStatus @@ -9,11 +9,8 @@ import pytest import voluptuous as vol from homeassistant import data_entry_flow -from homeassistant.components.resolution_center import ( - ResolutionCenterFlow, - async_create_issue, -) -from homeassistant.components.resolution_center.const import DOMAIN +from homeassistant.components.repairs import RepairsFlow, async_create_issue +from homeassistant.components.repairs.const import DOMAIN from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -49,7 +46,7 @@ async def create_issues(hass, ws_client): translation_placeholders=issue["translation_placeholders"], ) - await ws_client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) msg = await ws_client.receive_json() assert msg["success"] @@ -68,7 +65,7 @@ async def create_issues(hass, ws_client): return issues -class MockFixFlow(ResolutionCenterFlow): +class MockFixFlow(RepairsFlow): """Handler for an issue fixing flow.""" async def async_step_init( @@ -89,8 +86,8 @@ class MockFixFlow(ResolutionCenterFlow): @pytest.fixture(autouse=True) -async def mock_resolution_center_integration(hass): - """Mock a resolution_center integration.""" +async def mock_repairs_integration(hass): + """Mock a repairs integration.""" hass.config.components.add("fake_integration") hass.config.components.add("integration_without_diagnostics") @@ -99,12 +96,12 @@ async def mock_resolution_center_integration(hass): mock_platform( hass, - "fake_integration.resolution_center", + "fake_integration.repairs", Mock(async_create_fix_flow=AsyncMock(wraps=async_create_fix_flow)), ) mock_platform( hass, - "integration_without_diagnostics.resolution_center", + "integration_without_diagnostics.repairs", Mock(spec=[]), ) @@ -120,7 +117,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: await client.send_json( { "id": 2, - "type": "resolution_center/ignore_issue", + "type": "repairs/ignore_issue", "domain": "fake_integration", "issue_id": "no_such_issue", "ignore": True, @@ -132,7 +129,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: await client.send_json( { "id": 3, - "type": "resolution_center/ignore_issue", + "type": "repairs/ignore_issue", "domain": "fake_integration", "issue_id": "issue_1", "ignore": True, @@ -142,7 +139,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: assert msg["success"] assert msg["result"] is None - await client.send_json({"id": 4, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 4, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -161,7 +158,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: await client.send_json( { "id": 5, - "type": "resolution_center/ignore_issue", + "type": "repairs/ignore_issue", "domain": "fake_integration", "issue_id": "issue_1", "ignore": False, @@ -171,7 +168,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: assert msg["success"] assert msg["result"] is None - await client.send_json({"id": 6, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 6, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -200,21 +197,21 @@ async def test_fix_non_existing_issue( issues = await create_issues(hass, ws_client) - url = "/api/resolution_center/issues/fix" + url = "/api/repairs/issues/fix" resp = await client.post( url, json={"handler": "no_such_integration", "issue_id": "no_such_issue"} ) assert resp.status != HTTPStatus.OK - url = "/api/resolution_center/issues/fix" + url = "/api/repairs/issues/fix" resp = await client.post( url, json={"handler": "fake_integration", "issue_id": "no_such_issue"} ) assert resp.status != HTTPStatus.OK - await ws_client.send_json({"id": 3, "type": "resolution_center/list_issues"}) + await ws_client.send_json({"id": 3, "type": "repairs/list_issues"}) msg = await ws_client.receive_json() assert msg["success"] @@ -241,7 +238,7 @@ async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> No await create_issues(hass, ws_client) - url = "/api/resolution_center/issues/fix" + url = "/api/repairs/issues/fix" resp = await client.post( url, json={"handler": "fake_integration", "issue_id": "issue_1"} ) @@ -261,7 +258,7 @@ async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> No "type": "form", } - url = f"/api/resolution_center/issues/fix/{flow_id}" + url = f"/api/repairs/issues/fix/{flow_id}" # Test we can get the status of the flow resp2 = await client.get(url) @@ -286,7 +283,7 @@ async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> No "version": 1, } - await ws_client.send_json({"id": 4, "type": "resolution_center/list_issues"}) + await ws_client.send_json({"id": 4, "type": "repairs/list_issues"}) msg = await ws_client.receive_json() assert msg["success"] @@ -304,7 +301,7 @@ async def test_fix_issue_unauth( client = await hass_client() - url = "/api/resolution_center/issues/fix" + url = "/api/repairs/issues/fix" resp = await client.post( url, json={"handler": "fake_integration", "issue_id": "issue_1"} ) @@ -324,7 +321,7 @@ async def test_get_progress_unauth( await create_issues(hass, ws_client) - url = "/api/resolution_center/issues/fix" + url = "/api/repairs/issues/fix" resp = await client.post( url, json={"handler": "fake_integration", "issue_id": "issue_1"} ) @@ -334,7 +331,7 @@ async def test_get_progress_unauth( hass_admin_user.groups = [] - url = f"/api/resolution_center/issues/fix/{flow_id}" + url = f"/api/repairs/issues/fix/{flow_id}" # Test we can't get the status of the flow resp = await client.get(url) assert resp.status == HTTPStatus.UNAUTHORIZED @@ -352,7 +349,7 @@ async def test_step_unauth( await create_issues(hass, ws_client) - url = "/api/resolution_center/issues/fix" + url = "/api/repairs/issues/fix" resp = await client.post( url, json={"handler": "fake_integration", "issue_id": "issue_1"} ) @@ -362,7 +359,7 @@ async def test_step_unauth( hass_admin_user.groups = [] - url = f"/api/resolution_center/issues/fix/{flow_id}" + url = f"/api/repairs/issues/fix/{flow_id}" # Test we can't get the status of the flow resp = await client.post(url) assert resp.status == HTTPStatus.UNAUTHORIZED @@ -375,7 +372,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: client = await hass_ws_client(hass) - await client.send_json({"id": 1, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 1, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] @@ -417,7 +414,7 @@ async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: translation_placeholders=issue["translation_placeholders"], ) - await client.send_json({"id": 2, "type": "resolution_center/list_issues"}) + await client.send_json({"id": 2, "type": "repairs/list_issues"}) msg = await client.receive_json() assert msg["success"] diff --git a/tests/components/resolution_center/__init__.py b/tests/components/resolution_center/__init__.py deleted file mode 100644 index a6a86bf99bc..00000000000 --- a/tests/components/resolution_center/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the resolution center integration.""" From 712492b0665240d933206d402d5583fad2bd60ab Mon Sep 17 00:00:00 2001 From: Zach Berger Date: Wed, 20 Jul 2022 03:08:34 -0700 Subject: [PATCH 2716/3516] Update awair SensorDeviceClass to specify icon (#75385) --- homeassistant/components/awair/const.py | 7 +++---- tests/components/awair/test_sensor.py | 23 ++++++++--------------- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py index 5c2a3da89ce..6fcf63abb4d 100644 --- a/homeassistant/components/awair/const.py +++ b/homeassistant/components/awair/const.py @@ -86,7 +86,7 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = ( ), AwairSensorEntityDescription( key=API_VOC, - icon="mdi:cloud", + icon="mdi:molecule", native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, name="Volatile organic compounds", unique_id_tag="VOC", # matches legacy format @@ -101,7 +101,6 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = ( AwairSensorEntityDescription( key=API_CO2, device_class=SensorDeviceClass.CO2, - icon="mdi:cloud", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, name="Carbon dioxide", unique_id_tag="CO2", # matches legacy format @@ -111,14 +110,14 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = ( SENSOR_TYPES_DUST: tuple[AwairSensorEntityDescription, ...] = ( AwairSensorEntityDescription( key=API_PM25, - icon="mdi:blur", + device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, name="PM2.5", unique_id_tag="PM25", # matches legacy format ), AwairSensorEntityDescription( key=API_PM10, - icon="mdi:blur", + device_class=SensorDeviceClass.PM10, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, name="PM10", unique_id_tag="PM10", # matches legacy format diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index 5f30be81d6f..07b2f9ba00f 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -17,7 +17,6 @@ from homeassistant.components.awair.const import ( SENSOR_TYPES_DUST, ) from homeassistant.const import ( - ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, @@ -88,7 +87,7 @@ async def test_awair_gen1_sensors(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "88", - {ATTR_ICON: "mdi:blur"}, + {}, ) assert_expected_properties( @@ -116,7 +115,6 @@ async def test_awair_gen1_sensors(hass): f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_CO2].unique_id_tag}", "654.0", { - ATTR_ICON: "mdi:cloud", ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION, "awair_index": 0.0, }, @@ -129,7 +127,6 @@ async def test_awair_gen1_sensors(hass): f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_VOC].unique_id_tag}", "366", { - ATTR_ICON: "mdi:cloud", ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, "awair_index": 1.0, }, @@ -143,7 +140,6 @@ async def test_awair_gen1_sensors(hass): f"{AWAIR_UUID}_DUST", "14.3", { - ATTR_ICON: "mdi:blur", ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, "awair_index": 1.0, }, @@ -156,7 +152,6 @@ async def test_awair_gen1_sensors(hass): f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_PM10].unique_id_tag}", "14.3", { - ATTR_ICON: "mdi:blur", ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, "awair_index": 1.0, }, @@ -184,7 +179,7 @@ async def test_awair_gen2_sensors(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "97", - {ATTR_ICON: "mdi:blur"}, + {}, ) assert_expected_properties( @@ -194,7 +189,6 @@ async def test_awair_gen2_sensors(hass): f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_PM25].unique_id_tag}", "2.0", { - ATTR_ICON: "mdi:blur", ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, "awair_index": 0.0, }, @@ -218,7 +212,7 @@ async def test_awair_mint_sensors(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "98", - {ATTR_ICON: "mdi:blur"}, + {}, ) assert_expected_properties( @@ -228,7 +222,6 @@ async def test_awair_mint_sensors(hass): f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_PM25].unique_id_tag}", "1.0", { - ATTR_ICON: "mdi:blur", ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, "awair_index": 0.0, }, @@ -260,7 +253,7 @@ async def test_awair_glow_sensors(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "93", - {ATTR_ICON: "mdi:blur"}, + {}, ) # The glow does not have a particle sensor @@ -280,7 +273,7 @@ async def test_awair_omni_sensors(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "99", - {ATTR_ICON: "mdi:blur"}, + {}, ) assert_expected_properties( @@ -289,7 +282,7 @@ async def test_awair_omni_sensors(hass): "sensor.living_room_sound_level", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SPL_A].unique_id_tag}", "47.0", - {ATTR_ICON: "mdi:ear-hearing", ATTR_UNIT_OF_MEASUREMENT: "dBa"}, + {ATTR_UNIT_OF_MEASUREMENT: "dBa"}, ) assert_expected_properties( @@ -333,7 +326,7 @@ async def test_awair_unavailable(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", "88", - {ATTR_ICON: "mdi:blur"}, + {}, ) with patch("python_awair.AwairClient.query", side_effect=OFFLINE_FIXTURE): @@ -344,5 +337,5 @@ async def test_awair_unavailable(hass): "sensor.living_room_awair_score", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", STATE_UNAVAILABLE, - {ATTR_ICON: "mdi:blur"}, + {}, ) From 95e07508e54e31493911889c2adbba5997616a64 Mon Sep 17 00:00:00 2001 From: Pascal Winters Date: Wed, 20 Jul 2022 12:57:00 +0200 Subject: [PATCH 2717/3516] Bump pySwitchbot to 0.14.1 (#75487) --- 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 91ee8881a07..ccce534c6df 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.14.0"], + "requirements": ["PySwitchbot==0.14.1"], "config_flow": true, "codeowners": ["@danielhiversen", "@RenierM26"], "bluetooth": [{ "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" }], diff --git a/requirements_all.txt b/requirements_all.txt index cee83671b41..b3395085f2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.14.0 +PySwitchbot==0.14.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7bab6fbb9d6..f53ea0fd4cf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.14.0 +PySwitchbot==0.14.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From cd3e99564f3d60d1edef67a479c2c435126a784a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 13:02:13 +0200 Subject: [PATCH 2718/3516] Add repairs integration to core files (#75489) --- .core_files.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.core_files.yaml b/.core_files.yaml index 29390865611..df69df45cb6 100644 --- a/.core_files.yaml +++ b/.core_files.yaml @@ -88,6 +88,7 @@ components: &components - homeassistant/components/persistent_notification/** - homeassistant/components/person/** - homeassistant/components/recorder/** + - homeassistant/components/repairs/** - homeassistant/components/safe_mode/** - homeassistant/components/script/** - homeassistant/components/shopping_list/** From 3920844dca5c08a4e58b124709b9e6f10cda2f31 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 14:00:35 +0200 Subject: [PATCH 2719/3516] Adjust repairs re-exports (#75492) --- homeassistant/components/repairs/__init__.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/repairs/__init__.py b/homeassistant/components/repairs/__init__.py index 5a85d1999d0..f9a478514aa 100644 --- a/homeassistant/components/repairs/__init__.py +++ b/homeassistant/components/repairs/__init__.py @@ -6,10 +6,17 @@ from homeassistant.helpers.typing import ConfigType from . import issue_handler, websocket_api from .const import DOMAIN -from .issue_handler import RepairsFlow, async_create_issue, async_delete_issue +from .issue_handler import async_create_issue, async_delete_issue from .issue_registry import async_load as async_load_issue_registry +from .models import IssueSeverity, RepairsFlow -__all__ = ["DOMAIN", "RepairsFlow", "async_create_issue", "async_delete_issue"] +__all__ = [ + "async_create_issue", + "async_delete_issue", + "DOMAIN", + "IssueSeverity", + "RepairsFlow", +] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: From fb4aff25a25dcabe13b891093fe562e3488b92cf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 20 Jul 2022 14:46:06 +0200 Subject: [PATCH 2720/3516] Create issues in demo integration (#75081) * Create issues in demo integration * Add unfixable non-expiring issue * Update test * Adjust tests * update translations * add hassfest translation schema * Update homeassistant/components/demo/translations/en.json Co-authored-by: Zack Barett * Rename Resolution Center -> Repairs * Update homeassistant/components/demo/strings.json Co-authored-by: Zack Barett * Adjust hassfest to require description or fix_flow * Update homeassistant/components/demo/repairs.py Co-authored-by: Martin Hjelmare * Update tests/components/demo/test_init.py Co-authored-by: Martin Hjelmare * Add missing translation strings * black * Adjust repairs imports Co-authored-by: Bram Kragten Co-authored-by: Franck Nijhof Co-authored-by: Zack Barett Co-authored-by: Martin Hjelmare --- homeassistant/components/demo/__init__.py | 34 +++++ homeassistant/components/demo/manifest.json | 2 +- homeassistant/components/demo/repairs.py | 33 +++++ homeassistant/components/demo/strings.json | 21 +++ .../components/demo/translations/en.json | 24 ++++ script/hassfest/translations.py | 23 ++- tests/components/demo/test_init.py | 133 +++++++++++++++++- 7 files changed, 266 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/demo/repairs.py diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 2602d674005..3f0bb09cdbd 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -10,6 +10,7 @@ from homeassistant.components.recorder.statistics import ( async_add_external_statistics, get_last_statistics, ) +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, @@ -177,6 +178,39 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.bus.async_listen(EVENT_HOMEASSISTANT_START, demo_start_listener) + # Create issues + async_create_issue( + hass, + DOMAIN, + "transmogrifier_deprecated", + breaks_in_ha_version="2023.1.1", + is_fixable=False, + learn_more_url="https://en.wiktionary.org/wiki/transmogrifier", + severity=IssueSeverity.WARNING, + translation_key="transmogrifier_deprecated", + ) + + async_create_issue( + hass, + DOMAIN, + "out_of_blinker_fluid", + breaks_in_ha_version="2023.1.1", + is_fixable=True, + learn_more_url="https://www.youtube.com/watch?v=b9rntRxLlbU", + severity=IssueSeverity.CRITICAL, + translation_key="out_of_blinker_fluid", + ) + + async_create_issue( + hass, + DOMAIN, + "unfixable_problem", + is_fixable=False, + learn_more_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ", + severity=IssueSeverity.WARNING, + translation_key="unfixable_problem", + ) + return True diff --git a/homeassistant/components/demo/manifest.json b/homeassistant/components/demo/manifest.json index df6fa494079..2965a66e23e 100644 --- a/homeassistant/components/demo/manifest.json +++ b/homeassistant/components/demo/manifest.json @@ -3,7 +3,7 @@ "name": "Demo", "documentation": "https://www.home-assistant.io/integrations/demo", "after_dependencies": ["recorder"], - "dependencies": ["conversation", "group", "zone"], + "dependencies": ["conversation", "group", "repairs", "zone"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "calculated" diff --git a/homeassistant/components/demo/repairs.py b/homeassistant/components/demo/repairs.py new file mode 100644 index 00000000000..e5d31c18971 --- /dev/null +++ b/homeassistant/components/demo/repairs.py @@ -0,0 +1,33 @@ +"""Repairs platform for the demo integration.""" + +from __future__ import annotations + +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components.repairs import RepairsFlow + + +class DemoFixFlow(RepairsFlow): + """Handler for an issue fixing flow.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + + return await (self.async_step_confirm()) + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + return self.async_create_entry(title="Fixed issue", data={}) + + return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) + + +async def async_create_fix_flow(hass, issue_id): + """Create flow.""" + return DemoFixFlow() diff --git a/homeassistant/components/demo/strings.json b/homeassistant/components/demo/strings.json index c861ca9e5e9..a3a5b11f336 100644 --- a/homeassistant/components/demo/strings.json +++ b/homeassistant/components/demo/strings.json @@ -1,5 +1,26 @@ { "title": "Demo", + "issues": { + "out_of_blinker_fluid": { + "title": "The blinker fluid is empty and needs to be refilled", + "fix_flow": { + "step": { + "confirm": { + "title": "Blinker fluid needs to be refilled", + "description": "Press OK when blinker fluid has been refilled" + } + } + } + }, + "transmogrifier_deprecated": { + "title": "The transmogrifier component is deprecated", + "description": "The transmogrifier component is now deprecated due to the lack of local control available in the new API" + }, + "unfixable_problem": { + "title": "This is not a fixable problem", + "description": "This issue is never going to give up." + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/demo/translations/en.json b/homeassistant/components/demo/translations/en.json index 2e70c88962a..326e741f764 100644 --- a/homeassistant/components/demo/translations/en.json +++ b/homeassistant/components/demo/translations/en.json @@ -1,6 +1,30 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Press OK when blinker fluid has been refilled", + "title": "Blinker fluid needs to be refilled" + } + } + }, + "title": "The blinker fluid is empty and needs to be refilled" + }, + "transmogrifier_deprecated": { + "description": "The transmogrifier component is now deprecated due to the lack of local control available in the new API", + "title": "The transmogrifier component is deprecated" + }, + "unfixable_problem": { + "description": "This issue is never going to give up.", + "title": "This is not a fixable problem" + } + }, "options": { "step": { + "init": { + "data": {} + }, "options_1": { "data": { "bool": "Optional boolean", diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index a1f520808f6..9c4f75f1b2d 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -21,7 +21,7 @@ REMOVED = 2 RE_REFERENCE = r"\[\%key:(.+)\%\]" -# Only allow translatino of integration names if they contain non-brand names +# Only allow translation of integration names if they contain non-brand names ALLOW_NAME_TRANSLATION = { "cert_expiry", "cpuspeed", @@ -185,7 +185,7 @@ def gen_data_entry_schema( return vol.All(*validators) -def gen_strings_schema(config: Config, integration: Integration): +def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: """Generate a strings schema.""" return vol.Schema( { @@ -227,6 +227,25 @@ def gen_strings_schema(config: Config, integration: Integration): vol.Optional("application_credentials"): { vol.Optional("description"): cv.string_with_no_html, }, + vol.Optional("issues"): { + str: vol.All( + cv.has_at_least_one_key("description", "fix_flow"), + vol.Schema( + { + vol.Required("title"): cv.string_with_no_html, + vol.Exclusive( + "description", "fixable" + ): cv.string_with_no_html, + vol.Exclusive("fix_flow", "fixable"): gen_data_entry_schema( + config=config, + integration=integration, + flow_title=UNDEFINED, + require_step_title=False, + ), + }, + ), + ) + }, } ) diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index fa0aff8223b..85ff2a16405 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -1,6 +1,7 @@ """The tests for the Demo component.""" +from http import HTTPStatus import json -from unittest.mock import patch +from unittest.mock import ANY, patch import pytest @@ -69,3 +70,133 @@ async def test_demo_statistics(hass, recorder_mock): "statistic_id": "demo:energy_consumption", "unit_of_measurement": "kWh", } in statistic_ids + + +async def test_issues_created(hass, hass_client, hass_ws_client): + """Test issues are created and can be fixed.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + await hass.async_start() + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + { + "breaks_in_ha_version": "2023.1.1", + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "ignored": False, + "is_fixable": False, + "issue_id": "transmogrifier_deprecated", + "learn_more_url": "https://en.wiktionary.org/wiki/transmogrifier", + "severity": "warning", + "translation_key": "transmogrifier_deprecated", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": "2023.1.1", + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "ignored": False, + "is_fixable": True, + "issue_id": "out_of_blinker_fluid", + "learn_more_url": "https://www.youtube.com/watch?v=b9rntRxLlbU", + "severity": "critical", + "translation_key": "out_of_blinker_fluid", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "ignored": False, + "is_fixable": False, + "issue_id": "unfixable_problem", + "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "severity": "warning", + "translation_key": "unfixable_problem", + "translation_placeholders": None, + }, + ] + } + + url = "/api/repairs/issues/fix" + resp = await client.post( + url, json={"handler": "demo", "issue_id": "out_of_blinker_fluid"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "data_schema": [], + "description_placeholders": None, + "errors": None, + "flow_id": ANY, + "handler": "demo", + "last_step": None, + "step_id": "confirm", + "type": "form", + } + + url = f"/api/repairs/issues/fix/{flow_id}" + resp = await client.post(url) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "description": None, + "description_placeholders": None, + "flow_id": flow_id, + "handler": "demo", + "title": "Fixed issue", + "type": "create_entry", + "version": 1, + } + + await ws_client.send_json({"id": 4, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + { + "breaks_in_ha_version": "2023.1.1", + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "ignored": False, + "is_fixable": False, + "issue_id": "transmogrifier_deprecated", + "learn_more_url": "https://en.wiktionary.org/wiki/transmogrifier", + "severity": "warning", + "translation_key": "transmogrifier_deprecated", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "ignored": False, + "is_fixable": False, + "issue_id": "unfixable_problem", + "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "severity": "warning", + "translation_key": "unfixable_problem", + "translation_placeholders": None, + }, + ] + } From 8ad2bed36388434b413c1805b94815edc4987759 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 20 Jul 2022 15:51:54 +0200 Subject: [PATCH 2721/3516] Fix Netgear update entity (#75496) --- homeassistant/components/netgear/update.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netgear/update.py b/homeassistant/components/netgear/update.py index 8d4a9b4912a..e913d488c8e 100644 --- a/homeassistant/components/netgear/update.py +++ b/homeassistant/components/netgear/update.py @@ -59,7 +59,9 @@ class NetgearUpdateEntity(NetgearRouterEntity, UpdateEntity): """Latest version available for install.""" if self.coordinator.data is not None: new_version = self.coordinator.data.get("NewVersion") - if new_version is not None: + if new_version is not None and not new_version.startswith( + self.installed_version + ): return new_version return self.installed_version From 877a4030aa1ce72b59d355a33c59f98a8506e911 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 16:29:07 +0200 Subject: [PATCH 2722/3516] Add repairs as frontend dependency (#75501) --- homeassistant/components/frontend/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 288b4796e0e..f19ef00e2b7 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -12,6 +12,7 @@ "http", "lovelace", "onboarding", + "repairs", "search", "system_log", "websocket_api" From 87cfe215674a9eb18910f24bab3cd757d119825f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 18:55:33 +0200 Subject: [PATCH 2723/3516] Remove XBee integration (#75502) --- .coveragerc | 1 - homeassistant/components/xbee/__init__.py | 441 ------------------ .../components/xbee/binary_sensor.py | 29 -- homeassistant/components/xbee/const.py | 5 - homeassistant/components/xbee/light.py | 34 -- homeassistant/components/xbee/manifest.json | 10 - homeassistant/components/xbee/sensor.py | 97 ---- homeassistant/components/xbee/switch.py | 29 -- 8 files changed, 646 deletions(-) delete mode 100644 homeassistant/components/xbee/__init__.py delete mode 100644 homeassistant/components/xbee/binary_sensor.py delete mode 100644 homeassistant/components/xbee/const.py delete mode 100644 homeassistant/components/xbee/light.py delete mode 100644 homeassistant/components/xbee/manifest.json delete mode 100644 homeassistant/components/xbee/sensor.py delete mode 100644 homeassistant/components/xbee/switch.py diff --git a/.coveragerc b/.coveragerc index 3645286980b..0dd9f18af34 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1523,7 +1523,6 @@ omit = homeassistant/components/zha/light.py homeassistant/components/zha/sensor.py homeassistant/components/zhong_hong/climate.py - homeassistant/components/xbee/* homeassistant/components/ziggo_mediabox_xl/media_player.py homeassistant/components/zoneminder/* homeassistant/components/supla/* diff --git a/homeassistant/components/xbee/__init__.py b/homeassistant/components/xbee/__init__.py deleted file mode 100644 index 6a7aba16b95..00000000000 --- a/homeassistant/components/xbee/__init__.py +++ /dev/null @@ -1,441 +0,0 @@ -"""Support for XBee Zigbee devices.""" -# pylint: disable=import-error -from binascii import hexlify, unhexlify -import logging - -from serial import Serial, SerialException -import voluptuous as vol -from xbee_helper import ZigBee -import xbee_helper.const as xb_const -from xbee_helper.device import convert_adc -from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure - -from homeassistant.components.sensor import SensorEntity -from homeassistant.const import ( - CONF_ADDRESS, - CONF_DEVICE, - CONF_NAME, - CONF_PIN, - EVENT_HOMEASSISTANT_STOP, - PERCENTAGE, -) -from homeassistant.core import HomeAssistant -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.typing import ConfigType - -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) - -SIGNAL_XBEE_FRAME_RECEIVED = "xbee_frame_received" - -CONF_BAUD = "baud" - -DEFAULT_DEVICE = "/dev/ttyUSB0" -DEFAULT_BAUD = 9600 -DEFAULT_ADC_MAX_VOLTS = 1.2 - -ATTR_FRAME = "frame" - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_BAUD, default=DEFAULT_BAUD): cv.string, - vol.Optional(CONF_DEVICE, default=DEFAULT_DEVICE): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - -PLATFORM_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_PIN): cv.positive_int, - vol.Optional(CONF_ADDRESS): cv.string, - }, - extra=vol.ALLOW_EXTRA, -) - - -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the connection to the XBee Zigbee device.""" - usb_device = config[DOMAIN].get(CONF_DEVICE, DEFAULT_DEVICE) - baud = int(config[DOMAIN].get(CONF_BAUD, DEFAULT_BAUD)) - try: - ser = Serial(usb_device, baud) - except SerialException as exc: - _LOGGER.exception("Unable to open serial port for XBee: %s", exc) - return False - zigbee_device = ZigBee(ser) - - def close_serial_port(*args): - """Close the serial port we're using to communicate with the XBee.""" - zigbee_device.zb.serial.close() - - def _frame_received(frame): - """Run when a XBee Zigbee frame is received. - - Pickles the frame, then encodes it into base64 since it contains - non JSON serializable binary. - """ - dispatcher_send(hass, SIGNAL_XBEE_FRAME_RECEIVED, frame) - - hass.data[DOMAIN] = zigbee_device - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_serial_port) - zigbee_device.add_frame_rx_handler(_frame_received) - - return True - - -def frame_is_relevant(entity, frame): - """Test whether the frame is relevant to the entity.""" - if frame.get("source_addr_long") != entity.config.address: - return False - return "samples" in frame - - -class XBeeConfig: - """Handle the fetching of configuration from the config file.""" - - def __init__(self, config): - """Initialize the configuration.""" - self._config = config - self._should_poll = config.get("poll", True) - - @property - def name(self): - """Return the name given to the entity.""" - return self._config["name"] - - @property - def address(self): - """Return the address of the device. - - If an address has been provided, unhexlify it, otherwise return None - as we're talking to our local XBee device. - """ - if (address := self._config.get("address")) is not None: - return unhexlify(address) - return address - - @property - def should_poll(self): - """Return the polling state.""" - return self._should_poll - - -class XBeePinConfig(XBeeConfig): - """Handle the fetching of configuration from the configuration file.""" - - @property - def pin(self): - """Return the GPIO pin number.""" - return self._config["pin"] - - -class XBeeDigitalInConfig(XBeePinConfig): - """A subclass of XBeePinConfig.""" - - def __init__(self, config): - """Initialise the XBee Zigbee Digital input config.""" - super().__init__(config) - self._bool2state, self._state2bool = self.boolean_maps - - @property - def boolean_maps(self): - """Create mapping dictionaries for potential inversion of booleans. - - Create dicts to map the pin state (true/false) to potentially inverted - values depending on the on_state config value which should be set to - "low" or "high". - """ - if self._config.get("on_state", "").lower() == "low": - bool2state = {True: False, False: True} - else: - bool2state = {True: True, False: False} - state2bool = {v: k for k, v in bool2state.items()} - return bool2state, state2bool - - @property - def bool2state(self): - """Return a dictionary mapping the internal value to the Zigbee value. - - For the translation of on/off as being pin high or low. - """ - return self._bool2state - - @property - def state2bool(self): - """Return a dictionary mapping the Zigbee value to the internal value. - - For the translation of pin high/low as being on or off. - """ - return self._state2bool - - -class XBeeDigitalOutConfig(XBeePinConfig): - """A subclass of XBeePinConfig. - - Set _should_poll to default as False instead of True. The value will - still be overridden by the presence of a 'poll' config entry. - """ - - def __init__(self, config): - """Initialize the XBee Zigbee Digital out.""" - super().__init__(config) - self._bool2state, self._state2bool = self.boolean_maps - self._should_poll = config.get("poll", False) - - @property - def boolean_maps(self): - """Create dicts to map booleans to pin high/low and vice versa. - - Depends on the config item "on_state" which should be set to "low" - or "high". - """ - if self._config.get("on_state", "").lower() == "low": - bool2state = { - True: xb_const.GPIO_DIGITAL_OUTPUT_LOW, - False: xb_const.GPIO_DIGITAL_OUTPUT_HIGH, - } - else: - bool2state = { - True: xb_const.GPIO_DIGITAL_OUTPUT_HIGH, - False: xb_const.GPIO_DIGITAL_OUTPUT_LOW, - } - state2bool = {v: k for k, v in bool2state.items()} - return bool2state, state2bool - - @property - def bool2state(self): - """Return a dictionary mapping booleans to GPIOSetting objects. - - For the translation of on/off as being pin high or low. - """ - return self._bool2state - - @property - def state2bool(self): - """Return a dictionary mapping GPIOSetting objects to booleans. - - For the translation of pin high/low as being on or off. - """ - return self._state2bool - - -class XBeeAnalogInConfig(XBeePinConfig): - """Representation of a XBee Zigbee GPIO pin set to analog in.""" - - @property - def max_voltage(self): - """Return the voltage for ADC to report its highest value.""" - return float(self._config.get("max_volts", DEFAULT_ADC_MAX_VOLTS)) - - -class XBeeDigitalIn(Entity): - """Representation of a GPIO pin configured as a digital input.""" - - def __init__(self, config, device): - """Initialize the device.""" - self._config = config - self._device = device - self._state = False - - async def async_added_to_hass(self): - """Register callbacks.""" - - def handle_frame(frame): - """Handle an incoming frame. - - Handle an incoming frame and update our status if it contains - information relating to this device. - """ - if not frame_is_relevant(self, frame): - return - sample = next(iter(frame["samples"])) - pin_name = xb_const.DIGITAL_PINS[self._config.pin] - if pin_name not in sample: - # Doesn't contain information about our pin - return - # Set state to the value of sample, respecting any inversion - # logic from the on_state config variable. - self._state = self._config.state2bool[ - self._config.bool2state[sample[pin_name]] - ] - self.schedule_update_ha_state() - - async_dispatcher_connect(self.hass, SIGNAL_XBEE_FRAME_RECEIVED, handle_frame) - - @property - def name(self): - """Return the name of the input.""" - return self._config.name - - @property - def config(self): - """Return the entity's configuration.""" - return self._config - - @property - def should_poll(self): - """Return the state of the polling, if needed.""" - return self._config.should_poll - - @property - def is_on(self): - """Return True if the Entity is on, else False.""" - return self._state - - def update(self): - """Ask the Zigbee device what state its input pin is in.""" - try: - sample = self._device.get_sample(self._config.address) - except ZigBeeTxFailure: - _LOGGER.warning( - "Transmission failure when attempting to get sample from " - "Zigbee device at address: %s", - hexlify(self._config.address), - ) - return - except ZigBeeException as exc: - _LOGGER.exception("Unable to get sample from Zigbee device: %s", exc) - return - pin_name = xb_const.DIGITAL_PINS[self._config.pin] - if pin_name not in sample: - _LOGGER.warning( - "Pin %s (%s) was not in the sample provided by Zigbee device %s", - self._config.pin, - pin_name, - hexlify(self._config.address), - ) - return - self._state = self._config.state2bool[sample[pin_name]] - - -class XBeeDigitalOut(XBeeDigitalIn): - """Representation of a GPIO pin configured as a digital input.""" - - def _set_state(self, state): - """Initialize the XBee Zigbee digital out device.""" - try: - self._device.set_gpio_pin( - self._config.pin, self._config.bool2state[state], self._config.address - ) - except ZigBeeTxFailure: - _LOGGER.warning( - "Transmission failure when attempting to set output pin on " - "Zigbee device at address: %s", - hexlify(self._config.address), - ) - return - except ZigBeeException as exc: - _LOGGER.exception("Unable to set digital pin on XBee device: %s", exc) - return - self._state = state - if not self.should_poll: - self.schedule_update_ha_state() - - def turn_on(self, **kwargs): - """Set the digital output to its 'on' state.""" - self._set_state(True) - - def turn_off(self, **kwargs): - """Set the digital output to its 'off' state.""" - self._set_state(False) - - def update(self): - """Ask the XBee device what its output is set to.""" - try: - pin_state = self._device.get_gpio_pin( - self._config.pin, self._config.address - ) - except ZigBeeTxFailure: - _LOGGER.warning( - "Transmission failure when attempting to get output pin status" - " from Zigbee device at address: %s", - hexlify(self._config.address), - ) - return - except ZigBeeException as exc: - _LOGGER.exception( - "Unable to get output pin status from XBee device: %s", exc - ) - return - self._state = self._config.state2bool[pin_state] - - -class XBeeAnalogIn(SensorEntity): - """Representation of a GPIO pin configured as an analog input.""" - - _attr_native_unit_of_measurement = PERCENTAGE - - def __init__(self, config, device): - """Initialize the XBee analog in device.""" - self._config = config - self._device = device - self._value = None - - async def async_added_to_hass(self): - """Register callbacks.""" - - def handle_frame(frame): - """Handle an incoming frame. - - Handle an incoming frame and update our status if it contains - information relating to this device. - """ - if not frame_is_relevant(self, frame): - return - sample = frame["samples"].pop() - pin_name = xb_const.ANALOG_PINS[self._config.pin] - if pin_name not in sample: - # Doesn't contain information about our pin - return - self._value = convert_adc( - sample[pin_name], xb_const.ADC_PERCENTAGE, self._config.max_voltage - ) - self.schedule_update_ha_state() - - async_dispatcher_connect(self.hass, SIGNAL_XBEE_FRAME_RECEIVED, handle_frame) - - @property - def name(self): - """Return the name of the input.""" - return self._config.name - - @property - def config(self): - """Return the entity's configuration.""" - return self._config - - @property - def should_poll(self): - """Return the polling state, if needed.""" - return self._config.should_poll - - @property - def sensor_state(self): - """Return the state of the entity.""" - return self._value - - def update(self): - """Get the latest reading from the ADC.""" - try: - self._value = self._device.read_analog_pin( - self._config.pin, - self._config.max_voltage, - self._config.address, - xb_const.ADC_PERCENTAGE, - ) - except ZigBeeTxFailure: - _LOGGER.warning( - "Transmission failure when attempting to get sample from " - "Zigbee device at address: %s", - hexlify(self._config.address), - ) - except ZigBeeException as exc: - _LOGGER.exception("Unable to get sample from Zigbee device: %s", exc) diff --git a/homeassistant/components/xbee/binary_sensor.py b/homeassistant/components/xbee/binary_sensor.py deleted file mode 100644 index b1639085993..00000000000 --- a/homeassistant/components/xbee/binary_sensor.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Support for Zigbee binary sensors.""" -from __future__ import annotations - -import voluptuous as vol - -from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import PLATFORM_SCHEMA, XBeeDigitalIn, XBeeDigitalInConfig -from .const import CONF_ON_STATE, DOMAIN, STATES - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_ON_STATE): vol.In(STATES)}) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the XBee Zigbee binary sensor platform.""" - zigbee_device = hass.data[DOMAIN] - add_entities([XBeeBinarySensor(XBeeDigitalInConfig(config), zigbee_device)], True) - - -class XBeeBinarySensor(XBeeDigitalIn, BinarySensorEntity): - """Use XBeeDigitalIn as binary sensor.""" diff --git a/homeassistant/components/xbee/const.py b/homeassistant/components/xbee/const.py deleted file mode 100644 index a77e71e92f5..00000000000 --- a/homeassistant/components/xbee/const.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Constants for the xbee integration.""" -CONF_ON_STATE = "on_state" -DEFAULT_ON_STATE = "high" -DOMAIN = "xbee" -STATES = ["high", "low"] diff --git a/homeassistant/components/xbee/light.py b/homeassistant/components/xbee/light.py deleted file mode 100644 index 126eaf91f9d..00000000000 --- a/homeassistant/components/xbee/light.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Support for XBee Zigbee lights.""" -from __future__ import annotations - -import voluptuous as vol - -from homeassistant.components.light import ColorMode, LightEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import PLATFORM_SCHEMA, XBeeDigitalOut, XBeeDigitalOutConfig -from .const import CONF_ON_STATE, DEFAULT_ON_STATE, DOMAIN, STATES - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_ON_STATE, default=DEFAULT_ON_STATE): vol.In(STATES)} -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Create and add an entity based on the configuration.""" - zigbee_device = hass.data[DOMAIN] - add_entities([XBeeLight(XBeeDigitalOutConfig(config), zigbee_device)]) - - -class XBeeLight(XBeeDigitalOut, LightEntity): - """Use XBeeDigitalOut as light.""" - - _attr_color_mode = ColorMode.ONOFF - _attr_supported_color_modes = {ColorMode.ONOFF} diff --git a/homeassistant/components/xbee/manifest.json b/homeassistant/components/xbee/manifest.json deleted file mode 100644 index 150036129d2..00000000000 --- a/homeassistant/components/xbee/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "disabled": "Integration library not compatible with Python 3.10", - "domain": "xbee", - "name": "XBee", - "documentation": "https://www.home-assistant.io/integrations/xbee", - "requirements": ["xbee-helper==0.0.7"], - "codeowners": [], - "iot_class": "local_polling", - "loggers": ["xbee_helper"] -} diff --git a/homeassistant/components/xbee/sensor.py b/homeassistant/components/xbee/sensor.py deleted file mode 100644 index 9cea60ade8c..00000000000 --- a/homeassistant/components/xbee/sensor.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Support for XBee Zigbee sensors.""" -# pylint: disable=import-error -from __future__ import annotations - -from binascii import hexlify -import logging - -import voluptuous as vol -from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure - -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity -from homeassistant.const import CONF_TYPE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import DOMAIN, PLATFORM_SCHEMA, XBeeAnalogIn, XBeeAnalogInConfig, XBeeConfig - -_LOGGER = logging.getLogger(__name__) - -CONF_MAX_VOLTS = "max_volts" - -DEFAULT_VOLTS = 1.2 -TYPES = ["analog", "temperature"] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_TYPE): vol.In(TYPES), - vol.Optional(CONF_MAX_VOLTS, default=DEFAULT_VOLTS): vol.Coerce(float), - } -) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the XBee Zigbee platform. - - Uses the 'type' config value to work out which type of Zigbee sensor we're - dealing with and instantiates the relevant classes to handle it. - """ - zigbee_device = hass.data[DOMAIN] - typ = config[CONF_TYPE] - - try: - sensor_class, config_class = TYPE_CLASSES[typ] - except KeyError: - _LOGGER.exception("Unknown XBee Zigbee sensor type: %s", typ) - return - - add_entities([sensor_class(config_class(config), zigbee_device)], True) - - -class XBeeTemperatureSensor(SensorEntity): - """Representation of XBee Pro temperature sensor.""" - - _attr_device_class = SensorDeviceClass.TEMPERATURE - _attr_native_unit_of_measurement = TEMP_CELSIUS - - def __init__(self, config, device): - """Initialize the sensor.""" - self._config = config - self._device = device - self._temp = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._config.name - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._temp - - def update(self): - """Get the latest data.""" - try: - self._temp = self._device.get_temperature(self._config.address) - except ZigBeeTxFailure: - _LOGGER.warning( - "Transmission failure when attempting to get sample from " - "Zigbee device at address: %s", - hexlify(self._config.address), - ) - except ZigBeeException as exc: - _LOGGER.exception("Unable to get sample from Zigbee device: %s", exc) - - -# This must be below the classes to which it refers. -TYPE_CLASSES = { - "temperature": (XBeeTemperatureSensor, XBeeConfig), - "analog": (XBeeAnalogIn, XBeeAnalogInConfig), -} diff --git a/homeassistant/components/xbee/switch.py b/homeassistant/components/xbee/switch.py deleted file mode 100644 index 9cc25fbf7d2..00000000000 --- a/homeassistant/components/xbee/switch.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Support for XBee Zigbee switches.""" -from __future__ import annotations - -import voluptuous as vol - -from homeassistant.components.switch import SwitchEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import PLATFORM_SCHEMA, XBeeDigitalOut, XBeeDigitalOutConfig -from .const import CONF_ON_STATE, DOMAIN, STATES - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_ON_STATE): vol.In(STATES)}) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the XBee Zigbee switch platform.""" - zigbee_device = hass.data[DOMAIN] - add_entities([XBeeSwitch(XBeeDigitalOutConfig(config), zigbee_device)]) - - -class XBeeSwitch(XBeeDigitalOut, SwitchEntity): - """Representation of a XBee Zigbee Digital Out device.""" From 518001f00b92d24a699e9039e5c066f7c9795fb1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 18:58:48 +0200 Subject: [PATCH 2724/3516] Remove SoChain integration (#75505) --- .coveragerc | 1 - homeassistant/components/sochain/__init__.py | 1 - .../components/sochain/manifest.json | 10 --- homeassistant/components/sochain/sensor.py | 88 ------------------- 4 files changed, 100 deletions(-) delete mode 100644 homeassistant/components/sochain/__init__.py delete mode 100644 homeassistant/components/sochain/manifest.json delete mode 100644 homeassistant/components/sochain/sensor.py diff --git a/.coveragerc b/.coveragerc index 0dd9f18af34..f266e557e59 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1110,7 +1110,6 @@ omit = homeassistant/components/smtp/notify.py homeassistant/components/snapcast/* homeassistant/components/snmp/* - homeassistant/components/sochain/sensor.py homeassistant/components/solaredge/__init__.py homeassistant/components/solaredge/coordinator.py homeassistant/components/solaredge/sensor.py diff --git a/homeassistant/components/sochain/__init__.py b/homeassistant/components/sochain/__init__.py deleted file mode 100644 index 31a000d3947..00000000000 --- a/homeassistant/components/sochain/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The sochain component.""" diff --git a/homeassistant/components/sochain/manifest.json b/homeassistant/components/sochain/manifest.json deleted file mode 100644 index 5a568340197..00000000000 --- a/homeassistant/components/sochain/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "disabled": "Integration library depends on async_timeout==3.x.x", - "domain": "sochain", - "name": "SoChain", - "documentation": "https://www.home-assistant.io/integrations/sochain", - "requirements": ["python-sochain-api==0.0.2"], - "codeowners": [], - "iot_class": "cloud_polling", - "loggers": ["pysochain"] -} diff --git a/homeassistant/components/sochain/sensor.py b/homeassistant/components/sochain/sensor.py deleted file mode 100644 index 157d94b8706..00000000000 --- a/homeassistant/components/sochain/sensor.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Support for watching multiple cryptocurrencies.""" -# pylint: disable=import-error -from __future__ import annotations - -from datetime import timedelta - -from pysochain import ChainSo -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_ADDRESS, CONF_NAME -from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -ATTRIBUTION = "Data provided by chain.so" - -CONF_NETWORK = "network" - -DEFAULT_NAME = "Crypto Balance" - -SCAN_INTERVAL = timedelta(minutes=5) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_ADDRESS): cv.string, - vol.Required(CONF_NETWORK): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the sochain sensors.""" - - address = config[CONF_ADDRESS] - network = config[CONF_NETWORK] - name = config[CONF_NAME] - - session = async_get_clientsession(hass) - chainso = ChainSo(network, address, hass.loop, session) - - async_add_entities([SochainSensor(name, network.upper(), chainso)], True) - - -class SochainSensor(SensorEntity): - """Representation of a Sochain sensor.""" - - def __init__(self, name, unit_of_measurement, chainso): - """Initialize the sensor.""" - self._name = name - self._unit_of_measurement = unit_of_measurement - self.chainso = chainso - - @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.chainso.data.get("confirmed_balance") - if self.chainso is not None - else None - ) - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement this sensor expresses itself in.""" - return self._unit_of_measurement - - @property - def extra_state_attributes(self): - """Return the state attributes of the sensor.""" - return {ATTR_ATTRIBUTION: ATTRIBUTION} - - async def async_update(self): - """Get the latest state of the sensor.""" - await self.chainso.async_get_data() From 4395b967f2b06de7fc62a1be2853fd2465c59886 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 19:15:55 +0200 Subject: [PATCH 2725/3516] Remove Google Play Music Desktop Player (GPMDP) integration (#75508) --- .coveragerc | 1 - homeassistant/components/gpmdp/__init__.py | 1 - homeassistant/components/gpmdp/manifest.json | 10 - .../components/gpmdp/media_player.py | 382 ------------------ 4 files changed, 394 deletions(-) delete mode 100644 homeassistant/components/gpmdp/__init__.py delete mode 100644 homeassistant/components/gpmdp/manifest.json delete mode 100644 homeassistant/components/gpmdp/media_player.py diff --git a/.coveragerc b/.coveragerc index f266e557e59..873629ddd0b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -442,7 +442,6 @@ omit = homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py homeassistant/components/google_pubsub/__init__.py - homeassistant/components/gpmdp/media_player.py homeassistant/components/gpsd/sensor.py homeassistant/components/greenwave/light.py homeassistant/components/group/notify.py diff --git a/homeassistant/components/gpmdp/__init__.py b/homeassistant/components/gpmdp/__init__.py deleted file mode 100644 index a8aa82c69c3..00000000000 --- a/homeassistant/components/gpmdp/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The gpmdp component.""" diff --git a/homeassistant/components/gpmdp/manifest.json b/homeassistant/components/gpmdp/manifest.json deleted file mode 100644 index 51fad8e9e71..00000000000 --- a/homeassistant/components/gpmdp/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "gpmdp", - "name": "Google Play Music Desktop Player (GPMDP)", - "disabled": "Integration has incompatible requirements.", - "documentation": "https://www.home-assistant.io/integrations/gpmdp", - "requirements": ["websocket-client==0.54.0"], - "dependencies": ["configurator"], - "codeowners": [], - "iot_class": "local_polling" -} diff --git a/homeassistant/components/gpmdp/media_player.py b/homeassistant/components/gpmdp/media_player.py deleted file mode 100644 index b2861e4d96d..00000000000 --- a/homeassistant/components/gpmdp/media_player.py +++ /dev/null @@ -1,382 +0,0 @@ -"""Support for Google Play Music Desktop Player.""" -from __future__ import annotations - -import json -import logging -import socket -import time -from typing import Any - -import voluptuous as vol -from websocket import _exceptions, create_connection - -from homeassistant.components import configurator -from homeassistant.components.media_player import ( - PLATFORM_SCHEMA, - MediaPlayerEntity, - MediaPlayerEntityFeature, -) -from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) -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.json import load_json, save_json - -_CONFIGURING: dict[str, Any] = {} -_LOGGER = logging.getLogger(__name__) - -DEFAULT_HOST = "localhost" -DEFAULT_NAME = "GPM Desktop Player" -DEFAULT_PORT = 5672 - -GPMDP_CONFIG_FILE = "gpmpd.conf" - -PLAYBACK_DICT = {"0": STATE_PAUSED, "1": STATE_PAUSED, "2": STATE_PLAYING} # Stopped - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - } -) - - -def request_configuration(hass, config, url, add_entities_callback): - """Request configuration steps from the user.""" - if "gpmdp" in _CONFIGURING: - configurator.notify_errors( - hass, _CONFIGURING["gpmdp"], "Failed to register, please try again." - ) - - return - websocket = create_connection((url), timeout=1) - websocket.send( - json.dumps( - { - "namespace": "connect", - "method": "connect", - "arguments": ["Home Assistant"], - } - ) - ) - - def gpmdp_configuration_callback(callback_data): - """Handle configuration changes.""" - while True: - - try: - msg = json.loads(websocket.recv()) - except _exceptions.WebSocketConnectionClosedException: - continue - if msg["channel"] != "connect": - continue - if msg["payload"] != "CODE_REQUIRED": - continue - pin = callback_data.get("pin") - websocket.send( - json.dumps( - { - "namespace": "connect", - "method": "connect", - "arguments": ["Home Assistant", pin], - } - ) - ) - tmpmsg = json.loads(websocket.recv()) - if tmpmsg["channel"] == "time": - _LOGGER.error( - "Error setting up GPMDP. Please pause " - "the desktop player and try again" - ) - break - 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}) - websocket.send( - json.dumps( - { - "namespace": "connect", - "method": "connect", - "arguments": ["Home Assistant", code], - } - ) - ) - websocket.close() - break - - _CONFIGURING["gpmdp"] = configurator.request_config( - DEFAULT_NAME, - gpmdp_configuration_callback, - description=( - "Enter the pin that is displayed in the " - "Google Play Music Desktop Player." - ), - submit_caption="Submit", - fields=[{"id": "pin", "name": "Pin Code", "type": "number"}], - ) - - -def setup_gpmdp(hass, config, code, add_entities): - """Set up gpmdp.""" - name = config.get(CONF_NAME) - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - url = f"ws://{host}:{port}" - - if not code: - request_configuration(hass, config, url, add_entities) - return - - if "gpmdp" in _CONFIGURING: - configurator.request_done(hass, _CONFIGURING.pop("gpmdp")) - - add_entities([GPMDP(name, url, code)], True) - - -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the GPMDP platform.""" - codeconfig = load_json(hass.config.path(GPMDP_CONFIG_FILE)) - if codeconfig: - code = codeconfig.get("CODE") if isinstance(codeconfig, dict) else None - elif discovery_info is not None: - if "gpmdp" in _CONFIGURING: - return - code = None - else: - code = None - setup_gpmdp(hass, config, code, add_entities) - - -class GPMDP(MediaPlayerEntity): - """Representation of a GPMDP.""" - - _attr_supported_features = ( - MediaPlayerEntityFeature.PAUSE - | MediaPlayerEntityFeature.PREVIOUS_TRACK - | MediaPlayerEntityFeature.NEXT_TRACK - | MediaPlayerEntityFeature.SEEK - | MediaPlayerEntityFeature.VOLUME_SET - | MediaPlayerEntityFeature.PLAY - ) - - def __init__(self, name, url, code): - """Initialize the media player.""" - - self._connection = create_connection - self._url = url - self._authorization_code = code - self._name = name - self._status = STATE_OFF - self._ws = None - self._title = None - self._artist = None - self._albumart = None - self._seek_position = None - self._duration = None - self._volume = None - self._request_id = 0 - self._available = True - - def get_ws(self): - """Check if the websocket is setup and connected.""" - if self._ws is None: - try: - self._ws = self._connection((self._url), timeout=1) - msg = json.dumps( - { - "namespace": "connect", - "method": "connect", - "arguments": ["Home Assistant", self._authorization_code], - } - ) - self._ws.send(msg) - except (socket.timeout, ConnectionRefusedError, ConnectionResetError): - self._ws = None - return self._ws - - def send_gpmdp_msg(self, namespace, method, with_id=True): - """Send ws messages to GPMDP and verify request id in response.""" - - try: - if (websocket := self.get_ws()) is None: - self._status = STATE_OFF - return - self._request_id += 1 - websocket.send( - json.dumps( - { - "namespace": namespace, - "method": method, - "requestID": self._request_id, - } - ) - ) - if not with_id: - return - while True: - msg = json.loads(websocket.recv()) - if "requestID" in msg and msg["requestID"] == self._request_id: - return msg - except ( - ConnectionRefusedError, - ConnectionResetError, - _exceptions.WebSocketTimeoutException, - _exceptions.WebSocketProtocolException, - _exceptions.WebSocketPayloadException, - _exceptions.WebSocketConnectionClosedException, - ): - self._ws = None - - def update(self): - """Get the latest details from the player.""" - time.sleep(1) - try: - self._available = True - playstate = self.send_gpmdp_msg("playback", "getPlaybackState") - if playstate is None: - return - self._status = PLAYBACK_DICT[str(playstate["value"])] - time_data = self.send_gpmdp_msg("playback", "getCurrentTime") - if time_data is not None: - self._seek_position = int(time_data["value"] / 1000) - track_data = self.send_gpmdp_msg("playback", "getCurrentTrack") - if track_data is not None: - self._title = track_data["value"]["title"] - self._artist = track_data["value"]["artist"] - self._albumart = track_data["value"]["albumArt"] - self._duration = int(track_data["value"]["duration"] / 1000) - volume_data = self.send_gpmdp_msg("volume", "getVolume") - if volume_data is not None: - self._volume = volume_data["value"] / 100 - except OSError: - self._available = False - - @property - def available(self): - """Return if media player is available.""" - return self._available - - @property - def media_content_type(self): - """Content type of current playing media.""" - return MEDIA_TYPE_MUSIC - - @property - def state(self): - """Return the state of the device.""" - return self._status - - @property - def media_title(self): - """Title of current playing media.""" - return self._title - - @property - def media_artist(self): - """Artist of current playing media (Music track only).""" - return self._artist - - @property - def media_image_url(self): - """Image url of current playing media.""" - return self._albumart - - @property - def media_seek_position(self): - """Time in seconds of current seek position.""" - return self._seek_position - - @property - def media_duration(self): - """Time in seconds of current song duration.""" - return self._duration - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._volume - - @property - def name(self): - """Return the name of the device.""" - return self._name - - def media_next_track(self): - """Send media_next command to media player.""" - self.send_gpmdp_msg("playback", "forward", False) - - def media_previous_track(self): - """Send media_previous command to media player.""" - self.send_gpmdp_msg("playback", "rewind", False) - - def media_play(self): - """Send media_play command to media player.""" - self.send_gpmdp_msg("playback", "playPause", False) - self._status = STATE_PLAYING - self.schedule_update_ha_state() - - def media_pause(self): - """Send media_pause command to media player.""" - self.send_gpmdp_msg("playback", "playPause", False) - self._status = STATE_PAUSED - self.schedule_update_ha_state() - - def media_seek(self, position): - """Send media_seek command to media player.""" - if (websocket := self.get_ws()) is None: - return - websocket.send( - json.dumps( - { - "namespace": "playback", - "method": "setCurrentTime", - "arguments": [position * 1000], - } - ) - ) - self.schedule_update_ha_state() - - def volume_up(self): - """Send volume_up command to media player.""" - if (websocket := self.get_ws()) is None: - return - websocket.send('{"namespace": "volume", "method": "increaseVolume"}') - self.schedule_update_ha_state() - - def volume_down(self): - """Send volume_down command to media player.""" - if (websocket := self.get_ws()) is None: - return - websocket.send('{"namespace": "volume", "method": "decreaseVolume"}') - self.schedule_update_ha_state() - - def set_volume_level(self, volume): - """Set volume on media player, range(0..1).""" - if (websocket := self.get_ws()) is None: - return - websocket.send( - json.dumps( - { - "namespace": "volume", - "method": "setVolume", - "arguments": [volume * 100], - } - ) - ) - self.schedule_update_ha_state() From 48e82ff62fc6719fdc4f5fec8db52b26c7d82937 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 20 Jul 2022 12:25:17 -0500 Subject: [PATCH 2726/3516] Fix failure to raise on bad YAML syntax from include files (#75510) Co-authored-by: Franck Nijhof --- homeassistant/util/yaml/loader.py | 4 ++-- tests/util/yaml/fixtures/bad.yaml.txt | 26 ++++++++++++++++++++++++++ tests/util/yaml/test_init.py | 10 ++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 tests/util/yaml/fixtures/bad.yaml.txt diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index e3add3a7c44..09e19af6840 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections import OrderedDict from collections.abc import Iterator import fnmatch -from io import StringIO +from io import StringIO, TextIOWrapper import logging import os from pathlib import Path @@ -169,7 +169,7 @@ def parse_yaml( except yaml.YAMLError: # Loading failed, so we now load with the slow line loader # since the C one will not give us line numbers - if isinstance(content, (StringIO, TextIO)): + if isinstance(content, (StringIO, TextIO, TextIOWrapper)): # Rewind the stream so we can try again content.seek(0, 0) return _parse_yaml_pure_python(content, secrets) diff --git a/tests/util/yaml/fixtures/bad.yaml.txt b/tests/util/yaml/fixtures/bad.yaml.txt new file mode 100644 index 00000000000..8f6a62a6511 --- /dev/null +++ b/tests/util/yaml/fixtures/bad.yaml.txt @@ -0,0 +1,26 @@ +- id: '1658085239190' + alias: Config validation test + description: '' + trigger: + - platform: time + at: 00:02:03 + condition: [] + action: + - service: script.notify_admin + data: + title: 'Here's something that does not work...!' + message: failing + mode: single +- id: '165808523911590' + alias: Config validation test FIXED + description: '' + trigger: + - platform: time + at: 00:02:03 + condition: [] + action: + - service: script.notify_admin + data: + title: 'Here is something that should work...!' + message: fixed? + mode: single diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 11dc40233dc..8d1b7c1adf1 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -2,6 +2,7 @@ import importlib import io import os +import pathlib import unittest from unittest.mock import patch @@ -490,3 +491,12 @@ def test_input(try_both_loaders, try_both_dumpers): def test_c_loader_is_available_in_ci(): """Verify we are testing the C loader in the CI.""" assert yaml.loader.HAS_C_LOADER is True + + +async def test_loading_actual_file_with_syntax(hass, try_both_loaders): + """Test loading a real file with syntax errors.""" + with pytest.raises(HomeAssistantError): + fixture_path = pathlib.Path(__file__).parent.joinpath( + "fixtures", "bad.yaml.txt" + ) + await hass.async_add_executor_job(load_yaml_config_file, fixture_path) From 7ba3227d52e12c3f90b8488335a2a55757b90a13 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Wed, 20 Jul 2022 19:27:11 +0200 Subject: [PATCH 2727/3516] Fix - Forcast.solar issue on saving settings in options flow without api key (#75504) --- homeassistant/components/forecast_solar/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/forecast_solar/config_flow.py b/homeassistant/components/forecast_solar/config_flow.py index 86de17ef285..600ed363a8b 100644 --- a/homeassistant/components/forecast_solar/config_flow.py +++ b/homeassistant/components/forecast_solar/config_flow.py @@ -99,7 +99,7 @@ class ForecastSolarOptionFlowHandler(OptionsFlow): CONF_API_KEY, description={ "suggested_value": self.config_entry.options.get( - CONF_API_KEY + CONF_API_KEY, "" ) }, ): str, From a91ca46342789861e310ac04e189430cc373a748 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 20 Jul 2022 11:29:23 -0600 Subject: [PATCH 2728/3516] Fix incorrect Ambient PWS lightning strike sensor state classes (#75520) --- homeassistant/components/ambient_station/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 2944f33938d..4eae53b6a03 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -281,14 +281,14 @@ SENSOR_DESCRIPTIONS = ( name="Lightning strikes per day", icon="mdi:lightning-bolt", native_unit_of_measurement="strikes", - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_LIGHTNING_PER_HOUR, name="Lightning strikes per hour", icon="mdi:lightning-bolt", native_unit_of_measurement="strikes", - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_MAXDAILYGUST, From 079460d2dd09562577b98d5371a17f74eaac7b8a Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 20 Jul 2022 22:11:05 +0300 Subject: [PATCH 2729/3516] Bump aioshelly to 2.0.1 (#75523) --- 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 a9aade00933..48e48b618e4 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==2.0.0"], + "requirements": ["aioshelly==2.0.1"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index b3395085f2e..6e525274af2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -247,7 +247,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==2.0.0 +aioshelly==2.0.1 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f53ea0fd4cf..cf07a07117e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -222,7 +222,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==2.0.0 +aioshelly==2.0.1 # homeassistant.components.skybell aioskybell==22.7.0 From b749622c0131b22c41617444c3a8b60bde8eca3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 20 Jul 2022 22:28:53 +0300 Subject: [PATCH 2730/3516] Migrate Huawei LTE to new entity naming style (#75303) --- .../components/huawei_lte/__init__.py | 10 +------ .../components/huawei_lte/binary_sensor.py | 30 +++++++------------ .../components/huawei_lte/device_tracker.py | 3 +- homeassistant/components/huawei_lte/sensor.py | 8 ++--- homeassistant/components/huawei_lte/switch.py | 12 +++----- tests/components/huawei_lte/test_switches.py | 2 +- 6 files changed, 22 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 3ca3daff523..bace633f128 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -586,10 +586,7 @@ class HuaweiLteBaseEntity(Entity): _available: bool = field(default=True, init=False) _unsub_handlers: list[Callable] = field(default_factory=list, init=False) - - @property - def _entity_name(self) -> str: - raise NotImplementedError + _attr_has_entity_name: bool = field(default=True, init=False) @property def _device_unique_id(self) -> str: @@ -601,11 +598,6 @@ class HuaweiLteBaseEntity(Entity): """Return unique ID for entity.""" return f"{self.router.config_entry.unique_id}-{self._device_unique_id}" - @property - def name(self) -> str: - """Return entity name.""" - return f"Huawei {self.router.device_name} {self._entity_name}" - @property def available(self) -> bool: """Return whether the entity is available.""" diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py index 9bd455d9d59..695b7e2e815 100644 --- a/homeassistant/components/huawei_lte/binary_sensor.py +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -105,15 +105,13 @@ CONNECTION_STATE_ATTRIBUTES = { class HuaweiLteMobileConnectionBinarySensor(HuaweiLteBaseBinarySensor): """Huawei LTE mobile connection binary sensor.""" + _attr_name: str = field(default="Mobile connection", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_MONITORING_STATUS self.item = "ConnectionStatus" - @property - def _entity_name(self) -> str: - return "Mobile connection" - @property def is_on(self) -> bool: """Return whether the binary sensor is on.""" @@ -176,57 +174,49 @@ class HuaweiLteBaseWifiStatusBinarySensor(HuaweiLteBaseBinarySensor): class HuaweiLteWifiStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor): """Huawei LTE WiFi status binary sensor.""" + _attr_name: str = field(default="WiFi status", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_MONITORING_STATUS self.item = "WifiStatus" - @property - def _entity_name(self) -> str: - return "WiFi status" - @dataclass class HuaweiLteWifi24ghzStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor): """Huawei LTE 2.4GHz WiFi status binary sensor.""" + _attr_name: str = field(default="2.4GHz WiFi status", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_WLAN_WIFI_FEATURE_SWITCH self.item = "wifi24g_switch_enable" - @property - def _entity_name(self) -> str: - return "2.4GHz WiFi status" - @dataclass class HuaweiLteWifi5ghzStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor): """Huawei LTE 5GHz WiFi status binary sensor.""" + _attr_name: str = field(default="5GHz WiFi status", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_WLAN_WIFI_FEATURE_SWITCH self.item = "wifi5g_enabled" - @property - def _entity_name(self) -> str: - return "5GHz WiFi status" - @dataclass class HuaweiLteSmsStorageFullBinarySensor(HuaweiLteBaseBinarySensor): """Huawei LTE SMS storage full binary sensor.""" + _attr_name: str = field(default="SMS storage full", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_MONITORING_CHECK_NOTIFICATIONS self.item = "SmsStorageFull" - @property - def _entity_name(self) -> str: - return "SMS storage full" - @property def is_on(self) -> bool: """Return whether the binary sensor is on.""" diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index ee643c59b12..179587d72d7 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -185,7 +185,8 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): _extra_state_attributes: dict[str, Any] = field(default_factory=dict, init=False) @property - def _entity_name(self) -> str: + def name(self) -> str: + """Return the name of the entity.""" return self.hostname or self.mac_address @property diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 642d8368207..44d03677ebd 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -548,6 +548,10 @@ class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity): _state: StateType = field(default=STATE_UNKNOWN, init=False) _unit: str | None = field(default=None, init=False) + def __post_init__(self) -> None: + """Initialize remaining attributes.""" + self._attr_name = self.meta.name or self.item + async def async_added_to_hass(self) -> None: """Subscribe to needed data on add.""" await super().async_added_to_hass() @@ -558,10 +562,6 @@ class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity): await super().async_will_remove_from_hass() self.router.subscriptions[self.key].remove(f"{SENSOR_DOMAIN}/{self.item}") - @property - def _entity_name(self) -> str: - return self.meta.name or self.item - @property def _device_unique_id(self) -> str: return f"{self.key}.{self.item}" diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index 611f7212d9a..78579d62698 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -92,15 +92,13 @@ class HuaweiLteBaseSwitch(HuaweiLteBaseEntityWithDevice, SwitchEntity): class HuaweiLteMobileDataSwitch(HuaweiLteBaseSwitch): """Huawei LTE mobile data switch device.""" + _attr_name: str = field(default="Mobile data", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_DIALUP_MOBILE_DATASWITCH self.item = "dataswitch" - @property - def _entity_name(self) -> str: - return "Mobile data" - @property def _device_unique_id(self) -> str: return f"{self.key}.{self.item}" @@ -126,15 +124,13 @@ class HuaweiLteMobileDataSwitch(HuaweiLteBaseSwitch): class HuaweiLteWifiGuestNetworkSwitch(HuaweiLteBaseSwitch): """Huawei LTE WiFi guest network switch device.""" + _attr_name: str = field(default="WiFi guest network", init=False) + def __post_init__(self) -> None: """Initialize identifiers.""" self.key = KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH self.item = "WifiEnable" - @property - def _entity_name(self) -> str: - return "WiFi guest network" - @property def _device_unique_id(self) -> str: return f"{self.key}.{self.item}" diff --git a/tests/components/huawei_lte/test_switches.py b/tests/components/huawei_lte/test_switches.py index c5006add923..5bafed27e70 100644 --- a/tests/components/huawei_lte/test_switches.py +++ b/tests/components/huawei_lte/test_switches.py @@ -17,7 +17,7 @@ from homeassistant.helpers.entity_registry import EntityRegistry from tests.common import MockConfigEntry -SWITCH_WIFI_GUEST_NETWORK = "switch.huawei_lte_wifi_guest_network" +SWITCH_WIFI_GUEST_NETWORK = "switch.lte_wifi_guest_network" @fixture From fdaaed652384c43a0e8f17f630fa82fe20bc59cb Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 18 Jul 2022 10:20:49 -0400 Subject: [PATCH 2731/3516] Fix ZHA light turn on issues (#75220) * rename variable * default transition is for color commands not level * no extra command for groups * don't transition color change when light off -> on * clean up * update condition * fix condition again... * simplify * simplify * missed one * rename * simplify * rename * tests * color_provided_while_off with no changes * fix missing flag clear * more tests for transition scenarios * add to comment * fix comment * don't transition when force on is set * stale comment * dont transition when colors don't change * remove extra line * remove debug print :) * fix colors * restore color to 65535 until investigated --- homeassistant/components/zha/light.py | 98 ++-- tests/components/zha/test_light.py | 811 +++++++++++++++++++++++++- 2 files changed, 868 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index e1c85b39d8e..cc8dc475c43 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -73,7 +73,6 @@ CAPABILITIES_COLOR_LOOP = 0x4 CAPABILITIES_COLOR_XY = 0x08 CAPABILITIES_COLOR_TEMP = 0x10 -DEFAULT_TRANSITION = 1 DEFAULT_MIN_BRIGHTNESS = 2 UPDATE_COLORLOOP_ACTION = 0x1 @@ -119,7 +118,7 @@ class BaseLight(LogMixin, light.LightEntity): """Operations common to all light entities.""" _FORCE_ON = False - _DEFAULT_COLOR_FROM_OFF_TRANSITION = 0 + _DEFAULT_MIN_TRANSITION_TIME = 0 def __init__(self, *args, **kwargs): """Initialize the light.""" @@ -140,7 +139,7 @@ class BaseLight(LogMixin, light.LightEntity): self._level_channel = None self._color_channel = None self._identify_channel = None - self._default_transition = None + self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME self._attr_color_mode = ColorMode.UNKNOWN # Set by sub classes @property @@ -216,33 +215,49 @@ class BaseLight(LogMixin, light.LightEntity): transition = kwargs.get(light.ATTR_TRANSITION) duration = ( transition * 10 - if transition - else self._default_transition * 10 - if self._default_transition - else DEFAULT_TRANSITION - ) + if transition is not None + else self._zha_config_transition * 10 + ) or self._DEFAULT_MIN_TRANSITION_TIME # if 0 is passed in some devices still need the minimum default brightness = kwargs.get(light.ATTR_BRIGHTNESS) effect = kwargs.get(light.ATTR_EFFECT) flash = kwargs.get(light.ATTR_FLASH) + temperature = kwargs.get(light.ATTR_COLOR_TEMP) + hs_color = kwargs.get(light.ATTR_HS_COLOR) # If the light is currently off but a turn_on call with a color/temperature is sent, # the light needs to be turned on first at a low brightness level where the light is immediately transitioned # to the correct color. Afterwards, the transition is only from the low brightness to the new brightness. # Otherwise, the transition is from the color the light had before being turned on to the new color. - # This can look especially bad with transitions longer than a second. - color_provided_from_off = ( - not self._state + # This can look especially bad with transitions longer than a second. We do not want to do this for + # devices that need to be forced to use the on command because we would end up with 4 commands sent: + # move to level, on, color, move to level... We also will not set this if the bulb is already in the + # desired color mode with the desired color or color temperature. + new_color_provided_while_off = ( + not isinstance(self, LightGroup) + and not self._FORCE_ON + and not self._state + and ( + ( + temperature is not None + and ( + self._color_temp != temperature + or self._attr_color_mode != ColorMode.COLOR_TEMP + ) + ) + or ( + hs_color is not None + and ( + self.hs_color != hs_color + or self._attr_color_mode != ColorMode.HS + ) + ) + ) and brightness_supported(self._attr_supported_color_modes) - and (light.ATTR_COLOR_TEMP in kwargs or light.ATTR_HS_COLOR in kwargs) ) - final_duration = duration - if color_provided_from_off: - # Set the duration for the color changing commands to 0. - duration = 0 if ( brightness is None - and (self._off_with_transition or color_provided_from_off) + and (self._off_with_transition or new_color_provided_while_off) and self._off_brightness is not None ): brightness = self._off_brightness @@ -254,11 +269,11 @@ class BaseLight(LogMixin, light.LightEntity): t_log = {} - if color_provided_from_off: + if new_color_provided_while_off: # If the light is currently off, we first need to turn it on at a low brightness level with no transition. # After that, we set it to the desired color/temperature with no transition. result = await self._level_channel.move_to_level_with_on_off( - DEFAULT_MIN_BRIGHTNESS, self._DEFAULT_COLOR_FROM_OFF_TRANSITION + DEFAULT_MIN_BRIGHTNESS, self._DEFAULT_MIN_TRANSITION_TIME ) t_log["move_to_level_with_on_off"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: @@ -269,7 +284,7 @@ class BaseLight(LogMixin, light.LightEntity): if ( (brightness is not None or transition) - and not color_provided_from_off + and not new_color_provided_while_off and brightness_supported(self._attr_supported_color_modes) ): result = await self._level_channel.move_to_level_with_on_off( @@ -285,7 +300,7 @@ class BaseLight(LogMixin, light.LightEntity): if ( brightness is None - and not color_provided_from_off + and not new_color_provided_while_off or (self._FORCE_ON and brightness) ): # since some lights don't always turn on with move_to_level_with_on_off, @@ -297,9 +312,13 @@ class BaseLight(LogMixin, light.LightEntity): return self._state = True - if light.ATTR_COLOR_TEMP in kwargs: - temperature = kwargs[light.ATTR_COLOR_TEMP] - result = await self._color_channel.move_to_color_temp(temperature, duration) + if temperature is not None: + result = await self._color_channel.move_to_color_temp( + temperature, + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, + ) t_log["move_to_color_temp"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) @@ -308,11 +327,14 @@ class BaseLight(LogMixin, light.LightEntity): self._color_temp = temperature self._hs_color = None - if light.ATTR_HS_COLOR in kwargs: - hs_color = kwargs[light.ATTR_HS_COLOR] + if hs_color is not None: xy_color = color_util.color_hs_to_xy(*hs_color) result = await self._color_channel.move_to_color( - int(xy_color[0] * 65535), int(xy_color[1] * 65535), duration + int(xy_color[0] * 65535), + int(xy_color[1] * 65535), + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, ) t_log["move_to_color"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: @@ -322,9 +344,9 @@ class BaseLight(LogMixin, light.LightEntity): self._hs_color = hs_color self._color_temp = None - if color_provided_from_off: + if new_color_provided_while_off: # The light is has the correct color, so we can now transition it to the correct brightness level. - result = await self._level_channel.move_to_level(level, final_duration) + result = await self._level_channel.move_to_level(level, duration) t_log["move_to_level_if_color"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) @@ -371,12 +393,13 @@ class BaseLight(LogMixin, light.LightEntity): async def async_turn_off(self, **kwargs): """Turn the entity off.""" - duration = kwargs.get(light.ATTR_TRANSITION) + transition = kwargs.get(light.ATTR_TRANSITION) supports_level = brightness_supported(self._attr_supported_color_modes) - if duration and supports_level: + # is not none looks odd here but it will override built in bulb transition times if we pass 0 in here + if transition is not None and supports_level: result = await self._level_channel.move_to_level_with_on_off( - 0, duration * 10 + 0, transition * 10 ) else: result = await self._on_off_channel.off() @@ -387,7 +410,7 @@ class BaseLight(LogMixin, light.LightEntity): if supports_level: # store current brightness so that the next turn_on uses it. - self._off_with_transition = bool(duration) + self._off_with_transition = transition is not None self._off_brightness = self._brightness self.async_write_ha_state() @@ -460,7 +483,7 @@ class Light(BaseLight, ZhaEntity): if effect_list: self._effect_list = effect_list - self._default_transition = async_get_zha_config_value( + self._zha_config_transition = async_get_zha_config_value( zha_device.gateway.config_entry, ZHA_OPTIONS, CONF_DEFAULT_LIGHT_TRANSITION, @@ -472,6 +495,7 @@ class Light(BaseLight, ZhaEntity): """Set the state.""" self._state = bool(value) if value: + self._off_with_transition = False self._off_brightness = None self.async_write_ha_state() @@ -605,7 +629,7 @@ class HueLight(Light): @STRICT_MATCH( channel_names=CHANNEL_ON_OFF, aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}, - manufacturers={"Jasco", "Quotra-Vision"}, + manufacturers={"Jasco", "Quotra-Vision", "eWeLight", "eWeLink"}, ) class ForceOnLight(Light): """Representation of a light which does not respect move_to_level_with_on_off.""" @@ -621,7 +645,7 @@ class ForceOnLight(Light): class SengledLight(Light): """Representation of a Sengled light which does not react to move_to_color_temp with 0 as a transition.""" - _DEFAULT_COLOR_FROM_OFF_TRANSITION = 1 + _DEFAULT_MIN_TRANSITION_TIME = 1 @GROUP_MATCH() @@ -639,7 +663,7 @@ class LightGroup(BaseLight, ZhaGroupEntity): self._color_channel = group.endpoint[Color.cluster_id] self._identify_channel = group.endpoint[Identify.cluster_id] self._debounced_member_refresh = None - self._default_transition = async_get_zha_config_value( + self._zha_config_transition = async_get_zha_config_value( zha_device.gateway.config_entry, ZHA_OPTIONS, CONF_DEFAULT_LIGHT_TRANSITION, diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index dd6df0dff19..982ff622341 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -15,7 +15,11 @@ from homeassistant.components.light import ( ColorMode, ) from homeassistant.components.zha.core.group import GroupMember -from homeassistant.components.zha.light import FLASH_EFFECTS +from homeassistant.components.zha.light import ( + CAPABILITIES_COLOR_TEMP, + CAPABILITIES_COLOR_XY, + FLASH_EFFECTS, +) from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform import homeassistant.util.dt as dt_util @@ -142,6 +146,10 @@ async def device_light_1(hass, zigpy_device_mock, zha_device_joined): ieee=IEEE_GROUPABLE_DEVICE, nwk=0xB79D, ) + color_cluster = zigpy_device.endpoints[1].light_color + color_cluster.PLUGGED_ATTR_READS = { + "color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY + } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True return zha_device @@ -167,8 +175,13 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined): } }, ieee=IEEE_GROUPABLE_DEVICE2, + manufacturer="Sengled", nwk=0xC79E, ) + color_cluster = zigpy_device.endpoints[1].light_color + color_cluster.PLUGGED_ATTR_READS = { + "color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY + } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True return zha_device @@ -201,6 +214,38 @@ async def device_light_3(hass, zigpy_device_mock, zha_device_joined): return zha_device +@pytest.fixture +async def eWeLink_light(hass, zigpy_device_mock, zha_device_joined): + """Mock eWeLink light.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [ + general.OnOff.cluster_id, + general.LevelControl.cluster_id, + lighting.Color.cluster_id, + general.Groups.cluster_id, + general.Identify.cluster_id, + ], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + }, + ieee="03:2d:6f:00:0a:90:69:e3", + manufacturer="eWeLink", + nwk=0xB79D, + ) + color_cluster = zigpy_device.endpoints[1].light_color + color_cluster.PLUGGED_ATTR_READS = { + "color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY + } + zha_device = await zha_device_joined(zigpy_device) + zha_device.available = True + return zha_device + + async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored): """Test zha light platform refresh.""" @@ -323,6 +368,758 @@ async def test_light( await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_LONG) +@patch( + "zigpy.zcl.clusters.lighting.Color.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@patch( + "zigpy.zcl.clusters.general.Identify.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@patch( + "zigpy.zcl.clusters.general.LevelControl.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@patch( + "zigpy.zcl.clusters.general.OnOff.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +async def test_transitions( + hass, device_light_1, device_light_2, eWeLink_light, coordinator +): + """Test ZHA light transition code.""" + zha_gateway = get_zha_gateway(hass) + assert zha_gateway is not None + zha_gateway.coordinator_zha_device = coordinator + coordinator._zha_gateway = zha_gateway + device_light_1._zha_gateway = zha_gateway + device_light_2._zha_gateway = zha_gateway + member_ieee_addresses = [device_light_1.ieee, device_light_2.ieee] + members = [GroupMember(device_light_1.ieee, 1), GroupMember(device_light_2.ieee, 1)] + + assert coordinator.is_coordinator + + # test creating a group with 2 members + zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members) + await hass.async_block_till_done() + + assert zha_group is not None + assert len(zha_group.members) == 2 + for member in zha_group.members: + assert member.device.ieee in member_ieee_addresses + assert member.group == zha_group + assert member.endpoint is not None + + device_1_entity_id = await find_entity_id(Platform.LIGHT, device_light_1, hass) + device_2_entity_id = await find_entity_id(Platform.LIGHT, device_light_2, hass) + eWeLink_light_entity_id = await find_entity_id(Platform.LIGHT, eWeLink_light, hass) + assert device_1_entity_id != device_2_entity_id + + group_entity_id = async_find_group_entity_id(hass, Platform.LIGHT, zha_group) + assert hass.states.get(group_entity_id) is not None + + assert device_1_entity_id in zha_group.member_entity_ids + assert device_2_entity_id in zha_group.member_entity_ids + + dev1_cluster_on_off = device_light_1.device.endpoints[1].on_off + dev2_cluster_on_off = device_light_2.device.endpoints[1].on_off + eWeLink_cluster_on_off = eWeLink_light.device.endpoints[1].on_off + + dev1_cluster_level = device_light_1.device.endpoints[1].level + dev2_cluster_level = device_light_2.device.endpoints[1].level + eWeLink_cluster_level = eWeLink_light.device.endpoints[1].level + + dev1_cluster_color = device_light_1.device.endpoints[1].light_color + dev2_cluster_color = device_light_2.device.endpoints[1].light_color + eWeLink_cluster_color = eWeLink_light.device.endpoints[1].light_color + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, [device_light_1, device_light_2]) + await async_wait_for_updates(hass) + + # test that the lights were created and are off + group_state = hass.states.get(group_entity_id) + assert group_state.state == STATE_OFF + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_OFF + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_OFF + + # first test 0 length transition with no color provided + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_level.request.reset_mock() + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {"entity_id": device_1_entity_id, "transition": 0, "brightness": 50}, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 0 + assert dev1_cluster_on_off.request.await_count == 0 + assert dev1_cluster_color.request.call_count == 0 + assert dev1_cluster_color.request.await_count == 0 + assert dev1_cluster_level.request.call_count == 1 + assert dev1_cluster_level.request.await_count == 1 + assert dev1_cluster_level.request.call_args == call( + False, + 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 50, # brightness (level in ZCL) + 0, # transition time + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_ON + assert light1_state.attributes["brightness"] == 50 + + dev1_cluster_level.request.reset_mock() + + # test non 0 length transition with color provided while light is on + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_1_entity_id, + "transition": 3, + "brightness": 18, + "color_temp": 432, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 0 + assert dev1_cluster_on_off.request.await_count == 0 + assert dev1_cluster_color.request.call_count == 1 + assert dev1_cluster_color.request.await_count == 1 + assert dev1_cluster_level.request.call_count == 1 + assert dev1_cluster_level.request.await_count == 1 + assert dev1_cluster_level.request.call_args == call( + False, + 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 18, # brightness (level in ZCL) + 30, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_color.request.call_args == call( + False, + 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, + 432, # color temp mireds + 30.0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_ON + assert light1_state.attributes["brightness"] == 18 + assert light1_state.attributes["color_temp"] == 432 + assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + dev1_cluster_level.request.reset_mock() + dev1_cluster_color.request.reset_mock() + + # test 0 length transition to turn light off + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + "entity_id": device_1_entity_id, + "transition": 0, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 0 + assert dev1_cluster_on_off.request.await_count == 0 + assert dev1_cluster_color.request.call_count == 0 + assert dev1_cluster_color.request.await_count == 0 + assert dev1_cluster_level.request.call_count == 1 + assert dev1_cluster_level.request.await_count == 1 + assert dev1_cluster_level.request.call_args == call( + False, + 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 0, # brightness (level in ZCL) + 0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_OFF + + dev1_cluster_level.request.reset_mock() + + # test non 0 length transition and color temp while turning light on (color_provided_while_off) + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_1_entity_id, + "transition": 1, + "brightness": 25, + "color_temp": 235, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 0 + assert dev1_cluster_on_off.request.await_count == 0 + assert dev1_cluster_color.request.call_count == 1 + assert dev1_cluster_color.request.await_count == 1 + assert dev1_cluster_level.request.call_count == 2 + assert dev1_cluster_level.request.await_count == 2 + + # first it comes on with no transition at 2 brightness + assert dev1_cluster_level.request.call_args_list[0] == call( + False, + 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 2, # brightness (level in ZCL) + 0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_color.request.call_args == call( + False, + 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, + 235, # color temp mireds + 0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_level.request.call_args_list[1] == call( + False, + 0, + dev1_cluster_level.commands_by_name["move_to_level"].schema, + 25, # brightness (level in ZCL) + 10.0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_ON + assert light1_state.attributes["brightness"] == 25 + assert light1_state.attributes["color_temp"] == 235 + assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + dev1_cluster_level.request.reset_mock() + dev1_cluster_color.request.reset_mock() + + # turn light 1 back off + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + "entity_id": device_1_entity_id, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 1 + assert dev1_cluster_on_off.request.await_count == 1 + assert dev1_cluster_color.request.call_count == 0 + assert dev1_cluster_color.request.await_count == 0 + assert dev1_cluster_level.request.call_count == 0 + assert dev1_cluster_level.request.await_count == 0 + group_state = hass.states.get(group_entity_id) + assert group_state.state == STATE_OFF + + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_color.request.reset_mock() + dev1_cluster_level.request.reset_mock() + + # test no transition provided and color temp while turning light on (color_provided_while_off) + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_1_entity_id, + "brightness": 25, + "color_temp": 236, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 0 + assert dev1_cluster_on_off.request.await_count == 0 + assert dev1_cluster_color.request.call_count == 1 + assert dev1_cluster_color.request.await_count == 1 + assert dev1_cluster_level.request.call_count == 2 + assert dev1_cluster_level.request.await_count == 2 + + # first it comes on with no transition at 2 brightness + assert dev1_cluster_level.request.call_args_list[0] == call( + False, + 4, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 2, # brightness (level in ZCL) + 0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_color.request.call_args == call( + False, + 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, + 236, # color temp mireds + 0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_level.request.call_args_list[1] == call( + False, + 0, + dev1_cluster_level.commands_by_name["move_to_level"].schema, + 25, # brightness (level in ZCL) + 0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_ON + assert light1_state.attributes["brightness"] == 25 + assert light1_state.attributes["color_temp"] == 236 + assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + dev1_cluster_level.request.reset_mock() + dev1_cluster_color.request.reset_mock() + + # turn light 1 back off to setup group test + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + "entity_id": device_1_entity_id, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 1 + assert dev1_cluster_on_off.request.await_count == 1 + assert dev1_cluster_color.request.call_count == 0 + assert dev1_cluster_color.request.await_count == 0 + assert dev1_cluster_level.request.call_count == 0 + assert dev1_cluster_level.request.await_count == 0 + group_state = hass.states.get(group_entity_id) + assert group_state.state == STATE_OFF + + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_color.request.reset_mock() + dev1_cluster_level.request.reset_mock() + + # test no transition when the same color temp is provided from off + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_1_entity_id, + "color_temp": 236, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 1 + assert dev1_cluster_on_off.request.await_count == 1 + assert dev1_cluster_color.request.call_count == 1 + assert dev1_cluster_color.request.await_count == 1 + assert dev1_cluster_level.request.call_count == 0 + assert dev1_cluster_level.request.await_count == 0 + + assert dev1_cluster_on_off.request.call_args == call( + False, + 1, + dev1_cluster_on_off.commands_by_name["on"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + assert dev1_cluster_color.request.call_args == call( + False, + 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, + 236, # color temp mireds + 0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_ON + assert light1_state.attributes["brightness"] == 25 + assert light1_state.attributes["color_temp"] == 236 + assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_color.request.reset_mock() + + # turn light 1 back off to setup group test + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + "entity_id": device_1_entity_id, + }, + blocking=True, + ) + assert dev1_cluster_on_off.request.call_count == 1 + assert dev1_cluster_on_off.request.await_count == 1 + assert dev1_cluster_color.request.call_count == 0 + assert dev1_cluster_color.request.await_count == 0 + assert dev1_cluster_level.request.call_count == 0 + assert dev1_cluster_level.request.await_count == 0 + group_state = hass.states.get(group_entity_id) + assert group_state.state == STATE_OFF + + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_color.request.reset_mock() + dev1_cluster_level.request.reset_mock() + + # test sengled light uses default minimum transition time + dev2_cluster_on_off.request.reset_mock() + dev2_cluster_color.request.reset_mock() + dev2_cluster_level.request.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {"entity_id": device_2_entity_id, "transition": 0, "brightness": 100}, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 0 + assert dev2_cluster_on_off.request.await_count == 0 + assert dev2_cluster_color.request.call_count == 0 + assert dev2_cluster_color.request.await_count == 0 + assert dev2_cluster_level.request.call_count == 1 + assert dev2_cluster_level.request.await_count == 1 + assert dev2_cluster_level.request.call_args == call( + False, + 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 100, # brightness (level in ZCL) + 1, # transition time - sengled light uses default minimum + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_ON + assert light2_state.attributes["brightness"] == 100 + + dev2_cluster_level.request.reset_mock() + + # turn the sengled light back off + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + "entity_id": device_2_entity_id, + }, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 1 + assert dev2_cluster_on_off.request.await_count == 1 + assert dev2_cluster_color.request.call_count == 0 + assert dev2_cluster_color.request.await_count == 0 + assert dev2_cluster_level.request.call_count == 0 + assert dev2_cluster_level.request.await_count == 0 + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_OFF + + dev2_cluster_on_off.request.reset_mock() + + # test non 0 length transition and color temp while turning light on and sengled (color_provided_while_off) + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_2_entity_id, + "transition": 1, + "brightness": 25, + "color_temp": 235, + }, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 0 + assert dev2_cluster_on_off.request.await_count == 0 + assert dev2_cluster_color.request.call_count == 1 + assert dev2_cluster_color.request.await_count == 1 + assert dev2_cluster_level.request.call_count == 2 + assert dev2_cluster_level.request.await_count == 2 + + # first it comes on with no transition at 2 brightness + assert dev2_cluster_level.request.call_args_list[0] == call( + False, + 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 2, # brightness (level in ZCL) + 1, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev2_cluster_color.request.call_args == call( + False, + 10, + dev2_cluster_color.commands_by_name["move_to_color_temp"].schema, + 235, # color temp mireds + 1, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when color_provided_while_off + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev2_cluster_level.request.call_args_list[1] == call( + False, + 0, + dev2_cluster_level.commands_by_name["move_to_level"].schema, + 25, # brightness (level in ZCL) + 10.0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_ON + assert light2_state.attributes["brightness"] == 25 + assert light2_state.attributes["color_temp"] == 235 + assert light2_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + dev2_cluster_level.request.reset_mock() + dev2_cluster_color.request.reset_mock() + + # turn the sengled light back off + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + { + "entity_id": device_2_entity_id, + }, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 1 + assert dev2_cluster_on_off.request.await_count == 1 + assert dev2_cluster_color.request.call_count == 0 + assert dev2_cluster_color.request.await_count == 0 + assert dev2_cluster_level.request.call_count == 0 + assert dev2_cluster_level.request.await_count == 0 + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_OFF + + dev2_cluster_on_off.request.reset_mock() + + # test non 0 length transition and color temp while turning group light on (color_provided_while_off) + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": group_entity_id, + "transition": 1, + "brightness": 25, + "color_temp": 235, + }, + blocking=True, + ) + + group_on_off_channel = zha_group.endpoint[general.OnOff.cluster_id] + group_level_channel = zha_group.endpoint[general.LevelControl.cluster_id] + group_color_channel = zha_group.endpoint[lighting.Color.cluster_id] + assert group_on_off_channel.request.call_count == 0 + assert group_on_off_channel.request.await_count == 0 + assert group_color_channel.request.call_count == 1 + assert group_color_channel.request.await_count == 1 + assert group_level_channel.request.call_count == 1 + assert group_level_channel.request.await_count == 1 + + # groups are omitted from the 3 call dance for color_provided_while_off + assert group_color_channel.request.call_args == call( + False, + 10, + dev2_cluster_color.commands_by_name["move_to_color_temp"].schema, + 235, # color temp mireds + 10.0, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when color_provided_while_off + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert group_level_channel.request.call_args == call( + False, + 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 25, # brightness (level in ZCL) + 10.0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + group_state = hass.states.get(group_entity_id) + assert group_state.state == STATE_ON + assert group_state.attributes["brightness"] == 25 + assert group_state.attributes["color_temp"] == 235 + assert group_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + group_on_off_channel.request.reset_mock() + group_color_channel.request.reset_mock() + group_level_channel.request.reset_mock() + + # turn the sengled light back on + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_2_entity_id, + }, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 1 + assert dev2_cluster_on_off.request.await_count == 1 + assert dev2_cluster_color.request.call_count == 0 + assert dev2_cluster_color.request.await_count == 0 + assert dev2_cluster_level.request.call_count == 0 + assert dev2_cluster_level.request.await_count == 0 + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_ON + + dev2_cluster_on_off.request.reset_mock() + + # turn the light off with a transition + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_off", + {"entity_id": device_2_entity_id, "transition": 2}, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 0 + assert dev2_cluster_on_off.request.await_count == 0 + assert dev2_cluster_color.request.call_count == 0 + assert dev2_cluster_color.request.await_count == 0 + assert dev2_cluster_level.request.call_count == 1 + assert dev2_cluster_level.request.await_count == 1 + assert dev2_cluster_level.request.call_args == call( + False, + 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 0, # brightness (level in ZCL) + 20, # transition time + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_OFF + + dev2_cluster_level.request.reset_mock() + + # turn the light back on with no args should use a transition and last known brightness + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + {"entity_id": device_2_entity_id}, + blocking=True, + ) + assert dev2_cluster_on_off.request.call_count == 0 + assert dev2_cluster_on_off.request.await_count == 0 + assert dev2_cluster_color.request.call_count == 0 + assert dev2_cluster_color.request.await_count == 0 + assert dev2_cluster_level.request.call_count == 1 + assert dev2_cluster_level.request.await_count == 1 + assert dev2_cluster_level.request.call_args == call( + False, + 4, + dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + 25, # brightness (level in ZCL) - this is the last brightness we set a few tests above + 1, # transition time - sengled light uses default minimum + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light2_state = hass.states.get(device_2_entity_id) + assert light2_state.state == STATE_ON + + dev2_cluster_level.request.reset_mock() + + # test eWeLink color temp while turning light on from off (color_provided_while_off) + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": eWeLink_light_entity_id, + "color_temp": 235, + }, + blocking=True, + ) + assert eWeLink_cluster_on_off.request.call_count == 1 + assert eWeLink_cluster_on_off.request.await_count == 1 + assert eWeLink_cluster_color.request.call_count == 1 + assert eWeLink_cluster_color.request.await_count == 1 + assert eWeLink_cluster_level.request.call_count == 0 + assert eWeLink_cluster_level.request.await_count == 0 + + # first it comes on + assert eWeLink_cluster_on_off.request.call_args_list[0] == call( + False, + 1, + eWeLink_cluster_on_off.commands_by_name["on"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_color.request.call_args == call( + False, + 10, + dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, + 235, # color temp mireds + 0, # transition time (ZCL time in 10ths of a second) + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + eWeLink_state = hass.states.get(eWeLink_light_entity_id) + assert eWeLink_state.state == STATE_ON + assert eWeLink_state.attributes["color_temp"] == 235 + assert eWeLink_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + async def async_test_on_off_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light @@ -463,7 +1260,7 @@ async def async_test_level_on_off_from_hass( 4, level_cluster.commands_by_name["move_to_level_with_on_off"].schema, 10, - 1, + 0, expect_reply=True, manufacturer=None, tries=1, @@ -601,7 +1398,10 @@ async def test_zha_group_light_entity( # test that the lights were created and are off group_state = hass.states.get(group_entity_id) assert group_state.state == STATE_OFF - assert group_state.attributes["supported_color_modes"] == [ColorMode.HS] + assert group_state.attributes["supported_color_modes"] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] # Light which is off has no color mode assert "color_mode" not in group_state.attributes @@ -629,7 +1429,10 @@ async def test_zha_group_light_entity( # Check state group_state = hass.states.get(group_entity_id) assert group_state.state == STATE_ON - assert group_state.attributes["supported_color_modes"] == [ColorMode.HS] + assert group_state.attributes["supported_color_modes"] == [ + ColorMode.COLOR_TEMP, + ColorMode.HS, + ] assert group_state.attributes["color_mode"] == ColorMode.HS # test long flashing the lights from the HA From 1b61d72eaff9c8cfe85e87baa19b6d57cd273007 Mon Sep 17 00:00:00 2001 From: apaperclip <67401560+apaperclip@users.noreply.github.com> Date: Fri, 15 Jul 2022 04:57:23 -0400 Subject: [PATCH 2732/3516] Fix aruba ssh host key algorithm (#75224) --- homeassistant/components/aruba/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/aruba/device_tracker.py b/homeassistant/components/aruba/device_tracker.py index ecdcc5f70f2..dc2d2fee8e9 100644 --- a/homeassistant/components/aruba/device_tracker.py +++ b/homeassistant/components/aruba/device_tracker.py @@ -87,7 +87,7 @@ class ArubaDeviceScanner(DeviceScanner): def get_aruba_data(self): """Retrieve data from Aruba Access Point and return parsed result.""" - connect = f"ssh {self.username}@{self.host}" + connect = f"ssh {self.username}@{self.host} -o HostKeyAlgorithms=ssh-rsa" ssh = pexpect.spawn(connect) query = ssh.expect( [ From 8b4cf288e341faa1e8c6457df1af9b8000e35e98 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Sat, 16 Jul 2022 17:00:42 +0200 Subject: [PATCH 2733/3516] Force `_attr_native_value` to metric in bmw_connected_drive (#75225) Co-authored-by: rikroe --- .../components/bmw_connected_drive/coordinator.py | 3 ++- homeassistant/components/bmw_connected_drive/sensor.py | 7 ++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/coordinator.py b/homeassistant/components/bmw_connected_drive/coordinator.py index e5a968b47fd..08e90d3c4e0 100644 --- a/homeassistant/components/bmw_connected_drive/coordinator.py +++ b/homeassistant/components/bmw_connected_drive/coordinator.py @@ -33,7 +33,8 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator): entry.data[CONF_PASSWORD], get_region_from_name(entry.data[CONF_REGION]), observer_position=GPSPosition(hass.config.latitude, hass.config.longitude), - use_metric_units=hass.config.units.is_metric, + # Force metric system as BMW API apparently only returns metric values now + use_metric_units=True, ) self.read_only = entry.options[CONF_READ_ONLY] self._entry = entry diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 9f19673c398..f1046881ed3 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -16,7 +16,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, LENGTH_MILES, PERCENTAGE, @@ -183,10 +182,8 @@ class BMWSensor(BMWBaseEntity, SensorEntity): 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 = description.unit_metric + # Force metric system as BMW API apparently only returns metric values now + self._attr_native_unit_of_measurement = description.unit_metric @callback def _handle_coordinator_update(self) -> None: From bccdb29edc009845b99cc2e4ac6ca7447968818e Mon Sep 17 00:00:00 2001 From: clayton craft Date: Fri, 15 Jul 2022 15:38:06 -0700 Subject: [PATCH 2734/3516] Bump venstarcolortouch to 0.18 (#75237) venstarcolortouch: bump to 0.18 --- homeassistant/components/venstar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index 2f3331af6e2..4a6eea28e24 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -3,7 +3,7 @@ "name": "Venstar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/venstar", - "requirements": ["venstarcolortouch==0.17"], + "requirements": ["venstarcolortouch==0.18"], "codeowners": ["@garbled1"], "iot_class": "local_polling", "loggers": ["venstarcolortouch"] diff --git a/requirements_all.txt b/requirements_all.txt index bb4bad5d959..23b672581e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2387,7 +2387,7 @@ vehicle==0.4.0 velbus-aio==2022.6.2 # homeassistant.components.venstar -venstarcolortouch==0.17 +venstarcolortouch==0.18 # homeassistant.components.vilfo vilfo-api-client==0.3.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 79318dc1789..8540cf41520 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1590,7 +1590,7 @@ vehicle==0.4.0 velbus-aio==2022.6.2 # homeassistant.components.venstar -venstarcolortouch==0.17 +venstarcolortouch==0.18 # homeassistant.components.vilfo vilfo-api-client==0.3.2 From 3a2beb2212f156ea14b00252cb1fafcaf65f4e1a Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sat, 16 Jul 2022 18:28:17 -0400 Subject: [PATCH 2735/3516] Improve UniFi Protect unauth handling (#75269) --- homeassistant/components/unifiprotect/data.py | 12 ++++++--- tests/components/unifiprotect/test_init.py | 27 ++++++++++++++----- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 9e0783a99b1..d4140759a7b 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -72,6 +72,7 @@ class ProtectData: self._pending_camera_ids: set[str] = set() self._unsub_interval: CALLBACK_TYPE | None = None self._unsub_websocket: CALLBACK_TYPE | None = None + self._auth_failures = 0 self.last_update_success = False self.api = protect @@ -117,9 +118,13 @@ class ProtectData: try: updates = await self.api.update(force=force) except NotAuthorized: - await self.async_stop() - _LOGGER.exception("Reauthentication required") - self._entry.async_start_reauth(self._hass) + if self._auth_failures < 10: + _LOGGER.exception("Auth error while updating") + self._auth_failures += 1 + else: + await self.async_stop() + _LOGGER.exception("Reauthentication required") + self._entry.async_start_reauth(self._hass) self.last_update_success = False except ClientError: if self.last_update_success: @@ -129,6 +134,7 @@ class ProtectData: self._async_process_updates(self.api.bootstrap) else: self.last_update_success = True + self._auth_failures = 0 self._async_process_updates(updates) @callback diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index f6f0645df18..9392caa30ac 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -9,14 +9,18 @@ import aiohttp from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient from pyunifiprotect.data import NVR, Bootstrap, Light -from homeassistant.components.unifiprotect.const import CONF_DISABLE_RTSP, DOMAIN +from homeassistant.components.unifiprotect.const import ( + CONF_DISABLE_RTSP, + DEFAULT_SCAN_INTERVAL, + DOMAIN, +) from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from . import _patch_discovery -from .utils import MockUFPFixture, init_entry +from .utils import MockUFPFixture, init_entry, time_changed from tests.common import MockConfigEntry @@ -145,12 +149,23 @@ async def test_setup_failed_update(hass: HomeAssistant, ufp: MockUFPFixture): async def test_setup_failed_update_reauth(hass: HomeAssistant, ufp: MockUFPFixture): """Test setup of unifiprotect entry with update that gives unauthroized error.""" - ufp.api.update = AsyncMock(side_effect=NotAuthorized) - await hass.config_entries.async_setup(ufp.entry.entry_id) await hass.async_block_till_done() - assert ufp.entry.state == ConfigEntryState.SETUP_RETRY - assert ufp.api.update.called + assert ufp.entry.state == ConfigEntryState.LOADED + + # reauth should not be triggered until there are 10 auth failures in a row + # to verify it is not transient + ufp.api.update = AsyncMock(side_effect=NotAuthorized) + for _ in range(10): + await time_changed(hass, DEFAULT_SCAN_INTERVAL) + assert len(hass.config_entries.flow._progress) == 0 + + assert ufp.api.update.call_count == 10 + assert ufp.entry.state == ConfigEntryState.LOADED + + await time_changed(hass, DEFAULT_SCAN_INTERVAL) + assert ufp.api.update.call_count == 11 + assert len(hass.config_entries.flow._progress) == 1 async def test_setup_failed_error(hass: HomeAssistant, ufp: MockUFPFixture): From 75aea68b753db5d88821078cbd9ecef8156a02db Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sat, 16 Jul 2022 22:26:22 +0200 Subject: [PATCH 2736/3516] Update pyotgw to 2.0.0 (#75285) * Update pyotgw to 2.0.0 * Include updated tests --- .../components/opentherm_gw/__init__.py | 4 +- .../components/opentherm_gw/config_flow.py | 4 +- .../components/opentherm_gw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../opentherm_gw/test_config_flow.py | 42 +++++++++++++------ tests/components/opentherm_gw/test_init.py | 4 +- 7 files changed, 38 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 5ec8c6420d5..0d01550df22 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -416,7 +416,7 @@ class OpenThermGatewayDevice: self.status = {} self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update" self.options_update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_options_update" - self.gateway = pyotgw.pyotgw() + self.gateway = pyotgw.OpenThermGateway() self.gw_version = None async def cleanup(self, event=None): @@ -427,7 +427,7 @@ class OpenThermGatewayDevice: async def connect_and_subscribe(self): """Connect to serial device and subscribe report handler.""" - self.status = await self.gateway.connect(self.hass.loop, self.device_path) + self.status = await self.gateway.connect(self.device_path) version_string = self.status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) self.gw_version = version_string[18:] if version_string else None _LOGGER.debug( diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 1d66d6e2069..3f91496adab 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -59,8 +59,8 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def test_connection(): """Try to connect to the OpenTherm Gateway.""" - otgw = pyotgw.pyotgw() - status = await otgw.connect(self.hass.loop, device) + otgw = pyotgw.OpenThermGateway() + status = await otgw.connect(device) await otgw.disconnect() return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 7aa19224020..dfb60413721 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -2,7 +2,7 @@ "domain": "opentherm_gw", "name": "OpenTherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", - "requirements": ["pyotgw==1.1b1"], + "requirements": ["pyotgw==2.0.0"], "codeowners": ["@mvn23"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 23b672581e8..db0107a90cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1715,7 +1715,7 @@ pyopnsense==0.2.0 pyoppleio==1.0.5 # homeassistant.components.opentherm_gw -pyotgw==1.1b1 +pyotgw==2.0.0 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8540cf41520..66dae65c33e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1167,7 +1167,7 @@ pyopenuv==2022.04.0 pyopnsense==0.2.0 # homeassistant.components.opentherm_gw -pyotgw==1.1b1 +pyotgw==2.0.0 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index 99133cf17c3..344b21e4471 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -43,10 +43,12 @@ async def test_form_user(hass): "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS + "pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS ) as mock_pyotgw_connect, patch( - "pyotgw.pyotgw.disconnect", return_value=None - ) as mock_pyotgw_disconnect: + "pyotgw.OpenThermGateway.disconnect", return_value=None + ) as mock_pyotgw_disconnect, patch( + "pyotgw.status.StatusManager._process_updates", return_value=None + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} ) @@ -75,10 +77,12 @@ async def test_form_import(hass): "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS + "pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS ) as mock_pyotgw_connect, patch( - "pyotgw.pyotgw.disconnect", return_value=None - ) as mock_pyotgw_disconnect: + "pyotgw.OpenThermGateway.disconnect", return_value=None + ) as mock_pyotgw_disconnect, patch( + "pyotgw.status.StatusManager._process_updates", return_value=None + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, @@ -117,10 +121,12 @@ async def test_form_duplicate_entries(hass): "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS + "pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS ) as mock_pyotgw_connect, patch( - "pyotgw.pyotgw.disconnect", return_value=None - ) as mock_pyotgw_disconnect: + "pyotgw.OpenThermGateway.disconnect", return_value=None + ) as mock_pyotgw_disconnect, patch( + "pyotgw.status.StatusManager._process_updates", return_value=None + ): result1 = await hass.config_entries.flow.async_configure( flow1["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} ) @@ -148,8 +154,10 @@ async def test_form_connection_timeout(hass): ) with patch( - "pyotgw.pyotgw.connect", side_effect=(asyncio.TimeoutError) - ) as mock_connect: + "pyotgw.OpenThermGateway.connect", side_effect=(asyncio.TimeoutError) + ) as mock_connect, patch( + "pyotgw.status.StatusManager._process_updates", return_value=None + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "socket://192.0.2.254:1234"}, @@ -166,7 +174,11 @@ async def test_form_connection_error(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("pyotgw.pyotgw.connect", side_effect=(SerialException)) as mock_connect: + with patch( + "pyotgw.OpenThermGateway.connect", side_effect=(SerialException) + ) as mock_connect, patch( + "pyotgw.status.StatusManager._process_updates", return_value=None + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} ) @@ -196,7 +208,11 @@ async def test_options_migration(hass): with patch( "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.connect_and_subscribe", return_value=True, - ), patch("homeassistant.components.opentherm_gw.async_setup", return_value=True): + ), patch( + "homeassistant.components.opentherm_gw.async_setup", return_value=True + ), patch( + "pyotgw.status.StatusManager._process_updates", return_value=None + ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/opentherm_gw/test_init.py b/tests/components/opentherm_gw/test_init.py index 554f58fd81b..7e16805c683 100644 --- a/tests/components/opentherm_gw/test_init.py +++ b/tests/components/opentherm_gw/test_init.py @@ -34,7 +34,7 @@ async def test_device_registry_insert(hass): with patch( "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup", return_value=None, - ), patch("pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS): + ), patch("pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS): await setup.async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() @@ -62,7 +62,7 @@ async def test_device_registry_update(hass): with patch( "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup", return_value=None, - ), patch("pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS_UPD): + ), patch("pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS_UPD): await setup.async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() From 34c30f5ab954726a0b71161ec36a9ab5eb74d7f2 Mon Sep 17 00:00:00 2001 From: Khole Date: Fri, 15 Jul 2022 22:03:22 +0100 Subject: [PATCH 2737/3516] Add fixes for hive light (#75286) --- homeassistant/components/hive/light.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index c06237f3709..69345c430c7 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -44,13 +44,15 @@ class HiveDeviceLight(HiveEntity, LightEntity): super().__init__(hive, hive_device) if self.device["hiveType"] == "warmwhitelight": self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} + self._attr_color_mode = ColorMode.BRIGHTNESS elif self.device["hiveType"] == "tuneablelight": self._attr_supported_color_modes = {ColorMode.COLOR_TEMP} + self._attr_color_mode = ColorMode.COLOR_TEMP elif self.device["hiveType"] == "colourtuneablelight": self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} - self._attr_min_mireds = self.device.get("min_mireds") - self._attr_max_mireds = self.device.get("max_mireds") + self._attr_min_mireds = 153 + self._attr_max_mireds = 370 @refresh_system async def async_turn_on(self, **kwargs): @@ -94,6 +96,13 @@ class HiveDeviceLight(HiveEntity, LightEntity): if self._attr_available: self._attr_is_on = self.device["status"]["state"] self._attr_brightness = self.device["status"]["brightness"] + if self.device["hiveType"] == "tuneablelight": + self._attr_color_temp = self.device["status"].get("color_temp") if self.device["hiveType"] == "colourtuneablelight": - rgb = self.device["status"]["hs_color"] - self._attr_hs_color = color_util.color_RGB_to_hs(*rgb) + if self.device["status"]["mode"] == "COLOUR": + rgb = self.device["status"]["hs_color"] + self._attr_hs_color = color_util.color_RGB_to_hs(*rgb) + self._attr_color_mode = ColorMode.HS + else: + self._attr_color_temp = self.device["status"].get("color_temp") + self._attr_color_mode = ColorMode.COLOR_TEMP From 8232a780eb5832e109f580add01f10e66c2a0e60 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Sat, 16 Jul 2022 16:57:17 +0200 Subject: [PATCH 2738/3516] Bump bimmer_connected to 0.10.1 (#75287) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index b10d4842163..0381035a63e 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.9.6"], + "requirements": ["bimmer_connected==0.10.1"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index db0107a90cc..41bc6690907 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -396,7 +396,7 @@ beautifulsoup4==4.11.1 bellows==0.31.1 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.6 +bimmer_connected==0.10.1 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 66dae65c33e..ad85fd22e89 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -311,7 +311,7 @@ beautifulsoup4==4.11.1 bellows==0.31.1 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.9.6 +bimmer_connected==0.10.1 # homeassistant.components.blebox blebox_uniapi==2.0.1 From 8b270cb487f10bd4c9d513d6b0aa6d2079a4e451 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 16 Jul 2022 08:45:09 -0600 Subject: [PATCH 2739/3516] Bump simplisafe-python to 2022.07.0 (#75294) --- 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 a09c273076c..b6a139fba80 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==2022.06.1"], + "requirements": ["simplisafe-python==2022.07.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/requirements_all.txt b/requirements_all.txt index 41bc6690907..f77331bde78 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2168,7 +2168,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.06.1 +simplisafe-python==2022.07.0 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ad85fd22e89..1f3d1ce89dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1440,7 +1440,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.06.1 +simplisafe-python==2022.07.0 # homeassistant.components.slack slackclient==2.5.0 From 97b6912856e67b1acf16ad63e040a71826499ceb Mon Sep 17 00:00:00 2001 From: Nick Whyte Date: Mon, 18 Jul 2022 11:17:25 +0100 Subject: [PATCH 2740/3516] Upgrade ness_alarm dependencies (#75298) * Upgrade ness alarm dependencies to fix #74571 * Update requirements --- homeassistant/components/ness_alarm/__init__.py | 1 - homeassistant/components/ness_alarm/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ness_alarm/__init__.py b/homeassistant/components/ness_alarm/__init__.py index d082f77d837..c81c3e0c7f7 100644 --- a/homeassistant/components/ness_alarm/__init__.py +++ b/homeassistant/components/ness_alarm/__init__.py @@ -99,7 +99,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: client = Client( host=host, port=port, - loop=hass.loop, update_interval=scan_interval.total_seconds(), infer_arming_state=infer_arming_state, ) diff --git a/homeassistant/components/ness_alarm/manifest.json b/homeassistant/components/ness_alarm/manifest.json index 4aa01428d27..0b20ad7e6a9 100644 --- a/homeassistant/components/ness_alarm/manifest.json +++ b/homeassistant/components/ness_alarm/manifest.json @@ -2,7 +2,7 @@ "domain": "ness_alarm", "name": "Ness Alarm", "documentation": "https://www.home-assistant.io/integrations/ness_alarm", - "requirements": ["nessclient==0.9.15"], + "requirements": ["nessclient==0.10.0"], "codeowners": ["@nickw444"], "iot_class": "local_push", "loggers": ["nessclient"] diff --git a/requirements_all.txt b/requirements_all.txt index f77331bde78..6c2be7ec5ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1068,7 +1068,7 @@ nad_receiver==0.3.0 ndms2_client==0.1.1 # homeassistant.components.ness_alarm -nessclient==0.9.15 +nessclient==0.10.0 # homeassistant.components.netdata netdata==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f3d1ce89dc..87db37d0510 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -742,7 +742,7 @@ mutesync==0.0.1 ndms2_client==0.1.1 # homeassistant.components.ness_alarm -nessclient==0.9.15 +nessclient==0.10.0 # homeassistant.components.discovery netdisco==3.0.0 From 7f43064f36410725414b326dee0ae4d86406a760 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 4 Jul 2022 08:41:23 -0500 Subject: [PATCH 2741/3516] Use the orjson equivalent default encoder when save_json is passed the default encoder (#74377) --- homeassistant/util/json.py | 27 +++++++++++++++++++++++++-- tests/util/test_json.py | 18 +++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index d69a4106728..68273c89743 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -11,6 +11,10 @@ import orjson from homeassistant.core import Event, State from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.json import ( + JSONEncoder as DefaultHASSJSONEncoder, + json_encoder_default as default_hass_orjson_encoder, +) from .file import write_utf8_file, write_utf8_file_atomic @@ -52,6 +56,15 @@ def _orjson_encoder(data: Any) -> str: ).decode("utf-8") +def _orjson_default_encoder(data: Any) -> str: + """JSON encoder that uses orjson with hass defaults.""" + return orjson.dumps( + data, + option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS, + default=default_hass_orjson_encoder, + ).decode("utf-8") + + def save_json( filename: str, data: list | dict, @@ -64,10 +77,20 @@ def save_json( Returns True on success. """ - dump: Callable[[Any], Any] = json.dumps + dump: Callable[[Any], Any] try: if encoder: - json_data = json.dumps(data, indent=2, cls=encoder) + # For backwards compatibility, if they pass in the + # default json encoder we use _orjson_default_encoder + # which is the orjson equivalent to the default encoder. + if encoder is DefaultHASSJSONEncoder: + dump = _orjson_default_encoder + json_data = _orjson_default_encoder(data) + # If they pass a custom encoder that is not the + # DefaultHASSJSONEncoder, we use the slow path of json.dumps + else: + dump = json.dumps + json_data = json.dumps(data, indent=2, cls=encoder) else: dump = _orjson_encoder json_data = _orjson_encoder(data) diff --git a/tests/util/test_json.py b/tests/util/test_json.py index 9974cbb9628..28d321036c5 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -5,12 +5,13 @@ from json import JSONEncoder, dumps import math import os from tempfile import mkdtemp -from unittest.mock import Mock +from unittest.mock import Mock, patch import pytest from homeassistant.core import Event, State from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.json import JSONEncoder as DefaultHASSJSONEncoder from homeassistant.helpers.template import TupleWrapper from homeassistant.util.json import ( SerializationError, @@ -127,6 +128,21 @@ def test_custom_encoder(): assert data == "9" +def test_default_encoder_is_passed(): + """Test we use orjson if they pass in the default encoder.""" + fname = _path_for("test6") + with patch( + "homeassistant.util.json.orjson.dumps", return_value=b"{}" + ) as mock_orjson_dumps: + save_json(fname, {"any": 1}, encoder=DefaultHASSJSONEncoder) + assert len(mock_orjson_dumps.mock_calls) == 1 + # Patch json.dumps to make sure we are using the orjson path + with patch("homeassistant.util.json.json.dumps", side_effect=Exception): + save_json(fname, {"any": {1}}, encoder=DefaultHASSJSONEncoder) + data = load_json(fname) + assert data == {"any": [1]} + + def test_find_unserializable_data(): """Find unserializeable data.""" assert find_paths_unserializable_data(1) == {} From 340da786afa1cecd401201565ecdaf9dab8a94a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Jul 2022 07:25:19 -0500 Subject: [PATCH 2742/3516] Use default encoder when saving storage (#75319) --- homeassistant/util/json.py | 27 ++++++-------------- tests/helpers/test_storage.py | 48 ++++++++++++++++++++++++++++++++++- tests/util/test_json.py | 23 +++++------------ 3 files changed, 62 insertions(+), 36 deletions(-) diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index 68273c89743..1413f6d9b15 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -49,13 +49,6 @@ def load_json(filename: str, default: list | dict | None = None) -> list | dict: return {} if default is None else default -def _orjson_encoder(data: Any) -> str: - """JSON encoder that uses orjson.""" - return orjson.dumps( - data, option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS - ).decode("utf-8") - - def _orjson_default_encoder(data: Any) -> str: """JSON encoder that uses orjson with hass defaults.""" return orjson.dumps( @@ -79,21 +72,17 @@ def save_json( """ dump: Callable[[Any], Any] try: - if encoder: - # For backwards compatibility, if they pass in the - # default json encoder we use _orjson_default_encoder - # which is the orjson equivalent to the default encoder. - if encoder is DefaultHASSJSONEncoder: - dump = _orjson_default_encoder - json_data = _orjson_default_encoder(data) + # For backwards compatibility, if they pass in the + # default json encoder we use _orjson_default_encoder + # which is the orjson equivalent to the default encoder. + if encoder and encoder is not DefaultHASSJSONEncoder: # If they pass a custom encoder that is not the # DefaultHASSJSONEncoder, we use the slow path of json.dumps - else: - dump = json.dumps - json_data = json.dumps(data, indent=2, cls=encoder) + dump = json.dumps + json_data = json.dumps(data, indent=2, cls=encoder) else: - dump = _orjson_encoder - json_data = _orjson_encoder(data) + dump = _orjson_default_encoder + json_data = _orjson_default_encoder(data) except TypeError as error: msg = f"Failed to serialize to JSON: {filename}. Bad data at {format_unserializable_data(find_paths_unserializable_data(data, dump=dump))}" _LOGGER.error(msg) diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 53c1b8a4677..ca5cb92bfd5 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta import json +from typing import NamedTuple from unittest.mock import Mock, patch import pytest @@ -13,8 +14,9 @@ from homeassistant.const import ( from homeassistant.core import CoreState from homeassistant.helpers import storage from homeassistant.util import dt +from homeassistant.util.color import RGBColor -from tests.common import async_fire_time_changed +from tests.common import async_fire_time_changed, async_test_home_assistant MOCK_VERSION = 1 MOCK_VERSION_2 = 2 @@ -460,3 +462,47 @@ async def test_changing_delayed_written_data(hass, store, hass_storage): "key": MOCK_KEY, "data": {"hello": "world"}, } + + +async def test_saving_load_round_trip(tmpdir): + """Test saving and loading round trip.""" + loop = asyncio.get_running_loop() + hass = await async_test_home_assistant(loop) + + hass.config.config_dir = await hass.async_add_executor_job( + tmpdir.mkdir, "temp_storage" + ) + + class NamedTupleSubclass(NamedTuple): + """A NamedTuple subclass.""" + + name: str + + nts = NamedTupleSubclass("a") + + data = { + "named_tuple_subclass": nts, + "rgb_color": RGBColor(255, 255, 0), + "set": {1, 2, 3}, + "list": [1, 2, 3], + "tuple": (1, 2, 3), + "dict_with_int": {1: 1, 2: 2}, + "dict_with_named_tuple": {1: nts, 2: nts}, + } + + store = storage.Store( + hass, MOCK_VERSION_2, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1 + ) + await store.async_save(data) + load = await store.async_load() + assert load == { + "dict_with_int": {"1": 1, "2": 2}, + "dict_with_named_tuple": {"1": ["a"], "2": ["a"]}, + "list": [1, 2, 3], + "named_tuple_subclass": ["a"], + "rgb_color": [255, 255, 0], + "set": [1, 2, 3], + "tuple": [1, 2, 3], + } + + await hass.async_stop(force=True) diff --git a/tests/util/test_json.py b/tests/util/test_json.py index 28d321036c5..509c0376fae 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -12,7 +12,6 @@ import pytest from homeassistant.core import Event, State from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.json import JSONEncoder as DefaultHASSJSONEncoder -from homeassistant.helpers.template import TupleWrapper from homeassistant.util.json import ( SerializationError, find_paths_unserializable_data, @@ -83,23 +82,15 @@ def test_overwrite_and_reload(atomic_writes): def test_save_bad_data(): """Test error from trying to save unserializable data.""" + + class CannotSerializeMe: + """Cannot serialize this.""" + with pytest.raises(SerializationError) as excinfo: - save_json("test4", {"hello": set()}) + save_json("test4", {"hello": CannotSerializeMe()}) - assert ( - "Failed to serialize to JSON: test4. Bad data at $.hello=set()(" - in str(excinfo.value) - ) - - -def test_save_bad_data_tuple_wrapper(): - """Test error from trying to save unserializable data.""" - with pytest.raises(SerializationError) as excinfo: - save_json("test4", {"hello": TupleWrapper(("4", "5"))}) - - assert ( - "Failed to serialize to JSON: test4. Bad data at $.hello=('4', '5')(" - in str(excinfo.value) + assert "Failed to serialize to JSON: test4. Bad data at $.hello=" in str( + excinfo.value ) From 75641b6cd41b47215646d3cefee2f1abfd1a5f82 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sun, 17 Jul 2022 05:07:47 +0800 Subject: [PATCH 2743/3516] Apply filter to libav.hls logging namespace (#75330) --- homeassistant/components/stream/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index ef68ea7bcae..f0b4ed99654 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -187,14 +187,15 @@ def filter_libav_logging() -> None: return logging.getLogger(__name__).isEnabledFor(logging.DEBUG) for logging_namespace in ( - "libav.mp4", + "libav.NULL", "libav.h264", "libav.hevc", + "libav.hls", + "libav.mp4", + "libav.mpegts", "libav.rtsp", "libav.tcp", "libav.tls", - "libav.mpegts", - "libav.NULL", ): logging.getLogger(logging_namespace).addFilter(libav_filter) From 219d1a8a1ebe71850f9fad55665c096ca6ba1e2f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 16 Jul 2022 14:04:44 -0600 Subject: [PATCH 2744/3516] Handle (and better log) more AirVisual cloud API errors (#75332) --- .../components/airvisual/__init__.py | 11 ++++----- .../components/airvisual/config_flow.py | 4 +++- .../components/airvisual/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/airvisual/test_config_flow.py | 24 +++++++++++++++++++ 6 files changed, 35 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index d0e1a741f2e..986c5306e27 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, cast +from typing import Any from pyairvisual import CloudAPI, NodeSamba from pyairvisual.errors import ( @@ -12,6 +12,7 @@ from pyairvisual.errors import ( InvalidKeyError, KeyExpiredError, NodeProError, + UnauthorizedError, ) from homeassistant.config_entries import ConfigEntry @@ -210,9 +211,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: - data = await api_coro - return cast(dict[str, Any], data) - except (InvalidKeyError, KeyExpiredError) as ex: + return await api_coro + except (InvalidKeyError, KeyExpiredError, UnauthorizedError) as ex: raise ConfigEntryAuthFailed from ex except AirVisualError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err @@ -253,8 +253,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async with NodeSamba( entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD] ) as node: - data = await node.async_get_latest_measurements() - return cast(dict[str, Any], data) + return await node.async_get_latest_measurements() except NodeProError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index f97616c38fc..385c9f55753 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -9,8 +9,10 @@ from pyairvisual import CloudAPI, NodeSamba from pyairvisual.errors import ( AirVisualError, InvalidKeyError, + KeyExpiredError, NodeProError, NotFoundError, + UnauthorizedError, ) import voluptuous as vol @@ -119,7 +121,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input[CONF_API_KEY] not in valid_keys: try: await coro - except InvalidKeyError: + except (InvalidKeyError, KeyExpiredError, UnauthorizedError): errors[CONF_API_KEY] = "invalid_api_key" except NotFoundError: errors[CONF_CITY] = "location_not_found" diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index ed803a3e6a1..9a6279f34a6 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -3,7 +3,7 @@ "name": "AirVisual", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airvisual", - "requirements": ["pyairvisual==5.0.9"], + "requirements": ["pyairvisual==2022.07.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyairvisual", "pysmb"] diff --git a/requirements_all.txt b/requirements_all.txt index 6c2be7ec5ab..4de08b3ed71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1366,7 +1366,7 @@ pyaftership==21.11.0 pyairnow==1.1.0 # homeassistant.components.airvisual -pyairvisual==5.0.9 +pyairvisual==2022.07.0 # homeassistant.components.almond pyalmond==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 87db37d0510..1a8aaf50166 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -929,7 +929,7 @@ pyaehw4a1==0.3.9 pyairnow==1.1.0 # homeassistant.components.airvisual -pyairvisual==5.0.9 +pyairvisual==2022.07.0 # homeassistant.components.almond pyalmond==0.0.2 diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index f0a75417487..9f9deca384c 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -4,8 +4,10 @@ from unittest.mock import patch from pyairvisual.errors import ( AirVisualError, InvalidKeyError, + KeyExpiredError, NodeProError, NotFoundError, + UnauthorizedError, ) import pytest @@ -84,6 +86,28 @@ async def test_duplicate_error(hass, config, config_entry, data): {CONF_API_KEY: "invalid_api_key"}, INTEGRATION_TYPE_GEOGRAPHY_NAME, ), + ( + { + CONF_API_KEY: "abcde12345", + CONF_CITY: "Beijing", + CONF_STATE: "Beijing", + CONF_COUNTRY: "China", + }, + KeyExpiredError, + {CONF_API_KEY: "invalid_api_key"}, + INTEGRATION_TYPE_GEOGRAPHY_NAME, + ), + ( + { + CONF_API_KEY: "abcde12345", + CONF_CITY: "Beijing", + CONF_STATE: "Beijing", + CONF_COUNTRY: "China", + }, + UnauthorizedError, + {CONF_API_KEY: "invalid_api_key"}, + INTEGRATION_TYPE_GEOGRAPHY_NAME, + ), ( { CONF_API_KEY: "abcde12345", From 630f731020d8bab7e20083a4cc0f29419660c943 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Jul 2022 12:56:07 -0500 Subject: [PATCH 2745/3516] Fix HKC device triggers (#75371) --- .../components/homekit_controller/device_trigger.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index dcac7238c8e..6337045a51f 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -16,7 +16,7 @@ from homeassistant.components.automation import ( ) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, KNOWN_DEVICES, TRIGGERS @@ -86,13 +86,13 @@ class TriggerSource: ) -> CALLBACK_TYPE: """Attach a trigger.""" trigger_data = automation_info["trigger_data"] + job = HassJob(action) + @callback def event_handler(char): if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]: return - self._hass.async_create_task( - action({"trigger": {**trigger_data, **config}}) - ) + self._hass.async_run_hass_job(job, {"trigger": {**trigger_data, **config}}) trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]] iid = trigger["characteristic"] @@ -231,11 +231,11 @@ async def async_setup_triggers_for_entry(hass: HomeAssistant, config_entry): def async_fire_triggers(conn: HKDevice, events: dict[tuple[int, int], Any]): """Process events generated by a HomeKit accessory into automation triggers.""" + trigger_sources: dict[str, TriggerSource] = conn.hass.data[TRIGGERS] for (aid, iid), ev in events.items(): if aid in conn.devices: device_id = conn.devices[aid] - if device_id in conn.hass.data[TRIGGERS]: - source = conn.hass.data[TRIGGERS][device_id] + if source := trigger_sources.get(device_id): source.fire(iid, ev) From 5c2ef50fca48d03e786b0953eefa2329b65a50fb Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 18 Jul 2022 17:41:06 -0400 Subject: [PATCH 2746/3516] Bump AIOAladdinConnect to 0.1.27 (#75400) --- homeassistant/components/aladdin_connect/cover.py | 7 ++++--- homeassistant/components/aladdin_connect/manifest.json | 2 +- homeassistant/components/aladdin_connect/model.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/aladdin_connect/conftest.py | 1 + tests/components/aladdin_connect/test_cover.py | 5 +++++ 7 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 9c03cd322b6..5c28d649936 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -88,6 +88,7 @@ class AladdinDevice(CoverEntity): self._device_id = device["device_id"] self._number = device["door_number"] self._attr_name = device["name"] + self._serial = device["serial"] self._attr_unique_id = f"{self._device_id}-{self._number}" async def async_added_to_hass(self) -> None: @@ -97,8 +98,8 @@ class AladdinDevice(CoverEntity): """Schedule a state update.""" self.async_write_ha_state() - self._acc.register_callback(update_callback, self._number) - await self._acc.get_doors(self._number) + self._acc.register_callback(update_callback, self._serial) + await self._acc.get_doors(self._serial) async def async_will_remove_from_hass(self) -> None: """Close Aladdin Connect before removing.""" @@ -114,7 +115,7 @@ class AladdinDevice(CoverEntity): async def async_update(self) -> None: """Update status of cover.""" - await self._acc.get_doors(self._number) + await self._acc.get_doors(self._serial) @property def is_closed(self) -> bool | None: diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 5baeba33971..4a04bf69aed 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.25"], + "requirements": ["AIOAladdinConnect==0.1.27"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/homeassistant/components/aladdin_connect/model.py b/homeassistant/components/aladdin_connect/model.py index 4248f3504fe..63624b223a9 100644 --- a/homeassistant/components/aladdin_connect/model.py +++ b/homeassistant/components/aladdin_connect/model.py @@ -11,3 +11,4 @@ class DoorDevice(TypedDict): door_number: int name: str status: str + serial: str diff --git a/requirements_all.txt b/requirements_all.txt index 4de08b3ed71..0f20e80c459 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.25 +AIOAladdinConnect==0.1.27 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1a8aaf50166..1973add02b8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.25 +AIOAladdinConnect==0.1.27 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py index ee68d207361..c8f7d240ba5 100644 --- a/tests/components/aladdin_connect/conftest.py +++ b/tests/components/aladdin_connect/conftest.py @@ -10,6 +10,7 @@ DEVICE_CONFIG_OPEN = { "name": "home", "status": "open", "link_status": "Connected", + "serial": "12345", } diff --git a/tests/components/aladdin_connect/test_cover.py b/tests/components/aladdin_connect/test_cover.py index 54ec4ee5de1..4e65607fa9d 100644 --- a/tests/components/aladdin_connect/test_cover.py +++ b/tests/components/aladdin_connect/test_cover.py @@ -33,6 +33,7 @@ DEVICE_CONFIG_OPEN = { "name": "home", "status": "open", "link_status": "Connected", + "serial": "12345", } DEVICE_CONFIG_OPENING = { @@ -41,6 +42,7 @@ DEVICE_CONFIG_OPENING = { "name": "home", "status": "opening", "link_status": "Connected", + "serial": "12345", } DEVICE_CONFIG_CLOSED = { @@ -49,6 +51,7 @@ DEVICE_CONFIG_CLOSED = { "name": "home", "status": "closed", "link_status": "Connected", + "serial": "12345", } DEVICE_CONFIG_CLOSING = { @@ -57,6 +60,7 @@ DEVICE_CONFIG_CLOSING = { "name": "home", "status": "closing", "link_status": "Connected", + "serial": "12345", } DEVICE_CONFIG_DISCONNECTED = { @@ -65,6 +69,7 @@ DEVICE_CONFIG_DISCONNECTED = { "name": "home", "status": "open", "link_status": "Disconnected", + "serial": "12345", } DEVICE_CONFIG_BAD = { From 55ef33af26b50e957c6eaebb75a1ab317b11a3ea Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 20 Jul 2022 03:15:36 -0400 Subject: [PATCH 2747/3516] Bump pytomorrowio to 0.3.4 (#75478) --- homeassistant/components/tomorrowio/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tomorrowio/manifest.json b/homeassistant/components/tomorrowio/manifest.json index 5447b90d1ce..0823b5ac185 100644 --- a/homeassistant/components/tomorrowio/manifest.json +++ b/homeassistant/components/tomorrowio/manifest.json @@ -3,7 +3,7 @@ "name": "Tomorrow.io", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tomorrowio", - "requirements": ["pytomorrowio==0.3.3"], + "requirements": ["pytomorrowio==0.3.4"], "codeowners": ["@raman325", "@lymanepp"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 0f20e80c459..a13c639eacd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1973,7 +1973,7 @@ pythonegardia==1.0.40 pytile==2022.02.0 # homeassistant.components.tomorrowio -pytomorrowio==0.3.3 +pytomorrowio==0.3.4 # homeassistant.components.touchline pytouchline==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1973add02b8..5197e155d25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1314,7 +1314,7 @@ python_awair==0.2.3 pytile==2022.02.0 # homeassistant.components.tomorrowio -pytomorrowio==0.3.3 +pytomorrowio==0.3.4 # homeassistant.components.traccar pytraccar==0.10.0 From 0a11a623a5de9f2c77aa72661958c19e66b521d2 Mon Sep 17 00:00:00 2001 From: Pascal Winters Date: Wed, 20 Jul 2022 12:57:00 +0200 Subject: [PATCH 2748/3516] Bump pySwitchbot to 0.14.1 (#75487) --- 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 cb485ffd8a5..23fb36a3a41 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.14.0"], + "requirements": ["PySwitchbot==0.14.1"], "config_flow": true, "codeowners": ["@danielhiversen", "@RenierM26"], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index a13c639eacd..cb83971fe6e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.14.0 +PySwitchbot==0.14.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5197e155d25..fcdd3fb2760 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.14.0 +PySwitchbot==0.14.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 787f55e513338c61615fe251f0fc6a9368c85bd8 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 20 Jul 2022 15:51:54 +0200 Subject: [PATCH 2749/3516] Fix Netgear update entity (#75496) --- homeassistant/components/netgear/update.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/netgear/update.py b/homeassistant/components/netgear/update.py index 8d4a9b4912a..e913d488c8e 100644 --- a/homeassistant/components/netgear/update.py +++ b/homeassistant/components/netgear/update.py @@ -59,7 +59,9 @@ class NetgearUpdateEntity(NetgearRouterEntity, UpdateEntity): """Latest version available for install.""" if self.coordinator.data is not None: new_version = self.coordinator.data.get("NewVersion") - if new_version is not None: + if new_version is not None and not new_version.startswith( + self.installed_version + ): return new_version return self.installed_version From e53a072e8a6607e5115d417471fe81b62491e442 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Wed, 20 Jul 2022 19:27:11 +0200 Subject: [PATCH 2750/3516] Fix - Forcast.solar issue on saving settings in options flow without api key (#75504) --- homeassistant/components/forecast_solar/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/forecast_solar/config_flow.py b/homeassistant/components/forecast_solar/config_flow.py index 86de17ef285..600ed363a8b 100644 --- a/homeassistant/components/forecast_solar/config_flow.py +++ b/homeassistant/components/forecast_solar/config_flow.py @@ -99,7 +99,7 @@ class ForecastSolarOptionFlowHandler(OptionsFlow): CONF_API_KEY, description={ "suggested_value": self.config_entry.options.get( - CONF_API_KEY + CONF_API_KEY, "" ) }, ): str, From 4ac7d68552582110f27958fdc2b1b8962c885546 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 20 Jul 2022 12:25:17 -0500 Subject: [PATCH 2751/3516] Fix failure to raise on bad YAML syntax from include files (#75510) Co-authored-by: Franck Nijhof --- homeassistant/util/yaml/loader.py | 4 ++-- tests/util/yaml/fixtures/bad.yaml.txt | 26 ++++++++++++++++++++++++++ tests/util/yaml/test_init.py | 10 ++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 tests/util/yaml/fixtures/bad.yaml.txt diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index e3add3a7c44..09e19af6840 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections import OrderedDict from collections.abc import Iterator import fnmatch -from io import StringIO +from io import StringIO, TextIOWrapper import logging import os from pathlib import Path @@ -169,7 +169,7 @@ def parse_yaml( except yaml.YAMLError: # Loading failed, so we now load with the slow line loader # since the C one will not give us line numbers - if isinstance(content, (StringIO, TextIO)): + if isinstance(content, (StringIO, TextIO, TextIOWrapper)): # Rewind the stream so we can try again content.seek(0, 0) return _parse_yaml_pure_python(content, secrets) diff --git a/tests/util/yaml/fixtures/bad.yaml.txt b/tests/util/yaml/fixtures/bad.yaml.txt new file mode 100644 index 00000000000..8f6a62a6511 --- /dev/null +++ b/tests/util/yaml/fixtures/bad.yaml.txt @@ -0,0 +1,26 @@ +- id: '1658085239190' + alias: Config validation test + description: '' + trigger: + - platform: time + at: 00:02:03 + condition: [] + action: + - service: script.notify_admin + data: + title: 'Here's something that does not work...!' + message: failing + mode: single +- id: '165808523911590' + alias: Config validation test FIXED + description: '' + trigger: + - platform: time + at: 00:02:03 + condition: [] + action: + - service: script.notify_admin + data: + title: 'Here is something that should work...!' + message: fixed? + mode: single diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 11dc40233dc..8d1b7c1adf1 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -2,6 +2,7 @@ import importlib import io import os +import pathlib import unittest from unittest.mock import patch @@ -490,3 +491,12 @@ def test_input(try_both_loaders, try_both_dumpers): def test_c_loader_is_available_in_ci(): """Verify we are testing the C loader in the CI.""" assert yaml.loader.HAS_C_LOADER is True + + +async def test_loading_actual_file_with_syntax(hass, try_both_loaders): + """Test loading a real file with syntax errors.""" + with pytest.raises(HomeAssistantError): + fixture_path = pathlib.Path(__file__).parent.joinpath( + "fixtures", "bad.yaml.txt" + ) + await hass.async_add_executor_job(load_yaml_config_file, fixture_path) From e692d2e28460709f357516e5a8f6c01bb04c851a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 20 Jul 2022 11:29:23 -0600 Subject: [PATCH 2752/3516] Fix incorrect Ambient PWS lightning strike sensor state classes (#75520) --- homeassistant/components/ambient_station/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index c837ef6fdec..a103316f3ff 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -281,14 +281,14 @@ SENSOR_DESCRIPTIONS = ( name="Lightning Strikes Per Day", icon="mdi:lightning-bolt", native_unit_of_measurement="strikes", - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_LIGHTNING_PER_HOUR, name="Lightning Strikes Per Hour", icon="mdi:lightning-bolt", native_unit_of_measurement="strikes", - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_MAXDAILYGUST, From 67fc1ac40a6c57247887b7337ed39a46a1939ad6 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 20 Jul 2022 22:11:05 +0300 Subject: [PATCH 2753/3516] Bump aioshelly to 2.0.1 (#75523) --- 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 a9aade00933..48e48b618e4 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==2.0.0"], + "requirements": ["aioshelly==2.0.1"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index cb83971fe6e..11b785c4fc5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -244,7 +244,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==2.0.0 +aioshelly==2.0.1 # homeassistant.components.skybell aioskybell==22.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fcdd3fb2760..63ef39eb63e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -213,7 +213,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==2.0.0 +aioshelly==2.0.1 # homeassistant.components.skybell aioskybell==22.7.0 From 7402dc824e81ffce80a8b50eb09605b0c5d2aeda Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 20 Jul 2022 21:49:42 +0200 Subject: [PATCH 2754/3516] Bumped version to 2022.7.6 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a2d71ea6d71..08c92cee96e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "5" +PATCH_VERSION: Final = "6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index c68cd921da2..191b337eb65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.5" +version = "2022.7.6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 01c105b89ceacbd6a3dec0613605aa463c8f4692 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 20 Jul 2022 22:34:49 +0200 Subject: [PATCH 2755/3516] Use `DeviceInfo.hw_version` in DenonAVR integration (#75300) --- homeassistant/components/denonavr/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 10c3cb4f6b0..16814b72bc7 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -226,9 +226,10 @@ class DenonDevice(MediaPlayerEntity): assert config_entry.unique_id self._attr_device_info = DeviceInfo( configuration_url=f"http://{config_entry.data[CONF_HOST]}/", + hw_version=config_entry.data[CONF_TYPE], identifiers={(DOMAIN, config_entry.unique_id)}, manufacturer=config_entry.data[CONF_MANUFACTURER], - model=f"{config_entry.data[CONF_MODEL]}-{config_entry.data[CONF_TYPE]}", + model=config_entry.data[CONF_MODEL], name=config_entry.title, ) self._attr_sound_mode_list = receiver.sound_mode_list From 6da25c733e014624564140a761af5201e3bf0adc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 20 Jul 2022 15:54:37 -0500 Subject: [PATCH 2756/3516] Add coordinator and entity for passive bluetooth devices (#75468) Co-authored-by: Martin Hjelmare --- .../bluetooth/passive_update_coordinator.py | 341 +++++++ .../test_passive_update_coordinator.py | 868 ++++++++++++++++++ 2 files changed, 1209 insertions(+) create mode 100644 homeassistant/components/bluetooth/passive_update_coordinator.py create mode 100644 tests/components/bluetooth/test_passive_update_coordinator.py diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py new file mode 100644 index 00000000000..df4233452df --- /dev/null +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -0,0 +1,341 @@ +"""The Bluetooth integration.""" +from __future__ import annotations + +from collections.abc import Callable +import dataclasses +from datetime import datetime +import logging +import time +from typing import Any, Generic, TypeVar + +from home_assistant_bluetooth import BluetoothServiceInfo + +from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_call_later + +from . import ( + BluetoothCallbackMatcher, + BluetoothChange, + async_address_present, + async_register_callback, +) +from .const import DOMAIN + +UNAVAILABLE_SECONDS = 60 * 5 +NEVER_TIME = -UNAVAILABLE_SECONDS + + +@dataclasses.dataclass(frozen=True) +class PassiveBluetoothEntityKey: + """Key for a passive bluetooth entity. + + Example: + key: temperature + device_id: outdoor_sensor_1 + """ + + key: str + device_id: str | None + + +_T = TypeVar("_T") + + +@dataclasses.dataclass(frozen=True) +class PassiveBluetoothDataUpdate(Generic[_T]): + """Generic bluetooth data.""" + + devices: dict[str | None, DeviceInfo] = dataclasses.field(default_factory=dict) + entity_descriptions: dict[ + PassiveBluetoothEntityKey, EntityDescription + ] = dataclasses.field(default_factory=dict) + entity_data: dict[PassiveBluetoothEntityKey, _T] = dataclasses.field( + default_factory=dict + ) + + +_PassiveBluetoothDataUpdateCoordinatorT = TypeVar( + "_PassiveBluetoothDataUpdateCoordinatorT", + bound="PassiveBluetoothDataUpdateCoordinator[Any]", +) + + +class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): + """Passive bluetooth data update coordinator for bluetooth advertisements. + + The coordinator is responsible for keeping track of the bluetooth data, + updating subscribers, and device availability. + + The update_method must return a PassiveBluetoothDataUpdate object. Callers + are responsible for formatting the data returned from their parser into + the appropriate format. + + The coordinator will call the update_method every time the bluetooth device + receives a new advertisement with the following signature: + + update_method(service_info: BluetoothServiceInfo) -> PassiveBluetoothDataUpdate + + As the size of each advertisement is limited, the update_method should + return a PassiveBluetoothDataUpdate object that contains only data that + should be updated. The coordinator will then dispatch subscribers based + on the data in the PassiveBluetoothDataUpdate object. The accumulated data + is available in the devices, entity_data, and entity_descriptions attributes. + """ + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + address: str, + update_method: Callable[[BluetoothServiceInfo], PassiveBluetoothDataUpdate[_T]], + ) -> None: + """Initialize the coordinator.""" + self.hass = hass + self.logger = logger + self.name: str | None = None + self.address = address + self._listeners: list[ + Callable[[PassiveBluetoothDataUpdate[_T] | None], None] + ] = [] + self._entity_key_listeners: dict[ + PassiveBluetoothEntityKey, + list[Callable[[PassiveBluetoothDataUpdate[_T] | None], None]], + ] = {} + self.update_method = update_method + + self.entity_data: dict[PassiveBluetoothEntityKey, _T] = {} + self.entity_descriptions: dict[ + PassiveBluetoothEntityKey, EntityDescription + ] = {} + self.devices: dict[str | None, DeviceInfo] = {} + + self.last_update_success = True + self._last_callback_time: float = NEVER_TIME + self._cancel_track_available: CALLBACK_TYPE | None = None + self._present = False + + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._present and self.last_update_success + + @callback + def _async_cancel_available_tracker(self) -> None: + """Reset the available tracker.""" + if self._cancel_track_available: + self._cancel_track_available() + self._cancel_track_available = None + + @callback + def _async_schedule_available_tracker(self, time_remaining: float) -> None: + """Schedule the available tracker.""" + self._cancel_track_available = async_call_later( + self.hass, time_remaining, self._async_check_device_present + ) + + @callback + def _async_check_device_present(self, _: datetime) -> None: + """Check if the device is present.""" + time_passed_since_seen = time.monotonic() - self._last_callback_time + self._async_cancel_available_tracker() + if ( + not self._present + or time_passed_since_seen < UNAVAILABLE_SECONDS + or async_address_present(self.hass, self.address) + ): + self._async_schedule_available_tracker( + UNAVAILABLE_SECONDS - time_passed_since_seen + ) + return + self._present = False + self.async_update_listeners(None) + + @callback + def async_setup(self) -> CALLBACK_TYPE: + """Start the callback.""" + return async_register_callback( + self.hass, + self._async_handle_bluetooth_event, + BluetoothCallbackMatcher(address=self.address), + ) + + @callback + def async_add_entities_listener( + self, + entity_class: type[PassiveBluetoothCoordinatorEntity], + async_add_entites: AddEntitiesCallback, + ) -> Callable[[], None]: + """Add a listener for new entities.""" + created: set[PassiveBluetoothEntityKey] = set() + + @callback + def _async_add_or_update_entities( + data: PassiveBluetoothDataUpdate[_T] | None, + ) -> None: + """Listen for new entities.""" + if data is None: + return + entities: list[PassiveBluetoothCoordinatorEntity] = [] + for entity_key, description in data.entity_descriptions.items(): + if entity_key not in created: + entities.append(entity_class(self, entity_key, description)) + created.add(entity_key) + if entities: + async_add_entites(entities) + + return self.async_add_listener(_async_add_or_update_entities) + + @callback + def async_add_listener( + self, + update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None], + ) -> Callable[[], None]: + """Listen for all updates.""" + + @callback + def remove_listener() -> None: + """Remove update listener.""" + self._listeners.remove(update_callback) + + self._listeners.append(update_callback) + return remove_listener + + @callback + def async_add_entity_key_listener( + self, + update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None], + entity_key: PassiveBluetoothEntityKey, + ) -> Callable[[], None]: + """Listen for updates by device key.""" + + @callback + def remove_listener() -> None: + """Remove update listener.""" + self._entity_key_listeners[entity_key].remove(update_callback) + if not self._entity_key_listeners[entity_key]: + del self._entity_key_listeners[entity_key] + + self._entity_key_listeners.setdefault(entity_key, []).append(update_callback) + return remove_listener + + @callback + def async_update_listeners( + self, data: PassiveBluetoothDataUpdate[_T] | None + ) -> None: + """Update all registered listeners.""" + # Dispatch to listeners without a filter key + for update_callback in self._listeners: + update_callback(data) + + # Dispatch to listeners with a filter key + for listeners in self._entity_key_listeners.values(): + for update_callback in listeners: + update_callback(data) + + @callback + def _async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfo, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + self.name = service_info.name + self._last_callback_time = time.monotonic() + self._present = True + if not self._cancel_track_available: + self._async_schedule_available_tracker(UNAVAILABLE_SECONDS) + if self.hass.is_stopping: + return + + try: + new_data = self.update_method(service_info) + except Exception as err: # pylint: disable=broad-except + self.last_update_success = False + self.logger.exception( + "Unexpected error updating %s data: %s", self.name, err + ) + return + + if not isinstance(new_data, PassiveBluetoothDataUpdate): + self.last_update_success = False # type: ignore[unreachable] + self.logger.error( + "The update_method for %s returned %s instead of a PassiveBluetoothDataUpdate", + self.name, + new_data, + ) + return + + if not self.last_update_success: + self.last_update_success = True + self.logger.info("Processing %s data recovered", self.name) + + self.devices.update(new_data.devices) + self.entity_descriptions.update(new_data.entity_descriptions) + self.entity_data.update(new_data.entity_data) + self.async_update_listeners(new_data) + + +class PassiveBluetoothCoordinatorEntity( + Entity, Generic[_PassiveBluetoothDataUpdateCoordinatorT] +): + """A class for entities using PassiveBluetoothDataUpdateCoordinator.""" + + _attr_has_entity_name = True + _attr_should_poll = False + + def __init__( + self, + coordinator: _PassiveBluetoothDataUpdateCoordinatorT, + entity_key: PassiveBluetoothEntityKey, + description: EntityDescription, + context: Any = None, + ) -> None: + """Create the entity with a PassiveBluetoothDataUpdateCoordinator.""" + self.entity_description = description + self.entity_key = entity_key + self.coordinator = coordinator + self.coordinator_context = context + address = coordinator.address + device_id = entity_key.device_id + devices = coordinator.devices + key = entity_key.key + if device_id in devices: + base_device_info = devices[device_id] + else: + base_device_info = DeviceInfo({}) + if device_id: + self._attr_device_info = base_device_info | DeviceInfo( + {ATTR_IDENTIFIERS: {(DOMAIN, f"{address}-{device_id}")}} + ) + self._attr_unique_id = f"{address}-{key}-{device_id}" + else: + self._attr_device_info = base_device_info | DeviceInfo( + {ATTR_IDENTIFIERS: {(DOMAIN, address)}} + ) + self._attr_unique_id = f"{address}-{key}" + if ATTR_NAME not in self._attr_device_info: + self._attr_device_info[ATTR_NAME] = self.coordinator.name + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.coordinator.available + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + self.coordinator.async_add_entity_key_listener( + self._handle_coordinator_update, self.entity_key + ) + ) + + @callback + def _handle_coordinator_update( + self, new_data: PassiveBluetoothDataUpdate | None + ) -> None: + """Handle updated data from the coordinator.""" + self.async_write_ha_state() diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py new file mode 100644 index 00000000000..ca4ef6e909c --- /dev/null +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -0,0 +1,868 @@ +"""Tests for the Bluetooth integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +import time +from unittest.mock import MagicMock, patch + +from home_assistant_bluetooth import BluetoothServiceInfo + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.bluetooth.passive_update_coordinator import ( + UNAVAILABLE_SECONDS, + PassiveBluetoothCoordinatorEntity, + PassiveBluetoothDataUpdate, + PassiveBluetoothDataUpdateCoordinator, + PassiveBluetoothEntityKey, +) +from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription +from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import CoreState, callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.util import dt as dt_util + +from tests.common import MockEntityPlatform, async_fire_time_changed + +_LOGGER = logging.getLogger(__name__) + + +GENERIC_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( + name="Generic", + address="aa:bb:cc:dd:ee:ff", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x01\x01\x01\x01\x01", + }, + service_data={}, + service_uuids=[], + source="local", +) +GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + None: DeviceInfo( + name="Test Device", model="Test Model", manufacturer="Test Manufacturer" + ), + }, + entity_data={ + PassiveBluetoothEntityKey("temperature", None): 14.5, + PassiveBluetoothEntityKey("pressure", None): 1234, + }, + entity_descriptions={ + PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( + key="temperature", + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ), + PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( + key="pressure", + name="Pressure", + native_unit_of_measurement="hPa", + device_class=SensorDeviceClass.PRESSURE, + ), + }, +) + + +async def test_basic_usage(hass): + """Test basic usage of the PassiveBluetoothDataUpdateCoordinator.""" + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothDataUpdateCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel_coordinator = coordinator.async_setup() + + entity_key = PassiveBluetoothEntityKey("temperature", None) + entity_key_events = [] + all_events = [] + mock_entity = MagicMock() + mock_add_entities = MagicMock() + + def _async_entity_key_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock entity key listener.""" + entity_key_events.append(data) + + cancel_async_add_entity_key_listener = coordinator.async_add_entity_key_listener( + _async_entity_key_listener, + entity_key, + ) + + def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock an all listener.""" + all_events.append(data) + + cancel_listener = coordinator.async_add_listener( + _all_listener, + ) + + cancel_async_add_entities_listener = coordinator.async_add_entities_listener( + mock_entity, + mock_add_entities, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + # Each listener should receive the same data + # since both match + assert len(entity_key_events) == 1 + assert len(all_events) == 1 + + # There should be 4 calls to create entities + assert len(mock_entity.mock_calls) == 2 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + # Each listener should receive the same data + # since both match + assert len(entity_key_events) == 2 + assert len(all_events) == 2 + + # On the second, the entities should already be created + # so the mock should not be called again + assert len(mock_entity.mock_calls) == 2 + + cancel_async_add_entity_key_listener() + cancel_listener() + cancel_async_add_entities_listener() + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + # Each listener should not trigger any more now + # that they were cancelled + assert len(entity_key_events) == 2 + assert len(all_events) == 2 + assert len(mock_entity.mock_calls) == 2 + assert coordinator.available is True + + cancel_coordinator() + + +async def test_unavailable_after_no_data(hass): + """Test that the coordinator is unavailable after no data for a while.""" + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothDataUpdateCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel_coordinator = coordinator.async_setup() + + mock_entity = MagicMock() + mock_add_entities = MagicMock() + coordinator.async_add_entities_listener( + mock_entity, + mock_add_entities, + ) + + assert coordinator.available is False + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(mock_add_entities.mock_calls) == 1 + assert coordinator.available is True + + monotonic_now = time.monotonic() + now = dt_util.utcnow() + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.time.monotonic", + return_value=monotonic_now + UNAVAILABLE_SECONDS, + ): + async_fire_time_changed(hass, now + timedelta(seconds=UNAVAILABLE_SECONDS)) + await hass.async_block_till_done() + assert coordinator.available is False + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert coordinator.available is True + + # Now simulate the device is still present even though we got + # no data for a while + + monotonic_now = time.monotonic() + now = dt_util.utcnow() + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_address_present", + return_value=True, + ), patch( + "homeassistant.components.bluetooth.passive_update_coordinator.time.monotonic", + return_value=monotonic_now + UNAVAILABLE_SECONDS, + ): + async_fire_time_changed(hass, now + timedelta(seconds=UNAVAILABLE_SECONDS)) + await hass.async_block_till_done() + + assert coordinator.available is True + + # And finally that it can go unavailable again when its gone + monotonic_now = time.monotonic() + now = dt_util.utcnow() + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.time.monotonic", + return_value=monotonic_now + UNAVAILABLE_SECONDS, + ): + async_fire_time_changed(hass, now + timedelta(seconds=UNAVAILABLE_SECONDS)) + await hass.async_block_till_done() + assert coordinator.available is False + + cancel_coordinator() + + +async def test_no_updates_once_stopping(hass): + """Test updates are ignored once hass is stopping.""" + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothDataUpdateCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel_coordinator = coordinator.async_setup() + + all_events = [] + + def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock an all listener.""" + all_events.append(data) + + coordinator.async_add_listener( + _all_listener, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(all_events) == 1 + + hass.state = CoreState.stopping + + # We should stop processing events once hass is stopping + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(all_events) == 1 + + cancel_coordinator() + + +async def test_exception_from_update_method(hass, caplog): + """Test we handle exceptions from the update method.""" + run_count = 0 + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + nonlocal run_count + run_count += 1 + if run_count == 2: + raise Exception("Test exception") + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothDataUpdateCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel_coordinator = coordinator.async_setup() + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert coordinator.available is True + + # We should go unavailable once we get an exception + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert "Test exception" in caplog.text + assert coordinator.available is False + + # We should go available again once we get data again + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert coordinator.available is True + + cancel_coordinator() + + +async def test_bad_data_from_update_method(hass, caplog): + """Test we handle bad data from the update method.""" + run_count = 0 + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + nonlocal run_count + run_count += 1 + if run_count == 2: + return "bad_data" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothDataUpdateCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel_coordinator = coordinator.async_setup() + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert coordinator.available is True + + # We should go unavailable once we get bad data + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert "update_method" in caplog.text + assert "bad_data" in caplog.text + assert coordinator.available is False + + # We should go available again once we get good data again + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert coordinator.available is True + + cancel_coordinator() + + +GOVEE_B5178_REMOTE_SERVICE_INFO = BluetoothServiceInfo( + name="B5178D6FB", + address="749A17CB-F7A9-D466-C29F-AABE601938A0", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x04\xb5\xa2d\x00\x06L\x00\x02\x15INTELLI_ROCKS_HWPu\xf2\xff\xc2" + }, + service_data={}, + service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], + source="local", +) +GOVEE_B5178_PRIMARY_SERVICE_INFO = BluetoothServiceInfo( + name="B5178D6FB", + address="749A17CB-F7A9-D466-C29F-AABE601938A0", + rssi=-92, + manufacturer_data={ + 1: b"\x01\x01\x00\x03\x07Xd\x00\x00L\x00\x02\x15INTELLI_ROCKS_HWPu\xf2\xff\xc2" + }, + service_data={}, + service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], + source="local", +) + +GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + "remote": { + "name": "B5178D6FB Remote", + "manufacturer": "Govee", + "model": "H5178-REMOTE", + }, + }, + entity_descriptions={ + PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ): SensorEntityDescription( + key="temperature_remote", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Temperature", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="°C", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="humidity", device_id="remote" + ): SensorEntityDescription( + key="humidity_remote", + device_class=SensorDeviceClass.HUMIDITY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Humidity", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="battery", device_id="remote" + ): SensorEntityDescription( + key="battery_remote", + device_class=SensorDeviceClass.BATTERY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Battery", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): SensorEntityDescription( + key="signal_strength_remote", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=None, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Signal Strength", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="dBm", + state_class=None, + ), + }, + entity_data={ + PassiveBluetoothEntityKey(key="temperature", device_id="remote"): 30.8642, + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): 64.2, + PassiveBluetoothEntityKey(key="battery", device_id="remote"): 100, + PassiveBluetoothEntityKey(key="signal_strength", device_id="remote"): -95, + }, +) +GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( + PassiveBluetoothDataUpdate( + devices={ + "remote": { + "name": "B5178D6FB Remote", + "manufacturer": "Govee", + "model": "H5178-REMOTE", + }, + "primary": { + "name": "B5178D6FB Primary", + "manufacturer": "Govee", + "model": "H5178", + }, + }, + entity_descriptions={ + PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ): SensorEntityDescription( + key="temperature_remote", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Temperature", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="°C", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="humidity", device_id="remote" + ): SensorEntityDescription( + key="humidity_remote", + device_class=SensorDeviceClass.HUMIDITY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Humidity", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="battery", device_id="remote" + ): SensorEntityDescription( + key="battery_remote", + device_class=SensorDeviceClass.BATTERY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Battery", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): SensorEntityDescription( + key="signal_strength_remote", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=None, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Signal Strength", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="dBm", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="temperature", device_id="primary" + ): SensorEntityDescription( + key="temperature_primary", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Temperature", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="°C", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="humidity", device_id="primary" + ): SensorEntityDescription( + key="humidity_primary", + device_class=SensorDeviceClass.HUMIDITY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Humidity", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="battery", device_id="primary" + ): SensorEntityDescription( + key="battery_primary", + device_class=SensorDeviceClass.BATTERY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Battery", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="signal_strength", device_id="primary" + ): SensorEntityDescription( + key="signal_strength_primary", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=None, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + name="Signal Strength", + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="dBm", + state_class=None, + ), + }, + entity_data={ + PassiveBluetoothEntityKey(key="temperature", device_id="remote"): 30.8642, + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): 64.2, + PassiveBluetoothEntityKey(key="battery", device_id="remote"): 100, + PassiveBluetoothEntityKey(key="signal_strength", device_id="remote"): -92, + PassiveBluetoothEntityKey(key="temperature", device_id="primary"): 19.8488, + PassiveBluetoothEntityKey(key="humidity", device_id="primary"): 48.8, + PassiveBluetoothEntityKey(key="battery", device_id="primary"): 100, + PassiveBluetoothEntityKey(key="signal_strength", device_id="primary"): -92, + }, + ) +) + + +async def test_integration_with_entity(hass): + """Test integration of PassiveBluetoothDataUpdateCoordinator with PassiveBluetoothCoordinatorEntity.""" + + update_count = 0 + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + nonlocal update_count + update_count += 1 + if update_count > 2: + return GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE + return GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothDataUpdateCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_setup() + + mock_add_entities = MagicMock() + + coordinator.async_add_entities_listener( + PassiveBluetoothCoordinatorEntity, + mock_add_entities, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # First call with just the remote sensor entities results in them being added + assert len(mock_add_entities.mock_calls) == 1 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Second call with just the remote sensor entities does not add them again + assert len(mock_add_entities.mock_calls) == 1 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Third call with primary and remote sensor entities adds the primary sensor entities + assert len(mock_add_entities.mock_calls) == 2 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Forth call with both primary and remote sensor entities does not add them again + assert len(mock_add_entities.mock_calls) == 2 + + entities = [ + *mock_add_entities.mock_calls[0][1][0], + *mock_add_entities.mock_calls[1][1][0], + ] + + entity_one: PassiveBluetoothCoordinatorEntity = entities[0] + entity_one.hass = hass + assert entity_one.available is True + assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature-remote" + assert entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff-remote")}, + "manufacturer": "Govee", + "model": "H5178-REMOTE", + "name": "B5178D6FB Remote", + } + assert entity_one.entity_key == PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ) + + +NO_DEVICES_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( + name="Generic", + address="aa:bb:cc:dd:ee:ff", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x01\x01\x01\x01\x01", + }, + service_data={}, + service_uuids=[], + source="local", +) +NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={}, + entity_data={ + PassiveBluetoothEntityKey("temperature", None): 14.5, + PassiveBluetoothEntityKey("pressure", None): 1234, + }, + entity_descriptions={ + PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( + key="temperature", + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ), + PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( + key="pressure", + name="Pressure", + native_unit_of_measurement="hPa", + device_class=SensorDeviceClass.PRESSURE, + ), + }, +) + + +async def test_integration_with_entity_without_a_device(hass): + """Test integration with PassiveBluetoothCoordinatorEntity with no device.""" + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothDataUpdateCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_setup() + + mock_add_entities = MagicMock() + + coordinator.async_add_entities_listener( + PassiveBluetoothCoordinatorEntity, + mock_add_entities, + ) + + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # First call with just the remote sensor entities results in them being added + assert len(mock_add_entities.mock_calls) == 1 + + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Second call with just the remote sensor entities does not add them again + assert len(mock_add_entities.mock_calls) == 1 + + entities = mock_add_entities.mock_calls[0][1][0] + entity_one: PassiveBluetoothCoordinatorEntity = entities[0] + entity_one.hass = hass + assert entity_one.available is True + assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature" + assert entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "name": "Generic", + } + assert entity_one.entity_key == PassiveBluetoothEntityKey( + key="temperature", device_id=None + ) + + +async def test_passive_bluetooth_entity_with_entity_platform(hass): + """Test with a mock entity platform.""" + entity_platform = MockEntityPlatform(hass) + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothDataUpdateCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_setup() + + coordinator.async_add_entities_listener( + PassiveBluetoothCoordinatorEntity, + lambda entities: hass.async_create_task( + entity_platform.async_add_entities(entities) + ), + ) + + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert hass.states.get("test_domain.temperature") is not None + assert hass.states.get("test_domain.pressure") is not None From ac858cc2b555df8e989c5ca2079adce6fec3ecb6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 21 Jul 2022 00:19:02 +0200 Subject: [PATCH 2757/3516] Improve singleton helper typing (#75461) * Improve singleton helper typing * Fix type errors --- .strict-typing | 1 + homeassistant/helpers/restore_state.py | 4 +--- homeassistant/helpers/singleton.py | 14 +++++++------- mypy.ini | 3 +++ 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.strict-typing b/.strict-typing index 7616589a7b5..e64a9a04931 100644 --- a/.strict-typing +++ b/.strict-typing @@ -25,6 +25,7 @@ homeassistant.helpers.entity_values homeassistant.helpers.event homeassistant.helpers.reload homeassistant.helpers.script_variables +homeassistant.helpers.singleton homeassistant.helpers.sun homeassistant.helpers.translation homeassistant.util.async_ diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 314daecde48..73a00898d69 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -305,9 +305,7 @@ class RestoreEntity(Entity): # Return None if this entity isn't added to hass yet _LOGGER.warning("Cannot get last state. Entity not added to hass") # type: ignore[unreachable] return None - data = cast( - RestoreStateData, await RestoreStateData.async_get_instance(self.hass) - ) + data = await RestoreStateData.async_get_instance(self.hass) if self.entity_id not in data.last_states: return None return data.last_states[self.entity_id] diff --git a/homeassistant/helpers/singleton.py b/homeassistant/helpers/singleton.py index a4f6d32c303..f15806ae5ff 100644 --- a/homeassistant/helpers/singleton.py +++ b/homeassistant/helpers/singleton.py @@ -4,23 +4,23 @@ from __future__ import annotations import asyncio from collections.abc import Callable import functools -from typing import TypeVar, cast +from typing import Any, TypeVar, cast from homeassistant.core import HomeAssistant from homeassistant.loader import bind_hass _T = TypeVar("_T") -FUNC = Callable[[HomeAssistant], _T] +_FuncType = Callable[[HomeAssistant], _T] -def singleton(data_key: str) -> Callable[[FUNC], FUNC]: +def singleton(data_key: str) -> Callable[[_FuncType[_T]], _FuncType[_T]]: """Decorate a function that should be called once per instance. Result will be cached and simultaneous calls will be handled. """ - def wrapper(func: FUNC) -> FUNC: + def wrapper(func: _FuncType[_T]) -> _FuncType[_T]: """Wrap a function with caching logic.""" if not asyncio.iscoroutinefunction(func): @@ -35,10 +35,10 @@ def singleton(data_key: str) -> Callable[[FUNC], FUNC]: @bind_hass @functools.wraps(func) - async def async_wrapped(hass: HomeAssistant) -> _T: + async def async_wrapped(hass: HomeAssistant) -> Any: if data_key not in hass.data: evt = hass.data[data_key] = asyncio.Event() - result = await func(hass) + result = await func(hass) # type: ignore[misc] hass.data[data_key] = result evt.set() return cast(_T, result) @@ -51,6 +51,6 @@ def singleton(data_key: str) -> Callable[[FUNC], FUNC]: return cast(_T, obj_or_evt) - return async_wrapped + return async_wrapped # type: ignore[return-value] return wrapper diff --git a/mypy.ini b/mypy.ini index 84bc6ae00b0..ee953f13d74 100644 --- a/mypy.ini +++ b/mypy.ini @@ -87,6 +87,9 @@ disallow_any_generics = true [mypy-homeassistant.helpers.script_variables] disallow_any_generics = true +[mypy-homeassistant.helpers.singleton] +disallow_any_generics = true + [mypy-homeassistant.helpers.sun] disallow_any_generics = true From ca1f0909fbe26f9e748a0afff37d61f88e8b156d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 21 Jul 2022 02:13:10 +0200 Subject: [PATCH 2758/3516] Fix spelling in recorder integration (#75539) --- homeassistant/components/recorder/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 9585804690a..49e870b658f 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -295,9 +295,9 @@ class Recorder(threading.Thread): @callback def _async_check_queue(self, *_: Any) -> None: - """Periodic check of the queue size to ensure we do not exaust memory. + """Periodic check of the queue size to ensure we do not exhaust memory. - The queue grows during migraton or if something really goes wrong. + The queue grows during migration or if something really goes wrong. """ size = self.backlog _LOGGER.debug("Recorder queue size is: %s", size) From 87797c8b66787f4ede69a440f13047bd007876c3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 21 Jul 2022 00:26:18 +0000 Subject: [PATCH 2759/3516] [ci skip] Translation update --- .../ambiclimate/translations/hu.json | 2 +- .../components/apple_tv/translations/hu.json | 14 ++-- .../components/arcam_fmj/translations/hu.json | 2 +- .../components/asuswrt/translations/hu.json | 2 +- .../components/august/translations/hu.json | 2 +- .../azure_devops/translations/hu.json | 2 +- .../azure_event_hub/translations/hu.json | 8 +- .../components/bond/translations/hu.json | 2 +- .../components/bosch_shc/translations/hu.json | 4 +- .../components/coinbase/translations/hu.json | 2 +- .../components/control4/translations/hu.json | 2 +- .../crownstone/translations/hu.json | 2 +- .../components/demo/translations/ca.json | 21 ++++++ .../components/demo/translations/en.json | 3 - .../components/demo/translations/fr.json | 21 ++++++ .../components/demo/translations/pt-BR.json | 21 ++++++ .../components/demo/translations/zh-Hant.json | 21 ++++++ .../components/denonavr/translations/hu.json | 4 +- .../devolo_home_control/translations/hu.json | 2 +- .../components/ecobee/translations/hu.json | 2 +- .../eight_sleep/translations/ru.json | 19 +++++ .../components/esphome/translations/hu.json | 6 +- .../components/ezviz/translations/hu.json | 2 +- .../components/fivem/translations/hu.json | 2 +- .../forecast_solar/translations/hu.json | 4 +- .../forked_daapd/translations/hu.json | 4 +- .../freedompro/translations/hu.json | 2 +- .../components/generic/translations/ru.json | 2 + .../components/goalzero/translations/hu.json | 2 +- .../components/google/translations/hu.json | 5 +- .../components/google/translations/ru.json | 3 + .../google/translations/zh-Hant.json | 3 +- .../components/hassio/translations/ru.json | 1 + .../components/heos/translations/hu.json | 2 +- .../here_travel_time/translations/it.json | 3 + .../homekit_controller/translations/hu.json | 2 +- .../homekit_controller/translations/it.json | 2 +- .../components/honeywell/translations/hu.json | 2 +- .../huawei_lte/translations/hu.json | 2 +- .../components/hyperion/translations/hu.json | 2 +- .../components/iaqualink/translations/hu.json | 2 +- .../components/icloud/translations/hu.json | 2 +- .../intellifire/translations/hu.json | 2 +- .../components/iotawatt/translations/hu.json | 2 +- .../components/ipp/translations/hu.json | 2 +- .../components/knx/translations/hu.json | 4 +- .../components/konnected/translations/hu.json | 4 +- .../components/laundrify/translations/hu.json | 4 +- .../components/lifx/translations/hu.json | 20 +++++ .../components/lifx/translations/it.json | 12 +++ .../components/lifx/translations/zh-Hant.json | 20 +++++ .../logi_circle/translations/hu.json | 2 +- .../components/mazda/translations/hu.json | 2 +- .../components/meater/translations/hu.json | 2 +- .../minecraft_server/translations/hu.json | 6 +- .../components/mysensors/translations/hu.json | 2 +- .../components/nest/translations/ru.json | 14 ++++ .../components/netgear/translations/hu.json | 2 +- .../nfandroidtv/translations/hu.json | 2 +- .../ovo_energy/translations/hu.json | 2 +- .../components/plaato/translations/hu.json | 2 +- .../components/plugwise/translations/ca.json | 3 +- .../components/plugwise/translations/en.json | 3 +- .../components/plugwise/translations/et.json | 3 +- .../components/plugwise/translations/fr.json | 3 +- .../components/plugwise/translations/hu.json | 2 +- .../plugwise/translations/pt-BR.json | 3 +- .../components/plugwise/translations/ru.json | 6 +- .../plugwise/translations/zh-Hant.json | 3 +- .../components/powerwall/translations/hu.json | 2 +- .../radiotherm/translations/ru.json | 31 ++++++++ .../components/renault/translations/hu.json | 2 +- .../components/rhasspy/translations/it.json | 9 +++ .../components/roomba/translations/hu.json | 2 +- .../components/roon/translations/hu.json | 2 +- .../components/samsungtv/translations/hu.json | 6 +- .../components/scrape/translations/ru.json | 73 +++++++++++++++++++ .../sensibo/translations/sensor.ru.json | 8 ++ .../components/shelly/translations/hu.json | 2 +- .../components/sia/translations/hu.json | 4 +- .../components/skybell/translations/ru.json | 21 ++++++ .../components/smappee/translations/hu.json | 2 +- .../smartthings/translations/hu.json | 8 +- .../components/soma/translations/hu.json | 4 +- .../components/spotify/translations/hu.json | 2 +- .../steam_online/translations/hu.json | 2 +- .../steam_online/translations/ru.json | 3 + .../components/subaru/translations/hu.json | 8 +- .../system_bridge/translations/hu.json | 2 +- .../tankerkoenig/translations/ru.json | 11 ++- .../tomorrowio/translations/hu.json | 2 +- .../totalconnect/translations/ru.json | 11 +++ .../components/tractive/translations/hu.json | 2 +- .../unifiprotect/translations/it.json | 2 +- .../uptimerobot/translations/hu.json | 2 +- .../components/verisure/translations/hu.json | 17 ++++- .../components/verisure/translations/it.json | 11 +++ .../vlc_telnet/translations/hu.json | 2 +- .../components/vulcan/translations/hu.json | 6 +- .../components/webostv/translations/hu.json | 2 +- .../components/wiz/translations/hu.json | 2 +- .../xiaomi_miio/translations/hu.json | 4 +- .../components/zwave_js/translations/hu.json | 2 +- 103 files changed, 494 insertions(+), 120 deletions(-) create mode 100644 homeassistant/components/eight_sleep/translations/ru.json create mode 100644 homeassistant/components/radiotherm/translations/ru.json create mode 100644 homeassistant/components/rhasspy/translations/it.json create mode 100644 homeassistant/components/scrape/translations/ru.json create mode 100644 homeassistant/components/sensibo/translations/sensor.ru.json create mode 100644 homeassistant/components/skybell/translations/ru.json diff --git a/homeassistant/components/ambiclimate/translations/hu.json b/homeassistant/components/ambiclimate/translations/hu.json index 3de93421f42..92ea617393e 100644 --- a/homeassistant/components/ambiclimate/translations/hu.json +++ b/homeassistant/components/ambiclimate/translations/hu.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "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 **Mehet** gombot.\n(Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a megadott visszah\u00edv\u00e1si URL {cb_url})", + "description": "K\u00e9rem, k\u00f6vesse ezt a [link]({authorization_url}}) \u00e9s **Enged\u00e9lyezze** a hozz\u00e1f\u00e9r\u00e9st Ambiclimate -fi\u00f3kj\u00e1hoz, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi **Mehet** gombot.\n(Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a megadott visszah\u00edv\u00e1si URL {cb_url})", "title": "Ambiclimate hiteles\u00edt\u00e9se" } } diff --git a/homeassistant/components/apple_tv/translations/hu.json b/homeassistant/components/apple_tv/translations/hu.json index 9105c545758..ae1e55bf3ba 100644 --- a/homeassistant/components/apple_tv/translations/hu.json +++ b/homeassistant/components/apple_tv/translations/hu.json @@ -5,8 +5,8 @@ "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "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.", - "device_not_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a felder\u00edt\u00e9s sor\u00e1n, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni.", - "inconsistent_device": "Az elv\u00e1rt protokollok nem tal\u00e1lhat\u00f3k a felder\u00edt\u00e9s sor\u00e1n. Ez \u00e1ltal\u00e1ban a multicast DNS (Zeroconf) probl\u00e9m\u00e1j\u00e1t jelzi. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni az eszk\u00f6zt.", + "device_not_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a felder\u00edt\u00e9s sor\u00e1n, k\u00e9rem, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni.", + "inconsistent_device": "Az elv\u00e1rt protokollok nem tal\u00e1lhat\u00f3k a felder\u00edt\u00e9s sor\u00e1n. Ez \u00e1ltal\u00e1ban a multicast DNS (Zeroconf) probl\u00e9m\u00e1j\u00e1t jelzi. K\u00e9rem, pr\u00f3b\u00e1lja meg \u00fajra hozz\u00e1adni az eszk\u00f6zt.", "ipv6_not_supported": "Az IPv6 nem t\u00e1mogatott.", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", @@ -22,26 +22,26 @@ "flow_title": "{name} ({type})", "step": { "confirm": { - "description": "\u00d6n a `{name}`, `{type}` t\u00edpus\u00fa eszk\u00f6zt k\u00e9sz\u00fcl hozz\u00e1adni a Home Assistanthoz.\n\n**A folyamat befejez\u00e9s\u00e9hez t\u00f6bb PIN k\u00f3dot is meg kell adnia.**\n\nK\u00e9rj\u00fck, vegye figyelembe, hogy ezzel az integr\u00e1ci\u00f3val *nem* tudja kikapcsolni az Apple TV k\u00e9sz\u00fcl\u00e9k\u00e9t. Csak a Home Assistant m\u00e9dialej\u00e1tsz\u00f3ja fog kikapcsolni!", + "description": "\u00d6n a `{name}`, `{type}` t\u00edpus\u00fa eszk\u00f6zt k\u00e9sz\u00fcl hozz\u00e1adni a Home Assistanthoz.\n\n**A folyamat befejez\u00e9s\u00e9hez t\u00f6bb PIN k\u00f3dot is meg kell adnia.**\n\nK\u00e9rem, vegye figyelembe, hogy ezzel az integr\u00e1ci\u00f3val *nem* tudja kikapcsolni az Apple TV k\u00e9sz\u00fcl\u00e9k\u00e9t. Csak a Home Assistant m\u00e9dialej\u00e1tsz\u00f3ja fog kikapcsolni!", "title": "Apple TV sikeresen hozz\u00e1adva" }, "pair_no_pin": { - "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a {protocol} szolg\u00e1ltat\u00e1shoz. A folytat\u00e1shoz k\u00e9rj\u00fck, \u00edrja be k\u00e9sz\u00fcl\u00e9ken a PIN k\u00f3dot: {pin}.", + "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a {protocol} szolg\u00e1ltat\u00e1shoz. A folytat\u00e1shoz k\u00e9rem, \u00edrja be k\u00e9sz\u00fcl\u00e9ken a PIN k\u00f3dot: {pin}.", "title": "P\u00e1ros\u00edt\u00e1s" }, "pair_with_pin": { "data": { "pin": "PIN-k\u00f3d" }, - "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.", + "description": "P\u00e1ros\u00edt\u00e1sra van sz\u00fcks\u00e9g a {protocol} protokollhoz. K\u00e9rem, 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" }, "password": { - "description": "`{protocol}` jelsz\u00f3t ig\u00e9nyel. Ez m\u00e9g nem t\u00e1mogatott, k\u00e9rj\u00fck, a folytat\u00e1shoz tiltsa le a jelsz\u00f3t.", + "description": "`{protocol}` jelsz\u00f3t ig\u00e9nyel. Ez m\u00e9g nem t\u00e1mogatott, k\u00e9rem, a folytat\u00e1shoz tiltsa le a jelsz\u00f3t.", "title": "Jelsz\u00f3 sz\u00fcks\u00e9ges" }, "protocol_disabled": { - "description": "P\u00e1ros\u00edt\u00e1s sz\u00fcks\u00e9ges a `{protocol}` miatt, de az eszk\u00f6z\u00f6n le van tiltva. K\u00e9rj\u00fck, vizsg\u00e1lja meg az eszk\u00f6z\u00f6n az esetleges hozz\u00e1f\u00e9r\u00e9si korl\u00e1toz\u00e1sokat (pl. enged\u00e9lyezze a helyi h\u00e1l\u00f3zaton l\u00e9v\u0151 \u00f6sszes eszk\u00f6z csatlakoztat\u00e1s\u00e1t).\n\nFolytathatja a protokoll p\u00e1ros\u00edt\u00e1sa n\u00e9lk\u00fcl is, de bizonyos funkci\u00f3k korl\u00e1tozottak lesznek.", + "description": "P\u00e1ros\u00edt\u00e1s sz\u00fcks\u00e9ges a `{protocol}` miatt, de az eszk\u00f6z\u00f6n le van tiltva. K\u00e9rem, vizsg\u00e1lja meg az eszk\u00f6z\u00f6n az esetleges hozz\u00e1f\u00e9r\u00e9si korl\u00e1toz\u00e1sokat (pl. enged\u00e9lyezze a helyi h\u00e1l\u00f3zaton l\u00e9v\u0151 \u00f6sszes eszk\u00f6z csatlakoztat\u00e1s\u00e1t).\n\nFolytathatja a protokoll p\u00e1ros\u00edt\u00e1sa n\u00e9lk\u00fcl is, de bizonyos funkci\u00f3k korl\u00e1tozottak lesznek.", "title": "A p\u00e1ros\u00edt\u00e1s nem lehets\u00e9ges" }, "reconfigure": { diff --git a/homeassistant/components/arcam_fmj/translations/hu.json b/homeassistant/components/arcam_fmj/translations/hu.json index 897b462eb48..0e693857363 100644 --- a/homeassistant/components/arcam_fmj/translations/hu.json +++ b/homeassistant/components/arcam_fmj/translations/hu.json @@ -19,7 +19,7 @@ "host": "C\u00edm", "port": "Port" }, - "description": "K\u00e9rj\u00fck, adja meg az eszk\u00f6z hosztnev\u00e9t vagy c\u00edm\u00e9t" + "description": "K\u00e9rem, adja meg az eszk\u00f6z hosztnev\u00e9t vagy c\u00edm\u00e9t" } } }, diff --git a/homeassistant/components/asuswrt/translations/hu.json b/homeassistant/components/asuswrt/translations/hu.json index 34581ec6b7a..3627f1f22eb 100644 --- a/homeassistant/components/asuswrt/translations/hu.json +++ b/homeassistant/components/asuswrt/translations/hu.json @@ -8,7 +8,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", "pwd_and_ssh": "Csak jelsz\u00f3 vagy SSH kulcsf\u00e1jlt adjon meg", - "pwd_or_ssh": "K\u00e9rj\u00fck, adja meg a jelsz\u00f3t vagy az SSH kulcsf\u00e1jlt", + "pwd_or_ssh": "K\u00e9rem, adja meg a jelsz\u00f3t vagy az SSH kulcsf\u00e1jlt", "ssh_not_file": "Az SSH kulcsf\u00e1jl nem tal\u00e1lhat\u00f3", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/august/translations/hu.json b/homeassistant/components/august/translations/hu.json index 42f9860bdc2..68953d8b888 100644 --- a/homeassistant/components/august/translations/hu.json +++ b/homeassistant/components/august/translations/hu.json @@ -30,7 +30,7 @@ "data": { "code": "Ellen\u0151rz\u0151 k\u00f3d" }, - "description": "K\u00e9rj\u00fck, ellen\u0151rizze a {login_method} ({username}), \u00e9s \u00edrja be al\u00e1bb az ellen\u0151rz\u0151 k\u00f3dot", + "description": "K\u00e9rem, ellen\u0151rizze a {login_method} ({username}), \u00e9s \u00edrja be al\u00e1bb az ellen\u0151rz\u0151 k\u00f3dot", "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s" } } diff --git a/homeassistant/components/azure_devops/translations/hu.json b/homeassistant/components/azure_devops/translations/hu.json index 2d8879b9d68..ed0dd7f4d1e 100644 --- a/homeassistant/components/azure_devops/translations/hu.json +++ b/homeassistant/components/azure_devops/translations/hu.json @@ -15,7 +15,7 @@ "data": { "personal_access_token": "Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si token (PAT)" }, - "description": "{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\u00e9rem, adja meg az aktu\u00e1lis hiteles\u00edt\u0151 adatait.", "title": "\u00dajrahiteles\u00edt\u00e9s" }, "user": { diff --git a/homeassistant/components/azure_event_hub/translations/hu.json b/homeassistant/components/azure_event_hub/translations/hu.json index 21b6a4ba63f..e0c50a4ae56 100644 --- a/homeassistant/components/azure_event_hub/translations/hu.json +++ b/homeassistant/components/azure_event_hub/translations/hu.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", - "cannot_connect": "A csatlakoz\u00e1s a configuration.yaml-ben szerepl\u0151 hiteles\u00edt\u0151 adatokkal nem siker\u00fclt, k\u00e9rj\u00fck, t\u00e1vol\u00edtsa el ezeket, \u00e9s haszn\u00e1lja a kezel\u0151 fel\u00fcletet a konfigur\u00e1l\u00e1shoz.", + "cannot_connect": "A csatlakoz\u00e1s a configuration.yaml-ben szerepl\u0151 hiteles\u00edt\u0151 adatokkal nem siker\u00fclt, k\u00e9rem, t\u00e1vol\u00edtsa el ezeket, \u00e9s haszn\u00e1lja a kezel\u0151 fel\u00fcletet a konfigur\u00e1l\u00e1shoz.", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges.", - "unknown": "A csatlakoz\u00e1s a configuration.yaml-ben szerepl\u0151 hiteles\u00edt\u0151 adatokkal nem siker\u00fclt, k\u00e9rj\u00fck, t\u00e1vol\u00edtsa el ezeket, \u00e9s haszn\u00e1lja a kezel\u0151 fel\u00fcletet a konfigur\u00e1l\u00e1shoz." + "unknown": "A csatlakoz\u00e1s a configuration.yaml-ben szerepl\u0151 hiteles\u00edt\u0151 adatokkal nem siker\u00fclt, k\u00e9rem, t\u00e1vol\u00edtsa el ezeket, \u00e9s haszn\u00e1lja a kezel\u0151 fel\u00fcletet a konfigur\u00e1l\u00e1shoz." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -15,7 +15,7 @@ "data": { "event_hub_connection_string": "Event Hub csatlakoz\u00e1si karaktersor" }, - "description": "K\u00e9rj\u00fck, adja meg a csatlakoz\u00e1si karaktersort ehhez: {event_hub_instance_name}", + "description": "K\u00e9rem, adja meg a csatlakoz\u00e1si karaktersort ehhez: {event_hub_instance_name}", "title": "Csatlakoz\u00e1si karaktersor t\u00edpus" }, "sas": { @@ -24,7 +24,7 @@ "event_hub_sas_key": "Event Hub SAS kulcs", "event_hub_sas_policy": "Event Hub SAS h\u00e1zirend" }, - "description": "K\u00e9rj\u00fck, adja meg a SAS hiteles\u00edt\u0151 adatait ehhez: {event_hub_instance_name}", + "description": "K\u00e9rem, adja meg a SAS hiteles\u00edt\u0151 adatait ehhez: {event_hub_instance_name}", "title": "SAS hiteles\u00edt\u00e9s t\u00edpus" }, "user": { diff --git a/homeassistant/components/bond/translations/hu.json b/homeassistant/components/bond/translations/hu.json index 179ec599d9f..801a3bf8a44 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\u00edtse, miel\u0151tt folytatn\u00e1", + "old_firmware": "Nem t\u00e1mogatott r\u00e9gi firmware a Bond eszk\u00f6z\u00f6n - k\u00e9rem friss\u00edtse, miel\u0151tt folytatn\u00e1", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/bosch_shc/translations/hu.json b/homeassistant/components/bosch_shc/translations/hu.json index df5a484cabe..2008965d95d 100644 --- a/homeassistant/components/bosch_shc/translations/hu.json +++ b/homeassistant/components/bosch_shc/translations/hu.json @@ -7,14 +7,14 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "pairing_failed": "A p\u00e1ros\u00edt\u00e1s nem siker\u00fclt; K\u00e9rj\u00fck, ellen\u0151rizze, hogy a Bosch Smart Home Controller p\u00e1ros\u00edt\u00e1si m\u00f3dban van-e (villog a LED), \u00e9s hogy a jelszava helyes-e.", + "pairing_failed": "A p\u00e1ros\u00edt\u00e1s nem siker\u00fclt; K\u00e9rem, ellen\u0151rizze, hogy a Bosch Smart Home Controller p\u00e1ros\u00edt\u00e1si m\u00f3dban van-e (villog a LED), \u00e9s hogy a jelszava helyes-e.", "session_error": "Munkamenet hiba: Az API nem OK eredm\u00e9nyt ad vissza.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "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.\nK\u00e9szen \u00e1ll {model} @ {host} be\u00e1ll\u00edt\u00e1s\u00e1nak folytat\u00e1s\u00e1ra Home Assistant seg\u00edts\u00e9g\u00e9vel?" + "description": "K\u00e9rem, 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": { diff --git a/homeassistant/components/coinbase/translations/hu.json b/homeassistant/components/coinbase/translations/hu.json index bcf409d2dda..54122d29966 100644 --- a/homeassistant/components/coinbase/translations/hu.json +++ b/homeassistant/components/coinbase/translations/hu.json @@ -16,7 +16,7 @@ "api_key": "API kulcs", "api_token": "API jelsz\u00f3" }, - "description": "K\u00e9rj\u00fck, adja meg API kulcs\u00e1nak adatait a Coinbase \u00e1ltal megadott m\u00f3don.", + "description": "K\u00e9rem, adja meg API kulcs\u00e1nak adatait a Coinbase \u00e1ltal megadott m\u00f3don.", "title": "Coinbase API kulcs r\u00e9szletei" } } diff --git a/homeassistant/components/control4/translations/hu.json b/homeassistant/components/control4/translations/hu.json index 5d41eb09a84..033555af95e 100644 --- a/homeassistant/components/control4/translations/hu.json +++ b/homeassistant/components/control4/translations/hu.json @@ -15,7 +15,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rj\u00fck, adja meg Control4-fi\u00f3kj\u00e1nak adatait \u00e9s a helyi vez\u00e9rl\u0151 IP-c\u00edm\u00e9t." + "description": "K\u00e9rem, adja meg Control4-fi\u00f3kj\u00e1nak adatait \u00e9s a helyi vez\u00e9rl\u0151 IP-c\u00edm\u00e9t." } } }, diff --git a/homeassistant/components/crownstone/translations/hu.json b/homeassistant/components/crownstone/translations/hu.json index f2ffe6f2b07..1d7c6efe2ea 100644 --- a/homeassistant/components/crownstone/translations/hu.json +++ b/homeassistant/components/crownstone/translations/hu.json @@ -6,7 +6,7 @@ "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.", + "account_not_verified": "Nem ellen\u0151rz\u00f6tt fi\u00f3k. K\u00e9rem, 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" }, diff --git a/homeassistant/components/demo/translations/ca.json b/homeassistant/components/demo/translations/ca.json index dbccaaf24a2..4ffda91d97c 100644 --- a/homeassistant/components/demo/translations/ca.json +++ b/homeassistant/components/demo/translations/ca.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Prem D'acord quan s'hagi omplert el l\u00edquid d'intermitents", + "title": "Cal omplir el l\u00edquid d'intermitents" + } + } + }, + "title": "El l\u00edquid d'intermitents est\u00e0 buit i s'ha d'omplir" + }, + "transmogrifier_deprecated": { + "description": "El component 'transmogrifier' est\u00e0 obsolet, ja que el control local ja no est\u00e0 disponible a la nova API", + "title": "El component 'transmogrifier' est\u00e0 obsolet" + }, + "unfixable_problem": { + "description": "Aquest problema no es rendir\u00e0 mai.", + "title": "No \u00e9s un problema solucionable" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/en.json b/homeassistant/components/demo/translations/en.json index 326e741f764..11378fb94d4 100644 --- a/homeassistant/components/demo/translations/en.json +++ b/homeassistant/components/demo/translations/en.json @@ -22,9 +22,6 @@ }, "options": { "step": { - "init": { - "data": {} - }, "options_1": { "data": { "bool": "Optional boolean", diff --git a/homeassistant/components/demo/translations/fr.json b/homeassistant/components/demo/translations/fr.json index 2f979d80a32..c5a150d8cb6 100644 --- a/homeassistant/components/demo/translations/fr.json +++ b/homeassistant/components/demo/translations/fr.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Appuyez sur OK une fois le liquide de clignotant rempli", + "title": "Le liquide de clignotant doit \u00eatre rempli" + } + } + }, + "title": "Le r\u00e9servoir de liquide de clignotant est vide et doit \u00eatre rempli" + }, + "transmogrifier_deprecated": { + "description": "Le composant de transmogrification est d\u00e9sormais obsol\u00e8te en raison de l'absence de contr\u00f4le local dans la nouvelle API", + "title": "Le composant de transmogrification est obsol\u00e8te" + }, + "unfixable_problem": { + "description": "Ce probl\u00e8me ne va jamais s'arr\u00eater.", + "title": "Ce probl\u00e8me ne peut \u00eatre corrig\u00e9" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/demo/translations/pt-BR.json b/homeassistant/components/demo/translations/pt-BR.json index 49290be4ceb..26e903f345d 100644 --- a/homeassistant/components/demo/translations/pt-BR.json +++ b/homeassistant/components/demo/translations/pt-BR.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Pressione OK quando o fluido do pisca-pisca for reabastecido", + "title": "O fluido do pisca-pisca precisa ser reabastecido" + } + } + }, + "title": "O fluido do pisca-pisca est\u00e1 vazio e precisa ser reabastecido" + }, + "transmogrifier_deprecated": { + "description": "O componente transmogriifier agora est\u00e1 obsoleto devido \u00e0 falta de controle local dispon\u00edvel na nova API", + "title": "O componente transmogriificador est\u00e1 obsoleto" + }, + "unfixable_problem": { + "description": "Esta quest\u00e3o nunca vai desistir.", + "title": "Este n\u00e3o \u00e9 um problema corrig\u00edvel" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/zh-Hant.json b/homeassistant/components/demo/translations/zh-Hant.json index f9f798134ba..a984c9c62ca 100644 --- a/homeassistant/components/demo/translations/zh-Hant.json +++ b/homeassistant/components/demo/translations/zh-Hant.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u65bc\u8a0a\u865f\u5291\u88dc\u5145\u5f8c\u6309\u4e0b OK", + "title": "\u8a0a\u865f\u5291\u9700\u8981\u88dc\u5145" + } + } + }, + "title": "\u8a0a\u865f\u5291\u5df2\u7528\u5b8c\u3001\u9700\u8981\u91cd\u65b0\u88dc\u5145" + }, + "transmogrifier_deprecated": { + "description": "\u7531\u65bc\u65b0 API \u7f3a\u4e4f\u672c\u5730\u7aef\u63a7\u5236\u652f\u63f4\u3001Transmogrifier \u5143\u4ef6\u5df2\u7d93\u4e0d\u63a8\u85a6\u4f7f\u7528", + "title": "Transmogrifier \u5143\u4ef6\u5df2\u7d93\u4e0d\u63a8\u85a6\u4f7f\u7528" + }, + "unfixable_problem": { + "description": "\u9019\u554f\u984c\u7e3d\u662f\u4e0d\u6b7b\u5fc3\u7684\u51fa\u73fe\u3002", + "title": "\u9019\u4e0d\u662f\u4e00\u500b\u53ef\u4fee\u7684\u554f\u984c" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/denonavr/translations/hu.json b/homeassistant/components/denonavr/translations/hu.json index 8c51c7e990f..302a485395f 100644 --- a/homeassistant/components/denonavr/translations/hu.json +++ b/homeassistant/components/denonavr/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", - "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", + "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rem, 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, a felfedezett gy\u00e1rt\u00f3n\u00e9v nem megfelel\u0151", "not_denonavr_missing": "Nem Denon AVR h\u00e1l\u00f3zati vev\u0151, a felfedez\u00e9si inform\u00e1ci\u00f3k nem teljesek" }, @@ -13,7 +13,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "K\u00e9rj\u00fck, er\u0151s\u00edtse meg a vev\u0151 hozz\u00e1ad\u00e1s\u00e1t" + "description": "K\u00e9rem, er\u0151s\u00edtse meg a vev\u0151 hozz\u00e1ad\u00e1s\u00e1t" }, "select": { "data": { diff --git a/homeassistant/components/devolo_home_control/translations/hu.json b/homeassistant/components/devolo_home_control/translations/hu.json index 391eeb60727..b7fabbbef2f 100644 --- a/homeassistant/components/devolo_home_control/translations/hu.json +++ b/homeassistant/components/devolo_home_control/translations/hu.json @@ -6,7 +6,7 @@ }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "reauth_failed": "K\u00e9rj\u00fck, ugyanazt a mydevolo felhaszn\u00e1l\u00f3t haszn\u00e1lja, mint kor\u00e1bban." + "reauth_failed": "K\u00e9rem, ugyanazt a mydevolo felhaszn\u00e1l\u00f3t haszn\u00e1lja, mint kor\u00e1bban." }, "step": { "user": { diff --git a/homeassistant/components/ecobee/translations/hu.json b/homeassistant/components/ecobee/translations/hu.json index a2fdd5553a4..590cc066de9 100644 --- a/homeassistant/components/ecobee/translations/hu.json +++ b/homeassistant/components/ecobee/translations/hu.json @@ -9,7 +9,7 @@ }, "step": { "authorize": { - "description": "K\u00e9rj\u00fck, enged\u00e9lyezze ezt az alkalmaz\u00e1st a https://www.ecobee.com/consumerportal/index.html c\u00edmen a k\u00f6vetkez\u0151 PIN-k\u00f3ddal: \n\n {pin} \n \nEzut\u00e1n nyomja meg a Mehet gombot.", + "description": "K\u00e9rem, enged\u00e9lyezze ezt az alkalmaz\u00e1st a https://www.ecobee.com/consumerportal/index.html c\u00edmen a k\u00f6vetkez\u0151 PIN-k\u00f3ddal: \n\n{pin} \n \nEzut\u00e1n nyomja meg a Mehet gombot.", "title": "Alkalmaz\u00e1s enged\u00e9lyez\u00e9se ecobee.com-on" }, "user": { diff --git a/homeassistant/components/eight_sleep/translations/ru.json b/homeassistant/components/eight_sleep/translations/ru.json new file mode 100644 index 00000000000..0370f202938 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043e\u0431\u043b\u0430\u043a\u0443 Eight Sleep: {error}" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043e\u0431\u043b\u0430\u043a\u0443 Eight Sleep: {error}" + }, + "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/esphome/translations/hu.json b/homeassistant/components/esphome/translations/hu.json index 3eaf276b309..1f98953678c 100644 --- a/homeassistant/components/esphome/translations/hu.json +++ b/homeassistant/components/esphome/translations/hu.json @@ -8,7 +8,7 @@ "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.", "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.", + "invalid_psk": "Az adat\u00e1tviteli titkos\u00edt\u00e1si kulcs \u00e9rv\u00e9nytelen. K\u00e9rem, 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." }, "flow_title": "{name}", @@ -27,13 +27,13 @@ "data": { "noise_psk": "Titkos\u00edt\u00e1si kulcs" }, - "description": "K\u00e9rj\u00fck, adja meg a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151, {name} titkos\u00edt\u00e1si kulcs\u00e1t." + "description": "K\u00e9rem, adja meg a bekonfigur\u00e1lt titkos\u00edt\u00e1si kulcsot: {name}" }, "reauth_confirm": { "data": { "noise_psk": "Titkos\u00edt\u00e1si kulcs" }, - "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." + "description": "{name} ESPHome v\u00e9gpont aktiv\u00e1lta az adat\u00e1tviteli titkos\u00edt\u00e1st vagy megv\u00e1ltoztatta a titkos\u00edt\u00e1si kulcsot. K\u00e9rem, adja meg az aktu\u00e1lis titkos\u00edt\u00e1si kulcsot." }, "user": { "data": { diff --git a/homeassistant/components/ezviz/translations/hu.json b/homeassistant/components/ezviz/translations/hu.json index 5907f66ceb4..49cd4b43d08 100644 --- a/homeassistant/components/ezviz/translations/hu.json +++ b/homeassistant/components/ezviz/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "ezviz_cloud_account_missing": "Ezviz cloud fi\u00f3k hi\u00e1nyzik. K\u00e9rj\u00fck, konfigur\u00e1lja \u00fajra az Ezviz cloud fi\u00f3kot.", + "ezviz_cloud_account_missing": "Ezviz cloud fi\u00f3k hi\u00e1nyzik. K\u00e9rem, konfigur\u00e1lja \u00fajra az Ezviz cloud fi\u00f3kot.", "unknown": "V\u00e1ratlan hiba" }, "error": { diff --git a/homeassistant/components/fivem/translations/hu.json b/homeassistant/components/fivem/translations/hu.json index d29d0c9a802..28068946c16 100644 --- a/homeassistant/components/fivem/translations/hu.json +++ b/homeassistant/components/fivem/translations/hu.json @@ -4,7 +4,7 @@ "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" }, "error": { - "cannot_connect": "Nem siker\u00fclt csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a c\u00edmet \u00e9s a portot, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra. Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l is, hogy a leg\u00fajabb FiveM szervert futtatja.", + "cannot_connect": "Nem siker\u00fclt csatlakozni. K\u00e9rem, ellen\u0151rizze a c\u00edmet \u00e9s a portot, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra. Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l is, hogy a leg\u00fajabb FiveM szervert futtatja.", "invalid_game_name": "A j\u00e1t\u00e9k API-ja, amelyhez csatlakozni pr\u00f3b\u00e1l, nem FiveM.", "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/forecast_solar/translations/hu.json b/homeassistant/components/forecast_solar/translations/hu.json index 33a69ad2fd7..3aac09afe1b 100644 --- a/homeassistant/components/forecast_solar/translations/hu.json +++ b/homeassistant/components/forecast_solar/translations/hu.json @@ -10,7 +10,7 @@ "modules power": "A napelemmodulok teljes cs\u00facsteljes\u00edtm\u00e9nye (Watt)", "name": "Elnevez\u00e9s" }, - "description": "T\u00f6ltse ki a napelemek adatait. K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, ha egy mez\u0151 nem egy\u00e9rtelm\u0171." + "description": "T\u00f6ltse ki a napelemek adatait. K\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t, ha egy mez\u0151 nem egy\u00e9rtelm\u0171." } } }, @@ -25,7 +25,7 @@ "inverter_size": "Inverter m\u00e9rete (Watt)", "modules power": "A napelemmodulok teljes cs\u00facsteljes\u00edtm\u00e9nye (Watt)" }, - "description": "Ezek az \u00e9rt\u00e9kek lehet\u0151v\u00e9 teszik a Solar.Forecast eredm\u00e9ny m\u00f3dos\u00edt\u00e1s\u00e1t. K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, ha egy mez\u0151 nem egy\u00e9rtelm\u0171." + "description": "Ezek az \u00e9rt\u00e9kek lehet\u0151v\u00e9 teszik a Solar.Forecast eredm\u00e9ny m\u00f3dos\u00edt\u00e1s\u00e1t. K\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t, ha egy mez\u0151 kit\u00f6lt\u00e9se nem egy\u00e9rtelm\u0171." } } } diff --git a/homeassistant/components/forked_daapd/translations/hu.json b/homeassistant/components/forked_daapd/translations/hu.json index 2058bbd1cbe..51285d2f7d4 100644 --- a/homeassistant/components/forked_daapd/translations/hu.json +++ b/homeassistant/components/forked_daapd/translations/hu.json @@ -5,10 +5,10 @@ "not_forked_daapd": "Az eszk\u00f6z nem forked-daapd kiszolg\u00e1l\u00f3." }, "error": { - "forbidden": "Nem tud csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze a forked-daapd h\u00e1l\u00f3zati enged\u00e9lyeket.", + "forbidden": "A csatlakoz\u00e1s sikertelen. K\u00e9rem, 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 c\u00edmet \u00e9s a portot.", + "wrong_host_or_port": "A csatlakoz\u00e1s sikertelen. K\u00e9rem, 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 legal\u00e1bb 27.0." }, diff --git a/homeassistant/components/freedompro/translations/hu.json b/homeassistant/components/freedompro/translations/hu.json index e56cc7a4a41..dd916b5b11a 100644 --- a/homeassistant/components/freedompro/translations/hu.json +++ b/homeassistant/components/freedompro/translations/hu.json @@ -12,7 +12,7 @@ "data": { "api_key": "API kulcs" }, - "description": "K\u00e9rj\u00fck, adja meg a https://home.freedompro.eu webhelyr\u0151l kapott API-kulcsot", + "description": "K\u00e9rem, adja meg a https://home.freedompro.eu webhelyr\u0151l kapott API-kulcsot", "title": "Freedompro API kulcs" } } diff --git a/homeassistant/components/generic/translations/ru.json b/homeassistant/components/generic/translations/ru.json index 7314a172867..022af07b58b 100644 --- a/homeassistant/components/generic/translations/ru.json +++ b/homeassistant/components/generic/translations/ru.json @@ -17,6 +17,7 @@ "stream_no_video": "\u0412 \u043f\u043e\u0442\u043e\u043a\u0435 \u043d\u0435\u0442 \u0432\u0438\u0434\u0435\u043e.", "stream_not_permitted": "\u041e\u043f\u0435\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", "stream_unauthorised": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", + "template_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0448\u0430\u0431\u043b\u043e\u043d\u0430. \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 URL-\u0430\u0434\u0440\u0435\u0441\u0430.", "unable_still_load": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0441 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f (\u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0445\u043e\u0441\u0442, URL-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438). \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\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." @@ -61,6 +62,7 @@ "stream_no_video": "\u0412 \u043f\u043e\u0442\u043e\u043a\u0435 \u043d\u0435\u0442 \u0432\u0438\u0434\u0435\u043e.", "stream_not_permitted": "\u041e\u043f\u0435\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0430 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443. \u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u044b\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b RTSP?", "stream_unauthorised": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043f\u043e\u0442\u043e\u043a\u0443.", + "template_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0448\u0430\u0431\u043b\u043e\u043d\u0430. \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 URL-\u0430\u0434\u0440\u0435\u0441\u0430.", "unable_still_load": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u043e\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0441 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f (\u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0445\u043e\u0441\u0442, URL-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438). \u041f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0436\u0443\u0440\u043d\u0430\u043b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\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/goalzero/translations/hu.json b/homeassistant/components/goalzero/translations/hu.json index c0376fc29b9..607cc4e5b63 100644 --- a/homeassistant/components/goalzero/translations/hu.json +++ b/homeassistant/components/goalzero/translations/hu.json @@ -19,7 +19,7 @@ "host": "C\u00edm", "name": "Elnevez\u00e9s" }, - "description": "K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, hogy megbizonyosodjon arr\u00f3l, hogy minden k\u00f6vetelm\u00e9ny teljes\u00fcl." + "description": "K\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t, hogy megbizonyosodjon arr\u00f3l, hogy minden k\u00f6vetelm\u00e9ny teljes\u00fcl." } } } diff --git a/homeassistant/components/google/translations/hu.json b/homeassistant/components/google/translations/hu.json index 0ff516dcfed..b27e06b15c7 100644 --- a/homeassistant/components/google/translations/hu.json +++ b/homeassistant/components/google/translations/hu.json @@ -7,11 +7,12 @@ "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "code_expired": "A hiteles\u00edt\u00e9si k\u00f3d lej\u00e1rt vagy a hiteles\u00edt\u0151 adatok be\u00e1ll\u00edt\u00e1sa \u00e9rv\u00e9nytelen, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra.", + "code_expired": "A hiteles\u00edt\u00e9si k\u00f3d lej\u00e1rt vagy a hiteles\u00edt\u0151 adatok be\u00e1ll\u00edt\u00e1sa \u00e9rv\u00e9nytelen, k\u00e9rem, pr\u00f3b\u00e1lja meg \u00fajra.", "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token", "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "oauth_error": "\u00c9rv\u00e9nytelen token adatok \u00e9rkeztek.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "timeout_connect": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n" }, "create_entry": { "default": "Sikeres hiteles\u00edt\u00e9s" diff --git a/homeassistant/components/google/translations/ru.json b/homeassistant/components/google/translations/ru.json index 5d9f51fe14e..5fc2cb03feb 100644 --- a/homeassistant/components/google/translations/ru.json +++ b/homeassistant/components/google/translations/ru.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c]({more_info_url}) \u043d\u0430 [\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 OAuth]({oauth_consent_url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c Home Assistant \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u041a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u044e Google. \u0422\u0430\u043a\u0436\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441 \u0432\u0430\u0448\u0438\u043c \u043a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u0435\u043c:\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439]({oauth_creds_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f**.\n2. \u0412 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0435\u043c \u0441\u043f\u0438\u0441\u043a\u0435 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth**.\n3. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **TV and Limited Input devices** \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0422\u0438\u043f\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f." + }, "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.", diff --git a/homeassistant/components/google/translations/zh-Hant.json b/homeassistant/components/google/translations/zh-Hant.json index bee271ea8e7..45dc83c1b3c 100644 --- a/homeassistant/components/google/translations/zh-Hant.json +++ b/homeassistant/components/google/translations/zh-Hant.json @@ -11,7 +11,8 @@ "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", "oauth_error": "\u6536\u5230\u7121\u6548\u7684\u6b0a\u6756\u8cc7\u6599\u3002", - "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "timeout_connect": "\u5efa\u7acb\u9023\u7dda\u903e\u6642" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" diff --git a/homeassistant/components/hassio/translations/ru.json b/homeassistant/components/hassio/translations/ru.json index f9572edcd6f..5e1caa41ebf 100644 --- a/homeassistant/components/hassio/translations/ru.json +++ b/homeassistant/components/hassio/translations/ru.json @@ -1,6 +1,7 @@ { "system_health": { "info": { + "agent_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u0430\u0433\u0435\u043d\u0442\u0430", "board": "\u041f\u043b\u0430\u0442\u0430", "disk_total": "\u041f\u0430\u043c\u044f\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e", "disk_used": "\u041f\u0430\u043c\u044f\u0442\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u043e", diff --git a/homeassistant/components/heos/translations/hu.json b/homeassistant/components/heos/translations/hu.json index 8996c2a4530..49bee7bac8f 100644 --- a/homeassistant/components/heos/translations/hu.json +++ b/homeassistant/components/heos/translations/hu.json @@ -11,7 +11,7 @@ "data": { "host": "C\u00edm" }, - "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).", + "description": "K\u00e9rem, adja meg egy Heos-eszk\u00f6z hosztnev\u00e9t vagy c\u00edm\u00e9t (lehet\u0151leg egy vezet\u00e9kes h\u00e1l\u00f3zathoz csatlakoztatott eszk\u00f6zt).", "title": "Csatlakoz\u00e1s a Heos-hoz" } } diff --git a/homeassistant/components/here_travel_time/translations/it.json b/homeassistant/components/here_travel_time/translations/it.json index e9716318adb..7c87ef36d0e 100644 --- a/homeassistant/components/here_travel_time/translations/it.json +++ b/homeassistant/components/here_travel_time/translations/it.json @@ -39,6 +39,9 @@ }, "title": "Scegli la partenza" }, + "origin_menu": { + "title": "Scegli Origine" + }, "user": { "data": { "api_key": "Chiave API", diff --git a/homeassistant/components/homekit_controller/translations/hu.json b/homeassistant/components/homekit_controller/translations/hu.json index 607c46ede6d..c8f86e46f38 100644 --- a/homeassistant/components/homekit_controller/translations/hu.json +++ b/homeassistant/components/homekit_controller/translations/hu.json @@ -11,7 +11,7 @@ "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.", + "authentication_error": "Helytelen HomeKit k\u00f3d. K\u00e9rem, 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 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.", diff --git a/homeassistant/components/homekit_controller/translations/it.json b/homeassistant/components/homekit_controller/translations/it.json index d95eff05cea..947e03374b4 100644 --- a/homeassistant/components/homekit_controller/translations/it.json +++ b/homeassistant/components/homekit_controller/translations/it.json @@ -18,7 +18,7 @@ "unable_to_pair": "Impossibile abbinare, riprova.", "unknown_error": "Il dispositivo ha riportato un errore sconosciuto. L'abbinamento non \u00e8 riuscito." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Interrompi l'associazione su tutti i controller o provare a riavviare il dispositivo, quindi continua a riprendere l'associazione.", diff --git a/homeassistant/components/honeywell/translations/hu.json b/homeassistant/components/honeywell/translations/hu.json index 14ba9167ea5..b0552b23fe1 100644 --- a/homeassistant/components/honeywell/translations/hu.json +++ b/homeassistant/components/honeywell/translations/hu.json @@ -9,7 +9,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rj\u00fck, adja meg a mytotalconnectcomfort.com webhelyre val\u00f3 bejelentkez\u00e9shez haszn\u00e1lt hiteles\u00edt\u0151 adatokat." + "description": "K\u00e9rem, adja meg a mytotalconnectcomfort.com webhelyre val\u00f3 bejelentkez\u00e9shez haszn\u00e1lt hiteles\u00edt\u0151 adatokat." } } }, diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json index c01a5cb4bb8..e1a26405396 100644 --- a/homeassistant/components/huawei_lte/translations/hu.json +++ b/homeassistant/components/huawei_lte/translations/hu.json @@ -9,7 +9,7 @@ "incorrect_username": "Helytelen felhaszn\u00e1l\u00f3n\u00e9v", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "invalid_url": "\u00c9rv\u00e9nytelen URL", - "login_attempts_exceeded": "T\u00fall\u00e9pte a maxim\u00e1lis bejelentkez\u00e9si k\u00eds\u00e9rleteket. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb", + "login_attempts_exceeded": "T\u00fall\u00e9pte a maxim\u00e1lis bejelentkez\u00e9si k\u00eds\u00e9rleteket. K\u00e9rem, pr\u00f3b\u00e1lja \u00fajra k\u00e9s\u0151bb.", "response_error": "Ismeretlen hiba az eszk\u00f6zr\u0151l", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/hyperion/translations/hu.json b/homeassistant/components/hyperion/translations/hu.json index aaffa705279..abb02b1f6e7 100644 --- a/homeassistant/components/hyperion/translations/hu.json +++ b/homeassistant/components/hyperion/translations/hu.json @@ -27,7 +27,7 @@ "title": "Er\u0151s\u00edtse meg a Hyperion Ambilight szolg\u00e1ltat\u00e1s hozz\u00e1ad\u00e1s\u00e1t" }, "create_token": { - "description": "Az al\u00e1bbiakban v\u00e1lassza a **Mehet** 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 **Mehet** 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\u00e9rem, ellen\u0151rizze, hogy a megjelen\u00edtett azonos\u00edt\u00f3 \"{auth_id}\"", "title": "\u00daj hiteles\u00edt\u00e9si token automatikus l\u00e9trehoz\u00e1sa" }, "create_token_external": { diff --git a/homeassistant/components/iaqualink/translations/hu.json b/homeassistant/components/iaqualink/translations/hu.json index 4ee5ba39b8b..7bbac17747a 100644 --- a/homeassistant/components/iaqualink/translations/hu.json +++ b/homeassistant/components/iaqualink/translations/hu.json @@ -13,7 +13,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rj\u00fck, adja meg iAqualink-fi\u00f3kj\u00e1nak felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t.", + "description": "K\u00e9rem, 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/hu.json b/homeassistant/components/icloud/translations/hu.json index cff637ae03f..1cbbbfb6974 100644 --- a/homeassistant/components/icloud/translations/hu.json +++ b/homeassistant/components/icloud/translations/hu.json @@ -38,7 +38,7 @@ "data": { "verification_code": "Ellen\u0151rz\u0151 k\u00f3d" }, - "description": "K\u00e9rj\u00fck, \u00edrja be az iCloud-t\u00f3l \u00e9ppen kapott ellen\u0151rz\u0151 k\u00f3dot", + "description": "K\u00e9rem, \u00edrja be az iCloud-t\u00f3l \u00e9ppen kapott ellen\u0151rz\u0151 k\u00f3dot", "title": "iCloud ellen\u0151rz\u0151 k\u00f3d" } } diff --git a/homeassistant/components/intellifire/translations/hu.json b/homeassistant/components/intellifire/translations/hu.json index 2f680f2e776..afd5b99e8d7 100644 --- a/homeassistant/components/intellifire/translations/hu.json +++ b/homeassistant/components/intellifire/translations/hu.json @@ -31,7 +31,7 @@ "data": { "host": "C\u00edm" }, - "description": "A k\u00f6vetkez\u0151 IntelliFire eszk\u00f6z\u00f6k \u00e9szlelve. K\u00e9rj\u00fck, v\u00e1lassza ki, melyiket szeretn\u00e9 konfigur\u00e1lni.", + "description": "A k\u00f6vetkez\u0151 IntelliFire eszk\u00f6z\u00f6k \u00e9szlelve. K\u00e9rem, v\u00e1lassza ki, melyiket szeretn\u00e9 konfigur\u00e1lni.", "title": "Eszk\u00f6z v\u00e1laszt\u00e1sa" } } diff --git a/homeassistant/components/iotawatt/translations/hu.json b/homeassistant/components/iotawatt/translations/hu.json index 5f5d30136da..0c048e0102c 100644 --- a/homeassistant/components/iotawatt/translations/hu.json +++ b/homeassistant/components/iotawatt/translations/hu.json @@ -11,7 +11,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Az IoTawatt eszk\u00f6z hiteles\u00edt\u00e9st ig\u00e9nyel. K\u00e9rj\u00fck, adja meg felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t, majd folytassa." + "description": "Az IoTawatt eszk\u00f6z hiteles\u00edt\u00e9st ig\u00e9nyel. K\u00e9rem, adja meg felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t, majd folytassa." }, "user": { "data": { diff --git a/homeassistant/components/ipp/translations/hu.json b/homeassistant/components/ipp/translations/hu.json index 18381fde2cf..471c73c8abc 100644 --- a/homeassistant/components/ipp/translations/hu.json +++ b/homeassistant/components/ipp/translations/hu.json @@ -11,7 +11,7 @@ }, "error": { "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." + "connection_upgrade": "Nem siker\u00fclt csatlakozni a nyomtat\u00f3hoz. K\u00e9rem, esetleg pr\u00f3b\u00e1lja meg \u00fajra az SSL/TLS opci\u00f3 bejel\u00f6l\u00e9s\u00e9vel." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/knx/translations/hu.json b/homeassistant/components/knx/translations/hu.json index 28cab2cea5f..92411b58312 100644 --- a/homeassistant/components/knx/translations/hu.json +++ b/homeassistant/components/knx/translations/hu.json @@ -48,7 +48,7 @@ "knxkeys_filename": "A f\u00e1jl a `.storage/knx/` konfigur\u00e1ci\u00f3s k\u00f6nyvt\u00e1r\u00e1ban helyezend\u0151.\nHome Assistant oper\u00e1ci\u00f3s rendszer eset\u00e9n ez a k\u00f6vetkez\u0151 lenne: `/config/.storage/knx/`\nP\u00e9lda: \"my_project.knxkeys\".", "knxkeys_password": "Ez a be\u00e1ll\u00edt\u00e1s a f\u00e1jl ETS-b\u0151l t\u00f6rt\u00e9n\u0151 export\u00e1l\u00e1sakor t\u00f6rt\u00e9nt." }, - "description": "K\u00e9rj\u00fck, adja meg a '.knxkeys' f\u00e1jl adatait." + "description": "K\u00e9rem, adja meg a '.knxkeys' f\u00e1jl adatait." }, "secure_manual": { "data": { @@ -61,7 +61,7 @@ "user_id": "Ez gyakran a tunnel sz\u00e1ma +1. Teh\u00e1t a \"Tunnel 2\" felhaszn\u00e1l\u00f3i azonos\u00edt\u00f3ja \"3\".", "user_password": "Jelsz\u00f3 az adott tunnelhez, amely a tunnel \u201eProperties\u201d panelj\u00e9n van be\u00e1ll\u00edtva az ETS-ben." }, - "description": "K\u00e9rj\u00fck, adja meg az IP secure adatokat." + "description": "K\u00e9rem, adja meg az IP secure adatokat." }, "secure_tunneling": { "description": "V\u00e1lassza ki, hogyan szeretn\u00e9 konfigur\u00e1lni az KNX/IP secure-t.", diff --git a/homeassistant/components/konnected/translations/hu.json b/homeassistant/components/konnected/translations/hu.json index 57cbf0d9bb9..914546e4481 100644 --- a/homeassistant/components/konnected/translations/hu.json +++ b/homeassistant/components/konnected/translations/hu.json @@ -24,7 +24,7 @@ "host": "IP c\u00edm", "port": "Port" }, - "description": "K\u00e9rj\u00fck, adja meg a Konnected Panel csatlakoz\u00e1si adatait." + "description": "K\u00e9rem, adja meg a Konnected Panel csatlakoz\u00e1si adatait." } } }, @@ -91,7 +91,7 @@ "discovery": "V\u00e1laszoljon a h\u00e1l\u00f3zaton \u00e9rkez\u0151 felder\u00edt\u00e9si k\u00e9r\u00e9sekre", "override_api_host": "Az alap\u00e9rtelmezett Home Assistant API host-URL fel\u00fcl\u00edr\u00e1sa" }, - "description": "K\u00e9rj\u00fck, v\u00e1lassza ki a k\u00edv\u00e1nt viselked\u00e9st a panelhez", + "description": "K\u00e9rem, v\u00e1lassza ki panel k\u00edv\u00e1nt m\u0171k\u00f6d\u00e9s\u00e9t", "title": "Egy\u00e9b be\u00e1ll\u00edt\u00e1sa" }, "options_switch": { diff --git a/homeassistant/components/laundrify/translations/hu.json b/homeassistant/components/laundrify/translations/hu.json index aa22ddc3da3..604bffbec52 100644 --- a/homeassistant/components/laundrify/translations/hu.json +++ b/homeassistant/components/laundrify/translations/hu.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "invalid_format": "\u00c9rv\u00e9nytelen form\u00e1tum. K\u00e9rj\u00fck, adja meg \u00edgy: xxx-xxx.", + "invalid_format": "\u00c9rv\u00e9nytelen form\u00e1tum. K\u00e9rem, \u00edgy adja meg: xxx-xxx.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { @@ -14,7 +14,7 @@ "data": { "code": "Hiteles\u00edt\u00e9si k\u00f3d (xxx-xxx)" }, - "description": "K\u00e9rj\u00fck, adja meg a laundrify-alkalmaz\u00e1sban megjelen\u0151 szem\u00e9lyes enged\u00e9lyez\u00e9si k\u00f3dj\u00e1t." + "description": "K\u00e9rem, adja meg a laundrify-alkalmaz\u00e1sban megjelen\u0151 szem\u00e9lyes enged\u00e9lyez\u00e9si k\u00f3dj\u00e1t." }, "reauth_confirm": { "description": "A laundrify integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie.", diff --git a/homeassistant/components/lifx/translations/hu.json b/homeassistant/components/lifx/translations/hu.json index 3d728f21d07..588d5932e10 100644 --- a/homeassistant/components/lifx/translations/hu.json +++ b/homeassistant/components/lifx/translations/hu.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: LIFX?" + }, + "discovery_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Eszk\u00f6z" + } + }, + "user": { + "data": { + "host": "C\u00edm" + }, + "description": "Ha \u00fcresen hagyja a c\u00edm mez\u0151t, a rendszer megpr\u00f3b\u00e1lja az eszk\u00f6z\u00f6ket megkeresni." } } } diff --git a/homeassistant/components/lifx/translations/it.json b/homeassistant/components/lifx/translations/it.json index be167ec9994..d9c25e63590 100644 --- a/homeassistant/components/lifx/translations/it.json +++ b/homeassistant/components/lifx/translations/it.json @@ -4,9 +4,21 @@ "no_devices_found": "Nessun dispositivo trovato sulla rete", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Vuoi configurare LIFX?" + }, + "discovery_confirm": { + "description": "Vuoi configurare {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Dispositivo" + } + }, + "user": { + "description": "Se lasci l'host vuoto, il rilevamento verr\u00e0 utilizzato per trovare i dispositivi." } } } diff --git a/homeassistant/components/lifx/translations/zh-Hant.json b/homeassistant/components/lifx/translations/zh-Hant.json index 911eaa570d1..e8ff08be901 100644 --- a/homeassistant/components/lifx/translations/zh-Hant.json +++ b/homeassistant/components/lifx/translations/zh-Hant.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a LIFX\uff1f" + }, + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {label} ({host}) {serial}\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\u641c\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" } } } diff --git a/homeassistant/components/logi_circle/translations/hu.json b/homeassistant/components/logi_circle/translations/hu.json index 947f11e4907..1d24638705e 100644 --- a/homeassistant/components/logi_circle/translations/hu.json +++ b/homeassistant/components/logi_circle/translations/hu.json @@ -13,7 +13,7 @@ }, "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 **Mehet** gombot. \n\n [Link]({authorization_url})", + "description": "K\u00e9rem, 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 **Mehet** gombot. \n\n[Link]({authorization_url})", "title": "Hiteles\u00edt\u00e9s a LogiCircle seg\u00edts\u00e9g\u00e9vel" }, "user": { diff --git a/homeassistant/components/mazda/translations/hu.json b/homeassistant/components/mazda/translations/hu.json index 42afc763687..baa251ac301 100644 --- a/homeassistant/components/mazda/translations/hu.json +++ b/homeassistant/components/mazda/translations/hu.json @@ -17,7 +17,7 @@ "password": "Jelsz\u00f3", "region": "R\u00e9gi\u00f3" }, - "description": "K\u00e9rj\u00fck, adja meg azt az e-mail c\u00edmet \u00e9s jelsz\u00f3t, amelyet a MyMazda mobilalkalmaz\u00e1sba val\u00f3 bejelentkez\u00e9shez haszn\u00e1lt." + "description": "K\u00e9rem, adja meg azt az e-mail c\u00edmet \u00e9s jelsz\u00f3t, amelyet a MyMazda mobilalkalmaz\u00e1sba val\u00f3 bejelentkez\u00e9shez haszn\u00e1lt." } } } diff --git a/homeassistant/components/meater/translations/hu.json b/homeassistant/components/meater/translations/hu.json index 964c240b4e3..98f54598ee4 100644 --- a/homeassistant/components/meater/translations/hu.json +++ b/homeassistant/components/meater/translations/hu.json @@ -2,7 +2,7 @@ "config": { "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "service_unavailable_error": "Az API jelenleg nem el\u00e9rhet\u0151, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", + "service_unavailable_error": "Az API jelenleg nem el\u00e9rhet\u0151, k\u00e9rem, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", "unknown_auth_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { diff --git a/homeassistant/components/minecraft_server/translations/hu.json b/homeassistant/components/minecraft_server/translations/hu.json index 00fff153254..7c7232d46a0 100644 --- a/homeassistant/components/minecraft_server/translations/hu.json +++ b/homeassistant/components/minecraft_server/translations/hu.json @@ -4,9 +4,9 @@ "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 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." + "cannot_connect": "Nem siker\u00fclt csatlakozni a szerverhez. K\u00e9rem, 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\u00e9rem, jav\u00edtsa ki, \u00e9s pr\u00f3b\u00e1lja \u00fajra.", + "invalid_port": "A portsz\u00e1mnak 1024 \u00e9s 65535 k\u00f6z\u00f6tt kell lennie. K\u00e9rem, jav\u00edtsa ki, \u00e9s pr\u00f3b\u00e1lja \u00fajra." }, "step": { "user": { diff --git a/homeassistant/components/mysensors/translations/hu.json b/homeassistant/components/mysensors/translations/hu.json index 433714de477..e1d9c10b053 100644 --- a/homeassistant/components/mysensors/translations/hu.json +++ b/homeassistant/components/mysensors/translations/hu.json @@ -34,7 +34,7 @@ "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", + "not_a_number": "K\u00e9rem, sz\u00e1mot adjon meg", "port_out_of_range": "A portsz\u00e1mnak legal\u00e1bb 1-nek \u00e9s legfeljebb 65535-nek kell lennie", "same_topic": "A feliratkoz\u00e1s \u00e9s a k\u00f6zz\u00e9t\u00e9tel t\u00e9m\u00e1i ugyanazok", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index 9662d4175fd..0cb2c91d819 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c]({more_info_url}), \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Cloud Console: \n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 [\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 OAuth]({oauth_consent_url}) \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u043d\u0430 \u043d\u0435\u0439 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f.\n2. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439]({oauth_creds_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f**.\n3. \u0412 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0435\u043c \u0441\u043f\u0438\u0441\u043a\u0435 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth**.\n4. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **Web Application** \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0442\u0438\u043f\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.\n5. \u0414\u043e\u0431\u0430\u0432\u044c\u0442\u0435 `{redirect_url}` \u0432 \u043f\u043e\u043b\u0435 *Authorized redirect URI*." + }, "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.", @@ -30,6 +33,17 @@ "description": "[\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u0437\u0430\u043f\u0438\u0441\u044c Google. \n\n\u041f\u043e\u0441\u043b\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u043a\u043e\u043f\u0438\u0440\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u0430\u0433\u0430\u0435\u043c\u044b\u0439 \u0442\u043e\u043a\u0435\u043d.", "title": "\u041f\u0440\u0438\u0432\u044f\u0437\u043a\u0430 \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u0430 Google" }, + "auth_upgrade": { + "description": "\u041f\u0440\u0435\u0436\u043d\u0438\u0439 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0431\u044b\u043b \u0443\u043f\u0440\u0430\u0437\u0434\u043d\u0435\u043d Google \u0434\u043b\u044f \u043f\u043e\u0432\u044b\u0448\u0435\u043d\u0438\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438. \u0412\u0430\u043c \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.\n\n\u0427\u0442\u043e \u043d\u0443\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0434\u043b\u044f \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c Nest \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]({more_info_url}).", + "title": "Nest: \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + }, + "cloud_project": { + "data": { + "cloud_project_id": "Google Cloud Project ID" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 Cloud Project ID, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 *example-project-12345*. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 [Google Cloud Console({cloud_console_url}) \u0438\u043b\u0438 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044e]({more_info_url}).", + "title": "Nest: \u0432\u0432\u0435\u0434\u0438\u0442\u0435 Cloud Project ID" + }, "device_project_upgrade": { "title": "Nest: \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c" }, diff --git a/homeassistant/components/netgear/translations/hu.json b/homeassistant/components/netgear/translations/hu.json index 4432e08a508..c0c472bad30 100644 --- a/homeassistant/components/netgear/translations/hu.json +++ b/homeassistant/components/netgear/translations/hu.json @@ -4,7 +4,7 @@ "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" + "config": "Csatlakoz\u00e1si vagy bejelentkez\u00e9si hiba: k\u00e9rem, ellen\u0151rizze a konfigur\u00e1ci\u00f3t" }, "step": { "user": { diff --git a/homeassistant/components/nfandroidtv/translations/hu.json b/homeassistant/components/nfandroidtv/translations/hu.json index 65cd2bebc01..6e83a5f7c6d 100644 --- a/homeassistant/components/nfandroidtv/translations/hu.json +++ b/homeassistant/components/nfandroidtv/translations/hu.json @@ -13,7 +13,7 @@ "host": "C\u00edm", "name": "Elnevez\u00e9s" }, - "description": "K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t, hogy megbizonyosodjon arr\u00f3l, hogy minden k\u00f6vetelm\u00e9ny teljes\u00fcl." + "description": "K\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t, hogy megbizonyosodjon arr\u00f3l, hogy minden k\u00f6vetelm\u00e9ny teljes\u00fcl." } } } diff --git a/homeassistant/components/ovo_energy/translations/hu.json b/homeassistant/components/ovo_energy/translations/hu.json index 2c794b5cd9d..a58669fe11c 100644 --- a/homeassistant/components/ovo_energy/translations/hu.json +++ b/homeassistant/components/ovo_energy/translations/hu.json @@ -11,7 +11,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "Nem siker\u00fclt az OVO Energy hiteles\u00edt\u00e9se. K\u00e9rj\u00fck, adja meg jelenlegi hiteles\u00edt\u0151 adatait.", + "description": "Nem siker\u00fclt az OVO Energy hiteles\u00edt\u00e9se. K\u00e9rem, adja meg jelenlegi hiteles\u00edt\u0151 adatait.", "title": "\u00dajrahiteles\u00edt\u00e9s" }, "user": { diff --git a/homeassistant/components/plaato/translations/hu.json b/homeassistant/components/plaato/translations/hu.json index 245b0dc2a0c..990ecc7a561 100644 --- a/homeassistant/components/plaato/translations/hu.json +++ b/homeassistant/components/plaato/translations/hu.json @@ -20,7 +20,7 @@ "token": "Auth Token beilleszt\u00e9se ide", "use_webhook": "Webhook haszn\u00e1lata" }, - "description": "Az API lek\u00e9rdez\u00e9s\u00e9hez egy `auth_token` sz\u00fcks\u00e9ges, amelyet az [ezek] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) utas\u00edt\u00e1sok k\u00f6vet\u00e9s\u00e9vel lehet megszerezni. \n\n Kiv\u00e1lasztott eszk\u00f6z: ** {device_type} ** \n\nHa ink\u00e1bb a be\u00e9p\u00edtett webhook m\u00f3dszert haszn\u00e1lja (csak az Airlock eset\u00e9ben), k\u00e9rj\u00fck, jel\u00f6lje be az al\u00e1bbi n\u00e9gyzetet, \u00e9s hagyja \u00fcresen az Auth Token elemet", + "description": "Az API lek\u00e9rdez\u00e9s\u00e9hez egy `auth_token` sz\u00fcks\u00e9ges, amelyet az [ezek] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) utas\u00edt\u00e1sok k\u00f6vet\u00e9s\u00e9vel lehet megszerezni. \n\nKiv\u00e1lasztott eszk\u00f6z: **{device_type}** \n\nHa ink\u00e1bb a be\u00e9p\u00edtett webhook m\u00f3dszert haszn\u00e1lja (csak az Airlock eset\u00e9ben), k\u00e9rem, jel\u00f6lje be az al\u00e1bbi n\u00e9gyzetet, \u00e9s hagyja \u00fcresen az Auth Token elemet", "title": "API m\u00f3dszer kiv\u00e1laszt\u00e1sa" }, "user": { diff --git a/homeassistant/components/plugwise/translations/ca.json b/homeassistant/components/plugwise/translations/ca.json index 1bf9efee843..ccd3d344f39 100644 --- a/homeassistant/components/plugwise/translations/ca.json +++ b/homeassistant/components/plugwise/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El servei ja est\u00e0 configurat" + "already_configured": "El servei ja est\u00e0 configurat", + "anna_with_adam": "Anna i Adam detectats. Afegeix l'Adam en lloc de l'Anna" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", diff --git a/homeassistant/components/plugwise/translations/en.json b/homeassistant/components/plugwise/translations/en.json index 3f365bfa25e..cd10502d0c3 100644 --- a/homeassistant/components/plugwise/translations/en.json +++ b/homeassistant/components/plugwise/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Service is already configured" + "already_configured": "Service is already configured", + "anna_with_adam": "Both Anna and Adam detected. Add your Adam instead of your Anna" }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/plugwise/translations/et.json b/homeassistant/components/plugwise/translations/et.json index 2d50be06193..9f2f2e0b1b6 100644 --- a/homeassistant/components/plugwise/translations/et.json +++ b/homeassistant/components/plugwise/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Teenus on juba seadistatud" + "already_configured": "Teenus on juba seadistatud", + "anna_with_adam": "Nii Anna kui ka Adam on tuvastatud. Lisa oma Anna asemel oma Adam" }, "error": { "cannot_connect": "\u00dchendamine nurjus", diff --git a/homeassistant/components/plugwise/translations/fr.json b/homeassistant/components/plugwise/translations/fr.json index 0306437b405..85f4f652c18 100644 --- a/homeassistant/components/plugwise/translations/fr.json +++ b/homeassistant/components/plugwise/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "anna_with_adam": "Anna et Adam ont tous deux \u00e9t\u00e9 d\u00e9tect\u00e9s. Ajoutez votre Adam au lieu de votre Anna" }, "error": { "cannot_connect": "\u00c9chec de connexion", diff --git a/homeassistant/components/plugwise/translations/hu.json b/homeassistant/components/plugwise/translations/hu.json index b622109797c..b11137a2658 100644 --- a/homeassistant/components/plugwise/translations/hu.json +++ b/homeassistant/components/plugwise/translations/hu.json @@ -29,7 +29,7 @@ "port": "Port", "username": "Smile Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rj\u00fck, adja meg", + "description": "K\u00e9rem, adja meg", "title": "Csatlakoz\u00e1s a Smile-hoz" } } diff --git a/homeassistant/components/plugwise/translations/pt-BR.json b/homeassistant/components/plugwise/translations/pt-BR.json index 12f8070f074..5f667f8d6ff 100644 --- a/homeassistant/components/plugwise/translations/pt-BR.json +++ b/homeassistant/components/plugwise/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "anna_with_adam": "Tanto Anna quanto Adam detectaram. Adicione seu Adam em vez de sua Anna" }, "error": { "cannot_connect": "Falha ao conectar", diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json index 62b7c1e27d3..c7cfa232fd0 100644 --- a/homeassistant/components/plugwise/translations/ru.json +++ b/homeassistant/components/plugwise/translations/ru.json @@ -13,7 +13,11 @@ "step": { "user": { "data": { - "flow_type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + "flow_type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", + "host": "IP-\u0430\u0434\u0440\u0435\u0441", + "password": "Smile ID", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f Smile" }, "description": "\u041f\u0440\u043e\u0434\u0443\u043a\u0442:", "title": "\u0422\u0438\u043f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Plugwise" diff --git a/homeassistant/components/plugwise/translations/zh-Hant.json b/homeassistant/components/plugwise/translations/zh-Hant.json index 9d37a79462b..2ea2c4b09ba 100644 --- a/homeassistant/components/plugwise/translations/zh-Hant.json +++ b/homeassistant/components/plugwise/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "anna_with_adam": "\u767c\u73fe Anna \u8207 Adam\uff0c\u662f\u5426\u8981\u65b0\u589e Adam \u800c\u975e Anna" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/powerwall/translations/hu.json b/homeassistant/components/powerwall/translations/hu.json index 6f53b1ef575..2f59abef956 100644 --- a/homeassistant/components/powerwall/translations/hu.json +++ b/homeassistant/components/powerwall/translations/hu.json @@ -9,7 +9,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", - "wrong_version": "Az powerwall nem t\u00e1mogatott szoftververzi\u00f3t haszn\u00e1l. K\u00e9rj\u00fck, fontolja meg a probl\u00e9ma friss\u00edt\u00e9s\u00e9t vagy jelent\u00e9s\u00e9t, hogy megoldhat\u00f3 legyen." + "wrong_version": "Az powerwall nem t\u00e1mogatott szoftververzi\u00f3t haszn\u00e1l. K\u00e9rem, fontolja meg a probl\u00e9ma friss\u00edt\u00e9s\u00e9t vagy jelent\u00e9s\u00e9t, hogy megoldhat\u00f3 legyen." }, "flow_title": "{name} ({ip_address})", "step": { diff --git a/homeassistant/components/radiotherm/translations/ru.json b/homeassistant/components/radiotherm/translations/ru.json new file mode 100644 index 00000000000..daac6c7c5ae --- /dev/null +++ b/homeassistant/components/radiotherm/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." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{name} {model} ({host})", + "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} {model} ({host})?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u043f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u043e\u0433\u043e \u0443\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u044f \u043f\u0440\u0438 \u0440\u0435\u0433\u0443\u043b\u0438\u0440\u043e\u0432\u043a\u0435 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/renault/translations/hu.json b/homeassistant/components/renault/translations/hu.json index d74d8cdf9e4..9d2367af80b 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 {username} jelszav\u00e1t", + "description": "K\u00e9rem, friss\u00edtse {username} jelszav\u00e1t", "title": "Integr\u00e1ci\u00f3 \u00fajb\u00f3li hiteles\u00edt\u00e9se" }, "user": { diff --git a/homeassistant/components/rhasspy/translations/it.json b/homeassistant/components/rhasspy/translations/it.json new file mode 100644 index 00000000000..bd2daba89f6 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/it.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Vuoi abilitare il supporto Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json index e109b6e7043..b4126e202d1 100644 --- a/homeassistant/components/roomba/translations/hu.json +++ b/homeassistant/components/roomba/translations/hu.json @@ -19,7 +19,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "A jelsz\u00f3t nem siker\u00fclt automatikusan lek\u00e9rni az eszk\u00f6zr\u0151l. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3ban ismertetett l\u00e9p\u00e9seket: {auth_help_url}", + "description": "A jelsz\u00f3t nem siker\u00fclt automatikusan lek\u00e9rni az eszk\u00f6zr\u0151l. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3ban ismertetett l\u00e9p\u00e9seket: {auth_help_url}", "title": "Jelsz\u00f3 megad\u00e1sa" }, "manual": { diff --git a/homeassistant/components/roon/translations/hu.json b/homeassistant/components/roon/translations/hu.json index aa536aeeddd..cf5d1609dd3 100644 --- a/homeassistant/components/roon/translations/hu.json +++ b/homeassistant/components/roon/translations/hu.json @@ -13,7 +13,7 @@ "host": "C\u00edm", "port": "Port" }, - "description": "Nem siker\u00fclt felfedezni a Roon-kiszolg\u00e1l\u00f3t. K\u00e9rj\u00fck, adja meg g\u00e9p c\u00edm\u00e9t \u00e9s portj\u00e1t." + "description": "Nem siker\u00fclt felfedezni a Roon-kiszolg\u00e1l\u00f3t. K\u00e9rem, adja meg g\u00e9p c\u00edm\u00e9t \u00e9s portj\u00e1t." }, "link": { "description": "Enged\u00e9lyeznie kell az Home Assistantot a Roonban. Miut\u00e1n r\u00e1kattintott a Mehet 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.", diff --git a/homeassistant/components/samsungtv/translations/hu.json b/homeassistant/components/samsungtv/translations/hu.json index b53f50ff74a..d4f37d72922 100644 --- a/homeassistant/components/samsungtv/translations/hu.json +++ b/homeassistant/components/samsungtv/translations/hu.json @@ -12,7 +12,7 @@ }, "error": { "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.", - "invalid_pin": "A PIN k\u00f3d \u00e9rv\u00e9nytelen, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra." + "invalid_pin": "A PIN k\u00f3d \u00e9rv\u00e9nytelen, k\u00e9rem, pr\u00f3b\u00e1lja meg \u00fajra." }, "flow_title": "{device}", "step": { @@ -20,7 +20,7 @@ "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." }, "encrypted_pairing": { - "description": "K\u00e9rj\u00fck, adja meg az eszk\u00f6z\u00f6n megjelen\u0151 PIN-k\u00f3dot: {device}" + "description": "K\u00e9rem, adja meg az eszk\u00f6z\u00f6n megjelen\u0151 PIN-k\u00f3dot: {device}" }, "pairing": { "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." @@ -29,7 +29,7 @@ "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, vagy adja meg a PIN k\u00f3dot." }, "reauth_confirm_encrypted": { - "description": "K\u00e9rj\u00fck, adja meg az eszk\u00f6z\u00f6n megjelen\u0151 PIN-k\u00f3dot: {device}" + "description": "K\u00e9rem, adja meg az eszk\u00f6z\u00f6n megjelen\u0151 PIN-k\u00f3dot: {device}" }, "user": { "data": { diff --git a/homeassistant/components/scrape/translations/ru.json b/homeassistant/components/scrape/translations/ru.json new file mode 100644 index 00000000000..2d014592e85 --- /dev/null +++ b/homeassistant/components/scrape/translations/ru.json @@ -0,0 +1,73 @@ +{ + "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." + }, + "step": { + "user": { + "data": { + "attribute": "\u0410\u0442\u0440\u0438\u0431\u0443\u0442", + "authentication": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", + "device_class": "\u041a\u043b\u0430\u0441\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438", + "index": "\u0418\u043d\u0434\u0435\u043a\u0441", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "resource": "\u0420\u0435\u0441\u0443\u0440\u0441", + "select": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c", + "state_class": "\u041a\u043b\u0430\u0441\u0441 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", + "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", + "value_template": "\u0428\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "data_description": { + "attribute": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0442\u0435\u0433\u0430.", + "authentication": "\u0422\u0438\u043f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 HTTP: basic \u0438\u043b\u0438 digest.", + "device_class": "\u0422\u0438\u043f/\u043a\u043b\u0430\u0441\u0441 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435.", + "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0435 \u0434\u043b\u044f \u0432\u0435\u0431-\u0437\u0430\u043f\u0440\u043e\u0441\u0430.", + "index": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043a\u0430\u043a\u043e\u0439 \u0438\u0437 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u044b\u0445 \u0441\u0435\u043b\u0435\u043a\u0442\u043e\u0440\u043e\u043c CSS \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.", + "resource": "URL-\u0430\u0434\u0440\u0435\u0441 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435.", + "select": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043a\u0430\u043a\u043e\u0439 \u0442\u0435\u0433 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043a\u0430\u0442\u044c. \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0438 \u0441\u0435\u043b\u0435\u043a\u0442\u043e\u0440\u043e\u0432 CSS Beautifulsoup.", + "state_class": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 state_class \u0434\u043b\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u0430.", + "value_template": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u0448\u0430\u0431\u043b\u043e\u043d \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u0430.", + "verify_ssl": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442/\u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 SSL/TLS. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u044d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0435\u0441\u043b\u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441\u0430\u043c\u043e\u043f\u043e\u0434\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u0439." + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "attribute": "\u0410\u0442\u0440\u0438\u0431\u0443\u0442", + "authentication": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f", + "device_class": "\u041a\u043b\u0430\u0441\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438", + "index": "\u0418\u043d\u0434\u0435\u043a\u0441", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "resource": "\u0420\u0435\u0441\u0443\u0440\u0441", + "select": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c", + "state_class": "\u041a\u043b\u0430\u0441\u0441 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", + "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f", + "value_template": "\u0428\u0430\u0431\u043b\u043e\u043d \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "data_description": { + "attribute": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0442\u0435\u0433\u0430.", + "authentication": "\u0422\u0438\u043f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 HTTP: basic \u0438\u043b\u0438 digest.", + "device_class": "\u0422\u0438\u043f/\u043a\u043b\u0430\u0441\u0441 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435.", + "headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0435 \u0434\u043b\u044f \u0432\u0435\u0431-\u0437\u0430\u043f\u0440\u043e\u0441\u0430.", + "index": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043a\u0430\u043a\u043e\u0439 \u0438\u0437 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u043c\u044b\u0445 \u0441\u0435\u043b\u0435\u043a\u0442\u043e\u0440\u043e\u043c CSS \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.", + "resource": "URL-\u0430\u0434\u0440\u0435\u0441 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442\u0430, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435.", + "select": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442, \u043a\u0430\u043a\u043e\u0439 \u0442\u0435\u0433 \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043a\u0430\u0442\u044c. \u041f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435 \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0438 \u0441\u0435\u043b\u0435\u043a\u0442\u043e\u0440\u043e\u0432 CSS Beautifulsoup.", + "state_class": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 state_class \u0434\u043b\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u0430.", + "value_template": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u0448\u0430\u0431\u043b\u043e\u043d \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u0430.", + "verify_ssl": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442/\u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430 SSL/TLS. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u044d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u0438\u0433\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0435\u0441\u043b\u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441\u0430\u043c\u043e\u043f\u043e\u0434\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u0439." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sensor.ru.json b/homeassistant/components/sensibo/translations/sensor.ru.json new file mode 100644 index 00000000000..c916a2b22f4 --- /dev/null +++ b/homeassistant/components/sensibo/translations/sensor.ru.json @@ -0,0 +1,8 @@ +{ + "state": { + "sensibo__sensitivity": { + "n": "\u041d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u044b\u0439", + "s": "\u0427\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/hu.json b/homeassistant/components/shelly/translations/hu.json index 18f53ca232d..71a9e96126b 100644 --- a/homeassistant/components/shelly/translations/hu.json +++ b/homeassistant/components/shelly/translations/hu.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "firmware_not_fully_provisioned": "Az eszk\u00f6z nincs teljesen be\u00fczemelve. K\u00e9rj\u00fck, vegye fel a kapcsolatot a Shelly \u00fcgyf\u00e9lszolg\u00e1lat\u00e1val", + "firmware_not_fully_provisioned": "Az eszk\u00f6z be\u00fczemel\u00e9se nem teljes. K\u00e9rem, vegye fel a kapcsolatot a Shelly \u00fcgyf\u00e9lszolg\u00e1lat\u00e1val", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, diff --git a/homeassistant/components/sia/translations/hu.json b/homeassistant/components/sia/translations/hu.json index 63a5208a44c..cd3bf4a9578 100644 --- a/homeassistant/components/sia/translations/hu.json +++ b/homeassistant/components/sia/translations/hu.json @@ -1,9 +1,9 @@ { "config": { "error": { - "invalid_account_format": "A sz\u00e1mla nem hexa\u00e9rt\u00e9k, k\u00e9rj\u00fck, csak a 0-9 \u00e9s az A-F \u00e9rt\u00e9keket haszn\u00e1ljon.", + "invalid_account_format": "A megadott fi\u00f3k nem hexa\u00e9rt\u00e9k, k\u00e9rem, csak a 0-9 \u00e9s az A-F \u00e9rt\u00e9keket haszn\u00e1ljon.", "invalid_account_length": "A fi\u00f3k nem megfelel\u0151 hossz\u00fas\u00e1g\u00fa, 3 \u00e9s 16 karakter k\u00f6z\u00f6tt kell lennie.", - "invalid_key_format": "A kulcs nem hexa\u00e9rt\u00e9k, k\u00e9rj\u00fck, csak a 0-9 \u00e9s az A-F \u00e9rt\u00e9keket haszn\u00e1ljon.", + "invalid_key_format": "A megadott kulcs nem hexa\u00e9rt\u00e9k, k\u00e9rem, csak a 0-9 \u00e9s az A-F \u00e9rt\u00e9keket haszn\u00e1ljon.", "invalid_key_length": "A kulcs nem megfelel\u0151 hossz\u00fas\u00e1g\u00fa, 16, 24 vagy 32 hexa karakterb\u0151l kell \u00e1llnia.", "invalid_ping": "A ping intervallumnak 1 \u00e9s 1440 perc k\u00f6z\u00f6tt kell lennie.", "invalid_zones": "Legal\u00e1bb 1 z\u00f3n\u00e1nak kell lennie.", diff --git a/homeassistant/components/skybell/translations/ru.json b/homeassistant/components/skybell/translations/ru.json new file mode 100644 index 00000000000..8c30e7c8ad6 --- /dev/null +++ b/homeassistant/components/skybell/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.", + "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": { + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/hu.json b/homeassistant/components/smappee/translations/hu.json index 712cbd0951d..f1e0e5012ad 100644 --- a/homeassistant/components/smappee/translations/hu.json +++ b/homeassistant/components/smappee/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "already_configured_local_device": "A helyi eszk\u00f6z\u00f6k m\u00e1r konfigur\u00e1lva vannak. K\u00e9rj\u00fck, el\u0151sz\u00f6r t\u00e1vol\u00edtsa el ezeket, miel\u0151tt konfigur\u00e1lja a felh\u0151alap\u00fa eszk\u00f6zt.", + "already_configured_local_device": "A helyi eszk\u00f6z\u00f6k m\u00e1r konfigur\u00e1lva vannak. K\u00e9rem, el\u0151sz\u00f6r t\u00e1vol\u00edtsa el ezeket a rendszerb\u0151l, miel\u0151tt bekonfigur\u00e1lja \u0151ket felh\u0151 alapon.", "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.", diff --git a/homeassistant/components/smartthings/translations/hu.json b/homeassistant/components/smartthings/translations/hu.json index 90ea748ae33..47698ef528c 100644 --- a/homeassistant/components/smartthings/translations/hu.json +++ b/homeassistant/components/smartthings/translations/hu.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "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.", + "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\u00e9rem, 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": { @@ -9,7 +9,7 @@ "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": "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." + "webhook_error": "SmartThings nem tudta \u00e9rv\u00e9nyes\u00edteni a webhook URL-t. K\u00e9rem, ellen\u0151rizze, hogy a webhook URL el\u00e9rhet\u0151-e az internet fel\u0151l, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra." }, "step": { "authorize": { @@ -19,14 +19,14 @@ "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token" }, - "description": "K\u00e9rj\u00fck, adjon meg egy SmartThings [Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si tokent]({token_url}), amelyet az [utas\u00edt\u00e1sok]({component_url}) alapj\u00e1n hoztak l\u00e9tre. Ezt haszn\u00e1ljuk a Home Assistant integr\u00e1ci\u00f3j\u00e1nak l\u00e9trehoz\u00e1s\u00e1hoz a SmartThings-fi\u00f3kban.", + "description": "K\u00e9rem, adjon meg egy SmartThings [Szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si tokent]({token_url}), amelyet az [utas\u00edt\u00e1sok]({component_url}) alapj\u00e1n hoztak l\u00e9tre. Ezt haszn\u00e1ljuk a Home Assistant integr\u00e1ci\u00f3j\u00e1nak l\u00e9trehoz\u00e1s\u00e1hoz a SmartThings-fi\u00f3kban.", "title": "Adja meg a szem\u00e9lyes hozz\u00e1f\u00e9r\u00e9si Tokent" }, "select_location": { "data": { "location_id": "Elhelyezked\u00e9s" }, - "description": "K\u00e9rj\u00fck, v\u00e1lassza ki azt a SmartThings helyet, amelyet hozz\u00e1 szeretne adni a Home Assistant szolg\u00e1ltat\u00e1shoz. Ezut\u00e1n \u00faj ablakot nyitunk, \u00e9s megk\u00e9rj\u00fck, hogy jelentkezzen be, \u00e9s enged\u00e9lyezze a Home Assistant integr\u00e1ci\u00f3j\u00e1nak telep\u00edt\u00e9s\u00e9t a kiv\u00e1lasztott helyre.", + "description": "K\u00e9rem, v\u00e1lassza ki azt a SmartThings helyet, amelyet hozz\u00e1 szeretne adni a Home Assistant szolg\u00e1ltat\u00e1shoz. Ezut\u00e1n az \u00faj ablakban jelentkezzen be, \u00e9s enged\u00e9lyezze a Home Assistant integr\u00e1ci\u00f3j\u00e1nak telep\u00edt\u00e9s\u00e9t a kiv\u00e1lasztott helyre.", "title": "Hely kiv\u00e1laszt\u00e1sa" }, "user": { diff --git a/homeassistant/components/soma/translations/hu.json b/homeassistant/components/soma/translations/hu.json index 89194a7b204..3cd6dff8c2e 100644 --- a/homeassistant/components/soma/translations/hu.json +++ b/homeassistant/components/soma/translations/hu.json @@ -4,7 +4,7 @@ "already_setup": "Csak egy Soma-fi\u00f3k konfigur\u00e1lhat\u00f3.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "connection_error": "Nem siker\u00fclt csatlakozni.", - "missing_configuration": "A Soma \u00f6sszetev\u0151 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A Soma komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "result_error": "A SOMA Connect hiba\u00e1llapottal v\u00e1laszolt." }, "create_entry": { @@ -16,7 +16,7 @@ "host": "C\u00edm", "port": "Port" }, - "description": "K\u00e9rj\u00fck, adja meg a SOMA Connect csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sait.", + "description": "K\u00e9rem, adja meg a SOMA Connect csatlakoz\u00e1si be\u00e1ll\u00edt\u00e1sait.", "title": "SOMA Connect" } } diff --git a/homeassistant/components/spotify/translations/hu.json b/homeassistant/components/spotify/translations/hu.json index f387ff6bb3a..735c4942fb2 100644 --- a/homeassistant/components/spotify/translations/hu.json +++ b/homeassistant/components/spotify/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 Spotify integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A Spotify integr\u00e1ci\u00f3 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\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", "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/steam_online/translations/hu.json b/homeassistant/components/steam_online/translations/hu.json index a7d6c6a7de4..87e78abedb4 100644 --- a/homeassistant/components/steam_online/translations/hu.json +++ b/homeassistant/components/steam_online/translations/hu.json @@ -26,7 +26,7 @@ }, "options": { "error": { - "unauthorized": "Bar\u00e1ti lista korl\u00e1tozott: K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t arr\u00f3l, hogyan l\u00e1thatja az \u00f6sszes t\u00f6bbi bar\u00e1tot." + "unauthorized": "A bar\u00e1t-lista korl\u00e1tozva van: K\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t arr\u00f3l, hogyan l\u00e1thatja az \u00f6sszes t\u00f6bbi bar\u00e1tj\u00e1t." }, "step": { "init": { diff --git a/homeassistant/components/steam_online/translations/ru.json b/homeassistant/components/steam_online/translations/ru.json index 0da65eda473..b3d84b7f9e6 100644 --- a/homeassistant/components/steam_online/translations/ru.json +++ b/homeassistant/components/steam_online/translations/ru.json @@ -25,6 +25,9 @@ } }, "options": { + "error": { + "unauthorized": "\u0421\u043f\u0438\u0441\u043e\u043a \u0434\u0440\u0443\u0437\u0435\u0439 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d. \u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u043a \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0432\u0441\u0435\u0445 \u0434\u0440\u0443\u0437\u044c\u044f\u0445." + }, "step": { "init": { "data": { diff --git a/homeassistant/components/subaru/translations/hu.json b/homeassistant/components/subaru/translations/hu.json index 42f9f6bb2c9..9194b4e5044 100644 --- a/homeassistant/components/subaru/translations/hu.json +++ b/homeassistant/components/subaru/translations/hu.json @@ -11,14 +11,14 @@ "incorrect_pin": "Helytelen PIN", "incorrect_validation_code": "Helytelen \u00e9rv\u00e9nyes\u00edt\u00e9si k\u00f3d", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "two_factor_request_failed": "A k\u00e9tfaktoros hiteles\u00edt\u00e9si k\u00f3d lek\u00e9r\u00e9se sikertelen, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg \u00fajra" + "two_factor_request_failed": "A k\u00e9tfaktoros hiteles\u00edt\u00e9si k\u00f3d lek\u00e9r\u00e9se sikertelen, k\u00e9rem, pr\u00f3b\u00e1lja meg \u00fajra." }, "step": { "pin": { "data": { "pin": "PIN" }, - "description": "K\u00e9rj\u00fck, adja meg MySubaru PIN-k\u00f3dj\u00e1t\n MEGJEGYZ\u00c9S: A sz\u00e1ml\u00e1n szerepl\u0151 \u00f6sszes j\u00e1rm\u0171nek azonos PIN-k\u00f3ddal kell rendelkeznie", + "description": "K\u00e9rem, adja meg MySubaru PIN-k\u00f3dj\u00e1t\nMEGJEGYZ\u00c9S: A fi\u00f3kban szerepl\u0151 \u00f6sszes j\u00e1rm\u0171nek azonos PIN-k\u00f3ddal kell rendelkeznie", "title": "Subaru Starlink konfigur\u00e1ci\u00f3" }, "two_factor": { @@ -32,7 +32,7 @@ "data": { "validation_code": "\u00c9rv\u00e9nyes\u00edt\u00e9si k\u00f3d" }, - "description": "K\u00e9rj\u00fck, adja meg az \u00e9rv\u00e9nyes\u00edt\u00e9si k\u00f3dot", + "description": "K\u00e9rem, adja meg az \u00e9rv\u00e9nyes\u00edt\u00e9si k\u00f3dot", "title": "Subaru Starlink konfigur\u00e1ci\u00f3" }, "user": { @@ -41,7 +41,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "K\u00e9rj\u00fck, adja meg MySubaru hiteles\u00edt\u0151 adatait\n MEGJEGYZ\u00c9S: A kezdeti be\u00e1ll\u00edt\u00e1s ak\u00e1r 30 m\u00e1sodpercet is ig\u00e9nybe vehetnek", + "description": "K\u00e9rem, adja meg MySubaru hiteles\u00edt\u0151 adatait.\nMEGJEGYZ\u00c9S: A kezdeti be\u00e1ll\u00edt\u00e1s ak\u00e1r 30 m\u00e1sodpercet is ig\u00e9nybe vehetnek.", "title": "Subaru Starlink konfigur\u00e1ci\u00f3" } } diff --git a/homeassistant/components/system_bridge/translations/hu.json b/homeassistant/components/system_bridge/translations/hu.json index c570cd2e3c2..0671bcce90b 100644 --- a/homeassistant/components/system_bridge/translations/hu.json +++ b/homeassistant/components/system_bridge/translations/hu.json @@ -24,7 +24,7 @@ "host": "C\u00edm", "port": "Port" }, - "description": "K\u00e9rj\u00fck, adja meg kapcsolati adatait." + "description": "K\u00e9rem, adja meg kapcsolati adatait." } } } diff --git a/homeassistant/components/tankerkoenig/translations/ru.json b/homeassistant/components/tankerkoenig/translations/ru.json index bf61fddc0c5..d2b34eb264b 100644 --- a/homeassistant/components/tankerkoenig/translations/ru.json +++ b/homeassistant/components/tankerkoenig/translations/ru.json @@ -1,13 +1,19 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "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_stations": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u043d\u0438 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u0432 \u0440\u0430\u0434\u0438\u0443\u0441\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f." }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + }, "select_station": { "data": { "stations": "\u0421\u0442\u0430\u043d\u0446\u0438\u0438" @@ -32,7 +38,8 @@ "init": { "data": { "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f", - "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 \u043a\u0430\u0440\u0442\u0435" + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0430 \u043a\u0430\u0440\u0442\u0435", + "stations": "\u0421\u0442\u0430\u043d\u0446\u0438\u0438" }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Tankerkoenig" } diff --git a/homeassistant/components/tomorrowio/translations/hu.json b/homeassistant/components/tomorrowio/translations/hu.json index d619b6346a4..4d906cd1e03 100644 --- a/homeassistant/components/tomorrowio/translations/hu.json +++ b/homeassistant/components/tomorrowio/translations/hu.json @@ -3,7 +3,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", - "rate_limited": "Jelenleg korl\u00e1tozott a hozz\u00e1f\u00e9r\u00e9s, k\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", + "rate_limited": "Jelenleg korl\u00e1tozott a hozz\u00e1f\u00e9r\u00e9s, k\u00e9rem, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { diff --git a/homeassistant/components/totalconnect/translations/ru.json b/homeassistant/components/totalconnect/translations/ru.json index ee82564033c..ac79e28c1b0 100644 --- a/homeassistant/components/totalconnect/translations/ru.json +++ b/homeassistant/components/totalconnect/translations/ru.json @@ -28,5 +28,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0431\u0430\u0439\u043f\u0430\u0441 \u043f\u0440\u0438 \u043d\u0438\u0437\u043a\u043e\u043c \u0437\u0430\u0440\u044f\u0434\u0435 \u0431\u0430\u0442\u0430\u0440\u0435\u0438" + }, + "description": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u0442\u044c \u0437\u043e\u043d\u044b, \u043a\u043e\u0433\u0434\u0430 \u043e\u043d\u0438 \u0441\u043e\u043e\u0431\u0449\u0430\u044e\u0442 \u043e \u0440\u0430\u0437\u0440\u044f\u0434\u0435 \u0431\u0430\u0442\u0430\u0440\u0435\u0438.", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b TotalConnect" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/hu.json b/homeassistant/components/tractive/translations/hu.json index d0f75a28ed0..5b1d9a35512 100644 --- a/homeassistant/components/tractive/translations/hu.json +++ b/homeassistant/components/tractive/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "reauth_failed_existing": "Nem siker\u00fclt friss\u00edteni a konfigur\u00e1ci\u00f3s bejegyz\u00e9st. K\u00e9rj\u00fck, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra.", + "reauth_failed_existing": "Nem siker\u00fclt friss\u00edteni a konfigur\u00e1ci\u00f3s bejegyz\u00e9st. K\u00e9rem, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra.", "reauth_successful": "Az ism\u00e9telt hiteles\u00edt\u00e9s sikeres volt" }, "error": { diff --git a/homeassistant/components/unifiprotect/translations/it.json b/homeassistant/components/unifiprotect/translations/it.json index 1d0fff95c3a..8ea65342e78 100644 --- a/homeassistant/components/unifiprotect/translations/it.json +++ b/homeassistant/components/unifiprotect/translations/it.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "discovery_started": "Rilevamento " + "discovery_started": "Rilevamento iniziato" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/uptimerobot/translations/hu.json b/homeassistant/components/uptimerobot/translations/hu.json index d17c9bf8ac1..4a607c03303 100644 --- a/homeassistant/components/uptimerobot/translations/hu.json +++ b/homeassistant/components/uptimerobot/translations/hu.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", - "reauth_failed_existing": "Nem siker\u00fclt friss\u00edteni a konfigur\u00e1ci\u00f3s bejegyz\u00e9st. K\u00e9rj\u00fck, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra.", + "reauth_failed_existing": "Nem siker\u00fclt friss\u00edteni a konfigur\u00e1ci\u00f3s bejegyz\u00e9st. K\u00e9rem, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra.", "reauth_successful": "Az ism\u00e9telt hiteles\u00edt\u00e9s sikeres volt", "unknown": "V\u00e1ratlan hiba" }, diff --git a/homeassistant/components/verisure/translations/hu.json b/homeassistant/components/verisure/translations/hu.json index 324033b666a..fbb40f2325a 100644 --- a/homeassistant/components/verisure/translations/hu.json +++ b/homeassistant/components/verisure/translations/hu.json @@ -6,14 +6,21 @@ }, "error": { "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "unknown_mfa": "Ismeretlen hiba t\u00f6rt\u00e9nt a be\u00e1ll\u00edt\u00e1s sor\u00e1n" }, "step": { "installation": { "data": { "giid": "Telep\u00edt\u00e9s" }, - "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." + "description": "Home Assistant t\u00f6bb Verisure telep\u00edt\u00e9st tal\u00e1lt a Saj\u00e1t oldalak fi\u00f3kj\u00e1ban. K\u00e9rem, v\u00e1lassza ki azt a telep\u00edt\u00e9st, amelyet hozz\u00e1 k\u00edv\u00e1nja adni a Home Assistant p\u00e9ld\u00e1ny\u00e1hoz." + }, + "mfa": { + "data": { + "code": "Ellen\u0151rz\u0151 k\u00f3d", + "description": "A fi\u00f3kj\u00e1ban be van \u00e1ll\u00edtva a 2 l\u00e9pcs\u0151s ellen\u0151rz\u00e9s. K\u00e9rem, adja meg a Verisure \u00e1ltal k\u00fcld\u00f6tt ellen\u0151rz\u0151 k\u00f3dot." + } }, "reauth_confirm": { "data": { @@ -22,6 +29,12 @@ "password": "Jelsz\u00f3" } }, + "reauth_mfa": { + "data": { + "code": "Ellen\u0151rz\u0151 k\u00f3d", + "description": "A fi\u00f3kj\u00e1ban be van \u00e1ll\u00edtva a 2 l\u00e9pcs\u0151s ellen\u0151rz\u00e9s. K\u00e9rem, adja meg a Verisure \u00e1ltal k\u00fcld\u00f6tt ellen\u0151rz\u0151 k\u00f3dot." + } + }, "user": { "data": { "description": "Jelentkezzen be a Verisure My Pages fi\u00f3kj\u00e1val.", diff --git a/homeassistant/components/verisure/translations/it.json b/homeassistant/components/verisure/translations/it.json index e30976a37a6..b913a47af03 100644 --- a/homeassistant/components/verisure/translations/it.json +++ b/homeassistant/components/verisure/translations/it.json @@ -15,6 +15,12 @@ }, "description": "Home Assistant ha trovato pi\u00f9 installazioni Verisure nel tuo account My Pages. Per favore, seleziona l'installazione da aggiungere a Home Assistant." }, + "mfa": { + "data": { + "code": "Codice di verifica", + "description": "Il tuo account ha la verifica in due passaggi abilitata. Inserisci il codice di verifica che ti viene inviato da Verisure." + } + }, "reauth_confirm": { "data": { "description": "Autenticati nuovamente con il tuo account Verisure My Pages.", @@ -22,6 +28,11 @@ "password": "Password" } }, + "reauth_mfa": { + "data": { + "code": "Codice di verifica" + } + }, "user": { "data": { "description": "Accedi con il tuo account Verisure My Pages.", diff --git a/homeassistant/components/vlc_telnet/translations/hu.json b/homeassistant/components/vlc_telnet/translations/hu.json index bac42b4d4c3..b1247a74d2e 100644 --- a/homeassistant/components/vlc_telnet/translations/hu.json +++ b/homeassistant/components/vlc_telnet/translations/hu.json @@ -21,7 +21,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "K\u00e9rj\u00fck, adja meg a helyes jelsz\u00f3t: {host}" + "description": "K\u00e9rem, adja meg a helyes jelsz\u00f3t: {host}" }, "user": { "data": { diff --git a/homeassistant/components/vulcan/translations/hu.json b/homeassistant/components/vulcan/translations/hu.json index 65d90d6e24a..953eb90146f 100644 --- a/homeassistant/components/vulcan/translations/hu.json +++ b/homeassistant/components/vulcan/translations/hu.json @@ -3,13 +3,13 @@ "abort": { "all_student_already_configured": "M\u00e1r minden tanul\u00f3 hozz\u00e1adva.", "already_configured": "Ezt a tanul\u00f3t m\u00e1r hozz\u00e1adt\u00e1k.", - "no_matching_entries": "Nem tal\u00e1ltunk megfelel\u0151 bejegyz\u00e9st, k\u00e9rj\u00fck, haszn\u00e1ljon m\u00e1sik fi\u00f3kot, vagy t\u00e1vol\u00edtsa el az elavult di\u00e1kkal val\u00f3 integr\u00e1ci\u00f3t...", + "no_matching_entries": "Nem tal\u00e1lhat\u00f3 megfelel\u0151 bejegyz\u00e9s, k\u00e9rem, haszn\u00e1ljon m\u00e1sik fi\u00f3kot, vagy t\u00e1vol\u00edtsa el az elavult di\u00e1kkal val\u00f3 integr\u00e1ci\u00f3t...", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres" }, "error": { "cannot_connect": "Csatlakoz\u00e1si hiba \u2013 ellen\u0151rizze az internetkapcsolatot", - "expired_credentials": "Lej\u00e1rt hiteles\u00edt\u0151 adatok - k\u00e9rj\u00fck, hozzon l\u00e9tre \u00fajakat a Vulcan mobilalkalmaz\u00e1s regisztr\u00e1ci\u00f3s oldal\u00e1n.", - "expired_token": "Lej\u00e1rt token \u2013 k\u00e9rj\u00fck, hozzon l\u00e9tre egy \u00fajat", + "expired_credentials": "Lej\u00e1rt hiteles\u00edt\u0151 adatok - k\u00e9rem, hozzon l\u00e9tre \u00fajakat a Vulcan mobilalkalmaz\u00e1s regisztr\u00e1ci\u00f3s oldal\u00e1n.", + "expired_token": "Lej\u00e1rt token \u2013 k\u00e9rem, hozzon l\u00e9tre egy \u00fajat", "invalid_pin": "\u00c9rv\u00e9nytelen PIN-k\u00f3d", "invalid_symbol": "\u00c9rv\u00e9nytelen szimb\u00f3lum", "invalid_token": "\u00c9rv\u00e9nytelen token", diff --git a/homeassistant/components/webostv/translations/hu.json b/homeassistant/components/webostv/translations/hu.json index de3f59d8c90..b4ef5f39aa1 100644 --- a/homeassistant/components/webostv/translations/hu.json +++ b/homeassistant/components/webostv/translations/hu.json @@ -6,7 +6,7 @@ "error_pairing": "Csatlakozva az LG webOS TV-hez, de a p\u00e1ros\u00edt\u00e1s nem siker\u00fclt" }, "error": { - "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rj\u00fck, kapcsolja be a TV-t vagy ellen\u0151rizze az ip-c\u00edmet." + "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rem, kapcsolja be a TV-t vagy ellen\u0151rizze az ip-c\u00edmet." }, "flow_title": "LG webOS Smart TV", "step": { diff --git a/homeassistant/components/wiz/translations/hu.json b/homeassistant/components/wiz/translations/hu.json index 0c27ee730b3..90c3b3b35a2 100644 --- a/homeassistant/components/wiz/translations/hu.json +++ b/homeassistant/components/wiz/translations/hu.json @@ -6,7 +6,7 @@ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" }, "error": { - "bulb_time_out": "Nem lehet csatlakoztatni az izz\u00f3hoz. Lehet, hogy az izz\u00f3 offline \u00e1llapotban van, vagy rossz IP-t adott meg. K\u00e9rj\u00fck, kapcsolja fel a l\u00e1mp\u00e1t, \u00e9s pr\u00f3b\u00e1lja \u00fajra!", + "bulb_time_out": "Nem lehet csatlakoztatni az izz\u00f3hoz. Lehet, hogy az izz\u00f3 offline \u00e1llapotban van, vagy rossz IP-t adott meg. K\u00e9rem, kapcsolja fel a l\u00e1mp\u00e1t, \u00e9s pr\u00f3b\u00e1lja \u00fajra!", "cannot_connect": "Sikertelen csatlakoz\u00e1s", "no_ip": "\u00c9rv\u00e9nytelen IP-c\u00edm.", "no_wiz_light": "Az izz\u00f3 nem csatlakoztathat\u00f3 a WiZ Platform integr\u00e1ci\u00f3n kereszt\u00fcl.", diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json index 51f093b871c..a7536283240 100644 --- a/homeassistant/components/xiaomi_miio/translations/hu.json +++ b/homeassistant/components/xiaomi_miio/translations/hu.json @@ -9,7 +9,7 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "cloud_credentials_incomplete": "A felh\u0151alap\u00fa hiteles\u00edt\u0151 adatok hi\u00e1nyosak, k\u00e9rj\u00fck, adja meg a felhaszn\u00e1l\u00f3nevet, a jelsz\u00f3t \u00e9s az orsz\u00e1got", + "cloud_credentials_incomplete": "A felh\u0151alap\u00fa hiteles\u00edt\u0151 adatok hi\u00e1nyosak, k\u00e9rem, adja meg a felhaszn\u00e1l\u00f3nevet, a jelsz\u00f3t \u00e9s az orsz\u00e1got", "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.", "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.", @@ -52,7 +52,7 @@ }, "options": { "error": { - "cloud_credentials_incomplete": "A felh\u0151alap\u00fa hiteles\u00edt\u0151 adatok hi\u00e1nyosak, k\u00e9rj\u00fck, adja meg a felhaszn\u00e1l\u00f3nevet, a jelsz\u00f3t \u00e9s az orsz\u00e1got" + "cloud_credentials_incomplete": "A felh\u0151alap\u00fa hiteles\u00edt\u0151 adatok hi\u00e1nyosak, k\u00e9rem, adja meg a felhaszn\u00e1l\u00f3nevet, a jelsz\u00f3t \u00e9s az orsz\u00e1got" }, "step": { "init": { diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json index 37e19e5471f..f93ff278c5f 100644 --- a/homeassistant/components/zwave_js/translations/hu.json +++ b/homeassistant/components/zwave_js/translations/hu.json @@ -100,7 +100,7 @@ "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", "cannot_connect": "Nem siker\u00fclt csatlakozni", - "different_device": "A csatlakoztatott USB-eszk\u00f6z nem ugyanaz, mint amelyet kor\u00e1bban ehhez a konfigur\u00e1ci\u00f3s bejegyz\u00e9shez konfigur\u00e1ltak. K\u00e9rj\u00fck, ink\u00e1bb hozzon l\u00e9tre egy \u00faj konfigur\u00e1ci\u00f3s bejegyz\u00e9st az \u00faj eszk\u00f6zh\u00f6z." + "different_device": "A csatlakoztatott USB-eszk\u00f6z nem ugyanaz, mint amelyet kor\u00e1bban ehhez a konfigur\u00e1ci\u00f3s bejegyz\u00e9shez konfigur\u00e1ltak. K\u00e9rem, ink\u00e1bb hozzon l\u00e9tre egy \u00faj konfigur\u00e1ci\u00f3s bejegyz\u00e9st az \u00faj eszk\u00f6zh\u00f6z." }, "error": { "cannot_connect": "Nem siker\u00fclt csatlakozni", From 46551a50eff5c20ed9cd99b0eeea1ead15de108a Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Thu, 21 Jul 2022 03:04:04 -0400 Subject: [PATCH 2760/3516] Bump pymazda to 0.3.7 (#75546) --- 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 acf5282689f..d521bc748e0 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.3.6"], + "requirements": ["pymazda==0.3.7"], "codeowners": ["@bdr99"], "quality_scale": "platinum", "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 6e525274af2..26d7c6bce14 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1645,7 +1645,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.6 +pymazda==0.3.7 # homeassistant.components.mediaroom pymediaroom==0.6.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf07a07117e..48826ebdf1a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1124,7 +1124,7 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.3.6 +pymazda==0.3.7 # homeassistant.components.melcloud pymelcloud==2.5.6 From c861259749e8a6f421b5bfdf63afeb03974a447c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 21 Jul 2022 10:56:53 +0200 Subject: [PATCH 2761/3516] Hide inactive repairs issues (#75556) --- .../components/repairs/websocket_api.py | 1 + .../components/repairs/test_websocket_api.py | 24 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/repairs/websocket_api.py b/homeassistant/components/repairs/websocket_api.py index 2e4e166f3e9..b6a71773273 100644 --- a/homeassistant/components/repairs/websocket_api.py +++ b/homeassistant/components/repairs/websocket_api.py @@ -73,6 +73,7 @@ def ws_list_issues( issues = [ dataclasses.asdict(issue, dict_factory=ws_dict) for issue in issue_registry.issues.values() + if issue.active ] connection.send_result(msg["id"], {"issues": issues}) diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index 388912e0adc..73d1898fcb7 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -9,7 +9,11 @@ import pytest import voluptuous as vol from homeassistant import data_entry_flow -from homeassistant.components.repairs import RepairsFlow, async_create_issue +from homeassistant.components.repairs import ( + RepairsFlow, + async_create_issue, + issue_registry, +) from homeassistant.components.repairs.const import DOMAIN from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant @@ -366,8 +370,24 @@ async def test_step_unauth( @freeze_time("2022-07-19 07:53:05") -async def test_list_issues(hass: HomeAssistant, hass_ws_client) -> None: +async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> None: """Test we can list issues.""" + + # Add an inactive issue, this should not be exposed in the list + hass_storage[issue_registry.STORAGE_KEY] = { + "version": issue_registry.STORAGE_VERSION, + "data": { + "issues": [ + { + "created": "2022-07-19T09:41:13.746514+00:00", + "dismissed_version": None, + "domain": "test", + "issue_id": "issue_3_inactive", + }, + ] + }, + } + assert await async_setup_component(hass, DOMAIN, {}) client = await hass_ws_client(hass) From baeb55e313074e545e7d9e55ae0a9cbdb2c3e571 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 21 Jul 2022 11:11:51 +0200 Subject: [PATCH 2762/3516] Add sync methods for create/deleting issues in repairs (#75557) --- homeassistant/components/repairs/__init__.py | 9 ++- .../components/repairs/issue_handler.py | 42 +++++++++++ tests/components/repairs/test_init.py | 73 ++++++++++++++++++- 3 files changed, 122 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/repairs/__init__.py b/homeassistant/components/repairs/__init__.py index f9a478514aa..4471def0dcd 100644 --- a/homeassistant/components/repairs/__init__.py +++ b/homeassistant/components/repairs/__init__.py @@ -6,13 +6,20 @@ from homeassistant.helpers.typing import ConfigType from . import issue_handler, websocket_api from .const import DOMAIN -from .issue_handler import async_create_issue, async_delete_issue +from .issue_handler import ( + async_create_issue, + async_delete_issue, + create_issue, + delete_issue, +) from .issue_registry import async_load as async_load_issue_registry from .models import IssueSeverity, RepairsFlow __all__ = [ "async_create_issue", "async_delete_issue", + "create_issue", + "delete_issue", "DOMAIN", "IssueSeverity", "RepairsFlow", diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index ff23c1c70a4..8eff4ac64fe 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -1,6 +1,7 @@ """The repairs integration.""" from __future__ import annotations +import functools as ft from typing import Any from awesomeversion import AwesomeVersion, AwesomeVersionStrategy @@ -11,6 +12,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.integration_platform import ( async_process_integration_platforms, ) +from homeassistant.util.async_ import run_callback_threadsafe from .const import DOMAIN from .issue_registry import async_get as async_get_issue_registry @@ -113,6 +115,36 @@ def async_create_issue( ) +def create_issue( + hass: HomeAssistant, + domain: str, + issue_id: str, + *, + breaks_in_ha_version: str | None = None, + is_fixable: bool, + learn_more_url: str | None = None, + severity: IssueSeverity, + translation_key: str, + translation_placeholders: dict[str, str] | None = None, +) -> None: + """Create an issue, or replace an existing one.""" + return run_callback_threadsafe( + hass.loop, + ft.partial( + async_create_issue, + hass, + domain, + issue_id, + breaks_in_ha_version=breaks_in_ha_version, + is_fixable=is_fixable, + learn_more_url=learn_more_url, + severity=severity, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ), + ).result() + + @callback def async_delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: """Delete an issue. @@ -123,6 +155,16 @@ def async_delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: issue_registry.async_delete(domain, issue_id) +def delete_issue(hass: HomeAssistant, domain: str, issue_id: str) -> None: + """Delete an issue. + + It is not an error to delete an issue that does not exist. + """ + return run_callback_threadsafe( + hass.loop, async_delete_issue, hass, domain, issue_id + ).result() + + @callback def async_ignore_issue( hass: HomeAssistant, domain: str, issue_id: str, ignore: bool diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py index fbf240e740d..2f82a084968 100644 --- a/tests/components/repairs/test_init.py +++ b/tests/components/repairs/test_init.py @@ -1,15 +1,23 @@ """Test the repairs websocket API.""" +from collections.abc import Awaitable, Callable from unittest.mock import AsyncMock, Mock +from aiohttp import ClientWebSocketResponse from freezegun import freeze_time import pytest -from homeassistant.components.repairs import async_create_issue, async_delete_issue +from homeassistant.components.repairs import ( + async_create_issue, + async_delete_issue, + create_issue, + delete_issue, +) from homeassistant.components.repairs.const import DOMAIN from homeassistant.components.repairs.issue_handler import ( async_ignore_issue, async_process_repairs_platforms, ) +from homeassistant.components.repairs.models import IssueSeverity from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -446,3 +454,66 @@ async def test_non_compliant_platform(hass: HomeAssistant, hass_ws_client) -> No await async_process_repairs_platforms(hass) assert list(hass.data[DOMAIN]["platforms"].keys()) == ["fake_integration"] + + +@freeze_time("2022-07-21 08:22:00") +async def test_sync_methods( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +) -> None: + """Test sync method for creating and deleting an issue.""" + + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} + + def _create_issue() -> None: + create_issue( + hass, + "fake_integration", + "sync_issue", + breaks_in_ha_version="2022.9", + is_fixable=True, + learn_more_url="https://theuselessweb.com", + severity=IssueSeverity.ERROR, + translation_key="abc_123", + translation_placeholders={"abc": "123"}, + ) + + await hass.async_add_executor_job(_create_issue) + await client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + { + "breaks_in_ha_version": "2022.9", + "created": "2022-07-21T08:22:00+00:00", + "dismissed_version": None, + "domain": "fake_integration", + "ignored": False, + "is_fixable": True, + "issue_id": "sync_issue", + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + } + ] + } + + await hass.async_add_executor_job( + delete_issue, hass, "fake_integration", "sync_issue" + ) + await client.send_json({"id": 3, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + assert msg["success"] + assert msg["result"] == {"issues": []} From 41f63839571643b09b0cd7ea55375ea6c4f3abe9 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 21 Jul 2022 12:24:29 +0200 Subject: [PATCH 2763/3516] Update icons for breaking changes in MDI 7.0.96 (#75560) --- homeassistant/components/huawei_lte/sensor.py | 16 ++++++++-------- .../components/icloud/device_tracker.py | 2 +- homeassistant/components/netgear/const.py | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 44d03677ebd..c4cce70cbb7 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -299,7 +299,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { ) ), (KEY_MONITORING_CHECK_NOTIFICATIONS, "UnreadMessage"): SensorMeta( - name="SMS unread", icon="mdi:email-receive" + name="SMS unread", icon="mdi:email-arrow-left" ), KEY_MONITORING_MONTH_STATISTICS: SensorMeta( exclude=re.compile(r"^month(duration|lastcleartime)$", re.IGNORECASE) @@ -450,7 +450,7 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { ), (KEY_SMS_SMS_COUNT, "LocalDraft"): SensorMeta( name="SMS drafts (device)", - icon="mdi:email-send-outline", + icon="mdi:email-arrow-right-outline", ), (KEY_SMS_SMS_COUNT, "LocalInbox"): SensorMeta( name="SMS inbox (device)", @@ -462,15 +462,15 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { ), (KEY_SMS_SMS_COUNT, "LocalOutbox"): SensorMeta( name="SMS outbox (device)", - icon="mdi:email-send", + icon="mdi:email-arrow-right", ), (KEY_SMS_SMS_COUNT, "LocalUnread"): SensorMeta( name="SMS unread (device)", - icon="mdi:email-receive", + icon="mdi:email-arrow-left", ), (KEY_SMS_SMS_COUNT, "SimDraft"): SensorMeta( name="SMS drafts (SIM)", - icon="mdi:email-send-outline", + icon="mdi:email-arrow-right-outline", ), (KEY_SMS_SMS_COUNT, "SimInbox"): SensorMeta( name="SMS inbox (SIM)", @@ -482,15 +482,15 @@ SENSOR_META: dict[str | tuple[str, str], SensorMeta] = { ), (KEY_SMS_SMS_COUNT, "SimOutbox"): SensorMeta( name="SMS outbox (SIM)", - icon="mdi:email-send", + icon="mdi:email-arrow-right", ), (KEY_SMS_SMS_COUNT, "SimUnread"): SensorMeta( name="SMS unread (SIM)", - icon="mdi:email-receive", + icon="mdi:email-arrow-left", ), (KEY_SMS_SMS_COUNT, "SimUsed"): SensorMeta( name="SMS messages (SIM)", - icon="mdi:email-receive", + icon="mdi:email-arrow-left", ), } diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 6886d500a84..9c2004f0edb 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -148,7 +148,7 @@ def icon_for_icloud_device(icloud_device: IcloudDevice) -> str: "iPad": "mdi:tablet", "iPhone": "mdi:cellphone", "iPod": "mdi:ipod", - "iMac": "mdi:desktop-mac", + "iMac": "mdi:monitor", "MacBookPro": "mdi:laptop", } diff --git a/homeassistant/components/netgear/const.py b/homeassistant/components/netgear/const.py index eaa32362baf..cf6cc827519 100644 --- a/homeassistant/components/netgear/const.py +++ b/homeassistant/components/netgear/const.py @@ -78,7 +78,7 @@ DEVICE_ICONS = { 1: "mdi:book-open-variant", # Amazon Kindle 2: "mdi:android", # Android Device 3: "mdi:cellphone", # Android Phone - 4: "mdi:tablet-android", # Android Tablet + 4: "mdi:tablet", # Android Tablet 5: "mdi:router-wireless", # Apple Airport Express 6: "mdi:disc-player", # Blu-ray Player 7: "mdi:router-network", # Bridge @@ -87,7 +87,7 @@ DEVICE_ICONS = { 10: "mdi:router-network", # Router 11: "mdi:play-network", # DVR 12: "mdi:gamepad-variant", # Gaming Console - 13: "mdi:desktop-mac", # iMac + 13: "mdi:monitor", # iMac 14: "mdi:tablet", # iPad 15: "mdi:tablet", # iPad Mini 16: "mdi:cellphone", # iPhone 5/5S/5C From 1d7d2875e154220547e5f9cb830f75e450ac2d7b Mon Sep 17 00:00:00 2001 From: Thibault Cohen <47721+titilambert@users.noreply.github.com> Date: Thu, 21 Jul 2022 06:36:49 -0400 Subject: [PATCH 2764/3516] Add websocket command recorder/import_statistics (#73937) * Expose ws_add_external_statistics in websocket API * Refactor * Add tests * Improve test coverage Co-authored-by: Thibault Cohen Co-authored-by: Erik --- homeassistant/components/recorder/core.py | 8 +- .../components/recorder/statistics.py | 67 ++++- homeassistant/components/recorder/tasks.py | 8 +- .../components/recorder/websocket_api.py | 51 +++- tests/components/recorder/test_statistics.py | 207 ++++++++++--- .../components/recorder/test_websocket_api.py | 275 +++++++++++++++++- 6 files changed, 547 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 49e870b658f..f3ae79f9909 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -74,7 +74,7 @@ from .tasks import ( CommitTask, DatabaseLockTask, EventTask, - ExternalStatisticsTask, + ImportStatisticsTask, KeepAliveTask, PerodicCleanupTask, PurgeTask, @@ -480,11 +480,11 @@ class Recorder(threading.Thread): ) @callback - def async_external_statistics( + def async_import_statistics( self, metadata: StatisticMetaData, stats: Iterable[StatisticData] ) -> None: - """Schedule external statistics.""" - self.queue_task(ExternalStatisticsTask(metadata, stats)) + """Schedule import of statistics.""" + self.queue_task(ImportStatisticsTask(metadata, stats)) @callback def _async_setup_periodic_tasks(self) -> None: diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 26221aa199b..4ebd5e17902 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -29,7 +29,7 @@ from homeassistant.const import ( VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback, valid_entity_id from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry from homeassistant.helpers.json import JSONEncoder @@ -1360,6 +1360,54 @@ def _statistics_exists( return result["id"] if result else None +@callback +def _async_import_statistics( + hass: HomeAssistant, + metadata: StatisticMetaData, + statistics: Iterable[StatisticData], +) -> None: + """Validate timestamps and insert an import_statistics job in the recorder's queue.""" + 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) + + if "last_reset" in statistic and statistic["last_reset"] is not None: + last_reset = statistic["last_reset"] + if ( + last_reset.tzinfo is None + or last_reset.tzinfo.utcoffset(last_reset) is None + ): + raise HomeAssistantError("Naive timestamp") + statistic["last_reset"] = dt_util.as_utc(last_reset) + + # Insert job in recorder's queue + hass.data[DATA_INSTANCE].async_import_statistics(metadata, statistics) + + +@callback +def async_import_statistics( + hass: HomeAssistant, + metadata: StatisticMetaData, + statistics: Iterable[StatisticData], +) -> None: + """Import hourly statistics from an internal source. + + This inserts an import_statistics job in the recorder's queue. + """ + if not valid_entity_id(metadata["statistic_id"]): + raise HomeAssistantError("Invalid statistic_id") + + # The source must not be empty and must be aligned with the statistic_id + if not metadata["source"] or metadata["source"] != DOMAIN: + raise HomeAssistantError("Invalid source") + + _async_import_statistics(hass, metadata, statistics) + + @callback def async_add_external_statistics( hass: HomeAssistant, @@ -1368,7 +1416,7 @@ def async_add_external_statistics( ) -> None: """Add hourly statistics from an external source. - This inserts an add_external_statistics job in the recorder's queue. + This inserts an import_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"]): @@ -1379,16 +1427,7 @@ def async_add_external_statistics( 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) + _async_import_statistics(hass, metadata, statistics) def _filter_unique_constraint_integrity_error( @@ -1432,12 +1471,12 @@ def _filter_unique_constraint_integrity_error( @retryable_database_job("statistics") -def add_external_statistics( +def import_statistics( instance: Recorder, metadata: StatisticMetaData, statistics: Iterable[StatisticData], ) -> bool: - """Process an add_external_statistics job.""" + """Process an import_statistics job.""" with session_scope( session=instance.get_session(), diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index 5ec83a3cefc..6d1c9c360ab 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -124,18 +124,18 @@ class StatisticsTask(RecorderTask): @dataclass -class ExternalStatisticsTask(RecorderTask): - """An object to insert into the recorder queue to run an external statistics task.""" +class ImportStatisticsTask(RecorderTask): + """An object to insert into the recorder queue to run an import statistics task.""" metadata: StatisticMetaData statistics: Iterable[StatisticData] def run(self, instance: Recorder) -> None: """Run statistics task.""" - if statistics.add_external_statistics(instance, self.metadata, self.statistics): + if statistics.import_statistics(instance, self.metadata, self.statistics): return # Schedule a new statistics task if this one didn't finish - instance.queue_task(ExternalStatisticsTask(self.metadata, self.statistics)) + instance.queue_task(ImportStatisticsTask(self.metadata, self.statistics)) @dataclass diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 45e2cf5620b..4ba5f3c8a8b 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -7,11 +7,17 @@ from typing import TYPE_CHECKING import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback, valid_entity_id +from homeassistant.helpers import config_validation as cv from homeassistant.util import dt as dt_util from .const import DATA_INSTANCE, MAX_QUEUE_BACKLOG -from .statistics import list_statistic_ids, validate_statistics +from .statistics import ( + async_add_external_statistics, + async_import_statistics, + list_statistic_ids, + validate_statistics, +) from .util import async_migration_in_progress if TYPE_CHECKING: @@ -31,6 +37,7 @@ def async_setup(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, ws_backup_start) websocket_api.async_register_command(hass, ws_backup_end) websocket_api.async_register_command(hass, ws_adjust_sum_statistics) + websocket_api.async_register_command(hass, ws_import_statistics) @websocket_api.websocket_command( @@ -136,6 +143,46 @@ def ws_adjust_sum_statistics( connection.send_result(msg["id"]) +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required("type"): "recorder/import_statistics", + vol.Required("metadata"): { + vol.Required("has_mean"): bool, + vol.Required("has_sum"): bool, + vol.Required("name"): vol.Any(str, None), + vol.Required("source"): str, + vol.Required("statistic_id"): str, + vol.Required("unit_of_measurement"): vol.Any(str, None), + }, + vol.Required("stats"): [ + { + vol.Required("start"): cv.datetime, + vol.Optional("mean"): vol.Any(float, int), + vol.Optional("min"): vol.Any(float, int), + vol.Optional("max"): vol.Any(float, int), + vol.Optional("last_reset"): vol.Any(cv.datetime, None), + vol.Optional("state"): vol.Any(float, int), + vol.Optional("sum"): vol.Any(float, int), + } + ], + } +) +@callback +def ws_import_statistics( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict +) -> None: + """Adjust sum statistics.""" + metadata = msg["metadata"] + stats = msg["stats"] + + if valid_entity_id(metadata["statistic_id"]): + async_import_statistics(hass, metadata, stats) + else: + async_add_external_statistics(hass, metadata, stats) + connection.send_result(msg["id"]) + + @websocket_api.websocket_command( { vol.Required("type"): "recorder/info", diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 48639790d0d..30a2926844b 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -17,6 +17,7 @@ from homeassistant.components.recorder.db_schema import StatisticsShortTerm from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat from homeassistant.components.recorder.statistics import ( async_add_external_statistics, + async_import_statistics, delete_statistics_duplicates, delete_statistics_meta_duplicates, get_last_short_term_statistics, @@ -437,26 +438,45 @@ def test_statistics_duplicated(hass_recorder, caplog): caplog.clear() -async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): - """Test inserting external statistics.""" +@pytest.mark.parametrize("last_reset_str", ("2022-01-01T00:00:00+02:00", None)) +@pytest.mark.parametrize( + "source, statistic_id, import_fn", + ( + ("test", "test:total_energy_import", async_add_external_statistics), + ("recorder", "sensor.total_energy_import", async_import_statistics), + ), +) +async def test_import_statistics( + hass, + hass_ws_client, + recorder_mock, + caplog, + source, + statistic_id, + import_fn, + last_reset_str, +): + """Test importing statistics and inserting external statistics.""" client = await hass_ws_client() assert "Compiling statistics for" not in caplog.text assert "Statistics already compiled" not in caplog.text zero = dt_util.utcnow() + last_reset = dt_util.parse_datetime(last_reset_str) if last_reset_str else None + last_reset_utc_str = dt_util.as_utc(last_reset).isoformat() if last_reset else None 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_statistics1 = { "start": period1, - "last_reset": None, + "last_reset": last_reset, "state": 0, "sum": 2, } external_statistics2 = { "start": period2, - "last_reset": None, + "last_reset": last_reset, "state": 1, "sum": 3, } @@ -465,37 +485,35 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): "has_mean": False, "has_sum": True, "name": "Total imported energy", - "source": "test", - "statistic_id": "test:total_energy_import", + "source": source, + "statistic_id": statistic_id, "unit_of_measurement": "kWh", } - async_add_external_statistics( - hass, external_metadata, (external_statistics1, external_statistics2) - ) + import_fn(hass, external_metadata, (external_statistics1, external_statistics2)) await async_wait_recording_done(hass) stats = statistics_during_period(hass, zero, period="hour") assert stats == { - "test:total_energy_import": [ + statistic_id: [ { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period1.isoformat(), "end": (period1 + timedelta(hours=1)).isoformat(), "max": None, "mean": None, "min": None, - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(0.0), "sum": approx(2.0), }, { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period2.isoformat(), "end": (period2 + timedelta(hours=1)).isoformat(), "max": None, "mean": None, "min": None, - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(1.0), "sum": approx(3.0), }, @@ -506,37 +524,37 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): { "has_mean": False, "has_sum": True, - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "name": "Total imported energy", - "source": "test", + "source": source, "unit_of_measurement": "kWh", } ] - metadata = get_metadata(hass, statistic_ids=("test:total_energy_import",)) + metadata = get_metadata(hass, statistic_ids=(statistic_id,)) assert metadata == { - "test:total_energy_import": ( + statistic_id: ( 1, { "has_mean": False, "has_sum": True, "name": "Total imported energy", - "source": "test", - "statistic_id": "test:total_energy_import", + "source": source, + "statistic_id": statistic_id, "unit_of_measurement": "kWh", }, ) } - last_stats = get_last_statistics(hass, 1, "test:total_energy_import", True) + last_stats = get_last_statistics(hass, 1, statistic_id, True) assert last_stats == { - "test:total_energy_import": [ + statistic_id: [ { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period2.isoformat(), "end": (period2 + timedelta(hours=1)).isoformat(), "max": None, "mean": None, "min": None, - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(1.0), "sum": approx(3.0), }, @@ -550,13 +568,13 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): "state": 5, "sum": 6, } - async_add_external_statistics(hass, external_metadata, (external_statistics,)) + import_fn(hass, external_metadata, (external_statistics,)) await async_wait_recording_done(hass) stats = statistics_during_period(hass, zero, period="hour") assert stats == { - "test:total_energy_import": [ + statistic_id: [ { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period1.isoformat(), "end": (period1 + timedelta(hours=1)).isoformat(), "max": None, @@ -567,13 +585,13 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): "sum": approx(6.0), }, { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period2.isoformat(), "end": (period2 + timedelta(hours=1)).isoformat(), "max": None, "mean": None, "min": None, - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(1.0), "sum": approx(3.0), }, @@ -586,34 +604,34 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): "max": 1, "mean": 2, "min": 3, - "last_reset": None, + "last_reset": last_reset, "state": 4, "sum": 5, } - async_add_external_statistics(hass, external_metadata, (external_statistics,)) + import_fn(hass, external_metadata, (external_statistics,)) await async_wait_recording_done(hass) stats = statistics_during_period(hass, zero, period="hour") assert stats == { - "test:total_energy_import": [ + statistic_id: [ { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period1.isoformat(), "end": (period1 + timedelta(hours=1)).isoformat(), "max": approx(1.0), "mean": approx(2.0), "min": approx(3.0), - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(4.0), "sum": approx(5.0), }, { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period2.isoformat(), "end": (period2 + timedelta(hours=1)).isoformat(), "max": None, "mean": None, "min": None, - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(1.0), "sum": approx(3.0), }, @@ -624,7 +642,7 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): { "id": 1, "type": "recorder/adjust_sum_statistics", - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start_time": period2.isoformat(), "adjustment": 1000.0, } @@ -635,26 +653,26 @@ async def test_external_statistics(hass, hass_ws_client, recorder_mock, caplog): await async_wait_recording_done(hass) stats = statistics_during_period(hass, zero, period="hour") assert stats == { - "test:total_energy_import": [ + statistic_id: [ { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period1.isoformat(), "end": (period1 + timedelta(hours=1)).isoformat(), "max": approx(1.0), "mean": approx(2.0), "min": approx(3.0), - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(4.0), "sum": approx(5.0), }, { - "statistic_id": "test:total_energy_import", + "statistic_id": statistic_id, "start": period2.isoformat(), "end": (period2 + timedelta(hours=1)).isoformat(), "max": None, "mean": None, "min": None, - "last_reset": None, + "last_reset": last_reset_utc_str, "state": approx(1.0), "sum": approx(1003.0), }, @@ -670,11 +688,12 @@ def test_external_statistics_errors(hass_recorder, caplog): assert "Statistics already compiled" not in caplog.text zero = dt_util.utcnow() + last_reset = zero.replace(minute=0, second=0, microsecond=0) - timedelta(days=1) period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) _external_statistics = { "start": period1, - "last_reset": None, + "last_reset": last_reset, "state": 0, "sum": 2, } @@ -711,7 +730,7 @@ def test_external_statistics_errors(hass_recorder, caplog): assert list_statistic_ids(hass) == [] assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} - # Attempt to insert statistics for an naive starting time + # Attempt to insert statistics for a naive starting time external_metadata = {**_external_metadata} external_statistics = { **_external_statistics, @@ -734,6 +753,106 @@ def test_external_statistics_errors(hass_recorder, caplog): assert list_statistic_ids(hass) == [] assert get_metadata(hass, statistic_ids=("test:total_energy_import",)) == {} + # Attempt to insert statistics with a naive last_reset + external_metadata = {**_external_metadata} + external_statistics = { + **_external_statistics, + "last_reset": last_reset.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",)) == {} + + +def test_import_statistics_errors(hass_recorder, caplog): + """Test validation of imported 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() + last_reset = zero.replace(minute=0, second=0, microsecond=0) - timedelta(days=1) + period1 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1) + + _external_statistics = { + "start": period1, + "last_reset": last_reset, + "state": 0, + "sum": 2, + } + + _external_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "recorder", + "statistic_id": "sensor.total_energy_import", + "unit_of_measurement": "kWh", + } + + # Attempt to insert statistics for an external source + external_metadata = { + **_external_metadata, + "statistic_id": "test:total_energy_import", + } + external_statistics = {**_external_statistics} + with pytest.raises(HomeAssistantError): + async_import_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 the wrong domain + external_metadata = {**_external_metadata, "source": "sensor"} + external_statistics = {**_external_statistics} + with pytest.raises(HomeAssistantError): + async_import_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 a naive starting time + external_metadata = {**_external_metadata} + external_statistics = { + **_external_statistics, + "start": period1.replace(tzinfo=None), + } + with pytest.raises(HomeAssistantError): + async_import_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 an invalid starting time + external_metadata = {**_external_metadata} + external_statistics = {**_external_statistics, "start": period1.replace(minute=1)} + with pytest.raises(HomeAssistantError): + async_import_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 with a naive last_reset + external_metadata = {**_external_metadata} + external_statistics = { + **_external_statistics, + "last_reset": last_reset.replace(tzinfo=None), + } + with pytest.raises(HomeAssistantError): + async_import_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",)) == {} + @pytest.mark.parametrize("timezone", ["America/Regina", "Europe/Vienna", "UTC"]) @pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 55bbb13898e..a7ac01cf1d1 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -9,7 +9,13 @@ from pytest import approx from homeassistant.components import recorder from homeassistant.components.recorder.const import DATA_INSTANCE -from homeassistant.components.recorder.statistics import async_add_external_statistics +from homeassistant.components.recorder.statistics import ( + async_add_external_statistics, + get_last_statistics, + get_metadata, + list_statistic_ids, + statistics_during_period, +) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM @@ -547,3 +553,270 @@ async def test_get_statistics_metadata( "unit_of_measurement": unit, } ] + + +@pytest.mark.parametrize( + "source, statistic_id", + ( + ("test", "test:total_energy_import"), + ("recorder", "sensor.total_energy_import"), + ), +) +async def test_import_statistics( + hass, hass_ws_client, recorder_mock, caplog, source, statistic_id +): + """Test importing statistics.""" + client = await hass_ws_client() + + 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) + period2 = zero.replace(minute=0, second=0, microsecond=0) + timedelta(hours=2) + + external_statistics1 = { + "start": period1.isoformat(), + "last_reset": None, + "state": 0, + "sum": 2, + } + external_statistics2 = { + "start": period2.isoformat(), + "last_reset": None, + "state": 1, + "sum": 3, + } + + external_metadata = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": source, + "statistic_id": statistic_id, + "unit_of_measurement": "kWh", + } + + await client.send_json( + { + "id": 1, + "type": "recorder/import_statistics", + "metadata": external_metadata, + "stats": [external_statistics1, external_statistics2], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] is None + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "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_id": statistic_id, + "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) # TODO + assert statistic_ids == [ + { + "has_mean": False, + "has_sum": True, + "statistic_id": statistic_id, + "name": "Total imported energy", + "source": source, + "unit_of_measurement": "kWh", + } + ] + metadata = get_metadata(hass, statistic_ids=(statistic_id,)) + assert metadata == { + statistic_id: ( + 1, + { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": source, + "statistic_id": statistic_id, + "unit_of_measurement": "kWh", + }, + ) + } + last_stats = get_last_statistics(hass, 1, statistic_id, True) + assert last_stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "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.isoformat(), + "last_reset": None, + "state": 5, + "sum": 6, + } + + await client.send_json( + { + "id": 2, + "type": "recorder/import_statistics", + "metadata": external_metadata, + "stats": [external_statistics], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] is None + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "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": statistic_id, + "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.isoformat(), + "max": 1, + "mean": 2, + "min": 3, + "last_reset": None, + "state": 4, + "sum": 5, + } + + await client.send_json( + { + "id": 3, + "type": "recorder/import_statistics", + "metadata": external_metadata, + "stats": [external_statistics], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] is None + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "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), + }, + { + "statistic_id": statistic_id, + "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), + }, + ] + } + + await client.send_json( + { + "id": 4, + "type": "recorder/adjust_sum_statistics", + "statistic_id": statistic_id, + "start_time": period2.isoformat(), + "adjustment": 1000.0, + } + ) + response = await client.receive_json() + assert response["success"] + + await async_wait_recording_done(hass) + stats = statistics_during_period(hass, zero, period="hour") + assert stats == { + statistic_id: [ + { + "statistic_id": statistic_id, + "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), + }, + { + "statistic_id": statistic_id, + "start": period2.isoformat(), + "end": (period2 + timedelta(hours=1)).isoformat(), + "max": None, + "mean": None, + "min": None, + "last_reset": None, + "state": approx(1.0), + "sum": approx(1003.0), + }, + ] + } From b1ed1543c8d17c2e1c936ae80a0671f7d35e4c99 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 21 Jul 2022 13:07:42 +0200 Subject: [PATCH 2765/3516] Improve http decorator typing (#75541) --- homeassistant/components/auth/login_flow.py | 2 +- homeassistant/components/http/ban.py | 18 ++++++---- .../components/http/data_validator.py | 33 ++++++++++++------- .../components/repairs/websocket_api.py | 4 +-- homeassistant/helpers/data_entry_flow.py | 2 +- 5 files changed, 37 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index b24da92afdd..6cc9d94c7a6 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -257,7 +257,7 @@ class LoginFlowResourceView(LoginFlowBaseView): @RequestDataValidator(vol.Schema({"client_id": str}, extra=vol.ALLOW_EXTRA)) @log_invalid_auth - async def post(self, request, flow_id, data): + async def post(self, request, data, flow_id): """Handle progressing a login flow request.""" client_id = data.pop("client_id") diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index ee8324b2791..d2f5f9d8ba5 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -2,17 +2,18 @@ from __future__ import annotations from collections import defaultdict -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Coroutine from contextlib import suppress from datetime import datetime from http import HTTPStatus from ipaddress import IPv4Address, IPv6Address, ip_address import logging from socket import gethostbyaddr, herror -from typing import Any, Final +from typing import Any, Final, TypeVar -from aiohttp.web import Application, Request, StreamResponse, middleware +from aiohttp.web import Application, Request, Response, StreamResponse, middleware from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized +from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.components import persistent_notification @@ -24,6 +25,9 @@ from homeassistant.util import dt as dt_util, yaml from .view import HomeAssistantView +_HassViewT = TypeVar("_HassViewT", bound=HomeAssistantView) +_P = ParamSpec("_P") + _LOGGER: Final = logging.getLogger(__name__) KEY_BAN_MANAGER: Final = "ha_banned_ips_manager" @@ -82,13 +86,13 @@ async def ban_middleware( def log_invalid_auth( - func: Callable[..., Awaitable[StreamResponse]] -) -> Callable[..., Awaitable[StreamResponse]]: + func: Callable[Concatenate[_HassViewT, Request, _P], Awaitable[Response]] +) -> Callable[Concatenate[_HassViewT, Request, _P], Coroutine[Any, Any, Response]]: """Decorate function to handle invalid auth or failed login attempts.""" async def handle_req( - view: HomeAssistantView, request: Request, *args: Any, **kwargs: Any - ) -> StreamResponse: + view: _HassViewT, request: Request, *args: _P.args, **kwargs: _P.kwargs + ) -> Response: """Try to log failed login attempts if response status >= BAD_REQUEST.""" resp = await func(view, request, *args, **kwargs) if resp.status >= HTTPStatus.BAD_REQUEST: diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index cc661d43fd8..6647a6436c5 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -1,17 +1,21 @@ """Decorator for view methods to help with data validation.""" from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Coroutine from functools import wraps from http import HTTPStatus import logging -from typing import Any +from typing import Any, TypeVar from aiohttp import web +from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from .view import HomeAssistantView +_HassViewT = TypeVar("_HassViewT", bound=HomeAssistantView) +_P = ParamSpec("_P") + _LOGGER = logging.getLogger(__name__) @@ -33,33 +37,40 @@ class RequestDataValidator: self._allow_empty = allow_empty def __call__( - self, method: Callable[..., Awaitable[web.StreamResponse]] - ) -> Callable: + self, + method: Callable[ + Concatenate[_HassViewT, web.Request, dict[str, Any], _P], + Awaitable[web.Response], + ], + ) -> Callable[ + Concatenate[_HassViewT, web.Request, _P], + Coroutine[Any, Any, web.Response], + ]: """Decorate a function.""" @wraps(method) async def wrapper( - view: HomeAssistantView, request: web.Request, *args: Any, **kwargs: Any - ) -> web.StreamResponse: + view: _HassViewT, request: web.Request, *args: _P.args, **kwargs: _P.kwargs + ) -> web.Response: """Wrap a request handler with data validation.""" - data = None + raw_data = None try: - data = await request.json() + raw_data = await request.json() except ValueError: if not self._allow_empty or (await request.content.read()) != b"": _LOGGER.error("Invalid JSON received") return view.json_message("Invalid JSON.", HTTPStatus.BAD_REQUEST) - data = {} + raw_data = {} try: - kwargs["data"] = self._schema(data) + data: dict[str, Any] = self._schema(raw_data) except vol.Invalid as err: _LOGGER.error("Data does not match schema: %s", err) return view.json_message( f"Message format incorrect: {err}", HTTPStatus.BAD_REQUEST ) - result = await method(view, request, *args, **kwargs) + result = await method(view, request, data, *args, **kwargs) return result return wrapper diff --git a/homeassistant/components/repairs/websocket_api.py b/homeassistant/components/repairs/websocket_api.py index b6a71773273..2e9fcc5f8e4 100644 --- a/homeassistant/components/repairs/websocket_api.py +++ b/homeassistant/components/repairs/websocket_api.py @@ -113,7 +113,7 @@ class RepairsFlowIndexView(FlowManagerIndexView): result = self._prepare_result_json(result) - return self.json(result) # pylint: disable=arguments-differ + return self.json(result) class RepairsFlowResourceView(FlowManagerResourceView): @@ -136,4 +136,4 @@ class RepairsFlowResourceView(FlowManagerResourceView): raise Unauthorized(permission=POLICY_EDIT) # pylint: disable=no-value-for-parameter - return await super().post(request, flow_id) # type: ignore[no-any-return] + return await super().post(request, flow_id) diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 444876a7674..428a62f0c9d 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -102,7 +102,7 @@ class FlowManagerResourceView(_BaseFlowManagerView): @RequestDataValidator(vol.Schema(dict), allow_empty=True) async def post( - self, request: web.Request, flow_id: str, data: dict[str, Any] + self, request: web.Request, data: dict[str, Any], flow_id: str ) -> web.Response: """Handle a POST request.""" try: From 8523c66bb5dc6f7046286494a44a0c58e192094d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 21 Jul 2022 13:56:55 +0200 Subject: [PATCH 2766/3516] Deprecate U.S. Citizenship and Immigration Services (USCIS) integration (#75562) --- homeassistant/components/uscis/manifest.json | 1 + homeassistant/components/uscis/sensor.py | 13 +++++++++++++ homeassistant/components/uscis/strings.json | 8 ++++++++ homeassistant/components/uscis/translations/en.json | 8 ++++++++ 4 files changed, 30 insertions(+) create mode 100644 homeassistant/components/uscis/strings.json create mode 100644 homeassistant/components/uscis/translations/en.json diff --git a/homeassistant/components/uscis/manifest.json b/homeassistant/components/uscis/manifest.json index 0680848f70a..882dc588eba 100644 --- a/homeassistant/components/uscis/manifest.json +++ b/homeassistant/components/uscis/manifest.json @@ -3,6 +3,7 @@ "name": "U.S. Citizenship and Immigration Services (USCIS)", "documentation": "https://www.home-assistant.io/integrations/uscis", "requirements": ["uscisstatus==0.1.1"], + "dependencies": ["repairs"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["uscisstatus"] diff --git a/homeassistant/components/uscis/sensor.py b/homeassistant/components/uscis/sensor.py index 564c449e8b6..b4719243a8b 100644 --- a/homeassistant/components/uscis/sensor.py +++ b/homeassistant/components/uscis/sensor.py @@ -7,6 +7,7 @@ import logging import uscisstatus import voluptuous as vol +from homeassistant.components.repairs import IssueSeverity, create_issue from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant @@ -34,6 +35,18 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the platform in Home Assistant and Case Information.""" + create_issue( + hass, + "uscis", + "pending_removal", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="pending_removal", + ) + _LOGGER.warning( + "The USCIS sensor component is deprecated and will be removed in Home Assistant 2022.10" + ) uscis = UscisSensor(config["case_id"], config[CONF_NAME]) uscis.update() if uscis.valid_case_id: diff --git a/homeassistant/components/uscis/strings.json b/homeassistant/components/uscis/strings.json new file mode 100644 index 00000000000..b8dec86db18 --- /dev/null +++ b/homeassistant/components/uscis/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "title": "The USCIS integration is being removed", + "description": "The U.S. Citizenship and Immigration Services (USCIS) integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nThe integration is being removed, because it relies on webscraping, which is not allowed.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/uscis/translations/en.json b/homeassistant/components/uscis/translations/en.json new file mode 100644 index 00000000000..24e7e9ceea0 --- /dev/null +++ b/homeassistant/components/uscis/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "The U.S. Citizenship and Immigration Services (USCIS) integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nThe integration is being removed, because it relies on webscraping, which is not allowed.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The USCIS integration is being removed" + } + } +} \ No newline at end of file From f3c4bf571bad13afe3463f61b0062f7d4378f190 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 Jul 2022 09:44:53 -0500 Subject: [PATCH 2767/3516] Raise on bad update data instead of log in PassiveBluetoothDataUpdateCoordinator (#75536) --- .../components/bluetooth/passive_update_coordinator.py | 7 ++----- .../bluetooth/test_passive_update_coordinator.py | 9 +++++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index df4233452df..77b3fc54aba 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -261,12 +261,9 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): if not isinstance(new_data, PassiveBluetoothDataUpdate): self.last_update_success = False # type: ignore[unreachable] - self.logger.error( - "The update_method for %s returned %s instead of a PassiveBluetoothDataUpdate", - self.name, - new_data, + raise ValueError( + f"The update_method for {self.name} returned {new_data} instead of a PassiveBluetoothDataUpdate" ) - return if not self.last_update_success: self.last_update_success = True diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index ca4ef6e909c..a377a8f4a46 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -7,6 +7,7 @@ import time from unittest.mock import MagicMock, patch from home_assistant_bluetooth import BluetoothServiceInfo +import pytest from homeassistant.components.bluetooth import BluetoothChange from homeassistant.components.bluetooth.passive_update_coordinator import ( @@ -337,7 +338,7 @@ async def test_exception_from_update_method(hass, caplog): cancel_coordinator() -async def test_bad_data_from_update_method(hass, caplog): +async def test_bad_data_from_update_method(hass): """Test we handle bad data from the update method.""" run_count = 0 @@ -373,9 +374,9 @@ async def test_bad_data_from_update_method(hass, caplog): assert coordinator.available is True # We should go unavailable once we get bad data - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert "update_method" in caplog.text - assert "bad_data" in caplog.text + with pytest.raises(ValueError): + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert coordinator.available is False # We should go available again once we get good data again From 05b463b28254434ed9eecd4a68a74364551f1c7e Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 21 Jul 2022 18:38:59 +0200 Subject: [PATCH 2768/3516] Migrate AccuWeather to new entity naming style (#75127) * Use new entity naming style * Move AccuWeatherSensorDescription and SENSOR consts to sensor platform * Remove duplicate code * Suggested change * Format url --- .../components/accuweather/__init__.py | 26 +- homeassistant/components/accuweather/const.py | 279 --------------- homeassistant/components/accuweather/model.py | 14 - .../components/accuweather/sensor.py | 319 ++++++++++++++++-- .../components/accuweather/weather.py | 28 +- 5 files changed, 323 insertions(+), 343 deletions(-) delete mode 100644 homeassistant/components/accuweather/model.py diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index d36ea4e8466..9123648a38d 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -11,12 +11,14 @@ from aiohttp.client_exceptions import ClientConnectorError from async_timeout import timeout from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, Platform +from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN +from .const import ATTR_FORECAST, CONF_FORECAST, DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) @@ -26,6 +28,7 @@ PLATFORMS = [Platform.SENSOR, Platform.WEATHER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up AccuWeather as config entry.""" api_key: str = entry.data[CONF_API_KEY] + name: str = entry.data[CONF_NAME] assert entry.unique_id is not None location_key = entry.unique_id forecast: bool = entry.options.get(CONF_FORECAST, False) @@ -35,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: websession = async_get_clientsession(hass) coordinator = AccuWeatherDataUpdateCoordinator( - hass, websession, api_key, location_key, forecast + hass, websession, api_key, location_key, forecast, name ) await coordinator.async_config_entry_first_refresh() @@ -73,12 +76,27 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): api_key: str, location_key: str, forecast: bool, + name: str, ) -> None: """Initialize.""" self.location_key = location_key self.forecast = forecast self.is_metric = hass.config.units.is_metric - self.accuweather = AccuWeather(api_key, session, location_key=self.location_key) + self.accuweather = AccuWeather(api_key, session, location_key=location_key) + self.device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, location_key)}, + manufacturer=MANUFACTURER, + name=name, + # You don't need to provide specific details for the URL, + # so passing in _ characters is fine if the location key + # is correct + configuration_url=( + "http://accuweather.com/en/" + f"_/_/{location_key}/" + f"weather-forecast/{location_key}/" + ), + ) # Enabling the forecast download increases the number of requests per data # update, we use 40 minutes for current condition only and 80 minutes for diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index 408d4700422..c1b90de09e7 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -3,7 +3,6 @@ from __future__ import annotations from typing import Final -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -20,22 +19,6 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_CONDITION_WINDY, ) -from homeassistant.const import ( - CONCENTRATION_PARTS_PER_CUBIC_METER, - LENGTH_FEET, - LENGTH_INCHES, - LENGTH_METERS, - LENGTH_MILLIMETERS, - PERCENTAGE, - SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TIME_HOURS, - UV_INDEX, -) - -from .model import AccuWeatherSensorDescription API_IMPERIAL: Final = "Imperial" API_METRIC: Final = "Metric" @@ -45,7 +28,6 @@ CONF_FORECAST: Final = "forecast" DOMAIN: Final = "accuweather" MANUFACTURER: Final = "AccuWeather, Inc." MAX_FORECAST_DAYS: Final = 4 -NAME: Final = "AccuWeather" CONDITION_CLASSES: Final[dict[str, list[int]]] = { ATTR_CONDITION_CLEAR_NIGHT: [33, 34, 37], @@ -63,264 +45,3 @@ CONDITION_CLASSES: Final[dict[str, list[int]]] = { ATTR_CONDITION_SUNNY: [1, 2, 5], ATTR_CONDITION_WINDY: [32], } - -FORECAST_SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( - AccuWeatherSensorDescription( - key="CloudCoverDay", - icon="mdi:weather-cloudy", - name="Cloud Cover Day", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="CloudCoverNight", - icon="mdi:weather-cloudy", - name="Cloud Cover Night", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="Grass", - icon="mdi:grass", - name="Grass Pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="HoursOfSun", - icon="mdi:weather-partly-cloudy", - name="Hours Of Sun", - unit_metric=TIME_HOURS, - unit_imperial=TIME_HOURS, - ), - AccuWeatherSensorDescription( - key="Mold", - icon="mdi:blur", - name="Mold Pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="Ozone", - icon="mdi:vector-triangle", - name="Ozone", - unit_metric=None, - unit_imperial=None, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="Ragweed", - icon="mdi:sprout", - name="Ragweed Pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="RealFeelTemperatureMax", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel Temperature Max", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - ), - AccuWeatherSensorDescription( - key="RealFeelTemperatureMin", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel Temperature Min", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - ), - AccuWeatherSensorDescription( - key="RealFeelTemperatureShadeMax", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel Temperature Shade Max", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="RealFeelTemperatureShadeMin", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel Temperature Shade Min", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="ThunderstormProbabilityDay", - icon="mdi:weather-lightning", - name="Thunderstorm Probability Day", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, - ), - AccuWeatherSensorDescription( - key="ThunderstormProbabilityNight", - icon="mdi:weather-lightning", - name="Thunderstorm Probability Night", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, - ), - AccuWeatherSensorDescription( - key="Tree", - icon="mdi:tree-outline", - name="Tree Pollen", - unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, - unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="UVIndex", - icon="mdi:weather-sunny", - name="UV Index", - unit_metric=UV_INDEX, - unit_imperial=UV_INDEX, - ), - AccuWeatherSensorDescription( - key="WindGustDay", - icon="mdi:weather-windy", - name="Wind Gust Day", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="WindGustNight", - icon="mdi:weather-windy", - name="Wind Gust Night", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, - entity_registry_enabled_default=False, - ), - AccuWeatherSensorDescription( - key="WindDay", - icon="mdi:weather-windy", - name="Wind Day", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, - ), - AccuWeatherSensorDescription( - key="WindNight", - icon="mdi:weather-windy", - name="Wind Night", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, - ), -) - -SENSOR_TYPES: Final[tuple[AccuWeatherSensorDescription, ...]] = ( - AccuWeatherSensorDescription( - key="ApparentTemperature", - device_class=SensorDeviceClass.TEMPERATURE, - name="Apparent Temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="Ceiling", - icon="mdi:weather-fog", - name="Cloud Ceiling", - unit_metric=LENGTH_METERS, - unit_imperial=LENGTH_FEET, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="CloudCover", - icon="mdi:weather-cloudy", - name="Cloud Cover", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="DewPoint", - device_class=SensorDeviceClass.TEMPERATURE, - name="Dew Point", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="RealFeelTemperature", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel Temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="RealFeelTemperatureShade", - device_class=SensorDeviceClass.TEMPERATURE, - name="RealFeel Temperature Shade", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="Precipitation", - icon="mdi:weather-rainy", - name="Precipitation", - unit_metric=LENGTH_MILLIMETERS, - unit_imperial=LENGTH_INCHES, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="PressureTendency", - device_class="accuweather__pressure_tendency", - icon="mdi:gauge", - name="Pressure Tendency", - unit_metric=None, - unit_imperial=None, - ), - AccuWeatherSensorDescription( - key="UVIndex", - icon="mdi:weather-sunny", - name="UV Index", - unit_metric=UV_INDEX, - unit_imperial=UV_INDEX, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="WetBulbTemperature", - device_class=SensorDeviceClass.TEMPERATURE, - name="Wet Bulb Temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="WindChillTemperature", - device_class=SensorDeviceClass.TEMPERATURE, - name="Wind Chill Temperature", - unit_metric=TEMP_CELSIUS, - unit_imperial=TEMP_FAHRENHEIT, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="Wind", - icon="mdi:weather-windy", - name="Wind", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, - state_class=SensorStateClass.MEASUREMENT, - ), - AccuWeatherSensorDescription( - key="WindGust", - icon="mdi:weather-windy", - name="Wind Gust", - unit_metric=SPEED_KILOMETERS_PER_HOUR, - unit_imperial=SPEED_MILES_PER_HOUR, - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - ), -) diff --git a/homeassistant/components/accuweather/model.py b/homeassistant/components/accuweather/model.py deleted file mode 100644 index e74a6d46057..00000000000 --- a/homeassistant/components/accuweather/model.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Type definitions for AccuWeather integration.""" -from __future__ import annotations - -from dataclasses import dataclass - -from homeassistant.components.sensor import SensorEntityDescription - - -@dataclass -class AccuWeatherSensorDescription(SensorEntityDescription): - """Class describing AccuWeather sensor entities.""" - - unit_metric: str | None = None - unit_imperial: str | None = None diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 220575541ad..72182d4d635 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -1,14 +1,31 @@ """Support for the AccuWeather service.""" from __future__ import annotations +from dataclasses import dataclass from typing import Any, cast -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_CUBIC_METER, + LENGTH_FEET, + LENGTH_INCHES, + LENGTH_METERS, + LENGTH_MILLIMETERS, + PERCENTAGE, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TIME_HOURS, + UV_INDEX, +) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import DeviceEntryType -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -20,28 +37,292 @@ from .const import ( ATTR_FORECAST, ATTRIBUTION, DOMAIN, - FORECAST_SENSOR_TYPES, - MANUFACTURER, MAX_FORECAST_DAYS, - NAME, - SENSOR_TYPES, ) -from .model import AccuWeatherSensorDescription PARALLEL_UPDATES = 1 +@dataclass +class AccuWeatherSensorDescription(SensorEntityDescription): + """Class describing AccuWeather sensor entities.""" + + unit_metric: str | None = None + unit_imperial: str | None = None + + +FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( + AccuWeatherSensorDescription( + key="CloudCoverDay", + icon="mdi:weather-cloudy", + name="Cloud cover day", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="CloudCoverNight", + icon="mdi:weather-cloudy", + name="Cloud cover night", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="Grass", + icon="mdi:grass", + name="Grass pollen", + unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="HoursOfSun", + icon="mdi:weather-partly-cloudy", + name="Hours of sun", + unit_metric=TIME_HOURS, + unit_imperial=TIME_HOURS, + ), + AccuWeatherSensorDescription( + key="Mold", + icon="mdi:blur", + name="Mold pollen", + unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="Ozone", + icon="mdi:vector-triangle", + name="Ozone", + unit_metric=None, + unit_imperial=None, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="Ragweed", + icon="mdi:sprout", + name="Ragweed pollen", + unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureMax", + device_class=SensorDeviceClass.TEMPERATURE, + name="RealFeel temperature max", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureMin", + device_class=SensorDeviceClass.TEMPERATURE, + name="RealFeel temperature min", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureShadeMax", + device_class=SensorDeviceClass.TEMPERATURE, + name="RealFeel temperature shade max", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureShadeMin", + device_class=SensorDeviceClass.TEMPERATURE, + name="RealFeel temperature shade min", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="ThunderstormProbabilityDay", + icon="mdi:weather-lightning", + name="Thunderstorm probability day", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + ), + AccuWeatherSensorDescription( + key="ThunderstormProbabilityNight", + icon="mdi:weather-lightning", + name="Thunderstorm probability night", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + ), + AccuWeatherSensorDescription( + key="Tree", + icon="mdi:tree-outline", + name="Tree pollen", + unit_metric=CONCENTRATION_PARTS_PER_CUBIC_METER, + unit_imperial=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="UVIndex", + icon="mdi:weather-sunny", + name="UV index", + unit_metric=UV_INDEX, + unit_imperial=UV_INDEX, + ), + AccuWeatherSensorDescription( + key="WindGustDay", + icon="mdi:weather-windy", + name="Wind gust day", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="WindGustNight", + icon="mdi:weather-windy", + name="Wind gust night", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + entity_registry_enabled_default=False, + ), + AccuWeatherSensorDescription( + key="WindDay", + icon="mdi:weather-windy", + name="Wind day", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + ), + AccuWeatherSensorDescription( + key="WindNight", + icon="mdi:weather-windy", + name="Wind night", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + ), +) + +SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( + AccuWeatherSensorDescription( + key="ApparentTemperature", + device_class=SensorDeviceClass.TEMPERATURE, + name="Apparent temperature", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="Ceiling", + icon="mdi:weather-fog", + name="Cloud ceiling", + unit_metric=LENGTH_METERS, + unit_imperial=LENGTH_FEET, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="CloudCover", + icon="mdi:weather-cloudy", + name="Cloud cover", + unit_metric=PERCENTAGE, + unit_imperial=PERCENTAGE, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="DewPoint", + device_class=SensorDeviceClass.TEMPERATURE, + name="Dew point", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperature", + device_class=SensorDeviceClass.TEMPERATURE, + name="RealFeel temperature", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="RealFeelTemperatureShade", + device_class=SensorDeviceClass.TEMPERATURE, + name="RealFeel temperature shade", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="Precipitation", + icon="mdi:weather-rainy", + name="Precipitation", + unit_metric=LENGTH_MILLIMETERS, + unit_imperial=LENGTH_INCHES, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="PressureTendency", + device_class="accuweather__pressure_tendency", + icon="mdi:gauge", + name="Pressure tendency", + unit_metric=None, + unit_imperial=None, + ), + AccuWeatherSensorDescription( + key="UVIndex", + icon="mdi:weather-sunny", + name="UV index", + unit_metric=UV_INDEX, + unit_imperial=UV_INDEX, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="WetBulbTemperature", + device_class=SensorDeviceClass.TEMPERATURE, + name="Wet bulb temperature", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="WindChillTemperature", + device_class=SensorDeviceClass.TEMPERATURE, + name="Wind chill temperature", + unit_metric=TEMP_CELSIUS, + unit_imperial=TEMP_FAHRENHEIT, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="Wind", + icon="mdi:weather-windy", + name="Wind", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + state_class=SensorStateClass.MEASUREMENT, + ), + AccuWeatherSensorDescription( + key="WindGust", + icon="mdi:weather-windy", + name="Wind gust", + unit_metric=SPEED_KILOMETERS_PER_HOUR, + unit_imperial=SPEED_MILES_PER_HOUR, + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + ), +) + + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add AccuWeather entities from a config_entry.""" - name: str = entry.data[CONF_NAME] coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] sensors: list[AccuWeatherSensor] = [] for description in SENSOR_TYPES: - sensors.append(AccuWeatherSensor(name, coordinator, description)) + sensors.append(AccuWeatherSensor(coordinator, description)) if coordinator.forecast: for description in FORECAST_SENSOR_TYPES: @@ -50,9 +331,7 @@ async def async_setup_entry( # locations. if description.key in coordinator.data[ATTR_FORECAST][0]: sensors.append( - AccuWeatherSensor( - name, coordinator, description, forecast_day=day - ) + AccuWeatherSensor(coordinator, description, forecast_day=day) ) async_add_entities(sensors) @@ -64,11 +343,11 @@ class AccuWeatherSensor( """Define an AccuWeather entity.""" _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True entity_description: AccuWeatherSensorDescription def __init__( self, - name: str, coordinator: AccuWeatherDataUpdateCoordinator, description: AccuWeatherSensorDescription, forecast_day: int | None = None, @@ -81,12 +360,11 @@ class AccuWeatherSensor( ) self._attrs: dict[str, Any] = {} if forecast_day is not None: - self._attr_name = f"{name} {description.name} {forecast_day}d" + self._attr_name = f"{description.name} {forecast_day}d" self._attr_unique_id = ( f"{coordinator.location_key}-{description.key}-{forecast_day}".lower() ) else: - self._attr_name = f"{name} {description.name}" self._attr_unique_id = ( f"{coordinator.location_key}-{description.key}".lower() ) @@ -96,12 +374,7 @@ class AccuWeatherSensor( else: self._unit_system = API_IMPERIAL self._attr_native_unit_of_measurement = description.unit_imperial - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, coordinator.location_key)}, - manufacturer=MANUFACTURER, - name=NAME, - ) + self._attr_device_info = coordinator.device_info self.forecast_day = forecast_day @property diff --git a/homeassistant/components/accuweather/weather.py b/homeassistant/components/accuweather/weather.py index ae1824aef4a..e8a5b0ab396 100644 --- a/homeassistant/components/accuweather/weather.py +++ b/homeassistant/components/accuweather/weather.py @@ -18,7 +18,6 @@ from homeassistant.components.weather import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_NAME, LENGTH_INCHES, LENGTH_KILOMETERS, LENGTH_MILES, @@ -31,8 +30,6 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceEntryType -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 @@ -45,8 +42,6 @@ from .const import ( ATTRIBUTION, CONDITION_CLASSES, DOMAIN, - MANUFACTURER, - NAME, ) PARALLEL_UPDATES = 1 @@ -56,11 +51,10 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add a AccuWeather weather entity from a config_entry.""" - name: str = entry.data[CONF_NAME] coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - async_add_entities([AccuWeatherEntity(name, coordinator)]) + async_add_entities([AccuWeatherEntity(coordinator)]) class AccuWeatherEntity( @@ -68,9 +62,9 @@ class AccuWeatherEntity( ): """Define an AccuWeather entity.""" - def __init__( - self, name: str, coordinator: AccuWeatherDataUpdateCoordinator - ) -> None: + _attr_has_entity_name = True + + def __init__(self, coordinator: AccuWeatherDataUpdateCoordinator) -> None: """Initialize.""" super().__init__(coordinator) # Coordinator data is used also for sensors which don't have units automatically @@ -90,21 +84,9 @@ class AccuWeatherEntity( self._attr_native_temperature_unit = TEMP_FAHRENHEIT self._attr_native_visibility_unit = LENGTH_MILES self._attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR - self._attr_name = name self._attr_unique_id = coordinator.location_key self._attr_attribution = ATTRIBUTION - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, coordinator.location_key)}, - manufacturer=MANUFACTURER, - name=NAME, - # You don't need to provide specific details for the URL, - # so passing in _ characters is fine if the location key - # is correct - configuration_url="http://accuweather.com/en/" - f"_/_/{coordinator.location_key}/" - f"weather-forecast/{coordinator.location_key}/", - ) + self._attr_device_info = coordinator.device_info @property def condition(self) -> str | None: From 6cb1794720fa6df4ffe5c3fc431a31afbba6b5e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 Jul 2022 15:58:45 -0500 Subject: [PATCH 2769/3516] Bump aiohomekit to 1.1.9 (#75591) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 10ca50db820..c784bad0d06 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.1.8"], + "requirements": ["aiohomekit==1.1.9"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "after_dependencies": ["zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 26d7c6bce14..c88df779c25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.8 +aiohomekit==1.1.9 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 48826ebdf1a..57787b0cb72 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.03.2 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.8 +aiohomekit==1.1.9 # homeassistant.components.emulated_hue # homeassistant.components.http From 04c6b9c51963418ffebddc7753939700fbea7e42 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 21 Jul 2022 17:54:50 -0400 Subject: [PATCH 2770/3516] ZHA light entity cleanup (#75573) * use base class attributes * initial hue and saturation support * spec is 65536 not 65535 * fixes * enhanced current hue * fix comparison * clean up * fix channel test * oops * report enhanced current hue --- .../components/zha/core/channels/lighting.py | 71 ++++ homeassistant/components/zha/light.py | 344 ++++++++++-------- tests/components/zha/test_channels.py | 13 +- tests/components/zha/test_light.py | 23 +- 4 files changed, 285 insertions(+), 166 deletions(-) diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index 99e6101b0bd..36bb0beb17d 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -31,6 +31,9 @@ class ColorChannel(ZigbeeChannel): REPORT_CONFIG = ( AttrReportConfig(attr="current_x", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="current_y", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="current_hue", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="enhanced_current_hue", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig(attr="current_saturation", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="color_temperature", config=REPORT_CONFIG_DEFAULT), ) MAX_MIREDS: int = 500 @@ -52,6 +55,14 @@ class ColorChannel(ZigbeeChannel): return self.CAPABILITIES_COLOR_XY | self.CAPABILITIES_COLOR_TEMP return self.CAPABILITIES_COLOR_XY + @property + def zcl_color_capabilities(self) -> lighting.Color.ColorCapabilities: + """Return ZCL color capabilities of the light.""" + color_capabilities = self.cluster.get("color_capabilities") + if color_capabilities is None: + return lighting.Color.ColorCapabilities(self.CAPABILITIES_COLOR_XY) + return lighting.Color.ColorCapabilities(color_capabilities) + @property def color_mode(self) -> int | None: """Return cached value of the color_mode attribute.""" @@ -77,6 +88,21 @@ class ColorChannel(ZigbeeChannel): """Return cached value of the current_y attribute.""" return self.cluster.get("current_y") + @property + def current_hue(self) -> int | None: + """Return cached value of the current_hue attribute.""" + return self.cluster.get("current_hue") + + @property + def enhanced_current_hue(self) -> int | None: + """Return cached value of the enhanced_current_hue attribute.""" + return self.cluster.get("enhanced_current_hue") + + @property + def current_saturation(self) -> int | None: + """Return cached value of the current_saturation attribute.""" + return self.cluster.get("current_saturation") + @property def min_mireds(self) -> int: """Return the coldest color_temp that this channel supports.""" @@ -86,3 +112,48 @@ class ColorChannel(ZigbeeChannel): def max_mireds(self) -> int: """Return the warmest color_temp that this channel supports.""" return self.cluster.get("color_temp_physical_max", self.MAX_MIREDS) + + @property + def hs_supported(self) -> bool: + """Return True if the channel supports hue and saturation.""" + return ( + self.zcl_color_capabilities is not None + and lighting.Color.ColorCapabilities.Hue_and_saturation + in self.zcl_color_capabilities + ) + + @property + def enhanced_hue_supported(self) -> bool: + """Return True if the channel supports enhanced hue and saturation.""" + return ( + self.zcl_color_capabilities is not None + and lighting.Color.ColorCapabilities.Enhanced_hue + in self.zcl_color_capabilities + ) + + @property + def xy_supported(self) -> bool: + """Return True if the channel supports xy.""" + return ( + self.zcl_color_capabilities is not None + and lighting.Color.ColorCapabilities.XY_attributes + in self.zcl_color_capabilities + ) + + @property + def color_temp_supported(self) -> bool: + """Return True if the channel supports color temperature.""" + return ( + self.zcl_color_capabilities is not None + and lighting.Color.ColorCapabilities.Color_temperature + in self.zcl_color_capabilities + ) + + @property + def color_loop_supported(self) -> bool: + """Return True if the channel supports color loop.""" + return ( + self.zcl_color_capabilities is not None + and lighting.Color.ColorCapabilities.Color_loop + in self.zcl_color_capabilities + ) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index cc8dc475c43..8aab3d20a5c 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -15,15 +15,6 @@ from zigpy.zcl.foundation import Status from homeassistant.components import light from homeassistant.components.light import ( - ATTR_BRIGHTNESS, - ATTR_COLOR_MODE, - ATTR_COLOR_TEMP, - ATTR_EFFECT, - ATTR_EFFECT_LIST, - ATTR_HS_COLOR, - ATTR_MAX_MIREDS, - ATTR_MIN_MIREDS, - ATTR_SUPPORTED_COLOR_MODES, ColorMode, brightness_supported, filter_supported_color_modes, @@ -43,7 +34,6 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval -import homeassistant.util.color as color_util from .core import discovery, helpers from .core.const import ( @@ -69,10 +59,6 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) -CAPABILITIES_COLOR_LOOP = 0x4 -CAPABILITIES_COLOR_XY = 0x08 -CAPABILITIES_COLOR_TEMP = 0x10 - DEFAULT_MIN_BRIGHTNESS = 2 UPDATE_COLORLOOP_ACTION = 0x1 @@ -88,7 +74,7 @@ GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.LIGHT) PARALLEL_UPDATES = 0 SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed" -COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.HS} +COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.XY} SUPPORT_GROUP_LIGHT = ( light.LightEntityFeature.EFFECT | light.LightEntityFeature.FLASH @@ -123,24 +109,18 @@ class BaseLight(LogMixin, light.LightEntity): def __init__(self, *args, **kwargs): """Initialize the light.""" super().__init__(*args, **kwargs) - self._available: bool = False - self._brightness: int | None = None + self._attr_min_mireds: int | None = 153 + self._attr_max_mireds: int | None = 500 + self._attr_color_mode = ColorMode.UNKNOWN # Set by sub classes + self._attr_supported_features: int = 0 + self._attr_state: bool | None self._off_with_transition: bool = False self._off_brightness: int | None = None - self._hs_color: tuple[float, float] | None = None - self._color_temp: int | None = None - self._min_mireds: int | None = 153 - self._max_mireds: int | None = 500 - self._effect_list: list[str] | None = None - self._effect: str | None = None - self._supported_features: int = 0 - self._state: bool = False + self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME self._on_off_channel = None self._level_channel = None self._color_channel = None self._identify_channel = None - self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME - self._attr_color_mode = ColorMode.UNKNOWN # Set by sub classes @property def extra_state_attributes(self) -> dict[str, Any]: @@ -154,24 +134,9 @@ class BaseLight(LogMixin, light.LightEntity): @property def is_on(self) -> bool: """Return true if entity is on.""" - if self._state is None: + if self._attr_state is None: return False - return self._state - - @property - def brightness(self): - """Return the brightness of this light.""" - return self._brightness - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - return self._min_mireds - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - return self._max_mireds + return self._attr_state @callback def set_level(self, value): @@ -182,34 +147,9 @@ class BaseLight(LogMixin, light.LightEntity): level """ value = max(0, min(254, value)) - self._brightness = value + self._attr_brightness = value self.async_write_ha_state() - @property - def hs_color(self): - """Return the hs color value [int, int].""" - return self._hs_color - - @property - def color_temp(self): - """Return the CT color value in mireds.""" - return self._color_temp - - @property - def effect_list(self): - """Return the list of supported effects.""" - return self._effect_list - - @property - def effect(self): - """Return the current effect.""" - return self._effect - - @property - def supported_features(self): - """Flag supported features.""" - return self._supported_features - async def async_turn_on(self, **kwargs): """Turn the entity on.""" transition = kwargs.get(light.ATTR_TRANSITION) @@ -222,6 +162,7 @@ class BaseLight(LogMixin, light.LightEntity): effect = kwargs.get(light.ATTR_EFFECT) flash = kwargs.get(light.ATTR_FLASH) temperature = kwargs.get(light.ATTR_COLOR_TEMP) + xy_color = kwargs.get(light.ATTR_XY_COLOR) hs_color = kwargs.get(light.ATTR_HS_COLOR) # If the light is currently off but a turn_on call with a color/temperature is sent, @@ -235,19 +176,26 @@ class BaseLight(LogMixin, light.LightEntity): new_color_provided_while_off = ( not isinstance(self, LightGroup) and not self._FORCE_ON - and not self._state + and not self._attr_state and ( ( temperature is not None and ( - self._color_temp != temperature + self._attr_color_temp != temperature or self._attr_color_mode != ColorMode.COLOR_TEMP ) ) + or ( + xy_color is not None + and ( + self._attr_xy_color != xy_color + or self._attr_color_mode != ColorMode.XY + ) + ) or ( hs_color is not None and ( - self.hs_color != hs_color + self._attr_hs_color != hs_color or self._attr_color_mode != ColorMode.HS ) ) @@ -265,7 +213,7 @@ class BaseLight(LogMixin, light.LightEntity): if brightness is not None: level = min(254, brightness) else: - level = self._brightness or 254 + level = self._attr_brightness or 254 t_log = {} @@ -280,7 +228,7 @@ class BaseLight(LogMixin, light.LightEntity): self.debug("turned on: %s", t_log) return # Currently only setting it to "on", as the correct level state will be set at the second move_to_level call - self._state = True + self._attr_state = True if ( (brightness is not None or transition) @@ -294,9 +242,9 @@ class BaseLight(LogMixin, light.LightEntity): if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return - self._state = bool(level) + self._attr_state = bool(level) if level: - self._brightness = level + self._attr_brightness = level if ( brightness is None @@ -310,7 +258,7 @@ class BaseLight(LogMixin, light.LightEntity): if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return - self._state = True + self._attr_state = True if temperature is not None: result = await self._color_channel.move_to_color_temp( @@ -324,11 +272,39 @@ class BaseLight(LogMixin, light.LightEntity): self.debug("turned on: %s", t_log) return self._attr_color_mode = ColorMode.COLOR_TEMP - self._color_temp = temperature - self._hs_color = None + self._attr_color_temp = temperature + self._attr_xy_color = None + self._attr_hs_color = None if hs_color is not None: - xy_color = color_util.color_hs_to_xy(*hs_color) + if self._color_channel.enhanced_hue_supported: + result = await self._color_channel.enhanced_move_to_hue_and_saturation( + int(hs_color[0] * 65535 / 360), + int(hs_color[1] * 2.54), + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, + ) + t_log["enhanced_move_to_hue_and_saturation"] = result + else: + result = await self._color_channel.move_to_hue_and_saturation( + int(hs_color[0] * 254 / 360), + int(hs_color[1] * 2.54), + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, + ) + t_log["move_to_hue_and_saturation"] = result + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + self.debug("turned on: %s", t_log) + return + self._attr_color_mode = ColorMode.HS + self._attr_hs_color = hs_color + self._attr_xy_color = None + self._attr_color_temp = None + xy_color = None # don't set xy_color if it is also present + + if xy_color is not None: result = await self._color_channel.move_to_color( int(xy_color[0] * 65535), int(xy_color[1] * 65535), @@ -340,9 +316,10 @@ class BaseLight(LogMixin, light.LightEntity): if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return - self._attr_color_mode = ColorMode.HS - self._hs_color = hs_color - self._color_temp = None + self._attr_color_mode = ColorMode.XY + self._attr_xy_color = xy_color + self._attr_color_temp = None + self._attr_hs_color = None if new_color_provided_while_off: # The light is has the correct color, so we can now transition it to the correct brightness level. @@ -351,9 +328,9 @@ class BaseLight(LogMixin, light.LightEntity): if isinstance(result, Exception) or result[1] is not Status.SUCCESS: self.debug("turned on: %s", t_log) return - self._state = bool(level) + self._attr_state = bool(level) if level: - self._brightness = level + self._attr_brightness = level if effect == light.EFFECT_COLORLOOP: result = await self._color_channel.color_loop_set( @@ -366,9 +343,10 @@ class BaseLight(LogMixin, light.LightEntity): 0, # no hue ) t_log["color_loop_set"] = result - self._effect = light.EFFECT_COLORLOOP + self._attr_effect = light.EFFECT_COLORLOOP elif ( - self._effect == light.EFFECT_COLORLOOP and effect != light.EFFECT_COLORLOOP + self._attr_effect == light.EFFECT_COLORLOOP + and effect != light.EFFECT_COLORLOOP ): result = await self._color_channel.color_loop_set( UPDATE_COLORLOOP_ACTION, @@ -378,7 +356,7 @@ class BaseLight(LogMixin, light.LightEntity): 0x0, # update action only, action off, no dir, time, hue ) t_log["color_loop_set"] = result - self._effect = None + self._attr_effect = None if flash is not None: result = await self._identify_channel.trigger_effect( @@ -406,12 +384,12 @@ class BaseLight(LogMixin, light.LightEntity): self.debug("turned off: %s", result) if isinstance(result, Exception) or result[1] is not Status.SUCCESS: return - self._state = False + self._attr_state = False if supports_level: # store current brightness so that the next turn_on uses it. self._off_with_transition = transition is not None - self._off_brightness = self._brightness + self._off_brightness = self._attr_brightness self.async_write_ha_state() @@ -427,44 +405,59 @@ class Light(BaseLight, ZhaEntity): """Initialize the ZHA light.""" super().__init__(unique_id, zha_device, channels, **kwargs) self._on_off_channel = self.cluster_channels[CHANNEL_ON_OFF] - self._state = bool(self._on_off_channel.on_off) + self._attr_state = bool(self._on_off_channel.on_off) self._level_channel = self.cluster_channels.get(CHANNEL_LEVEL) self._color_channel = self.cluster_channels.get(CHANNEL_COLOR) self._identify_channel = self.zha_device.channels.identify_ch if self._color_channel: - self._min_mireds: int | None = self._color_channel.min_mireds - self._max_mireds: int | None = self._color_channel.max_mireds + self._attr_min_mireds: int = self._color_channel.min_mireds + self._attr_max_mireds: int = self._color_channel.max_mireds self._cancel_refresh_handle = None effect_list = [] self._attr_supported_color_modes = {ColorMode.ONOFF} if self._level_channel: self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS) - self._supported_features |= light.LightEntityFeature.TRANSITION - self._brightness = self._level_channel.current_level + self._attr_supported_features |= light.LightEntityFeature.TRANSITION + self._attr_brightness = self._level_channel.current_level if self._color_channel: - color_capabilities = self._color_channel.color_capabilities - if color_capabilities & CAPABILITIES_COLOR_TEMP: + if self._color_channel.color_temp_supported: self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP) - self._color_temp = self._color_channel.color_temperature + self._attr_color_temp = self._color_channel.color_temperature - if color_capabilities & CAPABILITIES_COLOR_XY: - self._attr_supported_color_modes.add(ColorMode.HS) + if ( + self._color_channel.xy_supported + and not self._color_channel.hs_supported + ): + self._attr_supported_color_modes.add(ColorMode.XY) curr_x = self._color_channel.current_x curr_y = self._color_channel.current_y if curr_x is not None and curr_y is not None: - self._hs_color = color_util.color_xy_to_hs( - float(curr_x / 65535), float(curr_y / 65535) + self._attr_xy_color = (curr_x / 65535, curr_y / 65535) + else: + self._attr_xy_color = (0, 0) + + if self._color_channel.hs_supported: + self._attr_supported_color_modes.add(ColorMode.HS) + if self._color_channel.enhanced_hue_supported: + curr_hue = self._color_channel.enhanced_current_hue * 65535 / 360 + else: + curr_hue = self._color_channel.current_hue * 254 / 360 + curr_saturation = self._color_channel.current_saturation + if curr_hue is not None and curr_saturation is not None: + self._attr_hs_color = ( + int(curr_hue), + int(curr_saturation * 2.54), ) else: - self._hs_color = (0, 0) + self._attr_hs_color = (0, 0) - if color_capabilities & CAPABILITIES_COLOR_LOOP: - self._supported_features |= light.LightEntityFeature.EFFECT + if self._color_channel.color_loop_supported: + self._attr_supported_features |= light.LightEntityFeature.EFFECT effect_list.append(light.EFFECT_COLORLOOP) if self._color_channel.color_loop_active == 1: - self._effect = light.EFFECT_COLORLOOP + self._attr_effect = light.EFFECT_COLORLOOP self._attr_supported_color_modes = filter_supported_color_modes( self._attr_supported_color_modes ) @@ -475,13 +468,13 @@ class Light(BaseLight, ZhaEntity): if self._color_channel.color_mode == Color.ColorMode.Color_temperature: self._attr_color_mode = ColorMode.COLOR_TEMP else: - self._attr_color_mode = ColorMode.HS + self._attr_color_mode = ColorMode.XY if self._identify_channel: - self._supported_features |= light.LightEntityFeature.FLASH + self._attr_supported_features |= light.LightEntityFeature.FLASH if effect_list: - self._effect_list = effect_list + self._attr_effect_list = effect_list self._zha_config_transition = async_get_zha_config_value( zha_device.gateway.config_entry, @@ -493,7 +486,7 @@ class Light(BaseLight, ZhaEntity): @callback def async_set_state(self, attr_id, attr_name, value): """Set the state.""" - self._state = bool(value) + self._attr_state = bool(value) if value: self._off_with_transition = False self._off_brightness = None @@ -529,9 +522,9 @@ class Light(BaseLight, ZhaEntity): @callback def async_restore_last_state(self, last_state): """Restore previous state.""" - self._state = last_state.state == STATE_ON + self._attr_state = last_state.state == STATE_ON if "brightness" in last_state.attributes: - self._brightness = last_state.attributes["brightness"] + self._attr_brightness = last_state.attributes["brightness"] if "off_with_transition" in last_state.attributes: self._off_with_transition = last_state.attributes["off_with_transition"] if "off_brightness" in last_state.attributes: @@ -539,15 +532,17 @@ class Light(BaseLight, ZhaEntity): if "color_mode" in last_state.attributes: self._attr_color_mode = ColorMode(last_state.attributes["color_mode"]) if "color_temp" in last_state.attributes: - self._color_temp = last_state.attributes["color_temp"] + self._attr_color_temp = last_state.attributes["color_temp"] + if "xy_color" in last_state.attributes: + self._attr_xy_color = last_state.attributes["xy_color"] if "hs_color" in last_state.attributes: - self._hs_color = last_state.attributes["hs_color"] + self._attr_hs_color = last_state.attributes["hs_color"] if "effect" in last_state.attributes: - self._effect = last_state.attributes["effect"] + self._attr_effect = last_state.attributes["effect"] async def async_get_state(self): """Attempt to retrieve the state from the light.""" - if not self.available: + if not self._attr_available: return self.debug("polling current state") if self._on_off_channel: @@ -555,21 +550,32 @@ class Light(BaseLight, ZhaEntity): "on_off", from_cache=False ) if state is not None: - self._state = state + self._attr_state = state if self._level_channel: level = await self._level_channel.get_attribute_value( "current_level", from_cache=False ) if level is not None: - self._brightness = level + self._attr_brightness = level if self._color_channel: attributes = [ "color_mode", - "color_temperature", "current_x", "current_y", - "color_loop_active", ] + if self._color_channel.enhanced_hue_supported: + attributes.append("enhanced_current_hue") + attributes.append("current_saturation") + if ( + self._color_channel.hs_supported + and not self._color_channel.enhanced_hue_supported + ): + attributes.append("current_hue") + attributes.append("current_saturation") + if self._color_channel.color_temp_supported: + attributes.append("color_temperature") + if self._color_channel.color_loop_supported: + attributes.append("color_loop_active") results = await self._color_channel.get_attributes( attributes, from_cache=False, only_cache=False @@ -580,24 +586,40 @@ class Light(BaseLight, ZhaEntity): self._attr_color_mode = ColorMode.COLOR_TEMP color_temp = results.get("color_temperature") if color_temp is not None and color_mode: - self._color_temp = color_temp - self._hs_color = None - else: + self._attr_color_temp = color_temp + self._attr_xy_color = None + self._attr_hs_color = None + elif color_mode == Color.ColorMode.Hue_and_saturation: self._attr_color_mode = ColorMode.HS + if self._color_channel.enhanced_hue_supported: + current_hue = results.get("enhanced_current_hue") + else: + current_hue = results.get("current_hue") + current_saturation = results.get("current_saturation") + if current_hue is not None and current_saturation is not None: + self._attr_hs_color = ( + int(current_hue * 360 / 65535) + if self._color_channel.enhanced_hue_supported + else int(current_hue * 360 / 254), + int(current_saturation / 254), + ) + self._attr_xy_color = None + self._attr_color_temp = None + else: + self._attr_color_mode = Color.ColorMode.X_and_Y color_x = results.get("current_x") color_y = results.get("current_y") if color_x is not None and color_y is not None: - self._hs_color = color_util.color_xy_to_hs( - float(color_x / 65535), float(color_y / 65535) - ) - self._color_temp = None + self._attr_xy_color = (color_x / 65535, color_y / 65535) + self._attr_color_temp = None + self._attr_hs_color = None color_loop_active = results.get("color_loop_active") if color_loop_active is not None: if color_loop_active == 1: - self._effect = light.EFFECT_COLORLOOP + self._attr_effect = light.EFFECT_COLORLOOP else: - self._effect = None + self._attr_effect = None async def async_update(self): """Update to the latest state.""" @@ -671,6 +693,12 @@ class LightGroup(BaseLight, ZhaGroupEntity): ) self._attr_color_mode = None + # remove this when all ZHA platforms and base entities are updated + @property + def available(self) -> bool: + """Return entity availability.""" + return self._attr_available + async def async_added_to_hass(self): """Run when about to be added to hass.""" await super().async_added_to_hass() @@ -700,39 +728,49 @@ class LightGroup(BaseLight, ZhaGroupEntity): states: list[State] = list(filter(None, all_states)) on_states = [state for state in states if state.state == STATE_ON] - self._state = len(on_states) > 0 - self._available = any(state.state != STATE_UNAVAILABLE for state in states) + self._attr_state = len(on_states) > 0 + self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states) - self._brightness = helpers.reduce_attribute(on_states, ATTR_BRIGHTNESS) - - self._hs_color = helpers.reduce_attribute( - on_states, ATTR_HS_COLOR, reduce=helpers.mean_tuple + self._attr_brightness = helpers.reduce_attribute( + on_states, light.ATTR_BRIGHTNESS ) - self._color_temp = helpers.reduce_attribute(on_states, ATTR_COLOR_TEMP) - self._min_mireds = helpers.reduce_attribute( - states, ATTR_MIN_MIREDS, default=153, reduce=min - ) - self._max_mireds = helpers.reduce_attribute( - states, ATTR_MAX_MIREDS, default=500, reduce=max + self._attr_xy_color = helpers.reduce_attribute( + on_states, light.ATTR_XY_COLOR, reduce=helpers.mean_tuple ) - self._effect_list = None - all_effect_lists = list(helpers.find_state_attributes(states, ATTR_EFFECT_LIST)) + self._attr_hs_color = helpers.reduce_attribute( + on_states, light.ATTR_HS_COLOR, reduce=helpers.mean_tuple + ) + + self._attr_color_temp = helpers.reduce_attribute( + on_states, light.ATTR_COLOR_TEMP + ) + self._attr_min_mireds = helpers.reduce_attribute( + states, light.ATTR_MIN_MIREDS, default=153, reduce=min + ) + self._attr_max_mireds = helpers.reduce_attribute( + states, light.ATTR_MAX_MIREDS, default=500, reduce=max + ) + + self._attr_effect_list = None + all_effect_lists = list( + helpers.find_state_attributes(states, light.ATTR_EFFECT_LIST) + ) if all_effect_lists: # Merge all effects from all effect_lists with a union merge. - self._effect_list = list(set().union(*all_effect_lists)) + self._attr_effect_list = list(set().union(*all_effect_lists)) - self._effect = None - all_effects = list(helpers.find_state_attributes(on_states, ATTR_EFFECT)) + self._attr_effect = None + all_effects = list(helpers.find_state_attributes(on_states, light.ATTR_EFFECT)) if all_effects: # Report the most common effect. effects_count = Counter(itertools.chain(all_effects)) - self._effect = effects_count.most_common(1)[0][0] + self._attr_effect = effects_count.most_common(1)[0][0] self._attr_color_mode = None all_color_modes = list( - helpers.find_state_attributes(on_states, ATTR_COLOR_MODE) + helpers.find_state_attributes(on_states, light.ATTR_COLOR_MODE) ) if all_color_modes: # Report the most common color mode, select brightness and onoff last @@ -745,7 +783,7 @@ class LightGroup(BaseLight, ZhaGroupEntity): self._attr_supported_color_modes = None all_supported_color_modes = list( - helpers.find_state_attributes(states, ATTR_SUPPORTED_COLOR_MODES) + helpers.find_state_attributes(states, light.ATTR_SUPPORTED_COLOR_MODES) ) if all_supported_color_modes: # Merge all color modes. @@ -753,14 +791,14 @@ class LightGroup(BaseLight, ZhaGroupEntity): set[str], set().union(*all_supported_color_modes) ) - self._supported_features = 0 + self._attr_supported_features = 0 for support in helpers.find_state_attributes(states, ATTR_SUPPORTED_FEATURES): # Merge supported features by emulating support for every feature # we find. - self._supported_features |= support + self._attr_supported_features |= support # Bitwise-and the supported features with the GroupedLight's features # so that we don't break in the future when a new feature is added. - self._supported_features &= SUPPORT_GROUP_LIGHT + self._attr_supported_features &= SUPPORT_GROUP_LIGHT async def _force_member_updates(self): """Force the update of member entities to ensure the states are correct for bulbs that don't report their state.""" diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index 7701992cab4..6aba5500a2a 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -151,7 +151,18 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock): }, ), (0x0202, 1, {"fan_mode"}), - (0x0300, 1, {"current_x", "current_y", "color_temperature"}), + ( + 0x0300, + 1, + { + "current_x", + "current_y", + "color_temperature", + "current_hue", + "enhanced_current_hue", + "current_saturation", + }, + ), (0x0400, 1, {"measured_value"}), (0x0401, 1, {"level_status"}), (0x0402, 1, {"measured_value"}), diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 982ff622341..f892448dc69 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -15,11 +15,7 @@ from homeassistant.components.light import ( ColorMode, ) from homeassistant.components.zha.core.group import GroupMember -from homeassistant.components.zha.light import ( - CAPABILITIES_COLOR_TEMP, - CAPABILITIES_COLOR_XY, - FLASH_EFFECTS, -) +from homeassistant.components.zha.light import FLASH_EFFECTS from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform import homeassistant.util.dt as dt_util @@ -148,7 +144,8 @@ async def device_light_1(hass, zigpy_device_mock, zha_device_joined): ) color_cluster = zigpy_device.endpoints[1].light_color color_cluster.PLUGGED_ATTR_READS = { - "color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY + "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature + | lighting.Color.ColorCapabilities.XY_attributes } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True @@ -180,7 +177,8 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined): ) color_cluster = zigpy_device.endpoints[1].light_color color_cluster.PLUGGED_ATTR_READS = { - "color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY + "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature + | lighting.Color.ColorCapabilities.XY_attributes } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True @@ -239,7 +237,8 @@ async def eWeLink_light(hass, zigpy_device_mock, zha_device_joined): ) color_cluster = zigpy_device.endpoints[1].light_color color_cluster.PLUGGED_ATTR_READS = { - "color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY + "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature + | lighting.Color.ColorCapabilities.XY_attributes } zha_device = await zha_device_joined(zigpy_device) zha_device.available = True @@ -302,7 +301,7 @@ async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored ) @pytest.mark.parametrize( "device, reporting", - [(LIGHT_ON_OFF, (1, 0, 0)), (LIGHT_LEVEL, (1, 1, 0)), (LIGHT_COLOR, (1, 1, 3))], + [(LIGHT_ON_OFF, (1, 0, 0)), (LIGHT_LEVEL, (1, 1, 0)), (LIGHT_COLOR, (1, 1, 6))], ) async def test_light( hass, zigpy_device_mock, zha_device_joined_restored, device, reporting @@ -1400,7 +1399,7 @@ async def test_zha_group_light_entity( assert group_state.state == STATE_OFF assert group_state.attributes["supported_color_modes"] == [ ColorMode.COLOR_TEMP, - ColorMode.HS, + ColorMode.XY, ] # Light which is off has no color mode assert "color_mode" not in group_state.attributes @@ -1431,9 +1430,9 @@ async def test_zha_group_light_entity( assert group_state.state == STATE_ON assert group_state.attributes["supported_color_modes"] == [ ColorMode.COLOR_TEMP, - ColorMode.HS, + ColorMode.XY, ] - assert group_state.attributes["color_mode"] == ColorMode.HS + assert group_state.attributes["color_mode"] == ColorMode.XY # test long flashing the lights from the HA await async_test_flash_from_hass( From 975378ba44803f409c5ad4f0c805dbc83dadee5b Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Fri, 22 Jul 2022 01:46:16 +0200 Subject: [PATCH 2771/3516] Add ZHA config option for "enhanced light transition from an off-state" (#75151) * Add ZHA config option for "enhanced light transition from an off-state" * Default option to disabled * Always disable "enhanced light transition" for ZHA LightGroups * Rename _enhanced_light_transition to _zha_config_enhanced_light_transition * Remove LightGroup check, as config option always disables for groups * Remove duplicated line * Remove duplicated line * Move ZHA config transition line below other config line * Renamed comments of renamed variable in tests color_provided_while_off -> new_color_provided_while_off * Enable "enhanced light transitions" for testing --- homeassistant/components/zha/core/const.py | 2 ++ homeassistant/components/zha/light.py | 11 +++++++++- homeassistant/components/zha/strings.json | 1 + .../components/zha/translations/en.json | 1 + tests/components/zha/conftest.py | 5 ++++- tests/components/zha/test_light.py | 22 +++++++++---------- 6 files changed, 29 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index de373215e52..acac96f3162 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -128,6 +128,7 @@ CONF_CUSTOM_QUIRKS_PATH = "custom_quirks_path" CONF_DATABASE = "database_path" CONF_DEFAULT_LIGHT_TRANSITION = "default_light_transition" CONF_DEVICE_CONFIG = "device_config" +CONF_ENABLE_ENHANCED_LIGHT_TRANSITION = "enhanced_light_transition" CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join" CONF_ENABLE_QUIRKS = "enable_quirks" CONF_FLOWCONTROL = "flow_control" @@ -143,6 +144,7 @@ CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY = 60 * 60 * 6 # 6 hours CONF_ZHA_OPTIONS_SCHEMA = vol.Schema( { vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION): cv.positive_int, + vol.Required(CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, default=False): cv.boolean, vol.Required(CONF_ENABLE_IDENTIFY_ON_JOIN, default=True): cv.boolean, vol.Optional( CONF_CONSIDER_UNAVAILABLE_MAINS, diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 8aab3d20a5c..6c26e17188b 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -41,6 +41,7 @@ from .core.const import ( CHANNEL_LEVEL, CHANNEL_ON_OFF, CONF_DEFAULT_LIGHT_TRANSITION, + CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, DATA_ZHA, EFFECT_BLINK, EFFECT_BREATHE, @@ -117,6 +118,7 @@ class BaseLight(LogMixin, light.LightEntity): self._off_with_transition: bool = False self._off_brightness: int | None = None self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME + self._zha_config_enhanced_light_transition: bool = False self._on_off_channel = None self._level_channel = None self._color_channel = None @@ -174,7 +176,7 @@ class BaseLight(LogMixin, light.LightEntity): # move to level, on, color, move to level... We also will not set this if the bulb is already in the # desired color mode with the desired color or color temperature. new_color_provided_while_off = ( - not isinstance(self, LightGroup) + self._zha_config_enhanced_light_transition and not self._FORCE_ON and not self._attr_state and ( @@ -482,6 +484,12 @@ class Light(BaseLight, ZhaEntity): CONF_DEFAULT_LIGHT_TRANSITION, 0, ) + self._zha_config_enhanced_light_transition = async_get_zha_config_value( + zha_device.gateway.config_entry, + ZHA_OPTIONS, + CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, + False, + ) @callback def async_set_state(self, attr_id, attr_name, value): @@ -691,6 +699,7 @@ class LightGroup(BaseLight, ZhaGroupEntity): CONF_DEFAULT_LIGHT_TRANSITION, 0, ) + self._zha_config_enhanced_light_transition = False self._attr_color_mode = None # remove this when all ZHA platforms and base entities are updated diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 5953df52e92..eb7753276f8 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -37,6 +37,7 @@ "config_panel": { "zha_options": { "title": "Global Options", + "enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state", "enable_identify_on_join": "Enable identify effect when devices join the network", "default_light_transition": "Default light transition time (seconds)", "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)", diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index 00c78101a53..91c23acc044 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)", "default_light_transition": "Default light transition time (seconds)", "enable_identify_on_join": "Enable identify effect when devices join the network", + "enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state", "title": "Global Options" } }, diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index d9223027668..b1041a3e2a3 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -70,11 +70,14 @@ async def config_entry_fixture(hass): }, options={ zha_const.CUSTOM_CONFIGURATION: { + zha_const.ZHA_OPTIONS: { + zha_const.CONF_ENABLE_ENHANCED_LIGHT_TRANSITION: True, + }, zha_const.ZHA_ALARM_OPTIONS: { zha_const.CONF_ALARM_ARM_REQUIRES_CODE: False, zha_const.CONF_ALARM_MASTER_CODE: "4321", zha_const.CONF_ALARM_FAILED_TRIES: 2, - } + }, } }, ) diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index f892448dc69..c5b02f1a02f 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -560,7 +560,7 @@ async def test_transitions( dev1_cluster_level.request.reset_mock() - # test non 0 length transition and color temp while turning light on (color_provided_while_off) + # test non 0 length transition and color temp while turning light on (new_color_provided_while_off) await hass.services.async_call( LIGHT_DOMAIN, "turn_on", @@ -596,7 +596,7 @@ async def test_transitions( 10, dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, 235, # color temp mireds - 0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off + 0, # transition time (ZCL time in 10ths of a second) - no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -645,7 +645,7 @@ async def test_transitions( dev1_cluster_color.request.reset_mock() dev1_cluster_level.request.reset_mock() - # test no transition provided and color temp while turning light on (color_provided_while_off) + # test no transition provided and color temp while turning light on (new_color_provided_while_off) await hass.services.async_call( LIGHT_DOMAIN, "turn_on", @@ -680,7 +680,7 @@ async def test_transitions( 10, dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, 236, # color temp mireds - 0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off + 0, # transition time (ZCL time in 10ths of a second) - no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -761,7 +761,7 @@ async def test_transitions( 10, dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, 236, # color temp mireds - 0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off + 0, # transition time (ZCL time in 10ths of a second) - no transition when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -854,7 +854,7 @@ async def test_transitions( dev2_cluster_on_off.request.reset_mock() - # test non 0 length transition and color temp while turning light on and sengled (color_provided_while_off) + # test non 0 length transition and color temp while turning light on and sengled (new_color_provided_while_off) await hass.services.async_call( LIGHT_DOMAIN, "turn_on", @@ -890,7 +890,7 @@ async def test_transitions( 10, dev2_cluster_color.commands_by_name["move_to_color_temp"].schema, 235, # color temp mireds - 1, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when color_provided_while_off + 1, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -937,7 +937,7 @@ async def test_transitions( dev2_cluster_on_off.request.reset_mock() - # test non 0 length transition and color temp while turning group light on (color_provided_while_off) + # test non 0 length transition and color temp while turning group light on (new_color_provided_while_off) await hass.services.async_call( LIGHT_DOMAIN, "turn_on", @@ -960,13 +960,13 @@ async def test_transitions( assert group_level_channel.request.call_count == 1 assert group_level_channel.request.await_count == 1 - # groups are omitted from the 3 call dance for color_provided_while_off + # groups are omitted from the 3 call dance for new_color_provided_while_off assert group_color_channel.request.call_args == call( False, 10, dev2_cluster_color.commands_by_name["move_to_color_temp"].schema, 235, # color temp mireds - 10.0, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when color_provided_while_off + 10.0, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when new_color_provided_while_off expect_reply=True, manufacturer=None, tries=1, @@ -1074,7 +1074,7 @@ async def test_transitions( dev2_cluster_level.request.reset_mock() - # test eWeLink color temp while turning light on from off (color_provided_while_off) + # test eWeLink color temp while turning light on from off (new_color_provided_while_off) await hass.services.async_call( LIGHT_DOMAIN, "turn_on", From 90ca3fe350a293d77f161d48164c5f25934a4f8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 Jul 2022 19:16:45 -0500 Subject: [PATCH 2772/3516] Improve availability tracking and coordinator setup in bluetooth (#75582) --- .../components/bluetooth/__init__.py | 94 +++++-- homeassistant/components/bluetooth/models.py | 10 +- .../bluetooth/passive_update_coordinator.py | 100 +++---- .../components/bluetooth/strings.json | 16 ++ .../components/bluetooth/translations/en.json | 16 ++ tests/components/bluetooth/test_init.py | 53 ++++ .../test_passive_update_coordinator.py | 263 ++++++++++-------- 7 files changed, 357 insertions(+), 195 deletions(-) create mode 100644 homeassistant/components/bluetooth/strings.json create mode 100644 homeassistant/components/bluetooth/translations/en.json diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 5edc0053203..46d1e5e8332 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from datetime import datetime, timedelta from enum import Enum import fnmatch import logging @@ -22,6 +23,7 @@ from homeassistant.core import ( callback as hass_callback, ) from homeassistant.helpers import discovery_flow +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.helpers.typing import ConfigType from homeassistant.loader import ( @@ -39,6 +41,8 @@ _LOGGER = logging.getLogger(__name__) MAX_REMEMBER_ADDRESSES: Final = 2048 +UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 + SOURCE_LOCAL: Final = "local" @@ -160,6 +164,20 @@ def async_register_callback( return manager.async_register_callback(callback, match_dict) +@hass_callback +def async_track_unavailable( + hass: HomeAssistant, + callback: Callable[[str], None], + address: str, +) -> Callable[[], None]: + """Register to receive a callback when an address is unavailable. + + Returns a callback that can be used to cancel the registration. + """ + manager: BluetoothManager = hass.data[DOMAIN] + return manager.async_track_unavailable(callback, address) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the bluetooth integration.""" integration_matchers = await async_get_bluetooth(hass) @@ -231,6 +249,8 @@ class BluetoothManager: self._integration_matchers = integration_matchers self.scanner: HaBleakScanner | None = None self._cancel_device_detected: CALLBACK_TYPE | None = None + self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None + self._unavailable_callbacks: dict[str, list[Callable[[str], None]]] = {} self._callbacks: list[ tuple[BluetoothCallback, BluetoothCallbackMatcher | None] ] = [] @@ -251,6 +271,7 @@ class BluetoothManager: ) return install_multiple_bleak_catcher(self.scanner) + self.async_setup_unavailable_tracking() # We have to start it right away as some integrations might # need it straight away. _LOGGER.debug("Starting bluetooth scanner") @@ -261,6 +282,34 @@ class BluetoothManager: self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) await self.scanner.start() + @hass_callback + def async_setup_unavailable_tracking(self) -> None: + """Set up the unavailable tracking.""" + + @hass_callback + def _async_check_unavailable(now: datetime) -> None: + """Watch for unavailable devices.""" + assert models.HA_BLEAK_SCANNER is not None + scanner = models.HA_BLEAK_SCANNER + history = set(scanner.history) + active = {device.address for device in scanner.discovered_devices} + disappeared = history.difference(active) + for address in disappeared: + del scanner.history[address] + if not (callbacks := self._unavailable_callbacks.get(address)): + continue + for callback in callbacks: + try: + callback(address) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in unavailable callback") + + self._cancel_unavailable_tracking = async_track_time_interval( + self.hass, + _async_check_unavailable, + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS), + ) + @hass_callback def _device_detected( self, device: BLEDevice, advertisement_data: AdvertisementData @@ -283,12 +332,13 @@ class BluetoothManager: } if matched_domains: self._matched[match_key] = True - _LOGGER.debug( - "Device detected: %s with advertisement_data: %s matched domains: %s", - device, - advertisement_data, - matched_domains, - ) + + _LOGGER.debug( + "Device detected: %s with advertisement_data: %s matched domains: %s", + device, + advertisement_data, + matched_domains, + ) if not matched_domains and not self._callbacks: return @@ -321,6 +371,21 @@ class BluetoothManager: service_info, ) + @hass_callback + def async_track_unavailable( + self, callback: Callable[[str], None], address: str + ) -> Callable[[], None]: + """Register a callback.""" + self._unavailable_callbacks.setdefault(address, []).append(callback) + + @hass_callback + def _async_remove_callback() -> None: + self._unavailable_callbacks[address].remove(callback) + if not self._unavailable_callbacks[address]: + del self._unavailable_callbacks[address] + + return _async_remove_callback + @hass_callback def async_register_callback( self, @@ -369,25 +434,17 @@ class BluetoothManager: def async_address_present(self, address: str) -> bool: """Return if the address is present.""" return bool( - models.HA_BLEAK_SCANNER - and any( - device.address == address - for device in models.HA_BLEAK_SCANNER.discovered_devices - ) + models.HA_BLEAK_SCANNER and address in models.HA_BLEAK_SCANNER.history ) @hass_callback def async_discovered_service_info(self) -> list[BluetoothServiceInfoBleak]: """Return if the address is present.""" if models.HA_BLEAK_SCANNER: - discovered = models.HA_BLEAK_SCANNER.discovered_devices history = models.HA_BLEAK_SCANNER.history return [ - BluetoothServiceInfoBleak.from_advertisement( - *history[device.address], SOURCE_LOCAL - ) - for device in discovered - if device.address in history + BluetoothServiceInfoBleak.from_advertisement(*device_adv, SOURCE_LOCAL) + for device_adv in history.values() ] return [] @@ -396,6 +453,9 @@ class BluetoothManager: if self._cancel_device_detected: self._cancel_device_detected() self._cancel_device_detected = None + if self._cancel_unavailable_tracking: + self._cancel_unavailable_tracking() + self._cancel_unavailable_tracking = None if self.scanner: await self.scanner.stop() models.HA_BLEAK_SCANNER = None diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index fd7559aeefa..ffb0ad107ec 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from collections.abc import Mapping import contextlib import logging from typing import Any, Final, cast @@ -14,7 +13,6 @@ from bleak.backends.scanner import ( AdvertisementDataCallback, BaseBleakScanner, ) -from lru import LRU # pylint: disable=no-name-in-module from homeassistant.core import CALLBACK_TYPE, callback as hass_callback @@ -24,8 +22,6 @@ FILTER_UUIDS: Final = "UUIDs" HA_BLEAK_SCANNER: HaBleakScanner | None = None -MAX_HISTORY_SIZE: Final = 512 - def _dispatch_callback( callback: AdvertisementDataCallback, @@ -57,9 +53,7 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] self._callbacks: list[ tuple[AdvertisementDataCallback, dict[str, set[str]]] ] = [] - self.history: Mapping[str, tuple[BLEDevice, AdvertisementData]] = LRU( - MAX_HISTORY_SIZE - ) + self.history: dict[str, tuple[BLEDevice, AdvertisementData]] = {} super().__init__(*args, **kwargs) @hass_callback @@ -90,7 +84,7 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] Here we get the actual callback from bleak and dispatch it to all the wrapped HaBleakScannerWrapper classes """ - self.history[device.address] = (device, advertisement_data) # type: ignore[index] + self.history[device.address] = (device, advertisement_data) for callback_filters in self._callbacks: _dispatch_callback(*callback_filters, device, advertisement_data) diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 77b3fc54aba..b2fdd5d7d58 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -1,9 +1,8 @@ """The Bluetooth integration.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Mapping import dataclasses -from datetime import datetime import logging import time from typing import Any, Generic, TypeVar @@ -14,19 +13,15 @@ from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later from . import ( BluetoothCallbackMatcher, BluetoothChange, - async_address_present, async_register_callback, + async_track_unavailable, ) from .const import DOMAIN -UNAVAILABLE_SECONDS = 60 * 5 -NEVER_TIME = -UNAVAILABLE_SECONDS - @dataclasses.dataclass(frozen=True) class PassiveBluetoothEntityKey: @@ -49,10 +44,13 @@ class PassiveBluetoothDataUpdate(Generic[_T]): """Generic bluetooth data.""" devices: dict[str | None, DeviceInfo] = dataclasses.field(default_factory=dict) - entity_descriptions: dict[ + entity_descriptions: Mapping[ PassiveBluetoothEntityKey, EntityDescription ] = dataclasses.field(default_factory=dict) - entity_data: dict[PassiveBluetoothEntityKey, _T] = dataclasses.field( + entity_names: Mapping[PassiveBluetoothEntityKey, str | None] = dataclasses.field( + default_factory=dict + ) + entity_data: Mapping[PassiveBluetoothEntityKey, _T] = dataclasses.field( default_factory=dict ) @@ -106,6 +104,7 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): ] = {} self.update_method = update_method + self.entity_names: dict[PassiveBluetoothEntityKey, str | None] = {} self.entity_data: dict[PassiveBluetoothEntityKey, _T] = {} self.entity_descriptions: dict[ PassiveBluetoothEntityKey, EntityDescription @@ -113,54 +112,45 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): self.devices: dict[str | None, DeviceInfo] = {} self.last_update_success = True - self._last_callback_time: float = NEVER_TIME - self._cancel_track_available: CALLBACK_TYPE | None = None - self._present = False + self._cancel_track_unavailable: CALLBACK_TYPE | None = None + self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None + self.present = False + self.last_seen = 0.0 @property def available(self) -> bool: """Return if the device is available.""" - return self._present and self.last_update_success + return self.present and self.last_update_success @callback - def _async_cancel_available_tracker(self) -> None: - """Reset the available tracker.""" - if self._cancel_track_available: - self._cancel_track_available() - self._cancel_track_available = None - - @callback - def _async_schedule_available_tracker(self, time_remaining: float) -> None: - """Schedule the available tracker.""" - self._cancel_track_available = async_call_later( - self.hass, time_remaining, self._async_check_device_present - ) - - @callback - def _async_check_device_present(self, _: datetime) -> None: - """Check if the device is present.""" - time_passed_since_seen = time.monotonic() - self._last_callback_time - self._async_cancel_available_tracker() - if ( - not self._present - or time_passed_since_seen < UNAVAILABLE_SECONDS - or async_address_present(self.hass, self.address) - ): - self._async_schedule_available_tracker( - UNAVAILABLE_SECONDS - time_passed_since_seen - ) - return - self._present = False + def _async_handle_unavailable(self, _address: str) -> None: + """Handle the device going unavailable.""" + self.present = False self.async_update_listeners(None) @callback - def async_setup(self) -> CALLBACK_TYPE: - """Start the callback.""" - return async_register_callback( + def _async_start(self) -> None: + """Start the callbacks.""" + self._cancel_bluetooth_advertisements = async_register_callback( self.hass, self._async_handle_bluetooth_event, BluetoothCallbackMatcher(address=self.address), ) + self._cancel_track_unavailable = async_track_unavailable( + self.hass, + self._async_handle_unavailable, + self.address, + ) + + @callback + def _async_stop(self) -> None: + """Stop the callbacks.""" + if self._cancel_bluetooth_advertisements is not None: + self._cancel_bluetooth_advertisements() + self._cancel_bluetooth_advertisements = None + if self._cancel_track_unavailable is not None: + self._cancel_track_unavailable() + self._cancel_track_unavailable = None @callback def async_add_entities_listener( @@ -199,10 +189,22 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): def remove_listener() -> None: """Remove update listener.""" self._listeners.remove(update_callback) + self._async_handle_listeners_changed() self._listeners.append(update_callback) + self._async_handle_listeners_changed() return remove_listener + @callback + def _async_handle_listeners_changed(self) -> None: + """Handle listeners changed.""" + has_listeners = self._listeners or self._entity_key_listeners + running = bool(self._cancel_bluetooth_advertisements) + if running and not has_listeners: + self._async_stop() + elif not running and has_listeners: + self._async_start() + @callback def async_add_entity_key_listener( self, @@ -217,8 +219,10 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): self._entity_key_listeners[entity_key].remove(update_callback) if not self._entity_key_listeners[entity_key]: del self._entity_key_listeners[entity_key] + self._async_handle_listeners_changed() self._entity_key_listeners.setdefault(entity_key, []).append(update_callback) + self._async_handle_listeners_changed() return remove_listener @callback @@ -242,11 +246,9 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" + self.last_seen = time.monotonic() self.name = service_info.name - self._last_callback_time = time.monotonic() - self._present = True - if not self._cancel_track_available: - self._async_schedule_available_tracker(UNAVAILABLE_SECONDS) + self.present = True if self.hass.is_stopping: return @@ -272,6 +274,7 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): self.devices.update(new_data.devices) self.entity_descriptions.update(new_data.entity_descriptions) self.entity_data.update(new_data.entity_data) + self.entity_names.update(new_data.entity_names) self.async_update_listeners(new_data) @@ -315,6 +318,7 @@ class PassiveBluetoothCoordinatorEntity( self._attr_unique_id = f"{address}-{key}" if ATTR_NAME not in self._attr_device_info: self._attr_device_info[ATTR_NAME] = self.coordinator.name + self._attr_name = coordinator.entity_names.get(entity_key) @property def available(self) -> bool: diff --git a/homeassistant/components/bluetooth/strings.json b/homeassistant/components/bluetooth/strings.json new file mode 100644 index 00000000000..925e9c512cc --- /dev/null +++ b/homeassistant/components/bluetooth/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "description": "Choose a device to setup", + "data": { + "address": "Device" + } + }, + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + } + } + } +} diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json new file mode 100644 index 00000000000..f75dc2603db --- /dev/null +++ b/homeassistant/components/bluetooth/translations/en.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 1a002e5e354..e76ad559305 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration.""" +from datetime import timedelta from unittest.mock import MagicMock, patch from bleak import BleakError @@ -7,12 +8,18 @@ from bleak.backends.scanner import AdvertisementData, BLEDevice from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( SOURCE_LOCAL, + UNAVAILABLE_TRACK_SECONDS, BluetoothChange, BluetoothServiceInfo, + async_track_unavailable, models, ) from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import callback from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed async def test_setup_and_stop(hass, mock_bleak_scanner_start): @@ -241,9 +248,55 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + wrong_device_went_unavailable = False + switchbot_device_went_unavailable = False + + @callback + def _wrong_device_unavailable_callback(_address: str) -> None: + """Wrong device unavailable callback.""" + nonlocal wrong_device_went_unavailable + wrong_device_went_unavailable = True + raise ValueError("blow up") + + @callback + def _switchbot_device_unavailable_callback(_address: str) -> None: + """Switchbot device unavailable callback.""" + nonlocal switchbot_device_went_unavailable + switchbot_device_went_unavailable = True + + wrong_device_unavailable_cancel = async_track_unavailable( + hass, _wrong_device_unavailable_callback, wrong_device.address + ) + switchbot_device_unavailable_cancel = async_track_unavailable( + hass, _switchbot_device_unavailable_callback, switchbot_device.address + ) + + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) await hass.async_block_till_done() service_infos = bluetooth.async_discovered_service_info(hass) + assert switchbot_device_went_unavailable is False + assert wrong_device_went_unavailable is True + + # See the devices again + models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv) + models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + # Cancel the callbacks + wrong_device_unavailable_cancel() + switchbot_device_unavailable_cancel() + wrong_device_went_unavailable = False + switchbot_device_went_unavailable = False + + # Verify the cancel is effective + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) + await hass.async_block_till_done() + assert switchbot_device_went_unavailable is False + assert wrong_device_went_unavailable is False + assert len(service_infos) == 1 # wrong_name should not appear because bleak no longer sees it assert service_infos[0].name == "wohand" diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index a377a8f4a46..755b2ec07f8 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -3,15 +3,17 @@ from __future__ import annotations from datetime import timedelta import logging -import time from unittest.mock import MagicMock, patch from home_assistant_bluetooth import BluetoothServiceInfo import pytest -from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.bluetooth import ( + DOMAIN, + UNAVAILABLE_TRACK_SECONDS, + BluetoothChange, +) from homeassistant.components.bluetooth.passive_update_coordinator import ( - UNAVAILABLE_SECONDS, PassiveBluetoothCoordinatorEntity, PassiveBluetoothDataUpdate, PassiveBluetoothDataUpdateCoordinator, @@ -21,6 +23,7 @@ from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescr from homeassistant.const import TEMP_CELSIUS from homeassistant.core import CoreState, callback from homeassistant.helpers.entity import DeviceInfo +from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from tests.common import MockEntityPlatform, async_fire_time_changed @@ -49,16 +52,18 @@ GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( PassiveBluetoothEntityKey("temperature", None): 14.5, PassiveBluetoothEntityKey("pressure", None): 1234, }, + entity_names={ + PassiveBluetoothEntityKey("temperature", None): "Temperature", + PassiveBluetoothEntityKey("pressure", None): "Pressure", + }, entity_descriptions={ PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( key="temperature", - name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( key="pressure", - name="Pressure", native_unit_of_measurement="hPa", device_class=SensorDeviceClass.PRESSURE, ), @@ -66,8 +71,9 @@ GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( ) -async def test_basic_usage(hass): +async def test_basic_usage(hass, mock_bleak_scanner_start): """Test basic usage of the PassiveBluetoothDataUpdateCoordinator.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @callback def _async_generate_mock_data( @@ -91,35 +97,36 @@ async def test_basic_usage(hass): "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - cancel_coordinator = coordinator.async_setup() - entity_key = PassiveBluetoothEntityKey("temperature", None) - entity_key_events = [] - all_events = [] - mock_entity = MagicMock() - mock_add_entities = MagicMock() + entity_key = PassiveBluetoothEntityKey("temperature", None) + entity_key_events = [] + all_events = [] + mock_entity = MagicMock() + mock_add_entities = MagicMock() - def _async_entity_key_listener(data: PassiveBluetoothDataUpdate | None) -> None: - """Mock entity key listener.""" - entity_key_events.append(data) + def _async_entity_key_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock entity key listener.""" + entity_key_events.append(data) - cancel_async_add_entity_key_listener = coordinator.async_add_entity_key_listener( - _async_entity_key_listener, - entity_key, - ) + cancel_async_add_entity_key_listener = ( + coordinator.async_add_entity_key_listener( + _async_entity_key_listener, + entity_key, + ) + ) - def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: - """Mock an all listener.""" - all_events.append(data) + def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock an all listener.""" + all_events.append(data) - cancel_listener = coordinator.async_add_listener( - _all_listener, - ) + cancel_listener = coordinator.async_add_listener( + _all_listener, + ) - cancel_async_add_entities_listener = coordinator.async_add_entities_listener( - mock_entity, - mock_add_entities, - ) + cancel_async_add_entities_listener = coordinator.async_add_entities_listener( + mock_entity, + mock_add_entities, + ) saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) @@ -155,11 +162,15 @@ async def test_basic_usage(hass): assert len(mock_entity.mock_calls) == 2 assert coordinator.available is True - cancel_coordinator() - -async def test_unavailable_after_no_data(hass): +async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): """Test that the coordinator is unavailable after no data for a while.""" + with patch( + "bleak.BleakScanner.discovered_devices", # Must patch before we setup + [MagicMock(address="44:44:33:11:23:45")], + ): + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() @callback def _async_generate_mock_data( @@ -183,14 +194,13 @@ async def test_unavailable_after_no_data(hass): "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - cancel_coordinator = coordinator.async_setup() - mock_entity = MagicMock() - mock_add_entities = MagicMock() - coordinator.async_add_entities_listener( - mock_entity, - mock_add_entities, - ) + mock_entity = MagicMock() + mock_add_entities = MagicMock() + coordinator.async_add_entities_listener( + mock_entity, + mock_add_entities, + ) assert coordinator.available is False @@ -198,52 +208,40 @@ async def test_unavailable_after_no_data(hass): assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True - monotonic_now = time.monotonic() - now = dt_util.utcnow() with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.time.monotonic", - return_value=monotonic_now + UNAVAILABLE_SECONDS, + "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", + [MagicMock(address="44:44:33:11:23:45")], + ), patch( + "homeassistant.components.bluetooth.models.HA_BLEAK_SCANNER.history", + {"aa:bb:cc:dd:ee:ff": MagicMock()}, ): - async_fire_time_changed(hass, now + timedelta(seconds=UNAVAILABLE_SECONDS)) + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) await hass.async_block_till_done() assert coordinator.available is False saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True - # Now simulate the device is still present even though we got - # no data for a while - - monotonic_now = time.monotonic() - now = dt_util.utcnow() with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_address_present", - return_value=True, + "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", + [MagicMock(address="44:44:33:11:23:45")], ), patch( - "homeassistant.components.bluetooth.passive_update_coordinator.time.monotonic", - return_value=monotonic_now + UNAVAILABLE_SECONDS, + "homeassistant.components.bluetooth.models.HA_BLEAK_SCANNER.history", + {"aa:bb:cc:dd:ee:ff": MagicMock()}, ): - async_fire_time_changed(hass, now + timedelta(seconds=UNAVAILABLE_SECONDS)) - await hass.async_block_till_done() - - assert coordinator.available is True - - # And finally that it can go unavailable again when its gone - monotonic_now = time.monotonic() - now = dt_util.utcnow() - with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.time.monotonic", - return_value=monotonic_now + UNAVAILABLE_SECONDS, - ): - async_fire_time_changed(hass, now + timedelta(seconds=UNAVAILABLE_SECONDS)) + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) await hass.async_block_till_done() assert coordinator.available is False - cancel_coordinator() - -async def test_no_updates_once_stopping(hass): +async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): """Test updates are ignored once hass is stopping.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @callback def _async_generate_mock_data( @@ -267,17 +265,16 @@ async def test_no_updates_once_stopping(hass): "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - cancel_coordinator = coordinator.async_setup() - all_events = [] + all_events = [] - def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: - """Mock an all listener.""" - all_events.append(data) + def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock an all listener.""" + all_events.append(data) - coordinator.async_add_listener( - _all_listener, - ) + coordinator.async_add_listener( + _all_listener, + ) saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert len(all_events) == 1 @@ -288,11 +285,11 @@ async def test_no_updates_once_stopping(hass): saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert len(all_events) == 1 - cancel_coordinator() - -async def test_exception_from_update_method(hass, caplog): +async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_start): """Test we handle exceptions from the update method.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + run_count = 0 @callback @@ -321,7 +318,7 @@ async def test_exception_from_update_method(hass, caplog): "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - cancel_coordinator = coordinator.async_setup() + coordinator.async_add_listener(MagicMock()) saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert coordinator.available is True @@ -335,11 +332,11 @@ async def test_exception_from_update_method(hass, caplog): saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert coordinator.available is True - cancel_coordinator() - -async def test_bad_data_from_update_method(hass): +async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): """Test we handle bad data from the update method.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + run_count = 0 @callback @@ -368,7 +365,7 @@ async def test_bad_data_from_update_method(hass): "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - cancel_coordinator = coordinator.async_setup() + coordinator.async_add_listener(MagicMock()) saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert coordinator.available is True @@ -383,8 +380,6 @@ async def test_bad_data_from_update_method(hass): saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert coordinator.available is True - cancel_coordinator() - GOVEE_B5178_REMOTE_SERVICE_INFO = BluetoothServiceInfo( name="B5178D6FB", @@ -429,7 +424,6 @@ GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( force_update=False, icon=None, has_entity_name=False, - name="Temperature", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="°C", @@ -446,7 +440,6 @@ GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( force_update=False, icon=None, has_entity_name=False, - name="Humidity", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="%", @@ -463,7 +456,6 @@ GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( force_update=False, icon=None, has_entity_name=False, - name="Battery", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="%", @@ -480,13 +472,20 @@ GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( force_update=False, icon=None, has_entity_name=False, - name="Signal Strength", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="dBm", state_class=None, ), }, + entity_names={ + PassiveBluetoothEntityKey(key="temperature", device_id="remote"): "Temperature", + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): "Humidity", + PassiveBluetoothEntityKey(key="battery", device_id="remote"): "Battery", + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): "Signal Strength", + }, entity_data={ PassiveBluetoothEntityKey(key="temperature", device_id="remote"): 30.8642, PassiveBluetoothEntityKey(key="humidity", device_id="remote"): 64.2, @@ -520,7 +519,6 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( force_update=False, icon=None, has_entity_name=False, - name="Temperature", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="°C", @@ -537,7 +535,6 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( force_update=False, icon=None, has_entity_name=False, - name="Humidity", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="%", @@ -554,7 +551,6 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( force_update=False, icon=None, has_entity_name=False, - name="Battery", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="%", @@ -571,7 +567,6 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( force_update=False, icon=None, has_entity_name=False, - name="Signal Strength", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="dBm", @@ -588,7 +583,6 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( force_update=False, icon=None, has_entity_name=False, - name="Temperature", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="°C", @@ -605,7 +599,6 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( force_update=False, icon=None, has_entity_name=False, - name="Humidity", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="%", @@ -622,7 +615,6 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( force_update=False, icon=None, has_entity_name=False, - name="Battery", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="%", @@ -639,13 +631,30 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( force_update=False, icon=None, has_entity_name=False, - name="Signal Strength", unit_of_measurement=None, last_reset=None, native_unit_of_measurement="dBm", state_class=None, ), }, + entity_names={ + PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ): "Temperature", + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): "Humidity", + PassiveBluetoothEntityKey(key="battery", device_id="remote"): "Battery", + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): "Signal Strength", + PassiveBluetoothEntityKey( + key="temperature", device_id="primary" + ): "Temperature", + PassiveBluetoothEntityKey(key="humidity", device_id="primary"): "Humidity", + PassiveBluetoothEntityKey(key="battery", device_id="primary"): "Battery", + PassiveBluetoothEntityKey( + key="signal_strength", device_id="primary" + ): "Signal Strength", + }, entity_data={ PassiveBluetoothEntityKey(key="temperature", device_id="remote"): 30.8642, PassiveBluetoothEntityKey(key="humidity", device_id="remote"): 64.2, @@ -660,8 +669,9 @@ GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( ) -async def test_integration_with_entity(hass): +async def test_integration_with_entity(hass, mock_bleak_scanner_start): """Test integration of PassiveBluetoothDataUpdateCoordinator with PassiveBluetoothCoordinatorEntity.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) update_count = 0 @@ -691,7 +701,7 @@ async def test_integration_with_entity(hass): "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - coordinator.async_setup() + coordinator.async_add_listener(MagicMock()) mock_add_entities = MagicMock() @@ -770,8 +780,9 @@ NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( ) -async def test_integration_with_entity_without_a_device(hass): +async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner_start): """Test integration with PassiveBluetoothCoordinatorEntity with no device.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @callback def _async_generate_mock_data( @@ -795,14 +806,13 @@ async def test_integration_with_entity_without_a_device(hass): "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - coordinator.async_setup() - mock_add_entities = MagicMock() + mock_add_entities = MagicMock() - coordinator.async_add_entities_listener( - PassiveBluetoothCoordinatorEntity, - mock_add_entities, - ) + coordinator.async_add_entities_listener( + PassiveBluetoothCoordinatorEntity, + mock_add_entities, + ) saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) # First call with just the remote sensor entities results in them being added @@ -826,8 +836,12 @@ async def test_integration_with_entity_without_a_device(hass): ) -async def test_passive_bluetooth_entity_with_entity_platform(hass): +async def test_passive_bluetooth_entity_with_entity_platform( + hass, mock_bleak_scanner_start +): """Test with a mock entity platform.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + entity_platform = MockEntityPlatform(hass) @callback @@ -852,18 +866,23 @@ async def test_passive_bluetooth_entity_with_entity_platform(hass): "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - coordinator.async_setup() - coordinator.async_add_entities_listener( - PassiveBluetoothCoordinatorEntity, - lambda entities: hass.async_create_task( - entity_platform.async_add_entities(entities) - ), + coordinator.async_add_entities_listener( + PassiveBluetoothCoordinatorEntity, + lambda entities: hass.async_create_task( + entity_platform.async_add_entities(entities) + ), + ) + + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert ( + hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_temperature") + is not None + ) + assert ( + hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_pressure") + is not None ) - - saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - await hass.async_block_till_done() - saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - await hass.async_block_till_done() - assert hass.states.get("test_domain.temperature") is not None - assert hass.states.get("test_domain.pressure") is not None From 36138afb93100eabd4059fe7c003fb5730e1c25f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 22 Jul 2022 00:29:27 +0000 Subject: [PATCH 2773/3516] [ci skip] Translation update --- .../components/demo/translations/de.json | 21 +++++++++++++++++++ .../components/demo/translations/et.json | 21 +++++++++++++++++++ .../components/demo/translations/pl.json | 21 +++++++++++++++++++ .../components/demo/translations/tr.json | 21 +++++++++++++++++++ .../components/google/translations/tr.json | 3 ++- .../homekit_controller/translations/tr.json | 4 ++-- .../components/lifx/translations/pl.json | 20 ++++++++++++++++++ .../components/lifx/translations/tr.json | 20 ++++++++++++++++++ .../components/mqtt/translations/bg.json | 2 ++ .../components/plugwise/translations/de.json | 3 ++- .../components/plugwise/translations/pl.json | 3 ++- .../components/plugwise/translations/tr.json | 3 ++- .../components/powerwall/translations/bg.json | 1 + .../components/uscis/translations/ca.json | 7 +++++++ .../components/uscis/translations/et.json | 8 +++++++ .../components/uscis/translations/fr.json | 7 +++++++ .../components/uscis/translations/pl.json | 8 +++++++ .../components/uscis/translations/pt-BR.json | 8 +++++++ .../uscis/translations/zh-Hant.json | 8 +++++++ .../components/zha/translations/pt-BR.json | 1 + 20 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/uscis/translations/ca.json create mode 100644 homeassistant/components/uscis/translations/et.json create mode 100644 homeassistant/components/uscis/translations/fr.json create mode 100644 homeassistant/components/uscis/translations/pl.json create mode 100644 homeassistant/components/uscis/translations/pt-BR.json create mode 100644 homeassistant/components/uscis/translations/zh-Hant.json diff --git a/homeassistant/components/demo/translations/de.json b/homeassistant/components/demo/translations/de.json index fd6239fa787..ab06043d52c 100644 --- a/homeassistant/components/demo/translations/de.json +++ b/homeassistant/components/demo/translations/de.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Dr\u00fccke OK, wenn die Blinkerfl\u00fcssigkeit nachgef\u00fcllt wurde.", + "title": "Blinkerfl\u00fcssigkeit muss nachgef\u00fcllt werden" + } + } + }, + "title": "Die Blinkerfl\u00fcssigkeit ist leer und muss nachgef\u00fcllt werden" + }, + "transmogrifier_deprecated": { + "description": "Die Transmogrifier-Komponente ist jetzt veraltet, da die neue API keine lokale Kontrolle mehr bietet.", + "title": "Die Transmogrifier-Komponente ist veraltet" + }, + "unfixable_problem": { + "description": "Dieses Problem wird niemals aufgeben.", + "title": "Dieses Problem kann nicht behoben werden" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/et.json b/homeassistant/components/demo/translations/et.json index ca06ed9c3bd..0e4c89dba01 100644 --- a/homeassistant/components/demo/translations/et.json +++ b/homeassistant/components/demo/translations/et.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Vajuta OK kui Blinkeri vedelik on uuesti t\u00e4idetud", + "title": "Blinkeri vedelikku on vaja uuesti t\u00e4ita" + } + } + }, + "title": "Blinkeri vedelik on otsas ja seda tuleb uuesti t\u00e4ita" + }, + "transmogrifier_deprecated": { + "description": "Transmogrifier komponent on n\u00fc\u00fcd aegunud, kuna uues API-s puudub kohalik kontroll", + "title": "Transmogrifieri komponent on aegunud" + }, + "unfixable_problem": { + "description": "See teema ei anna kunagi alla.", + "title": "See ei ole lahendatav probleem" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/pl.json b/homeassistant/components/demo/translations/pl.json index bc9e6701c65..c57a1e4f619 100644 --- a/homeassistant/components/demo/translations/pl.json +++ b/homeassistant/components/demo/translations/pl.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Naci\u015bnij OK po uzupe\u0142nieniu p\u0142ynu \u015bwiate\u0142ka", + "title": "P\u0142yn \u015bwiate\u0142ka nale\u017cy uzupe\u0142ni\u0107" + } + } + }, + "title": "P\u0142yn \u015bwiate\u0142ka jest pusty i nale\u017cy go uzupe\u0142ni\u0107" + }, + "transmogrifier_deprecated": { + "description": "Komponent transmogryfikatora jest ju\u017c przestarza\u0142y z powodu braku lokalnej kontroli w nowym API", + "title": "Komponent transmogryfikatora jest przestarza\u0142y" + }, + "unfixable_problem": { + "description": "Ten problem nigdy si\u0119 nie podda.", + "title": "Problem jest nie do naprawienia" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/demo/translations/tr.json b/homeassistant/components/demo/translations/tr.json index 1eea23e1bc5..b7ff98f9cd4 100644 --- a/homeassistant/components/demo/translations/tr.json +++ b/homeassistant/components/demo/translations/tr.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Fla\u015f\u00f6r yeniden dolduruldu\u011funda Tamam'a bas\u0131n", + "title": "Fla\u015f\u00f6r\u00fcn yeniden doldurulmas\u0131 gerekiyor" + } + } + }, + "title": "Fla\u015f\u00f6r bo\u015f ve yeniden doldurulmas\u0131 gerekiyor" + }, + "transmogrifier_deprecated": { + "description": "Transmogrifier bile\u015feni, yeni API'de mevcut olan yerel kontrol eksikli\u011fi nedeniyle art\u0131k kullan\u0131mdan kald\u0131r\u0131lm\u0131\u015ft\u0131r", + "title": "Transmogrifier bile\u015feni kullan\u0131mdan kald\u0131r\u0131ld\u0131" + }, + "unfixable_problem": { + "description": "Bu konudan asla vazge\u00e7ilmeyecek.", + "title": "Bu d\u00fczeltilebilir bir sorun de\u011fil" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/tr.json b/homeassistant/components/google/translations/tr.json index ec265926695..7d67018630f 100644 --- a/homeassistant/components/google/translations/tr.json +++ b/homeassistant/components/google/translations/tr.json @@ -11,7 +11,8 @@ "invalid_access_token": "Ge\u00e7ersiz eri\u015fim anahtar\u0131", "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", "oauth_error": "Ge\u00e7ersiz anahtar verileri al\u0131nd\u0131.", - "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "timeout_connect": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131" }, "create_entry": { "default": "Ba\u015far\u0131yla do\u011fruland\u0131" diff --git a/homeassistant/components/homekit_controller/translations/tr.json b/homeassistant/components/homekit_controller/translations/tr.json index 7ddb32ade8e..3086e3e34fb 100644 --- a/homeassistant/components/homekit_controller/translations/tr.json +++ b/homeassistant/components/homekit_controller/translations/tr.json @@ -18,7 +18,7 @@ "unable_to_pair": "E\u015fle\u015ftirilemiyor, l\u00fctfen tekrar deneyin.", "unknown_error": "Cihaz bilinmeyen bir hata bildirdi. E\u015fle\u015ftirme ba\u015far\u0131s\u0131z oldu." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "T\u00fcm denetleyicilerde e\u015fle\u015ftirmeyi durdurun veya cihaz\u0131 yeniden ba\u015flatmay\u0131 deneyin, ard\u0131ndan e\u015fle\u015ftirmeye devam edin.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "G\u00fcvenli olmayan kurulum kodlar\u0131yla e\u015fle\u015ftirmeye izin verin.", "pairing_code": "E\u015fle\u015ftirme Kodu" }, - "description": "HomeKit Denetleyici, ayr\u0131 bir HomeKit denetleyicisi veya iCloud olmadan g\u00fcvenli bir \u015fifreli ba\u011flant\u0131 kullanarak yerel alan a\u011f\u0131 \u00fczerinden {name} ile ileti\u015fim kurar. Bu aksesuar\u0131 kullanmak i\u00e7in HomeKit e\u015fle\u015ftirme kodunuzu (XXX-XX-XXX bi\u00e7iminde) girin. Bu kod genellikle cihaz\u0131n kendisinde veya ambalaj\u0131nda bulunur.", + "description": "HomeKit Denetleyici, ayr\u0131 bir HomeKit denetleyicisi veya iCloud olmadan g\u00fcvenli bir \u015fifreli ba\u011flant\u0131 kullanarak yerel alan a\u011f\u0131 \u00fczerinden {name} ( {category} ) ile ileti\u015fim kurar. Bu aksesuar\u0131 kullanmak i\u00e7in HomeKit e\u015fle\u015ftirme kodunuzu (XXX-XX-XXX bi\u00e7iminde) girin. Bu kod genellikle cihaz\u0131n kendisinde veya ambalaj\u0131nda bulunur.", "title": "HomeKit Aksesuar Protokol\u00fc arac\u0131l\u0131\u011f\u0131yla bir cihazla e\u015fle\u015ftirin" }, "protocol_error": { diff --git a/homeassistant/components/lifx/translations/pl.json b/homeassistant/components/lifx/translations/pl.json index a8ee3fa57ac..817867d7c62 100644 --- a/homeassistant/components/lifx/translations/pl.json +++ b/homeassistant/components/lifx/translations/pl.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + }, + "discovery_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Urz\u0105dzenie" + } + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Je\u015bli nie podasz IP lub nazwy hosta, zostanie u\u017cyte wykrywanie do odnalezienia urz\u0105dze\u0144." } } } diff --git a/homeassistant/components/lifx/translations/tr.json b/homeassistant/components/lifx/translations/tr.json index ca4cfa92020..0f212e225be 100644 --- a/homeassistant/components/lifx/translations/tr.json +++ b/homeassistant/components/lifx/translations/tr.json @@ -1,12 +1,32 @@ { "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", "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "LIFX'i kurmak istiyor musunuz?" + }, + "discovery_confirm": { + "description": "{label} ( {host} ) {serial} kurmak istiyor musunuz?" + }, + "pick_device": { + "data": { + "device": "Cihaz" + } + }, + "user": { + "data": { + "host": "Sunucu" + }, + "description": "Ana bilgisayar\u0131 bo\u015f b\u0131rak\u0131rsan\u0131z, cihazlar\u0131 bulmak i\u00e7in ke\u015fif kullan\u0131lacakt\u0131r." } } } diff --git a/homeassistant/components/mqtt/translations/bg.json b/homeassistant/components/mqtt/translations/bg.json index 65260eacabb..93b1d77dcfa 100644 --- a/homeassistant/components/mqtt/translations/bg.json +++ b/homeassistant/components/mqtt/translations/bg.json @@ -29,6 +29,8 @@ }, "device_automation": { "trigger_subtype": { + "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", + "button_4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "button_6": "\u0428\u0435\u0441\u0442\u0438 \u0431\u0443\u0442\u043e\u043d" } }, diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index 2b9d112977a..fb80fecef25 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Der Dienst ist bereits konfiguriert" + "already_configured": "Der Dienst ist bereits konfiguriert", + "anna_with_adam": "Sowohl Anna als auch Adam entdeckt. F\u00fcge deinen Adam anstelle deiner Anna hinzu" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/plugwise/translations/pl.json b/homeassistant/components/plugwise/translations/pl.json index 3d6a3a3b354..8de8da8e4e9 100644 --- a/homeassistant/components/plugwise/translations/pl.json +++ b/homeassistant/components/plugwise/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "anna_with_adam": "Wykryto zar\u00f3wno Ann\u0119, jak i Adama. Dodaj Adama zamiast Anny." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json index def3c6f8436..41f52761dbf 100644 --- a/homeassistant/components/plugwise/translations/tr.json +++ b/homeassistant/components/plugwise/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "anna_with_adam": "Anna ve Adam tespit edildi. Anna'n\u0131z\u0131n yerine Adam'\u0131n\u0131z\u0131 ekleyin" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", diff --git a/homeassistant/components/powerwall/translations/bg.json b/homeassistant/components/powerwall/translations/bg.json index 12186a54eec..f0092b14bc1 100644 --- a/homeassistant/components/powerwall/translations/bg.json +++ b/homeassistant/components/powerwall/translations/bg.json @@ -3,6 +3,7 @@ "abort": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, + "flow_title": "{name} ({ip_address})", "step": { "confirm_discovery": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name} ({ip_address})?" diff --git a/homeassistant/components/uscis/translations/ca.json b/homeassistant/components/uscis/translations/ca.json new file mode 100644 index 00000000000..7c836867d89 --- /dev/null +++ b/homeassistant/components/uscis/translations/ca.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "S'est\u00e0 eliminant la integraci\u00f3 USCIS" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/et.json b/homeassistant/components/uscis/translations/et.json new file mode 100644 index 00000000000..0dc9325b715 --- /dev/null +++ b/homeassistant/components/uscis/translations/et.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "USA kodakondsus- ja immigratsiooniteenistuse (USCIS) integratsioon ootab eemaldamist Home Assistantist ja ei ole enam k\u00e4ttesaadav alates Home Assistant 2022.10.\n\nIntegratsioon eemaldatakse, sest see p\u00f5hineb veebiotsingul, mis ei ole lubatud.\n\nProbleemi lahendamiseks eemaldage YAML-konfiguratsioon failist configuration.yaml ja k\u00e4ivitage Home Assistant uuesti.", + "title": "USCIS-i sidumine eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/fr.json b/homeassistant/components/uscis/translations/fr.json new file mode 100644 index 00000000000..8d9e1c3f8ba --- /dev/null +++ b/homeassistant/components/uscis/translations/fr.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "L'int\u00e9gration USCIS est en cours de suppression" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/pl.json b/homeassistant/components/uscis/translations/pl.json new file mode 100644 index 00000000000..82e02996f7c --- /dev/null +++ b/homeassistant/components/uscis/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integracja US Citizenship and Immigration Services (USCIS) oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.10. \n\nIntegracja jest usuwana, poniewa\u017c opiera si\u0119 na webscrapingu, co jest niedozwolone. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Trwa usuwanie integracji USCIS" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/pt-BR.json b/homeassistant/components/uscis/translations/pt-BR.json new file mode 100644 index 00000000000..76182bfa2d2 --- /dev/null +++ b/homeassistant/components/uscis/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "A integra\u00e7\u00e3o dos Servi\u00e7os de Cidadania e Imigra\u00e7\u00e3o dos EUA (USCIS) est\u00e1 pendente de remo\u00e7\u00e3o do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.10. \n\n A integra\u00e7\u00e3o est\u00e1 sendo removida, pois depende de webscraping, o que n\u00e3o \u00e9 permitido. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o do USCIS est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/zh-Hant.json b/homeassistant/components/uscis/translations/zh-Hant.json new file mode 100644 index 00000000000..4a5882dbd95 --- /dev/null +++ b/homeassistant/components/uscis/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u7f8e\u570b\u516c\u6c11\u8207\u79fb\u6c11\u670d\u52d9\uff08USCIS: U.S. Citizenship and Immigration Services\uff09\u6574\u5408\u6b63\u8a08\u5283\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u8acb\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u6574\u5408\u6b63\u5728\u79fb\u9664\u4e2d\u3001\u7531\u65bc\u4f7f\u7528\u4e86\u7db2\u8def\u8cc7\u6599\u64f7\u53d6\uff08webscraping\uff09\u65b9\u5f0f\u3001\u5c07\u4e0d\u88ab\u5141\u8a31\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "USCIS \u6574\u5408\u6b63\u6e96\u5099\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index 8c43bf7f3e6..ba54b4aba87 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "Considerar os dispositivos alimentados pela rede indispon\u00edveis ap\u00f3s (segundos)", "default_light_transition": "Tempo de transi\u00e7\u00e3o de luz padr\u00e3o (segundos)", "enable_identify_on_join": "Ativar o efeito de identifica\u00e7\u00e3o quando os dispositivos ingressarem na rede", + "enhanced_light_transition": "Ative a transi\u00e7\u00e3o de cor/temperatura da luz aprimorada de um estado desligado", "title": "Op\u00e7\u00f5es globais" } }, From 67e16d77e8627b88b3dd811f9b3328cac2873d58 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 Jul 2022 20:31:23 -0500 Subject: [PATCH 2774/3516] Add SensorPush BLE integration (#75531) --- CODEOWNERS | 2 + .../components/sensorpush/__init__.py | 56 +++++++ .../components/sensorpush/config_flow.py | 93 ++++++++++++ homeassistant/components/sensorpush/const.py | 3 + .../components/sensorpush/manifest.json | 15 ++ homeassistant/components/sensorpush/sensor.py | 143 ++++++++++++++++++ .../components/sensorpush/strings.json | 21 +++ .../sensorpush/translations/en.json | 21 +++ homeassistant/generated/bluetooth.py | 4 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/sensorpush/__init__.py | 34 +++++ tests/components/sensorpush/conftest.py | 8 + .../components/sensorpush/test_config_flow.py | 98 ++++++++++++ tests/components/sensorpush/test_sensor.py | 50 ++++++ 16 files changed, 555 insertions(+) create mode 100644 homeassistant/components/sensorpush/__init__.py create mode 100644 homeassistant/components/sensorpush/config_flow.py create mode 100644 homeassistant/components/sensorpush/const.py create mode 100644 homeassistant/components/sensorpush/manifest.json create mode 100644 homeassistant/components/sensorpush/sensor.py create mode 100644 homeassistant/components/sensorpush/strings.json create mode 100644 homeassistant/components/sensorpush/translations/en.json create mode 100644 tests/components/sensorpush/__init__.py create mode 100644 tests/components/sensorpush/conftest.py create mode 100644 tests/components/sensorpush/test_config_flow.py create mode 100644 tests/components/sensorpush/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index dbbb21f0d6c..2a3999779fc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -922,6 +922,8 @@ build.json @home-assistant/supervisor /tests/components/sensibo/ @andrey-git @gjohansson-ST /homeassistant/components/sensor/ @home-assistant/core /tests/components/sensor/ @home-assistant/core +/homeassistant/components/sensorpush/ @bdraco +/tests/components/sensorpush/ @bdraco /homeassistant/components/sentry/ @dcramer @frenck /tests/components/sentry/ @dcramer @frenck /homeassistant/components/senz/ @milanmeu diff --git a/homeassistant/components/sensorpush/__init__.py b/homeassistant/components/sensorpush/__init__.py new file mode 100644 index 00000000000..89e2287be70 --- /dev/null +++ b/homeassistant/components/sensorpush/__init__.py @@ -0,0 +1,56 @@ +"""The SensorPush Bluetooth integration.""" +from __future__ import annotations + +import logging + +from sensorpush_ble import SensorPushBluetoothDeviceData + +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothDataUpdate, + PassiveBluetoothDataUpdateCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +from .const import DOMAIN +from .sensor import sensor_update_to_bluetooth_data_update + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up SensorPush BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + + data = SensorPushBluetoothDeviceData() + + @callback + def _async_update_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Update data from SensorPush Bluetooth.""" + return sensor_update_to_bluetooth_data_update(data.update(service_info)) + + hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothDataUpdateCoordinator( + hass, + _LOGGER, + update_method=_async_update_data, + address=address, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/sensorpush/config_flow.py b/homeassistant/components/sensorpush/config_flow.py new file mode 100644 index 00000000000..1a8e8b47abe --- /dev/null +++ b/homeassistant/components/sensorpush/config_flow.py @@ -0,0 +1,93 @@ +"""Config flow for sensorpush integration.""" +from __future__ import annotations + +from typing import Any + +from sensorpush_ble import SensorPushBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfo, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class SensorPushConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for sensorpush.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfo | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/sensorpush/const.py b/homeassistant/components/sensorpush/const.py new file mode 100644 index 00000000000..8f566c72d07 --- /dev/null +++ b/homeassistant/components/sensorpush/const.py @@ -0,0 +1,3 @@ +"""Constants for the SensorPush Bluetooth integration.""" + +DOMAIN = "sensorpush" diff --git a/homeassistant/components/sensorpush/manifest.json b/homeassistant/components/sensorpush/manifest.json new file mode 100644 index 00000000000..08212f88132 --- /dev/null +++ b/homeassistant/components/sensorpush/manifest.json @@ -0,0 +1,15 @@ +{ + "domain": "sensorpush", + "name": "SensorPush", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/sensorpush", + "bluetooth": [ + { + "local_name": "SensorPush*" + } + ], + "requirements": ["sensorpush-ble==1.4.2"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/sensorpush/sensor.py b/homeassistant/components/sensorpush/sensor.py new file mode 100644 index 00000000000..147555990e4 --- /dev/null +++ b/homeassistant/components/sensorpush/sensor.py @@ -0,0 +1,143 @@ +"""Support for sensorpush ble sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from sensorpush_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothCoordinatorEntity, + PassiveBluetoothDataUpdate, + PassiveBluetoothDataUpdateCoordinator, + PassiveBluetoothEntityKey, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + PERCENTAGE, + PRESSURE_MBAR, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( + key=f"{DeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=PRESSURE_MBAR, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def _sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: _sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the SensorPush BLE sensors.""" + coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + entry.async_on_unload( + coordinator.async_add_entities_listener( + SensorPushBluetoothSensorEntity, async_add_entities + ) + ) + + +class SensorPushBluetoothSensorEntity( + PassiveBluetoothCoordinatorEntity[ + PassiveBluetoothDataUpdateCoordinator[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a sensorpush ble sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.coordinator.entity_data.get(self.entity_key) diff --git a/homeassistant/components/sensorpush/strings.json b/homeassistant/components/sensorpush/strings.json new file mode 100644 index 00000000000..7111626cca1 --- /dev/null +++ b/homeassistant/components/sensorpush/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/sensorpush/translations/en.json b/homeassistant/components/sensorpush/translations/en.json new file mode 100644 index 00000000000..d24df64f135 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 03486b6043c..e3a4edc11d4 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -14,6 +14,10 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ 6 ] }, + { + "domain": "sensorpush", + "local_name": "SensorPush*" + }, { "domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3c02842e856..95050ea6dbd 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -308,6 +308,7 @@ FLOWS = { "sense", "senseme", "sensibo", + "sensorpush", "sentry", "senz", "sharkiq", diff --git a/requirements_all.txt b/requirements_all.txt index c88df779c25..f125a49c0a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2154,6 +2154,9 @@ sendgrid==6.8.2 # homeassistant.components.sense sense_energy==0.10.4 +# homeassistant.components.sensorpush +sensorpush-ble==1.4.2 + # homeassistant.components.sentry sentry-sdk==1.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 57787b0cb72..605eb8352ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1438,6 +1438,9 @@ securetar==2022.2.0 # homeassistant.components.sense sense_energy==0.10.4 +# homeassistant.components.sensorpush +sensorpush-ble==1.4.2 + # homeassistant.components.sentry sentry-sdk==1.7.2 diff --git a/tests/components/sensorpush/__init__.py b/tests/components/sensorpush/__init__.py new file mode 100644 index 00000000000..0fe9ced64df --- /dev/null +++ b/tests/components/sensorpush/__init__.py @@ -0,0 +1,34 @@ +"""Tests for the SensorPush integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_SENSOR_PUSH_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +HTW_SERVICE_INFO = BluetoothServiceInfo( + name="SensorPush HT.w 0CA1", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={11271: b"\xfe\x00\x01"}, + service_data={}, + service_uuids=["ef090000-11d6-42ba-93b8-9dd7ec090ab0"], + source="local", +) + +HTPWX_SERVICE_INFO = BluetoothServiceInfo( + name="SensorPush HTP.xw F4D", + address="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + rssi=-56, + manufacturer_data={7168: b"\xcd=!\xd1\xb9"}, + service_data={}, + service_uuids=["ef090000-11d6-42ba-93b8-9dd7ec090ab0"], + source="local", +) diff --git a/tests/components/sensorpush/conftest.py b/tests/components/sensorpush/conftest.py new file mode 100644 index 00000000000..c6497a4e76d --- /dev/null +++ b/tests/components/sensorpush/conftest.py @@ -0,0 +1,8 @@ +"""SensorPush session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def auto_mock_bleak_scanner_start(mock_bleak_scanner_start): + """Auto mock bleak scanner start.""" diff --git a/tests/components/sensorpush/test_config_flow.py b/tests/components/sensorpush/test_config_flow.py new file mode 100644 index 00000000000..662841af3f1 --- /dev/null +++ b/tests/components/sensorpush/test_config_flow.py @@ -0,0 +1,98 @@ +"""Test the SensorPush config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.sensorpush.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import HTPWX_SERVICE_INFO, HTW_SERVICE_INFO, NOT_SENSOR_PUSH_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HTPWX_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.sensorpush.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "HTP.xw F4D" + assert result2["data"] == {} + assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" + + +async def test_async_step_bluetooth_not_sensorpush(hass): + """Test discovery via bluetooth not sensorpush.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_SENSOR_PUSH_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.sensorpush.config_flow.async_discovered_service_info", + return_value=[HTW_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.sensorpush.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "HT.w 0CA1" + assert result2["data"] == {} + assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensorpush.config_flow.async_discovered_service_info", + return_value=[HTW_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" diff --git a/tests/components/sensorpush/test_sensor.py b/tests/components/sensorpush/test_sensor.py new file mode 100644 index 00000000000..c48b8bc3407 --- /dev/null +++ b/tests/components/sensorpush/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the SensorPush config flow.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.components.sensorpush.const import DOMAIN +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import HTPWX_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback(HTPWX_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 3 + + temp_sensor = hass.states.get("sensor.htp_xw_f4d_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "20.11" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "HTP.xw F4D Temperature" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From b0261dd2eb8b03a6ecf431a282f1be5120887e72 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 21 Jul 2022 20:32:42 -0600 Subject: [PATCH 2775/3516] Modify Guardian to store a single dataclass in `hass.data` (#75454) * Modify Guardian to store a single dataclass in `hass.data` * Clarity is better * Allow entry unload to cancel task --- homeassistant/components/guardian/__init__.py | 162 ++++++++---------- .../components/guardian/binary_sensor.py | 15 +- homeassistant/components/guardian/button.py | 19 +- homeassistant/components/guardian/const.py | 4 - .../components/guardian/diagnostics.py | 15 +- homeassistant/components/guardian/sensor.py | 15 +- homeassistant/components/guardian/switch.py | 19 +- tests/components/guardian/test_diagnostics.py | 12 +- 8 files changed, 112 insertions(+), 149 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index d15b7d57aea..da22066d7aa 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -35,9 +35,6 @@ from .const import ( API_VALVE_STATUS, API_WIFI_STATUS, CONF_UID, - DATA_CLIENT, - DATA_COORDINATOR, - DATA_COORDINATOR_PAIRED_SENSOR, DOMAIN, LOGGER, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, @@ -89,6 +86,16 @@ SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] +@dataclass +class GuardianData: + """Define an object to be stored in `hass.data`.""" + + entry: ConfigEntry + client: Client + valve_controller_coordinators: dict[str, GuardianDataUpdateCoordinator] + paired_sensor_manager: PairedSensorManager + + @callback def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) -> str: """Get the entry ID related to a service call (by device ID).""" @@ -131,7 +138,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api_lock = asyncio.Lock() # Set up GuardianDataUpdateCoordinators for the valve controller: - coordinators: dict[str, GuardianDataUpdateCoordinator] = {} + valve_controller_coordinators: dict[str, GuardianDataUpdateCoordinator] = {} init_valve_controller_tasks = [] for api, api_coro in ( (API_SENSOR_PAIR_DUMP, client.sensor.pair_dump), @@ -140,7 +147,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: (API_VALVE_STATUS, client.valve.status), (API_WIFI_STATUS, client.wifi.status), ): - coordinator = coordinators[api] = GuardianDataUpdateCoordinator( + coordinator = valve_controller_coordinators[ + api + ] = GuardianDataUpdateCoordinator( hass, client=client, api_name=api, @@ -154,45 +163,38 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Set up an object to evaluate each batch of paired sensor UIDs and add/remove # devices as appropriate: - paired_sensor_manager = PairedSensorManager(hass, entry, client, api_lock) - await paired_sensor_manager.async_process_latest_paired_sensor_uids() + paired_sensor_manager = PairedSensorManager( + hass, + entry, + client, + api_lock, + valve_controller_coordinators[API_SENSOR_PAIR_DUMP], + ) + await paired_sensor_manager.async_initialize() hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_CLIENT: client, - DATA_COORDINATOR: coordinators, - DATA_COORDINATOR_PAIRED_SENSOR: {}, - DATA_PAIRED_SENSOR_MANAGER: paired_sensor_manager, - } - - @callback - def async_process_paired_sensor_uids() -> None: - """Define a callback for when new paired sensor data is received.""" - hass.async_create_task( - paired_sensor_manager.async_process_latest_paired_sensor_uids() - ) - - coordinators[API_SENSOR_PAIR_DUMP].async_add_listener( - async_process_paired_sensor_uids + hass.data[DOMAIN][entry.entry_id] = GuardianData( + entry=entry, + client=client, + valve_controller_coordinators=valve_controller_coordinators, + paired_sensor_manager=paired_sensor_manager, ) # Set up all of the Guardian entity platforms: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @callback - def hydrate_with_entry_and_client(func: Callable) -> Callable: - """Define a decorator to hydrate a method with args based on service call.""" + def call_with_data(func: Callable) -> Callable: + """Hydrate a service call with the appropriate GuardianData object.""" async def wrapper(call: ServiceCall) -> None: """Wrap the service function.""" entry_id = async_get_entry_id_for_service_call(hass, call) - client = hass.data[DOMAIN][entry_id][DATA_CLIENT] - entry = hass.config_entries.async_get_entry(entry_id) - assert entry + data = hass.data[DOMAIN][entry_id] try: - async with client: - await func(call, entry, client) + async with data.client: + await func(call, data) except GuardianError as err: raise HomeAssistantError( f"Error while executing {func.__name__}: {err}" @@ -200,78 +202,58 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return wrapper - @hydrate_with_entry_and_client - async def async_disable_ap( - call: ServiceCall, entry: ConfigEntry, client: Client - ) -> None: + @call_with_data + async def async_disable_ap(call: ServiceCall, data: GuardianData) -> None: """Disable the onboard AP.""" - await client.wifi.disable_ap() + await data.client.wifi.disable_ap() - @hydrate_with_entry_and_client - async def async_enable_ap( - call: ServiceCall, entry: ConfigEntry, client: Client - ) -> None: + @call_with_data + async def async_enable_ap(call: ServiceCall, data: GuardianData) -> None: """Enable the onboard AP.""" - await client.wifi.enable_ap() + await data.client.wifi.enable_ap() - @hydrate_with_entry_and_client - async def async_pair_sensor( - call: ServiceCall, entry: ConfigEntry, client: Client - ) -> None: + @call_with_data + async def async_pair_sensor(call: ServiceCall, data: GuardianData) -> None: """Add a new paired sensor.""" - paired_sensor_manager = hass.data[DOMAIN][entry.entry_id][ - DATA_PAIRED_SENSOR_MANAGER - ] uid = call.data[CONF_UID] + await data.client.sensor.pair_sensor(uid) + await data.paired_sensor_manager.async_pair_sensor(uid) - await client.sensor.pair_sensor(uid) - await paired_sensor_manager.async_pair_sensor(uid) - - @hydrate_with_entry_and_client - async def async_reboot( - call: ServiceCall, entry: ConfigEntry, client: Client - ) -> None: + @call_with_data + async def async_reboot(call: ServiceCall, data: GuardianData) -> None: """Reboot the valve controller.""" async_log_deprecated_service_call( hass, call, "button.press", - f"button.guardian_valve_controller_{entry.data[CONF_UID]}_reboot", + f"button.guardian_valve_controller_{data.entry.data[CONF_UID]}_reboot", ) - await client.system.reboot() + await data.client.system.reboot() - @hydrate_with_entry_and_client + @call_with_data async def async_reset_valve_diagnostics( - call: ServiceCall, entry: ConfigEntry, client: Client + call: ServiceCall, data: GuardianData ) -> None: """Fully reset system motor diagnostics.""" async_log_deprecated_service_call( hass, call, "button.press", - f"button.guardian_valve_controller_{entry.data[CONF_UID]}_reset_valve_diagnostics", + f"button.guardian_valve_controller_{data.entry.data[CONF_UID]}_reset_valve_diagnostics", ) - await client.valve.reset() + await data.client.valve.reset() - @hydrate_with_entry_and_client - async def async_unpair_sensor( - call: ServiceCall, entry: ConfigEntry, client: Client - ) -> None: + @call_with_data + async def async_unpair_sensor(call: ServiceCall, data: GuardianData) -> None: """Remove a paired sensor.""" - paired_sensor_manager = hass.data[DOMAIN][entry.entry_id][ - DATA_PAIRED_SENSOR_MANAGER - ] uid = call.data[CONF_UID] + await data.client.sensor.unpair_sensor(uid) + await data.paired_sensor_manager.async_unpair_sensor(uid) - await client.sensor.unpair_sensor(uid) - await paired_sensor_manager.async_unpair_sensor(uid) - - @hydrate_with_entry_and_client - async def async_upgrade_firmware( - call: ServiceCall, entry: ConfigEntry, client: Client - ) -> None: + @call_with_data + async def async_upgrade_firmware(call: ServiceCall, data: GuardianData) -> None: """Upgrade the device firmware.""" - await client.system.upgrade_firmware( + await data.client.system.upgrade_firmware( url=call.data[CONF_URL], port=call.data[CONF_PORT], filename=call.data[CONF_FILENAME], @@ -338,6 +320,7 @@ class PairedSensorManager: entry: ConfigEntry, client: Client, api_lock: asyncio.Lock, + sensor_pair_dump_coordinator: GuardianDataUpdateCoordinator, ) -> None: """Initialize.""" self._api_lock = api_lock @@ -345,6 +328,21 @@ class PairedSensorManager: self._entry = entry self._hass = hass self._paired_uids: set[str] = set() + self._sensor_pair_dump_coordinator = sensor_pair_dump_coordinator + self.coordinators: dict[str, GuardianDataUpdateCoordinator] = {} + + async def async_initialize(self) -> None: + """Initialize the manager.""" + + @callback + def async_create_process_task() -> None: + """Define a callback for when new paired sensor data is received.""" + self._hass.async_create_task(self.async_process_latest_paired_sensor_uids()) + + cancel_process_task = self._sensor_pair_dump_coordinator.async_add_listener( + async_create_process_task + ) + self._entry.async_on_unload(cancel_process_task) async def async_pair_sensor(self, uid: str) -> None: """Add a new paired sensor coordinator.""" @@ -352,9 +350,7 @@ class PairedSensorManager: self._paired_uids.add(uid) - coordinator = self._hass.data[DOMAIN][self._entry.entry_id][ - DATA_COORDINATOR_PAIRED_SENSOR - ][uid] = GuardianDataUpdateCoordinator( + coordinator = self.coordinators[uid] = GuardianDataUpdateCoordinator( self._hass, client=self._client, api_name=f"{API_SENSOR_PAIRED_SENSOR_STATUS}_{uid}", @@ -375,11 +371,7 @@ class PairedSensorManager: async def async_process_latest_paired_sensor_uids(self) -> None: """Process a list of new UIDs.""" try: - uids = set( - self._hass.data[DOMAIN][self._entry.entry_id][DATA_COORDINATOR][ - API_SENSOR_PAIR_DUMP - ].data["paired_uids"] - ) + uids = set(self._sensor_pair_dump_coordinator.data["paired_uids"]) except KeyError: # Sometimes the paired_uids key can fail to exist; the user can't do anything # about it, so in this case, we quietly abort and return: @@ -403,9 +395,7 @@ class PairedSensorManager: # Clear out objects related to this paired sensor: self._paired_uids.remove(uid) - self._hass.data[DOMAIN][self._entry.entry_id][ - DATA_COORDINATOR_PAIRED_SENSOR - ].pop(uid) + self.coordinators.pop(uid) # Remove the paired sensor device from the device registry (which will # clean up entities and the entity registry): diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index 9b824ab589f..eb6d49c3ec1 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -15,6 +15,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ( + GuardianData, PairedSensorEntity, ValveControllerEntity, ValveControllerEntityDescription, @@ -23,8 +24,6 @@ from .const import ( API_SYSTEM_ONBOARD_SENSOR_STATUS, API_WIFI_STATUS, CONF_UID, - DATA_COORDINATOR, - DATA_COORDINATOR_PAIRED_SENSOR, DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, ) @@ -79,16 +78,14 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" - entry_data = hass.data[DOMAIN][entry.entry_id] - paired_sensor_coordinators = entry_data[DATA_COORDINATOR_PAIRED_SENSOR] - valve_controller_coordinators = entry_data[DATA_COORDINATOR] + data: GuardianData = hass.data[DOMAIN][entry.entry_id] @callback def add_new_paired_sensor(uid: str) -> None: """Add a new paired sensor.""" async_add_entities( PairedSensorBinarySensor( - entry, paired_sensor_coordinators[uid], description + entry, data.paired_sensor_manager.coordinators[uid], description ) for description in PAIRED_SENSOR_DESCRIPTIONS ) @@ -104,7 +101,9 @@ async def async_setup_entry( # Add all valve controller-specific binary sensors: sensors: list[PairedSensorBinarySensor | ValveControllerBinarySensor] = [ - ValveControllerBinarySensor(entry, valve_controller_coordinators, description) + ValveControllerBinarySensor( + entry, data.valve_controller_coordinators, description + ) for description in VALVE_CONTROLLER_DESCRIPTIONS ] @@ -112,7 +111,7 @@ async def async_setup_entry( sensors.extend( [ PairedSensorBinarySensor(entry, coordinator, description) - for coordinator in paired_sensor_coordinators.values() + for coordinator in data.paired_sensor_manager.coordinators.values() for description in PAIRED_SENSOR_DESCRIPTIONS ] ) diff --git a/homeassistant/components/guardian/button.py b/homeassistant/components/guardian/button.py index e013cde85d6..01efb7deba4 100644 --- a/homeassistant/components/guardian/button.py +++ b/homeassistant/components/guardian/button.py @@ -18,9 +18,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ValveControllerEntity, ValveControllerEntityDescription -from .const import API_SYSTEM_DIAGNOSTICS, DATA_CLIENT, DATA_COORDINATOR, DOMAIN -from .util import GuardianDataUpdateCoordinator +from . import GuardianData, ValveControllerEntity, ValveControllerEntityDescription +from .const import API_SYSTEM_DIAGNOSTICS, DOMAIN @dataclass @@ -77,13 +76,10 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian buttons based on a config entry.""" - entry_data = hass.data[DOMAIN][entry.entry_id] - client = entry_data[DATA_CLIENT] - valve_controller_coordinators = entry_data[DATA_COORDINATOR] + data: GuardianData = hass.data[DOMAIN][entry.entry_id] async_add_entities( - GuardianButton(entry, valve_controller_coordinators, description, client) - for description in BUTTON_DESCRIPTIONS + GuardianButton(entry, data, description) for description in BUTTON_DESCRIPTIONS ) @@ -98,14 +94,13 @@ class GuardianButton(ValveControllerEntity, ButtonEntity): def __init__( self, entry: ConfigEntry, - coordinators: dict[str, GuardianDataUpdateCoordinator], + data: GuardianData, description: ValveControllerButtonDescription, - client: Client, ) -> None: """Initialize.""" - super().__init__(entry, coordinators, description) + super().__init__(entry, data.valve_controller_coordinators, description) - self._client = client + self._client = data.client async def async_press(self) -> None: """Send out a restart command.""" diff --git a/homeassistant/components/guardian/const.py b/homeassistant/components/guardian/const.py index 3499db24c03..c7d025ba712 100644 --- a/homeassistant/components/guardian/const.py +++ b/homeassistant/components/guardian/const.py @@ -14,8 +14,4 @@ API_WIFI_STATUS = "wifi_status" CONF_UID = "uid" -DATA_CLIENT = "client" -DATA_COORDINATOR = "coordinator" -DATA_COORDINATOR_PAIRED_SENSOR = "coordinator_paired_sensor" - SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED = "guardian_paired_sensor_coordinator_added_{0}" diff --git a/homeassistant/components/guardian/diagnostics.py b/homeassistant/components/guardian/diagnostics.py index 175136b33f4..d53dcb68fa8 100644 --- a/homeassistant/components/guardian/diagnostics.py +++ b/homeassistant/components/guardian/diagnostics.py @@ -7,8 +7,8 @@ from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import CONF_UID, DATA_COORDINATOR, DATA_COORDINATOR_PAIRED_SENSOR, DOMAIN -from .util import GuardianDataUpdateCoordinator +from . import GuardianData +from .const import CONF_UID, DOMAIN CONF_BSSID = "bssid" CONF_PAIRED_UIDS = "paired_uids" @@ -26,12 +26,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data = hass.data[DOMAIN][entry.entry_id] - - coordinators: dict[str, GuardianDataUpdateCoordinator] = data[DATA_COORDINATOR] - paired_sensor_coordinators: dict[str, GuardianDataUpdateCoordinator] = data[ - DATA_COORDINATOR_PAIRED_SENSOR - ] + data: GuardianData = hass.data[DOMAIN][entry.entry_id] return { "entry": { @@ -41,11 +36,11 @@ async def async_get_config_entry_diagnostics( "data": { "valve_controller": { api_category: async_redact_data(coordinator.data, TO_REDACT) - for api_category, coordinator in coordinators.items() + for api_category, coordinator in data.valve_controller_coordinators.items() }, "paired_sensors": [ async_redact_data(coordinator.data, TO_REDACT) - for coordinator in paired_sensor_coordinators.values() + for coordinator in data.paired_sensor_manager.coordinators.values() ], }, } diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index c4fd1e110fa..bf7d9e7122a 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -17,6 +17,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ( + GuardianData, PairedSensorEntity, ValveControllerEntity, ValveControllerEntityDescription, @@ -25,8 +26,6 @@ from .const import ( API_SYSTEM_DIAGNOSTICS, API_SYSTEM_ONBOARD_SENSOR_STATUS, CONF_UID, - DATA_COORDINATOR, - DATA_COORDINATOR_PAIRED_SENSOR, DOMAIN, SIGNAL_PAIRED_SENSOR_COORDINATOR_ADDED, ) @@ -83,15 +82,15 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" - entry_data = hass.data[DOMAIN][entry.entry_id] - paired_sensor_coordinators = entry_data[DATA_COORDINATOR_PAIRED_SENSOR] - valve_controller_coordinators = entry_data[DATA_COORDINATOR] + data: GuardianData = hass.data[DOMAIN][entry.entry_id] @callback def add_new_paired_sensor(uid: str) -> None: """Add a new paired sensor.""" async_add_entities( - PairedSensorSensor(entry, paired_sensor_coordinators[uid], description) + PairedSensorSensor( + entry, data.paired_sensor_manager.coordinators[uid], description + ) for description in PAIRED_SENSOR_DESCRIPTIONS ) @@ -106,7 +105,7 @@ async def async_setup_entry( # Add all valve controller-specific binary sensors: sensors: list[PairedSensorSensor | ValveControllerSensor] = [ - ValveControllerSensor(entry, valve_controller_coordinators, description) + ValveControllerSensor(entry, data.valve_controller_coordinators, description) for description in VALVE_CONTROLLER_DESCRIPTIONS ] @@ -114,7 +113,7 @@ async def async_setup_entry( sensors.extend( [ PairedSensorSensor(entry, coordinator, description) - for coordinator in paired_sensor_coordinators.values() + for coordinator in data.paired_sensor_manager.coordinators.values() for description in PAIRED_SENSOR_DESCRIPTIONS ] ) diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index c58f4548a87..4e100ce4fe4 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -4,7 +4,6 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any -from aioguardian import Client from aioguardian.errors import GuardianError from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription @@ -13,9 +12,8 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import ValveControllerEntity, ValveControllerEntityDescription -from .const import API_VALVE_STATUS, DATA_CLIENT, DATA_COORDINATOR, DOMAIN -from .util import GuardianDataUpdateCoordinator +from . import GuardianData, ValveControllerEntity, ValveControllerEntityDescription +from .const import API_VALVE_STATUS, DOMAIN ATTR_AVG_CURRENT = "average_current" ATTR_INST_CURRENT = "instantaneous_current" @@ -46,12 +44,10 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Guardian switches based on a config entry.""" - entry_data = hass.data[DOMAIN][entry.entry_id] - client = entry_data[DATA_CLIENT] - valve_controller_coordinators = entry_data[DATA_COORDINATOR] + data: GuardianData = hass.data[DOMAIN][entry.entry_id] async_add_entities( - ValveControllerSwitch(entry, valve_controller_coordinators, description, client) + ValveControllerSwitch(entry, data, description) for description in VALVE_CONTROLLER_DESCRIPTIONS ) @@ -71,15 +67,14 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity): def __init__( self, entry: ConfigEntry, - coordinators: dict[str, GuardianDataUpdateCoordinator], + data: GuardianData, description: ValveControllerSwitchDescription, - client: Client, ) -> None: """Initialize.""" - super().__init__(entry, coordinators, description) + super().__init__(entry, data.valve_controller_coordinators, description) self._attr_is_on = True - self._client = client + self._client = data.client @callback def _async_update_from_latest_data(self) -> None: diff --git a/tests/components/guardian/test_diagnostics.py b/tests/components/guardian/test_diagnostics.py index f48c988c907..2269d09b1eb 100644 --- a/tests/components/guardian/test_diagnostics.py +++ b/tests/components/guardian/test_diagnostics.py @@ -1,22 +1,16 @@ """Test Guardian diagnostics.""" from homeassistant.components.diagnostics import REDACTED -from homeassistant.components.guardian import ( - DATA_PAIRED_SENSOR_MANAGER, - DOMAIN, - PairedSensorManager, -) +from homeassistant.components.guardian import DOMAIN, GuardianData from tests.components.diagnostics import get_diagnostics_for_config_entry async def test_entry_diagnostics(hass, config_entry, hass_client, setup_guardian): """Test config entry diagnostics.""" - paired_sensor_manager: PairedSensorManager = hass.data[DOMAIN][ - config_entry.entry_id - ][DATA_PAIRED_SENSOR_MANAGER] + data: GuardianData = hass.data[DOMAIN][config_entry.entry_id] # Simulate the pairing of a paired sensor: - await paired_sensor_manager.async_pair_sensor("AABBCCDDEEFF") + await data.paired_sensor_manager.async_pair_sensor("AABBCCDDEEFF") assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { From 06115bcbff2900366e09e5c9393b11d62b4242f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 21 Jul 2022 22:17:09 -0500 Subject: [PATCH 2776/3516] Add inkbird (BLE) integration (#75594) --- CODEOWNERS | 2 + homeassistant/components/inkbird/__init__.py | 56 +++++++ .../components/inkbird/config_flow.py | 93 ++++++++++++ homeassistant/components/inkbird/const.py | 3 + .../components/inkbird/manifest.json | 16 ++ homeassistant/components/inkbird/sensor.py | 142 ++++++++++++++++++ homeassistant/components/inkbird/strings.json | 21 +++ .../components/inkbird/translations/en.json | 21 +++ homeassistant/generated/bluetooth.py | 16 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/inkbird/__init__.py | 36 +++++ tests/components/inkbird/conftest.py | 8 + tests/components/inkbird/test_config_flow.py | 94 ++++++++++++ tests/components/inkbird/test_sensor.py | 50 ++++++ 16 files changed, 565 insertions(+) create mode 100644 homeassistant/components/inkbird/__init__.py create mode 100644 homeassistant/components/inkbird/config_flow.py create mode 100644 homeassistant/components/inkbird/const.py create mode 100644 homeassistant/components/inkbird/manifest.json create mode 100644 homeassistant/components/inkbird/sensor.py create mode 100644 homeassistant/components/inkbird/strings.json create mode 100644 homeassistant/components/inkbird/translations/en.json create mode 100644 tests/components/inkbird/__init__.py create mode 100644 tests/components/inkbird/conftest.py create mode 100644 tests/components/inkbird/test_config_flow.py create mode 100644 tests/components/inkbird/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 2a3999779fc..792a6302b79 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -498,6 +498,8 @@ build.json @home-assistant/supervisor /homeassistant/components/incomfort/ @zxdavb /homeassistant/components/influxdb/ @mdegat01 /tests/components/influxdb/ @mdegat01 +/homeassistant/components/inkbird/ @bdraco +/tests/components/inkbird/ @bdraco /homeassistant/components/input_boolean/ @home-assistant/core /tests/components/input_boolean/ @home-assistant/core /homeassistant/components/input_button/ @home-assistant/core diff --git a/homeassistant/components/inkbird/__init__.py b/homeassistant/components/inkbird/__init__.py new file mode 100644 index 00000000000..29b63e34263 --- /dev/null +++ b/homeassistant/components/inkbird/__init__.py @@ -0,0 +1,56 @@ +"""The INKBIRD Bluetooth integration.""" +from __future__ import annotations + +import logging + +from inkbird_ble import INKBIRDBluetoothDeviceData + +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothDataUpdate, + PassiveBluetoothDataUpdateCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +from .const import DOMAIN +from .sensor import sensor_update_to_bluetooth_data_update + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up INKBIRD BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + + data = INKBIRDBluetoothDeviceData() + + @callback + def _async_update_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Update data from INKBIRD Bluetooth.""" + return sensor_update_to_bluetooth_data_update(data.update(service_info)) + + hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothDataUpdateCoordinator( + hass, + _LOGGER, + update_method=_async_update_data, + address=address, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/inkbird/config_flow.py b/homeassistant/components/inkbird/config_flow.py new file mode 100644 index 00000000000..679ff43b19e --- /dev/null +++ b/homeassistant/components/inkbird/config_flow.py @@ -0,0 +1,93 @@ +"""Config flow for inkbird ble integration.""" +from __future__ import annotations + +from typing import Any + +from inkbird_ble import INKBIRDBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfo, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class INKBIRDConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for inkbird.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfo | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/inkbird/const.py b/homeassistant/components/inkbird/const.py new file mode 100644 index 00000000000..9d0e1638958 --- /dev/null +++ b/homeassistant/components/inkbird/const.py @@ -0,0 +1,3 @@ +"""Constants for the INKBIRD Bluetooth integration.""" + +DOMAIN = "inkbird" diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json new file mode 100644 index 00000000000..686c9bada2d --- /dev/null +++ b/homeassistant/components/inkbird/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "inkbird", + "name": "INKBIRD", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/inkbird", + "bluetooth": [ + { "local_name": "sps" }, + { "local_name": "Inkbird*" }, + { "local_name": "iBBQ*" }, + { "local_name": "tps" } + ], + "requirements": ["inkbird-ble==0.5.1"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/inkbird/sensor.py b/homeassistant/components/inkbird/sensor.py new file mode 100644 index 00000000000..26311131181 --- /dev/null +++ b/homeassistant/components/inkbird/sensor.py @@ -0,0 +1,142 @@ +"""Support for inkbird ble sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from inkbird_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothCoordinatorEntity, + PassiveBluetoothDataUpdate, + PassiveBluetoothDataUpdateCoordinator, + PassiveBluetoothEntityKey, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def _sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: _sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the INKBIRD BLE sensors.""" + coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + entry.async_on_unload( + coordinator.async_add_entities_listener( + INKBIRDBluetoothSensorEntity, async_add_entities + ) + ) + + +class INKBIRDBluetoothSensorEntity( + PassiveBluetoothCoordinatorEntity[ + PassiveBluetoothDataUpdateCoordinator[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a inkbird ble sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.coordinator.entity_data.get(self.entity_key) diff --git a/homeassistant/components/inkbird/strings.json b/homeassistant/components/inkbird/strings.json new file mode 100644 index 00000000000..7111626cca1 --- /dev/null +++ b/homeassistant/components/inkbird/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/inkbird/translations/en.json b/homeassistant/components/inkbird/translations/en.json new file mode 100644 index 00000000000..d24df64f135 --- /dev/null +++ b/homeassistant/components/inkbird/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index e3a4edc11d4..1edd25a7609 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -14,6 +14,22 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ 6 ] }, + { + "domain": "inkbird", + "local_name": "sps" + }, + { + "domain": "inkbird", + "local_name": "Inkbird*" + }, + { + "domain": "inkbird", + "local_name": "iBBQ*" + }, + { + "domain": "inkbird", + "local_name": "tps" + }, { "domain": "sensorpush", "local_name": "SensorPush*" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 95050ea6dbd..33e34035b34 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -166,6 +166,7 @@ FLOWS = { "iaqualink", "icloud", "ifttt", + "inkbird", "insteon", "intellifire", "ios", diff --git a/requirements_all.txt b/requirements_all.txt index f125a49c0a4..0b3c0e4b370 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -896,6 +896,9 @@ influxdb-client==1.24.0 # homeassistant.components.influxdb influxdb==5.3.1 +# homeassistant.components.inkbird +inkbird-ble==0.5.1 + # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 605eb8352ea..53f1689e476 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -648,6 +648,9 @@ influxdb-client==1.24.0 # homeassistant.components.influxdb influxdb==5.3.1 +# homeassistant.components.inkbird +inkbird-ble==0.5.1 + # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 diff --git a/tests/components/inkbird/__init__.py b/tests/components/inkbird/__init__.py new file mode 100644 index 00000000000..0a74e50ae0e --- /dev/null +++ b/tests/components/inkbird/__init__.py @@ -0,0 +1,36 @@ +"""Tests for the INKBIRD integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_INKBIRD_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +SPS_SERVICE_INFO = BluetoothServiceInfo( + name="sps", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + service_data={}, + manufacturer_data={2096: b"\x0f\x12\x00Z\xc7W\x06"}, + service_uuids=["0000fff0-0000-1000-8000-00805f9b34fb"], + source="local", +) + +IBBQ_SERVICE_INFO = BluetoothServiceInfo( + name="iBBQ", + address="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + rssi=-56, + manufacturer_data={ + 0: b"\x00\x000\xe2\x83}\xb5\x02\xc8\x00\xc8\x00\xc8\x00\xc8\x00" + }, + service_uuids=["0000fff0-0000-1000-8000-00805f9b34fb"], + service_data={}, + source="local", +) diff --git a/tests/components/inkbird/conftest.py b/tests/components/inkbird/conftest.py new file mode 100644 index 00000000000..c44e9f7929a --- /dev/null +++ b/tests/components/inkbird/conftest.py @@ -0,0 +1,8 @@ +"""INKBIRD session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(mock_bleak_scanner_start): + """Auto mock bluetooth.""" diff --git a/tests/components/inkbird/test_config_flow.py b/tests/components/inkbird/test_config_flow.py new file mode 100644 index 00000000000..b783562b126 --- /dev/null +++ b/tests/components/inkbird/test_config_flow.py @@ -0,0 +1,94 @@ +"""Test the INKBIRD config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.inkbird.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import IBBQ_SERVICE_INFO, NOT_INKBIRD_SERVICE_INFO, SPS_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=IBBQ_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch("homeassistant.components.inkbird.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "iBBQ 6AADDD4CAC3D" + assert result2["data"] == {} + assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" + + +async def test_async_step_bluetooth_not_inkbird(hass): + """Test discovery via bluetooth not inkbird.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_INKBIRD_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.inkbird.config_flow.async_discovered_service_info", + return_value=[SPS_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch("homeassistant.components.inkbird.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "IBS-TH 75BBE1738105" + assert result2["data"] == {} + assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.inkbird.config_flow.async_discovered_service_info", + return_value=[SPS_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" diff --git a/tests/components/inkbird/test_sensor.py b/tests/components/inkbird/test_sensor.py new file mode 100644 index 00000000000..80a2179666f --- /dev/null +++ b/tests/components/inkbird/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the INKBIRD config flow.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.inkbird.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import SPS_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback(SPS_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 3 + + temp_sensor = hass.states.get("sensor.ibs_th_75bbe1738105_battery") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "87" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "IBS-TH 75BBE1738105 Battery" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From a612d7a0f31f2a5835ebdede77d06015ff0c8578 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 22 Jul 2022 11:46:00 +0800 Subject: [PATCH 2777/3516] Round up for stream record lookback (#75580) --- homeassistant/components/stream/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index f0b4ed99654..354f9a77672 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -503,15 +503,16 @@ class Stream: await self.start() + self._logger.debug("Started a stream recording of %s seconds", duration) + # Take advantage of lookback hls: HlsStreamOutput = cast(HlsStreamOutput, self.outputs().get(HLS_PROVIDER)) - if lookback > 0 and hls: - num_segments = min(int(lookback // hls.target_duration), MAX_SEGMENTS) + if hls: + num_segments = min(int(lookback / hls.target_duration) + 1, MAX_SEGMENTS) # Wait for latest segment, then add the lookback await hls.recv() recorder.prepend(list(hls.get_segments())[-num_segments - 1 : -1]) - self._logger.debug("Started a stream recording of %s seconds", duration) await recorder.async_record() async def async_get_image( From dddd4e24e29566ddf5f296f7f421885151268517 Mon Sep 17 00:00:00 2001 From: Thijs W Date: Fri, 22 Jul 2022 11:11:31 +0200 Subject: [PATCH 2778/3516] Bump afsapi to 0.2.7 (#75579) --- homeassistant/components/frontier_silicon/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/frontier_silicon/manifest.json b/homeassistant/components/frontier_silicon/manifest.json index b04d68e672d..1abce3b9a60 100644 --- a/homeassistant/components/frontier_silicon/manifest.json +++ b/homeassistant/components/frontier_silicon/manifest.json @@ -2,7 +2,7 @@ "domain": "frontier_silicon", "name": "Frontier Silicon", "documentation": "https://www.home-assistant.io/integrations/frontier_silicon", - "requirements": ["afsapi==0.2.6"], + "requirements": ["afsapi==0.2.7"], "codeowners": ["@wlcrs"], "iot_class": "local_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 0b3c0e4b370..927207a8eeb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -89,7 +89,7 @@ adguardhome==0.5.1 advantage_air==0.3.1 # homeassistant.components.frontier_silicon -afsapi==0.2.6 +afsapi==0.2.7 # homeassistant.components.agent_dvr agent-py==0.0.23 From f0eea62c1eeb8b7e7cd319f885ad5c849eda8796 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 22 Jul 2022 11:12:00 +0200 Subject: [PATCH 2779/3516] Address some MQTT review comments (#75482) --- .../mqtt/device_tracker/__init__.py | 13 +++++++-- .../mqtt/device_tracker/schema_yaml.py | 29 ++++++++++++++----- homeassistant/components/mqtt/mixins.py | 2 +- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/mqtt/device_tracker/__init__.py b/homeassistant/components/mqtt/device_tracker/__init__.py index 99e0c87044b..342817e38cc 100644 --- a/homeassistant/components/mqtt/device_tracker/__init__.py +++ b/homeassistant/components/mqtt/device_tracker/__init__.py @@ -10,7 +10,11 @@ from ..const import MQTT_DATA_DEVICE_TRACKER_LEGACY from ..mixins import warn_for_legacy_schema from .schema_discovery import PLATFORM_SCHEMA_MODERN # noqa: F401 from .schema_discovery import async_setup_entry_from_discovery -from .schema_yaml import PLATFORM_SCHEMA_YAML, async_setup_scanner_from_yaml +from .schema_yaml import ( + PLATFORM_SCHEMA_YAML, + MQTTLegacyDeviceTrackerData, + async_setup_scanner_from_yaml, +) # Configuring MQTT Device Trackers under the device_tracker platform key is deprecated in HA Core 2022.6 PLATFORM_SCHEMA = vol.All( @@ -30,6 +34,11 @@ async def async_setup_entry( await async_setup_entry_from_discovery(hass, config_entry, async_add_entities) # (re)load legacy service if MQTT_DATA_DEVICE_TRACKER_LEGACY in hass.data: + yaml_device_tracker_data: MQTTLegacyDeviceTrackerData = hass.data[ + MQTT_DATA_DEVICE_TRACKER_LEGACY + ] await async_setup_scanner_from_yaml( - hass, **hass.data[MQTT_DATA_DEVICE_TRACKER_LEGACY] + hass, + config=yaml_device_tracker_data.config, + async_see=yaml_device_tracker_data.async_see, ) diff --git a/homeassistant/components/mqtt/device_tracker/schema_yaml.py b/homeassistant/components/mqtt/device_tracker/schema_yaml.py index 1990b380dcb..c005a82dbeb 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_yaml.py +++ b/homeassistant/components/mqtt/device_tracker/schema_yaml.py @@ -1,6 +1,8 @@ """Support for tracking MQTT enabled devices defined in YAML.""" +from __future__ import annotations -from collections.abc import Callable +from collections.abc import Awaitable, Callable +import dataclasses import logging from typing import Any @@ -10,6 +12,7 @@ from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE from homeassistant.const import CONF_DEVICES, STATE_HOME, STATE_NOT_HOME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from ... import mqtt from ..client import async_subscribe @@ -33,9 +36,20 @@ PLATFORM_SCHEMA_YAML = PLATFORM_SCHEMA.extend(SCHEMA_BASE).extend( ) +@dataclasses.dataclass +class MQTTLegacyDeviceTrackerData: + """Class to hold device tracker data.""" + + async_see: Callable[..., Awaitable[None]] + config: ConfigType + + async def async_setup_scanner_from_yaml( - hass: HomeAssistant, config, async_see, discovery_info=None -): + hass: HomeAssistant, + config: ConfigType, + async_see: Callable[..., Awaitable[None]], + discovery_info: DiscoveryInfoType | None = None, +) -> bool: """Set up the MQTT tracker.""" devices = config[CONF_DEVICES] qos = config[CONF_QOS] @@ -45,15 +59,14 @@ async def async_setup_scanner_from_yaml( config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] subscriptions: list[Callable] = [] - hass.data[MQTT_DATA_DEVICE_TRACKER_LEGACY] = { - "async_see": async_see, - "config": config, - } + hass.data[MQTT_DATA_DEVICE_TRACKER_LEGACY] = MQTTLegacyDeviceTrackerData( + async_see, config + ) if not mqtt_config_entry_enabled(hass): _LOGGER.info( "MQTT device trackers will be not available until the config entry is enabled", ) - return + return False @callback def _entry_unload(*_: Any) -> None: diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index af0660b4c93..f0cc0c4ad44 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -674,7 +674,7 @@ class MqttDiscoveryDeviceUpdate: stop_discovery_updates( self.hass, self._discovery_data, self._remove_discovery_updated ) - self.hass.async_add_job(self.async_tear_down()) + self._config_entry.async_create_task(self.hass, self.async_tear_down()) async def async_discovery_update( self, From 630c28d253035eb0b141599f1707d0b6b20c97d1 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 22 Jul 2022 03:24:07 -0600 Subject: [PATCH 2780/3516] Fix incorrect battery unit on paired Guardian sensors (#75402) --- homeassistant/components/guardian/manifest.json | 2 +- homeassistant/components/guardian/sensor.py | 6 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index fe9a453a166..7fab487563c 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -3,7 +3,7 @@ "name": "Elexa Guardian", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/guardian", - "requirements": ["aioguardian==2022.03.2"], + "requirements": ["aioguardian==2022.07.0"], "zeroconf": ["_api._udp.local."], "codeowners": ["@bachya"], "iot_class": "local_polling", diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index bf7d9e7122a..5b4c621cce6 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -10,7 +10,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, TEMP_FAHRENHEIT, TIME_MINUTES +from homeassistant.const import ELECTRIC_POTENTIAL_VOLT, TEMP_FAHRENHEIT, TIME_MINUTES from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory @@ -46,9 +46,9 @@ PAIRED_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( key=SENSOR_KIND_BATTERY, name="Battery", - device_class=SensorDeviceClass.BATTERY, + device_class=SensorDeviceClass.VOLTAGE, entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=PERCENTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, ), SensorEntityDescription( key=SENSOR_KIND_TEMPERATURE, diff --git a/requirements_all.txt b/requirements_all.txt index 927207a8eeb..c17e031f07a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -162,7 +162,7 @@ aioftp==0.12.0 aiogithubapi==22.2.4 # homeassistant.components.guardian -aioguardian==2022.03.2 +aioguardian==2022.07.0 # homeassistant.components.harmony aioharmony==0.2.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 53f1689e476..8745afe1e64 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -146,7 +146,7 @@ aioflo==2021.11.0 aiogithubapi==22.2.4 # homeassistant.components.guardian -aioguardian==2022.03.2 +aioguardian==2022.07.0 # homeassistant.components.harmony aioharmony==0.2.9 From 06c8eb0304ae1ce422571aaa34b35c507b802594 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 22 Jul 2022 11:57:36 +0200 Subject: [PATCH 2781/3516] Migrate SMHI to new entity naming style (#75213) --- homeassistant/components/smhi/weather.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index d7df54957c0..f8afcb7b59a 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -126,6 +126,8 @@ class SmhiWeather(WeatherEntity): _attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND _attr_native_pressure_unit = PRESSURE_HPA + _attr_has_entity_name = True + def __init__( self, name: str, @@ -134,8 +136,6 @@ class SmhiWeather(WeatherEntity): session: aiohttp.ClientSession, ) -> None: """Initialize the SMHI weather entity.""" - - self._attr_name = name self._attr_unique_id = f"{latitude}, {longitude}" self._forecasts: list[SmhiForecast] | None = None self._fail_count = 0 From 606d5441573f52de961010551b34d23aef3317dc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 22 Jul 2022 11:58:26 +0200 Subject: [PATCH 2782/3516] Use recorder get_instance function to improve typing (#75567) --- homeassistant/components/recorder/__init__.py | 11 ++--- homeassistant/components/recorder/backup.py | 8 ++-- .../components/recorder/statistics.py | 7 +-- homeassistant/components/recorder/util.py | 10 +++- .../components/recorder/websocket_api.py | 24 ++++------ tests/components/recorder/common.py | 5 +- tests/components/recorder/test_backup.py | 8 ++-- tests/components/recorder/test_init.py | 46 ++++++++++--------- tests/components/recorder/test_migrate.py | 11 ++--- tests/components/recorder/test_statistics.py | 6 +-- tests/components/recorder/test_util.py | 16 +++---- .../components/recorder/test_websocket_api.py | 3 +- tests/components/sensor/test_recorder.py | 7 ++- 13 files changed, 77 insertions(+), 85 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 4063e443e8b..238b013f366 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -31,6 +31,7 @@ from .const import ( from .core import Recorder from .services import async_register_services from .tasks import AddRecorderPlatformTask +from .util import get_instance _LOGGER = logging.getLogger(__name__) @@ -108,12 +109,6 @@ CONFIG_SCHEMA = vol.Schema( ) -def get_instance(hass: HomeAssistant) -> Recorder: - """Get the recorder instance.""" - instance: Recorder = hass.data[DATA_INSTANCE] - return instance - - @bind_hass def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool: """Check if an entity is being recorded. @@ -122,7 +117,7 @@ def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool: """ if DATA_INSTANCE not in hass.data: return False - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) return instance.entity_filter(entity_id) @@ -177,5 +172,5 @@ async def _process_recorder_platform( hass: HomeAssistant, domain: str, platform: Any ) -> None: """Process a recorder platform.""" - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) instance.queue_task(AddRecorderPlatformTask(domain, platform)) diff --git a/homeassistant/components/recorder/backup.py b/homeassistant/components/recorder/backup.py index cec9f85748b..a1f6f4f39bc 100644 --- a/homeassistant/components/recorder/backup.py +++ b/homeassistant/components/recorder/backup.py @@ -4,9 +4,7 @@ from logging import getLogger from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from . import Recorder -from .const import DATA_INSTANCE -from .util import async_migration_in_progress +from .util import async_migration_in_progress, get_instance _LOGGER = getLogger(__name__) @@ -14,7 +12,7 @@ _LOGGER = getLogger(__name__) async def async_pre_backup(hass: HomeAssistant) -> None: """Perform operations before a backup starts.""" _LOGGER.info("Backup start notification, locking database for writes") - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) if async_migration_in_progress(hass): raise HomeAssistantError("Database migration in progress") await instance.lock_database() @@ -22,7 +20,7 @@ async def async_pre_backup(hass: HomeAssistant) -> None: async def async_post_backup(hass: HomeAssistant) -> None: """Perform operations after a backup finishes.""" - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) _LOGGER.info("Backup end notification, releasing write lock") if not instance.unlock_database(): raise HomeAssistantError("Could not release database write lock") diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 4ebd5e17902..bce77e8a31e 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -41,7 +41,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 DATA_INSTANCE, DOMAIN, MAX_ROWS_TO_PURGE, SupportedDialect +from .const import DOMAIN, MAX_ROWS_TO_PURGE, SupportedDialect from .db_schema import Statistics, StatisticsMeta, StatisticsRuns, StatisticsShortTerm from .models import ( StatisticData, @@ -53,6 +53,7 @@ from .models import ( from .util import ( execute, execute_stmt_lambda_element, + get_instance, retryable_database_job, session_scope, ) @@ -209,7 +210,7 @@ def async_setup(hass: HomeAssistant) -> None: @callback def _async_entity_id_changed(event: Event) -> None: - hass.data[DATA_INSTANCE].async_update_statistics_metadata( + get_instance(hass).async_update_statistics_metadata( event.data["old_entity_id"], new_statistic_id=event.data["entity_id"] ) @@ -1385,7 +1386,7 @@ def _async_import_statistics( statistic["last_reset"] = dt_util.as_utc(last_reset) # Insert job in recorder's queue - hass.data[DATA_INSTANCE].async_import_statistics(metadata, statistics) + get_instance(hass).async_import_statistics(metadata, statistics) @callback diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index c1fbc831987..fdf42665ef5 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -80,7 +80,7 @@ def session_scope( ) -> Generator[Session, None, None]: """Provide a transactional scope around a series of operations.""" if session is None and hass is not None: - session = hass.data[DATA_INSTANCE].get_session() + session = get_instance(hass).get_session() if session is None: raise RuntimeError("Session required") @@ -559,7 +559,7 @@ def async_migration_in_progress(hass: HomeAssistant) -> bool: """ if DATA_INSTANCE not in hass.data: return False - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) return instance.migration_in_progress @@ -577,3 +577,9 @@ def second_sunday(year: int, month: int) -> date: def is_second_sunday(date_time: datetime) -> bool: """Check if a time is the second sunday of the month.""" return bool(second_sunday(date_time.year, date_time.month).day == date_time.day) + + +def get_instance(hass: HomeAssistant) -> Recorder: + """Get the recorder instance.""" + instance: Recorder = hass.data[DATA_INSTANCE] + return instance diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 4ba5f3c8a8b..c143d8b4f0b 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING import voluptuous as vol @@ -11,17 +10,14 @@ from homeassistant.core import HomeAssistant, callback, valid_entity_id from homeassistant.helpers import config_validation as cv from homeassistant.util import dt as dt_util -from .const import DATA_INSTANCE, MAX_QUEUE_BACKLOG +from .const import MAX_QUEUE_BACKLOG from .statistics import ( async_add_external_statistics, async_import_statistics, list_statistic_ids, validate_statistics, ) -from .util import async_migration_in_progress - -if TYPE_CHECKING: - from . import Recorder +from .util import async_migration_in_progress, get_instance _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -50,7 +46,7 @@ async def ws_validate_statistics( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Fetch a list of available statistic_id.""" - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) statistic_ids = await instance.async_add_executor_job( validate_statistics, hass, @@ -74,7 +70,7 @@ def ws_clear_statistics( Note: The WS call posts a job to the recorder's queue and then returns, it doesn't wait until the job is completed. """ - hass.data[DATA_INSTANCE].async_clear_statistics(msg["statistic_ids"]) + get_instance(hass).async_clear_statistics(msg["statistic_ids"]) connection.send_result(msg["id"]) @@ -89,7 +85,7 @@ async def ws_get_statistics_metadata( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Get metadata for a list of statistic_ids.""" - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) statistic_ids = await instance.async_add_executor_job( list_statistic_ids, hass, msg.get("statistic_ids") ) @@ -109,7 +105,7 @@ def ws_update_statistics_metadata( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Update statistics metadata for a statistic_id.""" - hass.data[DATA_INSTANCE].async_update_statistics_metadata( + get_instance(hass).async_update_statistics_metadata( msg["statistic_id"], new_unit_of_measurement=msg["unit_of_measurement"] ) connection.send_result(msg["id"]) @@ -137,7 +133,7 @@ def ws_adjust_sum_statistics( connection.send_error(msg["id"], "invalid_start_time", "Invalid start time") return - hass.data[DATA_INSTANCE].async_adjust_statistics( + get_instance(hass).async_adjust_statistics( msg["statistic_id"], start_time, msg["adjustment"] ) connection.send_result(msg["id"]) @@ -193,7 +189,7 @@ def ws_info( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict ) -> None: """Return status of the recorder.""" - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) backlog = instance.backlog if instance else None migration_in_progress = async_migration_in_progress(hass) @@ -219,7 +215,7 @@ async def ws_backup_start( """Backup start notification.""" _LOGGER.info("Backup start notification, locking database for writes") - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) try: await instance.lock_database() except TimeoutError as err: @@ -236,7 +232,7 @@ async def ws_backup_end( ) -> None: """Backup end notification.""" - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) _LOGGER.info("Backup end notification, releasing write lock") if not instance.unlock_database(): connection.send_error( diff --git a/tests/components/recorder/common.py b/tests/components/recorder/common.py index 20df89eca5b..083630c7ea8 100644 --- a/tests/components/recorder/common.py +++ b/tests/components/recorder/common.py @@ -62,7 +62,7 @@ def wait_recording_done(hass: HomeAssistant) -> None: hass.block_till_done() trigger_db_commit(hass) hass.block_till_done() - hass.data[recorder.DATA_INSTANCE].block_till_done() + recorder.get_instance(hass).block_till_done() hass.block_till_done() @@ -105,8 +105,7 @@ def async_trigger_db_commit(hass: HomeAssistant) -> None: async def async_recorder_block_till_done(hass: HomeAssistant) -> None: """Non blocking version of recorder.block_till_done().""" - instance: recorder.Recorder = hass.data[recorder.DATA_INSTANCE] - await hass.async_add_executor_job(instance.block_till_done) + await hass.async_add_executor_job(recorder.get_instance(hass).block_till_done) def corrupt_db_file(test_db_file): diff --git a/tests/components/recorder/test_backup.py b/tests/components/recorder/test_backup.py index d7c5a55b56a..e829c2aa13b 100644 --- a/tests/components/recorder/test_backup.py +++ b/tests/components/recorder/test_backup.py @@ -13,7 +13,7 @@ from homeassistant.exceptions import HomeAssistantError async def test_async_pre_backup(hass: HomeAssistant, recorder_mock) -> None: """Test pre backup.""" with patch( - "homeassistant.components.recorder.backup.Recorder.lock_database" + "homeassistant.components.recorder.core.Recorder.lock_database" ) as lock_mock: await async_pre_backup(hass) assert lock_mock.called @@ -24,7 +24,7 @@ async def test_async_pre_backup_with_timeout( ) -> None: """Test pre backup with timeout.""" with patch( - "homeassistant.components.recorder.backup.Recorder.lock_database", + "homeassistant.components.recorder.core.Recorder.lock_database", side_effect=TimeoutError(), ) as lock_mock, pytest.raises(TimeoutError): await async_pre_backup(hass) @@ -45,7 +45,7 @@ async def test_async_pre_backup_with_migration( async def test_async_post_backup(hass: HomeAssistant, recorder_mock) -> None: """Test post backup.""" with patch( - "homeassistant.components.recorder.backup.Recorder.unlock_database" + "homeassistant.components.recorder.core.Recorder.unlock_database" ) as unlock_mock: await async_post_backup(hass) assert unlock_mock.called @@ -54,7 +54,7 @@ async def test_async_post_backup(hass: HomeAssistant, recorder_mock) -> None: async def test_async_post_backup_failure(hass: HomeAssistant, recorder_mock) -> None: """Test post backup failure.""" with patch( - "homeassistant.components.recorder.backup.Recorder.unlock_database", + "homeassistant.components.recorder.core.Recorder.unlock_database", return_value=False, ) as unlock_mock, pytest.raises(HomeAssistantError): await async_post_backup(hass) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 3e25a54e39d..82444f86a05 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -24,7 +24,7 @@ from homeassistant.components.recorder import ( Recorder, get_instance, ) -from homeassistant.components.recorder.const import DATA_INSTANCE, KEEPALIVE_TIME +from homeassistant.components.recorder.const import KEEPALIVE_TIME from homeassistant.components.recorder.db_schema import ( SCHEMA_VERSION, EventData, @@ -100,13 +100,13 @@ async def test_shutdown_before_startup_finishes( } hass.state = CoreState.not_running - await async_setup_recorder_instance(hass, config) - await hass.data[DATA_INSTANCE].async_db_ready + instance = await async_setup_recorder_instance(hass, config) + await instance.async_db_ready await hass.async_block_till_done() - session = await hass.async_add_executor_job(hass.data[DATA_INSTANCE].get_session) + session = await hass.async_add_executor_job(instance.get_session) - with patch.object(hass.data[DATA_INSTANCE], "engine"): + with patch.object(instance, "engine"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() await hass.async_stop() @@ -214,14 +214,16 @@ async def test_saving_many_states( hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT ): """Test we expire after many commits.""" - await async_setup_recorder_instance(hass, {recorder.CONF_COMMIT_INTERVAL: 0}) + instance = await async_setup_recorder_instance( + hass, {recorder.CONF_COMMIT_INTERVAL: 0} + ) entity_id = "test.recorder" attributes = {"test_attr": 5, "test_attr_10": "nice"} - with patch.object( - hass.data[DATA_INSTANCE].event_session, "expire_all" - ) as expire_all, patch.object(recorder.core, "EXPIRE_AFTER_COMMITS", 2): + with patch.object(instance.event_session, "expire_all") as expire_all, patch.object( + recorder.core, "EXPIRE_AFTER_COMMITS", 2 + ): for _ in range(3): hass.states.async_set(entity_id, "on", attributes) await async_wait_recording_done(hass) @@ -269,14 +271,14 @@ def test_saving_state_with_exception(hass, hass_recorder, caplog): attributes = {"test_attr": 5, "test_attr_10": "nice"} def _throw_if_state_in_session(*args, **kwargs): - for obj in hass.data[DATA_INSTANCE].event_session: + for obj in get_instance(hass).event_session: if isinstance(obj, States): raise OperationalError( "insert the state", "fake params", "forced to fail" ) with patch("time.sleep"), patch.object( - hass.data[DATA_INSTANCE].event_session, + get_instance(hass).event_session, "flush", side_effect=_throw_if_state_in_session, ): @@ -307,14 +309,14 @@ def test_saving_state_with_sqlalchemy_exception(hass, hass_recorder, caplog): attributes = {"test_attr": 5, "test_attr_10": "nice"} def _throw_if_state_in_session(*args, **kwargs): - for obj in hass.data[DATA_INSTANCE].event_session: + for obj in get_instance(hass).event_session: if isinstance(obj, States): raise SQLAlchemyError( "insert the state", "fake params", "forced to fail" ) with patch("time.sleep"), patch.object( - hass.data[DATA_INSTANCE].event_session, + get_instance(hass).event_session, "flush", side_effect=_throw_if_state_in_session, ): @@ -390,7 +392,7 @@ def test_saving_event(hass, hass_recorder): assert len(events) == 1 event: Event = events[0] - hass.data[DATA_INSTANCE].block_till_done() + get_instance(hass).block_till_done() events: list[Event] = [] with session_scope(hass=hass) as session: @@ -421,7 +423,7 @@ def test_saving_event(hass, hass_recorder): def test_saving_state_with_commit_interval_zero(hass_recorder): """Test saving a state with a commit interval of zero.""" hass = hass_recorder({"commit_interval": 0}) - assert hass.data[DATA_INSTANCE].commit_interval == 0 + get_instance(hass).commit_interval == 0 entity_id = "test.recorder" state = "restoring_from_db" @@ -690,7 +692,7 @@ def run_tasks_at_time(hass, test_time): """Advance the clock and wait for any callbacks to finish.""" fire_time_changed(hass, test_time) hass.block_till_done() - hass.data[DATA_INSTANCE].block_till_done() + get_instance(hass).block_till_done() @pytest.mark.parametrize("enable_nightly_purge", [True]) @@ -1258,7 +1260,7 @@ async def test_database_corruption_while_running(hass, tmpdir, caplog): sqlite3_exception.__cause__ = sqlite3.DatabaseError() with patch.object( - hass.data[DATA_INSTANCE].event_session, + get_instance(hass).event_session, "close", side_effect=OperationalError("statement", {}, []), ): @@ -1267,7 +1269,7 @@ async def test_database_corruption_while_running(hass, tmpdir, caplog): await async_wait_recording_done(hass) with patch.object( - hass.data[DATA_INSTANCE].event_session, + get_instance(hass).event_session, "commit", side_effect=[sqlite3_exception, None], ): @@ -1357,7 +1359,7 @@ async def test_database_lock_and_unlock( with session_scope(hass=hass) as session: return list(session.query(Events).filter_by(event_type=event_type)) - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) assert await instance.lock_database() @@ -1399,7 +1401,7 @@ async def test_database_lock_and_overflow( with session_scope(hass=hass) as session: return list(session.query(Events).filter_by(event_type=event_type)) - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) with patch.object(recorder.core, "MAX_QUEUE_BACKLOG", 1), patch.object( recorder.core, "DB_LOCK_QUEUE_CHECK_TIMEOUT", 0.1 @@ -1424,7 +1426,7 @@ async def test_database_lock_timeout(hass, recorder_mock): """Test locking database timeout when recorder stopped.""" hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) class BlockQueue(recorder.tasks.RecorderTask): event: threading.Event = threading.Event() @@ -1447,7 +1449,7 @@ async def test_database_lock_without_instance(hass, recorder_mock): """Test database lock doesn't fail if instance is not initialized.""" hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - instance: Recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) with patch.object(instance, "engine", None): try: assert await instance.lock_database() diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 38d6a191809..a57bd246f8b 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -21,7 +21,6 @@ from sqlalchemy.pool import StaticPool from homeassistant.bootstrap import async_setup_component from homeassistant.components import persistent_notification as pn, recorder from homeassistant.components.recorder import db_schema, migration -from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.db_schema import ( SCHEMA_VERSION, RecorderRuns, @@ -82,7 +81,7 @@ async def test_migration_in_progress(hass): await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) - await hass.data[DATA_INSTANCE].async_migration_event.wait() + await recorder.get_instance(hass).async_migration_event.wait() assert recorder.util.async_migration_in_progress(hass) is True await async_wait_recording_done(hass) @@ -112,7 +111,7 @@ async def test_database_migration_failed(hass): hass.states.async_set("my.entity", "on", {}) hass.states.async_set("my.entity", "off", {}) await hass.async_block_till_done() - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].join) + await hass.async_add_executor_job(recorder.get_instance(hass).join) await hass.async_block_till_done() assert recorder.util.async_migration_in_progress(hass) is False @@ -172,7 +171,7 @@ async def test_database_migration_encounters_corruption_not_sqlite(hass): hass.states.async_set("my.entity", "on", {}) hass.states.async_set("my.entity", "off", {}) await hass.async_block_till_done() - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].join) + await hass.async_add_executor_job(recorder.get_instance(hass).join) await hass.async_block_till_done() assert recorder.util.async_migration_in_progress(hass) is False @@ -201,7 +200,7 @@ async def test_events_during_migration_are_queued(hass): async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=2)) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4)) - await hass.data[DATA_INSTANCE].async_recorder_ready.wait() + await recorder.get_instance(hass).async_recorder_ready.wait() await async_wait_recording_done(hass) assert recorder.util.async_migration_in_progress(hass) is False @@ -232,7 +231,7 @@ async def test_events_during_migration_queue_exhausted(hass): async_fire_time_changed(hass, dt_util.utcnow() + datetime.timedelta(hours=4)) await hass.async_block_till_done() hass.states.async_set("my.entity", "off", {}) - await hass.data[DATA_INSTANCE].async_recorder_ready.wait() + await recorder.get_instance(hass).async_recorder_ready.wait() await async_wait_recording_done(hass) assert recorder.util.async_migration_in_progress(hass) is False diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 30a2926844b..ee76b40a15b 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -12,7 +12,7 @@ from sqlalchemy.orm import Session from homeassistant.components import recorder from homeassistant.components.recorder import history, statistics -from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX +from homeassistant.components.recorder.const import SQLITE_URL_PREFIX from homeassistant.components.recorder.db_schema import StatisticsShortTerm from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat from homeassistant.components.recorder.statistics import ( @@ -45,7 +45,7 @@ ORIG_TZ = dt_util.DEFAULT_TIME_ZONE def test_compile_hourly_statistics(hass_recorder): """Test compiling hourly statistics.""" hass = hass_recorder() - recorder = hass.data[DATA_INSTANCE] + instance = recorder.get_instance(hass) setup_component(hass, "sensor", {}) zero, four, states = record_states(hass) hist = history.get_significant_states(hass, zero, four) @@ -142,7 +142,7 @@ def test_compile_hourly_statistics(hass_recorder): stats = get_last_short_term_statistics(hass, 1, "sensor.test3", True) assert stats == {} - recorder.get_session().query(StatisticsShortTerm).delete() + instance.get_session().query(StatisticsShortTerm).delete() # Should not fail there is nothing in the table stats = get_latest_short_term_statistics(hass, ["sensor.test1"]) assert stats == {} diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 8624719f951..ac4eeada3d3 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -13,7 +13,7 @@ from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components import recorder from homeassistant.components.recorder import history, util -from homeassistant.components.recorder.const import DATA_INSTANCE, SQLITE_URL_PREFIX +from homeassistant.components.recorder.const import SQLITE_URL_PREFIX from homeassistant.components.recorder.db_schema import RecorderRuns from homeassistant.components.recorder.models import UnsupportedDialect from homeassistant.components.recorder.util import ( @@ -35,7 +35,7 @@ def test_session_scope_not_setup(hass_recorder): """Try to create a session scope when not setup.""" hass = hass_recorder() with patch.object( - hass.data[DATA_INSTANCE], "get_session", return_value=None + util.get_instance(hass), "get_session", return_value=None ), pytest.raises(RuntimeError): with util.session_scope(hass=hass): pass @@ -547,7 +547,7 @@ def test_basic_sanity_check(hass_recorder): """Test the basic sanity checks with a missing table.""" hass = hass_recorder() - cursor = hass.data[DATA_INSTANCE].engine.raw_connection().cursor() + cursor = util.get_instance(hass).engine.raw_connection().cursor() assert util.basic_sanity_check(cursor) is True @@ -560,7 +560,7 @@ def test_basic_sanity_check(hass_recorder): def test_combined_checks(hass_recorder, caplog): """Run Checks on the open database.""" hass = hass_recorder() - instance = recorder.get_instance(hass) + instance = util.get_instance(hass) instance.db_retry_wait = 0 cursor = instance.engine.raw_connection().cursor() @@ -639,8 +639,8 @@ def test_end_incomplete_runs(hass_recorder, caplog): def test_periodic_db_cleanups(hass_recorder): """Test periodic db cleanups.""" hass = hass_recorder() - with patch.object(hass.data[DATA_INSTANCE].engine, "connect") as connect_mock: - util.periodic_db_cleanups(hass.data[DATA_INSTANCE]) + with patch.object(util.get_instance(hass).engine, "connect") as connect_mock: + util.periodic_db_cleanups(util.get_instance(hass)) text_obj = connect_mock.return_value.__enter__.return_value.execute.mock_calls[0][ 1 @@ -663,11 +663,9 @@ async def test_write_lock_db( config = { recorder.CONF_DB_URL: "sqlite:///" + str(tmp_path / "pytest.db?timeout=0.1") } - await async_setup_recorder_instance(hass, config) + instance = await async_setup_recorder_instance(hass, config) await hass.async_block_till_done() - instance = hass.data[DATA_INSTANCE] - def _drop_table(): with instance.engine.connect() as connection: connection.execute(text("DROP TABLE events;")) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index a7ac01cf1d1..283883030fa 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -8,7 +8,6 @@ import pytest from pytest import approx from homeassistant.components import recorder -from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.components.recorder.statistics import ( async_add_external_statistics, get_last_statistics, @@ -304,7 +303,7 @@ async def test_recorder_info_bad_recorder_config(hass, hass_ws_client): await hass.async_block_till_done() # Wait for recorder to shut down - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].join) + await hass.async_add_executor_job(recorder.get_instance(hass).join) await client.send_json({"id": 1, "type": "recorder/info"}) response = await client.receive_json() diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 4be59e4c82c..cc2f9c76f1f 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -10,7 +10,6 @@ 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.db_schema import StatisticsMeta from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat from homeassistant.components.recorder.statistics import ( @@ -18,7 +17,7 @@ from homeassistant.components.recorder.statistics import ( list_statistic_ids, statistics_during_period, ) -from homeassistant.components.recorder.util import session_scope +from homeassistant.components.recorder.util import get_instance, session_scope from homeassistant.const import STATE_UNAVAILABLE from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util @@ -2290,7 +2289,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): hass = hass_recorder() # Remove this after dropping the use of the hass_recorder fixture hass.config.set_time_zone("America/Regina") - recorder = hass.data[DATA_INSTANCE] + instance = get_instance(hass) setup_component(hass, "sensor", {}) wait_recording_done(hass) # Wait for the sensor recorder platform to be added attributes = { @@ -2454,7 +2453,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): sum_adjustement_start = zero + timedelta(minutes=65) for i in range(13, 24): expected_sums["sensor.test4"][i] += sum_adjustment - recorder.async_adjust_statistics( + instance.async_adjust_statistics( "sensor.test4", sum_adjustement_start, sum_adjustment ) wait_recording_done(hass) From 9d0a252ca73ea1b002bf13c7d77f652d5542794b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 22 Jul 2022 13:36:43 +0200 Subject: [PATCH 2783/3516] Improve handling of MQTT config entry data (#72691) * Improve handling of MQTT config entry data * Add test * Add warning * Adjust tests --- homeassistant/components/mqtt/__init__.py | 31 ++++++ tests/components/mqtt/test_diagnostics.py | 2 +- tests/components/mqtt/test_discovery.py | 18 +++- tests/components/mqtt/test_init.py | 122 ++++++++++++---------- tests/conftest.py | 31 ++++-- 5 files changed, 135 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 394e4af3e2e..2bea1a593d1 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -107,6 +107,16 @@ CONNECTION_SUCCESS = "connection_success" CONNECTION_FAILED = "connection_failed" CONNECTION_FAILED_RECOVERABLE = "connection_failed_recoverable" +CONFIG_ENTRY_CONFIG_KEYS = [ + CONF_BIRTH_MESSAGE, + CONF_BROKER, + CONF_DISCOVERY, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_WILL_MESSAGE, +] + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( @@ -185,6 +195,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True +def _filter_entry_config(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Remove unknown keys from config entry data. + + Extra keys may have been added when importing MQTT yaml configuration. + """ + filtered_data = { + k: entry.data[k] for k in CONFIG_ENTRY_CONFIG_KEYS if k in entry.data + } + if entry.data.keys() != filtered_data.keys(): + _LOGGER.warning( + "The following unsupported configuration options were removed from the " + "MQTT config entry: %s. Add them to configuration.yaml if they are needed", + entry.data.keys() - filtered_data.keys(), + ) + hass.config_entries.async_update_entry(entry, data=filtered_data) + + def _merge_basic_config( hass: HomeAssistant, entry: ConfigEntry, yaml_config: dict[str, Any] ) -> None: @@ -243,6 +270,10 @@ async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | mqtt_config = CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {})) hass.data[DATA_MQTT_CONFIG] = mqtt_config + # Remove unknown keys from config entry data + _filter_entry_config(hass, entry) + + # Merge basic configuration, and add missing defaults for basic options _merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {})) # Bail out if broker setting is missing if CONF_BROKER not in entry.data: diff --git a/tests/components/mqtt/test_diagnostics.py b/tests/components/mqtt/test_diagnostics.py index 8cc5d0b1070..7c486f9af74 100644 --- a/tests/components/mqtt/test_diagnostics.py +++ b/tests/components/mqtt/test_diagnostics.py @@ -158,7 +158,7 @@ async def test_entry_diagnostics( @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index d185d3334d0..e4c6f44883a 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -46,7 +46,7 @@ def entity_reg(hass): @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) async def test_subscribing_config_topic(hass, mqtt_mock_entry_no_yaml_config): @@ -1238,19 +1238,27 @@ async def test_no_implicit_state_topic_switch( @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.BINARY_SENSOR]) @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_DISCOVERY_PREFIX: "my_home/homeassistant/register", } ], ) async def test_complex_discovery_topic_prefix( - hass, mqtt_mock_entry_no_yaml_config, caplog + hass, mqtt_mock_entry_with_yaml_config, caplog ): """Tests handling of discovery topic prefix with multiple slashes.""" - await mqtt_mock_entry_no_yaml_config() + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + mqtt.CONF_DISCOVERY_PREFIX: "my_home/homeassistant/register", + } + }, + ) + async_fire_mqtt_message( hass, ("my_home/homeassistant/register/binary_sensor/node1/object1/config"), diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index cec6038ae04..1f263d40fc2 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1249,7 +1249,7 @@ async def test_unsubscribe_race(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_ @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) async def test_restore_subscriptions_on_reconnect( @@ -1272,7 +1272,7 @@ async def test_restore_subscriptions_on_reconnect( @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_DISCOVERY: False}], ) async def test_restore_all_active_subscriptions_on_reconnect( @@ -1486,19 +1486,19 @@ async def test_setup_manual_mqtt_empty_platform(hass, caplog): @patch("homeassistant.components.mqtt.PLATFORMS", []) -async def test_setup_mqtt_client_protocol(hass): +async def test_setup_mqtt_client_protocol(hass, mqtt_mock_entry_with_yaml_config): """Test MQTT client protocol setup.""" - entry = MockConfigEntry( - domain=mqtt.DOMAIN, - data={ - mqtt.CONF_BROKER: "test-broker", - mqtt.config_integration.CONF_PROTOCOL: "3.1", - }, - ) - entry.add_to_hass(hass) with patch("paho.mqtt.client.Client") as mock_client: + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + mqtt.config_integration.CONF_PROTOCOL: "3.1", + } + }, + ) mock_client.on_connect(return_value=0) - assert await mqtt.async_setup_entry(hass, entry) await hass.async_block_till_done() # check if protocol setup was correctly @@ -1563,9 +1563,17 @@ async def test_setup_raises_ConfigEntryNotReady_if_no_connect_broker(hass, caplo assert "Failed to connect to MQTT server due to exception:" in caplog.text -@pytest.mark.parametrize("insecure", [None, False, True]) +@pytest.mark.parametrize( + "config, insecure_param", + [ + ({"certificate": "auto"}, "not set"), + ({"certificate": "auto", "tls_insecure": False}, False), + ({"certificate": "auto", "tls_insecure": True}, True), + ], +) +@patch("homeassistant.components.mqtt.PLATFORMS", []) async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( - hass, insecure + hass, config, insecure_param, mqtt_mock_entry_with_yaml_config ): """Test setup uses bundled certs when certificate is set to auto and insecure.""" calls = [] @@ -1577,18 +1585,14 @@ async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( def mock_tls_insecure_set(insecure_param): insecure_check["insecure"] = insecure_param - config_item_data = {mqtt.CONF_BROKER: "test-broker", "certificate": "auto"} - if insecure is not None: - config_item_data["tls_insecure"] = insecure with patch("paho.mqtt.client.Client") as mock_client: mock_client().tls_set = mock_tls_set mock_client().tls_insecure_set = mock_tls_insecure_set - entry = MockConfigEntry( - domain=mqtt.DOMAIN, - data=config_item_data, + assert await async_setup_component( + hass, + mqtt.DOMAIN, + {mqtt.DOMAIN: config}, ) - entry.add_to_hass(hass) - assert await mqtt.async_setup_entry(hass, entry) await hass.async_block_till_done() assert calls @@ -1596,19 +1600,14 @@ async def test_setup_uses_certificate_on_certificate_set_to_auto_and_insecure( import certifi expectedCertificate = certifi.where() - # assert mock_mqtt.mock_calls[0][1][2]["certificate"] == expectedCertificate assert calls[0][0] == expectedCertificate # test if insecure is set - assert ( - insecure_check["insecure"] == insecure - if insecure is not None - else insecure_check["insecure"] == "not set" - ) + assert insecure_check["insecure"] == insecure_param -async def test_setup_without_tls_config_uses_tlsv1_under_python36(hass): - """Test setup defaults to TLSv1 under python3.6.""" +async def test_tls_version(hass, mqtt_mock_entry_with_yaml_config): + """Test setup defaults for tls.""" calls = [] def mock_tls_set(certificate, certfile=None, keyfile=None, tls_version=None): @@ -1616,28 +1615,19 @@ async def test_setup_without_tls_config_uses_tlsv1_under_python36(hass): with patch("paho.mqtt.client.Client") as mock_client: mock_client().tls_set = mock_tls_set - entry = MockConfigEntry( - domain=mqtt.DOMAIN, - data={"certificate": "auto", mqtt.CONF_BROKER: "test-broker"}, + assert await async_setup_component( + hass, + mqtt.DOMAIN, + {mqtt.DOMAIN: {"certificate": "auto"}}, ) - entry.add_to_hass(hass) - assert await mqtt.async_setup_entry(hass, entry) await hass.async_block_till_done() assert calls - - import sys - - if sys.hexversion >= 0x03060000: - expectedTlsVersion = ssl.PROTOCOL_TLS # pylint: disable=no-member - else: - expectedTlsVersion = ssl.PROTOCOL_TLSv1 - - assert calls[0][3] == expectedTlsVersion + assert calls[0][3] == ssl.PROTOCOL_TLS @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", @@ -1670,7 +1660,7 @@ async def test_custom_birth_message( @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", @@ -1705,7 +1695,7 @@ async def test_default_birth_message( @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}}], ) async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config): @@ -1719,7 +1709,7 @@ async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_ @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", @@ -1733,7 +1723,7 @@ async def test_no_birth_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_ ], ) async def test_delayed_birth_message( - hass, mqtt_client_mock, mqtt_config, mqtt_mock_entry_no_yaml_config + hass, mqtt_client_mock, mqtt_config_entry_data, mqtt_mock_entry_no_yaml_config ): """Test sending birth message does not happen until Home Assistant starts.""" mqtt_mock = await mqtt_mock_entry_no_yaml_config() @@ -1743,7 +1733,7 @@ async def test_delayed_birth_message( await hass.async_block_till_done() - entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config) + entry = MockConfigEntry(domain=mqtt.DOMAIN, data=mqtt_config_entry_data) entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -1780,7 +1770,7 @@ async def test_delayed_birth_message( @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", @@ -1816,7 +1806,7 @@ async def test_default_will_message( @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [{mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_WILL_MESSAGE: {}}], ) async def test_no_will_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_config): @@ -1827,7 +1817,7 @@ async def test_no_will_message(hass, mqtt_client_mock, mqtt_mock_entry_no_yaml_c @pytest.mark.parametrize( - "mqtt_config", + "mqtt_config_entry_data", [ { mqtt.CONF_BROKER: "mock-broker", @@ -2922,3 +2912,29 @@ async def test_setup_manual_items_with_unique_ids( assert hass.states.get("light.test1") is not None assert (hass.states.get("light.test2") is not None) == unique assert bool("Platform mqtt does not generate unique IDs." in caplog.text) != unique + + +async def test_remove_unknown_conf_entry_options(hass, mqtt_client_mock, caplog): + """Test unknown keys in config entry data is removed.""" + mqtt_config_entry_data = { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_BIRTH_MESSAGE: {}, + mqtt.client.CONF_PROTOCOL: mqtt.const.PROTOCOL_311, + } + + entry = MockConfigEntry( + data=mqtt_config_entry_data, + domain=mqtt.DOMAIN, + title="MQTT", + ) + + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert mqtt.client.CONF_PROTOCOL not in entry.data + assert ( + "The following unsupported configuration options were removed from the " + "MQTT config entry: {'protocol'}. Add them to configuration.yaml if they " + "are needed" + ) in caplog.text diff --git a/tests/conftest.py b/tests/conftest.py index ee2e903e88d..dc5f3069332 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -501,7 +501,7 @@ def fail_on_log_exception(request, monkeypatch): @pytest.fixture -def mqtt_config(): +def mqtt_config_entry_data(): """Fixture to allow overriding MQTT config.""" return None @@ -553,7 +553,7 @@ def mqtt_client_mock(hass): async def mqtt_mock( hass, mqtt_client_mock, - mqtt_config, + mqtt_config_entry_data, mqtt_mock_entry_no_yaml_config, ): """Fixture to mock MQTT component.""" @@ -561,15 +561,18 @@ async def mqtt_mock( @asynccontextmanager -async def _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config): +async def _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config_entry_data): """Fixture to mock a delayed setup of the MQTT config entry.""" - if mqtt_config is None: - mqtt_config = {mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}} + if mqtt_config_entry_data is None: + mqtt_config_entry_data = { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_BIRTH_MESSAGE: {}, + } await hass.async_block_till_done() entry = MockConfigEntry( - data=mqtt_config, + data=mqtt_config_entry_data, domain=mqtt.DOMAIN, title="MQTT", ) @@ -613,7 +616,9 @@ async def _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config): @pytest.fixture -async def mqtt_mock_entry_no_yaml_config(hass, mqtt_client_mock, mqtt_config): +async def mqtt_mock_entry_no_yaml_config( + hass, mqtt_client_mock, mqtt_config_entry_data +): """Set up an MQTT config entry without MQTT yaml config.""" async def _async_setup_config_entry(hass, entry): @@ -626,12 +631,16 @@ async def mqtt_mock_entry_no_yaml_config(hass, mqtt_client_mock, mqtt_config): """Set up the MQTT config entry.""" return await mqtt_mock_entry(_async_setup_config_entry) - async with _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config) as mqtt_mock_entry: + async with _mqtt_mock_entry( + hass, mqtt_client_mock, mqtt_config_entry_data + ) as mqtt_mock_entry: yield _setup_mqtt_entry @pytest.fixture -async def mqtt_mock_entry_with_yaml_config(hass, mqtt_client_mock, mqtt_config): +async def mqtt_mock_entry_with_yaml_config( + hass, mqtt_client_mock, mqtt_config_entry_data +): """Set up an MQTT config entry with MQTT yaml config.""" async def _async_do_not_setup_config_entry(hass, entry): @@ -642,7 +651,9 @@ async def mqtt_mock_entry_with_yaml_config(hass, mqtt_client_mock, mqtt_config): """Set up the MQTT config entry.""" return await mqtt_mock_entry(_async_do_not_setup_config_entry) - async with _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config) as mqtt_mock_entry: + async with _mqtt_mock_entry( + hass, mqtt_client_mock, mqtt_config_entry_data + ) as mqtt_mock_entry: yield _setup_mqtt_entry From fd6ffef52f337df71542b48565a95300c0ab2766 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 22 Jul 2022 15:11:34 +0200 Subject: [PATCH 2784/3516] Support non-live database migration (#72433) * Support non-live database migration * Tweak startup order, add test * Address review comments * Fix typo * Clarify comment about promoting dependencies * Tweak * Fix merge mistake * Fix some tests * Fix additional test * Fix additional test * Adjust tests * Improve test coverage --- homeassistant/bootstrap.py | 75 +- homeassistant/components/recorder/__init__.py | 1 - homeassistant/components/recorder/core.py | 42 +- .../components/recorder/migration.py | 15 +- homeassistant/components/recorder/models.py | 10 + .../components/recorder/statistics.py | 6 +- homeassistant/components/recorder/tasks.py | 2 +- homeassistant/components/recorder/util.py | 14 +- .../components/recorder/websocket_api.py | 4 +- homeassistant/helpers/recorder.py | 26 +- tests/common.py | 3 + tests/components/default_config/test_init.py | 2 + tests/components/recorder/db_schema_25.py | 673 ++++++++++++++++++ tests/components/recorder/test_init.py | 25 +- tests/components/recorder/test_migrate.py | 47 +- tests/components/recorder/test_statistics.py | 5 + .../recorder/test_statistics_v23_migration.py | 9 + .../components/recorder/test_websocket_api.py | 15 +- tests/conftest.py | 4 +- tests/test_bootstrap.py | 76 ++ 20 files changed, 993 insertions(+), 61 deletions(-) create mode 100644 tests/components/recorder/db_schema_25.py diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index b8ec5987142..d2858bfcdf1 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -24,7 +24,7 @@ from .const import ( SIGNAL_BOOTSTRAP_INTEGRATONS, ) from .exceptions import HomeAssistantError -from .helpers import area_registry, device_registry, entity_registry +from .helpers import area_registry, device_registry, entity_registry, recorder from .helpers.dispatcher import async_dispatcher_send from .helpers.typing import ConfigType from .setup import ( @@ -66,6 +66,15 @@ LOGGING_INTEGRATIONS = { # Error logging "system_log", "sentry", +} +FRONTEND_INTEGRATIONS = { + # Get the frontend up and running as soon as possible so problem + # integrations can be removed and database migration status is + # visible in frontend + "frontend", +} +RECORDER_INTEGRATIONS = { + # Setup after frontend # To record data "recorder", } @@ -83,10 +92,6 @@ STAGE_1_INTEGRATIONS = { "cloud", # Ensure supervisor is available "hassio", - # Get the frontend up and running as soon - # as possible so problem integrations can - # be removed - "frontend", } @@ -504,11 +509,43 @@ async def _async_set_up_integrations( _LOGGER.info("Domains to be set up: %s", domains_to_setup) + def _cache_uname_processor() -> None: + """Cache the result of platform.uname().processor in the executor. + + Multiple modules call this function at startup which + executes a blocking subprocess call. This is a problem for the + asyncio event loop. By primeing the cache of uname we can + avoid the blocking call in the event loop. + """ + platform.uname().processor # pylint: disable=expression-not-assigned + + # Load the registries and cache the result of platform.uname().processor + await asyncio.gather( + device_registry.async_load(hass), + entity_registry.async_load(hass), + area_registry.async_load(hass), + hass.async_add_executor_job(_cache_uname_processor), + ) + + # Initialize recorder + if "recorder" in domains_to_setup: + recorder.async_initialize_recorder(hass) + # Load logging as soon as possible if logging_domains := domains_to_setup & LOGGING_INTEGRATIONS: _LOGGER.info("Setting up logging: %s", logging_domains) await async_setup_multi_components(hass, logging_domains, config) + # Setup frontend + if frontend_domains := domains_to_setup & FRONTEND_INTEGRATIONS: + _LOGGER.info("Setting up frontend: %s", frontend_domains) + await async_setup_multi_components(hass, frontend_domains, config) + + # Setup recorder + if recorder_domains := domains_to_setup & RECORDER_INTEGRATIONS: + _LOGGER.info("Setting up recorder: %s", recorder_domains) + await async_setup_multi_components(hass, recorder_domains, config) + # Start up debuggers. Start these first in case they want to wait. if debuggers := domains_to_setup & DEBUGGER_INTEGRATIONS: _LOGGER.debug("Setting up debuggers: %s", debuggers) @@ -518,7 +555,8 @@ async def _async_set_up_integrations( stage_1_domains: set[str] = set() # Find all dependencies of any dependency of any stage 1 integration that - # we plan on loading and promote them to stage 1 + # we plan on loading and promote them to stage 1. This is done only to not + # get misleading log messages deps_promotion: set[str] = STAGE_1_INTEGRATIONS while deps_promotion: old_deps_promotion = deps_promotion @@ -535,24 +573,13 @@ async def _async_set_up_integrations( deps_promotion.update(dep_itg.all_dependencies) - stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains - - def _cache_uname_processor() -> None: - """Cache the result of platform.uname().processor in the executor. - - Multiple modules call this function at startup which - executes a blocking subprocess call. This is a problem for the - asyncio event loop. By primeing the cache of uname we can - avoid the blocking call in the event loop. - """ - platform.uname().processor # pylint: disable=expression-not-assigned - - # Load the registries - await asyncio.gather( - device_registry.async_load(hass), - entity_registry.async_load(hass), - area_registry.async_load(hass), - hass.async_add_executor_job(_cache_uname_processor), + stage_2_domains = ( + domains_to_setup + - logging_domains + - frontend_domains + - recorder_domains + - debuggers + - stage_1_domains ) # Start setup diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 238b013f366..f9ed5f59333 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -123,7 +123,6 @@ def is_entity_recorded(hass: HomeAssistant, entity_id: str) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the recorder.""" - hass.data[DOMAIN] = {} exclude_attributes_by_domain: dict[str, set[str]] = {} hass.data[EXCLUDE_ATTRIBUTES] = exclude_attributes_by_domain conf = config[DOMAIN] diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index f3ae79f9909..92e10b47126 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -43,6 +43,7 @@ import homeassistant.util.dt as dt_util from . import migration, statistics from .const import ( DB_WORKER_PREFIX, + DOMAIN, KEEPALIVE_TIME, MAX_QUEUE_BACKLOG, MYSQLDB_URL_PREFIX, @@ -166,7 +167,12 @@ class Recorder(threading.Thread): self.db_max_retries = db_max_retries self.db_retry_wait = db_retry_wait self.engine_version: AwesomeVersion | None = None + # Database connection is ready, but non-live migration may be in progress + db_connected: asyncio.Future[bool] = hass.data[DOMAIN].db_connected + self.async_db_connected: asyncio.Future[bool] = db_connected + # Database is ready to use but live migration may be in progress self.async_db_ready: asyncio.Future[bool] = asyncio.Future() + # Database is ready to use and all migration steps completed (used by tests) self.async_recorder_ready = asyncio.Event() self._queue_watch = threading.Event() self.engine: Engine | None = None @@ -188,6 +194,7 @@ class Recorder(threading.Thread): self._completed_first_database_setup: bool | None = None self.async_migration_event = asyncio.Event() self.migration_in_progress = False + self.migration_is_live = False self._database_lock_task: DatabaseLockTask | None = None self._db_executor: DBInterruptibleThreadPoolExecutor | None = None self._exclude_attributes_by_domain = exclude_attributes_by_domain @@ -289,7 +296,8 @@ class Recorder(threading.Thread): def _stop_executor(self) -> None: """Stop the executor.""" - assert self._db_executor is not None + if self._db_executor is None: + return self._db_executor.shutdown() self._db_executor = None @@ -410,6 +418,7 @@ class Recorder(threading.Thread): @callback def async_connection_failed(self) -> None: """Connect failed tasks.""" + self.async_db_connected.set_result(False) self.async_db_ready.set_result(False) persistent_notification.async_create( self.hass, @@ -420,13 +429,29 @@ class Recorder(threading.Thread): @callback def async_connection_success(self) -> None: - """Connect success tasks.""" + """Connect to the database succeeded, schema version and migration need known. + + The database may not yet be ready for use in case of a non-live migration. + """ + self.async_db_connected.set_result(True) + + @callback + def async_set_recorder_ready(self) -> None: + """Database live and ready for use. + + Called after non-live migration steps are finished. + """ + if self.async_db_ready.done(): + return self.async_db_ready.set_result(True) self.async_start_executor() @callback - def _async_recorder_ready(self) -> None: - """Finish start and mark recorder ready.""" + def _async_set_recorder_ready_migration_done(self) -> None: + """Finish start and mark recorder ready. + + Called after all migration steps are finished. + """ self._async_setup_periodic_tasks() self.async_recorder_ready.set() @@ -548,6 +573,7 @@ class Recorder(threading.Thread): self._setup_run() else: self.migration_in_progress = True + self.migration_is_live = migration.live_migration(current_version) self.hass.add_job(self.async_connection_success) @@ -557,6 +583,7 @@ class Recorder(threading.Thread): # Make sure we cleanly close the run if # we restart before startup finishes self._shutdown() + self.hass.add_job(self.async_set_recorder_ready) return # We wait to start the migration until startup has finished @@ -577,11 +604,14 @@ class Recorder(threading.Thread): "Database Migration Failed", "recorder_database_migration", ) + self.hass.add_job(self.async_set_recorder_ready) self._shutdown() return + self.hass.add_job(self.async_set_recorder_ready) + _LOGGER.debug("Recorder processing the queue") - self.hass.add_job(self._async_recorder_ready) + self.hass.add_job(self._async_set_recorder_ready_migration_done) self._run_event_loop() def _run_event_loop(self) -> None: @@ -659,7 +689,7 @@ class Recorder(threading.Thread): try: migration.migrate_schema( - self.hass, self.engine, self.get_session, current_version + self, self.hass, self.engine, self.get_session, current_version ) except exc.DatabaseError as err: if self._handle_database_error(err): diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 7e11e62502d..de6fd8f01fe 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -3,7 +3,7 @@ from collections.abc import Callable, Iterable import contextlib from datetime import timedelta import logging -from typing import cast +from typing import Any, cast import sqlalchemy from sqlalchemy import ForeignKeyConstraint, MetaData, Table, func, text @@ -40,6 +40,8 @@ from .statistics import ( ) from .util import session_scope +LIVE_MIGRATION_MIN_SCHEMA_VERSION = 0 + _LOGGER = logging.getLogger(__name__) @@ -78,7 +80,13 @@ def schema_is_current(current_version: int) -> bool: return current_version == SCHEMA_VERSION +def live_migration(current_version: int) -> bool: + """Check if live migration is possible.""" + return current_version >= LIVE_MIGRATION_MIN_SCHEMA_VERSION + + def migrate_schema( + instance: Any, hass: HomeAssistant, engine: Engine, session_maker: Callable[[], Session], @@ -86,7 +94,12 @@ def migrate_schema( ) -> None: """Check if the schema needs to be upgraded.""" _LOGGER.warning("Database is about to upgrade. Schema version: %s", current_version) + db_ready = False for version in range(current_version, SCHEMA_VERSION): + if live_migration(version) and not db_ready: + db_ready = True + instance.migration_is_live = True + hass.add_job(instance.async_set_recorder_ready) new_version = version + 1 _LOGGER.info("Upgrading recorder db schema to version %s", new_version) _apply_update(hass, engine, session_maker, new_version, current_version) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index ff53d9be3d1..98c9fc7c9b2 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -1,6 +1,8 @@ """Models for Recorder.""" from __future__ import annotations +import asyncio +from dataclasses import dataclass, field from datetime import datetime import logging from typing import Any, TypedDict, overload @@ -30,6 +32,14 @@ class UnsupportedDialect(Exception): """The dialect or its version is not supported.""" +@dataclass +class RecorderData: + """Recorder data stored in hass.data.""" + + recorder_platforms: dict[str, Any] = field(default_factory=dict) + db_connected: asyncio.Future = field(default_factory=asyncio.Future) + + class StatisticResult(TypedDict): """Statistic result data class. diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index bce77e8a31e..fcd8e4f3930 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -576,7 +576,7 @@ def compile_statistics(instance: Recorder, start: datetime) -> bool: platform_stats: list[StatisticResult] = [] current_metadata: dict[str, tuple[int, StatisticMetaData]] = {} # Collect statistics from all platforms implementing support - for domain, platform in instance.hass.data[DOMAIN].items(): + for domain, platform in instance.hass.data[DOMAIN].recorder_platforms.items(): if not hasattr(platform, "compile_statistics"): continue compiled: PlatformCompiledStatistics = platform.compile_statistics( @@ -851,7 +851,7 @@ def list_statistic_ids( } # Query all integrations with a registered recorder platform - for platform in hass.data[DOMAIN].values(): + for platform in hass.data[DOMAIN].recorder_platforms.values(): if not hasattr(platform, "list_statistic_ids"): continue platform_statistic_ids = platform.list_statistic_ids( @@ -1339,7 +1339,7 @@ def _sorted_statistics_to_dict( def validate_statistics(hass: HomeAssistant) -> dict[str, list[ValidationIssue]]: """Validate statistics.""" platform_validation: dict[str, list[ValidationIssue]] = {} - for platform in hass.data[DOMAIN].values(): + for platform in hass.data[DOMAIN].recorder_platforms.values(): if not hasattr(platform, "validate_statistics"): continue platform_validation.update(platform.validate_statistics(hass)) diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index 6d1c9c360ab..cdb97d9d67c 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -249,7 +249,7 @@ class AddRecorderPlatformTask(RecorderTask): domain = self.domain platform = self.platform - platforms: dict[str, Any] = hass.data[DOMAIN] + platforms: dict[str, Any] = hass.data[DOMAIN].recorder_platforms platforms[domain] = platform if hasattr(self.platform, "exclude_attributes"): hass.data[EXCLUDE_ATTRIBUTES][domain] = platform.exclude_attributes(hass) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index fdf42665ef5..ddc8747f79b 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -552,7 +552,7 @@ def write_lock_db_sqlite(instance: Recorder) -> Generator[None, None, None]: def async_migration_in_progress(hass: HomeAssistant) -> bool: - """Determine is a migration is in progress. + """Determine if a migration is in progress. This is a thin wrapper that allows us to change out the implementation later. @@ -563,6 +563,18 @@ def async_migration_in_progress(hass: HomeAssistant) -> bool: return instance.migration_in_progress +def async_migration_is_live(hass: HomeAssistant) -> bool: + """Determine if a migration is live. + + This is a thin wrapper that allows us to change + out the implementation later. + """ + if DATA_INSTANCE not in hass.data: + return False + instance: Recorder = hass.data[DATA_INSTANCE] + return instance.migration_is_live + + def second_sunday(year: int, month: int) -> date: """Return the datetime.date for the second sunday of a month.""" second = date(year, month, FIRST_POSSIBLE_SUNDAY) diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index c143d8b4f0b..16813944780 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -17,7 +17,7 @@ from .statistics import ( list_statistic_ids, validate_statistics, ) -from .util import async_migration_in_progress, get_instance +from .util import async_migration_in_progress, async_migration_is_live, get_instance _LOGGER: logging.Logger = logging.getLogger(__package__) @@ -193,6 +193,7 @@ def ws_info( backlog = instance.backlog if instance else None migration_in_progress = async_migration_in_progress(hass) + migration_is_live = async_migration_is_live(hass) recording = instance.recording if instance else False thread_alive = instance.is_alive() if instance else False @@ -200,6 +201,7 @@ def ws_info( "backlog": backlog, "max_backlog": MAX_QUEUE_BACKLOG, "migration_in_progress": migration_in_progress, + "migration_is_live": migration_is_live, "recording": recording, "thread_running": thread_alive, } diff --git a/homeassistant/helpers/recorder.py b/homeassistant/helpers/recorder.py index a51d9de59e2..2049300e460 100644 --- a/homeassistant/helpers/recorder.py +++ b/homeassistant/helpers/recorder.py @@ -1,7 +1,8 @@ """Helpers to check recorder.""" +import asyncio -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback def async_migration_in_progress(hass: HomeAssistant) -> bool: @@ -12,3 +13,26 @@ def async_migration_in_progress(hass: HomeAssistant) -> bool: from homeassistant.components import recorder return recorder.util.async_migration_in_progress(hass) + + +@callback +def async_initialize_recorder(hass: HomeAssistant) -> None: + """Initialize recorder data.""" + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.recorder import const, models + + hass.data[const.DOMAIN] = models.RecorderData() + + +async def async_wait_recorder(hass: HomeAssistant) -> bool: + """Wait for recorder to initialize and return connection status. + + Returns False immediately if the recorder is not enabled. + """ + # pylint: disable-next=import-outside-toplevel + from homeassistant.components.recorder import const + + if const.DOMAIN not in hass.data: + return False + db_connected: asyncio.Future[bool] = hass.data[const.DOMAIN].db_connected + return await db_connected diff --git a/tests/common.py b/tests/common.py index 80f0913cace..acc50e26889 100644 --- a/tests/common.py +++ b/tests/common.py @@ -50,6 +50,7 @@ from homeassistant.helpers import ( entity_platform, entity_registry, intent, + recorder as recorder_helper, restore_state, storage, ) @@ -914,6 +915,8 @@ def init_recorder_component(hass, add_config=None): with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.migration.migrate_schema" ): + if recorder.DOMAIN not in hass.data: + recorder_helper.async_initialize_recorder(hass) assert setup_component(hass, recorder.DOMAIN, {recorder.DOMAIN: config}) assert recorder.DOMAIN in hass.config.components _LOGGER.info( diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index 7701eb55b90..d82b4109839 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -3,6 +3,7 @@ from unittest.mock import patch import pytest +from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import async_setup_component from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 @@ -24,4 +25,5 @@ def recorder_url_mock(): async def test_setup(hass, mock_zeroconf, mock_get_source_ip): """Test setup.""" + recorder_helper.async_initialize_recorder(hass) assert await async_setup_component(hass, "default_config", {"foo": "bar"}) diff --git a/tests/components/recorder/db_schema_25.py b/tests/components/recorder/db_schema_25.py new file mode 100644 index 00000000000..43aa245a761 --- /dev/null +++ b/tests/components/recorder/db_schema_25.py @@ -0,0 +1,673 @@ +"""Models for SQLAlchemy.""" +from __future__ import annotations + +from datetime import datetime, timedelta +import json +import logging +from typing import Any, TypedDict, cast, overload + +from fnvhash import fnv1a_32 +from sqlalchemy import ( + BigInteger, + Boolean, + Column, + DateTime, + Float, + ForeignKey, + Identity, + Index, + Integer, + String, + Text, + distinct, +) +from sqlalchemy.dialects import mysql, oracle, postgresql +from sqlalchemy.engine.row import Row +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.orm import declarative_base, relationship +from sqlalchemy.orm.session import Session + +from homeassistant.components.recorder.const import ALL_DOMAIN_EXCLUDE_ATTRS, JSON_DUMP +from homeassistant.const import ( + MAX_LENGTH_EVENT_CONTEXT_ID, + MAX_LENGTH_EVENT_EVENT_TYPE, + MAX_LENGTH_EVENT_ORIGIN, + MAX_LENGTH_STATE_ENTITY_ID, + MAX_LENGTH_STATE_STATE, +) +from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id +from homeassistant.helpers.typing import UNDEFINED, UndefinedType +import homeassistant.util.dt as dt_util + +# SQLAlchemy Schema +# pylint: disable=invalid-name +Base = declarative_base() + +SCHEMA_VERSION = 25 + +_LOGGER = logging.getLogger(__name__) + +DB_TIMEZONE = "+00:00" + +TABLE_EVENTS = "events" +TABLE_STATES = "states" +TABLE_STATE_ATTRIBUTES = "state_attributes" +TABLE_RECORDER_RUNS = "recorder_runs" +TABLE_SCHEMA_CHANGES = "schema_changes" +TABLE_STATISTICS = "statistics" +TABLE_STATISTICS_META = "statistics_meta" +TABLE_STATISTICS_RUNS = "statistics_runs" +TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" + +ALL_TABLES = [ + TABLE_STATES, + TABLE_EVENTS, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + TABLE_STATISTICS, + TABLE_STATISTICS_META, + TABLE_STATISTICS_RUNS, + TABLE_STATISTICS_SHORT_TERM, +] + +EMPTY_JSON_OBJECT = "{}" + + +DATETIME_TYPE = DateTime(timezone=True).with_variant( + mysql.DATETIME(timezone=True, fsp=6), "mysql" +) +DOUBLE_TYPE = ( + Float() + .with_variant(mysql.DOUBLE(asdecimal=False), "mysql") + .with_variant(oracle.DOUBLE_PRECISION(), "oracle") + .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") +) + + +class Events(Base): # type: ignore[misc,valid-type] + """Event history data.""" + + __table_args__ = ( + # Used for fetching events at a specific time + # see logbook + Index("ix_events_event_type_time_fired", "event_type", "time_fired"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_EVENTS + event_id = Column(Integer, Identity(), primary_key=True) + event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) + event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) + time_fired = Column(DATETIME_TYPE, index=True) + context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event( + event: Event, event_data: UndefinedType | None = UNDEFINED + ) -> Events: + """Create an event database object from a native event.""" + return Events( + event_type=event.event_type, + event_data=JSON_DUMP(event.data) if event_data is UNDEFINED else event_data, + origin=str(event.origin.value), + time_fired=event.time_fired, + context_id=event.context.id, + context_user_id=event.context.user_id, + context_parent_id=event.context.parent_id, + ) + + def to_native(self, validate_entity_id: bool = True) -> Event | None: + """Convert to a native HA Event.""" + context = Context( + id=self.context_id, + user_id=self.context_user_id, + parent_id=self.context_parent_id, + ) + try: + return Event( + self.event_type, + json.loads(self.event_data), + EventOrigin(self.origin), + process_timestamp(self.time_fired), + context=context, + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting to event: %s", self) + return None + + +class States(Base): # type: ignore[misc,valid-type] + """State change history.""" + + __table_args__ = ( + # Used for fetching the state of entities at a specific time + # (get_states in history.py) + Index("ix_states_entity_id_last_updated", "entity_id", "last_updated"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATES + state_id = Column(Integer, Identity(), primary_key=True) + entity_id = Column(String(MAX_LENGTH_STATE_ENTITY_ID)) + state = Column(String(MAX_LENGTH_STATE_STATE)) + attributes = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + event_id = Column( + Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True + ) + last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) + last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) + old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) + attributes_id = Column( + Integer, ForeignKey("state_attributes.attributes_id"), index=True + ) + event = relationship("Events", uselist=False) + old_state = relationship("States", remote_side=[state_id]) + state_attributes = relationship("StateAttributes") + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> States: + """Create object from a state_changed event.""" + entity_id = event.data["entity_id"] + state: State | None = event.data.get("new_state") + dbstate = States(entity_id=entity_id, attributes=None) + + # None state means the state was removed from the state machine + if state is None: + dbstate.state = "" + dbstate.last_changed = event.time_fired + dbstate.last_updated = event.time_fired + else: + dbstate.state = state.state + dbstate.last_changed = state.last_changed + dbstate.last_updated = state.last_updated + + return dbstate + + def to_native(self, validate_entity_id: bool = True) -> State | None: + """Convert to an HA state object.""" + try: + return State( + self.entity_id, + self.state, + # Join the state_attributes table on attributes_id to get the attributes + # for newer states + json.loads(self.attributes) if self.attributes else {}, + process_timestamp(self.last_changed), + process_timestamp(self.last_updated), + # Join the events table on event_id to get the context instead + # as it will always be there for state_changed events + context=Context(id=None), # type: ignore[arg-type] + validate_entity_id=validate_entity_id, + ) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state: %s", self) + return None + + +class StateAttributes(Base): # type: ignore[misc,valid-type] + """State attribute change history.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATE_ATTRIBUTES + attributes_id = Column(Integer, Identity(), primary_key=True) + hash = Column(BigInteger, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_attrs = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> StateAttributes: + """Create object from a state_changed event.""" + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + dbstate = StateAttributes( + shared_attrs="{}" if state is None else JSON_DUMP(state.attributes) + ) + dbstate.hash = StateAttributes.hash_shared_attrs(dbstate.shared_attrs) + return dbstate + + @staticmethod + def shared_attrs_from_event( + event: Event, exclude_attrs_by_domain: dict[str, set[str]] + ) -> str: + """Create shared_attrs from a state_changed event.""" + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + if state is None: + return "{}" + domain = split_entity_id(state.entity_id)[0] + exclude_attrs = ( + exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS + ) + return JSON_DUMP( + {k: v for k, v in state.attributes.items() if k not in exclude_attrs} + ) + + @staticmethod + def hash_shared_attrs(shared_attrs: str) -> int: + """Return the hash of json encoded shared attributes.""" + return cast(int, fnv1a_32(shared_attrs.encode("utf-8"))) + + def to_native(self) -> dict[str, Any]: + """Convert to an HA state object.""" + try: + return cast(dict[str, Any], json.loads(self.shared_attrs)) + except ValueError: + # When json.loads fails + _LOGGER.exception("Error converting row to state attributes: %s", self) + return {} + + +class StatisticResult(TypedDict): + """Statistic result data class. + + Allows multiple datapoints for the same statistic_id. + """ + + meta: StatisticMetaData + stat: StatisticData + + +class StatisticDataBase(TypedDict): + """Mandatory fields for statistic data class.""" + + start: datetime + + +class StatisticData(StatisticDataBase, total=False): + """Statistic data class.""" + + mean: float + min: float + max: float + last_reset: datetime | None + state: float + sum: float + + +class StatisticsBase: + """Statistics base class.""" + + id = Column(Integer, Identity(), primary_key=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + + @declared_attr # type: ignore[misc] + def metadata_id(self) -> Column: + """Define the metadata_id column for sub classes.""" + return Column( + Integer, + ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), + index=True, + ) + + start = Column(DATETIME_TYPE, index=True) + mean = Column(DOUBLE_TYPE) + min = Column(DOUBLE_TYPE) + max = Column(DOUBLE_TYPE) + last_reset = Column(DATETIME_TYPE) + state = Column(DOUBLE_TYPE) + sum = Column(DOUBLE_TYPE) + + @classmethod + def from_stats(cls, metadata_id: int, stats: StatisticData) -> StatisticsBase: + """Create object from a statistics.""" + return cls( # type: ignore[call-arg,misc] + metadata_id=metadata_id, + **stats, + ) + + +class Statistics(Base, StatisticsBase): # type: ignore[misc,valid-type] + """Long term statistics.""" + + duration = timedelta(hours=1) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index("ix_statistics_statistic_id_start", "metadata_id", "start", unique=True), + ) + __tablename__ = TABLE_STATISTICS + + +class StatisticsShortTerm(Base, StatisticsBase): # type: ignore[misc,valid-type] + """Short term statistics.""" + + duration = timedelta(minutes=5) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index( + "ix_statistics_short_term_statistic_id_start", + "metadata_id", + "start", + unique=True, + ), + ) + __tablename__ = TABLE_STATISTICS_SHORT_TERM + + +class StatisticMetaData(TypedDict): + """Statistic meta data class.""" + + has_mean: bool + has_sum: bool + name: str | None + source: str + statistic_id: str + unit_of_measurement: str | None + + +class StatisticsMeta(Base): # type: ignore[misc,valid-type] + """Statistics meta data.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATISTICS_META + id = Column(Integer, Identity(), primary_key=True) + statistic_id = Column(String(255), index=True) + source = Column(String(32)) + unit_of_measurement = Column(String(255)) + has_mean = Column(Boolean) + has_sum = Column(Boolean) + name = Column(String(255)) + + @staticmethod + def from_meta(meta: StatisticMetaData) -> StatisticsMeta: + """Create object from meta data.""" + return StatisticsMeta(**meta) + + +class RecorderRuns(Base): # type: ignore[misc,valid-type] + """Representation of recorder run.""" + + __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) + __tablename__ = TABLE_RECORDER_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True), default=dt_util.utcnow) + end = Column(DateTime(timezone=True)) + closed_incorrect = Column(Boolean, default=False) + created = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + end = ( + f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None + ) + return ( + f"" + ) + + def entity_ids(self, point_in_time: datetime | None = None) -> list[str]: + """Return the entity ids that existed in this run. + + Specify point_in_time if you want to know which existed at that point + in time inside the run. + """ + session = Session.object_session(self) + + assert session is not None, "RecorderRuns need to be persisted" + + query = session.query(distinct(States.entity_id)).filter( + States.last_updated >= self.start + ) + + if point_in_time is not None: + query = query.filter(States.last_updated < point_in_time) + elif self.end is not None: + query = query.filter(States.last_updated < self.end) + + return [row[0] for row in query] + + def to_native(self, validate_entity_id: bool = True) -> RecorderRuns: + """Return self, native format is this model.""" + return self + + +class SchemaChanges(Base): # type: ignore[misc,valid-type] + """Representation of schema version changes.""" + + __tablename__ = TABLE_SCHEMA_CHANGES + change_id = Column(Integer, Identity(), primary_key=True) + schema_version = Column(Integer) + changed = Column(DateTime(timezone=True), default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +class StatisticsRuns(Base): # type: ignore[misc,valid-type] + """Representation of statistics run.""" + + __tablename__ = TABLE_STATISTICS_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DateTime(timezone=True)) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +@overload +def process_timestamp(ts: None) -> None: + ... + + +@overload +def process_timestamp(ts: datetime) -> datetime: + ... + + +def process_timestamp(ts: datetime | None) -> datetime | None: + """Process a timestamp into datetime object.""" + if ts is None: + return None + if ts.tzinfo is None: + return ts.replace(tzinfo=dt_util.UTC) + + return dt_util.as_utc(ts) + + +@overload +def process_timestamp_to_utc_isoformat(ts: None) -> None: + ... + + +@overload +def process_timestamp_to_utc_isoformat(ts: datetime) -> str: + ... + + +def process_timestamp_to_utc_isoformat(ts: datetime | None) -> str | None: + """Process a timestamp into UTC isotime.""" + if ts is None: + return None + if ts.tzinfo == dt_util.UTC: + return ts.isoformat() + if ts.tzinfo is None: + return f"{ts.isoformat()}{DB_TIMEZONE}" + return ts.astimezone(dt_util.UTC).isoformat() + + +class LazyState(State): + """A lazy version of core State.""" + + __slots__ = [ + "_row", + "_attributes", + "_last_changed", + "_last_updated", + "_context", + "_attr_cache", + ] + + def __init__( # pylint: disable=super-init-not-called + self, row: Row, attr_cache: dict[str, dict[str, Any]] | None = None + ) -> None: + """Init the lazy state.""" + self._row = row + self.entity_id: str = self._row.entity_id + self.state = self._row.state or "" + self._attributes: dict[str, Any] | None = None + self._last_changed: datetime | None = None + self._last_updated: datetime | None = None + self._context: Context | None = None + self._attr_cache = attr_cache + + @property # type: ignore[override] + def attributes(self) -> dict[str, Any]: # type: ignore[override] + """State attributes.""" + if self._attributes is None: + source = self._row.shared_attrs or self._row.attributes + if self._attr_cache is not None and ( + attributes := self._attr_cache.get(source) + ): + self._attributes = attributes + return attributes + if source == EMPTY_JSON_OBJECT or source is None: + self._attributes = {} + return self._attributes + try: + self._attributes = json.loads(source) + except ValueError: + # When json.loads fails + _LOGGER.exception( + "Error converting row to state attributes: %s", self._row + ) + self._attributes = {} + if self._attr_cache is not None: + self._attr_cache[source] = self._attributes + return self._attributes + + @attributes.setter + def attributes(self, value: dict[str, Any]) -> None: + """Set attributes.""" + self._attributes = value + + @property # type: ignore[override] + def context(self) -> Context: # type: ignore[override] + """State context.""" + if self._context is None: + self._context = Context(id=None) # type: ignore[arg-type] + return self._context + + @context.setter + def context(self, value: Context) -> None: + """Set context.""" + self._context = value + + @property # type: ignore[override] + def last_changed(self) -> datetime: # type: ignore[override] + """Last changed datetime.""" + if self._last_changed is None: + self._last_changed = process_timestamp(self._row.last_changed) + return self._last_changed + + @last_changed.setter + def last_changed(self, value: datetime) -> None: + """Set last changed datetime.""" + self._last_changed = value + + @property # type: ignore[override] + def last_updated(self) -> datetime: # type: ignore[override] + """Last updated datetime.""" + if self._last_updated is None: + if (last_updated := self._row.last_updated) is not None: + self._last_updated = process_timestamp(last_updated) + else: + self._last_updated = self.last_changed + return self._last_updated + + @last_updated.setter + def last_updated(self, value: datetime) -> None: + """Set last updated datetime.""" + self._last_updated = value + + def as_dict(self) -> dict[str, Any]: # type: ignore[override] + """Return a dict representation of the LazyState. + + Async friendly. + + To be used for JSON serialization. + """ + if self._last_changed is None and self._last_updated is None: + last_changed_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_changed + ) + if ( + self._row.last_updated is None + or self._row.last_changed == self._row.last_updated + ): + last_updated_isoformat = last_changed_isoformat + else: + last_updated_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_updated + ) + else: + last_changed_isoformat = self.last_changed.isoformat() + if self.last_changed == self.last_updated: + last_updated_isoformat = last_changed_isoformat + else: + last_updated_isoformat = self.last_updated.isoformat() + return { + "entity_id": self.entity_id, + "state": self.state, + "attributes": self._attributes or self.attributes, + "last_changed": last_changed_isoformat, + "last_updated": last_updated_isoformat, + } + + def __eq__(self, other: Any) -> bool: + """Return the comparison.""" + return ( + other.__class__ in [self.__class__, State] + and self.entity_id == other.entity_id + and self.state == other.state + and self.attributes == other.attributes + ) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 82444f86a05..0c3a41ab8ef 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -51,6 +51,7 @@ from homeassistant.const import ( STATE_UNLOCKED, ) from homeassistant.core import CoreState, Event, HomeAssistant, callback +from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import async_setup_component, setup_component from homeassistant.util import dt as dt_util @@ -100,9 +101,10 @@ async def test_shutdown_before_startup_finishes( } hass.state = CoreState.not_running - instance = await async_setup_recorder_instance(hass, config) - await instance.async_db_ready - await hass.async_block_till_done() + recorder_helper.async_initialize_recorder(hass) + hass.create_task(async_setup_recorder_instance(hass, config)) + await recorder_helper.async_wait_recorder(hass) + instance = get_instance(hass) session = await hass.async_add_executor_job(instance.get_session) @@ -125,9 +127,11 @@ async def test_canceled_before_startup_finishes( ): """Test recorder shuts down when its startup future is canceled out from under it.""" hass.state = CoreState.not_running - await async_setup_recorder_instance(hass) + recorder_helper.async_initialize_recorder(hass) + hass.create_task(async_setup_recorder_instance(hass)) + await recorder_helper.async_wait_recorder(hass) + instance = get_instance(hass) - await instance.async_db_ready instance._hass_started.cancel() with patch.object(instance, "engine"): await hass.async_block_till_done() @@ -170,7 +174,9 @@ async def test_state_gets_saved_when_set_before_start_event( hass.state = CoreState.not_running - await async_setup_recorder_instance(hass) + recorder_helper.async_initialize_recorder(hass) + hass.create_task(async_setup_recorder_instance(hass)) + await recorder_helper.async_wait_recorder(hass) entity_id = "test.recorder" state = "restoring_from_db" @@ -643,6 +649,7 @@ def test_saving_state_and_removing_entity(hass, hass_recorder): def test_recorder_setup_failure(hass): """Test some exceptions.""" + recorder_helper.async_initialize_recorder(hass) with patch.object(Recorder, "_setup_connection") as setup, patch( "homeassistant.components.recorder.core.time.sleep" ): @@ -657,6 +664,7 @@ def test_recorder_setup_failure(hass): def test_recorder_setup_failure_without_event_listener(hass): """Test recorder setup failure when the event listener is not setup.""" + recorder_helper.async_initialize_recorder(hass) with patch.object(Recorder, "_setup_connection") as setup, patch( "homeassistant.components.recorder.core.time.sleep" ): @@ -985,6 +993,7 @@ def test_compile_missing_statistics(tmpdir): ): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) hass.start() wait_recording_done(hass) @@ -1006,6 +1015,7 @@ def test_compile_missing_statistics(tmpdir): ): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) hass.start() wait_recording_done(hass) @@ -1197,6 +1207,7 @@ def test_service_disable_run_information_recorded(tmpdir): dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) hass.start() wait_recording_done(hass) @@ -1218,6 +1229,7 @@ def test_service_disable_run_information_recorded(tmpdir): hass.stop() hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl}}) hass.start() wait_recording_done(hass) @@ -1246,6 +1258,7 @@ async def test_database_corruption_while_running(hass, tmpdir, caplog): test_db_file = await hass.async_add_executor_job(_create_tmpdir_for_test_db) dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + recorder_helper.async_initialize_recorder(hass) assert await async_setup_component( hass, DOMAIN, {DOMAIN: {CONF_DB_URL: dburl, CONF_COMMIT_INTERVAL: 0}} ) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index a57bd246f8b..bbac01bb5d3 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -27,6 +27,7 @@ from homeassistant.components.recorder.db_schema import ( States, ) from homeassistant.components.recorder.util import session_scope +from homeassistant.helpers import recorder as recorder_helper import homeassistant.util.dt as dt_util from .common import async_wait_recording_done, create_engine_test @@ -53,6 +54,7 @@ async def test_schema_update_calls(hass): "homeassistant.components.recorder.migration._apply_update", wraps=migration._apply_update, ) as update: + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) @@ -74,10 +76,11 @@ async def test_migration_in_progress(hass): """Test that we can check for migration in progress.""" assert recorder.util.async_migration_in_progress(hass) is False - with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True,), patch( + with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.core.create_engine", new=create_engine_test, ): + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) @@ -105,6 +108,7 @@ async def test_database_migration_failed(hass): "homeassistant.components.persistent_notification.dismiss", side_effect=pn.dismiss, ) as mock_dismiss: + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) @@ -136,6 +140,7 @@ async def test_database_migration_encounters_corruption(hass): ), patch( "homeassistant.components.recorder.core.move_away_broken_database" ) as move_away: + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) @@ -165,6 +170,7 @@ async def test_database_migration_encounters_corruption_not_sqlite(hass): "homeassistant.components.persistent_notification.dismiss", side_effect=pn.dismiss, ) as mock_dismiss: + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) @@ -189,6 +195,7 @@ async def test_events_during_migration_are_queued(hass): "homeassistant.components.recorder.core.create_engine", new=create_engine_test, ): + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", @@ -219,6 +226,7 @@ async def test_events_during_migration_queue_exhausted(hass): "homeassistant.components.recorder.core.create_engine", new=create_engine_test, ), patch.object(recorder.core, "MAX_QUEUE_BACKLOG", 1): + recorder_helper.async_initialize_recorder(hass) await async_setup_component( hass, "recorder", @@ -247,8 +255,11 @@ async def test_events_during_migration_queue_exhausted(hass): assert len(db_states) == 2 -@pytest.mark.parametrize("start_version", [0, 16, 18, 22]) -async def test_schema_migrate(hass, start_version): +@pytest.mark.parametrize( + "start_version,live", + [(0, True), (16, True), (18, True), (22, True), (25, True)], +) +async def test_schema_migrate(hass, start_version, live): """Test the full schema migration logic. We're just testing that the logic can execute successfully here without @@ -259,7 +270,8 @@ async def test_schema_migrate(hass, start_version): migration_done = threading.Event() migration_stall = threading.Event() migration_version = None - real_migration = recorder.migration.migrate_schema + real_migrate_schema = recorder.migration.migrate_schema + real_apply_update = recorder.migration._apply_update def _create_engine_test(*args, **kwargs): """Test version of create_engine that initializes with old schema. @@ -284,14 +296,12 @@ async def test_schema_migrate(hass, start_version): start=self.run_history.recording_start, created=dt_util.utcnow() ) - def _instrument_migration(*args): + def _instrument_migrate_schema(*args): """Control migration progress and check results.""" nonlocal migration_done nonlocal migration_version - nonlocal migration_stall - migration_stall.wait() try: - real_migration(*args) + real_migrate_schema(*args) except Exception: migration_done.set() raise @@ -307,6 +317,12 @@ async def test_schema_migrate(hass, start_version): migration_version = res.schema_version migration_done.set() + def _instrument_apply_update(*args): + """Control migration progress.""" + nonlocal migration_stall + migration_stall.wait() + real_apply_update(*args) + with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.core.create_engine", new=_create_engine_test, @@ -316,12 +332,21 @@ async def test_schema_migrate(hass, start_version): autospec=True, ) as setup_run, patch( "homeassistant.components.recorder.migration.migrate_schema", - wraps=_instrument_migration, + wraps=_instrument_migrate_schema, + ), patch( + "homeassistant.components.recorder.migration._apply_update", + wraps=_instrument_apply_update, ): - await async_setup_component( - hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + recorder_helper.async_initialize_recorder(hass) + hass.async_create_task( + async_setup_component( + hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + ) ) + await recorder_helper.async_wait_recorder(hass) + assert recorder.util.async_migration_in_progress(hass) is True + assert recorder.util.async_migration_is_live(hass) == live migration_stall.set() await hass.async_block_till_done() await hass.async_add_executor_job(migration_done.wait) diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index ee76b40a15b..8db4587f1cf 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -31,6 +31,7 @@ from homeassistant.components.recorder.util import session_scope from homeassistant.const import TEMP_CELSIUS from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util @@ -1128,6 +1129,7 @@ def test_delete_metadata_duplicates(caplog, tmpdir): "homeassistant.components.recorder.core.create_engine", new=_create_engine_28 ): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) wait_recording_done(hass) @@ -1158,6 +1160,7 @@ def test_delete_metadata_duplicates(caplog, tmpdir): # Test that the duplicates are removed during migration from schema 28 hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) hass.start() wait_recording_done(hass) @@ -1217,6 +1220,7 @@ def test_delete_metadata_duplicates_many(caplog, tmpdir): "homeassistant.components.recorder.core.create_engine", new=_create_engine_28 ): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) wait_recording_done(hass) @@ -1249,6 +1253,7 @@ def test_delete_metadata_duplicates_many(caplog, tmpdir): # Test that the duplicates are removed during migration from schema 28 hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) hass.start() wait_recording_done(hass) diff --git a/tests/components/recorder/test_statistics_v23_migration.py b/tests/components/recorder/test_statistics_v23_migration.py index 50311a987d6..a7cc2b35e61 100644 --- a/tests/components/recorder/test_statistics_v23_migration.py +++ b/tests/components/recorder/test_statistics_v23_migration.py @@ -16,6 +16,7 @@ from sqlalchemy.orm import Session from homeassistant.components import recorder from homeassistant.components.recorder import SQLITE_URL_PREFIX, statistics from homeassistant.components.recorder.util import session_scope +from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util @@ -179,6 +180,7 @@ def test_delete_duplicates(caplog, tmpdir): recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) wait_recording_done(hass) @@ -206,6 +208,7 @@ def test_delete_duplicates(caplog, tmpdir): # Test that the duplicates are removed during migration from schema 23 hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) hass.start() wait_recording_done(hass) @@ -347,6 +350,7 @@ def test_delete_duplicates_many(caplog, tmpdir): recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) wait_recording_done(hass) @@ -380,6 +384,7 @@ def test_delete_duplicates_many(caplog, tmpdir): # Test that the duplicates are removed during migration from schema 23 hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) hass.start() wait_recording_done(hass) @@ -492,6 +497,7 @@ def test_delete_duplicates_non_identical(caplog, tmpdir): recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) wait_recording_done(hass) @@ -515,6 +521,7 @@ def test_delete_duplicates_non_identical(caplog, tmpdir): # Test that the duplicates are removed during migration from schema 23 hass = get_test_home_assistant() hass.config.config_dir = tmpdir + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) hass.start() wait_recording_done(hass) @@ -592,6 +599,7 @@ def test_delete_duplicates_short_term(caplog, tmpdir): recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION ), patch(CREATE_ENGINE_TARGET, new=_create_engine_test): hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) wait_recording_done(hass) wait_recording_done(hass) @@ -614,6 +622,7 @@ def test_delete_duplicates_short_term(caplog, tmpdir): # Test that the duplicates are removed during migration from schema 23 hass = get_test_home_assistant() hass.config.config_dir = tmpdir + recorder_helper.async_initialize_recorder(hass) setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) hass.start() wait_recording_done(hass) diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 283883030fa..b604cc53e6c 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -15,6 +15,7 @@ from homeassistant.components.recorder.statistics import ( list_statistic_ids, statistics_during_period, ) +from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM @@ -274,6 +275,7 @@ async def test_recorder_info(hass, hass_ws_client, recorder_mock): "backlog": 0, "max_backlog": 40000, "migration_in_progress": False, + "migration_is_live": False, "recording": True, "thread_running": True, } @@ -296,6 +298,7 @@ async def test_recorder_info_bad_recorder_config(hass, hass_ws_client): client = await hass_ws_client() with patch("homeassistant.components.recorder.migration.migrate_schema"): + recorder_helper.async_initialize_recorder(hass) assert not await async_setup_component( hass, recorder.DOMAIN, {recorder.DOMAIN: config} ) @@ -318,7 +321,7 @@ async def test_recorder_info_migration_queue_exhausted(hass, hass_ws_client): migration_done = threading.Event() - real_migration = recorder.migration.migrate_schema + real_migration = recorder.migration._apply_update def stalled_migration(*args): """Make migration stall.""" @@ -334,12 +337,16 @@ async def test_recorder_info_migration_queue_exhausted(hass, hass_ws_client): ), patch.object( recorder.core, "MAX_QUEUE_BACKLOG", 1 ), patch( - "homeassistant.components.recorder.migration.migrate_schema", + "homeassistant.components.recorder.migration._apply_update", wraps=stalled_migration, ): - await async_setup_component( - hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + recorder_helper.async_initialize_recorder(hass) + hass.create_task( + async_setup_component( + hass, "recorder", {"recorder": {"db_url": "sqlite://"}} + ) ) + await recorder_helper.async_wait_recorder(hass) hass.states.async_set("my.entity", "on", {}) await hass.async_block_till_done() diff --git a/tests/conftest.py b/tests/conftest.py index dc5f3069332..9ca29c60658 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,7 +31,7 @@ from homeassistant.components.websocket_api.auth import ( from homeassistant.components.websocket_api.http import URL from homeassistant.const import HASSIO_USER_NAME from homeassistant.core import CoreState, HomeAssistant -from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers import config_entry_oauth2_flow, recorder as recorder_helper from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util, location @@ -790,6 +790,8 @@ async def _async_init_recorder_component(hass, add_config=None): with patch("homeassistant.components.recorder.ALLOW_IN_MEMORY_DB", True), patch( "homeassistant.components.recorder.migration.migrate_schema" ): + if recorder.DOMAIN not in hass.data: + recorder_helper.async_initialize_recorder(hass) assert await async_setup_component( hass, recorder.DOMAIN, {recorder.DOMAIN: config} ) diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index 232d8fb6bbf..06f800af7f3 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -211,6 +211,82 @@ async def test_setup_after_deps_in_stage_1_ignored(hass): assert order == ["cloud", "an_after_dep", "normal_integration"] +@pytest.mark.parametrize("load_registries", [False]) +async def test_setup_frontend_before_recorder(hass): + """Test frontend is setup before recorder.""" + order = [] + + def gen_domain_setup(domain): + async def async_setup(hass, config): + order.append(domain) + return True + + return async_setup + + mock_integration( + hass, + MockModule( + domain="normal_integration", + async_setup=gen_domain_setup("normal_integration"), + partial_manifest={"after_dependencies": ["an_after_dep"]}, + ), + ) + mock_integration( + hass, + MockModule( + domain="an_after_dep", + async_setup=gen_domain_setup("an_after_dep"), + ), + ) + mock_integration( + hass, + MockModule( + domain="frontend", + async_setup=gen_domain_setup("frontend"), + partial_manifest={ + "dependencies": ["http"], + "after_dependencies": ["an_after_dep"], + }, + ), + ) + mock_integration( + hass, + MockModule( + domain="http", + async_setup=gen_domain_setup("http"), + ), + ) + mock_integration( + hass, + MockModule( + domain="recorder", + async_setup=gen_domain_setup("recorder"), + ), + ) + + await bootstrap._async_set_up_integrations( + hass, + { + "frontend": {}, + "http": {}, + "recorder": {}, + "normal_integration": {}, + "an_after_dep": {}, + }, + ) + + assert "frontend" in hass.config.components + assert "normal_integration" in hass.config.components + assert "recorder" in hass.config.components + assert order == [ + "http", + "frontend", + "recorder", + "an_after_dep", + "normal_integration", + ] + + @pytest.mark.parametrize("load_registries", [False]) async def test_setup_after_deps_via_platform(hass): """Test after_dependencies set up via platform.""" From e9697872c8ebb8e0b41c5e7bf78dab6d3b58afcf Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 22 Jul 2022 16:02:10 +0200 Subject: [PATCH 2785/3516] Fix small homekit type error (#75617) --- homeassistant/components/homekit/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 5d7e647e466..6852ff4fa9f 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -8,7 +8,6 @@ import ipaddress import logging import os from typing import Any, cast -from uuid import UUID from aiohttp import web from pyhap.const import STANDALONE_AID @@ -510,7 +509,7 @@ class HomeKit: self.bridge: HomeBridge | None = None - def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: UUID) -> None: + def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> None: """Set up bridge and accessory driver.""" persist_file = get_persist_fullpath_for_entry_id(self.hass, self._entry_id) From 148f96351052b0a4ba31e7d15dad16d7639e1ceb Mon Sep 17 00:00:00 2001 From: "Diogo F. Andrade Murteira" Date: Fri, 22 Jul 2022 17:03:02 +0100 Subject: [PATCH 2786/3516] Add Switchbot hygrometers (#75325) * Switchbot add support for hygrometers * Update CODEOWNERS * Improve debug * Remove redundant mention to temp unit * Adopt FlowResultType * Modify SwitchBot data within coordinator * Increase logging for switchbot sensor * Revert "Increase logging for switchbot sensor" This reverts commit d8b377429c562fc7044a3c98a6e976e4cd71847e. Co-authored-by: J. Nick Koston --- CODEOWNERS | 4 +-- .../components/switchbot/__init__.py | 2 ++ .../components/switchbot/config_flow.py | 2 ++ homeassistant/components/switchbot/const.py | 7 +++- .../components/switchbot/coordinator.py | 13 ++++++- .../components/switchbot/manifest.json | 2 +- homeassistant/components/switchbot/sensor.py | 11 ++++++ tests/components/switchbot/__init__.py | 6 ++++ tests/components/switchbot/conftest.py | 15 ++++++++ .../components/switchbot/test_config_flow.py | 35 ++++++++++++++++++- 10 files changed, 91 insertions(+), 6 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 792a6302b79..3b38b6e1a5a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1038,8 +1038,8 @@ build.json @home-assistant/supervisor /tests/components/switch/ @home-assistant/core /homeassistant/components/switch_as_x/ @home-assistant/core /tests/components/switch_as_x/ @home-assistant/core -/homeassistant/components/switchbot/ @danielhiversen @RenierM26 -/tests/components/switchbot/ @danielhiversen @RenierM26 +/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas +/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas /homeassistant/components/switcher_kis/ @tomerfi @thecode /tests/components/switcher_kis/ @tomerfi @thecode /homeassistant/components/switchmate/ @danielhiversen diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 0a3d01f3382..d4418685cff 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -9,6 +9,7 @@ from homeassistant.core import HomeAssistant from .const import ( ATTR_BOT, ATTR_CURTAIN, + ATTR_HYGROMETER, COMMON_OPTIONS, CONF_RETRY_COUNT, CONF_RETRY_TIMEOUT, @@ -26,6 +27,7 @@ from .coordinator import SwitchbotDataUpdateCoordinator PLATFORMS_BY_TYPE = { ATTR_BOT: [Platform.SWITCH, Platform.SENSOR], ATTR_CURTAIN: [Platform.COVER, Platform.BINARY_SENSOR, Platform.SENSOR], + ATTR_HYGROMETER: [Platform.SENSOR], } diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 362f3b01ae7..b35ba052d0a 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -87,6 +87,8 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): try: self._discovered_devices = await self._get_switchbots() + for device in self._discovered_devices.values(): + _LOGGER.debug("Found %s", device) except NotConnectedError: return self.async_abort(reason="cannot_connect") diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index b1587e97c10..a363c030eb1 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -5,8 +5,13 @@ MANUFACTURER = "switchbot" # Config Attributes ATTR_BOT = "bot" ATTR_CURTAIN = "curtain" +ATTR_HYGROMETER = "hygrometer" DEFAULT_NAME = "Switchbot" -SUPPORTED_MODEL_TYPES = {"WoHand": ATTR_BOT, "WoCurtain": ATTR_CURTAIN} +SUPPORTED_MODEL_TYPES = { + "WoHand": ATTR_BOT, + "WoCurtain": ATTR_CURTAIN, + "WoSensorTH": ATTR_HYGROMETER, +} # Config Defaults DEFAULT_RETRY_COUNT = 3 diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index e8e2e240dc6..4a4831f2cdb 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -14,6 +14,14 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +def flatten_sensors_data(sensor): + """Deconstruct SwitchBot library temp object C/Fº readings from dictionary.""" + if "temp" in sensor["data"]: + sensor["data"]["temperature"] = sensor["data"]["temp"]["c"] + + return sensor + + class SwitchbotDataUpdateCoordinator(DataUpdateCoordinator): """Class to manage fetching switchbot data.""" @@ -47,4 +55,7 @@ class SwitchbotDataUpdateCoordinator(DataUpdateCoordinator): if not switchbot_data: raise UpdateFailed("Unable to fetch switchbot services data") - return switchbot_data + return { + identifier: flatten_sensors_data(sensor) + for identifier, sensor in switchbot_data.items() + } diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index ccce534c6df..76b43628ab3 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "requirements": ["PySwitchbot==0.14.1"], "config_flow": true, - "codeowners": ["@danielhiversen", "@RenierM26"], + "codeowners": ["@danielhiversen", "@RenierM26", "@murtas"], "bluetooth": [{ "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" }], "iot_class": "local_polling", "loggers": ["switchbot"] diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index 759a504d19a..0bc7fe8a3b2 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_NAME, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady @@ -43,6 +44,16 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { native_unit_of_measurement="Level", device_class=SensorDeviceClass.ILLUMINANCE, ), + "humidity": SensorEntityDescription( + key="humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + ), + "temperature": SensorEntityDescription( + key="temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ), } diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index 376406ac50c..8f1501bfa9a 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -26,6 +26,12 @@ USER_INPUT_CURTAIN = { CONF_MAC: "e7:89:43:90:90:90", } +USER_INPUT_SENSOR = { + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_MAC: "c0:ce:b0:d4:26:be", +} + USER_INPUT_UNSUPPORTED_DEVICE = { CONF_NAME: "test-name", CONF_PASSWORD: "test-password", diff --git a/tests/components/switchbot/conftest.py b/tests/components/switchbot/conftest.py index 550aeb08082..2e6421f22a4 100644 --- a/tests/components/switchbot/conftest.py +++ b/tests/components/switchbot/conftest.py @@ -72,6 +72,19 @@ class MocGetSwitchbotDevices: }, "modelName": "WoCurtain", } + self._sensor_data = { + "mac_address": "c0:ce:b0:d4:26:be", + "isEncrypted": False, + "data": { + "temp": {"c": 21.6, "f": 70.88}, + "fahrenheit": False, + "humidity": 73, + "battery": 100, + "rssi": -58, + }, + "model": "T", + "modelName": "WoSensorTH", + } self._unsupported_device = { "mac_address": "test", "isEncrypted": False, @@ -97,6 +110,8 @@ class MocGetSwitchbotDevices: return self._unsupported_device if mac == "e7:89:43:90:90:90": return self._curtain_all_services_data + if mac == "c0:ce:b0:d4:26:be": + return self._sensor_data return None diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index 814e79cf591..aa1adb3a16e 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -11,7 +11,13 @@ from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE from homeassistant.data_entry_flow import FlowResultType -from . import USER_INPUT, USER_INPUT_CURTAIN, init_integration, patch_async_setup_entry +from . import ( + USER_INPUT, + USER_INPUT_CURTAIN, + USER_INPUT_SENSOR, + init_integration, + patch_async_setup_entry, +) DOMAIN = "switchbot" @@ -71,6 +77,33 @@ async def test_user_form_valid_mac(hass): assert len(mock_setup_entry.mock_calls) == 1 + # test sensor device creation. + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT_SENSOR, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "test-name" + assert result["data"] == { + CONF_MAC: "c0:ce:b0:d4:26:be", + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_SENSOR_TYPE: "hygrometer", + } + + assert len(mock_setup_entry.mock_calls) == 1 + # tests abort if no unconfigured devices are found. result = await hass.config_entries.flow.async_init( From c05905ebda3921874ce81683b7cbf4e5ce0edfbb Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 22 Jul 2022 19:09:02 +0200 Subject: [PATCH 2787/3516] Remove callback decorator from coroutine functions (#75626) * Remove callback decorator from coroutine functions * Remove some more callback decorators --- homeassistant/components/fitbit/sensor.py | 3 +-- homeassistant/components/fritz/common.py | 1 - homeassistant/components/generic_hygrostat/humidifier.py | 3 --- homeassistant/components/group/__init__.py | 1 - homeassistant/components/motioneye/__init__.py | 1 - homeassistant/components/mqtt/client.py | 1 - homeassistant/components/mqtt/tag.py | 3 +-- homeassistant/components/network/network.py | 1 - homeassistant/components/philips_js/__init__.py | 1 - homeassistant/components/switcher_kis/__init__.py | 1 - homeassistant/components/zwave_js/device_condition.py | 1 - tests/components/logbook/test_websocket_api.py | 3 +-- 12 files changed, 3 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 48b05482dec..3165843a23d 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -25,7 +25,7 @@ from homeassistant.const import ( CONF_CLIENT_SECRET, CONF_UNIT_SYSTEM, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.icon import icon_for_battery_level @@ -279,7 +279,6 @@ class FitbitAuthCallbackView(HomeAssistantView): self.add_entities = add_entities self.oauth = oauth - @callback async def get(self, request: Request) -> str: """Finish OAuth callback request.""" hass: HomeAssistant = request.app["hass"] diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 4a01367cc20..d748bdcf7dd 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -240,7 +240,6 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator): self.device_conn_type, "GetInfo" ).get("NewEnable") - @callback async def _async_update_data(self) -> None: """Update FritzboxTools data.""" try: diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index fa8b05b5ef8..8072c76ea07 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -173,7 +173,6 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): if self._keep_alive: async_track_time_interval(self.hass, self._async_operate, self._keep_alive) - @callback async def _async_startup(event): """Init on startup.""" sensor_state = self.hass.states.get(self._sensor_entity_id) @@ -309,7 +308,6 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): # Get default humidity from super class return super().max_humidity - @callback async def _async_sensor_changed(self, entity_id, old_state, new_state): """Handle ambient humidity changes.""" if new_state is None: @@ -328,7 +326,6 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): await self._async_operate() await self.async_update_ha_state() - @callback async def _async_sensor_not_responding(self, now=None): """Handle sensor stale event.""" diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 943b15dd870..04bc109d15b 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -574,7 +574,6 @@ class Group(Entity): return group @staticmethod - @callback async def async_create_group( hass: HomeAssistant, name: str, diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 462f5ef03d2..7c87dda1bd2 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -328,7 +328,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, DOMAIN, "motionEye", entry.data[CONF_WEBHOOK_ID], handle_webhook ) - @callback async def async_update_data() -> dict[str, Any] | None: try: return await client.async_get_cameras() diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 6e6f67e4e7a..81142fadb87 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -343,7 +343,6 @@ class MQTT: self.init_client() - @callback async def async_stop_mqtt(_event: Event): """Stop MQTT component.""" await self.async_disconnect() diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 9452d5fc259..dc4cc0e109d 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, CONF_PLATFORM, CONF_VALUE_TEMPLATE -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -128,7 +128,6 @@ class MQTTTagScanner(MqttDiscoveryDeviceUpdate): async def subscribe_topics(self) -> None: """Subscribe to MQTT topics.""" - @callback async def tag_scanned(msg: ReceiveMessage) -> None: tag_id = self._value_template(msg.payload, "").strip() if not tag_id: # No output from template, ignore diff --git a/homeassistant/components/network/network.py b/homeassistant/components/network/network.py index e9542ec2d54..0b90023bfd4 100644 --- a/homeassistant/components/network/network.py +++ b/homeassistant/components/network/network.py @@ -22,7 +22,6 @@ _LOGGER = logging.getLogger(__name__) @singleton(DATA_NETWORK) -@callback async def async_get_network(hass: HomeAssistant) -> Network: """Get network singleton.""" network = Network(hass) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 40e803e4b35..9e574e69f90 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -187,7 +187,6 @@ class PhilipsTVDataUpdateCoordinator(DataUpdateCoordinator[None]): super()._unschedule_refresh() self._async_notify_stop() - @callback async def _async_update_data(self): """Fetch the latest data from the source.""" try: diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index e0f328e9312..31273dce23d 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -106,7 +106,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await async_start_bridge(hass, on_device_data_callback) - @callback async def stop_bridge(event: Event) -> None: await async_stop_bridge(hass) diff --git a/homeassistant/components/zwave_js/device_condition.py b/homeassistant/components/zwave_js/device_condition.py index 7775995437a..c42b5af71c4 100644 --- a/homeassistant/components/zwave_js/device_condition.py +++ b/homeassistant/components/zwave_js/device_condition.py @@ -196,7 +196,6 @@ def async_condition_from_config( raise HomeAssistantError(f"Unhandled condition type {condition_type}") -@callback async def async_get_condition_capabilities( hass: HomeAssistant, config: ConfigType ) -> dict[str, vol.Schema]: diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 6c4908a2ad5..ec4f2183a9a 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -27,7 +27,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import Event, HomeAssistant, State from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS from homeassistant.setup import async_setup_component @@ -51,7 +51,6 @@ def set_utc(hass): hass.config.set_time_zone("UTC") -@callback async def _async_mock_logbook_platform(hass: HomeAssistant) -> None: class MockLogbookPlatform: """Mock a logbook platform.""" From 20b6c4c48e9fc1b33eefed1844afd64ae19ab064 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 Jul 2022 12:37:25 -0500 Subject: [PATCH 2788/3516] Fix recorder hanging at start (#75627) --- homeassistant/components/recorder/core.py | 27 +++++++++++-------- .../components/recorder/migration.py | 2 +- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 92e10b47126..30ece9e98a5 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -436,7 +436,7 @@ class Recorder(threading.Thread): self.async_db_connected.set_result(True) @callback - def async_set_recorder_ready(self) -> None: + def async_set_db_ready(self) -> None: """Database live and ready for use. Called after non-live migration steps are finished. @@ -577,14 +577,19 @@ class Recorder(threading.Thread): self.hass.add_job(self.async_connection_success) - # If shutdown happened before Home Assistant finished starting - if self._wait_startup_or_shutdown() is SHUTDOWN_TASK: - self.migration_in_progress = False - # Make sure we cleanly close the run if - # we restart before startup finishes - self._shutdown() - self.hass.add_job(self.async_set_recorder_ready) - return + if self.migration_is_live or schema_is_current: + # If the migrate is live or the schema is current, we need to + # wait for startup to complete. If its not live, we need to continue + # on. + self.hass.add_job(self.async_set_db_ready) + # If shutdown happened before Home Assistant finished starting + if self._wait_startup_or_shutdown() is SHUTDOWN_TASK: + self.migration_in_progress = False + # Make sure we cleanly close the run if + # we restart before startup finishes + self._shutdown() + self.hass.add_job(self.async_set_db_ready) + return # We wait to start the migration until startup has finished # since it can be cpu intensive and we do not want it to compete @@ -604,11 +609,11 @@ class Recorder(threading.Thread): "Database Migration Failed", "recorder_database_migration", ) - self.hass.add_job(self.async_set_recorder_ready) + self.hass.add_job(self.async_set_db_ready) self._shutdown() return - self.hass.add_job(self.async_set_recorder_ready) + self.hass.add_job(self.async_set_db_ready) _LOGGER.debug("Recorder processing the queue") self.hass.add_job(self._async_set_recorder_ready_migration_done) diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index de6fd8f01fe..6e4a67c9da5 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -99,7 +99,7 @@ def migrate_schema( if live_migration(version) and not db_ready: db_ready = True instance.migration_is_live = True - hass.add_job(instance.async_set_recorder_ready) + hass.add_job(instance.async_set_db_ready) new_version = version + 1 _LOGGER.info("Upgrading recorder db schema to version %s", new_version) _apply_update(hass, engine, session_maker, new_version, current_version) From 38bccadaa6c2669d34731e84c2cdb6cd2f8d3e1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 Jul 2022 13:19:53 -0500 Subject: [PATCH 2789/3516] Add support for setting up and removing bluetooth in the UI (#75600) Co-authored-by: Paulus Schoutsen --- .../components/bluetooth/__init__.py | 135 +++- .../components/bluetooth/config_flow.py | 37 + homeassistant/components/bluetooth/const.py | 1 + .../components/bluetooth/manifest.json | 3 +- homeassistant/components/bluetooth/models.py | 15 +- .../components/bluetooth/strings.json | 6 + .../components/bluetooth/translations/en.json | 6 + homeassistant/components/bluetooth/usage.py | 13 +- .../components/default_config/manifest.json | 1 + homeassistant/generated/config_flows.py | 1 + homeassistant/package_constraints.txt | 2 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + .../components/bluetooth/test_config_flow.py | 106 +++ tests/components/bluetooth/test_init.py | 694 +++++++++++------- .../test_passive_update_coordinator.py | 12 +- tests/components/bluetooth/test_usage.py | 21 +- .../bluetooth_le_tracker/conftest.py | 7 +- tests/components/default_config/test_init.py | 2 +- tests/components/inkbird/conftest.py | 2 +- tests/components/sensorpush/conftest.py | 4 +- tests/conftest.py | 20 +- 22 files changed, 755 insertions(+), 339 deletions(-) create mode 100644 homeassistant/components/bluetooth/config_flow.py create mode 100644 tests/components/bluetooth/test_config_flow.py diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 46d1e5e8332..3c7f2393257 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -7,6 +7,7 @@ from datetime import datetime, timedelta from enum import Enum import fnmatch import logging +import platform from typing import Final, TypedDict, Union from bleak import BleakError @@ -35,7 +36,7 @@ from homeassistant.loader import ( from . import models from .const import DOMAIN from .models import HaBleakScanner -from .usage import install_multiple_bleak_catcher +from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher _LOGGER = logging.getLogger(__name__) @@ -115,6 +116,15 @@ BluetoothCallback = Callable[ ] +@hass_callback +def async_get_scanner(hass: HomeAssistant) -> HaBleakScanner: + """Return a HaBleakScanner.""" + if DOMAIN not in hass.data: + raise RuntimeError("Bluetooth integration not loaded") + manager: BluetoothManager = hass.data[DOMAIN] + return manager.async_get_scanner() + + @hass_callback def async_discovered_service_info( hass: HomeAssistant, @@ -178,14 +188,62 @@ def async_track_unavailable( return manager.async_track_unavailable(callback, address) +async def _async_has_bluetooth_adapter() -> bool: + """Return if the device has a bluetooth adapter.""" + if platform.system() == "Darwin": # CoreBluetooth is built in on MacOS hardware + return True + if platform.system() == "Windows": # We don't have a good way to detect on windows + return False + from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel + get_bluetooth_adapters, + ) + + return bool(await get_bluetooth_adapters()) + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the bluetooth integration.""" integration_matchers = await async_get_bluetooth(hass) - bluetooth_discovery = BluetoothManager( - hass, integration_matchers, BluetoothScanningMode.PASSIVE - ) - await bluetooth_discovery.async_setup() - hass.data[DOMAIN] = bluetooth_discovery + manager = BluetoothManager(hass, integration_matchers) + manager.async_setup() + hass.data[DOMAIN] = manager + # The config entry is responsible for starting the manager + # if its enabled + + if hass.config_entries.async_entries(DOMAIN): + return True + if DOMAIN in config: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} + ) + ) + elif await _async_has_bluetooth_adapter(): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={}, + ) + ) + return True + + +async def async_setup_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: + """Set up the bluetooth integration from a config entry.""" + manager: BluetoothManager = hass.data[DOMAIN] + await manager.async_start(BluetoothScanningMode.ACTIVE) + return True + + +async def async_unload_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry +) -> bool: + """Unload a config entry.""" + manager: BluetoothManager = hass.data[DOMAIN] + await manager.async_stop() return True @@ -241,11 +299,9 @@ class BluetoothManager: self, hass: HomeAssistant, integration_matchers: list[BluetoothMatcher], - scanning_mode: BluetoothScanningMode, ) -> None: """Init bluetooth discovery.""" self.hass = hass - self.scanning_mode = scanning_mode self._integration_matchers = integration_matchers self.scanner: HaBleakScanner | None = None self._cancel_device_detected: CALLBACK_TYPE | None = None @@ -258,19 +314,27 @@ class BluetoothManager: # an LRU to avoid memory issues. self._matched: LRU = LRU(MAX_REMEMBER_ADDRESSES) - async def async_setup(self) -> None: + @hass_callback + def async_setup(self) -> None: + """Set up the bluetooth manager.""" + models.HA_BLEAK_SCANNER = self.scanner = HaBleakScanner() + + @hass_callback + def async_get_scanner(self) -> HaBleakScanner: + """Get the scanner.""" + assert self.scanner is not None + return self.scanner + + async def async_start(self, scanning_mode: BluetoothScanningMode) -> None: """Set up BT Discovery.""" + assert self.scanner is not None try: - self.scanner = HaBleakScanner( - scanning_mode=SCANNING_MODE_TO_BLEAK[self.scanning_mode] + self.scanner.async_setup( + scanning_mode=SCANNING_MODE_TO_BLEAK[scanning_mode] ) except (FileNotFoundError, BleakError) as ex: - _LOGGER.warning( - "Could not create bluetooth scanner (is bluetooth present and enabled?): %s", - ex, - ) - return - install_multiple_bleak_catcher(self.scanner) + raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex + install_multiple_bleak_catcher() self.async_setup_unavailable_tracking() # We have to start it right away as some integrations might # need it straight away. @@ -279,8 +343,11 @@ class BluetoothManager: self._cancel_device_detected = self.scanner.async_register_callback( self._device_detected, {} ) + try: + await self.scanner.start() + except (FileNotFoundError, BleakError) as ex: + raise RuntimeError(f"Failed to start Bluetooth: {ex}") from ex self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) - await self.scanner.start() @hass_callback def async_setup_unavailable_tracking(self) -> None: @@ -289,8 +356,8 @@ class BluetoothManager: @hass_callback def _async_check_unavailable(now: datetime) -> None: """Watch for unavailable devices.""" - assert models.HA_BLEAK_SCANNER is not None - scanner = models.HA_BLEAK_SCANNER + scanner = self.scanner + assert scanner is not None history = set(scanner.history) active = {device.address for device in scanner.discovered_devices} disappeared = history.difference(active) @@ -406,8 +473,8 @@ class BluetoothManager: if ( matcher and (address := matcher.get(ADDRESS)) - and models.HA_BLEAK_SCANNER - and (device_adv_data := models.HA_BLEAK_SCANNER.history.get(address)) + and self.scanner + and (device_adv_data := self.scanner.history.get(address)) ): try: callback( @@ -424,31 +491,25 @@ class BluetoothManager: @hass_callback def async_ble_device_from_address(self, address: str) -> BLEDevice | None: """Return the BLEDevice if present.""" - if models.HA_BLEAK_SCANNER and ( - ble_adv := models.HA_BLEAK_SCANNER.history.get(address) - ): + if self.scanner and (ble_adv := self.scanner.history.get(address)): return ble_adv[0] return None @hass_callback def async_address_present(self, address: str) -> bool: """Return if the address is present.""" - return bool( - models.HA_BLEAK_SCANNER and address in models.HA_BLEAK_SCANNER.history - ) + return bool(self.scanner and address in self.scanner.history) @hass_callback def async_discovered_service_info(self) -> list[BluetoothServiceInfoBleak]: """Return if the address is present.""" - if models.HA_BLEAK_SCANNER: - history = models.HA_BLEAK_SCANNER.history - return [ - BluetoothServiceInfoBleak.from_advertisement(*device_adv, SOURCE_LOCAL) - for device_adv in history.values() - ] - return [] + assert self.scanner is not None + return [ + BluetoothServiceInfoBleak.from_advertisement(*device_adv, SOURCE_LOCAL) + for device_adv in self.scanner.history.values() + ] - async def async_stop(self, event: Event) -> None: + async def async_stop(self, event: Event | None = None) -> None: """Stop bluetooth discovery.""" if self._cancel_device_detected: self._cancel_device_detected() @@ -458,4 +519,4 @@ class BluetoothManager: self._cancel_unavailable_tracking = None if self.scanner: await self.scanner.stop() - models.HA_BLEAK_SCANNER = None + uninstall_multiple_bleak_catcher() diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py new file mode 100644 index 00000000000..2193170810f --- /dev/null +++ b/homeassistant/components/bluetooth/config_flow.py @@ -0,0 +1,37 @@ +"""Config flow to configure the Bluetooth integration.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult + +from .const import DEFAULT_NAME, DOMAIN + + +class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): + """Config flow for Bluetooth.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + return await self.async_step_enable_bluetooth() + + async def async_step_enable_bluetooth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user or import.""" + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + if user_input is not None: + return self.async_create_entry(title=DEFAULT_NAME, data={}) + + return self.async_show_form(step_id="enable_bluetooth") + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Handle import from configuration.yaml.""" + return await self.async_step_enable_bluetooth(user_input) diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index ca5777ccdc2..1e577f6064a 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -1,3 +1,4 @@ """Constants for the Bluetooth integration.""" DOMAIN = "bluetooth" +DEFAULT_NAME = "Bluetooth" diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 0cc11ee14b3..551bb1c3733 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,8 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.14.3"], + "requirements": ["bleak==0.14.3", "bluetooth-adapters==0.1.1"], "codeowners": ["@bdraco"], + "config_flow": true, "iot_class": "local_push" } diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index ffb0ad107ec..e1d15c27243 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -48,13 +48,24 @@ def _dispatch_callback( class HaBleakScanner(BleakScanner): # type: ignore[misc] """BleakScanner that cannot be stopped.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: + def __init__( # pylint: disable=super-init-not-called + self, *args: Any, **kwargs: Any + ) -> None: """Initialize the BleakScanner.""" self._callbacks: list[ tuple[AdvertisementDataCallback, dict[str, set[str]]] ] = [] self.history: dict[str, tuple[BLEDevice, AdvertisementData]] = {} - super().__init__(*args, **kwargs) + # Init called later in async_setup if we are enabling the scanner + # since init has side effects that can throw exceptions + self._setup = False + + @hass_callback + def async_setup(self, *args: Any, **kwargs: Any) -> None: + """Deferred setup of the BleakScanner since __init__ has side effects.""" + if not self._setup: + super().__init__(*args, **kwargs) + self._setup = True @hass_callback def async_register_callback( diff --git a/homeassistant/components/bluetooth/strings.json b/homeassistant/components/bluetooth/strings.json index 925e9c512cc..328a001ad96 100644 --- a/homeassistant/components/bluetooth/strings.json +++ b/homeassistant/components/bluetooth/strings.json @@ -2,6 +2,9 @@ "config": { "flow_title": "{name}", "step": { + "enable_bluetooth": { + "description": "Do you want to setup Bluetooth?" + }, "user": { "description": "Choose a device to setup", "data": { @@ -11,6 +14,9 @@ "bluetooth_confirm": { "description": "Do you want to setup {name}?" } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } } } diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json index f75dc2603db..85019bdd689 100644 --- a/homeassistant/components/bluetooth/translations/en.json +++ b/homeassistant/components/bluetooth/translations/en.json @@ -1,10 +1,16 @@ { "config": { + "abort": { + "already_configured": "Service is already configured" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, + "enable_bluetooth": { + "description": "Do you want to setup Bluetooth?" + }, "user": { "data": { "address": "Device" diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py index e305576f97f..da5d062a36f 100644 --- a/homeassistant/components/bluetooth/usage.py +++ b/homeassistant/components/bluetooth/usage.py @@ -3,11 +3,16 @@ from __future__ import annotations import bleak -from . import models -from .models import HaBleakScanner, HaBleakScannerWrapper +from .models import HaBleakScannerWrapper + +ORIGINAL_BLEAK_SCANNER = bleak.BleakScanner -def install_multiple_bleak_catcher(hass_bleak_scanner: HaBleakScanner) -> None: +def install_multiple_bleak_catcher() -> None: """Wrap the bleak classes to return the shared instance if multiple instances are detected.""" - models.HA_BLEAK_SCANNER = hass_bleak_scanner bleak.BleakScanner = HaBleakScannerWrapper + + +def uninstall_multiple_bleak_catcher() -> None: + """Unwrap the bleak classes.""" + bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 1742092cc70..3cb9e60a278 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -5,6 +5,7 @@ "dependencies": [ "application_credentials", "automation", + "bluetooth", "cloud", "counter", "dhcp", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 33e34035b34..2c7732fee8d 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -47,6 +47,7 @@ FLOWS = { "balboa", "blebox", "blink", + "bluetooth", "bmw_connected_drive", "bond", "bosch_shc", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d787cec62f2..ad640ff596b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,6 +10,8 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 +bleak==0.14.3 +bluetooth-adapters==0.1.1 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==36.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index c17e031f07a..15c62c63c2f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -424,6 +424,9 @@ blockchain==1.4.4 # homeassistant.components.zengge # bluepy==1.3.0 +# homeassistant.components.bluetooth +bluetooth-adapters==0.1.1 + # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8745afe1e64..742d6eb0f3a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -334,6 +334,9 @@ blebox_uniapi==2.0.2 # homeassistant.components.blink blinkpy==0.19.0 +# homeassistant.components.bluetooth +bluetooth-adapters==0.1.1 + # homeassistant.components.bond bond-async==0.1.22 diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py new file mode 100644 index 00000000000..550f5a583d7 --- /dev/null +++ b/tests/components/bluetooth/test_config_flow.py @@ -0,0 +1,106 @@ +"""Test the bluetooth config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.bluetooth.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_async_step_user(hass): + """Test setting up manually.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "enable_bluetooth" + with patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Bluetooth" + assert result2["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_async_step_user_only_allows_one(hass): + """Test setting up manually with an existing entry.""" + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_integration_discovery(hass): + """Test setting up from integration discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "enable_bluetooth" + with patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Bluetooth" + assert result2["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_async_step_integration_discovery_already_exists(hass): + """Test setting up from integration discovery when an entry already exists.""" + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_import(hass): + """Test setting up from integration discovery.""" + with patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Bluetooth" + assert result["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_async_step_import_already_exists(hass): + """Test setting up from yaml when an entry already exists.""" + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index e76ad559305..8aef5f3ddbb 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock, patch from bleak import BleakError from bleak.backends.scanner import AdvertisementData, BLEDevice +import pytest from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( @@ -11,6 +12,7 @@ from homeassistant.components.bluetooth import ( UNAVAILABLE_TRACK_SECONDS, BluetoothChange, BluetoothServiceInfo, + async_get_scanner, async_track_unavailable, models, ) @@ -19,10 +21,10 @@ from homeassistant.core import callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed -async def test_setup_and_stop(hass, mock_bleak_scanner_start): +async def test_setup_and_stop(hass, mock_bleak_scanner_start, enable_bluetooth): """Test we and setup and stop the scanner.""" mock_bt = [ {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} @@ -47,33 +49,57 @@ async def test_setup_and_stop_no_bluetooth(hass, caplog): {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} ] with patch( - "homeassistant.components.bluetooth.HaBleakScanner", side_effect=BleakError + "homeassistant.components.bluetooth.HaBleakScanner.async_setup", + side_effect=BleakError, ) as mock_ha_bleak_scanner, patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt - ), patch.object( - hass.config_entries.flow, "async_init" ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) + await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() assert len(mock_ha_bleak_scanner.mock_calls) == 1 - assert "Could not create bluetooth scanner" in caplog.text + assert "Failed to initialize Bluetooth" in caplog.text + + +async def test_setup_and_stop_broken_bluetooth(hass, caplog): + """Test we fail gracefully when bluetooth/dbus is broken.""" + mock_bt = [ + {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} + ] + + with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=BleakError, + ), patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "Failed to start Bluetooth" in caplog.text + assert len(bluetooth.async_discovered_service_info(hass)) == 0 async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): """Test we fail gracefully when asking for discovered devices and there is no blueooth.""" mock_bt = [] with patch( - "homeassistant.components.bluetooth.HaBleakScanner", side_effect=BleakError - ) as mock_ha_bleak_scanner, patch( + "homeassistant.components.bluetooth.HaBleakScanner.async_setup", + side_effect=FileNotFoundError, + ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt - ), patch.object( - hass.config_entries.flow, "async_init" ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} @@ -83,13 +109,14 @@ async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - assert len(mock_ha_bleak_scanner.mock_calls) == 1 - assert "Could not create bluetooth scanner" in caplog.text + assert "Failed to initialize Bluetooth" in caplog.text assert not bluetooth.async_discovered_service_info(hass) assert not bluetooth.async_address_present(hass, "aa:bb:bb:dd:ee:ff") -async def test_discovery_match_by_service_uuid(hass, mock_bleak_scanner_start): +async def test_discovery_match_by_service_uuid( + hass, mock_bleak_scanner_start, enable_bluetooth +): """Test bluetooth discovery match by service_uuid.""" mock_bt = [ {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} @@ -108,7 +135,7 @@ async def test_discovery_match_by_service_uuid(hass, mock_bleak_scanner_start): wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv) + async_get_scanner(hass)._callback(wrong_device, wrong_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -118,7 +145,7 @@ async def test_discovery_match_by_service_uuid(hass, mock_bleak_scanner_start): local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -130,10 +157,13 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): mock_bt = [{"domain": "switchbot", "local_name": "wohand"}] with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -142,7 +172,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv) + async_get_scanner(hass)._callback(wrong_device, wrong_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -150,7 +180,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -170,10 +200,13 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( ] with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt - ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -186,7 +219,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( manufacturer_data={76: b"\x06\x02\x03\x99"}, ) - models.HA_BLEAK_SCANNER._callback(hkc_device, hkc_adv) + async_get_scanner(hass)._callback(hkc_device, hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -194,7 +227,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( mock_config_flow.reset_mock() # 2nd discovery should not generate another flow - models.HA_BLEAK_SCANNER._callback(hkc_device, hkc_adv) + async_get_scanner(hass)._callback(hkc_device, hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -205,7 +238,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( local_name="lock", service_uuids=[], manufacturer_data={76: b"\x02"} ) - models.HA_BLEAK_SCANNER._callback(not_hkc_device, not_hkc_adv) + async_get_scanner(hass)._callback(not_hkc_device, not_hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -214,14 +247,14 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( local_name="lock", service_uuids=[], manufacturer_data={21: b"\x02"} ) - models.HA_BLEAK_SCANNER._callback(not_apple_device, not_apple_adv) + async_get_scanner(hass)._callback(not_apple_device, not_apple_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): - """Test the async_discovered_device_api.""" + """Test the async_discovered_device API.""" mock_bt = [] with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -231,84 +264,86 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): ): assert not bluetooth.async_discovered_service_info(hass) assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22") - assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert len(mock_bleak_scanner_start.mock_calls) == 1 + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() - assert not bluetooth.async_discovered_service_info(hass) + assert len(mock_bleak_scanner_start.mock_calls) == 1 - wrong_device = BLEDevice("44:44:33:11:23:42", "wrong_name") - wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv) - switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") - switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) - wrong_device_went_unavailable = False - switchbot_device_went_unavailable = False + assert not bluetooth.async_discovered_service_info(hass) - @callback - def _wrong_device_unavailable_callback(_address: str) -> None: - """Wrong device unavailable callback.""" - nonlocal wrong_device_went_unavailable - wrong_device_went_unavailable = True - raise ValueError("blow up") + wrong_device = BLEDevice("44:44:33:11:23:42", "wrong_name") + wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) + async_get_scanner(hass)._callback(wrong_device, wrong_adv) + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + wrong_device_went_unavailable = False + switchbot_device_went_unavailable = False - @callback - def _switchbot_device_unavailable_callback(_address: str) -> None: - """Switchbot device unavailable callback.""" - nonlocal switchbot_device_went_unavailable - switchbot_device_went_unavailable = True + @callback + def _wrong_device_unavailable_callback(_address: str) -> None: + """Wrong device unavailable callback.""" + nonlocal wrong_device_went_unavailable + wrong_device_went_unavailable = True + raise ValueError("blow up") - wrong_device_unavailable_cancel = async_track_unavailable( - hass, _wrong_device_unavailable_callback, wrong_device.address - ) - switchbot_device_unavailable_cancel = async_track_unavailable( - hass, _switchbot_device_unavailable_callback, switchbot_device.address - ) + @callback + def _switchbot_device_unavailable_callback(_address: str) -> None: + """Switchbot device unavailable callback.""" + nonlocal switchbot_device_went_unavailable + switchbot_device_went_unavailable = True - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) - ) - await hass.async_block_till_done() + wrong_device_unavailable_cancel = async_track_unavailable( + hass, _wrong_device_unavailable_callback, wrong_device.address + ) + switchbot_device_unavailable_cancel = async_track_unavailable( + hass, _switchbot_device_unavailable_callback, switchbot_device.address + ) - service_infos = bluetooth.async_discovered_service_info(hass) - assert switchbot_device_went_unavailable is False - assert wrong_device_went_unavailable is True + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) + await hass.async_block_till_done() - # See the devices again - models.HA_BLEAK_SCANNER._callback(wrong_device, wrong_adv) - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) - # Cancel the callbacks - wrong_device_unavailable_cancel() - switchbot_device_unavailable_cancel() - wrong_device_went_unavailable = False - switchbot_device_went_unavailable = False + service_infos = bluetooth.async_discovered_service_info(hass) + assert switchbot_device_went_unavailable is False + assert wrong_device_went_unavailable is True - # Verify the cancel is effective - async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) - ) - await hass.async_block_till_done() - assert switchbot_device_went_unavailable is False - assert wrong_device_went_unavailable is False + # See the devices again + async_get_scanner(hass)._callback(wrong_device, wrong_adv) + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + # Cancel the callbacks + wrong_device_unavailable_cancel() + switchbot_device_unavailable_cancel() + wrong_device_went_unavailable = False + switchbot_device_went_unavailable = False - assert len(service_infos) == 1 - # wrong_name should not appear because bleak no longer sees it - assert service_infos[0].name == "wohand" - assert service_infos[0].source == SOURCE_LOCAL - assert isinstance(service_infos[0].device, BLEDevice) - assert isinstance(service_infos[0].advertisement, AdvertisementData) + # Verify the cancel is effective + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) + await hass.async_block_till_done() + assert switchbot_device_went_unavailable is False + assert wrong_device_went_unavailable is False - assert bluetooth.async_address_present(hass, "44:44:33:11:23:42") is False - assert bluetooth.async_address_present(hass, "44:44:33:11:23:45") is True + assert len(service_infos) == 1 + # wrong_name should not appear because bleak no longer sees it + assert service_infos[0].name == "wohand" + assert service_infos[0].source == SOURCE_LOCAL + assert isinstance(service_infos[0].device, BLEDevice) + assert isinstance(service_infos[0].advertisement, AdvertisementData) + + assert bluetooth.async_address_present(hass, "44:44:33:11:23:42") is False + assert bluetooth.async_address_present(hass, "44:44:33:11:23:45") is True -async def test_register_callbacks(hass, mock_bleak_scanner_start): +async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetooth): """Test registering a callback.""" mock_bt = [] callbacks = [] @@ -347,25 +382,25 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start): service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + async_get_scanner(hass)._callback(empty_device, empty_adv) await hass.async_block_till_done() empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") # 3rd callback raises ValueError but is still tracked - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + async_get_scanner(hass)._callback(empty_device, empty_adv) await hass.async_block_till_done() cancel() # 4th callback should not be tracked since we canceled - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + async_get_scanner(hass)._callback(empty_device, empty_adv) await hass.async_block_till_done() assert len(callbacks) == 3 @@ -389,7 +424,9 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start): assert service_info.manufacturer_id is None -async def test_register_callback_by_address(hass, mock_bleak_scanner_start): +async def test_register_callback_by_address( + hass, mock_bleak_scanner_start, enable_bluetooth +): """Test registering a callback by address.""" mock_bt = [] callbacks = [] @@ -404,10 +441,13 @@ async def test_register_callback_by_address(hass, mock_bleak_scanner_start): with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt - ), patch.object(hass.config_entries.flow, "async_init"): + ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -427,25 +467,25 @@ async def test_register_callback_by_address(hass, mock_bleak_scanner_start): service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + async_get_scanner(hass)._callback(empty_device, empty_adv) await hass.async_block_till_done() empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") # 3rd callback raises ValueError but is still tracked - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + async_get_scanner(hass)._callback(empty_device, empty_adv) await hass.async_block_till_done() cancel() # 4th callback should not be tracked since we canceled - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) + async_get_scanner(hass)._callback(empty_device, empty_adv) await hass.async_block_till_done() # Now register again with a callback that fails to @@ -475,121 +515,133 @@ async def test_register_callback_by_address(hass, mock_bleak_scanner_start): assert service_info.manufacturer_id == 89 -async def test_wrapped_instance_with_filter(hass, mock_bleak_scanner_start): +async def test_wrapped_instance_with_filter( + hass, mock_bleak_scanner_start, enable_bluetooth +): """Test consumers can use the wrapped instance with a filter as if it was normal BleakScanner.""" with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] - ), patch.object(hass.config_entries.flow, "async_init"): + ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - detected = [] + detected = [] - def _device_detected( - device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: - """Handle a detected device.""" - detected.append((device, advertisement_data)) + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) - switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") - switchbot_adv = AdvertisementData( - local_name="wohand", - service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], - manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, - service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, - ) - empty_device = BLEDevice("11:22:33:44:55:66", "empty") - empty_adv = AdvertisementData(local_name="empty") + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") - assert models.HA_BLEAK_SCANNER is not None - scanner = models.HaBleakScannerWrapper( - filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} - ) - scanner.register_detection_callback(_device_detected) + assert async_get_scanner(hass) is not None + scanner = models.HaBleakScannerWrapper( + filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} + ) + scanner.register_detection_callback(_device_detected) - mock_discovered = [MagicMock()] - type(models.HA_BLEAK_SCANNER).discovered_devices = mock_discovered - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) - await hass.async_block_till_done() + mock_discovered = [MagicMock()] + type(async_get_scanner(hass)).discovered_devices = mock_discovered + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() - discovered = await scanner.discover(timeout=0) - assert len(discovered) == 1 - assert discovered == mock_discovered - assert len(detected) == 1 + discovered = await scanner.discover(timeout=0) + assert len(discovered) == 1 + assert discovered == mock_discovered + assert len(detected) == 1 - scanner.register_detection_callback(_device_detected) - # We should get a reply from the history when we register again - assert len(detected) == 2 - scanner.register_detection_callback(_device_detected) - # We should get a reply from the history when we register again - assert len(detected) == 3 + scanner.register_detection_callback(_device_detected) + # We should get a reply from the history when we register again + assert len(detected) == 2 + scanner.register_detection_callback(_device_detected) + # We should get a reply from the history when we register again + assert len(detected) == 3 - type(models.HA_BLEAK_SCANNER).discovered_devices = [] - discovered = await scanner.discover(timeout=0) - assert len(discovered) == 0 - assert discovered == [] + type(async_get_scanner(hass)).discovered_devices = [] + discovered = await scanner.discover(timeout=0) + assert len(discovered) == 0 + assert discovered == [] - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) - assert len(detected) == 4 + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + assert len(detected) == 4 - # The filter we created in the wrapped scanner with should be respected - # and we should not get another callback - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) - assert len(detected) == 4 + # The filter we created in the wrapped scanner with should be respected + # and we should not get another callback + async_get_scanner(hass)._callback(empty_device, empty_adv) + assert len(detected) == 4 -async def test_wrapped_instance_with_service_uuids(hass, mock_bleak_scanner_start): +async def test_wrapped_instance_with_service_uuids( + hass, mock_bleak_scanner_start, enable_bluetooth +): """Test consumers can use the wrapped instance with a service_uuids list as if it was normal BleakScanner.""" with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] - ), patch.object(hass.config_entries.flow, "async_init"): + ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - detected = [] + detected = [] - def _device_detected( - device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: - """Handle a detected device.""" - detected.append((device, advertisement_data)) + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) - switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") - switchbot_adv = AdvertisementData( - local_name="wohand", - service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], - manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, - service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, - ) - empty_device = BLEDevice("11:22:33:44:55:66", "empty") - empty_adv = AdvertisementData(local_name="empty") + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") - assert models.HA_BLEAK_SCANNER is not None - scanner = models.HaBleakScannerWrapper( - service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] - ) - scanner.register_detection_callback(_device_detected) + assert async_get_scanner(hass) is not None + scanner = models.HaBleakScannerWrapper( + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + scanner.register_detection_callback(_device_detected) - type(models.HA_BLEAK_SCANNER).discovered_devices = [MagicMock()] - for _ in range(2): - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) - await hass.async_block_till_done() + type(async_get_scanner(hass)).discovered_devices = [MagicMock()] + for _ in range(2): + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() - assert len(detected) == 2 + assert len(detected) == 2 - # The UUIDs list we created in the wrapped scanner with should be respected - # and we should not get another callback - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) - assert len(detected) == 2 + # The UUIDs list we created in the wrapped scanner with should be respected + # and we should not get another callback + async_get_scanner(hass)._callback(empty_device, empty_adv) + assert len(detected) == 2 -async def test_wrapped_instance_with_broken_callbacks(hass, mock_bleak_scanner_start): +async def test_wrapped_instance_with_broken_callbacks( + hass, mock_bleak_scanner_start, enable_bluetooth +): """Test broken callbacks do not cause the scanner to fail.""" with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] @@ -597,158 +649,173 @@ async def test_wrapped_instance_with_broken_callbacks(hass, mock_bleak_scanner_s assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - detected = [] + detected = [] - def _device_detected( - device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: - """Handle a detected device.""" - if detected: - raise ValueError - detected.append((device, advertisement_data)) + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + if detected: + raise ValueError + detected.append((device, advertisement_data)) - switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") - switchbot_adv = AdvertisementData( - local_name="wohand", - service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], - manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, - service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, - ) + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) - assert models.HA_BLEAK_SCANNER is not None - scanner = models.HaBleakScannerWrapper( - service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] - ) - scanner.register_detection_callback(_device_detected) + assert async_get_scanner(hass) is not None + scanner = models.HaBleakScannerWrapper( + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + scanner.register_detection_callback(_device_detected) - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) - await hass.async_block_till_done() - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) - await hass.async_block_till_done() - assert len(detected) == 1 + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + assert len(detected) == 1 -async def test_wrapped_instance_changes_uuids(hass, mock_bleak_scanner_start): +async def test_wrapped_instance_changes_uuids( + hass, mock_bleak_scanner_start, enable_bluetooth +): """Test consumers can use the wrapped instance can change the uuids later.""" with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] - ), patch.object(hass.config_entries.flow, "async_init"): + ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() + detected = [] - detected = [] + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) - def _device_detected( - device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: - """Handle a detected device.""" - detected.append((device, advertisement_data)) + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:66", "empty") + empty_adv = AdvertisementData(local_name="empty") - switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") - switchbot_adv = AdvertisementData( - local_name="wohand", - service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], - manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, - service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, - ) - empty_device = BLEDevice("11:22:33:44:55:66", "empty") - empty_adv = AdvertisementData(local_name="empty") + assert async_get_scanner(hass) is not None + scanner = models.HaBleakScannerWrapper() + scanner.set_scanning_filter( + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + scanner.register_detection_callback(_device_detected) - assert models.HA_BLEAK_SCANNER is not None - scanner = models.HaBleakScannerWrapper() - scanner.set_scanning_filter(service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]) - scanner.register_detection_callback(_device_detected) + type(async_get_scanner(hass)).discovered_devices = [MagicMock()] + for _ in range(2): + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() - type(models.HA_BLEAK_SCANNER).discovered_devices = [MagicMock()] - for _ in range(2): - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) - await hass.async_block_till_done() + assert len(detected) == 2 - assert len(detected) == 2 - - # The UUIDs list we created in the wrapped scanner with should be respected - # and we should not get another callback - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) - assert len(detected) == 2 + # The UUIDs list we created in the wrapped scanner with should be respected + # and we should not get another callback + async_get_scanner(hass)._callback(empty_device, empty_adv) + assert len(detected) == 2 -async def test_wrapped_instance_changes_filters(hass, mock_bleak_scanner_start): +async def test_wrapped_instance_changes_filters( + hass, mock_bleak_scanner_start, enable_bluetooth +): """Test consumers can use the wrapped instance can change the filter later.""" with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] - ), patch.object(hass.config_entries.flow, "async_init"): + ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() + detected = [] - detected = [] + def _device_detected( + device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + detected.append((device, advertisement_data)) - def _device_detected( - device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: - """Handle a detected device.""" - detected.append((device, advertisement_data)) + switchbot_device = BLEDevice("44:44:33:11:23:42", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + empty_device = BLEDevice("11:22:33:44:55:62", "empty") + empty_adv = AdvertisementData(local_name="empty") - switchbot_device = BLEDevice("44:44:33:11:23:42", "wohand") - switchbot_adv = AdvertisementData( - local_name="wohand", - service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], - manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, - service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, - ) - empty_device = BLEDevice("11:22:33:44:55:62", "empty") - empty_adv = AdvertisementData(local_name="empty") + assert async_get_scanner(hass) is not None + scanner = models.HaBleakScannerWrapper() + scanner.set_scanning_filter( + filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} + ) + scanner.register_detection_callback(_device_detected) - assert models.HA_BLEAK_SCANNER is not None - scanner = models.HaBleakScannerWrapper() - scanner.set_scanning_filter( - filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} - ) - scanner.register_detection_callback(_device_detected) + type(async_get_scanner(hass)).discovered_devices = [MagicMock()] + for _ in range(2): + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() - type(models.HA_BLEAK_SCANNER).discovered_devices = [MagicMock()] - for _ in range(2): - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) - await hass.async_block_till_done() + assert len(detected) == 2 - assert len(detected) == 2 - - # The UUIDs list we created in the wrapped scanner with should be respected - # and we should not get another callback - models.HA_BLEAK_SCANNER._callback(empty_device, empty_adv) - assert len(detected) == 2 + # The UUIDs list we created in the wrapped scanner with should be respected + # and we should not get another callback + async_get_scanner(hass)._callback(empty_device, empty_adv) + assert len(detected) == 2 async def test_wrapped_instance_unsupported_filter( - hass, mock_bleak_scanner_start, caplog + hass, mock_bleak_scanner_start, caplog, enable_bluetooth ): """Test we want when their filter is ineffective.""" with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] - ), patch.object(hass.config_entries.flow, "async_init"): + ): assert await async_setup_component( hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} ) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert models.HA_BLEAK_SCANNER is not None - scanner = models.HaBleakScannerWrapper() - scanner.set_scanning_filter( - filters={ - "unsupported": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], - "DuplicateData": True, - } - ) - assert "Only UUIDs filters are supported" in caplog.text + with patch.object(hass.config_entries.flow, "async_init"): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert async_get_scanner(hass) is not None + scanner = models.HaBleakScannerWrapper() + scanner.set_scanning_filter( + filters={ + "unsupported": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + "DuplicateData": True, + } + ) + assert "Only UUIDs filters are supported" in caplog.text async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): @@ -778,7 +845,7 @@ async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - models.HA_BLEAK_SCANNER._callback(switchbot_device, switchbot_adv) + async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert ( @@ -789,3 +856,82 @@ async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): assert ( bluetooth.async_ble_device_from_address(hass, "00:66:33:22:11:22") is None ) + + +async def test_setup_without_bluetooth_in_configuration_yaml(hass, mock_bluetooth): + """Test setting up without bluetooth in configuration.yaml does not create the config entry.""" + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + + +async def test_setup_with_bluetooth_in_configuration_yaml(hass, mock_bluetooth): + """Test setting up with bluetooth in configuration.yaml creates the config entry.""" + assert await async_setup_component(hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}) + await hass.async_block_till_done() + assert hass.config_entries.async_entries(bluetooth.DOMAIN) + + +async def test_can_unsetup_bluetooth(hass, mock_bleak_scanner_start, enable_bluetooth): + """Test we can setup and unsetup bluetooth.""" + entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}) + entry.add_to_hass(hass) + for _ in range(2): + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_auto_detect_bluetooth_adapters_linux(hass): + """Test we auto detect bluetooth adapters on linux.""" + with patch( + "bluetooth_adapters.get_bluetooth_adapters", return_value={"hci0"} + ), patch( + "homeassistant.components.bluetooth.platform.system", return_value="Linux" + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1 + + +async def test_auto_detect_bluetooth_adapters_linux_none_found(hass): + """Test we auto detect bluetooth adapters on linux with no adapters found.""" + with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=set()), patch( + "homeassistant.components.bluetooth.platform.system", return_value="Linux" + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 0 + + +async def test_auto_detect_bluetooth_adapters_macos(hass): + """Test we auto detect bluetooth adapters on macos.""" + with patch( + "homeassistant.components.bluetooth.platform.system", return_value="Darwin" + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1 + + +async def test_no_auto_detect_bluetooth_adapters_windows(hass): + """Test we auto detect bluetooth adapters on windows.""" + with patch( + "homeassistant.components.bluetooth.platform.system", return_value="Windows" + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 0 + + +async def test_raising_runtime_error_when_no_bluetooth(hass): + """Test we raise an exception if we try to get the scanner when its not there.""" + with pytest.raises(RuntimeError): + bluetooth.async_get_scanner(hass) diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 755b2ec07f8..010989628e1 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -12,6 +12,7 @@ from homeassistant.components.bluetooth import ( DOMAIN, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, + async_get_scanner, ) from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, @@ -207,12 +208,14 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True + scanner = async_get_scanner(hass) with patch( "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", [MagicMock(address="44:44:33:11:23:45")], - ), patch( - "homeassistant.components.bluetooth.models.HA_BLEAK_SCANNER.history", + ), patch.object( + scanner, + "history", {"aa:bb:cc:dd:ee:ff": MagicMock()}, ): async_fire_time_changed( @@ -228,8 +231,9 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): with patch( "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", [MagicMock(address="44:44:33:11:23:45")], - ), patch( - "homeassistant.components.bluetooth.models.HA_BLEAK_SCANNER.history", + ), patch.object( + scanner, + "history", {"aa:bb:cc:dd:ee:ff": MagicMock()}, ): async_fire_time_changed( diff --git a/tests/components/bluetooth/test_usage.py b/tests/components/bluetooth/test_usage.py index 92339735340..8e566a7ce5a 100644 --- a/tests/components/bluetooth/test_usage.py +++ b/tests/components/bluetooth/test_usage.py @@ -1,22 +1,25 @@ """Tests for the Bluetooth integration.""" -from unittest.mock import MagicMock import bleak -from homeassistant.components.bluetooth import models from homeassistant.components.bluetooth.models import HaBleakScannerWrapper -from homeassistant.components.bluetooth.usage import install_multiple_bleak_catcher +from homeassistant.components.bluetooth.usage import ( + install_multiple_bleak_catcher, + uninstall_multiple_bleak_catcher, +) async def test_multiple_bleak_scanner_instances(hass): - """Test creating multiple zeroconf throws without an integration.""" - assert models.HA_BLEAK_SCANNER is None - mock_scanner = MagicMock() - - install_multiple_bleak_catcher(mock_scanner) + """Test creating multiple BleakScanners without an integration.""" + install_multiple_bleak_catcher() instance = bleak.BleakScanner() assert isinstance(instance, HaBleakScannerWrapper) - assert models.HA_BLEAK_SCANNER is mock_scanner + + uninstall_multiple_bleak_catcher() + + instance = bleak.BleakScanner() + + assert not isinstance(instance, HaBleakScannerWrapper) diff --git a/tests/components/bluetooth_le_tracker/conftest.py b/tests/components/bluetooth_le_tracker/conftest.py index 30b2d5a44fb..9fce8e85ea8 100644 --- a/tests/components/bluetooth_le_tracker/conftest.py +++ b/tests/components/bluetooth_le_tracker/conftest.py @@ -1,7 +1,8 @@ -"""Tests for the bluetooth_le_tracker component.""" +"""Session fixtures.""" + import pytest @pytest.fixture(autouse=True) -def bluetooth_le_tracker_auto_mock_bluetooth(mock_bluetooth): - """Mock the bluetooth integration scanner.""" +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index d82b4109839..f8f8c20dbb2 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -23,7 +23,7 @@ def recorder_url_mock(): yield -async def test_setup(hass, mock_zeroconf, mock_get_source_ip): +async def test_setup(hass, mock_zeroconf, mock_get_source_ip, mock_bluetooth): """Test setup.""" recorder_helper.async_initialize_recorder(hass) assert await async_setup_component(hass, "default_config", {"foo": "bar"}) diff --git a/tests/components/inkbird/conftest.py b/tests/components/inkbird/conftest.py index c44e9f7929a..3450cb933fe 100644 --- a/tests/components/inkbird/conftest.py +++ b/tests/components/inkbird/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def mock_bluetooth(mock_bleak_scanner_start): +def mock_bluetooth(enable_bluetooth): """Auto mock bluetooth.""" diff --git a/tests/components/sensorpush/conftest.py b/tests/components/sensorpush/conftest.py index c6497a4e76d..2a983a7a4ed 100644 --- a/tests/components/sensorpush/conftest.py +++ b/tests/components/sensorpush/conftest.py @@ -4,5 +4,5 @@ import pytest @pytest.fixture(autouse=True) -def auto_mock_bleak_scanner_start(mock_bleak_scanner_start): - """Auto mock bleak scanner start.""" +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/conftest.py b/tests/conftest.py index 9ca29c60658..e0f4fb5ab90 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -871,6 +871,24 @@ def mock_integration_frame(): yield correct_frame +@pytest.fixture(name="enable_bluetooth") +async def mock_enable_bluetooth( + hass, mock_bleak_scanner_start, mock_bluetooth_adapters +): + """Fixture to mock starting the bleak scanner.""" + entry = MockConfigEntry(domain="bluetooth") + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + +@pytest.fixture(name="mock_bluetooth_adapters") +def mock_bluetooth_adapters(): + """Fixture to mock bluetooth adapters.""" + with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=set()): + yield + + @pytest.fixture(name="mock_bleak_scanner_start") def mock_bleak_scanner_start(): """Fixture to mock starting the bleak scanner.""" @@ -900,5 +918,5 @@ def mock_bleak_scanner_start(): @pytest.fixture(name="mock_bluetooth") -def mock_bluetooth(mock_bleak_scanner_start): +def mock_bluetooth(mock_bleak_scanner_start, mock_bluetooth_adapters): """Mock out bluetooth from starting.""" From 19db6ecf6d3a7e35c40a0db6e109ccc03d2ef491 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 Jul 2022 16:02:15 -0500 Subject: [PATCH 2790/3516] Add missing inkbird config flow tests (#75630) --- tests/components/inkbird/test_config_flow.py | 58 ++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/components/inkbird/test_config_flow.py b/tests/components/inkbird/test_config_flow.py index b783562b126..86097aec208 100644 --- a/tests/components/inkbird/test_config_flow.py +++ b/tests/components/inkbird/test_config_flow.py @@ -92,3 +92,61 @@ async def test_async_step_user_with_found_devices_already_setup(hass): ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SPS_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SPS_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SPS_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=SPS_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.sensorpush.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "IBS-TH 75BBE1738105" + assert result2["data"] == {} + assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" From 5b555066ea428427945aca0824b30e496c653831 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 23 Jul 2022 00:18:22 +0200 Subject: [PATCH 2791/3516] Add new NextDNS sensors (#74789) --- homeassistant/components/nextdns/sensor.py | 48 ++++++++++++----- tests/components/nextdns/__init__.py | 6 ++- tests/components/nextdns/test_sensor.py | 60 +++++++++++++++++----- 3 files changed, 87 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index 174357864b3..168c0be8cd0 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -63,7 +63,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:dns", name="DNS queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.all_queries, ), NextDnsSensorEntityDescription[AnalyticsStatus]( @@ -73,7 +73,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:dns", name="DNS queries blocked", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.blocked_queries, ), NextDnsSensorEntityDescription[AnalyticsStatus]( @@ -83,7 +83,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:dns", name="DNS queries relayed", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.relayed_queries, ), NextDnsSensorEntityDescription[AnalyticsStatus]( @@ -104,7 +104,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:dns", name="DNS-over-HTTPS queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.doh_queries, ), NextDnsSensorEntityDescription[AnalyticsProtocols]( @@ -115,7 +115,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:dns", name="DNS-over-TLS queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.dot_queries, ), NextDnsSensorEntityDescription[AnalyticsProtocols]( @@ -126,9 +126,20 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:dns", name="DNS-over-QUIC queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.doq_queries, ), + NextDnsSensorEntityDescription[AnalyticsProtocols]( + key="tcp_queries", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="TCP Queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.TOTAL, + value=lambda data: data.tcp_queries, + ), NextDnsSensorEntityDescription[AnalyticsProtocols]( key="udp_queries", coordinator_type=ATTR_PROTOCOLS, @@ -137,7 +148,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:dns", name="UDP queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.udp_queries, ), NextDnsSensorEntityDescription[AnalyticsProtocols]( @@ -173,6 +184,17 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doq_queries_ratio, ), + NextDnsSensorEntityDescription[AnalyticsProtocols]( + key="tcp_queries_ratio", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="TCP Queries Ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.tcp_queries_ratio, + ), NextDnsSensorEntityDescription[AnalyticsProtocols]( key="udp_queries_ratio", coordinator_type=ATTR_PROTOCOLS, @@ -192,7 +214,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:lock", name="Encrypted queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.encrypted_queries, ), NextDnsSensorEntityDescription[AnalyticsEncryption]( @@ -203,7 +225,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:lock-open", name="Unencrypted queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.unencrypted_queries, ), NextDnsSensorEntityDescription[AnalyticsEncryption]( @@ -225,7 +247,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:ip", name="IPv4 queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.ipv4_queries, ), NextDnsSensorEntityDescription[AnalyticsIpVersions]( @@ -236,7 +258,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:ip", name="IPv6 queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.ipv6_queries, ), NextDnsSensorEntityDescription[AnalyticsIpVersions]( @@ -258,7 +280,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:lock-check", name="DNSSEC validated queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.validated_queries, ), NextDnsSensorEntityDescription[AnalyticsDnssec]( @@ -269,7 +291,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( icon="mdi:lock-alert", name="DNSSEC not validated queries", native_unit_of_measurement="queries", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, value=lambda data: data.not_validated_queries, ), NextDnsSensorEntityDescription[AnalyticsDnssec]( diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py index 82c55f56bbb..6eb258ef2c4 100644 --- a/tests/components/nextdns/__init__.py +++ b/tests/components/nextdns/__init__.py @@ -24,7 +24,11 @@ DNSSEC = AnalyticsDnssec(not_validated_queries=25, validated_queries=75) ENCRYPTION = AnalyticsEncryption(encrypted_queries=60, unencrypted_queries=40) IP_VERSIONS = AnalyticsIpVersions(ipv4_queries=90, ipv6_queries=10) PROTOCOLS = AnalyticsProtocols( - doh_queries=20, doq_queries=10, dot_queries=30, udp_queries=40 + doh_queries=20, + doq_queries=10, + dot_queries=30, + tcp_queries=0, + udp_queries=40, ) SETTINGS = Settings( ai_threat_detection=True, diff --git a/tests/components/nextdns/test_sensor.py b/tests/components/nextdns/test_sensor.py index 8a7d13866f3..731f98203ab 100644 --- a/tests/components/nextdns/test_sensor.py +++ b/tests/components/nextdns/test_sensor.py @@ -121,6 +121,20 @@ async def test_sensor(hass): suggested_object_id="fake_profile_ipv6_queries_ratio", disabled_by=None, ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_tcp_queries", + suggested_object_id="fake_profile_tcp_queries", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_tcp_queries_ratio", + suggested_object_id="fake_profile_tcp_queries_ratio", + disabled_by=None, + ) registry.async_get_or_create( SENSOR_DOMAIN, DOMAIN, @@ -148,7 +162,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dns_queries") assert state assert state.state == "100" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_dns_queries") @@ -158,7 +172,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dns_queries_blocked") assert state assert state.state == "20" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_dns_queries_blocked") @@ -178,7 +192,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dns_queries_relayed") assert state assert state.state == "10" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_dns_queries_relayed") @@ -188,7 +202,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dns_over_https_queries") assert state assert state.state == "20" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_dns_over_https_queries") @@ -208,7 +222,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dns_over_quic_queries") assert state assert state.state == "10" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_dns_over_quic_queries") @@ -228,7 +242,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dns_over_tls_queries") assert state assert state.state == "30" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_dns_over_tls_queries") @@ -248,7 +262,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dnssec_not_validated_queries") assert state assert state.state == "25" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_dnssec_not_validated_queries") @@ -258,7 +272,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_dnssec_validated_queries") assert state assert state.state == "75" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_dnssec_validated_queries") @@ -278,7 +292,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_encrypted_queries") assert state assert state.state == "60" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_encrypted_queries") @@ -288,7 +302,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_unencrypted_queries") assert state assert state.state == "40" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_unencrypted_queries") @@ -308,7 +322,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_ipv4_queries") assert state assert state.state == "90" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_ipv4_queries") @@ -318,7 +332,7 @@ async def test_sensor(hass): state = hass.states.get("sensor.fake_profile_ipv6_queries") assert state assert state.state == "10" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_ipv6_queries") @@ -335,10 +349,30 @@ async def test_sensor(hass): assert entry assert entry.unique_id == "xyz12_ipv6_queries_ratio" + state = hass.states.get("sensor.fake_profile_tcp_queries") + assert state + assert state.state == "0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_tcp_queries") + assert entry + assert entry.unique_id == "xyz12_tcp_queries" + + state = hass.states.get("sensor.fake_profile_tcp_queries_ratio") + assert state + assert state.state == "0.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_tcp_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_tcp_queries_ratio" + state = hass.states.get("sensor.fake_profile_udp_queries") assert state assert state.state == "40" - assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" entry = registry.async_get("sensor.fake_profile_udp_queries") From 88b9a518115a47c51ae4d6143cab08d04b08876c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 Jul 2022 17:43:13 -0500 Subject: [PATCH 2792/3516] Fix inkbird config flow tests to correctly test discovery and user flow (#75638) * Fix inkbird config flow tests to correctly test discovery and user flow * Fix inkbird config flow tests to correctly test discovery and user flow --- tests/components/inkbird/test_config_flow.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/components/inkbird/test_config_flow.py b/tests/components/inkbird/test_config_flow.py index 86097aec208..c1f8b3ef545 100644 --- a/tests/components/inkbird/test_config_flow.py +++ b/tests/components/inkbird/test_config_flow.py @@ -141,12 +141,24 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): assert result["step_id"] == "bluetooth_confirm" with patch( - "homeassistant.components.sensorpush.async_setup_entry", return_value=True + "homeassistant.components.inkbird.config_flow.async_discovered_service_info", + return_value=[SPS_SERVICE_INFO], ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch("homeassistant.components.inkbird.async_setup_entry", return_value=True): result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY assert result2["title"] == "IBS-TH 75BBE1738105" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) From 6bb51782fab9fab8d48db1923aac79c0ea7c8e81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 Jul 2022 17:45:22 -0500 Subject: [PATCH 2793/3516] Add missing config flow tests for sensorpush (#75629) * Add missing config flow tests for sensorpush * merge correct commits from integration --- .../components/sensorpush/test_config_flow.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/components/sensorpush/test_config_flow.py b/tests/components/sensorpush/test_config_flow.py index 662841af3f1..1c825640603 100644 --- a/tests/components/sensorpush/test_config_flow.py +++ b/tests/components/sensorpush/test_config_flow.py @@ -96,3 +96,75 @@ async def test_async_step_user_with_found_devices_already_setup(hass): ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HTW_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HTW_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HTW_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=HTW_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.sensorpush.config_flow.async_discovered_service_info", + return_value=[HTW_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.sensorpush.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "HT.w 0CA1" + assert result2["data"] == {} + assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) From 402e533fefe17b736c2c72f3747dc9edbe8d9eae Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 22 Jul 2022 23:55:06 +0100 Subject: [PATCH 2794/3516] Initial xiaomi_ble integration (#75618) * Initial xiaomi_ble integration * black * Update homeassistant/components/xiaomi_ble/config_flow.py Co-authored-by: Ernst Klamer * Update homeassistant/components/xiaomi_ble/config_flow.py Co-authored-by: Ernst Klamer * Apply suggestions from code review Co-authored-by: Ernst Klamer * Update tests/components/xiaomi_ble/test_config_flow.py Co-authored-by: Ernst Klamer * Update homeassistant/components/xiaomi_ble/sensor.py Co-authored-by: Ernst Klamer * Update tests/components/xiaomi_ble/test_config_flow.py Co-authored-by: Ernst Klamer * Remove debug code * Need 'proper' MAC when running tests on linux * Need to use proper MAC so validation passes * Add tests for already_in_progress and already_configured * copy test, add session fixture * fix test Co-authored-by: Ernst Klamer Co-authored-by: J. Nick Koston --- CODEOWNERS | 2 + .../components/xiaomi_ble/__init__.py | 56 ++++++ .../components/xiaomi_ble/config_flow.py | 93 ++++++++++ homeassistant/components/xiaomi_ble/const.py | 3 + .../components/xiaomi_ble/manifest.json | 15 ++ homeassistant/components/xiaomi_ble/sensor.py | 149 +++++++++++++++ .../components/xiaomi_ble/strings.json | 21 +++ .../xiaomi_ble/translations/en.json | 21 +++ homeassistant/generated/bluetooth.py | 4 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/xiaomi_ble/__init__.py | 38 ++++ tests/components/xiaomi_ble/conftest.py | 8 + .../components/xiaomi_ble/test_config_flow.py | 174 ++++++++++++++++++ tests/components/xiaomi_ble/test_sensor.py | 50 +++++ 16 files changed, 641 insertions(+) create mode 100644 homeassistant/components/xiaomi_ble/__init__.py create mode 100644 homeassistant/components/xiaomi_ble/config_flow.py create mode 100644 homeassistant/components/xiaomi_ble/const.py create mode 100644 homeassistant/components/xiaomi_ble/manifest.json create mode 100644 homeassistant/components/xiaomi_ble/sensor.py create mode 100644 homeassistant/components/xiaomi_ble/strings.json create mode 100644 homeassistant/components/xiaomi_ble/translations/en.json create mode 100644 tests/components/xiaomi_ble/__init__.py create mode 100644 tests/components/xiaomi_ble/conftest.py create mode 100644 tests/components/xiaomi_ble/test_config_flow.py create mode 100644 tests/components/xiaomi_ble/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 3b38b6e1a5a..073170795a1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1220,6 +1220,8 @@ build.json @home-assistant/supervisor /homeassistant/components/xbox_live/ @MartinHjelmare /homeassistant/components/xiaomi_aqara/ @danielhiversen @syssi /tests/components/xiaomi_aqara/ @danielhiversen @syssi +/homeassistant/components/xiaomi_ble/ @Jc2k @Ernst79 +/tests/components/xiaomi_ble/ @Jc2k @Ernst79 /homeassistant/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG @bieniu /tests/components/xiaomi_miio/ @rytilahti @syssi @starkillerOG @bieniu /homeassistant/components/xiaomi_tv/ @simse diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py new file mode 100644 index 00000000000..036ff37a306 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -0,0 +1,56 @@ +"""The Xiaomi Bluetooth integration.""" +from __future__ import annotations + +import logging + +from xiaomi_ble import XiaomiBluetoothDeviceData + +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothDataUpdate, + PassiveBluetoothDataUpdateCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +from .const import DOMAIN +from .sensor import sensor_update_to_bluetooth_data_update + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Xiaomi BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + + data = XiaomiBluetoothDeviceData() + + @callback + def _async_update_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Update data from Xiaomi Bluetooth.""" + return sensor_update_to_bluetooth_data_update(data.update(service_info)) + + hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothDataUpdateCoordinator( + hass, + _LOGGER, + update_method=_async_update_data, + address=address, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py new file mode 100644 index 00000000000..8f478442d6a --- /dev/null +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -0,0 +1,93 @@ +"""Config flow for Xiaomi Bluetooth integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol +from xiaomi_ble import XiaomiBluetoothDeviceData as DeviceData + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfo, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Xiaomi Bluetooth.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfo | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/xiaomi_ble/const.py b/homeassistant/components/xiaomi_ble/const.py new file mode 100644 index 00000000000..9a38c75c05f --- /dev/null +++ b/homeassistant/components/xiaomi_ble/const.py @@ -0,0 +1,3 @@ +"""Constants for the Xiaomi Bluetooth integration.""" + +DOMAIN = "xiaomi_ble" diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json new file mode 100644 index 00000000000..cf5ef00777b --- /dev/null +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -0,0 +1,15 @@ +{ + "domain": "xiaomi_ble", + "name": "Xiaomi BLE", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", + "bluetooth": [ + { + "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" + } + ], + "requirements": ["xiaomi-ble==0.1.0"], + "dependencies": ["bluetooth"], + "codeowners": ["@Jc2k", "@Ernst79"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py new file mode 100644 index 00000000000..c380490dc8c --- /dev/null +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -0,0 +1,149 @@ +"""Support for xiaomi ble sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from xiaomi_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothCoordinatorEntity, + PassiveBluetoothDataUpdate, + PassiveBluetoothDataUpdateCoordinator, + PassiveBluetoothEntityKey, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + PERCENTAGE, + PRESSURE_MBAR, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( + key=f"{DeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=PRESSURE_MBAR, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def _sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: _sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Xiaomi BLE sensors.""" + coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + entry.async_on_unload( + coordinator.async_add_entities_listener( + XiaomiBluetoothSensorEntity, async_add_entities + ) + ) + + +class XiaomiBluetoothSensorEntity( + PassiveBluetoothCoordinatorEntity[ + PassiveBluetoothDataUpdateCoordinator[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a xiaomi ble sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.coordinator.entity_data.get(self.entity_key) diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json new file mode 100644 index 00000000000..7111626cca1 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json new file mode 100644 index 00000000000..d24df64f135 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 1edd25a7609..175b82dbcfd 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -37,5 +37,9 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ { "domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + }, + { + "domain": "xiaomi_ble", + "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ] diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2c7732fee8d..917eca321ea 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -420,6 +420,7 @@ FLOWS = { "ws66i", "xbox", "xiaomi_aqara", + "xiaomi_ble", "xiaomi_miio", "yale_smart_alarm", "yamaha_musiccast", diff --git a/requirements_all.txt b/requirements_all.txt index 15c62c63c2f..9451d3b8495 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2470,6 +2470,9 @@ xbox-webapi==2.0.11 # homeassistant.components.xbox_live xboxapi==2.0.1 +# homeassistant.components.xiaomi_ble +xiaomi-ble==0.1.0 + # homeassistant.components.knx xknx==0.21.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 742d6eb0f3a..3faa25b829f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1655,6 +1655,9 @@ wolf_smartset==0.1.11 # homeassistant.components.xbox xbox-webapi==2.0.11 +# homeassistant.components.xiaomi_ble +xiaomi-ble==0.1.0 + # homeassistant.components.knx xknx==0.21.5 diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py new file mode 100644 index 00000000000..6022b15bf51 --- /dev/null +++ b/tests/components/xiaomi_ble/__init__.py @@ -0,0 +1,38 @@ +"""Tests for the SensorPush integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_SENSOR_PUSH_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="00:00:00:00:00:00", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +LYWSDCGQ_SERVICE_INFO = BluetoothServiceInfo( + name="LYWSDCGQ", + address="58:2D:34:35:93:21", + rssi=-63, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": b"P \xaa\x01\xda!\x9354-X\r\x10\x04\xfe\x00H\x02" + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", +) + +MMC_T201_1_SERVICE_INFO = BluetoothServiceInfo( + name="MMC_T201_1", + address="00:81:F9:DD:6F:C1", + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": b'p"\xdb\x00o\xc1o\xdd\xf9\x81\x00\t\x00 \x05\xc6\rc\rQ' + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", +) diff --git a/tests/components/xiaomi_ble/conftest.py b/tests/components/xiaomi_ble/conftest.py new file mode 100644 index 00000000000..9fce8e85ea8 --- /dev/null +++ b/tests/components/xiaomi_ble/conftest.py @@ -0,0 +1,8 @@ +"""Session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py new file mode 100644 index 00000000000..f99cbd21296 --- /dev/null +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -0,0 +1,174 @@ +"""Test the Xiaomi config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.xiaomi_ble.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import ( + LYWSDCGQ_SERVICE_INFO, + MMC_T201_1_SERVICE_INFO, + NOT_SENSOR_PUSH_SERVICE_INFO, +) + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MMC_T201_1_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "MMC_T201_1" + assert result2["data"] == {} + assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" + + +async def test_async_step_bluetooth_not_xiaomi(hass): + """Test discovery via bluetooth not xiaomi.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_SENSOR_PUSH_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[LYWSDCGQ_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "58:2D:34:35:93:21"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "LYWSDCGQ" + assert result2["data"] == {} + assert result2["result"].unique_id == "58:2D:34:35:93:21" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="58:2D:34:35:93:21", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[LYWSDCGQ_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="00:81:F9:DD:6F:C1", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MMC_T201_1_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MMC_T201_1_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MMC_T201_1_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MMC_T201_1_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[MMC_T201_1_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "00:81:F9:DD:6F:C1"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "MMC_T201_1" + assert result2["data"] == {} + assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py new file mode 100644 index 00000000000..044b2ea3e87 --- /dev/null +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the Xiaomi config flow.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.components.xiaomi_ble.const import DOMAIN +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import MMC_T201_1_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="00:81:F9:DD:6F:C1", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback(MMC_T201_1_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + temp_sensor = hass.states.get("sensor.mmc_t201_1_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "36.8719980616822" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "MMC_T201_1 Temperature" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From cb543a21b390319692ede988905dba051936fb61 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 23 Jul 2022 00:58:48 +0200 Subject: [PATCH 2795/3516] Address NextDNS late review (#75635) * Init instance attributes * Remove condition * Improve typing in tests * Suggested change --- .../components/nextdns/config_flow.py | 6 ++++-- tests/components/nextdns/__init__.py | 7 +------ tests/components/nextdns/test_button.py | 5 +++-- tests/components/nextdns/test_config_flow.py | 15 +++++++------- tests/components/nextdns/test_diagnostics.py | 9 ++++++++- tests/components/nextdns/test_init.py | 20 +++++++++++++------ tests/components/nextdns/test_sensor.py | 5 +++-- .../components/nextdns/test_system_health.py | 10 ++++++++-- 8 files changed, 49 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/nextdns/config_flow.py b/homeassistant/components/nextdns/config_flow.py index c621accfe81..5c9bf04cfc1 100644 --- a/homeassistant/components/nextdns/config_flow.py +++ b/homeassistant/components/nextdns/config_flow.py @@ -24,8 +24,8 @@ class NextDnsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self.nextdns: NextDns - self.api_key: str + self.nextdns: NextDns | None = None + self.api_key: str | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -63,6 +63,8 @@ class NextDnsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle the profiles step.""" errors: dict[str, str] = {} + assert self.nextdns is not None + if user_input is not None: profile_name = user_input[CONF_PROFILE_NAME] profile_id = self.nextdns.get_profile_id(profile_name) diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py index 6eb258ef2c4..3b513b811b8 100644 --- a/tests/components/nextdns/__init__.py +++ b/tests/components/nextdns/__init__.py @@ -57,9 +57,7 @@ SETTINGS = Settings( ) -async def init_integration( - hass: HomeAssistant, add_to_hass: bool = True -) -> MockConfigEntry: +async def init_integration(hass: HomeAssistant) -> MockConfigEntry: """Set up the NextDNS integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, @@ -68,9 +66,6 @@ async def init_integration( data={CONF_API_KEY: "fake_api_key", CONF_PROFILE_ID: "xyz12"}, ) - if not add_to_hass: - return entry - with patch( "homeassistant.components.nextdns.NextDns.get_profiles", return_value=PROFILES ), patch( diff --git a/tests/components/nextdns/test_button.py b/tests/components/nextdns/test_button.py index 39201a668a4..fabf87f6462 100644 --- a/tests/components/nextdns/test_button.py +++ b/tests/components/nextdns/test_button.py @@ -3,13 +3,14 @@ from unittest.mock import patch from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util from . import init_integration -async def test_button(hass): +async def test_button(hass: HomeAssistant) -> None: """Test states of the button.""" registry = er.async_get(hass) @@ -24,7 +25,7 @@ async def test_button(hass): assert entry.unique_id == "xyz12_clear_logs" -async def test_button_press(hass): +async def test_button_press(hass: HomeAssistant) -> None: """Test button press.""" await init_integration(hass) diff --git a/tests/components/nextdns/test_config_flow.py b/tests/components/nextdns/test_config_flow.py index ad17de1c150..5f387fd1f64 100644 --- a/tests/components/nextdns/test_config_flow.py +++ b/tests/components/nextdns/test_config_flow.py @@ -13,11 +13,12 @@ from homeassistant.components.nextdns.const import ( ) from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant from . import PROFILES, init_integration -async def test_form_create_entry(hass): +async def test_form_create_entry(hass: HomeAssistant) -> None: """Test that the user step works.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -52,7 +53,7 @@ async def test_form_create_entry(hass): @pytest.mark.parametrize( - "error", + "exc,base_error", [ (ApiError("API Error"), "cannot_connect"), (InvalidApiKeyError, "invalid_api_key"), @@ -60,9 +61,10 @@ async def test_form_create_entry(hass): (ValueError, "unknown"), ], ) -async def test_form_errors(hass, error): +async def test_form_errors( + hass: HomeAssistant, exc: Exception, base_error: str +) -> None: """Test we handle errors.""" - exc, base_error = error with patch( "homeassistant.components.nextdns.NextDns.get_profiles", side_effect=exc ): @@ -75,10 +77,9 @@ async def test_form_errors(hass, error): assert result["errors"] == {"base": base_error} -async def test_form_already_configured(hass): +async def test_form_already_configured(hass: HomeAssistant) -> None: """Test that errors are shown when duplicates are added.""" - entry = await init_integration(hass) - entry.add_to_hass(hass) + await init_integration(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} diff --git a/tests/components/nextdns/test_diagnostics.py b/tests/components/nextdns/test_diagnostics.py index ba4a4d2ccb4..85dbceafff9 100644 --- a/tests/components/nextdns/test_diagnostics.py +++ b/tests/components/nextdns/test_diagnostics.py @@ -1,11 +1,18 @@ """Test NextDNS diagnostics.""" +from collections.abc import Awaitable, Callable + +from aiohttp import ClientSession + from homeassistant.components.diagnostics import REDACTED +from homeassistant.core import HomeAssistant from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.components.nextdns import init_integration -async def test_entry_diagnostics(hass, hass_client): +async def test_entry_diagnostics( + hass: HomeAssistant, hass_client: Callable[..., Awaitable[ClientSession]] +) -> None: """Test config entry diagnostics.""" entry = await init_integration(hass) diff --git a/tests/components/nextdns/test_init.py b/tests/components/nextdns/test_init.py index c16fad4e812..fb9ea74509e 100644 --- a/tests/components/nextdns/test_init.py +++ b/tests/components/nextdns/test_init.py @@ -3,14 +3,17 @@ from unittest.mock import patch from nextdns import ApiError -from homeassistant.components.nextdns.const import DOMAIN +from homeassistant.components.nextdns.const import CONF_PROFILE_ID, DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.const import CONF_API_KEY, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant from . import init_integration +from tests.common import MockConfigEntry -async def test_async_setup_entry(hass): + +async def test_async_setup_entry(hass: HomeAssistant) -> None: """Test a successful setup entry.""" await init_integration(hass) @@ -20,9 +23,14 @@ async def test_async_setup_entry(hass): assert state.state == "20.0" -async def test_config_not_ready(hass): +async def test_config_not_ready(hass: HomeAssistant) -> None: """Test for setup failure if the connection to the service fails.""" - entry = await init_integration(hass, add_to_hass=False) + entry = MockConfigEntry( + domain=DOMAIN, + title="Fake Profile", + unique_id="xyz12", + data={CONF_API_KEY: "fake_api_key", CONF_PROFILE_ID: "xyz12"}, + ) with patch( "homeassistant.components.nextdns.NextDns.get_profiles", @@ -33,7 +41,7 @@ async def test_config_not_ready(hass): assert entry.state is ConfigEntryState.SETUP_RETRY -async def test_unload_entry(hass): +async def test_unload_entry(hass: HomeAssistant) -> None: """Test successful unload of entry.""" entry = await init_integration(hass) diff --git a/tests/components/nextdns/test_sensor.py b/tests/components/nextdns/test_sensor.py index 731f98203ab..a90999a592b 100644 --- a/tests/components/nextdns/test_sensor.py +++ b/tests/components/nextdns/test_sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow @@ -19,7 +20,7 @@ from . import DNSSEC, ENCRYPTION, IP_VERSIONS, PROTOCOLS, STATUS, init_integrati from tests.common import async_fire_time_changed -async def test_sensor(hass): +async def test_sensor(hass: HomeAssistant) -> None: """Test states of sensors.""" registry = er.async_get(hass) @@ -390,7 +391,7 @@ async def test_sensor(hass): assert entry.unique_id == "xyz12_udp_queries_ratio" -async def test_availability(hass): +async def test_availability(hass: HomeAssistant) -> None: """Ensure that we mark the entities unavailable correctly when service causes an error.""" registry = er.async_get(hass) diff --git a/tests/components/nextdns/test_system_health.py b/tests/components/nextdns/test_system_health.py index 4fc3a1a7e72..14d447947c1 100644 --- a/tests/components/nextdns/test_system_health.py +++ b/tests/components/nextdns/test_system_health.py @@ -5,12 +5,16 @@ from aiohttp import ClientError from nextdns.const import API_ENDPOINT from homeassistant.components.nextdns.const import DOMAIN +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from tests.common import get_system_health_info +from tests.test_util.aiohttp import AiohttpClientMocker -async def test_nextdns_system_health(hass, aioclient_mock): +async def test_nextdns_system_health( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test NextDNS system health.""" aioclient_mock.get(API_ENDPOINT, text="") hass.config.components.add(DOMAIN) @@ -25,7 +29,9 @@ async def test_nextdns_system_health(hass, aioclient_mock): assert info == {"can_reach_server": "ok"} -async def test_nextdns_system_health_fail(hass, aioclient_mock): +async def test_nextdns_system_health_fail( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: """Test NextDNS system health.""" aioclient_mock.get(API_ENDPOINT, exc=ClientError) hass.config.components.add(DOMAIN) From 326e05dcf1fdc524abdde92ccae98612bdfe15bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 Jul 2022 18:12:08 -0500 Subject: [PATCH 2796/3516] Fix async_get_scanner to return the correct bluetooth scanner (#75637) --- .../components/bluetooth/__init__.py | 15 +-- tests/components/bluetooth/__init__.py | 7 ++ tests/components/bluetooth/test_init.py | 95 ++++++++++--------- .../test_passive_update_coordinator.py | 5 +- 4 files changed, 70 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 3c7f2393257..a50be7f4ace 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -35,7 +35,7 @@ from homeassistant.loader import ( from . import models from .const import DOMAIN -from .models import HaBleakScanner +from .models import HaBleakScanner, HaBleakScannerWrapper from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher _LOGGER = logging.getLogger(__name__) @@ -117,8 +117,12 @@ BluetoothCallback = Callable[ @hass_callback -def async_get_scanner(hass: HomeAssistant) -> HaBleakScanner: - """Return a HaBleakScanner.""" +def async_get_scanner(hass: HomeAssistant) -> HaBleakScannerWrapper: + """Return a HaBleakScannerWrapper. + + This is a wrapper around our BleakScanner singleton that allows + multiple integrations to share the same BleakScanner. + """ if DOMAIN not in hass.data: raise RuntimeError("Bluetooth integration not loaded") manager: BluetoothManager = hass.data[DOMAIN] @@ -320,10 +324,9 @@ class BluetoothManager: models.HA_BLEAK_SCANNER = self.scanner = HaBleakScanner() @hass_callback - def async_get_scanner(self) -> HaBleakScanner: + def async_get_scanner(self) -> HaBleakScannerWrapper: """Get the scanner.""" - assert self.scanner is not None - return self.scanner + return HaBleakScannerWrapper() async def async_start(self, scanning_mode: BluetoothScanningMode) -> None: """Set up BT Discovery.""" diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index 6bf53afcd1e..3dc80d55590 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -1 +1,8 @@ """Tests for the Bluetooth integration.""" + +from homeassistant.components.bluetooth import models + + +def _get_underlying_scanner(): + """Return the underlying scanner that has been wrapped.""" + return models.HA_BLEAK_SCANNER diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 8aef5f3ddbb..5d932c56349 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -12,7 +12,6 @@ from homeassistant.components.bluetooth import ( UNAVAILABLE_TRACK_SECONDS, BluetoothChange, BluetoothServiceInfo, - async_get_scanner, async_track_unavailable, models, ) @@ -21,6 +20,8 @@ from homeassistant.core import callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from . import _get_underlying_scanner + from tests.common import MockConfigEntry, async_fire_time_changed @@ -135,7 +136,7 @@ async def test_discovery_match_by_service_uuid( wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - async_get_scanner(hass)._callback(wrong_device, wrong_adv) + _get_underlying_scanner()._callback(wrong_device, wrong_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -145,7 +146,7 @@ async def test_discovery_match_by_service_uuid( local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -172,7 +173,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - async_get_scanner(hass)._callback(wrong_device, wrong_adv) + _get_underlying_scanner()._callback(wrong_device, wrong_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -180,7 +181,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -219,7 +220,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( manufacturer_data={76: b"\x06\x02\x03\x99"}, ) - async_get_scanner(hass)._callback(hkc_device, hkc_adv) + _get_underlying_scanner()._callback(hkc_device, hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -227,7 +228,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( mock_config_flow.reset_mock() # 2nd discovery should not generate another flow - async_get_scanner(hass)._callback(hkc_device, hkc_adv) + _get_underlying_scanner()._callback(hkc_device, hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -238,7 +239,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( local_name="lock", service_uuids=[], manufacturer_data={76: b"\x02"} ) - async_get_scanner(hass)._callback(not_hkc_device, not_hkc_adv) + _get_underlying_scanner()._callback(not_hkc_device, not_hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -247,7 +248,7 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( local_name="lock", service_uuids=[], manufacturer_data={21: b"\x02"} ) - async_get_scanner(hass)._callback(not_apple_device, not_apple_adv) + _get_underlying_scanner()._callback(not_apple_device, not_apple_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -279,10 +280,10 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): wrong_device = BLEDevice("44:44:33:11:23:42", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - async_get_scanner(hass)._callback(wrong_device, wrong_adv) + _get_underlying_scanner()._callback(wrong_device, wrong_adv) switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) wrong_device_went_unavailable = False switchbot_device_went_unavailable = False @@ -316,8 +317,8 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): assert wrong_device_went_unavailable is True # See the devices again - async_get_scanner(hass)._callback(wrong_device, wrong_adv) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(wrong_device, wrong_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) # Cancel the callbacks wrong_device_unavailable_cancel() switchbot_device_unavailable_cancel() @@ -382,25 +383,25 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) await hass.async_block_till_done() empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") # 3rd callback raises ValueError but is still tracked - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) await hass.async_block_till_done() cancel() # 4th callback should not be tracked since we canceled - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) await hass.async_block_till_done() assert len(callbacks) == 3 @@ -467,25 +468,25 @@ async def test_register_callback_by_address( service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) await hass.async_block_till_done() empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") # 3rd callback raises ValueError but is still tracked - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) await hass.async_block_till_done() cancel() # 4th callback should not be tracked since we canceled - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) await hass.async_block_till_done() # Now register again with a callback that fails to @@ -549,15 +550,15 @@ async def test_wrapped_instance_with_filter( empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - assert async_get_scanner(hass) is not None + assert _get_underlying_scanner() is not None scanner = models.HaBleakScannerWrapper( filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} ) scanner.register_detection_callback(_device_detected) mock_discovered = [MagicMock()] - type(async_get_scanner(hass)).discovered_devices = mock_discovered - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + type(_get_underlying_scanner()).discovered_devices = mock_discovered + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() discovered = await scanner.discover(timeout=0) @@ -572,17 +573,17 @@ async def test_wrapped_instance_with_filter( # We should get a reply from the history when we register again assert len(detected) == 3 - type(async_get_scanner(hass)).discovered_devices = [] + type(_get_underlying_scanner()).discovered_devices = [] discovered = await scanner.discover(timeout=0) assert len(discovered) == 0 assert discovered == [] - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) assert len(detected) == 4 # The filter we created in the wrapped scanner with should be respected # and we should not get another callback - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) assert len(detected) == 4 @@ -620,22 +621,22 @@ async def test_wrapped_instance_with_service_uuids( empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - assert async_get_scanner(hass) is not None + assert _get_underlying_scanner() is not None scanner = models.HaBleakScannerWrapper( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) scanner.register_detection_callback(_device_detected) - type(async_get_scanner(hass)).discovered_devices = [MagicMock()] + type(_get_underlying_scanner()).discovered_devices = [MagicMock()] for _ in range(2): - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 2 # The UUIDs list we created in the wrapped scanner with should be respected # and we should not get another callback - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) assert len(detected) == 2 @@ -673,15 +674,15 @@ async def test_wrapped_instance_with_broken_callbacks( service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - assert async_get_scanner(hass) is not None + assert _get_underlying_scanner() is not None scanner = models.HaBleakScannerWrapper( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) scanner.register_detection_callback(_device_detected) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 1 @@ -719,23 +720,23 @@ async def test_wrapped_instance_changes_uuids( empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - assert async_get_scanner(hass) is not None + assert _get_underlying_scanner() is not None scanner = models.HaBleakScannerWrapper() scanner.set_scanning_filter( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) scanner.register_detection_callback(_device_detected) - type(async_get_scanner(hass)).discovered_devices = [MagicMock()] + type(_get_underlying_scanner()).discovered_devices = [MagicMock()] for _ in range(2): - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 2 # The UUIDs list we created in the wrapped scanner with should be respected # and we should not get another callback - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) assert len(detected) == 2 @@ -772,23 +773,23 @@ async def test_wrapped_instance_changes_filters( empty_device = BLEDevice("11:22:33:44:55:62", "empty") empty_adv = AdvertisementData(local_name="empty") - assert async_get_scanner(hass) is not None + assert _get_underlying_scanner() is not None scanner = models.HaBleakScannerWrapper() scanner.set_scanning_filter( filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} ) scanner.register_detection_callback(_device_detected) - type(async_get_scanner(hass)).discovered_devices = [MagicMock()] + type(_get_underlying_scanner()).discovered_devices = [MagicMock()] for _ in range(2): - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 2 # The UUIDs list we created in the wrapped scanner with should be respected # and we should not get another callback - async_get_scanner(hass)._callback(empty_device, empty_adv) + _get_underlying_scanner()._callback(empty_device, empty_adv) assert len(detected) == 2 @@ -807,7 +808,7 @@ async def test_wrapped_instance_unsupported_filter( with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert async_get_scanner(hass) is not None + assert _get_underlying_scanner() is not None scanner = models.HaBleakScannerWrapper() scanner.set_scanning_filter( filters={ @@ -845,7 +846,7 @@ async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - async_get_scanner(hass)._callback(switchbot_device, switchbot_adv) + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert ( @@ -935,3 +936,9 @@ async def test_raising_runtime_error_when_no_bluetooth(hass): """Test we raise an exception if we try to get the scanner when its not there.""" with pytest.raises(RuntimeError): bluetooth.async_get_scanner(hass) + + +async def test_getting_the_scanner_returns_the_wrapped_instance(hass, enable_bluetooth): + """Test getting the scanner returns the wrapped instance.""" + scanner = bluetooth.async_get_scanner(hass) + assert isinstance(scanner, models.HaBleakScannerWrapper) diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 010989628e1..48f2e8edf06 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -12,7 +12,6 @@ from homeassistant.components.bluetooth import ( DOMAIN, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, - async_get_scanner, ) from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, @@ -27,6 +26,8 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util +from . import _get_underlying_scanner + from tests.common import MockEntityPlatform, async_fire_time_changed _LOGGER = logging.getLogger(__name__) @@ -208,7 +209,7 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True - scanner = async_get_scanner(hass) + scanner = _get_underlying_scanner() with patch( "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", From 8e8612447021848bc6c5f69b07255fdcd17a2b90 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 23 Jul 2022 00:25:17 +0000 Subject: [PATCH 2797/3516] [ci skip] Translation update --- .../components/bluetooth/translations/de.json | 16 ++++++++++++++ .../components/bluetooth/translations/el.json | 16 ++++++++++++++ .../components/bluetooth/translations/id.json | 16 ++++++++++++++ .../components/bluetooth/translations/it.json | 16 ++++++++++++++ .../components/bluetooth/translations/ja.json | 16 ++++++++++++++ .../components/bluetooth/translations/pl.json | 16 ++++++++++++++ .../bluetooth/translations/pt-BR.json | 16 ++++++++++++++ .../bluetooth/translations/zh-Hant.json | 16 ++++++++++++++ .../components/demo/translations/el.json | 21 +++++++++++++++++++ .../components/demo/translations/id.json | 21 +++++++++++++++++++ .../components/demo/translations/it.json | 5 +++++ .../components/demo/translations/ja.json | 21 +++++++++++++++++++ .../components/google/translations/el.json | 3 ++- .../homekit_controller/translations/it.json | 2 +- .../components/inkbird/translations/de.json | 21 +++++++++++++++++++ .../components/inkbird/translations/el.json | 21 +++++++++++++++++++ .../components/inkbird/translations/id.json | 21 +++++++++++++++++++ .../components/inkbird/translations/ja.json | 21 +++++++++++++++++++ .../components/inkbird/translations/pl.json | 21 +++++++++++++++++++ .../inkbird/translations/pt-BR.json | 21 +++++++++++++++++++ .../inkbird/translations/zh-Hant.json | 21 +++++++++++++++++++ .../components/lifx/translations/el.json | 20 ++++++++++++++++++ .../components/lifx/translations/id.json | 20 ++++++++++++++++++ .../components/lifx/translations/ja.json | 20 ++++++++++++++++++ .../components/plugwise/translations/el.json | 3 ++- .../components/plugwise/translations/id.json | 3 ++- .../components/plugwise/translations/ja.json | 3 ++- .../sensorpush/translations/de.json | 21 +++++++++++++++++++ .../sensorpush/translations/el.json | 21 +++++++++++++++++++ .../sensorpush/translations/id.json | 21 +++++++++++++++++++ .../sensorpush/translations/ja.json | 21 +++++++++++++++++++ .../sensorpush/translations/pl.json | 21 +++++++++++++++++++ .../sensorpush/translations/pt-BR.json | 21 +++++++++++++++++++ .../sensorpush/translations/zh-Hant.json | 21 +++++++++++++++++++ .../components/uscis/translations/de.json | 8 +++++++ .../components/uscis/translations/el.json | 8 +++++++ .../components/uscis/translations/id.json | 8 +++++++ .../components/uscis/translations/it.json | 8 +++++++ .../components/uscis/translations/ja.json | 7 +++++++ .../components/verisure/translations/el.json | 15 ++++++++++++- .../components/withings/translations/it.json | 3 +++ .../components/zha/translations/de.json | 1 + .../components/zha/translations/el.json | 1 + .../components/zha/translations/id.json | 1 + .../components/zha/translations/pl.json | 1 + .../components/zha/translations/zh-Hant.json | 1 + 46 files changed, 620 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/bluetooth/translations/de.json create mode 100644 homeassistant/components/bluetooth/translations/el.json create mode 100644 homeassistant/components/bluetooth/translations/id.json create mode 100644 homeassistant/components/bluetooth/translations/it.json create mode 100644 homeassistant/components/bluetooth/translations/ja.json create mode 100644 homeassistant/components/bluetooth/translations/pl.json create mode 100644 homeassistant/components/bluetooth/translations/pt-BR.json create mode 100644 homeassistant/components/bluetooth/translations/zh-Hant.json create mode 100644 homeassistant/components/inkbird/translations/de.json create mode 100644 homeassistant/components/inkbird/translations/el.json create mode 100644 homeassistant/components/inkbird/translations/id.json create mode 100644 homeassistant/components/inkbird/translations/ja.json create mode 100644 homeassistant/components/inkbird/translations/pl.json create mode 100644 homeassistant/components/inkbird/translations/pt-BR.json create mode 100644 homeassistant/components/inkbird/translations/zh-Hant.json create mode 100644 homeassistant/components/sensorpush/translations/de.json create mode 100644 homeassistant/components/sensorpush/translations/el.json create mode 100644 homeassistant/components/sensorpush/translations/id.json create mode 100644 homeassistant/components/sensorpush/translations/ja.json create mode 100644 homeassistant/components/sensorpush/translations/pl.json create mode 100644 homeassistant/components/sensorpush/translations/pt-BR.json create mode 100644 homeassistant/components/sensorpush/translations/zh-Hant.json create mode 100644 homeassistant/components/uscis/translations/de.json create mode 100644 homeassistant/components/uscis/translations/el.json create mode 100644 homeassistant/components/uscis/translations/id.json create mode 100644 homeassistant/components/uscis/translations/it.json create mode 100644 homeassistant/components/uscis/translations/ja.json diff --git a/homeassistant/components/bluetooth/translations/de.json b/homeassistant/components/bluetooth/translations/de.json new file mode 100644 index 00000000000..1c677be6828 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/de.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/el.json b/homeassistant/components/bluetooth/translations/el.json new file mode 100644 index 00000000000..05198f0929a --- /dev/null +++ b/homeassistant/components/bluetooth/translations/el.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/id.json b/homeassistant/components/bluetooth/translations/id.json new file mode 100644 index 00000000000..12653696241 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/id.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/it.json b/homeassistant/components/bluetooth/translations/it.json new file mode 100644 index 00000000000..12d79b3efff --- /dev/null +++ b/homeassistant/components/bluetooth/translations/it.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/ja.json b/homeassistant/components/bluetooth/translations/ja.json new file mode 100644 index 00000000000..f42ea56e2c8 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/ja.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/pl.json b/homeassistant/components/bluetooth/translations/pl.json new file mode 100644 index 00000000000..4b93cce9f7e --- /dev/null +++ b/homeassistant/components/bluetooth/translations/pl.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/pt-BR.json b/homeassistant/components/bluetooth/translations/pt-BR.json new file mode 100644 index 00000000000..9cafa844652 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/pt-BR.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/zh-Hant.json b/homeassistant/components/bluetooth/translations/zh-Hant.json new file mode 100644 index 00000000000..3f92640469e --- /dev/null +++ b/homeassistant/components/bluetooth/translations/zh-Hant.json @@ -0,0 +1,16 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/el.json b/homeassistant/components/demo/translations/el.json index 34afbe9df01..c4c539034bd 100644 --- a/homeassistant/components/demo/translations/el.json +++ b/homeassistant/components/demo/translations/el.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u03a0\u03b9\u03ad\u03c3\u03c4\u03b5 OK \u03cc\u03c4\u03b1\u03bd \u03c4\u03bf \u03c5\u03b3\u03c1\u03cc \u03c4\u03c9\u03bd \u03c6\u03bb\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03be\u03b1\u03bd\u03b1\u03b3\u03b5\u03bc\u03af\u03c3\u03b5\u03b9.", + "title": "\u03a4\u03bf \u03c5\u03b3\u03c1\u03cc \u03c4\u03c9\u03bd \u03c6\u03bb\u03b1\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03b1\u03bd\u03b1\u03b3\u03b5\u03bc\u03af\u03c3\u03b5\u03b9" + } + } + }, + "title": "\u03a4\u03bf \u03c5\u03b3\u03c1\u03cc \u03c4\u03c9\u03bd \u03c6\u03bb\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ac\u03b4\u03b5\u03b9\u03bf \u03ba\u03b1\u03b9 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03b1\u03bd\u03b1\u03b3\u03b5\u03bc\u03af\u03c3\u03b5\u03b9." + }, + "transmogrifier_deprecated": { + "description": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf transmogrifier \u03ad\u03c7\u03b5\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af \u03bb\u03cc\u03b3\u03c9 \u03c4\u03b7\u03c2 \u03ad\u03bb\u03bb\u03b5\u03b9\u03c8\u03b7\u03c2 \u03c4\u03bf\u03c0\u03b9\u03ba\u03bf\u03cd \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c0\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03bf\u03c2 \u03c3\u03c4\u03bf \u03bd\u03ad\u03bf API", + "title": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03c4\u03bf\u03c5 transmogrifier \u03b1\u03c0\u03bf\u03c3\u03cd\u03c1\u03b5\u03c4\u03b1\u03b9" + }, + "unfixable_problem": { + "description": "\u0391\u03c5\u03c4\u03cc \u03c4\u03bf \u03b8\u03ad\u03bc\u03b1 \u03b4\u03b5\u03bd \u03c0\u03c1\u03cc\u03ba\u03b5\u03b9\u03c4\u03b1\u03b9 \u03c0\u03bf\u03c4\u03ad \u03bd\u03b1 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03bb\u03b5\u03af\u03c8\u03b5\u03b9.", + "title": "\u0391\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1 \u03b4\u03b5\u03bd \u03b5\u03c0\u03b9\u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03bd\u03b5\u03c4\u03b1\u03b9" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/id.json b/homeassistant/components/demo/translations/id.json index 8adbeb3e3c4..e8f827e2b86 100644 --- a/homeassistant/components/demo/translations/id.json +++ b/homeassistant/components/demo/translations/id.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Tekan Oke saat cairan blinker telah diisi ulang", + "title": "Cairan blinker perlu diisi ulang" + } + } + }, + "title": "Cairan blinker kosong dan perlu diisi ulang" + }, + "transmogrifier_deprecated": { + "description": "Komponen transmogrifier tidak akan digunakan lagi karena tidak tersedianya kontrol lokal yang tersedia di API baru", + "title": "Komponen transmogrifier tidak akan digunakan lagi" + }, + "unfixable_problem": { + "description": "Masalah ini akan terus terjadi.", + "title": "Masalah ini tidak dapat diatasi." + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/it.json b/homeassistant/components/demo/translations/it.json index 1c94dea053f..7fc00caf26e 100644 --- a/homeassistant/components/demo/translations/it.json +++ b/homeassistant/components/demo/translations/it.json @@ -1,4 +1,9 @@ { + "issues": { + "unfixable_problem": { + "title": "Questo non \u00e8 un problema risolvibile" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/demo/translations/ja.json b/homeassistant/components/demo/translations/ja.json index e543a83bfe5..30467e3df5b 100644 --- a/homeassistant/components/demo/translations/ja.json +++ b/homeassistant/components/demo/translations/ja.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u30d6\u30ea\u30f3\u30ab\u30fc\u6db2\u306e\u88dc\u5145\u304c\u5b8c\u4e86\u3057\u305f\u3089\u3001OK\u3092\u62bc\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "\u30d6\u30ea\u30f3\u30ab\u30fc\u6db2\u3092\u88dc\u5145\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" + } + } + }, + "title": "\u30d6\u30ea\u30f3\u30ab\u30fc\u6db2\u304c\u7a7a\u306b\u306a\u3063\u305f\u306e\u3067\u3001\u88dc\u5145\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" + }, + "transmogrifier_deprecated": { + "description": "\u65b0\u3057\u3044API\u3067\u5229\u7528\u3067\u304d\u308b\u30ed\u30fc\u30ab\u30eb\u5236\u5fa1\u304c\u306a\u3044\u305f\u3081\u3001transmogrifier\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306f\u3001\u975e\u63a8\u5968\u306b\u306a\u308a\u307e\u3057\u305f", + "title": "transmogrifier\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u306f\u3001\u975e\u63a8\u5968\u306b\u306a\u308a\u307e\u3057\u305f" + }, + "unfixable_problem": { + "description": "\u3053\u306e\u554f\u984c\u306f\u6c7a\u3057\u3066\u3042\u304d\u3089\u3081\u308b\u3064\u3082\u308a\u306f\u3042\u308a\u307e\u305b\u3093\u3002", + "title": "\u3053\u308c\u306f\u3001\u4fee\u6b63\u53ef\u80fd\u306a\u554f\u984c\u3067\u306f\u3042\u308a\u307e\u305b\u3093" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/google/translations/el.json b/homeassistant/components/google/translations/el.json index 65cc5a0038d..21e5580da3d 100644 --- a/homeassistant/components/google/translations/el.json +++ b/homeassistant/components/google/translations/el.json @@ -11,7 +11,8 @@ "invalid_access_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "missing_configuration": "\u03a4\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", "oauth_error": "\u039b\u03ae\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd.", - "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "timeout_connect": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "create_entry": { "default": "\u0395\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" diff --git a/homeassistant/components/homekit_controller/translations/it.json b/homeassistant/components/homekit_controller/translations/it.json index 947e03374b4..5c71b00a801 100644 --- a/homeassistant/components/homekit_controller/translations/it.json +++ b/homeassistant/components/homekit_controller/translations/it.json @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Consenti l'associazione con codici di installazione non sicuri.", "pairing_code": "Codice di abbinamento" }, - "description": "Il controller HomeKit comunica con {name} sulla rete locale utilizzando una connessione cifrata sicura senza un controller HomeKit separato o iCloud. Inserisci il tuo codice di associazione HomeKit (nel formato XXX-XX-XXX) per utilizzare questo accessorio. Questo codice si trova solitamente sul dispositivo stesso o nella confezione.", + "description": "Il controller HomeKit comunica con {name} ({category}) sulla rete locale utilizzando una connessione cifrata sicura senza un controller HomeKit separato o iCloud. Inserisci il tuo codice di associazione HomeKit (nel formato XXX-XX-XXX) per utilizzare questo accessorio. Questo codice si trova solitamente sul dispositivo stesso o nella confezione.", "title": "Associazione con un dispositivo tramite il Protocollo degli Accessori HomeKit" }, "protocol_error": { diff --git a/homeassistant/components/inkbird/translations/de.json b/homeassistant/components/inkbird/translations/de.json new file mode 100644 index 00000000000..81dda510bc5 --- /dev/null +++ b/homeassistant/components/inkbird/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/el.json b/homeassistant/components/inkbird/translations/el.json new file mode 100644 index 00000000000..0a802a0bc89 --- /dev/null +++ b/homeassistant/components/inkbird/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/id.json b/homeassistant/components/inkbird/translations/id.json new file mode 100644 index 00000000000..07426a0e290 --- /dev/null +++ b/homeassistant/components/inkbird/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/ja.json b/homeassistant/components/inkbird/translations/ja.json new file mode 100644 index 00000000000..38f862bd2f6 --- /dev/null +++ b/homeassistant/components/inkbird/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/pl.json b/homeassistant/components/inkbird/translations/pl.json new file mode 100644 index 00000000000..51168716783 --- /dev/null +++ b/homeassistant/components/inkbird/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/pt-BR.json b/homeassistant/components/inkbird/translations/pt-BR.json new file mode 100644 index 00000000000..2067d7f9312 --- /dev/null +++ b/homeassistant/components/inkbird/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/zh-Hant.json b/homeassistant/components/inkbird/translations/zh-Hant.json new file mode 100644 index 00000000000..d4eaa8cb41f --- /dev/null +++ b/homeassistant/components/inkbird/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/el.json b/homeassistant/components/lifx/translations/el.json index 5a1d7707f55..4ebea49190d 100644 --- a/homeassistant/components/lifx/translations/el.json +++ b/homeassistant/components/lifx/translations/el.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf LIFX;" + }, + "discovery_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {label} ({host}) {serial};" + }, + "pick_device": { + "data": { + "device": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + } + }, + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "description": "\u0391\u03bd \u03b1\u03c6\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03ba\u03b5\u03bd\u03cc, \u03b7 \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd." } } } diff --git a/homeassistant/components/lifx/translations/id.json b/homeassistant/components/lifx/translations/id.json index 03b2b387c6f..8781581bb0f 100644 --- a/homeassistant/components/lifx/translations/id.json +++ b/homeassistant/components/lifx/translations/id.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Ingin menyiapkan LIFX?" + }, + "discovery_confirm": { + "description": "Ingin menyiapkan {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Perangkat" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Jika host dibiarkan kosong, proses penemuan akan digunakan untuk menemukan perangkat." } } } diff --git a/homeassistant/components/lifx/translations/ja.json b/homeassistant/components/lifx/translations/ja.json index 1945c3112f0..c3b144222a4 100644 --- a/homeassistant/components/lifx/translations/ja.json +++ b/homeassistant/components/lifx/translations/ja.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "LIFX\u306e\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u304b\uff1f" + }, + "discovery_confirm": { + "description": "{label} ({host}) {serial} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b?" + }, + "pick_device": { + "data": { + "device": "\u30c7\u30d0\u30a4\u30b9" + } + }, + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + }, + "description": "\u30db\u30b9\u30c8\u3092\u7a7a\u767d\u306b\u3057\u3066\u304a\u304f\u3068\u3001\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc\u3092\u4f7f\u3063\u3066\u30c7\u30d0\u30a4\u30b9\u3092\u691c\u7d22\u3057\u307e\u3059\u3002" } } } diff --git a/homeassistant/components/plugwise/translations/el.json b/homeassistant/components/plugwise/translations/el.json index caee7bb5a88..18a50e86b66 100644 --- a/homeassistant/components/plugwise/translations/el.json +++ b/homeassistant/components/plugwise/translations/el.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "anna_with_adam": "\u03a4\u03cc\u03c3\u03bf \u03b7 \u0386\u03bd\u03bd\u03b1 \u03cc\u03c3\u03bf \u03ba\u03b1\u03b9 \u03bf \u0391\u03b4\u03ac\u03bc \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b1\u03bd. \u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u0391\u03b4\u03ac\u03bc \u03c3\u03b1\u03c2 \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u0386\u03bd\u03bd\u03b1 \u03c3\u03b1\u03c2" }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/plugwise/translations/id.json b/homeassistant/components/plugwise/translations/id.json index 22c871e4f83..daa2824df27 100644 --- a/homeassistant/components/plugwise/translations/id.json +++ b/homeassistant/components/plugwise/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Layanan sudah dikonfigurasi" + "already_configured": "Layanan sudah dikonfigurasi", + "anna_with_adam": "Baik Anna dan Adam terdeteksi. Tambahkan Adam, bukan Anna" }, "error": { "cannot_connect": "Gagal terhubung", diff --git a/homeassistant/components/plugwise/translations/ja.json b/homeassistant/components/plugwise/translations/ja.json index 0103774c138..15eb388d2c8 100644 --- a/homeassistant/components/plugwise/translations/ja.json +++ b/homeassistant/components/plugwise/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "anna_with_adam": "\u30a2\u30f3\u30ca\u3068\u30a2\u30c0\u30e0\u306e\u4e21\u65b9\u3092\u691c\u51fa\u3057\u307e\u3057\u305f\u3002\u30a2\u30f3\u30ca\u306e\u4ee3\u308f\u308a\u306b\u30a2\u30c0\u30e0\u3092\u8ffd\u52a0" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", diff --git a/homeassistant/components/sensorpush/translations/de.json b/homeassistant/components/sensorpush/translations/de.json new file mode 100644 index 00000000000..81dda510bc5 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/el.json b/homeassistant/components/sensorpush/translations/el.json new file mode 100644 index 00000000000..0a802a0bc89 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/id.json b/homeassistant/components/sensorpush/translations/id.json new file mode 100644 index 00000000000..07426a0e290 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/ja.json b/homeassistant/components/sensorpush/translations/ja.json new file mode 100644 index 00000000000..38f862bd2f6 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/pl.json b/homeassistant/components/sensorpush/translations/pl.json new file mode 100644 index 00000000000..51168716783 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/pt-BR.json b/homeassistant/components/sensorpush/translations/pt-BR.json new file mode 100644 index 00000000000..2067d7f9312 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/zh-Hant.json b/homeassistant/components/sensorpush/translations/zh-Hant.json new file mode 100644 index 00000000000..d4eaa8cb41f --- /dev/null +++ b/homeassistant/components/sensorpush/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/de.json b/homeassistant/components/uscis/translations/de.json new file mode 100644 index 00000000000..273a340d892 --- /dev/null +++ b/homeassistant/components/uscis/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Die Integration der U.S. Citizenship and Immigration Services (USCIS) wird aus Home Assistant entfernt und steht ab Home Assistant 2022.10 nicht mehr zur Verf\u00fcgung.\n\nDie Integration wird entfernt, weil sie auf Webscraping beruht, was nicht erlaubt ist.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die USCIS-Integration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/el.json b/homeassistant/components/uscis/translations/el.json new file mode 100644 index 00000000000..d89ae50773a --- /dev/null +++ b/homeassistant/components/uscis/translations/el.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03a5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd \u0399\u03b8\u03b1\u03b3\u03ad\u03bd\u03b5\u03b9\u03b1\u03c2 \u03ba\u03b1\u03b9 \u039c\u03b5\u03c4\u03b1\u03bd\u03ac\u03c3\u03c4\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c4\u03c9\u03bd \u0397\u03a0\u0391 (USCIS) \u03b5\u03ba\u03ba\u03c1\u03b5\u03bc\u03b5\u03af \u03c0\u03c1\u03bf\u03c2 \u03b1\u03c6\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant 2022.10.\n\n\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03af\u03c4\u03b1\u03b9, \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03b2\u03b1\u03c3\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 webscraping, \u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03b4\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9.\n\n\u0391\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 USCIS \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/id.json b/homeassistant/components/uscis/translations/id.json new file mode 100644 index 00000000000..37e7278a916 --- /dev/null +++ b/homeassistant/components/uscis/translations/id.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integrasi Layanan Kewarganegaraan dan Imigrasi AS (USCIS) sedang menunggu penghapusan dari Home Assistant dan tidak akan lagi tersedia pada Home Assistant 2022.10.\n\nIntegrasi ini dalam proses penghapusan, karena bergantung pada proses webscraping, yang tidak diizinkan.\n\nHapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Integrasi USCIS dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/it.json b/homeassistant/components/uscis/translations/it.json new file mode 100644 index 00000000000..1e23b69eee5 --- /dev/null +++ b/homeassistant/components/uscis/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "L'integrazione U.S. Citizenship and Immigration Services (USCIS) \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\n L'integrazione verr\u00e0 rimossa, perch\u00e9 si basa sul webscraping, che non \u00e8 consentito. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "L'integrazione USCIS verr\u00e0 rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/ja.json b/homeassistant/components/uscis/translations/ja.json new file mode 100644 index 00000000000..b5abb7e0825 --- /dev/null +++ b/homeassistant/components/uscis/translations/ja.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "USCIS\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/el.json b/homeassistant/components/verisure/translations/el.json index 7d46b4ed96b..fc9e4cc52df 100644 --- a/homeassistant/components/verisure/translations/el.json +++ b/homeassistant/components/verisure/translations/el.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "unknown_mfa": "\u0395\u03bc\u03c6\u03b1\u03bd\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ac\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 MFA" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "\u03a4\u03bf Home Assistant \u03b2\u03c1\u03ae\u03ba\u03b5 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ad\u03c2 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2 Verisure \u03c3\u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 My Pages. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce, \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03bf Home Assistant." }, + "mfa": { + "data": { + "code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7\u03c2", + "description": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03c3\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7 \u03c3\u03b5 2 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1. \u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c3\u03b1\u03c2 \u03c3\u03c4\u03ad\u03bb\u03bd\u03b5\u03b9 \u03b7 Verisure." + } + }, "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 \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Verisure My Pages.", @@ -22,6 +29,12 @@ "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03a0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" } }, + "reauth_mfa": { + "data": { + "code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7\u03c2", + "description": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03c3\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7 \u03c3\u03b5 2 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1. \u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03c0\u03b1\u03bb\u03ae\u03b8\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c3\u03b1\u03c2 \u03c3\u03c4\u03ad\u03bb\u03bd\u03b5\u03b9 \u03b7 Verisure." + } + }, "user": { "data": { "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03bc\u03b5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 Verisure My Pages.", diff --git a/homeassistant/components/withings/translations/it.json b/homeassistant/components/withings/translations/it.json index 079acb0b503..412b75fc1b5 100644 --- a/homeassistant/components/withings/translations/it.json +++ b/homeassistant/components/withings/translations/it.json @@ -27,6 +27,9 @@ "reauth": { "description": "Il profilo \"{profile}\" deve essere autenticato nuovamente per continuare a ricevere i dati Withings.", "title": "Autentica nuovamente l'integrazione" + }, + "reauth_confirm": { + "description": "Il profilo \" {profile} \" deve essere riautenticato per poter continuare a ricevere i dati Withings." } } } diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 88638c6c696..9b8acdf5b87 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "Netzbetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)", "default_light_transition": "Standardlicht\u00fcbergangszeit (Sekunden)", "enable_identify_on_join": "Aktiviere den Identifikationseffekt, wenn Ger\u00e4te dem Netzwerk beitreten", + "enhanced_light_transition": "Aktiviere einen verbesserten Lichtfarben-/Temperatur\u00fcbergang aus einem ausgeschalteten Zustand", "title": "Globale Optionen" } }, diff --git a/homeassistant/components/zha/translations/el.json b/homeassistant/components/zha/translations/el.json index e0fb76cd6cb..e0e063df426 100644 --- a/homeassistant/components/zha/translations/el.json +++ b/homeassistant/components/zha/translations/el.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "\u0398\u03b5\u03c9\u03c1\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03c9\u03c2 \u03bc\u03b7 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b5\u03c2 \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)", "default_light_transition": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c6\u03c9\u03c4\u03cc\u03c2 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)", "enable_identify_on_join": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03c6\u03ad \u03b1\u03bd\u03b1\u03b3\u03bd\u03ce\u03c1\u03b9\u03c3\u03b7\u03c2 \u03cc\u03c4\u03b1\u03bd \u03bf\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c5\u03bd\u03b4\u03ad\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "enhanced_light_transition": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b2\u03b5\u03bb\u03c4\u03b9\u03c9\u03bc\u03ad\u03bd\u03b7 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7 \u03c7\u03c1\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c6\u03c9\u03c4\u03cc\u03c2/\u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03b1\u03c0\u03cc \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2", "title": "\u039a\u03b1\u03b8\u03bf\u03bb\u03b9\u03ba\u03ad\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2" } }, diff --git a/homeassistant/components/zha/translations/id.json b/homeassistant/components/zha/translations/id.json index 5a10e3d01af..63eee2f376b 100644 --- a/homeassistant/components/zha/translations/id.json +++ b/homeassistant/components/zha/translations/id.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "Anggap perangkat bertenaga listrik sebagai tidak tersedia setelah (detik)", "default_light_transition": "Waktu transisi lampu default (detik)", "enable_identify_on_join": "Aktifkan efek identifikasi saat perangkat bergabung dengan jaringan", + "enhanced_light_transition": "Aktifkan versi canggih untuk transisi warna/suhu cahaya dari keadaan tidak aktif", "title": "Opsi Global" } }, diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index 6c67c6aea93..0b8e90f5ac0 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "Uznaj urz\u0105dzenia zasilane z gniazdka za niedost\u0119pne po (sekundach)", "default_light_transition": "Domy\u015blny czas efektu przej\u015bcia dla \u015bwiat\u0142a (w sekundach)", "enable_identify_on_join": "W\u0142\u0105cz efekt identyfikacji, gdy urz\u0105dzenia do\u0142\u0105czaj\u0105 do sieci", + "enhanced_light_transition": "W\u0142\u0105cz ulepszone przej\u015bcie koloru \u015bwiat\u0142a/temperatury ze stanu wy\u0142\u0105czenia", "title": "Opcje og\u00f3lne" } }, diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index 9bf7d3c9208..9505da31e80 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "\u5c07\u4e3b\u4f9b\u96fb\u88dd\u7f6e\u8996\u70ba\u4e0d\u53ef\u7528\uff08\u79d2\u6578\uff09", "default_light_transition": "\u9810\u8a2d\u71c8\u5149\u8f49\u63db\u6642\u9593\uff08\u79d2\uff09", "enable_identify_on_join": "\u7576\u88dd\u7f6e\u52a0\u5165\u7db2\u8def\u6642\u3001\u958b\u555f\u8b58\u5225\u6548\u679c", + "enhanced_light_transition": "\u958b\u555f\u7531\u95dc\u9589\u72c0\u614b\u589e\u5f37\u5149\u8272/\u8272\u6eab\u8f49\u63db", "title": "Global \u9078\u9805" } }, From edaebcd85d0e62739b883d65eb2848072094dbae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 23 Jul 2022 02:47:02 -0500 Subject: [PATCH 2798/3516] Pass in the bleak scanner instance to HKC (#75636) --- .../components/homekit_controller/manifest.json | 4 ++-- homeassistant/components/homekit_controller/utils.py | 9 +++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit_controller/conftest.py | 5 +++++ 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index c784bad0d06..47472ba66bf 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,10 +3,10 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.1.9"], + "requirements": ["aiohomekit==1.1.10"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], - "after_dependencies": ["zeroconf"], + "dependencies": ["bluetooth", "zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"] diff --git a/homeassistant/components/homekit_controller/utils.py b/homeassistant/components/homekit_controller/utils.py index 892040d535f..d7780029331 100644 --- a/homeassistant/components/homekit_controller/utils.py +++ b/homeassistant/components/homekit_controller/utils.py @@ -3,7 +3,7 @@ from typing import cast from aiohomekit import Controller -from homeassistant.components import zeroconf +from homeassistant.components import bluetooth, zeroconf from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant @@ -28,7 +28,12 @@ async def async_get_controller(hass: HomeAssistant) -> Controller: if existing := hass.data.get(CONTROLLER): return cast(Controller, existing) - controller = Controller(async_zeroconf_instance=async_zeroconf_instance) + bleak_scanner_instance = bluetooth.async_get_scanner(hass) + + controller = Controller( + async_zeroconf_instance=async_zeroconf_instance, + bleak_scanner_instance=bleak_scanner_instance, + ) hass.data[CONTROLLER] = controller diff --git a/requirements_all.txt b/requirements_all.txt index 9451d3b8495..293eff63383 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.9 +aiohomekit==1.1.10 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3faa25b829f..c4fb04b7cda 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.9 +aiohomekit==1.1.10 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index 81688f88a4b..043213ec159 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -37,3 +37,8 @@ def controller(hass): @pytest.fixture(autouse=True) def hk_mock_async_zeroconf(mock_async_zeroconf): """Auto mock zeroconf.""" + + +@pytest.fixture(autouse=True) +def auto_mock_bluetooth(mock_bluetooth): + """Auto mock bluetooth.""" From b60a59270c4d906f277b81572a077c6d5bc7d1d6 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 23 Jul 2022 09:40:56 +0100 Subject: [PATCH 2799/3516] Add support for rest of sensors for HHCCJCY01 (#75646) --- .../components/xiaomi_ble/manifest.json | 2 +- homeassistant/components/xiaomi_ble/sensor.py | 24 +++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/xiaomi_ble/__init__.py | 15 ++++ tests/components/xiaomi_ble/test_sensor.py | 84 ++++++++++++++++++- 6 files changed, 124 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index cf5ef00777b..17e22accd6d 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.1.0"], + "requirements": ["xiaomi-ble==0.2.0"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index c380490dc8c..f485ccb8eb6 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -22,6 +22,8 @@ from homeassistant.const import ( ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME, + CONDUCTIVITY, + LIGHT_LUX, PERCENTAGE, PRESSURE_MBAR, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -46,6 +48,12 @@ SENSOR_DESCRIPTIONS = { native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), + (DeviceClass.ILLUMINANCE, Units.LIGHT_LUX): SensorEntityDescription( + key=f"{DeviceClass.ILLUMINANCE}_{Units.LIGHT_LUX}", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=LIGHT_LUX, + state_class=SensorStateClass.MEASUREMENT, + ), (DeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( key=f"{DeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", device_class=SensorDeviceClass.PRESSURE, @@ -68,6 +76,20 @@ SENSOR_DESCRIPTIONS = { state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), + # Used for e.g. moisture sensor on HHCCJCY01 + (None, Units.PERCENTAGE): SensorEntityDescription( + key=str(Units.PERCENTAGE), + device_class=None, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + # Used for e.g. conductivity sensor on HHCCJCY01 + (None, Units.CONDUCTIVITY): SensorEntityDescription( + key=str(Units.CONDUCTIVITY), + device_class=None, + native_unit_of_measurement=CONDUCTIVITY, + state_class=SensorStateClass.MEASUREMENT, + ), } @@ -106,7 +128,7 @@ def sensor_update_to_bluetooth_data_update( (description.device_class, description.native_unit_of_measurement) ] for device_key, description in sensor_update.entity_descriptions.items() - if description.device_class and description.native_unit_of_measurement + if description.native_unit_of_measurement }, entity_data={ _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value diff --git a/requirements_all.txt b/requirements_all.txt index 293eff63383..80ca6871b48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2471,7 +2471,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.1.0 +xiaomi-ble==0.2.0 # homeassistant.components.knx xknx==0.21.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c4fb04b7cda..6d0e6f54715 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1656,7 +1656,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.1.0 +xiaomi-ble==0.2.0 # homeassistant.components.knx xknx==0.21.5 diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py index 6022b15bf51..80ec2f19989 100644 --- a/tests/components/xiaomi_ble/__init__.py +++ b/tests/components/xiaomi_ble/__init__.py @@ -36,3 +36,18 @@ MMC_T201_1_SERVICE_INFO = BluetoothServiceInfo( service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", ) + + +def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfo: + """Make a dummy advertisement.""" + return BluetoothServiceInfo( + name="Test Device", + address=address, + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": payload, + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", + ) diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 044b2ea3e87..44b29ff1051 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -7,7 +7,7 @@ from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.xiaomi_ble.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT -from . import MMC_T201_1_SERVICE_INFO +from . import MMC_T201_1_SERVICE_INFO, make_advertisement from tests.common import MockConfigEntry @@ -48,3 +48,85 @@ async def test_sensors(hass): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_xiaomi_HHCCJCY01(hass): + """This device has multiple advertisements before all sensors are visible. Test that this works.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="C4:7C:8D:6A:3E:7A", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00fz>j\x8d|\xc4\r\x07\x10\x03\x00\x00\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00hz>j\x8d|\xc4\r\t\x10\x02W\x02" + ), + BluetoothChange.ADVERTISEMENT, + ) + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00Gz>j\x8d|\xc4\r\x08\x10\x01@" + ), + BluetoothChange.ADVERTISEMENT, + ) + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x04\x10\x02\xf4\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 4 + + illum_sensor = hass.states.get("sensor.test_device_illuminance") + illum_sensor_attr = illum_sensor.attributes + assert illum_sensor.state == "0" + assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Illuminance" + assert illum_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "lx" + assert illum_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + cond_sensor = hass.states.get("sensor.test_device_conductivity") + cond_sensor_attribtes = cond_sensor.attributes + assert cond_sensor.state == "599" + assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Conductivity" + assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" + assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + moist_sensor = hass.states.get("sensor.test_device_moisture") + moist_sensor_attribtes = moist_sensor.attributes + assert moist_sensor.state == "64" + assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Moisture" + assert moist_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert moist_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + temp_sensor = hass.states.get("sensor.test_device_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "24.4" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Temperature" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 5e10716dd84fe5cb227368e7f0e42b0b2c3a66ea Mon Sep 17 00:00:00 2001 From: Jelte Zeilstra Date: Sat, 23 Jul 2022 12:42:43 +0200 Subject: [PATCH 2800/3516] Do not access hass.data in unifi test (#75348) * Do not access hass.data in test * Process review feedback --- tests/components/unifi/test_update.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/unifi/test_update.py b/tests/components/unifi/test_update.py index 7bbeb65497e..677491319a1 100644 --- a/tests/components/unifi/test_update.py +++ b/tests/components/unifi/test_update.py @@ -5,7 +5,7 @@ from aiounifi.controller import MESSAGE_DEVICE from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING from yarl import URL -from homeassistant.components.unifi.const import DOMAIN as UNIFI_DOMAIN +from homeassistant.components.unifi.const import CONF_SITE_ID from homeassistant.components.update import ( ATTR_IN_PROGRESS, ATTR_INSTALLED_VERSION, @@ -19,6 +19,7 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, + CONF_HOST, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, @@ -166,8 +167,7 @@ async def test_install(hass, aioclient_mock): device_state = hass.states.get("update.device_1") assert device_state.state == STATE_ON - controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - url = f"https://{controller.host}:1234/api/s/{controller.site}/cmd/devmgr" + url = f"https://{config_entry.data[CONF_HOST]}:1234/api/s/{config_entry.data[CONF_SITE_ID]}/cmd/devmgr" aioclient_mock.clear_requests() aioclient_mock.post(url) From 5c234a3504fd0d399c63b045dbe86fbe2d26f292 Mon Sep 17 00:00:00 2001 From: James Date: Sat, 23 Jul 2022 07:24:34 -0400 Subject: [PATCH 2801/3516] Use CO Device Class Instead of Gas in zwave_js (#75649) Use CO Device Class Instead of Gas Switches the carbon monoxide sensor from `Gas` to `CO` --- homeassistant/components/zwave_js/binary_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index f6480689910..7fa20b2b1f5 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -90,7 +90,7 @@ NOTIFICATION_SENSOR_MAPPINGS: tuple[NotificationZWaveJSEntityDescription, ...] = # NotificationType 2: Carbon Monoxide - State Id's 1 and 2 key=NOTIFICATION_CARBON_MONOOXIDE, states=("1", "2"), - device_class=BinarySensorDeviceClass.GAS, + device_class=BinarySensorDeviceClass.CO, ), NotificationZWaveJSEntityDescription( # NotificationType 2: Carbon Monoxide - All other State Id's From b71e3397fd69c6ea63eaa6cd918bcd4b12fa424e Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Sat, 23 Jul 2022 23:40:45 +0800 Subject: [PATCH 2802/3516] Add error message for duplicate stream recordings (#75654) --- homeassistant/components/stream/recorder.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index b33a5fbbf84..42b98946c15 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -163,4 +163,10 @@ class RecorderOutput(StreamOutput): _LOGGER.error("Recording failed to capture anything") else: output.close() - os.rename(self.video_path + ".tmp", self.video_path) + try: + os.rename(self.video_path + ".tmp", self.video_path) + except FileNotFoundError: + _LOGGER.error( + "Error writing to '%s'. There are likely multiple recordings writing to the same file", + self.video_path, + ) From 8d6247446bac4060d47ded1761b8a44e2b61bb55 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 23 Jul 2022 17:47:12 +0200 Subject: [PATCH 2803/3516] Automatically set up Bluetooth during onboarding (#75658) --- .../components/bluetooth/config_flow.py | 3 ++- .../components/bluetooth/test_config_flow.py | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index 2193170810f..8fe01be769d 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any +from homeassistant.components import onboarding from homeassistant.config_entries import ConfigFlow from homeassistant.data_entry_flow import FlowResult @@ -27,7 +28,7 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): if self._async_current_entries(): return self.async_abort(reason="already_configured") - if user_input is not None: + if user_input is not None or not onboarding.async_is_onboarded(self.hass): return self.async_create_entry(title=DEFAULT_NAME, data={}) return self.async_show_form(step_id="enable_bluetooth") diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index 550f5a583d7..5c6199b9bf0 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -64,6 +64,27 @@ async def test_async_step_integration_discovery(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_async_step_integration_discovery_during_onboarding(hass): + """Test setting up from integration discovery during onboarding.""" + + with patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Bluetooth" + assert result["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_onboarding.mock_calls) == 1 + + async def test_async_step_integration_discovery_already_exists(hass): """Test setting up from integration discovery when an entry already exists.""" entry = MockConfigEntry(domain=DOMAIN) From 759add51841450b89f5742414345bf1144dcf195 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 23 Jul 2022 11:14:06 -0500 Subject: [PATCH 2804/3516] Add state class to HKC sensors (#75662) --- .../components/homekit_controller/sensor.py | 20 +++++++++---------- .../specific_devices/test_aqara_switch.py | 2 ++ .../specific_devices/test_arlo_baby.py | 3 +++ .../specific_devices/test_eve_degree.py | 3 +++ .../specific_devices/test_hue_bridge.py | 2 ++ .../test_ryse_smart_bridge.py | 7 +++++++ .../specific_devices/test_velux_gateway.py | 4 ++++ .../homekit_controller/test_sensor.py | 5 +++-- 8 files changed, 34 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index a3a722fea67..cddcbc59cde 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -34,8 +34,6 @@ from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity from .connection import HKDevice from .utils import folded_name -CO2_ICON = "mdi:molecule-co2" - @dataclass class HomeKitSensorEntityDescription(SensorEntityDescription): @@ -200,9 +198,11 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { } -class HomeKitSensor(HomeKitEntity): +class HomeKitSensor(HomeKitEntity, SensorEntity): """Representation of a HomeKit sensor.""" + _attr_state_class = SensorStateClass.MEASUREMENT + @property def name(self) -> str | None: """Return the name of the device.""" @@ -217,7 +217,7 @@ class HomeKitSensor(HomeKitEntity): return full_name -class HomeKitHumiditySensor(HomeKitSensor, SensorEntity): +class HomeKitHumiditySensor(HomeKitSensor): """Representation of a Homekit humidity sensor.""" _attr_device_class = SensorDeviceClass.HUMIDITY @@ -238,7 +238,7 @@ class HomeKitHumiditySensor(HomeKitSensor, SensorEntity): return self.service.value(CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT) -class HomeKitTemperatureSensor(HomeKitSensor, SensorEntity): +class HomeKitTemperatureSensor(HomeKitSensor): """Representation of a Homekit temperature sensor.""" _attr_device_class = SensorDeviceClass.TEMPERATURE @@ -259,7 +259,7 @@ class HomeKitTemperatureSensor(HomeKitSensor, SensorEntity): return self.service.value(CharacteristicsTypes.TEMPERATURE_CURRENT) -class HomeKitLightSensor(HomeKitSensor, SensorEntity): +class HomeKitLightSensor(HomeKitSensor): """Representation of a Homekit light level sensor.""" _attr_device_class = SensorDeviceClass.ILLUMINANCE @@ -280,10 +280,10 @@ class HomeKitLightSensor(HomeKitSensor, SensorEntity): return self.service.value(CharacteristicsTypes.LIGHT_LEVEL_CURRENT) -class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): +class HomeKitCarbonDioxideSensor(HomeKitSensor): """Representation of a Homekit Carbon Dioxide sensor.""" - _attr_icon = CO2_ICON + _attr_device_class = SensorDeviceClass.CO2 _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION def get_characteristic_types(self) -> list[str]: @@ -293,7 +293,7 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): @property def default_name(self) -> str: """Return the default name of the device.""" - return "CO2" + return "Carbon Dioxide" @property def native_value(self) -> int: @@ -301,7 +301,7 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity, SensorEntity): return self.service.value(CharacteristicsTypes.CARBON_DIOXIDE_LEVEL) -class HomeKitBatterySensor(HomeKitSensor, SensorEntity): +class HomeKitBatterySensor(HomeKitSensor): """Representation of a Homekit battery sensor.""" _attr_device_class = SensorDeviceClass.BATTERY diff --git a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py index 16bef749429..daa6d593988 100644 --- a/tests/components/homekit_controller/specific_devices/test_aqara_switch.py +++ b/tests/components/homekit_controller/specific_devices/test_aqara_switch.py @@ -7,6 +7,7 @@ service-label-index despite not being linked to a service-label. https://github.com/home-assistant/core/pull/39090 """ +from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE from tests.components.homekit_controller.common import ( @@ -41,6 +42,7 @@ async def test_aqara_switch_setup(hass): entity_id="sensor.programmable_switch_battery_sensor", friendly_name="Programmable Switch Battery Sensor", unique_id="homekit-111a1111a1a111-5", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, state="100", ), diff --git a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py index 2cb312fc7f5..fe3d1ea5efc 100644 --- a/tests/components/homekit_controller/specific_devices/test_arlo_baby.py +++ b/tests/components/homekit_controller/specific_devices/test_arlo_baby.py @@ -46,6 +46,7 @@ async def test_arlo_baby_setup(hass): entity_id="sensor.arlobabya0_battery", unique_id="homekit-00A0000000000-700", friendly_name="ArloBabyA0 Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, state="82", ), @@ -53,6 +54,7 @@ async def test_arlo_baby_setup(hass): entity_id="sensor.arlobabya0_humidity", unique_id="homekit-00A0000000000-900", friendly_name="ArloBabyA0 Humidity", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, state="60.099998", ), @@ -60,6 +62,7 @@ async def test_arlo_baby_setup(hass): entity_id="sensor.arlobabya0_temperature", unique_id="homekit-00A0000000000-1000", friendly_name="ArloBabyA0 Temperature", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=TEMP_CELSIUS, state="24.0", ), diff --git a/tests/components/homekit_controller/specific_devices/test_eve_degree.py b/tests/components/homekit_controller/specific_devices/test_eve_degree.py index 51880bc076a..55377801529 100644 --- a/tests/components/homekit_controller/specific_devices/test_eve_degree.py +++ b/tests/components/homekit_controller/specific_devices/test_eve_degree.py @@ -36,6 +36,7 @@ async def test_eve_degree_setup(hass): entity_id="sensor.eve_degree_aa11_temperature", unique_id="homekit-AA00A0A00000-22", friendly_name="Eve Degree AA11 Temperature", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=TEMP_CELSIUS, state="22.7719116210938", ), @@ -43,6 +44,7 @@ async def test_eve_degree_setup(hass): entity_id="sensor.eve_degree_aa11_humidity", unique_id="homekit-AA00A0A00000-27", friendly_name="Eve Degree AA11 Humidity", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, state="59.4818115234375", ), @@ -58,6 +60,7 @@ async def test_eve_degree_setup(hass): entity_id="sensor.eve_degree_aa11_battery", unique_id="homekit-AA00A0A00000-17", friendly_name="Eve Degree AA11 Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unit_of_measurement=PERCENTAGE, state="65", ), diff --git a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py index 52a5fb83972..e64dc8378c5 100644 --- a/tests/components/homekit_controller/specific_devices/test_hue_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_hue_bridge.py @@ -1,5 +1,6 @@ """Tests for handling accessories on a Hue bridge via HomeKit.""" +from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE from tests.components.homekit_controller.common import ( @@ -41,6 +42,7 @@ async def test_hue_bridge_setup(hass): entities=[ EntityTestInfo( entity_id="sensor.hue_dimmer_switch_battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, friendly_name="Hue dimmer switch battery", unique_id="homekit-6623462389072572-644245094400", unit_of_measurement=PERCENTAGE, diff --git a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py index eb9c36d0b79..155eb1c5c27 100644 --- a/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py +++ b/tests/components/homekit_controller/specific_devices/test_ryse_smart_bridge.py @@ -5,6 +5,7 @@ from homeassistant.components.cover import ( SUPPORT_OPEN, SUPPORT_SET_POSITION, ) +from homeassistant.components.sensor import SensorStateClass from homeassistant.const import PERCENTAGE from tests.components.homekit_controller.common import ( @@ -54,6 +55,7 @@ async def test_ryse_smart_bridge_setup(hass): EntityTestInfo( entity_id="sensor.master_bath_south_ryse_shade_battery", friendly_name="Master Bath South RYSE Shade Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-2-64", unit_of_measurement=PERCENTAGE, state="100", @@ -80,6 +82,7 @@ async def test_ryse_smart_bridge_setup(hass): EntityTestInfo( entity_id="sensor.ryse_smartshade_ryse_shade_battery", friendly_name="RYSE SmartShade RYSE Shade Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-3-64", unit_of_measurement=PERCENTAGE, state="100", @@ -129,6 +132,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): EntityTestInfo( entity_id="sensor.lr_left_ryse_shade_battery", friendly_name="LR Left RYSE Shade Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-2-64", unit_of_measurement=PERCENTAGE, state="89", @@ -155,6 +159,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): EntityTestInfo( entity_id="sensor.lr_right_ryse_shade_battery", friendly_name="LR Right RYSE Shade Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-3-64", unit_of_measurement=PERCENTAGE, state="100", @@ -181,6 +186,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): EntityTestInfo( entity_id="sensor.br_left_ryse_shade_battery", friendly_name="BR Left RYSE Shade Battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-00:00:00:00:00:00-4-64", unit_of_measurement=PERCENTAGE, state="100", @@ -206,6 +212,7 @@ async def test_ryse_smart_bridge_four_shades_setup(hass): ), EntityTestInfo( entity_id="sensor.rzss_ryse_shade_battery", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, friendly_name="RZSS RYSE Shade Battery", unique_id="homekit-00:00:00:00:00:00-5-64", unit_of_measurement=PERCENTAGE, diff --git a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py index 21aa91fc933..07c35fb867d 100644 --- a/tests/components/homekit_controller/specific_devices/test_velux_gateway.py +++ b/tests/components/homekit_controller/specific_devices/test_velux_gateway.py @@ -9,6 +9,7 @@ from homeassistant.components.cover import ( SUPPORT_OPEN, SUPPORT_SET_POSITION, ) +from homeassistant.components.sensor import SensorStateClass from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, @@ -75,6 +76,7 @@ async def test_velux_cover_setup(hass): EntityTestInfo( entity_id="sensor.velux_sensor_temperature_sensor", friendly_name="VELUX Sensor Temperature sensor", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-a11b111-8", unit_of_measurement=TEMP_CELSIUS, state="18.9", @@ -82,6 +84,7 @@ async def test_velux_cover_setup(hass): EntityTestInfo( entity_id="sensor.velux_sensor_humidity_sensor", friendly_name="VELUX Sensor Humidity sensor", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-a11b111-11", unit_of_measurement=PERCENTAGE, state="58", @@ -89,6 +92,7 @@ async def test_velux_cover_setup(hass): EntityTestInfo( entity_id="sensor.velux_sensor_carbon_dioxide_sensor", friendly_name="VELUX Sensor Carbon Dioxide sensor", + capabilities={"state_class": SensorStateClass.MEASUREMENT}, unique_id="homekit-a11b111-14", unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state="400", diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index 145d85eeed7..836da1e466f 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -3,7 +3,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes from aiohomekit.protocol.statuscodes import HapStatusCode -from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from tests.components.homekit_controller.common import Helper, setup_test_component @@ -79,6 +79,7 @@ async def test_temperature_sensor_read_state(hass, utcnow): assert state.state == "20" assert state.attributes["device_class"] == SensorDeviceClass.TEMPERATURE + assert state.attributes["state_class"] == SensorStateClass.MEASUREMENT async def test_temperature_sensor_not_added_twice(hass, utcnow): @@ -146,7 +147,7 @@ async def test_light_level_sensor_read_state(hass, utcnow): async def test_carbon_dioxide_level_sensor_read_state(hass, utcnow): """Test reading the state of a HomeKit carbon dioxide sensor accessory.""" helper = await setup_test_component( - hass, create_carbon_dioxide_level_sensor_service, suffix="co2" + hass, create_carbon_dioxide_level_sensor_service, suffix="carbon_dioxide" ) state = await helper.async_update( From 2951a941b615cd4ab6eb6c06e399cc9f4135fa09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sat, 23 Jul 2022 18:44:58 +0200 Subject: [PATCH 2805/3516] Import correct scan interval in traccar (#75660) --- homeassistant/components/traccar/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 0ed13bceefa..50b0e7827aa 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -10,6 +10,7 @@ from stringcase import camelcase import voluptuous as vol from homeassistant.components.device_tracker import ( + CONF_SCAN_INTERVAL, PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SOURCE_TYPE_GPS, ) @@ -21,7 +22,6 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_PASSWORD, CONF_PORT, - CONF_SCAN_INTERVAL, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, From 240bbfa0805ef28bf448abe7687904be3776e9f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 23 Jul 2022 12:00:34 -0500 Subject: [PATCH 2806/3516] Retry later if bluetooth fails to start (#75647) --- .../components/bluetooth/__init__.py | 6 ++- tests/components/bluetooth/test_init.py | 42 +++++++++++++++++-- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index a50be7f4ace..901b8ee0644 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -23,6 +23,7 @@ from homeassistant.core import ( HomeAssistant, callback as hass_callback, ) +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo @@ -338,7 +339,6 @@ class BluetoothManager: except (FileNotFoundError, BleakError) as ex: raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex install_multiple_bleak_catcher() - self.async_setup_unavailable_tracking() # We have to start it right away as some integrations might # need it straight away. _LOGGER.debug("Starting bluetooth scanner") @@ -349,7 +349,9 @@ class BluetoothManager: try: await self.scanner.start() except (FileNotFoundError, BleakError) as ex: - raise RuntimeError(f"Failed to start Bluetooth: {ex}") from ex + self._cancel_device_detected() + raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex + self.async_setup_unavailable_tracking() self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) @hass_callback diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 5d932c56349..6dbec251026 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -15,6 +15,7 @@ from homeassistant.components.bluetooth import ( async_track_unavailable, models, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.setup import async_setup_component @@ -70,10 +71,7 @@ async def test_setup_and_stop_no_bluetooth(hass, caplog): async def test_setup_and_stop_broken_bluetooth(hass, caplog): """Test we fail gracefully when bluetooth/dbus is broken.""" - mock_bt = [ - {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} - ] - + mock_bt = [] with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( "homeassistant.components.bluetooth.HaBleakScanner.start", side_effect=BleakError, @@ -93,6 +91,42 @@ async def test_setup_and_stop_broken_bluetooth(hass, caplog): assert len(bluetooth.async_discovered_service_info(hass)) == 0 +async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): + """Test we retry if the adapter is not yet available.""" + mock_bt = [] + with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=BleakError, + ), patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] + + assert "Failed to start Bluetooth" in caplog.text + assert len(bluetooth.async_discovered_service_info(hass)) == 0 + assert entry.state == ConfigEntryState.SETUP_RETRY + + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + ): + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.stop", + ): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): """Test we fail gracefully when asking for discovered devices and there is no blueooth.""" mock_bt = [] From 8300d5b89e621b0905c4db3a7949e89f45b7c872 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 23 Jul 2022 12:59:35 -0500 Subject: [PATCH 2807/3516] Add bluetooth connection constant to the device registry (#75666) --- homeassistant/helpers/device_registry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index a19f476495a..f133eba8f92 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -38,6 +38,7 @@ STORAGE_VERSION_MINOR = 3 SAVE_DELAY = 10 CLEANUP_DELAY = 10 +CONNECTION_BLUETOOTH = "bluetooth" CONNECTION_NETWORK_MAC = "mac" CONNECTION_UPNP = "upnp" CONNECTION_ZIGBEE = "zigbee" From c5afaa2e6a1af77a69a0151dc1b8d7f5e3dcf2da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 23 Jul 2022 13:03:01 -0500 Subject: [PATCH 2808/3516] Refactor PassiveBluetoothDataUpdateCoordinator to support multiple platforms (#75642) --- .../bluetooth/passive_update_coordinator.py | 235 ++++++++------ homeassistant/components/inkbird/__init__.py | 18 +- homeassistant/components/inkbird/sensor.py | 27 +- .../components/sensorpush/__init__.py | 18 +- homeassistant/components/sensorpush/sensor.py | 27 +- .../components/xiaomi_ble/__init__.py | 18 +- homeassistant/components/xiaomi_ble/sensor.py | 27 +- .../test_passive_update_coordinator.py | 307 ++++++++++++++---- 8 files changed, 443 insertions(+), 234 deletions(-) diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index b2fdd5d7d58..507350f2162 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -55,32 +55,11 @@ class PassiveBluetoothDataUpdate(Generic[_T]): ) -_PassiveBluetoothDataUpdateCoordinatorT = TypeVar( - "_PassiveBluetoothDataUpdateCoordinatorT", - bound="PassiveBluetoothDataUpdateCoordinator[Any]", -) - - -class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): +class PassiveBluetoothDataUpdateCoordinator: """Passive bluetooth data update coordinator for bluetooth advertisements. - The coordinator is responsible for keeping track of the bluetooth data, - updating subscribers, and device availability. - - The update_method must return a PassiveBluetoothDataUpdate object. Callers - are responsible for formatting the data returned from their parser into - the appropriate format. - - The coordinator will call the update_method every time the bluetooth device - receives a new advertisement with the following signature: - - update_method(service_info: BluetoothServiceInfo) -> PassiveBluetoothDataUpdate - - As the size of each advertisement is limited, the update_method should - return a PassiveBluetoothDataUpdate object that contains only data that - should be updated. The coordinator will then dispatch subscribers based - on the data in the PassiveBluetoothDataUpdate object. The accumulated data - is available in the devices, entity_data, and entity_descriptions attributes. + The coordinator is responsible for dispatching the bluetooth data, + to each processor, and tracking devices. """ def __init__( @@ -88,45 +67,22 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): hass: HomeAssistant, logger: logging.Logger, address: str, - update_method: Callable[[BluetoothServiceInfo], PassiveBluetoothDataUpdate[_T]], ) -> None: """Initialize the coordinator.""" self.hass = hass self.logger = logger self.name: str | None = None self.address = address - self._listeners: list[ - Callable[[PassiveBluetoothDataUpdate[_T] | None], None] - ] = [] - self._entity_key_listeners: dict[ - PassiveBluetoothEntityKey, - list[Callable[[PassiveBluetoothDataUpdate[_T] | None], None]], - ] = {} - self.update_method = update_method - - self.entity_names: dict[PassiveBluetoothEntityKey, str | None] = {} - self.entity_data: dict[PassiveBluetoothEntityKey, _T] = {} - self.entity_descriptions: dict[ - PassiveBluetoothEntityKey, EntityDescription - ] = {} - self.devices: dict[str | None, DeviceInfo] = {} - - self.last_update_success = True + self._processors: list[PassiveBluetoothDataProcessor] = [] self._cancel_track_unavailable: CALLBACK_TYPE | None = None self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None - self.present = False + self._present = False self.last_seen = 0.0 @property def available(self) -> bool: """Return if the device is available.""" - return self.present and self.last_update_success - - @callback - def _async_handle_unavailable(self, _address: str) -> None: - """Handle the device going unavailable.""" - self.present = False - self.async_update_listeners(None) + return self._present @callback def _async_start(self) -> None: @@ -152,10 +108,121 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): self._cancel_track_unavailable() self._cancel_track_unavailable = None + @callback + def async_register_processor( + self, processor: PassiveBluetoothDataProcessor + ) -> Callable[[], None]: + """Register a processor that subscribes to updates.""" + processor.coordinator = self + + @callback + def remove_processor() -> None: + """Remove a processor.""" + self._processors.remove(processor) + self._async_handle_processors_changed() + + self._processors.append(processor) + self._async_handle_processors_changed() + return remove_processor + + @callback + def _async_handle_processors_changed(self) -> None: + """Handle processors changed.""" + running = bool(self._cancel_bluetooth_advertisements) + if running and not self._processors: + self._async_stop() + elif not running and self._processors: + self._async_start() + + @callback + def _async_handle_unavailable(self, _address: str) -> None: + """Handle the device going unavailable.""" + self._present = False + for processor in self._processors: + processor.async_handle_unavailable() + + @callback + def _async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfo, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + self.last_seen = time.monotonic() + self.name = service_info.name + self._present = True + if self.hass.is_stopping: + return + for processor in self._processors: + processor.async_handle_bluetooth_event(service_info, change) + + +_PassiveBluetoothDataProcessorT = TypeVar( + "_PassiveBluetoothDataProcessorT", + bound="PassiveBluetoothDataProcessor[Any]", +) + + +class PassiveBluetoothDataProcessor(Generic[_T]): + """Passive bluetooth data processor for bluetooth advertisements. + + The processor is responsible for keeping track of the bluetooth data + and updating subscribers. + + The update_method must return a PassiveBluetoothDataUpdate object. Callers + are responsible for formatting the data returned from their parser into + the appropriate format. + + The processor will call the update_method every time the bluetooth device + receives a new advertisement data from the coordinator with the following signature: + + update_method(service_info: BluetoothServiceInfo) -> PassiveBluetoothDataUpdate + + As the size of each advertisement is limited, the update_method should + return a PassiveBluetoothDataUpdate object that contains only data that + should be updated. The coordinator will then dispatch subscribers based + on the data in the PassiveBluetoothDataUpdate object. The accumulated data + is available in the devices, entity_data, and entity_descriptions attributes. + """ + + coordinator: PassiveBluetoothDataUpdateCoordinator + + def __init__( + self, + update_method: Callable[[BluetoothServiceInfo], PassiveBluetoothDataUpdate[_T]], + ) -> None: + """Initialize the coordinator.""" + self.coordinator: PassiveBluetoothDataUpdateCoordinator + self._listeners: list[ + Callable[[PassiveBluetoothDataUpdate[_T] | None], None] + ] = [] + self._entity_key_listeners: dict[ + PassiveBluetoothEntityKey, + list[Callable[[PassiveBluetoothDataUpdate[_T] | None], None]], + ] = {} + self.update_method = update_method + self.entity_names: dict[PassiveBluetoothEntityKey, str | None] = {} + self.entity_data: dict[PassiveBluetoothEntityKey, _T] = {} + self.entity_descriptions: dict[ + PassiveBluetoothEntityKey, EntityDescription + ] = {} + self.devices: dict[str | None, DeviceInfo] = {} + self.last_update_success = True + + @property + def available(self) -> bool: + """Return if the device is available.""" + return self.coordinator.available and self.last_update_success + + @callback + def async_handle_unavailable(self) -> None: + """Handle the device going unavailable.""" + self.async_update_listeners(None) + @callback def async_add_entities_listener( self, - entity_class: type[PassiveBluetoothCoordinatorEntity], + entity_class: type[PassiveBluetoothProcessorEntity], async_add_entites: AddEntitiesCallback, ) -> Callable[[], None]: """Add a listener for new entities.""" @@ -168,7 +235,7 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): """Listen for new entities.""" if data is None: return - entities: list[PassiveBluetoothCoordinatorEntity] = [] + entities: list[PassiveBluetoothProcessorEntity] = [] for entity_key, description in data.entity_descriptions.items(): if entity_key not in created: entities.append(entity_class(self, entity_key, description)) @@ -189,22 +256,10 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): def remove_listener() -> None: """Remove update listener.""" self._listeners.remove(update_callback) - self._async_handle_listeners_changed() self._listeners.append(update_callback) - self._async_handle_listeners_changed() return remove_listener - @callback - def _async_handle_listeners_changed(self) -> None: - """Handle listeners changed.""" - has_listeners = self._listeners or self._entity_key_listeners - running = bool(self._cancel_bluetooth_advertisements) - if running and not has_listeners: - self._async_stop() - elif not running and has_listeners: - self._async_start() - @callback def async_add_entity_key_listener( self, @@ -219,10 +274,8 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): self._entity_key_listeners[entity_key].remove(update_callback) if not self._entity_key_listeners[entity_key]: del self._entity_key_listeners[entity_key] - self._async_handle_listeners_changed() self._entity_key_listeners.setdefault(entity_key, []).append(update_callback) - self._async_handle_listeners_changed() return remove_listener @callback @@ -240,36 +293,32 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): update_callback(data) @callback - def _async_handle_bluetooth_event( + def async_handle_bluetooth_event( self, service_info: BluetoothServiceInfo, change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" - self.last_seen = time.monotonic() - self.name = service_info.name - self.present = True - if self.hass.is_stopping: - return - try: new_data = self.update_method(service_info) except Exception as err: # pylint: disable=broad-except self.last_update_success = False - self.logger.exception( - "Unexpected error updating %s data: %s", self.name, err + self.coordinator.logger.exception( + "Unexpected error updating %s data: %s", self.coordinator.name, err ) return if not isinstance(new_data, PassiveBluetoothDataUpdate): self.last_update_success = False # type: ignore[unreachable] raise ValueError( - f"The update_method for {self.name} returned {new_data} instead of a PassiveBluetoothDataUpdate" + f"The update_method for {self.coordinator.name} returned {new_data} instead of a PassiveBluetoothDataUpdate" ) if not self.last_update_success: self.last_update_success = True - self.logger.info("Processing %s data recovered", self.name) + self.coordinator.logger.info( + "Processing %s data recovered", self.coordinator.name + ) self.devices.update(new_data.devices) self.entity_descriptions.update(new_data.entity_descriptions) @@ -278,29 +327,27 @@ class PassiveBluetoothDataUpdateCoordinator(Generic[_T]): self.async_update_listeners(new_data) -class PassiveBluetoothCoordinatorEntity( - Entity, Generic[_PassiveBluetoothDataUpdateCoordinatorT] -): - """A class for entities using PassiveBluetoothDataUpdateCoordinator.""" +class PassiveBluetoothProcessorEntity(Entity, Generic[_PassiveBluetoothDataProcessorT]): + """A class for entities using PassiveBluetoothDataProcessor.""" _attr_has_entity_name = True _attr_should_poll = False def __init__( self, - coordinator: _PassiveBluetoothDataUpdateCoordinatorT, + processor: _PassiveBluetoothDataProcessorT, entity_key: PassiveBluetoothEntityKey, description: EntityDescription, context: Any = None, ) -> None: - """Create the entity with a PassiveBluetoothDataUpdateCoordinator.""" + """Create the entity with a PassiveBluetoothDataProcessor.""" self.entity_description = description self.entity_key = entity_key - self.coordinator = coordinator - self.coordinator_context = context - address = coordinator.address + self.processor = processor + self.processor_context = context + address = processor.coordinator.address device_id = entity_key.device_id - devices = coordinator.devices + devices = processor.devices key = entity_key.key if device_id in devices: base_device_info = devices[device_id] @@ -317,26 +364,26 @@ class PassiveBluetoothCoordinatorEntity( ) self._attr_unique_id = f"{address}-{key}" if ATTR_NAME not in self._attr_device_info: - self._attr_device_info[ATTR_NAME] = self.coordinator.name - self._attr_name = coordinator.entity_names.get(entity_key) + self._attr_device_info[ATTR_NAME] = self.processor.coordinator.name + self._attr_name = processor.entity_names.get(entity_key) @property def available(self) -> bool: """Return if entity is available.""" - return self.coordinator.available + return self.processor.available async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() self.async_on_remove( - self.coordinator.async_add_entity_key_listener( - self._handle_coordinator_update, self.entity_key + self.processor.async_add_entity_key_listener( + self._handle_processor_update, self.entity_key ) ) @callback - def _handle_coordinator_update( + def _handle_processor_update( self, new_data: PassiveBluetoothDataUpdate | None ) -> None: - """Handle updated data from the coordinator.""" + """Handle updated data from the processor.""" self.async_write_ha_state() diff --git a/homeassistant/components/inkbird/__init__.py b/homeassistant/components/inkbird/__init__.py index 29b63e34263..330d85b665c 100644 --- a/homeassistant/components/inkbird/__init__.py +++ b/homeassistant/components/inkbird/__init__.py @@ -3,19 +3,14 @@ from __future__ import annotations import logging -from inkbird_ble import INKBIRDBluetoothDeviceData - from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothDataUpdate, PassiveBluetoothDataUpdateCoordinator, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from homeassistant.core import HomeAssistant from .const import DOMAIN -from .sensor import sensor_update_to_bluetooth_data_update PLATFORMS: list[Platform] = [Platform.SENSOR] @@ -26,22 +21,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up INKBIRD BLE device from a config entry.""" address = entry.unique_id assert address is not None - - data = INKBIRDBluetoothDeviceData() - - @callback - def _async_update_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Update data from INKBIRD Bluetooth.""" - return sensor_update_to_bluetooth_data_update(data.update(service_info)) - hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothDataUpdateCoordinator( hass, _LOGGER, - update_method=_async_update_data, address=address, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/inkbird/sensor.py b/homeassistant/components/inkbird/sensor.py index 26311131181..600f28b6880 100644 --- a/homeassistant/components/inkbird/sensor.py +++ b/homeassistant/components/inkbird/sensor.py @@ -3,14 +3,22 @@ from __future__ import annotations from typing import Optional, Union -from inkbird_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units +from inkbird_ble import ( + DeviceClass, + DeviceKey, + INKBIRDBluetoothDeviceData, + SensorDeviceInfo, + SensorUpdate, + Units, +) from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothCoordinatorEntity, + PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, PassiveBluetoothDataUpdateCoordinator, PassiveBluetoothEntityKey, + PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -121,16 +129,23 @@ async def async_setup_entry( coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][ entry.entry_id ] + data = INKBIRDBluetoothDeviceData() + processor = PassiveBluetoothDataProcessor( + lambda service_info: sensor_update_to_bluetooth_data_update( + data.update(service_info) + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) entry.async_on_unload( - coordinator.async_add_entities_listener( + processor.async_add_entities_listener( INKBIRDBluetoothSensorEntity, async_add_entities ) ) class INKBIRDBluetoothSensorEntity( - PassiveBluetoothCoordinatorEntity[ - PassiveBluetoothDataUpdateCoordinator[Optional[Union[float, int]]] + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] ], SensorEntity, ): @@ -139,4 +154,4 @@ class INKBIRDBluetoothSensorEntity( @property def native_value(self) -> int | float | None: """Return the native value.""" - return self.coordinator.entity_data.get(self.entity_key) + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/sensorpush/__init__.py b/homeassistant/components/sensorpush/__init__.py index 89e2287be70..94e98e30f82 100644 --- a/homeassistant/components/sensorpush/__init__.py +++ b/homeassistant/components/sensorpush/__init__.py @@ -3,19 +3,14 @@ from __future__ import annotations import logging -from sensorpush_ble import SensorPushBluetoothDeviceData - from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothDataUpdate, PassiveBluetoothDataUpdateCoordinator, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from homeassistant.core import HomeAssistant from .const import DOMAIN -from .sensor import sensor_update_to_bluetooth_data_update PLATFORMS: list[Platform] = [Platform.SENSOR] @@ -26,22 +21,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SensorPush BLE device from a config entry.""" address = entry.unique_id assert address is not None - - data = SensorPushBluetoothDeviceData() - - @callback - def _async_update_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Update data from SensorPush Bluetooth.""" - return sensor_update_to_bluetooth_data_update(data.update(service_info)) - hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothDataUpdateCoordinator( hass, _LOGGER, - update_method=_async_update_data, address=address, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/sensorpush/sensor.py b/homeassistant/components/sensorpush/sensor.py index 147555990e4..f592b594289 100644 --- a/homeassistant/components/sensorpush/sensor.py +++ b/homeassistant/components/sensorpush/sensor.py @@ -3,14 +3,22 @@ from __future__ import annotations from typing import Optional, Union -from sensorpush_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units +from sensorpush_ble import ( + DeviceClass, + DeviceKey, + SensorDeviceInfo, + SensorPushBluetoothDeviceData, + SensorUpdate, + Units, +) from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothCoordinatorEntity, + PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, PassiveBluetoothDataUpdateCoordinator, PassiveBluetoothEntityKey, + PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -122,16 +130,23 @@ async def async_setup_entry( coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][ entry.entry_id ] + data = SensorPushBluetoothDeviceData() + processor = PassiveBluetoothDataProcessor( + lambda service_info: sensor_update_to_bluetooth_data_update( + data.update(service_info) + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) entry.async_on_unload( - coordinator.async_add_entities_listener( + processor.async_add_entities_listener( SensorPushBluetoothSensorEntity, async_add_entities ) ) class SensorPushBluetoothSensorEntity( - PassiveBluetoothCoordinatorEntity[ - PassiveBluetoothDataUpdateCoordinator[Optional[Union[float, int]]] + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] ], SensorEntity, ): @@ -140,4 +155,4 @@ class SensorPushBluetoothSensorEntity( @property def native_value(self) -> int | float | None: """Return the native value.""" - return self.coordinator.entity_data.get(self.entity_key) + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 036ff37a306..d7f3e4071aa 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -3,19 +3,14 @@ from __future__ import annotations import logging -from xiaomi_ble import XiaomiBluetoothDeviceData - from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothDataUpdate, PassiveBluetoothDataUpdateCoordinator, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from homeassistant.core import HomeAssistant from .const import DOMAIN -from .sensor import sensor_update_to_bluetooth_data_update PLATFORMS: list[Platform] = [Platform.SENSOR] @@ -26,22 +21,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Xiaomi BLE device from a config entry.""" address = entry.unique_id assert address is not None - - data = XiaomiBluetoothDeviceData() - - @callback - def _async_update_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Update data from Xiaomi Bluetooth.""" - return sensor_update_to_bluetooth_data_update(data.update(service_info)) - hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothDataUpdateCoordinator( hass, _LOGGER, - update_method=_async_update_data, address=address, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index f485ccb8eb6..1452b5e9053 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -3,14 +3,22 @@ from __future__ import annotations from typing import Optional, Union -from xiaomi_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units +from xiaomi_ble import ( + DeviceClass, + DeviceKey, + SensorDeviceInfo, + SensorUpdate, + Units, + XiaomiBluetoothDeviceData, +) from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothCoordinatorEntity, + PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, PassiveBluetoothDataUpdateCoordinator, PassiveBluetoothEntityKey, + PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import ( SensorDeviceClass, @@ -150,16 +158,23 @@ async def async_setup_entry( coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][ entry.entry_id ] + data = XiaomiBluetoothDeviceData() + processor = PassiveBluetoothDataProcessor( + lambda service_info: sensor_update_to_bluetooth_data_update( + data.update(service_info) + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) entry.async_on_unload( - coordinator.async_add_entities_listener( + processor.async_add_entities_listener( XiaomiBluetoothSensorEntity, async_add_entities ) ) class XiaomiBluetoothSensorEntity( - PassiveBluetoothCoordinatorEntity[ - PassiveBluetoothDataUpdateCoordinator[Optional[Union[float, int]]] + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] ], SensorEntity, ): @@ -168,4 +183,4 @@ class XiaomiBluetoothSensorEntity( @property def native_value(self) -> int | float | None: """Return the native value.""" - return self.coordinator.entity_data.get(self.entity_key) + return self.processor.entity_data.get(self.entity_key) diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 48f2e8edf06..2b8c876460f 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -8,16 +8,21 @@ from unittest.mock import MagicMock, patch from home_assistant_bluetooth import BluetoothServiceInfo import pytest +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntityDescription, +) from homeassistant.components.bluetooth import ( DOMAIN, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, ) from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothCoordinatorEntity, + PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, PassiveBluetoothDataUpdateCoordinator, PassiveBluetoothEntityKey, + PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription from homeassistant.const import TEMP_CELSIUS @@ -85,7 +90,7 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" ) assert coordinator.available is False # no data yet saved_callback = None @@ -95,40 +100,41 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): saved_callback = _callback return lambda: None + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): + unregister_processor = coordinator.async_register_processor(processor) - entity_key = PassiveBluetoothEntityKey("temperature", None) - entity_key_events = [] - all_events = [] - mock_entity = MagicMock() - mock_add_entities = MagicMock() + entity_key = PassiveBluetoothEntityKey("temperature", None) + entity_key_events = [] + all_events = [] + mock_entity = MagicMock() + mock_add_entities = MagicMock() - def _async_entity_key_listener(data: PassiveBluetoothDataUpdate | None) -> None: - """Mock entity key listener.""" - entity_key_events.append(data) + def _async_entity_key_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock entity key listener.""" + entity_key_events.append(data) - cancel_async_add_entity_key_listener = ( - coordinator.async_add_entity_key_listener( - _async_entity_key_listener, - entity_key, - ) - ) + cancel_async_add_entity_key_listener = processor.async_add_entity_key_listener( + _async_entity_key_listener, + entity_key, + ) - def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: - """Mock an all listener.""" - all_events.append(data) + def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock an all listener.""" + all_events.append(data) - cancel_listener = coordinator.async_add_listener( - _all_listener, - ) + cancel_listener = processor.async_add_listener( + _all_listener, + ) - cancel_async_add_entities_listener = coordinator.async_add_entities_listener( - mock_entity, - mock_add_entities, - ) + cancel_async_add_entities_listener = processor.async_add_entities_listener( + mock_entity, + mock_add_entities, + ) saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) @@ -164,6 +170,8 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): assert len(mock_entity.mock_calls) == 2 assert coordinator.available is True + unregister_processor() + async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): """Test that the coordinator is unavailable after no data for a while.""" @@ -182,7 +190,7 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" ) assert coordinator.available is False # no data yet saved_callback = None @@ -192,23 +200,27 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): saved_callback = _callback return lambda: None + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) with patch( "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): + unregister_processor = coordinator.async_register_processor(processor) - mock_entity = MagicMock() - mock_add_entities = MagicMock() - coordinator.async_add_entities_listener( - mock_entity, - mock_add_entities, - ) + mock_entity = MagicMock() + mock_add_entities = MagicMock() + processor.async_add_entities_listener( + mock_entity, + mock_add_entities, + ) assert coordinator.available is False + assert processor.available is False saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True + assert processor.available is True scanner = _get_underlying_scanner() with patch( @@ -224,10 +236,12 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): ) await hass.async_block_till_done() assert coordinator.available is False + assert processor.available is False saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True + assert processor.available is True with patch( "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", @@ -242,6 +256,9 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): ) await hass.async_block_till_done() assert coordinator.available is False + assert processor.available is False + + unregister_processor() async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): @@ -256,7 +273,7 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" ) assert coordinator.available is False # no data yet saved_callback = None @@ -266,20 +283,23 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): saved_callback = _callback return lambda: None + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): + unregister_processor = coordinator.async_register_processor(processor) - all_events = [] + all_events = [] - def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: - """Mock an all listener.""" - all_events.append(data) + def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock an all listener.""" + all_events.append(data) - coordinator.async_add_listener( - _all_listener, - ) + processor.async_add_listener( + _all_listener, + ) saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert len(all_events) == 1 @@ -289,6 +309,7 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): # We should stop processing events once hass is stopping saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert len(all_events) == 1 + unregister_processor() async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_start): @@ -309,7 +330,7 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" ) assert coordinator.available is False # no data yet saved_callback = None @@ -319,23 +340,27 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta saved_callback = _callback return lambda: None + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) with patch( "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - coordinator.async_add_listener(MagicMock()) + unregister_processor = coordinator.async_register_processor(processor) + + processor.async_add_listener(MagicMock()) saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert coordinator.available is True + assert processor.available is True # We should go unavailable once we get an exception saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert "Test exception" in caplog.text - assert coordinator.available is False + assert processor.available is False # We should go available again once we get data again saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert coordinator.available is True + assert processor.available is True + unregister_processor() async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): @@ -356,7 +381,7 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" ) assert coordinator.available is False # no data yet saved_callback = None @@ -366,24 +391,28 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): saved_callback = _callback return lambda: None + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) with patch( "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - coordinator.async_add_listener(MagicMock()) + unregister_processor = coordinator.async_register_processor(processor) + + processor.async_add_listener(MagicMock()) saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert coordinator.available is True + assert processor.available is True # We should go unavailable once we get bad data with pytest.raises(ValueError): saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert coordinator.available is False + assert processor.available is False # We should go available again once we get good data again saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert coordinator.available is True + assert processor.available is True + unregister_processor() GOVEE_B5178_REMOTE_SERVICE_INFO = BluetoothServiceInfo( @@ -692,7 +721,7 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): return GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" ) assert coordinator.available is False # no data yet saved_callback = None @@ -702,16 +731,19 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): saved_callback = _callback return lambda: None + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) with patch( "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): - coordinator.async_add_listener(MagicMock()) + coordinator.async_register_processor(processor) + + processor.async_add_listener(MagicMock()) mock_add_entities = MagicMock() - coordinator.async_add_entities_listener( - PassiveBluetoothCoordinatorEntity, + processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, mock_add_entities, ) @@ -736,7 +768,7 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): *mock_add_entities.mock_calls[1][1][0], ] - entity_one: PassiveBluetoothCoordinatorEntity = entities[0] + entity_one: PassiveBluetoothProcessorEntity = entities[0] entity_one.hass = hass assert entity_one.available is True assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature-remote" @@ -797,7 +829,7 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" ) assert coordinator.available is False # no data yet saved_callback = None @@ -807,17 +839,19 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner saved_callback = _callback return lambda: None + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) with patch( "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): + coordinator.async_register_processor(processor) - mock_add_entities = MagicMock() + mock_add_entities = MagicMock() - coordinator.async_add_entities_listener( - PassiveBluetoothCoordinatorEntity, - mock_add_entities, - ) + processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_entities, + ) saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) # First call with just the remote sensor entities results in them being added @@ -828,7 +862,7 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner assert len(mock_add_entities.mock_calls) == 1 entities = mock_add_entities.mock_calls[0][1][0] - entity_one: PassiveBluetoothCoordinatorEntity = entities[0] + entity_one: PassiveBluetoothProcessorEntity = entities[0] entity_one.hass = hass assert entity_one.available is True assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature" @@ -857,7 +891,7 @@ async def test_passive_bluetooth_entity_with_entity_platform( return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", _async_generate_mock_data + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" ) assert coordinator.available is False # no data yet saved_callback = None @@ -867,18 +901,19 @@ async def test_passive_bluetooth_entity_with_entity_platform( saved_callback = _callback return lambda: None + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) with patch( "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", _async_register_callback, ): + coordinator.async_register_processor(processor) - coordinator.async_add_entities_listener( - PassiveBluetoothCoordinatorEntity, - lambda entities: hass.async_create_task( - entity_platform.async_add_entities(entities) - ), - ) - + processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + lambda entities: hass.async_create_task( + entity_platform.async_add_entities(entities) + ), + ) saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) await hass.async_block_till_done() saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) @@ -891,3 +926,133 @@ async def test_passive_bluetooth_entity_with_entity_platform( hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_pressure") is not None ) + + +SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + None: DeviceInfo( + name="Test Device", model="Test Model", manufacturer="Test Manufacturer" + ), + }, + entity_data={ + PassiveBluetoothEntityKey("pressure", None): 1234, + }, + entity_names={ + PassiveBluetoothEntityKey("pressure", None): "Pressure", + }, + entity_descriptions={ + PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( + key="pressure", + native_unit_of_measurement="hPa", + device_class=SensorDeviceClass.PRESSURE, + ), + }, +) + + +BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + None: DeviceInfo( + name="Test Device", model="Test Model", manufacturer="Test Manufacturer" + ), + }, + entity_data={ + PassiveBluetoothEntityKey("motion", None): True, + }, + entity_names={ + PassiveBluetoothEntityKey("motion", None): "Motion", + }, + entity_descriptions={ + PassiveBluetoothEntityKey("motion", None): BinarySensorEntityDescription( + key="motion", + device_class=BinarySensorDeviceClass.MOTION, + ), + }, +) + + +async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_start): + """Test integration of PassiveBluetoothDataUpdateCoordinator with multiple platforms.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + coordinator = PassiveBluetoothDataUpdateCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + binary_sensor_processor = PassiveBluetoothDataProcessor( + lambda service_info: BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE + ) + sesnor_processor = PassiveBluetoothDataProcessor( + lambda service_info: SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE + ) + + with patch( + "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_register_processor(binary_sensor_processor) + coordinator.async_register_processor(sesnor_processor) + + binary_sensor_processor.async_add_listener(MagicMock()) + sesnor_processor.async_add_listener(MagicMock()) + + mock_add_sensor_entities = MagicMock() + mock_add_binary_sensor_entities = MagicMock() + + sesnor_processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_sensor_entities, + ) + binary_sensor_processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_binary_sensor_entities, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # First call with just the remote sensor entities results in them being added + assert len(mock_add_binary_sensor_entities.mock_calls) == 1 + assert len(mock_add_sensor_entities.mock_calls) == 1 + + binary_sesnor_entities = [ + *mock_add_binary_sensor_entities.mock_calls[0][1][0], + ] + sesnor_entities = [ + *mock_add_sensor_entities.mock_calls[0][1][0], + ] + + sensor_entity_one: PassiveBluetoothProcessorEntity = sesnor_entities[0] + sensor_entity_one.hass = hass + assert sensor_entity_one.available is True + assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure" + assert sensor_entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "manufacturer": "Test Manufacturer", + "model": "Test Model", + "name": "Test Device", + } + assert sensor_entity_one.entity_key == PassiveBluetoothEntityKey( + key="pressure", device_id=None + ) + + binary_sensor_entity_one: PassiveBluetoothProcessorEntity = binary_sesnor_entities[ + 0 + ] + binary_sensor_entity_one.hass = hass + assert binary_sensor_entity_one.available is True + assert binary_sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-motion" + assert binary_sensor_entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "manufacturer": "Test Manufacturer", + "model": "Test Model", + "name": "Test Device", + } + assert binary_sensor_entity_one.entity_key == PassiveBluetoothEntityKey( + key="motion", device_id=None + ) From 7cf2d1759dde088105f77ca61dba8e58e3474b83 Mon Sep 17 00:00:00 2001 From: On Freund Date: Sun, 24 Jul 2022 00:44:48 +0300 Subject: [PATCH 2809/3516] Upgrade pyrisco to 0.5.0 (#75648) * Upgrade to pyrisco 0.4.0 * Parametrized error tests in config flow * Inline error parameters * Switch to RiscoCloud --- homeassistant/components/risco/__init__.py | 4 +- homeassistant/components/risco/config_flow.py | 4 +- homeassistant/components/risco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../risco/test_alarm_control_panel.py | 40 ++++++++--- tests/components/risco/test_binary_sensor.py | 8 +-- tests/components/risco/test_config_flow.py | 66 ++++++------------- tests/components/risco/test_sensor.py | 8 +-- tests/components/risco/util.py | 12 ++-- 10 files changed, 71 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 1932548a907..fea3ee63aac 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -2,7 +2,7 @@ from datetime import timedelta import logging -from pyrisco import CannotConnectError, OperationError, RiscoAPI, UnauthorizedError +from pyrisco import CannotConnectError, OperationError, RiscoCloud, UnauthorizedError from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -30,7 +30,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Risco from a config entry.""" data = entry.data - risco = RiscoAPI(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN]) + risco = RiscoCloud(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN]) try: await risco.login(async_get_clientsession(hass)) except CannotConnectError as error: diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index e2e139a19e0..5f8f40cb5f7 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from pyrisco import CannotConnectError, RiscoAPI, UnauthorizedError +from pyrisco import CannotConnectError, RiscoCloud, UnauthorizedError import voluptuous as vol from homeassistant import config_entries, core @@ -52,7 +52,7 @@ async def validate_input(hass: core.HomeAssistant, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ - risco = RiscoAPI(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN]) + risco = RiscoCloud(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN]) try: await risco.login(async_get_clientsession(hass)) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 736adcf0c35..a7c07af3e18 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -3,7 +3,7 @@ "name": "Risco", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", - "requirements": ["pyrisco==0.3.1"], + "requirements": ["pyrisco==0.5.0"], "codeowners": ["@OnFreund"], "quality_scale": "platinum", "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 80ca6871b48..4309d2f34c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1785,7 +1785,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.3.1 +pyrisco==0.5.0 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6d0e6f54715..4d8e790b50f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1222,7 +1222,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.3.1 +pyrisco==0.5.0 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index 70ec7844624..4a82656147d 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -99,7 +99,7 @@ def two_part_alarm(): "partitions", new_callable=PropertyMock(return_value=partition_mocks), ), patch( - "homeassistant.components.risco.RiscoAPI.get_state", + "homeassistant.components.risco.RiscoCloud.get_state", return_value=alarm_mock, ): yield alarm_mock @@ -109,7 +109,7 @@ async def test_cannot_connect(hass): """Test connection error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", side_effect=CannotConnectError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) @@ -125,7 +125,7 @@ async def test_unauthorized(hass): """Test unauthorized error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", side_effect=UnauthorizedError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) @@ -228,7 +228,7 @@ async def test_states(hass, two_part_alarm): async def _test_service_call( hass, service, method, entity_id, partition_id, *args, **kwargs ): - with patch(f"homeassistant.components.risco.RiscoAPI.{method}") as set_mock: + with patch(f"homeassistant.components.risco.RiscoCloud.{method}") as set_mock: await _call_alarm_service(hass, service, entity_id, **kwargs) set_mock.assert_awaited_once_with(partition_id, *args) @@ -236,7 +236,7 @@ async def _test_service_call( async def _test_no_service_call( hass, service, method, entity_id, partition_id, **kwargs ): - with patch(f"homeassistant.components.risco.RiscoAPI.{method}") as set_mock: + with patch(f"homeassistant.components.risco.RiscoCloud.{method}") as set_mock: await _call_alarm_service(hass, service, entity_id, **kwargs) set_mock.assert_not_awaited() @@ -302,10 +302,20 @@ async def test_sets_full_custom_mapping(hass, two_part_alarm): hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", SECOND_ENTITY_ID, 1, "C" ) await _test_service_call( - hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "group_arm", FIRST_ENTITY_ID, 0, "D" + hass, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "group_arm", + FIRST_ENTITY_ID, + 0, + "D", ) await _test_service_call( - hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "group_arm", SECOND_ENTITY_ID, 1, "D" + hass, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + "group_arm", + SECOND_ENTITY_ID, + 1, + "D", ) @@ -333,10 +343,22 @@ async def test_sets_with_correct_code(hass, two_part_alarm): hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_ENTITY_ID, 1, **code ) await _test_service_call( - hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", FIRST_ENTITY_ID, 0, "C", **code + hass, + SERVICE_ALARM_ARM_NIGHT, + "group_arm", + FIRST_ENTITY_ID, + 0, + "C", + **code, ) await _test_service_call( - hass, SERVICE_ALARM_ARM_NIGHT, "group_arm", SECOND_ENTITY_ID, 1, "C", **code + hass, + SERVICE_ALARM_ARM_NIGHT, + "group_arm", + SECOND_ENTITY_ID, + 1, + "C", + **code, ) with pytest.raises(HomeAssistantError): await _test_no_service_call( diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py index 7f68db7939d..a7c11c9cb00 100644 --- a/tests/components/risco/test_binary_sensor.py +++ b/tests/components/risco/test_binary_sensor.py @@ -20,7 +20,7 @@ async def test_cannot_connect(hass): """Test connection error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", side_effect=CannotConnectError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) @@ -36,7 +36,7 @@ async def test_unauthorized(hass): """Test unauthorized error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", side_effect=UnauthorizedError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) @@ -106,7 +106,7 @@ async def test_states(hass, two_zone_alarm): # noqa: F811 async def test_bypass(hass, two_zone_alarm): # noqa: F811 """Test bypassing a zone.""" await setup_risco(hass) - with patch("homeassistant.components.risco.RiscoAPI.bypass_zone") as mock: + with patch("homeassistant.components.risco.RiscoCloud.bypass_zone") as mock: data = {"entity_id": FIRST_ENTITY_ID} await hass.services.async_call( @@ -119,7 +119,7 @@ async def test_bypass(hass, two_zone_alarm): # noqa: F811 async def test_unbypass(hass, two_zone_alarm): # noqa: F811 """Test unbypassing a zone.""" await setup_risco(hass) - with patch("homeassistant.components.risco.RiscoAPI.bypass_zone") as mock: + with patch("homeassistant.components.risco.RiscoCloud.bypass_zone") as mock: data = {"entity_id": FIRST_ENTITY_ID} await hass.services.async_call( diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index 8dd7c4bf7f7..8d04f478e44 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -51,13 +51,13 @@ async def test_form(hass): assert result["errors"] == {} with patch( - "homeassistant.components.risco.config_flow.RiscoAPI.login", + "homeassistant.components.risco.config_flow.RiscoCloud.login", return_value=True, ), patch( - "homeassistant.components.risco.config_flow.RiscoAPI.site_name", + "homeassistant.components.risco.config_flow.RiscoCloud.site_name", new_callable=PropertyMock(return_value=TEST_SITE_NAME), ), patch( - "homeassistant.components.risco.config_flow.RiscoAPI.close" + "homeassistant.components.risco.config_flow.RiscoCloud.close" ) as mock_close, patch( "homeassistant.components.risco.async_setup_entry", return_value=True, @@ -74,61 +74,33 @@ async def test_form(hass): mock_close.assert_awaited_once() -async def test_form_invalid_auth(hass): - """Test we handle invalid auth.""" +@pytest.mark.parametrize( + "exception, error", + [ + (UnauthorizedError, "invalid_auth"), + (CannotConnectError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_error(hass, exception, error): + """Test we handle config flow errors.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( - "homeassistant.components.risco.config_flow.RiscoAPI.login", - side_effect=UnauthorizedError, - ), patch("homeassistant.components.risco.config_flow.RiscoAPI.close") as mock_close: + "homeassistant.components.risco.config_flow.RiscoCloud.login", + side_effect=exception, + ), patch( + "homeassistant.components.risco.config_flow.RiscoCloud.close" + ) as mock_close: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], TEST_DATA ) - assert result2["type"] == "form" - assert result2["errors"] == {"base": "invalid_auth"} mock_close.assert_awaited_once() - - -async def test_form_cannot_connect(hass): - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.risco.config_flow.RiscoAPI.login", - side_effect=CannotConnectError, - ), patch("homeassistant.components.risco.config_flow.RiscoAPI.close") as mock_close: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_DATA - ) - assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} - mock_close.assert_awaited_once() - - -async def test_form_exception(hass): - """Test we handle unknown exception.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.risco.config_flow.RiscoAPI.login", - side_effect=Exception, - ), patch("homeassistant.components.risco.config_flow.RiscoAPI.close") as mock_close: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], TEST_DATA - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"base": "unknown"} - mock_close.assert_awaited_once() + assert result2["errors"] == {"base": error} async def test_form_already_exists(hass): diff --git a/tests/components/risco/test_sensor.py b/tests/components/risco/test_sensor.py index 24efbabf087..8fb4daf8624 100644 --- a/tests/components/risco/test_sensor.py +++ b/tests/components/risco/test_sensor.py @@ -113,7 +113,7 @@ async def test_cannot_connect(hass): """Test connection error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", side_effect=CannotConnectError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) @@ -130,7 +130,7 @@ async def test_unauthorized(hass): """Test unauthorized error.""" with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", side_effect=UnauthorizedError, ): config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) @@ -175,7 +175,7 @@ async def test_setup(hass, two_zone_alarm): # noqa: F811 assert not registry.async_is_registered(id) with patch( - "homeassistant.components.risco.RiscoAPI.site_uuid", + "homeassistant.components.risco.RiscoCloud.site_uuid", new_callable=PropertyMock(return_value=TEST_SITE_UUID), ), patch( "homeassistant.components.risco.Store.async_save", @@ -191,7 +191,7 @@ async def test_setup(hass, two_zone_alarm): # noqa: F811 _check_state(hass, category, entity_id) with patch( - "homeassistant.components.risco.RiscoAPI.get_events", return_value=[] + "homeassistant.components.risco.RiscoCloud.get_events", return_value=[] ) as events_mock, patch( "homeassistant.components.risco.Store.async_load", return_value={LAST_EVENT_TIMESTAMP_KEY: TEST_EVENTS[0].time}, diff --git a/tests/components/risco/util.py b/tests/components/risco/util.py index 8b918f32c12..3fa81586d27 100644 --- a/tests/components/risco/util.py +++ b/tests/components/risco/util.py @@ -23,18 +23,18 @@ async def setup_risco(hass, events=[], options={}): config_entry.add_to_hass(hass) with patch( - "homeassistant.components.risco.RiscoAPI.login", + "homeassistant.components.risco.RiscoCloud.login", return_value=True, ), patch( - "homeassistant.components.risco.RiscoAPI.site_uuid", + "homeassistant.components.risco.RiscoCloud.site_uuid", new_callable=PropertyMock(return_value=TEST_SITE_UUID), ), patch( - "homeassistant.components.risco.RiscoAPI.site_name", + "homeassistant.components.risco.RiscoCloud.site_name", new_callable=PropertyMock(return_value=TEST_SITE_NAME), ), patch( - "homeassistant.components.risco.RiscoAPI.close" + "homeassistant.components.risco.RiscoCloud.close" ), patch( - "homeassistant.components.risco.RiscoAPI.get_events", + "homeassistant.components.risco.RiscoCloud.get_events", return_value=events, ): await hass.config_entries.async_setup(config_entry.entry_id) @@ -68,7 +68,7 @@ def two_zone_alarm(): "zones", new_callable=PropertyMock(return_value=zone_mocks), ), patch( - "homeassistant.components.risco.RiscoAPI.get_state", + "homeassistant.components.risco.RiscoCloud.get_state", return_value=alarm_mock, ): yield alarm_mock From 19f82e52016ebd7a35543d09a62b83d1c26197c8 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 24 Jul 2022 00:28:19 +0000 Subject: [PATCH 2810/3516] [ci skip] Translation update --- .../components/bluetooth/translations/ca.json | 22 +++++++++++++++++++ .../components/bluetooth/translations/de.json | 6 +++++ .../components/bluetooth/translations/el.json | 6 +++++ .../components/bluetooth/translations/fr.json | 22 +++++++++++++++++++ .../components/bluetooth/translations/hu.json | 22 +++++++++++++++++++ .../components/bluetooth/translations/pl.json | 6 +++++ .../components/bluetooth/translations/ru.json | 22 +++++++++++++++++++ .../components/demo/translations/hu.json | 21 ++++++++++++++++++ .../components/demo/translations/ru.json | 15 +++++++++++++ .../components/inkbird/translations/ca.json | 21 ++++++++++++++++++ .../components/inkbird/translations/fr.json | 21 ++++++++++++++++++ .../components/inkbird/translations/hu.json | 21 ++++++++++++++++++ .../components/inkbird/translations/ru.json | 21 ++++++++++++++++++ .../components/plugwise/translations/hu.json | 3 ++- .../components/plugwise/translations/ru.json | 3 ++- .../sensorpush/translations/ca.json | 21 ++++++++++++++++++ .../sensorpush/translations/fr.json | 21 ++++++++++++++++++ .../sensorpush/translations/hu.json | 21 ++++++++++++++++++ .../sensorpush/translations/ru.json | 21 ++++++++++++++++++ .../components/uscis/translations/ca.json | 1 + .../components/uscis/translations/hu.json | 8 +++++++ .../components/uscis/translations/ru.json | 8 +++++++ .../xiaomi_ble/translations/ca.json | 21 ++++++++++++++++++ .../xiaomi_ble/translations/de.json | 21 ++++++++++++++++++ .../xiaomi_ble/translations/el.json | 21 ++++++++++++++++++ .../xiaomi_ble/translations/fr.json | 21 ++++++++++++++++++ .../xiaomi_ble/translations/hu.json | 21 ++++++++++++++++++ .../xiaomi_ble/translations/id.json | 7 ++++++ .../xiaomi_ble/translations/pl.json | 21 ++++++++++++++++++ .../xiaomi_ble/translations/ru.json | 21 ++++++++++++++++++ .../components/zha/translations/ca.json | 1 + .../components/zha/translations/hu.json | 1 + .../components/zha/translations/ru.json | 1 + 33 files changed, 488 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/bluetooth/translations/ca.json create mode 100644 homeassistant/components/bluetooth/translations/fr.json create mode 100644 homeassistant/components/bluetooth/translations/hu.json create mode 100644 homeassistant/components/bluetooth/translations/ru.json create mode 100644 homeassistant/components/inkbird/translations/ca.json create mode 100644 homeassistant/components/inkbird/translations/fr.json create mode 100644 homeassistant/components/inkbird/translations/hu.json create mode 100644 homeassistant/components/inkbird/translations/ru.json create mode 100644 homeassistant/components/sensorpush/translations/ca.json create mode 100644 homeassistant/components/sensorpush/translations/fr.json create mode 100644 homeassistant/components/sensorpush/translations/hu.json create mode 100644 homeassistant/components/sensorpush/translations/ru.json create mode 100644 homeassistant/components/uscis/translations/hu.json create mode 100644 homeassistant/components/uscis/translations/ru.json create mode 100644 homeassistant/components/xiaomi_ble/translations/ca.json create mode 100644 homeassistant/components/xiaomi_ble/translations/de.json create mode 100644 homeassistant/components/xiaomi_ble/translations/el.json create mode 100644 homeassistant/components/xiaomi_ble/translations/fr.json create mode 100644 homeassistant/components/xiaomi_ble/translations/hu.json create mode 100644 homeassistant/components/xiaomi_ble/translations/id.json create mode 100644 homeassistant/components/xiaomi_ble/translations/pl.json create mode 100644 homeassistant/components/xiaomi_ble/translations/ru.json diff --git a/homeassistant/components/bluetooth/translations/ca.json b/homeassistant/components/bluetooth/translations/ca.json new file mode 100644 index 00000000000..8fb4c022e0a --- /dev/null +++ b/homeassistant/components/bluetooth/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "enable_bluetooth": { + "description": "Vols configurar Bluetooth?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/de.json b/homeassistant/components/bluetooth/translations/de.json index 1c677be6828..ef305862210 100644 --- a/homeassistant/components/bluetooth/translations/de.json +++ b/homeassistant/components/bluetooth/translations/de.json @@ -1,10 +1,16 @@ { "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "M\u00f6chtest du {name} einrichten?" }, + "enable_bluetooth": { + "description": "M\u00f6chtest du Bluetooth einrichten?" + }, "user": { "data": { "address": "Ger\u00e4t" diff --git a/homeassistant/components/bluetooth/translations/el.json b/homeassistant/components/bluetooth/translations/el.json index 05198f0929a..03b3be612a3 100644 --- a/homeassistant/components/bluetooth/translations/el.json +++ b/homeassistant/components/bluetooth/translations/el.json @@ -1,10 +1,16 @@ { "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" }, + "enable_bluetooth": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Bluetooth;" + }, "user": { "data": { "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" diff --git a/homeassistant/components/bluetooth/translations/fr.json b/homeassistant/components/bluetooth/translations/fr.json new file mode 100644 index 00000000000..80a0ac6ea3f --- /dev/null +++ b/homeassistant/components/bluetooth/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "enable_bluetooth": { + "description": "Voulez-vous configurer le Bluetooth\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/hu.json b/homeassistant/components/bluetooth/translations/hu.json new file mode 100644 index 00000000000..5cdc199476c --- /dev/null +++ b/homeassistant/components/bluetooth/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "enable_bluetooth": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani a Bluetooth-ot?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/pl.json b/homeassistant/components/bluetooth/translations/pl.json index 4b93cce9f7e..99ad564ebd9 100644 --- a/homeassistant/components/bluetooth/translations/pl.json +++ b/homeassistant/components/bluetooth/translations/pl.json @@ -1,10 +1,16 @@ { "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, + "enable_bluetooth": { + "description": "Czy chcesz skonfigurowa\u0107 Bluetooth?" + }, "user": { "data": { "address": "Urz\u0105dzenie" diff --git a/homeassistant/components/bluetooth/translations/ru.json b/homeassistant/components/bluetooth/translations/ru.json new file mode 100644 index 00000000000..972b858718c --- /dev/null +++ b/homeassistant/components/bluetooth/translations/ru.json @@ -0,0 +1,22 @@ +{ + "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." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "enable_bluetooth": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Bluetooth?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/hu.json b/homeassistant/components/demo/translations/hu.json index 87810814aac..1ff83e4ed06 100644 --- a/homeassistant/components/demo/translations/hu.json +++ b/homeassistant/components/demo/translations/hu.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Nyomja meg az OK gombot, ha a villog\u00f3 folyad\u00e9kot felt\u00f6lt\u00f6tt\u00e9k.", + "title": "A villog\u00f3 folyad\u00e9kot fel kell t\u00f6lteni" + } + } + }, + "title": "A villog\u00f3 folyad\u00e9k ki\u00fcr\u00fclt, \u00e9s \u00fajra kell t\u00f6lteni" + }, + "transmogrifier_deprecated": { + "description": "A transzmogrifier komponens az \u00faj API-ban el\u00e9rhet\u0151 helyi vez\u00e9rl\u00e9s hi\u00e1nya miatt elavult", + "title": "A transzmogrifier komponens elavult" + }, + "unfixable_problem": { + "description": "Ez az eset soha nem fog le\u00e1llni.", + "title": "Ez a hiba nem jav\u00edthat\u00f3" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/demo/translations/ru.json b/homeassistant/components/demo/translations/ru.json index e3bd96b880a..c08143d6b41 100644 --- a/homeassistant/components/demo/translations/ru.json +++ b/homeassistant/components/demo/translations/ru.json @@ -1,4 +1,19 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 OK, \u043a\u043e\u0433\u0434\u0430 \u0436\u0438\u0434\u043a\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u043f\u043e\u0432\u043e\u0440\u043e\u0442\u043d\u0438\u043a\u043e\u0432 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0430.", + "title": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u043e\u043b\u0438\u0442\u044c \u0436\u0438\u0434\u043a\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u043f\u043e\u0432\u043e\u0440\u043e\u0442\u043d\u0438\u043a\u043e\u0432" + } + } + } + }, + "unfixable_problem": { + "title": "\u042d\u0442\u043e \u043d\u0435 \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u043c\u0430\u044f \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/inkbird/translations/ca.json b/homeassistant/components/inkbird/translations/ca.json new file mode 100644 index 00000000000..0cd4571dc9d --- /dev/null +++ b/homeassistant/components/inkbird/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/fr.json b/homeassistant/components/inkbird/translations/fr.json new file mode 100644 index 00000000000..c8a1af034cf --- /dev/null +++ b/homeassistant/components/inkbird/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/hu.json b/homeassistant/components/inkbird/translations/hu.json new file mode 100644 index 00000000000..7ef0d3a6301 --- /dev/null +++ b/homeassistant/components/inkbird/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/ru.json b/homeassistant/components/inkbird/translations/ru.json new file mode 100644 index 00000000000..c912fc120e4 --- /dev/null +++ b/homeassistant/components/inkbird/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/hu.json b/homeassistant/components/plugwise/translations/hu.json index b11137a2658..c97911bad09 100644 --- a/homeassistant/components/plugwise/translations/hu.json +++ b/homeassistant/components/plugwise/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "anna_with_adam": "Anna \u00e9s \u00c1d\u00e1m is \u00e9szlelve lett. Adja hozz\u00e1 az \u00c1d\u00e1mot az Anna helyett" }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json index c7cfa232fd0..92d78e7f4ad 100644 --- a/homeassistant/components/plugwise/translations/ru.json +++ b/homeassistant/components/plugwise/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "anna_with_adam": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b \u0410\u043d\u043d\u0430 \u0438 \u0410\u0434\u0430\u043c. \u0414\u043e\u0431\u0430\u0432\u044c\u0442\u0435 \u0410\u0434\u0430\u043c\u0430 \u0432\u043c\u0435\u0441\u0442\u043e \u0410\u043d\u043d\u044b." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", diff --git a/homeassistant/components/sensorpush/translations/ca.json b/homeassistant/components/sensorpush/translations/ca.json new file mode 100644 index 00000000000..0cd4571dc9d --- /dev/null +++ b/homeassistant/components/sensorpush/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/fr.json b/homeassistant/components/sensorpush/translations/fr.json new file mode 100644 index 00000000000..c8a1af034cf --- /dev/null +++ b/homeassistant/components/sensorpush/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/hu.json b/homeassistant/components/sensorpush/translations/hu.json new file mode 100644 index 00000000000..7ef0d3a6301 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/ru.json b/homeassistant/components/sensorpush/translations/ru.json new file mode 100644 index 00000000000..c912fc120e4 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/ca.json b/homeassistant/components/uscis/translations/ca.json index 7c836867d89..2072cfb0d92 100644 --- a/homeassistant/components/uscis/translations/ca.json +++ b/homeassistant/components/uscis/translations/ca.json @@ -1,6 +1,7 @@ { "issues": { "pending_removal": { + "description": "La integraci\u00f3 de Serveis de Ciutadania i Immigraci\u00f3 dels Estats Units (USCIS) est\u00e0 pendent d'eliminar-se de Home Assistant i ja no estar\u00e0 disponible a partir de Home Assistant 2022.10. \n\nLa integraci\u00f3 s'est\u00e0 eliminant, perqu\u00e8 es basa en el 'webscraping', que no est\u00e0 adm\u00e8s. \n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per arreglar aquest error.", "title": "S'est\u00e0 eliminant la integraci\u00f3 USCIS" } } diff --git a/homeassistant/components/uscis/translations/hu.json b/homeassistant/components/uscis/translations/hu.json new file mode 100644 index 00000000000..181cbaf076f --- /dev/null +++ b/homeassistant/components/uscis/translations/hu.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "A U.S. Citizenship and Immigration Services (USCIS) integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra v\u00e1r a Home Assistantb\u00f3l, \u00e9s a Home Assistant 2022.10-t\u0151l m\u00e1r nem lesz el\u00e9rhet\u0151.\n\nAz integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sa az\u00e9rt t\u00f6rt\u00e9nik, mert webscrapingre t\u00e1maszkodik, ami nem megengedett.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "Az USCIS integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/ru.json b/homeassistant/components/uscis/translations/ru.json new file mode 100644 index 00000000000..f3b70020245 --- /dev/null +++ b/homeassistant/components/uscis/translations/ru.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0421\u043b\u0443\u0436\u0431\u044b \u0433\u0440\u0430\u0436\u0434\u0430\u043d\u0441\u0442\u0432\u0430 \u0438 \u0438\u043c\u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0421\u0428\u0410 (USCIS) \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10. \n\n\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0430 \u043d\u0430 \u0432\u0435\u0431-\u0441\u043a\u0440\u0430\u043f\u0438\u043d\u0433\u0435, \u0447\u0442\u043e \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043e. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e YAML \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f USCIS \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/ca.json b/homeassistant/components/xiaomi_ble/translations/ca.json new file mode 100644 index 00000000000..0cd4571dc9d --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/de.json b/homeassistant/components/xiaomi_ble/translations/de.json new file mode 100644 index 00000000000..81dda510bc5 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/el.json b/homeassistant/components/xiaomi_ble/translations/el.json new file mode 100644 index 00000000000..0a802a0bc89 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/fr.json b/homeassistant/components/xiaomi_ble/translations/fr.json new file mode 100644 index 00000000000..c8a1af034cf --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/hu.json b/homeassistant/components/xiaomi_ble/translations/hu.json new file mode 100644 index 00000000000..7ef0d3a6301 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/id.json b/homeassistant/components/xiaomi_ble/translations/id.json new file mode 100644 index 00000000000..8b24900933b --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/id.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_in_progress": "Alur konfigurasi sedang berlangsung" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/pl.json b/homeassistant/components/xiaomi_ble/translations/pl.json new file mode 100644 index 00000000000..51168716783 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/ru.json b/homeassistant/components/xiaomi_ble/translations/ru.json new file mode 100644 index 00000000000..c912fc120e4 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index 2fe8d1a151c..4167704e4a0 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "Considera els dispositius connectats a la xarxa el\u00e8ctrica com a no disponibles al cap de (segons)", "default_light_transition": "Temps de transici\u00f3 predeterminat (segons)", "enable_identify_on_join": "Activa l'efecte d'identificaci\u00f3 quan els dispositius s'uneixin a la xarxa", + "enhanced_light_transition": "Activa la transici\u00f3 millorada de color/temperatura de llum des de l'estat apagat", "title": "Opcions globals" } }, diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index d446de8e17a..61b0f0c0c9d 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "H\u00e1l\u00f3zati t\u00e1pell\u00e1t\u00e1s\u00fa eszk\u00f6z\u00f6k nem el\u00e9rhet\u0151 \u00e1llapot\u00faak ennyi id\u0151 ut\u00e1n (mp.)", "default_light_transition": "Alap\u00e9rtelmezett f\u00e9ny-\u00e1tmeneti id\u0151 (m\u00e1sodpercben)", "enable_identify_on_join": "Azonos\u00edt\u00f3 hat\u00e1s, amikor az eszk\u00f6z\u00f6k csatlakoznak a h\u00e1l\u00f3zathoz", + "enhanced_light_transition": "F\u00e9ny sz\u00edn/sz\u00ednh\u0151m\u00e9rs\u00e9klet \u00e1tmenete kikapcsolt \u00e1llapotb\u00f3l", "title": "Glob\u00e1lis be\u00e1ll\u00edt\u00e1sok" } }, diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index be7fb30dbec..83ba818087a 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043e\u0442 \u0441\u0435\u0442\u0438 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "default_light_transition": "\u0412\u0440\u0435\u043c\u044f \u043f\u043b\u0430\u0432\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u0441\u0432\u0435\u0442\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "enable_identify_on_join": "\u042d\u0444\u0444\u0435\u043a\u0442 \u0434\u043b\u044f \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043a \u0441\u0435\u0442\u0438", + "enhanced_light_transition": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0443\u043b\u0443\u0447\u0448\u0435\u043d\u043d\u044b\u0439 \u043f\u0435\u0440\u0435\u0445\u043e\u0434 \u0446\u0432\u0435\u0442\u0430/\u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b \u0441\u0432\u0435\u0442\u0430 \u0438\u0437 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", "title": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" } }, From da131beced42203630778aa1460c1767200f00ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 23 Jul 2022 19:33:47 -0500 Subject: [PATCH 2811/3516] Split bluetooth coordinator into two classes (#75675) --- .../bluetooth/passive_update_coordinator.py | 397 +------ .../bluetooth/passive_update_processor.py | 346 ++++++ .../bluetooth/update_coordinator.py | 84 ++ homeassistant/components/inkbird/__init__.py | 6 +- homeassistant/components/inkbird/sensor.py | 6 +- .../components/sensorpush/__init__.py | 6 +- homeassistant/components/sensorpush/sensor.py | 6 +- .../components/xiaomi_ble/__init__.py | 6 +- homeassistant/components/xiaomi_ble/sensor.py | 6 +- .../test_passive_update_coordinator.py | 1033 ++-------------- .../test_passive_update_processor.py | 1058 +++++++++++++++++ tests/components/inkbird/test_sensor.py | 2 +- tests/components/sensorpush/test_sensor.py | 2 +- tests/components/xiaomi_ble/test_sensor.py | 4 +- 14 files changed, 1667 insertions(+), 1295 deletions(-) create mode 100644 homeassistant/components/bluetooth/passive_update_processor.py create mode 100644 homeassistant/components/bluetooth/update_coordinator.py create mode 100644 tests/components/bluetooth/test_passive_update_processor.py diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 507350f2162..4a22f03449e 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -1,65 +1,23 @@ -"""The Bluetooth integration.""" +"""Passive update coordinator for the Bluetooth integration.""" from __future__ import annotations -from collections.abc import Callable, Mapping -import dataclasses +from collections.abc import Callable, Generator import logging -import time -from typing import Any, Generic, TypeVar +from typing import Any -from home_assistant_bluetooth import BluetoothServiceInfo - -from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import ( - BluetoothCallbackMatcher, - BluetoothChange, - async_register_callback, - async_track_unavailable, -) -from .const import DOMAIN +from . import BluetoothChange +from .update_coordinator import BasePassiveBluetoothCoordinator -@dataclasses.dataclass(frozen=True) -class PassiveBluetoothEntityKey: - """Key for a passive bluetooth entity. +class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): + """Class to manage passive bluetooth advertisements. - Example: - key: temperature - device_id: outdoor_sensor_1 - """ - - key: str - device_id: str | None - - -_T = TypeVar("_T") - - -@dataclasses.dataclass(frozen=True) -class PassiveBluetoothDataUpdate(Generic[_T]): - """Generic bluetooth data.""" - - devices: dict[str | None, DeviceInfo] = dataclasses.field(default_factory=dict) - entity_descriptions: Mapping[ - PassiveBluetoothEntityKey, EntityDescription - ] = dataclasses.field(default_factory=dict) - entity_names: Mapping[PassiveBluetoothEntityKey, str | None] = dataclasses.field( - default_factory=dict - ) - entity_data: Mapping[PassiveBluetoothEntityKey, _T] = dataclasses.field( - default_factory=dict - ) - - -class PassiveBluetoothDataUpdateCoordinator: - """Passive bluetooth data update coordinator for bluetooth advertisements. - - The coordinator is responsible for dispatching the bluetooth data, - to each processor, and tracking devices. + This coordinator is responsible for dispatching the bluetooth data + and tracking devices. """ def __init__( @@ -68,78 +26,52 @@ class PassiveBluetoothDataUpdateCoordinator: logger: logging.Logger, address: str, ) -> None: - """Initialize the coordinator.""" - self.hass = hass - self.logger = logger - self.name: str | None = None - self.address = address - self._processors: list[PassiveBluetoothDataProcessor] = [] - self._cancel_track_unavailable: CALLBACK_TYPE | None = None - self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None - self._present = False - self.last_seen = 0.0 - - @property - def available(self) -> bool: - """Return if the device is available.""" - return self._present + """Initialize PassiveBluetoothDataUpdateCoordinator.""" + super().__init__(hass, logger, address) + self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {} @callback - def _async_start(self) -> None: - """Start the callbacks.""" - self._cancel_bluetooth_advertisements = async_register_callback( - self.hass, - self._async_handle_bluetooth_event, - BluetoothCallbackMatcher(address=self.address), - ) - self._cancel_track_unavailable = async_track_unavailable( - self.hass, - self._async_handle_unavailable, - self.address, - ) + def async_update_listeners(self) -> None: + """Update all registered listeners.""" + for update_callback, _ in list(self._listeners.values()): + update_callback() @callback - def _async_stop(self) -> None: - """Stop the callbacks.""" - if self._cancel_bluetooth_advertisements is not None: - self._cancel_bluetooth_advertisements() - self._cancel_bluetooth_advertisements = None - if self._cancel_track_unavailable is not None: - self._cancel_track_unavailable() - self._cancel_track_unavailable = None + def _async_handle_unavailable(self, address: str) -> None: + """Handle the device going unavailable.""" + super()._async_handle_unavailable(address) + self.async_update_listeners() @callback - def async_register_processor( - self, processor: PassiveBluetoothDataProcessor - ) -> Callable[[], None]: - """Register a processor that subscribes to updates.""" - processor.coordinator = self + def async_start(self) -> CALLBACK_TYPE: + """Start the data updater.""" + self._async_start() @callback - def remove_processor() -> None: - """Remove a processor.""" - self._processors.remove(processor) - self._async_handle_processors_changed() - - self._processors.append(processor) - self._async_handle_processors_changed() - return remove_processor - - @callback - def _async_handle_processors_changed(self) -> None: - """Handle processors changed.""" - running = bool(self._cancel_bluetooth_advertisements) - if running and not self._processors: + def _async_cancel() -> None: self._async_stop() - elif not running and self._processors: - self._async_start() + + return _async_cancel @callback - def _async_handle_unavailable(self, _address: str) -> None: - """Handle the device going unavailable.""" - self._present = False - for processor in self._processors: - processor.async_handle_unavailable() + def async_add_listener( + self, update_callback: CALLBACK_TYPE, context: Any = None + ) -> Callable[[], None]: + """Listen for data updates.""" + + @callback + def remove_listener() -> None: + """Remove update listener.""" + self._listeners.pop(remove_listener) + + self._listeners[remove_listener] = (update_callback, context) + return remove_listener + + def async_contexts(self) -> Generator[Any, None, None]: + """Return all registered contexts.""" + yield from ( + context for _, context in self._listeners.values() if context is not None + ) @callback def _async_handle_bluetooth_event( @@ -148,242 +80,19 @@ class PassiveBluetoothDataUpdateCoordinator: change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" - self.last_seen = time.monotonic() - self.name = service_info.name - self._present = True - if self.hass.is_stopping: - return - for processor in self._processors: - processor.async_handle_bluetooth_event(service_info, change) + super()._async_handle_bluetooth_event(service_info, change) + self.async_update_listeners() -_PassiveBluetoothDataProcessorT = TypeVar( - "_PassiveBluetoothDataProcessorT", - bound="PassiveBluetoothDataProcessor[Any]", -) - - -class PassiveBluetoothDataProcessor(Generic[_T]): - """Passive bluetooth data processor for bluetooth advertisements. - - The processor is responsible for keeping track of the bluetooth data - and updating subscribers. - - The update_method must return a PassiveBluetoothDataUpdate object. Callers - are responsible for formatting the data returned from their parser into - the appropriate format. - - The processor will call the update_method every time the bluetooth device - receives a new advertisement data from the coordinator with the following signature: - - update_method(service_info: BluetoothServiceInfo) -> PassiveBluetoothDataUpdate - - As the size of each advertisement is limited, the update_method should - return a PassiveBluetoothDataUpdate object that contains only data that - should be updated. The coordinator will then dispatch subscribers based - on the data in the PassiveBluetoothDataUpdate object. The accumulated data - is available in the devices, entity_data, and entity_descriptions attributes. - """ +class PassiveBluetoothCoordinatorEntity(CoordinatorEntity): + """A class for entities using DataUpdateCoordinator.""" coordinator: PassiveBluetoothDataUpdateCoordinator - def __init__( - self, - update_method: Callable[[BluetoothServiceInfo], PassiveBluetoothDataUpdate[_T]], - ) -> None: - """Initialize the coordinator.""" - self.coordinator: PassiveBluetoothDataUpdateCoordinator - self._listeners: list[ - Callable[[PassiveBluetoothDataUpdate[_T] | None], None] - ] = [] - self._entity_key_listeners: dict[ - PassiveBluetoothEntityKey, - list[Callable[[PassiveBluetoothDataUpdate[_T] | None], None]], - ] = {} - self.update_method = update_method - self.entity_names: dict[PassiveBluetoothEntityKey, str | None] = {} - self.entity_data: dict[PassiveBluetoothEntityKey, _T] = {} - self.entity_descriptions: dict[ - PassiveBluetoothEntityKey, EntityDescription - ] = {} - self.devices: dict[str | None, DeviceInfo] = {} - self.last_update_success = True - - @property - def available(self) -> bool: - """Return if the device is available.""" - return self.coordinator.available and self.last_update_success - - @callback - def async_handle_unavailable(self) -> None: - """Handle the device going unavailable.""" - self.async_update_listeners(None) - - @callback - def async_add_entities_listener( - self, - entity_class: type[PassiveBluetoothProcessorEntity], - async_add_entites: AddEntitiesCallback, - ) -> Callable[[], None]: - """Add a listener for new entities.""" - created: set[PassiveBluetoothEntityKey] = set() - - @callback - def _async_add_or_update_entities( - data: PassiveBluetoothDataUpdate[_T] | None, - ) -> None: - """Listen for new entities.""" - if data is None: - return - entities: list[PassiveBluetoothProcessorEntity] = [] - for entity_key, description in data.entity_descriptions.items(): - if entity_key not in created: - entities.append(entity_class(self, entity_key, description)) - created.add(entity_key) - if entities: - async_add_entites(entities) - - return self.async_add_listener(_async_add_or_update_entities) - - @callback - def async_add_listener( - self, - update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None], - ) -> Callable[[], None]: - """Listen for all updates.""" - - @callback - def remove_listener() -> None: - """Remove update listener.""" - self._listeners.remove(update_callback) - - self._listeners.append(update_callback) - return remove_listener - - @callback - def async_add_entity_key_listener( - self, - update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None], - entity_key: PassiveBluetoothEntityKey, - ) -> Callable[[], None]: - """Listen for updates by device key.""" - - @callback - def remove_listener() -> None: - """Remove update listener.""" - self._entity_key_listeners[entity_key].remove(update_callback) - if not self._entity_key_listeners[entity_key]: - del self._entity_key_listeners[entity_key] - - self._entity_key_listeners.setdefault(entity_key, []).append(update_callback) - return remove_listener - - @callback - def async_update_listeners( - self, data: PassiveBluetoothDataUpdate[_T] | None - ) -> None: - """Update all registered listeners.""" - # Dispatch to listeners without a filter key - for update_callback in self._listeners: - update_callback(data) - - # Dispatch to listeners with a filter key - for listeners in self._entity_key_listeners.values(): - for update_callback in listeners: - update_callback(data) - - @callback - def async_handle_bluetooth_event( - self, - service_info: BluetoothServiceInfo, - change: BluetoothChange, - ) -> None: - """Handle a Bluetooth event.""" - try: - new_data = self.update_method(service_info) - except Exception as err: # pylint: disable=broad-except - self.last_update_success = False - self.coordinator.logger.exception( - "Unexpected error updating %s data: %s", self.coordinator.name, err - ) - return - - if not isinstance(new_data, PassiveBluetoothDataUpdate): - self.last_update_success = False # type: ignore[unreachable] - raise ValueError( - f"The update_method for {self.coordinator.name} returned {new_data} instead of a PassiveBluetoothDataUpdate" - ) - - if not self.last_update_success: - self.last_update_success = True - self.coordinator.logger.info( - "Processing %s data recovered", self.coordinator.name - ) - - self.devices.update(new_data.devices) - self.entity_descriptions.update(new_data.entity_descriptions) - self.entity_data.update(new_data.entity_data) - self.entity_names.update(new_data.entity_names) - self.async_update_listeners(new_data) - - -class PassiveBluetoothProcessorEntity(Entity, Generic[_PassiveBluetoothDataProcessorT]): - """A class for entities using PassiveBluetoothDataProcessor.""" - - _attr_has_entity_name = True - _attr_should_poll = False - - def __init__( - self, - processor: _PassiveBluetoothDataProcessorT, - entity_key: PassiveBluetoothEntityKey, - description: EntityDescription, - context: Any = None, - ) -> None: - """Create the entity with a PassiveBluetoothDataProcessor.""" - self.entity_description = description - self.entity_key = entity_key - self.processor = processor - self.processor_context = context - address = processor.coordinator.address - device_id = entity_key.device_id - devices = processor.devices - key = entity_key.key - if device_id in devices: - base_device_info = devices[device_id] - else: - base_device_info = DeviceInfo({}) - if device_id: - self._attr_device_info = base_device_info | DeviceInfo( - {ATTR_IDENTIFIERS: {(DOMAIN, f"{address}-{device_id}")}} - ) - self._attr_unique_id = f"{address}-{key}-{device_id}" - else: - self._attr_device_info = base_device_info | DeviceInfo( - {ATTR_IDENTIFIERS: {(DOMAIN, address)}} - ) - self._attr_unique_id = f"{address}-{key}" - if ATTR_NAME not in self._attr_device_info: - self._attr_device_info[ATTR_NAME] = self.processor.coordinator.name - self._attr_name = processor.entity_names.get(entity_key) + async def async_update(self) -> None: + """All updates are passive.""" @property def available(self) -> bool: """Return if entity is available.""" - return self.processor.available - - async def async_added_to_hass(self) -> None: - """When entity is added to hass.""" - await super().async_added_to_hass() - self.async_on_remove( - self.processor.async_add_entity_key_listener( - self._handle_processor_update, self.entity_key - ) - ) - - @callback - def _handle_processor_update( - self, new_data: PassiveBluetoothDataUpdate | None - ) -> None: - """Handle updated data from the processor.""" - self.async_write_ha_state() + return self.coordinator.available diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py new file mode 100644 index 00000000000..2e4118000bd --- /dev/null +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -0,0 +1,346 @@ +"""Passive update processors for the Bluetooth integration.""" +from __future__ import annotations + +from collections.abc import Callable, Mapping +import dataclasses +import logging +from typing import Any, Generic, TypeVar + +from home_assistant_bluetooth import BluetoothServiceInfo + +from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import BluetoothChange +from .const import DOMAIN +from .update_coordinator import BasePassiveBluetoothCoordinator + + +@dataclasses.dataclass(frozen=True) +class PassiveBluetoothEntityKey: + """Key for a passive bluetooth entity. + + Example: + key: temperature + device_id: outdoor_sensor_1 + """ + + key: str + device_id: str | None + + +_T = TypeVar("_T") + + +@dataclasses.dataclass(frozen=True) +class PassiveBluetoothDataUpdate(Generic[_T]): + """Generic bluetooth data.""" + + devices: dict[str | None, DeviceInfo] = dataclasses.field(default_factory=dict) + entity_descriptions: Mapping[ + PassiveBluetoothEntityKey, EntityDescription + ] = dataclasses.field(default_factory=dict) + entity_names: Mapping[PassiveBluetoothEntityKey, str | None] = dataclasses.field( + default_factory=dict + ) + entity_data: Mapping[PassiveBluetoothEntityKey, _T] = dataclasses.field( + default_factory=dict + ) + + +class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): + """Passive bluetooth processor coordinator for bluetooth advertisements. + + The coordinator is responsible for dispatching the bluetooth data, + to each processor, and tracking devices. + """ + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + address: str, + ) -> None: + """Initialize the coordinator.""" + super().__init__(hass, logger, address) + self._processors: list[PassiveBluetoothDataProcessor] = [] + + @callback + def async_register_processor( + self, processor: PassiveBluetoothDataProcessor + ) -> Callable[[], None]: + """Register a processor that subscribes to updates.""" + processor.coordinator = self + + @callback + def remove_processor() -> None: + """Remove a processor.""" + self._processors.remove(processor) + self._async_handle_processors_changed() + + self._processors.append(processor) + self._async_handle_processors_changed() + return remove_processor + + @callback + def _async_handle_processors_changed(self) -> None: + """Handle processors changed.""" + running = bool(self._cancel_bluetooth_advertisements) + if running and not self._processors: + self._async_stop() + elif not running and self._processors: + self._async_start() + + @callback + def _async_handle_unavailable(self, address: str) -> None: + """Handle the device going unavailable.""" + super()._async_handle_unavailable(address) + for processor in self._processors: + processor.async_handle_unavailable() + + @callback + def _async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfo, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + super()._async_handle_bluetooth_event(service_info, change) + if self.hass.is_stopping: + return + for processor in self._processors: + processor.async_handle_bluetooth_event(service_info, change) + + +_PassiveBluetoothDataProcessorT = TypeVar( + "_PassiveBluetoothDataProcessorT", + bound="PassiveBluetoothDataProcessor[Any]", +) + + +class PassiveBluetoothDataProcessor(Generic[_T]): + """Passive bluetooth data processor for bluetooth advertisements. + + The processor is responsible for keeping track of the bluetooth data + and updating subscribers. + + The update_method must return a PassiveBluetoothDataUpdate object. Callers + are responsible for formatting the data returned from their parser into + the appropriate format. + + The processor will call the update_method every time the bluetooth device + receives a new advertisement data from the coordinator with the following signature: + + update_method(service_info: BluetoothServiceInfo) -> PassiveBluetoothDataUpdate + + As the size of each advertisement is limited, the update_method should + return a PassiveBluetoothDataUpdate object that contains only data that + should be updated. The coordinator will then dispatch subscribers based + on the data in the PassiveBluetoothDataUpdate object. The accumulated data + is available in the devices, entity_data, and entity_descriptions attributes. + """ + + coordinator: PassiveBluetoothProcessorCoordinator + + def __init__( + self, + update_method: Callable[[BluetoothServiceInfo], PassiveBluetoothDataUpdate[_T]], + ) -> None: + """Initialize the coordinator.""" + self.coordinator: PassiveBluetoothProcessorCoordinator + self._listeners: list[ + Callable[[PassiveBluetoothDataUpdate[_T] | None], None] + ] = [] + self._entity_key_listeners: dict[ + PassiveBluetoothEntityKey, + list[Callable[[PassiveBluetoothDataUpdate[_T] | None], None]], + ] = {} + self.update_method = update_method + self.entity_names: dict[PassiveBluetoothEntityKey, str | None] = {} + self.entity_data: dict[PassiveBluetoothEntityKey, _T] = {} + self.entity_descriptions: dict[ + PassiveBluetoothEntityKey, EntityDescription + ] = {} + self.devices: dict[str | None, DeviceInfo] = {} + self.last_update_success = True + + @property + def available(self) -> bool: + """Return if the device is available.""" + return self.coordinator.available and self.last_update_success + + @callback + def async_handle_unavailable(self) -> None: + """Handle the device going unavailable.""" + self.async_update_listeners(None) + + @callback + def async_add_entities_listener( + self, + entity_class: type[PassiveBluetoothProcessorEntity], + async_add_entites: AddEntitiesCallback, + ) -> Callable[[], None]: + """Add a listener for new entities.""" + created: set[PassiveBluetoothEntityKey] = set() + + @callback + def _async_add_or_update_entities( + data: PassiveBluetoothDataUpdate[_T] | None, + ) -> None: + """Listen for new entities.""" + if data is None: + return + entities: list[PassiveBluetoothProcessorEntity] = [] + for entity_key, description in data.entity_descriptions.items(): + if entity_key not in created: + entities.append(entity_class(self, entity_key, description)) + created.add(entity_key) + if entities: + async_add_entites(entities) + + return self.async_add_listener(_async_add_or_update_entities) + + @callback + def async_add_listener( + self, + update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None], + ) -> Callable[[], None]: + """Listen for all updates.""" + + @callback + def remove_listener() -> None: + """Remove update listener.""" + self._listeners.remove(update_callback) + + self._listeners.append(update_callback) + return remove_listener + + @callback + def async_add_entity_key_listener( + self, + update_callback: Callable[[PassiveBluetoothDataUpdate[_T] | None], None], + entity_key: PassiveBluetoothEntityKey, + ) -> Callable[[], None]: + """Listen for updates by device key.""" + + @callback + def remove_listener() -> None: + """Remove update listener.""" + self._entity_key_listeners[entity_key].remove(update_callback) + if not self._entity_key_listeners[entity_key]: + del self._entity_key_listeners[entity_key] + + self._entity_key_listeners.setdefault(entity_key, []).append(update_callback) + return remove_listener + + @callback + def async_update_listeners( + self, data: PassiveBluetoothDataUpdate[_T] | None + ) -> None: + """Update all registered listeners.""" + # Dispatch to listeners without a filter key + for update_callback in self._listeners: + update_callback(data) + + # Dispatch to listeners with a filter key + for listeners in self._entity_key_listeners.values(): + for update_callback in listeners: + update_callback(data) + + @callback + def async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfo, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + try: + new_data = self.update_method(service_info) + except Exception as err: # pylint: disable=broad-except + self.last_update_success = False + self.coordinator.logger.exception( + "Unexpected error updating %s data: %s", self.coordinator.name, err + ) + return + + if not isinstance(new_data, PassiveBluetoothDataUpdate): + self.last_update_success = False # type: ignore[unreachable] + raise ValueError( + f"The update_method for {self.coordinator.name} returned {new_data} instead of a PassiveBluetoothDataUpdate" + ) + + if not self.last_update_success: + self.last_update_success = True + self.coordinator.logger.info( + "Processing %s data recovered", self.coordinator.name + ) + + self.devices.update(new_data.devices) + self.entity_descriptions.update(new_data.entity_descriptions) + self.entity_data.update(new_data.entity_data) + self.entity_names.update(new_data.entity_names) + self.async_update_listeners(new_data) + + +class PassiveBluetoothProcessorEntity(Entity, Generic[_PassiveBluetoothDataProcessorT]): + """A class for entities using PassiveBluetoothDataProcessor.""" + + _attr_has_entity_name = True + _attr_should_poll = False + + def __init__( + self, + processor: _PassiveBluetoothDataProcessorT, + entity_key: PassiveBluetoothEntityKey, + description: EntityDescription, + context: Any = None, + ) -> None: + """Create the entity with a PassiveBluetoothDataProcessor.""" + self.entity_description = description + self.entity_key = entity_key + self.processor = processor + self.processor_context = context + address = processor.coordinator.address + device_id = entity_key.device_id + devices = processor.devices + key = entity_key.key + if device_id in devices: + base_device_info = devices[device_id] + else: + base_device_info = DeviceInfo({}) + if device_id: + self._attr_device_info = base_device_info | DeviceInfo( + {ATTR_IDENTIFIERS: {(DOMAIN, f"{address}-{device_id}")}} + ) + self._attr_unique_id = f"{address}-{key}-{device_id}" + else: + self._attr_device_info = base_device_info | DeviceInfo( + {ATTR_IDENTIFIERS: {(DOMAIN, address)}} + ) + self._attr_unique_id = f"{address}-{key}" + if ATTR_NAME not in self._attr_device_info: + self._attr_device_info[ATTR_NAME] = self.processor.coordinator.name + self._attr_name = processor.entity_names.get(entity_key) + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.processor.available + + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self.async_on_remove( + self.processor.async_add_entity_key_listener( + self._handle_processor_update, self.entity_key + ) + ) + + @callback + def _handle_processor_update( + self, new_data: PassiveBluetoothDataUpdate | None + ) -> None: + """Handle updated data from the processor.""" + self.async_write_ha_state() diff --git a/homeassistant/components/bluetooth/update_coordinator.py b/homeassistant/components/bluetooth/update_coordinator.py new file mode 100644 index 00000000000..d45514ab9ab --- /dev/null +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -0,0 +1,84 @@ +"""Update coordinator for the Bluetooth integration.""" +from __future__ import annotations + +import logging +import time + +from home_assistant_bluetooth import BluetoothServiceInfo + +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback + +from . import ( + BluetoothCallbackMatcher, + BluetoothChange, + async_register_callback, + async_track_unavailable, +) + + +class BasePassiveBluetoothCoordinator: + """Base class for passive bluetooth coordinator for bluetooth advertisements. + + The coordinator is responsible for tracking devices. + """ + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + address: str, + ) -> None: + """Initialize the coordinator.""" + self.hass = hass + self.logger = logger + self.name: str | None = None + self.address = address + self._cancel_track_unavailable: CALLBACK_TYPE | None = None + self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None + self._present = False + self.last_seen = 0.0 + + @property + def available(self) -> bool: + """Return if the device is available.""" + return self._present + + @callback + def _async_start(self) -> None: + """Start the callbacks.""" + self._cancel_bluetooth_advertisements = async_register_callback( + self.hass, + self._async_handle_bluetooth_event, + BluetoothCallbackMatcher(address=self.address), + ) + self._cancel_track_unavailable = async_track_unavailable( + self.hass, + self._async_handle_unavailable, + self.address, + ) + + @callback + def _async_stop(self) -> None: + """Stop the callbacks.""" + if self._cancel_bluetooth_advertisements is not None: + self._cancel_bluetooth_advertisements() + self._cancel_bluetooth_advertisements = None + if self._cancel_track_unavailable is not None: + self._cancel_track_unavailable() + self._cancel_track_unavailable = None + + @callback + def _async_handle_unavailable(self, address: str) -> None: + """Handle the device going unavailable.""" + self._present = False + + @callback + def _async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfo, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + self.last_seen = time.monotonic() + self.name = service_info.name + self._present = True diff --git a/homeassistant/components/inkbird/__init__.py b/homeassistant/components/inkbird/__init__.py index 330d85b665c..574f2a23355 100644 --- a/homeassistant/components/inkbird/__init__.py +++ b/homeassistant/components/inkbird/__init__.py @@ -3,8 +3,8 @@ from __future__ import annotations import logging -from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothDataUpdateCoordinator, +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: assert address is not None hass.data.setdefault(DOMAIN, {})[ entry.entry_id - ] = PassiveBluetoothDataUpdateCoordinator( + ] = PassiveBluetoothProcessorCoordinator( hass, _LOGGER, address=address, diff --git a/homeassistant/components/inkbird/sensor.py b/homeassistant/components/inkbird/sensor.py index 600f28b6880..6b472edefe9 100644 --- a/homeassistant/components/inkbird/sensor.py +++ b/homeassistant/components/inkbird/sensor.py @@ -13,11 +13,11 @@ from inkbird_ble import ( ) from homeassistant import config_entries -from homeassistant.components.bluetooth.passive_update_coordinator import ( +from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, - PassiveBluetoothDataUpdateCoordinator, PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import ( @@ -126,7 +126,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the INKBIRD BLE sensors.""" - coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][ + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] data = INKBIRDBluetoothDeviceData() diff --git a/homeassistant/components/sensorpush/__init__.py b/homeassistant/components/sensorpush/__init__.py index 94e98e30f82..0f0d4d4fe8f 100644 --- a/homeassistant/components/sensorpush/__init__.py +++ b/homeassistant/components/sensorpush/__init__.py @@ -3,8 +3,8 @@ from __future__ import annotations import logging -from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothDataUpdateCoordinator, +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: assert address is not None hass.data.setdefault(DOMAIN, {})[ entry.entry_id - ] = PassiveBluetoothDataUpdateCoordinator( + ] = PassiveBluetoothProcessorCoordinator( hass, _LOGGER, address=address, diff --git a/homeassistant/components/sensorpush/sensor.py b/homeassistant/components/sensorpush/sensor.py index f592b594289..3921cbe43dd 100644 --- a/homeassistant/components/sensorpush/sensor.py +++ b/homeassistant/components/sensorpush/sensor.py @@ -13,11 +13,11 @@ from sensorpush_ble import ( ) from homeassistant import config_entries -from homeassistant.components.bluetooth.passive_update_coordinator import ( +from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, - PassiveBluetoothDataUpdateCoordinator, PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import ( @@ -127,7 +127,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the SensorPush BLE sensors.""" - coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][ + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] data = SensorPushBluetoothDeviceData() diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index d7f3e4071aa..a40dc8995d1 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -3,8 +3,8 @@ from __future__ import annotations import logging -from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothDataUpdateCoordinator, +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: assert address is not None hass.data.setdefault(DOMAIN, {})[ entry.entry_id - ] = PassiveBluetoothDataUpdateCoordinator( + ] = PassiveBluetoothProcessorCoordinator( hass, _LOGGER, address=address, diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index 1452b5e9053..50cbb0f66cf 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -13,11 +13,11 @@ from xiaomi_ble import ( ) from homeassistant import config_entries -from homeassistant.components.bluetooth.passive_update_coordinator import ( +from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, - PassiveBluetoothDataUpdateCoordinator, PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import ( @@ -155,7 +155,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Xiaomi BLE sensors.""" - coordinator: PassiveBluetoothDataUpdateCoordinator = hass.data[DOMAIN][ + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] data = XiaomiBluetoothDeviceData() diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 2b8c876460f..4da14fb13d3 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -1,39 +1,27 @@ -"""Tests for the Bluetooth integration.""" +"""Tests for the Bluetooth integration PassiveBluetoothDataUpdateCoordinator.""" from __future__ import annotations from datetime import timedelta import logging +from typing import Any from unittest.mock import MagicMock, patch -from home_assistant_bluetooth import BluetoothServiceInfo -import pytest - -from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, - BinarySensorEntityDescription, -) from homeassistant.components.bluetooth import ( DOMAIN, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, ) from homeassistant.components.bluetooth.passive_update_coordinator import ( - PassiveBluetoothDataProcessor, - PassiveBluetoothDataUpdate, + PassiveBluetoothCoordinatorEntity, PassiveBluetoothDataUpdateCoordinator, - PassiveBluetoothEntityKey, - PassiveBluetoothProcessorEntity, ) -from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription -from homeassistant.const import TEMP_CELSIUS -from homeassistant.core import CoreState, callback -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util from . import _get_underlying_scanner -from tests.common import MockEntityPlatform, async_fire_time_changed +from tests.common import async_fire_time_changed _LOGGER = logging.getLogger(__name__) @@ -49,49 +37,30 @@ GENERIC_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( service_uuids=[], source="local", ) -GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( - devices={ - None: DeviceInfo( - name="Test Device", model="Test Model", manufacturer="Test Manufacturer" - ), - }, - entity_data={ - PassiveBluetoothEntityKey("temperature", None): 14.5, - PassiveBluetoothEntityKey("pressure", None): 1234, - }, - entity_names={ - PassiveBluetoothEntityKey("temperature", None): "Temperature", - PassiveBluetoothEntityKey("pressure", None): "Pressure", - }, - entity_descriptions={ - PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( - key="temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( - key="pressure", - native_unit_of_measurement="hPa", - device_class=SensorDeviceClass.PRESSURE, - ), - }, -) + + +class MyCoordinator(PassiveBluetoothDataUpdateCoordinator): + """An example coordinator that subclasses PassiveBluetoothDataUpdateCoordinator.""" + + def __init__(self, hass, logger, device_id) -> None: + """Initialize the coordinator.""" + super().__init__(hass, logger, device_id) + self.data: dict[str, Any] = {} + + def _async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfo, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + self.data = {"rssi": service_info.rssi} + super()._async_handle_bluetooth_event(service_info, change) async def test_basic_usage(hass, mock_bleak_scanner_start): """Test basic usage of the PassiveBluetoothDataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - - @callback - def _async_generate_mock_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Generate mock data.""" - return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE - - coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" - ) + coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") assert coordinator.available is False # no data yet saved_callback = None @@ -100,98 +69,87 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): saved_callback = _callback return lambda: None - processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + mock_listener = MagicMock() + unregister_listener = coordinator.async_add_listener(mock_listener) with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", _async_register_callback, ): - unregister_processor = coordinator.async_register_processor(processor) + cancel = coordinator.async_start() - entity_key = PassiveBluetoothEntityKey("temperature", None) - entity_key_events = [] - all_events = [] - mock_entity = MagicMock() - mock_add_entities = MagicMock() - - def _async_entity_key_listener(data: PassiveBluetoothDataUpdate | None) -> None: - """Mock entity key listener.""" - entity_key_events.append(data) - - cancel_async_add_entity_key_listener = processor.async_add_entity_key_listener( - _async_entity_key_listener, - entity_key, - ) - - def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: - """Mock an all listener.""" - all_events.append(data) - - cancel_listener = processor.async_add_listener( - _all_listener, - ) - - cancel_async_add_entities_listener = processor.async_add_entities_listener( - mock_entity, - mock_add_entities, - ) + assert saved_callback is not None saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - # Each listener should receive the same data - # since both match - assert len(entity_key_events) == 1 - assert len(all_events) == 1 - - # There should be 4 calls to create entities - assert len(mock_entity.mock_calls) == 2 - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - - # Each listener should receive the same data - # since both match - assert len(entity_key_events) == 2 - assert len(all_events) == 2 - - # On the second, the entities should already be created - # so the mock should not be called again - assert len(mock_entity.mock_calls) == 2 - - cancel_async_add_entity_key_listener() - cancel_listener() - cancel_async_add_entities_listener() - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - - # Each listener should not trigger any more now - # that they were cancelled - assert len(entity_key_events) == 2 - assert len(all_events) == 2 - assert len(mock_entity.mock_calls) == 2 + assert len(mock_listener.mock_calls) == 1 + assert coordinator.data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO.rssi} assert coordinator.available is True - unregister_processor() + unregister_listener() + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + assert len(mock_listener.mock_calls) == 1 + assert coordinator.data == {"rssi": GENERIC_BLUETOOTH_SERVICE_INFO.rssi} + assert coordinator.available is True + cancel() -async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): - """Test that the coordinator is unavailable after no data for a while.""" +async def test_context_compatiblity_with_data_update_coordinator( + hass, mock_bleak_scanner_start +): + """Test contexts can be passed for compatibility with DataUpdateCoordinator.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + mock_listener = MagicMock() + coordinator.async_add_listener(mock_listener) + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_start() + + assert not set(coordinator.async_contexts()) + + def update_callback1(): + pass + + def update_callback2(): + pass + + unsub1 = coordinator.async_add_listener(update_callback1, 1) + assert set(coordinator.async_contexts()) == {1} + + unsub2 = coordinator.async_add_listener(update_callback2, 2) + assert set(coordinator.async_contexts()) == {1, 2} + + unsub1() + assert set(coordinator.async_contexts()) == {2} + + unsub2() + assert not set(coordinator.async_contexts()) + + +async def test_unavailable_callbacks_mark_the_coordinator_unavailable( + hass, mock_bleak_scanner_start +): + """Test that the coordinator goes unavailable when the bluetooth stack no longer sees the device.""" with patch( "bleak.BleakScanner.discovered_devices", # Must patch before we setup [MagicMock(address="44:44:33:11:23:45")], ): await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - - @callback - def _async_generate_mock_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Generate mock data.""" - return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE - - coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" - ) + coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") assert coordinator.available is False # no data yet saved_callback = None @@ -200,27 +158,19 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): saved_callback = _callback return lambda: None - processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + mock_listener = MagicMock() + coordinator.async_add_listener(mock_listener) + with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", _async_register_callback, ): - unregister_processor = coordinator.async_register_processor(processor) - - mock_entity = MagicMock() - mock_add_entities = MagicMock() - processor.async_add_entities_listener( - mock_entity, - mock_add_entities, - ) + coordinator.async_start() assert coordinator.available is False - assert processor.available is False - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True - assert processor.available is True + scanner = _get_underlying_scanner() with patch( @@ -236,12 +186,9 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): ) await hass.async_block_till_done() assert coordinator.available is False - assert processor.available is False saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True - assert processor.available is True with patch( "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", @@ -256,474 +203,15 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): ) await hass.async_block_till_done() assert coordinator.available is False - assert processor.available is False - - unregister_processor() -async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): - """Test updates are ignored once hass is stopping.""" - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - - @callback - def _async_generate_mock_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Generate mock data.""" - return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE - - coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" - ) - assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) - - with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", - _async_register_callback, - ): - unregister_processor = coordinator.async_register_processor(processor) - - all_events = [] - - def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: - """Mock an all listener.""" - all_events.append(data) - - processor.async_add_listener( - _all_listener, - ) - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert len(all_events) == 1 - - hass.state = CoreState.stopping - - # We should stop processing events once hass is stopping - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert len(all_events) == 1 - unregister_processor() - - -async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_start): - """Test we handle exceptions from the update method.""" - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - - run_count = 0 - - @callback - def _async_generate_mock_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Generate mock data.""" - nonlocal run_count - run_count += 1 - if run_count == 2: - raise Exception("Test exception") - return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE - - coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" - ) - assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) - with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", - _async_register_callback, - ): - unregister_processor = coordinator.async_register_processor(processor) - - processor.async_add_listener(MagicMock()) - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert processor.available is True - - # We should go unavailable once we get an exception - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert "Test exception" in caplog.text - assert processor.available is False - - # We should go available again once we get data again - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert processor.available is True - unregister_processor() - - -async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): - """Test we handle bad data from the update method.""" - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - - run_count = 0 - - @callback - def _async_generate_mock_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Generate mock data.""" - nonlocal run_count - run_count += 1 - if run_count == 2: - return "bad_data" - return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE - - coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" - ) - assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) - with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", - _async_register_callback, - ): - unregister_processor = coordinator.async_register_processor(processor) - - processor.async_add_listener(MagicMock()) - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert processor.available is True - - # We should go unavailable once we get bad data - with pytest.raises(ValueError): - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - - assert processor.available is False - - # We should go available again once we get good data again - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - assert processor.available is True - unregister_processor() - - -GOVEE_B5178_REMOTE_SERVICE_INFO = BluetoothServiceInfo( - name="B5178D6FB", - address="749A17CB-F7A9-D466-C29F-AABE601938A0", - rssi=-95, - manufacturer_data={ - 1: b"\x01\x01\x01\x04\xb5\xa2d\x00\x06L\x00\x02\x15INTELLI_ROCKS_HWPu\xf2\xff\xc2" - }, - service_data={}, - service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], - source="local", -) -GOVEE_B5178_PRIMARY_SERVICE_INFO = BluetoothServiceInfo( - name="B5178D6FB", - address="749A17CB-F7A9-D466-C29F-AABE601938A0", - rssi=-92, - manufacturer_data={ - 1: b"\x01\x01\x00\x03\x07Xd\x00\x00L\x00\x02\x15INTELLI_ROCKS_HWPu\xf2\xff\xc2" - }, - service_data={}, - service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], - source="local", -) - -GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( - devices={ - "remote": { - "name": "B5178D6FB Remote", - "manufacturer": "Govee", - "model": "H5178-REMOTE", - }, - }, - entity_descriptions={ - PassiveBluetoothEntityKey( - key="temperature", device_id="remote" - ): SensorEntityDescription( - key="temperature_remote", - device_class=SensorDeviceClass.TEMPERATURE, - entity_category=None, - entity_registry_enabled_default=True, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="°C", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="humidity", device_id="remote" - ): SensorEntityDescription( - key="humidity_remote", - device_class=SensorDeviceClass.HUMIDITY, - entity_category=None, - entity_registry_enabled_default=True, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="%", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="battery", device_id="remote" - ): SensorEntityDescription( - key="battery_remote", - device_class=SensorDeviceClass.BATTERY, - entity_category=None, - entity_registry_enabled_default=True, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="%", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="signal_strength", device_id="remote" - ): SensorEntityDescription( - key="signal_strength_remote", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_category=None, - entity_registry_enabled_default=False, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="dBm", - state_class=None, - ), - }, - entity_names={ - PassiveBluetoothEntityKey(key="temperature", device_id="remote"): "Temperature", - PassiveBluetoothEntityKey(key="humidity", device_id="remote"): "Humidity", - PassiveBluetoothEntityKey(key="battery", device_id="remote"): "Battery", - PassiveBluetoothEntityKey( - key="signal_strength", device_id="remote" - ): "Signal Strength", - }, - entity_data={ - PassiveBluetoothEntityKey(key="temperature", device_id="remote"): 30.8642, - PassiveBluetoothEntityKey(key="humidity", device_id="remote"): 64.2, - PassiveBluetoothEntityKey(key="battery", device_id="remote"): 100, - PassiveBluetoothEntityKey(key="signal_strength", device_id="remote"): -95, - }, -) -GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( - PassiveBluetoothDataUpdate( - devices={ - "remote": { - "name": "B5178D6FB Remote", - "manufacturer": "Govee", - "model": "H5178-REMOTE", - }, - "primary": { - "name": "B5178D6FB Primary", - "manufacturer": "Govee", - "model": "H5178", - }, - }, - entity_descriptions={ - PassiveBluetoothEntityKey( - key="temperature", device_id="remote" - ): SensorEntityDescription( - key="temperature_remote", - device_class=SensorDeviceClass.TEMPERATURE, - entity_category=None, - entity_registry_enabled_default=True, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="°C", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="humidity", device_id="remote" - ): SensorEntityDescription( - key="humidity_remote", - device_class=SensorDeviceClass.HUMIDITY, - entity_category=None, - entity_registry_enabled_default=True, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="%", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="battery", device_id="remote" - ): SensorEntityDescription( - key="battery_remote", - device_class=SensorDeviceClass.BATTERY, - entity_category=None, - entity_registry_enabled_default=True, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="%", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="signal_strength", device_id="remote" - ): SensorEntityDescription( - key="signal_strength_remote", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_category=None, - entity_registry_enabled_default=False, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="dBm", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="temperature", device_id="primary" - ): SensorEntityDescription( - key="temperature_primary", - device_class=SensorDeviceClass.TEMPERATURE, - entity_category=None, - entity_registry_enabled_default=True, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="°C", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="humidity", device_id="primary" - ): SensorEntityDescription( - key="humidity_primary", - device_class=SensorDeviceClass.HUMIDITY, - entity_category=None, - entity_registry_enabled_default=True, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="%", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="battery", device_id="primary" - ): SensorEntityDescription( - key="battery_primary", - device_class=SensorDeviceClass.BATTERY, - entity_category=None, - entity_registry_enabled_default=True, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="%", - state_class=None, - ), - PassiveBluetoothEntityKey( - key="signal_strength", device_id="primary" - ): SensorEntityDescription( - key="signal_strength_primary", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - entity_category=None, - entity_registry_enabled_default=False, - entity_registry_visible_default=True, - force_update=False, - icon=None, - has_entity_name=False, - unit_of_measurement=None, - last_reset=None, - native_unit_of_measurement="dBm", - state_class=None, - ), - }, - entity_names={ - PassiveBluetoothEntityKey( - key="temperature", device_id="remote" - ): "Temperature", - PassiveBluetoothEntityKey(key="humidity", device_id="remote"): "Humidity", - PassiveBluetoothEntityKey(key="battery", device_id="remote"): "Battery", - PassiveBluetoothEntityKey( - key="signal_strength", device_id="remote" - ): "Signal Strength", - PassiveBluetoothEntityKey( - key="temperature", device_id="primary" - ): "Temperature", - PassiveBluetoothEntityKey(key="humidity", device_id="primary"): "Humidity", - PassiveBluetoothEntityKey(key="battery", device_id="primary"): "Battery", - PassiveBluetoothEntityKey( - key="signal_strength", device_id="primary" - ): "Signal Strength", - }, - entity_data={ - PassiveBluetoothEntityKey(key="temperature", device_id="remote"): 30.8642, - PassiveBluetoothEntityKey(key="humidity", device_id="remote"): 64.2, - PassiveBluetoothEntityKey(key="battery", device_id="remote"): 100, - PassiveBluetoothEntityKey(key="signal_strength", device_id="remote"): -92, - PassiveBluetoothEntityKey(key="temperature", device_id="primary"): 19.8488, - PassiveBluetoothEntityKey(key="humidity", device_id="primary"): 48.8, - PassiveBluetoothEntityKey(key="battery", device_id="primary"): 100, - PassiveBluetoothEntityKey(key="signal_strength", device_id="primary"): -92, - }, - ) -) - - -async def test_integration_with_entity(hass, mock_bleak_scanner_start): +async def test_passive_bluetooth_coordinator_entity(hass, mock_bleak_scanner_start): """Test integration of PassiveBluetoothDataUpdateCoordinator with PassiveBluetoothCoordinatorEntity.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + entity = PassiveBluetoothCoordinatorEntity(coordinator) + assert entity.available is False - update_count = 0 - - @callback - def _async_generate_mock_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Generate mock data.""" - nonlocal update_count - update_count += 1 - if update_count > 2: - return GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE - return GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE - - coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" - ) - assert coordinator.available is False # no data yet saved_callback = None def _async_register_callback(_hass, _callback, _matcher): @@ -731,328 +219,15 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): saved_callback = _callback return lambda: None - processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", _async_register_callback, ): - coordinator.async_register_processor(processor) - - processor.async_add_listener(MagicMock()) - - mock_add_entities = MagicMock() - - processor.async_add_entities_listener( - PassiveBluetoothProcessorEntity, - mock_add_entities, - ) + coordinator.async_start() + assert coordinator.available is False saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - # First call with just the remote sensor entities results in them being added - assert len(mock_add_entities.mock_calls) == 1 - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - # Second call with just the remote sensor entities does not add them again - assert len(mock_add_entities.mock_calls) == 1 - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - # Third call with primary and remote sensor entities adds the primary sensor entities - assert len(mock_add_entities.mock_calls) == 2 - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - # Forth call with both primary and remote sensor entities does not add them again - assert len(mock_add_entities.mock_calls) == 2 - - entities = [ - *mock_add_entities.mock_calls[0][1][0], - *mock_add_entities.mock_calls[1][1][0], - ] - - entity_one: PassiveBluetoothProcessorEntity = entities[0] - entity_one.hass = hass - assert entity_one.available is True - assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature-remote" - assert entity_one.device_info == { - "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff-remote")}, - "manufacturer": "Govee", - "model": "H5178-REMOTE", - "name": "B5178D6FB Remote", - } - assert entity_one.entity_key == PassiveBluetoothEntityKey( - key="temperature", device_id="remote" - ) - - -NO_DEVICES_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( - name="Generic", - address="aa:bb:cc:dd:ee:ff", - rssi=-95, - manufacturer_data={ - 1: b"\x01\x01\x01\x01\x01\x01\x01\x01", - }, - service_data={}, - service_uuids=[], - source="local", -) -NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( - devices={}, - entity_data={ - PassiveBluetoothEntityKey("temperature", None): 14.5, - PassiveBluetoothEntityKey("pressure", None): 1234, - }, - entity_descriptions={ - PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( - key="temperature", - name="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( - key="pressure", - name="Pressure", - native_unit_of_measurement="hPa", - device_class=SensorDeviceClass.PRESSURE, - ), - }, -) - - -async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner_start): - """Test integration with PassiveBluetoothCoordinatorEntity with no device.""" - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - - @callback - def _async_generate_mock_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Generate mock data.""" - return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE - - coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" - ) - assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) - with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", - _async_register_callback, - ): - coordinator.async_register_processor(processor) - - mock_add_entities = MagicMock() - - processor.async_add_entities_listener( - PassiveBluetoothProcessorEntity, - mock_add_entities, - ) - - saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - # First call with just the remote sensor entities results in them being added - assert len(mock_add_entities.mock_calls) == 1 - - saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - # Second call with just the remote sensor entities does not add them again - assert len(mock_add_entities.mock_calls) == 1 - - entities = mock_add_entities.mock_calls[0][1][0] - entity_one: PassiveBluetoothProcessorEntity = entities[0] - entity_one.hass = hass - assert entity_one.available is True - assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature" - assert entity_one.device_info == { - "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, - "name": "Generic", - } - assert entity_one.entity_key == PassiveBluetoothEntityKey( - key="temperature", device_id=None - ) - - -async def test_passive_bluetooth_entity_with_entity_platform( - hass, mock_bleak_scanner_start -): - """Test with a mock entity platform.""" - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - - entity_platform = MockEntityPlatform(hass) - - @callback - def _async_generate_mock_data( - service_info: BluetoothServiceInfo, - ) -> PassiveBluetoothDataUpdate: - """Generate mock data.""" - return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE - - coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" - ) - assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) - with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", - _async_register_callback, - ): - coordinator.async_register_processor(processor) - - processor.async_add_entities_listener( - PassiveBluetoothProcessorEntity, - lambda entities: hass.async_create_task( - entity_platform.async_add_entities(entities) - ), - ) - saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - await hass.async_block_till_done() - saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - await hass.async_block_till_done() - assert ( - hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_temperature") - is not None - ) - assert ( - hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_pressure") - is not None - ) - - -SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( - devices={ - None: DeviceInfo( - name="Test Device", model="Test Model", manufacturer="Test Manufacturer" - ), - }, - entity_data={ - PassiveBluetoothEntityKey("pressure", None): 1234, - }, - entity_names={ - PassiveBluetoothEntityKey("pressure", None): "Pressure", - }, - entity_descriptions={ - PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( - key="pressure", - native_unit_of_measurement="hPa", - device_class=SensorDeviceClass.PRESSURE, - ), - }, -) - - -BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( - devices={ - None: DeviceInfo( - name="Test Device", model="Test Model", manufacturer="Test Manufacturer" - ), - }, - entity_data={ - PassiveBluetoothEntityKey("motion", None): True, - }, - entity_names={ - PassiveBluetoothEntityKey("motion", None): "Motion", - }, - entity_descriptions={ - PassiveBluetoothEntityKey("motion", None): BinarySensorEntityDescription( - key="motion", - device_class=BinarySensorDeviceClass.MOTION, - ), - }, -) - - -async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_start): - """Test integration of PassiveBluetoothDataUpdateCoordinator with multiple platforms.""" - await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - - coordinator = PassiveBluetoothDataUpdateCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" - ) - assert coordinator.available is False # no data yet - saved_callback = None - - def _async_register_callback(_hass, _callback, _matcher): - nonlocal saved_callback - saved_callback = _callback - return lambda: None - - binary_sensor_processor = PassiveBluetoothDataProcessor( - lambda service_info: BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE - ) - sesnor_processor = PassiveBluetoothDataProcessor( - lambda service_info: SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE - ) - - with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", - _async_register_callback, - ): - coordinator.async_register_processor(binary_sensor_processor) - coordinator.async_register_processor(sesnor_processor) - - binary_sensor_processor.async_add_listener(MagicMock()) - sesnor_processor.async_add_listener(MagicMock()) - - mock_add_sensor_entities = MagicMock() - mock_add_binary_sensor_entities = MagicMock() - - sesnor_processor.async_add_entities_listener( - PassiveBluetoothProcessorEntity, - mock_add_sensor_entities, - ) - binary_sensor_processor.async_add_entities_listener( - PassiveBluetoothProcessorEntity, - mock_add_binary_sensor_entities, - ) - - saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) - # First call with just the remote sensor entities results in them being added - assert len(mock_add_binary_sensor_entities.mock_calls) == 1 - assert len(mock_add_sensor_entities.mock_calls) == 1 - - binary_sesnor_entities = [ - *mock_add_binary_sensor_entities.mock_calls[0][1][0], - ] - sesnor_entities = [ - *mock_add_sensor_entities.mock_calls[0][1][0], - ] - - sensor_entity_one: PassiveBluetoothProcessorEntity = sesnor_entities[0] - sensor_entity_one.hass = hass - assert sensor_entity_one.available is True - assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure" - assert sensor_entity_one.device_info == { - "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, - "manufacturer": "Test Manufacturer", - "model": "Test Model", - "name": "Test Device", - } - assert sensor_entity_one.entity_key == PassiveBluetoothEntityKey( - key="pressure", device_id=None - ) - - binary_sensor_entity_one: PassiveBluetoothProcessorEntity = binary_sesnor_entities[ - 0 - ] - binary_sensor_entity_one.hass = hass - assert binary_sensor_entity_one.available is True - assert binary_sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-motion" - assert binary_sensor_entity_one.device_info == { - "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, - "manufacturer": "Test Manufacturer", - "model": "Test Model", - "name": "Test Device", - } - assert binary_sensor_entity_one.entity_key == PassiveBluetoothEntityKey( - key="motion", device_id=None - ) + assert coordinator.available is True + entity.hass = hass + await entity.async_update() + assert entity.available is True diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py new file mode 100644 index 00000000000..e5f992eebb2 --- /dev/null +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -0,0 +1,1058 @@ +"""Tests for the Bluetooth integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from unittest.mock import MagicMock, patch + +from home_assistant_bluetooth import BluetoothServiceInfo +import pytest + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntityDescription, +) +from homeassistant.components.bluetooth import ( + DOMAIN, + UNAVAILABLE_TRACK_SECONDS, + BluetoothChange, +) +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription +from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import CoreState, callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import _get_underlying_scanner + +from tests.common import MockEntityPlatform, async_fire_time_changed + +_LOGGER = logging.getLogger(__name__) + + +GENERIC_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( + name="Generic", + address="aa:bb:cc:dd:ee:ff", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x01\x01\x01\x01\x01", + }, + service_data={}, + service_uuids=[], + source="local", +) +GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + None: DeviceInfo( + name="Test Device", model="Test Model", manufacturer="Test Manufacturer" + ), + }, + entity_data={ + PassiveBluetoothEntityKey("temperature", None): 14.5, + PassiveBluetoothEntityKey("pressure", None): 1234, + }, + entity_names={ + PassiveBluetoothEntityKey("temperature", None): "Temperature", + PassiveBluetoothEntityKey("pressure", None): "Pressure", + }, + entity_descriptions={ + PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( + key="temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ), + PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( + key="pressure", + native_unit_of_measurement="hPa", + device_class=SensorDeviceClass.PRESSURE, + ), + }, +) + + +async def test_basic_usage(hass, mock_bleak_scanner_start): + """Test basic usage of the PassiveBluetoothProcessorCoordinator.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + unregister_processor = coordinator.async_register_processor(processor) + + entity_key = PassiveBluetoothEntityKey("temperature", None) + entity_key_events = [] + all_events = [] + mock_entity = MagicMock() + mock_add_entities = MagicMock() + + def _async_entity_key_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock entity key listener.""" + entity_key_events.append(data) + + cancel_async_add_entity_key_listener = processor.async_add_entity_key_listener( + _async_entity_key_listener, + entity_key, + ) + + def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock an all listener.""" + all_events.append(data) + + cancel_listener = processor.async_add_listener( + _all_listener, + ) + + cancel_async_add_entities_listener = processor.async_add_entities_listener( + mock_entity, + mock_add_entities, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + # Each listener should receive the same data + # since both match + assert len(entity_key_events) == 1 + assert len(all_events) == 1 + + # There should be 4 calls to create entities + assert len(mock_entity.mock_calls) == 2 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + # Each listener should receive the same data + # since both match + assert len(entity_key_events) == 2 + assert len(all_events) == 2 + + # On the second, the entities should already be created + # so the mock should not be called again + assert len(mock_entity.mock_calls) == 2 + + cancel_async_add_entity_key_listener() + cancel_listener() + cancel_async_add_entities_listener() + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + # Each listener should not trigger any more now + # that they were cancelled + assert len(entity_key_events) == 2 + assert len(all_events) == 2 + assert len(mock_entity.mock_calls) == 2 + assert coordinator.available is True + + unregister_processor() + + +async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): + """Test that the coordinator is unavailable after no data for a while.""" + with patch( + "bleak.BleakScanner.discovered_devices", # Must patch before we setup + [MagicMock(address="44:44:33:11:23:45")], + ): + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + unregister_processor = coordinator.async_register_processor(processor) + + mock_entity = MagicMock() + mock_add_entities = MagicMock() + processor.async_add_entities_listener( + mock_entity, + mock_add_entities, + ) + + assert coordinator.available is False + assert processor.available is False + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(mock_add_entities.mock_calls) == 1 + assert coordinator.available is True + assert processor.available is True + scanner = _get_underlying_scanner() + + with patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", + [MagicMock(address="44:44:33:11:23:45")], + ), patch.object( + scanner, + "history", + {"aa:bb:cc:dd:ee:ff": MagicMock()}, + ): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) + await hass.async_block_till_done() + assert coordinator.available is False + assert processor.available is False + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(mock_add_entities.mock_calls) == 1 + assert coordinator.available is True + assert processor.available is True + + with patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", + [MagicMock(address="44:44:33:11:23:45")], + ), patch.object( + scanner, + "history", + {"aa:bb:cc:dd:ee:ff": MagicMock()}, + ): + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) + await hass.async_block_till_done() + assert coordinator.available is False + assert processor.available is False + + unregister_processor() + + +async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): + """Test updates are ignored once hass is stopping.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + unregister_processor = coordinator.async_register_processor(processor) + + all_events = [] + + def _all_listener(data: PassiveBluetoothDataUpdate | None) -> None: + """Mock an all listener.""" + all_events.append(data) + + processor.async_add_listener( + _all_listener, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(all_events) == 1 + + hass.state = CoreState.stopping + + # We should stop processing events once hass is stopping + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert len(all_events) == 1 + unregister_processor() + + +async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_start): + """Test we handle exceptions from the update method.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + run_count = 0 + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + nonlocal run_count + run_count += 1 + if run_count == 2: + raise Exception("Test exception") + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + unregister_processor = coordinator.async_register_processor(processor) + + processor.async_add_listener(MagicMock()) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert processor.available is True + + # We should go unavailable once we get an exception + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert "Test exception" in caplog.text + assert processor.available is False + + # We should go available again once we get data again + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert processor.available is True + unregister_processor() + + +async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): + """Test we handle bad data from the update method.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + run_count = 0 + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + nonlocal run_count + run_count += 1 + if run_count == 2: + return "bad_data" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + unregister_processor = coordinator.async_register_processor(processor) + + processor.async_add_listener(MagicMock()) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert processor.available is True + + # We should go unavailable once we get bad data + with pytest.raises(ValueError): + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + assert processor.available is False + + # We should go available again once we get good data again + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert processor.available is True + unregister_processor() + + +GOVEE_B5178_REMOTE_SERVICE_INFO = BluetoothServiceInfo( + name="B5178D6FB", + address="749A17CB-F7A9-D466-C29F-AABE601938A0", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x04\xb5\xa2d\x00\x06L\x00\x02\x15INTELLI_ROCKS_HWPu\xf2\xff\xc2" + }, + service_data={}, + service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], + source="local", +) +GOVEE_B5178_PRIMARY_SERVICE_INFO = BluetoothServiceInfo( + name="B5178D6FB", + address="749A17CB-F7A9-D466-C29F-AABE601938A0", + rssi=-92, + manufacturer_data={ + 1: b"\x01\x01\x00\x03\x07Xd\x00\x00L\x00\x02\x15INTELLI_ROCKS_HWPu\xf2\xff\xc2" + }, + service_data={}, + service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], + source="local", +) + +GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + "remote": { + "name": "B5178D6FB Remote", + "manufacturer": "Govee", + "model": "H5178-REMOTE", + }, + }, + entity_descriptions={ + PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ): SensorEntityDescription( + key="temperature_remote", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="°C", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="humidity", device_id="remote" + ): SensorEntityDescription( + key="humidity_remote", + device_class=SensorDeviceClass.HUMIDITY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="battery", device_id="remote" + ): SensorEntityDescription( + key="battery_remote", + device_class=SensorDeviceClass.BATTERY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): SensorEntityDescription( + key="signal_strength_remote", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=None, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="dBm", + state_class=None, + ), + }, + entity_names={ + PassiveBluetoothEntityKey(key="temperature", device_id="remote"): "Temperature", + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): "Humidity", + PassiveBluetoothEntityKey(key="battery", device_id="remote"): "Battery", + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): "Signal Strength", + }, + entity_data={ + PassiveBluetoothEntityKey(key="temperature", device_id="remote"): 30.8642, + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): 64.2, + PassiveBluetoothEntityKey(key="battery", device_id="remote"): 100, + PassiveBluetoothEntityKey(key="signal_strength", device_id="remote"): -95, + }, +) +GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE = ( + PassiveBluetoothDataUpdate( + devices={ + "remote": { + "name": "B5178D6FB Remote", + "manufacturer": "Govee", + "model": "H5178-REMOTE", + }, + "primary": { + "name": "B5178D6FB Primary", + "manufacturer": "Govee", + "model": "H5178", + }, + }, + entity_descriptions={ + PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ): SensorEntityDescription( + key="temperature_remote", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="°C", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="humidity", device_id="remote" + ): SensorEntityDescription( + key="humidity_remote", + device_class=SensorDeviceClass.HUMIDITY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="battery", device_id="remote" + ): SensorEntityDescription( + key="battery_remote", + device_class=SensorDeviceClass.BATTERY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): SensorEntityDescription( + key="signal_strength_remote", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=None, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="dBm", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="temperature", device_id="primary" + ): SensorEntityDescription( + key="temperature_primary", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="°C", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="humidity", device_id="primary" + ): SensorEntityDescription( + key="humidity_primary", + device_class=SensorDeviceClass.HUMIDITY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="battery", device_id="primary" + ): SensorEntityDescription( + key="battery_primary", + device_class=SensorDeviceClass.BATTERY, + entity_category=None, + entity_registry_enabled_default=True, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="%", + state_class=None, + ), + PassiveBluetoothEntityKey( + key="signal_strength", device_id="primary" + ): SensorEntityDescription( + key="signal_strength_primary", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=None, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + force_update=False, + icon=None, + has_entity_name=False, + unit_of_measurement=None, + last_reset=None, + native_unit_of_measurement="dBm", + state_class=None, + ), + }, + entity_names={ + PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ): "Temperature", + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): "Humidity", + PassiveBluetoothEntityKey(key="battery", device_id="remote"): "Battery", + PassiveBluetoothEntityKey( + key="signal_strength", device_id="remote" + ): "Signal Strength", + PassiveBluetoothEntityKey( + key="temperature", device_id="primary" + ): "Temperature", + PassiveBluetoothEntityKey(key="humidity", device_id="primary"): "Humidity", + PassiveBluetoothEntityKey(key="battery", device_id="primary"): "Battery", + PassiveBluetoothEntityKey( + key="signal_strength", device_id="primary" + ): "Signal Strength", + }, + entity_data={ + PassiveBluetoothEntityKey(key="temperature", device_id="remote"): 30.8642, + PassiveBluetoothEntityKey(key="humidity", device_id="remote"): 64.2, + PassiveBluetoothEntityKey(key="battery", device_id="remote"): 100, + PassiveBluetoothEntityKey(key="signal_strength", device_id="remote"): -92, + PassiveBluetoothEntityKey(key="temperature", device_id="primary"): 19.8488, + PassiveBluetoothEntityKey(key="humidity", device_id="primary"): 48.8, + PassiveBluetoothEntityKey(key="battery", device_id="primary"): 100, + PassiveBluetoothEntityKey(key="signal_strength", device_id="primary"): -92, + }, + ) +) + + +async def test_integration_with_entity(hass, mock_bleak_scanner_start): + """Test integration of PassiveBluetoothProcessorCoordinator with PassiveBluetoothCoordinatorEntity.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + update_count = 0 + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + nonlocal update_count + update_count += 1 + if update_count > 2: + return GOVEE_B5178_PRIMARY_AND_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE + return GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_register_processor(processor) + + processor.async_add_listener(MagicMock()) + + mock_add_entities = MagicMock() + + processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_entities, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # First call with just the remote sensor entities results in them being added + assert len(mock_add_entities.mock_calls) == 1 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Second call with just the remote sensor entities does not add them again + assert len(mock_add_entities.mock_calls) == 1 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Third call with primary and remote sensor entities adds the primary sensor entities + assert len(mock_add_entities.mock_calls) == 2 + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Forth call with both primary and remote sensor entities does not add them again + assert len(mock_add_entities.mock_calls) == 2 + + entities = [ + *mock_add_entities.mock_calls[0][1][0], + *mock_add_entities.mock_calls[1][1][0], + ] + + entity_one: PassiveBluetoothProcessorEntity = entities[0] + entity_one.hass = hass + assert entity_one.available is True + assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature-remote" + assert entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff-remote")}, + "manufacturer": "Govee", + "model": "H5178-REMOTE", + "name": "B5178D6FB Remote", + } + assert entity_one.entity_key == PassiveBluetoothEntityKey( + key="temperature", device_id="remote" + ) + + +NO_DEVICES_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( + name="Generic", + address="aa:bb:cc:dd:ee:ff", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x01\x01\x01\x01\x01", + }, + service_data={}, + service_uuids=[], + source="local", +) +NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={}, + entity_data={ + PassiveBluetoothEntityKey("temperature", None): 14.5, + PassiveBluetoothEntityKey("pressure", None): 1234, + }, + entity_descriptions={ + PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( + key="temperature", + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ), + PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( + key="pressure", + name="Pressure", + native_unit_of_measurement="hPa", + device_class=SensorDeviceClass.PRESSURE, + ), + }, +) + + +async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner_start): + """Test integration with PassiveBluetoothCoordinatorEntity with no device.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_register_processor(processor) + + mock_add_entities = MagicMock() + + processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_entities, + ) + + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # First call with just the remote sensor entities results in them being added + assert len(mock_add_entities.mock_calls) == 1 + + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Second call with just the remote sensor entities does not add them again + assert len(mock_add_entities.mock_calls) == 1 + + entities = mock_add_entities.mock_calls[0][1][0] + entity_one: PassiveBluetoothProcessorEntity = entities[0] + entity_one.hass = hass + assert entity_one.available is True + assert entity_one.unique_id == "aa:bb:cc:dd:ee:ff-temperature" + assert entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "name": "Generic", + } + assert entity_one.entity_key == PassiveBluetoothEntityKey( + key="temperature", device_id=None + ) + + +async def test_passive_bluetooth_entity_with_entity_platform( + hass, mock_bleak_scanner_start +): + """Test with a mock entity platform.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + entity_platform = MockEntityPlatform(hass) + + @callback + def _async_generate_mock_data( + service_info: BluetoothServiceInfo, + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_register_processor(processor) + + processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + lambda entities: hass.async_create_task( + entity_platform.async_add_entities(entities) + ), + ) + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + saved_callback(NO_DEVICES_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert ( + hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_temperature") + is not None + ) + assert ( + hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_pressure") + is not None + ) + + +SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + None: DeviceInfo( + name="Test Device", model="Test Model", manufacturer="Test Manufacturer" + ), + }, + entity_data={ + PassiveBluetoothEntityKey("pressure", None): 1234, + }, + entity_names={ + PassiveBluetoothEntityKey("pressure", None): "Pressure", + }, + entity_descriptions={ + PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( + key="pressure", + native_unit_of_measurement="hPa", + device_class=SensorDeviceClass.PRESSURE, + ), + }, +) + + +BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( + devices={ + None: DeviceInfo( + name="Test Device", model="Test Model", manufacturer="Test Manufacturer" + ), + }, + entity_data={ + PassiveBluetoothEntityKey("motion", None): True, + }, + entity_names={ + PassiveBluetoothEntityKey("motion", None): "Motion", + }, + entity_descriptions={ + PassiveBluetoothEntityKey("motion", None): BinarySensorEntityDescription( + key="motion", + device_class=BinarySensorDeviceClass.MOTION, + ), + }, +) + + +async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_start): + """Test integration of PassiveBluetoothProcessorCoordinator with multiple platforms.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + binary_sensor_processor = PassiveBluetoothDataProcessor( + lambda service_info: BINARY_SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE + ) + sesnor_processor = PassiveBluetoothDataProcessor( + lambda service_info: SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE + ) + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + coordinator.async_register_processor(binary_sensor_processor) + coordinator.async_register_processor(sesnor_processor) + + binary_sensor_processor.async_add_listener(MagicMock()) + sesnor_processor.async_add_listener(MagicMock()) + + mock_add_sensor_entities = MagicMock() + mock_add_binary_sensor_entities = MagicMock() + + sesnor_processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_sensor_entities, + ) + binary_sensor_processor.async_add_entities_listener( + PassiveBluetoothProcessorEntity, + mock_add_binary_sensor_entities, + ) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # First call with just the remote sensor entities results in them being added + assert len(mock_add_binary_sensor_entities.mock_calls) == 1 + assert len(mock_add_sensor_entities.mock_calls) == 1 + + binary_sesnor_entities = [ + *mock_add_binary_sensor_entities.mock_calls[0][1][0], + ] + sesnor_entities = [ + *mock_add_sensor_entities.mock_calls[0][1][0], + ] + + sensor_entity_one: PassiveBluetoothProcessorEntity = sesnor_entities[0] + sensor_entity_one.hass = hass + assert sensor_entity_one.available is True + assert sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-pressure" + assert sensor_entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "manufacturer": "Test Manufacturer", + "model": "Test Model", + "name": "Test Device", + } + assert sensor_entity_one.entity_key == PassiveBluetoothEntityKey( + key="pressure", device_id=None + ) + + binary_sensor_entity_one: PassiveBluetoothProcessorEntity = binary_sesnor_entities[ + 0 + ] + binary_sensor_entity_one.hass = hass + assert binary_sensor_entity_one.available is True + assert binary_sensor_entity_one.unique_id == "aa:bb:cc:dd:ee:ff-motion" + assert binary_sensor_entity_one.device_info == { + "identifiers": {("bluetooth", "aa:bb:cc:dd:ee:ff")}, + "manufacturer": "Test Manufacturer", + "model": "Test Model", + "name": "Test Device", + } + assert binary_sensor_entity_one.entity_key == PassiveBluetoothEntityKey( + key="motion", device_id=None + ) diff --git a/tests/components/inkbird/test_sensor.py b/tests/components/inkbird/test_sensor.py index 80a2179666f..a851cb92ec3 100644 --- a/tests/components/inkbird/test_sensor.py +++ b/tests/components/inkbird/test_sensor.py @@ -28,7 +28,7 @@ async def test_sensors(hass): return lambda: None with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", _async_register_callback, ): assert await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/sensorpush/test_sensor.py b/tests/components/sensorpush/test_sensor.py index c48b8bc3407..31fbdd8d712 100644 --- a/tests/components/sensorpush/test_sensor.py +++ b/tests/components/sensorpush/test_sensor.py @@ -28,7 +28,7 @@ async def test_sensors(hass): return lambda: None with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", _async_register_callback, ): assert await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 44b29ff1051..74a4fe65131 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -28,7 +28,7 @@ async def test_sensors(hass): return lambda: None with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", _async_register_callback, ): assert await hass.config_entries.async_setup(entry.entry_id) @@ -66,7 +66,7 @@ async def test_xiaomi_HHCCJCY01(hass): return lambda: None with patch( - "homeassistant.components.bluetooth.passive_update_coordinator.async_register_callback", + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", _async_register_callback, ): assert await hass.config_entries.async_setup(entry.entry_id) From a499dfb8ff4eabe00aa06d2e9c5668f913f97cb8 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 23 Jul 2022 20:06:10 -0600 Subject: [PATCH 2812/3516] Fix AssertionError in RainMachine (#75668) --- homeassistant/components/rainmachine/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 5a5329ad1fa..e57386fe0ec 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -243,10 +243,8 @@ class TimeRemainingSensor(RainMachineEntity, RestoreSensor): seconds_remaining = self.calculate_seconds_remaining() new_timestamp = now + timedelta(seconds=seconds_remaining) - assert isinstance(self._attr_native_value, datetime) - if ( - self._attr_native_value + isinstance(self._attr_native_value, datetime) and new_timestamp - self._attr_native_value < DEFAULT_ZONE_COMPLETION_TIME_WOBBLE_TOLERANCE ): From 82c92b56348423e082ac742683eb40f60d9396b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Jul 2022 02:51:14 -0500 Subject: [PATCH 2813/3516] Add Moat (BLE) integration (#75643) * Add Moat (BLE) integration * fix pin * resync * Update tests/components/moat/test_sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/moat/sensor.py * backmerge from integration * purge dead code Co-authored-by: Martin Hjelmare --- CODEOWNERS | 2 + homeassistant/components/moat/__init__.py | 40 +++++ homeassistant/components/moat/config_flow.py | 93 ++++++++++ homeassistant/components/moat/const.py | 3 + homeassistant/components/moat/manifest.json | 11 ++ homeassistant/components/moat/sensor.py | 164 ++++++++++++++++++ homeassistant/components/moat/strings.json | 21 +++ .../components/moat/translations/en.json | 21 +++ homeassistant/generated/bluetooth.py | 4 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/moat/__init__.py | 32 ++++ tests/components/moat/conftest.py | 8 + tests/components/moat/test_config_flow.py | 164 ++++++++++++++++++ tests/components/moat/test_sensor.py | 50 ++++++ 16 files changed, 620 insertions(+) create mode 100644 homeassistant/components/moat/__init__.py create mode 100644 homeassistant/components/moat/config_flow.py create mode 100644 homeassistant/components/moat/const.py create mode 100644 homeassistant/components/moat/manifest.json create mode 100644 homeassistant/components/moat/sensor.py create mode 100644 homeassistant/components/moat/strings.json create mode 100644 homeassistant/components/moat/translations/en.json create mode 100644 tests/components/moat/__init__.py create mode 100644 tests/components/moat/conftest.py create mode 100644 tests/components/moat/test_config_flow.py create mode 100644 tests/components/moat/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 073170795a1..c5bfb0cf24c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -648,6 +648,8 @@ build.json @home-assistant/supervisor /tests/components/minecraft_server/ @elmurato /homeassistant/components/minio/ @tkislan /tests/components/minio/ @tkislan +/homeassistant/components/moat/ @bdraco +/tests/components/moat/ @bdraco /homeassistant/components/mobile_app/ @home-assistant/core /tests/components/mobile_app/ @home-assistant/core /homeassistant/components/modbus/ @adamchengtkc @janiversen @vzahradnik diff --git a/homeassistant/components/moat/__init__.py b/homeassistant/components/moat/__init__.py new file mode 100644 index 00000000000..bd32ef64a0f --- /dev/null +++ b/homeassistant/components/moat/__init__.py @@ -0,0 +1,40 @@ +"""The Moat Bluetooth BLE integration.""" +from __future__ import annotations + +import logging + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Moat BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/moat/config_flow.py b/homeassistant/components/moat/config_flow.py new file mode 100644 index 00000000000..a353f4963ad --- /dev/null +++ b/homeassistant/components/moat/config_flow.py @@ -0,0 +1,93 @@ +"""Config flow for moat ble integration.""" +from __future__ import annotations + +from typing import Any + +from moat_ble import MoatBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfo, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class MoatConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for moat.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfo | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/moat/const.py b/homeassistant/components/moat/const.py new file mode 100644 index 00000000000..c2975343758 --- /dev/null +++ b/homeassistant/components/moat/const.py @@ -0,0 +1,3 @@ +"""Constants for the Moat Bluetooth integration.""" + +DOMAIN = "moat" diff --git a/homeassistant/components/moat/manifest.json b/homeassistant/components/moat/manifest.json new file mode 100644 index 00000000000..49e6985d1c1 --- /dev/null +++ b/homeassistant/components/moat/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "moat", + "name": "Moat", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/moat", + "bluetooth": [{ "local_name": "Moat_S*" }], + "requirements": ["moat-ble==0.1.1"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/moat/sensor.py b/homeassistant/components/moat/sensor.py new file mode 100644 index 00000000000..f29111a3406 --- /dev/null +++ b/homeassistant/components/moat/sensor.py @@ -0,0 +1,164 @@ +"""Support for moat ble sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from moat_ble import ( + DeviceClass, + DeviceKey, + MoatBluetoothDeviceData, + SensorDeviceInfo, + SensorUpdate, + Units, +) + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ELECTRIC_POTENTIAL_VOLT, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.VOLTAGE, Units.ELECTRIC_POTENTIAL_VOLT): SensorEntityDescription( + key=f"{DeviceClass.VOLTAGE}_{Units.ELECTRIC_POTENTIAL_VOLT}", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def _sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to hass device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: _sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Moat BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + data = MoatBluetoothDeviceData() + processor = PassiveBluetoothDataProcessor( + lambda service_info: sensor_update_to_bluetooth_data_update( + data.update(service_info) + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + entry.async_on_unload( + processor.async_add_entities_listener( + MoatBluetoothSensorEntity, async_add_entities + ) + ) + + +class MoatBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a moat ble sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/moat/strings.json b/homeassistant/components/moat/strings.json new file mode 100644 index 00000000000..7111626cca1 --- /dev/null +++ b/homeassistant/components/moat/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/moat/translations/en.json b/homeassistant/components/moat/translations/en.json new file mode 100644 index 00000000000..d24df64f135 --- /dev/null +++ b/homeassistant/components/moat/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 175b82dbcfd..4e527a02bc0 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -30,6 +30,10 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "domain": "inkbird", "local_name": "tps" }, + { + "domain": "moat", + "local_name": "Moat_S*" + }, { "domain": "sensorpush", "local_name": "SensorPush*" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 917eca321ea..33bd4ea0a52 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -217,6 +217,7 @@ FLOWS = { "mill", "minecraft_server", "mjpeg", + "moat", "mobile_app", "modem_callerid", "modern_forms", diff --git a/requirements_all.txt b/requirements_all.txt index 4309d2f34c5..e0f3abff185 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1049,6 +1049,9 @@ minio==5.0.10 # homeassistant.components.mitemp_bt mitemp_bt==0.0.5 +# homeassistant.components.moat +moat-ble==0.1.1 + # homeassistant.components.moehlenhoff_alpha2 moehlenhoff-alpha2==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4d8e790b50f..ee41c383f4a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -738,6 +738,9 @@ millheater==0.9.0 # homeassistant.components.minio minio==5.0.10 +# homeassistant.components.moat +moat-ble==0.1.1 + # homeassistant.components.moehlenhoff_alpha2 moehlenhoff-alpha2==1.2.1 diff --git a/tests/components/moat/__init__.py b/tests/components/moat/__init__.py new file mode 100644 index 00000000000..358d338993f --- /dev/null +++ b/tests/components/moat/__init__.py @@ -0,0 +1,32 @@ +"""Tests for the Moat BLE integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_MOAT_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +MOAT_S2_SERVICE_INFO = BluetoothServiceInfo( + name="Moat_S2", + manufacturer_data={}, + service_data={ + "00005000-0000-1000-8000-00805f9b34fb": b"\xdfy\xe3\xa6\x12\xb3\xf5\x0b", + "00001000-0000-1000-8000-00805f9b34fb": ( + b"\xdfy\xe3\xa6\x12\xb3\x11S\xdbb\xfcbpq" b"\xf5\x0b\xff\xff" + ), + }, + service_uuids=[ + "00001000-0000-1000-8000-00805f9b34fb", + "00002000-0000-1000-8000-00805f9b34fb", + ], + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + source="local", +) diff --git a/tests/components/moat/conftest.py b/tests/components/moat/conftest.py new file mode 100644 index 00000000000..1f7f00c8d2f --- /dev/null +++ b/tests/components/moat/conftest.py @@ -0,0 +1,8 @@ +"""Moat session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/moat/test_config_flow.py b/tests/components/moat/test_config_flow.py new file mode 100644 index 00000000000..7ceeb2ad73f --- /dev/null +++ b/tests/components/moat/test_config_flow.py @@ -0,0 +1,164 @@ +"""Test the Moat config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.moat.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import MOAT_S2_SERVICE_INFO, NOT_MOAT_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MOAT_S2_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch("homeassistant.components.moat.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Moat S2 EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_bluetooth_not_moat(hass): + """Test discovery via bluetooth not moat.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_MOAT_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.moat.config_flow.async_discovered_service_info", + return_value=[MOAT_S2_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch("homeassistant.components.moat.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Moat S2 EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.moat.config_flow.async_discovered_service_info", + return_value=[MOAT_S2_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MOAT_S2_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MOAT_S2_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MOAT_S2_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MOAT_S2_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.moat.config_flow.async_discovered_service_info", + return_value=[MOAT_S2_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch("homeassistant.components.moat.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Moat S2 EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/moat/test_sensor.py b/tests/components/moat/test_sensor.py new file mode 100644 index 00000000000..826bbc72cdb --- /dev/null +++ b/tests/components/moat/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the Moat sensors.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.moat.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import MOAT_S2_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback(MOAT_S2_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 4 + + temp_sensor = hass.states.get("sensor.moat_s2_eeff_voltage") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "3.061" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Moat S2 EEFF Voltage" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "V" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 7075032bf743f8702d942410c0c41214c90c212b Mon Sep 17 00:00:00 2001 From: Dave T <17680170+davet2001@users.noreply.github.com> Date: Sun, 24 Jul 2022 09:21:01 +0100 Subject: [PATCH 2814/3516] Fix diagnostics export for generic camera (#75665) Fix url redaction and add tests Co-authored-by: Dave T --- .../components/generic/diagnostics.py | 6 ++-- tests/components/generic/test_diagnostics.py | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/generic/diagnostics.py b/homeassistant/components/generic/diagnostics.py index 00be287f053..39d6a81ad88 100644 --- a/homeassistant/components/generic/diagnostics.py +++ b/homeassistant/components/generic/diagnostics.py @@ -21,12 +21,12 @@ TO_REDACT = { # A very similar redact function is in components.sql. Possible to be made common. def redact_url(data: str) -> str: """Redact credentials from string url.""" - url_in = yarl.URL(data) + url = url_in = yarl.URL(data) if url_in.user: - url = url_in.with_user("****") + url = url.with_user("****") if url_in.password: url = url.with_password("****") - if url_in.path: + if url_in.path != "/": url = url.with_path("****") if url_in.query_string: url = url.with_query("****=****") diff --git a/tests/components/generic/test_diagnostics.py b/tests/components/generic/test_diagnostics.py index 2d4e4c536d8..d31503c11c8 100644 --- a/tests/components/generic/test_diagnostics.py +++ b/tests/components/generic/test_diagnostics.py @@ -1,5 +1,8 @@ """Test generic (IP camera) diagnostics.""" +import pytest + from homeassistant.components.diagnostics import REDACTED +from homeassistant.components.generic.diagnostics import redact_url from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -22,3 +25,34 @@ async def test_entry_diagnostics(hass, hass_client, setup_entry): "content_type": "image/jpeg", }, } + + +@pytest.mark.parametrize( + ("url_in", "url_out_expected"), + [ + ( + "http://www.example.com", + "http://www.example.com", + ), + ( + "http://fred:letmein1@www.example.com/image.php?key=secret2", + "http://****:****@www.example.com/****?****=****", + ), + ( + "http://fred@www.example.com/image.php?key=secret2", + "http://****@www.example.com/****?****=****", + ), + ( + "http://fred@www.example.com/image.php", + "http://****@www.example.com/****", + ), + ( + "http://:letmein1@www.example.com", + "http://:****@www.example.com", + ), + ], +) +def test_redact_url(url_in, url_out_expected): + """Test url redaction.""" + url_out = redact_url(url_in) + assert url_out == url_out_expected From ba71a3c24dbff4ebee2dd7a7fbd905d98fb8c7e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Jul 2022 03:39:23 -0500 Subject: [PATCH 2815/3516] Add Govee BLE integration (#75631) * Add Govee BLE integration * add missing files * remove test file not needed yet * fix * add bbq sensors * fixed lib * bump again to fix the names * fix discovery of the newer bbq devices * fix the test to test the right thing * verify no outstanding flows * only accept entities that match the platform * refactor * refactor * refactor * Refactor PassiveBluetoothDataUpdateCoordinator to support multiple platforms * cover * Update for new model * Update for new model * Update tests/components/govee_ble/test_sensor.py Co-authored-by: Martin Hjelmare * purge dead code * backmerge from integration * Update docstring * Update docstring Co-authored-by: Martin Hjelmare --- CODEOWNERS | 2 + .../components/govee_ble/__init__.py | 40 +++++ .../components/govee_ble/config_flow.py | 93 ++++++++++ homeassistant/components/govee_ble/const.py | 3 + .../components/govee_ble/manifest.json | 27 +++ homeassistant/components/govee_ble/sensor.py | 157 ++++++++++++++++ .../components/govee_ble/strings.json | 21 +++ .../components/govee_ble/translations/en.json | 21 +++ homeassistant/generated/bluetooth.py | 27 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/govee_ble/__init__.py | 38 ++++ tests/components/govee_ble/conftest.py | 8 + .../components/govee_ble/test_config_flow.py | 170 ++++++++++++++++++ tests/components/govee_ble/test_sensor.py | 50 ++++++ 16 files changed, 664 insertions(+) create mode 100644 homeassistant/components/govee_ble/__init__.py create mode 100644 homeassistant/components/govee_ble/config_flow.py create mode 100644 homeassistant/components/govee_ble/const.py create mode 100644 homeassistant/components/govee_ble/manifest.json create mode 100644 homeassistant/components/govee_ble/sensor.py create mode 100644 homeassistant/components/govee_ble/strings.json create mode 100644 homeassistant/components/govee_ble/translations/en.json create mode 100644 tests/components/govee_ble/__init__.py create mode 100644 tests/components/govee_ble/conftest.py create mode 100644 tests/components/govee_ble/test_config_flow.py create mode 100644 tests/components/govee_ble/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index c5bfb0cf24c..66e54b25b9e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -411,6 +411,8 @@ build.json @home-assistant/supervisor /homeassistant/components/google_cloud/ @lufton /homeassistant/components/google_travel_time/ @eifinger /tests/components/google_travel_time/ @eifinger +/homeassistant/components/govee_ble/ @bdraco +/tests/components/govee_ble/ @bdraco /homeassistant/components/gpsd/ @fabaff /homeassistant/components/gree/ @cmroche /tests/components/gree/ @cmroche diff --git a/homeassistant/components/govee_ble/__init__.py b/homeassistant/components/govee_ble/__init__.py new file mode 100644 index 00000000000..3cb9e4659d6 --- /dev/null +++ b/homeassistant/components/govee_ble/__init__.py @@ -0,0 +1,40 @@ +"""The Govee Bluetooth BLE integration.""" +from __future__ import annotations + +import logging + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Govee BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/govee_ble/config_flow.py b/homeassistant/components/govee_ble/config_flow.py new file mode 100644 index 00000000000..9f2efac0ce9 --- /dev/null +++ b/homeassistant/components/govee_ble/config_flow.py @@ -0,0 +1,93 @@ +"""Config flow for govee ble integration.""" +from __future__ import annotations + +from typing import Any + +from govee_ble import GoveeBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfo, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class GoveeConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for govee.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfo | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/govee_ble/const.py b/homeassistant/components/govee_ble/const.py new file mode 100644 index 00000000000..4f30ee5023f --- /dev/null +++ b/homeassistant/components/govee_ble/const.py @@ -0,0 +1,3 @@ +"""Constants for the Govee Bluetooth integration.""" + +DOMAIN = "govee_ble" diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json new file mode 100644 index 00000000000..aa86215da59 --- /dev/null +++ b/homeassistant/components/govee_ble/manifest.json @@ -0,0 +1,27 @@ +{ + "domain": "govee_ble", + "name": "Govee Bluetooth", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/govee_ble", + "bluetooth": [ + { "local_name": "Govee*" }, + { "local_name": "GVH5*" }, + { "local_name": "B5178*" }, + { + "manufacturer_id": 26589, + "service_uuid": "00008351-0000-1000-8000-00805f9b34fb" + }, + { + "manufacturer_id": 18994, + "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" + }, + { + "manufacturer_id": 14474, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + } + ], + "requirements": ["govee-ble==0.12.3"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/govee_ble/sensor.py b/homeassistant/components/govee_ble/sensor.py new file mode 100644 index 00000000000..b8af7df8fb9 --- /dev/null +++ b/homeassistant/components/govee_ble/sensor.py @@ -0,0 +1,157 @@ +"""Support for govee ble sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from govee_ble import ( + DeviceClass, + DeviceKey, + GoveeBluetoothDeviceData, + SensorDeviceInfo, + SensorUpdate, + Units, +) + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothEntityKey, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN + +SENSOR_DESCRIPTIONS = { + (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( + key=f"{DeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (DeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{DeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + DeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{DeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), +} + + +def _device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def _sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to hass device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: _sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Govee BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + data = GoveeBluetoothDeviceData() + processor = PassiveBluetoothDataProcessor( + lambda service_info: sensor_update_to_bluetooth_data_update( + data.update(service_info) + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + entry.async_on_unload( + processor.async_add_entities_listener( + GoveeBluetoothSensorEntity, async_add_entities + ) + ) + + +class GoveeBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a govee ble sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/govee_ble/strings.json b/homeassistant/components/govee_ble/strings.json new file mode 100644 index 00000000000..7111626cca1 --- /dev/null +++ b/homeassistant/components/govee_ble/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/govee_ble/translations/en.json b/homeassistant/components/govee_ble/translations/en.json new file mode 100644 index 00000000000..d24df64f135 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 4e527a02bc0..f9dd4352e28 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -7,6 +7,33 @@ from __future__ import annotations # fmt: off BLUETOOTH: list[dict[str, str | int | list[int]]] = [ + { + "domain": "govee_ble", + "local_name": "Govee*" + }, + { + "domain": "govee_ble", + "local_name": "GVH5*" + }, + { + "domain": "govee_ble", + "local_name": "B5178*" + }, + { + "domain": "govee_ble", + "manufacturer_id": 26589, + "service_uuid": "00008351-0000-1000-8000-00805f9b34fb" + }, + { + "domain": "govee_ble", + "manufacturer_id": 18994, + "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" + }, + { + "domain": "govee_ble", + "manufacturer_id": 14474, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + }, { "domain": "homekit_controller", "manufacturer_id": 76, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 33bd4ea0a52..28d0ad6b44b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -138,6 +138,7 @@ FLOWS = { "goodwe", "google", "google_travel_time", + "govee_ble", "gpslogger", "gree", "growatt_server", diff --git a/requirements_all.txt b/requirements_all.txt index e0f3abff185..512c4535b47 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,6 +760,9 @@ googlemaps==2.5.1 # homeassistant.components.slide goslide-api==0.5.1 +# homeassistant.components.govee_ble +govee-ble==0.12.3 + # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ee41c383f4a..a805e61fba2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -560,6 +560,9 @@ google-nest-sdm==2.0.0 # homeassistant.components.google_travel_time googlemaps==2.5.1 +# homeassistant.components.govee_ble +govee-ble==0.12.3 + # homeassistant.components.gree greeclimate==1.2.0 diff --git a/tests/components/govee_ble/__init__.py b/tests/components/govee_ble/__init__.py new file mode 100644 index 00000000000..3baea5e1140 --- /dev/null +++ b/tests/components/govee_ble/__init__.py @@ -0,0 +1,38 @@ +"""Tests for the Govee BLE integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_GOVEE_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +GVH5075_SERVICE_INFO = BluetoothServiceInfo( + name="GVH5075_2762", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={ + 60552: b"\x00\x03A\xc2d\x00L\x00\x02\x15INTELLI_ROCKS_HWPu\xf2\xff\x0c" + }, + service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], + service_data={}, + source="local", +) + +GVH5177_SERVICE_INFO = BluetoothServiceInfo( + name="GVH5177_2EC8", + address="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + rssi=-56, + manufacturer_data={ + 1: b"\x01\x01\x036&dL\x00\x02\x15INTELLI_ROCKS_HWQw\xf2\xff\xc2" + }, + service_uuids=["0000ec88-0000-1000-8000-00805f9b34fb"], + service_data={}, + source="local", +) diff --git a/tests/components/govee_ble/conftest.py b/tests/components/govee_ble/conftest.py new file mode 100644 index 00000000000..382854a5a28 --- /dev/null +++ b/tests/components/govee_ble/conftest.py @@ -0,0 +1,8 @@ +"""Govee session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/govee_ble/test_config_flow.py b/tests/components/govee_ble/test_config_flow.py new file mode 100644 index 00000000000..a1b9fed3cd7 --- /dev/null +++ b/tests/components/govee_ble/test_config_flow.py @@ -0,0 +1,170 @@ +"""Test the Govee config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.govee_ble.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import GVH5075_SERVICE_INFO, GVH5177_SERVICE_INFO, NOT_GOVEE_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=GVH5075_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.govee_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "H5075_2762" + assert result2["data"] == {} + assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" + + +async def test_async_step_bluetooth_not_govee(hass): + """Test discovery via bluetooth not govee.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_GOVEE_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.govee_ble.config_flow.async_discovered_service_info", + return_value=[GVH5177_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.govee_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "H5177_2EC8" + assert result2["data"] == {} + assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.govee_ble.config_flow.async_discovered_service_info", + return_value=[GVH5177_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=GVH5177_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=GVH5177_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=GVH5177_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=GVH5177_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.govee_ble.config_flow.async_discovered_service_info", + return_value=[GVH5177_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.govee_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "H5177_2EC8" + assert result2["data"] == {} + assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/govee_ble/test_sensor.py b/tests/components/govee_ble/test_sensor.py new file mode 100644 index 00000000000..7a6ecbaed51 --- /dev/null +++ b/tests/components/govee_ble/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the Govee BLE sensors.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.govee_ble.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import GVH5075_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback(GVH5075_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 3 + + temp_sensor = hass.states.get("sensor.h5075_2762_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "21.3442" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "H5075_2762 Temperature" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 79be87f9ce7e5e624cbd7b7457b99cc06dbf466b Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sun, 24 Jul 2022 10:48:22 +0200 Subject: [PATCH 2816/3516] Update pyotgw to 2.0.1 (#75663) --- homeassistant/components/opentherm_gw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index dfb60413721..0bc69387d0b 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -2,7 +2,7 @@ "domain": "opentherm_gw", "name": "OpenTherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", - "requirements": ["pyotgw==2.0.0"], + "requirements": ["pyotgw==2.0.1"], "codeowners": ["@mvn23"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 512c4535b47..3186aee8707 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1732,7 +1732,7 @@ pyopnsense==0.2.0 pyoppleio==1.0.5 # homeassistant.components.opentherm_gw -pyotgw==2.0.0 +pyotgw==2.0.1 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a805e61fba2..b8b42ec87e8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1190,7 +1190,7 @@ pyopenuv==2022.04.0 pyopnsense==0.2.0 # homeassistant.components.opentherm_gw -pyotgw==2.0.0 +pyotgw==2.0.1 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp From 198167a2c82a4a7358491a259d91b253b8d8ad0f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Jul 2022 11:38:45 -0500 Subject: [PATCH 2817/3516] Update switchbot to be local push (#75645) * Update switchbot to be local push * fixes * fixes * fixes * fixes * adjust * cover is not assumed anymore * cleanups * adjust * adjust * add missing cover * import compat * fixes * uses lower * uses lower * bleak users upper case addresses * fixes * bump * keep conf_mac and deprecated options for rollback * reuse coordinator * adjust * move around * move around * move around * move around * refactor fixes * compat with DataUpdateCoordinator * fix available * Update homeassistant/components/bluetooth/passive_update_processor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/bluetooth/passive_update_coordinator.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/bluetooth/update_coordinator.py Co-authored-by: Martin Hjelmare * Split bluetooth coordinator into PassiveBluetoothDataUpdateCoordinator and PassiveBluetoothProcessorCoordinator The PassiveBluetoothDataUpdateCoordinator is now used to replace instances of DataUpdateCoordinator where the data is coming from bluetooth advertisements, and the integration may also mix in active updates The PassiveBluetoothProcessorCoordinator is used for integrations that want to process each bluetooth advertisement with multiple processors which can be dispatched to individual platforms or areas or the integration as it chooes * change connections * reduce code churn to reduce review overhead * reduce code churn to reduce review overhead * Update homeassistant/components/bluetooth/passive_update_coordinator.py Co-authored-by: Martin Hjelmare * add basic test * add basic test * complete coverage * Update homeassistant/components/switchbot/coordinator.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/switchbot/coordinator.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/switchbot/__init__.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/switchbot/__init__.py Co-authored-by: Martin Hjelmare * lint Co-authored-by: Martin Hjelmare --- .../components/switchbot/__init__.py | 120 +++++---- .../components/switchbot/binary_sensor.py | 27 +- .../components/switchbot/config_flow.py | 143 +++++------ homeassistant/components/switchbot/const.py | 9 +- .../components/switchbot/coordinator.py | 82 ++++--- homeassistant/components/switchbot/cover.py | 43 ++-- homeassistant/components/switchbot/entity.py | 35 ++- .../components/switchbot/manifest.json | 5 +- homeassistant/components/switchbot/sensor.py | 29 +-- .../components/switchbot/strings.json | 8 +- homeassistant/components/switchbot/switch.py | 37 ++- .../components/switchbot/translations/en.json | 9 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/switchbot/__init__.py | 82 ++++++- tests/components/switchbot/conftest.py | 139 +---------- .../components/switchbot/test_config_flow.py | 232 ++++++++++++++---- 17 files changed, 542 insertions(+), 462 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index d4418685cff..2dbc66a864d 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -1,10 +1,24 @@ """Support for Switchbot devices.""" +from collections.abc import Mapping +import logging +from types import MappingProxyType +from typing import Any + import switchbot +from homeassistant.components import bluetooth from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_SENSOR_TYPE, Platform +from homeassistant.const import ( + CONF_ADDRESS, + CONF_MAC, + CONF_PASSWORD, + CONF_SENSOR_TYPE, + Platform, +) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr from .const import ( ATTR_BOT, @@ -13,13 +27,8 @@ from .const import ( COMMON_OPTIONS, CONF_RETRY_COUNT, CONF_RETRY_TIMEOUT, - CONF_SCAN_TIMEOUT, - CONF_TIME_BETWEEN_UPDATE_COMMAND, - DATA_COORDINATOR, DEFAULT_RETRY_COUNT, DEFAULT_RETRY_TIMEOUT, - DEFAULT_SCAN_TIMEOUT, - DEFAULT_TIME_BETWEEN_UPDATE_COMMAND, DOMAIN, ) from .coordinator import SwitchbotDataUpdateCoordinator @@ -29,57 +38,67 @@ PLATFORMS_BY_TYPE = { ATTR_CURTAIN: [Platform.COVER, Platform.BINARY_SENSOR, Platform.SENSOR], ATTR_HYGROMETER: [Platform.SENSOR], } +CLASS_BY_DEVICE = { + ATTR_CURTAIN: switchbot.SwitchbotCurtain, + ATTR_BOT: switchbot.Switchbot, +} + +_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Switchbot from a config entry.""" hass.data.setdefault(DOMAIN, {}) + domain_data = hass.data[DOMAIN] - if not entry.options: - options = { - CONF_TIME_BETWEEN_UPDATE_COMMAND: DEFAULT_TIME_BETWEEN_UPDATE_COMMAND, - CONF_RETRY_COUNT: DEFAULT_RETRY_COUNT, - CONF_RETRY_TIMEOUT: DEFAULT_RETRY_TIMEOUT, - CONF_SCAN_TIMEOUT: DEFAULT_SCAN_TIMEOUT, - } - - hass.config_entries.async_update_entry(entry, options=options) - - # Use same coordinator instance for all entities. - # Uses BTLE advertisement data, all Switchbot devices in range is stored here. - if DATA_COORDINATOR not in hass.data[DOMAIN]: - - if COMMON_OPTIONS not in hass.data[DOMAIN]: - hass.data[DOMAIN][COMMON_OPTIONS] = {**entry.options} - - switchbot.DEFAULT_RETRY_TIMEOUT = hass.data[DOMAIN][COMMON_OPTIONS][ - CONF_RETRY_TIMEOUT - ] - - # Store api in coordinator. - coordinator = SwitchbotDataUpdateCoordinator( - hass, - update_interval=hass.data[DOMAIN][COMMON_OPTIONS][ - CONF_TIME_BETWEEN_UPDATE_COMMAND - ], - api=switchbot, - retry_count=hass.data[DOMAIN][COMMON_OPTIONS][CONF_RETRY_COUNT], - scan_timeout=hass.data[DOMAIN][COMMON_OPTIONS][CONF_SCAN_TIMEOUT], + if CONF_ADDRESS not in entry.data and CONF_MAC in entry.data: + # Bleak uses addresses not mac addresses which are are actually + # UUIDs on some platforms (MacOS). + mac = entry.data[CONF_MAC] + if "-" not in mac: + mac = dr.format_mac(mac) + hass.config_entries.async_update_entry( + entry, + data={**entry.data, CONF_ADDRESS: mac}, ) - hass.data[DOMAIN][DATA_COORDINATOR] = coordinator + if not entry.options: + hass.config_entries.async_update_entry( + entry, + options={ + CONF_RETRY_COUNT: DEFAULT_RETRY_COUNT, + CONF_RETRY_TIMEOUT: DEFAULT_RETRY_TIMEOUT, + }, + ) - else: - coordinator = hass.data[DOMAIN][DATA_COORDINATOR] + sensor_type: str = entry.data[CONF_SENSOR_TYPE] + address: str = entry.data[CONF_ADDRESS] + ble_device = bluetooth.async_ble_device_from_address(hass, address.upper()) + if not ble_device: + raise ConfigEntryNotReady( + f"Could not find Switchbot {sensor_type} with address {address}" + ) - await coordinator.async_config_entry_first_refresh() + if COMMON_OPTIONS not in domain_data: + domain_data[COMMON_OPTIONS] = entry.options + + common_options: Mapping[str, int] = domain_data[COMMON_OPTIONS] + switchbot.DEFAULT_RETRY_TIMEOUT = common_options[CONF_RETRY_TIMEOUT] + + cls = CLASS_BY_DEVICE.get(sensor_type, switchbot.SwitchbotDevice) + device = cls( + device=ble_device, + password=entry.data.get(CONF_PASSWORD), + retry_count=entry.options[CONF_RETRY_COUNT], + ) + coordinator = hass.data[DOMAIN][entry.entry_id] = SwitchbotDataUpdateCoordinator( + hass, _LOGGER, ble_device, device, common_options + ) + entry.async_on_unload(coordinator.async_start()) + if not await coordinator.async_wait_ready(): + raise ConfigEntryNotReady(f"Switchbot {sensor_type} with {address} not ready") entry.async_on_unload(entry.add_update_listener(_async_update_listener)) - - hass.data[DOMAIN][entry.entry_id] = {DATA_COORDINATOR: coordinator} - - sensor_type = entry.data[CONF_SENSOR_TYPE] - await hass.config_entries.async_forward_entry_setups( entry, PLATFORMS_BY_TYPE[sensor_type] ) @@ -96,8 +115,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - - if len(hass.config_entries.async_entries(DOMAIN)) == 0: + if not hass.config_entries.async_entries(DOMAIN): hass.data.pop(DOMAIN) return unload_ok @@ -106,8 +124,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" # Update entity options stored in hass. - if {**entry.options} != hass.data[DOMAIN][COMMON_OPTIONS]: - hass.data[DOMAIN][COMMON_OPTIONS] = {**entry.options} - hass.data[DOMAIN].pop(DATA_COORDINATOR) - - await hass.config_entries.async_reload(entry.entry_id) + common_options: MappingProxyType[str, Any] = hass.data[DOMAIN][COMMON_OPTIONS] + if entry.options != common_options: + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index e2a5a951d1d..d644f603697 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -6,13 +6,12 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MAC, CONF_NAME +from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_COORDINATOR, DOMAIN +from .const import DOMAIN from .coordinator import SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity @@ -30,23 +29,19 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Switchbot curtain based on a config entry.""" - coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR - ] - - if not coordinator.data.get(entry.unique_id): - raise PlatformNotReady - + coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + unique_id = entry.unique_id + assert unique_id is not None async_add_entities( [ SwitchBotBinarySensor( coordinator, - entry.unique_id, + unique_id, binary_sensor, - entry.data[CONF_MAC], + entry.data[CONF_ADDRESS], entry.data[CONF_NAME], ) - for binary_sensor in coordinator.data[entry.unique_id]["data"] + for binary_sensor in coordinator.data["data"] if binary_sensor in BINARY_SENSOR_TYPES ] ) @@ -58,15 +53,15 @@ class SwitchBotBinarySensor(SwitchbotEntity, BinarySensorEntity): def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, - idx: str | None, + unique_id: str, binary_sensor: str, mac: str, switchbot_name: str, ) -> None: """Initialize the Switchbot sensor.""" - super().__init__(coordinator, idx, mac, name=switchbot_name) + super().__init__(coordinator, unique_id, mac, name=switchbot_name) self._sensor = binary_sensor - self._attr_unique_id = f"{idx}-{binary_sensor}" + self._attr_unique_id = f"{unique_id}-{binary_sensor}" self._attr_name = f"{switchbot_name} {binary_sensor.title()}" self.entity_description = BINARY_SENSOR_TYPES[binary_sensor] diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index b35ba052d0a..f6f175819b6 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -2,25 +2,26 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, cast -from switchbot import GetSwitchbotDevices +from switchbot import SwitchBotAdvertisement, parse_advertisement_data import voluptuous as vol +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow -from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from .const import ( CONF_RETRY_COUNT, CONF_RETRY_TIMEOUT, - CONF_SCAN_TIMEOUT, - CONF_TIME_BETWEEN_UPDATE_COMMAND, DEFAULT_RETRY_COUNT, DEFAULT_RETRY_TIMEOUT, - DEFAULT_SCAN_TIMEOUT, - DEFAULT_TIME_BETWEEN_UPDATE_COMMAND, DOMAIN, SUPPORTED_MODEL_TYPES, ) @@ -28,15 +29,9 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -async def _btle_connect() -> dict: - """Scan for BTLE advertisement data.""" - - switchbot_devices = await GetSwitchbotDevices().discover() - - if not switchbot_devices: - raise NotConnectedError("Failed to discover switchbot") - - return switchbot_devices +def format_unique_id(address: str) -> str: + """Format the unique ID for a switchbot.""" + return address.replace(":", "").lower() class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): @@ -44,18 +39,6 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - async def _get_switchbots(self) -> dict: - """Try to discover nearby Switchbot devices.""" - # asyncio.lock prevents btle adapter exceptions if there are multiple calls to this method. - # store asyncio.lock in hass data if not present. - if DOMAIN not in self.hass.data: - self.hass.data.setdefault(DOMAIN, {}) - - # Discover switchbots nearby. - _btle_adv_data = await _btle_connect() - - return _btle_adv_data - @staticmethod @callback def async_get_options_flow( @@ -66,62 +49,79 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the config flow.""" - self._discovered_devices = {} + self._discovered_adv: SwitchBotAdvertisement | None = None + self._discovered_advs: dict[str, SwitchBotAdvertisement] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfo + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + _LOGGER.debug("Discovered bluetooth device: %s", discovery_info) + await self.async_set_unique_id(format_unique_id(discovery_info.address)) + self._abort_if_unique_id_configured() + discovery_info_bleak = cast(BluetoothServiceInfoBleak, discovery_info) + parsed = parse_advertisement_data( + discovery_info_bleak.device, discovery_info_bleak.advertisement + ) + if not parsed or parsed.data.get("modelName") not in SUPPORTED_MODEL_TYPES: + return self.async_abort(reason="not_supported") + self._discovered_adv = parsed + data = parsed.data + self.context["title_placeholders"] = { + "name": data["modelName"], + "address": discovery_info.address, + } + return await self.async_step_user() async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Handle a flow initiated by the user.""" - + """Handle the user step to pick discovered device.""" errors: dict[str, str] = {} if user_input is not None: - await self.async_set_unique_id(user_input[CONF_MAC].replace(":", "")) + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id( + format_unique_id(address), raise_on_progress=False + ) self._abort_if_unique_id_configured() - user_input[CONF_SENSOR_TYPE] = SUPPORTED_MODEL_TYPES[ - self._discovered_devices[self.unique_id]["modelName"] + self._discovered_advs[address].data["modelName"] ] - return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) - try: - self._discovered_devices = await self._get_switchbots() - for device in self._discovered_devices.values(): - _LOGGER.debug("Found %s", device) + if discovery := self._discovered_adv: + self._discovered_advs[discovery.address] = discovery + else: + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if ( + format_unique_id(address) in current_addresses + or address in self._discovered_advs + ): + continue + parsed = parse_advertisement_data( + discovery_info.device, discovery_info.advertisement + ) + if parsed and parsed.data.get("modelName") in SUPPORTED_MODEL_TYPES: + self._discovered_advs[address] = parsed - except NotConnectedError: - return self.async_abort(reason="cannot_connect") - - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - return self.async_abort(reason="unknown") - - # Get devices already configured. - configured_devices = { - item.data[CONF_MAC] - for item in self._async_current_entries(include_ignore=False) - } - - # Get supported devices not yet configured. - unconfigured_devices = { - device["mac_address"]: f"{device['mac_address']} {device['modelName']}" - for device in self._discovered_devices.values() - if device.get("modelName") in SUPPORTED_MODEL_TYPES - and device["mac_address"] not in configured_devices - } - - if not unconfigured_devices: + if not self._discovered_advs: return self.async_abort(reason="no_unconfigured_devices") data_schema = vol.Schema( { - vol.Required(CONF_MAC): vol.In(unconfigured_devices), + vol.Required(CONF_ADDRESS): vol.In( + { + address: f"{parsed.data['modelName']} ({address})" + for address, parsed in self._discovered_advs.items() + } + ), vol.Required(CONF_NAME): str, vol.Optional(CONF_PASSWORD): str, } ) - return self.async_show_form( step_id="user", data_schema=data_schema, errors=errors ) @@ -148,13 +148,6 @@ class SwitchbotOptionsFlowHandler(OptionsFlow): return self.async_create_entry(title="", data=user_input) options = { - vol.Optional( - CONF_TIME_BETWEEN_UPDATE_COMMAND, - default=self.config_entry.options.get( - CONF_TIME_BETWEEN_UPDATE_COMMAND, - DEFAULT_TIME_BETWEEN_UPDATE_COMMAND, - ), - ): int, vol.Optional( CONF_RETRY_COUNT, default=self.config_entry.options.get( @@ -167,16 +160,6 @@ class SwitchbotOptionsFlowHandler(OptionsFlow): CONF_RETRY_TIMEOUT, DEFAULT_RETRY_TIMEOUT ), ): int, - vol.Optional( - CONF_SCAN_TIMEOUT, - default=self.config_entry.options.get( - CONF_SCAN_TIMEOUT, DEFAULT_SCAN_TIMEOUT - ), - ): int, } return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) - - -class NotConnectedError(Exception): - """Exception for unable to find device.""" diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index a363c030eb1..a841b8388dd 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -16,15 +16,14 @@ SUPPORTED_MODEL_TYPES = { # Config Defaults DEFAULT_RETRY_COUNT = 3 DEFAULT_RETRY_TIMEOUT = 5 -DEFAULT_TIME_BETWEEN_UPDATE_COMMAND = 60 -DEFAULT_SCAN_TIMEOUT = 5 # Config Options -CONF_TIME_BETWEEN_UPDATE_COMMAND = "update_time" CONF_RETRY_COUNT = "retry_count" -CONF_RETRY_TIMEOUT = "retry_timeout" CONF_SCAN_TIMEOUT = "scan_timeout" +# Deprecated config Entry Options to be removed in 2023.4 +CONF_TIME_BETWEEN_UPDATE_COMMAND = "update_time" +CONF_RETRY_TIMEOUT = "retry_timeout" + # Data -DATA_COORDINATOR = "coordinator" COMMON_OPTIONS = "common_options" diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 4a4831f2cdb..74cc54402d1 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -1,15 +1,22 @@ """Provides the switchbot DataUpdateCoordinator.""" from __future__ import annotations -from datetime import timedelta +import asyncio +from collections.abc import Mapping import logging +from typing import Any, cast +from bleak.backends.device import BLEDevice import switchbot +from switchbot import parse_advertisement_data -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothDataUpdateCoordinator, +) +from homeassistant.core import HomeAssistant, callback -from .const import DOMAIN +from .const import CONF_RETRY_COUNT _LOGGER = logging.getLogger(__name__) @@ -22,40 +29,53 @@ def flatten_sensors_data(sensor): return sensor -class SwitchbotDataUpdateCoordinator(DataUpdateCoordinator): +class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): """Class to manage fetching switchbot data.""" def __init__( self, hass: HomeAssistant, - *, - update_interval: int, - api: switchbot, - retry_count: int, - scan_timeout: int, + logger: logging.Logger, + ble_device: BLEDevice, + device: switchbot.SwitchbotDevice, + common_options: Mapping[str, int], ) -> None: """Initialize global switchbot data updater.""" - self.switchbot_api = api - self.switchbot_data = self.switchbot_api.GetSwitchbotDevices() - self.retry_count = retry_count - self.scan_timeout = scan_timeout - self.update_interval = timedelta(seconds=update_interval) + super().__init__(hass, logger, ble_device.address) + self.ble_device = ble_device + self.device = device + self.common_options = common_options + self.data: dict[str, Any] = {} + self._ready_event = asyncio.Event() - super().__init__( - hass, _LOGGER, name=DOMAIN, update_interval=self.update_interval - ) + @property + def retry_count(self) -> int: + """Return retry count.""" + return self.common_options[CONF_RETRY_COUNT] - async def _async_update_data(self) -> dict | None: - """Fetch data from switchbot.""" + @callback + def _async_handle_bluetooth_event( + self, + service_info: bluetooth.BluetoothServiceInfo, + change: bluetooth.BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + super()._async_handle_bluetooth_event(service_info, change) + discovery_info_bleak = cast(bluetooth.BluetoothServiceInfoBleak, service_info) + if adv := parse_advertisement_data( + discovery_info_bleak.device, discovery_info_bleak.advertisement + ): + self.data = flatten_sensors_data(adv.data) + if "modelName" in self.data: + self._ready_event.set() + _LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data) + self.device.update_from_advertisement(adv) + self.async_update_listeners() - switchbot_data = await self.switchbot_data.discover( - retry=self.retry_count, scan_timeout=self.scan_timeout - ) - - if not switchbot_data: - raise UpdateFailed("Unable to fetch switchbot services data") - - return { - identifier: flatten_sensors_data(sensor) - for identifier, sensor in switchbot_data.items() - } + async def async_wait_ready(self) -> bool: + """Wait for the device to be ready.""" + try: + await asyncio.wait_for(self._ready_event.wait(), timeout=55) + except asyncio.TimeoutError: + return False + return True diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index 9223217c173..dc9ddf4e616 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -14,13 +14,12 @@ from homeassistant.components.cover import ( CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD +from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from .const import CONF_RETRY_COUNT, DATA_COORDINATOR, DOMAIN +from .const import DOMAIN from .coordinator import SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity @@ -33,25 +32,17 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Switchbot curtain based on a config entry.""" - coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR - ] - - if not coordinator.data.get(entry.unique_id): - raise PlatformNotReady - + coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + unique_id = entry.unique_id + assert unique_id is not None async_add_entities( [ SwitchBotCurtainEntity( coordinator, - entry.unique_id, - entry.data[CONF_MAC], + unique_id, + entry.data[CONF_ADDRESS], entry.data[CONF_NAME], - coordinator.switchbot_api.SwitchbotCurtain( - mac=entry.data[CONF_MAC], - password=entry.data.get(CONF_PASSWORD), - retry_count=entry.options[CONF_RETRY_COUNT], - ), + coordinator.device, ) ] ) @@ -67,19 +58,18 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): | CoverEntityFeature.STOP | CoverEntityFeature.SET_POSITION ) - _attr_assumed_state = True def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, - idx: str | None, - mac: str, + unique_id: str, + address: str, name: str, device: SwitchbotCurtain, ) -> None: """Initialize the Switchbot.""" - super().__init__(coordinator, idx, mac, name) - self._attr_unique_id = idx + super().__init__(coordinator, unique_id, address, name) + self._attr_unique_id = unique_id self._attr_is_closed = None self._device = device @@ -97,21 +87,21 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): async def async_open_cover(self, **kwargs: Any) -> None: """Open the curtain.""" - _LOGGER.debug("Switchbot to open curtain %s", self._mac) + _LOGGER.debug("Switchbot to open curtain %s", self._address) self._last_run_success = bool(await self._device.open()) self.async_write_ha_state() async def async_close_cover(self, **kwargs: Any) -> None: """Close the curtain.""" - _LOGGER.debug("Switchbot to close the curtain %s", self._mac) + _LOGGER.debug("Switchbot to close the curtain %s", self._address) self._last_run_success = bool(await self._device.close()) self.async_write_ha_state() async def async_stop_cover(self, **kwargs: Any) -> None: """Stop the moving of this device.""" - _LOGGER.debug("Switchbot to stop %s", self._mac) + _LOGGER.debug("Switchbot to stop %s", self._address) self._last_run_success = bool(await self._device.stop()) self.async_write_ha_state() @@ -119,7 +109,7 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): """Move the cover shutter to a specific position.""" position = kwargs.get(ATTR_POSITION) - _LOGGER.debug("Switchbot to move at %d %s", position, self._mac) + _LOGGER.debug("Switchbot to move at %d %s", position, self._address) self._last_run_success = bool(await self._device.set_position(position)) self.async_write_ha_state() @@ -128,4 +118,5 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): """Handle updated data from the coordinator.""" self._attr_current_cover_position = self.data["data"]["position"] self._attr_is_closed = self.data["data"]["position"] <= 20 + self._attr_is_opening = self.data["data"]["inMotion"] self.async_write_ha_state() diff --git a/homeassistant/components/switchbot/entity.py b/homeassistant/components/switchbot/entity.py index c27c40613c7..4e69da4ec11 100644 --- a/homeassistant/components/switchbot/entity.py +++ b/homeassistant/components/switchbot/entity.py @@ -4,43 +4,58 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any +from homeassistant.components.bluetooth.passive_update_coordinator import ( + PassiveBluetoothCoordinatorEntity, +) +from homeassistant.const import ATTR_CONNECTIONS from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.entity import DeviceInfo, Entity -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.entity import DeviceInfo from .const import MANUFACTURER from .coordinator import SwitchbotDataUpdateCoordinator -class SwitchbotEntity(CoordinatorEntity[SwitchbotDataUpdateCoordinator], Entity): +class SwitchbotEntity(PassiveBluetoothCoordinatorEntity): """Generic entity encapsulating common features of Switchbot device.""" + coordinator: SwitchbotDataUpdateCoordinator + def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, - idx: str | None, - mac: str, + unique_id: str, + address: str, name: str, ) -> None: """Initialize the entity.""" super().__init__(coordinator) self._last_run_success: bool | None = None - self._idx = idx - self._mac = mac + self._unique_id = unique_id + self._address = address self._attr_name = name self._attr_device_info = DeviceInfo( - connections={(dr.CONNECTION_NETWORK_MAC, self._mac)}, + connections={(dr.CONNECTION_BLUETOOTH, self._address)}, manufacturer=MANUFACTURER, model=self.data["modelName"], name=name, ) + if ":" not in self._address: + # MacOS Bluetooth addresses are not mac addresses + return + # If the bluetooth address is also a mac address, + # add this connection as well to prevent a new device + # entry from being created when upgrading from a previous + # version of the integration. + self._attr_device_info[ATTR_CONNECTIONS].add( + (dr.CONNECTION_NETWORK_MAC, self._address) + ) @property def data(self) -> dict[str, Any]: """Return coordinator data for this entity.""" - return self.coordinator.data[self._idx] + return self.coordinator.data @property def extra_state_attributes(self) -> Mapping[Any, Any]: """Return the state attributes.""" - return {"last_run_success": self._last_run_success, "mac_address": self._mac} + return {"last_run_success": self._last_run_success} diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 76b43628ab3..c23891083a4 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,10 +2,11 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.14.1"], + "requirements": ["PySwitchbot==0.15.0"], "config_flow": true, + "dependencies": ["bluetooth"], "codeowners": ["@danielhiversen", "@RenierM26", "@murtas"], "bluetooth": [{ "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" }], - "iot_class": "local_polling", + "iot_class": "local_push", "loggers": ["switchbot"] } diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index 0bc7fe8a3b2..25863a57df5 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -8,18 +8,17 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_MAC, + CONF_ADDRESS, CONF_NAME, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DATA_COORDINATOR, DOMAIN +from .const import DOMAIN from .coordinator import SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity @@ -61,23 +60,19 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Switchbot sensor based on a config entry.""" - coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR - ] - - if not coordinator.data.get(entry.unique_id): - raise PlatformNotReady - + coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + unique_id = entry.unique_id + assert unique_id is not None async_add_entities( [ SwitchBotSensor( coordinator, - entry.unique_id, + unique_id, sensor, - entry.data[CONF_MAC], + entry.data[CONF_ADDRESS], entry.data[CONF_NAME], ) - for sensor in coordinator.data[entry.unique_id]["data"] + for sensor in coordinator.data["data"] if sensor in SENSOR_TYPES ] ) @@ -89,15 +84,15 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity): def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, - idx: str | None, + unique_id: str, sensor: str, - mac: str, + address: str, switchbot_name: str, ) -> None: """Initialize the Switchbot sensor.""" - super().__init__(coordinator, idx, mac, name=switchbot_name) + super().__init__(coordinator, unique_id, address, name=switchbot_name) self._sensor = sensor - self._attr_unique_id = f"{idx}-{sensor}" + self._attr_unique_id = f"{unique_id}-{sensor}" self._attr_name = f"{switchbot_name} {sensor.title()}" self.entity_description = SENSOR_TYPES[sensor] diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index 8c308083982..f0758d767aa 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -1,11 +1,11 @@ { "config": { - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "title": "Setup Switchbot device", "data": { - "mac": "Device MAC address", + "address": "Device address", "name": "[%key:common::config_flow::data::name%]", "password": "[%key:common::config_flow::data::password%]" } @@ -24,10 +24,8 @@ "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" + "retry_timeout": "Timeout between retries" } } } diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 404a92eda82..51f15c488d1 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -8,13 +8,12 @@ from switchbot import Switchbot from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, STATE_ON +from homeassistant.const import CONF_ADDRESS, CONF_NAME, STATE_ON from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import entity_platform from homeassistant.helpers.restore_state import RestoreEntity -from .const import CONF_RETRY_COUNT, DATA_COORDINATOR, DOMAIN +from .const import DOMAIN from .coordinator import SwitchbotDataUpdateCoordinator from .entity import SwitchbotEntity @@ -29,25 +28,17 @@ async def async_setup_entry( async_add_entities: entity_platform.AddEntitiesCallback, ) -> None: """Set up Switchbot based on a config entry.""" - coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ - DATA_COORDINATOR - ] - - if not coordinator.data.get(entry.unique_id): - raise PlatformNotReady - + coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + unique_id = entry.unique_id + assert unique_id is not None async_add_entities( [ SwitchBotBotEntity( coordinator, - entry.unique_id, - entry.data[CONF_MAC], + unique_id, + entry.data[CONF_ADDRESS], entry.data[CONF_NAME], - coordinator.switchbot_api.Switchbot( - mac=entry.data[CONF_MAC], - password=entry.data.get(CONF_PASSWORD), - retry_count=entry.options[CONF_RETRY_COUNT], - ), + coordinator.device, ) ] ) @@ -61,14 +52,14 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, - idx: str | None, - mac: str, + unique_id: str, + address: str, name: str, device: Switchbot, ) -> None: """Initialize the Switchbot.""" - super().__init__(coordinator, idx, mac, name) - self._attr_unique_id = idx + super().__init__(coordinator, unique_id, address, name) + self._attr_unique_id = unique_id self._device = device self._attr_is_on = False @@ -82,7 +73,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn device on.""" - _LOGGER.info("Turn Switchbot bot on %s", self._mac) + _LOGGER.info("Turn Switchbot bot on %s", self._address) self._last_run_success = bool(await self._device.turn_on()) if self._last_run_success: @@ -91,7 +82,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn device off.""" - _LOGGER.info("Turn Switchbot bot off %s", self._mac) + _LOGGER.info("Turn Switchbot bot off %s", self._address) self._last_run_success = bool(await self._device.turn_off()) if self._last_run_success: diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index 1cfaee8750f..15127b82101 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -7,11 +7,12 @@ "switchbot_unsupported_type": "Unsupported Switchbot Type.", "unknown": "Unexpected error" }, - "flow_title": "{name}", + "error": {}, + "flow_title": "{name} ({address})", "step": { "user": { "data": { - "mac": "Device MAC address", + "address": "Device address", "name": "Name", "password": "Password" }, @@ -24,9 +25,7 @@ "init": { "data": { "retry_count": "Retry count", - "retry_timeout": "Timeout between retries", - "scan_timeout": "How long to scan for advertisement data", - "update_time": "Time between updates (seconds)" + "retry_timeout": "Timeout between retries" } } } diff --git a/requirements_all.txt b/requirements_all.txt index 3186aee8707..e9fc510330e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.14.1 +PySwitchbot==0.15.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b8b42ec87e8..235d69e091c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.14.1 +PySwitchbot==0.15.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index 8f1501bfa9a..b4b2e56b39c 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -1,7 +1,11 @@ """Tests for the switchbot integration.""" from unittest.mock import patch -from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -11,37 +15,37 @@ DOMAIN = "switchbot" ENTRY_CONFIG = { CONF_NAME: "test-name", CONF_PASSWORD: "test-password", - CONF_MAC: "e7:89:43:99:99:99", + CONF_ADDRESS: "e7:89:43:99:99:99", } USER_INPUT = { CONF_NAME: "test-name", CONF_PASSWORD: "test-password", - CONF_MAC: "e7:89:43:99:99:99", + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", } USER_INPUT_CURTAIN = { CONF_NAME: "test-name", CONF_PASSWORD: "test-password", - CONF_MAC: "e7:89:43:90:90:90", + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", } USER_INPUT_SENSOR = { CONF_NAME: "test-name", CONF_PASSWORD: "test-password", - CONF_MAC: "c0:ce:b0:d4:26:be", + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", } USER_INPUT_UNSUPPORTED_DEVICE = { CONF_NAME: "test-name", CONF_PASSWORD: "test-password", - CONF_MAC: "test", + CONF_ADDRESS: "test", } USER_INPUT_INVALID = { CONF_NAME: "test-name", CONF_PASSWORD: "test-password", - CONF_MAC: "invalid-mac", + CONF_ADDRESS: "invalid-mac", } @@ -68,3 +72,67 @@ async def init_integration( await hass.async_block_till_done() return entry + + +WOHAND_SERVICE_INFO = BluetoothServiceInfoBleak( + name="WoHand", + manufacturer_data={89: b"\xfd`0U\x92W"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + source="local", + advertisement=AdvertisementData( + local_name="WoHand", + manufacturer_data={89: b"\xfd`0U\x92W"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoHand"), +) +WOCURTAIN_SERVICE_INFO = BluetoothServiceInfoBleak( + name="WoCurtain", + address="aa:bb:cc:dd:ee:ff", + manufacturer_data={89: b"\xc1\xc7'}U\xab"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"c\xd0Y\x00\x11\x04"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + rssi=-60, + source="local", + advertisement=AdvertisementData( + local_name="WoCurtain", + manufacturer_data={89: b"\xc1\xc7'}U\xab"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"c\xd0Y\x00\x11\x04"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoCurtain"), +) + +WOSENSORTH_SERVICE_INFO = BluetoothServiceInfoBleak( + name="WoSensorTH", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="aa:bb:cc:dd:ee:ff", + manufacturer_data={2409: b"\xda,\x1e\xb1\x86Au\x03\x00\x96\xac"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"T\x00d\x00\x96\xac"}, + rssi=-60, + source="local", + advertisement=AdvertisementData( + manufacturer_data={2409: b"\xda,\x1e\xb1\x86Au\x03\x00\x96\xac"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"T\x00d\x00\x96\xac"}, + ), + device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoSensorTH"), +) + +NOT_SWITCHBOT_INFO = BluetoothServiceInfoBleak( + name="unknown", + service_uuids=[], + address="aa:bb:cc:dd:ee:ff", + manufacturer_data={}, + service_data={}, + rssi=-60, + source="local", + advertisement=AdvertisementData( + manufacturer_data={}, + service_data={}, + ), + device=BLEDevice("aa:bb:cc:dd:ee:ff", "unknown"), +) diff --git a/tests/components/switchbot/conftest.py b/tests/components/switchbot/conftest.py index 2e6421f22a4..3df082c4361 100644 --- a/tests/components/switchbot/conftest.py +++ b/tests/components/switchbot/conftest.py @@ -1,139 +1,8 @@ """Define fixtures available for all tests.""" -import sys -from unittest.mock import MagicMock, patch -from pytest import fixture +import pytest -class MocGetSwitchbotDevices: - """Scan for all Switchbot devices and return by type.""" - - def __init__(self, interface=None) -> None: - """Get switchbot devices class constructor.""" - self._interface = interface - self._all_services_data = { - "e78943999999": { - "mac_address": "e7:89:43:99:99:99", - "isEncrypted": False, - "model": "H", - "data": { - "switchMode": "true", - "isOn": "true", - "battery": 91, - "rssi": -71, - }, - "modelName": "WoHand", - }, - "e78943909090": { - "mac_address": "e7:89:43:90:90:90", - "isEncrypted": False, - "model": "c", - "data": { - "calibration": True, - "battery": 74, - "inMotion": False, - "position": 100, - "lightLevel": 2, - "deviceChain": 1, - "rssi": -73, - }, - "modelName": "WoCurtain", - }, - "ffffff19ffff": { - "mac_address": "ff:ff:ff:19:ff:ff", - "isEncrypted": False, - "model": "m", - "rawAdvData": "000d6d00", - }, - "c0ceb0d426be": { - "mac_address": "c0:ce:b0:d4:26:be", - "isEncrypted": False, - "data": { - "temp": {"c": 21.6, "f": 70.88}, - "fahrenheit": False, - "humidity": 73, - "battery": 100, - "rssi": -58, - }, - "model": "T", - "modelName": "WoSensorTH", - }, - } - self._curtain_all_services_data = { - "mac_address": "e7:89:43:90:90:90", - "isEncrypted": False, - "model": "c", - "data": { - "calibration": True, - "battery": 74, - "position": 100, - "lightLevel": 2, - "rssi": -73, - }, - "modelName": "WoCurtain", - } - self._sensor_data = { - "mac_address": "c0:ce:b0:d4:26:be", - "isEncrypted": False, - "data": { - "temp": {"c": 21.6, "f": 70.88}, - "fahrenheit": False, - "humidity": 73, - "battery": 100, - "rssi": -58, - }, - "model": "T", - "modelName": "WoSensorTH", - } - self._unsupported_device = { - "mac_address": "test", - "isEncrypted": False, - "model": "HoN", - "data": { - "switchMode": "true", - "isOn": "true", - "battery": 91, - "rssi": -71, - }, - "modelName": "WoOther", - } - - async def discover(self, retry=0, scan_timeout=0): - """Mock discover.""" - return self._all_services_data - - async def get_device_data(self, mac=None): - """Return data for specific device.""" - if mac == "e7:89:43:99:99:99": - return self._all_services_data - if mac == "test": - return self._unsupported_device - if mac == "e7:89:43:90:90:90": - return self._curtain_all_services_data - if mac == "c0:ce:b0:d4:26:be": - return self._sensor_data - - return None - - -class MocNotConnectedError(Exception): - """Mock exception.""" - - -module = type(sys)("switchbot") -module.GetSwitchbotDevices = MocGetSwitchbotDevices -module.NotConnectedError = MocNotConnectedError -sys.modules["switchbot"] = module - - -@fixture -def switchbot_config_flow(hass): - """Mock the bluepy api for easier config flow testing.""" - with patch.object(MocGetSwitchbotDevices, "discover", return_value=True), patch( - "homeassistant.components.switchbot.config_flow.GetSwitchbotDevices" - ) as mock_switchbot: - instance = mock_switchbot.return_value - - instance.discover = MagicMock(return_value=True) - - yield mock_switchbot +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index aa1adb3a16e..b9d1d556b09 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -1,33 +1,104 @@ """Test the switchbot config flow.""" -from homeassistant.components.switchbot.config_flow import NotConnectedError +from unittest.mock import patch + from homeassistant.components.switchbot.const import ( CONF_RETRY_COUNT, CONF_RETRY_TIMEOUT, - CONF_SCAN_TIMEOUT, - CONF_TIME_BETWEEN_UPDATE_COMMAND, ) -from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE +from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE from homeassistant.data_entry_flow import FlowResultType from . import ( + NOT_SWITCHBOT_INFO, USER_INPUT, USER_INPUT_CURTAIN, USER_INPUT_SENSOR, + WOCURTAIN_SERVICE_INFO, + WOHAND_SERVICE_INFO, + WOSENSORTH_SERVICE_INFO, init_integration, patch_async_setup_entry, ) +from tests.common import MockConfigEntry + DOMAIN = "switchbot" -async def test_user_form_valid_mac(hass): +async def test_bluetooth_discovery(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WOHAND_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + with patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + USER_INPUT, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "test-name" + assert result["data"] == { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_SENSOR_TYPE: "bot", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_bluetooth_discovery_already_setup(hass): + """Test discovery via bluetooth with a valid device when already setup.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_SENSOR_TYPE: "bot", + }, + unique_id="aabbccddeeff", + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WOHAND_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_not_switchbot(hass): + """Test discovery via bluetooth not switchbot.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=NOT_SWITCHBOT_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_user_setup_wohand(hass): """Test the user initiated form with password and valid mac.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOHAND_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -42,7 +113,7 @@ async def test_user_form_valid_mac(hass): assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-name" assert result["data"] == { - CONF_MAC: "e7:89:43:99:99:99", + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_NAME: "test-name", CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "bot", @@ -50,11 +121,41 @@ async def test_user_form_valid_mac(hass): assert len(mock_setup_entry.mock_calls) == 1 - # test curtain device creation. - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} +async def test_user_setup_wohand_already_configured(hass): + """Test the user initiated form with password and valid mac.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_SENSOR_TYPE: "bot", + }, + unique_id="aabbccddeeff", ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOHAND_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_unconfigured_devices" + + +async def test_user_setup_wocurtain(hass): + """Test the user initiated form with password and valid mac.""" + + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOCURTAIN_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -69,7 +170,7 @@ async def test_user_form_valid_mac(hass): assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-name" assert result["data"] == { - CONF_MAC: "e7:89:43:90:90:90", + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_NAME: "test-name", CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "curtain", @@ -77,11 +178,16 @@ async def test_user_form_valid_mac(hass): assert len(mock_setup_entry.mock_calls) == 1 - # test sensor device creation. - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) +async def test_user_setup_wosensor(hass): + """Test the user initiated form with password and valid mac.""" + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOSENSORTH_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} @@ -96,7 +202,7 @@ async def test_user_form_valid_mac(hass): assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "test-name" assert result["data"] == { - CONF_MAC: "c0:ce:b0:d4:26:be", + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", CONF_NAME: "test-name", CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "hygrometer", @@ -104,39 +210,78 @@ async def test_user_form_valid_mac(hass): assert len(mock_setup_entry.mock_calls) == 1 - # tests abort if no unconfigured devices are found. - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) +async def test_user_no_devices(hass): + """Test the user initiated form with password and valid mac.""" + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "no_unconfigured_devices" -async def test_user_form_exception(hass, switchbot_config_flow): - """Test we handle exception on user form.""" - - switchbot_config_flow.side_effect = NotConnectedError - +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WOCURTAIN_SERVICE_INFO, ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "cannot_connect" + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOCURTAIN_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM - switchbot_config_flow.side_effect = Exception + with patch_async_setup_entry() as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=USER_INPUT, + ) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "test-name" + assert result2["data"] == { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_SENSOR_TYPE: "curtain", + } - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "unknown" + assert len(mock_setup_entry.mock_calls) == 1 + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) async def test_options_flow(hass): """Test updating options.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "test-name", + CONF_PASSWORD: "test-password", + CONF_SENSOR_TYPE: "bot", + }, + options={ + CONF_RETRY_COUNT: 10, + CONF_RETRY_TIMEOUT: 10, + }, + unique_id="aabbccddeeff", + ) + entry.add_to_hass(hass) + with patch_async_setup_entry() as mock_setup_entry: entry = await init_integration(hass) @@ -148,21 +293,17 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - CONF_TIME_BETWEEN_UPDATE_COMMAND: 60, CONF_RETRY_COUNT: 3, CONF_RETRY_TIMEOUT: 5, - CONF_SCAN_TIMEOUT: 5, }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"][CONF_TIME_BETWEEN_UPDATE_COMMAND] == 60 assert result["data"][CONF_RETRY_COUNT] == 3 assert result["data"][CONF_RETRY_TIMEOUT] == 5 - assert result["data"][CONF_SCAN_TIMEOUT] == 5 - assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 2 # Test changing of entry options. @@ -177,18 +318,17 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - CONF_TIME_BETWEEN_UPDATE_COMMAND: 66, CONF_RETRY_COUNT: 6, CONF_RETRY_TIMEOUT: 6, - CONF_SCAN_TIMEOUT: 6, }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"][CONF_TIME_BETWEEN_UPDATE_COMMAND] == 66 assert result["data"][CONF_RETRY_COUNT] == 6 assert result["data"][CONF_RETRY_TIMEOUT] == 6 - assert result["data"][CONF_SCAN_TIMEOUT] == 6 assert len(mock_setup_entry.mock_calls) == 1 + + assert entry.options[CONF_RETRY_COUNT] == 6 + assert entry.options[CONF_RETRY_TIMEOUT] == 6 From 0df08b6b0ce6e8d853ba4981e8f1e1492227a198 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Jul 2022 13:12:16 -0500 Subject: [PATCH 2818/3516] Bump aiohomekit to 1.2.0 (#75686) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 47472ba66bf..51d753f77fc 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.1.10"], + "requirements": ["aiohomekit==1.2.0"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index e9fc510330e..011b57860ee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.10 +aiohomekit==1.2.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 235d69e091c..e2dae175598 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.1.10 +aiohomekit==1.2.0 # homeassistant.components.emulated_hue # homeassistant.components.http From 9fae638f6587087ea9ff708cc0d79d131b079302 Mon Sep 17 00:00:00 2001 From: Glenn Waters Date: Sun, 24 Jul 2022 14:40:42 -0400 Subject: [PATCH 2819/3516] Migrate ElkM1 to new entity naming style (#75023) --- homeassistant/components/elkm1/__init__.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 472d31ccb93..2ce0e726fc4 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -443,13 +443,14 @@ def create_elk_entities( class ElkEntity(Entity): """Base class for all Elk entities.""" + _attr_has_entity_name = True + def __init__(self, element: Element, elk: Elk, elk_data: dict[str, Any]) -> None: """Initialize the base of all Elk devices.""" self._elk = elk self._element = element self._mac = elk_data["mac"] self._prefix = elk_data["prefix"] - self._name_prefix = f"{self._prefix} " if self._prefix else "" self._temperature_unit: str = elk_data["config"]["temperature_unit"] # unique_id starts with elkm1_ iff there is no prefix # it starts with elkm1m_{prefix} iff there is a prefix @@ -464,11 +465,7 @@ class ElkEntity(Entity): else: uid_start = "elkm1" self._unique_id = f"{uid_start}_{self._element.default_name('_')}".lower() - - @property - def name(self) -> str: - """Name of the element.""" - return f"{self._name_prefix}{self._element.name}" + self._attr_name = element.name @property def unique_id(self) -> str: From f94a79b409917f5260317b7d498c0c2bcd0d3dcf Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 24 Jul 2022 20:59:25 +0200 Subject: [PATCH 2820/3516] Bump motionblinds to 0.6.11 (#75581) --- 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 bc09d3e9e38..3499932c1d8 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,7 +3,7 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.6.8"], + "requirements": ["motionblinds==0.6.11"], "dependencies": ["network"], "dhcp": [ { "registered_devices": true }, diff --git a/requirements_all.txt b/requirements_all.txt index 011b57860ee..d6a4201cfd5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1059,7 +1059,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.8 +motionblinds==0.6.11 # homeassistant.components.motioneye motioneye-client==0.3.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e2dae175598..ae40622f221 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -748,7 +748,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.8 +motionblinds==0.6.11 # homeassistant.components.motioneye motioneye-client==0.3.12 From e18819c678a79b3fc97ae18b6b57d1f5772b1f0f Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sun, 24 Jul 2022 20:00:56 +0100 Subject: [PATCH 2821/3516] Support for encrypted BLE MiBeacon devices (#75677) * Support for encrypted devices * Sensor should use bindkey if available * Error message if encryption fails * Let mypy know this is always set by now * Towards supporting encryption in step_user * Add tests for the 4 new happy paths * Add test coverage for failure cases * Add strings * Bump to 0.5.1. Legacy MiBeacon does not use an authentication token, so harder to detect incorrect key * Add _title() helper * Fix test after rebase * Update homeassistant/components/xiaomi_ble/strings.json Co-authored-by: Martin Hjelmare * Remove unused lines Co-authored-by: Martin Hjelmare --- .../components/xiaomi_ble/config_flow.py | 141 +++++- .../components/xiaomi_ble/manifest.json | 2 +- homeassistant/components/xiaomi_ble/sensor.py | 5 +- .../components/xiaomi_ble/strings.json | 17 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/xiaomi_ble/__init__.py | 24 + .../components/xiaomi_ble/test_config_flow.py | 434 ++++++++++++++++++ tests/components/xiaomi_ble/test_sensor.py | 45 ++ 9 files changed, 649 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index 8f478442d6a..8b3ec22def7 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -1,10 +1,12 @@ """Config flow for Xiaomi Bluetooth integration.""" from __future__ import annotations +import dataclasses from typing import Any import voluptuous as vol from xiaomi_ble import XiaomiBluetoothDeviceData as DeviceData +from xiaomi_ble.parser import EncryptionScheme from homeassistant.components.bluetooth import ( BluetoothServiceInfo, @@ -17,6 +19,19 @@ from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN +@dataclasses.dataclass +class Discovery: + """A discovered bluetooth device.""" + + title: str + discovery_info: BluetoothServiceInfo + device: DeviceData + + +def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str: + return device.title or device.get_device_name() or discovery_info.name + + class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Xiaomi Bluetooth.""" @@ -26,7 +41,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): """Initialize the config flow.""" self._discovery_info: BluetoothServiceInfo | None = None self._discovered_device: DeviceData | None = None - self._discovered_devices: dict[str, str] = {} + self._discovered_devices: dict[str, Discovery] = {} async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfo @@ -39,25 +54,101 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_supported") self._discovery_info = discovery_info self._discovered_device = device + + title = _title(discovery_info, device) + self.context["title_placeholders"] = {"name": title} + + if device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: + return await self.async_step_get_encryption_key_legacy() + if device.encryption_scheme == EncryptionScheme.MIBEACON_4_5: + return await self.async_step_get_encryption_key_4_5() return await self.async_step_bluetooth_confirm() + async def async_step_get_encryption_key_legacy( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Enter a legacy bindkey for a v2/v3 MiBeacon device.""" + assert self._discovery_info + errors = {} + + if user_input is not None: + bindkey = user_input["bindkey"] + + if len(bindkey) != 24: + errors["bindkey"] = "expected_24_characters" + else: + device = DeviceData(bindkey=bytes.fromhex(bindkey)) + + # If we got this far we already know supported will + # return true so we don't bother checking that again + # We just want to retry the decryption + device.supported(self._discovery_info) + + if device.bindkey_verified: + return self.async_create_entry( + title=self.context["title_placeholders"]["name"], + data={"bindkey": bindkey}, + ) + + errors["bindkey"] = "decryption_failed" + + return self.async_show_form( + step_id="get_encryption_key_legacy", + description_placeholders=self.context["title_placeholders"], + data_schema=vol.Schema({vol.Required("bindkey"): vol.All(str, vol.Strip)}), + errors=errors, + ) + + async def async_step_get_encryption_key_4_5( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Enter a bindkey for a v4/v5 MiBeacon device.""" + assert self._discovery_info + + errors = {} + + if user_input is not None: + bindkey = user_input["bindkey"] + + if len(bindkey) != 32: + errors["bindkey"] = "expected_32_characters" + else: + device = DeviceData(bindkey=bytes.fromhex(bindkey)) + + # If we got this far we already know supported will + # return true so we don't bother checking that again + # We just want to retry the decryption + device.supported(self._discovery_info) + + if device.bindkey_verified: + return self.async_create_entry( + title=self.context["title_placeholders"]["name"], + data={"bindkey": bindkey}, + ) + + errors["bindkey"] = "decryption_failed" + + return self.async_show_form( + step_id="get_encryption_key_4_5", + description_placeholders=self.context["title_placeholders"], + data_schema=vol.Schema({vol.Required("bindkey"): vol.All(str, vol.Strip)}), + errors=errors, + ) + async def async_step_bluetooth_confirm( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm discovery.""" - assert self._discovered_device is not None - device = self._discovered_device - assert self._discovery_info is not None - discovery_info = self._discovery_info - title = device.title or device.get_device_name() or discovery_info.name if user_input is not None: - return self.async_create_entry(title=title, data={}) + return self.async_create_entry( + title=self.context["title_placeholders"]["name"], + data={}, + ) self._set_confirm_only() - placeholders = {"name": title} - self.context["title_placeholders"] = placeholders return self.async_show_form( - step_id="bluetooth_confirm", description_placeholders=placeholders + step_id="bluetooth_confirm", + description_placeholders=self.context["title_placeholders"], ) async def async_step_user( @@ -67,9 +158,19 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: address = user_input[CONF_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) - return self.async_create_entry( - title=self._discovered_devices[address], data={} - ) + discovery = self._discovered_devices[address] + + if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: + self._discovery_info = discovery.discovery_info + self.context["title_placeholders"] = {"name": discovery.title} + return await self.async_step_get_encryption_key_legacy() + + if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_4_5: + self._discovery_info = discovery.discovery_info + self.context["title_placeholders"] = {"name": discovery.title} + return await self.async_step_get_encryption_key_4_5() + + return self.async_create_entry(title=discovery.title, data={}) current_addresses = self._async_current_ids() for discovery_info in async_discovered_service_info(self.hass): @@ -78,16 +179,20 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): continue device = DeviceData() if device.supported(discovery_info): - self._discovered_devices[address] = ( - device.title or device.get_device_name() or discovery_info.name + self._discovered_devices[address] = Discovery( + title=_title(discovery_info, device), + discovery_info=discovery_info, + device=device, ) if not self._discovered_devices: return self.async_abort(reason="no_devices_found") + titles = { + address: discovery.title + for (address, discovery) in self._discovered_devices.items() + } return self.async_show_form( step_id="user", - data_schema=vol.Schema( - {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} - ), + data_schema=vol.Schema({vol.Required(CONF_ADDRESS): vol.In(titles)}), ) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 17e22accd6d..457eac60407 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.2.0"], + "requirements": ["xiaomi-ble==0.5.1"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index 50cbb0f66cf..9cdf661f5ad 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -158,7 +158,10 @@ async def async_setup_entry( coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] - data = XiaomiBluetoothDeviceData() + kwargs = {} + if bindkey := entry.data.get("bindkey"): + kwargs["bindkey"] = bytes.fromhex(bindkey) + data = XiaomiBluetoothDeviceData(**kwargs) processor = PassiveBluetoothDataProcessor( lambda service_info: sensor_update_to_bluetooth_data_update( data.update(service_info) diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index 7111626cca1..e12a15a0671 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -10,12 +10,27 @@ }, "bluetooth_confirm": { "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + }, + "get_encryption_key_legacy": { + "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey.", + "data": { + "bindkey": "Bindkey" + } + }, + "get_encryption_key_4_5": { + "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 32 character hexadecimal bindkey.", + "data": { + "bindkey": "Bindkey" + } } }, "abort": { "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", + "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", + "expected_32_characters": "Expected a 32 character hexadecimal bindkey." } } } diff --git a/requirements_all.txt b/requirements_all.txt index d6a4201cfd5..b078bf9d4d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2477,7 +2477,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.2.0 +xiaomi-ble==0.5.1 # homeassistant.components.knx xknx==0.21.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ae40622f221..269a2323760 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1662,7 +1662,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.2.0 +xiaomi-ble==0.5.1 # homeassistant.components.knx xknx==0.21.5 diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py index 80ec2f19989..a6269a02d12 100644 --- a/tests/components/xiaomi_ble/__init__.py +++ b/tests/components/xiaomi_ble/__init__.py @@ -37,6 +37,30 @@ MMC_T201_1_SERVICE_INFO = BluetoothServiceInfo( source="local", ) +JTYJGD03MI_SERVICE_INFO = BluetoothServiceInfo( + name="JTYJGD03MI", + address="54:EF:44:E3:9C:BC", + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": b'XY\x97\td\xbc\x9c\xe3D\xefT" `\x88\xfd\x00\x00\x00\x00:\x14\x8f\xb3' + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", +) + +YLKG07YL_SERVICE_INFO = BluetoothServiceInfo( + name="YLKG07YL", + address="F8:24:41:C5:98:8B", + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": b"X0\xb6\x03\xd2\x8b\x98\xc5A$\xf8\xc3I\x14vu~\x00\x00\x00\x99", + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", +) + def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfo: """Make a dummy advertisement.""" diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index f99cbd21296..fc625bdf7ec 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -7,9 +7,11 @@ from homeassistant.components.xiaomi_ble.const import DOMAIN from homeassistant.data_entry_flow import FlowResultType from . import ( + JTYJGD03MI_SERVICE_INFO, LYWSDCGQ_SERVICE_INFO, MMC_T201_1_SERVICE_INFO, NOT_SENSOR_PUSH_SERVICE_INFO, + YLKG07YL_SERVICE_INFO, ) from tests.common import MockConfigEntry @@ -36,6 +38,187 @@ async def test_async_step_bluetooth_valid_device(hass): assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" +async def test_async_step_bluetooth_valid_device_legacy_encryption(hass): + """Test discovery via bluetooth with a valid device, with legacy encryption.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=YLKG07YL_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_legacy" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "YLKG07YL" + assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} + assert result2["result"].unique_id == "F8:24:41:C5:98:8B" + + +async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key(hass): + """Test discovery via bluetooth with a valid device, with legacy encryption and invalid key.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=YLKG07YL_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_legacy" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaa"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_legacy" + assert result2["errors"]["bindkey"] == "decryption_failed" + + # Test can finish flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "YLKG07YL" + assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} + assert result2["result"].unique_id == "F8:24:41:C5:98:8B" + + +async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key_length( + hass, +): + """Test discovery via bluetooth with a valid device, with legacy encryption and wrong key length.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=YLKG07YL_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_legacy" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaa"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_legacy" + assert result2["errors"]["bindkey"] == "expected_24_characters" + + # Test can finish flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "YLKG07YL" + assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} + assert result2["result"].unique_id == "F8:24:41:C5:98:8B" + + +async def test_async_step_bluetooth_valid_device_v4_encryption(hass): + """Test discovery via bluetooth with a valid device, with v4 encryption.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=JTYJGD03MI_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_4_5" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JTYJGD03MI" + assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} + assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" + + +async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key(hass): + """Test discovery via bluetooth with a valid device, with v4 encryption and wrong key.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=JTYJGD03MI_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_4_5" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_4_5" + assert result2["errors"]["bindkey"] == "decryption_failed" + + # Test can finish flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JTYJGD03MI" + assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} + assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" + + +async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key_length(hass): + """Test discovery via bluetooth with a valid device, with v4 encryption and wrong key length.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=JTYJGD03MI_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_4_5" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18fda143a58"}, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_4_5" + assert result2["errors"]["bindkey"] == "expected_32_characters" + + # Test can finish flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JTYJGD03MI" + assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} + assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" + + async def test_async_step_bluetooth_not_xiaomi(hass): """Test discovery via bluetooth not xiaomi.""" result = await hass.config_entries.flow.async_init( @@ -82,6 +265,257 @@ async def test_async_step_user_with_found_devices(hass): assert result2["result"].unique_id == "58:2D:34:35:93:21" +async def test_async_step_user_with_found_devices_v4_encryption(hass): + """Test setup from service info cache with devices found, with v4 encryption.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[JTYJGD03MI_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "54:EF:44:E3:9C:BC"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_4_5" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JTYJGD03MI" + assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} + assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" + + +async def test_async_step_user_with_found_devices_v4_encryption_wrong_key(hass): + """Test setup from service info cache with devices found, with v4 encryption and wrong key.""" + # Get a list of devices + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[JTYJGD03MI_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + # Pick a device + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "54:EF:44:E3:9C:BC"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_4_5" + + # Try an incorrect key + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_4_5" + assert result2["errors"]["bindkey"] == "decryption_failed" + + # Check can still finish flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JTYJGD03MI" + assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} + assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" + + +async def test_async_step_user_with_found_devices_v4_encryption_wrong_key_length(hass): + """Test setup from service info cache with devices found, with v4 encryption and wrong key length.""" + # Get a list of devices + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[JTYJGD03MI_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + # Select a single device + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "54:EF:44:E3:9C:BC"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_4_5" + + # Try an incorrect key + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef1dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_4_5" + assert result2["errors"]["bindkey"] == "expected_32_characters" + + # Check can still finish flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JTYJGD03MI" + assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} + assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" + + +async def test_async_step_user_with_found_devices_legacy_encryption(hass): + """Test setup from service info cache with devices found, with legacy encryption.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[YLKG07YL_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "F8:24:41:C5:98:8B"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_legacy" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "YLKG07YL" + assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} + assert result2["result"].unique_id == "F8:24:41:C5:98:8B" + + +async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key( + hass, +): + """Test setup from service info cache with devices found, with legacy encryption and wrong key.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[YLKG07YL_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "F8:24:41:C5:98:8B"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_legacy" + + # Enter an incorrect code + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "aaaaaaaaaaaaaaaaaaaaaaaa"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_legacy" + assert result2["errors"]["bindkey"] == "decryption_failed" + + # Check you can finish the flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "YLKG07YL" + assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} + assert result2["result"].unique_id == "F8:24:41:C5:98:8B" + + +async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key_length( + hass, +): + """Test setup from service info cache with devices found, with legacy encryption and wrong key length.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[YLKG07YL_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "F8:24:41:C5:98:8B"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_legacy" + + # Enter an incorrect code + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b85307518487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_legacy" + assert result2["errors"]["bindkey"] == "expected_24_characters" + + # Check you can finish the flow + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "YLKG07YL" + assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} + assert result2["result"].unique_id == "F8:24:41:C5:98:8B" + + async def test_async_step_user_with_found_devices_already_setup(hass): """Test setup from service info cache with devices found.""" entry = MockConfigEntry( diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 74a4fe65131..b95ea37311e 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -130,3 +130,48 @@ async def test_xiaomi_HHCCJCY01(hass): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_xiaomi_CGDK2(hass): + """This device has encrypion so we need to retrieve its bindkey from the configentry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="58:2D:34:12:20:89", + data={"bindkey": "a3bfe9853dd85a620debe3620caaa351"}, + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback( + make_advertisement( + "58:2D:34:12:20:89", + b"XXo\x06\x07\x89 \x124-X_\x17m\xd5O\x02\x00\x00/\xa4S\xfa", + ), + BluetoothChange.ADVERTISEMENT, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + temp_sensor = hass.states.get("sensor.test_device_temperature") + temp_sensor_attribtes = temp_sensor.attributes + assert temp_sensor.state == "22.6" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Temperature" + assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" + assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From c0e68520778e5a059311d0fff39a746cb1a8d648 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 24 Jul 2022 21:11:30 +0200 Subject: [PATCH 2822/3516] Update pip version range to 22.3 (#75572) --- .github/workflows/ci.yaml | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- tox.ini | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 29527383bab..24d37b94518 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -517,7 +517,7 @@ jobs: python -m venv venv . venv/bin/activate python --version - pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.2" setuptools wheel + pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<22.3" setuptools wheel pip install --cache-dir=$PIP_CACHE -r requirements_all.txt --use-deprecated=legacy-resolver pip install --cache-dir=$PIP_CACHE -r requirements_test.txt --use-deprecated=legacy-resolver pip install -e . diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ad640ff596b..568ba909ba5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -26,7 +26,7 @@ lru-dict==1.1.8 orjson==3.7.8 paho-mqtt==1.6.1 pillow==9.2.0 -pip>=21.0,<22.2 +pip>=21.0,<22.3 pyserial==3.5 python-slugify==4.0.1 pyudev==0.23.2 diff --git a/pyproject.toml b/pyproject.toml index eb1209234be..48d12964414 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ # PyJWT has loose dependency. We want the latest one. "cryptography==36.0.2", "orjson==3.7.8", - "pip>=21.0,<22.2", + "pip>=21.0,<22.3", "python-slugify==4.0.1", "pyyaml==6.0", "requests==2.28.1", diff --git a/requirements.txt b/requirements.txt index 4785d344e64..ce77253b752 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ lru-dict==1.1.8 PyJWT==2.4.0 cryptography==36.0.2 orjson==3.7.8 -pip>=21.0,<22.2 +pip>=21.0,<22.3 python-slugify==4.0.1 pyyaml==6.0 requests==2.28.1 diff --git a/tox.ini b/tox.ini index b39caacf471..b96ab648fa2 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ isolated_build = True [testenv] basepython = {env:PYTHON3_PATH:python3} # pip version duplicated in homeassistant/package_constraints.txt -pip_version = pip>=21.0,<22.2 +pip_version = pip>=21.0,<22.3 install_command = python -m pip install --use-deprecated legacy-resolver {opts} {packages} commands = {envpython} -X dev -m pytest --timeout=9 --durations=10 -n auto --dist=loadfile -qq -o console_output_style=count -p no:sugar {posargs} From 2d4bd4d7c155f579fc7e87b8906551d4e5b6725e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 24 Jul 2022 14:09:02 -0600 Subject: [PATCH 2823/3516] Revert SimpliSafe auth flow to the quasi-manual OAuth method from 2021.11.0 (#75641) * Revert "Migrate SimpliSafe to new web-based authentication (#57212)" This reverts commit bf7c99c1f8f33720149b58a0a3b1687189b29179. * Tests 100% * Version bump * Add manifest version for custom component testing * Remove manifest version * Code review * Fix tests --- .../components/simplisafe/__init__.py | 4 +- .../components/simplisafe/config_flow.py | 269 ++++--------- .../components/simplisafe/manifest.json | 2 +- .../components/simplisafe/strings.json | 26 +- .../simplisafe/translations/en.json | 26 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/simplisafe/conftest.py | 18 +- .../components/simplisafe/test_config_flow.py | 373 +++++------------- 9 files changed, 197 insertions(+), 525 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 716163c4957..c28a3740694 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -277,10 +277,8 @@ def _async_standardize_config_entry(hass: HomeAssistant, entry: ConfigEntry) -> """Bring a config entry up to current standards.""" if CONF_TOKEN not in entry.data: raise ConfigEntryAuthFailed( - "New SimpliSafe OAuth standard requires re-authentication" + "SimpliSafe OAuth standard requires re-authentication" ) - if CONF_USERNAME not in entry.data: - raise ConfigEntryAuthFailed("Need to re-auth with username/password") entry_updates = {} if not entry.unique_id: diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 0b95de2c186..0b92871ccb2 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -1,48 +1,52 @@ """Config flow to configure the SimpliSafe component.""" from __future__ import annotations -import asyncio from collections.abc import Mapping -from typing import Any +from typing import Any, NamedTuple -import async_timeout from simplipy import API -from simplipy.api import AuthStates -from simplipy.errors import InvalidCredentialsError, SimplipyError, Verify2FAPending +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_TOKEN, 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, config_validation as cv from .const import DOMAIN, LOGGER -DEFAULT_EMAIL_2FA_SLEEP = 3 -DEFAULT_EMAIL_2FA_TIMEOUT = 600 - -STEP_REAUTH_SCHEMA = vol.Schema( - { - vol.Required(CONF_PASSWORD): cv.string, - } -) - -STEP_SMS_2FA_SCHEMA = vol.Schema( - { - vol.Required(CONF_CODE): cv.string, - } -) +CONF_AUTH_CODE = "auth_code" STEP_USER_SCHEMA = vol.Schema( { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_AUTH_CODE): cv.string, } ) +class SimpliSafeOAuthValues(NamedTuple): + """Define a named tuple to handle SimpliSafe OAuth strings.""" + + auth_url: str + code_verifier: 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(auth_url, code_verifier) + + class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a SimpliSafe config flow.""" @@ -50,45 +54,8 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._email_2fa_task: asyncio.Task | None = None - self._password: str | None = None + self._oauth_values: SimpliSafeOAuthValues = async_get_simplisafe_oauth_values() self._reauth: bool = False - self._simplisafe: API | None = None - self._username: str | None = None - - async def _async_authenticate( - self, originating_step_id: str, originating_step_schema: vol.Schema - ) -> FlowResult: - """Attempt to authenticate to the SimpliSafe API.""" - assert self._password - assert self._username - - errors = {} - session = aiohttp_client.async_get_clientsession(self.hass) - - try: - self._simplisafe = await API.async_from_credentials( - self._username, self._password, session=session - ) - 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: - return self.async_show_form( - step_id=originating_step_id, - data_schema=originating_step_schema, - errors=errors, - description_placeholders={CONF_USERNAME: self._username}, - ) - - assert self._simplisafe - - if self._simplisafe.auth_state == AuthStates.PENDING_2FA_SMS: - return await self.async_step_sms_2fa() - return await self.async_step_email_2fa() @staticmethod @callback @@ -98,146 +65,66 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Define the config flow to handle options.""" return SimpliSafeOptionsFlowHandler(config_entry) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._reauth = True - - if CONF_USERNAME not in entry_data: - # Old versions of the config flow may not have the username by this point; - # in that case, we reauth them by making them go through the user flow: - return await self.async_step_user() - - self._username = entry_data[CONF_USERNAME] - return await self.async_step_reauth_confirm() - - async def _async_get_email_2fa(self) -> None: - """Define a task to wait for email-based 2FA.""" - assert self._simplisafe - - try: - async with async_timeout.timeout(DEFAULT_EMAIL_2FA_TIMEOUT): - while True: - try: - await self._simplisafe.async_verify_2fa_email() - except Verify2FAPending: - LOGGER.info("Email-based 2FA pending; trying again") - await asyncio.sleep(DEFAULT_EMAIL_2FA_SLEEP) - else: - break - finally: - self.hass.async_create_task( - self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) - ) - - async def async_step_email_2fa( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle email-based two-factor authentication.""" - if not self._email_2fa_task: - self._email_2fa_task = self.hass.async_create_task( - self._async_get_email_2fa() - ) - return self.async_show_progress( - step_id="email_2fa", progress_action="email_2fa" - ) - - try: - await self._email_2fa_task - except asyncio.TimeoutError: - return self.async_show_progress_done(next_step_id="email_2fa_error") - return self.async_show_progress_done(next_step_id="finish") - - async def async_step_email_2fa_error( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle an error during email-based two-factor authentication.""" - return self.async_abort(reason="email_2fa_timed_out") - - async def async_step_finish( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle the final step.""" - assert self._simplisafe - assert self._username - - data = { - CONF_USERNAME: self._username, - CONF_TOKEN: self._simplisafe.refresh_token, - } - - user_id = str(self._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 – - # only one can exist at a time, but the presence of either one is a - # candidate for re-auth: - if existing_entries := [ - entry - for entry in self.hass.config_entries.async_entries() - if entry.domain == DOMAIN - and entry.unique_id in (self._username, user_id) - ]: - existing_entry = existing_entries[0] - self.hass.config_entries.async_update_entry( - existing_entry, unique_id=user_id, title=self._username, data=data - ) - self.hass.async_create_task( - self.hass.config_entries.async_reload(existing_entry.entry_id) - ) - return self.async_abort(reason="reauth_successful") - - await self.async_set_unique_id(user_id) - self._abort_if_unique_id_configured() - return self.async_create_entry(title=self._username, data=data) - - 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_SCHEMA, - description_placeholders={CONF_USERNAME: self._username}, - ) - - self._password = user_input[CONF_PASSWORD] - return await self._async_authenticate("reauth_confirm", STEP_REAUTH_SCHEMA) - - async def async_step_sms_2fa( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle SMS-based two-factor authentication.""" - if not user_input: - return self.async_show_form( - step_id="sms_2fa", - data_schema=STEP_SMS_2FA_SCHEMA, - ) - - assert self._simplisafe - - try: - await self._simplisafe.async_verify_2fa_sms(user_input[CONF_CODE]) - except InvalidCredentialsError: - return self.async_show_form( - step_id="sms_2fa", - data_schema=STEP_SMS_2FA_SCHEMA, - errors={CONF_CODE: "invalid_auth"}, - ) - - return await self.async_step_finish() + 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 user_input is None: - return self.async_show_form(step_id="user", data_schema=STEP_USER_SCHEMA) + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_SCHEMA, + description_placeholders={CONF_URL: self._oauth_values.auth_url}, + ) - self._username = user_input[CONF_USERNAME] - self._password = user_input[CONF_PASSWORD] - return await self._async_authenticate("user", STEP_USER_SCHEMA) + errors = {} + session = aiohttp_client.async_get_clientsession(self.hass) + + try: + simplisafe = await API.async_from_auth( + user_input[CONF_AUTH_CODE], + self._oauth_values.code_verifier, + session=session, + ) + 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: + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_SCHEMA, + errors=errors, + description_placeholders={CONF_URL: self._oauth_values.auth_url}, + ) + + simplisafe_user_id = str(simplisafe.user_id) + data = {CONF_USERNAME: simplisafe_user_id, CONF_TOKEN: simplisafe.refresh_token} + + if self._reauth: + existing_entry = await self.async_set_unique_id(simplisafe_user_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=simplisafe_user_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") + + await self.async_set_unique_id(simplisafe_user_id) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=simplisafe_user_id, data=data) class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow): diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index b6a139fba80..b08799e4082 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==2022.07.0"], + "requirements": ["simplisafe-python==2022.07.1"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 85e579fd455..16ae7111abf 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -1,38 +1,22 @@ { "config": { "step": { - "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%]" - } - }, - "sms_2fa": { - "description": "Input the two-factor authentication code sent to you via SMS.", - "data": { - "code": "Code" - } - }, "user": { - "description": "Input your username and password.", + "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL.", "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "auth_code": "Authorization Code" } } }, "error": { + "identifier_exists": "Account already registered", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { "already_configured": "This SimpliSafe account is already in use.", - "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" - }, - "progress": { - "email_2fa": "Check your email for a verification link from Simplisafe." + "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 0da6f6442e4..82320df4864 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -2,36 +2,20 @@ "config": { "abort": { "already_configured": "This SimpliSafe account is already in use.", - "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", - "reauth_successful": "Re-authentication was successful" + "reauth_successful": "Re-authentication was successful", + "wrong_account": "The user credentials provided do not match this SimpliSafe account." }, "error": { + "identifier_exists": "Account already registered", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "progress": { - "email_2fa": "Check your email for a verification link from Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Password" - }, - "description": "Please re-enter the password for {username}.", - "title": "Reauthenticate Integration" - }, - "sms_2fa": { - "data": { - "code": "Code" - }, - "description": "Input the two-factor authentication code sent to you via SMS." - }, "user": { "data": { - "password": "Password", - "username": "Username" + "auth_code": "Authorization Code" }, - "description": "Input your username and password." + "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL." } } }, diff --git a/requirements_all.txt b/requirements_all.txt index b078bf9d4d1..fff6dd9a48f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2188,7 +2188,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.07.0 +simplisafe-python==2022.07.1 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 269a2323760..b485db528ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1466,7 +1466,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.07.0 +simplisafe-python==2022.07.1 # homeassistant.components.slack slackclient==2.5.0 diff --git a/tests/components/simplisafe/conftest.py b/tests/components/simplisafe/conftest.py index 82bd04a7349..54ab7fbe9d7 100644 --- a/tests/components/simplisafe/conftest.py +++ b/tests/components/simplisafe/conftest.py @@ -3,7 +3,6 @@ import json from unittest.mock import AsyncMock, Mock, patch import pytest -from simplipy.api import AuthStates from simplipy.system.v3 import SystemV3 from homeassistant.components.simplisafe.const import DOMAIN @@ -19,20 +18,11 @@ PASSWORD = "password" SYSTEM_ID = "system_123" -@pytest.fixture(name="api_auth_state") -def api_auth_state_fixture(): - """Define a SimpliSafe API auth state.""" - return AuthStates.PENDING_2FA_SMS - - @pytest.fixture(name="api") -def api_fixture(api_auth_state, data_subscription, system_v3, websocket): +def api_fixture(data_subscription, system_v3, websocket): """Define a simplisafe-python API object.""" return Mock( async_get_systems=AsyncMock(return_value={SYSTEM_ID: system_v3}), - async_verify_2fa_email=AsyncMock(), - async_verify_2fa_sms=AsyncMock(), - auth_state=api_auth_state, refresh_token=REFRESH_TOKEN, subscription_data=data_subscription, user_id=USER_ID, @@ -104,12 +94,10 @@ def reauth_config_fixture(): async def setup_simplisafe_fixture(hass, api, config): """Define a fixture to set up SimpliSafe.""" with patch( - "homeassistant.components.simplisafe.config_flow.DEFAULT_EMAIL_2FA_SLEEP", 0 - ), patch( - "homeassistant.components.simplisafe.config_flow.API.async_from_credentials", + "homeassistant.components.simplisafe.config_flow.API.async_from_auth", return_value=api, ), patch( - "homeassistant.components.simplisafe.API.async_from_credentials", + "homeassistant.components.simplisafe.API.async_from_auth", return_value=api, ), patch( "homeassistant.components.simplisafe.API.async_from_refresh_token", diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 9211ec8ea28..6e6f99ad4bb 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -2,317 +2,148 @@ from unittest.mock import patch import pytest -from simplipy.api import AuthStates -from simplipy.errors import InvalidCredentialsError, SimplipyError, Verify2FAPending +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.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_CODE, CONF_TOKEN, CONF_USERNAME -from .common import REFRESH_TOKEN, USER_ID, USERNAME -from tests.common import MockConfigEntry - -CONF_USER_ID = "user_id" - - -async def test_duplicate_error( - hass, config_entry, credentials_config, setup_simplisafe, sms_config -): +async def test_duplicate_error(config_entry, hass, setup_simplisafe): """Test that errors are shown when duplicates are added.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.FlowResultType.FORM + with patch( + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ): + 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=credentials_config - ) - assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=sms_config - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "already_configured" + 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_options_flow(hass, config_entry): +async def test_invalid_credentials(hass): + """Test that invalid credentials show the correct error.""" + with patch( + "homeassistant.components.simplisafe.config_flow.API.async_from_auth", + 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={CONF_AUTH_CODE: "code123"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_options_flow(config_entry, hass): """Test config flow options.""" with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM + + 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_CODE: "4321"} ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == {CONF_CODE: "4321"} -@pytest.mark.parametrize("unique_id", [USERNAME, USER_ID]) -async def test_step_reauth( - hass, config, config_entry, reauth_config, setup_simplisafe, sms_config, unique_id -): - """Test the re-auth step (testing both username and user ID as unique ID).""" - # Add a second config entry (tied to a random domain, but with the same unique ID - # that could exist in a SimpliSafe entry) to ensure that this reauth process only - # touches the SimpliSafe entry: - entry = MockConfigEntry(domain="random", unique_id=USERNAME, data={"some": "data"}) - entry.add_to_hass(hass) - +async def test_step_reauth(config_entry, hass, setup_simplisafe): + """Test the re-auth step.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=config - ) - assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=reauth_config - ) - assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=sms_config - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "reauth_successful" - - assert len(hass.config_entries.async_entries()) == 2 - - # Test that the SimpliSafe config flow is updated: - [config_entry] = hass.config_entries.async_entries(DOMAIN) - assert config_entry.unique_id == USER_ID - assert config_entry.data == config - - # Test that the non-SimpliSafe config flow remains the same: - [config_entry] = hass.config_entries.async_entries("random") - assert config_entry == entry - - -@pytest.mark.parametrize( - "exc,error_string", - [(InvalidCredentialsError, "invalid_auth"), (SimplipyError, "unknown")], -) -async def test_step_reauth_errors(hass, config, error_string, exc, reauth_config): - """Test that errors during the reauth step are handled.""" - with patch( - "homeassistant.components.simplisafe.API.async_from_credentials", - side_effect=exc, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=config - ) - assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=reauth_config - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - assert result["errors"] == {"base": error_string} - - -@pytest.mark.parametrize( - "config,unique_id", - [ - ( - { - CONF_TOKEN: REFRESH_TOKEN, - CONF_USER_ID: USER_ID, - }, - USERNAME, - ), - ( - { - CONF_TOKEN: REFRESH_TOKEN, - CONF_USER_ID: USER_ID, - }, - USER_ID, - ), - ], -) -async def test_step_reauth_from_scratch( - hass, config, config_entry, credentials_config, setup_simplisafe, sms_config -): - """Test the re-auth step when a complete redo is needed.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=config + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={CONF_USERNAME: "12345", CONF_TOKEN: "token123"}, ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.FlowResultType.FORM - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=credentials_config - ) - assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=sms_config - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "reauth_successful" + with patch( + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + ) + 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.unique_id == USER_ID - assert config_entry.data == { - CONF_TOKEN: REFRESH_TOKEN, - CONF_USERNAME: USERNAME, - } + assert config_entry.data == {CONF_USERNAME: "12345", CONF_TOKEN: "token123"} -@pytest.mark.parametrize( - "exc,error_string", - [(InvalidCredentialsError, "invalid_auth"), (SimplipyError, "unknown")], -) -async def test_step_user_errors(hass, credentials_config, error_string, exc): - """Test that errors during the user step are handled.""" +@pytest.mark.parametrize("unique_id", ["some_other_id"]) +async def test_step_reauth_wrong_account(config_entry, hass, setup_simplisafe): + """Test the re-auth step where the wrong account is used during login.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={CONF_USERNAME: "12345", CONF_TOKEN: "token123"}, + ) + assert result["step_id"] == "user" + with patch( - "homeassistant.components.simplisafe.API.async_from_credentials", - side_effect=exc, + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "wrong_account" + + +async def test_step_user(hass, setup_simplisafe): + """Test the user step.""" + 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.config_entries.ConfigEntries.async_reload"): + 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_CREATE_ENTRY + + assert len(hass.config_entries.async_entries()) == 1 + [config_entry] = hass.config_entries.async_entries(DOMAIN) + assert config_entry.data == {CONF_USERNAME: "12345", CONF_TOKEN: "token123"} + + +async def test_unknown_error(hass, setup_simplisafe): + """Test that an unknown error shows ohe correct error.""" + with patch( + "homeassistant.components.simplisafe.config_flow.API.async_from_auth", + 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.FlowResultType.FORM + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=credentials_config + result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": error_string} - - -@pytest.mark.parametrize("api_auth_state", [AuthStates.PENDING_2FA_EMAIL]) -async def test_step_user_email_2fa( - api, api_auth_state, hass, config, credentials_config, setup_simplisafe -): - """Test the user step with email-based 2FA.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - # Patch API.async_verify_2fa_email to first return pending, then return all done: - api.async_verify_2fa_email.side_effect = [Verify2FAPending, None] - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=credentials_config - ) - assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS_DONE - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - - assert len(hass.config_entries.async_entries()) == 1 - [config_entry] = hass.config_entries.async_entries(DOMAIN) - assert config_entry.unique_id == USER_ID - assert config_entry.data == config - - -@patch("homeassistant.components.simplisafe.config_flow.DEFAULT_EMAIL_2FA_TIMEOUT", 0) -@pytest.mark.parametrize("api_auth_state", [AuthStates.PENDING_2FA_EMAIL]) -async def test_step_user_email_2fa_timeout( - api, hass, config, credentials_config, setup_simplisafe -): - """Test a timeout during the user step with email-based 2FA.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - # Patch API.async_verify_2fa_email to return pending: - api.async_verify_2fa_email.side_effect = Verify2FAPending - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=credentials_config - ) - assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.SHOW_PROGRESS_DONE - assert result["step_id"] == "email_2fa_error" - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "email_2fa_timed_out" - - -async def test_step_user_sms_2fa( - hass, config, credentials_config, setup_simplisafe, sms_config -): - """Test the user step with SMS-based 2FA.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=credentials_config - ) - assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=sms_config - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - - assert len(hass.config_entries.async_entries()) == 1 - [config_entry] = hass.config_entries.async_entries(DOMAIN) - assert config_entry.unique_id == USER_ID - assert config_entry.data == config - - -@pytest.mark.parametrize( - "exc,error_string", [(InvalidCredentialsError, "invalid_auth")] -) -async def test_step_user_sms_2fa_errors( - api, - hass, - config, - credentials_config, - error_string, - exc, - setup_simplisafe, - sms_config, -): - """Test that errors during the SMS-based 2FA step are handled.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=credentials_config - ) - assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.FlowResultType.FORM - - # Simulate entering the incorrect SMS code: - api.async_verify_2fa_sms.side_effect = InvalidCredentialsError - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=sms_config - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {"code": error_string} + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} From c9ae409d9a5d15ba78d12af0d88a55b456e5b430 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 24 Jul 2022 22:38:09 +0200 Subject: [PATCH 2824/3516] Update sentry-sdk to 1.8.0 (#75691) --- 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 c1163ea1791..f6567fbd04d 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.7.2"], + "requirements": ["sentry-sdk==1.8.0"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index fff6dd9a48f..7194370ca6a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2170,7 +2170,7 @@ sense_energy==0.10.4 sensorpush-ble==1.4.2 # homeassistant.components.sentry -sentry-sdk==1.7.2 +sentry-sdk==1.8.0 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b485db528ef..0edf582d77d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1454,7 +1454,7 @@ sense_energy==0.10.4 sensorpush-ble==1.4.2 # homeassistant.components.sentry -sentry-sdk==1.7.2 +sentry-sdk==1.8.0 # homeassistant.components.sharkiq sharkiq==0.0.1 From d890598da7003fdc4ec0b33ebf761824fe3ee661 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Jul 2022 16:38:07 -0500 Subject: [PATCH 2825/3516] Update PySwitchbot to improve connection reliability (#75692) --- .../components/switchbot/__init__.py | 35 ++++--------------- .../components/switchbot/config_flow.py | 22 ++---------- homeassistant/components/switchbot/const.py | 6 +--- .../components/switchbot/coordinator.py | 10 ------ .../components/switchbot/manifest.json | 2 +- .../components/switchbot/strings.json | 3 +- .../components/switchbot/translations/en.json | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/switchbot/test_config_flow.py | 11 +----- 10 files changed, 16 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 2dbc66a864d..46d6755553a 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -1,9 +1,6 @@ """Support for Switchbot devices.""" -from collections.abc import Mapping import logging -from types import MappingProxyType -from typing import Any import switchbot @@ -24,11 +21,8 @@ from .const import ( ATTR_BOT, ATTR_CURTAIN, ATTR_HYGROMETER, - COMMON_OPTIONS, CONF_RETRY_COUNT, - CONF_RETRY_TIMEOUT, DEFAULT_RETRY_COUNT, - DEFAULT_RETRY_TIMEOUT, DOMAIN, ) from .coordinator import SwitchbotDataUpdateCoordinator @@ -49,8 +43,6 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Switchbot from a config entry.""" hass.data.setdefault(DOMAIN, {}) - domain_data = hass.data[DOMAIN] - if CONF_ADDRESS not in entry.data and CONF_MAC in entry.data: # Bleak uses addresses not mac addresses which are are actually # UUIDs on some platforms (MacOS). @@ -65,10 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not entry.options: hass.config_entries.async_update_entry( entry, - options={ - CONF_RETRY_COUNT: DEFAULT_RETRY_COUNT, - CONF_RETRY_TIMEOUT: DEFAULT_RETRY_TIMEOUT, - }, + options={CONF_RETRY_COUNT: DEFAULT_RETRY_COUNT}, ) sensor_type: str = entry.data[CONF_SENSOR_TYPE] @@ -78,13 +67,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady( f"Could not find Switchbot {sensor_type} with address {address}" ) - - if COMMON_OPTIONS not in domain_data: - domain_data[COMMON_OPTIONS] = entry.options - - common_options: Mapping[str, int] = domain_data[COMMON_OPTIONS] - switchbot.DEFAULT_RETRY_TIMEOUT = common_options[CONF_RETRY_TIMEOUT] - cls = CLASS_BY_DEVICE.get(sensor_type, switchbot.SwitchbotDevice) device = cls( device=ble_device, @@ -92,7 +74,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: retry_count=entry.options[CONF_RETRY_COUNT], ) coordinator = hass.data[DOMAIN][entry.entry_id] = SwitchbotDataUpdateCoordinator( - hass, _LOGGER, ble_device, device, common_options + hass, _LOGGER, ble_device, device ) entry.async_on_unload(coordinator.async_start()) if not await coordinator.async_wait_ready(): @@ -106,6 +88,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" sensor_type = entry.data[CONF_SENSOR_TYPE] @@ -119,11 +106,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.pop(DOMAIN) return unload_ok - - -async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Handle options update.""" - # Update entity options stored in hass. - common_options: MappingProxyType[str, Any] = hass.data[DOMAIN][COMMON_OPTIONS] - if entry.options != common_options: - await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index f6f175819b6..3a34a89d9fd 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -17,14 +17,7 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo -from .const import ( - CONF_RETRY_COUNT, - CONF_RETRY_TIMEOUT, - DEFAULT_RETRY_COUNT, - DEFAULT_RETRY_TIMEOUT, - DOMAIN, - SUPPORTED_MODEL_TYPES, -) +from .const import CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, SUPPORTED_MODEL_TYPES _LOGGER = logging.getLogger(__name__) @@ -140,11 +133,6 @@ class SwitchbotOptionsFlowHandler(OptionsFlow): """Manage Switchbot options.""" if user_input is not None: # Update common entity options for all other entities. - for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.unique_id != self.config_entry.unique_id: - self.hass.config_entries.async_update_entry( - entry, options=user_input - ) return self.async_create_entry(title="", data=user_input) options = { @@ -153,13 +141,7 @@ class SwitchbotOptionsFlowHandler(OptionsFlow): default=self.config_entry.options.get( CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT ), - ): int, - vol.Optional( - CONF_RETRY_TIMEOUT, - default=self.config_entry.options.get( - CONF_RETRY_TIMEOUT, DEFAULT_RETRY_TIMEOUT - ), - ): int, + ): int } return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index a841b8388dd..e9602a19048 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -15,15 +15,11 @@ SUPPORTED_MODEL_TYPES = { # Config Defaults DEFAULT_RETRY_COUNT = 3 -DEFAULT_RETRY_TIMEOUT = 5 # Config Options CONF_RETRY_COUNT = "retry_count" -CONF_SCAN_TIMEOUT = "scan_timeout" # Deprecated config Entry Options to be removed in 2023.4 CONF_TIME_BETWEEN_UPDATE_COMMAND = "update_time" CONF_RETRY_TIMEOUT = "retry_timeout" - -# Data -COMMON_OPTIONS = "common_options" +CONF_SCAN_TIMEOUT = "scan_timeout" diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 74cc54402d1..31f7f2d3992 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from collections.abc import Mapping import logging from typing import Any, cast @@ -16,8 +15,6 @@ from homeassistant.components.bluetooth.passive_update_coordinator import ( ) from homeassistant.core import HomeAssistant, callback -from .const import CONF_RETRY_COUNT - _LOGGER = logging.getLogger(__name__) @@ -38,21 +35,14 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): logger: logging.Logger, ble_device: BLEDevice, device: switchbot.SwitchbotDevice, - common_options: Mapping[str, int], ) -> None: """Initialize global switchbot data updater.""" super().__init__(hass, logger, ble_device.address) self.ble_device = ble_device self.device = device - self.common_options = common_options self.data: dict[str, Any] = {} self._ready_event = asyncio.Event() - @property - def retry_count(self) -> int: - """Return retry count.""" - return self.common_options[CONF_RETRY_COUNT] - @callback def _async_handle_bluetooth_event( self, diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index c23891083a4..a82ef264698 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.15.0"], + "requirements": ["PySwitchbot==0.15.1"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": ["@danielhiversen", "@RenierM26", "@murtas"], diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index f0758d767aa..797d1d7613c 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -24,8 +24,7 @@ "step": { "init": { "data": { - "retry_count": "Retry count", - "retry_timeout": "Timeout between retries" + "retry_count": "Retry count" } } } diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index 15127b82101..0407f71ece3 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -24,8 +24,7 @@ "step": { "init": { "data": { - "retry_count": "Retry count", - "retry_timeout": "Timeout between retries" + "retry_count": "Retry count" } } } diff --git a/requirements_all.txt b/requirements_all.txt index 7194370ca6a..3a39b405c91 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.15.0 +PySwitchbot==0.15.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0edf582d77d..e9f699d61d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.15.0 +PySwitchbot==0.15.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index b9d1d556b09..0ae3430eeb1 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -2,10 +2,7 @@ from unittest.mock import patch -from homeassistant.components.switchbot.const import ( - CONF_RETRY_COUNT, - CONF_RETRY_TIMEOUT, -) +from homeassistant.components.switchbot.const import CONF_RETRY_COUNT from homeassistant.config_entries import SOURCE_BLUETOOTH, SOURCE_USER from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE from homeassistant.data_entry_flow import FlowResultType @@ -276,7 +273,6 @@ async def test_options_flow(hass): }, options={ CONF_RETRY_COUNT: 10, - CONF_RETRY_TIMEOUT: 10, }, unique_id="aabbccddeeff", ) @@ -294,14 +290,12 @@ async def test_options_flow(hass): result["flow_id"], user_input={ CONF_RETRY_COUNT: 3, - CONF_RETRY_TIMEOUT: 5, }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_RETRY_COUNT] == 3 - assert result["data"][CONF_RETRY_TIMEOUT] == 5 assert len(mock_setup_entry.mock_calls) == 2 @@ -319,16 +313,13 @@ async def test_options_flow(hass): result["flow_id"], user_input={ CONF_RETRY_COUNT: 6, - CONF_RETRY_TIMEOUT: 6, }, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"][CONF_RETRY_COUNT] == 6 - assert result["data"][CONF_RETRY_TIMEOUT] == 6 assert len(mock_setup_entry.mock_calls) == 1 assert entry.options[CONF_RETRY_COUNT] == 6 - assert entry.options[CONF_RETRY_TIMEOUT] == 6 From bbb9443b00693a0bad2e6cfbe789baa5dc5542da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Jul 2022 16:39:53 -0500 Subject: [PATCH 2826/3516] Fix bluetooth integration matching with service_data_uuids and service_uuids (#75687) * Fix bluetooth integration with service_data and service_uuids We would only dispatch a new flow when the address was seen for the first time or the manufacturer_data appeared in a followup advertisement. Its also possible for the service_data and service_uuids to appear in a followup advertisement so we need to track these as well * improve logging to avoid overly large messages * improve logging to avoid overly large messages * adjust * adjsut * split * coverage * coverage * coverage * coverage * fix matcher * more coverage * more coverage * more coverage * revert switchbot changes and move to seperate PR --- .../components/bluetooth/__init__.py | 117 ++------- homeassistant/components/bluetooth/match.py | 139 ++++++++++ homeassistant/loader.py | 1 + script/hassfest/manifest.py | 1 + tests/components/bluetooth/test_init.py | 243 +++++++++++++++++- 5 files changed, 398 insertions(+), 103 deletions(-) create mode 100644 homeassistant/components/bluetooth/match.py diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 901b8ee0644..fb514a33e99 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -5,15 +5,13 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum -import fnmatch import logging import platform -from typing import Final, TypedDict, Union +from typing import Final, Union from bleak import BleakError from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData -from lru import LRU # pylint: disable=no-name-in-module from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -28,20 +26,21 @@ from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.helpers.typing import ConfigType -from homeassistant.loader import ( - BluetoothMatcher, - BluetoothMatcherOptional, - async_get_bluetooth, -) +from homeassistant.loader import async_get_bluetooth from . import models from .const import DOMAIN +from .match import ( + ADDRESS, + BluetoothCallbackMatcher, + IntegrationMatcher, + ble_device_matches, +) from .models import HaBleakScanner, HaBleakScannerWrapper from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher _LOGGER = logging.getLogger(__name__) -MAX_REMEMBER_ADDRESSES: Final = 2048 UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 @@ -79,19 +78,6 @@ class BluetoothServiceInfoBleak(BluetoothServiceInfo): ) -class BluetoothCallbackMatcherOptional(TypedDict, total=False): - """Matcher for the bluetooth integration for callback optional fields.""" - - address: str - - -class BluetoothCallbackMatcher( - BluetoothMatcherOptional, - BluetoothCallbackMatcherOptional, -): - """Callback matcher for the bluetooth integration.""" - - class BluetoothScanningMode(Enum): """The mode of scanning for bluetooth devices.""" @@ -104,12 +90,6 @@ SCANNING_MODE_TO_BLEAK = { BluetoothScanningMode.PASSIVE: "passive", } -ADDRESS: Final = "address" -LOCAL_NAME: Final = "local_name" -SERVICE_UUID: Final = "service_uuid" -MANUFACTURER_ID: Final = "manufacturer_id" -MANUFACTURER_DATA_START: Final = "manufacturer_data_start" - BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") BluetoothCallback = Callable[ @@ -208,8 +188,8 @@ async def _async_has_bluetooth_adapter() -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the bluetooth integration.""" - integration_matchers = await async_get_bluetooth(hass) - manager = BluetoothManager(hass, integration_matchers) + integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass)) + manager = BluetoothManager(hass, integration_matcher) manager.async_setup() hass.data[DOMAIN] = manager # The config entry is responsible for starting the manager @@ -252,62 +232,17 @@ async def async_unload_entry( return True -def _ble_device_matches( - matcher: BluetoothCallbackMatcher | BluetoothMatcher, - device: BLEDevice, - advertisement_data: AdvertisementData, -) -> bool: - """Check if a ble device and advertisement_data matches the matcher.""" - if ( - matcher_address := matcher.get(ADDRESS) - ) is not None and device.address != matcher_address: - return False - - if ( - matcher_local_name := matcher.get(LOCAL_NAME) - ) is not None and not fnmatch.fnmatch( - advertisement_data.local_name or device.name or device.address, - matcher_local_name, - ): - return False - - if ( - matcher_service_uuid := matcher.get(SERVICE_UUID) - ) is not None and matcher_service_uuid not in advertisement_data.service_uuids: - return False - - if ( - (matcher_manfacturer_id := matcher.get(MANUFACTURER_ID)) is not None - and matcher_manfacturer_id not in advertisement_data.manufacturer_data - ): - return False - - if ( - matcher_manufacturer_data_start := matcher.get(MANUFACTURER_DATA_START) - ) is not None: - matcher_manufacturer_data_start_bytes = bytearray( - matcher_manufacturer_data_start - ) - if not any( - manufacturer_data.startswith(matcher_manufacturer_data_start_bytes) - for manufacturer_data in advertisement_data.manufacturer_data.values() - ): - return False - - return True - - class BluetoothManager: """Manage Bluetooth.""" def __init__( self, hass: HomeAssistant, - integration_matchers: list[BluetoothMatcher], + integration_matcher: IntegrationMatcher, ) -> None: """Init bluetooth discovery.""" self.hass = hass - self._integration_matchers = integration_matchers + self._integration_matcher = integration_matcher self.scanner: HaBleakScanner | None = None self._cancel_device_detected: CALLBACK_TYPE | None = None self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None @@ -315,9 +250,6 @@ class BluetoothManager: self._callbacks: list[ tuple[BluetoothCallback, BluetoothCallbackMatcher | None] ] = [] - # Some devices use a random address so we need to use - # an LRU to avoid memory issues. - self._matched: LRU = LRU(MAX_REMEMBER_ADDRESSES) @hass_callback def async_setup(self) -> None: @@ -387,27 +319,12 @@ class BluetoothManager: self, device: BLEDevice, advertisement_data: AdvertisementData ) -> None: """Handle a detected device.""" - matched_domains: set[str] | None = None - match_key = (device.address, bool(advertisement_data.manufacturer_data)) - match_key_has_mfr_data = (device.address, True) - - # If we matched without manufacturer_data, we need to do it again - # since we may think the device is unsupported otherwise - if ( - match_key_has_mfr_data not in self._matched - and match_key not in self._matched - ): - matched_domains = { - matcher["domain"] - for matcher in self._integration_matchers - if _ble_device_matches(matcher, device, advertisement_data) - } - if matched_domains: - self._matched[match_key] = True - + matched_domains = self._integration_matcher.match_domains( + device, advertisement_data + ) _LOGGER.debug( "Device detected: %s with advertisement_data: %s matched domains: %s", - device, + device.address, advertisement_data, matched_domains, ) @@ -417,7 +334,7 @@ class BluetoothManager: service_info: BluetoothServiceInfoBleak | None = None for callback, matcher in self._callbacks: - if matcher is None or _ble_device_matches( + if matcher is None or ble_device_matches( matcher, device, advertisement_data ): if service_info is None: diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py new file mode 100644 index 00000000000..c4560287feb --- /dev/null +++ b/homeassistant/components/bluetooth/match.py @@ -0,0 +1,139 @@ +"""The bluetooth integration matchers.""" +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +import fnmatch +from typing import Final, TypedDict + +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData +from lru import LRU # pylint: disable=no-name-in-module + +from homeassistant.loader import BluetoothMatcher, BluetoothMatcherOptional + +MAX_REMEMBER_ADDRESSES: Final = 2048 + + +ADDRESS: Final = "address" +LOCAL_NAME: Final = "local_name" +SERVICE_UUID: Final = "service_uuid" +SERVICE_DATA_UUID: Final = "service_data_uuid" +MANUFACTURER_ID: Final = "manufacturer_id" +MANUFACTURER_DATA_START: Final = "manufacturer_data_start" + + +class BluetoothCallbackMatcherOptional(TypedDict, total=False): + """Matcher for the bluetooth integration for callback optional fields.""" + + address: str + + +class BluetoothCallbackMatcher( + BluetoothMatcherOptional, + BluetoothCallbackMatcherOptional, +): + """Callback matcher for the bluetooth integration.""" + + +@dataclass(frozen=False) +class IntegrationMatchHistory: + """Track which fields have been seen.""" + + manufacturer_data: bool + service_data: bool + service_uuids: bool + + +def seen_all_fields( + previous_match: IntegrationMatchHistory, adv_data: AdvertisementData +) -> bool: + """Return if we have seen all fields.""" + if not previous_match.manufacturer_data and adv_data.manufacturer_data: + return False + if not previous_match.service_data and adv_data.service_data: + return False + if not previous_match.service_uuids and adv_data.service_uuids: + return False + return True + + +class IntegrationMatcher: + """Integration matcher for the bluetooth integration.""" + + def __init__(self, integration_matchers: list[BluetoothMatcher]) -> None: + """Initialize the matcher.""" + self._integration_matchers = integration_matchers + # Some devices use a random address so we need to use + # an LRU to avoid memory issues. + self._matched: Mapping[str, IntegrationMatchHistory] = LRU( + MAX_REMEMBER_ADDRESSES + ) + + def match_domains(self, device: BLEDevice, adv_data: AdvertisementData) -> set[str]: + """Return the domains that are matched.""" + matched_domains: set[str] = set() + if (previous_match := self._matched.get(device.address)) and seen_all_fields( + previous_match, adv_data + ): + # We have seen all fields so we can skip the rest of the matchers + return matched_domains + matched_domains = { + matcher["domain"] + for matcher in self._integration_matchers + if ble_device_matches(matcher, device, adv_data) + } + if not matched_domains: + return matched_domains + if previous_match: + previous_match.manufacturer_data |= bool(adv_data.manufacturer_data) + previous_match.service_data |= bool(adv_data.service_data) + previous_match.service_uuids |= bool(adv_data.service_uuids) + else: + self._matched[device.address] = IntegrationMatchHistory( # type: ignore[index] + manufacturer_data=bool(adv_data.manufacturer_data), + service_data=bool(adv_data.service_data), + service_uuids=bool(adv_data.service_uuids), + ) + return matched_domains + + +def ble_device_matches( + matcher: BluetoothCallbackMatcher | BluetoothMatcher, + device: BLEDevice, + adv_data: AdvertisementData, +) -> bool: + """Check if a ble device and advertisement_data matches the matcher.""" + if (address := matcher.get(ADDRESS)) is not None and device.address != address: + return False + + if (local_name := matcher.get(LOCAL_NAME)) is not None and not fnmatch.fnmatch( + adv_data.local_name or device.name or device.address, + local_name, + ): + return False + + if ( + service_uuid := matcher.get(SERVICE_UUID) + ) is not None and service_uuid not in adv_data.service_uuids: + return False + + if ( + service_data_uuid := matcher.get(SERVICE_DATA_UUID) + ) is not None and service_data_uuid not in adv_data.service_data: + return False + + if ( + manfacturer_id := matcher.get(MANUFACTURER_ID) + ) is not None and manfacturer_id not in adv_data.manufacturer_data: + return False + + if (manufacturer_data_start := matcher.get(MANUFACTURER_DATA_START)) is not None: + manufacturer_data_start_bytes = bytearray(manufacturer_data_start) + if not any( + manufacturer_data.startswith(manufacturer_data_start_bytes) + for manufacturer_data in adv_data.manufacturer_data.values() + ): + return False + + return True diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 9de06c48786..e4aff23d3f1 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -88,6 +88,7 @@ class BluetoothMatcherOptional(TypedDict, total=False): local_name: str service_uuid: str + service_data_uuid: str manufacturer_id: int manufacturer_data_start: list[int] diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 129899cff11..c6cc019ff31 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -194,6 +194,7 @@ MANIFEST_SCHEMA = vol.Schema( vol.Schema( { vol.Optional("service_uuid"): vol.All(str, verify_lowercase), + vol.Optional("service_data_uuid"): vol.All(str, verify_lowercase), vol.Optional("local_name"): vol.All(str), vol.Optional("manufacturer_id"): int, vol.Optional("manufacturer_data_start"): [int], diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 6dbec251026..8b0d60bc42d 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -222,7 +222,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): assert mock_config_flow.mock_calls[0][1][0] == "switchbot" -async def test_discovery_match_by_manufacturer_id_and_first_byte( +async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( hass, mock_bleak_scanner_start ): """Test bluetooth discovery match by manufacturer_id and manufacturer_data_start.""" @@ -248,20 +248,33 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( assert len(mock_bleak_scanner_start.mock_calls) == 1 hkc_device = BLEDevice("44:44:33:11:23:45", "lock") + hkc_adv_no_mfr_data = AdvertisementData( + local_name="lock", + service_uuids=[], + manufacturer_data={}, + ) hkc_adv = AdvertisementData( local_name="lock", service_uuids=[], manufacturer_data={76: b"\x06\x02\x03\x99"}, ) + # 1st discovery with no manufacturer data + # should not trigger config flow + _get_underlying_scanner()._callback(hkc_device, hkc_adv_no_mfr_data) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + mock_config_flow.reset_mock() + + # 2nd discovery with manufacturer data + # should trigger a config flow _get_underlying_scanner()._callback(hkc_device, hkc_adv) await hass.async_block_till_done() - assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "homekit_controller" mock_config_flow.reset_mock() - # 2nd discovery should not generate another flow + # 3rd discovery should not generate another flow _get_underlying_scanner()._callback(hkc_device, hkc_adv) await hass.async_block_till_done() @@ -288,6 +301,230 @@ async def test_discovery_match_by_manufacturer_id_and_first_byte( assert len(mock_config_flow.mock_calls) == 0 +async def test_discovery_match_by_service_data_uuid_then_others( + hass, mock_bleak_scanner_start +): + """Test bluetooth discovery match by service_data_uuid and then other fields.""" + mock_bt = [ + { + "domain": "my_domain", + "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb", + }, + { + "domain": "my_domain", + "service_uuid": "0000fd3d-0000-1000-8000-00805f9b34fc", + }, + { + "domain": "other_domain", + "manufacturer_id": 323, + }, + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + device = BLEDevice("44:44:33:11:23:45", "lock") + adv_without_service_data_uuid = AdvertisementData( + local_name="lock", + service_uuids=[], + manufacturer_data={}, + ) + adv_with_mfr_data = AdvertisementData( + local_name="lock", + service_uuids=[], + manufacturer_data={323: b"\x01\x02\x03"}, + service_data={}, + ) + adv_with_service_data_uuid = AdvertisementData( + local_name="lock", + service_uuids=[], + manufacturer_data={}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x01\x02\x03"}, + ) + adv_with_service_data_uuid_and_mfr_data = AdvertisementData( + local_name="lock", + service_uuids=[], + manufacturer_data={323: b"\x01\x02\x03"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x01\x02\x03"}, + ) + adv_with_service_data_uuid_and_mfr_data_and_service_uuid = AdvertisementData( + local_name="lock", + manufacturer_data={323: b"\x01\x02\x03"}, + service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x01\x02\x03"}, + service_uuids=["0000fd3d-0000-1000-8000-00805f9b34fd"], + ) + adv_with_service_uuid = AdvertisementData( + local_name="lock", + manufacturer_data={}, + service_data={}, + service_uuids=["0000fd3d-0000-1000-8000-00805f9b34fd"], + ) + # 1st discovery should not generate a flow because the + # service_data_uuid is not in the advertisement + _get_underlying_scanner()._callback(device, adv_without_service_data_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + mock_config_flow.reset_mock() + + # 2nd discovery should not generate a flow because the + # service_data_uuid is not in the advertisement + _get_underlying_scanner()._callback(device, adv_without_service_data_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + mock_config_flow.reset_mock() + + # 3rd discovery should generate a flow because the + # manufacturer_data is in the advertisement + _get_underlying_scanner()._callback(device, adv_with_mfr_data) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "other_domain" + mock_config_flow.reset_mock() + + # 4th discovery should generate a flow because the + # service_data_uuid is in the advertisement and + # we never saw a service_data_uuid before + _get_underlying_scanner()._callback(device, adv_with_service_data_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "my_domain" + mock_config_flow.reset_mock() + + # 5th discovery should not generate a flow because the + # we already saw an advertisement with the service_data_uuid + _get_underlying_scanner()._callback(device, adv_with_service_data_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + + # 6th discovery should not generate a flow because the + # manufacturer_data is in the advertisement + # and we saw manufacturer_data before + _get_underlying_scanner()._callback( + device, adv_with_service_data_uuid_and_mfr_data + ) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + mock_config_flow.reset_mock() + + # 7th discovery should generate a flow because the + # service_uuids is in the advertisement + # and we never saw service_uuids before + _get_underlying_scanner()._callback( + device, adv_with_service_data_uuid_and_mfr_data_and_service_uuid + ) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 2 + assert { + mock_config_flow.mock_calls[0][1][0], + mock_config_flow.mock_calls[1][1][0], + } == {"my_domain", "other_domain"} + mock_config_flow.reset_mock() + + # 8th discovery should not generate a flow + # since all fields have been seen at this point + _get_underlying_scanner()._callback( + device, adv_with_service_data_uuid_and_mfr_data_and_service_uuid + ) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + mock_config_flow.reset_mock() + + # 9th discovery should not generate a flow + # since all fields have been seen at this point + _get_underlying_scanner()._callback(device, adv_with_service_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + + # 10th discovery should not generate a flow + # since all fields have been seen at this point + _get_underlying_scanner()._callback(device, adv_with_service_data_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + + # 11th discovery should not generate a flow + # since all fields have been seen at this point + _get_underlying_scanner()._callback(device, adv_without_service_data_uuid) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + + +async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( + hass, mock_bleak_scanner_start +): + """Test bluetooth discovery matches twice for service_uuid and then manufacturer_id.""" + mock_bt = [ + { + "domain": "my_domain", + "manufacturer_id": 76, + }, + { + "domain": "my_domain", + "service_uuid": "0000fd3d-0000-1000-8000-00805f9b34fc", + }, + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + + with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + device = BLEDevice("44:44:33:11:23:45", "lock") + adv_service_uuids = AdvertisementData( + local_name="lock", + service_uuids=["0000fd3d-0000-1000-8000-00805f9b34fc"], + manufacturer_data={}, + ) + adv_manufacturer_data = AdvertisementData( + local_name="lock", + service_uuids=[], + manufacturer_data={76: b"\x06\x02\x03\x99"}, + ) + + # 1st discovery with matches service_uuid + # should trigger config flow + _get_underlying_scanner()._callback(device, adv_service_uuids) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "my_domain" + mock_config_flow.reset_mock() + + # 2nd discovery with manufacturer data + # should trigger a config flow + _get_underlying_scanner()._callback(device, adv_manufacturer_data) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "my_domain" + mock_config_flow.reset_mock() + + # 3rd discovery should not generate another flow + _get_underlying_scanner()._callback(device, adv_service_uuids) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + + # 4th discovery should not generate another flow + _get_underlying_scanner()._callback(device, adv_manufacturer_data) + await hass.async_block_till_done() + assert len(mock_config_flow.mock_calls) == 0 + + async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): """Test the async_discovered_device API.""" mock_bt = [] From 511af3c455d8dd1779fbe1f6127e0b6d258a5bcb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Jul 2022 16:55:52 -0500 Subject: [PATCH 2827/3516] Update switchbot bluetooth matchers for sensor devices (#75690) --- CODEOWNERS | 4 ++-- homeassistant/components/switchbot/manifest.json | 11 +++++++++-- homeassistant/generated/bluetooth.py | 4 ++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 66e54b25b9e..bcbd6c20364 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1042,8 +1042,8 @@ build.json @home-assistant/supervisor /tests/components/switch/ @home-assistant/core /homeassistant/components/switch_as_x/ @home-assistant/core /tests/components/switch_as_x/ @home-assistant/core -/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas -/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas +/homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas +/tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas /homeassistant/components/switcher_kis/ @tomerfi @thecode /tests/components/switcher_kis/ @tomerfi @thecode /homeassistant/components/switchmate/ @danielhiversen diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index a82ef264698..3bccbb4f674 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -5,8 +5,15 @@ "requirements": ["PySwitchbot==0.15.1"], "config_flow": true, "dependencies": ["bluetooth"], - "codeowners": ["@danielhiversen", "@RenierM26", "@murtas"], - "bluetooth": [{ "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" }], + "codeowners": ["@bdraco", "@danielhiversen", "@RenierM26", "@murtas"], + "bluetooth": [ + { + "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb" + }, + { + "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" + } + ], "iot_class": "local_push", "loggers": ["switchbot"] } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index f9dd4352e28..5dde90f1f7a 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -65,6 +65,10 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "domain": "sensorpush", "local_name": "SensorPush*" }, + { + "domain": "switchbot", + "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb" + }, { "domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b" From 22ca28b93d4c88eda928b4e22b2d5510a369821c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Jul 2022 19:23:23 -0500 Subject: [PATCH 2828/3516] Ensure bluetooth can be reloaded when hot plugging a bluetooth adapter (#75699) --- homeassistant/components/bluetooth/__init__.py | 8 +++++++- tests/components/bluetooth/test_init.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index fb514a33e99..9266603839c 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -440,5 +440,11 @@ class BluetoothManager: self._cancel_unavailable_tracking() self._cancel_unavailable_tracking = None if self.scanner: - await self.scanner.stop() + try: + await self.scanner.stop() + except BleakError as ex: + # This is not fatal, and they may want to reload + # the config entry to restart the scanner if they + # change the bluetooth dongle. + _LOGGER.error("Error stopping scanner: %s", ex) uninstall_multiple_bleak_catcher() diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 8b0d60bc42d..aa3a6253b83 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1213,3 +1213,20 @@ async def test_getting_the_scanner_returns_the_wrapped_instance(hass, enable_blu """Test getting the scanner returns the wrapped instance.""" scanner = bluetooth.async_get_scanner(hass) assert isinstance(scanner, models.HaBleakScannerWrapper) + + +async def test_config_entry_can_be_reloaded_when_stop_raises( + hass, caplog, enable_bluetooth +): + """Test we can reload if stopping the scanner raises.""" + entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] + assert entry.state == ConfigEntryState.LOADED + + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.stop", side_effect=BleakError + ): + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + assert "Error stopping scanner" in caplog.text From 4a50010458e3552cba4e617eb827839587a9be79 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 25 Jul 2022 00:25:55 +0000 Subject: [PATCH 2829/3516] [ci skip] Translation update --- .../components/bluetooth/translations/et.json | 22 ++++++++++++ .../components/bluetooth/translations/id.json | 6 ++++ .../components/bluetooth/translations/it.json | 6 ++++ .../components/bluetooth/translations/ja.json | 6 ++++ .../bluetooth/translations/pt-BR.json | 6 ++++ .../bluetooth/translations/zh-Hant.json | 6 ++++ .../components/demo/translations/it.json | 16 +++++++++ .../components/generic/translations/it.json | 4 ++- .../components/google/translations/it.json | 3 +- .../components/govee_ble/translations/ca.json | 21 +++++++++++ .../components/govee_ble/translations/de.json | 21 +++++++++++ .../components/govee_ble/translations/el.json | 21 +++++++++++ .../components/govee_ble/translations/fr.json | 21 +++++++++++ .../components/govee_ble/translations/id.json | 21 +++++++++++ .../components/govee_ble/translations/it.json | 21 +++++++++++ .../components/govee_ble/translations/ja.json | 21 +++++++++++ .../govee_ble/translations/pt-BR.json | 21 +++++++++++ .../here_travel_time/translations/it.json | 10 ++++-- .../components/homekit/translations/it.json | 4 +-- .../components/inkbird/translations/et.json | 21 +++++++++++ .../components/inkbird/translations/it.json | 21 +++++++++++ .../components/lifx/translations/it.json | 8 +++++ .../components/moat/translations/ca.json | 21 +++++++++++ .../components/moat/translations/de.json | 21 +++++++++++ .../components/moat/translations/el.json | 21 +++++++++++ .../components/moat/translations/fr.json | 21 +++++++++++ .../components/moat/translations/id.json | 21 +++++++++++ .../components/moat/translations/it.json | 21 +++++++++++ .../components/moat/translations/ja.json | 21 +++++++++++ .../components/moat/translations/pt-BR.json | 21 +++++++++++ .../components/nextdns/translations/it.json | 15 ++++++++ .../components/plugwise/translations/it.json | 3 +- .../components/rhasspy/translations/it.json | 3 ++ .../sensorpush/translations/et.json | 21 +++++++++++ .../sensorpush/translations/it.json | 21 +++++++++++ .../simplisafe/translations/de.json | 7 ++-- .../simplisafe/translations/en.json | 21 ++++++++++- .../simplisafe/translations/fr.json | 8 +++-- .../simplisafe/translations/it.json | 7 ++-- .../simplisafe/translations/pt-BR.json | 7 ++-- .../components/switchbot/translations/de.json | 1 + .../components/switchbot/translations/en.json | 7 ++-- .../components/switchbot/translations/fr.json | 3 +- .../components/switchbot/translations/it.json | 3 +- .../switchbot/translations/pt-BR.json | 3 +- .../twentemilieu/translations/de.json | 2 +- .../components/uscis/translations/it.json | 2 +- .../components/verisure/translations/it.json | 6 ++-- .../components/withings/translations/it.json | 3 +- .../xiaomi_ble/translations/de.json | 9 +++++ .../xiaomi_ble/translations/en.json | 15 ++++++++ .../xiaomi_ble/translations/et.json | 21 +++++++++++ .../xiaomi_ble/translations/id.json | 16 ++++++++- .../xiaomi_ble/translations/it.json | 36 +++++++++++++++++++ .../xiaomi_ble/translations/ja.json | 21 +++++++++++ .../xiaomi_ble/translations/pt-BR.json | 36 +++++++++++++++++++ .../xiaomi_ble/translations/zh-Hant.json | 21 +++++++++++ .../components/zha/translations/et.json | 1 + .../components/zha/translations/it.json | 1 + .../components/zha/translations/ja.json | 1 + 60 files changed, 767 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/bluetooth/translations/et.json create mode 100644 homeassistant/components/govee_ble/translations/ca.json create mode 100644 homeassistant/components/govee_ble/translations/de.json create mode 100644 homeassistant/components/govee_ble/translations/el.json create mode 100644 homeassistant/components/govee_ble/translations/fr.json create mode 100644 homeassistant/components/govee_ble/translations/id.json create mode 100644 homeassistant/components/govee_ble/translations/it.json create mode 100644 homeassistant/components/govee_ble/translations/ja.json create mode 100644 homeassistant/components/govee_ble/translations/pt-BR.json create mode 100644 homeassistant/components/inkbird/translations/et.json create mode 100644 homeassistant/components/inkbird/translations/it.json create mode 100644 homeassistant/components/moat/translations/ca.json create mode 100644 homeassistant/components/moat/translations/de.json create mode 100644 homeassistant/components/moat/translations/el.json create mode 100644 homeassistant/components/moat/translations/fr.json create mode 100644 homeassistant/components/moat/translations/id.json create mode 100644 homeassistant/components/moat/translations/it.json create mode 100644 homeassistant/components/moat/translations/ja.json create mode 100644 homeassistant/components/moat/translations/pt-BR.json create mode 100644 homeassistant/components/sensorpush/translations/et.json create mode 100644 homeassistant/components/sensorpush/translations/it.json create mode 100644 homeassistant/components/xiaomi_ble/translations/et.json create mode 100644 homeassistant/components/xiaomi_ble/translations/it.json create mode 100644 homeassistant/components/xiaomi_ble/translations/ja.json create mode 100644 homeassistant/components/xiaomi_ble/translations/pt-BR.json create mode 100644 homeassistant/components/xiaomi_ble/translations/zh-Hant.json diff --git a/homeassistant/components/bluetooth/translations/et.json b/homeassistant/components/bluetooth/translations/et.json new file mode 100644 index 00000000000..4098abefbf7 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name} ?" + }, + "enable_bluetooth": { + "description": "Kas soovid Bluetoothi seadistada?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/id.json b/homeassistant/components/bluetooth/translations/id.json index 12653696241..5fe99b68c2f 100644 --- a/homeassistant/components/bluetooth/translations/id.json +++ b/homeassistant/components/bluetooth/translations/id.json @@ -1,10 +1,16 @@ { "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Ingin menyiapkan {name}?" }, + "enable_bluetooth": { + "description": "Ingin menyiapkan Bluetooth?" + }, "user": { "data": { "address": "Perangkat" diff --git a/homeassistant/components/bluetooth/translations/it.json b/homeassistant/components/bluetooth/translations/it.json index 12d79b3efff..af83bfd271d 100644 --- a/homeassistant/components/bluetooth/translations/it.json +++ b/homeassistant/components/bluetooth/translations/it.json @@ -1,10 +1,16 @@ { "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Vuoi configurare {name}?" }, + "enable_bluetooth": { + "description": "Vuoi configurare il Bluetooth?" + }, "user": { "data": { "address": "Dispositivo" diff --git a/homeassistant/components/bluetooth/translations/ja.json b/homeassistant/components/bluetooth/translations/ja.json index f42ea56e2c8..6257d1c67d4 100644 --- a/homeassistant/components/bluetooth/translations/ja.json +++ b/homeassistant/components/bluetooth/translations/ja.json @@ -1,10 +1,16 @@ { "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, + "enable_bluetooth": { + "description": "Bluetooth\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, "user": { "data": { "address": "\u30c7\u30d0\u30a4\u30b9" diff --git a/homeassistant/components/bluetooth/translations/pt-BR.json b/homeassistant/components/bluetooth/translations/pt-BR.json index 9cafa844652..05ddee20ecb 100644 --- a/homeassistant/components/bluetooth/translations/pt-BR.json +++ b/homeassistant/components/bluetooth/translations/pt-BR.json @@ -1,10 +1,16 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Deseja configurar {name}?" }, + "enable_bluetooth": { + "description": "Deseja configurar o Bluetooth?" + }, "user": { "data": { "address": "Dispositivo" diff --git a/homeassistant/components/bluetooth/translations/zh-Hant.json b/homeassistant/components/bluetooth/translations/zh-Hant.json index 3f92640469e..7d69eb15ac1 100644 --- a/homeassistant/components/bluetooth/translations/zh-Hant.json +++ b/homeassistant/components/bluetooth/translations/zh-Hant.json @@ -1,10 +1,16 @@ { "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, + "enable_bluetooth": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u85cd\u82bd\uff1f" + }, "user": { "data": { "address": "\u88dd\u7f6e" diff --git a/homeassistant/components/demo/translations/it.json b/homeassistant/components/demo/translations/it.json index 7fc00caf26e..80281a62703 100644 --- a/homeassistant/components/demo/translations/it.json +++ b/homeassistant/components/demo/translations/it.json @@ -1,6 +1,22 @@ { "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Premere OK quando il liquido delle frecce \u00e8 stato riempito", + "title": "Il liquido delle frecce deve essere rabboccato" + } + } + }, + "title": "Il liquido delle frecce \u00e8 vuoto e deve essere rabboccato" + }, + "transmogrifier_deprecated": { + "description": "Il componente transmogrifier \u00e8 ora deprecato a causa della mancanza di controllo locale disponibile nella nuova API", + "title": "Il componente transmogrifier \u00e8 deprecato" + }, "unfixable_problem": { + "description": "Questo problema non si risolver\u00e0 mai.", "title": "Questo non \u00e8 un problema risolvibile" } }, diff --git a/homeassistant/components/generic/translations/it.json b/homeassistant/components/generic/translations/it.json index 80f8c90ce16..14d4b6e8720 100644 --- a/homeassistant/components/generic/translations/it.json +++ b/homeassistant/components/generic/translations/it.json @@ -7,7 +7,7 @@ "error": { "already_exists": "Esiste gi\u00e0 una telecamera con queste impostazioni URL.", "invalid_still_image": "L'URL non ha restituito un'immagine fissa valida", - "malformed_url": "URL malformato", + "malformed_url": "URL non valido", "no_still_image_or_stream_url": "Devi specificare almeno un'immagine fissa o un URL di un flusso", "relative_url": "Non sono consentiti URL relativi", "stream_file_not_found": "File non trovato durante il tentativo di connessione al (\u00e8 installato ffmpeg?)", @@ -52,7 +52,9 @@ "error": { "already_exists": "Esiste gi\u00e0 una telecamera con queste impostazioni URL.", "invalid_still_image": "L'URL non ha restituito un'immagine fissa valida", + "malformed_url": "URL non valido", "no_still_image_or_stream_url": "Devi specificare almeno un'immagine fissa o un URL di un flusso", + "relative_url": "Non sono consentiti URL relativi", "stream_file_not_found": "File non trovato durante il tentativo di connessione al (\u00e8 installato ffmpeg?)", "stream_http_not_found": "HTTP 404 Non trovato durante il tentativo di connessione al flusso", "stream_io_error": "Errore di input/output durante il tentativo di connessione al flusso. Protocollo di trasporto RTSP errato?", diff --git a/homeassistant/components/google/translations/it.json b/homeassistant/components/google/translations/it.json index ef5ec01202d..782fed55d5b 100644 --- a/homeassistant/components/google/translations/it.json +++ b/homeassistant/components/google/translations/it.json @@ -11,7 +11,8 @@ "invalid_access_token": "Token di accesso non valido", "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", "oauth_error": "Ricevuti dati token non validi.", - "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "timeout_connect": "Tempo scaduto per stabile la connessione." }, "create_entry": { "default": "Autenticazione riuscita" diff --git a/homeassistant/components/govee_ble/translations/ca.json b/homeassistant/components/govee_ble/translations/ca.json new file mode 100644 index 00000000000..0cd4571dc9d --- /dev/null +++ b/homeassistant/components/govee_ble/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/de.json b/homeassistant/components/govee_ble/translations/de.json new file mode 100644 index 00000000000..81dda510bc5 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/el.json b/homeassistant/components/govee_ble/translations/el.json new file mode 100644 index 00000000000..0a802a0bc89 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/fr.json b/homeassistant/components/govee_ble/translations/fr.json new file mode 100644 index 00000000000..c8a1af034cf --- /dev/null +++ b/homeassistant/components/govee_ble/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/id.json b/homeassistant/components/govee_ble/translations/id.json new file mode 100644 index 00000000000..07426a0e290 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/it.json b/homeassistant/components/govee_ble/translations/it.json new file mode 100644 index 00000000000..501b5095826 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/ja.json b/homeassistant/components/govee_ble/translations/ja.json new file mode 100644 index 00000000000..38f862bd2f6 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/pt-BR.json b/homeassistant/components/govee_ble/translations/pt-BR.json new file mode 100644 index 00000000000..2067d7f9312 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/it.json b/homeassistant/components/here_travel_time/translations/it.json index 7c87ef36d0e..17cd09fed8b 100644 --- a/homeassistant/components/here_travel_time/translations/it.json +++ b/homeassistant/components/here_travel_time/translations/it.json @@ -22,8 +22,8 @@ }, "destination_menu": { "menu_options": { - "destination_coordinates": "Utilizzando una posizione sulla mappa", - "destination_entity": "Utilizzando un'entit\u00e0" + "destination_coordinates": "Utilizzo di una posizione sulla mappa", + "destination_entity": "Utilizzo di un'entit\u00e0" }, "title": "Scegli la destinazione" }, @@ -40,7 +40,11 @@ "title": "Scegli la partenza" }, "origin_menu": { - "title": "Scegli Origine" + "menu_options": { + "origin_coordinates": "Utilizzo di una posizione sulla mappa", + "origin_entity": "Utilizzo di un'entit\u00e0" + }, + "title": "Scegli la partenza" }, "user": { "data": { diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index c46acd8965d..3aefb01d63a 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -44,14 +44,14 @@ "data": { "entities": "Entit\u00e0" }, - "description": "Tutte le entit\u00e0 \"{domains}\" saranno incluse a eccezione delle entit\u00e0 escluse e delle entit\u00e0 categorizzate.", + "description": "Tutte le entit\u00e0 \"{domains}\" saranno incluse ad eccezione delle entit\u00e0 escluse e delle entit\u00e0 categorizzate.", "title": "Seleziona le entit\u00e0 da escludere" }, "include": { "data": { "entities": "Entit\u00e0" }, - "description": "Tutte le entit\u00e0 \"{domains}\" saranno incluse a eccezione delle entit\u00e0 escluse e delle entit\u00e0 categorizzate.", + "description": "Tutte le entit\u00e0 \"{domains}\" saranno incluse a meno che non siano selezionate entit\u00e0 specifiche.", "title": "Seleziona le entit\u00e0 da includere" }, "init": { diff --git a/homeassistant/components/inkbird/translations/et.json b/homeassistant/components/inkbird/translations/et.json new file mode 100644 index 00000000000..749f7e45de5 --- /dev/null +++ b/homeassistant/components/inkbird/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "no_devices_found": "V\u00f6rgust seadmeid ei leitud" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/it.json b/homeassistant/components/inkbird/translations/it.json new file mode 100644 index 00000000000..501b5095826 --- /dev/null +++ b/homeassistant/components/inkbird/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/it.json b/homeassistant/components/lifx/translations/it.json index d9c25e63590..9e8c090ad0d 100644 --- a/homeassistant/components/lifx/translations/it.json +++ b/homeassistant/components/lifx/translations/it.json @@ -1,9 +1,14 @@ { "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", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { @@ -18,6 +23,9 @@ } }, "user": { + "data": { + "host": "Host" + }, "description": "Se lasci l'host vuoto, il rilevamento verr\u00e0 utilizzato per trovare i dispositivi." } } diff --git a/homeassistant/components/moat/translations/ca.json b/homeassistant/components/moat/translations/ca.json new file mode 100644 index 00000000000..0cd4571dc9d --- /dev/null +++ b/homeassistant/components/moat/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/de.json b/homeassistant/components/moat/translations/de.json new file mode 100644 index 00000000000..81dda510bc5 --- /dev/null +++ b/homeassistant/components/moat/translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/el.json b/homeassistant/components/moat/translations/el.json new file mode 100644 index 00000000000..0a802a0bc89 --- /dev/null +++ b/homeassistant/components/moat/translations/el.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/fr.json b/homeassistant/components/moat/translations/fr.json new file mode 100644 index 00000000000..c8a1af034cf --- /dev/null +++ b/homeassistant/components/moat/translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/id.json b/homeassistant/components/moat/translations/id.json new file mode 100644 index 00000000000..07426a0e290 --- /dev/null +++ b/homeassistant/components/moat/translations/id.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/it.json b/homeassistant/components/moat/translations/it.json new file mode 100644 index 00000000000..501b5095826 --- /dev/null +++ b/homeassistant/components/moat/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/ja.json b/homeassistant/components/moat/translations/ja.json new file mode 100644 index 00000000000..38f862bd2f6 --- /dev/null +++ b/homeassistant/components/moat/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/pt-BR.json b/homeassistant/components/moat/translations/pt-BR.json new file mode 100644 index 00000000000..2067d7f9312 --- /dev/null +++ b/homeassistant/components/moat/translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/it.json b/homeassistant/components/nextdns/translations/it.json index 67c9520f2d4..68d98f77d09 100644 --- a/homeassistant/components/nextdns/translations/it.json +++ b/homeassistant/components/nextdns/translations/it.json @@ -3,12 +3,27 @@ "abort": { "already_configured": "Questo profilo NextDNS \u00e8 gi\u00e0 configurato." }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_api_key": "Chiave API non valida", + "unknown": "Errore imprevisto" + }, "step": { "profiles": { "data": { "profile": "Profilo" } + }, + "user": { + "data": { + "api_key": "Chiave API" + } } } + }, + "system_health": { + "info": { + "can_reach_server": "Server raggiungibile" + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/it.json b/homeassistant/components/plugwise/translations/it.json index 24e75f3e846..e4bf239b7bc 100644 --- a/homeassistant/components/plugwise/translations/it.json +++ b/homeassistant/components/plugwise/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "anna_with_adam": "Sia Anna che Adam sono stati rilevati. Aggiungi il tuo Adamo al posto della tua Anna" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/rhasspy/translations/it.json b/homeassistant/components/rhasspy/translations/it.json index bd2daba89f6..55ce19aff24 100644 --- a/homeassistant/components/rhasspy/translations/it.json +++ b/homeassistant/components/rhasspy/translations/it.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, "step": { "user": { "description": "Vuoi abilitare il supporto Rhasspy?" diff --git a/homeassistant/components/sensorpush/translations/et.json b/homeassistant/components/sensorpush/translations/et.json new file mode 100644 index 00000000000..94bc17992fe --- /dev/null +++ b/homeassistant/components/sensorpush/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine juba k\u00e4ib", + "no_devices_found": "V\u00f6rgust seadmeid ei leitud" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/it.json b/homeassistant/components/sensorpush/translations/it.json new file mode 100644 index 00000000000..501b5095826 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index d224f3c2441..4788f2201b4 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Dieses SimpliSafe-Konto wird bereits verwendet.", "email_2fa_timed_out": "Zeit\u00fcberschreitung beim Warten auf E-Mail-basierte Zwei-Faktor-Authentifizierung.", - "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", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "Autorisierungscode", "password": "Passwort", "username": "Benutzername" }, - "description": "Gib deinen Benutzernamen und Passwort ein." + "description": "SimpliSafe authentifiziert Benutzer \u00fcber seine Web-App. Aufgrund technischer Einschr\u00e4nkungen gibt es am Ende dieses Prozesses einen manuellen Schritt; Bitte stelle sicher, dass Du die [Dokumentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) lesen, bevor Sie beginnen. \n\n Wenn Sie fertig sind, klicke [hier]({url}), um die SimpliSafe-Web-App zu \u00f6ffnen und Ihre Anmeldeinformationen einzugeben. Wenn der Vorgang abgeschlossen ist, kehre hierher zur\u00fcck und gebe den Autorisierungscode von der SimpliSafe-Web-App-URL ein." } } }, diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 82320df4864..70b0cc15383 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "This SimpliSafe account is already in use.", + "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", "reauth_successful": "Re-authentication was successful", "wrong_account": "The user credentials provided do not match this SimpliSafe account." }, @@ -10,10 +11,28 @@ "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, + "progress": { + "email_2fa": "Check your email for a verification link from Simplisafe." + }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Please re-enter the password for {username}.", + "title": "Reauthenticate Integration" + }, + "sms_2fa": { + "data": { + "code": "Code" + }, + "description": "Input the two-factor authentication code sent to you via SMS." + }, "user": { "data": { - "auth_code": "Authorization Code" + "auth_code": "Authorization Code", + "password": "Password", + "username": "Username" }, "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL." } diff --git a/homeassistant/components/simplisafe/translations/fr.json b/homeassistant/components/simplisafe/translations/fr.json index 4de2af95885..15ad31ce463 100644 --- a/homeassistant/components/simplisafe/translations/fr.json +++ b/homeassistant/components/simplisafe/translations/fr.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Ce compte SimpliSafe est d\u00e9j\u00e0 utilis\u00e9.", "email_2fa_timed_out": "D\u00e9lai d'attente de l'authentification \u00e0 deux facteurs par courriel expir\u00e9.", - "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "wrong_account": "Les informations d'identification d'utilisateur fournies ne correspondent pas \u00e0 ce compte SimpliSafe." }, "error": { + "identifier_exists": "Compte d\u00e9j\u00e0 enregistr\u00e9", "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, @@ -28,10 +30,10 @@ }, "user": { "data": { + "auth_code": "Code d'autorisation", "password": "Mot de passe", "username": "Nom d'utilisateur" - }, - "description": "Saisissez votre nom d'utilisateur et votre mot de passe." + } } } }, diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index b9caae3f0a4..ba75954ce71 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Questo account SimpliSafe \u00e8 gi\u00e0 in uso.", "email_2fa_timed_out": "Timeout durante l'attesa dell'autenticazione a due fattori basata su email.", - "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", "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "Codice di autorizzazione", "password": "Password", "username": "Nome utente" }, - "description": "Digita il tuo nome utente e password." + "description": "SimpliSafe autentica gli utenti tramite la sua app web. A causa di limitazioni tecniche, alla fine di questo processo \u00e8 previsto un passaggio manuale; assicurati, prima di iniziare, di leggere la [documentazione](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code). \n\nQuando sei pronto, fai clic [qui]({url}) per aprire l'app Web SimpliSafe e inserire le tue credenziali. Al termine del processo, ritorna qui e inserisci il codice di autorizzazione dall'URL dell'app Web SimpliSafe." } } }, diff --git a/homeassistant/components/simplisafe/translations/pt-BR.json b/homeassistant/components/simplisafe/translations/pt-BR.json index ccfb13b6cc1..74b30d2a9ef 100644 --- a/homeassistant/components/simplisafe/translations/pt-BR.json +++ b/homeassistant/components/simplisafe/translations/pt-BR.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "A conta j\u00e1 foi configurada", "email_2fa_timed_out": "Expirou enquanto aguardava a autentica\u00e7\u00e3o de dois fatores enviada por e-mail.", - "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "wrong_account": "As credenciais de usu\u00e1rio fornecidas n\u00e3o correspondem a esta conta SimpliSafe." }, "error": { + "identifier_exists": "Conta j\u00e1 cadastrada", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "C\u00f3digo de autoriza\u00e7\u00e3o", "password": "Senha", "username": "Usu\u00e1rio" }, - "description": "Insira seu nome de usu\u00e1rio e senha." + "description": "O SimpliSafe autentica os usu\u00e1rios por meio de seu aplicativo da web. Por limita\u00e7\u00f5es t\u00e9cnicas, existe uma etapa manual ao final deste processo; certifique-se de ler a [documenta\u00e7\u00e3o](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de come\u00e7ar. \n\n Quando estiver pronto, clique [aqui]( {url} ) para abrir o aplicativo Web SimpliSafe e insira suas credenciais. Quando o processo estiver conclu\u00eddo, retorne aqui e insira o c\u00f3digo de autoriza\u00e7\u00e3o da URL do aplicativo Web SimpliSafe." } } }, diff --git a/homeassistant/components/switchbot/translations/de.json b/homeassistant/components/switchbot/translations/de.json index 439524c8aa6..c1ab660e6a5 100644 --- a/homeassistant/components/switchbot/translations/de.json +++ b/homeassistant/components/switchbot/translations/de.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "address": "Ger\u00e4teadresse", "mac": "MAC-Adresse des Ger\u00e4ts", "name": "Name", "password": "Passwort" diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index 0407f71ece3..b583c60061b 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -7,12 +7,12 @@ "switchbot_unsupported_type": "Unsupported Switchbot Type.", "unknown": "Unexpected error" }, - "error": {}, "flow_title": "{name} ({address})", "step": { "user": { "data": { "address": "Device address", + "mac": "Device MAC address", "name": "Name", "password": "Password" }, @@ -24,7 +24,10 @@ "step": { "init": { "data": { - "retry_count": "Retry count" + "retry_count": "Retry count", + "retry_timeout": "Timeout between retries", + "scan_timeout": "How long to scan for advertisement data", + "update_time": "Time between updates (seconds)" } } } diff --git a/homeassistant/components/switchbot/translations/fr.json b/homeassistant/components/switchbot/translations/fr.json index 75eff0a9b7c..ea070f9f3c2 100644 --- a/homeassistant/components/switchbot/translations/fr.json +++ b/homeassistant/components/switchbot/translations/fr.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "Type Switchbot non pris en charge.", "unknown": "Erreur inattendue" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Adresse de l'appareil", "mac": "Adresse MAC de l'appareil", "name": "Nom", "password": "Mot de passe" diff --git a/homeassistant/components/switchbot/translations/it.json b/homeassistant/components/switchbot/translations/it.json index b8997f9247b..bcd6465acae 100644 --- a/homeassistant/components/switchbot/translations/it.json +++ b/homeassistant/components/switchbot/translations/it.json @@ -11,10 +11,11 @@ "one": "Vuoto", "other": "Vuoti" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Indirizzo del dispositivo", "mac": "Indirizzo MAC del dispositivo", "name": "Nome", "password": "Password" diff --git a/homeassistant/components/switchbot/translations/pt-BR.json b/homeassistant/components/switchbot/translations/pt-BR.json index 3959425cbd3..3edc04ba6f3 100644 --- a/homeassistant/components/switchbot/translations/pt-BR.json +++ b/homeassistant/components/switchbot/translations/pt-BR.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "Tipo de Switchbot sem suporte.", "unknown": "Erro inesperado" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Endere\u00e7o do dispositivo", "mac": "Endere\u00e7o MAC do dispositivo", "name": "Nome", "password": "Senha" diff --git a/homeassistant/components/twentemilieu/translations/de.json b/homeassistant/components/twentemilieu/translations/de.json index 36ea2123bdb..42e9f0b3e23 100644 --- a/homeassistant/components/twentemilieu/translations/de.json +++ b/homeassistant/components/twentemilieu/translations/de.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "house_letter": "Hausbrief/zusatz", + "house_letter": "Hausbrief/Zusatz", "house_number": "Hausnummer", "post_code": "Postleitzahl" }, diff --git a/homeassistant/components/uscis/translations/it.json b/homeassistant/components/uscis/translations/it.json index 1e23b69eee5..1cb9e54a6b7 100644 --- a/homeassistant/components/uscis/translations/it.json +++ b/homeassistant/components/uscis/translations/it.json @@ -1,7 +1,7 @@ { "issues": { "pending_removal": { - "description": "L'integrazione U.S. Citizenship and Immigration Services (USCIS) \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\n L'integrazione verr\u00e0 rimossa, perch\u00e9 si basa sul webscraping, che non \u00e8 consentito. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "description": "L'integrazione U.S. Citizenship and Immigration Services (USCIS) \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nL'integrazione sar\u00e0 rimossa, perch\u00e9 si basa sul webscraping, che non \u00e8 consentito. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "L'integrazione USCIS verr\u00e0 rimossa" } } diff --git a/homeassistant/components/verisure/translations/it.json b/homeassistant/components/verisure/translations/it.json index b913a47af03..8361765c5c2 100644 --- a/homeassistant/components/verisure/translations/it.json +++ b/homeassistant/components/verisure/translations/it.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Autenticazione non valida", - "unknown": "Errore imprevisto" + "unknown": "Errore imprevisto", + "unknown_mfa": "Si \u00e8 verificato un errore sconosciuto durante la configurazione dell'autenticazione a pi\u00f9 fattori" }, "step": { "installation": { @@ -30,7 +31,8 @@ }, "reauth_mfa": { "data": { - "code": "Codice di verifica" + "code": "Codice di verifica", + "description": "Il tuo account ha la verifica in due passaggi abilitata. Inserisci il codice di verifica che ti viene inviato da Verisure." } }, "user": { diff --git a/homeassistant/components/withings/translations/it.json b/homeassistant/components/withings/translations/it.json index 412b75fc1b5..30836be5da5 100644 --- a/homeassistant/components/withings/translations/it.json +++ b/homeassistant/components/withings/translations/it.json @@ -29,7 +29,8 @@ "title": "Autentica nuovamente l'integrazione" }, "reauth_confirm": { - "description": "Il profilo \" {profile} \" deve essere riautenticato per poter continuare a ricevere i dati Withings." + "description": "Il profilo \"{profile}\" deve essere nuovamente autenticato per continuare a ricevere i dati di Withings.", + "title": "Autentica nuovamente l'integrazione" } } } diff --git a/homeassistant/components/xiaomi_ble/translations/de.json b/homeassistant/components/xiaomi_ble/translations/de.json index 81dda510bc5..b81386a076f 100644 --- a/homeassistant/components/xiaomi_ble/translations/de.json +++ b/homeassistant/components/xiaomi_ble/translations/de.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "decryption_failed": "Der bereitgestellte Bindkey funktionierte nicht, Sensordaten konnten nicht entschl\u00fcsselt werden. Bitte \u00fcberpr\u00fcfe es und versuche es erneut.", + "expected_24_characters": "Erwartet wird ein 24-stelliger hexadezimaler Bindkey.", + "expected_32_characters": "Erwartet wird ein 32-stelliger hexadezimaler Bindkey.", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" }, "flow_title": "{name}", @@ -10,6 +13,12 @@ "bluetooth_confirm": { "description": "M\u00f6chtest du {name} einrichten?" }, + "get_encryption_key_4_5": { + "description": "Die vom Sensor \u00fcbertragenen Sensordaten sind verschl\u00fcsselt. Um sie zu entschl\u00fcsseln, ben\u00f6tigen wir einen 32-stelligen hexadezimalen Bindungsschl\u00fcssel." + }, + "get_encryption_key_legacy": { + "description": "Die vom Sensor \u00fcbertragenen Sensordaten sind verschl\u00fcsselt. Um sie zu entschl\u00fcsseln, ben\u00f6tigen wir einen 24-stelligen hexadezimalen Bindungsschl\u00fcssel." + }, "user": { "data": { "address": "Ger\u00e4t" diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json index d24df64f135..b9f4e024f92 100644 --- a/homeassistant/components/xiaomi_ble/translations/en.json +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", + "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", + "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", + "expected_32_characters": "Expected a 32 character hexadecimal bindkey.", "no_devices_found": "No devices found on the network" }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 32 character hexadecimal bindkey." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey." + }, "user": { "data": { "address": "Device" diff --git a/homeassistant/components/xiaomi_ble/translations/et.json b/homeassistant/components/xiaomi_ble/translations/et.json new file mode 100644 index 00000000000..749f7e45de5 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "no_devices_found": "V\u00f6rgust seadmeid ei leitud" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/id.json b/homeassistant/components/xiaomi_ble/translations/id.json index 8b24900933b..07426a0e290 100644 --- a/homeassistant/components/xiaomi_ble/translations/id.json +++ b/homeassistant/components/xiaomi_ble/translations/id.json @@ -1,7 +1,21 @@ { "config": { "abort": { - "already_in_progress": "Alur konfigurasi sedang berlangsung" + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/it.json b/homeassistant/components/xiaomi_ble/translations/it.json new file mode 100644 index 00000000000..99adacee466 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/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", + "decryption_failed": "La chiave di collegamento fornita non funziona, i dati del sensore non possono essere decifrati. Controllare e riprovare.", + "expected_24_characters": "Prevista una chiave di collegamento esadecimale di 24 caratteri.", + "expected_32_characters": "Prevista una chiave di collegamento esadecimale di 32 caratteri.", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Chiave di collegamento" + }, + "description": "I dati trasmessi dal sensore sono criptati. Per decifrarli \u00e8 necessaria una chiave di collegamento esadecimale di 32 caratteri." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Chiave di collegamento" + }, + "description": "I dati trasmessi dal sensore sono criptati. Per decifrarli \u00e8 necessaria una chiave di collegamento esadecimale di 24 caratteri." + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/ja.json b/homeassistant/components/xiaomi_ble/translations/ja.json new file mode 100644 index 00000000000..38f862bd2f6 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/ja.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/pt-BR.json b/homeassistant/components/xiaomi_ble/translations/pt-BR.json new file mode 100644 index 00000000000..21c251bf0eb --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/pt-BR.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "decryption_failed": "A bindkey fornecida n\u00e3o funcionou, os dados do sensor n\u00e3o puderam ser descriptografados. Por favor verifique e tente novamente.", + "expected_24_characters": "Espera-se uma bindkey hexadecimal de 24 caracteres.", + "expected_32_characters": "Esperado um bindkey hexadecimal de 32 caracteres.", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Os dados do sensor transmitidos pelo sensor s\u00e3o criptografados. Para decifr\u00e1-lo, precisamos de uma chave de liga\u00e7\u00e3o hexadecimal de 32 caracteres." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Os dados do sensor transmitidos pelo sensor s\u00e3o criptografados. Para decifr\u00e1-lo, precisamos de uma bindkey hexadecimal de 24 caracteres." + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/zh-Hant.json b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json new file mode 100644 index 00000000000..d4eaa8cb41f --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index 16ab4a84b6d..567ee5f359c 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "Arvesta, et v\u00f5rgutoitega seadmed pole p\u00e4rast (sekundit) saadaval", "default_light_transition": "Heleduse vaike\u00fclemineku aeg (sekundites)", "enable_identify_on_join": "Luba tuvastamine kui seadmed liituvad v\u00f5rguga", + "enhanced_light_transition": "Luba t\u00e4iustatud valguse v\u00e4rvi/temperatuuri \u00fcleminek v\u00e4ljal\u00fclitatud olekust", "title": "\u00dcldised valikud" } }, diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 57be4c7acb6..30c85de48da 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "Considera i dispositivi alimentati dalla rete non disponibili dopo (secondi)", "default_light_transition": "Tempo di transizione della luce predefinito (secondi)", "enable_identify_on_join": "Abilita l'effetto di identificazione quando i dispositivi si uniscono alla rete", + "enhanced_light_transition": "Abilita una transizione migliorata del colore/temperatura della luce da uno stato spento", "title": "Opzioni globali" } }, diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 9e8289960f8..14ba0b9280f 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -50,6 +50,7 @@ "consider_unavailable_mains": "(\u79d2)\u5f8c\u306b\u4e3b\u96fb\u6e90\u304c\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3068\u898b\u306a\u3059", "default_light_transition": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30e9\u30a4\u30c8\u9077\u79fb\u6642\u9593(\u79d2)", "enable_identify_on_join": "\u30c7\u30d0\u30a4\u30b9\u304c\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u53c2\u52a0\u3059\u308b\u969b\u306b\u3001\u8b58\u5225\u52b9\u679c\u3092\u6709\u52b9\u306b\u3059\u308b", + "enhanced_light_transition": "\u30aa\u30d5\u72b6\u614b\u304b\u3089\u3001\u30a8\u30f3\u30cf\u30f3\u30b9\u30c9\u30e9\u30a4\u30c8\u30ab\u30e9\u30fc/\u8272\u6e29\u5ea6\u3078\u306e\u9077\u79fb\u3092\u6709\u52b9\u306b\u3057\u307e\u3059", "title": "\u30b0\u30ed\u30fc\u30d0\u30eb\u30aa\u30d7\u30b7\u30e7\u30f3" } }, From f31f2cca07135d7b20452cfc5db59f92dc4fa9a2 Mon Sep 17 00:00:00 2001 From: Pedro Lamas Date: Mon, 25 Jul 2022 11:03:56 +0100 Subject: [PATCH 2830/3516] Use DataUpdateCoordinator in london_underground (#75304) * Use DataUpdateCoordinator in london_underground * Update homeassistant/components/london_underground/sensor.py Co-authored-by: avee87 <6134677+avee87@users.noreply.github.com> * Follow up on PR comments * Removes unused callback import * Update homeassistant/components/london_underground/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/london_underground/sensor.py Co-authored-by: Martin Hjelmare * Adds missing PlatformNotReady import * Linting fixes Co-authored-by: avee87 <6134677+avee87@users.noreply.github.com> Co-authored-by: Martin Hjelmare --- .../components/london_underground/sensor.py | 59 ++++++++++++++----- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/london_underground/sensor.py b/homeassistant/components/london_underground/sensor.py index a4cc66a8447..96ff9bc5056 100644 --- a/homeassistant/components/london_underground/sensor.py +++ b/homeassistant/components/london_underground/sensor.py @@ -2,17 +2,28 @@ from __future__ import annotations from datetime import timedelta +import logging +import async_timeout from london_tube_status import TubeData import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "london_underground" ATTRIBUTION = "Powered by TfL Open Data" @@ -55,24 +66,46 @@ async def async_setup_platform( session = async_get_clientsession(hass) data = TubeData(session) - await data.update() + coordinator = LondonTubeCoordinator(hass, data) + + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise PlatformNotReady sensors = [] for line in config[CONF_LINE]: - sensors.append(LondonTubeSensor(line, data)) + sensors.append(LondonTubeSensor(coordinator, line)) - async_add_entities(sensors, True) + async_add_entities(sensors) -class LondonTubeSensor(SensorEntity): +class LondonTubeCoordinator(DataUpdateCoordinator): + """London Underground sensor coordinator.""" + + def __init__(self, hass, data): + """Initialize coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + self._data = data + + async def _async_update_data(self): + async with async_timeout.timeout(10): + await self._data.update() + return self._data.data + + +class LondonTubeSensor(CoordinatorEntity[LondonTubeCoordinator], SensorEntity): """Sensor that reads the status of a line from Tube Data.""" - def __init__(self, name, data): + def __init__(self, coordinator, name): """Initialize the London Underground sensor.""" - self._data = data - self._description = None + super().__init__(coordinator) self._name = name - self._state = None self.attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} @property @@ -83,7 +116,7 @@ class LondonTubeSensor(SensorEntity): @property def native_value(self): """Return the state of the sensor.""" - return self._state + return self.coordinator.data[self.name]["State"] @property def icon(self): @@ -93,11 +126,5 @@ class LondonTubeSensor(SensorEntity): @property def extra_state_attributes(self): """Return other details about the sensor state.""" - self.attrs["Description"] = self._description + self.attrs["Description"] = self.coordinator.data[self.name]["Description"] return self.attrs - - async def async_update(self): - """Update the sensor.""" - await self._data.update() - self._state = self._data.data[self.name]["State"] - self._description = self._data.data[self.name]["Description"] From fc9a0ba46bbbf8f7d191569c5a81d74c9dd732ff Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 25 Jul 2022 20:20:15 +1000 Subject: [PATCH 2831/3516] Refactor Advantage Air classes for expansion (#75422) --- .../components/advantage_air/binary_sensor.py | 20 ++++-------- .../components/advantage_air/climate.py | 18 +++++------ .../components/advantage_air/cover.py | 7 ++-- .../components/advantage_air/entity.py | 32 +++++++++++++++---- .../components/advantage_air/select.py | 8 ++--- .../components/advantage_air/sensor.py | 26 ++++++--------- .../components/advantage_air/switch.py | 8 ++--- 7 files changed, 57 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/advantage_air/binary_sensor.py b/homeassistant/components/advantage_air/binary_sensor.py index 9fc53d7e1dc..c0934239fe7 100644 --- a/homeassistant/components/advantage_air/binary_sensor.py +++ b/homeassistant/components/advantage_air/binary_sensor.py @@ -11,7 +11,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN -from .entity import AdvantageAirEntity +from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity PARALLEL_UPDATES = 0 @@ -38,7 +38,7 @@ async def async_setup_entry( async_add_entities(entities) -class AdvantageAirFilter(AdvantageAirEntity, BinarySensorEntity): +class AdvantageAirFilter(AdvantageAirAcEntity, BinarySensorEntity): """Advantage Air Filter sensor.""" _attr_device_class = BinarySensorDeviceClass.PROBLEM @@ -48,9 +48,7 @@ class AdvantageAirFilter(AdvantageAirEntity, BinarySensorEntity): def __init__(self, instance, ac_key): """Initialize an Advantage Air Filter sensor.""" super().__init__(instance, ac_key) - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-filter' - ) + self._attr_unique_id += "-filter" @property def is_on(self): @@ -58,7 +56,7 @@ class AdvantageAirFilter(AdvantageAirEntity, BinarySensorEntity): return self._ac["filterCleanStatus"] -class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): +class AdvantageAirZoneMotion(AdvantageAirZoneEntity, BinarySensorEntity): """Advantage Air Zone Motion sensor.""" _attr_device_class = BinarySensorDeviceClass.MOTION @@ -67,9 +65,7 @@ class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): """Initialize an Advantage Air Zone Motion sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} motion' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-motion' - ) + self._attr_unique_id += "-motion" @property def is_on(self): @@ -77,7 +73,7 @@ class AdvantageAirZoneMotion(AdvantageAirEntity, BinarySensorEntity): return self._zone["motion"] == 20 -class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): +class AdvantageAirZoneMyZone(AdvantageAirZoneEntity, BinarySensorEntity): """Advantage Air Zone MyZone sensor.""" _attr_entity_registry_enabled_default = False @@ -87,9 +83,7 @@ class AdvantageAirZoneMyZone(AdvantageAirEntity, BinarySensorEntity): """Initialize an Advantage Air Zone MyZone sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} myZone' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-myzone' - ) + self._attr_unique_id += "-myzone" @property def is_on(self): diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index 1d89c313579..db060e739b1 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -25,7 +25,7 @@ from .const import ( ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN, ) -from .entity import AdvantageAirEntity +from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity ADVANTAGE_AIR_HVAC_MODES = { "heat": HVACMode.HEAT, @@ -87,18 +87,13 @@ async def async_setup_entry( ) -class AdvantageAirClimateEntity(AdvantageAirEntity, ClimateEntity): - """AdvantageAir Climate class.""" +class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity): + """AdvantageAir AC unit.""" _attr_temperature_unit = TEMP_CELSIUS _attr_target_temperature_step = PRECISION_WHOLE _attr_max_temp = 32 _attr_min_temp = 16 - - -class AdvantageAirAC(AdvantageAirClimateEntity): - """AdvantageAir AC unit.""" - _attr_fan_modes = [FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH] _attr_hvac_modes = AC_HVAC_MODES _attr_supported_features = ( @@ -108,7 +103,6 @@ class AdvantageAirAC(AdvantageAirClimateEntity): def __init__(self, instance, ac_key): """Initialize an AdvantageAir AC unit.""" super().__init__(instance, ac_key) - self._attr_unique_id = f'{self.coordinator.data["system"]["rid"]}-{ac_key}' if self._ac.get("myAutoModeEnabled"): self._attr_hvac_modes = AC_HVAC_MODES + [HVACMode.AUTO] @@ -159,9 +153,13 @@ class AdvantageAirAC(AdvantageAirClimateEntity): await self.async_change({self.ac_key: {"info": {"setTemp": temp}}}) -class AdvantageAirZone(AdvantageAirClimateEntity): +class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): """AdvantageAir Zone control.""" + _attr_temperature_unit = TEMP_CELSIUS + _attr_target_temperature_step = PRECISION_WHOLE + _attr_max_temp = 32 + _attr_min_temp = 16 _attr_hvac_modes = ZONE_HVAC_MODES _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE diff --git a/homeassistant/components/advantage_air/cover.py b/homeassistant/components/advantage_air/cover.py index 391f39953d2..4b3f371f52e 100644 --- a/homeassistant/components/advantage_air/cover.py +++ b/homeassistant/components/advantage_air/cover.py @@ -16,7 +16,7 @@ from .const import ( ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN, ) -from .entity import AdvantageAirEntity +from .entity import AdvantageAirZoneEntity PARALLEL_UPDATES = 0 @@ -39,7 +39,7 @@ async def async_setup_entry( async_add_entities(entities) -class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): +class AdvantageAirZoneVent(AdvantageAirZoneEntity, CoverEntity): """Advantage Air Zone Vent.""" _attr_device_class = CoverDeviceClass.DAMPER @@ -53,9 +53,6 @@ class AdvantageAirZoneVent(AdvantageAirEntity, CoverEntity): """Initialize an Advantage Air Zone Vent.""" super().__init__(instance, ac_key, zone_key) self._attr_name = self._zone["name"] - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}' - ) @property def is_closed(self) -> bool: diff --git a/homeassistant/components/advantage_air/entity.py b/homeassistant/components/advantage_air/entity.py index 6c434518656..375bfa255c4 100644 --- a/homeassistant/components/advantage_air/entity.py +++ b/homeassistant/components/advantage_air/entity.py @@ -11,26 +11,44 @@ class AdvantageAirEntity(CoordinatorEntity): _attr_has_entity_name = True - def __init__(self, instance, ac_key, zone_key=None): - """Initialize common aspects of an Advantage Air sensor.""" + def __init__(self, instance): + """Initialize common aspects of an Advantage Air entity.""" super().__init__(instance["coordinator"]) + self._attr_unique_id = self.coordinator.data["system"]["rid"] + + +class AdvantageAirAcEntity(AdvantageAirEntity): + """Parent class for Advantage Air AC Entities.""" + + def __init__(self, instance, ac_key): + """Initialize common aspects of an Advantage Air ac entity.""" + super().__init__(instance) self.async_change = instance["async_change"] self.ac_key = ac_key - self.zone_key = zone_key + self._attr_unique_id += f"-{ac_key}" + self._attr_device_info = DeviceInfo( via_device=(DOMAIN, self.coordinator.data["system"]["rid"]), - identifiers={ - (DOMAIN, f"{self.coordinator.data['system']['rid']}_{ac_key}") - }, + identifiers={(DOMAIN, self._attr_unique_id)}, manufacturer="Advantage Air", model=self.coordinator.data["system"]["sysType"], - name=self._ac["name"], + name=self.coordinator.data["aircons"][self.ac_key]["info"]["name"], ) @property def _ac(self): return self.coordinator.data["aircons"][self.ac_key]["info"] + +class AdvantageAirZoneEntity(AdvantageAirAcEntity): + """Parent class for Advantage Air Zone Entities.""" + + def __init__(self, instance, ac_key, zone_key): + """Initialize common aspects of an Advantage Air zone entity.""" + super().__init__(instance, ac_key) + self.zone_key = zone_key + self._attr_unique_id += f"-{zone_key}" + @property def _zone(self): return self.coordinator.data["aircons"][self.ac_key]["zones"][self.zone_key] diff --git a/homeassistant/components/advantage_air/select.py b/homeassistant/components/advantage_air/select.py index 5dfa92c10ad..9cfece25b24 100644 --- a/homeassistant/components/advantage_air/select.py +++ b/homeassistant/components/advantage_air/select.py @@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN -from .entity import AdvantageAirEntity +from .entity import AdvantageAirAcEntity ADVANTAGE_AIR_INACTIVE = "Inactive" @@ -25,7 +25,7 @@ async def async_setup_entry( async_add_entities(entities) -class AdvantageAirMyZone(AdvantageAirEntity, SelectEntity): +class AdvantageAirMyZone(AdvantageAirAcEntity, SelectEntity): """Representation of Advantage Air MyZone control.""" _attr_icon = "mdi:home-thermometer" @@ -37,9 +37,7 @@ class AdvantageAirMyZone(AdvantageAirEntity, SelectEntity): def __init__(self, instance, ac_key): """Initialize an Advantage Air MyZone control.""" super().__init__(instance, ac_key) - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-myzone' - ) + self._attr_unique_id += "-myzone" for zone in instance["coordinator"].data["aircons"][ac_key]["zones"].values(): if zone["type"] > 0: diff --git a/homeassistant/components/advantage_air/sensor.py b/homeassistant/components/advantage_air/sensor.py index 370aab7b292..b110294b2fd 100644 --- a/homeassistant/components/advantage_air/sensor.py +++ b/homeassistant/components/advantage_air/sensor.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ADVANTAGE_AIR_STATE_OPEN, DOMAIN as ADVANTAGE_AIR_DOMAIN -from .entity import AdvantageAirEntity +from .entity import AdvantageAirAcEntity, AdvantageAirZoneEntity ADVANTAGE_AIR_SET_COUNTDOWN_VALUE = "minutes" ADVANTAGE_AIR_SET_COUNTDOWN_UNIT = "min" @@ -56,7 +56,7 @@ async def async_setup_entry( ) -class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): +class AdvantageAirTimeTo(AdvantageAirAcEntity, SensorEntity): """Representation of Advantage Air timer control.""" _attr_native_unit_of_measurement = ADVANTAGE_AIR_SET_COUNTDOWN_UNIT @@ -68,9 +68,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): self.action = action self._time_key = f"countDownTo{action}" self._attr_name = f"Time to {action}" - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{self.ac_key}-timeto{action}' - ) + self._attr_unique_id += f"-timeto{action}" @property def native_value(self): @@ -90,7 +88,7 @@ class AdvantageAirTimeTo(AdvantageAirEntity, SensorEntity): await self.async_change({self.ac_key: {"info": {self._time_key: value}}}) -class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): +class AdvantageAirZoneVent(AdvantageAirZoneEntity, SensorEntity): """Representation of Advantage Air Zone Vent Sensor.""" _attr_native_unit_of_measurement = PERCENTAGE @@ -101,9 +99,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): """Initialize an Advantage Air Zone Vent Sensor.""" super().__init__(instance, ac_key, zone_key=zone_key) self._attr_name = f'{self._zone["name"]} vent' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-vent' - ) + self._attr_unique_id += "-vent" @property def native_value(self): @@ -120,7 +116,7 @@ class AdvantageAirZoneVent(AdvantageAirEntity, SensorEntity): return "mdi:fan-off" -class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): +class AdvantageAirZoneSignal(AdvantageAirZoneEntity, SensorEntity): """Representation of Advantage Air Zone wireless signal sensor.""" _attr_native_unit_of_measurement = PERCENTAGE @@ -131,9 +127,7 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): """Initialize an Advantage Air Zone wireless signal sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} signal' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-signal' - ) + self._attr_unique_id += "-signal" @property def native_value(self): @@ -154,7 +148,7 @@ class AdvantageAirZoneSignal(AdvantageAirEntity, SensorEntity): return "mdi:wifi-strength-outline" -class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity): +class AdvantageAirZoneTemp(AdvantageAirZoneEntity, SensorEntity): """Representation of Advantage Air Zone temperature sensor.""" _attr_native_unit_of_measurement = TEMP_CELSIUS @@ -167,9 +161,7 @@ class AdvantageAirZoneTemp(AdvantageAirEntity, SensorEntity): """Initialize an Advantage Air Zone Temp Sensor.""" super().__init__(instance, ac_key, zone_key) self._attr_name = f'{self._zone["name"]} temperature' - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-{zone_key}-temp' - ) + self._attr_unique_id += "-temp" @property def native_value(self): diff --git a/homeassistant/components/advantage_air/switch.py b/homeassistant/components/advantage_air/switch.py index 504578c72e2..d9d46427599 100644 --- a/homeassistant/components/advantage_air/switch.py +++ b/homeassistant/components/advantage_air/switch.py @@ -9,7 +9,7 @@ from .const import ( ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN, ) -from .entity import AdvantageAirEntity +from .entity import AdvantageAirAcEntity async def async_setup_entry( @@ -28,7 +28,7 @@ async def async_setup_entry( async_add_entities(entities) -class AdvantageAirFreshAir(AdvantageAirEntity, SwitchEntity): +class AdvantageAirFreshAir(AdvantageAirAcEntity, SwitchEntity): """Representation of Advantage Air fresh air control.""" _attr_icon = "mdi:air-filter" @@ -37,9 +37,7 @@ class AdvantageAirFreshAir(AdvantageAirEntity, SwitchEntity): def __init__(self, instance, ac_key): """Initialize an Advantage Air fresh air control.""" super().__init__(instance, ac_key) - self._attr_unique_id = ( - f'{self.coordinator.data["system"]["rid"]}-{ac_key}-freshair' - ) + self._attr_unique_id += "-freshair" @property def is_on(self): From de46243ce5fd90b392e8f8511eecef159ee42c97 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 25 Jul 2022 12:31:53 +0200 Subject: [PATCH 2832/3516] Raise YAML deprecation issue for Radio Therm (#75513) --- homeassistant/components/radiotherm/climate.py | 12 +++++++++++- homeassistant/components/radiotherm/manifest.json | 1 + homeassistant/components/radiotherm/strings.json | 6 ++++++ .../components/radiotherm/translations/en.json | 6 ++++++ 4 files changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index 3f8e87e74a4..c466a7108e8 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -18,6 +18,7 @@ from homeassistant.components.climate.const import ( HVACAction, HVACMode, ) +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, @@ -125,13 +126,22 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Radio Thermostat.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) _LOGGER.warning( - # config flow added in 2022.7 and should be removed in 2022.9 "Configuration of the Radio Thermostat climate platform in YAML is deprecated and " "will be removed in Home Assistant 2022.9; Your existing configuration " "has been imported into the UI automatically and can be safely removed " "from your configuration.yaml file" ) + hosts: list[str] = [] if CONF_HOST in config: hosts = config[CONF_HOST] diff --git a/homeassistant/components/radiotherm/manifest.json b/homeassistant/components/radiotherm/manifest.json index c6ae4e5bb06..5c37b4e3cde 100644 --- a/homeassistant/components/radiotherm/manifest.json +++ b/homeassistant/components/radiotherm/manifest.json @@ -3,6 +3,7 @@ "name": "Radio Thermostat", "documentation": "https://www.home-assistant.io/integrations/radiotherm", "requirements": ["radiotherm==2.1.0"], + "dependencies": ["repairs"], "codeowners": ["@bdraco", "@vinnyfuria"], "iot_class": "local_polling", "loggers": ["radiotherm"], diff --git a/homeassistant/components/radiotherm/strings.json b/homeassistant/components/radiotherm/strings.json index 22f17224285..51505d4d727 100644 --- a/homeassistant/components/radiotherm/strings.json +++ b/homeassistant/components/radiotherm/strings.json @@ -19,6 +19,12 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, + "issues": { + "deprecated_yaml": { + "title": "The Radio Thermostat YAML configuration is being removed", + "description": "Configuring the Radio Thermostat climate platform using YAML is being removed in Home Assistant 2022.9.\n\nYour existing configuration has been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/en.json b/homeassistant/components/radiotherm/translations/en.json index b524f188e59..224c8ffeb3c 100644 --- a/homeassistant/components/radiotherm/translations/en.json +++ b/homeassistant/components/radiotherm/translations/en.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Configuring the Radio Thermostat climate platform using YAML is being removed in Home Assistant 2022.9.\n\nYour existing configuration has been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Radio Thermostat YAML configuration is being removed" + } + }, "options": { "step": { "init": { From b8ae883f1813240ec58f0b3e9a2e03d22af0abdd Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Mon, 25 Jul 2022 14:13:01 +0200 Subject: [PATCH 2833/3516] Set min transition time for Sengled lights in ZHA groups (#75644) * Set min transition time for Sengled lights in ZHA groups * Change test to expect correct min transition time for group with Sengled light * Fix turn_off with transition 0 for Sengled lights --- homeassistant/components/zha/light.py | 13 +++++++++---- tests/components/zha/test_light.py | 10 +++++++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 6c26e17188b..190e0b2319d 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -74,6 +74,7 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.LIGHT) GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.LIGHT) PARALLEL_UPDATES = 0 SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed" +DEFAULT_MIN_TRANSITION_MANUFACTURERS = {"Sengled"} COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.XY} SUPPORT_GROUP_LIGHT = ( @@ -379,7 +380,7 @@ class BaseLight(LogMixin, light.LightEntity): # is not none looks odd here but it will override built in bulb transition times if we pass 0 in here if transition is not None and supports_level: result = await self._level_channel.move_to_level_with_on_off( - 0, transition * 10 + 0, transition * 10 or self._DEFAULT_MIN_TRANSITION_TIME ) else: result = await self._on_off_channel.off() @@ -670,10 +671,10 @@ class ForceOnLight(Light): @STRICT_MATCH( channel_names=CHANNEL_ON_OFF, aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}, - manufacturers={"Sengled"}, + manufacturers=DEFAULT_MIN_TRANSITION_MANUFACTURERS, ) -class SengledLight(Light): - """Representation of a Sengled light which does not react to move_to_color_temp with 0 as a transition.""" +class MinTransitionLight(Light): + """Representation of a light which does not react to any "move to" calls with 0 as a transition.""" _DEFAULT_MIN_TRANSITION_TIME = 1 @@ -688,6 +689,10 @@ class LightGroup(BaseLight, ZhaGroupEntity): """Initialize a light group.""" super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) group = self.zha_device.gateway.get_group(self._group_id) + self._DEFAULT_MIN_TRANSITION_TIME = any( # pylint: disable=invalid-name + member.device.manufacturer in DEFAULT_MIN_TRANSITION_MANUFACTURERS + for member in group.members + ) self._on_off_channel = group.endpoint[OnOff.cluster_id] self._level_channel = group.endpoint[LevelControl.cluster_id] self._color_channel = group.endpoint[Color.cluster_id] diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index c5b02f1a02f..94f0c96c38d 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -1184,7 +1184,7 @@ async def async_test_off_from_hass(hass, cluster, entity_id): async def async_test_level_on_off_from_hass( - hass, on_off_cluster, level_cluster, entity_id + hass, on_off_cluster, level_cluster, entity_id, expected_default_transition: int = 0 ): """Test on off functionality from hass.""" @@ -1259,7 +1259,7 @@ async def async_test_level_on_off_from_hass( 4, level_cluster.commands_by_name["move_to_level_with_on_off"].schema, 10, - 0, + expected_default_transition, expect_reply=True, manufacturer=None, tries=1, @@ -1417,7 +1417,11 @@ async def test_zha_group_light_entity( # test turning the lights on and off from the HA await async_test_level_on_off_from_hass( - hass, group_cluster_on_off, group_cluster_level, group_entity_id + hass, + group_cluster_on_off, + group_cluster_level, + group_entity_id, + expected_default_transition=1, # a Sengled light is in that group and needs a minimum 0.1s transition ) # test getting a brightness change from the network From a813cf987bf11bd9c0ee6aea04d2299f39b26e07 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 25 Jul 2022 09:52:35 -0500 Subject: [PATCH 2834/3516] Add bluetooth options flow to pick the adapter (#75701) --- .../components/bluetooth/__init__.py | 53 +++++--- .../components/bluetooth/config_flow.py | 44 ++++++- homeassistant/components/bluetooth/const.py | 7 ++ .../components/bluetooth/manifest.json | 2 +- homeassistant/components/bluetooth/match.py | 4 + homeassistant/components/bluetooth/models.py | 6 + .../components/bluetooth/strings.json | 12 +- .../components/bluetooth/translations/en.json | 12 +- homeassistant/components/bluetooth/util.py | 27 ++++ homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/bluetooth/test_config_flow.py | 115 +++++++++++++++++- tests/components/bluetooth/test_init.py | 59 ++++++++- tests/conftest.py | 2 +- 15 files changed, 318 insertions(+), 31 deletions(-) create mode 100644 homeassistant/components/bluetooth/util.py diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 9266603839c..551e93d5bd9 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -6,7 +6,6 @@ from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum import logging -import platform from typing import Final, Union from bleak import BleakError @@ -29,7 +28,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_bluetooth from . import models -from .const import DOMAIN +from .const import CONF_ADAPTER, DEFAULT_ADAPTERS, DOMAIN from .match import ( ADDRESS, BluetoothCallbackMatcher, @@ -38,6 +37,7 @@ from .match import ( ) from .models import HaBleakScanner, HaBleakScannerWrapper from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher +from .util import async_get_bluetooth_adapters _LOGGER = logging.getLogger(__name__) @@ -175,15 +175,7 @@ def async_track_unavailable( async def _async_has_bluetooth_adapter() -> bool: """Return if the device has a bluetooth adapter.""" - if platform.system() == "Darwin": # CoreBluetooth is built in on MacOS hardware - return True - if platform.system() == "Windows": # We don't have a good way to detect on windows - return False - from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel - get_bluetooth_adapters, - ) - - return bool(await get_bluetooth_adapters()) + return bool(await async_get_bluetooth_adapters()) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -219,10 +211,22 @@ async def async_setup_entry( ) -> bool: """Set up the bluetooth integration from a config entry.""" manager: BluetoothManager = hass.data[DOMAIN] - await manager.async_start(BluetoothScanningMode.ACTIVE) + await manager.async_start( + BluetoothScanningMode.ACTIVE, entry.options.get(CONF_ADAPTER) + ) + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True +async def _async_update_listener( + hass: HomeAssistant, entry: config_entries.ConfigEntry +) -> None: + """Handle options update.""" + manager: BluetoothManager = hass.data[DOMAIN] + manager.async_start_reload() + await hass.config_entries.async_reload(entry.entry_id) + + async def async_unload_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: @@ -250,6 +254,7 @@ class BluetoothManager: self._callbacks: list[ tuple[BluetoothCallback, BluetoothCallbackMatcher | None] ] = [] + self._reloading = False @hass_callback def async_setup(self) -> None: @@ -261,13 +266,29 @@ class BluetoothManager: """Get the scanner.""" return HaBleakScannerWrapper() - async def async_start(self, scanning_mode: BluetoothScanningMode) -> None: + @hass_callback + def async_start_reload(self) -> None: + """Start reloading.""" + self._reloading = True + + async def async_start( + self, scanning_mode: BluetoothScanningMode, adapter: str | None + ) -> None: """Set up BT Discovery.""" assert self.scanner is not None + if self._reloading: + # On reload, we need to reset the scanner instance + # since the devices in its history may not be reachable + # anymore. + self.scanner.async_reset() + self._integration_matcher.async_clear_history() + self._reloading = False + scanner_kwargs = {"scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode]} + if adapter and adapter not in DEFAULT_ADAPTERS: + scanner_kwargs["adapter"] = adapter + _LOGGER.debug("Initializing bluetooth scanner with %s", scanner_kwargs) try: - self.scanner.async_setup( - scanning_mode=SCANNING_MODE_TO_BLEAK[scanning_mode] - ) + self.scanner.async_setup(**scanner_kwargs) except (FileNotFoundError, BleakError) as ex: raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex install_multiple_bleak_catcher() diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index 8fe01be769d..bbba5f411b2 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -3,11 +3,15 @@ from __future__ import annotations from typing import Any +import voluptuous as vol + from homeassistant.components import onboarding -from homeassistant.config_entries import ConfigFlow +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from .const import DEFAULT_NAME, DOMAIN +from .const import CONF_ADAPTER, DEFAULT_NAME, DOMAIN +from .util import async_get_bluetooth_adapters class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): @@ -36,3 +40,39 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: """Handle import from configuration.yaml.""" return await self.async_step_enable_bluetooth(user_input) + + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> OptionsFlowHandler: + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(OptionsFlow): + """Handle the option flow for bluetooth.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + if not (adapters := await async_get_bluetooth_adapters()): + return self.async_abort(reason="no_adapters") + + data_schema = vol.Schema( + { + vol.Required( + CONF_ADAPTER, + default=self.config_entry.options.get(CONF_ADAPTER, adapters[0]), + ): vol.In(adapters), + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index 1e577f6064a..f3f00f581ee 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -2,3 +2,10 @@ DOMAIN = "bluetooth" DEFAULT_NAME = "Bluetooth" + +CONF_ADAPTER = "adapter" + +MACOS_DEFAULT_BLUETOOTH_ADAPTER = "CoreBluetooth" +UNIX_DEFAULT_BLUETOOTH_ADAPTER = "hci0" + +DEFAULT_ADAPTERS = {MACOS_DEFAULT_BLUETOOTH_ADAPTER, UNIX_DEFAULT_BLUETOOTH_ADAPTER} diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 551bb1c3733..c6ca8b11400 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.14.3", "bluetooth-adapters==0.1.1"], + "requirements": ["bleak==0.14.3", "bluetooth-adapters==0.1.2"], "codeowners": ["@bdraco"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index c4560287feb..000f39eefd4 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -70,6 +70,10 @@ class IntegrationMatcher: MAX_REMEMBER_ADDRESSES ) + def async_clear_history(self) -> None: + """Clear the history.""" + self._matched = {} + def match_domains(self, device: BLEDevice, adv_data: AdvertisementData) -> set[str]: """Return the domains that are matched.""" matched_domains: set[str] = set() diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index e1d15c27243..408e0698879 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -67,6 +67,12 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] super().__init__(*args, **kwargs) self._setup = True + @hass_callback + def async_reset(self) -> None: + """Reset the scanner so it can be setup again.""" + self.history = {} + self._setup = False + @hass_callback def async_register_callback( self, callback: AdvertisementDataCallback, filters: dict[str, set[str]] diff --git a/homeassistant/components/bluetooth/strings.json b/homeassistant/components/bluetooth/strings.json index 328a001ad96..beff2fd8312 100644 --- a/homeassistant/components/bluetooth/strings.json +++ b/homeassistant/components/bluetooth/strings.json @@ -16,7 +16,17 @@ } }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "no_adapters": "No Bluetooth adapters found" + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "The Bluetooth Adapter to use for scanning" + } + } } } } diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json index 85019bdd689..4b53822b771 100644 --- a/homeassistant/components/bluetooth/translations/en.json +++ b/homeassistant/components/bluetooth/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Service is already configured" + "already_configured": "Service is already configured", + "no_adapters": "No Bluetooth adapters found" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "Choose a device to setup" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "The Bluetooth Adapter to use for scanning" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py new file mode 100644 index 00000000000..68920050748 --- /dev/null +++ b/homeassistant/components/bluetooth/util.py @@ -0,0 +1,27 @@ +"""The bluetooth integration utilities.""" +from __future__ import annotations + +import platform + +from .const import MACOS_DEFAULT_BLUETOOTH_ADAPTER, UNIX_DEFAULT_BLUETOOTH_ADAPTER + + +async def async_get_bluetooth_adapters() -> list[str]: + """Return a list of bluetooth adapters.""" + if platform.system() == "Windows": # We don't have a good way to detect on windows + return [] + if platform.system() == "Darwin": # CoreBluetooth is built in on MacOS hardware + return [MACOS_DEFAULT_BLUETOOTH_ADAPTER] + from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel + get_bluetooth_adapters, + ) + + adapters = await get_bluetooth_adapters() + if ( + UNIX_DEFAULT_BLUETOOTH_ADAPTER in adapters + and adapters[0] != UNIX_DEFAULT_BLUETOOTH_ADAPTER + ): + # The default adapter always needs to be the first in the list + # because that is how bleak works. + adapters.insert(0, adapters.pop(adapters.index(UNIX_DEFAULT_BLUETOOTH_ADAPTER))) + return adapters diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 568ba909ba5..3a6ae411c5e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 bleak==0.14.3 -bluetooth-adapters==0.1.1 +bluetooth-adapters==0.1.2 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==36.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index 3a39b405c91..64ccf9a0763 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -425,7 +425,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.1.1 +bluetooth-adapters==0.1.2 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e9f699d61d8..03538ea10cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ blebox_uniapi==2.0.2 blinkpy==0.19.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.1.1 +bluetooth-adapters==0.1.2 # homeassistant.components.bond bond-async==0.1.22 diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index 5c6199b9bf0..1053133cac9 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -3,7 +3,11 @@ from unittest.mock import patch from homeassistant import config_entries -from homeassistant.components.bluetooth.const import DOMAIN +from homeassistant.components.bluetooth.const import ( + CONF_ADAPTER, + DOMAIN, + MACOS_DEFAULT_BLUETOOTH_ADAPTER, +) from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -125,3 +129,112 @@ async def test_async_step_import_already_exists(hass): ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" + + +@patch("homeassistant.components.bluetooth.util.platform.system", return_value="Linux") +async def test_options_flow_linux(mock_system, hass, mock_bleak_scanner_start): + """Test options on Linux.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={}, + unique_id="DOMAIN", + ) + entry.add_to_hass(hass) + + # Verify we can keep it as hci0 + with patch( + "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci0", "hci1"] + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] is None + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_ADAPTER: "hci0", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"][CONF_ADAPTER] == "hci0" + + # Verify we can change it to hci1 + with patch( + "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci0", "hci1"] + ): + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] is None + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_ADAPTER: "hci1", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"][CONF_ADAPTER] == "hci1" + + +@patch("homeassistant.components.bluetooth.util.platform.system", return_value="Darwin") +async def test_options_flow_macos(mock_system, hass, mock_bleak_scanner_start): + """Test options on MacOS.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={}, + unique_id="DOMAIN", + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + assert result["errors"] is None + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_ADAPTER: MACOS_DEFAULT_BLUETOOTH_ADAPTER, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"][CONF_ADAPTER] == MACOS_DEFAULT_BLUETOOTH_ADAPTER + + +@patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Windows" +) +async def test_options_flow_windows(mock_system, hass, mock_bleak_scanner_start): + """Test options on Windows.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={}, + options={}, + unique_id="DOMAIN", + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_adapters" diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index aa3a6253b83..bb2c5f49cc9 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -15,6 +15,10 @@ from homeassistant.components.bluetooth import ( async_track_unavailable, models, ) +from homeassistant.components.bluetooth.const import ( + CONF_ADAPTER, + UNIX_DEFAULT_BLUETOOTH_ADAPTER, +) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback @@ -1160,9 +1164,22 @@ async def test_can_unsetup_bluetooth(hass, mock_bleak_scanner_start, enable_blue async def test_auto_detect_bluetooth_adapters_linux(hass): """Test we auto detect bluetooth adapters on linux.""" with patch( - "bluetooth_adapters.get_bluetooth_adapters", return_value={"hci0"} + "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci0"] ), patch( - "homeassistant.components.bluetooth.platform.system", return_value="Linux" + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ): + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(bluetooth.DOMAIN) + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1 + + +async def test_auto_detect_bluetooth_adapters_linux_multiple(hass): + """Test we auto detect bluetooth adapters on linux with multiple adapters.""" + with patch( + "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci1", "hci0"] + ), patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" ): assert await async_setup_component(hass, bluetooth.DOMAIN, {}) await hass.async_block_till_done() @@ -1173,7 +1190,7 @@ async def test_auto_detect_bluetooth_adapters_linux(hass): async def test_auto_detect_bluetooth_adapters_linux_none_found(hass): """Test we auto detect bluetooth adapters on linux with no adapters found.""" with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=set()), patch( - "homeassistant.components.bluetooth.platform.system", return_value="Linux" + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" ): assert await async_setup_component(hass, bluetooth.DOMAIN, {}) await hass.async_block_till_done() @@ -1184,7 +1201,7 @@ async def test_auto_detect_bluetooth_adapters_linux_none_found(hass): async def test_auto_detect_bluetooth_adapters_macos(hass): """Test we auto detect bluetooth adapters on macos.""" with patch( - "homeassistant.components.bluetooth.platform.system", return_value="Darwin" + "homeassistant.components.bluetooth.util.platform.system", return_value="Darwin" ): assert await async_setup_component(hass, bluetooth.DOMAIN, {}) await hass.async_block_till_done() @@ -1195,7 +1212,8 @@ async def test_auto_detect_bluetooth_adapters_macos(hass): async def test_no_auto_detect_bluetooth_adapters_windows(hass): """Test we auto detect bluetooth adapters on windows.""" with patch( - "homeassistant.components.bluetooth.platform.system", return_value="Windows" + "homeassistant.components.bluetooth.util.platform.system", + return_value="Windows", ): assert await async_setup_component(hass, bluetooth.DOMAIN, {}) await hass.async_block_till_done() @@ -1230,3 +1248,34 @@ async def test_config_entry_can_be_reloaded_when_stop_raises( assert entry.state == ConfigEntryState.LOADED assert "Error stopping scanner" in caplog.text + + +async def test_changing_the_adapter_at_runtime(hass): + """Test we can change the adapter at runtime.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, + data={}, + options={CONF_ADAPTER: UNIX_DEFAULT_BLUETOOTH_ADAPTER}, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.async_setup" + ) as mock_setup, patch( + "homeassistant.components.bluetooth.HaBleakScanner.start" + ), patch( + "homeassistant.components.bluetooth.HaBleakScanner.stop" + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert "adapter" not in mock_setup.mock_calls[0][2] + + entry.options = {CONF_ADAPTER: "hci1"} + + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + assert mock_setup.mock_calls[1][2]["adapter"] == "hci1" + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() diff --git a/tests/conftest.py b/tests/conftest.py index e0f4fb5ab90..50c24df8d44 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -885,7 +885,7 @@ async def mock_enable_bluetooth( @pytest.fixture(name="mock_bluetooth_adapters") def mock_bluetooth_adapters(): """Fixture to mock bluetooth adapters.""" - with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=set()): + with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=[]): yield From e3fb4ceb099bfe77375c5040f16eaa79ceacdcd8 Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 25 Jul 2022 11:31:05 -0400 Subject: [PATCH 2835/3516] Bump AIOAladdinConnect to 0.1.31 (#75721) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 4a04bf69aed..d551b91bce9 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.27"], + "requirements": ["AIOAladdinConnect==0.1.31"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 64ccf9a0763..4cdebd91a2a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.27 +AIOAladdinConnect==0.1.31 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03538ea10cc..83afdad8daa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.27 +AIOAladdinConnect==0.1.31 # homeassistant.components.adax Adax-local==0.1.4 From f4e7436421479d902569c919d8e7279812cd9571 Mon Sep 17 00:00:00 2001 From: hahn-th Date: Mon, 25 Jul 2022 18:15:02 +0200 Subject: [PATCH 2836/3516] Add device HmIP-STE2-PCB to homematicip_cloud (#75369) --- .../homematicip_cloud/manifest.json | 2 +- .../components/homematicip_cloud/sensor.py | 80 ++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../homematicip_cloud/test_device.py | 2 +- .../homematicip_cloud/test_sensor.py | 75 +++++++++++++++ tests/fixtures/homematicip_cloud.json | 94 +++++++++++++++++++ 7 files changed, 253 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index db0833f8114..0d06d595f1b 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==1.0.5"], + "requirements": ["homematicip==1.0.7"], "codeowners": [], "quality_scale": "platinum", "iot_class": "cloud_push", diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 80cdbd351b1..57a8b7bd714 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -17,6 +17,7 @@ from homematicip.aio.device import ( AsyncPlugableSwitchMeasuring, AsyncPresenceDetectorIndoor, AsyncRoomControlDeviceAnalog, + AsyncTemperatureDifferenceSensor2, AsyncTemperatureHumiditySensorDisplay, AsyncTemperatureHumiditySensorOutdoor, AsyncTemperatureHumiditySensorWithoutDisplay, @@ -124,6 +125,10 @@ async def async_setup_entry( entities.append(HomematicipTodayRainSensor(hap, device)) if isinstance(device, AsyncPassageDetector): entities.append(HomematicipPassageDetectorDeltaCounter(hap, device)) + if isinstance(device, AsyncTemperatureDifferenceSensor2): + entities.append(HomematicpTemperatureExternalSensorCh1(hap, device)) + entities.append(HomematicpTemperatureExternalSensorCh2(hap, device)) + entities.append(HomematicpTemperatureExternalSensorDelta(hap, device)) if entities: async_add_entities(entities) @@ -387,6 +392,81 @@ class HomematicipTodayRainSensor(HomematicipGenericEntity, SensorEntity): return LENGTH_MILLIMETERS +class HomematicpTemperatureExternalSensorCh1(HomematicipGenericEntity, SensorEntity): + """Representation of the HomematicIP device HmIP-STE2-PCB.""" + + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__(hap, device, post="Channel 1 Temperature") + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return SensorDeviceClass.TEMPERATURE + + @property + def native_value(self) -> float: + """Return the state.""" + return self._device.temperatureExternalOne + + @property + def native_unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return TEMP_CELSIUS + + +class HomematicpTemperatureExternalSensorCh2(HomematicipGenericEntity, SensorEntity): + """Representation of the HomematicIP device HmIP-STE2-PCB.""" + + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__(hap, device, post="Channel 2 Temperature") + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return SensorDeviceClass.TEMPERATURE + + @property + def native_value(self) -> float: + """Return the state.""" + return self._device.temperatureExternalTwo + + @property + def native_unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return TEMP_CELSIUS + + +class HomematicpTemperatureExternalSensorDelta(HomematicipGenericEntity, SensorEntity): + """Representation of the HomematicIP device HmIP-STE2-PCB.""" + + _attr_state_class = SensorStateClass.MEASUREMENT + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the device.""" + super().__init__(hap, device, post="Delta Temperature") + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return SensorDeviceClass.TEMPERATURE + + @property + def native_value(self) -> float: + """Return the state.""" + return self._device.temperatureExternalDelta + + @property + def native_unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return TEMP_CELSIUS + + class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity, SensorEntity): """Representation of the HomematicIP passage detector delta counter.""" diff --git a/requirements_all.txt b/requirements_all.txt index 4cdebd91a2a..b56c176652a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -846,7 +846,7 @@ home-assistant-frontend==20220707.1 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.5 +homematicip==1.0.7 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 83afdad8daa..8e5146bcfc2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -622,7 +622,7 @@ home-assistant-frontend==20220707.1 homeconnect==0.7.1 # homeassistant.components.homematicip_cloud -homematicip==1.0.5 +homematicip==1.0.7 # homeassistant.components.home_plus_control homepluscontrol==0.0.5 diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 8e3d80ca839..44b91c4ed47 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) == 258 + assert len(mock_hap.hmip_device_by_entity_id) == 262 async def test_hmip_remove_device(hass, default_mock_hap_factory): diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index 34c119595b3..823508d5fee 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -340,6 +340,81 @@ async def test_hmip_today_rain_sensor(hass, default_mock_hap_factory): assert ha_state.state == "14.2" +async def test_hmip_temperature_external_sensor_channel_1( + hass, default_mock_hap_factory +): + """Test HomematicipTemperatureDifferenceSensor Channel 1 HmIP-STE2-PCB.""" + entity_id = "sensor.ste2_channel_1_temperature" + entity_name = "STE2 Channel 1 Temperature" + device_model = "HmIP-STE2-PCB" + + mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_devices=["STE2"]) + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + hmip_device = mock_hap.hmip_device_by_entity_id.get(entity_id) + + await async_manipulate_test_data(hass, hmip_device, "temperatureExternalOne", 25.4) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == "25.4" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + await async_manipulate_test_data(hass, hmip_device, "temperatureExternalOne", 23.5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "23.5" + + +async def test_hmip_temperature_external_sensor_channel_2( + hass, default_mock_hap_factory +): + """Test HomematicipTemperatureDifferenceSensor Channel 2 HmIP-STE2-PCB.""" + entity_id = "sensor.ste2_channel_2_temperature" + entity_name = "STE2 Channel 2 Temperature" + device_model = "HmIP-STE2-PCB" + + mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_devices=["STE2"]) + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + hmip_device = mock_hap.hmip_device_by_entity_id.get(entity_id) + + await async_manipulate_test_data(hass, hmip_device, "temperatureExternalTwo", 22.4) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == "22.4" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + await async_manipulate_test_data(hass, hmip_device, "temperatureExternalTwo", 23.4) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "23.4" + + +async def test_hmip_temperature_external_sensor_delta(hass, default_mock_hap_factory): + """Test HomematicipTemperatureDifferenceSensor Delta HmIP-STE2-PCB.""" + entity_id = "sensor.ste2_delta_temperature" + entity_name = "STE2 Delta Temperature" + device_model = "HmIP-STE2-PCB" + + mock_hap = await default_mock_hap_factory.async_get_mock_hap(test_devices=["STE2"]) + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + hmip_device = mock_hap.hmip_device_by_entity_id.get(entity_id) + + await async_manipulate_test_data(hass, hmip_device, "temperatureExternalDelta", 0.4) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == "0.4" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + await async_manipulate_test_data( + hass, hmip_device, "temperatureExternalDelta", -0.5 + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "-0.5" + + async def test_hmip_passage_detector_delta_counter(hass, default_mock_hap_factory): """Test HomematicipPassageDetectorDeltaCounter.""" entity_id = "sensor.spdr_1" diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index 2c125d9fada..b0037aa3800 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -6631,6 +6631,100 @@ "serializedGlobalTradeItemNumber": "3014F7110000000000056775", "type": "FULL_FLUSH_CONTACT_INTERFACE_6", "updateState": "UP_TO_DATE" + }, + "3014F7110000000000STE2015": { + "availableFirmwareVersion": "1.0.26", + "connectionType": "HMIP_RF", + "firmwareVersion": "1.0.18", + "firmwareVersionInteger": 65554, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000STE2015", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "displayContrast": null, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": ["00000000-0000-0000-0000-000000000024"], + "index": 0, + "label": "", + "lockJammed": null, + "lowBat": false, + "mountingOrientation": null, + "multicastRoutingEnabled": false, + "particulateMatterSensorCommunicationError": null, + "particulateMatterSensorError": null, + "powerShortCircuit": null, + "profilePeriodLimitReached": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -60, + "rssiPeerValue": null, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceParticulateMatterSensorCommunicationError": false, + "IFeatureDeviceParticulateMatterSensorError": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceTemperatureHumiditySensorCommunicationError": false, + "IFeatureDeviceTemperatureHumiditySensorError": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureProfilePeriodLimit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDeviceErrorLockJammed": false, + "IOptionalFeatureDisplayContrast": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": true, + "IOptionalFeatureMountingOrientation": false + }, + "temperatureHumiditySensorCommunicationError": null, + "temperatureHumiditySensorError": null, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000STE2015", + "functionalChannelType": "TEMPERATURE_SENSOR_2_EXTERNAL_DELTA_CHANNEL", + "groupIndex": 1, + "groups": ["00000000-0000-0000-0000-000000000025"], + "index": 1, + "label": "", + "temperatureExternalDelta": -0.9, + "temperatureExternalOne": 24.5, + "temperatureExternalTwo": 25.4 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000STE2015", + "label": "STE2", + "lastStatusUpdate": 1645012379988, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 415, + "modelType": "HmIP-STE2-PCB", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000STE2015", + "type": "TEMPERATURE_SENSOR_2_EXTERNAL_DELTA", + "updateState": "TRANSFERING_UPDATE" } }, "groups": { From 2df20e7a42c23b65f11f13d51e19b6e75dfdab08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 25 Jul 2022 13:21:43 -0500 Subject: [PATCH 2837/3516] Make lifx async_migrate_legacy_entries a callback (#75719) --- homeassistant/components/lifx/__init__.py | 4 ++-- homeassistant/components/lifx/migration.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 8816226ff84..3faf69483d5 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -77,7 +77,7 @@ async def async_legacy_migration( } # device.mac_addr is not the mac_address, its the serial number hosts_by_serial = {device.mac_addr: device.ip_addr for device in discovered_devices} - missing_discovery_count = await async_migrate_legacy_entries( + missing_discovery_count = async_migrate_legacy_entries( hass, hosts_by_serial, existing_serials, legacy_entry ) if missing_discovery_count: @@ -180,7 +180,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if legacy_entry := async_get_legacy_entry(hass): # If the legacy entry still exists, harvest the entities # that are moving to this config entry. - await async_migrate_entities_devices(hass, legacy_entry.entry_id, entry) + async_migrate_entities_devices(hass, legacy_entry.entry_id, entry) assert entry.unique_id is not None domain_data = hass.data[DOMAIN] diff --git a/homeassistant/components/lifx/migration.py b/homeassistant/components/lifx/migration.py index 1ff94daa92f..359480a4507 100644 --- a/homeassistant/components/lifx/migration.py +++ b/homeassistant/components/lifx/migration.py @@ -2,14 +2,15 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from .const import _LOGGER, DOMAIN from .discovery import async_init_discovery_flow -async def async_migrate_legacy_entries( +@callback +def async_migrate_legacy_entries( hass: HomeAssistant, discovered_hosts_by_serial: dict[str, str], existing_serials: set[str], @@ -41,7 +42,8 @@ async def async_migrate_legacy_entries( return len(remaining_devices) -async def async_migrate_entities_devices( +@callback +def async_migrate_entities_devices( hass: HomeAssistant, legacy_entry_id: str, new_entry: ConfigEntry ) -> None: """Move entities and devices to the new config entry.""" From 3aa75f3fccf47224ad59b141d39b55e7180e823a Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 26 Jul 2022 04:32:00 +0800 Subject: [PATCH 2838/3516] Don't use executor for lutron subscription (#75726) --- homeassistant/components/lutron/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index b2b01aa3c44..1583d8b74eb 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -124,9 +124,7 @@ class LutronDevice(Entity): async def async_added_to_hass(self): """Register callbacks.""" - self.hass.async_add_executor_job( - self._lutron_device.subscribe, self._update_callback, None - ) + self._lutron_device.subscribe(self._update_callback, None) def _update_callback(self, _device, _context, _event, _params): """Run when invoked by pylutron when the device state changes.""" From 274584f2a47b7db9943168316122196a540080e5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 25 Jul 2022 22:52:13 +0200 Subject: [PATCH 2839/3516] Add strict typing for litterrobot (#75540) --- .strict-typing | 1 + .../components/litterrobot/__init__.py | 1 + .../components/litterrobot/config_flow.py | 9 ++++++- .../components/litterrobot/entity.py | 25 +++++++++++++------ homeassistant/components/litterrobot/hub.py | 3 ++- .../components/litterrobot/sensor.py | 11 ++++---- .../components/litterrobot/switch.py | 14 ++++++----- mypy.ini | 11 ++++++++ 8 files changed, 53 insertions(+), 22 deletions(-) diff --git a/.strict-typing b/.strict-typing index e64a9a04931..45d11f089dd 100644 --- a/.strict-typing +++ b/.strict-typing @@ -152,6 +152,7 @@ homeassistant.components.laundrify.* homeassistant.components.lcn.* homeassistant.components.light.* homeassistant.components.lifx.* +homeassistant.components.litterrobot.* homeassistant.components.local_ip.* homeassistant.components.lock.* homeassistant.components.logbook.* diff --git a/homeassistant/components/litterrobot/__init__.py b/homeassistant/components/litterrobot/__init__.py index a2612966a98..f3b150f2c1d 100644 --- a/homeassistant/components/litterrobot/__init__.py +++ b/homeassistant/components/litterrobot/__init__.py @@ -1,4 +1,5 @@ """The Litter-Robot integration.""" +from __future__ import annotations from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException diff --git a/homeassistant/components/litterrobot/config_flow.py b/homeassistant/components/litterrobot/config_flow.py index c49d18c5257..fbe32fa9749 100644 --- a/homeassistant/components/litterrobot/config_flow.py +++ b/homeassistant/components/litterrobot/config_flow.py @@ -1,11 +1,16 @@ """Config flow for Litter-Robot integration.""" +from __future__ import annotations + +from collections.abc import Mapping import logging +from typing import Any from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN from .hub import LitterRobotHub @@ -22,7 +27,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: Mapping[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" errors = {} diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 51b88bb4f79..501b71fbd06 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -1,29 +1,35 @@ """Litter-Robot entities for common data and methods.""" from __future__ import annotations +from collections.abc import Callable, Coroutine from datetime import time import logging -from types import MethodType from typing import Any from pylitterbot import Robot from pylitterbot.exceptions import InvalidCommandException +from typing_extensions import ParamSpec from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.event import async_call_later -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) import homeassistant.util.dt as dt_util from .const import DOMAIN from .hub import LitterRobotHub +_P = ParamSpec("_P") + _LOGGER = logging.getLogger(__name__) REFRESH_WAIT_TIME_SECONDS = 8 -class LitterRobotEntity(CoordinatorEntity): +class LitterRobotEntity(CoordinatorEntity[DataUpdateCoordinator[bool]]): """Generic Litter-Robot entity representing common data and methods.""" def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: @@ -63,7 +69,10 @@ class LitterRobotControlEntity(LitterRobotEntity): self._refresh_callback: CALLBACK_TYPE | None = None async def perform_action_and_refresh( - self, action: MethodType, *args: Any, **kwargs: Any + self, + action: Callable[_P, Coroutine[Any, Any, bool]], + *args: _P.args, + **kwargs: _P.kwargs, ) -> bool: """Perform an action and initiates a refresh of the robot data after a few seconds.""" success = False @@ -82,7 +91,7 @@ class LitterRobotControlEntity(LitterRobotEntity): ) return success - async def async_call_later_callback(self, *_) -> None: + async def async_call_later_callback(self, *_: Any) -> None: """Perform refresh request on callback.""" self._refresh_callback = None await self.coordinator.async_request_refresh() @@ -92,7 +101,7 @@ class LitterRobotControlEntity(LitterRobotEntity): self.async_cancel_refresh_callback() @callback - def async_cancel_refresh_callback(self): + def async_cancel_refresh_callback(self) -> None: """Clear the refresh callback if it has not already fired.""" if self._refresh_callback is not None: self._refresh_callback() @@ -126,10 +135,10 @@ class LitterRobotConfigEntity(LitterRobotControlEntity): def __init__(self, robot: Robot, entity_type: str, hub: LitterRobotHub) -> None: """Init a Litter-Robot control entity.""" super().__init__(robot=robot, entity_type=entity_type, hub=hub) - self._assumed_state: Any = None + self._assumed_state: bool | None = None async def perform_action_and_assume_state( - self, action: MethodType, assumed_state: Any + self, action: Callable[[bool], Coroutine[Any, Any, bool]], assumed_state: bool ) -> None: """Perform an action and assume the state passed in if call is successful.""" if await self.perform_action_and_refresh(action, assumed_state): diff --git a/homeassistant/components/litterrobot/hub.py b/homeassistant/components/litterrobot/hub.py index 43d60e534ea..bde4c780482 100644 --- a/homeassistant/components/litterrobot/hub.py +++ b/homeassistant/components/litterrobot/hub.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Mapping from datetime import timedelta import logging +from typing import Any from pylitterbot import Account from pylitterbot.exceptions import LitterRobotException, LitterRobotLoginException @@ -24,7 +25,7 @@ class LitterRobotHub: account: Account - def __init__(self, hass: HomeAssistant, data: Mapping) -> None: + def __init__(self, hass: HomeAssistant, data: Mapping[str, Any]) -> None: """Initialize the Litter-Robot hub.""" self._data = data diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 01deaa302cf..b6dd2a976c3 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime -from typing import Any +from typing import Any, Union, cast from pylitterbot.robot import Robot @@ -12,7 +12,6 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, - StateType, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE @@ -61,12 +60,12 @@ class LitterRobotSensorEntity(LitterRobotEntity, SensorEntity): self.entity_description = description @property - def native_value(self) -> StateType | datetime: + def native_value(self) -> float | datetime | str | None: """Return the state.""" if self.entity_description.should_report(self.robot): if isinstance(val := getattr(self.robot, self.entity_description.key), str): return val.lower() - return val + return cast(Union[float, datetime, None], val) return None @property @@ -88,13 +87,13 @@ ROBOT_SENSORS = [ name="Sleep Mode Start Time", key="sleep_mode_start_time", device_class=SensorDeviceClass.TIMESTAMP, - should_report=lambda robot: robot.sleep_mode_enabled, + should_report=lambda robot: robot.sleep_mode_enabled, # type: ignore[no-any-return] ), LitterRobotSensorEntityDescription( name="Sleep Mode End Time", key="sleep_mode_end_time", device_class=SensorDeviceClass.TIMESTAMP, - should_report=lambda robot: robot.sleep_mode_enabled, + should_report=lambda robot: robot.sleep_mode_enabled, # type: ignore[no-any-return] ), LitterRobotSensorEntityDescription( name="Last Seen", diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index 4d302a0d4ae..5374add1e34 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -17,11 +17,11 @@ class LitterRobotNightLightModeSwitch(LitterRobotConfigEntity, SwitchEntity): """Litter-Robot Night Light Mode Switch.""" @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if switch is on.""" if self._refresh_callback is not None: return self._assumed_state - return self.robot.night_light_mode_enabled + return self.robot.night_light_mode_enabled # type: ignore[no-any-return] @property def icon(self) -> str: @@ -41,11 +41,11 @@ class LitterRobotPanelLockoutSwitch(LitterRobotConfigEntity, SwitchEntity): """Litter-Robot Panel Lockout Switch.""" @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if switch is on.""" if self._refresh_callback is not None: return self._assumed_state - return self.robot.panel_lock_enabled + return self.robot.panel_lock_enabled # type: ignore[no-any-return] @property def icon(self) -> str: @@ -61,7 +61,9 @@ class LitterRobotPanelLockoutSwitch(LitterRobotConfigEntity, SwitchEntity): await self.perform_action_and_assume_state(self.robot.set_panel_lockout, False) -ROBOT_SWITCHES: list[tuple[type[LitterRobotConfigEntity], str]] = [ +ROBOT_SWITCHES: list[ + tuple[type[LitterRobotNightLightModeSwitch | LitterRobotPanelLockoutSwitch], str] +] = [ (LitterRobotNightLightModeSwitch, "Night Light Mode"), (LitterRobotPanelLockoutSwitch, "Panel Lockout"), ] @@ -75,7 +77,7 @@ async def async_setup_entry( """Set up Litter-Robot switches using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - entities = [] + entities: list[SwitchEntity] = [] for robot in hub.account.robots: for switch_class, switch_type in ROBOT_SWITCHES: entities.append(switch_class(robot=robot, entity_type=switch_type, hub=hub)) diff --git a/mypy.ini b/mypy.ini index ee953f13d74..af6abe6658f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1395,6 +1395,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.litterrobot.*] +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.local_ip.*] check_untyped_defs = true disallow_incomplete_defs = true From a287abe76377479be09f8d82d29e128de96a6f14 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Mon, 25 Jul 2022 15:53:22 -0500 Subject: [PATCH 2840/3516] Update name of Z-WaveJS to Z-Wave (#75136) --- homeassistant/components/zwave_js/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index d1097a6cd65..76f5d94b589 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -1,6 +1,6 @@ { "domain": "zwave_js", - "name": "Z-Wave JS", + "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", "requirements": ["zwave-js-server-python==0.39.0"], From ea354f3d5fb4ec04f740c0fb416bb0c355aa3eaa Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 26 Jul 2022 07:56:36 +1000 Subject: [PATCH 2841/3516] Remove deprecated MyZone service in Advantage Air (#75160) --- .../components/advantage_air/climate.py | 18 ------------------ .../components/advantage_air/services.yaml | 8 -------- tests/components/advantage_air/test_climate.py | 17 ----------------- 3 files changed, 43 deletions(-) diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index db060e739b1..e69dc06dd7d 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -15,7 +15,6 @@ from homeassistant.components.climate.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -53,7 +52,6 @@ ADVANTAGE_AIR_FAN_MODES = { HASS_FAN_MODES = {v: k for k, v in ADVANTAGE_AIR_FAN_MODES.items()} FAN_SPEEDS = {FAN_LOW: 30, FAN_MEDIUM: 60, FAN_HIGH: 100} -ADVANTAGE_AIR_SERVICE_SET_MYZONE = "set_myzone" ZONE_HVAC_MODES = [HVACMode.OFF, HVACMode.HEAT_COOL] PARALLEL_UPDATES = 0 @@ -79,13 +77,6 @@ async def async_setup_entry( entities.append(AdvantageAirZone(instance, ac_key, zone_key)) async_add_entities(entities) - platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service( - ADVANTAGE_AIR_SERVICE_SET_MYZONE, - {}, - "set_myzone", - ) - class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity): """AdvantageAir AC unit.""" @@ -213,12 +204,3 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): await self.async_change( {self.ac_key: {"zones": {self.zone_key: {"setTemp": temp}}}} ) - - async def set_myzone(self, **kwargs): - """Set this zone as the 'MyZone'.""" - _LOGGER.warning( - "The advantage_air.set_myzone service has been deprecated and will be removed in a future version, please use the select.select_option service on the MyZone entity" - ) - await self.async_change( - {self.ac_key: {"info": {"myZone": self._zone["number"]}}} - ) diff --git a/homeassistant/components/advantage_air/services.yaml b/homeassistant/components/advantage_air/services.yaml index 38f21e57128..6bd3bf815d6 100644 --- a/homeassistant/components/advantage_air/services.yaml +++ b/homeassistant/components/advantage_air/services.yaml @@ -15,11 +15,3 @@ set_time_to: min: 0 max: 1440 unit_of_measurement: minutes - -set_myzone: - name: Set MyZone - description: Change which zone is set as the reference for temperature control (deprecated) - target: - entity: - integration: advantage_air - domain: climate diff --git a/tests/components/advantage_air/test_climate.py b/tests/components/advantage_air/test_climate.py index 6ee0a614b09..f1b118ab3b3 100644 --- a/tests/components/advantage_air/test_climate.py +++ b/tests/components/advantage_air/test_climate.py @@ -2,14 +2,12 @@ from json import loads from homeassistant.components.advantage_air.climate import ( - ADVANTAGE_AIR_SERVICE_SET_MYZONE, HASS_FAN_MODES, HASS_HVAC_MODES, ) from homeassistant.components.advantage_air.const import ( ADVANTAGE_AIR_STATE_OFF, ADVANTAGE_AIR_STATE_ON, - DOMAIN as ADVANTAGE_AIR_DOMAIN, ) from homeassistant.components.climate.const import ( ATTR_FAN_MODE, @@ -170,21 +168,6 @@ async def test_climate_async_setup_entry(hass, aioclient_mock): assert aioclient_mock.mock_calls[-1][0] == "GET" assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" - # Test set_myair service - await hass.services.async_call( - ADVANTAGE_AIR_DOMAIN, - ADVANTAGE_AIR_SERVICE_SET_MYZONE, - {ATTR_ENTITY_ID: [entity_id]}, - blocking=True, - ) - assert len(aioclient_mock.mock_calls) == 17 - assert aioclient_mock.mock_calls[-2][0] == "GET" - assert aioclient_mock.mock_calls[-2][1].path == "/setAircon" - data = loads(aioclient_mock.mock_calls[-2][1].query["json"]) - assert data["ac1"]["info"]["myZone"] == 1 - assert aioclient_mock.mock_calls[-1][0] == "GET" - assert aioclient_mock.mock_calls[-1][1].path == "/getSystemData" - async def test_climate_async_failed_update(hass, aioclient_mock): """Test climate change failure.""" From e87c2b9e2590eadcaa14f7256388675fcc64918d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 26 Jul 2022 00:45:01 +0200 Subject: [PATCH 2842/3516] Bump pytraccar to 1.0.0 (#75671) --- .../components/traccar/device_tracker.py | 182 +++++++++--------- .../components/traccar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/traccar/test_device_tracker.py | 43 +++-- 5 files changed, 128 insertions(+), 103 deletions(-) diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 50b0e7827aa..970cd20d640 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -1,11 +1,19 @@ """Support for Traccar device tracking.""" from __future__ import annotations +import asyncio from collections.abc import Awaitable, Callable from datetime import datetime, timedelta import logging -from pytraccar.api import API +from pytraccar import ( + ApiClient, + DeviceModel, + GeofenceModel, + PositionModel, + TraccarAuthenticationException, + TraccarException, +) from stringcase import camelcase import voluptuous as vol @@ -170,17 +178,13 @@ async def async_setup_scanner( discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Validate the configuration and return a Traccar scanner.""" - - session = async_get_clientsession(hass, config[CONF_VERIFY_SSL]) - - api = API( - hass.loop, - session, - config[CONF_USERNAME], - config[CONF_PASSWORD], - config[CONF_HOST], - config[CONF_PORT], - config[CONF_SSL], + api = ApiClient( + host=config[CONF_HOST], + port=config[CONF_PORT], + ssl=config[CONF_SSL], + username=config[CONF_USERNAME], + password=config[CONF_PASSWORD], + client_session=async_get_clientsession(hass, config[CONF_VERIFY_SSL]), ) scanner = TraccarScanner( @@ -202,15 +206,15 @@ class TraccarScanner: def __init__( self, - api, - hass, - async_see, - scan_interval, - max_accuracy, - skip_accuracy_on, - custom_attributes, - event_types, - ): + api: ApiClient, + hass: HomeAssistant, + async_see: Callable[..., Awaitable[None]], + scan_interval: timedelta, + max_accuracy: int, + skip_accuracy_on: bool, + custom_attributes: list[str], + event_types: list[str], + ) -> None: """Initialize.""" if EVENT_ALL_EVENTS in event_types: @@ -220,15 +224,18 @@ class TraccarScanner: self._scan_interval = scan_interval self._async_see = async_see self._api = api - self.connected = False self._hass = hass self._max_accuracy = max_accuracy self._skip_accuracy_on = skip_accuracy_on + self._devices: list[DeviceModel] = [] + self._positions: list[PositionModel] = [] + self._geofences: list[GeofenceModel] = [] async def async_init(self): """Further initialize connection to Traccar.""" - await self._api.test_connection() - if self._api.connected and not self._api.authenticated: + try: + await self._api.get_server() + except TraccarAuthenticationException: _LOGGER.error("Authentication for Traccar failed") return False @@ -238,57 +245,63 @@ class TraccarScanner: async def _async_update(self, now=None): """Update info from Traccar.""" - if not self.connected: - _LOGGER.debug("Testing connection to Traccar") - await self._api.test_connection() - self.connected = self._api.connected - if self.connected: - _LOGGER.info("Connection to Traccar restored") - else: - return _LOGGER.debug("Updating device data") - await self._api.get_device_info(self._custom_attributes) + try: + (self._devices, self._positions, self._geofences,) = await asyncio.gather( + self._api.get_devices(), + self._api.get_positions(), + self._api.get_geofences(), + ) + except TraccarException as ex: + _LOGGER.error("Error while updating device data: %s", ex) + return + self._hass.async_create_task(self.import_device_data()) if self._event_types: self._hass.async_create_task(self.import_events()) - self.connected = self._api.connected async def import_device_data(self): """Import device data from Traccar.""" - for device_unique_id in self._api.device_info: - device_info = self._api.device_info[device_unique_id] - device = None - attr = {} + for position in self._positions: + device = next( + (dev for dev in self._devices if dev.id == position.device_id), None + ) + + if not device: + continue + + attr = { + ATTR_TRACKER: "traccar", + ATTR_ADDRESS: position.address, + ATTR_SPEED: position.speed, + ATTR_ALTITUDE: position.altitude, + ATTR_MOTION: position.attributes.get("motion", False), + ATTR_TRACCAR_ID: device.id, + ATTR_GEOFENCE: next( + ( + geofence.name + for geofence in self._geofences + if geofence.id in (device.geofence_ids or []) + ), + None, + ), + ATTR_CATEGORY: device.category, + ATTR_STATUS: device.status, + } + skip_accuracy_filter = False - attr[ATTR_TRACKER] = "traccar" - if device_info.get("address") is not None: - attr[ATTR_ADDRESS] = device_info["address"] - if device_info.get("geofence") is not None: - attr[ATTR_GEOFENCE] = device_info["geofence"] - if device_info.get("category") is not None: - attr[ATTR_CATEGORY] = device_info["category"] - if device_info.get("speed") is not None: - attr[ATTR_SPEED] = device_info["speed"] - if device_info.get("motion") is not None: - attr[ATTR_MOTION] = device_info["motion"] - if device_info.get("traccar_id") is not None: - attr[ATTR_TRACCAR_ID] = device_info["traccar_id"] - for dev in self._api.devices: - if dev["id"] == device_info["traccar_id"]: - device = dev - break - if device is not None and device.get("status") is not None: - attr[ATTR_STATUS] = device["status"] for custom_attr in self._custom_attributes: - if device_info.get(custom_attr) is not None: - attr[custom_attr] = device_info[custom_attr] + if device.attributes.get(custom_attr) is not None: + attr[custom_attr] = position.attributes[custom_attr] + if custom_attr in self._skip_accuracy_on: + skip_accuracy_filter = True + if position.attributes.get(custom_attr) is not None: + attr[custom_attr] = position.attributes[custom_attr] if custom_attr in self._skip_accuracy_on: skip_accuracy_filter = True - accuracy = 0.0 - if device_info.get("accuracy") is not None: - accuracy = device_info["accuracy"] + accuracy = position.accuracy or 0.0 if ( not skip_accuracy_filter and self._max_accuracy > 0 @@ -302,42 +315,39 @@ class TraccarScanner: continue await self._async_see( - dev_id=slugify(device_info["device_id"]), - gps=(device_info.get("latitude"), device_info.get("longitude")), + dev_id=slugify(device.name), + gps=(position.latitude, position.longitude), gps_accuracy=accuracy, - battery=device_info.get("battery"), + battery=position.attributes.get("batteryLevel", -1), attributes=attr, ) async def import_events(self): """Import events from Traccar.""" - device_ids = [device["id"] for device in self._api.devices] - end_interval = datetime.utcnow() - start_interval = end_interval - self._scan_interval - events = await self._api.get_events( - device_ids=device_ids, - from_time=start_interval, - to_time=end_interval, + start_intervel = datetime.utcnow() + events = await self._api.get_reports_events( + devices=[device.id for device in self._devices], + start_time=start_intervel, + end_time=start_intervel - self._scan_interval, event_types=self._event_types.keys(), ) if events is not None: for event in events: - device_name = next( - ( - dev.get("name") - for dev in self._api.devices - if dev.get("id") == event["deviceId"] - ), - None, - ) self._hass.bus.async_fire( - f"traccar_{self._event_types.get(event['type'])}", + f"traccar_{self._event_types.get(event.type)}", { - "device_traccar_id": event["deviceId"], - "device_name": device_name, - "type": event["type"], - "serverTime": event.get("eventTime") or event.get("serverTime"), - "attributes": event["attributes"], + "device_traccar_id": event.device_id, + "device_name": next( + ( + dev.name + for dev in self._devices + if dev.id == event.device_id + ), + None, + ), + "type": event.type, + "serverTime": event.event_time, + "attributes": event.attributes, }, ) diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json index 7f0df1b1f3f..d7b26100ab6 100644 --- a/homeassistant/components/traccar/manifest.json +++ b/homeassistant/components/traccar/manifest.json @@ -3,7 +3,7 @@ "name": "Traccar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/traccar", - "requirements": ["pytraccar==0.10.0", "stringcase==1.2.0"], + "requirements": ["pytraccar==1.0.0", "stringcase==1.2.0"], "dependencies": ["webhook"], "codeowners": ["@ludeeus"], "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index b56c176652a..fa189bc1d0f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1996,7 +1996,7 @@ pytomorrowio==0.3.4 pytouchline==0.7 # homeassistant.components.traccar -pytraccar==0.10.0 +pytraccar==1.0.0 # homeassistant.components.tradfri pytradfri[async]==9.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8e5146bcfc2..9fca788e61e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1340,7 +1340,7 @@ pytile==2022.02.0 pytomorrowio==0.3.4 # homeassistant.components.traccar -pytraccar==0.10.0 +pytraccar==1.0.0 # homeassistant.components.tradfri pytradfri[async]==9.0.0 diff --git a/tests/components/traccar/test_device_tracker.py b/tests/components/traccar/test_device_tracker.py index 4e2f5e0ff09..61bbe371a75 100644 --- a/tests/components/traccar/test_device_tracker.py +++ b/tests/components/traccar/test_device_tracker.py @@ -2,6 +2,8 @@ from datetime import datetime from unittest.mock import AsyncMock, patch +from pytraccar import ReportsEventeModel + from homeassistant.components.device_tracker.const import DOMAIN from homeassistant.components.traccar.device_tracker import ( PLATFORM_SCHEMA as TRACCAR_PLATFORM_SCHEMA, @@ -35,26 +37,39 @@ async def test_import_events_catch_all(hass): device = {"id": 1, "name": "abc123"} api_mock = AsyncMock() api_mock.devices = [device] - api_mock.get_events.return_value = [ - { - "deviceId": device["id"], - "type": "ignitionOn", - "serverTime": datetime.utcnow(), - "attributes": {}, - }, - { - "deviceId": device["id"], - "type": "ignitionOff", - "serverTime": datetime.utcnow(), - "attributes": {}, - }, + api_mock.get_reports_events.return_value = [ + ReportsEventeModel( + **{ + "id": 1, + "positionId": 1, + "geofenceId": 1, + "maintenanceId": 1, + "deviceId": device["id"], + "type": "ignitionOn", + "eventTime": datetime.utcnow().isoformat(), + "attributes": {}, + } + ), + ReportsEventeModel( + **{ + "id": 2, + "positionId": 2, + "geofenceId": 1, + "maintenanceId": 1, + "deviceId": device["id"], + "type": "ignitionOff", + "eventTime": datetime.utcnow().isoformat(), + "attributes": {}, + } + ), ] events_ignition_on = async_capture_events(hass, "traccar_ignition_on") events_ignition_off = async_capture_events(hass, "traccar_ignition_off") with patch( - "homeassistant.components.traccar.device_tracker.API", return_value=api_mock + "homeassistant.components.traccar.device_tracker.ApiClient", + return_value=api_mock, ): assert await async_setup_component(hass, DOMAIN, conf_dict) From c7ddc595ed7243be20e18b6f1b40dba5ea3e3fac Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Jul 2022 01:09:59 +0200 Subject: [PATCH 2843/3516] Add issue to repairs for removed Spotify YAML configuration (#75736) * Add issue to repairs for removed Spotify YAML configuration * Tweak message --- homeassistant/components/spotify/__init__.py | 18 ++++++++++++++++++ homeassistant/components/spotify/manifest.json | 2 +- homeassistant/components/spotify/strings.json | 6 ++++++ .../components/spotify/translations/en.json | 6 ++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 7ee4e9649a5..83841a1780e 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -9,6 +9,7 @@ import aiohttp import requests from spotipy import Spotify, SpotifyException +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -18,6 +19,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, async_get_config_entry_implementation, ) +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .browse_media import async_browse_media @@ -51,6 +53,22 @@ class HomeAssistantSpotifyData: session: OAuth2Session +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Spotify integration.""" + if DOMAIN in config: + async_create_issue( + hass, + DOMAIN, + "removed_yaml", + breaks_in_ha_version="2022.8.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_yaml", + ) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Spotify from a config entry.""" implementation = await async_get_config_entry_implementation(hass, entry) diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 2940700d230..0556cad26b7 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/spotify", "requirements": ["spotipy==2.20.0"], "zeroconf": ["_spotify-connect._tcp.local."], - "dependencies": ["application_credentials"], + "dependencies": ["application_credentials", "repairs"], "codeowners": ["@frenck"], "config_flow": true, "quality_scale": "silver", diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index caec5b8a288..4405bd21310 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -21,5 +21,11 @@ "info": { "api_endpoint_reachable": "Spotify API endpoint reachable" } + }, + "issues": { + "removed_yaml": { + "title": "The Spotify YAML configuration has been removed", + "description": "Configuring Spotify using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/spotify/translations/en.json b/homeassistant/components/spotify/translations/en.json index 7136e5a8e71..0ccebc9833b 100644 --- a/homeassistant/components/spotify/translations/en.json +++ b/homeassistant/components/spotify/translations/en.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Configuring Spotify using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Spotify YAML configuration has been removed" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Spotify API endpoint reachable" From 4ea532b3ea1a17e658a2b618eccc711cb394dd2c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Jul 2022 01:11:07 +0200 Subject: [PATCH 2844/3516] Add issue to repairs for removed Steam YAML configuration (#75737) --- .../components/steam_online/__init__.py | 18 ++++++++++++++++++ .../components/steam_online/manifest.json | 1 + .../components/steam_online/strings.json | 6 ++++++ .../steam_online/translations/en.json | 6 ++++++ 4 files changed, 31 insertions(+) diff --git a/homeassistant/components/steam_online/__init__.py b/homeassistant/components/steam_online/__init__.py index eae81dd8435..c422269a277 100644 --- a/homeassistant/components/steam_online/__init__.py +++ b/homeassistant/components/steam_online/__init__.py @@ -1,11 +1,13 @@ """The Steam integration.""" from __future__ import annotations +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DEFAULT_NAME, DOMAIN @@ -14,6 +16,22 @@ from .coordinator import SteamDataUpdateCoordinator PLATFORMS = [Platform.SENSOR] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Steam integration.""" + if DOMAIN in config: + async_create_issue( + hass, + DOMAIN, + "removed_yaml", + breaks_in_ha_version="2022.8.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_yaml", + ) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Steam from a config entry.""" coordinator = SteamDataUpdateCoordinator(hass) diff --git a/homeassistant/components/steam_online/manifest.json b/homeassistant/components/steam_online/manifest.json index f8aba1aee07..f2e3a35bbe7 100644 --- a/homeassistant/components/steam_online/manifest.json +++ b/homeassistant/components/steam_online/manifest.json @@ -4,6 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/steam_online", "requirements": ["steamodd==4.21"], + "dependencies": ["repairs"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", "loggers": ["steam"] diff --git a/homeassistant/components/steam_online/strings.json b/homeassistant/components/steam_online/strings.json index 1b431795ea4..63dc7cce22a 100644 --- a/homeassistant/components/steam_online/strings.json +++ b/homeassistant/components/steam_online/strings.json @@ -35,5 +35,11 @@ "error": { "unauthorized": "Friends list restricted: Please refer to the documentation on how to see all other friends" } + }, + "issues": { + "removed_yaml": { + "title": "The Steam YAML configuration has been removed", + "description": "Configuring Steam using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/steam_online/translations/en.json b/homeassistant/components/steam_online/translations/en.json index 7226c5ee177..2a7bef3f683 100644 --- a/homeassistant/components/steam_online/translations/en.json +++ b/homeassistant/components/steam_online/translations/en.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Configuring Steam using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Steam YAML configuration has been removed" + } + }, "options": { "error": { "unauthorized": "Friends list restricted: Please refer to the documentation on how to see all other friends" From 7fd47717cf40afaa5464360b19f381564cc9fdc3 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 26 Jul 2022 02:10:05 +0200 Subject: [PATCH 2845/3516] Bump goodwe to 0.2.18 (#75615) --- homeassistant/components/goodwe/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/goodwe/manifest.json b/homeassistant/components/goodwe/manifest.json index 45895d0d4b0..c91b91c02a9 100644 --- a/homeassistant/components/goodwe/manifest.json +++ b/homeassistant/components/goodwe/manifest.json @@ -3,7 +3,7 @@ "name": "GoodWe Inverter", "documentation": "https://www.home-assistant.io/integrations/goodwe", "codeowners": ["@mletenay", "@starkillerOG"], - "requirements": ["goodwe==0.2.15"], + "requirements": ["goodwe==0.2.18"], "config_flow": true, "iot_class": "local_polling", "loggers": ["goodwe"] diff --git a/requirements_all.txt b/requirements_all.txt index fa189bc1d0f..526f5f29c71 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -743,7 +743,7 @@ glances_api==0.3.5 goalzero==0.2.1 # homeassistant.components.goodwe -goodwe==0.2.15 +goodwe==0.2.18 # homeassistant.components.google_pubsub google-cloud-pubsub==2.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9fca788e61e..a24245dfc3a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -549,7 +549,7 @@ glances_api==0.3.5 goalzero==0.2.1 # homeassistant.components.goodwe -goodwe==0.2.15 +goodwe==0.2.18 # homeassistant.components.google_pubsub google-cloud-pubsub==2.11.0 From 9c725bc106aeba8320e8e855dc5cb0aaa1901784 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 26 Jul 2022 00:26:43 +0000 Subject: [PATCH 2846/3516] [ci skip] Translation update --- .../components/bluetooth/translations/ca.json | 12 ++++++++++- .../components/bluetooth/translations/de.json | 12 ++++++++++- .../components/bluetooth/translations/et.json | 12 ++++++++++- .../bluetooth/translations/pt-BR.json | 12 ++++++++++- .../bluetooth/translations/zh-Hant.json | 12 ++++++++++- .../components/govee_ble/translations/et.json | 21 +++++++++++++++++++ .../components/govee_ble/translations/hu.json | 21 +++++++++++++++++++ .../govee_ble/translations/zh-Hant.json | 21 +++++++++++++++++++ .../components/moat/translations/et.json | 21 +++++++++++++++++++ .../components/moat/translations/hu.json | 21 +++++++++++++++++++ .../components/moat/translations/zh-Hant.json | 21 +++++++++++++++++++ .../radiotherm/translations/ca.json | 6 ++++++ .../radiotherm/translations/de.json | 6 ++++++ .../radiotherm/translations/et.json | 6 ++++++ .../radiotherm/translations/pt-BR.json | 6 ++++++ .../radiotherm/translations/zh-Hant.json | 6 ++++++ .../simplisafe/translations/ca.json | 7 +++++-- .../simplisafe/translations/et.json | 7 +++++-- .../simplisafe/translations/hu.json | 5 ++++- .../simplisafe/translations/zh-Hant.json | 7 +++++-- .../components/switchbot/translations/ca.json | 3 ++- .../components/switchbot/translations/de.json | 2 +- .../components/switchbot/translations/et.json | 3 ++- .../components/switchbot/translations/hu.json | 1 + .../switchbot/translations/zh-Hant.json | 3 ++- .../xiaomi_ble/translations/ca.json | 15 +++++++++++++ .../xiaomi_ble/translations/de.json | 6 ++++++ .../xiaomi_ble/translations/et.json | 15 +++++++++++++ .../xiaomi_ble/translations/hu.json | 15 +++++++++++++ .../xiaomi_ble/translations/zh-Hant.json | 15 +++++++++++++ 30 files changed, 304 insertions(+), 16 deletions(-) create mode 100644 homeassistant/components/govee_ble/translations/et.json create mode 100644 homeassistant/components/govee_ble/translations/hu.json create mode 100644 homeassistant/components/govee_ble/translations/zh-Hant.json create mode 100644 homeassistant/components/moat/translations/et.json create mode 100644 homeassistant/components/moat/translations/hu.json create mode 100644 homeassistant/components/moat/translations/zh-Hant.json diff --git a/homeassistant/components/bluetooth/translations/ca.json b/homeassistant/components/bluetooth/translations/ca.json index 8fb4c022e0a..082803a48dc 100644 --- a/homeassistant/components/bluetooth/translations/ca.json +++ b/homeassistant/components/bluetooth/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El servei ja est\u00e0 configurat" + "already_configured": "El servei ja est\u00e0 configurat", + "no_adapters": "No s'ha trobat cap adaptador Bluetooth" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "Tria un dispositiu a configurar" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Adaptador Bluetooth a utilitzar per escanejar" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/de.json b/homeassistant/components/bluetooth/translations/de.json index ef305862210..6e65b985478 100644 --- a/homeassistant/components/bluetooth/translations/de.json +++ b/homeassistant/components/bluetooth/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Der Dienst ist bereits konfiguriert" + "already_configured": "Der Dienst ist bereits konfiguriert", + "no_adapters": "Keine Bluetooth-Adapter gefunden" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Der zum Scannen zu verwendende Bluetooth-Adapter" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/et.json b/homeassistant/components/bluetooth/translations/et.json index 4098abefbf7..da1dbdb1a5f 100644 --- a/homeassistant/components/bluetooth/translations/et.json +++ b/homeassistant/components/bluetooth/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Teenus on juba h\u00e4\u00e4lestatud" + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", + "no_adapters": "Bluetoothi adaptereid ei leitud" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "Vali h\u00e4\u00e4lestatav seade" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Sk\u00e4nnimiseks kasutatav Bluetoothi adapter" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/pt-BR.json b/homeassistant/components/bluetooth/translations/pt-BR.json index 05ddee20ecb..0a5cf354495 100644 --- a/homeassistant/components/bluetooth/translations/pt-BR.json +++ b/homeassistant/components/bluetooth/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "no_adapters": "Nenhum adaptador Bluetooth encontrado" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "Escolha um dispositivo para configurar" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "O adaptador Bluetooth a ser usado para escaneamento" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/zh-Hant.json b/homeassistant/components/bluetooth/translations/zh-Hant.json index 7d69eb15ac1..34ab75775ab 100644 --- a/homeassistant/components/bluetooth/translations/zh-Hant.json +++ b/homeassistant/components/bluetooth/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "no_adapters": "\u627e\u4e0d\u5230\u4efb\u4f55\u85cd\u82bd\u50b3\u8f38\u5668" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "\u7528\u4ee5\u9032\u884c\u5075\u6e2c\u7684\u85cd\u82bd\u50b3\u8f38\u5668" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/et.json b/homeassistant/components/govee_ble/translations/et.json new file mode 100644 index 00000000000..8dc1b9f6ed0 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/et.json @@ -0,0 +1,21 @@ +{ + "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" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/hu.json b/homeassistant/components/govee_ble/translations/hu.json new file mode 100644 index 00000000000..7ef0d3a6301 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/zh-Hant.json b/homeassistant/components/govee_ble/translations/zh-Hant.json new file mode 100644 index 00000000000..d4eaa8cb41f --- /dev/null +++ b/homeassistant/components/govee_ble/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/et.json b/homeassistant/components/moat/translations/et.json new file mode 100644 index 00000000000..8dc1b9f6ed0 --- /dev/null +++ b/homeassistant/components/moat/translations/et.json @@ -0,0 +1,21 @@ +{ + "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" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/hu.json b/homeassistant/components/moat/translations/hu.json new file mode 100644 index 00000000000..7ef0d3a6301 --- /dev/null +++ b/homeassistant/components/moat/translations/hu.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/zh-Hant.json b/homeassistant/components/moat/translations/zh-Hant.json new file mode 100644 index 00000000000..d4eaa8cb41f --- /dev/null +++ b/homeassistant/components/moat/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/ca.json b/homeassistant/components/radiotherm/translations/ca.json index d1249ddd23f..58e8487607f 100644 --- a/homeassistant/components/radiotherm/translations/ca.json +++ b/homeassistant/components/radiotherm/translations/ca.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de la plataforma 'Radio Thermostat' mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant a la versi\u00f3 2022.9. \n\nLa configuraci\u00f3 existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "S'est\u00e0 eliminant la configuraci\u00f3 YAML de Thermostat Radio" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/de.json b/homeassistant/components/radiotherm/translations/de.json index 50315afce52..2845d3d6190 100644 --- a/homeassistant/components/radiotherm/translations/de.json +++ b/homeassistant/components/radiotherm/translations/de.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration der Radiothermostat-Klimaplattform mit YAML wird in Home Assistant 2022.9 entfernt. \n\nDeine vorhandene Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die YAML-Konfiguration des Funkthermostats wird entfernt" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/et.json b/homeassistant/components/radiotherm/translations/et.json index f8a6a7303ab..8eefd4125bb 100644 --- a/homeassistant/components/radiotherm/translations/et.json +++ b/homeassistant/components/radiotherm/translations/et.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Raadiotermostaadi kliimaplatvormi konfigureerimine YAML-i abil eemaldatakse rakenduses Home Assistant 2022.9. \n\n Olemasolev konfiguratsioon imporditi kasutajaliidesesse automaatselt. Selle probleemi lahendamiseks eemalda YAML-i konfiguratsioon failist configuration.yaml ja taask\u00e4ivita Home Assistant.", + "title": "Raadiotermostaadi YAML-i konfiguratsioon eemaldatakse" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/pt-BR.json b/homeassistant/components/radiotherm/translations/pt-BR.json index da10f6bd457..de9b3f092cc 100644 --- a/homeassistant/components/radiotherm/translations/pt-BR.json +++ b/homeassistant/components/radiotherm/translations/pt-BR.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o da plataforma clim\u00e1tica Radio Thermostat usando YAML est\u00e1 sendo removida no Home Assistant 2022.9. \n\n Sua configura\u00e7\u00e3o existente foi importada para a interface do usu\u00e1rio automaticamente. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do r\u00e1dio termostato est\u00e1 sendo removida" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/zh-Hant.json b/homeassistant/components/radiotherm/translations/zh-Hant.json index 7a5a40817f7..ad1af3bb442 100644 --- a/homeassistant/components/radiotherm/translations/zh-Hant.json +++ b/homeassistant/components/radiotherm/translations/zh-Hant.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Radio \u6eab\u63a7\u5668\u5df2\u7d93\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684\u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Radio \u6eab\u63a7\u5668 YAML \u8a2d\u5b9a\u5df2\u79fb\u9664" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json index a2fd356932c..dbb3d0e4207 100644 --- a/homeassistant/components/simplisafe/translations/ca.json +++ b/homeassistant/components/simplisafe/translations/ca.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Aquest compte SimpliSafe ja est\u00e0 en \u00fas.", "email_2fa_timed_out": "S'ha esgotat el temps d'espera de l'autenticaci\u00f3 de dos factors a trav\u00e9s de correu electr\u00f2nic.", - "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": "Compte ja est\u00e0 registrat", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "Codi d'autoritzaci\u00f3", "password": "Contrasenya", "username": "Nom d'usuari" }, - "description": "Introdueix el teu nom d'usuari i contrasenya." + "description": "SimpliSafe autentica els seus usuaris a trav\u00e9s de la seva aplicaci\u00f3 web. A causa de les limitacions t\u00e8cniques, hi ha un pas manual al final d'aquest proc\u00e9s; assegura't de llegir la [documentaci\u00f3](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) abans de comen\u00e7ar.\n\nQuan ja estiguis, fes clic [aqu\u00ed]({url}) per obrir l'aplicaci\u00f3 web de SimpliSafe i introdueix les teves credencials. Quan el proc\u00e9s s'hagi completat, torna aqu\u00ed i introdueix, a sota, el codi d'autoritzaci\u00f3 de l'URL de l'aplicaci\u00f3 web de SimpliSafe." } } }, diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json index 0f01f4d9b9c..073d20555c1 100644 --- a/homeassistant/components/simplisafe/translations/et.json +++ b/homeassistant/components/simplisafe/translations/et.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "See SimpliSafe'i konto on juba kasutusel.", "email_2fa_timed_out": "Meilip\u00f5hise kahefaktorilise autentimise ajal\u00f5pp.", - "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", "invalid_auth": "Tuvastamise viga", "unknown": "Tundmatu viga" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "Tuvastuskood", "password": "Salas\u00f5na", "username": "Kasutajanimi" }, - "description": "Sisesta kasutajatunnus ja salas\u00f5na" + "description": "SimpliSafe autendib kasutajaid oma veebirakenduse kaudu. Tehniliste piirangute t\u00f5ttu on selle protsessi l\u00f5pus k\u00e4sitsi samm; palun veendu, et loed enne alustamist l\u00e4bi [dokumendid](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code).\n\nKui oled valmis, kl\u00f5psa veebirakenduse SimpliSafe avamiseks ja mandaadi sisestamiseks kl\u00f5psa [here]({url}). Kui protsess on l\u00f5pule j\u00f5udnud, naase siia ja sisesta autoriseerimiskood SimpliSafe veebirakenduse URL-ist." } } }, diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index 12f745b3b69..ece2b0a0dfb 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Ez a SimpliSafe-fi\u00f3k m\u00e1r haszn\u00e1latban van.", "email_2fa_timed_out": "Az e-mail alap\u00fa k\u00e9tfaktoros hiteles\u00edt\u00e9sre val\u00f3 v\u00e1rakoz\u00e1s ideje lej\u00e1rt", - "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": "A fi\u00f3k m\u00e1r regisztr\u00e1lt", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, @@ -28,6 +30,7 @@ }, "user": { "data": { + "auth_code": "Enged\u00e9lyez\u00e9si k\u00f3d", "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json index ce763da538e..99f08eb14d4 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/translations/zh-Hant.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "\u6b64 SimpliSafe \u5e33\u865f\u5df2\u88ab\u4f7f\u7528\u3002", "email_2fa_timed_out": "\u7b49\u5f85\u5169\u6b65\u9a5f\u9a57\u8b49\u78bc\u90f5\u4ef6\u903e\u6642\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", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "\u8a8d\u8b49\u78bc", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8f38\u5165\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002" + "description": "SimpliSafe \u70ba\u900f\u904e Web App \u65b9\u5f0f\u7684\u8a8d\u8b49\u5176\u4f7f\u7528\u8005\u3002\u7531\u65bc\u6280\u8853\u9650\u5236\u3001\u65bc\u6b64\u904e\u7a0b\u7d50\u675f\u6642\u5c07\u6703\u6709\u4e00\u6b65\u624b\u52d5\u968e\u6bb5\uff1b\u65bc\u958b\u59cb\u524d\u3001\u8acb\u78ba\u5b9a\u53c3\u95b1 [\u76f8\u95dc\u6587\u4ef6](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3002\n\n\u6e96\u5099\u5c31\u7dd2\u5f8c\u3001\u9ede\u9078 [\u6b64\u8655]({url}) \u4ee5\u958b\u555f SimpliSafe Web App \u4e26\u8f38\u5165\u9a57\u8b49\u3002\u5b8c\u6210\u5f8c\u56de\u5230\u9019\u88e1\u4e26\u8f38\u5165\u7531 SimpliSafe Web App \u6240\u53d6\u7684\u8a8d\u8b49\u78bc\u3002" } } }, diff --git a/homeassistant/components/switchbot/translations/ca.json b/homeassistant/components/switchbot/translations/ca.json index 576aeda3b16..a3ba38e2f24 100644 --- a/homeassistant/components/switchbot/translations/ca.json +++ b/homeassistant/components/switchbot/translations/ca.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "Tipus de Switchbot no compatible.", "unknown": "Error inesperat" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Adre\u00e7a del dispositiu", "mac": "Adre\u00e7a MAC del dispositiu", "name": "Nom", "password": "Contrasenya" diff --git a/homeassistant/components/switchbot/translations/de.json b/homeassistant/components/switchbot/translations/de.json index c1ab660e6a5..ca306d6fa2d 100644 --- a/homeassistant/components/switchbot/translations/de.json +++ b/homeassistant/components/switchbot/translations/de.json @@ -7,7 +7,7 @@ "switchbot_unsupported_type": "Nicht unterst\u00fctzter Switchbot-Typ.", "unknown": "Unerwarteter Fehler" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { diff --git a/homeassistant/components/switchbot/translations/et.json b/homeassistant/components/switchbot/translations/et.json index 358a4748724..a46ec376a96 100644 --- a/homeassistant/components/switchbot/translations/et.json +++ b/homeassistant/components/switchbot/translations/et.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "Toetamata Switchboti t\u00fc\u00fcp.", "unknown": "Ootamatu t\u00f5rge" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Seadme aadress", "mac": "Seadme MAC-aadress", "name": "Nimi", "password": "Salas\u00f5na" diff --git a/homeassistant/components/switchbot/translations/hu.json b/homeassistant/components/switchbot/translations/hu.json index b870e577426..e44bfe7c811 100644 --- a/homeassistant/components/switchbot/translations/hu.json +++ b/homeassistant/components/switchbot/translations/hu.json @@ -15,6 +15,7 @@ "step": { "user": { "data": { + "address": "Eszk\u00f6z c\u00edme", "mac": "Eszk\u00f6z MAC-c\u00edme", "name": "Elnevez\u00e9s", "password": "Jelsz\u00f3" diff --git a/homeassistant/components/switchbot/translations/zh-Hant.json b/homeassistant/components/switchbot/translations/zh-Hant.json index 617129167ed..6d14e05aff7 100644 --- a/homeassistant/components/switchbot/translations/zh-Hant.json +++ b/homeassistant/components/switchbot/translations/zh-Hant.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "\u4e0d\u652f\u6301\u7684 Switchbot \u985e\u5225\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "\u88dd\u7f6e\u4f4d\u5740", "mac": "\u88dd\u7f6e MAC \u4f4d\u5740", "name": "\u540d\u7a31", "password": "\u5bc6\u78bc" diff --git a/homeassistant/components/xiaomi_ble/translations/ca.json b/homeassistant/components/xiaomi_ble/translations/ca.json index 0cd4571dc9d..1c24ab7172e 100644 --- a/homeassistant/components/xiaomi_ble/translations/ca.json +++ b/homeassistant/components/xiaomi_ble/translations/ca.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "decryption_failed": "La clau d'enlla\u00e7 proporcionada no ha funcionat, les dades del sensor no s'han pogut desxifrar. Comprova-la i torna-ho a provar.", + "expected_24_characters": "S'espera una clau d'enlla\u00e7 de 24 car\u00e0cters hexadecimals.", + "expected_32_characters": "S'espera una clau d'enlla\u00e7 de 32 car\u00e0cters hexadecimals.", "no_devices_found": "No s'han trobat dispositius a la xarxa" }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "Vols configurar {name}?" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Les dades del sensor emeses estan xifrades. Per desxifrar-les necessites una clau d'enlla\u00e7 de 32 car\u00e0cters hexadecimals." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Les dades del sensor emeses estan xifrades. Per desxifrar-les necessites una clau d'enlla\u00e7 de 24 car\u00e0cters hexadecimals." + }, "user": { "data": { "address": "Dispositiu" diff --git a/homeassistant/components/xiaomi_ble/translations/de.json b/homeassistant/components/xiaomi_ble/translations/de.json index b81386a076f..0d3e2c542d2 100644 --- a/homeassistant/components/xiaomi_ble/translations/de.json +++ b/homeassistant/components/xiaomi_ble/translations/de.json @@ -14,9 +14,15 @@ "description": "M\u00f6chtest du {name} einrichten?" }, "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindungsschl\u00fcssel" + }, "description": "Die vom Sensor \u00fcbertragenen Sensordaten sind verschl\u00fcsselt. Um sie zu entschl\u00fcsseln, ben\u00f6tigen wir einen 32-stelligen hexadezimalen Bindungsschl\u00fcssel." }, "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindungsschl\u00fcssel" + }, "description": "Die vom Sensor \u00fcbertragenen Sensordaten sind verschl\u00fcsselt. Um sie zu entschl\u00fcsseln, ben\u00f6tigen wir einen 24-stelligen hexadezimalen Bindungsschl\u00fcssel." }, "user": { diff --git a/homeassistant/components/xiaomi_ble/translations/et.json b/homeassistant/components/xiaomi_ble/translations/et.json index 749f7e45de5..1895097e7b1 100644 --- a/homeassistant/components/xiaomi_ble/translations/et.json +++ b/homeassistant/components/xiaomi_ble/translations/et.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Seadistamine on juba k\u00e4imas", + "decryption_failed": "Esitatud sidumisv\u00f5ti ei t\u00f6\u00f6tanud, sensori andmeid ei saanud dekr\u00fcpteerida. Palun kontrolli seda ja proovi uuesti.", + "expected_24_characters": "Eeldati 24-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit.", + "expected_32_characters": "Eeldati 32-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit.", "no_devices_found": "V\u00f6rgust seadmeid ei leitud" }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "Kas seadistada {name}?" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Sidumisv\u00f5ti" + }, + "description": "Anduri edastatavad andmed on kr\u00fcpteeritud. Selle dekr\u00fcpteerimiseks vajame 32-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Sidumisv\u00f5ti" + }, + "description": "Anduri edastatavad andmed on kr\u00fcpteeritud. Selle dekr\u00fcpteerimiseks vajame 24-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit." + }, "user": { "data": { "address": "Seade" diff --git a/homeassistant/components/xiaomi_ble/translations/hu.json b/homeassistant/components/xiaomi_ble/translations/hu.json index 7ef0d3a6301..b03a41f60a6 100644 --- a/homeassistant/components/xiaomi_ble/translations/hu.json +++ b/homeassistant/components/xiaomi_ble/translations/hu.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "decryption_failed": "A megadott kulcs nem m\u0171k\u00f6d\u00f6tt, az \u00e9rz\u00e9kel\u0151adatokat nem lehetett kiolvasni. K\u00e9rj\u00fck, ellen\u0151rizze \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", + "expected_24_characters": "24 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g.", + "expected_32_characters": "32 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g.", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Kulcs (bindkey)" + }, + "description": "Az \u00e9rz\u00e9kel\u0151 adatai titkos\u00edtva vannak. A visszafejt\u00e9shez egy 32 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Kulcs (bindkey)" + }, + "description": "Az \u00e9rz\u00e9kel\u0151 adatai titkos\u00edtva vannak. A visszafejt\u00e9shez egy 24 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g." + }, "user": { "data": { "address": "Eszk\u00f6z" diff --git a/homeassistant/components/xiaomi_ble/translations/zh-Hant.json b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json index d4eaa8cb41f..81f7e2050af 100644 --- a/homeassistant/components/xiaomi_ble/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "decryption_failed": "\u6240\u63d0\u4f9b\u7684\u7d81\u5b9a\u78bc\u7121\u6cd5\u4f7f\u7528\u3001\u50b3\u611f\u5668\u8cc7\u6599\u7121\u6cd5\u89e3\u5bc6\u3002\u8acb\u4fee\u6b63\u5f8c\u3001\u518d\u8a66\u4e00\u6b21\u3002", + "expected_24_characters": "\u9700\u8981 24 \u500b\u5b57\u5143\u4e4b\u5341\u516d\u9032\u4f4d\u7d81\u5b9a\u78bc\u3002", + "expected_32_characters": "\u9700\u8981 32 \u500b\u5b57\u5143\u4e4b\u5341\u516d\u9032\u4f4d\u7d81\u5b9a\u78bc\u3002", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "\u7d81\u5b9a\u78bc" + }, + "description": "\u7531\u50b3\u611f\u5668\u6240\u5ee3\u64ad\u4e4b\u8cc7\u6599\u70ba\u52a0\u5bc6\u8cc7\u6599\u3002\u82e5\u8981\u89e3\u78bc\u3001\u9700\u8981 32 \u500b\u5b57\u5143\u4e4b\u7d81\u5b9a\u78bc\u3002" + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "\u7d81\u5b9a\u78bc" + }, + "description": "\u7531\u50b3\u611f\u5668\u6240\u5ee3\u64ad\u4e4b\u8cc7\u6599\u70ba\u52a0\u5bc6\u8cc7\u6599\u3002\u82e5\u8981\u89e3\u78bc\u3001\u9700\u8981 24 \u500b\u5b57\u5143\u4e4b\u7d81\u5b9a\u78bc\u3002" + }, "user": { "data": { "address": "\u88dd\u7f6e" From 2b617e3885d8a2d1b8c9299abc5fd1e547ccb5ac Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 26 Jul 2022 03:04:19 +0200 Subject: [PATCH 2847/3516] Improve mqtt MessageCallback typing (#75614) * Improve mqtt MessageCallback typing * Use MQTTMessage --- homeassistant/components/mqtt/client.py | 13 +++++++------ homeassistant/components/mqtt/models.py | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 81142fadb87..192de624f17 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable, Iterable +from collections.abc import Awaitable, Callable, Coroutine, Iterable from functools import lru_cache, partial, wraps import inspect from itertools import groupby @@ -15,6 +15,7 @@ import uuid import attr import certifi +from paho.mqtt.client import MQTTMessage from homeassistant.const import ( CONF_CLIENT_ID, @@ -246,7 +247,7 @@ class Subscription: topic: str = attr.ib() matcher: Any = attr.ib() - job: HassJob = attr.ib() + job: HassJob[[ReceiveMessage], Coroutine[Any, Any, None] | None] = attr.ib() qos: int = attr.ib(default=0) encoding: str | None = attr.ib(default="utf-8") @@ -444,7 +445,7 @@ class MQTT: async def async_subscribe( self, topic: str, - msg_callback: MessageCallbackType, + msg_callback: AsyncMessageCallbackType | MessageCallbackType, qos: int, encoding: str | None = None, ) -> Callable[[], None]: @@ -597,15 +598,15 @@ class MQTT: self.hass.add_job(self._mqtt_handle_message, msg) @lru_cache(2048) - def _matching_subscriptions(self, topic): - subscriptions = [] + def _matching_subscriptions(self, topic: str) -> list[Subscription]: + subscriptions: list[Subscription] = [] for subscription in self.subscriptions: if subscription.matcher(topic): subscriptions.append(subscription) return subscriptions @callback - def _mqtt_handle_message(self, msg) -> None: + def _mqtt_handle_message(self, msg: MQTTMessage) -> None: _LOGGER.debug( "Received message on %s%s: %s", msg.topic, diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index d5560f6954e..84bf704a262 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -2,7 +2,7 @@ from __future__ import annotations from ast import literal_eval -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Coroutine import datetime as dt from typing import Any, Union @@ -42,7 +42,7 @@ class ReceiveMessage: timestamp: dt.datetime = attr.ib(default=None) -AsyncMessageCallbackType = Callable[[ReceiveMessage], Awaitable[None]] +AsyncMessageCallbackType = Callable[[ReceiveMessage], Coroutine[Any, Any, None]] MessageCallbackType = Callable[[ReceiveMessage], None] From 1f73a553c803869d8baaf523a6a02b4d79bac809 Mon Sep 17 00:00:00 2001 From: qiz-li <47799695+qiz-li@users.noreply.github.com> Date: Mon, 25 Jul 2022 19:50:04 -0700 Subject: [PATCH 2848/3516] Bump Switchmate dependency to 0.5.1 (#75163) Co-authored-by: J. Nick Koston --- CODEOWNERS | 2 +- .../components/switchmate/manifest.json | 4 ++-- homeassistant/components/switchmate/switch.py | 16 ++++++++-------- requirements_all.txt | 2 +- script/gen_requirements_all.py | 1 - 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index bcbd6c20364..3ad4c23816c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1046,7 +1046,7 @@ build.json @home-assistant/supervisor /tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas /homeassistant/components/switcher_kis/ @tomerfi @thecode /tests/components/switcher_kis/ @tomerfi @thecode -/homeassistant/components/switchmate/ @danielhiversen +/homeassistant/components/switchmate/ @danielhiversen @qiz-li /homeassistant/components/syncthing/ @zhulik /tests/components/syncthing/ @zhulik /homeassistant/components/syncthru/ @nielstron diff --git a/homeassistant/components/switchmate/manifest.json b/homeassistant/components/switchmate/manifest.json index c4a263aca19..930fb6bf88e 100644 --- a/homeassistant/components/switchmate/manifest.json +++ b/homeassistant/components/switchmate/manifest.json @@ -2,8 +2,8 @@ "domain": "switchmate", "name": "Switchmate SimplySmart Home", "documentation": "https://www.home-assistant.io/integrations/switchmate", - "requirements": ["pySwitchmate==0.4.6"], - "codeowners": ["@danielhiversen"], + "requirements": ["pySwitchmate==0.5.1"], + "codeowners": ["@danielhiversen", "@qiz-li"], "iot_class": "local_polling", "loggers": ["switchmate"] } diff --git a/homeassistant/components/switchmate/switch.py b/homeassistant/components/switchmate/switch.py index b0f2b58a1fa..7beb89f8de1 100644 --- a/homeassistant/components/switchmate/switch.py +++ b/homeassistant/components/switchmate/switch.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import timedelta # pylint: disable=import-error -import switchmate +from switchmate import Switchmate import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity @@ -49,7 +49,7 @@ class SwitchmateEntity(SwitchEntity): self._mac = mac self._name = name - self._device = switchmate.Switchmate(mac=mac, flip_on_off=flip_on_off) + self._device = Switchmate(mac=mac, flip_on_off=flip_on_off) @property def unique_id(self) -> str: @@ -66,19 +66,19 @@ class SwitchmateEntity(SwitchEntity): """Return the name of the switch.""" return self._name - def update(self) -> None: + async def async_update(self) -> None: """Synchronize state with switch.""" - self._device.update() + await self._device.update() @property def is_on(self) -> bool: """Return true if it is on.""" return self._device.state - def turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs) -> None: """Turn the switch on.""" - self._device.turn_on() + await self._device.turn_on() - def turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Turn the switch off.""" - self._device.turn_off() + await self._device.turn_off() diff --git a/requirements_all.txt b/requirements_all.txt index 526f5f29c71..6aaa99d7838 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1357,7 +1357,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.switchmate -# pySwitchmate==0.4.6 +pySwitchmate==0.5.1 # homeassistant.components.tibber pyTibber==0.22.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index dcc53a73df0..5ef794b4eab 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -30,7 +30,6 @@ COMMENT_REQUIREMENTS = ( "opencv-python-headless", "pybluez", "pycups", - "pySwitchmate", "python-eq3bt", "python-gammu", "python-lirc", From ce4e53938cbee61244a22595526f10141dffe799 Mon Sep 17 00:00:00 2001 From: On Freund Date: Tue, 26 Jul 2022 07:24:39 +0300 Subject: [PATCH 2849/3516] Change monoprice config flow to sync (#75306) --- homeassistant/components/monoprice/config_flow.py | 4 ++-- tests/components/monoprice/test_config_flow.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/monoprice/config_flow.py b/homeassistant/components/monoprice/config_flow.py index 4065b003ba3..9c659d4f733 100644 --- a/homeassistant/components/monoprice/config_flow.py +++ b/homeassistant/components/monoprice/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from pymonoprice import get_async_monoprice +from pymonoprice import get_monoprice from serial import SerialException import voluptuous as vol @@ -56,7 +56,7 @@ async def validate_input(hass: core.HomeAssistant, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ try: - await get_async_monoprice(data[CONF_PORT], hass.loop) + await hass.async_add_executor_job(get_monoprice, data[CONF_PORT]) except SerialException as err: _LOGGER.error("Error connecting to Monoprice controller") raise CannotConnect from err diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index b88d4d240bd..c9f5bb9dee7 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -33,7 +33,7 @@ async def test_form(hass): assert result["errors"] == {} with patch( - "homeassistant.components.monoprice.config_flow.get_async_monoprice", + "homeassistant.components.monoprice.config_flow.get_monoprice", return_value=True, ), patch( "homeassistant.components.monoprice.async_setup_entry", @@ -60,7 +60,7 @@ async def test_form_cannot_connect(hass): ) with patch( - "homeassistant.components.monoprice.config_flow.get_async_monoprice", + "homeassistant.components.monoprice.config_flow.get_monoprice", side_effect=SerialException, ): result2 = await hass.config_entries.flow.async_configure( @@ -78,7 +78,7 @@ async def test_generic_exception(hass): ) with patch( - "homeassistant.components.monoprice.config_flow.get_async_monoprice", + "homeassistant.components.monoprice.config_flow.get_monoprice", side_effect=Exception, ): result2 = await hass.config_entries.flow.async_configure( From b57e0d13b454b9ae7077f291de90b05491624b4b Mon Sep 17 00:00:00 2001 From: Pawel Date: Tue, 26 Jul 2022 08:50:21 +0200 Subject: [PATCH 2850/3516] Fix Epson wrong volume value (#75264) --- homeassistant/components/epson/manifest.json | 2 +- homeassistant/components/epson/media_player.py | 7 ++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/epson/manifest.json b/homeassistant/components/epson/manifest.json index 310b66c0d37..82b74486377 100644 --- a/homeassistant/components/epson/manifest.json +++ b/homeassistant/components/epson/manifest.json @@ -3,7 +3,7 @@ "name": "Epson", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/epson", - "requirements": ["epson-projector==0.4.2"], + "requirements": ["epson-projector==0.4.6"], "codeowners": ["@pszafer"], "iot_class": "local_polling", "loggers": ["epson_projector"] diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index f72b0f69d69..98152efb3b2 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -133,7 +133,10 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): self._source = SOURCE_LIST.get(source, self._source) volume = await self._projector.get_property(VOLUME) if volume: - self._volume = volume + try: + self._volume = float(volume) + except ValueError: + self._volume = None elif power_state == BUSY: self._state = STATE_ON else: @@ -176,11 +179,13 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): """Turn on epson.""" if self._state == STATE_OFF: await self._projector.send_command(TURN_ON) + self._state = STATE_ON async def async_turn_off(self): """Turn off epson.""" if self._state == STATE_ON: await self._projector.send_command(TURN_OFF) + self._state = STATE_OFF @property def source_list(self): diff --git a/requirements_all.txt b/requirements_all.txt index 6aaa99d7838..3804872c58e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -619,7 +619,7 @@ envoy_reader==0.20.1 ephem==4.1.2 # homeassistant.components.epson -epson-projector==0.4.2 +epson-projector==0.4.6 # homeassistant.components.epsonworkforce epsonprinter==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a24245dfc3a..1fae9c42e30 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -465,7 +465,7 @@ envoy_reader==0.20.1 ephem==4.1.2 # homeassistant.components.epson -epson-projector==0.4.2 +epson-projector==0.4.6 # homeassistant.components.faa_delays faadelays==0.0.7 From 5e45b0baf977be00743170962ce7701021bb9a32 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Jul 2022 09:03:31 +0200 Subject: [PATCH 2851/3516] Automatically set up Xiaomi BLE during onboarding (#75748) --- .../components/xiaomi_ble/config_flow.py | 3 ++- .../components/xiaomi_ble/test_config_flow.py | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index 8b3ec22def7..e7c4a3e1f8c 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -8,6 +8,7 @@ import voluptuous as vol from xiaomi_ble import XiaomiBluetoothDeviceData as DeviceData from xiaomi_ble.parser import EncryptionScheme +from homeassistant.components import onboarding from homeassistant.components.bluetooth import ( BluetoothServiceInfo, async_discovered_service_info, @@ -139,7 +140,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm discovery.""" - if user_input is not None: + if user_input is not None or not onboarding.async_is_onboarded(self.hass): return self.async_create_entry( title=self.context["title_placeholders"]["name"], data={}, diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index fc625bdf7ec..d4b4300d2c1 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -38,6 +38,28 @@ async def test_async_step_bluetooth_valid_device(hass): assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" +async def test_async_step_bluetooth_during_onboarding(hass): + """Test discovery via bluetooth during onboarding.""" + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MMC_T201_1_SERVICE_INFO, + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "MMC_T201_1" + assert result["data"] == {} + assert result["result"].unique_id == "00:81:F9:DD:6F:C1" + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_onboarding.mock_calls) == 1 + + async def test_async_step_bluetooth_valid_device_legacy_encryption(hass): """Test discovery via bluetooth with a valid device, with legacy encryption.""" result = await hass.config_entries.flow.async_init( From 47713d968620f146251d10032ff2c84c0fc3e3ae Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Tue, 26 Jul 2022 00:52:15 -0700 Subject: [PATCH 2852/3516] Raise issue for Google Calendar YAML deprecations (#75743) --- homeassistant/components/google/__init__.py | 29 ++++++++++++------- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/google/strings.json | 10 +++++++ .../components/google/translations/en.json | 10 +++++++ 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index e05de4f5b79..4b72aaa77ad 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -20,6 +20,7 @@ from homeassistant.components.application_credentials import ( ClientCredential, async_import_client_credential, ) +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_CLIENT_ID, @@ -203,21 +204,27 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: }, ) ) - - _LOGGER.warning( - "Configuration of Google Calendar in YAML in configuration.yaml is " - "is deprecated and will be removed in a future release; Your existing " - "OAuth Application Credentials and access settings have been imported " - "into the UI automatically and can be safely removed from your " - "configuration.yaml file" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", # Warning first added in 2022.6.0 + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", ) if conf.get(CONF_TRACK_NEW) is False: # The track_new as False would previously result in new entries - # in google_calendars.yaml with track set to Fasle which is + # in google_calendars.yaml with track set to False which is # handled at calendar entity creation time. - _LOGGER.warning( - "You must manually set the integration System Options in the " - "UI to disable newly discovered entities going forward" + async_create_issue( + hass, + DOMAIN, + "removed_track_new_yaml", + breaks_in_ha_version="2022.6.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_track_new_yaml", ) return True diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index d39f2093cf0..bf745a72927 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -2,7 +2,7 @@ "domain": "google", "name": "Google Calendars", "config_flow": true, - "dependencies": ["application_credentials"], + "dependencies": ["application_credentials", "repairs"], "documentation": "https://www.home-assistant.io/integrations/calendar.google/", "requirements": ["gcal-sync==0.10.0", "oauth2client==4.1.3"], "codeowners": ["@allenporter"], diff --git a/homeassistant/components/google/strings.json b/homeassistant/components/google/strings.json index b4c5270e003..58e5cedd98d 100644 --- a/homeassistant/components/google/strings.json +++ b/homeassistant/components/google/strings.json @@ -41,5 +41,15 @@ }, "application_credentials": { "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Calendar. You also need to create Application Credentials linked to your Calendar:\n1. Go to [Credentials]({oauth_creds_url}) and click **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **TV and Limited Input devices** for the Application Type.\n\n" + }, + "issues": { + "deprecated_yaml": { + "title": "The Google Calendar YAML configuration is being removed", + "description": "Configuring the Google Calendar in configuration.yaml is being removed in Home Assistant 2022.9.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + }, + "removed_track_new_yaml": { + "title": "Google Calendar entity tracking has changed", + "description": "You have disabled entity tracking for Google Calendar in configuration.yaml, which is no longer supported. You must manually change the integration System Options in the UI to disable newly discovered entities going forward. Remove the track_new setting from configuration.yaml and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/google/translations/en.json b/homeassistant/components/google/translations/en.json index 1720e8c1454..bd1769fcbe0 100644 --- a/homeassistant/components/google/translations/en.json +++ b/homeassistant/components/google/translations/en.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Configuring the Google Calendar in configuration.yaml is being removed in Home Assistant 2022.9.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Google Calendar YAML configuration is being removed" + }, + "removed_track_new_yaml": { + "description": "Your Google Calendar configuration.yaml has disabled new entity tracking, which is no longer supported. You must manually set the integration System Options in the UI to disable newly discovered entities going forward. Remove the track_new setting from configuration.yaml and restart Home Assistant to fix this issue.", + "title": "The Google Calendar entity tracking has changed" + } + }, "options": { "step": { "init": { From a98f658854682ec1908d1b5b179032ca1cfb3ab0 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 26 Jul 2022 10:32:26 +0200 Subject: [PATCH 2853/3516] Update xknx to 0.22.0 (#75749) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/knx/conftest.py | 12 ++++++++---- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 0c34428f0a1..4197cb76209 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.21.5"], + "requirements": ["xknx==0.22.0"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 3804872c58e..425b114231a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2480,7 +2480,7 @@ xboxapi==2.0.1 xiaomi-ble==0.5.1 # homeassistant.components.knx -xknx==0.21.5 +xknx==0.22.0 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1fae9c42e30..3c365f1fc85 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1665,7 +1665,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.5.1 # homeassistant.components.knx -xknx==0.21.5 +xknx==0.22.0 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index ccfd3a35085..e5cf18b0c3c 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -55,19 +55,23 @@ class KNXTestKit: async def setup_integration(self, config): """Create the KNX integration.""" + def disable_rate_limiter(): + """Disable rate limiter for tests.""" + # after XKNX.__init__() to not overwrite it by the config entry again + # before StateUpdater starts to avoid slow down of tests + self.xknx.rate_limit = 0 + def knx_ip_interface_mock(): """Create a xknx knx ip interface mock.""" mock = Mock() - mock.start = AsyncMock() + mock.start = AsyncMock(side_effect=disable_rate_limiter) mock.stop = AsyncMock() mock.send_telegram = AsyncMock(side_effect=self._outgoing_telegrams.put) return mock def fish_xknx(*args, **kwargs): """Get the XKNX object from the constructor call.""" - self.xknx = kwargs["xknx"] - # disable rate limiter for tests (before StateUpdater starts) - self.xknx.rate_limit = 0 + self.xknx = args[0] return DEFAULT with patch( From eaee923e4cde4d6ed9a1fcf31f66484b063dabe6 Mon Sep 17 00:00:00 2001 From: Tom Schneider Date: Tue, 26 Jul 2022 10:47:03 +0200 Subject: [PATCH 2854/3516] Fix hvv departures authentication (#75146) --- homeassistant/components/hvv_departures/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hvv_departures/manifest.json b/homeassistant/components/hvv_departures/manifest.json index f0334b5af92..eb7be8b8c64 100644 --- a/homeassistant/components/hvv_departures/manifest.json +++ b/homeassistant/components/hvv_departures/manifest.json @@ -3,7 +3,7 @@ "name": "HVV Departures", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hvv_departures", - "requirements": ["pygti==0.9.2"], + "requirements": ["pygti==0.9.3"], "codeowners": ["@vigonotion"], "iot_class": "cloud_polling", "loggers": ["pygti"] diff --git a/requirements_all.txt b/requirements_all.txt index 425b114231a..47afd92827c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1549,7 +1549,7 @@ pygatt[GATTTOOL]==4.0.5 pygtfs==0.1.6 # homeassistant.components.hvv_departures -pygti==0.9.2 +pygti==0.9.3 # homeassistant.components.version pyhaversion==22.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c365f1fc85..32d8b2bf514 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1055,7 +1055,7 @@ pyfronius==0.7.1 pyfttt==0.3 # homeassistant.components.hvv_departures -pygti==0.9.2 +pygti==0.9.3 # homeassistant.components.version pyhaversion==22.4.1 From 58b7f9a0324c82df7523f2ebe56c8e6e7f34a8d8 Mon Sep 17 00:00:00 2001 From: Tom Schneider Date: Tue, 26 Jul 2022 10:47:03 +0200 Subject: [PATCH 2855/3516] Fix hvv departures authentication (#75146) --- homeassistant/components/hvv_departures/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hvv_departures/manifest.json b/homeassistant/components/hvv_departures/manifest.json index f0334b5af92..eb7be8b8c64 100644 --- a/homeassistant/components/hvv_departures/manifest.json +++ b/homeassistant/components/hvv_departures/manifest.json @@ -3,7 +3,7 @@ "name": "HVV Departures", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hvv_departures", - "requirements": ["pygti==0.9.2"], + "requirements": ["pygti==0.9.3"], "codeowners": ["@vigonotion"], "iot_class": "cloud_polling", "loggers": ["pygti"] diff --git a/requirements_all.txt b/requirements_all.txt index 11b785c4fc5..537e49c35ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1532,7 +1532,7 @@ pygatt[GATTTOOL]==4.0.5 pygtfs==0.1.6 # homeassistant.components.hvv_departures -pygti==0.9.2 +pygti==0.9.3 # homeassistant.components.version pyhaversion==22.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 63ef39eb63e..5afb6c3cce7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1032,7 +1032,7 @@ pyfttt==0.3 pygatt[GATTTOOL]==4.0.5 # homeassistant.components.hvv_departures -pygti==0.9.2 +pygti==0.9.3 # homeassistant.components.version pyhaversion==22.4.1 From a3950937e005c3bfc82583c3889e67cdbe76d664 Mon Sep 17 00:00:00 2001 From: Pawel Date: Tue, 26 Jul 2022 08:50:21 +0200 Subject: [PATCH 2856/3516] Fix Epson wrong volume value (#75264) --- homeassistant/components/epson/manifest.json | 2 +- homeassistant/components/epson/media_player.py | 7 ++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/epson/manifest.json b/homeassistant/components/epson/manifest.json index 310b66c0d37..82b74486377 100644 --- a/homeassistant/components/epson/manifest.json +++ b/homeassistant/components/epson/manifest.json @@ -3,7 +3,7 @@ "name": "Epson", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/epson", - "requirements": ["epson-projector==0.4.2"], + "requirements": ["epson-projector==0.4.6"], "codeowners": ["@pszafer"], "iot_class": "local_polling", "loggers": ["epson_projector"] diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index f72b0f69d69..98152efb3b2 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -133,7 +133,10 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): self._source = SOURCE_LIST.get(source, self._source) volume = await self._projector.get_property(VOLUME) if volume: - self._volume = volume + try: + self._volume = float(volume) + except ValueError: + self._volume = None elif power_state == BUSY: self._state = STATE_ON else: @@ -176,11 +179,13 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): """Turn on epson.""" if self._state == STATE_OFF: await self._projector.send_command(TURN_ON) + self._state = STATE_ON async def async_turn_off(self): """Turn off epson.""" if self._state == STATE_ON: await self._projector.send_command(TURN_OFF) + self._state = STATE_OFF @property def source_list(self): diff --git a/requirements_all.txt b/requirements_all.txt index 537e49c35ef..04a02d55ba9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -610,7 +610,7 @@ envoy_reader==0.20.1 ephem==4.1.2 # homeassistant.components.epson -epson-projector==0.4.2 +epson-projector==0.4.6 # homeassistant.components.epsonworkforce epsonprinter==0.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5afb6c3cce7..e1625a465e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -447,7 +447,7 @@ envoy_reader==0.20.1 ephem==4.1.2 # homeassistant.components.epson -epson-projector==0.4.2 +epson-projector==0.4.6 # homeassistant.components.faa_delays faadelays==0.0.7 From ec4835ef046930fb64e4fbc7afe922dd0ffdad4b Mon Sep 17 00:00:00 2001 From: On Freund Date: Tue, 26 Jul 2022 07:24:39 +0300 Subject: [PATCH 2857/3516] Change monoprice config flow to sync (#75306) --- homeassistant/components/monoprice/config_flow.py | 4 ++-- tests/components/monoprice/test_config_flow.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/monoprice/config_flow.py b/homeassistant/components/monoprice/config_flow.py index 4065b003ba3..9c659d4f733 100644 --- a/homeassistant/components/monoprice/config_flow.py +++ b/homeassistant/components/monoprice/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from pymonoprice import get_async_monoprice +from pymonoprice import get_monoprice from serial import SerialException import voluptuous as vol @@ -56,7 +56,7 @@ async def validate_input(hass: core.HomeAssistant, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ try: - await get_async_monoprice(data[CONF_PORT], hass.loop) + await hass.async_add_executor_job(get_monoprice, data[CONF_PORT]) except SerialException as err: _LOGGER.error("Error connecting to Monoprice controller") raise CannotConnect from err diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index 0ed4ac35eaa..98275b052e4 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -33,7 +33,7 @@ async def test_form(hass): assert result["errors"] == {} with patch( - "homeassistant.components.monoprice.config_flow.get_async_monoprice", + "homeassistant.components.monoprice.config_flow.get_monoprice", return_value=True, ), patch( "homeassistant.components.monoprice.async_setup_entry", @@ -60,7 +60,7 @@ async def test_form_cannot_connect(hass): ) with patch( - "homeassistant.components.monoprice.config_flow.get_async_monoprice", + "homeassistant.components.monoprice.config_flow.get_monoprice", side_effect=SerialException, ): result2 = await hass.config_entries.flow.async_configure( @@ -78,7 +78,7 @@ async def test_generic_exception(hass): ) with patch( - "homeassistant.components.monoprice.config_flow.get_async_monoprice", + "homeassistant.components.monoprice.config_flow.get_monoprice", side_effect=Exception, ): result2 = await hass.config_entries.flow.async_configure( From 0407fc45816329d8dbb75afe379a5870947f93fc Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Fri, 22 Jul 2022 11:46:00 +0800 Subject: [PATCH 2858/3516] Round up for stream record lookback (#75580) --- homeassistant/components/stream/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index f0b4ed99654..354f9a77672 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -503,15 +503,16 @@ class Stream: await self.start() + self._logger.debug("Started a stream recording of %s seconds", duration) + # Take advantage of lookback hls: HlsStreamOutput = cast(HlsStreamOutput, self.outputs().get(HLS_PROVIDER)) - if lookback > 0 and hls: - num_segments = min(int(lookback // hls.target_duration), MAX_SEGMENTS) + if hls: + num_segments = min(int(lookback / hls.target_duration) + 1, MAX_SEGMENTS) # Wait for latest segment, then add the lookback await hls.recv() recorder.prepend(list(hls.get_segments())[-num_segments - 1 : -1]) - self._logger.debug("Started a stream recording of %s seconds", duration) await recorder.async_record() async def async_get_image( From 9173aef1ef4151fb8d7ed79a344dbc59739518fa Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 24 Jul 2022 14:09:02 -0600 Subject: [PATCH 2859/3516] Revert SimpliSafe auth flow to the quasi-manual OAuth method from 2021.11.0 (#75641) * Revert "Migrate SimpliSafe to new web-based authentication (#57212)" This reverts commit bf7c99c1f8f33720149b58a0a3b1687189b29179. * Tests 100% * Version bump * Add manifest version for custom component testing * Remove manifest version * Code review * Fix tests --- .../components/simplisafe/__init__.py | 4 +- .../components/simplisafe/config_flow.py | 269 ++++--------- .../components/simplisafe/manifest.json | 2 +- .../components/simplisafe/strings.json | 26 +- .../simplisafe/translations/en.json | 26 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/simplisafe/conftest.py | 18 +- .../components/simplisafe/test_config_flow.py | 364 +++++------------- 9 files changed, 192 insertions(+), 521 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index c74efab61ac..660f45b355d 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -278,10 +278,8 @@ def _async_standardize_config_entry(hass: HomeAssistant, entry: ConfigEntry) -> """Bring a config entry up to current standards.""" if CONF_TOKEN not in entry.data: raise ConfigEntryAuthFailed( - "New SimpliSafe OAuth standard requires re-authentication" + "SimpliSafe OAuth standard requires re-authentication" ) - if CONF_USERNAME not in entry.data: - raise ConfigEntryAuthFailed("Need to re-auth with username/password") entry_updates = {} if not entry.unique_id: diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 0b95de2c186..0b92871ccb2 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -1,48 +1,52 @@ """Config flow to configure the SimpliSafe component.""" from __future__ import annotations -import asyncio from collections.abc import Mapping -from typing import Any +from typing import Any, NamedTuple -import async_timeout from simplipy import API -from simplipy.api import AuthStates -from simplipy.errors import InvalidCredentialsError, SimplipyError, Verify2FAPending +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_TOKEN, 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, config_validation as cv from .const import DOMAIN, LOGGER -DEFAULT_EMAIL_2FA_SLEEP = 3 -DEFAULT_EMAIL_2FA_TIMEOUT = 600 - -STEP_REAUTH_SCHEMA = vol.Schema( - { - vol.Required(CONF_PASSWORD): cv.string, - } -) - -STEP_SMS_2FA_SCHEMA = vol.Schema( - { - vol.Required(CONF_CODE): cv.string, - } -) +CONF_AUTH_CODE = "auth_code" STEP_USER_SCHEMA = vol.Schema( { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_AUTH_CODE): cv.string, } ) +class SimpliSafeOAuthValues(NamedTuple): + """Define a named tuple to handle SimpliSafe OAuth strings.""" + + auth_url: str + code_verifier: 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(auth_url, code_verifier) + + class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a SimpliSafe config flow.""" @@ -50,45 +54,8 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._email_2fa_task: asyncio.Task | None = None - self._password: str | None = None + self._oauth_values: SimpliSafeOAuthValues = async_get_simplisafe_oauth_values() self._reauth: bool = False - self._simplisafe: API | None = None - self._username: str | None = None - - async def _async_authenticate( - self, originating_step_id: str, originating_step_schema: vol.Schema - ) -> FlowResult: - """Attempt to authenticate to the SimpliSafe API.""" - assert self._password - assert self._username - - errors = {} - session = aiohttp_client.async_get_clientsession(self.hass) - - try: - self._simplisafe = await API.async_from_credentials( - self._username, self._password, session=session - ) - 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: - return self.async_show_form( - step_id=originating_step_id, - data_schema=originating_step_schema, - errors=errors, - description_placeholders={CONF_USERNAME: self._username}, - ) - - assert self._simplisafe - - if self._simplisafe.auth_state == AuthStates.PENDING_2FA_SMS: - return await self.async_step_sms_2fa() - return await self.async_step_email_2fa() @staticmethod @callback @@ -98,146 +65,66 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Define the config flow to handle options.""" return SimpliSafeOptionsFlowHandler(config_entry) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth(self, config: Mapping[str, Any]) -> FlowResult: """Handle configuration by re-auth.""" self._reauth = True - - if CONF_USERNAME not in entry_data: - # Old versions of the config flow may not have the username by this point; - # in that case, we reauth them by making them go through the user flow: - return await self.async_step_user() - - self._username = entry_data[CONF_USERNAME] - return await self.async_step_reauth_confirm() - - async def _async_get_email_2fa(self) -> None: - """Define a task to wait for email-based 2FA.""" - assert self._simplisafe - - try: - async with async_timeout.timeout(DEFAULT_EMAIL_2FA_TIMEOUT): - while True: - try: - await self._simplisafe.async_verify_2fa_email() - except Verify2FAPending: - LOGGER.info("Email-based 2FA pending; trying again") - await asyncio.sleep(DEFAULT_EMAIL_2FA_SLEEP) - else: - break - finally: - self.hass.async_create_task( - self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) - ) - - async def async_step_email_2fa( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle email-based two-factor authentication.""" - if not self._email_2fa_task: - self._email_2fa_task = self.hass.async_create_task( - self._async_get_email_2fa() - ) - return self.async_show_progress( - step_id="email_2fa", progress_action="email_2fa" - ) - - try: - await self._email_2fa_task - except asyncio.TimeoutError: - return self.async_show_progress_done(next_step_id="email_2fa_error") - return self.async_show_progress_done(next_step_id="finish") - - async def async_step_email_2fa_error( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle an error during email-based two-factor authentication.""" - return self.async_abort(reason="email_2fa_timed_out") - - async def async_step_finish( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle the final step.""" - assert self._simplisafe - assert self._username - - data = { - CONF_USERNAME: self._username, - CONF_TOKEN: self._simplisafe.refresh_token, - } - - user_id = str(self._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 – - # only one can exist at a time, but the presence of either one is a - # candidate for re-auth: - if existing_entries := [ - entry - for entry in self.hass.config_entries.async_entries() - if entry.domain == DOMAIN - and entry.unique_id in (self._username, user_id) - ]: - existing_entry = existing_entries[0] - self.hass.config_entries.async_update_entry( - existing_entry, unique_id=user_id, title=self._username, data=data - ) - self.hass.async_create_task( - self.hass.config_entries.async_reload(existing_entry.entry_id) - ) - return self.async_abort(reason="reauth_successful") - - await self.async_set_unique_id(user_id) - self._abort_if_unique_id_configured() - return self.async_create_entry(title=self._username, data=data) - - 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_SCHEMA, - description_placeholders={CONF_USERNAME: self._username}, - ) - - self._password = user_input[CONF_PASSWORD] - return await self._async_authenticate("reauth_confirm", STEP_REAUTH_SCHEMA) - - async def async_step_sms_2fa( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle SMS-based two-factor authentication.""" - if not user_input: - return self.async_show_form( - step_id="sms_2fa", - data_schema=STEP_SMS_2FA_SCHEMA, - ) - - assert self._simplisafe - - try: - await self._simplisafe.async_verify_2fa_sms(user_input[CONF_CODE]) - except InvalidCredentialsError: - return self.async_show_form( - step_id="sms_2fa", - data_schema=STEP_SMS_2FA_SCHEMA, - errors={CONF_CODE: "invalid_auth"}, - ) - - return await self.async_step_finish() + 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 user_input is None: - return self.async_show_form(step_id="user", data_schema=STEP_USER_SCHEMA) + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_SCHEMA, + description_placeholders={CONF_URL: self._oauth_values.auth_url}, + ) - self._username = user_input[CONF_USERNAME] - self._password = user_input[CONF_PASSWORD] - return await self._async_authenticate("user", STEP_USER_SCHEMA) + errors = {} + session = aiohttp_client.async_get_clientsession(self.hass) + + try: + simplisafe = await API.async_from_auth( + user_input[CONF_AUTH_CODE], + self._oauth_values.code_verifier, + session=session, + ) + 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: + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_SCHEMA, + errors=errors, + description_placeholders={CONF_URL: self._oauth_values.auth_url}, + ) + + simplisafe_user_id = str(simplisafe.user_id) + data = {CONF_USERNAME: simplisafe_user_id, CONF_TOKEN: simplisafe.refresh_token} + + if self._reauth: + existing_entry = await self.async_set_unique_id(simplisafe_user_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=simplisafe_user_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") + + await self.async_set_unique_id(simplisafe_user_id) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=simplisafe_user_id, data=data) class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow): diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index b6a139fba80..b08799e4082 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==2022.07.0"], + "requirements": ["simplisafe-python==2022.07.1"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "dhcp": [ diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 85e579fd455..16ae7111abf 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -1,38 +1,22 @@ { "config": { "step": { - "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%]" - } - }, - "sms_2fa": { - "description": "Input the two-factor authentication code sent to you via SMS.", - "data": { - "code": "Code" - } - }, "user": { - "description": "Input your username and password.", + "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL.", "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]" + "auth_code": "Authorization Code" } } }, "error": { + "identifier_exists": "Account already registered", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { "already_configured": "This SimpliSafe account is already in use.", - "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" - }, - "progress": { - "email_2fa": "Check your email for a verification link from Simplisafe." + "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 0da6f6442e4..82320df4864 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -2,36 +2,20 @@ "config": { "abort": { "already_configured": "This SimpliSafe account is already in use.", - "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", - "reauth_successful": "Re-authentication was successful" + "reauth_successful": "Re-authentication was successful", + "wrong_account": "The user credentials provided do not match this SimpliSafe account." }, "error": { + "identifier_exists": "Account already registered", "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, - "progress": { - "email_2fa": "Check your email for a verification link from Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Password" - }, - "description": "Please re-enter the password for {username}.", - "title": "Reauthenticate Integration" - }, - "sms_2fa": { - "data": { - "code": "Code" - }, - "description": "Input the two-factor authentication code sent to you via SMS." - }, "user": { "data": { - "password": "Password", - "username": "Username" + "auth_code": "Authorization Code" }, - "description": "Input your username and password." + "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL." } } }, diff --git a/requirements_all.txt b/requirements_all.txt index 04a02d55ba9..1ca6064c4f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2168,7 +2168,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.07.0 +simplisafe-python==2022.07.1 # homeassistant.components.sisyphus sisyphus-control==3.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e1625a465e4..54418e06eb7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1440,7 +1440,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==2022.07.0 +simplisafe-python==2022.07.1 # homeassistant.components.slack slackclient==2.5.0 diff --git a/tests/components/simplisafe/conftest.py b/tests/components/simplisafe/conftest.py index 82bd04a7349..54ab7fbe9d7 100644 --- a/tests/components/simplisafe/conftest.py +++ b/tests/components/simplisafe/conftest.py @@ -3,7 +3,6 @@ import json from unittest.mock import AsyncMock, Mock, patch import pytest -from simplipy.api import AuthStates from simplipy.system.v3 import SystemV3 from homeassistant.components.simplisafe.const import DOMAIN @@ -19,20 +18,11 @@ PASSWORD = "password" SYSTEM_ID = "system_123" -@pytest.fixture(name="api_auth_state") -def api_auth_state_fixture(): - """Define a SimpliSafe API auth state.""" - return AuthStates.PENDING_2FA_SMS - - @pytest.fixture(name="api") -def api_fixture(api_auth_state, data_subscription, system_v3, websocket): +def api_fixture(data_subscription, system_v3, websocket): """Define a simplisafe-python API object.""" return Mock( async_get_systems=AsyncMock(return_value={SYSTEM_ID: system_v3}), - async_verify_2fa_email=AsyncMock(), - async_verify_2fa_sms=AsyncMock(), - auth_state=api_auth_state, refresh_token=REFRESH_TOKEN, subscription_data=data_subscription, user_id=USER_ID, @@ -104,12 +94,10 @@ def reauth_config_fixture(): async def setup_simplisafe_fixture(hass, api, config): """Define a fixture to set up SimpliSafe.""" with patch( - "homeassistant.components.simplisafe.config_flow.DEFAULT_EMAIL_2FA_SLEEP", 0 - ), patch( - "homeassistant.components.simplisafe.config_flow.API.async_from_credentials", + "homeassistant.components.simplisafe.config_flow.API.async_from_auth", return_value=api, ), patch( - "homeassistant.components.simplisafe.API.async_from_credentials", + "homeassistant.components.simplisafe.API.async_from_auth", return_value=api, ), patch( "homeassistant.components.simplisafe.API.async_from_refresh_token", diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 2e0b85bc6c6..4cb248cdbd0 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -2,45 +2,53 @@ from unittest.mock import patch import pytest -from simplipy.api import AuthStates -from simplipy.errors import InvalidCredentialsError, SimplipyError, Verify2FAPending +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.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_CODE, CONF_TOKEN, CONF_USERNAME -from .common import REFRESH_TOKEN, USER_ID, USERNAME -from tests.common import MockConfigEntry - -CONF_USER_ID = "user_id" - - -async def test_duplicate_error( - hass, config_entry, credentials_config, setup_simplisafe, sms_config -): +async def test_duplicate_error(config_entry, hass, setup_simplisafe): """Test that errors are shown when duplicates are added.""" - 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 + with patch( + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ): + 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=credentials_config - ) - assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=sms_config - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + 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_options_flow(hass, config_entry): +async def test_invalid_credentials(hass): + """Test that invalid credentials show the correct error.""" + with patch( + "homeassistant.components.simplisafe.config_flow.API.async_from_auth", + 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={CONF_AUTH_CODE: "code123"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_options_flow(config_entry, hass): """Test config flow options.""" with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True @@ -53,134 +61,79 @@ async def test_options_flow(hass, config_entry): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_CODE: "4321"} ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == {CONF_CODE: "4321"} -@pytest.mark.parametrize("unique_id", [USERNAME, USER_ID]) -async def test_step_reauth( - hass, config, config_entry, reauth_config, setup_simplisafe, sms_config, unique_id -): - """Test the re-auth step (testing both username and user ID as unique ID).""" - # Add a second config entry (tied to a random domain, but with the same unique ID - # that could exist in a SimpliSafe entry) to ensure that this reauth process only - # touches the SimpliSafe entry: - entry = MockConfigEntry(domain="random", unique_id=USERNAME, data={"some": "data"}) - entry.add_to_hass(hass) - +async def test_step_reauth(config_entry, hass, setup_simplisafe): + """Test the re-auth step.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=config - ) - assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=reauth_config - ) - assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=sms_config - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "reauth_successful" - - assert len(hass.config_entries.async_entries()) == 2 - - # Test that the SimpliSafe config flow is updated: - [config_entry] = hass.config_entries.async_entries(DOMAIN) - assert config_entry.unique_id == USER_ID - assert config_entry.data == config - - # Test that the non-SimpliSafe config flow remains the same: - [config_entry] = hass.config_entries.async_entries("random") - assert config_entry == entry - - -@pytest.mark.parametrize( - "exc,error_string", - [(InvalidCredentialsError, "invalid_auth"), (SimplipyError, "unknown")], -) -async def test_step_reauth_errors(hass, config, error_string, exc, reauth_config): - """Test that errors during the reauth step are handled.""" - with patch( - "homeassistant.components.simplisafe.API.async_from_credentials", - side_effect=exc, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=config - ) - assert result["step_id"] == "reauth_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=reauth_config - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "reauth_confirm" - assert result["errors"] == {"base": error_string} - - -@pytest.mark.parametrize( - "config,unique_id", - [ - ( - { - CONF_TOKEN: REFRESH_TOKEN, - CONF_USER_ID: USER_ID, - }, - USERNAME, - ), - ( - { - CONF_TOKEN: REFRESH_TOKEN, - CONF_USER_ID: USER_ID, - }, - USER_ID, - ), - ], -) -async def test_step_reauth_from_scratch( - hass, config, config_entry, credentials_config, setup_simplisafe, sms_config -): - """Test the re-auth step when a complete redo is needed.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=config + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={CONF_USERNAME: "12345", CONF_TOKEN: "token123"}, ) 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=credentials_config - ) - assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=sms_config - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "reauth_successful" + with patch( + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + ) + 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.unique_id == USER_ID - assert config_entry.data == { - CONF_TOKEN: REFRESH_TOKEN, - CONF_USERNAME: USERNAME, - } + assert config_entry.data == {CONF_USERNAME: "12345", CONF_TOKEN: "token123"} -@pytest.mark.parametrize( - "exc,error_string", - [(InvalidCredentialsError, "invalid_auth"), (SimplipyError, "unknown")], -) -async def test_step_user_errors(hass, credentials_config, error_string, exc): - """Test that errors during the user step are handled.""" +@pytest.mark.parametrize("unique_id", ["some_other_id"]) +async def test_step_reauth_wrong_account(config_entry, hass, setup_simplisafe): + """Test the re-auth step where the wrong account is used during login.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={CONF_USERNAME: "12345", CONF_TOKEN: "token123"}, + ) + assert result["step_id"] == "user" + with patch( - "homeassistant.components.simplisafe.API.async_from_credentials", - side_effect=exc, + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "wrong_account" + + +async def test_step_user(hass, setup_simplisafe): + """Test the user step.""" + 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.config_entries.ConfigEntries.async_reload"): + 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_CREATE_ENTRY + + assert len(hass.config_entries.async_entries()) == 1 + [config_entry] = hass.config_entries.async_entries(DOMAIN) + assert config_entry.data == {CONF_USERNAME: "12345", CONF_TOKEN: "token123"} + + +async def test_unknown_error(hass, setup_simplisafe): + """Test that an unknown error shows ohe correct error.""" + with patch( + "homeassistant.components.simplisafe.config_flow.API.async_from_auth", + side_effect=SimplipyError, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} @@ -189,130 +142,7 @@ async def test_step_user_errors(hass, credentials_config, error_string, exc): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=credentials_config + result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": error_string} - - -@pytest.mark.parametrize("api_auth_state", [AuthStates.PENDING_2FA_EMAIL]) -async def test_step_user_email_2fa( - api, api_auth_state, hass, config, credentials_config, setup_simplisafe -): - """Test the user step with email-based 2FA.""" - 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 - - # Patch API.async_verify_2fa_email to first return pending, then return all done: - api.async_verify_2fa_email.side_effect = [Verify2FAPending, None] - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=credentials_config - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS_DONE - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - - assert len(hass.config_entries.async_entries()) == 1 - [config_entry] = hass.config_entries.async_entries(DOMAIN) - assert config_entry.unique_id == USER_ID - assert config_entry.data == config - - -@patch("homeassistant.components.simplisafe.config_flow.DEFAULT_EMAIL_2FA_TIMEOUT", 0) -@pytest.mark.parametrize("api_auth_state", [AuthStates.PENDING_2FA_EMAIL]) -async def test_step_user_email_2fa_timeout( - api, hass, config, credentials_config, setup_simplisafe -): - """Test a timeout during the user step with email-based 2FA.""" - 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 - - # Patch API.async_verify_2fa_email to return pending: - api.async_verify_2fa_email.side_effect = Verify2FAPending - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=credentials_config - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_SHOW_PROGRESS_DONE - assert result["step_id"] == "email_2fa_error" - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "email_2fa_timed_out" - - -async def test_step_user_sms_2fa( - hass, config, credentials_config, setup_simplisafe, sms_config -): - """Test the user step with SMS-based 2FA.""" - 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=credentials_config - ) - assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=sms_config - ) - 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.unique_id == USER_ID - assert config_entry.data == config - - -@pytest.mark.parametrize( - "exc,error_string", [(InvalidCredentialsError, "invalid_auth")] -) -async def test_step_user_sms_2fa_errors( - api, - hass, - config, - credentials_config, - error_string, - exc, - setup_simplisafe, - sms_config, -): - """Test that errors during the SMS-based 2FA step are handled.""" - 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=credentials_config - ) - assert result["step_id"] == "sms_2fa" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - # Simulate entering the incorrect SMS code: - api.async_verify_2fa_sms.side_effect = InvalidCredentialsError - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=sms_config - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"code": error_string} + assert result["errors"] == {"base": "unknown"} From 674a59f138b8bda87b7f9e41121c472ec7500bc9 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sun, 24 Jul 2022 10:48:22 +0200 Subject: [PATCH 2860/3516] Update pyotgw to 2.0.1 (#75663) --- homeassistant/components/opentherm_gw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index dfb60413721..0bc69387d0b 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -2,7 +2,7 @@ "domain": "opentherm_gw", "name": "OpenTherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", - "requirements": ["pyotgw==2.0.0"], + "requirements": ["pyotgw==2.0.1"], "codeowners": ["@mvn23"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 1ca6064c4f5..f33648aec8a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1715,7 +1715,7 @@ pyopnsense==0.2.0 pyoppleio==1.0.5 # homeassistant.components.opentherm_gw -pyotgw==2.0.0 +pyotgw==2.0.1 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 54418e06eb7..6e11f13233e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1167,7 +1167,7 @@ pyopenuv==2022.04.0 pyopnsense==0.2.0 # homeassistant.components.opentherm_gw -pyotgw==2.0.0 +pyotgw==2.0.1 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp From d756936a4e756dfb57a7e2352ef1836b0ab0b058 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 23 Jul 2022 20:06:10 -0600 Subject: [PATCH 2861/3516] Fix AssertionError in RainMachine (#75668) --- homeassistant/components/rainmachine/sensor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 7550756f8c4..2982d7176a6 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -243,10 +243,8 @@ class TimeRemainingSensor(RainMachineEntity, RestoreSensor): seconds_remaining = self.calculate_seconds_remaining() new_timestamp = now + timedelta(seconds=seconds_remaining) - assert isinstance(self._attr_native_value, datetime) - if ( - self._attr_native_value + isinstance(self._attr_native_value, datetime) and new_timestamp - self._attr_native_value < DEFAULT_ZONE_COMPLETION_TIME_WOBBLE_TOLERANCE ): From fc43ee772ce5cae9c490a58f284839f1c7d38ba6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Jul 2022 10:56:44 +0200 Subject: [PATCH 2862/3516] Bumped version to 2022.7.7 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 08c92cee96e..71d30ae608d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 7 -PATCH_VERSION: Final = "6" +PATCH_VERSION: Final = "7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 191b337eb65..97a9c51dbd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.7.6" +version = "2022.7.7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 0006629ca23455eb3d16ad28774660a3e84a7a45 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 26 Jul 2022 12:26:31 +0200 Subject: [PATCH 2863/3516] Fix small type issue [synology_dsm] (#75762) --- homeassistant/components/synology_dsm/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 89bbc4ae8c2..0314165eb41 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -383,7 +383,7 @@ class SynologyDSMOptionsFlowHandler(OptionsFlow): return self.async_show_form(step_id="init", data_schema=data_schema) -def _login_and_fetch_syno_info(api: SynologyDSM, otp_code: str) -> str: +def _login_and_fetch_syno_info(api: SynologyDSM, otp_code: str | None) -> str: """Login to the NAS and fetch basic data.""" # These do i/o api.login(otp_code) From 5cb4bbd9067b5538552f73ca0b963e4976fa6666 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 26 Jul 2022 04:17:28 -1000 Subject: [PATCH 2864/3516] Fix min and max mireds with HKC (#75744) --- .../components/homekit_controller/light.py | 12 + .../homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../fixtures/nanoleaf_strip_nl55.json | 354 ++++++++++++++++++ .../test_nanoleaf_strip_nl55.py | 55 +++ 6 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 tests/components/homekit_controller/fixtures/nanoleaf_strip_nl55.json create mode 100644 tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index 4073c1ac9fe..df691ac3f6f 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -71,6 +71,18 @@ class HomeKitLight(HomeKitEntity, LightEntity): self.service.value(CharacteristicsTypes.SATURATION), ) + @property + def min_mireds(self) -> int: + """Return minimum supported color temperature.""" + min_value = self.service[CharacteristicsTypes.COLOR_TEMPERATURE].minValue + return int(min_value) if min_value else super().min_mireds + + @property + def max_mireds(self) -> int: + """Return the maximum color temperature.""" + max_value = self.service[CharacteristicsTypes.COLOR_TEMPERATURE].maxValue + return int(max_value) if max_value else super().max_mireds + @property def color_temp(self) -> int: """Return the color temperature.""" diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 51d753f77fc..2de2a915d41 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.0"], + "requirements": ["aiohomekit==1.2.2"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 47afd92827c..bbf8e44cc80 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.0 +aiohomekit==1.2.2 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 32d8b2bf514..b383bce296f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.0 +aiohomekit==1.2.2 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/fixtures/nanoleaf_strip_nl55.json b/tests/components/homekit_controller/fixtures/nanoleaf_strip_nl55.json new file mode 100644 index 00000000000..142af6c03a2 --- /dev/null +++ b/tests/components/homekit_controller/fixtures/nanoleaf_strip_nl55.json @@ -0,0 +1,354 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 2, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pr"], + "format": "string", + "value": "Nanoleaf", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "NL55", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "Nanoleaf Strip 3B32", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pr"], + "format": "string", + "value": "AAAA011111111111", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": ["pr"], + "format": "string", + "value": "1.4.40", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000053-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "1.2.4", + "description": "Hardware Revision", + "maxLen": 64 + }, + { + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "iid": 9, + "perms": ["pr", "hd"], + "format": "string", + "value": "5.0;dfeceb3a", + "maxLen": 64 + }, + { + "type": "00000220-0000-1000-8000-0026BB765291", + "iid": 10, + "perms": ["pr", "hd"], + "format": "data", + "value": "cf0c2e5a4476e152" + } + ] + }, + { + "iid": 11, + "type": "00000055-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000004C-0000-1000-8000-0026BB765291", + "iid": 34, + "perms": [], + "format": "data", + "description": "Pair Setup" + }, + { + "type": "0000004E-0000-1000-8000-0026BB765291", + "iid": 35, + "perms": [], + "format": "data", + "description": "Pair Verify" + }, + { + "type": "0000004F-0000-1000-8000-0026BB765291", + "iid": 36, + "perms": [], + "format": "uint8", + "description": "Pairing Features" + }, + { + "type": "00000050-0000-1000-8000-0026BB765291", + "iid": 37, + "perms": ["pr", "pw"], + "format": "data", + "value": null, + "description": "Pairing Pairings" + } + ] + }, + { + "iid": 16, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "000000A5-0000-1000-8000-0026BB765291", + "iid": 17, + "perms": ["pr"], + "format": "data", + "value": "" + }, + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 18, + "perms": ["pr"], + "format": "string", + "value": "2.2.0", + "description": "Version", + "maxLen": 64 + } + ] + }, + { + "iid": 19, + "type": "00000043-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "000000A5-0000-1000-8000-0026BB765291", + "iid": 49, + "perms": ["pr"], + "format": "data", + "value": "" + }, + { + "type": "00000025-0000-1000-8000-0026BB765291", + "iid": 51, + "perms": ["pr", "pw", "ev"], + "format": "bool", + "value": true, + "description": "On" + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 50, + "perms": ["pr"], + "format": "string", + "value": "Nanoleaf Light Strip", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000008-0000-1000-8000-0026BB765291", + "iid": 52, + "perms": ["pr", "pw", "ev"], + "format": "int", + "value": 100, + "description": "Brightness", + "unit": "percentage", + "minValue": 0, + "maxValue": 100, + "minStep": 1 + }, + { + "type": "000000CE-0000-1000-8000-0026BB765291", + "iid": 55, + "perms": ["pr", "pw", "ev"], + "format": "uint32", + "value": 470, + "description": "Color Temperature", + "minValue": 153, + "maxValue": 470, + "minStep": 1 + }, + { + "type": "00000013-0000-1000-8000-0026BB765291", + "iid": 53, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 30.0, + "description": "Hue", + "unit": "arcdegrees", + "minValue": 0.0, + "maxValue": 360.0, + "minStep": 1.0 + }, + { + "type": "0000002F-0000-1000-8000-0026BB765291", + "iid": 54, + "perms": ["pr", "pw", "ev"], + "format": "float", + "value": 89.0, + "description": "Saturation", + "unit": "percentage", + "minValue": 0.0, + "maxValue": 100.0, + "minStep": 1.0 + }, + { + "type": "A28E1902-CFA1-4D37-A10F-0071CEEEEEBD", + "iid": 60, + "perms": ["pr", "pw", "hd"], + "format": "data", + "value": "" + }, + { + "type": "00000143-0000-1000-8000-0026BB765291", + "iid": 56, + "perms": ["pr", "pw"], + "format": "data", + "value": "" + }, + { + "type": "00000144-0000-1000-8000-0026BB765291", + "iid": 57, + "perms": ["pr"], + "format": "data", + "value": "010901013402040100000000000109010137020402000000" + }, + { + "type": "0000024B-0000-1000-8000-0026BB765291", + "iid": 58, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 2 + } + ] + }, + { + "iid": 31, + "type": "00000701-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "000000A5-0000-1000-8000-0026BB765291", + "iid": 113, + "perms": ["pr"], + "format": "data", + "value": null + }, + { + "type": "00000706-0000-1000-8000-0026BB765291", + "iid": 116, + "perms": ["pr"], + "format": "string", + "value": "", + "maxLen": 64 + }, + { + "type": "00000702-0000-1000-8000-0026BB765291", + "iid": 115, + "perms": ["pr"], + "format": "uint16", + "value": 31, + "description": "Thread Node Capabilities", + "minValue": 0, + "maxValue": 31 + }, + { + "type": "00000703-0000-1000-8000-0026BB765291", + "iid": 117, + "perms": ["pr", "ev"], + "format": "uint16", + "value": 127, + "description": "Thread Status", + "minValue": 0, + "maxValue": 127 + }, + { + "type": "0000022B-0000-1000-8000-0026BB765291", + "iid": 118, + "perms": ["pr"], + "format": "bool", + "value": false + }, + { + "type": "00000704-0000-1000-8000-0026BB765291", + "iid": 119, + "perms": ["pr", "pw"], + "format": "data", + "value": null + } + ] + }, + { + "iid": 38, + "type": "00000239-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "000000A5-0000-1000-8000-0026BB765291", + "iid": 2564, + "perms": ["pr"], + "format": "data", + "value": "" + }, + { + "type": "0000023A-0000-1000-8000-0026BB765291", + "iid": 2561, + "perms": ["pr"], + "format": "uint32", + "value": 0, + "minValue": 0, + "maxValue": 67108863 + }, + { + "type": "0000023C-0000-1000-8000-0026BB765291", + "iid": 2562, + "perms": ["pr"], + "format": "data", + "value": "" + }, + { + "type": "0000024A-0000-1000-8000-0026BB765291", + "iid": 2565, + "perms": ["pr", "ev"], + "format": "uint32", + "value": 1 + } + ] + }, + { + "iid": 43, + "type": "0E9CC677-A71F-8B83-B84D-568278790CB3", + "characteristics": [] + }, + { + "iid": 44, + "type": "6D2AE1C4-9AEA-11EA-BB37-0242AC130002", + "characteristics": [] + } + ] + } +] diff --git a/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py new file mode 100644 index 00000000000..7e6a9bb672b --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py @@ -0,0 +1,55 @@ +"""Make sure that Nanoleaf NL55 works with BLE.""" + +from homeassistant.helpers.entity import EntityCategory + +from tests.components.homekit_controller.common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + +LIGHT_ON = ("lightbulb", "on") + + +async def test_nanoleaf_nl55_setup(hass): + """Test that a Nanoleaf NL55 can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "nanoleaf_strip_nl55.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="Nanoleaf Strip 3B32", + model="NL55", + manufacturer="Nanoleaf", + sw_version="1.4.40", + hw_version="1.2.4", + serial_number="AAAA011111111111", + devices=[], + entities=[ + EntityTestInfo( + entity_id="light.nanoleaf_strip_3b32_nanoleaf_light_strip", + friendly_name="Nanoleaf Strip 3B32 Nanoleaf Light Strip", + unique_id="homekit-AAAA011111111111-19", + supported_features=0, + capabilities={ + "max_mireds": 470, + "min_mireds": 153, + "supported_color_modes": ["color_temp", "hs"], + }, + state="on", + ), + EntityTestInfo( + entity_id="button.nanoleaf_strip_3b32_identify", + friendly_name="Nanoleaf Strip 3B32 Identify", + unique_id="homekit-AAAA011111111111-aid:1-sid:1-cid:2", + entity_category=EntityCategory.DIAGNOSTIC, + state="unknown", + ), + ], + ), + ) From 9ad273a59f57161ee84ac90f489ae017076f851a Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 26 Jul 2022 22:27:16 +0800 Subject: [PATCH 2865/3516] Fix entity typo (#75767) --- homeassistant/components/radio_browser/__init__.py | 2 +- tests/components/mqtt/test_cover.py | 10 +++++----- tests/components/mqtt/test_init.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/radio_browser/__init__.py b/homeassistant/components/radio_browser/__init__.py index 89c2f220159..d93d7c48823 100644 --- a/homeassistant/components/radio_browser/__init__.py +++ b/homeassistant/components/radio_browser/__init__.py @@ -15,7 +15,7 @@ from .const import DOMAIN async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Radio Browser from a config entry. - This integration doesn't set up any enitites, as it provides a media source + This integration doesn't set up any entities, as it provides a media source only. """ session = async_get_clientsession(hass) diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 3f85d4e89b1..b1c162073ed 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -987,7 +987,7 @@ async def test_set_tilt_templated_and_attributes( "set_position_topic": "set-position-topic", "set_position_template": "{{position-1}}", "tilt_command_template": "{" - '"enitity_id": "{{ entity_id }}",' + '"entity_id": "{{ entity_id }}",' '"value": {{ value }},' '"tilt_position": {{ tilt_position }}' "}", @@ -1009,7 +1009,7 @@ async def test_set_tilt_templated_and_attributes( mqtt_mock.async_publish.assert_called_once_with( "tilt-command-topic", - '{"enitity_id": "cover.test","value": 45,"tilt_position": 45}', + '{"entity_id": "cover.test","value": 45,"tilt_position": 45}', 0, False, ) @@ -1023,7 +1023,7 @@ async def test_set_tilt_templated_and_attributes( ) mqtt_mock.async_publish.assert_called_once_with( "tilt-command-topic", - '{"enitity_id": "cover.test","value": 100,"tilt_position": 100}', + '{"entity_id": "cover.test","value": 100,"tilt_position": 100}', 0, False, ) @@ -1037,7 +1037,7 @@ async def test_set_tilt_templated_and_attributes( ) mqtt_mock.async_publish.assert_called_once_with( "tilt-command-topic", - '{"enitity_id": "cover.test","value": 0,"tilt_position": 0}', + '{"entity_id": "cover.test","value": 0,"tilt_position": 0}', 0, False, ) @@ -1051,7 +1051,7 @@ async def test_set_tilt_templated_and_attributes( ) mqtt_mock.async_publish.assert_called_once_with( "tilt-command-topic", - '{"enitity_id": "cover.test","value": 100,"tilt_position": 100}', + '{"entity_id": "cover.test","value": 100,"tilt_position": 100}', 0, False, ) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 1f263d40fc2..fe8f483adf2 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -281,7 +281,7 @@ async def test_command_template_value(hass): async def test_command_template_variables(hass, mqtt_mock_entry_with_yaml_config): - """Test the rendering of enitity_variables.""" + """Test the rendering of entity variables.""" topic = "test/select" fake_state = ha.State("select.test", "milk") From af7df260a0dad192bc4790176f1120cd034a8230 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 26 Jul 2022 16:28:22 +0200 Subject: [PATCH 2866/3516] Fix small type issues [core] (#75760) --- homeassistant/auth/mfa_modules/notify.py | 2 +- homeassistant/components/http/static.py | 2 +- homeassistant/config.py | 1 + homeassistant/helpers/event.py | 6 +++--- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 464ce495050..1de6c38aecf 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -320,6 +320,7 @@ class NotifySetupFlow(SetupFlow): errors: dict[str, str] = {} hass = self._auth_module.hass + assert self._secret and self._count if user_input: verified = await hass.async_add_executor_job( _verify_otp, self._secret, user_input["code"], self._count @@ -334,7 +335,6 @@ class NotifySetupFlow(SetupFlow): errors["base"] = "invalid_code" # generate code every time, no retry logic - assert self._secret and self._count code = await hass.async_add_executor_job( _generate_otp, self._secret, self._count ) diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index e5e84ca141d..c4dc97727a9 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -23,7 +23,7 @@ PATH_CACHE = LRU(512) def _get_file_path( - filename: str, directory: Path, follow_symlinks: bool + filename: str | Path, directory: Path, follow_symlinks: bool ) -> Path | None: filepath = directory.joinpath(filename).resolve() if not follow_symlinks: diff --git a/homeassistant/config.py b/homeassistant/config.py index 5c870918231..91f94bbbf40 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -286,6 +286,7 @@ async def async_create_default_config(hass: HomeAssistant) -> bool: Return if creation was successful. """ + assert hass.config.config_dir return await hass.async_add_executor_job( _write_default_config, hass.config.config_dir ) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 85cd684fca1..d18af953ec6 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -141,7 +141,7 @@ def threaded_listener_factory( def async_track_state_change( hass: HomeAssistant, entity_ids: str | Iterable[str], - action: Callable[[str, State, State], Awaitable[None] | None], + action: Callable[[str, State | None, State], Awaitable[None] | None], from_state: None | str | Iterable[str] = None, to_state: None | str | Iterable[str] = None, ) -> CALLBACK_TYPE: @@ -197,9 +197,9 @@ def async_track_state_change( """Handle specific state changes.""" hass.async_run_hass_job( job, - event.data.get("entity_id"), + event.data["entity_id"], event.data.get("old_state"), - event.data.get("new_state"), + event.data["new_state"], ) @callback From 516324ff540f904ea39c1517f8870c0f73d2ebff Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 26 Jul 2022 16:30:12 +0200 Subject: [PATCH 2867/3516] Fix small type issue [fritz] (#75761) --- homeassistant/components/fritz/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritz/config_flow.py b/homeassistant/components/fritz/config_flow.py index ddc09cb73a9..ea6461cef32 100644 --- a/homeassistant/components/fritz/config_flow.py +++ b/homeassistant/components/fritz/config_flow.py @@ -90,7 +90,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN): async def async_check_configured_entry(self) -> ConfigEntry | None: """Check if entry is configured.""" - + assert self._host current_host = await self.hass.async_add_executor_job( socket.gethostbyname, self._host ) From 7d895c79e821b450748f73a96990dad504dfc399 Mon Sep 17 00:00:00 2001 From: Jevgeni Kiski Date: Tue, 26 Jul 2022 17:58:22 +0300 Subject: [PATCH 2868/3516] Bump vallox-websocket-api to 2.12.0 (#75734) --- homeassistant/components/vallox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vallox/manifest.json b/homeassistant/components/vallox/manifest.json index 71b0750e2f2..5c862562fc1 100644 --- a/homeassistant/components/vallox/manifest.json +++ b/homeassistant/components/vallox/manifest.json @@ -2,7 +2,7 @@ "domain": "vallox", "name": "Vallox", "documentation": "https://www.home-assistant.io/integrations/vallox", - "requirements": ["vallox-websocket-api==2.11.0"], + "requirements": ["vallox-websocket-api==2.12.0"], "codeowners": ["@andre-richter", "@slovdahl", "@viiru-"], "config_flow": true, "iot_class": "local_polling", diff --git a/requirements_all.txt b/requirements_all.txt index bbf8e44cc80..c57f5c350af 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2398,7 +2398,7 @@ uscisstatus==0.1.1 uvcclient==0.11.0 # homeassistant.components.vallox -vallox-websocket-api==2.11.0 +vallox-websocket-api==2.12.0 # homeassistant.components.rdw vehicle==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b383bce296f..b0773fc089a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1607,7 +1607,7 @@ url-normalize==1.4.3 uvcclient==0.11.0 # homeassistant.components.vallox -vallox-websocket-api==2.11.0 +vallox-websocket-api==2.12.0 # homeassistant.components.rdw vehicle==0.4.0 From e6802f4f7e616f1f7605b388ac8d5167b87badfa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 26 Jul 2022 05:14:02 -1000 Subject: [PATCH 2869/3516] Add support for switchbot contact/door sensor (#75730) --- .../components/switchbot/__init__.py | 2 ++ .../components/switchbot/binary_sensor.py | 27 ++++++++++++++++++- homeassistant/components/switchbot/const.py | 2 ++ .../components/switchbot/manifest.json | 2 +- homeassistant/components/switchbot/sensor.py | 5 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 38 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 46d6755553a..42ca0856b02 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -19,6 +19,7 @@ from homeassistant.helpers import device_registry as dr from .const import ( ATTR_BOT, + ATTR_CONTACT, ATTR_CURTAIN, ATTR_HYGROMETER, CONF_RETRY_COUNT, @@ -31,6 +32,7 @@ PLATFORMS_BY_TYPE = { ATTR_BOT: [Platform.SWITCH, Platform.SENSOR], ATTR_CURTAIN: [Platform.COVER, Platform.BINARY_SENSOR, Platform.SENSOR], ATTR_HYGROMETER: [Platform.SENSOR], + ATTR_CONTACT: [Platform.BINARY_SENSOR, Platform.SENSOR], } CLASS_BY_DEVICE = { ATTR_CURTAIN: switchbot.SwitchbotCurtain, diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index d644f603697..4da4ed531b0 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -20,8 +21,30 @@ PARALLEL_UPDATES = 1 BINARY_SENSOR_TYPES: dict[str, BinarySensorEntityDescription] = { "calibration": BinarySensorEntityDescription( key="calibration", + name="Calibration", entity_category=EntityCategory.DIAGNOSTIC, ), + "motion_detected": BinarySensorEntityDescription( + key="pir_state", + name="Motion detected", + device_class=BinarySensorDeviceClass.MOTION, + ), + "contact_open": BinarySensorEntityDescription( + key="contact_open", + name="Door open", + device_class=BinarySensorDeviceClass.DOOR, + ), + "contact_timeout": BinarySensorEntityDescription( + key="contact_timeout", + name="Door timeout", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + ), + "is_light": BinarySensorEntityDescription( + key="is_light", + name="Light", + device_class=BinarySensorDeviceClass.LIGHT, + ), } @@ -50,6 +73,8 @@ async def async_setup_entry( class SwitchBotBinarySensor(SwitchbotEntity, BinarySensorEntity): """Representation of a Switchbot binary sensor.""" + _attr_has_entity_name = True + def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, @@ -62,8 +87,8 @@ class SwitchBotBinarySensor(SwitchbotEntity, BinarySensorEntity): super().__init__(coordinator, unique_id, mac, name=switchbot_name) self._sensor = binary_sensor self._attr_unique_id = f"{unique_id}-{binary_sensor}" - self._attr_name = f"{switchbot_name} {binary_sensor.title()}" self.entity_description = BINARY_SENSOR_TYPES[binary_sensor] + self._attr_name = self.entity_description.name @property def is_on(self) -> bool: diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index e9602a19048..dc5abc139e6 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -6,11 +6,13 @@ MANUFACTURER = "switchbot" ATTR_BOT = "bot" ATTR_CURTAIN = "curtain" ATTR_HYGROMETER = "hygrometer" +ATTR_CONTACT = "contact" DEFAULT_NAME = "Switchbot" SUPPORTED_MODEL_TYPES = { "WoHand": ATTR_BOT, "WoCurtain": ATTR_CURTAIN, "WoSensorTH": ATTR_HYGROMETER, + "WoContact": ATTR_CONTACT, } # Config Defaults diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 3bccbb4f674..d6acb69431e 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.15.1"], + "requirements": ["PySwitchbot==0.15.2"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": ["@bdraco", "@danielhiversen", "@RenierM26", "@murtas"], diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index 25863a57df5..f796ea05e7b 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -5,6 +5,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -36,21 +37,25 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { key="battery", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), "lightLevel": SensorEntityDescription( key="lightLevel", native_unit_of_measurement="Level", + state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.ILLUMINANCE, ), "humidity": SensorEntityDescription( key="humidity", native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.HUMIDITY, ), "temperature": SensorEntityDescription( key="temperature", native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.TEMPERATURE, ), } diff --git a/requirements_all.txt b/requirements_all.txt index c57f5c350af..2fa86ae9053 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.15.1 +PySwitchbot==0.15.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b0773fc089a..bd79723fc79 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.15.1 +PySwitchbot==0.15.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 3ee0ca8550beca5031c028cc4c78ab293d69fe22 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Jul 2022 17:35:47 +0200 Subject: [PATCH 2870/3516] Raise repair issue for Mi Flora (#75752) --- .../components/miflora/manifest.json | 6 +- homeassistant/components/miflora/sensor.py | 238 +----------------- homeassistant/components/miflora/strings.json | 8 + .../components/miflora/translations/en.json | 8 + requirements_all.txt | 4 - 5 files changed, 32 insertions(+), 232 deletions(-) create mode 100644 homeassistant/components/miflora/strings.json create mode 100644 homeassistant/components/miflora/translations/en.json diff --git a/homeassistant/components/miflora/manifest.json b/homeassistant/components/miflora/manifest.json index eea4b2b82fe..faae8fb140e 100644 --- a/homeassistant/components/miflora/manifest.json +++ b/homeassistant/components/miflora/manifest.json @@ -2,8 +2,8 @@ "domain": "miflora", "name": "Mi Flora", "documentation": "https://www.home-assistant.io/integrations/miflora", - "requirements": ["bluepy==1.3.0", "miflora==0.7.2"], + "requirements": [], + "dependencies": ["repairs"], "codeowners": ["@danielhiversen", "@basnijholt"], - "iot_class": "local_polling", - "loggers": ["btlewrap", "miflora"] + "iot_class": "local_polling" } diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index 2dd819e45c5..76808e9706c 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -1,113 +1,13 @@ """Support for Xiaomi Mi Flora BLE plant sensor.""" from __future__ import annotations -from datetime import timedelta -import logging -from typing import Any - -import btlewrap -from btlewrap import BluetoothBackendException -from miflora import miflora_poller -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import ( - CONDUCTIVITY, - CONF_FORCE_UPDATE, - CONF_MAC, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - CONF_SCAN_INTERVAL, - EVENT_HOMEASSISTANT_START, - LIGHT_LUX, - PERCENTAGE, - TEMP_CELSIUS, -) -from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv +from homeassistant.components.repairs import IssueSeverity, async_create_issue +from homeassistant.components.sensor import PLATFORM_SCHEMA_BASE +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -import homeassistant.util.dt as dt_util -try: - import bluepy.btle # noqa: F401 pylint: disable=unused-import - - BACKEND = btlewrap.BluepyBackend -except ImportError: - BACKEND = btlewrap.GatttoolBackend - -_LOGGER = logging.getLogger(__name__) - -CONF_ADAPTER = "adapter" -CONF_MEDIAN = "median" -CONF_GO_UNAVAILABLE_TIMEOUT = "go_unavailable_timeout" - -DEFAULT_ADAPTER = "hci0" -DEFAULT_FORCE_UPDATE = False -DEFAULT_MEDIAN = 3 -DEFAULT_NAME = "Mi Flora" -DEFAULT_GO_UNAVAILABLE_TIMEOUT = timedelta(seconds=7200) - -SCAN_INTERVAL = timedelta(seconds=1200) - -ATTR_LAST_SUCCESSFUL_UPDATE = "last_successful_update" - -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="temperature", - name="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - ), - SensorEntityDescription( - key="light", - name="Light intensity", - native_unit_of_measurement=LIGHT_LUX, - device_class=SensorDeviceClass.ILLUMINANCE, - ), - SensorEntityDescription( - key="moisture", - name="Moisture", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:water-percent", - ), - SensorEntityDescription( - key="conductivity", - name="Conductivity", - native_unit_of_measurement=CONDUCTIVITY, - icon="mdi:lightning-bolt-circle", - ), - SensorEntityDescription( - key="battery", - name="Battery", - native_unit_of_measurement=PERCENTAGE, - device_class=SensorDeviceClass.BATTERY, - ), -) - -SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_MAC): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MEDIAN, default=DEFAULT_MEDIAN): cv.positive_int, - vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, - vol.Optional(CONF_ADAPTER, default=DEFAULT_ADAPTER): cv.string, - vol.Optional( - CONF_GO_UNAVAILABLE_TIMEOUT, default=DEFAULT_GO_UNAVAILABLE_TIMEOUT - ): cv.time_period, - } -) +PLATFORM_SCHEMA = PLATFORM_SCHEMA_BASE async def async_setup_platform( @@ -117,125 +17,13 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the MiFlora sensor.""" - backend = BACKEND - _LOGGER.debug("Miflora is using %s backend", backend.__name__) - - cache = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL).total_seconds() - poller = miflora_poller.MiFloraPoller( - config[CONF_MAC], - cache_timeout=cache, - adapter=config[CONF_ADAPTER], - backend=backend, + async_create_issue( + hass, + "miflora", + "replaced", + breaks_in_ha_version="2022.8.0", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="replaced", + learn_more_url="https://www.home-assistant.io/integrations/xiaomi_ble/", ) - force_update = config[CONF_FORCE_UPDATE] - median = config[CONF_MEDIAN] - - go_unavailable_timeout = config[CONF_GO_UNAVAILABLE_TIMEOUT] - - prefix = config[CONF_NAME] - monitored_conditions = config[CONF_MONITORED_CONDITIONS] - entities = [ - MiFloraSensor( - description, - poller, - prefix, - force_update, - median, - go_unavailable_timeout, - ) - for description in SENSOR_TYPES - if description.key in monitored_conditions - ] - - async_add_entities(entities) - - -class MiFloraSensor(SensorEntity): - """Implementing the MiFlora sensor.""" - - _attr_state_class = SensorStateClass.MEASUREMENT - - def __init__( - self, - description: SensorEntityDescription, - poller, - prefix, - force_update, - median, - go_unavailable_timeout, - ): - """Initialize the sensor.""" - self.entity_description = description - self.poller = poller - self.data: list[Any] = [] - if prefix: - self._attr_name = f"{prefix} {description.name}" - self._attr_force_update = force_update - self.go_unavailable_timeout = go_unavailable_timeout - self.last_successful_update = dt_util.utc_from_timestamp(0) - # Median is used to filter out outliers. median of 3 will filter - # single outliers, while median of 5 will filter double outliers - # Use median_count = 1 if no filtering is required. - self.median_count = median - - async def async_added_to_hass(self): - """Set initial state.""" - - @callback - def on_startup(_): - self.async_schedule_update_ha_state(True) - - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, on_startup) - - @property - def available(self): - """Return True if did update since 2h.""" - return self.last_successful_update > ( - dt_util.utcnow() - self.go_unavailable_timeout - ) - - @property - def extra_state_attributes(self): - """Return the state attributes of the device.""" - return {ATTR_LAST_SUCCESSFUL_UPDATE: self.last_successful_update} - - def update(self): - """ - Update current conditions. - - This uses a rolling median over 3 values to filter out outliers. - """ - try: - _LOGGER.debug("Polling data for %s", self.name) - data = self.poller.parameter_value(self.entity_description.key) - except (OSError, BluetoothBackendException) as err: - _LOGGER.info("Polling error %s: %s", type(err).__name__, err) - return - - if data is not None: - _LOGGER.debug("%s = %s", self.name, data) - self.data.append(data) - self.last_successful_update = dt_util.utcnow() - else: - _LOGGER.info("Did not receive any data from Mi Flora sensor %s", self.name) - # Remove old data from median list or set sensor value to None - # if no data is available anymore - if self.data: - self.data = self.data[1:] - else: - self._attr_native_value = None - return - - _LOGGER.debug("Data collected: %s", self.data) - if len(self.data) > self.median_count: - self.data = self.data[1:] - - if len(self.data) == self.median_count: - median = sorted(self.data)[int((self.median_count - 1) / 2)] - _LOGGER.debug("Median is: %s", median) - self._attr_native_value = median - elif self._attr_native_value is None: - _LOGGER.debug("Set initial state") - self._attr_native_value = self.data[0] - else: - _LOGGER.debug("Not yet enough data for median calculation") diff --git a/homeassistant/components/miflora/strings.json b/homeassistant/components/miflora/strings.json new file mode 100644 index 00000000000..03427e88af9 --- /dev/null +++ b/homeassistant/components/miflora/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "title": "The Mi Flora integration has been replaced", + "description": "The Mi Flora integration stopped working in Home Assistant 2022.7 and replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Mi Flora device using the new integration manually.\n\nYour existing Mi Flora YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/miflora/translations/en.json b/homeassistant/components/miflora/translations/en.json new file mode 100644 index 00000000000..52a8a71594c --- /dev/null +++ b/homeassistant/components/miflora/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "The Mi Flora integration stopped working in Home Assistant 2022.7 and replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Mi Flora device using the new integration manually.\n\nYour existing Mi Flora YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Mi Flora integration has been replaced" + } + } +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 2fa86ae9053..972aae86c85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -420,7 +420,6 @@ blinkstick==1.2.0 blockchain==1.4.4 # homeassistant.components.decora -# homeassistant.components.miflora # homeassistant.components.zengge # bluepy==1.3.0 @@ -1037,9 +1036,6 @@ mficlient==0.3.0 # homeassistant.components.xiaomi_miio micloud==0.5 -# homeassistant.components.miflora -miflora==0.7.2 - # homeassistant.components.mill mill-local==0.1.1 From c63a838b472b9c285e4266689ff89bd492cc3518 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Jul 2022 17:42:22 +0200 Subject: [PATCH 2871/3516] Raise repair issue for Xiaomi Mijia BLE Temperature and Humidity Sensor (#75754) --- .../components/mitemp_bt/manifest.json | 6 +- homeassistant/components/mitemp_bt/sensor.py | 187 ++---------------- .../components/mitemp_bt/strings.json | 8 + .../components/mitemp_bt/translations/en.json | 8 + requirements_all.txt | 3 - 5 files changed, 33 insertions(+), 179 deletions(-) create mode 100644 homeassistant/components/mitemp_bt/strings.json create mode 100644 homeassistant/components/mitemp_bt/translations/en.json diff --git a/homeassistant/components/mitemp_bt/manifest.json b/homeassistant/components/mitemp_bt/manifest.json index 07121b3695b..3af60301802 100644 --- a/homeassistant/components/mitemp_bt/manifest.json +++ b/homeassistant/components/mitemp_bt/manifest.json @@ -2,8 +2,8 @@ "domain": "mitemp_bt", "name": "Xiaomi Mijia BLE Temperature and Humidity Sensor", "documentation": "https://www.home-assistant.io/integrations/mitemp_bt", - "requirements": ["mitemp_bt==0.0.5"], + "requirements": [], + "dependencies": ["repairs"], "codeowners": [], - "iot_class": "local_polling", - "loggers": ["btlewrap", "mitemp_bt"] + "iot_class": "local_polling" } diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py index e7f6237fdb1..74d0db7648c 100644 --- a/homeassistant/components/mitemp_bt/sensor.py +++ b/homeassistant/components/mitemp_bt/sensor.py @@ -1,188 +1,29 @@ """Support for Xiaomi Mi Temp BLE environmental sensor.""" from __future__ import annotations -import logging -from typing import Any - -import btlewrap -from btlewrap.base import BluetoothBackendException -from mitemp_bt import mitemp_bt_poller -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import ( - CONF_FORCE_UPDATE, - CONF_MAC, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - CONF_TIMEOUT, - PERCENTAGE, - TEMP_CELSIUS, -) +from homeassistant.components.repairs import IssueSeverity, async_create_issue +from homeassistant.components.sensor import PLATFORM_SCHEMA_BASE 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 -try: - import bluepy.btle # noqa: F401 pylint: disable=unused-import - - BACKEND = btlewrap.BluepyBackend -except ImportError: - BACKEND = btlewrap.GatttoolBackend - -_LOGGER = logging.getLogger(__name__) - -CONF_ADAPTER = "adapter" -CONF_CACHE = "cache_value" -CONF_MEDIAN = "median" -CONF_RETRIES = "retries" - -DEFAULT_ADAPTER = "hci0" -DEFAULT_UPDATE_INTERVAL = 300 -DEFAULT_FORCE_UPDATE = False -DEFAULT_MEDIAN = 3 -DEFAULT_NAME = "MiTemp BT" -DEFAULT_RETRIES = 2 -DEFAULT_TIMEOUT = 10 +PLATFORM_SCHEMA = PLATFORM_SCHEMA_BASE -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key="temperature", - name="Temperature", - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=TEMP_CELSIUS, - ), - SensorEntityDescription( - key="humidity", - name="Humidity", - device_class=SensorDeviceClass.HUMIDITY, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key="battery", - name="Battery", - device_class=SensorDeviceClass.BATTERY, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=PERCENTAGE, - ), -) - -SENSOR_KEYS = [desc.key for desc in SENSOR_TYPES] - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_MAC): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MEDIAN, default=DEFAULT_MEDIAN): cv.positive_int, - vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - vol.Optional(CONF_RETRIES, default=DEFAULT_RETRIES): cv.positive_int, - vol.Optional(CONF_CACHE, default=DEFAULT_UPDATE_INTERVAL): cv.positive_int, - vol.Optional(CONF_ADAPTER, default=DEFAULT_ADAPTER): cv.string, - } -) - - -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the MiTempBt sensor.""" - backend = BACKEND - _LOGGER.debug("MiTempBt is using %s backend", backend.__name__) - - cache = config[CONF_CACHE] - poller = mitemp_bt_poller.MiTempBtPoller( - config[CONF_MAC], - cache_timeout=cache, - adapter=config[CONF_ADAPTER], - backend=backend, + async_create_issue( + hass, + "mitemp_bt", + "replaced", + breaks_in_ha_version="2022.8.0", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="replaced", + learn_more_url="https://www.home-assistant.io/integrations/xiaomi_ble/", ) - prefix = config[CONF_NAME] - force_update = config[CONF_FORCE_UPDATE] - median = config[CONF_MEDIAN] - poller.ble_timeout = config[CONF_TIMEOUT] - poller.retries = config[CONF_RETRIES] - - monitored_conditions = config[CONF_MONITORED_CONDITIONS] - entities = [ - MiTempBtSensor(poller, prefix, force_update, median, description) - for description in SENSOR_TYPES - if description.key in monitored_conditions - ] - - add_entities(entities) - - -class MiTempBtSensor(SensorEntity): - """Implementing the MiTempBt sensor.""" - - def __init__( - self, poller, prefix, force_update, median, description: SensorEntityDescription - ): - """Initialize the sensor.""" - self.entity_description = description - self.poller = poller - self.data: list[Any] = [] - self._attr_name = f"{prefix} {description.name}" - self._attr_force_update = force_update - # Median is used to filter out outliers. median of 3 will filter - # single outliers, while median of 5 will filter double outliers - # Use median_count = 1 if no filtering is required. - self.median_count = median - - def update(self): - """ - Update current conditions. - - This uses a rolling median over 3 values to filter out outliers. - """ - try: - _LOGGER.debug("Polling data for %s", self.name) - data = self.poller.parameter_value(self.entity_description.key) - except OSError as ioerr: - _LOGGER.warning("Polling error %s", ioerr) - return - except BluetoothBackendException as bterror: - _LOGGER.warning("Polling error %s", bterror) - return - - if data is not None: - _LOGGER.debug("%s = %s", self.name, data) - self.data.append(data) - else: - _LOGGER.warning( - "Did not receive any data from Mi Temp sensor %s", self.name - ) - # Remove old data from median list or set sensor value to None - # if no data is available anymore - if self.data: - self.data = self.data[1:] - else: - self._attr_native_value = None - return - - if len(self.data) > self.median_count: - self.data = self.data[1:] - - if len(self.data) == self.median_count: - median = sorted(self.data)[int((self.median_count - 1) / 2)] - _LOGGER.debug("Median is: %s", median) - self._attr_native_value = median - else: - _LOGGER.debug("Not yet enough data for median calculation") diff --git a/homeassistant/components/mitemp_bt/strings.json b/homeassistant/components/mitemp_bt/strings.json new file mode 100644 index 00000000000..78f047896e1 --- /dev/null +++ b/homeassistant/components/mitemp_bt/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced", + "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/mitemp_bt/translations/en.json b/homeassistant/components/mitemp_bt/translations/en.json new file mode 100644 index 00000000000..ff131abe367 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced" + } + } +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 972aae86c85..61f62f3778c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1045,9 +1045,6 @@ millheater==0.9.0 # homeassistant.components.minio minio==5.0.10 -# homeassistant.components.mitemp_bt -mitemp_bt==0.0.5 - # homeassistant.components.moat moat-ble==0.1.1 From fad7a6cb08ef402456dcdd71d48e7d6f99f96ba2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 26 Jul 2022 07:09:46 -1000 Subject: [PATCH 2872/3516] Bump sensorpush-ble to 1.5.1 (#75771) --- homeassistant/components/sensorpush/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensorpush/manifest.json b/homeassistant/components/sensorpush/manifest.json index 08212f88132..a5d900aaf3b 100644 --- a/homeassistant/components/sensorpush/manifest.json +++ b/homeassistant/components/sensorpush/manifest.json @@ -8,7 +8,7 @@ "local_name": "SensorPush*" } ], - "requirements": ["sensorpush-ble==1.4.2"], + "requirements": ["sensorpush-ble==1.5.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 61f62f3778c..8f89fa07f01 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2160,7 +2160,7 @@ sendgrid==6.8.2 sense_energy==0.10.4 # homeassistant.components.sensorpush -sensorpush-ble==1.4.2 +sensorpush-ble==1.5.1 # homeassistant.components.sentry sentry-sdk==1.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd79723fc79..d87bce47ab5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1451,7 +1451,7 @@ securetar==2022.2.0 sense_energy==0.10.4 # homeassistant.components.sensorpush -sensorpush-ble==1.4.2 +sensorpush-ble==1.5.1 # homeassistant.components.sentry sentry-sdk==1.8.0 From cb17a01e48132f7adc2452ce71758512d99a7e1f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Jul 2022 20:01:15 +0200 Subject: [PATCH 2873/3516] Raise YAML removal issue for nVent RAYCHEM SENZ (#75757) --- homeassistant/components/senz/__init__.py | 18 ++++++++++++++++++ homeassistant/components/senz/manifest.json | 2 +- homeassistant/components/senz/strings.json | 6 ++++++ .../components/senz/translations/en.json | 6 ++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/senz/__init__.py b/homeassistant/components/senz/__init__.py index 012b40f5def..9155c8ca036 100644 --- a/homeassistant/components/senz/__init__.py +++ b/homeassistant/components/senz/__init__.py @@ -7,6 +7,7 @@ import logging from aiosenz import SENZAPI, Thermostat from httpx import RequestError +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -16,6 +17,7 @@ from homeassistant.helpers import ( config_validation as cv, httpx_client, ) +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .api import SENZConfigEntryAuth @@ -32,6 +34,22 @@ PLATFORMS = [Platform.CLIMATE] SENZDataUpdateCoordinator = DataUpdateCoordinator[dict[str, Thermostat]] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the SENZ integration.""" + if DOMAIN in config: + async_create_issue( + hass, + DOMAIN, + "removed_yaml", + breaks_in_ha_version="2022.8.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_yaml", + ) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SENZ from a config entry.""" implementation = ( diff --git a/homeassistant/components/senz/manifest.json b/homeassistant/components/senz/manifest.json index 937a20d8482..36687e46d4a 100644 --- a/homeassistant/components/senz/manifest.json +++ b/homeassistant/components/senz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/senz", "requirements": ["aiosenz==1.0.0"], - "dependencies": ["application_credentials"], + "dependencies": ["application_credentials", "repairs"], "codeowners": ["@milanmeu"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/senz/strings.json b/homeassistant/components/senz/strings.json index 316f7234f9b..74ca9f5e3bf 100644 --- a/homeassistant/components/senz/strings.json +++ b/homeassistant/components/senz/strings.json @@ -16,5 +16,11 @@ "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } + }, + "issues": { + "removed_yaml": { + "title": "The nVent RAYCHEM SENZ YAML configuration has been removed", + "description": "Configuring nVent RAYCHEM SENZ using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/senz/translations/en.json b/homeassistant/components/senz/translations/en.json index bdf574691c5..fc1cfd561d4 100644 --- a/homeassistant/components/senz/translations/en.json +++ b/homeassistant/components/senz/translations/en.json @@ -16,5 +16,11 @@ "title": "Pick Authentication Method" } } + }, + "issues": { + "removed_yaml": { + "description": "Configuring nVent RAYCHEM SENZ using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The nVent RAYCHEM SENZ YAML configuration has been removed" + } } } \ No newline at end of file From 157f7292d7df5f2fad60f445b9370551b724585c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Jul 2022 20:02:50 +0200 Subject: [PATCH 2874/3516] Raise YAML removal issue for Lyric (#75756) --- homeassistant/components/lyric/__init__.py | 18 ++++++++++++++++++ homeassistant/components/lyric/manifest.json | 2 +- homeassistant/components/lyric/strings.json | 6 ++++++ .../components/lyric/translations/en.json | 6 ++++++ 4 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lyric/__init__.py b/homeassistant/components/lyric/__init__.py index 4e4eec85899..d1048ac8e58 100644 --- a/homeassistant/components/lyric/__init__.py +++ b/homeassistant/components/lyric/__init__.py @@ -12,6 +12,7 @@ from aiolyric.objects.device import LyricDevice from aiolyric.objects.location import LyricLocation import async_timeout +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -23,6 +24,7 @@ from homeassistant.helpers import ( device_registry as dr, ) from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -43,6 +45,22 @@ _LOGGER = logging.getLogger(__name__) PLATFORMS = [Platform.CLIMATE, Platform.SENSOR] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Honeywell Lyric integration.""" + if DOMAIN in config: + async_create_issue( + hass, + DOMAIN, + "removed_yaml", + breaks_in_ha_version="2022.8.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_yaml", + ) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Honeywell Lyric from a config entry.""" implementation = ( diff --git a/homeassistant/components/lyric/manifest.json b/homeassistant/components/lyric/manifest.json index c0d9168f46f..91b152cdf21 100644 --- a/homeassistant/components/lyric/manifest.json +++ b/homeassistant/components/lyric/manifest.json @@ -3,7 +3,7 @@ "name": "Honeywell Lyric", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lyric", - "dependencies": ["application_credentials"], + "dependencies": ["application_credentials", "repairs"], "requirements": ["aiolyric==1.0.8"], "codeowners": ["@timmo001"], "quality_scale": "silver", diff --git a/homeassistant/components/lyric/strings.json b/homeassistant/components/lyric/strings.json index 3c9cd6043df..dd9a89f294d 100644 --- a/homeassistant/components/lyric/strings.json +++ b/homeassistant/components/lyric/strings.json @@ -17,5 +17,11 @@ "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } + }, + "issues": { + "removed_yaml": { + "title": "The Honeywell Lyric YAML configuration has been removed", + "description": "Configuring Honeywell Lyric using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/lyric/translations/en.json b/homeassistant/components/lyric/translations/en.json index 17586f16109..3d1df448ba2 100644 --- a/homeassistant/components/lyric/translations/en.json +++ b/homeassistant/components/lyric/translations/en.json @@ -17,5 +17,11 @@ "title": "Reauthenticate Integration" } } + }, + "issues": { + "removed_yaml": { + "description": "Configuring Honeywell Lyric using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Honeywell Lyric YAML configuration has been removed" + } } } \ No newline at end of file From 1e85ddabfd0e59a0106ea8d50f1e6ee5d3f84bb3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 26 Jul 2022 09:29:23 -1000 Subject: [PATCH 2875/3516] Fix startup race in BLE integrations (#75780) --- .../bluetooth/passive_update_coordinator.py | 11 ----------- .../bluetooth/passive_update_processor.py | 11 ----------- .../components/bluetooth/update_coordinator.py | 11 +++++++++++ homeassistant/components/govee_ble/__init__.py | 5 ++++- homeassistant/components/govee_ble/sensor.py | 2 +- homeassistant/components/inkbird/__init__.py | 5 ++++- homeassistant/components/inkbird/sensor.py | 2 +- homeassistant/components/moat/__init__.py | 5 ++++- homeassistant/components/moat/sensor.py | 2 +- .../components/sensorpush/__init__.py | 5 ++++- homeassistant/components/sensorpush/sensor.py | 2 +- .../components/xiaomi_ble/__init__.py | 5 ++++- homeassistant/components/xiaomi_ble/sensor.py | 2 +- .../bluetooth/test_passive_update_processor.py | 18 ++++++++++++++++++ 14 files changed, 54 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 4a22f03449e..97e7ddc49ee 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -42,17 +42,6 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): super()._async_handle_unavailable(address) self.async_update_listeners() - @callback - def async_start(self) -> CALLBACK_TYPE: - """Start the data updater.""" - self._async_start() - - @callback - def _async_cancel() -> None: - self._async_stop() - - return _async_cancel - @callback def async_add_listener( self, update_callback: CALLBACK_TYPE, context: Any = None diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 2e4118000bd..43467701879 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -78,21 +78,10 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): def remove_processor() -> None: """Remove a processor.""" self._processors.remove(processor) - self._async_handle_processors_changed() self._processors.append(processor) - self._async_handle_processors_changed() return remove_processor - @callback - def _async_handle_processors_changed(self) -> None: - """Handle processors changed.""" - running = bool(self._cancel_bluetooth_advertisements) - if running and not self._processors: - self._async_stop() - elif not running and self._processors: - self._async_start() - @callback def _async_handle_unavailable(self, address: str) -> None: """Handle the device going unavailable.""" diff --git a/homeassistant/components/bluetooth/update_coordinator.py b/homeassistant/components/bluetooth/update_coordinator.py index d45514ab9ab..b1cb2de1453 100644 --- a/homeassistant/components/bluetooth/update_coordinator.py +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -38,6 +38,17 @@ class BasePassiveBluetoothCoordinator: self._present = False self.last_seen = 0.0 + @callback + def async_start(self) -> CALLBACK_TYPE: + """Start the data updater.""" + self._async_start() + + @callback + def _async_cancel() -> None: + self._async_stop() + + return _async_cancel + @property def available(self) -> bool: """Return if the device is available.""" diff --git a/homeassistant/components/govee_ble/__init__.py b/homeassistant/components/govee_ble/__init__.py index 3cb9e4659d6..3099d401e9b 100644 --- a/homeassistant/components/govee_ble/__init__.py +++ b/homeassistant/components/govee_ble/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Govee BLE device from a config entry.""" address = entry.unique_id assert address is not None - hass.data.setdefault(DOMAIN, {})[ + coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( hass, @@ -29,6 +29,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address=address, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe return True diff --git a/homeassistant/components/govee_ble/sensor.py b/homeassistant/components/govee_ble/sensor.py index b8af7df8fb9..d0b9447d9e3 100644 --- a/homeassistant/components/govee_ble/sensor.py +++ b/homeassistant/components/govee_ble/sensor.py @@ -135,12 +135,12 @@ async def async_setup_entry( data.update(service_info) ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) entry.async_on_unload( processor.async_add_entities_listener( GoveeBluetoothSensorEntity, async_add_entities ) ) + entry.async_on_unload(coordinator.async_register_processor(processor)) class GoveeBluetoothSensorEntity( diff --git a/homeassistant/components/inkbird/__init__.py b/homeassistant/components/inkbird/__init__.py index 574f2a23355..5553b1c6ded 100644 --- a/homeassistant/components/inkbird/__init__.py +++ b/homeassistant/components/inkbird/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up INKBIRD BLE device from a config entry.""" address = entry.unique_id assert address is not None - hass.data.setdefault(DOMAIN, {})[ + coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( hass, @@ -29,6 +29,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address=address, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe return True diff --git a/homeassistant/components/inkbird/sensor.py b/homeassistant/components/inkbird/sensor.py index 6b472edefe9..0648ca80383 100644 --- a/homeassistant/components/inkbird/sensor.py +++ b/homeassistant/components/inkbird/sensor.py @@ -135,12 +135,12 @@ async def async_setup_entry( data.update(service_info) ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) entry.async_on_unload( processor.async_add_entities_listener( INKBIRDBluetoothSensorEntity, async_add_entities ) ) + entry.async_on_unload(coordinator.async_register_processor(processor)) class INKBIRDBluetoothSensorEntity( diff --git a/homeassistant/components/moat/__init__.py b/homeassistant/components/moat/__init__.py index bd32ef64a0f..259b6b66709 100644 --- a/homeassistant/components/moat/__init__.py +++ b/homeassistant/components/moat/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Moat BLE device from a config entry.""" address = entry.unique_id assert address is not None - hass.data.setdefault(DOMAIN, {})[ + coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( hass, @@ -29,6 +29,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address=address, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe return True diff --git a/homeassistant/components/moat/sensor.py b/homeassistant/components/moat/sensor.py index f29111a3406..295e2877aed 100644 --- a/homeassistant/components/moat/sensor.py +++ b/homeassistant/components/moat/sensor.py @@ -142,12 +142,12 @@ async def async_setup_entry( data.update(service_info) ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) entry.async_on_unload( processor.async_add_entities_listener( MoatBluetoothSensorEntity, async_add_entities ) ) + entry.async_on_unload(coordinator.async_register_processor(processor)) class MoatBluetoothSensorEntity( diff --git a/homeassistant/components/sensorpush/__init__.py b/homeassistant/components/sensorpush/__init__.py index 0f0d4d4fe8f..0a9efcbc752 100644 --- a/homeassistant/components/sensorpush/__init__.py +++ b/homeassistant/components/sensorpush/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SensorPush BLE device from a config entry.""" address = entry.unique_id assert address is not None - hass.data.setdefault(DOMAIN, {})[ + coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( hass, @@ -29,6 +29,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address=address, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe return True diff --git a/homeassistant/components/sensorpush/sensor.py b/homeassistant/components/sensorpush/sensor.py index 3921cbe43dd..9bfa59e3876 100644 --- a/homeassistant/components/sensorpush/sensor.py +++ b/homeassistant/components/sensorpush/sensor.py @@ -136,12 +136,12 @@ async def async_setup_entry( data.update(service_info) ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) entry.async_on_unload( processor.async_add_entities_listener( SensorPushBluetoothSensorEntity, async_add_entities ) ) + entry.async_on_unload(coordinator.async_register_processor(processor)) class SensorPushBluetoothSensorEntity( diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index a40dc8995d1..4eb20dbd943 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -21,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Xiaomi BLE device from a config entry.""" address = entry.unique_id assert address is not None - hass.data.setdefault(DOMAIN, {})[ + coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( hass, @@ -29,6 +29,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address=address, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe return True diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index 9cdf661f5ad..f33c864fa4a 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -167,12 +167,12 @@ async def async_setup_entry( data.update(service_info) ) ) - entry.async_on_unload(coordinator.async_register_processor(processor)) entry.async_on_unload( processor.async_add_entities_listener( XiaomiBluetoothSensorEntity, async_add_entities ) ) + entry.async_on_unload(coordinator.async_register_processor(processor)) class XiaomiBluetoothSensorEntity( diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index e5f992eebb2..ebb69d7c7d0 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -107,6 +107,7 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): _async_register_callback, ): unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() entity_key = PassiveBluetoothEntityKey("temperature", None) entity_key_events = [] @@ -171,6 +172,7 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): assert coordinator.available is True unregister_processor() + cancel_coordinator() async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): @@ -206,6 +208,7 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): _async_register_callback, ): unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() mock_entity = MagicMock() mock_add_entities = MagicMock() @@ -259,6 +262,7 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): assert processor.available is False unregister_processor() + cancel_coordinator() async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): @@ -290,6 +294,7 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): _async_register_callback, ): unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() all_events = [] @@ -310,6 +315,7 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert len(all_events) == 1 unregister_processor() + cancel_coordinator() async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_start): @@ -346,6 +352,7 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta _async_register_callback, ): unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() processor.async_add_listener(MagicMock()) @@ -361,6 +368,7 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert processor.available is True unregister_processor() + cancel_coordinator() async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): @@ -397,6 +405,7 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): _async_register_callback, ): unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() processor.async_add_listener(MagicMock()) @@ -413,6 +422,7 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert processor.available is True unregister_processor() + cancel_coordinator() GOVEE_B5178_REMOTE_SERVICE_INFO = BluetoothServiceInfo( @@ -737,6 +747,7 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): _async_register_callback, ): coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() processor.async_add_listener(MagicMock()) @@ -781,6 +792,7 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): assert entity_one.entity_key == PassiveBluetoothEntityKey( key="temperature", device_id="remote" ) + cancel_coordinator() NO_DEVICES_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( @@ -845,6 +857,7 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner _async_register_callback, ): coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() mock_add_entities = MagicMock() @@ -873,6 +886,7 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner assert entity_one.entity_key == PassiveBluetoothEntityKey( key="temperature", device_id=None ) + cancel_coordinator() async def test_passive_bluetooth_entity_with_entity_platform( @@ -907,6 +921,7 @@ async def test_passive_bluetooth_entity_with_entity_platform( _async_register_callback, ): coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() processor.async_add_entities_listener( PassiveBluetoothProcessorEntity, @@ -926,6 +941,7 @@ async def test_passive_bluetooth_entity_with_entity_platform( hass.states.get("test_domain.test_platform_aa_bb_cc_dd_ee_ff_pressure") is not None ) + cancel_coordinator() SENSOR_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( @@ -999,6 +1015,7 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st ): coordinator.async_register_processor(binary_sensor_processor) coordinator.async_register_processor(sesnor_processor) + cancel_coordinator = coordinator.async_start() binary_sensor_processor.async_add_listener(MagicMock()) sesnor_processor.async_add_listener(MagicMock()) @@ -1056,3 +1073,4 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st assert binary_sensor_entity_one.entity_key == PassiveBluetoothEntityKey( key="motion", device_id=None ) + cancel_coordinator() From bb4ee1ba3273d03b0b9d86dade9c1e145847f1bd Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Jul 2022 21:44:12 +0200 Subject: [PATCH 2876/3516] Adjust wording in raised mitemp_bt issue (#75779) --- homeassistant/components/mitemp_bt/strings.json | 2 +- homeassistant/components/mitemp_bt/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mitemp_bt/strings.json b/homeassistant/components/mitemp_bt/strings.json index 78f047896e1..d36c25eafec 100644 --- a/homeassistant/components/mitemp_bt/strings.json +++ b/homeassistant/components/mitemp_bt/strings.json @@ -2,7 +2,7 @@ "issues": { "replaced": { "title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced", - "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." } } } diff --git a/homeassistant/components/mitemp_bt/translations/en.json b/homeassistant/components/mitemp_bt/translations/en.json index ff131abe367..cd5113ee02f 100644 --- a/homeassistant/components/mitemp_bt/translations/en.json +++ b/homeassistant/components/mitemp_bt/translations/en.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", "title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced" } } From 184e254a43405c6add32f15512f0b4075a5ce98e Mon Sep 17 00:00:00 2001 From: Teemu R Date: Tue, 26 Jul 2022 21:50:41 +0200 Subject: [PATCH 2877/3516] Bump python-eq3bt requirement (#75145) --- homeassistant/components/eq3btsmart/climate.py | 3 +-- homeassistant/components/eq3btsmart/manifest.json | 7 ++++--- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index 1f95f03bd17..412bb8eddeb 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -3,7 +3,6 @@ from __future__ import annotations import logging -from bluepy.btle import BTLEException # pylint: disable=import-error import eq3bt as eq3 # pylint: disable=import-error import voluptuous as vol @@ -218,5 +217,5 @@ class EQ3BTSmartThermostat(ClimateEntity): try: self._thermostat.update() - except BTLEException as ex: + except eq3.BackendException as ex: _LOGGER.warning("Updating the state failed: %s", ex) diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index 4ad8d08adf5..ade3bc0d912 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -1,9 +1,10 @@ { "domain": "eq3btsmart", - "name": "EQ3 Bluetooth Smart Thermostats", + "name": "eQ-3 Bluetooth Smart Thermostats", "documentation": "https://www.home-assistant.io/integrations/eq3btsmart", - "requirements": ["construct==2.10.56", "python-eq3bt==0.1.11"], + "requirements": ["construct==2.10.56", "python-eq3bt==0.2"], + "dependencies": ["bluetooth"], "codeowners": ["@rytilahti"], "iot_class": "local_polling", - "loggers": ["bluepy", "eq3bt"] + "loggers": ["bleak", "eq3bt"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8f89fa07f01..11503aa8f1a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1893,7 +1893,7 @@ python-digitalocean==1.13.2 python-ecobee-api==0.2.14 # homeassistant.components.eq3btsmart -# python-eq3bt==0.1.11 +# python-eq3bt==0.2 # homeassistant.components.etherscan python-etherscan-api==0.0.3 From aaaca0b2bdc9174f1abd9a2cdc85716989f2e971 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Tue, 26 Jul 2022 22:02:50 +0200 Subject: [PATCH 2878/3516] Add tests for the Plugwise Select platform (#75774) --- .coveragerc | 1 - tests/components/plugwise/test_select.py | 44 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/components/plugwise/test_select.py diff --git a/.coveragerc b/.coveragerc index 873629ddd0b..d529cdbd9ca 100644 --- a/.coveragerc +++ b/.coveragerc @@ -927,7 +927,6 @@ omit = homeassistant/components/plex/cast.py homeassistant/components/plex/media_player.py homeassistant/components/plex/view.py - homeassistant/components/plugwise/select.py homeassistant/components/plum_lightpad/light.py homeassistant/components/pocketcasts/sensor.py homeassistant/components/point/__init__.py diff --git a/tests/components/plugwise/test_select.py b/tests/components/plugwise/test_select.py new file mode 100644 index 00000000000..7ec5559a608 --- /dev/null +++ b/tests/components/plugwise/test_select.py @@ -0,0 +1,44 @@ +"""Tests for the Plugwise Select integration.""" + +from unittest.mock import MagicMock + +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_adam_select_entities( + hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test a select.""" + + state = hass.states.get("select.zone_lisa_wk_thermostat_schedule") + assert state + assert state.state == "GF7 Woonkamer" + + +async def test_adam_change_select_entity( + hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test changing of select entities.""" + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.zone_lisa_wk_thermostat_schedule", + ATTR_OPTION: "Badkamer Schema", + }, + blocking=True, + ) + + assert mock_smile_adam.set_schedule_state.call_count == 1 + mock_smile_adam.set_schedule_state.assert_called_with( + "c50f167537524366a5af7aa3942feb1e", "Badkamer Schema", "on" + ) From 2b1a5e5549700df99b2630a9f3840aac499b6cd7 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 26 Jul 2022 16:19:51 -0400 Subject: [PATCH 2879/3516] Bump ZHA dependencies (#75785) --- homeassistant/components/zha/manifest.json | 6 +++--- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 5d5ce7fef12..179302af1cd 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,12 +4,12 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.31.1", + "bellows==0.31.2", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.77", + "zha-quirks==0.0.78", "zigpy-deconz==0.18.0", - "zigpy==0.47.3", + "zigpy==0.48.0", "zigpy-xbee==0.15.0", "zigpy-zigate==0.9.0", "zigpy-znp==0.8.1" diff --git a/requirements_all.txt b/requirements_all.txt index 11503aa8f1a..7a8f314df9f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -396,7 +396,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.31.1 +bellows==0.31.2 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.1 @@ -2514,7 +2514,7 @@ zengge==0.2 zeroconf==0.38.7 # homeassistant.components.zha -zha-quirks==0.0.77 +zha-quirks==0.0.78 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2535,7 +2535,7 @@ zigpy-zigate==0.9.0 zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.47.3 +zigpy==0.48.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d87bce47ab5..bc952f553db 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -320,7 +320,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.31.1 +bellows==0.31.2 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.1 @@ -1694,7 +1694,7 @@ youless-api==0.16 zeroconf==0.38.7 # homeassistant.components.zha -zha-quirks==0.0.77 +zha-quirks==0.0.78 # homeassistant.components.zha zigpy-deconz==0.18.0 @@ -1709,7 +1709,7 @@ zigpy-zigate==0.9.0 zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.47.3 +zigpy==0.48.0 # homeassistant.components.zwave_js zwave-js-server-python==0.39.0 From e2dd2c9424ebbe61559d2bfe29ff53ed651dca52 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 26 Jul 2022 16:42:08 -0400 Subject: [PATCH 2880/3516] Add guards to ZHA light groups when using hs color mode (#75599) * fix polling currentSaturation * Guard light groups against enhanced current hue * Use XY if all group members do not support HS * add config option to always prefer XY color mode * use correct enum * remove periods --- homeassistant/components/zha/core/const.py | 2 + homeassistant/components/zha/light.py | 58 +++++++++++++++---- homeassistant/components/zha/strings.json | 1 + .../components/zha/translations/en.json | 1 + 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index acac96f3162..c72b6373ad0 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -129,6 +129,7 @@ CONF_DATABASE = "database_path" CONF_DEFAULT_LIGHT_TRANSITION = "default_light_transition" CONF_DEVICE_CONFIG = "device_config" CONF_ENABLE_ENHANCED_LIGHT_TRANSITION = "enhanced_light_transition" +CONF_ALWAYS_PREFER_XY_COLOR_MODE = "always_prefer_xy_color_mode" CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join" CONF_ENABLE_QUIRKS = "enable_quirks" CONF_FLOWCONTROL = "flow_control" @@ -145,6 +146,7 @@ CONF_ZHA_OPTIONS_SCHEMA = vol.Schema( { vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION): cv.positive_int, vol.Required(CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, default=False): cv.boolean, + vol.Required(CONF_ALWAYS_PREFER_XY_COLOR_MODE, default=True): cv.boolean, vol.Required(CONF_ENABLE_IDENTIFY_ON_JOIN, default=True): cv.boolean, vol.Optional( CONF_CONSIDER_UNAVAILABLE_MAINS, diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 190e0b2319d..23552c0f469 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -40,6 +40,7 @@ from .core.const import ( CHANNEL_COLOR, CHANNEL_LEVEL, CHANNEL_ON_OFF, + CONF_ALWAYS_PREFER_XY_COLOR_MODE, CONF_DEFAULT_LIGHT_TRANSITION, CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, DATA_ZHA, @@ -120,6 +121,7 @@ class BaseLight(LogMixin, light.LightEntity): self._off_brightness: int | None = None self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME self._zha_config_enhanced_light_transition: bool = False + self._zha_config_always_prefer_xy_color_mode: bool = True self._on_off_channel = None self._level_channel = None self._color_channel = None @@ -280,7 +282,10 @@ class BaseLight(LogMixin, light.LightEntity): self._attr_hs_color = None if hs_color is not None: - if self._color_channel.enhanced_hue_supported: + if ( + not isinstance(self, LightGroup) + and self._color_channel.enhanced_hue_supported + ): result = await self._color_channel.enhanced_move_to_hue_and_saturation( int(hs_color[0] * 65535 / 360), int(hs_color[1] * 2.54), @@ -418,6 +423,13 @@ class Light(BaseLight, ZhaEntity): self._cancel_refresh_handle = None effect_list = [] + self._zha_config_always_prefer_xy_color_mode = async_get_zha_config_value( + zha_device.gateway.config_entry, + ZHA_OPTIONS, + CONF_ALWAYS_PREFER_XY_COLOR_MODE, + True, + ) + self._attr_supported_color_modes = {ColorMode.ONOFF} if self._level_channel: self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS) @@ -429,9 +441,9 @@ class Light(BaseLight, ZhaEntity): self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP) self._attr_color_temp = self._color_channel.color_temperature - if ( - self._color_channel.xy_supported - and not self._color_channel.hs_supported + if self._color_channel.xy_supported and ( + self._zha_config_always_prefer_xy_color_mode + or not self._color_channel.hs_supported ): self._attr_supported_color_modes.add(ColorMode.XY) curr_x = self._color_channel.current_x @@ -441,7 +453,10 @@ class Light(BaseLight, ZhaEntity): else: self._attr_xy_color = (0, 0) - if self._color_channel.hs_supported: + if ( + self._color_channel.hs_supported + and not self._zha_config_always_prefer_xy_color_mode + ): self._attr_supported_color_modes.add(ColorMode.HS) if self._color_channel.enhanced_hue_supported: curr_hue = self._color_channel.enhanced_current_hue * 65535 / 360 @@ -572,12 +587,16 @@ class Light(BaseLight, ZhaEntity): "current_x", "current_y", ] - if self._color_channel.enhanced_hue_supported: + if ( + not self._zha_config_always_prefer_xy_color_mode + and self._color_channel.enhanced_hue_supported + ): attributes.append("enhanced_current_hue") attributes.append("current_saturation") if ( self._color_channel.hs_supported and not self._color_channel.enhanced_hue_supported + and not self._zha_config_always_prefer_xy_color_mode ): attributes.append("current_hue") attributes.append("current_saturation") @@ -598,7 +617,10 @@ class Light(BaseLight, ZhaEntity): self._attr_color_temp = color_temp self._attr_xy_color = None self._attr_hs_color = None - elif color_mode == Color.ColorMode.Hue_and_saturation: + elif ( + color_mode == Color.ColorMode.Hue_and_saturation + and not self._zha_config_always_prefer_xy_color_mode + ): self._attr_color_mode = ColorMode.HS if self._color_channel.enhanced_hue_supported: current_hue = results.get("enhanced_current_hue") @@ -610,12 +632,12 @@ class Light(BaseLight, ZhaEntity): int(current_hue * 360 / 65535) if self._color_channel.enhanced_hue_supported else int(current_hue * 360 / 254), - int(current_saturation / 254), + int(current_saturation / 2.54), ) self._attr_xy_color = None self._attr_color_temp = None else: - self._attr_color_mode = Color.ColorMode.X_and_Y + self._attr_color_mode = ColorMode.XY color_x = results.get("current_x") color_y = results.get("current_y") if color_x is not None and color_y is not None: @@ -704,6 +726,12 @@ class LightGroup(BaseLight, ZhaGroupEntity): CONF_DEFAULT_LIGHT_TRANSITION, 0, ) + self._zha_config_always_prefer_xy_color_mode = async_get_zha_config_value( + zha_device.gateway.config_entry, + ZHA_OPTIONS, + CONF_ALWAYS_PREFER_XY_COLOR_MODE, + True, + ) self._zha_config_enhanced_light_transition = False self._attr_color_mode = None @@ -753,9 +781,10 @@ class LightGroup(BaseLight, ZhaGroupEntity): on_states, light.ATTR_XY_COLOR, reduce=helpers.mean_tuple ) - self._attr_hs_color = helpers.reduce_attribute( - on_states, light.ATTR_HS_COLOR, reduce=helpers.mean_tuple - ) + if not self._zha_config_always_prefer_xy_color_mode: + self._attr_hs_color = helpers.reduce_attribute( + on_states, light.ATTR_HS_COLOR, reduce=helpers.mean_tuple + ) self._attr_color_temp = helpers.reduce_attribute( on_states, light.ATTR_COLOR_TEMP @@ -794,6 +823,11 @@ class LightGroup(BaseLight, ZhaGroupEntity): if ColorMode.BRIGHTNESS in color_mode_count: color_mode_count[ColorMode.BRIGHTNESS] = 0 self._attr_color_mode = color_mode_count.most_common(1)[0][0] + if self._attr_color_mode == ColorMode.HS and ( + color_mode_count[ColorMode.HS] != len(self._group.members) + or self._zha_config_always_prefer_xy_color_mode + ): # switch to XY if all members do not support HS + self._attr_color_mode = ColorMode.XY self._attr_supported_color_modes = None all_supported_color_modes = list( diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index eb7753276f8..f5321565be0 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -38,6 +38,7 @@ "zha_options": { "title": "Global Options", "enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state", + "always_prefer_xy_color_mode": "Always prefer XY color mode", "enable_identify_on_join": "Enable identify effect when devices join the network", "default_light_transition": "Default light transition time (seconds)", "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)", diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index 91c23acc044..0a7795a47b4 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -51,6 +51,7 @@ "default_light_transition": "Default light transition time (seconds)", "enable_identify_on_join": "Enable identify effect when devices join the network", "enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state", + "always_prefer_xy_color_mode": "Always prefer XY color mode", "title": "Global Options" } }, From 0ff34f232c931367d5c221170bd2c31de65d07a6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 26 Jul 2022 22:42:19 +0200 Subject: [PATCH 2881/3516] Add events to repairs issue registry changes (#75784) --- .../components/repairs/issue_registry.py | 17 ++++++ .../components/repairs/test_issue_registry.py | 59 ++++++++++++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/repairs/issue_registry.py b/homeassistant/components/repairs/issue_registry.py index c1eda0d53be..5c459309cc0 100644 --- a/homeassistant/components/repairs/issue_registry.py +++ b/homeassistant/components/repairs/issue_registry.py @@ -13,6 +13,7 @@ import homeassistant.util.dt as dt_util from .models import IssueSeverity DATA_REGISTRY = "issue_registry" +EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED = "repairs_issue_registry_updated" STORAGE_KEY = "repairs.issue_registry" STORAGE_VERSION = 1 SAVE_DELAY = 10 @@ -82,6 +83,10 @@ class IssueRegistry: ) self.issues[(domain, issue_id)] = issue self.async_schedule_save() + self.hass.bus.async_fire( + EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED, + {"action": "create", "domain": domain, "issue_id": issue_id}, + ) else: issue = self.issues[(domain, issue_id)] = dataclasses.replace( issue, @@ -93,6 +98,10 @@ class IssueRegistry: translation_key=translation_key, translation_placeholders=translation_placeholders, ) + self.hass.bus.async_fire( + EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED, + {"action": "update", "domain": domain, "issue_id": issue_id}, + ) return issue @@ -103,6 +112,10 @@ class IssueRegistry: return self.async_schedule_save() + self.hass.bus.async_fire( + EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED, + {"action": "remove", "domain": domain, "issue_id": issue_id}, + ) @callback def async_ignore(self, domain: str, issue_id: str, ignore: bool) -> IssueEntry: @@ -118,6 +131,10 @@ class IssueRegistry: ) self.async_schedule_save() + self.hass.bus.async_fire( + EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED, + {"action": "update", "domain": domain, "issue_id": issue_id}, + ) return issue diff --git a/tests/components/repairs/test_issue_registry.py b/tests/components/repairs/test_issue_registry.py index 1af67601581..523f75bfdc2 100644 --- a/tests/components/repairs/test_issue_registry.py +++ b/tests/components/repairs/test_issue_registry.py @@ -1,11 +1,14 @@ """Test the repairs websocket API.""" from homeassistant.components.repairs import async_create_issue, issue_registry from homeassistant.components.repairs.const import DOMAIN -from homeassistant.components.repairs.issue_handler import async_ignore_issue +from homeassistant.components.repairs.issue_handler import ( + async_delete_issue, + async_ignore_issue, +) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.common import flush_store +from tests.common import async_capture_events, flush_store async def test_load_issues(hass: HomeAssistant) -> None: @@ -33,8 +36,22 @@ async def test_load_issues(hass: HomeAssistant) -> None: "translation_key": "even_worse", "translation_placeholders": {"def": "456"}, }, + { + "breaks_in_ha_version": "2022.7", + "domain": "test", + "issue_id": "issue_3", + "is_fixable": True, + "learn_more_url": "https://checkboxrace.com", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"def": "789"}, + }, ] + events = async_capture_events( + hass, issue_registry.EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED + ) + for issue in issues: async_create_issue( hass, @@ -47,7 +64,45 @@ async def test_load_issues(hass: HomeAssistant) -> None: translation_key=issue["translation_key"], translation_placeholders=issue["translation_placeholders"], ) + + await hass.async_block_till_done() + + assert len(events) == 3 + assert events[0].data == { + "action": "create", + "domain": "test", + "issue_id": "issue_1", + } + assert events[1].data == { + "action": "create", + "domain": "test", + "issue_id": "issue_2", + } + assert events[2].data == { + "action": "create", + "domain": "test", + "issue_id": "issue_3", + } + async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], True) + await hass.async_block_till_done() + + assert len(events) == 4 + assert events[3].data == { + "action": "update", + "domain": "test", + "issue_id": "issue_1", + } + + async_delete_issue(hass, issues[2]["domain"], issues[2]["issue_id"]) + await hass.async_block_till_done() + + assert len(events) == 5 + assert events[4].data == { + "action": "remove", + "domain": "test", + "issue_id": "issue_3", + } registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] assert len(registry.issues) == 2 From c5912f0faee0936b972bbe994bf45018dabb793e Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 27 Jul 2022 05:04:08 +0800 Subject: [PATCH 2882/3516] Cleanup unused camera constants (#75772) Cleanup unused camera constants after #75452 --- homeassistant/components/camera/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index dcee6c6c5ed..247f73c89f2 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -129,14 +129,6 @@ CAMERA_SERVICE_RECORD: Final = { vol.Optional(CONF_LOOKBACK, default=0): vol.Coerce(int), } -WS_TYPE_CAMERA_THUMBNAIL: Final = "camera_thumbnail" -SCHEMA_WS_CAMERA_THUMBNAIL: Final = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - { - vol.Required("type"): WS_TYPE_CAMERA_THUMBNAIL, - vol.Required("entity_id"): cv.entity_id, - } -) - @dataclass class CameraEntityDescription(EntityDescription): From fb52f5098fd84bff7753628907926de2661d16ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Henriques?= Date: Tue, 26 Jul 2022 22:36:46 +0100 Subject: [PATCH 2883/3516] Add en-GB locale for AlexaMotionSensor and AlexaContactSensor (#75705) Added en-GB as supported locale for AlexaMotionSensor and AlexaContactSensor --- homeassistant/components/alexa/capabilities.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 25ec43b689c..3963372fec1 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -903,6 +903,7 @@ class AlexaContactSensor(AlexaCapability): "en-CA", "en-IN", "en-US", + "en-GB", "es-ES", "it-IT", "ja-JP", @@ -951,6 +952,7 @@ class AlexaMotionSensor(AlexaCapability): "en-CA", "en-IN", "en-US", + "en-GB", "es-ES", "it-IT", "ja-JP", From 6868865e3ebc303e6e3d76d669f5ba31fad32b14 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 27 Jul 2022 00:01:49 +0200 Subject: [PATCH 2884/3516] Add 1.5 second sleep to motion blinds update (#75494) --- .../components/motion_blinds/__init__.py | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index 184e721aeed..dfbc6ab74a7 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -65,36 +65,43 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): self._wait_for_push = coordinator_info[CONF_WAIT_FOR_PUSH] def update_gateway(self): - """Call all updates using one async_add_executor_job.""" - data = {} - + """Fetch data from gateway.""" try: self._gateway.Update() except (timeout, ParseException): # let the error be logged and handled by the motionblinds library - data[KEY_GATEWAY] = {ATTR_AVAILABLE: False} - return data + return {ATTR_AVAILABLE: False} else: - data[KEY_GATEWAY] = {ATTR_AVAILABLE: True} + return {ATTR_AVAILABLE: True} - for blind in self._gateway.device_list.values(): - try: - 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} + def update_blind(self, blind): + """Fetch data from a blind.""" + try: + if self._wait_for_push: + blind.Update() else: - data[blind.mac] = {ATTR_AVAILABLE: True} - - return data + blind.Update_trigger() + except (timeout, ParseException): + # let the error be logged and handled by the motionblinds library + return {ATTR_AVAILABLE: False} + else: + return {ATTR_AVAILABLE: True} async def _async_update_data(self): """Fetch the latest data from the gateway and blinds.""" + data = {} + async with self.api_lock: - data = await self.hass.async_add_executor_job(self.update_gateway) + data[KEY_GATEWAY] = await self.hass.async_add_executor_job( + self.update_gateway + ) + + for blind in self._gateway.device_list.values(): + await asyncio.sleep(1.5) + async with self.api_lock: + data[blind.mac] = await self.hass.async_add_executor_job( + self.update_blind, blind + ) all_available = all(device[ATTR_AVAILABLE] for device in data.values()) if all_available: From 129b42cd23301fb4df30a48c971af1200b4c6e00 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Wed, 27 Jul 2022 02:03:17 +0200 Subject: [PATCH 2885/3516] Fix ZHA light brightness jumping around during transitions (#74849) * Moved color commands to a new ``async_handle_color_commands`` method * Fixed tests * Fix brightness jumping around during transitions * Add config option to disable "Enhanced brightness slider during light transition" --- homeassistant/components/zha/core/const.py | 2 + homeassistant/components/zha/light.py | 355 ++++++++++++++---- homeassistant/components/zha/strings.json | 1 + .../components/zha/translations/en.json | 3 +- tests/components/zha/common.py | 11 + tests/components/zha/test_light.py | 14 + 6 files changed, 320 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index c72b6373ad0..4c8c6e03c79 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -129,6 +129,7 @@ CONF_DATABASE = "database_path" CONF_DEFAULT_LIGHT_TRANSITION = "default_light_transition" CONF_DEVICE_CONFIG = "device_config" CONF_ENABLE_ENHANCED_LIGHT_TRANSITION = "enhanced_light_transition" +CONF_ENABLE_LIGHT_TRANSITIONING_FLAG = "light_transitioning_flag" CONF_ALWAYS_PREFER_XY_COLOR_MODE = "always_prefer_xy_color_mode" CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join" CONF_ENABLE_QUIRKS = "enable_quirks" @@ -146,6 +147,7 @@ CONF_ZHA_OPTIONS_SCHEMA = vol.Schema( { vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION): cv.positive_int, vol.Required(CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, default=False): cv.boolean, + vol.Required(CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, default=True): cv.boolean, vol.Required(CONF_ALWAYS_PREFER_XY_COLOR_MODE, default=True): cv.boolean, vol.Required(CONF_ENABLE_IDENTIFY_ON_JOIN, default=True): cv.boolean, vol.Optional( diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 23552c0f469..9fc089e2241 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -1,7 +1,9 @@ """Lights on Zigbee Home Automation networks.""" from __future__ import annotations +import asyncio from collections import Counter +from collections.abc import Callable from datetime import timedelta import functools import itertools @@ -26,14 +28,14 @@ from homeassistant.const import ( STATE_UNAVAILABLE, Platform, ) -from homeassistant.core import HomeAssistant, State, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.event import async_call_later, async_track_time_interval from .core import discovery, helpers from .core.const import ( @@ -43,6 +45,7 @@ from .core.const import ( CONF_ALWAYS_PREFER_XY_COLOR_MODE, CONF_DEFAULT_LIGHT_TRANSITION, CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, + CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, DATA_ZHA, EFFECT_BLINK, EFFECT_BREATHE, @@ -61,6 +64,10 @@ if TYPE_CHECKING: _LOGGER = logging.getLogger(__name__) +DEFAULT_ON_OFF_TRANSITION = 1 # most bulbs default to a 1-second turn on/off transition +DEFAULT_EXTRA_TRANSITION_DELAY_SHORT = 0.25 +DEFAULT_EXTRA_TRANSITION_DELAY_LONG = 2.0 +DEFAULT_LONG_TRANSITION_TIME = 10 DEFAULT_MIN_BRIGHTNESS = 2 UPDATE_COLORLOOP_ACTION = 0x1 @@ -75,6 +82,8 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.LIGHT) GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.LIGHT) PARALLEL_UPDATES = 0 SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed" +SIGNAL_LIGHT_GROUP_TRANSITION_START = "zha_light_group_transition_start" +SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED = "zha_light_group_transition_finished" DEFAULT_MIN_TRANSITION_MANUFACTURERS = {"Sengled"} COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.XY} @@ -111,6 +120,7 @@ class BaseLight(LogMixin, light.LightEntity): def __init__(self, *args, **kwargs): """Initialize the light.""" + self._zha_device: ZHADevice = None super().__init__(*args, **kwargs) self._attr_min_mireds: int | None = 153 self._attr_max_mireds: int | None = 500 @@ -121,11 +131,14 @@ class BaseLight(LogMixin, light.LightEntity): self._off_brightness: int | None = None self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME self._zha_config_enhanced_light_transition: bool = False + self._zha_config_enable_light_transitioning_flag: bool = True self._zha_config_always_prefer_xy_color_mode: bool = True self._on_off_channel = None self._level_channel = None self._color_channel = None self._identify_channel = None + self._transitioning: bool = False + self._transition_listener: Callable[[], None] | None = None @property def extra_state_attributes(self) -> dict[str, Any]: @@ -151,6 +164,12 @@ class BaseLight(LogMixin, light.LightEntity): on at `on_level` Zigbee attribute value, regardless of the last set level """ + if self._transitioning: + self.debug( + "received level %s while transitioning - skipping update", + value, + ) + return value = max(0, min(254, value)) self._attr_brightness = value self.async_write_ha_state() @@ -170,6 +189,36 @@ class BaseLight(LogMixin, light.LightEntity): xy_color = kwargs.get(light.ATTR_XY_COLOR) hs_color = kwargs.get(light.ATTR_HS_COLOR) + set_transition_flag = ( + brightness_supported(self._attr_supported_color_modes) + or temperature is not None + or xy_color is not None + or hs_color is not None + ) and self._zha_config_enable_light_transitioning_flag + transition_time = ( + ( + duration / 10 + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT + if ( + (brightness is not None or transition is not None) + and brightness_supported(self._attr_supported_color_modes) + or (self._off_with_transition and self._off_brightness is not None) + or temperature is not None + or xy_color is not None + or hs_color is not None + ) + else DEFAULT_ON_OFF_TRANSITION + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT + ) + if set_transition_flag + else 0 + ) + + # If we need to pause attribute report parsing, we'll do so here. + # After successful calls, we later start a timer to unset the flag after transition_time. + # On an error on the first move to level call, we unset the flag immediately if no previous timer is running. + # On an error on subsequent calls, we start the transition timer, as a brightness call might have come through. + if set_transition_flag: + self.async_transition_set_flag() + # If the light is currently off but a turn_on call with a color/temperature is sent, # the light needs to be turned on first at a low brightness level where the light is immediately transitioned # to the correct color. Afterwards, the transition is only from the low brightness to the new brightness. @@ -230,6 +279,10 @@ class BaseLight(LogMixin, light.LightEntity): ) t_log["move_to_level_with_on_off"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + # First 'move to level' call failed, so if the transitioning delay isn't running from a previous call, + # the flag can be unset immediately + if set_transition_flag and not self._transition_listener: + self.async_transition_complete() self.debug("turned on: %s", t_log) return # Currently only setting it to "on", as the correct level state will be set at the second move_to_level call @@ -245,6 +298,10 @@ class BaseLight(LogMixin, light.LightEntity): ) t_log["move_to_level_with_on_off"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + # First 'move to level' call failed, so if the transitioning delay isn't running from a previous call, + # the flag can be unset immediately + if set_transition_flag and not self._transition_listener: + self.async_transition_complete() self.debug("turned on: %s", t_log) return self._attr_state = bool(level) @@ -261,73 +318,25 @@ class BaseLight(LogMixin, light.LightEntity): result = await self._on_off_channel.on() t_log["on_off"] = result if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + # 'On' call failed, but as brightness may still transition (for FORCE_ON lights), + # we start the timer to unset the flag after the transition_time if necessary. + self.async_transition_start_timer(transition_time) self.debug("turned on: %s", t_log) return self._attr_state = True - if temperature is not None: - result = await self._color_channel.move_to_color_temp( - temperature, - self._DEFAULT_MIN_TRANSITION_TIME - if new_color_provided_while_off - else duration, - ) - t_log["move_to_color_temp"] = result - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: - self.debug("turned on: %s", t_log) - return - self._attr_color_mode = ColorMode.COLOR_TEMP - self._attr_color_temp = temperature - self._attr_xy_color = None - self._attr_hs_color = None - - if hs_color is not None: - if ( - not isinstance(self, LightGroup) - and self._color_channel.enhanced_hue_supported - ): - result = await self._color_channel.enhanced_move_to_hue_and_saturation( - int(hs_color[0] * 65535 / 360), - int(hs_color[1] * 2.54), - self._DEFAULT_MIN_TRANSITION_TIME - if new_color_provided_while_off - else duration, - ) - t_log["enhanced_move_to_hue_and_saturation"] = result - else: - result = await self._color_channel.move_to_hue_and_saturation( - int(hs_color[0] * 254 / 360), - int(hs_color[1] * 2.54), - self._DEFAULT_MIN_TRANSITION_TIME - if new_color_provided_while_off - else duration, - ) - t_log["move_to_hue_and_saturation"] = result - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: - self.debug("turned on: %s", t_log) - return - self._attr_color_mode = ColorMode.HS - self._attr_hs_color = hs_color - self._attr_xy_color = None - self._attr_color_temp = None - xy_color = None # don't set xy_color if it is also present - - if xy_color is not None: - result = await self._color_channel.move_to_color( - int(xy_color[0] * 65535), - int(xy_color[1] * 65535), - self._DEFAULT_MIN_TRANSITION_TIME - if new_color_provided_while_off - else duration, - ) - t_log["move_to_color"] = result - if isinstance(result, Exception) or result[1] is not Status.SUCCESS: - self.debug("turned on: %s", t_log) - return - self._attr_color_mode = ColorMode.XY - self._attr_xy_color = xy_color - self._attr_color_temp = None - self._attr_hs_color = None + if not await self.async_handle_color_commands( + temperature, + duration, + hs_color, + xy_color, + new_color_provided_while_off, + t_log, + ): + # Color calls failed, but as brightness may still transition, we start the timer to unset the flag + self.async_transition_start_timer(transition_time) + self.debug("turned on: %s", t_log) + return if new_color_provided_while_off: # The light is has the correct color, so we can now transition it to the correct brightness level. @@ -340,6 +349,10 @@ class BaseLight(LogMixin, light.LightEntity): if level: self._attr_brightness = level + # Our light is guaranteed to have just started the transitioning process if necessary, + # so we start the delay for the transition (to stop parsing attribute reports after the completed transition). + self.async_transition_start_timer(transition_time) + if effect == light.EFFECT_COLORLOOP: result = await self._color_channel.color_loop_set( UPDATE_COLORLOOP_ACTION @@ -382,6 +395,15 @@ class BaseLight(LogMixin, light.LightEntity): transition = kwargs.get(light.ATTR_TRANSITION) supports_level = brightness_supported(self._attr_supported_color_modes) + transition_time = ( + transition or self._DEFAULT_MIN_TRANSITION_TIME + if transition is not None + else DEFAULT_ON_OFF_TRANSITION + ) + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT + # Start pausing attribute report parsing + if self._zha_config_enable_light_transitioning_flag: + self.async_transition_set_flag() + # is not none looks odd here but it will override built in bulb transition times if we pass 0 in here if transition is not None and supports_level: result = await self._level_channel.move_to_level_with_on_off( @@ -389,6 +411,10 @@ class BaseLight(LogMixin, light.LightEntity): ) else: result = await self._on_off_channel.off() + + # Pause parsing attribute reports until transition is complete + if self._zha_config_enable_light_transitioning_flag: + self.async_transition_start_timer(transition_time) self.debug("turned off: %s", result) if isinstance(result, Exception) or result[1] is not Status.SUCCESS: return @@ -401,6 +427,127 @@ class BaseLight(LogMixin, light.LightEntity): self.async_write_ha_state() + async def async_handle_color_commands( + self, + temperature, + duration, + hs_color, + xy_color, + new_color_provided_while_off, + t_log, + ): + """Process ZCL color commands.""" + if temperature is not None: + result = await self._color_channel.move_to_color_temp( + temperature, + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, + ) + t_log["move_to_color_temp"] = result + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + return False + self._attr_color_mode = ColorMode.COLOR_TEMP + self._attr_color_temp = temperature + self._attr_xy_color = None + self._attr_hs_color = None + + if hs_color is not None: + if ( + not isinstance(self, LightGroup) + and self._color_channel.enhanced_hue_supported + ): + result = await self._color_channel.enhanced_move_to_hue_and_saturation( + int(hs_color[0] * 65535 / 360), + int(hs_color[1] * 2.54), + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, + ) + t_log["enhanced_move_to_hue_and_saturation"] = result + else: + result = await self._color_channel.move_to_hue_and_saturation( + int(hs_color[0] * 254 / 360), + int(hs_color[1] * 2.54), + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, + ) + t_log["move_to_hue_and_saturation"] = result + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + return False + self._attr_color_mode = ColorMode.HS + self._attr_hs_color = hs_color + self._attr_xy_color = None + self._attr_color_temp = None + xy_color = None # don't set xy_color if it is also present + + if xy_color is not None: + result = await self._color_channel.move_to_color( + int(xy_color[0] * 65535), + int(xy_color[1] * 65535), + self._DEFAULT_MIN_TRANSITION_TIME + if new_color_provided_while_off + else duration, + ) + t_log["move_to_color"] = result + if isinstance(result, Exception) or result[1] is not Status.SUCCESS: + return False + self._attr_color_mode = ColorMode.XY + self._attr_xy_color = xy_color + self._attr_color_temp = None + self._attr_hs_color = None + + return True + + @callback + def async_transition_set_flag(self) -> None: + """Set _transitioning to True.""" + self.debug("setting transitioning flag to True") + self._transitioning = True + if isinstance(self, LightGroup): + async_dispatcher_send( + self.hass, + SIGNAL_LIGHT_GROUP_TRANSITION_START, + {"entity_ids": self._entity_ids}, + ) + if self._transition_listener is not None: + self._transition_listener() + + @callback + def async_transition_start_timer(self, transition_time) -> None: + """Start a timer to unset _transitioning after transition_time if necessary.""" + if not transition_time: + return + # For longer transitions, we want to extend the timer a bit more + if transition_time >= DEFAULT_LONG_TRANSITION_TIME: + transition_time += DEFAULT_EXTRA_TRANSITION_DELAY_LONG + self.debug("starting transitioning timer for %s", transition_time) + self._transition_listener = async_call_later( + self._zha_device.hass, + transition_time, + self.async_transition_complete, + ) + + @callback + def async_transition_complete(self, _=None) -> None: + """Set _transitioning to False and write HA state.""" + self.debug("transition complete - future attribute reports will write HA state") + self._transitioning = False + if self._transition_listener: + self._transition_listener() + self._transition_listener = None + self.async_write_ha_state() + if isinstance(self, LightGroup): + async_dispatcher_send( + self.hass, + SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED, + {"entity_ids": self._entity_ids}, + ) + if self._debounced_member_refresh is not None: + self.debug("transition complete - refreshing group member states") + asyncio.create_task(self._debounced_member_refresh.async_call()) + @STRICT_MATCH(channel_names=CHANNEL_ON_OFF, aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}) class Light(BaseLight, ZhaEntity): @@ -506,10 +653,22 @@ class Light(BaseLight, ZhaEntity): CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, False, ) + self._zha_config_enable_light_transitioning_flag = async_get_zha_config_value( + zha_device.gateway.config_entry, + ZHA_OPTIONS, + CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, + True, + ) @callback def async_set_state(self, attr_id, attr_name, value): """Set the state.""" + if self._transitioning: + self.debug( + "received onoff %s while transitioning - skipping update", + value, + ) + return self._attr_state = bool(value) if value: self._off_with_transition = False @@ -537,6 +696,38 @@ class Light(BaseLight, ZhaEntity): signal_override=True, ) + @callback + def transition_on(signal): + """Handle a transition start event from a group.""" + if self.entity_id in signal["entity_ids"]: + self.debug( + "group transition started - setting member transitioning flag" + ) + self._transitioning = True + + self.async_accept_signal( + None, + SIGNAL_LIGHT_GROUP_TRANSITION_START, + transition_on, + signal_override=True, + ) + + @callback + def transition_off(signal): + """Handle a transition finished event from a group.""" + if self.entity_id in signal["entity_ids"]: + self.debug( + "group transition completed - unsetting member transitioning flag" + ) + self._transitioning = False + + self.async_accept_signal( + None, + SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED, + transition_off, + signal_override=True, + ) + async def async_will_remove_from_hass(self) -> None: """Disconnect entity object when removed.""" assert self._cancel_refresh_handle @@ -654,16 +845,25 @@ class Light(BaseLight, ZhaEntity): async def async_update(self): """Update to the latest state.""" + if self._transitioning: + self.debug("skipping async_update while transitioning") + return await self.async_get_state() async def _refresh(self, time): """Call async_get_state at an interval.""" + if self._transitioning: + self.debug("skipping _refresh while transitioning") + return await self.async_get_state() self.async_write_ha_state() async def _maybe_force_refresh(self, signal): """Force update the state if the signal contains the entity id for this entity.""" if self.entity_id in signal["entity_ids"]: + if self._transitioning: + self.debug("skipping _maybe_force_refresh while transitioning") + return await self.async_get_state() self.async_write_ha_state() @@ -726,6 +926,12 @@ class LightGroup(BaseLight, ZhaGroupEntity): CONF_DEFAULT_LIGHT_TRANSITION, 0, ) + self._zha_config_enable_light_transitioning_flag = async_get_zha_config_value( + zha_device.gateway.config_entry, + ZHA_OPTIONS, + CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, + True, + ) self._zha_config_always_prefer_xy_color_mode = async_get_zha_config_value( zha_device.gateway.config_entry, ZHA_OPTIONS, @@ -757,13 +963,32 @@ class LightGroup(BaseLight, ZhaGroupEntity): async def async_turn_on(self, **kwargs): """Turn the entity on.""" await super().async_turn_on(**kwargs) + if self._transitioning: + return await self._debounced_member_refresh.async_call() async def async_turn_off(self, **kwargs): """Turn the entity off.""" await super().async_turn_off(**kwargs) + if self._transitioning: + return await self._debounced_member_refresh.async_call() + @callback + def async_state_changed_listener(self, event: Event): + """Handle child updates.""" + if self._transitioning: + self.debug("skipping group entity state update during transition") + return + super().async_state_changed_listener(event) + + async def async_update_ha_state(self, force_refresh: bool = False) -> None: + """Update Home Assistant with current state of entity.""" + if self._transitioning: + self.debug("skipping group entity state update during transition") + return + await super().async_update_ha_state(force_refresh) + async def async_update(self) -> None: """Query all members and determine the light group state.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index f5321565be0..4eb872f4fae 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -38,6 +38,7 @@ "zha_options": { "title": "Global Options", "enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state", + "light_transitioning_flag": "Enable enhanced brightness slider during light transition", "always_prefer_xy_color_mode": "Always prefer XY color mode", "enable_identify_on_join": "Enable identify effect when devices join the network", "default_light_transition": "Default light transition time (seconds)", diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index 0a7795a47b4..757ab338ec6 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -46,12 +46,13 @@ "title": "Alarm Control Panel Options" }, "zha_options": { + "always_prefer_xy_color_mode": "Always prefer XY color mode", "consider_unavailable_battery": "Consider battery powered devices unavailable after (seconds)", "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)", "default_light_transition": "Default light transition time (seconds)", "enable_identify_on_join": "Enable identify effect when devices join the network", "enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state", - "always_prefer_xy_color_mode": "Always prefer XY color mode", + "light_transitioning_flag": "Enable enhanced brightness slider during light transition", "title": "Global Options" } }, diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 6a51f441a78..cad8020267f 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -1,5 +1,6 @@ """Common test objects.""" import asyncio +from datetime import timedelta import math from unittest.mock import AsyncMock, Mock @@ -8,6 +9,9 @@ import zigpy.zcl.foundation as zcl_f import homeassistant.components.zha.core.const as zha_const from homeassistant.helpers import entity_registry +import homeassistant.util.dt as dt_util + +from tests.common import async_fire_time_changed def patch_cluster(cluster): @@ -232,3 +236,10 @@ async def async_wait_for_updates(hass): await asyncio.sleep(0) await asyncio.sleep(0) await hass.async_block_till_done() + + +async def async_shift_time(hass): + """Shift time to cause call later tasks to run.""" + next_update = dt_util.utcnow() + timedelta(seconds=11) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 94f0c96c38d..156f692aa14 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -22,6 +22,7 @@ import homeassistant.util.dt as dt_util from .common import ( async_enable_traffic, async_find_group_entity_id, + async_shift_time, async_test_rejoin, find_entity_id, get_zha_gateway, @@ -346,6 +347,7 @@ async def test_light( await async_test_level_on_off_from_hass( hass, cluster_on_off, cluster_level, entity_id ) + await async_shift_time(hass) # test getting a brightness change from the network await async_test_on_from_light(hass, cluster_on_off, entity_id) @@ -1190,6 +1192,8 @@ async def async_test_level_on_off_from_hass( on_off_cluster.request.reset_mock() level_cluster.request.reset_mock() + await async_shift_time(hass) + # turn on via UI await hass.services.async_call( LIGHT_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True @@ -1210,6 +1214,8 @@ async def async_test_level_on_off_from_hass( on_off_cluster.request.reset_mock() level_cluster.request.reset_mock() + await async_shift_time(hass) + await hass.services.async_call( LIGHT_DOMAIN, "turn_on", @@ -1407,11 +1413,15 @@ async def test_zha_group_light_entity( # test turning the lights on and off from the HA await async_test_on_off_from_hass(hass, group_cluster_on_off, group_entity_id) + await async_shift_time(hass) + # test short flashing the lights from the HA await async_test_flash_from_hass( hass, group_cluster_identify, group_entity_id, FLASH_SHORT ) + await async_shift_time(hass) + # test turning the lights on and off from the light await async_test_on_off_from_light(hass, dev1_cluster_on_off, group_entity_id) @@ -1424,6 +1434,8 @@ async def test_zha_group_light_entity( expected_default_transition=1, # a Sengled light is in that group and needs a minimum 0.1s transition ) + await async_shift_time(hass) + # test getting a brightness change from the network await async_test_on_from_light(hass, dev1_cluster_on_off, group_entity_id) await async_test_dimmer_from_light( @@ -1443,6 +1455,8 @@ async def test_zha_group_light_entity( hass, group_cluster_identify, group_entity_id, FLASH_LONG ) + await async_shift_time(hass) + assert len(zha_group.members) == 2 # test some of the group logic to make sure we key off states correctly await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) From 33c635809c1de156845d9a10b5fd364a53b3efb5 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 27 Jul 2022 00:28:57 +0000 Subject: [PATCH 2886/3516] [ci skip] Translation update --- .../components/bluetooth/translations/el.json | 12 ++++++++++- .../components/bluetooth/translations/it.json | 12 ++++++++++- .../components/bluetooth/translations/pl.json | 12 ++++++++++- .../components/bluetooth/translations/ru.json | 12 ++++++++++- .../components/demo/translations/ru.json | 8 ++++++- .../components/esphome/translations/ru.json | 4 ++-- .../components/google/translations/de.json | 10 +++++++++ .../components/google/translations/el.json | 10 +++++++++ .../components/google/translations/en.json | 4 ++-- .../components/google/translations/it.json | 10 +++++++++ .../components/google/translations/pl.json | 10 +++++++++ .../components/google/translations/pt-BR.json | 10 +++++++++ .../components/google/translations/ru.json | 10 +++++++++ .../google/translations/zh-Hant.json | 10 +++++++++ .../components/govee_ble/translations/pl.json | 21 +++++++++++++++++++ .../components/govee_ble/translations/ru.json | 21 +++++++++++++++++++ .../homekit_controller/translations/ru.json | 4 ++-- .../components/knx/translations/ru.json | 2 +- .../components/life360/translations/ru.json | 2 +- .../components/lyric/translations/el.json | 6 ++++++ .../components/lyric/translations/it.json | 6 ++++++ .../components/lyric/translations/pl.json | 6 ++++++ .../components/miflora/translations/de.json | 8 +++++++ .../components/miflora/translations/el.json | 8 +++++++ .../components/miflora/translations/it.json | 8 +++++++ .../components/miflora/translations/pl.json | 8 +++++++ .../miflora/translations/pt-BR.json | 8 +++++++ .../miflora/translations/zh-Hant.json | 8 +++++++ .../components/mitemp_bt/translations/de.json | 8 +++++++ .../components/mitemp_bt/translations/el.json | 8 +++++++ .../components/mitemp_bt/translations/pl.json | 8 +++++++ .../mitemp_bt/translations/pt-BR.json | 8 +++++++ .../mitemp_bt/translations/zh-Hant.json | 8 +++++++ .../components/moat/translations/pl.json | 21 +++++++++++++++++++ .../components/moat/translations/ru.json | 21 +++++++++++++++++++ .../components/nest/translations/ru.json | 14 ++++++++++++- .../components/plugwise/translations/ru.json | 4 ++-- .../radiotherm/translations/el.json | 6 ++++++ .../radiotherm/translations/it.json | 5 +++++ .../radiotherm/translations/pl.json | 6 ++++++ .../radiotherm/translations/ru.json | 6 ++++++ .../components/senz/translations/el.json | 6 ++++++ .../components/senz/translations/it.json | 6 ++++++ .../components/senz/translations/pl.json | 6 ++++++ .../simplisafe/translations/el.json | 5 ++++- .../simplisafe/translations/pl.json | 7 +++++-- .../simplisafe/translations/ru.json | 7 +++++-- .../components/spotify/translations/de.json | 6 ++++++ .../components/spotify/translations/el.json | 6 ++++++ .../components/spotify/translations/it.json | 6 ++++++ .../components/spotify/translations/pl.json | 6 ++++++ .../spotify/translations/pt-BR.json | 6 ++++++ .../components/spotify/translations/ru.json | 6 ++++++ .../spotify/translations/zh-Hant.json | 6 ++++++ .../steam_online/translations/de.json | 6 ++++++ .../steam_online/translations/el.json | 6 ++++++ .../steam_online/translations/it.json | 6 ++++++ .../steam_online/translations/pl.json | 6 ++++++ .../steam_online/translations/pt-BR.json | 6 ++++++ .../steam_online/translations/ru.json | 6 ++++++ .../steam_online/translations/zh-Hant.json | 6 ++++++ .../components/switchbot/translations/el.json | 1 + .../components/switchbot/translations/pl.json | 3 ++- .../components/switchbot/translations/ru.json | 3 ++- .../xiaomi_ble/translations/el.json | 15 +++++++++++++ .../xiaomi_ble/translations/pl.json | 15 +++++++++++++ .../xiaomi_ble/translations/ru.json | 15 +++++++++++++ .../components/zha/translations/it.json | 1 + 68 files changed, 524 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/govee_ble/translations/pl.json create mode 100644 homeassistant/components/govee_ble/translations/ru.json create mode 100644 homeassistant/components/miflora/translations/de.json create mode 100644 homeassistant/components/miflora/translations/el.json create mode 100644 homeassistant/components/miflora/translations/it.json create mode 100644 homeassistant/components/miflora/translations/pl.json create mode 100644 homeassistant/components/miflora/translations/pt-BR.json create mode 100644 homeassistant/components/miflora/translations/zh-Hant.json create mode 100644 homeassistant/components/mitemp_bt/translations/de.json create mode 100644 homeassistant/components/mitemp_bt/translations/el.json create mode 100644 homeassistant/components/mitemp_bt/translations/pl.json create mode 100644 homeassistant/components/mitemp_bt/translations/pt-BR.json create mode 100644 homeassistant/components/mitemp_bt/translations/zh-Hant.json create mode 100644 homeassistant/components/moat/translations/pl.json create mode 100644 homeassistant/components/moat/translations/ru.json diff --git a/homeassistant/components/bluetooth/translations/el.json b/homeassistant/components/bluetooth/translations/el.json index 03b3be612a3..5a0aee96322 100644 --- a/homeassistant/components/bluetooth/translations/el.json +++ b/homeassistant/components/bluetooth/translations/el.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "no_adapters": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03b5\u03af\u03c2 Bluetooth" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "\u039f \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ad\u03b1\u03c2 Bluetooth \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/it.json b/homeassistant/components/bluetooth/translations/it.json index af83bfd271d..86809b41a7d 100644 --- a/homeassistant/components/bluetooth/translations/it.json +++ b/homeassistant/components/bluetooth/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "no_adapters": "Nessun adattatore Bluetooth trovato" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "Seleziona un dispositivo da configurare" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "L'adattatore Bluetooth da utilizzare per la scansione" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/pl.json b/homeassistant/components/bluetooth/translations/pl.json index 99ad564ebd9..f7e6fe060af 100644 --- a/homeassistant/components/bluetooth/translations/pl.json +++ b/homeassistant/components/bluetooth/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "no_adapters": "Nie znaleziono adapter\u00f3w Bluetooth" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "Wybierz urz\u0105dzenie do skonfigurowania" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Adapter Bluetooth u\u017cywany do skanowania" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/ru.json b/homeassistant/components/bluetooth/translations/ru.json index 972b858718c..802470d7c29 100644 --- a/homeassistant/components/bluetooth/translations/ru.json +++ b/homeassistant/components/bluetooth/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "no_adapters": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440\u044b Bluetooth \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b." }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "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" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440 Bluetooth, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/demo/translations/ru.json b/homeassistant/components/demo/translations/ru.json index c08143d6b41..3c919e12d84 100644 --- a/homeassistant/components/demo/translations/ru.json +++ b/homeassistant/components/demo/translations/ru.json @@ -8,9 +8,15 @@ "title": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u043e\u043b\u0438\u0442\u044c \u0436\u0438\u0434\u043a\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u043f\u043e\u0432\u043e\u0440\u043e\u0442\u043d\u0438\u043a\u043e\u0432" } } - } + }, + "title": "\u0416\u0438\u0434\u043a\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u043f\u043e\u0432\u043e\u0440\u043e\u0442\u043d\u0438\u043a\u043e\u0432 \u0437\u0430\u043a\u0430\u043d\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044f" + }, + "transmogrifier_deprecated": { + "description": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0442\u0440\u0430\u043d\u0441\u043c\u043e\u0433\u0440\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0443\u0441\u0442\u0430\u0440\u0435\u043b \u0432 \u0441\u0432\u044f\u0437\u0438 \u0441 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435\u043c \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f, \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0433\u043e \u0432 \u043d\u043e\u0432\u043e\u043c API.", + "title": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0442\u0440\u0430\u043d\u0441\u043c\u043e\u0433\u0440\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0443\u0441\u0442\u0430\u0440\u0435\u043b" }, "unfixable_problem": { + "description": "\u042d\u0442\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u043d\u0438\u043a\u043e\u0433\u0434\u0430 \u043d\u0435 \u043e\u0442\u0441\u0442\u0443\u043f\u0438\u0442.", "title": "\u042d\u0442\u043e \u043d\u0435 \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u043c\u0430\u044f \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430" } }, diff --git a/homeassistant/components/esphome/translations/ru.json b/homeassistant/components/esphome/translations/ru.json index 8ba4a573cec..ea0a3105226 100644 --- a/homeassistant/components/esphome/translations/ru.json +++ b/homeassistant/components/esphome/translations/ru.json @@ -9,7 +9,7 @@ "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." + "resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \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, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441." }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 [ESPHome](https://esphomelib.com/)." + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/google/translations/de.json b/homeassistant/components/google/translations/de.json index 9900d3eb6d1..2e81b2357c8 100644 --- a/homeassistant/components/google/translations/de.json +++ b/homeassistant/components/google/translations/de.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration des Google Kalenders in configuration.yaml wird in Home Assistant 2022.9 entfernt. \n\nDeine bestehenden OAuth-Anwendungsdaten und Zugriffseinstellungen wurden automatisch in die Benutzeroberfl\u00e4che importiert. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Google Calendar YAML-Konfiguration wird entfernt" + }, + "removed_track_new_yaml": { + "description": "Du hast die Entit\u00e4tsverfolgung f\u00fcr Google Kalender in configuration.yaml deaktiviert, was nicht mehr unterst\u00fctzt wird. Du musst die Integrationssystemoptionen in der Benutzeroberfl\u00e4che manuell \u00e4ndern, um neu entdeckte Entit\u00e4ten in Zukunft zu deaktivieren. Entferne die Einstellung track_new aus configuration.yaml und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Google Calendar Entity Tracking hat sich ge\u00e4ndert" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/el.json b/homeassistant/components/google/translations/el.json index 21e5580da3d..0bf592d60d4 100644 --- a/homeassistant/components/google/translations/el.json +++ b/homeassistant/components/google/translations/el.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03bf\u03b3\u03af\u03bf\u03c5 Google \u03c3\u03c4\u03bf configuration.yaml \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant 2022.9. \n\n \u03a4\u03b1 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03bd\u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 OAuth \u03ba\u03b1\u03b9 \u03bf\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03bf\u03c5 \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03bf\u03b3\u03af\u03bf\u03c5 Google \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + }, + "removed_track_new_yaml": { + "description": "\u0388\u03c7\u03b5\u03c4\u03b5 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03b3\u03b9\u03b1 \u03c4\u03bf \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03cc\u03b3\u03b9\u03bf Google \u03c3\u03c4\u03bf configuration.yaml, \u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd. \u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03c4\u03b5 \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf \u03c4\u03b9\u03c2 \u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c0\u03bf\u03c5 \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03b1. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 track_new \u03b1\u03c0\u03cc \u03c4\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7 \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03c4\u03bf\u03c5 \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03bf\u03b3\u03af\u03bf\u03c5 Google \u03ad\u03c7\u03b5\u03b9 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03b9" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/en.json b/homeassistant/components/google/translations/en.json index bd1769fcbe0..4ce207ccd5b 100644 --- a/homeassistant/components/google/translations/en.json +++ b/homeassistant/components/google/translations/en.json @@ -39,8 +39,8 @@ "title": "The Google Calendar YAML configuration is being removed" }, "removed_track_new_yaml": { - "description": "Your Google Calendar configuration.yaml has disabled new entity tracking, which is no longer supported. You must manually set the integration System Options in the UI to disable newly discovered entities going forward. Remove the track_new setting from configuration.yaml and restart Home Assistant to fix this issue.", - "title": "The Google Calendar entity tracking has changed" + "description": "You have disabled entity tracking for Google Calendar in configuration.yaml, which is no longer supported. You must manually change the integration System Options in the UI to disable newly discovered entities going forward. Remove the track_new setting from configuration.yaml and restart Home Assistant to fix this issue.", + "title": "Google Calendar entity tracking has changed" } }, "options": { diff --git a/homeassistant/components/google/translations/it.json b/homeassistant/components/google/translations/it.json index 782fed55d5b..c29eb8d1a2c 100644 --- a/homeassistant/components/google/translations/it.json +++ b/homeassistant/components/google/translations/it.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Google Calendar in configuration.yaml verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Google Calendar \u00e8 stata rimossa" + }, + "removed_track_new_yaml": { + "description": "Hai disabilitato il tracciamento delle entit\u00e0 per Google Calendar in configuration.yaml, il che non \u00e8 pi\u00f9 supportato. \u00c8 necessario modificare manualmente le opzioni di sistema dell'integrazione nell'interfaccia utente per disabilitare le entit\u00e0 appena rilevate da adesso in poi. Rimuovi l'impostazione track_new da configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "Il tracciamento dell'entit\u00e0 di Google Calendar \u00e8 cambiato" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/pl.json b/homeassistant/components/google/translations/pl.json index ff7b8af3bfc..fb85af430df 100644 --- a/homeassistant/components/google/translations/pl.json +++ b/homeassistant/components/google/translations/pl.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Kalendarza Google w configuration.yaml zostanie usuni\u0119ta w Home Assistant 2022.9. \n\nTwoje istniej\u0105ce po\u015bwiadczenia aplikacji OAuth i ustawienia dost\u0119pu zosta\u0142y automatycznie zaimportowane do interfejsu u\u017cytkownika. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Kalendarza Google zostanie usuni\u0119ta" + }, + "removed_track_new_yaml": { + "description": "Wy\u0142\u0105czy\u0142e\u015b \u015bledzenie encji w Kalendarzu Google w pliku configuration.yaml, kt\u00f3ry nie jest ju\u017c obs\u0142ugiwany. Musisz r\u0119cznie zmieni\u0107 ustawienie w Opcjach Systemu integracji, aby wy\u0142\u0105czy\u0107 nowo wykryte encje w przysz\u0142o\u015bci. Usu\u0144 ustawienie track_new z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "\u015aledzenie encji Kalendarza Google uleg\u0142o zmianie" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/pt-BR.json b/homeassistant/components/google/translations/pt-BR.json index 83d4bb54afa..0b115c46423 100644 --- a/homeassistant/components/google/translations/pt-BR.json +++ b/homeassistant/components/google/translations/pt-BR.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Google Agenda em configuration.yaml est\u00e1 sendo removida no Home Assistant 2022.9. \n\n Suas credenciais de aplicativo OAuth e configura\u00e7\u00f5es de acesso existentes foram importadas para a interface do usu\u00e1rio automaticamente. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Google Agenda est\u00e1 sendo removida" + }, + "removed_track_new_yaml": { + "description": "Voc\u00ea desativou as entidades de rastreamento para o Google Agenda em configuration.yaml, que n\u00e3o \u00e9 mais compat\u00edvel. Voc\u00ea deve alterar manualmente as op\u00e7\u00f5es do sistema de integra\u00e7\u00e3o na interface do usu\u00e1rio para desativar as entidades rec\u00e9m-descobertas daqui para frente. Remova a configura\u00e7\u00e3o track_new de configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A entidade de rastreamento do Google Agenda foi alterado" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/ru.json b/homeassistant/components/google/translations/ru.json index 5fc2cb03feb..57a6791cedf 100644 --- a/homeassistant/components/google/translations/ru.json +++ b/homeassistant/components/google/translations/ru.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Google Calendar \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u044b. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Google Calendar \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + }, + "removed_track_new_yaml": { + "description": "\u0412\u044b \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u043b\u0438 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0434\u043b\u044f Google Calendar \u0432 \u0444\u0430\u0439\u043b\u0435 configuration.yaml, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u0427\u0442\u043e\u0431\u044b \u043d\u043e\u0432\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u043d\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u043b\u0438\u0441\u044c \u0432 Home Assistant \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438, \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u043c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 track_new \u0438\u0437 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0418\u0437\u043c\u0435\u043d\u0435\u043d \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u044f \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 Google Calendar" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/zh-Hant.json b/homeassistant/components/google/translations/zh-Hant.json index 45dc83c1b3c..43c208d69e8 100644 --- a/homeassistant/components/google/translations/zh-Hant.json +++ b/homeassistant/components/google/translations/zh-Hant.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Google \u65e5\u66c6\u5df2\u7d93\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 OAuth \u61c9\u7528\u6191\u8b49\u8207\u5b58\u53d6\u6b0a\u9650\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Google \u65e5\u66c6 YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" + }, + "removed_track_new_yaml": { + "description": "\u65bc configuration.yaml \u5167\u6240\u8a2d\u5b9a\u7684 Google \u65e5\u66c6\u5be6\u9ad4\u8ffd\u8e64\u529f\u80fd\uff0c\u7531\u65bc\u4e0d\u518d\u652f\u6301\u3001\u5df2\u7d93\u906d\u5230\u95dc\u9589\u3002\u4e4b\u5f8c\u5fc5\u9808\u624b\u52d5\u900f\u904e\u4ecb\u9762\u5167\u7684\u6574\u5408\u529f\u80fd\u3001\u4ee5\u95dc\u9589\u4efb\u4f55\u65b0\u767c\u73fe\u7684\u5be6\u9ad4\u3002\u8acb\u7531 configuration.yaml \u4e2d\u79fb\u9664R track_new \u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Google \u65e5\u66c6\u5be6\u9ad4\u8ffd\u8e64\u5df2\u7d93\u8b8a\u66f4" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/govee_ble/translations/pl.json b/homeassistant/components/govee_ble/translations/pl.json new file mode 100644 index 00000000000..51168716783 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/ru.json b/homeassistant/components/govee_ble/translations/ru.json new file mode 100644 index 00000000000..c912fc120e4 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/ru.json b/homeassistant/components/homekit_controller/translations/ru.json index d4ab6771ee5..00a4d9fcb7c 100644 --- a/homeassistant/components/homekit_controller/translations/ru.json +++ b/homeassistant/components/homekit_controller/translations/ru.json @@ -18,7 +18,7 @@ "unable_to_pair": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", "unknown_error": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u043e\u043e\u0431\u0449\u0438\u043b\u043e \u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435. \u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "\u041e\u0442\u043c\u0435\u043d\u0438\u0442\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u0432\u0441\u0435\u0445 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430\u0445 \u0438\u043b\u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0437\u0430\u0442\u0435\u043c \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u043c\u0438 \u043a\u043e\u0434\u0430\u043c\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", "pairing_code": "\u041a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f" }, - "description": "HomeKit Controller \u043e\u0431\u043c\u0435\u043d\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u0441 {name} \u043f\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0431\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 HomeKit \u0438\u043b\u0438 iCloud. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 XXX-XX-XXX), \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440. \u042d\u0442\u043e\u0442 \u043a\u043e\u0434 \u043e\u0431\u044b\u0447\u043d\u043e \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u043d\u0430 \u0441\u0430\u043c\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u0438\u043b\u0438 \u043d\u0430 \u0443\u043f\u0430\u043a\u043e\u0432\u043a\u0435.", + "description": "HomeKit Controller \u043e\u0431\u043c\u0435\u043d\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u0441 {name} ({category}) \u043f\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0431\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 HomeKit \u0438\u043b\u0438 iCloud. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 XXX-XX-XXX), \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440. \u042d\u0442\u043e\u0442 \u043a\u043e\u0434 \u043e\u0431\u044b\u0447\u043d\u043e \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u043d\u0430 \u0441\u0430\u043c\u043e\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u0438\u043b\u0438 \u043d\u0430 \u0443\u043f\u0430\u043a\u043e\u0432\u043a\u0435.", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u0432 HomeKit" }, "protocol_error": { diff --git a/homeassistant/components/knx/translations/ru.json b/homeassistant/components/knx/translations/ru.json index 1ca87d9ac80..d5c5bef24fa 100644 --- a/homeassistant/components/knx/translations/ru.json +++ b/homeassistant/components/knx/translations/ru.json @@ -102,7 +102,7 @@ "multicast_group": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f. \u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: `224.0.23.12`", "multicast_port": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f. \u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: `3671`", "rate_limit": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0442\u0435\u043b\u0435\u0433\u0440\u0430\u043c\u043c \u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0443.\n\u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f: \u043e\u0442 20 \u0434\u043e 40", - "state_updater": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u043e \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0438\u043b\u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0447\u0442\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0438\u0437 \u0448\u0438\u043d\u044b KNX. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d, Home Assistant \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0430\u043a\u0442\u0438\u0432\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0438\u0437 \u0448\u0438\u043d\u044b KNX, \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u043e\u0431\u044a\u0435\u043a\u0442\u0430 sync_state \u043d\u0435 \u0431\u0443\u0434\u0443\u0442 \u0438\u043c\u0435\u0442\u044c \u043d\u0438\u043a\u0430\u043a\u043e\u0433\u043e \u044d\u0444\u0444\u0435\u043a\u0442\u0430." + "state_updater": "\u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0434\u043b\u044f \u0447\u0442\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 \u0438\u0437 \u0448\u0438\u043d\u044b KNX. \u0415\u0441\u043b\u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e, Home Assistant \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0430\u043a\u0442\u0438\u0432\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0441 \u0448\u0438\u043d\u044b KNX. \u041c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043e\u0442\u043c\u0435\u043d\u0435\u043d \u043e\u043f\u0446\u0438\u044f\u043c\u0438 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 `sync_state`." } }, "tunnel": { diff --git a/homeassistant/components/life360/translations/ru.json b/homeassistant/components/life360/translations/ru.json index 1f1f92977ed..c0cd51a72d4 100644 --- a/homeassistant/components/life360/translations/ru.json +++ b/homeassistant/components/life360/translations/ru.json @@ -29,7 +29,7 @@ "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u044d\u0442\u043e \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", - "title": "Life360" + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Life360" } } }, diff --git a/homeassistant/components/lyric/translations/el.json b/homeassistant/components/lyric/translations/el.json index e9321950874..a16d2844dcb 100644 --- a/homeassistant/components/lyric/translations/el.json +++ b/homeassistant/components/lyric/translations/el.json @@ -17,5 +17,11 @@ "title": "\u0395\u03c0\u03b1\u03bd\u03b1\u03bb\u03b7\u03c0\u03c4\u03b9\u03ba\u03cc\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" } } + }, + "issues": { + "removed_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Honeywell Lyric \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03bf\u03c5 YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03b7\u03c2 Honeywell Lyric \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/it.json b/homeassistant/components/lyric/translations/it.json index 6fb3fb44275..e0e97ef3246 100644 --- a/homeassistant/components/lyric/translations/it.json +++ b/homeassistant/components/lyric/translations/it.json @@ -17,5 +17,11 @@ "title": "Autentica nuovamente l'integrazione" } } + }, + "issues": { + "removed_yaml": { + "description": "La configurazione di Honeywell Lyric tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Honeywell Lyric \u00e8 stata rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/pl.json b/homeassistant/components/lyric/translations/pl.json index 09ae3ba273a..bddf67f62e8 100644 --- a/homeassistant/components/lyric/translations/pl.json +++ b/homeassistant/components/lyric/translations/pl.json @@ -17,5 +17,11 @@ "title": "Ponownie uwierzytelnij integracj\u0119" } } + }, + "issues": { + "removed_yaml": { + "description": "Konfiguracja Honeywell Lyric za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Honeywell Lyric zosta\u0142a usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/de.json b/homeassistant/components/miflora/translations/de.json new file mode 100644 index 00000000000..8120ddcd25a --- /dev/null +++ b/homeassistant/components/miflora/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Die Mi Flora-Integration funktioniert in Home Assistant 2022.7 nicht mehr und wurde in der Version 2022.8 durch die Xiaomi BLE-Integration ersetzt.\n\nEs ist kein Migrationspfad m\u00f6glich, daher musst du dein Mi Flora-Ger\u00e4t mit der neuen Integration manuell hinzuf\u00fcgen.\n\nDeine bestehende Mi Flora YAML-Konfiguration wird vom Home Assistant nicht mehr verwendet. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Mi Flora-Integration wurde ersetzt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/el.json b/homeassistant/components/miflora/translations/el.json new file mode 100644 index 00000000000..bf4076979d1 --- /dev/null +++ b/homeassistant/components/miflora/translations/el.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Mi Flora \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03c3\u03c4\u03bf Home Assistant 2022.7 \u03ba\u03b1\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Xiaomi BLE \u03c3\u03c4\u03b7\u03bd \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 2022.8. \n\n \u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03bc\u03b5\u03c4\u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2, \u03b5\u03c0\u03bf\u03bc\u03ad\u03bd\u03c9\u03c2, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2 Mi Flora \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b7 \u03bd\u03ad\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Mi Flora YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Mi Flora \u03ad\u03c7\u03b5\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b1\u03b8\u03b5\u03af" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/it.json b/homeassistant/components/miflora/translations/it.json new file mode 100644 index 00000000000..cdd2d89ca72 --- /dev/null +++ b/homeassistant/components/miflora/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "L'integrazione Mi Flora ha smesso di funzionare in Home Assistant 2022.7 e sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Mi Flora utilizzando la nuova integrazione. \n\nLa configurazione YAML di Mi Flora esistente non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "L'integrazione Mi Flora \u00e8 stata sostituita" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/pl.json b/homeassistant/components/miflora/translations/pl.json new file mode 100644 index 00000000000..d8b80bdd26d --- /dev/null +++ b/homeassistant/components/miflora/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Integracja Mi Flora przesta\u0142a dzia\u0142a\u0107 w Home Assistant 2022.7 i zosta\u0142a zast\u0105piona integracj\u0105 Xiaomi BLE w wydaniu 2022.8. \n\nNie ma mo\u017cliwo\u015bci migracji, dlatego musisz r\u0119cznie doda\u0107 urz\u0105dzenie Mi Flora za pomoc\u0105 nowej integracji. \n\nTwoja istniej\u0105ca konfiguracja YAML dla Mi Flora nie jest ju\u017c u\u017cywana przez Home Assistanta. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja Mi Flora zosta\u0142a zast\u0105piona" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/pt-BR.json b/homeassistant/components/miflora/translations/pt-BR.json new file mode 100644 index 00000000000..3b8b4658040 --- /dev/null +++ b/homeassistant/components/miflora/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "A integra\u00e7\u00e3o do Mi Flora parou de funcionar no Home Assistant 2022.7 e foi substitu\u00edda pela integra\u00e7\u00e3o do Xiaomi BLE na vers\u00e3o 2022.8. \n\n N\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o poss\u00edvel, portanto, voc\u00ea deve adicionar seu dispositivo Mi Flora usando a nova integra\u00e7\u00e3o manualmente. \n\n Sua configura\u00e7\u00e3o existente do Mi Flora YAML n\u00e3o \u00e9 mais usada pelo Home Assistant. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o do Mi Flora foi substitu\u00edda" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/zh-Hant.json b/homeassistant/components/miflora/translations/zh-Hant.json new file mode 100644 index 00000000000..e6af26efcb9 --- /dev/null +++ b/homeassistant/components/miflora/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "\u5c0f\u7c73\u82b1\u82b1\u8349\u8349\u6574\u5408\u5df2\u7d93\u65bc Home Assistant 2022.7 \u4e2d\u505c\u6b62\u904b\u4f5c\u3001\u4e26\u65bc 2022.8 \u7248\u4e2d\u4ee5\u5c0f\u7c73\u85cd\u82bd\u6574\u5408\u9032\u884c\u53d6\u4ee3\u3002\n\n\u7531\u65bc\u6c92\u6709\u81ea\u52d5\u8f49\u79fb\u7684\u65b9\u5f0f\uff0c\u56e0\u6b64\u60a8\u5fc5\u9808\u624b\u52d5\u65bc\u6574\u5408\u4e2d\u65b0\u589e\u5c0f\u7c73\u82b1\u82b1\u8349\u8349\u88dd\u7f6e\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u65e2\u6709\u7684\u5c0f\u7c73\u82b1\u82b1\u8349\u8349 YAML \u8a2d\u5b9a\uff0c\u8acb\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "\u5c0f\u7c73\u82b1\u82b1\u8349\u8349\u6574\u5408\u5df2\u88ab\u53d6\u4ee3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/de.json b/homeassistant/components/mitemp_bt/translations/de.json new file mode 100644 index 00000000000..7d4887ab638 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Die Integration des Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensors funktioniert in Home Assistant 2022.7 nicht mehr und wurde in der Version 2022.8 durch die Xiaomi BLE Integration ersetzt.\n\nEs ist kein Migrationspfad m\u00f6glich, daher musst du dein Xiaomi Mijia BLE-Ger\u00e4t manuell mit der neuen Integration hinzuf\u00fcgen.\n\nDeine bestehende Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensor YAML-Konfiguration wird von Home Assistant nicht mehr verwendet. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Integration des Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensors wurde ersetzt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/el.json b/homeassistant/components/mitemp_bt/translations/el.json new file mode 100644 index 00000000000..f8ac6849c10 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/el.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03ba\u03b1\u03b9 \u03c5\u03b3\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 Xiaomi Mijia BLE \u03c3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03c3\u03c4\u03bf Home Assistant 2022.7 \u03ba\u03b1\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03b8\u03b7\u03ba\u03b5 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Xiaomi BLE \u03c3\u03c4\u03b7\u03bd \u03ba\u03c5\u03ba\u03bb\u03bf\u03c6\u03bf\u03c1\u03af\u03b1 \u03c4\u03bf\u03c5 2022.8. \n\n \u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03bc\u03b5\u03c4\u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2, \u03b5\u03c0\u03bf\u03bc\u03ad\u03bd\u03c9\u03c2, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Xiaomi Mijia BLE \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b7 \u03bd\u03ad\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03ba\u03b1\u03b9 \u03c5\u03b3\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 Xiaomi Mijia BLE \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03ba\u03b1\u03b9 \u03c5\u03b3\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 Xiaomi Mijia BLE \u03ad\u03c7\u03b5\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b1\u03b8\u03b5\u03af" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/pl.json b/homeassistant/components/mitemp_bt/translations/pl.json new file mode 100644 index 00000000000..08770ff011b --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Integracja sensora temperatury i wilgotno\u015bci Xiaomi Mijia BLE przesta\u0142a dzia\u0142a\u0107 w Home Assistant 2022.7 i zosta\u0142a zast\u0105piona integracj\u0105 Xiaomi BLE w wersji 2022.8. \n\nNie ma mo\u017cliwo\u015bci migracji, dlatego musisz r\u0119cznie doda\u0107 swoje urz\u0105dzenie Xiaomi Mijia BLE przy u\u017cyciu nowej integracji. \n\nTwoja istniej\u0105ca konfiguracja YAML dla sensora temperatury i wilgotno\u015bci Xiaomi Mijia BLE nie jest ju\u017c u\u017cywana przez Home Assistanta. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja sensora temperatury i wilgotno\u015bci Xiaomi Mijia BLE zosta\u0142a zast\u0105piona" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/pt-BR.json b/homeassistant/components/mitemp_bt/translations/pt-BR.json new file mode 100644 index 00000000000..991a749a729 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "A integra\u00e7\u00e3o do sensor de temperatura e umidade Xiaomi Mijia BLE parou de funcionar no Home Assistant 2022.7 e foi substitu\u00edda pela integra\u00e7\u00e3o Xiaomi BLE na vers\u00e3o 2022.8. \n\n N\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o poss\u00edvel, portanto, voc\u00ea deve adicionar seu dispositivo Xiaomi Mijia BLE usando a nova integra\u00e7\u00e3o manualmente. \n\n Sua configura\u00e7\u00e3o YAML existente do sensor de temperatura e umidade Xiaomi Mijia BLE n\u00e3o \u00e9 mais usada pelo Home Assistant. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o do sensor de temperatura e umidade Xiaomi Mijia BLE foi substitu\u00edda" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/zh-Hant.json b/homeassistant/components/mitemp_bt/translations/zh-Hant.json new file mode 100644 index 00000000000..05799bbd712 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668\u6574\u5408\u5df2\u7d93\u65bc Home Assistant 2022.7 \u4e2d\u505c\u6b62\u904b\u4f5c\u3001\u4e26\u65bc 2022.8 \u7248\u4e2d\u4ee5\u5c0f\u7c73\u85cd\u82bd\u6574\u5408\u9032\u884c\u53d6\u4ee3\u3002\n\n\u7531\u65bc\u6c92\u6709\u81ea\u52d5\u8f49\u79fb\u7684\u65b9\u5f0f\uff0c\u56e0\u6b64\u60a8\u5fc5\u9808\u624b\u52d5\u65bc\u6574\u5408\u4e2d\u65b0\u589e\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u88dd\u7f6e\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u65e2\u6709\u7684\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668 YAML \u8a2d\u5b9a\uff0c\u8acb\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668\u6574\u5408\u5df2\u88ab\u53d6\u4ee3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/pl.json b/homeassistant/components/moat/translations/pl.json new file mode 100644 index 00000000000..51168716783 --- /dev/null +++ b/homeassistant/components/moat/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/ru.json b/homeassistant/components/moat/translations/ru.json new file mode 100644 index 00000000000..c912fc120e4 --- /dev/null +++ b/homeassistant/components/moat/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index 0cb2c91d819..e071637713f 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -23,7 +23,7 @@ "subscriber_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0433\u043e \u043f\u043e\u0434\u043f\u0438\u0441\u0447\u0438\u043a\u0430. \u0411\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0432 \u0436\u0443\u0440\u043d\u0430\u043b\u0430\u0445.", "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.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", - "wrong_project_id": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 Cloud Project ID (\u043d\u0430\u0439\u0434\u0435\u043d Device Access Project ID)" + "wrong_project_id": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 Cloud Project ID (\u0442\u0430\u043a\u043e\u0439 \u0436\u0435 \u043a\u0430\u043a \u0438 Device Access Project ID)" }, "step": { "auth": { @@ -44,7 +44,19 @@ "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 Cloud Project ID, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 *example-project-12345*. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 [Google Cloud Console({cloud_console_url}) \u0438\u043b\u0438 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044e]({more_info_url}).", "title": "Nest: \u0432\u0432\u0435\u0434\u0438\u0442\u0435 Cloud Project ID" }, + "create_cloud_project": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Nest \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u044b, \u043a\u0430\u043c\u0435\u0440\u044b \u0438 \u0434\u0432\u0435\u0440\u043d\u044b\u0435 \u0437\u0432\u043e\u043d\u043a\u0438 Nest \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e API Smart Device Management. SDM API **\u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0435\u0434\u0438\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e\u0439 \u043f\u043b\u0430\u0442\u044b \u0437\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0432 \u0440\u0430\u0437\u043c\u0435\u0440\u0435 US $5**. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \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]({more_info_url}).\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [Google Cloud Console]({cloud_console_url}).\n2. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e \u0412\u0430\u0448 \u043f\u0435\u0440\u0432\u044b\u0439 \u043f\u0440\u043e\u0435\u043a\u0442, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Create Project**, \u0437\u0430\u0442\u0435\u043c **New Project**.\n3. \u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Create**.\n4. \u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u0435 Cloud Project ID, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, *example-project-12345*, \u043e\u043d \u043f\u043e\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0441\u044f \u0412\u0430\u043c \u043f\u043e\u0437\u0436\u0435.\n5. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443 API \u0434\u043b\u044f [Smart Device Management API]({sdm_api_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Enable**.\n6. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443 API \u0434\u043b\u044f [Cloud Pub/Sub API]({pubsub_api_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Enable**.\n\n\u041f\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044e \u0432\u044b\u0448\u0435\u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0445 \u0448\u0430\u0433\u043e\u0432 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443.", + "title": "Nest: \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043e\u0431\u043b\u0430\u0447\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0430" + }, + "device_project": { + "data": { + "project_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "description": "\u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u043f\u0440\u043e\u0435\u043a\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443, \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e **\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043b\u0430\u0442\u0430 \u0432 \u0440\u0430\u0437\u043c\u0435\u0440\u0435 5 \u0434\u043e\u043b\u043b\u0430\u0440\u043e\u0432 \u0421\u0428\u0410**.\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [\u041a\u043e\u043d\u0441\u043e\u043b\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c]({device_access_console_url}) \u0438 \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u043e\u043f\u043b\u0430\u0442\u044b.\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 **Create project**.\n3. \u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Next**.\n4. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth.\n5. \u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0439\u0442\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f, \u043d\u0430\u0436\u0430\u0432 **Enable** \u0438 **Create project**. \n\n\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c \u043d\u0438\u0436\u0435 ([\u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435]({more_info_url})).", + "title": "Nest: \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, "device_project_upgrade": { + "description": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u043f\u0440\u043e\u0435\u043a\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443, \u0443\u043a\u0430\u0437\u0430\u0432 \u043d\u043e\u0432\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth ([\u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435]({more_info_url})).\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [\u041a\u043e\u043d\u0441\u043e\u043b\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c]({device_access_console_url}).\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a \u043a\u043e\u0440\u0437\u0438\u043d\u044b \u0440\u044f\u0434\u043e\u043c \u0441 *OAuth Client ID*.\n3. \u041e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043c\u0435\u043d\u044e, \u043d\u0430\u0436\u0430\u0432 \u043d\u0430 \u043a\u043d\u043e\u043f\u043a\u0443 `...`, \u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 *Add Client ID*.\n4. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043d\u043e\u0432\u044b\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Add**. \n\n\u0412\u0430\u0448 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth: `{client_id}`", "title": "Nest: \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c" }, "init": { diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json index 92d78e7f4ad..a6a31b7b63a 100644 --- a/homeassistant/components/plugwise/translations/ru.json +++ b/homeassistant/components/plugwise/translations/ru.json @@ -20,8 +20,8 @@ "port": "\u041f\u043e\u0440\u0442", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f Smile" }, - "description": "\u041f\u0440\u043e\u0434\u0443\u043a\u0442:", - "title": "\u0422\u0438\u043f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Plugwise" + "description": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Smile" }, "user_gateway": { "data": { diff --git a/homeassistant/components/radiotherm/translations/el.json b/homeassistant/components/radiotherm/translations/el.json index 9b276da3670..8e60a12d1e8 100644 --- a/homeassistant/components/radiotherm/translations/el.json +++ b/homeassistant/components/radiotherm/translations/el.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03ba\u03bb\u03b9\u03bc\u03b1\u03c4\u03b9\u03ba\u03ae\u03c2 \u03c0\u03bb\u03b1\u03c4\u03c6\u03cc\u03c1\u03bc\u03b1\u03c2 \u03c1\u03b1\u03b4\u03b9\u03bf\u03b8\u03b5\u03c1\u03bc\u03bf\u03c3\u03c4\u03ac\u03c4\u03b7 \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant 2022.9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c1\u03b1\u03b4\u03b9\u03bf\u03b8\u03b5\u03c1\u03bc\u03bf\u03c3\u03c4\u03ac\u03c4\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/it.json b/homeassistant/components/radiotherm/translations/it.json index 653dd56321b..9e35beb648a 100644 --- a/homeassistant/components/radiotherm/translations/it.json +++ b/homeassistant/components/radiotherm/translations/it.json @@ -19,6 +19,11 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione della piattaforma climatica Radio Thermostat tramite YAML \u00e8 stata rimossa in Home Assistant 2022.9. \n\nLa configurazione esistente \u00e8 stata importata automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema." + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/pl.json b/homeassistant/components/radiotherm/translations/pl.json index e69568131d6..4265577b46e 100644 --- a/homeassistant/components/radiotherm/translations/pl.json +++ b/homeassistant/components/radiotherm/translations/pl.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja platformy klimatycznej Radio Thermostat za pomoc\u0105 YAML zostanie usuni\u0119ta w Home Assistant 2022.9. \n\nTwoja istniej\u0105ca konfiguracja zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Radio Thermostat zostanie usuni\u0119ta" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/ru.json b/homeassistant/components/radiotherm/translations/ru.json index daac6c7c5ae..6d4615f9820 100644 --- a/homeassistant/components/radiotherm/translations/ru.json +++ b/homeassistant/components/radiotherm/translations/ru.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Radio Thermostat \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Radio Thermostat \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/senz/translations/el.json b/homeassistant/components/senz/translations/el.json index cd34896da39..b6ae9a85e99 100644 --- a/homeassistant/components/senz/translations/el.json +++ b/homeassistant/components/senz/translations/el.json @@ -16,5 +16,11 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" } } + }, + "issues": { + "removed_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 nVent RAYCHEM SENZ \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd YAML \u03c4\u03bf\u03c5 nVent RAYCHEM SENZ \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/it.json b/homeassistant/components/senz/translations/it.json index c92ebb2a57c..be6608af499 100644 --- a/homeassistant/components/senz/translations/it.json +++ b/homeassistant/components/senz/translations/it.json @@ -16,5 +16,11 @@ "title": "Scegli il metodo di autenticazione" } } + }, + "issues": { + "removed_yaml": { + "description": "La configurazione di nVent RAYCHEM SENZ tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di nVent RAYCHEM SENZ \u00e8 stata rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/pl.json b/homeassistant/components/senz/translations/pl.json index d58148cb8fa..79fd5e3a29c 100644 --- a/homeassistant/components/senz/translations/pl.json +++ b/homeassistant/components/senz/translations/pl.json @@ -16,5 +16,11 @@ "title": "Wybierz metod\u0119 uwierzytelniania" } } + }, + "issues": { + "removed_yaml": { + "description": "Konfiguracja nVent RAYCHEM SENZ za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla nVent RAYCHEM SENZ zosta\u0142a usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/el.json b/homeassistant/components/simplisafe/translations/el.json index d852664ce38..ecc263e6821 100644 --- a/homeassistant/components/simplisafe/translations/el.json +++ b/homeassistant/components/simplisafe/translations/el.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "\u0391\u03c5\u03c4\u03cc\u03c2 \u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 SimpliSafe \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7.", "email_2fa_timed_out": "\u03a4\u03bf \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03ad\u03bb\u03b7\u03be\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae \u03b3\u03b9\u03b1 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd \u03c0\u03bf\u03c5 \u03b2\u03b1\u03c3\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 email.", - "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "wrong_account": "\u03a4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03bf\u03c5\u03bd \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc\u03bd \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc SimpliSafe." }, "error": { + "identifier_exists": "\u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ae\u03b4\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, @@ -28,6 +30,7 @@ }, "user": { "data": { + "auth_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "username": "Email" }, diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json index 22c4739b7dd..04f6e8dd44b 100644 --- a/homeassistant/components/simplisafe/translations/pl.json +++ b/homeassistant/components/simplisafe/translations/pl.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "To konto SimpliSafe jest ju\u017c w u\u017cyciu", "email_2fa_timed_out": "Przekroczono limit czasu oczekiwania na e-mailowe uwierzytelnianie dwusk\u0142adnikowe.", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "wrong_account": "Podane dane uwierzytelniaj\u0105ce u\u017cytkownika nie pasuj\u0105 do tego konta SimpliSafe." }, "error": { + "identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane", "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "Kod autoryzacji", "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" }, - "description": "Wprowad\u017a swoj\u0105 nazw\u0119 u\u017cytkownika i has\u0142o." + "description": "SimpliSafe uwierzytelnia u\u017cytkownik\u00f3w za po\u015brednictwem swojej aplikacji internetowej. Ze wzgl\u0119du na ograniczenia techniczne na ko\u0144cu tego procesu znajduje si\u0119 r\u0119czny krok; upewnij si\u0119, \u017ce przeczyta\u0142e\u015b [dokumentacj\u0119](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) przed rozpocz\u0119ciem. \n\nGdy b\u0119dziesz ju\u017c gotowy, kliknij [tutaj]({url}), aby otworzy\u0107 aplikacj\u0119 internetow\u0105 SimpliSafe i wprowadzi\u0107 swoje dane uwierzytelniaj\u0105ce. Po zako\u0144czeniu procesu wr\u00f3\u0107 tutaj i wprowad\u017a kod autoryzacji z adresu URL aplikacji internetowej SimpliSafe." } } }, diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index 198d5225983..1b88417dd27 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -3,9 +3,11 @@ "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.", "email_2fa_timed_out": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043e\u0436\u0438\u0434\u0430\u043d\u0438\u044f \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", - "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\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "\u041a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438", "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." + "description": "SimpliSafe \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u0442 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0447\u0435\u0440\u0435\u0437 \u0441\u0432\u043e\u0435 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. \u0418\u0437-\u0437\u0430 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0439, \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u043e\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u043c \u0432\u0440\u0443\u0447\u043d\u0443\u044e. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) \u043f\u0435\u0440\u0435\u0434 \u0437\u0430\u043f\u0443\u0441\u043a\u043e\u043c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \n\n\u041a\u043e\u0433\u0434\u0430 \u0412\u044b \u0431\u0443\u0434\u0435\u0442\u0435 \u0433\u043e\u0442\u043e\u0432\u044b, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 [\u0441\u044e\u0434\u0430]({url}), \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 SimpliSafe \u0438 \u0432\u0432\u0435\u0441\u0442\u0438 \u0441\u0432\u043e\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435. \u041a\u043e\u0433\u0434\u0430 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d, \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f SimpliSafe." } } }, diff --git a/homeassistant/components/spotify/translations/de.json b/homeassistant/components/spotify/translations/de.json index 799f3717b81..9d48e7b58e2 100644 --- a/homeassistant/components/spotify/translations/de.json +++ b/homeassistant/components/spotify/translations/de.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Die Konfiguration von Spotify \u00fcber YAML wurde entfernt.\n\nDeine bestehende YAML-Konfiguration wird von Home Assistant nicht verwendet.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Spotify-YAML-Konfiguration wurde entfernt" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Spotify-API-Endpunkt erreichbar" diff --git a/homeassistant/components/spotify/translations/el.json b/homeassistant/components/spotify/translations/el.json index 1b9aadb7caf..1c89861b325 100644 --- a/homeassistant/components/spotify/translations/el.json +++ b/homeassistant/components/spotify/translations/el.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Spotify \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Spotify YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + } + }, "system_health": { "info": { "api_endpoint_reachable": "\u03a4\u03bf \u03c4\u03b5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b7\u03bc\u03b5\u03af\u03bf \u03c4\u03bf\u03c5 Spotify API \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03bf" diff --git a/homeassistant/components/spotify/translations/it.json b/homeassistant/components/spotify/translations/it.json index 445b7a233e4..2ca8d323607 100644 --- a/homeassistant/components/spotify/translations/it.json +++ b/homeassistant/components/spotify/translations/it.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "La configurazione di Spotify tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Spotify \u00e8 stata rimossa" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Endpoint API Spotify raggiungibile" diff --git a/homeassistant/components/spotify/translations/pl.json b/homeassistant/components/spotify/translations/pl.json index 52028d4d368..bbdda0ae586 100644 --- a/homeassistant/components/spotify/translations/pl.json +++ b/homeassistant/components/spotify/translations/pl.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Konfiguracja Spotify za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Spotify zosta\u0142a usuni\u0119ta" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Punkt ko\u0144cowy Spotify API osi\u0105galny" diff --git a/homeassistant/components/spotify/translations/pt-BR.json b/homeassistant/components/spotify/translations/pt-BR.json index c49fbfabfa0..97948f71641 100644 --- a/homeassistant/components/spotify/translations/pt-BR.json +++ b/homeassistant/components/spotify/translations/pt-BR.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do Spotify usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o Spotify YAML foi removida" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Endpoint da API do Spotify acess\u00edvel" diff --git a/homeassistant/components/spotify/translations/ru.json b/homeassistant/components/spotify/translations/ru.json index fc9362697e2..35918b2634f 100644 --- a/homeassistant/components/spotify/translations/ru.json +++ b/homeassistant/components/spotify/translations/ru.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Spotify \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Spotify \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" + } + }, "system_health": { "info": { "api_endpoint_reachable": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a API Spotify" diff --git a/homeassistant/components/spotify/translations/zh-Hant.json b/homeassistant/components/spotify/translations/zh-Hant.json index ce35507e661..ce89e224a8c 100644 --- a/homeassistant/components/spotify/translations/zh-Hant.json +++ b/homeassistant/components/spotify/translations/zh-Hant.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Spotify \u7684\u529f\u80fd\u5df2\u906d\u5230\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Spotify YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Spotify API \u53ef\u9054\u7aef\u9ede" diff --git a/homeassistant/components/steam_online/translations/de.json b/homeassistant/components/steam_online/translations/de.json index 44fb7f8b08b..aef08bfe269 100644 --- a/homeassistant/components/steam_online/translations/de.json +++ b/homeassistant/components/steam_online/translations/de.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Die Konfiguration von Steam \u00fcber YAML wurde entfernt.\n\nDeine bestehende YAML-Konfiguration wird von Home Assistant nicht verwendet.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Steam-YAML-Konfiguration wurde entfernt" + } + }, "options": { "error": { "unauthorized": "Freundesliste eingeschr\u00e4nkt: Bitte lies in der Dokumentation nach, wie du alle anderen Freunde sehen kannst" diff --git a/homeassistant/components/steam_online/translations/el.json b/homeassistant/components/steam_online/translations/el.json index 02405dc0215..33a864e0945 100644 --- a/homeassistant/components/steam_online/translations/el.json +++ b/homeassistant/components/steam_online/translations/el.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Steam \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03bf YAML \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd Steam YAML \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" + } + }, "options": { "error": { "unauthorized": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1 \u03c6\u03af\u03bb\u03c9\u03bd: \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03b4\u03b5\u03af\u03c4\u03b5 \u03cc\u03bb\u03bf\u03c5\u03c2 \u03c4\u03bf\u03c5\u03c2 \u03ac\u03bb\u03bb\u03bf\u03c5\u03c2 \u03c6\u03af\u03bb\u03bf\u03c5\u03c2" diff --git a/homeassistant/components/steam_online/translations/it.json b/homeassistant/components/steam_online/translations/it.json index 3f2e9481301..fb962124f2a 100644 --- a/homeassistant/components/steam_online/translations/it.json +++ b/homeassistant/components/steam_online/translations/it.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "La configurazione di Steam tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Steam \u00e8 stata rimossa" + } + }, "options": { "error": { "unauthorized": "Elenco amici limitato: consultare la documentazione su come vedere tutti gli altri amici." diff --git a/homeassistant/components/steam_online/translations/pl.json b/homeassistant/components/steam_online/translations/pl.json index 3ceff9e442f..08c05b7de41 100644 --- a/homeassistant/components/steam_online/translations/pl.json +++ b/homeassistant/components/steam_online/translations/pl.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Konfiguracja Steam za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistant, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Steam zosta\u0142a usuni\u0119ta" + } + }, "options": { "error": { "unauthorized": "Lista znajomych ograniczona: zapoznaj si\u0119 z dokumentacj\u0105, aby dowiedzie\u0107 si\u0119, jak zobaczy\u0107 wszystkich innych znajomych" diff --git a/homeassistant/components/steam_online/translations/pt-BR.json b/homeassistant/components/steam_online/translations/pt-BR.json index 5aa9d0d2520..9afd81f4a5f 100644 --- a/homeassistant/components/steam_online/translations/pt-BR.json +++ b/homeassistant/components/steam_online/translations/pt-BR.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o da Steam usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o da Steam YAML foi removida" + } + }, "options": { "error": { "unauthorized": "Lista restrita de amigos: consulte a documenta\u00e7\u00e3o sobre como ver todos os outros amigos" diff --git a/homeassistant/components/steam_online/translations/ru.json b/homeassistant/components/steam_online/translations/ru.json index b3d84b7f9e6..b69b1a72696 100644 --- a/homeassistant/components/steam_online/translations/ru.json +++ b/homeassistant/components/steam_online/translations/ru.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Steam \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Steam \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" + } + }, "options": { "error": { "unauthorized": "\u0421\u043f\u0438\u0441\u043e\u043a \u0434\u0440\u0443\u0437\u0435\u0439 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d. \u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u043a \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0432\u0441\u0435\u0445 \u0434\u0440\u0443\u0437\u044c\u044f\u0445." diff --git a/homeassistant/components/steam_online/translations/zh-Hant.json b/homeassistant/components/steam_online/translations/zh-Hant.json index 7de1d6f1a3c..e3c725532e9 100644 --- a/homeassistant/components/steam_online/translations/zh-Hant.json +++ b/homeassistant/components/steam_online/translations/zh-Hant.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Steam \u7684\u529f\u80fd\u5df2\u906d\u5230\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Steam YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" + } + }, "options": { "error": { "unauthorized": "\u597d\u53cb\u5217\u8868\u53d7\u9650\uff1a\u8acb\u53c3\u8003\u6587\u4ef6\u8cc7\u6599\u4ee5\u4e86\u89e3\u5982\u4f55\u986f\u793a\u6240\u6709\u597d\u53cb\u3002" diff --git a/homeassistant/components/switchbot/translations/el.json b/homeassistant/components/switchbot/translations/el.json index b191c90f53c..585dd3ccbd4 100644 --- a/homeassistant/components/switchbot/translations/el.json +++ b/homeassistant/components/switchbot/translations/el.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", "mac": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 MAC \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", "name": "\u038c\u03bd\u03bf\u03bc\u03b1", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" diff --git a/homeassistant/components/switchbot/translations/pl.json b/homeassistant/components/switchbot/translations/pl.json index 156ddbb9924..dc43a7f610d 100644 --- a/homeassistant/components/switchbot/translations/pl.json +++ b/homeassistant/components/switchbot/translations/pl.json @@ -13,10 +13,11 @@ "one": "Pusty", "other": "" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Adres urz\u0105dzenia", "mac": "Adres MAC urz\u0105dzenia", "name": "Nazwa", "password": "Has\u0142o" diff --git a/homeassistant/components/switchbot/translations/ru.json b/homeassistant/components/switchbot/translations/ru.json index 9ca076ff499..7b2f4f73cc2 100644 --- a/homeassistant/components/switchbot/translations/ru.json +++ b/homeassistant/components/switchbot/translations/ru.json @@ -7,10 +7,11 @@ "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." }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "\u0410\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", "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" diff --git a/homeassistant/components/xiaomi_ble/translations/el.json b/homeassistant/components/xiaomi_ble/translations/el.json index 0a802a0bc89..30dc61cb5dc 100644 --- a/homeassistant/components/xiaomi_ble/translations/el.json +++ b/homeassistant/components/xiaomi_ble/translations/el.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "decryption_failed": "\u03a4\u03bf \u03c0\u03b1\u03c1\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03cd\u03c1\u03b3\u03b7\u03c3\u03b5, \u03c4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03c3\u03b1\u03bd \u03bd\u03b1 \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03b8\u03bf\u03cd\u03bd. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", + "expected_24_characters": "\u0391\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03c4\u03b1\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 24 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd.", + "expected_32_characters": "\u0391\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03c4\u03b1\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd.", "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "\u03a4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03bc\u03b5\u03c4\u03b1\u03b4\u03af\u03b4\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03bc\u03ad\u03bd\u03b1. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03c4\u03bf \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03ae\u03c3\u03bf\u03c5\u03bc\u03b5 \u03c7\u03c1\u03b5\u03b9\u03b1\u03b6\u03cc\u03bc\u03b1\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "\u03a4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03bc\u03b5\u03c4\u03b1\u03b4\u03af\u03b4\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03bc\u03ad\u03bd\u03b1. \u0393\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03ae \u03c4\u03bf\u03c5\u03c2 \u03c7\u03c1\u03b5\u03b9\u03b1\u03b6\u03cc\u03bc\u03b1\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03ad\u03c3\u03bc\u03b5\u03c5\u03c3\u03b7\u03c2 24 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03bf\u03cd \u03b1\u03c1\u03b9\u03b8\u03bc\u03bf\u03cd." + }, "user": { "data": { "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" diff --git a/homeassistant/components/xiaomi_ble/translations/pl.json b/homeassistant/components/xiaomi_ble/translations/pl.json index 51168716783..2ca956019ef 100644 --- a/homeassistant/components/xiaomi_ble/translations/pl.json +++ b/homeassistant/components/xiaomi_ble/translations/pl.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "decryption_failed": "Podany klucz (bindkey) nie zadzia\u0142a\u0142, dane czujnika nie mog\u0142y zosta\u0107 odszyfrowane. Sprawd\u017a go i spr\u00f3buj ponownie.", + "expected_24_characters": "Oczekiwano 24-znakowego szesnastkowego klucza bindkey.", + "expected_32_characters": "Oczekiwano 32-znakowego szesnastkowego klucza bindkey.", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Dane przesy\u0142ane przez sensor s\u0105 szyfrowane. Aby je odszyfrowa\u0107, potrzebujemy 32-znakowego szesnastkowego klucza bindkey." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Dane przesy\u0142ane przez sensor s\u0105 szyfrowane. Aby je odszyfrowa\u0107, potrzebujemy 24-znakowego szesnastkowego klucza bindkey." + }, "user": { "data": { "address": "Urz\u0105dzenie" diff --git a/homeassistant/components/xiaomi_ble/translations/ru.json b/homeassistant/components/xiaomi_ble/translations/ru.json index c912fc120e4..a90da71d84e 100644 --- a/homeassistant/components/xiaomi_ble/translations/ru.json +++ b/homeassistant/components/xiaomi_ble/translations/ru.json @@ -3,6 +3,9 @@ "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.", + "decryption_failed": "\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438 \u043d\u0435 \u0441\u0440\u0430\u0431\u043e\u0442\u0430\u043b, \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0435\u0433\u043e \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", + "expected_24_characters": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 24-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438.", + "expected_32_characters": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 32-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438" + }, + "description": "\u041f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0435\u043c\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u044b. \u0414\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0445, \u043d\u0443\u0436\u0435\u043d 32-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438" + }, + "description": "\u041f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0435\u043c\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u044b. \u0414\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0445, \u043d\u0443\u0436\u0435\u043d 24-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438." + }, "user": { "data": { "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 30c85de48da..4666ba7e494 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -46,6 +46,7 @@ "title": "Opzioni del pannello di controllo degli allarmi" }, "zha_options": { + "always_prefer_xy_color_mode": "Preferisci sempre la modalit\u00e0 colore XY", "consider_unavailable_battery": "Considera i dispositivi alimentati a batteria non disponibili dopo (secondi)", "consider_unavailable_mains": "Considera i dispositivi alimentati dalla rete non disponibili dopo (secondi)", "default_light_transition": "Tempo di transizione della luce predefinito (secondi)", From 89493f2d7f64110288db4253a2909f5d73c30c32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roy?= Date: Tue, 26 Jul 2022 20:36:41 -0700 Subject: [PATCH 2887/3516] Add occupancy sensor to the BAF integration (#75793) Co-authored-by: J. Nick Koston --- homeassistant/components/baf/__init__.py | 1 + homeassistant/components/baf/binary_sensor.py | 80 +++++++++++++++++++ homeassistant/components/baf/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/baf/binary_sensor.py diff --git a/homeassistant/components/baf/__init__.py b/homeassistant/components/baf/__init__.py index 9bb056ece4f..7e80341deab 100644 --- a/homeassistant/components/baf/__init__.py +++ b/homeassistant/components/baf/__init__.py @@ -15,6 +15,7 @@ from .const import DOMAIN, QUERY_INTERVAL, RUN_TIMEOUT from .models import BAFData PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.FAN, Platform.LIGHT, diff --git a/homeassistant/components/baf/binary_sensor.py b/homeassistant/components/baf/binary_sensor.py new file mode 100644 index 00000000000..8a00a7e8bd5 --- /dev/null +++ b/homeassistant/components/baf/binary_sensor.py @@ -0,0 +1,80 @@ +"""Support for Big Ass Fans binary sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Optional, cast + +from aiobafi6 import Device + +from homeassistant import config_entries +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import BAFEntity +from .models import BAFData + + +@dataclass +class BAFBinarySensorDescriptionMixin: + """Required values for BAF binary sensors.""" + + value_fn: Callable[[Device], bool | None] + + +@dataclass +class BAFBinarySensorDescription( + BinarySensorEntityDescription, + BAFBinarySensorDescriptionMixin, +): + """Class describing BAF binary sensor entities.""" + + +OCCUPANCY_SENSORS = ( + BAFBinarySensorDescription( + key="occupancy", + name="Occupancy", + device_class=BinarySensorDeviceClass.OCCUPANCY, + value_fn=lambda device: cast(Optional[bool], device.fan_occupancy_detected), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up BAF binary sensors.""" + data: BAFData = hass.data[DOMAIN][entry.entry_id] + device = data.device + sensors_descriptions: list[BAFBinarySensorDescription] = [] + if device.has_occupancy: + sensors_descriptions.extend(OCCUPANCY_SENSORS) + async_add_entities( + BAFBinarySensor(device, description) for description in sensors_descriptions + ) + + +class BAFBinarySensor(BAFEntity, BinarySensorEntity): + """BAF binary sensor.""" + + entity_description: BAFBinarySensorDescription + + def __init__(self, device: Device, description: BAFBinarySensorDescription) -> None: + """Initialize the entity.""" + self.entity_description = description + super().__init__(device, f"{device.name} {description.name}") + self._attr_unique_id = f"{self._device.mac_address}-{description.key}" + + @callback + def _async_update_attrs(self) -> None: + """Update attrs from device.""" + description = self.entity_description + self._attr_is_on = description.value_fn(self._device) diff --git a/homeassistant/components/baf/manifest.json b/homeassistant/components/baf/manifest.json index 821ad1a21cb..15e0272b2b0 100644 --- a/homeassistant/components/baf/manifest.json +++ b/homeassistant/components/baf/manifest.json @@ -3,7 +3,7 @@ "name": "Big Ass Fans", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/baf", - "requirements": ["aiobafi6==0.6.0"], + "requirements": ["aiobafi6==0.7.0"], "codeowners": ["@bdraco", "@jfroy"], "iot_class": "local_push", "zeroconf": [ diff --git a/requirements_all.txt b/requirements_all.txt index 7a8f314df9f..f1e14dc0aeb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -128,7 +128,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.6.0 +aiobafi6==0.7.0 # homeassistant.components.aws aiobotocore==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bc952f553db..d52446c4a26 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -115,7 +115,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.6.0 +aiobafi6==0.7.0 # homeassistant.components.aws aiobotocore==2.1.0 From 5e6217f20ce90f952a647ea9bd446f00026f6168 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Wed, 27 Jul 2022 14:13:38 +0800 Subject: [PATCH 2888/3516] Use executor to finish stream recording (#75776) --- homeassistant/components/stream/recorder.py | 44 +++++++++++++-------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 42b98946c15..3bda8acfa7b 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -4,6 +4,7 @@ from __future__ import annotations from io import BytesIO import logging import os +from typing import TYPE_CHECKING import av @@ -16,6 +17,9 @@ from .const import ( ) from .core import PROVIDERS, IdleTimer, Segment, StreamOutput, StreamSettings +if TYPE_CHECKING: + import deque + _LOGGER = logging.getLogger(__name__) @@ -139,6 +143,25 @@ class RecorderOutput(StreamOutput): source.close() + def finish_writing( + segments: deque[Segment], output: av.OutputContainer, video_path: str + ) -> None: + """Finish writing output.""" + # Should only have 0 or 1 segments, but loop through just in case + while segments: + write_segment(segments.popleft()) + if output is None: + _LOGGER.error("Recording failed to capture anything") + return + output.close() + try: + os.rename(video_path + ".tmp", video_path) + except FileNotFoundError: + _LOGGER.error( + "Error writing to '%s'. There are likely multiple recordings writing to the same file", + video_path, + ) + # Write lookback segments while len(self._segments) > 1: # The last segment is in progress await self._hass.async_add_executor_job( @@ -153,20 +176,7 @@ class RecorderOutput(StreamOutput): await self._hass.async_add_executor_job( write_segment, self._segments.popleft() ) - # Write remaining segments - # Should only have 0 or 1 segments, but loop through just in case - while self._segments: - await self._hass.async_add_executor_job( - write_segment, self._segments.popleft() - ) - if output is None: - _LOGGER.error("Recording failed to capture anything") - else: - output.close() - try: - os.rename(self.video_path + ".tmp", self.video_path) - except FileNotFoundError: - _LOGGER.error( - "Error writing to '%s'. There are likely multiple recordings writing to the same file", - self.video_path, - ) + # Write remaining segments and close output + await self._hass.async_add_executor_job( + finish_writing, self._segments, output, self.video_path + ) From 56871507862def17e3a37a7f8f097a8192303361 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Wed, 27 Jul 2022 10:01:09 +0200 Subject: [PATCH 2889/3516] Add state to CheckControlMessages in bmw_connected_drive (#75802) Co-authored-by: rikroe --- .../components/bmw_connected_drive/binary_sensor.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index a19ccc8f715..94d8902fe23 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -39,14 +39,8 @@ def _condition_based_services( def _check_control_messages(vehicle: MyBMWVehicle) -> dict[str, Any]: extra_attributes: dict[str, Any] = {} - if vehicle.check_control_messages.has_check_control_messages: - cbs_list = [ - message.description_short - for message in vehicle.check_control_messages.messages - ] - extra_attributes["check_control_messages"] = cbs_list - else: - extra_attributes["check_control_messages"] = "OK" + for message in vehicle.check_control_messages.messages: + extra_attributes.update({message.description_short: message.state.value}) return extra_attributes From bbdce93291d50dbe99a5d3af4c1bf39c02623628 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 27 Jul 2022 03:01:44 -0500 Subject: [PATCH 2890/3516] Fix error on shutdown when no Sonos devices available (#75798) --- homeassistant/components/sonos/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 837b6c0f749..d94b49e52f2 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -201,7 +201,11 @@ class SonosDiscoveryManager: speaker.activity_stats.log_report() speaker.event_stats.log_report() if zgs := next( - speaker.soco.zone_group_state for speaker in self.data.discovered.values() + ( + speaker.soco.zone_group_state + for speaker in self.data.discovered.values() + ), + None, ): _LOGGER.debug( "ZoneGroupState stats: (%s/%s) processed", From 51c3836ec2dbf14afa5f158bcc41052b0b41f26a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 27 Jul 2022 10:13:16 +0200 Subject: [PATCH 2891/3516] Create Repairs based on Alerts (#75397) Co-authored-by: Paulus Schoutsen Co-authored-by: Martin Hjelmare --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/default_config/manifest.json | 1 + .../homeassistant_alerts/__init__.py | 185 ++++++++ .../homeassistant_alerts/manifest.json | 8 + .../homeassistant_alerts/strings.json | 8 + mypy.ini | 11 + script/hassfest/manifest.py | 1 + .../homeassistant_alerts/__init__.py | 1 + .../fixtures/alerts_1.json | 174 +++++++ .../fixtures/alerts_2.json | 159 +++++++ .../fixtures/alerts_no_integrations.json | 27 ++ .../fixtures/alerts_no_package.json | 33 ++ .../fixtures/alerts_no_url.json | 34 ++ .../homeassistant_alerts/test_init.py | 433 ++++++++++++++++++ 15 files changed, 1078 insertions(+) create mode 100644 homeassistant/components/homeassistant_alerts/__init__.py create mode 100644 homeassistant/components/homeassistant_alerts/manifest.json create mode 100644 homeassistant/components/homeassistant_alerts/strings.json create mode 100644 tests/components/homeassistant_alerts/__init__.py create mode 100644 tests/components/homeassistant_alerts/fixtures/alerts_1.json create mode 100644 tests/components/homeassistant_alerts/fixtures/alerts_2.json create mode 100644 tests/components/homeassistant_alerts/fixtures/alerts_no_integrations.json create mode 100644 tests/components/homeassistant_alerts/fixtures/alerts_no_package.json create mode 100644 tests/components/homeassistant_alerts/fixtures/alerts_no_url.json create mode 100644 tests/components/homeassistant_alerts/test_init.py diff --git a/.strict-typing b/.strict-typing index 45d11f089dd..c5b4e376414 100644 --- a/.strict-typing +++ b/.strict-typing @@ -115,6 +115,7 @@ homeassistant.components.group.* homeassistant.components.guardian.* homeassistant.components.history.* homeassistant.components.homeassistant.triggers.event +homeassistant.components.homeassistant_alerts.* homeassistant.components.homekit homeassistant.components.homekit.accessories homeassistant.components.homekit.aidmanager diff --git a/CODEOWNERS b/CODEOWNERS index 3ad4c23816c..bd39fd68590 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -455,6 +455,8 @@ build.json @home-assistant/supervisor /tests/components/home_plus_control/ @chemaaa /homeassistant/components/homeassistant/ @home-assistant/core /tests/components/homeassistant/ @home-assistant/core +/homeassistant/components/homeassistant_alerts/ @home-assistant/core +/tests/components/homeassistant_alerts/ @home-assistant/core /homeassistant/components/homeassistant_yellow/ @home-assistant/core /tests/components/homeassistant_yellow/ @home-assistant/core /homeassistant/components/homekit/ @bdraco diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 3cb9e60a278..f790292c27a 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -11,6 +11,7 @@ "dhcp", "energy", "frontend", + "homeassistant_alerts", "history", "input_boolean", "input_button", diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py new file mode 100644 index 00000000000..1aedd6c5419 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -0,0 +1,185 @@ +"""The Home Assistant alerts integration.""" +from __future__ import annotations + +import asyncio +import dataclasses +from datetime import timedelta +import logging + +import aiohttp +from awesomeversion import AwesomeVersion, AwesomeVersionStrategy + +from homeassistant.components.repairs import async_create_issue, async_delete_issue +from homeassistant.components.repairs.models import IssueSeverity +from homeassistant.const import __version__ +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.util.yaml import parse_yaml + +DOMAIN = "homeassistant_alerts" +UPDATE_INTERVAL = timedelta(hours=3) +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up alerts.""" + last_alerts: dict[str, str | None] = {} + + async def async_update_alerts() -> None: + nonlocal last_alerts + + active_alerts: dict[str, str | None] = {} + + for issue_id, alert in coordinator.data.items(): + # Skip creation if already created and not updated since then + if issue_id in last_alerts and alert.date_updated == last_alerts[issue_id]: + active_alerts[issue_id] = alert.date_updated + continue + + # Fetch alert to get title + description + try: + response = await async_get_clientsession(hass).get( + f"https://alerts.home-assistant.io/alerts/{alert.filename}", + timeout=aiohttp.ClientTimeout(total=10), + ) + except asyncio.TimeoutError: + _LOGGER.warning("Error fetching %s: timeout", alert.filename) + continue + + alert_content = await response.text() + alert_parts = alert_content.split("---") + + if len(alert_parts) != 3: + _LOGGER.warning( + "Error parsing %s: unexpected metadata format", alert.filename + ) + continue + + try: + alert_info = parse_yaml(alert_parts[1]) + except ValueError as err: + _LOGGER.warning("Error parsing %s metadata: %s", alert.filename, err) + continue + + if not isinstance(alert_info, dict) or "title" not in alert_info: + _LOGGER.warning("Error in %s metadata: title not found", alert.filename) + continue + + alert_title = alert_info["title"] + alert_content = alert_parts[2].strip() + + async_create_issue( + hass, + DOMAIN, + issue_id, + is_fixable=False, + learn_more_url=alert.alert_url, + severity=IssueSeverity.WARNING, + translation_key="alert", + translation_placeholders={ + "title": alert_title, + "description": alert_content, + }, + ) + active_alerts[issue_id] = alert.date_updated + + inactive_alerts = last_alerts.keys() - active_alerts.keys() + for issue_id in inactive_alerts: + async_delete_issue(hass, DOMAIN, issue_id) + + last_alerts = active_alerts + + @callback + def async_schedule_update_alerts() -> None: + if not coordinator.last_update_success: + return + + hass.async_create_task(async_update_alerts()) + + coordinator = AlertUpdateCoordinator(hass) + coordinator.async_add_listener(async_schedule_update_alerts) + await coordinator.async_refresh() + + return True + + +@dataclasses.dataclass(frozen=True) +class IntegrationAlert: + """Issue Registry Entry.""" + + integration: str + filename: str + date_updated: str | None + alert_url: str | None + + @property + def issue_id(self) -> str: + """Return the issue id.""" + return f"{self.filename}_{self.integration}" + + +class AlertUpdateCoordinator(DataUpdateCoordinator[dict[str, IntegrationAlert]]): + """Data fetcher for HA Alerts.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the data updater.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=UPDATE_INTERVAL, + ) + self.ha_version = AwesomeVersion( + __version__, + ensure_strategy=AwesomeVersionStrategy.CALVER, + find_first_match=False, + ) + + async def _async_update_data(self) -> dict[str, IntegrationAlert]: + response = await async_get_clientsession(self.hass).get( + "https://alerts.home-assistant.io/alerts.json", + timeout=aiohttp.ClientTimeout(total=10), + ) + alerts = await response.json() + + result = {} + + for alert in alerts: + if "alert_url" not in alert or "integrations" not in alert: + continue + + if "homeassistant" in alert: + if "affected_from_version" in alert["homeassistant"]: + affected_from_version = AwesomeVersion( + alert["homeassistant"]["affected_from_version"], + find_first_match=False, + ) + if self.ha_version < affected_from_version: + continue + if "resolved_in_version" in alert["homeassistant"]: + resolved_in_version = AwesomeVersion( + alert["homeassistant"]["resolved_in_version"], + find_first_match=False, + ) + if self.ha_version >= resolved_in_version: + continue + + for integration in alert["integrations"]: + if "package" not in integration: + continue + + if integration["package"] not in self.hass.config.components: + continue + + integration_alert = IntegrationAlert( + integration=integration["package"], + filename=alert["filename"], + date_updated=alert.get("date_updated"), + alert_url=alert["alert_url"], + ) + + result[integration_alert.issue_id] = integration_alert + + return result diff --git a/homeassistant/components/homeassistant_alerts/manifest.json b/homeassistant/components/homeassistant_alerts/manifest.json new file mode 100644 index 00000000000..0d276c6f3ae --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "homeassistant_alerts", + "name": "Home Assistant alerts", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/homeassistant_alerts", + "codeowners": ["@home-assistant/core"], + "dependencies": ["repairs"] +} diff --git a/homeassistant/components/homeassistant_alerts/strings.json b/homeassistant/components/homeassistant_alerts/strings.json new file mode 100644 index 00000000000..7a9634d6268 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "title": "{title}", + "description": "{description}" + } + } +} diff --git a/mypy.ini b/mypy.ini index af6abe6658f..37765023f74 100644 --- a/mypy.ini +++ b/mypy.ini @@ -988,6 +988,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.homeassistant_alerts.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.homekit] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index c6cc019ff31..5ce67b59198 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -56,6 +56,7 @@ NO_IOT_CLASS = [ "hardware", "history", "homeassistant", + "homeassistant_alerts", "homeassistant_yellow", "image", "input_boolean", diff --git a/tests/components/homeassistant_alerts/__init__.py b/tests/components/homeassistant_alerts/__init__.py new file mode 100644 index 00000000000..e8e83fad6bd --- /dev/null +++ b/tests/components/homeassistant_alerts/__init__.py @@ -0,0 +1 @@ +"""Tests for the Home Assistant alerts integration.""" diff --git a/tests/components/homeassistant_alerts/fixtures/alerts_1.json b/tests/components/homeassistant_alerts/fixtures/alerts_1.json new file mode 100644 index 00000000000..381a31d7a5d --- /dev/null +++ b/tests/components/homeassistant_alerts/fixtures/alerts_1.json @@ -0,0 +1,174 @@ +[ + { + "title": "Aladdin Connect is turning off their previous connection method", + "created": "2022-07-14T06:00:00.000Z", + "integrations": [ + { + "package": "aladdin_connect" + } + ], + "homeassistant": { + "package": "homeassistant", + "resolved_in_version": "2022.7" + }, + "filename": "aladdin_connect.markdown", + "alert_url": "https://alerts.home-assistant.io/#aladdin_connect.markdown" + }, + { + "title": "Dark Sky API closed for new users", + "created": "2020-03-31T14:40:00.000Z", + "integrations": [ + { + "package": "darksky" + } + ], + "github_issue": "https://github.com/home-assistant/home-assistant.io/pull/12591", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "dark_sky.markdown", + "alert_url": "https://alerts.home-assistant.io/#dark_sky.markdown" + }, + { + "title": "Hikvision Security Vulnerability", + "created": "2021-09-20T22:08:00.000Z", + "integrations": [ + { + "package": "hikvision" + }, + { + "package": "hikvisioncam" + } + ], + "filename": "hikvision.markdown", + "alert_url": "https://alerts.home-assistant.io/#hikvision.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "Hive shutting down North American Servers", + "created": "2021-11-13T13:58:00.000Z", + "integrations": [ + { + "package": "hive" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "2021.11.0" + }, + "filename": "hive_us.markdown", + "alert_url": "https://alerts.home-assistant.io/#hive_us.markdown" + }, + { + "title": "HomematicIP (EQ-3) blocks public IP addresses, if access to the Cloud is too frequent.", + "created": "2020-12-20T12:00:00.000Z", + "integrations": [ + { + "package": "homematicip_cloud" + } + ], + "packages": [ + { + "package": "homematicip" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.7" + }, + "filename": "homematicip_cloud.markdown", + "alert_url": "https://alerts.home-assistant.io/#homematicip_cloud.markdown" + }, + { + "title": "Logitech no longer accepting API applications", + "created": "2022-05-16T12:00:00.000Z", + "integrations": [ + { + "package": "logi_circle" + } + ], + "github_issue": "https://github.com/home-assistant/core/issues/71945", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.92.0" + }, + "filename": "logi_circle.markdown", + "alert_url": "https://alerts.home-assistant.io/#logi_circle.markdown" + }, + { + "title": "New Neato Botvacs Do Not Support Existing API", + "created": "2021-12-20T13:27:00.000Z", + "integrations": [ + { + "package": "neato" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "neato.markdown", + "alert_url": "https://alerts.home-assistant.io/#neato.markdown" + }, + { + "title": "Nest Desktop Auth Deprecation", + "created": "2022-05-12T14:04:00.000Z", + "integrations": [ + { + "package": "nest" + } + ], + "github_issue": "https://github.com/home-assistant/core/issues/67662#issuecomment-1144425848", + "filename": "nest.markdown", + "alert_url": "https://alerts.home-assistant.io/#nest.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "Haiku Firmware Update Protocol Change", + "created": "2022-04-05T00:00:00.000Z", + "integrations": [ + { + "package": "senseme" + } + ], + "filename": "senseme.markdown", + "alert_url": "https://alerts.home-assistant.io/#senseme.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "The SoChain integration is disabled due to a dependency conflict", + "created": "2022-02-01T00:00:00.000Z", + "integrations": [ + { + "package": "sochain" + } + ], + "filename": "sochain.markdown", + "alert_url": "https://alerts.home-assistant.io/#sochain.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "Yeelight-manufactured Xiaomi-branded devices removed Local Control", + "created": "2021-03-29T06:00:00.000Z", + "integrations": [ + { + "package": "yeelight" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.89" + }, + "filename": "yeelight.markdown", + "alert_url": "https://alerts.home-assistant.io/#yeelight.markdown" + } +] diff --git a/tests/components/homeassistant_alerts/fixtures/alerts_2.json b/tests/components/homeassistant_alerts/fixtures/alerts_2.json new file mode 100644 index 00000000000..2941d9da143 --- /dev/null +++ b/tests/components/homeassistant_alerts/fixtures/alerts_2.json @@ -0,0 +1,159 @@ +[ + { + "title": "Dark Sky API closed for new users", + "created": "2020-03-31T14:40:00.000Z", + "integrations": [ + { + "package": "darksky" + } + ], + "github_issue": "https://github.com/home-assistant/home-assistant.io/pull/12591", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "dark_sky.markdown", + "alert_url": "https://alerts.home-assistant.io/#dark_sky.markdown" + }, + { + "title": "Hikvision Security Vulnerability", + "created": "2021-09-20T22:08:00.000Z", + "integrations": [ + { + "package": "hikvision" + }, + { + "package": "hikvisioncam" + } + ], + "filename": "hikvision.markdown", + "alert_url": "https://alerts.home-assistant.io/#hikvision.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "Hive shutting down North American Servers", + "created": "2021-11-13T13:58:00.000Z", + "integrations": [ + { + "package": "hive" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "2021.11.0" + }, + "filename": "hive_us.markdown", + "alert_url": "https://alerts.home-assistant.io/#hive_us.markdown" + }, + { + "title": "HomematicIP (EQ-3) blocks public IP addresses, if access to the Cloud is too frequent.", + "created": "2020-12-20T12:00:00.000Z", + "integrations": [ + { + "package": "homematicip_cloud" + } + ], + "packages": [ + { + "package": "homematicip" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.7" + }, + "filename": "homematicip_cloud.markdown", + "alert_url": "https://alerts.home-assistant.io/#homematicip_cloud.markdown" + }, + { + "title": "Logitech no longer accepting API applications", + "created": "2022-05-16T12:00:00.000Z", + "integrations": [ + { + "package": "logi_circle" + } + ], + "github_issue": "https://github.com/home-assistant/core/issues/71945", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.92.0" + }, + "filename": "logi_circle.markdown", + "alert_url": "https://alerts.home-assistant.io/#logi_circle.markdown" + }, + { + "title": "New Neato Botvacs Do Not Support Existing API", + "created": "2021-12-20T13:27:00.000Z", + "integrations": [ + { + "package": "neato" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "neato.markdown", + "alert_url": "https://alerts.home-assistant.io/#neato.markdown" + }, + { + "title": "Nest Desktop Auth Deprecation", + "created": "2022-05-12T14:04:00.000Z", + "integrations": [ + { + "package": "nest" + } + ], + "github_issue": "https://github.com/home-assistant/core/issues/67662#issuecomment-1144425848", + "filename": "nest.markdown", + "alert_url": "https://alerts.home-assistant.io/#nest.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "Haiku Firmware Update Protocol Change", + "created": "2022-04-05T00:00:00.000Z", + "integrations": [ + { + "package": "senseme" + } + ], + "filename": "senseme.markdown", + "alert_url": "https://alerts.home-assistant.io/#senseme.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "The SoChain integration is disabled due to a dependency conflict", + "created": "2022-02-01T00:00:00.000Z", + "integrations": [ + { + "package": "sochain" + } + ], + "filename": "sochain.markdown", + "alert_url": "https://alerts.home-assistant.io/#sochain.markdown", + "homeassistant": { + "package": "homeassistant" + } + }, + { + "title": "Yeelight-manufactured Xiaomi-branded devices removed Local Control", + "created": "2021-03-29T06:00:00.000Z", + "integrations": [ + { + "package": "yeelight" + } + ], + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.89" + }, + "filename": "yeelight.markdown", + "alert_url": "https://alerts.home-assistant.io/#yeelight.markdown" + } +] diff --git a/tests/components/homeassistant_alerts/fixtures/alerts_no_integrations.json b/tests/components/homeassistant_alerts/fixtures/alerts_no_integrations.json new file mode 100644 index 00000000000..25ce79d7e7c --- /dev/null +++ b/tests/components/homeassistant_alerts/fixtures/alerts_no_integrations.json @@ -0,0 +1,27 @@ +[ + { + "title": "Dark Sky API closed for new users", + "created": "2020-03-31T14:40:00.000Z", + "integrations": [ + { + "package": "darksky" + } + ], + "github_issue": "https://github.com/home-assistant/home-assistant.io/pull/12591", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "dark_sky.markdown", + "alert_url": "https://alerts.home-assistant.io/#dark_sky.markdown" + }, + { + "title": "Hikvision Security Vulnerability", + "created": "2021-09-20T22:08:00.000Z", + "filename": "hikvision.markdown", + "alert_url": "https://alerts.home-assistant.io/#hikvision.markdown", + "homeassistant": { + "package": "homeassistant" + } + } +] diff --git a/tests/components/homeassistant_alerts/fixtures/alerts_no_package.json b/tests/components/homeassistant_alerts/fixtures/alerts_no_package.json new file mode 100644 index 00000000000..bcc0a0223ee --- /dev/null +++ b/tests/components/homeassistant_alerts/fixtures/alerts_no_package.json @@ -0,0 +1,33 @@ +[ + { + "title": "Dark Sky API closed for new users", + "created": "2020-03-31T14:40:00.000Z", + "integrations": [ + { + "package": "darksky" + } + ], + "github_issue": "https://github.com/home-assistant/home-assistant.io/pull/12591", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "dark_sky.markdown", + "alert_url": "https://alerts.home-assistant.io/#dark_sky.markdown" + }, + { + "title": "Hikvision Security Vulnerability", + "created": "2021-09-20T22:08:00.000Z", + "integrations": [ + { + "package": "hikvision" + }, + {} + ], + "filename": "hikvision.markdown", + "alert_url": "https://alerts.home-assistant.io/#hikvision.markdown", + "homeassistant": { + "package": "homeassistant" + } + } +] diff --git a/tests/components/homeassistant_alerts/fixtures/alerts_no_url.json b/tests/components/homeassistant_alerts/fixtures/alerts_no_url.json new file mode 100644 index 00000000000..89f277cf69b --- /dev/null +++ b/tests/components/homeassistant_alerts/fixtures/alerts_no_url.json @@ -0,0 +1,34 @@ +[ + { + "title": "Dark Sky API closed for new users", + "created": "2020-03-31T14:40:00.000Z", + "integrations": [ + { + "package": "darksky" + } + ], + "github_issue": "https://github.com/home-assistant/home-assistant.io/pull/12591", + "homeassistant": { + "package": "homeassistant", + "affected_from_version": "0.30" + }, + "filename": "dark_sky.markdown", + "alert_url": "https://alerts.home-assistant.io/#dark_sky.markdown" + }, + { + "title": "Hikvision Security Vulnerability", + "created": "2021-09-20T22:08:00.000Z", + "integrations": [ + { + "package": "hikvision" + }, + { + "package": "hikvisioncam" + } + ], + "filename": "hikvision.markdown", + "homeassistant": { + "package": "homeassistant" + } + } +] diff --git a/tests/components/homeassistant_alerts/test_init.py b/tests/components/homeassistant_alerts/test_init.py new file mode 100644 index 00000000000..c0b6f471033 --- /dev/null +++ b/tests/components/homeassistant_alerts/test_init.py @@ -0,0 +1,433 @@ +"""Test creating repairs from alerts.""" +from __future__ import annotations + +from datetime import timedelta +import json +from unittest.mock import ANY, patch + +import pytest + +from homeassistant.components.homeassistant_alerts import DOMAIN, UPDATE_INTERVAL +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import assert_lists_same, async_fire_time_changed, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +def stub_alert(aioclient_mock, filename): + """Stub an alert.""" + aioclient_mock.get( + f"https://alerts.home-assistant.io/alerts/{filename}", + text=f"""--- +title: Title for {filename} +--- +Content for {filename} +""", + ) + + +@pytest.mark.parametrize( + "ha_version, expected_alerts", + ( + ( + "2022.7.0", + [ + ("aladdin_connect.markdown", "aladdin_connect"), + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("hive_us.markdown", "hive"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + ), + ( + "2022.8.0", + [ + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("hive_us.markdown", "hive"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + ), + ( + "2021.10.0", + [ + ("aladdin_connect.markdown", "aladdin_connect"), + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + ), + ), +) +async def test_alerts( + hass: HomeAssistant, + hass_ws_client, + aioclient_mock: AiohttpClientMocker, + ha_version, + expected_alerts, +) -> None: + """Test creating issues based on alerts.""" + + aioclient_mock.clear_requests() + aioclient_mock.get( + "https://alerts.home-assistant.io/alerts.json", + text=load_fixture("alerts_1.json", "homeassistant_alerts"), + ) + for alert in expected_alerts: + stub_alert(aioclient_mock, alert[0]) + + activated_components = ( + "aladdin_connect", + "darksky", + "hikvision", + "hikvisioncam", + "hive", + "homematicip_cloud", + "logi_circle", + "neato", + "nest", + "senseme", + "sochain", + ) + for domain in activated_components: + hass.config.components.add(domain) + + with patch( + "homeassistant.components.homeassistant_alerts.__version__", + ha_version, + ): + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == { + "issues": [ + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "homeassistant_alerts", + "ignored": False, + "is_fixable": False, + "issue_id": f"{alert}_{integration}", + "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "severity": "warning", + "translation_key": "alert", + "translation_placeholders": { + "title": f"Title for {alert}", + "description": f"Content for {alert}", + }, + } + for alert, integration in expected_alerts + ] + } + + +@pytest.mark.parametrize( + "ha_version, fixture, expected_alerts", + ( + ( + "2022.7.0", + "alerts_no_url.json", + [ + ("dark_sky.markdown", "darksky"), + ], + ), + ( + "2022.7.0", + "alerts_no_integrations.json", + [ + ("dark_sky.markdown", "darksky"), + ], + ), + ( + "2022.7.0", + "alerts_no_package.json", + [ + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ], + ), + ), +) +async def test_bad_alerts( + hass: HomeAssistant, + hass_ws_client, + aioclient_mock: AiohttpClientMocker, + ha_version, + fixture, + expected_alerts, +) -> None: + """Test creating issues based on alerts.""" + fixture_content = load_fixture(fixture, "homeassistant_alerts") + aioclient_mock.clear_requests() + aioclient_mock.get( + "https://alerts.home-assistant.io/alerts.json", + text=fixture_content, + ) + for alert in json.loads(fixture_content): + stub_alert(aioclient_mock, alert["filename"]) + + activated_components = ( + "darksky", + "hikvision", + "hikvisioncam", + ) + for domain in activated_components: + hass.config.components.add(domain) + + with patch( + "homeassistant.components.homeassistant_alerts.__version__", + ha_version, + ): + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == { + "issues": [ + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "homeassistant_alerts", + "ignored": False, + "is_fixable": False, + "issue_id": f"{alert}_{integration}", + "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "severity": "warning", + "translation_key": "alert", + "translation_placeholders": { + "title": f"Title for {alert}", + "description": f"Content for {alert}", + }, + } + for alert, integration in expected_alerts + ] + } + + +async def test_no_alerts( + hass: HomeAssistant, + hass_ws_client, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test creating issues based on alerts.""" + + aioclient_mock.clear_requests() + aioclient_mock.get( + "https://alerts.home-assistant.io/alerts.json", + text="", + ) + + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"issues": []} + + +@pytest.mark.parametrize( + "ha_version, fixture_1, expected_alerts_1, fixture_2, expected_alerts_2", + ( + ( + "2022.7.0", + "alerts_1.json", + [ + ("aladdin_connect.markdown", "aladdin_connect"), + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("hive_us.markdown", "hive"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + "alerts_2.json", + [ + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("hive_us.markdown", "hive"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + ), + ( + "2022.7.0", + "alerts_2.json", + [ + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("hive_us.markdown", "hive"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + "alerts_1.json", + [ + ("aladdin_connect.markdown", "aladdin_connect"), + ("dark_sky.markdown", "darksky"), + ("hikvision.markdown", "hikvision"), + ("hikvision.markdown", "hikvisioncam"), + ("hive_us.markdown", "hive"), + ("homematicip_cloud.markdown", "homematicip_cloud"), + ("logi_circle.markdown", "logi_circle"), + ("neato.markdown", "neato"), + ("nest.markdown", "nest"), + ("senseme.markdown", "senseme"), + ("sochain.markdown", "sochain"), + ], + ), + ), +) +async def test_alerts_change( + hass: HomeAssistant, + hass_ws_client, + aioclient_mock: AiohttpClientMocker, + ha_version: str, + fixture_1: str, + expected_alerts_1: list[tuple(str, str)], + fixture_2: str, + expected_alerts_2: list[tuple(str, str)], +) -> None: + """Test creating issues based on alerts.""" + fixture_1_content = load_fixture(fixture_1, "homeassistant_alerts") + aioclient_mock.clear_requests() + aioclient_mock.get( + "https://alerts.home-assistant.io/alerts.json", + text=fixture_1_content, + ) + for alert in json.loads(fixture_1_content): + stub_alert(aioclient_mock, alert["filename"]) + + activated_components = ( + "aladdin_connect", + "darksky", + "hikvision", + "hikvisioncam", + "hive", + "homematicip_cloud", + "logi_circle", + "neato", + "nest", + "senseme", + "sochain", + ) + for domain in activated_components: + hass.config.components.add(domain) + + with patch( + "homeassistant.components.homeassistant_alerts.__version__", + ha_version, + ): + assert await async_setup_component(hass, DOMAIN, {}) + + now = dt_util.utcnow() + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert_lists_same( + msg["result"]["issues"], + [ + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "homeassistant_alerts", + "ignored": False, + "is_fixable": False, + "issue_id": f"{alert}_{integration}", + "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "severity": "warning", + "translation_key": "alert", + "translation_placeholders": { + "title": f"Title for {alert}", + "description": f"Content for {alert}", + }, + } + for alert, integration in expected_alerts_1 + ], + ) + + fixture_2_content = load_fixture(fixture_2, "homeassistant_alerts") + aioclient_mock.clear_requests() + aioclient_mock.get( + "https://alerts.home-assistant.io/alerts.json", + text=fixture_2_content, + ) + for alert in json.loads(fixture_2_content): + stub_alert(aioclient_mock, alert["filename"]) + + future = now + UPDATE_INTERVAL + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + await client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert_lists_same( + msg["result"]["issues"], + [ + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "homeassistant_alerts", + "ignored": False, + "is_fixable": False, + "issue_id": f"{alert}_{integration}", + "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "severity": "warning", + "translation_key": "alert", + "translation_placeholders": { + "title": f"Title for {alert}", + "description": f"Content for {alert}", + }, + } + for alert, integration in expected_alerts_2 + ], + ) From a4d4170279f712a779ea96aedd3cb2275a51a0ba Mon Sep 17 00:00:00 2001 From: Jevgeni Kiski Date: Wed, 27 Jul 2022 11:58:43 +0300 Subject: [PATCH 2892/3516] Add new vallox temperature and fan sensors (#75783) Co-authored-by: Franck Nijhof --- homeassistant/components/vallox/sensor.py | 77 ++++++++++++++++++----- 1 file changed, 63 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 48b44dd97fe..d3357e50ad2 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, + FREQUENCY_HERTZ, PERCENTAGE, TEMP_CELSIUS, ) @@ -32,7 +33,7 @@ from .const import ( ) -class ValloxSensor(ValloxEntity, SensorEntity): +class ValloxSensorEntity(ValloxEntity, SensorEntity): """Representation of a Vallox sensor.""" entity_description: ValloxSensorEntityDescription @@ -58,10 +59,17 @@ class ValloxSensor(ValloxEntity, SensorEntity): if (metric_key := self.entity_description.metric_key) is None: return None - return self.coordinator.data.get_metric(metric_key) + value = self.coordinator.data.get_metric(metric_key) + + if self.entity_description.round_ndigits is not None and isinstance( + value, float + ): + value = round(value, self.entity_description.round_ndigits) + + return value -class ValloxProfileSensor(ValloxSensor): +class ValloxProfileSensor(ValloxSensorEntity): """Child class for profile reporting.""" @property @@ -77,7 +85,7 @@ class ValloxProfileSensor(ValloxSensor): # # 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): +class ValloxFanSpeedSensor(ValloxSensorEntity): """Child class for fan speed reporting.""" @property @@ -87,7 +95,7 @@ class ValloxFanSpeedSensor(ValloxSensor): return super().native_value if fan_is_on else 0 -class ValloxFilterRemainingSensor(ValloxSensor): +class ValloxFilterRemainingSensor(ValloxSensorEntity): """Child class for filter remaining time reporting.""" @property @@ -104,7 +112,7 @@ class ValloxFilterRemainingSensor(ValloxSensor): ) -class ValloxCellStateSensor(ValloxSensor): +class ValloxCellStateSensor(ValloxSensorEntity): """Child class for cell state reporting.""" @property @@ -123,15 +131,16 @@ class ValloxSensorEntityDescription(SensorEntityDescription): """Describes Vallox sensor entity.""" metric_key: str | None = None - sensor_type: type[ValloxSensor] = ValloxSensor + entity_type: type[ValloxSensorEntity] = ValloxSensorEntity + round_ndigits: int | None = None -SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( +SENSOR_ENTITIES: tuple[ValloxSensorEntityDescription, ...] = ( ValloxSensorEntityDescription( key="current_profile", name="Current profile", icon="mdi:gauge", - sensor_type=ValloxProfileSensor, + entity_type=ValloxProfileSensor, ), ValloxSensorEntityDescription( key="fan_speed", @@ -140,20 +149,40 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( icon="mdi:fan", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, - sensor_type=ValloxFanSpeedSensor, + entity_type=ValloxFanSpeedSensor, + ), + ValloxSensorEntityDescription( + key="extract_fan_speed", + name="Extract fan speed", + metric_key="A_CYC_EXTR_FAN_SPEED", + icon="mdi:fan", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=FREQUENCY_HERTZ, + entity_type=ValloxFanSpeedSensor, + entity_registry_enabled_default=False, + ), + ValloxSensorEntityDescription( + key="supply_fan_speed", + name="Supply fan speed", + metric_key="A_CYC_SUPP_FAN_SPEED", + icon="mdi:fan", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=FREQUENCY_HERTZ, + entity_type=ValloxFanSpeedSensor, + entity_registry_enabled_default=False, ), ValloxSensorEntityDescription( key="remaining_time_for_filter", name="Remaining time for filter", device_class=SensorDeviceClass.TIMESTAMP, - sensor_type=ValloxFilterRemainingSensor, + entity_type=ValloxFilterRemainingSensor, ), ValloxSensorEntityDescription( key="cell_state", name="Cell state", icon="mdi:swap-horizontal-bold", metric_key="A_CYC_CELL_STATE", - sensor_type=ValloxCellStateSensor, + entity_type=ValloxCellStateSensor, ), ValloxSensorEntityDescription( key="extract_air", @@ -187,6 +216,23 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=TEMP_CELSIUS, ), + ValloxSensorEntityDescription( + key="supply_cell_air", + name="Supply cell air", + metric_key="A_CYC_TEMP_SUPPLY_CELL_AIR", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + ), + ValloxSensorEntityDescription( + key="optional_air", + name="Optional air", + metric_key="A_CYC_TEMP_OPTIONAL", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=TEMP_CELSIUS, + entity_registry_enabled_default=False, + ), ValloxSensorEntityDescription( key="humidity", name="Humidity", @@ -202,6 +248,8 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( icon="mdi:gauge", state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=PERCENTAGE, + entity_registry_enabled_default=False, + round_ndigits=0, ), ValloxSensorEntityDescription( key="co2", @@ -210,6 +258,7 @@ SENSORS: tuple[ValloxSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + entity_registry_enabled_default=False, ), ) @@ -223,7 +272,7 @@ async def async_setup_entry( async_add_entities( [ - description.sensor_type(name, coordinator, description) - for description in SENSORS + description.entity_type(name, coordinator, description) + for description in SENSOR_ENTITIES ] ) From aaf5837759de59b18992bb2ce893a71def209526 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 11:29:52 +0200 Subject: [PATCH 2893/3516] Deprecate the Ambee integration (#75805) --- homeassistant/components/ambee/__init__.py | 22 ++++++++++++++ homeassistant/components/ambee/manifest.json | 1 + homeassistant/components/ambee/strings.json | 6 ++++ .../components/ambee/translations/en.json | 6 ++++ tests/components/ambee/test_init.py | 11 +++++++ tests/components/repairs/__init__.py | 29 +++++++++++++++++++ 6 files changed, 75 insertions(+) diff --git a/homeassistant/components/ambee/__init__.py b/homeassistant/components/ambee/__init__.py index ee311df75fa..dbc503928c4 100644 --- a/homeassistant/components/ambee/__init__.py +++ b/homeassistant/components/ambee/__init__.py @@ -3,10 +3,16 @@ from __future__ import annotations from ambee import AirQuality, Ambee, AmbeeAuthenticationError, Pollen +from homeassistant.components.repairs import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN, LOGGER, SCAN_INTERVAL, SERVICE_AIR_QUALITY, SERVICE_POLLEN @@ -14,6 +20,20 @@ from .const import DOMAIN, LOGGER, SCAN_INTERVAL, SERVICE_AIR_QUALITY, SERVICE_P PLATFORMS = [Platform.SENSOR] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Ambee integration.""" + async_create_issue( + hass, + DOMAIN, + "pending_removal", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="pending_removal", + ) + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ambee from a config entry.""" hass.data.setdefault(DOMAIN, {}).setdefault(entry.entry_id, {}) @@ -67,4 +87,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: del hass.data[DOMAIN][entry.entry_id] + if not hass.data[DOMAIN]: + async_delete_issue(hass, DOMAIN, "pending_removal") return unload_ok diff --git a/homeassistant/components/ambee/manifest.json b/homeassistant/components/ambee/manifest.json index 3226e9de3a3..f74832100cd 100644 --- a/homeassistant/components/ambee/manifest.json +++ b/homeassistant/components/ambee/manifest.json @@ -4,6 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambee", "requirements": ["ambee==0.4.0"], + "dependencies": ["repairs"], "codeowners": ["@frenck"], "quality_scale": "platinum", "iot_class": "cloud_polling" diff --git a/homeassistant/components/ambee/strings.json b/homeassistant/components/ambee/strings.json index e3c306788dd..7d0e75877c9 100644 --- a/homeassistant/components/ambee/strings.json +++ b/homeassistant/components/ambee/strings.json @@ -24,5 +24,11 @@ "abort": { "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "issues": { + "pending_removal": { + "title": "The Ambee integration is being removed", + "description": "The Ambee integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nThe integration is being removed, because Ambee removed their free (limited) accounts and doesn't provide a way for regular users to sign up for a paid plan anymore.\n\nRemove the Ambee integration entry from your instance to fix this issue." + } } } diff --git a/homeassistant/components/ambee/translations/en.json b/homeassistant/components/ambee/translations/en.json index 433580e8023..03f4c3241b6 100644 --- a/homeassistant/components/ambee/translations/en.json +++ b/homeassistant/components/ambee/translations/en.json @@ -24,5 +24,11 @@ "description": "Set up Ambee to integrate with Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "The Ambee integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nThe integration is being removed, because Ambee removed their free (limited) accounts and doesn't provide a way for regular users to sign up for a paid plan anymore.\n\nRemove the Ambee integration entry from your instance to fix this issue.", + "title": "The Ambee integration is being removed" + } } } \ No newline at end of file diff --git a/tests/components/ambee/test_init.py b/tests/components/ambee/test_init.py index c6ad45735ff..059c58da803 100644 --- a/tests/components/ambee/test_init.py +++ b/tests/components/ambee/test_init.py @@ -1,6 +1,8 @@ """Tests for the Ambee integration.""" +from collections.abc import Awaitable, Callable from unittest.mock import AsyncMock, MagicMock, patch +from aiohttp import ClientWebSocketResponse from ambee import AmbeeConnectionError from ambee.exceptions import AmbeeAuthenticationError import pytest @@ -10,12 +12,14 @@ from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +from tests.components.repairs import get_repairs async def test_load_unload_config_entry( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_ambee: AsyncMock, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], ) -> None: """Test the Ambee configuration entry loading/unloading.""" mock_config_entry.add_to_hass(hass) @@ -24,9 +28,16 @@ async def test_load_unload_config_entry( assert mock_config_entry.state is ConfigEntryState.LOADED + issues = await get_repairs(hass, hass_ws_client) + assert len(issues) == 1 + assert issues[0]["issue_id"] == "pending_removal" + await hass.config_entries.async_unload(mock_config_entry.entry_id) await hass.async_block_till_done() + issues = await get_repairs(hass, hass_ws_client) + assert len(issues) == 0 + assert not hass.data.get(DOMAIN) diff --git a/tests/components/repairs/__init__.py b/tests/components/repairs/__init__.py index b0d26f49ee4..77971d0284b 100644 --- a/tests/components/repairs/__init__.py +++ b/tests/components/repairs/__init__.py @@ -1 +1,30 @@ """Tests for the repairs integration.""" +from collections.abc import Awaitable, Callable + +from aiohttp import ClientWebSocketResponse + +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def get_repairs( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], +): + """Return the repairs list of issues.""" + assert await async_setup_component(hass, "repairs", {}) + + client = await hass_ws_client(hass) + await hass.async_block_till_done() + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + + client = await hass_ws_client(hass) + await hass.async_block_till_done() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] + + return msg["result"]["issues"] From f8b115dd9ddeb9ff60c11a972166d466ecc92670 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 27 Jul 2022 11:28:58 +0100 Subject: [PATCH 2894/3516] Add xiaomi_ble voltage, consumable and formaldehyde sensors (#75807) Co-authored-by: Martin Hjelmare --- .../components/xiaomi_ble/manifest.json | 2 +- homeassistant/components/xiaomi_ble/sensor.py | 14 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/xiaomi_ble/test_sensor.py | 148 ++++++++++++++++++ 5 files changed, 165 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 457eac60407..2e1a502ca69 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.5.1"], + "requirements": ["xiaomi-ble==0.6.1"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index f33c864fa4a..a96722620a9 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -30,7 +30,9 @@ from homeassistant.const import ( ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME, + CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONDUCTIVITY, + ELECTRIC_POTENTIAL_VOLT, LIGHT_LUX, PERCENTAGE, PRESSURE_MBAR, @@ -74,6 +76,12 @@ SENSOR_DESCRIPTIONS = { native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), + (DeviceClass.VOLTAGE, Units.ELECTRIC_POTENTIAL_VOLT): SensorEntityDescription( + key=str(Units.ELECTRIC_POTENTIAL_VOLT), + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + state_class=SensorStateClass.MEASUREMENT, + ), ( DeviceClass.SIGNAL_STRENGTH, Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -98,6 +106,12 @@ SENSOR_DESCRIPTIONS = { native_unit_of_measurement=CONDUCTIVITY, state_class=SensorStateClass.MEASUREMENT, ), + # Used for e.g. formaldehyde + (None, Units.CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER): SensorEntityDescription( + key=str(Units.CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER), + native_unit_of_measurement=CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), } diff --git a/requirements_all.txt b/requirements_all.txt index f1e14dc0aeb..7240c52de1c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2470,7 +2470,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.5.1 +xiaomi-ble==0.6.1 # homeassistant.components.knx xknx==0.22.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d52446c4a26..0bf0328b77e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1662,7 +1662,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.5.1 +xiaomi-ble==0.6.1 # homeassistant.components.knx xknx==0.22.0 diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index b95ea37311e..dca00e92254 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -50,6 +50,154 @@ async def test_sensors(hass): await hass.async_block_till_done() +async def test_xiaomi_formaldeyhde(hass): + """Make sure that formldehyde sensors are correctly mapped.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="C4:7C:8D:6A:3E:7A", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1010, payload len is 0x2 and payload is 0xf400 + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x10\x10\x02\xf4\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + sensor = hass.states.get("sensor.test_device_formaldehyde") + sensor_attr = sensor.attributes + assert sensor.state == "2.44" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Formaldehyde" + assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "mg/m³" + assert sensor_attr[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_xiaomi_consumable(hass): + """Make sure that consumable sensors are correctly mapped.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="C4:7C:8D:6A:3E:7A", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x13\x10\x02\x60\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + sensor = hass.states.get("sensor.test_device_consumable") + sensor_attr = sensor.attributes + assert sensor.state == "96" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Consumable" + assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert sensor_attr[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_xiaomi_battery_voltage(hass): + """Make sure that battery voltage sensors are correctly mapped.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="C4:7C:8D:6A:3E:7A", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x0a10, payload len is 0x2 and payload is 0x6400 + saved_callback( + make_advertisement( + "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x0a\x10\x02\x64\x00" + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + volt_sensor = hass.states.get("sensor.test_device_voltage") + volt_sensor_attr = volt_sensor.attributes + assert volt_sensor.state == "3.1" + assert volt_sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Voltage" + assert volt_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "V" + assert volt_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + bat_sensor = hass.states.get("sensor.test_device_battery") + bat_sensor_attr = bat_sensor.attributes + assert bat_sensor.state == "100" + assert bat_sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Battery" + assert bat_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert bat_sensor_attr[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + async def test_xiaomi_HHCCJCY01(hass): """This device has multiple advertisements before all sensors are visible. Test that this works.""" entry = MockConfigEntry( From 07a433a5164600c31581e12acc5a0dcd3d6b02d5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 12:52:13 +0200 Subject: [PATCH 2895/3516] Deprecate the OpenALPR Local integration (#75810) * Deprecate the OpenALPR Local integration * Remove tests --- .../openalpr_local/image_processing.py | 16 ++ .../components/openalpr_local/manifest.json | 1 + .../components/openalpr_local/strings.json | 8 + .../openalpr_local/translations/en.json | 8 + tests/components/openalpr_local/__init__.py | 1 - .../openalpr_local/test_image_processing.py | 142 ------------------ 6 files changed, 33 insertions(+), 143 deletions(-) create mode 100644 homeassistant/components/openalpr_local/strings.json create mode 100644 homeassistant/components/openalpr_local/translations/en.json delete mode 100644 tests/components/openalpr_local/__init__.py delete mode 100644 tests/components/openalpr_local/test_image_processing.py diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py index e24af7c7d1f..06688b3b297 100644 --- a/homeassistant/components/openalpr_local/image_processing.py +++ b/homeassistant/components/openalpr_local/image_processing.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import io +import logging import re import voluptuous as vol @@ -13,6 +14,7 @@ from homeassistant.components.image_processing import ( PLATFORM_SCHEMA, ImageProcessingEntity, ) +from homeassistant.components.repairs import IssueSeverity, create_issue from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITY_ID, @@ -26,6 +28,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.async_ import run_callback_threadsafe +_LOGGER = logging.getLogger(__name__) + RE_ALPR_PLATE = re.compile(r"^plate\d*:") RE_ALPR_RESULT = re.compile(r"- (\w*)\s*confidence: (\d*.\d*)") @@ -69,6 +73,18 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the OpenALPR local platform.""" + create_issue( + hass, + "openalpr_local", + "pending_removal", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="pending_removal", + ) + _LOGGER.warning( + "The OpenALPR Local is deprecated and will be removed in Home Assistant 2022.10" + ) command = [config[CONF_ALPR_BIN], "-c", config[CONF_REGION], "-"] confidence = config[CONF_CONFIDENCE] diff --git a/homeassistant/components/openalpr_local/manifest.json b/homeassistant/components/openalpr_local/manifest.json index 8837d79369d..5243aa2b282 100644 --- a/homeassistant/components/openalpr_local/manifest.json +++ b/homeassistant/components/openalpr_local/manifest.json @@ -3,5 +3,6 @@ "name": "OpenALPR Local", "documentation": "https://www.home-assistant.io/integrations/openalpr_local", "codeowners": [], + "dependencies": ["repairs"], "iot_class": "local_push" } diff --git a/homeassistant/components/openalpr_local/strings.json b/homeassistant/components/openalpr_local/strings.json new file mode 100644 index 00000000000..b0dc80c6d06 --- /dev/null +++ b/homeassistant/components/openalpr_local/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "title": "The OpenALPR Local integration is being removed", + "description": "The OpenALPR Local integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/openalpr_local/translations/en.json b/homeassistant/components/openalpr_local/translations/en.json new file mode 100644 index 00000000000..9bc9035515b --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "The OpenALPR Local integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The OpenALPR Local integration is being removed" + } + } +} \ No newline at end of file diff --git a/tests/components/openalpr_local/__init__.py b/tests/components/openalpr_local/__init__.py deleted file mode 100644 index 36b7703491f..00000000000 --- a/tests/components/openalpr_local/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the openalpr_local component.""" diff --git a/tests/components/openalpr_local/test_image_processing.py b/tests/components/openalpr_local/test_image_processing.py deleted file mode 100644 index fefc42fe2ab..00000000000 --- a/tests/components/openalpr_local/test_image_processing.py +++ /dev/null @@ -1,142 +0,0 @@ -"""The tests for the openalpr local platform.""" -from unittest.mock import MagicMock, PropertyMock, patch - -import pytest - -import homeassistant.components.image_processing as ip -from homeassistant.const import ATTR_ENTITY_PICTURE -from homeassistant.setup import async_setup_component - -from tests.common import assert_setup_component, async_capture_events, load_fixture -from tests.components.image_processing import common - - -@pytest.fixture -async def setup_openalpr_local(hass): - """Set up openalpr local.""" - config = { - ip.DOMAIN: { - "platform": "openalpr_local", - "source": {"entity_id": "camera.demo_camera", "name": "test local"}, - "region": "eu", - }, - "camera": {"platform": "demo"}, - } - - with patch( - "homeassistant.components.openalpr_local.image_processing." - "OpenAlprLocalEntity.should_poll", - new_callable=PropertyMock(return_value=False), - ): - await async_setup_component(hass, ip.DOMAIN, config) - await hass.async_block_till_done() - - -@pytest.fixture -def url(hass, setup_openalpr_local): - """Return the camera URL.""" - state = hass.states.get("camera.demo_camera") - return f"{hass.config.internal_url}{state.attributes.get(ATTR_ENTITY_PICTURE)}" - - -@pytest.fixture -async def alpr_events(hass): - """Listen for events.""" - return async_capture_events(hass, "image_processing.found_plate") - - -@pytest.fixture -def popen_mock(): - """Get a Popen mock back.""" - async_popen = MagicMock() - - async def communicate(input=None): - """Communicate mock.""" - fixture = bytes(load_fixture("alpr_stdout.txt"), "utf-8") - return (fixture, None) - - async_popen.communicate = communicate - - with patch("asyncio.create_subprocess_exec", return_value=async_popen) as mock: - yield mock - - -async def test_setup_platform(hass): - """Set up platform with one entity.""" - config = { - ip.DOMAIN: { - "platform": "openalpr_local", - "source": {"entity_id": "camera.demo_camera"}, - "region": "eu", - }, - "camera": {"platform": "demo"}, - } - - with assert_setup_component(1, ip.DOMAIN): - await async_setup_component(hass, ip.DOMAIN, config) - await hass.async_block_till_done() - - assert hass.states.get("image_processing.openalpr_demo_camera") - - -async def test_setup_platform_name(hass): - """Set up platform with one entity and set name.""" - config = { - ip.DOMAIN: { - "platform": "openalpr_local", - "source": {"entity_id": "camera.demo_camera", "name": "test local"}, - "region": "eu", - }, - "camera": {"platform": "demo"}, - } - - with assert_setup_component(1, ip.DOMAIN): - await async_setup_component(hass, ip.DOMAIN, config) - await hass.async_block_till_done() - - assert hass.states.get("image_processing.test_local") - - -async def test_setup_platform_without_region(hass): - """Set up platform with one entity without region.""" - config = { - ip.DOMAIN: { - "platform": "openalpr_local", - "source": {"entity_id": "camera.demo_camera"}, - }, - "camera": {"platform": "demo"}, - } - - with assert_setup_component(0, ip.DOMAIN): - await async_setup_component(hass, ip.DOMAIN, config) - await hass.async_block_till_done() - - -async def test_openalpr_process_image( - setup_openalpr_local, - url, - hass, - alpr_events, - popen_mock, - aioclient_mock, -): - """Set up and scan a picture and test plates from event.""" - aioclient_mock.get(url, content=b"image") - - common.async_scan(hass, entity_id="image_processing.test_local") - await hass.async_block_till_done() - - state = hass.states.get("image_processing.test_local") - - assert popen_mock.called - assert len(alpr_events) == 5 - assert state.attributes.get("vehicles") == 1 - assert state.state == "PE3R2X" - - event_data = [ - event.data for event in alpr_events if event.data.get("plate") == "PE3R2X" - ] - assert len(event_data) == 1 - assert event_data[0]["plate"] == "PE3R2X" - assert event_data[0]["confidence"] == float(98.9371) - assert event_data[0]["entity_id"] == "image_processing.test_local" From 699fff08edacd4349ac69d9393cdb20b0e75e7bd Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 27 Jul 2022 13:21:07 +0200 Subject: [PATCH 2896/3516] Update frontend to 20220727.0 (#75813) --- 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 f19ef00e2b7..2a1fb2d3b37 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220707.1"], + "requirements": ["home-assistant-frontend==20220727.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3a6ae411c5e..2554f0185b4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.1 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220707.1 +home-assistant-frontend==20220727.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 7240c52de1c..ea3f2dd3e6c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -839,7 +839,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220707.1 +home-assistant-frontend==20220727.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0bf0328b77e..9f19f813747 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,7 +616,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220707.1 +home-assistant-frontend==20220727.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 6254142b8a7f0becdd5953efbb49f97afc93ec34 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 27 Jul 2022 08:03:51 -0400 Subject: [PATCH 2897/3516] Fix ZHA on with timed off cluster command (#75815) --- homeassistant/components/zha/core/channels/general.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index b2870d84e15..d310157327b 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -365,7 +365,7 @@ class OnOffChannel(ZigbeeChannel): should_accept = args[0] on_time = args[1] # 0 is always accept 1 is only accept when already on - if should_accept == 0 or (should_accept == 1 and self._state): + if should_accept == 0 or (should_accept == 1 and bool(self.on_off)): if self._off_listener is not None: self._off_listener() self._off_listener = None From 314778cb50b5107bc18da04778ef9c8818352994 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 14:17:22 +0200 Subject: [PATCH 2898/3516] Raise YAML removal issue for Anthem A/V Receivers (#75816) --- homeassistant/components/anthemav/manifest.json | 1 + homeassistant/components/anthemav/media_player.py | 15 ++++++++++++++- homeassistant/components/anthemav/strings.json | 6 ++++++ .../components/anthemav/translations/en.json | 6 ++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/anthemav/manifest.json b/homeassistant/components/anthemav/manifest.json index 33fe565ad03..27db9df32a3 100644 --- a/homeassistant/components/anthemav/manifest.json +++ b/homeassistant/components/anthemav/manifest.json @@ -3,6 +3,7 @@ "name": "Anthem A/V Receivers", "documentation": "https://www.home-assistant.io/integrations/anthemav", "requirements": ["anthemav==1.3.2"], + "dependencies": ["repairs"], "codeowners": ["@hyralex"], "config_flow": true, "iot_class": "local_push", diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index 93fdedef054..3c3a363a6db 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -12,6 +12,7 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, ) +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -55,8 +56,20 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up our socket to the AVR.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) _LOGGER.warning( - "AnthemAV configuration is deprecated and has been automatically imported. Please remove the integration from your configuration file" + "Configuration of the Anthem A/V Receivers integration in YAML is " + "deprecated and will be removed in Home Assistant 2022.10; Your " + "existing configuration has been imported into the UI automatically " + "and can be safely removed from your configuration.yaml file" ) await hass.config_entries.flow.async_init( DOMAIN, diff --git a/homeassistant/components/anthemav/strings.json b/homeassistant/components/anthemav/strings.json index 1f1dd0ec75b..b4e777c4de1 100644 --- a/homeassistant/components/anthemav/strings.json +++ b/homeassistant/components/anthemav/strings.json @@ -15,5 +15,11 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Anthem A/V Receivers YAML configuration is being removed", + "description": "Configuring Anthem A/V Receivers using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Anthem A/V Receivers YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/anthemav/translations/en.json b/homeassistant/components/anthemav/translations/en.json index 9177d5a6e70..af4c83eb2a8 100644 --- a/homeassistant/components/anthemav/translations/en.json +++ b/homeassistant/components/anthemav/translations/en.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Anthem A/V Receivers using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Anthem A/V Receivers YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Anthem A/V Receivers YAML configuration is being removed" + } } } \ No newline at end of file From 49854b809cc8ce10e7e18ad17a8cffd541555ed7 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 27 Jul 2022 14:17:38 +0200 Subject: [PATCH 2899/3516] Netatmo entity renaming and clean up (#75337) --- homeassistant/components/netatmo/camera.py | 12 +-- homeassistant/components/netatmo/climate.py | 20 ++--- homeassistant/components/netatmo/const.py | 66 +++++++------- .../components/netatmo/data_handler.py | 86 +++++++++---------- homeassistant/components/netatmo/light.py | 7 +- .../components/netatmo/netatmo_entity_base.py | 20 +++-- homeassistant/components/netatmo/select.py | 4 +- homeassistant/components/netatmo/sensor.py | 23 +++-- tests/components/netatmo/test_camera.py | 26 +++--- tests/components/netatmo/test_light.py | 2 +- tests/components/netatmo/test_sensor.py | 6 +- 11 files changed, 128 insertions(+), 144 deletions(-) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index e5eaad50c9f..3235d16479c 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -112,6 +112,8 @@ async def async_setup_entry( class NetatmoCamera(NetatmoBase, Camera): """Representation of a Netatmo camera.""" + _attr_brand = MANUFACTURER + _attr_has_entity_name = True _attr_supported_features = CameraEntityFeature.STREAM def __init__( @@ -126,14 +128,13 @@ class NetatmoCamera(NetatmoBase, Camera): Camera.__init__(self) super().__init__(data_handler) - self._data_classes.append( + self._publishers.append( {"name": CAMERA_DATA_CLASS_NAME, SIGNAL_NAME: CAMERA_DATA_CLASS_NAME} ) self._id = camera_id self._home_id = home_id 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}" @@ -193,7 +194,7 @@ class NetatmoCamera(NetatmoBase, Camera): """Return data for this entity.""" return cast( pyatmo.AsyncCameraData, - self.data_handler.data[self._data_classes[0]["name"]], + self.data_handler.data[self._publishers[0]["name"]], ) async def async_camera_image( @@ -219,11 +220,6 @@ class NetatmoCamera(NetatmoBase, Camera): """Return True if entity is available.""" return bool(self._alim_status == "on" or self._status == "disconnected") - @property - def brand(self) -> str: - """Return the camera brand.""" - return MANUFACTURER - @property def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index cc515e50a11..eb7d996eefb 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -127,7 +127,7 @@ async def async_setup_entry( for home_id in climate_topology.home_ids: signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" - await data_handler.register_data_class( + await data_handler.subscribe( CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id ) @@ -185,14 +185,10 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._room = room self._id = self._room.entity_id - self._climate_state_class = ( - f"{CLIMATE_STATE_CLASS_NAME}-{self._room.home.entity_id}" - ) - self._climate_state: pyatmo.AsyncClimate = data_handler.data[ - self._climate_state_class - ] + self._signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{self._room.home.entity_id}" + self._climate_state: pyatmo.AsyncClimate = data_handler.data[self._signal_name] - self._data_classes.extend( + self._publishers.extend( [ { "name": CLIMATE_TOPOLOGY_CLASS_NAME, @@ -201,7 +197,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): { "name": CLIMATE_STATE_CLASS_NAME, "home_id": self._room.home.entity_id, - SIGNAL_NAME: self._climate_state_class, + SIGNAL_NAME: self._signal_name, }, ] ) @@ -254,7 +250,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self.data_handler, module, self._id, - self._climate_state_class, + self._signal_name, ), ) @@ -278,7 +274,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): ATTR_SELECTED_SCHEDULE ] = self._selected_schedule self.async_write_ha_state() - self.data_handler.async_force_update(self._climate_state_class) + self.data_handler.async_force_update(self._signal_name) return home = data["home"] @@ -295,7 +291,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._attr_target_temperature = self._away_temperature elif self._attr_preset_mode == PRESET_SCHEDULE: self.async_update_callback() - self.data_handler.async_force_update(self._climate_state_class) + self.data_handler.async_force_update(self._signal_name) self.async_write_ha_state() return diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 5fda8759540..6bd66fa9644 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -73,16 +73,16 @@ DATA_HANDLER = "netatmo_data_handler" SIGNAL_NAME = "signal_name" NETATMO_CREATE_BATTERY = "netatmo_create_battery" -CONF_CLOUDHOOK_URL = "cloudhook_url" -CONF_WEATHER_AREAS = "weather_areas" -CONF_NEW_AREA = "new_area" CONF_AREA_NAME = "area_name" +CONF_CLOUDHOOK_URL = "cloudhook_url" CONF_LAT_NE = "lat_ne" -CONF_LON_NE = "lon_ne" CONF_LAT_SW = "lat_sw" +CONF_LON_NE = "lon_ne" CONF_LON_SW = "lon_sw" +CONF_NEW_AREA = "new_area" CONF_PUBLIC_MODE = "mode" CONF_UUID = "uuid" +CONF_WEATHER_AREAS = "weather_areas" OAUTH2_AUTHORIZE = "https://api.netatmo.com/oauth2/authorize" OAUTH2_TOKEN = "https://api.netatmo.com/oauth2/token" @@ -94,53 +94,53 @@ DATA_HOMES = "netatmo_homes" DATA_PERSONS = "netatmo_persons" DATA_SCHEDULES = "netatmo_schedules" -NETATMO_WEBHOOK_URL = None NETATMO_EVENT = "netatmo_event" +NETATMO_WEBHOOK_URL = None -DEFAULT_PERSON = "unknown" DEFAULT_DISCOVERY = True +DEFAULT_PERSON = "unknown" DEFAULT_WEBHOOKS = False -ATTR_PSEUDO = "pseudo" +ATTR_CAMERA_LIGHT_MODE = "camera_light_mode" ATTR_EVENT_TYPE = "event_type" +ATTR_FACE_URL = "face_url" ATTR_HEATING_POWER_REQUEST = "heating_power_request" ATTR_HOME_ID = "home_id" ATTR_HOME_NAME = "home_name" +ATTR_IS_KNOWN = "is_known" ATTR_PERSON = "person" ATTR_PERSONS = "persons" -ATTR_IS_KNOWN = "is_known" -ATTR_FACE_URL = "face_url" +ATTR_PSEUDO = "pseudo" ATTR_SCHEDULE_ID = "schedule_id" ATTR_SCHEDULE_NAME = "schedule_name" ATTR_SELECTED_SCHEDULE = "selected_schedule" -ATTR_CAMERA_LIGHT_MODE = "camera_light_mode" SERVICE_SET_CAMERA_LIGHT = "set_camera_light" -SERVICE_SET_SCHEDULE = "set_schedule" -SERVICE_SET_PERSONS_HOME = "set_persons_home" SERVICE_SET_PERSON_AWAY = "set_person_away" +SERVICE_SET_PERSONS_HOME = "set_persons_home" +SERVICE_SET_SCHEDULE = "set_schedule" # Climate events -EVENT_TYPE_SET_POINT = "set_point" EVENT_TYPE_CANCEL_SET_POINT = "cancel_set_point" -EVENT_TYPE_THERM_MODE = "therm_mode" EVENT_TYPE_SCHEDULE = "schedule" +EVENT_TYPE_SET_POINT = "set_point" +EVENT_TYPE_THERM_MODE = "therm_mode" # Camera events -EVENT_TYPE_LIGHT_MODE = "light_mode" -EVENT_TYPE_CAMERA_OUTDOOR = "outdoor" EVENT_TYPE_CAMERA_ANIMAL = "animal" EVENT_TYPE_CAMERA_HUMAN = "human" -EVENT_TYPE_CAMERA_VEHICLE = "vehicle" EVENT_TYPE_CAMERA_MOVEMENT = "movement" +EVENT_TYPE_CAMERA_OUTDOOR = "outdoor" EVENT_TYPE_CAMERA_PERSON = "person" EVENT_TYPE_CAMERA_PERSON_AWAY = "person_away" +EVENT_TYPE_CAMERA_VEHICLE = "vehicle" +EVENT_TYPE_LIGHT_MODE = "light_mode" # Door tags -EVENT_TYPE_DOOR_TAG_SMALL_MOVE = "tag_small_move" +EVENT_TYPE_ALARM_STARTED = "alarm_started" EVENT_TYPE_DOOR_TAG_BIG_MOVE = "tag_big_move" EVENT_TYPE_DOOR_TAG_OPEN = "tag_open" +EVENT_TYPE_DOOR_TAG_SMALL_MOVE = "tag_small_move" EVENT_TYPE_OFF = "off" EVENT_TYPE_ON = "on" -EVENT_TYPE_ALARM_STARTED = "alarm_started" OUTDOOR_CAMERA_TRIGGERS = [ EVENT_TYPE_CAMERA_ANIMAL, @@ -149,46 +149,46 @@ OUTDOOR_CAMERA_TRIGGERS = [ EVENT_TYPE_CAMERA_VEHICLE, ] INDOOR_CAMERA_TRIGGERS = [ - EVENT_TYPE_CAMERA_MOVEMENT, - EVENT_TYPE_CAMERA_PERSON, - EVENT_TYPE_CAMERA_PERSON_AWAY, EVENT_TYPE_ALARM_STARTED, + EVENT_TYPE_CAMERA_MOVEMENT, + EVENT_TYPE_CAMERA_PERSON_AWAY, + EVENT_TYPE_CAMERA_PERSON, ] DOOR_TAG_TRIGGERS = [ - EVENT_TYPE_DOOR_TAG_SMALL_MOVE, EVENT_TYPE_DOOR_TAG_BIG_MOVE, EVENT_TYPE_DOOR_TAG_OPEN, + EVENT_TYPE_DOOR_TAG_SMALL_MOVE, ] CLIMATE_TRIGGERS = [ - EVENT_TYPE_SET_POINT, EVENT_TYPE_CANCEL_SET_POINT, + EVENT_TYPE_SET_POINT, EVENT_TYPE_THERM_MODE, ] EVENT_ID_MAP = { - EVENT_TYPE_CAMERA_MOVEMENT: "device_id", - EVENT_TYPE_CAMERA_PERSON: "device_id", - EVENT_TYPE_CAMERA_PERSON_AWAY: "device_id", + EVENT_TYPE_ALARM_STARTED: "device_id", EVENT_TYPE_CAMERA_ANIMAL: "device_id", EVENT_TYPE_CAMERA_HUMAN: "device_id", + EVENT_TYPE_CAMERA_MOVEMENT: "device_id", EVENT_TYPE_CAMERA_OUTDOOR: "device_id", + EVENT_TYPE_CAMERA_PERSON_AWAY: "device_id", + EVENT_TYPE_CAMERA_PERSON: "device_id", EVENT_TYPE_CAMERA_VEHICLE: "device_id", - EVENT_TYPE_DOOR_TAG_SMALL_MOVE: "device_id", + EVENT_TYPE_CANCEL_SET_POINT: "room_id", EVENT_TYPE_DOOR_TAG_BIG_MOVE: "device_id", EVENT_TYPE_DOOR_TAG_OPEN: "device_id", + EVENT_TYPE_DOOR_TAG_SMALL_MOVE: "device_id", EVENT_TYPE_LIGHT_MODE: "device_id", - EVENT_TYPE_ALARM_STARTED: "device_id", - EVENT_TYPE_CANCEL_SET_POINT: "room_id", EVENT_TYPE_SET_POINT: "room_id", EVENT_TYPE_THERM_MODE: "home_id", } -MODE_LIGHT_ON = "on" -MODE_LIGHT_OFF = "off" MODE_LIGHT_AUTO = "auto" +MODE_LIGHT_OFF = "off" +MODE_LIGHT_ON = "on" CAMERA_LIGHT_MODES = [MODE_LIGHT_ON, MODE_LIGHT_OFF, MODE_LIGHT_AUTO] WEBHOOK_ACTIVATION = "webhook_activation" WEBHOOK_DEACTIVATION = "webhook_deactivation" +WEBHOOK_LIGHT_MODE = "NOC-light_mode" WEBHOOK_NACAMERA_CONNECTION = "NACamera-connection" WEBHOOK_PUSH_TYPE = "push_type" -WEBHOOK_LIGHT_MODE = "NOC-light_mode" diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 1d6345506c1..3a1ea73e311 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -64,11 +64,11 @@ class NetatmoDevice: data_handler: NetatmoDataHandler device: pyatmo.climate.NetatmoModule parent_id: str - state_class_name: str + signal_name: str @dataclass -class NetatmoDataClass: +class NetatmoPublisher: """Class for keeping track of Netatmo data class metadata.""" name: str @@ -85,7 +85,7 @@ class NetatmoDataHandler: self.hass = hass self.config_entry = config_entry self._auth = hass.data[DOMAIN][config_entry.entry_id][AUTH] - self.data_classes: dict = {} + self.publisher: dict[str, NetatmoPublisher] = {} self.data: dict = {} self._queue: deque = deque() self._webhook: bool = False @@ -107,7 +107,7 @@ class NetatmoDataHandler: await asyncio.gather( *[ - self.register_data_class(data_class, data_class, None) + self.subscribe(data_class, data_class, None) for data_class in ( CLIMATE_TOPOLOGY_CLASS_NAME, CAMERA_DATA_CLASS_NAME, @@ -128,20 +128,18 @@ class NetatmoDataHandler: if data_class.next_scan > time(): continue - if data_class_name := data_class.name: - self.data_classes[data_class_name].next_scan = ( - time() + data_class.interval - ) + if publisher := data_class.name: + self.publisher[publisher].next_scan = time() + data_class.interval - await self.async_fetch_data(data_class_name) + await self.async_fetch_data(publisher) self._queue.rotate(BATCH_SIZE) @callback - def async_force_update(self, data_class_entry: str) -> None: + def async_force_update(self, signal_name: str) -> None: """Prioritize data retrieval for given data class entry.""" - self.data_classes[data_class_entry].next_scan = time() - self._queue.rotate(-(self._queue.index(self.data_classes[data_class_entry]))) + self.publisher[signal_name].next_scan = time() + self._queue.rotate(-(self._queue.index(self.publisher[signal_name]))) async def handle_event(self, event: dict) -> None: """Handle webhook events.""" @@ -157,17 +155,17 @@ class NetatmoDataHandler: _LOGGER.debug("%s camera reconnected", MANUFACTURER) self.async_force_update(CAMERA_DATA_CLASS_NAME) - async def async_fetch_data(self, data_class_entry: str) -> None: + async def async_fetch_data(self, signal_name: str) -> None: """Fetch data and notify.""" - if self.data[data_class_entry] is None: + if self.data[signal_name] is None: return try: - await self.data[data_class_entry].async_update() + await self.data[signal_name].async_update() except pyatmo.NoDevice as err: _LOGGER.debug(err) - self.data[data_class_entry] = None + self.data[signal_name] = None except pyatmo.ApiError as err: _LOGGER.debug(err) @@ -176,56 +174,52 @@ class NetatmoDataHandler: _LOGGER.debug(err) return - for update_callback in self.data_classes[data_class_entry].subscriptions: + for update_callback in self.publisher[signal_name].subscriptions: if update_callback: update_callback() - async def register_data_class( + async def subscribe( self, - data_class_name: str, - data_class_entry: str, + publisher: str, + signal_name: str, update_callback: CALLBACK_TYPE | None, **kwargs: Any, ) -> None: - """Register data class.""" - if data_class_entry in self.data_classes: - if update_callback not in self.data_classes[data_class_entry].subscriptions: - self.data_classes[data_class_entry].subscriptions.append( - update_callback - ) + """Subscribe to publisher.""" + if signal_name in self.publisher: + if update_callback not in self.publisher[signal_name].subscriptions: + self.publisher[signal_name].subscriptions.append(update_callback) return - self.data_classes[data_class_entry] = NetatmoDataClass( - name=data_class_entry, - interval=DEFAULT_INTERVALS[data_class_name], - next_scan=time() + DEFAULT_INTERVALS[data_class_name], + self.publisher[signal_name] = NetatmoPublisher( + name=signal_name, + interval=DEFAULT_INTERVALS[publisher], + next_scan=time() + DEFAULT_INTERVALS[publisher], subscriptions=[update_callback], ) - self.data[data_class_entry] = DATA_CLASSES[data_class_name]( - self._auth, **kwargs - ) + self.data[signal_name] = DATA_CLASSES[publisher](self._auth, **kwargs) try: - await self.async_fetch_data(data_class_entry) + await self.async_fetch_data(signal_name) except KeyError: - self.data_classes.pop(data_class_entry) + self.publisher.pop(signal_name) raise - self._queue.append(self.data_classes[data_class_entry]) - _LOGGER.debug("Data class %s added", data_class_entry) + self._queue.append(self.publisher[signal_name]) + _LOGGER.debug("Publisher %s added", signal_name) - async def unregister_data_class( - self, data_class_entry: str, update_callback: CALLBACK_TYPE | None + async def unsubscribe( + self, signal_name: str, update_callback: CALLBACK_TYPE | None ) -> None: - """Unregister data class.""" - self.data_classes[data_class_entry].subscriptions.remove(update_callback) + """Unsubscribe from publisher.""" + self.publisher[signal_name].subscriptions.remove(update_callback) - if not self.data_classes[data_class_entry].subscriptions: - self._queue.remove(self.data_classes[data_class_entry]) - self.data_classes.pop(data_class_entry) - self.data.pop(data_class_entry) - _LOGGER.debug("Data class %s removed", data_class_entry) + if not self.publisher[signal_name].subscriptions: + self._queue.remove(self.publisher[signal_name]) + self.publisher.pop(signal_name) + self.data.pop(signal_name) + _LOGGER.debug("Publisher %s removed", signal_name) @property def webhook(self) -> bool: diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py index 7e078153a8a..6567ae770f2 100644 --- a/homeassistant/components/netatmo/light.py +++ b/homeassistant/components/netatmo/light.py @@ -17,7 +17,6 @@ from .const import ( DATA_HANDLER, DOMAIN, EVENT_TYPE_LIGHT_MODE, - MANUFACTURER, SIGNAL_NAME, TYPE_SECURITY, WEBHOOK_LIGHT_MODE, @@ -63,6 +62,7 @@ class NetatmoLight(NetatmoBase, LightEntity): """Representation of a Netatmo Presence camera light.""" _attr_color_mode = ColorMode.ONOFF + _attr_has_entity_name = True _attr_supported_color_modes = {ColorMode.ONOFF} def __init__( @@ -76,7 +76,7 @@ class NetatmoLight(NetatmoBase, LightEntity): LightEntity.__init__(self) super().__init__(data_handler) - self._data_classes.append( + self._publishers.append( {"name": CAMERA_DATA_CLASS_NAME, SIGNAL_NAME: CAMERA_DATA_CLASS_NAME} ) self._id = camera_id @@ -84,7 +84,6 @@ class NetatmoLight(NetatmoBase, LightEntity): 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 self._attr_unique_id = f"{self._id}-light" @@ -123,7 +122,7 @@ class NetatmoLight(NetatmoBase, LightEntity): """Return data for this entity.""" return cast( pyatmo.AsyncCameraData, - self.data_handler.data[self._data_classes[0]["name"]], + self.data_handler.data[self._publishers[0]["name"]], ) @property diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index decedbbdfbd..e8a346ccd84 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -1,6 +1,8 @@ """Base class for Netatmo entities.""" from __future__ import annotations +from typing import Any + from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback from homeassistant.helpers import device_registry as dr @@ -23,7 +25,7 @@ class NetatmoBase(Entity): def __init__(self, data_handler: NetatmoDataHandler) -> None: """Set up Netatmo entity base.""" self.data_handler = data_handler - self._data_classes: list[dict] = [] + self._publishers: list[dict[str, Any]] = [] self._device_name: str = "" self._id: str = "" @@ -35,11 +37,11 @@ class NetatmoBase(Entity): async def async_added_to_hass(self) -> None: """Entity created.""" - for data_class in self._data_classes: + for data_class in self._publishers: signal_name = data_class[SIGNAL_NAME] if "home_id" in data_class: - await self.data_handler.register_data_class( + await self.data_handler.subscribe( data_class["name"], signal_name, self.async_update_callback, @@ -47,7 +49,7 @@ class NetatmoBase(Entity): ) elif data_class["name"] == PUBLICDATA_DATA_CLASS_NAME: - await self.data_handler.register_data_class( + await self.data_handler.subscribe( data_class["name"], signal_name, self.async_update_callback, @@ -58,13 +60,13 @@ class NetatmoBase(Entity): ) else: - await self.data_handler.register_data_class( + await self.data_handler.subscribe( data_class["name"], signal_name, self.async_update_callback ) - for sub in self.data_handler.data_classes[signal_name].subscriptions: + for sub in self.data_handler.publisher[signal_name].subscriptions: if sub is None: - await self.data_handler.unregister_data_class(signal_name, None) + await self.data_handler.unsubscribe(signal_name, None) registry = dr.async_get(self.hass) if device := registry.async_get_device({(DOMAIN, self._id)}): @@ -76,8 +78,8 @@ class NetatmoBase(Entity): """Run when entity will be removed from hass.""" await super().async_will_remove_from_hass() - for data_class in self._data_classes: - await self.data_handler.unregister_data_class( + for data_class in self._publishers: + await self.data_handler.unsubscribe( data_class[SIGNAL_NAME], self.async_update_callback ) diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index 56f33b04432..62e6ef25969 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -46,7 +46,7 @@ async def async_setup_entry( for home_id in climate_topology.home_ids: signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" - await data_handler.register_data_class( + await data_handler.subscribe( CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id ) @@ -92,7 +92,7 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity): self._home = self._climate_state.homes[self._home_id] - self._data_classes.extend( + self._publishers.extend( [ { "name": CLIMATE_TOPOLOGY_CLASS_NAME, diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 217b2146cc9..bec9af96442 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -41,7 +41,6 @@ from .const import ( CONF_WEATHER_AREAS, DATA_HANDLER, DOMAIN, - MANUFACTURER, NETATMO_CREATE_BATTERY, SIGNAL_NAME, TYPE_WEATHER, @@ -422,7 +421,7 @@ async def async_setup_entry( ) continue - await data_handler.register_data_class( + await data_handler.subscribe( PUBLICDATA_DATA_CLASS_NAME, signal_name, None, @@ -487,9 +486,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity): super().__init__(data_handler) self.entity_description = description - self._data_classes.append( - {"name": data_class_name, SIGNAL_NAME: data_class_name} - ) + self._publishers.append({"name": data_class_name, SIGNAL_NAME: data_class_name}) self._id = module_info["_id"] self._station_id = module_info.get("main_device", self._id) @@ -507,7 +504,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity): f"{module_info.get('module_name', device['type'])}" ) - self._attr_name = f"{MANUFACTURER} {self._device_name} {description.name}" + self._attr_name = f"{self._device_name} {description.name}" self._model = device["type"] self._netatmo_type = TYPE_WEATHER self._attr_unique_id = f"{self._id}-{description.key}" @@ -517,7 +514,7 @@ class NetatmoSensor(NetatmoBase, SensorEntity): """Return data for this entity.""" return cast( pyatmo.AsyncWeatherStationData, - self.data_handler.data[self._data_classes[0]["name"]], + self.data_handler.data[self._publishers[0]["name"]], ) @property @@ -598,7 +595,7 @@ class NetatmoClimateBatterySensor(NetatmoBase, SensorEntity): self._id = netatmo_device.parent_id self._attr_name = f"{self._module.name} {self.entity_description.name}" - self._state_class_name = netatmo_device.state_class_name + self._signal_name = netatmo_device.signal_name self._room_id = self._module.room_id self._model = getattr(self._module.device_type, "value") @@ -734,7 +731,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): self._signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}" - self._data_classes.append( + self._publishers.append( { "name": PUBLICDATA_DATA_CLASS_NAME, "lat_ne": area.lat_ne, @@ -751,7 +748,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): self._area_name = area.area_name self._id = self._area_name self._device_name = f"{self._area_name}" - self._attr_name = f"{MANUFACTURER} {self._device_name} {description.name}" + self._attr_name = f"{self._device_name} {description.name}" self._show_on_map = area.show_on_map self._attr_unique_id = ( f"{self._device_name.replace(' ', '-')}-{description.key}" @@ -788,13 +785,13 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): if self.area == area: return - await self.data_handler.unregister_data_class( + await self.data_handler.unsubscribe( self._signal_name, self.async_update_callback ) self.area = area self._signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}" - self._data_classes = [ + self._publishers = [ { "name": PUBLICDATA_DATA_CLASS_NAME, "lat_ne": area.lat_ne, @@ -807,7 +804,7 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity): ] self._mode = area.mode self._show_on_map = area.show_on_map - await self.data_handler.register_data_class( + await self.data_handler.subscribe( PUBLICDATA_DATA_CLASS_NAME, self._signal_name, self.async_update_callback, diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index 2eaf713e8ee..0e10ce92288 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -32,8 +32,8 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): webhook_id = config_entry.data[CONF_WEBHOOK_ID] await hass.async_block_till_done() - camera_entity_indoor = "camera.netatmo_hall" - camera_entity_outdoor = "camera.netatmo_garden" + camera_entity_indoor = "camera.hall" + camera_entity_outdoor = "camera.garden" assert hass.states.get(camera_entity_indoor).state == "streaming" response = { "event_type": "off", @@ -95,7 +95,7 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: await hass.services.async_call( - "camera", "turn_off", service_data={"entity_id": "camera.netatmo_hall"} + "camera", "turn_off", service_data={"entity_id": "camera.hall"} ) await hass.async_block_till_done() mock_set_state.assert_called_once_with( @@ -106,7 +106,7 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: await hass.services.async_call( - "camera", "turn_on", service_data={"entity_id": "camera.netatmo_hall"} + "camera", "turn_on", service_data={"entity_id": "camera.hall"} ) await hass.async_block_till_done() mock_set_state.assert_called_once_with( @@ -130,7 +130,7 @@ async def test_camera_image_local(hass, config_entry, requests_mock, netatmo_aut uri = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d" stream_uri = uri + "/live/files/high/index.m3u8" - camera_entity_indoor = "camera.netatmo_hall" + camera_entity_indoor = "camera.hall" cam = hass.states.get(camera_entity_indoor) assert cam is not None @@ -161,7 +161,7 @@ async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth) "6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTw,," ) stream_uri = uri + "/live/files/high/index.m3u8" - camera_entity_indoor = "camera.netatmo_garden" + camera_entity_indoor = "camera.garden" cam = hass.states.get(camera_entity_indoor) assert cam is not None @@ -188,7 +188,7 @@ async def test_service_set_person_away(hass, config_entry, netatmo_auth): await hass.async_block_till_done() data = { - "entity_id": "camera.netatmo_hall", + "entity_id": "camera.hall", "person": "Richard Doe", } @@ -205,7 +205,7 @@ async def test_service_set_person_away(hass, config_entry, netatmo_auth): ) data = { - "entity_id": "camera.netatmo_hall", + "entity_id": "camera.hall", } with patch( @@ -231,7 +231,7 @@ async def test_service_set_person_away_invalid_person(hass, config_entry, netatm await hass.async_block_till_done() data = { - "entity_id": "camera.netatmo_hall", + "entity_id": "camera.hall", "person": "Batman", } @@ -259,7 +259,7 @@ async def test_service_set_persons_home_invalid_person( await hass.async_block_till_done() data = { - "entity_id": "camera.netatmo_hall", + "entity_id": "camera.hall", "persons": "Batman", } @@ -285,7 +285,7 @@ async def test_service_set_persons_home(hass, config_entry, netatmo_auth): await hass.async_block_till_done() data = { - "entity_id": "camera.netatmo_hall", + "entity_id": "camera.hall", "persons": "John Doe", } @@ -312,7 +312,7 @@ async def test_service_set_camera_light(hass, config_entry, netatmo_auth): await hass.async_block_till_done() data = { - "entity_id": "camera.netatmo_garden", + "entity_id": "camera.garden", "camera_light_mode": "on", } @@ -485,7 +485,7 @@ async def test_camera_image_raises_exception(hass, config_entry, requests_mock): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - camera_entity_indoor = "camera.netatmo_hall" + camera_entity_indoor = "camera.hall" with pytest.raises(Exception) as excinfo: await camera.async_get_image(hass, camera_entity_indoor) diff --git a/tests/components/netatmo/test_light.py b/tests/components/netatmo/test_light.py index a0992e7ea2c..433841f3878 100644 --- a/tests/components/netatmo/test_light.py +++ b/tests/components/netatmo/test_light.py @@ -27,7 +27,7 @@ async def test_light_setup_and_services(hass, config_entry, netatmo_auth): await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) await hass.async_block_till_done() - light_entity = "light.netatmo_garden" + light_entity = "light.garden" assert hass.states.get(light_entity).state == "unavailable" # Trigger light mode change diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index b1b5b11265a..9adc7423bd6 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -17,7 +17,7 @@ async def test_weather_sensor(hass, config_entry, netatmo_auth): await hass.async_block_till_done() - prefix = "sensor.netatmo_mystation_" + prefix = "sensor.mystation_" assert hass.states.get(f"{prefix}temperature").state == "24.6" assert hass.states.get(f"{prefix}humidity").state == "36" @@ -34,13 +34,13 @@ async def test_public_weather_sensor(hass, config_entry, netatmo_auth): assert len(hass.states.async_all()) > 0 - prefix = "sensor.netatmo_home_max_" + prefix = "sensor.home_max_" assert hass.states.get(f"{prefix}temperature").state == "27.4" assert hass.states.get(f"{prefix}humidity").state == "76" assert hass.states.get(f"{prefix}pressure").state == "1014.4" - prefix = "sensor.netatmo_home_avg_" + prefix = "sensor.home_avg_" assert hass.states.get(f"{prefix}temperature").state == "22.7" assert hass.states.get(f"{prefix}humidity").state == "63.2" From 9dc05448350d3df14bfed51799d997db4bdb2a41 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 14:41:00 +0200 Subject: [PATCH 2900/3516] Raise YAML removal issue for Bose SoundTouch (#75817) --- .../components/soundtouch/manifest.json | 1 + .../components/soundtouch/media_player.py | 18 ++++++++++++++---- .../components/soundtouch/strings.json | 6 ++++++ .../components/soundtouch/translations/en.json | 6 ++++++ 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/soundtouch/manifest.json b/homeassistant/components/soundtouch/manifest.json index c1c2abd3b80..4512f3a8f9b 100644 --- a/homeassistant/components/soundtouch/manifest.json +++ b/homeassistant/components/soundtouch/manifest.json @@ -4,6 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/soundtouch", "requirements": ["libsoundtouch==0.8"], "zeroconf": ["_soundtouch._tcp.local."], + "dependencies": ["repairs"], "codeowners": ["@kroimon"], "iot_class": "local_polling", "loggers": ["libsoundtouch"], diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index 2ed3dd9beea..74d89404d27 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -19,6 +19,7 @@ from homeassistant.components.media_player import ( from homeassistant.components.media_player.browse_media import ( async_process_play_media_url, ) +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, @@ -70,11 +71,20 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Bose SoundTouch platform.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) _LOGGER.warning( - "Configuration of the Bose SoundTouch platform in YAML is deprecated and will be " - "removed in a future release; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" + "Configuration of the Bose SoundTouch integration in YAML is " + "deprecated and will be removed in Home Assistant 2022.10; 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( diff --git a/homeassistant/components/soundtouch/strings.json b/homeassistant/components/soundtouch/strings.json index 7ebcd4c5285..6a8896c8f56 100644 --- a/homeassistant/components/soundtouch/strings.json +++ b/homeassistant/components/soundtouch/strings.json @@ -17,5 +17,11 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Bose SoundTouch YAML configuration is being removed", + "description": "Configuring Bose SoundTouch using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Bose SoundTouch YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/soundtouch/translations/en.json b/homeassistant/components/soundtouch/translations/en.json index 2e025d3f187..28eb32b7d43 100644 --- a/homeassistant/components/soundtouch/translations/en.json +++ b/homeassistant/components/soundtouch/translations/en.json @@ -17,5 +17,11 @@ "title": "Confirm adding Bose SoundTouch device" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Bose SoundTouch using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Bose SoundTouch YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Bose SoundTouch YAML configuration is being removed" + } } } \ No newline at end of file From 8ffdbfc462350bf1111a40523086dd83b13a057f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 15:10:29 +0200 Subject: [PATCH 2901/3516] Bumped version to 2022.8.0b0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 10523aa6d53..02896fcbe96 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 48d12964414..5306a589863 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0.dev0" +version = "2022.8.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 7659555ce57a47f5f1c4407f2987ab53fefa12c8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 15:55:52 +0200 Subject: [PATCH 2902/3516] Bump version to 2022.9.0dev0 (#75818) --- .github/workflows/ci.yaml | 2 +- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 24d37b94518..fd3909ca23a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ on: env: CACHE_VERSION: 1 PIP_CACHE_VERSION: 1 - HA_SHORT_VERSION: 2022.8 + HA_SHORT_VERSION: 2022.9 DEFAULT_PYTHON: 3.9 PRE_COMMIT_CACHE: ~/.cache/pre-commit PIP_CACHE: /tmp/pip-cache diff --git a/homeassistant/const.py b/homeassistant/const.py index 10523aa6d53..7542ce0d77e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -6,7 +6,7 @@ from typing import Final from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 -MINOR_VERSION: Final = 8 +MINOR_VERSION: Final = 9 PATCH_VERSION: Final = "0.dev0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" diff --git a/pyproject.toml b/pyproject.toml index 48d12964414..288bcc5225a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0.dev0" +version = "2022.9.0.dev0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From d99334eb07a8cf87877b6c72cac3bcea17191972 Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Wed, 27 Jul 2022 10:37:22 -0400 Subject: [PATCH 2903/3516] Add LaCrosse View integration (#71896) * Add new LaCrosse View integration * Add new LaCrosse View integration * Add retry logic * Actually use the start time for the retry logic * Get new token after 1 hour * Replace retry logic with more reliable logic * Improve test coverage * Add device info and unique id to config entry * Fix manufacturer name * Improve token refresh and check sensor permission * Improve test cover * Add LaCrosse View to .strict-typing * Remove empty fields in manifest.json * Fix mypy * Add retry logic for get_data * Add missing break statement in retry decorator * Fix requirements * Finish suggestions by Allen Porter * Suggestions by Allen Porter * Fix typing issues with calls to get_locations and get_sensors --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/lacrosse_view/__init__.py | 78 +++++++++ .../components/lacrosse_view/config_flow.py | 128 ++++++++++++++ .../components/lacrosse_view/const.py | 6 + .../components/lacrosse_view/manifest.json | 9 + .../components/lacrosse_view/sensor.py | 136 +++++++++++++++ .../components/lacrosse_view/strings.json | 20 +++ .../lacrosse_view/translations/en.json | 25 +++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 11 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/lacrosse_view/__init__.py | 32 ++++ .../lacrosse_view/test_config_flow.py | 163 ++++++++++++++++++ tests/components/lacrosse_view/test_init.py | 102 +++++++++++ tests/components/lacrosse_view/test_sensor.py | 49 ++++++ 17 files changed, 769 insertions(+) create mode 100644 homeassistant/components/lacrosse_view/__init__.py create mode 100644 homeassistant/components/lacrosse_view/config_flow.py create mode 100644 homeassistant/components/lacrosse_view/const.py create mode 100644 homeassistant/components/lacrosse_view/manifest.json create mode 100644 homeassistant/components/lacrosse_view/sensor.py create mode 100644 homeassistant/components/lacrosse_view/strings.json create mode 100644 homeassistant/components/lacrosse_view/translations/en.json create mode 100644 tests/components/lacrosse_view/__init__.py create mode 100644 tests/components/lacrosse_view/test_config_flow.py create mode 100644 tests/components/lacrosse_view/test_init.py create mode 100644 tests/components/lacrosse_view/test_sensor.py diff --git a/.strict-typing b/.strict-typing index c5b4e376414..d3102572eb1 100644 --- a/.strict-typing +++ b/.strict-typing @@ -148,6 +148,7 @@ homeassistant.components.jewish_calendar.* homeassistant.components.kaleidescape.* homeassistant.components.knx.* homeassistant.components.kraken.* +homeassistant.components.lacrosse_view.* homeassistant.components.lametric.* homeassistant.components.laundrify.* homeassistant.components.lcn.* diff --git a/CODEOWNERS b/CODEOWNERS index bd39fd68590..96238f61fbb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -573,6 +573,8 @@ build.json @home-assistant/supervisor /tests/components/kraken/ @eifinger /homeassistant/components/kulersky/ @emlove /tests/components/kulersky/ @emlove +/homeassistant/components/lacrosse_view/ @IceBotYT +/tests/components/lacrosse_view/ @IceBotYT /homeassistant/components/lametric/ @robbiet480 @frenck /homeassistant/components/launch_library/ @ludeeus @DurgNomis-drol /tests/components/launch_library/ @ludeeus @DurgNomis-drol diff --git a/homeassistant/components/lacrosse_view/__init__.py b/homeassistant/components/lacrosse_view/__init__.py new file mode 100644 index 00000000000..0d3147f43a5 --- /dev/null +++ b/homeassistant/components/lacrosse_view/__init__.py @@ -0,0 +1,78 @@ +"""The LaCrosse View integration.""" +from __future__ import annotations + +from datetime import datetime, timedelta + +from lacrosse_view import LaCrosse, Location, LoginError, Sensor + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, LOGGER, SCAN_INTERVAL + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up LaCrosse View from a config entry.""" + + async def get_data() -> list[Sensor]: + """Get the data from the LaCrosse View.""" + now = datetime.utcnow() + + if hass.data[DOMAIN][entry.entry_id]["last_update"] < now - timedelta( + minutes=59 + ): # Get new token + hass.data[DOMAIN][entry.entry_id]["last_update"] = now + await api.login(entry.data["username"], entry.data["password"]) + + # Get the timestamp for yesterday at 6 PM (this is what is used in the app, i noticed it when proxying the request) + yesterday = now - timedelta(days=1) + yesterday = yesterday.replace(hour=18, minute=0, second=0, microsecond=0) + yesterday_timestamp = datetime.timestamp(yesterday) + + return await api.get_sensors( + location=Location(id=entry.data["id"], name=entry.data["name"]), + tz=hass.config.time_zone, + start=str(int(yesterday_timestamp)), + end=str(int(datetime.timestamp(now))), + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + "api": LaCrosse(async_get_clientsession(hass)), + "last_update": datetime.utcnow(), + } + api: LaCrosse = hass.data[DOMAIN][entry.entry_id]["api"] + + try: + await api.login(entry.data["username"], entry.data["password"]) + except LoginError as error: + raise ConfigEntryNotReady from error + + coordinator = DataUpdateCoordinator( + hass, + LOGGER, + name="LaCrosse View", + update_method=get_data, + update_interval=timedelta(seconds=SCAN_INTERVAL), + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id]["coordinator"] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/lacrosse_view/config_flow.py b/homeassistant/components/lacrosse_view/config_flow.py new file mode 100644 index 00000000000..b5b89828e9b --- /dev/null +++ b/homeassistant/components/lacrosse_view/config_flow.py @@ -0,0 +1,128 @@ +"""Config flow for LaCrosse View integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from lacrosse_view import LaCrosse, Location, LoginError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required("username"): str, + vol.Required("password"): str, + } +) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> list[Location]: + """Validate the user input allows us to connect.""" + + api = LaCrosse(async_get_clientsession(hass)) + + try: + await api.login(data["username"], data["password"]) + + locations = await api.get_locations() + except LoginError as error: + raise InvalidAuth from error + + if not locations: + raise NoLocations("No locations found for account {}".format(data["username"])) + + return locations + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for LaCrosse View.""" + + VERSION = 1 + data: dict[str, str] = {} + locations: list[Location] = [] + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + try: + info = await validate_input(self.hass, user_input) + except InvalidAuth: + errors["base"] = "invalid_auth" + except NoLocations: + errors["base"] = "no_locations" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + self.data = user_input + self.locations = info + return await self.async_step_location() + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_location( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the location step.""" + + if not user_input: + return self.async_show_form( + step_id="location", + data_schema=vol.Schema( + { + vol.Required("location"): vol.In( + {location.id: location.name for location in self.locations} + ) + } + ), + ) + + location_id = user_input["location"] + + for location in self.locations: + if location.id == location_id: + location_name = location.name + + await self.async_set_unique_id(location_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=location_name, + data={ + "id": location_id, + "name": location_name, + "username": self.data["username"], + "password": self.data["password"], + }, + ) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(HomeAssistantError): + """Error to indicate there is invalid auth.""" + + +class NoLocations(HomeAssistantError): + """Error to indicate there are no locations.""" diff --git a/homeassistant/components/lacrosse_view/const.py b/homeassistant/components/lacrosse_view/const.py new file mode 100644 index 00000000000..cae11315bc7 --- /dev/null +++ b/homeassistant/components/lacrosse_view/const.py @@ -0,0 +1,6 @@ +"""Constants for the LaCrosse View integration.""" +import logging + +DOMAIN = "lacrosse_view" +LOGGER = logging.getLogger(__package__) +SCAN_INTERVAL = 30 diff --git a/homeassistant/components/lacrosse_view/manifest.json b/homeassistant/components/lacrosse_view/manifest.json new file mode 100644 index 00000000000..64f40267c8a --- /dev/null +++ b/homeassistant/components/lacrosse_view/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "lacrosse_view", + "name": "LaCrosse View", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/lacrosse_view", + "requirements": ["lacrosse-view==0.0.9"], + "codeowners": ["@IceBotYT"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py new file mode 100644 index 00000000000..8ccbe0514b4 --- /dev/null +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -0,0 +1,136 @@ +"""Sensor component for LaCrosse View.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from re import sub + +from lacrosse_view import Sensor + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN, LOGGER + + +@dataclass +class LaCrosseSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[..., float] + + +@dataclass +class LaCrosseSensorEntityDescription( + SensorEntityDescription, LaCrosseSensorEntityDescriptionMixin +): + """Description for LaCrosse View sensor.""" + + +PARALLEL_UPDATES = 0 +ICON_LIST = { + "Temperature": "mdi:thermometer", + "Humidity": "mdi:water-percent", + "HeatIndex": "mdi:thermometer", + "WindSpeed": "mdi:weather-windy", + "Rain": "mdi:water", +} +UNIT_LIST = { + "degrees_celsius": "°C", + "degrees_fahrenheit": "°F", + "relative_humidity": "%", + "kilometers_per_hour": "km/h", + "inches": "in", +} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up LaCrosse View from a config entry.""" + coordinator: DataUpdateCoordinator[list[Sensor]] = hass.data[DOMAIN][ + entry.entry_id + ]["coordinator"] + sensors: list[Sensor] = coordinator.data + + sensor_list = [] + for i, sensor in enumerate(sensors): + if not sensor.permissions.get("read"): + LOGGER.warning( + "No permission to read sensor %s, are you sure you're signed into the right account?", + sensor.name, + ) + continue + for field in sensor.sensor_field_names: + sensor_list.append( + LaCrosseViewSensor( + coordinator=coordinator, + description=LaCrosseSensorEntityDescription( + key=str(i), + device_class="temperature" if field == "Temperature" else None, + # The regex is to convert CamelCase to Human Case + # e.g. "RelativeHumidity" -> "Relative Humidity" + name=f"{sensor.name} {sub(r'(? None: + """Initialize.""" + super().__init__(coordinator) + sensor = self.coordinator.data[int(description.key)] + + self.entity_description = description + self._attr_unique_id = f"{sensor.location.id}-{description.key}-{field}" + self._attr_name = f"{sensor.location.name} {description.name}" + self._attr_icon = ICON_LIST.get(field, "mdi:thermometer") + self._attr_device_info = { + "identifiers": {(DOMAIN, sensor.sensor_id)}, + "name": sensor.name.split(" ")[0], + "manufacturer": "LaCrosse Technology", + "model": sensor.model, + "via_device": (DOMAIN, sensor.location.id), + } + + @property + def native_value(self) -> float: + """Return the sensor value.""" + return self.entity_description.value_fn() diff --git a/homeassistant/components/lacrosse_view/strings.json b/homeassistant/components/lacrosse_view/strings.json new file mode 100644 index 00000000000..76f1971518a --- /dev/null +++ b/homeassistant/components/lacrosse_view/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "no_locations": "No locations found" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/lacrosse_view/translations/en.json b/homeassistant/components/lacrosse_view/translations/en.json new file mode 100644 index 00000000000..c972d6d12f9 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error", + "no_locations": "No locations found" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + } + }, + "location": { + "data": { + "location": "Location" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 28d0ad6b44b..6d92f7cf7e7 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -191,6 +191,7 @@ FLOWS = { "kostal_plenticore", "kraken", "kulersky", + "lacrosse_view", "launch_library", "laundrify", "lg_soundbar", diff --git a/mypy.ini b/mypy.ini index 37765023f74..af039c74de3 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1351,6 +1351,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.lacrosse_view.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.lametric.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index ea3f2dd3e6c..bdbff5adabd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -943,6 +943,9 @@ kostal_plenticore==0.2.0 # homeassistant.components.kraken krakenex==2.1.0 +# homeassistant.components.lacrosse_view +lacrosse-view==0.0.9 + # homeassistant.components.eufy lakeside==0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f19f813747..3dc23a1e07d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -684,6 +684,9 @@ kostal_plenticore==0.2.0 # homeassistant.components.kraken krakenex==2.1.0 +# homeassistant.components.lacrosse_view +lacrosse-view==0.0.9 + # homeassistant.components.laundrify laundrify_aio==1.1.2 diff --git a/tests/components/lacrosse_view/__init__.py b/tests/components/lacrosse_view/__init__.py new file mode 100644 index 00000000000..ea01e7a72e3 --- /dev/null +++ b/tests/components/lacrosse_view/__init__.py @@ -0,0 +1,32 @@ +"""Tests for the LaCrosse View integration.""" + +from lacrosse_view import Location, Sensor + +MOCK_ENTRY_DATA = { + "username": "test-username", + "password": "test-password", + "id": "1", + "name": "Test", +} +TEST_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["Temperature"], + location=Location(id="1", name="Test"), + data={"Temperature": {"values": [{"s": "2"}], "unit": "degrees_celsius"}}, + permissions={"read": True}, + model="Test", +) +TEST_NO_PERMISSION_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["Temperature"], + location=Location(id="1", name="Test"), + data={"Temperature": {"values": [{"s": "2"}], "unit": "degrees_celsius"}}, + permissions={"read": False}, + model="Test", +) diff --git a/tests/components/lacrosse_view/test_config_flow.py b/tests/components/lacrosse_view/test_config_flow.py new file mode 100644 index 00000000000..82178f2801b --- /dev/null +++ b/tests/components/lacrosse_view/test_config_flow.py @@ -0,0 +1,163 @@ +"""Test the LaCrosse View config flow.""" +from unittest.mock import patch + +from lacrosse_view import Location, LoginError + +from homeassistant import config_entries +from homeassistant.components.lacrosse_view.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch("lacrosse_view.LaCrosse.login", return_value=True,), patch( + "lacrosse_view.LaCrosse.get_locations", + return_value=[Location(id=1, name="Test")], + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "location" + assert result2["errors"] is None + + with patch( + "homeassistant.components.lacrosse_view.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "location": "1", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "Test" + assert result3["data"] == { + "username": "test-username", + "password": "test-password", + "id": "1", + "name": "Test", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_auth_false(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "lacrosse_view.LaCrosse.login", + return_value=False, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("lacrosse_view.LaCrosse.login", side_effect=LoginError): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_login_first(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_locations", side_effect=LoginError + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_no_locations(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_locations", + return_value=None, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "no_locations"} + + +async def test_form_unexpected_error(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.lacrosse_view.config_flow.validate_input", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/lacrosse_view/test_init.py b/tests/components/lacrosse_view/test_init.py new file mode 100644 index 00000000000..a719536f737 --- /dev/null +++ b/tests/components/lacrosse_view/test_init.py @@ -0,0 +1,102 @@ +"""Test the LaCrosse View initialization.""" +from datetime import datetime, timedelta +from unittest.mock import patch + +from freezegun import freeze_time +from lacrosse_view import HTTPError, LoginError + +from homeassistant.components.lacrosse_view.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import MOCK_ENTRY_DATA, TEST_SENSOR + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_unload_entry(hass: HomeAssistant) -> None: + """Test the unload entry.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_SENSOR], + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + + await hass.config_entries.async_unload(entries[0].entry_id) + await hass.async_block_till_done() + assert entries[0].state == ConfigEntryState.NOT_LOADED + + +async def test_login_error(hass: HomeAssistant) -> None: + """Test login error.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", side_effect=LoginError("Test")): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.SETUP_RETRY + + +async def test_http_error(hass: HomeAssistant) -> None: + """Test http error.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_sensors", side_effect=HTTPError + ): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.SETUP_RETRY + + +async def test_new_token(hass: HomeAssistant) -> None: + """Test new token.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True) as login, patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_SENSOR], + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + login.assert_called_once() + + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + + one_hour_after = datetime.utcnow() + timedelta(hours=1) + + with patch("lacrosse_view.LaCrosse.login", return_value=True) as login, patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_SENSOR], + ), freeze_time(one_hour_after): + async_fire_time_changed(hass, one_hour_after) + await hass.async_block_till_done() + + login.assert_called_once() diff --git a/tests/components/lacrosse_view/test_sensor.py b/tests/components/lacrosse_view/test_sensor.py new file mode 100644 index 00000000000..57197662cc9 --- /dev/null +++ b/tests/components/lacrosse_view/test_sensor.py @@ -0,0 +1,49 @@ +"""Test the LaCrosse View sensors.""" +from unittest.mock import patch + +from homeassistant.components.lacrosse_view import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import MOCK_ENTRY_DATA, TEST_NO_PERMISSION_SENSOR, TEST_SENSOR + +from tests.common import MockConfigEntry + + +async def test_entities_added(hass: HomeAssistant) -> None: + """Test the entities are added.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_SENSOR] + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + assert hass.states.get("sensor.test_test_temperature") + + +async def test_sensor_permission(hass: HomeAssistant, caplog) -> None: + """Test if it raises a warning when there is no permission to read the sensor.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_NO_PERMISSION_SENSOR] + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + assert hass.states.get("sensor.test_test_temperature") is None + assert "No permission to read sensor" in caplog.text From 3a8748bc9398316b389aa0307fb149829eab20fd Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Jul 2022 12:01:00 -0400 Subject: [PATCH 2904/3516] Bump zwave-js-server-python to 0.40.0 (#75795) --- homeassistant/components/zwave_js/api.py | 4 ++-- homeassistant/components/zwave_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/zwave_js/test_api.py | 8 ++++---- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 8b98c61c4b2..28afdeb4db8 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -1888,7 +1888,7 @@ async def websocket_get_firmware_update_progress( node: Node, ) -> None: """Get whether firmware update is in progress.""" - connection.send_result(msg[ID], await node.async_get_firmware_update_progress()) + connection.send_result(msg[ID], await node.async_is_firmware_update_in_progress()) def _get_firmware_update_progress_dict( @@ -2012,7 +2012,7 @@ async def websocket_get_any_firmware_update_progress( ) -> None: """Get whether any firmware updates are in progress.""" connection.send_result( - msg[ID], await driver.controller.async_get_any_firmware_update_progress() + msg[ID], await driver.controller.async_is_any_ota_firmware_update_in_progress() ) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 76f5d94b589..ed969e58042 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.39.0"], + "requirements": ["zwave-js-server-python==0.40.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index bdbff5adabd..efc7282d703 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2544,7 +2544,7 @@ zigpy==0.48.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.39.0 +zwave-js-server-python==0.40.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3dc23a1e07d..03bfde54c5c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1715,7 +1715,7 @@ zigpy-znp==0.8.1 zigpy==0.48.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.39.0 +zwave-js-server-python==0.40.0 # homeassistant.components.zwave_me zwave_me_ws==0.2.4 diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 3a2bff54e88..72d8fce7424 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -3459,12 +3459,12 @@ async def test_get_firmware_update_progress( assert len(client.async_send_command.call_args_list) == 1 args = client.async_send_command.call_args[0][0] - assert args["command"] == "node.get_firmware_update_progress" + assert args["command"] == "node.is_firmware_update_in_progress" assert args["nodeId"] == multisensor_6.node_id # Test FailedZWaveCommand is caught with patch( - "zwave_js_server.model.node.Node.async_get_firmware_update_progress", + "zwave_js_server.model.node.Node.async_is_firmware_update_in_progress", side_effect=FailedZWaveCommand("failed_command", 1, "error message"), ): await ws_client.send_json( @@ -3744,11 +3744,11 @@ async def test_get_any_firmware_update_progress( assert len(client.async_send_command.call_args_list) == 1 args = client.async_send_command.call_args[0][0] - assert args["command"] == "controller.get_any_firmware_update_progress" + assert args["command"] == "controller.is_any_ota_firmware_update_in_progress" # Test FailedZWaveCommand is caught with patch( - "zwave_js_server.model.controller.Controller.async_get_any_firmware_update_progress", + "zwave_js_server.model.controller.Controller.async_is_any_ota_firmware_update_in_progress", side_effect=FailedZWaveCommand("failed_command", 1, "error message"), ): await ws_client.send_json( From 2e4f13996f5393de2e8718f28f7638540dc67246 Mon Sep 17 00:00:00 2001 From: borky Date: Wed, 27 Jul 2022 19:33:07 +0300 Subject: [PATCH 2905/3516] Set Level for MIOT purifiers as in python-miio (#75814) * Set Level for MIOT purifiers as in python-miio * Refactoring after review suggestion --- .../components/xiaomi_miio/number.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index e7c61044e25..577e82e3fd8 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -104,6 +104,15 @@ class OscillationAngleValues: step: float | None = None +@dataclass +class FavoriteLevelValues: + """A class that describes favorite level values.""" + + max_value: float | None = None + min_value: float | None = None + step: float | None = None + + NUMBER_TYPES = { FEATURE_SET_MOTOR_SPEED: XiaomiMiioNumberDescription( key=ATTR_MOTOR_SPEED, @@ -237,6 +246,11 @@ OSCILLATION_ANGLE_VALUES = { MODEL_FAN_P11: OscillationAngleValues(max_value=140, min_value=30, step=30), } +FAVORITE_LEVEL_VALUES = { + tuple(MODELS_PURIFIER_MIIO): FavoriteLevelValues(max_value=17, min_value=0, step=1), + tuple(MODELS_PURIFIER_MIOT): FavoriteLevelValues(max_value=14, min_value=0, step=1), +} + async def async_setup_entry( hass: HomeAssistant, @@ -280,6 +294,15 @@ async def async_setup_entry( native_min_value=OSCILLATION_ANGLE_VALUES[model].min_value, native_step=OSCILLATION_ANGLE_VALUES[model].step, ) + elif description.key == ATTR_FAVORITE_LEVEL: + for list_models, favorite_level_value in FAVORITE_LEVEL_VALUES.items(): + if model in list_models: + description = dataclasses.replace( + description, + native_max_value=favorite_level_value.max_value, + native_min_value=favorite_level_value.min_value, + native_step=favorite_level_value.step, + ) entities.append( XiaomiNumberEntity( From 04d00a9acde086db5d0890b92f8d2833143cab42 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Jul 2022 12:59:43 -0700 Subject: [PATCH 2906/3516] Remove learn more URL from Home Assistant alerts (#75838) --- .../homeassistant_alerts/__init__.py | 5 +-- .../fixtures/alerts_no_url.json | 34 ------------------- .../homeassistant_alerts/test_init.py | 15 +++----- 3 files changed, 5 insertions(+), 49 deletions(-) delete mode 100644 tests/components/homeassistant_alerts/fixtures/alerts_no_url.json diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index 1aedd6c5419..12ba4dce8ba 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -75,7 +75,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, issue_id, is_fixable=False, - learn_more_url=alert.alert_url, severity=IssueSeverity.WARNING, translation_key="alert", translation_placeholders={ @@ -112,7 +111,6 @@ class IntegrationAlert: integration: str filename: str date_updated: str | None - alert_url: str | None @property def issue_id(self) -> str: @@ -147,7 +145,7 @@ class AlertUpdateCoordinator(DataUpdateCoordinator[dict[str, IntegrationAlert]]) result = {} for alert in alerts: - if "alert_url" not in alert or "integrations" not in alert: + if "integrations" not in alert: continue if "homeassistant" in alert: @@ -177,7 +175,6 @@ class AlertUpdateCoordinator(DataUpdateCoordinator[dict[str, IntegrationAlert]]) integration=integration["package"], filename=alert["filename"], date_updated=alert.get("date_updated"), - alert_url=alert["alert_url"], ) result[integration_alert.issue_id] = integration_alert diff --git a/tests/components/homeassistant_alerts/fixtures/alerts_no_url.json b/tests/components/homeassistant_alerts/fixtures/alerts_no_url.json deleted file mode 100644 index 89f277cf69b..00000000000 --- a/tests/components/homeassistant_alerts/fixtures/alerts_no_url.json +++ /dev/null @@ -1,34 +0,0 @@ -[ - { - "title": "Dark Sky API closed for new users", - "created": "2020-03-31T14:40:00.000Z", - "integrations": [ - { - "package": "darksky" - } - ], - "github_issue": "https://github.com/home-assistant/home-assistant.io/pull/12591", - "homeassistant": { - "package": "homeassistant", - "affected_from_version": "0.30" - }, - "filename": "dark_sky.markdown", - "alert_url": "https://alerts.home-assistant.io/#dark_sky.markdown" - }, - { - "title": "Hikvision Security Vulnerability", - "created": "2021-09-20T22:08:00.000Z", - "integrations": [ - { - "package": "hikvision" - }, - { - "package": "hikvisioncam" - } - ], - "filename": "hikvision.markdown", - "homeassistant": { - "package": "homeassistant" - } - } -] diff --git a/tests/components/homeassistant_alerts/test_init.py b/tests/components/homeassistant_alerts/test_init.py index c0b6f471033..cb39fb73108 100644 --- a/tests/components/homeassistant_alerts/test_init.py +++ b/tests/components/homeassistant_alerts/test_init.py @@ -133,7 +133,7 @@ async def test_alerts( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", - "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "learn_more_url": None, "severity": "warning", "translation_key": "alert", "translation_placeholders": { @@ -149,13 +149,6 @@ async def test_alerts( @pytest.mark.parametrize( "ha_version, fixture, expected_alerts", ( - ( - "2022.7.0", - "alerts_no_url.json", - [ - ("dark_sky.markdown", "darksky"), - ], - ), ( "2022.7.0", "alerts_no_integrations.json", @@ -220,7 +213,7 @@ async def test_bad_alerts( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", - "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "learn_more_url": None, "severity": "warning", "translation_key": "alert", "translation_placeholders": { @@ -381,7 +374,7 @@ async def test_alerts_change( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", - "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "learn_more_url": None, "severity": "warning", "translation_key": "alert", "translation_placeholders": { @@ -420,7 +413,7 @@ async def test_alerts_change( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", - "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "learn_more_url": None, "severity": "warning", "translation_key": "alert", "translation_placeholders": { From 4ffd6fc4be7959753b930224a8b16d16b6eabf8d Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Wed, 27 Jul 2022 16:06:33 -0400 Subject: [PATCH 2907/3516] Add Insteon lock and load controller devices (#75632) --- homeassistant/components/insteon/const.py | 1 + homeassistant/components/insteon/ipdb.py | 5 + homeassistant/components/insteon/lock.py | 49 ++++++++ .../components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/insteon/mock_devices.py | 21 +++- tests/components/insteon/test_lock.py | 109 ++++++++++++++++++ 8 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/insteon/lock.py create mode 100644 tests/components/insteon/test_lock.py diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py index fb7b2387d73..5337ccd36c3 100644 --- a/homeassistant/components/insteon/const.py +++ b/homeassistant/components/insteon/const.py @@ -44,6 +44,7 @@ INSTEON_PLATFORMS = [ Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.SWITCH, ] diff --git a/homeassistant/components/insteon/ipdb.py b/homeassistant/components/insteon/ipdb.py index 6866e052368..7f4ff92380f 100644 --- a/homeassistant/components/insteon/ipdb.py +++ b/homeassistant/components/insteon/ipdb.py @@ -1,5 +1,6 @@ """Utility methods for the Insteon platform.""" from pyinsteon.device_types import ( + AccessControl_Morningstar, ClimateControl_Thermostat, ClimateControl_WirelessThermostat, DimmableLightingControl, @@ -12,6 +13,7 @@ from pyinsteon.device_types import ( DimmableLightingControl_OutletLinc, DimmableLightingControl_SwitchLinc, DimmableLightingControl_ToggleLinc, + EnergyManagement_LoadController, GeneralController_ControlLinc, GeneralController_MiniRemote_4, GeneralController_MiniRemote_8, @@ -44,11 +46,13 @@ from homeassistant.components.climate import DOMAIN as CLIMATE from homeassistant.components.cover import DOMAIN as COVER from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.light import DOMAIN as LIGHT +from homeassistant.components.lock import DOMAIN as LOCK from homeassistant.components.switch import DOMAIN as SWITCH from .const import ON_OFF_EVENTS DEVICE_PLATFORM = { + AccessControl_Morningstar: {LOCK: [1]}, DimmableLightingControl: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_DinRail: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_FanLinc: {LIGHT: [1], FAN: [2], ON_OFF_EVENTS: [1, 2]}, @@ -67,6 +71,7 @@ DEVICE_PLATFORM = { DimmableLightingControl_OutletLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_SwitchLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_ToggleLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, + EnergyManagement_LoadController: {SWITCH: [1], BINARY_SENSOR: [2]}, GeneralController_ControlLinc: {ON_OFF_EVENTS: [1]}, GeneralController_MiniRemote_4: {ON_OFF_EVENTS: range(1, 5)}, GeneralController_MiniRemote_8: {ON_OFF_EVENTS: range(1, 9)}, diff --git a/homeassistant/components/insteon/lock.py b/homeassistant/components/insteon/lock.py new file mode 100644 index 00000000000..17a7cf20111 --- /dev/null +++ b/homeassistant/components/insteon/lock.py @@ -0,0 +1,49 @@ +"""Support for INSTEON locks.""" + +from typing import Any + +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import SIGNAL_ADD_ENTITIES +from .insteon_entity import InsteonEntity +from .utils import async_add_insteon_entities + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Insteon locks from a config entry.""" + + @callback + def async_add_insteon_lock_entities(discovery_info=None): + """Add the Insteon entities for the platform.""" + async_add_insteon_entities( + hass, LOCK_DOMAIN, InsteonLockEntity, async_add_entities, discovery_info + ) + + signal = f"{SIGNAL_ADD_ENTITIES}_{LOCK_DOMAIN}" + async_dispatcher_connect(hass, signal, async_add_insteon_lock_entities) + async_add_insteon_lock_entities() + + +class InsteonLockEntity(InsteonEntity, LockEntity): + """A Class for an Insteon lock entity.""" + + @property + def is_locked(self) -> bool: + """Return the boolean response if the node is on.""" + return bool(self._insteon_device_group.value) + + async def async_lock(self, **kwargs: Any) -> None: + """Lock the device.""" + await self._insteon_device.async_lock() + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock the device.""" + await self._insteon_device.async_unlock() diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index c48d502c16e..577383e8976 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/insteon", "dependencies": ["http", "websocket_api"], "requirements": [ - "pyinsteon==1.1.3", + "pyinsteon==1.2.0", "insteon-frontend-home-assistant==0.2.0" ], "codeowners": ["@teharris1"], diff --git a/requirements_all.txt b/requirements_all.txt index efc7282d703..04e86f3ab7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1572,7 +1572,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.3 +pyinsteon==1.2.0 # homeassistant.components.intesishome pyintesishome==1.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03bfde54c5c..c8ac231bea1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1079,7 +1079,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.3 +pyinsteon==1.2.0 # homeassistant.components.ipma pyipma==2.0.5 diff --git a/tests/components/insteon/mock_devices.py b/tests/components/insteon/mock_devices.py index ef64b1e0969..417769d6696 100644 --- a/tests/components/insteon/mock_devices.py +++ b/tests/components/insteon/mock_devices.py @@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from pyinsteon.address import Address from pyinsteon.constants import ALDBStatus, ResponseStatus from pyinsteon.device_types import ( + AccessControl_Morningstar, DimmableLightingControl_KeypadLinc_8, GeneralController_RemoteLinc, Hub, @@ -59,12 +60,13 @@ class MockDevices: async def async_load(self, *args, **kwargs): """Load the mock devices.""" - if self._connected: + if self._connected and not self._devices: addr0 = Address("AA.AA.AA") addr1 = Address("11.11.11") addr2 = Address("22.22.22") addr3 = Address("33.33.33") addr4 = Address("44.44.44") + addr5 = Address("55.55.55") self._devices[addr0] = Hub(addr0, 0x03, 0x00, 0x00, "Hub AA.AA.AA", "0") self._devices[addr1] = MockSwitchLinc( addr1, 0x02, 0x00, 0x00, "Device 11.11.11", "1" @@ -78,9 +80,12 @@ class MockDevices: self._devices[addr4] = SensorsActuators_IOLink( addr4, 0x07, 0x00, 0x00, "Device 44.44.44", "4" ) + self._devices[addr5] = AccessControl_Morningstar( + addr5, 0x0F, 0x0A, 0x00, "Device 55.55.55", "5" + ) for device in [ - self._devices[addr] for addr in [addr1, addr2, addr3, addr4] + self._devices[addr] for addr in [addr1, addr2, addr3, addr4, addr5] ]: device.async_read_config = AsyncMock() device.aldb.async_write = AsyncMock() @@ -99,7 +104,9 @@ class MockDevices: return_value=ResponseStatus.SUCCESS ) - for device in [self._devices[addr] for addr in [addr2, addr3, addr4]]: + for device in [ + self._devices[addr] for addr in [addr2, addr3, addr4, addr5] + ]: device.async_status = AsyncMock() self._devices[addr1].async_status = AsyncMock(side_effect=AttributeError) self._devices[addr0].aldb.async_load = AsyncMock() @@ -117,6 +124,12 @@ class MockDevices: return_value=ResponseStatus.FAILURE ) + self._devices[addr5].async_lock = AsyncMock( + return_value=ResponseStatus.SUCCESS + ) + self._devices[addr5].async_unlock = AsyncMock( + return_value=ResponseStatus.SUCCESS + ) self.modem = self._devices[addr0] self.modem.async_read_config = AsyncMock() @@ -155,6 +168,6 @@ class MockDevices: yield address await asyncio.sleep(0.01) - def subscribe(self, listener): + def subscribe(self, listener, force_strong_ref=False): """Mock the subscribe function.""" subscribe_topic(listener, DEVICE_LIST_CHANGED) diff --git a/tests/components/insteon/test_lock.py b/tests/components/insteon/test_lock.py new file mode 100644 index 00000000000..6f847543a9f --- /dev/null +++ b/tests/components/insteon/test_lock.py @@ -0,0 +1,109 @@ +"""Tests for the Insteon lock.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components import insteon +from homeassistant.components.insteon import ( + DOMAIN, + insteon_entity, + utils as insteon_utils, +) +from homeassistant.components.lock import ( # SERVICE_LOCK,; SERVICE_UNLOCK, + DOMAIN as LOCK_DOMAIN, +) +from homeassistant.const import ( # ATTR_ENTITY_ID,; + EVENT_HOMEASSISTANT_STOP, + STATE_LOCKED, + STATE_UNLOCKED, + Platform, +) +from homeassistant.helpers import entity_registry as er + +from .const import MOCK_USER_INPUT_PLM +from .mock_devices import MockDevices + +from tests.common import MockConfigEntry + +devices = MockDevices() + + +@pytest.fixture(autouse=True) +def lock_platform_only(): + """Only setup the lock and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.insteon.INSTEON_PLATFORMS", + (Platform.LOCK,), + ): + yield + + +@pytest.fixture(autouse=True) +def patch_setup_and_devices(): + """Patch the Insteon setup process and devices.""" + with patch.object(insteon, "async_connect", new=mock_connection), patch.object( + insteon, "async_close" + ), patch.object(insteon, "devices", devices), patch.object( + insteon_utils, "devices", devices + ), patch.object( + insteon_entity, "devices", devices + ): + yield + + +async def mock_connection(*args, **kwargs): + """Return a successful connection.""" + return True + + +async def test_lock_lock(hass): + """Test locking an Insteon lock device.""" + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT_PLM) + config_entry.add_to_hass(hass) + registry_entity = er.async_get(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + try: + lock = registry_entity.async_get("lock.device_55_55_55_55_55_55") + state = hass.states.get(lock.entity_id) + assert state.state is STATE_UNLOCKED + + # lock via UI + await hass.services.async_call( + LOCK_DOMAIN, "lock", {"entity_id": lock.entity_id}, blocking=True + ) + assert devices["55.55.55"].async_lock.call_count == 1 + finally: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + +async def test_lock_unlock(hass): + """Test locking an Insteon lock device.""" + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT_PLM) + config_entry.add_to_hass(hass) + registry_entity = er.async_get(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + devices["55.55.55"].groups[1].set_value(255) + + try: + lock = registry_entity.async_get("lock.device_55_55_55_55_55_55") + state = hass.states.get(lock.entity_id) + + assert state.state is STATE_LOCKED + + # lock via UI + await hass.services.async_call( + LOCK_DOMAIN, "unlock", {"entity_id": lock.entity_id}, blocking=True + ) + assert devices["55.55.55"].async_unlock.call_count == 1 + finally: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() From b0f877eca2c30d189647dba54109a78556781c69 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Jul 2022 13:53:51 -0700 Subject: [PATCH 2908/3516] Add issue_domain to repairs (#75839) --- .../components/homeassistant_alerts/__init__.py | 1 + homeassistant/components/repairs/issue_handler.py | 2 ++ homeassistant/components/repairs/issue_registry.py | 6 ++++++ tests/components/demo/test_init.py | 5 +++++ tests/components/homeassistant_alerts/test_init.py | 4 ++++ tests/components/repairs/test_init.py | 13 +++++++++++++ tests/components/repairs/test_websocket_api.py | 7 +++++++ 7 files changed, 38 insertions(+) diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index 12ba4dce8ba..d405b9e257d 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -75,6 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, issue_id, is_fixable=False, + issue_domain=alert.integration, severity=IssueSeverity.WARNING, translation_key="alert", translation_placeholders={ diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index 8eff4ac64fe..c139026ec48 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -86,6 +86,7 @@ def async_create_issue( domain: str, issue_id: str, *, + issue_domain: str | None = None, breaks_in_ha_version: str | None = None, is_fixable: bool, learn_more_url: str | None = None, @@ -106,6 +107,7 @@ def async_create_issue( issue_registry.async_get_or_create( domain, issue_id, + issue_domain=issue_domain, breaks_in_ha_version=breaks_in_ha_version, is_fixable=is_fixable, learn_more_url=learn_more_url, diff --git a/homeassistant/components/repairs/issue_registry.py b/homeassistant/components/repairs/issue_registry.py index 5c459309cc0..bd201f1007c 100644 --- a/homeassistant/components/repairs/issue_registry.py +++ b/homeassistant/components/repairs/issue_registry.py @@ -30,6 +30,8 @@ class IssueEntry: domain: str is_fixable: bool | None issue_id: str + # Used if an integration creates issues for other integrations (ie alerts) + issue_domain: str | None learn_more_url: str | None severity: IssueSeverity | None translation_key: str | None @@ -58,6 +60,7 @@ class IssueRegistry: domain: str, issue_id: str, *, + issue_domain: str | None = None, breaks_in_ha_version: str | None = None, is_fixable: bool, learn_more_url: str | None = None, @@ -75,6 +78,7 @@ class IssueRegistry: dismissed_version=None, domain=domain, is_fixable=is_fixable, + issue_domain=issue_domain, issue_id=issue_id, learn_more_url=learn_more_url, severity=severity, @@ -93,6 +97,7 @@ class IssueRegistry: active=True, breaks_in_ha_version=breaks_in_ha_version, is_fixable=is_fixable, + issue_domain=issue_domain, learn_more_url=learn_more_url, severity=severity, translation_key=translation_key, @@ -155,6 +160,7 @@ class IssueRegistry: domain=issue["domain"], is_fixable=None, issue_id=issue["issue_id"], + issue_domain=None, learn_more_url=None, severity=None, translation_key=None, diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 85ff2a16405..5b322cb776f 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -95,6 +95,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": False, "issue_id": "transmogrifier_deprecated", + "issue_domain": None, "learn_more_url": "https://en.wiktionary.org/wiki/transmogrifier", "severity": "warning", "translation_key": "transmogrifier_deprecated", @@ -108,6 +109,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": True, "issue_id": "out_of_blinker_fluid", + "issue_domain": None, "learn_more_url": "https://www.youtube.com/watch?v=b9rntRxLlbU", "severity": "critical", "translation_key": "out_of_blinker_fluid", @@ -121,6 +123,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": False, "issue_id": "unfixable_problem", + "issue_domain": None, "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "severity": "warning", "translation_key": "unfixable_problem", @@ -180,6 +183,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": False, "issue_id": "transmogrifier_deprecated", + "issue_domain": None, "learn_more_url": "https://en.wiktionary.org/wiki/transmogrifier", "severity": "warning", "translation_key": "transmogrifier_deprecated", @@ -193,6 +197,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": False, "issue_id": "unfixable_problem", + "issue_domain": None, "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "severity": "warning", "translation_key": "unfixable_problem", diff --git a/tests/components/homeassistant_alerts/test_init.py b/tests/components/homeassistant_alerts/test_init.py index cb39fb73108..a0fb2e8557d 100644 --- a/tests/components/homeassistant_alerts/test_init.py +++ b/tests/components/homeassistant_alerts/test_init.py @@ -133,6 +133,7 @@ async def test_alerts( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", + "issue_domain": integration, "learn_more_url": None, "severity": "warning", "translation_key": "alert", @@ -213,6 +214,7 @@ async def test_bad_alerts( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", + "issue_domain": integration, "learn_more_url": None, "severity": "warning", "translation_key": "alert", @@ -374,6 +376,7 @@ async def test_alerts_change( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", + "issue_domain": integration, "learn_more_url": None, "severity": "warning", "translation_key": "alert", @@ -413,6 +416,7 @@ async def test_alerts_change( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", + "issue_domain": integration, "learn_more_url": None, "severity": "warning", "translation_key": "alert", diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py index 2f82a084968..d70f6c6e11d 100644 --- a/tests/components/repairs/test_init.py +++ b/tests/components/repairs/test_init.py @@ -85,6 +85,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -97,6 +98,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: issues[0]["issue_id"], breaks_in_ha_version=issues[0]["breaks_in_ha_version"], is_fixable=issues[0]["is_fixable"], + issue_domain="my_issue_domain", learn_more_url="blablabla", severity=issues[0]["severity"], translation_key=issues[0]["translation_key"], @@ -113,6 +115,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: dismissed_version=None, ignored=False, learn_more_url="blablabla", + issue_domain="my_issue_domain", ) @@ -206,6 +209,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -226,6 +230,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -245,6 +250,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=ha_version, ignored=True, + issue_domain=None, ) for issue in issues ] @@ -264,6 +270,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=ha_version, ignored=True, + issue_domain=None, ) for issue in issues ] @@ -292,6 +299,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: dismissed_version=ha_version, ignored=True, learn_more_url="blablabla", + issue_domain=None, ) # Unignore the same issue @@ -309,6 +317,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: dismissed_version=None, ignored=False, learn_more_url="blablabla", + issue_domain=None, ) for issue in issues ] @@ -359,6 +368,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -378,6 +388,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -428,6 +439,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non created="2022-07-19T08:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -501,6 +513,7 @@ async def test_sync_methods( "ignored": False, "is_fixable": True, "issue_id": "sync_issue", + "issue_domain": None, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index 73d1898fcb7..d778b043832 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -61,6 +61,7 @@ async def create_issues(hass, ws_client): created=ANY, dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -154,6 +155,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: created=ANY, dismissed_version=ha_version, ignored=True, + issue_domain=None, ) for issue in issues ] @@ -183,6 +185,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: created=ANY, dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -226,6 +229,7 @@ async def test_fix_non_existing_issue( created=ANY, dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -383,6 +387,7 @@ async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> "dismissed_version": None, "domain": "test", "issue_id": "issue_3_inactive", + "issue_domain": None, }, ] }, @@ -404,6 +409,7 @@ async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> "domain": "test", "is_fixable": True, "issue_id": "issue_1", + "issue_domain": None, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -414,6 +420,7 @@ async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> "domain": "test", "is_fixable": False, "issue_id": "issue_2", + "issue_domain": None, "learn_more_url": "https://theuselessweb.com/abc", "severity": "other", "translation_key": "even_worse", From ef9142f37903be4117eab1e0dfa18b0ffffcba90 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 22:58:58 +0200 Subject: [PATCH 2909/3516] Fix temperature unit in evohome (#75842) --- homeassistant/components/evohome/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 841619af6f1..c1a630d0d05 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -126,6 +126,8 @@ async def async_setup_platform( class EvoClimateEntity(EvoDevice, ClimateEntity): """Base for an evohome Climate device.""" + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, evo_broker, evo_device) -> None: """Initialize a Climate device.""" super().__init__(evo_broker, evo_device) @@ -316,7 +318,6 @@ class EvoController(EvoClimateEntity): _attr_icon = "mdi:thermostat" _attr_precision = PRECISION_TENTHS - _attr_temperature_unit = TEMP_CELSIUS def __init__(self, evo_broker, evo_device) -> None: """Initialize a Honeywell TCC Controller/Location.""" From b0c17d67df92af09da7119f24cd87e066a5ffe5d Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Wed, 27 Jul 2022 16:39:39 -0500 Subject: [PATCH 2910/3516] Add Leviton as a supported brand of ZwaveJS (#75729) Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/manifest.json | 5 ++++- homeassistant/generated/supported_brands.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index ed969e58042..29be66cd024 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -30,5 +30,8 @@ } ], "zeroconf": ["_zwave-js-server._tcp.local."], - "loggers": ["zwave_js_server"] + "loggers": ["zwave_js_server"], + "supported_brands": { + "leviton_z_wave": "Leviton Z-Wave" + } } diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py index 589e0462cf7..4e151f5578d 100644 --- a/homeassistant/generated/supported_brands.py +++ b/homeassistant/generated/supported_brands.py @@ -11,5 +11,6 @@ HAS_SUPPORTED_BRANDS = ( "motion_blinds", "overkiz", "renault", - "wemo" + "wemo", + "zwave_js" ) From 44f1d928907727474afed6aeec70095f6b4c46b6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Jul 2022 17:40:44 -0400 Subject: [PATCH 2911/3516] Add new zwave_js notification parameters (#75796) --- homeassistant/components/zwave_js/__init__.py | 5 +++++ homeassistant/components/zwave_js/const.py | 2 ++ tests/components/zwave_js/test_device_trigger.py | 8 +++++++- tests/components/zwave_js/test_events.py | 13 +++++++++++-- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index fe616e8bdb9..482f635da65 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -43,12 +43,14 @@ from .const import ( ATTR_COMMAND_CLASS, ATTR_COMMAND_CLASS_NAME, ATTR_DATA_TYPE, + ATTR_DATA_TYPE_LABEL, ATTR_DIRECTION, ATTR_ENDPOINT, ATTR_EVENT, ATTR_EVENT_DATA, ATTR_EVENT_LABEL, ATTR_EVENT_TYPE, + ATTR_EVENT_TYPE_LABEL, ATTR_HOME_ID, ATTR_LABEL, ATTR_NODE_ID, @@ -476,7 +478,9 @@ async def setup_driver( # noqa: C901 { ATTR_COMMAND_CLASS_NAME: "Entry Control", ATTR_EVENT_TYPE: notification.event_type, + ATTR_EVENT_TYPE_LABEL: notification.event_type_label, ATTR_DATA_TYPE: notification.data_type, + ATTR_DATA_TYPE_LABEL: notification.data_type_label, ATTR_EVENT_DATA: notification.event_data, } ) @@ -505,6 +509,7 @@ async def setup_driver( # noqa: C901 { ATTR_COMMAND_CLASS_NAME: "Multilevel Switch", ATTR_EVENT_TYPE: notification.event_type, + ATTR_EVENT_TYPE_LABEL: notification.event_type_label, ATTR_DIRECTION: notification.direction, } ) diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py index 1fd8e3e9d14..3e0bdb9c3f6 100644 --- a/homeassistant/components/zwave_js/const.py +++ b/homeassistant/components/zwave_js/const.py @@ -56,6 +56,8 @@ ATTR_OPTIONS = "options" ATTR_TEST_NODE_ID = "test_node_id" ATTR_STATUS = "status" ATTR_ACKNOWLEDGED_FRAMES = "acknowledged_frames" +ATTR_EVENT_TYPE_LABEL = "event_type_label" +ATTR_DATA_TYPE_LABEL = "data_type_label" ATTR_NODE = "node" ATTR_ZWAVE_VALUE = "zwave_value" diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py index decac4cd50f..859164aa4c3 100644 --- a/tests/components/zwave_js/test_device_trigger.py +++ b/tests/components/zwave_js/test_device_trigger.py @@ -252,7 +252,13 @@ async def test_if_entry_control_notification_fires( "event": "notification", "nodeId": node.node_id, "ccId": 111, - "args": {"eventType": 5, "dataType": 2, "eventData": "555"}, + "args": { + "eventType": 5, + "eventTypeLabel": "label 1", + "dataType": 2, + "dataTypeLabel": "label 2", + "eventData": "555", + }, }, ) node.receive_event(event) diff --git a/tests/components/zwave_js/test_events.py b/tests/components/zwave_js/test_events.py index 19f38d4aa57..8552f69936d 100644 --- a/tests/components/zwave_js/test_events.py +++ b/tests/components/zwave_js/test_events.py @@ -182,7 +182,13 @@ async def test_notifications(hass, hank_binary_switch, integration, client): "event": "notification", "nodeId": 32, "ccId": 111, - "args": {"eventType": 5, "dataType": 2, "eventData": "555"}, + "args": { + "eventType": 5, + "eventTypeLabel": "test1", + "dataType": 2, + "dataTypeLabel": "test2", + "eventData": "555", + }, }, ) @@ -193,7 +199,9 @@ async def test_notifications(hass, hank_binary_switch, integration, client): assert events[1].data["home_id"] == client.driver.controller.home_id assert events[1].data["node_id"] == 32 assert events[1].data["event_type"] == 5 + assert events[1].data["event_type_label"] == "test1" assert events[1].data["data_type"] == 2 + assert events[1].data["data_type_label"] == "test2" assert events[1].data["event_data"] == "555" assert events[1].data["command_class"] == CommandClass.ENTRY_CONTROL assert events[1].data["command_class_name"] == "Entry Control" @@ -206,7 +214,7 @@ async def test_notifications(hass, hank_binary_switch, integration, client): "event": "notification", "nodeId": 32, "ccId": 38, - "args": {"eventType": 4, "direction": "up"}, + "args": {"eventType": 4, "eventTypeLabel": "test1", "direction": "up"}, }, ) @@ -217,6 +225,7 @@ async def test_notifications(hass, hank_binary_switch, integration, client): assert events[2].data["home_id"] == client.driver.controller.home_id assert events[2].data["node_id"] == 32 assert events[2].data["event_type"] == 4 + assert events[2].data["event_type_label"] == "test1" assert events[2].data["direction"] == "up" assert events[2].data["command_class"] == CommandClass.SWITCH_MULTILEVEL assert events[2].data["command_class_name"] == "Multilevel Switch" From 4aa6300b8bd294727a2d4ee8086b5e6a90ff8460 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 27 Jul 2022 17:42:17 -0400 Subject: [PATCH 2912/3516] Update zwave_js WS API names (#75797) --- homeassistant/components/zwave_js/api.py | 16 +++++++++------- tests/components/zwave_js/test_api.py | 22 +++++++++++----------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 28afdeb4db8..1a9435509f2 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -414,7 +414,9 @@ def async_register_api(hass: HomeAssistant) -> None: ) websocket_api.async_register_command(hass, websocket_data_collection_status) websocket_api.async_register_command(hass, websocket_abort_firmware_update) - websocket_api.async_register_command(hass, websocket_get_firmware_update_progress) + websocket_api.async_register_command( + hass, websocket_is_node_firmware_update_in_progress + ) websocket_api.async_register_command( hass, websocket_subscribe_firmware_update_status ) @@ -422,7 +424,7 @@ def async_register_api(hass: HomeAssistant) -> None: hass, websocket_get_firmware_update_capabilities ) websocket_api.async_register_command( - hass, websocket_get_any_firmware_update_progress + hass, websocket_is_any_ota_firmware_update_in_progress ) websocket_api.async_register_command(hass, websocket_check_for_config_updates) websocket_api.async_register_command(hass, websocket_install_config_update) @@ -1874,20 +1876,20 @@ async def websocket_abort_firmware_update( @websocket_api.require_admin @websocket_api.websocket_command( { - vol.Required(TYPE): "zwave_js/get_firmware_update_progress", + vol.Required(TYPE): "zwave_js/is_node_firmware_update_in_progress", vol.Required(DEVICE_ID): str, } ) @websocket_api.async_response @async_handle_failed_command @async_get_node -async def websocket_get_firmware_update_progress( +async def websocket_is_node_firmware_update_in_progress( hass: HomeAssistant, connection: ActiveConnection, msg: dict, node: Node, ) -> None: - """Get whether firmware update is in progress.""" + """Get whether firmware update is in progress for given node.""" connection.send_result(msg[ID], await node.async_is_firmware_update_in_progress()) @@ -1995,14 +1997,14 @@ async def websocket_get_firmware_update_capabilities( @websocket_api.require_admin @websocket_api.websocket_command( { - vol.Required(TYPE): "zwave_js/get_any_firmware_update_progress", + vol.Required(TYPE): "zwave_js/is_any_ota_firmware_update_in_progress", vol.Required(ENTRY_ID): str, } ) @websocket_api.async_response @async_handle_failed_command @async_get_entry -async def websocket_get_any_firmware_update_progress( +async def websocket_is_any_ota_firmware_update_in_progress( hass: HomeAssistant, connection: ActiveConnection, msg: dict, diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 72d8fce7424..68618edfbeb 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -3437,10 +3437,10 @@ async def test_abort_firmware_update( assert msg["error"]["code"] == ERR_NOT_FOUND -async def test_get_firmware_update_progress( +async def test_is_node_firmware_update_in_progress( hass, client, multisensor_6, integration, hass_ws_client ): - """Test that the get_firmware_update_progress WS API call works.""" + """Test that the is_firmware_update_in_progress WS API call works.""" entry = integration ws_client = await hass_ws_client(hass) device = get_device(hass, multisensor_6) @@ -3449,7 +3449,7 @@ async def test_get_firmware_update_progress( await ws_client.send_json( { ID: 1, - TYPE: "zwave_js/get_firmware_update_progress", + TYPE: "zwave_js/is_node_firmware_update_in_progress", DEVICE_ID: device.id, } ) @@ -3470,7 +3470,7 @@ async def test_get_firmware_update_progress( await ws_client.send_json( { ID: 2, - TYPE: "zwave_js/get_firmware_update_progress", + TYPE: "zwave_js/is_node_firmware_update_in_progress", DEVICE_ID: device.id, } ) @@ -3487,7 +3487,7 @@ async def test_get_firmware_update_progress( await ws_client.send_json( { ID: 3, - TYPE: "zwave_js/get_firmware_update_progress", + TYPE: "zwave_js/is_node_firmware_update_in_progress", DEVICE_ID: device.id, } ) @@ -3723,10 +3723,10 @@ async def test_get_firmware_update_capabilities( assert msg["error"]["code"] == ERR_NOT_FOUND -async def test_get_any_firmware_update_progress( +async def test_is_any_ota_firmware_update_in_progress( hass, client, integration, hass_ws_client ): - """Test that the get_any_firmware_update_progress WS API call works.""" + """Test that the is_any_ota_firmware_update_in_progress WS API call works.""" entry = integration ws_client = await hass_ws_client(hass) @@ -3734,7 +3734,7 @@ async def test_get_any_firmware_update_progress( await ws_client.send_json( { ID: 1, - TYPE: "zwave_js/get_any_firmware_update_progress", + TYPE: "zwave_js/is_any_ota_firmware_update_in_progress", ENTRY_ID: entry.entry_id, } ) @@ -3754,7 +3754,7 @@ async def test_get_any_firmware_update_progress( await ws_client.send_json( { ID: 2, - TYPE: "zwave_js/get_any_firmware_update_progress", + TYPE: "zwave_js/is_any_ota_firmware_update_in_progress", ENTRY_ID: entry.entry_id, } ) @@ -3771,7 +3771,7 @@ async def test_get_any_firmware_update_progress( await ws_client.send_json( { ID: 3, - TYPE: "zwave_js/get_any_firmware_update_progress", + TYPE: "zwave_js/is_any_ota_firmware_update_in_progress", ENTRY_ID: entry.entry_id, } ) @@ -3784,7 +3784,7 @@ async def test_get_any_firmware_update_progress( await ws_client.send_json( { ID: 4, - TYPE: "zwave_js/get_any_firmware_update_progress", + TYPE: "zwave_js/is_any_ota_firmware_update_in_progress", ENTRY_ID: "invalid_entry", } ) From f07d64e7dbe4c36cc649cede845efaf06b48a141 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 23:49:22 +0200 Subject: [PATCH 2913/3516] Raise YAML removal issue for Xbox (#75843) --- homeassistant/components/xbox/__init__.py | 12 +++++++++++- homeassistant/components/xbox/manifest.json | 2 +- homeassistant/components/xbox/strings.json | 6 ++++++ homeassistant/components/xbox/translations/en.json | 6 ++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index 6e8492e45ca..c49fd55e8c8 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -21,6 +21,7 @@ from xbox.webapi.api.provider.smartglass.models import ( ) from homeassistant.components import application_credentials +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform from homeassistant.core import HomeAssistant @@ -74,9 +75,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: config[DOMAIN][CONF_CLIENT_ID], config[DOMAIN][CONF_CLIENT_SECRET] ), ) + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) _LOGGER.warning( "Configuration of Xbox integration in YAML is deprecated and " - "will be removed in a future release; Your existing configuration " + "will be removed in Home Assistant 2022.9.; Your existing configuration " "(including OAuth Application Credentials) has been imported into " "the UI automatically and can be safely removed from your " "configuration.yaml file" diff --git a/homeassistant/components/xbox/manifest.json b/homeassistant/components/xbox/manifest.json index 5adfa54a901..8857a55d66d 100644 --- a/homeassistant/components/xbox/manifest.json +++ b/homeassistant/components/xbox/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xbox", "requirements": ["xbox-webapi==2.0.11"], - "dependencies": ["auth", "application_credentials"], + "dependencies": ["auth", "application_credentials", "repairs"], "codeowners": ["@hunterjm"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/xbox/strings.json b/homeassistant/components/xbox/strings.json index accd6775941..68af0176fa8 100644 --- a/homeassistant/components/xbox/strings.json +++ b/homeassistant/components/xbox/strings.json @@ -13,5 +13,11 @@ "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Xbox YAML configuration is being removed", + "description": "Configuring the Xbox in configuration.yaml is being removed in Home Assistant 2022.9.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/xbox/translations/en.json b/homeassistant/components/xbox/translations/en.json index 0bb1266bded..2ef065af458 100644 --- a/homeassistant/components/xbox/translations/en.json +++ b/homeassistant/components/xbox/translations/en.json @@ -13,5 +13,11 @@ "title": "Pick Authentication Method" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring the Xbox in configuration.yaml is being removed in Home Assistant 2022.9.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Xbox YAML configuration is being removed" + } } } \ No newline at end of file From 997f03d0eab98fe25442fb888f44e660f3b104fe Mon Sep 17 00:00:00 2001 From: Rolf Berkenbosch <30292281+rolfberkenbosch@users.noreply.github.com> Date: Wed, 27 Jul 2022 23:50:41 +0200 Subject: [PATCH 2914/3516] Fix fetching MeteoAlarm XML data (#75840) --- homeassistant/components/meteoalarm/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/meteoalarm/manifest.json b/homeassistant/components/meteoalarm/manifest.json index 35333f6ea01..9a3da54d34f 100644 --- a/homeassistant/components/meteoalarm/manifest.json +++ b/homeassistant/components/meteoalarm/manifest.json @@ -2,7 +2,7 @@ "domain": "meteoalarm", "name": "MeteoAlarm", "documentation": "https://www.home-assistant.io/integrations/meteoalarm", - "requirements": ["meteoalertapi==0.2.0"], + "requirements": ["meteoalertapi==0.3.0"], "codeowners": ["@rolfberkenbosch"], "iot_class": "cloud_polling", "loggers": ["meteoalertapi"] diff --git a/requirements_all.txt b/requirements_all.txt index 04e86f3ab7a..b4b84a84fd1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1028,7 +1028,7 @@ meater-python==0.0.8 messagebird==1.2.0 # homeassistant.components.meteoalarm -meteoalertapi==0.2.0 +meteoalertapi==0.3.0 # homeassistant.components.meteo_france meteofrance-api==1.0.2 From 0317cbb388e685ec00d77a49849693c418aa7ee4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 28 Jul 2022 00:25:05 +0000 Subject: [PATCH 2915/3516] [ci skip] Translation update --- .../components/ambee/translations/de.json | 6 ++++++ .../components/ambee/translations/it.json | 6 ++++++ .../components/ambee/translations/pl.json | 6 ++++++ .../components/ambee/translations/pt-BR.json | 6 ++++++ .../ambee/translations/zh-Hant.json | 6 ++++++ .../components/anthemav/translations/de.json | 6 ++++++ .../components/anthemav/translations/it.json | 6 ++++++ .../anthemav/translations/pt-BR.json | 6 ++++++ .../anthemav/translations/zh-Hant.json | 6 ++++++ .../components/google/translations/it.json | 2 +- .../google/translations/zh-Hant.json | 2 +- .../homeassistant_alerts/translations/de.json | 8 ++++++++ .../homeassistant_alerts/translations/en.json | 8 ++++++++ .../homeassistant_alerts/translations/it.json | 8 ++++++++ .../homeassistant_alerts/translations/pl.json | 8 ++++++++ .../translations/pt-BR.json | 8 ++++++++ .../translations/zh-Hant.json | 8 ++++++++ .../lacrosse_view/translations/de.json | 20 +++++++++++++++++++ .../lacrosse_view/translations/en.json | 9 ++------- .../lacrosse_view/translations/it.json | 20 +++++++++++++++++++ .../lacrosse_view/translations/pt-BR.json | 20 +++++++++++++++++++ .../lacrosse_view/translations/zh-Hant.json | 20 +++++++++++++++++++ .../components/lyric/translations/de.json | 6 ++++++ .../components/lyric/translations/pt-BR.json | 6 ++++++ .../lyric/translations/zh-Hant.json | 6 ++++++ .../components/mitemp_bt/translations/de.json | 2 +- .../components/mitemp_bt/translations/it.json | 8 ++++++++ .../mitemp_bt/translations/pt-BR.json | 2 +- .../openalpr_local/translations/de.json | 8 ++++++++ .../openalpr_local/translations/it.json | 8 ++++++++ .../openalpr_local/translations/pl.json | 8 ++++++++ .../openalpr_local/translations/pt-BR.json | 8 ++++++++ .../openalpr_local/translations/zh-Hant.json | 8 ++++++++ .../radiotherm/translations/it.json | 3 ++- .../components/senz/translations/de.json | 6 ++++++ .../components/senz/translations/pt-BR.json | 6 ++++++ .../components/senz/translations/zh-Hant.json | 6 ++++++ .../soundtouch/translations/de.json | 6 ++++++ .../soundtouch/translations/it.json | 6 ++++++ .../soundtouch/translations/pt-BR.json | 6 ++++++ .../soundtouch/translations/zh-Hant.json | 6 ++++++ .../spotify/translations/zh-Hant.json | 4 ++-- .../steam_online/translations/zh-Hant.json | 4 ++-- .../uscis/translations/zh-Hant.json | 4 ++-- .../components/xbox/translations/it.json | 6 ++++++ .../components/xbox/translations/pt-BR.json | 6 ++++++ .../components/zha/translations/de.json | 2 ++ .../components/zha/translations/pl.json | 2 ++ .../components/zha/translations/pt-BR.json | 2 ++ .../components/zha/translations/zh-Hant.json | 2 ++ 50 files changed, 324 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/homeassistant_alerts/translations/de.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/en.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/it.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/pl.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/pt-BR.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/zh-Hant.json create mode 100644 homeassistant/components/lacrosse_view/translations/de.json create mode 100644 homeassistant/components/lacrosse_view/translations/it.json create mode 100644 homeassistant/components/lacrosse_view/translations/pt-BR.json create mode 100644 homeassistant/components/lacrosse_view/translations/zh-Hant.json create mode 100644 homeassistant/components/mitemp_bt/translations/it.json create mode 100644 homeassistant/components/openalpr_local/translations/de.json create mode 100644 homeassistant/components/openalpr_local/translations/it.json create mode 100644 homeassistant/components/openalpr_local/translations/pl.json create mode 100644 homeassistant/components/openalpr_local/translations/pt-BR.json create mode 100644 homeassistant/components/openalpr_local/translations/zh-Hant.json diff --git a/homeassistant/components/ambee/translations/de.json b/homeassistant/components/ambee/translations/de.json index 4359ab72349..8055ef5210f 100644 --- a/homeassistant/components/ambee/translations/de.json +++ b/homeassistant/components/ambee/translations/de.json @@ -24,5 +24,11 @@ "description": "Richte Ambee f\u00fcr die Integration mit Home Assistant ein." } } + }, + "issues": { + "pending_removal": { + "description": "Die Ambee-Integration ist dabei, aus Home Assistant entfernt zu werden und wird ab Home Assistant 2022.10 nicht mehr verf\u00fcgbar sein.\n\nDie Integration wird entfernt, weil Ambee seine kostenlosen (begrenzten) Konten entfernt hat und keine M\u00f6glichkeit mehr f\u00fcr regul\u00e4re Nutzer bietet, sich f\u00fcr einen kostenpflichtigen Plan anzumelden.\n\nEntferne den Ambee-Integrationseintrag aus deiner Instanz, um dieses Problem zu beheben.", + "title": "Die Ambee-Integration wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/it.json b/homeassistant/components/ambee/translations/it.json index fe97ce33686..db330a9b239 100644 --- a/homeassistant/components/ambee/translations/it.json +++ b/homeassistant/components/ambee/translations/it.json @@ -24,5 +24,11 @@ "description": "Configura Ambee per l'integrazione con Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "L'integrazione Ambee \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nL'integrazione \u00e8 stata rimossa, perch\u00e9 Ambee ha rimosso i loro account gratuiti (limitati) e non offre pi\u00f9 agli utenti regolari un modo per iscriversi a un piano a pagamento. \n\nRimuovi la voce di integrazione Ambee dalla tua istanza per risolvere questo problema.", + "title": "L'integrazione Ambee verr\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/pl.json b/homeassistant/components/ambee/translations/pl.json index d0b2225cc9a..255d402175d 100644 --- a/homeassistant/components/ambee/translations/pl.json +++ b/homeassistant/components/ambee/translations/pl.json @@ -24,5 +24,11 @@ "description": "Skonfiguruj Ambee, aby zintegrowa\u0107 go z Home Assistantem." } } + }, + "issues": { + "pending_removal": { + "description": "Integracja Ambee oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.10. \n\nIntegracja jest usuwana, poniewa\u017c Ambee usun\u0105\u0142 ich bezp\u0142atne (ograniczone) konta i nie zapewnia ju\u017c zwyk\u0142ym u\u017cytkownikom mo\u017cliwo\u015bci zarejestrowania si\u0119 w p\u0142atnym planie. \n\nUsu\u0144 integracj\u0119 Ambee z Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja Ambee zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/pt-BR.json b/homeassistant/components/ambee/translations/pt-BR.json index 2d960e17df2..3220de5104e 100644 --- a/homeassistant/components/ambee/translations/pt-BR.json +++ b/homeassistant/components/ambee/translations/pt-BR.json @@ -24,5 +24,11 @@ "description": "Configure o Ambee para integrar com o Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "A integra\u00e7\u00e3o do Ambee est\u00e1 com remo\u00e7\u00e3o pendente do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.10. \n\n A integra\u00e7\u00e3o est\u00e1 sendo removida, porque a Ambee removeu suas contas gratuitas (limitadas) e n\u00e3o oferece mais uma maneira de usu\u00e1rios regulares se inscreverem em um plano pago. \n\n Remova a entrada de integra\u00e7\u00e3o Ambee de sua inst\u00e2ncia para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o Ambee est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/zh-Hant.json b/homeassistant/components/ambee/translations/zh-Hant.json index 2e1de25fde2..ccebea49c6f 100644 --- a/homeassistant/components/ambee/translations/zh-Hant.json +++ b/homeassistant/components/ambee/translations/zh-Hant.json @@ -24,5 +24,11 @@ "description": "\u8a2d\u5b9a Ambee \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" } } + }, + "issues": { + "pending_removal": { + "description": "Ambee \u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531\u65bc Ambee \u79fb\u9664\u4e86\u5176\u514d\u8cbb\uff08\u6709\u9650\uff09\u5e33\u865f\u3001\u4e26\u4e14\u4e0d\u518d\u63d0\u4f9b\u4e00\u822c\u4f7f\u7528\u8005\u8a3b\u518a\u4ed8\u8cbb\u670d\u52d9\u3001\u6574\u5408\u5373\u5c07\u79fb\u9664\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Ambee \u6574\u5408\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/de.json b/homeassistant/components/anthemav/translations/de.json index 622384629fe..d751349b005 100644 --- a/homeassistant/components/anthemav/translations/de.json +++ b/homeassistant/components/anthemav/translations/de.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Anthem A/V-Receivern mit YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die Anthem A/V Receivers YAML Konfiguration aus deiner configuration.yaml Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die YAML-Konfiguration von Anthem A/V Receivers wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/it.json b/homeassistant/components/anthemav/translations/it.json index 12b0df56f0f..b8bec832581 100644 --- a/homeassistant/components/anthemav/translations/it.json +++ b/homeassistant/components/anthemav/translations/it.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Anthem A/V Receivers tramite YAML verr\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovere la configurazione YAML di Anthem A/V Receivers dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Anthem A/V Receivers verr\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/pt-BR.json b/homeassistant/components/anthemav/translations/pt-BR.json index 309aca9b8ef..5a6038bb480 100644 --- a/homeassistant/components/anthemav/translations/pt-BR.json +++ b/homeassistant/components/anthemav/translations/pt-BR.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o de receptores A/V Anthem usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML dos receptores A/V do Anthem do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML dos receptores A/V do Anthem est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/zh-Hant.json b/homeassistant/components/anthemav/translations/zh-Hant.json index d1b286afd81..0751331f82f 100644 --- a/homeassistant/components/anthemav/translations/zh-Hant.json +++ b/homeassistant/components/anthemav/translations/zh-Hant.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Anthem A/V \u63a5\u6536\u5668\u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/it.json b/homeassistant/components/google/translations/it.json index c29eb8d1a2c..6e24178996f 100644 --- a/homeassistant/components/google/translations/it.json +++ b/homeassistant/components/google/translations/it.json @@ -36,7 +36,7 @@ "issues": { "deprecated_yaml": { "description": "La configurazione di Google Calendar in configuration.yaml verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Google Calendar \u00e8 stata rimossa" + "title": "La configurazione YAML di Google Calendar verr\u00e0 rimossa" }, "removed_track_new_yaml": { "description": "Hai disabilitato il tracciamento delle entit\u00e0 per Google Calendar in configuration.yaml, il che non \u00e8 pi\u00f9 supportato. \u00c8 necessario modificare manualmente le opzioni di sistema dell'integrazione nell'interfaccia utente per disabilitare le entit\u00e0 appena rilevate da adesso in poi. Rimuovi l'impostazione track_new da configuration.yaml e riavvia Home Assistant per risolvere questo problema.", diff --git a/homeassistant/components/google/translations/zh-Hant.json b/homeassistant/components/google/translations/zh-Hant.json index 43c208d69e8..0d2031c368f 100644 --- a/homeassistant/components/google/translations/zh-Hant.json +++ b/homeassistant/components/google/translations/zh-Hant.json @@ -36,7 +36,7 @@ "issues": { "deprecated_yaml": { "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Google \u65e5\u66c6\u5df2\u7d93\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 OAuth \u61c9\u7528\u6191\u8b49\u8207\u5b58\u53d6\u6b0a\u9650\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Google \u65e5\u66c6 YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" + "title": "Google \u65e5\u66c6 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" }, "removed_track_new_yaml": { "description": "\u65bc configuration.yaml \u5167\u6240\u8a2d\u5b9a\u7684 Google \u65e5\u66c6\u5be6\u9ad4\u8ffd\u8e64\u529f\u80fd\uff0c\u7531\u65bc\u4e0d\u518d\u652f\u6301\u3001\u5df2\u7d93\u906d\u5230\u95dc\u9589\u3002\u4e4b\u5f8c\u5fc5\u9808\u624b\u52d5\u900f\u904e\u4ecb\u9762\u5167\u7684\u6574\u5408\u529f\u80fd\u3001\u4ee5\u95dc\u9589\u4efb\u4f55\u65b0\u767c\u73fe\u7684\u5be6\u9ad4\u3002\u8acb\u7531 configuration.yaml \u4e2d\u79fb\u9664R track_new \u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", diff --git a/homeassistant/components/homeassistant_alerts/translations/de.json b/homeassistant/components/homeassistant_alerts/translations/de.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/en.json b/homeassistant/components/homeassistant_alerts/translations/en.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/it.json b/homeassistant/components/homeassistant_alerts/translations/it.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/pl.json b/homeassistant/components/homeassistant_alerts/translations/pl.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/pt-BR.json b/homeassistant/components/homeassistant_alerts/translations/pt-BR.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/zh-Hant.json b/homeassistant/components/homeassistant_alerts/translations/zh-Hant.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/de.json b/homeassistant/components/lacrosse_view/translations/de.json new file mode 100644 index 00000000000..d9aa1210fa0 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "no_locations": "Keine Standorte gefunden", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/en.json b/homeassistant/components/lacrosse_view/translations/en.json index c972d6d12f9..a2a7fd23272 100644 --- a/homeassistant/components/lacrosse_view/translations/en.json +++ b/homeassistant/components/lacrosse_view/translations/en.json @@ -5,8 +5,8 @@ }, "error": { "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error", - "no_locations": "No locations found" + "no_locations": "No locations found", + "unknown": "Unexpected error" }, "step": { "user": { @@ -14,11 +14,6 @@ "password": "Password", "username": "Username" } - }, - "location": { - "data": { - "location": "Location" - } } } } diff --git a/homeassistant/components/lacrosse_view/translations/it.json b/homeassistant/components/lacrosse_view/translations/it.json new file mode 100644 index 00000000000..9ce6c75dcbc --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "invalid_auth": "Autenticazione non valida", + "no_locations": "Nessuna localit\u00e0 trovata", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/pt-BR.json b/homeassistant/components/lacrosse_view/translations/pt-BR.json new file mode 100644 index 00000000000..29b458e5599 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_locations": "Nenhum local encontrado", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Nome de usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/zh-Hant.json b/homeassistant/components/lacrosse_view/translations/zh-Hant.json new file mode 100644 index 00000000000..78235452297 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "no_locations": "\u627e\u4e0d\u5230\u5ea7\u6a19", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/de.json b/homeassistant/components/lyric/translations/de.json index b067b299145..1ef7e65fbf4 100644 --- a/homeassistant/components/lyric/translations/de.json +++ b/homeassistant/components/lyric/translations/de.json @@ -17,5 +17,11 @@ "title": "Integration erneut authentifizieren" } } + }, + "issues": { + "removed_yaml": { + "description": "Die Konfiguration von Honeywell Lyric mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Honeywell Lyric YAML-Konfiguration wurde entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/pt-BR.json b/homeassistant/components/lyric/translations/pt-BR.json index 907a396d5e2..d70832bfbf6 100644 --- a/homeassistant/components/lyric/translations/pt-BR.json +++ b/homeassistant/components/lyric/translations/pt-BR.json @@ -17,5 +17,11 @@ "title": "Reautenticar Integra\u00e7\u00e3o" } } + }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do Honeywell Lyric usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o Honeywell Lyric YAML foi removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/zh-Hant.json b/homeassistant/components/lyric/translations/zh-Hant.json index 850507ec0b3..bb7fbc3aed6 100644 --- a/homeassistant/components/lyric/translations/zh-Hant.json +++ b/homeassistant/components/lyric/translations/zh-Hant.json @@ -17,5 +17,11 @@ "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" } } + }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Honeywell Lyric \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Honeywell Lyric YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/de.json b/homeassistant/components/mitemp_bt/translations/de.json index 7d4887ab638..3c9e6960aeb 100644 --- a/homeassistant/components/mitemp_bt/translations/de.json +++ b/homeassistant/components/mitemp_bt/translations/de.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "Die Integration des Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensors funktioniert in Home Assistant 2022.7 nicht mehr und wurde in der Version 2022.8 durch die Xiaomi BLE Integration ersetzt.\n\nEs ist kein Migrationspfad m\u00f6glich, daher musst du dein Xiaomi Mijia BLE-Ger\u00e4t manuell mit der neuen Integration hinzuf\u00fcgen.\n\nDeine bestehende Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensor YAML-Konfiguration wird von Home Assistant nicht mehr verwendet. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "description": "Die Integration des Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensors funktioniert in Home Assistant 2022.7 nicht mehr und wurde in der Version 2022.8 durch die Xiaomi BLE Integration ersetzt.\n\nEs ist kein Migrationspfad m\u00f6glich, daher musst du dein Xiaomi Mijia BLE-Ger\u00e4t mit der neuen Integration manuell hinzuf\u00fcgen.\n\nDeine bestehende Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensor YAML-Konfiguration wird von Home Assistant nicht mehr verwendet. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", "title": "Die Integration des Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensors wurde ersetzt" } } diff --git a/homeassistant/components/mitemp_bt/translations/it.json b/homeassistant/components/mitemp_bt/translations/it.json new file mode 100644 index 00000000000..cc383e4184c --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor ha smesso di funzionare in Home Assistant 2022.7 ed \u00e8 stata sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Xiaomi Mijia BLE utilizzando la nuova integrazione. \n\nLa configurazione YAML di Xiaomi Mijia BLE Temperature and Humidity Sensor esistente non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor \u00e8 stata sostituita" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/pt-BR.json b/homeassistant/components/mitemp_bt/translations/pt-BR.json index 991a749a729..634f5dd71fd 100644 --- a/homeassistant/components/mitemp_bt/translations/pt-BR.json +++ b/homeassistant/components/mitemp_bt/translations/pt-BR.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "A integra\u00e7\u00e3o do sensor de temperatura e umidade Xiaomi Mijia BLE parou de funcionar no Home Assistant 2022.7 e foi substitu\u00edda pela integra\u00e7\u00e3o Xiaomi BLE na vers\u00e3o 2022.8. \n\n N\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o poss\u00edvel, portanto, voc\u00ea deve adicionar seu dispositivo Xiaomi Mijia BLE usando a nova integra\u00e7\u00e3o manualmente. \n\n Sua configura\u00e7\u00e3o YAML existente do sensor de temperatura e umidade Xiaomi Mijia BLE n\u00e3o \u00e9 mais usada pelo Home Assistant. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "description": "A integra\u00e7\u00e3o do sensor de temperatura e umidade do Xiaomi Mijia BLE parou de funcionar no Home Assistant 2022.7 e foi substitu\u00edda pela integra\u00e7\u00e3o do Xiaomi BLE na vers\u00e3o 2022.8. \n\n N\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o poss\u00edvel, portanto, voc\u00ea deve adicionar seu dispositivo Xiaomi Mijia BLE usando a nova integra\u00e7\u00e3o manualmente. \n\n Sua configura\u00e7\u00e3o YAML existente do sensor de temperatura e umidade Xiaomi Mijia BLE n\u00e3o \u00e9 mais usada pelo Home Assistant. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", "title": "A integra\u00e7\u00e3o do sensor de temperatura e umidade Xiaomi Mijia BLE foi substitu\u00edda" } } diff --git a/homeassistant/components/openalpr_local/translations/de.json b/homeassistant/components/openalpr_local/translations/de.json new file mode 100644 index 00000000000..d517fe0b37f --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Die lokale OpenALPR-Integration wird derzeit aus dem Home Assistant entfernt und wird ab Home Assistant 2022.10 nicht mehr verf\u00fcgbar sein.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die lokale OpenALPR-Integration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/it.json b/homeassistant/components/openalpr_local/translations/it.json new file mode 100644 index 00000000000..26ce80ee584 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "L'integrazione OpenALPR Local \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "L'integrazione OpenALPR Local verr\u00e0 rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/pl.json b/homeassistant/components/openalpr_local/translations/pl.json new file mode 100644 index 00000000000..ac367d20809 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integracja OpenALPR Local oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.10. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja OpenALPR Local zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/pt-BR.json b/homeassistant/components/openalpr_local/translations/pt-BR.json new file mode 100644 index 00000000000..96b2c244b5c --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "A integra\u00e7\u00e3o do OpenALPR Local est\u00e1 pendente de remo\u00e7\u00e3o do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.10. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o do OpenALPR Local est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/zh-Hant.json b/homeassistant/components/openalpr_local/translations/zh-Hant.json new file mode 100644 index 00000000000..8ec55e5a004 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "OpenALPR \u672c\u5730\u7aef\u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "OpenALPR \u672c\u5730\u7aef\u6574\u5408\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/it.json b/homeassistant/components/radiotherm/translations/it.json index 9e35beb648a..fef1c64746b 100644 --- a/homeassistant/components/radiotherm/translations/it.json +++ b/homeassistant/components/radiotherm/translations/it.json @@ -21,7 +21,8 @@ }, "issues": { "deprecated_yaml": { - "description": "La configurazione della piattaforma climatica Radio Thermostat tramite YAML \u00e8 stata rimossa in Home Assistant 2022.9. \n\nLa configurazione esistente \u00e8 stata importata automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema." + "description": "La configurazione della piattaforma climatica Radio Thermostat tramite YAML verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLa configurazione esistente \u00e8 stata importata automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Radio Thermostat verr\u00e0 rimossa" } }, "options": { diff --git a/homeassistant/components/senz/translations/de.json b/homeassistant/components/senz/translations/de.json index ffbc7bb458f..fbae91321be 100644 --- a/homeassistant/components/senz/translations/de.json +++ b/homeassistant/components/senz/translations/de.json @@ -16,5 +16,11 @@ "title": "W\u00e4hle die Authentifizierungsmethode" } } + }, + "issues": { + "removed_yaml": { + "description": "Die Konfiguration von nVent RAYCHEM SENZ mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die nVent RAYCHEM SENZ YAML Konfiguration wurde entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/pt-BR.json b/homeassistant/components/senz/translations/pt-BR.json index 7e3ff2f64a9..02c31d97816 100644 --- a/homeassistant/components/senz/translations/pt-BR.json +++ b/homeassistant/components/senz/translations/pt-BR.json @@ -16,5 +16,11 @@ "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" } } + }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do nVent RAYCHEM SENZ usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o nVent RAYCHEM SENZ YAML foi removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/zh-Hant.json b/homeassistant/components/senz/translations/zh-Hant.json index 3bf08cf34c7..7094ee18a02 100644 --- a/homeassistant/components/senz/translations/zh-Hant.json +++ b/homeassistant/components/senz/translations/zh-Hant.json @@ -16,5 +16,11 @@ "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" } } + }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a nVent RAYCHEM SENZ \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "nVent RAYCHEM SENZ YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/de.json b/homeassistant/components/soundtouch/translations/de.json index 8d28b988834..379516e31be 100644 --- a/homeassistant/components/soundtouch/translations/de.json +++ b/homeassistant/components/soundtouch/translations/de.json @@ -17,5 +17,11 @@ "title": "Best\u00e4tige das Hinzuf\u00fcgen des Bose SoundTouch-Ger\u00e4ts" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Bose SoundTouch mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die Bose SoundTouch YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Bose SoundTouch YAML-Konfiguration wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/it.json b/homeassistant/components/soundtouch/translations/it.json index a14492bf5c3..f9c6d512b2a 100644 --- a/homeassistant/components/soundtouch/translations/it.json +++ b/homeassistant/components/soundtouch/translations/it.json @@ -17,5 +17,11 @@ "title": "Conferma l'aggiunta del dispositivo Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Bose SoundTouch tramite YAML verr\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovere la configurazione YAML di Bose SoundTouch dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Bose SoundTouch verr\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/pt-BR.json b/homeassistant/components/soundtouch/translations/pt-BR.json index e707219f342..7446cbc5a06 100644 --- a/homeassistant/components/soundtouch/translations/pt-BR.json +++ b/homeassistant/components/soundtouch/translations/pt-BR.json @@ -17,5 +17,11 @@ "title": "Confirme a adi\u00e7\u00e3o do dispositivo Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Bose SoundTouch usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML do Bose SoundTouch do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Bose SoundTouch est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/zh-Hant.json b/homeassistant/components/soundtouch/translations/zh-Hant.json index 08231b8571d..f3d8e8e8560 100644 --- a/homeassistant/components/soundtouch/translations/zh-Hant.json +++ b/homeassistant/components/soundtouch/translations/zh-Hant.json @@ -17,5 +17,11 @@ "title": "\u78ba\u8a8d\u65b0\u589e Bose SoundTouch \u88dd\u7f6e" } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Bose SoundTouch \u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Bose SoundTouch YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Bose SoundTouch YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/zh-Hant.json b/homeassistant/components/spotify/translations/zh-Hant.json index ce89e224a8c..52773f3e411 100644 --- a/homeassistant/components/spotify/translations/zh-Hant.json +++ b/homeassistant/components/spotify/translations/zh-Hant.json @@ -21,8 +21,8 @@ }, "issues": { "removed_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Spotify \u7684\u529f\u80fd\u5df2\u906d\u5230\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Spotify YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Spotify \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Spotify YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" } }, "system_health": { diff --git a/homeassistant/components/steam_online/translations/zh-Hant.json b/homeassistant/components/steam_online/translations/zh-Hant.json index e3c725532e9..8b0c932735b 100644 --- a/homeassistant/components/steam_online/translations/zh-Hant.json +++ b/homeassistant/components/steam_online/translations/zh-Hant.json @@ -26,8 +26,8 @@ }, "issues": { "removed_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Steam \u7684\u529f\u80fd\u5df2\u906d\u5230\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Steam YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Steam \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Steam YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" } }, "options": { diff --git a/homeassistant/components/uscis/translations/zh-Hant.json b/homeassistant/components/uscis/translations/zh-Hant.json index 4a5882dbd95..ccc72d3d1dd 100644 --- a/homeassistant/components/uscis/translations/zh-Hant.json +++ b/homeassistant/components/uscis/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "issues": { "pending_removal": { - "description": "\u7f8e\u570b\u516c\u6c11\u8207\u79fb\u6c11\u670d\u52d9\uff08USCIS: U.S. Citizenship and Immigration Services\uff09\u6574\u5408\u6b63\u8a08\u5283\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u8acb\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u6574\u5408\u6b63\u5728\u79fb\u9664\u4e2d\u3001\u7531\u65bc\u4f7f\u7528\u4e86\u7db2\u8def\u8cc7\u6599\u64f7\u53d6\uff08webscraping\uff09\u65b9\u5f0f\u3001\u5c07\u4e0d\u88ab\u5141\u8a31\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "USCIS \u6574\u5408\u6b63\u6e96\u5099\u79fb\u9664" + "description": "\u7f8e\u570b\u516c\u6c11\u8207\u79fb\u6c11\u670d\u52d9\uff08USCIS: U.S. Citizenship and Immigration Services\uff09\u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531\u65bc\u4f7f\u7528\u4e86\u4e0d\u88ab\u5141\u8a31\u7684\u7db2\u8def\u8cc7\u6599\u64f7\u53d6\uff08webscraping\uff09\u65b9\u5f0f\u3001\u6574\u5408\u5373\u5c07\u79fb\u9664\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "USCIS \u6574\u5408\u5373\u5c07\u79fb\u9664" } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/it.json b/homeassistant/components/xbox/translations/it.json index e60c37c9e5f..6cf5bf15bb9 100644 --- a/homeassistant/components/xbox/translations/it.json +++ b/homeassistant/components/xbox/translations/it.json @@ -13,5 +13,11 @@ "title": "Scegli il metodo di autenticazione" } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Xbox in configuration.yaml verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Xbox verr\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/pt-BR.json b/homeassistant/components/xbox/translations/pt-BR.json index 7f788c1ebb8..d1bb02e84dd 100644 --- a/homeassistant/components/xbox/translations/pt-BR.json +++ b/homeassistant/components/xbox/translations/pt-BR.json @@ -13,5 +13,11 @@ "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" } } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Xbox em configuration.yaml est\u00e1 sendo removida no Home Assistant 2022.9. \n\n Suas credenciais de aplicativo OAuth e configura\u00e7\u00f5es de acesso existentes foram importadas para a interface do usu\u00e1rio automaticamente. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Xbox est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 9b8acdf5b87..b4ad58636a7 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -46,11 +46,13 @@ "title": "Optionen f\u00fcr die Alarmsteuerung" }, "zha_options": { + "always_prefer_xy_color_mode": "Immer den XY-Farbmodus bevorzugen", "consider_unavailable_battery": "Batteriebetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)", "consider_unavailable_mains": "Netzbetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)", "default_light_transition": "Standardlicht\u00fcbergangszeit (Sekunden)", "enable_identify_on_join": "Aktiviere den Identifikationseffekt, wenn Ger\u00e4te dem Netzwerk beitreten", "enhanced_light_transition": "Aktiviere einen verbesserten Lichtfarben-/Temperatur\u00fcbergang aus einem ausgeschalteten Zustand", + "light_transitioning_flag": "Erweiterten Helligkeitsregler w\u00e4hrend des Licht\u00fcbergangs aktivieren", "title": "Globale Optionen" } }, diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index 0b8e90f5ac0..99c519782db 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -46,11 +46,13 @@ "title": "Opcje panelu alarmowego" }, "zha_options": { + "always_prefer_xy_color_mode": "Zawsze preferuj tryb kolor\u00f3w XY", "consider_unavailable_battery": "Uznaj urz\u0105dzenia zasilane bateryjnie za niedost\u0119pne po (sekundach)", "consider_unavailable_mains": "Uznaj urz\u0105dzenia zasilane z gniazdka za niedost\u0119pne po (sekundach)", "default_light_transition": "Domy\u015blny czas efektu przej\u015bcia dla \u015bwiat\u0142a (w sekundach)", "enable_identify_on_join": "W\u0142\u0105cz efekt identyfikacji, gdy urz\u0105dzenia do\u0142\u0105czaj\u0105 do sieci", "enhanced_light_transition": "W\u0142\u0105cz ulepszone przej\u015bcie koloru \u015bwiat\u0142a/temperatury ze stanu wy\u0142\u0105czenia", + "light_transitioning_flag": "W\u0142\u0105cz suwak zwi\u0119kszonej jasno\u015bci podczas przej\u015bcia \u015bwiat\u0142a", "title": "Opcje og\u00f3lne" } }, diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index ba54b4aba87..69b8ced6970 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -46,11 +46,13 @@ "title": "Op\u00e7\u00f5es do painel de controle de alarme" }, "zha_options": { + "always_prefer_xy_color_mode": "Sempre prefira o modo de cor XY", "consider_unavailable_battery": "Considerar dispositivos alimentados por bateria indispon\u00edveis ap\u00f3s (segundos)", "consider_unavailable_mains": "Considerar os dispositivos alimentados pela rede indispon\u00edveis ap\u00f3s (segundos)", "default_light_transition": "Tempo de transi\u00e7\u00e3o de luz padr\u00e3o (segundos)", "enable_identify_on_join": "Ativar o efeito de identifica\u00e7\u00e3o quando os dispositivos ingressarem na rede", "enhanced_light_transition": "Ative a transi\u00e7\u00e3o de cor/temperatura da luz aprimorada de um estado desligado", + "light_transitioning_flag": "Ative o controle deslizante de brilho aprimorado durante a transi\u00e7\u00e3o de luz", "title": "Op\u00e7\u00f5es globais" } }, diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index 9505da31e80..546a2f77c31 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -46,11 +46,13 @@ "title": "\u8b66\u6212\u63a7\u5236\u9762\u677f\u9078\u9805" }, "zha_options": { + "always_prefer_xy_color_mode": "\u504f\u597d XY \u8272\u5f69\u6a21\u5f0f", "consider_unavailable_battery": "\u5c07\u96fb\u6c60\u4f9b\u96fb\u88dd\u7f6e\u8996\u70ba\u4e0d\u53ef\u7528\uff08\u79d2\u6578\uff09", "consider_unavailable_mains": "\u5c07\u4e3b\u4f9b\u96fb\u88dd\u7f6e\u8996\u70ba\u4e0d\u53ef\u7528\uff08\u79d2\u6578\uff09", "default_light_transition": "\u9810\u8a2d\u71c8\u5149\u8f49\u63db\u6642\u9593\uff08\u79d2\uff09", "enable_identify_on_join": "\u7576\u88dd\u7f6e\u52a0\u5165\u7db2\u8def\u6642\u3001\u958b\u555f\u8b58\u5225\u6548\u679c", "enhanced_light_transition": "\u958b\u555f\u7531\u95dc\u9589\u72c0\u614b\u589e\u5f37\u5149\u8272/\u8272\u6eab\u8f49\u63db", + "light_transitioning_flag": "\u958b\u555f\u71c8\u5149\u8f49\u63db\u589e\u5f37\u4eae\u5ea6\u8abf\u6574\u5217", "title": "Global \u9078\u9805" } }, From 4f25b8d58e00fe6a03c825233fc85dd221147117 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 28 Jul 2022 12:05:56 +0300 Subject: [PATCH 2916/3516] Add issue to repairs for deprecated Simplepush YAML configuration (#75850) --- homeassistant/components/simplepush/manifest.json | 1 + homeassistant/components/simplepush/notify.py | 12 ++++++++++++ homeassistant/components/simplepush/strings.json | 6 ++++++ .../components/simplepush/translations/en.json | 6 ++++++ 4 files changed, 25 insertions(+) diff --git a/homeassistant/components/simplepush/manifest.json b/homeassistant/components/simplepush/manifest.json index 7c37546485a..6b4ee263ba6 100644 --- a/homeassistant/components/simplepush/manifest.json +++ b/homeassistant/components/simplepush/manifest.json @@ -5,6 +5,7 @@ "requirements": ["simplepush==1.1.4"], "codeowners": ["@engrbm87"], "config_flow": true, + "dependencies": ["repairs"], "iot_class": "cloud_polling", "loggers": ["simplepush"] } diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index e9cd9813175..358d95c770a 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -14,6 +14,8 @@ from homeassistant.components.notify import ( BaseNotificationService, ) from homeassistant.components.notify.const import ATTR_DATA +from homeassistant.components.repairs.issue_handler import async_create_issue +from homeassistant.components.repairs.models import IssueSeverity from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_EVENT, CONF_PASSWORD from homeassistant.core import HomeAssistant @@ -41,6 +43,16 @@ async def async_get_service( discovery_info: DiscoveryInfoType | None = None, ) -> SimplePushNotificationService | None: """Get the Simplepush notification service.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + if discovery_info is None: hass.async_create_task( hass.config_entries.flow.async_init( diff --git a/homeassistant/components/simplepush/strings.json b/homeassistant/components/simplepush/strings.json index 0031dc32340..77ed05c4b48 100644 --- a/homeassistant/components/simplepush/strings.json +++ b/homeassistant/components/simplepush/strings.json @@ -17,5 +17,11 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Simplepush YAML configuration is being removed", + "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/simplepush/translations/en.json b/homeassistant/components/simplepush/translations/en.json index a36a3b2b273..bf373d8baf0 100644 --- a/homeassistant/components/simplepush/translations/en.json +++ b/homeassistant/components/simplepush/translations/en.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "The Simplepush YAML configuration is being removed", + "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } \ No newline at end of file From c16db4c3e1cfdfd0d40c6202e5254ce50bf2510b Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 28 Jul 2022 11:41:03 +0200 Subject: [PATCH 2917/3516] Make Axis utilise forward_entry_setups (#75178) --- homeassistant/components/axis/__init__.py | 26 ++++-- homeassistant/components/axis/config_flow.py | 11 +-- homeassistant/components/axis/device.py | 86 ++++++++------------ tests/components/axis/test_config_flow.py | 4 +- tests/components/axis/test_device.py | 17 ++-- 5 files changed, 62 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 5e211c00028..4af066f4e89 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -4,28 +4,36 @@ import logging from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, CONF_MAC, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.entity_registry import async_migrate_entries -from .const import DOMAIN as AXIS_DOMAIN -from .device import AxisNetworkDevice +from .const import DOMAIN as AXIS_DOMAIN, PLATFORMS +from .device import AxisNetworkDevice, get_axis_device +from .errors import AuthenticationRequired, CannotConnect _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: - """Set up the Axis component.""" + """Set up the Axis integration.""" hass.data.setdefault(AXIS_DOMAIN, {}) - device = AxisNetworkDevice(hass, config_entry) - - if not await device.async_setup(): - return False - - hass.data[AXIS_DOMAIN][config_entry.unique_id] = device + try: + api = await get_axis_device(hass, config_entry.data) + except CannotConnect as err: + raise ConfigEntryNotReady from err + except AuthenticationRequired as err: + raise ConfigEntryAuthFailed from err + device = hass.data[AXIS_DOMAIN][config_entry.unique_id] = AxisNetworkDevice( + hass, config_entry, api + ) await device.async_update_device_registry() + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + device.async_setup_events() + config_entry.add_update_listener(device.async_new_address_callback) config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown) ) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index f94c27dc2ac..1ce2f08c045 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Mapping from ipaddress import ip_address +from types import MappingProxyType from typing import Any from urllib.parse import urlsplit @@ -32,7 +33,7 @@ from .const import ( DEFAULT_VIDEO_SOURCE, DOMAIN as AXIS_DOMAIN, ) -from .device import AxisNetworkDevice, get_device +from .device import AxisNetworkDevice, get_axis_device from .errors import AuthenticationRequired, CannotConnect AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f"} @@ -66,13 +67,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): if user_input is not None: try: - device = await get_device( - self.hass, - host=user_input[CONF_HOST], - port=user_input[CONF_PORT], - username=user_input[CONF_USERNAME], - password=user_input[CONF_PASSWORD], - ) + device = await get_axis_device(self.hass, MappingProxyType(user_input)) serial = device.vapix.serial_number await self.async_set_unique_id(format_mac(serial)) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index d0d5e230d2f..683991d0f65 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -1,6 +1,8 @@ """Axis network device abstraction.""" import asyncio +from types import MappingProxyType +from typing import Any import async_timeout import axis @@ -24,7 +26,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -50,15 +51,15 @@ from .errors import AuthenticationRequired, CannotConnect class AxisNetworkDevice: """Manages a Axis device.""" - def __init__(self, hass, config_entry): + def __init__(self, hass, config_entry, api): """Initialize the device.""" self.hass = hass self.config_entry = config_entry - self.available = True + self.api = api - self.api = None - self.fw_version = None - self.product_type = None + self.available = True + self.fw_version = api.vapix.firmware_version + self.product_type = api.vapix.product_type @property def host(self): @@ -184,7 +185,7 @@ class AxisNetworkDevice: sw_version=self.fw_version, ) - async def use_mqtt(self, hass: HomeAssistant, component: str) -> None: + async def async_use_mqtt(self, hass: HomeAssistant, component: str) -> None: """Set up to use MQTT.""" try: status = await self.api.vapix.mqtt.get_client_status() @@ -209,50 +210,18 @@ class AxisNetworkDevice: # Setup and teardown methods - async def async_setup(self): - """Set up the device.""" - try: - self.api = await get_device( - self.hass, - host=self.host, - port=self.port, - username=self.username, - password=self.password, + def async_setup_events(self): + """Set up the device events.""" + + if self.option_events: + self.api.stream.connection_status_callback.append( + self.async_connection_status_callback ) + self.api.enable_events(event_callback=self.async_event_callback) + self.api.stream.start() - except CannotConnect as err: - raise ConfigEntryNotReady from err - - except AuthenticationRequired as err: - raise ConfigEntryAuthFailed from err - - self.fw_version = self.api.vapix.firmware_version - self.product_type = self.api.vapix.product_type - - async def start_platforms(): - await asyncio.gather( - *( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, platform - ) - for platform in PLATFORMS - ) - ) - if self.option_events: - self.api.stream.connection_status_callback.append( - self.async_connection_status_callback - ) - self.api.enable_events(event_callback=self.async_event_callback) - self.api.stream.start() - - if self.api.vapix.mqtt: - async_when_setup(self.hass, MQTT_DOMAIN, self.use_mqtt) - - self.hass.async_create_task(start_platforms()) - - self.config_entry.add_update_listener(self.async_new_address_callback) - - return True + if self.api.vapix.mqtt: + async_when_setup(self.hass, MQTT_DOMAIN, self.async_use_mqtt) @callback def disconnect_from_stream(self): @@ -274,14 +243,21 @@ class AxisNetworkDevice: ) -async def get_device( - hass: HomeAssistant, host: str, port: int, username: str, password: str +async def get_axis_device( + hass: HomeAssistant, + config: MappingProxyType[str, Any], ) -> axis.AxisDevice: """Create a Axis device.""" session = get_async_client(hass, verify_ssl=False) device = axis.AxisDevice( - Configuration(session, host, port=port, username=username, password=password) + Configuration( + session, + config[CONF_HOST], + port=config[CONF_PORT], + username=config[CONF_USERNAME], + password=config[CONF_PASSWORD], + ) ) try: @@ -291,11 +267,13 @@ async def get_device( return device except axis.Unauthorized as err: - LOGGER.warning("Connected to device at %s but not registered", host) + LOGGER.warning( + "Connected to device at %s but not registered", config[CONF_HOST] + ) raise AuthenticationRequired from err except (asyncio.TimeoutError, axis.RequestError) as err: - LOGGER.error("Error connecting to the Axis device at %s", host) + LOGGER.error("Error connecting to the Axis device at %s", config[CONF_HOST]) raise CannotConnect from err except axis.AxisException as err: diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 1459ae215d9..2daf350ac93 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -123,7 +123,7 @@ async def test_flow_fails_faulty_credentials(hass): assert result["step_id"] == SOURCE_USER with patch( - "homeassistant.components.axis.config_flow.get_device", + "homeassistant.components.axis.config_flow.get_axis_device", side_effect=config_flow.AuthenticationRequired, ): result = await hass.config_entries.flow.async_configure( @@ -149,7 +149,7 @@ async def test_flow_fails_cannot_connect(hass): assert result["step_id"] == SOURCE_USER with patch( - "homeassistant.components.axis.config_flow.get_device", + "homeassistant.components.axis.config_flow.get_axis_device", side_effect=config_flow.CannotConnect, ): result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index 4717e2915c1..ba6df6e2e2d 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -441,7 +441,7 @@ async def test_device_reset(hass): async def test_device_not_accessible(hass): """Failed setup schedules a retry of setup.""" - with patch.object(axis.device, "get_device", side_effect=axis.errors.CannotConnect): + with patch.object(axis, "get_axis_device", side_effect=axis.errors.CannotConnect): await setup_axis_integration(hass) assert hass.data[AXIS_DOMAIN] == {} @@ -449,7 +449,7 @@ async def test_device_not_accessible(hass): async def test_device_trigger_reauth_flow(hass): """Failed authentication trigger a reauthentication flow.""" with patch.object( - axis.device, "get_device", side_effect=axis.errors.AuthenticationRequired + axis, "get_axis_device", side_effect=axis.errors.AuthenticationRequired ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: await setup_axis_integration(hass) mock_flow_init.assert_called_once() @@ -458,7 +458,7 @@ async def test_device_trigger_reauth_flow(hass): async def test_device_unknown_error(hass): """Unknown errors are handled.""" - with patch.object(axis.device, "get_device", side_effect=Exception): + with patch.object(axis, "get_axis_device", side_effect=Exception): await setup_axis_integration(hass) assert hass.data[AXIS_DOMAIN] == {} @@ -468,7 +468,7 @@ async def test_new_event_sends_signal(hass): entry = Mock() entry.data = ENTRY_CONFIG - axis_device = axis.device.AxisNetworkDevice(hass, entry) + axis_device = axis.device.AxisNetworkDevice(hass, entry, Mock()) with patch.object(axis.device, "async_dispatcher_send") as mock_dispatch_send: axis_device.async_event_callback(action=OPERATION_INITIALIZED, event_id="event") @@ -484,8 +484,7 @@ async def test_shutdown(): entry = Mock() entry.data = ENTRY_CONFIG - axis_device = axis.device.AxisNetworkDevice(hass, entry) - axis_device.api = Mock() + axis_device = axis.device.AxisNetworkDevice(hass, entry, Mock()) await axis_device.shutdown(None) @@ -497,7 +496,7 @@ async def test_get_device_fails(hass): with patch( "axis.vapix.Vapix.request", side_effect=axislib.Unauthorized ), pytest.raises(axis.errors.AuthenticationRequired): - await axis.device.get_device(hass, host="", port="", username="", password="") + await axis.device.get_axis_device(hass, ENTRY_CONFIG) async def test_get_device_device_unavailable(hass): @@ -505,7 +504,7 @@ async def test_get_device_device_unavailable(hass): with patch( "axis.vapix.Vapix.request", side_effect=axislib.RequestError ), pytest.raises(axis.errors.CannotConnect): - await axis.device.get_device(hass, host="", port="", username="", password="") + await axis.device.get_axis_device(hass, ENTRY_CONFIG) async def test_get_device_unknown_error(hass): @@ -513,4 +512,4 @@ async def test_get_device_unknown_error(hass): with patch( "axis.vapix.Vapix.request", side_effect=axislib.AxisException ), pytest.raises(axis.errors.AuthenticationRequired): - await axis.device.get_device(hass, host="", port="", username="", password="") + await axis.device.get_axis_device(hass, ENTRY_CONFIG) From 25d943d272b8a1de671694960e499917e5190563 Mon Sep 17 00:00:00 2001 From: borky Date: Thu, 28 Jul 2022 13:38:04 +0300 Subject: [PATCH 2918/3516] Add xiaomi air purifier 4 and 4 pro support (#75745) Co-authored-by: Martin Hjelmare --- homeassistant/components/xiaomi_miio/const.py | 14 ++++++ homeassistant/components/xiaomi_miio/fan.py | 11 +++++ .../components/xiaomi_miio/number.py | 5 +++ .../components/xiaomi_miio/sensor.py | 44 +++++++++++++++++++ .../components/xiaomi_miio/switch.py | 32 ++++++++++++++ .../components/xiaomi_miio/vacuum.py | 2 +- 6 files changed, 107 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 3577e7b9907..54289bb8389 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -47,6 +47,8 @@ class SetupException(Exception): # Fan Models +MODEL_AIRPURIFIER_4 = "zhimi.airp.mb5" +MODEL_AIRPURIFIER_4_PRO = "zhimi.airp.vb4" MODEL_AIRPURIFIER_2H = "zhimi.airpurifier.mc2" MODEL_AIRPURIFIER_2S = "zhimi.airpurifier.mc1" MODEL_AIRPURIFIER_3 = "zhimi.airpurifier.ma4" @@ -114,6 +116,8 @@ MODELS_PURIFIER_MIOT = [ MODEL_AIRPURIFIER_3C, MODEL_AIRPURIFIER_3H, MODEL_AIRPURIFIER_PROH, + MODEL_AIRPURIFIER_4, + MODEL_AIRPURIFIER_4_PRO, ] MODELS_PURIFIER_MIIO = [ MODEL_AIRPURIFIER_V1, @@ -316,6 +320,7 @@ FEATURE_SET_FAVORITE_RPM = 262144 FEATURE_SET_IONIZER = 524288 FEATURE_SET_DISPLAY = 1048576 FEATURE_SET_PTC = 2097152 +FEATURE_SET_ANION = 4194304 FEATURE_FLAGS_AIRPURIFIER_MIIO = ( FEATURE_SET_BUZZER @@ -335,6 +340,15 @@ FEATURE_FLAGS_AIRPURIFIER_MIOT = ( | FEATURE_SET_LED_BRIGHTNESS ) +FEATURE_FLAGS_AIRPURIFIER_4 = ( + FEATURE_SET_BUZZER + | FEATURE_SET_CHILD_LOCK + | FEATURE_SET_FAVORITE_LEVEL + | FEATURE_SET_FAN_LEVEL + | FEATURE_SET_LED_BRIGHTNESS + | FEATURE_SET_ANION +) + FEATURE_FLAGS_AIRPURIFIER_3C = ( FEATURE_SET_BUZZER | FEATURE_SET_CHILD_LOCK diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index aa4b8a8a1bc..39988976564 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -48,6 +48,7 @@ from .const import ( FEATURE_FLAGS_AIRFRESH_T2017, FEATURE_FLAGS_AIRPURIFIER_2S, FEATURE_FLAGS_AIRPURIFIER_3C, + FEATURE_FLAGS_AIRPURIFIER_4, FEATURE_FLAGS_AIRPURIFIER_MIIO, FEATURE_FLAGS_AIRPURIFIER_MIOT, FEATURE_FLAGS_AIRPURIFIER_PRO, @@ -68,6 +69,8 @@ from .const import ( MODEL_AIRPURIFIER_2H, MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_3C, + MODEL_AIRPURIFIER_4, + MODEL_AIRPURIFIER_4_PRO, MODEL_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V3, @@ -411,6 +414,14 @@ class XiaomiAirPurifier(XiaomiGenericAirPurifier): self._preset_modes = PRESET_MODES_AIRPURIFIER_PRO self._attr_supported_features = FanEntityFeature.PRESET_MODE self._speed_count = 1 + elif self._model in [MODEL_AIRPURIFIER_4, MODEL_AIRPURIFIER_4_PRO]: + self._device_features = FEATURE_FLAGS_AIRPURIFIER_4 + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_MIOT + self._preset_modes = PRESET_MODES_AIRPURIFIER_MIOT + self._attr_supported_features = ( + FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE + ) + self._speed_count = 3 elif self._model == MODEL_AIRPURIFIER_PRO_V7: self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO_V7 self._available_attributes = AVAILABLE_ATTRIBUTES_AIRPURIFIER_PRO_V7 diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 577e82e3fd8..364bd59772c 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -24,6 +24,7 @@ from .const import ( FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB, FEATURE_FLAGS_AIRPURIFIER_2S, FEATURE_FLAGS_AIRPURIFIER_3C, + FEATURE_FLAGS_AIRPURIFIER_4, FEATURE_FLAGS_AIRPURIFIER_MIIO, FEATURE_FLAGS_AIRPURIFIER_MIOT, FEATURE_FLAGS_AIRPURIFIER_PRO, @@ -55,6 +56,8 @@ from .const import ( MODEL_AIRHUMIDIFIER_CB1, MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_3C, + MODEL_AIRPURIFIER_4, + MODEL_AIRPURIFIER_4_PRO, MODEL_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V1, @@ -224,6 +227,8 @@ MODEL_TO_FEATURES_MAP = { MODEL_AIRPURIFIER_PRO_V7: FEATURE_FLAGS_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V1: FEATURE_FLAGS_AIRPURIFIER_V1, MODEL_AIRPURIFIER_V3: FEATURE_FLAGS_AIRPURIFIER_V3, + MODEL_AIRPURIFIER_4: FEATURE_FLAGS_AIRPURIFIER_4, + MODEL_AIRPURIFIER_4_PRO: FEATURE_FLAGS_AIRPURIFIER_4, MODEL_FAN_1C: FEATURE_FLAGS_FAN_1C, MODEL_FAN_P10: FEATURE_FLAGS_FAN_P10_P11, MODEL_FAN_P11: FEATURE_FLAGS_FAN_P10_P11, diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index 235d103b53d..5370357c58c 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -60,6 +60,8 @@ from .const import ( MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1, MODEL_AIRPURIFIER_3C, + MODEL_AIRPURIFIER_4, + MODEL_AIRPURIFIER_4_PRO, MODEL_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V2, @@ -100,6 +102,7 @@ ATTR_DISPLAY_CLOCK = "display_clock" ATTR_FAVORITE_SPEED = "favorite_speed" ATTR_FILTER_LIFE_REMAINING = "filter_life_remaining" ATTR_FILTER_HOURS_USED = "filter_hours_used" +ATTR_FILTER_LEFT_TIME = "filter_left_time" ATTR_DUST_FILTER_LIFE_REMAINING = "dust_filter_life_remaining" ATTR_DUST_FILTER_LIFE_REMAINING_DAYS = "dust_filter_life_remaining_days" ATTR_UPPER_FILTER_LIFE_REMAINING = "upper_filter_life_remaining" @@ -114,6 +117,7 @@ ATTR_MOTOR_SPEED = "motor_speed" ATTR_NIGHT_MODE = "night_mode" ATTR_NIGHT_TIME_BEGIN = "night_time_begin" ATTR_NIGHT_TIME_END = "night_time_end" +ATTR_PM10 = "pm10_density" ATTR_PM25 = "pm25" ATTR_PM25_2 = "pm25_2" ATTR_POWER = "power" @@ -253,6 +257,13 @@ SENSOR_TYPES = { icon="mdi:cloud", state_class=SensorStateClass.MEASUREMENT, ), + ATTR_PM10: XiaomiMiioSensorDescription( + key=ATTR_PM10, + name="PM10", + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + device_class=SensorDeviceClass.PM10, + state_class=SensorStateClass.MEASUREMENT, + ), ATTR_PM25: XiaomiMiioSensorDescription( key=ATTR_AQI, name="PM2.5", @@ -284,6 +295,14 @@ SENSOR_TYPES = { state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), + ATTR_FILTER_LEFT_TIME: XiaomiMiioSensorDescription( + key=ATTR_FILTER_LEFT_TIME, + name="Filter time left", + native_unit_of_measurement=TIME_DAYS, + icon="mdi:clock-outline", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), ATTR_DUST_FILTER_LIFE_REMAINING: XiaomiMiioSensorDescription( key=ATTR_DUST_FILTER_LIFE_REMAINING, name="Dust filter life remaining", @@ -385,6 +404,29 @@ PURIFIER_MIOT_SENSORS = ( ATTR_TEMPERATURE, ATTR_USE_TIME, ) +PURIFIER_4_SENSORS = ( + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_LEFT_TIME, + ATTR_FILTER_USE, + ATTR_HUMIDITY, + ATTR_MOTOR_SPEED, + ATTR_PM25, + ATTR_PURIFY_VOLUME, + ATTR_TEMPERATURE, + ATTR_USE_TIME, +) +PURIFIER_4_PRO_SENSORS = ( + ATTR_FILTER_LIFE_REMAINING, + ATTR_FILTER_LEFT_TIME, + ATTR_FILTER_USE, + ATTR_HUMIDITY, + ATTR_MOTOR_SPEED, + ATTR_PM25, + ATTR_PM10, + ATTR_PURIFY_VOLUME, + ATTR_TEMPERATURE, + ATTR_USE_TIME, +) PURIFIER_3C_SENSORS = ( ATTR_FILTER_LIFE_REMAINING, ATTR_FILTER_USE, @@ -478,6 +520,8 @@ MODEL_TO_SENSORS_MAP: dict[str, tuple[str, ...]] = { MODEL_AIRHUMIDIFIER_CA1: HUMIDIFIER_CA1_CB1_SENSORS, MODEL_AIRHUMIDIFIER_CB1: HUMIDIFIER_CA1_CB1_SENSORS, MODEL_AIRPURIFIER_3C: PURIFIER_3C_SENSORS, + MODEL_AIRPURIFIER_4: PURIFIER_4_SENSORS, + MODEL_AIRPURIFIER_4_PRO: PURIFIER_4_PRO_SENSORS, MODEL_AIRPURIFIER_PRO: PURIFIER_PRO_SENSORS, MODEL_AIRPURIFIER_PRO_V7: PURIFIER_PRO_V7_SENSORS, MODEL_AIRPURIFIER_V2: PURIFIER_V2_SENSORS, diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index f80b6343d09..89cc80ce74f 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -43,6 +43,7 @@ from .const import ( FEATURE_FLAGS_AIRHUMIDIFIER_MJSSQ, FEATURE_FLAGS_AIRPURIFIER_2S, FEATURE_FLAGS_AIRPURIFIER_3C, + FEATURE_FLAGS_AIRPURIFIER_4, FEATURE_FLAGS_AIRPURIFIER_MIIO, FEATURE_FLAGS_AIRPURIFIER_MIOT, FEATURE_FLAGS_AIRPURIFIER_PRO, @@ -55,6 +56,7 @@ from .const import ( FEATURE_FLAGS_FAN_P9, FEATURE_FLAGS_FAN_P10_P11, FEATURE_FLAGS_FAN_ZA5, + FEATURE_SET_ANION, FEATURE_SET_AUTO_DETECT, FEATURE_SET_BUZZER, FEATURE_SET_CHILD_LOCK, @@ -76,6 +78,8 @@ from .const import ( MODEL_AIRPURIFIER_2H, MODEL_AIRPURIFIER_2S, MODEL_AIRPURIFIER_3C, + MODEL_AIRPURIFIER_4, + MODEL_AIRPURIFIER_4_PRO, MODEL_AIRPURIFIER_PRO, MODEL_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V1, @@ -128,6 +132,7 @@ ATTR_DRY = "dry" ATTR_LEARN_MODE = "learn_mode" ATTR_LED = "led" ATTR_IONIZER = "ionizer" +ATTR_ANION = "anion" ATTR_LOAD_POWER = "load_power" ATTR_MODEL = "model" ATTR_POWER = "power" @@ -188,6 +193,8 @@ MODEL_TO_FEATURES_MAP = { MODEL_AIRPURIFIER_PRO_V7: FEATURE_FLAGS_AIRPURIFIER_PRO_V7, MODEL_AIRPURIFIER_V1: FEATURE_FLAGS_AIRPURIFIER_V1, MODEL_AIRPURIFIER_V3: FEATURE_FLAGS_AIRPURIFIER_V3, + MODEL_AIRPURIFIER_4: FEATURE_FLAGS_AIRPURIFIER_4, + MODEL_AIRPURIFIER_4_PRO: FEATURE_FLAGS_AIRPURIFIER_4, MODEL_FAN_1C: FEATURE_FLAGS_FAN_1C, MODEL_FAN_P10: FEATURE_FLAGS_FAN_P10_P11, MODEL_FAN_P11: FEATURE_FLAGS_FAN_P10_P11, @@ -300,6 +307,15 @@ SWITCH_TYPES = ( method_off="async_set_ionizer_off", entity_category=EntityCategory.CONFIG, ), + XiaomiMiioSwitchDescription( + key=ATTR_ANION, + feature=FEATURE_SET_ANION, + name="Ionizer", + icon="mdi:shimmer", + method_on="async_set_anion_on", + method_off="async_set_anion_off", + entity_category=EntityCategory.CONFIG, + ), XiaomiMiioSwitchDescription( key=ATTR_PTC, feature=FEATURE_SET_PTC, @@ -678,6 +694,22 @@ class XiaomiGenericCoordinatedSwitch(XiaomiCoordinatedMiioEntity, SwitchEntity): False, ) + async def async_set_anion_on(self) -> bool: + """Turn ionizer on.""" + return await self._try_command( + "Turning ionizer of the miio device on failed.", + self._device.set_anion, + True, + ) + + async def async_set_anion_off(self) -> bool: + """Turn ionizer off.""" + return await self._try_command( + "Turning ionizer of the miio device off failed.", + self._device.set_anion, + False, + ) + async def async_set_ptc_on(self) -> bool: """Turn ionizer on.""" return await self._try_command( diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index e8f4b334544..7b866d5ff71 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -21,10 +21,10 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.dt import as_utc from . import VacuumCoordinatorData -from ...helpers.update_coordinator import DataUpdateCoordinator from .const import ( CONF_DEVICE, CONF_FLOW_TYPE, From 4f5602849105ae9df7441b9a178ad65c1e181833 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 12:39:10 +0200 Subject: [PATCH 2919/3516] Fix unit of measurement usage in COSignal (#75856) --- homeassistant/components/co2signal/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index f7514664698..841848621ec 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -103,8 +103,8 @@ class CO2Sensor(CoordinatorEntity[CO2SignalCoordinator], SensorEntity): @property def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" - if self.entity_description.unit_of_measurement: - return self.entity_description.unit_of_measurement + if self.entity_description.native_unit_of_measurement: + return self.entity_description.native_unit_of_measurement return cast( str, self.coordinator.data["units"].get(self.entity_description.key) ) From 15f87ca0a199856ed345b6bc926561fa75092450 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 28 Jul 2022 00:25:05 +0000 Subject: [PATCH 2920/3516] [ci skip] Translation update --- .../components/ambee/translations/de.json | 6 ++++++ .../components/ambee/translations/it.json | 6 ++++++ .../components/ambee/translations/pl.json | 6 ++++++ .../components/ambee/translations/pt-BR.json | 6 ++++++ .../ambee/translations/zh-Hant.json | 6 ++++++ .../components/anthemav/translations/de.json | 6 ++++++ .../components/anthemav/translations/it.json | 6 ++++++ .../anthemav/translations/pt-BR.json | 6 ++++++ .../anthemav/translations/zh-Hant.json | 6 ++++++ .../components/google/translations/it.json | 2 +- .../google/translations/zh-Hant.json | 2 +- .../homeassistant_alerts/translations/de.json | 8 ++++++++ .../homeassistant_alerts/translations/en.json | 8 ++++++++ .../homeassistant_alerts/translations/it.json | 8 ++++++++ .../homeassistant_alerts/translations/pl.json | 8 ++++++++ .../translations/pt-BR.json | 8 ++++++++ .../translations/zh-Hant.json | 8 ++++++++ .../lacrosse_view/translations/de.json | 20 +++++++++++++++++++ .../lacrosse_view/translations/en.json | 20 +++++++++++++++++++ .../lacrosse_view/translations/it.json | 20 +++++++++++++++++++ .../lacrosse_view/translations/pt-BR.json | 20 +++++++++++++++++++ .../lacrosse_view/translations/zh-Hant.json | 20 +++++++++++++++++++ .../components/lyric/translations/de.json | 6 ++++++ .../components/lyric/translations/pt-BR.json | 6 ++++++ .../lyric/translations/zh-Hant.json | 6 ++++++ .../components/mitemp_bt/translations/de.json | 2 +- .../components/mitemp_bt/translations/it.json | 8 ++++++++ .../mitemp_bt/translations/pt-BR.json | 2 +- .../openalpr_local/translations/de.json | 8 ++++++++ .../openalpr_local/translations/it.json | 8 ++++++++ .../openalpr_local/translations/pl.json | 8 ++++++++ .../openalpr_local/translations/pt-BR.json | 8 ++++++++ .../openalpr_local/translations/zh-Hant.json | 8 ++++++++ .../radiotherm/translations/it.json | 3 ++- .../components/senz/translations/de.json | 6 ++++++ .../components/senz/translations/pt-BR.json | 6 ++++++ .../components/senz/translations/zh-Hant.json | 6 ++++++ .../soundtouch/translations/de.json | 6 ++++++ .../soundtouch/translations/it.json | 6 ++++++ .../soundtouch/translations/pt-BR.json | 6 ++++++ .../soundtouch/translations/zh-Hant.json | 6 ++++++ .../spotify/translations/zh-Hant.json | 4 ++-- .../steam_online/translations/zh-Hant.json | 4 ++-- .../uscis/translations/zh-Hant.json | 4 ++-- .../components/xbox/translations/it.json | 6 ++++++ .../components/xbox/translations/pt-BR.json | 6 ++++++ .../components/zha/translations/de.json | 2 ++ .../components/zha/translations/pl.json | 2 ++ .../components/zha/translations/pt-BR.json | 2 ++ .../components/zha/translations/zh-Hant.json | 2 ++ 50 files changed, 342 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/homeassistant_alerts/translations/de.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/en.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/it.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/pl.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/pt-BR.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/zh-Hant.json create mode 100644 homeassistant/components/lacrosse_view/translations/de.json create mode 100644 homeassistant/components/lacrosse_view/translations/en.json create mode 100644 homeassistant/components/lacrosse_view/translations/it.json create mode 100644 homeassistant/components/lacrosse_view/translations/pt-BR.json create mode 100644 homeassistant/components/lacrosse_view/translations/zh-Hant.json create mode 100644 homeassistant/components/mitemp_bt/translations/it.json create mode 100644 homeassistant/components/openalpr_local/translations/de.json create mode 100644 homeassistant/components/openalpr_local/translations/it.json create mode 100644 homeassistant/components/openalpr_local/translations/pl.json create mode 100644 homeassistant/components/openalpr_local/translations/pt-BR.json create mode 100644 homeassistant/components/openalpr_local/translations/zh-Hant.json diff --git a/homeassistant/components/ambee/translations/de.json b/homeassistant/components/ambee/translations/de.json index 4359ab72349..8055ef5210f 100644 --- a/homeassistant/components/ambee/translations/de.json +++ b/homeassistant/components/ambee/translations/de.json @@ -24,5 +24,11 @@ "description": "Richte Ambee f\u00fcr die Integration mit Home Assistant ein." } } + }, + "issues": { + "pending_removal": { + "description": "Die Ambee-Integration ist dabei, aus Home Assistant entfernt zu werden und wird ab Home Assistant 2022.10 nicht mehr verf\u00fcgbar sein.\n\nDie Integration wird entfernt, weil Ambee seine kostenlosen (begrenzten) Konten entfernt hat und keine M\u00f6glichkeit mehr f\u00fcr regul\u00e4re Nutzer bietet, sich f\u00fcr einen kostenpflichtigen Plan anzumelden.\n\nEntferne den Ambee-Integrationseintrag aus deiner Instanz, um dieses Problem zu beheben.", + "title": "Die Ambee-Integration wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/it.json b/homeassistant/components/ambee/translations/it.json index fe97ce33686..db330a9b239 100644 --- a/homeassistant/components/ambee/translations/it.json +++ b/homeassistant/components/ambee/translations/it.json @@ -24,5 +24,11 @@ "description": "Configura Ambee per l'integrazione con Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "L'integrazione Ambee \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nL'integrazione \u00e8 stata rimossa, perch\u00e9 Ambee ha rimosso i loro account gratuiti (limitati) e non offre pi\u00f9 agli utenti regolari un modo per iscriversi a un piano a pagamento. \n\nRimuovi la voce di integrazione Ambee dalla tua istanza per risolvere questo problema.", + "title": "L'integrazione Ambee verr\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/pl.json b/homeassistant/components/ambee/translations/pl.json index d0b2225cc9a..255d402175d 100644 --- a/homeassistant/components/ambee/translations/pl.json +++ b/homeassistant/components/ambee/translations/pl.json @@ -24,5 +24,11 @@ "description": "Skonfiguruj Ambee, aby zintegrowa\u0107 go z Home Assistantem." } } + }, + "issues": { + "pending_removal": { + "description": "Integracja Ambee oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.10. \n\nIntegracja jest usuwana, poniewa\u017c Ambee usun\u0105\u0142 ich bezp\u0142atne (ograniczone) konta i nie zapewnia ju\u017c zwyk\u0142ym u\u017cytkownikom mo\u017cliwo\u015bci zarejestrowania si\u0119 w p\u0142atnym planie. \n\nUsu\u0144 integracj\u0119 Ambee z Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja Ambee zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/pt-BR.json b/homeassistant/components/ambee/translations/pt-BR.json index 2d960e17df2..3220de5104e 100644 --- a/homeassistant/components/ambee/translations/pt-BR.json +++ b/homeassistant/components/ambee/translations/pt-BR.json @@ -24,5 +24,11 @@ "description": "Configure o Ambee para integrar com o Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "A integra\u00e7\u00e3o do Ambee est\u00e1 com remo\u00e7\u00e3o pendente do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.10. \n\n A integra\u00e7\u00e3o est\u00e1 sendo removida, porque a Ambee removeu suas contas gratuitas (limitadas) e n\u00e3o oferece mais uma maneira de usu\u00e1rios regulares se inscreverem em um plano pago. \n\n Remova a entrada de integra\u00e7\u00e3o Ambee de sua inst\u00e2ncia para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o Ambee est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/zh-Hant.json b/homeassistant/components/ambee/translations/zh-Hant.json index 2e1de25fde2..ccebea49c6f 100644 --- a/homeassistant/components/ambee/translations/zh-Hant.json +++ b/homeassistant/components/ambee/translations/zh-Hant.json @@ -24,5 +24,11 @@ "description": "\u8a2d\u5b9a Ambee \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" } } + }, + "issues": { + "pending_removal": { + "description": "Ambee \u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531\u65bc Ambee \u79fb\u9664\u4e86\u5176\u514d\u8cbb\uff08\u6709\u9650\uff09\u5e33\u865f\u3001\u4e26\u4e14\u4e0d\u518d\u63d0\u4f9b\u4e00\u822c\u4f7f\u7528\u8005\u8a3b\u518a\u4ed8\u8cbb\u670d\u52d9\u3001\u6574\u5408\u5373\u5c07\u79fb\u9664\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Ambee \u6574\u5408\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/de.json b/homeassistant/components/anthemav/translations/de.json index 622384629fe..d751349b005 100644 --- a/homeassistant/components/anthemav/translations/de.json +++ b/homeassistant/components/anthemav/translations/de.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Anthem A/V-Receivern mit YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die Anthem A/V Receivers YAML Konfiguration aus deiner configuration.yaml Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die YAML-Konfiguration von Anthem A/V Receivers wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/it.json b/homeassistant/components/anthemav/translations/it.json index 12b0df56f0f..b8bec832581 100644 --- a/homeassistant/components/anthemav/translations/it.json +++ b/homeassistant/components/anthemav/translations/it.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Anthem A/V Receivers tramite YAML verr\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovere la configurazione YAML di Anthem A/V Receivers dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Anthem A/V Receivers verr\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/pt-BR.json b/homeassistant/components/anthemav/translations/pt-BR.json index 309aca9b8ef..5a6038bb480 100644 --- a/homeassistant/components/anthemav/translations/pt-BR.json +++ b/homeassistant/components/anthemav/translations/pt-BR.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o de receptores A/V Anthem usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML dos receptores A/V do Anthem do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML dos receptores A/V do Anthem est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/zh-Hant.json b/homeassistant/components/anthemav/translations/zh-Hant.json index d1b286afd81..0751331f82f 100644 --- a/homeassistant/components/anthemav/translations/zh-Hant.json +++ b/homeassistant/components/anthemav/translations/zh-Hant.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Anthem A/V \u63a5\u6536\u5668\u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/it.json b/homeassistant/components/google/translations/it.json index c29eb8d1a2c..6e24178996f 100644 --- a/homeassistant/components/google/translations/it.json +++ b/homeassistant/components/google/translations/it.json @@ -36,7 +36,7 @@ "issues": { "deprecated_yaml": { "description": "La configurazione di Google Calendar in configuration.yaml verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Google Calendar \u00e8 stata rimossa" + "title": "La configurazione YAML di Google Calendar verr\u00e0 rimossa" }, "removed_track_new_yaml": { "description": "Hai disabilitato il tracciamento delle entit\u00e0 per Google Calendar in configuration.yaml, il che non \u00e8 pi\u00f9 supportato. \u00c8 necessario modificare manualmente le opzioni di sistema dell'integrazione nell'interfaccia utente per disabilitare le entit\u00e0 appena rilevate da adesso in poi. Rimuovi l'impostazione track_new da configuration.yaml e riavvia Home Assistant per risolvere questo problema.", diff --git a/homeassistant/components/google/translations/zh-Hant.json b/homeassistant/components/google/translations/zh-Hant.json index 43c208d69e8..0d2031c368f 100644 --- a/homeassistant/components/google/translations/zh-Hant.json +++ b/homeassistant/components/google/translations/zh-Hant.json @@ -36,7 +36,7 @@ "issues": { "deprecated_yaml": { "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Google \u65e5\u66c6\u5df2\u7d93\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 OAuth \u61c9\u7528\u6191\u8b49\u8207\u5b58\u53d6\u6b0a\u9650\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Google \u65e5\u66c6 YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" + "title": "Google \u65e5\u66c6 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" }, "removed_track_new_yaml": { "description": "\u65bc configuration.yaml \u5167\u6240\u8a2d\u5b9a\u7684 Google \u65e5\u66c6\u5be6\u9ad4\u8ffd\u8e64\u529f\u80fd\uff0c\u7531\u65bc\u4e0d\u518d\u652f\u6301\u3001\u5df2\u7d93\u906d\u5230\u95dc\u9589\u3002\u4e4b\u5f8c\u5fc5\u9808\u624b\u52d5\u900f\u904e\u4ecb\u9762\u5167\u7684\u6574\u5408\u529f\u80fd\u3001\u4ee5\u95dc\u9589\u4efb\u4f55\u65b0\u767c\u73fe\u7684\u5be6\u9ad4\u3002\u8acb\u7531 configuration.yaml \u4e2d\u79fb\u9664R track_new \u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", diff --git a/homeassistant/components/homeassistant_alerts/translations/de.json b/homeassistant/components/homeassistant_alerts/translations/de.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/en.json b/homeassistant/components/homeassistant_alerts/translations/en.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/it.json b/homeassistant/components/homeassistant_alerts/translations/it.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/pl.json b/homeassistant/components/homeassistant_alerts/translations/pl.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/pt-BR.json b/homeassistant/components/homeassistant_alerts/translations/pt-BR.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/zh-Hant.json b/homeassistant/components/homeassistant_alerts/translations/zh-Hant.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/de.json b/homeassistant/components/lacrosse_view/translations/de.json new file mode 100644 index 00000000000..d9aa1210fa0 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "no_locations": "Keine Standorte gefunden", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/en.json b/homeassistant/components/lacrosse_view/translations/en.json new file mode 100644 index 00000000000..a2a7fd23272 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "invalid_auth": "Invalid authentication", + "no_locations": "No locations found", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/it.json b/homeassistant/components/lacrosse_view/translations/it.json new file mode 100644 index 00000000000..9ce6c75dcbc --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "invalid_auth": "Autenticazione non valida", + "no_locations": "Nessuna localit\u00e0 trovata", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/pt-BR.json b/homeassistant/components/lacrosse_view/translations/pt-BR.json new file mode 100644 index 00000000000..29b458e5599 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_locations": "Nenhum local encontrado", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Nome de usu\u00e1rio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/zh-Hant.json b/homeassistant/components/lacrosse_view/translations/zh-Hant.json new file mode 100644 index 00000000000..78235452297 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "no_locations": "\u627e\u4e0d\u5230\u5ea7\u6a19", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/de.json b/homeassistant/components/lyric/translations/de.json index b067b299145..1ef7e65fbf4 100644 --- a/homeassistant/components/lyric/translations/de.json +++ b/homeassistant/components/lyric/translations/de.json @@ -17,5 +17,11 @@ "title": "Integration erneut authentifizieren" } } + }, + "issues": { + "removed_yaml": { + "description": "Die Konfiguration von Honeywell Lyric mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Honeywell Lyric YAML-Konfiguration wurde entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/pt-BR.json b/homeassistant/components/lyric/translations/pt-BR.json index 907a396d5e2..d70832bfbf6 100644 --- a/homeassistant/components/lyric/translations/pt-BR.json +++ b/homeassistant/components/lyric/translations/pt-BR.json @@ -17,5 +17,11 @@ "title": "Reautenticar Integra\u00e7\u00e3o" } } + }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do Honeywell Lyric usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o Honeywell Lyric YAML foi removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/zh-Hant.json b/homeassistant/components/lyric/translations/zh-Hant.json index 850507ec0b3..bb7fbc3aed6 100644 --- a/homeassistant/components/lyric/translations/zh-Hant.json +++ b/homeassistant/components/lyric/translations/zh-Hant.json @@ -17,5 +17,11 @@ "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" } } + }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Honeywell Lyric \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Honeywell Lyric YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/de.json b/homeassistant/components/mitemp_bt/translations/de.json index 7d4887ab638..3c9e6960aeb 100644 --- a/homeassistant/components/mitemp_bt/translations/de.json +++ b/homeassistant/components/mitemp_bt/translations/de.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "Die Integration des Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensors funktioniert in Home Assistant 2022.7 nicht mehr und wurde in der Version 2022.8 durch die Xiaomi BLE Integration ersetzt.\n\nEs ist kein Migrationspfad m\u00f6glich, daher musst du dein Xiaomi Mijia BLE-Ger\u00e4t manuell mit der neuen Integration hinzuf\u00fcgen.\n\nDeine bestehende Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensor YAML-Konfiguration wird von Home Assistant nicht mehr verwendet. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "description": "Die Integration des Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensors funktioniert in Home Assistant 2022.7 nicht mehr und wurde in der Version 2022.8 durch die Xiaomi BLE Integration ersetzt.\n\nEs ist kein Migrationspfad m\u00f6glich, daher musst du dein Xiaomi Mijia BLE-Ger\u00e4t mit der neuen Integration manuell hinzuf\u00fcgen.\n\nDeine bestehende Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensor YAML-Konfiguration wird von Home Assistant nicht mehr verwendet. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", "title": "Die Integration des Xiaomi Mijia BLE Temperatur- und Luftfeuchtigkeitssensors wurde ersetzt" } } diff --git a/homeassistant/components/mitemp_bt/translations/it.json b/homeassistant/components/mitemp_bt/translations/it.json new file mode 100644 index 00000000000..cc383e4184c --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor ha smesso di funzionare in Home Assistant 2022.7 ed \u00e8 stata sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Xiaomi Mijia BLE utilizzando la nuova integrazione. \n\nLa configurazione YAML di Xiaomi Mijia BLE Temperature and Humidity Sensor esistente non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor \u00e8 stata sostituita" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/pt-BR.json b/homeassistant/components/mitemp_bt/translations/pt-BR.json index 991a749a729..634f5dd71fd 100644 --- a/homeassistant/components/mitemp_bt/translations/pt-BR.json +++ b/homeassistant/components/mitemp_bt/translations/pt-BR.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "A integra\u00e7\u00e3o do sensor de temperatura e umidade Xiaomi Mijia BLE parou de funcionar no Home Assistant 2022.7 e foi substitu\u00edda pela integra\u00e7\u00e3o Xiaomi BLE na vers\u00e3o 2022.8. \n\n N\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o poss\u00edvel, portanto, voc\u00ea deve adicionar seu dispositivo Xiaomi Mijia BLE usando a nova integra\u00e7\u00e3o manualmente. \n\n Sua configura\u00e7\u00e3o YAML existente do sensor de temperatura e umidade Xiaomi Mijia BLE n\u00e3o \u00e9 mais usada pelo Home Assistant. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "description": "A integra\u00e7\u00e3o do sensor de temperatura e umidade do Xiaomi Mijia BLE parou de funcionar no Home Assistant 2022.7 e foi substitu\u00edda pela integra\u00e7\u00e3o do Xiaomi BLE na vers\u00e3o 2022.8. \n\n N\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o poss\u00edvel, portanto, voc\u00ea deve adicionar seu dispositivo Xiaomi Mijia BLE usando a nova integra\u00e7\u00e3o manualmente. \n\n Sua configura\u00e7\u00e3o YAML existente do sensor de temperatura e umidade Xiaomi Mijia BLE n\u00e3o \u00e9 mais usada pelo Home Assistant. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", "title": "A integra\u00e7\u00e3o do sensor de temperatura e umidade Xiaomi Mijia BLE foi substitu\u00edda" } } diff --git a/homeassistant/components/openalpr_local/translations/de.json b/homeassistant/components/openalpr_local/translations/de.json new file mode 100644 index 00000000000..d517fe0b37f --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Die lokale OpenALPR-Integration wird derzeit aus dem Home Assistant entfernt und wird ab Home Assistant 2022.10 nicht mehr verf\u00fcgbar sein.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die lokale OpenALPR-Integration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/it.json b/homeassistant/components/openalpr_local/translations/it.json new file mode 100644 index 00000000000..26ce80ee584 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "L'integrazione OpenALPR Local \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "L'integrazione OpenALPR Local verr\u00e0 rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/pl.json b/homeassistant/components/openalpr_local/translations/pl.json new file mode 100644 index 00000000000..ac367d20809 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integracja OpenALPR Local oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.10. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja OpenALPR Local zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/pt-BR.json b/homeassistant/components/openalpr_local/translations/pt-BR.json new file mode 100644 index 00000000000..96b2c244b5c --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "A integra\u00e7\u00e3o do OpenALPR Local est\u00e1 pendente de remo\u00e7\u00e3o do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.10. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o do OpenALPR Local est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/zh-Hant.json b/homeassistant/components/openalpr_local/translations/zh-Hant.json new file mode 100644 index 00000000000..8ec55e5a004 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "OpenALPR \u672c\u5730\u7aef\u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "OpenALPR \u672c\u5730\u7aef\u6574\u5408\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/it.json b/homeassistant/components/radiotherm/translations/it.json index 9e35beb648a..fef1c64746b 100644 --- a/homeassistant/components/radiotherm/translations/it.json +++ b/homeassistant/components/radiotherm/translations/it.json @@ -21,7 +21,8 @@ }, "issues": { "deprecated_yaml": { - "description": "La configurazione della piattaforma climatica Radio Thermostat tramite YAML \u00e8 stata rimossa in Home Assistant 2022.9. \n\nLa configurazione esistente \u00e8 stata importata automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema." + "description": "La configurazione della piattaforma climatica Radio Thermostat tramite YAML verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLa configurazione esistente \u00e8 stata importata automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Radio Thermostat verr\u00e0 rimossa" } }, "options": { diff --git a/homeassistant/components/senz/translations/de.json b/homeassistant/components/senz/translations/de.json index ffbc7bb458f..fbae91321be 100644 --- a/homeassistant/components/senz/translations/de.json +++ b/homeassistant/components/senz/translations/de.json @@ -16,5 +16,11 @@ "title": "W\u00e4hle die Authentifizierungsmethode" } } + }, + "issues": { + "removed_yaml": { + "description": "Die Konfiguration von nVent RAYCHEM SENZ mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die nVent RAYCHEM SENZ YAML Konfiguration wurde entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/pt-BR.json b/homeassistant/components/senz/translations/pt-BR.json index 7e3ff2f64a9..02c31d97816 100644 --- a/homeassistant/components/senz/translations/pt-BR.json +++ b/homeassistant/components/senz/translations/pt-BR.json @@ -16,5 +16,11 @@ "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" } } + }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do nVent RAYCHEM SENZ usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o nVent RAYCHEM SENZ YAML foi removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/zh-Hant.json b/homeassistant/components/senz/translations/zh-Hant.json index 3bf08cf34c7..7094ee18a02 100644 --- a/homeassistant/components/senz/translations/zh-Hant.json +++ b/homeassistant/components/senz/translations/zh-Hant.json @@ -16,5 +16,11 @@ "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" } } + }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a nVent RAYCHEM SENZ \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "nVent RAYCHEM SENZ YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/de.json b/homeassistant/components/soundtouch/translations/de.json index 8d28b988834..379516e31be 100644 --- a/homeassistant/components/soundtouch/translations/de.json +++ b/homeassistant/components/soundtouch/translations/de.json @@ -17,5 +17,11 @@ "title": "Best\u00e4tige das Hinzuf\u00fcgen des Bose SoundTouch-Ger\u00e4ts" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Bose SoundTouch mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die Bose SoundTouch YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Bose SoundTouch YAML-Konfiguration wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/it.json b/homeassistant/components/soundtouch/translations/it.json index a14492bf5c3..f9c6d512b2a 100644 --- a/homeassistant/components/soundtouch/translations/it.json +++ b/homeassistant/components/soundtouch/translations/it.json @@ -17,5 +17,11 @@ "title": "Conferma l'aggiunta del dispositivo Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Bose SoundTouch tramite YAML verr\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovere la configurazione YAML di Bose SoundTouch dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Bose SoundTouch verr\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/pt-BR.json b/homeassistant/components/soundtouch/translations/pt-BR.json index e707219f342..7446cbc5a06 100644 --- a/homeassistant/components/soundtouch/translations/pt-BR.json +++ b/homeassistant/components/soundtouch/translations/pt-BR.json @@ -17,5 +17,11 @@ "title": "Confirme a adi\u00e7\u00e3o do dispositivo Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Bose SoundTouch usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML do Bose SoundTouch do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Bose SoundTouch est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/zh-Hant.json b/homeassistant/components/soundtouch/translations/zh-Hant.json index 08231b8571d..f3d8e8e8560 100644 --- a/homeassistant/components/soundtouch/translations/zh-Hant.json +++ b/homeassistant/components/soundtouch/translations/zh-Hant.json @@ -17,5 +17,11 @@ "title": "\u78ba\u8a8d\u65b0\u589e Bose SoundTouch \u88dd\u7f6e" } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Bose SoundTouch \u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Bose SoundTouch YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Bose SoundTouch YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/zh-Hant.json b/homeassistant/components/spotify/translations/zh-Hant.json index ce89e224a8c..52773f3e411 100644 --- a/homeassistant/components/spotify/translations/zh-Hant.json +++ b/homeassistant/components/spotify/translations/zh-Hant.json @@ -21,8 +21,8 @@ }, "issues": { "removed_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Spotify \u7684\u529f\u80fd\u5df2\u906d\u5230\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Spotify YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Spotify \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Spotify YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" } }, "system_health": { diff --git a/homeassistant/components/steam_online/translations/zh-Hant.json b/homeassistant/components/steam_online/translations/zh-Hant.json index e3c725532e9..8b0c932735b 100644 --- a/homeassistant/components/steam_online/translations/zh-Hant.json +++ b/homeassistant/components/steam_online/translations/zh-Hant.json @@ -26,8 +26,8 @@ }, "issues": { "removed_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Steam \u7684\u529f\u80fd\u5df2\u906d\u5230\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "Steam YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Steam \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Steam YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" } }, "options": { diff --git a/homeassistant/components/uscis/translations/zh-Hant.json b/homeassistant/components/uscis/translations/zh-Hant.json index 4a5882dbd95..ccc72d3d1dd 100644 --- a/homeassistant/components/uscis/translations/zh-Hant.json +++ b/homeassistant/components/uscis/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "issues": { "pending_removal": { - "description": "\u7f8e\u570b\u516c\u6c11\u8207\u79fb\u6c11\u670d\u52d9\uff08USCIS: U.S. Citizenship and Immigration Services\uff09\u6574\u5408\u6b63\u8a08\u5283\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u8acb\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u6574\u5408\u6b63\u5728\u79fb\u9664\u4e2d\u3001\u7531\u65bc\u4f7f\u7528\u4e86\u7db2\u8def\u8cc7\u6599\u64f7\u53d6\uff08webscraping\uff09\u65b9\u5f0f\u3001\u5c07\u4e0d\u88ab\u5141\u8a31\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "USCIS \u6574\u5408\u6b63\u6e96\u5099\u79fb\u9664" + "description": "\u7f8e\u570b\u516c\u6c11\u8207\u79fb\u6c11\u670d\u52d9\uff08USCIS: U.S. Citizenship and Immigration Services\uff09\u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531\u65bc\u4f7f\u7528\u4e86\u4e0d\u88ab\u5141\u8a31\u7684\u7db2\u8def\u8cc7\u6599\u64f7\u53d6\uff08webscraping\uff09\u65b9\u5f0f\u3001\u6574\u5408\u5373\u5c07\u79fb\u9664\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "USCIS \u6574\u5408\u5373\u5c07\u79fb\u9664" } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/it.json b/homeassistant/components/xbox/translations/it.json index e60c37c9e5f..6cf5bf15bb9 100644 --- a/homeassistant/components/xbox/translations/it.json +++ b/homeassistant/components/xbox/translations/it.json @@ -13,5 +13,11 @@ "title": "Scegli il metodo di autenticazione" } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Xbox in configuration.yaml verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Xbox verr\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/pt-BR.json b/homeassistant/components/xbox/translations/pt-BR.json index 7f788c1ebb8..d1bb02e84dd 100644 --- a/homeassistant/components/xbox/translations/pt-BR.json +++ b/homeassistant/components/xbox/translations/pt-BR.json @@ -13,5 +13,11 @@ "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" } } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Xbox em configuration.yaml est\u00e1 sendo removida no Home Assistant 2022.9. \n\n Suas credenciais de aplicativo OAuth e configura\u00e7\u00f5es de acesso existentes foram importadas para a interface do usu\u00e1rio automaticamente. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Xbox est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 9b8acdf5b87..b4ad58636a7 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -46,11 +46,13 @@ "title": "Optionen f\u00fcr die Alarmsteuerung" }, "zha_options": { + "always_prefer_xy_color_mode": "Immer den XY-Farbmodus bevorzugen", "consider_unavailable_battery": "Batteriebetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)", "consider_unavailable_mains": "Netzbetriebene Ger\u00e4te als nicht verf\u00fcgbar betrachten nach (Sekunden)", "default_light_transition": "Standardlicht\u00fcbergangszeit (Sekunden)", "enable_identify_on_join": "Aktiviere den Identifikationseffekt, wenn Ger\u00e4te dem Netzwerk beitreten", "enhanced_light_transition": "Aktiviere einen verbesserten Lichtfarben-/Temperatur\u00fcbergang aus einem ausgeschalteten Zustand", + "light_transitioning_flag": "Erweiterten Helligkeitsregler w\u00e4hrend des Licht\u00fcbergangs aktivieren", "title": "Globale Optionen" } }, diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index 0b8e90f5ac0..99c519782db 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -46,11 +46,13 @@ "title": "Opcje panelu alarmowego" }, "zha_options": { + "always_prefer_xy_color_mode": "Zawsze preferuj tryb kolor\u00f3w XY", "consider_unavailable_battery": "Uznaj urz\u0105dzenia zasilane bateryjnie za niedost\u0119pne po (sekundach)", "consider_unavailable_mains": "Uznaj urz\u0105dzenia zasilane z gniazdka za niedost\u0119pne po (sekundach)", "default_light_transition": "Domy\u015blny czas efektu przej\u015bcia dla \u015bwiat\u0142a (w sekundach)", "enable_identify_on_join": "W\u0142\u0105cz efekt identyfikacji, gdy urz\u0105dzenia do\u0142\u0105czaj\u0105 do sieci", "enhanced_light_transition": "W\u0142\u0105cz ulepszone przej\u015bcie koloru \u015bwiat\u0142a/temperatury ze stanu wy\u0142\u0105czenia", + "light_transitioning_flag": "W\u0142\u0105cz suwak zwi\u0119kszonej jasno\u015bci podczas przej\u015bcia \u015bwiat\u0142a", "title": "Opcje og\u00f3lne" } }, diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index ba54b4aba87..69b8ced6970 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -46,11 +46,13 @@ "title": "Op\u00e7\u00f5es do painel de controle de alarme" }, "zha_options": { + "always_prefer_xy_color_mode": "Sempre prefira o modo de cor XY", "consider_unavailable_battery": "Considerar dispositivos alimentados por bateria indispon\u00edveis ap\u00f3s (segundos)", "consider_unavailable_mains": "Considerar os dispositivos alimentados pela rede indispon\u00edveis ap\u00f3s (segundos)", "default_light_transition": "Tempo de transi\u00e7\u00e3o de luz padr\u00e3o (segundos)", "enable_identify_on_join": "Ativar o efeito de identifica\u00e7\u00e3o quando os dispositivos ingressarem na rede", "enhanced_light_transition": "Ative a transi\u00e7\u00e3o de cor/temperatura da luz aprimorada de um estado desligado", + "light_transitioning_flag": "Ative o controle deslizante de brilho aprimorado durante a transi\u00e7\u00e3o de luz", "title": "Op\u00e7\u00f5es globais" } }, diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index 9505da31e80..546a2f77c31 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -46,11 +46,13 @@ "title": "\u8b66\u6212\u63a7\u5236\u9762\u677f\u9078\u9805" }, "zha_options": { + "always_prefer_xy_color_mode": "\u504f\u597d XY \u8272\u5f69\u6a21\u5f0f", "consider_unavailable_battery": "\u5c07\u96fb\u6c60\u4f9b\u96fb\u88dd\u7f6e\u8996\u70ba\u4e0d\u53ef\u7528\uff08\u79d2\u6578\uff09", "consider_unavailable_mains": "\u5c07\u4e3b\u4f9b\u96fb\u88dd\u7f6e\u8996\u70ba\u4e0d\u53ef\u7528\uff08\u79d2\u6578\uff09", "default_light_transition": "\u9810\u8a2d\u71c8\u5149\u8f49\u63db\u6642\u9593\uff08\u79d2\uff09", "enable_identify_on_join": "\u7576\u88dd\u7f6e\u52a0\u5165\u7db2\u8def\u6642\u3001\u958b\u555f\u8b58\u5225\u6548\u679c", "enhanced_light_transition": "\u958b\u555f\u7531\u95dc\u9589\u72c0\u614b\u589e\u5f37\u5149\u8272/\u8272\u6eab\u8f49\u63db", + "light_transitioning_flag": "\u958b\u555f\u71c8\u5149\u8f49\u63db\u589e\u5f37\u4eae\u5ea6\u8abf\u6574\u5217", "title": "Global \u9078\u9805" } }, From 7251445ffbae9544a3fdc39b1662b0f7a647f7a9 Mon Sep 17 00:00:00 2001 From: Chaim Turkel Date: Thu, 28 Jul 2022 17:19:20 +0300 Subject: [PATCH 2921/3516] Add shabat sensors to jewish_calendar (#57866) * add shabat sensors * add shabat sensors * add shabat sensors * add shabat sensors * add shabat sensors * Remove redundunt classes and combine sensors * Update homeassistant/components/jewish_calendar/binary_sensor.py Co-authored-by: Yuval Aboulafia * Update homeassistant/components/jewish_calendar/binary_sensor.py Co-authored-by: Yuval Aboulafia * updated requirements * call get_zmanim once * add type hint to entity description * fix errors resulted from type hints introduction * fix mypy error * use attr for state * Update homeassistant/components/jewish_calendar/binary_sensor.py Co-authored-by: Teemu R. * Fix typing Co-authored-by: Yuval Aboulafia Co-authored-by: Teemu R. --- .../jewish_calendar/binary_sensor.py | 53 ++++++++++++++++--- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py index f239dfc31b6..3d28e2bb0c0 100644 --- a/homeassistant/components/jewish_calendar/binary_sensor.py +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -1,9 +1,10 @@ """Support for Jewish Calendar binary sensors.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass import datetime as dt from datetime import datetime -from typing import cast import hdate from hdate.zmanim import Zmanim @@ -20,10 +21,38 @@ import homeassistant.util.dt as dt_util from . import DOMAIN -BINARY_SENSORS = BinarySensorEntityDescription( - key="issur_melacha_in_effect", - name="Issur Melacha in Effect", - icon="mdi:power-plug-off", + +@dataclass +class JewishCalendarBinarySensorMixIns(BinarySensorEntityDescription): + """Binary Sensor description mixin class for Jewish Calendar.""" + + is_on: Callable[..., bool] = lambda _: False + + +@dataclass +class JewishCalendarBinarySensorEntityDescription( + JewishCalendarBinarySensorMixIns, BinarySensorEntityDescription +): + """Binary Sensor Entity description for Jewish Calendar.""" + + +BINARY_SENSORS: tuple[JewishCalendarBinarySensorEntityDescription, ...] = ( + JewishCalendarBinarySensorEntityDescription( + key="issur_melacha_in_effect", + name="Issur Melacha in Effect", + icon="mdi:power-plug-off", + is_on=lambda state: bool(state.issur_melacha_in_effect), + ), + JewishCalendarBinarySensorEntityDescription( + key="erev_shabbat_hag", + name="Erev Shabbat/Hag", + is_on=lambda state: bool(state.erev_shabbat_hag), + ), + JewishCalendarBinarySensorEntityDescription( + key="motzei_shabbat_hag", + name="Motzei Shabbat/Hag", + is_on=lambda state: bool(state.motzei_shabbat_hag), + ), ) @@ -37,20 +66,27 @@ async def async_setup_platform( if discovery_info is None: return - async_add_entities([JewishCalendarBinarySensor(hass.data[DOMAIN], BINARY_SENSORS)]) + async_add_entities( + [ + JewishCalendarBinarySensor(hass.data[DOMAIN], description) + for description in BINARY_SENSORS + ] + ) class JewishCalendarBinarySensor(BinarySensorEntity): """Representation of an Jewish Calendar binary sensor.""" _attr_should_poll = False + entity_description: JewishCalendarBinarySensorEntityDescription def __init__( self, data: dict[str, str | bool | int | float], - description: BinarySensorEntityDescription, + description: JewishCalendarBinarySensorEntityDescription, ) -> None: """Initialize the binary sensor.""" + self.entity_description = description self._attr_name = f"{data['name']} {description.name}" self._attr_unique_id = f"{data['prefix']}_{description.key}" self._location = data["location"] @@ -62,7 +98,8 @@ class JewishCalendarBinarySensor(BinarySensorEntity): @property def is_on(self) -> bool: """Return true if sensor is on.""" - return cast(bool, self._get_zmanim().issur_melacha_in_effect) + zmanim = self._get_zmanim() + return self.entity_description.is_on(zmanim) def _get_zmanim(self) -> Zmanim: """Return the Zmanim object for now().""" From 91180923ae4a50a6b95f5e8a5c54f991139dc71d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 16:43:32 +0200 Subject: [PATCH 2922/3516] Fix HTTP 404 being logged as a stack trace (#75861) --- homeassistant/components/http/static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index c4dc97727a9..6cb1bafdaca 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -33,7 +33,7 @@ def _get_file_path( return None if filepath.is_file(): return filepath - raise HTTPNotFound + raise FileNotFoundError class CachingStaticResource(StaticResource): From 8e2f0497cef2cf61de3da53b99b61dd26b224228 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Thu, 28 Jul 2022 11:24:31 -0400 Subject: [PATCH 2923/3516] ZHA network backup and restore API (#75791) * Implement WS API endpoints for zigpy backups * Implement backup restoration * Display error messages caused by invalid backup JSON * Indicate to the frontend when a backup is incomplete * Perform a coordinator backup before HA performs a backup * Fix `backup.async_post_backup` docstring * Rename `data` to `backup` in restore command * Add unit tests for new websocket APIs * Unit test backup platform * Move code to overwrite EZSP EUI64 into ZHA * Include the radio type in the network settings API response --- homeassistant/components/zha/api.py | 113 ++++++++++++++++++++++- homeassistant/components/zha/backup.py | 21 +++++ tests/components/zha/conftest.py | 12 ++- tests/components/zha/test_api.py | 123 +++++++++++++++++++++++++ tests/components/zha/test_backup.py | 20 ++++ 5 files changed, 286 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/zha/backup.py create mode 100644 tests/components/zha/test_backup.py diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 89d360577d4..40996be3248 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -6,6 +6,8 @@ import logging from typing import TYPE_CHECKING, Any, NamedTuple import voluptuous as vol +import zigpy.backups +from zigpy.backups import NetworkBackup from zigpy.config.validators import cv_boolean from zigpy.types.named import EUI64 from zigpy.zcl.clusters.security import IasAce @@ -43,6 +45,7 @@ from .core.const import ( CLUSTER_COMMANDS_SERVER, CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, + CONF_RADIO_TYPE, CUSTOM_CONFIGURATION, DATA_ZHA, DATA_ZHA_GATEWAY, @@ -229,6 +232,15 @@ def _cv_cluster_binding(value: dict[str, Any]) -> ClusterBinding: ) +def _cv_zigpy_network_backup(value: dict[str, Any]) -> zigpy.backups.NetworkBackup: + """Transform a zigpy network backup.""" + + try: + return zigpy.backups.NetworkBackup.from_dict(value) + except ValueError as err: + raise vol.Invalid(str(err)) from err + + GROUP_MEMBER_SCHEMA = vol.All( vol.Schema( { @@ -302,7 +314,7 @@ async def websocket_permit_devices( ) else: await zha_gateway.application_controller.permit(time_s=duration, node=ieee) - connection.send_result(msg["id"]) + connection.send_result(msg[ID]) @websocket_api.require_admin @@ -989,7 +1001,7 @@ async def websocket_get_configuration( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Get ZHA configuration.""" - zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] import voluptuous_serialize # pylint: disable=import-outside-toplevel def custom_serializer(schema: Any) -> Any: @@ -1047,6 +1059,99 @@ async def websocket_update_zha_configuration( connection.send_result(msg[ID], status) +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required(TYPE): "zha/network/settings"}) +@websocket_api.async_response +async def websocket_get_network_settings( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Get ZHA network settings.""" + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + application_controller = zha_gateway.application_controller + + # Serialize the current network settings + backup = NetworkBackup( + node_info=application_controller.state.node_info, + network_info=application_controller.state.network_info, + ) + + connection.send_result( + msg[ID], + { + "radio_type": zha_gateway.config_entry.data[CONF_RADIO_TYPE], + "settings": backup.as_dict(), + }, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required(TYPE): "zha/network/backups/list"}) +@websocket_api.async_response +async def websocket_list_network_backups( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Get ZHA network settings.""" + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + application_controller = zha_gateway.application_controller + + # Serialize known backups + connection.send_result( + msg[ID], [backup.as_dict() for backup in application_controller.backups] + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required(TYPE): "zha/network/backups/create"}) +@websocket_api.async_response +async def websocket_create_network_backup( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Create a ZHA network backup.""" + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + application_controller = zha_gateway.application_controller + + # This can take 5-30s + backup = await application_controller.backups.create_backup(load_devices=True) + connection.send_result( + msg[ID], + { + "backup": backup.as_dict(), + "is_complete": backup.is_complete(), + }, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zha/network/backups/restore", + vol.Required("backup"): _cv_zigpy_network_backup, + vol.Optional("ezsp_force_write_eui64", default=False): cv.boolean, + } +) +@websocket_api.async_response +async def websocket_restore_network_backup( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: + """Restore a ZHA network backup.""" + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + application_controller = zha_gateway.application_controller + backup = msg["backup"] + + if msg["ezsp_force_write_eui64"]: + backup.network_info.stack_specific.setdefault("ezsp", {})[ + "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" + ] = True + + # This can take 30-40s + try: + await application_controller.backups.restore_backup(backup) + except ValueError as err: + connection.send_error(msg[ID], websocket_api.const.ERR_INVALID_FORMAT, str(err)) + else: + connection.send_result(msg[ID]) + + @callback def async_load_api(hass: HomeAssistant) -> None: """Set up the web socket API.""" @@ -1356,6 +1461,10 @@ def async_load_api(hass: HomeAssistant) -> None: websocket_api.async_register_command(hass, websocket_update_topology) websocket_api.async_register_command(hass, websocket_get_configuration) websocket_api.async_register_command(hass, websocket_update_zha_configuration) + websocket_api.async_register_command(hass, websocket_get_network_settings) + websocket_api.async_register_command(hass, websocket_list_network_backups) + websocket_api.async_register_command(hass, websocket_create_network_backup) + websocket_api.async_register_command(hass, websocket_restore_network_backup) @callback diff --git a/homeassistant/components/zha/backup.py b/homeassistant/components/zha/backup.py new file mode 100644 index 00000000000..89d5294e1c4 --- /dev/null +++ b/homeassistant/components/zha/backup.py @@ -0,0 +1,21 @@ +"""Backup platform for the ZHA integration.""" +import logging + +from homeassistant.core import HomeAssistant + +from .core import ZHAGateway +from .core.const import DATA_ZHA, DATA_ZHA_GATEWAY + +_LOGGER = logging.getLogger(__name__) + + +async def async_pre_backup(hass: HomeAssistant) -> None: + """Perform operations before a backup starts.""" + _LOGGER.debug("Performing coordinator backup") + + zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + await zha_gateway.application_controller.backups.create_backup(load_devices=True) + + +async def async_post_backup(hass: HomeAssistant) -> None: + """Perform operations after a backup finishes.""" diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index b1041a3e2a3..27155e16cc7 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch import pytest import zigpy from zigpy.application import ControllerApplication +import zigpy.backups import zigpy.config from zigpy.const import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE import zigpy.device @@ -54,7 +55,16 @@ def zigpy_app_controller(): app.ieee.return_value = zigpy.types.EUI64.convert("00:15:8d:00:02:32:4f:32") type(app).nwk = PropertyMock(return_value=zigpy.types.NWK(0x0000)) type(app).devices = PropertyMock(return_value={}) - type(app).state = PropertyMock(return_value=State()) + type(app).backups = zigpy.backups.BackupManager(app) + + state = State() + state.node_info.ieee = app.ieee.return_value + state.network_info.extended_pan_id = app.ieee.return_value + state.network_info.pan_id = 0x1234 + state.network_info.channel = 15 + state.network_info.network_key.key = zigpy.types.KeyData(range(16)) + type(app).state = PropertyMock(return_value=state) + return app diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index 08766bc74ac..b25bffebec7 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, patch import pytest import voluptuous as vol +import zigpy.backups import zigpy.profiles.zha import zigpy.types import zigpy.zcl.clusters.general as general @@ -620,3 +621,125 @@ async def test_ws_permit_ha12(app_controller, zha_client, params, duration, node assert app_controller.permit.await_args[1]["time_s"] == duration assert app_controller.permit.await_args[1]["node"] == node assert app_controller.permit_with_key.call_count == 0 + + +async def test_get_network_settings(app_controller, zha_client): + """Test current network settings are returned.""" + + await app_controller.backups.create_backup() + + await zha_client.send_json({ID: 6, TYPE: f"{DOMAIN}/network/settings"}) + msg = await zha_client.receive_json() + + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert "radio_type" in msg["result"] + assert "network_info" in msg["result"]["settings"] + + +async def test_list_network_backups(app_controller, zha_client): + """Test backups are serialized.""" + + await app_controller.backups.create_backup() + + await zha_client.send_json({ID: 6, TYPE: f"{DOMAIN}/network/backups/list"}) + msg = await zha_client.receive_json() + + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert "network_info" in msg["result"][0] + + +async def test_create_network_backup(app_controller, zha_client): + """Test creating backup.""" + + assert not app_controller.backups.backups + await zha_client.send_json({ID: 6, TYPE: f"{DOMAIN}/network/backups/create"}) + msg = await zha_client.receive_json() + assert len(app_controller.backups.backups) == 1 + + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + assert "backup" in msg["result"] and "is_complete" in msg["result"] + + +async def test_restore_network_backup_success(app_controller, zha_client): + """Test successfully restoring a backup.""" + + backup = zigpy.backups.NetworkBackup() + + with patch.object(app_controller.backups, "restore_backup", new=AsyncMock()) as p: + await zha_client.send_json( + { + ID: 6, + TYPE: f"{DOMAIN}/network/backups/restore", + "backup": backup.as_dict(), + } + ) + msg = await zha_client.receive_json() + + p.assert_called_once_with(backup) + assert "ezsp" not in backup.network_info.stack_specific + + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + +async def test_restore_network_backup_force_write_eui64(app_controller, zha_client): + """Test successfully restoring a backup.""" + + backup = zigpy.backups.NetworkBackup() + + with patch.object(app_controller.backups, "restore_backup", new=AsyncMock()) as p: + await zha_client.send_json( + { + ID: 6, + TYPE: f"{DOMAIN}/network/backups/restore", + "backup": backup.as_dict(), + "ezsp_force_write_eui64": True, + } + ) + msg = await zha_client.receive_json() + + # EUI64 will be overwritten + p.assert_called_once_with( + backup.replace( + network_info=backup.network_info.replace( + stack_specific={ + "ezsp": { + "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it": True + } + } + ) + ) + ) + + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + +@patch("zigpy.backups.NetworkBackup.from_dict", new=lambda v: v) +async def test_restore_network_backup_failure(app_controller, zha_client): + """Test successfully restoring a backup.""" + + with patch.object( + app_controller.backups, + "restore_backup", + new=AsyncMock(side_effect=ValueError("Restore failed")), + ) as p: + await zha_client.send_json( + {ID: 6, TYPE: f"{DOMAIN}/network/backups/restore", "backup": "a backup"} + ) + msg = await zha_client.receive_json() + + p.assert_called_once_with("a backup") + + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == const.ERR_INVALID_FORMAT diff --git a/tests/components/zha/test_backup.py b/tests/components/zha/test_backup.py new file mode 100644 index 00000000000..aea50cf0923 --- /dev/null +++ b/tests/components/zha/test_backup.py @@ -0,0 +1,20 @@ +"""Unit tests for ZHA backup platform.""" + +from unittest.mock import AsyncMock, patch + +from homeassistant.components.zha.backup import async_post_backup, async_pre_backup + + +async def test_pre_backup(hass, setup_zha): + """Test backup creation when `async_pre_backup` is called.""" + with patch("zigpy.backups.BackupManager.create_backup", AsyncMock()) as backup_mock: + await setup_zha() + await async_pre_backup(hass) + + backup_mock.assert_called_once_with(load_devices=True) + + +async def test_post_backup(hass, setup_zha): + """Test no-op `async_post_backup`.""" + await setup_zha() + await async_post_backup(hass) From 1012064bb7d7aa6e83dc71ac2d0de260619a4f9f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 18:13:16 +0200 Subject: [PATCH 2924/3516] Remove state class from daily net sensors in DSMR Reader (#75864) --- homeassistant/components/dsmr_reader/definitions.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 9f4ee7ed918..ac61837afec 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -225,42 +225,36 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Low tariff usage", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity2", name="High tariff usage", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity1_returned", name="Low tariff return", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity2_returned", name="High tariff return", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity_merged", name="Power usage total", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity_returned_merged", name="Power return total", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity1_cost", From 16e75f2a134ade73f38420463a42f960430ad3e6 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 18:15:27 +0200 Subject: [PATCH 2925/3516] Fix incorrect sensor key in DSMR (#75865) --- homeassistant/components/dsmr/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 7ab1a3bb45b..aa01c798072 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -85,7 +85,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key="electricity_delivery", + key="current_electricity_delivery", name="Power production", obis_reference=obis_references.CURRENT_ELECTRICITY_DELIVERY, device_class=SensorDeviceClass.POWER, From a020482c232d8902088c1123f28562f14ddd5e2a Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 28 Jul 2022 11:20:10 -0500 Subject: [PATCH 2926/3516] Update frontend to 20220728.0 (#75872) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 2a1fb2d3b37..45331491aa0 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220727.0"], + "requirements": ["home-assistant-frontend==20220728.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2554f0185b4..1f86ff4c7c5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.1 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220727.0 +home-assistant-frontend==20220728.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index b4b84a84fd1..4b1232999c2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -839,7 +839,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220727.0 +home-assistant-frontend==20220728.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c8ac231bea1..cf4c54297a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,7 +616,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220727.0 +home-assistant-frontend==20220728.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 166e58eaa4f1d1538b47a84a1b3e980712fa8a23 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 18:20:39 +0200 Subject: [PATCH 2927/3516] Fix camera token to trigger authentication IP ban (#75870) --- homeassistant/components/camera/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 247f73c89f2..3bf86dedea1 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -715,7 +715,9 @@ class CameraView(HomeAssistantView): ) if not authenticated: - raise web.HTTPUnauthorized() + if request[KEY_AUTHENTICATED]: + raise web.HTTPUnauthorized() + raise web.HTTPForbidden() if not camera.is_on: _LOGGER.debug("Camera is off") From 4ed0463438c86c460e6c425fe5a6efc96723b760 Mon Sep 17 00:00:00 2001 From: Brandon West Date: Thu, 28 Jul 2022 12:27:48 -0400 Subject: [PATCH 2928/3516] Bump russound_rio to 0.1.8 (#75837) --- homeassistant/components/russound_rio/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 4b9b7a2c8d0..e844f478322 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -2,7 +2,7 @@ "domain": "russound_rio", "name": "Russound RIO", "documentation": "https://www.home-assistant.io/integrations/russound_rio", - "requirements": ["russound_rio==0.1.7"], + "requirements": ["russound_rio==0.1.8"], "codeowners": [], "iot_class": "local_push", "loggers": ["russound_rio"] diff --git a/requirements_all.txt b/requirements_all.txt index 4b1232999c2..593bb4e040c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2126,7 +2126,7 @@ rtsp-to-webrtc==0.5.1 russound==0.1.9 # homeassistant.components.russound_rio -russound_rio==0.1.7 +russound_rio==0.1.8 # homeassistant.components.yamaha rxv==0.7.0 From 3b8650d05353ad2c43d8175469f162ca6b0015b7 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 28 Jul 2022 11:41:03 +0200 Subject: [PATCH 2929/3516] Make Axis utilise forward_entry_setups (#75178) --- homeassistant/components/axis/__init__.py | 26 ++++-- homeassistant/components/axis/config_flow.py | 11 +-- homeassistant/components/axis/device.py | 86 ++++++++------------ tests/components/axis/test_config_flow.py | 4 +- tests/components/axis/test_device.py | 17 ++-- 5 files changed, 62 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 5e211c00028..4af066f4e89 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -4,28 +4,36 @@ import logging from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, CONF_MAC, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.entity_registry import async_migrate_entries -from .const import DOMAIN as AXIS_DOMAIN -from .device import AxisNetworkDevice +from .const import DOMAIN as AXIS_DOMAIN, PLATFORMS +from .device import AxisNetworkDevice, get_axis_device +from .errors import AuthenticationRequired, CannotConnect _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: - """Set up the Axis component.""" + """Set up the Axis integration.""" hass.data.setdefault(AXIS_DOMAIN, {}) - device = AxisNetworkDevice(hass, config_entry) - - if not await device.async_setup(): - return False - - hass.data[AXIS_DOMAIN][config_entry.unique_id] = device + try: + api = await get_axis_device(hass, config_entry.data) + except CannotConnect as err: + raise ConfigEntryNotReady from err + except AuthenticationRequired as err: + raise ConfigEntryAuthFailed from err + device = hass.data[AXIS_DOMAIN][config_entry.unique_id] = AxisNetworkDevice( + hass, config_entry, api + ) await device.async_update_device_registry() + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) + device.async_setup_events() + config_entry.add_update_listener(device.async_new_address_callback) config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown) ) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index f94c27dc2ac..1ce2f08c045 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Mapping from ipaddress import ip_address +from types import MappingProxyType from typing import Any from urllib.parse import urlsplit @@ -32,7 +33,7 @@ from .const import ( DEFAULT_VIDEO_SOURCE, DOMAIN as AXIS_DOMAIN, ) -from .device import AxisNetworkDevice, get_device +from .device import AxisNetworkDevice, get_axis_device from .errors import AuthenticationRequired, CannotConnect AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f"} @@ -66,13 +67,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): if user_input is not None: try: - device = await get_device( - self.hass, - host=user_input[CONF_HOST], - port=user_input[CONF_PORT], - username=user_input[CONF_USERNAME], - password=user_input[CONF_PASSWORD], - ) + device = await get_axis_device(self.hass, MappingProxyType(user_input)) serial = device.vapix.serial_number await self.async_set_unique_id(format_mac(serial)) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index d0d5e230d2f..683991d0f65 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -1,6 +1,8 @@ """Axis network device abstraction.""" import asyncio +from types import MappingProxyType +from typing import Any import async_timeout import axis @@ -24,7 +26,6 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -50,15 +51,15 @@ from .errors import AuthenticationRequired, CannotConnect class AxisNetworkDevice: """Manages a Axis device.""" - def __init__(self, hass, config_entry): + def __init__(self, hass, config_entry, api): """Initialize the device.""" self.hass = hass self.config_entry = config_entry - self.available = True + self.api = api - self.api = None - self.fw_version = None - self.product_type = None + self.available = True + self.fw_version = api.vapix.firmware_version + self.product_type = api.vapix.product_type @property def host(self): @@ -184,7 +185,7 @@ class AxisNetworkDevice: sw_version=self.fw_version, ) - async def use_mqtt(self, hass: HomeAssistant, component: str) -> None: + async def async_use_mqtt(self, hass: HomeAssistant, component: str) -> None: """Set up to use MQTT.""" try: status = await self.api.vapix.mqtt.get_client_status() @@ -209,50 +210,18 @@ class AxisNetworkDevice: # Setup and teardown methods - async def async_setup(self): - """Set up the device.""" - try: - self.api = await get_device( - self.hass, - host=self.host, - port=self.port, - username=self.username, - password=self.password, + def async_setup_events(self): + """Set up the device events.""" + + if self.option_events: + self.api.stream.connection_status_callback.append( + self.async_connection_status_callback ) + self.api.enable_events(event_callback=self.async_event_callback) + self.api.stream.start() - except CannotConnect as err: - raise ConfigEntryNotReady from err - - except AuthenticationRequired as err: - raise ConfigEntryAuthFailed from err - - self.fw_version = self.api.vapix.firmware_version - self.product_type = self.api.vapix.product_type - - async def start_platforms(): - await asyncio.gather( - *( - self.hass.config_entries.async_forward_entry_setup( - self.config_entry, platform - ) - for platform in PLATFORMS - ) - ) - if self.option_events: - self.api.stream.connection_status_callback.append( - self.async_connection_status_callback - ) - self.api.enable_events(event_callback=self.async_event_callback) - self.api.stream.start() - - if self.api.vapix.mqtt: - async_when_setup(self.hass, MQTT_DOMAIN, self.use_mqtt) - - self.hass.async_create_task(start_platforms()) - - self.config_entry.add_update_listener(self.async_new_address_callback) - - return True + if self.api.vapix.mqtt: + async_when_setup(self.hass, MQTT_DOMAIN, self.async_use_mqtt) @callback def disconnect_from_stream(self): @@ -274,14 +243,21 @@ class AxisNetworkDevice: ) -async def get_device( - hass: HomeAssistant, host: str, port: int, username: str, password: str +async def get_axis_device( + hass: HomeAssistant, + config: MappingProxyType[str, Any], ) -> axis.AxisDevice: """Create a Axis device.""" session = get_async_client(hass, verify_ssl=False) device = axis.AxisDevice( - Configuration(session, host, port=port, username=username, password=password) + Configuration( + session, + config[CONF_HOST], + port=config[CONF_PORT], + username=config[CONF_USERNAME], + password=config[CONF_PASSWORD], + ) ) try: @@ -291,11 +267,13 @@ async def get_device( return device except axis.Unauthorized as err: - LOGGER.warning("Connected to device at %s but not registered", host) + LOGGER.warning( + "Connected to device at %s but not registered", config[CONF_HOST] + ) raise AuthenticationRequired from err except (asyncio.TimeoutError, axis.RequestError) as err: - LOGGER.error("Error connecting to the Axis device at %s", host) + LOGGER.error("Error connecting to the Axis device at %s", config[CONF_HOST]) raise CannotConnect from err except axis.AxisException as err: diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 1459ae215d9..2daf350ac93 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -123,7 +123,7 @@ async def test_flow_fails_faulty_credentials(hass): assert result["step_id"] == SOURCE_USER with patch( - "homeassistant.components.axis.config_flow.get_device", + "homeassistant.components.axis.config_flow.get_axis_device", side_effect=config_flow.AuthenticationRequired, ): result = await hass.config_entries.flow.async_configure( @@ -149,7 +149,7 @@ async def test_flow_fails_cannot_connect(hass): assert result["step_id"] == SOURCE_USER with patch( - "homeassistant.components.axis.config_flow.get_device", + "homeassistant.components.axis.config_flow.get_axis_device", side_effect=config_flow.CannotConnect, ): result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index 4717e2915c1..ba6df6e2e2d 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -441,7 +441,7 @@ async def test_device_reset(hass): async def test_device_not_accessible(hass): """Failed setup schedules a retry of setup.""" - with patch.object(axis.device, "get_device", side_effect=axis.errors.CannotConnect): + with patch.object(axis, "get_axis_device", side_effect=axis.errors.CannotConnect): await setup_axis_integration(hass) assert hass.data[AXIS_DOMAIN] == {} @@ -449,7 +449,7 @@ async def test_device_not_accessible(hass): async def test_device_trigger_reauth_flow(hass): """Failed authentication trigger a reauthentication flow.""" with patch.object( - axis.device, "get_device", side_effect=axis.errors.AuthenticationRequired + axis, "get_axis_device", side_effect=axis.errors.AuthenticationRequired ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: await setup_axis_integration(hass) mock_flow_init.assert_called_once() @@ -458,7 +458,7 @@ async def test_device_trigger_reauth_flow(hass): async def test_device_unknown_error(hass): """Unknown errors are handled.""" - with patch.object(axis.device, "get_device", side_effect=Exception): + with patch.object(axis, "get_axis_device", side_effect=Exception): await setup_axis_integration(hass) assert hass.data[AXIS_DOMAIN] == {} @@ -468,7 +468,7 @@ async def test_new_event_sends_signal(hass): entry = Mock() entry.data = ENTRY_CONFIG - axis_device = axis.device.AxisNetworkDevice(hass, entry) + axis_device = axis.device.AxisNetworkDevice(hass, entry, Mock()) with patch.object(axis.device, "async_dispatcher_send") as mock_dispatch_send: axis_device.async_event_callback(action=OPERATION_INITIALIZED, event_id="event") @@ -484,8 +484,7 @@ async def test_shutdown(): entry = Mock() entry.data = ENTRY_CONFIG - axis_device = axis.device.AxisNetworkDevice(hass, entry) - axis_device.api = Mock() + axis_device = axis.device.AxisNetworkDevice(hass, entry, Mock()) await axis_device.shutdown(None) @@ -497,7 +496,7 @@ async def test_get_device_fails(hass): with patch( "axis.vapix.Vapix.request", side_effect=axislib.Unauthorized ), pytest.raises(axis.errors.AuthenticationRequired): - await axis.device.get_device(hass, host="", port="", username="", password="") + await axis.device.get_axis_device(hass, ENTRY_CONFIG) async def test_get_device_device_unavailable(hass): @@ -505,7 +504,7 @@ async def test_get_device_device_unavailable(hass): with patch( "axis.vapix.Vapix.request", side_effect=axislib.RequestError ), pytest.raises(axis.errors.CannotConnect): - await axis.device.get_device(hass, host="", port="", username="", password="") + await axis.device.get_axis_device(hass, ENTRY_CONFIG) async def test_get_device_unknown_error(hass): @@ -513,4 +512,4 @@ async def test_get_device_unknown_error(hass): with patch( "axis.vapix.Vapix.request", side_effect=axislib.AxisException ), pytest.raises(axis.errors.AuthenticationRequired): - await axis.device.get_device(hass, host="", port="", username="", password="") + await axis.device.get_axis_device(hass, ENTRY_CONFIG) From 70731c0bc7042da51db5bc0a122a8a0f1f5e86eb Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Wed, 27 Jul 2022 16:06:33 -0400 Subject: [PATCH 2930/3516] Add Insteon lock and load controller devices (#75632) --- homeassistant/components/insteon/const.py | 1 + homeassistant/components/insteon/ipdb.py | 5 + homeassistant/components/insteon/lock.py | 49 ++++++++ .../components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/insteon/mock_devices.py | 21 +++- tests/components/insteon/test_lock.py | 109 ++++++++++++++++++ 8 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/insteon/lock.py create mode 100644 tests/components/insteon/test_lock.py diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py index fb7b2387d73..5337ccd36c3 100644 --- a/homeassistant/components/insteon/const.py +++ b/homeassistant/components/insteon/const.py @@ -44,6 +44,7 @@ INSTEON_PLATFORMS = [ Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.SWITCH, ] diff --git a/homeassistant/components/insteon/ipdb.py b/homeassistant/components/insteon/ipdb.py index 6866e052368..7f4ff92380f 100644 --- a/homeassistant/components/insteon/ipdb.py +++ b/homeassistant/components/insteon/ipdb.py @@ -1,5 +1,6 @@ """Utility methods for the Insteon platform.""" from pyinsteon.device_types import ( + AccessControl_Morningstar, ClimateControl_Thermostat, ClimateControl_WirelessThermostat, DimmableLightingControl, @@ -12,6 +13,7 @@ from pyinsteon.device_types import ( DimmableLightingControl_OutletLinc, DimmableLightingControl_SwitchLinc, DimmableLightingControl_ToggleLinc, + EnergyManagement_LoadController, GeneralController_ControlLinc, GeneralController_MiniRemote_4, GeneralController_MiniRemote_8, @@ -44,11 +46,13 @@ from homeassistant.components.climate import DOMAIN as CLIMATE from homeassistant.components.cover import DOMAIN as COVER from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.light import DOMAIN as LIGHT +from homeassistant.components.lock import DOMAIN as LOCK from homeassistant.components.switch import DOMAIN as SWITCH from .const import ON_OFF_EVENTS DEVICE_PLATFORM = { + AccessControl_Morningstar: {LOCK: [1]}, DimmableLightingControl: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_DinRail: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_FanLinc: {LIGHT: [1], FAN: [2], ON_OFF_EVENTS: [1, 2]}, @@ -67,6 +71,7 @@ DEVICE_PLATFORM = { DimmableLightingControl_OutletLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_SwitchLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, DimmableLightingControl_ToggleLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, + EnergyManagement_LoadController: {SWITCH: [1], BINARY_SENSOR: [2]}, GeneralController_ControlLinc: {ON_OFF_EVENTS: [1]}, GeneralController_MiniRemote_4: {ON_OFF_EVENTS: range(1, 5)}, GeneralController_MiniRemote_8: {ON_OFF_EVENTS: range(1, 9)}, diff --git a/homeassistant/components/insteon/lock.py b/homeassistant/components/insteon/lock.py new file mode 100644 index 00000000000..17a7cf20111 --- /dev/null +++ b/homeassistant/components/insteon/lock.py @@ -0,0 +1,49 @@ +"""Support for INSTEON locks.""" + +from typing import Any + +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import SIGNAL_ADD_ENTITIES +from .insteon_entity import InsteonEntity +from .utils import async_add_insteon_entities + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Insteon locks from a config entry.""" + + @callback + def async_add_insteon_lock_entities(discovery_info=None): + """Add the Insteon entities for the platform.""" + async_add_insteon_entities( + hass, LOCK_DOMAIN, InsteonLockEntity, async_add_entities, discovery_info + ) + + signal = f"{SIGNAL_ADD_ENTITIES}_{LOCK_DOMAIN}" + async_dispatcher_connect(hass, signal, async_add_insteon_lock_entities) + async_add_insteon_lock_entities() + + +class InsteonLockEntity(InsteonEntity, LockEntity): + """A Class for an Insteon lock entity.""" + + @property + def is_locked(self) -> bool: + """Return the boolean response if the node is on.""" + return bool(self._insteon_device_group.value) + + async def async_lock(self, **kwargs: Any) -> None: + """Lock the device.""" + await self._insteon_device.async_lock() + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock the device.""" + await self._insteon_device.async_unlock() diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index c48d502c16e..577383e8976 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/insteon", "dependencies": ["http", "websocket_api"], "requirements": [ - "pyinsteon==1.1.3", + "pyinsteon==1.2.0", "insteon-frontend-home-assistant==0.2.0" ], "codeowners": ["@teharris1"], diff --git a/requirements_all.txt b/requirements_all.txt index ea3f2dd3e6c..57ddf87a256 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1569,7 +1569,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.3 +pyinsteon==1.2.0 # homeassistant.components.intesishome pyintesishome==1.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9f19f813747..1f650a56a30 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1076,7 +1076,7 @@ pyialarm==2.2.0 pyicloud==1.0.0 # homeassistant.components.insteon -pyinsteon==1.1.3 +pyinsteon==1.2.0 # homeassistant.components.ipma pyipma==2.0.5 diff --git a/tests/components/insteon/mock_devices.py b/tests/components/insteon/mock_devices.py index ef64b1e0969..417769d6696 100644 --- a/tests/components/insteon/mock_devices.py +++ b/tests/components/insteon/mock_devices.py @@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from pyinsteon.address import Address from pyinsteon.constants import ALDBStatus, ResponseStatus from pyinsteon.device_types import ( + AccessControl_Morningstar, DimmableLightingControl_KeypadLinc_8, GeneralController_RemoteLinc, Hub, @@ -59,12 +60,13 @@ class MockDevices: async def async_load(self, *args, **kwargs): """Load the mock devices.""" - if self._connected: + if self._connected and not self._devices: addr0 = Address("AA.AA.AA") addr1 = Address("11.11.11") addr2 = Address("22.22.22") addr3 = Address("33.33.33") addr4 = Address("44.44.44") + addr5 = Address("55.55.55") self._devices[addr0] = Hub(addr0, 0x03, 0x00, 0x00, "Hub AA.AA.AA", "0") self._devices[addr1] = MockSwitchLinc( addr1, 0x02, 0x00, 0x00, "Device 11.11.11", "1" @@ -78,9 +80,12 @@ class MockDevices: self._devices[addr4] = SensorsActuators_IOLink( addr4, 0x07, 0x00, 0x00, "Device 44.44.44", "4" ) + self._devices[addr5] = AccessControl_Morningstar( + addr5, 0x0F, 0x0A, 0x00, "Device 55.55.55", "5" + ) for device in [ - self._devices[addr] for addr in [addr1, addr2, addr3, addr4] + self._devices[addr] for addr in [addr1, addr2, addr3, addr4, addr5] ]: device.async_read_config = AsyncMock() device.aldb.async_write = AsyncMock() @@ -99,7 +104,9 @@ class MockDevices: return_value=ResponseStatus.SUCCESS ) - for device in [self._devices[addr] for addr in [addr2, addr3, addr4]]: + for device in [ + self._devices[addr] for addr in [addr2, addr3, addr4, addr5] + ]: device.async_status = AsyncMock() self._devices[addr1].async_status = AsyncMock(side_effect=AttributeError) self._devices[addr0].aldb.async_load = AsyncMock() @@ -117,6 +124,12 @@ class MockDevices: return_value=ResponseStatus.FAILURE ) + self._devices[addr5].async_lock = AsyncMock( + return_value=ResponseStatus.SUCCESS + ) + self._devices[addr5].async_unlock = AsyncMock( + return_value=ResponseStatus.SUCCESS + ) self.modem = self._devices[addr0] self.modem.async_read_config = AsyncMock() @@ -155,6 +168,6 @@ class MockDevices: yield address await asyncio.sleep(0.01) - def subscribe(self, listener): + def subscribe(self, listener, force_strong_ref=False): """Mock the subscribe function.""" subscribe_topic(listener, DEVICE_LIST_CHANGED) diff --git a/tests/components/insteon/test_lock.py b/tests/components/insteon/test_lock.py new file mode 100644 index 00000000000..6f847543a9f --- /dev/null +++ b/tests/components/insteon/test_lock.py @@ -0,0 +1,109 @@ +"""Tests for the Insteon lock.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components import insteon +from homeassistant.components.insteon import ( + DOMAIN, + insteon_entity, + utils as insteon_utils, +) +from homeassistant.components.lock import ( # SERVICE_LOCK,; SERVICE_UNLOCK, + DOMAIN as LOCK_DOMAIN, +) +from homeassistant.const import ( # ATTR_ENTITY_ID,; + EVENT_HOMEASSISTANT_STOP, + STATE_LOCKED, + STATE_UNLOCKED, + Platform, +) +from homeassistant.helpers import entity_registry as er + +from .const import MOCK_USER_INPUT_PLM +from .mock_devices import MockDevices + +from tests.common import MockConfigEntry + +devices = MockDevices() + + +@pytest.fixture(autouse=True) +def lock_platform_only(): + """Only setup the lock and required base platforms to speed up tests.""" + with patch( + "homeassistant.components.insteon.INSTEON_PLATFORMS", + (Platform.LOCK,), + ): + yield + + +@pytest.fixture(autouse=True) +def patch_setup_and_devices(): + """Patch the Insteon setup process and devices.""" + with patch.object(insteon, "async_connect", new=mock_connection), patch.object( + insteon, "async_close" + ), patch.object(insteon, "devices", devices), patch.object( + insteon_utils, "devices", devices + ), patch.object( + insteon_entity, "devices", devices + ): + yield + + +async def mock_connection(*args, **kwargs): + """Return a successful connection.""" + return True + + +async def test_lock_lock(hass): + """Test locking an Insteon lock device.""" + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT_PLM) + config_entry.add_to_hass(hass) + registry_entity = er.async_get(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + try: + lock = registry_entity.async_get("lock.device_55_55_55_55_55_55") + state = hass.states.get(lock.entity_id) + assert state.state is STATE_UNLOCKED + + # lock via UI + await hass.services.async_call( + LOCK_DOMAIN, "lock", {"entity_id": lock.entity_id}, blocking=True + ) + assert devices["55.55.55"].async_lock.call_count == 1 + finally: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + +async def test_lock_unlock(hass): + """Test locking an Insteon lock device.""" + + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT_PLM) + config_entry.add_to_hass(hass) + registry_entity = er.async_get(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + devices["55.55.55"].groups[1].set_value(255) + + try: + lock = registry_entity.async_get("lock.device_55_55_55_55_55_55") + state = hass.states.get(lock.entity_id) + + assert state.state is STATE_LOCKED + + # lock via UI + await hass.services.async_call( + LOCK_DOMAIN, "unlock", {"entity_id": lock.entity_id}, blocking=True + ) + assert devices["55.55.55"].async_unlock.call_count == 1 + finally: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() From 7811518d7c6c538c32242e4f8e575157b80da03a Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Wed, 27 Jul 2022 16:39:39 -0500 Subject: [PATCH 2931/3516] Add Leviton as a supported brand of ZwaveJS (#75729) Co-authored-by: Martin Hjelmare --- homeassistant/components/zwave_js/manifest.json | 5 ++++- homeassistant/generated/supported_brands.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 76f5d94b589..8b6ecefc5f5 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -30,5 +30,8 @@ } ], "zeroconf": ["_zwave-js-server._tcp.local."], - "loggers": ["zwave_js_server"] + "loggers": ["zwave_js_server"], + "supported_brands": { + "leviton_z_wave": "Leviton Z-Wave" + } } diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py index 589e0462cf7..4e151f5578d 100644 --- a/homeassistant/generated/supported_brands.py +++ b/homeassistant/generated/supported_brands.py @@ -11,5 +11,6 @@ HAS_SUPPORTED_BRANDS = ( "motion_blinds", "overkiz", "renault", - "wemo" + "wemo", + "zwave_js" ) From 15e6fcca41f42ba666881f8170207613a2104884 Mon Sep 17 00:00:00 2001 From: Brandon West Date: Thu, 28 Jul 2022 12:27:48 -0400 Subject: [PATCH 2932/3516] Bump russound_rio to 0.1.8 (#75837) --- homeassistant/components/russound_rio/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 4b9b7a2c8d0..e844f478322 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -2,7 +2,7 @@ "domain": "russound_rio", "name": "Russound RIO", "documentation": "https://www.home-assistant.io/integrations/russound_rio", - "requirements": ["russound_rio==0.1.7"], + "requirements": ["russound_rio==0.1.8"], "codeowners": [], "iot_class": "local_push", "loggers": ["russound_rio"] diff --git a/requirements_all.txt b/requirements_all.txt index 57ddf87a256..95da109691c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2123,7 +2123,7 @@ rtsp-to-webrtc==0.5.1 russound==0.1.9 # homeassistant.components.russound_rio -russound_rio==0.1.7 +russound_rio==0.1.8 # homeassistant.components.yamaha rxv==0.7.0 From 96587c1227efae30865acc9f074dce62cab229a5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Jul 2022 12:59:43 -0700 Subject: [PATCH 2933/3516] Remove learn more URL from Home Assistant alerts (#75838) --- .../homeassistant_alerts/__init__.py | 5 +-- .../fixtures/alerts_no_url.json | 34 ------------------- .../homeassistant_alerts/test_init.py | 15 +++----- 3 files changed, 5 insertions(+), 49 deletions(-) delete mode 100644 tests/components/homeassistant_alerts/fixtures/alerts_no_url.json diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index 1aedd6c5419..12ba4dce8ba 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -75,7 +75,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, issue_id, is_fixable=False, - learn_more_url=alert.alert_url, severity=IssueSeverity.WARNING, translation_key="alert", translation_placeholders={ @@ -112,7 +111,6 @@ class IntegrationAlert: integration: str filename: str date_updated: str | None - alert_url: str | None @property def issue_id(self) -> str: @@ -147,7 +145,7 @@ class AlertUpdateCoordinator(DataUpdateCoordinator[dict[str, IntegrationAlert]]) result = {} for alert in alerts: - if "alert_url" not in alert or "integrations" not in alert: + if "integrations" not in alert: continue if "homeassistant" in alert: @@ -177,7 +175,6 @@ class AlertUpdateCoordinator(DataUpdateCoordinator[dict[str, IntegrationAlert]]) integration=integration["package"], filename=alert["filename"], date_updated=alert.get("date_updated"), - alert_url=alert["alert_url"], ) result[integration_alert.issue_id] = integration_alert diff --git a/tests/components/homeassistant_alerts/fixtures/alerts_no_url.json b/tests/components/homeassistant_alerts/fixtures/alerts_no_url.json deleted file mode 100644 index 89f277cf69b..00000000000 --- a/tests/components/homeassistant_alerts/fixtures/alerts_no_url.json +++ /dev/null @@ -1,34 +0,0 @@ -[ - { - "title": "Dark Sky API closed for new users", - "created": "2020-03-31T14:40:00.000Z", - "integrations": [ - { - "package": "darksky" - } - ], - "github_issue": "https://github.com/home-assistant/home-assistant.io/pull/12591", - "homeassistant": { - "package": "homeassistant", - "affected_from_version": "0.30" - }, - "filename": "dark_sky.markdown", - "alert_url": "https://alerts.home-assistant.io/#dark_sky.markdown" - }, - { - "title": "Hikvision Security Vulnerability", - "created": "2021-09-20T22:08:00.000Z", - "integrations": [ - { - "package": "hikvision" - }, - { - "package": "hikvisioncam" - } - ], - "filename": "hikvision.markdown", - "homeassistant": { - "package": "homeassistant" - } - } -] diff --git a/tests/components/homeassistant_alerts/test_init.py b/tests/components/homeassistant_alerts/test_init.py index c0b6f471033..cb39fb73108 100644 --- a/tests/components/homeassistant_alerts/test_init.py +++ b/tests/components/homeassistant_alerts/test_init.py @@ -133,7 +133,7 @@ async def test_alerts( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", - "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "learn_more_url": None, "severity": "warning", "translation_key": "alert", "translation_placeholders": { @@ -149,13 +149,6 @@ async def test_alerts( @pytest.mark.parametrize( "ha_version, fixture, expected_alerts", ( - ( - "2022.7.0", - "alerts_no_url.json", - [ - ("dark_sky.markdown", "darksky"), - ], - ), ( "2022.7.0", "alerts_no_integrations.json", @@ -220,7 +213,7 @@ async def test_bad_alerts( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", - "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "learn_more_url": None, "severity": "warning", "translation_key": "alert", "translation_placeholders": { @@ -381,7 +374,7 @@ async def test_alerts_change( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", - "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "learn_more_url": None, "severity": "warning", "translation_key": "alert", "translation_placeholders": { @@ -420,7 +413,7 @@ async def test_alerts_change( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", - "learn_more_url": f"https://alerts.home-assistant.io/#{alert}", + "learn_more_url": None, "severity": "warning", "translation_key": "alert", "translation_placeholders": { From 937fd490f2d8326e4175e98ce78d26ef2a4e8243 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 Jul 2022 13:53:51 -0700 Subject: [PATCH 2934/3516] Add issue_domain to repairs (#75839) --- .../components/homeassistant_alerts/__init__.py | 1 + homeassistant/components/repairs/issue_handler.py | 2 ++ homeassistant/components/repairs/issue_registry.py | 6 ++++++ tests/components/demo/test_init.py | 5 +++++ tests/components/homeassistant_alerts/test_init.py | 4 ++++ tests/components/repairs/test_init.py | 13 +++++++++++++ tests/components/repairs/test_websocket_api.py | 7 +++++++ 7 files changed, 38 insertions(+) diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index 12ba4dce8ba..d405b9e257d 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -75,6 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: DOMAIN, issue_id, is_fixable=False, + issue_domain=alert.integration, severity=IssueSeverity.WARNING, translation_key="alert", translation_placeholders={ diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index 8eff4ac64fe..c139026ec48 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -86,6 +86,7 @@ def async_create_issue( domain: str, issue_id: str, *, + issue_domain: str | None = None, breaks_in_ha_version: str | None = None, is_fixable: bool, learn_more_url: str | None = None, @@ -106,6 +107,7 @@ def async_create_issue( issue_registry.async_get_or_create( domain, issue_id, + issue_domain=issue_domain, breaks_in_ha_version=breaks_in_ha_version, is_fixable=is_fixable, learn_more_url=learn_more_url, diff --git a/homeassistant/components/repairs/issue_registry.py b/homeassistant/components/repairs/issue_registry.py index 5c459309cc0..bd201f1007c 100644 --- a/homeassistant/components/repairs/issue_registry.py +++ b/homeassistant/components/repairs/issue_registry.py @@ -30,6 +30,8 @@ class IssueEntry: domain: str is_fixable: bool | None issue_id: str + # Used if an integration creates issues for other integrations (ie alerts) + issue_domain: str | None learn_more_url: str | None severity: IssueSeverity | None translation_key: str | None @@ -58,6 +60,7 @@ class IssueRegistry: domain: str, issue_id: str, *, + issue_domain: str | None = None, breaks_in_ha_version: str | None = None, is_fixable: bool, learn_more_url: str | None = None, @@ -75,6 +78,7 @@ class IssueRegistry: dismissed_version=None, domain=domain, is_fixable=is_fixable, + issue_domain=issue_domain, issue_id=issue_id, learn_more_url=learn_more_url, severity=severity, @@ -93,6 +97,7 @@ class IssueRegistry: active=True, breaks_in_ha_version=breaks_in_ha_version, is_fixable=is_fixable, + issue_domain=issue_domain, learn_more_url=learn_more_url, severity=severity, translation_key=translation_key, @@ -155,6 +160,7 @@ class IssueRegistry: domain=issue["domain"], is_fixable=None, issue_id=issue["issue_id"], + issue_domain=None, learn_more_url=None, severity=None, translation_key=None, diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 85ff2a16405..5b322cb776f 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -95,6 +95,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": False, "issue_id": "transmogrifier_deprecated", + "issue_domain": None, "learn_more_url": "https://en.wiktionary.org/wiki/transmogrifier", "severity": "warning", "translation_key": "transmogrifier_deprecated", @@ -108,6 +109,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": True, "issue_id": "out_of_blinker_fluid", + "issue_domain": None, "learn_more_url": "https://www.youtube.com/watch?v=b9rntRxLlbU", "severity": "critical", "translation_key": "out_of_blinker_fluid", @@ -121,6 +123,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": False, "issue_id": "unfixable_problem", + "issue_domain": None, "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "severity": "warning", "translation_key": "unfixable_problem", @@ -180,6 +183,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": False, "issue_id": "transmogrifier_deprecated", + "issue_domain": None, "learn_more_url": "https://en.wiktionary.org/wiki/transmogrifier", "severity": "warning", "translation_key": "transmogrifier_deprecated", @@ -193,6 +197,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "ignored": False, "is_fixable": False, "issue_id": "unfixable_problem", + "issue_domain": None, "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "severity": "warning", "translation_key": "unfixable_problem", diff --git a/tests/components/homeassistant_alerts/test_init.py b/tests/components/homeassistant_alerts/test_init.py index cb39fb73108..a0fb2e8557d 100644 --- a/tests/components/homeassistant_alerts/test_init.py +++ b/tests/components/homeassistant_alerts/test_init.py @@ -133,6 +133,7 @@ async def test_alerts( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", + "issue_domain": integration, "learn_more_url": None, "severity": "warning", "translation_key": "alert", @@ -213,6 +214,7 @@ async def test_bad_alerts( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", + "issue_domain": integration, "learn_more_url": None, "severity": "warning", "translation_key": "alert", @@ -374,6 +376,7 @@ async def test_alerts_change( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", + "issue_domain": integration, "learn_more_url": None, "severity": "warning", "translation_key": "alert", @@ -413,6 +416,7 @@ async def test_alerts_change( "ignored": False, "is_fixable": False, "issue_id": f"{alert}_{integration}", + "issue_domain": integration, "learn_more_url": None, "severity": "warning", "translation_key": "alert", diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py index 2f82a084968..d70f6c6e11d 100644 --- a/tests/components/repairs/test_init.py +++ b/tests/components/repairs/test_init.py @@ -85,6 +85,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -97,6 +98,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: issues[0]["issue_id"], breaks_in_ha_version=issues[0]["breaks_in_ha_version"], is_fixable=issues[0]["is_fixable"], + issue_domain="my_issue_domain", learn_more_url="blablabla", severity=issues[0]["severity"], translation_key=issues[0]["translation_key"], @@ -113,6 +115,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: dismissed_version=None, ignored=False, learn_more_url="blablabla", + issue_domain="my_issue_domain", ) @@ -206,6 +209,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -226,6 +230,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -245,6 +250,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=ha_version, ignored=True, + issue_domain=None, ) for issue in issues ] @@ -264,6 +270,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: created="2022-07-19T07:53:05+00:00", dismissed_version=ha_version, ignored=True, + issue_domain=None, ) for issue in issues ] @@ -292,6 +299,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: dismissed_version=ha_version, ignored=True, learn_more_url="blablabla", + issue_domain=None, ) # Unignore the same issue @@ -309,6 +317,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: dismissed_version=None, ignored=False, learn_more_url="blablabla", + issue_domain=None, ) for issue in issues ] @@ -359,6 +368,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -378,6 +388,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non created="2022-07-19T07:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -428,6 +439,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non created="2022-07-19T08:53:05+00:00", dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -501,6 +513,7 @@ async def test_sync_methods( "ignored": False, "is_fixable": True, "issue_id": "sync_issue", + "issue_domain": None, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index 73d1898fcb7..d778b043832 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -61,6 +61,7 @@ async def create_issues(hass, ws_client): created=ANY, dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -154,6 +155,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: created=ANY, dismissed_version=ha_version, ignored=True, + issue_domain=None, ) for issue in issues ] @@ -183,6 +185,7 @@ async def test_dismiss_issue(hass: HomeAssistant, hass_ws_client) -> None: created=ANY, dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -226,6 +229,7 @@ async def test_fix_non_existing_issue( created=ANY, dismissed_version=None, ignored=False, + issue_domain=None, ) for issue in issues ] @@ -383,6 +387,7 @@ async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> "dismissed_version": None, "domain": "test", "issue_id": "issue_3_inactive", + "issue_domain": None, }, ] }, @@ -404,6 +409,7 @@ async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> "domain": "test", "is_fixable": True, "issue_id": "issue_1", + "issue_domain": None, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -414,6 +420,7 @@ async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> "domain": "test", "is_fixable": False, "issue_id": "issue_2", + "issue_domain": None, "learn_more_url": "https://theuselessweb.com/abc", "severity": "other", "translation_key": "even_worse", From add9ff57363a3fef6b11cbeaa2aad25c164ae29b Mon Sep 17 00:00:00 2001 From: Rolf Berkenbosch <30292281+rolfberkenbosch@users.noreply.github.com> Date: Wed, 27 Jul 2022 23:50:41 +0200 Subject: [PATCH 2935/3516] Fix fetching MeteoAlarm XML data (#75840) --- homeassistant/components/meteoalarm/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/meteoalarm/manifest.json b/homeassistant/components/meteoalarm/manifest.json index 35333f6ea01..9a3da54d34f 100644 --- a/homeassistant/components/meteoalarm/manifest.json +++ b/homeassistant/components/meteoalarm/manifest.json @@ -2,7 +2,7 @@ "domain": "meteoalarm", "name": "MeteoAlarm", "documentation": "https://www.home-assistant.io/integrations/meteoalarm", - "requirements": ["meteoalertapi==0.2.0"], + "requirements": ["meteoalertapi==0.3.0"], "codeowners": ["@rolfberkenbosch"], "iot_class": "cloud_polling", "loggers": ["meteoalertapi"] diff --git a/requirements_all.txt b/requirements_all.txt index 95da109691c..9c48e9984eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1025,7 +1025,7 @@ meater-python==0.0.8 messagebird==1.2.0 # homeassistant.components.meteoalarm -meteoalertapi==0.2.0 +meteoalertapi==0.3.0 # homeassistant.components.meteo_france meteofrance-api==1.0.2 From e7ff97bac0240ee87989de01428e905137a71f40 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 22:58:58 +0200 Subject: [PATCH 2936/3516] Fix temperature unit in evohome (#75842) --- homeassistant/components/evohome/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 841619af6f1..c1a630d0d05 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -126,6 +126,8 @@ async def async_setup_platform( class EvoClimateEntity(EvoDevice, ClimateEntity): """Base for an evohome Climate device.""" + _attr_temperature_unit = TEMP_CELSIUS + def __init__(self, evo_broker, evo_device) -> None: """Initialize a Climate device.""" super().__init__(evo_broker, evo_device) @@ -316,7 +318,6 @@ class EvoController(EvoClimateEntity): _attr_icon = "mdi:thermostat" _attr_precision = PRECISION_TENTHS - _attr_temperature_unit = TEMP_CELSIUS def __init__(self, evo_broker, evo_device) -> None: """Initialize a Honeywell TCC Controller/Location.""" From b4d2c25f8e9cea621ec2d2f78373fd9d8a610631 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Jul 2022 23:49:22 +0200 Subject: [PATCH 2937/3516] Raise YAML removal issue for Xbox (#75843) --- homeassistant/components/xbox/__init__.py | 12 +++++++++++- homeassistant/components/xbox/manifest.json | 2 +- homeassistant/components/xbox/strings.json | 6 ++++++ homeassistant/components/xbox/translations/en.json | 6 ++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index 6e8492e45ca..c49fd55e8c8 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -21,6 +21,7 @@ from xbox.webapi.api.provider.smartglass.models import ( ) from homeassistant.components import application_credentials +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, Platform from homeassistant.core import HomeAssistant @@ -74,9 +75,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: config[DOMAIN][CONF_CLIENT_ID], config[DOMAIN][CONF_CLIENT_SECRET] ), ) + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) _LOGGER.warning( "Configuration of Xbox integration in YAML is deprecated and " - "will be removed in a future release; Your existing configuration " + "will be removed in Home Assistant 2022.9.; Your existing configuration " "(including OAuth Application Credentials) has been imported into " "the UI automatically and can be safely removed from your " "configuration.yaml file" diff --git a/homeassistant/components/xbox/manifest.json b/homeassistant/components/xbox/manifest.json index 5adfa54a901..8857a55d66d 100644 --- a/homeassistant/components/xbox/manifest.json +++ b/homeassistant/components/xbox/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/xbox", "requirements": ["xbox-webapi==2.0.11"], - "dependencies": ["auth", "application_credentials"], + "dependencies": ["auth", "application_credentials", "repairs"], "codeowners": ["@hunterjm"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/xbox/strings.json b/homeassistant/components/xbox/strings.json index accd6775941..68af0176fa8 100644 --- a/homeassistant/components/xbox/strings.json +++ b/homeassistant/components/xbox/strings.json @@ -13,5 +13,11 @@ "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Xbox YAML configuration is being removed", + "description": "Configuring the Xbox in configuration.yaml is being removed in Home Assistant 2022.9.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/xbox/translations/en.json b/homeassistant/components/xbox/translations/en.json index 0bb1266bded..2ef065af458 100644 --- a/homeassistant/components/xbox/translations/en.json +++ b/homeassistant/components/xbox/translations/en.json @@ -13,5 +13,11 @@ "title": "Pick Authentication Method" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring the Xbox in configuration.yaml is being removed in Home Assistant 2022.9.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Xbox YAML configuration is being removed" + } } } \ No newline at end of file From 2dc318be54f00a610e7007a041bab2cdf09acbb1 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 28 Jul 2022 12:05:56 +0300 Subject: [PATCH 2938/3516] Add issue to repairs for deprecated Simplepush YAML configuration (#75850) --- homeassistant/components/simplepush/manifest.json | 1 + homeassistant/components/simplepush/notify.py | 12 ++++++++++++ homeassistant/components/simplepush/strings.json | 6 ++++++ .../components/simplepush/translations/en.json | 6 ++++++ 4 files changed, 25 insertions(+) diff --git a/homeassistant/components/simplepush/manifest.json b/homeassistant/components/simplepush/manifest.json index 7c37546485a..6b4ee263ba6 100644 --- a/homeassistant/components/simplepush/manifest.json +++ b/homeassistant/components/simplepush/manifest.json @@ -5,6 +5,7 @@ "requirements": ["simplepush==1.1.4"], "codeowners": ["@engrbm87"], "config_flow": true, + "dependencies": ["repairs"], "iot_class": "cloud_polling", "loggers": ["simplepush"] } diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index e9cd9813175..358d95c770a 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -14,6 +14,8 @@ from homeassistant.components.notify import ( BaseNotificationService, ) from homeassistant.components.notify.const import ATTR_DATA +from homeassistant.components.repairs.issue_handler import async_create_issue +from homeassistant.components.repairs.models import IssueSeverity from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_EVENT, CONF_PASSWORD from homeassistant.core import HomeAssistant @@ -41,6 +43,16 @@ async def async_get_service( discovery_info: DiscoveryInfoType | None = None, ) -> SimplePushNotificationService | None: """Get the Simplepush notification service.""" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + if discovery_info is None: hass.async_create_task( hass.config_entries.flow.async_init( diff --git a/homeassistant/components/simplepush/strings.json b/homeassistant/components/simplepush/strings.json index 0031dc32340..77ed05c4b48 100644 --- a/homeassistant/components/simplepush/strings.json +++ b/homeassistant/components/simplepush/strings.json @@ -17,5 +17,11 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Simplepush YAML configuration is being removed", + "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/simplepush/translations/en.json b/homeassistant/components/simplepush/translations/en.json index a36a3b2b273..bf373d8baf0 100644 --- a/homeassistant/components/simplepush/translations/en.json +++ b/homeassistant/components/simplepush/translations/en.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "The Simplepush YAML configuration is being removed", + "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } \ No newline at end of file From c10ed6edbaae83e1dffec0796ae53794dcea3ee8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 12:39:10 +0200 Subject: [PATCH 2939/3516] Fix unit of measurement usage in COSignal (#75856) --- homeassistant/components/co2signal/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index f7514664698..841848621ec 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -103,8 +103,8 @@ class CO2Sensor(CoordinatorEntity[CO2SignalCoordinator], SensorEntity): @property def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" - if self.entity_description.unit_of_measurement: - return self.entity_description.unit_of_measurement + if self.entity_description.native_unit_of_measurement: + return self.entity_description.native_unit_of_measurement return cast( str, self.coordinator.data["units"].get(self.entity_description.key) ) From a000687eb58abf69004d17aa24777b37db3a8aa9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 16:43:32 +0200 Subject: [PATCH 2940/3516] Fix HTTP 404 being logged as a stack trace (#75861) --- homeassistant/components/http/static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index c4dc97727a9..6cb1bafdaca 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -33,7 +33,7 @@ def _get_file_path( return None if filepath.is_file(): return filepath - raise HTTPNotFound + raise FileNotFoundError class CachingStaticResource(StaticResource): From 4be623a4922681f8cbc491cef81115f0dbda4ca3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 18:13:16 +0200 Subject: [PATCH 2941/3516] Remove state class from daily net sensors in DSMR Reader (#75864) --- homeassistant/components/dsmr_reader/definitions.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index 9f4ee7ed918..ac61837afec 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -225,42 +225,36 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( name="Low tariff usage", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity2", name="High tariff usage", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity1_returned", name="Low tariff return", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity2_returned", name="High tariff return", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity_merged", name="Power usage total", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity_returned_merged", name="Power return total", device_class=SensorDeviceClass.ENERGY, native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - state_class=SensorStateClass.TOTAL_INCREASING, ), DSMRReaderSensorEntityDescription( key="dsmr/day-consumption/electricity1_cost", From 2bf10799ed68c96a86d16812922da53e8271451a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 18:15:27 +0200 Subject: [PATCH 2942/3516] Fix incorrect sensor key in DSMR (#75865) --- homeassistant/components/dsmr/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 7ab1a3bb45b..aa01c798072 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -85,7 +85,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, ), DSMRSensorEntityDescription( - key="electricity_delivery", + key="current_electricity_delivery", name="Power production", obis_reference=obis_references.CURRENT_ELECTRICITY_DELIVERY, device_class=SensorDeviceClass.POWER, From f98d95c76f30eb0bbcdeab72d0a931b8971ec9b8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 18:20:39 +0200 Subject: [PATCH 2943/3516] Fix camera token to trigger authentication IP ban (#75870) --- homeassistant/components/camera/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 247f73c89f2..3bf86dedea1 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -715,7 +715,9 @@ class CameraView(HomeAssistantView): ) if not authenticated: - raise web.HTTPUnauthorized() + if request[KEY_AUTHENTICATED]: + raise web.HTTPUnauthorized() + raise web.HTTPForbidden() if not camera.is_on: _LOGGER.debug("Camera is off") From 38909855bfe811275245b28b4a1e31877c58bea3 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Thu, 28 Jul 2022 11:20:10 -0500 Subject: [PATCH 2944/3516] Update frontend to 20220728.0 (#75872) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 2a1fb2d3b37..45331491aa0 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220727.0"], + "requirements": ["home-assistant-frontend==20220728.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2554f0185b4..1f86ff4c7c5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.1 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220727.0 +home-assistant-frontend==20220728.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 9c48e9984eb..24a8a5ee1bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -839,7 +839,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220727.0 +home-assistant-frontend==20220728.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f650a56a30..81e192ce360 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,7 +616,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220727.0 +home-assistant-frontend==20220728.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 53870dd0bce72dd2e65736028f57aac3d05ab51c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 19:09:46 +0200 Subject: [PATCH 2945/3516] Bumped version to 2022.8.0b1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 02896fcbe96..dfa12ccd824 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 5306a589863..f5b35dd63dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0b0" +version = "2022.8.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 10356b93795abd8c21539d1b25ebe5ce2e457c09 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 28 Jul 2022 19:10:37 +0100 Subject: [PATCH 2946/3516] Fix Xiaomi BLE not detecting encryption for some devices (#75851) --- .../components/bluetooth/__init__.py | 30 +++++ .../components/xiaomi_ble/config_flow.py | 44 ++++++- .../components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/test_init.py | 90 ++++++++++++- tests/components/xiaomi_ble/__init__.py | 12 ++ .../components/xiaomi_ble/test_config_flow.py | 123 ++++++++++++++++++ 8 files changed, 299 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 551e93d5bd9..1853ffa0203 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -1,6 +1,7 @@ """The bluetooth integration.""" from __future__ import annotations +from asyncio import Future from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta @@ -8,6 +9,7 @@ from enum import Enum import logging from typing import Final, Union +import async_timeout from bleak import BleakError from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData @@ -95,6 +97,9 @@ BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") BluetoothCallback = Callable[ [Union[BluetoothServiceInfoBleak, BluetoothServiceInfo], BluetoothChange], None ] +ProcessAdvertisementCallback = Callable[ + [Union[BluetoothServiceInfoBleak, BluetoothServiceInfo]], bool +] @hass_callback @@ -159,6 +164,31 @@ def async_register_callback( return manager.async_register_callback(callback, match_dict) +async def async_process_advertisements( + hass: HomeAssistant, + callback: ProcessAdvertisementCallback, + match_dict: BluetoothCallbackMatcher, + timeout: int, +) -> BluetoothServiceInfo: + """Process advertisements until callback returns true or timeout expires.""" + done: Future[BluetoothServiceInfo] = Future() + + @hass_callback + def _async_discovered_device( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + if callback(service_info): + done.set_result(service_info) + + unload = async_register_callback(hass, _async_discovered_device, match_dict) + + try: + async with async_timeout.timeout(timeout): + return await done + finally: + unload() + + @hass_callback def async_track_unavailable( hass: HomeAssistant, diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index e7c4a3e1f8c..f352f43d0bf 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Xiaomi Bluetooth integration.""" from __future__ import annotations +import asyncio import dataclasses from typing import Any @@ -12,6 +13,7 @@ from homeassistant.components import onboarding from homeassistant.components.bluetooth import ( BluetoothServiceInfo, async_discovered_service_info, + async_process_advertisements, ) from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_ADDRESS @@ -19,6 +21,9 @@ from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN +# How long to wait for additional advertisement packets if we don't have the right ones +ADDITIONAL_DISCOVERY_TIMEOUT = 5 + @dataclasses.dataclass class Discovery: @@ -44,6 +49,24 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, Discovery] = {} + async def _async_wait_for_full_advertisement( + self, discovery_info: BluetoothServiceInfo, device: DeviceData + ) -> BluetoothServiceInfo: + """Sometimes first advertisement we receive is blank or incomplete. Wait until we get a useful one.""" + if not device.pending: + return discovery_info + + def _process_more_advertisements(service_info: BluetoothServiceInfo) -> bool: + device.update(service_info) + return not device.pending + + return await async_process_advertisements( + self.hass, + _process_more_advertisements, + {"address": discovery_info.address}, + ADDITIONAL_DISCOVERY_TIMEOUT, + ) + async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfo ) -> FlowResult: @@ -53,6 +76,16 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): device = DeviceData() if not device.supported(discovery_info): return self.async_abort(reason="not_supported") + + # Wait until we have received enough information about this device to detect its encryption type + try: + discovery_info = await self._async_wait_for_full_advertisement( + discovery_info, device + ) + except asyncio.TimeoutError: + # If we don't see a valid packet within the timeout then this device is not supported. + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info self._discovered_device = device @@ -161,13 +194,20 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(address, raise_on_progress=False) discovery = self._discovered_devices[address] + # Wait until we have received enough information about this device to detect its encryption type + try: + self._discovery_info = await self._async_wait_for_full_advertisement( + discovery.discovery_info, discovery.device + ) + except asyncio.TimeoutError: + # If we don't see a valid packet within the timeout then this device is not supported. + return self.async_abort(reason="not_supported") + if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: - self._discovery_info = discovery.discovery_info self.context["title_placeholders"] = {"name": discovery.title} return await self.async_step_get_encryption_key_legacy() if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_4_5: - self._discovery_info = discovery.discovery_info self.context["title_placeholders"] = {"name": discovery.title} return await self.async_step_get_encryption_key_4_5() diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 2e1a502ca69..41512291749 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.6.1"], + "requirements": ["xiaomi-ble==0.6.2"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 593bb4e040c..5094f5a3241 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2473,7 +2473,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.1 +xiaomi-ble==0.6.2 # homeassistant.components.knx xknx==0.22.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cf4c54297a2..e8b8c5be6b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1665,7 +1665,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.1 +xiaomi-ble==0.6.2 # homeassistant.components.knx xknx==0.22.0 diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index bb2c5f49cc9..f3f3eda1f27 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration.""" +import asyncio from datetime import timedelta from unittest.mock import MagicMock, patch @@ -12,6 +13,7 @@ from homeassistant.components.bluetooth import ( UNAVAILABLE_TRACK_SECONDS, BluetoothChange, BluetoothServiceInfo, + async_process_advertisements, async_track_unavailable, models, ) @@ -21,7 +23,7 @@ from homeassistant.components.bluetooth.const import ( ) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -791,6 +793,92 @@ async def test_register_callback_by_address( assert service_info.manufacturer_id == 89 +async def test_process_advertisements_bail_on_good_advertisement( + hass: HomeAssistant, mock_bleak_scanner_start, enable_bluetooth +): + """Test as soon as we see a 'good' advertisement we return it.""" + done = asyncio.Future() + + def _callback(service_info: BluetoothServiceInfo) -> bool: + done.set_result(None) + return len(service_info.service_data) > 0 + + handle = hass.async_create_task( + async_process_advertisements( + hass, _callback, {"address": "aa:44:33:11:23:45"}, 5 + ) + ) + + while not done.done(): + device = BLEDevice("aa:44:33:11:23:45", "wohand") + adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51a"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fa": b"H\x10c"}, + ) + + _get_underlying_scanner()._callback(device, adv) + await asyncio.sleep(0) + + result = await handle + assert result.name == "wohand" + + +async def test_process_advertisements_ignore_bad_advertisement( + hass: HomeAssistant, mock_bleak_scanner_start, enable_bluetooth +): + """Check that we ignore bad advertisements.""" + done = asyncio.Event() + return_value = asyncio.Event() + + device = BLEDevice("aa:44:33:11:23:45", "wohand") + adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51a"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fa": b""}, + ) + + def _callback(service_info: BluetoothServiceInfo) -> bool: + done.set() + return return_value.is_set() + + handle = hass.async_create_task( + async_process_advertisements( + hass, _callback, {"address": "aa:44:33:11:23:45"}, 5 + ) + ) + + # The goal of this loop is to make sure that async_process_advertisements sees at least one + # callback that returns False + while not done.is_set(): + _get_underlying_scanner()._callback(device, adv) + await asyncio.sleep(0) + + # Set the return value and mutate the advertisement + # Check that scan ends and correct advertisement data is returned + return_value.set() + adv.service_data["00000d00-0000-1000-8000-00805f9b34fa"] = b"H\x10c" + _get_underlying_scanner()._callback(device, adv) + await asyncio.sleep(0) + + result = await handle + assert result.service_data["00000d00-0000-1000-8000-00805f9b34fa"] == b"H\x10c" + + +async def test_process_advertisements_timeout( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test we timeout if no advertisements at all.""" + + def _callback(service_info: BluetoothServiceInfo) -> bool: + return False + + with pytest.raises(asyncio.TimeoutError): + await async_process_advertisements(hass, _callback, {}, 0) + + async def test_wrapped_instance_with_filter( hass, mock_bleak_scanner_start, enable_bluetooth ): diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py index a6269a02d12..1dd1eeed65a 100644 --- a/tests/components/xiaomi_ble/__init__.py +++ b/tests/components/xiaomi_ble/__init__.py @@ -61,6 +61,18 @@ YLKG07YL_SERVICE_INFO = BluetoothServiceInfo( source="local", ) +MISSING_PAYLOAD_ENCRYPTED = BluetoothServiceInfo( + name="LYWSD02MMC", + address="A4:C1:38:56:53:84", + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": b"0X[\x05\x02\x84\x53\x568\xc1\xa4\x08", + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", +) + def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfo: """Make a dummy advertisement.""" diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index d4b4300d2c1..b424228cc6c 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Xiaomi config flow.""" +import asyncio from unittest.mock import patch from homeassistant import config_entries @@ -9,9 +10,11 @@ from homeassistant.data_entry_flow import FlowResultType from . import ( JTYJGD03MI_SERVICE_INFO, LYWSDCGQ_SERVICE_INFO, + MISSING_PAYLOAD_ENCRYPTED, MMC_T201_1_SERVICE_INFO, NOT_SENSOR_PUSH_SERVICE_INFO, YLKG07YL_SERVICE_INFO, + make_advertisement, ) from tests.common import MockConfigEntry @@ -38,6 +41,57 @@ async def test_async_step_bluetooth_valid_device(hass): assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" +async def test_async_step_bluetooth_valid_device_but_missing_payload(hass): + """Test discovery via bluetooth with a valid device but missing payload.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + side_effect=asyncio.TimeoutError(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MISSING_PAYLOAD_ENCRYPTED, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_bluetooth_valid_device_but_missing_payload_then_full(hass): + """Test discovering a valid device. Payload is too short, but later we get full one.""" + + async def _async_process_advertisements(_hass, _callback, _matcher, _timeout): + service_info = make_advertisement( + "A4:C1:38:56:53:84", + b"XX\xe4\x16,\x84SV8\xc1\xa4+n\xf2\xe9\x12\x00\x00l\x88M\x9e", + ) + assert _callback(service_info) + return service_info + + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + _async_process_advertisements, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MISSING_PAYLOAD_ENCRYPTED, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_4_5" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "a115210eed7a88e50ad52662e732a9fb"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["data"] == {"bindkey": "a115210eed7a88e50ad52662e732a9fb"} + assert result2["result"].unique_id == "A4:C1:38:56:53:84" + + async def test_async_step_bluetooth_during_onboarding(hass): """Test discovery via bluetooth during onboarding.""" with patch( @@ -287,6 +341,75 @@ async def test_async_step_user_with_found_devices(hass): assert result2["result"].unique_id == "58:2D:34:35:93:21" +async def test_async_step_user_short_payload(hass): + """Test setup from service info cache with devices found but short payloads.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[MISSING_PAYLOAD_ENCRYPTED], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + side_effect=asyncio.TimeoutError(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "A4:C1:38:56:53:84"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "not_supported" + + +async def test_async_step_user_short_payload_then_full(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[MISSING_PAYLOAD_ENCRYPTED], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + async def _async_process_advertisements(_hass, _callback, _matcher, _timeout): + service_info = make_advertisement( + "A4:C1:38:56:53:84", + b"XX\xe4\x16,\x84SV8\xc1\xa4+n\xf2\xe9\x12\x00\x00l\x88M\x9e", + ) + assert _callback(service_info) + return service_info + + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + _async_process_advertisements, + ): + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "A4:C1:38:56:53:84"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_4_5" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "a115210eed7a88e50ad52662e732a9fb"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "LYWSD02MMC" + assert result2["data"] == {"bindkey": "a115210eed7a88e50ad52662e732a9fb"} + + async def test_async_step_user_with_found_devices_v4_encryption(hass): """Test setup from service info cache with devices found, with v4 encryption.""" with patch( From 6ba0b9cff21e225fe64b0336a2d32f3a2785b512 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 22:36:11 +0200 Subject: [PATCH 2947/3516] Fix AdGuard Home rules count sensor (#75879) --- homeassistant/components/adguard/sensor.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 07a483f03c4..86104d15ef2 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -26,7 +26,7 @@ PARALLEL_UPDATES = 4 class AdGuardHomeEntityDescriptionMixin: """Mixin for required keys.""" - value_fn: Callable[[AdGuardHome], Callable[[], Coroutine[Any, Any, int | float]]] + value_fn: Callable[[AdGuardHome], Coroutine[Any, Any, int | float]] @dataclass @@ -42,56 +42,56 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( name="DNS queries", icon="mdi:magnify", native_unit_of_measurement="queries", - value_fn=lambda adguard: adguard.stats.dns_queries, + value_fn=lambda adguard: adguard.stats.dns_queries(), ), AdGuardHomeEntityDescription( key="blocked_filtering", name="DNS queries blocked", icon="mdi:magnify-close", native_unit_of_measurement="queries", - value_fn=lambda adguard: adguard.stats.blocked_filtering, + value_fn=lambda adguard: adguard.stats.blocked_filtering(), ), AdGuardHomeEntityDescription( key="blocked_percentage", name="DNS queries blocked ratio", icon="mdi:magnify-close", native_unit_of_measurement=PERCENTAGE, - value_fn=lambda adguard: adguard.stats.blocked_percentage, + value_fn=lambda adguard: adguard.stats.blocked_percentage(), ), AdGuardHomeEntityDescription( key="blocked_parental", name="Parental control blocked", icon="mdi:human-male-girl", native_unit_of_measurement="requests", - value_fn=lambda adguard: adguard.stats.replaced_parental, + value_fn=lambda adguard: adguard.stats.replaced_parental(), ), AdGuardHomeEntityDescription( key="blocked_safebrowsing", name="Safe browsing blocked", icon="mdi:shield-half-full", native_unit_of_measurement="requests", - value_fn=lambda adguard: adguard.stats.replaced_safebrowsing, + value_fn=lambda adguard: adguard.stats.replaced_safebrowsing(), ), AdGuardHomeEntityDescription( key="enforced_safesearch", name="Safe searches enforced", icon="mdi:shield-search", native_unit_of_measurement="requests", - value_fn=lambda adguard: adguard.stats.replaced_safesearch, + value_fn=lambda adguard: adguard.stats.replaced_safesearch(), ), AdGuardHomeEntityDescription( key="average_speed", name="Average processing speed", icon="mdi:speedometer", native_unit_of_measurement=TIME_MILLISECONDS, - value_fn=lambda adguard: adguard.stats.avg_processing_time, + value_fn=lambda adguard: adguard.stats.avg_processing_time(), ), AdGuardHomeEntityDescription( key="rules_count", name="Rules count", icon="mdi:counter", native_unit_of_measurement="rules", - value_fn=lambda adguard: adguard.stats.avg_processing_time, + value_fn=lambda adguard: adguard.filtering.rules_count(allowlist=False), entity_registry_enabled_default=False, ), ) @@ -144,7 +144,7 @@ class AdGuardHomeSensor(AdGuardHomeEntity, SensorEntity): async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" - value = await self.entity_description.value_fn(self.adguard)() + value = await self.entity_description.value_fn(self.adguard) self._attr_native_value = value if isinstance(value, float): self._attr_native_value = f"{value:.2f}" From 702cef3fc7bc8290f6d9a72b1016251bea22df19 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jul 2022 11:14:13 -1000 Subject: [PATCH 2948/3516] Add startup timeout to bluetooth (#75848) Co-authored-by: Martin Hjelmare --- .../components/bluetooth/__init__.py | 10 ++++++- tests/components/bluetooth/test_init.py | 27 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 1853ffa0203..eb8e31baef0 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -1,6 +1,7 @@ """The bluetooth integration.""" from __future__ import annotations +import asyncio from asyncio import Future from collections.abc import Callable from dataclasses import dataclass @@ -45,6 +46,7 @@ _LOGGER = logging.getLogger(__name__) UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 +START_TIMEOUT = 15 SOURCE_LOCAL: Final = "local" @@ -330,7 +332,13 @@ class BluetoothManager: self._device_detected, {} ) try: - await self.scanner.start() + async with async_timeout.timeout(START_TIMEOUT): + await self.scanner.start() + except asyncio.TimeoutError as ex: + self._cancel_device_detected() + raise ConfigEntryNotReady( + f"Timed out starting Bluetooth after {START_TIMEOUT} seconds" + ) from ex except (FileNotFoundError, BleakError) as ex: self._cancel_device_detected() raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index f3f3eda1f27..0664f82dbab 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -97,6 +97,33 @@ async def test_setup_and_stop_broken_bluetooth(hass, caplog): assert len(bluetooth.async_discovered_service_info(hass)) == 0 +async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): + """Test we fail gracefully when bluetooth/dbus is hanging.""" + mock_bt = [] + + async def _mock_hang(): + await asyncio.sleep(1) + + with patch.object(bluetooth, "START_TIMEOUT", 0), patch( + "homeassistant.components.bluetooth.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=_mock_hang, + ), patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "Timed out starting Bluetooth" in caplog.text + + async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): """Test we retry if the adapter is not yet available.""" mock_bt = [] From 003fe9220e1574e3205a9e3cc274069a66753f6b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 29 Jul 2022 00:27:47 +0200 Subject: [PATCH 2949/3516] Add protocol types for device_tracker `async_see` and `see` (#75891) --- .../components/aprs/device_tracker.py | 13 +++-- .../bluetooth_le_tracker/device_tracker.py | 4 +- .../bluetooth_tracker/device_tracker.py | 7 +-- .../components/demo/device_tracker.py | 6 +-- .../components/device_tracker/__init__.py | 2 + .../components/device_tracker/legacy.py | 50 +++++++++++++++++-- .../components/fleetgo/device_tracker.py | 6 +-- .../components/google_maps/device_tracker.py | 6 +-- .../components/icloud/device_tracker.py | 5 +- .../components/meraki/device_tracker.py | 6 +-- .../components/mqtt_json/device_tracker.py | 4 +- .../components/mysensors/device_tracker.py | 11 ++-- .../components/ping/device_tracker.py | 4 +- .../components/tile/device_tracker.py | 4 +- .../components/traccar/device_tracker.py | 6 +-- .../components/volvooncall/device_tracker.py | 8 ++- pylint/plugins/hass_enforce_type_hints.py | 4 +- tests/pylint/test_enforce_type_hints.py | 4 +- 18 files changed, 101 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/aprs/device_tracker.py b/homeassistant/components/aprs/device_tracker.py index 6bae9ce6ebe..b1467a6d2e4 100644 --- a/homeassistant/components/aprs/device_tracker.py +++ b/homeassistant/components/aprs/device_tracker.py @@ -1,7 +1,6 @@ """Support for APRS device tracking.""" from __future__ import annotations -from collections.abc import Callable import logging import threading @@ -12,6 +11,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SeeCallback, ) from homeassistant.const import ( ATTR_GPS_ACCURACY, @@ -87,7 +87,7 @@ def gps_accuracy(gps, posambiguity: int) -> int: def setup_scanner( hass: HomeAssistant, config: ConfigType, - see: Callable[..., None], + see: SeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the APRS tracker.""" @@ -123,8 +123,13 @@ class AprsListenerThread(threading.Thread): """APRS message listener.""" def __init__( - self, callsign: str, password: str, host: str, server_filter: str, see - ): + self, + callsign: str, + password: str, + host: str, + server_filter: str, + see: SeeCallback, + ) -> None: """Initialize the class.""" super().__init__() diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index f85cc2bad0a..a650e65b8f2 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable from datetime import datetime, timedelta import logging from uuid import UUID @@ -22,6 +21,7 @@ from homeassistant.components.device_tracker.const import ( ) from homeassistant.components.device_tracker.legacy import ( YAML_DEVICES, + AsyncSeeCallback, async_load_config, ) from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_STOP @@ -57,7 +57,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( async def async_setup_scanner( # noqa: C901 hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the Bluetooth LE Scanner.""" diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index b62333c0489..90ae473a0cd 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable from datetime import datetime, timedelta import logging from typing import Final @@ -23,6 +23,7 @@ from homeassistant.components.device_tracker.const import ( ) from homeassistant.components.device_tracker.legacy import ( YAML_DEVICES, + AsyncSeeCallback, Device, async_load_config, ) @@ -78,7 +79,7 @@ def discover_devices(device_id: int) -> list[tuple[str, str]]: async def see_device( hass: HomeAssistant, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, mac: str, device_name: str, rssi: tuple[int] | None = None, @@ -130,7 +131,7 @@ def lookup_name(mac: str) -> str | None: async def async_setup_scanner( hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the Bluetooth Scanner.""" diff --git a/homeassistant/components/demo/device_tracker.py b/homeassistant/components/demo/device_tracker.py index 74122932337..dacbd95219b 100644 --- a/homeassistant/components/demo/device_tracker.py +++ b/homeassistant/components/demo/device_tracker.py @@ -1,9 +1,9 @@ """Demo platform for the Device tracker component.""" from __future__ import annotations -from collections.abc import Callable import random +from homeassistant.components.device_tracker import SeeCallback from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -13,7 +13,7 @@ from .const import DOMAIN, SERVICE_RANDOMIZE_DEVICE_TRACKER_DATA def setup_scanner( hass: HomeAssistant, config: ConfigType, - see: Callable[..., None], + see: SeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the demo tracker.""" @@ -42,7 +42,7 @@ def setup_scanner( see( dev_id="demo_home_boy", host_name="Home Boy", - gps=[hass.config.latitude - 0.00002, hass.config.longitude + 0.00002], + gps=(hass.config.latitude - 0.00002, hass.config.longitude + 0.00002), gps_accuracy=20, battery=53, ) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index e9222156b00..5617d56ac3f 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -33,7 +33,9 @@ from .legacy import ( # noqa: F401 SERVICE_SEE, SERVICE_SEE_PAYLOAD_SCHEMA, SOURCE_TYPES, + AsyncSeeCallback, DeviceScanner, + SeeCallback, async_setup_integration as async_setup_legacy_integration, see, ) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 87026fc32ff..d8097a68ad5 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -6,7 +6,7 @@ from collections.abc import Callable, Coroutine, Sequence from datetime import datetime, timedelta import hashlib from types import ModuleType -from typing import Any, Final, final +from typing import Any, Final, Protocol, final import attr import voluptuous as vol @@ -124,6 +124,48 @@ YAML_DEVICES: Final = "known_devices.yaml" EVENT_NEW_DEVICE: Final = "device_tracker_new_device" +class SeeCallback(Protocol): + """Protocol type for DeviceTracker.see callback.""" + + def __call__( + self, + mac: str | None = None, + dev_id: str | None = None, + host_name: str | None = None, + location_name: str | None = None, + gps: GPSType | None = None, + gps_accuracy: int | None = None, + battery: int | None = None, + attributes: dict[str, Any] | None = None, + source_type: str = SOURCE_TYPE_GPS, + picture: str | None = None, + icon: str | None = None, + consider_home: timedelta | None = None, + ) -> None: + """Define see type.""" + + +class AsyncSeeCallback(Protocol): + """Protocol type for DeviceTracker.async_see callback.""" + + async def __call__( + self, + mac: str | None = None, + dev_id: str | None = None, + host_name: str | None = None, + location_name: str | None = None, + gps: GPSType | None = None, + gps_accuracy: int | None = None, + battery: int | None = None, + attributes: dict[str, Any] | None = None, + source_type: str = SOURCE_TYPE_GPS, + picture: str | None = None, + icon: str | None = None, + consider_home: timedelta | None = None, + ) -> None: + """Define async_see type.""" + + def see( hass: HomeAssistant, mac: str | None = None, @@ -133,7 +175,7 @@ def see( gps: GPSType | None = None, gps_accuracy: int | None = None, battery: int | None = None, - attributes: dict | None = None, + attributes: dict[str, Any] | None = None, ) -> None: """Call service to notify you see device.""" data: dict[str, Any] = { @@ -447,7 +489,7 @@ class DeviceTracker: gps: GPSType | None = None, gps_accuracy: int | None = None, battery: int | None = None, - attributes: dict | None = None, + attributes: dict[str, Any] | None = None, source_type: str = SOURCE_TYPE_GPS, picture: str | None = None, icon: str | None = None, @@ -480,7 +522,7 @@ class DeviceTracker: gps: GPSType | None = None, gps_accuracy: int | None = None, battery: int | None = None, - attributes: dict | None = None, + attributes: dict[str, Any] | None = None, source_type: str = SOURCE_TYPE_GPS, picture: str | None = None, icon: str | None = None, diff --git a/homeassistant/components/fleetgo/device_tracker.py b/homeassistant/components/fleetgo/device_tracker.py index bfafb5561d7..e80acae8627 100644 --- a/homeassistant/components/fleetgo/device_tracker.py +++ b/homeassistant/components/fleetgo/device_tracker.py @@ -1,7 +1,6 @@ """Support for FleetGO Platform.""" from __future__ import annotations -from collections.abc import Callable import logging import requests @@ -10,6 +9,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SeeCallback, ) from homeassistant.const import ( CONF_CLIENT_ID, @@ -39,7 +39,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( def setup_scanner( hass: HomeAssistant, config: ConfigType, - see: Callable[..., None], + see: SeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the DeviceScanner and check if login is valid.""" @@ -53,7 +53,7 @@ def setup_scanner( class FleetGoDeviceScanner: """Define a scanner for the FleetGO platform.""" - def __init__(self, config, see): + def __init__(self, config, see: SeeCallback): """Initialize FleetGoDeviceScanner.""" self._include = config.get(CONF_INCLUDE) self._see = see diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index 62627c2e905..8d8be8c0fe1 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -1,7 +1,6 @@ """Support for Google Maps location sharing.""" from __future__ import annotations -from collections.abc import Callable from datetime import timedelta import logging @@ -12,6 +11,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PLATFORM_SCHEMA_BASE, SOURCE_TYPE_GPS, + SeeCallback, ) from homeassistant.const import ( ATTR_BATTERY_CHARGING, @@ -50,7 +50,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA_BASE.extend( def setup_scanner( hass: HomeAssistant, config: ConfigType, - see: Callable[..., None], + see: SeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the Google Maps Location sharing scanner.""" @@ -61,7 +61,7 @@ def setup_scanner( class GoogleMapsScanner: """Representation of an Google Maps location sharing account.""" - def __init__(self, hass, config: ConfigType, see) -> None: + def __init__(self, hass, config: ConfigType, see: SeeCallback) -> None: """Initialize the scanner.""" self.see = see self.username = config[CONF_USERNAME] diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 9c2004f0edb..ec543a9ed30 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -1,10 +1,9 @@ """Support for tracking for iCloud devices.""" from __future__ import annotations -from collections.abc import Awaitable, Callable from typing import Any -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS, AsyncSeeCallback from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -25,7 +24,7 @@ from .const import ( async def async_setup_scanner( hass: HomeAssistant, config: ConfigType, - see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Old way of setting up the iCloud tracker.""" diff --git a/homeassistant/components/meraki/device_tracker.py b/homeassistant/components/meraki/device_tracker.py index 86c9ecf95fb..7447d8ce879 100644 --- a/homeassistant/components/meraki/device_tracker.py +++ b/homeassistant/components/meraki/device_tracker.py @@ -1,7 +1,6 @@ """Support for the Meraki CMX location service.""" from __future__ import annotations -from collections.abc import Awaitable, Callable from http import HTTPStatus import json import logging @@ -11,6 +10,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SOURCE_TYPE_ROUTER, + AsyncSeeCallback, ) from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback @@ -34,7 +34,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( async def async_setup_scanner( hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up an endpoint for the Meraki tracker.""" @@ -50,7 +50,7 @@ class MerakiView(HomeAssistantView): name = "api:meraki" requires_auth = False - def __init__(self, config, async_see): + def __init__(self, config: ConfigType, async_see: AsyncSeeCallback) -> None: """Initialize Meraki URL endpoints.""" self.async_see = async_see self.validator = config[CONF_VALIDATOR] diff --git a/homeassistant/components/mqtt_json/device_tracker.py b/homeassistant/components/mqtt_json/device_tracker.py index 1d99e6d7b6f..f0330e39e86 100644 --- a/homeassistant/components/mqtt_json/device_tracker.py +++ b/homeassistant/components/mqtt_json/device_tracker.py @@ -1,7 +1,6 @@ """Support for GPS tracking MQTT enabled devices.""" from __future__ import annotations -from collections.abc import Awaitable, Callable import json import logging @@ -10,6 +9,7 @@ import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + AsyncSeeCallback, ) from homeassistant.components.mqtt import CONF_QOS from homeassistant.const import ( @@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(mqtt.config.SCHEMA_BASE).extend( async def async_setup_scanner( hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the MQTT JSON tracker.""" diff --git a/homeassistant/components/mysensors/device_tracker.py b/homeassistant/components/mysensors/device_tracker.py index d5332ab70bf..3c204776b7d 100644 --- a/homeassistant/components/mysensors/device_tracker.py +++ b/homeassistant/components/mysensors/device_tracker.py @@ -1,10 +1,10 @@ """Support for tracking MySensors devices.""" from __future__ import annotations -from collections.abc import Awaitable, Callable from typing import Any, cast from homeassistant.components import mysensors +from homeassistant.components.device_tracker import AsyncSeeCallback from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -18,7 +18,7 @@ from .helpers import on_unload async def async_setup_scanner( hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the MySensors device scanner.""" @@ -63,7 +63,12 @@ async def async_setup_scanner( class MySensorsDeviceScanner(mysensors.device.MySensorsDevice): """Represent a MySensors scanner.""" - def __init__(self, hass: HomeAssistant, async_see: Callable, *args: Any) -> None: + def __init__( + self, + hass: HomeAssistant, + async_see: AsyncSeeCallback, + *args: Any, + ) -> None: """Set up instance.""" super().__init__(*args) self.async_see = async_see diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index 0a0e397e6d8..cbce224a373 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable from datetime import timedelta import logging import subprocess @@ -13,6 +12,7 @@ import voluptuous as vol from homeassistant import const, util from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, + AsyncSeeCallback, ) from homeassistant.components.device_tracker.const import ( CONF_SCAN_INTERVAL, @@ -83,7 +83,7 @@ class HostSubProcess: async def async_setup_scanner( hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the Host objects and return the update function.""" diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 61e9a1bdcd9..492c202df08 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -1,11 +1,11 @@ """Support for Tile device trackers.""" from __future__ import annotations -from collections.abc import Awaitable, Callable import logging from pytile.tile import Tile +from homeassistant.components.device_tracker import AsyncSeeCallback from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -52,7 +52,7 @@ async def async_setup_entry( async def async_setup_scanner( hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Detect a legacy configuration and import it.""" diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index 970cd20d640..f9676d37aa2 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable from datetime import datetime, timedelta import logging @@ -21,6 +20,7 @@ from homeassistant.components.device_tracker import ( CONF_SCAN_INTERVAL, PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, SOURCE_TYPE_GPS, + AsyncSeeCallback, ) from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry @@ -174,7 +174,7 @@ async def async_setup_entry( async def async_setup_scanner( hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Validate the configuration and return a Traccar scanner.""" @@ -208,7 +208,7 @@ class TraccarScanner: self, api: ApiClient, hass: HomeAssistant, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, scan_interval: timedelta, max_accuracy: int, skip_accuracy_on: bool, diff --git a/homeassistant/components/volvooncall/device_tracker.py b/homeassistant/components/volvooncall/device_tracker.py index 74a88fb69ab..866634fc5e1 100644 --- a/homeassistant/components/volvooncall/device_tracker.py +++ b/homeassistant/components/volvooncall/device_tracker.py @@ -1,9 +1,7 @@ """Support for tracking a Volvo.""" from __future__ import annotations -from collections.abc import Awaitable, Callable - -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS, AsyncSeeCallback from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -15,7 +13,7 @@ from . import DATA_KEY, SIGNAL_STATE_UPDATED async def async_setup_scanner( hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: """Set up the Volvo tracker.""" @@ -26,7 +24,7 @@ async def async_setup_scanner( data = hass.data[DATA_KEY] instrument = data.instrument(vin, component, attr, slug_attr) - async def see_vehicle(): + async def see_vehicle() -> None: """Handle the reporting of the vehicle position.""" host_name = instrument.vehicle_name dev_id = f"volvo_{slugify(host_name)}" diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 680d25414aa..3b50c072eb6 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -293,7 +293,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { arg_types={ 0: "HomeAssistant", 1: "ConfigType", - 2: "Callable[..., None]", + 2: "SeeCallback", 3: "DiscoveryInfoType | None", }, return_type="bool", @@ -303,7 +303,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { arg_types={ 0: "HomeAssistant", 1: "ConfigType", - 2: "Callable[..., Awaitable[None]]", + 2: "AsyncSeeCallback", 3: "DiscoveryInfoType | None", }, return_type="bool", diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index e549d21fe0f..54824e5c0b0 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -201,7 +201,7 @@ def test_invalid_discovery_info( async def async_setup_scanner( #@ hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: dict[str, Any] | None = None, #@ ) -> bool: pass @@ -234,7 +234,7 @@ def test_valid_discovery_info( async def async_setup_scanner( #@ hass: HomeAssistant, config: ConfigType, - async_see: Callable[..., Awaitable[None]], + async_see: AsyncSeeCallback, discovery_info: DiscoveryInfoType | None = None, ) -> bool: pass From a1d96175a891728dc16dc7cad3d950cef96771d3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 29 Jul 2022 00:25:31 +0000 Subject: [PATCH 2950/3516] [ci skip] Translation update --- .../components/ambee/translations/ca.json | 6 ++++++ .../components/ambee/translations/it.json | 2 +- .../components/anthemav/translations/ca.json | 6 ++++++ .../components/anthemav/translations/it.json | 4 ++-- .../anthemav/translations/zh-Hant.json | 2 +- .../components/google/translations/ca.json | 10 ++++++++++ .../components/google/translations/it.json | 6 +++--- .../google/translations/zh-Hant.json | 2 +- .../homeassistant_alerts/translations/ca.json | 8 ++++++++ .../lacrosse_view/translations/ca.json | 20 +++++++++++++++++++ .../components/lyric/translations/ca.json | 6 ++++++ .../components/lyric/translations/it.json | 2 +- .../components/miflora/translations/ca.json | 8 ++++++++ .../components/miflora/translations/it.json | 2 +- .../components/mitemp_bt/translations/ca.json | 8 ++++++++ .../components/mitemp_bt/translations/it.json | 2 +- .../openalpr_local/translations/ca.json | 8 ++++++++ .../openalpr_local/translations/it.json | 2 +- .../radiotherm/translations/ca.json | 2 +- .../radiotherm/translations/it.json | 4 ++-- .../radiotherm/translations/zh-Hant.json | 2 +- .../components/senz/translations/ca.json | 6 ++++++ .../components/senz/translations/it.json | 2 +- .../simplepush/translations/ca.json | 6 ++++++ .../simplepush/translations/de.json | 6 ++++++ .../simplepush/translations/en.json | 4 ++-- .../simplepush/translations/it.json | 6 ++++++ .../simplepush/translations/pt-BR.json | 6 ++++++ .../simplepush/translations/zh-Hant.json | 6 ++++++ .../soundtouch/translations/ca.json | 6 ++++++ .../soundtouch/translations/it.json | 4 ++-- .../soundtouch/translations/zh-Hant.json | 2 +- .../components/spotify/translations/ca.json | 6 ++++++ .../components/spotify/translations/it.json | 2 +- .../steam_online/translations/ca.json | 6 ++++++ .../steam_online/translations/it.json | 2 +- .../components/uscis/translations/ca.json | 2 +- .../components/xbox/translations/ca.json | 6 ++++++ .../components/xbox/translations/de.json | 6 ++++++ .../components/xbox/translations/it.json | 4 ++-- .../components/xbox/translations/zh-Hant.json | 6 ++++++ .../xiaomi_ble/translations/it.json | 2 +- .../components/zha/translations/ca.json | 2 ++ .../components/zha/translations/it.json | 1 + 44 files changed, 183 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/homeassistant_alerts/translations/ca.json create mode 100644 homeassistant/components/lacrosse_view/translations/ca.json create mode 100644 homeassistant/components/miflora/translations/ca.json create mode 100644 homeassistant/components/mitemp_bt/translations/ca.json create mode 100644 homeassistant/components/openalpr_local/translations/ca.json diff --git a/homeassistant/components/ambee/translations/ca.json b/homeassistant/components/ambee/translations/ca.json index ac48eea1cd6..bb4d49642b5 100644 --- a/homeassistant/components/ambee/translations/ca.json +++ b/homeassistant/components/ambee/translations/ca.json @@ -24,5 +24,11 @@ "description": "Configura la integraci\u00f3 d'Ambee amb Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "La integraci\u00f3 d'Ambee s'eliminar\u00e0 de Home Assistant i deixar\u00e0 d'estar disponible a la versi\u00f3 de Home Assistant 2022.10.\n\nLa integraci\u00f3 s'eliminar\u00e0 perqu\u00e8 Ambee ha eliminat els seus comptes gratu\u00efts (limitats) i no ha donat cap manera per als usuaris normals de registrar-se a un pla de pagament.\n\nElimina la integraci\u00f3 d'Ambee del Home Assistant per solucionar aquest problema.", + "title": "La integraci\u00f3 Ambee est\u00e0 sent eliminada" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/it.json b/homeassistant/components/ambee/translations/it.json index db330a9b239..f2054c8a6ff 100644 --- a/homeassistant/components/ambee/translations/it.json +++ b/homeassistant/components/ambee/translations/it.json @@ -28,7 +28,7 @@ "issues": { "pending_removal": { "description": "L'integrazione Ambee \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nL'integrazione \u00e8 stata rimossa, perch\u00e9 Ambee ha rimosso i loro account gratuiti (limitati) e non offre pi\u00f9 agli utenti regolari un modo per iscriversi a un piano a pagamento. \n\nRimuovi la voce di integrazione Ambee dalla tua istanza per risolvere questo problema.", - "title": "L'integrazione Ambee verr\u00e0 rimossa" + "title": "L'integrazione Ambee sar\u00e0 rimossa" } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/ca.json b/homeassistant/components/anthemav/translations/ca.json index 723883d5c1a..20785a9e67d 100644 --- a/homeassistant/components/anthemav/translations/ca.json +++ b/homeassistant/components/anthemav/translations/ca.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 d'Anthem A/V Receivers mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML d'Anthem A/V Receivers del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML d'Anthem A/V Receivers est\u00e0 sent eliminada" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/it.json b/homeassistant/components/anthemav/translations/it.json index b8bec832581..847332b57ad 100644 --- a/homeassistant/components/anthemav/translations/it.json +++ b/homeassistant/components/anthemav/translations/it.json @@ -18,8 +18,8 @@ }, "issues": { "deprecated_yaml": { - "description": "La configurazione di Anthem A/V Receivers tramite YAML verr\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovere la configurazione YAML di Anthem A/V Receivers dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Anthem A/V Receivers verr\u00e0 rimossa" + "description": "La configurazione di Anthem A/V Receivers tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Anthem A/V Receivers dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Anthem A/V Receivers sar\u00e0 rimossa" } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/zh-Hant.json b/homeassistant/components/anthemav/translations/zh-Hant.json index 0751331f82f..acba04f49e6 100644 --- a/homeassistant/components/anthemav/translations/zh-Hant.json +++ b/homeassistant/components/anthemav/translations/zh-Hant.json @@ -18,7 +18,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Anthem A/V \u63a5\u6536\u5668\u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Anthem A/V \u63a5\u6536\u5668\u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" } } diff --git a/homeassistant/components/google/translations/ca.json b/homeassistant/components/google/translations/ca.json index 066630df50d..b7645abe54b 100644 --- a/homeassistant/components/google/translations/ca.json +++ b/homeassistant/components/google/translations/ca.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de Google Calentdar mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant a la versi\u00f3 2022.9. \n\nLa configuraci\u00f3 existent de credencials d'aplicaci\u00f3 OAuth i d'acc\u00e9s s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Google Calendar est\u00e0 sent eliminada" + }, + "removed_track_new_yaml": { + "description": "Has desactivat el seguiment d'entitats de Google Calendar a configuration.yaml, que ja no \u00e9s compatible. Per desactivar les entitats descobertes recentment, a partir d'ara, has de canviar manualment les opcions de sistema de la integraci\u00f3 a trav\u00e9s de la interf\u00edcie d'usuari. Elimina la configuraci\u00f3 'track_new' de configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "El seguiment d'entitats de Google Calendar ha canviat" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/it.json b/homeassistant/components/google/translations/it.json index 6e24178996f..282c1d06544 100644 --- a/homeassistant/components/google/translations/it.json +++ b/homeassistant/components/google/translations/it.json @@ -35,11 +35,11 @@ }, "issues": { "deprecated_yaml": { - "description": "La configurazione di Google Calendar in configuration.yaml verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Google Calendar verr\u00e0 rimossa" + "description": "La configurazione di Google Calendar in configuration.yaml sar\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Google Calendar sar\u00e0 rimossa" }, "removed_track_new_yaml": { - "description": "Hai disabilitato il tracciamento delle entit\u00e0 per Google Calendar in configuration.yaml, il che non \u00e8 pi\u00f9 supportato. \u00c8 necessario modificare manualmente le opzioni di sistema dell'integrazione nell'interfaccia utente per disabilitare le entit\u00e0 appena rilevate da adesso in poi. Rimuovi l'impostazione track_new da configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "description": "Hai disabilitato il tracciamento delle entit\u00e0 per Google Calendar in configuration.yaml, che non \u00e8 pi\u00f9 supportato. \u00c8 necessario modificare manualmente le opzioni di sistema dell'integrazione nell'interfaccia utente per disabilitare le nuove entit\u00e0 rilevate in futuro. Rimuovi l'impostazione track_new da configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "Il tracciamento dell'entit\u00e0 di Google Calendar \u00e8 cambiato" } }, diff --git a/homeassistant/components/google/translations/zh-Hant.json b/homeassistant/components/google/translations/zh-Hant.json index 0d2031c368f..93e2fa8f7ba 100644 --- a/homeassistant/components/google/translations/zh-Hant.json +++ b/homeassistant/components/google/translations/zh-Hant.json @@ -35,7 +35,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Google \u65e5\u66c6\u5df2\u7d93\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 OAuth \u61c9\u7528\u6191\u8b49\u8207\u5b58\u53d6\u6b0a\u9650\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Google \u65e5\u66c6\u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 OAuth \u61c9\u7528\u6191\u8b49\u8207\u5b58\u53d6\u6b0a\u9650\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "Google \u65e5\u66c6 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" }, "removed_track_new_yaml": { diff --git a/homeassistant/components/homeassistant_alerts/translations/ca.json b/homeassistant/components/homeassistant_alerts/translations/ca.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/ca.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/ca.json b/homeassistant/components/lacrosse_view/translations/ca.json new file mode 100644 index 00000000000..bdf5e41bf54 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "no_locations": "No s'han trobat ubicacions", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/ca.json b/homeassistant/components/lyric/translations/ca.json index 3e301a4bf4b..47f4fdb559b 100644 --- a/homeassistant/components/lyric/translations/ca.json +++ b/homeassistant/components/lyric/translations/ca.json @@ -17,5 +17,11 @@ "title": "Reautenticaci\u00f3 de la integraci\u00f3" } } + }, + "issues": { + "removed_yaml": { + "description": "La configuraci\u00f3 de Honeywell Lyric mitjan\u00e7ant YAML s'ha eliminat de Home Assistant.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Honeywell Lyric s'ha eliminat" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/it.json b/homeassistant/components/lyric/translations/it.json index e0e97ef3246..57c201e952d 100644 --- a/homeassistant/components/lyric/translations/it.json +++ b/homeassistant/components/lyric/translations/it.json @@ -20,7 +20,7 @@ }, "issues": { "removed_yaml": { - "description": "La configurazione di Honeywell Lyric tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "description": "La configurazione di Honeywell Lyric tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non \u00e8 utilizzata da Home Assistant. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "La configurazione YAML di Honeywell Lyric \u00e8 stata rimossa" } } diff --git a/homeassistant/components/miflora/translations/ca.json b/homeassistant/components/miflora/translations/ca.json new file mode 100644 index 00000000000..c4dd9209a00 --- /dev/null +++ b/homeassistant/components/miflora/translations/ca.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "La integraci\u00f3 Mi Flora ha deixar de funcionar a Home Assistant 2022.7 i ha estat substitu\u00efda per la integraci\u00f3 Xiaomi BLE a la versi\u00f3 Home Assistant 2022.8.\n\nNo hi ha cap migraci\u00f3 possible, per tant, has d'afegir els teus dispositius Mi Flora manualment utilitzant la nova integraci\u00f3.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML de l'antiga integraci\u00f3. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La integraci\u00f3 de Mi Flora ha estat substitu\u00efda" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/it.json b/homeassistant/components/miflora/translations/it.json index cdd2d89ca72..b5893f33d43 100644 --- a/homeassistant/components/miflora/translations/it.json +++ b/homeassistant/components/miflora/translations/it.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "L'integrazione Mi Flora ha smesso di funzionare in Home Assistant 2022.7 e sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Mi Flora utilizzando la nuova integrazione. \n\nLa configurazione YAML di Mi Flora esistente non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "description": "L'integrazione Mi Flora ha smesso di funzionare in Home Assistant 2022.7 e sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Mi Flora utilizzando la nuova integrazione. \n\nLa configurazione YAML di Mi Flora esistente non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "L'integrazione Mi Flora \u00e8 stata sostituita" } } diff --git a/homeassistant/components/mitemp_bt/translations/ca.json b/homeassistant/components/mitemp_bt/translations/ca.json new file mode 100644 index 00000000000..a567bceb950 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/ca.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "La integraci\u00f3 Xiaomi Mijia sensor de temperatura i humitat BLE va deixar de funcionar a Home Assistant 2022.7 i ha estat substitu\u00efda per la integraci\u00f3 Xiaomi BLE a la versi\u00f3 Home Assistant 2022.8.\n\nNo hi ha cap migraci\u00f3 possible, per tant, has d'afegir els teus dispositius Xiaomi Mijia BLE manualment utilitzant la nova integraci\u00f3.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML de l'antiga integraci\u00f3. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La integraci\u00f3 Xiaomi Mijia sensor de temperatura i humitat BLE ha estat substitu\u00efda" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/it.json b/homeassistant/components/mitemp_bt/translations/it.json index cc383e4184c..0fe7ab58919 100644 --- a/homeassistant/components/mitemp_bt/translations/it.json +++ b/homeassistant/components/mitemp_bt/translations/it.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor ha smesso di funzionare in Home Assistant 2022.7 ed \u00e8 stata sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Xiaomi Mijia BLE utilizzando la nuova integrazione. \n\nLa configurazione YAML di Xiaomi Mijia BLE Temperature and Humidity Sensor esistente non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "description": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor ha smesso di funzionare in Home Assistant 2022.7 ed \u00e8 stata sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Xiaomi Mijia BLE utilizzando la nuova integrazione. \n\nLa configurazione YAML di Xiaomi Mijia BLE Temperature and Humidity Sensor esistente non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor \u00e8 stata sostituita" } } diff --git a/homeassistant/components/openalpr_local/translations/ca.json b/homeassistant/components/openalpr_local/translations/ca.json new file mode 100644 index 00000000000..3617117ac4a --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/ca.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "La integraci\u00f3 d'OpenALPR Local s'eliminar\u00e0 de Home Assistant i deixar\u00e0 d'estar disponible a la versi\u00f3 de Home Assistant 2022.10.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per arreglar aquest error.", + "title": "La integraci\u00f3 OpenALPR Local est\u00e0 sent eliminada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/it.json b/homeassistant/components/openalpr_local/translations/it.json index 26ce80ee584..d4227ca7e36 100644 --- a/homeassistant/components/openalpr_local/translations/it.json +++ b/homeassistant/components/openalpr_local/translations/it.json @@ -2,7 +2,7 @@ "issues": { "pending_removal": { "description": "L'integrazione OpenALPR Local \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", - "title": "L'integrazione OpenALPR Local verr\u00e0 rimossa" + "title": "L'integrazione OpenALPR Local sar\u00e0 rimossa" } } } \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/ca.json b/homeassistant/components/radiotherm/translations/ca.json index 58e8487607f..b362fc467ec 100644 --- a/homeassistant/components/radiotherm/translations/ca.json +++ b/homeassistant/components/radiotherm/translations/ca.json @@ -22,7 +22,7 @@ "issues": { "deprecated_yaml": { "description": "La configuraci\u00f3 de la plataforma 'Radio Thermostat' mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant a la versi\u00f3 2022.9. \n\nLa configuraci\u00f3 existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", - "title": "S'est\u00e0 eliminant la configuraci\u00f3 YAML de Thermostat Radio" + "title": "La configuraci\u00f3 YAML de Thermostat Radio est\u00e0 sent eliminada" } }, "options": { diff --git a/homeassistant/components/radiotherm/translations/it.json b/homeassistant/components/radiotherm/translations/it.json index fef1c64746b..58860ab3293 100644 --- a/homeassistant/components/radiotherm/translations/it.json +++ b/homeassistant/components/radiotherm/translations/it.json @@ -21,8 +21,8 @@ }, "issues": { "deprecated_yaml": { - "description": "La configurazione della piattaforma climatica Radio Thermostat tramite YAML verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLa configurazione esistente \u00e8 stata importata automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Radio Thermostat verr\u00e0 rimossa" + "description": "La configurazione della piattaforma climatica Radio Thermostat tramite YAML sar\u00e0 rimossa in Home Assistant 2022.9. \n\nLa configurazione esistente \u00e8 stata importata automaticamente nell'interfaccia utente. Rimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Radio Thermostat sar\u00e0 rimossa" } }, "options": { diff --git a/homeassistant/components/radiotherm/translations/zh-Hant.json b/homeassistant/components/radiotherm/translations/zh-Hant.json index ad1af3bb442..96949adb129 100644 --- a/homeassistant/components/radiotherm/translations/zh-Hant.json +++ b/homeassistant/components/radiotherm/translations/zh-Hant.json @@ -21,7 +21,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Radio \u6eab\u63a7\u5668\u5df2\u7d93\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684\u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Radio \u6eab\u63a7\u5668\u5df2\u7d93\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684\u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "Radio \u6eab\u63a7\u5668 YAML \u8a2d\u5b9a\u5df2\u79fb\u9664" } }, diff --git a/homeassistant/components/senz/translations/ca.json b/homeassistant/components/senz/translations/ca.json index 20b2ceceddd..964e0319367 100644 --- a/homeassistant/components/senz/translations/ca.json +++ b/homeassistant/components/senz/translations/ca.json @@ -16,5 +16,11 @@ "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" } } + }, + "issues": { + "removed_yaml": { + "description": "La configuraci\u00f3 de nVent RAYCHEM SENZ mitjan\u00e7ant YAML s'ha eliminat de Home Assistant.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de nVent RAYCHEM SENZ s'ha eliminat" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/it.json b/homeassistant/components/senz/translations/it.json index be6608af499..d3ec2a1568c 100644 --- a/homeassistant/components/senz/translations/it.json +++ b/homeassistant/components/senz/translations/it.json @@ -19,7 +19,7 @@ }, "issues": { "removed_yaml": { - "description": "La configurazione di nVent RAYCHEM SENZ tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "description": "La configurazione di nVent RAYCHEM SENZ tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non \u00e8 utilizzata da Home Assistant. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "La configurazione YAML di nVent RAYCHEM SENZ \u00e8 stata rimossa" } } diff --git a/homeassistant/components/simplepush/translations/ca.json b/homeassistant/components/simplepush/translations/ca.json index 161e1a3c36c..d4e449d8a35 100644 --- a/homeassistant/components/simplepush/translations/ca.json +++ b/homeassistant/components/simplepush/translations/ca.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de Simplepush mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML de Simplepush del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Simplepush est\u00e0 sent eliminada" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/de.json b/homeassistant/components/simplepush/translations/de.json index c7f633d312d..523ffda32bf 100644 --- a/homeassistant/components/simplepush/translations/de.json +++ b/homeassistant/components/simplepush/translations/de.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Simplepush mittels YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die Simplepush-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Simplepush YAML-Konfiguration wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/en.json b/homeassistant/components/simplepush/translations/en.json index bf373d8baf0..8674616dda1 100644 --- a/homeassistant/components/simplepush/translations/en.json +++ b/homeassistant/components/simplepush/translations/en.json @@ -20,8 +20,8 @@ }, "issues": { "deprecated_yaml": { - "title": "The Simplepush YAML configuration is being removed", - "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Simplepush YAML configuration is being removed" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/it.json b/homeassistant/components/simplepush/translations/it.json index 2ba1bea5d96..b3a9b44f938 100644 --- a/homeassistant/components/simplepush/translations/it.json +++ b/homeassistant/components/simplepush/translations/it.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Simplepush tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Simplepush dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Simplepush sar\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/pt-BR.json b/homeassistant/components/simplepush/translations/pt-BR.json index bf933fe94da..f0a330ff1d3 100644 --- a/homeassistant/components/simplepush/translations/pt-BR.json +++ b/homeassistant/components/simplepush/translations/pt-BR.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Simplepush usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o Simplepush YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o Simplepush YAML est\u00e1 sendo removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/zh-Hant.json b/homeassistant/components/simplepush/translations/zh-Hant.json index 891f2242467..15cd0bedb37 100644 --- a/homeassistant/components/simplepush/translations/zh-Hant.json +++ b/homeassistant/components/simplepush/translations/zh-Hant.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Simplepush \u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Simplepush YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Simplepush YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ca.json b/homeassistant/components/soundtouch/translations/ca.json index baba19644fb..165668e0016 100644 --- a/homeassistant/components/soundtouch/translations/ca.json +++ b/homeassistant/components/soundtouch/translations/ca.json @@ -17,5 +17,11 @@ "title": "Confirma l'addici\u00f3 del dispositiu Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de Bose SoundTouch mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML de Bose SoundTouch del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Bose SoundTouch est\u00e0 sent eliminada" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/it.json b/homeassistant/components/soundtouch/translations/it.json index f9c6d512b2a..11aaba04caf 100644 --- a/homeassistant/components/soundtouch/translations/it.json +++ b/homeassistant/components/soundtouch/translations/it.json @@ -20,8 +20,8 @@ }, "issues": { "deprecated_yaml": { - "description": "La configurazione di Bose SoundTouch tramite YAML verr\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovere la configurazione YAML di Bose SoundTouch dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Bose SoundTouch verr\u00e0 rimossa" + "description": "La configurazione di Bose SoundTouch tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Bose SoundTouch dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Bose SoundTouch sar\u00e0 rimossa" } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/zh-Hant.json b/homeassistant/components/soundtouch/translations/zh-Hant.json index f3d8e8e8560..80d78ba9d20 100644 --- a/homeassistant/components/soundtouch/translations/zh-Hant.json +++ b/homeassistant/components/soundtouch/translations/zh-Hant.json @@ -20,7 +20,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Bose SoundTouch \u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Bose SoundTouch YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Bose SoundTouch \u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Bose SoundTouch YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "Bose SoundTouch YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" } } diff --git a/homeassistant/components/spotify/translations/ca.json b/homeassistant/components/spotify/translations/ca.json index fffb248573d..0bde1dc49e3 100644 --- a/homeassistant/components/spotify/translations/ca.json +++ b/homeassistant/components/spotify/translations/ca.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "La configuraci\u00f3 de Spotify mitjan\u00e7ant YAML s'ha eliminat de Home Assistant.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "S'ha eliminat la configuraci\u00f3 YAML de Spotify" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Endpoint de l'API d'Spotify accessible" diff --git a/homeassistant/components/spotify/translations/it.json b/homeassistant/components/spotify/translations/it.json index 2ca8d323607..2324e80c8ca 100644 --- a/homeassistant/components/spotify/translations/it.json +++ b/homeassistant/components/spotify/translations/it.json @@ -21,7 +21,7 @@ }, "issues": { "removed_yaml": { - "description": "La configurazione di Spotify tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "description": "La configurazione di Spotify tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "La configurazione YAML di Spotify \u00e8 stata rimossa" } }, diff --git a/homeassistant/components/steam_online/translations/ca.json b/homeassistant/components/steam_online/translations/ca.json index a9491e2e502..bd995f8d0e2 100644 --- a/homeassistant/components/steam_online/translations/ca.json +++ b/homeassistant/components/steam_online/translations/ca.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "La configuraci\u00f3 de Steam mitjan\u00e7ant YAML s'ha eliminat de Home Assistant.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Steam s'ha eliminat" + } + }, "options": { "error": { "unauthorized": "Llista d'amics restringida: consulta la documentaci\u00f3 sobre com veure tots els amics" diff --git a/homeassistant/components/steam_online/translations/it.json b/homeassistant/components/steam_online/translations/it.json index fb962124f2a..5d7712c7072 100644 --- a/homeassistant/components/steam_online/translations/it.json +++ b/homeassistant/components/steam_online/translations/it.json @@ -26,7 +26,7 @@ }, "issues": { "removed_yaml": { - "description": "La configurazione di Steam tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non viene utilizzata da Home Assistant. \n\nRimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "description": "La configurazione di Steam tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non \u00e8 utilizzata da Home Assistant. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "La configurazione YAML di Steam \u00e8 stata rimossa" } }, diff --git a/homeassistant/components/uscis/translations/ca.json b/homeassistant/components/uscis/translations/ca.json index 2072cfb0d92..858214eee34 100644 --- a/homeassistant/components/uscis/translations/ca.json +++ b/homeassistant/components/uscis/translations/ca.json @@ -2,7 +2,7 @@ "issues": { "pending_removal": { "description": "La integraci\u00f3 de Serveis de Ciutadania i Immigraci\u00f3 dels Estats Units (USCIS) est\u00e0 pendent d'eliminar-se de Home Assistant i ja no estar\u00e0 disponible a partir de Home Assistant 2022.10. \n\nLa integraci\u00f3 s'est\u00e0 eliminant, perqu\u00e8 es basa en el 'webscraping', que no est\u00e0 adm\u00e8s. \n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per arreglar aquest error.", - "title": "S'est\u00e0 eliminant la integraci\u00f3 USCIS" + "title": "La integraci\u00f3 USCIS est\u00e0 sent eliminada" } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/ca.json b/homeassistant/components/xbox/translations/ca.json index e0fe185973b..56f3a7b3440 100644 --- a/homeassistant/components/xbox/translations/ca.json +++ b/homeassistant/components/xbox/translations/ca.json @@ -13,5 +13,11 @@ "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de Xbox mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant a la versi\u00f3 2022.9. \n\nLa configuraci\u00f3 existent de credencials d'aplicaci\u00f3 OAuth i d'acc\u00e9s s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Xbox est\u00e0 sent eliminada" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/de.json b/homeassistant/components/xbox/translations/de.json index 615c8f8cf2a..e42cf1d37dc 100644 --- a/homeassistant/components/xbox/translations/de.json +++ b/homeassistant/components/xbox/translations/de.json @@ -13,5 +13,11 @@ "title": "W\u00e4hle die Authentifizierungsmethode" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration der Xbox in configuration.yaml wird in Home Assistant 2022.9 entfernt.\n\nDeine bestehenden OAuth-Anmeldedaten und Zugriffseinstellungen wurden automatisch in die Benutzeroberfl\u00e4che importiert. Entferne die YAML-Konfiguration aus deiner configuration.yaml und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Xbox YAML-Konfiguration wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/it.json b/homeassistant/components/xbox/translations/it.json index 6cf5bf15bb9..b1bb503bae0 100644 --- a/homeassistant/components/xbox/translations/it.json +++ b/homeassistant/components/xbox/translations/it.json @@ -16,8 +16,8 @@ }, "issues": { "deprecated_yaml": { - "description": "La configurazione di Xbox in configuration.yaml verr\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovere la configurazione YAML dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", - "title": "La configurazione YAML di Xbox verr\u00e0 rimossa" + "description": "La configurazione di Xbox in configuration.yaml sar\u00e0 rimossa in Home Assistant 2022.9. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Xbox sar\u00e0 rimossa" } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/zh-Hant.json b/homeassistant/components/xbox/translations/zh-Hant.json index 9d348536ec3..4a44e9bc233 100644 --- a/homeassistant/components/xbox/translations/zh-Hant.json +++ b/homeassistant/components/xbox/translations/zh-Hant.json @@ -13,5 +13,11 @@ "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Xbox \u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 OAuth \u61c9\u7528\u6191\u8b49\u8207\u5b58\u53d6\u6b0a\u9650\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Xbox YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/it.json b/homeassistant/components/xiaomi_ble/translations/it.json index 99adacee466..018829bfbd2 100644 --- a/homeassistant/components/xiaomi_ble/translations/it.json +++ b/homeassistant/components/xiaomi_ble/translations/it.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", - "decryption_failed": "La chiave di collegamento fornita non funziona, i dati del sensore non possono essere decifrati. Controllare e riprovare.", + "decryption_failed": "La chiave di collegamento fornita non funziona, i dati del sensore non possono essere decifrati. Controlla e riprova.", "expected_24_characters": "Prevista una chiave di collegamento esadecimale di 24 caratteri.", "expected_32_characters": "Prevista una chiave di collegamento esadecimale di 32 caratteri.", "no_devices_found": "Nessun dispositivo trovato sulla rete" diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index 4167704e4a0..dbb8a20be82 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -46,11 +46,13 @@ "title": "Opcions del panell de control d'alarma" }, "zha_options": { + "always_prefer_xy_color_mode": "Utilitza preferentment el mode de color XY", "consider_unavailable_battery": "Considera els dispositius amb bateria com a no disponibles al cap de (segons)", "consider_unavailable_mains": "Considera els dispositius connectats a la xarxa el\u00e8ctrica com a no disponibles al cap de (segons)", "default_light_transition": "Temps de transici\u00f3 predeterminat (segons)", "enable_identify_on_join": "Activa l'efecte d'identificaci\u00f3 quan els dispositius s'uneixin a la xarxa", "enhanced_light_transition": "Activa la transici\u00f3 millorada de color/temperatura de llum des de l'estat apagat", + "light_transitioning_flag": "Activa el control lliscant de brillantor millorat durant la transici\u00f3", "title": "Opcions globals" } }, diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 4666ba7e494..a71e96ed308 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -52,6 +52,7 @@ "default_light_transition": "Tempo di transizione della luce predefinito (secondi)", "enable_identify_on_join": "Abilita l'effetto di identificazione quando i dispositivi si uniscono alla rete", "enhanced_light_transition": "Abilita una transizione migliorata del colore/temperatura della luce da uno stato spento", + "light_transitioning_flag": "Abilita il cursore della luminosit\u00e0 avanzata durante la transizione della luce", "title": "Opzioni globali" } }, From bbd7041a73572547be49ead53b183aa1e55a6d75 Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Fri, 29 Jul 2022 13:20:05 +1200 Subject: [PATCH 2951/3516] Refactor and improve anthemav (#75852) --- homeassistant/components/anthemav/__init__.py | 14 ++++- .../components/anthemav/media_player.py | 21 ++++--- tests/components/anthemav/conftest.py | 22 +++++++ tests/components/anthemav/test_config_flow.py | 36 +++++++++-- tests/components/anthemav/test_init.py | 61 +++++++++++-------- 5 files changed, 112 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/anthemav/__init__.py b/homeassistant/components/anthemav/__init__.py index 5ad845b52d6..eb1d9b0b560 100644 --- a/homeassistant/components/anthemav/__init__.py +++ b/homeassistant/components/anthemav/__init__.py @@ -6,8 +6,8 @@ import logging import anthemav from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -25,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: def async_anthemav_update_callback(message): """Receive notification from transport that new data exists.""" _LOGGER.debug("Received update callback from AVR: %s", message) - async_dispatcher_send(hass, f"{ANTHEMAV_UDATE_SIGNAL}_{entry.data[CONF_NAME]}") + async_dispatcher_send(hass, f"{ANTHEMAV_UDATE_SIGNAL}_{entry.entry_id}") try: avr = await anthemav.Connection.create( @@ -41,6 +41,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_setup_platforms(entry, PLATFORMS) + @callback + def close_avr(event: Event) -> None: + avr.close() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_avr) + ) + return True diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index 3c3a363a6db..bf8172083e6 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -85,17 +85,17 @@ async def async_setup_entry( ) -> None: """Set up entry.""" name = config_entry.data[CONF_NAME] - macaddress = config_entry.data[CONF_MAC] + mac_address = config_entry.data[CONF_MAC] model = config_entry.data[CONF_MODEL] avr = hass.data[DOMAIN][config_entry.entry_id] - device = AnthemAVR(avr, name, macaddress, model) + entity = AnthemAVR(avr, name, mac_address, model, config_entry.entry_id) - _LOGGER.debug("dump_devicedata: %s", device.dump_avrdata) - _LOGGER.debug("dump_conndata: %s", avr.dump_conndata) + _LOGGER.debug("Device data dump: %s", entity.dump_avrdata) + _LOGGER.debug("Connection data dump: %s", avr.dump_conndata) - async_add_entities([device]) + async_add_entities([entity]) class AnthemAVR(MediaPlayerEntity): @@ -110,14 +110,17 @@ class AnthemAVR(MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOURCE ) - def __init__(self, avr: Connection, name: str, macaddress: str, model: str) -> None: + def __init__( + self, avr: Connection, name: str, mac_address: str, model: str, entry_id: str + ) -> None: """Initialize entity with transport.""" super().__init__() self.avr = avr + self._entry_id = entry_id self._attr_name = name - self._attr_unique_id = macaddress + self._attr_unique_id = mac_address self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, macaddress)}, + identifiers={(DOMAIN, mac_address)}, name=name, manufacturer=MANUFACTURER, model=model, @@ -131,7 +134,7 @@ class AnthemAVR(MediaPlayerEntity): self.async_on_remove( async_dispatcher_connect( self.hass, - f"{ANTHEMAV_UDATE_SIGNAL}_{self._attr_name}", + f"{ANTHEMAV_UDATE_SIGNAL}_{self._entry_id}", self.async_write_ha_state, ) ) diff --git a/tests/components/anthemav/conftest.py b/tests/components/anthemav/conftest.py index 8fbdf3145c3..f96696fe308 100644 --- a/tests/components/anthemav/conftest.py +++ b/tests/components/anthemav/conftest.py @@ -3,6 +3,11 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest +from homeassistant.components.anthemav.const import CONF_MODEL, DOMAIN +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT + +from tests.common import MockConfigEntry + @pytest.fixture def mock_anthemav() -> AsyncMock: @@ -14,6 +19,7 @@ def mock_anthemav() -> AsyncMock: avr.close = MagicMock() avr.protocol.input_list = [] avr.protocol.audio_listening_mode_list = [] + avr.protocol.power = False return avr @@ -26,3 +32,19 @@ def mock_connection_create(mock_anthemav: AsyncMock) -> AsyncMock: return_value=mock_anthemav, ) as mock: yield mock + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 14999, + CONF_NAME: "Anthem AV", + CONF_MAC: "00:00:00:00:00:01", + CONF_MODEL: "MRX 520", + }, + unique_id="00:00:00:00:00:01", + ) diff --git a/tests/components/anthemav/test_config_flow.py b/tests/components/anthemav/test_config_flow.py index 1f3dec8d5e1..f8bec435dc6 100644 --- a/tests/components/anthemav/test_config_flow.py +++ b/tests/components/anthemav/test_config_flow.py @@ -2,19 +2,22 @@ from unittest.mock import AsyncMock, patch from anthemav.device_error import DeviceError +import pytest -from homeassistant import config_entries from homeassistant.components.anthemav.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from tests.common import MockConfigEntry + async def test_form_with_valid_connection( hass: HomeAssistant, mock_connection_create: AsyncMock, mock_anthemav: AsyncMock ) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.FORM assert result["errors"] is None @@ -47,7 +50,7 @@ async def test_form_with_valid_connection( async def test_form_device_info_error(hass: HomeAssistant) -> None: """Test we handle DeviceError from library.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) with patch( @@ -71,7 +74,7 @@ async def test_form_device_info_error(hass: HomeAssistant) -> None: async def test_form_cannot_connect(hass: HomeAssistant) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) with patch( @@ -102,7 +105,7 @@ async def test_import_configuration( "name": "Anthem Av Import", } result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=config + DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) assert result["type"] == FlowResultType.CREATE_ENTRY @@ -113,3 +116,26 @@ async def test_import_configuration( "mac": "00:00:00:00:00:01", "model": "MRX 520", } + + +@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT]) +async def test_device_already_configured( + hass: HomeAssistant, + mock_connection_create: AsyncMock, + mock_anthemav: AsyncMock, + mock_config_entry: MockConfigEntry, + source: str, +) -> None: + """Test we import existing configuration.""" + config = { + "host": "1.1.1.1", + "port": 14999, + } + + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=config + ) + + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "already_configured" diff --git a/tests/components/anthemav/test_init.py b/tests/components/anthemav/test_init.py index 866925f4e46..97dc5be95b0 100644 --- a/tests/components/anthemav/test_init.py +++ b/tests/components/anthemav/test_init.py @@ -2,28 +2,19 @@ from unittest.mock import ANY, AsyncMock, patch from homeassistant import config_entries -from homeassistant.components.anthemav.const import CONF_MODEL, DOMAIN -from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry async def test_load_unload_config_entry( - hass: HomeAssistant, mock_connection_create: AsyncMock, mock_anthemav: AsyncMock + hass: HomeAssistant, + mock_connection_create: AsyncMock, + mock_anthemav: AsyncMock, + mock_config_entry: MockConfigEntry, ) -> None: """Test load and unload AnthemAv component.""" - mock_config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_HOST: "1.1.1.1", - CONF_PORT: 14999, - CONF_NAME: "Anthem AV", - CONF_MAC: "aabbccddeeff", - CONF_MODEL: "MRX 520", - }, - ) - mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() @@ -42,18 +33,10 @@ async def test_load_unload_config_entry( mock_anthemav.close.assert_called_once() -async def test_config_entry_not_ready(hass: HomeAssistant) -> None: +async def test_config_entry_not_ready( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: """Test AnthemAV configuration entry not ready.""" - mock_config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_HOST: "1.1.1.1", - CONF_PORT: 14999, - CONF_NAME: "Anthem AV", - CONF_MAC: "aabbccddeeff", - CONF_MODEL: "MRX 520", - }, - ) with patch( "anthemav.Connection.create", @@ -63,3 +46,31 @@ async def test_config_entry_not_ready(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() assert mock_config_entry.state is config_entries.ConfigEntryState.SETUP_RETRY + + +async def test_anthemav_dispatcher_signal( + hass: HomeAssistant, + mock_connection_create: AsyncMock, + mock_anthemav: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test send update signal to dispatcher.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + states = hass.states.get("media_player.anthem_av") + assert states + assert states.state == STATE_OFF + + # change state of the AVR + mock_anthemav.protocol.power = True + + # get the callback function that trigger the signal to update the state + avr_update_callback = mock_connection_create.call_args[1]["update_callback"] + avr_update_callback("power") + + await hass.async_block_till_done() + + states = hass.states.get("media_player.anthem_av") + assert states.state == STATE_ON From 9f16c1468103ed145dfde3e08a2b464e70543a25 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jul 2022 17:07:32 -1000 Subject: [PATCH 2952/3516] Fix incorrect manufacturer_id for govee 5182 model (#75899) --- homeassistant/components/govee_ble/manifest.json | 6 +++++- homeassistant/generated/bluetooth.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index aa86215da59..270858d04d4 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -18,9 +18,13 @@ { "manufacturer_id": 14474, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + }, + { + "manufacturer_id": 10032, + "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.12.3"], + "requirements": ["govee-ble==0.12.4"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 5dde90f1f7a..8d92d6eab4a 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -34,6 +34,11 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "manufacturer_id": 14474, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" }, + { + "domain": "govee_ble", + "manufacturer_id": 10032, + "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" + }, { "domain": "homekit_controller", "manufacturer_id": 76, diff --git a/requirements_all.txt b/requirements_all.txt index 5094f5a3241..b3879d922db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.3 +govee-ble==0.12.4 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8b8c5be6b7..0a81c1cb8d3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,7 +561,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.3 +govee-ble==0.12.4 # homeassistant.components.gree greeclimate==1.2.0 From 4b2beda4731c6fcbb3025a24e1412f6b460100cc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Jul 2022 22:26:37 -0700 Subject: [PATCH 2953/3516] Move some bleak imports to be behind TYPE_CHECKING (#75894) --- .../components/bluetooth_le_tracker/device_tracker.py | 5 ++++- homeassistant/components/fjaraskupan/__init__.py | 8 ++++++-- homeassistant/components/switchbot/coordinator.py | 10 ++++++---- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index a650e65b8f2..fa55c22f994 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -4,10 +4,10 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta import logging +from typing import TYPE_CHECKING from uuid import UUID from bleak import BleakClient, BleakError -from bleak.backends.device import BLEDevice import voluptuous as vol from homeassistant.components import bluetooth @@ -31,6 +31,9 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + _LOGGER = logging.getLogger(__name__) # Base UUID: 00000000-0000-1000-8000-00805F9B34FB diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 85e95db5513..36608fb026d 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -5,10 +5,9 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta import logging +from typing import TYPE_CHECKING from bleak import BleakScanner -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData from fjaraskupan import Device, State, device_filter from homeassistant.config_entries import ConfigEntry @@ -24,6 +23,11 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DISPATCH_DETECTION, DOMAIN +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + from bleak.backends.scanner import AdvertisementData + + PLATFORMS = [ Platform.BINARY_SENSOR, Platform.FAN, diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 31f7f2d3992..f461a3e0f4c 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -3,11 +3,9 @@ from __future__ import annotations import asyncio import logging -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast -from bleak.backends.device import BLEDevice import switchbot -from switchbot import parse_advertisement_data from homeassistant.components import bluetooth from homeassistant.components.bluetooth.passive_update_coordinator import ( @@ -15,6 +13,10 @@ from homeassistant.components.bluetooth.passive_update_coordinator import ( ) from homeassistant.core import HomeAssistant, callback +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + + _LOGGER = logging.getLogger(__name__) @@ -52,7 +54,7 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): """Handle a Bluetooth event.""" super()._async_handle_bluetooth_event(service_info, change) discovery_info_bleak = cast(bluetooth.BluetoothServiceInfoBleak, service_info) - if adv := parse_advertisement_data( + if adv := switchbot.parse_advertisement_data( discovery_info_bleak.device, discovery_info_bleak.advertisement ): self.data = flatten_sensors_data(adv.data) From aec885a46704baa71537e30d01e4efe52af9faa5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Jul 2022 00:53:08 -0700 Subject: [PATCH 2954/3516] Fix Roon media player being set up before hass.data set up (#75904) --- homeassistant/components/roon/__init__.py | 11 ++++++++++- homeassistant/components/roon/server.py | 9 +-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/roon/__init__.py b/homeassistant/components/roon/__init__.py index 9e5c38f0211..9969b694895 100644 --- a/homeassistant/components/roon/__init__.py +++ b/homeassistant/components/roon/__init__.py @@ -1,12 +1,14 @@ """Roon (www.roonlabs.com) component.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from .const import CONF_ROON_NAME, DOMAIN from .server import RoonServer +PLATFORMS = [Platform.MEDIA_PLAYER] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a roonserver from a config entry.""" @@ -28,10 +30,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: manufacturer="Roonlabs", name=f"Roon Core ({name})", ) + + # initialize media_player platform + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + return False + roonserver = hass.data[DOMAIN].pop(entry.entry_id) return await roonserver.async_reset() diff --git a/homeassistant/components/roon/server.py b/homeassistant/components/roon/server.py index df9dec3d9af..997db44583d 100644 --- a/homeassistant/components/roon/server.py +++ b/homeassistant/components/roon/server.py @@ -4,7 +4,7 @@ import logging from roonapi import RoonApi, RoonDiscovery -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, Platform +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util.dt import utcnow @@ -13,7 +13,6 @@ from .const import CONF_ROON_ID, ROON_APPINFO _LOGGER = logging.getLogger(__name__) INITIAL_SYNC_INTERVAL = 5 FULL_SYNC_INTERVAL = 30 -PLATFORMS = [Platform.MEDIA_PLAYER] class RoonServer: @@ -53,7 +52,6 @@ class RoonServer: (host, port) = get_roon_host() return RoonApi(ROON_APPINFO, token, host, port, blocking_init=True) - hass = self.hass core_id = self.config_entry.data.get(CONF_ROON_ID) self.roonapi = await self.hass.async_add_executor_job(get_roon_api) @@ -67,11 +65,6 @@ class RoonServer: core_id if core_id is not None else self.config_entry.data[CONF_HOST] ) - # initialize media_player platform - await hass.config_entries.async_forward_entry_setups( - self.config_entry, PLATFORMS - ) - # Initialize Roon background polling asyncio.create_task(self.async_do_loop()) From 08b169a7adf5c5500c5aca806652d238265a2aaf Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 29 Jul 2022 11:57:19 +0200 Subject: [PATCH 2955/3516] Fix broken Yale lock (#75918) Yale fix lock --- homeassistant/components/yale_smart_alarm/lock.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yale_smart_alarm/lock.py b/homeassistant/components/yale_smart_alarm/lock.py index a97a98a2afb..8f9ed6c9ce1 100644 --- a/homeassistant/components/yale_smart_alarm/lock.py +++ b/homeassistant/components/yale_smart_alarm/lock.py @@ -46,6 +46,7 @@ class YaleDoorlock(YaleEntity, LockEntity): """Initialize the Yale Lock Device.""" super().__init__(coordinator, data) self._attr_code_format = f"^\\d{code_format}$" + self.lock_name = data["name"] async def async_unlock(self, **kwargs: Any) -> None: """Send unlock command.""" @@ -65,7 +66,7 @@ class YaleDoorlock(YaleEntity, LockEntity): try: get_lock = await self.hass.async_add_executor_job( - self.coordinator.yale.lock_api.get, self._attr_name + self.coordinator.yale.lock_api.get, self.lock_name ) if command == "locked": lock_state = await self.hass.async_add_executor_job( From ab5dfb3c42426c184afae3c405c58817bb312076 Mon Sep 17 00:00:00 2001 From: Nephiel Date: Fri, 29 Jul 2022 12:08:32 +0200 Subject: [PATCH 2956/3516] Use climate enums in google_assistant (#75888) --- .../components/google_assistant/trait.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index ee41dd0c678..edc8ed124b3 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -864,13 +864,13 @@ class TemperatureSettingTrait(_Trait): # We do not support "on" as we are unable to know how to restore # the last mode. hvac_to_google = { - climate.HVAC_MODE_HEAT: "heat", - climate.HVAC_MODE_COOL: "cool", - climate.HVAC_MODE_OFF: "off", - climate.HVAC_MODE_AUTO: "auto", - climate.HVAC_MODE_HEAT_COOL: "heatcool", - climate.HVAC_MODE_FAN_ONLY: "fan-only", - climate.HVAC_MODE_DRY: "dry", + climate.HVACMode.HEAT: "heat", + climate.HVACMode.COOL: "cool", + climate.HVACMode.OFF: "off", + climate.HVACMode.AUTO: "auto", + climate.HVACMode.HEAT_COOL: "heatcool", + climate.HVACMode.FAN_ONLY: "fan-only", + climate.HVACMode.DRY: "dry", } google_to_hvac = {value: key for key, value in hvac_to_google.items()} @@ -949,7 +949,7 @@ class TemperatureSettingTrait(_Trait): if current_humidity is not None: response["thermostatHumidityAmbient"] = current_humidity - if operation in (climate.HVAC_MODE_AUTO, climate.HVAC_MODE_HEAT_COOL): + if operation in (climate.HVACMode.AUTO, climate.HVACMode.HEAT_COOL): if supported & climate.SUPPORT_TARGET_TEMPERATURE_RANGE: response["thermostatTemperatureSetpointHigh"] = round( temp_util.convert( From 2b1e1365fdb3bfe72feb515fcf2e02331caa4088 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 29 Jul 2022 13:09:03 +0200 Subject: [PATCH 2957/3516] Add StrEnum for device_tracker `SourceType` (#75892) Add StrEnum for device_tracker SourceType --- .../components/device_tracker/__init__.py | 1 + .../components/device_tracker/config_entry.py | 3 +- .../components/device_tracker/const.py | 16 ++++++++++ .../components/device_tracker/legacy.py | 29 +++++++++---------- .../components/mobile_app/device_tracker.py | 6 ++-- 5 files changed, 35 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 5617d56ac3f..9e58c5bbc92 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -26,6 +26,7 @@ from .const import ( # noqa: F401 SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER, + SourceType, ) from .legacy import ( # noqa: F401 PLATFORM_SCHEMA, diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index c9b8534c2bc..b587f17d58e 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -30,6 +30,7 @@ from .const import ( CONNECTED_DEVICE_REGISTERED, DOMAIN, LOGGER, + SourceType, ) @@ -187,7 +188,7 @@ class BaseTrackerEntity(Entity): return None @property - def source_type(self) -> str: + def source_type(self) -> SourceType | str: """Return the source type, eg gps or router, of the device.""" raise NotImplementedError diff --git a/homeassistant/components/device_tracker/const.py b/homeassistant/components/device_tracker/const.py index c52241ae51f..ad68472d9b0 100644 --- a/homeassistant/components/device_tracker/const.py +++ b/homeassistant/components/device_tracker/const.py @@ -1,8 +1,12 @@ """Device tracker constants.""" +from __future__ import annotations + from datetime import timedelta import logging from typing import Final +from homeassistant.backports.enum import StrEnum + LOGGER: Final = logging.getLogger(__package__) DOMAIN: Final = "device_tracker" @@ -11,11 +15,23 @@ ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" PLATFORM_TYPE_LEGACY: Final = "legacy" PLATFORM_TYPE_ENTITY: Final = "entity_platform" +# SOURCE_TYPE_* below are deprecated as of 2022.9 +# use the SourceType enum instead. SOURCE_TYPE_GPS: Final = "gps" SOURCE_TYPE_ROUTER: Final = "router" SOURCE_TYPE_BLUETOOTH: Final = "bluetooth" SOURCE_TYPE_BLUETOOTH_LE: Final = "bluetooth_le" + +class SourceType(StrEnum): + """Source type for device trackers.""" + + GPS = "gps" + ROUTER = "router" + BLUETOOTH = "bluetooth" + BLUETOOTH_LE = "bluetooth_le" + + CONF_SCAN_INTERVAL: Final = "interval_seconds" SCAN_INTERVAL: Final = timedelta(seconds=12) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index d8097a68ad5..8216c5fba27 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -66,19 +66,16 @@ from .const import ( LOGGER, PLATFORM_TYPE_LEGACY, SCAN_INTERVAL, - SOURCE_TYPE_BLUETOOTH, - SOURCE_TYPE_BLUETOOTH_LE, - SOURCE_TYPE_GPS, - SOURCE_TYPE_ROUTER, + SourceType, ) SERVICE_SEE: Final = "see" SOURCE_TYPES: Final[tuple[str, ...]] = ( - SOURCE_TYPE_GPS, - SOURCE_TYPE_ROUTER, - SOURCE_TYPE_BLUETOOTH, - SOURCE_TYPE_BLUETOOTH_LE, + SourceType.GPS, + SourceType.ROUTER, + SourceType.BLUETOOTH, + SourceType.BLUETOOTH_LE, ) NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any( @@ -137,7 +134,7 @@ class SeeCallback(Protocol): gps_accuracy: int | None = None, battery: int | None = None, attributes: dict[str, Any] | None = None, - source_type: str = SOURCE_TYPE_GPS, + source_type: SourceType | str = SourceType.GPS, picture: str | None = None, icon: str | None = None, consider_home: timedelta | None = None, @@ -158,7 +155,7 @@ class AsyncSeeCallback(Protocol): gps_accuracy: int | None = None, battery: int | None = None, attributes: dict[str, Any] | None = None, - source_type: str = SOURCE_TYPE_GPS, + source_type: SourceType | str = SourceType.GPS, picture: str | None = None, icon: str | None = None, consider_home: timedelta | None = None, @@ -412,7 +409,7 @@ def async_setup_scanner_platform( kwargs: dict[str, Any] = { "mac": mac, "host_name": host_name, - "source_type": SOURCE_TYPE_ROUTER, + "source_type": SourceType.ROUTER, "attributes": { "scanner": scanner.__class__.__name__, **extra_attributes, @@ -490,7 +487,7 @@ class DeviceTracker: gps_accuracy: int | None = None, battery: int | None = None, attributes: dict[str, Any] | None = None, - source_type: str = SOURCE_TYPE_GPS, + source_type: SourceType | str = SourceType.GPS, picture: str | None = None, icon: str | None = None, consider_home: timedelta | None = None, @@ -523,7 +520,7 @@ class DeviceTracker: gps_accuracy: int | None = None, battery: int | None = None, attributes: dict[str, Any] | None = None, - source_type: str = SOURCE_TYPE_GPS, + source_type: SourceType | str = SourceType.GPS, picture: str | None = None, icon: str | None = None, consider_home: timedelta | None = None, @@ -709,7 +706,7 @@ class Device(RestoreEntity): self._icon = icon - self.source_type: str | None = None + self.source_type: SourceType | str | None = None self._attributes: dict[str, Any] = {} @@ -762,7 +759,7 @@ class Device(RestoreEntity): gps_accuracy: int | None = None, battery: int | None = None, attributes: dict[str, Any] | None = None, - source_type: str = SOURCE_TYPE_GPS, + source_type: SourceType | str = SourceType.GPS, consider_home: timedelta | None = None, ) -> None: """Mark the device as seen.""" @@ -815,7 +812,7 @@ class Device(RestoreEntity): return if self.location_name: self._state = self.location_name - elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS: + elif self.gps is not None and self.source_type == SourceType.GPS: zone_state = zone.async_active_zone( self.hass, self.gps[0], self.gps[1], self.gps_accuracy ) diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index 0f6f0835c3b..d0f1db6caff 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -6,7 +6,7 @@ from homeassistant.components.device_tracker import ( ATTR_LOCATION_NAME, ) from homeassistant.components.device_tracker.config_entry import TrackerEntity -from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.const import SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -103,9 +103,9 @@ class MobileAppEntity(TrackerEntity, RestoreEntity): return self._entry.data[ATTR_DEVICE_NAME] @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS @property def device_info(self): From e4e36b51b6e54f2e11554433944de44ca0c30db4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jul 2022 11:14:13 -1000 Subject: [PATCH 2958/3516] Add startup timeout to bluetooth (#75848) Co-authored-by: Martin Hjelmare --- .../components/bluetooth/__init__.py | 11 +++++++- tests/components/bluetooth/test_init.py | 28 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 551e93d5bd9..d2bdb54d5ba 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -1,6 +1,7 @@ """The bluetooth integration.""" from __future__ import annotations +import asyncio from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta @@ -8,6 +9,7 @@ from enum import Enum import logging from typing import Final, Union +import async_timeout from bleak import BleakError from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData @@ -43,6 +45,7 @@ _LOGGER = logging.getLogger(__name__) UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 +START_TIMEOUT = 15 SOURCE_LOCAL: Final = "local" @@ -300,7 +303,13 @@ class BluetoothManager: self._device_detected, {} ) try: - await self.scanner.start() + async with async_timeout.timeout(START_TIMEOUT): + await self.scanner.start() + except asyncio.TimeoutError as ex: + self._cancel_device_detected() + raise ConfigEntryNotReady( + f"Timed out starting Bluetooth after {START_TIMEOUT} seconds" + ) from ex except (FileNotFoundError, BleakError) as ex: self._cancel_device_detected() raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index bb2c5f49cc9..66a9ed396ec 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -1,4 +1,5 @@ """Tests for the Bluetooth integration.""" +import asyncio from datetime import timedelta from unittest.mock import MagicMock, patch @@ -95,6 +96,33 @@ async def test_setup_and_stop_broken_bluetooth(hass, caplog): assert len(bluetooth.async_discovered_service_info(hass)) == 0 +async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): + """Test we fail gracefully when bluetooth/dbus is hanging.""" + mock_bt = [] + + async def _mock_hang(): + await asyncio.sleep(1) + + with patch.object(bluetooth, "START_TIMEOUT", 0), patch( + "homeassistant.components.bluetooth.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=_mock_hang, + ), patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "Timed out starting Bluetooth" in caplog.text + + async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): """Test we retry if the adapter is not yet available.""" mock_bt = [] From 2b1fbbfae3b55f300a892f178fa9e4b285254f2d Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 28 Jul 2022 19:10:37 +0100 Subject: [PATCH 2959/3516] Fix Xiaomi BLE not detecting encryption for some devices (#75851) --- .../components/bluetooth/__init__.py | 29 +++++ .../components/xiaomi_ble/config_flow.py | 44 ++++++- .../components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/test_init.py | 89 ++++++++++++- tests/components/xiaomi_ble/__init__.py | 12 ++ .../components/xiaomi_ble/test_config_flow.py | 123 ++++++++++++++++++ 8 files changed, 297 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index d2bdb54d5ba..eb8e31baef0 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from asyncio import Future from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta @@ -98,6 +99,9 @@ BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") BluetoothCallback = Callable[ [Union[BluetoothServiceInfoBleak, BluetoothServiceInfo], BluetoothChange], None ] +ProcessAdvertisementCallback = Callable[ + [Union[BluetoothServiceInfoBleak, BluetoothServiceInfo]], bool +] @hass_callback @@ -162,6 +166,31 @@ def async_register_callback( return manager.async_register_callback(callback, match_dict) +async def async_process_advertisements( + hass: HomeAssistant, + callback: ProcessAdvertisementCallback, + match_dict: BluetoothCallbackMatcher, + timeout: int, +) -> BluetoothServiceInfo: + """Process advertisements until callback returns true or timeout expires.""" + done: Future[BluetoothServiceInfo] = Future() + + @hass_callback + def _async_discovered_device( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + if callback(service_info): + done.set_result(service_info) + + unload = async_register_callback(hass, _async_discovered_device, match_dict) + + try: + async with async_timeout.timeout(timeout): + return await done + finally: + unload() + + @hass_callback def async_track_unavailable( hass: HomeAssistant, diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index e7c4a3e1f8c..f352f43d0bf 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Xiaomi Bluetooth integration.""" from __future__ import annotations +import asyncio import dataclasses from typing import Any @@ -12,6 +13,7 @@ from homeassistant.components import onboarding from homeassistant.components.bluetooth import ( BluetoothServiceInfo, async_discovered_service_info, + async_process_advertisements, ) from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_ADDRESS @@ -19,6 +21,9 @@ from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN +# How long to wait for additional advertisement packets if we don't have the right ones +ADDITIONAL_DISCOVERY_TIMEOUT = 5 + @dataclasses.dataclass class Discovery: @@ -44,6 +49,24 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, Discovery] = {} + async def _async_wait_for_full_advertisement( + self, discovery_info: BluetoothServiceInfo, device: DeviceData + ) -> BluetoothServiceInfo: + """Sometimes first advertisement we receive is blank or incomplete. Wait until we get a useful one.""" + if not device.pending: + return discovery_info + + def _process_more_advertisements(service_info: BluetoothServiceInfo) -> bool: + device.update(service_info) + return not device.pending + + return await async_process_advertisements( + self.hass, + _process_more_advertisements, + {"address": discovery_info.address}, + ADDITIONAL_DISCOVERY_TIMEOUT, + ) + async def async_step_bluetooth( self, discovery_info: BluetoothServiceInfo ) -> FlowResult: @@ -53,6 +76,16 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): device = DeviceData() if not device.supported(discovery_info): return self.async_abort(reason="not_supported") + + # Wait until we have received enough information about this device to detect its encryption type + try: + discovery_info = await self._async_wait_for_full_advertisement( + discovery_info, device + ) + except asyncio.TimeoutError: + # If we don't see a valid packet within the timeout then this device is not supported. + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info self._discovered_device = device @@ -161,13 +194,20 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(address, raise_on_progress=False) discovery = self._discovered_devices[address] + # Wait until we have received enough information about this device to detect its encryption type + try: + self._discovery_info = await self._async_wait_for_full_advertisement( + discovery.discovery_info, discovery.device + ) + except asyncio.TimeoutError: + # If we don't see a valid packet within the timeout then this device is not supported. + return self.async_abort(reason="not_supported") + if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: - self._discovery_info = discovery.discovery_info self.context["title_placeholders"] = {"name": discovery.title} return await self.async_step_get_encryption_key_legacy() if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_4_5: - self._discovery_info = discovery.discovery_info self.context["title_placeholders"] = {"name": discovery.title} return await self.async_step_get_encryption_key_4_5() diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 2e1a502ca69..41512291749 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.6.1"], + "requirements": ["xiaomi-ble==0.6.2"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 24a8a5ee1bd..ed487c9b84c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2470,7 +2470,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.1 +xiaomi-ble==0.6.2 # homeassistant.components.knx xknx==0.22.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81e192ce360..73b2ad3adc0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1662,7 +1662,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.1 +xiaomi-ble==0.6.2 # homeassistant.components.knx xknx==0.22.0 diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 66a9ed396ec..0664f82dbab 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -13,6 +13,7 @@ from homeassistant.components.bluetooth import ( UNAVAILABLE_TRACK_SECONDS, BluetoothChange, BluetoothServiceInfo, + async_process_advertisements, async_track_unavailable, models, ) @@ -22,7 +23,7 @@ from homeassistant.components.bluetooth.const import ( ) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -819,6 +820,92 @@ async def test_register_callback_by_address( assert service_info.manufacturer_id == 89 +async def test_process_advertisements_bail_on_good_advertisement( + hass: HomeAssistant, mock_bleak_scanner_start, enable_bluetooth +): + """Test as soon as we see a 'good' advertisement we return it.""" + done = asyncio.Future() + + def _callback(service_info: BluetoothServiceInfo) -> bool: + done.set_result(None) + return len(service_info.service_data) > 0 + + handle = hass.async_create_task( + async_process_advertisements( + hass, _callback, {"address": "aa:44:33:11:23:45"}, 5 + ) + ) + + while not done.done(): + device = BLEDevice("aa:44:33:11:23:45", "wohand") + adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51a"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fa": b"H\x10c"}, + ) + + _get_underlying_scanner()._callback(device, adv) + await asyncio.sleep(0) + + result = await handle + assert result.name == "wohand" + + +async def test_process_advertisements_ignore_bad_advertisement( + hass: HomeAssistant, mock_bleak_scanner_start, enable_bluetooth +): + """Check that we ignore bad advertisements.""" + done = asyncio.Event() + return_value = asyncio.Event() + + device = BLEDevice("aa:44:33:11:23:45", "wohand") + adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51a"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fa": b""}, + ) + + def _callback(service_info: BluetoothServiceInfo) -> bool: + done.set() + return return_value.is_set() + + handle = hass.async_create_task( + async_process_advertisements( + hass, _callback, {"address": "aa:44:33:11:23:45"}, 5 + ) + ) + + # The goal of this loop is to make sure that async_process_advertisements sees at least one + # callback that returns False + while not done.is_set(): + _get_underlying_scanner()._callback(device, adv) + await asyncio.sleep(0) + + # Set the return value and mutate the advertisement + # Check that scan ends and correct advertisement data is returned + return_value.set() + adv.service_data["00000d00-0000-1000-8000-00805f9b34fa"] = b"H\x10c" + _get_underlying_scanner()._callback(device, adv) + await asyncio.sleep(0) + + result = await handle + assert result.service_data["00000d00-0000-1000-8000-00805f9b34fa"] == b"H\x10c" + + +async def test_process_advertisements_timeout( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test we timeout if no advertisements at all.""" + + def _callback(service_info: BluetoothServiceInfo) -> bool: + return False + + with pytest.raises(asyncio.TimeoutError): + await async_process_advertisements(hass, _callback, {}, 0) + + async def test_wrapped_instance_with_filter( hass, mock_bleak_scanner_start, enable_bluetooth ): diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py index a6269a02d12..1dd1eeed65a 100644 --- a/tests/components/xiaomi_ble/__init__.py +++ b/tests/components/xiaomi_ble/__init__.py @@ -61,6 +61,18 @@ YLKG07YL_SERVICE_INFO = BluetoothServiceInfo( source="local", ) +MISSING_PAYLOAD_ENCRYPTED = BluetoothServiceInfo( + name="LYWSD02MMC", + address="A4:C1:38:56:53:84", + rssi=-56, + manufacturer_data={}, + service_data={ + "0000fe95-0000-1000-8000-00805f9b34fb": b"0X[\x05\x02\x84\x53\x568\xc1\xa4\x08", + }, + service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], + source="local", +) + def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfo: """Make a dummy advertisement.""" diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index d4b4300d2c1..b424228cc6c 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Xiaomi config flow.""" +import asyncio from unittest.mock import patch from homeassistant import config_entries @@ -9,9 +10,11 @@ from homeassistant.data_entry_flow import FlowResultType from . import ( JTYJGD03MI_SERVICE_INFO, LYWSDCGQ_SERVICE_INFO, + MISSING_PAYLOAD_ENCRYPTED, MMC_T201_1_SERVICE_INFO, NOT_SENSOR_PUSH_SERVICE_INFO, YLKG07YL_SERVICE_INFO, + make_advertisement, ) from tests.common import MockConfigEntry @@ -38,6 +41,57 @@ async def test_async_step_bluetooth_valid_device(hass): assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" +async def test_async_step_bluetooth_valid_device_but_missing_payload(hass): + """Test discovery via bluetooth with a valid device but missing payload.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + side_effect=asyncio.TimeoutError(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MISSING_PAYLOAD_ENCRYPTED, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_bluetooth_valid_device_but_missing_payload_then_full(hass): + """Test discovering a valid device. Payload is too short, but later we get full one.""" + + async def _async_process_advertisements(_hass, _callback, _matcher, _timeout): + service_info = make_advertisement( + "A4:C1:38:56:53:84", + b"XX\xe4\x16,\x84SV8\xc1\xa4+n\xf2\xe9\x12\x00\x00l\x88M\x9e", + ) + assert _callback(service_info) + return service_info + + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + _async_process_advertisements, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=MISSING_PAYLOAD_ENCRYPTED, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_4_5" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "a115210eed7a88e50ad52662e732a9fb"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["data"] == {"bindkey": "a115210eed7a88e50ad52662e732a9fb"} + assert result2["result"].unique_id == "A4:C1:38:56:53:84" + + async def test_async_step_bluetooth_during_onboarding(hass): """Test discovery via bluetooth during onboarding.""" with patch( @@ -287,6 +341,75 @@ async def test_async_step_user_with_found_devices(hass): assert result2["result"].unique_id == "58:2D:34:35:93:21" +async def test_async_step_user_short_payload(hass): + """Test setup from service info cache with devices found but short payloads.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[MISSING_PAYLOAD_ENCRYPTED], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + side_effect=asyncio.TimeoutError(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "A4:C1:38:56:53:84"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "not_supported" + + +async def test_async_step_user_short_payload_then_full(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[MISSING_PAYLOAD_ENCRYPTED], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + async def _async_process_advertisements(_hass, _callback, _matcher, _timeout): + service_info = make_advertisement( + "A4:C1:38:56:53:84", + b"XX\xe4\x16,\x84SV8\xc1\xa4+n\xf2\xe9\x12\x00\x00l\x88M\x9e", + ) + assert _callback(service_info) + return service_info + + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_process_advertisements", + _async_process_advertisements, + ): + result1 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "A4:C1:38:56:53:84"}, + ) + assert result1["type"] == FlowResultType.FORM + assert result1["step_id"] == "get_encryption_key_4_5" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "a115210eed7a88e50ad52662e732a9fb"}, + ) + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "LYWSD02MMC" + assert result2["data"] == {"bindkey": "a115210eed7a88e50ad52662e732a9fb"} + + async def test_async_step_user_with_found_devices_v4_encryption(hass): """Test setup from service info cache with devices found, with v4 encryption.""" with patch( From c469bdea75badecf2bfbf4bc43260ae0cc1fccb2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Jul 2022 22:36:11 +0200 Subject: [PATCH 2960/3516] Fix AdGuard Home rules count sensor (#75879) --- homeassistant/components/adguard/sensor.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 07a483f03c4..86104d15ef2 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -26,7 +26,7 @@ PARALLEL_UPDATES = 4 class AdGuardHomeEntityDescriptionMixin: """Mixin for required keys.""" - value_fn: Callable[[AdGuardHome], Callable[[], Coroutine[Any, Any, int | float]]] + value_fn: Callable[[AdGuardHome], Coroutine[Any, Any, int | float]] @dataclass @@ -42,56 +42,56 @@ SENSORS: tuple[AdGuardHomeEntityDescription, ...] = ( name="DNS queries", icon="mdi:magnify", native_unit_of_measurement="queries", - value_fn=lambda adguard: adguard.stats.dns_queries, + value_fn=lambda adguard: adguard.stats.dns_queries(), ), AdGuardHomeEntityDescription( key="blocked_filtering", name="DNS queries blocked", icon="mdi:magnify-close", native_unit_of_measurement="queries", - value_fn=lambda adguard: adguard.stats.blocked_filtering, + value_fn=lambda adguard: adguard.stats.blocked_filtering(), ), AdGuardHomeEntityDescription( key="blocked_percentage", name="DNS queries blocked ratio", icon="mdi:magnify-close", native_unit_of_measurement=PERCENTAGE, - value_fn=lambda adguard: adguard.stats.blocked_percentage, + value_fn=lambda adguard: adguard.stats.blocked_percentage(), ), AdGuardHomeEntityDescription( key="blocked_parental", name="Parental control blocked", icon="mdi:human-male-girl", native_unit_of_measurement="requests", - value_fn=lambda adguard: adguard.stats.replaced_parental, + value_fn=lambda adguard: adguard.stats.replaced_parental(), ), AdGuardHomeEntityDescription( key="blocked_safebrowsing", name="Safe browsing blocked", icon="mdi:shield-half-full", native_unit_of_measurement="requests", - value_fn=lambda adguard: adguard.stats.replaced_safebrowsing, + value_fn=lambda adguard: adguard.stats.replaced_safebrowsing(), ), AdGuardHomeEntityDescription( key="enforced_safesearch", name="Safe searches enforced", icon="mdi:shield-search", native_unit_of_measurement="requests", - value_fn=lambda adguard: adguard.stats.replaced_safesearch, + value_fn=lambda adguard: adguard.stats.replaced_safesearch(), ), AdGuardHomeEntityDescription( key="average_speed", name="Average processing speed", icon="mdi:speedometer", native_unit_of_measurement=TIME_MILLISECONDS, - value_fn=lambda adguard: adguard.stats.avg_processing_time, + value_fn=lambda adguard: adguard.stats.avg_processing_time(), ), AdGuardHomeEntityDescription( key="rules_count", name="Rules count", icon="mdi:counter", native_unit_of_measurement="rules", - value_fn=lambda adguard: adguard.stats.avg_processing_time, + value_fn=lambda adguard: adguard.filtering.rules_count(allowlist=False), entity_registry_enabled_default=False, ), ) @@ -144,7 +144,7 @@ class AdGuardHomeSensor(AdGuardHomeEntity, SensorEntity): async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" - value = await self.entity_description.value_fn(self.adguard)() + value = await self.entity_description.value_fn(self.adguard) self._attr_native_value = value if isinstance(value, float): self._attr_native_value = f"{value:.2f}" From 97c6c949e71a1978f87ac79abe74fd0fdd5c2418 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Jul 2022 17:07:32 -1000 Subject: [PATCH 2961/3516] Fix incorrect manufacturer_id for govee 5182 model (#75899) --- homeassistant/components/govee_ble/manifest.json | 6 +++++- homeassistant/generated/bluetooth.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index aa86215da59..270858d04d4 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -18,9 +18,13 @@ { "manufacturer_id": 14474, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + }, + { + "manufacturer_id": 10032, + "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.12.3"], + "requirements": ["govee-ble==0.12.4"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 5dde90f1f7a..8d92d6eab4a 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -34,6 +34,11 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "manufacturer_id": 14474, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" }, + { + "domain": "govee_ble", + "manufacturer_id": 10032, + "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" + }, { "domain": "homekit_controller", "manufacturer_id": 76, diff --git a/requirements_all.txt b/requirements_all.txt index ed487c9b84c..3bfd3880eee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.3 +govee-ble==0.12.4 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 73b2ad3adc0..6ce69ed9678 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,7 +561,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.3 +govee-ble==0.12.4 # homeassistant.components.gree greeclimate==1.2.0 From dfd503cc1aaf17aa7e95f6950814fec11a710dff Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Jul 2022 00:53:08 -0700 Subject: [PATCH 2962/3516] Fix Roon media player being set up before hass.data set up (#75904) --- homeassistant/components/roon/__init__.py | 11 ++++++++++- homeassistant/components/roon/server.py | 9 +-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/roon/__init__.py b/homeassistant/components/roon/__init__.py index 9e5c38f0211..9969b694895 100644 --- a/homeassistant/components/roon/__init__.py +++ b/homeassistant/components/roon/__init__.py @@ -1,12 +1,14 @@ """Roon (www.roonlabs.com) component.""" from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from .const import CONF_ROON_NAME, DOMAIN from .server import RoonServer +PLATFORMS = [Platform.MEDIA_PLAYER] + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a roonserver from a config entry.""" @@ -28,10 +30,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: manufacturer="Roonlabs", name=f"Roon Core ({name})", ) + + # initialize media_player platform + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + return False + roonserver = hass.data[DOMAIN].pop(entry.entry_id) return await roonserver.async_reset() diff --git a/homeassistant/components/roon/server.py b/homeassistant/components/roon/server.py index df9dec3d9af..997db44583d 100644 --- a/homeassistant/components/roon/server.py +++ b/homeassistant/components/roon/server.py @@ -4,7 +4,7 @@ import logging from roonapi import RoonApi, RoonDiscovery -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, Platform +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util.dt import utcnow @@ -13,7 +13,6 @@ from .const import CONF_ROON_ID, ROON_APPINFO _LOGGER = logging.getLogger(__name__) INITIAL_SYNC_INTERVAL = 5 FULL_SYNC_INTERVAL = 30 -PLATFORMS = [Platform.MEDIA_PLAYER] class RoonServer: @@ -53,7 +52,6 @@ class RoonServer: (host, port) = get_roon_host() return RoonApi(ROON_APPINFO, token, host, port, blocking_init=True) - hass = self.hass core_id = self.config_entry.data.get(CONF_ROON_ID) self.roonapi = await self.hass.async_add_executor_job(get_roon_api) @@ -67,11 +65,6 @@ class RoonServer: core_id if core_id is not None else self.config_entry.data[CONF_HOST] ) - # initialize media_player platform - await hass.config_entries.async_forward_entry_setups( - self.config_entry, PLATFORMS - ) - # Initialize Roon background polling asyncio.create_task(self.async_do_loop()) From f4defb660be0f407bb48d7f0c9fbebdc4544bb96 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 29 Jul 2022 11:57:19 +0200 Subject: [PATCH 2963/3516] Fix broken Yale lock (#75918) Yale fix lock --- homeassistant/components/yale_smart_alarm/lock.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yale_smart_alarm/lock.py b/homeassistant/components/yale_smart_alarm/lock.py index a97a98a2afb..8f9ed6c9ce1 100644 --- a/homeassistant/components/yale_smart_alarm/lock.py +++ b/homeassistant/components/yale_smart_alarm/lock.py @@ -46,6 +46,7 @@ class YaleDoorlock(YaleEntity, LockEntity): """Initialize the Yale Lock Device.""" super().__init__(coordinator, data) self._attr_code_format = f"^\\d{code_format}$" + self.lock_name = data["name"] async def async_unlock(self, **kwargs: Any) -> None: """Send unlock command.""" @@ -65,7 +66,7 @@ class YaleDoorlock(YaleEntity, LockEntity): try: get_lock = await self.hass.async_add_executor_job( - self.coordinator.yale.lock_api.get, self._attr_name + self.coordinator.yale.lock_api.get, self.lock_name ) if command == "locked": lock_state = await self.hass.async_add_executor_job( From 48b97a1f2dcaaa9483207774cfadb8f2ee820024 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Jul 2022 13:41:22 +0200 Subject: [PATCH 2964/3516] Bumped version to 2022.8.0b2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index dfa12ccd824..a3753d36d07 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index f5b35dd63dc..558f366b357 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0b1" +version = "2022.8.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From ca6676a70826c608d903aaa19166df69b558d00c Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 29 Jul 2022 13:28:39 +0100 Subject: [PATCH 2965/3516] Fix xiaomi_ble discovery for devices that don't put the fe95 uuid in service_uuids (#75923) --- homeassistant/components/xiaomi_ble/manifest.json | 2 +- homeassistant/generated/bluetooth.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 41512291749..0d97dcbedf8 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "bluetooth": [ { - "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" + "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], "requirements": ["xiaomi-ble==0.6.2"], diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 8d92d6eab4a..2cbaebb6074 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -80,6 +80,6 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ }, { "domain": "xiaomi_ble", - "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" + "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ] From 9bbbee3d8808e2a98dbd37bd878df2ce69953109 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 29 Jul 2022 18:54:49 +0200 Subject: [PATCH 2966/3516] Update xknx to 0.22.1 (#75932) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 4197cb76209..266eceaacee 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.22.0"], + "requirements": ["xknx==0.22.1"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index b3879d922db..842376bc696 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2476,7 +2476,7 @@ xboxapi==2.0.1 xiaomi-ble==0.6.2 # homeassistant.components.knx -xknx==0.22.0 +xknx==0.22.1 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0a81c1cb8d3..0d61f2d618a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1668,7 +1668,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.6.2 # homeassistant.components.knx -xknx==0.22.0 +xknx==0.22.1 # homeassistant.components.bluesound # homeassistant.components.fritz From 2798a77b5f41fe9c695b7f35f621f67e4d50c462 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Jul 2022 18:56:19 +0200 Subject: [PATCH 2967/3516] Fix SimplePush repairs issue (#75922) --- homeassistant/components/simplepush/notify.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index 358d95c770a..2e58748f323 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -43,17 +43,16 @@ async def async_get_service( discovery_info: DiscoveryInfoType | None = None, ) -> SimplePushNotificationService | None: """Get the Simplepush notification service.""" - async_create_issue( - hass, - DOMAIN, - "deprecated_yaml", - breaks_in_ha_version="2022.9.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - ) - if discovery_info is None: + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config From 879b41541559d250d5c06d8c3ac2149c1958367d Mon Sep 17 00:00:00 2001 From: Jan Stienstra <65826735+j-stienstra@users.noreply.github.com> Date: Fri, 29 Jul 2022 19:11:53 +0200 Subject: [PATCH 2968/3516] Fix incorrect check for media source (#75880) * Fix incorrect check for media source * Update homeassistant/components/jellyfin/media_source.py Co-authored-by: Franck Nijhof Co-authored-by: Paulus Schoutsen Co-authored-by: Franck Nijhof --- homeassistant/components/jellyfin/media_source.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index 879f4a4d4c8..8a09fd8d552 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -363,14 +363,18 @@ class JellyfinSource(MediaSource): def _media_mime_type(media_item: dict[str, Any]) -> str: """Return the mime type of a media item.""" - if not media_item[ITEM_KEY_MEDIA_SOURCES]: + if not media_item.get(ITEM_KEY_MEDIA_SOURCES): raise BrowseError("Unable to determine mime type for item without media source") media_source = media_item[ITEM_KEY_MEDIA_SOURCES][0] + + if MEDIA_SOURCE_KEY_PATH not in media_source: + raise BrowseError("Unable to determine mime type for media source without path") + path = media_source[MEDIA_SOURCE_KEY_PATH] mime_type, _ = mimetypes.guess_type(path) - if mime_type is not None: - return mime_type + if mime_type is None: + raise BrowseError(f"Unable to determine mime type for path {path}") - raise BrowseError(f"Unable to determine mime type for path {path}") + return mime_type From 69a0943205b7578388d37e610386f876bd26bfd1 Mon Sep 17 00:00:00 2001 From: Bob van Mierlo <38190383+bobvmierlo@users.noreply.github.com> Date: Fri, 29 Jul 2022 22:46:30 +0200 Subject: [PATCH 2969/3516] Increase the discovery timeout (#75948) --- homeassistant/components/xiaomi_ble/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index f352f43d0bf..91c7e223f1a 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -22,7 +22,7 @@ from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN # How long to wait for additional advertisement packets if we don't have the right ones -ADDITIONAL_DISCOVERY_TIMEOUT = 5 +ADDITIONAL_DISCOVERY_TIMEOUT = 60 @dataclasses.dataclass From c4ad6d46aeeeeecde26d05114b23b9559602b7ad Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 30 Jul 2022 00:22:48 +0000 Subject: [PATCH 2970/3516] [ci skip] Translation update --- .../components/airzone/translations/sv.json | 7 ++ .../components/ambee/translations/el.json | 6 ++ .../components/ambee/translations/id.json | 6 ++ .../components/ambee/translations/ja.json | 5 ++ .../components/ambee/translations/nl.json | 5 ++ .../components/ambee/translations/sv.json | 6 ++ .../ambient_station/translations/sv.json | 3 + .../components/anthemav/translations/el.json | 6 ++ .../components/anthemav/translations/id.json | 6 ++ .../components/anthemav/translations/ja.json | 5 ++ .../components/anthemav/translations/nl.json | 5 ++ .../components/anthemav/translations/pl.json | 6 ++ .../components/anthemav/translations/sv.json | 25 +++++++ .../components/awair/translations/sv.json | 7 ++ .../components/baf/translations/sv.json | 9 ++- .../components/bluetooth/translations/id.json | 12 +++- .../components/bluetooth/translations/ja.json | 12 +++- .../components/bluetooth/translations/nl.json | 32 +++++++++ .../components/bluetooth/translations/sv.json | 32 +++++++++ .../components/demo/translations/sv.json | 21 ++++++ .../derivative/translations/sv.json | 3 +- .../eight_sleep/translations/sv.json | 6 +- .../components/fan/translations/sv.json | 1 + .../components/fritz/translations/sv.json | 3 + .../components/fritzbox/translations/sv.json | 3 + .../components/generic/translations/sv.json | 28 ++++++++ .../geocaching/translations/sv.json | 4 ++ .../components/google/translations/id.json | 10 +++ .../components/google/translations/ja.json | 8 +++ .../components/google/translations/nl.json | 8 ++- .../components/google/translations/sv.json | 25 ++++++- .../components/govee_ble/translations/nl.json | 21 ++++++ .../components/govee_ble/translations/sv.json | 21 ++++++ .../components/group/translations/sv.json | 17 +++++ .../components/hassio/translations/sv.json | 7 ++ .../here_travel_time/translations/sv.json | 72 ++++++++++++++++++- .../components/hive/translations/sv.json | 7 ++ .../homeassistant/translations/sv.json | 7 ++ .../homeassistant_alerts/translations/el.json | 8 +++ .../homeassistant_alerts/translations/id.json | 8 +++ .../homeassistant_alerts/translations/ja.json | 8 +++ .../homeassistant_alerts/translations/nl.json | 8 +++ .../homeassistant_alerts/translations/ru.json | 8 +++ .../homeassistant_alerts/translations/sv.json | 8 +++ .../components/homekit/translations/id.json | 2 +- .../homekit_controller/translations/ja.json | 2 +- .../components/inkbird/translations/nl.json | 21 ++++++ .../components/inkbird/translations/sv.json | 21 ++++++ .../components/insteon/translations/sv.json | 4 ++ .../integration/translations/sv.json | 22 ++++++ .../intellifire/translations/sv.json | 12 +++- .../components/knx/translations/sv.json | 54 +++++++++++++- .../components/konnected/translations/sv.json | 1 + .../lacrosse_view/translations/el.json | 20 ++++++ .../lacrosse_view/translations/id.json | 20 ++++++ .../lacrosse_view/translations/ja.json | 20 ++++++ .../lacrosse_view/translations/nl.json | 20 ++++++ .../lacrosse_view/translations/pl.json | 20 ++++++ .../lacrosse_view/translations/sv.json | 20 ++++++ .../components/laundrify/translations/sv.json | 13 ++++ .../components/lcn/translations/sv.json | 7 ++ .../lg_soundbar/translations/sv.json | 18 +++++ .../components/life360/translations/sv.json | 25 +++++++ .../components/lifx/translations/nl.json | 19 +++++ .../components/lifx/translations/sv.json | 20 ++++++ .../litterrobot/translations/sensor.sv.json | 28 ++++++++ .../components/lyric/translations/id.json | 6 ++ .../components/lyric/translations/ja.json | 5 ++ .../components/lyric/translations/nl.json | 5 ++ .../components/lyric/translations/ru.json | 6 ++ .../components/lyric/translations/sv.json | 8 +++ .../components/meater/translations/sv.json | 7 +- .../components/miflora/translations/id.json | 8 +++ .../components/miflora/translations/ja.json | 7 ++ .../components/miflora/translations/nl.json | 7 ++ .../components/miflora/translations/ru.json | 8 +++ .../components/miflora/translations/sv.json | 8 +++ .../components/min_max/translations/sv.json | 13 ++++ .../components/mitemp_bt/translations/id.json | 8 +++ .../components/mitemp_bt/translations/nl.json | 7 ++ .../components/mitemp_bt/translations/ru.json | 8 +++ .../components/mitemp_bt/translations/sv.json | 8 +++ .../components/moat/translations/nl.json | 21 ++++++ .../components/moat/translations/sv.json | 21 ++++++ .../components/mqtt/translations/sv.json | 10 +++ .../components/nest/translations/sv.json | 29 ++++++++ .../components/nextdns/translations/sv.json | 29 ++++++++ .../components/nina/translations/sv.json | 24 +++++++ .../components/onewire/translations/sv.json | 10 +++ .../openalpr_local/translations/el.json | 8 +++ .../openalpr_local/translations/id.json | 8 +++ .../openalpr_local/translations/ja.json | 7 ++ .../openalpr_local/translations/nl.json | 7 ++ .../openalpr_local/translations/sv.json | 8 +++ .../components/overkiz/translations/sv.json | 3 + .../components/plex/translations/sv.json | 2 + .../components/plugwise/translations/sv.json | 7 +- .../components/qnap_qsw/translations/sv.json | 6 ++ .../radiotherm/translations/id.json | 6 ++ .../radiotherm/translations/ja.json | 5 ++ .../radiotherm/translations/sv.json | 18 +++++ .../components/recorder/translations/sv.json | 3 + .../components/rhasspy/translations/sv.json | 12 ++++ .../components/roon/translations/sv.json | 13 ++++ .../components/sabnzbd/translations/sv.json | 3 +- .../components/scrape/translations/sv.json | 17 +++++ .../sensibo/translations/sensor.sv.json | 3 +- .../sensorpush/translations/nl.json | 21 ++++++ .../sensorpush/translations/sv.json | 21 ++++++ .../components/senz/translations/id.json | 6 ++ .../components/senz/translations/ja.json | 5 ++ .../components/senz/translations/nl.json | 5 ++ .../components/senz/translations/ru.json | 6 ++ .../components/senz/translations/sv.json | 26 +++++++ .../components/shelly/translations/sv.json | 3 + .../simplepush/translations/el.json | 6 ++ .../simplepush/translations/id.json | 6 ++ .../simplepush/translations/ja.json | 5 ++ .../simplepush/translations/nl.json | 5 ++ .../simplepush/translations/pl.json | 6 ++ .../simplepush/translations/sv.json | 27 +++++++ .../simplisafe/translations/id.json | 7 +- .../simplisafe/translations/ja.json | 5 +- .../simplisafe/translations/sv.json | 8 ++- .../components/siren/translations/sv.json | 3 + .../components/slack/translations/sv.json | 11 ++- .../components/sms/translations/sv.json | 11 +++ .../soundtouch/translations/el.json | 6 ++ .../soundtouch/translations/id.json | 6 ++ .../soundtouch/translations/ja.json | 5 ++ .../soundtouch/translations/nl.json | 5 ++ .../soundtouch/translations/pl.json | 6 ++ .../soundtouch/translations/sv.json | 27 +++++++ .../components/spotify/translations/id.json | 6 ++ .../components/spotify/translations/ja.json | 5 ++ .../components/spotify/translations/nl.json | 5 ++ .../components/spotify/translations/ru.json | 2 +- .../components/spotify/translations/sv.json | 6 ++ .../components/sql/translations/sv.json | 11 +++ .../steam_online/translations/id.json | 6 ++ .../steam_online/translations/ja.json | 5 ++ .../steam_online/translations/nl.json | 5 ++ .../steam_online/translations/ru.json | 2 +- .../steam_online/translations/sv.json | 23 +++++- .../components/switchbot/translations/id.json | 3 +- .../components/switchbot/translations/ja.json | 1 + .../components/switchbot/translations/sv.json | 11 +++ .../tankerkoenig/translations/sv.json | 29 +++++++- .../components/tautulli/translations/sv.json | 13 +++- .../components/tod/translations/sv.json | 3 + .../tomorrowio/translations/sv.json | 3 +- .../totalconnect/translations/sv.json | 11 +++ .../trafikverket_ferry/translations/sv.json | 15 +++- .../trafikverket_train/translations/sv.json | 13 +++- .../transmission/translations/sv.json | 1 + .../ukraine_alarm/translations/sv.json | 17 +++++ .../components/unifi/translations/sv.json | 3 + .../components/uscis/translations/nl.json | 7 ++ .../components/uscis/translations/sv.json | 8 +++ .../components/verisure/translations/sv.json | 15 +++- .../components/vulcan/translations/sv.json | 43 +++++++++++ .../components/withings/translations/sv.json | 4 ++ .../components/ws66i/translations/sv.json | 31 ++++++++ .../components/xbox/translations/el.json | 6 ++ .../components/xbox/translations/id.json | 6 ++ .../components/xbox/translations/ja.json | 5 ++ .../components/xbox/translations/nl.json | 5 ++ .../components/xbox/translations/pl.json | 6 ++ .../components/xbox/translations/sv.json | 8 +++ .../xiaomi_ble/translations/id.json | 15 ++++ .../xiaomi_ble/translations/ja.json | 15 ++++ .../xiaomi_ble/translations/nl.json | 21 ++++++ .../xiaomi_ble/translations/sv.json | 36 ++++++++++ .../components/yolink/translations/sv.json | 1 + .../components/zha/translations/el.json | 2 + .../components/zha/translations/id.json | 2 + .../components/zha/translations/ja.json | 2 + .../components/zha/translations/ru.json | 2 + .../components/zha/translations/sv.json | 7 ++ .../components/zwave_js/translations/sv.json | 7 ++ 180 files changed, 1968 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/airzone/translations/sv.json create mode 100644 homeassistant/components/anthemav/translations/sv.json create mode 100644 homeassistant/components/bluetooth/translations/nl.json create mode 100644 homeassistant/components/bluetooth/translations/sv.json create mode 100644 homeassistant/components/govee_ble/translations/nl.json create mode 100644 homeassistant/components/govee_ble/translations/sv.json create mode 100644 homeassistant/components/hassio/translations/sv.json create mode 100644 homeassistant/components/homeassistant/translations/sv.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/el.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/id.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/ja.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/nl.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/ru.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/sv.json create mode 100644 homeassistant/components/inkbird/translations/nl.json create mode 100644 homeassistant/components/inkbird/translations/sv.json create mode 100644 homeassistant/components/integration/translations/sv.json create mode 100644 homeassistant/components/lacrosse_view/translations/el.json create mode 100644 homeassistant/components/lacrosse_view/translations/id.json create mode 100644 homeassistant/components/lacrosse_view/translations/ja.json create mode 100644 homeassistant/components/lacrosse_view/translations/nl.json create mode 100644 homeassistant/components/lacrosse_view/translations/pl.json create mode 100644 homeassistant/components/lacrosse_view/translations/sv.json create mode 100644 homeassistant/components/lcn/translations/sv.json create mode 100644 homeassistant/components/lg_soundbar/translations/sv.json create mode 100644 homeassistant/components/litterrobot/translations/sensor.sv.json create mode 100644 homeassistant/components/lyric/translations/sv.json create mode 100644 homeassistant/components/miflora/translations/id.json create mode 100644 homeassistant/components/miflora/translations/ja.json create mode 100644 homeassistant/components/miflora/translations/nl.json create mode 100644 homeassistant/components/miflora/translations/ru.json create mode 100644 homeassistant/components/miflora/translations/sv.json create mode 100644 homeassistant/components/mitemp_bt/translations/id.json create mode 100644 homeassistant/components/mitemp_bt/translations/nl.json create mode 100644 homeassistant/components/mitemp_bt/translations/ru.json create mode 100644 homeassistant/components/mitemp_bt/translations/sv.json create mode 100644 homeassistant/components/moat/translations/nl.json create mode 100644 homeassistant/components/moat/translations/sv.json create mode 100644 homeassistant/components/nextdns/translations/sv.json create mode 100644 homeassistant/components/nina/translations/sv.json create mode 100644 homeassistant/components/openalpr_local/translations/el.json create mode 100644 homeassistant/components/openalpr_local/translations/id.json create mode 100644 homeassistant/components/openalpr_local/translations/ja.json create mode 100644 homeassistant/components/openalpr_local/translations/nl.json create mode 100644 homeassistant/components/openalpr_local/translations/sv.json create mode 100644 homeassistant/components/rhasspy/translations/sv.json create mode 100644 homeassistant/components/roon/translations/sv.json create mode 100644 homeassistant/components/sensorpush/translations/nl.json create mode 100644 homeassistant/components/sensorpush/translations/sv.json create mode 100644 homeassistant/components/senz/translations/sv.json create mode 100644 homeassistant/components/simplepush/translations/sv.json create mode 100644 homeassistant/components/siren/translations/sv.json create mode 100644 homeassistant/components/sms/translations/sv.json create mode 100644 homeassistant/components/soundtouch/translations/sv.json create mode 100644 homeassistant/components/switchbot/translations/sv.json create mode 100644 homeassistant/components/tod/translations/sv.json create mode 100644 homeassistant/components/uscis/translations/nl.json create mode 100644 homeassistant/components/uscis/translations/sv.json create mode 100644 homeassistant/components/vulcan/translations/sv.json create mode 100644 homeassistant/components/ws66i/translations/sv.json create mode 100644 homeassistant/components/xbox/translations/sv.json create mode 100644 homeassistant/components/xiaomi_ble/translations/nl.json create mode 100644 homeassistant/components/xiaomi_ble/translations/sv.json diff --git a/homeassistant/components/airzone/translations/sv.json b/homeassistant/components/airzone/translations/sv.json new file mode 100644 index 00000000000..daa1cb38499 --- /dev/null +++ b/homeassistant/components/airzone/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_system_id": "Ogiltigt Airzone System ID" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/el.json b/homeassistant/components/ambee/translations/el.json index 3576b6cd852..99198a39817 100644 --- a/homeassistant/components/ambee/translations/el.json +++ b/homeassistant/components/ambee/translations/el.json @@ -24,5 +24,11 @@ "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf Ambee \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03c9\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Ambee \u03b5\u03ba\u03ba\u03c1\u03b5\u03bc\u03b5\u03af \u03ba\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant 2022.10. \n\n \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9, \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03b7 Ambee \u03b1\u03c6\u03b1\u03af\u03c1\u03b5\u03c3\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03b4\u03c9\u03c1\u03b5\u03ac\u03bd (\u03c0\u03b5\u03c1\u03b9\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c5\u03c2) \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd\u03c2 \u03c4\u03bf\u03c5\u03c2 \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03c3\u03c4\u03bf\u03c5\u03c2 \u03c4\u03b1\u03ba\u03c4\u03b9\u03ba\u03bf\u03cd\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2 \u03bd\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03bf\u03cd\u03bd \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03b5\u03c0\u03af \u03c0\u03bb\u03b7\u03c1\u03c9\u03bc\u03ae. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 Ambee \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03b6\u03ae\u03c4\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Ambee \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/id.json b/homeassistant/components/ambee/translations/id.json index a5790d95ecd..686e36fd17b 100644 --- a/homeassistant/components/ambee/translations/id.json +++ b/homeassistant/components/ambee/translations/id.json @@ -24,5 +24,11 @@ "description": "Siapkan Ambee Anda untuk diintegrasikan dengan Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "Integrasi Ambee sedang menunggu penghapusan dari Home Assistant dan tidak akan lagi tersedia pada Home Assistant 2022.10.\n\nIntegrasi ini dalam proses penghapusan, karena Ambee telah menghapus akun versi gratis (terbatas) mereka dan tidak menyediakan cara bagi pengguna biasa untuk mendaftar paket berbayar lagi.\n\nHapus entri integrasi Ambee dari instans Anda untuk memperbaiki masalah ini.", + "title": "Integrasi Ambee dalam proses penghapusan" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/ja.json b/homeassistant/components/ambee/translations/ja.json index e320189e010..e4502068d69 100644 --- a/homeassistant/components/ambee/translations/ja.json +++ b/homeassistant/components/ambee/translations/ja.json @@ -24,5 +24,10 @@ "description": "Ambee \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" } } + }, + "issues": { + "pending_removal": { + "title": "Ambee\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/nl.json b/homeassistant/components/ambee/translations/nl.json index 5d356037652..957c3547be2 100644 --- a/homeassistant/components/ambee/translations/nl.json +++ b/homeassistant/components/ambee/translations/nl.json @@ -24,5 +24,10 @@ "description": "Stel Ambee in om te integreren met Home Assistant." } } + }, + "issues": { + "pending_removal": { + "title": "De Ambee-integratie wordt verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sv.json b/homeassistant/components/ambee/translations/sv.json index 5ad5b5b6db4..a8147496b74 100644 --- a/homeassistant/components/ambee/translations/sv.json +++ b/homeassistant/components/ambee/translations/sv.json @@ -12,5 +12,11 @@ } } } + }, + "issues": { + "pending_removal": { + "description": "Ambee-integrationen v\u00e4ntar p\u00e5 borttagning fr\u00e5n Home Assistant och kommer inte l\u00e4ngre att vara tillg\u00e4nglig fr\u00e5n och med Home Assistant 2022.10. \n\n Integrationen tas bort eftersom Ambee tog bort sina gratis (begr\u00e4nsade) konton och inte l\u00e4ngre ger vanliga anv\u00e4ndare m\u00f6jlighet att registrera sig f\u00f6r en betalplan. \n\n Ta bort Ambee-integreringsposten fr\u00e5n din instans f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Ambee-integrationen tas bort" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambient_station/translations/sv.json b/homeassistant/components/ambient_station/translations/sv.json index 7c6be84d594..35ff795627e 100644 --- a/homeassistant/components/ambient_station/translations/sv.json +++ b/homeassistant/components/ambient_station/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, "error": { "invalid_key": "Ogiltigt API-nyckel och/eller applikationsnyckel", "no_devices": "Inga enheter hittades i kontot" diff --git a/homeassistant/components/anthemav/translations/el.json b/homeassistant/components/anthemav/translations/el.json index 983e89155e8..5da08413782 100644 --- a/homeassistant/components/anthemav/translations/el.json +++ b/homeassistant/components/anthemav/translations/el.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03c4\u03ce\u03bd Anthem A/V \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03bf\u03c5 Anthem A/V Receivers \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03bf\u03c5 Anthem A/V Receivers \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/id.json b/homeassistant/components/anthemav/translations/id.json index 1eb2ba0b5a1..8c7e40b4c0b 100644 --- a/homeassistant/components/anthemav/translations/id.json +++ b/homeassistant/components/anthemav/translations/id.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Receiver Anthem A/V lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Receiver Anthem A/V dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Anthem A/V Receiver dalam proses penghapusan" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/ja.json b/homeassistant/components/anthemav/translations/ja.json index 8c87d02e557..b55e8b2b030 100644 --- a/homeassistant/components/anthemav/translations/ja.json +++ b/homeassistant/components/anthemav/translations/ja.json @@ -15,5 +15,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "Anthem A/V Receivers YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/nl.json b/homeassistant/components/anthemav/translations/nl.json index c09dde1bfc3..8754427ea61 100644 --- a/homeassistant/components/anthemav/translations/nl.json +++ b/homeassistant/components/anthemav/translations/nl.json @@ -14,5 +14,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "De Anthem A/V Receivers YAML-configuratie wordt verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/pl.json b/homeassistant/components/anthemav/translations/pl.json index 18eaecb9845..ca40384e6f0 100644 --- a/homeassistant/components/anthemav/translations/pl.json +++ b/homeassistant/components/anthemav/translations/pl.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Anthem A/V Receivers przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Anthem A/V Receivers zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/sv.json b/homeassistant/components/anthemav/translations/sv.json new file mode 100644 index 00000000000..dd3f6f891e2 --- /dev/null +++ b/homeassistant/components/anthemav/translations/sv.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "cannot_receive_deviceinfo": "Det gick inte att h\u00e4mta MAC-adress. Se till att enheten \u00e4r p\u00e5slagen" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Anthem A/V-mottagare med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Anthem A/V Receivers YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Anthem A/V-mottagarens YAML-konfiguration tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/sv.json b/homeassistant/components/awair/translations/sv.json index 1fda5b91f5a..a7dd53ffad4 100644 --- a/homeassistant/components/awair/translations/sv.json +++ b/homeassistant/components/awair/translations/sv.json @@ -1,6 +1,13 @@ { "config": { "step": { + "reauth_confirm": { + "data": { + "access_token": "\u00c5tkomsttoken", + "email": "Epost" + }, + "description": "Ange din Awair-utvecklar\u00e5tkomsttoken igen." + }, "user": { "data": { "access_token": "\u00c5tkomstnyckel" diff --git a/homeassistant/components/baf/translations/sv.json b/homeassistant/components/baf/translations/sv.json index e3270d4036a..0e346a0f72a 100644 --- a/homeassistant/components/baf/translations/sv.json +++ b/homeassistant/components/baf/translations/sv.json @@ -1,11 +1,18 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "ipv6_not_supported": "IPv6 st\u00f6ds inte." }, "error": { "cannot_connect": "Det gick inte att ansluta.", "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name} - {model} ( {ip_address} )", + "step": { + "discovery_confirm": { + "description": "Vill du st\u00e4lla in {name} - {model} ( {ip_address} )?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/id.json b/homeassistant/components/bluetooth/translations/id.json index 5fe99b68c2f..3fc2d6a7623 100644 --- a/homeassistant/components/bluetooth/translations/id.json +++ b/homeassistant/components/bluetooth/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Layanan sudah dikonfigurasi" + "already_configured": "Layanan sudah dikonfigurasi", + "no_adapters": "Tidak ada adaptor Bluetooth yang ditemukan" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "Pilih perangkat untuk disiapkan" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Adaptor Bluetooth yang digunakan untuk pemindaian" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/ja.json b/homeassistant/components/bluetooth/translations/ja.json index 6257d1c67d4..b3f6b794ee9 100644 --- a/homeassistant/components/bluetooth/translations/ja.json +++ b/homeassistant/components/bluetooth/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "no_adapters": "Bluetooth\u30a2\u30c0\u30d7\u30bf\u30fc\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "\u30b9\u30ad\u30e3\u30f3\u306b\u4f7f\u7528\u3059\u308bBluetooth\u30a2\u30c0\u30d7\u30bf\u30fc" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/nl.json b/homeassistant/components/bluetooth/translations/nl.json new file mode 100644 index 00000000000..9a0e95df8bc --- /dev/null +++ b/homeassistant/components/bluetooth/translations/nl.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Dienst is al geconfigureerd", + "no_adapters": "Geen Bluetooth-adapters gevonden" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "enable_bluetooth": { + "description": "Wilt u Bluetooth instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "De Bluetooth-adapter die gebruikt moet worden voor het scannen." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/sv.json b/homeassistant/components/bluetooth/translations/sv.json new file mode 100644 index 00000000000..fe07d338101 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/sv.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "no_adapters": "Inga Bluetooth-adaptrar hittades" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du konfigurera {name}?" + }, + "enable_bluetooth": { + "description": "Vill du s\u00e4tta upp Bluetooth?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att konfigurera" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Bluetooth-adaptern som ska anv\u00e4ndas f\u00f6r skanning" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/sv.json b/homeassistant/components/demo/translations/sv.json index b577b02e25b..55872e8c81e 100644 --- a/homeassistant/components/demo/translations/sv.json +++ b/homeassistant/components/demo/translations/sv.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Tryck p\u00e5 OK n\u00e4r blinkersv\u00e4tska har fyllts p\u00e5", + "title": "Blinkerv\u00e4tska m\u00e5ste fyllas p\u00e5" + } + } + }, + "title": "Blinkersv\u00e4tskan \u00e4r tom och m\u00e5ste fyllas p\u00e5" + }, + "transmogrifier_deprecated": { + "description": "Transmogrifier-komponenten \u00e4r nu utfasad p\u00e5 grund av bristen p\u00e5 lokal kontroll tillg\u00e4nglig i det nya API:et", + "title": "Transmogrifier-komponenten \u00e4r utfasad" + }, + "unfixable_problem": { + "description": "Det h\u00e4r problemet kommer aldrig att ge upp.", + "title": "Detta \u00e4r inte ett problem som g\u00e5r att fixa" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/derivative/translations/sv.json b/homeassistant/components/derivative/translations/sv.json index ac5b766d124..0f36236b1ae 100644 --- a/homeassistant/components/derivative/translations/sv.json +++ b/homeassistant/components/derivative/translations/sv.json @@ -28,7 +28,8 @@ "unit_time": "Tidsenhet" }, "data_description": { - "round": "Anger antal decimaler i resultatet." + "round": "Anger antal decimaler i resultatet.", + "unit_prefix": "." } } } diff --git a/homeassistant/components/eight_sleep/translations/sv.json b/homeassistant/components/eight_sleep/translations/sv.json index af5c7e7fe8d..cc8fb8e5149 100644 --- a/homeassistant/components/eight_sleep/translations/sv.json +++ b/homeassistant/components/eight_sleep/translations/sv.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Kan inte ansluta till Eight Sleep-molnet: {error}" + }, + "error": { + "cannot_connect": "Kan inte ansluta till Eight Sleep-molnet: {error}" }, "step": { "user": { diff --git a/homeassistant/components/fan/translations/sv.json b/homeassistant/components/fan/translations/sv.json index dd1aaad4052..31df690b766 100644 --- a/homeassistant/components/fan/translations/sv.json +++ b/homeassistant/components/fan/translations/sv.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "V\u00e4xla {entity_name}", "turn_off": "St\u00e4ng av {entity_name}", "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" }, diff --git a/homeassistant/components/fritz/translations/sv.json b/homeassistant/components/fritz/translations/sv.json index 02cd1e39b0e..89c6cb84221 100644 --- a/homeassistant/components/fritz/translations/sv.json +++ b/homeassistant/components/fritz/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "ignore_ip6_link_local": "IPv6-l\u00e4nkens lokala adress st\u00f6ds inte." + }, "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox/translations/sv.json b/homeassistant/components/fritzbox/translations/sv.json index b611b3a9893..347aeeecc4a 100644 --- a/homeassistant/components/fritzbox/translations/sv.json +++ b/homeassistant/components/fritzbox/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "ignore_ip6_link_local": "IPv6-l\u00e4nkens lokala adress st\u00f6ds inte." + }, "step": { "confirm": { "data": { diff --git a/homeassistant/components/generic/translations/sv.json b/homeassistant/components/generic/translations/sv.json index 62b30963a50..020b0093092 100644 --- a/homeassistant/components/generic/translations/sv.json +++ b/homeassistant/components/generic/translations/sv.json @@ -3,21 +3,49 @@ "abort": { "no_devices_found": "Inga enheter hittades i n\u00e4tverket" }, + "error": { + "malformed_url": "Ogiltig URL", + "relative_url": "Relativa URL:er \u00e4r inte till\u00e5tna", + "template_error": "Problem att rendera mall. Kolla i loggen f\u00f6r mer information." + }, "step": { + "content_type": { + "data": { + "content_type": "Inneh\u00e5llstyp" + }, + "description": "Ange inneh\u00e5llstypen f\u00f6r str\u00f6mmen." + }, "user": { "data": { "authentication": "Autentiseringen", + "password": "L\u00f6senord", + "rtsp_transport": "RTSP transportprotokoll", "username": "Anv\u00e4ndarnamn" } } } }, "options": { + "error": { + "malformed_url": "Ogiltig URL", + "relative_url": "Relativa URL:er \u00e4r inte till\u00e5tet", + "template_error": "Problem att rendera mall. Kolla i loggen f\u00f6r mer information." + }, "step": { + "content_type": { + "data": { + "content_type": "Inneh\u00e5llstyp" + }, + "description": "Ange tyen av inneh\u00e5ll f\u00f6r str\u00f6mmen" + }, "init": { "data": { "authentication": "Autentiseringen", + "use_wallclock_as_timestamps": "Anv\u00e4nd v\u00e4ggklocka som tidsst\u00e4mplar", "username": "Anv\u00e4ndarnamn" + }, + "data_description": { + "use_wallclock_as_timestamps": "Det h\u00e4r alternativet kan korrigera segmenteringsproblem eller kraschproblem som uppst\u00e5r p\u00e5 grund av felaktig implementering av tidsst\u00e4mplar p\u00e5 vissa kameror." } } } diff --git a/homeassistant/components/geocaching/translations/sv.json b/homeassistant/components/geocaching/translations/sv.json index d8622ba37fb..bf674fb43f1 100644 --- a/homeassistant/components/geocaching/translations/sv.json +++ b/homeassistant/components/geocaching/translations/sv.json @@ -1,6 +1,9 @@ { "config": { "abort": { + "already_configured": "Konto har redan konfigurerats", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", "oauth_error": "Mottog ogiltiga tokendata.", @@ -14,6 +17,7 @@ "title": "V\u00e4lj autentiseringsmetod" }, "reauth_confirm": { + "description": "Geocaching-integrationen m\u00e5ste autentisera ditt konto igen", "title": "\u00c5terautenticera integration" } } diff --git a/homeassistant/components/google/translations/id.json b/homeassistant/components/google/translations/id.json index 20ed21a56be..6de37cee947 100644 --- a/homeassistant/components/google/translations/id.json +++ b/homeassistant/components/google/translations/id.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Google Kalender di configuration.yaml dalam proses penghapusan di Home Assistant 2022.9.\n\nKredensial Aplikasi OAuth yang Anda dan setelan akses telah diimpor ke antarmuka secara otomatis. Hapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Google Kalender dalam proses penghapusan" + }, + "removed_track_new_yaml": { + "description": "Anda telah menonaktifkan pelacakan entitas untuk Google Kalender di configuration.yaml, yang kini tidak lagi didukung. Anda harus secara manual mengubah Opsi Sistem integrasi di antarmuka untuk menonaktifkan entitas yang baru ditemukan di masa datang. Hapus pengaturan track_new dari configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Pelacakan entitas Google Kalender telah berubah" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/ja.json b/homeassistant/components/google/translations/ja.json index 6e2aac00c5d..9507f32812d 100644 --- a/homeassistant/components/google/translations/ja.json +++ b/homeassistant/components/google/translations/ja.json @@ -33,6 +33,14 @@ } } }, + "issues": { + "deprecated_yaml": { + "title": "Google\u30ab\u30ec\u30f3\u30c0\u30fcyaml\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "removed_track_new_yaml": { + "title": "Google\u30ab\u30ec\u30f3\u30c0\u30fc\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30c8\u30e9\u30c3\u30ad\u30f3\u30b0\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/nl.json b/homeassistant/components/google/translations/nl.json index 2f8d67af1e2..572ac985041 100644 --- a/homeassistant/components/google/translations/nl.json +++ b/homeassistant/components/google/translations/nl.json @@ -8,7 +8,8 @@ "invalid_access_token": "Ongeldig toegangstoken", "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", "oauth_error": "Ongeldige tokengegevens ontvangen.", - "reauth_successful": "Herauthenticatie geslaagd" + "reauth_successful": "Herauthenticatie geslaagd", + "timeout_connect": "Time-out bij het maken van verbinding" }, "create_entry": { "default": "Authenticatie geslaagd" @@ -29,6 +30,11 @@ } } }, + "issues": { + "deprecated_yaml": { + "title": "De Google Calendar YAML-configuratie wordt verwijderd" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/sv.json b/homeassistant/components/google/translations/sv.json index 7f1f140af90..d1e92e9b8a0 100644 --- a/homeassistant/components/google/translations/sv.json +++ b/homeassistant/components/google/translations/sv.json @@ -1,8 +1,31 @@ { + "application_credentials": { + "description": "F\u00f6lj [instruktionerna]( {more_info_url} ) f\u00f6r [OAuth-samtyckessk\u00e4rmen]( {oauth_consent_url} ) f\u00f6r att ge Home Assistant \u00e5tkomst till din Google-kalender. Du m\u00e5ste ocks\u00e5 skapa applikationsuppgifter kopplade till din kalender:\n 1. G\u00e5 till [Inloggningsuppgifter]( {oauth_creds_url} ) och klicka p\u00e5 **Skapa inloggningsuppgifter**.\n 1. V\u00e4lj **OAuth-klient-ID** i rullgardinsmenyn.\n 1. V\u00e4lj **TV och begr\u00e4nsade ing\u00e5ngsenheter** f\u00f6r applikationstyp. \n\n" + }, "config": { "abort": { "already_configured": "Konto har redan konfigurerats", - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "timeout_connect": "Timeout vid anslutningsf\u00f6rs\u00f6k" + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Google Kalender i configuration.yaml tas bort i Home Assistant 2022.9. \n\n Dina befintliga OAuth-applikationsuppgifter och \u00e5tkomstinst\u00e4llningar har importerats till anv\u00e4ndargr\u00e4nssnittet automatiskt. Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Google Kalender YAML-konfigurationen tas bort" + }, + "removed_track_new_yaml": { + "description": "Du har inaktiverat enhetssp\u00e5rning f\u00f6r Google Kalender i configuration.yaml, som inte l\u00e4ngre st\u00f6ds. Du m\u00e5ste manuellt \u00e4ndra integrationssystemalternativen i anv\u00e4ndargr\u00e4nssnittet f\u00f6r att inaktivera nyuppt\u00e4ckta enheter fram\u00f6ver. Ta bort inst\u00e4llningen track_new fr\u00e5n configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Sp\u00e5rning av enheter i Google Kalender har \u00e4ndrats" + } + }, + "options": { + "step": { + "init": { + "data": { + "calendar_access": "Home Assistant-\u00e5tkomst till Google Kalender" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/nl.json b/homeassistant/components/govee_ble/translations/nl.json new file mode 100644 index 00000000000..a46f954fe5f --- /dev/null +++ b/homeassistant/components/govee_ble/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/sv.json b/homeassistant/components/govee_ble/translations/sv.json new file mode 100644 index 00000000000..911ef1b168f --- /dev/null +++ b/homeassistant/components/govee_ble/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enhet \u00e4r redan konfigurerad", + "already_in_progress": "Konfiguration redan ig\u00e5ng", + "no_devices_found": "Inga enheter hittades p\u00e5 n\u00e4tverket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du s\u00e4tta upp {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att konfigurera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/sv.json b/homeassistant/components/group/translations/sv.json index c62d9e18ffc..8ba098e02cd 100644 --- a/homeassistant/components/group/translations/sv.json +++ b/homeassistant/components/group/translations/sv.json @@ -12,7 +12,18 @@ "light": { "title": "L\u00e4gg till grupp" }, + "lock": { + "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar", + "name": "Namn" + }, + "title": "L\u00e4gg till grupp" + }, "user": { + "menu_options": { + "lock": "L\u00e5sgrupp" + }, "title": "L\u00e4gg till grupp" } } @@ -23,6 +34,12 @@ "data": { "all": "Alla entiteter" } + }, + "lock": { + "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar" + } } } }, diff --git a/homeassistant/components/hassio/translations/sv.json b/homeassistant/components/hassio/translations/sv.json new file mode 100644 index 00000000000..7d3d7684558 --- /dev/null +++ b/homeassistant/components/hassio/translations/sv.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "agent_version": "Agentversion" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/sv.json b/homeassistant/components/here_travel_time/translations/sv.json index 0757cc44bf1..bb0f36a448f 100644 --- a/homeassistant/components/here_travel_time/translations/sv.json +++ b/homeassistant/components/here_travel_time/translations/sv.json @@ -8,11 +8,81 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "destination_coordinates": { + "data": { + "destination": "Destination som GPS-koordinater" + }, + "title": "V\u00e4lj destination" + }, + "destination_entity_id": { + "data": { + "destination_entity_id": "Destination med hj\u00e4lp av en enhet" + }, + "title": "V\u00e4lj destination" + }, + "destination_menu": { + "menu_options": { + "destination_coordinates": "Anv\u00e4nda en kartplats", + "destination_entity": "Anv\u00e4nda en enhet" + }, + "title": "V\u00e4lj destination" + }, + "origin_coordinates": { + "data": { + "origin": "Ursprung som GPS-koordinater" + }, + "title": "V\u00e4lj ursprung" + }, + "origin_entity_id": { + "data": { + "origin_entity_id": "Ursprung med hj\u00e4lp av en enhet" + }, + "title": "V\u00e4lj ursprung" + }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Anv\u00e4nda en kartplats", + "origin_entity": "Anv\u00e4nda en enhet" + }, + "title": "V\u00e4lj ursprung" + }, "user": { "data": { - "api_key": "API-nyckel" + "api_key": "API-nyckel", + "mode": "Resel\u00e4ge" } } } + }, + "options": { + "step": { + "arrival_time": { + "data": { + "arrival_time": "Ankomsttid" + }, + "title": "V\u00e4lj ankomsttid" + }, + "departure_time": { + "data": { + "departure_time": "Avg\u00e5ngstid" + }, + "title": "V\u00e4lj avg\u00e5ngstid" + }, + "init": { + "data": { + "route_mode": "Ruttl\u00e4ge", + "traffic_mode": "Trafikl\u00e4ge", + "unit_system": "Enhetssystem" + } + }, + "time_menu": { + "menu_options": { + "arrival_time": "Konfigurera en ankomsttid", + "departure_time": "Konfigurera en avg\u00e5ngstid", + "no_time": "Konfigurera inte en tid" + }, + "title": "V\u00e4lj tidstyp" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hive/translations/sv.json b/homeassistant/components/hive/translations/sv.json index 6d76a51e90b..60b51beb78b 100644 --- a/homeassistant/components/hive/translations/sv.json +++ b/homeassistant/components/hive/translations/sv.json @@ -4,6 +4,13 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "configuration": { + "data": { + "device_name": "Enhetsnamn" + }, + "description": "Ange din Hive-konfiguration", + "title": "Hive-konfiguration." + }, "reauth": { "data": { "password": "L\u00f6senord", diff --git a/homeassistant/components/homeassistant/translations/sv.json b/homeassistant/components/homeassistant/translations/sv.json new file mode 100644 index 00000000000..e4778a3a9e0 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/sv.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "config_dir": "Konfigurationskatalog" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/el.json b/homeassistant/components/homeassistant_alerts/translations/el.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/el.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/id.json b/homeassistant/components/homeassistant_alerts/translations/id.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/id.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/ja.json b/homeassistant/components/homeassistant_alerts/translations/ja.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/ja.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/nl.json b/homeassistant/components/homeassistant_alerts/translations/nl.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/nl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/ru.json b/homeassistant/components/homeassistant_alerts/translations/ru.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/ru.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/sv.json b/homeassistant/components/homeassistant_alerts/translations/sv.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/sv.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/id.json b/homeassistant/components/homekit/translations/id.json index e3889ce031f..eca3dcdd173 100644 --- a/homeassistant/components/homekit/translations/id.json +++ b/homeassistant/components/homekit/translations/id.json @@ -60,7 +60,7 @@ "include_exclude_mode": "Mode Penyertaan", "mode": "Mode HomeKit" }, - "description": "HomeKit dapat dikonfigurasi untuk memaparkakan sebuah bridge atau sebuah aksesori. Dalam mode aksesori, hanya satu entitas yang dapat digunakan. Mode aksesori diperlukan agar pemutar media dengan kelas perangkat TV berfungsi dengan baik. Entitas di \"Domain yang akan disertakan\" akan disertakan ke HomeKit. Anda akan dapat memilih entitas mana yang akan disertakan atau dikecualikan dari daftar ini pada layar berikutnya.", + "description": "HomeKit dapat dikonfigurasi untuk memaparkan sebuah bridge atau sebuah aksesori. Dalam mode aksesori, hanya satu entitas yang dapat digunakan. Mode aksesori diperlukan agar pemutar media dengan kelas perangkat TV berfungsi dengan baik. Entitas di \"Domain yang akan disertakan\" akan disertakan ke HomeKit. Anda akan dapat memilih entitas mana yang akan disertakan atau dikecualikan dari daftar ini pada layar berikutnya.", "title": "Pilih mode dan domain." }, "yaml": { diff --git a/homeassistant/components/homekit_controller/translations/ja.json b/homeassistant/components/homekit_controller/translations/ja.json index fbac8823897..3d013323fe3 100644 --- a/homeassistant/components/homekit_controller/translations/ja.json +++ b/homeassistant/components/homekit_controller/translations/ja.json @@ -11,7 +11,7 @@ "no_devices": "\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u306a\u3044\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f" }, "error": { - "authentication_error": "HomeKit\u30b3\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002\u78ba\u8a8d\u3057\u3066\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "authentication_error": "HomeKit\u30b3\u30fc\u30c9\u304c\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002\u78ba\u8a8d\u306e\u4e0a\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "insecure_setup_code": "\u8981\u6c42\u3055\u308c\u305f\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u30b3\u30fc\u30c9\u306f\u3001\u5358\u7d14\u3059\u304e\u308b\u306e\u3067\u5b89\u5168\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a2\u30af\u30bb\u30b5\u30ea\u306f\u3001\u57fa\u672c\u7684\u306a\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u8981\u4ef6\u3092\u6e80\u305f\u3057\u3066\u3044\u307e\u305b\u3093\u3002", "max_peers_error": "\u30c7\u30d0\u30a4\u30b9\u306b\u306f\u7121\u6599\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u30b9\u30c8\u30ec\u30fc\u30b8\u304c\u306a\u3044\u305f\u3081\u3001\u30da\u30a2\u30ea\u30f3\u30b0\u306e\u8ffd\u52a0\u3092\u62d2\u5426\u3057\u307e\u3057\u305f\u3002", "pairing_failed": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u3068\u306e\u30da\u30a2\u30ea\u30f3\u30b0\u4e2d\u306b\u3001\u672a\u51e6\u7406\u306e\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002\u3053\u308c\u306f\u4e00\u6642\u7684\u306a\u969c\u5bb3\u304b\u3001\u30c7\u30d0\u30a4\u30b9\u304c\u73fe\u5728\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002", diff --git a/homeassistant/components/inkbird/translations/nl.json b/homeassistant/components/inkbird/translations/nl.json new file mode 100644 index 00000000000..a46f954fe5f --- /dev/null +++ b/homeassistant/components/inkbird/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/sv.json b/homeassistant/components/inkbird/translations/sv.json new file mode 100644 index 00000000000..1b04e08e2f7 --- /dev/null +++ b/homeassistant/components/inkbird/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationen \u00e4r redan ig\u00e5ng", + "no_devices_found": "Inga enheter hittades p\u00e5 n\u00e4tverket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du s\u00e4tta upp {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att s\u00e4tta upp" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/sv.json b/homeassistant/components/insteon/translations/sv.json index 4992e704f9e..6f3f3666b12 100644 --- a/homeassistant/components/insteon/translations/sv.json +++ b/homeassistant/components/insteon/translations/sv.json @@ -3,7 +3,11 @@ "abort": { "not_insteon_device": "Uppt\u00e4ckt enhet \u00e4r inte en Insteon-enhet" }, + "flow_title": "{name}", "step": { + "confirm_usb": { + "description": "Vill du konfigurera {name}?" + }, "hubv2": { "data": { "username": "Anv\u00e4ndarnamn" diff --git a/homeassistant/components/integration/translations/sv.json b/homeassistant/components/integration/translations/sv.json new file mode 100644 index 00000000000..4665a9b8816 --- /dev/null +++ b/homeassistant/components/integration/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "data_description": { + "round": "Anger antal decimaler i resultatet.", + "unit_prefix": "Utdata kommer att skalas enligt det valda metriska prefixet.", + "unit_time": "Utg\u00e5ngen kommer att skalas enligt den valda tidsenheten." + } + } + } + }, + "options": { + "step": { + "init": { + "data_description": { + "round": "Anger antal decimaler i resultatet." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/intellifire/translations/sv.json b/homeassistant/components/intellifire/translations/sv.json index afffc97862b..05685de670b 100644 --- a/homeassistant/components/intellifire/translations/sv.json +++ b/homeassistant/components/intellifire/translations/sv.json @@ -1,16 +1,24 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "Omautentiseringen lyckades" }, "error": { - "cannot_connect": "Det gick inte att ansluta." + "api_error": "Inloggningen misslyckades", + "cannot_connect": "Det gick inte att ansluta.", + "iftapi_connect": "Fel vid anslutning till iftapi.net" }, "step": { "api_config": { "data": { + "password": "L\u00f6senord", "username": "E-postadress" } + }, + "pick_device": { + "description": "F\u00f6ljande IntelliFire-enheter uppt\u00e4cktes. V\u00e4lj vilken du vill konfigurera.", + "title": "Val av enhet" } } } diff --git a/homeassistant/components/knx/translations/sv.json b/homeassistant/components/knx/translations/sv.json index de5def120c9..7c4d6ee8e61 100644 --- a/homeassistant/components/knx/translations/sv.json +++ b/homeassistant/components/knx/translations/sv.json @@ -1,19 +1,71 @@ { "config": { "error": { + "file_not_found": "Den angivna `.knxkeys`-filen hittades inte i s\u00f6kv\u00e4gen config/.storage/knx/", "invalid_individual_address": "V\u00e4rdet matchar inte m\u00f6nstret f\u00f6r en individuell adress i KNX.\n'area.line.device'", - "invalid_ip_address": "Ogiltig IPv4-adress." + "invalid_ip_address": "Ogiltig IPv4-adress.", + "invalid_signature": "L\u00f6senordet f\u00f6r att dekryptera `.knxkeys`-filen \u00e4r fel." }, "step": { "manual_tunnel": { "data": { "tunneling_type": "KNX tunneltyp" + }, + "data_description": { + "host": "IP-adressen f\u00f6r KNX/IP-tunnelenheten.", + "local_ip": "L\u00e4mna tomt f\u00f6r att anv\u00e4nda automatisk uppt\u00e4ckt.", + "port": "Port p\u00e5 KNX/IP-tunnelenheten." + } + }, + "routing": { + "data_description": { + "individual_address": "KNX-adress som ska anv\u00e4ndas av Home Assistant, t.ex. `0.0.4`", + "local_ip": "L\u00e4mna tomt f\u00f6r att anv\u00e4nda automatisk uppt\u00e4ckt." + } + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "Filnamnet p\u00e5 din `.knxkeys`-fil (inklusive till\u00e4gget)", + "knxkeys_password": "L\u00f6senordet f\u00f6r att dekryptera filen `.knxkeys`" + }, + "data_description": { + "knxkeys_filename": "Filen f\u00f6rv\u00e4ntas finnas i din config-katalog i `.storage/knx/`.\n I Home Assistant OS skulle detta vara `/config/.storage/knx/`\n Exempel: `my_project.knxkeys`", + "knxkeys_password": "Detta st\u00e4lldes in n\u00e4r filen exporterades fr\u00e5n ETS." + }, + "description": "V\u00e4nligen ange informationen f\u00f6r din `.knxkeys`-fil." + }, + "secure_manual": { + "data": { + "device_authentication": "L\u00f6senord f\u00f6r enhetsautentisering", + "user_id": "Anv\u00e4ndar-ID", + "user_password": "Anv\u00e4ndarl\u00f6senord" + }, + "data_description": { + "device_authentication": "Detta st\u00e4lls in i 'IP'-panelen i gr\u00e4nssnittet i ETS." + }, + "description": "Ange din s\u00e4kra IP-information." + }, + "secure_tunneling": { + "description": "V\u00e4lj hur du vill konfigurera KNX/IP Secure.", + "menu_options": { + "secure_knxkeys": "Anv\u00e4nd en fil `.knxkeys` som inneh\u00e5ller s\u00e4kra IP-nycklar.", + "secure_manual": "Konfigurera s\u00e4kra IP nycklar manuellt" } } } }, "options": { "step": { + "init": { + "data_description": { + "individual_address": "KNX-adress som ska anv\u00e4ndas av Home Assistant, t.ex. `0.0.4`", + "local_ip": "Anv\u00e4nd `0.0.0.0.0` f\u00f6r automatisk identifiering.", + "multicast_group": "Anv\u00e4nds f\u00f6r routing och uppt\u00e4ckt. Standard: \"224.0.23.12\".", + "multicast_port": "Anv\u00e4nds f\u00f6r routing och uppt\u00e4ckt. Standard: \"3671\".", + "rate_limit": "Maximalt antal utg\u00e5ende telegram per sekund.\n Rekommenderad: 20 till 40", + "state_updater": "St\u00e4ll in som standard f\u00f6r att l\u00e4sa tillst\u00e5nd fr\u00e5n KNX-bussen. N\u00e4r den \u00e4r inaktiverad kommer Home Assistant inte aktivt att h\u00e4mta entitetstillst\u00e5nd fr\u00e5n KNX-bussen. Kan \u00e5sidos\u00e4ttas av entitetsalternativ \"sync_state\"." + } + }, "tunnel": { "data": { "tunneling_type": "KNX tunneltyp" diff --git a/homeassistant/components/konnected/translations/sv.json b/homeassistant/components/konnected/translations/sv.json index 0a0716f87ff..a96f612010a 100644 --- a/homeassistant/components/konnected/translations/sv.json +++ b/homeassistant/components/konnected/translations/sv.json @@ -15,6 +15,7 @@ "title": "Konnected-enheten redo" }, "import_confirm": { + "description": "En ansluten larmpanel med ID {id} har uppt\u00e4ckts i configuration.yaml. Detta fl\u00f6de l\u00e5ter dig importera det till en konfigurationspost.", "title": "Importera Konnected enhet" }, "user": { diff --git a/homeassistant/components/lacrosse_view/translations/el.json b/homeassistant/components/lacrosse_view/translations/el.json new file mode 100644 index 00000000000..1975028e9c9 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/el.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "no_locations": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b5\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/id.json b/homeassistant/components/lacrosse_view/translations/id.json new file mode 100644 index 00000000000..d244ba002b8 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "invalid_auth": "Autentikasi tidak valid", + "no_locations": "Tidak ada lokasi yang ditemukan", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/ja.json b/homeassistant/components/lacrosse_view/translations/ja.json new file mode 100644 index 00000000000..6b058f78cdb --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/ja.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "no_locations": "\u5834\u6240\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/nl.json b/homeassistant/components/lacrosse_view/translations/nl.json new file mode 100644 index 00000000000..44c1bc93f79 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "invalid_auth": "Ongeldige authenticatie", + "no_locations": "Geen locaties gevonden", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/pl.json b/homeassistant/components/lacrosse_view/translations/pl.json new file mode 100644 index 00000000000..b7fbbe50779 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie", + "no_locations": "Nie znaleziono lokalizacji", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/sv.json b/homeassistant/components/lacrosse_view/translations/sv.json new file mode 100644 index 00000000000..241263b2c53 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "no_locations": "Inga platser hittades", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/sv.json b/homeassistant/components/laundrify/translations/sv.json index dd7447e847e..f22a4ea3d3f 100644 --- a/homeassistant/components/laundrify/translations/sv.json +++ b/homeassistant/components/laundrify/translations/sv.json @@ -6,7 +6,20 @@ "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", + "invalid_format": "Ogiltigt format. V\u00e4nligen ange som xxx-xxx.", "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "init": { + "data": { + "code": "Auth-kod (xxx-xxx)" + }, + "description": "V\u00e4nligen ange din personliga autentiseringskod som visas i laundrify-appen." + }, + "reauth_confirm": { + "description": "Laundrify-integrationen m\u00e5ste autentiseras p\u00e5 nytt.", + "title": "Autentisera om integration" + } } } } \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/sv.json b/homeassistant/components/lcn/translations/sv.json new file mode 100644 index 00000000000..59b06895f48 --- /dev/null +++ b/homeassistant/components/lcn/translations/sv.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "trigger_type": { + "codelock": "kodl\u00e5skod mottagen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/sv.json b/homeassistant/components/lg_soundbar/translations/sv.json new file mode 100644 index 00000000000..9b8ff6ea1aa --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "existing_instance_updated": "Uppdaterade existerande konfiguration." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/sv.json b/homeassistant/components/life360/translations/sv.json index 27a669dfb5c..1a5c7f2e569 100644 --- a/homeassistant/components/life360/translations/sv.json +++ b/homeassistant/components/life360/translations/sv.json @@ -1,12 +1,23 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, "create_entry": { "default": "F\u00f6r att st\u00e4lla in avancerade alternativ, se [Life360 documentation]({docs_url})." }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "invalid_username": "Ogiltigt anv\u00e4ndarnmn" }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "password": "L\u00f6senord", @@ -16,5 +27,19 @@ "title": "Life360 kontoinformation" } } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Visa k\u00f6rning som tillst\u00e5nd", + "driving_speed": "K\u00f6rhastighet", + "limit_gps_acc": "Begr\u00e4nsa GPS-noggrannheten", + "max_gps_accuracy": "Maximal GPS-noggrannhet (meter)", + "set_drive_speed": "St\u00e4ll in k\u00f6rhastighetsgr\u00e4ns" + }, + "title": "Konto-alternativ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/nl.json b/homeassistant/components/lifx/translations/nl.json index c8d0ca83dd8..51091fcd365 100644 --- a/homeassistant/components/lifx/translations/nl.json +++ b/homeassistant/components/lifx/translations/nl.json @@ -1,12 +1,31 @@ { "config": { "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", "no_devices_found": "Geen apparaten gevonden op het netwerk", "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Wilt u LIFX instellen?" + }, + "discovery_confirm": { + "description": "Wilt u {label} ({host}) {serial} instellen?" + }, + "pick_device": { + "data": { + "device": "Apparaat" + } + }, + "user": { + "data": { + "host": "Host" + } } } } diff --git a/homeassistant/components/lifx/translations/sv.json b/homeassistant/components/lifx/translations/sv.json index 82a55b48edb..dfd7de02d94 100644 --- a/homeassistant/components/lifx/translations/sv.json +++ b/homeassistant/components/lifx/translations/sv.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", "no_devices_found": "Inga LIFX enheter hittas i n\u00e4tverket.", "single_instance_allowed": "Endast en enda konfiguration av LIFX \u00e4r m\u00f6jlig." }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "Vill du st\u00e4lla in LIFX?" + }, + "discovery_confirm": { + "description": "Vill du st\u00e4lla in {label} ( {host} ) {serial} ?" + }, + "pick_device": { + "data": { + "device": "Enhet" + } + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Om du l\u00e4mnar v\u00e4rden tomt anv\u00e4nds discovery f\u00f6r att hitta enheter." } } } diff --git a/homeassistant/components/litterrobot/translations/sensor.sv.json b/homeassistant/components/litterrobot/translations/sensor.sv.json new file mode 100644 index 00000000000..c54c705b8c4 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/sensor.sv.json @@ -0,0 +1,28 @@ +{ + "state": { + "litterrobot__status_code": { + "br": "Huven \u00e4r borttagen", + "ccc": "Reningscykel klar", + "ccp": "Reng\u00f6ringscykel p\u00e5g\u00e5r", + "csf": "Kattsensor fel", + "csi": "Kattsensor avbruten", + "cst": "Kattsensor timing", + "df1": "L\u00e5da n\u00e4stan full - 2 cykler kvar", + "df2": "L\u00e5da n\u00e4stan full - 1 cykel kvar", + "dfs": "L\u00e5dan full", + "dhf": "Dump + fel i heml\u00e4get", + "dpf": "Fel i dumpningsl\u00e4get", + "ec": "T\u00f6mningscykel", + "hpf": "Hempositionsfel", + "off": "Avst\u00e4ngd", + "offline": "Offline", + "otf": "Fel vid f\u00f6r h\u00f6gt vridmoment", + "p": "Pausad", + "pd": "Pinch Detect", + "rdy": "Redo", + "scf": "Fel p\u00e5 kattsensorn vid uppstart", + "sdf": "L\u00e5dan full vid uppstart", + "spf": "Pinch Detect vid start" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/id.json b/homeassistant/components/lyric/translations/id.json index a519e2e5f9e..b75093d1718 100644 --- a/homeassistant/components/lyric/translations/id.json +++ b/homeassistant/components/lyric/translations/id.json @@ -17,5 +17,11 @@ "title": "Autentikasi Ulang Integrasi" } } + }, + "issues": { + "removed_yaml": { + "description": "Proses konfigurasi Honeywell Lyric lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Honeywell Lyric telah dihapus" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/ja.json b/homeassistant/components/lyric/translations/ja.json index 2a5abdcb5b7..5394e978c27 100644 --- a/homeassistant/components/lyric/translations/ja.json +++ b/homeassistant/components/lyric/translations/ja.json @@ -17,5 +17,10 @@ "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" } } + }, + "issues": { + "removed_yaml": { + "title": "Honeywell Lyric YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/nl.json b/homeassistant/components/lyric/translations/nl.json index e820174aa5a..da6f0ed06c1 100644 --- a/homeassistant/components/lyric/translations/nl.json +++ b/homeassistant/components/lyric/translations/nl.json @@ -17,5 +17,10 @@ "title": "Integratie herauthenticeren" } } + }, + "issues": { + "removed_yaml": { + "title": "De Honeywell Lyric YAML-configuratie is verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/ru.json b/homeassistant/components/lyric/translations/ru.json index 7aef03ff3c5..536f1a9c0cc 100644 --- a/homeassistant/components/lyric/translations/ru.json +++ b/homeassistant/components/lyric/translations/ru.json @@ -17,5 +17,11 @@ "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" } } + }, + "issues": { + "removed_yaml": { + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"Honeywell Lyric\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Honeywell Lyric \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/sv.json b/homeassistant/components/lyric/translations/sv.json new file mode 100644 index 00000000000..a34370b5ce5 --- /dev/null +++ b/homeassistant/components/lyric/translations/sv.json @@ -0,0 +1,8 @@ +{ + "issues": { + "removed_yaml": { + "description": "Konfigurering av Honeywell Lyric med YAML har tagits bort. \n\n Din befintliga YAML-konfiguration anv\u00e4nds inte av Home Assistant. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Honeywell Lyric YAML-konfigurationen har tagits bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/sv.json b/homeassistant/components/meater/translations/sv.json index 47a719743f5..c5ecfbc1d84 100644 --- a/homeassistant/components/meater/translations/sv.json +++ b/homeassistant/components/meater/translations/sv.json @@ -2,7 +2,8 @@ "config": { "error": { "invalid_auth": "Ogiltig autentisering", - "service_unavailable_error": "Programmeringsgr\u00e4nssnittet g\u00e5r inte att komma \u00e5t f\u00f6r n\u00e4rvarande. F\u00f6rs\u00f6k igen senare." + "service_unavailable_error": "Programmeringsgr\u00e4nssnittet g\u00e5r inte att komma \u00e5t f\u00f6r n\u00e4rvarande. F\u00f6rs\u00f6k igen senare.", + "unknown_auth_error": "Ov\u00e4ntat fel" }, "step": { "reauth_confirm": { @@ -13,11 +14,13 @@ }, "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" }, "data_description": { "username": "Meater Cloud anv\u00e4ndarnamn, vanligtvis en e-postadress." - } + }, + "description": "Konfigurera ditt Meater Cloud-konto." } } } diff --git a/homeassistant/components/miflora/translations/id.json b/homeassistant/components/miflora/translations/id.json new file mode 100644 index 00000000000..643d41c9fbd --- /dev/null +++ b/homeassistant/components/miflora/translations/id.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Integrasi Mi Flora berhenti bekerja di Home Assistant 2022.7 dan digantikan oleh integrasi Xiaomi BLE dalam rilis 2022.8.\n\nTidak ada jalur migrasi yang bisa dilakukan, oleh karena itu, Anda harus menambahkan perangkat Mi Flora Anda menggunakan integrasi baru secara manual.\n\nKonfigurasi Mi Flora YAML Anda yang ada tidak lagi digunakan oleh Home Assistant. Hapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Integrasi Mi Flora telah diganti" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/ja.json b/homeassistant/components/miflora/translations/ja.json new file mode 100644 index 00000000000..30b2730980b --- /dev/null +++ b/homeassistant/components/miflora/translations/ja.json @@ -0,0 +1,7 @@ +{ + "issues": { + "replaced": { + "title": "MiFlora\u306e\u7d71\u5408\u306f\u7f6e\u304d\u63db\u3048\u3089\u308c\u307e\u3057\u305f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/nl.json b/homeassistant/components/miflora/translations/nl.json new file mode 100644 index 00000000000..5189996b75b --- /dev/null +++ b/homeassistant/components/miflora/translations/nl.json @@ -0,0 +1,7 @@ +{ + "issues": { + "replaced": { + "title": "De Mi Flora-integratie is vervangen." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/ru.json b/homeassistant/components/miflora/translations/ru.json new file mode 100644 index 00000000000..b5bf7bfd3c1 --- /dev/null +++ b/homeassistant/components/miflora/translations/ru.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Mi Flora\" \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u043b\u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.7 \u0438 \u0431\u044b\u043b\u0430 \u0437\u0430\u043c\u0435\u043d\u0435\u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439 \"Xiaomi BLE\" \u0432 \u0432\u0435\u0440\u0441\u0438\u0438 2022.8.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Mi Flora \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043d\u043e\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.\n\n\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \"Mi Flora\" \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Mi Flora\" \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/sv.json b/homeassistant/components/miflora/translations/sv.json new file mode 100644 index 00000000000..bff3f7f785e --- /dev/null +++ b/homeassistant/components/miflora/translations/sv.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Mi Flora-integrationen slutade fungera i Home Assistant 2022.7 och ersattes av Xiaomi BLE-integrationen i 2022.8-versionen. \n\n Det finns ingen migreringsv\u00e4g m\u00f6jlig, d\u00e4rf\u00f6r m\u00e5ste du l\u00e4gga till din Mi Flora-enhet med den nya integrationen manuellt. \n\n Din befintliga Mi Flora YAML-konfiguration anv\u00e4nds inte l\u00e4ngre av Home Assistant. Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Mi Flora-integrationen har ersatts" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/sv.json b/homeassistant/components/min_max/translations/sv.json index d39c277daff..7a48b0e631b 100644 --- a/homeassistant/components/min_max/translations/sv.json +++ b/homeassistant/components/min_max/translations/sv.json @@ -1,9 +1,22 @@ { + "config": { + "step": { + "user": { + "data_description": { + "round_digits": "Styr antalet decimalsiffror i utg\u00e5ngen n\u00e4r statistikegenskapen \u00e4r medelv\u00e4rde eller median." + }, + "title": "L\u00e4gg till min / max / medelv\u00e4rde / mediansensor" + } + } + }, "options": { "step": { "init": { "data": { "round_digits": "Precision" + }, + "data_description": { + "round_digits": "Styr antalet decimalsiffror i utg\u00e5ngen n\u00e4r statistikegenskapen \u00e4r medelv\u00e4rde eller median." } } } diff --git a/homeassistant/components/mitemp_bt/translations/id.json b/homeassistant/components/mitemp_bt/translations/id.json new file mode 100644 index 00000000000..ae334e0e1ab --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/id.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Integrasi Sensor Temperatur dan Kelembaban Xiaomi Mijia BLE berhenti bekerja di Home Assistant 2022.7 dan digantikan oleh integrasi Xiaomi BLE dalam rilis 2022.8.\n\nTidak ada jalur migrasi yang bisa dilakukan, oleh karena itu, Anda harus menambahkan perangkat Mi Flora Anda menggunakan integrasi baru secara manual.\n\nKonfigurasi Sensor Temperatur dan Kelembaban Xiaomi Mijia BLE Anda yang ada tidak lagi digunakan oleh Home Assistant. Hapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Integrasi Sensor Temperatur dan Kelembaban BLE Xiaomi Mijia telah diganti" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/nl.json b/homeassistant/components/mitemp_bt/translations/nl.json new file mode 100644 index 00000000000..e75f4eba261 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/nl.json @@ -0,0 +1,7 @@ +{ + "issues": { + "replaced": { + "title": "De Xiaomi Mijia BLE Temperature and Humidity Sensor-integratie is vervangen." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/ru.json b/homeassistant/components/mitemp_bt/translations/ru.json new file mode 100644 index 00000000000..e25532777b8 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/ru.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Xiaomi Mijia BLE Temperature and Humidity Sensor\" \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u043b\u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.7 \u0438 \u0437\u0430\u043c\u0435\u043d\u0435\u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439 \"Xiaomi BLE\" \u0432 \u0432\u0435\u0440\u0441\u0438\u0438 2022.8.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Xiaomi Mijia BLE \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043d\u043e\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.\n\n\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \"Xiaomi Mijia BLE Temperature and Humidity Sensor\" \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Xiaomi Mijia BLE Temperature and Humidity Sensor\" \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/sv.json b/homeassistant/components/mitemp_bt/translations/sv.json new file mode 100644 index 00000000000..e708454e100 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/sv.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Xiaomi Mijia BLE temperatur- och fuktsensorintegreringen slutade fungera i Home Assistant 2022.7 och ersattes av Xiaomi BLE-integrationen i 2022.8-versionen. \n\n Det finns ingen migreringsv\u00e4g m\u00f6jlig, d\u00e4rf\u00f6r m\u00e5ste du l\u00e4gga till din Xiaomi Mijia BLE-enhet med den nya integrationen manuellt. \n\n Din befintliga Xiaomi Mijia BLE temperatur- och luftfuktighetssensor YAML-konfiguration anv\u00e4nds inte l\u00e4ngre av Home Assistant. Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Xiaomi Mijia BLE temperatur- och fuktsensorintegrering har ersatts" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/nl.json b/homeassistant/components/moat/translations/nl.json new file mode 100644 index 00000000000..a46f954fe5f --- /dev/null +++ b/homeassistant/components/moat/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/sv.json b/homeassistant/components/moat/translations/sv.json new file mode 100644 index 00000000000..8c794885dd2 --- /dev/null +++ b/homeassistant/components/moat/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enhet \u00e4r redan konfigurerad", + "already_in_progress": "Konfiguration redan ig\u00e5ng", + "no_devices_found": "Inga enheter hittades p\u00e5 n\u00e4tverket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du s\u00e4tta upp {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att st\u00e4lla in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/sv.json b/homeassistant/components/mqtt/translations/sv.json index b3088ca49a9..1699051db86 100644 --- a/homeassistant/components/mqtt/translations/sv.json +++ b/homeassistant/components/mqtt/translations/sv.json @@ -36,6 +36,16 @@ "button_6": "Sj\u00e4tte knappen", "turn_off": "St\u00e4ng av", "turn_on": "Starta" + }, + "trigger_type": { + "button_double_press": "\"{subtyp}\" dubbelklickad", + "button_long_press": "\" {subtype} \" kontinuerligt nedtryckt", + "button_long_release": "\" {subtype} \" sl\u00e4pptes efter l\u00e5ng tryckning", + "button_quadruple_press": "\"{subtyp}\" fyrdubbelt klickad", + "button_quintuple_press": "\"{subtype}\" kvintubbel klickade", + "button_short_press": "\"{subtyp}\" tryckt", + "button_short_release": "\"{subtyp}\" sl\u00e4pptes", + "button_triple_press": "\" {subtype}\" trippelklickad" } }, "options": { diff --git a/homeassistant/components/nest/translations/sv.json b/homeassistant/components/nest/translations/sv.json index 9e91f3ddf94..750a7643ee2 100644 --- a/homeassistant/components/nest/translations/sv.json +++ b/homeassistant/components/nest/translations/sv.json @@ -1,4 +1,7 @@ { + "application_credentials": { + "description": "F\u00f6lj [instruktionerna]( {more_info_url} ) f\u00f6r att konfigurera Cloud Console: \n\n 1. G\u00e5 till [OAuth-samtyckessk\u00e4rmen]( {oauth_consent_url} ) och konfigurera\n 1. G\u00e5 till [Inloggningsuppgifter]( {oauth_creds_url} ) och klicka p\u00e5 **Skapa inloggningsuppgifter**.\n 1. V\u00e4lj **OAuth-klient-ID** i rullgardinsmenyn.\n 1. V\u00e4lj **Webbapplikation** f\u00f6r applikationstyp.\n 1. L\u00e4gg till ` {redirect_url} ` under *Auktoriserad omdirigerings-URI*." + }, "config": { "abort": { "already_configured": "Konto har redan konfigurerats", @@ -10,6 +13,32 @@ "unknown": "Ok\u00e4nt fel vid validering av kod" }, "step": { + "auth_upgrade": { + "description": "App Auth har fasats ut av Google f\u00f6r att f\u00f6rb\u00e4ttra s\u00e4kerheten, och du m\u00e5ste vidta \u00e5tg\u00e4rder genom att skapa nya applikationsuppgifter. \n\n \u00d6ppna [dokumentationen]( {more_info_url} ) f\u00f6r att f\u00f6lja med eftersom n\u00e4sta steg guidar dig genom stegen du beh\u00f6ver ta f\u00f6r att \u00e5terst\u00e4lla \u00e5tkomsten till dina Nest-enheter.", + "title": "Nest: Utfasning av appautentisering" + }, + "cloud_project": { + "data": { + "cloud_project_id": "Projekt-ID f\u00f6r Google Cloud" + }, + "description": "Ange molnprojekt-ID nedan, t.ex. *example-project-12345*. Se [Google Cloud Console]({cloud_console_url}) eller dokumentationen f\u00f6r [mer information]({more_info_url}).", + "title": "Nest: Ange molnprojekt-ID" + }, + "create_cloud_project": { + "description": "Med Nest-integreringen kan du integrera dina Nest-termostater, kameror och d\u00f6rrklockor med hj\u00e4lp av Smart Device Management API. SDM API **kr\u00e4ver en 5 USD** eng\u00e5ngsavgift f\u00f6r installation. Se dokumentationen f\u00f6r [mer info]( {more_info_url} ). \n\n 1. G\u00e5 till [Google Cloud Console]( {cloud_console_url} ).\n 1. Om detta \u00e4r ditt f\u00f6rsta projekt klickar du p\u00e5 **Skapa projekt** och sedan p\u00e5 **Nytt projekt**.\n 1. Ge ditt molnprojekt ett namn och klicka sedan p\u00e5 **Skapa**.\n 1. Spara Cloud Project ID t.ex. *example-project-12345* som du kommer att beh\u00f6va det senare\n 1. G\u00e5 till API Library f\u00f6r [Smart Device Management API]( {sdm_api_url} ) och klicka p\u00e5 **Aktivera**.\n 1. G\u00e5 till API Library f\u00f6r [Cloud Pub/Sub API]( {pubsub_api_url} ) och klicka p\u00e5 **Aktivera**. \n\n Forts\u00e4tt n\u00e4r ditt molnprojekt har konfigurerats.", + "title": "Nest: Skapa och konfigurera molnprojekt" + }, + "device_project": { + "data": { + "project_id": "Projekt-ID f\u00f6r enhets\u00e5tkomst" + }, + "description": "Skapa ett Nest Device Access-projekt som **kr\u00e4ver en avgift p\u00e5 5 USD** f\u00f6r att konfigurera.\n 1. G\u00e5 till [Device Access Console]( {device_access_console_url} ) och genom betalningsfl\u00f6det.\n 1. Klicka p\u00e5 **Skapa projekt**\n 1. Ge ditt Device Access-projekt ett namn och klicka p\u00e5 **N\u00e4sta**.\n 1. Ange ditt OAuth-klient-ID\n 1. Aktivera h\u00e4ndelser genom att klicka p\u00e5 **Aktivera** och **Skapa projekt**. \n\n Ange ditt Device Access Project ID nedan ([mer info]( {more_info_url} )).\n", + "title": "Nest: Skapa ett projekt f\u00f6r enhets\u00e5tkomst" + }, + "device_project_upgrade": { + "description": "Uppdatera Nest Device Access Project med ditt nya OAuth-klient-ID ([mer info]( {more_info_url} ))\n 1. G\u00e5 till [Device Access Console]( {device_access_console_url} ).\n 1. Klicka p\u00e5 papperskorgen bredvid *OAuth Client ID*.\n 1. Klicka p\u00e5 menyn \"...\" och *L\u00e4gg till klient-ID*.\n 1. Ange ditt nya OAuth-klient-ID och klicka p\u00e5 **L\u00e4gg till**. \n\n Ditt OAuth-klient-ID \u00e4r: ` {client_id} `", + "title": "Nest: Uppdatera Device Access Project" + }, "init": { "data": { "flow_impl": "Leverant\u00f6r" diff --git a/homeassistant/components/nextdns/translations/sv.json b/homeassistant/components/nextdns/translations/sv.json new file mode 100644 index 00000000000..ae0c7c5eeef --- /dev/null +++ b/homeassistant/components/nextdns/translations/sv.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Denna NextDNS-profil \u00e4r redan konfigurerad." + }, + "error": { + "cannot_connect": "Misslyckades att ansluta", + "invalid_api_key": "Ogiltig API nyckel", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "profiles": { + "data": { + "profile": "Profil" + } + }, + "user": { + "data": { + "api_key": "API nyckel" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "N\u00e5 servern" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/sv.json b/homeassistant/components/nina/translations/sv.json new file mode 100644 index 00000000000..11f2e28deac --- /dev/null +++ b/homeassistant/components/nina/translations/sv.json @@ -0,0 +1,24 @@ +{ + "options": { + "error": { + "cannot_connect": "Misslyckades att ansluta", + "no_selection": "V\u00e4lj minst en stad/l\u00e4n", + "unknown": "Ov\u00e4ntatn fel" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Stad/l\u00e4n (AD)", + "_e_to_h": "Stad/l\u00e4n (EH)", + "_i_to_l": "Stad/l\u00e4n (I-L)", + "_m_to_q": "Stad/l\u00e4n (M-Q)", + "_r_to_u": "Stad/l\u00e4n (R-U)", + "_v_to_z": "Stad/l\u00e4n (V-Z)", + "corona_filter": "Ta bort Corona-varningar", + "slots": "Maximala varningar per stad/l\u00e4n" + }, + "title": "Alternativ" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/sv.json b/homeassistant/components/onewire/translations/sv.json index 9b57beabc8f..e4ce4947ca9 100644 --- a/homeassistant/components/onewire/translations/sv.json +++ b/homeassistant/components/onewire/translations/sv.json @@ -1,4 +1,14 @@ { + "config": { + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + } + } + } + }, "options": { "step": { "device_selection": { diff --git a/homeassistant/components/openalpr_local/translations/el.json b/homeassistant/components/openalpr_local/translations/el.json new file mode 100644 index 00000000000..ba56c490298 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/el.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0397 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 OpenALPR \u03b5\u03ba\u03ba\u03c1\u03b5\u03bc\u03b5\u03af \u03ba\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant 2022.10. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 OpenALPR \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/id.json b/homeassistant/components/openalpr_local/translations/id.json new file mode 100644 index 00000000000..1039c96daa1 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/id.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integrasi OpenALPR Local sedang menunggu penghapusan dari Home Assistant dan tidak akan lagi tersedia pada Home Assistant 2022.10.\n\nHapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Integrasi OpenALPR dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/ja.json b/homeassistant/components/openalpr_local/translations/ja.json new file mode 100644 index 00000000000..dbdd930cd10 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/ja.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "OpenALPR Local\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/nl.json b/homeassistant/components/openalpr_local/translations/nl.json new file mode 100644 index 00000000000..06bd27e2d56 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/nl.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "De OpenALPR Local-integratie wordt verwijderd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/sv.json b/homeassistant/components/openalpr_local/translations/sv.json new file mode 100644 index 00000000000..2c02b30458d --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/sv.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "OpenALPR Local integration v\u00e4ntar p\u00e5 borttagning fr\u00e5n Home Assistant och kommer inte l\u00e4ngre att vara tillg\u00e4nglig fr\u00e5n och med Home Assistant 2022.10. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "OpenALPR Local integrationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sv.json b/homeassistant/components/overkiz/translations/sv.json index c825e3cd616..d88861d15e0 100644 --- a/homeassistant/components/overkiz/translations/sv.json +++ b/homeassistant/components/overkiz/translations/sv.json @@ -4,6 +4,9 @@ "reauth_successful": "\u00c5terautentisering lyckades", "reauth_wrong_account": "Du kan bara \u00e5terautentisera denna post med samma Overkiz-konto och hub" }, + "error": { + "too_many_attempts": "F\u00f6r m\u00e5nga f\u00f6rs\u00f6k med en ogiltig token, tillf\u00e4lligt avst\u00e4ngd" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/plex/translations/sv.json b/homeassistant/components/plex/translations/sv.json index 63b12e70e40..4227e45b707 100644 --- a/homeassistant/components/plex/translations/sv.json +++ b/homeassistant/components/plex/translations/sv.json @@ -36,7 +36,9 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "Ignorera nya hanterade/delade anv\u00e4ndare", "ignore_plex_web_clients": "Ignorera Plex Web-klienter", + "monitored_users": "\u00d6vervakade anv\u00e4ndare", "use_episode_art": "Anv\u00e4nd avsnittsbild" }, "description": "Alternativ f\u00f6r Plex-mediaspelare" diff --git a/homeassistant/components/plugwise/translations/sv.json b/homeassistant/components/plugwise/translations/sv.json index affb73f907e..a22c1e882bc 100644 --- a/homeassistant/components/plugwise/translations/sv.json +++ b/homeassistant/components/plugwise/translations/sv.json @@ -1,9 +1,14 @@ { "config": { + "abort": { + "anna_with_adam": "B\u00e5de Anna och Adam uppt\u00e4ckte. L\u00e4gg till din Adam ist\u00e4llet f\u00f6r din Anna" + }, "step": { "user": { "data": { - "port": "Port" + "password": "Smile ID", + "port": "Port", + "username": "Smile Anv\u00e4ndarnamn" } }, "user_gateway": { diff --git a/homeassistant/components/qnap_qsw/translations/sv.json b/homeassistant/components/qnap_qsw/translations/sv.json index ec6c8842dca..f447af18c52 100644 --- a/homeassistant/components/qnap_qsw/translations/sv.json +++ b/homeassistant/components/qnap_qsw/translations/sv.json @@ -8,6 +8,12 @@ "invalid_auth": "Ogiltig autentisering" }, "step": { + "discovered_connection": { + "data": { + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" + } + }, "user": { "data": { "username": "Anv\u00e4ndarnamn" diff --git a/homeassistant/components/radiotherm/translations/id.json b/homeassistant/components/radiotherm/translations/id.json index 1e454cc8cc8..21198e3fad1 100644 --- a/homeassistant/components/radiotherm/translations/id.json +++ b/homeassistant/components/radiotherm/translations/id.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi platform cuaca Radio Thermostat lewat YAML dalam proses penghapusan di Home Assistant 2022.9.\n\nKonfigurasi yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML platform cuaca Radio Thermostat dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Radio Thermostat dalam proses penghapusan" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/ja.json b/homeassistant/components/radiotherm/translations/ja.json index b792d0a1c9b..67f667bacc1 100644 --- a/homeassistant/components/radiotherm/translations/ja.json +++ b/homeassistant/components/radiotherm/translations/ja.json @@ -19,6 +19,11 @@ } } }, + "issues": { + "deprecated_yaml": { + "title": "Radio Thermostat YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/sv.json b/homeassistant/components/radiotherm/translations/sv.json index f341a6314ee..3999a2a90e0 100644 --- a/homeassistant/components/radiotherm/translations/sv.json +++ b/homeassistant/components/radiotherm/translations/sv.json @@ -8,11 +8,29 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "confirm": { + "description": "Vill du konfigurera {name} {model} ( {host} )?" + }, "user": { "data": { "host": "V\u00e4rd" } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av radiotermostatens klimatplattform med YAML tas bort i Home Assistant 2022.9. \n\n Din befintliga konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Radiotermostatens YAML-konfiguration tas bort" + } + }, + "options": { + "step": { + "init": { + "data": { + "hold_temp": "St\u00e4ll in en permanent h\u00e5llning n\u00e4r du justerar temperaturen." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recorder/translations/sv.json b/homeassistant/components/recorder/translations/sv.json index bf4d6ccf0a5..0caafb1ee1b 100644 --- a/homeassistant/components/recorder/translations/sv.json +++ b/homeassistant/components/recorder/translations/sv.json @@ -2,6 +2,9 @@ "system_health": { "info": { "current_recorder_run": "Aktuell starttid", + "database_engine": "Databasmotor", + "database_version": "Databasversion", + "estimated_db_size": "Ber\u00e4knad databasstorlek (MiB)", "oldest_recorder_run": "\u00c4ldsta starttid" } } diff --git a/homeassistant/components/rhasspy/translations/sv.json b/homeassistant/components/rhasspy/translations/sv.json new file mode 100644 index 00000000000..9f3e32535be --- /dev/null +++ b/homeassistant/components/rhasspy/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Enbart en konfiguration \u00e4r m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du aktivera Rhasspy-support?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/sv.json b/homeassistant/components/roon/translations/sv.json new file mode 100644 index 00000000000..420e19171ae --- /dev/null +++ b/homeassistant/components/roon/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "fallback": { + "data": { + "host": "V\u00e4rdnamn", + "port": "Port" + }, + "description": "Kunde inte uppt\u00e4cka Roon-servern, ange ditt v\u00e4rdnamn och port." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/translations/sv.json b/homeassistant/components/sabnzbd/translations/sv.json index 61847ff0eb4..d82e85f8c48 100644 --- a/homeassistant/components/sabnzbd/translations/sv.json +++ b/homeassistant/components/sabnzbd/translations/sv.json @@ -9,7 +9,8 @@ "data": { "api_key": "API Nyckel", "name": "Namn", - "path": "S\u00f6kv\u00e4g" + "path": "S\u00f6kv\u00e4g", + "url": "URL" } } } diff --git a/homeassistant/components/scrape/translations/sv.json b/homeassistant/components/scrape/translations/sv.json index c70f08008dc..f1cf81c5aa6 100644 --- a/homeassistant/components/scrape/translations/sv.json +++ b/homeassistant/components/scrape/translations/sv.json @@ -6,9 +6,22 @@ "step": { "user": { "data": { + "attribute": "Attribut", + "authentication": "Autentisering", + "device_class": "Enhetsklass", + "headers": "Headers", + "index": "Index", "password": "L\u00f6senord", + "resource": "Resurs", + "select": "V\u00e4lj", + "state_class": "Tillst\u00e5ndsklass", + "unit_of_measurement": "M\u00e5ttenhet", "username": "Anv\u00e4ndarnamn", + "value_template": "V\u00e4rdemall", "verify_ssl": "Verifiera SSL-certifikat" + }, + "data_description": { + "attribute": "H\u00e4mta v\u00e4rdet av ett attribut p\u00e5 den valda taggen" } } } @@ -17,7 +30,11 @@ "step": { "init": { "data": { + "attribute": "Attribut", + "authentication": "Autentisering", + "index": "Index", "password": "L\u00f6senord", + "select": "V\u00e4lj", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/sensibo/translations/sensor.sv.json b/homeassistant/components/sensibo/translations/sensor.sv.json index ead64d63cd6..b07d40e18fd 100644 --- a/homeassistant/components/sensibo/translations/sensor.sv.json +++ b/homeassistant/components/sensibo/translations/sensor.sv.json @@ -1,7 +1,8 @@ { "state": { "sensibo__sensitivity": { - "n": "Normal" + "n": "Normal", + "s": "K\u00e4nslighet" } } } \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/nl.json b/homeassistant/components/sensorpush/translations/nl.json new file mode 100644 index 00000000000..a46f954fe5f --- /dev/null +++ b/homeassistant/components/sensorpush/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/sv.json b/homeassistant/components/sensorpush/translations/sv.json new file mode 100644 index 00000000000..7606ba7df45 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du konfigurera {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att konfigurera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/id.json b/homeassistant/components/senz/translations/id.json index a2fdf8837bd..6f4eec8f3b9 100644 --- a/homeassistant/components/senz/translations/id.json +++ b/homeassistant/components/senz/translations/id.json @@ -16,5 +16,11 @@ "title": "Pilih Metode Autentikasi" } } + }, + "issues": { + "removed_yaml": { + "description": "Proses konfigurasi nVent RAYCHEM SENZ lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML nVent RAYCHEM SENZ telah dihapus" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/ja.json b/homeassistant/components/senz/translations/ja.json index b6aa94ef30c..dbb794e2cff 100644 --- a/homeassistant/components/senz/translations/ja.json +++ b/homeassistant/components/senz/translations/ja.json @@ -16,5 +16,10 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" } } + }, + "issues": { + "removed_yaml": { + "title": "nVent RAYCHEM SENZ YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/nl.json b/homeassistant/components/senz/translations/nl.json index 7f1eaccf89c..f12901ee3d5 100644 --- a/homeassistant/components/senz/translations/nl.json +++ b/homeassistant/components/senz/translations/nl.json @@ -16,5 +16,10 @@ "title": "Kies een authenticatie methode" } } + }, + "issues": { + "removed_yaml": { + "title": "De nVent RAYCHEM SENZ YAML-configuratie is verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/ru.json b/homeassistant/components/senz/translations/ru.json index 2f572831b5b..21c6b830841 100644 --- a/homeassistant/components/senz/translations/ru.json +++ b/homeassistant/components/senz/translations/ru.json @@ -16,5 +16,11 @@ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" } } + }, + "issues": { + "removed_yaml": { + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"nVent RAYCHEM SENZ\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 nVent RAYCHEM SENZ \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/sv.json b/homeassistant/components/senz/translations/sv.json new file mode 100644 index 00000000000..9955b604e20 --- /dev/null +++ b/homeassistant/components/senz/translations/sv.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Kontot \u00e4r redan konfigurerat", + "already_in_progress": "Konfiguration \u00e4r redan ig\u00e5ng", + "authorize_url_timeout": "Timout under skapandet av autentiseringsURL:en", + "missing_configuration": "Komponenten \u00e4r inte konfigurerad", + "no_url_available": "Ingen URL tillg\u00e4nglig. F\u00f6r mer information om detta felet [kolla i hj\u00e4lpsektionen]({docs_url})", + "oauth_error": "Mottog felaktig token." + }, + "create_entry": { + "default": "Autentiseringen lyckades" + }, + "step": { + "pick_implementation": { + "title": "V\u00e4j autentiseringsmetod" + } + } + }, + "issues": { + "removed_yaml": { + "description": "Konfigurering av nVent RAYCHEM SENZ med YAML har tagits bort. \n\n Din befintliga YAML-konfiguration anv\u00e4nds inte av Home Assistant. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "nVent RAYCHEM SENZ YAML-konfigurationen har tagits bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/sv.json b/homeassistant/components/shelly/translations/sv.json index 36b53053594..21a5ba55b58 100644 --- a/homeassistant/components/shelly/translations/sv.json +++ b/homeassistant/components/shelly/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "firmware_not_fully_provisioned": "Enheten \u00e4r inte helt etablerad. Kontakta Shellys support" + }, "step": { "credentials": { "data": { diff --git a/homeassistant/components/simplepush/translations/el.json b/homeassistant/components/simplepush/translations/el.json index bdcc7239acc..8f8c4691045 100644 --- a/homeassistant/components/simplepush/translations/el.json +++ b/homeassistant/components/simplepush/translations/el.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/id.json b/homeassistant/components/simplepush/translations/id.json index 35984af5d5f..715cb3c893b 100644 --- a/homeassistant/components/simplepush/translations/id.json +++ b/homeassistant/components/simplepush/translations/id.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Simplepush lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Simplepush dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Simplepush dalam proses penghapusan" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/ja.json b/homeassistant/components/simplepush/translations/ja.json index 8e3023e602e..fd22d7dfef5 100644 --- a/homeassistant/components/simplepush/translations/ja.json +++ b/homeassistant/components/simplepush/translations/ja.json @@ -17,5 +17,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "Simplepush YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/nl.json b/homeassistant/components/simplepush/translations/nl.json index 176318b3f3c..8916c7db473 100644 --- a/homeassistant/components/simplepush/translations/nl.json +++ b/homeassistant/components/simplepush/translations/nl.json @@ -13,5 +13,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "De Simplepush YAML-configuratie wordt verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/pl.json b/homeassistant/components/simplepush/translations/pl.json index fe19feb39a1..10f83b3401c 100644 --- a/homeassistant/components/simplepush/translations/pl.json +++ b/homeassistant/components/simplepush/translations/pl.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Simplepush przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Simplepush zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/sv.json b/homeassistant/components/simplepush/translations/sv.json new file mode 100644 index 00000000000..9ed6908f0c3 --- /dev/null +++ b/homeassistant/components/simplepush/translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Misslyckades att ansluta" + }, + "step": { + "user": { + "data": { + "device_key": "Enhetsnyckeln f\u00f6r din enhet", + "event": "H\u00e4ndelsen f\u00f6r h\u00e4ndelserna.", + "name": "Namn", + "password": "L\u00f6senordet f\u00f6r krypteringen som anv\u00e4nds av din enhet", + "salt": "Det salt som anv\u00e4nds av din enhet." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Simplepush med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Simplepush YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Simplepush YAML-konfigurationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/id.json b/homeassistant/components/simplisafe/translations/id.json index 68da3b7689b..e9919dc734b 100644 --- a/homeassistant/components/simplisafe/translations/id.json +++ b/homeassistant/components/simplisafe/translations/id.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Akun SimpliSafe ini sudah digunakan.", "email_2fa_timed_out": "Tenggang waktu habis ketika menunggu autentikasi dua faktor berbasis email.", - "reauth_successful": "Autentikasi ulang berhasil" + "reauth_successful": "Autentikasi ulang berhasil", + "wrong_account": "Kredensial pengguna yang diberikan tidak cocok dengan akun SimpliSafe ini." }, "error": { + "identifier_exists": "Akun sudah terdaftar", "invalid_auth": "Autentikasi tidak valid", "unknown": "Kesalahan yang tidak diharapkan" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "Kode Otorisasi", "password": "Kata Sandi", "username": "Nama Pengguna" }, - "description": "Masukkan nama pengguna dan kata sandi Anda." + "description": "SimpliSafe mengautentikasi pengguna melalui aplikasi webnya. Karena keterbatasan teknis, ada langkah manual di akhir proses ini; pastikan bahwa Anda membaca [dokumentasi] (http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) sebelum memulai.\n\nJika sudah siap, klik [di sini]({url}) untuk membuka aplikasi web SimpliSafe dan memasukkan kredensial Anda. Setelah proses selesai, kembali ke sini dan masukkan kode otorisasi dari URL aplikasi web SimpliSafe." } } }, diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index e264ec18aab..4e65fe8a057 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "\u3053\u306eSimpliSafe account\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002", "email_2fa_timed_out": "\u96fb\u5b50\u30e1\u30fc\u30eb\u306b\u3088\u308b2\u8981\u7d20\u8a8d\u8a3c\u306e\u5f85\u6a5f\u4e2d\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", - "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "wrong_account": "\u63d0\u4f9b\u3055\u308c\u305f\u30e6\u30fc\u30b6\u30fc\u8a8d\u8a3c\u60c5\u5831\u306f\u3001\u3053\u306eSimpliSafe\u30a2\u30ab\u30a6\u30f3\u30c8\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002" }, "error": { + "identifier_exists": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u767b\u9332\u3055\u308c\u3066\u3044\u307e\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, @@ -28,6 +30,7 @@ }, "user": { "data": { + "auth_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", "username": "E\u30e1\u30fc\u30eb" }, diff --git a/homeassistant/components/simplisafe/translations/sv.json b/homeassistant/components/simplisafe/translations/sv.json index 2b2d675f2b5..6a3d08b799c 100644 --- a/homeassistant/components/simplisafe/translations/sv.json +++ b/homeassistant/components/simplisafe/translations/sv.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "Det h\u00e4r SimpliSafe-kontot har redan konfigurerats." + "already_configured": "Det h\u00e4r SimpliSafe-kontot har redan konfigurerats.", + "email_2fa_timed_out": "Tidsgr\u00e4nsen tog slut i v\u00e4ntan p\u00e5 tv\u00e5faktorsautentisering", + "wrong_account": "De angivna anv\u00e4ndaruppgifterna matchar inte detta SimpliSafe-konto." + }, + "error": { + "identifier_exists": "Kontot \u00e4r redan registrerat" }, "progress": { "email_2fa": "Kontrollera din e-post f\u00f6r en verifieringsl\u00e4nk fr\u00e5n Simplisafe." @@ -15,6 +20,7 @@ }, "user": { "data": { + "auth_code": "Auktoriseringskod", "password": "L\u00f6senord", "username": "E-postadress" } diff --git a/homeassistant/components/siren/translations/sv.json b/homeassistant/components/siren/translations/sv.json new file mode 100644 index 00000000000..549bad8914b --- /dev/null +++ b/homeassistant/components/siren/translations/sv.json @@ -0,0 +1,3 @@ +{ + "title": "Siren" +} \ No newline at end of file diff --git a/homeassistant/components/slack/translations/sv.json b/homeassistant/components/slack/translations/sv.json index 34e67b311a2..eb285934e7e 100644 --- a/homeassistant/components/slack/translations/sv.json +++ b/homeassistant/components/slack/translations/sv.json @@ -12,8 +12,17 @@ "user": { "data": { "api_key": "API-nyckel", + "default_channel": "Standardkanal", + "icon": "Ikon", "username": "Anv\u00e4ndarnamn" - } + }, + "data_description": { + "api_key": "Slack API-token som ska anv\u00e4ndas f\u00f6r att skicka Slack-meddelanden.", + "default_channel": "Kanalen att posta till om ingen kanal anges n\u00e4r ett meddelande skickas.", + "icon": "Anv\u00e4nd en av Slack-emojis som en ikon f\u00f6r det angivna anv\u00e4ndarnamnet.", + "username": "Home Assistant kommer att skicka inl\u00e4gg till Slack med det angivna anv\u00e4ndarnamnet." + }, + "description": "Se dokumentationen om hur du skaffar din Slack API-nyckel." } } } diff --git a/homeassistant/components/sms/translations/sv.json b/homeassistant/components/sms/translations/sv.json new file mode 100644 index 00000000000..0020bfcfc37 --- /dev/null +++ b/homeassistant/components/sms/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "baud_speed": "Baud-hastighet" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/el.json b/homeassistant/components/soundtouch/translations/el.json index 7346aeca660..b2eea10cb86 100644 --- a/homeassistant/components/soundtouch/translations/el.json +++ b/homeassistant/components/soundtouch/translations/el.json @@ -17,5 +17,11 @@ "title": "\u0395\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03af\u03c9\u03c3\u03b7 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Bose SoundTouch \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Bose SoundTouch YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Bose SoundTouch YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/id.json b/homeassistant/components/soundtouch/translations/id.json index ce2c8f4e1a3..b5114dcb398 100644 --- a/homeassistant/components/soundtouch/translations/id.json +++ b/homeassistant/components/soundtouch/translations/id.json @@ -17,5 +17,11 @@ "title": "Konfirmasi penambahan perangkat Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Bose SoundTouch lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Bose SoundTouch dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Bose SoundTouch dalam proses penghapusan" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ja.json b/homeassistant/components/soundtouch/translations/ja.json index c4a94a23a7e..9bc5e427baf 100644 --- a/homeassistant/components/soundtouch/translations/ja.json +++ b/homeassistant/components/soundtouch/translations/ja.json @@ -17,5 +17,10 @@ "title": "Bose SoundTouch\u30c7\u30d0\u30a4\u30b9\u306e\u8ffd\u52a0\u3092\u78ba\u8a8d\u3059\u308b" } } + }, + "issues": { + "deprecated_yaml": { + "title": "Bose SoundTouch YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/nl.json b/homeassistant/components/soundtouch/translations/nl.json index 0ccc8057ac8..8328756da76 100644 --- a/homeassistant/components/soundtouch/translations/nl.json +++ b/homeassistant/components/soundtouch/translations/nl.json @@ -13,5 +13,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "De Bose SoundTouch YAML-configuratie wordt verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/pl.json b/homeassistant/components/soundtouch/translations/pl.json index 10a760e1acf..4dad61aa103 100644 --- a/homeassistant/components/soundtouch/translations/pl.json +++ b/homeassistant/components/soundtouch/translations/pl.json @@ -17,5 +17,11 @@ "title": "Potwierd\u017a dodanie urz\u0105dzenia Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Bose SoundTouch przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Bose SoundTouch zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/sv.json b/homeassistant/components/soundtouch/translations/sv.json new file mode 100644 index 00000000000..2415acd1993 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + }, + "zeroconf_confirm": { + "description": "Du h\u00e5ller p\u00e5 att l\u00e4gga till SoundTouch-enheten med namnet ` {name} ` till Home Assistant.", + "title": "Bekr\u00e4fta att du l\u00e4gger till Bose SoundTouch-enhet" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Bose SoundTouch med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Bose SoundTouch YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Bose SoundTouch YAML-konfigurationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/id.json b/homeassistant/components/spotify/translations/id.json index f75f4159a96..ef201bc638f 100644 --- a/homeassistant/components/spotify/translations/id.json +++ b/homeassistant/components/spotify/translations/id.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Proses konfigurasi Spotify lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Spotify telah dihapus" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Titik akhir API Spotify dapat dijangkau" diff --git a/homeassistant/components/spotify/translations/ja.json b/homeassistant/components/spotify/translations/ja.json index 4a65f5037dd..ae5f52b68a2 100644 --- a/homeassistant/components/spotify/translations/ja.json +++ b/homeassistant/components/spotify/translations/ja.json @@ -19,6 +19,11 @@ } } }, + "issues": { + "removed_yaml": { + "title": "Spotify YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Spotify API\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u306b\u30a2\u30af\u30bb\u30b9\u53ef\u80fd" diff --git a/homeassistant/components/spotify/translations/nl.json b/homeassistant/components/spotify/translations/nl.json index 2e478de73ab..710ec3deb7a 100644 --- a/homeassistant/components/spotify/translations/nl.json +++ b/homeassistant/components/spotify/translations/nl.json @@ -19,6 +19,11 @@ } } }, + "issues": { + "removed_yaml": { + "title": "De Spotify YAML-configuratie wordt verwijderd" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Spotify API-eindpunt is bereikbaar" diff --git a/homeassistant/components/spotify/translations/ru.json b/homeassistant/components/spotify/translations/ru.json index 35918b2634f..869d839947c 100644 --- a/homeassistant/components/spotify/translations/ru.json +++ b/homeassistant/components/spotify/translations/ru.json @@ -21,7 +21,7 @@ }, "issues": { "removed_yaml": { - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Spotify \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Spotify \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Spotify \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } }, diff --git a/homeassistant/components/spotify/translations/sv.json b/homeassistant/components/spotify/translations/sv.json index 55f94e6b717..0a64cad7a65 100644 --- a/homeassistant/components/spotify/translations/sv.json +++ b/homeassistant/components/spotify/translations/sv.json @@ -12,5 +12,11 @@ "title": "V\u00e4lj autentiseringsmetod." } } + }, + "issues": { + "removed_yaml": { + "description": "Att konfigurera Spotify med YAML har tagits bort. \n\n Din befintliga YAML-konfiguration anv\u00e4nds inte av Home Assistant. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Spotify YAML-konfigurationen har tagits bort" + } } } \ No newline at end of file diff --git a/homeassistant/components/sql/translations/sv.json b/homeassistant/components/sql/translations/sv.json index 25bd828223f..9009cac8ee3 100644 --- a/homeassistant/components/sql/translations/sv.json +++ b/homeassistant/components/sql/translations/sv.json @@ -1,9 +1,20 @@ { "config": { + "abort": { + "already_configured": "Konto \u00e4r redan konfigurerat" + }, + "error": { + "db_url_invalid": "Databasens URL \u00e4r ogiltig", + "query_invalid": "SQL fr\u00e5ga \u00e4r ogiltig" + }, "step": { "user": { "data": { + "column": "Kolumn", "name": "Namn" + }, + "data_description": { + "name": "Namn som kommer att anv\u00e4ndas f\u00f6r konfigurationsinmatning och \u00e4ven f\u00f6r sensorn." } } } diff --git a/homeassistant/components/steam_online/translations/id.json b/homeassistant/components/steam_online/translations/id.json index e944662fee1..07130a2a7da 100644 --- a/homeassistant/components/steam_online/translations/id.json +++ b/homeassistant/components/steam_online/translations/id.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Proses konfigurasi Steam lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Steam telah dihapus" + } + }, "options": { "error": { "unauthorized": "Daftar teman dibatasi: Rujuk ke dokumentasi tentang cara melihat semua teman lain" diff --git a/homeassistant/components/steam_online/translations/ja.json b/homeassistant/components/steam_online/translations/ja.json index f62fd45e767..46c3eeb7d22 100644 --- a/homeassistant/components/steam_online/translations/ja.json +++ b/homeassistant/components/steam_online/translations/ja.json @@ -24,6 +24,11 @@ } } }, + "issues": { + "removed_yaml": { + "title": "Steam YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + }, "options": { "error": { "unauthorized": "\u30d5\u30ec\u30f3\u30c9\u30ea\u30b9\u30c8\u306e\u5236\u9650: \u4ed6\u306e\u3059\u3079\u3066\u306e\u30d5\u30ec\u30f3\u30c9\u3092\u8868\u793a\u3059\u308b\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u53c2\u7167\u3057\u3066\u304f\u3060\u3055\u3044" diff --git a/homeassistant/components/steam_online/translations/nl.json b/homeassistant/components/steam_online/translations/nl.json index 5512c18ee2b..85a2b020730 100644 --- a/homeassistant/components/steam_online/translations/nl.json +++ b/homeassistant/components/steam_online/translations/nl.json @@ -24,6 +24,11 @@ } } }, + "issues": { + "removed_yaml": { + "title": "De Steam YAML-configuratie wordt verwijderd" + } + }, "options": { "error": { "unauthorized": "Vriendenlijst beperkt: raadpleeg de documentatie over hoe je alle andere vrienden kunt zien" diff --git a/homeassistant/components/steam_online/translations/ru.json b/homeassistant/components/steam_online/translations/ru.json index b69b1a72696..828b66f1dc8 100644 --- a/homeassistant/components/steam_online/translations/ru.json +++ b/homeassistant/components/steam_online/translations/ru.json @@ -26,7 +26,7 @@ }, "issues": { "removed_yaml": { - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Steam \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Steam \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Steam \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } }, diff --git a/homeassistant/components/steam_online/translations/sv.json b/homeassistant/components/steam_online/translations/sv.json index 51c39c12b35..8a087ebd3a7 100644 --- a/homeassistant/components/steam_online/translations/sv.json +++ b/homeassistant/components/steam_online/translations/sv.json @@ -1,18 +1,39 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "reauth_successful": "Omautentiseringen lyckades" + }, + "error": { + "cannot_connect": "Misslyckades att ansluta", + "invalid_account": "Ogiltigt konto-ID", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "reauth_confirm": { - "description": "Steam-integrationen m\u00e5ste autentiseras p\u00e5 nytt manuellt\n\nDu hittar din nyckel h\u00e4r: {api_key_url}" + "description": "Steam-integrationen m\u00e5ste autentiseras p\u00e5 nytt manuellt\n\nDu hittar din nyckel h\u00e4r: {api_key_url}", + "title": "Om autentisera integration" }, "user": { "data": { + "account": "Steam-konto-ID", "api_key": "API Nyckel" }, "description": "Anv\u00e4nd {account_id_url} f\u00f6r att hitta ditt Steam-konto-ID" } } }, + "issues": { + "removed_yaml": { + "description": "Konfigurering av Steam med YAML har tagits bort. \n\n Din befintliga YAML-konfiguration anv\u00e4nds inte av Home Assistant. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Steam YAML-konfigurationen har tagits bort" + } + }, "options": { + "error": { + "unauthorized": "Begr\u00e4nsad v\u00e4nlista: Se dokumentationen om hur du ser alla andra v\u00e4nner" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/switchbot/translations/id.json b/homeassistant/components/switchbot/translations/id.json index f3a9cd169ef..f7baed8c8db 100644 --- a/homeassistant/components/switchbot/translations/id.json +++ b/homeassistant/components/switchbot/translations/id.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "Jenis Switchbot yang tidak didukung.", "unknown": "Kesalahan yang tidak diharapkan" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { + "address": "Alamat perangkat", "mac": "Alamat MAC perangkat", "name": "Nama", "password": "Kata Sandi" diff --git a/homeassistant/components/switchbot/translations/ja.json b/homeassistant/components/switchbot/translations/ja.json index 91d87431774..3f9425d179a 100644 --- a/homeassistant/components/switchbot/translations/ja.json +++ b/homeassistant/components/switchbot/translations/ja.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "address": "\u30c7\u30d0\u30a4\u30b9\u30a2\u30c9\u30ec\u30b9", "mac": "\u30c7\u30d0\u30a4\u30b9\u306eMAC\u30a2\u30c9\u30ec\u30b9", "name": "\u540d\u524d", "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" diff --git a/homeassistant/components/switchbot/translations/sv.json b/homeassistant/components/switchbot/translations/sv.json new file mode 100644 index 00000000000..6b1608c9da6 --- /dev/null +++ b/homeassistant/components/switchbot/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "address": "Enhetsadress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/sv.json b/homeassistant/components/tankerkoenig/translations/sv.json index 4b9b566c7d1..f183c4ccb29 100644 --- a/homeassistant/components/tankerkoenig/translations/sv.json +++ b/homeassistant/components/tankerkoenig/translations/sv.json @@ -1,17 +1,44 @@ { "config": { "abort": { + "already_configured": "Plats \u00e4r redan konfigurerad", "reauth_successful": "\u00c5terautentisering lyckades" }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "no_stations": "Det gick inte att hitta n\u00e5gon station inom r\u00e4ckh\u00e5ll." + }, "step": { "reauth_confirm": { "data": { "api_key": "API-nyckel" } }, + "select_station": { + "data": { + "stations": "Stationer" + }, + "description": "hittade {stations_count} stationer i detta omr\u00e5det", + "title": "V\u00e4lj stationer att l\u00e4gga till" + }, "user": { "data": { - "api_key": "API-nyckel" + "api_key": "API-nyckel", + "fuel_types": "Br\u00e4nsletyper", + "location": "Plats", + "name": "Regionens namn", + "radius": "S\u00f6kradie", + "stations": "Ytterligare bensinstationer" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Uppdateringsintervall", + "stations": "Stationer" } } } diff --git a/homeassistant/components/tautulli/translations/sv.json b/homeassistant/components/tautulli/translations/sv.json index 0058ba541bf..66b23701c73 100644 --- a/homeassistant/components/tautulli/translations/sv.json +++ b/homeassistant/components/tautulli/translations/sv.json @@ -2,12 +2,19 @@ "config": { "step": { "reauth_confirm": { - "description": "F\u00f6r att hitta din API-nyckel, \u00f6ppna Tautullis webbsida och navigera till Inst\u00e4llningar och sedan till webbgr\u00e4nssnitt. API-nyckeln finns l\u00e4ngst ner p\u00e5 sidan." + "data": { + "api_key": "API-nyckel" + }, + "description": "F\u00f6r att hitta din API-nyckel, \u00f6ppna Tautullis webbsida och navigera till Inst\u00e4llningar och sedan till webbgr\u00e4nssnitt. API-nyckeln finns l\u00e4ngst ner p\u00e5 sidan.", + "title": "\u00c5terautenticera Tautulli" }, "user": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "url": "URL", + "verify_ssl": "Verifiera SSL certifikat" + }, + "description": "F\u00f6r att hitta din API-nyckel, \u00f6ppna Tautullis webbsida och navigera till Inst\u00e4llningar och sedan till webbgr\u00e4nssnittet. API-nyckeln finns l\u00e4ngst ner p\u00e5 sidan. \n\n Exempel p\u00e5 URL: ```http://192.168.0.10:8181``` med 8181 som standardport." } } } diff --git a/homeassistant/components/tod/translations/sv.json b/homeassistant/components/tod/translations/sv.json new file mode 100644 index 00000000000..79e74cf3935 --- /dev/null +++ b/homeassistant/components/tod/translations/sv.json @@ -0,0 +1,3 @@ +{ + "title": "Tider p\u00e5 dagen sensor" +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sv.json b/homeassistant/components/tomorrowio/translations/sv.json index f4a63bb449d..15498795844 100644 --- a/homeassistant/components/tomorrowio/translations/sv.json +++ b/homeassistant/components/tomorrowio/translations/sv.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "api_key": "API-nyckel" + "api_key": "API-nyckel", + "location": "Plats" } } } diff --git a/homeassistant/components/totalconnect/translations/sv.json b/homeassistant/components/totalconnect/translations/sv.json index bff6edd489e..8504a696619 100644 --- a/homeassistant/components/totalconnect/translations/sv.json +++ b/homeassistant/components/totalconnect/translations/sv.json @@ -11,5 +11,16 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "auto_bypass_low_battery": "Automatisk f\u00f6rbikoppling av l\u00e5gt batteri" + }, + "description": "Koppla automatiskt f\u00f6rbi zoner n\u00e4r de rapporterar l\u00e5gt batteri.", + "title": "TotalConnect-alternativ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/sv.json b/homeassistant/components/trafikverket_ferry/translations/sv.json index ad82dbe221d..1abd7dcb6d4 100644 --- a/homeassistant/components/trafikverket_ferry/translations/sv.json +++ b/homeassistant/components/trafikverket_ferry/translations/sv.json @@ -1,7 +1,14 @@ { "config": { + "abort": { + "already_configured": "Konto \u00e4r redan konfigurerat", + "reauth_successful": "Omautentiseringen lyckades" + }, "error": { - "invalid_auth": "Ogiltig autentisering" + "cannot_connect": "Misslyckades att ansluta", + "incorrect_api_key": "Ogiltig API-nyckel f\u00f6r valt konto", + "invalid_auth": "Ogiltig autentisering", + "invalid_route": "Kunde inte hitta rutt med tillhandah\u00e5llen information" }, "step": { "reauth_confirm": { @@ -11,7 +18,11 @@ }, "user": { "data": { - "api_key": "API-nyckel" + "api_key": "API-nyckel", + "from": "Fr\u00e5n hamn", + "time": "Tid", + "to": "Till hamnen", + "weekday": "Veckodagar" } } } diff --git a/homeassistant/components/trafikverket_train/translations/sv.json b/homeassistant/components/trafikverket_train/translations/sv.json index 5ad5b5b6db4..b6d7ecada04 100644 --- a/homeassistant/components/trafikverket_train/translations/sv.json +++ b/homeassistant/components/trafikverket_train/translations/sv.json @@ -1,5 +1,12 @@ { "config": { + "error": { + "incorrect_api_key": "Ogiltig API-nyckel f\u00f6r valt konto", + "invalid_auth": "Ogiltig autentisering", + "invalid_station": "Det gick inte att hitta en station med det angivna namnet", + "invalid_time": "Ogiltig tid har angetts", + "more_stations": "Hittade flera stationer med det angivna namnet" + }, "step": { "reauth_confirm": { "data": { @@ -8,7 +15,11 @@ }, "user": { "data": { - "api_key": "API-nyckel" + "api_key": "API-nyckel", + "from": "Fr\u00e5n station", + "time": "Tid (valfritt)", + "to": "Till station", + "weekday": "Dagar" } } } diff --git a/homeassistant/components/transmission/translations/sv.json b/homeassistant/components/transmission/translations/sv.json index 0ffbebed9f6..79b40b78ff5 100644 --- a/homeassistant/components/transmission/translations/sv.json +++ b/homeassistant/components/transmission/translations/sv.json @@ -13,6 +13,7 @@ "data": { "password": "L\u00f6senord" }, + "description": "L\u00f6senordet f\u00f6r {username} \u00e4r ogiltigt.", "title": "\u00c5terautenticera integration" }, "user": { diff --git a/homeassistant/components/ukraine_alarm/translations/sv.json b/homeassistant/components/ukraine_alarm/translations/sv.json index 4a9945525c8..cd280080774 100644 --- a/homeassistant/components/ukraine_alarm/translations/sv.json +++ b/homeassistant/components/ukraine_alarm/translations/sv.json @@ -1,8 +1,25 @@ { "config": { "abort": { + "already_configured": "Plats \u00e4r redan konfigurerad", "cannot_connect": "Det gick inte att ansluta.", + "max_regions": "Max 5 regioner kan konfigureras", + "rate_limit": "F\u00f6r mycket f\u00f6rfr\u00e5gningar", "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "community": { + "data": { + "region": "Region" + }, + "description": "Om du inte bara vill \u00f6vervaka stat och distrikt, v\u00e4lj dess specifika gemenskap" + }, + "district": { + "description": "Om du inte bara vill \u00f6vervaka staten, v\u00e4lj dess specifika distrikt" + }, + "user": { + "description": "V\u00e4lj tillst\u00e5nd att \u00f6vervaka" + } } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/sv.json b/homeassistant/components/unifi/translations/sv.json index a84558a6b58..bea79d27094 100644 --- a/homeassistant/components/unifi/translations/sv.json +++ b/homeassistant/components/unifi/translations/sv.json @@ -33,10 +33,12 @@ "device_tracker": { "data": { "detection_time": "Tid i sekunder fr\u00e5n senast sett tills den anses borta", + "ssid_filter": "V\u00e4lj SSID att sp\u00e5ra tr\u00e5dl\u00f6sa klienter p\u00e5", "track_clients": "Sp\u00e5ra n\u00e4tverksklienter", "track_devices": "Sp\u00e5ra n\u00e4tverksenheter (Ubiquiti-enheter)", "track_wired_clients": "Inkludera tr\u00e5dbundna n\u00e4tverksklienter" }, + "description": "Konfigurera enhetssp\u00e5rning", "title": "UniFi-inst\u00e4llningar 1/3" }, "init": { @@ -52,6 +54,7 @@ "data": { "allow_bandwidth_sensors": "Skapa bandbreddsanv\u00e4ndningssensorer f\u00f6r n\u00e4tverksklienter" }, + "description": "Konfigurera statistiksensorer", "title": "UniFi-inst\u00e4llningar 2/3" } } diff --git a/homeassistant/components/uscis/translations/nl.json b/homeassistant/components/uscis/translations/nl.json new file mode 100644 index 00000000000..b4f2be41ed0 --- /dev/null +++ b/homeassistant/components/uscis/translations/nl.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "De USCIS-integratie wordt verwijderd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/sv.json b/homeassistant/components/uscis/translations/sv.json new file mode 100644 index 00000000000..a139ce02491 --- /dev/null +++ b/homeassistant/components/uscis/translations/sv.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integrationen av US Citizenship and Immigration Services (USCIS) v\u00e4ntar p\u00e5 borttagning fr\u00e5n Home Assistant och kommer inte l\u00e4ngre att vara tillg\u00e4nglig fr\u00e5n och med Home Assistant 2022.10. \n\n Integrationen tas bort eftersom den f\u00f6rlitar sig p\u00e5 webbskrapning, vilket inte \u00e4r till\u00e5tet. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "USCIS-integrationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/sv.json b/homeassistant/components/verisure/translations/sv.json index 3d3dbdb8bda..1113a387339 100644 --- a/homeassistant/components/verisure/translations/sv.json +++ b/homeassistant/components/verisure/translations/sv.json @@ -1,14 +1,27 @@ { "config": { "error": { - "unknown": "Ov\u00e4ntat fel" + "unknown": "Ov\u00e4ntat fel", + "unknown_mfa": "Ett ok\u00e4nt fel har intr\u00e4ffat under konfiguration av MFA" }, "step": { + "mfa": { + "data": { + "code": "Verifieringskod", + "description": "Ditt konto har 2-stegsverifiering aktiverat. Skriv in verifieringskoden Verisure skickar till dig." + } + }, "reauth_confirm": { "data": { "password": "L\u00f6senord" } }, + "reauth_mfa": { + "data": { + "code": "Verifieringskod", + "description": "Ditt konto har 2-stegsverifiering aktiverat. Skriv in verifieringskoden Verisure skickar till dig." + } + }, "user": { "data": { "password": "L\u00f6senord" diff --git a/homeassistant/components/vulcan/translations/sv.json b/homeassistant/components/vulcan/translations/sv.json new file mode 100644 index 00000000000..f62155f1fd3 --- /dev/null +++ b/homeassistant/components/vulcan/translations/sv.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "no_matching_entries": "Inga matchande poster hittades, anv\u00e4nd ett annat konto eller ta bort integration med f\u00f6r\u00e5ldrad student.." + }, + "step": { + "add_next_config_entry": { + "data": { + "use_saved_credentials": "Anv\u00e4nd sparade autentiseringsuppgifter" + }, + "description": "L\u00e4gg till ytterligare en elev." + }, + "auth": { + "data": { + "pin": "Knappn\u00e5l", + "region": "Symbol", + "token": "Token" + }, + "description": "Logga in p\u00e5 ditt Vulcan-konto med registreringssidan f\u00f6r mobilappen." + }, + "reauth_confirm": { + "data": { + "pin": "Knappn\u00e5l", + "region": "Symbol", + "token": "Token" + }, + "description": "Logga in p\u00e5 ditt Vulcan-konto med registreringssidan f\u00f6r mobilappen." + }, + "select_saved_credentials": { + "data": { + "credentials": "Logga in" + }, + "description": "V\u00e4lj sparade autentiseringsuppgifter." + }, + "select_student": { + "data": { + "student_name": "V\u00e4lj elev" + }, + "description": "V\u00e4lj elev, du kan l\u00e4gga till fler elever genom att l\u00e4gga till integration igen." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/sv.json b/homeassistant/components/withings/translations/sv.json index c5b6b0fddab..4adb00bfad5 100644 --- a/homeassistant/components/withings/translations/sv.json +++ b/homeassistant/components/withings/translations/sv.json @@ -17,6 +17,10 @@ }, "description": "Vilken profil valde du p\u00e5 Withings webbplats? Det \u00e4r viktigt att profilerna matchar, annars kommer data att vara felm\u00e4rkta.", "title": "Anv\u00e4ndarprofil." + }, + "reauth_confirm": { + "description": "Profilen \" {profile} \" m\u00e5ste autentiseras p\u00e5 nytt f\u00f6r att kunna forts\u00e4tta att ta emot Withings-data.", + "title": "G\u00f6r om autentiseringen f\u00f6r integrationen" } } } diff --git a/homeassistant/components/ws66i/translations/sv.json b/homeassistant/components/ws66i/translations/sv.json new file mode 100644 index 00000000000..9daf43b9987 --- /dev/null +++ b/homeassistant/components/ws66i/translations/sv.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "title": "Anslut till enheten" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Namn p\u00e5 k\u00e4lla #1", + "source_2": "Namn p\u00e5 k\u00e4lla #2", + "source_3": "Namn p\u00e5 k\u00e4lla #3", + "source_4": "Namn p\u00e5 k\u00e4lla #4", + "source_5": "Namn p\u00e5 k\u00e4lla #5", + "source_6": "Namn p\u00e5 k\u00e4lla #6" + }, + "title": "Konfigurera k\u00e4llor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/el.json b/homeassistant/components/xbox/translations/el.json index 93e620b6b5e..4accbbee1ac 100644 --- a/homeassistant/components/xbox/translations/el.json +++ b/homeassistant/components/xbox/translations/el.json @@ -13,5 +13,11 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Xbox \u03c3\u03c4\u03bf configuration.yaml \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant 2022.9. \n\n \u03a4\u03b1 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03bd\u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 OAuth \u03ba\u03b1\u03b9 \u03bf\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Xbox YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/id.json b/homeassistant/components/xbox/translations/id.json index ed8106b0144..3df7f3ee8f2 100644 --- a/homeassistant/components/xbox/translations/id.json +++ b/homeassistant/components/xbox/translations/id.json @@ -13,5 +13,11 @@ "title": "Pilih Metode Autentikasi" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Xbox di configuration.yaml sedang dihapus di Home Assistant 2022.9. \n\nKredensial Aplikasi OAuth yang Anda dan setelan akses telah diimpor ke antarmuka secara otomatis. Hapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Xbox dalam proses penghapusan" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/ja.json b/homeassistant/components/xbox/translations/ja.json index 2d1e95019b7..f1c31b6c64c 100644 --- a/homeassistant/components/xbox/translations/ja.json +++ b/homeassistant/components/xbox/translations/ja.json @@ -13,5 +13,10 @@ "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" } } + }, + "issues": { + "deprecated_yaml": { + "title": "Xbox YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/nl.json b/homeassistant/components/xbox/translations/nl.json index 9d68863cc27..3544fe9f90e 100644 --- a/homeassistant/components/xbox/translations/nl.json +++ b/homeassistant/components/xbox/translations/nl.json @@ -13,5 +13,10 @@ "title": "Kies een authenticatie methode" } } + }, + "issues": { + "deprecated_yaml": { + "title": "De Xbox YAML-configuratie wordt verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/pl.json b/homeassistant/components/xbox/translations/pl.json index b4a5c4d8f38..e035422de62 100644 --- a/homeassistant/components/xbox/translations/pl.json +++ b/homeassistant/components/xbox/translations/pl.json @@ -13,5 +13,11 @@ "title": "Wybierz metod\u0119 uwierzytelniania" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Xbox w configuration.yaml zostanie usuni\u0119ta w Home Assistant 2022.9. \n\nTwoje istniej\u0105ce po\u015bwiadczenia aplikacji OAuth i ustawienia dost\u0119pu zosta\u0142y automatycznie zaimportowane do interfejsu u\u017cytkownika. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Xbox zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/sv.json b/homeassistant/components/xbox/translations/sv.json new file mode 100644 index 00000000000..525e91ded0e --- /dev/null +++ b/homeassistant/components/xbox/translations/sv.json @@ -0,0 +1,8 @@ +{ + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Xbox i configuration.yaml tas bort i Home Assistant 2022.9. \n\n Dina befintliga OAuth-applikationsuppgifter och \u00e5tkomstinst\u00e4llningar har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Xbox YAML-konfigurationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/id.json b/homeassistant/components/xiaomi_ble/translations/id.json index 07426a0e290..ea45a7ba9c3 100644 --- a/homeassistant/components/xiaomi_ble/translations/id.json +++ b/homeassistant/components/xiaomi_ble/translations/id.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Perangkat sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", + "decryption_failed": "Bindkey yang disediakan tidak berfungsi, data sensor tidak dapat didekripsi. Silakan periksa dan coba lagi.", + "expected_24_characters": "Diharapkan bindkey berupa 24 karakter heksadesimal.", + "expected_32_characters": "Diharapkan bindkey berupa 32 karakter heksadesimal.", "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "Ingin menyiapkan {name}?" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Data sensor yang disiarkan oleh sensor telah dienkripsi. Untuk mendekripsinya, diperlukan 32 karakter bindkey heksadesimal ." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Data sensor yang disiarkan oleh sensor telah dienkripsi. Untuk mendekripsinya, diperlukan 24 karakter bindkey heksadesimal ." + }, "user": { "data": { "address": "Perangkat" diff --git a/homeassistant/components/xiaomi_ble/translations/ja.json b/homeassistant/components/xiaomi_ble/translations/ja.json index 38f862bd2f6..d15f89bf9d3 100644 --- a/homeassistant/components/xiaomi_ble/translations/ja.json +++ b/homeassistant/components/xiaomi_ble/translations/ja.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "decryption_failed": "\u63d0\u4f9b\u3055\u308c\u305f\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u6a5f\u80fd\u305b\u305a\u3001\u30bb\u30f3\u30b5\u30fc \u30c7\u30fc\u30bf\u3092\u5fa9\u53f7\u5316\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u78ba\u8a8d\u306e\u4e0a\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "expected_24_characters": "24\u6587\u5b57\u306716\u9032\u6570\u306a\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002", + "expected_32_characters": "32\u6587\u5b57\u304b\u3089\u306a\u308b16\u9032\u6570\u306e\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, "flow_title": "{name}", @@ -10,6 +13,18 @@ "bluetooth_confirm": { "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc" + }, + "description": "\u30bb\u30f3\u30b5\u30fc\u304b\u3089\u30d6\u30ed\u30fc\u30c9\u30ad\u30e3\u30b9\u30c8\u3055\u308c\u308b\u30bb\u30f3\u30b5\u30fc\u30c7\u30fc\u30bf\u306f\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5fa9\u53f7\u5316\u3059\u308b\u306b\u306f\u300116\u9032\u6570\u306732\u6587\u5b57\u306a\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002" + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc" + }, + "description": "\u30bb\u30f3\u30b5\u30fc\u304b\u3089\u30d6\u30ed\u30fc\u30c9\u30ad\u30e3\u30b9\u30c8\u3055\u308c\u308b\u30bb\u30f3\u30b5\u30fc\u30c7\u30fc\u30bf\u306f\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5fa9\u53f7\u5316\u3059\u308b\u306b\u306f\u300116\u9032\u6570\u306724\u6587\u5b57\u306a\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002" + }, "user": { "data": { "address": "\u30c7\u30d0\u30a4\u30b9" diff --git a/homeassistant/components/xiaomi_ble/translations/nl.json b/homeassistant/components/xiaomi_ble/translations/nl.json new file mode 100644 index 00000000000..a46f954fe5f --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/sv.json b/homeassistant/components/xiaomi_ble/translations/sv.json new file mode 100644 index 00000000000..68373aca702 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/sv.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfiguration redan ig\u00e5ng", + "decryption_failed": "Den tillhandah\u00e5llna bindningsnyckeln fungerade inte, sensordata kunde inte dekrypteras. Kontrollera den och f\u00f6rs\u00f6k igen.", + "expected_24_characters": "F\u00f6rv\u00e4ntade ett hexadecimalt bindningsnyckel med 24 tecken.", + "expected_32_characters": "F\u00f6rv\u00e4ntade ett hexadecimalt bindningsnyckel med 32 tecken.", + "no_devices_found": "Inga enheter hittades p\u00e5 n\u00e4tverket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vill du s\u00e4tta upp {name}?" + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindningsnyckel" + }, + "description": "De sensordata som s\u00e4nds av sensorn \u00e4r krypterade. F\u00f6r att dekryptera dem beh\u00f6ver vi en hexadecimal bindningsnyckel med 32 tecken." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindningsnyckel" + }, + "description": "De sensordata som s\u00e4nds av sensorn \u00e4r krypterade. F\u00f6r att dekryptera dem beh\u00f6ver vi en hexadecimal bindningsnyckel med 24 tecken." + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "V\u00e4lj en enhet att s\u00e4tta upp" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/sv.json b/homeassistant/components/yolink/translations/sv.json index 3c6db089e0e..07392481a29 100644 --- a/homeassistant/components/yolink/translations/sv.json +++ b/homeassistant/components/yolink/translations/sv.json @@ -17,6 +17,7 @@ "title": "V\u00e4lj autentiseringsmetod" }, "reauth_confirm": { + "description": "Yolink-integrationen m\u00e5ste autentisera ditt konto igen", "title": "\u00c5terautenticera integration" } } diff --git a/homeassistant/components/zha/translations/el.json b/homeassistant/components/zha/translations/el.json index e0e063df426..85bb3ccce6d 100644 --- a/homeassistant/components/zha/translations/el.json +++ b/homeassistant/components/zha/translations/el.json @@ -46,11 +46,13 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c3\u03c5\u03bd\u03b1\u03b3\u03b5\u03c1\u03bc\u03bf\u03cd" }, "zha_options": { + "always_prefer_xy_color_mode": "\u039d\u03b1 \u03c0\u03c1\u03bf\u03c4\u03b9\u03bc\u03ac\u03c4\u03b1\u03b9 \u03c0\u03ac\u03bd\u03c4\u03b1 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c7\u03c1\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 XY", "consider_unavailable_battery": "\u0398\u03b5\u03c9\u03c1\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03bc\u03b5 \u03bc\u03c0\u03b1\u03c4\u03b1\u03c1\u03af\u03b1 \u03c9\u03c2 \u03bc\u03b7 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b5\u03c2 \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)", "consider_unavailable_mains": "\u0398\u03b5\u03c9\u03c1\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c0\u03bf\u03c5 \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03c9\u03c2 \u03bc\u03b7 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b5\u03c2 \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)", "default_light_transition": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c6\u03c9\u03c4\u03cc\u03c2 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)", "enable_identify_on_join": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03c6\u03ad \u03b1\u03bd\u03b1\u03b3\u03bd\u03ce\u03c1\u03b9\u03c3\u03b7\u03c2 \u03cc\u03c4\u03b1\u03bd \u03bf\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c5\u03bd\u03b4\u03ad\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", "enhanced_light_transition": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b2\u03b5\u03bb\u03c4\u03b9\u03c9\u03bc\u03ad\u03bd\u03b7 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7 \u03c7\u03c1\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c6\u03c9\u03c4\u03cc\u03c2/\u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03b1\u03c0\u03cc \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2", + "light_transitioning_flag": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b2\u03b5\u03bb\u03c4\u03b9\u03c9\u03bc\u03ad\u03bd\u03bf\u03c5 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd \u03c6\u03c9\u03c4\u03b5\u03b9\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7 \u03c6\u03c9\u03c4\u03cc\u03c2", "title": "\u039a\u03b1\u03b8\u03bf\u03bb\u03b9\u03ba\u03ad\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2" } }, diff --git a/homeassistant/components/zha/translations/id.json b/homeassistant/components/zha/translations/id.json index 63eee2f376b..ef91a4227c8 100644 --- a/homeassistant/components/zha/translations/id.json +++ b/homeassistant/components/zha/translations/id.json @@ -46,11 +46,13 @@ "title": "Opsi Panel Kontrol Alarm" }, "zha_options": { + "always_prefer_xy_color_mode": "Selalu pilih mode warna XY", "consider_unavailable_battery": "Anggap perangkat bertenaga baterai sebagai tidak tersedia setelah (detik)", "consider_unavailable_mains": "Anggap perangkat bertenaga listrik sebagai tidak tersedia setelah (detik)", "default_light_transition": "Waktu transisi lampu default (detik)", "enable_identify_on_join": "Aktifkan efek identifikasi saat perangkat bergabung dengan jaringan", "enhanced_light_transition": "Aktifkan versi canggih untuk transisi warna/suhu cahaya dari keadaan tidak aktif", + "light_transitioning_flag": "Aktifkan penggeser kecerahan yang lebih canggih pada waktu transisi lampu", "title": "Opsi Global" } }, diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 14ba0b9280f..25d5a5bd9f6 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -46,11 +46,13 @@ "title": "\u30a2\u30e9\u30fc\u30e0 \u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" }, "zha_options": { + "always_prefer_xy_color_mode": "\u5e38\u306bXY\u30ab\u30e9\u30fc\u30e2\u30fc\u30c9\u3092\u512a\u5148", "consider_unavailable_battery": "(\u79d2)\u5f8c\u306b\u30d0\u30c3\u30c6\u30ea\u30fc\u99c6\u52d5\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3068\u898b\u306a\u3059", "consider_unavailable_mains": "(\u79d2)\u5f8c\u306b\u4e3b\u96fb\u6e90\u304c\u30c7\u30d0\u30a4\u30b9\u304b\u3089\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u308b\u3068\u898b\u306a\u3059", "default_light_transition": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30e9\u30a4\u30c8\u9077\u79fb\u6642\u9593(\u79d2)", "enable_identify_on_join": "\u30c7\u30d0\u30a4\u30b9\u304c\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u53c2\u52a0\u3059\u308b\u969b\u306b\u3001\u8b58\u5225\u52b9\u679c\u3092\u6709\u52b9\u306b\u3059\u308b", "enhanced_light_transition": "\u30aa\u30d5\u72b6\u614b\u304b\u3089\u3001\u30a8\u30f3\u30cf\u30f3\u30b9\u30c9\u30e9\u30a4\u30c8\u30ab\u30e9\u30fc/\u8272\u6e29\u5ea6\u3078\u306e\u9077\u79fb\u3092\u6709\u52b9\u306b\u3057\u307e\u3059", + "light_transitioning_flag": "\u5149\u6e90\u79fb\u884c\u6642\u306e\u8f1d\u5ea6\u30b9\u30e9\u30a4\u30c0\u30fc\u306e\u62e1\u5f35\u3092\u6709\u52b9\u306b\u3059\u308b", "title": "\u30b0\u30ed\u30fc\u30d0\u30eb\u30aa\u30d7\u30b7\u30e7\u30f3" } }, diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index 83ba818087a..6eae6313424 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -46,11 +46,13 @@ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0430\u043d\u0435\u043b\u0435\u0439 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439" }, "zha_options": { + "always_prefer_xy_color_mode": "\u0412\u0441\u0435\u0433\u0434\u0430 \u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0438\u0442\u0430\u0442\u044c \u0446\u0432\u0435\u0442\u043e\u0432\u043e\u0439 \u0440\u0435\u0436\u0438\u043c XY", "consider_unavailable_battery": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u0430\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u044b\u043c \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "consider_unavailable_mains": "\u0421\u0447\u0438\u0442\u0430\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u043f\u0438\u0442\u0430\u043d\u0438\u0435\u043c \u043e\u0442 \u0441\u0435\u0442\u0438 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c\u0438 \u0447\u0435\u0440\u0435\u0437 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "default_light_transition": "\u0412\u0440\u0435\u043c\u044f \u043f\u043b\u0430\u0432\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u0441\u0432\u0435\u0442\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "enable_identify_on_join": "\u042d\u0444\u0444\u0435\u043a\u0442 \u0434\u043b\u044f \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043a \u0441\u0435\u0442\u0438", "enhanced_light_transition": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0443\u043b\u0443\u0447\u0448\u0435\u043d\u043d\u044b\u0439 \u043f\u0435\u0440\u0435\u0445\u043e\u0434 \u0446\u0432\u0435\u0442\u0430/\u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b \u0441\u0432\u0435\u0442\u0430 \u0438\u0437 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", + "light_transitioning_flag": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0443\u043b\u0443\u0447\u0448\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u043b\u0437\u0443\u043d\u043e\u043a \u044f\u0440\u043a\u043e\u0441\u0442\u0438 \u043f\u0440\u0438 \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0435 \u0441\u0432\u0435\u0442\u0430", "title": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" } }, diff --git a/homeassistant/components/zha/translations/sv.json b/homeassistant/components/zha/translations/sv.json index 7bbfe8dcfb1..8917cbe8cd6 100644 --- a/homeassistant/components/zha/translations/sv.json +++ b/homeassistant/components/zha/translations/sv.json @@ -31,6 +31,13 @@ } } }, + "config_panel": { + "zha_options": { + "always_prefer_xy_color_mode": "F\u00f6redrar alltid XY-f\u00e4rgl\u00e4ge", + "enhanced_light_transition": "Aktivera f\u00f6rb\u00e4ttrad ljusf\u00e4rg/temperatur\u00f6verg\u00e5ng fr\u00e5n ett avst\u00e4ngt l\u00e4ge", + "light_transitioning_flag": "Aktivera f\u00f6rb\u00e4ttrad ljusstyrka vid ljus\u00f6verg\u00e5ng" + } + }, "device_automation": { "action_type": { "squawk": "Kraxa", diff --git a/homeassistant/components/zwave_js/translations/sv.json b/homeassistant/components/zwave_js/translations/sv.json index 907924e08ac..386b306e1b8 100644 --- a/homeassistant/components/zwave_js/translations/sv.json +++ b/homeassistant/components/zwave_js/translations/sv.json @@ -1,4 +1,11 @@ { + "config": { + "step": { + "zeroconf_confirm": { + "title": "Uppt\u00e4ckte Z-Wave JS Server" + } + } + }, "device_automation": { "condition_type": { "value": "Nuvarande v\u00e4rde f\u00f6r ett Z-Wave v\u00e4rde" From 80a9659524a47868070a16cdfa7f654fba7f6bb7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Jul 2022 14:53:33 -1000 Subject: [PATCH 2971/3516] Update to bleak 0.15 (#75941) --- .../components/bluetooth/__init__.py | 29 ++++++++------- .../components/bluetooth/manifest.json | 2 +- homeassistant/components/bluetooth/models.py | 34 ++++++++++++----- .../bluetooth/passive_update_coordinator.py | 8 ++-- .../bluetooth/passive_update_processor.py | 17 +++++---- .../bluetooth/update_coordinator.py | 9 +++-- homeassistant/components/bluetooth/usage.py | 5 ++- .../bluetooth_le_tracker/device_tracker.py | 18 ++++----- .../components/govee_ble/__init__.py | 2 + .../components/govee_ble/config_flow.py | 6 +-- .../homekit_controller/config_flow.py | 7 +++- .../components/homekit_controller/utils.py | 2 +- homeassistant/components/inkbird/__init__.py | 5 +-- .../components/inkbird/config_flow.py | 6 +-- homeassistant/components/moat/__init__.py | 5 +-- homeassistant/components/moat/config_flow.py | 6 +-- .../components/sensorpush/__init__.py | 5 +-- .../components/sensorpush/config_flow.py | 6 +-- .../components/switchbot/config_flow.py | 8 ++-- .../components/switchbot/coordinator.py | 11 +++--- .../components/xiaomi_ble/__init__.py | 5 +-- .../components/xiaomi_ble/config_flow.py | 20 ++++++---- homeassistant/config_entries.py | 4 +- homeassistant/helpers/config_entry_flow.py | 4 +- .../helpers/service_info/bluetooth.py | 1 + homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/test_init.py | 21 +++++++++-- .../test_passive_update_coordinator.py | 29 ++++++++++----- .../test_passive_update_processor.py | 37 ++++++++++--------- .../test_device_tracker.py | 23 +++++++++--- tests/components/fjaraskupan/conftest.py | 6 +++ tests/components/govee_ble/test_sensor.py | 2 +- tests/components/inkbird/test_sensor.py | 2 +- tests/components/moat/test_sensor.py | 2 +- tests/components/sensorpush/test_sensor.py | 2 +- .../components/xiaomi_ble/test_config_flow.py | 8 +++- tests/components/xiaomi_ble/test_sensor.py | 12 +++--- 39 files changed, 223 insertions(+), 152 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index eb8e31baef0..9adaac84333 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum import logging -from typing import Final, Union +from typing import Final import async_timeout from bleak import BleakError @@ -96,12 +96,8 @@ SCANNING_MODE_TO_BLEAK = { BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") -BluetoothCallback = Callable[ - [Union[BluetoothServiceInfoBleak, BluetoothServiceInfo], BluetoothChange], None -] -ProcessAdvertisementCallback = Callable[ - [Union[BluetoothServiceInfoBleak, BluetoothServiceInfo]], bool -] +BluetoothCallback = Callable[[BluetoothServiceInfoBleak, BluetoothChange], None] +ProcessAdvertisementCallback = Callable[[BluetoothServiceInfoBleak], bool] @hass_callback @@ -157,9 +153,15 @@ def async_register_callback( hass: HomeAssistant, callback: BluetoothCallback, match_dict: BluetoothCallbackMatcher | None, + mode: BluetoothScanningMode, ) -> Callable[[], None]: """Register to receive a callback on bluetooth change. + mode is currently not used as we only support active scanning. + Passive scanning will be available in the future. The flag + is required to be present to avoid a future breaking change + when we support passive scanning. + Returns a callback that can be used to cancel the registration. """ manager: BluetoothManager = hass.data[DOMAIN] @@ -170,19 +172,20 @@ async def async_process_advertisements( hass: HomeAssistant, callback: ProcessAdvertisementCallback, match_dict: BluetoothCallbackMatcher, + mode: BluetoothScanningMode, timeout: int, -) -> BluetoothServiceInfo: +) -> BluetoothServiceInfoBleak: """Process advertisements until callback returns true or timeout expires.""" - done: Future[BluetoothServiceInfo] = Future() + done: Future[BluetoothServiceInfoBleak] = Future() @hass_callback def _async_discovered_device( - service_info: BluetoothServiceInfo, change: BluetoothChange + service_info: BluetoothServiceInfoBleak, change: BluetoothChange ) -> None: if callback(service_info): done.set_result(service_info) - unload = async_register_callback(hass, _async_discovered_device, match_dict) + unload = async_register_callback(hass, _async_discovered_device, match_dict, mode) try: async with async_timeout.timeout(timeout): @@ -333,7 +336,7 @@ class BluetoothManager: ) try: async with async_timeout.timeout(START_TIMEOUT): - await self.scanner.start() + await self.scanner.start() # type: ignore[no-untyped-call] except asyncio.TimeoutError as ex: self._cancel_device_detected() raise ConfigEntryNotReady( @@ -500,7 +503,7 @@ class BluetoothManager: self._cancel_unavailable_tracking = None if self.scanner: try: - await self.scanner.stop() + await self.scanner.stop() # type: ignore[no-untyped-call] except BleakError as ex: # This is not fatal, and they may want to reload # the config entry to restart the scanner if they diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index c6ca8b11400..f215e8fa161 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.14.3", "bluetooth-adapters==0.1.2"], + "requirements": ["bleak==0.15.0", "bluetooth-adapters==0.1.2"], "codeowners": ["@bdraco"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 408e0698879..6f814c7b66b 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio import contextlib import logging -from typing import Any, Final, cast +from typing import Any, Final from bleak import BleakScanner from bleak.backends.device import BLEDevice @@ -32,7 +32,7 @@ def _dispatch_callback( """Dispatch the callback.""" if not callback: # Callback destroyed right before being called, ignore - return + return # type: ignore[unreachable] if (uuids := filters.get(FILTER_UUIDS)) and not uuids.intersection( advertisement_data.service_uuids @@ -45,7 +45,7 @@ def _dispatch_callback( _LOGGER.exception("Error in callback: %s", callback) -class HaBleakScanner(BleakScanner): # type: ignore[misc] +class HaBleakScanner(BleakScanner): """BleakScanner that cannot be stopped.""" def __init__( # pylint: disable=super-init-not-called @@ -106,16 +106,29 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] _dispatch_callback(*callback_filters, device, advertisement_data) -class HaBleakScannerWrapper(BaseBleakScanner): # type: ignore[misc] +class HaBleakScannerWrapper(BaseBleakScanner): """A wrapper that uses the single instance.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: + def __init__( + self, + *args: Any, + detection_callback: AdvertisementDataCallback | None = None, + service_uuids: list[str] | None = None, + **kwargs: Any, + ) -> None: """Initialize the BleakScanner.""" self._detection_cancel: CALLBACK_TYPE | None = None self._mapped_filters: dict[str, set[str]] = {} self._adv_data_callback: AdvertisementDataCallback | None = None - self._map_filters(*args, **kwargs) - super().__init__(*args, **kwargs) + remapped_kwargs = { + "detection_callback": detection_callback, + "service_uuids": service_uuids or [], + **kwargs, + } + self._map_filters(*args, **remapped_kwargs) + super().__init__( + detection_callback=detection_callback, service_uuids=service_uuids or [] + ) async def stop(self, *args: Any, **kwargs: Any) -> None: """Stop scanning for devices.""" @@ -153,9 +166,11 @@ class HaBleakScannerWrapper(BaseBleakScanner): # type: ignore[misc] def discovered_devices(self) -> list[BLEDevice]: """Return a list of discovered devices.""" assert HA_BLEAK_SCANNER is not None - return cast(list[BLEDevice], HA_BLEAK_SCANNER.discovered_devices) + return HA_BLEAK_SCANNER.discovered_devices - def register_detection_callback(self, callback: AdvertisementDataCallback) -> None: + def register_detection_callback( + self, callback: AdvertisementDataCallback | None + ) -> None: """Register a callback that is called when a device is discovered or has a property changed. This method takes the callback and registers it with the long running @@ -171,6 +186,7 @@ class HaBleakScannerWrapper(BaseBleakScanner): # type: ignore[misc] self._cancel_callback() super().register_detection_callback(self._adv_data_callback) assert HA_BLEAK_SCANNER is not None + assert self._callback is not None self._detection_cancel = HA_BLEAK_SCANNER.async_register_callback( self._callback, self._mapped_filters ) diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 97e7ddc49ee..31a6b065830 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -6,10 +6,9 @@ import logging from typing import Any from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import BluetoothChange +from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .update_coordinator import BasePassiveBluetoothCoordinator @@ -25,9 +24,10 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): hass: HomeAssistant, logger: logging.Logger, address: str, + mode: BluetoothScanningMode, ) -> None: """Initialize PassiveBluetoothDataUpdateCoordinator.""" - super().__init__(hass, logger, address) + super().__init__(hass, logger, address, mode) self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {} @callback @@ -65,7 +65,7 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): @callback def _async_handle_bluetooth_event( self, - service_info: BluetoothServiceInfo, + service_info: BluetoothServiceInfoBleak, change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 43467701879..1f2047c02cb 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -6,14 +6,12 @@ import dataclasses import logging from typing import Any, Generic, TypeVar -from home_assistant_bluetooth import BluetoothServiceInfo - from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BluetoothChange +from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .const import DOMAIN from .update_coordinator import BasePassiveBluetoothCoordinator @@ -62,9 +60,10 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): hass: HomeAssistant, logger: logging.Logger, address: str, + mode: BluetoothScanningMode, ) -> None: """Initialize the coordinator.""" - super().__init__(hass, logger, address) + super().__init__(hass, logger, address, mode) self._processors: list[PassiveBluetoothDataProcessor] = [] @callback @@ -92,7 +91,7 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): @callback def _async_handle_bluetooth_event( self, - service_info: BluetoothServiceInfo, + service_info: BluetoothServiceInfoBleak, change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" @@ -122,7 +121,7 @@ class PassiveBluetoothDataProcessor(Generic[_T]): The processor will call the update_method every time the bluetooth device receives a new advertisement data from the coordinator with the following signature: - update_method(service_info: BluetoothServiceInfo) -> PassiveBluetoothDataUpdate + update_method(service_info: BluetoothServiceInfoBleak) -> PassiveBluetoothDataUpdate As the size of each advertisement is limited, the update_method should return a PassiveBluetoothDataUpdate object that contains only data that @@ -135,7 +134,9 @@ class PassiveBluetoothDataProcessor(Generic[_T]): def __init__( self, - update_method: Callable[[BluetoothServiceInfo], PassiveBluetoothDataUpdate[_T]], + update_method: Callable[ + [BluetoothServiceInfoBleak], PassiveBluetoothDataUpdate[_T] + ], ) -> None: """Initialize the coordinator.""" self.coordinator: PassiveBluetoothProcessorCoordinator @@ -241,7 +242,7 @@ class PassiveBluetoothDataProcessor(Generic[_T]): @callback def async_handle_bluetooth_event( self, - service_info: BluetoothServiceInfo, + service_info: BluetoothServiceInfoBleak, change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" diff --git a/homeassistant/components/bluetooth/update_coordinator.py b/homeassistant/components/bluetooth/update_coordinator.py index b1cb2de1453..d0f38ce32c6 100644 --- a/homeassistant/components/bluetooth/update_coordinator.py +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -4,13 +4,13 @@ from __future__ import annotations import logging import time -from home_assistant_bluetooth import BluetoothServiceInfo - from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from . import ( BluetoothCallbackMatcher, BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfoBleak, async_register_callback, async_track_unavailable, ) @@ -27,6 +27,7 @@ class BasePassiveBluetoothCoordinator: hass: HomeAssistant, logger: logging.Logger, address: str, + mode: BluetoothScanningMode, ) -> None: """Initialize the coordinator.""" self.hass = hass @@ -36,6 +37,7 @@ class BasePassiveBluetoothCoordinator: self._cancel_track_unavailable: CALLBACK_TYPE | None = None self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None self._present = False + self.mode = mode self.last_seen = 0.0 @callback @@ -61,6 +63,7 @@ class BasePassiveBluetoothCoordinator: self.hass, self._async_handle_bluetooth_event, BluetoothCallbackMatcher(address=self.address), + self.mode, ) self._cancel_track_unavailable = async_track_unavailable( self.hass, @@ -86,7 +89,7 @@ class BasePassiveBluetoothCoordinator: @callback def _async_handle_bluetooth_event( self, - service_info: BluetoothServiceInfo, + service_info: BluetoothServiceInfoBleak, change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py index da5d062a36f..b3a6783cf30 100644 --- a/homeassistant/components/bluetooth/usage.py +++ b/homeassistant/components/bluetooth/usage.py @@ -1,4 +1,5 @@ """bluetooth usage utility to handle multiple instances.""" + from __future__ import annotations import bleak @@ -10,9 +11,9 @@ ORIGINAL_BLEAK_SCANNER = bleak.BleakScanner def install_multiple_bleak_catcher() -> None: """Wrap the bleak classes to return the shared instance if multiple instances are detected.""" - bleak.BleakScanner = HaBleakScannerWrapper + bleak.BleakScanner = HaBleakScannerWrapper # type: ignore[misc, assignment] def uninstall_multiple_bleak_catcher() -> None: """Unwrap the bleak classes.""" - bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER + bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER # type: ignore[misc] diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index fa55c22f994..6ba33e506cc 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio from datetime import datetime, timedelta import logging -from typing import TYPE_CHECKING from uuid import UUID from bleak import BleakClient, BleakError @@ -31,9 +30,6 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -if TYPE_CHECKING: - from bleak.backends.device import BLEDevice - _LOGGER = logging.getLogger(__name__) # Base UUID: 00000000-0000-1000-8000-00805F9B34FB @@ -139,15 +135,12 @@ async def async_setup_scanner( # noqa: C901 async def _async_see_update_ble_battery( mac: str, now: datetime, - service_info: bluetooth.BluetoothServiceInfo, + service_info: bluetooth.BluetoothServiceInfoBleak, ) -> None: """Lookup Bluetooth LE devices and update status.""" battery = None - ble_device: BLEDevice | str = ( - bluetooth.async_ble_device_from_address(hass, mac) or mac - ) try: - async with BleakClient(ble_device) as client: + async with BleakClient(service_info.device) as client: bat_char = await client.read_gatt_char(BATTERY_CHARACTERISTIC_UUID) battery = ord(bat_char) except asyncio.TimeoutError: @@ -168,7 +161,8 @@ async def async_setup_scanner( # noqa: C901 @callback def _async_update_ble( - service_info: bluetooth.BluetoothServiceInfo, change: bluetooth.BluetoothChange + service_info: bluetooth.BluetoothServiceInfoBleak, + change: bluetooth.BluetoothChange, ) -> None: """Update from a ble callback.""" mac = service_info.address @@ -202,7 +196,9 @@ async def async_setup_scanner( # noqa: C901 _async_update_ble(service_info, bluetooth.BluetoothChange.ADVERTISEMENT) cancels = [ - bluetooth.async_register_callback(hass, _async_update_ble, None), + bluetooth.async_register_callback( + hass, _async_update_ble, None, bluetooth.BluetoothScanningMode.ACTIVE + ), async_track_time_interval(hass, _async_refresh_ble, interval), ] diff --git a/homeassistant/components/govee_ble/__init__.py b/homeassistant/components/govee_ble/__init__.py index 3099d401e9b..7a134e43ace 100644 --- a/homeassistant/components/govee_ble/__init__.py +++ b/homeassistant/components/govee_ble/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -27,6 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, _LOGGER, address=address, + mode=BluetoothScanningMode.ACTIVE, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/govee_ble/config_flow.py b/homeassistant/components/govee_ble/config_flow.py index 9f2efac0ce9..1e3a5566bfd 100644 --- a/homeassistant/components/govee_ble/config_flow.py +++ b/homeassistant/components/govee_ble/config_flow.py @@ -7,7 +7,7 @@ from govee_ble import GoveeBluetoothDeviceData as DeviceData import voluptuous as vol from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothServiceInfoBleak, async_discovered_service_info, ) from homeassistant.config_entries import ConfigFlow @@ -24,12 +24,12 @@ class GoveeConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, str] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index d8b3fda2d06..31677e37b20 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -20,13 +20,16 @@ from homeassistant.components import zeroconf from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.service_info import bluetooth from .connection import HKDevice from .const import DOMAIN, KNOWN_DEVICES from .storage import async_get_entity_storage from .utils import async_get_controller +if TYPE_CHECKING: + from homeassistant.components import bluetooth + + HOMEKIT_DIR = ".homekit" HOMEKIT_BRIDGE_DOMAIN = "homekit" @@ -359,7 +362,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self._async_step_pair_show_form() async def async_step_bluetooth( - self, discovery_info: bluetooth.BluetoothServiceInfo + self, discovery_info: bluetooth.BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" if not aiohomekit_const.BLE_TRANSPORT_SUPPORTED: diff --git a/homeassistant/components/homekit_controller/utils.py b/homeassistant/components/homekit_controller/utils.py index d7780029331..6e272067b54 100644 --- a/homeassistant/components/homekit_controller/utils.py +++ b/homeassistant/components/homekit_controller/utils.py @@ -32,7 +32,7 @@ async def async_get_controller(hass: HomeAssistant) -> Controller: controller = Controller( async_zeroconf_instance=async_zeroconf_instance, - bleak_scanner_instance=bleak_scanner_instance, + bleak_scanner_instance=bleak_scanner_instance, # type: ignore[arg-type] ) hass.data[CONTROLLER] = controller diff --git a/homeassistant/components/inkbird/__init__.py b/homeassistant/components/inkbird/__init__.py index 5553b1c6ded..0272114b83c 100644 --- a/homeassistant/components/inkbird/__init__.py +++ b/homeassistant/components/inkbird/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -24,9 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, + hass, _LOGGER, address=address, mode=BluetoothScanningMode.ACTIVE ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/inkbird/config_flow.py b/homeassistant/components/inkbird/config_flow.py index 679ff43b19e..21ed85e117e 100644 --- a/homeassistant/components/inkbird/config_flow.py +++ b/homeassistant/components/inkbird/config_flow.py @@ -7,7 +7,7 @@ from inkbird_ble import INKBIRDBluetoothDeviceData as DeviceData import voluptuous as vol from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothServiceInfoBleak, async_discovered_service_info, ) from homeassistant.config_entries import ConfigFlow @@ -24,12 +24,12 @@ class INKBIRDConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, str] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/components/moat/__init__.py b/homeassistant/components/moat/__init__.py index 259b6b66709..237948a8ff6 100644 --- a/homeassistant/components/moat/__init__.py +++ b/homeassistant/components/moat/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -24,9 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, + hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/moat/config_flow.py b/homeassistant/components/moat/config_flow.py index a353f4963ad..6f51b62d110 100644 --- a/homeassistant/components/moat/config_flow.py +++ b/homeassistant/components/moat/config_flow.py @@ -7,7 +7,7 @@ from moat_ble import MoatBluetoothDeviceData as DeviceData import voluptuous as vol from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothServiceInfoBleak, async_discovered_service_info, ) from homeassistant.config_entries import ConfigFlow @@ -24,12 +24,12 @@ class MoatConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, str] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/components/sensorpush/__init__.py b/homeassistant/components/sensorpush/__init__.py index 0a9efcbc752..d4a0872ba3f 100644 --- a/homeassistant/components/sensorpush/__init__.py +++ b/homeassistant/components/sensorpush/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -24,9 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, + hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/sensorpush/config_flow.py b/homeassistant/components/sensorpush/config_flow.py index 1a8e8b47abe..d10c2f481a6 100644 --- a/homeassistant/components/sensorpush/config_flow.py +++ b/homeassistant/components/sensorpush/config_flow.py @@ -7,7 +7,7 @@ from sensorpush_ble import SensorPushBluetoothDeviceData as DeviceData import voluptuous as vol from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothServiceInfoBleak, async_discovered_service_info, ) from homeassistant.config_entries import ConfigFlow @@ -24,12 +24,12 @@ class SensorPushConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, str] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 3a34a89d9fd..eaad573d370 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import Any from switchbot import SwitchBotAdvertisement, parse_advertisement_data import voluptuous as vol @@ -15,7 +15,6 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from .const import CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, SUPPORTED_MODEL_TYPES @@ -46,15 +45,14 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): self._discovered_advs: dict[str, SwitchBotAdvertisement] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" _LOGGER.debug("Discovered bluetooth device: %s", discovery_info) await self.async_set_unique_id(format_unique_id(discovery_info.address)) self._abort_if_unique_id_configured() - discovery_info_bleak = cast(BluetoothServiceInfoBleak, discovery_info) parsed = parse_advertisement_data( - discovery_info_bleak.device, discovery_info_bleak.advertisement + discovery_info.device, discovery_info.advertisement ) if not parsed or parsed.data.get("modelName") not in SUPPORTED_MODEL_TYPES: return self.async_abort(reason="not_supported") diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index f461a3e0f4c..43c576249df 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio import logging -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any import switchbot @@ -39,7 +39,9 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): device: switchbot.SwitchbotDevice, ) -> None: """Initialize global switchbot data updater.""" - super().__init__(hass, logger, ble_device.address) + super().__init__( + hass, logger, ble_device.address, bluetooth.BluetoothScanningMode.ACTIVE + ) self.ble_device = ble_device self.device = device self.data: dict[str, Any] = {} @@ -48,14 +50,13 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): @callback def _async_handle_bluetooth_event( self, - service_info: bluetooth.BluetoothServiceInfo, + service_info: bluetooth.BluetoothServiceInfoBleak, change: bluetooth.BluetoothChange, ) -> None: """Handle a Bluetooth event.""" super()._async_handle_bluetooth_event(service_info, change) - discovery_info_bleak = cast(bluetooth.BluetoothServiceInfoBleak, service_info) if adv := switchbot.parse_advertisement_data( - discovery_info_bleak.device, discovery_info_bleak.advertisement + service_info.device, service_info.advertisement ): self.data = flatten_sensors_data(adv.data) if "modelName" in self.data: diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 4eb20dbd943..791ac1447ad 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -24,9 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, + hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index 91c7e223f1a..aa1ffc24895 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -11,7 +11,8 @@ from xiaomi_ble.parser import EncryptionScheme from homeassistant.components import onboarding from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothScanningMode, + BluetoothServiceInfoBleak, async_discovered_service_info, async_process_advertisements, ) @@ -30,11 +31,11 @@ class Discovery: """A discovered bluetooth device.""" title: str - discovery_info: BluetoothServiceInfo + discovery_info: BluetoothServiceInfoBleak device: DeviceData -def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str: +def _title(discovery_info: BluetoothServiceInfoBleak, device: DeviceData) -> str: return device.title or device.get_device_name() or discovery_info.name @@ -45,18 +46,20 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, Discovery] = {} async def _async_wait_for_full_advertisement( - self, discovery_info: BluetoothServiceInfo, device: DeviceData - ) -> BluetoothServiceInfo: + self, discovery_info: BluetoothServiceInfoBleak, device: DeviceData + ) -> BluetoothServiceInfoBleak: """Sometimes first advertisement we receive is blank or incomplete. Wait until we get a useful one.""" if not device.pending: return discovery_info - def _process_more_advertisements(service_info: BluetoothServiceInfo) -> bool: + def _process_more_advertisements( + service_info: BluetoothServiceInfoBleak, + ) -> bool: device.update(service_info) return not device.pending @@ -64,11 +67,12 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): self.hass, _process_more_advertisements, {"address": discovery_info.address}, + BluetoothScanningMode.ACTIVE, ADDITIONAL_DISCOVERY_TIMEOUT, ) async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b5d5804f100..b0c04323005 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -27,12 +27,12 @@ from .util import uuid as uuid_util from .util.decorator import Registry if TYPE_CHECKING: + from .components.bluetooth import BluetoothServiceInfoBleak from .components.dhcp import DhcpServiceInfo from .components.hassio import HassioServiceInfo from .components.ssdp import SsdpServiceInfo from .components.usb import UsbServiceInfo from .components.zeroconf import ZeroconfServiceInfo - from .helpers.service_info.bluetooth import BluetoothServiceInfo from .helpers.service_info.mqtt import MqttServiceInfo _LOGGER = logging.getLogger(__name__) @@ -1485,7 +1485,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): ) async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> data_entry_flow.FlowResult: """Handle a flow initialized by Bluetooth discovery.""" return await self._async_step_discovery_without_unique_id() diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index bf2f95c12c6..c9d6ffe6065 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -15,11 +15,11 @@ from .typing import DiscoveryInfoType if TYPE_CHECKING: import asyncio + from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.ssdp import SsdpServiceInfo from homeassistant.components.zeroconf import ZeroconfServiceInfo - from .service_info.bluetooth import BluetoothServiceInfo from .service_info.mqtt import MqttServiceInfo _R = TypeVar("_R", bound="Awaitable[bool] | bool") @@ -97,7 +97,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle a flow initialized by bluetooth discovery.""" if self._async_in_progress() or self._async_current_entries(): diff --git a/homeassistant/helpers/service_info/bluetooth.py b/homeassistant/helpers/service_info/bluetooth.py index 968d1dde95f..0db3a39b114 100644 --- a/homeassistant/helpers/service_info/bluetooth.py +++ b/homeassistant/helpers/service_info/bluetooth.py @@ -1,4 +1,5 @@ """The bluetooth integration service info.""" + from home_assistant_bluetooth import BluetoothServiceInfo __all__ = ["BluetoothServiceInfo"] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1f86ff4c7c5..c9d424daa33 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 -bleak==0.14.3 +bleak==0.15.0 bluetooth-adapters==0.1.2 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 842376bc696..7fe58a0899b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -405,7 +405,7 @@ bimmer_connected==0.10.1 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak==0.14.3 +bleak==0.15.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0d61f2d618a..16919de3c48 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -326,7 +326,7 @@ bellows==0.31.2 bimmer_connected==0.10.1 # homeassistant.components.bluetooth -bleak==0.14.3 +bleak==0.15.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 0664f82dbab..a47916506df 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -12,6 +12,7 @@ from homeassistant.components.bluetooth import ( SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, + BluetoothScanningMode, BluetoothServiceInfo, async_process_advertisements, async_track_unavailable, @@ -675,6 +676,7 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo hass, _fake_subscriber, {"service_uuids": {"cba20d00-224d-11e6-9fb8-0002a5d5c51b"}}, + BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 @@ -760,6 +762,7 @@ async def test_register_callback_by_address( hass, _fake_subscriber, {"address": "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 @@ -799,6 +802,7 @@ async def test_register_callback_by_address( hass, _fake_subscriber, {"address": "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, ) cancel() @@ -808,6 +812,7 @@ async def test_register_callback_by_address( hass, _fake_subscriber, {"address": "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, ) cancel() @@ -832,7 +837,11 @@ async def test_process_advertisements_bail_on_good_advertisement( handle = hass.async_create_task( async_process_advertisements( - hass, _callback, {"address": "aa:44:33:11:23:45"}, 5 + hass, + _callback, + {"address": "aa:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, + 5, ) ) @@ -873,7 +882,11 @@ async def test_process_advertisements_ignore_bad_advertisement( handle = hass.async_create_task( async_process_advertisements( - hass, _callback, {"address": "aa:44:33:11:23:45"}, 5 + hass, + _callback, + {"address": "aa:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, + 5, ) ) @@ -903,7 +916,9 @@ async def test_process_advertisements_timeout( return False with pytest.raises(asyncio.TimeoutError): - await async_process_advertisements(hass, _callback, {}, 0) + await async_process_advertisements( + hass, _callback, {}, BluetoothScanningMode.ACTIVE, 0 + ) async def test_wrapped_instance_with_filter( diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 4da14fb13d3..31530cd6995 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -10,6 +10,7 @@ from homeassistant.components.bluetooth import ( DOMAIN, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, + BluetoothScanningMode, ) from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, @@ -42,9 +43,9 @@ GENERIC_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( class MyCoordinator(PassiveBluetoothDataUpdateCoordinator): """An example coordinator that subclasses PassiveBluetoothDataUpdateCoordinator.""" - def __init__(self, hass, logger, device_id) -> None: + def __init__(self, hass, logger, device_id, mode) -> None: """Initialize the coordinator.""" - super().__init__(hass, logger, device_id) + super().__init__(hass, logger, device_id, mode) self.data: dict[str, Any] = {} def _async_handle_bluetooth_event( @@ -60,11 +61,13 @@ class MyCoordinator(PassiveBluetoothDataUpdateCoordinator): async def test_basic_usage(hass, mock_bleak_scanner_start): """Test basic usage of the PassiveBluetoothDataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -100,11 +103,13 @@ async def test_context_compatiblity_with_data_update_coordinator( ): """Test contexts can be passed for compatibility with DataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -149,11 +154,13 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( ): await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.PASSIVE + ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -208,13 +215,15 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( async def test_passive_bluetooth_coordinator_entity(hass, mock_bleak_scanner_start): """Test integration of PassiveBluetoothDataUpdateCoordinator with PassiveBluetoothCoordinatorEntity.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) entity = PassiveBluetoothCoordinatorEntity(coordinator) assert entity.available is False saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index ebb69d7c7d0..6a092746a68 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -16,6 +16,7 @@ from homeassistant.components.bluetooth import ( DOMAIN, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, + BluetoothScanningMode, ) from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, @@ -90,12 +91,12 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -192,12 +193,12 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -277,12 +278,12 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -336,12 +337,12 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -389,12 +390,12 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -731,12 +732,12 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): return GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -841,12 +842,12 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -905,12 +906,12 @@ async def test_passive_bluetooth_entity_with_entity_platform( return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -992,12 +993,12 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index dfe47b38b33..f9f0a51fc0f 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -5,8 +5,9 @@ from datetime import timedelta from unittest.mock import patch from bleak import BleakError +from bleak.backends.scanner import AdvertisementData, BLEDevice -from homeassistant.components.bluetooth import BluetoothServiceInfo +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.bluetooth_le_tracker import device_tracker from homeassistant.components.bluetooth_le_tracker.device_tracker import ( CONF_TRACK_BATTERY, @@ -79,7 +80,7 @@ async def test_preserve_new_tracked_device_name( device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=name, address=address, rssi=-19, @@ -87,6 +88,8 @@ async def test_preserve_new_tracked_device_name( service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -100,7 +103,7 @@ async def test_preserve_new_tracked_device_name( assert result # Seen once here; return without name when seen subsequent times - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=None, address=address, rssi=-19, @@ -108,6 +111,8 @@ async def test_preserve_new_tracked_device_name( service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -140,7 +145,7 @@ async def test_tracking_battery_times_out( device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=name, address=address, rssi=-19, @@ -148,6 +153,8 @@ async def test_tracking_battery_times_out( service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -202,7 +209,7 @@ async def test_tracking_battery_fails(hass, mock_bluetooth, mock_device_tracker_ device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=name, address=address, rssi=-19, @@ -210,6 +217,8 @@ async def test_tracking_battery_fails(hass, mock_bluetooth, mock_device_tracker_ service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -266,7 +275,7 @@ async def test_tracking_battery_successful( device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=name, address=address, rssi=-19, @@ -274,6 +283,8 @@ async def test_tracking_battery_successful( service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] diff --git a/tests/components/fjaraskupan/conftest.py b/tests/components/fjaraskupan/conftest.py index d60abcdb9ad..4e06b2ad046 100644 --- a/tests/components/fjaraskupan/conftest.py +++ b/tests/components/fjaraskupan/conftest.py @@ -17,6 +17,12 @@ def fixture_scanner(hass): class MockScanner(BaseBleakScanner): """Mock Scanner.""" + def __init__(self, *args, **kwargs) -> None: + """Initialize the scanner.""" + super().__init__( + detection_callback=kwargs.pop("detection_callback"), service_uuids=[] + ) + async def start(self): """Start scanning for devices.""" for device in devices: diff --git a/tests/components/govee_ble/test_sensor.py b/tests/components/govee_ble/test_sensor.py index 7a6ecbaed51..75d269ea0ba 100644 --- a/tests/components/govee_ble/test_sensor.py +++ b/tests/components/govee_ble/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/inkbird/test_sensor.py b/tests/components/inkbird/test_sensor.py index a851cb92ec3..cafc22911c3 100644 --- a/tests/components/inkbird/test_sensor.py +++ b/tests/components/inkbird/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/moat/test_sensor.py b/tests/components/moat/test_sensor.py index 826bbc72cdb..6424144106b 100644 --- a/tests/components/moat/test_sensor.py +++ b/tests/components/moat/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/sensorpush/test_sensor.py b/tests/components/sensorpush/test_sensor.py index 31fbdd8d712..34179985d78 100644 --- a/tests/components/sensorpush/test_sensor.py +++ b/tests/components/sensorpush/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index b424228cc6c..86fda21aaa0 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -59,7 +59,9 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload(hass): async def test_async_step_bluetooth_valid_device_but_missing_payload_then_full(hass): """Test discovering a valid device. Payload is too short, but later we get full one.""" - async def _async_process_advertisements(_hass, _callback, _matcher, _timeout): + async def _async_process_advertisements( + _hass, _callback, _matcher, _mode, _timeout + ): service_info = make_advertisement( "A4:C1:38:56:53:84", b"XX\xe4\x16,\x84SV8\xc1\xa4+n\xf2\xe9\x12\x00\x00l\x88M\x9e", @@ -378,7 +380,9 @@ async def test_async_step_user_short_payload_then_full(hass): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - async def _async_process_advertisements(_hass, _callback, _matcher, _timeout): + async def _async_process_advertisements( + _hass, _callback, _matcher, _mode, _timeout + ): service_info = make_advertisement( "A4:C1:38:56:53:84", b"XX\xe4\x16,\x84SV8\xc1\xa4+n\xf2\xe9\x12\x00\x00l\x88M\x9e", diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index dca00e92254..011c6daecae 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -60,7 +60,7 @@ async def test_xiaomi_formaldeyhde(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -107,7 +107,7 @@ async def test_xiaomi_consumable(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -154,7 +154,7 @@ async def test_xiaomi_battery_voltage(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -208,7 +208,7 @@ async def test_xiaomi_HHCCJCY01(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -291,7 +291,7 @@ async def test_xiaomi_CGDK2(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None From 176d44190e01dcdd3b201c541dea5fbb04b7fef7 Mon Sep 17 00:00:00 2001 From: Jan Stienstra <65826735+j-stienstra@users.noreply.github.com> Date: Fri, 29 Jul 2022 19:11:53 +0200 Subject: [PATCH 2972/3516] Fix incorrect check for media source (#75880) * Fix incorrect check for media source * Update homeassistant/components/jellyfin/media_source.py Co-authored-by: Franck Nijhof Co-authored-by: Paulus Schoutsen Co-authored-by: Franck Nijhof --- homeassistant/components/jellyfin/media_source.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index 879f4a4d4c8..8a09fd8d552 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -363,14 +363,18 @@ class JellyfinSource(MediaSource): def _media_mime_type(media_item: dict[str, Any]) -> str: """Return the mime type of a media item.""" - if not media_item[ITEM_KEY_MEDIA_SOURCES]: + if not media_item.get(ITEM_KEY_MEDIA_SOURCES): raise BrowseError("Unable to determine mime type for item without media source") media_source = media_item[ITEM_KEY_MEDIA_SOURCES][0] + + if MEDIA_SOURCE_KEY_PATH not in media_source: + raise BrowseError("Unable to determine mime type for media source without path") + path = media_source[MEDIA_SOURCE_KEY_PATH] mime_type, _ = mimetypes.guess_type(path) - if mime_type is not None: - return mime_type + if mime_type is None: + raise BrowseError(f"Unable to determine mime type for path {path}") - raise BrowseError(f"Unable to determine mime type for path {path}") + return mime_type From d7827d99020c77c986905637928d0f0e6dd7f6c3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Jul 2022 18:56:19 +0200 Subject: [PATCH 2973/3516] Fix SimplePush repairs issue (#75922) --- homeassistant/components/simplepush/notify.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index 358d95c770a..2e58748f323 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -43,17 +43,16 @@ async def async_get_service( discovery_info: DiscoveryInfoType | None = None, ) -> SimplePushNotificationService | None: """Get the Simplepush notification service.""" - async_create_issue( - hass, - DOMAIN, - "deprecated_yaml", - breaks_in_ha_version="2022.9.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - ) - if discovery_info is None: + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config From c3c5442467be7179f4d5c3c8efc3f975ead576a8 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 29 Jul 2022 13:28:39 +0100 Subject: [PATCH 2974/3516] Fix xiaomi_ble discovery for devices that don't put the fe95 uuid in service_uuids (#75923) --- homeassistant/components/xiaomi_ble/manifest.json | 2 +- homeassistant/generated/bluetooth.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 41512291749..0d97dcbedf8 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/xiaomi_ble", "bluetooth": [ { - "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" + "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], "requirements": ["xiaomi-ble==0.6.2"], diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 8d92d6eab4a..2cbaebb6074 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -80,6 +80,6 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ }, { "domain": "xiaomi_ble", - "service_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" + "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ] From 241ffe07b977c245abb1849817fa62651618d6b2 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Fri, 29 Jul 2022 18:54:49 +0200 Subject: [PATCH 2975/3516] Update xknx to 0.22.1 (#75932) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 4197cb76209..266eceaacee 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.22.0"], + "requirements": ["xknx==0.22.1"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 3bfd3880eee..b7b3ed89b27 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2473,7 +2473,7 @@ xboxapi==2.0.1 xiaomi-ble==0.6.2 # homeassistant.components.knx -xknx==0.22.0 +xknx==0.22.1 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6ce69ed9678..d04b537c9fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1665,7 +1665,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.6.2 # homeassistant.components.knx -xknx==0.22.0 +xknx==0.22.1 # homeassistant.components.bluesound # homeassistant.components.fritz From 0f0b51bee71ff87a39e9895645f6bfca236644cd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 28 Jul 2022 22:26:37 -0700 Subject: [PATCH 2976/3516] Move some bleak imports to be behind TYPE_CHECKING (#75894) --- .../components/bluetooth_le_tracker/device_tracker.py | 5 ++++- homeassistant/components/fjaraskupan/__init__.py | 8 ++++++-- homeassistant/components/switchbot/coordinator.py | 10 ++++++---- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index f85cc2bad0a..e42d19ac547 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -5,10 +5,10 @@ import asyncio from collections.abc import Awaitable, Callable from datetime import datetime, timedelta import logging +from typing import TYPE_CHECKING from uuid import UUID from bleak import BleakClient, BleakError -from bleak.backends.device import BLEDevice import voluptuous as vol from homeassistant.components import bluetooth @@ -31,6 +31,9 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + _LOGGER = logging.getLogger(__name__) # Base UUID: 00000000-0000-1000-8000-00805F9B34FB diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 85e95db5513..36608fb026d 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -5,10 +5,9 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta import logging +from typing import TYPE_CHECKING from bleak import BleakScanner -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData from fjaraskupan import Device, State, device_filter from homeassistant.config_entries import ConfigEntry @@ -24,6 +23,11 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DISPATCH_DETECTION, DOMAIN +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + from bleak.backends.scanner import AdvertisementData + + PLATFORMS = [ Platform.BINARY_SENSOR, Platform.FAN, diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 31f7f2d3992..f461a3e0f4c 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -3,11 +3,9 @@ from __future__ import annotations import asyncio import logging -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast -from bleak.backends.device import BLEDevice import switchbot -from switchbot import parse_advertisement_data from homeassistant.components import bluetooth from homeassistant.components.bluetooth.passive_update_coordinator import ( @@ -15,6 +13,10 @@ from homeassistant.components.bluetooth.passive_update_coordinator import ( ) from homeassistant.core import HomeAssistant, callback +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + + _LOGGER = logging.getLogger(__name__) @@ -52,7 +54,7 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): """Handle a Bluetooth event.""" super()._async_handle_bluetooth_event(service_info, change) discovery_info_bleak = cast(bluetooth.BluetoothServiceInfoBleak, service_info) - if adv := parse_advertisement_data( + if adv := switchbot.parse_advertisement_data( discovery_info_bleak.device, discovery_info_bleak.advertisement ): self.data = flatten_sensors_data(adv.data) From 26c475d3dc51f39a588c540021a7f8f19f2f84ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Jul 2022 14:53:33 -1000 Subject: [PATCH 2977/3516] Update to bleak 0.15 (#75941) --- .../components/bluetooth/__init__.py | 29 ++++++++------- .../components/bluetooth/manifest.json | 2 +- homeassistant/components/bluetooth/models.py | 34 ++++++++++++----- .../bluetooth/passive_update_coordinator.py | 8 ++-- .../bluetooth/passive_update_processor.py | 17 +++++---- .../bluetooth/update_coordinator.py | 9 +++-- homeassistant/components/bluetooth/usage.py | 5 ++- .../bluetooth_le_tracker/device_tracker.py | 18 ++++----- .../components/govee_ble/__init__.py | 2 + .../components/govee_ble/config_flow.py | 6 +-- .../homekit_controller/config_flow.py | 7 +++- .../components/homekit_controller/utils.py | 2 +- homeassistant/components/inkbird/__init__.py | 5 +-- .../components/inkbird/config_flow.py | 6 +-- homeassistant/components/moat/__init__.py | 5 +-- homeassistant/components/moat/config_flow.py | 6 +-- .../components/sensorpush/__init__.py | 5 +-- .../components/sensorpush/config_flow.py | 6 +-- .../components/switchbot/config_flow.py | 8 ++-- .../components/switchbot/coordinator.py | 11 +++--- .../components/xiaomi_ble/__init__.py | 5 +-- .../components/xiaomi_ble/config_flow.py | 20 ++++++---- homeassistant/config_entries.py | 4 +- homeassistant/helpers/config_entry_flow.py | 4 +- .../helpers/service_info/bluetooth.py | 1 + homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/test_init.py | 21 +++++++++-- .../test_passive_update_coordinator.py | 29 ++++++++++----- .../test_passive_update_processor.py | 37 ++++++++++--------- .../test_device_tracker.py | 23 +++++++++--- tests/components/fjaraskupan/conftest.py | 6 +++ tests/components/govee_ble/test_sensor.py | 2 +- tests/components/inkbird/test_sensor.py | 2 +- tests/components/moat/test_sensor.py | 2 +- tests/components/sensorpush/test_sensor.py | 2 +- .../components/xiaomi_ble/test_config_flow.py | 8 +++- tests/components/xiaomi_ble/test_sensor.py | 12 +++--- 39 files changed, 223 insertions(+), 152 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index eb8e31baef0..9adaac84333 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum import logging -from typing import Final, Union +from typing import Final import async_timeout from bleak import BleakError @@ -96,12 +96,8 @@ SCANNING_MODE_TO_BLEAK = { BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") -BluetoothCallback = Callable[ - [Union[BluetoothServiceInfoBleak, BluetoothServiceInfo], BluetoothChange], None -] -ProcessAdvertisementCallback = Callable[ - [Union[BluetoothServiceInfoBleak, BluetoothServiceInfo]], bool -] +BluetoothCallback = Callable[[BluetoothServiceInfoBleak, BluetoothChange], None] +ProcessAdvertisementCallback = Callable[[BluetoothServiceInfoBleak], bool] @hass_callback @@ -157,9 +153,15 @@ def async_register_callback( hass: HomeAssistant, callback: BluetoothCallback, match_dict: BluetoothCallbackMatcher | None, + mode: BluetoothScanningMode, ) -> Callable[[], None]: """Register to receive a callback on bluetooth change. + mode is currently not used as we only support active scanning. + Passive scanning will be available in the future. The flag + is required to be present to avoid a future breaking change + when we support passive scanning. + Returns a callback that can be used to cancel the registration. """ manager: BluetoothManager = hass.data[DOMAIN] @@ -170,19 +172,20 @@ async def async_process_advertisements( hass: HomeAssistant, callback: ProcessAdvertisementCallback, match_dict: BluetoothCallbackMatcher, + mode: BluetoothScanningMode, timeout: int, -) -> BluetoothServiceInfo: +) -> BluetoothServiceInfoBleak: """Process advertisements until callback returns true or timeout expires.""" - done: Future[BluetoothServiceInfo] = Future() + done: Future[BluetoothServiceInfoBleak] = Future() @hass_callback def _async_discovered_device( - service_info: BluetoothServiceInfo, change: BluetoothChange + service_info: BluetoothServiceInfoBleak, change: BluetoothChange ) -> None: if callback(service_info): done.set_result(service_info) - unload = async_register_callback(hass, _async_discovered_device, match_dict) + unload = async_register_callback(hass, _async_discovered_device, match_dict, mode) try: async with async_timeout.timeout(timeout): @@ -333,7 +336,7 @@ class BluetoothManager: ) try: async with async_timeout.timeout(START_TIMEOUT): - await self.scanner.start() + await self.scanner.start() # type: ignore[no-untyped-call] except asyncio.TimeoutError as ex: self._cancel_device_detected() raise ConfigEntryNotReady( @@ -500,7 +503,7 @@ class BluetoothManager: self._cancel_unavailable_tracking = None if self.scanner: try: - await self.scanner.stop() + await self.scanner.stop() # type: ignore[no-untyped-call] except BleakError as ex: # This is not fatal, and they may want to reload # the config entry to restart the scanner if they diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index c6ca8b11400..f215e8fa161 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.14.3", "bluetooth-adapters==0.1.2"], + "requirements": ["bleak==0.15.0", "bluetooth-adapters==0.1.2"], "codeowners": ["@bdraco"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 408e0698879..6f814c7b66b 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio import contextlib import logging -from typing import Any, Final, cast +from typing import Any, Final from bleak import BleakScanner from bleak.backends.device import BLEDevice @@ -32,7 +32,7 @@ def _dispatch_callback( """Dispatch the callback.""" if not callback: # Callback destroyed right before being called, ignore - return + return # type: ignore[unreachable] if (uuids := filters.get(FILTER_UUIDS)) and not uuids.intersection( advertisement_data.service_uuids @@ -45,7 +45,7 @@ def _dispatch_callback( _LOGGER.exception("Error in callback: %s", callback) -class HaBleakScanner(BleakScanner): # type: ignore[misc] +class HaBleakScanner(BleakScanner): """BleakScanner that cannot be stopped.""" def __init__( # pylint: disable=super-init-not-called @@ -106,16 +106,29 @@ class HaBleakScanner(BleakScanner): # type: ignore[misc] _dispatch_callback(*callback_filters, device, advertisement_data) -class HaBleakScannerWrapper(BaseBleakScanner): # type: ignore[misc] +class HaBleakScannerWrapper(BaseBleakScanner): """A wrapper that uses the single instance.""" - def __init__(self, *args: Any, **kwargs: Any) -> None: + def __init__( + self, + *args: Any, + detection_callback: AdvertisementDataCallback | None = None, + service_uuids: list[str] | None = None, + **kwargs: Any, + ) -> None: """Initialize the BleakScanner.""" self._detection_cancel: CALLBACK_TYPE | None = None self._mapped_filters: dict[str, set[str]] = {} self._adv_data_callback: AdvertisementDataCallback | None = None - self._map_filters(*args, **kwargs) - super().__init__(*args, **kwargs) + remapped_kwargs = { + "detection_callback": detection_callback, + "service_uuids": service_uuids or [], + **kwargs, + } + self._map_filters(*args, **remapped_kwargs) + super().__init__( + detection_callback=detection_callback, service_uuids=service_uuids or [] + ) async def stop(self, *args: Any, **kwargs: Any) -> None: """Stop scanning for devices.""" @@ -153,9 +166,11 @@ class HaBleakScannerWrapper(BaseBleakScanner): # type: ignore[misc] def discovered_devices(self) -> list[BLEDevice]: """Return a list of discovered devices.""" assert HA_BLEAK_SCANNER is not None - return cast(list[BLEDevice], HA_BLEAK_SCANNER.discovered_devices) + return HA_BLEAK_SCANNER.discovered_devices - def register_detection_callback(self, callback: AdvertisementDataCallback) -> None: + def register_detection_callback( + self, callback: AdvertisementDataCallback | None + ) -> None: """Register a callback that is called when a device is discovered or has a property changed. This method takes the callback and registers it with the long running @@ -171,6 +186,7 @@ class HaBleakScannerWrapper(BaseBleakScanner): # type: ignore[misc] self._cancel_callback() super().register_detection_callback(self._adv_data_callback) assert HA_BLEAK_SCANNER is not None + assert self._callback is not None self._detection_cancel = HA_BLEAK_SCANNER.async_register_callback( self._callback, self._mapped_filters ) diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 97e7ddc49ee..31a6b065830 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -6,10 +6,9 @@ import logging from typing import Any from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import BluetoothChange +from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .update_coordinator import BasePassiveBluetoothCoordinator @@ -25,9 +24,10 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): hass: HomeAssistant, logger: logging.Logger, address: str, + mode: BluetoothScanningMode, ) -> None: """Initialize PassiveBluetoothDataUpdateCoordinator.""" - super().__init__(hass, logger, address) + super().__init__(hass, logger, address, mode) self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {} @callback @@ -65,7 +65,7 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): @callback def _async_handle_bluetooth_event( self, - service_info: BluetoothServiceInfo, + service_info: BluetoothServiceInfoBleak, change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 43467701879..1f2047c02cb 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -6,14 +6,12 @@ import dataclasses import logging from typing import Any, Generic, TypeVar -from home_assistant_bluetooth import BluetoothServiceInfo - from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BluetoothChange +from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .const import DOMAIN from .update_coordinator import BasePassiveBluetoothCoordinator @@ -62,9 +60,10 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): hass: HomeAssistant, logger: logging.Logger, address: str, + mode: BluetoothScanningMode, ) -> None: """Initialize the coordinator.""" - super().__init__(hass, logger, address) + super().__init__(hass, logger, address, mode) self._processors: list[PassiveBluetoothDataProcessor] = [] @callback @@ -92,7 +91,7 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): @callback def _async_handle_bluetooth_event( self, - service_info: BluetoothServiceInfo, + service_info: BluetoothServiceInfoBleak, change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" @@ -122,7 +121,7 @@ class PassiveBluetoothDataProcessor(Generic[_T]): The processor will call the update_method every time the bluetooth device receives a new advertisement data from the coordinator with the following signature: - update_method(service_info: BluetoothServiceInfo) -> PassiveBluetoothDataUpdate + update_method(service_info: BluetoothServiceInfoBleak) -> PassiveBluetoothDataUpdate As the size of each advertisement is limited, the update_method should return a PassiveBluetoothDataUpdate object that contains only data that @@ -135,7 +134,9 @@ class PassiveBluetoothDataProcessor(Generic[_T]): def __init__( self, - update_method: Callable[[BluetoothServiceInfo], PassiveBluetoothDataUpdate[_T]], + update_method: Callable[ + [BluetoothServiceInfoBleak], PassiveBluetoothDataUpdate[_T] + ], ) -> None: """Initialize the coordinator.""" self.coordinator: PassiveBluetoothProcessorCoordinator @@ -241,7 +242,7 @@ class PassiveBluetoothDataProcessor(Generic[_T]): @callback def async_handle_bluetooth_event( self, - service_info: BluetoothServiceInfo, + service_info: BluetoothServiceInfoBleak, change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" diff --git a/homeassistant/components/bluetooth/update_coordinator.py b/homeassistant/components/bluetooth/update_coordinator.py index b1cb2de1453..d0f38ce32c6 100644 --- a/homeassistant/components/bluetooth/update_coordinator.py +++ b/homeassistant/components/bluetooth/update_coordinator.py @@ -4,13 +4,13 @@ from __future__ import annotations import logging import time -from home_assistant_bluetooth import BluetoothServiceInfo - from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from . import ( BluetoothCallbackMatcher, BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfoBleak, async_register_callback, async_track_unavailable, ) @@ -27,6 +27,7 @@ class BasePassiveBluetoothCoordinator: hass: HomeAssistant, logger: logging.Logger, address: str, + mode: BluetoothScanningMode, ) -> None: """Initialize the coordinator.""" self.hass = hass @@ -36,6 +37,7 @@ class BasePassiveBluetoothCoordinator: self._cancel_track_unavailable: CALLBACK_TYPE | None = None self._cancel_bluetooth_advertisements: CALLBACK_TYPE | None = None self._present = False + self.mode = mode self.last_seen = 0.0 @callback @@ -61,6 +63,7 @@ class BasePassiveBluetoothCoordinator: self.hass, self._async_handle_bluetooth_event, BluetoothCallbackMatcher(address=self.address), + self.mode, ) self._cancel_track_unavailable = async_track_unavailable( self.hass, @@ -86,7 +89,7 @@ class BasePassiveBluetoothCoordinator: @callback def _async_handle_bluetooth_event( self, - service_info: BluetoothServiceInfo, + service_info: BluetoothServiceInfoBleak, change: BluetoothChange, ) -> None: """Handle a Bluetooth event.""" diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py index da5d062a36f..b3a6783cf30 100644 --- a/homeassistant/components/bluetooth/usage.py +++ b/homeassistant/components/bluetooth/usage.py @@ -1,4 +1,5 @@ """bluetooth usage utility to handle multiple instances.""" + from __future__ import annotations import bleak @@ -10,9 +11,9 @@ ORIGINAL_BLEAK_SCANNER = bleak.BleakScanner def install_multiple_bleak_catcher() -> None: """Wrap the bleak classes to return the shared instance if multiple instances are detected.""" - bleak.BleakScanner = HaBleakScannerWrapper + bleak.BleakScanner = HaBleakScannerWrapper # type: ignore[misc, assignment] def uninstall_multiple_bleak_catcher() -> None: """Unwrap the bleak classes.""" - bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER + bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER # type: ignore[misc] diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index e42d19ac547..01b3d48205a 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -5,7 +5,6 @@ import asyncio from collections.abc import Awaitable, Callable from datetime import datetime, timedelta import logging -from typing import TYPE_CHECKING from uuid import UUID from bleak import BleakClient, BleakError @@ -31,9 +30,6 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -if TYPE_CHECKING: - from bleak.backends.device import BLEDevice - _LOGGER = logging.getLogger(__name__) # Base UUID: 00000000-0000-1000-8000-00805F9B34FB @@ -139,15 +135,12 @@ async def async_setup_scanner( # noqa: C901 async def _async_see_update_ble_battery( mac: str, now: datetime, - service_info: bluetooth.BluetoothServiceInfo, + service_info: bluetooth.BluetoothServiceInfoBleak, ) -> None: """Lookup Bluetooth LE devices and update status.""" battery = None - ble_device: BLEDevice | str = ( - bluetooth.async_ble_device_from_address(hass, mac) or mac - ) try: - async with BleakClient(ble_device) as client: + async with BleakClient(service_info.device) as client: bat_char = await client.read_gatt_char(BATTERY_CHARACTERISTIC_UUID) battery = ord(bat_char) except asyncio.TimeoutError: @@ -168,7 +161,8 @@ async def async_setup_scanner( # noqa: C901 @callback def _async_update_ble( - service_info: bluetooth.BluetoothServiceInfo, change: bluetooth.BluetoothChange + service_info: bluetooth.BluetoothServiceInfoBleak, + change: bluetooth.BluetoothChange, ) -> None: """Update from a ble callback.""" mac = service_info.address @@ -202,7 +196,9 @@ async def async_setup_scanner( # noqa: C901 _async_update_ble(service_info, bluetooth.BluetoothChange.ADVERTISEMENT) cancels = [ - bluetooth.async_register_callback(hass, _async_update_ble, None), + bluetooth.async_register_callback( + hass, _async_update_ble, None, bluetooth.BluetoothScanningMode.ACTIVE + ), async_track_time_interval(hass, _async_refresh_ble, interval), ] diff --git a/homeassistant/components/govee_ble/__init__.py b/homeassistant/components/govee_ble/__init__.py index 3099d401e9b..7a134e43ace 100644 --- a/homeassistant/components/govee_ble/__init__.py +++ b/homeassistant/components/govee_ble/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -27,6 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, _LOGGER, address=address, + mode=BluetoothScanningMode.ACTIVE, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/govee_ble/config_flow.py b/homeassistant/components/govee_ble/config_flow.py index 9f2efac0ce9..1e3a5566bfd 100644 --- a/homeassistant/components/govee_ble/config_flow.py +++ b/homeassistant/components/govee_ble/config_flow.py @@ -7,7 +7,7 @@ from govee_ble import GoveeBluetoothDeviceData as DeviceData import voluptuous as vol from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothServiceInfoBleak, async_discovered_service_info, ) from homeassistant.config_entries import ConfigFlow @@ -24,12 +24,12 @@ class GoveeConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, str] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index d8b3fda2d06..31677e37b20 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -20,13 +20,16 @@ from homeassistant.components import zeroconf from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.service_info import bluetooth from .connection import HKDevice from .const import DOMAIN, KNOWN_DEVICES from .storage import async_get_entity_storage from .utils import async_get_controller +if TYPE_CHECKING: + from homeassistant.components import bluetooth + + HOMEKIT_DIR = ".homekit" HOMEKIT_BRIDGE_DOMAIN = "homekit" @@ -359,7 +362,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self._async_step_pair_show_form() async def async_step_bluetooth( - self, discovery_info: bluetooth.BluetoothServiceInfo + self, discovery_info: bluetooth.BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" if not aiohomekit_const.BLE_TRANSPORT_SUPPORTED: diff --git a/homeassistant/components/homekit_controller/utils.py b/homeassistant/components/homekit_controller/utils.py index d7780029331..6e272067b54 100644 --- a/homeassistant/components/homekit_controller/utils.py +++ b/homeassistant/components/homekit_controller/utils.py @@ -32,7 +32,7 @@ async def async_get_controller(hass: HomeAssistant) -> Controller: controller = Controller( async_zeroconf_instance=async_zeroconf_instance, - bleak_scanner_instance=bleak_scanner_instance, + bleak_scanner_instance=bleak_scanner_instance, # type: ignore[arg-type] ) hass.data[CONTROLLER] = controller diff --git a/homeassistant/components/inkbird/__init__.py b/homeassistant/components/inkbird/__init__.py index 5553b1c6ded..0272114b83c 100644 --- a/homeassistant/components/inkbird/__init__.py +++ b/homeassistant/components/inkbird/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -24,9 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, + hass, _LOGGER, address=address, mode=BluetoothScanningMode.ACTIVE ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/inkbird/config_flow.py b/homeassistant/components/inkbird/config_flow.py index 679ff43b19e..21ed85e117e 100644 --- a/homeassistant/components/inkbird/config_flow.py +++ b/homeassistant/components/inkbird/config_flow.py @@ -7,7 +7,7 @@ from inkbird_ble import INKBIRDBluetoothDeviceData as DeviceData import voluptuous as vol from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothServiceInfoBleak, async_discovered_service_info, ) from homeassistant.config_entries import ConfigFlow @@ -24,12 +24,12 @@ class INKBIRDConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, str] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/components/moat/__init__.py b/homeassistant/components/moat/__init__.py index 259b6b66709..237948a8ff6 100644 --- a/homeassistant/components/moat/__init__.py +++ b/homeassistant/components/moat/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -24,9 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, + hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/moat/config_flow.py b/homeassistant/components/moat/config_flow.py index a353f4963ad..6f51b62d110 100644 --- a/homeassistant/components/moat/config_flow.py +++ b/homeassistant/components/moat/config_flow.py @@ -7,7 +7,7 @@ from moat_ble import MoatBluetoothDeviceData as DeviceData import voluptuous as vol from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothServiceInfoBleak, async_discovered_service_info, ) from homeassistant.config_entries import ConfigFlow @@ -24,12 +24,12 @@ class MoatConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, str] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/components/sensorpush/__init__.py b/homeassistant/components/sensorpush/__init__.py index 0a9efcbc752..d4a0872ba3f 100644 --- a/homeassistant/components/sensorpush/__init__.py +++ b/homeassistant/components/sensorpush/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -24,9 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, + hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/sensorpush/config_flow.py b/homeassistant/components/sensorpush/config_flow.py index 1a8e8b47abe..d10c2f481a6 100644 --- a/homeassistant/components/sensorpush/config_flow.py +++ b/homeassistant/components/sensorpush/config_flow.py @@ -7,7 +7,7 @@ from sensorpush_ble import SensorPushBluetoothDeviceData as DeviceData import voluptuous as vol from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothServiceInfoBleak, async_discovered_service_info, ) from homeassistant.config_entries import ConfigFlow @@ -24,12 +24,12 @@ class SensorPushConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, str] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 3a34a89d9fd..eaad573d370 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import Any from switchbot import SwitchBotAdvertisement, parse_advertisement_data import voluptuous as vol @@ -15,7 +15,6 @@ from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from .const import CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, SUPPORTED_MODEL_TYPES @@ -46,15 +45,14 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): self._discovered_advs: dict[str, SwitchBotAdvertisement] = {} async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" _LOGGER.debug("Discovered bluetooth device: %s", discovery_info) await self.async_set_unique_id(format_unique_id(discovery_info.address)) self._abort_if_unique_id_configured() - discovery_info_bleak = cast(BluetoothServiceInfoBleak, discovery_info) parsed = parse_advertisement_data( - discovery_info_bleak.device, discovery_info_bleak.advertisement + discovery_info.device, discovery_info.advertisement ) if not parsed or parsed.data.get("modelName") not in SUPPORTED_MODEL_TYPES: return self.async_abort(reason="not_supported") diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index f461a3e0f4c..43c576249df 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio import logging -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any import switchbot @@ -39,7 +39,9 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): device: switchbot.SwitchbotDevice, ) -> None: """Initialize global switchbot data updater.""" - super().__init__(hass, logger, ble_device.address) + super().__init__( + hass, logger, ble_device.address, bluetooth.BluetoothScanningMode.ACTIVE + ) self.ble_device = ble_device self.device = device self.data: dict[str, Any] = {} @@ -48,14 +50,13 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): @callback def _async_handle_bluetooth_event( self, - service_info: bluetooth.BluetoothServiceInfo, + service_info: bluetooth.BluetoothServiceInfoBleak, change: bluetooth.BluetoothChange, ) -> None: """Handle a Bluetooth event.""" super()._async_handle_bluetooth_event(service_info, change) - discovery_info_bleak = cast(bluetooth.BluetoothServiceInfoBleak, service_info) if adv := switchbot.parse_advertisement_data( - discovery_info_bleak.device, discovery_info_bleak.advertisement + service_info.device, service_info.advertisement ): self.data = flatten_sensors_data(adv.data) if "modelName" in self.data: diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 4eb20dbd943..791ac1447ad 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -24,9 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, - _LOGGER, - address=address, + hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index f352f43d0bf..a2fa2d185dc 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -11,7 +11,8 @@ from xiaomi_ble.parser import EncryptionScheme from homeassistant.components import onboarding from homeassistant.components.bluetooth import ( - BluetoothServiceInfo, + BluetoothScanningMode, + BluetoothServiceInfoBleak, async_discovered_service_info, async_process_advertisements, ) @@ -30,11 +31,11 @@ class Discovery: """A discovered bluetooth device.""" title: str - discovery_info: BluetoothServiceInfo + discovery_info: BluetoothServiceInfoBleak device: DeviceData -def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str: +def _title(discovery_info: BluetoothServiceInfoBleak, device: DeviceData) -> str: return device.title or device.get_device_name() or discovery_info.name @@ -45,18 +46,20 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfo | None = None + self._discovery_info: BluetoothServiceInfoBleak | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, Discovery] = {} async def _async_wait_for_full_advertisement( - self, discovery_info: BluetoothServiceInfo, device: DeviceData - ) -> BluetoothServiceInfo: + self, discovery_info: BluetoothServiceInfoBleak, device: DeviceData + ) -> BluetoothServiceInfoBleak: """Sometimes first advertisement we receive is blank or incomplete. Wait until we get a useful one.""" if not device.pending: return discovery_info - def _process_more_advertisements(service_info: BluetoothServiceInfo) -> bool: + def _process_more_advertisements( + service_info: BluetoothServiceInfoBleak, + ) -> bool: device.update(service_info) return not device.pending @@ -64,11 +67,12 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): self.hass, _process_more_advertisements, {"address": discovery_info.address}, + BluetoothScanningMode.ACTIVE, ADDITIONAL_DISCOVERY_TIMEOUT, ) async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b5d5804f100..b0c04323005 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -27,12 +27,12 @@ from .util import uuid as uuid_util from .util.decorator import Registry if TYPE_CHECKING: + from .components.bluetooth import BluetoothServiceInfoBleak from .components.dhcp import DhcpServiceInfo from .components.hassio import HassioServiceInfo from .components.ssdp import SsdpServiceInfo from .components.usb import UsbServiceInfo from .components.zeroconf import ZeroconfServiceInfo - from .helpers.service_info.bluetooth import BluetoothServiceInfo from .helpers.service_info.mqtt import MqttServiceInfo _LOGGER = logging.getLogger(__name__) @@ -1485,7 +1485,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): ) async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> data_entry_flow.FlowResult: """Handle a flow initialized by Bluetooth discovery.""" return await self._async_step_discovery_without_unique_id() diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index bf2f95c12c6..c9d6ffe6065 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -15,11 +15,11 @@ from .typing import DiscoveryInfoType if TYPE_CHECKING: import asyncio + from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.ssdp import SsdpServiceInfo from homeassistant.components.zeroconf import ZeroconfServiceInfo - from .service_info.bluetooth import BluetoothServiceInfo from .service_info.mqtt import MqttServiceInfo _R = TypeVar("_R", bound="Awaitable[bool] | bool") @@ -97,7 +97,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow, Generic[_R]): return await self.async_step_confirm() async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfo + self, discovery_info: BluetoothServiceInfoBleak ) -> FlowResult: """Handle a flow initialized by bluetooth discovery.""" if self._async_in_progress() or self._async_current_entries(): diff --git a/homeassistant/helpers/service_info/bluetooth.py b/homeassistant/helpers/service_info/bluetooth.py index 968d1dde95f..0db3a39b114 100644 --- a/homeassistant/helpers/service_info/bluetooth.py +++ b/homeassistant/helpers/service_info/bluetooth.py @@ -1,4 +1,5 @@ """The bluetooth integration service info.""" + from home_assistant_bluetooth import BluetoothServiceInfo __all__ = ["BluetoothServiceInfo"] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1f86ff4c7c5..c9d424daa33 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 -bleak==0.14.3 +bleak==0.15.0 bluetooth-adapters==0.1.2 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index b7b3ed89b27..58f2e5f73ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -405,7 +405,7 @@ bimmer_connected==0.10.1 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak==0.14.3 +bleak==0.15.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d04b537c9fd..f7358e148dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -326,7 +326,7 @@ bellows==0.31.2 bimmer_connected==0.10.1 # homeassistant.components.bluetooth -bleak==0.14.3 +bleak==0.15.0 # homeassistant.components.blebox blebox_uniapi==2.0.2 diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 0664f82dbab..a47916506df 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -12,6 +12,7 @@ from homeassistant.components.bluetooth import ( SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, + BluetoothScanningMode, BluetoothServiceInfo, async_process_advertisements, async_track_unavailable, @@ -675,6 +676,7 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo hass, _fake_subscriber, {"service_uuids": {"cba20d00-224d-11e6-9fb8-0002a5d5c51b"}}, + BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 @@ -760,6 +762,7 @@ async def test_register_callback_by_address( hass, _fake_subscriber, {"address": "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, ) assert len(mock_bleak_scanner_start.mock_calls) == 1 @@ -799,6 +802,7 @@ async def test_register_callback_by_address( hass, _fake_subscriber, {"address": "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, ) cancel() @@ -808,6 +812,7 @@ async def test_register_callback_by_address( hass, _fake_subscriber, {"address": "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, ) cancel() @@ -832,7 +837,11 @@ async def test_process_advertisements_bail_on_good_advertisement( handle = hass.async_create_task( async_process_advertisements( - hass, _callback, {"address": "aa:44:33:11:23:45"}, 5 + hass, + _callback, + {"address": "aa:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, + 5, ) ) @@ -873,7 +882,11 @@ async def test_process_advertisements_ignore_bad_advertisement( handle = hass.async_create_task( async_process_advertisements( - hass, _callback, {"address": "aa:44:33:11:23:45"}, 5 + hass, + _callback, + {"address": "aa:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, + 5, ) ) @@ -903,7 +916,9 @@ async def test_process_advertisements_timeout( return False with pytest.raises(asyncio.TimeoutError): - await async_process_advertisements(hass, _callback, {}, 0) + await async_process_advertisements( + hass, _callback, {}, BluetoothScanningMode.ACTIVE, 0 + ) async def test_wrapped_instance_with_filter( diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 4da14fb13d3..31530cd6995 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -10,6 +10,7 @@ from homeassistant.components.bluetooth import ( DOMAIN, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, + BluetoothScanningMode, ) from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, @@ -42,9 +43,9 @@ GENERIC_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( class MyCoordinator(PassiveBluetoothDataUpdateCoordinator): """An example coordinator that subclasses PassiveBluetoothDataUpdateCoordinator.""" - def __init__(self, hass, logger, device_id) -> None: + def __init__(self, hass, logger, device_id, mode) -> None: """Initialize the coordinator.""" - super().__init__(hass, logger, device_id) + super().__init__(hass, logger, device_id, mode) self.data: dict[str, Any] = {} def _async_handle_bluetooth_event( @@ -60,11 +61,13 @@ class MyCoordinator(PassiveBluetoothDataUpdateCoordinator): async def test_basic_usage(hass, mock_bleak_scanner_start): """Test basic usage of the PassiveBluetoothDataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -100,11 +103,13 @@ async def test_context_compatiblity_with_data_update_coordinator( ): """Test contexts can be passed for compatibility with DataUpdateCoordinator.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -149,11 +154,13 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( ): await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) await hass.async_block_till_done() - coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.PASSIVE + ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -208,13 +215,15 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( async def test_passive_bluetooth_coordinator_entity(hass, mock_bleak_scanner_start): """Test integration of PassiveBluetoothDataUpdateCoordinator with PassiveBluetoothCoordinatorEntity.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - coordinator = MyCoordinator(hass, _LOGGER, "aa:bb:cc:dd:ee:ff") + coordinator = MyCoordinator( + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + ) entity = PassiveBluetoothCoordinatorEntity(coordinator) assert entity.available is False saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index ebb69d7c7d0..6a092746a68 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -16,6 +16,7 @@ from homeassistant.components.bluetooth import ( DOMAIN, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, + BluetoothScanningMode, ) from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, @@ -90,12 +91,12 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -192,12 +193,12 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -277,12 +278,12 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -336,12 +337,12 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -389,12 +390,12 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -731,12 +732,12 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): return GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -841,12 +842,12 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -905,12 +906,12 @@ async def test_passive_bluetooth_entity_with_entity_platform( return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -992,12 +993,12 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff" + hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE ) assert coordinator.available is False # no data yet saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index dfe47b38b33..f9f0a51fc0f 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -5,8 +5,9 @@ from datetime import timedelta from unittest.mock import patch from bleak import BleakError +from bleak.backends.scanner import AdvertisementData, BLEDevice -from homeassistant.components.bluetooth import BluetoothServiceInfo +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.bluetooth_le_tracker import device_tracker from homeassistant.components.bluetooth_le_tracker.device_tracker import ( CONF_TRACK_BATTERY, @@ -79,7 +80,7 @@ async def test_preserve_new_tracked_device_name( device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=name, address=address, rssi=-19, @@ -87,6 +88,8 @@ async def test_preserve_new_tracked_device_name( service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -100,7 +103,7 @@ async def test_preserve_new_tracked_device_name( assert result # Seen once here; return without name when seen subsequent times - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=None, address=address, rssi=-19, @@ -108,6 +111,8 @@ async def test_preserve_new_tracked_device_name( service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -140,7 +145,7 @@ async def test_tracking_battery_times_out( device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=name, address=address, rssi=-19, @@ -148,6 +153,8 @@ async def test_tracking_battery_times_out( service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -202,7 +209,7 @@ async def test_tracking_battery_fails(hass, mock_bluetooth, mock_device_tracker_ device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=name, address=address, rssi=-19, @@ -210,6 +217,8 @@ async def test_tracking_battery_fails(hass, mock_bluetooth, mock_device_tracker_ service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] @@ -266,7 +275,7 @@ async def test_tracking_battery_successful( device_tracker, "MIN_SEEN_NEW", 3 ): - device = BluetoothServiceInfo( + device = BluetoothServiceInfoBleak( name=name, address=address, rssi=-19, @@ -274,6 +283,8 @@ async def test_tracking_battery_successful( service_data={}, service_uuids=[], source="local", + device=BLEDevice(address, None), + advertisement=AdvertisementData(local_name="empty"), ) # Return with name when seen first time mock_async_discovered_service_info.return_value = [device] diff --git a/tests/components/fjaraskupan/conftest.py b/tests/components/fjaraskupan/conftest.py index d60abcdb9ad..4e06b2ad046 100644 --- a/tests/components/fjaraskupan/conftest.py +++ b/tests/components/fjaraskupan/conftest.py @@ -17,6 +17,12 @@ def fixture_scanner(hass): class MockScanner(BaseBleakScanner): """Mock Scanner.""" + def __init__(self, *args, **kwargs) -> None: + """Initialize the scanner.""" + super().__init__( + detection_callback=kwargs.pop("detection_callback"), service_uuids=[] + ) + async def start(self): """Start scanning for devices.""" for device in devices: diff --git a/tests/components/govee_ble/test_sensor.py b/tests/components/govee_ble/test_sensor.py index 7a6ecbaed51..75d269ea0ba 100644 --- a/tests/components/govee_ble/test_sensor.py +++ b/tests/components/govee_ble/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/inkbird/test_sensor.py b/tests/components/inkbird/test_sensor.py index a851cb92ec3..cafc22911c3 100644 --- a/tests/components/inkbird/test_sensor.py +++ b/tests/components/inkbird/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/moat/test_sensor.py b/tests/components/moat/test_sensor.py index 826bbc72cdb..6424144106b 100644 --- a/tests/components/moat/test_sensor.py +++ b/tests/components/moat/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/sensorpush/test_sensor.py b/tests/components/sensorpush/test_sensor.py index 31fbdd8d712..34179985d78 100644 --- a/tests/components/sensorpush/test_sensor.py +++ b/tests/components/sensorpush/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index b424228cc6c..86fda21aaa0 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -59,7 +59,9 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload(hass): async def test_async_step_bluetooth_valid_device_but_missing_payload_then_full(hass): """Test discovering a valid device. Payload is too short, but later we get full one.""" - async def _async_process_advertisements(_hass, _callback, _matcher, _timeout): + async def _async_process_advertisements( + _hass, _callback, _matcher, _mode, _timeout + ): service_info = make_advertisement( "A4:C1:38:56:53:84", b"XX\xe4\x16,\x84SV8\xc1\xa4+n\xf2\xe9\x12\x00\x00l\x88M\x9e", @@ -378,7 +380,9 @@ async def test_async_step_user_short_payload_then_full(hass): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - async def _async_process_advertisements(_hass, _callback, _matcher, _timeout): + async def _async_process_advertisements( + _hass, _callback, _matcher, _mode, _timeout + ): service_info = make_advertisement( "A4:C1:38:56:53:84", b"XX\xe4\x16,\x84SV8\xc1\xa4+n\xf2\xe9\x12\x00\x00l\x88M\x9e", diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index dca00e92254..011c6daecae 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -22,7 +22,7 @@ async def test_sensors(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -60,7 +60,7 @@ async def test_xiaomi_formaldeyhde(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -107,7 +107,7 @@ async def test_xiaomi_consumable(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -154,7 +154,7 @@ async def test_xiaomi_battery_voltage(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -208,7 +208,7 @@ async def test_xiaomi_HHCCJCY01(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None @@ -291,7 +291,7 @@ async def test_xiaomi_CGDK2(hass): saved_callback = None - def _async_register_callback(_hass, _callback, _matcher): + def _async_register_callback(_hass, _callback, _matcher, _mode): nonlocal saved_callback saved_callback = _callback return lambda: None From 8f8bccd982bf4a60016d98bb084244a251a08773 Mon Sep 17 00:00:00 2001 From: Bob van Mierlo <38190383+bobvmierlo@users.noreply.github.com> Date: Fri, 29 Jul 2022 22:46:30 +0200 Subject: [PATCH 2978/3516] Increase the discovery timeout (#75948) --- homeassistant/components/xiaomi_ble/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index a2fa2d185dc..aa1ffc24895 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -23,7 +23,7 @@ from homeassistant.data_entry_flow import FlowResult from .const import DOMAIN # How long to wait for additional advertisement packets if we don't have the right ones -ADDITIONAL_DISCOVERY_TIMEOUT = 5 +ADDITIONAL_DISCOVERY_TIMEOUT = 60 @dataclasses.dataclass From e2a9ab18311b974a6e60f54e967db18460490c52 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 29 Jul 2022 17:55:32 -0700 Subject: [PATCH 2979/3516] Bumped version to 2022.8.0b3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a3753d36d07..4a37902e79f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 558f366b357..8fca49a67f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0b2" +version = "2022.8.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From bb3e094552cc164f799d990fc904f471c02ad709 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Jul 2022 18:33:29 -1000 Subject: [PATCH 2980/3516] Fix switchbot failing to setup when last_run_success is not saved (#75887) --- homeassistant/components/switchbot/cover.py | 9 ++++++--- homeassistant/components/switchbot/switch.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index dc9ddf4e616..0ae225f55d7 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -80,9 +80,12 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): if not last_state or ATTR_CURRENT_POSITION not in last_state.attributes: return - self._attr_current_cover_position = last_state.attributes[ATTR_CURRENT_POSITION] - self._last_run_success = last_state.attributes["last_run_success"] - self._attr_is_closed = last_state.attributes[ATTR_CURRENT_POSITION] <= 20 + self._attr_current_cover_position = last_state.attributes.get( + ATTR_CURRENT_POSITION + ) + self._last_run_success = last_state.attributes.get("last_run_success") + if self._attr_current_cover_position is not None: + self._attr_is_closed = self._attr_current_cover_position <= 20 async def async_open_cover(self, **kwargs: Any) -> None: """Open the curtain.""" diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 51f15c488d1..e6ba77fa164 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -69,7 +69,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): if not (last_state := await self.async_get_last_state()): return self._attr_is_on = last_state.state == STATE_ON - self._last_run_success = last_state.attributes["last_run_success"] + self._last_run_success = last_state.attributes.get("last_run_success") async def async_turn_on(self, **kwargs: Any) -> None: """Turn device on.""" From 8181da70901c6b848ebc2efb2d39a7a3536599f3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 30 Jul 2022 11:04:31 +0200 Subject: [PATCH 2981/3516] Improve type hints in axis (#75910) --- homeassistant/components/axis/__init__.py | 7 +++--- homeassistant/components/axis/axis_base.py | 12 +++++----- .../components/axis/binary_sensor.py | 17 +++++++++----- homeassistant/components/axis/camera.py | 7 +++--- homeassistant/components/axis/device.py | 14 +++++++----- homeassistant/components/axis/diagnostics.py | 3 ++- homeassistant/components/axis/light.py | 22 +++++++++++-------- homeassistant/components/axis/switch.py | 19 ++++++++++------ 8 files changed, 61 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 4af066f4e89..56ddbc6d8c5 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -26,9 +26,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b except AuthenticationRequired as err: raise ConfigEntryAuthFailed from err - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] = AxisNetworkDevice( - hass, config_entry, api - ) + device = AxisNetworkDevice(hass, config_entry, api) + hass.data[AXIS_DOMAIN][config_entry.unique_id] = device await device.async_update_device_registry() await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) device.async_setup_events() @@ -43,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload Axis device config entry.""" - device = hass.data[AXIS_DOMAIN].pop(config_entry.unique_id) + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN].pop(config_entry.unique_id) return await device.async_reset() diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py index 3d887478528..2f21d900662 100644 --- a/homeassistant/components/axis/axis_base.py +++ b/homeassistant/components/axis/axis_base.py @@ -1,10 +1,12 @@ """Base classes for Axis entities.""" +from axis.event_stream import AxisEvent from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN as AXIS_DOMAIN +from .device import AxisNetworkDevice class AxisEntityBase(Entity): @@ -12,7 +14,7 @@ class AxisEntityBase(Entity): _attr_has_entity_name = True - def __init__(self, device): + def __init__(self, device: AxisNetworkDevice) -> None: """Initialize the Axis event.""" self.device = device @@ -20,7 +22,7 @@ class AxisEntityBase(Entity): identifiers={(AXIS_DOMAIN, device.unique_id)} ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe device events.""" self.async_on_remove( async_dispatcher_connect( @@ -29,12 +31,12 @@ class AxisEntityBase(Entity): ) @property - def available(self): + def available(self) -> bool: """Return True if device is available.""" return self.device.available @callback - def update_callback(self, no_delay=None): + def update_callback(self, no_delay=None) -> None: """Update the entities state.""" self.async_write_ha_state() @@ -44,7 +46,7 @@ class AxisEventBase(AxisEntityBase): _attr_should_poll = False - def __init__(self, event, device): + def __init__(self, event: AxisEvent, device: AxisNetworkDevice) -> None: """Initialize the Axis event.""" super().__init__(device) self.event = event diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index fba7e8d6248..3e90f5ff2a1 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -1,4 +1,6 @@ """Support for Axis binary sensors.""" +from __future__ import annotations + from datetime import timedelta from axis.event_stream import ( @@ -8,6 +10,8 @@ from axis.event_stream import ( CLASS_OUTPUT, CLASS_PTZ, CLASS_SOUND, + AxisBinaryEvent, + AxisEvent, FenceGuard, LoiteringGuard, MotionGuard, @@ -28,6 +32,7 @@ from homeassistant.util.dt import utcnow from .axis_base import AxisEventBase from .const import DOMAIN as AXIS_DOMAIN +from .device import AxisNetworkDevice DEVICE_CLASS = { CLASS_INPUT: BinarySensorDeviceClass.CONNECTIVITY, @@ -43,12 +48,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Axis binary sensor.""" - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] @callback def async_add_sensor(event_id): """Add binary sensor from Axis device.""" - event = device.api.event[event_id] + event: AxisEvent = device.api.event[event_id] if event.CLASS not in (CLASS_OUTPUT, CLASS_PTZ) and not ( event.CLASS == CLASS_LIGHT and event.TYPE == "Light" @@ -63,7 +68,9 @@ async def async_setup_entry( class AxisBinarySensor(AxisEventBase, BinarySensorEntity): """Representation of a binary Axis event.""" - def __init__(self, event, device): + event: AxisBinaryEvent + + def __init__(self, event: AxisEvent, device: AxisNetworkDevice) -> None: """Initialize the Axis binary sensor.""" super().__init__(event, device) self.cancel_scheduled_update = None @@ -98,12 +105,12 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): ) @property - def is_on(self): + def is_on(self) -> bool: """Return true if event is active.""" return self.event.is_tripped @property - def name(self): + def name(self) -> str | None: """Return the name of the event.""" if ( self.event.CLASS == CLASS_INPUT diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index 4df9a5e2141..992410d9725 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -11,6 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .axis_base import AxisEntityBase from .const import DEFAULT_STREAM_PROFILE, DEFAULT_VIDEO_SOURCE, DOMAIN as AXIS_DOMAIN +from .device import AxisNetworkDevice async def async_setup_entry( @@ -21,7 +22,7 @@ async def async_setup_entry( """Set up the Axis camera video stream.""" filter_urllib3_logging() - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] if not device.api.vapix.params.image_format: return @@ -34,7 +35,7 @@ class AxisCamera(AxisEntityBase, MjpegCamera): _attr_supported_features = CameraEntityFeature.STREAM - def __init__(self, device): + def __init__(self, device: AxisNetworkDevice) -> None: """Initialize Axis Communications camera component.""" AxisEntityBase.__init__(self, device) @@ -49,7 +50,7 @@ class AxisCamera(AxisEntityBase, MjpegCamera): self._attr_unique_id = f"{device.unique_id}-camera" - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe camera events.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 683991d0f65..ea7edcf0483 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -51,7 +51,9 @@ from .errors import AuthenticationRequired, CannotConnect class AxisNetworkDevice: """Manages a Axis device.""" - def __init__(self, hass, config_entry, api): + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, api: axis.AxisDevice + ) -> None: """Initialize the device.""" self.hass = hass self.config_entry = config_entry @@ -167,11 +169,11 @@ class AxisNetworkDevice: This is a static method because a class method (bound method), can not be used with weak references. """ - device = hass.data[AXIS_DOMAIN][entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][entry.unique_id] device.api.config.host = device.host async_dispatcher_send(hass, device.signal_new_address) - async def async_update_device_registry(self): + async def async_update_device_registry(self) -> None: """Update device registry.""" device_registry = dr.async_get(self.hass) device_registry.async_get_or_create( @@ -224,17 +226,17 @@ class AxisNetworkDevice: async_when_setup(self.hass, MQTT_DOMAIN, self.async_use_mqtt) @callback - def disconnect_from_stream(self): + def disconnect_from_stream(self) -> None: """Stop stream.""" if self.api.stream.state != STATE_STOPPED: self.api.stream.connection_status_callback.clear() self.api.stream.stop() - async def shutdown(self, event): + async def shutdown(self, event) -> None: """Stop the event stream.""" self.disconnect_from_stream() - async def async_reset(self): + async def async_reset(self) -> bool: """Reset this device to default state.""" self.disconnect_from_stream() diff --git a/homeassistant/components/axis/diagnostics.py b/homeassistant/components/axis/diagnostics.py index be3da8daf9e..1c805e8f35b 100644 --- a/homeassistant/components/axis/diagnostics.py +++ b/homeassistant/components/axis/diagnostics.py @@ -9,6 +9,7 @@ from homeassistant.const import CONF_MAC, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_US from homeassistant.core import HomeAssistant from .const import DOMAIN as AXIS_DOMAIN +from .device import AxisNetworkDevice REDACT_CONFIG = {CONF_MAC, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME} REDACT_BASIC_DEVICE_INFO = {"SerialNumber", "SocSerialNumber"} @@ -19,7 +20,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] diag: dict[str, Any] = {} diag["config"] = async_redact_data(config_entry.as_dict(), REDACT_CONFIG) diff --git a/homeassistant/components/axis/light.py b/homeassistant/components/axis/light.py index e34c0d4a2d6..c75d18b1908 100644 --- a/homeassistant/components/axis/light.py +++ b/homeassistant/components/axis/light.py @@ -1,5 +1,7 @@ """Support for Axis lights.""" -from axis.event_stream import CLASS_LIGHT +from typing import Any + +from axis.event_stream import CLASS_LIGHT, AxisBinaryEvent, AxisEvent from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry @@ -9,6 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .axis_base import AxisEventBase from .const import DOMAIN as AXIS_DOMAIN +from .device import AxisNetworkDevice async def async_setup_entry( @@ -17,7 +20,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Axis light.""" - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] if ( device.api.vapix.light_control is None @@ -28,7 +31,7 @@ async def async_setup_entry( @callback def async_add_sensor(event_id): """Add light from Axis device.""" - event = device.api.event[event_id] + event: AxisEvent = device.api.event[event_id] if event.CLASS == CLASS_LIGHT and event.TYPE == "Light": async_add_entities([AxisLight(event, device)]) @@ -42,8 +45,9 @@ class AxisLight(AxisEventBase, LightEntity): """Representation of a light Axis event.""" _attr_should_poll = True + event: AxisBinaryEvent - def __init__(self, event, device): + def __init__(self, event: AxisEvent, device: AxisNetworkDevice) -> None: """Initialize the Axis light.""" super().__init__(event, device) @@ -75,16 +79,16 @@ class AxisLight(AxisEventBase, LightEntity): self.max_intensity = max_intensity["data"]["ranges"][0]["high"] @property - def is_on(self): + def is_on(self) -> bool: """Return true if light is on.""" return self.event.is_tripped @property - def brightness(self): + def brightness(self) -> int: """Return the brightness of this light between 0..255.""" return int((self.current_intensity / self.max_intensity) * 255) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on light.""" if not self.is_on: await self.device.api.vapix.light_control.activate_light(self.light_id) @@ -95,12 +99,12 @@ class AxisLight(AxisEventBase, LightEntity): self.light_id, intensity ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off light.""" if self.is_on: await self.device.api.vapix.light_control.deactivate_light(self.light_id) - async def async_update(self): + async def async_update(self) -> None: """Update brightness.""" current_intensity = ( await self.device.api.vapix.light_control.get_current_intensity( diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index 61f16cfc789..6576e9d519e 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -1,5 +1,7 @@ """Support for Axis switches.""" -from axis.event_stream import CLASS_OUTPUT +from typing import Any + +from axis.event_stream import CLASS_OUTPUT, AxisBinaryEvent, AxisEvent from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -9,6 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .axis_base import AxisEventBase from .const import DOMAIN as AXIS_DOMAIN +from .device import AxisNetworkDevice async def async_setup_entry( @@ -17,12 +20,12 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Axis switch.""" - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] @callback def async_add_switch(event_id): """Add switch from Axis device.""" - event = device.api.event[event_id] + event: AxisEvent = device.api.event[event_id] if event.CLASS == CLASS_OUTPUT: async_add_entities([AxisSwitch(event, device)]) @@ -35,7 +38,9 @@ async def async_setup_entry( class AxisSwitch(AxisEventBase, SwitchEntity): """Representation of a Axis switch.""" - def __init__(self, event, device): + event: AxisBinaryEvent + + def __init__(self, event: AxisEvent, device: AxisNetworkDevice) -> None: """Initialize the Axis switch.""" super().__init__(event, device) @@ -43,14 +48,14 @@ class AxisSwitch(AxisEventBase, SwitchEntity): self._attr_name = device.api.vapix.ports[event.id].name @property - def is_on(self): + def is_on(self) -> bool: """Return true if event is active.""" return self.event.is_tripped - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" await self.device.api.vapix.ports[self.event.id].close() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off switch.""" await self.device.api.vapix.ports[self.event.id].open() From ace359b1bd1b86dd8c5f9989c79033496135ccb7 Mon Sep 17 00:00:00 2001 From: Alex Henry Date: Sun, 31 Jul 2022 00:04:24 +1200 Subject: [PATCH 2982/3516] Add multi-zone support to Anthem AV receiver and distribution solution (#74779) * Add multi-zone support to Anthem AV receiver and distribution amplifier * Fix typo in comment * Convert properties to attribute and add test * Migrate entity name * Fix after rebase add strict typing and bump version * fix typing * Simplify test * Small improvement * remove dispatcher send and use callback --- .strict-typing | 1 + homeassistant/components/anthemav/__init__.py | 9 +- .../components/anthemav/config_flow.py | 10 +- homeassistant/components/anthemav/const.py | 1 + .../components/anthemav/manifest.json | 2 +- .../components/anthemav/media_player.py | 123 ++++++++---------- mypy.ini | 11 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/anthemav/conftest.py | 35 ++++- tests/components/anthemav/test_init.py | 37 +++--- .../components/anthemav/test_media_player.py | 71 ++++++++++ 12 files changed, 200 insertions(+), 104 deletions(-) create mode 100644 tests/components/anthemav/test_media_player.py diff --git a/.strict-typing b/.strict-typing index d3102572eb1..ddeaf4f3844 100644 --- a/.strict-typing +++ b/.strict-typing @@ -56,6 +56,7 @@ homeassistant.components.ambee.* homeassistant.components.ambient_station.* homeassistant.components.amcrest.* homeassistant.components.ampio.* +homeassistant.components.anthemav.* homeassistant.components.aseko_pool_live.* homeassistant.components.asuswrt.* homeassistant.components.automation.* diff --git a/homeassistant/components/anthemav/__init__.py b/homeassistant/components/anthemav/__init__.py index eb1d9b0b560..24a83d0aff0 100644 --- a/homeassistant/components/anthemav/__init__.py +++ b/homeassistant/components/anthemav/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging import anthemav +from anthemav.device_error import DeviceError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform @@ -11,7 +12,7 @@ from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import ANTHEMAV_UDATE_SIGNAL, DOMAIN +from .const import ANTHEMAV_UDATE_SIGNAL, DEVICE_TIMEOUT_SECONDS, DOMAIN PLATFORMS = [Platform.MEDIA_PLAYER] @@ -22,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Anthem A/V Receivers from a config entry.""" @callback - def async_anthemav_update_callback(message): + def async_anthemav_update_callback(message: str) -> None: """Receive notification from transport that new data exists.""" _LOGGER.debug("Received update callback from AVR: %s", message) async_dispatcher_send(hass, f"{ANTHEMAV_UDATE_SIGNAL}_{entry.entry_id}") @@ -34,7 +35,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_callback=async_anthemav_update_callback, ) - except OSError as err: + # Wait for the zones to be initialised based on the model + await avr.protocol.wait_for_device_initialised(DEVICE_TIMEOUT_SECONDS) + except (OSError, DeviceError) as err: raise ConfigEntryNotReady from err hass.data.setdefault(DOMAIN, {})[entry.entry_id] = avr diff --git a/homeassistant/components/anthemav/config_flow.py b/homeassistant/components/anthemav/config_flow.py index 55e1fd42e07..0e878bcc913 100644 --- a/homeassistant/components/anthemav/config_flow.py +++ b/homeassistant/components/anthemav/config_flow.py @@ -15,9 +15,13 @@ from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import format_mac -from .const import CONF_MODEL, DEFAULT_NAME, DEFAULT_PORT, DOMAIN - -DEVICE_TIMEOUT_SECONDS = 4.0 +from .const import ( + CONF_MODEL, + DEFAULT_NAME, + DEFAULT_PORT, + DEVICE_TIMEOUT_SECONDS, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/anthemav/const.py b/homeassistant/components/anthemav/const.py index c63a477f7f9..02f56aed5c4 100644 --- a/homeassistant/components/anthemav/const.py +++ b/homeassistant/components/anthemav/const.py @@ -5,3 +5,4 @@ DEFAULT_NAME = "Anthem AV" DEFAULT_PORT = 14999 DOMAIN = "anthemav" MANUFACTURER = "Anthem" +DEVICE_TIMEOUT_SECONDS = 4.0 diff --git a/homeassistant/components/anthemav/manifest.json b/homeassistant/components/anthemav/manifest.json index 27db9df32a3..2055ec75f27 100644 --- a/homeassistant/components/anthemav/manifest.json +++ b/homeassistant/components/anthemav/manifest.json @@ -2,7 +2,7 @@ "domain": "anthemav", "name": "Anthem A/V Receivers", "documentation": "https://www.home-assistant.io/integrations/anthemav", - "requirements": ["anthemav==1.3.2"], + "requirements": ["anthemav==1.4.1"], "dependencies": ["repairs"], "codeowners": ["@hyralex"], "config_flow": true, diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index bf8172083e6..4754a416d27 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -2,13 +2,14 @@ from __future__ import annotations import logging -from typing import Any from anthemav.connection import Connection +from anthemav.protocol import AVR import voluptuous as vol from homeassistant.components.media_player import ( PLATFORM_SCHEMA, + MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, ) @@ -22,7 +23,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo @@ -88,20 +89,28 @@ async def async_setup_entry( mac_address = config_entry.data[CONF_MAC] model = config_entry.data[CONF_MODEL] - avr = hass.data[DOMAIN][config_entry.entry_id] + avr: Connection = hass.data[DOMAIN][config_entry.entry_id] - entity = AnthemAVR(avr, name, mac_address, model, config_entry.entry_id) + entities = [] + for zone_number in avr.protocol.zones: + _LOGGER.debug("Initializing Zone %s", zone_number) + entity = AnthemAVR( + avr.protocol, name, mac_address, model, zone_number, config_entry.entry_id + ) + entities.append(entity) - _LOGGER.debug("Device data dump: %s", entity.dump_avrdata) _LOGGER.debug("Connection data dump: %s", avr.dump_conndata) - async_add_entities([entity]) + async_add_entities(entities) class AnthemAVR(MediaPlayerEntity): """Entity reading values from Anthem AVR protocol.""" + _attr_has_entity_name = True _attr_should_poll = False + _attr_device_class = MediaPlayerDeviceClass.RECEIVER + _attr_icon = "mdi:audio-video" _attr_supported_features = ( MediaPlayerEntityFeature.VOLUME_SET | MediaPlayerEntityFeature.VOLUME_MUTE @@ -111,23 +120,33 @@ class AnthemAVR(MediaPlayerEntity): ) def __init__( - self, avr: Connection, name: str, mac_address: str, model: str, entry_id: str + self, + avr: AVR, + name: str, + mac_address: str, + model: str, + zone_number: int, + entry_id: str, ) -> None: """Initialize entity with transport.""" super().__init__() self.avr = avr self._entry_id = entry_id - self._attr_name = name - self._attr_unique_id = mac_address + self._zone_number = zone_number + self._zone = avr.zones[zone_number] + if zone_number > 1: + self._attr_name = f"zone {zone_number}" + self._attr_unique_id = f"{mac_address}_{zone_number}" + else: + self._attr_unique_id = mac_address + self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, mac_address)}, name=name, manufacturer=MANUFACTURER, model=model, ) - - def _lookup(self, propname: str, dval: Any | None = None) -> Any | None: - return getattr(self.avr.protocol, propname, dval) + self.set_states() async def async_added_to_hass(self) -> None: """When entity is added to hass.""" @@ -135,82 +154,42 @@ class AnthemAVR(MediaPlayerEntity): async_dispatcher_connect( self.hass, f"{ANTHEMAV_UDATE_SIGNAL}_{self._entry_id}", - self.async_write_ha_state, + self.update_states, ) ) - @property - def state(self) -> str | None: - """Return state of power on/off.""" - pwrstate = self._lookup("power") + @callback + def update_states(self) -> None: + """Update states for the current zone.""" + self.set_states() + self.async_write_ha_state() - if pwrstate is True: - return STATE_ON - if pwrstate is False: - return STATE_OFF - return None - - @property - def is_volume_muted(self) -> bool | None: - """Return boolean reflecting mute state on device.""" - return self._lookup("mute", False) - - @property - def volume_level(self) -> float | None: - """Return volume level from 0 to 1.""" - return self._lookup("volume_as_percentage", 0.0) - - @property - def media_title(self) -> str | None: - """Return current input name (closest we have to media title).""" - return self._lookup("input_name", "No Source") - - @property - def app_name(self) -> str | None: - """Return details about current video and audio stream.""" - return ( - f"{self._lookup('video_input_resolution_text', '')} " - f"{self._lookup('audio_input_name', '')}" - ) - - @property - def source(self) -> str | None: - """Return currently selected input.""" - return self._lookup("input_name", "Unknown") - - @property - def source_list(self) -> list[str] | None: - """Return all active, configured inputs.""" - return self._lookup("input_list", ["Unknown"]) + def set_states(self) -> None: + """Set all the states from the device to the entity.""" + self._attr_state = STATE_ON if self._zone.power is True else STATE_OFF + self._attr_is_volume_muted = self._zone.mute + self._attr_volume_level = self._zone.volume_as_percentage + self._attr_media_title = self._zone.input_name + self._attr_app_name = self._zone.input_format + self._attr_source = self._zone.input_name + self._attr_source_list = self.avr.input_list async def async_select_source(self, source: str) -> None: """Change AVR to the designated source (by name).""" - self._update_avr("input_name", source) + self._zone.input_name = source async def async_turn_off(self) -> None: """Turn AVR power off.""" - self._update_avr("power", False) + self._zone.power = False async def async_turn_on(self) -> None: """Turn AVR power on.""" - self._update_avr("power", True) + self._zone.power = True async def async_set_volume_level(self, volume: float) -> None: """Set AVR volume (0 to 1).""" - self._update_avr("volume_as_percentage", volume) + self._zone.volume_as_percentage = volume async def async_mute_volume(self, mute: bool) -> None: """Engage AVR mute.""" - self._update_avr("mute", mute) - - def _update_avr(self, propname: str, value: Any | None) -> None: - """Update a property in the AVR.""" - _LOGGER.debug("Sending command to AVR: set %s to %s", propname, str(value)) - setattr(self.avr.protocol, propname, value) - - @property - def dump_avrdata(self): - """Return state of avr object for debugging forensics.""" - attrs = vars(self) - items_string = ", ".join(f"{item}: {item}" for item in attrs.items()) - return f"dump_avrdata: {items_string}" + self._zone.mute = mute diff --git a/mypy.ini b/mypy.ini index af039c74de3..4ef79368f73 100644 --- a/mypy.ini +++ b/mypy.ini @@ -339,6 +339,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.anthemav.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.aseko_pool_live.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 7fe58a0899b..9b5e51affe6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -313,7 +313,7 @@ androidtv[async]==0.0.67 anel_pwrctrl-homeassistant==0.0.1.dev2 # homeassistant.components.anthemav -anthemav==1.3.2 +anthemav==1.4.1 # homeassistant.components.apcupsd apcaccess==0.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 16919de3c48..fb09140e435 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -279,7 +279,7 @@ ambiclimate==0.2.1 androidtv[async]==0.0.67 # homeassistant.components.anthemav -anthemav==1.3.2 +anthemav==1.4.1 # homeassistant.components.apprise apprise==0.9.9 diff --git a/tests/components/anthemav/conftest.py b/tests/components/anthemav/conftest.py index f96696fe308..595c867304b 100644 --- a/tests/components/anthemav/conftest.py +++ b/tests/components/anthemav/conftest.py @@ -1,10 +1,12 @@ """Fixtures for anthemav integration tests.""" +from typing import Callable from unittest.mock import AsyncMock, MagicMock, patch import pytest from homeassistant.components.anthemav.const import CONF_MODEL, DOMAIN from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -16,17 +18,25 @@ def mock_anthemav() -> AsyncMock: avr.protocol.macaddress = "000000000001" avr.protocol.model = "MRX 520" avr.reconnect = AsyncMock() + avr.protocol.wait_for_device_initialised = AsyncMock() avr.close = MagicMock() avr.protocol.input_list = [] avr.protocol.audio_listening_mode_list = [] - avr.protocol.power = False + avr.protocol.zones = {1: get_zone(), 2: get_zone()} return avr +def get_zone() -> MagicMock: + """Return a mocked zone.""" + zone = MagicMock() + + zone.power = False + return zone + + @pytest.fixture def mock_connection_create(mock_anthemav: AsyncMock) -> AsyncMock: """Return the default mocked connection.create.""" - with patch( "anthemav.Connection.create", return_value=mock_anthemav, @@ -34,6 +44,12 @@ def mock_connection_create(mock_anthemav: AsyncMock) -> AsyncMock: yield mock +@pytest.fixture +def update_callback(mock_connection_create: AsyncMock) -> Callable[[str], None]: + """Return the update_callback used when creating the connection.""" + return mock_connection_create.call_args[1]["update_callback"] + + @pytest.fixture def mock_config_entry() -> MockConfigEntry: """Return the default mocked config entry.""" @@ -48,3 +64,18 @@ def mock_config_entry() -> MockConfigEntry: }, unique_id="00:00:00:00:00:01", ) + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_connection_create: AsyncMock, +) -> MockConfigEntry: + """Set up the AnthemAv integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/anthemav/test_init.py b/tests/components/anthemav/test_init.py index 97dc5be95b0..63bd8390958 100644 --- a/tests/components/anthemav/test_init.py +++ b/tests/components/anthemav/test_init.py @@ -1,6 +1,10 @@ """Test the Anthem A/V Receivers config flow.""" +from typing import Callable from unittest.mock import ANY, AsyncMock, patch +from anthemav.device_error import DeviceError +import pytest + from homeassistant import config_entries from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant @@ -12,35 +16,31 @@ async def test_load_unload_config_entry( hass: HomeAssistant, mock_connection_create: AsyncMock, mock_anthemav: AsyncMock, - mock_config_entry: MockConfigEntry, + init_integration: MockConfigEntry, ) -> None: """Test load and unload AnthemAv component.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - # assert avr is created mock_connection_create.assert_called_with( host="1.1.1.1", port=14999, update_callback=ANY ) - assert mock_config_entry.state == config_entries.ConfigEntryState.LOADED + assert init_integration.state == config_entries.ConfigEntryState.LOADED # unload - await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.config_entries.async_unload(init_integration.entry_id) await hass.async_block_till_done() # assert unload and avr is closed - assert mock_config_entry.state == config_entries.ConfigEntryState.NOT_LOADED + assert init_integration.state == config_entries.ConfigEntryState.NOT_LOADED mock_anthemav.close.assert_called_once() -async def test_config_entry_not_ready( - hass: HomeAssistant, mock_config_entry: MockConfigEntry +@pytest.mark.parametrize("error", [OSError, DeviceError]) +async def test_config_entry_not_ready_when_oserror( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, error: Exception ) -> None: """Test AnthemAV configuration entry not ready.""" - with patch( "anthemav.Connection.create", - side_effect=OSError, + side_effect=error, ): mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -52,23 +52,18 @@ async def test_anthemav_dispatcher_signal( hass: HomeAssistant, mock_connection_create: AsyncMock, mock_anthemav: AsyncMock, - mock_config_entry: MockConfigEntry, + init_integration: MockConfigEntry, + update_callback: Callable[[str], None], ) -> None: """Test send update signal to dispatcher.""" - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - states = hass.states.get("media_player.anthem_av") assert states assert states.state == STATE_OFF # change state of the AVR - mock_anthemav.protocol.power = True + mock_anthemav.protocol.zones[1].power = True - # get the callback function that trigger the signal to update the state - avr_update_callback = mock_connection_create.call_args[1]["update_callback"] - avr_update_callback("power") + update_callback("power") await hass.async_block_till_done() diff --git a/tests/components/anthemav/test_media_player.py b/tests/components/anthemav/test_media_player.py new file mode 100644 index 00000000000..a741c5e3b53 --- /dev/null +++ b/tests/components/anthemav/test_media_player.py @@ -0,0 +1,71 @@ +"""Test the Anthem A/V Receivers config flow.""" +from typing import Callable +from unittest.mock import AsyncMock + +import pytest + +from homeassistant.components.media_player.const import ( + ATTR_APP_NAME, + ATTR_INPUT_SOURCE, + ATTR_INPUT_SOURCE_LIST, + ATTR_MEDIA_TITLE, + ATTR_MEDIA_VOLUME_MUTED, +) +from homeassistant.components.siren.const import ATTR_VOLUME_LEVEL +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.mark.parametrize( + "entity_id,entity_name", + [ + ("media_player.anthem_av", "Anthem AV"), + ("media_player.anthem_av_zone_2", "Anthem AV zone 2"), + ], +) +async def test_zones_loaded( + hass: HomeAssistant, + init_integration: MockConfigEntry, + entity_id: str, + entity_name: str, +) -> None: + """Test zones are loaded.""" + + states = hass.states.get(entity_id) + + assert states + assert states.state == STATE_OFF + assert states.name == entity_name + + +async def test_update_states_zone1( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_anthemav: AsyncMock, + update_callback: Callable[[str], None], +) -> None: + """Test zone states are updated.""" + + mock_zone = mock_anthemav.protocol.zones[1] + + mock_zone.power = True + mock_zone.mute = True + mock_zone.volume_as_percentage = 42 + mock_zone.input_name = "TEST INPUT" + mock_zone.input_format = "2.0 PCM" + mock_anthemav.protocol.input_list = ["TEST INPUT", "INPUT 2"] + + update_callback("command") + await hass.async_block_till_done() + + states = hass.states.get("media_player.anthem_av") + assert states + assert states.state == STATE_ON + assert states.attributes[ATTR_VOLUME_LEVEL] == 42 + assert states.attributes[ATTR_MEDIA_VOLUME_MUTED] is True + assert states.attributes[ATTR_INPUT_SOURCE] == "TEST INPUT" + assert states.attributes[ATTR_MEDIA_TITLE] == "TEST INPUT" + assert states.attributes[ATTR_APP_NAME] == "2.0 PCM" + assert states.attributes[ATTR_INPUT_SOURCE_LIST] == ["TEST INPUT", "INPUT 2"] From faf25b2235cabea7ceeb94908f94949dc20c1806 Mon Sep 17 00:00:00 2001 From: ildar170975 <71872483+ildar170975@users.noreply.github.com> Date: Sat, 30 Jul 2022 15:06:55 +0300 Subject: [PATCH 2983/3516] Add telegram disable_web_page_preview (#75898) * Add telegram disable_web_page_preview Adds ability to specify disable_web_page_preview to telegram.notify: ``` - service: notify.telegram data: message: >- HA site data: parse_mode: html disable_web_page_preview: true ``` * Update homeassistant/components/telegram/notify.py Co-authored-by: Martin Hjelmare * Update notify.py * Update notify.py Co-authored-by: Martin Hjelmare --- homeassistant/components/telegram/notify.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/telegram/notify.py b/homeassistant/components/telegram/notify.py index b87ddc670c3..bca48e4b85a 100644 --- a/homeassistant/components/telegram/notify.py +++ b/homeassistant/components/telegram/notify.py @@ -13,6 +13,7 @@ from homeassistant.components.notify import ( ) from homeassistant.components.telegram_bot import ( ATTR_DISABLE_NOTIF, + ATTR_DISABLE_WEB_PREV, ATTR_MESSAGE_TAG, ATTR_PARSER, ) @@ -76,6 +77,11 @@ class TelegramNotificationService(BaseNotificationService): parse_mode = data.get(ATTR_PARSER) service_data.update({ATTR_PARSER: parse_mode}) + # Set disable_web_page_preview + if data is not None and ATTR_DISABLE_WEB_PREV in data: + disable_web_page_preview = data[ATTR_DISABLE_WEB_PREV] + service_data.update({ATTR_DISABLE_WEB_PREV: disable_web_page_preview}) + # Get keyboard info if data is not None and ATTR_KEYBOARD in data: keys = data.get(ATTR_KEYBOARD) From b9b916cdcd32e09c37637fd5895a4ef2b09c8a44 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Jul 2022 06:15:06 -0700 Subject: [PATCH 2984/3516] Bump govee-ble to fix H5179 sensors (#75957) Changelog: https://github.com/Bluetooth-Devices/govee-ble/compare/v0.12.4...v0.12.5 --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 270858d04d4..c7909d3e1af 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -24,7 +24,7 @@ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.12.4"], + "requirements": ["govee-ble==0.12.5"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 9b5e51affe6..c602bb56697 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.4 +govee-ble==0.12.5 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fb09140e435..0caac1ec257 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,7 +561,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.4 +govee-ble==0.12.5 # homeassistant.components.gree greeclimate==1.2.0 From 377f56ff5f6c3031b6abf4de1db630f6177050d4 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 31 Jul 2022 00:25:44 +0000 Subject: [PATCH 2985/3516] [ci skip] Translation update --- .../components/discord/translations/sv.json | 13 ++++++++++ .../components/fibaro/translations/sv.json | 9 +++++++ .../components/filesize/translations/sv.json | 7 ++++++ .../components/group/translations/sv.json | 24 ++++++++++++++++++- .../components/qnap_qsw/translations/sv.json | 2 ++ .../radiotherm/translations/sv.json | 1 + .../components/samsungtv/translations/sv.json | 3 +++ .../components/scrape/translations/sv.json | 12 +++++++++- .../components/senz/translations/ja.json | 1 + .../components/sql/translations/sv.json | 6 ++++- .../components/tautulli/translations/sv.json | 9 +++++++ .../trafikverket_train/translations/sv.json | 5 ++++ 12 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/discord/translations/sv.json create mode 100644 homeassistant/components/filesize/translations/sv.json diff --git a/homeassistant/components/discord/translations/sv.json b/homeassistant/components/discord/translations/sv.json new file mode 100644 index 00000000000..1b52e7816d2 --- /dev/null +++ b/homeassistant/components/discord/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/sv.json b/homeassistant/components/fibaro/translations/sv.json index 23c825f256f..89cfc8f6c3b 100644 --- a/homeassistant/components/fibaro/translations/sv.json +++ b/homeassistant/components/fibaro/translations/sv.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/filesize/translations/sv.json b/homeassistant/components/filesize/translations/sv.json new file mode 100644 index 00000000000..c0b662beebe --- /dev/null +++ b/homeassistant/components/filesize/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/sv.json b/homeassistant/components/group/translations/sv.json index 8ba098e02cd..93cbf568052 100644 --- a/homeassistant/components/group/translations/sv.json +++ b/homeassistant/components/group/translations/sv.json @@ -9,6 +9,12 @@ }, "title": "Ny grupp" }, + "cover": { + "title": "L\u00e4gg till grupp" + }, + "fan": { + "title": "L\u00e4gg till grupp" + }, "light": { "title": "L\u00e4gg till grupp" }, @@ -20,6 +26,15 @@ }, "title": "L\u00e4gg till grupp" }, + "media_player": { + "title": "L\u00e4gg till grupp" + }, + "switch": { + "data": { + "name": "Namn" + }, + "title": "L\u00e4gg till grupp" + }, "user": { "menu_options": { "lock": "L\u00e5sgrupp" @@ -32,7 +47,8 @@ "step": { "binary_sensor": { "data": { - "all": "Alla entiteter" + "all": "Alla entiteter", + "entities": "Medlemmar" } }, "lock": { @@ -40,6 +56,12 @@ "entities": "Medlemmar", "hide_members": "D\u00f6lj medlemmar" } + }, + "switch": { + "data": { + "all": "Alla entiteter", + "entities": "Medlemmar" + } } } }, diff --git a/homeassistant/components/qnap_qsw/translations/sv.json b/homeassistant/components/qnap_qsw/translations/sv.json index f447af18c52..8db64916805 100644 --- a/homeassistant/components/qnap_qsw/translations/sv.json +++ b/homeassistant/components/qnap_qsw/translations/sv.json @@ -16,6 +16,8 @@ }, "user": { "data": { + "password": "L\u00f6senord", + "url": "URL", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/radiotherm/translations/sv.json b/homeassistant/components/radiotherm/translations/sv.json index 3999a2a90e0..54b0407d29e 100644 --- a/homeassistant/components/radiotherm/translations/sv.json +++ b/homeassistant/components/radiotherm/translations/sv.json @@ -7,6 +7,7 @@ "cannot_connect": "Det gick inte att ansluta.", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{name} {model} ({host})", "step": { "confirm": { "description": "Vill du konfigurera {name} {model} ( {host} )?" diff --git a/homeassistant/components/samsungtv/translations/sv.json b/homeassistant/components/samsungtv/translations/sv.json index 0141800c5c0..feff1c2fc86 100644 --- a/homeassistant/components/samsungtv/translations/sv.json +++ b/homeassistant/components/samsungtv/translations/sv.json @@ -13,6 +13,9 @@ "confirm": { "description": "Vill du st\u00e4lla in Samsung TV {device}? Om du aldrig har anslutit Home Assistant innan du ska se ett popup-f\u00f6nster p\u00e5 tv:n och be om auktorisering. Manuella konfigurationer f\u00f6r den h\u00e4r TV:n skrivs \u00f6ver." }, + "pairing": { + "description": "Vill du st\u00e4lla in Samsung TV {device}? Om du aldrig har anslutit Home Assistant innan du ska se ett popup-f\u00f6nster p\u00e5 tv:n och be om auktorisering. Manuella konfigurationer f\u00f6r den h\u00e4r TV:n skrivs \u00f6ver." + }, "user": { "data": { "host": "V\u00e4rdnamn eller IP-adress", diff --git a/homeassistant/components/scrape/translations/sv.json b/homeassistant/components/scrape/translations/sv.json index f1cf81c5aa6..2c4da93bb6d 100644 --- a/homeassistant/components/scrape/translations/sv.json +++ b/homeassistant/components/scrape/translations/sv.json @@ -32,10 +32,20 @@ "data": { "attribute": "Attribut", "authentication": "Autentisering", + "device_class": "Enhetsklass", + "headers": "Headers", "index": "Index", "password": "L\u00f6senord", + "resource": "Resurs", "select": "V\u00e4lj", - "username": "Anv\u00e4ndarnamn" + "state_class": "Tillst\u00e5ndsklass", + "unit_of_measurement": "M\u00e5ttenhet", + "username": "Anv\u00e4ndarnamn", + "value_template": "V\u00e4rdemall", + "verify_ssl": "Verifiera SSL-certifikat" + }, + "data_description": { + "attribute": "H\u00e4mta v\u00e4rdet av ett attribut p\u00e5 den valda taggen" } } } diff --git a/homeassistant/components/senz/translations/ja.json b/homeassistant/components/senz/translations/ja.json index dbb794e2cff..1d454cf23bc 100644 --- a/homeassistant/components/senz/translations/ja.json +++ b/homeassistant/components/senz/translations/ja.json @@ -19,6 +19,7 @@ }, "issues": { "removed_yaml": { + "description": "nVent RAYCHEM SENZ\u306f\u3001YAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306e\u3001YAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "nVent RAYCHEM SENZ YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/sql/translations/sv.json b/homeassistant/components/sql/translations/sv.json index 9009cac8ee3..dec86c2b66b 100644 --- a/homeassistant/components/sql/translations/sv.json +++ b/homeassistant/components/sql/translations/sv.json @@ -14,12 +14,16 @@ "name": "Namn" }, "data_description": { - "name": "Namn som kommer att anv\u00e4ndas f\u00f6r konfigurationsinmatning och \u00e4ven f\u00f6r sensorn." + "name": "Namn som kommer att anv\u00e4ndas f\u00f6r konfigurationsinmatning och \u00e4ven f\u00f6r sensorn.", + "value_template": "V\u00e4rdemall (valfritt)" } } } }, "options": { + "error": { + "db_url_invalid": "Databasens URL \u00e4r ogiltig" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/tautulli/translations/sv.json b/homeassistant/components/tautulli/translations/sv.json index 66b23701c73..abcbe307998 100644 --- a/homeassistant/components/tautulli/translations/sv.json +++ b/homeassistant/components/tautulli/translations/sv.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "reauth_successful": "\u00c5terautentisering lyckades", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/trafikverket_train/translations/sv.json b/homeassistant/components/trafikverket_train/translations/sv.json index b6d7ecada04..e08a6492532 100644 --- a/homeassistant/components/trafikverket_train/translations/sv.json +++ b/homeassistant/components/trafikverket_train/translations/sv.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "incorrect_api_key": "Ogiltig API-nyckel f\u00f6r valt konto", "invalid_auth": "Ogiltig autentisering", "invalid_station": "Det gick inte att hitta en station med det angivna namnet", From abb7495ced8d9ea3cd13a33939d7fd5aa2afbd61 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sun, 31 Jul 2022 12:21:25 +0200 Subject: [PATCH 2986/3516] Handle failed connection attempts in opentherm_gw (#75961) --- .../components/opentherm_gw/__init__.py | 18 ++++++++++++++++-- .../components/opentherm_gw/config_flow.py | 15 +++++++++++---- homeassistant/components/opentherm_gw/const.py | 2 ++ .../components/opentherm_gw/strings.json | 3 ++- .../opentherm_gw/test_config_flow.py | 2 +- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 7c27eeceede..cdf360c8795 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -1,9 +1,11 @@ """Support for OpenTherm Gateway devices.""" +import asyncio from datetime import date, datetime import logging import pyotgw import pyotgw.vars as gw_vars +from serial import SerialException import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -23,6 +25,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType @@ -37,6 +40,7 @@ from .const import ( CONF_PRECISION, CONF_READ_PRECISION, CONF_SET_PRECISION, + CONNECTION_TIMEOUT, DATA_GATEWAYS, DATA_OPENTHERM_GW, DOMAIN, @@ -107,8 +111,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b config_entry.add_update_listener(options_updated) - # Schedule directly on the loop to avoid blocking HA startup. - hass.loop.create_task(gateway.connect_and_subscribe()) + try: + await asyncio.wait_for( + gateway.connect_and_subscribe(), + timeout=CONNECTION_TIMEOUT, + ) + except (asyncio.TimeoutError, ConnectionError, SerialException) as ex: + raise ConfigEntryNotReady( + f"Could not connect to gateway at {gateway.device_path}: {ex}" + ) from ex await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -428,6 +439,9 @@ class OpenThermGatewayDevice: async def connect_and_subscribe(self): """Connect to serial device and subscribe report handler.""" self.status = await self.gateway.connect(self.device_path) + if not self.status: + await self.cleanup() + raise ConnectionError version_string = self.status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) self.gw_version = version_string[18:] if version_string else None _LOGGER.debug( diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 3f91496adab..c3a955b2387 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -26,6 +26,7 @@ from .const import ( CONF_READ_PRECISION, CONF_SET_PRECISION, CONF_TEMPORARY_OVRD_MODE, + CONNECTION_TIMEOUT, ) @@ -62,15 +63,21 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): otgw = pyotgw.OpenThermGateway() status = await otgw.connect(device) await otgw.disconnect() + if not status: + raise ConnectionError return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) try: - res = await asyncio.wait_for(test_connection(), timeout=10) - except (asyncio.TimeoutError, SerialException): + await asyncio.wait_for( + test_connection(), + timeout=CONNECTION_TIMEOUT, + ) + except asyncio.TimeoutError: + return self._show_form({"base": "timeout_connect"}) + except (ConnectionError, SerialException): return self._show_form({"base": "cannot_connect"}) - if res: - return self._create_entry(gw_id, name, device) + return self._create_entry(gw_id, name, device) return self._show_form() diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index a5042628529..d72469759f1 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -25,6 +25,8 @@ CONF_READ_PRECISION = "read_precision" CONF_SET_PRECISION = "set_precision" CONF_TEMPORARY_OVRD_MODE = "temporary_override_mode" +CONNECTION_TIMEOUT = 10 + DATA_GATEWAYS = "gateways" DATA_OPENTHERM_GW = "opentherm_gw" diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json index f53ffeda6f6..a80a059481d 100644 --- a/homeassistant/components/opentherm_gw/strings.json +++ b/homeassistant/components/opentherm_gw/strings.json @@ -12,7 +12,8 @@ "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "id_exists": "Gateway id already exists", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" } }, "options": { diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index 46d53bc54b5..080e9a96d58 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -164,7 +164,7 @@ async def test_form_connection_timeout(hass): ) assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} + assert result2["errors"] == {"base": "timeout_connect"} assert len(mock_connect.mock_calls) == 1 From 1204b4f7000cda093290ecf3d07eaca2d00bdee1 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Sun, 31 Jul 2022 12:26:16 +0100 Subject: [PATCH 2987/3516] Add typings to Certificate Expiry integration (#75945) --- .../components/cert_expiry/config_flow.py | 18 +++++++++++++++--- homeassistant/components/cert_expiry/helper.py | 12 ++++++++++-- homeassistant/components/cert_expiry/sensor.py | 10 ++++++++-- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index 13336c59771..ed294cab981 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -1,12 +1,15 @@ """Config flow for the Cert Expiry platform.""" from __future__ import annotations +from collections.abc import Mapping import logging +from typing import Any import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.data_entry_flow import FlowResult from .const import DEFAULT_PORT, DOMAIN from .errors import ( @@ -29,7 +32,10 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Initialize the config flow.""" self._errors: dict[str, str] = {} - async def _test_connection(self, user_input=None): + async def _test_connection( + self, + user_input: Mapping[str, Any], + ): """Test connection to the server and try to get the certificate.""" try: await get_cert_expiry_timestamp( @@ -48,7 +54,10 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return True return False - async def async_step_user(self, user_input=None): + async def async_step_user( + self, + user_input: Mapping[str, Any] | None = None, + ) -> FlowResult: """Step when user initializes a integration.""" self._errors = {} if user_input is not None: @@ -85,7 +94,10 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=self._errors, ) - async def async_step_import(self, user_input=None): + async def async_step_import( + self, + user_input: Mapping[str, Any] | None = None, + ) -> FlowResult: """Import a config entry. Only host was required in the yaml file all other fields are optional diff --git a/homeassistant/components/cert_expiry/helper.py b/homeassistant/components/cert_expiry/helper.py index c00a99c8e86..fcd808a6521 100644 --- a/homeassistant/components/cert_expiry/helper.py +++ b/homeassistant/components/cert_expiry/helper.py @@ -2,6 +2,7 @@ import socket import ssl +from homeassistant.core import HomeAssistant from homeassistant.util import dt from .const import TIMEOUT @@ -13,7 +14,10 @@ from .errors import ( ) -def get_cert(host, port): +def get_cert( + host: str, + port: int, +): """Get the certificate for the host and port combination.""" ctx = ssl.create_default_context() address = (host, port) @@ -23,7 +27,11 @@ def get_cert(host, port): return cert -async def get_cert_expiry_timestamp(hass, hostname, port): +async def get_cert_expiry_timestamp( + hass: HomeAssistant, + hostname: str, + port: int, +): """Return the certificate's expiration timestamp.""" try: cert = await hass.async_add_executor_job(get_cert, hostname, port) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 9ee79e5c449..0c1b6116cdd 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -19,6 +19,7 @@ from homeassistant.helpers.event import async_call_later from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import CertExpiryDataUpdateCoordinator from .const import DEFAULT_PORT, DOMAIN SCAN_INTERVAL = timedelta(hours=12) @@ -57,7 +58,9 @@ async def async_setup_platform( async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Add cert-expiry entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] @@ -88,7 +91,10 @@ class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity): _attr_device_class = SensorDeviceClass.TIMESTAMP - def __init__(self, coordinator) -> None: + def __init__( + self, + coordinator: CertExpiryDataUpdateCoordinator, + ) -> None: """Initialize a Cert Expiry timestamp sensor.""" super().__init__(coordinator) self._attr_name = f"Cert Expiry Timestamp ({coordinator.name})" From 1e115341af6528a2f002140cd33aecb555e2ac36 Mon Sep 17 00:00:00 2001 From: Heine Furubotten Date: Sun, 31 Jul 2022 13:28:09 +0200 Subject: [PATCH 2988/3516] Bump enturclient to 0.2.4 (#75928) --- homeassistant/components/entur_public_transport/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/entur_public_transport/manifest.json b/homeassistant/components/entur_public_transport/manifest.json index c7f4fbeef53..3bbacb8c3d4 100644 --- a/homeassistant/components/entur_public_transport/manifest.json +++ b/homeassistant/components/entur_public_transport/manifest.json @@ -2,7 +2,7 @@ "domain": "entur_public_transport", "name": "Entur", "documentation": "https://www.home-assistant.io/integrations/entur_public_transport", - "requirements": ["enturclient==0.2.3"], + "requirements": ["enturclient==0.2.4"], "codeowners": ["@hfurubotten"], "iot_class": "cloud_polling", "loggers": ["enturclient"] diff --git a/requirements_all.txt b/requirements_all.txt index c602bb56697..fc08ba28427 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -606,7 +606,7 @@ emulated_roku==0.2.1 enocean==0.50 # homeassistant.components.entur_public_transport -enturclient==0.2.3 +enturclient==0.2.4 # homeassistant.components.environment_canada env_canada==0.5.22 From ee273daf8d4e833bd5e0d9096122fb1fd703d693 Mon Sep 17 00:00:00 2001 From: MasonCrawford Date: Sun, 31 Jul 2022 19:32:40 +0800 Subject: [PATCH 2989/3516] Small fixes for LG soundbar (#75938) --- homeassistant/components/lg_soundbar/media_player.py | 3 --- homeassistant/components/lg_soundbar/strings.json | 3 +-- homeassistant/components/lg_soundbar/translations/en.json | 3 +-- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index f8f6fcf26fd..941042d5bce 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -47,7 +47,6 @@ class LGDevice(MediaPlayerEntity): self._port = port self._attr_unique_id = unique_id - self._name = None self._volume = 0 self._volume_min = 0 self._volume_max = 0 @@ -94,8 +93,6 @@ class LGDevice(MediaPlayerEntity): elif response["msg"] == "SPK_LIST_VIEW_INFO": if "i_vol" in data: self._volume = data["i_vol"] - if "s_user_name" in data: - self._name = data["s_user_name"] if "i_vol_min" in data: self._volume_min = data["i_vol_min"] if "i_vol_max" in data: diff --git a/homeassistant/components/lg_soundbar/strings.json b/homeassistant/components/lg_soundbar/strings.json index ef7bf32a051..52d57eda809 100644 --- a/homeassistant/components/lg_soundbar/strings.json +++ b/homeassistant/components/lg_soundbar/strings.json @@ -11,8 +11,7 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { - "existing_instance_updated": "Updated existing configuration.", - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } } diff --git a/homeassistant/components/lg_soundbar/translations/en.json b/homeassistant/components/lg_soundbar/translations/en.json index a646279203f..10441d21536 100644 --- a/homeassistant/components/lg_soundbar/translations/en.json +++ b/homeassistant/components/lg_soundbar/translations/en.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is already configured", - "existing_instance_updated": "Updated existing configuration." + "already_configured": "Device is already configured" }, "error": { "cannot_connect": "Failed to connect" From 9e76e8cef824a7356f09a9a3348decd24440fe17 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 31 Jul 2022 04:37:29 -0700 Subject: [PATCH 2990/3516] Bump grpc requirements to 1.48.0 (#75603) --- homeassistant/package_constraints.txt | 4 ++-- script/gen_requirements_all.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c9d424daa33..d4344ec256c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -54,8 +54,8 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.46.1 -grpcio-status==1.46.1 +grpcio==1.48.0 +grpcio-status==1.48.0 # libcst >=0.4.0 requires a newer Rust than we currently have available, # thus our wheels builds fail. This pins it to the last working version, diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 5ef794b4eab..3cb35eec147 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -68,8 +68,8 @@ httplib2>=0.19.0 # gRPC is an implicit dependency that we want to make explicit so we manage # upgrades intentionally. It is a large package to build from source and we # want to ensure we have wheels built. -grpcio==1.46.1 -grpcio-status==1.46.1 +grpcio==1.48.0 +grpcio-status==1.48.0 # libcst >=0.4.0 requires a newer Rust than we currently have available, # thus our wheels builds fail. This pins it to the last working version, From 90458ee200d6d9e6fe7458fec3021d904e365c13 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 31 Jul 2022 13:46:25 +0200 Subject: [PATCH 2991/3516] Use attributes in zerproc light (#75951) --- homeassistant/components/zerproc/light.py | 76 ++++++++--------------- 1 file changed, 26 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/zerproc/light.py b/homeassistant/components/zerproc/light.py index 9bc2e4d29f3..5a32ca23332 100644 --- a/homeassistant/components/zerproc/light.py +++ b/homeassistant/components/zerproc/light.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any import pyzerproc @@ -14,7 +15,7 @@ from homeassistant.components.light import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant +from homeassistant.core import Event, HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval @@ -79,26 +80,24 @@ class ZerprocLight(LightEntity): """Representation of an Zerproc Light.""" _attr_color_mode = ColorMode.HS + _attr_icon = "mdi:string-lights" _attr_supported_color_modes = {ColorMode.HS} - def __init__(self, light): + def __init__(self, light) -> None: """Initialize a Zerproc light.""" self._light = light - self._name = None - self._is_on = None - self._hs_color = None - self._brightness = None - self._available = True async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" self.async_on_remove( - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self.async_will_remove_from_hass - ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._hass_stop) ) - async def async_will_remove_from_hass(self, *args) -> None: + async def _hass_stop(self, event: Event) -> None: + """Run on EVENT_HOMEASSISTANT_STOP.""" + await self.async_will_remove_from_hass() + + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" try: await self._light.disconnect() @@ -126,64 +125,41 @@ class ZerprocLight(LightEntity): name=self.name, ) - @property - def icon(self) -> str | None: - """Return the icon to use in the frontend.""" - return "mdi:string-lights" - - @property - def brightness(self): - """Return the brightness of the light.""" - return self._brightness - - @property - def hs_color(self): - """Return the hs color.""" - return self._hs_color - - @property - def is_on(self): - """Return true if light is on.""" - return self._is_on - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._available - - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" if ATTR_BRIGHTNESS in kwargs or ATTR_HS_COLOR in kwargs: - default_hs = (0, 0) if self._hs_color is None else self._hs_color + default_hs = (0, 0) if self.hs_color is None else self.hs_color hue_sat = kwargs.get(ATTR_HS_COLOR, default_hs) - default_brightness = 255 if self._brightness is None else self._brightness + default_brightness = 255 if self.brightness is None else self.brightness brightness = kwargs.get(ATTR_BRIGHTNESS, default_brightness) - rgb = color_util.color_hsv_to_RGB(*hue_sat, brightness / 255 * 100) + rgb = color_util.color_hsv_to_RGB( + hue_sat[0], hue_sat[1], brightness / 255 * 100 + ) await self._light.set_color(*rgb) else: await self._light.turn_on() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" await self._light.turn_off() - async def async_update(self): + async def async_update(self) -> None: """Fetch new state data for this light.""" try: - if not self._available: + if not self.available: await self._light.connect() state = await self._light.get_state() except pyzerproc.ZerprocException: - if self._available: + if self.available: _LOGGER.warning("Unable to connect to %s", self._light.address) - self._available = False + self._attr_available = False return - if self._available is False: + if not self.available: _LOGGER.info("Reconnected to %s", self._light.address) - self._available = True - self._is_on = state.is_on + self._attr_available = True + self._attr_is_on = state.is_on hsv = color_util.color_RGB_to_hsv(*state.color) - self._hs_color = hsv[:2] - self._brightness = int(round((hsv[2] / 100) * 255)) + self._attr_hs_color = hsv[:2] + self._attr_brightness = int(round((hsv[2] / 100) * 255)) From 11a19c2612f9704721c14ea4300c76257a75e468 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 31 Jul 2022 13:50:24 +0200 Subject: [PATCH 2992/3516] Improve type hints in light [s-z] (#75946) --- homeassistant/components/scsgate/light.py | 5 +-- homeassistant/components/sisyphus/light.py | 11 +++--- homeassistant/components/smartthings/light.py | 7 ++-- homeassistant/components/smarttub/light.py | 6 ++-- homeassistant/components/tellduslive/light.py | 5 +-- homeassistant/components/tikteck/light.py | 5 +-- homeassistant/components/twinkly/light.py | 4 +-- homeassistant/components/unifiled/light.py | 7 ++-- homeassistant/components/upb/light.py | 8 +++-- homeassistant/components/velux/light.py | 6 ++-- homeassistant/components/vera/light.py | 2 +- homeassistant/components/vesync/light.py | 3 +- homeassistant/components/x10/light.py | 7 ++-- .../components/xiaomi_aqara/light.py | 3 +- homeassistant/components/xiaomi_miio/light.py | 35 ++++++++++--------- homeassistant/components/yeelight/light.py | 9 ++--- .../components/yeelightsunflower/light.py | 7 ++-- homeassistant/components/zengge/light.py | 7 ++-- homeassistant/components/zwave_me/light.py | 2 +- 19 files changed, 79 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/scsgate/light.py b/homeassistant/components/scsgate/light.py index 3957aa4cd95..df63bad49d4 100644 --- a/homeassistant/components/scsgate/light.py +++ b/homeassistant/components/scsgate/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from scsgate.tasks import ToggleStatusTask import voluptuous as vol @@ -76,7 +77,7 @@ class SCSGateLight(LightEntity): """Return true if light is on.""" return self._toggled - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._scsgate.append_task(ToggleStatusTask(target=self._scs_id, toggled=True)) @@ -84,7 +85,7 @@ class SCSGateLight(LightEntity): self._toggled = True self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._scsgate.append_task(ToggleStatusTask(target=self._scs_id, toggled=False)) diff --git a/homeassistant/components/sisyphus/light.py b/homeassistant/components/sisyphus/light.py index 7601ccf7657..d0cd7597e58 100644 --- a/homeassistant/components/sisyphus/light.py +++ b/homeassistant/components/sisyphus/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import aiohttp @@ -47,16 +48,16 @@ class SisyphusLight(LightEntity): self._name = name self._table = table - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Add listeners after this object has been initialized.""" self._table.add_listener(self.async_write_ha_state) - async def async_update(self): + async def async_update(self) -> None: """Force update the table state.""" await self._table.refresh() @property - def available(self): + def available(self) -> bool: """Return true if the table is responding to heartbeats.""" return self._table.is_connected @@ -80,12 +81,12 @@ class SisyphusLight(LightEntity): """Return the current brightness of the table's ring light.""" return self._table.brightness * 255 - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Put the table to sleep.""" await self._table.sleep() _LOGGER.debug("Sisyphus table %s: sleep") - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Wake up the table if necessary, optionally changes brightness.""" if not self.is_on: await self._table.wakeup() diff --git a/homeassistant/components/smartthings/light.py b/homeassistant/components/smartthings/light.py index 1b1738a94d4..918e8b4258c 100644 --- a/homeassistant/components/smartthings/light.py +++ b/homeassistant/components/smartthings/light.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio from collections.abc import Sequence +from typing import Any from pysmartthings import Capability @@ -96,7 +97,7 @@ class SmartThingsLight(SmartThingsEntity, LightEntity): return features - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" tasks = [] # Color temperature @@ -121,7 +122,7 @@ class SmartThingsLight(SmartThingsEntity, LightEntity): # the entity state ahead of receiving the confirming push updates self.async_schedule_update_ha_state(True) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" # Switch/transition if ( @@ -136,7 +137,7 @@ class SmartThingsLight(SmartThingsEntity, LightEntity): # the entity state ahead of receiving the confirming push updates self.async_schedule_update_ha_state(True) - async def async_update(self): + async def async_update(self) -> None: """Update entity attributes when the device status has changed.""" # Brightness and transition if self._supported_features & SUPPORT_BRIGHTNESS: diff --git a/homeassistant/components/smarttub/light.py b/homeassistant/components/smarttub/light.py index b8750c50611..f7e229449e0 100644 --- a/homeassistant/components/smarttub/light.py +++ b/homeassistant/components/smarttub/light.py @@ -1,4 +1,6 @@ """Platform for light integration.""" +from typing import Any + from smarttub import SpaLight from homeassistant.components.light import ( @@ -125,7 +127,7 @@ class SmartTubLight(SmartTubEntity, LightEntity): return SpaLight.LightMode[effect.upper()] - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" mode = self._effect_to_light_mode(kwargs.get(ATTR_EFFECT, DEFAULT_LIGHT_EFFECT)) @@ -136,7 +138,7 @@ class SmartTubLight(SmartTubEntity, LightEntity): await self.light.set_mode(mode, intensity) await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self.light.set_mode(SpaLight.LightMode.OFF, 0) await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/tellduslive/light.py b/homeassistant/components/tellduslive/light.py index 7609cc6adee..205fff840d6 100644 --- a/homeassistant/components/tellduslive/light.py +++ b/homeassistant/components/tellduslive/light.py @@ -1,5 +1,6 @@ """Support for Tellstick lights using Tellstick Net.""" import logging +from typing import Any from homeassistant.components import light, tellduslive from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity @@ -58,7 +59,7 @@ class TelldusLiveLight(TelldusLiveEntity, LightEntity): """Return true if light is on.""" return self.device.is_on - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS, self._last_brightness) if brightness == 0: @@ -70,7 +71,7 @@ class TelldusLiveLight(TelldusLiveEntity, LightEntity): self.device.dim(level=brightness) self.changed() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" self.device.turn_off() self.changed() diff --git a/homeassistant/components/tikteck/light.py b/homeassistant/components/tikteck/light.py index 0f810c434fb..28daf4baac1 100644 --- a/homeassistant/components/tikteck/light.py +++ b/homeassistant/components/tikteck/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import tikteck import voluptuous as vol @@ -111,7 +112,7 @@ class TikteckLight(LightEntity): """Set the bulb state.""" return self._bulb.set_state(red, green, blue, brightness) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the specified light on.""" self._state = True @@ -128,7 +129,7 @@ class TikteckLight(LightEntity): self.set_state(rgb[0], rgb[1], rgb[2], self.brightness) self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the specified light off.""" self._state = False self.set_state(0, 0, 0, 0) diff --git a/homeassistant/components/twinkly/light.py b/homeassistant/components/twinkly/light.py index 9068c04e01b..ba6ac7cf492 100644 --- a/homeassistant/components/twinkly/light.py +++ b/homeassistant/components/twinkly/light.py @@ -140,7 +140,7 @@ class TwinklyLight(LightEntity): return attributes - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn device on.""" if ATTR_BRIGHTNESS in kwargs: brightness = int(int(kwargs[ATTR_BRIGHTNESS]) / 2.55) @@ -183,7 +183,7 @@ class TwinklyLight(LightEntity): if not self._is_on: await self._client.turn_on() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn device off.""" await self._client.turn_off() diff --git a/homeassistant/components/unifiled/light.py b/homeassistant/components/unifiled/light.py index dd5a5b5290a..8ba9fc2b6f9 100644 --- a/homeassistant/components/unifiled/light.py +++ b/homeassistant/components/unifiled/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from unifiled import unifiled import voluptuous as vol @@ -98,7 +99,7 @@ class UnifiLedLight(LightEntity): """Return true if light is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" self._api.setdevicebrightness( self._unique_id, @@ -106,11 +107,11 @@ class UnifiLedLight(LightEntity): ) self._api.setdeviceoutput(self._unique_id, 1) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" self._api.setdeviceoutput(self._unique_id, 0) - def update(self): + def update(self) -> None: """Update the light states.""" self._state = self._api.getlightstate(self._unique_id) self._brightness = self._api.convertfrom100to255( diff --git a/homeassistant/components/upb/light.py b/homeassistant/components/upb/light.py index 98a775c18ab..85cda1f0061 100644 --- a/homeassistant/components/upb/light.py +++ b/homeassistant/components/upb/light.py @@ -1,4 +1,6 @@ """Platform for UPB light integration.""" +from typing import Any + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_FLASH, @@ -85,7 +87,7 @@ class UpbLight(UpbAttachedEntity, LightEntity): """Get the current brightness.""" return self._brightness != 0 - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" if flash := kwargs.get(ATTR_FLASH): await self.async_light_blink(0.5 if flash == "short" else 1.5) @@ -94,7 +96,7 @@ class UpbLight(UpbAttachedEntity, LightEntity): brightness = round(kwargs.get(ATTR_BRIGHTNESS, 255) / 2.55) self._element.turn_on(brightness, rate) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the device.""" rate = kwargs.get(ATTR_TRANSITION, -1) self._element.turn_off(rate) @@ -114,7 +116,7 @@ class UpbLight(UpbAttachedEntity, LightEntity): blink_rate = int(blink_rate * 60) # Convert seconds to 60 hz pulses self._element.blink(blink_rate) - async def async_update(self): + async def async_update(self) -> None: """Request the device to update its status.""" self._element.update_status() diff --git a/homeassistant/components/velux/light.py b/homeassistant/components/velux/light.py index f8a52fc05c1..a600aceedd2 100644 --- a/homeassistant/components/velux/light.py +++ b/homeassistant/components/velux/light.py @@ -1,6 +1,8 @@ """Support for Velux lights.""" from __future__ import annotations +from typing import Any + from pyvlx import Intensity, LighteningDevice from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity @@ -43,7 +45,7 @@ class VeluxLight(VeluxEntity, LightEntity): """Return true if light is on.""" return not self.node.intensity.off and self.node.intensity.known - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" if ATTR_BRIGHTNESS in kwargs: intensity_percent = int(100 - kwargs[ATTR_BRIGHTNESS] / 255 * 100) @@ -54,6 +56,6 @@ class VeluxLight(VeluxEntity, LightEntity): else: await self.node.turn_on(wait_for_completion=True) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" await self.node.turn_off(wait_for_completion=True) diff --git a/homeassistant/components/vera/light.py b/homeassistant/components/vera/light.py index e91492a934f..fa017be475e 100644 --- a/homeassistant/components/vera/light.py +++ b/homeassistant/components/vera/light.py @@ -88,7 +88,7 @@ class VeraLight(VeraDevice[veraApi.VeraDimmer], LightEntity): self._state = True self.schedule_update_ha_state(True) - def turn_off(self, **kwargs: Any): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" self.vera_device.switch_off() self._state = False diff --git a/homeassistant/components/vesync/light.py b/homeassistant/components/vesync/light.py index d01319ff0e7..8727a770112 100644 --- a/homeassistant/components/vesync/light.py +++ b/homeassistant/components/vesync/light.py @@ -1,5 +1,6 @@ """Support for VeSync bulbs and wall dimmers.""" import logging +from typing import Any from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -83,7 +84,7 @@ class VeSyncBaseLight(VeSyncDevice, LightEntity): # convert percent brightness to ha expected range return round((max(1, brightness_value) / 100) * 255) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" attribute_adjustment_only = False # set white temperature diff --git a/homeassistant/components/x10/light.py b/homeassistant/components/x10/light.py index 42a15a643bd..b7e331e2199 100644 --- a/homeassistant/components/x10/light.py +++ b/homeassistant/components/x10/light.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging from subprocess import STDOUT, CalledProcessError, check_output +from typing import Any import voluptuous as vol @@ -87,7 +88,7 @@ class X10Light(LightEntity): """Return true if light is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" if self._is_cm11a: x10_command(f"on {self._id}") @@ -96,7 +97,7 @@ class X10Light(LightEntity): self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255) self._state = True - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" if self._is_cm11a: x10_command(f"off {self._id}") @@ -104,7 +105,7 @@ class X10Light(LightEntity): x10_command(f"foff {self._id}") self._state = False - def update(self): + def update(self) -> None: """Fetch update state.""" if self._is_cm11a: self._state = bool(get_unit_status(self._id)) diff --git a/homeassistant/components/xiaomi_aqara/light.py b/homeassistant/components/xiaomi_aqara/light.py index 812cd1c96b8..173ffe564fc 100644 --- a/homeassistant/components/xiaomi_aqara/light.py +++ b/homeassistant/components/xiaomi_aqara/light.py @@ -2,6 +2,7 @@ import binascii import logging import struct +from typing import Any from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -113,7 +114,7 @@ class XiaomiGatewayLight(XiaomiDevice, LightEntity): self._state = True self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" if self._write_to_hub(self._sid, **{self._data_key: 0}): self._state = False diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index fd6af0d9560..2a7fce01442 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -7,6 +7,7 @@ from datetime import timedelta from functools import partial import logging from math import ceil +from typing import Any from miio import ( Ceil, @@ -292,7 +293,7 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): return False - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] @@ -311,11 +312,11 @@ class XiaomiPhilipsAbstractLight(XiaomiMiioEntity, LightEntity): else: await self._try_command("Turning the light on failed.", self._device.on) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._try_command("Turning the light off failed.", self._device.off) - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" try: state = await self.hass.async_add_executor_job(self._device.status) @@ -341,7 +342,7 @@ class XiaomiPhilipsGenericLight(XiaomiPhilipsAbstractLight): self._state_attrs.update({ATTR_SCENE: None, ATTR_DELAYED_TURN_OFF: None}) - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" try: state = await self.hass.async_add_executor_job(self._device.status) @@ -430,7 +431,7 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): """Return the warmest color_temp that this light supports.""" return 333 - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_COLOR_TEMP in kwargs: color_temp = kwargs[ATTR_COLOR_TEMP] @@ -497,7 +498,7 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): else: await self._try_command("Turning the light on failed.", self._device.on) - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" try: state = await self.hass.async_add_executor_job(self._device.status) @@ -556,7 +557,7 @@ class XiaomiPhilipsCeilingLamp(XiaomiPhilipsBulb): """Return the warmest color_temp that this light supports.""" return 370 - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" try: state = await self.hass.async_add_executor_job(self._device.status) @@ -602,7 +603,7 @@ class XiaomiPhilipsEyecareLamp(XiaomiPhilipsGenericLight): {ATTR_REMINDER: None, ATTR_NIGHT_LIGHT_MODE: None, ATTR_EYECARE_MODE: None} ) - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" try: state = await self.hass.async_add_executor_job(self._device.status) @@ -714,7 +715,7 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): unique_id = f"{unique_id}-ambient" super().__init__(name, device, entry, unique_id) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] @@ -739,13 +740,13 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): "Turning the ambient light on failed.", self._device.ambient_on ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._try_command( "Turning the ambient light off failed.", self._device.ambient_off ) - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" try: state = await self.hass.async_add_executor_job(self._device.status) @@ -805,7 +806,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): return ColorMode.HS return ColorMode.COLOR_TEMP - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_COLOR_TEMP in kwargs: color_temp = kwargs[ATTR_COLOR_TEMP] @@ -905,7 +906,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): else: await self._try_command("Turning the light on failed.", self._device.on) - async def async_update(self): + async def async_update(self) -> None: """Fetch state from the device.""" try: state = await self.hass.async_add_executor_job(self._device.status) @@ -996,7 +997,7 @@ class XiaomiGatewayLight(LightEntity): """Return the hs color value.""" return self._hs - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_HS_COLOR in kwargs: rgb = color.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) @@ -1012,7 +1013,7 @@ class XiaomiGatewayLight(LightEntity): self.schedule_update_ha_state() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" self._gateway.light.set_rgb(0, self._rgb) self.schedule_update_ha_state() @@ -1071,7 +1072,7 @@ class XiaomiGatewayBulb(XiaomiGatewayDevice, LightEntity): """Return max cct.""" return self._sub_device.status["cct_max"] - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" await self.hass.async_add_executor_job(self._sub_device.on) @@ -1087,6 +1088,6 @@ class XiaomiGatewayBulb(XiaomiGatewayDevice, LightEntity): self._sub_device.set_brightness, brightness ) - async def async_turn_off(self, **kwargsf): + async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" await self.hass.async_add_executor_job(self._sub_device.off) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index a368490e51e..9bb0ed21989 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import logging import math +from typing import Any import voluptuous as vol import yeelight @@ -442,7 +443,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): self._async_cancel_pending_state_check() self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" self.async_on_remove( async_dispatcher_connect( @@ -585,7 +586,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): """Return yeelight device.""" return self._device - async def async_update(self): + async def async_update(self) -> None: """Update light properties.""" await self.device.async_update(True) @@ -774,7 +775,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): power_mode=self._turn_on_power_mode, ) - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the bulb on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) colortemp = kwargs.get(ATTR_COLOR_TEMP) @@ -836,7 +837,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): """Turn off with a given transition duration wrapped with _async_cmd.""" await self._bulb.async_turn_off(duration=duration, light_type=self.light_type) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off.""" if not self.is_on: return diff --git a/homeassistant/components/yeelightsunflower/light.py b/homeassistant/components/yeelightsunflower/light.py index 1b76aef1328..8b51dad40f7 100644 --- a/homeassistant/components/yeelightsunflower/light.py +++ b/homeassistant/components/yeelightsunflower/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol import yeelightsunflower @@ -87,7 +88,7 @@ class SunflowerBulb(LightEntity): """Return the color property.""" return color_util.color_RGB_to_hs(*self._rgb_color) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on, optionally set colour/brightness.""" # when no arguments, just turn light on (full brightness) if not kwargs: @@ -104,11 +105,11 @@ class SunflowerBulb(LightEntity): bright = int(kwargs[ATTR_BRIGHTNESS] / 255 * 100) self._light.set_brightness(bright) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" self._light.turn_off() - def update(self): + def update(self) -> None: """Fetch new state data for this light and update local values.""" self._light.update() self._available = self._light.available diff --git a/homeassistant/components/zengge/light.py b/homeassistant/components/zengge/light.py index 057b049eefd..6939ecb276b 100644 --- a/homeassistant/components/zengge/light.py +++ b/homeassistant/components/zengge/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol from zengge import zengge @@ -123,7 +124,7 @@ class ZenggeLight(LightEntity): """Set the white state.""" return self._bulb.set_white(white) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the specified light on.""" self._state = True self._bulb.on() @@ -153,12 +154,12 @@ class ZenggeLight(LightEntity): ) self._set_rgb(*rgb) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the specified light off.""" self._state = False self._bulb.off() - def update(self): + def update(self) -> None: """Synchronise internal state with the actual light state.""" rgb = self._bulb.get_colour() hsv = color_util.color_RGB_to_hsv(*rgb) diff --git a/homeassistant/components/zwave_me/light.py b/homeassistant/components/zwave_me/light.py index fd4488b8dbf..5da0f955059 100644 --- a/homeassistant/components/zwave_me/light.py +++ b/homeassistant/components/zwave_me/light.py @@ -52,7 +52,7 @@ class ZWaveMeRGB(ZWaveMeEntity, LightEntity): """Turn the device on.""" self.controller.zwave_api.send_command(self.device.id, "off") - def turn_on(self, **kwargs: Any): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" color = kwargs.get(ATTR_RGB_COLOR) From 7b1463e03db39b90ac222b0f3143514aa5d7b7f9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 31 Jul 2022 13:53:22 +0200 Subject: [PATCH 2993/3516] Improve type hints in light [i-r] (#75943) --- homeassistant/components/insteon/light.py | 8 +++++--- homeassistant/components/kulersky/light.py | 5 +++-- homeassistant/components/lightwave/light.py | 6 ++++-- homeassistant/components/limitlessled/light.py | 2 +- homeassistant/components/litejet/light.py | 11 ++++++----- homeassistant/components/lutron/light.py | 8 +++++--- homeassistant/components/lutron_caseta/light.py | 5 +++-- homeassistant/components/lw12wifi/light.py | 3 ++- homeassistant/components/mochad/light.py | 5 +++-- homeassistant/components/myq/light.py | 6 ++++-- homeassistant/components/mystrom/light.py | 7 ++++--- homeassistant/components/niko_home_control/light.py | 7 ++++--- homeassistant/components/opple/light.py | 7 ++++--- homeassistant/components/osramlightify/light.py | 7 ++++--- homeassistant/components/philips_js/light.py | 5 +++-- homeassistant/components/pilight/light.py | 4 +++- homeassistant/components/plum_lightpad/light.py | 13 +++++++------ homeassistant/components/rflink/light.py | 11 ++++++----- homeassistant/components/rfxtrx/light.py | 7 ++++--- homeassistant/components/ring/light.py | 5 +++-- 20 files changed, 78 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py index bf8b693b103..062a3f54d1b 100644 --- a/homeassistant/components/insteon/light.py +++ b/homeassistant/components/insteon/light.py @@ -1,4 +1,6 @@ """Support for Insteon lights via PowerLinc Modem.""" +from typing import Any + from pyinsteon.config import ON_LEVEL from homeassistant.components.light import ( @@ -50,11 +52,11 @@ class InsteonDimmerEntity(InsteonEntity, LightEntity): return self._insteon_device_group.value @property - def is_on(self): + def is_on(self) -> bool: """Return the boolean response if the node is on.""" return bool(self.brightness) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn light on.""" if ATTR_BRIGHTNESS in kwargs: brightness = int(kwargs[ATTR_BRIGHTNESS]) @@ -67,6 +69,6 @@ class InsteonDimmerEntity(InsteonEntity, LightEntity): else: await self._insteon_device.async_on(group=self._insteon_device_group.group) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn light off.""" await self._insteon_device.async_off(self._insteon_device_group.group) diff --git a/homeassistant/components/kulersky/light.py b/homeassistant/components/kulersky/light.py index 0bef2245a7a..c1cc68c9035 100644 --- a/homeassistant/components/kulersky/light.py +++ b/homeassistant/components/kulersky/light.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any import pykulersky @@ -116,7 +117,7 @@ class KulerskyLight(LightEntity): """Return True if entity is available.""" return self._available - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" default_rgbw = (255,) * 4 if self.rgbw_color is None else self.rgbw_color rgbw = kwargs.get(ATTR_RGBW_COLOR, default_rgbw) @@ -134,7 +135,7 @@ class KulerskyLight(LightEntity): await self._light.set_color(*rgbw_scaled) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" await self._light.set_color(0, 0, 0, 0) diff --git a/homeassistant/components/lightwave/light.py b/homeassistant/components/lightwave/light.py index 8d46592f439..f89dbb6bf5f 100644 --- a/homeassistant/components/lightwave/light.py +++ b/homeassistant/components/lightwave/light.py @@ -1,6 +1,8 @@ """Support for LightwaveRF lights.""" from __future__ import annotations +from typing import Any + from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant @@ -46,7 +48,7 @@ class LWRFLight(LightEntity): self._attr_brightness = MAX_BRIGHTNESS self._lwlink = lwlink - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the LightWave light on.""" self._attr_is_on = True @@ -62,7 +64,7 @@ class LWRFLight(LightEntity): self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the LightWave light off.""" self._attr_is_on = False self._lwlink.turn_off(self._device_id, self._attr_name) diff --git a/homeassistant/components/limitlessled/light.py b/homeassistant/components/limitlessled/light.py index 41668470dfb..5073e3b4a7d 100644 --- a/homeassistant/components/limitlessled/light.py +++ b/homeassistant/components/limitlessled/light.py @@ -240,7 +240,7 @@ class LimitlessLEDGroup(LightEntity, RestoreEntity): self._color = None self._effect = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity about to be added to hass event.""" await super().async_added_to_hass() if last_state := await self.async_get_last_state(): diff --git a/homeassistant/components/litejet/light.py b/homeassistant/components/litejet/light.py index aae28536849..74395117c46 100644 --- a/homeassistant/components/litejet/light.py +++ b/homeassistant/components/litejet/light.py @@ -1,5 +1,6 @@ """Support for LiteJet lights.""" import logging +from typing import Any from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -53,12 +54,12 @@ class LiteJetLight(LightEntity): self._brightness = 0 self._name = name - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when this Entity has been added to HA.""" self._lj.on_load_activated(self._index, self._on_load_changed) self._lj.on_load_deactivated(self._index, self._on_load_changed) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Entity being removed from hass.""" self._lj.unsubscribe(self._on_load_changed) @@ -97,7 +98,7 @@ class LiteJetLight(LightEntity): """Return the device state attributes.""" return {ATTR_NUMBER: self._index} - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" # If neither attribute is specified then the simple activate load @@ -115,7 +116,7 @@ class LiteJetLight(LightEntity): self._lj.activate_load_at(self._index, brightness, int(transition)) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" if ATTR_TRANSITION in kwargs: self._lj.activate_load_at(self._index, 0, kwargs[ATTR_TRANSITION]) @@ -126,6 +127,6 @@ class LiteJetLight(LightEntity): # transition value programmed in the LiteJet system. self._lj.deactivate_load(self._index) - def update(self): + def update(self) -> None: """Retrieve the light's brightness from the LiteJet system.""" self._brightness = int(self._lj.get_load_level(self._index) / 99 * 255) diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py index 52ff2d7843c..15024122338 100644 --- a/homeassistant/components/lutron/light.py +++ b/homeassistant/components/lutron/light.py @@ -1,6 +1,8 @@ """Support for Lutron lights.""" from __future__ import annotations +from typing import Any + from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -53,7 +55,7 @@ class LutronLight(LutronDevice, LightEntity): self._prev_brightness = new_brightness return new_brightness - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs and self._lutron_device.is_dimmable: brightness = kwargs[ATTR_BRIGHTNESS] @@ -64,7 +66,7 @@ class LutronLight(LutronDevice, LightEntity): self._prev_brightness = brightness self._lutron_device.level = to_lutron_level(brightness) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" self._lutron_device.level = 0 @@ -78,7 +80,7 @@ class LutronLight(LutronDevice, LightEntity): """Return true if device is on.""" return self._lutron_device.last_level() > 0 - def update(self): + def update(self) -> None: """Call when forcing a refresh of the device.""" if self._prev_brightness is None: self._prev_brightness = to_hass_level(self._lutron_device.level) diff --git a/homeassistant/components/lutron_caseta/light.py b/homeassistant/components/lutron_caseta/light.py index 9fbb80284f5..cfad8115a20 100644 --- a/homeassistant/components/lutron_caseta/light.py +++ b/homeassistant/components/lutron_caseta/light.py @@ -1,5 +1,6 @@ """Support for Lutron Caseta lights.""" from datetime import timedelta +from typing import Any from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -69,13 +70,13 @@ class LutronCasetaLight(LutronCasetaDeviceUpdatableEntity, LightEntity): self.device_id, to_lutron_level(brightness), **args ) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" brightness = kwargs.pop(ATTR_BRIGHTNESS, 255) await self._set_brightness(brightness, **kwargs) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._set_brightness(0, **kwargs) diff --git a/homeassistant/components/lw12wifi/light.py b/homeassistant/components/lw12wifi/light.py index deb721da29e..de23436a560 100644 --- a/homeassistant/components/lw12wifi/light.py +++ b/homeassistant/components/lw12wifi/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import lw12 import voluptuous as vol @@ -146,7 +147,7 @@ class LW12WiFi(LightEntity): self._light.set_light_option(lw12.LW12_LIGHT.FLASH, transition_speed) self._state = True - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" self._light.light_off() self._state = False diff --git a/homeassistant/components/mochad/light.py b/homeassistant/components/mochad/light.py index 8cc5f4da0a8..3d06a09f479 100644 --- a/homeassistant/components/mochad/light.py +++ b/homeassistant/components/mochad/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pymochad import device from pymochad.exceptions import MochadException @@ -112,7 +113,7 @@ class MochadLight(LightEntity): self.light.send_cmd(f"bright {mochad_brightness}") self._controller.read_data() - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Send the command to turn the light on.""" _LOGGER.debug("Reconnect %s:%s", self._controller.server, self._controller.port) brightness = kwargs.get(ATTR_BRIGHTNESS, 255) @@ -137,7 +138,7 @@ class MochadLight(LightEntity): except (MochadException, OSError) as exc: _LOGGER.error("Error with mochad communication: %s", exc) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Send the command to turn the light on.""" _LOGGER.debug("Reconnect %s:%s", self._controller.server, self._controller.port) with REQ_LOCK: diff --git a/homeassistant/components/myq/light.py b/homeassistant/components/myq/light.py index 1c001eac7fe..684af64a82e 100644 --- a/homeassistant/components/myq/light.py +++ b/homeassistant/components/myq/light.py @@ -1,4 +1,6 @@ """Support for MyQ-Enabled lights.""" +from typing import Any + from pymyq.errors import MyQError from homeassistant.components.light import ColorMode, LightEntity @@ -43,7 +45,7 @@ class MyQLight(MyQEntity, LightEntity): """Return true if the light is off, else False.""" return MYQ_TO_HASS.get(self._device.state) == STATE_OFF - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Issue on command to light.""" if self.is_on: return @@ -58,7 +60,7 @@ class MyQLight(MyQEntity, LightEntity): # Write new state to HASS self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Issue off command to light.""" if self.is_off: return diff --git a/homeassistant/components/mystrom/light.py b/homeassistant/components/mystrom/light.py index f6f214301df..26ce5b11567 100644 --- a/homeassistant/components/mystrom/light.py +++ b/homeassistant/components/mystrom/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pymystrom.bulb import MyStromBulb from pymystrom.exceptions import MyStromConnectionError @@ -120,7 +121,7 @@ class MyStromLight(LightEntity): """Return true if light is on.""" return self._state - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" brightness = kwargs.get(ATTR_BRIGHTNESS, 255) effect = kwargs.get(ATTR_EFFECT) @@ -147,14 +148,14 @@ class MyStromLight(LightEntity): except MyStromConnectionError: _LOGGER.warning("No route to myStrom bulb") - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the bulb.""" try: await self._bulb.set_off() except MyStromConnectionError: _LOGGER.warning("The myStrom bulb not online") - async def async_update(self): + async def async_update(self) -> None: """Fetch new state data for this light.""" try: await self._bulb.get_state() diff --git a/homeassistant/components/niko_home_control/light.py b/homeassistant/components/niko_home_control/light.py index f4d62fd6dc2..4d12591a472 100644 --- a/homeassistant/components/niko_home_control/light.py +++ b/homeassistant/components/niko_home_control/light.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any import nikohomecontrol import voluptuous as vol @@ -77,17 +78,17 @@ class NikoHomeControlLight(LightEntity): """Return true if light is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" _LOGGER.debug("Turn on: %s", self.name) self._light.turn_on() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" _LOGGER.debug("Turn off: %s", self.name) self._light.turn_off() - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from NikoHomeControl API.""" await self._data.async_update() self._state = self._data.get_state(self._light.id) diff --git a/homeassistant/components/opple/light.py b/homeassistant/components/opple/light.py index a9e24029a13..25bcf821fc9 100644 --- a/homeassistant/components/opple/light.py +++ b/homeassistant/components/opple/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pyoppleio.OppleLightDevice import OppleLightDevice import voluptuous as vol @@ -107,7 +108,7 @@ class OppleLight(LightEntity): """Return maximum supported color temperature.""" return 333 - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" _LOGGER.debug("Turn on light %s %s", self._device.ip, kwargs) if not self.is_on: @@ -120,12 +121,12 @@ class OppleLight(LightEntity): color_temp = mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) self._device.color_temperature = color_temp - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" self._device.power_on = False _LOGGER.debug("Turn off light %s", self._device.ip) - def update(self): + def update(self) -> None: """Synchronize state with light.""" prev_available = self.available self._device.update() diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py index a247497550f..6c5c7179006 100644 --- a/homeassistant/components/osramlightify/light.py +++ b/homeassistant/components/osramlightify/light.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging import random +from typing import Any from lightify import Lightify import voluptuous as vol @@ -306,7 +307,7 @@ class Luminary(LightEntity): return False - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" transition = int(kwargs.get(ATTR_TRANSITION, 0) * 10) if ATTR_EFFECT in kwargs: @@ -331,7 +332,7 @@ class Luminary(LightEntity): else: self._luminary.set_onoff(True) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._is_on = False if ATTR_TRANSITION in kwargs: @@ -374,7 +375,7 @@ class Luminary(LightEntity): if self._supported_features & SUPPORT_COLOR: self._rgb_color = self._luminary.rgb() - def update(self): + def update(self) -> None: """Synchronize state with bridge.""" changed = self.update_func() if changed > self._changed: diff --git a/homeassistant/components/philips_js/light.py b/homeassistant/components/philips_js/light.py index 4fa3b066214..c218c30347e 100644 --- a/homeassistant/components/philips_js/light.py +++ b/homeassistant/components/philips_js/light.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Any from haphilipsjs import PhilipsTV from haphilipsjs.typing import AmbilightCurrentConfiguration @@ -337,7 +338,7 @@ class PhilipsTVLightEntity( if await self._tv.setAmbilightCurrentConfiguration(config) is False: raise Exception("Failed to set ambilight mode") - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the bulb on.""" brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness) hs_color = kwargs.get(ATTR_HS_COLOR, self.hs_color) @@ -378,7 +379,7 @@ class PhilipsTVLightEntity( self._update_from_coordinator() self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn of ambilight.""" if not self._tv.on: diff --git a/homeassistant/components/pilight/light.py b/homeassistant/components/pilight/light.py index c96644f7ea7..a225489437a 100644 --- a/homeassistant/components/pilight/light.py +++ b/homeassistant/components/pilight/light.py @@ -1,6 +1,8 @@ """Support for switching devices via Pilight to on and off.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant.components.light import ( @@ -63,7 +65,7 @@ class PilightLight(PilightBaseDevice, LightEntity): """Return the brightness.""" return self._brightness - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on by calling pilight.send service with on code.""" # Update brightness only if provided as an argument. # This will allow the switch to keep its previous brightness level. diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py index 42fdcb538d8..4b7f34f942f 100644 --- a/homeassistant/components/plum_lightpad/light.py +++ b/homeassistant/components/plum_lightpad/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from typing import Any from plumlightpad import Plum @@ -69,7 +70,7 @@ class PlumLight(LightEntity): self._load = load self._brightness = load.level - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to dimmerchange events.""" self._load.add_event_listener("dimmerchange", self.dimmerchange) @@ -125,14 +126,14 @@ class PlumLight(LightEntity): """Flag supported color modes.""" return {self.color_mode} - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: await self._load.turn_on(kwargs[ATTR_BRIGHTNESS]) else: await self._load.turn_on() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._load.turn_off() @@ -155,7 +156,7 @@ class GlowRing(LightEntity): self._green = lightpad.glow_color["green"] self._blue = lightpad.glow_color["blue"] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to configchange events.""" self._lightpad.add_event_listener("configchange", self.configchange_event) @@ -222,7 +223,7 @@ class GlowRing(LightEntity): """Return the crop-portrait icon representing the glow ring.""" return "mdi:crop-portrait" - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: brightness_pct = kwargs[ATTR_BRIGHTNESS] / 255.0 @@ -234,7 +235,7 @@ class GlowRing(LightEntity): else: await self._lightpad.set_config({"glowEnabled": True}) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" if ATTR_BRIGHTNESS in kwargs: brightness_pct = kwargs[ATTR_BRIGHTNESS] / 255.0 diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py index e1ebf24c63b..94576155502 100644 --- a/homeassistant/components/rflink/light.py +++ b/homeassistant/components/rflink/light.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging import re +from typing import Any import voluptuous as vol @@ -184,7 +185,7 @@ class DimmableRflinkLight(SwitchableRflinkDevice, LightEntity): _attr_supported_color_modes = {ColorMode.BRIGHTNESS} _brightness = 255 - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore RFLink light brightness attribute.""" await super().async_added_to_hass() @@ -196,7 +197,7 @@ class DimmableRflinkLight(SwitchableRflinkDevice, LightEntity): # restore also brightness in dimmables devices self._brightness = int(old_state.attributes[ATTR_BRIGHTNESS]) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if ATTR_BRIGHTNESS in kwargs: # rflink only support 16 brightness levels @@ -242,7 +243,7 @@ class HybridRflinkLight(DimmableRflinkLight): Which results in a nice house disco :) """ - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on and set dim level.""" await super().async_turn_on(**kwargs) # if the receiving device does not support dimlevel this @@ -269,10 +270,10 @@ class ToggleRflinkLight(RflinkLight): # if the state is true, it gets set as false self._state = self._state in [None, False] - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self._async_handle_command("toggle") - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self._async_handle_command("toggle") diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index 198d0ffd3f1..7f32ee0bc83 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import RFXtrx as rfxtrxmod @@ -59,7 +60,7 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): _attr_brightness: int = 0 _device: rfxtrxmod.LightingDevice - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore RFXtrx device state (ON/OFF).""" await super().async_added_to_hass() @@ -70,7 +71,7 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): if brightness := old_state.attributes.get(ATTR_BRIGHTNESS): self._attr_brightness = int(brightness) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) self._attr_is_on = True @@ -83,7 +84,7 @@ class RfxtrxLight(RfxtrxCommandEntity, LightEntity): self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self._async_send(self._device.send_off) self._attr_is_on = False diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index 5b2cd1c54db..e6b29b94fbf 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -1,6 +1,7 @@ """This component provides HA switch support for Ring Door Bell/Chimes.""" from datetime import timedelta import logging +from typing import Any import requests @@ -93,10 +94,10 @@ class RingLight(RingEntityMixin, LightEntity): self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY self.async_write_ha_state() - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on for 30 seconds.""" self._set_light(ON_STATE) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" self._set_light(OFF_STATE) From c9ddb10024179dfcbcb6a2867bb95aa282c1832a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 31 Jul 2022 14:01:18 +0200 Subject: [PATCH 2994/3516] Use device_tracker SourceType enum [s-z] (#75966) --- homeassistant/components/starline/device_tracker.py | 6 +++--- homeassistant/components/tile/device_tracker.py | 6 +++--- homeassistant/components/traccar/device_tracker.py | 6 +++--- homeassistant/components/tractive/device_tracker.py | 11 ++++------- homeassistant/components/unifi/device_tracker.py | 10 +++++----- .../components/volvooncall/device_tracker.py | 4 ++-- homeassistant/components/zha/device_tracker.py | 6 +++--- 7 files changed, 23 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/starline/device_tracker.py b/homeassistant/components/starline/device_tracker.py index 796e8ddf08c..35adbd38a4a 100644 --- a/homeassistant/components/starline/device_tracker.py +++ b/homeassistant/components/starline/device_tracker.py @@ -1,6 +1,6 @@ """StarLine device tracker.""" from homeassistant.components.device_tracker.config_entry import TrackerEntity -from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.const import SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -56,9 +56,9 @@ class StarlineDeviceTracker(StarlineEntity, TrackerEntity, RestoreEntity): return self._device.position["y"] @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS @property def icon(self): diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 492c202df08..f749d500b46 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -7,7 +7,7 @@ from pytile.tile import Tile from homeassistant.components.device_tracker import AsyncSeeCallback from homeassistant.components.device_tracker.config_entry import TrackerEntity -from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.const import SourceType from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback @@ -122,9 +122,9 @@ class TileDeviceTracker(CoordinatorEntity, TrackerEntity): return self._tile.longitude @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS @callback def _handle_coordinator_update(self) -> None: diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index f9676d37aa2..784e3c70f33 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -19,8 +19,8 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( CONF_SCAN_INTERVAL, PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, - SOURCE_TYPE_GPS, AsyncSeeCallback, + SourceType, ) from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry @@ -407,9 +407,9 @@ class TraccarEntity(TrackerEntity, RestoreEntity): return {"name": self._name, "identifiers": {(DOMAIN, self._unique_id)}} @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS async def async_added_to_hass(self): """Register state update callback.""" diff --git a/homeassistant/components/tractive/device_tracker.py b/homeassistant/components/tractive/device_tracker.py index 4b08defbf97..19d30aa9856 100644 --- a/homeassistant/components/tractive/device_tracker.py +++ b/homeassistant/components/tractive/device_tracker.py @@ -3,10 +3,7 @@ from __future__ import annotations from typing import Any -from homeassistant.components.device_tracker import ( - SOURCE_TYPE_BLUETOOTH, - SOURCE_TYPE_GPS, -) +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -56,11 +53,11 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity): self._attr_unique_id = item.trackable["_id"] @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" if self._source_type == "PHONE": - return SOURCE_TYPE_BLUETOOTH - return SOURCE_TYPE_GPS + return SourceType.BLUETOOTH + return SourceType.GPS @property def latitude(self) -> float: diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index f2c2230b9e0..5484bcf2d5b 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -19,7 +19,7 @@ from aiounifi.events import ( from homeassistant.components.device_tracker import DOMAIN from homeassistant.components.device_tracker.config_entry import ScannerEntity -from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker.const import SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -276,9 +276,9 @@ class UniFiClientTracker(UniFiClientBase, ScannerEntity): return self._is_connected @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type of the client.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @property def unique_id(self) -> str: @@ -406,9 +406,9 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): return self._is_connected @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type of the device.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @property def name(self) -> str: diff --git a/homeassistant/components/volvooncall/device_tracker.py b/homeassistant/components/volvooncall/device_tracker.py index 866634fc5e1..4f9300fd021 100644 --- a/homeassistant/components/volvooncall/device_tracker.py +++ b/homeassistant/components/volvooncall/device_tracker.py @@ -1,7 +1,7 @@ """Support for tracking a Volvo.""" from __future__ import annotations -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS, AsyncSeeCallback +from homeassistant.components.device_tracker import AsyncSeeCallback, SourceType from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -31,7 +31,7 @@ async def async_setup_scanner( await async_see( dev_id=dev_id, host_name=host_name, - source_type=SOURCE_TYPE_GPS, + source_type=SourceType.GPS, gps=instrument.state, icon="mdi:car", ) diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index cf4a830f4da..eed80a05055 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -4,7 +4,7 @@ from __future__ import annotations import functools import time -from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -85,9 +85,9 @@ class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity): return self._connected @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @callback def async_battery_percentage_remaining_updated(self, attr_id, attr_name, value): From 450b7cd6443a9547c4c3273816adedc9b7232d37 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 31 Jul 2022 15:48:43 +0200 Subject: [PATCH 2995/3516] Use device_tracker SourceType enum [n-r] (#75965) --- homeassistant/components/netgear/device_tracker.py | 6 +++--- homeassistant/components/nmap_tracker/device_tracker.py | 6 +++--- homeassistant/components/owntracks/device_tracker.py | 6 +++--- homeassistant/components/owntracks/messages.py | 9 +++------ homeassistant/components/person/__init__.py | 4 ++-- homeassistant/components/ping/device_tracker.py | 6 +++--- homeassistant/components/renault/device_tracker.py | 6 +++--- .../components/ruckus_unleashed/device_tracker.py | 6 +++--- 8 files changed, 23 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index 0f7f5cffb10..cd648990e05 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging -from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -79,9 +79,9 @@ class NetgearScannerEntity(NetgearBaseEntity, ScannerEntity): return self._active @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @property def ip_address(self) -> str: diff --git a/homeassistant/components/nmap_tracker/device_tracker.py b/homeassistant/components/nmap_tracker/device_tracker.py index 36a461c4891..afb931b82dd 100644 --- a/homeassistant/components/nmap_tracker/device_tracker.py +++ b/homeassistant/components/nmap_tracker/device_tracker.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from typing import Any -from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -93,9 +93,9 @@ class NmapTrackerEntity(ScannerEntity): return short_hostname(self._device.hostname) @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return tracker source type.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @property def should_poll(self) -> bool: diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index 92f34617462..5119168e7ae 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -3,7 +3,7 @@ from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.components.device_tracker.const import ( ATTR_SOURCE_TYPE, DOMAIN, - SOURCE_TYPE_GPS, + SourceType, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -115,9 +115,9 @@ class OwnTracksEntity(TrackerEntity, RestoreEntity): return self._data.get("host_name") @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return self._data.get("source_type", SOURCE_TYPE_GPS) + return self._data.get("source_type", SourceType.GPS) @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 4f17c8a5375..cd474054029 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -6,10 +6,7 @@ from nacl.encoding import Base64Encoder from nacl.secret import SecretBox from homeassistant.components import zone as zone_comp -from homeassistant.components.device_tracker import ( - SOURCE_TYPE_BLUETOOTH_LE, - SOURCE_TYPE_GPS, -) +from homeassistant.components.device_tracker import SourceType from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, STATE_HOME from homeassistant.util import decorator, slugify @@ -84,9 +81,9 @@ def _parse_see_args(message, subscribe_topic): kwargs["attributes"]["battery_status"] = message["bs"] if "t" in message: if message["t"] in ("c", "u"): - kwargs["source_type"] = SOURCE_TYPE_GPS + kwargs["source_type"] = SourceType.GPS if message["t"] == "b": - kwargs["source_type"] = SOURCE_TYPE_BLUETOOTH_LE + kwargs["source_type"] = SourceType.BLUETOOTH_LE return dev_id, kwargs diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 12ee0da3b82..85a6cf6135e 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -11,7 +11,7 @@ from homeassistant.components import persistent_notification, websocket_api from homeassistant.components.device_tracker import ( ATTR_SOURCE_TYPE, DOMAIN as DEVICE_TRACKER_DOMAIN, - SOURCE_TYPE_GPS, + SourceType, ) from homeassistant.const import ( ATTR_EDITABLE, @@ -469,7 +469,7 @@ class Person(RestoreEntity): if not state or state.state in IGNORE_STATES: continue - if state.attributes.get(ATTR_SOURCE_TYPE) == SOURCE_TYPE_GPS: + if state.attributes.get(ATTR_SOURCE_TYPE) == SourceType.GPS: latest_gps = _get_latest(latest_gps, state) elif state.state == STATE_HOME: latest_non_gps_home = _get_latest(latest_non_gps_home, state) diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index cbce224a373..52285350cd4 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -17,7 +17,7 @@ from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker.const import ( CONF_SCAN_INTERVAL, SCAN_INTERVAL, - SOURCE_TYPE_ROUTER, + SourceType, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -114,7 +114,7 @@ async def async_setup_scanner( ) await asyncio.gather( *( - async_see(dev_id=host.dev_id, source_type=SOURCE_TYPE_ROUTER) + async_see(dev_id=host.dev_id, source_type=SourceType.ROUTER) for idx, host in enumerate(hosts) if results[idx] ) @@ -133,7 +133,7 @@ async def async_setup_scanner( _LOGGER.debug("Multiping responses: %s", responses) await asyncio.gather( *( - async_see(dev_id=dev_id, source_type=SOURCE_TYPE_ROUTER) + async_see(dev_id=dev_id, source_type=SourceType.ROUTER) for idx, dev_id in enumerate(ip_to_dev_id.values()) if responses[idx].is_alive ) diff --git a/homeassistant/components/renault/device_tracker.py b/homeassistant/components/renault/device_tracker.py index 3e9a2608f80..da267277d10 100644 --- a/homeassistant/components/renault/device_tracker.py +++ b/homeassistant/components/renault/device_tracker.py @@ -3,7 +3,7 @@ from __future__ import annotations from renault_api.kamereon.models import KamereonVehicleLocationData -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -46,9 +46,9 @@ class RenaultDeviceTracker( return self.coordinator.data.gpsLongitude if self.coordinator.data else None @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS DEVICE_TRACKER_TYPES: tuple[RenaultDataEntityDescription, ...] = ( diff --git a/homeassistant/components/ruckus_unleashed/device_tracker.py b/homeassistant/components/ruckus_unleashed/device_tracker.py index 67c86f3bb51..8c3e78ae479 100644 --- a/homeassistant/components/ruckus_unleashed/device_tracker.py +++ b/homeassistant/components/ruckus_unleashed/device_tracker.py @@ -1,7 +1,7 @@ """Support for Ruckus Unleashed devices.""" from __future__ import annotations -from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -111,6 +111,6 @@ class RuckusUnleashedDevice(CoordinatorEntity, ScannerEntity): return self._mac in self.coordinator.data[API_CLIENTS] @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER From f068fc8eb71ee51b7fe87ddbc0227bf745aab1c5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 31 Jul 2022 15:49:57 +0200 Subject: [PATCH 2996/3516] Use device_tracker SourceType enum [h-m] (#75964) --- homeassistant/components/huawei_lte/device_tracker.py | 8 ++++---- homeassistant/components/icloud/device_tracker.py | 6 +++--- homeassistant/components/keenetic_ndms2/device_tracker.py | 6 +++--- homeassistant/components/life360/device_tracker.py | 6 +++--- homeassistant/components/locative/device_tracker.py | 6 +++--- homeassistant/components/mazda/device_tracker.py | 6 +++--- homeassistant/components/meraki/device_tracker.py | 4 ++-- homeassistant/components/mikrotik/device_tracker.py | 6 +++--- 8 files changed, 24 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 179587d72d7..18d797aefc6 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -11,7 +11,7 @@ from stringcase import snakecase from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import ( DOMAIN as DEVICE_TRACKER_DOMAIN, - SOURCE_TYPE_ROUTER, + SourceType, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -194,9 +194,9 @@ class HuaweiLteScannerEntity(HuaweiLteBaseEntity, ScannerEntity): return self.mac_address @property - def source_type(self) -> str: - """Return SOURCE_TYPE_ROUTER.""" - return SOURCE_TYPE_ROUTER + def source_type(self) -> SourceType: + """Return SourceType.ROUTER.""" + return SourceType.ROUTER @property def ip_address(self) -> str | None: diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index ec543a9ed30..eaebb0b7717 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS, AsyncSeeCallback +from homeassistant.components.device_tracker import AsyncSeeCallback, SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -105,9 +105,9 @@ class IcloudTrackerEntity(TrackerEntity): return self._device.battery_level @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS @property def icon(self) -> str: diff --git a/homeassistant/components/keenetic_ndms2/device_tracker.py b/homeassistant/components/keenetic_ndms2/device_tracker.py index b625cabbbc1..397d1888a9e 100644 --- a/homeassistant/components/keenetic_ndms2/device_tracker.py +++ b/homeassistant/components/keenetic_ndms2/device_tracker.py @@ -7,7 +7,7 @@ from ndms2_client import Device from homeassistant.components.device_tracker import ( DOMAIN as DEVICE_TRACKER_DOMAIN, - SOURCE_TYPE_ROUTER, + SourceType, ) from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry @@ -109,9 +109,9 @@ class KeeneticTracker(ScannerEntity): ) @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type of the client.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @property def name(self) -> str: diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index 5a18422487e..ebb179beba2 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -5,7 +5,7 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any, cast -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_BATTERY_CHARGING @@ -192,9 +192,9 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity): return self._data.battery_level @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS @property def location_accuracy(self) -> int: diff --git a/homeassistant/components/locative/device_tracker.py b/homeassistant/components/locative/device_tracker.py index 4ededae6b01..cd927aace38 100644 --- a/homeassistant/components/locative/device_tracker.py +++ b/homeassistant/components/locative/device_tracker.py @@ -1,5 +1,5 @@ """Support for the Locative platform.""" -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -60,9 +60,9 @@ class LocativeEntity(TrackerEntity): return self._name @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS async def async_added_to_hass(self): """Register state update callback.""" diff --git a/homeassistant/components/mazda/device_tracker.py b/homeassistant/components/mazda/device_tracker.py index 79a9ef90b9b..946cff72f5b 100644 --- a/homeassistant/components/mazda/device_tracker.py +++ b/homeassistant/components/mazda/device_tracker.py @@ -1,5 +1,5 @@ """Platform for Mazda device tracker integration.""" -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -40,9 +40,9 @@ class MazdaDeviceTracker(MazdaEntity, TrackerEntity): self._attr_unique_id = self.vin @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS @property def latitude(self): diff --git a/homeassistant/components/meraki/device_tracker.py b/homeassistant/components/meraki/device_tracker.py index 7447d8ce879..dae98734ae7 100644 --- a/homeassistant/components/meraki/device_tracker.py +++ b/homeassistant/components/meraki/device_tracker.py @@ -9,8 +9,8 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, - SOURCE_TYPE_ROUTER, AsyncSeeCallback, + SourceType, ) from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback @@ -127,7 +127,7 @@ class MerakiView(HomeAssistantView): self.async_see( gps=gps_location, mac=mac, - source_type=SOURCE_TYPE_ROUTER, + source_type=SourceType.ROUTER, gps_accuracy=accuracy, attributes=attrs, ) diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 158d95dd683..d02bf69b5ab 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -6,7 +6,7 @@ from typing import Any from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import ( DOMAIN as DEVICE_TRACKER, - SOURCE_TYPE_ROUTER, + SourceType, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -101,9 +101,9 @@ class MikrotikDataUpdateCoordinatorTracker( return False @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type of the client.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @property def hostname(self) -> str: From 1a8ccfeb56095b16d6de471e99c3a82471a83716 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 31 Jul 2022 15:51:04 +0200 Subject: [PATCH 2997/3516] Use device_tracker SourceType enum [a-g] (#75963) --- homeassistant/components/asuswrt/device_tracker.py | 6 +++--- .../components/bluetooth_le_tracker/device_tracker.py | 4 ++-- .../components/bluetooth_tracker/device_tracker.py | 4 ++-- .../components/bmw_connected_drive/device_tracker.py | 7 +++---- .../components/devolo_home_network/device_tracker.py | 6 +++--- homeassistant/components/dhcp/__init__.py | 4 ++-- homeassistant/components/freebox/device_tracker.py | 6 +++--- homeassistant/components/fritz/device_tracker.py | 6 +++--- homeassistant/components/geofency/device_tracker.py | 6 +++--- homeassistant/components/google_maps/device_tracker.py | 4 ++-- homeassistant/components/gpslogger/device_tracker.py | 6 +++--- 11 files changed, 29 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index af43294c954..fc2d4ede26d 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -1,7 +1,7 @@ """Support for ASUSWRT routers.""" from __future__ import annotations -from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -69,9 +69,9 @@ class AsusWrtDevice(ScannerEntity): return self._device.is_connected @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @property def hostname(self) -> str | None: diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 6ba33e506cc..b416fe3e070 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -16,7 +16,7 @@ from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker.const import ( CONF_TRACK_NEW, SCAN_INTERVAL, - SOURCE_TYPE_BLUETOOTH_LE, + SourceType, ) from homeassistant.components.device_tracker.legacy import ( YAML_DEVICES, @@ -106,7 +106,7 @@ async def async_setup_scanner( # noqa: C901 await async_see( mac=BLE_PREFIX + address, host_name=name, - source_type=SOURCE_TYPE_BLUETOOTH_LE, + source_type=SourceType.BLUETOOTH_LE, battery=battery, ) diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 90ae473a0cd..d266ba5d542 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -19,7 +19,7 @@ from homeassistant.components.device_tracker.const import ( CONF_TRACK_NEW, DEFAULT_TRACK_NEW, SCAN_INTERVAL, - SOURCE_TYPE_BLUETOOTH, + SourceType, ) from homeassistant.components.device_tracker.legacy import ( YAML_DEVICES, @@ -93,7 +93,7 @@ async def see_device( mac=f"{BT_PREFIX}{mac}", host_name=device_name, attributes=attributes, - source_type=SOURCE_TYPE_BLUETOOTH, + source_type=SourceType.BLUETOOTH, ) diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index dc71100455d..c06ecdaa9bb 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -2,11 +2,10 @@ from __future__ import annotations import logging -from typing import Literal from bimmer_connected.vehicle import MyBMWVehicle -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -80,6 +79,6 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity): ) @property - def source_type(self) -> Literal["gps"]: + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS diff --git a/homeassistant/components/devolo_home_network/device_tracker.py b/homeassistant/components/devolo_home_network/device_tracker.py index 9dffeef7db9..0e3e47d9320 100644 --- a/homeassistant/components/devolo_home_network/device_tracker.py +++ b/homeassistant/components/devolo_home_network/device_tracker.py @@ -7,7 +7,7 @@ from devolo_plc_api.device import Device from homeassistant.components.device_tracker import ( DOMAIN as DEVICE_TRACKER_DOMAIN, - SOURCE_TYPE_ROUTER, + SourceType, ) from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry @@ -149,9 +149,9 @@ class DevoloScannerEntity(CoordinatorEntity, ScannerEntity): return self._mac @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return tracker source type.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @property def unique_id(self) -> str: diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py index 8581a5f2241..be9cbd7426d 100644 --- a/homeassistant/components/dhcp/__init__.py +++ b/homeassistant/components/dhcp/__init__.py @@ -31,7 +31,7 @@ from homeassistant.components.device_tracker.const import ( ATTR_SOURCE_TYPE, CONNECTED_DEVICE_REGISTERED, DOMAIN as DEVICE_TRACKER_DOMAIN, - SOURCE_TYPE_ROUTER, + SourceType, ) from homeassistant.const import ( EVENT_HOMEASSISTANT_STARTED, @@ -318,7 +318,7 @@ class DeviceTrackerWatcher(WatcherBase): attributes = state.attributes - if attributes.get(ATTR_SOURCE_TYPE) != SOURCE_TYPE_ROUTER: + if attributes.get(ATTR_SOURCE_TYPE) != SourceType.ROUTER: return ip_address = attributes.get(ATTR_IP) diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 0fe04fe7eb9..1fd7a35e975 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import datetime from typing import Any -from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -98,9 +98,9 @@ class FreeboxDevice(ScannerEntity): return self._active @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return the source type.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER @property def icon(self) -> str: diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index 34c3f64e1e5..9591fec156b 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -4,7 +4,7 @@ from __future__ import annotations import datetime import logging -from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -118,6 +118,6 @@ class FritzBoxTracker(FritzDeviceBase, ScannerEntity): return attrs @property - def source_type(self) -> str: + def source_type(self) -> SourceType: """Return tracker source type.""" - return SOURCE_TYPE_ROUTER + return SourceType.ROUTER diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index bd4b2852019..61deb9ede7d 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -1,5 +1,5 @@ """Support for the Geofency device tracker platform.""" -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE @@ -96,9 +96,9 @@ class GeofencyEntity(TrackerEntity, RestoreEntity): return DeviceInfo(identifiers={(GF_DOMAIN, self._unique_id)}, name=self._name) @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS async def async_added_to_hass(self): """Register state update callback.""" diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index 8d8be8c0fe1..8f15278e214 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -10,8 +10,8 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA as PLATFORM_SCHEMA_BASE, - SOURCE_TYPE_GPS, SeeCallback, + SourceType, ) from homeassistant.const import ( ATTR_BATTERY_CHARGING, @@ -129,7 +129,7 @@ class GoogleMapsScanner: dev_id=dev_id, gps=(person.latitude, person.longitude), picture=person.picture_url, - source_type=SOURCE_TYPE_GPS, + source_type=SourceType.GPS, gps_accuracy=person.accuracy, attributes=attrs, ) diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index ea648ed7495..22e9529706f 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -1,5 +1,5 @@ """Support for the GPSLogger device tracking.""" -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker import SourceType from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -118,9 +118,9 @@ class GPSLoggerEntity(TrackerEntity, RestoreEntity): return DeviceInfo(identifiers={(GPL_DOMAIN, self._unique_id)}, name=self._name) @property - def source_type(self): + def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" - return SOURCE_TYPE_GPS + return SourceType.GPS async def async_added_to_hass(self): """Register state update callback.""" From c79559751156f009c22e1b8f1f0d61e2cb4f7082 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 31 Jul 2022 18:00:42 +0200 Subject: [PATCH 2998/3516] Improve authentication handling for camera view (#75979) --- homeassistant/components/camera/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 3bf86dedea1..77bd0b57f1c 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -14,7 +14,7 @@ import os from random import SystemRandom from typing import Final, Optional, cast, final -from aiohttp import web +from aiohttp import hdrs, web import async_timeout import attr import voluptuous as vol @@ -715,8 +715,11 @@ class CameraView(HomeAssistantView): ) if not authenticated: - if request[KEY_AUTHENTICATED]: + # Attempt with invalid bearer token, raise unauthorized + # so ban middleware can handle it. + if hdrs.AUTHORIZATION in request.headers: raise web.HTTPUnauthorized() + # Invalid sigAuth or camera access token raise web.HTTPForbidden() if not camera.is_on: From 20fec104e2a11b1a5164d7fe779eb0d894e098cf Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 31 Jul 2022 20:46:13 +0200 Subject: [PATCH 2999/3516] Improve type hints in light [a-i] (#75936) * Improve type hints in ads light * Improve type hints in avea light * Improve type hints in avion light * Improve type hints in broadlink light * More type hints * One more --- homeassistant/components/ads/light.py | 8 +++++--- homeassistant/components/avea/light.py | 8 +++++--- homeassistant/components/avion/light.py | 5 +++-- homeassistant/components/broadlink/light.py | 5 +++-- homeassistant/components/control4/light.py | 5 +++-- homeassistant/components/decora_wifi/light.py | 11 ++++++----- homeassistant/components/dynalite/light.py | 6 ++++-- homeassistant/components/enocean/light.py | 5 +++-- homeassistant/components/eufy/light.py | 14 ++++++++------ homeassistant/components/firmata/light.py | 5 +++-- homeassistant/components/fjaraskupan/light.py | 6 ++++-- homeassistant/components/futurenow/light.py | 12 +++++++----- homeassistant/components/greenwave/light.py | 7 ++++--- homeassistant/components/homematic/light.py | 10 ++++++---- .../components/homematicip_cloud/light.py | 12 ++++++------ homeassistant/components/homeworks/light.py | 7 ++++--- homeassistant/components/iaqualink/light.py | 8 +++++--- homeassistant/components/iglo/light.py | 9 +++++---- homeassistant/components/ihc/light.py | 6 ++++-- 19 files changed, 88 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/ads/light.py b/homeassistant/components/ads/light.py index a4ec970ee15..8dd55775b7a 100644 --- a/homeassistant/components/ads/light.py +++ b/homeassistant/components/ads/light.py @@ -1,6 +1,8 @@ """Support for ADS light sources.""" from __future__ import annotations +from typing import Any + import pyads import voluptuous as vol @@ -66,7 +68,7 @@ class AdsLight(AdsEntity, LightEntity): self._attr_color_mode = ColorMode.ONOFF self._attr_supported_color_modes = {ColorMode.ONOFF} - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register device notification.""" await self.async_initialize_device(self._ads_var, pyads.PLCTYPE_BOOL) @@ -87,7 +89,7 @@ class AdsLight(AdsEntity, LightEntity): """Return True if the entity is on.""" return self._state_dict[STATE_KEY_STATE] - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on or set a specific dimmer value.""" brightness = kwargs.get(ATTR_BRIGHTNESS) self._ads_hub.write_by_name(self._ads_var, True, pyads.PLCTYPE_BOOL) @@ -97,6 +99,6 @@ class AdsLight(AdsEntity, LightEntity): self._ads_var_brightness, brightness, pyads.PLCTYPE_UINT ) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" self._ads_hub.write_by_name(self._ads_var, False, pyads.PLCTYPE_BOOL) diff --git a/homeassistant/components/avea/light.py b/homeassistant/components/avea/light.py index 31054e2e287..5b306b058d3 100644 --- a/homeassistant/components/avea/light.py +++ b/homeassistant/components/avea/light.py @@ -1,6 +1,8 @@ """Support for the Elgato Avea lights.""" from __future__ import annotations +from typing import Any + import avea # pylint: disable=import-error from homeassistant.components.light import ( @@ -46,7 +48,7 @@ class AveaLight(LightEntity): self._attr_name = light.name self._attr_brightness = light.brightness - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" if not kwargs: self._light.set_brightness(4095) @@ -58,11 +60,11 @@ class AveaLight(LightEntity): rgb = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) self._light.set_rgb(rgb[0], rgb[1], rgb[2]) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" self._light.set_brightness(0) - def update(self): + def update(self) -> None: """Fetch new state data for this light. This is the only method that should fetch new data for Home Assistant. diff --git a/homeassistant/components/avion/light.py b/homeassistant/components/avion/light.py index 7df17c2e74e..54d06d50a60 100644 --- a/homeassistant/components/avion/light.py +++ b/homeassistant/components/avion/light.py @@ -3,6 +3,7 @@ from __future__ import annotations import importlib import time +from typing import Any import voluptuous as vol @@ -103,7 +104,7 @@ class AvionLight(LightEntity): self._switch.connect() return True - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: self._attr_brightness = brightness @@ -111,7 +112,7 @@ class AvionLight(LightEntity): self.set_state(self.brightness) self._attr_is_on = True - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the specified or all lights off.""" self.set_state(0) self._attr_is_on = False diff --git a/homeassistant/components/broadlink/light.py b/homeassistant/components/broadlink/light.py index 4b655e4bd42..c4dd48b7dc0 100644 --- a/homeassistant/components/broadlink/light.py +++ b/homeassistant/components/broadlink/light.py @@ -1,5 +1,6 @@ """Support for Broadlink lights.""" import logging +from typing import Any from broadlink.exceptions import BroadlinkException @@ -88,7 +89,7 @@ class BroadlinkLight(BroadlinkEntity, LightEntity): # Scenes are not yet supported. self._attr_color_mode = ColorMode.UNKNOWN - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" state = {"pwr": 1} @@ -122,7 +123,7 @@ class BroadlinkLight(BroadlinkEntity, LightEntity): await self._async_set_state(state) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" await self._async_set_state({"pwr": 0}) diff --git a/homeassistant/components/control4/light.py b/homeassistant/components/control4/light.py index ded72944af7..539547a79f1 100644 --- a/homeassistant/components/control4/light.py +++ b/homeassistant/components/control4/light.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from datetime import timedelta import logging +from typing import Any from pyControl4.error_handling import C4Exception from pyControl4.light import C4Light @@ -197,7 +198,7 @@ class Control4Light(Control4Entity, LightEntity): return LightEntityFeature.TRANSITION return 0 - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" c4_light = self.create_api_object() if self._is_dimmer: @@ -220,7 +221,7 @@ class Control4Light(Control4Entity, LightEntity): await asyncio.sleep(delay_time) await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" c4_light = self.create_api_object() if self._is_dimmer: diff --git a/homeassistant/components/decora_wifi/light.py b/homeassistant/components/decora_wifi/light.py index 7d3b1e353e2..3c43e816097 100644 --- a/homeassistant/components/decora_wifi/light.py +++ b/homeassistant/components/decora_wifi/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any # pylint: disable=import-error from decora_wifi import DecoraWiFiSession @@ -111,7 +112,7 @@ class DecoraWifiLight(LightEntity): return {self.color_mode} @property - def supported_features(self): + def supported_features(self) -> int: """Return supported features.""" if self._switch.canSetLevel: return LightEntityFeature.TRANSITION @@ -137,9 +138,9 @@ class DecoraWifiLight(LightEntity): """Return true if switch is on.""" return self._switch.power == "ON" - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Instruct the switch to turn on & adjust brightness.""" - attribs = {"power": "ON"} + attribs: dict[str, Any] = {"power": "ON"} if ATTR_BRIGHTNESS in kwargs: min_level = self._switch.data.get("minLevel", 0) @@ -157,7 +158,7 @@ class DecoraWifiLight(LightEntity): except ValueError: _LOGGER.error("Failed to turn on myLeviton switch") - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Instruct the switch to turn off.""" attribs = {"power": "OFF"} try: @@ -165,7 +166,7 @@ class DecoraWifiLight(LightEntity): except ValueError: _LOGGER.error("Failed to turn off myLeviton switch") - def update(self): + def update(self) -> None: """Fetch new state data for this switch.""" try: self._switch.refresh() diff --git a/homeassistant/components/dynalite/light.py b/homeassistant/components/dynalite/light.py index 826506fa9b7..27cd6f8cae8 100644 --- a/homeassistant/components/dynalite/light.py +++ b/homeassistant/components/dynalite/light.py @@ -1,5 +1,7 @@ """Support for Dynalite channels as lights.""" +from typing import Any + from homeassistant.components.light import ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -35,10 +37,10 @@ class DynaliteLight(DynaliteBase, LightEntity): """Return true if device is on.""" return self._device.is_on - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" await self._device.async_turn_on(**kwargs) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._device.async_turn_off(**kwargs) diff --git a/homeassistant/components/enocean/light.py b/homeassistant/components/enocean/light.py index 8ecc3f63831..479723bd051 100644 --- a/homeassistant/components/enocean/light.py +++ b/homeassistant/components/enocean/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import math +from typing import Any from enocean.utils import combine_hex import voluptuous as vol @@ -80,7 +81,7 @@ class EnOceanLight(EnOceanEntity, LightEntity): """If light is on.""" return self._on_state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light source on or sets a specific dimmer value.""" if (brightness := kwargs.get(ATTR_BRIGHTNESS)) is not None: self._brightness = brightness @@ -94,7 +95,7 @@ class EnOceanLight(EnOceanEntity, LightEntity): self.send_command(command, [], 0x01) self._on_state = True - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light source off.""" command = [0xA5, 0x02, 0x00, 0x01, 0x09] command.extend(self._sender_id) diff --git a/homeassistant/components/eufy/light.py b/homeassistant/components/eufy/light.py index 57d54146137..f3d5fe58e7d 100644 --- a/homeassistant/components/eufy/light.py +++ b/homeassistant/components/eufy/light.py @@ -1,6 +1,8 @@ """Support for Eufy lights.""" from __future__ import annotations +from typing import Any + import lakeside from homeassistant.components.light import ( @@ -59,7 +61,7 @@ class EufyLight(LightEntity): self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} self._bulb.connect() - def update(self): + def update(self) -> None: """Synchronise state from the bulb.""" self._bulb.update() if self._bulb.power: @@ -93,12 +95,12 @@ class EufyLight(LightEntity): return int(self._brightness * 255 / 100) @property - def min_mireds(self): + def min_mireds(self) -> int: """Return minimum supported color temperature.""" return kelvin_to_mired(EUFY_MAX_KELVIN) @property - def max_mireds(self): + def max_mireds(self) -> int: """Return maximum supported color temperature.""" return kelvin_to_mired(EUFY_MIN_KELVIN) @@ -116,7 +118,7 @@ class EufyLight(LightEntity): return self._hs @property - def color_mode(self) -> str | None: + def color_mode(self) -> ColorMode: """Return the color mode of the light.""" if self._type == "T1011": return ColorMode.BRIGHTNESS @@ -127,7 +129,7 @@ class EufyLight(LightEntity): return ColorMode.COLOR_TEMP return ColorMode.HS - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the specified light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) colortemp = kwargs.get(ATTR_COLOR_TEMP) @@ -169,7 +171,7 @@ class EufyLight(LightEntity): power=True, brightness=brightness, temperature=temp, colors=rgb ) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the specified light off.""" try: self._bulb.set_state(power=False) diff --git a/homeassistant/components/firmata/light.py b/homeassistant/components/firmata/light.py index 8f562b12d30..59be2484d66 100644 --- a/homeassistant/components/firmata/light.py +++ b/homeassistant/components/firmata/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry @@ -82,14 +83,14 @@ class FirmataLight(FirmataPinEntity, LightEntity): """Return the brightness of the light.""" return self._api.state - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on light.""" level = kwargs.get(ATTR_BRIGHTNESS, self._last_on_level) await self._api.set_level(level) self.async_write_ha_state() self._last_on_level = level - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off light.""" await self._api.set_level(0) self.async_write_ha_state() diff --git a/homeassistant/components/fjaraskupan/light.py b/homeassistant/components/fjaraskupan/light.py index f492f628a3a..c42943710de 100644 --- a/homeassistant/components/fjaraskupan/light.py +++ b/homeassistant/components/fjaraskupan/light.py @@ -1,6 +1,8 @@ """Support for lights.""" from __future__ import annotations +from typing import Any + from fjaraskupan import COMMAND_LIGHT_ON_OFF, Device from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity @@ -45,7 +47,7 @@ class Light(CoordinatorEntity[Coordinator], LightEntity): self._attr_unique_id = device.address self._attr_device_info = device_info - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: await self._device.send_dim(int(kwargs[ATTR_BRIGHTNESS] * (100.0 / 255.0))) @@ -54,7 +56,7 @@ class Light(CoordinatorEntity[Coordinator], LightEntity): await self._device.send_command(COMMAND_LIGHT_ON_OFF) self.coordinator.async_set_updated_data(self._device.state) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" if self.is_on: await self._device.send_command(COMMAND_LIGHT_ON_OFF) diff --git a/homeassistant/components/futurenow/light.py b/homeassistant/components/futurenow/light.py index e27436966fc..d070d832052 100644 --- a/homeassistant/components/futurenow/light.py +++ b/homeassistant/components/futurenow/light.py @@ -1,6 +1,8 @@ """Support for FutureNow Ethernet unit outputs as Lights.""" from __future__ import annotations +from typing import Any + import pyfnip import voluptuous as vol @@ -106,18 +108,18 @@ class FutureNowLight(LightEntity): return self._brightness @property - def color_mode(self) -> str: + def color_mode(self) -> ColorMode: """Return the color mode of the light.""" if self._dimmable: return ColorMode.BRIGHTNESS return ColorMode.ONOFF @property - def supported_color_modes(self) -> set[str] | None: + def supported_color_modes(self) -> set[ColorMode]: """Flag supported color modes.""" return {self.color_mode} - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if self._dimmable: level = kwargs.get(ATTR_BRIGHTNESS, self._last_brightness) @@ -125,13 +127,13 @@ class FutureNowLight(LightEntity): level = 255 self._light.turn_on(to_futurenow_level(level)) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" self._light.turn_off() if self._brightness: self._last_brightness = self._brightness - def update(self): + def update(self) -> None: """Fetch new state data for this light.""" state = int(self._light.is_on()) self._state = bool(state) diff --git a/homeassistant/components/greenwave/light.py b/homeassistant/components/greenwave/light.py index a6b018f33ff..1e991feee90 100644 --- a/homeassistant/components/greenwave/light.py +++ b/homeassistant/components/greenwave/light.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta import logging import os +from typing import Any import greenwavereality as greenwave import voluptuous as vol @@ -99,17 +100,17 @@ class GreenwaveLight(LightEntity): """Return true if light is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" temp_brightness = int((kwargs.get(ATTR_BRIGHTNESS, 255) / 255) * 100) greenwave.set_brightness(self._host, self._did, temp_brightness, self._token) greenwave.turn_on(self._host, self._did, self._token) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" greenwave.turn_off(self._host, self._did, self._token) - def update(self): + def update(self) -> None: """Fetch new state data for this light.""" self._gatewaydata.update() bulbs = self._gatewaydata.greenwave diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index 4087f83dd2a..38a75266a17 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -1,6 +1,8 @@ """Support for Homematic lights.""" from __future__ import annotations +from typing import Any + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -80,9 +82,9 @@ class HMLight(HMDevice, LightEntity): return color_modes @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" - features = LightEntityFeature.TRANSITION + features: int = LightEntityFeature.TRANSITION if "PROGRAM" in self._hmdevice.WRITENODE: features |= LightEntityFeature.EFFECT return features @@ -117,7 +119,7 @@ class HMLight(HMDevice, LightEntity): return None return self._hmdevice.get_effect() - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on and/or change color or color effect settings.""" if ATTR_TRANSITION in kwargs: self._hmdevice.setValue("RAMP_TIME", kwargs[ATTR_TRANSITION], self._channel) @@ -146,7 +148,7 @@ class HMLight(HMDevice, LightEntity): if ATTR_EFFECT in kwargs: self._hmdevice.set_effect(kwargs[ATTR_EFFECT]) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" if ATTR_TRANSITION in kwargs: self._hmdevice.setValue("RAMP_TIME", kwargs[ATTR_TRANSITION], self._channel) diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 0cf02d88471..c43ac510023 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -81,11 +81,11 @@ class HomematicipLight(HomematicipGenericEntity, LightEntity): """Return true if light is on.""" return self._device.on - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" await self._device.turn_on() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._device.turn_off() @@ -125,7 +125,7 @@ class HomematicipMultiDimmer(HomematicipGenericEntity, LightEntity): (self._device.functionalChannels[self._channel].dimLevel or 0.0) * 255 ) - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the dimmer on.""" if ATTR_BRIGHTNESS in kwargs: await self._device.set_dim_level( @@ -134,7 +134,7 @@ class HomematicipMultiDimmer(HomematicipGenericEntity, LightEntity): else: await self._device.set_dim_level(1, self._channel) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the dimmer off.""" await self._device.set_dim_level(0, self._channel) @@ -213,7 +213,7 @@ class HomematicipNotificationLight(HomematicipGenericEntity, LightEntity): """Return a unique ID.""" return f"{self.__class__.__name__}_{self._post}_{self._device.id}" - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" # Use hs_color from kwargs, # if not applicable use current hs_color. @@ -241,7 +241,7 @@ class HomematicipNotificationLight(HomematicipGenericEntity, LightEntity): rampTime=transition, ) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" simple_rgb_color = self._func_channel.simpleRGBColorState transition = kwargs.get(ATTR_TRANSITION, 0.5) diff --git a/homeassistant/components/homeworks/light.py b/homeassistant/components/homeworks/light.py index 0a8d7f2fb28..35a9e5665d1 100644 --- a/homeassistant/components/homeworks/light.py +++ b/homeassistant/components/homeworks/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pyhomeworks.pyhomeworks import HW_LIGHT_CHANGED @@ -50,7 +51,7 @@ class HomeworksLight(HomeworksDevice, LightEntity): self._level = 0 self._prev_level = 0 - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" signal = f"homeworks_entity_{self._addr}" _LOGGER.debug("connecting %s", signal) @@ -59,7 +60,7 @@ class HomeworksLight(HomeworksDevice, LightEntity): ) self._controller.request_dimmer_level(self._addr) - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" if ATTR_BRIGHTNESS in kwargs: new_level = kwargs[ATTR_BRIGHTNESS] @@ -69,7 +70,7 @@ class HomeworksLight(HomeworksDevice, LightEntity): new_level = self._prev_level self._set_brightness(new_level) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" self._set_brightness(0) diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py index 505ad8da0cc..3a8b1e0fb4a 100644 --- a/homeassistant/components/iaqualink/light.py +++ b/homeassistant/components/iaqualink/light.py @@ -1,6 +1,8 @@ """Support for Aqualink pool lights.""" from __future__ import annotations +from typing import Any + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_EFFECT, @@ -46,7 +48,7 @@ class HassAqualinkLight(AqualinkEntity, LightEntity): return self.dev.is_on @refresh_system - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light. This handles brightness and light effects for lights that do support @@ -63,7 +65,7 @@ class HassAqualinkLight(AqualinkEntity, LightEntity): await await_or_reraise(self.dev.turn_on()) @refresh_system - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" await await_or_reraise(self.dev.turn_off()) @@ -93,7 +95,7 @@ class HassAqualinkLight(AqualinkEntity, LightEntity): return ColorMode.ONOFF @property - def supported_color_modes(self) -> set[str] | None: + def supported_color_modes(self) -> set[ColorMode]: """Flag supported color modes.""" return {self.color_mode} diff --git a/homeassistant/components/iglo/light.py b/homeassistant/components/iglo/light.py index 39e1a5986ab..5de24625ea3 100644 --- a/homeassistant/components/iglo/light.py +++ b/homeassistant/components/iglo/light.py @@ -2,6 +2,7 @@ from __future__ import annotations import math +from typing import Any from iglo import Lamp from iglo.lamp import MODE_WHITE @@ -86,14 +87,14 @@ class IGloLamp(LightEntity): return color_util.color_temperature_kelvin_to_mired(self._lamp.state()["white"]) @property - def min_mireds(self): + def min_mireds(self) -> int: """Return the coldest color_temp that this light supports.""" return math.ceil( color_util.color_temperature_kelvin_to_mired(self._lamp.max_kelvin) ) @property - def max_mireds(self): + def max_mireds(self) -> int: """Return the warmest color_temp that this light supports.""" return math.ceil( color_util.color_temperature_kelvin_to_mired(self._lamp.min_kelvin) @@ -119,7 +120,7 @@ class IGloLamp(LightEntity): """Return true if light is on.""" return self._lamp.state()["on"] - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if not self.is_on: self._lamp.switch(True) @@ -145,6 +146,6 @@ class IGloLamp(LightEntity): self._lamp.effect(effect) return - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" self._lamp.switch(False) diff --git a/homeassistant/components/ihc/light.py b/homeassistant/components/ihc/light.py index caceaf9737c..089317ca7d7 100644 --- a/homeassistant/components/ihc/light.py +++ b/homeassistant/components/ihc/light.py @@ -1,6 +1,8 @@ """Support for IHC lights.""" from __future__ import annotations +from typing import Any + from ihcsdk.ihccontroller import IHCController from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity @@ -90,7 +92,7 @@ class IhcLight(IHCDevice, LightEntity): """Return true if light is on.""" return self._state - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: brightness = kwargs[ATTR_BRIGHTNESS] @@ -108,7 +110,7 @@ class IhcLight(IHCDevice, LightEntity): else: await async_set_bool(self.hass, self.ihc_controller, self.ihc_id, True) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" if self._dimmable: await async_set_int(self.hass, self.ihc_controller, self.ihc_id, 0) From 84d91d2b3ad04102169824a78ca306494c4ecd44 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sun, 31 Jul 2022 21:14:03 +0200 Subject: [PATCH 3000/3516] Bump pyotgw to 2.0.2 (#75980) --- homeassistant/components/opentherm_gw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 0bc69387d0b..02b1604ea11 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -2,7 +2,7 @@ "domain": "opentherm_gw", "name": "OpenTherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", - "requirements": ["pyotgw==2.0.1"], + "requirements": ["pyotgw==2.0.2"], "codeowners": ["@mvn23"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index fc08ba28427..a5d214626ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1728,7 +1728,7 @@ pyopnsense==0.2.0 pyoppleio==1.0.5 # homeassistant.components.opentherm_gw -pyotgw==2.0.1 +pyotgw==2.0.2 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0caac1ec257..abf62aaccdc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1193,7 +1193,7 @@ pyopenuv==2022.04.0 pyopnsense==0.2.0 # homeassistant.components.opentherm_gw -pyotgw==2.0.1 +pyotgw==2.0.2 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp From 97b30bec8b3f6727183bafebc2fb2696d46d966e Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Sun, 31 Jul 2022 22:25:16 +0300 Subject: [PATCH 3001/3516] Added a configuration_url for the ukraine_alarm service (#75988) --- homeassistant/components/ukraine_alarm/binary_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/ukraine_alarm/binary_sensor.py b/homeassistant/components/ukraine_alarm/binary_sensor.py index b98add95e03..10d66e0cb64 100644 --- a/homeassistant/components/ukraine_alarm/binary_sensor.py +++ b/homeassistant/components/ukraine_alarm/binary_sensor.py @@ -98,6 +98,7 @@ class UkraineAlarmSensor( identifiers={(DOMAIN, unique_id)}, manufacturer=MANUFACTURER, name=name, + configuration_url="https://siren.pp.ua/", ) @property From 98293f2179fdb6d0d6dc559f7d80d601efeb96e3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 31 Jul 2022 21:29:54 +0200 Subject: [PATCH 3002/3516] Use climate enums in alexa (#75911) --- homeassistant/components/alexa/capabilities.py | 2 +- homeassistant/components/alexa/const.py | 16 ++++++++-------- homeassistant/components/alexa/entities.py | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 3963372fec1..1456221e20e 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -379,7 +379,7 @@ class AlexaPowerController(AlexaCapability): raise UnsupportedProperty(name) if self.entity.domain == climate.DOMAIN: - is_on = self.entity.state != climate.HVAC_MODE_OFF + is_on = self.entity.state != climate.HVACMode.OFF elif self.entity.domain == fan.DOMAIN: is_on = self.entity.state == fan.STATE_ON elif self.entity.domain == vacuum.DOMAIN: diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 6b509d9b3c6..c6ac3071d94 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -68,16 +68,16 @@ API_TEMP_UNITS = {TEMP_FAHRENHEIT: "FAHRENHEIT", TEMP_CELSIUS: "CELSIUS"} # back to HA state. API_THERMOSTAT_MODES = OrderedDict( [ - (climate.HVAC_MODE_HEAT, "HEAT"), - (climate.HVAC_MODE_COOL, "COOL"), - (climate.HVAC_MODE_HEAT_COOL, "AUTO"), - (climate.HVAC_MODE_AUTO, "AUTO"), - (climate.HVAC_MODE_OFF, "OFF"), - (climate.HVAC_MODE_FAN_ONLY, "OFF"), - (climate.HVAC_MODE_DRY, "CUSTOM"), + (climate.HVACMode.HEAT, "HEAT"), + (climate.HVACMode.COOL, "COOL"), + (climate.HVACMode.HEAT_COOL, "AUTO"), + (climate.HVACMode.AUTO, "AUTO"), + (climate.HVACMode.OFF, "OFF"), + (climate.HVACMode.FAN_ONLY, "OFF"), + (climate.HVACMode.DRY, "CUSTOM"), ] ) -API_THERMOSTAT_MODES_CUSTOM = {climate.HVAC_MODE_DRY: "DEHUMIDIFY"} +API_THERMOSTAT_MODES_CUSTOM = {climate.HVACMode.DRY: "DEHUMIDIFY"} API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} # AlexaModeController does not like a single mode for the fan preset, we add PRESET_MODE_NA if a fan has only one preset_mode diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index f380f990449..ac78dbeed5e 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -460,7 +460,7 @@ class ClimateCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" # If we support two modes, one being off, we allow turning on too. - if climate.HVAC_MODE_OFF in self.entity.attributes.get( + if climate.HVACMode.OFF in self.entity.attributes.get( climate.ATTR_HVAC_MODES, [] ): yield AlexaPowerController(self.entity) From a3bffdf523ef962d10395cc94346a12dd48e8d2c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 31 Jul 2022 14:10:29 -0600 Subject: [PATCH 3003/3516] Appropriately mark Guardian entities as `unavailable` during reboot (#75234) --- homeassistant/components/guardian/__init__.py | 13 ++++++- .../components/guardian/binary_sensor.py | 2 +- homeassistant/components/guardian/button.py | 3 ++ homeassistant/components/guardian/sensor.py | 4 +- homeassistant/components/guardian/util.py | 38 ++++++++++++++++++- 5 files changed, 54 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index da22066d7aa..58d70667cdf 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -137,6 +137,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # so we use a lock to ensure that only one API request is reaching it at a time: api_lock = asyncio.Lock() + async def async_init_coordinator( + coordinator: GuardianDataUpdateCoordinator, + ) -> None: + """Initialize a GuardianDataUpdateCoordinator.""" + await coordinator.async_initialize() + await coordinator.async_config_entry_first_refresh() + # Set up GuardianDataUpdateCoordinators for the valve controller: valve_controller_coordinators: dict[str, GuardianDataUpdateCoordinator] = {} init_valve_controller_tasks = [] @@ -151,13 +158,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api ] = GuardianDataUpdateCoordinator( hass, + entry=entry, client=client, api_name=api, api_coro=api_coro, api_lock=api_lock, valve_controller_uid=entry.data[CONF_UID], ) - init_valve_controller_tasks.append(coordinator.async_refresh()) + init_valve_controller_tasks.append(async_init_coordinator(coordinator)) await asyncio.gather(*init_valve_controller_tasks) @@ -352,6 +360,7 @@ class PairedSensorManager: coordinator = self.coordinators[uid] = GuardianDataUpdateCoordinator( self._hass, + entry=self._entry, client=self._client, api_name=f"{API_SENSOR_PAIRED_SENSOR_STATUS}_{uid}", api_coro=lambda: cast( @@ -422,7 +431,7 @@ class GuardianEntity(CoordinatorEntity[GuardianDataUpdateCoordinator]): @callback def _async_update_from_latest_data(self) -> None: - """Update the entity. + """Update the entity's underlying data. This should be extended by Guardian platforms. """ diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index eb6d49c3ec1..766e5d961e8 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -137,7 +137,7 @@ class PairedSensorBinarySensor(PairedSensorEntity, BinarySensorEntity): @callback def _async_update_from_latest_data(self) -> None: - """Update the entity.""" + """Update the entity's underlying data.""" if self.entity_description.key == SENSOR_KIND_LEAK_DETECTED: self._attr_is_on = self.coordinator.data["wet"] elif self.entity_description.key == SENSOR_KIND_MOVED: diff --git a/homeassistant/components/guardian/button.py b/homeassistant/components/guardian/button.py index 01efb7deba4..740cce43c62 100644 --- a/homeassistant/components/guardian/button.py +++ b/homeassistant/components/guardian/button.py @@ -15,6 +15,7 @@ from homeassistant.components.button import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -111,3 +112,5 @@ class GuardianButton(ValveControllerEntity, ButtonEntity): raise HomeAssistantError( f'Error while pressing button "{self.entity_id}": {err}' ) from err + + async_dispatcher_send(self.hass, self.coordinator.signal_reboot_requested) diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 5b4c621cce6..05de437b10a 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -128,7 +128,7 @@ class PairedSensorSensor(PairedSensorEntity, SensorEntity): @callback def _async_update_from_latest_data(self) -> None: - """Update the entity.""" + """Update the entity's underlying data.""" if self.entity_description.key == SENSOR_KIND_BATTERY: self._attr_native_value = self.coordinator.data["battery"] elif self.entity_description.key == SENSOR_KIND_TEMPERATURE: @@ -142,7 +142,7 @@ class ValveControllerSensor(ValveControllerEntity, SensorEntity): @callback def _async_update_from_latest_data(self) -> None: - """Update the entity.""" + """Update the entity's underlying data.""" if self.entity_description.key == SENSOR_KIND_TEMPERATURE: self._attr_native_value = self.coordinator.data["temperature"] elif self.entity_description.key == SENSOR_KIND_UPTIME: diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py index 2cedcf9c1e4..c88d6762e51 100644 --- a/homeassistant/components/guardian/util.py +++ b/homeassistant/components/guardian/util.py @@ -9,21 +9,28 @@ from typing import Any, cast from aioguardian import Client from aioguardian.errors import GuardianError -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import LOGGER DEFAULT_UPDATE_INTERVAL = timedelta(seconds=30) +SIGNAL_REBOOT_REQUESTED = "guardian_reboot_requested_{0}" + class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): """Define an extended DataUpdateCoordinator with some Guardian goodies.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, *, + entry: ConfigEntry, client: Client, api_name: str, api_coro: Callable[..., Awaitable], @@ -41,6 +48,12 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): self._api_coro = api_coro self._api_lock = api_lock self._client = client + self._signal_handler_unsubs: list[Callable[..., None]] = [] + + self.config_entry = entry + self.signal_reboot_requested = SIGNAL_REBOOT_REQUESTED.format( + self.config_entry.entry_id + ) async def _async_update_data(self) -> dict[str, Any]: """Execute a "locked" API request against the valve controller.""" @@ -50,3 +63,26 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): except GuardianError as err: raise UpdateFailed(err) from err return cast(dict[str, Any], resp["data"]) + + async def async_initialize(self) -> None: + """Initialize the coordinator.""" + + @callback + def async_reboot_requested() -> None: + """Respond to a reboot request.""" + self.last_update_success = False + self.async_update_listeners() + + self._signal_handler_unsubs.append( + async_dispatcher_connect( + self.hass, self.signal_reboot_requested, async_reboot_requested + ) + ) + + @callback + def async_teardown() -> None: + """Tear the coordinator down appropriately.""" + for unsub in self._signal_handler_unsubs: + unsub() + + self.config_entry.async_on_unload(async_teardown) From a95851c9c23124ff5d46e8a815d8e3ea5c33b845 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Jul 2022 13:13:05 -0700 Subject: [PATCH 3004/3516] Bump pySwitchbot to 0.16.0 to fix compat with bleak 0.15 (#75991) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index d6acb69431e..dcb33c03882 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.15.2"], + "requirements": ["PySwitchbot==0.16.0"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": ["@bdraco", "@danielhiversen", "@RenierM26", "@murtas"], diff --git a/requirements_all.txt b/requirements_all.txt index a5d214626ba..1d14d244148 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.15.2 +PySwitchbot==0.16.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index abf62aaccdc..3b44079a48d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.15.2 +PySwitchbot==0.16.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 4af95ecf787b72ec2a23e4354f441985a338aeef Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 31 Jul 2022 22:14:09 +0200 Subject: [PATCH 3005/3516] Fix Home Connect services not being set up (#75997) --- .../components/home_connect/__init__.py | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index a2876dd86f5..6e664ad07e4 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -117,24 +117,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Home Connect component.""" hass.data[DOMAIN] = {} - if DOMAIN not in config: - return True - - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential( - config[DOMAIN][CONF_CLIENT_ID], - config[DOMAIN][CONF_CLIENT_SECRET], - ), - ) - _LOGGER.warning( - "Configuration of Home Connect integration in YAML is deprecated and " - "will be removed in a future release; Your existing OAuth " - "Application Credentials have been imported into the UI " - "automatically and can be safely removed from your " - "configuration.yaml file" - ) + if DOMAIN in config: + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential( + config[DOMAIN][CONF_CLIENT_ID], + config[DOMAIN][CONF_CLIENT_SECRET], + ), + ) + _LOGGER.warning( + "Configuration of Home Connect integration in YAML is deprecated and " + "will be removed in a future release; Your existing OAuth " + "Application Credentials have been imported into the UI " + "automatically and can be safely removed from your " + "configuration.yaml file" + ) async def _async_service_program(call, method): """Execute calls to services taking a program.""" From 003ee853a3ce7dce7f7890d39c2c5d3586ea354a Mon Sep 17 00:00:00 2001 From: mkmer Date: Sun, 31 Jul 2022 16:14:30 -0400 Subject: [PATCH 3006/3516] Bump AIOAladdinConnect to 0.1.33 (#75986) Bump aladdin_connect 0.1.33 --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index d551b91bce9..a142e838f3e 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.31"], + "requirements": ["AIOAladdinConnect==0.1.33"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 1d14d244148..e3dfbab9e19 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.31 +AIOAladdinConnect==0.1.33 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b44079a48d..e6653d8f599 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.31 +AIOAladdinConnect==0.1.33 # homeassistant.components.adax Adax-local==0.1.4 From 240890e496a156b47eb41b8d0c35287121d11b7c Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 31 Jul 2022 14:10:29 -0600 Subject: [PATCH 3007/3516] Appropriately mark Guardian entities as `unavailable` during reboot (#75234) --- homeassistant/components/guardian/__init__.py | 13 ++++++- .../components/guardian/binary_sensor.py | 2 +- homeassistant/components/guardian/button.py | 3 ++ homeassistant/components/guardian/sensor.py | 4 +- homeassistant/components/guardian/util.py | 38 ++++++++++++++++++- 5 files changed, 54 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index da22066d7aa..58d70667cdf 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -137,6 +137,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # so we use a lock to ensure that only one API request is reaching it at a time: api_lock = asyncio.Lock() + async def async_init_coordinator( + coordinator: GuardianDataUpdateCoordinator, + ) -> None: + """Initialize a GuardianDataUpdateCoordinator.""" + await coordinator.async_initialize() + await coordinator.async_config_entry_first_refresh() + # Set up GuardianDataUpdateCoordinators for the valve controller: valve_controller_coordinators: dict[str, GuardianDataUpdateCoordinator] = {} init_valve_controller_tasks = [] @@ -151,13 +158,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api ] = GuardianDataUpdateCoordinator( hass, + entry=entry, client=client, api_name=api, api_coro=api_coro, api_lock=api_lock, valve_controller_uid=entry.data[CONF_UID], ) - init_valve_controller_tasks.append(coordinator.async_refresh()) + init_valve_controller_tasks.append(async_init_coordinator(coordinator)) await asyncio.gather(*init_valve_controller_tasks) @@ -352,6 +360,7 @@ class PairedSensorManager: coordinator = self.coordinators[uid] = GuardianDataUpdateCoordinator( self._hass, + entry=self._entry, client=self._client, api_name=f"{API_SENSOR_PAIRED_SENSOR_STATUS}_{uid}", api_coro=lambda: cast( @@ -422,7 +431,7 @@ class GuardianEntity(CoordinatorEntity[GuardianDataUpdateCoordinator]): @callback def _async_update_from_latest_data(self) -> None: - """Update the entity. + """Update the entity's underlying data. This should be extended by Guardian platforms. """ diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index eb6d49c3ec1..766e5d961e8 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -137,7 +137,7 @@ class PairedSensorBinarySensor(PairedSensorEntity, BinarySensorEntity): @callback def _async_update_from_latest_data(self) -> None: - """Update the entity.""" + """Update the entity's underlying data.""" if self.entity_description.key == SENSOR_KIND_LEAK_DETECTED: self._attr_is_on = self.coordinator.data["wet"] elif self.entity_description.key == SENSOR_KIND_MOVED: diff --git a/homeassistant/components/guardian/button.py b/homeassistant/components/guardian/button.py index 01efb7deba4..740cce43c62 100644 --- a/homeassistant/components/guardian/button.py +++ b/homeassistant/components/guardian/button.py @@ -15,6 +15,7 @@ from homeassistant.components.button import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -111,3 +112,5 @@ class GuardianButton(ValveControllerEntity, ButtonEntity): raise HomeAssistantError( f'Error while pressing button "{self.entity_id}": {err}' ) from err + + async_dispatcher_send(self.hass, self.coordinator.signal_reboot_requested) diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 5b4c621cce6..05de437b10a 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -128,7 +128,7 @@ class PairedSensorSensor(PairedSensorEntity, SensorEntity): @callback def _async_update_from_latest_data(self) -> None: - """Update the entity.""" + """Update the entity's underlying data.""" if self.entity_description.key == SENSOR_KIND_BATTERY: self._attr_native_value = self.coordinator.data["battery"] elif self.entity_description.key == SENSOR_KIND_TEMPERATURE: @@ -142,7 +142,7 @@ class ValveControllerSensor(ValveControllerEntity, SensorEntity): @callback def _async_update_from_latest_data(self) -> None: - """Update the entity.""" + """Update the entity's underlying data.""" if self.entity_description.key == SENSOR_KIND_TEMPERATURE: self._attr_native_value = self.coordinator.data["temperature"] elif self.entity_description.key == SENSOR_KIND_UPTIME: diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py index 2cedcf9c1e4..c88d6762e51 100644 --- a/homeassistant/components/guardian/util.py +++ b/homeassistant/components/guardian/util.py @@ -9,21 +9,28 @@ from typing import Any, cast from aioguardian import Client from aioguardian.errors import GuardianError -from homeassistant.core import HomeAssistant +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import LOGGER DEFAULT_UPDATE_INTERVAL = timedelta(seconds=30) +SIGNAL_REBOOT_REQUESTED = "guardian_reboot_requested_{0}" + class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): """Define an extended DataUpdateCoordinator with some Guardian goodies.""" + config_entry: ConfigEntry + def __init__( self, hass: HomeAssistant, *, + entry: ConfigEntry, client: Client, api_name: str, api_coro: Callable[..., Awaitable], @@ -41,6 +48,12 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): self._api_coro = api_coro self._api_lock = api_lock self._client = client + self._signal_handler_unsubs: list[Callable[..., None]] = [] + + self.config_entry = entry + self.signal_reboot_requested = SIGNAL_REBOOT_REQUESTED.format( + self.config_entry.entry_id + ) async def _async_update_data(self) -> dict[str, Any]: """Execute a "locked" API request against the valve controller.""" @@ -50,3 +63,26 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): except GuardianError as err: raise UpdateFailed(err) from err return cast(dict[str, Any], resp["data"]) + + async def async_initialize(self) -> None: + """Initialize the coordinator.""" + + @callback + def async_reboot_requested() -> None: + """Respond to a reboot request.""" + self.last_update_success = False + self.async_update_listeners() + + self._signal_handler_unsubs.append( + async_dispatcher_connect( + self.hass, self.signal_reboot_requested, async_reboot_requested + ) + ) + + @callback + def async_teardown() -> None: + """Tear the coordinator down appropriately.""" + for unsub in self._signal_handler_unsubs: + unsub() + + self.config_entry.async_on_unload(async_teardown) From bdb627539ec283b5625e66bb706f372168d4b3ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Jul 2022 18:33:29 -1000 Subject: [PATCH 3008/3516] Fix switchbot failing to setup when last_run_success is not saved (#75887) --- homeassistant/components/switchbot/cover.py | 9 ++++++--- homeassistant/components/switchbot/switch.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index dc9ddf4e616..0ae225f55d7 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -80,9 +80,12 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): if not last_state or ATTR_CURRENT_POSITION not in last_state.attributes: return - self._attr_current_cover_position = last_state.attributes[ATTR_CURRENT_POSITION] - self._last_run_success = last_state.attributes["last_run_success"] - self._attr_is_closed = last_state.attributes[ATTR_CURRENT_POSITION] <= 20 + self._attr_current_cover_position = last_state.attributes.get( + ATTR_CURRENT_POSITION + ) + self._last_run_success = last_state.attributes.get("last_run_success") + if self._attr_current_cover_position is not None: + self._attr_is_closed = self._attr_current_cover_position <= 20 async def async_open_cover(self, **kwargs: Any) -> None: """Open the curtain.""" diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 51f15c488d1..e6ba77fa164 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -69,7 +69,7 @@ class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): if not (last_state := await self.async_get_last_state()): return self._attr_is_on = last_state.state == STATE_ON - self._last_run_success = last_state.attributes["last_run_success"] + self._last_run_success = last_state.attributes.get("last_run_success") async def async_turn_on(self, **kwargs: Any) -> None: """Turn device on.""" From a3276e00b963542eebf4590d7c91c7c1defb52fa Mon Sep 17 00:00:00 2001 From: Heine Furubotten Date: Sun, 31 Jul 2022 13:28:09 +0200 Subject: [PATCH 3009/3516] Bump enturclient to 0.2.4 (#75928) --- homeassistant/components/entur_public_transport/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/entur_public_transport/manifest.json b/homeassistant/components/entur_public_transport/manifest.json index c7f4fbeef53..3bbacb8c3d4 100644 --- a/homeassistant/components/entur_public_transport/manifest.json +++ b/homeassistant/components/entur_public_transport/manifest.json @@ -2,7 +2,7 @@ "domain": "entur_public_transport", "name": "Entur", "documentation": "https://www.home-assistant.io/integrations/entur_public_transport", - "requirements": ["enturclient==0.2.3"], + "requirements": ["enturclient==0.2.4"], "codeowners": ["@hfurubotten"], "iot_class": "cloud_polling", "loggers": ["enturclient"] diff --git a/requirements_all.txt b/requirements_all.txt index 58f2e5f73ec..2ad42cf511c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -606,7 +606,7 @@ emulated_roku==0.2.1 enocean==0.50 # homeassistant.components.entur_public_transport -enturclient==0.2.3 +enturclient==0.2.4 # homeassistant.components.environment_canada env_canada==0.5.22 From d84bc20a58233b903bf25afc14ccea88f015b161 Mon Sep 17 00:00:00 2001 From: MasonCrawford Date: Sun, 31 Jul 2022 19:32:40 +0800 Subject: [PATCH 3010/3516] Small fixes for LG soundbar (#75938) --- homeassistant/components/lg_soundbar/media_player.py | 3 --- homeassistant/components/lg_soundbar/strings.json | 3 +-- homeassistant/components/lg_soundbar/translations/en.json | 3 +-- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index f8f6fcf26fd..941042d5bce 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -47,7 +47,6 @@ class LGDevice(MediaPlayerEntity): self._port = port self._attr_unique_id = unique_id - self._name = None self._volume = 0 self._volume_min = 0 self._volume_max = 0 @@ -94,8 +93,6 @@ class LGDevice(MediaPlayerEntity): elif response["msg"] == "SPK_LIST_VIEW_INFO": if "i_vol" in data: self._volume = data["i_vol"] - if "s_user_name" in data: - self._name = data["s_user_name"] if "i_vol_min" in data: self._volume_min = data["i_vol_min"] if "i_vol_max" in data: diff --git a/homeassistant/components/lg_soundbar/strings.json b/homeassistant/components/lg_soundbar/strings.json index ef7bf32a051..52d57eda809 100644 --- a/homeassistant/components/lg_soundbar/strings.json +++ b/homeassistant/components/lg_soundbar/strings.json @@ -11,8 +11,7 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { - "existing_instance_updated": "Updated existing configuration.", - "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } } diff --git a/homeassistant/components/lg_soundbar/translations/en.json b/homeassistant/components/lg_soundbar/translations/en.json index a646279203f..10441d21536 100644 --- a/homeassistant/components/lg_soundbar/translations/en.json +++ b/homeassistant/components/lg_soundbar/translations/en.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Service is already configured", - "existing_instance_updated": "Updated existing configuration." + "already_configured": "Device is already configured" }, "error": { "cannot_connect": "Failed to connect" From 38ae2f4e9e4fdcf02d10a6ab4a12289a77b48af4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Jul 2022 06:15:06 -0700 Subject: [PATCH 3011/3516] Bump govee-ble to fix H5179 sensors (#75957) Changelog: https://github.com/Bluetooth-Devices/govee-ble/compare/v0.12.4...v0.12.5 --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 270858d04d4..c7909d3e1af 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -24,7 +24,7 @@ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.12.4"], + "requirements": ["govee-ble==0.12.5"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 2ad42cf511c..0ce0cb1529b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.4 +govee-ble==0.12.5 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7358e148dc..e619216cc0b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,7 +561,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.4 +govee-ble==0.12.5 # homeassistant.components.gree greeclimate==1.2.0 From d205fb5064f01681a768afa4c1e186accc091660 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sun, 31 Jul 2022 12:21:25 +0200 Subject: [PATCH 3012/3516] Handle failed connection attempts in opentherm_gw (#75961) --- .../components/opentherm_gw/__init__.py | 18 ++++++++++++++++-- .../components/opentherm_gw/config_flow.py | 15 +++++++++++---- homeassistant/components/opentherm_gw/const.py | 2 ++ .../components/opentherm_gw/strings.json | 3 ++- .../opentherm_gw/test_config_flow.py | 2 +- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 7c27eeceede..cdf360c8795 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -1,9 +1,11 @@ """Support for OpenTherm Gateway devices.""" +import asyncio from datetime import date, datetime import logging import pyotgw import pyotgw.vars as gw_vars +from serial import SerialException import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -23,6 +25,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType @@ -37,6 +40,7 @@ from .const import ( CONF_PRECISION, CONF_READ_PRECISION, CONF_SET_PRECISION, + CONNECTION_TIMEOUT, DATA_GATEWAYS, DATA_OPENTHERM_GW, DOMAIN, @@ -107,8 +111,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b config_entry.add_update_listener(options_updated) - # Schedule directly on the loop to avoid blocking HA startup. - hass.loop.create_task(gateway.connect_and_subscribe()) + try: + await asyncio.wait_for( + gateway.connect_and_subscribe(), + timeout=CONNECTION_TIMEOUT, + ) + except (asyncio.TimeoutError, ConnectionError, SerialException) as ex: + raise ConfigEntryNotReady( + f"Could not connect to gateway at {gateway.device_path}: {ex}" + ) from ex await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -428,6 +439,9 @@ class OpenThermGatewayDevice: async def connect_and_subscribe(self): """Connect to serial device and subscribe report handler.""" self.status = await self.gateway.connect(self.device_path) + if not self.status: + await self.cleanup() + raise ConnectionError version_string = self.status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) self.gw_version = version_string[18:] if version_string else None _LOGGER.debug( diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 3f91496adab..c3a955b2387 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -26,6 +26,7 @@ from .const import ( CONF_READ_PRECISION, CONF_SET_PRECISION, CONF_TEMPORARY_OVRD_MODE, + CONNECTION_TIMEOUT, ) @@ -62,15 +63,21 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): otgw = pyotgw.OpenThermGateway() status = await otgw.connect(device) await otgw.disconnect() + if not status: + raise ConnectionError return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) try: - res = await asyncio.wait_for(test_connection(), timeout=10) - except (asyncio.TimeoutError, SerialException): + await asyncio.wait_for( + test_connection(), + timeout=CONNECTION_TIMEOUT, + ) + except asyncio.TimeoutError: + return self._show_form({"base": "timeout_connect"}) + except (ConnectionError, SerialException): return self._show_form({"base": "cannot_connect"}) - if res: - return self._create_entry(gw_id, name, device) + return self._create_entry(gw_id, name, device) return self._show_form() diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index a5042628529..d72469759f1 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -25,6 +25,8 @@ CONF_READ_PRECISION = "read_precision" CONF_SET_PRECISION = "set_precision" CONF_TEMPORARY_OVRD_MODE = "temporary_override_mode" +CONNECTION_TIMEOUT = 10 + DATA_GATEWAYS = "gateways" DATA_OPENTHERM_GW = "opentherm_gw" diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json index f53ffeda6f6..a80a059481d 100644 --- a/homeassistant/components/opentherm_gw/strings.json +++ b/homeassistant/components/opentherm_gw/strings.json @@ -12,7 +12,8 @@ "error": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "id_exists": "Gateway id already exists", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" } }, "options": { diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index 46d53bc54b5..080e9a96d58 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -164,7 +164,7 @@ async def test_form_connection_timeout(hass): ) assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} + assert result2["errors"] == {"base": "timeout_connect"} assert len(mock_connect.mock_calls) == 1 From 58265664d1848f4e78cc745cc9e59d9e6b563c44 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 31 Jul 2022 18:00:42 +0200 Subject: [PATCH 3013/3516] Improve authentication handling for camera view (#75979) --- homeassistant/components/camera/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 3bf86dedea1..77bd0b57f1c 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -14,7 +14,7 @@ import os from random import SystemRandom from typing import Final, Optional, cast, final -from aiohttp import web +from aiohttp import hdrs, web import async_timeout import attr import voluptuous as vol @@ -715,8 +715,11 @@ class CameraView(HomeAssistantView): ) if not authenticated: - if request[KEY_AUTHENTICATED]: + # Attempt with invalid bearer token, raise unauthorized + # so ban middleware can handle it. + if hdrs.AUTHORIZATION in request.headers: raise web.HTTPUnauthorized() + # Invalid sigAuth or camera access token raise web.HTTPForbidden() if not camera.is_on: From 26a3621bb3eda49cebffbb7e82fa5e952c1affbe Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sun, 31 Jul 2022 21:14:03 +0200 Subject: [PATCH 3014/3516] Bump pyotgw to 2.0.2 (#75980) --- homeassistant/components/opentherm_gw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 0bc69387d0b..02b1604ea11 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -2,7 +2,7 @@ "domain": "opentherm_gw", "name": "OpenTherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", - "requirements": ["pyotgw==2.0.1"], + "requirements": ["pyotgw==2.0.2"], "codeowners": ["@mvn23"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 0ce0cb1529b..75f6000bd3c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1725,7 +1725,7 @@ pyopnsense==0.2.0 pyoppleio==1.0.5 # homeassistant.components.opentherm_gw -pyotgw==2.0.1 +pyotgw==2.0.2 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e619216cc0b..495c91e52ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1190,7 +1190,7 @@ pyopenuv==2022.04.0 pyopnsense==0.2.0 # homeassistant.components.opentherm_gw -pyotgw==2.0.1 +pyotgw==2.0.2 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp From e330147751d6305348fd4939692f52931f4dacb1 Mon Sep 17 00:00:00 2001 From: mkmer Date: Sun, 31 Jul 2022 16:14:30 -0400 Subject: [PATCH 3015/3516] Bump AIOAladdinConnect to 0.1.33 (#75986) Bump aladdin_connect 0.1.33 --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index d551b91bce9..a142e838f3e 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.31"], + "requirements": ["AIOAladdinConnect==0.1.33"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 75f6000bd3c..fc5b29770d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.31 +AIOAladdinConnect==0.1.33 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 495c91e52ab..06b8d92a551 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.31 +AIOAladdinConnect==0.1.33 # homeassistant.components.adax Adax-local==0.1.4 From ebf91fe46b143a2abbfdca7c392deb85c26f56ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Jul 2022 13:13:05 -0700 Subject: [PATCH 3016/3516] Bump pySwitchbot to 0.16.0 to fix compat with bleak 0.15 (#75991) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index d6acb69431e..dcb33c03882 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.15.2"], + "requirements": ["PySwitchbot==0.16.0"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": ["@bdraco", "@danielhiversen", "@RenierM26", "@murtas"], diff --git a/requirements_all.txt b/requirements_all.txt index fc5b29770d2..aeaf75b987e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.15.2 +PySwitchbot==0.16.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 06b8d92a551..03cf9488f37 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.15.2 +PySwitchbot==0.16.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From ffd2813150c9375c6136ff04aac7bdf5776fe3c4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 31 Jul 2022 22:14:09 +0200 Subject: [PATCH 3017/3516] Fix Home Connect services not being set up (#75997) --- .../components/home_connect/__init__.py | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index a2876dd86f5..6e664ad07e4 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -117,24 +117,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Home Connect component.""" hass.data[DOMAIN] = {} - if DOMAIN not in config: - return True - - await async_import_client_credential( - hass, - DOMAIN, - ClientCredential( - config[DOMAIN][CONF_CLIENT_ID], - config[DOMAIN][CONF_CLIENT_SECRET], - ), - ) - _LOGGER.warning( - "Configuration of Home Connect integration in YAML is deprecated and " - "will be removed in a future release; Your existing OAuth " - "Application Credentials have been imported into the UI " - "automatically and can be safely removed from your " - "configuration.yaml file" - ) + if DOMAIN in config: + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential( + config[DOMAIN][CONF_CLIENT_ID], + config[DOMAIN][CONF_CLIENT_SECRET], + ), + ) + _LOGGER.warning( + "Configuration of Home Connect integration in YAML is deprecated and " + "will be removed in a future release; Your existing OAuth " + "Application Credentials have been imported into the UI " + "automatically and can be safely removed from your " + "configuration.yaml file" + ) async def _async_service_program(call, method): """Execute calls to services taking a program.""" From 5ab549653ba9ddc9a3b18000dbc772e4f154b624 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 31 Jul 2022 13:29:52 -0700 Subject: [PATCH 3018/3516] Bumped version to 2022.8.0b4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4a37902e79f..9ef45865665 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 8fca49a67f2..284585734dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0b3" +version = "2022.8.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 0167875789cac40a15b53d52b3b14165caaccf06 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sun, 31 Jul 2022 21:30:29 +0100 Subject: [PATCH 3019/3516] Add physical controls lock to homekit_controller (#75993) --- homeassistant/components/homekit_controller/switch.py | 6 ++++++ .../homekit_controller/specific_devices/test_eve_energy.py | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 53b3958ecf6..be6c3b8bfe0 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -50,6 +50,12 @@ SWITCH_ENTITIES: dict[str, DeclarativeSwitchEntityDescription] = { icon="mdi:lock-open", entity_category=EntityCategory.CONFIG, ), + CharacteristicsTypes.LOCK_PHYSICAL_CONTROLS: DeclarativeSwitchEntityDescription( + key=CharacteristicsTypes.LOCK_PHYSICAL_CONTROLS, + name="Lock Physical Controls", + icon="mdi:lock-open", + entity_category=EntityCategory.CONFIG, + ), } diff --git a/tests/components/homekit_controller/specific_devices/test_eve_energy.py b/tests/components/homekit_controller/specific_devices/test_eve_energy.py index 0ba9b0bee25..70ae4a1db23 100644 --- a/tests/components/homekit_controller/specific_devices/test_eve_energy.py +++ b/tests/components/homekit_controller/specific_devices/test_eve_energy.py @@ -74,6 +74,13 @@ async def test_eve_degree_setup(hass): unit_of_measurement=ENERGY_KILO_WATT_HOUR, state="0.28999999165535", ), + EntityTestInfo( + entity_id="switch.eve_energy_50ff_lock_physical_controls", + unique_id="homekit-AA00A0A00000-aid:1-sid:28-cid:36", + friendly_name="Eve Energy 50FF Lock Physical Controls", + entity_category=EntityCategory.CONFIG, + state="off", + ), EntityTestInfo( entity_id="button.eve_energy_50ff_identify", unique_id="homekit-AA00A0A00000-aid:1-sid:1-cid:3", From 89729b2c495ff3310e255c3c975ba9211f6b1bff Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Aug 2022 00:39:38 +0200 Subject: [PATCH 3020/3516] Improve Registry typing in Alexa handlers (#75921) --- homeassistant/components/alexa/handlers.py | 440 ++++++++++++++++----- 1 file changed, 342 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index f9162026fe8..ba3892a62f2 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -1,6 +1,10 @@ """Alexa message handlers.""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine import logging import math +from typing import Any from homeassistant import core as ha from homeassistant.components import ( @@ -51,6 +55,7 @@ from homeassistant.util.decorator import Registry import homeassistant.util.dt as dt_util from homeassistant.util.temperature import convert as convert_temperature +from .config import AbstractConfig from .const import ( API_TEMP_UNITS, API_THERMOSTAT_MODES, @@ -70,14 +75,27 @@ from .errors import ( AlexaUnsupportedThermostatModeError, AlexaVideoActionNotPermittedForContentError, ) +from .messages import AlexaDirective, AlexaResponse from .state_report import async_enable_proactive_mode _LOGGER = logging.getLogger(__name__) -HANDLERS = Registry() # type: ignore[var-annotated] +DIRECTIVE_NOT_SUPPORTED = "Entity does not support directive" +HANDLERS: Registry[ + tuple[str, str], + Callable[ + [ha.HomeAssistant, AbstractConfig, AlexaDirective, ha.Context], + Coroutine[Any, Any, AlexaResponse], + ], +] = Registry() @HANDLERS.register(("Alexa.Discovery", "Discover")) -async def async_api_discovery(hass, config, directive, context): +async def async_api_discovery( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Create a API formatted discovery response. Async friendly. @@ -96,7 +114,12 @@ async def async_api_discovery(hass, config, directive, context): @HANDLERS.register(("Alexa.Authorization", "AcceptGrant")) -async def async_api_accept_grant(hass, config, directive, context): +async def async_api_accept_grant( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Create a API formatted AcceptGrant response. Async friendly. @@ -116,7 +139,12 @@ async def async_api_accept_grant(hass, config, directive, context): @HANDLERS.register(("Alexa.PowerController", "TurnOn")) -async def async_api_turn_on(hass, config, directive, context): +async def async_api_turn_on( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a turn on request.""" entity = directive.entity if (domain := entity.domain) == group.DOMAIN: @@ -157,7 +185,12 @@ async def async_api_turn_on(hass, config, directive, context): @HANDLERS.register(("Alexa.PowerController", "TurnOff")) -async def async_api_turn_off(hass, config, directive, context): +async def async_api_turn_off( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a turn off request.""" entity = directive.entity domain = entity.domain @@ -199,7 +232,12 @@ async def async_api_turn_off(hass, config, directive, context): @HANDLERS.register(("Alexa.BrightnessController", "SetBrightness")) -async def async_api_set_brightness(hass, config, directive, context): +async def async_api_set_brightness( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a set brightness request.""" entity = directive.entity brightness = int(directive.payload["brightness"]) @@ -216,7 +254,12 @@ async def async_api_set_brightness(hass, config, directive, context): @HANDLERS.register(("Alexa.BrightnessController", "AdjustBrightness")) -async def async_api_adjust_brightness(hass, config, directive, context): +async def async_api_adjust_brightness( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process an adjust brightness request.""" entity = directive.entity brightness_delta = int(directive.payload["brightnessDelta"]) @@ -237,7 +280,12 @@ async def async_api_adjust_brightness(hass, config, directive, context): @HANDLERS.register(("Alexa.ColorController", "SetColor")) -async def async_api_set_color(hass, config, directive, context): +async def async_api_set_color( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a set color request.""" entity = directive.entity rgb = color_util.color_hsb_to_RGB( @@ -258,7 +306,12 @@ async def async_api_set_color(hass, config, directive, context): @HANDLERS.register(("Alexa.ColorTemperatureController", "SetColorTemperature")) -async def async_api_set_color_temperature(hass, config, directive, context): +async def async_api_set_color_temperature( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a set color temperature request.""" entity = directive.entity kelvin = int(directive.payload["colorTemperatureInKelvin"]) @@ -275,7 +328,12 @@ async def async_api_set_color_temperature(hass, config, directive, context): @HANDLERS.register(("Alexa.ColorTemperatureController", "DecreaseColorTemperature")) -async def async_api_decrease_color_temp(hass, config, directive, context): +async def async_api_decrease_color_temp( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a decrease color temperature request.""" entity = directive.entity current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) @@ -294,7 +352,12 @@ async def async_api_decrease_color_temp(hass, config, directive, context): @HANDLERS.register(("Alexa.ColorTemperatureController", "IncreaseColorTemperature")) -async def async_api_increase_color_temp(hass, config, directive, context): +async def async_api_increase_color_temp( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process an increase color temperature request.""" entity = directive.entity current = int(entity.attributes.get(light.ATTR_COLOR_TEMP)) @@ -313,7 +376,12 @@ async def async_api_increase_color_temp(hass, config, directive, context): @HANDLERS.register(("Alexa.SceneController", "Activate")) -async def async_api_activate(hass, config, directive, context): +async def async_api_activate( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process an activate request.""" entity = directive.entity domain = entity.domain @@ -343,7 +411,12 @@ async def async_api_activate(hass, config, directive, context): @HANDLERS.register(("Alexa.SceneController", "Deactivate")) -async def async_api_deactivate(hass, config, directive, context): +async def async_api_deactivate( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a deactivate request.""" entity = directive.entity domain = entity.domain @@ -367,16 +440,24 @@ async def async_api_deactivate(hass, config, directive, context): @HANDLERS.register(("Alexa.PercentageController", "SetPercentage")) -async def async_api_set_percentage(hass, config, directive, context): +async def async_api_set_percentage( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a set percentage request.""" entity = directive.entity - service = None - data = {ATTR_ENTITY_ID: entity.entity_id} - if entity.domain == fan.DOMAIN: - service = fan.SERVICE_SET_PERCENTAGE - percentage = int(directive.payload["percentage"]) - data[fan.ATTR_PERCENTAGE] = percentage + if entity.domain != fan.DOMAIN: + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) + + percentage = int(directive.payload["percentage"]) + service = fan.SERVICE_SET_PERCENTAGE + data = { + ATTR_ENTITY_ID: entity.entity_id, + fan.ATTR_PERCENTAGE: percentage, + } await hass.services.async_call( entity.domain, service, data, blocking=False, context=context @@ -386,20 +467,27 @@ async def async_api_set_percentage(hass, config, directive, context): @HANDLERS.register(("Alexa.PercentageController", "AdjustPercentage")) -async def async_api_adjust_percentage(hass, config, directive, context): +async def async_api_adjust_percentage( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process an adjust percentage request.""" entity = directive.entity + + if entity.domain != fan.DOMAIN: + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) + percentage_delta = int(directive.payload["percentageDelta"]) - service = None - data = {ATTR_ENTITY_ID: entity.entity_id} - - if entity.domain == fan.DOMAIN: - service = fan.SERVICE_SET_PERCENTAGE - current = entity.attributes.get(fan.ATTR_PERCENTAGE) or 0 - - # set percentage - percentage = min(100, max(0, percentage_delta + current)) - data[fan.ATTR_PERCENTAGE] = percentage + current = entity.attributes.get(fan.ATTR_PERCENTAGE) or 0 + # set percentage + percentage = min(100, max(0, percentage_delta + current)) + service = fan.SERVICE_SET_PERCENTAGE + data = { + ATTR_ENTITY_ID: entity.entity_id, + fan.ATTR_PERCENTAGE: percentage, + } await hass.services.async_call( entity.domain, service, data, blocking=False, context=context @@ -409,7 +497,12 @@ async def async_api_adjust_percentage(hass, config, directive, context): @HANDLERS.register(("Alexa.LockController", "Lock")) -async def async_api_lock(hass, config, directive, context): +async def async_api_lock( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a lock request.""" entity = directive.entity await hass.services.async_call( @@ -428,7 +521,12 @@ async def async_api_lock(hass, config, directive, context): @HANDLERS.register(("Alexa.LockController", "Unlock")) -async def async_api_unlock(hass, config, directive, context): +async def async_api_unlock( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process an unlock request.""" if config.locale not in {"de-DE", "en-US", "ja-JP"}: msg = f"The unlock directive is not supported for the following locales: {config.locale}" @@ -452,7 +550,12 @@ async def async_api_unlock(hass, config, directive, context): @HANDLERS.register(("Alexa.Speaker", "SetVolume")) -async def async_api_set_volume(hass, config, directive, context): +async def async_api_set_volume( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a set volume request.""" volume = round(float(directive.payload["volume"] / 100), 2) entity = directive.entity @@ -470,7 +573,12 @@ async def async_api_set_volume(hass, config, directive, context): @HANDLERS.register(("Alexa.InputController", "SelectInput")) -async def async_api_select_input(hass, config, directive, context): +async def async_api_select_input( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a set input request.""" media_input = directive.payload["input"] entity = directive.entity @@ -514,7 +622,12 @@ async def async_api_select_input(hass, config, directive, context): @HANDLERS.register(("Alexa.Speaker", "AdjustVolume")) -async def async_api_adjust_volume(hass, config, directive, context): +async def async_api_adjust_volume( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process an adjust volume request.""" volume_delta = int(directive.payload["volume"]) @@ -542,7 +655,12 @@ async def async_api_adjust_volume(hass, config, directive, context): @HANDLERS.register(("Alexa.StepSpeaker", "AdjustVolume")) -async def async_api_adjust_volume_step(hass, config, directive, context): +async def async_api_adjust_volume_step( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process an adjust volume step request.""" # media_player volume up/down service does not support specifying steps # each component handles it differently e.g. via config. @@ -575,7 +693,12 @@ async def async_api_adjust_volume_step(hass, config, directive, context): @HANDLERS.register(("Alexa.StepSpeaker", "SetMute")) @HANDLERS.register(("Alexa.Speaker", "SetMute")) -async def async_api_set_mute(hass, config, directive, context): +async def async_api_set_mute( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a set mute request.""" mute = bool(directive.payload["mute"]) entity = directive.entity @@ -592,7 +715,12 @@ async def async_api_set_mute(hass, config, directive, context): @HANDLERS.register(("Alexa.PlaybackController", "Play")) -async def async_api_play(hass, config, directive, context): +async def async_api_play( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a play request.""" entity = directive.entity data = {ATTR_ENTITY_ID: entity.entity_id} @@ -605,7 +733,12 @@ async def async_api_play(hass, config, directive, context): @HANDLERS.register(("Alexa.PlaybackController", "Pause")) -async def async_api_pause(hass, config, directive, context): +async def async_api_pause( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a pause request.""" entity = directive.entity data = {ATTR_ENTITY_ID: entity.entity_id} @@ -618,7 +751,12 @@ async def async_api_pause(hass, config, directive, context): @HANDLERS.register(("Alexa.PlaybackController", "Stop")) -async def async_api_stop(hass, config, directive, context): +async def async_api_stop( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a stop request.""" entity = directive.entity data = {ATTR_ENTITY_ID: entity.entity_id} @@ -631,7 +769,12 @@ async def async_api_stop(hass, config, directive, context): @HANDLERS.register(("Alexa.PlaybackController", "Next")) -async def async_api_next(hass, config, directive, context): +async def async_api_next( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a next request.""" entity = directive.entity data = {ATTR_ENTITY_ID: entity.entity_id} @@ -644,7 +787,12 @@ async def async_api_next(hass, config, directive, context): @HANDLERS.register(("Alexa.PlaybackController", "Previous")) -async def async_api_previous(hass, config, directive, context): +async def async_api_previous( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a previous request.""" entity = directive.entity data = {ATTR_ENTITY_ID: entity.entity_id} @@ -676,7 +824,12 @@ def temperature_from_object(hass, temp_obj, interval=False): @HANDLERS.register(("Alexa.ThermostatController", "SetTargetTemperature")) -async def async_api_set_target_temp(hass, config, directive, context): +async def async_api_set_target_temp( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a set target temperature request.""" entity = directive.entity min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP) @@ -736,7 +889,12 @@ async def async_api_set_target_temp(hass, config, directive, context): @HANDLERS.register(("Alexa.ThermostatController", "AdjustTargetTemperature")) -async def async_api_adjust_target_temp(hass, config, directive, context): +async def async_api_adjust_target_temp( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process an adjust target temperature request.""" entity = directive.entity min_temp = entity.attributes.get(climate.ATTR_MIN_TEMP) @@ -773,7 +931,12 @@ async def async_api_adjust_target_temp(hass, config, directive, context): @HANDLERS.register(("Alexa.ThermostatController", "SetThermostatMode")) -async def async_api_set_thermostat_mode(hass, config, directive, context): +async def async_api_set_thermostat_mode( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a set thermostat mode request.""" entity = directive.entity mode = directive.payload["thermostatMode"] @@ -836,13 +999,23 @@ async def async_api_set_thermostat_mode(hass, config, directive, context): @HANDLERS.register(("Alexa", "ReportState")) -async def async_api_reportstate(hass, config, directive, context): +async def async_api_reportstate( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a ReportState request.""" return directive.response(name="StateReport") @HANDLERS.register(("Alexa.SecurityPanelController", "Arm")) -async def async_api_arm(hass, config, directive, context): +async def async_api_arm( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a Security Panel Arm request.""" entity = directive.entity service = None @@ -859,6 +1032,8 @@ async def async_api_arm(hass, config, directive, context): service = SERVICE_ALARM_ARM_NIGHT elif arm_state == "ARMED_STAY": service = SERVICE_ALARM_ARM_HOME + else: + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) await hass.services.async_call( entity.domain, service, data, blocking=False, context=context @@ -883,7 +1058,12 @@ async def async_api_arm(hass, config, directive, context): @HANDLERS.register(("Alexa.SecurityPanelController", "Disarm")) -async def async_api_disarm(hass, config, directive, context): +async def async_api_disarm( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a Security Panel Disarm request.""" entity = directive.entity data = {ATTR_ENTITY_ID: entity.entity_id} @@ -916,7 +1096,12 @@ async def async_api_disarm(hass, config, directive, context): @HANDLERS.register(("Alexa.ModeController", "SetMode")) -async def async_api_set_mode(hass, config, directive, context): +async def async_api_set_mode( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a SetMode directive.""" entity = directive.entity instance = directive.instance @@ -955,9 +1140,8 @@ async def async_api_set_mode(hass, config, directive, context): elif position == "custom": service = cover.SERVICE_STOP_COVER - else: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + if not service: + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) await hass.services.async_call( domain, service, data, blocking=False, context=context @@ -977,7 +1161,12 @@ async def async_api_set_mode(hass, config, directive, context): @HANDLERS.register(("Alexa.ModeController", "AdjustMode")) -async def async_api_adjust_mode(hass, config, directive, context): +async def async_api_adjust_mode( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a AdjustMode request. Requires capabilityResources supportedModes to be ordered. @@ -985,26 +1174,30 @@ async def async_api_adjust_mode(hass, config, directive, context): """ # Currently no supportedModes are configured with ordered=True to support this request. - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) @HANDLERS.register(("Alexa.ToggleController", "TurnOn")) -async def async_api_toggle_on(hass, config, directive, context): +async def async_api_toggle_on( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a toggle on request.""" entity = directive.entity instance = directive.instance domain = entity.domain - service = None - data = {ATTR_ENTITY_ID: entity.entity_id} # Fan Oscillating - if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": - service = fan.SERVICE_OSCILLATE - data[fan.ATTR_OSCILLATING] = True - else: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + if instance != f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) + + service = fan.SERVICE_OSCILLATE + data = { + ATTR_ENTITY_ID: entity.entity_id, + fan.ATTR_OSCILLATING: True, + } await hass.services.async_call( domain, service, data, blocking=False, context=context @@ -1024,21 +1217,26 @@ async def async_api_toggle_on(hass, config, directive, context): @HANDLERS.register(("Alexa.ToggleController", "TurnOff")) -async def async_api_toggle_off(hass, config, directive, context): +async def async_api_toggle_off( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a toggle off request.""" entity = directive.entity instance = directive.instance domain = entity.domain - service = None - data = {ATTR_ENTITY_ID: entity.entity_id} # Fan Oscillating - if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": - service = fan.SERVICE_OSCILLATE - data[fan.ATTR_OSCILLATING] = False - else: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + if instance != f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) + + service = fan.SERVICE_OSCILLATE + data = { + ATTR_ENTITY_ID: entity.entity_id, + fan.ATTR_OSCILLATING: False, + } await hass.services.async_call( domain, service, data, blocking=False, context=context @@ -1058,7 +1256,12 @@ async def async_api_toggle_off(hass, config, directive, context): @HANDLERS.register(("Alexa.RangeController", "SetRangeValue")) -async def async_api_set_range(hass, config, directive, context): +async def async_api_set_range( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a next request.""" entity = directive.entity instance = directive.instance @@ -1125,8 +1328,7 @@ async def async_api_set_range(hass, config, directive, context): data[vacuum.ATTR_FAN_SPEED] = speed else: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) await hass.services.async_call( domain, service, data, blocking=False, context=context @@ -1146,16 +1348,21 @@ async def async_api_set_range(hass, config, directive, context): @HANDLERS.register(("Alexa.RangeController", "AdjustRangeValue")) -async def async_api_adjust_range(hass, config, directive, context): +async def async_api_adjust_range( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a next request.""" entity = directive.entity instance = directive.instance domain = entity.domain service = None - data = {ATTR_ENTITY_ID: entity.entity_id} + data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} range_delta = directive.payload["rangeValueDelta"] range_delta_default = bool(directive.payload["rangeValueDeltaDefault"]) - response_value = 0 + response_value: int | None = 0 # Cover Position if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": @@ -1232,12 +1439,10 @@ async def async_api_adjust_range(hass, config, directive, context): speed = next( (v for i, v in enumerate(speed_list) if i == new_speed_index), None ) - data[vacuum.ATTR_FAN_SPEED] = response_value = speed else: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) await hass.services.async_call( domain, service, data, blocking=False, context=context @@ -1257,7 +1462,12 @@ async def async_api_adjust_range(hass, config, directive, context): @HANDLERS.register(("Alexa.ChannelController", "ChangeChannel")) -async def async_api_changechannel(hass, config, directive, context): +async def async_api_changechannel( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a change channel request.""" channel = "0" entity = directive.entity @@ -1309,7 +1519,12 @@ async def async_api_changechannel(hass, config, directive, context): @HANDLERS.register(("Alexa.ChannelController", "SkipChannels")) -async def async_api_skipchannel(hass, config, directive, context): +async def async_api_skipchannel( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a skipchannel request.""" channel = int(directive.payload["channelCount"]) entity = directive.entity @@ -1340,7 +1555,12 @@ async def async_api_skipchannel(hass, config, directive, context): @HANDLERS.register(("Alexa.SeekController", "AdjustSeekPosition")) -async def async_api_seek(hass, config, directive, context): +async def async_api_seek( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a seek request.""" entity = directive.entity position_delta = int(directive.payload["deltaPositionMilliseconds"]) @@ -1379,7 +1599,12 @@ async def async_api_seek(hass, config, directive, context): @HANDLERS.register(("Alexa.EqualizerController", "SetMode")) -async def async_api_set_eq_mode(hass, config, directive, context): +async def async_api_set_eq_mode( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a SetMode request for EqualizerController.""" mode = directive.payload["mode"] entity = directive.entity @@ -1406,18 +1631,27 @@ async def async_api_set_eq_mode(hass, config, directive, context): @HANDLERS.register(("Alexa.EqualizerController", "AdjustBands")) @HANDLERS.register(("Alexa.EqualizerController", "ResetBands")) @HANDLERS.register(("Alexa.EqualizerController", "SetBands")) -async def async_api_bands_directive(hass, config, directive, context): +async def async_api_bands_directive( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Handle an AdjustBands, ResetBands, SetBands request. Only mode directives are currently supported for the EqualizerController. """ # Currently bands directives are not supported. - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) @HANDLERS.register(("Alexa.TimeHoldController", "Hold")) -async def async_api_hold(hass, config, directive, context): +async def async_api_hold( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a TimeHoldController Hold request.""" entity = directive.entity data = {ATTR_ENTITY_ID: entity.entity_id} @@ -1429,8 +1663,7 @@ async def async_api_hold(hass, config, directive, context): service = vacuum.SERVICE_START_PAUSE else: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) await hass.services.async_call( entity.domain, service, data, blocking=False, context=context @@ -1440,7 +1673,12 @@ async def async_api_hold(hass, config, directive, context): @HANDLERS.register(("Alexa.TimeHoldController", "Resume")) -async def async_api_resume(hass, config, directive, context): +async def async_api_resume( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a TimeHoldController Resume request.""" entity = directive.entity data = {ATTR_ENTITY_ID: entity.entity_id} @@ -1452,8 +1690,7 @@ async def async_api_resume(hass, config, directive, context): service = vacuum.SERVICE_START_PAUSE else: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) await hass.services.async_call( entity.domain, service, data, blocking=False, context=context @@ -1463,11 +1700,18 @@ async def async_api_resume(hass, config, directive, context): @HANDLERS.register(("Alexa.CameraStreamController", "InitializeCameraStreams")) -async def async_api_initialize_camera_stream(hass, config, directive, context): +async def async_api_initialize_camera_stream( + hass: ha.HomeAssistant, + config: AbstractConfig, + directive: AlexaDirective, + context: ha.Context, +) -> AlexaResponse: """Process a InitializeCameraStreams request.""" entity = directive.entity stream_source = await camera.async_request_stream(hass, entity.entity_id, fmt="hls") - camera_image = hass.states.get(entity.entity_id).attributes[ATTR_ENTITY_PICTURE] + state = hass.states.get(entity.entity_id) + assert state + camera_image = state.attributes[ATTR_ENTITY_PICTURE] try: external_url = network.get_url( From 5bb59206971261e138eb89837068aa4f4a49b78e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 1 Aug 2022 00:28:32 +0000 Subject: [PATCH 3021/3516] [ci skip] Translation update --- .../components/ambee/translations/fr.json | 5 +++++ .../components/bluetooth/translations/hu.json | 12 ++++++++++- .../homeassistant_alerts/translations/fr.json | 8 ++++++++ .../lacrosse_view/translations/fr.json | 20 +++++++++++++++++++ .../lacrosse_view/translations/hu.json | 20 +++++++++++++++++++ .../lg_soundbar/translations/ca.json | 2 +- .../lg_soundbar/translations/de.json | 2 +- .../lg_soundbar/translations/en.json | 3 ++- .../lg_soundbar/translations/fr.json | 2 +- .../lg_soundbar/translations/pt-BR.json | 2 +- .../opentherm_gw/translations/ca.json | 3 ++- .../opentherm_gw/translations/de.json | 3 ++- .../opentherm_gw/translations/en.json | 3 ++- .../opentherm_gw/translations/fr.json | 3 ++- .../opentherm_gw/translations/hu.json | 3 ++- .../opentherm_gw/translations/it.json | 3 ++- .../opentherm_gw/translations/pl.json | 3 ++- .../opentherm_gw/translations/pt-BR.json | 3 ++- .../opentherm_gw/translations/zh-Hant.json | 3 ++- .../radiotherm/translations/hu.json | 6 ++++++ .../simplepush/translations/fr.json | 5 +++++ .../soundtouch/translations/fr.json | 5 +++++ .../soundtouch/translations/hu.json | 5 +++++ .../components/spotify/translations/hu.json | 6 ++++++ .../steam_online/translations/hu.json | 6 ++++++ .../components/xbox/translations/fr.json | 5 +++++ .../components/xbox/translations/hu.json | 5 +++++ 27 files changed, 131 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/homeassistant_alerts/translations/fr.json create mode 100644 homeassistant/components/lacrosse_view/translations/fr.json create mode 100644 homeassistant/components/lacrosse_view/translations/hu.json diff --git a/homeassistant/components/ambee/translations/fr.json b/homeassistant/components/ambee/translations/fr.json index cfa3e0b9946..da3932962a6 100644 --- a/homeassistant/components/ambee/translations/fr.json +++ b/homeassistant/components/ambee/translations/fr.json @@ -24,5 +24,10 @@ "description": "Configurer Ambee pour l'int\u00e9grer \u00e0 Home Assistant." } } + }, + "issues": { + "pending_removal": { + "title": "L'int\u00e9gration Ambee est en cours de suppression" + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/hu.json b/homeassistant/components/bluetooth/translations/hu.json index 5cdc199476c..8b51191e938 100644 --- a/homeassistant/components/bluetooth/translations/hu.json +++ b/homeassistant/components/bluetooth/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "no_adapters": "Nem tal\u00e1lhat\u00f3 Bluetooth adapter" }, "flow_title": "{name}", "step": { @@ -18,5 +19,14 @@ "description": "V\u00e1lassza ki a be\u00e1ll\u00edtani k\u00edv\u00e1nt eszk\u00f6zt" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "A szkennel\u00e9shez haszn\u00e1lhat\u00f3 Bluetooth-adapter" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/fr.json b/homeassistant/components/homeassistant_alerts/translations/fr.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/fr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/fr.json b/homeassistant/components/lacrosse_view/translations/fr.json new file mode 100644 index 00000000000..689d3cf7cf6 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "invalid_auth": "Authentification non valide", + "no_locations": "Aucun emplacement trouv\u00e9", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/hu.json b/homeassistant/components/lacrosse_view/translations/hu.json new file mode 100644 index 00000000000..a040bd96b91 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "no_locations": "Nem tal\u00e1lhat\u00f3k helyek", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/ca.json b/homeassistant/components/lg_soundbar/translations/ca.json index 8c445361eb8..3d1b6f3bc98 100644 --- a/homeassistant/components/lg_soundbar/translations/ca.json +++ b/homeassistant/components/lg_soundbar/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El servei ja est\u00e0 configurat", + "already_configured": "El dispositiu ja est\u00e0 configurat", "existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent." }, "error": { diff --git a/homeassistant/components/lg_soundbar/translations/de.json b/homeassistant/components/lg_soundbar/translations/de.json index a840fb04abe..b8458a653aa 100644 --- a/homeassistant/components/lg_soundbar/translations/de.json +++ b/homeassistant/components/lg_soundbar/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Der Dienst ist bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert." }, "error": { diff --git a/homeassistant/components/lg_soundbar/translations/en.json b/homeassistant/components/lg_soundbar/translations/en.json index 10441d21536..cbf35dc2976 100644 --- a/homeassistant/components/lg_soundbar/translations/en.json +++ b/homeassistant/components/lg_soundbar/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "existing_instance_updated": "Updated existing configuration." }, "error": { "cannot_connect": "Failed to connect" diff --git a/homeassistant/components/lg_soundbar/translations/fr.json b/homeassistant/components/lg_soundbar/translations/fr.json index b13f3d0d595..5f6977ed3ab 100644 --- a/homeassistant/components/lg_soundbar/translations/fr.json +++ b/homeassistant/components/lg_soundbar/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour." }, "error": { diff --git a/homeassistant/components/lg_soundbar/translations/pt-BR.json b/homeassistant/components/lg_soundbar/translations/pt-BR.json index dfbff8cddc8..8a2b69e069d 100644 --- a/homeassistant/components/lg_soundbar/translations/pt-BR.json +++ b/homeassistant/components/lg_soundbar/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "existing_instance_updated": "Configura\u00e7\u00e3o existente atualizada." }, "error": { diff --git a/homeassistant/components/opentherm_gw/translations/ca.json b/homeassistant/components/opentherm_gw/translations/ca.json index 6427c3c7633..fcfc8186b18 100644 --- a/homeassistant/components/opentherm_gw/translations/ca.json +++ b/homeassistant/components/opentherm_gw/translations/ca.json @@ -3,7 +3,8 @@ "error": { "already_configured": "El dispositiu ja est\u00e0 configurat", "cannot_connect": "Ha fallat la connexi\u00f3", - "id_exists": "L'identificador de passarel\u00b7la ja existeix" + "id_exists": "L'identificador de passarel\u00b7la ja existeix", + "timeout_connect": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/de.json b/homeassistant/components/opentherm_gw/translations/de.json index 1217839eaed..322ac29aec4 100644 --- a/homeassistant/components/opentherm_gw/translations/de.json +++ b/homeassistant/components/opentherm_gw/translations/de.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", - "id_exists": "Gateway-ID ist bereits vorhanden" + "id_exists": "Gateway-ID ist bereits vorhanden", + "timeout_connect": "Zeit\u00fcberschreitung beim Verbindungsaufbau" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/en.json b/homeassistant/components/opentherm_gw/translations/en.json index c84e9dcf0e2..a44e121063d 100644 --- a/homeassistant/components/opentherm_gw/translations/en.json +++ b/homeassistant/components/opentherm_gw/translations/en.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Device is already configured", "cannot_connect": "Failed to connect", - "id_exists": "Gateway id already exists" + "id_exists": "Gateway id already exists", + "timeout_connect": "Timeout establishing connection" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/fr.json b/homeassistant/components/opentherm_gw/translations/fr.json index d0a058eacfa..4a9cee29f1f 100644 --- a/homeassistant/components/opentherm_gw/translations/fr.json +++ b/homeassistant/components/opentherm_gw/translations/fr.json @@ -3,7 +3,8 @@ "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", - "id_exists": "L'identifiant de la passerelle existe d\u00e9j\u00e0" + "id_exists": "L'identifiant de la passerelle existe d\u00e9j\u00e0", + "timeout_connect": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/hu.json b/homeassistant/components/opentherm_gw/translations/hu.json index 9e2b6478bec..262a42aaaa5 100644 --- a/homeassistant/components/opentherm_gw/translations/hu.json +++ b/homeassistant/components/opentherm_gw/translations/hu.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "id_exists": "Az \u00e1tj\u00e1r\u00f3 azonos\u00edt\u00f3ja m\u00e1r l\u00e9tezik" + "id_exists": "Az \u00e1tj\u00e1r\u00f3 azonos\u00edt\u00f3ja m\u00e1r l\u00e9tezik", + "timeout_connect": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/it.json b/homeassistant/components/opentherm_gw/translations/it.json index 4d1feac60dd..16f7a54bf90 100644 --- a/homeassistant/components/opentherm_gw/translations/it.json +++ b/homeassistant/components/opentherm_gw/translations/it.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "cannot_connect": "Impossibile connettersi", - "id_exists": "ID del gateway esiste gi\u00e0" + "id_exists": "ID del gateway esiste gi\u00e0", + "timeout_connect": "Tempo scaduto per stabile la connessione." }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/pl.json b/homeassistant/components/opentherm_gw/translations/pl.json index fafc4fdc0d7..b2c15052b69 100644 --- a/homeassistant/components/opentherm_gw/translations/pl.json +++ b/homeassistant/components/opentherm_gw/translations/pl.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "id_exists": "Identyfikator bramki ju\u017c istnieje" + "id_exists": "Identyfikator bramki ju\u017c istnieje", + "timeout_connect": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/pt-BR.json b/homeassistant/components/opentherm_gw/translations/pt-BR.json index 018aed87dc3..3d2649aad08 100644 --- a/homeassistant/components/opentherm_gw/translations/pt-BR.json +++ b/homeassistant/components/opentherm_gw/translations/pt-BR.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "cannot_connect": "Falha ao conectar", - "id_exists": "ID do gateway j\u00e1 existe" + "id_exists": "ID do gateway j\u00e1 existe", + "timeout_connect": "Tempo limite estabelecendo conex\u00e3o" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/zh-Hant.json b/homeassistant/components/opentherm_gw/translations/zh-Hant.json index d5b9d02de42..8227c53e1e9 100644 --- a/homeassistant/components/opentherm_gw/translations/zh-Hant.json +++ b/homeassistant/components/opentherm_gw/translations/zh-Hant.json @@ -3,7 +3,8 @@ "error": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", - "id_exists": "\u9598\u9053\u5668 ID \u5df2\u5b58\u5728" + "id_exists": "\u9598\u9053\u5668 ID \u5df2\u5b58\u5728", + "timeout_connect": "\u5efa\u7acb\u9023\u7dda\u903e\u6642" }, "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/hu.json b/homeassistant/components/radiotherm/translations/hu.json index f55ea666f5f..05e1ece66ec 100644 --- a/homeassistant/components/radiotherm/translations/hu.json +++ b/homeassistant/components/radiotherm/translations/hu.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "A Radio Thermostat kl\u00edmaplatform YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa a Home Assistant 2022.9-ben megsz\u0171nik. \n\nMegl\u00e9v\u0151 konfigur\u00e1ci\u00f3ja automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre. T\u00e1vol\u00edtsa el a YAML-konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistant alkalmaz\u00e1st a probl\u00e9ma megold\u00e1s\u00e1hoz.", + "title": "A r\u00e1di\u00f3s termoszt\u00e1t YAML konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/simplepush/translations/fr.json b/homeassistant/components/simplepush/translations/fr.json index 546d03bb131..49356553426 100644 --- a/homeassistant/components/simplepush/translations/fr.json +++ b/homeassistant/components/simplepush/translations/fr.json @@ -17,5 +17,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour Simplepush est en cours de suppression" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/fr.json b/homeassistant/components/soundtouch/translations/fr.json index e0e187756af..94cbd1ed69e 100644 --- a/homeassistant/components/soundtouch/translations/fr.json +++ b/homeassistant/components/soundtouch/translations/fr.json @@ -17,5 +17,10 @@ "title": "Confirmer l'ajout de l'appareil Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour Bose SoundTouch est en cours de suppression" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/hu.json b/homeassistant/components/soundtouch/translations/hu.json index a0f5c364af6..2e4b4c36d8a 100644 --- a/homeassistant/components/soundtouch/translations/hu.json +++ b/homeassistant/components/soundtouch/translations/hu.json @@ -17,5 +17,10 @@ "title": "Bose SoundTouch eszk\u00f6z hozz\u00e1ad\u00e1s\u00e1nak meger\u0151s\u00edt\u00e9se" } } + }, + "issues": { + "deprecated_yaml": { + "title": "A Bose SoundTouch YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/hu.json b/homeassistant/components/spotify/translations/hu.json index 735c4942fb2..846ddaf5ce3 100644 --- a/homeassistant/components/spotify/translations/hu.json +++ b/homeassistant/components/spotify/translations/hu.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "A Spotify YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant nem haszn\u00e1lja.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Spotify YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fclt" + } + }, "system_health": { "info": { "api_endpoint_reachable": "A Spotify API v\u00e9gpont el\u00e9rhet\u0151" diff --git a/homeassistant/components/steam_online/translations/hu.json b/homeassistant/components/steam_online/translations/hu.json index 87e78abedb4..5dcc97464c8 100644 --- a/homeassistant/components/steam_online/translations/hu.json +++ b/homeassistant/components/steam_online/translations/hu.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "A Steam YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML-konfigur\u00e1ci\u00f3t a Home Assistant nem haszn\u00e1lja.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Steam YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fclt" + } + }, "options": { "error": { "unauthorized": "A bar\u00e1t-lista korl\u00e1tozva van: K\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t arr\u00f3l, hogyan l\u00e1thatja az \u00f6sszes t\u00f6bbi bar\u00e1tj\u00e1t." diff --git a/homeassistant/components/xbox/translations/fr.json b/homeassistant/components/xbox/translations/fr.json index 5b37704cec7..ee3af75d401 100644 --- a/homeassistant/components/xbox/translations/fr.json +++ b/homeassistant/components/xbox/translations/fr.json @@ -13,5 +13,10 @@ "title": "S\u00e9lectionner une m\u00e9thode d'authentification" } } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour XBox est en cours de suppression" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/hu.json b/homeassistant/components/xbox/translations/hu.json index fc8428b3101..b7c32ad8008 100644 --- a/homeassistant/components/xbox/translations/hu.json +++ b/homeassistant/components/xbox/translations/hu.json @@ -13,5 +13,10 @@ "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" } } + }, + "issues": { + "deprecated_yaml": { + "title": "Az Xbox YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } } } \ No newline at end of file From 0738f082156def8db0445fefd719d1b167497bb7 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 1 Aug 2022 09:52:51 +0200 Subject: [PATCH 3022/3516] Add missing sensors for Shelly Plus H&T (#76001) * Add missing sensors for Shelly Plus H&T * Cleanup * Fix * Add voltage to battery sensor * Apply review comments --- homeassistant/components/shelly/entity.py | 6 ++-- homeassistant/components/shelly/sensor.py | 37 ++++++++++++++++++++++- homeassistant/components/shelly/utils.py | 17 +++++++++-- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 8cba4d2804e..a38bef54bea 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -190,9 +190,9 @@ def async_setup_entry_rpc( ] and not description.supported(wrapper.device.status[key]): continue - # Filter and remove entities that according to settings should not create an entity + # Filter and remove entities that according to settings/status should not create an entity if description.removal_condition and description.removal_condition( - wrapper.device.config, key + wrapper.device.config, wrapper.device.status, key ): domain = sensor_class.__module__.split(".")[-1] unique_id = f"{wrapper.mac}-{key}-{sensor_id}" @@ -268,7 +268,7 @@ class RpcEntityDescription(EntityDescription, RpcEntityRequiredKeysMixin): value: Callable[[Any, Any], Any] | None = None available: Callable[[dict], bool] | None = None - removal_condition: Callable[[dict, str], bool] | None = None + removal_condition: Callable[[dict, dict, str], bool] | None = None extra_state_attributes: Callable[[dict, dict], dict | None] | None = None use_polling_wrapper: bool = False supported: Callable = lambda _: False diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 92c19734414..5a36b5e99bf 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -46,7 +46,12 @@ from .entity import ( async_setup_entry_rest, async_setup_entry_rpc, ) -from .utils import get_device_entry_gen, get_device_uptime, temperature_unit +from .utils import ( + get_device_entry_gen, + get_device_uptime, + is_rpc_device_externally_powered, + temperature_unit, +) @dataclass @@ -352,6 +357,15 @@ RPC_SENSORS: Final = { entity_category=EntityCategory.DIAGNOSTIC, use_polling_wrapper=True, ), + "temperature_0": RpcSensorDescription( + key="temperature:0", + sub_key="tC", + name="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + ), "rssi": RpcSensorDescription( key="wifi", sub_key="rssi", @@ -373,6 +387,27 @@ RPC_SENSORS: Final = { entity_category=EntityCategory.DIAGNOSTIC, use_polling_wrapper=True, ), + "humidity_0": RpcSensorDescription( + key="humidity:0", + sub_key="rh", + name="Humidity", + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=True, + ), + "battery": RpcSensorDescription( + key="devicepower:0", + sub_key="battery", + name="Battery", + native_unit_of_measurement=PERCENTAGE, + value=lambda status, _: status["percent"], + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + removal_condition=is_rpc_device_externally_powered, + entity_registry_enabled_default=True, + entity_category=EntityCategory.DIAGNOSTIC, + ), } diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 6dfc2fb3be8..7eeb93f2918 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -271,7 +271,9 @@ def get_rpc_channel_name(device: RpcDevice, key: str) -> str: entity_name = device.config[key].get("name", device_name) if entity_name is None: - return f"{device_name} {key.replace(':', '_')}" + if [k for k in key if k.startswith(("input", "switch"))]: + return f"{device_name} {key.replace(':', '_')}" + return device_name return entity_name @@ -325,7 +327,9 @@ def get_rpc_key_ids(keys_dict: dict[str, Any], key: str) -> list[int]: return key_ids -def is_rpc_momentary_input(config: dict[str, Any], key: str) -> bool: +def is_rpc_momentary_input( + config: dict[str, Any], status: dict[str, Any], key: str +) -> bool: """Return true if rpc input button settings is set to a momentary type.""" return cast(bool, config[key]["type"] == "button") @@ -342,6 +346,13 @@ def is_rpc_channel_type_light(config: dict[str, Any], channel: int) -> bool: return con_types is not None and con_types[channel].lower().startswith("light") +def is_rpc_device_externally_powered( + config: dict[str, Any], status: dict[str, Any], key: str +) -> bool: + """Return true if device has external power instead of battery.""" + return cast(bool, status[key]["external"]["present"]) + + def get_rpc_input_triggers(device: RpcDevice) -> list[tuple[str, str]]: """Return list of input triggers for RPC device.""" triggers = [] @@ -350,7 +361,7 @@ def get_rpc_input_triggers(device: RpcDevice) -> list[tuple[str, str]]: for id_ in key_ids: key = f"input:{id_}" - if not is_rpc_momentary_input(device.config, key): + if not is_rpc_momentary_input(device.config, device.status, key): continue for trigger_type in RPC_INPUTS_EVENTS_TYPES: From 81d1786a16e0c5153239be9f4da2d2b2d93a4ce0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 1 Aug 2022 10:13:05 +0200 Subject: [PATCH 3023/3516] Remove unused logging args parameter (#75619) --- homeassistant/util/logging.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 6c4a55f51f2..76df4bb17b6 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -118,22 +118,20 @@ def log_exception(format_err: Callable[..., Any], *args: Any) -> None: @overload def catch_log_exception( - func: Callable[..., Coroutine[Any, Any, Any]], - format_err: Callable[..., Any], - *args: Any, + func: Callable[..., Coroutine[Any, Any, Any]], format_err: Callable[..., Any] ) -> Callable[..., Coroutine[Any, Any, None]]: """Overload for Callables that return a Coroutine.""" @overload def catch_log_exception( - func: Callable[..., Any], format_err: Callable[..., Any], *args: Any + func: Callable[..., Any], format_err: Callable[..., Any] ) -> Callable[..., None | Coroutine[Any, Any, None]]: """Overload for Callables that return Any.""" def catch_log_exception( - func: Callable[..., Any], format_err: Callable[..., Any], *args: Any + func: Callable[..., Any], format_err: Callable[..., Any] ) -> Callable[..., None | Coroutine[Any, Any, None]]: """Decorate a callback to catch and log exceptions.""" From 826de707e47c5277e6e0b6d6127cbf8b8617ca33 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Mon, 1 Aug 2022 11:35:31 +0200 Subject: [PATCH 3024/3516] Add strict typing to openexchangerates (#76004) --- .strict-typing | 1 + .../components/openexchangerates/sensor.py | 47 +++++++++---------- mypy.ini | 11 +++++ 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/.strict-typing b/.strict-typing index ddeaf4f3844..37dcdd7623f 100644 --- a/.strict-typing +++ b/.strict-typing @@ -186,6 +186,7 @@ homeassistant.components.nut.* homeassistant.components.oncue.* homeassistant.components.onewire.* homeassistant.components.open_meteo.* +homeassistant.components.openexchangerates.* homeassistant.components.openuv.* homeassistant.components.peco.* homeassistant.components.overkiz.* diff --git a/homeassistant/components/openexchangerates/sensor.py b/homeassistant/components/openexchangerates/sensor.py index 37879551ee4..318cae4ae0e 100644 --- a/homeassistant/components/openexchangerates/sensor.py +++ b/homeassistant/components/openexchangerates/sensor.py @@ -4,18 +4,13 @@ from __future__ import annotations from datetime import timedelta from http import HTTPStatus import logging +from typing import Any import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_API_KEY, - CONF_BASE, - CONF_NAME, - CONF_QUOTE, -) +from homeassistant.const import CONF_API_KEY, CONF_BASE, CONF_NAME, CONF_QUOTE from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -49,10 +44,10 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Open Exchange Rates sensor.""" - name = config.get(CONF_NAME) - api_key = config.get(CONF_API_KEY) - base = config.get(CONF_BASE) - quote = config.get(CONF_QUOTE) + name: str = config[CONF_NAME] + api_key: str = config[CONF_API_KEY] + base: str = config[CONF_BASE] + quote: str = config[CONF_QUOTE] parameters = {"base": base, "app_id": api_key} @@ -70,50 +65,55 @@ def setup_platform( class OpenexchangeratesSensor(SensorEntity): """Representation of an Open Exchange Rates sensor.""" - def __init__(self, rest, name, quote): + _attr_attribution = ATTRIBUTION + + def __init__(self, rest: OpenexchangeratesData, name: str, quote: str) -> None: """Initialize the sensor.""" self.rest = rest self._name = name self._quote = quote - self._state = None + self._state: float | None = None @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return self._name @property - def native_value(self): + def native_value(self) -> float | None: """Return the state of the sensor.""" return self._state @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return other attributes of the sensor.""" attr = self.rest.data - attr[ATTR_ATTRIBUTION] = ATTRIBUTION return attr - def update(self): + def update(self) -> None: """Update current conditions.""" self.rest.update() - value = self.rest.data - self._state = round(value[str(self._quote)], 4) + if (value := self.rest.data) is None: + self._attr_available = False + return + + self._attr_available = True + self._state = round(value[self._quote], 4) class OpenexchangeratesData: """Get data from Openexchangerates.org.""" - def __init__(self, resource, parameters, quote): + def __init__(self, resource: str, parameters: dict[str, str], quote: str) -> None: """Initialize the data object.""" self._resource = resource self._parameters = parameters self._quote = quote - self.data = None + self.data: dict[str, Any] | None = None @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): + def update(self) -> None: """Get the latest data from openexchangerates.org.""" try: result = requests.get(self._resource, params=self._parameters, timeout=10) @@ -121,4 +121,3 @@ class OpenexchangeratesData: except requests.exceptions.HTTPError: _LOGGER.error("Check the Openexchangerates API key") self.data = None - return False diff --git a/mypy.ini b/mypy.ini index 4ef79368f73..8c85b550f71 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1769,6 +1769,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.openexchangerates.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.openuv.*] check_untyped_defs = true disallow_incomplete_defs = true From 1a40d400dca69bb55f2450db24a118faccbafe10 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Aug 2022 14:09:47 +0200 Subject: [PATCH 3025/3516] Add function/property name to pylint message (#75913) --- pylint/plugins/hass_enforce_type_hints.py | 14 +++++---- tests/pylint/test_enforce_type_hints.py | 35 ++++++++++++----------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 3b50c072eb6..ac21a9bf686 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1577,12 +1577,12 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] priority = -1 msgs = { "W7431": ( - "Argument %s should be of type %s", + "Argument %s should be of type %s in %s", "hass-argument-type", "Used when method argument type is incorrect", ), "W7432": ( - "Return type should be %s", + "Return type should be %s in %s", "hass-return-type", "Used when method return type is incorrect", ), @@ -1669,7 +1669,7 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] self.add_message( "hass-argument-type", node=node.args.args[key], - args=(key + 1, expected_type), + args=(key + 1, expected_type, node.name), ) # Check that all keyword arguments are correctly annotated. @@ -1680,7 +1680,7 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] self.add_message( "hass-argument-type", node=arg_node, - args=(arg_name, expected_type), + args=(arg_name, expected_type, node.name), ) # Check that kwargs is correctly annotated. @@ -1690,13 +1690,15 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] self.add_message( "hass-argument-type", node=node, - args=(node.args.kwarg, match.kwargs_type), + args=(node.args.kwarg, match.kwargs_type, node.name), ) # Check the return type. if not _is_valid_return_type(match, node.returns): self.add_message( - "hass-return-type", node=node, args=match.return_type or "None" + "hass-return-type", + node=node, + args=(match.return_type or "None", node.name), ) diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 54824e5c0b0..53c17880716 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -215,7 +215,7 @@ def test_invalid_discovery_info( pylint.testutils.MessageTest( msg_id="hass-argument-type", node=discovery_info_node, - args=(4, "DiscoveryInfoType | None"), + args=(4, "DiscoveryInfoType | None", "async_setup_scanner"), line=6, col_offset=4, end_line=6, @@ -268,7 +268,10 @@ def test_invalid_list_dict_str_any( pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, - args=["list[dict[str, str]]", "list[dict[str, Any]]"], + args=( + ["list[dict[str, str]]", "list[dict[str, Any]]"], + "async_get_triggers", + ), line=2, col_offset=0, end_line=2, @@ -325,7 +328,7 @@ def test_invalid_config_flow_step( pylint.testutils.MessageTest( msg_id="hass-argument-type", node=arg_node, - args=(2, "ZeroconfServiceInfo"), + args=(2, "ZeroconfServiceInfo", "async_step_zeroconf"), line=10, col_offset=8, end_line=10, @@ -334,7 +337,7 @@ def test_invalid_config_flow_step( pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, - args="FlowResult", + args=("FlowResult", "async_step_zeroconf"), line=8, col_offset=4, end_line=8, @@ -399,7 +402,7 @@ def test_invalid_config_flow_async_get_options_flow( pylint.testutils.MessageTest( msg_id="hass-argument-type", node=arg_node, - args=(1, "ConfigEntry"), + args=(1, "ConfigEntry", "async_get_options_flow"), line=12, col_offset=8, end_line=12, @@ -408,7 +411,7 @@ def test_invalid_config_flow_async_get_options_flow( pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, - args="OptionsFlow", + args=("OptionsFlow", "async_get_options_flow"), line=11, col_offset=4, end_line=11, @@ -491,7 +494,7 @@ def test_invalid_entity_properties( pylint.testutils.MessageTest( msg_id="hass-return-type", node=prop_node, - args=["str", None], + args=(["str", None], "changed_by"), line=9, col_offset=4, end_line=9, @@ -500,7 +503,7 @@ def test_invalid_entity_properties( pylint.testutils.MessageTest( msg_id="hass-argument-type", node=func_node, - args=("kwargs", "Any"), + args=("kwargs", "Any", "async_lock"), line=14, col_offset=4, end_line=14, @@ -509,7 +512,7 @@ def test_invalid_entity_properties( pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, - args="None", + args=("None", "async_lock"), line=14, col_offset=4, end_line=14, @@ -587,7 +590,7 @@ def test_named_arguments( pylint.testutils.MessageTest( msg_id="hass-argument-type", node=percentage_node, - args=("percentage", "int | None"), + args=("percentage", "int | None", "async_turn_on"), line=10, col_offset=8, end_line=10, @@ -596,7 +599,7 @@ def test_named_arguments( pylint.testutils.MessageTest( msg_id="hass-argument-type", node=preset_mode_node, - args=("preset_mode", "str | None"), + args=("preset_mode", "str | None", "async_turn_on"), line=12, col_offset=8, end_line=12, @@ -605,7 +608,7 @@ def test_named_arguments( pylint.testutils.MessageTest( msg_id="hass-argument-type", node=func_node, - args=("kwargs", "Any"), + args=("kwargs", "Any", "async_turn_on"), line=8, col_offset=4, end_line=8, @@ -614,7 +617,7 @@ def test_named_arguments( pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, - args="None", + args=("None", "async_turn_on"), line=8, col_offset=4, end_line=8, @@ -670,7 +673,7 @@ def test_invalid_mapping_return_type( pylint.testutils.MessageTest( msg_id="hass-return-type", node=property_node, - args=["Mapping[str, Any]", None], + args=(["Mapping[str, Any]", None], "capability_attributes"), line=15, col_offset=4, end_line=15, @@ -809,7 +812,7 @@ def test_invalid_long_tuple( pylint.testutils.MessageTest( msg_id="hass-return-type", node=rgbw_node, - args=["tuple[int, int, int, int]", None], + args=(["tuple[int, int, int, int]", None], "rgbw_color"), line=15, col_offset=4, end_line=15, @@ -818,7 +821,7 @@ def test_invalid_long_tuple( pylint.testutils.MessageTest( msg_id="hass-return-type", node=rgbww_node, - args=["tuple[int, int, int, int, int]", None], + args=(["tuple[int, int, int, int, int]", None], "rgbww_color"), line=21, col_offset=4, end_line=21, From 687ac91947d2a17b730c6b520e378a5815ba8e98 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 1 Aug 2022 14:22:24 +0200 Subject: [PATCH 3026/3516] Support MWh for gas consumption sensors (#76016) --- homeassistant/components/energy/validate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index 02549ddfe96..9d6b3bd53c7 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -40,7 +40,11 @@ GAS_USAGE_DEVICE_CLASSES = ( sensor.SensorDeviceClass.GAS, ) GAS_USAGE_UNITS = { - sensor.SensorDeviceClass.ENERGY: (ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR), + sensor.SensorDeviceClass.ENERGY: ( + ENERGY_WATT_HOUR, + ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, + ), sensor.SensorDeviceClass.GAS: (VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET), } GAS_PRICE_UNITS = tuple( From f2da46d99b778592630b3efc2564a5784332ff7c Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 1 Aug 2022 10:07:17 -0400 Subject: [PATCH 3027/3516] Remove aiohttp close from aladdin connect config_flow (#76029) Remove aiohttp close from config_flow - throwing error found in #75933 --- homeassistant/components/aladdin_connect/config_flow.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index 44d3f01a9ec..4f03d7cdb3b 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -44,7 +44,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: CLIENT_ID, ) login = await acc.login() - await acc.close() if not login: raise InvalidAuth From 2dd62b14b65bef4037a2fbcfa86029633433290c Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 1 Aug 2022 16:56:08 +0200 Subject: [PATCH 3028/3516] =?UTF-8?q?Convert=20fj=C3=A4r=C3=A5skupan=20to?= =?UTF-8?q?=20built=20in=20bluetooth=20(#75380)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add bluetooth discovery * Use home assistant standard api * Fixup manufacture data * Adjust config flow to use standard features * Fixup tests * Mock bluetooth * Simplify device check * Fix missing typing Co-authored-by: Martin Hjelmare --- .../components/fjaraskupan/__init__.py | 87 +++++++++---------- .../components/fjaraskupan/config_flow.py | 29 ++----- .../components/fjaraskupan/manifest.json | 9 +- homeassistant/generated/bluetooth.py | 12 +++ tests/components/fjaraskupan/__init__.py | 10 +++ tests/components/fjaraskupan/conftest.py | 46 +--------- .../fjaraskupan/test_config_flow.py | 48 +++++----- 7 files changed, 109 insertions(+), 132 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 36608fb026d..fbd2f13d2b4 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -5,14 +5,20 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta import logging -from typing import TYPE_CHECKING -from bleak import BleakScanner -from fjaraskupan import Device, State, device_filter +from fjaraskupan import Device, State +from homeassistant.components.bluetooth import ( + BluetoothCallbackMatcher, + BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfoBleak, + async_address_present, + async_register_callback, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -23,11 +29,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DISPATCH_DETECTION, DOMAIN -if TYPE_CHECKING: - from bleak.backends.device import BLEDevice - from bleak.backends.scanner import AdvertisementData - - PLATFORMS = [ Platform.BINARY_SENSOR, Platform.FAN, @@ -70,16 +71,18 @@ class Coordinator(DataUpdateCoordinator[State]): async def _async_update_data(self) -> State: """Handle an explicit update request.""" if self._refresh_was_scheduled: - raise UpdateFailed("No data received within schedule.") + if async_address_present(self.hass, self.device.address): + return self.device.state + raise UpdateFailed( + "No data received within schedule, and device is no longer present" + ) await self.device.update() return self.device.state - def detection_callback( - self, ble_device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: + def detection_callback(self, service_info: BluetoothServiceInfoBleak) -> None: """Handle a new announcement of data.""" - self.device.detection_callback(ble_device, advertisement_data) + self.device.detection_callback(service_info.device, service_info.advertisement) self.async_set_updated_data(self.device.state) @@ -87,59 +90,52 @@ class Coordinator(DataUpdateCoordinator[State]): class EntryState: """Store state of config entry.""" - scanner: BleakScanner coordinators: dict[str, Coordinator] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Fjäråskupan from a config entry.""" - scanner = BleakScanner(filters={"DuplicateData": True}) - - state = EntryState(scanner, {}) + state = EntryState({}) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = state - async def detection_callback( - ble_device: BLEDevice, advertisement_data: AdvertisementData + def detection_callback( + service_info: BluetoothServiceInfoBleak, change: BluetoothChange ) -> None: - if data := state.coordinators.get(ble_device.address): - _LOGGER.debug( - "Update: %s %s - %s", ble_device.name, ble_device, advertisement_data - ) - - data.detection_callback(ble_device, advertisement_data) + if change != BluetoothChange.ADVERTISEMENT: + return + if data := state.coordinators.get(service_info.address): + _LOGGER.debug("Update: %s", service_info) + data.detection_callback(service_info) else: - if not device_filter(ble_device, advertisement_data): - return + _LOGGER.debug("Detected: %s", service_info) - _LOGGER.debug( - "Detected: %s %s - %s", ble_device.name, ble_device, advertisement_data - ) - - device = Device(ble_device) + device = Device(service_info.device) device_info = DeviceInfo( - identifiers={(DOMAIN, ble_device.address)}, + identifiers={(DOMAIN, service_info.address)}, manufacturer="Fjäråskupan", name="Fjäråskupan", ) coordinator: Coordinator = Coordinator(hass, device, device_info) - coordinator.detection_callback(ble_device, advertisement_data) + coordinator.detection_callback(service_info) - state.coordinators[ble_device.address] = coordinator + state.coordinators[service_info.address] = coordinator async_dispatcher_send( hass, f"{DISPATCH_DETECTION}.{entry.entry_id}", coordinator ) - scanner.register_detection_callback(detection_callback) - await scanner.start() - - async def on_hass_stop(event: Event) -> None: - await scanner.stop() - entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + async_register_callback( + hass, + detection_callback, + BluetoothCallbackMatcher( + manufacturer_id=20296, + manufacturer_data_start=[79, 68, 70, 74, 65, 82], + ), + BluetoothScanningMode.ACTIVE, + ) ) hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -177,7 +173,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - entry_state: EntryState = hass.data[DOMAIN].pop(entry.entry_id) - await entry_state.scanner.stop() + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/fjaraskupan/config_flow.py b/homeassistant/components/fjaraskupan/config_flow.py index ffac366500b..dd1dc03d3ad 100644 --- a/homeassistant/components/fjaraskupan/config_flow.py +++ b/homeassistant/components/fjaraskupan/config_flow.py @@ -1,42 +1,25 @@ """Config flow for Fjäråskupan integration.""" from __future__ import annotations -import asyncio - -import async_timeout -from bleak import BleakScanner -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData from fjaraskupan import device_filter +from homeassistant.components.bluetooth import async_discovered_service_info from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_flow import register_discovery_flow from .const import DOMAIN -CONST_WAIT_TIME = 5.0 - async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" - event = asyncio.Event() + service_infos = async_discovered_service_info(hass) - def detection(device: BLEDevice, advertisement_data: AdvertisementData): - if device_filter(device, advertisement_data): - event.set() + for service_info in service_infos: + if device_filter(service_info.device, service_info.advertisement): + return True - async with BleakScanner( - detection_callback=detection, - filters={"DuplicateData": True}, - ): - try: - async with async_timeout.timeout(CONST_WAIT_TIME): - await event.wait() - except asyncio.TimeoutError: - return False - - return True + return False register_discovery_flow(DOMAIN, "Fjäråskupan", _async_has_devices) diff --git a/homeassistant/components/fjaraskupan/manifest.json b/homeassistant/components/fjaraskupan/manifest.json index 3ff6e599a6b..bf7956d297d 100644 --- a/homeassistant/components/fjaraskupan/manifest.json +++ b/homeassistant/components/fjaraskupan/manifest.json @@ -6,5 +6,12 @@ "requirements": ["fjaraskupan==1.0.2"], "codeowners": ["@elupus"], "iot_class": "local_polling", - "loggers": ["bleak", "fjaraskupan"] + "loggers": ["bleak", "fjaraskupan"], + "dependencies": ["bluetooth"], + "bluetooth": [ + { + "manufacturer_id": 20296, + "manufacturer_data_start": [79, 68, 70, 74, 65, 82] + } + ] } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 2cbaebb6074..ef8193dad28 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -7,6 +7,18 @@ from __future__ import annotations # fmt: off BLUETOOTH: list[dict[str, str | int | list[int]]] = [ + { + "domain": "fjaraskupan", + "manufacturer_id": 20296, + "manufacturer_data_start": [ + 79, + 68, + 70, + 74, + 65, + 82 + ] + }, { "domain": "govee_ble", "local_name": "Govee*" diff --git a/tests/components/fjaraskupan/__init__.py b/tests/components/fjaraskupan/__init__.py index 26a5ecd6605..35c69f98d65 100644 --- a/tests/components/fjaraskupan/__init__.py +++ b/tests/components/fjaraskupan/__init__.py @@ -1 +1,11 @@ """Tests for the Fjäråskupan integration.""" + + +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from homeassistant.components.bluetooth import SOURCE_LOCAL, BluetoothServiceInfoBleak + +COOKER_SERVICE_INFO = BluetoothServiceInfoBleak.from_advertisement( + BLEDevice("1.1.1.1", "COOKERHOOD_FJAR"), AdvertisementData(), source=SOURCE_LOCAL +) diff --git a/tests/components/fjaraskupan/conftest.py b/tests/components/fjaraskupan/conftest.py index 4e06b2ad046..46ff5ae167a 100644 --- a/tests/components/fjaraskupan/conftest.py +++ b/tests/components/fjaraskupan/conftest.py @@ -1,47 +1,9 @@ """Standard fixtures for the Fjäråskupan integration.""" from __future__ import annotations -from unittest.mock import patch - -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData, BaseBleakScanner -from pytest import fixture +import pytest -@fixture(name="scanner", autouse=True) -def fixture_scanner(hass): - """Fixture for scanner.""" - - devices = [BLEDevice("1.1.1.1", "COOKERHOOD_FJAR")] - - class MockScanner(BaseBleakScanner): - """Mock Scanner.""" - - def __init__(self, *args, **kwargs) -> None: - """Initialize the scanner.""" - super().__init__( - detection_callback=kwargs.pop("detection_callback"), service_uuids=[] - ) - - async def start(self): - """Start scanning for devices.""" - for device in devices: - self._callback(device, AdvertisementData()) - - async def stop(self): - """Stop scanning for devices.""" - - @property - def discovered_devices(self) -> list[BLEDevice]: - """Return discovered devices.""" - return devices - - def set_scanning_filter(self, **kwargs): - """Set the scanning filter.""" - - with patch( - "homeassistant.components.fjaraskupan.config_flow.BleakScanner", new=MockScanner - ), patch( - "homeassistant.components.fjaraskupan.config_flow.CONST_WAIT_TIME", new=0.01 - ): - yield devices +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/fjaraskupan/test_config_flow.py b/tests/components/fjaraskupan/test_config_flow.py index a51b8c6f9fa..bef53e18073 100644 --- a/tests/components/fjaraskupan/test_config_flow.py +++ b/tests/components/fjaraskupan/test_config_flow.py @@ -3,7 +3,6 @@ from __future__ import annotations from unittest.mock import patch -from bleak.backends.device import BLEDevice from pytest import fixture from homeassistant import config_entries @@ -11,6 +10,8 @@ from homeassistant.components.fjaraskupan.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from . import COOKER_SERVICE_INFO + @fixture(name="mock_setup_entry", autouse=True) async def fixture_mock_setup_entry(hass): @@ -24,31 +25,38 @@ async def fixture_mock_setup_entry(hass): async def test_configure(hass: HomeAssistant, mock_setup_entry) -> None: """Test we get the form.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + with patch( + "homeassistant.components.fjaraskupan.config_flow.async_discovered_service_info", + return_value=[COOKER_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) - assert result["type"] == FlowResultType.FORM - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Fjäråskupan" - assert result["data"] == {} + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Fjäråskupan" + assert result["data"] == {} - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 1 + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 -async def test_scan_no_devices(hass: HomeAssistant, scanner: list[BLEDevice]) -> None: +async def test_scan_no_devices(hass: HomeAssistant) -> None: """Test we get the form.""" - scanner.clear() - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + with patch( + "homeassistant.components.fjaraskupan.config_flow.async_discovered_service_info", + return_value=[], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) - assert result["type"] == FlowResultType.FORM - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "no_devices_found" + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" From 1eb0983fba0116eeb2d18707daadad59b29422a0 Mon Sep 17 00:00:00 2001 From: Ethan Madden Date: Mon, 1 Aug 2022 08:06:28 -0700 Subject: [PATCH 3029/3516] Enable air quality sensor for Core300s (#75695) --- homeassistant/components/vesync/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vesync/sensor.py b/homeassistant/components/vesync/sensor.py index 45018c79ca0..c5a2e7182a9 100644 --- a/homeassistant/components/vesync/sensor.py +++ b/homeassistant/components/vesync/sensor.py @@ -73,8 +73,8 @@ def ha_dev_type(device): FILTER_LIFE_SUPPORTED = ["LV-PUR131S", "Core200S", "Core300S", "Core400S", "Core600S"] -AIR_QUALITY_SUPPORTED = ["LV-PUR131S", "Core400S", "Core600S"] -PM25_SUPPORTED = ["Core400S", "Core600S"] +AIR_QUALITY_SUPPORTED = ["LV-PUR131S", "Core300S", "Core400S", "Core600S"] +PM25_SUPPORTED = ["Core300S", "Core400S", "Core600S"] SENSORS: tuple[VeSyncSensorEntityDescription, ...] = ( VeSyncSensorEntityDescription( From 91384e07d0320b2252dee64d768e971da1425816 Mon Sep 17 00:00:00 2001 From: Aaron Godfrey Date: Mon, 1 Aug 2022 10:15:51 -0500 Subject: [PATCH 3030/3516] Add unique id for todoist calendar entity (#75674) Co-authored-by: Martin Hjelmare --- homeassistant/components/todoist/calendar.py | 1 + tests/components/todoist/test_calendar.py | 67 +++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index b1e15e42221..f1240f33b1b 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -296,6 +296,7 @@ class TodoistProjectEntity(CalendarEntity): ) self._cal_data = {} self._name = data[CONF_NAME] + self._attr_unique_id = data.get(CONF_ID) @property def event(self) -> CalendarEvent: diff --git a/tests/components/todoist/test_calendar.py b/tests/components/todoist/test_calendar.py index 2f0832fe739..3458a7bf5d3 100644 --- a/tests/components/todoist/test_calendar.py +++ b/tests/components/todoist/test_calendar.py @@ -1,8 +1,15 @@ """Unit tests for the Todoist calendar platform.""" from datetime import datetime +from typing import Any +from unittest.mock import Mock, patch -from homeassistant.components.todoist.calendar import _parse_due_date +import pytest + +from homeassistant import setup +from homeassistant.components.todoist.calendar import DOMAIN, _parse_due_date from homeassistant.components.todoist.types import DueDate +from homeassistant.const import CONF_TOKEN +from homeassistant.helpers import entity_registry from homeassistant.util import dt @@ -42,3 +49,61 @@ def test_parse_due_date_without_timezone_uses_offset(): } actual = _parse_due_date(data, timezone_offset=-8) assert datetime(2022, 2, 2, 22, 0, 0, tzinfo=dt.UTC) == actual + + +@pytest.fixture(name="state") +def mock_state() -> dict[str, Any]: + """Mock the api state.""" + return { + "collaborators": [], + "labels": [{"name": "label1", "id": 1}], + "projects": [{"id": 12345, "name": "Name"}], + } + + +@patch("homeassistant.components.todoist.calendar.TodoistAPI") +async def test_calendar_entity_unique_id(todoist_api, hass, state): + """Test unique id is set to project id.""" + api = Mock(state=state) + todoist_api.return_value = api + assert await setup.async_setup_component( + hass, + "calendar", + { + "calendar": { + "platform": DOMAIN, + CONF_TOKEN: "token", + } + }, + ) + await hass.async_block_till_done() + + registry = entity_registry.async_get(hass) + entity = registry.async_get("calendar.name") + assert 12345 == entity.unique_id + + +@patch("homeassistant.components.todoist.calendar.TodoistAPI") +async def test_calendar_custom_project_unique_id(todoist_api, hass, state): + """Test unique id is None for any custom projects.""" + api = Mock(state=state) + todoist_api.return_value = api + assert await setup.async_setup_component( + hass, + "calendar", + { + "calendar": { + "platform": DOMAIN, + CONF_TOKEN: "token", + "custom_projects": [{"name": "All projects"}], + } + }, + ) + await hass.async_block_till_done() + + registry = entity_registry.async_get(hass) + entity = registry.async_get("calendar.all_projects") + assert entity is None + + state = hass.states.get("calendar.all_projects") + assert state.state == "off" From 7141c36f8b9d8249348d3573680c1825854c79e7 Mon Sep 17 00:00:00 2001 From: rhadamantys <46837767+rhadamantys@users.noreply.github.com> Date: Mon, 1 Aug 2022 17:42:47 +0200 Subject: [PATCH 3031/3516] Fix invalid enocean unique_id (#74508) Co-authored-by: Martin Hjelmare --- homeassistant/components/enocean/switch.py | 43 +++++++++++-- tests/components/enocean/test_switch.py | 73 ++++++++++++++++++++++ 2 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 tests/components/enocean/test_switch.py diff --git a/homeassistant/components/enocean/switch.py b/homeassistant/components/enocean/switch.py index a53f691df19..5edd2bb6155 100644 --- a/homeassistant/components/enocean/switch.py +++ b/homeassistant/components/enocean/switch.py @@ -5,12 +5,14 @@ from enocean.utils import combine_hex import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import CONF_ID, CONF_NAME +from homeassistant.const import CONF_ID, CONF_NAME, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import DOMAIN, LOGGER from .device import EnOceanEntity CONF_CHANNEL = "channel" @@ -25,10 +27,40 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( +def generate_unique_id(dev_id: list[int], channel: int) -> str: + """Generate a valid unique id.""" + return f"{combine_hex(dev_id)}-{channel}" + + +def _migrate_to_new_unique_id(hass: HomeAssistant, dev_id, channel) -> None: + """Migrate old unique ids to new unique ids.""" + old_unique_id = f"{combine_hex(dev_id)}" + + ent_reg = entity_registry.async_get(hass) + entity_id = ent_reg.async_get_entity_id(Platform.SWITCH, DOMAIN, old_unique_id) + + if entity_id is not None: + new_unique_id = generate_unique_id(dev_id, channel) + try: + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) + except ValueError: + LOGGER.warning( + "Skip migration of id [%s] to [%s] because it already exists", + old_unique_id, + new_unique_id, + ) + else: + LOGGER.debug( + "Migrating unique_id from [%s] to [%s]", + old_unique_id, + new_unique_id, + ) + + +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the EnOcean switch platform.""" @@ -36,7 +68,8 @@ def setup_platform( dev_id = config.get(CONF_ID) dev_name = config.get(CONF_NAME) - add_entities([EnOceanSwitch(dev_id, dev_name, channel)]) + _migrate_to_new_unique_id(hass, dev_id, channel) + async_add_entities([EnOceanSwitch(dev_id, dev_name, channel)]) class EnOceanSwitch(EnOceanEntity, SwitchEntity): @@ -49,7 +82,7 @@ class EnOceanSwitch(EnOceanEntity, SwitchEntity): self._on_state = False self._on_state2 = False self.channel = channel - self._attr_unique_id = f"{combine_hex(dev_id)}" + self._attr_unique_id = generate_unique_id(dev_id, channel) @property def is_on(self): diff --git a/tests/components/enocean/test_switch.py b/tests/components/enocean/test_switch.py new file mode 100644 index 00000000000..a7aafa6fc73 --- /dev/null +++ b/tests/components/enocean/test_switch.py @@ -0,0 +1,73 @@ +"""Tests for the EnOcean switch platform.""" + +from enocean.utils import combine_hex + +from homeassistant.components.enocean import DOMAIN as ENOCEAN_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, assert_setup_component + +SWITCH_CONFIG = { + "switch": [ + { + "platform": ENOCEAN_DOMAIN, + "id": [0xDE, 0xAD, 0xBE, 0xEF], + "channel": 1, + "name": "room0", + }, + ] +} + + +async def test_unique_id_migration(hass: HomeAssistant) -> None: + """Test EnOcean switch ID migration.""" + + entity_name = SWITCH_CONFIG["switch"][0]["name"] + switch_entity_id = f"{SWITCH_DOMAIN}.{entity_name}" + dev_id = SWITCH_CONFIG["switch"][0]["id"] + channel = SWITCH_CONFIG["switch"][0]["channel"] + + ent_reg = er.async_get(hass) + + old_unique_id = f"{combine_hex(dev_id)}" + + entry = MockConfigEntry(domain=ENOCEAN_DOMAIN, data={"device": "/dev/null"}) + + entry.add_to_hass(hass) + + # Add a switch with an old unique_id to the entity registry + entity_entry = ent_reg.async_get_or_create( + SWITCH_DOMAIN, + ENOCEAN_DOMAIN, + old_unique_id, + suggested_object_id=entity_name, + config_entry=entry, + original_name=entity_name, + ) + + assert entity_entry.entity_id == switch_entity_id + assert entity_entry.unique_id == old_unique_id + + # Now add the sensor to check, whether the old unique_id is migrated + + with assert_setup_component(1, SWITCH_DOMAIN): + assert await async_setup_component( + hass, + SWITCH_DOMAIN, + SWITCH_CONFIG, + ) + + await hass.async_block_till_done() + + # Check that new entry has a new unique_id + entity_entry = ent_reg.async_get(switch_entity_id) + new_unique_id = f"{combine_hex(dev_id)}-{channel}" + + assert entity_entry.unique_id == new_unique_id + assert ( + ent_reg.async_get_entity_id(SWITCH_DOMAIN, ENOCEAN_DOMAIN, old_unique_id) + is None + ) From bd3de4452bb673c19f1e555b4566c5db032a598f Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 1 Aug 2022 11:43:07 -0400 Subject: [PATCH 3032/3516] Enhance logging for ZHA device trigger validation (#76036) * Enhance logging for ZHA device trigger validation * use IntegrationError --- homeassistant/components/zha/core/gateway.py | 4 +++- homeassistant/components/zha/core/helpers.py | 18 ++++++++++++++++-- homeassistant/components/zha/device_trigger.py | 4 ++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index f2fd226249b..14fbf2cf701 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -142,6 +142,7 @@ class ZHAGateway: self._log_relay_handler = LogRelayHandler(hass, self) self.config_entry = config_entry self._unsubs: list[Callable[[], None]] = [] + self.initialized: bool = False async def async_initialize(self) -> None: """Initialize controller and connect radio.""" @@ -183,6 +184,7 @@ class ZHAGateway: self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(self.coordinator_ieee) self.async_load_devices() self.async_load_groups() + self.initialized = True @callback def async_load_devices(self) -> None: @@ -217,7 +219,7 @@ class ZHAGateway: async def async_initialize_devices_and_entities(self) -> None: """Initialize devices and load entities.""" - _LOGGER.debug("Loading all devices") + _LOGGER.debug("Initializing all devices from Zigpy cache") await asyncio.gather( *(dev.async_initialize(from_cache=True) for dev in self.devices.values()) ) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index b60f61b1e8e..7fd789ac3f5 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -26,6 +26,7 @@ import zigpy.zdo.types as zdo_types from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, State, callback +from homeassistant.exceptions import IntegrationError from homeassistant.helpers import device_registry as dr from .const import ( @@ -42,6 +43,7 @@ if TYPE_CHECKING: from .gateway import ZHAGateway _T = TypeVar("_T") +_LOGGER = logging.getLogger(__name__) @dataclass @@ -170,10 +172,22 @@ def async_get_zha_device(hass: HomeAssistant, device_id: str) -> ZHADevice: device_registry = dr.async_get(hass) registry_device = device_registry.async_get(device_id) if not registry_device: + _LOGGER.error("Device id `%s` not found in registry", device_id) raise KeyError(f"Device id `{device_id}` not found in registry.") zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ieee_address = list(list(registry_device.identifiers)[0])[1] - ieee = zigpy.types.EUI64.convert(ieee_address) + if not zha_gateway.initialized: + _LOGGER.error("Attempting to get a ZHA device when ZHA is not initialized") + raise IntegrationError("ZHA is not initialized yet") + try: + ieee_address = list(list(registry_device.identifiers)[0])[1] + ieee = zigpy.types.EUI64.convert(ieee_address) + except (IndexError, ValueError) as ex: + _LOGGER.error( + "Unable to determine device IEEE for device with device id `%s`", device_id + ) + raise KeyError( + f"Unable to determine device IEEE for device with device id `{device_id}`." + ) from ex return zha_gateway.devices[ieee] diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 44682aaa559..cdd98110f83 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -13,7 +13,7 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, IntegrationError from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -39,7 +39,7 @@ async def async_validate_trigger_config( trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) try: zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) - except (KeyError, AttributeError) as err: + except (KeyError, AttributeError, IntegrationError) as err: raise InvalidDeviceAutomationConfig from err if ( zha_device.device_automation_triggers is None From fc399f21e9d81e3f1251d9b3887576564478f437 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 1 Aug 2022 17:54:06 +0200 Subject: [PATCH 3033/3516] Guard imports for type hinting in Bluetooth (#75984) --- homeassistant/components/bluetooth/__init__.py | 12 ++++++++---- homeassistant/components/bluetooth/config_flow.py | 6 ++++-- homeassistant/components/bluetooth/match.py | 12 ++++++++---- homeassistant/components/bluetooth/models.py | 7 +++++-- .../bluetooth/passive_update_coordinator.py | 11 +++++++---- .../components/bluetooth/passive_update_processor.py | 12 ++++++++---- 6 files changed, 40 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 9adaac84333..d42f6b4f230 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -8,12 +8,10 @@ from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum import logging -from typing import Final +from typing import TYPE_CHECKING, Final import async_timeout from bleak import BleakError -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -27,7 +25,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo -from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_bluetooth from . import models @@ -42,6 +39,13 @@ from .models import HaBleakScanner, HaBleakScannerWrapper from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher from .util import async_get_bluetooth_adapters +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + from bleak.backends.scanner import AdvertisementData + + from homeassistant.helpers.typing import ConfigType + + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index bbba5f411b2..1a0be8706bf 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -1,18 +1,20 @@ """Config flow to configure the Bluetooth integration.""" from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import voluptuous as vol from homeassistant.components import onboarding from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import CONF_ADAPTER, DEFAULT_NAME, DOMAIN from .util import async_get_bluetooth_adapters +if TYPE_CHECKING: + from homeassistant.data_entry_flow import FlowResult + class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for Bluetooth.""" diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 000f39eefd4..2cd4f62ae5e 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -1,17 +1,21 @@ """The bluetooth integration matchers.""" from __future__ import annotations -from collections.abc import Mapping from dataclasses import dataclass import fnmatch -from typing import Final, TypedDict +from typing import TYPE_CHECKING, Final, TypedDict -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData from lru import LRU # pylint: disable=no-name-in-module from homeassistant.loader import BluetoothMatcher, BluetoothMatcherOptional +if TYPE_CHECKING: + from collections.abc import Mapping + + from bleak.backends.device import BLEDevice + from bleak.backends.scanner import AdvertisementData + + MAX_REMEMBER_ADDRESSES: Final = 2048 diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 6f814c7b66b..51704a2f530 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -4,10 +4,9 @@ from __future__ import annotations import asyncio import contextlib import logging -from typing import Any, Final +from typing import TYPE_CHECKING, Any, Final from bleak import BleakScanner -from bleak.backends.device import BLEDevice from bleak.backends.scanner import ( AdvertisementData, AdvertisementDataCallback, @@ -16,6 +15,10 @@ from bleak.backends.scanner import ( from homeassistant.core import CALLBACK_TYPE, callback as hass_callback +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + + _LOGGER = logging.getLogger(__name__) FILTER_UUIDS: Final = "UUIDs" diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 31a6b065830..5c6b5b79509 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -1,16 +1,19 @@ """Passive update coordinator for the Bluetooth integration.""" from __future__ import annotations -from collections.abc import Callable, Generator -import logging -from typing import Any +from typing import TYPE_CHECKING, Any from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .update_coordinator import BasePassiveBluetoothCoordinator +if TYPE_CHECKING: + from collections.abc import Callable, Generator + import logging + + from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak + class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): """Class to manage passive bluetooth advertisements. diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 1f2047c02cb..78966d9b7ab 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -1,20 +1,24 @@ """Passive update processors for the Bluetooth integration.""" from __future__ import annotations -from collections.abc import Callable, Mapping import dataclasses import logging -from typing import Any, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Generic, TypeVar from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .const import DOMAIN from .update_coordinator import BasePassiveBluetoothCoordinator +if TYPE_CHECKING: + from collections.abc import Callable, Mapping + + from homeassistant.helpers.entity_platform import AddEntitiesCallback + + from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak + @dataclasses.dataclass(frozen=True) class PassiveBluetoothEntityKey: From 567f181a21e60bf2d13ae08640be7adf3c05259c Mon Sep 17 00:00:00 2001 From: krazos Date: Mon, 1 Aug 2022 12:45:18 -0400 Subject: [PATCH 3034/3516] Fix capitalization of Sonos "Status light" entity name (#76035) Tweak capitalization of "Status light" entity name Tweak capitalization of "Status light" entity name for consistency with blog post guidance, which states that entity names should start with a capital letter, with the rest of the words lower case --- homeassistant/components/sonos/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 8dcf47fd0a2..a348b40cb0f 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -72,7 +72,7 @@ FRIENDLY_NAMES = { ATTR_MUSIC_PLAYBACK_FULL_VOLUME: "Surround music full volume", ATTR_NIGHT_SOUND: "Night sound", ATTR_SPEECH_ENHANCEMENT: "Speech enhancement", - ATTR_STATUS_LIGHT: "Status Light", + ATTR_STATUS_LIGHT: "Status light", ATTR_SUB_ENABLED: "Subwoofer enabled", ATTR_SURROUND_ENABLED: "Surround enabled", ATTR_TOUCH_CONTROLS: "Touch controls", From deff0ad61e8ed01a9ffc2e5c4e224af1fb444649 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Aug 2022 19:18:29 +0200 Subject: [PATCH 3035/3516] Implement generic in Deconz base device (#76015) * Make DevonzBase a generic * Adjust alarm_control_panel * Adjust binary_sensor * Adjust climate * More platforms * Adjust light * Ignore type-var * Add space * Implement recommendation * Use type: ignore[union-attr] * Revert "Use type: ignore[union-attr]" This reverts commit 983443062aab0a9c599b2750d823d0c5148c05ce. * Adjust assert * Adjust lock * Rename type variables * type: ignore[union-attr] * Formatting Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- .../components/deconz/alarm_control_panel.py | 3 +- .../components/deconz/binary_sensor.py | 3 +- homeassistant/components/deconz/climate.py | 3 +- homeassistant/components/deconz/cover.py | 3 +- .../components/deconz/deconz_device.py | 41 ++++++++++++------- homeassistant/components/deconz/fan.py | 3 +- homeassistant/components/deconz/light.py | 14 ++----- homeassistant/components/deconz/lock.py | 5 +-- homeassistant/components/deconz/number.py | 3 +- homeassistant/components/deconz/sensor.py | 3 +- homeassistant/components/deconz/siren.py | 3 +- homeassistant/components/deconz/switch.py | 3 +- 12 files changed, 41 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py index bf0f39b75d0..e1fb0757b12 100644 --- a/homeassistant/components/deconz/alarm_control_panel.py +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -80,11 +80,10 @@ async def async_setup_entry( ) -class DeconzAlarmControlPanel(DeconzDevice, AlarmControlPanelEntity): +class DeconzAlarmControlPanel(DeconzDevice[AncillaryControl], AlarmControlPanelEntity): """Representation of a deCONZ alarm control panel.""" TYPE = DOMAIN - _device: AncillaryControl _attr_code_format = CodeFormat.NUMBER _attr_supported_features = ( diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 814dec443e0..a7dbc2eacff 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -233,11 +233,10 @@ async def async_setup_entry( ) -class DeconzBinarySensor(DeconzDevice, BinarySensorEntity): +class DeconzBinarySensor(DeconzDevice[SensorResources], BinarySensorEntity): """Representation of a deCONZ binary sensor.""" TYPE = DOMAIN - _device: SensorResources entity_description: DeconzBinarySensorDescription def __init__( diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 75b37db2c13..e49918e14f2 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -92,11 +92,10 @@ async def async_setup_entry( ) -class DeconzThermostat(DeconzDevice, ClimateEntity): +class DeconzThermostat(DeconzDevice[Thermostat], ClimateEntity): """Representation of a deCONZ thermostat.""" TYPE = DOMAIN - _device: Thermostat _attr_temperature_unit = TEMP_CELSIUS diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 3e56882f15a..8df974cf146 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -49,11 +49,10 @@ async def async_setup_entry( ) -class DeconzCover(DeconzDevice, CoverEntity): +class DeconzCover(DeconzDevice[Cover], CoverEntity): """Representation of a deCONZ cover.""" TYPE = DOMAIN - _device: Cover def __init__(self, cover_id: str, gateway: DeconzGateway) -> None: """Set up cover device.""" diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index e9be4658fb5..0ac7acf5b49 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -2,10 +2,13 @@ from __future__ import annotations -from pydeconz.models.group import Group as DeconzGroup -from pydeconz.models.light import LightBase as DeconzLight +from typing import Generic, TypeVar, Union + +from pydeconz.models.deconz_device import DeconzDevice as PydeconzDevice +from pydeconz.models.group import Group as PydeconzGroup +from pydeconz.models.light import LightBase as PydeconzLightBase from pydeconz.models.scene import Scene as PydeconzScene -from pydeconz.models.sensor import SensorBase as DeconzSensor +from pydeconz.models.sensor import SensorBase as PydeconzSensorBase from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE @@ -15,29 +18,39 @@ from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN as DECONZ_DOMAIN from .gateway import DeconzGateway +_DeviceT = TypeVar( + "_DeviceT", + bound=Union[ + PydeconzGroup, + PydeconzLightBase, + PydeconzSensorBase, + PydeconzScene, + ], +) -class DeconzBase: + +class DeconzBase(Generic[_DeviceT]): """Common base for deconz entities and events.""" def __init__( self, - device: DeconzGroup | DeconzLight | DeconzSensor | PydeconzScene, + device: _DeviceT, gateway: DeconzGateway, ) -> None: """Set up device and add update callback to get data from websocket.""" - self._device = device + self._device: _DeviceT = device self.gateway = gateway @property def unique_id(self) -> str: """Return a unique identifier for this device.""" - assert not isinstance(self._device, PydeconzScene) + assert isinstance(self._device, PydeconzDevice) return self._device.unique_id @property def serial(self) -> str | None: """Return a serial number for this device.""" - assert not isinstance(self._device, PydeconzScene) + assert isinstance(self._device, PydeconzDevice) if not self._device.unique_id or self._device.unique_id.count(":") != 7: return None return self._device.unique_id.split("-", 1)[0] @@ -45,7 +58,7 @@ class DeconzBase: @property def device_info(self) -> DeviceInfo | None: """Return a device description for device registry.""" - assert not isinstance(self._device, PydeconzScene) + assert isinstance(self._device, PydeconzDevice) if self.serial is None: return None @@ -60,7 +73,7 @@ class DeconzBase: ) -class DeconzDevice(DeconzBase, Entity): +class DeconzDevice(DeconzBase[_DeviceT], Entity): """Representation of a deCONZ device.""" _attr_should_poll = False @@ -69,7 +82,7 @@ class DeconzDevice(DeconzBase, Entity): def __init__( self, - device: DeconzGroup | DeconzLight | DeconzSensor | PydeconzScene, + device: _DeviceT, gateway: DeconzGateway, ) -> None: """Set up device and add update callback to get data from websocket.""" @@ -114,16 +127,14 @@ class DeconzDevice(DeconzBase, Entity): """Return True if device is available.""" if isinstance(self._device, PydeconzScene): return self.gateway.available - return self.gateway.available and self._device.reachable + return self.gateway.available and self._device.reachable # type: ignore[union-attr] -class DeconzSceneMixin(DeconzDevice): +class DeconzSceneMixin(DeconzDevice[PydeconzScene]): """Representation of a deCONZ scene.""" _attr_has_entity_name = True - _device: PydeconzScene - def __init__( self, device: PydeconzScene, diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index 6002e61d326..a0d62126b92 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -49,11 +49,10 @@ async def async_setup_entry( ) -class DeconzFan(DeconzDevice, FanEntity): +class DeconzFan(DeconzDevice[Light], FanEntity): """Representation of a deCONZ fan.""" TYPE = DOMAIN - _device: Light _default_on_speed = LightFanSpeed.PERCENT_50 _attr_supported_features = FanEntityFeature.SET_SPEED diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 7c6f1a0e362..590c0795e65 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -1,7 +1,7 @@ """Support for deCONZ lights.""" from __future__ import annotations -from typing import Any, Generic, TypedDict, TypeVar +from typing import Any, TypedDict, TypeVar, Union from pydeconz.interfaces.groups import GroupHandler from pydeconz.interfaces.lights import LightHandler @@ -47,7 +47,7 @@ DECONZ_TO_COLOR_MODE = { LightColorMode.XY: ColorMode.XY, } -_L = TypeVar("_L", Group, Light) +_LightDeviceT = TypeVar("_LightDeviceT", bound=Union[Group, Light]) class SetStateAttributes(TypedDict, total=False): @@ -121,14 +121,12 @@ async def async_setup_entry( ) -class DeconzBaseLight(Generic[_L], DeconzDevice, LightEntity): +class DeconzBaseLight(DeconzDevice[_LightDeviceT], LightEntity): """Representation of a deCONZ light.""" TYPE = DOMAIN - _device: _L - - def __init__(self, device: _L, gateway: DeconzGateway) -> None: + def __init__(self, device: _LightDeviceT, gateway: DeconzGateway) -> None: """Set up light.""" super().__init__(device, gateway) @@ -261,8 +259,6 @@ class DeconzBaseLight(Generic[_L], DeconzDevice, LightEntity): class DeconzLight(DeconzBaseLight[Light]): """Representation of a deCONZ light.""" - _device: Light - @property def max_mireds(self) -> int: """Return the warmest color_temp that this light supports.""" @@ -289,8 +285,6 @@ class DeconzGroup(DeconzBaseLight[Group]): _attr_has_entity_name = True - _device: Group - def __init__(self, device: Group, gateway: DeconzGateway) -> None: """Set up group and create an unique id.""" self._unique_id = f"{gateway.bridgeid}-{device.deconz_id}" diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index d6f9d670c01..9c4c5b43dbe 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any +from typing import Any, Union from pydeconz.models.event import EventType from pydeconz.models.light.lock import Lock @@ -50,11 +50,10 @@ async def async_setup_entry( ) -class DeconzLock(DeconzDevice, LockEntity): +class DeconzLock(DeconzDevice[Union[DoorLock, Lock]], LockEntity): """Representation of a deCONZ lock.""" TYPE = DOMAIN - _device: DoorLock | Lock @property def is_locked(self) -> bool: diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index 4c0959f950d..636711d609d 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -81,11 +81,10 @@ async def async_setup_entry( ) -class DeconzNumber(DeconzDevice, NumberEntity): +class DeconzNumber(DeconzDevice[Presence], NumberEntity): """Representation of a deCONZ number entity.""" TYPE = DOMAIN - _device: Presence def __init__( self, diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 15af1b3dd8f..941729ac8c2 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -273,11 +273,10 @@ async def async_setup_entry( ) -class DeconzSensor(DeconzDevice, SensorEntity): +class DeconzSensor(DeconzDevice[SensorResources], SensorEntity): """Representation of a deCONZ sensor.""" TYPE = DOMAIN - _device: SensorResources entity_description: DeconzSensorDescription def __init__( diff --git a/homeassistant/components/deconz/siren.py b/homeassistant/components/deconz/siren.py index d44bce01aad..45c81c9e31c 100644 --- a/homeassistant/components/deconz/siren.py +++ b/homeassistant/components/deconz/siren.py @@ -41,7 +41,7 @@ async def async_setup_entry( ) -class DeconzSiren(DeconzDevice, SirenEntity): +class DeconzSiren(DeconzDevice[Siren], SirenEntity): """Representation of a deCONZ siren.""" TYPE = DOMAIN @@ -50,7 +50,6 @@ class DeconzSiren(DeconzDevice, SirenEntity): | SirenEntityFeature.TURN_OFF | SirenEntityFeature.DURATION ) - _device: Siren @property def is_on(self) -> bool: diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index b21ec929909..990de24dffc 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -43,11 +43,10 @@ async def async_setup_entry( ) -class DeconzPowerPlug(DeconzDevice, SwitchEntity): +class DeconzPowerPlug(DeconzDevice[Light], SwitchEntity): """Representation of a deCONZ power plug.""" TYPE = DOMAIN - _device: Light @property def is_on(self) -> bool: From 27ed3d324f35b0c04b7d604667bc3ccfc1373a8b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 1 Aug 2022 19:34:06 +0200 Subject: [PATCH 3036/3516] Replace object with enum for pylint sentinel (#76030) * Replace object with enum for pylint sentinel * Use standard enum --- pylint/plugins/hass_enforce_type_hints.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index ac21a9bf686..551db458b1d 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +from enum import Enum import re from astroid import nodes @@ -10,8 +11,13 @@ from pylint.lint import PyLinter from homeassistant.const import Platform -DEVICE_CLASS = object() -UNDEFINED = object() + +class _Special(Enum): + """Sentinel values""" + + UNDEFINED = 1 + DEVICE_CLASS = 2 + _PLATFORMS: set[str] = {platform.value for platform in Platform} @@ -21,7 +27,7 @@ class TypeHintMatch: """Class for pattern matching.""" function_name: str - return_type: list[str] | str | None | object + return_type: list[str | _Special | None] | str | _Special | None arg_types: dict[int, str] | None = None """arg_types is for positional arguments""" named_arg_types: dict[str, str] | None = None @@ -361,7 +367,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 0: "HomeAssistant", 1: "ConfigEntry", }, - return_type=UNDEFINED, + return_type=_Special.UNDEFINED, ), TypeHintMatch( function_name="async_get_device_diagnostics", @@ -370,7 +376,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "ConfigEntry", 2: "DeviceEntry", }, - return_type=UNDEFINED, + return_type=_Special.UNDEFINED, ), ], } @@ -499,7 +505,7 @@ _ENTITY_MATCH: list[TypeHintMatch] = [ ), TypeHintMatch( function_name="device_class", - return_type=[DEVICE_CLASS, "str", None], + return_type=[_Special.DEVICE_CLASS, "str", None], ), TypeHintMatch( function_name="unit_of_measurement", @@ -1407,11 +1413,11 @@ def _is_valid_type( in_return: bool = False, ) -> bool: """Check the argument node against the expected type.""" - if expected_type is UNDEFINED: + if expected_type is _Special.UNDEFINED: return True # Special case for device_class - if expected_type == DEVICE_CLASS and in_return: + if expected_type is _Special.DEVICE_CLASS and in_return: return ( isinstance(node, nodes.Name) and node.name.endswith("DeviceClass") From 652a8e9e8af6b6c2a39f89f51c64f8ca0e64db3a Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 1 Aug 2022 22:04:16 +0100 Subject: [PATCH 3037/3516] Add reauth flow to xiaomi_ble, fixes problem adding LYWSD03MMC (#76028) --- .../components/xiaomi_ble/config_flow.py | 136 +++++--- .../components/xiaomi_ble/manifest.json | 2 +- homeassistant/components/xiaomi_ble/sensor.py | 45 ++- .../components/xiaomi_ble/strings.json | 4 + .../xiaomi_ble/translations/en.json | 6 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/xiaomi_ble/test_config_flow.py | 293 +++++++++++++++++- 8 files changed, 439 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index aa1ffc24895..092c60e9713 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping import dataclasses from typing import Any @@ -12,7 +13,7 @@ from xiaomi_ble.parser import EncryptionScheme from homeassistant.components import onboarding from homeassistant.components.bluetooth import ( BluetoothScanningMode, - BluetoothServiceInfoBleak, + BluetoothServiceInfo, async_discovered_service_info, async_process_advertisements, ) @@ -31,11 +32,11 @@ class Discovery: """A discovered bluetooth device.""" title: str - discovery_info: BluetoothServiceInfoBleak + discovery_info: BluetoothServiceInfo device: DeviceData -def _title(discovery_info: BluetoothServiceInfoBleak, device: DeviceData) -> str: +def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str: return device.title or device.get_device_name() or discovery_info.name @@ -46,19 +47,19 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovery_info: BluetoothServiceInfo | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, Discovery] = {} async def _async_wait_for_full_advertisement( - self, discovery_info: BluetoothServiceInfoBleak, device: DeviceData - ) -> BluetoothServiceInfoBleak: + self, discovery_info: BluetoothServiceInfo, device: DeviceData + ) -> BluetoothServiceInfo: """Sometimes first advertisement we receive is blank or incomplete. Wait until we get a useful one.""" if not device.pending: return discovery_info def _process_more_advertisements( - service_info: BluetoothServiceInfoBleak, + service_info: BluetoothServiceInfo, ) -> bool: device.update(service_info) return not device.pending @@ -72,7 +73,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): ) async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfoBleak + self, discovery_info: BluetoothServiceInfo ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) @@ -81,20 +82,21 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): if not device.supported(discovery_info): return self.async_abort(reason="not_supported") + title = _title(discovery_info, device) + self.context["title_placeholders"] = {"name": title} + + self._discovered_device = device + # Wait until we have received enough information about this device to detect its encryption type try: - discovery_info = await self._async_wait_for_full_advertisement( + self._discovery_info = await self._async_wait_for_full_advertisement( discovery_info, device ) except asyncio.TimeoutError: - # If we don't see a valid packet within the timeout then this device is not supported. - return self.async_abort(reason="not_supported") - - self._discovery_info = discovery_info - self._discovered_device = device - - title = _title(discovery_info, device) - self.context["title_placeholders"] = {"name": title} + # This device might have a really long advertising interval + # So create a config entry for it, and if we discover it has encryption later + # We can do a reauth + return await self.async_step_confirm_slow() if device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: return await self.async_step_get_encryption_key_legacy() @@ -107,6 +109,8 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Enter a legacy bindkey for a v2/v3 MiBeacon device.""" assert self._discovery_info + assert self._discovered_device + errors = {} if user_input is not None: @@ -115,18 +119,15 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): if len(bindkey) != 24: errors["bindkey"] = "expected_24_characters" else: - device = DeviceData(bindkey=bytes.fromhex(bindkey)) + self._discovered_device.bindkey = bytes.fromhex(bindkey) # If we got this far we already know supported will # return true so we don't bother checking that again # We just want to retry the decryption - device.supported(self._discovery_info) + self._discovered_device.supported(self._discovery_info) - if device.bindkey_verified: - return self.async_create_entry( - title=self.context["title_placeholders"]["name"], - data={"bindkey": bindkey}, - ) + if self._discovered_device.bindkey_verified: + return self._async_get_or_create_entry(bindkey) errors["bindkey"] = "decryption_failed" @@ -142,6 +143,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Enter a bindkey for a v4/v5 MiBeacon device.""" assert self._discovery_info + assert self._discovered_device errors = {} @@ -151,18 +153,15 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): if len(bindkey) != 32: errors["bindkey"] = "expected_32_characters" else: - device = DeviceData(bindkey=bytes.fromhex(bindkey)) + self._discovered_device.bindkey = bytes.fromhex(bindkey) # If we got this far we already know supported will # return true so we don't bother checking that again # We just want to retry the decryption - device.supported(self._discovery_info) + self._discovered_device.supported(self._discovery_info) - if device.bindkey_verified: - return self.async_create_entry( - title=self.context["title_placeholders"]["name"], - data={"bindkey": bindkey}, - ) + if self._discovered_device.bindkey_verified: + return self._async_get_or_create_entry(bindkey) errors["bindkey"] = "decryption_failed" @@ -178,10 +177,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Confirm discovery.""" if user_input is not None or not onboarding.async_is_onboarded(self.hass): - return self.async_create_entry( - title=self.context["title_placeholders"]["name"], - data={}, - ) + return self._async_get_or_create_entry() self._set_confirm_only() return self.async_show_form( @@ -189,6 +185,19 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): description_placeholders=self.context["title_placeholders"], ) + async def async_step_confirm_slow( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Ack that device is slow.""" + if user_input is not None or not onboarding.async_is_onboarded(self.hass): + return self._async_get_or_create_entry() + + self._set_confirm_only() + return self.async_show_form( + step_id="confirm_slow", + description_placeholders=self.context["title_placeholders"], + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -198,24 +207,28 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(address, raise_on_progress=False) discovery = self._discovered_devices[address] + self.context["title_placeholders"] = {"name": discovery.title} + # Wait until we have received enough information about this device to detect its encryption type try: self._discovery_info = await self._async_wait_for_full_advertisement( discovery.discovery_info, discovery.device ) except asyncio.TimeoutError: - # If we don't see a valid packet within the timeout then this device is not supported. - return self.async_abort(reason="not_supported") + # This device might have a really long advertising interval + # So create a config entry for it, and if we discover it has encryption later + # We can do a reauth + return await self.async_step_confirm_slow() + + self._discovered_device = discovery.device if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: - self.context["title_placeholders"] = {"name": discovery.title} return await self.async_step_get_encryption_key_legacy() if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_4_5: - self.context["title_placeholders"] = {"name": discovery.title} return await self.async_step_get_encryption_key_4_5() - return self.async_create_entry(title=discovery.title, data={}) + return self._async_get_or_create_entry() current_addresses = self._async_current_ids() for discovery_info in async_discovered_service_info(self.hass): @@ -241,3 +254,46 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema({vol.Required(CONF_ADDRESS): vol.In(titles)}), ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle a flow initialized by a reauth event.""" + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert entry is not None + + device: DeviceData = self.context["device"] + self._discovered_device = device + + self._discovery_info = device.last_service_info + + if device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: + return await self.async_step_get_encryption_key_legacy() + + if device.encryption_scheme == EncryptionScheme.MIBEACON_4_5: + return await self.async_step_get_encryption_key_4_5() + + # Otherwise there wasn't actually encryption so abort + return self.async_abort(reason="reauth_successful") + + def _async_get_or_create_entry(self, bindkey=None): + data = {} + + if bindkey: + data["bindkey"] = bindkey + + if entry_id := self.context.get("entry_id"): + entry = self.hass.config_entries.async_get_entry(entry_id) + assert entry is not None + + self.hass.config_entries.async_update_entry(entry, data=data) + + # Reload the config entry to notify of updated config + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry( + title=self.context["title_placeholders"]["name"], + data=data, + ) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 0d97dcbedf8..a901439b2c9 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.6.2"], + "requirements": ["xiaomi-ble==0.6.4"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index a96722620a9..b3cd5126967 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -11,8 +11,10 @@ from xiaomi_ble import ( Units, XiaomiBluetoothDeviceData, ) +from xiaomi_ble.parser import EncryptionScheme from homeassistant import config_entries +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, @@ -163,6 +165,45 @@ def sensor_update_to_bluetooth_data_update( ) +def process_service_info( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + data: XiaomiBluetoothDeviceData, + service_info: BluetoothServiceInfoBleak, +) -> PassiveBluetoothDataUpdate: + """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data.""" + update = data.update(service_info) + + # If device isn't pending we know it has seen at least one broadcast with a payload + # If that payload was encrypted and the bindkey was not verified then we need to reauth + if ( + not data.pending + and data.encryption_scheme != EncryptionScheme.NONE + and not data.bindkey_verified + ): + flow_context = { + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "title_placeholders": {"name": entry.title}, + "unique_id": entry.unique_id, + "device": data, + } + + for flow in hass.config_entries.flow.async_progress_by_handler(DOMAIN): + if flow["context"] == flow_context: + break + else: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context=flow_context, + data=entry.data, + ) + ) + + return sensor_update_to_bluetooth_data_update(update) + + async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry, @@ -177,9 +218,7 @@ async def async_setup_entry( kwargs["bindkey"] = bytes.fromhex(bindkey) data = XiaomiBluetoothDeviceData(**kwargs) processor = PassiveBluetoothDataProcessor( - lambda service_info: sensor_update_to_bluetooth_data_update( - data.update(service_info) - ) + lambda service_info: process_service_info(hass, entry, data, service_info) ) entry.async_on_unload( processor.async_add_entities_listener( diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index e12a15a0671..48d5c3a87f7 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -11,6 +11,9 @@ "bluetooth_confirm": { "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" }, + "slow_confirm": { + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if its needed." + }, "get_encryption_key_legacy": { "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey.", "data": { @@ -25,6 +28,7 @@ } }, "abort": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json index b9f4e024f92..836ccc51637 100644 --- a/homeassistant/components/xiaomi_ble/translations/en.json +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -6,7 +6,8 @@ "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", "expected_32_characters": "Expected a 32 character hexadecimal bindkey.", - "no_devices_found": "No devices found on the network" + "no_devices_found": "No devices found on the network", + "reauth_successful": "Re-authentication was successful" }, "flow_title": "{name}", "step": { @@ -25,6 +26,9 @@ }, "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey." }, + "slow_confirm": { + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if its needed." + }, "user": { "data": { "address": "Device" diff --git a/requirements_all.txt b/requirements_all.txt index e3dfbab9e19..5a56eb1b3b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2473,7 +2473,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.2 +xiaomi-ble==0.6.4 # homeassistant.components.knx xknx==0.22.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6653d8f599..110f0446b0d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1665,7 +1665,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.2 +xiaomi-ble==0.6.4 # homeassistant.components.knx xknx==0.22.1 diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index 86fda21aaa0..0d123f0cd54 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -3,7 +3,10 @@ import asyncio from unittest.mock import patch +from xiaomi_ble import XiaomiBluetoothDeviceData as DeviceData + from homeassistant import config_entries +from homeassistant.components.bluetooth import BluetoothChange from homeassistant.components.xiaomi_ble.const import DOMAIN from homeassistant.data_entry_flow import FlowResultType @@ -52,8 +55,19 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload(hass): context={"source": config_entries.SOURCE_BLUETOOTH}, data=MISSING_PAYLOAD_ENCRYPTED, ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "not_supported" + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "confirm_slow" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "LYWSD02MMC" + assert result2["data"] == {} + assert result2["result"].unique_id == "A4:C1:38:56:53:84" async def test_async_step_bluetooth_valid_device_but_missing_payload_then_full(hass): @@ -318,6 +332,24 @@ async def test_async_step_user_no_devices_found(hass): assert result["reason"] == "no_devices_found" +async def test_async_step_user_no_devices_found_2(hass): + """ + Test setup from service info cache with no devices found. + + This variant tests with a non-Xiaomi device known to us. + """ + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[NOT_SENSOR_PUSH_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + async def test_async_step_user_with_found_devices(hass): """Test setup from service info cache with devices found.""" with patch( @@ -363,8 +395,19 @@ async def test_async_step_user_short_payload(hass): result["flow_id"], user_input={"address": "A4:C1:38:56:53:84"}, ) - assert result2["type"] == FlowResultType.ABORT - assert result2["reason"] == "not_supported" + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "confirm_slow" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "LYWSD02MMC" + assert result3["data"] == {} + assert result3["result"].unique_id == "A4:C1:38:56:53:84" async def test_async_step_user_short_payload_then_full(hass): @@ -755,3 +798,245 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): # Verify the original one was aborted assert not hass.config_entries.flow.async_progress(DOMAIN) + + +async def test_async_step_reauth_legacy(hass): + """Test reauth with a legacy key.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="F8:24:41:C5:98:8B", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "F8:24:41:C5:98:8B", + b"X0\xb6\x03\xd2\x8b\x98\xc5A$\xf8\xc3I\x14vu~\x00\x00\x00\x99", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_legacy" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_legacy_wrong_key(hass): + """Test reauth with a bad legacy key, and that we can recover.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="F8:24:41:C5:98:8B", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "F8:24:41:C5:98:8B", + b"X0\xb6\x03\xd2\x8b\x98\xc5A$\xf8\xc3I\x14vu~\x00\x00\x00\x99", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_legacy" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b85307515a487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_legacy" + assert result2["errors"]["bindkey"] == "decryption_failed" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_v4(hass): + """Test reauth with a v4 key.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "54:EF:44:E3:9C:BC", + b"XY\x97\tf\xbc\x9c\xe3D\xefT\x01" b"\x08\x12\x05\x00\x00\x00q^\xbe\x90", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_4_5" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_v4_wrong_key(hass): + """Test reauth for v4 with a bad key, and that we can recover.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "54:EF:44:E3:9C:BC", + b"XY\x97\tf\xbc\x9c\xe3D\xefT\x01" b"\x08\x12\x05\x00\x00\x00q^\xbe\x90", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_4_5" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dada143a58"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_4_5" + assert result2["errors"]["bindkey"] == "decryption_failed" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_abort_early(hass): + """ + Test we can abort the reauth if there is no encryption. + + (This can't currently happen in practice). + """ + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + ) + entry.add_to_hass(hass) + + device = DeviceData() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "title_placeholders": {"name": entry.title}, + "unique_id": entry.unique_id, + "device": device, + }, + data=entry.data, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" From 3eafe13085444a5f29b57c3a43740eff5be36f35 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Aug 2022 00:03:52 +0200 Subject: [PATCH 3038/3516] Improve UI in pylint plugin (#74157) * Adjust FlowResult result type * Adjust tests * Adjust return_type * Use StrEnum for base device_class * Add test for device_class * Add and use SentinelValues.DEVICE_CLASS * Remove duplicate device_class * Cleanup return-type * Drop inheritance check from device_class * Add caching for class methods * Improve tests * Adjust duplicate checks * Adjust tests * Fix rebase --- pylint/plugins/hass_enforce_type_hints.py | 29 ++--- tests/pylint/test_enforce_type_hints.py | 131 +++++++++++++++++----- 2 files changed, 116 insertions(+), 44 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 551db458b1d..d0d20cedd7c 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -16,7 +16,6 @@ class _Special(Enum): """Sentinel values""" UNDEFINED = 1 - DEVICE_CLASS = 2 _PLATFORMS: set[str] = {platform.value for platform in Platform} @@ -466,6 +465,7 @@ _CLASS_MATCH: dict[str, list[ClassTypeHintMatch]] = { } # Overriding properties and functions are normally checked by mypy, and will only # be checked by pylint when --ignore-missing-annotations is False + _ENTITY_MATCH: list[TypeHintMatch] = [ TypeHintMatch( function_name="should_poll", @@ -505,7 +505,7 @@ _ENTITY_MATCH: list[TypeHintMatch] = [ ), TypeHintMatch( function_name="device_class", - return_type=[_Special.DEVICE_CLASS, "str", None], + return_type=["str", None], ), TypeHintMatch( function_name="unit_of_measurement", @@ -1416,15 +1416,6 @@ def _is_valid_type( if expected_type is _Special.UNDEFINED: return True - # Special case for device_class - if expected_type is _Special.DEVICE_CLASS and in_return: - return ( - isinstance(node, nodes.Name) - and node.name.endswith("DeviceClass") - or isinstance(node, nodes.Attribute) - and node.attrname.endswith("DeviceClass") - ) - if isinstance(expected_type, list): for expected_type_item in expected_type: if _is_valid_type(expected_type_item, node, in_return): @@ -1636,18 +1627,28 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] def visit_classdef(self, node: nodes.ClassDef) -> None: """Called when a ClassDef node is visited.""" ancestor: nodes.ClassDef + checked_class_methods: set[str] = set() for ancestor in node.ancestors(): for class_matches in self._class_matchers: if ancestor.name == class_matches.base_class: - self._visit_class_functions(node, class_matches.matches) + self._visit_class_functions( + node, class_matches.matches, checked_class_methods + ) def _visit_class_functions( - self, node: nodes.ClassDef, matches: list[TypeHintMatch] + self, + node: nodes.ClassDef, + matches: list[TypeHintMatch], + checked_class_methods: set[str], ) -> None: + cached_methods: list[nodes.FunctionDef] = list(node.mymethods()) for match in matches: - for function_node in node.mymethods(): + for function_node in cached_methods: + if function_node.name in checked_class_methods: + continue if match.need_to_check_function(function_node): self._check_function(function_node, match) + checked_class_methods.add(function_node.name) def visit_functiondef(self, node: nodes.FunctionDef) -> None: """Called when a FunctionDef node is visited.""" diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 53c17880716..d9edde9fdee 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -307,7 +307,10 @@ def test_invalid_config_flow_step( """Ensure invalid hints are rejected for ConfigFlow step.""" class_node, func_node, arg_node = astroid.extract_node( """ - class ConfigFlow(): + class FlowHandler(): + pass + + class ConfigFlow(FlowHandler): pass class AxisFlowHandler( #@ @@ -329,18 +332,18 @@ def test_invalid_config_flow_step( msg_id="hass-argument-type", node=arg_node, args=(2, "ZeroconfServiceInfo", "async_step_zeroconf"), - line=10, + line=13, col_offset=8, - end_line=10, + end_line=13, end_col_offset=27, ), pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, args=("FlowResult", "async_step_zeroconf"), - line=8, + line=11, col_offset=4, - end_line=8, + end_line=11, end_col_offset=33, ), ): @@ -353,7 +356,10 @@ def test_valid_config_flow_step( """Ensure valid hints are accepted for ConfigFlow step.""" class_node = astroid.extract_node( """ - class ConfigFlow(): + class FlowHandler(): + pass + + class ConfigFlow(FlowHandler): pass class AxisFlowHandler( #@ @@ -377,9 +383,16 @@ def test_invalid_config_flow_async_get_options_flow( linter: UnittestLinter, type_hint_checker: BaseChecker ) -> None: """Ensure invalid hints are rejected for ConfigFlow async_get_options_flow.""" + # AxisOptionsFlow doesn't inherit OptionsFlow, and therefore should fail class_node, func_node, arg_node = astroid.extract_node( """ - class ConfigFlow(): + class FlowHandler(): + pass + + class ConfigFlow(FlowHandler): + pass + + class OptionsFlow(FlowHandler): pass class AxisOptionsFlow(): @@ -403,18 +416,18 @@ def test_invalid_config_flow_async_get_options_flow( msg_id="hass-argument-type", node=arg_node, args=(1, "ConfigEntry", "async_get_options_flow"), - line=12, + line=18, col_offset=8, - end_line=12, + end_line=18, end_col_offset=20, ), pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, args=("OptionsFlow", "async_get_options_flow"), - line=11, + line=17, col_offset=4, - end_line=11, + end_line=17, end_col_offset=30, ), ): @@ -427,10 +440,13 @@ def test_valid_config_flow_async_get_options_flow( """Ensure valid hints are accepted for ConfigFlow async_get_options_flow.""" class_node = astroid.extract_node( """ - class ConfigFlow(): + class FlowHandler(): pass - class OptionsFlow(): + class ConfigFlow(FlowHandler): + pass + + class OptionsFlow(FlowHandler): pass class AxisOptionsFlow(OptionsFlow): @@ -467,7 +483,10 @@ def test_invalid_entity_properties( class_node, prop_node, func_node = astroid.extract_node( """ - class LockEntity(): + class Entity(): + pass + + class LockEntity(Entity): pass class DoorLock( #@ @@ -495,27 +514,27 @@ def test_invalid_entity_properties( msg_id="hass-return-type", node=prop_node, args=(["str", None], "changed_by"), - line=9, + line=12, col_offset=4, - end_line=9, + end_line=12, end_col_offset=18, ), pylint.testutils.MessageTest( msg_id="hass-argument-type", node=func_node, args=("kwargs", "Any", "async_lock"), - line=14, + line=17, col_offset=4, - end_line=14, + end_line=17, end_col_offset=24, ), pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, args=("None", "async_lock"), - line=14, + line=17, col_offset=4, - end_line=14, + end_line=17, end_col_offset=24, ), ): @@ -531,7 +550,10 @@ def test_ignore_invalid_entity_properties( class_node = astroid.extract_node( """ - class LockEntity(): + class Entity(): + pass + + class LockEntity(Entity): pass class DoorLock( #@ @@ -566,7 +588,13 @@ def test_named_arguments( class_node, func_node, percentage_node, preset_mode_node = astroid.extract_node( """ - class FanEntity(): + class Entity(): + pass + + class ToggleEntity(Entity): + pass + + class FanEntity(ToggleEntity): pass class MyFan( #@ @@ -591,36 +619,36 @@ def test_named_arguments( msg_id="hass-argument-type", node=percentage_node, args=("percentage", "int | None", "async_turn_on"), - line=10, + line=16, col_offset=8, - end_line=10, + end_line=16, end_col_offset=18, ), pylint.testutils.MessageTest( msg_id="hass-argument-type", node=preset_mode_node, args=("preset_mode", "str | None", "async_turn_on"), - line=12, + line=18, col_offset=8, - end_line=12, + end_line=18, end_col_offset=24, ), pylint.testutils.MessageTest( msg_id="hass-argument-type", node=func_node, args=("kwargs", "Any", "async_turn_on"), - line=8, + line=14, col_offset=4, - end_line=8, + end_line=14, end_col_offset=27, ), pylint.testutils.MessageTest( msg_id="hass-return-type", node=func_node, args=("None", "async_turn_on"), - line=8, + line=14, col_offset=4, - end_line=8, + end_line=14, end_col_offset=27, ), ): @@ -829,3 +857,46 @@ def test_invalid_long_tuple( ), ): type_hint_checker.visit_classdef(class_node) + + +def test_invalid_device_class( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Ensure invalid hints are rejected for entity device_class.""" + # Set bypass option + type_hint_checker.config.ignore_missing_annotations = False + + class_node, prop_node = astroid.extract_node( + """ + class Entity(): + pass + + class CoverEntity(Entity): + pass + + class MyCover( #@ + CoverEntity + ): + @property + def device_class( #@ + self + ): + pass + """, + "homeassistant.components.pylint_test.cover", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_adds_messages( + linter, + pylint.testutils.MessageTest( + msg_id="hass-return-type", + node=prop_node, + args=(["CoverDeviceClass", "str", None], "device_class"), + line=12, + col_offset=4, + end_line=12, + end_col_offset=20, + ), + ): + type_hint_checker.visit_classdef(class_node) From 81e3ef03f7e980e7540bc29e9edbecb635e85ad9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 2 Aug 2022 00:27:42 +0000 Subject: [PATCH 3039/3516] [ci skip] Translation update --- .../components/abode/translations/sv.json | 16 +++- .../accuweather/translations/sensor.sv.json | 3 +- .../accuweather/translations/sv.json | 29 ++++++- .../components/acmeda/translations/sv.json | 12 +++ .../components/adax/translations/sv.json | 10 +++ .../components/adguard/translations/sv.json | 6 ++ .../components/aemet/translations/sv.json | 21 +++++- .../components/agent_dvr/translations/sv.json | 3 +- .../components/airnow/translations/sv.json | 7 ++ .../components/airthings/translations/sv.json | 21 ++++++ .../components/airvisual/translations/sv.json | 22 ++++++ .../alarm_control_panel/translations/sv.json | 8 ++ .../alarmdecoder/translations/sv.json | 50 ++++++++++++- .../components/almond/translations/sv.json | 3 +- .../components/ambee/translations/hu.json | 6 ++ .../components/ambee/translations/no.json | 6 ++ .../ambee/translations/sensor.sv.json | 9 +++ .../components/ambee/translations/sv.json | 6 +- .../components/anthemav/translations/hu.json | 6 ++ .../components/anthemav/translations/no.json | 6 ++ .../components/arcam_fmj/translations/sv.json | 24 ++++++ .../components/asuswrt/translations/sv.json | 2 + .../components/atag/translations/sv.json | 4 + .../components/august/translations/sv.json | 1 + .../components/aurora/translations/sv.json | 17 ++++- .../components/awair/translations/sv.json | 19 ++++- .../components/axis/translations/sv.json | 14 +++- .../azure_devops/translations/sv.json | 23 +++++- .../components/baf/translations/sv.json | 5 ++ .../binary_sensor/translations/sv.json | 3 + .../components/blebox/translations/sv.json | 14 +++- .../components/blink/translations/sv.json | 29 ++++++- .../components/bluetooth/translations/no.json | 32 ++++++++ .../components/bond/translations/sv.json | 16 +++- .../components/bosch_shc/translations/sv.json | 37 +++++++++ .../components/braviatv/translations/sv.json | 18 ++++- .../components/broadlink/translations/sv.json | 23 +++++- .../components/brother/translations/sv.json | 1 + .../components/bsblan/translations/sv.json | 7 ++ .../buienradar/translations/sv.json | 29 +++++++ .../components/cast/translations/sv.json | 13 ++++ .../cert_expiry/translations/sv.json | 3 +- .../components/co2signal/translations/sv.json | 29 +++++++ .../components/coinbase/translations/sv.json | 24 +++++- .../components/control4/translations/sv.json | 17 +++++ .../coolmaster/translations/sv.json | 1 + .../coronavirus/translations/sv.json | 3 +- .../components/cover/translations/sv.json | 7 +- .../crownstone/translations/sv.json | 75 +++++++++++++++++++ .../components/daikin/translations/sv.json | 7 +- .../components/deconz/translations/sv.json | 8 ++ .../components/demo/translations/no.json | 21 ++++++ .../components/demo/translations/sv.json | 1 + .../components/denonavr/translations/sv.json | 37 +++++++++ .../device_tracker/translations/sv.json | 4 + .../devolo_home_control/translations/sv.json | 3 + .../components/dexcom/translations/sv.json | 4 + .../dialogflow/translations/sv.json | 1 + .../components/directv/translations/sv.json | 2 + .../components/doorbird/translations/sv.json | 21 +++++- .../components/dsmr/translations/sv.json | 17 +++++ .../components/dunehd/translations/sv.json | 20 +++++ .../components/elgato/translations/sv.json | 6 +- .../components/elkm1/translations/sv.json | 10 +++ .../components/emonitor/translations/sv.json | 12 +++ .../emulated_roku/translations/sv.json | 3 + .../components/enocean/translations/sv.json | 25 +++++++ .../enphase_envoy/translations/sv.json | 6 +- .../components/epson/translations/sv.json | 8 ++ .../components/firmata/translations/sv.json | 7 ++ .../flick_electric/translations/sv.json | 13 +++- .../components/flipr/translations/sv.json | 29 +++++++ .../components/flo/translations/sv.json | 6 ++ .../components/flume/translations/sv.json | 10 ++- .../flunearyou/translations/sv.json | 4 +- .../forecast_solar/translations/sv.json | 19 ++++- .../forked_daapd/translations/sv.json | 24 +++++- .../components/foscam/translations/sv.json | 11 +++ .../components/freebox/translations/sv.json | 3 + .../freedompro/translations/sv.json | 9 ++- .../components/fritz/translations/sv.json | 16 +++- .../components/fritzbox/translations/sv.json | 11 ++- .../fritzbox_callmonitor/translations/sv.json | 30 ++++++++ .../components/generic/translations/cs.json | 3 +- .../components/generic/translations/no.json | 1 + .../components/generic/translations/sv.json | 34 +++++++-- .../components/geofency/translations/sv.json | 4 +- .../components/gios/translations/sv.json | 5 ++ .../components/gogogate2/translations/sv.json | 13 +++- .../components/google/translations/hu.json | 10 +++ .../components/google/translations/no.json | 13 +++- .../google_travel_time/translations/sv.json | 17 ++++- .../components/govee_ble/translations/no.json | 15 ++++ .../components/gpslogger/translations/sv.json | 4 + .../components/gree/translations/sv.json | 13 ++++ .../growatt_server/translations/sv.json | 21 +++++- .../components/guardian/translations/sv.json | 21 ++++++ .../components/habitica/translations/sv.json | 12 ++- .../components/harmony/translations/sv.json | 14 +++- .../components/hassio/translations/sv.json | 14 +++- .../here_travel_time/translations/sv.json | 3 +- .../components/hive/translations/hu.json | 2 +- .../components/hive/translations/sv.json | 36 ++++++++- .../components/hlk_sw16/translations/sv.json | 12 ++- .../home_connect/translations/sv.json | 16 ++++ .../home_plus_control/translations/sv.json | 11 +++ .../homeassistant/translations/sv.json | 4 +- .../homeassistant_alerts/translations/hu.json | 8 ++ .../components/homekit/translations/sv.json | 19 +++++ .../homekit_controller/translations/hu.json | 4 +- .../homekit_controller/translations/sv.json | 17 +++++ .../huawei_lte/translations/sv.json | 9 ++- .../components/hue/translations/sv.json | 15 +++- .../humidifier/translations/sv.json | 20 ++++- .../hvv_departures/translations/sv.json | 7 +- .../components/hyperion/translations/sv.json | 53 +++++++++++++ .../components/ialarm/translations/sv.json | 19 +++++ .../components/icloud/translations/sv.json | 3 +- .../components/ifttt/translations/sv.json | 4 + .../components/inkbird/translations/no.json | 20 +++++ .../components/insteon/translations/sv.json | 41 ++++++++++ .../integration/translations/sv.json | 3 +- .../components/iotawatt/translations/sv.json | 4 + .../components/ipma/translations/sv.json | 5 ++ .../components/ipp/translations/sv.json | 5 +- .../islamic_prayer_times/translations/sv.json | 7 ++ .../components/isy994/translations/sv.json | 32 +++++++- .../keenetic_ndms2/translations/sv.json | 33 ++++++++ .../components/kmtronic/translations/sv.json | 9 +++ .../components/knx/translations/sv.json | 4 +- .../components/konnected/translations/sv.json | 6 +- .../components/kulersky/translations/sv.json | 13 ++++ .../lacrosse_view/translations/no.json | 20 +++++ .../components/life360/translations/hu.json | 2 +- .../components/life360/translations/sv.json | 5 +- .../components/lifx/translations/no.json | 20 +++++ .../components/light/translations/sv.json | 1 + .../components/locative/translations/sv.json | 4 + .../components/lovelace/translations/sv.json | 3 +- .../lutron_caseta/translations/sv.json | 36 +++++++++ .../components/lyric/translations/hu.json | 6 ++ .../components/lyric/translations/no.json | 6 ++ .../components/lyric/translations/sv.json | 19 +++++ .../components/mailgun/translations/sv.json | 4 + .../components/mazda/translations/sv.json | 12 +++ .../media_player/translations/cs.json | 1 + .../media_player/translations/sv.json | 4 +- .../met_eireann/translations/sv.json | 3 + .../meteo_france/translations/sv.json | 15 +++- .../components/miflora/translations/hu.json | 8 ++ .../components/miflora/translations/no.json | 8 ++ .../components/mill/translations/sv.json | 3 + .../components/mitemp_bt/translations/hu.json | 8 ++ .../components/mitemp_bt/translations/no.json | 8 ++ .../components/mjpeg/translations/cs.json | 1 + .../components/moat/translations/no.json | 10 +++ .../mobile_app/translations/sv.json | 3 +- .../modem_callerid/translations/sv.json | 22 ++++++ .../components/monoprice/translations/sv.json | 23 +++++- .../motion_blinds/translations/sv.json | 12 ++- .../components/motioneye/translations/sv.json | 10 +++ .../components/mqtt/translations/sv.json | 23 +++++- .../components/mutesync/translations/sv.json | 16 ++++ .../components/myq/translations/sv.json | 10 ++- .../components/mysensors/translations/sv.json | 32 +++++++- .../components/nanoleaf/translations/sv.json | 23 +++++- .../components/neato/translations/sv.json | 14 +++- .../components/nest/translations/sv.json | 11 ++- .../components/netatmo/translations/sv.json | 36 +++++++++ .../components/nexia/translations/sv.json | 3 + .../nfandroidtv/translations/sv.json | 19 +++++ .../nightscout/translations/sv.json | 18 +++++ .../nmap_tracker/translations/sv.json | 12 +++ .../components/notion/translations/sv.json | 13 +++- .../components/nuheat/translations/sv.json | 6 +- .../components/nuki/translations/sv.json | 12 +++ .../components/nut/translations/sv.json | 3 +- .../components/nws/translations/sv.json | 7 +- .../components/nzbget/translations/sv.json | 26 ++++++- .../components/omnilogic/translations/sv.json | 9 +++ .../components/onvif/translations/sv.json | 41 +++++++++- .../openalpr_local/translations/hu.json | 8 ++ .../opengarage/translations/sv.json | 13 +++- .../opentherm_gw/translations/el.json | 3 +- .../opentherm_gw/translations/sv.json | 5 +- .../components/openuv/translations/sv.json | 14 ++++ .../openweathermap/translations/sv.json | 8 +- .../ovo_energy/translations/sv.json | 5 ++ .../components/owntracks/translations/sv.json | 3 + .../panasonic_viera/translations/sv.json | 4 + .../philips_js/translations/sv.json | 18 ++++- .../components/pi_hole/translations/sv.json | 13 +++- .../components/picnic/translations/sv.json | 7 ++ .../components/plaato/translations/sv.json | 10 +++ .../components/plex/translations/sv.json | 10 ++- .../components/plugwise/translations/sv.json | 29 ++++++- .../components/powerwall/translations/sv.json | 7 +- .../progettihwsw/translations/sv.json | 22 ++++++ .../pvpc_hourly_pricing/translations/sv.json | 15 ++++ .../components/qnap_qsw/translations/sv.json | 1 + .../components/rachio/translations/sv.json | 10 +++ .../radiotherm/translations/hu.json | 2 +- .../rainforest_eagle/translations/sv.json | 11 +++ .../rainmachine/translations/sv.json | 3 + .../recollect_waste/translations/sv.json | 28 +++++++ .../components/remote/translations/sv.json | 1 + .../components/renault/translations/sv.json | 5 +- .../components/rfxtrx/translations/sv.json | 10 +++ .../components/rhasspy/translations/no.json | 5 ++ .../components/risco/translations/sv.json | 15 ++++ .../translations/sv.json | 21 ++++++ .../components/roku/translations/sv.json | 8 +- .../components/roomba/translations/sv.json | 24 ++++++ .../components/roon/translations/sv.json | 11 +++ .../components/rpi_power/translations/sv.json | 4 +- .../components/scrape/translations/sv.json | 24 +++++- .../screenlogic/translations/sv.json | 7 ++ .../components/sense/translations/sv.json | 3 +- .../components/sensor/translations/sv.json | 7 ++ .../sensorpush/translations/no.json | 21 ++++++ .../components/sentry/translations/sv.json | 26 +++++++ .../components/senz/translations/hu.json | 6 ++ .../components/senz/translations/no.json | 6 ++ .../components/sharkiq/translations/sv.json | 9 +++ .../components/shelly/translations/sv.json | 4 +- .../components/sia/translations/sv.json | 36 +++++++++ .../simplepush/translations/hu.json | 6 ++ .../simplepush/translations/no.json | 5 ++ .../simplisafe/translations/hu.json | 4 +- .../simplisafe/translations/sv.json | 21 +++++- .../components/sma/translations/sv.json | 27 +++++++ .../components/smappee/translations/sv.json | 35 +++++++++ .../smart_meter_texas/translations/sv.json | 9 +++ .../smartthings/translations/sv.json | 11 +++ .../components/smarttub/translations/sv.json | 21 ++++++ .../components/sms/translations/sv.json | 14 +++- .../somfy_mylink/translations/sv.json | 20 +++++ .../components/sonarr/translations/sv.json | 8 ++ .../components/sonos/translations/sv.json | 1 + .../soundtouch/translations/hu.json | 1 + .../speedtestdotnet/translations/sv.json | 12 +++ .../components/spider/translations/sv.json | 6 ++ .../components/spotify/translations/hu.json | 4 +- .../components/spotify/translations/sv.json | 8 +- .../components/sql/translations/sv.json | 27 ++++++- .../squeezebox/translations/sv.json | 17 ++++- .../srp_energy/translations/sv.json | 3 + .../steam_online/translations/hu.json | 4 +- .../components/subaru/translations/sv.json | 28 +++++++ .../components/switchbot/translations/hu.json | 2 +- .../components/syncthru/translations/sv.json | 22 +++++- .../synology_dsm/translations/sv.json | 26 ++++++- .../components/tado/translations/sv.json | 1 + .../tankerkoenig/translations/sv.json | 4 +- .../components/tasmota/translations/sv.json | 20 +++++ .../tellduslive/translations/sv.json | 3 + .../components/tibber/translations/sv.json | 11 ++- .../components/tile/translations/sv.json | 17 ++++- .../components/toon/translations/sv.json | 18 ++++- .../totalconnect/translations/sv.json | 3 + .../components/tplink/translations/sv.json | 15 ++++ .../components/traccar/translations/sv.json | 4 + .../components/tractive/translations/sv.json | 21 ++++++ .../components/twilio/translations/sv.json | 4 + .../components/twinkly/translations/sv.json | 17 +++++ .../ukraine_alarm/translations/sv.json | 6 ++ .../components/unifi/translations/sv.json | 15 +++- .../components/upb/translations/sv.json | 5 ++ .../components/upcloud/translations/sv.json | 14 ++++ .../components/upnp/translations/sv.json | 7 ++ .../uptimerobot/translations/sv.json | 19 ++++- .../components/vera/translations/sv.json | 28 +++++++ .../components/vesync/translations/sv.json | 3 + .../components/vizio/translations/sv.json | 10 +++ .../components/volumio/translations/sv.json | 24 ++++++ .../components/vulcan/translations/sv.json | 12 ++- .../components/wallbox/translations/sv.json | 10 +++ .../water_heater/translations/sv.json | 9 +++ .../components/watttime/translations/sv.json | 10 ++- .../waze_travel_time/translations/sv.json | 1 + .../components/wiffi/translations/sv.json | 9 +++ .../components/withings/translations/sv.json | 12 ++- .../components/wled/translations/sv.json | 13 ++++ .../wolflink/translations/sensor.sv.json | 47 +++++++++++- .../components/wolflink/translations/sv.json | 17 ++++- .../components/ws66i/translations/sv.json | 3 + .../components/xbox/translations/hu.json | 1 + .../components/xbox/translations/sv.json | 14 ++++ .../xiaomi_aqara/translations/sv.json | 41 ++++++++++ .../xiaomi_miio/translations/select.sv.json | 9 +++ .../xiaomi_miio/translations/sv.json | 40 +++++++++- .../yamaha_musiccast/translations/sv.json | 23 ++++++ .../components/yeelight/translations/sv.json | 26 +++++++ .../components/zha/translations/hu.json | 2 + .../components/zha/translations/sv.json | 29 ++++++- .../zoneminder/translations/sv.json | 9 ++- .../components/zwave_js/translations/sv.json | 58 +++++++++++++- 297 files changed, 3758 insertions(+), 167 deletions(-) create mode 100644 homeassistant/components/acmeda/translations/sv.json create mode 100644 homeassistant/components/adax/translations/sv.json create mode 100644 homeassistant/components/airthings/translations/sv.json create mode 100644 homeassistant/components/ambee/translations/sensor.sv.json create mode 100644 homeassistant/components/arcam_fmj/translations/sv.json create mode 100644 homeassistant/components/bluetooth/translations/no.json create mode 100644 homeassistant/components/bosch_shc/translations/sv.json create mode 100644 homeassistant/components/buienradar/translations/sv.json create mode 100644 homeassistant/components/co2signal/translations/sv.json create mode 100644 homeassistant/components/crownstone/translations/sv.json create mode 100644 homeassistant/components/denonavr/translations/sv.json create mode 100644 homeassistant/components/dsmr/translations/sv.json create mode 100644 homeassistant/components/dunehd/translations/sv.json create mode 100644 homeassistant/components/enocean/translations/sv.json create mode 100644 homeassistant/components/firmata/translations/sv.json create mode 100644 homeassistant/components/flipr/translations/sv.json create mode 100644 homeassistant/components/govee_ble/translations/no.json create mode 100644 homeassistant/components/gree/translations/sv.json create mode 100644 homeassistant/components/guardian/translations/sv.json create mode 100644 homeassistant/components/home_connect/translations/sv.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/hu.json create mode 100644 homeassistant/components/hyperion/translations/sv.json create mode 100644 homeassistant/components/ialarm/translations/sv.json create mode 100644 homeassistant/components/inkbird/translations/no.json create mode 100644 homeassistant/components/islamic_prayer_times/translations/sv.json create mode 100644 homeassistant/components/keenetic_ndms2/translations/sv.json create mode 100644 homeassistant/components/kulersky/translations/sv.json create mode 100644 homeassistant/components/lacrosse_view/translations/no.json create mode 100644 homeassistant/components/lutron_caseta/translations/sv.json create mode 100644 homeassistant/components/mazda/translations/sv.json create mode 100644 homeassistant/components/miflora/translations/hu.json create mode 100644 homeassistant/components/miflora/translations/no.json create mode 100644 homeassistant/components/mitemp_bt/translations/hu.json create mode 100644 homeassistant/components/mitemp_bt/translations/no.json create mode 100644 homeassistant/components/moat/translations/no.json create mode 100644 homeassistant/components/modem_callerid/translations/sv.json create mode 100644 homeassistant/components/mutesync/translations/sv.json create mode 100644 homeassistant/components/nfandroidtv/translations/sv.json create mode 100644 homeassistant/components/nightscout/translations/sv.json create mode 100644 homeassistant/components/nmap_tracker/translations/sv.json create mode 100644 homeassistant/components/openalpr_local/translations/hu.json create mode 100644 homeassistant/components/progettihwsw/translations/sv.json create mode 100644 homeassistant/components/pvpc_hourly_pricing/translations/sv.json create mode 100644 homeassistant/components/rainforest_eagle/translations/sv.json create mode 100644 homeassistant/components/recollect_waste/translations/sv.json create mode 100644 homeassistant/components/rituals_perfume_genie/translations/sv.json create mode 100644 homeassistant/components/sensorpush/translations/no.json create mode 100644 homeassistant/components/sma/translations/sv.json create mode 100644 homeassistant/components/smappee/translations/sv.json create mode 100644 homeassistant/components/smarttub/translations/sv.json create mode 100644 homeassistant/components/somfy_mylink/translations/sv.json create mode 100644 homeassistant/components/tasmota/translations/sv.json create mode 100644 homeassistant/components/tractive/translations/sv.json create mode 100644 homeassistant/components/twinkly/translations/sv.json create mode 100644 homeassistant/components/vera/translations/sv.json create mode 100644 homeassistant/components/volumio/translations/sv.json create mode 100644 homeassistant/components/xiaomi_aqara/translations/sv.json create mode 100644 homeassistant/components/xiaomi_miio/translations/select.sv.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/sv.json diff --git a/homeassistant/components/abode/translations/sv.json b/homeassistant/components/abode/translations/sv.json index ef61917ad43..df8937dc092 100644 --- a/homeassistant/components/abode/translations/sv.json +++ b/homeassistant/components/abode/translations/sv.json @@ -1,13 +1,27 @@ { "config": { "abort": { + "reauth_successful": "\u00c5terautentisering lyckades", "single_instance_allowed": "Endast en enda konfiguration av Abode \u00e4r till\u00e5ten." }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "invalid_mfa_code": "Ogiltig MFA-kod" + }, "step": { + "mfa": { + "data": { + "mfa_code": "MFA-kod (6 siffror)" + }, + "title": "Ange din MFA-kod f\u00f6r Abode" + }, "reauth_confirm": { "data": { + "password": "L\u00f6senord", "username": "E-postadress" - } + }, + "title": "Fyll i din Abode-inloggningsinformation" }, "user": { "data": { diff --git a/homeassistant/components/accuweather/translations/sensor.sv.json b/homeassistant/components/accuweather/translations/sensor.sv.json index cc940f75b17..33020a4c601 100644 --- a/homeassistant/components/accuweather/translations/sensor.sv.json +++ b/homeassistant/components/accuweather/translations/sensor.sv.json @@ -2,7 +2,8 @@ "state": { "accuweather__pressure_tendency": { "falling": "Fallande", - "rising": "Stigande" + "rising": "Stigande", + "steady": "Stadig" } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/sv.json b/homeassistant/components/accuweather/translations/sv.json index f4a63bb449d..a87b6736271 100644 --- a/homeassistant/components/accuweather/translations/sv.json +++ b/homeassistant/components/accuweather/translations/sv.json @@ -1,11 +1,38 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_api_key": "Ogiltig API-nyckel", + "requests_exceeded": "Det till\u00e5tna antalet f\u00f6rfr\u00e5gningar till Accuweather API har \u00f6verskridits. Du m\u00e5ste v\u00e4nta eller \u00e4ndra API-nyckel." + }, "step": { "user": { "data": { - "api_key": "API-nyckel" + "api_key": "API-nyckel", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Namn" } } } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "V\u00e4derprognos" + }, + "description": "P\u00e5 grund av begr\u00e4nsningarna f\u00f6r den kostnadsfria versionen av AccuWeather API-nyckeln, n\u00e4r du aktiverar v\u00e4derprognos, kommer datauppdateringar att utf\u00f6ras var 80:e minut ist\u00e4llet f\u00f6r var 40:e minut." + } + } + }, + "system_health": { + "info": { + "can_reach_server": "N\u00e5 AccuWeather-servern", + "remaining_requests": "\u00c5terst\u00e5ende till\u00e5tna f\u00f6rfr\u00e5gningar" + } } } \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/sv.json b/homeassistant/components/acmeda/translations/sv.json new file mode 100644 index 00000000000..487295ecade --- /dev/null +++ b/homeassistant/components/acmeda/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "id": "V\u00e4rd-ID" + }, + "title": "V\u00e4lj en hubb att l\u00e4gga till" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adax/translations/sv.json b/homeassistant/components/adax/translations/sv.json new file mode 100644 index 00000000000..be36fec5fe3 --- /dev/null +++ b/homeassistant/components/adax/translations/sv.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/sv.json b/homeassistant/components/adguard/translations/sv.json index 0b58d9dcc97..155d2e9afd1 100644 --- a/homeassistant/components/adguard/translations/sv.json +++ b/homeassistant/components/adguard/translations/sv.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", "existing_instance_updated": "Uppdaterade existerande konfiguration." }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "hassio_confirm": { "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till AdGuard Home som tillhandah\u00e5lls av Supervisor Add-on: {addon}?", @@ -10,7 +14,9 @@ }, "user": { "data": { + "host": "V\u00e4rd", "password": "L\u00f6senord", + "port": "Port", "ssl": "AdGuard Home anv\u00e4nder ett SSL-certifikat", "username": "Anv\u00e4ndarnamn", "verify_ssl": "AdGuard Home anv\u00e4nder ett korrekt certifikat" diff --git a/homeassistant/components/aemet/translations/sv.json b/homeassistant/components/aemet/translations/sv.json index f4a63bb449d..7b88d6d5ed2 100644 --- a/homeassistant/components/aemet/translations/sv.json +++ b/homeassistant/components/aemet/translations/sv.json @@ -1,9 +1,28 @@ { "config": { + "abort": { + "already_configured": "Platsen \u00e4r redan konfigurerad" + }, + "error": { + "invalid_api_key": "Ogiltig API-nyckel" + }, "step": { "user": { "data": { - "api_key": "API-nyckel" + "api_key": "API-nyckel", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Integrationens namn" + }, + "description": "F\u00f6r att generera API-nyckel g\u00e5 till https://opendata.aemet.es/centrodedescargas/altaUsuario" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "station_updates": "Samla data fr\u00e5n AEMET v\u00e4derstationer" } } } diff --git a/homeassistant/components/agent_dvr/translations/sv.json b/homeassistant/components/agent_dvr/translations/sv.json index cf600b98e96..fa0abfb767d 100644 --- a/homeassistant/components/agent_dvr/translations/sv.json +++ b/homeassistant/components/agent_dvr/translations/sv.json @@ -4,7 +4,8 @@ "already_configured": "Enheten \u00e4r redan konfigurerad" }, "error": { - "already_in_progress": "Konfigurationsfl\u00f6de f\u00f6r enhet p\u00e5g\u00e5r redan." + "already_in_progress": "Konfigurationsfl\u00f6de f\u00f6r enhet p\u00e5g\u00e5r redan.", + "cannot_connect": "Det gick inte att ansluta." }, "step": { "user": { diff --git a/homeassistant/components/airnow/translations/sv.json b/homeassistant/components/airnow/translations/sv.json index f4a63bb449d..138764f44d9 100644 --- a/homeassistant/components/airnow/translations/sv.json +++ b/homeassistant/components/airnow/translations/sv.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/airthings/translations/sv.json b/homeassistant/components/airthings/translations/sv.json new file mode 100644 index 00000000000..aa2cb03973e --- /dev/null +++ b/homeassistant/components/airthings/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "description": "Logga in p\u00e5 {url} f\u00f6r att hitta dina autentiseringsuppgifter", + "id": "ID", + "secret": "Hemlighet" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sv.json b/homeassistant/components/airvisual/translations/sv.json index 5273668aa12..d3559f89aa0 100644 --- a/homeassistant/components/airvisual/translations/sv.json +++ b/homeassistant/components/airvisual/translations/sv.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "Platsen \u00e4r redan konfigurerad eller Node/Pro ID \u00e4r redan regristrerat.", + "reauth_successful": "\u00c5terautentisering lyckades" + }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "general_error": "Ett ok\u00e4nt fel intr\u00e4ffade.", "invalid_api_key": "Ogiltig API-nyckel" }, @@ -16,7 +21,24 @@ "password": "Enhetsl\u00f6senord" } }, + "reauth_confirm": { + "data": { + "api_key": "API-nyckel" + }, + "title": "Autentisera AirVisual igen" + }, "user": { + "description": "V\u00e4lj typ av AirVisual data att \u00f6vervaka.", + "title": "Konfigurera AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Visa \u00f6vervakad geografi p\u00e5 kartan" + }, "title": "Konfigurera AirVisual" } } diff --git a/homeassistant/components/alarm_control_panel/translations/sv.json b/homeassistant/components/alarm_control_panel/translations/sv.json index cff9e0cbc52..cd737ef3ebe 100644 --- a/homeassistant/components/alarm_control_panel/translations/sv.json +++ b/homeassistant/components/alarm_control_panel/translations/sv.json @@ -4,16 +4,23 @@ "arm_away": "Larma {entity_name} borta", "arm_home": "Larma {entity_name} hemma", "arm_night": "Larma {entity_name} natt", + "arm_vacation": "Larma semesterl\u00e4ge {entity_name}", "disarm": "Avlarma {entity_name}", "trigger": "Utl\u00f6sare {entity_name}" }, "condition_type": { + "is_armed_away": "{entity_name} \u00e4r bortalarmat", + "is_armed_home": "{entity_name} \u00e4r hemmalarmat", + "is_armed_night": "{entity_name} \u00e4r nattlarmat", + "is_armed_vacation": "{entity_name} \u00e4r larmad i semesterl\u00e4ge", + "is_disarmed": "{entity_name} \u00e4r bortkopplad", "is_triggered": "har utl\u00f6sts" }, "trigger_type": { "armed_away": "{entity_name} larmad borta", "armed_home": "{entity_name} larmad hemma", "armed_night": "{entity_name} larmad natt", + "armed_vacation": "{entity_name} larmad i semesterl\u00e4ge", "disarmed": "{entity_name} bortkopplad", "triggered": "{entity_name} utl\u00f6st" } @@ -25,6 +32,7 @@ "armed_custom_bypass": "Larm f\u00f6rbikopplat", "armed_home": "Hemmalarmat", "armed_night": "Nattlarmat", + "armed_vacation": "Larmad semesterl\u00e4ge", "arming": "Tillkopplar", "disarmed": "Avlarmat", "disarming": "Fr\u00e5nkopplar", diff --git a/homeassistant/components/alarmdecoder/translations/sv.json b/homeassistant/components/alarmdecoder/translations/sv.json index 6c9f0dbcb43..0e8f0208b6c 100644 --- a/homeassistant/components/alarmdecoder/translations/sv.json +++ b/homeassistant/components/alarmdecoder/translations/sv.json @@ -1,26 +1,70 @@ { "config": { + "create_entry": { + "default": "Ansluten till AlarmDecoder." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "protocol": { "data": { - "device_path": "Enhetsv\u00e4g" + "device_baudrate": "Enhetens Baud Rate", + "device_path": "Enhetsv\u00e4g", + "host": "V\u00e4rd", + "port": "Port" }, "title": "Konfigurera anslutningsinst\u00e4llningar" }, "user": { "data": { "protocol": "Protokoll" - } + }, + "title": "V\u00e4lj AlarmDecoder Protocol" } } }, "options": { + "error": { + "int": "F\u00e4ltet nedan m\u00e5ste vara ett heltal.", + "loop_range": "RF Loop m\u00e5ste vara ett heltal mellan 1 och 4.", + "loop_rfid": "RF Loop kan inte anv\u00e4ndas utan RF Serial.", + "relay_inclusive": "Rel\u00e4adress och rel\u00e4kanal \u00e4r beroende av varandra och m\u00e5ste inkluderas tillsammans." + }, "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternativt nattl\u00e4ge", + "auto_bypass": "Automatisk f\u00f6rbikoppling p\u00e5 arm", + "code_arm_required": "Kod kr\u00e4vs f\u00f6r tillkoppling" + }, + "title": "Konfigurera AlarmDecoder" + }, "init": { "data": { "edit_select": "Redigera" }, - "description": "Vad vill du redigera?" + "description": "Vad vill du redigera?", + "title": "Konfigurera AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF loop", + "zone_name": "Zonnamn", + "zone_relayaddr": "Rel\u00e4adress", + "zone_relaychan": "Rel\u00e4kanal", + "zone_rfid": "RF seriell", + "zone_type": "Zontyp" + }, + "description": "Ange detaljer f\u00f6r zon {zone_number} . F\u00f6r att ta bort zon {zone_number} l\u00e4mnar du Zonnamn tomt.", + "title": "Konfigurera AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "Zonnummer" + }, + "description": "Ange zonnumret du vill l\u00e4gga till, redigera eller ta bort.", + "title": "Konfigurera AlarmDecoder" } } } diff --git a/homeassistant/components/almond/translations/sv.json b/homeassistant/components/almond/translations/sv.json index 8b20512df9b..6cccf60b2c2 100644 --- a/homeassistant/components/almond/translations/sv.json +++ b/homeassistant/components/almond/translations/sv.json @@ -2,7 +2,8 @@ "config": { "abort": { "cannot_connect": "Det g\u00e5r inte att ansluta till Almond-servern.", - "missing_configuration": "Kontrollera dokumentationen f\u00f6r hur du st\u00e4ller in Almond." + "missing_configuration": "Kontrollera dokumentationen f\u00f6r hur du st\u00e4ller in Almond.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/ambee/translations/hu.json b/homeassistant/components/ambee/translations/hu.json index 80b14ac7470..98e9fbdabea 100644 --- a/homeassistant/components/ambee/translations/hu.json +++ b/homeassistant/components/ambee/translations/hu.json @@ -24,5 +24,11 @@ "description": "Integr\u00e1lja \u00f6ssze Ambeet Home Assistanttal." } } + }, + "issues": { + "pending_removal": { + "description": "Az Ambee integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra v\u00e1r a Home Assistantb\u00f3l, \u00e9s a 2022.10-es Home Assistant-t\u00f3l m\u00e1r nem lesz el\u00e9rhet\u0151.\n\nAz integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sa az\u00e9rt t\u00f6rt\u00e9nik, mert az Ambee elt\u00e1vol\u00edtotta az ingyenes (korl\u00e1tozott) fi\u00f3kjait, \u00e9s a rendszeres felhaszn\u00e1l\u00f3k sz\u00e1m\u00e1ra m\u00e1r nem biztos\u00edt lehet\u0151s\u00e9get arra, hogy fizet\u0151s csomagra regisztr\u00e1ljanak.\n\nA hiba\u00fczenet elrejt\u00e9s\u00e9hez t\u00e1vol\u00edtsa el az Ambee integr\u00e1ci\u00f3s bejegyz\u00e9st a rendszerb\u0151l.", + "title": "Az Ambee integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/no.json b/homeassistant/components/ambee/translations/no.json index b735ee91509..2c10f596722 100644 --- a/homeassistant/components/ambee/translations/no.json +++ b/homeassistant/components/ambee/translations/no.json @@ -24,5 +24,11 @@ "description": "Sett opp Ambee for \u00e5 integrere med Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "Ambee-integrasjonen venter p\u00e5 fjerning fra Home Assistant og vil ikke lenger v\u00e6re tilgjengelig fra Home Assistant 2022.10. \n\n Integrasjonen blir fjernet, fordi Ambee fjernet deres gratis (begrensede) kontoer og ikke gir vanlige brukere mulighet til \u00e5 registrere seg for en betalt plan lenger. \n\n Fjern Ambee-integrasjonsoppf\u00f8ringen fra forekomsten din for \u00e5 fikse dette problemet.", + "title": "Ambee-integrasjonen blir fjernet" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sensor.sv.json b/homeassistant/components/ambee/translations/sensor.sv.json new file mode 100644 index 00000000000..a7c17b93906 --- /dev/null +++ b/homeassistant/components/ambee/translations/sensor.sv.json @@ -0,0 +1,9 @@ +{ + "state": { + "ambee__risk": { + "low": "L\u00e5g", + "moderate": "M\u00e5ttlig", + "very high": "V\u00e4ldigt h\u00f6gt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/sv.json b/homeassistant/components/ambee/translations/sv.json index a8147496b74..77149dc7889 100644 --- a/homeassistant/components/ambee/translations/sv.json +++ b/homeassistant/components/ambee/translations/sv.json @@ -1,9 +1,13 @@ { "config": { + "abort": { + "reauth_successful": "\u00c5terautentisering lyckades" + }, "step": { "reauth_confirm": { "data": { - "api_key": "API-nyckel" + "api_key": "API-nyckel", + "description": "Autentisera p\u00e5 nytt med ditt Ambee-konto." } }, "user": { diff --git a/homeassistant/components/anthemav/translations/hu.json b/homeassistant/components/anthemav/translations/hu.json index f13544fff61..af7d008356c 100644 --- a/homeassistant/components/anthemav/translations/hu.json +++ b/homeassistant/components/anthemav/translations/hu.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Az Anthem A/V egys\u00e9gek YAML-ben megadott konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA hiba kijav\u00edt\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el az Anthem A/V egys\u00e9gek YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "Az Anthem A/V Receivers YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/no.json b/homeassistant/components/anthemav/translations/no.json index e7b3f66ae8d..ae6b59cc89c 100644 --- a/homeassistant/components/anthemav/translations/no.json +++ b/homeassistant/components/anthemav/translations/no.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Anthem A/V-mottakere ved hjelp av YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Anthem A/V Receivers YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Anthem A/V-mottakernes YAML-konfigurasjon blir fjernet" + } } } \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/sv.json b/homeassistant/components/arcam_fmj/translations/sv.json new file mode 100644 index 00000000000..42d58bfc929 --- /dev/null +++ b/homeassistant/components/arcam_fmj/translations/sv.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{host}", + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + }, + "description": "Ange v\u00e4rdnamnet eller IP-adressen f\u00f6r enheten." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "{entity_name} har ombetts att sl\u00e5 p\u00e5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/sv.json b/homeassistant/components/asuswrt/translations/sv.json index 4e7196b1b21..ec6717b75f1 100644 --- a/homeassistant/components/asuswrt/translations/sv.json +++ b/homeassistant/components/asuswrt/translations/sv.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Kunde inte ansluta", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress", "pwd_and_ssh": "Ange endast l\u00f6senord eller SSH-nyckelfil", "pwd_or_ssh": "V\u00e4nligen ange l\u00f6senord eller SSH-nyckelfil", "ssh_not_file": "SSH-nyckelfil hittades inte", @@ -14,6 +15,7 @@ "step": { "user": { "data": { + "host": "V\u00e4rd", "mode": "L\u00e4ge", "name": "Namn", "password": "L\u00f6senord", diff --git a/homeassistant/components/atag/translations/sv.json b/homeassistant/components/atag/translations/sv.json index ae07cfa6221..480da89cb4a 100644 --- a/homeassistant/components/atag/translations/sv.json +++ b/homeassistant/components/atag/translations/sv.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unauthorized": "Parning nekad, kontrollera enheten f\u00f6r autentiseringsbeg\u00e4ran" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/august/translations/sv.json b/homeassistant/components/august/translations/sv.json index 762d5fd7640..b4d4e8835fa 100644 --- a/homeassistant/components/august/translations/sv.json +++ b/homeassistant/components/august/translations/sv.json @@ -24,6 +24,7 @@ "data": { "code": "Verifieringskod" }, + "description": "Kontrollera din {login_method} ( {username} ) och ange verifieringskoden nedan", "title": "Tv\u00e5faktorsautentisering" } } diff --git a/homeassistant/components/aurora/translations/sv.json b/homeassistant/components/aurora/translations/sv.json index 7e16a2c036e..eabf6a41d0b 100644 --- a/homeassistant/components/aurora/translations/sv.json +++ b/homeassistant/components/aurora/translations/sv.json @@ -1,4 +1,18 @@ { + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Namn" + } + } + } + }, "options": { "step": { "init": { @@ -7,5 +21,6 @@ } } } - } + }, + "title": "NOAA Aurora Sensor" } \ No newline at end of file diff --git a/homeassistant/components/awair/translations/sv.json b/homeassistant/components/awair/translations/sv.json index a7dd53ffad4..4823ac2df6a 100644 --- a/homeassistant/components/awair/translations/sv.json +++ b/homeassistant/components/awair/translations/sv.json @@ -1,6 +1,22 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "invalid_access_token": "Ogiltig \u00e5tkomstnyckel", + "unknown": "Ov\u00e4ntat fel" + }, "step": { + "reauth": { + "data": { + "access_token": "\u00c5tkomstnyckel", + "email": "E-post" + }, + "description": "Ange din Awair-utvecklar\u00e5tkomsttoken igen." + }, "reauth_confirm": { "data": { "access_token": "\u00c5tkomsttoken", @@ -10,7 +26,8 @@ }, "user": { "data": { - "access_token": "\u00c5tkomstnyckel" + "access_token": "\u00c5tkomstnyckel", + "email": "E-post" } } } diff --git a/homeassistant/components/axis/translations/sv.json b/homeassistant/components/axis/translations/sv.json index e04267cb5d7..73de4193654 100644 --- a/homeassistant/components/axis/translations/sv.json +++ b/homeassistant/components/axis/translations/sv.json @@ -7,7 +7,9 @@ }, "error": { "already_configured": "Enheten \u00e4r redan konfigurerad", - "already_in_progress": "Konfigurations fl\u00f6det f\u00f6r enheten p\u00e5g\u00e5r redan." + "already_in_progress": "Konfigurations fl\u00f6det f\u00f6r enheten p\u00e5g\u00e5r redan.", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" }, "flow_title": "Axisenhet: {name} ({host})", "step": { @@ -21,5 +23,15 @@ "title": "Konfigurera Axis-enhet" } } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "V\u00e4lj streamprofil att anv\u00e4nda" + }, + "title": "Konfigurera enhetens videostr\u00f6mningsalternativ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/azure_devops/translations/sv.json b/homeassistant/components/azure_devops/translations/sv.json index e87d9570334..0728a279d21 100644 --- a/homeassistant/components/azure_devops/translations/sv.json +++ b/homeassistant/components/azure_devops/translations/sv.json @@ -1,10 +1,31 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "project_error": "Kunde inte h\u00e4mta projektinformation" + }, + "flow_title": "{project_url}", "step": { + "reauth": { + "data": { + "personal_access_token": "Personlig \u00e5tkomsttoken (PAT)" + }, + "description": "Autentisering misslyckades f\u00f6r {project_url} . Ange dina nuvarande uppgifter.", + "title": "\u00c5terautentisering" + }, "user": { "data": { + "organization": "Organisation", + "personal_access_token": "Personlig \u00e5tkomsttoken (PAT)", "project": "Projekt" - } + }, + "description": "Konfigurera en Azure DevOps-instans f\u00f6r att komma \u00e5t ditt projekt. En personlig \u00e5tkomsttoken kr\u00e4vs endast f\u00f6r ett privat projekt.", + "title": "L\u00e4gg till Azure DevOps Project" } } } diff --git a/homeassistant/components/baf/translations/sv.json b/homeassistant/components/baf/translations/sv.json index 0e346a0f72a..b0126b30f97 100644 --- a/homeassistant/components/baf/translations/sv.json +++ b/homeassistant/components/baf/translations/sv.json @@ -12,6 +12,11 @@ "step": { "discovery_confirm": { "description": "Vill du st\u00e4lla in {name} - {model} ( {ip_address} )?" + }, + "user": { + "data": { + "ip_address": "IP-adress" + } } } } diff --git a/homeassistant/components/binary_sensor/translations/sv.json b/homeassistant/components/binary_sensor/translations/sv.json index 904ecd8fddc..8b05d4b024e 100644 --- a/homeassistant/components/binary_sensor/translations/sv.json +++ b/homeassistant/components/binary_sensor/translations/sv.json @@ -104,6 +104,9 @@ "off": "Normal", "on": "L\u00e5g" }, + "battery_charging": { + "off": "Laddar inte" + }, "cold": { "off": "Normal", "on": "Kallt" diff --git a/homeassistant/components/blebox/translations/sv.json b/homeassistant/components/blebox/translations/sv.json index 892b8b2cd91..e521f4c6f4a 100644 --- a/homeassistant/components/blebox/translations/sv.json +++ b/homeassistant/components/blebox/translations/sv.json @@ -1,10 +1,22 @@ { "config": { + "abort": { + "address_already_configured": "En BleBox-enhet \u00e4r redan konfigurerad p\u00e5 {address} .", + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel", + "unsupported_version": "BleBox-enheten har f\u00f6r\u00e5ldrad firmware. Uppgradera den f\u00f6rst." + }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { "port": "Port" - } + }, + "description": "St\u00e4ll in din BleBox f\u00f6r att integrera med Home Assistant.", + "title": "Konfigurera din BleBox-enhet" } } } diff --git a/homeassistant/components/blink/translations/sv.json b/homeassistant/components/blink/translations/sv.json index 23c825f256f..282dc374e67 100644 --- a/homeassistant/components/blink/translations/sv.json +++ b/homeassistant/components/blink/translations/sv.json @@ -1,10 +1,37 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { + "2fa": { + "data": { + "2fa": "Tv\u00e5faktorkod" + }, + "description": "Ange PIN-koden som skickades till din e-post", + "title": "Tv\u00e5faktorautentisering" + }, "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Logga in med Blink-konto" + } + } + }, + "options": { + "step": { + "simple_options": { + "data": { + "scan_interval": "Skanningsintervall (sekunder)" + }, + "description": "Konfigurera Blink integrationen", + "title": "Blink alternativ" } } } diff --git a/homeassistant/components/bluetooth/translations/no.json b/homeassistant/components/bluetooth/translations/no.json new file mode 100644 index 00000000000..fbc59772d6f --- /dev/null +++ b/homeassistant/components/bluetooth/translations/no.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "no_adapters": "Finner ingen Bluetooth-adaptere" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, + "enable_bluetooth": { + "description": "Vil du konfigurere Bluetooth?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Bluetooth-adapteren som skal brukes til skanning" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/sv.json b/homeassistant/components/bond/translations/sv.json index 1fda5b91f5a..e745283a84a 100644 --- a/homeassistant/components/bond/translations/sv.json +++ b/homeassistant/components/bond/translations/sv.json @@ -1,9 +1,23 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "old_firmware": "Gamla firmware som inte st\u00f6ds p\u00e5 Bond-enheten - uppgradera innan du forts\u00e4tter." + }, + "flow_title": "{name} ({host})", "step": { - "user": { + "confirm": { "data": { "access_token": "\u00c5tkomstnyckel" + }, + "description": "Vill du konfigurera {name}?" + }, + "user": { + "data": { + "access_token": "\u00c5tkomstnyckel", + "host": "V\u00e4rd" } } } diff --git a/homeassistant/components/bosch_shc/translations/sv.json b/homeassistant/components/bosch_shc/translations/sv.json new file mode 100644 index 00000000000..280064d278c --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/sv.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "pairing_failed": "Kopplingen misslyckades; kontrollera att Bosch Smart Home Controller \u00e4r i kopplingsl\u00e4ge (lysdioden blinkar) och att ditt l\u00f6senord \u00e4r korrekt.", + "session_error": "Sessionsfel: API returnerar resultat som inte \u00e4r OK.", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "Bosch SHC: {name}", + "step": { + "confirm_discovery": { + "description": "Tryck p\u00e5 knappen p\u00e5 framsidan av Bosch Smart Home Controller tills lysdioden b\u00f6rjar blinka.\nRedo att forts\u00e4tta att st\u00e4lla in {modell} @ {host} med Home Assistant?" + }, + "credentials": { + "data": { + "password": "L\u00f6senord f\u00f6r Smart Home Controller" + } + }, + "reauth_confirm": { + "description": "Bosch_shc-integrationen m\u00e5ste autentisera ditt konto igen", + "title": "\u00c5terautenticera integration" + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "St\u00e4ll in din Bosch Smart Home Controller f\u00f6r att till\u00e5ta \u00f6vervakning och kontroll med Home Assistant.", + "title": "SHC-autentiseringsparametrar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/sv.json b/homeassistant/components/braviatv/translations/sv.json index e3b98b4e76e..f545bdcabfb 100644 --- a/homeassistant/components/braviatv/translations/sv.json +++ b/homeassistant/components/braviatv/translations/sv.json @@ -1,20 +1,34 @@ { "config": { "abort": { - "already_configured": "Den h\u00e4r TV:n \u00e4r redan konfigurerad" + "already_configured": "Den h\u00e4r TV:n \u00e4r redan konfigurerad", + "no_ip_control": "IP-kontroll \u00e4r inaktiverat p\u00e5 din TV eller s\u00e5 st\u00f6ds inte TV:n." }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress.", "unsupported_model": "Den h\u00e4r tv modellen st\u00f6ds inte." }, "step": { "authorize": { + "description": "Ange PIN-koden som visas p\u00e5 Sony Bravia TV. \n\n Om PIN-koden inte visas m\u00e5ste du avregistrera Home Assistant p\u00e5 din TV, g\u00e5 till: Inst\u00e4llningar - > N\u00e4tverk - > Inst\u00e4llningar f\u00f6r fj\u00e4rrenhet - > Avregistrera fj\u00e4rrenhet.", "title": "Auktorisera Sony Bravia TV" }, "user": { "data": { "host": "V\u00e4rdnamn eller IP-adress f\u00f6r TV" - } + }, + "description": "Se till att din TV \u00e4r p\u00e5slagen innan du f\u00f6rs\u00f6ker st\u00e4lla in den." + } + } + }, + "options": { + "step": { + "user": { + "data": { + "ignored_sources": "Lista \u00f6ver ignorerade k\u00e4llor" + }, + "title": "Alternativ f\u00f6r Sony Bravia TV" } } } diff --git a/homeassistant/components/broadlink/translations/sv.json b/homeassistant/components/broadlink/translations/sv.json index 38d02e42d90..bc621dfaa6a 100644 --- a/homeassistant/components/broadlink/translations/sv.json +++ b/homeassistant/components/broadlink/translations/sv.json @@ -1,7 +1,28 @@ { "config": { "abort": { - "not_supported": "Enheten st\u00f6ds inte" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress", + "not_supported": "Enheten st\u00f6ds inte", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name} ({model} p\u00e5 {host})", + "step": { + "auth": { + "title": "Autentisera till enheten" + }, + "finish": { + "data": { + "name": "Namn" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/brother/translations/sv.json b/homeassistant/components/brother/translations/sv.json index 00d29aa3a0a..9a4fd5948d9 100644 --- a/homeassistant/components/brother/translations/sv.json +++ b/homeassistant/components/brother/translations/sv.json @@ -5,6 +5,7 @@ "unsupported_model": "Den h\u00e4r skrivarmodellen st\u00f6ds inte." }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "snmp_error": "SNMP-servern har st\u00e4ngts av eller s\u00e5 st\u00f6ds inte skrivaren.", "wrong_host": "Ogiltigt v\u00e4rdnamn eller IP-adress." }, diff --git a/homeassistant/components/bsblan/translations/sv.json b/homeassistant/components/bsblan/translations/sv.json index 6dad0946ee5..f96cbe0051f 100644 --- a/homeassistant/components/bsblan/translations/sv.json +++ b/homeassistant/components/bsblan/translations/sv.json @@ -1,11 +1,18 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "cannot_connect": "Det gick inte att ansluta." }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{name}", "step": { "user": { "data": { + "host": "V\u00e4rd", + "passkey": "Nyckelstr\u00e4ng", "port": "Port", "username": "Anv\u00e4ndarnamn" } diff --git a/homeassistant/components/buienradar/translations/sv.json b/homeassistant/components/buienradar/translations/sv.json new file mode 100644 index 00000000000..f0a63c7d5ed --- /dev/null +++ b/homeassistant/components/buienradar/translations/sv.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Platsen \u00e4r redan konfigurerad" + }, + "error": { + "already_configured": "Platsen \u00e4r redan konfigurerad" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "country_code": "Landskod f\u00f6r landet f\u00f6r att visa kamerabilder.", + "delta": "Tidsintervall i sekunder mellan kamerabilduppdateringar", + "timeframe": "Minuter att se fram\u00e5t f\u00f6r nederb\u00f6rdsprognos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/translations/sv.json b/homeassistant/components/cast/translations/sv.json index 0d5fb586cc8..49ec796088a 100644 --- a/homeassistant/components/cast/translations/sv.json +++ b/homeassistant/components/cast/translations/sv.json @@ -3,13 +3,26 @@ "abort": { "single_instance_allowed": "Endast en enda konfiguration av Google Cast \u00e4r n\u00f6dv\u00e4ndig." }, + "error": { + "invalid_known_hosts": "K\u00e4nda v\u00e4rdar m\u00e5sta vara en kommaseparerad lista av v\u00e4rdnamn." + }, "step": { + "config": { + "data": { + "known_hosts": "K\u00e4nad v\u00e4rdar" + }, + "description": "K\u00e4nda v\u00e4rdar - En kommaseparerad lista \u00f6ver v\u00e4rdnamn eller IP-adresser f\u00f6r cast-enheter, anv\u00e4nd om mDNS-uppt\u00e4ckt inte fungerar.", + "title": "Google Cast-konfiguration" + }, "confirm": { "description": "Vill du konfigurera Google Cast?" } } }, "options": { + "error": { + "invalid_known_hosts": "K\u00e4nda v\u00e4rdar m\u00e5sta vara en kommaseparerad lista av v\u00e4rdnamn." + }, "step": { "advanced_options": { "data": { diff --git a/homeassistant/components/cert_expiry/translations/sv.json b/homeassistant/components/cert_expiry/translations/sv.json index f00fc236d09..aefa1c45444 100644 --- a/homeassistant/components/cert_expiry/translations/sv.json +++ b/homeassistant/components/cert_expiry/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Tj\u00e4nsten har redan konfigurerats" + "already_configured": "Tj\u00e4nsten har redan konfigurerats", + "import_failed": "Importering av konfiguration misslyckades" }, "error": { "connection_refused": "Anslutningen blev tillbakavisad under anslutning till v\u00e4rd.", diff --git a/homeassistant/components/co2signal/translations/sv.json b/homeassistant/components/co2signal/translations/sv.json new file mode 100644 index 00000000000..0abdae46923 --- /dev/null +++ b/homeassistant/components/co2signal/translations/sv.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "api_ratelimit": "API-hastighetsgr\u00e4nsen har \u00f6verskridits", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "coordinates": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + } + }, + "country": { + "data": { + "country_code": "Landskod" + } + }, + "user": { + "data": { + "api_key": "\u00c5tkomstnyckel", + "location": "H\u00e4mta uppgifter f\u00f6r" + }, + "description": "Bes\u00f6k https://co2signal.com/ f\u00f6r att beg\u00e4ra en token." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/sv.json b/homeassistant/components/coinbase/translations/sv.json index 1ab71904a13..7d6a4a507aa 100644 --- a/homeassistant/components/coinbase/translations/sv.json +++ b/homeassistant/components/coinbase/translations/sv.json @@ -1,17 +1,35 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "api_token": "API-hemlighet" + }, + "title": "Coinbase API-nyckeldetaljer" } } }, "options": { "error": { "currency_unavailable": "En eller flera av de beg\u00e4rda valutasaldona tillhandah\u00e5lls inte av ditt Coinbase API.", - "exchange_rate_unavailable": "En eller flera av de beg\u00e4rda v\u00e4xelkurserna tillhandah\u00e5lls inte av Coinbase." + "exchange_rate_unavailable": "En eller flera av de beg\u00e4rda v\u00e4xelkurserna tillhandah\u00e5lls inte av Coinbase.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "init": { + "data": { + "account_balance_currencies": "Balans i pl\u00e5nboken att rapportera.", + "exchange_rate_currencies": "Valutakurser att rapportera.", + "exchnage_rate_precision": "Antal decimaler f\u00f6r v\u00e4xelkurser." + }, + "description": "Justera Coinbase-alternativ" + } } } } \ No newline at end of file diff --git a/homeassistant/components/control4/translations/sv.json b/homeassistant/components/control4/translations/sv.json index e1ecf8798c1..1ee1c5f34ae 100644 --- a/homeassistant/components/control4/translations/sv.json +++ b/homeassistant/components/control4/translations/sv.json @@ -1,12 +1,29 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "host": "IP-adress", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" + }, + "description": "V\u00e4nligen ange dina Control4-kontouppgifter och IP-adressen till din lokala controller." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Sekunder mellan uppdateringarna" } } } diff --git a/homeassistant/components/coolmaster/translations/sv.json b/homeassistant/components/coolmaster/translations/sv.json index 366295c0966..0d3c7278a7f 100644 --- a/homeassistant/components/coolmaster/translations/sv.json +++ b/homeassistant/components/coolmaster/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Det gick inte att ansluta.", "no_units": "Det gick inte att hitta n\u00e5gra HVAC-enheter i CoolMasterNet-v\u00e4rden." }, "step": { diff --git a/homeassistant/components/coronavirus/translations/sv.json b/homeassistant/components/coronavirus/translations/sv.json index 7e6686c2a04..28a3cbee34f 100644 --- a/homeassistant/components/coronavirus/translations/sv.json +++ b/homeassistant/components/coronavirus/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Detta land \u00e4r redan konfigurerat." + "already_configured": "Detta land \u00e4r redan konfigurerat.", + "cannot_connect": "Det gick inte att ansluta." }, "step": { "user": { diff --git a/homeassistant/components/cover/translations/sv.json b/homeassistant/components/cover/translations/sv.json index 624b5102d82..f2ef369ae4f 100644 --- a/homeassistant/components/cover/translations/sv.json +++ b/homeassistant/components/cover/translations/sv.json @@ -2,7 +2,12 @@ "device_automation": { "action_type": { "close": "St\u00e4ng {entity_name}", - "open": "\u00d6ppna {entity_name}" + "close_tilt": "St\u00e4ng {entity_name} lutning", + "open": "\u00d6ppna {entity_name}", + "open_tilt": "\u00d6ppna {entity_name} lutning", + "set_position": "S\u00e4tt {entity_name} position", + "set_tilt_position": "S\u00e4tt {entity_name} lutningsposition", + "stop": "Stoppa {entity_name}" }, "condition_type": { "is_closed": "{entity_name} \u00e4r st\u00e4ngd", diff --git a/homeassistant/components/crownstone/translations/sv.json b/homeassistant/components/crownstone/translations/sv.json new file mode 100644 index 00000000000..69519c18dc0 --- /dev/null +++ b/homeassistant/components/crownstone/translations/sv.json @@ -0,0 +1,75 @@ +{ + "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "usb_setup_complete": "Crownstone USB-installation klar.", + "usb_setup_unsuccessful": "Crownstone USB-installationen misslyckades." + }, + "error": { + "account_not_verified": "Kontot \u00e4r inte verifierat. V\u00e4nligen aktivera ditt konto via aktiveringsmailet fr\u00e5n Crownstone.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "usb_config": { + "data": { + "usb_path": "USB-enhetens s\u00f6kv\u00e4g" + }, + "description": "V\u00e4lj den seriella porten p\u00e5 Crownstone USB-dongeln, eller v\u00e4lj \"Anv\u00e4nd inte USB\" om du inte vill st\u00e4lla in en USB-dongel. \n\n Leta efter en enhet med VID 10C4 och PID EA60.", + "title": "Crownstone USB-dongelkonfiguration" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "USB-enhetens s\u00f6kv\u00e4g" + }, + "description": "Ange s\u00f6kv\u00e4gen f\u00f6r en Crownstone USB-dongel manuellt.", + "title": "Crownstone USB-dongel manuell v\u00e4g" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "V\u00e4lj en Crownstone Sphere d\u00e4r USB-enheten finns.", + "title": "Crownstone USB Sphere" + }, + "user": { + "data": { + "email": "E-post", + "password": "L\u00f6senord" + }, + "title": "Crownstone konto" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "usb_sphere_option": "Crownstone Sphere d\u00e4r USB-enheten finns", + "use_usb_option": "Anv\u00e4nd en Crownstone USB-dongel f\u00f6r lokal data\u00f6verf\u00f6ring" + } + }, + "usb_config": { + "data": { + "usb_path": "USB-enhetens s\u00f6kv\u00e4g" + }, + "description": "V\u00e4lj serieporten p\u00e5 Crownstone USB-dongeln. \n\n Leta efter en enhet med VID 10C4 och PID EA60.", + "title": "Crownstone USB-dongelkonfiguration" + }, + "usb_manual_config": { + "data": { + "usb_manual_path": "USB-enhetens s\u00f6kv\u00e4g" + }, + "description": "Ange s\u00f6kv\u00e4gen f\u00f6r en Crownstone USB-dongel manuellt.", + "title": "Crownstone USB-dongel manuell v\u00e4g" + }, + "usb_sphere_config": { + "data": { + "usb_sphere": "Crownstone Sphere" + }, + "description": "V\u00e4lj en Crownstone Sphere d\u00e4r USB-enheten finns.", + "title": "Crownstone USB Sphere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/sv.json b/homeassistant/components/daikin/translations/sv.json index 03b7358ee1b..1ea73051e9b 100644 --- a/homeassistant/components/daikin/translations/sv.json +++ b/homeassistant/components/daikin/translations/sv.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" }, "step": { "user": { diff --git a/homeassistant/components/deconz/translations/sv.json b/homeassistant/components/deconz/translations/sv.json index fd0968a941a..774144c5ba3 100644 --- a/homeassistant/components/deconz/translations/sv.json +++ b/homeassistant/components/deconz/translations/sv.json @@ -25,12 +25,18 @@ "host": "V\u00e4rd", "port": "Port" } + }, + "user": { + "data": { + "host": "V\u00e4lj uppt\u00e4ckt deCONZ-gateway" + } } } }, "device_automation": { "trigger_subtype": { "both_buttons": "B\u00e5da knapparna", + "bottom_buttons": "Bottenknappar", "button_1": "F\u00f6rsta knappen", "button_2": "Andra knappen", "button_3": "Tredje knappen", @@ -51,6 +57,7 @@ "side_4": "Sida 4", "side_5": "Sida 5", "side_6": "Sida 6", + "top_buttons": "Toppknappar", "turn_off": "St\u00e4ng av", "turn_on": "Starta" }, @@ -62,6 +69,7 @@ "remote_button_quadruple_press": "\"{subtype}\"-knappen klickades \nfyrfaldigt", "remote_button_quintuple_press": "\"{subtype}\"-knappen klickades \nfemfaldigt", "remote_button_rotated": "Knappen roterade \"{subtype}\"", + "remote_button_rotated_fast": "Knappen roterades snabbt \" {subtype} \"", "remote_button_rotation_stopped": "Knapprotationen \"{subtype}\" stoppades", "remote_button_short_press": "\"{subtype}\"-knappen trycktes in", "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt", diff --git a/homeassistant/components/demo/translations/no.json b/homeassistant/components/demo/translations/no.json index 48da80fd629..7cbf50d76bb 100644 --- a/homeassistant/components/demo/translations/no.json +++ b/homeassistant/components/demo/translations/no.json @@ -1,4 +1,25 @@ { + "issues": { + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Trykk OK n\u00e5r blinklysv\u00e6ske er fylt p\u00e5 igjen", + "title": "Blinkerv\u00e6ske m\u00e5 etterfylles" + } + } + }, + "title": "Blinklysv\u00e6sken er tom og m\u00e5 etterfylles" + }, + "transmogrifier_deprecated": { + "description": "Transmogrifier-komponenten er n\u00e5 avviklet p\u00e5 grunn av mangelen p\u00e5 lokal kontroll tilgjengelig i det nye API-et", + "title": "Transmogrifier-komponenten er utdatert" + }, + "unfixable_problem": { + "description": "Denne saken kommer aldri til \u00e5 gi opp.", + "title": "Dette er ikke et problem som kan fikses" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/sv.json b/homeassistant/components/demo/translations/sv.json index 55872e8c81e..d88f06a3f99 100644 --- a/homeassistant/components/demo/translations/sv.json +++ b/homeassistant/components/demo/translations/sv.json @@ -31,6 +31,7 @@ "options_1": { "data": { "bool": "Valfritt boolesk", + "constant": "Konstant", "int": "Numerisk inmatning" } }, diff --git a/homeassistant/components/denonavr/translations/sv.json b/homeassistant/components/denonavr/translations/sv.json new file mode 100644 index 00000000000..f27ff32b05c --- /dev/null +++ b/homeassistant/components/denonavr/translations/sv.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen, att koppla bort n\u00e4t- och ethernetkablar och \u00e5teransluta dem kan hj\u00e4lpa" + }, + "step": { + "confirm": { + "description": "Bekr\u00e4fta att du l\u00e4gger till receivern" + }, + "select": { + "data": { + "select_host": "Receiverns IP-adress" + }, + "description": "K\u00f6r installationen igen om du vill ansluta ytterligare mottagare", + "title": "V\u00e4lj receivern du vill ansluta till" + }, + "user": { + "data": { + "host": "IP-adress" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_all_sources": "Visa alla k\u00e4llor", + "update_audyssey": "Uppdatera Audyssey-inst\u00e4llningarna", + "zone2": "St\u00e4ll in zon 2", + "zone3": "St\u00e4ll in zon 3" + }, + "description": "Ange valfria inst\u00e4llningar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/translations/sv.json b/homeassistant/components/device_tracker/translations/sv.json index 7ef1cc7b2f8..cff85228a59 100644 --- a/homeassistant/components/device_tracker/translations/sv.json +++ b/homeassistant/components/device_tracker/translations/sv.json @@ -3,6 +3,10 @@ "condition_type": { "is_home": "{entity_name} \u00e4r hemma", "is_not_home": "{entity_name} \u00e4r inte hemma" + }, + "trigger_type": { + "enters": "{entity_name} g\u00e5r in i en zon", + "leaves": "{entity_name} l\u00e4mnar en zon" } }, "state": { diff --git a/homeassistant/components/devolo_home_control/translations/sv.json b/homeassistant/components/devolo_home_control/translations/sv.json index 13e09780b40..6727e5c5107 100644 --- a/homeassistant/components/devolo_home_control/translations/sv.json +++ b/homeassistant/components/devolo_home_control/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/dexcom/translations/sv.json b/homeassistant/components/dexcom/translations/sv.json index 23c825f256f..d82f098eb83 100644 --- a/homeassistant/components/dexcom/translations/sv.json +++ b/homeassistant/components/dexcom/translations/sv.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/dialogflow/translations/sv.json b/homeassistant/components/dialogflow/translations/sv.json index ebae7e612d0..daf0f4f3ea6 100644 --- a/homeassistant/components/dialogflow/translations/sv.json +++ b/homeassistant/components/dialogflow/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", "webhook_not_internet_accessible": "Din Home Assistant instans m\u00e5ste kunna n\u00e5s fr\u00e5n Internet f\u00f6r att ta emot webhook meddelanden" }, "create_entry": { diff --git a/homeassistant/components/directv/translations/sv.json b/homeassistant/components/directv/translations/sv.json index c42c03d9944..91275d8c75a 100644 --- a/homeassistant/components/directv/translations/sv.json +++ b/homeassistant/components/directv/translations/sv.json @@ -1,11 +1,13 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "unknown": "Ov\u00e4ntat fel" }, "error": { "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen" }, + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "Do vill du konfigurera {name}?" diff --git a/homeassistant/components/doorbird/translations/sv.json b/homeassistant/components/doorbird/translations/sv.json index 56c44dee6fb..d91cc55a3be 100644 --- a/homeassistant/components/doorbird/translations/sv.json +++ b/homeassistant/components/doorbird/translations/sv.json @@ -1,8 +1,16 @@ { "config": { - "error": { - "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen" + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "link_local_address": "Lokala l\u00e4nkadresser st\u00f6ds inte", + "not_doorbird_device": "Den h\u00e4r enheten \u00e4r inte en DoorBird" }, + "error": { + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { @@ -13,5 +21,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "events": "Kommaseparerad lista \u00f6ver h\u00e4ndelser." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/sv.json b/homeassistant/components/dsmr/translations/sv.json new file mode 100644 index 00000000000..38b86e964b6 --- /dev/null +++ b/homeassistant/components/dsmr/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "Minsta tid mellan enhetsuppdateringar [s]" + }, + "title": "DSMR-alternativ" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dunehd/translations/sv.json b/homeassistant/components/dunehd/translations/sv.json new file mode 100644 index 00000000000..b4d134814a2 --- /dev/null +++ b/homeassistant/components/dunehd/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Se till att din spelare \u00e4r p\u00e5slagen." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/sv.json b/homeassistant/components/elgato/translations/sv.json index 4ed0161b0e3..57fd302ab90 100644 --- a/homeassistant/components/elgato/translations/sv.json +++ b/homeassistant/components/elgato/translations/sv.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Den h\u00e4r Elgato Key Light-enheten \u00e4r redan konfigurerad." + "already_configured": "Den h\u00e4r Elgato Key Light-enheten \u00e4r redan konfigurerad.", + "cannot_connect": "Det gick inte att ansluta." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." }, "flow_title": "Elgato Key Light: {serial_number}", "step": { diff --git a/homeassistant/components/elkm1/translations/sv.json b/homeassistant/components/elkm1/translations/sv.json index 8765d95baf6..305329feff4 100644 --- a/homeassistant/components/elkm1/translations/sv.json +++ b/homeassistant/components/elkm1/translations/sv.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "address_already_configured": "En ElkM1 med denna adress \u00e4r redan konfigurerad", + "already_configured": "En ElkM1 med detta prefix \u00e4r redan konfigurerad", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "error": { "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", "invalid_auth": "Ogiltig autentisering", @@ -16,6 +22,10 @@ "protocol": "Protokoll", "username": "Anv\u00e4ndarnamn" } + }, + "user": { + "description": "V\u00e4lj ett uppt\u00e4ckt system eller \"Manuell inmatning\" om inga enheter har uppt\u00e4ckts.", + "title": "Anslut till Elk-M1 Control" } } } diff --git a/homeassistant/components/emonitor/translations/sv.json b/homeassistant/components/emonitor/translations/sv.json index c5ad71d784d..d7081bafe9c 100644 --- a/homeassistant/components/emonitor/translations/sv.json +++ b/homeassistant/components/emonitor/translations/sv.json @@ -6,6 +6,18 @@ "error": { "cannot_connect": "Kunde inte ansluta", "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vill du konfigurera {name} ({host})?", + "title": "St\u00e4ll in SiteSage Emonitor" + }, + "user": { + "data": { + "host": "V\u00e4rd" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/sv.json b/homeassistant/components/emulated_roku/translations/sv.json index ddf62b50df1..f186cfe0f76 100644 --- a/homeassistant/components/emulated_roku/translations/sv.json +++ b/homeassistant/components/emulated_roku/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/enocean/translations/sv.json b/homeassistant/components/enocean/translations/sv.json new file mode 100644 index 00000000000..8a881b2a9a2 --- /dev/null +++ b/homeassistant/components/enocean/translations/sv.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "invalid_dongle_path": "Ogiltig s\u00f6kv\u00e4g f\u00f6r dongle", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "invalid_dongle_path": "Ingen giltig dongel hittades f\u00f6r denna s\u00f6kv\u00e4g" + }, + "step": { + "detect": { + "data": { + "path": "USB-dongle-s\u00f6kv\u00e4g" + }, + "title": "V\u00e4lj s\u00f6kv\u00e4g till din ENOcean dongle" + }, + "manual": { + "data": { + "path": "USB-dongle-s\u00f6kv\u00e4g" + }, + "title": "Ange s\u00f6kv\u00e4gen till din ENOcean-dongel" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/sv.json b/homeassistant/components/enphase_envoy/translations/sv.json index ecc6740fc9d..3889eae836b 100644 --- a/homeassistant/components/enphase_envoy/translations/sv.json +++ b/homeassistant/components/enphase_envoy/translations/sv.json @@ -1,15 +1,19 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Kunde inte ansluta", + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{serial} ({host})", "step": { "user": { "data": { + "host": "V\u00e4rd", "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } diff --git a/homeassistant/components/epson/translations/sv.json b/homeassistant/components/epson/translations/sv.json index e7ec27624a5..45224016263 100644 --- a/homeassistant/components/epson/translations/sv.json +++ b/homeassistant/components/epson/translations/sv.json @@ -2,6 +2,14 @@ "config": { "error": { "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/firmata/translations/sv.json b/homeassistant/components/firmata/translations/sv.json new file mode 100644 index 00000000000..46631acc69a --- /dev/null +++ b/homeassistant/components/firmata/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "Det gick inte att ansluta." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/sv.json b/homeassistant/components/flick_electric/translations/sv.json index 2957bed953a..9c6855a6c81 100644 --- a/homeassistant/components/flick_electric/translations/sv.json +++ b/homeassistant/components/flick_electric/translations/sv.json @@ -1,13 +1,22 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, "error": { - "invalid_auth": "Ogiltig autentisering" + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "client_id": "Klient ID (valfritt)", + "client_secret": "Klient Nyckel (valfritt)", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Flick Autentiseringsuppgifter" } } } diff --git a/homeassistant/components/flipr/translations/sv.json b/homeassistant/components/flipr/translations/sv.json new file mode 100644 index 00000000000..ec835585137 --- /dev/null +++ b/homeassistant/components/flipr/translations/sv.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "no_flipr_id_found": "Inget flipr-ID kopplat till ditt konto f\u00f6r tillf\u00e4llet. Du b\u00f6r f\u00f6rst verifiera att den fungerar med Fliprs mobilapp.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "flipr_id": { + "data": { + "flipr_id": "Flipr ID" + }, + "description": "V\u00e4lj ditt Flipr-ID i listan", + "title": "V\u00e4lj din Flipr" + }, + "user": { + "data": { + "email": "E-post", + "password": "L\u00f6senord" + }, + "description": "Anslut med ditt Flipr-konto." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/sv.json b/homeassistant/components/flo/translations/sv.json index 78879942876..a07a2c509bc 100644 --- a/homeassistant/components/flo/translations/sv.json +++ b/homeassistant/components/flo/translations/sv.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/flume/translations/sv.json b/homeassistant/components/flume/translations/sv.json index f4fdb861fc7..768f499b2ac 100644 --- a/homeassistant/components/flume/translations/sv.json +++ b/homeassistant/components/flume/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Det h\u00e4r kontot har redan konfigurerats." + "already_configured": "Det h\u00e4r kontot har redan konfigurerats.", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", @@ -9,6 +10,13 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "L\u00f6senordet f\u00f6r {username} \u00e4r inte l\u00e4ngre giltigt.", + "title": "Autentisera ditt Flume-konto igen" + }, "user": { "data": { "client_id": "Klient ID", diff --git a/homeassistant/components/flunearyou/translations/sv.json b/homeassistant/components/flunearyou/translations/sv.json index e39e45a3ec0..eb41d4ff78f 100644 --- a/homeassistant/components/flunearyou/translations/sv.json +++ b/homeassistant/components/flunearyou/translations/sv.json @@ -8,7 +8,9 @@ "data": { "latitude": "Latitud", "longitude": "Longitud" - } + }, + "description": "\u00d6vervaka anv\u00e4ndarbaserade och CDC-rapporter f\u00f6r ett par koordinater.", + "title": "Konfigurera influensa i n\u00e4rheten av dig" } } } diff --git a/homeassistant/components/forecast_solar/translations/sv.json b/homeassistant/components/forecast_solar/translations/sv.json index fceb441190b..8a1e0911c8d 100644 --- a/homeassistant/components/forecast_solar/translations/sv.json +++ b/homeassistant/components/forecast_solar/translations/sv.json @@ -3,7 +3,24 @@ "step": { "user": { "data": { - "modules power": "Total maxeffekt (Watt) p\u00e5 dina solpaneler" + "azimuth": "Azimuth (360 grader, 0 = norr, 90 = \u00f6st, 180 = s\u00f6der, 270 = v\u00e4ster)", + "declination": "Deklination (0 = horisontell, 90 = vertikal)", + "latitude": "Latitud", + "longitude": "Longitud", + "modules power": "Total maxeffekt (Watt) p\u00e5 dina solpaneler", + "name": "Namn" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "api_key": "API-nyckel f\u00f6r Forecast.Solar (valfritt)", + "azimuth": "Azimuth (360 grader, 0 = norr, 90 = \u00f6st, 180 = s\u00f6der, 270 = v\u00e4ster)", + "damping": "D\u00e4mpningsfaktor: justerar resultaten p\u00e5 morgonen och kv\u00e4llen", + "declination": "Deklination (0 = horisontell, 90 = vertikal)" } } } diff --git a/homeassistant/components/forked_daapd/translations/sv.json b/homeassistant/components/forked_daapd/translations/sv.json index 80f45e2d887..3e36427c525 100644 --- a/homeassistant/components/forked_daapd/translations/sv.json +++ b/homeassistant/components/forked_daapd/translations/sv.json @@ -1,10 +1,32 @@ { "config": { + "error": { + "wrong_server_type": "Forked-daapd-integrationen kr\u00e4ver en forked-daapd-server med version > = 27.0." + }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { + "host": "V\u00e4rd", + "name": "Eget namn", + "password": "API-l\u00f6senord (l\u00e4mna tomt om inget l\u00f6senord)", "port": "API port" - } + }, + "title": "Konfigurera forked-daapd-enhet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "Port f\u00f6r librespot-java pipe control (om s\u00e5dan anv\u00e4nds)", + "max_playlists": "Max antal spellistor som anv\u00e4nds som k\u00e4llor", + "tts_pause_time": "Sekunder att pausa f\u00f6re och efter TTS", + "tts_volume": "TTS-volym (flytande inom intervallet [0,1])" + }, + "description": "St\u00e4ll in olika alternativ f\u00f6r forked-daapd-integreringen.", + "title": "Konfigurera alternativ f\u00f6r forked-daapd" } } } diff --git a/homeassistant/components/foscam/translations/sv.json b/homeassistant/components/foscam/translations/sv.json index 78879942876..9de4793eabe 100644 --- a/homeassistant/components/foscam/translations/sv.json +++ b/homeassistant/components/foscam/translations/sv.json @@ -1,9 +1,20 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "host": "V\u00e4rd", "password": "L\u00f6senord", + "port": "Port", + "stream": "Str\u00f6m", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/freebox/translations/sv.json b/homeassistant/components/freebox/translations/sv.json index aa43ee66032..ed8ebe67ae9 100644 --- a/homeassistant/components/freebox/translations/sv.json +++ b/homeassistant/components/freebox/translations/sv.json @@ -9,6 +9,9 @@ "unknown": "Ok\u00e4nt fel: f\u00f6rs\u00f6k igen senare" }, "step": { + "link": { + "title": "L\u00e4nka Freebox-router" + }, "user": { "data": { "host": "V\u00e4rd", diff --git a/homeassistant/components/freedompro/translations/sv.json b/homeassistant/components/freedompro/translations/sv.json index 9feab4808f7..83f1c6b3dac 100644 --- a/homeassistant/components/freedompro/translations/sv.json +++ b/homeassistant/components/freedompro/translations/sv.json @@ -1,13 +1,18 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" }, "step": { "user": { "data": { "api_key": "API-nyckel" - } + }, + "title": "Freedompro API-nyckel" } } } diff --git a/homeassistant/components/fritz/translations/sv.json b/homeassistant/components/fritz/translations/sv.json index 89c6cb84221..5afcb156965 100644 --- a/homeassistant/components/fritz/translations/sv.json +++ b/homeassistant/components/fritz/translations/sv.json @@ -3,6 +3,9 @@ "abort": { "ignore_ip6_link_local": "IPv6-l\u00e4nkens lokala adress st\u00f6ds inte." }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "confirm": { "data": { @@ -12,13 +15,24 @@ "reauth_confirm": { "data": { "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Uppdaterar FRITZ!Box Tools - referenser" }, "user": { "data": { + "host": "V\u00e4rd", "username": "Anv\u00e4ndarnamn" } } } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Sekunder att \u00f6verv\u00e4ga en enhet hemma" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/sv.json b/homeassistant/components/fritzbox/translations/sv.json index 347aeeecc4a..a028ec5f217 100644 --- a/homeassistant/components/fritzbox/translations/sv.json +++ b/homeassistant/components/fritzbox/translations/sv.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "ignore_ip6_link_local": "IPv6-l\u00e4nkens lokala adress st\u00f6ds inte." + "ignore_ip6_link_local": "IPv6-l\u00e4nkens lokala adress st\u00f6ds inte.", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "invalid_auth": "Ogiltig autentisering" }, "step": { "confirm": { @@ -13,8 +18,10 @@ }, "reauth_confirm": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Uppdatera din inloggningsinformation f\u00f6r {name} ." }, "user": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/sv.json b/homeassistant/components/fritzbox_callmonitor/translations/sv.json index 23c825f256f..7a0fbc4f3f6 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/sv.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/sv.json @@ -1,11 +1,41 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "insufficient_permissions": "Anv\u00e4ndaren har otillr\u00e4ckliga beh\u00f6righeter f\u00f6r att komma \u00e5t AVM FRITZ!Box-inst\u00e4llningarna och dess telefonb\u00f6cker.", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket" + }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, + "flow_title": "{name}", "step": { + "phonebook": { + "data": { + "phonebook": "Telefonbok" + } + }, "user": { "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "port": "Port", "username": "Anv\u00e4ndarnamn" } } } + }, + "options": { + "error": { + "malformed_prefixes": "Prefix \u00e4r felaktiga, kontrollera deras format." + }, + "step": { + "init": { + "data": { + "prefixes": "Prefix (kommaseparerad lista)" + }, + "title": "Konfigurera prefix" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/generic/translations/cs.json b/homeassistant/components/generic/translations/cs.json index 8ef4333b872..73c3e470129 100644 --- a/homeassistant/components/generic/translations/cs.json +++ b/homeassistant/components/generic/translations/cs.json @@ -13,7 +13,8 @@ "data": { "password": "Heslo", "username": "U\u017eivatelsk\u00e9 jm\u00e9no" - } + }, + "description": "Zadejte nastaven\u00ed pro p\u0159ipojen\u00ed ke kame\u0159e." } } }, diff --git a/homeassistant/components/generic/translations/no.json b/homeassistant/components/generic/translations/no.json index 5c718dc0f47..23319f0a938 100644 --- a/homeassistant/components/generic/translations/no.json +++ b/homeassistant/components/generic/translations/no.json @@ -54,6 +54,7 @@ "invalid_still_image": "URL returnerte ikke et gyldig stillbilde", "malformed_url": "Feil utforming p\u00e5 URL", "no_still_image_or_stream_url": "Du m\u00e5 angi minst en URL-adresse for stillbilde eller dataflyt", + "relative_url": "Relative URL-adresser ikke tillatt", "stream_file_not_found": "Filen ble ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m (er ffmpeg installert?)", "stream_http_not_found": "HTTP 404 Ikke funnet under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m", "stream_io_error": "Inn-/utdatafeil under fors\u00f8k p\u00e5 \u00e5 koble til str\u00f8m. Feil RTSP-transportprotokoll?", diff --git a/homeassistant/components/generic/translations/sv.json b/homeassistant/components/generic/translations/sv.json index 020b0093092..9a4067bbcfc 100644 --- a/homeassistant/components/generic/translations/sv.json +++ b/homeassistant/components/generic/translations/sv.json @@ -1,14 +1,25 @@ { "config": { "abort": { - "no_devices_found": "Inga enheter hittades i n\u00e4tverket" + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, "error": { "malformed_url": "Ogiltig URL", "relative_url": "Relativa URL:er \u00e4r inte till\u00e5tna", - "template_error": "Problem att rendera mall. Kolla i loggen f\u00f6r mer information." + "stream_io_error": "Inmatnings-/utg\u00e5ngsfel vid f\u00f6rs\u00f6k att ansluta till stream. Fel RTSP-transportprotokoll?", + "stream_no_video": "Str\u00f6mmen har ingen video", + "stream_not_permitted": "\u00c5tg\u00e4rden \u00e4r inte till\u00e5ten n\u00e4r du f\u00f6rs\u00f6ker ansluta till streamen. Fel RTSP-transportprotokoll?", + "stream_unauthorised": "Auktoriseringen misslyckades n\u00e4r du f\u00f6rs\u00f6kte ansluta till str\u00f6mmen", + "template_error": "Problem att rendera mall. Kolla i loggen f\u00f6r mer information.", + "timeout": "Timeout vid h\u00e4mtning fr\u00e5n URL", + "unable_still_load": "Det g\u00e5r inte att ladda giltig bild fr\u00e5n stillbilds-URL (t.ex. ogiltig v\u00e4rd, URL eller autentiseringsfel). Granska loggen f\u00f6r mer information.", + "unknown": "Ov\u00e4ntat fel" }, "step": { + "confirm": { + "description": "Vill du starta konfigurationen?" + }, "content_type": { "data": { "content_type": "Inneh\u00e5llstyp" @@ -18,10 +29,16 @@ "user": { "data": { "authentication": "Autentiseringen", + "framerate": "Bildfrekvens (Hz)", + "limit_refetch_to_url_change": "Begr\u00e4nsa \u00e5terh\u00e4mtning till \u00e4ndring av webbadress", "password": "L\u00f6senord", "rtsp_transport": "RTSP transportprotokoll", - "username": "Anv\u00e4ndarnamn" - } + "still_image_url": "URL f\u00f6r stillbild (t.ex. http://...)", + "stream_source": "URL f\u00f6r str\u00f6mk\u00e4lla (t.ex. rtsp://...)", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera SSL-certifikat" + }, + "description": "Skriv in inst\u00e4llningarna f\u00f6r att ansluta till kameran." } } }, @@ -29,7 +46,8 @@ "error": { "malformed_url": "Ogiltig URL", "relative_url": "Relativa URL:er \u00e4r inte till\u00e5tet", - "template_error": "Problem att rendera mall. Kolla i loggen f\u00f6r mer information." + "template_error": "Problem att rendera mall. Kolla i loggen f\u00f6r mer information.", + "timeout": "Timeout vid h\u00e4mtning fr\u00e5n URL" }, "step": { "content_type": { @@ -41,8 +59,12 @@ "init": { "data": { "authentication": "Autentiseringen", + "rtsp_transport": "RTSP transportprotokoll", + "still_image_url": "URL f\u00f6r stillbild (t.ex. http://...)", + "stream_source": "URL f\u00f6r str\u00f6mk\u00e4lla (t.ex. rtsp://...)", "use_wallclock_as_timestamps": "Anv\u00e4nd v\u00e4ggklocka som tidsst\u00e4mplar", - "username": "Anv\u00e4ndarnamn" + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera SSL-certifikat" }, "data_description": { "use_wallclock_as_timestamps": "Det h\u00e4r alternativet kan korrigera segmenteringsproblem eller kraschproblem som uppst\u00e5r p\u00e5 grund av felaktig implementering av tidsst\u00e4mplar p\u00e5 vissa kameror." diff --git a/homeassistant/components/geofency/translations/sv.json b/homeassistant/components/geofency/translations/sv.json index 8b48a30ce8b..5565034ff74 100644 --- a/homeassistant/components/geofency/translations/sv.json +++ b/homeassistant/components/geofency/translations/sv.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "cloud_not_connected": "Ej ansluten till Home Assistant Cloud." + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." }, "create_entry": { "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera webhook funktionen i Geofency.\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n\nSe [dokumentation]({docs_url}) om hur du konfigurerar detta f\u00f6r mer information." diff --git a/homeassistant/components/gios/translations/sv.json b/homeassistant/components/gios/translations/sv.json index 98e9333c821..c93367a393b 100644 --- a/homeassistant/components/gios/translations/sv.json +++ b/homeassistant/components/gios/translations/sv.json @@ -17,5 +17,10 @@ "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" } } + }, + "system_health": { + "info": { + "can_reach_server": "N\u00e5 GIO\u015a-servern" + } } } \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/sv.json b/homeassistant/components/gogogate2/translations/sv.json index f7461922566..b34d63016f2 100644 --- a/homeassistant/components/gogogate2/translations/sv.json +++ b/homeassistant/components/gogogate2/translations/sv.json @@ -1,13 +1,22 @@ { "config": { - "error": { + "abort": { "cannot_connect": "Det gick inte att ansluta." }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, + "flow_title": "{device} ({ip_address})", "step": { "user": { "data": { + "ip_address": "IP-adress", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange n\u00f6dv\u00e4ndig information nedan.", + "title": "St\u00e4ll in Gogogate2 eller ismartgate" } } } diff --git a/homeassistant/components/google/translations/hu.json b/homeassistant/components/google/translations/hu.json index b27e06b15c7..467b1a663f2 100644 --- a/homeassistant/components/google/translations/hu.json +++ b/homeassistant/components/google/translations/hu.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "A Google Napt\u00e1r konfigur\u00e1l\u00e1sa a configuration.yaml f\u00e1jlban a 2022.9-es Home Assistant verzi\u00f3ban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 OAuth alkalmaz\u00e1s hiteles\u00edt\u0151 adatai \u00e9s hozz\u00e1f\u00e9r\u00e9si be\u00e1ll\u00edt\u00e1sai automatikusan import\u00e1l\u00e1sra ker\u00fcltek a felhaszn\u00e1l\u00f3i fel\u00fcletbe. A probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Google Calendar YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + }, + "removed_track_new_yaml": { + "description": "A configuration.yaml f\u00e1jlban a Google Calendar sz\u00e1m\u00e1ra az entit\u00e1sk\u00f6vet\u00e9s ki lett kapcsolva, ami m\u00e1r nem t\u00e1mogatott. Manu\u00e1lisan sz\u00fcks\u00e9ges m\u00f3dos\u00edtani az integr\u00e1ci\u00f3s rendszerbe\u00e1ll\u00edt\u00e1sokat a felhaszn\u00e1l\u00f3i fel\u00fcleten, hogy a j\u00f6v\u0151ben letiltsa az \u00fajonnan felfedezett entit\u00e1sokat. A probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a track_new be\u00e1ll\u00edt\u00e1st a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Google Napt\u00e1r entit\u00e1sk\u00f6vet\u00e9se megv\u00e1ltozott" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/no.json b/homeassistant/components/google/translations/no.json index 9842da0362c..55103db2a47 100644 --- a/homeassistant/components/google/translations/no.json +++ b/homeassistant/components/google/translations/no.json @@ -11,7 +11,8 @@ "invalid_access_token": "Ugyldig tilgangstoken", "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "oauth_error": "Mottatt ugyldige token data.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "timeout_connect": "Tidsavbrudd oppretter forbindelse" }, "create_entry": { "default": "Vellykket godkjenning" @@ -32,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Google Kalender i configuration.yaml blir fjernet i Home Assistant 2022.9. \n\n Din eksisterende OAuth-applikasjonslegitimasjon og tilgangsinnstillinger er automatisk importert til brukergrensesnittet. Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Google Kalender YAML-konfigurasjonen blir fjernet" + }, + "removed_track_new_yaml": { + "description": "Du har deaktivert enhetssporing for Google Kalender i configuration.yaml, som ikke lenger st\u00f8ttes. Du m\u00e5 manuelt endre integreringssystemalternativene i brukergrensesnittet for \u00e5 deaktivere nyoppdagede enheter fremover. Fjern track_new-innstillingen fra configuration.yaml og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Google Kalender-enhetssporing er endret" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google_travel_time/translations/sv.json b/homeassistant/components/google_travel_time/translations/sv.json index 1b8cc14bce7..fc9d03286e1 100644 --- a/homeassistant/components/google_travel_time/translations/sv.json +++ b/homeassistant/components/google_travel_time/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Platsen \u00e4r redan konfigurerad" + }, "error": { "cannot_connect": "Kunde inte ansluta" }, @@ -8,8 +11,10 @@ "data": { "api_key": "API-nyckel", "destination": "Destination", + "name": "Namn", "origin": "Ursprung" - } + }, + "description": "N\u00e4r du anger ursprung och destination kan du ange en eller flera platser \u00e5tskilda av pipe, i form av en adress, latitud/longitudkoordinater eller ett plats-ID fr\u00e5n Google. N\u00e4r du anger platsen med hj\u00e4lp av ett plats-ID fr\u00e5n Google m\u00e5ste ID:t ha prefixet \"place_id:\"." } } }, @@ -19,10 +24,16 @@ "data": { "avoid": "Undvik", "language": "Spr\u00e5k", + "mode": "Resel\u00e4ge", "time": "Tid", + "time_type": "Tidstyp", + "transit_mode": "Transportmedel", + "transit_routing_preference": "Inst\u00e4llning f\u00f6r kollektivtrafik", "units": "Enheter" - } + }, + "description": "Du kan valfritt ange antingen en avg\u00e5ngstid eller ankomsttid. Om du anger en avg\u00e5ngstid kan du ange \"nu\", en Unix-tidsst\u00e4mpel eller en 24-timmars tidsstr\u00e4ng som \"08:00:00\". Om du anger en ankomsttid kan du anv\u00e4nda en Unix-tidsst\u00e4mpel eller en 24-timmars tidsstr\u00e4ng som \"08:00:00\"" } } - } + }, + "title": "Google Maps restid" } \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/no.json b/homeassistant/components/govee_ble/translations/no.json new file mode 100644 index 00000000000..4fd1e1d0c9d --- /dev/null +++ b/homeassistant/components/govee_ble/translations/no.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/translations/sv.json b/homeassistant/components/gpslogger/translations/sv.json index 40b54557d80..f73ba63337d 100644 --- a/homeassistant/components/gpslogger/translations/sv.json +++ b/homeassistant/components/gpslogger/translations/sv.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." + }, "create_entry": { "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera webhook funktionen i GPSLogger.\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n\nSe [dokumentation]({docs_url}) om hur du konfigurerar detta f\u00f6r mer information." }, diff --git a/homeassistant/components/gree/translations/sv.json b/homeassistant/components/gree/translations/sv.json new file mode 100644 index 00000000000..18a80850e45 --- /dev/null +++ b/homeassistant/components/gree/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "confirm": { + "description": "Vill du starta konfigurationen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/sv.json b/homeassistant/components/growatt_server/translations/sv.json index 23c825f256f..090e7f7ac15 100644 --- a/homeassistant/components/growatt_server/translations/sv.json +++ b/homeassistant/components/growatt_server/translations/sv.json @@ -1,11 +1,28 @@ { "config": { + "abort": { + "no_plants": "Inga v\u00e4xter har hittats p\u00e5 detta konto" + }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, "step": { + "plant": { + "data": { + "plant_id": "Anl\u00e4ggning" + }, + "title": "V\u00e4lj din anl\u00e4ggning" + }, "user": { "data": { + "name": "Namn", + "password": "L\u00f6senord", + "url": "URL", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Ange din Growatt-information" } } - } + }, + "title": "Growatt-server" } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/sv.json b/homeassistant/components/guardian/translations/sv.json new file mode 100644 index 00000000000..54fcf49904d --- /dev/null +++ b/homeassistant/components/guardian/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "discovery_confirm": { + "description": "Vill du konfigurera denna Guardian-enhet?" + }, + "user": { + "data": { + "ip_address": "IP-adress", + "port": "Port" + }, + "description": "Konfigurera en lokal Elexa Guardian-enhet." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/sv.json b/homeassistant/components/habitica/translations/sv.json index f4a63bb449d..bb924d55b3c 100644 --- a/homeassistant/components/habitica/translations/sv.json +++ b/homeassistant/components/habitica/translations/sv.json @@ -1,10 +1,18 @@ { "config": { + "error": { + "invalid_credentials": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "api_user": "Habiticas API-anv\u00e4ndar-ID", + "name": "\u00d6verstyrning f\u00f6r Habiticas anv\u00e4ndarnamn. Kommer att anv\u00e4ndas f\u00f6r serviceanrop.", + "url": "URL" + }, + "description": "Anslut din Habitica-profil f\u00f6r att till\u00e5ta \u00f6vervakning av din anv\u00e4ndares profil och uppgifter. Observera att api_id och api_key m\u00e5ste h\u00e4mtas fr\u00e5n https://habitica.com/user/settings/api" } } } diff --git a/homeassistant/components/harmony/translations/sv.json b/homeassistant/components/harmony/translations/sv.json index 1f240051056..15d8b2c1dfa 100644 --- a/homeassistant/components/harmony/translations/sv.json +++ b/homeassistant/components/harmony/translations/sv.json @@ -7,20 +7,28 @@ "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{name}", "step": { "link": { - "description": "Do vill du konfigurera {name} ({host})?" + "description": "Do vill du konfigurera {name} ({host})?", + "title": "Konfigurera Logitech Harmony Hub" }, "user": { "data": { - "host": "V\u00e4rdnamn eller IP-adress" - } + "host": "V\u00e4rdnamn eller IP-adress", + "name": "Namn p\u00e5 hubben" + }, + "title": "Konfigurera Logitech Harmony Hub" } } }, "options": { "step": { "init": { + "data": { + "activity": "Standardaktivitet som ska utf\u00f6ras n\u00e4r ingen aktivitet har angetts.", + "delay_secs": "F\u00f6rdr\u00f6jningen mellan att skicka kommandon." + }, "description": "Justera inst\u00e4llningarna f\u00f6r Harmony Hub" } } diff --git a/homeassistant/components/hassio/translations/sv.json b/homeassistant/components/hassio/translations/sv.json index 7d3d7684558..2fbad6d4915 100644 --- a/homeassistant/components/hassio/translations/sv.json +++ b/homeassistant/components/hassio/translations/sv.json @@ -1,7 +1,19 @@ { "system_health": { "info": { - "agent_version": "Agentversion" + "agent_version": "Agentversion", + "board": "Kort", + "disk_total": "Total disk", + "disk_used": "Disk som anv\u00e4nds", + "docker_version": "Docker-version", + "healthy": "Frisk", + "host_os": "V\u00e4rdens operativsystem", + "installed_addons": "Installerade till\u00e4gg", + "supervisor_api": "Supervisor API", + "supervisor_version": "Supervisor version", + "supported": "St\u00f6ds", + "update_channel": "Uppdatera kanal", + "version_api": "Version API" } } } \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/sv.json b/homeassistant/components/here_travel_time/translations/sv.json index bb0f36a448f..6d0085d9c0f 100644 --- a/homeassistant/components/here_travel_time/translations/sv.json +++ b/homeassistant/components/here_travel_time/translations/sv.json @@ -49,7 +49,8 @@ "user": { "data": { "api_key": "API-nyckel", - "mode": "Resel\u00e4ge" + "mode": "Resel\u00e4ge", + "name": "Namn" } } } diff --git a/homeassistant/components/hive/translations/hu.json b/homeassistant/components/hive/translations/hu.json index 8a265ff63c0..fa56592b9ab 100644 --- a/homeassistant/components/hive/translations/hu.json +++ b/homeassistant/components/hive/translations/hu.json @@ -41,7 +41,7 @@ "scan_interval": "Szkennel\u00e9si intervallum (m\u00e1sodperc)", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Adja meg a Hive bejelentkez\u00e9si adatait \u00e9s konfigur\u00e1ci\u00f3j\u00e1t.", + "description": "Adja meg a Hive bejelentkez\u00e9si adatait.", "title": "Hive Bejelentkez\u00e9s" } } diff --git a/homeassistant/components/hive/translations/sv.json b/homeassistant/components/hive/translations/sv.json index 60b51beb78b..a80808c91df 100644 --- a/homeassistant/components/hive/translations/sv.json +++ b/homeassistant/components/hive/translations/sv.json @@ -1,9 +1,25 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades", + "unknown_entry": "Det gick inte att hitta befintlig post." + }, "error": { + "invalid_code": "Det gick inte att logga in p\u00e5 Hive. Din tv\u00e5faktorsautentiseringskod var felaktig.", + "invalid_password": "Det gick inte att logga in p\u00e5 Hive. Felaktigt l\u00f6senord. Var sn\u00e4ll och f\u00f6rs\u00f6k igen.", + "invalid_username": "Det gick inte att logga in p\u00e5 Hive. Din e-postadress k\u00e4nns inte igen.", + "no_internet_available": "En internetanslutning kr\u00e4vs f\u00f6r att ansluta till Hive.", "unknown": "Ov\u00e4ntat fel" }, "step": { + "2fa": { + "data": { + "2fa": "Tv\u00e5faktorskod" + }, + "description": "Ange din Hive-autentiseringskod. \n\n Ange kod 0000 f\u00f6r att beg\u00e4ra en annan kod.", + "title": "Hive tv\u00e5faktorsautentisering." + }, "configuration": { "data": { "device_name": "Enhetsnamn" @@ -15,13 +31,29 @@ "data": { "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange din Hive-inloggningsinformation igen.", + "title": "Hive-inloggning" }, "user": { "data": { "password": "L\u00f6senord", + "scan_interval": "Skanningsintervall (sekunder)", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange din Hive-inloggningsinformation.", + "title": "Hive-inloggning" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Skanningsintervall (sekunder)" + }, + "description": "Uppdatera skanningsintervallet f\u00f6r att polla efter data oftare.", + "title": "Alternativ f\u00f6r Hive" } } } diff --git a/homeassistant/components/hlk_sw16/translations/sv.json b/homeassistant/components/hlk_sw16/translations/sv.json index eba844f6c03..f85a02855d6 100644 --- a/homeassistant/components/hlk_sw16/translations/sv.json +++ b/homeassistant/components/hlk_sw16/translations/sv.json @@ -1,9 +1,19 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { - "host": "V\u00e4rd" + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "username": "Anv\u00e4ndarnamn" } } } diff --git a/homeassistant/components/home_connect/translations/sv.json b/homeassistant/components/home_connect/translations/sv.json new file mode 100644 index 00000000000..9f710ef442c --- /dev/null +++ b/homeassistant/components/home_connect/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" + }, + "create_entry": { + "default": "Autentiserats" + }, + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_plus_control/translations/sv.json b/homeassistant/components/home_plus_control/translations/sv.json index 5307b489a72..3f43cd0559c 100644 --- a/homeassistant/components/home_plus_control/translations/sv.json +++ b/homeassistant/components/home_plus_control/translations/sv.json @@ -1,5 +1,16 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "create_entry": { + "default": "Autentiserats" + }, "step": { "pick_implementation": { "title": "V\u00e4lj autentiseringsmetod" diff --git a/homeassistant/components/homeassistant/translations/sv.json b/homeassistant/components/homeassistant/translations/sv.json index e4778a3a9e0..b0f67a5754e 100644 --- a/homeassistant/components/homeassistant/translations/sv.json +++ b/homeassistant/components/homeassistant/translations/sv.json @@ -1,7 +1,9 @@ { "system_health": { "info": { - "config_dir": "Konfigurationskatalog" + "config_dir": "Konfigurationskatalog", + "hassio": "Supervisor", + "user": "Anv\u00e4ndare" } } } \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/hu.json b/homeassistant/components/homeassistant_alerts/translations/hu.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/hu.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/sv.json b/homeassistant/components/homekit/translations/sv.json index be1e79453e8..250bb634736 100644 --- a/homeassistant/components/homekit/translations/sv.json +++ b/homeassistant/components/homekit/translations/sv.json @@ -5,12 +5,23 @@ "title": "Para HomeKit" }, "user": { + "data": { + "include_domains": "Dom\u00e4ner att inkludera" + }, + "description": "V\u00e4lj de dom\u00e4ner som ska inkluderas. Alla st\u00f6dda enheter i dom\u00e4nen kommer att inkluderas f\u00f6rutom kategoriserade enheter. En separat HomeKit-instans i tillbeh\u00f6rsl\u00e4ge kommer att skapas f\u00f6r varje tv-mediaspelare, aktivitetsbaserad fj\u00e4rrkontroll, l\u00e5s och kamera.", "title": "Aktivera HomeKit" } } }, "options": { "step": { + "advanced": { + "data": { + "devices": "Enheter (utl\u00f6sare)" + }, + "description": "Programmerbara switchar skapas f\u00f6r varje vald enhet. N\u00e4r en enhetsutl\u00f6sare utl\u00f6ses kan HomeKit konfigureras f\u00f6r att k\u00f6ra en automatisering eller scen.", + "title": "Avancerad konfiguration" + }, "cameras": { "data": { "camera_copy": "Kameror som st\u00f6der inbyggda H.264-str\u00f6mmar" @@ -19,7 +30,15 @@ "title": "V\u00e4lj kamerans videoavkodare." }, "init": { + "data": { + "mode": "HomeKit-l\u00e4ge" + }, + "description": "HomeKit kan konfigureras f\u00f6r att exponera en bro eller ett enskilt tillbeh\u00f6r. I tillbeh\u00f6rsl\u00e4get kan endast en enda enhet anv\u00e4ndas. Tillbeh\u00f6rsl\u00e4ge kr\u00e4vs f\u00f6r att mediaspelare med enhetsklassen TV ska fungera korrekt. Enheter i \"Dom\u00e4ner att inkludera\" kommer att inkluderas i HomeKit. Du kommer att kunna v\u00e4lja vilka enheter som ska inkluderas eller uteslutas fr\u00e5n listan p\u00e5 n\u00e4sta sk\u00e4rm.", "title": "V\u00e4lj dom\u00e4ner som ska inkluderas." + }, + "yaml": { + "description": "Denna post styrs via YAML", + "title": "Justera HomeKit-alternativ" } } } diff --git a/homeassistant/components/homekit_controller/translations/hu.json b/homeassistant/components/homekit_controller/translations/hu.json index c8f86e46f38..9bf7922e3e0 100644 --- a/homeassistant/components/homekit_controller/translations/hu.json +++ b/homeassistant/components/homekit_controller/translations/hu.json @@ -18,7 +18,7 @@ "unable_to_pair": "Nem siker\u00fclt p\u00e1ros\u00edtani, pr\u00f3b\u00e1ld \u00fajra.", "unknown_error": "Az eszk\u00f6z ismeretlen hib\u00e1t jelentett. A p\u00e1ros\u00edt\u00e1s sikertelen." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Sz\u00fcntesse meg a p\u00e1ros\u00edt\u00e1st az \u00f6sszes vez\u00e9rl\u0151n, vagy pr\u00f3b\u00e1lja \u00fajraind\u00edtani az eszk\u00f6zt, majd folytassa a p\u00e1ros\u00edt\u00e1st.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "P\u00e1ros\u00edt\u00e1s enged\u00e9lyez\u00e9se a nem biztons\u00e1gos be\u00e1ll\u00edt\u00e1si k\u00f3dokkal.", "pairing_code": "P\u00e1ros\u00edt\u00e1si k\u00f3d" }, - "description": "A HomeKit Controller {name} n\u00e9vvel kommunik\u00e1l a helyi h\u00e1l\u00f3zaton kereszt\u00fcl, biztons\u00e1gos titkos\u00edtott kapcsolaton kereszt\u00fcl, k\u00fcl\u00f6n HomeKit vez\u00e9rl\u0151 vagy iCloud n\u00e9lk\u00fcl. A tartoz\u00e9k haszn\u00e1lat\u00e1hoz adja meg HomeKit p\u00e1ros\u00edt\u00e1si k\u00f3dj\u00e1t (XXX-XX-XXX form\u00e1tumban). Ez a k\u00f3d \u00e1ltal\u00e1ban mag\u00e1ban az eszk\u00f6z\u00f6n vagy a csomagol\u00e1sban tal\u00e1lhat\u00f3.", + "description": "A HomeKit Controller {name} ({category}) n\u00e9vvel kommunik\u00e1l a helyi h\u00e1l\u00f3zaton kereszt\u00fcl, biztons\u00e1gos titkos\u00edtott kapcsolaton kereszt\u00fcl, k\u00fcl\u00f6n HomeKit vez\u00e9rl\u0151 vagy iCloud n\u00e9lk\u00fcl. Az eszk\u00f6z haszn\u00e1lat\u00e1hoz adja meg HomeKit p\u00e1ros\u00edt\u00e1si k\u00f3dj\u00e1t (XXX-XX-XXX form\u00e1tumban). Ez a k\u00f3d \u00e1ltal\u00e1ban mag\u00e1ban az eszk\u00f6z\u00f6n vagy a csomagol\u00e1sban tal\u00e1lhat\u00f3.", "title": "P\u00e1ros\u00edt\u00e1s egy eszk\u00f6zzel a HomeKit Accessory Protocol protokollon seg\u00edts\u00e9g\u00e9vel" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/sv.json b/homeassistant/components/homekit_controller/translations/sv.json index 348c0305e03..e5a05b62b84 100644 --- a/homeassistant/components/homekit_controller/translations/sv.json +++ b/homeassistant/components/homekit_controller/translations/sv.json @@ -7,6 +7,7 @@ "already_paired": "Det h\u00e4r tillbeh\u00f6ret \u00e4r redan kopplat till en annan enhet. \u00c5terst\u00e4ll tillbeh\u00f6ret och f\u00f6rs\u00f6k igen.", "ignored_model": "HomeKit-st\u00f6d f\u00f6r den h\u00e4r modellen blockeras eftersom en mer komplett inbyggd integration \u00e4r tillg\u00e4nglig.", "invalid_config_entry": "Den h\u00e4r enheten visas som redo att paras ihop, men det finns redan en motstridig konfigurations-post f\u00f6r den i Home Assistant som f\u00f6rst m\u00e5ste tas bort.", + "invalid_properties": "Ogiltiga egenskaper har meddelats av enheten.", "no_devices": "Inga oparade enheter kunde hittas" }, "error": { @@ -18,6 +19,13 @@ }, "flow_title": "HomeKit-tillbeh\u00f6r: {name}", "step": { + "busy_error": { + "description": "Avbryt ihopparningen p\u00e5 alla kontroller eller f\u00f6rs\u00f6k starta om enheten och forts\u00e4tt sedan f\u00f6r att \u00e5teruppta ihopparningen.", + "title": "Enheten paras redan med en annan styrenhet" + }, + "max_tries_error": { + "title": "Maximalt antal autentiseringsf\u00f6rs\u00f6k har \u00f6verskridits" + }, "pair": { "data": { "pairing_code": "Parningskod" @@ -25,6 +33,10 @@ "description": "HomeKit Controller kommunicerar med {name} \u00f6ver det lokala n\u00e4tverket med hj\u00e4lp av en s\u00e4ker krypterad anslutning utan en separat HomeKit-kontroller eller iCloud. Ange din HomeKit-kopplingskod (i formatet XXX-XX-XXX) f\u00f6r att anv\u00e4nda detta tillbeh\u00f6r. Denna kod finns vanligtvis p\u00e5 sj\u00e4lva enheten eller i f\u00f6rpackningen.", "title": "Para HomeKit-tillbeh\u00f6r" }, + "protocol_error": { + "description": "Enheten kanske inte \u00e4r i ihopparningsl\u00e4ge och kan kr\u00e4va en fysisk eller virtuell knapptryckning. Se till att enheten \u00e4r i ihopparningsl\u00e4ge eller f\u00f6rs\u00f6k starta om enheten och forts\u00e4tt sedan f\u00f6r att \u00e5teruppta ihopparningen.", + "title": "Fel vid kommunikation med tillbeh\u00f6ret" + }, "user": { "data": { "device": "Enhet" @@ -47,6 +59,11 @@ "button8": "Knapp 8", "button9": "Knapp 9", "doorbell": "D\u00f6rrklocka" + }, + "trigger_type": { + "double_press": "\" {subtype} \" tryckt tv\u00e5 g\u00e5nger", + "long_press": "\"{subtyp}\" tryckt och h\u00e5llen", + "single_press": "\" {subtype} \" tryckt" } }, "title": "HomeKit-tillbeh\u00f6r" diff --git a/homeassistant/components/huawei_lte/translations/sv.json b/homeassistant/components/huawei_lte/translations/sv.json index 83cbe335b2d..4efe112d468 100644 --- a/homeassistant/components/huawei_lte/translations/sv.json +++ b/homeassistant/components/huawei_lte/translations/sv.json @@ -7,10 +7,13 @@ "connection_timeout": "Timeout f\u00f6r anslutning", "incorrect_password": "Felaktigt l\u00f6senord", "incorrect_username": "Felaktigt anv\u00e4ndarnamn", + "invalid_auth": "Ogiltig autentisering", "invalid_url": "Ogiltig URL", "login_attempts_exceeded": "Maximala inloggningsf\u00f6rs\u00f6k har \u00f6verskridits, f\u00f6rs\u00f6k igen senare", - "response_error": "Ok\u00e4nt fel fr\u00e5n enheten" + "response_error": "Ok\u00e4nt fel fr\u00e5n enheten", + "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{name}", "step": { "user": { "data": { @@ -27,7 +30,9 @@ "init": { "data": { "name": "Namn p\u00e5 meddelandetj\u00e4nsten (\u00e4ndring kr\u00e4ver omstart)", - "recipient": "Mottagare av SMS-meddelanden" + "recipient": "Mottagare av SMS-meddelanden", + "track_wired_clients": "Sp\u00e5ra tr\u00e5dbundna n\u00e4tverksklienter", + "unauthenticated_mode": "Oautentiserat l\u00e4ge (\u00e4ndring kr\u00e4ver omladdning)" } } } diff --git a/homeassistant/components/hue/translations/sv.json b/homeassistant/components/hue/translations/sv.json index c4687726357..1cf157f02e5 100644 --- a/homeassistant/components/hue/translations/sv.json +++ b/homeassistant/components/hue/translations/sv.json @@ -36,13 +36,26 @@ "button_4": "Fj\u00e4rde knappen", "dim_down": "Dimma ned", "dim_up": "Dimma upp", + "double_buttons_1_3": "F\u00f6rsta och tredje knapparna", + "double_buttons_2_4": "Andra och fj\u00e4rde knapparna", "turn_off": "St\u00e4ng av", "turn_on": "Starta" }, "trigger_type": { "remote_button_long_release": "\"{subtype}\" knappen sl\u00e4pptes efter ett l\u00e5ngt tryck", "remote_button_short_press": "\"{subtype}\" knappen nedtryckt", - "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt" + "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt", + "remote_double_button_long_press": "B\u00e5da \"{subtype}\" sl\u00e4pptes efter en l\u00e5ngtryckning", + "remote_double_button_short_press": "B\u00e5da \"{subtyp}\" sl\u00e4pptes" + } + }, + "options": { + "step": { + "init": { + "data": { + "allow_hue_groups": "Till\u00e5t Hue-grupper" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/sv.json b/homeassistant/components/humidifier/translations/sv.json index 2818d7b7b04..a500d9dc18e 100644 --- a/homeassistant/components/humidifier/translations/sv.json +++ b/homeassistant/components/humidifier/translations/sv.json @@ -1,12 +1,28 @@ { "device_automation": { + "action_type": { + "set_humidity": "St\u00e4ll in luftfuktighet f\u00f6r {entity_name}", + "set_mode": "\u00c4ndra l\u00e4ge p\u00e5 {entity_name}", + "toggle": "V\u00e4xla {entity_name}", + "turn_off": "St\u00e4ng av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + }, + "condition_type": { + "is_mode": "{entity_name} \u00e4r inst\u00e4lld p\u00e5 ett specifikt l\u00e4ge", + "is_off": "{entity_name} \u00e4r avst\u00e4ngd", + "is_on": "{entity_name} \u00e4r p\u00e5" + }, "trigger_type": { - "turned_off": "{entity_name} st\u00e4ngdes av" + "target_humidity_changed": "{entity_name} m\u00e5lfuktighet har \u00e4ndrats", + "turned_off": "{entity_name} st\u00e4ngdes av", + "turned_on": "{entity_name} slogs p\u00e5" } }, "state": { "_": { + "off": "Av", "on": "P\u00e5" } - } + }, + "title": "Luftfuktare" } \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/sv.json b/homeassistant/components/hvv_departures/translations/sv.json index 3a2983d1035..c1621e16f2c 100644 --- a/homeassistant/components/hvv_departures/translations/sv.json +++ b/homeassistant/components/hvv_departures/translations/sv.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "no_results": "Inga resultat. F\u00f6rs\u00f6k med en annan station/adress" }, "step": { "user": { diff --git a/homeassistant/components/hyperion/translations/sv.json b/homeassistant/components/hyperion/translations/sv.json new file mode 100644 index 00000000000..56cee0c4ad2 --- /dev/null +++ b/homeassistant/components/hyperion/translations/sv.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "auth_new_token_not_granted_error": "Nyskapad token godk\u00e4ndes inte p\u00e5 Hyperion UI", + "auth_new_token_not_work_error": "Det gick inte att autentisera med nyskapad token", + "auth_required_error": "Det gick inte att avg\u00f6ra om auktorisering kr\u00e4vs", + "cannot_connect": "Det gick inte att ansluta.", + "no_id": "Hyperion Ambilight-instansen rapporterade inte sitt id", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_access_token": "Ogiltig \u00e5tkomstnyckel" + }, + "step": { + "auth": { + "data": { + "create_token": "Skapa ny token automatiskt", + "token": "Eller ange redan existerande token" + }, + "description": "Konfigurera auktorisering till din Hyperion Ambilight-server" + }, + "confirm": { + "title": "Bekr\u00e4fta till\u00e4gg av Hyperion Ambilight-tj\u00e4nst" + }, + "create_token": { + "description": "V\u00e4lj **Skicka** nedan f\u00f6r att beg\u00e4ra en ny autentiseringstoken. Du kommer att omdirigeras till Hyperion UI f\u00f6r att godk\u00e4nna beg\u00e4ran. Kontrollera att det visade id:t \u00e4r \" {auth_id} \"", + "title": "Skapa automatiskt ny autentiseringstoken" + }, + "create_token_external": { + "title": "Acceptera ny token i Hyperion UI" + }, + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "effect_show_list": "Hyperion-effekter att visa", + "priority": "Hyperion prioritet att anv\u00e4nda f\u00f6r f\u00e4rger och effekter" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ialarm/translations/sv.json b/homeassistant/components/ialarm/translations/sv.json new file mode 100644 index 00000000000..17bab09c61a --- /dev/null +++ b/homeassistant/components/ialarm/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/sv.json b/homeassistant/components/icloud/translations/sv.json index 2bba72d49df..5aa6e9574f8 100644 --- a/homeassistant/components/icloud/translations/sv.json +++ b/homeassistant/components/icloud/translations/sv.json @@ -19,7 +19,8 @@ "user": { "data": { "password": "L\u00f6senord", - "username": "Email" + "username": "Email", + "with_family": "Med familj" }, "description": "Ange dina autentiseringsuppgifter", "title": "iCloud-autentiseringsuppgifter" diff --git a/homeassistant/components/ifttt/translations/sv.json b/homeassistant/components/ifttt/translations/sv.json index 57837a63b15..16ec51d62c2 100644 --- a/homeassistant/components/ifttt/translations/sv.json +++ b/homeassistant/components/ifttt/translations/sv.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." + }, "create_entry": { "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du anv\u00e4nda \u00e5tg\u00e4rden \"G\u00f6r en webbf\u00f6rfr\u00e5gan\" fr\u00e5n [IFTTT Webhook applet] ( {applet_url} ).\n\n Fyll i f\u00f6ljande information:\n \n - URL: ` {webhook_url} `\n - Metod: POST\n - Inneh\u00e5llstyp: application / json\n\n Se [dokumentationen] ( {docs_url} ) om hur du konfigurerar automatiseringar f\u00f6r att hantera inkommande data." }, diff --git a/homeassistant/components/inkbird/translations/no.json b/homeassistant/components/inkbird/translations/no.json new file mode 100644 index 00000000000..3cf7f2b76c8 --- /dev/null +++ b/homeassistant/components/inkbird/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/sv.json b/homeassistant/components/insteon/translations/sv.json index 6f3f3666b12..218b8d6d748 100644 --- a/homeassistant/components/insteon/translations/sv.json +++ b/homeassistant/components/insteon/translations/sv.json @@ -17,10 +17,51 @@ }, "options": { "step": { + "add_override": { + "data": { + "cat": "Enhetskategori (dvs. 0x10)", + "subcat": "Enhetsunderkategori (dvs. 0x0a)" + }, + "description": "L\u00e4gg till en enhets\u00e5sidos\u00e4ttning." + }, + "add_x10": { + "data": { + "housecode": "Huskod (a - p)", + "platform": "Plattform", + "steps": "Dimmersteg (endast f\u00f6r ljusanordningar, standardv\u00e4rde 22)", + "unitcode": "Enhetskod (1 - 16)" + }, + "description": "\u00c4ndra l\u00f6senordet f\u00f6r Insteon Hub." + }, "change_hub_config": { "data": { + "host": "IP-adress", + "password": "L\u00f6senord", + "port": "Port", "username": "Anv\u00e4ndarnamn" + }, + "description": "\u00c4ndra Insteon Hub-anslutningsinformationen. Du m\u00e5ste starta om Home Assistant efter att ha gjort denna \u00e4ndring. Detta \u00e4ndrar inte konfigurationen av sj\u00e4lva hubben. Anv\u00e4nd Hub-appen f\u00f6r att \u00e4ndra konfigurationen i Hubben." + }, + "init": { + "data": { + "add_override": "L\u00e4gg till en enhets\u00e5sidos\u00e4ttning.", + "add_x10": "L\u00e4gg till en X10-enhet.", + "change_hub_config": "\u00c4ndra Hub-konfigurationen.", + "remove_override": "Ta bort en \u00e5sidos\u00e4ttning av en enhet.", + "remove_x10": "Ta bort en X10-enhet." } + }, + "remove_override": { + "data": { + "address": "V\u00e4lj en enhetsadress att ta bort" + }, + "description": "Ta bort en enhets\u00e5sidos\u00e4ttning" + }, + "remove_x10": { + "data": { + "address": "V\u00e4lj en enhetsadress att ta bort" + }, + "description": "Ta bort en X10-enhet" } } } diff --git a/homeassistant/components/integration/translations/sv.json b/homeassistant/components/integration/translations/sv.json index 4665a9b8816..e0c12be775f 100644 --- a/homeassistant/components/integration/translations/sv.json +++ b/homeassistant/components/integration/translations/sv.json @@ -18,5 +18,6 @@ } } } - } + }, + "title": "Integration - Riemann summa integral sensor" } \ No newline at end of file diff --git a/homeassistant/components/iotawatt/translations/sv.json b/homeassistant/components/iotawatt/translations/sv.json index d1d69759ba6..500a65ce603 100644 --- a/homeassistant/components/iotawatt/translations/sv.json +++ b/homeassistant/components/iotawatt/translations/sv.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "auth": { "data": { diff --git a/homeassistant/components/ipma/translations/sv.json b/homeassistant/components/ipma/translations/sv.json index 1606c8cb5ed..908b8f91721 100644 --- a/homeassistant/components/ipma/translations/sv.json +++ b/homeassistant/components/ipma/translations/sv.json @@ -15,5 +15,10 @@ "title": "Location" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "IPMA API-slutpunkt kan n\u00e5s" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/sv.json b/homeassistant/components/ipp/translations/sv.json index 70b270e63a2..2096cf5648c 100644 --- a/homeassistant/components/ipp/translations/sv.json +++ b/homeassistant/components/ipp/translations/sv.json @@ -2,12 +2,15 @@ "config": { "abort": { "already_configured": "Den h\u00e4r skrivaren \u00e4r redan konfigurerad.", + "cannot_connect": "Det gick inte att ansluta.", "connection_upgrade": "Misslyckades att ansluta till skrivaren d\u00e5 anslutningen beh\u00f6ver uppgraderas.", "ipp_error": "IPP-fel p\u00e5tr\u00e4ffades.", "ipp_version_error": "IPP versionen st\u00f6ds inte av skrivaren", - "parse_error": "Det gick inte att f\u00f6rst\u00e5 responsen fr\u00e5n skrivaren" + "parse_error": "Det gick inte att f\u00f6rst\u00e5 responsen fr\u00e5n skrivaren", + "unique_id_required": "Enheten saknar unik identifiering som kr\u00e4vs f\u00f6r uppt\u00e4ckt." }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "connection_upgrade": "Kunde inte ansluta till skrivaren. F\u00f6rs\u00f6k igen med SSL/TLS alternativet ifyllt." }, "flow_title": "Skrivare: {name}", diff --git a/homeassistant/components/islamic_prayer_times/translations/sv.json b/homeassistant/components/islamic_prayer_times/translations/sv.json new file mode 100644 index 00000000000..f865c5a2c6a --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/sv.json b/homeassistant/components/isy994/translations/sv.json index bd14c77dd60..334c3e0fb0d 100644 --- a/homeassistant/components/isy994/translations/sv.json +++ b/homeassistant/components/isy994/translations/sv.json @@ -1,8 +1,16 @@ { "config": { - "error": { - "reauth_successful": "\u00c5terautentiseringen lyckades" + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "invalid_host": "V\u00e4rdposten var inte i fullst\u00e4ndigt URL-format, t.ex. http://192.168.10.100:80", + "reauth_successful": "\u00c5terautentiseringen lyckades", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name} ({host})", "step": { "reauth_confirm": { "data": { @@ -14,8 +22,26 @@ }, "user": { "data": { + "host": "URL", + "password": "L\u00f6senord", + "tls": "TLS-versionen av ISY-styrenheten.", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "V\u00e4rdposten m\u00e5ste vara i fullst\u00e4ndigt URL-format, t.ex. http://192.168.10.100:80", + "title": "Anslut till din ISY" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "Ignorera str\u00e4ng", + "restore_light_state": "\u00c5terst\u00e4ll ljusstyrkan", + "sensor_string": "Nodsensorstr\u00e4ng" + }, + "description": "St\u00e4ll in alternativen f\u00f6r ISY-integration:\n \u2022 Nodsensorstr\u00e4ng: Alla enheter eller mappar som inneh\u00e5ller 'Nodsensorstr\u00e4ng' i namnet kommer att behandlas som en sensor eller bin\u00e4r sensor.\n \u2022 Ignorera str\u00e4ng: Alla enheter med 'Ignorera str\u00e4ng' i namnet kommer att ignoreras.\n \u2022 Variabel sensorstr\u00e4ng: Varje variabel som inneh\u00e5ller 'Variabel sensorstr\u00e4ng' kommer att l\u00e4ggas till som en sensor.\n \u2022 \u00c5terst\u00e4ll ljusstyrka: Om den \u00e4r aktiverad kommer den tidigare ljusstyrkan att \u00e5terst\u00e4llas n\u00e4r du sl\u00e5r p\u00e5 en lampa ist\u00e4llet f\u00f6r enhetens inbyggda On-Level.", + "title": "ISY alternativ" } } } diff --git a/homeassistant/components/keenetic_ndms2/translations/sv.json b/homeassistant/components/keenetic_ndms2/translations/sv.json new file mode 100644 index 00000000000..df37b421209 --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/sv.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "port": "Port", + "username": "Anv\u00e4ndarnamn" + }, + "title": "Konfigurera Keenetic NDMS2 Router" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "include_arp": "Anv\u00e4nd ARP-data (ignoreras om hotspot-data anv\u00e4nds)", + "include_associated": "Anv\u00e4nd WiFi AP-associationsdata (ignoreras om hotspotdata anv\u00e4nds)", + "interfaces": "V\u00e4lj gr\u00e4nssnitt att skanna", + "try_hotspot": "Anv\u00e4nd \"ip hotspot\"-data (mest korrekt)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/sv.json b/homeassistant/components/kmtronic/translations/sv.json index a265d988aaa..8ec6cc04dc9 100644 --- a/homeassistant/components/kmtronic/translations/sv.json +++ b/homeassistant/components/kmtronic/translations/sv.json @@ -8,5 +8,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "reverse": "Logik f\u00f6r omv\u00e4nd omkoppling (anv\u00e4nd NC)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/knx/translations/sv.json b/homeassistant/components/knx/translations/sv.json index 7c4d6ee8e61..f5986f966d0 100644 --- a/homeassistant/components/knx/translations/sv.json +++ b/homeassistant/components/knx/translations/sv.json @@ -41,7 +41,9 @@ "user_password": "Anv\u00e4ndarl\u00f6senord" }, "data_description": { - "device_authentication": "Detta st\u00e4lls in i 'IP'-panelen i gr\u00e4nssnittet i ETS." + "device_authentication": "Detta st\u00e4lls in i 'IP'-panelen i gr\u00e4nssnittet i ETS.", + "user_id": "Detta \u00e4r ofta tunnelnummer +1. S\u00e5 'Tunnel 2' skulle ha anv\u00e4ndar-ID '3'.", + "user_password": "L\u00f6senord f\u00f6r den specifika tunnelanslutningen som anges i panelen \"Egenskaper\" i tunneln i ETS." }, "description": "Ange din s\u00e4kra IP-information." }, diff --git a/homeassistant/components/konnected/translations/sv.json b/homeassistant/components/konnected/translations/sv.json index a96f612010a..3e236f05952 100644 --- a/homeassistant/components/konnected/translations/sv.json +++ b/homeassistant/components/konnected/translations/sv.json @@ -32,6 +32,7 @@ "not_konn_panel": "Inte en erk\u00e4nd Konnected.io-enhet" }, "error": { + "bad_host": "Ogiltig \u00e5sidos\u00e4tt API-v\u00e4rd-URL", "one": "Tom", "other": "Tomma" }, @@ -84,7 +85,9 @@ }, "options_misc": { "data": { - "blink": "Blinka p\u00e5 panel-LED n\u00e4r du skickar tillst\u00e5nds\u00e4ndring" + "api_host": "\u00c5sidos\u00e4tt API-v\u00e4rdens URL", + "blink": "Blinka p\u00e5 panel-LED n\u00e4r du skickar tillst\u00e5nds\u00e4ndring", + "discovery": "Svara p\u00e5 uppt\u00e4cktsf\u00f6rfr\u00e5gningar i ditt n\u00e4tverk" }, "description": "V\u00e4lj \u00f6nskat beteende f\u00f6r din panel", "title": "Konfigurera \u00d6vrigt" @@ -93,6 +96,7 @@ "data": { "activation": "Utdata n\u00e4r den \u00e4r p\u00e5", "momentary": "Pulsvarighet (ms) (valfritt)", + "more_states": "Konfigurera ytterligare tillst\u00e5nd f\u00f6r denna zon", "name": "Namn (valfritt)", "pause": "Paus mellan pulser (ms) (valfritt)", "repeat": "G\u00e5nger att upprepa (-1=o\u00e4ndligt) (tillval)" diff --git a/homeassistant/components/kulersky/translations/sv.json b/homeassistant/components/kulersky/translations/sv.json new file mode 100644 index 00000000000..18a80850e45 --- /dev/null +++ b/homeassistant/components/kulersky/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "confirm": { + "description": "Vill du starta konfigurationen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/no.json b/homeassistant/components/lacrosse_view/translations/no.json new file mode 100644 index 00000000000..9cef140da52 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "invalid_auth": "Ugyldig godkjenning", + "no_locations": "Ingen steder funnet", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/hu.json b/homeassistant/components/life360/translations/hu.json index 33f615d00c7..1eec6f50643 100644 --- a/homeassistant/components/life360/translations/hu.json +++ b/homeassistant/components/life360/translations/hu.json @@ -29,7 +29,7 @@ "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, "description": "A speci\u00e1lis be\u00e1ll\u00edt\u00e1sok megad\u00e1s\u00e1hoz l\u00e1sd a [Life360 dokument\u00e1ci\u00f3]({docs_url}) c\u00edm\u0171 r\u00e9szt.\n \u00c9rdemes ezt megtenni a fi\u00f3kok hozz\u00e1ad\u00e1sa el\u0151tt.", - "title": "Life360 fi\u00f3kadatok" + "title": "Life360 fi\u00f3k be\u00e1ll\u00edt\u00e1sa" } } }, diff --git a/homeassistant/components/life360/translations/sv.json b/homeassistant/components/life360/translations/sv.json index 1a5c7f2e569..ac104f24f39 100644 --- a/homeassistant/components/life360/translations/sv.json +++ b/homeassistant/components/life360/translations/sv.json @@ -2,14 +2,17 @@ "config": { "abort": { "already_configured": "Konto har redan konfigurerats", + "invalid_auth": "Ogiltig autentisering", "reauth_successful": "\u00c5terautentisering lyckades" }, "create_entry": { "default": "F\u00f6r att st\u00e4lla in avancerade alternativ, se [Life360 documentation]({docs_url})." }, "error": { + "already_configured": "Konto har redan konfigurerats", "cannot_connect": "Det gick inte att ansluta.", - "invalid_username": "Ogiltigt anv\u00e4ndarnmn" + "invalid_username": "Ogiltigt anv\u00e4ndarnmn", + "unknown": "Ov\u00e4ntat fel" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/lifx/translations/no.json b/homeassistant/components/lifx/translations/no.json index 4771b4c05d7..49ff5dea624 100644 --- a/homeassistant/components/lifx/translations/no.json +++ b/homeassistant/components/lifx/translations/no.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "flow_title": "{label} ( {host} ) {serial}", "step": { "confirm": { "description": "\u00d8nsker du \u00e5 sette opp LIFX?" + }, + "discovery_confirm": { + "description": "Vil du sette opp {label} ( {host} ) {serial} ?" + }, + "pick_device": { + "data": { + "device": "Enhet" + } + }, + "user": { + "data": { + "host": "Vert" + }, + "description": "Hvis du lar verten st\u00e5 tom, vil oppdagelse bli brukt til \u00e5 finne enheter." } } } diff --git a/homeassistant/components/light/translations/sv.json b/homeassistant/components/light/translations/sv.json index d5f0bdaf767..1f32324d707 100644 --- a/homeassistant/components/light/translations/sv.json +++ b/homeassistant/components/light/translations/sv.json @@ -3,6 +3,7 @@ "action_type": { "brightness_decrease": "Minska ljusstyrkan f\u00f6r {entity_name}", "brightness_increase": "\u00d6ka ljusstyrkan f\u00f6r {entity_name}", + "flash": "Blinka {entity_name}", "toggle": "V\u00e4xla {entity_name}", "turn_off": "St\u00e4ng av {entity_name}", "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" diff --git a/homeassistant/components/locative/translations/sv.json b/homeassistant/components/locative/translations/sv.json index 7f1991f8a7b..80da7825d40 100644 --- a/homeassistant/components/locative/translations/sv.json +++ b/homeassistant/components/locative/translations/sv.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." + }, "create_entry": { "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera webhook funktionen i Locative appen.\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n\nSe [dokumentation]({docs_url}) om hur du konfigurerar detta f\u00f6r mer information." }, diff --git a/homeassistant/components/lovelace/translations/sv.json b/homeassistant/components/lovelace/translations/sv.json index b622e036a0f..c0b5bf9f948 100644 --- a/homeassistant/components/lovelace/translations/sv.json +++ b/homeassistant/components/lovelace/translations/sv.json @@ -1,7 +1,8 @@ { "system_health": { "info": { - "dashboards": "Kontrollpaneler" + "dashboards": "Kontrollpaneler", + "views": "Vyer" } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/sv.json b/homeassistant/components/lutron_caseta/translations/sv.json new file mode 100644 index 00000000000..1d512e32d47 --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/sv.json @@ -0,0 +1,36 @@ +{ + "device_automation": { + "trigger_subtype": { + "close_1": "St\u00e4ng 1", + "close_2": "St\u00e4ng 2", + "close_3": "St\u00e4ng 3", + "close_4": "St\u00e4ng 4", + "close_all": "St\u00e4ng alla", + "group_1_button_1": "F\u00f6rsta gruppen f\u00f6rsta knappen", + "group_1_button_2": "F\u00f6rsta gruppen andra knappen", + "group_2_button_1": "Andra gruppen f\u00f6rsta knappen", + "group_2_button_2": "Andra gruppen andra knappen", + "lower": "S\u00e4nk", + "lower_1": "S\u00e4nk 1", + "lower_2": "S\u00e4nk 2", + "lower_3": "S\u00e4nk 3", + "lower_4": "S\u00e4nk 4", + "lower_all": "S\u00e4nk alla", + "off": "Av", + "on": "P\u00e5", + "open_1": "\u00d6ppna 1", + "open_2": "\u00d6ppna 2", + "open_3": "\u00d6ppna 3", + "open_4": "\u00d6ppna 4", + "open_all": "\u00d6ppna alla", + "raise": "H\u00f6j", + "raise_1": "H\u00f6j 1", + "raise_2": "H\u00f6j 2", + "raise_3": "H\u00f6j 3", + "raise_4": "H\u00f6j 4", + "raise_all": "H\u00f6j alla", + "stop": "Stoppa (favorit)", + "stop_1": "Stoppa 1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/hu.json b/homeassistant/components/lyric/translations/hu.json index b7eac97525c..fe958e40ec9 100644 --- a/homeassistant/components/lyric/translations/hu.json +++ b/homeassistant/components/lyric/translations/hu.json @@ -17,5 +17,11 @@ "title": "Az integr\u00e1ci\u00f3 \u00fajb\u00f3li azonos\u00edt\u00e1sa" } } + }, + "issues": { + "removed_yaml": { + "description": "A Honeywell Lyric YAML-ben megadott konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3t a Home Assistant nem haszn\u00e1lja.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Honeywell Lyric YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/no.json b/homeassistant/components/lyric/translations/no.json index 537cc7fcced..e41e3a0c581 100644 --- a/homeassistant/components/lyric/translations/no.json +++ b/homeassistant/components/lyric/translations/no.json @@ -17,5 +17,11 @@ "title": "Godkjenne integrering p\u00e5 nytt" } } + }, + "issues": { + "removed_yaml": { + "description": "Konfigurering av Honeywell Lyric med YAML er fjernet. \n\n Din eksisterende YAML-konfigurasjon brukes ikke av Home Assistant. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Honeywell Lyric YAML-konfigurasjonen er fjernet" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/sv.json b/homeassistant/components/lyric/translations/sv.json index a34370b5ce5..f4f52873240 100644 --- a/homeassistant/components/lyric/translations/sv.json +++ b/homeassistant/components/lyric/translations/sv.json @@ -1,4 +1,23 @@ { + "config": { + "abort": { + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "create_entry": { + "default": "Autentiserats" + }, + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + }, + "reauth_confirm": { + "description": "Lyric-integrationen m\u00e5ste autentisera ditt konto igen.", + "title": "\u00c5terautenticera integration" + } + } + }, "issues": { "removed_yaml": { "description": "Konfigurering av Honeywell Lyric med YAML har tagits bort. \n\n Din befintliga YAML-konfiguration anv\u00e4nds inte av Home Assistant. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", diff --git a/homeassistant/components/mailgun/translations/sv.json b/homeassistant/components/mailgun/translations/sv.json index aec197c766a..3ca0139183c 100644 --- a/homeassistant/components/mailgun/translations/sv.json +++ b/homeassistant/components/mailgun/translations/sv.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." + }, "create_entry": { "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera [Webhooks med Mailgun]({mailgun_url}).\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\n Se [dokumentationen]({docs_url}) om hur du konfigurerar automatiseringar f\u00f6r att hantera inkommande data." }, diff --git a/homeassistant/components/mazda/translations/sv.json b/homeassistant/components/mazda/translations/sv.json new file mode 100644 index 00000000000..24f538688bd --- /dev/null +++ b/homeassistant/components/mazda/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "region": "Region" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/translations/cs.json b/homeassistant/components/media_player/translations/cs.json index 19e88635f8b..0d4840027d1 100644 --- a/homeassistant/components/media_player/translations/cs.json +++ b/homeassistant/components/media_player/translations/cs.json @@ -8,6 +8,7 @@ "is_playing": "{entity_name} p\u0159ehr\u00e1v\u00e1" }, "trigger_type": { + "changed_states": "{entity_name} zm\u011bnil stavy", "paused": "{entity_name} je pozastaveno", "turned_on": "{entity_name} bylo zapnuto" } diff --git a/homeassistant/components/media_player/translations/sv.json b/homeassistant/components/media_player/translations/sv.json index 6aae6d6b208..159ff201fba 100644 --- a/homeassistant/components/media_player/translations/sv.json +++ b/homeassistant/components/media_player/translations/sv.json @@ -9,7 +9,9 @@ "is_playing": "{entity_name} spelar" }, "trigger_type": { - "buffering": "{entity_name} b\u00f6rjar buffra" + "buffering": "{entity_name} b\u00f6rjar buffra", + "turned_off": "{entity_name} st\u00e4ngdes av", + "turned_on": "{entity_name} slogs p\u00e5" } }, "state": { diff --git a/homeassistant/components/met_eireann/translations/sv.json b/homeassistant/components/met_eireann/translations/sv.json index 80cb6773677..754fa336a08 100644 --- a/homeassistant/components/met_eireann/translations/sv.json +++ b/homeassistant/components/met_eireann/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/meteo_france/translations/sv.json b/homeassistant/components/meteo_france/translations/sv.json index f7f1a68478f..53819baddad 100644 --- a/homeassistant/components/meteo_france/translations/sv.json +++ b/homeassistant/components/meteo_france/translations/sv.json @@ -4,11 +4,15 @@ "already_configured": "Staden har redan konfigurerats", "unknown": "Ok\u00e4nt fel: f\u00f6rs\u00f6k igen senare" }, + "error": { + "empty": "Inget resultat i stadss\u00f6kning: kontrollera stadsf\u00e4ltet" + }, "step": { "cities": { "data": { "city": "Stad" - } + }, + "description": "V\u00e4lj din stad fr\u00e5n listan" }, "user": { "data": { @@ -17,5 +21,14 @@ "description": "Ange postnumret (endast f\u00f6r Frankrike, rekommenderat) eller ortsnamn" } } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "Prognosl\u00e4ge" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/hu.json b/homeassistant/components/miflora/translations/hu.json new file mode 100644 index 00000000000..c998a98e9ad --- /dev/null +++ b/homeassistant/components/miflora/translations/hu.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "A Mi Flora integr\u00e1ci\u00f3 a 2022.7-es Home Assistantban le\u00e1llt, \u00e9s a 2022.8-as kiad\u00e1sban a Xiaomi BLE integr\u00e1ci\u00f3val v\u00e1ltotta fel.\n\nNincs lehet\u0151s\u00e9g migr\u00e1ci\u00f3ra, ez\u00e9rt manu\u00e1lisan kell be\u00e1ll\u00edtani a Mi Flora eszk\u00f6zt az \u00faj integr\u00e1ci\u00f3 haszn\u00e1lat\u00e1val.\n\nA megl\u00e9v\u0151 Mi Flora YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant m\u00e1r nem haszn\u00e1lja. A probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Mi Flora integr\u00e1ci\u00f3 lecser\u00e9l\u0151d\u00f6tt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/no.json b/homeassistant/components/miflora/translations/no.json new file mode 100644 index 00000000000..e7e69709307 --- /dev/null +++ b/homeassistant/components/miflora/translations/no.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Mi Flora-integrasjonen sluttet \u00e5 fungere i Home Assistant 2022.7 og erstattet av Xiaomi BLE-integrasjonen i 2022.8-utgivelsen. \n\n Det er ingen migreringsbane mulig, derfor m\u00e5 du legge til Mi Flora-enheten ved \u00e5 bruke den nye integrasjonen manuelt. \n\n Din eksisterende Mi Flora YAML-konfigurasjon brukes ikke lenger av Home Assistant. Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Mi Flora-integrasjonen er erstattet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/sv.json b/homeassistant/components/mill/translations/sv.json index 8cef92a32b9..5c7362eeb7b 100644 --- a/homeassistant/components/mill/translations/sv.json +++ b/homeassistant/components/mill/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "cloud": { "data": { diff --git a/homeassistant/components/mitemp_bt/translations/hu.json b/homeassistant/components/mitemp_bt/translations/hu.json new file mode 100644 index 00000000000..7e6b52a25ee --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/hu.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "A Xiaomi Mijia BLE h\u0151m\u00e9rs\u00e9klet- \u00e9s p\u00e1ratartalom-\u00e9rz\u00e9kel\u0151 integr\u00e1ci\u00f3ja nem le\u00e1llt a 2022.7-es Home Assistantban, \u00e9s a 2022.8-as kiad\u00e1sban a Xiaomi BLE integr\u00e1ci\u00f3val v\u00e1ltott\u00e1k fel.\n\nNincs lehet\u0151s\u00e9g migr\u00e1ci\u00f3ra, ez\u00e9rt a Xiaomi Mijia BLE eszk\u00f6zt az \u00faj integr\u00e1ci\u00f3 haszn\u00e1lat\u00e1val manu\u00e1lisan \u00fajra be kell \u00e1ll\u00edtani.\n\nA megl\u00e9v\u0151 Xiaomi Mijia BLE h\u0151m\u00e9rs\u00e9klet- \u00e9s p\u00e1ratartalom \u00e9rz\u00e9kel\u0151 YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant m\u00e1r nem haszn\u00e1lja. A probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Xiaomi Mijia BLE h\u0151m\u00e9rs\u00e9klet- \u00e9s p\u00e1ratartalom-\u00e9rz\u00e9kel\u0151 integr\u00e1ci\u00f3ja lecser\u00e9l\u0151d\u00f6tt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/no.json b/homeassistant/components/mitemp_bt/translations/no.json new file mode 100644 index 00000000000..248f44b29af --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/no.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Xiaomi Mijia BLE temperatur- og fuktighetssensorintegrasjonen sluttet \u00e5 fungere i Home Assistant 2022.7 og ble erstattet av Xiaomi BLE-integrasjonen i 2022.8-utgivelsen. \n\n Det er ingen migreringsbane mulig, derfor m\u00e5 du legge til Xiaomi Mijia BLE-enheten ved \u00e5 bruke den nye integrasjonen manuelt. \n\n Din eksisterende Xiaomi Mijia BLE temperatur- og fuktighetssensor YAML-konfigurasjon brukes ikke lenger av Home Assistant. Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Xiaomi Mijia BLE temperatur- og fuktighetssensorintegrasjon er erstattet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/cs.json b/homeassistant/components/mjpeg/translations/cs.json index 3fc5acfe8dd..9c6676509bf 100644 --- a/homeassistant/components/mjpeg/translations/cs.json +++ b/homeassistant/components/mjpeg/translations/cs.json @@ -4,6 +4,7 @@ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" }, "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, "step": { diff --git a/homeassistant/components/moat/translations/no.json b/homeassistant/components/moat/translations/no.json new file mode 100644 index 00000000000..bce03ad33d7 --- /dev/null +++ b/homeassistant/components/moat/translations/no.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" + }, + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/sv.json b/homeassistant/components/mobile_app/translations/sv.json index 68473a92763..4fd209e10cf 100644 --- a/homeassistant/components/mobile_app/translations/sv.json +++ b/homeassistant/components/mobile_app/translations/sv.json @@ -8,5 +8,6 @@ "description": "Vill du konfigurera komponenten Mobile App?" } } - } + }, + "title": "Mobilapp" } \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/sv.json b/homeassistant/components/modem_callerid/translations/sv.json new file mode 100644 index 00000000000..584cc33c277 --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga \u00e5terst\u00e5ende enheter hittades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "usb_confirm": { + "description": "Detta \u00e4r en integration f\u00f6r fasta samtal med ett CX93001 r\u00f6stmodem. Detta kan h\u00e4mta nummerpresentationsinformation med ett alternativ att avvisa ett inkommande samtal." + }, + "user": { + "data": { + "name": "Namn", + "port": "Port" + }, + "description": "Detta \u00e4r en integration f\u00f6r fasta samtal med ett CX93001 r\u00f6stmodem. Detta kan h\u00e4mta nummerpresentationsinformation med ett alternativ att avvisa ett inkommande samtal." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/sv.json b/homeassistant/components/monoprice/translations/sv.json index 95b7bce9e8c..c23ee9a02e2 100644 --- a/homeassistant/components/monoprice/translations/sv.json +++ b/homeassistant/components/monoprice/translations/sv.json @@ -10,10 +10,31 @@ "step": { "user": { "data": { - "port": "Port" + "port": "Port", + "source_1": "Namn p\u00e5 k\u00e4lla #1", + "source_2": "Namn p\u00e5 k\u00e4lla #2", + "source_3": "Namn p\u00e5 k\u00e4lla #3", + "source_4": "Namn p\u00e5 k\u00e4lla #4", + "source_5": "Namn p\u00e5 k\u00e4lla #5", + "source_6": "Namn p\u00e5 k\u00e4lla #6" }, "title": "Anslut till enheten" } } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Namn p\u00e5 k\u00e4lla #1", + "source_2": "Namn p\u00e5 k\u00e4lla #2", + "source_3": "Namn p\u00e5 k\u00e4lla #3", + "source_4": "Namn p\u00e5 k\u00e4lla #4", + "source_5": "Namn p\u00e5 k\u00e4lla #5", + "source_6": "Namn p\u00e5 k\u00e4lla #6" + }, + "title": "Konfigurera k\u00e4llor" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/sv.json b/homeassistant/components/motion_blinds/translations/sv.json index eee10b4c765..5cee7f232c8 100644 --- a/homeassistant/components/motion_blinds/translations/sv.json +++ b/homeassistant/components/motion_blinds/translations/sv.json @@ -3,11 +3,21 @@ "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad" }, + "error": { + "discovery_error": "Det gick inte att uppt\u00e4cka en Motion Gateway" + }, "step": { "connect": { "data": { "api_key": "API-nyckel" - } + }, + "description": "Du beh\u00f6ver en API-nyckel p\u00e5 16 tecken, se https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key f\u00f6r instruktioner" + }, + "select": { + "data": { + "select_ip": "IP-adress" + }, + "description": "K\u00f6r installationen igen om du vill ansluta ytterligare Motion Gateways" } } } diff --git a/homeassistant/components/motioneye/translations/sv.json b/homeassistant/components/motioneye/translations/sv.json index 8fd6e00680b..75bf4f2e2f9 100644 --- a/homeassistant/components/motioneye/translations/sv.json +++ b/homeassistant/components/motioneye/translations/sv.json @@ -7,5 +7,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "webhook_set": "Konfigurera motionEye webhooks f\u00f6r att rapportera h\u00e4ndelser till Home Assistant", + "webhook_set_overwrite": "Skriv \u00f6ver ok\u00e4nda webhooks" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/sv.json b/homeassistant/components/mqtt/translations/sv.json index 1699051db86..30acc3c1098 100644 --- a/homeassistant/components/mqtt/translations/sv.json +++ b/homeassistant/components/mqtt/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", "single_instance_allowed": "Endast en enda konfiguration av MQTT \u00e4r till\u00e5ten." }, "error": { @@ -49,11 +50,31 @@ } }, "options": { + "error": { + "bad_birth": "Ogiltigt f\u00f6delse\u00e4mne.", + "bad_will": "Ogiltigt testamente.", + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "broker": { "data": { + "broker": "Broker", + "password": "L\u00f6senord", + "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "V\u00e4nligen ange anslutningsinformationen f\u00f6r din MQTT broker." + }, + "options": { + "data": { + "birth_enable": "Aktivera f\u00f6delsemeddelande", + "birth_payload": "Nyttolast f\u00f6r f\u00f6delsemeddelande", + "birth_qos": "F\u00f6delsemeddelande QoS", + "birth_retain": "F\u00f6delsemeddelande beh\u00e5lls", + "birth_topic": "\u00c4mne f\u00f6r f\u00f6delsemeddelande", + "discovery": "Aktivera uppt\u00e4ckt" + }, + "description": "Uppt\u00e4ckt - Om uppt\u00e4ckt \u00e4r aktiverat (rekommenderas), kommer Home Assistant automatiskt att uppt\u00e4cka enheter och enheter som publicerar sin konfiguration p\u00e5 MQTT-brokern. Om uppt\u00e4ckten \u00e4r inaktiverad m\u00e5ste all konfiguration g\u00f6ras manuellt.\n F\u00f6delsemeddelande - F\u00f6delsemeddelandet kommer att skickas varje g\u00e5ng Home Assistant (\u00e5ter)ansluter till MQTT-brokern.\n Kommer meddelande - Viljemeddelandet kommer att skickas varje g\u00e5ng Home Assistant f\u00f6rlorar sin anslutning till brokern, b\u00e5de i h\u00e4ndelse av en reng\u00f6ring (t.ex. Home Assistant st\u00e4ngs av) och i h\u00e4ndelse av en oren (t.ex. Home Assistant kraschar eller f\u00f6rlorar sin n\u00e4tverksanslutning) koppla ifr\u00e5n." } } } diff --git a/homeassistant/components/mutesync/translations/sv.json b/homeassistant/components/mutesync/translations/sv.json new file mode 100644 index 00000000000..be78d104856 --- /dev/null +++ b/homeassistant/components/mutesync/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Aktivera autentisering i m\u00fctesync-inst\u00e4llningar > Autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/sv.json b/homeassistant/components/myq/translations/sv.json index da06f32aa92..dfe61d049d0 100644 --- a/homeassistant/components/myq/translations/sv.json +++ b/homeassistant/components/myq/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Tj\u00e4nsten har redan konfigurerats" + "already_configured": "Tj\u00e4nsten har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Anslutningen misslyckades", @@ -9,6 +10,13 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "L\u00f6senordet f\u00f6r {username} \u00e4r inte l\u00e4ngre giltigt.", + "title": "Autentisera ditt MyQ-konto igen" + }, "user": { "data": { "password": "L\u00f6senord", diff --git a/homeassistant/components/mysensors/translations/sv.json b/homeassistant/components/mysensors/translations/sv.json index fbbcbdff5e6..daebe65e30e 100644 --- a/homeassistant/components/mysensors/translations/sv.json +++ b/homeassistant/components/mysensors/translations/sv.json @@ -1,7 +1,37 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "duplicate_persistence_file": "Persistensfil anv\u00e4nds redan", + "duplicate_topic": "\u00c4mnet anv\u00e4nds redan", + "invalid_auth": "Ogiltig autentisering", + "invalid_device": "Ogiltig enhet", + "invalid_ip": "Ogiltig IP-adress", + "invalid_persistence_file": "Ogiltig persistensfil", + "invalid_port": "Ogiltigt portnummer", + "invalid_publish_topic": "Ogiltigt \u00e4mne f\u00f6r publicering", + "invalid_serial": "Ogiltig serieport", + "invalid_subscribe_topic": "Ogiltigt \u00e4mne f\u00f6r prenumeration", + "invalid_version": "Ogiltig version av MySensors", + "not_a_number": "Ange ett nummer", + "port_out_of_range": "Portnummer m\u00e5ste vara minst 1 och h\u00f6gst 65535", + "same_topic": "\u00c4mnen f\u00f6r prenumeration och publicering \u00e4r desamma", + "unknown": "Ov\u00e4ntat fel" + }, "error": { - "invalid_serial": "Ogiltig serieport" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "duplicate_persistence_file": "Persistensfil anv\u00e4nds redan", + "duplicate_topic": "\u00c4mnet anv\u00e4nds redan", + "invalid_auth": "Ogiltig autentisering", + "invalid_device": "Ogiltig enhet", + "invalid_ip": "Ogiltig IP-adress", + "invalid_persistence_file": "Ogiltig persistensfil", + "invalid_port": "Ogiltigt portnummer", + "invalid_publish_topic": "Ogiltigt \u00e4mne f\u00f6r publicering", + "invalid_serial": "Ogiltig serieport", + "mqtt_required": "MQTT-integrationen \u00e4r inte konfigurerad" } } } \ No newline at end of file diff --git a/homeassistant/components/nanoleaf/translations/sv.json b/homeassistant/components/nanoleaf/translations/sv.json index 4ca6ad5c3de..bbea90511da 100644 --- a/homeassistant/components/nanoleaf/translations/sv.json +++ b/homeassistant/components/nanoleaf/translations/sv.json @@ -1,7 +1,28 @@ { "config": { "abort": { - "reauth_successful": "\u00c5terautentisering lyckades" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_token": "Ogiltig \u00e5tkomstnyckel", + "reauth_successful": "\u00c5terautentisering lyckades", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "not_allowing_new_tokens": "Nanoleaf till\u00e5ter inte nya tokens, f\u00f6lj instruktionerna ovan.", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name}", + "step": { + "link": { + "description": "Tryck och h\u00e5ll in str\u00f6mbrytaren p\u00e5 din Nanoleaf i 5 sekunder tills knappens lysdioder b\u00f6rjar blinka, klicka sedan p\u00e5 **Skicka** inom 30 sekunder.", + "title": "L\u00e4nka Nanoleaf" + }, + "user": { + "data": { + "host": "V\u00e4rd" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/sv.json b/homeassistant/components/neato/translations/sv.json index 71f24f595e5..f69c0aecfd1 100644 --- a/homeassistant/components/neato/translations/sv.json +++ b/homeassistant/components/neato/translations/sv.json @@ -1,10 +1,22 @@ { "config": { "abort": { - "already_configured": "Redan konfigurerad" + "already_configured": "Redan konfigurerad", + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", + "reauth_successful": "\u00c5terautentisering lyckades" }, "create_entry": { "default": "Se [Neato-dokumentation]({docs_url})." + }, + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + }, + "reauth_confirm": { + "title": "Vill du starta konfigurationen?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/sv.json b/homeassistant/components/nest/translations/sv.json index 750a7643ee2..5cc9d3d68c0 100644 --- a/homeassistant/components/nest/translations/sv.json +++ b/homeassistant/components/nest/translations/sv.json @@ -5,7 +5,8 @@ "config": { "abort": { "already_configured": "Konto har redan konfigurerats", - "authorize_url_timeout": "Timeout vid generering av en autentisieringsadress." + "authorize_url_timeout": "Timeout vid generering av en autentisieringsadress.", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "internal_error": "Internt fel vid validering av kod", @@ -52,13 +53,19 @@ }, "description": "F\u00f6r att l\u00e4nka ditt Nest-konto, [autentisiera ditt konto]({url}). \n\nEfter autentisiering, klipp och klistra in den angivna pin-koden nedan.", "title": "L\u00e4nka Nest-konto" + }, + "reauth_confirm": { + "description": "Nest-integreringen m\u00e5ste autentisera ditt konto igen", + "title": "\u00c5terautenticera integration" } } }, "device_automation": { "trigger_type": { "camera_motion": "R\u00f6relse uppt\u00e4ckt", - "camera_person": "Person detekterad" + "camera_person": "Person detekterad", + "camera_sound": "Ljud uppt\u00e4ckt", + "doorbell_chime": "D\u00f6rrklockan tryckt" } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/sv.json b/homeassistant/components/netatmo/translations/sv.json index 32dfd2db6a0..2fe3a86b0c8 100644 --- a/homeassistant/components/netatmo/translations/sv.json +++ b/homeassistant/components/netatmo/translations/sv.json @@ -1,12 +1,48 @@ { "config": { + "abort": { + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, "create_entry": { "default": "Autentiserad med Netatmo." + }, + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + } } }, "device_automation": { "trigger_subtype": { "schedule": "schema" } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "Namn p\u00e5 omr\u00e5det", + "lat_ne": "Latitud Nord\u00f6stra h\u00f6rnet", + "lat_sw": "Latitud Sydv\u00e4stra h\u00f6rnet", + "lon_ne": "Longitud Nord\u00f6stra h\u00f6rnet", + "lon_sw": "Longitud Sydv\u00e4stra h\u00f6rnet", + "mode": "Ber\u00e4kning", + "show_on_map": "Visa p\u00e5 karta" + }, + "description": "Konfigurera en offentlig v\u00e4dersensor f\u00f6r ett omr\u00e5de.", + "title": "Netatmo offentlig v\u00e4dersensor" + }, + "public_weather_areas": { + "data": { + "new_area": "Omr\u00e5desnamn", + "weather_areas": "V\u00e4deromr\u00e5den" + }, + "description": "Konfigurera offentliga v\u00e4dersensorer.", + "title": "Netatmo offentlig v\u00e4dersensor" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/sv.json b/homeassistant/components/nexia/translations/sv.json index 9cfd620ac73..b00dc6e93b2 100644 --- a/homeassistant/components/nexia/translations/sv.json +++ b/homeassistant/components/nexia/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", "invalid_auth": "Ogiltig autentisering", diff --git a/homeassistant/components/nfandroidtv/translations/sv.json b/homeassistant/components/nfandroidtv/translations/sv.json new file mode 100644 index 00000000000..c832c7a8880 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/sv.json b/homeassistant/components/nightscout/translations/sv.json new file mode 100644 index 00000000000..d51243a77f1 --- /dev/null +++ b/homeassistant/components/nightscout/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nmap_tracker/translations/sv.json b/homeassistant/components/nmap_tracker/translations/sv.json new file mode 100644 index 00000000000..b69cd27fe52 --- /dev/null +++ b/homeassistant/components/nmap_tracker/translations/sv.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "init": { + "data": { + "consider_home": "Sekunder att v\u00e4nta tills en enhetssp\u00e5rare markeras som inte hemma efter att den inte setts.", + "interval_seconds": "Skanningsintervall" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/sv.json b/homeassistant/components/notion/translations/sv.json index 71836f79877..b565872aacc 100644 --- a/homeassistant/components/notion/translations/sv.json +++ b/homeassistant/components/notion/translations/sv.json @@ -1,9 +1,20 @@ { "config": { "abort": { - "already_configured": "Det h\u00e4r anv\u00e4ndarnamnet anv\u00e4nds redan." + "already_configured": "Det h\u00e4r anv\u00e4ndarnamnet anv\u00e4nds redan.", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "unknown": "Ov\u00e4ntat fel" }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Skriv om l\u00f6senordet f\u00f6r {username}", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "password": "L\u00f6senord", diff --git a/homeassistant/components/nuheat/translations/sv.json b/homeassistant/components/nuheat/translations/sv.json index 327bdf8c4ca..ffe743f1f14 100644 --- a/homeassistant/components/nuheat/translations/sv.json +++ b/homeassistant/components/nuheat/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", "invalid_auth": "Ogiltig autentisering", @@ -13,7 +16,8 @@ "serial_number": "Termostatens serienummer.", "username": "Anv\u00e4ndarnamn" }, - "description": "F\u00e5 tillg\u00e5ng till din termostats serienummer eller ID genom att logga in p\u00e5 https://MyNuHeat.com och v\u00e4lja din termostat." + "description": "F\u00e5 tillg\u00e5ng till din termostats serienummer eller ID genom att logga in p\u00e5 https://MyNuHeat.com och v\u00e4lja din termostat.", + "title": "Anslut till NuHeat" } } } diff --git a/homeassistant/components/nuki/translations/sv.json b/homeassistant/components/nuki/translations/sv.json index 563e2d4a773..9a4ec6946b7 100644 --- a/homeassistant/components/nuki/translations/sv.json +++ b/homeassistant/components/nuki/translations/sv.json @@ -1,10 +1,22 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "reauth_confirm": { "data": { "token": "\u00c5tkomstnyckel" } + }, + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port", + "token": "\u00c5tkomstnyckel" + } } } } diff --git a/homeassistant/components/nut/translations/sv.json b/homeassistant/components/nut/translations/sv.json index 9a3cb690744..5af0dd86fc4 100644 --- a/homeassistant/components/nut/translations/sv.json +++ b/homeassistant/components/nut/translations/sv.json @@ -20,7 +20,8 @@ "password": "L\u00f6senord", "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Anslut till NUT-servern" } } }, diff --git a/homeassistant/components/nws/translations/sv.json b/homeassistant/components/nws/translations/sv.json index 5ce9a828148..b695f12a694 100644 --- a/homeassistant/components/nws/translations/sv.json +++ b/homeassistant/components/nws/translations/sv.json @@ -12,8 +12,11 @@ "data": { "api_key": "API nyckel", "latitude": "Latitud", - "longitude": "Longitud" - } + "longitude": "Longitud", + "station": "METAR stationskod" + }, + "description": "Om en METAR-stationskod inte anges, kommer latitud och longitud att anv\u00e4ndas f\u00f6r att hitta den n\u00e4rmaste stationen. F\u00f6r n\u00e4rvarande kan en API-nyckel vara vad som helst. Det rekommenderas att anv\u00e4nda en giltig e-postadress.", + "title": "Anslut till den nationella v\u00e4dertj\u00e4nsten" } } } diff --git a/homeassistant/components/nzbget/translations/sv.json b/homeassistant/components/nzbget/translations/sv.json index 23c825f256f..0e50fcbbe00 100644 --- a/homeassistant/components/nzbget/translations/sv.json +++ b/homeassistant/components/nzbget/translations/sv.json @@ -1,9 +1,33 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{name}", "step": { "user": { "data": { - "username": "Anv\u00e4ndarnamn" + "host": "V\u00e4rd", + "name": "Namn", + "password": "L\u00f6senord", + "port": "Port", + "ssl": "Anv\u00e4nd ett SSL certifikat", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera SSL-certifikat" + }, + "title": "Anslut till NZBGet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Uppdateringsfrekvens (sekunder)" } } } diff --git a/homeassistant/components/omnilogic/translations/sv.json b/homeassistant/components/omnilogic/translations/sv.json index 23c825f256f..70e9ad8a483 100644 --- a/homeassistant/components/omnilogic/translations/sv.json +++ b/homeassistant/components/omnilogic/translations/sv.json @@ -7,5 +7,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "ph_offset": "pH-f\u00f6rskjutning (positiv eller negativ)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/sv.json b/homeassistant/components/onvif/translations/sv.json index 2cc40c1e465..579922f31e6 100644 --- a/homeassistant/components/onvif/translations/sv.json +++ b/homeassistant/components/onvif/translations/sv.json @@ -1,14 +1,53 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_h264": "Det fanns inga tillg\u00e4ngliga H264-str\u00f6mmar. Kontrollera profilkonfigurationen p\u00e5 din enhet.", + "no_mac": "Det gick inte att konfigurera unikt ID f\u00f6r ONVIF-enhet.", + "onvif_error": "Problem med att konfigurera ONVIF enheten. Kolla loggarna f\u00f6r mer information." + }, "step": { "configure": { "data": { + "host": "V\u00e4rd", + "name": "Namn", "password": "L\u00f6senord", + "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Konfigurera ONVIF-enhet" }, "configure_profile": { + "data": { + "include": "Skapa kameraentitet" + }, + "description": "Skapa kameraentitet f\u00f6r {profile} i {resolution}-uppl\u00f6sning?", "title": "Konfigurera Profiler" + }, + "device": { + "data": { + "host": "V\u00e4lj uppt\u00e4ckt ONVIF enhet" + }, + "title": "V\u00e4lj ONVIF enhet" + }, + "user": { + "data": { + "auto": "S\u00f6k automatiskt" + }, + "description": "Genom att klicka p\u00e5 skicka kommer vi att s\u00f6ka i ditt n\u00e4tverk efter ONVIF-enheter som st\u00f6der Profile S. \n\n Vissa tillverkare har b\u00f6rjat inaktivera ONVIF som standard. Se till att ONVIF \u00e4r aktiverat i din kameras konfiguration.", + "title": "Inst\u00e4llning av ONVIF-enhet" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Extra FFMPEG-argument", + "rtsp_transport": "RTSP transportmekanism" + }, + "title": "ONVIF enhetsalternativ" } } } diff --git a/homeassistant/components/openalpr_local/translations/hu.json b/homeassistant/components/openalpr_local/translations/hu.json new file mode 100644 index 00000000000..30232ae48cb --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/hu.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Az OpenALPR Local integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra v\u00e1r a Home Assistantb\u00f3l, \u00e9s a 2022.10-es Home Assistant-t\u00f3l kezdve nem lesz el\u00e9rhet\u0151.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "Az OpenALPR Local integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/sv.json b/homeassistant/components/opengarage/translations/sv.json index eba844f6c03..1b5b204fc99 100644 --- a/homeassistant/components/opengarage/translations/sv.json +++ b/homeassistant/components/opengarage/translations/sv.json @@ -1,9 +1,20 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { - "host": "V\u00e4rd" + "device_key": "Enhetsnyckel", + "host": "V\u00e4rd", + "port": "Port", + "verify_ssl": "Verifiera SSL-certifikat" } } } diff --git a/homeassistant/components/opentherm_gw/translations/el.json b/homeassistant/components/opentherm_gw/translations/el.json index 82be1ff6ce9..9db15d34e49 100644 --- a/homeassistant/components/opentherm_gw/translations/el.json +++ b/homeassistant/components/opentherm_gw/translations/el.json @@ -3,7 +3,8 @@ "error": { "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "id_exists": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03cd\u03bb\u03b7\u03c2 \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7" + "id_exists": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03cd\u03bb\u03b7\u03c2 \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7", + "timeout_connect": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/sv.json b/homeassistant/components/opentherm_gw/translations/sv.json index 8ba8716897f..95ed092fece 100644 --- a/homeassistant/components/opentherm_gw/translations/sv.json +++ b/homeassistant/components/opentherm_gw/translations/sv.json @@ -18,7 +18,10 @@ "step": { "init": { "data": { - "floor_temperature": "Golvetemperatur" + "floor_temperature": "Golvetemperatur", + "read_precision": "L\u00e4sprecision", + "set_precision": "St\u00e4ll in Precision", + "temporary_override_mode": "Tempor\u00e4rt l\u00e4ge f\u00f6r \u00e5sidos\u00e4ttande av inst\u00e4llningsv\u00e4rde" } } } diff --git a/homeassistant/components/openuv/translations/sv.json b/homeassistant/components/openuv/translations/sv.json index 0df9a0434ab..9f1620fb980 100644 --- a/homeassistant/components/openuv/translations/sv.json +++ b/homeassistant/components/openuv/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Platsen \u00e4r redan konfigurerad" + }, "error": { "invalid_api_key": "Ogiltigt API-l\u00f6senord" }, @@ -14,5 +17,16 @@ "title": "Fyll i dina uppgifter" } } + }, + "options": { + "step": { + "init": { + "data": { + "from_window": "Startande UV-index f\u00f6r skyddsf\u00f6nstret", + "to_window": "Slutande UV-index f\u00f6r skyddsf\u00f6nstret" + }, + "title": "Konfigurera OpenUV" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/sv.json b/homeassistant/components/openweathermap/translations/sv.json index c0fdf3bbfdb..64212920aa7 100644 --- a/homeassistant/components/openweathermap/translations/sv.json +++ b/homeassistant/components/openweathermap/translations/sv.json @@ -3,14 +3,20 @@ "abort": { "already_configured": "OpenWeatherMap-integrationen f\u00f6r dessa koordinater \u00e4r redan konfigurerad." }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "user": { "data": { "api_key": "OpenWeatherMap API-nyckel", "language": "Spr\u00e5k", + "latitude": "Latitud", + "longitude": "Longitud", "mode": "L\u00e4ge", "name": "Integrationens namn" - } + }, + "description": "F\u00f6r att generera API-nyckel g\u00e5 till https://openweathermap.org/appid" } } }, diff --git a/homeassistant/components/ovo_energy/translations/sv.json b/homeassistant/components/ovo_energy/translations/sv.json index 054280346d3..bd443851b14 100644 --- a/homeassistant/components/ovo_energy/translations/sv.json +++ b/homeassistant/components/ovo_energy/translations/sv.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "already_configured": "Konto har redan konfigurerats", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "reauth": { "data": { diff --git a/homeassistant/components/owntracks/translations/sv.json b/homeassistant/components/owntracks/translations/sv.json index fd2162f153b..3c9146e49f2 100644 --- a/homeassistant/components/owntracks/translations/sv.json +++ b/homeassistant/components/owntracks/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, "create_entry": { "default": "\n\n P\u00e5 Android, \u00f6ppna [OwnTracks-appen]({android_url}), g\u00e5 till inst\u00e4llningar -> anslutning. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: Privat HTTP \n - V\u00e4rden: {webhook_url}\n - Identifiering: \n - Anv\u00e4ndarnamn: ``\n - Enhets-ID: `` \n\n P\u00e5 IOS, \u00f6ppna [OwnTracks-appen]({ios_url}), tryck p\u00e5 (i) ikonen i \u00f6vre v\u00e4nstra h\u00f6rnet -> inst\u00e4llningarna. \u00c4ndra f\u00f6ljande inst\u00e4llningar: \n - L\u00e4ge: HTTP \n - URL: {webhook_url}\n - Sl\u00e5 p\u00e5 autentisering \n - UserID: `` \n\n {secret} \n \n Se [dokumentationen]({docs_url}) f\u00f6r mer information." }, diff --git a/homeassistant/components/panasonic_viera/translations/sv.json b/homeassistant/components/panasonic_viera/translations/sv.json index 2c863b587ec..326c8c57dc5 100644 --- a/homeassistant/components/panasonic_viera/translations/sv.json +++ b/homeassistant/components/panasonic_viera/translations/sv.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "unknown": "Ett ov\u00e4ntat fel intr\u00e4ffade. Kontrollera loggarna f\u00f6r mer information." }, + "error": { + "invalid_pin_code": "Pin-kod du angav \u00e4r ogiltig" + }, "step": { "pairing": { "data": { diff --git a/homeassistant/components/philips_js/translations/sv.json b/homeassistant/components/philips_js/translations/sv.json index 418a59f0bdc..47b456c1ff7 100644 --- a/homeassistant/components/philips_js/translations/sv.json +++ b/homeassistant/components/philips_js/translations/sv.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { - "invalid_pin": "Ogiltig PIN-kod" + "cannot_connect": "Det gick inte att ansluta.", + "invalid_pin": "Ogiltig PIN-kod", + "unknown": "Ov\u00e4ntat fel" }, "step": { "pair": { @@ -9,7 +14,18 @@ "pin": "PIN-kod" }, "title": "Para ihop" + }, + "user": { + "data": { + "api_version": "API-version", + "host": "V\u00e4rd" + } } } + }, + "device_automation": { + "trigger_type": { + "turn_on": "Enheten uppmanas att sl\u00e5s p\u00e5" + } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/sv.json b/homeassistant/components/pi_hole/translations/sv.json index 0459a7596f3..a1a0a54f9af 100644 --- a/homeassistant/components/pi_hole/translations/sv.json +++ b/homeassistant/components/pi_hole/translations/sv.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "api_key": { "data": { @@ -9,7 +15,12 @@ "user": { "data": { "api_key": "API-nyckel", - "host": "V\u00e4rd" + "host": "V\u00e4rd", + "location": "Plats", + "name": "Namn", + "port": "Port", + "ssl": "Anv\u00e4nd ett SSL certifikat", + "verify_ssl": "Verifiera SSL-certifikat" } } } diff --git a/homeassistant/components/picnic/translations/sv.json b/homeassistant/components/picnic/translations/sv.json index 23c825f256f..60959ec71fb 100644 --- a/homeassistant/components/picnic/translations/sv.json +++ b/homeassistant/components/picnic/translations/sv.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/plaato/translations/sv.json b/homeassistant/components/plaato/translations/sv.json index 25e86261699..6368ff7222e 100644 --- a/homeassistant/components/plaato/translations/sv.json +++ b/homeassistant/components/plaato/translations/sv.json @@ -1,8 +1,18 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." + }, "create_entry": { "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera webhook funktionen i Plaato Airlock.\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n\nSe [dokumentation]({docs_url}) f\u00f6r mer information." }, + "error": { + "invalid_webhook_device": "Du har valt en enhet som inte st\u00f6der att skicka data till en webhook. Den \u00e4r endast tillg\u00e4nglig f\u00f6r Airlock", + "no_api_method": "Du m\u00e5ste l\u00e4gga till en autentiseringstoken eller v\u00e4lja webhook", + "no_auth_token": "Du m\u00e5ste l\u00e4gga till en autentiseringstoken" + }, "step": { "user": { "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera Plaato Webhook?", diff --git a/homeassistant/components/plex/translations/sv.json b/homeassistant/components/plex/translations/sv.json index 4227e45b707..bd5b5570ac0 100644 --- a/homeassistant/components/plex/translations/sv.json +++ b/homeassistant/components/plex/translations/sv.json @@ -9,13 +9,19 @@ }, "error": { "faulty_credentials": "Auktoriseringen misslyckades", + "host_or_token": "M\u00e5ste tillhandah\u00e5lla minst en av v\u00e4rd eller token", "no_servers": "Inga servrar l\u00e4nkade till konto", - "not_found": "Plex-server hittades inte" + "not_found": "Plex-server hittades inte", + "ssl_error": "Problem med SSL-certifikat" }, "step": { "manual_setup": { "data": { - "port": "Port" + "host": "V\u00e4rd", + "port": "Port", + "ssl": "Anv\u00e4nd ett SSL certifikat", + "token": "Token (valfritt)", + "verify_ssl": "Verifiera SSL-certifikat" } }, "select_server": { diff --git a/homeassistant/components/plugwise/translations/sv.json b/homeassistant/components/plugwise/translations/sv.json index a22c1e882bc..072006dd32e 100644 --- a/homeassistant/components/plugwise/translations/sv.json +++ b/homeassistant/components/plugwise/translations/sv.json @@ -1,22 +1,45 @@ { "config": { "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", "anna_with_adam": "B\u00e5de Anna och Adam uppt\u00e4ckte. L\u00e4gg till din Adam ist\u00e4llet f\u00f6r din Anna" }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "flow_type": "Anslutningstyp", + "host": "IP-adress", "password": "Smile ID", "port": "Port", "username": "Smile Anv\u00e4ndarnamn" - } + }, + "description": "Ange", + "title": "Anslut till leendet" }, "user_gateway": { "data": { "host": "IP address", - "port": "Port" + "password": "Smile ID", + "port": "Port", + "username": "Smile Anv\u00e4ndarnamn" }, - "description": "V\u00e4nligen ange:" + "description": "V\u00e4nligen ange:", + "title": "Anslut till leendet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Skanningsintervall (sekunder)" + }, + "description": "Justera Plugwise-alternativ" } } } diff --git a/homeassistant/components/powerwall/translations/sv.json b/homeassistant/components/powerwall/translations/sv.json index 01b1eccd5c0..fd0d0eb86c8 100644 --- a/homeassistant/components/powerwall/translations/sv.json +++ b/homeassistant/components/powerwall/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta", @@ -14,7 +15,9 @@ "data": { "ip_address": "IP-adress", "password": "L\u00f6senord" - } + }, + "description": "L\u00f6senordet \u00e4r vanligtvis de sista 5 tecknen i serienumret f\u00f6r Backup Gateway och kan hittas i Tesla-appen eller de sista 5 tecknen i l\u00f6senordet som finns innanf\u00f6r d\u00f6rren f\u00f6r Backup Gateway 2.", + "title": "Anslut till powerwall" } } } diff --git a/homeassistant/components/progettihwsw/translations/sv.json b/homeassistant/components/progettihwsw/translations/sv.json new file mode 100644 index 00000000000..c7c31766d08 --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "relay_modes": { + "data": { + "relay_1": "Rel\u00e4 1", + "relay_10": "Rel\u00e4 10", + "relay_11": "Rel\u00e4 11", + "relay_12": "Rel\u00e4 12", + "relay_13": "Rel\u00e4 13" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/sv.json b/homeassistant/components/pvpc_hourly_pricing/translations/sv.json new file mode 100644 index 00000000000..e45b374e5da --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "step": { + "user": { + "data": { + "name": "Sensornamn", + "tariff": "Till\u00e4mplig taxa per geografisk zon" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/sv.json b/homeassistant/components/qnap_qsw/translations/sv.json index 8db64916805..bc5f2b35a28 100644 --- a/homeassistant/components/qnap_qsw/translations/sv.json +++ b/homeassistant/components/qnap_qsw/translations/sv.json @@ -5,6 +5,7 @@ "invalid_id": "Enheten returnerade ett ogiltigt unikt ID" }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering" }, "step": { diff --git a/homeassistant/components/rachio/translations/sv.json b/homeassistant/components/rachio/translations/sv.json index 4932b17ebfa..8bbd8d91d5f 100644 --- a/homeassistant/components/rachio/translations/sv.json +++ b/homeassistant/components/rachio/translations/sv.json @@ -13,8 +13,18 @@ "data": { "api_key": "API nyckel" }, + "description": "Du beh\u00f6ver API-nyckeln fr\u00e5n https://app.rach.io/. G\u00e5 till Inst\u00e4llningar och klicka sedan p\u00e5 'GET API KEY'.", "title": "Anslut till Rachio-enheten" } } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Varaktighet i minuter att k\u00f6ra n\u00e4r du aktiverar en zonomkopplare" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/hu.json b/homeassistant/components/radiotherm/translations/hu.json index 05e1ece66ec..74da62c3f88 100644 --- a/homeassistant/components/radiotherm/translations/hu.json +++ b/homeassistant/components/radiotherm/translations/hu.json @@ -22,7 +22,7 @@ "issues": { "deprecated_yaml": { "description": "A Radio Thermostat kl\u00edmaplatform YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa a Home Assistant 2022.9-ben megsz\u0171nik. \n\nMegl\u00e9v\u0151 konfigur\u00e1ci\u00f3ja automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre. T\u00e1vol\u00edtsa el a YAML-konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistant alkalmaz\u00e1st a probl\u00e9ma megold\u00e1s\u00e1hoz.", - "title": "A r\u00e1di\u00f3s termoszt\u00e1t YAML konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + "title": "A Radio Thermostat YAML konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } }, "options": { diff --git a/homeassistant/components/rainforest_eagle/translations/sv.json b/homeassistant/components/rainforest_eagle/translations/sv.json new file mode 100644 index 00000000000..eba844f6c03 --- /dev/null +++ b/homeassistant/components/rainforest_eagle/translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/sv.json b/homeassistant/components/rainmachine/translations/sv.json index 5077b9bb967..40295676501 100644 --- a/homeassistant/components/rainmachine/translations/sv.json +++ b/homeassistant/components/rainmachine/translations/sv.json @@ -17,6 +17,9 @@ "options": { "step": { "init": { + "data": { + "zone_run_time": "Standardzonens k\u00f6rtid (i sekunder)" + }, "title": "Konfigurera RainMachine" } } diff --git a/homeassistant/components/recollect_waste/translations/sv.json b/homeassistant/components/recollect_waste/translations/sv.json new file mode 100644 index 00000000000..5d61ecf184c --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/sv.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "invalid_place_or_service_id": "Ogiltigt plats- eller tj\u00e4nst-ID" + }, + "step": { + "user": { + "data": { + "place_id": "Plats-ID", + "service_id": "Tj\u00e4nst-ID" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "Anv\u00e4nd v\u00e4nliga namn f\u00f6r upph\u00e4mtningstyper (n\u00e4r det \u00e4r m\u00f6jligt)" + }, + "title": "Konfigurera Recollect Waste" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/remote/translations/sv.json b/homeassistant/components/remote/translations/sv.json index 1b6584c5bf8..ef7a2ab0ad2 100644 --- a/homeassistant/components/remote/translations/sv.json +++ b/homeassistant/components/remote/translations/sv.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "V\u00e4xla {entity_name}", "turn_off": "St\u00e4ng av {entity_name}", "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" }, diff --git a/homeassistant/components/renault/translations/sv.json b/homeassistant/components/renault/translations/sv.json index 26e9f2d6a49..a3bea645b0f 100644 --- a/homeassistant/components/renault/translations/sv.json +++ b/homeassistant/components/renault/translations/sv.json @@ -3,8 +3,11 @@ "step": { "user": { "data": { + "locale": "Plats", + "password": "L\u00f6senord", "username": "E-postadress" - } + }, + "title": "St\u00e4ll in Renault-uppgifter" } } } diff --git a/homeassistant/components/rfxtrx/translations/sv.json b/homeassistant/components/rfxtrx/translations/sv.json index 28f5c911c3a..304513880e6 100644 --- a/homeassistant/components/rfxtrx/translations/sv.json +++ b/homeassistant/components/rfxtrx/translations/sv.json @@ -31,6 +31,16 @@ } } }, + "device_automation": { + "action_type": { + "send_command": "Skicka kommando: {subtype}", + "send_status": "Skicka statusuppdatering: {subtype}" + }, + "trigger_type": { + "command": "Mottaget kommando: {subtype}", + "status": "Mottagen status: {subtype}" + } + }, "options": { "error": { "already_configured_device": "Enheten \u00e4r redan konfigurerad", diff --git a/homeassistant/components/rhasspy/translations/no.json b/homeassistant/components/rhasspy/translations/no.json index 4dc3393a982..52682f70186 100644 --- a/homeassistant/components/rhasspy/translations/no.json +++ b/homeassistant/components/rhasspy/translations/no.json @@ -2,6 +2,11 @@ "config": { "abort": { "single_instance_allowed": "Allerede konfigurert. Kun \u00e9n konfigurert instans i gangen." + }, + "step": { + "user": { + "description": "Vil du aktivere Rhasspy-st\u00f8tte?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/risco/translations/sv.json b/homeassistant/components/risco/translations/sv.json index 23c825f256f..1f7f95730d1 100644 --- a/homeassistant/components/risco/translations/sv.json +++ b/homeassistant/components/risco/translations/sv.json @@ -7,5 +7,20 @@ } } } + }, + "options": { + "step": { + "risco_to_ha": { + "data": { + "B": "Grupp B", + "C": "Grupp C", + "D": "Grupp D", + "arm": "Larmat (Borta)", + "partial_arm": "Delvis larmat (hemma)" + }, + "description": "V\u00e4lj vilket tillst\u00e5nd ditt Home Assistant-larm ska rapportera f\u00f6r varje tillst\u00e5nd som rapporteras av Risco", + "title": "Mappa Risco-tillst\u00e5nd till Home Assistant-tillst\u00e5nd" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rituals_perfume_genie/translations/sv.json b/homeassistant/components/rituals_perfume_genie/translations/sv.json new file mode 100644 index 00000000000..4e593cd0c44 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "email": "E-post", + "password": "L\u00f6senord" + }, + "title": "Anslut till ditt Rituals-konto" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/sv.json b/homeassistant/components/roku/translations/sv.json index 4272f65b2ae..0acdb41359d 100644 --- a/homeassistant/components/roku/translations/sv.json +++ b/homeassistant/components/roku/translations/sv.json @@ -1,14 +1,20 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", "unknown": "Ov\u00e4ntat fel" }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "flow_title": "Roku: {name}", "step": { "user": { "data": { "host": "V\u00e4rd" - } + }, + "description": "Ange din Roku information." } } } diff --git a/homeassistant/components/roomba/translations/sv.json b/homeassistant/components/roomba/translations/sv.json index e2c491df80b..1c731cd2af9 100644 --- a/homeassistant/components/roomba/translations/sv.json +++ b/homeassistant/components/roomba/translations/sv.json @@ -1,13 +1,37 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "not_irobot_device": "Uppt\u00e4ckt enhet \u00e4r inte en iRobot-enhet", + "short_blid": "BLID trunkerades" + }, "error": { "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen" }, "step": { + "link": { + "description": "Tryck och h\u00e5ll hemknappen p\u00e5 {name} tills enheten genererar ett ljud (cirka tv\u00e5 sekunder), skicka sedan inom 30 sekunder.", + "title": "H\u00e4mta l\u00f6senord" + }, + "link_manual": { + "data": { + "password": "L\u00f6senord" + }, + "title": "Ange l\u00f6senord" + }, + "manual": { + "data": { + "host": "V\u00e4rd" + }, + "description": "No Roomba or Braava have been discovered on your network.", + "title": "Anslut till enheten manuellt" + }, "user": { "data": { "host": "V\u00e4rdnamn eller IP-adress" }, + "description": "V\u00e4lj Roomba eller Braava.", "title": "Anslut till enheten" } } diff --git a/homeassistant/components/roon/translations/sv.json b/homeassistant/components/roon/translations/sv.json index 420e19171ae..51e3fdfa83f 100644 --- a/homeassistant/components/roon/translations/sv.json +++ b/homeassistant/components/roon/translations/sv.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "fallback": { "data": { @@ -7,6 +14,10 @@ "port": "Port" }, "description": "Kunde inte uppt\u00e4cka Roon-servern, ange ditt v\u00e4rdnamn och port." + }, + "link": { + "description": "Du m\u00e5ste auktorisera Home Assistant i Roon. N\u00e4r du har klickat p\u00e5 skicka, g\u00e5 till Roon Core-applikationen, \u00f6ppna Inst\u00e4llningar och aktivera HomeAssistant p\u00e5 fliken Till\u00e4gg.", + "title": "Auktorisera HomeAssistant i Roon" } } } diff --git a/homeassistant/components/rpi_power/translations/sv.json b/homeassistant/components/rpi_power/translations/sv.json index 0ca2f1f5748..554e410f6a5 100644 --- a/homeassistant/components/rpi_power/translations/sv.json +++ b/homeassistant/components/rpi_power/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "no_devices_found": "Kan inte hitta systemklassen som beh\u00f6vs f\u00f6r den h\u00e4r komponenten, se till att din k\u00e4rna \u00e4r ny och att h\u00e5rdvaran st\u00f6ds", "single_instance_allowed": "Redan konfigurerad. Bara en konfiguration \u00e4r till\u00e5ten." }, "step": { @@ -8,5 +9,6 @@ "description": "Vill du b\u00f6rja med inst\u00e4llning?" } } - } + }, + "title": "Raspberry Pi str\u00f6mf\u00f6rs\u00f6rjningskontroll" } \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/sv.json b/homeassistant/components/scrape/translations/sv.json index 2c4da93bb6d..130d7505018 100644 --- a/homeassistant/components/scrape/translations/sv.json +++ b/homeassistant/components/scrape/translations/sv.json @@ -11,6 +11,7 @@ "device_class": "Enhetsklass", "headers": "Headers", "index": "Index", + "name": "Namn", "password": "L\u00f6senord", "resource": "Resurs", "select": "V\u00e4lj", @@ -21,7 +22,16 @@ "verify_ssl": "Verifiera SSL-certifikat" }, "data_description": { - "attribute": "H\u00e4mta v\u00e4rdet av ett attribut p\u00e5 den valda taggen" + "attribute": "H\u00e4mta v\u00e4rdet av ett attribut p\u00e5 den valda taggen", + "authentication": "Typ av HTTP-autentisering. Antingen basic eller digest", + "device_class": "Typ/klass av sensorn f\u00f6r att st\u00e4lla in ikonen i frontend", + "headers": "Rubriker att anv\u00e4nda f\u00f6r webbf\u00f6rfr\u00e5gan", + "index": "Definierar vilka av elementen som returneras av CSS-v\u00e4ljaren som ska anv\u00e4ndas", + "resource": "Webbadressen till webbplatsen som inneh\u00e5ller v\u00e4rdet", + "select": "Definierar vilken tagg som ska s\u00f6kas efter. Se Beautifulsoup CSS-selektorer f\u00f6r mer information.", + "state_class": "Tillst\u00e5ndsklassen f\u00f6r sensorn", + "value_template": "Definierar en mall f\u00f6r att f\u00e5 sensorns tillst\u00e5nd", + "verify_ssl": "Aktiverar/inaktiverar verifiering av SSL/TLS-certifikat, till exempel om det \u00e4r sj\u00e4lvsignerat" } } } @@ -35,6 +45,7 @@ "device_class": "Enhetsklass", "headers": "Headers", "index": "Index", + "name": "Namn", "password": "L\u00f6senord", "resource": "Resurs", "select": "V\u00e4lj", @@ -45,7 +56,16 @@ "verify_ssl": "Verifiera SSL-certifikat" }, "data_description": { - "attribute": "H\u00e4mta v\u00e4rdet av ett attribut p\u00e5 den valda taggen" + "attribute": "H\u00e4mta v\u00e4rdet av ett attribut p\u00e5 den valda taggen", + "authentication": "Typ av HTTP-autentisering. Antingen basic eller digest", + "device_class": "Typ/klass av sensorn f\u00f6r att st\u00e4lla in ikonen i frontend", + "headers": "Rubriker att anv\u00e4nda f\u00f6r webbf\u00f6rfr\u00e5gan", + "index": "Definierar vilka av elementen som returneras av CSS-v\u00e4ljaren som ska anv\u00e4ndas", + "resource": "Webbadressen till webbplatsen som inneh\u00e5ller v\u00e4rdet", + "select": "Definierar vilken tagg som ska s\u00f6kas efter. Se Beautifulsoup CSS-selektorer f\u00f6r mer information.", + "state_class": "Tillst\u00e5ndsklassen f\u00f6r sensorn", + "value_template": "Definierar en mall f\u00f6r att f\u00e5 sensorns tillst\u00e5nd", + "verify_ssl": "Aktiverar/inaktiverar verifiering av SSL/TLS-certifikat, till exempel om det \u00e4r sj\u00e4lvsignerat" } } } diff --git a/homeassistant/components/screenlogic/translations/sv.json b/homeassistant/components/screenlogic/translations/sv.json index 7be3515deb0..54ae7f2b212 100644 --- a/homeassistant/components/screenlogic/translations/sv.json +++ b/homeassistant/components/screenlogic/translations/sv.json @@ -13,5 +13,12 @@ } } } + }, + "options": { + "step": { + "init": { + "title": "ScreenLogic" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sense/translations/sv.json b/homeassistant/components/sense/translations/sv.json index 02939a27dbb..76e41ccb1ba 100644 --- a/homeassistant/components/sense/translations/sv.json +++ b/homeassistant/components/sense/translations/sv.json @@ -13,7 +13,8 @@ "data": { "email": "E-postadress", "password": "L\u00f6senord" - } + }, + "title": "Anslut till din Sense Energy Monitor" } } } diff --git a/homeassistant/components/sensor/translations/sv.json b/homeassistant/components/sensor/translations/sv.json index 49c49b16c69..0fae4fe1047 100644 --- a/homeassistant/components/sensor/translations/sv.json +++ b/homeassistant/components/sensor/translations/sv.json @@ -7,10 +7,12 @@ "is_current": "Nuvarande", "is_energy": "Nuvarande {entity_name} energi", "is_frequency": "Nuvarande frekvens", + "is_gas": "Nuvarande {entity_name} gas", "is_humidity": "Nuvarande {entity_name} fuktighet", "is_illuminance": "Nuvarande {entity_name} belysning", "is_nitrogen_dioxide": "Nuvarande {entity_name} koncentration av kv\u00e4vedioxid", "is_nitrogen_monoxide": "Nuvarande {entity_name} koncentration av kv\u00e4veoxid", + "is_nitrous_oxide": "Nuvarande koncentration av lustgas i {entity_name}.", "is_ozone": "Nuvarande {entity_name} koncentration av ozon", "is_pm1": "Nuvarande {entity_name} koncentration av PM1 partiklar", "is_pm10": "Nuvarande {entity_name} koncentration av PM10 partiklar", @@ -20,6 +22,7 @@ "is_pressure": "Aktuellt {entity_name} tryck", "is_reactive_power": "Nuvarande {entity_name} reaktiv effekt", "is_signal_strength": "Nuvarande {entity_name} signalstyrka", + "is_sulphur_dioxide": "Nuvarande koncentration av svaveldioxid i {entity_name}.", "is_temperature": "Aktuell {entity_name} temperatur", "is_value": "Nuvarande {entity_name} v\u00e4rde", "is_volatile_organic_compounds": "Nuvarande {entity_name} koncentration av flyktiga organiska \u00e4mnen", @@ -27,9 +30,13 @@ }, "trigger_type": { "battery_level": "{entity_name} batteriniv\u00e5 \u00e4ndras", + "carbon_dioxide": "{entity_name} f\u00f6r\u00e4ndringar av koldioxidkoncentrationen", + "carbon_monoxide": "{entity_name} f\u00f6r\u00e4ndringar av kolmonoxidkoncentrationen", "energy": "Energif\u00f6r\u00e4ndringar", + "gas": "{entity_name} gasf\u00f6r\u00e4ndringar", "humidity": "{entity_name} fuktighet \u00e4ndras", "illuminance": "{entity_name} belysning \u00e4ndras", + "nitrogen_dioxide": "{entity_name} kv\u00e4vedioxidkoncentrationen f\u00f6r\u00e4ndras.", "power": "{entity_name} effektf\u00f6r\u00e4ndringar", "power_factor": "effektfaktorf\u00f6r\u00e4ndringar", "pressure": "{entity_name} tryckf\u00f6r\u00e4ndringar", diff --git a/homeassistant/components/sensorpush/translations/no.json b/homeassistant/components/sensorpush/translations/no.json new file mode 100644 index 00000000000..28ec4582177 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/sv.json b/homeassistant/components/sentry/translations/sv.json index 45c4508ff9e..8c1deb2dc58 100644 --- a/homeassistant/components/sentry/translations/sv.json +++ b/homeassistant/components/sentry/translations/sv.json @@ -1,8 +1,34 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, "error": { "bad_dsn": "Ogiltig DSN", "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "dsn": "Sentry DSN" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "Valfritt namn p\u00e5 milj\u00f6n.", + "event_custom_components": "Skicka h\u00e4ndelser fr\u00e5n anpassade komponenter", + "event_handled": "Skicka hanterade h\u00e4ndelser", + "event_third_party_packages": "Skicka h\u00e4ndelser fr\u00e5n tredjepartspaket", + "logging_event_level": "Loggniv\u00e5n Sentry kommer att registrera en h\u00e4ndelse f\u00f6r", + "logging_level": "Loggniv\u00e5n Sentry kommer att registrera loggar som br\u00f6dtexter f\u00f6r", + "tracing": "Aktivera prestandasp\u00e5rning", + "tracing_sample_rate": "Samplingsfrekvens f\u00f6r sp\u00e5rning; mellan 0,0 och 1,0 (1,0 = 100 %)." + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/hu.json b/homeassistant/components/senz/translations/hu.json index a3b07f0d3ef..357d5729418 100644 --- a/homeassistant/components/senz/translations/hu.json +++ b/homeassistant/components/senz/translations/hu.json @@ -16,5 +16,11 @@ "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" } } + }, + "issues": { + "removed_yaml": { + "description": "Az nVent RAYCHEM SENZ YAML-ben megadott konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant nem haszn\u00e1lja.\n\nK\u00e9rem, t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "Az nVent RAYCHEM SENZ YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/no.json b/homeassistant/components/senz/translations/no.json index 6c384bb2e15..89ac20c8331 100644 --- a/homeassistant/components/senz/translations/no.json +++ b/homeassistant/components/senz/translations/no.json @@ -16,5 +16,11 @@ "title": "Velg Autentiserings metode" } } + }, + "issues": { + "removed_yaml": { + "description": "Konfigurering av nVent RAYCHEM SENZ med YAML er fjernet. \n\n Din eksisterende YAML-konfigurasjon brukes ikke av Home Assistant. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "nVent RAYCHEM SENZ YAML-konfigurasjonen er fjernet" + } } } \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/sv.json b/homeassistant/components/sharkiq/translations/sv.json index cae80c6c25f..2ef8a989166 100644 --- a/homeassistant/components/sharkiq/translations/sv.json +++ b/homeassistant/components/sharkiq/translations/sv.json @@ -1,6 +1,14 @@ { "config": { "abort": { + "already_configured": "Konto har redan konfigurerats", + "cannot_connect": "Det gick inte att ansluta.", + "reauth_successful": "\u00c5terautentisering lyckades", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, "step": { @@ -12,6 +20,7 @@ }, "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/shelly/translations/sv.json b/homeassistant/components/shelly/translations/sv.json index 21a5ba55b58..458f4be3b24 100644 --- a/homeassistant/components/shelly/translations/sv.json +++ b/homeassistant/components/shelly/translations/sv.json @@ -1,11 +1,13 @@ { "config": { "error": { - "firmware_not_fully_provisioned": "Enheten \u00e4r inte helt etablerad. Kontakta Shellys support" + "firmware_not_fully_provisioned": "Enheten \u00e4r inte helt etablerad. Kontakta Shellys support", + "invalid_auth": "Ogiltig autentisering" }, "step": { "credentials": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/sia/translations/sv.json b/homeassistant/components/sia/translations/sv.json index 67fd98a8827..fe4c471f302 100644 --- a/homeassistant/components/sia/translations/sv.json +++ b/homeassistant/components/sia/translations/sv.json @@ -1,7 +1,43 @@ { "config": { "error": { + "invalid_account_length": "Kontot har inte r\u00e4tt l\u00e4ngd, det m\u00e5ste vara mellan 3 och 16 tecken.", + "invalid_key_format": "Nyckeln \u00e4r inte ett hexadecimalt v\u00e4rde, anv\u00e4nd endast 0-9 och AF.", + "invalid_key_length": "Nyckeln har inte r\u00e4tt l\u00e4ngd, den m\u00e5ste vara 16, 24 eller 32 hexadecken.", + "invalid_ping": "Pingintervallet m\u00e5ste vara mellan 1 och 1440 minuter.", + "invalid_zones": "Det m\u00e5ste finnas minst 1 zon.", "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "additional_account": { + "data": { + "account": "Konto-ID", + "encryption_key": "Krypteringsnyckel", + "ping_interval": "Pingintervall (min)" + }, + "title": "L\u00e4gg till ett annat konto till den aktuella porten." + }, + "user": { + "data": { + "account": "Konto-ID", + "encryption_key": "Krypteringsnyckel", + "ping_interval": "Pingintervall (min)", + "port": "Port", + "protocol": "Protokoll" + }, + "title": "Skapa en anslutning f\u00f6r SIA-baserade larmsystem." + } + } + }, + "options": { + "step": { + "options": { + "data": { + "ignore_timestamps": "Ignorera tidsst\u00e4mpelkontrollen f\u00f6r SIA-h\u00e4ndelserna" + }, + "description": "St\u00e4ll in alternativen f\u00f6r kontot: {account}", + "title": "Alternativ f\u00f6r SIA-installationen." + } } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/hu.json b/homeassistant/components/simplepush/translations/hu.json index e5deb2bf2fc..b1f7b519dc5 100644 --- a/homeassistant/components/simplepush/translations/hu.json +++ b/homeassistant/components/simplepush/translations/hu.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "A Simplepush YAML-ben megadott konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a Simplepush YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Simplepush YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/no.json b/homeassistant/components/simplepush/translations/no.json index 78cf864a33d..5c2e447b098 100644 --- a/homeassistant/components/simplepush/translations/no.json +++ b/homeassistant/components/simplepush/translations/no.json @@ -17,5 +17,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Simplepush med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Simplepush YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet." + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index ece2b0a0dfb..5b2e898a1e5 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -7,7 +7,7 @@ "wrong_account": "A megadott felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok nem j\u00f3k ehhez a SimpliSafe fi\u00f3khoz." }, "error": { - "identifier_exists": "A fi\u00f3k m\u00e1r regisztr\u00e1lt", + "identifier_exists": "A fi\u00f3k m\u00e1r regisztr\u00e1lva van", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, @@ -34,7 +34,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Adja meg felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t." + "description": "A SimpliSafe a webes alkalmaz\u00e1son kereszt\u00fcl hiteles\u00edti a felhaszn\u00e1l\u00f3kat. A technikai korl\u00e1toz\u00e1sok miatt a folyamat v\u00e9g\u00e9n van egy k\u00e9zi l\u00e9p\u00e9s: k\u00e9rem, hogy a kezd\u00e9s el\u0151tt olvassa el a [dokument\u00e1ci\u00f3t](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code).\n\nHa k\u00e9szen \u00e1ll, kattintson [ide]({url}) a SimpliSafe webes alkalmaz\u00e1s megnyit\u00e1s\u00e1hoz \u00e9s a hiteles\u00edt\u0151 adatok megad\u00e1s\u00e1hoz. Ha a folyamat befejez\u0151d\u00f6tt, t\u00e9rjen vissza ide, \u00e9s adja meg a SimpliSafe webalkalmaz\u00e1s URL-c\u00edm\u00e9r\u0151l sz\u00e1rmaz\u00f3 enged\u00e9lyez\u00e9si k\u00f3dot." } } }, diff --git a/homeassistant/components/simplisafe/translations/sv.json b/homeassistant/components/simplisafe/translations/sv.json index 6a3d08b799c..e7e8ec28715 100644 --- a/homeassistant/components/simplisafe/translations/sv.json +++ b/homeassistant/components/simplisafe/translations/sv.json @@ -3,15 +3,24 @@ "abort": { "already_configured": "Det h\u00e4r SimpliSafe-kontot har redan konfigurerats.", "email_2fa_timed_out": "Tidsgr\u00e4nsen tog slut i v\u00e4ntan p\u00e5 tv\u00e5faktorsautentisering", + "reauth_successful": "\u00c5terautentisering lyckades", "wrong_account": "De angivna anv\u00e4ndaruppgifterna matchar inte detta SimpliSafe-konto." }, "error": { - "identifier_exists": "Kontot \u00e4r redan registrerat" + "identifier_exists": "Kontot \u00e4r redan registrerat", + "unknown": "Ov\u00e4ntat fel" }, "progress": { "email_2fa": "Kontrollera din e-post f\u00f6r en verifieringsl\u00e4nk fr\u00e5n Simplisafe." }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Skriv om l\u00f6senordet f\u00f6r {username}", + "title": "\u00c5terautenticera integration" + }, "sms_2fa": { "data": { "code": "Kod" @@ -26,5 +35,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "code": "Kod (anv\u00e4ndsi Home Assistant UI)" + }, + "title": "Konfigurera SimpliSafe" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sma/translations/sv.json b/homeassistant/components/sma/translations/sv.json new file mode 100644 index 00000000000..d957d820748 --- /dev/null +++ b/homeassistant/components/sma/translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "cannot_retrieve_device_info": "Ansluten, men det g\u00e5r inte att h\u00e4mta enhetsinformationen", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "group": "Grupp", + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "ssl": "Anv\u00e4nd ett SSL certifikat", + "verify_ssl": "Verifiera SSL-certifikat" + }, + "description": "Ange din SMA-enhetsinformation.", + "title": "St\u00e4ll in SMA Solar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/sv.json b/homeassistant/components/smappee/translations/sv.json new file mode 100644 index 00000000000..eb9b997abb7 --- /dev/null +++ b/homeassistant/components/smappee/translations/sv.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured_device": "Enheten \u00e4r redan konfigurerad", + "already_configured_local_device": "Lokala enheter \u00e4r redan konfigurerade. Ta bort dem f\u00f6rst innan du konfigurerar en molnenhet.", + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_mdns": "Enhet som inte st\u00f6ds f\u00f6r Smappee-integrationen.", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" + }, + "flow_title": "{name}", + "step": { + "environment": { + "data": { + "environment": "Milj\u00f6" + }, + "description": "Konfigurera din Smappee f\u00f6r att integrera med Home Assistant." + }, + "local": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Ange v\u00e4rden f\u00f6r att initiera Smappee lokala integration" + }, + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + }, + "zeroconf_confirm": { + "description": "Vill du l\u00e4gga till Smappee-enheten med serienumret {serialnumber} i Home Assistant?", + "title": "Uppt\u00e4ckta Smappee-enheter" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/sv.json b/homeassistant/components/smart_meter_texas/translations/sv.json index 23c825f256f..89cfc8f6c3b 100644 --- a/homeassistant/components/smart_meter_texas/translations/sv.json +++ b/homeassistant/components/smart_meter_texas/translations/sv.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/smartthings/translations/sv.json b/homeassistant/components/smartthings/translations/sv.json index 413a4279cdd..21591e7c256 100644 --- a/homeassistant/components/smartthings/translations/sv.json +++ b/homeassistant/components/smartthings/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_available_locations": "Det finns inga tillg\u00e4ngliga SmartThings-platser att st\u00e4lla in i Home Assistant." + }, "error": { "app_setup_error": "Det gick inte att installera Home Assistant SmartApp. V\u00e4nligen f\u00f6rs\u00f6k igen.", "token_forbidden": "Token har inte det som kr\u00e4vs inom omf\u00e5ng f\u00f6r OAuth.", @@ -11,10 +14,18 @@ "authorize": { "title": "Auktorisera Home Assistant" }, + "pat": { + "data": { + "access_token": "\u00c5tkomstnyckel" + }, + "description": "V\u00e4nligen ange en [personlig \u00e5tkomsttoken]({token_url}) f\u00f6r SmartThings som har skapats enligt [instruktionerna]({component_url}).", + "title": "Ange personlig \u00e5tkomsttoken" + }, "select_location": { "data": { "location_id": "Position" }, + "description": "V\u00e4lj den SmartThings-plats du vill l\u00e4gga till i Home Assistant. Vi \u00f6ppnar sedan ett nytt f\u00f6nster och ber dig att logga in och godk\u00e4nna installationen av Home Assistant-integrationen p\u00e5 den valda platsen.", "title": "V\u00e4lj plats" }, "user": { diff --git a/homeassistant/components/smarttub/translations/sv.json b/homeassistant/components/smarttub/translations/sv.json new file mode 100644 index 00000000000..bfa44298487 --- /dev/null +++ b/homeassistant/components/smarttub/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, + "step": { + "user": { + "data": { + "email": "E-post", + "password": "L\u00f6senord" + }, + "description": "Ange din SmartTub-e-postadress och ditt l\u00f6senord f\u00f6r att logga in", + "title": "Logga in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sms/translations/sv.json b/homeassistant/components/sms/translations/sv.json index 0020bfcfc37..539446618d9 100644 --- a/homeassistant/components/sms/translations/sv.json +++ b/homeassistant/components/sms/translations/sv.json @@ -1,10 +1,20 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { - "baud_speed": "Baud-hastighet" - } + "baud_speed": "Baud-hastighet", + "device": "Enhet" + }, + "title": "Anslut till modemet" } } } diff --git a/homeassistant/components/somfy_mylink/translations/sv.json b/homeassistant/components/somfy_mylink/translations/sv.json new file mode 100644 index 00000000000..9ab3fa7ec0c --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "flow_title": "{mac} ({ip})", + "step": { + "user": { + "description": "System-ID kan erh\u00e5llas i MyLink-appen under Integration genom att v\u00e4lja vilken tj\u00e4nst som helst som inte kommer fr\u00e5n molnet." + } + } + }, + "options": { + "abort": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "init": { + "title": "Konfigurera MyLink-alternativ" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/sv.json b/homeassistant/components/sonarr/translations/sv.json index 7745fb77e1b..9128ea57a38 100644 --- a/homeassistant/components/sonarr/translations/sv.json +++ b/homeassistant/components/sonarr/translations/sv.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/sonos/translations/sv.json b/homeassistant/components/sonos/translations/sv.json index 4f2202ff8f5..142c1409788 100644 --- a/homeassistant/components/sonos/translations/sv.json +++ b/homeassistant/components/sonos/translations/sv.json @@ -2,6 +2,7 @@ "config": { "abort": { "no_devices_found": "Inga Sonos-enheter hittades i n\u00e4tverket.", + "not_sonos_device": "Uppt\u00e4ckt enhet \u00e4r inte en Sonos-enhet", "single_instance_allowed": "Endast en enda konfiguration av Sonos \u00e4r n\u00f6dv\u00e4ndig." }, "step": { diff --git a/homeassistant/components/soundtouch/translations/hu.json b/homeassistant/components/soundtouch/translations/hu.json index 2e4b4c36d8a..b0406ad66b7 100644 --- a/homeassistant/components/soundtouch/translations/hu.json +++ b/homeassistant/components/soundtouch/translations/hu.json @@ -20,6 +20,7 @@ }, "issues": { "deprecated_yaml": { + "description": "A Bose SoundTouch YAML-ben megadott konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a Bose SoundTouch YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistant programot.", "title": "A Bose SoundTouch YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } } diff --git a/homeassistant/components/speedtestdotnet/translations/sv.json b/homeassistant/components/speedtestdotnet/translations/sv.json index 78b043f1a37..cde9095fba8 100644 --- a/homeassistant/components/speedtestdotnet/translations/sv.json +++ b/homeassistant/components/speedtestdotnet/translations/sv.json @@ -1,8 +1,20 @@ { + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du starta konfigurationen?" + } + } + }, "options": { "step": { "init": { "data": { + "manual": "Inaktivera automatisk uppdatering", + "scan_interval": "Uppdateringsfrekvens (minuter)", "server_name": "V\u00e4lj testserver" } } diff --git a/homeassistant/components/spider/translations/sv.json b/homeassistant/components/spider/translations/sv.json index 23c825f256f..d078c6d4110 100644 --- a/homeassistant/components/spider/translations/sv.json +++ b/homeassistant/components/spider/translations/sv.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/spotify/translations/hu.json b/homeassistant/components/spotify/translations/hu.json index 846ddaf5ce3..52b81a71faf 100644 --- a/homeassistant/components/spotify/translations/hu.json +++ b/homeassistant/components/spotify/translations/hu.json @@ -21,8 +21,8 @@ }, "issues": { "removed_yaml": { - "description": "A Spotify YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant nem haszn\u00e1lja.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", - "title": "A Spotify YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fclt" + "description": "A Spotify YAML-ben megadott konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant nem haszn\u00e1lja.\n\nK\u00e9rem, t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Spotify YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } }, "system_health": { diff --git a/homeassistant/components/spotify/translations/sv.json b/homeassistant/components/spotify/translations/sv.json index 0a64cad7a65..08c7e415edf 100644 --- a/homeassistant/components/spotify/translations/sv.json +++ b/homeassistant/components/spotify/translations/sv.json @@ -2,7 +2,8 @@ "config": { "abort": { "authorize_url_timeout": "Skapandet av en auktoriseringsadress \u00f6verskred tidsgr\u00e4nsen.", - "missing_configuration": "Spotify-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen." + "missing_configuration": "Spotify-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" }, "create_entry": { "default": "Lyckad autentisering med Spotify." @@ -18,5 +19,10 @@ "description": "Att konfigurera Spotify med YAML har tagits bort. \n\n Din befintliga YAML-konfiguration anv\u00e4nds inte av Home Assistant. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", "title": "Spotify YAML-konfigurationen har tagits bort" } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Spotify API-slutpunkt kan n\u00e5s" + } } } \ No newline at end of file diff --git a/homeassistant/components/sql/translations/sv.json b/homeassistant/components/sql/translations/sv.json index dec86c2b66b..fc2f876d479 100644 --- a/homeassistant/components/sql/translations/sv.json +++ b/homeassistant/components/sql/translations/sv.json @@ -11,10 +11,18 @@ "user": { "data": { "column": "Kolumn", - "name": "Namn" + "db_url": "Databas URL", + "name": "Namn", + "query": "V\u00e4lj fr\u00e5ga", + "unit_of_measurement": "M\u00e5ttenhet", + "value_template": "V\u00e4rdemall" }, "data_description": { + "column": "Kolumn f\u00f6r returnerad fr\u00e5ga f\u00f6r att presentera som tillst\u00e5nd", + "db_url": "URL till databasen, l\u00e4mna tomt om du vill anv\u00e4nda standarddatabasen f\u00f6r HA.", "name": "Namn som kommer att anv\u00e4ndas f\u00f6r konfigurationsinmatning och \u00e4ven f\u00f6r sensorn.", + "query": "Fr\u00e5ga som ska k\u00f6ras, m\u00e5ste b\u00f6rja med \"SELECT\"", + "unit_of_measurement": "M\u00e5ttenhet (valfritt)", "value_template": "V\u00e4rdemall (valfritt)" } } @@ -22,15 +30,26 @@ }, "options": { "error": { - "db_url_invalid": "Databasens URL \u00e4r ogiltig" + "db_url_invalid": "Databasens URL \u00e4r ogiltig", + "query_invalid": "SQL fr\u00e5ga \u00e4r ogiltig" }, "step": { "init": { "data": { - "name": "Namn" + "column": "Kolumn", + "db_url": "Databas URL", + "name": "Namn", + "query": "V\u00e4lj fr\u00e5ga", + "unit_of_measurement": "M\u00e5ttenhet", + "value_template": "V\u00e4rdemall" }, "data_description": { - "name": "Namn som kommer att anv\u00e4ndas f\u00f6r Config Entry och \u00e4ven sensorn" + "column": "Kolumn f\u00f6r returnerad fr\u00e5ga f\u00f6r att presentera som tillst\u00e5nd", + "db_url": "URL till databasen, l\u00e4mna tomt om du vill anv\u00e4nda standarddatabasen f\u00f6r HA.", + "name": "Namn som kommer att anv\u00e4ndas f\u00f6r Config Entry och \u00e4ven sensorn", + "query": "Fr\u00e5ga som ska k\u00f6ras, m\u00e5ste b\u00f6rja med \"SELECT\"", + "unit_of_measurement": "M\u00e5ttenhet (valfritt)", + "value_template": "V\u00e4rdemall (valfritt)" } } } diff --git a/homeassistant/components/squeezebox/translations/sv.json b/homeassistant/components/squeezebox/translations/sv.json index 796ddb0da2e..2957c503876 100644 --- a/homeassistant/components/squeezebox/translations/sv.json +++ b/homeassistant/components/squeezebox/translations/sv.json @@ -1,10 +1,25 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "no_server_found": "Ingen LMS server hittades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "no_server_found": "Kunde inte hitta servern automatiskt.", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{host}", "step": { "edit": { "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Redigera anslutningsinformation" }, "user": { "data": { diff --git a/homeassistant/components/srp_energy/translations/sv.json b/homeassistant/components/srp_energy/translations/sv.json index 880970c74ff..b2068a4d12d 100644 --- a/homeassistant/components/srp_energy/translations/sv.json +++ b/homeassistant/components/srp_energy/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, "error": { "cannot_connect": "Kan inte ansluta", "unknown": "Ov\u00e4ntat fel" diff --git a/homeassistant/components/steam_online/translations/hu.json b/homeassistant/components/steam_online/translations/hu.json index 5dcc97464c8..1902d129c52 100644 --- a/homeassistant/components/steam_online/translations/hu.json +++ b/homeassistant/components/steam_online/translations/hu.json @@ -26,8 +26,8 @@ }, "issues": { "removed_yaml": { - "description": "A Steam YAML haszn\u00e1lat\u00e1val t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML-konfigur\u00e1ci\u00f3t a Home Assistant nem haszn\u00e1lja.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", - "title": "A Steam YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fclt" + "description": "A Steam YAML-ben megadott konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant nem haszn\u00e1lja.\n\nK\u00e9rem, t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Steam YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } }, "options": { diff --git a/homeassistant/components/subaru/translations/sv.json b/homeassistant/components/subaru/translations/sv.json index 23c825f256f..38d552ff4aa 100644 --- a/homeassistant/components/subaru/translations/sv.json +++ b/homeassistant/components/subaru/translations/sv.json @@ -1,6 +1,34 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "cannot_connect": "Det gick inte att ansluta." + }, + "error": { + "bad_pin_format": "PIN-koden ska vara fyra siffror", + "bad_validation_code_format": "Valideringskoden ska vara 6 siffror", + "cannot_connect": "Det gick inte att ansluta.", + "incorrect_pin": "Felaktig PIN-kod", + "incorrect_validation_code": "Felaktig valideringkod", + "invalid_auth": "Ogiltig autentisering" + }, "step": { + "pin": { + "data": { + "pin": "PIN-kod" + }, + "description": "Ange din MySubaru PIN-kod\n OBS: Alla fordon p\u00e5 kontot m\u00e5ste ha samma PIN-kod" + }, + "two_factor": { + "description": "Tv\u00e5faktorautentisering kr\u00e4vs", + "title": "Subaru Starlink-konfiguration" + }, + "two_factor_validate": { + "data": { + "validation_code": "Valideringskod" + }, + "title": "Subaru Starlink-konfiguration" + }, "user": { "data": { "username": "Anv\u00e4ndarnamn" diff --git a/homeassistant/components/switchbot/translations/hu.json b/homeassistant/components/switchbot/translations/hu.json index e44bfe7c811..bd363de0738 100644 --- a/homeassistant/components/switchbot/translations/hu.json +++ b/homeassistant/components/switchbot/translations/hu.json @@ -11,7 +11,7 @@ "one": "\u00dcres", "other": "\u00dcres" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { diff --git a/homeassistant/components/syncthru/translations/sv.json b/homeassistant/components/syncthru/translations/sv.json index 76687105e49..29f0e7519a3 100644 --- a/homeassistant/components/syncthru/translations/sv.json +++ b/homeassistant/components/syncthru/translations/sv.json @@ -1,7 +1,27 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { - "invalid_url": "Ogiltig URL" + "invalid_url": "Ogiltig URL", + "syncthru_not_supported": "Enheten st\u00f6der inte SyncThru", + "unknown_state": "Skrivarens status ok\u00e4nd, verifiera URL och n\u00e4tverksanslutning" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "data": { + "name": "Namn", + "url": "Webbgr\u00e4nssnitt URL" + } + }, + "user": { + "data": { + "name": "Namn", + "url": "Webbgr\u00e4nssnitt URL" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/sv.json b/homeassistant/components/synology_dsm/translations/sv.json index 012d092de41..acfc243382d 100644 --- a/homeassistant/components/synology_dsm/translations/sv.json +++ b/homeassistant/components/synology_dsm/translations/sv.json @@ -1,14 +1,31 @@ { "config": { "abort": { - "already_configured": "V\u00e4rden \u00e4r redan konfigurerad." + "already_configured": "V\u00e4rden \u00e4r redan konfigurerad.", + "reauth_successful": "\u00c5terautentisering lyckades", + "reconfigure_successful": "Omkonfigurationen lyckades" }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "missing_data": "Saknade data: f\u00f6rs\u00f6k igen senare eller en annan konfiguration", + "otp_failed": "Tv\u00e5stegsautentisering misslyckades, f\u00f6rs\u00f6k igen med ett nytt l\u00f6senord" + }, + "flow_title": "{name} ({host})", "step": { + "2sa": { + "data": { + "otp_code": "Kod" + }, + "title": "Synology DSM: tv\u00e5stegsautentisering" + }, "link": { "data": { "password": "L\u00f6senord", "port": "Port (Valfri)", - "username": "Anv\u00e4ndarnamn" + "ssl": "Anv\u00e4nd ett SSL certifikat", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera SSL-certifikat" }, "description": "Do vill du konfigurera {name} ({host})?" }, @@ -21,7 +38,10 @@ "data": { "host": "V\u00e4rd", "password": "L\u00f6senord", - "username": "Anv\u00e4ndarnamn" + "port": "Port", + "ssl": "Anv\u00e4nd ett SSL certifikat", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera SSL-certifikat" } } } diff --git a/homeassistant/components/tado/translations/sv.json b/homeassistant/components/tado/translations/sv.json index f6813633b80..23ad948cdb6 100644 --- a/homeassistant/components/tado/translations/sv.json +++ b/homeassistant/components/tado/translations/sv.json @@ -25,6 +25,7 @@ "data": { "fallback": "Aktivera reservl\u00e4ge." }, + "description": "Med Fallback-l\u00e4get kan du v\u00e4lja n\u00e4r du vill \u00e5terg\u00e5 till Smart Schedule fr\u00e5n din manuella zon\u00f6verl\u00e4ggning. (NEXT_TIME_BLOCK:= Byt vid n\u00e4sta \u00e4ndring av Smart Schedule; MANUAL:= Byt inte f\u00f6rr\u00e4n du avbryter; TADO_DEFAULT:= Byt baserat p\u00e5 din inst\u00e4llning i Tado App).", "title": "Justera Tado inst\u00e4llningarna." } } diff --git a/homeassistant/components/tankerkoenig/translations/sv.json b/homeassistant/components/tankerkoenig/translations/sv.json index f183c4ccb29..55c362cc717 100644 --- a/homeassistant/components/tankerkoenig/translations/sv.json +++ b/homeassistant/components/tankerkoenig/translations/sv.json @@ -38,8 +38,10 @@ "init": { "data": { "scan_interval": "Uppdateringsintervall", + "show_on_map": "Visa stationer p\u00e5 kartan", "stations": "Stationer" - } + }, + "title": "Tankerkoenig inst\u00e4llningar" } } } diff --git a/homeassistant/components/tasmota/translations/sv.json b/homeassistant/components/tasmota/translations/sv.json new file mode 100644 index 00000000000..df8bff40946 --- /dev/null +++ b/homeassistant/components/tasmota/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "invalid_discovery_topic": "Ogiltigt \u00e4mnesprefix f\u00f6r uppt\u00e4ckt." + }, + "step": { + "config": { + "data": { + "discovery_prefix": "Uppt\u00e4ckt \u00e4mnesprefix" + } + }, + "confirm": { + "description": "Vill du konfigurera Tasmota?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/sv.json b/homeassistant/components/tellduslive/translations/sv.json index 9b45e05fe9c..98fa56f928f 100644 --- a/homeassistant/components/tellduslive/translations/sv.json +++ b/homeassistant/components/tellduslive/translations/sv.json @@ -4,6 +4,9 @@ "authorize_url_timeout": "Timeout n\u00e4r genererar auktorisera url.", "unknown": "Ok\u00e4nt fel intr\u00e4ffade" }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, "step": { "auth": { "description": "F\u00f6r att l\u00e4nka ditt \"Telldus Live!\" konto: \n 1. Klicka p\u00e5 l\u00e4nken nedan \n 2. Logga in p\u00e5 Telldus Live!\n 3. Godk\u00e4nn **{app_name}** (klicka **Yes**). \n 4. Kom tillbaka hit och klicka p\u00e5 **SUBMIT**. \n\n [L\u00e4nk till Telldus Live konto]({auth_url})", diff --git a/homeassistant/components/tibber/translations/sv.json b/homeassistant/components/tibber/translations/sv.json index 1fda5b91f5a..604e18c3659 100644 --- a/homeassistant/components/tibber/translations/sv.json +++ b/homeassistant/components/tibber/translations/sv.json @@ -1,10 +1,19 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_access_token": "Ogiltig \u00e5tkomstnyckel", + "timeout": "Timeout f\u00f6r anslutning till Tibber" + }, "step": { "user": { "data": { "access_token": "\u00c5tkomstnyckel" - } + }, + "description": "Ange din \u00e5tkomsttoken fr\u00e5n https://developer.tibber.com/settings/accesstoken" } } } diff --git a/homeassistant/components/tile/translations/sv.json b/homeassistant/components/tile/translations/sv.json index 26e9f2d6a49..b3a1bc0e169 100644 --- a/homeassistant/components/tile/translations/sv.json +++ b/homeassistant/components/tile/translations/sv.json @@ -1,10 +1,25 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "E-postadress" - } + }, + "title": "Konfigurera Tile" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_inactive": "Visa inaktiva Tiles" + }, + "title": "Konfigurera Tile" } } } diff --git a/homeassistant/components/toon/translations/sv.json b/homeassistant/components/toon/translations/sv.json index 034f5f41e68..95864e9bbe3 100644 --- a/homeassistant/components/toon/translations/sv.json +++ b/homeassistant/components/toon/translations/sv.json @@ -1,7 +1,23 @@ { "config": { "abort": { - "no_agreements": "Det h\u00e4r kontot har inga Toon-sk\u00e4rmar." + "already_configured": "Det valda avtalet \u00e4r redan konfigurerat.", + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "no_agreements": "Det h\u00e4r kontot har inga Toon-sk\u00e4rmar.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" + }, + "step": { + "agreement": { + "data": { + "agreement": "Avtal" + }, + "description": "V\u00e4lj den avtalsadress du vill l\u00e4gga till.", + "title": "V\u00e4lj ditt avtal" + }, + "pick_implementation": { + "title": "V\u00e4lj din klientorganisation att autentisera med" + } } } } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/sv.json b/homeassistant/components/totalconnect/translations/sv.json index 8504a696619..064a6b2f9d3 100644 --- a/homeassistant/components/totalconnect/translations/sv.json +++ b/homeassistant/components/totalconnect/translations/sv.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Kontot har redan konfigurerats" }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tplink/translations/sv.json b/homeassistant/components/tplink/translations/sv.json index c11ec674c36..923647633d9 100644 --- a/homeassistant/components/tplink/translations/sv.json +++ b/homeassistant/components/tplink/translations/sv.json @@ -1,7 +1,22 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "no_devices_found": "Inga TP-Link enheter hittades p\u00e5 n\u00e4tverket." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{name} {model} ({host})", + "step": { + "discovery_confirm": { + "description": "Vill du konfigurera {name} {model} ( {host} )?" + }, + "pick_device": { + "data": { + "device": "Enhet" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/traccar/translations/sv.json b/homeassistant/components/traccar/translations/sv.json index 274de7cfe7b..b6858100aa4 100644 --- a/homeassistant/components/traccar/translations/sv.json +++ b/homeassistant/components/traccar/translations/sv.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." + }, "create_entry": { "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du st\u00e4lla in webhook-funktionen i Traccar.\n\nAnv\u00e4nd f\u00f6ljande url: `{webhook_url}`\n\nMer information finns i [dokumentationen]({docs_url})." }, diff --git a/homeassistant/components/tractive/translations/sv.json b/homeassistant/components/tractive/translations/sv.json new file mode 100644 index 00000000000..112ae3cdcfa --- /dev/null +++ b/homeassistant/components/tractive/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_failed_existing": "Det gick inte att uppdatera konfigurationsposten, ta bort integrationen och konfigurera den igen.", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "email": "E-post", + "password": "L\u00f6senord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/translations/sv.json b/homeassistant/components/twilio/translations/sv.json index c2e9f425f1c..8bb70f9cd64 100644 --- a/homeassistant/components/twilio/translations/sv.json +++ b/homeassistant/components/twilio/translations/sv.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." + }, "create_entry": { "default": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera [Webhooks med Twilio]({twilio_url}).\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\n Se [dokumentationen]({docs_url}) om hur du konfigurerar automatiseringar f\u00f6r att hantera inkommande data." }, diff --git a/homeassistant/components/twinkly/translations/sv.json b/homeassistant/components/twinkly/translations/sv.json new file mode 100644 index 00000000000..9b541d7ceac --- /dev/null +++ b/homeassistant/components/twinkly/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "device_exists": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ukraine_alarm/translations/sv.json b/homeassistant/components/ukraine_alarm/translations/sv.json index cd280080774..3e8c6255251 100644 --- a/homeassistant/components/ukraine_alarm/translations/sv.json +++ b/homeassistant/components/ukraine_alarm/translations/sv.json @@ -15,9 +15,15 @@ "description": "Om du inte bara vill \u00f6vervaka stat och distrikt, v\u00e4lj dess specifika gemenskap" }, "district": { + "data": { + "region": "Region" + }, "description": "Om du inte bara vill \u00f6vervaka staten, v\u00e4lj dess specifika distrikt" }, "user": { + "data": { + "region": "Region" + }, "description": "V\u00e4lj tillst\u00e5nd att \u00f6vervaka" } } diff --git a/homeassistant/components/unifi/translations/sv.json b/homeassistant/components/unifi/translations/sv.json index bea79d27094..3c7e9dcc110 100644 --- a/homeassistant/components/unifi/translations/sv.json +++ b/homeassistant/components/unifi/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Controller-platsen \u00e4r redan konfigurerad" + "already_configured": "Controller-platsen \u00e4r redan konfigurerad", + "configuration_updated": "Konfigurationen uppdaterad" }, "error": { "faulty_credentials": "Felaktiga anv\u00e4ndaruppgifter", @@ -28,11 +29,18 @@ }, "step": { "client_control": { + "data": { + "block_client": "N\u00e4tverks\u00e5tkomstkontrollerade klienter", + "dpi_restrictions": "Till\u00e5t kontroll av DPI-restriktionsgrupper", + "poe_clients": "Till\u00e5t POE-kontroll av klienter" + }, + "description": "Konfigurera klientkontroller \n\n Skapa switchar f\u00f6r serienummer som du vill kontrollera n\u00e4tverks\u00e5tkomst f\u00f6r.", "title": "UniFi-inst\u00e4llningar 2/3" }, "device_tracker": { "data": { "detection_time": "Tid i sekunder fr\u00e5n senast sett tills den anses borta", + "ignore_wired_bug": "Inaktivera logiken f\u00f6r fel i UniFi Network med kabelanslutning", "ssid_filter": "V\u00e4lj SSID att sp\u00e5ra tr\u00e5dl\u00f6sa klienter p\u00e5", "track_clients": "Sp\u00e5ra n\u00e4tverksklienter", "track_devices": "Sp\u00e5ra n\u00e4tverksenheter (Ubiquiti-enheter)", @@ -48,6 +56,11 @@ } }, "simple_options": { + "data": { + "block_client": "N\u00e4tverks\u00e5tkomstkontrollerade klienter", + "track_clients": "Sp\u00e5ra n\u00e4tverksklienter", + "track_devices": "Sp\u00e5ra n\u00e4tverksenheter (Ubiquiti-enheter)" + }, "description": "Konfigurera UniFi-integrationen" }, "statistics_sensors": { diff --git a/homeassistant/components/upb/translations/sv.json b/homeassistant/components/upb/translations/sv.json index f1f229cc175..03daf756094 100644 --- a/homeassistant/components/upb/translations/sv.json +++ b/homeassistant/components/upb/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { "cannot_connect": "Det gick inte att ansluta till UPB PIM, f\u00f6rs\u00f6k igen.", "invalid_upb_file": "Saknar eller ogiltig UPB UPStart-exportfil, kontrollera filens namn och s\u00f6kv\u00e4g.", @@ -9,8 +12,10 @@ "user": { "data": { "address": "Adress (se beskrivning ovan)", + "file_path": "S\u00f6kv\u00e4g och namn f\u00f6r UPStart UPB-exportfilen.", "protocol": "Protokoll" }, + "description": "Anslut en Universal Powerline Bus Powerline Interface Module (UPB PIM). Adressstr\u00e4ngen m\u00e5ste vara i formen 'adress[:port]' f\u00f6r 'tcp'. Porten \u00e4r valfri och har som standard 2101. Exempel: '192.168.1.42'. F\u00f6r det seriella protokollet m\u00e5ste adressen vara i formen 'tty[:baud]'. Bauden \u00e4r valfri och \u00e4r standard till 4800. Exempel: '/dev/ttyS1'.", "title": "Anslut till UPB PIM" } } diff --git a/homeassistant/components/upcloud/translations/sv.json b/homeassistant/components/upcloud/translations/sv.json index 23c825f256f..4c3c154261d 100644 --- a/homeassistant/components/upcloud/translations/sv.json +++ b/homeassistant/components/upcloud/translations/sv.json @@ -1,11 +1,25 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Uppdateringsintervall i sekunder, minst 30" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/sv.json b/homeassistant/components/upnp/translations/sv.json index 5ffe4a62f26..a067f819bba 100644 --- a/homeassistant/components/upnp/translations/sv.json +++ b/homeassistant/components/upnp/translations/sv.json @@ -2,11 +2,18 @@ "config": { "abort": { "already_configured": "UPnP/IGD \u00e4r redan konfigurerad", + "incomplete_discovery": "Ofullst\u00e4ndig uppt\u00e4ckt", "no_devices_found": "Inga UPnP/IGD-enheter hittades p\u00e5 n\u00e4tverket." }, "error": { "one": "En", "other": "Andra" + }, + "flow_title": "{name}", + "step": { + "ssdp_confirm": { + "description": "Vill du konfigurera denna UPnP/IGD enhet?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sv.json b/homeassistant/components/uptimerobot/translations/sv.json index 5ad5b5b6db4..d9db1fc1deb 100644 --- a/homeassistant/components/uptimerobot/translations/sv.json +++ b/homeassistant/components/uptimerobot/translations/sv.json @@ -1,15 +1,30 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_failed_existing": "Det gick inte att uppdatera konfigurationsposten, ta bort integrationen och konfigurera den igen.", + "reauth_successful": "\u00c5terautentisering lyckades", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_api_key": "Ogiltig API-nyckel", + "reauth_failed_matching_account": "API-nyckeln du angav matchar inte konto-id:t f\u00f6r befintlig konfiguration.", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "reauth_confirm": { "data": { "api_key": "API-nyckel" - } + }, + "description": "Du m\u00e5ste ange en ny \"huvud\" API-nyckel fr\u00e5n UptimeRobot", + "title": "\u00c5terautenticera integration" }, "user": { "data": { "api_key": "API-nyckel" - } + }, + "description": "Du m\u00e5ste ange \"huvud\" API-nyckeln fr\u00e5n UptimeRobot" } } } diff --git a/homeassistant/components/vera/translations/sv.json b/homeassistant/components/vera/translations/sv.json new file mode 100644 index 00000000000..8c7335c2b13 --- /dev/null +++ b/homeassistant/components/vera/translations/sv.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "cannot_connect": "Kunde inte ansluta till kontrollern med URL {base_url}" + }, + "step": { + "user": { + "data": { + "exclude": "Vera enhets-id att utesluta fr\u00e5n Home Assistant.", + "lights": "Vera switch enhets-ID att behandla som lampor i Home Assistant.", + "vera_controller_url": "Controller-URL" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "Vera enhets-id att utesluta fr\u00e5n Home Assistant.", + "lights": "Vera switch enhets-ID att behandla som lampor i Home Assistant." + }, + "description": "Se dokumentationen om vera f\u00f6r mer information om valfria parametrar: https://www.home-assistant.io/integrations/vera/. Observera: Alla \u00e4ndringar h\u00e4r kr\u00e4ver en omstart av Home Assistant-servern. Om du vill radera v\u00e4rden anger du ett mellanslag.", + "title": "Alternativ f\u00f6r Vera-kontroller" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/translations/sv.json b/homeassistant/components/vesync/translations/sv.json index 4621636cecc..8cfb8ce52f7 100644 --- a/homeassistant/components/vesync/translations/sv.json +++ b/homeassistant/components/vesync/translations/sv.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vizio/translations/sv.json b/homeassistant/components/vizio/translations/sv.json index 82483d80fe8..dc1f5514d80 100644 --- a/homeassistant/components/vizio/translations/sv.json +++ b/homeassistant/components/vizio/translations/sv.json @@ -1,8 +1,14 @@ { "config": { "abort": { + "already_configured_device": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", "updated_entry": "Den h\u00e4r posten har redan konfigurerats, men namnet och/eller alternativen som definierats i konfigurationen matchar inte den tidigare importerade konfigurationen och d\u00e4rf\u00f6r har konfigureringsposten uppdaterats i enlighet med detta." }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "existing_config_entry_found": "En befintlig St\u00e4ll in Vizio SmartCast-klient konfigurationspost med samma serienummer har redan konfigurerats. Du m\u00e5ste ta bort den befintliga posten f\u00f6r att konfigurera denna." + }, "step": { "pair_tv": { "data": { @@ -12,9 +18,11 @@ "title": "Slutf\u00f6r parningsprocessen" }, "pairing_complete": { + "description": "Din St\u00e4ll in Vizio SmartCast-klient \u00e4r nu ansluten till din Home Assistant.", "title": "Parkopplingen slutf\u00f6rd" }, "pairing_complete_import": { + "description": "Din St\u00e4ll in Vizio SmartCast-klient \u00e4r nu ansluten till din Home Assistant.\n\nDin \u00c5tkomstnyckel \u00e4r '**{access_token}**'.", "title": "Parkopplingen slutf\u00f6rd" }, "user": { @@ -24,6 +32,7 @@ "host": ":", "name": "Namn" }, + "description": "En \u00c5tkomstnyckel beh\u00f6vs endast f\u00f6r TV. Om du konfigurerar en TV och inte har en \u00c5tkomstnyckel \u00e4n, l\u00e4mna blank f\u00f6r att starta en parningsprocess.", "title": "St\u00e4ll in Vizio SmartCast-klient" } } @@ -36,6 +45,7 @@ "include_or_exclude": "Inkludera eller exkludera appar?", "volume_step": "Storlek p\u00e5 volymsteg" }, + "description": "Om du har en Smart TV kan du valfritt filtrera din k\u00e4lllista genom att v\u00e4lja vilka appar som ska inkluderas eller uteslutas i din k\u00e4lllista.", "title": "Uppdatera Vizo SmartCast-alternativ" } } diff --git a/homeassistant/components/volumio/translations/sv.json b/homeassistant/components/volumio/translations/sv.json new file mode 100644 index 00000000000..5eb83302226 --- /dev/null +++ b/homeassistant/components/volumio/translations/sv.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Kan inte ansluta till uppt\u00e4ckt Volumio" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "discovery_confirm": { + "description": "Vill du l\u00e4gga till Volumio ` {name} ` till Home Assistant?", + "title": "Uppt\u00e4ckt Volumio" + }, + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vulcan/translations/sv.json b/homeassistant/components/vulcan/translations/sv.json index f62155f1fd3..cbe2d7174b1 100644 --- a/homeassistant/components/vulcan/translations/sv.json +++ b/homeassistant/components/vulcan/translations/sv.json @@ -1,7 +1,17 @@ { "config": { "abort": { - "no_matching_entries": "Inga matchande poster hittades, anv\u00e4nd ett annat konto eller ta bort integration med f\u00f6r\u00e5ldrad student.." + "all_student_already_configured": "Alla studenter har redan lagts till.", + "already_configured": "Studenten har redan lagts till.", + "no_matching_entries": "Inga matchande poster hittades, anv\u00e4nd ett annat konto eller ta bort integration med f\u00f6r\u00e5ldrad student..", + "reauth_successful": "Reautentisering lyckades" + }, + "error": { + "expired_token": "Token som har upph\u00f6rt att g\u00e4lla \u2013 generera en ny token", + "invalid_pin": "Ogiltig pin", + "invalid_symbol": "Ogiltig symbol", + "invalid_token": "Ogiltig token", + "unknown": "Ett ok\u00e4nt fel har intr\u00e4ffat" }, "step": { "add_next_config_entry": { diff --git a/homeassistant/components/wallbox/translations/sv.json b/homeassistant/components/wallbox/translations/sv.json index 8a60ea1a5dc..79a05e057b8 100644 --- a/homeassistant/components/wallbox/translations/sv.json +++ b/homeassistant/components/wallbox/translations/sv.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "reauth_confirm": { "data": { @@ -8,6 +16,8 @@ }, "user": { "data": { + "password": "L\u00f6senord", + "station": "Stationens serienummer", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/water_heater/translations/sv.json b/homeassistant/components/water_heater/translations/sv.json index cb4826c461a..9b45fec830e 100644 --- a/homeassistant/components/water_heater/translations/sv.json +++ b/homeassistant/components/water_heater/translations/sv.json @@ -1,6 +1,15 @@ { + "device_automation": { + "action_type": { + "turn_off": "St\u00e4ng av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + } + }, "state": { "_": { + "eco": "Eco", + "electric": "Elektrisk", + "gas": "Gas", "heat_pump": "V\u00e4rmepump", "off": "Av" } diff --git a/homeassistant/components/watttime/translations/sv.json b/homeassistant/components/watttime/translations/sv.json index 23c825f256f..dadc8b53d2d 100644 --- a/homeassistant/components/watttime/translations/sv.json +++ b/homeassistant/components/watttime/translations/sv.json @@ -1,10 +1,18 @@ { "config": { "step": { + "location": { + "data": { + "location_type": "Plats" + }, + "description": "V\u00e4lj en plats att \u00f6vervaka:" + }, "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange ditt anv\u00e4ndarnamn och l\u00f6senord:" } } } diff --git a/homeassistant/components/waze_travel_time/translations/sv.json b/homeassistant/components/waze_travel_time/translations/sv.json index 84113d1284e..05407519a87 100644 --- a/homeassistant/components/waze_travel_time/translations/sv.json +++ b/homeassistant/components/waze_travel_time/translations/sv.json @@ -7,6 +7,7 @@ "user": { "data": { "destination": "Destination", + "name": "Namn", "origin": "Ursprung", "region": "Region" } diff --git a/homeassistant/components/wiffi/translations/sv.json b/homeassistant/components/wiffi/translations/sv.json index 86a5180fbfd..1fdd2ec4aa7 100644 --- a/homeassistant/components/wiffi/translations/sv.json +++ b/homeassistant/components/wiffi/translations/sv.json @@ -3,5 +3,14 @@ "abort": { "addr_in_use": "Serverporten \u00e4r upptagen." } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Timeout (minuter)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/withings/translations/sv.json b/homeassistant/components/withings/translations/sv.json index 4adb00bfad5..8bf931882f4 100644 --- a/homeassistant/components/withings/translations/sv.json +++ b/homeassistant/components/withings/translations/sv.json @@ -1,12 +1,18 @@ { "config": { "abort": { + "already_configured": "Konfiguration uppdaterad f\u00f6r profilen", "authorize_url_timeout": "Skapandet av en auktoriseringsadress \u00f6verskred tidsgr\u00e4nsen.", - "missing_configuration": "Withings-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen." + "missing_configuration": "Withings-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" }, "create_entry": { "default": "Lyckad autentisering med Withings." }, + "error": { + "already_configured": "Konto har redan konfigurerats" + }, + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "V\u00e4lj autentiseringsmetod" @@ -18,6 +24,10 @@ "description": "Vilken profil valde du p\u00e5 Withings webbplats? Det \u00e4r viktigt att profilerna matchar, annars kommer data att vara felm\u00e4rkta.", "title": "Anv\u00e4ndarprofil." }, + "reauth": { + "description": "Profilen \" {profile} \" m\u00e5ste autentiseras p\u00e5 nytt f\u00f6r att kunna forts\u00e4tta att ta emot Withings-data.", + "title": "\u00c5terautenticera integration" + }, "reauth_confirm": { "description": "Profilen \" {profile} \" m\u00e5ste autentiseras p\u00e5 nytt f\u00f6r att kunna forts\u00e4tta att ta emot Withings-data.", "title": "G\u00f6r om autentiseringen f\u00f6r integrationen" diff --git a/homeassistant/components/wled/translations/sv.json b/homeassistant/components/wled/translations/sv.json index a795bd52359..9ebff05a36c 100644 --- a/homeassistant/components/wled/translations/sv.json +++ b/homeassistant/components/wled/translations/sv.json @@ -2,8 +2,12 @@ "config": { "abort": { "already_configured": "Enheten har redan konfigurerats", + "cannot_connect": "Det gick inte att ansluta.", "cct_unsupported": "Denna WLED-enhet anv\u00e4nder CCT-kanaler, vilket inte st\u00f6ds av denna integration" }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "flow_title": "WLED: {name}", "step": { "user": { @@ -17,5 +21,14 @@ "title": "Uppt\u00e4ckte WLED-enhet" } } + }, + "options": { + "step": { + "init": { + "data": { + "keep_master_light": "Beh\u00e5ll huvudljuset, \u00e4ven med 1 LED-segment." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.sv.json b/homeassistant/components/wolflink/translations/sensor.sv.json index ddfd466dce2..797a7d6a230 100644 --- a/homeassistant/components/wolflink/translations/sensor.sv.json +++ b/homeassistant/components/wolflink/translations/sensor.sv.json @@ -1,10 +1,55 @@ { "state": { "wolflink__state": { + "1_x_warmwasser": "1 x varmvatten", + "abgasklappe": "R\u00f6kgasspj\u00e4ll", + "absenkbetrieb": "\u00c5terst\u00e4llningsl\u00e4ge", + "absenkstop": "Stopp f\u00f6r \u00e5terst\u00e4llning", "aktiviert": "Aktiverad", + "antilegionellenfunktion": "Anti-legionella funktion", + "at_abschaltung": "OT-avst\u00e4ngning", + "at_frostschutz": "OT frostskydd", "aus": "Inaktiverad", + "auto": "Automatiskt", + "estrichtrocknung": "Avj\u00e4mningstorkning", + "externe_deaktivierung": "Extern avaktivering", + "fernschalter_ein": "Fj\u00e4rrkontroll aktiverad", + "frost_heizkreis": "Frost p\u00e5 v\u00e4rmekretsen", + "frost_warmwasser": "Varmvatten frost", + "frostschutz": "Frostskydd", + "gasdruck": "Gastryck", + "glt_betrieb": "BMS-l\u00e4ge", + "gradienten_uberwachung": "Gradient\u00f6vervakning", + "heizbetrieb": "V\u00e4rmel\u00e4ge", + "heizgerat_mit_speicher": "Panna med cylinder", + "heizung": "V\u00e4rmer", + "initialisierung": "Initiering", + "kalibration": "Kalibrering", + "kalibration_heizbetrieb": "V\u00e4rmel\u00e4ge kalibrering", + "kalibration_kombibetrieb": "Kombinationsl\u00e4ge kalibrering", + "kalibration_warmwasserbetrieb": "DHW kalibrering", + "kaskadenbetrieb": "Kaskaddrift", + "kombibetrieb": "Kombil\u00e4ge", + "kombigerat": "Kombipanna", + "kombigerat_mit_solareinbindung": "Kombipanna med solintegration", + "mindest_kombizeit": "Minsta kombitid", + "nachlauf_heizkreispumpe": "Pump f\u00f6r uppv\u00e4rmningskretsen ig\u00e5ng", + "nachspulen": "Efterspolning", + "nur_heizgerat": "Enbart panna", + "parallelbetrieb": "Parallellt l\u00e4ge", + "partymodus": "Festl\u00e4ge", + "perm_cooling": "PermCooling", + "permanent": "Permanent", + "permanentbetrieb": "Permanent l\u00e4ge", "sparen": "Ekonomi", - "test": "Test" + "test": "Test", + "vorspulen": "Ing\u00e5ngssk\u00f6ljning", + "warmwasser": "Varmvatten", + "warmwasser_schnellstart": "Snabbstart f\u00f6r varmvatten", + "warmwasserbetrieb": "Varmvattenl\u00e4ge", + "warmwassernachlauf": "Varmvatten p\u00e5slaget", + "warmwasservorrang": "Prioritet f\u00f6r varmvattenberedning", + "zunden": "T\u00e4ndning" } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sv.json b/homeassistant/components/wolflink/translations/sv.json index 78879942876..28174fb1693 100644 --- a/homeassistant/components/wolflink/translations/sv.json +++ b/homeassistant/components/wolflink/translations/sv.json @@ -1,11 +1,26 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { + "device": { + "data": { + "device_name": "Enhet" + }, + "title": "V\u00e4lj WOLF-enhet" + }, "user": { "data": { "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "WOLF SmartSet-anslutning" } } } diff --git a/homeassistant/components/ws66i/translations/sv.json b/homeassistant/components/ws66i/translations/sv.json index 9daf43b9987..2ed5262ea3d 100644 --- a/homeassistant/components/ws66i/translations/sv.json +++ b/homeassistant/components/ws66i/translations/sv.json @@ -9,6 +9,9 @@ }, "step": { "user": { + "data": { + "ip_address": "IP-adress" + }, "title": "Anslut till enheten" } } diff --git a/homeassistant/components/xbox/translations/hu.json b/homeassistant/components/xbox/translations/hu.json index b7c32ad8008..357008f5623 100644 --- a/homeassistant/components/xbox/translations/hu.json +++ b/homeassistant/components/xbox/translations/hu.json @@ -16,6 +16,7 @@ }, "issues": { "deprecated_yaml": { + "description": "Az Xbox konfigur\u00e1l\u00e1sa a configuration.yaml f\u00e1jlban a 2022.9-es Home Assistantb\u00f3l elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 OAuth-alkalmaz\u00e1s hiteles\u00edt\u0151 adatai \u00e9s hozz\u00e1f\u00e9r\u00e9si be\u00e1ll\u00edt\u00e1sai automatikusan import\u00e1l\u00e1sra ker\u00fcltek a felhaszn\u00e1l\u00f3i fel\u00fcletre. A probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", "title": "Az Xbox YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } } diff --git a/homeassistant/components/xbox/translations/sv.json b/homeassistant/components/xbox/translations/sv.json index 525e91ded0e..cb98fb350b7 100644 --- a/homeassistant/components/xbox/translations/sv.json +++ b/homeassistant/components/xbox/translations/sv.json @@ -1,4 +1,18 @@ { + "config": { + "abort": { + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "create_entry": { + "default": "Autentiserats" + }, + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + } + } + }, "issues": { "deprecated_yaml": { "description": "Konfigurering av Xbox i configuration.yaml tas bort i Home Assistant 2022.9. \n\n Dina befintliga OAuth-applikationsuppgifter och \u00e5tkomstinst\u00e4llningar har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", diff --git a/homeassistant/components/xiaomi_aqara/translations/sv.json b/homeassistant/components/xiaomi_aqara/translations/sv.json new file mode 100644 index 00000000000..9a1ed92aa1d --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/sv.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "not_xiaomi_aqara": "Inte en Xiaomi Aqara Gateway, uppt\u00e4ckt enhet matchade inte k\u00e4nda gateways" + }, + "error": { + "discovery_error": "Det gick inte att uppt\u00e4cka en Xiaomi Aqara Gateway, f\u00f6rs\u00f6k anv\u00e4nda IP:n f\u00f6r enheten som k\u00f6r HomeAssistant som gr\u00e4nssnitt", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress, Kolla https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "invalid_interface": "Ogiltigt n\u00e4tverksgr\u00e4nssnitt", + "invalid_key": "Ogiltig gatewaynyckel", + "invalid_mac": "Ogiltig MAC-adress" + }, + "flow_title": "{name}", + "step": { + "select": { + "data": { + "select_ip": "IP-adress" + }, + "description": "V\u00e4lj den Xiaomi Aqara Gateway du vill ansluta" + }, + "settings": { + "data": { + "key": "Nyckeln till din gateway", + "name": "Namnet p\u00e5 gatewayen" + }, + "description": "Nyckeln (l\u00f6senordet) kan h\u00e4mtas med hj\u00e4lp av den h\u00e4r handledningen: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Om nyckeln inte anges kommer endast sensorer att vara tillg\u00e4ngliga.", + "title": "Valfria inst\u00e4llningar" + }, + "user": { + "data": { + "host": "IP-adress (valfritt)", + "interface": "N\u00e4tverksgr\u00e4nssnittet som ska anv\u00e4ndas", + "mac": "MAC-adress (valfritt)" + }, + "description": "Om IP- och MAC-adresserna l\u00e4mnas tomma anv\u00e4nds automatisk uppt\u00e4ckt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.sv.json b/homeassistant/components/xiaomi_miio/translations/select.sv.json new file mode 100644 index 00000000000..47c2ffaa90d --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/select.sv.json @@ -0,0 +1,9 @@ +{ + "state": { + "xiaomi_miio__led_brightness": { + "bright": "Ljus", + "dim": "Dimma", + "off": "Av" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/sv.json b/homeassistant/components/xiaomi_miio/translations/sv.json index 20e4d8c6d07..a52c1ecbf98 100644 --- a/homeassistant/components/xiaomi_miio/translations/sv.json +++ b/homeassistant/components/xiaomi_miio/translations/sv.json @@ -1,11 +1,49 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "incomplete_info": "Ofullst\u00e4ndig information till installationsenheten, ingen v\u00e4rd eller token tillhandah\u00e5lls.", + "not_xiaomi_miio": "Enheten st\u00f6ds (\u00e4nnu) inte av Xiaomi Miio.", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "cloud_credentials_incomplete": "Cloud-uppgifterna \u00e4r ofullst\u00e4ndiga, v\u00e4nligen fyll i anv\u00e4ndarnamn, l\u00f6senord och land", + "cloud_login_error": "Kunde inte logga in p\u00e5 Xiaomi Miio Cloud, kontrollera anv\u00e4ndaruppgifterna.", + "cloud_no_devices": "Inga enheter hittades i detta Xiaomi Miio molnkonto.", + "unknown_device": "Enhetsmodellen \u00e4r inte k\u00e4nd, kan inte st\u00e4lla in enheten med hj\u00e4lp av konfigurationsfl\u00f6de." + }, + "flow_title": "{name}", "step": { "cloud": { "data": { + "cloud_country": "Molnserverland", "cloud_password": "Molnl\u00f6senord", - "cloud_username": "Molnanv\u00e4ndarnamn" + "cloud_username": "Molnanv\u00e4ndarnamn", + "manual": "Konfigurera manuellt (rekommenderas inte)" + }, + "description": "Logga in p\u00e5 Xiaomi Miio-molnet, se https://www.openhab.org/addons/bindings/miio/#country-servers f\u00f6r molnservern att anv\u00e4nda." + }, + "connect": { + "data": { + "model": "Enhetsmodell" } + }, + "manual": { + "data": { + "host": "IP-adress" + } + }, + "reauth_confirm": { + "description": "Xiaomi Miio-integrationen m\u00e5ste autentisera ditt konto igen f\u00f6r att uppdatera tokens eller l\u00e4gga till saknade molnuppgifter.", + "title": "\u00c5terautenticera integration" + }, + "select": { + "data": { + "select_device": "Miio-enhet" + }, + "description": "V\u00e4lj Xiaomi Miio-enheten f\u00f6r att st\u00e4lla in." } } } diff --git a/homeassistant/components/yamaha_musiccast/translations/sv.json b/homeassistant/components/yamaha_musiccast/translations/sv.json new file mode 100644 index 00000000000..7326abb364a --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "yxc_control_url_missing": "Kontroll-URL:n anges inte i ssdp-beskrivningen." + }, + "error": { + "no_musiccast_device": "Den h\u00e4r enheten verkar inte vara n\u00e5gon MusicCast-enhet." + }, + "flow_title": "MusicCast: {name}", + "step": { + "confirm": { + "description": "Vill du starta konfigurationen?" + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Konfigurera MusicCast f\u00f6r att integrera med Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/sv.json b/homeassistant/components/yeelight/translations/sv.json index 9fdd341e941..8575090ff7c 100644 --- a/homeassistant/components/yeelight/translations/sv.json +++ b/homeassistant/components/yeelight/translations/sv.json @@ -1,10 +1,36 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{model} {id} ({host})", "step": { + "discovery_confirm": { + "description": "Vill du st\u00e4lla in {model} ( {host} )?" + }, "pick_device": { "data": { "device": "Enhet" } + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Om du l\u00e4mnar v\u00e4rden tomt anv\u00e4nds discovery f\u00f6r att hitta enheter." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "Modell", + "nightlight_switch": "Anv\u00e4nd nattljusbrytare", + "save_on_change": "Spara status vid \u00e4ndring", + "transition": "\u00d6verg\u00e5ngstid (ms)", + "use_music_mode": "Aktivera musikl\u00e4ge" + } } } } diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index 61b0f0c0c9d..2b4fe26e0d6 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -46,11 +46,13 @@ "title": "Riaszt\u00f3 vez\u00e9rl\u0151panel opci\u00f3k" }, "zha_options": { + "always_prefer_xy_color_mode": "Az XY sz\u00ednm\u00f3dot el\u0151nyben r\u00e9szes\u00edtse", "consider_unavailable_battery": "Elemmel ell\u00e1tott eszk\u00f6z\u00f6k nem el\u00e9rhet\u0151 \u00e1llapot\u00faak ennyi id\u0151 ut\u00e1n (mp.)", "consider_unavailable_mains": "H\u00e1l\u00f3zati t\u00e1pell\u00e1t\u00e1s\u00fa eszk\u00f6z\u00f6k nem el\u00e9rhet\u0151 \u00e1llapot\u00faak ennyi id\u0151 ut\u00e1n (mp.)", "default_light_transition": "Alap\u00e9rtelmezett f\u00e9ny-\u00e1tmeneti id\u0151 (m\u00e1sodpercben)", "enable_identify_on_join": "Azonos\u00edt\u00f3 hat\u00e1s, amikor az eszk\u00f6z\u00f6k csatlakoznak a h\u00e1l\u00f3zathoz", "enhanced_light_transition": "F\u00e9ny sz\u00edn/sz\u00ednh\u0151m\u00e9rs\u00e9klet \u00e1tmenete kikapcsolt \u00e1llapotb\u00f3l", + "light_transitioning_flag": "Fokozott f\u00e9nyer\u0151-szab\u00e1lyoz\u00f3 enged\u00e9lyez\u00e9se f\u00e9nyv\u00e1lt\u00e1skor", "title": "Glob\u00e1lis be\u00e1ll\u00edt\u00e1sok" } }, diff --git a/homeassistant/components/zha/translations/sv.json b/homeassistant/components/zha/translations/sv.json index 8917cbe8cd6..ee189ef1463 100644 --- a/homeassistant/components/zha/translations/sv.json +++ b/homeassistant/components/zha/translations/sv.json @@ -1,12 +1,18 @@ { "config": { "abort": { - "single_instance_allowed": "Endast en enda konfiguration av ZHA \u00e4r till\u00e5ten." + "not_zha_device": "Den h\u00e4r enheten \u00e4r inte en zha-enhet", + "single_instance_allowed": "Endast en enda konfiguration av ZHA \u00e4r till\u00e5ten.", + "usb_probe_failed": "Det gick inte att unders\u00f6ka usb-enheten" }, "error": { "cannot_connect": "Det gick inte att ansluta till ZHA enhet." }, + "flow_title": "{name}", "step": { + "confirm": { + "description": "Vill du konfigurera {name}?" + }, "pick_radio": { "data": { "radio_type": "Radiotyp" @@ -27,15 +33,25 @@ "data": { "path": "Seriell enhetsv\u00e4g" }, + "description": "V\u00e4lj serieport f\u00f6r Zigbee-radio", "title": "ZHA" } } }, "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Kod kr\u00e4vs f\u00f6r tillkopplings\u00e5tg\u00e4rder", + "alarm_failed_tries": "Antalet misslyckade kodinmatningar i f\u00f6ljd f\u00f6r att utl\u00f6sa ett larm.", + "alarm_master_code": "Huvudkod f\u00f6r larmcentralen/-larmcentralerna.", + "title": "Alternativ f\u00f6r larmkontrollpanel" + }, "zha_options": { "always_prefer_xy_color_mode": "F\u00f6redrar alltid XY-f\u00e4rgl\u00e4ge", + "default_light_transition": "Standard ljus\u00f6verg\u00e5ngstid (sekunder)", + "enable_identify_on_join": "Aktivera identifieringseffekt n\u00e4r enheter ansluter till n\u00e4tverket", "enhanced_light_transition": "Aktivera f\u00f6rb\u00e4ttrad ljusf\u00e4rg/temperatur\u00f6verg\u00e5ng fr\u00e5n ett avst\u00e4ngt l\u00e4ge", - "light_transitioning_flag": "Aktivera f\u00f6rb\u00e4ttrad ljusstyrka vid ljus\u00f6verg\u00e5ng" + "light_transitioning_flag": "Aktivera f\u00f6rb\u00e4ttrad ljusstyrka vid ljus\u00f6verg\u00e5ng", + "title": "Globala alternativ" } }, "device_automation": { @@ -71,10 +87,19 @@ "device_dropped": "Enheten tappades", "device_flipped": "Enheten v\u00e4nd \"{subtype}\"", "device_knocked": "Enheten knackad \"{subtype}\"", + "device_offline": "Enhet offline", "device_rotated": "Enheten roterade \"{subtype}\"", "device_shaken": "Enheten skakad", "device_slid": "Enheten gled \"{subtype}\"", "device_tilted": "Enheten lutad", + "remote_button_alt_double_press": "\"{subtype}\" dubbelklickades (Alternativt l\u00e4ge)", + "remote_button_alt_long_press": "\"{subtype}\" h\u00f6lls nedtryckt (Alternativt l\u00e4ge)", + "remote_button_alt_long_release": "\"{subtype}\" sl\u00e4pptes upp efter en l\u00e5ngtryckning (Alternativt l\u00e4ge)", + "remote_button_alt_quadruple_press": "\"{subtype}\" trycktes fyrfaldigt (Alternativt l\u00e4ge)", + "remote_button_alt_quintuple_press": "\"{subtype}\" trycktes femfaldigt (Alternativt l\u00e4ge)", + "remote_button_alt_short_press": "\"{subtype}\" trycktes (Alternativt l\u00e4ge)", + "remote_button_alt_short_release": "\"{subtype}\" sl\u00e4pptes upp (Alternativt l\u00e4ge)", + "remote_button_alt_triple_press": "\"{subtype}\" trippelklickades (Alternativt l\u00e4ge)", "remote_button_double_press": "\"{subtype}\"-knappen dubbelklickades", "remote_button_long_press": "\"{subtype}\"-knappen kontinuerligt nedtryckt", "remote_button_long_release": "\"{subtype}\"-knappen sl\u00e4pptes efter ett l\u00e5ngttryck", diff --git a/homeassistant/components/zoneminder/translations/sv.json b/homeassistant/components/zoneminder/translations/sv.json index 4f0da20207f..f3e7d2891bd 100644 --- a/homeassistant/components/zoneminder/translations/sv.json +++ b/homeassistant/components/zoneminder/translations/sv.json @@ -1,10 +1,15 @@ { "config": { "abort": { - "auth_fail": "Anv\u00e4ndarnamn eller l\u00f6senord \u00e4r felaktigt." + "auth_fail": "Anv\u00e4ndarnamn eller l\u00f6senord \u00e4r felaktigt.", + "connection_error": "Det gick inte att ansluta till en ZoneMinder-server." + }, + "create_entry": { + "default": "ZoneMinder-server har lagts till." }, "error": { - "auth_fail": "Anv\u00e4ndarnamn eller l\u00f6senord \u00e4r felaktigt." + "auth_fail": "Anv\u00e4ndarnamn eller l\u00f6senord \u00e4r felaktigt.", + "connection_error": "Det gick inte att ansluta till en ZoneMinder-server." }, "step": { "user": { diff --git a/homeassistant/components/zwave_js/translations/sv.json b/homeassistant/components/zwave_js/translations/sv.json index 386b306e1b8..8f8777f92b0 100644 --- a/homeassistant/components/zwave_js/translations/sv.json +++ b/homeassistant/components/zwave_js/translations/sv.json @@ -1,19 +1,75 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "discovery_requires_supervisor": "Uppt\u00e4ckt kr\u00e4ver \u00f6vervakaren.", + "not_zwave_device": "Uppt\u00e4ckt enhet \u00e4r inte en Z-Wave-enhet." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_ws_url": "Ogiltig websocket-URL", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name}", "step": { + "usb_confirm": { + "description": "Vill du konfigurera {name} med Z-Wave JS till\u00e4gget?" + }, "zeroconf_confirm": { "title": "Uppt\u00e4ckte Z-Wave JS Server" } } }, "device_automation": { + "action_type": { + "set_config_parameter": "Ange v\u00e4rde f\u00f6r konfigurationsparametern {subtype}", + "set_lock_usercode": "Ange en anv\u00e4ndarkod p\u00e5 {entity_name}", + "set_value": "St\u00e4ll in v\u00e4rdet f\u00f6r ett Z-Wave-v\u00e4rde" + }, "condition_type": { + "config_parameter": "V\u00e4rde f\u00f6r konfigurationsparameter {subtype}", + "node_status": "Nodstatus", "value": "Nuvarande v\u00e4rde f\u00f6r ett Z-Wave v\u00e4rde" + }, + "trigger_type": { + "zwave_js.value_updated.config_parameter": "V\u00e4rde\u00e4ndring p\u00e5 konfigurationsparameter {subtype}" } }, "options": { + "abort": { + "addon_install_failed": "Det gick inte att installera Z-Wave JS-till\u00e4gget.", + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "different_device": "Den anslutna USB-enheten \u00e4r inte densamma som tidigare konfigurerats f\u00f6r den h\u00e4r konfigurationsposten. Skapa ist\u00e4llet en ny konfigurationspost f\u00f6r den nya enheten." + }, "error": { - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "invalid_ws_url": "Ogiltig websocket-URL", + "unknown": "Ov\u00e4ntat fel" + }, + "progress": { + "install_addon": "V\u00e4nta medan installationen av Z-Wave JS-till\u00e4gget slutf\u00f6rs. Detta kan ta flera minuter.", + "start_addon": "V\u00e4nta medan Z-Wave JS-till\u00e4ggsstarten slutf\u00f6rs. Detta kan ta n\u00e5gra sekunder." + }, + "step": { + "configure_addon": { + "data": { + "emulate_hardware": "Emulera h\u00e5rdvara", + "log_level": "Loggniv\u00e5", + "usb_path": "USB-enhetens s\u00f6kv\u00e4g" + } + }, + "install_addon": { + "title": "Z-Wave JS-till\u00e4ggsinstallationen har startat" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "title": "V\u00e4lj anslutningsmetod" + } } } } \ No newline at end of file From 26e2ef81757c5ec10fc5ba6a634882e1a2977d60 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 1 Aug 2022 18:20:20 -0700 Subject: [PATCH 3040/3516] Add repair issues for nest app auth removal and yaml deprecation (#75974) * Add repair issues for nest app auth removal and yaml deprecation * Apply PR feedback * Re-apply suggestion that i force pushed over * Update criticality level --- homeassistant/components/nest/__init__.py | 33 +++++++++++++++---- homeassistant/components/nest/manifest.json | 2 +- homeassistant/components/nest/strings.json | 10 ++++++ .../components/nest/translations/en.json | 18 +++++----- 4 files changed, 48 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 72759ac0f52..44658558b62 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -29,6 +29,11 @@ from homeassistant.components.application_credentials import ( from homeassistant.components.camera import Image, img_util from homeassistant.components.http.const import KEY_HASS_USER from homeassistant.components.http.view import HomeAssistantView +from homeassistant.components.repairs import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_BINARY_SENSORS, @@ -187,6 +192,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, unique_id=entry.data[CONF_PROJECT_ID] ) + async_delete_issue(hass, DOMAIN, "removed_app_auth") + subscriber = await api.new_subscriber(hass, entry) if not subscriber: return False @@ -255,6 +262,18 @@ async def async_import_config(hass: HomeAssistant, entry: ConfigEntry) -> None: if entry.data["auth_implementation"] == INSTALLED_AUTH_DOMAIN: # App Auth credentials have been deprecated and must be re-created # by the user in the config flow + async_create_issue( + hass, + DOMAIN, + "removed_app_auth", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="removed_app_auth", + translation_placeholders={ + "more_info_url": "https://www.home-assistant.io/more-info/nest-auth-deprecation", + "documentation_url": "https://www.home-assistant.io/integrations/nest/", + }, + ) raise ConfigEntryAuthFailed( "Google has deprecated App Auth credentials, and the integration " "must be reconfigured in the UI to restore access to Nest Devices." @@ -271,12 +290,14 @@ async def async_import_config(hass: HomeAssistant, entry: ConfigEntry) -> None: WEB_AUTH_DOMAIN, ) - _LOGGER.warning( - "Configuration of Nest integration in YAML is deprecated and " - "will be removed in a future release; Your existing configuration " - "(including OAuth Application Credentials) has been imported into " - "the UI automatically and can be safely removed from your " - "configuration.yaml file" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", ) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 72e0aed8420..d826272b207 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -2,7 +2,7 @@ "domain": "nest", "name": "Nest", "config_flow": true, - "dependencies": ["ffmpeg", "http", "application_credentials"], + "dependencies": ["ffmpeg", "http", "application_credentials", "repairs"], "after_dependencies": ["media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.0.0"], diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 0a13de41511..07ba63ac479 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -88,5 +88,15 @@ "camera_sound": "Sound detected", "doorbell_chime": "Doorbell pressed" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Nest YAML configuration is being removed", + "description": "Configuring Nest in configuration.yaml is being removed in Home Assistant 2022.10.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + }, + "removed_app_auth": { + "title": "Nest Authentication Credentials must be updated", + "description": "To improve security and reduce phishing risk Google has deprecated the authentication method used by Home Assistant.\n\n**This requires action by you to resolve** ([more info]({more_info_url}))\n\n1. Visit the integrations page\n1. Click Reconfigure on the Nest integration.\n1. Home Assistant will walk you through the steps to upgrade to Web Authentication.\n\nSee the Nest [integration instructions]({documentation_url}) for troubleshooting information." + } } } diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index 5f026e55f31..cd8274d635a 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -10,7 +10,6 @@ "missing_configuration": "The component is not configured. Please follow the documentation.", "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", "reauth_successful": "Re-authentication was successful", - "single_instance_allowed": "Already configured. Only a single configuration possible.", "unknown_authorize_url_generation": "Unknown error generating an authorize URL." }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "Please enter a valid Cloud Project ID (was same as Device Access Project ID)" }, "step": { - "auth": { - "data": { - "code": "Access Token" - }, - "description": "To link your Google account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided Auth Token code below.", - "title": "Link Google Account" - }, "auth_upgrade": { "description": "App Auth has been deprecated by Google to improve security, and you need to take action by creating new application credentials.\n\nOpen the [documentation]({more_info_url}) to follow along as the next steps will guide you through the steps you need to take to restore access to your Nest devices.", "title": "Nest: App Auth Deprecation" @@ -96,5 +88,15 @@ "camera_sound": "Sound detected", "doorbell_chime": "Doorbell pressed" } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Nest in configuration.yaml is being removed in Home Assistant 2022.10.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Nest YAML configuration is being removed" + }, + "removed_app_auth": { + "description": "To improve security and reduce phishing risk Google has deprecated the authentication method used by Home Assistant.\n\n**This requires action by you to resolve** ([more info]({more_info_url}))\n\n1. Visit the integrations page\n1. Click Reconfigure on the Nest integration.\n1. Home Assistant will walk you through the steps to upgrade to Web Authentication.\n\nSee the Nest [integration instructions]({documentation_url}) for troubleshooting information.", + "title": "Nest Authentication Credentials must be updated" + } } } \ No newline at end of file From 44b621321758435a159aff5050cf2a88623c0fba Mon Sep 17 00:00:00 2001 From: Eloston Date: Tue, 2 Aug 2022 02:29:44 +0000 Subject: [PATCH 3041/3516] Add support for SwitchBot Plug Mini (#76056) --- CODEOWNERS | 4 ++-- homeassistant/components/switchbot/__init__.py | 3 +++ homeassistant/components/switchbot/const.py | 2 ++ homeassistant/components/switchbot/manifest.json | 10 ++++++++-- homeassistant/components/switchbot/sensor.py | 9 ++++++++- homeassistant/components/switchbot/switch.py | 6 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 28 insertions(+), 10 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 96238f61fbb..ce31d6e8dd3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1046,8 +1046,8 @@ build.json @home-assistant/supervisor /tests/components/switch/ @home-assistant/core /homeassistant/components/switch_as_x/ @home-assistant/core /tests/components/switch_as_x/ @home-assistant/core -/homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas -/tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas +/homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston +/tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston /homeassistant/components/switcher_kis/ @tomerfi @thecode /tests/components/switcher_kis/ @tomerfi @thecode /homeassistant/components/switchmate/ @danielhiversen @qiz-li diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 42ca0856b02..e32252a7615 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -22,6 +22,7 @@ from .const import ( ATTR_CONTACT, ATTR_CURTAIN, ATTR_HYGROMETER, + ATTR_PLUG, CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, @@ -30,6 +31,7 @@ from .coordinator import SwitchbotDataUpdateCoordinator PLATFORMS_BY_TYPE = { ATTR_BOT: [Platform.SWITCH, Platform.SENSOR], + ATTR_PLUG: [Platform.SWITCH, Platform.SENSOR], ATTR_CURTAIN: [Platform.COVER, Platform.BINARY_SENSOR, Platform.SENSOR], ATTR_HYGROMETER: [Platform.SENSOR], ATTR_CONTACT: [Platform.BINARY_SENSOR, Platform.SENSOR], @@ -37,6 +39,7 @@ PLATFORMS_BY_TYPE = { CLASS_BY_DEVICE = { ATTR_CURTAIN: switchbot.SwitchbotCurtain, ATTR_BOT: switchbot.Switchbot, + ATTR_PLUG: switchbot.SwitchbotPlugMini, } _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index dc5abc139e6..9cc2acebbf8 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -7,12 +7,14 @@ ATTR_BOT = "bot" ATTR_CURTAIN = "curtain" ATTR_HYGROMETER = "hygrometer" ATTR_CONTACT = "contact" +ATTR_PLUG = "plug" DEFAULT_NAME = "Switchbot" SUPPORTED_MODEL_TYPES = { "WoHand": ATTR_BOT, "WoCurtain": ATTR_CURTAIN, "WoSensorTH": ATTR_HYGROMETER, "WoContact": ATTR_CONTACT, + "WoPlug": ATTR_PLUG, } # Config Defaults diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index dcb33c03882..41d0d7efda6 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,10 +2,16 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.16.0"], + "requirements": ["PySwitchbot==0.17.1"], "config_flow": true, "dependencies": ["bluetooth"], - "codeowners": ["@bdraco", "@danielhiversen", "@RenierM26", "@murtas"], + "codeowners": [ + "@bdraco", + "@danielhiversen", + "@RenierM26", + "@murtas", + "@Eloston" + ], "bluetooth": [ { "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb" diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index f796ea05e7b..fb24ae22679 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -33,6 +33,13 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), + "wifi_rssi": SensorEntityDescription( + key="wifi_rssi", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), "battery": SensorEntityDescription( key="battery", native_unit_of_measurement=PERCENTAGE, @@ -98,7 +105,7 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity): super().__init__(coordinator, unique_id, address, name=switchbot_name) self._sensor = sensor self._attr_unique_id = f"{unique_id}-{sensor}" - self._attr_name = f"{switchbot_name} {sensor.title()}" + self._attr_name = f"{switchbot_name} {sensor.replace('_', ' ').title()}" self.entity_description = SENSOR_TYPES[sensor] @property diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index e6ba77fa164..65c7588acbd 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -33,7 +33,7 @@ async def async_setup_entry( assert unique_id is not None async_add_entities( [ - SwitchBotBotEntity( + SwitchBotSwitch( coordinator, unique_id, entry.data[CONF_ADDRESS], @@ -44,8 +44,8 @@ async def async_setup_entry( ) -class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): - """Representation of a Switchbot.""" +class SwitchBotSwitch(SwitchbotEntity, SwitchEntity, RestoreEntity): + """Representation of a Switchbot switch.""" _attr_device_class = SwitchDeviceClass.SWITCH diff --git a/requirements_all.txt b/requirements_all.txt index 5a56eb1b3b2..69098880563 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.16.0 +PySwitchbot==0.17.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 110f0446b0d..44d4e9fd0ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.16.0 +PySwitchbot==0.17.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 41d7eba1ad006c1a0ecde74a81d70250aeb9a357 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Aug 2022 20:34:48 -1000 Subject: [PATCH 3042/3516] Fix govee H5074 data (#76057) --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index c7909d3e1af..624a38ebe9d 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -24,7 +24,7 @@ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.12.5"], + "requirements": ["govee-ble==0.12.6"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 69098880563..690a0886451 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.5 +govee-ble==0.12.6 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 44d4e9fd0ee..1ac8b0117e3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,7 +561,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.5 +govee-ble==0.12.6 # homeassistant.components.gree greeclimate==1.2.0 From 33651d14df58401eb836897fd19aec307e3d99ab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Aug 2022 20:36:27 -1000 Subject: [PATCH 3043/3516] Bump bluetooth-adapters to 0.1.3 (#76052) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index f215e8fa161..40e63ec7180 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.15.0", "bluetooth-adapters==0.1.2"], + "requirements": ["bleak==0.15.0", "bluetooth-adapters==0.1.3"], "codeowners": ["@bdraco"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d4344ec256c..860d1538727 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 bleak==0.15.0 -bluetooth-adapters==0.1.2 +bluetooth-adapters==0.1.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==36.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index 690a0886451..ba339f73b25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -424,7 +424,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.1.2 +bluetooth-adapters==0.1.3 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ac8b0117e3..0f26add462a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ blebox_uniapi==2.0.2 blinkpy==0.19.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.1.2 +bluetooth-adapters==0.1.3 # homeassistant.components.bond bond-async==0.1.22 From 56050e9fbe374f550028685958c65a24de9aca0b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Aug 2022 20:46:22 -1000 Subject: [PATCH 3044/3516] Lower bluetooth startup timeout to 9s to avoid warning (#76050) --- homeassistant/components/bluetooth/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index d42f6b4f230..39629ab6d85 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -50,7 +50,7 @@ _LOGGER = logging.getLogger(__name__) UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 -START_TIMEOUT = 15 +START_TIMEOUT = 9 SOURCE_LOCAL: Final = "local" From 32b1259786578c8cf9beee4c8d1ebab6ef7aede7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Aug 2022 08:54:28 +0200 Subject: [PATCH 3045/3516] Support multiple trigger instances for a single webhook (#76037) --- homeassistant/components/webhook/trigger.py | 65 +++++++++++++++------ tests/components/mobile_app/test_webhook.py | 2 +- tests/components/webhook/test_trigger.py | 63 ++++++++++++++++++-- 3 files changed, 107 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/webhook/trigger.py b/homeassistant/components/webhook/trigger.py index 3f790b1ec42..498a7363a61 100644 --- a/homeassistant/components/webhook/trigger.py +++ b/homeassistant/components/webhook/trigger.py @@ -1,5 +1,7 @@ """Offer webhook triggered automation rules.""" -from functools import partial +from __future__ import annotations + +from dataclasses import dataclass from aiohttp import hdrs import voluptuous as vol @@ -13,7 +15,7 @@ from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from . import async_register, async_unregister +from . import DOMAIN, async_register, async_unregister # mypy: allow-untyped-defs @@ -26,20 +28,35 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( } ) +WEBHOOK_TRIGGERS = f"{DOMAIN}_triggers" -async def _handle_webhook(job, trigger_data, hass, webhook_id, request): + +@dataclass +class TriggerInstance: + """Attached trigger settings.""" + + automation_info: AutomationTriggerInfo + job: HassJob + + +async def _handle_webhook(hass, webhook_id, request): """Handle incoming webhook.""" - result = {"platform": "webhook", "webhook_id": webhook_id} + base_result = {"platform": "webhook", "webhook_id": webhook_id} if "json" in request.headers.get(hdrs.CONTENT_TYPE, ""): - result["json"] = await request.json() + base_result["json"] = await request.json() else: - result["data"] = await request.post() + base_result["data"] = await request.post() - result["query"] = request.query - result["description"] = "webhook" - result.update(**trigger_data) - hass.async_run_hass_job(job, {"trigger": result}) + base_result["query"] = request.query + base_result["description"] = "webhook" + + triggers: dict[str, list[TriggerInstance]] = hass.data.setdefault( + WEBHOOK_TRIGGERS, {} + ) + for trigger in triggers[webhook_id]: + result = {**base_result, **trigger.automation_info["trigger_data"]} + hass.async_run_hass_job(trigger.job, {"trigger": result}) async def async_attach_trigger( @@ -49,20 +66,32 @@ async def async_attach_trigger( automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Trigger based on incoming webhooks.""" - trigger_data = automation_info["trigger_data"] webhook_id: str = config[CONF_WEBHOOK_ID] job = HassJob(action) - async_register( - hass, - automation_info["domain"], - automation_info["name"], - webhook_id, - partial(_handle_webhook, job, trigger_data), + + triggers: dict[str, list[TriggerInstance]] = hass.data.setdefault( + WEBHOOK_TRIGGERS, {} ) + if webhook_id not in triggers: + async_register( + hass, + automation_info["domain"], + automation_info["name"], + webhook_id, + _handle_webhook, + ) + triggers[webhook_id] = [] + + trigger_instance = TriggerInstance(automation_info, job) + triggers[webhook_id].append(trigger_instance) + @callback def unregister(): """Unregister webhook.""" - async_unregister(hass, webhook_id) + triggers[webhook_id].remove(trigger_instance) + if not triggers[webhook_id]: + async_unregister(hass, webhook_id) + triggers.pop(webhook_id) return unregister diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 0bc237b1c11..b7b95dff392 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -840,7 +840,7 @@ async def test_webhook_handle_scan_tag(hass, create_registrations, webhook_clien @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("tag_scanned", store_event) diff --git a/tests/components/webhook/test_trigger.py b/tests/components/webhook/test_trigger.py index 2deac022b1e..e8d88845f5a 100644 --- a/tests/components/webhook/test_trigger.py +++ b/tests/components/webhook/test_trigger.py @@ -23,7 +23,7 @@ async def test_webhook_json(hass, hass_client_no_auth): @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) @@ -62,7 +62,7 @@ async def test_webhook_post(hass, hass_client_no_auth): @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) @@ -97,7 +97,7 @@ async def test_webhook_query(hass, hass_client_no_auth): @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) @@ -126,13 +126,68 @@ async def test_webhook_query(hass, hass_client_no_auth): assert events[0].data["hello"] == "yo world" +async def test_webhook_multiple(hass, hass_client_no_auth): + """Test triggering multiple triggers with a POST webhook.""" + events1 = [] + events2 = [] + + @callback + def store_event1(event): + """Help store events.""" + events1.append(event) + + @callback + def store_event2(event): + """Help store events.""" + events2.append(event) + + hass.bus.async_listen("test_success1", store_event1) + hass.bus.async_listen("test_success2", store_event2) + + assert await async_setup_component( + hass, + "automation", + { + "automation": [ + { + "trigger": {"platform": "webhook", "webhook_id": "post_webhook"}, + "action": { + "event": "test_success1", + "event_data_template": {"hello": "yo {{ trigger.data.hello }}"}, + }, + }, + { + "trigger": {"platform": "webhook", "webhook_id": "post_webhook"}, + "action": { + "event": "test_success2", + "event_data_template": { + "hello": "yo2 {{ trigger.data.hello }}" + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + + client = await hass_client_no_auth() + + await client.post("/api/webhook/post_webhook", data={"hello": "world"}) + await hass.async_block_till_done() + + assert len(events1) == 1 + assert events1[0].data["hello"] == "yo world" + assert len(events2) == 1 + assert events2[0].data["hello"] == "yo2 world" + + async def test_webhook_reload(hass, hass_client_no_auth): """Test reloading a webhook.""" events = [] @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) From bec4b168d6a0d8a23e07f0c91864cbfbe006d08e Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 2 Aug 2022 02:56:50 -0400 Subject: [PATCH 3046/3516] Bump AIOAladdinConnect to 0.1.37 (#76046) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index a142e838f3e..008b8f81c89 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.33"], + "requirements": ["AIOAladdinConnect==0.1.37"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index ba339f73b25..df7bb65f9da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.33 +AIOAladdinConnect==0.1.37 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f26add462a..196cca0c3ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.33 +AIOAladdinConnect==0.1.37 # homeassistant.components.adax Adax-local==0.1.4 From dbac8b804fb778ee663e8fb756ced68dd930be81 Mon Sep 17 00:00:00 2001 From: Sven Serlier <85389871+wrt54g@users.noreply.github.com> Date: Tue, 2 Aug 2022 09:09:24 +0200 Subject: [PATCH 3047/3516] Update featured integrations image (#76011) --- README.rst | 4 ++-- docs/screenshot-components.png | Bin 121315 -> 0 bytes docs/screenshot-integrations.png | Bin 0 -> 121315 bytes 3 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 docs/screenshot-components.png create mode 100644 docs/screenshot-integrations.png diff --git a/README.rst b/README.rst index 05e13695a58..6f5e0e69892 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ demo `__, `installation instructions `__ and the `section on creating your own components `__. @@ -24,5 +24,5 @@ of a component, check the `Home Assistant help section 21BEC!?oW?B|aMwmzpTLbp3-0YUq@$IGqWzby#lwg2}@{i33CuKRxz z1oDfKJAUI&LEhh3RJ1Sd1ADrlMZ0R+G4biV^2%Z##t9fM!rAOQIZPUN(l|LBVst?5 zNbL31ek19x3@rc4;2^A<+J|43YwWJD>4C(4rK_Df)C}GEhq1cPq5EGILG`&TDqB+? zKbWJL(mnQ;+x_ZTG(%<5b8>R3x`k!B*kC;nHZd&2>g42P_%oLHKpL9>hkc9=ccJZ2 z8ry&W0>jg33@iVu<^uNv*tF%n;rPjVj(`hXw>dc_r9`hx=SE6idn=5m{>!7!@axBL zp?f*X@l~+@=?-4P|8yq_Mq+{&!7eIl!o$N;AHt$SQ+T2UW>&4d@Mu1J6|}_Rf4cJD z0N`oXd!&b3ZKL>05xn*@l?H}q`~HB?67fH2#QqO92VqZIj%a9lTAHmdYm5#bvuY}f zD>iDd(`(yXvLjjF=_y}}B!i%7yrSh(ECTY{G03*VCf(%!wRbo9-Kk zrtDS8pWB-qdOlP^yW%++NQ{U+Q|Cb1__V~lv^4G$51c}(3b3ZpA+SK#!*z`*z zLhYwNM;o~R+sXcf7~cPP+2e)$e_X7SqoU23>_1Y?rf9L<*1f-bBA z)O>szM>B!4d*gX*CQYBya&yUzI|EzPas<@p%8jij78_l1)N+Ni(>cvp+kE~eQ;7Nu zwfkM#ygsb89U%&!c$IN?W1#8NA67|B<$n*?VQpJX^IWwvPj!Kj7L2Z=NsBq2HoF>0 zW5z8vP19w{Ivl-BajHzp%kW{HF1_Sb6(Z#uB(87FD5&VfoT<|#s;VD8WYaSsJxF+X zdREXF%zFGd5jRqK$+9<3C1f)oEfQ5^H7KWMDp)bWk}OZr`)$AvO0BJ0Sl)GphNJO# z>?j$xc$CzCUj0BS_?f-VexmVv47aIU4-$_b+@{U-a&OFI6ga5l@83%3dOTN=qLRis zT=p}cbv^i{!+EKG7<_p(qd9>eh}y=eo~_gs^pBpOpC4o>zt}`zFT9+^Q0t&xiIJbq zfxo{Gi-{W$BIug^l}S%Q#yAok$*39!S5B^6v=AOTPp{kAv3XTW$$^i5n$SbPwCNFoN@FB^AYncj=={XNrR z;NwW~U2*-QUJ=U`Yh4n{t-ZkKmCULTgb8^rf6I}q+S{J z^61$zvk`;uuufaoj5i_UgJN7Q;^bC7hr$Px4X-==lQMip}}7_-hQo_nG|(gI8J={ysj5nzC^ z3w7=@E^bY)renUqftQQ=p4MOFCMV;GS!r6K#r|HWTO}6aoNoBWyf0IKnI_H>@1cP5 zuNE`t-r1|MU4l>b*)*4ptSQGS)5dbSdW?%VV-NPNOt^MzA-aK;K{?9{u6+JB#v^2# z!`|!bf`4sweqG)Vt+BOax?Y3*p&d-&#_j>c-AdyY0^!OJwe_4C>?9)H@NW?@K{xBc zCSlF`$8!TXoPOvj9s}2>&D&UxOZ8Cdz`TlvO z+1=x!pV%an2s7XF`sfdj-Du`U{+*PK_sPQ4+Ke;>L`lwzXD3-RocR&h?H8{QN2iyRJ_ ztzkJ+@N~7MELqTBqv(V!W_#87``uLw-{I0l;aaDO;$g*4wL`e-DZA6wIwj_|{O{Ak z!rb!fWTNX_glV_u_J_Wu8P|HCh+Fkgi4`;6)6*@kNIe-0{T)T)T7&&~C^l@+J2dBC z^q)=*!``zDeA-m3b(oTAIej=Tv8(vdV{s#SlRlN{tB&tG2LSe?7omsYa$zHhO*K=_ zx-2)2XyB)tUJBx1K@-!{KR;`j3LQ+X*I55-xz+GIGbLN^3s6w-mnf>@CQ!)Tw4~$0 z=d`D8y`naaK5bwUA0ieIR|6=KZLEcdR?~}M^hx{pq+=o?3vAMuO{-_8@;K_4%HG7( z<+@{C$aG>7OZG-$XJnZB569s3AyG1b1>x+G6h4{P@wm669HP6ax+kuqRJ}9crjtZj zteP&|mr=nsIE>BqWTEEf$#x`tktP|H|J{`PAO+LQ-Jm2rFE20XR-DEK1)I`2QfgqC z<^3)!t>J&Sr5gb}I_Og~JMZO>QaJaW| z)Dl^YY2Z*O{tKU7f9h}$oxf22H%d=;^i`4>1#_pXO!r47vR{Xnz<`FaHR?$f{ikNa zc`5o=g{W;%WlpD>!I=>eOEl8%hYl$$56i>=-_h%%E!931lT^H=>Uv;;Pw%PQDtMFz1ze<;EZrp6Lp$o-9^0 z2N;a_<0EfSgQugONG$>9Y8PqObaNInOd#e1j6-Ph1 z*&d`~6Z2twukw84Sk`y(-mV6!HK?ES+8b{)@GHFZVER0XWIHtZ$}RK!U`&r3edb-TwoS#An9eqLp)d(L-( z0{@UYjigtM@?y!d=>Dh&S~)3=q>q(&?#Jtl0P0Ir?FT5Z@{=$TuI4R*M|N0(VJC~8 z5G~BnKWGu}qLWAKnaC*|be;}9~c|H%C7nj&aWZqzXNFI3E^!(ZQV>U#??T~xKB z)f-aHqFWkhoV$B+F-X<4bvVj9k@`#j_9|uz(;Aq7uPHW#)vx-lkC^iJ?OMQu|M>T4 zoOtZ6T#Nblpqt~z>5gAY`)<&GEJI`gmc<|I5A$QPwOxL3OcA1F64>L==bw{^Wz>xypI-+ zBf_xAM3kQIwy|4|E8CnqKPs_YK&uBx`9Cd)mG)_9k1TefoFzZwTX|(gxXs~~c_`1% zz9mLo%j{NwEEJh5<00K>99%&-Wm@e?9|jU*{jvWN%VuwT$( zN-I;mO_hXMx^Gu3$9~f7gRXV1cXpeMn<^@q=JOIg-1!@)v#(^YTl>iE614o%Zk^>w zvmw6w=w;BLby0E#wG`yeJP#ob3Gkpz4yTQFU!gvAgl-R-t{Q8tk!)S zdAY4DzwT)Rla$}TJaGL*(Rx-bX?Mh6VH3iayt!e{m;0(MdE>^tj%$WS*=-@0@QBJ4 zfovx=9%yy&3&JZxWoCPe6ARj>l@@ArUbO3AnfW=9xT|pB<|~J=8O6ZfM=~Mlb-=}A zGsLCvPQ7-bp5E#ALl7S^FSvsX*qQre ztqWxH<%qg=UYEP9cgp@ElkKdwL`4lJf%j*u58Ykhk9l&`eihbf#o-k5E#9{X9;+7AxpO5oP-X2Ex2htUp>>zy5h_(>anxFo15 z`Dy1#>$nLR#C`Y?I2WHm5P(nAV4RR=K5zQ7BB%zB;X4-B=9u8Rb5*F;uGn6V_tEU| zfM73=AIF;lKtm*@e<`Nu1@r<`^@^cv#bYr{{&qdM$u`Afk0QvFeDYoz1zWa-q*Z-UgKq_0Q#5aC zN`W9;hFps{(eT#bC&q0NevC*S4?f$bm_5)phxYrA&#<&CJ5PDc9&f!r1ow))d@L?d z+}OcbS)m)qQrUFJhfiz5rk?jVc>(3WF(7ArkU)|?+gWM3v{^W}P~W|3<39m9;4%_? zYzM}X^4FW=G!E>!VwkZaLwq6L;oS>0+qQS->uTFP9H)HW5X`OmaQ0uGF5T#A?Uo8X zPq@iK*<(EBNAFFhl|Wn8zF(dMh5wkyri zHzFxaFOI$DMjjpjoyH-tLDU)4_|z`@iH2M1&Y5YQICmrCS9|Q(^vkCwEMzblWMf1_ z>plSKf&kAWG027bf|&BQ5fJ1)Jo9~w0w(`7DwHSpq=1Gr;hmZOHsEmXq&9=4u{m7= z{RaK0(O$mw$hzdy23~ZZD^J@*;f|s9zV5IV+>*MQ4gWoO7yDh>31Ez63p85NYHk?Q zKNEAAf0E~bdh85^pspG2ItPctHJ^V<`)ZvoDvCue3o#K85%p})uHtZyvh;31g}kA- z)&wU|@-NBA)ETQ_7hzqP-fWKeXP0e&2BKs;a&RXpS7l%(A%(hAfA0%VMq1Ao8mo zouN!g$)O9bDsX+_L~U2|QkztkqO=?W=AnTcjSo;JbPFWDI0t#>3803D5iX&M?ReP% zu`>!sFB-`r5PD=uwfFlF*O=R*!o6J^IM|=A?)M9p+x7LVZgO3QZ$@6yZH5On!hye9 zh<1~Aq5h6@bVOWHwRX12wrf6Jd2PsY>&Pi{aS;FCA5hOoV!W?^=?hrdX%ukI0B$UK z@O$aYH?7`Nr?odF4}=tTYYWvuHmg7VQF#S+ooWJ@0~U<3+=g_kxhdMeQe9RuWqViR z{RSNDg5J!pe;oe_=dvZGaz%_n7;Z4kMn>b)BckP7$TcH5|%f0cmvXq}gqa-sf+sbDMmbA`=3-_XX=DVXo4r{+M#`mCtdc-_1VJK zVDb31pskSn#jm7yTSrq&Ya}|i%V$O9%^%u9(S*nd+ik75wwNd9+>mufbyC${{ySYexZJSq#GeR2V8|XPz%qXA02-qT+28sx9)&ee{y0jFx@( zep6Xfb0jv>ZJ>^LE5cs7`uX!|0x_<1UyvAalx}+fhkB=jWjLLQ!^Hh@pk_oB;vOqNvE@Ezt9fjYaf#`|m#wHc{Esr}jN_Yz!NM)p^L znb_+6N%hY!{S(h0S)ifu;YQoU=8~Y*k8sBO87ueE45FR{XXV)0#^~$0EV)8^}Yi{GhP4a!=ZLnEf4|;~&qh_Ro=;ib+e6n7IO>Pobsntlc-+ zn>IW8uy>aVevCKTKa&M7(zQP?HM%$VU<-c=^WR)WBJit`+=Mg zjU+v7r`1PPVO;FEyj`gXPA>kb{t14mb+}d($=~;qE22M_se(nLSTamY7)K_UgwEMM ziHguA9Fz)NvR`u2LGu{>U_C1+ps=|6a;LaP?!a-R zTo;5FfN{CcoMh1 z;MS`^p9c;co#I~H$^{25`=V1INqlpFiQwg{o$SWsPctgxLYI7Hb`V=vbJU3Kiz9B+ zzd5eK)s}DITo>z|tA(L0sukI)EgPSl%$u2dSI*-h>mM3E4UzoQNTG}W zXc_uMi0U3Ocy{dC+0@H3tQ*2DA42JWK;uy?jExHzKW8Y9T9kYS(DB-h^U*r!ukoWw ze&2)y{rqqtBa0tDDQr*3nwqhlaOOB|8-ZJPIrdB_V8I(1+w8FZti={CL-uA1feZJt zMj?*Ux*rFc0@^Tor@Y?DG(4r~Tl1Ecv=7}8KK#;I{Wi{iixk0fN+tf(KNS-N-6b51 zXsMb{DsERkOl*DfmLfc^UaAz2N|DmL*-RUVw}VFXfy7mAvpY1bLXe;t`E9lC;LX60 z@XGsIH#g2T3$g0poQaTgOfMmFe+QGpFHrZ!I+_oiUIdE5o?=&%vW=%7gOYv6jj6xD z7Sd2jNENyKn}r?Cqv+ldT{f4-9!A?MQ98<*5mlt1_wK(sXVXWxx>iyzD0@1Rq<1{I zPNh+%!>}coGqU_eR@&K5<8XRWH(GFvW>F+xp6bkY6aP>x-p-l08UqQiP8Hka?`g-Q z$uNvF=9>q<+^#ZBNIaGk)-|pAtvTM^>x~$FlE#?oi zD8jv8N#@~6_W{LKCb&EVONt^`Bnrcm{35z6#ItPdw+V?QUk11_ICP`5M0kJ$Z)5?X znka--!cyOK&k!!cd%!)TXQ^gO%<-OFoIgL)2GhJfXD3RXFAY1O{$aM`IvsK6al0jd z5Ec6!b@Y3BU981A=ZGMIMqWc@3VlLu7N@Okn{&{!`!MNcy|2mrYC9X?MCzD6<73R=)YezG@&i&{VgIa zRn2#2!7olrzlvO*8D4mQ{*7U)to9Qm-yuB0n0E}>z?`>BDKK3uGT&@A`u3zVphPuZ zWDP69XWP-K`q;LKiJ1{psj;16zYz!z;6r8e7QLphT{CH_{>8PM_%Qez={?kB92Lbf zZqZY~K|h{-p)~-Y<3tBDN=g#8lP~nD)ifY$i}rJt0y2KA7-3laLt!1!XWZyq#yyT-S8jlEe^ZJDW zRYTo#|IB9DM2e-(JR5*C_Yj|tnC{?s-dB5t0n%OcgNO>3>2HyKr<9G{z7kJvYY(mmzy_ZR5e18%P_dg=XHHM-75FE4PKc_0XmI|V zUn65KfsS-0$8XI(&rL>%wmo^j^9)k`W%!$Qmo2r9a#ZOfJEVZyIEa*cf%H#!C@gI= z*1o_SOzyX{`NOKxev7MJFzJh4`W2|*^Qw*ysk98z80;%3HXeBYTRkV8Q37rV?Zc-W zosf80^3OShY~#1kIbO3F4oS~Dfpr6m0e&+*$=>X*(z>p z>Xnz>Fww!0^PU4OdadP<=>1aD7P!Yr&EOE)AeB8a1sZ7UrX6duH!P~u*(Dt1Ek6!6 zLXE0l;b#^8-A?yy1s@BcW=UD%u7?NFwsX`ArH2{qnf;x$>7qyAbg4Tcn-{bn?QLdz zJaKHll0ep#tpm$^f6*XGiQ0!bfn8cWzVOk_$D?Cc2(Wnxyn6GAwD8MO*_ZOk0a+qW ziBP!wvm!wX6^VE^x9U~~I19N&#g??~>;C^JLvGFYMc_dDV?S%XnQC7DkmIM_( z=aA>XKVfXdz28-xwc-mPUQ~Q@U{LQlR)$2P(klc(vZmS`*u&6<)5xZsTe)LyrFm_D z;x7icT<|9}bKNky0tzDbxrRIyGNVzzDN`g$|3ZY&3B$)kJA5<1G8lNXXn#Yx z9(Wm7=JZ@m<_UuJy*uYLiCUsmwi=&Vauc3Qyp?n(Z$yWsbo}*H_E~1ulM71jFD65m zy$@M$p}KahZ)GPqLWc8PW6qa|=}0)V9Vnp^Hj0s5lh}sTo*3x#YuHKpCAWuRYTOq} zd?0PE`&Dv(;j<*Tn4404)lZy_}6RWBM&j2n-nN;h;I&6JR3%o&YH{Obgb1C*_Q3mwZ0PXRfs z)ka{Db5GiNw5#=b@}c72(ve50ZACg=oLtqqkD+Z3j2K*=6eGI*b=!hKqavL`ajS{V zJfaB$`ZWDg45I`37-bi5CNTauE&eKrd;FyksAY`7AIFiEcgJL^=UmnK6jPB})pdbf z_V>_ofGz5yO+@VxBP0!yq(tt~WvB4Ys#}fkey1IM96KL3|GR))cP{;&5^yjE7JJ;_ zFs0^ybHX{MZd9sQ#NXIx$-TdkMss>WP9uPWtXIn(nc&vDjEe;wSz!m`*Yb&3Ze#=g z3?g^=IQ5p1g*i~enN$1sx#HPmX8^*73`mYc&$*CBb)3}4m;jeo{TxConW#(BS_JRNR zNHkL(wabU5TF23rq>b!c*=G({Xo!@Y8~iCW!ZuJK{wSwaWZF|GrYKjKTgX8T_}1uO zVq6z4=@rI|10B!zXx`H}_osrFr@{BUM<1ZjhP=%XcaJgtdAY#XECt?o^e*VvjL6Z; z+(Sy2ZujpI6Po*mq?D{?FUhYIbdeXD1rAk~zTzIFzWzi}HFyz+rtC=wGv@<*PdTop_jH$y=+QN4A1Xm!|T#vr;r& z?88uSLVjLhqh3Jz#1OthuKamcPsgMl>y#NVje)C5*MYN^`=xp9f<%&rG`(2e)+^20 zz=ZbU8QVq-gh1%otYRO+X0N~{n2UUt1p~1}EoI(;_t1u>AbEYrcP&t*lRg1&@JWGk zSE9a6jdv>$#_tKm(R&(5)P_;K2AE0{4o0_9>~}%av3GW#iT800K}|JP!U?s~!t~G0 zf}20a=nC*O1dhBq@z1(j#E9MHzsTO(Yzx8YCzKcEGdIO|{KF#xa5Rokk3>%geK$m1 zBHEa$;8EbFTapPP_$u`pLkex!s+Lm7&2Y{p1qoTytT{ z(H!Y^9OD#7H!W>I0qA?&M;?y#muIK+TsJFMM`&(x;w4GUP_-O*F3)d#QQ$IMrkK{l zF6%hg|KZTD169tWSWVNq+U}av>AcCsx3nU+mp>m%Oz^DhG5=aFZ;s0in>B$;;&Ej> zu5G{b5gv_+G7&ap(>Zx=E8x!5#$?J{I)u3g9o7*Dpp>UNFa>r|zz&JuRr#K}FOll_3k?zdb)MpBX4w zw+CGu+Vcc1aNqB{G^sevO#1f%I7Ur#WQN-oI9qqNHrCDw8i3YA%T#oVBdKLGebH(Mqz#Of@X0ZS-7Lev2jXB<_1FN$Wi{o}Vf_9w0`NMf2ov9>m?0YC(_6+Nh0 z1iq#t9ZaI`1o%WJZhkS*TS3L2wqCxeP;mHDod+m1=ZZ%sv@fQjY>`!$5Z%kH{2^22 z#CBlhcCqmeV@(@F*wdJdviAKY)rE7!p6AD?gCXrAbvoes06L}WR>{OLQ?{2)P2AW0 z**BZtPB$ovIjU?n79IK=^KTgd&r+K^$4k{$0+^>J91-D%DjcW#&a-PP_! zzWatQq9qa4qq<{|5i7Gv(dfJOtj8YGcJ}c{4C^5)waGg?2@G$4r9+B%`tfEaP1YuMP7;#->Z+D<4C8b*!y(nQgz3bNtwKdpbus?C;QL-2~TUZlV_xS=z z<`!aLfRq^a_1nXN0pU&P1b?m&9S6{j`zlr^Lx&wr@1tbtT`py~KFBTTO25vhjbYg)~ zxEkiP<3O<%3kT!A<4YJ@)dKegEG*!(Wzalqvo4Uy9N6AMd|TX0S_t4Ad;h(3w_J^- zzeuR0~yB5q$6m$>Zol5cwIUd7jd_uzWke`u07 z3eAOC#iA5M<679OLl&s!5O+8uSmHt^Uka3{s3dzTzcPiZ+@VP+(@c{rMBGMTDf7G| zN~yn<%UzSO&5{86TrFMNHcVV!5}s6@cs3N)p8nM-Jcbv>-&oZLal<6?I9zE7b6HvCKPb->a<9dOh~6;UBhUp#5RO!?P(zk71AA1`HX)~9rHpt+ zr=wX6h>OH^f=fK@w)%ozr?{ybiPNNcNU2Qda5idL) z2Kr7uNeS9qD@Kr^+|Id^1+~EyA7euOUeltC>GH)wh4%d)zk1>4{q#~wD#|7)B>YmN zu9_{pH^eyX;n8NpVjrHjIzQfLqH?EEU96%{-y*&7C9p>3-qZ0wWLmZmt6P`Qk?sZO zXVklJ#P5+_QZ^|lICoxqS_z4_W_e&5&DU2gl|OzPG%hQa7=-b2?_u^al`ekp5~B5u zE-X;yo@Sl)UD2*@WoUKLm?DHawV7(3S`tq}M0IF>>SH{m6=a;v(1I>1N>U_W#!#m? z{u{eCXZy|Dzn#YiGKo0YVs&i3SR!gHCYvI?r5Pa3aZgKx=RFcXPM97aDlrx=O!6X< zx)+OD-7y0Vdt!Gh-HmQw!8Blm|3GdVv=n0Gyab9`Z(SfG7{95*9scAzN=3%+QH=3; zPfTLXvqo#}Olt$D&p$dbt&6lVc|d=)UrIJ>Z@KEE&mFOmsn6S;pL{V_DJLE0V< zI2|_3a^}ejc)W4(F;|yH=8XU#^pgLOlMSV-;|;$`SP#zF?7~Pv+a6q|#AZb#vX72% z4A$Vk$iP4FHf6L<7FC;z*B}#n$|wIvj}+=SSt{&RWCqYPpIsOq}n&na3q|sIW7|Qq$PHP(R}S z=9u$xV}qmfTN#oB0J={iicBFpmr1AC*)!j#_RF|_N!qlZU%O+6s+CM zWtMaXCuL)R#{&M+EU}!DU_{`6mO?xCtZLE*DZuD8C9$kRH^$`K1;*7Ayw;hqs||B| zmBR<8BJP{v9S>b6Hx%&P_c*?C&hSm*XU1bF3T2{GRfi9@H9G->4I*XE9f!J^A}{ry z4x+Ky9G(_QF`=9T$Sd&Cg}89QeZ#LacG z&qz?F-14w-Sc52LT>)!_Ildrf+X9iWJB4rCz+L=MDPx;WUO3_nqHR7;c(tt2M#ab_sG=b;z3uUEp3s#4~CX<9R+q?h~F|g^^ zVSb^Ws0Rd<$KBLadh^3_oi7vV<-!)faFi05P~!25*JC735cV4OVWj$US4cs0yYc0A zc-42sF|vfv_nktR+T&`|I_&RDyFO*Wj<(;+s_*W<+qDe)0{;)E&3`T6hcD0eWKl|P z&x%w*)H!k5H&vo%E7qt8o`!%*z~`(dEgc9DtQ|EJW#>#l$ipHoiM{#~vf_U^2A>OF zOT<eGTIYLb)PVPNkN6wYj#pqS39K3r9^zZg>)9w-53(Z89*j3Nt*+}rZzX@gV4zA~! zXt~iP(VSe5N(e>?;`^Wue{cBOs{ZSC|Cd3TGe-65@8AlBT#kwLkGcYYO5pLCa4n4k z0glC3jv&8+&Ms{P4Utj3bs6qC)?S}wdz&S#d}w4&#zmf3L4b1HX<^wa&TdBA$;4@&qQXG#LW|k9g3$)p2#&+gxYT;h|%kC zjpTxSExyV0?>nX_?V1vom`S}0YM#(^kPqMeZ+?LXp2RTqQBsY^5@=doNMC?d4n zr=f5PtZ6YmbCRzkEmEC@by;H4{+kGx94}s4)IUsHY3gyMcW^6v&(@w~GE3bWqtG!e*oQCGjxxv#)l!9m5B#>{1a}(d&Fi)zv7|&)%0&IlofVEqiT-rQj^WrI zWxHt+k(euGo)28Ahg*QUZ**)ArL3rAc@MN`uv$7* zt6z<(YY_dHtgj~U{9rb~eF3p%PqU(2NO82b-293JA2)GbvedTE3y5F2-9Uuj>1x7? zG{tCq*F2ldWV%QrXh4DArTeidg|s^V9XUBJm1nJZjf}yEwiAZ&H62G~$0cRjo5kcyL%T$gEu4PZSiRku$L)e$bZ91uo_3Dx z0W_*TUvQL4Tz)I0iOi^SGc|FUcrqrw)z@QFA1hGkWl>hfdF3gqaJ zCqrIg(baarfOQc_$Zx?zU)zY=QEq@H_PBPsJ7{#b5r$oUu_3VMKEC;`+l zW}?%%nvuVT3uDa&M>|a7;sFA$`T_(os*TPRg@%a%Qc=th4Y@yb31U|J5s5FrY{2P( z|7~xmd+#Of&4JQJibxdqfNl^f_BA!wwVccm5A(@dZpq#VQKmbhDEfsL640P z7o})>^o~ip4X7zALuF+u3!1CkegYyZ3#k5*H8S8N5A|l%VxoIKkg@_XUG_VmqfvR| z^`?~-eNWS6(zr0iL&31HdnUc?Lsg`23v|WvM4zf3{mc*@pWVi9D9}PE6Z7WbbVB`J z-~6wRgmOZWb&{N&G@3bO;DE6`tn6o=9hvH$Rn2=^)n?!0snr{BW-fskTMad8(BrkB zqqE!}Bw`Djj0O8LU(yL=OtKGL8f`a!o$W9p)Wp^R7+o(nzs>w#hBy@?^x-AyvKjg5 zpDj*JaYhA~YpmNiHfs3rB0|54eVSDg)!V3Lt4!S16zZwu89s{a6OVN}&;AOdoRH@O z{WS71Xv8gKneG9gi3eOK>uUUU%IH>&J!F=WlXF7d4uwMsKd=_*nOl$k!(}SW&#qw9XmnlXSO7I3UJ!ETZ7(|98K3m+ z)VGU-Vv+BW`tRrAazfD0-0)#-2_!vvXJ#BnGM?msRg|8717I@M#yE5G8kc=`Gfu z`*2Z3LH~@Dpn7_;+~0M8bm2DOJ1a_52DDQI1~MQl@7dBJl7YckflL@+IHL zd(}77)J)~)SSBsJi_#WxJg4**yxDZzH%3z}ebay1JJMhJP7WX|9F2Hn%0wPbEE0;N z2ns}_q7Z(HH)O+m^WOI48e7fr7 z&TXe2HANi7sd`NY8y?|$QKa;{Dx-CA?YOQqBVF$iuT>-0Dr=1iM@H<;BnFF?3;i}O zT98wIjnqzzahth{WN-}f+{u&40N&@5y83Ocm8TCmaeUvcDc+&iU4xNYH-L_aV92{k z3PWS}wCtlam@f5xR0{?vk`$NwT!~y=51h^-gLUFJ3%qbRTh;z0G7bu0t@3Byza89n zZSy!(qMyIEU#{QwnhWOtEeY;od<~7`Hu13BqBc=0(-z``X#@Z!02=4niavzvPtxAl z-pG`!a(Ov?l%IXZQ_j`Yx;(YjW6D;JFg9#;1^TU0u*Pdz3Xbj*60Yq3GaV_%%Tg_kpJ z_w--#tT(_lfoG%qzeIGe{jYD+9C^?r=yhrZT>TYsw7@PK&(4Zzi znxMma43{wLh^OwB!uwM0KIdowR%^=6+0))8oBQ4UKa2A4eD^)1UrGZS9}Zo|Lc{+H!z+e?C&#|$zrSTssyn564sI#wIlzcu<6LVNW? z{c)yKON0_6NlLg>_OpPo;nRF$+NXqk@!ibNt2W)5l1uaNg&a;wJ7w7LQ6k1TWD6YH z2e597YNd*S*k#OQ=J8rZ^Jfzc_mkp8=J==kUo_yrG*-tP=4L8)@=piA*@|Y-56Qiz zGp?s2|B6buE<#Y|iMO3Fta?m!rl%1{_CJjNLp)59BV>k}@n!0sz~NJA@k?Q)8LhoN*84#c9{V zrt3VouuW@sTgDVz`51YB!luLp#;1Wk|2qp_-ZTc_MDdB)k5X;=6^*IWx(pFly08tI z80i#lCu*2NOEU_IQ@;EoPzi);7t@r(t*@M3qvDOXzxhTbH3O_IM9zO3NC+Z9XqJoyQ9+=j#KKrCeK>)_`tU^T!fJ1FIc!s8&Uu9d-g zt#Qz#ya85fwjvtK{^+dKI)SVNzj&~xoA}LF*y{$qs#JbW>LIU_$-I&0H>D(wvNKkxA3t%OOxr}G>X4;eZ zK?x!S9_qb)m;od{BH}`2WqJiR57De>k6St(k^YBRiACgKo!NnJ@1cxh zE2-a}t_I$i+4Ao@)gl;<5f?4af{Hy^_-nyy56I(5fy3n9(0FT$?a7nw+MYYzZo#LM zPJ>V8VJXBG+bg%FX$Oc-c^SKYv9ceF*^&}Ht#OJ+7%%Hr0{6_luPez42$N80p1RFw zi@uD>cf)zN&!VvwxYyG#Nqjc@yJW6d+dm=pbBW?aZz1|ZmO-lkNm`s7*6p`>3meKk zEy+7&(p*`r1PYM`-j-q0eG&&^Z40i49|K7XH(^B{xtNII#EJRIu8*$auXI`|SPsuy zD89L970irKMfKjd`Mf8*0-3RN$6$n;F>D}_FpOPklGY;4hcR4V(yNuCtE+zBjqsN` z>#9|X@8(mWu7%qw_!v;N%daScNSqs-tl@}7#7(iuw_dT=scAlINjE*hfuU<57GvY( zI(R-^{m#e$^8KA3F`*qXuge&t@#uy9p!+*zHEg4GJ3fha(Mdi}aE>FR!t@`3eWVLl zkz^qBB=YnQ#Hs$E)C8ovUI_CEugof)(W!Nl5sNSr`t6dJC#J+hN37ABjEiQ}yP{g3 z4&?NVVS~1A&vN)4PVlT=^Y@_3&Eeu>N8H2jWV_IBi5{rqn0@w_catz(j%)N4^X_eN zx|sW@>*Z(^^YVlK?6(})$gSVgcd=t&a-WGYfRz0sQ&aKZUXm9L*GVj)zjy<|M=88Q z)hz%$pFIA`E}gR#6u>~Sa*Imnh>1Iwv9$_RYo>^r27EAMaQ&eyb2l3@7?>YO^el5t z$!vq(yPq&6GMnB_JJB2#K^Bg;6XsQtkk_YJq)vz@9`Tl2d4+yf^5NR1vyGs@w_j^< zK;><;Y#pB&@m?PWZ2%o@;O`WPD0jZ9JU1;;zJnhIBsTz%J-_?J1 zda2{CSqfBq#-%)ZCHo^Git49agZ@yfYZ1ppg58dlwBH;!d_Cds@U}cFC5;ghbs)@> z<5ZXShVcM59Ufyi>@}AQ{vP*g0jJyKGP7w|fr#d(oE$ERgnw+Ojm48I;h(_~zr?@-BY`Pco2D`=g*@_o9=U^kt%cej9O(9_Y6Q3=4NlQmfy!zVDoVItu(Ih?Eh zud~McLdnP5JZujA+cl-OaB|r}npuvL#)*NW>wWyt6}VVbJPC#ILq66+MM=lP4bSg* za0@(&kcC(B3L!+A?REJr%hITiqyv`*GD@KxE#hYbbV@QcY7Bwlz0x;}#AiJoA96!a zOUAa{>Xl;&>G#M}1_H$0p4e)V&V2uH%noJEeL>JXNXhA<17_%Jc7%{}8L_Pe++@U) zODGP1Ubc~+!6(FcmXyE~P(5Xv^-K;T9VG~`IsOX|Mocws%^Bk#am&uQd z5~?mYy6dy4u7O+Gb#K_Yo#cA4cZIug#rgE8g__GIE#81oe9SPwNwE*o?SwCRR z6AnNJy6*;a!|x;AM;k#t`Ka^#X1kIo$|rKQzD<4&Yy{p*3^){i9hF`Tef6)U=THv( z3df|HfI7DwwfzRYqI7HhNZ%_K*L)m*dzM8ENoSA-6 zWLux~c3Io+pJ`d6ZiCnxn~Nu;Rs>!WFs2s+*#RRLqM1$`eDpz>oEyV5bZcq6$^!in z?gMW>lt5+)$s`^IbndgObc)mS zXU5KTYO?1CSsi~yz>BI1GnT6*H4?3eOkQHh43)`J=qw3#%| zL%`!B?x;n>Ld_mm`mDz*@F)S!BvChGhE#C&z8imD!V5+(p|jFb4{ z;uBpx3v}fL^2;6)1-Q8h*V_-8MPI)(Lr80X*ErTgo$w4*!55R|FwTw>DRn!j_Gq;^;t@t5jX z{eXGFFtZi$dL8+C`gH7=i><#a`sEz*+r;OSOo4(N@Qu36Ra&;|CRFgpdVUAHlB9Va z^CzEelN2omEj{si+t={jFeKtEAnv#O3U zsv_O!pQn70|FFGmiC1}>IalUnXk#Rl&R3$ijEpbnN^Fdg-LT0RaV+mYaOQuO!$B3S zdH*H4QFN}_zcm_Ln@)CjvVie^T71BQ@`UQ%jze1>qaPP7HX3qpGT^LeW%L=&xHQW3 z=GV98kzgoA?;*{x2E-^R7mfd24i-{oqv?2;KjA?23+RNW{z1&CQ0Auh*12JiCt$r*z?$0|t562|J_Hw2&MqHy?hHX7r?AP!vZ zGx3gJ4$Vp$r$t9dU%h95e#R4jKRL2xWay~#7GC+A-UYwDmN?syqNLttc*)lu2Zn>; z5wX5v)jwuUi|KlqKqI88m<<_x%ZkgMtW*b5o*?zMpxCUY^N0*lXk&Azk!3MQ8R*60 zfr+fZa~-LKm$3?BH^Pbh##5B2>?N>KMcw_(0&lf{l_0 zv!0$d_gS^`U-mVy@a=8>!A6%4K6%FL*mt%$Z8IPQ`Oa;ZxG^HjZI*OPNZNlGPIr@g zLQMR)!8Yw_0J{4XrFK+G6uQQs-I2i54A&3XNy0p0qu7T16y4+}^&g4joZntHDsoo* zbc~5Dj6W>7FMZe9)cb4O;?-2ZoY6@^Z8{EOXL6C=BSv5$g^~Z2c?*tF3b&Kb5vhYV z$0Do71j~d@M(R%TnaX7+S$Sst_!d5AgKZ|Euj|F#9J}N`RNmN)@reIwH97?4DF+6H z5fT>JYn3<^f=aSDO)Yk~LW}bj&e7K;qeIy^QV9RTUV$&7aDYs+Gn0TViMA1Js zkysdb^cWvPWaJwI+h3^+%Y$3HJ1|5L2 z@AC5dSXjN`#wn;VPfGWi5oKJHkR2kA^ImrpKVA_?IMshGBWu$MKh^cUXcjq?U~A=8{Lb#w4ABO!^VQr?+f0#LNdqBm>J`b>!js4Xl)X_zcKCx#{?P3 zQ=P8ntKC><2%~=Z)fA69Gh@w+s2vrXOx}})pVhD*q1MXc*cn`$jmx_!qb=@fFk_qjBVpoRv zlXod22oJdt_K94tKvd>WP=5Y!n-vc;d@U@m1cIDiH~1idc_~@`T=+ypLS5Zw$y6n{ zx8HFWG$|vHd)jtScZ~dt43pTLnR43Rr;D`G@|oyTr7O*YByE=LG=&G-KK`@!M2n?j z`6#bzW}o$`MvXtH)JF>}*rP5jT!`{J*kxArhh_F~H+T*6)D+rxIVK+N=EYEYZfEHm zH@N3kiihr{t6ZqmzN%ErV^$*{R^S*9QU~6i$NOXK9I!{K&`M5JR7%i;FU-^ig0w*N zzh%2y57V+8p6m%%(00Il^|iuYe$3dr!WimjX=4Y_5A1-pce~xmUM84dcG8QV?wqnG z>+RQEI3PhB9oq#q*{CAU*#D`NDT5>^o<&{>5Q~$8mdw)pt%0N#z3>TU`_R)BwUASO==FJw zVHnBWkJVwD2)_H>T(@yU%RoEa;K*-Kcj)ZazLn^HXk-#&d7H48C_{uOMR+dM1P-SH zg@}{MZNTvm9^xqTJ_9G&USUz_Pslq$!3+`Lh{!1ZcoD2)ZJ8TX=m=9VA{3LHcaT~x zCtHf>nRpWX`6x;eI3TvpU!riEES$2{P^*FE*)A?!{d=$ zBhFhK*AOd0tYuMg&1!Kv%zuC-M_EhHz)g&Bo-}2`qY5WXVu7+uk1F9*mINt|@>j*z zdAT5bKPwK)+Z4tLoT?q&DGm0lJD$U`Z$|c;nJQ1WHa~gP+~{6k7M0455LyHdLVU@1 zIT(OScC8KQt{b918gBPkx-dw7FiDcr%xoB0J)K!e>5Z=yj^!xX-XQa?iG%D%rE%^~ z7CmP}HNpRKk^ff(T})Wh+8W*;QzBZV{RV6xWqd2Fv~M+f&4QXGe;!B}buVR;i>Du$cSYMC!ob;ftQ$zYLH&8eLy z?Y*%Hiv?(3=hVBttZS-#^eBp%-1IlpR|yp#$m18c{VKNo+G@=o`_q#Z<1G z?$3I6i)}v@Ucp$F>El@qv@*(f@8FqJhUuv|xGk-3pOA1RxN+^oukB41IB{(6v^@1z z#PTXIsw%3(<%U{q`I#}!y>WQkPBwfEG-Mr0PM6U4uJ4F}<@jQ*nSx~}s02o85^E`WPaCoIjFD;P^XVYAR-(uwg9%)oq zQnABD(eyUM zKm&DowycCT#nEvVY^?TEgIzA6>$fRBYRGivtJ;9qr%+${%BPPcL=sm5hxlU&m(&8$dhWvZnb?K7*?zh0Y zVf4`L^U4`-x{E zJ6$Yx+9aboR=O~h6ULwWDn4r5QPR-ytSuJPd+r#zQSvBBzJ^|?F{1-OoIav<+DS=iHy zXKq_hm^p%9z2H)2b-A&)R38{+E=i4FK&e>27_AsC>qvwyE`CJL*ymd%DH?PqMB zX$u&_;)JwIg?=F~F~i;x8Nfs(Re)j^`adKLWA2);@A{y=GyJL$dQQq?#FHehpA2}wfqy^de}*8|NTOowq~~k+4EA(moHqPoC zEf(qFEVSn)e6^9bm9q@cxpI%iU!0D`)wkDvnk3Na^rkOp8Kq#9B%QQm+hSEu1=w&V ze1oE9JqFbjp4ZU)!+si^$J60_M!mcYj-gKLcMI1HIG^ULn!V)jc0~{l&Zv|A)m2r_ zoTbrG5pkKLO^yNw{!^qpCe9H&BMgFK-VVI^FWwMXWD)Ri^#%&e5o(OF&{nl>$WqbA zx4dhUsUXA;kAn1Z{vM46yj) zZ2}8(#*T4JnDmeP%YZB%yfdkOQmx%kv^d-TLH6ATBD?QZi{5Q)j!FRpk)iVA-C&AS z%~X+gUIIk~Y#>`8fo*2CX%PPHK%I-4^OHPK)`~- zAHjFxJ9a|vVnUvxE5I9H)CSh8G#TnHNuqZ?B0rJ7zZdbEGgvSbz7E(mPJ23`b-Y9Z z`V$GV-bf(qX$W1yWVHQmBvOjv#AS^EB5p8AC1dquV ziOi-GM=kX7jMtVYu)ZdOZ6*8wiEoqLoX_n04q&LHz0&`r8ijYboi5qs0>RXR0C&cL zX@Go-S!q%Iu8UV~oKf)fnoGDpZi_zod%FyMQ{OO3KM0>hj&r_R$r-i`X7n}@bmLj= zpymd81{!aRWx6-ZHdcJ@(Jd@GrjSW+2Ftp)*jKwbD_->PM#%=r_J;^$NzLfp2(-kp zPj%aMnH`ac$|k4z??_fNBeU#Z-#*!pB^UviOpTXw)O}IcN)>2>(Dz^7u8j)7|0rW5 z);kKoE7WsdOyRBWK<4sbTM>Jcdz@@MUmf()2mj6ghIknK_(nmZu$Z=KvWg|si;Xx1 z=4_@QD@*w!-?gaH5B(4Jz8aPE6kLF+^S2)c;|c z$Hcf&L`^INf36{Db6&*qH?Ns_b7D&`=zEl&-YXV#P3;l-SvuhS;JX^EAm0nFMX5IH z^K$_iE1M;`foWcyvJr%Et7#_0N1)(+jyYk>3lI5CCtjM+$=ACsu-4b5-!kcn1kj z6%ePAqG$3^iH88?wF&MKxmc_ax094oQvFtes-e&+r66b5xXn$G{fHLCFxJo08Jds5 zWBFX9mudtUma)&~NlnMeaXUh2%+@5L3ZlLL#=@_>O5gyv<{*r|}-)@?3P!`O5d+f!%{MuayYG zBcHU9g)b+8UM-c@Uu4$fh{w63qT1Plk*94N`G35YUf;&RaG-=(SD0PAR@aQGxgt&+ z0HdJ^Doxzc6eG{%;k^LOKUm>JXr{4h)Q1YKldK4+7ZE7RgJ>A$HUWkvm#7*?ZATo#!?Ib`PE0S2gyF^(N|>00b~ZRyHgSMx23VMl zG<@NmBFB79b3%eVmVRF_35OXd1{Ek9H&Sejthg#ZSNv}r{HA)85Qi$CZr*@2q)>tG zBM3Tx@I~2r~dMyrO-wgV*;30j%9^sCatd=_XL4sD9carM)&QA zKF|?P;J|yH44rWv%+M1k9&vH9yL+%{Wj1W2aVV7b+b6?jT*%Y)x@qb|)=f52!rn&k zW6U@DoYpYI=!89+XFf14cgw4TLG078?9F^<4GNJK-Wf$bdMw|ZX0y-? z%y%pR_N)16Z4Mz^$VsmiJB&_vk)S<1stq&RwuU+zMH#*@BLGnPRepZ0;pR3G6ma~E zta(0$m2g@b5$5zVrSYe|0ukvPikrTlVsF}Lo$T(`hQe!Pe8wtE=7C&=8D^;K*}leEB!CWhR5UI*32i7r2z86WWCoWrmQ9L}PCz;DwS-<*MA` zMJ<8r&g{gIa@4M6{~28x>hBe7&sb6<63n7}_2Et@nRRCja*v=RWjS&J6ms1$%(S11 z@`3>F%YXjL|N5Xkkf6!&xm8^>P#JExyP3%KLmYB^Qz;QHi zGiM^8fH&CRPl= z+PH=!_b=+}lQ@GX6^Q)_zAl{xy!KlpZtg+D`ATX84M7QfG~03*!Gr3G*oZ;ju+i6; zv-!RjjIqTGW=(*|Wz)wJWMCTfzXfj|8_9bD=Zn(X4L6jo%ec9B^9s?c>Dsuhm-GwIN_1efS`cSW_VQJ(3~0IPd1D+0r)@H zo_lZOIxCrzihuG(Zi?n@e|@b?lYpIW4)vAzM6sH5_PbFH)W_GDXDcbAS8z*;`; zdE^1I!fB3Q%Ety{h{AfPRvZw3j)F>@&bt8U3a(oQO4GP@CbC*bJ`3Lgh@dngO-Nr0 zE}5Boh{W!KO;QHX$u@4W2gFgDNBo}o+z;T1sU!W5B{uSdsKr`W3TwLrEz@xgv@OL`_67CMObYoLf zxJT?km%|dz`<}2*1H@n`(H7!-I*rG?V-#S|S@~<>c?((qZ#f%@pYsBkbNmPR$3nf0`>`vK zu&=tx{%z6b4s~}saFG`uNdWQ5YD!$_<@Pve)os-6U{RnOr3*6}_`3ntfU|#v?+d1B zO2pw2W-=p?+po9cX!&v&1gA-F2Rrv7Uk_U|cHENK;wyEGJoG+comRds4Ei&6Nj$qe z-|S?lqNdguL-b0ouXQbmGfCny+su2>?rafOxad*&_-DRnoA&WKs(N7!zJg79{02+5M1F%@T9}HhOiT82Mo=w7rYM^Qg{Od zhvzN)s}UF9P5B+?m^$46mq$y;--+T<6eENr9s>A>_+}`|i$yr?p7KEhb>Z-F3cHH} z|D$?|B>2;Rw(x9b2%Q$JB{X#|(Lg*+%suy)rbbT`$%_vJfoasn`eNJb1%&S{Hg^9( zP3KW6tT51E`o%9UG4V>DW27e_cbK5|SxK3R`ztbO2cF_k0+3t`AcSxKCIv(P(!fTY z*7&|>@mjjh42+h<+cbILFF8m8If&q64}7~v=zNX2FfZd$y)T0f0QtWG_)OdJ0@IjB zF;t+bgyE(saZ*RvK1SXbxFrZ_(D}TRmT<&?1J9u{W zYksE3`)~=dwGxz=FB#~E3`+C8mn@udBW_AwHlOuvRH+*7%J4?wir z9|04=ZecfQdTW|*~EAB|i^0&x#1@?NW#4v0QXoL|ZJO`gPF1_ZCgnOQ z47^;tJO&7y3v1YvZns|)jTtoHyTDqrn9zgYKB4*7YQM|zz}olj_W!i20*yN1#G+cK}2JdlTC#qR9@;9wU)rnR|3t8 z0+p}B3yl))ZMxKLR+J*H&i*1RvyvElfe$x(XTgtG!Dq8__$t6$Jq%(gOQexlx?F)r zrMXvk2i`6r)7riE(65fjEv7fbBtpswFs_bN5T~0~L)_w4;sRcn>d;2Yc{L4|_Z81WRDUakwG6mi@xUc4GxbJVT$X;%Y zn15HdvFcjGtBIS>rAMd>CJ8y8rqe61YyR-XH?JFT`EBd}YXfNfncG#1{GGzPe`5~; z*+usxco2zuiZt1o4Y)Mr0LGO@xwsQQKI?w9rjwK0uof(Qp`9Gz8K`_VwO@?V+YNQ( zW&g7Mxr!;xlEwFG(|Nx~GgD3SL8s9>Dnp({(C+Y!1#f%9qZN*YSW`mN)aWSY5pFLV zrdxrX*TAW@3*)9lVjyyBG^>d>h3!>tpGW$BFHQpq9mW=VMUUeEepD<_uJo9g-u3Yf3oQEbDwct7;)z+DGl>yh!UZQh#?zU zYIbtj=(s)VEQ8Gy+z1cf>fX2qV&JlVVkBf0S$&<>XQjcf?SLhLFNzzj4H9P1bt@~R z*NlWW9;RG^L5&%?IKMQc{sN%0j{+MtGq8#T78rTTW8?RTf_t+K-hy9Km*d5|#2?N6 z9c?k$Wi>Zb&A{GsaQbQbOU*Gu9xbs$!CR9MU@9+erQfHy?<-Lso&5w#I`Z5WY^q%3 zt43T_JM=&5Ra%RoA(B7jOpwPEH<2GCeM8#FymzW;AI53{RyP%Z>U+TPX~F)KibxeY zefs=Uf)U6c9=!4PIgA+7j~$e&#wgcc&$pWyD_npEE|y86#MNGIaj91QFdv~Vztuli zRW$n#`06LHvt?4cwn3B7IX@FCA?SDu zBPYSDIu>OE>nx+7nWv#_&q)9xr)=;ewNhu_dakQ6f zm_ep%3s{49vA=~pqWKDR zIm(gB+kIWEp8rB02_W~x@!NoPQa0v_s+rKot2bHz3w1ot1HtCaAMPV|d(e28ZOYHQ zH4*m|7bHQzwWcx?9!7(`TLN(yg=^tNM9@%NX_E6+@yOw0lFu&ZO3z?^nwh%E_S-~UzxI7S?89#? zg}OCxOJejbWH0&rN|vT120F9FlX%y0s1CvlQ@R{boNx$5Tr?<_6dBfqh$vT%w(L+0?jV66%fhz+pT;F98?7gex8RGJo`*IE#t1NRuF^9$03o_SWeKOBC;{@LBs6ak@r=P$ooX0Qbgf_#oe{**} zv&<}&#bf?zX2T>!xOD}lshJo&SE}Ou>@(mXSf0`&^%m9j2|l9aL8M$Zk}z8!)KVHW za`>5f1H=U;GTz0G73Sx=3_Jpb=Lhu%e9OHdD2Jza(;oGMxmS1hF3Xl#G(7vJO?PDb zkh3gDoI9uQRb#gj1r3+Lc7OBNa%L*r#yat12DpD8Lt05#oIfXisb1HZF#gT`z3i8< z{ZhW1q-~7bQam8DT38$V^mTq%c|1#totMjodP!4$$qZghJ>yoSX7MGGykt64rhi}$ z@M7fFR%6fGE4?B!Z<9+3JZzpnJL+qs)OlrzyOk(v)QVD+2lxjk%3BF`Cj0d)1qbq|1+^{<*J z>vl7IB>@MGg1>~1rp!nRHy=S}%ibFjuBHJ7S}q*j*`E>P zeR2O=_Q-CZtv1LJ$AwmymS(fIIyC+s!TVB&q3D0YSO34lSIOJwH=)6R>69v*^RL{s z4F0cJ;lCZGv;1WR|J}Gr0$ai^W*d@gYAD?Sq_P$L%*ZI0nwI7g61K8p2KZzNfq(J26!8CLKK+;cl}Jr=3slH% z7KHT;2-VpE$D_-%{ylChlLcZsks9|W!7sA`D<*SJPR?bT>>d_KJBaOHDqQdX_D}_$ z_N@T=<8~hny23~q97W2(Z5!J_=|+#z{beg9HPu?=0Z$^5|L@RQ|2G-zCOa(P4`4mA z9@l=)8m?vdzR8MRwQrkK-TwR%+@a<6FYoK=zmi!0*51~q3vf z{nt|X|48K<02~0%fRLL0&-q%~l`=L}sBxQnQ96&g$=uu=6Fd9t72s;)1VSv&|4v8o zgQT5<;SZ0=_!yI!Q_BsZ{y?3=;-p^tkHPY@vwZVVe^vCQ0w5)R`d`}DpQ0~`73ytW zvR2S~l#dP`HrHBM+Ft6!;`JXi*V~+WDoOiZMmTjq> znu)5Fn#Oo$`nH)0|NJI}+0;+obA10suT&;Z5o2$=4+}0z2C>eoIV`3=cB8_LF31!f zN*~+Na+jQjAk)aIWnIPZuH4+YKIOSi$rzInlW(4FJz&u^;STWP1}$GX`2StK^7hn+ zfT_T^lzkYreUMruBN7{Y>2u?M%GsK_{ZcBkw4pgvEIqaV!C*QS-=Fi^P=zJ?Dq&XT zY)ADFi?UQNeL6^# z8Ynlt5RCe`^zX-20-lu#LcIs8Ko=e?L&NQ>fDyfsSrdn zz|roh6x%Ho2`qwDNK}apwMC^x)L; zZKrqQ1RgDHxh~7og2rt%sgsua8oO0B9A1-qX6NnpPUX%k7URx24N%kagK?FXz0ALL z7UO*pz^2bgDyl#QuLQmm$)iV`gC zUnHNeO>4BINsvJ^H`CHN$`n!qQoCGpuf4akrEb3ErKQsAeuyjm?Pna2qkkZ2w^p>t z^*rdjM~lPGHN$lZrq*iTxIH?2esBlgjp1YEk`Uq?R0?npD{+Atd~{yS;mP%}HL6|8 z_D!(6UYoKTtYT^Kb|MFy(u@BZ8ioELV*`Us=Y={Mz*FiX)#|q9ip?)FF*%w3cQef3 z&@Tltt(Uvw1yze=tK`ROwB$thk<2CR2iWLXA zfJYOl|B|{WOCn|S-@W4?Cnq;>rkifF?DD&)a(G$Pk_$*MN83EMoY9mKf{t_YORa8h z5!Je9pNFf$t;3CviZ@aP{urOuX0u72uO4agXISkMtJEa3t5lZj4(2H|(GRP4*tx9m zwC8%q&O{3ZHKH5e{7GH8u~&KJ(Q>UjclgHBK9E@yrREtGl&4P?t(&8o`QqRSLuJ_k zpX#YcU7SE9hj4YbuE{qu`Y0hIIwg^OZkJzNDL5Cg%&6n{nvA0lX1b<9`K$87JPu3( zne)kuoS(>U1B6Imv7YWaRhu|lXir*z2v&}WTS3<~a2u*_<)Q#;QY&?JJH1^TSK8`H zXn~AfV~DQ2VmydG3>|lz2nO6M(r6_779bps1jE|&n!~DETCK&oGhrHVCmVyOV^f(2 z?8nMS+ag0f9w~(OA(C#vv3&0m*wol_>AS5=PSvEXQrvWm+f%#VeAup9v#)Agk9E6C z*U<@>nn_M)JR-2HR82kKi%PwnaJhu9ld9;OC}VAMbycl?Ag0Cm06AC7m^0nEHV`QD z$(1U5Gqp2UvGj^PtD7;|JfrKWo&WhXPEKpbM|Desm*`UtjA7-?sozZ1axLQu2!NZz z&N{>2bae})$lZ0RNyUEZ-QWC0E|A)yZLsPK6$fOie3KLD>^2g1N96}2Z`WE}awiKU zOoQ$(41Y2AZ$*04qSkd?GN&ILIOkr*k@05c2*7rI{|v{IGXl1rmO>&H^<*H!Y5chx zhy|LaaE^EP_INeg;@@Un2M22hP?9ZVAyrc??;IsnDoqr1C&;1p9CB5rs`*l)`kpUU zbNNAn4FM{tx-aB(KT(EcKreGBh~ySpZSt=_bTf)dS#Y#d?&hHEt3P`fhPK!XUDX@v zrx&khz1R^MwF$hP7`&xT^WpKe|%1C*T2ng^xD%jZgtH+z+6hSNw{}kV*jB01s!+ik5f&33ySc_@BJ@! zt4qzk`Mu+PXpRNI73nx`{gIdSedAkNuUOulj()_MF#yLxc;|0As>yn}we%?OrZ+ zF-u7XU=IC{Ou!^e2W-A7)qwm137M5}T>4_1!X2=C&rR$z;5zHL^yGw75JU`9f zzSmJ2pCl2oz5hN}z8jqsHslqFd-G?ac42xIgT~iL^Tj?>&cmLd?{y!B-O56}=h4mH z7-NzYPp*hdW;bi0GvhS?dP#1amYDbcLx#EmSj|f+Td&Mwn)K|;sryK3B|_=e-y!{w zDUO>lPQ%#wtHvynazHv!WPY+q6Ms`IC7r*;IP%%;FfL_+i)OANyzW^HwYaqF3!=i) zULp5Q=hX&c^NUEE_+p4^E;Lp+~H13MHZ zUHaz7>|fD+g{;2CEfL4G>OYFwJCh=I4tEoqmZE)K7+F5QVr$humZr}TTaF)fuw+F@ z%yGsfZA9tC)=ZD{%Q|-wIxKc3SYPfrFR{1hw&dQML9S8e>ttO;x>W&eLD?o3A=T(6 zPVJ-z_hagpWvz5%^@rsQ(v17KH>Lu1v+>{0rb;jQ;fiXDO^lb`$I_&*Y5Vty_a|Lf zJ4F~Yb&)5ciqR6kM;X0Qzsp^%@d7+o`bWftLHkiTrQK%uGtWzMn91@+F`(=+b?wyG zeW}a593AaI-3>b96~D@>vTWsPgNck#2P1wXhlkiEuvn8@N{Et zR;Ig3uf3s3beljF=*%sX8^Kcy6U^k$|0F+2xw4n@p@9F&gk&YH@Llt(G10wzR=^>e z0LSG`kD?F5n3(O8Q>U51t$I*55P1a0p0tx$VghYW3~>Tjr06f?=JVQ~B5X0ac9S>m z&q!@~yzt8b|x-$A+N4>P2EAH?O6#Wd*9wKLSE58?=0)28EHga=y35%QQkuHDd-Fgm< z8gp{%re8E(JnId-8eb0y+g>EINDhMK(evJ~#hsl!b^%LSb1DXnpYjXp@5YJ`4^fjs zmkPB+iCIf;exe>H7w!zB%Y5bLYk9>sZ|0M4<`!;=a1ob1y=$DQpLsr>4^*$rn+2Pn zm^xdjnS1P)8N^3=1_0Ugnft4;FIwX4C_#68Z6bVTJ_8rRnwrPU^Mk)u){&b}PBr|l zBo6f$t+XY_qZ*H???-P9v|%`>u@xwziCKg zz$q`J%~l0hAi$62ivR7vqu?5gxzz@oqQ(I|wu=OeV^Wnz?>&yC9Ul4li1O8oa{hyF zpR%YUdz1C%x`9p8+k*@h6x#3?Uj30h8ZNJ5Y4#Pang5>GaUIPbZwJVR+4&zk1f<`` z=TkEKY?q{p{b6UEgj1vSPi~8lK2+!!q)wFXk=a+~FxY*JTSjrnb$4 z^ggb9cHLgZD`Vpg8_hDU)o`$(hlg*pFxk>h&S1xfWl!fLS^VYsyxq~fY&x`=Wa>ba ztXD-s`>wm+mm?DW`d{LCYdG{Y{aeFH=@3|;J}l_3!`@Rxn$^HP`Rd75>p_ndskc0h z8MY-j9adtF^UoY|*_E(~8pH?m%-Yr3i4P9iTxnTRaM9@z-XWT`HXEIyk_-m5+do6W zfpan0zH9!xRn&x#>D!|&u4K9My1@JG>_{s6I_qnZ>vdJ;Q6-c-%VW?jqhreuK~jm1 z{DX4W%^krls*A;bnpWkTX-{}`>G1|pug&~d11>YN?i_A)m}ms;+4#Q*i>Wc`Dpbm3 zxv;2m;%NeQry_0vV9J}lLp!&1hz)90!Ui=_8q2?JlU^^padp zA$wb8*{j1Gh%(ikZ5Iy8fHCt`c1c|6fygzQbEJdq3}KQ|u_7kt8j|di~F?@q`8k*YtE=Df(vh!>j}Qkl^BRqpx2%E^bHT zq)VYvx-t+xT~EzfDUV^i@DfOTZrVq19qS8$_Pg%v(Ue?%3~BsNhLIPix2w}?Z}lG> z*=wSvT?D8*z1yx0-BwmRhFh(j&dRoUW-c=Zk`Xh9WF-xK`6m-$`8Ugj?4SLHw-L#j#r*Q#Hg zTMnMmBB)UCeAthL1mA1{}B)Z}~CX~Mt!{f{m@ZJ>eG(BD!vZ+1}+ zw>2FPW4sNs`2l@+Tq8jwax9rz^Z_gUj~ScYcWg#?Tg)D-i4r3is5Rd8!286&mB#?~ zF_F^j`VczW_N~b5hiPJq{*lMaz1r%a8Gze_yr#CuI8W%|)#~`N%jZM(N9s(!CQ!=n zFp|0iQfuya9eeHvEJxV(kPJIH z4c1p--#2`$)irj9)U}zlfez|l!76F9q?VyK zPbHqd;b^SjurnmH@X*cC!%o-QGB0c3u*;ydpkw+rhS0H@8~U`ltAS6bGZ3GZ=792i zY2l&2!RU}VrsfjYmeuBJnU>eKlSP>(eIImk1ecXC(qs7ZbjicH1?2Os8yK1la!fEt zJvrlF0SlBl&rY4!Ej&4b*zc!pTTS7$I8SP)#+9}#d4j0A@8-Zy_$s(e+D3g=wpTsS zSEe@RTka{OP<8d!5wslbr)0DC+PJHHp@JD|yQ@i2ZE1H9sBgUn=AeIJit}BcKX4@< z7!~y1qb5lGD(^|B>u+JOh(0llvf?D^LZ+ttT=w4UsWahDVKi}ONcVZ#{Q+WJAd|2$ zS%V&)Sh{um|A1XHk9n@__@etGPbNQ(wNd8_{sRQdoJ`D~mMk5%JqEY^PSZe=+8B9g zN;cEeKU4~KWwfkA4VrU0C{cNw<>7bsefhSfs0_ zRb*u&33sxwPUBNwp`jU8#BunU<7+XEYKd+aYtDTbo=ES$d$M}BBJpab`QrN2On@N7 z$&|gzEVO~>@_D1fQy~W`aPW?CJ?_mb+e_bR+P?i}b0uWb7LIzG zzI0fCO*lAsv^{%N)rOd4pZkOToZ-lXDs#hk_6*^8x-mT+xK#~po4Gf`np+oP>-D-m zt4%xKs;>EW*?{&5Q`6($VOe6?<6K5v?9ub)Wbnqk&{@;*r@{eK_}J4+6VXf={+aBk z9tgeF(oSeJ@@TF-U}lSRDVw*@;=YVA>@AJ>ky`d{G=tzYp`5vo?RG6SF~>CR1)I;C z?^1GK#)^6{B5OFZ?#HK94M zGFz5d?uL%!uxas@`tJC81+P6xDv0ay@3-jPs{DFZ!LD;MsS1tk4VWxzK`c2Y)mt>Q zX7N5ZMW@kqmE;C@`NfizRo^vE`f%++UDCfs=H8iR?;=w*kuO_L+1*u_J(-p7aE?M) z&e))RG(_HK&Mjk0yZJU-tcKIHLX=@ojdf&is{lWyXsd*L>~a1e-xSTMnBS=_ulR?y zm-jap@eU1O_ZJBAB0~B86!FdGSaSTRi=t?z_npsZ7JiCwWD;V*iawt0PPgDO;?u@J z?6{fFwm##@5eA6te9Bj%a9G4fRhcl zV-#e}!0hIsLmi51rbE~fwpCK-N9XIp< z9K$i_#n0~X^!u&`UeTfPE(!>StKTSvd}03Myu9rv#wnV6nLv~2^V_#B(!pwOI>BVD zQ}#0FDZgqN{JdL^${BceyMrQ~#(4l;?pn3|2uh-Z?WuYseNotp_T81>)0-HZQzCbn zlz}Df+2*y7qo4+-CH=O$G2R2-^ad?@X~4JTM^W_6Asy~I;8oZabRgz%FPshsR~SB1Hzz4oJG&BJo?p3VxRhNcN!4+Jw>gBSQtwSfq*JM9f*BCmPp_Ox95=KXI2bP0LO9n z!(K5$Nzw|t(x|psaOiNoyW4DomFzViFj3er%<2*YcXMO#EIX!S%O45ce9`0~o5cQE zDU#NWB!a$@$%E1HDwp4UBnC*yr@3}r4@FJl?dXz9{c|T5i0%X(DDaW4^P!{LQtiZ1 z+7lZpq$E{kTmkHlQ6Nm~5Ut0S@h>4f$h9VoxW7lO1HZf;^v6@$r0c-F2a+=>Xc*Xw zk_x|5bx7aGTpx7-1Id3e2U(W2Ho5EAr0A~(<;#0@%z`{9HrKjTdtCWa%n#gerR0PX z+_C!-3PQJ!oVRS`526S08_zuuyarlnPNlX_hMp0vEaKK#o#pYB=;g0RNBa_avNm%ZqqJUa$f^4^xH5H~RL-9{ zk1`qP7%Gy?{`TX6$`6@`tx=VnZzAjH*K%~!%)*8bes@1MT0ZO5{PJ-C+a*4$`TiU+ z|LOFt0j6^)Y1TfRgI1(5bALp@A7?a$mf!ZSug@1fV{-8+$@w2au4Wh$+;gM8ZxOUk zCmlIQS=)U>*S0;BXB+CUt|Qd9wKXkA#K$)OIg(O6;oQC0!)evw#fSPr;`#BKR>i}B z4Yao3Ga6MNTK5qeNGkS4`>7lkS&yxu1U%l4m%bhBA#BcH#n<|>hmje`x?QM*Q-@k> zbJZszG~^nIVYfn%)Mm-?Sre40+1+L+w`t{!5~(0(!lqa%8!C zlqwl$s3G2A1Yg9cSv-30oj^xahig&*InXO*%X}6iW&jFCqnQhIo&aSjLryBo+2K-zx7Pz?7t#gyjx1!D{t&ax{7}aTsqnjo#QuDhQxsE zM`;m)rf&jiRlKkUGcFixGsM}5S&a-i8mo}lA);LIEQ@1?X<+tWttGPk`ChayZnbiZs|FyDmK^7M3q;|~_bCPGn6IW*ogKlOs`}MVcW2C(Vo+Z>rl8i2 zYtw}d*l(|jZZ&hsThVs=_wH_rd$gyc@@rXwwf;8A2k{%1qm)O=i7Qf-6=RFn7n6+x zPB+RN!!k<-@&z=re^XK|-%KqCT&dB$el8^a2=x=W(NuTV?>w3lG z+B=3|BuHF#J#b6DEQruTqJ)M%$rp~be6YA-ob`A28{uXX9^<}fnNEwKSTeSv82ox~ zp*DS5{{D6F^>ucqHOUZI$qG|?i)o9xSc>Y{lH~`fV!abCgN)qNnR0{(CTagN9w>rX zQ0#E2v7GUAp)IfvFV5TVod^k5P5XT_ z(bYyXO0<1rYS%4K3dPmZwCTA=%Sp&AG7Zao#IMERi`6W5VJP0O;5X{l*MX+@w7n{G zmIFD-lf(hGCnQu@UoCC3QccSk+?Gl>JY5{vFGXe8v7PjMwsV1Wr&N(9(Fx;YO$=Uf zj?mbj@Iha=G*CiqOBm}N_LhyJRgyVPX-}J|qMX|Hnd8$5-m??2}yRwLWUvfpL39sWw z!uGeY^|+}cPx|pDyKmv^_^PXM^hoSHZ$}!r>VRY8is7mX{Y7_b(hkKxirrxiT*ym3UKx+7py6&4b zML!NX#3_Rs2M@%io^njN3F69-6Ml{N?pNxQl95F9X!Pkm+g<9Z>_fhcAjW6N^2Kx)&(mV zMcAZ*4rte17Qg|?4shHGov$jApY+v9A4YRc49aT&<53aOK1@MI)UK<9Dgmi5G{+hQ z8R(SQ+SJpGki^(oV*Ri>R@m_|RL3xag9s{sNPJfIf?XT955|cCtT$Y3ndKAdAd(=7 zi?|irVO&g|05L=~U2>c*kPyyA(puj|;bczI8!X=U8BvAY7_WMw{*5?0(6HaS2q=xUFJt7(RB-ZaKd17lbKsKyFPt+Y85S5mvuik#@MV%B% z4P{5Dt{&opxuDE@Z8oaM&%2Ob>NP+0(zIyOf!iL#Q}yjV_s|}+F@=|%aeK(com8T< zw5c-2Qrl5%gDLAzZ>qQTc7rIRexA{dcCKG!ws%XvIlS(wg+rQeZwuK-9)74k+z#tC zSOuJ;N*nx2(Dx(lw zG~vyvt@Cjim|XnTm{5Jc`|Il|0F)hnlzr@D^6VB7)mZU!EJ)BA#%*{vzSGzMv2uWy z)X(8<_81bPU}A#R%S~zHwHJi#TITM2;84D__pv?nnqe|e9KgmrpER2b~KSZ8$#drC90z9o%_geZMtc^9i; z^zQQ}B3n|4gR7!Xi6Zp@`l2nGJbOaHlOR0Bejt}zH&E5->@J2Y+v2!v^NrF(xX6gFAk6{caIV>dUxNX1fY<#{|U?9A%4 z-=Gl;t-};gdPDI=FE~9zaL6i{llE?|mRMdT%k}A1P|l!2Z>e#~<1Zz<>!IHlA>Twy zk!i&$uhx~ghWSdk_^P;Ub|tv=)djQLlqwFR&qxWveA9v+{yE+G_+$h*3WjREU*?)q>dsl8g0(_z}VkKH_JM>+k@PMn?nJGBg5(?#l-wU3Xs zoGb{BK@XDeoy8bNt4)9W&;&g52=WN+GB}pS-aTr={e4Dsv87-1=}Yuv3UiP|2$z@5 z2pzzfQtR8iXYn}J-n@6WOkfXW`)ZBT$s6+w=WiC_P*m?BS#$ua@o}0mMlloGCRTCb z-tj0lnB?SlgcS#_1(4znuahQWVbPB*Uf1fq`qw;_9?fZqhSm>#!6Q*Z(R}pn#iAYo z3L%ircr3rrtJMHPMJ_!`rcETR@u%&76;+_P&@V^8D|jliA8>!Ma4?oplO0x9r}u7z zgwRa66|e`#9|M8;5!JE{pzZf-%J+N<^qN?AuVtho^6+E^;JL&6Xo&} zt_7rG^FjOCM*vfoGR1-l$0^fu^RkJ2UQD4}{;at8<8<*r4|z;0zD}YOMPu4p8QT%k zb_zLjk;Cl{^C_iKPZ0`Al2X)lD8w3#Lgo{6kaP^m)#1uV5%vP8`%EK&=6uUZgoL5g ze($E+>=ySfnVn*bnJfJxGY&!Sv`vg47zumXWMzS298gsgm|N z99Fwbd@*4!&IAT)8Mt)?*u=(uj{E-HysCOfrx=FMy|LTg=@gw11Z9jaK3!lC zRs5T=F8i_6GVpZXRdxKR(N~9nn@==rKEpTofX9_se#pfY>AZ?e7mgl33lA7wwI~!I zSU6x3!^oKM4LRajdZ3Xt{aKNQ>$I9FAV?_J^+Rm3LTlT0?IVUO<-$B)-&|F$*?IhB z9!P{l{yYVkp)Nf|+9cy7$1zeBK+ohKQv(h=3)^~~_FIW<+ zuWxWEIW+Ft{XlicD_1uMED}-w?5Z)TEPGu;W+7E+&{6~UzwItjZ#?#<;gyg(S+r>w z6g8`m1STdnLDrlyEi;U4c(y>I!aVQ6=aotDodiI8+?AHiK02ZJq}z3T#z;E(pU( zTLHGPc%93Wz!IC*T`T7JD3*P&j8I0pj!aC9rZR)Wh>&WtE=fg0x6P>?Om*T;OYQ>s zCeJajO-1lP8`ZasOu5_b@(>P?Z;U*vdx|65Xk?iFMq78RtBx2&;!xfVWYQNQav zFe72GJbGDmwQCZPTUXa5BV5-a^L(QuW@WrTDKk;}RPdtPUxSv`G$Q-u@K`%AseGiz zOBfP@*H5gch_BdKasabd)gsgcr^1W>pg4$Yd+?%G#$Z1ds;yqAG&%@$$GhSs+Wr%*@e5ig?mwd}sF-l!x#c{;!hgQqH@Fl5zX8>v{pqoL~H(%$9HYuLvb*Rrq zN^Q_taJ|h!_#ZF>tjh9XRZC%=lJiBgyt?^KTRD)KJ`U)M%d`y8mAOKW)pWm_wr>ny zx7o_H@E@r&nFOx9aX80a$J~DLLld%lXWyFE+C9su^)VdA^T|{>@@Zb8cHDPA@(n(s zLuq`KpcTHZ0A(VRF%mO}sJ@>p?&?!bIhN=)z4@AN>*RRN>cvBeVe6-&elr~DC`9FB zJ!Om2YNIIgO1WlkgT8nYD>OFZ*fM>v!3ttk9u;4n+Wp8%FM;WF=2+7?^gO6f;Uj`I zkxQ;#6o-h0J7oIfzl78it&hvpuT>k^i$fWgi+Afiw&#r0{fA&yQ{o?o+J9KOlNveq zxG51@z=xqpF_HrWyqF@kghi356J5If*+=`sjsv>rP4Yh!x&6TC=i2mKVDA8j*Rf5( z5nQ%%u)@6)iO=Yp8ynYBFiH%iTGc4_z=`NMu30G>!?^5;L6Q{K{2#|dQtnFN#usXj z5BCR(v_F-dx#1Y@3C26FHMbDF-kmkQKHBCiZgi-)sF91tcj%IP6H0y4C7)@ zZP$)A$(QG>4N69r9Pq;nd>Nd+%riWB>$zJ(0r?kI(`6Y9f%EJtYsb51Nm!Rsnsj-! z58wwpCnU}hsOUIPc8K|l<7soxbSTDU4~!BH6rQ%I=Cf6UB?r4a9YgxJcbbr3zl*+P zASbc78wtc|yjFH0V`T*aHdCvI-vd`_p3!!H33VbqDT~x^hhzKQhb3=+JCh%6=?ogZ z)$asX6@vqTEFZ5)tH349FAEWZ7zQSh;fuR12Rgrmnhk5$xYVR(o0(TT^MZz#06VRf zbn8(!O3v?mGkQC5!J@@krkT(uirOh+ZYZm7E$@0k^ZGY%ifA}kGzR<@2H20MjquTD7j)cdAo( z1I9+lRb-~ z_HdRlT-&Q}#R9}DWaim^i_>Fq0)Z!rfhh{44#XWg;Or~kHAO_kJzx2B80!Eq^+a}C zqSTadh*ckf69?fmJMMFrMYMe?%6su$s63%{TPEVEVrpE@UR%?SB-bxRcXtWs@kXE@ zJ}$=Rv+9<$G=NAl9~YEFgp=9alKte|JYvvW(#@=P#NZM~O{ZlW=jkB9XLVEYLPnQ& z;vq>_EiNJ}Dx-;YCwU<&Dks~vw6nZ*eZ{6K>t=%Ty^brBOl91DDdFby?g<@o9j54O zXjzt;=4BQbbZ<=9jlAyr?ap#?zEwnl?;ki%^7XmFG*%}!_%k76tXKozEL`@bGNfkRi#MZ+DQ+>oY3;eX_lO;Vo)d(7gXoD#5o-D<1nwR4J{$?+9#9k?J0RER`0` zC&CQ&$m;ptZrkm4cRb0(da!2IpacKuXs~=XG>GN$b*Yd zq_k}eM%!9Z;LxQJj<~k9VUDUf%SZu9t<#Dz^n+Pf0_2FQ^Y}y1D5FhV|IQXn1rF0o~ zMJPUFkx;S?+=37NWzq?lBj;AhuM+*<@cD=0(g|BfP`&|2?`>V|0RSb&8QcL@Yl2V> zum{5ei@+k3xMtcNKFz2knIpOg)LZH(H8`L?9ntF{kDZi(;BmiaCcWu_{p;X#H#d1i zxJ_5kbmuc?T41(8U?v6^0-qalBM15(A z`k=->((FRI)GX%)135R?WrAFhTHQ#eRlWvR$&Mnh(n-N-`W25jaiCFL;(c{KC-bG@ zaqWL|V=igbQ#XoJhtCxB5ytnVBeT?Rh6Pu^e@tKI0h@Cx3;#~&DZ`36IjrY(@IS)%)nSW$GW zo`V6|#zl%y?$?+7rra3Pry%=mPFYUVuJ#8p2h)JB_i|8s4L_HjSgg24$n=B$@|=ls zW<8FD)_XyT`y+S{_3bSa>$Ba1wT1RcLcj0pM*6~I(gzKm=@Z`4Bm9L0oV*g-A*i?i z(>0@jNNueeHlctO(2ogBQu-BhUjzg@u0m#79mxFvHJKCSvJa%W(i*s5?I!C+(rSZ{Sjl1bWIs$ ze=hpn!jR3#OK-GAg?gchMFC&eUQJ7~lc;K-z0{|jt(v9f>8^+jaZ$vAkk=LCf@c_s4)JGhvYI-O z5j_3qBu4a;HOJaXPAazTGo0NBIfC5WNJBQfjj)aDFOq(8yOi5>0~iZFpHl+Z2h^Dn z6oOeAynV^IsxqmE3>t3{Ka~bRz;ehIoIwte3f+Dl!_k#aElKPi^+B;`bJSDTIe#yz zk4yl8Umw%Mp?RlG*T7aHK|~uhmSWPUbmuPjCsdbfN|NiXmG>9P6$xbU3Lv~%N4I!Y zs^P&&$YemJDVIL;Z6KQ}XW&3bEUcpn1c5ar6QoN3L9Cc*r%T66fmD9p1W5uyyHTKT z{h$){KJT_dQiJBUT{0Y)P#(MIdSszy5H=hudFeW<*xQPlKMm!P|1q>A50bt#{qT%^ zqOa3t((|n*R1kF3W)Alz)gtseM+8x+%AtKk(enI8w=J>7JIV-8(NG;?VEXf8Kc8b% zxcl%rV57usm$jAMEZOetQQ}g_4>_D}H+r*55GX~g>^>y5HI$UO3(PI!lP_yX4V#eY{2ffhCb%qM0C;&$Jor(sfbjap<>ea{IfK z+PqGq3CLO~$laWixAWL?$C5#gf{#P*o+FIT_;iMC^M^5f-bk!%`5blnfRc=##v+`y z<1#(QV!1~D&Q*p5pdVZB;xGk|m{ZqHcU|p^UjGg!aen)>XMxT&hvAmnE@`J`H)QJB-!C^)~)0|Q|_@U zt)2Z#$5X6hIq(=Y>n`fuh<2u7>j`p_h~zytStGZlVb9YJhWHJZ{d^6q;V{a5u2gW; zrd*6+R@ClyVlommMX($3aNBL$)dtN>hU4tk2W|utT+m!H8se##aQkzsmsDL@>d;Pv z+q2tkiTw-w$BU@UG~yDReuDxbjGDo3AhQ@gInX2+LwHaO1RkL8OS^|>pMU-V5{ywi zWuBhAj)=bX6v#96iWrJ$Vt-KAI8CFTiFRyrbYtcrs)_7DF0EE#eeP{rL&uItY~|LJ zGO+HK-@{0qB*4hhf8W57Iriy4tYA$+t}cq_!M|NaAD;`-*>SG%4`BLH2ZhV8fyvfG#No=dHfZa{aAL7WqT*1JYT&y@jHEduIi#ha0(i;$+@;&$PG00=|Z^BTand4IXyYy-h_- zlPC*a&T8ZiJ0S|D9BKRa{n3uebvP|*3pU$h(Km%`f`kX0l@ig+))yP{&w-&bio!^0 zD?gHeY9z+0-!8pv=bGA_h1$&j>sRf@Z4mp&yCc80XZ?dw#7u0Wkckwz@_g$Rzop`X8TDq3kTP#1>4T51&ZHm~>QYPlf!er4Z8 zh1co@>onq&K3NeIKu^^@4u}>sD(9<#Y$Qg}bK7G}$L)y^ZP=>T^2zBjFz#8TmKOD@|8k<9Gpb<( zZ~%cpyn`Rg!!2m%i`9B$*bn)c{CM#gOox{_japKlW?A1tV||}fect4Ufb%@@`KLa_ zv(V8fMuC!g<3Zy)9Xx^7i|igW+C%wb9g;L7=W%X?3r}&;6Wz5!-4>oq?Ds><>*{oI z(D8-Io@WSC9LBb??t{6WRmHGH5Lvx6X8D2tijSIl0Au*{PoE}~lnZJJ;1I1ixTjx2 z@#JHbTq!7-tv1?lF}hj%I#Qr_hlqm2-3{qm5Lra7qqN>@3aA_a>Twgq&sf2w&n@hZ z#XzJUvtt+NKH(dvryd|ZK_W7b2GHmi*behsZDB05IeX-V;JjT9@Zr*Gn-e(xoXaU9 z&x@6b^y>hGn?WO(Wq*Jl#R?YCX>E(gDnd9hhf1zcIAQ7=oW?gkOD`KO6WEZpet&D< z=uEbrl(-{$nb-ztuxY&VX!Pk08HyDR`D8HcI%DjE-SO_B2|w-E9UJdV24edKDWpzR z`ppym8s2{UZz=QNvekiw;nRj{j}6O)vV@NdY65;Jk!y#y^h>FfeuJ$Tdim}G1TTJ7 zne;C@ByJ}(PB%1T3UUP>bssG^KQskH)QHY*Q&_{RQBkE!x$A>77lk1=Unw0ZdEYeg z9*Q}@2bPnAbfm>TJEwEI``pk%i~~a3^e1sGRKD|>H9V^lio>c|xk(Y9Z}cf1nlxMFL& zn$gf?`b%brL-8gbP29e5XL6t6xeAF2KJSS+4FI3FN=)4-I(gyB=fp#?>Twf-0CKzQ=Uzi*30F`y9ox#N6nT%;dY$WX>s67ENB&P^hJ6Vtg4Sjnv|P6MX=c1Lp| zg^b>EDHtcsK18((PiwS^3rkANP3Q#~yo6kZy&s#MO{-;NBmgMlx1C#`(b1{;_^=Fq zQ+Q%g?iU8C{J^K{sXZ{1kC#jDJ*jEXlEuL}IV_tp3@ z<%wBp%u+~8t9L#YP2*3cQfR1l*{m*mH+ULd`Sq7hUO+RIpS{lK{LfI^wurEWj0Hw# z7-K8R0b<0jq^$RnrzIkVw58PxlZv=CY8#U*>cStn8!~4H$rsB2og| zx`#{S8ZkeN2c05EeN3R)?lf`3GGBpmzR61+ zhzX59zsx-B%hgY7w{X;oc^s?V2wF}*H@I33s$(vOeal3)v3739yEEF3*W4)`Zf#nE zAaS2Dak-|@OSPUpYkiR`7)9!-LgxuPP7$tk>iK}?Ll1OZgDC&2yw$CD=R0)I`dJQ9 z)64k$p`MPy>e8m=Dv?{v1hdLNYpQ(r=-0uegm+g}4sPD({CWsUGY>9W@9}47MS3}xfVjIWWhZ3ZifDJm z4*88!odLI@D&Lm2%1;oR#XekbnP?75Uk$I3<26eTr4;mHV(cY}#iLIyE2DM1M@ zZ1g$Z$K-fK2qk#L*|sA1V4in+=#v>t5Mgt!_4U6x z>nt%Elc4w`sumc?GdR}EH`P(X3DK}$*8cj|zw#U?L*_4Ar7`?`*TtcM^v!!(9}W>w zO|#vndPU^+gicG2nkC04*y6GB*U^8kSiTCd8ET zK_}30D643EB+HLqHa;6gzz(sZ%F_oWLUdo`Wnz*#P=1WdfBrhq?mf=s)4`~?M%4<~&`+7Gha`y+`8u2&F+2 z1)9tqlzQ%Y!Aui?yO~%egqAiO4 zI3h(NzBQb!rCm`6%BFs}xvxE6PK6ih1h+9^3{mNqp=Lk1p0o_SYkD0ipyoM|00ykZWa$Nh0M;PfUx5Loezghp&CTgcdFBbz`Akb5*#vA(vC=+Hp2}2*|2&|LUq38agJrdL4~00!H0J?fZ!Eb2lO*V^hW@ z&EGgY>>B#M7wG-|d-l@O&s7tVp8GRL`}XqkZ!T{NDu9|hFQ}RSD@dH#CF1^73}tqO z7ukr#Jo1epGYAsq_MR1?N&24V=>%rcj?~Y6K9}DMBU7G?gA0#B5eZbvFEpWOe_uk1 zgEhtf&X2t;_`)#2+ejAI&M8fo`!^tQ-a_Vc2!U|GXLh9&;YWX)gLTz0QH3vl%K3PTMC_5dEWc#9 zJ{GLr|FsUh+M}{L;83pQe9`Kvcbsz9b}tjv&bW39eWIg?*UrSxmvd2l@a%C$qcT ze-l?A#r!|FO^cTUxh%M0|L4Zc0{rd&+0siw3OxRQHf-R;NRt2028Ck`;3fah29_%{ zwyFQSf%NV_;9dN`e=Yb!jOhRKJn}F8|M)b$wWDKWfOpbVS6|=S352O2DfRE5{d)=P z9veMqfXG66u@OS@&zC_#NlCj7Y|0P(pF`2>T14Zx)*&?|bEX*)5%HZ$bY68-P)H(sP71X6Ap6?^V`F1N0hdmIXR&aIO2KUcxYv=$s;6}KZt%kh z;JWzmvsjf~Ol)q^BcVXr*ZS8NJsL?j7IEk0h`YN$AUDtGzq!uA$@vmRT2fxV=e_wW z)FH|t+F>MO`@b`X;N|{?zW`$Y*Y<+%dQkb9$*kNJJtV&IeDs;`C};L3fG=PS5DMz& zq&l=!hg&Q(O})R#k0XrC{a%)bs5UvwVgpW)8*|{LByMb2ZspnJTq5_c_|B3Uq^$RY z2z^mjw;dH8B!{QZHrJz%=5ozmV}(o(=R)dW<2M4%qgA(nprV$WkMG%Kg$bb2AOY;f zAi&)*@2ziIawk3Xlyk}$s+BJZ0!YC>tvCNx)(%bNik<)otG$UcfPg#`NzSXsMgOb= z;C-95$UNcT_#IFn3L!r1x^>|Lw~|5%yr}>}Ftd&8x}s9RUUiGtCJ$~o8->k+nQy_v zZJukF0^misSicmK|0rw|HC2h(OFNt3~weDJyrDPyF)@I@+3$Al%Gh~SIu(mf@8oel3iFxc?7hs z|Gr`#M*#>B`T(Z>2Z`qR`?HF_vbg2M4>`%MYaJ#M#3G4+Jw=&7M4X|?X^9M=tCKSY z9{186<*R9G4gg1`+WFvfyny117avX%DW5(%scCCB@2|Emd}bO_@BTvgi;SGy#_w_o z!p{RTyfNoAj~~0^=Sh8gdUnPe6e2^-nZqX=aM|Rv`h9K_wZ>PG@-6W8pyg)m+5|9z zY~BL!rnfGwo7m%@6vFueF89A47YJr$WKhKOwDJK=C%3En(WW0+?+gG6SXNaPU+3|W z!RS;)qflb&D-?X{=w6Ip77+bDuNIxmTLGjBRkha*;{uHgKu@Xf;gaBJ`~lVQ+FV;j zTJ%i4j1?#0c_*vArjq{6+sl#)TS{1zfXhUK+;kv zJNy-)6ntR`G3CR!6xEhvM{)G#%@j{iHvlQs01`|5g&_}Z+dyD%e&JofI@q{u-DkyF zd%E7LA3MkbAkWWE|9~I<{l1@F0S|BAMNE%>)ra||W6PM_@iWVNAY<(I7wfZt&tIKB z?SPjwuym~fJc$3F5rVV*Ic)sv+lQrP6Ca;uPA;xtkFoRodd_(MgujD(S=BKyxWFf+ z?qYx5)OD^J2jG|v89Evr13IFT)zsfzJ=1{jrWH;(Ixu?=z#hHg!F%)7dbQ@V2Ok@3 zN6A*n{)_|-Ms!*L0+8^Pt(E@yI6(CH0#69+|EHAlt?~cj?k%I@TDq>$B)Gdf1PK-h zE{#iqOK_)y1$T$w4j}{!?(PI>+!}&AjWim9dvLq^ocBKCea8KN$N0wgySw+^UA1=A zsyWx1^8=tYc9!$}fTFIhzO+3fLssYE&1*#K-2rgdZL1Ew4mMhnEGJ9?*WB>ed&7C? znsD-b^Z@4*T)+6eYTt`70bAeA-Y%`MaoASc$PDnR)yx1^NYd^F}m{19=#5~_Vz2Z)#`fXdxR5f>}EN>P;@F(Xe;0!wm2*F$ux@b3FO zr=ktD+H?mBx2VEI_mfqlwusKFC0od*M4kNgFK8TvEr|l)oDSZ+bjhH-iAKU8wkC-J zT&(O>?P$&DT!G5uk1lgXBKU*ZT=uFzct4bYwP6me+r&mJ{y>Ff%by>%jd9Z!qoHw3 zjWidOhUn<%l-VJ*-9;5MaRRHQ7#e>t>=2!iFl?71_xK0gCAk*60=pb;%$J^k_ovaf zvqrbcF5Q6_{4@bY3#bEOG}TB8z#CFQfQG^6cnuED zf9WFLgj_2nXxjaP!!olcn-BdRlh;!`5~lc~k3=6&!U-yMYL)lc4?dozmIyLbtgWNw zwRyST`@9>!%_!!^n|EQav0^~rcAWs1wSU$laa(kAVhzFoyR+Q2XJEZ7T*o);)s|@> z8V}g;2ypC;rjWL6=(WUlvm`I9HAhOivRZTMfs({OZw4sMQ1gUe&G_#!YI&==UpWiBv$a zZm1kW3Y8KKpv27G>5Ms(&2YbA-t<1BHh2IkqP+?TG1koNlUZfvjeOtYe)>_h2H4}w zg3$ZIu_|5*3pJC~B!GPK^73{8U$joK6yH^3ZqtFJtD$VErKTDuPn_PekP5#{n^a>> zyA~_5KIESA6qA+fz~}p%CZu_stRA;KsCybbKLJ{i z7wBz(JNbzF=yT_of|)Lu8&*e*#g;f~;4|klQ?3_(g;nHK$ef|0NmZCv9hR1%?Dwx? z6>>tprtj)m10FLIH(@_1(m0%W(LJ~uoGvWI$*JJ?O1h@CB%@@6AMDai9jT3kc3CYr zD<5R#9AfFkx{i#sl*ObySc*myasx{m-|IIG$#hQ?E~s+ zFJ1qgBIFmu*gz6j*Tg_zc}h7n;Wi}kim7n|(4Z%^oote}PO?N4BsOIqVyUYw64mgeQEWrb3Zg>PLV!DfFKP-#lq~ekZ zbT~oPuYzGIpiut)dEX|l3@ockN^UpM0PRt?P8z3TlLd#tk)FzQfVWALgN~?x!ImB# zPspxXTKzYu%b=DiuDsfc5Pnm9bxceFYQXAh2yI%e^A!K1R`bKAV~fh!z+Dl;u1DIp zZ<|_FUoEIa$VuHI_m)E!2C%Ww#g;6Ulwv*9AAk(RJ%Rh;bxOgg6{%O~!jAo|O-#vx z^E;Q}NFFKaTF)8NVg&{+3{LG*rJLzDGHFvkk_D4aPo1(PFT8lsRj;xnwbovSabO(&a+$|}udc7HUCj1lwU?UfindngYX_#=v$-wn zNyzEqnQ%)M$G5*of4rz5vglG;xgC=oX=L?}y+;qLp*Gn~GWT^6%XTxYX2>U{U+Twh zZaa=zl(ffnPbcmB=C4{Nw->}r%#c#~yHmssslI4rtAe{~gbrPqdWvFyYGqA1B#QD1M zSZ?KThzisMEm$srj*_TJf!yY9spHK;nu%aJYJGAxrj2(-hrvl4z=ssqXCQ!Sui z*V*!fsx8M`w5d_esct)%^LwRT{lP6@55^T>HekoMai(XMtBBdwvWR{#jH8dM!V7CN zb@VjU{m@tit6vE6=p~l9nw?<`0j*Lx+MMLmqh@!vr+IU*S-R;Ju3PymLEJ0qIX8edftU`_S+eCZG=<&eN>0Rbf_20$8FaIDXx6d z7p$-mas@A#xKQDRx_JdRjT~$bwCmhMjGD6#2RTif%mv?l12WisVnq2` zHFL%UgeLe;F2UgI@%evr)r5#5+>*^O7%`}?$d%H#Bq=Dd;^3Q>F<}f0`9UtC6uxM? z$bHjr+MC&Ln(3;8C0#$w_a9I zZL^3GGs-b+JzKi+aC#uaV3#b#E?zGY1T&Vh2Io}Gv#VM&xDl9d4|{BQ0nd_R+q9ir zW46V_X24zodokpaHaQYz!Tg^Q_cgN0{4J*1FGBs@(4VCl;M1#d&ssW1k!|G4{NbBOU?PqnQmVq1DRZXge8QG`tCu zL{)u|SmY@_nS<#QG5+eI{LB8Z4yBcZoe~j5hJ}`3ELIH6~l8Ts;5?=aS&E4RiQql?{nE1ZcQm$;3CX8-{B-1@sMgh)P` zB1MrML(jQ@<XFw`%PCFQ>-H-$w6d)Zd+_9F3(4z80!_j>ppW+Iz>_egk<6Iqz+I`r?)SQ2 z9pFu-x}Y`os3&i^B{Cw?AU5J}-_a=ujg^Ykl&ljQhWe&NQ93GTfU}k|&S#$fO&uA3 zF)u2eWV0)D7_9nV8^z!&`S1sFKHMkk7}u5d8`uOgGO@Gb>3x@&zCXVJ+$igR|M{bB zBhDDg&U&V=-(+V4h^NGF0|mm(4^Q_ecLEqBoU`|*K(Ssnak9in;NX@YfEjEL>VyT! zTHvP?78aiS8*-F)HE zi;bFEA|dh$rfFl29y{cAHOjwvSuE9(zmnnnHV0P z#&{MZI;vzH0Xi~Ri+q>EmTy7VJk8wxRYq;|E?aQ|pM5v;(K8g9T$k;e_Xg&vZLFF+ ztT+`Uw3a*pv8C-DQ(?;=I$`BaFPGQiD21lKefy@-IcDgURnE1rqZwA9LUFYqBl7#_ z1yJSeUBdYGZOydvCn2Gu*#Ci`R{pU2{}_oTVskSV1B|}1Je2O&9Su1|sV|(g-qZsn zRihhqt-p763_da{qzbipo_Mq*w6-)D=`{6(Z8Oh^odD8cHpE&{vpawI1ebC!YO=0R zp%jj~A1#!ZzZKkD++G5~CBn`IH%dL)90Q%goihG&l0WMZRuK4OzGR}Vo@lYzAGJh1 z3K{>wXZ!c0HEpnAVo+04lWqI*k4Dz{$fAc8Mor9R2cV6!d%xN0g;Pnglma&NGI@x< z?*3aDnr{JkjHU>z`Drfy+31V73;-Nf>qO6o7`EMI1L`viIAGKhN#`@_w>!XcFcedj zb=J!L7GoXxQiCmYhcI5;j?wG@!2lEXia9fFQuaHCtepi9Tr&C0hQ_^)NAeWQ-0D9F zF4)$}Ek}#~de+7EFAC}9*Xpn)*OKqwzgs&)ISr990RZz~^mb!bx_{9*{Hfue5PNF+ z+g7x84g`$Jk+1KZmX4OlyM+H>w9HKOVVHAoiyR!;B%IH|g|z++=EspQ;&j^=GK_d{{YAT7vaPI4Cnu!Xg4A< zA|feZbhnN{x%eHHPJ2TPOn9kx|2Tl&*EPS{6Z$XAy-_ep8yAR%YP%1IO1OR@z}II* z{|ybndPd-zq2%53utHpd&>xb(=bx$ny^@{1VuH{X0M(h>;Ung;PKGP_*318Yf8M~u zG5!hxA@Te7(ENOQa04yss>E_0)>O(N)|erQ2tnvR-M`Gizjx$3Vh4fX8ft1^Mn{!C zefqQxKttP5%R1e^CA6r?+*ogTc?ki_R9QnqN^>w6kD59@H<#Alsj#vV6TnDuMFz}v-XpSX)lZfJfMa56YKgdwUlTGDlnHf^D=y~Lkz7+o29chD;gUIS zoR>@LK2JF+3oUr(zvv7BkK$B7RUX4Ya`$>DEO413nKYf2vTuF}6+r&*C@CvRp6t7D z?B4^o6aAli{yQ-3bvyyczkmN`p{Q>PJ)PlY;bxa~Qbhu5#MomL+}hY`0wcQ)`rkKi zD=N#z87gjv1@I#$+*o4|%f!pQioiw7TjnBJssuTibe_vAUT!YQQu0QsC0zSo?CnaU|Ex8o)c^I5hsR%)!ETTYBoHM_ zg&XOX@a>~(wL&p~*+X2T{ojDN^016}dTAX~^fO|OA_cm7w5Q!ak{``>(&&nQ4>b@nCx8!C?f6>Pp4-?INtVx&<2!Qz1= zAzwCz7|JAxTR!~sc#lU!6gD}jhKQ`cECw(?Y9Bv_k~cI=_$^%npTar3)p98XQf&d#8OgHu^pft zz=W>_u(UI+(~aebM^n2UOi~q#0w}IEmh(4u17nE{$AI7ugIKAb2^@JnFRdvF!sp$4FQ@AQ zUYqwKsI&2+N(O0QW2~ClT1iSu3cn==_&BlOEyR}Iw=8;flts(Uw7CxUp_rthU!cmf zBpQOK%mPo^HhutZ&22NC0R7>ZS8Ki8#Nl&w7$)X_mkW5?`(%_v3c9k7zn_oyhFvcc zu>6{M*W@K<`d;uyoDZS9kBPb68D{a+ucW}ay`sj(S=c@q;V$4xzUZvH%v>P|o)-D0 z7diI98}=hJf$Ghz;8)|w75YmR(f27=ZJ`l}`O+Pze{HGbW-{1(t0>I;7*LO$UyoPL zGokiIg|Qkbq_9f@MzwkCkDgzj=M`BVjx}{%TwgD|xcJeMPVmcv=!25 z%jI3UBN-gFsOm*+6%`x&HOyLu|7;8C+G%4MW&?_a>Co#j{`PjYRuz^aro4N5qIzVk zui;}zEQVm6%@hWrB_EQCfj}grr!SN*7XC#1ef{Tc#Odw14BQ~4Mke`f9_@0H{uk90CiBGr z8y2GSc-0khy|bLV>5rqnp418-H?P0fws1AltWtMy?uN}j8WaRn+@WH+RFdd%+5c!j zn=$?o{-}6eP&rhm9wrlNddLHI9+%!BHDGo1+A^zrb_FTKP&I$==Z}@`V!NlKhc=VZ zVNCn8U6~ymRwc_sevq+442Sb!O43O}iLXz^@SB z>O8!>$V#dxVUGP=v-|MUwN5c|kI$tGSs>S>l}5u+AJ-JfORzK+KAekO`h+q%cb)bz zmR|)P*)h^?n^lSbQX(dRW76NwvnIO(G&qJO6IGWluqwAIVDd&<qFRi%V(;A4CJV%wB*63YCt)CNmHX*}Dgb z+aCL7c=)>M!pz$TkUBL!rc53Q*!o<8F{l-??`)#3M)%>&1j26TlEy11?#D|clUuOZwF44G`D=NS1 z0^(Q5{prUwJc`z5&T=*<^-tbWC3V+I@b0go!Y2;m@jPQCJ|3}bWeiioJqZK5E(KEj zApK<$7}yw4U+)t#+hjdH8zQEacQ0jIWuYn2YL%FG6SRg;T_bRG636!|DWwx_R(sSa zLcz&P9JX8OaTBz>)6PIcoG(@L*kbrLLF(2Jl;OR|*W$4Hw(pd=_|Bi&@jq7`i)*? zk*ue%m~H=VCZ8=;Jx(O=S1Adec2~jb7@iy0ZzGJvt((;GB*W$w5T%)sG>;cxju9-7 zyK%F56<{1V3v)c)0f^*P>4o1sXU*JuXQssk&2S7CN@MHzaR1&pk?s zh(q;6-qqe)cb>(8g#{w}g{U^;eiBfK&8+bko6FZ<`2GJ{L z!dlnKc<(8D^O#k*)-4mhSaL;NHDF5+|*wQeVeczGuvjMJ}lN-vrW+mv+Z<+&OLG^&vsgqDB(sug**qewsOJS&zfudf3?5_1AK2B%4Bx^Wm4W zOVUL|L@5p3X98=Oq*|t!Z^jXvZEVw+?|jRGBtxk?Bk`JJyQ#X?f?D{Uwf7Fe==JDa zCy(AN`*G0bhSq5$8Ce~)qMTPGy;I|65 z5d6VB8t6 z$J*rWv~C4v&KZtv5?t(n6N_O=@b0&_2a_0{6=lZUeeWu%&xKm7k0~Li|F*`;-BDGG z?>YQtqVRbg;#l2xg98P3DnuJ> z_hG;s90Q!k*V$kQ@BD5e6-FlW`X+&*5V!r=4-rZNMnj@ORTE`pWj6r77|X$oleslE zb#d-N_U^Y*LblD(YARjl%qiGBQYw~S$^Wx}oI7eqweNWRWwsu>?0cgdd<7R)pJkvl z0zCX({HJYMGq3gJi&PV_Rb7H|blVKy8FaN>$pRh`(^pw6Jkg?KiJ4xt-HxFZl?J3;BirTC{U0$=9(WbL?PxUnC<;X4?KoGKoo%t5T=g` z3JSgDT5Q~aVmOuDn&#a#&6j@&Jq@R?1m2^IKm&bwh}li>snf&?Gc#yZe4s!a^I{Dx*4ZP)#Fj`^61T4q!<2cqq(9qD^o{W9LnCnkkH*p{(l*b>k zTTeV05;nYsVrCgC2Sfqj2C1O4o5eBimgEx=vL~!>F40c6&C6+P5VuSE3$4%E0-b^U>1gjFa5Lw2$kS@H&j%(wSTjp8LKY&&5zAMMj$Fd7U z&LYp%?$CD#$(&4fg|hL~nhhg!piB2kO}4AL@}BJYI1}}UMsZZ@W%jKta>ljs&Y#Ra zC?8q=q&l{0J2f_n+8Jtj&wI2uyWu!iB)djj3UBwgZAFsA^}}LoAw+A!U|Yk~RWDJY zYWzH>aGhJH@wA<4lIOeyg}#M11U`!$uC(^)iU^syN!{;g%?)TuZT-mI_-cShN$e2G zC1O?_jd}pNGe{8E47||f@V>XqU!Ron&#bT4pnCD?`WWN`r-0ddta|T!*!82q&3zI& zz-nM-=px+nj^ARoQyw0KNDD!9MsM*Wv*QYWcH5SQu7>ixDW|rf>_$5}kj;Jkz-z;Y z5Y&QiSt#kog3O!X!8$6qvr-(oqF_FnZRu=gU7nJZxV7N=2N6AC%vAt9&`;!psimHB zv09l%%-wQ0HZpl1yqb)NEISfhL%cr0{3Ii(AEfFtu5@Pv$5{w;2EBrZKDhM<@u8gF zCg(6%lpIvs6WnL>L>K(*sv78DX4?OHHJ)2)JzIq5V=_%~6y7M^7muZ0QH^gdXG@ZU z<9mwjC>YDp&}@2HI5+gcd6jkKdhsrHzQucUEYg;;#J~f7=F*kw{HYHQTt$zmIzc^z zhZAe+ut%7^a0PdH$Y>*;|DE;AtL$1)#rKLut>)?GGl!CqS>SOwEeL=u6Auq5fv%A} z*^58WlzrFytIRC_tHpl~IB|T}0KThq-CkJKigs3Q`WqTS6F>^EaokP1LW#cPq+#cG z2aa6x@idIBWrFW6EIv786j3Xc5u47RSv>HDk~Np9K2jM`-)7y3l;7g^=bXK|q8y*U znDQ1O{(2I$`2Zll3xUrWnV2A}-)(che4JmCA2u7VbZ|Nxt*HSaOM2t1~sJ{s6Gk#uwUh6abnf=*9h<^-TxU+c9 z>%-tX;}d@zWh>T#tP|kF*h?YkxG}tx6Gn4#i)R~Pp!o)DwH>%VhcE^9!vV*fnDWX* zvpa6^=w1&B7aqBq)Cx23&=JIlnfz|rsT$ekY}p<(;M$*3w-zr1nePvNw<->uh;aSk z3<-9A7Qps=@-8|4;EJ4cvRdTJ{akR%+E;fq)M~II`SRC+(_aJEIudz9&&HgiO%=7h z(CT+D<-2|wm9vckcn>BsJPYJoT|JAoItADcVv=%A7AOc0m|0pDi(1j(Qfi^N-^I#X z+ZjoR?)i8knORb~TC!6{6#Gtnclw|o9cwSXH72lDs_~V$^CIRg_0E(CiD*cN<_0x? zwX0z|1D+fm9tGZ^4_dQ!<*#ySA%_9VotyoN(BWUb&`?oO=L8jl$Q_|&iKeRh}DmLe_|IJqpZZoeixxPkOTe-sW;8@n$(=|&pYq>hI#akO_ zc9rF#-(0JCI&v?)oqa6RJ$sD)9EZ2u>cs5v)G)K>TCl#ym5<=?v|$N1`19d}+IL$& zdmi6YwSz(#BA|}K-B}ybq3w1ss^mr!tS$Ib-DX%1f}hDSF)G4B%uaj_T9>kb{TQaD z%Wl~|6SDgB$>;YrZ3{DCt2BG$-->Ve(W01EGtfl~gSTLz^1-pJ|SnUfZ zqlo1g%oW`5;x}msoC`QKZeRZK3iN4DKrykYYrn&iP(G$p_jf%>y=)h8%fZG`0cRFc!s?n0leL+}(?NwbNl)7B2_HXk%fDuoJ`R z%?}CrcH6IY-(gpL3yZ^@t9(yVgdiUfVR158`^jEA;=(?xuCh{>D4(K^=hCr|gmN-D z(4f^x5~!NNv7;|15%iMaEh?`zyQ3F6MjIWtbn?Tk03TOM{F#y*BQ zMJ@OC6Oh8~e|;;0wK>uht3FfIbUY!i`aUDr!JCZW7j;{%U9A@Y)q^7r9nA5IPOi|- zEA0QF`pL=)Jbx$zYjGwBcyRuT^%UuG><&f|Za1xc3bLeo-iI zpV!#Z{?1IiS5TN=!$al+YQa%FfI8|ANddhJYkn08_R&=ZK-#xo;dlxtXlxawn)H<9 z_{U2}XbIz&V;O81G})C67Pf|*gq$oYp-5etzJFXQfd}g=x3NuGD9PAG^Il$an?buc zP|Z)$iLEVA>p!jMsRNa2SE8!xZDK&-?NpJX4mf6=ZU5az|4gf(PEv{A8Oej^UFBah z4JxH~+iVl|7xJ|kuYVB`UAa{WehtwO*k1QGI-q45Z7tE#mPGTQc8A|X-=K&8jNzGA zQHhT^#ALyexEpt4zxN{xXy1}=51hJByMprmE)=4^Ak-2;FQBFjV-ttq2mPcpF`t^E ze|T8SsRP?=znrYQteD#W-LLWv$IATZ#8&;|y57m+6iB}AbSBJaRF~0>$?B-uHDmoVHkUF8`r+p=xF^YtrXyQj`Fe$LMk%a|f}CC_953v2BZ8DB~4v%U~H=!AIeC{b{_LoDftlgXa z{$Bol=7yp>R)+Tzgze4Fe3sO(7!I2bGbUk3(oujCS60!VLG}&Z3w1 zEit>|dv^oD)?lK8tTvC0=CvmYJ41Gd_6;9QYFKj z5U4wGR3QM-0-oArgj|Pi+Fczk>b5yh0j>Nc{;*q5Gs%|@kTW?$_iZoGQ;fre=<2B6 z=e3^)PFIn5EmuD(@Mvfvasr;ne*Kz9SCBHNw;auk(yCaKW zvO#4?u!@xO?&c1TH#E!NjBsT0Slae@ccc|ncr}t~;c4rTLGJf+LWAxVl?+YcmIy43`$>6 zOIcG5rq4}P$#ZXAk_{=G!GC8|-8D~7VIP?>S}rb(H14{fAT6G#KE${44nh~NqdR?K zlSH(iTBa>($OdD7lELJN(6t=B(PGg1YDFsPF0vAnGDG5K_{o6~{WJA0G6o{=x3MU1oT`IF015OdZo-qMsgnG1wUwYhRBS(>W>I{N(*! zT*EV8p;}giyG+2uMrS@I02l(-z1enr^mNxvuBWM~)TFl^qkL%cWWaZ6pcJTvb*HGK z=VQIu^Rn4vG8A_gqE7F^ldm_LnAk*z6uBSIq9LGQO28rRQ$PnxCZAmzzIwx7i3h;3 zlz{<6wUg}{AHP3e_<KJ?gJbS zecpu)$eY}Mx=J?sCZtk1jV2pyH2tgZE)T%tnXPxaN)&pZyD-MZRZU0+=mWbf4zwTn z2BaG-l@m+)&5eOV&PJQK%uK^aioClt(D!WI(#QRBV`|i7w5o-d&PIcDETK~r%0b1Q zsA}e;yB`{&`6^f%AOb0GDA)(mjJ>Vbh928>3D+_$^T31gP~x72zYh3eH}5CNg?5p+ zy}@(s*)YLoEi@iaJPZ-ONSWBA4H0H~jB87}i7_`tBMP~^53v_e6tOlT5hHo|lE+KU z%~4H(IRc^_xKaAq1SeCR>#V~Y3XRj_UC*-_usEHWh?P;z!*WW8{^s38wY-w%V25gN?~$ts`niFnl5=&5D4{7S*M2#f~-a=%t|%zk}%*W@errHJsQMN?J$ zaP^3;NbG>OQS@^Yr9T%$M^H|Xk_x+Rv-_5i9f`XB-_3$`~OTN)80h)@1 z%ATg9-SL@BDB5v_HS)OGiCRVSkgO9!4;Gk#>$`{c=6$L+;`TF%Lb7 z?O`Jcy%&}pVi*-qh8A{Z)S0h+Z$N$Sq(1VMw>DT!6U~qd!F;Y}wQH~KV(sJAZLgLf z0nEnmU!F*6ypQ4HI%`#^sK z)O;>XXEPxYd^V8Yce-86bGEbVTDKP+!{xdn)KVoc-xwP=@4AB9^u!_dHMp&~YM`k= zYZs)EA5P?AaCHQapk@5Ql9>c@;IJ*utw0;pTu$y!n&jOutKELUQJ^Ll_>Bs$4o!kie z-P`bc=*;$oF7#a0kxl)694!5;Ye$Leup-WcGvJi`bU&t-&**%-5^>v#a>D#m5bH7Y z0%g?b~nQ%Qbw^VOi-^{X29EGMe<0lfBOhJ9mf3u1^NWd}|4eqZ`=;K;R$D5}qH zUPN|Lg`%P19LfSWWx$Ms@|1_EfgANIng844n7n7QXu4)hNnJSC>k~yeNuQS$7|^$e zYK{ySrcCq7dJ!p)2KmM#K2Q>E(#>|8g<@2lw-zI72CP(r8g@O|+rC`uOD|F5oJ`&b z*55{En=Yyo4lHqe-uhdivXF?g@Z7nouhS4D6{d-l=`mm<%-#-)n5XG&Bq^p7Z>TpQ zU{<8Qgs~8A6PYbGkHl6Hnc>V&iiFc2Nstr!L*C0ryCK5qQQ+@Tu|#-zn?6zz?`KNe z&onASQ-+;_|8O$2wfJ>rz7`Srf%ZScy83xwmVwM03Sht~^o0y9y%HaTxuQsIUk2gedN z2R^@4%HXF$K*pB1zdHKO(t~u`%vhh zRj$=aiXW(C2?OP~$*4F963b2Y-*wq0JFI|CF6ol^EVU99drF~ir8`Uc%Y`-28A<_> zi?21EcN#g%RQ5}aQ^&U1y|-flx7$|#v?szgDLZW|Yz>*gF+^e&K&Rd>U03X!t*>;_ zK)n$PSaGbI_vv!Mgu=EuDk^FU%q~PFtwj=@1N0P8$X5#YI9HkdQk8LDB2ISv0kWD` ztsvFR0j!qA<1T5F1w8HaBa4>%60=N%Xh?2ETnzv z=viN!*%^eEw&#UEG|-oqtA;9$+jhB?lBk=Ve@5>kTb?-`UXi}BbtNwrzKLu7>!6QuX?xpr zHZ?z9xhg33V<-01^zO^Pg@zzv1TsUXjp%sbp1ye9tyix(Na!br=vCyheD1`YzgtZrh|p8hIDNBWHR~7*IdHm&UoRXad>6msk)moC zKgy5{ProsQ#P3HVAAu-d3}>^-4+@win7jF}g_&-*xd8iib^Hp;jX=@JpM2{nOhg$O z`osJTJP&vQs@+W7Ped>d^JD;pr{w8S1m3I}DM8v<7;7v>y?WCwUvfy6A*f{L3ea+c zD{9esRC1M;gu*~@GU?f5CBw6{UsBYjwt(o+S%Z0L_*E(%y&J9B6%{kixpF4!uI`lZg z{b06CZE<9Dv^y9n=5(tths*U3JJoE#ha@P3mK5}vFrS{`?4Ty;Y;Iv~m)pMYV_2OQ zo9)w}u`tAFQKQDesGn_v7!NS&+sTe8?3X!kf8Zmk&9N&W4DA3UCFW?zy9;9%e`LSa(* z5UNpl{C<1^q?s8Qw|q;v(i2r9u6{3eaUIZpJ3;5y#`+;<`DcV{?SXYCzD8Jb3aISX z2R%r3UsSwCwWlp6o+Yn=CWk{+=0}WGw`(g#K<{q!(SdvmCAj%hAaOl>G-0%LAGtZY zZYhV#&xG(Cq!A|+YUB(mmj8rPfU1NzE;Rj|g5{1mf}HeON0H<^af0YpyY+M1cSSw@nmrZj;&={z>bt!2BGfK4?trwc2VXc}8+n$4;YA6{ zRR%hbqVIE7JGhSXG5)SG70AW)r%lag4^x96TY%$DH^_4>tfCt7s>A29AK`kfLgJAmK$lI*2)~vIr6$cdQMI_a+L>PW-kAxnldLc-#7=rH zQR+JXV|g`eK%87TRqnC5j-_J3-eB7yp3u76hU(l|I|1BBWsSig7h8Iktq>Toxs%?7 zv$;ry(kM{?Gq*_ZAe#%it0*ZCT?3US;#tm*R($4*rKnF2qWqAS)zJpA_9o!&(u71*MfBn)%}0uYVU6{%URO z)uY68=&e<$Ky4LxMcRtHnu`ujuK~wDoZiy-v%6c7;GO*4LMIp5k);i5yc1YaA}CAo z8saHUHO1K^3Q7Ygv@HDUW76fKD`yND7emG2$TZZyowWiE&u1Q)&*!(OsGbkSF8Hpe z2Mk@Gucdv-J?$#tx0K2nSxq8X6#^ILXdfMhqaOPFd4^U zc%1v*vQYeL9^FjIvuz?e{ z9_7d{%|1w7_oiA>_18bH%C7w3De^Tml;PTclV`zsIoFFWDqXeaX{p&R+{8$KNZy|T z4bP8{FoU@!ld_suts%I~cDm)c$-6R+{~+UZCuHDhF0s{VZxr3B{>nrsq^`1w=|nk~ z%9eB*?2|3T36-6ao$}LeQnap3RHLf3c4P`z{&YE78wPHNW&HZkY}9k+j%C#H>R9y0 z2|uQIYo|${i*J2Ib$XdYHk~gU%FlAqx%qR1MF-}Icyz4()}w@+wszHiG9xt2t3p~E zd3+6=!u>PF@lXVe#39)G*Z?Qq>c8yGQoBNU*@->@ECQc%&U_eMD`JTx7t=B z1C@oWl z7Aww-jw0KyemZ#(|tX9wKbh(14Dq=^S+>u~0} zS!`44t$yd0nnT5mk^UILhKmpX2yz4$Jn^!YJ}mF@H3U**WssPC%%Oa2OJtDGasy?p zc2nn?2o#$qQ3yyFungVtSA|ZFz^~_wV*F!Z|9TIkAtG3BD6i}CsLFP{J&h6spGMa> z+@L$=-1CdI+#vY*A1vHFF0R$xUC*iP&=#xDob8lp$u?$rS zPIA3wM_dT>>*&jF{?hwG$;aVLT*#o_M;C4!EdA;ov=hj=4(m|!>(NpUMDBHt5;+)Nwb=SArC!@}C zyKi2ejp6AwSJhb#d`rSqY;s%)e^&OeqL$F_sguiD&!xq*oi0389-OBkqD!8(`9(Urcn}%pxh30u@$mXJY~;%j|2WoBb#M z$f?*$Hu>N4gIZfE&Y0`1)ild-OpANRoG*0 z7t7ceZ+gSE5%lve7PI2`Ch|z5ZGP)E;y0DM#8TAcLAY*)=hNrWEU0>3MS{Eu2ZEn_ z>zQsEdr%|YvhjQ{3j~`EP4ZmK-V7t3e*>xmQy1?^^c9UlJe=dEBsn?@E&N_1^Hpd* z-JmNf)`^4U7po3l4H;ll2^WThPcl{ue=1#bgtdHVTR`JOktG>STI8HV`_|i@K1FuY z_5YCfmO*iKZMbGgAh^4GkjC8!p5PGNp@VyHcejw>?hxF)AvnR^o#5^cv-#wlcV?=l zYJQwkH8np{sY=nE?zPw2?DeeYzOEN2{85*;9nGV^RtB54?V<#I}eqp2kz;V zIxIp^Ux&@?ZKai0SJX5$$mad*Dl-s<`6GW zJBPv~92(Xj+ikhi8cewut%70h_{GY@T|4q+)isbWiMN%i0=mRoBPb4n-NKMug^VZs zrh_$Jy9HlsHPJ5I_u90sLQ#p6w|X5e!LtrJ89p8)<@oN<%}}A6^Lj-gew=9$)k*MX zZapd_Hhwp~qMnlWT!%PhR=H4uIOK-;7QOfz&XfnxGUN5uVo?IbsG95&d!l3hf{Y-8 z5SwOt(23^TTeqRa-03FvNi`G+UNS&yeLr3!Dmec9DujKx9;JGRo|5v5`U1Q#e$aTm zR0MFVGrms(0ta|5<3ab6frckd!d;cPSVWd`f9t!usfZ7oyp zKGciEoYTgxpualiNzw~-lG14@yU=hf@i0CXDQ%cfH0sa;a^LHVycr`T2#5;3mgpns zy;Wc870Db9GeNOE&Ve59xy#AqcaNL)eGlFcwN_+XgJRCAw+gT5Y$V)cdX>yDc5a_& zcCg$WUyyUe#(sJCR4th|^=Pb{v{Kq8k8|>>ow|P0Nm+Qy05!+$L%Krzz?2}bqeUkx zbkf$hOM+)N`6k4V<(hdlCd$W_V-E`|@neZ-JLtX%3F${AFVutDF{lDf73Te0%}ckfjL+H>mQp!`MNGXZYZcIe zbgX0DXP2m|6luxDatn;0Ac?S4%;e?d2ark<9$cQhYb^=Yk&J7tmLa+OWAYz9>X9WF zm_L91bPQ?M40LVKP*WRjbd$z@|6VjpS8pc)(2#SRaxkwlqq6364g~u9$>7vo9eFnd=vTOeAb;-@RHO>67MZT7g{Z1`t4e<#k0d(vV_h z9phkL*e)t8o_I-U8ovM5@Z!61DAos^{LC@Z)2^D{r(A$@knpXZ2(IUHy6*C%Kq0e$ zkV#%uAbp6NjRUTb+hxg88T(eACozR)`U3*KX}M#)q7^GoFi1n;;hRJbNuhCP<-ON5 zq=|5_TL#i7rF#%g>@lI!axwqSdi6ejfG;>yzjd~02gXIXLChM-QoHJ>-$iAUN60dP z#GW3FsiKDbYKBcIZ}85^1Pg$>;xz&rWn{2*5I<>mtMK&>0BtY2N_WwC?!tVJx6Ndv zxb~7NTw#%d_7%Ck)&*0pS4M8ED`l-V1 z=nVvsuIhpAh`klA>uR9!3r;X*zV+@UE`m==HiZM8TKe#edne6-Yed*KGQ;40SEA2=0rp!4O5{5%bN_=_GO*p7X$yKnxJe zO`C$4M4H?JC5xgh)xtp_8&!b&IyaaCU{SfEw+X|QXDD1wI0TKuB;i>ZdsXU<=w3Jy z!3Rw+P)XHn2(VwuuD0YCJFgm zA~vhXIxm8?XYwdelZ(7o>fts1HwJ^Wi2J(@uX&z*qfQ%T1;LjLISbxH-Kn3>G&_5H zZYcL>K0VZ2hJ5)-gdG- zGMz2*g4VtomSWfCuZ*Z#`MTN>rdu(3Sac zy%_^}ID5!=y0yN6dTc&;<2~>8M)*W=us-0{twgJys57`5DX!YY@Heg2F+$DhDT^R4 z2TfLi7wP9uAKTl!s_Z8}EQ90Y;v&_`v=dfA!XRyvHzP!B8ZCV19?c(Y78>yZF1h5l zZwLU%EXQL`RMg*o?RVAQN2_eht$j|*@p?H+FZt(Sn{ceR=H_blGnIz#h?Ew&ZI%MN z^Qf>E8}+{Zu*isjIKYR6u_X+{+U0BjkiU)kMVmevpp!^4ia@=@lNUW}al6vc^p~+l zd?J8z#+ND)GKSgf*LmWSjyNx?w+d%|t!z9@9%($5GklycbMorBh8xoLDLsq47OqEzl5tuR;JRou;V8~9rWjoDxK+^o4VEKfs01m@0Ik^qG5T7 zl66z))TqWd`v>28vSwK2GgU)FoNCDT@giN#MhgdjI1+Ff7Cm2IQ8)(nRP;9VF+LHh zA*{Ajv!W(T|84e1XXQ0ym&m4c9YyjviI!x|E{=2|ie#d0z>~RdLamjspY0rBj0GRk z9$K9&qZ1Q~^I+9+4%_3J5~v2he0yP@tIPQo_iKpohW=;CE!6=> zMF1SX@I*?^tOR7tBI{v*E|);>_B;qh?9@YM$AK@`O&HgK`Bxw7L6qk0#h!4ichN=? znyF$x@rolJ=1EFu)ED}eh?L5;6Q|G297iita}>A8#XpDyET||ld{EXEO192+(47|p zqy?r~or{E^kohQgmvGS>uOr|H$`d*iepy2zZFSKw-Z^?eK`pBbp0``PJ$rmI1Z&QgOF$+0dUsSEQntP8V_ zFaNWqul5dOH=_g2daq`L-BV;wqshIp&fTnXWM)pn>NJ}hosf1NB22N-IwL?IRW*%p zS2p@_Oq|^U{`RdJM+AAF+fJL#v?3nV}FPtn9=r(!{pN59nFV$rJOwEOv z?hp$Z^rD}fhNj+UCL_yL#jZ(&n}8(kUZmK z?RB)0FARavP_tY_wepoFL1>qh*y-7&23EXEt#&hjD1W-a?v2Z!ceKXJ2d!kV*vo}y zq%}7m$_(~Z5XS6(4q5K7mn;`YH{4vD1wjyh^sMBCgmCukG3GciuPmjP2k?#~pM~p< z2qrdjTryPK2JDVWW6~CG#T$fR(B|2wghv1EuQFzivrak z%nN@hg+i~?JloqdiSL!#L)IH6Lm5y~i91EC4$3yy{(3&a+fm;_M*BC%#!1jZ{FL)0 zn&-<`(vjX$7ZdUavmTsSAdwf@2o5avBp|fvUy2V4Jc92{!9HGf`g?ZPWXe1d#H_np ze^WY`#AG%<%MJFiXef$eFBk6G;-%BrX=BG-UAje1@P^b*u-8$$nX)~5Z`pdf4JT@_ zn@4fR^JVKjxZ~aJegey($rJ51yHee}IS+SGA@3AxZ+XX0-=IB~@kZJW1K!U(O|xk*c8wAQ zzeg?|Zxa<09jmj%jl>t!YT;xv`kMjqxO8cK26#0FQ$?{nwug9F^w~||KKZBRUr0R( zB|+KKn;RQrW!l_2j^jK67eh6-YXPWNkEWY_`q>^M@>{0|FCXqao+W3lh2wXt7wV^n z=!T|ff|BkTCMlwT&i%5I_K1kLr+-%Md43!>!kTpW`>@#RZDo_t@L$or=j-+OTiwXW zOM1d2kV6A1r%xlh@6m*dBOh~f5{+&RP7lxBTb#HOUnyVXfV#fOWtHAO zf5+SJz#c+Oh&p1nw`gFPf$=>p&1$`s-b$j3K5P;5><@J&nSF#)T3}(Ic*t74zO7gy zI(C2@)rnj$O`?~7SIN<~in8r)TH#1XBf#n)ETuY;@BMO=p{%d1Pad__^#k>4HCp4} zu|^REDrc!AO@(tpPW|X&^h?xjW|dm( ze`1a*NvFvo{B5n*GNn((WmP@ASl_X}$$4-}Op?fyJp&r*4+S1DuPCkRQ?5S~7p;_^;lh2NK0X4n1`)gCZy55x=JD`YNLiSihk z0?cMC2Hj?rm?nI_OWEavt7DH7AmTvM_;Mr zt((=2kne3($NGu%m(}!GNAS8NUpfHkIyNIC`1+^}-4K;jC=z%HG|<4yy+2cyJIU0S z$EaD2g{x;rlEP&jys<&?)pTd~BbH;BthBtOjXcx-)UN=4f6v!OfTxEWDIhT8^bzek z4(RMt(8mmjbyS~SDJiu5a4%%zyiE=~Gb@iyd4Uf13Nxd%ZJMSvYB~>gycCfxh-Zpb zWfBeBHvR(=aOAUt8NK`aQ(*%DWpme0ArbBLa~vJJ_Y=D;`5jcWcW6A^(EbsM{5y|I zE^Hp6l``2=*P*L4#~ z*Pg;uZMyN_#5H2-=7hsxHMdeu!A5E9*bWiA$vQ4c=g>((LBkHH1?!RVpIb>3YD-@n zBF=UaI0Qe}jxmr z-p|!~X*AC|)&AzXq8JM^T++oNEJ58?*%hI0muVS9P~%!@w&5WFiAcKiBf~fCPLbs- zaZX(6Y8X0-H-AeRCr(dhmB@tf=P?rPYm+f}) z_@g*Mk=s0b9iKHW$8)4@b7!$3b_aplAh`GP&z{;-)^JR9Y8n<=Y1GyVogUy7vHg{S zpC4V?i}=eQ9{B{Blh|9aO{thEQ8V@Lc!quIFM;gui;O|31y2QR%uiG_v)IERJS zY-*UR^no&1&?P3H-`<9cGQe4Az#5_ohWOq`l|HCo4nF78CchNZS4|;+NPy17{amYx z9?cozG^Hej!GY8`Nswf6br0JWkw1ZcXjZ~3-DatGiO*+qArspsT!wIIm0<1C5p>dN zf}MKzx#j@md5PqHh9Tb`tuLMG+TOh@mwDgpanSJm5EoEI`fj)PEiEG&iq-b4q!8I4 z!|n{;rcbrred(90LzhwQS8eW9Hk+N~wRM!Dp9<^Di#=!_s8gDf6? ztbd8ugLAJQ5n)31^7~PKTZYcgPDiZ=ARlJGoh&w%)lsF#DtK%)Y4X~*a?&O61G}?E z3Tf7-M#|>=?R+$N6Ysz1G|7stJYOu>T_^%pFX# z(X3Cy9pGs^64_pdW3}H)1O< zr9J#9_@=avG`q^zo6O@dRFz)*jR|#Vjy&UOXs%=*y%pzp7>|&92 z{Gecc0Y18WzAEmVxypK*>u;V~8b6%|-7;U5_-LX6vVh__}swaK%z;E|@kg%Q!qek;61%@hd^iiM7;8_tFe&vxWa5m9aPzRE(TTn&SHO`kQw2J!_<5#s(aRM^tiQKC{Boo zD>^%f$jqbv61|FyXV~y7Oa_CA=FH^GJE*B}De>2ea%& zdpqLkDiY!qEK2@vb%K)~Ugl+Z_Kp|BwVx#qQ&LZ6t22o&PWEbXs)6~!44gBimC9o| z>WCo}G*6%Slu=aTBmGd1rgQEZ$WgC<#=?a7X)gX`~LaKKLtgzKxSxKtP`{k&YygyP{+KvQZIk zykPDOB_};F@I7hA{o{3e5M7$=;jG+YAKCcU_EEhN`oaoeQ;v4AYi%udIA;!#~ikoph(?^>2zG_>gc}SuP;${)H--_r%ev2n}2Fj_sDQFMg%{{{?yhe*eQ`71&OGM z{K{|``bm)^;fY5=w18ESIV&AgS6mpFLo*f;8~co*@-&T(mDIJ^v%WRXcnLiaO@R{} z0=-{lT1b(`U#>7|)~)71z|bszZHul;H}rBwArD^@GGY-NOMm1#QM88q(yUEs&e{q? zl6sX?WkPN2gRnw!#x>!rj&wmXO@=V*<0aUfzBmkYOzeZcux_2v-P@`mSWJ{vHLLWL zjpJOSl|CDD7PNB^)3qoUbB-o6dfGskj`Cp)RCV6;l%4T3Yh&Z&>_#=j-VCe1;iu|k zfU{YFQ#$bu@GWSAm;BOvWE=PBo#<>XUI_z*UWX=ds0r;{#Is$45%h~Tl^-b~$1$#$ zLzMt6G|MEBBJZFE4z671?j|9w^eU-so#|*qBQIJvPw1e>xdS_WrG$#)knT&}ua_bB zw=mRDy-76&NEQp2E>5H+Y_mR)GHEgOj|c=cxZy095N;C@tL8*R?kCLtVvU^Yl@1>mS|yGMGA$u#8Cj+H=XaTi5|#Ugz8ptN)XB zFHhtL>4#{Qt9iA$E<5)KJczB6{ok2gO#6gtewfUt7bMtOo(%Rr;;J(6*}#bUB~0?) zf}M?*ky~M+Wi?h8#4z}dI(@we@6CIB?b6amH|n;=N5m)YW%AWgAN`l3P)yylFE_~r?-D!(Sz@1-Xk^Vd}+|cV@a@#cXQSDhB5c$DQt$W z^I5YJEsBtH3KJPKT3Ea48}*11_CqDh&E4CWv*89zyAA-=Xh0mD_5G8Kj1;94i8cy} zKpsu=@noS3jQy@{D9;5Ex~VGV&2MR<`vY{W9V$%;^`JB3?LxY-DOB2#DGV%(;L4Gg z4$MrOPJk&{3VZPup#}g%B905C)l?VX2Bc%hYkbe5Kar{qcXVvel`b;gP9$ql468E= z=ZvW9EGv(w#0|o}W>GbdR&(l@3pJQh^AB_IWNj3~R9!d8f<+BSSce9;W0+J7^9MSG zs1kSg>ka7?=+~H0Bjld-6{rX{q=`kQ)2+PN%7TdZ&Ro|%rD>LxrLa&Kgw*hyF;UF+X4BP^%qgP{_?9tdYavC{i!)U3b928TxKP|c_Vm)qS)*>xaLo!^uXe5V$ zPPl%$q@$Iwf{`&2qt@bPjnv|SSN?F$w`0eN!iWm zz$Ac=<2j95ayls1P9{9rQd!4%UvKBCptFEt9&u};8u4`#r5SvF8$B|ZZ=cw!MTF27 z_8Sdad@<^1U>Q+HY3mK&N+)d;oo4Ek{xQgT1aGER*fOU#1%rxLs*0UbiMVRN?-IN8 zmSw^aNCABBv$Fqc+>g2b*%fJod0U!+h|dvk^5uC^qedh>C$F}9(*eHo&!^8e^vdcl zp*cgjTgMSoKN6G|5~uUW_ead4SvOrC`O&hG-;=6nG_*wv@j z41~bebDBC~vr+4^2f_L7uKJ|>s~o@!qNxN?3u2YGI%ZM(Y}i8}~zwc?Wy zEU1jm_k51)yD17;x88I_i%qgaH=&`#0t=VRjFaEX8Je)kLOcr4qZ#uCOZeTnqa?*h zW5_RXGB4B6)%Jfnf5>L(^E${Dgc5-=n}Hk6I z3IcmQn5CaalSQWu7#3G0_pV(WYbCGK;$ecwTW=nKeL#A~7sP97xvpZ!2}K1}g|to7 zUjuoH6-{>ZkQ(;L)4Pt)h|;K=5S^Ia?>;W2)VcJ}&wZI@)78}X*GIV^v%NihT3XtV zxxG=fmhTNyq(6Ta5f!BXf*o}3xcL4FoY-|ph!mNSki4)=fi_7DxUagprfw}leTO9=7>$Hk+pEFl zMCfYK4r2F6uwz<6m$x(I0Q`|Lb092S8>`)k-=!aefM+t;t|D(6*hCjeI3PAws8PjH zd;yta;t@5f?hHL1CyPb_^ri>fmbJ`R zYPg&_&;H~e)jh0pm;5-pJBMDL+()ZR1{1fp6u;9dStVw+DBX!ve8X`#hQ@ebd+H9n zrs)~NsAD^k|9Q*HgS&!i=##i?;Im#}a{TvGv?m6FT3g?H>jTOHy-4r%ykTFi#>ezQ z^w}vlRDvDJ)R$+^Auro<`m9!vM&K7BRWN+fw3e!YRnA2`cCPCS_kB(AU)&FyYva7M zOuu+y3mQ10i(}XcZY1s2s#dmgn>2xxv*%ZwrVO16S24p{vooXC`vpR+tmL7W9a5lm znE(Ux=jrGpj5|uLTq^fLIHn+_>erOont#*`1TxAGwqra?*Z^v8pra}z5x!iKf$ z<02~cy^|yVv_GkNmjhi~MjNQY8#k*j-I4h0ti}WSx0f}e_LsX=!RbJZU}LbNts}cD z1aRy~w%$C$YP5b06UwkN>TvuSAHP)-)OR5s$Q$54J8oZ6?JBL*Ye>a(i$xt2Ia0K5 zxg}`aySnc@l)-Lkn|z;|X%S}ShSHgeGSdf-gw$E2lpFusqHGpIPDl57ej)Sz4&Hw) zqeAV2DuxLW*x5MqUMq=s7}T56H9r!R+%bQ&8C}BS5IpKW5{QY2lSKHuq)*+Gguh3|n(}CLK z<6uQ2fR7$))8!{F{o3iWdhn-Ylz>ZRhf?4B4%_`^FPA2XiH>eu)V}{a`R-z2gT2Zs zrJ!lXruiuT9uT$&x;rbDxj9~*Zej*0Gz9KVPiEApm1*tK5f;msnc-#lJi9f-iZSL> z&k5fd=Gi72LxkefH3EDqhyN77Mu~`A(llrbQ|{ME4zU!{>a6yG3 zqW0n@HcTp!#n5mrIDAOcINf4b?A`L+w^O#TnKHqG4jYw7LR5p+|S5KVic-Owl z8J1%`sRkdmer>DH!Jc;ZBH@}K1p8ylVpL@0n4mrt9j~Tp0s`o;E3yr!oAc%F)D&x1 zl$T?r3RQRXPE1d)Edww0M4RUfNd5EU zFlemR7$3}7YgMYp1Lgxhf%1mIYGWBe=?8O5Z_)c1-o638tL(m?zZB#}D?El8%RMHC z^w<|fX!nu4bNpE-t>Cg#%`UcA6so$ymT2Ve$2Bkkky-LxK92>>(5GcBa^M$qay!jr zJ_{w${@8Uva;-N0ozs>$7}Bk1_2Ow$6l=SnHICI0KqQHgjWy$06Q=?vC7T*nyY<{&CBn^sXW>be*wz z;-~03@sPS5Ejc9cmkAgX47#0LkkF&(N3zW@NJ(RWf~RUjxW_wdtCdzajYc~W7!OS7 zY>Fm{d=yLcOM|buAhDg%#dt$2C)t)}bi8@il~RPq17I|J*5_&+NQH1Wh4EsdO z9iu2{Q~hnm(9gus4V#*)B@_%Ug*Wfr(jlZAQ;kkPluS=U|Zg)Digv ze`_*GBGBN7S)N%>WGb5!K(k%mSKRgM)6hUyON2AU=pPQ$5j$0EaM2_7pt?c#={g>p zD@%FvJ_2y8BYpG34Z&lQC!w}EUg%m9D4;=^qTojwnj-f!Mlk)pJxe?wc_+f7L<>*} z_lrrD>>OjNdQiey5jYqp=D^OSvka{^9^K9)A--fg83wvDfi9 zM9_#Zu?cn+RgPppzgKES1@-kr3oiaF^S&%gEbnJ&rukf^paY`GW6lE~IiDq=J7x=RXUeH$#YC93l5 zR*Mzr6LE?D6PIhzjRunWLdWqO4%OdngMnvVSMU?P0K^EMM5h|gg;^tXal6=fgknj^ z6#h7^2I(ceouHy(E*#$jQ&&KH0h* zOD|3}JEdNo++XdR&q_yI=MXM(uX=|>5%Mnkk)`4NC7uthm1Dn5VlSlinDLiIKi7YG*8cqJ0aT+Ym0Jm|ot$m)lP-v*E@Pv4BUa-Ah(7=jI`+)nRc(gwZMijzM!q z&05*k-quD{RIu%ZYf>%~vqvD;@T)s-zbw z3JBD)KQ|T#iwdN*e1}53zd8z#L?%p7;29<(1vYKDC`t+_L_DEOO9GLEF6h0H_)&;V zJo+Ky`n6_NHs(95HjU5C2!6zPtjDcY-~rtd+L3a|wne;7r{k_CyP_x%yJWHW%e%F+ z(Igc`6c*YKh9Vxp<3Z~RMw#|0%+Fu{B>J|z`5YmVnEgF)WKPoTeB^IFStwI0xgPi% zI5iiWd_nDLnh7RLq{U<=btyn|>W~X!!e0JAcl|fRHeWdqNQHP9HjS zwmZwG0PAkzPHBG7uuvU4HOX7Bk)ksPK2&~>^czl*Icjx^B5l$Vy9+2N!#Xs@qCjQ) zXdDX>Vo6O(&Stw+ds$x$r(v|ETTf^yMV6u+qtKFSUpCcMrI)FiYNif2r1GHer;

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

33Qa^y@i#1+7E>R@6o=H;X2RD1_Mvr`rNrSI)e5>w?Fn8(tVK6 z*wMZD%B_*m!o9D_v{LAEwBEmapjW4e;EAZG#NND$J#SWwf2)RC?6nwd6v z&CGXf@1NCmK^0ZN@7T=BVdn`~|xYry5(VM1_aqs&o3_?Rzgmul5yxBM6SDuASvSHvyJp=e_Zs5ZI|XqP~3; zA%k7deP~jamW|vy{)dv8D*w03qn%+%$w2rMR~c)*h2BQY%gwye9zIt#c0TaH0~LDL zJaubiqawwIBRB?~)3eZB(%$oezolSgFer*9zE4MZGd~C;Y7C6VvY6$lEwl{^YQ78BoA}Ob;TSgtTVeg(6}EQ9d*^w*5@)M+rDEl-qt-}I z>c?&BN8^H?&CobD0u7?W(Y0Y5@`_54npcQYRfsUKM|xo~8bpP`qMqH7BWXC4O*ciW zFl!zafzeItqkCKo;#9)v5>pG2>f-j~se)hc1=J1=!8!HoBG~TX6Sh3hRu%lL%q%of z75=NQy#{^y_C;M)F=(hNb1ho7)GEFIfB`yDghzxUUsZaSEM9^ifA~SW@vtJt-~bdn zOI4!dHoElfP?fJ;s&dL_Q!6?epQ;!4$M1h&*^;HkKHGFfa;y4GOHI{+BRVF!JW=r_ zZ{D;?C)D33|As{i7ir_9(BiSTsdMbwwM*Xvh2F*u8c)`}+Iz zv~>NFe^bX>yCQLP#dXA zJr|13EVVydbTU-Ml&*&3F1}blKYq{W$MHbS7!p*4&J<@lF{^D{|7ZSuC4!FK6DzET z4iYo7Qg%V!he*3L<>7-lhhV|>Upxd#1hyHzny8m_S*6M3zZxAbtJ$v=i z_m-)I?ELxv;M;G$(buv{Wehwo-|cqwd*099y?d1SuC2$#*k=HasBU4#^!Wenod(ie7JXVQP&N$E5v14t!<-NM0{nMc{r_axum=h>t>ImUds(wnm#sRDFTmwzH#&?K}6#<&VB&7oB!UNludUJGGFj z`?QzTB8S@knvL6JfDwql|0O&;D7|PAx$(l&I0)Il$2opqMH{PN4b##+jYu?Yh=vH;2%^0OPhL#aYB1(t_lO!tDELl`7w~?TB-4J% zu=CBDA6H5$q!zWIj+N4iO7~h7<{PUdMePe4MP!SZfubDpiAxlX9~P*z2#`Pq#qHJ-jmF6`KSGWfsA(kSQ1L zhep0+@sg0TH#1A%rxe)ueu1ekD2FI+2+QTlm9_N+;hO89q~S?@?Uh%oML zp4(1Jq#v5^Ndk~ZUY{uQmWHPbCv)ClJZ_c%D({>s6&0-n; z!3_C_IWKFgy*+WhELpKu_GIoef?>FFuP9y)ZqNDJX35nnO8Rp*NTE(GLVKcfaP5`J0|7}kA5}}I%b%Nf$>LR8WWn#7 zD8dJbgEIoE?T{K-8P%4@!UF<<>U&brK(Zjnw2SG2ZA~4 zjAFHMn=IM9LrNI|pk%Tc^vyH!oV$!-M#e`!C^UoxD&J-!SbX%shw|*x&&cReqm1A_ z$0|#1VcrV`iRGbk@4EYLx#|D@Px@anKpG$4#0s-RF1t*wH|IXy2+fZ6 z&D^_due;7}c*A08ps|4PwKsFG^t-s9DT`ZdSx_dYb~x2qF;Fz8Pn~9!Q;v-VtPfQ9 z`t|C|jW^yTx0v^Ei#g|ImtSr>4BjE{;p6{(Y{L+Na(&kb(RltSA3Sn>&g)~}-yL_| zY0CI^n}7sGjrYOttU?jmwr|^Ry_G2RbP~AQufP5Vx$&kO&Aa_SxoYTDwsGTiM3~J< z(uOeYw!iIdi})a_(u(rB%a2Ydq0LzMA)|eD>|)uypy@&dOhP^Ug{`S(RGVt$j$q9p>l;a2dQ?7gR1Nn5u zkCI`OxH!8RrJ#Vkm;;ilmY4KbZk58l+lBUIgOL$;-}8!OemEnvBL`$AE+Gcv2y@6V zN_is;9FQ-VE5^8&Qy8x!8Pn*iiR6d{#)F*9cXQ)phK_}3^VNyW0OV;u`GxTj12OPD zaslhZGXISEZ6wfQ9vCuXH2DYF5!oAqq~Dl_G148%h%psUCW>p9quD1 z#HE}a*k^9Qdd>pYgSH}Lhtx(Ht5H_k)u|x$DwMF{hsReeDHk-WAuTJHk)PLZm-iR` zA)^Mmq*C+Mi_W*_;5SV3ojLo#g7UVE6CZs&z4Ir zy+qD9^GrF@2*&;TUu-_l%z7yleD@xwTW$ZWo@Yt-(@&R+`dwsQ#t3-`{lBgFO;)e| z)4puHIJ0NZvH~V=f)%7&K(C z{q05l`q^va?d#OJlezaZjL=oh-ZST;@^hbTcsrbD2_tfSiVEi(WXht~*=O6o`(Dsj zF7Dso3X}-htJkct7LVP#!o#&9VFh#Z+lvSEmy3+jce)Y&cn`c2J`n&B;ubFa-gZR1 z54@T)XU;O$*kuJy%Jp)iR1YvpE14mge@J=tIsZJ{`8j3GCx4+}5)O+3!}Tj2SILII zG8eB?=g#(duD|gHx&DS5?0w~hLgEJs8E$~I0Ot;(){^(6GnMFd3XeosmV}{ z!I20X#>z^~^qEnX)Xn|4X#$|dEbnq_74&PNwoB< z1k9N=%Z6C;H}CK4b9#qaW4Jez(w@C~*?V)&VPBv5hfbtyxw4X8EZx=%LLgr?%FU>^ zM%jcQC^aNR>D;Art~;%GVV)5R1o766g67n_Xiye(Y`D=;(nvu|1|k# z;8-n?Ie~LKeUTTQF*{|R^1^e*JOl>xFtq*dvxr209E0%)6uBs)7~{*xfw z?2z5}zABk-O%CnJIZ4G5V{|0Z6Go^=N&<2mNjee{woY4()L!Eqt8zQGp_ImRj!6Bz zBLhT|Sr|W7WAB-9C3An?AoG@OlyUP{$&97zWX4Y!@^i+v z;6l~rxM13Ra{>kq-iO(?@7^c#*KCn-%h$?RMoC*>l(g}`XUNxUHd)2Z9&fhfgTdq> zktkWJr1h>0z2<7U=DKTbNa8u?o-0j`Ki(=0%!6W$Gpx2NJeoL_AU!SpFhK}Gn3u-y zoxjV4JY;SV_|wcw=RCY%l3Zk(jWD|1yLZ|BgvF7P-cNWT%VXmvjg4>}(ZH<9)yL{2@92W;}3U!?E7uUXF-bJlVK zwPn+`-SW)))8w|{V`Z!BK9qt@Qia%)&m@&CA?ZdjD>|SX!dYl<6igqo>!Fdd_vLZs z@7VgRh|rvJxCpfyqE)WC>Z*u+8DkGZJIX~O`gmlgNCs1;+Yp@*3&>J_S%c$&XO4Az zr2mf;K99!_P-6Yynf&I&`@y)xoP07Vq60kq@WXPh(GT47>yQQg47BvCggjC<(cnEY z-GiheD1CU=Sl^9|*2tcbjKA3tmvUA(*(gfpKC{*}GoN_QW-VPW-!9IORqJ-hzP$(R z#{9D~sfFdFYGtKPnPO6McHp`cPY>*`zw;KhA zHPnLVM-fCUFOp3|<>7Ht5@L7d(5vKzn{Je$S6^-akC00xAbG^Xg^P^h7jD^0GUv`t z=;eFT(R=a!7MX@hM~pEROukD+$%|lB=6e*6?7R^i=e~{~R1l8IeRGeF?FQ?$cSZ&)FXo9D`*{gK@4x7S2fJlVZvqZwZ}Z@La+DDBfi188m3H z^fSuOX&pP-`A+-Ic?o0t{IhX3JodR~pOyFDeJ^5McV5Bs&}pD}(&))dM5j+ehgd@g zsE6D)iW&3Lu;@jKe|MlIS#Nk5!H1n&+? z2wArx4Jz@#yN%TDvCzdjsOALDAIS^py#H9>SV9~L`CySzype=D#%f5SJyW0X?2 zjrIJLAUk&LmAOCvVfwj4{b~Jb<)uN5a8KCEjLq`yl<+j&70j61w{we#2|!q+`hU}Z zkiLI=%_vieF*vZM=_{YkvWY`tRq&N#o8$(ud54mUr%CFAgQUp$F?!6l?Udd3jF8M{ z#)$0AeS)Fn8o7^-A%PU*NhBqQKA*X_J$v@dvB2&HV>##bgQ);0C9%Mm=FIiVDa`$f zRcS)TiX@A$WDdlV_1F#L5lR zjw>Y>wx}WfPN*S0>K|u2@R`4FkgY~3BWyAFo6v7VN?-7AquiMN#gbBNdi_Y?(p>e& zA6dc-F9=1AIb#=Gc%ju?Ve{rKl9932X!ha4Weoj5L=GQ-0`|qFrKMYi3_+81wYat) zSQ*xv?+MR~PWIMptK6h`6r4O5AMM#A+>3K%`)wW!&Q>`Km2rA{nzariJpZvWa-1>WlW9;gXyOTQHFSu5)oeL2N^RXSsb~i+{Wj7 zp&l`bnX)8|lyFBsrNU9r2p44@8uO=eD}2(qQ^tPRV{V;yy_7`3eWS4Z#mjRq)Telq zUqBc6X?aajg5dY~E+yXasieF>HA9 zoHLPNg#;#RH@dfNc7B~3yoxKyh@?gt+w;V`vgf(~g@ldVQ>1^F&|f)&K8xszNSOf{qX`42k4z+t zi_w-_AuKWbYwQT)%`S!OB)??PZVrgkszNA!1 zPnANP%PM&0#98Mw?bPfqWqwyYg1b4mPSsa-fnnyDA#`L3P3D8~!WlHO#;B}6Lgc1# zy9BhL;t;gRSFc{phWN1$VR+7***2S`8@v#N(%}Z(v^h3wLo|3!QYQ0BB|%xu+!xzC zqj<1S94gajOLJl zsm*cU6T0ctcQ-r(3L44~6K~LYMXyHO~*QMQL$>_Vyc2Bn3 z!jX4KkJjN{XU>1S5uDd-IAnAjFzhX9-2ZWDbn#O-{M;nXK&xCESee#`b+p$Lm{p0PhbNFpd2R<1(TfwDp z#}mvyQM^QHMI?3DASpVyM{Hq*<|Xd=*L$+}iT6Z|qMZ!tjY?Vw^)Mc7B6A z$ulw5C2HJ`OF64phVU?>0GM}(GDe)QRnW}e<%$)N_Khk^*JhQayV?4+sV&t?r_1CY z*2psxm&y|p7RwvoE;l!RKzcW>D(5t+BB$4>B;9LQl!j%B8%4|r%^^k4QP9ZdSjGrS zeqoIW;$M6*L7sc&Ss7(s%!vQKEMv!vmD#gqTY;MjIqtJ@)*=fOFkwvYkg!NEBd0i( z8+Rua41wjh6)SAU$d5n%*j6Iuz5Vj@&sL%M;Jx?l8e984R=b>l$B@Ocne&BZZ|Tw{ zHk6aelL%tbEC=zEeIuqUa@*LwxX;<4!)bOxEy5l`9qW~`kw%_1%1G?--n;Ku z#menM=L{$qtWEda@aN>IC!UlKKls2V1tBB>A)HKT2h2+h}5iH z*7i*fWM}*%BRpY9@wB3HVVB5jL;bLPt-LjH?qT(q`rfWtExDm@2PtBdv?4~RjFQ)_03n ztjWqexoP_@S-LXAC~1d&U$k&?Sa|2MRU72TILyxTr=F9n_CI#nd4u73tz zNl%s3+b@*VD|^P|F$H=Xd{(=fN<~xI@VO^UptTKO@OKTn8hq9~h-EmU4cs z-z5?e8Mn&-ap#v82xJh#N9oE=v#T}q^xn+p|!qoA?o*@^YaOUv5jWb@X& z^3%$#rnAkop0wholRubwKn`Rcl){oEC5sj^7fq7o8+ORA>vu@iVrkODC~3t_0S6Vm zpn}G^3k8ydMXC=K`5{ow7A%-AYu2u{f;Zau@MoWuCmwrDp85CF^0@h=ps;ddU1-wu zcw6{~2?eefnj2`C0m3lMXRX?`ZI(a0JG{vG^XAJ-FTN=Me(GQH-~asACYxYkYZL-r ze6+B%SV3s7TeqIA$UG5B>dQu9edXm5b}cU|(UF2mZ2PIh`>I@F%Q(~l)^AW>&N7`+ zC8Kb&4@Hdf9scZa^W6WnODVAS#k8r@Y=d;(yM5i}cV?a~XQ z-x?*O-h4}5d-XM|-10k=dW6mV2IY)0MfeyU0~22ft>9i62;7|&I2{SXd8E)WIv+?chWqg2pvABv3#j? zdE)xsQp}9;u|N`#7sq@pYc?FdCL#vH*-KZ-PrnD-GhvqLso@5$A6I0^hAqMDyCgGl zFKWGJ;||FPdCpMU2q$!d5?Q|VAuq(}iSum&6+Z|M{c6E7`**{d6^(*cBTS|+m$&!1 z+U4o{?#JKl^SNnrd`+oWJ@5LUW09nar6q;XMo%|NX2i4y2TJN){mq~gTN1IoBR-Qo zkG>szVTr=@?>3~-W$=iH88c>NEoSTo;|Gce(tfJR+A|a*4eU$6?}y=O8@M&1t8dX2S;k zKyJV*#yLrxhT)lH96TcmC+A`wGYTX60-=uoHL{`KyJ9?|{QV+3xDK8(o(Cn2(0dGs zoSVoH);;4n9fK$Jf#=Pwd=!^fP_ay!RIO0V2&}=-FhGeiYLzsoTvC>=*={<))iPnu zDw+4|1}R=NMJg0C8g0lUh5}ZpSW#KOeUE(p!)lqdbdAjXeWR2$N?aM!K(pqqq41gG zOPb?PiZI9^sIidq8E5pcDbo=Gn7f6xO!`U`k{w0>MX+mq$|=^mfU=ZGyn z2)JFlb(8b^o-fTpR`?C;H`vh2_uqTZE~bnH8(|2AF_t+Vu?UGLp4iI9+49UNB!uhX znOnYWd6?%Y&v4yhuq+bSiPE6xv<`+kF3R}Aeiupa-e*hQ&?3=1yP|UFYF@O=8>IZT6+*4jX zGi|VI*RD3P3L&|E-bn`1yqa%!wDF5aAt_UK%y_@>m(^iypK#4h+jiOO?AR5YC+0w%Y1O?|R2*H`wVmJ&!QBasYkWh)jwa|s4J{d{NpOfGM$vt)>>brpd?k4 zsAXv8N|cYXduaOyi{eT~4M5Hy@p^k6>`n&`K4p+HX(BCEFm0rLIKGpLD625L5W}qK zbUQ5A1n^Gf(#M32I6%ZMlint{U2apDiv2j;<{BS3LVxCsZe6s*x=X|t5i+}wK#J@J`jC^6Rta{t)8$4Z zRkh?2%R~1HM?$#q?GFqp^0tukGn4|AB1zYylhzfKIO_!kvCS~; z;(L1&PM7(fuMf+1#b#xSEl@LbDWAI~<9@3)YAR&mu4nlt#^;z-E|i<>x2toH;L?3; z`;a6b^pty5_sKO1lctf^AG`P}b}wMBvpRPjbU58OoQpQ z1gDTB^~prw&LkAv{P_t2LK^VmPrO`&4wov+n(Tk75^~xbPxrY(ulIfO8exBf5ZP=C zU)#swyJ;9*TCz;d(8Bmdi^D>N)BYU#WjguI2iy6&TM@IZ+$D@JCyg0#kbBP_I=;PD zXU2U?1rC;*<<7|8KTiZM1(7<|aeAyA7d1*%yiVqSBBgsA{cN^aZPHQefSc3MLT^62 z96LbBPKmpu5u-wFXtkM{{7~4bEU!GOy%ppoZW`Jr7IBxR5D>aA+Mb`yCaElFc))m2 zUIZj9#JuCZH?rqYLs3Nq4adZb)1+*AXE_~~IX9Hye{^7I`;+j32)t=9@dVce8#(aSe-#P5w~8b$@w7f z!<35R`aLWsWDqWad|bb=!E(}_e0%GQM;6UoDpX#1+9|CBmuTUn1D9+9pt&zJlF6rF z!3mPKcyt^}WL`XPUJ0uZ>nzykO|0x%c`IZ3L!RFHf5Myu_4n?G$ z)PEfMN}X@9N)o+f;P+xAD)@9V9_nv<@j$vhT6E#4KgjsNE=N!hD4iwzHzQUn-q2CY|dY4XV_|hm=!7^X}Y`7azAHnE}wrhHCSJ3+HLmJj(`!Pj_2yW zG=0H;?#B46GgoD{ugG;pD(VS1#g?SJt9dY-S*Qt)I6qS-M9N9;4H-l5W5ka%-EG0W z%GPeFBpBM=QPY(>;&P}W-^7^#Yk`Z@Ik`nOv(%QQ8?ip?Nes5*?u##)Q0#I!tmsmt zC_$R99&b|Bma|P0r0#n|2fFf!f5FO%(XrkB93~#%M-`=Mth`(0j%zY_yqTRx5UQ5f zsY}C)Vk(E2;7k!uczn^Qw8gU#T!O&L#%U?Lj~9MX8E-Rse^X@MFuh_OgTug#O7sCT zUplEATfgH>>`|Gd;(5n}%t!U4#zhk+EZyEfhE6r&J3V$*sa|cmfwSObjqQ4xw2d!) z5ntHmakRdcW&-&6?EXM28wdM8EMA}5s@GCn?M)?};rTh?!~n)SOA?hb-oeJ!6B7`e zp3PRshrmYf>%R$Sc1B{eMOb5*?OY$NsYY_`IfB9EDOc!)9bWI2PocNSM$~Iy4>r{d z@?Fs-Q=9(K?G~iByPwM5QGE7_Xr5RLFzr*ptjESoP&x?DD~zO#%<#t*h}RIYbimAA zW2uL^j&oCiFA`bF_al}gJzS?-u%2Nq7pgCPGb}7jfVC%*QB5QzLou1MFInL7+n-8;?TuKEV0wN@FYog<`tI&x8(mK-LNL0)sz#Ud<+rCNwfDg_qRCZ0 z;ldYcXhk79IXaOF0uGNPgYxBq3<7Slw$4cGgu1&kDI+Gx1xI?sixdaWr$ z(gTDzyD#tZBI%6lHkyKit#YZid-QC%r8u2bD+^Ih<`m-@^kzQ}49gpUwz~~Xm*vkt zsSc^_&IjaJjISCUyS?=?`tfIb?c23-o#)vai_zz^95+|}``JU+2fDbE_VrvImOl!K zE#GV>X53bEoJel(liK%cYxeh_;N$#4ZykO=S+G0wu~&Lu+DmanSzF1JPO1`WrcZj6 zo~xb+nh#U%dnFdDe#bID&sy{9p?|AoIUQ!QtWwW?3c;SM0jeqQ!rEz&!ani~{RtJv zh{b2?PirOW9ypI(-2rL@#?~hnV^yXfv#b|A7pt?}Pgj??uLa@!I(5;g>$5m*R1X3p zjdkCQBoXk=AI&YKRE3_4|JI?#uT4798ING&piShAtCQ@T z+drQjvR(9OGW)WUo!3!@LLV+9|UxxqF50v%~+`Q?ybPlK5O`m;v*pZE$qYb+EnwALuJgx5f5d`imKzXB;I`whV=U0Ywx`Hkwey@$zBduRES{Bx2}5) z>M}+IhHV~*6#cu$wevt24^b3wyjP!pUDBH%D~iEHv+QSLvx$W#IEuxy>{YDYX_wVd zs(jODTAO|;vH(!e(^E`WAirU!FYfc6pML0ZAXZO?aTh;7d^A;#?w2DSI=w1M2;Ld- z&@<0fyf@xOvUI^ai||LOPc$k}lCZfHx(2_v5w^`)3f(Sr9n0YXC;OJTvr?+d(8oQ3 z#f%A=#R9#cJ)i7z4v5gySo;$@|3hDg*(p&p!EF@O!9G*L6CznJvNX}fe1bH!C>Z)T zp$CLFreTL#HH!0murmAmnX=~(g1ro%RH2sy_&8SennMWNH7~BWQ>ReB4hA{C^Xl2V za=GkueE+S-!-1{f$L(h}Yt`F3fm^&2gMthSVGPqKoH>JB2;!|9VCfcibEifQY1E>H zK~3AcU;I2TsQBRFlzQD+oJVE{fx-04$ngv$D9TuMkxBnVnyV$0Nl-c|5yHS(sU>09 zxeb-2yYbeEBokFCK#+FQ4DX#=VZfTMZY^2#w?Aj;V_!V_+70xJK8DZEKHPo2ywg+b zk8Kogqa2gT^>O;li2BVSbUqHK0?aMLx2R0ipd|CPOIGkbn^^~`avS|nS${CCxn2h! zuQqA{;)v~8iOaX>%ZE0dd9yv?{sd;)`E)$N&lEz{{O(=YMHz;3WnJ#J8q;a@d9IRc zdQW;0liF?ljz(G4RVw^b_F2AmuSPP3%Q~*#WHA8(EZXE_DweeI9GA=&bzP)Z8`Ez0 z^)_U+R(7FhMNh*Ar6g~>5Bu)fDFL&cYN8C8+iB{Q2FY@GDLaIz#mWm<)YqTaRd=#) z`ZjhFp9-_zZqvLY8!eVUD|o$qnMZa-|1Lr$72nizk#!ccI^_2uQhJ;q9s0GYEN3H-vp!aPjLSE> z{G0wONIMww`9VjYmiSF45H9qPoyUXSKG=wH-O!~(~k&-SqVJd zM(#8!2{m6KPe7Q=U~LFtNH9_HDEyD5%J|Llx z(Bd-j@=f*KX6GI8EaXG?O@!o6J~3@|bcL`?$2w2L=vu`!NzZ(rTXIIj7KhN@wg>sk zzYHA>T>D7IZ<2V!x_65u?t6EBMA#ebbnD?y!8=V&6cQyl|18;|#R61{zbQf)hUNbS zfz72g9e?IM@=oOt-Yl(BE+?UxM77v1p5Olpp*jx$S$PpaZ>Yhm3}ze!nM(TqdRlk8 z(z6ma2^#KwSGg4#p5`U9LfM@M`L-&wu+R6K_I95X0VwWmLwHk6y|>0Uy&ZJ%78>K? z={pEvd@1duqeLM^cXsdYm#6N;*V%@?0&sMvVkyiI!J4Y34;zimPKHvvdD<)tHt9s; z``0dw78P#mkLNczZn_-zy&z?C8~5J1#VSJ$8{K^#8IEYmt5sYkA`xGI;gjZc8a56 zlA^Cx+g8PA@AX0YaAp=JV0_6WsGb6e(4ZOm;oJXSP0^M- ze3>grbZ-ssbY3<(R+UyuDRf(nD) zVN(TCa>XXbPR9nua>ts8-?k-)$cP{gU;$GeH;8=lWC)}bIMo)+KbSFe@uxmE6ki|{ zFT8)?(L!Fb;wc+lb{BVnHgitLM z7?4Y`?xpyJwf_i98>ZK5yHJ}-jupnsKgZA5HaRIZxSR^JV7I#9UFuu|nM%sIMFc$B z{FV=5S5iz4BFKZL+nak53X%wYU3re|mScvPH;VtPRu_H9(^gxa2kq-!V8crL2EGFM zPLCaHR_*Ms8E9h>(S$S(bHj1$*Oe9QbM9R7q9uLoujkPWHBIZyogp&i2&U&l-`j ziU-6pN(VLr+9GbVh50PVo#9{w5A#88-9=`WLA^-fKT%79dwev7@RgOD>dTX|p{HH= z$^T$=3#WltP~hZ$$8lb8W1_s#J{skbZjptfy=gC?qV-@2HRN|@h7dK#T%6K*34xT- z4vmf9|A7ksemFxCj0hBy63FN+mM0{QXNnNQX4GNAQU2EQR9^5-hrmXY-6{|jGnSvN zVr{amlF8DCepZ85X1%C0@z^EtnMm0FM4$fz_@tH07zVnoOvS@dHh*9_-G1u!ayzV3 zk-Y!Z9Zw85``S^S!tX7C-f@}!?JoRW z1wF~@pr=SAFXnjQ*#v*rpRENmv)B9Ea0V1V{5nl+pCLZksN{k0>lwE}h|4FpOT-`V zPV4w8o=PnGp`3|LxW4}eg%S(a!93cn z8w{}znV$^*;x#`L;9UHO5-IqS8&^&Ny|qdcxCkOH*Y7TV`g0>NpEzNZbX^1H6os*_QG6XcY&WD;y@F^z7|3pFQ1dxqI{UqVo!uL|b7X|~7>=JIMx$i+Ooo4@0g9gXpn+#53B7G!ywu7x#(l%Q1* z|Ba@^1XYI1LJGg|yC+SBa&!_w=sHYg*xR#_PU=TTmGwo|8~jYjy-o17-tMoj3a+eX zF);Iv5NL*l36cK5TT9txEG?O&Zov9Sg)tWFJt|GS8{uLJFt9!2WNFUM)uy)4?A!+i zcXTymr6X}uOH8WSRU5QbR;*+D9hnoUB_?mfmyl?(dUTrD+q^AQ8gb;97*i)oC!##Y z$H3LLqxqg5#cH9^s{O}W5N&dpAahndE~k22N;p(F3@#V}dfw{?cd7#epP#Aodg5uL zoz$%+fKLkVi=6YkBX!fwpgF2W?3KY+EPA*B#RJaQ|8o08F{;)zBX$FvBiW#Y4A zw1_H<=xiuzg(#K7<5Ji)V!co}Rg`fh?`Nqc z_3n?dJ)+OpRAttRr>63fL)e_x`)>*5+4Vu!{$d}c_h+8yxntL?njI+9b6r&Dhy4KZ z67z@tTD$=N?sqNI)^m$Z+xeH%Pt96__fs`?aGe}h>v+-MyFxowcA*Sk%u6+2G|o#co=X zd#zi5a`JavsY+V~=gXy;LIFAP9|M)s4h#lLw&cwW>CL1AszWilO6%X{4VxMv@0+PB zMCq1ZPZ9=`;k-V(kv=hr-2^r(-}v(wuy16L0|l-Ph$8G8_w;x7r|AvT_L*lH4u7zh z_-q@%fRT4R=gQHx#n~s9hT9S)>Z_j@$@eenOWclW$9XL?Q*L~1_RZ9(@+nOtv8JAV z^%WTxhOEWxJ%YL5cqrh znR-fY**t`qRwLzxRy_&L@@TQ&o~crLKTV^=cf!_HG3&FdOXTuws{+&EVq5Z$T6NKA zyl;91I$$o5QOSGW>mO)bneY_O*S{AUGx4T>|5U4KKh^Ch7SWh%Z-Za#q-+f($b}0? z$XED8)Dr`@Zy5s@!W>O-6vrC1h{zp_Q)LyVi{U!F0kU55nQ5a9bFrF08%GWg#&^MG zv|N7|t;2tNn3z6J;DKNP7^s1&s<1-KqOB$?T;a0uCJ2y_&X7wIcri}ac(0L$(&Ks> zE0H^ewyxn(3C+7cf!6EO*hVfC@#}mn#|e&nII}+{uK8~=Sx{)4#&Bh?2~@bIaVSgY z`UieYW8aEUJ)Zws!0+mzq(+9$5{UUxonDX9UhigK(g#RbRLyR~hT97#V3jD$iA``U z@|v$N-JPX{-4GXA?y_@Tz>Rc1EXik8%=)J{B>|?>%3m}YEzVxQ(r%<{9Bm`hQE@hk z+F*Kc*1Jy3=lGgmX6M;sayWM8q6Yw<&u_AcY-jAdk%hzSN2@QZR*-K+nfx|Z zrH(kZhFYqMRVxcUQH^iWn*mFyLj}(I3g&%_t^ZKS+15RlryLDO-6o9WLO>FR+rgfU041L1q2Ii ztg9gVqH4+Kdqb%2CP~sbP-pC|sO;$aB}2rJ-m15wgcuBehDAeY5q+--)|d+VD8IyB zr#jCsgeYx_Nb?PCJmt}%KoQ$`FWdxm_wz-N>qwUibEm(znvdsiFU4O37ir0f>{ zL=8(Ej=g8hT*X4pLfX;uSeJx=&xa@h>=@sf0DJqEBOlDdGx)0+)~#K4MorW0Pavyk z)kK*=;|S64+g|2&j7wLF{HsUpPG#b0fkc6*op+NPl+6t5QLF9^-tGs-D$dH3b)^^C zt9YBK{8m-Y%()4Z3adl~_NxpbA3Kpk@%9XpM38HN&|PF>K8{2-eO9l;(-?@Rmet!*Lu2W-HAaDwTG4;}PN zLbBqWX_ZIN#{qpXV`JCH>jvPt!0*~qpFgfMm0xw98<*KKI)&;5GT_(9{raMBj;)s^(PMakdRvXxYUfqT$&I8x z$D3Nsop`-a%7UQ9TA^yy;>h^yn{A!S!_8)4$LTEeC0l~FJz0hLYfCcZtH$oH=;nE% zG>plv(xxif3UsvOrWNLf1+ZoxLexlX`#jp85&0zJ3roy!wyxe_3~vbg9#0B!tj}q{ zOsmlCs+%Xj>LsqwE~6Hr2(|F!JGPkq%~(C5$Wl``jUVV+b|3UM?}O1#U}Ua zgF1P`){?;QhTtut&oJXsWOE*=rbtRgeVtHtAfb;h2Y*y1&)v7!#9V}xy~Y_@jbY7= zO|SY186n6EN-+FnR~6d72v}%Sz916G!$8Ok>BiH+hT2jWvurb}>21uRbwzb^^+lrv zcdGCe@E{dXFVJw%N~-?5gvy)-YW)`Q@~AFJYo0jQ<-OepK@>@TEw2)teNI%4h4-F3 zH{W84-+#6fP0Vf#xI%0HUGKG zZ@-D*{o)97`kWp&(h1`JGad5I5Svlt3m!_axkCfT3TMl8_)xPEJ_prghv$Bf<7|qQueq>ic~6YR3j@8w1IziN`!8DHFP0 zpXG1(pVDAjPD5DLdL3#k)32`4f7bg|#SkD#O}2=|M+1QHj@liT1=@Y~je5 zH2Vz&7H&ur;+8eup8wU!?sZ@$+W;&((u-Y70A0mL#zs{P$pb*gr@oug#^}Yk0bIFP zP^PDN67>=Zv}^5ul&joyG592ObA(VKYkU~7KB+C7~_u(Ba$)aVP zX^`O$H0`E`P!$<(O|}58YAT!A@ZzVaY(*3DuLVkZf}Tj>^p=4FU$W_<3qi7Pi`*|b z?-vyI$Jd}cNYa|943*Cnt(7$lzix}#7@i#%dh4jfOD;Ft(E=!dhA6?&Nn*POTnJyX zOVX3s&S;XSV%z0HkqPik33IN>s$7yfSVfuLuKQn}7T;gaF4kJ~u~eRjPSuNI#@ph^ z$xM5WOhBjSFz?D8eUu)-T%L*-%z&4rq}wazfcJj*bI6tX10ZoYAW*yg$i!37G{NRg zj$l*Dq*fRiCq6Gi?WvR0n8KzBJ}?H(PeM!Mps40xo@PzT#RhYdQqR4LC5r(~$Bp^J z`zK*PId%)fT7WK5sepCpV~cgTSXlFpKIBXU>^bB4cW-*D=;V?Ayiw<7Z*XYC-}1tC z&qn#jfc8m)%g8G24waUZRjTmVON}B`0vp}3Xm>{qrN22g3%OFV7PJi4$qEYp|8yV_>$na%Mq(z1 z)wqV10S(|-p9Fzm+sqb6tpc9*mjHA!JpSPb?0udu|Cv)p@a4|z@fDyZj{|^ji!<)! z`7as__W-ST=cu-)<#nHfu%5IGbe{yEK*oW>WLkr-?)BGg?uWCpjaEwcz`bvAw%%1x zgk56HX8WVOij89SW=0G>JYJA^7u8MMt^c!I#t|SH{g}FwU-T+TQxdc*y9l)n75g@vZ0(EbuuzX=) z`t>H8IZ>(CX8ZL{otNv{z_FW?m6KN>!%g4id)sn5!DYY5YO~SZwRX3%B;@={m`>vh z+k5^iim^;r!+aFImEC8}iK@m>LFM!#-R>juW zVc`B<;wD2h9BD~O2w^7?W*@I?)< z3B!GmH2?WUV;(c4wpgx|~zBw8MoS1EKq`6qM?J>lIMMC>#QRg$}6#2fM&!=P3$SywP zBRjQi!c}Nie+H~(STv*B0UOa1ufMSHBb$fEjaSFGsCyhMKVqwi_Z=63bhGdX>rJ^GU{1J0~zkd^hGG6z!As4B}6v~ z0o1g(Ro}BNO&2QcOMqpQ1kh8Ko&n1GS0-^rM`T>ax*oSXfXi*)Ms1QpJm6%rcc;~u&z3#VSSgL3z5j7?e`8lAG_jh0FVLsNLc~*DiWl9nrdezy?z4xu1 zVuQTv*}k2+Ki5>uWFJanDqvM4*eSzM-8~4yD3**PlmSpPV!k2d{4)V{Pb~UvtWDh7 zcmPVrw|y&`!QS6u`%EGRH#{DJP=Ixoyj;a8=VqR(mgQHj{A00ExzLe+IJr%{4JD47RAp$^Lp4Gl#>H+ znC27MO5Q6$z`a1ZEg@#t4+Thea9B>(nWX)J{+nrqqIZx|iHG^K5D;R)X>AlsBA&{V z&$=l=WuJV3xDD??s+@Z#~ULHUA zUbH*TP}?Mn%${E4cRu!dp>N>a2kWW7jn#e*_@|C0kC_TEt2lReoPL$d_1;4CC0V}x zn;4FJSU=iTj~ASshOn@Opsm6#PDVV;I2p*5?WeKw5*3 zwTrZ2fF~Z5r7@zprRB0eK!ry|A?BIM9YEpL8TtHnB=;)Isk{F5btgp$Du>StEklIr z2)_mJuU~qZUpmv@1}HM<#5~UAM?6LRaPku98PNb5y(?3efVIC6m;@fX+zi#~8&7lM zj!+~A|tNfMAuml^W`c%8XCF4!**b9lc%#WC#bH|VQ`2LP=dQy>XY1ddmHB<|DJkDgeIx&Cjd`{v z299#r*e(1Uy|t~4rW}hNm5o?Y%Mn^CR?>McN(aRptJs~_$D5Nzhe-q-&(31Ml~yM* zd_K>r=XC_lw^Tf^xgUSiJ-%v>2vcRi0dVYG4-&325?6tmBf(*Wah;Ki8bDgZW`!yc zm|DtP+lzHOz4$;yHPB*k(`qv}+;R;duohcRUmQeMkzDzTx@H`$g_@{Z^#}KU&`9M* zX>hp)?5BPoP9P&ISFcc=cfq1nY+_%@2D>%Ho9NXU2d_=vUJ&oMf-m3SVmWN%$M16U zTcy%#Kmlvyab9$bYhZR-di1K<=eJVaIauYN``*^RLf`?{-Y)Dt6_lY%1=4A?-s}&r zo{zVv0oZUiPC^NUnwVCXoOu9n&-HSQHgZS@XyjY_xGKNf^hz6x&a{De=1h))tX0EZ zJf_~npQ#_+u_$nnb%f;^&LBW|Y6X`Yhk?6Co&jfR@SQlcWBDxuttKkBwz2|r1BFR^ zYn{bJ+WgOIw@t{Zu>Sz1QWrHbR1E(~e2aaUXx7OOckdhR&N=P<3&jqx&xi^Agi8L@ zVH15T-S?=kBZZM4Smv)XgNGwro&fvZ<`Xnz#*^qoyw07|4SHEaVQ=PjcK%lC@hFdM zih{^@x%MD3f5B(&T$%?benas>jAiK?vtfa~P5ez!-Bw43K(ZtJQJ)OkUO!x_#6^GV z&0aT21l9uiRcJ%-G8(DfurpUbF(O~KfNu+a@bC*)*^Hu8EWXKff>xq3{a$4ni_!WG zi~husidww?LF-}d#f^U8^Ry;=DO{eoQ`z*ck4w?=v6krXlpbpf%ZpsAXthI7)gqxFa-0eOuIL!8ACXPjVT>T z!rz)8Qy?wS)t`i|yvsU^=<7p%Tn!}-g-SnP^rw zM`8y@b1BmH0IVoIt>?X^y6KhoT_-DYRr^#IP@9Peo(Ny5sVJLb-VWR4t3bpX&Bri_ z;z?tS55u{F5YZZp( zK`U4iPuQnW!5Be8##QVS>$HTgc$C|D3SHNBm0tXwA=ZVw6d$ZC*iFnTwsdM0yUMZ{ zQPO#w$@>?6n6-BfugCc3oxiY>$atQQ1t!Dv+PsF)rZrJZmcq3289v@&@V@3zIL*r7o#e_Pl<9`YSEp{{7= z7iaJ2X7#PTxtat;^nP1Wl-Wh*R)H|cN}gO2s>voEgAYx+`NVe+N$#B0y zKcNsSl=8myL>Sj>u$tC8?XEg>Ct?1>;B5i2+t8qBiQIF>Syr~19scd*Byz9@SSn%CetHnxE-;0dO90vK|Fws_l6&F!y7*s^zzPU3rgtI2t zi`h_`lobdvj-Ic!K!G4Hd`!f?TWBY^hPO}@)1|~Ktd2a0!qPkM?8cwGEvWc~*0=A3 zwu$ylR(nI`SEBa_-cRzRZDYcoU?vc}Q}&)&HSYY?e&FU>#6xmuHC~I2up8TMfr6|3 zvkTXbkjhQ{;He&M500J|*#%Q6rg_?B&QwJvOzX5ZLUKSD@IBhpMT2ba=LP2C)QA$K zsKBEKuY4X9o-74{VV&Nj-2nX00o|1@D6Cvv)Ah`=5MzlJi7?T50idg})7IQ<(3MO{ z@`v_xB>Wo|w*EZm^@nVviRfe!$39HhxNcs|CiDmA)(;Qic!qVN6#1VD62C8)sT(t= z@u7@mrVslmHSli9gnEw%PmSq@<1>s7exlq7r}h-2cCO58PJDsIM1)puVY4|39KS(IUesx z+qmGMP$=-pR6;V4LRQDU_el2K&C82|#8ph=k>=&Yq?fr417ud@1d0`I&NepCiV?gV6NEhn zKML;A=YKNsr}-%uatO3H<~6%V=)KW^hzBho_9cG}ht2);ZaLhGYE!~92^m3alFzKb zs~@#u+7TmPc@clRu9%M^0Zag`;+Mf7YwHLj)cO|PEEcO&p2$pp@JfEA2o;82*@+ZW0f+qu`Cu#{O`V#GJ<#({e~H`pYTa;sILDZb$N$`lV1)CJ_8A&x0X> znB4cxWXa<`U4|Gj_B+CW)AXEDwU_VH-GwJg$6;yK@!jTVL9EkMw#uIUY#P2?1PXyE zY7meMh(2l)@5_im8Bvwtr(M{7v9SU*iAV3GQ9aAwCwstBU!T7`KRQIKUAa0uILv*A z1Jo@%)=c?oje3X0Itt@3`7Ew9c4`_% zo7A-tmE2b#?zES+%ngle;DQpLq{~Sdj%yLmVn+%oy_*_fqN1WQ4r>o0Kyg?rJk1ng zG7h7E%z4!Qz4CN_1)Xl-nOR+HU)*1I!aE{vs}=BQ1|}n_cn19Xr>eSxlsGbM2%ll> zm#5G+vb%XZm}VC@*Rt!GW-k~g`gwMXyS#4P>>17SeW^6M-+hGw9N+|x`Id>yxaEG$ z%-c9g(#6;?W^y~_l6c*bY~yUFwFVmNb^H1J0&r-}a$P*TgZ_;_6hs_rVJmXL*K0nn zNnEtmeu@@Ug6&g3D%7AQlNv|9$4kcIzgq;t()sp9YdMzA^ALlrV7Hl4ZNYxMNHc+{ zjtgS`ghut)jbX1-^#}VV#N`M#0YRJX(}4b3zq@t5+y;1*-e=pE1>>#hgn0)dS zVG1`I5br*_p!7G$Wj5jkU9jk)*5{|c2;4#M7;nq%i_IfD za^9sHL%xS(RjZ5VStBxB0AqLpk%n&)dM76CYl!v?YLhwF=PHKY+_Oc3kzFLW>DZTXhO&W?nHoH z@gCk=$!D;N{Ka{7sEY(p_iSv`NBo~%fCj+k@$n%%BgT`J)^sJIXZ4%YHKK=`MOr2% z1w-#m*p7?VWbOZ|q7eK(y&FKYkWFVcCLdVa;`PoCHKkNaiD>OzSXdfE<>J2~I(QeNW`4X_fAh); zG;bZwl_dZ##0&$UJ^n6)EUth5{Hs2U0$2YD8`{$;+3Q^-3IV(Ly8o>KANO&~iem~x zZKtZ|$uiCB{dC~YtQ<=!JgK*^WCE!};6PXR&9WW4`+>6W3A)gevLMi;1jInG`u^8H zFaJg>+mvg0>DjAiL2sdVfUmP9nrh(>sh0BpE*#Tets4Q2vM{8jfQ}#&NBE&wA&cws z8~4U25F?684JiWQV0CijJ`y_St5iFopkLcHW7p+4!+c}7YM7}90MQ-;D6;>^jvO#P zFZcTvGx;JnZGS`R?x#yR8=ztPuNF58+|89$IRG_zzUVJGbqNr&CbPkFr>`p;FYkd9 z+&umh5Ob|?>cf%omaiUwqh4KQ84-N`GpD&!f`ap3A)V&EG)!`4rj3AQZ+4~Q+SjpS znH1L?{6UlZ%iYU0uZ=MvkY*W*#z8*8sU*KXUDE?Fa&|f`_Bz(JqhqvrjDY@#@~lKH zAg~rth>t_BKKf$N>FrVhu!4Z-yZ7TaWkw(>q4+Dupfb7tA_UCDn~{h|hD9CE(l^du zfxW}R4RF=}cb(eziv0nG)SFiQM@y2>ZLz0Q090&kQw(e>NQG=FefSM=h=0%1WlWCJ zUXW`lpnN}UP^`-0ZP3z^G2d`Z+W~jrLe2tSEF*h1uU0lPk`YJO6XlW8KL%|`g-pR>E@a{}6mgK1oBZ0t;~SY547 zqorz%e)~*rcHe3{pc#t-v_1YK8a+TORzM0#mQ+pj=E1zxOZ-1$1SR+RaU|O%@`N^B zpPu|Jf^eHwp3;U#O(1mZQ4U!hV+F{xc_*u!4S31sFcxFadYJKvYl#@+^cr8nZuU^v z>9$U&qTmgM;X6a!_9hXZS-9c{pK+=G_g#Z9rg(rj;1FoC$7a&yY`Vqp&YI%ClF!25 z0dfebe<-v3zTY9NR?R%V&@S z0!;h$V$xoB;Oh&Qg}~w28j^-$xo-29Z;9R`BwmYu9=%_+2V%ii-hHdyXVVi&Z5ttB z|9h;x?GynKdk+Jy(KyS#JjDyNI-`FM=NwEiW(Ru!fpA{Uvkw+eTM1{J!020r20M#Na4Gm?P}_69%ZDHKY>lCIemcG>x6b3%dT)!Vc^jN;8ZOg zM^DYZ3Ao*-sXc2YPig|L`ZuXlIkT!NknD@cvz`@5o%UpQl{7p$Y175tGLW-k`zKK) zwUSx5BXH`uXI`&9{r{RqN*+T3(u!N>c4~hIsQ*6$taFv*{@(!o(l}33wL0a6X$X}{|++v zuT0!O={Ip(D^0MRqGB4rWgnZIL^;vKrXjWFYob_AZY==6E&#)|i~Vo7sO2dxr*-1R zMymw-wXYg2_DhtNn$_dB_QArv0rb(XJc?rUs>PqwD>W5>X@;ppHZ}Rj4|N$CnR-+- zG*W&LJQ^_uFfOLhKL`K)Z+20M@g&wPS$-R7rrh&voe7Ze8}c{>y`blYOF@y&gm~xN zadg0ZHajGt1)|}{!yD^n)?$B0_59z=pXqpF(xB(%`{+>G1YwEhP8k&#douMss4 G`Tqcr1ZG+Q diff --git a/docs/screenshot-integrations.png b/docs/screenshot-integrations.png new file mode 100644 index 0000000000000000000000000000000000000000..7b5297340e72c01cb20f856db41623e4f2def610 GIT binary patch literal 121315 zcmcG#byQnl^e}c=LaR(SCx}g`!6@Bb~5TRuU^$8VLw=)z1(BEDI0jcdWF~b-y3Pj^{4f#SI;FX z@-jMprbl^bAv%BHLr|yFS}$J_%Wvl27UJ?2LRVK~SMQq;cD|qE;z9vfKUr5-^Tuu_ zpMy@umjfjvTG~(~+@$Uoazv`u6D~VoD-cOBF)>s3&FjoS?_gabW?>Lb(zEs*^xK`< z``;K7c5$zR1q;|zA273^$ZRYdx$#40a67n}!h6|mYf<=mxAzw)QOPHJDed&zqL(FG znu2$nWF^Slyo9ysFRvgU!dsK~KGw(r*=cvFsi~^c7%tm893=k{&glm2>~Oq%`w)n_ zN6%vB@IvH(kr!X=j$L2M^9lWfMz@PHkL-H33~4jKjDH(A!4C~VMoEwhyu{VSs%aWo*Dvv36;)bewR6nK^s8V zwBqqOH}GcB%qY-H%pGYUmPn_wze#v$%$s;Q@CSrwj(orSe#D?Y0CtUBBg} zDD_ygx>8&l*^)~R6HItg)f3 zn@nhX%QvmTDxOSG!?LeEuc5740rztvt*cQ9vBLXLuRoJ;r9R#)Yu;b&GcGPJ)_(fK z&e1FG^^Y!3+`D$muzVZF1X-g#N~(|o1#d`U@(N>|Y1 zk#Njg5Txy>_OqmmjEu}&f_`LVB){Ect}2g$!k`Bp@=%_1a3^stU|L{ORPLk`;gTcl z{N<60=!ozLbYjyJhE@x{v>ky$n{a)4y3@#=%t(T;hoW#P!%l{J5!#KoV?fs~;^C?D z$ka62CB<=(u#>~!xSa+wZ0l)5AuM8Mpr(dKBAbN6=9yt%j%r_dU4~k{6C25$qe;y} z^>-~@vTNdUP7U=7WyM-0&{l&bkF@-_^|4M8)eO_e8`oUUMK+peLmMT_u}rRuHHp*F z-bn17+A*H2JubuBU=^YmGA_@Sbz8s~MoX7cho-bg}j(x0CYnu${P_LSl|U%s%vu&Uck zHXh>XdSIw9Y^b%gfv@^zopvH7PXxRVbi^)(8IP~)Bzu#9W8&Ti!!ZPmQdL>kdbSL&A`%3$vX}|Wbh!DQDZtRAEJ>ce!%*? z#Q#Oam77Da@+ue`7y2vc@qVT&I2gQNl2vTc7xiQo06Om{^q5r_x$DY+)eO<39agqY zK36dK30AdVjDCY!|H$)64Ss$U+4B52s{JP*(~RljJ_h;jW}V^C`E%42Q3|7vdWyh} zMno=_L$WtyxsIy^6I3DZbTNijX1-^_&!#AWaY4ysuJc(NYa0}$h1g`F8* z%z$^$^PLe;_hyAkngnU+rUeY<4_s2qkq!=ZT4`=wXq(g2D+7`LOsc3pI7JKi(X&L`9m?U5SmfUhlgy^#V2o?Xpq8oB?iG?92- z+SurjyG4RhGe~ONwY^0e@@(6TqAVBnj4;}$$qfXrK|z=C#(tZTH*F}$cvZ!9)TnJV z@Y5qX0Q=)JRer$5$Oj}+KQn-+th(GLKoroPXxicbE2Iqdy zg*9ZS(Q4KjCTs+yE7mAE4~rrOZKvlYP>M9(F)=dsb!B|lkv3B3CU_Ecu&N1Y{;8$A z1-`6;$sap1k96Cz&2V#!_|MF!nfexVPQ^4&444|^9%h*7k}RFQ`zto5ay^_fp}qTU zaPVx7-zQoY&Dd*a$0;SHD=GE-`GLhV!Ed=;Z0PwNnDy0Cqk|1@`y>3+mHnIUli6>) z-ND=Q=aB{X;IVRpx{8_;;mnmY9K%MZH>+*l0)wady80gnLV9DKwxRC)7QOO0626_~ zMx^4!amfun$4kSL9gsy&5vPUj=XYLblivy4C=;|0*YhbCaOvkSzFk@ummG%dD+0Y} z1Pwzcf!0ZqK4rS$uX;o;;M4rz3mt=6YlGpl=A$FSnHnpM{aHJ`MZ41=VWMjB?y>&I zBmoD@z467H4%ByqI~b@wO)e(Z1%WrIG^=en^@d`hCV6*9&rhe%PG&+8D!c&Sek(7; zaT8*`9|ILQxK*r@=A>^c3WN}{Xw-*)GY&W(xB=7p|0`@vGGTS;xYoTpZdzvtG#3Qj ztKdmLxIu7&BzDuacU}~vDZJwBu^{*Z_a`K5^CT;LTjrwKR z(|uW>bazntr61S2-!|@HENZ32T06L8w>OTM!}@${;9?DOYQTr>$Am9+Yu@HOTdIEC zYJT>hOnUgYqV6>QsylGw57P>9Qpsz|7lna7=+ntP$Ie`nbdNwvN5vksnI->pF`%|6 zfG-}y-e0UAIC$894YWSpYok~exc85`_za4k&*g=^Wi5j*Ir518`xahK+W98@Rp8}> z$g$l_P;u0^(GBk9vJVAHV}GUA8_mlA&42Dp!JZbOYRg*2lb4NleMDkK>R+tCJW#6y z)X>sruXA(M%i^6m{-%yz#dxG^i`}SoPLZJ=CpG6R%hiI>Y&wb=98+HQ=MgE{=Z0IO2fidPb?>*L+^2 zHt80NCl?qrr99o5)7EV0Wg+r=-F1IZ=7F5}cm`rX2eZ7mj@n}c2~ID^Y1eZQBF8Ri z{-m<)q;{Pe_|ES_l*H!a&nPTe3Tgs4;!OOCHp{*7v>5KWCN@tnf3AC|7M0m29ju=@-~?Es)J4N#vFD^O5d1VZoG7|nZXnR+tRB&s50!py ze+=kBBb7Hvl*dL9o~9)^ertvG=(|1fVzy3l#IVoISYf_OUHWS*^an;L6t9#y1nJW3 zzB3yl={eZQlQzauzO(*fKy{w!l-T9FP1I5y_+?Htk#j9Xec`4*^H*(f+T2W_=w9uL zWsXz-W+w1)*D`cmiJ|CYGPI@}+uSTu3(?xN>B*FP+>&n(|AjMc3}MQ(8~X5ZW$5?O zb@L{f(INRD0yG`c9EdoE@74?zBpS3M!Oxx$w3O%Y0OceTCegu{@RU*R1YG@L3)e5I zD}jo+Y0bas;Suilyfj>H+A$b6_@f}G@W0V$*Y>+g(ACs7fAEv+^U|o?z*!GQfsShX zOR)2Kw{iJ2rK;=xZ`T_i5^iJb_>R7n)wYkR!>0iN%Ls7OV&Q4B#PMOC{4QQ(bufC`r|53N%AGGf= zzV&VvPTnHl0j^B*tqo41--%-0(m4-=?<~7^bqEfIeeG@DWS%S2gLVmve*N6;L(_yC zltr%K+9W>W@g`p+-8lY*pEb+aig8u>1-{$}qp9nR97fUEKRHqkvbHka%6fd)rF1ra z@C=hgD?OJ-Jy}VPnkw_IZljL8wJN`xg-l_&af|F8^efAA0xi$1`gen-ppN3Nf&Bae z&e(G#b&npN?Jb^rOK!l#uC=2iUq%6rvib}x-T<+ixZN?E#Kru>9DZ;db&ca?>BJTv ze(2@*X6l7Z$VPS{iR{S2P{kQIX)KFNWL`+=&69{ssd}EJRSTbR%bQ9rU}9g(!CZwS zoTZ4(kLT(Pvo{K)uWvO|(ky~9D>KX<}DsNnDwWUB+wgmJ4WRY$r(=-YnW_I;{LGY)NXk>1e@4 zl)THzxe}MTS*j~g@tkhLabGP6y2<$Uue02|I>qNDi(~Qon<%=)D|VZVGI0)1ZaiLE zuv6=*??$E}xPujJ7rSpO%G%qBp5lXHPZ%^p8H3{X>bE9;>BEqF6?{jq7z{>LjV zR&(=O#>NcNPq%5r1H}RuH&rGQMmI}k1(MPZmpf^i$w(;o%bht9ue8@ZPVp#(o$BdZ zAlbg8@09Jl_XOJYL9?MK829t(7*)D2vE&hJ`n2Vkm3HRrYf7#;v-l^!MVl;@aP;ZO zhu1E{bLelSdG>x|qYa`d0(Z$^-#&gp_<(fM&wW?iqaUeo@v~ zgP%L{P?CH-mYtg-ukJ4^$pq1wU3(=~@6MB?pQ;P<+-6i*28f=NDT;WgVdfg0eRvY% z&!1m&ym;!<1onCpS9ZBP&LpZe2?c&N?~m;PIXi(*g*$=p45D}%`bfgTcO&-=sY|VS zL?xxexe3@dRz0@+i{H|67mvw)eI=1vPf`3AyzeJ3S3Q;+iFcmy4;Po3wa-Wx`c{E6{js(FFZu5cui)P*Gdr9*4UfSbJBpeO zq2*@>9E8J7c7#>U1clYD@6p`*$+k0DD}Z<75PJU{tRT?#nE`M1u@a19apUTY)1Ib5 z!_;=tT2@?ohdS{YD(xztU!@>OR(~tN+t2nR?+pT%Y{RIDSuf~0u=s)Z2qN*COi@Qw z_4f|>W=~72_bIm$wP*bmf9!&D@#AeDKL1r&)o!DNOA}mO8XXk}o}Qa)$)St7D}}hp z^2ET;v@PZRj2d|lD+scU|1O=dVCFt7Y4OQ^#tqoU}A=AbP(g%0~ZSs12a-lWiGLl+s(tkP$D z(fD8=HzE;SHH(08J|!|n4?73je#2_nL`Q{{c!Xm+4&{v%8q_8n{d%|6v8w(%Gf{h8 zH*mBlNvgNB<0Uj%lA@#|I$y9#6ogNY-PB?GNUk8#f6eZG=ILy-`03*36VXg#l}L^2 zH%SBDQ8A!!%U$(ZR?rYX*XQ^ImhSoHN)fm#b^lM`YZaC9p`Ix?miYZN@29zfiwlnD zI)|mudMW#40!PGk6I6sOmk|G-T|uzJ=$GdQ$H13LilIyAzBmG>sIc{dT*nu{3(@hm697C`9o+iTK}QTdvH<%#I``QaHC_D}B6NT;$mst-a3Bc9S$> z9_e5ydFMQmp{@y{i?mWQ;>itXX=}8m@or}-aIfSXF=9N#7FI2azn>}f*?C{SU!a15 zbQ<;E(xDxbbP4LyGkWty5hTaTMrqHva%uLa5_$}RY*PlnJIp6_~*4yg}^B% zd+^cP^@p96#$O+xG5z+NrizbGZ68;;jOx?T6DSNd@iFbMfB(5yjv>$uFmXFq@c2mp zB-1#$9D_=ajq!9^6Izbxa+TEmQnPoS5ROEF?ya9c>-W*TPqCHPm9%>6b2=?&*x39B8pR+)ngO^^E^XM6S zm(=qUi|~?C5QBL=JR8Xv&pfrKtVeA0jJYMQPn-p2^vnVrP^Xq>n#`mZ9Zx%scpsQh z*EqJ^ZZCCdN$W}@^LuL= zyu`B#S*Ba-0)n|1^alF+<^|+Xjy|%O$TwCD=v}nJYNcvlIN>Y71xH6o1$fLG%0+&& zrvB#&i|y`dIJ^vP@0u!o>J<{3=3?pzx*k5Et$|5R=^&?dNfp+m8aeFv(zo9x-e!>z z3@LxW^U4n1SVNQp+s1Rp2rh%;X}GY}Y!-?gv|r+n8ta_#2;Z1#)Mv`)ssJnY?N#`K zoQsg92-P1O{Y1neE%;`UbRx*+?mPIhba1$1?I|G&bjeK8?Li74H;_rpg>mjrVUh@=YvDe$8N;XSu0l+Ze8hDPs;j)@3wp&`eA| ze^H`mgX_FvrBH8~N?I+P922w-1tC(YYf`UvLX`eZO1SWyvNr?v^3bqEQE7XDNff6( zRUP-EC0WMi9HsW(UPJD4NNq-m%xIA@Mn6NMR%mp!kmsLA%Ulj^+F zX$(|;(?0u?qE?u!)ptawB)^ zKL$vAH-sLex)D3 zJ>m4XxKJ=tL5lfxgkFN9_HK<86LR$990ohMCL#1z+Io9c#Le=V$A+ni*JojO{@no$ z$(2W zi0VSC(rqzIpPqZEfZZF1j#;W!_&yvdez>T(;w4pT53`Gh6uE~EVmf)SBxviU+Mcah zdDhWXNe(fqi=CO;ixxT+RdHH=J=GBF@uU>aaI(!Z<45=T9U^}=uh@veob8Oi(W2CkEksXM5S zG2*M5dwh#aznM|muYcuZ-WOD@KBu`Qm*iUi=^og3##Ui=u} z^WCBA2p@bB@+y&&%FQUGB*`qv#)kC54}h&yVYud-{}*j{U{QB!A@yD*RARS|D)l3s|XTSXBCpeJtzV)+Ky9nD_P7b}+S_ z$LOuQ|F;dbzWT@aBqVhvy5`pRXRkwvRu;(#dbgJ?xO0QdFs@r=V5MY@lUdr5*bP+8 z$BYcDU3`ilOsqlqA_``sP$mt8|FeRQ@jl}>h8 z7bTSK#_@3@!zp<2`7IXPZ!SSTn0GEJU7AwYxtse_y;h^XZA z*Y^`4Gm7B@nTFK#OJZ!{6&BOJ-qa@Zo90YEjZr?4GzKu!SI&JU>j>#dV2O$h%rOk} zjmwI!@3t>U|Ai+XK_}ay2NPpl>AKsagshYKrsD_r#LI)E{jh-5N6=FzB}4Rlg5{ZS zZfjszBu-D@zryI9`@^cH`5|FHc%qQE=ryN97_8ent(4qCa1N0q|MAkGgEQ$tyg}Jm z^40h6GF(Zyt-fFCoickuF<8;q{#8v$6YUCHY}QNIo3O&XQD z2i(SxOAsJOPHhSv3lz_nP?XXRJaefDUIEAWh7aW&kMKBY%HiChS9eugRot zu^MLrp&g+a-n)goK_HWB*JESg;De%-I7~)sy1VyjK^Nd~fz(Bi9|znrY{rjZw3FPwhcuw=3}Gi@I9nDQP$Zc@;n*X`(6U4bW+ee_@!4&XC}Tm7h(5ZA=Iev zsyJJgs&ATZf^nalIopaTm+&_^Hb~1wij3?qfkLQ}Aqr9Uuz;@Hza{s>CuyIrGQHCmu#D0tHFzv^QR1iI*3%WmNsZAOwj!}P&hIqI+1bqDl zZQBU@#>GxWYvCFHYXS4FyAKw%?vV0!qm_dN<2A!(;Yt=_tye}CC-e4|OzgiejyQM9 zXCS47ht-q1F_$)7zs%gC(2?5}k;fA(^`3uI!Ciq9wFM7?4=i`fMEdQQJW`M2fB{U8 z5t}T-l>IlxOTTQ^TqU#tXpKJyQLtX>R*1EzQ8C`Rd(0tPtG;VXG?c6R!%BAe(b@M6 zG%Pe|98jz&WXFIOGEOkFOtzL0D#+(|X3(fP_fq{LtxrYW<7<1k_^f7F zzETYbgkkfL85b&eJ=|jQLwt6;{%sh~<|}s3z<8?_q$n0(VV6)LV#Pa^bW(0(&e_r| zuquj-a`3Ii-CzH9ThiR%M0MJl?zUpHrIWVP1G8?-`ooxoLXs`QA zMk2bn8ndBKkMj^et675{fwGIu_z67|dXCPBio)g=%iQHTk!9==E3|>qW8TLbBNBsY zIEX?TL5(iU%6mqTfVYAjmN=yt&m&KdZpN9cCn|z#IT;2h;vR@WJy2_RO)*azq)oVQrJf@N%O(jruG=7_kl=l|MCv^&19u)|3rlXRMRtd zkF0Ry)8Ug^oL}uq^*j&Yb>s7LXs|Pi*P*})Ic$4rRUX0KHcvuMJLYM^eZzl*_lVL2 zoXI$DSXqmIk7zj;eQx~jyv-!$k)@-8O=Ck#REjJ5%`UFq^$1eB2E48vu!>)G>%s`_ zJON;KE90Zfh6vJr+U#I7U&N!od1=@$*GGKoZvIiZDM|0YuvU|&oK>Wh)Od$&M1;UX z>Iqtq=!?RwyF>Xp`C?5N1jkNA^$Vh6!7pX=jG`9({RXwGh9m8E6pmSucZrE z?V7hF)hCh(!Hy(W-3yo|6TE~?G(FtmyZK^(ikkys7h8ctE&UoB2V9eyN{Xz@*|?F`uo-IRXZ zde%)D_?(q4Fo%SXjgp~5eD5%QPZBp>^vuaZMG%x_9MHMQ5+JI|^wRm`)*~bMM^dY^ z#i$s675o_w3pNWCWwsx9&E>bq-F5Wc!$LwQ!Ytsa;n_%`l+qn`Lzfy<)o~N&QAl6o zaqkd5iuto)5MXY3tMwptfbXa;&O5(BphR`>Od{czu@C)cJDGctKU0BJv8&S!AcxB? zd|p1oLp#Nj3;Da_X2XO|SSWiQ2I*GDHnx}CqmYv^RLtf53E-O6Fi{o@&B9_#3gVG( z+TgNGWmKXn4KUkk~kGz!pf&LJ0RpIYEEnfE_^(OIT>TK(N zhskr_Qka^sY5i#cmXR>z!ck3m0-7oldVVGYO&?Vh2 zH_3|bqD875yaRQk8y=<=>`e|rIu(K@q4V}9QuZ_Ns+wC#Iea}+$j41k1!0_sRZA(D zf7ok2RL1CMfDZ&sHoPimQ98j!@K70(CNR$hgQT61Ze)y_%?pVoO7NR&#fx7f z+%@>|=`;Pt6cfoTq%V6hp;%dW=6^;%UOae07sov>otrW^+=Rd2B!6vF_#*6R$eo57 z9-lZYL)TBq&rBZX_Gh|CaWUxA^TT6P#RDrZnbsSMH=1AU^UZ3wa5A|KJ_(Q_>dvGv z-{0;OB~5rP*bg|HTd0Zti)G2PM+|q|TyX3`AcwIy%xgc1M6%W1>9fs`H)xBdrF>?n z*ZN2r@uQ`Kj)=-{eFg1fxz-fl>%y<#OOgc`*-OXJb`f^C;RpTgfjj-y^v?a3XZHg2 zvA5MHp;N$3wAJ_2mOYRA$yI7F{*mJVB-1K(T%T6_J{`q5qUd8;=B~WZxX3Qi+O+wU z>x796-Kd05jMGC@cxQx3Tl#g%MoSWn&>Q=lh-pI6Gv(7fNVH(c_sAZ%GbZtJ5u2s0 zfphXOF{1Rm80`3iZZv#Vrhk+#z1=DW&M`JZOj@t_mIL(dNZs9TG{zJV72bugr}6tW zf_(Wyey-_6i-C@5OAURdimCM27e>6AxplnJR1Bn6DX~38$1tS=z6kYtY||-aTrZxE ztJLST1qhZP9W~JG>Kf_d)et_$T9-L&@6PH0sVkad0@z2gO}t`Ljp9v@jLdsa{hkb< zSgUQyNYlhTaR}a7dHk@uu&UiRxu0p{b$AXb9^F9tHS^nIQ9x~f?Hxa!SQrM(Y`f2D zH2(?(KO?%$m2vZgPZ)I123aESm3D7Qiv(P7uH#HT8k#pAL)<|0v zl4e`|+$^OOLf3ru5VnovBkxQHpd3{?p+^F(jKYkyGCzrYy8xlk+y=9vbChht$Yx4}N05s3s@d zsZpZIaI9?qU>fj*28QSP?qrafrM8_m%ei#y+fqt33DMM|U?R$Ry3#4DIy6qY;8Nhj z1d*Kzk@eIenjm?X)+hA4vvB7h-a`c7!ZjWF^2eGP4mm&7N#VVu1)Lv^cvL zBqTI|m7+!7AWla=8z#6j*|yVKR+Xt|=5MeDMmU3?a_n|*zPhGJxOPln9oT;}wLM&KHSh{MjaE*M#f zY)A6N<?ZawDa#>dND0Yi|Pf11*FvYIPewvQK%i`~pqzpSE@9mEWOCX->#PxPy# zg+-|fJj#CCScg;vz1PbueB0h47d~Qj(&N~OZdVZ4NX?6aGTLG8CHlzJD|3rSFO<@q zZN-Z-RHVH+WvLD@*^s^cSWM!8F~eNs!Vg3a^su*Q+DZBwOvkn5{q6ZxtLD_?S`ILb5;UVfQ&)0Dlw!OCE;Xs%+4`L*EIy2Ef! zTO3OoZ*6yEk5R5;sZ?qEx~`UdeR%Hwqlttus8xuD8Ri=Vb&cRyqeTrUS07RtMn~1Im40!yb=LG9Z)09*=X-05^UpkUIoiC8t zudZ!>G-$g4;Wg9^7gH2x+wk?Vtb#xnrN;JyFJ-%H7$;d6v|#;{PB@K(IK$tSaf84V0aO3$##nQ$Jqw?V`A`URCp;#HlbUr4KOE+Z|bEZyARZfrobfn?#*aPa#5 zrnwYv)BEAeO75PWZZ|||v1nvoH*3`>p%iL|_Vr{u6ZgSrRe%ai_R z2jxi|#gvZWmZtoD)OBKooWG}%mfM5*4XVTRIr?mNBpgTihp`-8IX)@crl0${7JWVd z#)M1%!M@p2i+Qgb+a}E&H*t7CVU-lOsrWF;H1Z}b_4U&Z9Zz9vkO z5#zG4_}*t`N=~1}hSi2~KLbfO<`ZCVe~3hej(>}*u=wh{#A){)SgyvKc#Ey~LjpN- zkUk%Y0OKmW=sfN)M(2$a2Rqk>xLp>xq}wCo4{nfR zh>&}*5)VSgpI`-@)kQt90@ojo=2vQp-aCtM!JORWzU9P*YO&qQDLr)Ryd&b_UA&esG*T050l|DB{#Hs^L$(+)s=bxV z$-1KW4;xbyie>Yrr6G(=bW;n77}%tl_*I`QDhtIY-`&3eKm*J|yJ4@!yGWGbpI&2c zZmjZZexn4Lo}9HFyrQQRO=U@gE6oCCY85V%3w1L>LyeJPl6B{gZ;o5tlkNdK>ok%a zldk&YfB_sevrq@1ZUwv9AaH5aIOt;}gg&9o9e<-vs5?!@?&>X+IR8yl_WBzAX{3P9 ztD@LU_o1|*oA`jNnC4Rv{SJzlw$5VH#mwe)LQ~Z=sEZs&a~o|_csm3K1j zm$b>L1|o63n`v9Cq8i1lUZ~ovtbOOd83KTf8qL0+Us*{s_M}Cc_Yun9%VeWn z?!5mY8aTE6yeK9oWUIQ8*%q8nOS4LlG8@ z)4M7{pB?iSmBaJbW~3KSdEfR#hX+?E192O^D|vV--5h{&mmElP5c4h{n??TizL*%oXvhr=xRF5|dQAq=1RNfud=ip!*x=_lU_Iy7xUtAayX#gT~>r)kHj z2L4%M$>jVaI_kPDBZz!Z$e5lG=f0Njlbuh6lURR@BI?ySOFms1i+U#h`tvtqnYv!G z_+r|vmZm6WYz%Ec^WD~1;S1gXrZ?96^Ro*U7P~IPXA`hz(irUz9=B8G%y7rW+Cj>fz+M7aM+Bzltv ztd%rw4yc>c3eEr$q+Q;qe|df7mtl}C+&huUwF@P7<#lmTAM|tM@!LG}kVo;xI~7=Y zouD7XQg1)IG~OY$sZwt}t2H3|&PViTwEZuin9}K@H@$(Pn?vNd#96Tx5+xe~N&_XdEThCz2oduZ`Jq;K_bj?M%#`C+Vpw;r(q_Hfcb{ z=y5dJXAV1i-*)YYO?r~Er{dE{EGnie`vK9J!XRu|^n97G~g?n@#9GHcII8I90Nrdbf-rT$2o z(HJFe{S_nKpb@brTlLWX`=_d@=*@#|j#v~2NuBAZ)OvnctW6xUDNSsy#STgW0Y+2~ ze%4eOCG1OF+`k{X=U?=rMcG(63#xx=tp&qPj5cUqTJtjtOgmcDxNBnZ>q*k(q5%yk zFWGjIbf&HP*Tw83zn(|_Vjh%{sVsIfNA20|s2zjKEBQ#oJOl@EVkEKOh7v`rU~C?`{Wi*at-5=h1|Jz44~y^b$D!A(VVGIi zYF|%c%R;?!bI1{HcQqeAd~oWha5?z6n&5MJsAeP%}16@ z10Fr<`d~8g8mCPic6-U10({CxIf-X&cG;n$vW|j*++~UMqY(}W_ zMK9bW@zs(_Swt)yD*i;fiXAn3!;D=(eEMmR2&cdWg&vnqRbP1sfG1BC?r>&&&ds>= zPdid1zmn{2uz$O7I_?~!V2Bn;Y82VSP!2|7iZteIU+G(3`uu7c`{beR7o2w=6p*Hd z)~(!pltj0^;0LHVJqwur6Lpa#_3H$@(kzT>CTgE;vGKYx#Y*B*&_gUh z5yu#0Jgja9+3>g6Bk63r9k%u30ajQAMy{O$QLU|;H00(sFObV!!|a3Xu#+o+$z-;@ zyw3CcXX{^V0OWOAG)nSyY7WUgDBs1SZ{Jb>dRuny7&wXuKx*b6dyUumJlR$|Z07F! zZ=q)r9z8$HwKd$YHW{=o3151OH|nd7zv?V2xIkw4VoSdGSUp_T<&k&eMsUH?2E*BiI9zc zppf>~I#OIYdhYeg&P56c+kdpZQl?9Aapcn`21uihirmMe{pG-$LmJ0LOD8D)?2NgF zIrMY4l~1{k;Gpw0n(G<^o597GJK%Rbc6|rb72nd@pJZaMF(Y0hCyKS%vkpdee~!qu z+`#K6v+Ui6$LL#i;~!zrNz&~B2>S8piM~Dpk*>-V=;)g7R>>bwYx?ePKUbSeG%5|# zE3zsrtdZ+PI2PlRua5+$s|fbfC7&g_uPJ+zYU9&&|3mD@yo;a;!+;W97SsD6A5!-N zw_*bOuIu`4-k5k$`%xhoCEG>Eyp-SJNB`>LFNUvYsnL{>WwGBY*|XzCHLV9Nmpa^R zw*E_s(&GYxzyOVWAzat1p%NTePVyQ!i0Z4O?QgVEAv+^facB-aF?o-?I?++?En{;t zX+FfJn`2T!iPO_~as=_48}x=C8*1ujjO_HxMUrDVXls&30N7d9Tf+%ZKK`{Q+0 zz~KHa|NehS!N_s!s&-?;Zq)7RXqv_fRvbmyt~R%Fe#?Uqmi+W!bwwKwFaFoF)XDE> z)i?XSFLRZHp&hqj!8|o6jqMLxpMB)jSI#x1;pJT8fRTs$F`iw<%S7JNRLt}ki7 zI!Z%~9Wkml?@ay92Xw3HX{wrcJ~+7WONB3>a`@R!X0!RO_?PA3pInqx*2BQhtMGDw z1|M?iyR6C&mWf@~)0UKRDz`R_p0s9x=aD$nBvJw%1?>`8v*WtP$7WkR zS;#%$6}u6A(tCNJ1Ai7!l7I%SHOGkGRE-q`LX8LdEOFXewc_n#zpjgpvoQZ$;V;8W zh=n(`oQZuaRj5dzt zcUjvTNH*45=5O)tTjV{dZc1eEDMPNhuieVK1Nka3o4%*SK+GdRa5^r$hyv$F4j|?Tvm{B;A^118;X7r^yuKWPK{9+aqz%s)>-7F>g z9$~2BAz!ECkhPn#^oxe?86(n)bMkT&vPV`YLY#AWR#9c;Bl;cun`%XrnVo37yXaO~7iXLM*qF3PUD>Y$!NK(+FDj|d}?TtpN zSe@Ni$RkDsmEd-j`?mE>`JzX!3QOj1U;Z1fG`AqWg94Rb1lC-J_353D_eb5z0t}A>YB3eHW_(LcJML#3f8>=nRU2H|UZnkUQ@f~tfs-UV zh(hN^w2OW6mOL)lexy_UB!lA!hAor^(=Dg&DW6$WL@a11n$ta%=JcAcEVwtWQ!+@A zfjIiF{XO$=v}UHPRsSaNMQ*$9kyi>kMEEL?UFGAL&vbk}W>H$$-M7o%lvZ{7ZMeDW zH+x(=^#}iWN)A8u620L6qV6rDvh3RRUj<1?>6Vi2?(XjH?vfUe?(UXSP-&#Q8$qO7 zLb^NdJ$csquJP_Q{$qbzWAFXZamyX^x~>`Lc^tn3i=Ax%dizt$#_;~*>IXviXzYzJRR=W}tP3UK#s9+vOC^xjOQI%G#h zL3)HtC!=aD)<&f6vC}`>+nUaX=?+hWMB2KGSKfEWQ{mE1B65rRpTPc{`xD}A$_~r6 zNyN;~jLm|!1cgE#(uGV|-Fd$G@Sunna~gEI^IpU_pgV?HnaFz5k}pPVPx}+7ql@DH zagTu2{gr7OXy#16#>@P6H+c~4w%`+1E;?HoQ_fFTemXClMv~8J&gn(C&7+9}S1BBI zH|O&6#KG}1ZUf|zT?4-BmCz>oqfP^UX5U}#qCh8!VBv0|W>m&{hLA?2hub;B_H7XI zQ%&Lav#$+xz5Rl+(L7IFnP4WK+>bstec9=g*Kyv;VtY#O+&0JUrT6z<9k0IWqaK&2 z?32JKgyGc>AH0{>t?16acUtPmd3Y1~?C7y=f5QAo)9&yW$umB>uIW~JqtUd&f%joV zJ0I^|gKxBUKCYZbs*m~@+-jOAG>bh{wFT@#9rC>*mNinmvx;eL1Qa8R?#Tz9T<_h@ zE{2`baCDjS*xZE6G2;o1Ll)O1UsyUU`mZ3M9%J<+1frF`NHU}UXv%4xSXLu)h&!l;6Q<)5mtR&Q~U^4^TLU?KG`DsU}>ZUQmC4N~_T zM1B+aQ{!1BavHH@xEY)f2bM&xjrl-_9J7Z}0zJ$zsr=?`$qHRpQaH6QFY$mR$CVH>Ahm6Sj%skmL$)|fyWj1VF zgDBkn)hXJxL#ef}LJ0hNtzQ@?a189jrs#NmkeT%w$IFqHkuSt@9s&>EJsQL+`?O4( zdBK@MQY>nyM}aMgVkDws>(+37Bda6!R9xT*O#X`-;wnY1;MXrXE&oFc*O|?L(SX@2*im zJX3iJ%ih&WOa9Q3p4Aa`h40p|25W%7&VavUzqzPGSX(?*2o(~^lN?WQNYzHW?|~tM zP39jIf~%s@9G=CFe$NgsZfG&_IiP;;XaE;#65Vsr-O9`w=DK?TY5=mmXu|5}Gfya9 zoZb~Y!9G#YN;=Rsk-;+p-_8o%O4}7(e7S>SjrA0x4@&tUt>xB}TcwRxzg)GrZBqKwLKd~S6#^PWZ%he9ed5UeK zE?5KDrq4gy!>Rx1bKR}ka-9%BO*?dbYcCc<4^+5Kg-nM znjq~hLD&`5AJ_d?ALBEkn(Uw6##GK4`T^Jhe+NWRH_!J83&QnEDhnMW-bn*>PJwIH zamrK2iz&GfSgAGoGnVc`uV=21(DKC15dET{zON73iEtS=TDx^s7LXC6$`RO@7;@jB5sEX1~YF23D61b4bImnzFJr+%iHRP=qUw-^{i0NUeO>7ub*dhKNf<;&)@i z!=(9@2h|VB>}A(w52{cqMnkGeZ^UXWX-6kyY{!r9DueuCmA0F&Os`>7Zt!m^=<4g1 zCAM*JAN&*>#;a5NEdOI9?gdn|pb}DPU^sj{CM-R9Gt0?KL)1Gubl!p9A+IuAijVEm zF>#Ww=}f{}nR*HQ+Th1}g1g>gc&(OpmbEr;w3@|;+$~j|@SumtYZNSXVMGSzNMFm( z2d?k5e%pKF#1S?fmNfmOD6_CdaQePckH4HTxp+KYPBlLEbq?&+fM!lO#rE| zW`MZ2L#x8I-`PB5Xfhxs(b1qKghTP;End+NV3&A_AJp~u#Iwz`FYs#W0%(=zPjo9PSx=B~ ziEaS(COOyh20bD&`n-Z<-febQtIjHQ)|fg zQXOj&SPa+~n#grcM6>wJ(7D(@4dr)tQEigUQ2-oI_<8~%swyc_>QaLU-sg{~f{6uU z9kuy^W|#YmQn09ke3t9Njo`~7afEFu)36LI+Ex0y9`St9`g^?k?Pt!*AM@U61d9au?MLR?I80V*r{1|G*FR0muKbZ#ZtM&RBuzo02+n}L8$=QW_a*=#1!30}WH6f~ zwVv$}^bcOgPQxAe-`RW_oRmUE@}=+HB=SUJnV8~YLO0K@^Z1JH4@XiLAV2$3zh^|U zRUwDH4{< z#SD_RJ(jgFe!b2gl=IqY%&8-`rspRcVLLQ5>;ksmzPl1v<3LfR~PgqBUwCRW$nCKABS`@7>anZEBQIL8TpzjB;J5rdl% zW41ntzN!9Jx)tv#C>(C=d*{}4!zZI%=Yvi`zT+i5Ms7$>!abk!attS@nwl9J8nw#ya^7rWaTg6MGjGfq{|x@hBrEOu5_dMrC1T}G*s{`5aO z+>iU6?WUJVHbaL!@mS@fyQs_PhMc4*WbzttI?!Uc;NAy3@r;M@R_L}ZU~&(sju4Kp zG;=vFQLtH#*ZcJe-fpttnY<#L=o|}#LrVA)C$MmU@u3s8n_yRt&#eEu>piP3q3Qdp z10K%rNt2ogkfNV|R_;UI$kbdaXTOxlwalefntjGcH+) zcYKA5EXLskJC5i+eblH*88JHQ;IX*4c)>F`+%5|}r>C97aiwR6^MUzBNKU4iuw zGjk^Lylw%Uze{CBx!;^`qiVX08&^AGKjqqrzhyZ8VK-Z|J79p27)Pa$WiK#7;`h)b zpQ+Dj!^iHh0MKcFKkyCcR}GKE;-~$a=NGBjX#-zU*qgYA!XxrQ;4)xhxKKil4i|!g zyYId~gY6WlHS^uTN?htBa;+)Pz+;zzOFuwEoO~I1_GD8{UrYz`&6pXVv}e?QLs)7B}N@Da7EoBRCthS zWmIF>JDilVSsna)$>m-lbx)1kb<^7W&zR?k_xaL^f9BE0rvpXiowu%c3 z{{5RL(PSmcUaEUWQ*PMDFW+Y<8SaZ9SkkLyZd`T?OZL7hisKo_v&u82`o4W<+GW^7 z>_c({3)c- z>GVGe!UKUZ*&&IM(8+|E*Yk)v-N+-+uVa!Q2d>^C$X$HnfXD5yG$SwpTP8W8!MFo@ zV&kL-wVJfpYpqa10Q%oQF>(g_3n2JyNb}S3-}zpyl%-$n{qDIUCx4${CJ_|$NhJ7A z(_Y{2(qgK`lL>)y!<3zSw#w}|*ZeTcJt;fgF4T;|3k5z882t4LepVO|)G~fp72sNe zzP~>1NH@o;t_3zdI#{o~#KFi-=#e^P-_`%FG6?z_Nd&sqe(H%bN5BlN)EE(r%W~^|WtiNCcq4 z+~LcD{|~peS9p%H&Bmn(8UpO?+6U0!t`>zh32FFlgRxz_XZ^}Z$!4-96N~XTLLOFy zNmr0Wz0h3i?Ya5Z@NZ3F=Ly`Mu`CW2(~@ZP4+7;ou_kBVptnQKjMH}6EQhDde#r^A z>0<;H7NB#K>op|h3AD)VjOTt=j?^IJ{uwZSIB7rKSm0fc)h6zhnMSWeoBTzd<9;+H zl}=OVSj$y5s_D2{vaX(^yE6Q7c=vpK44E9ld>Fj$bJ6V0ug}wMAy(eM0hRyIJi*g{>-y>6)JK3XRQdk|>G8i{lmF{L$^ZS2@xXlv z79eh$-(OR6-cQNYfTsqcOF88>G&YtBjJoom#v=&9mzb}1K6?TBwv?0FwU)7jF1_eU zIXQ%`^9~Km=KV2vPXgkthLtfye45V&)!m;3!ONee;)z}V#BvQk+@3$`-5LvBq4K{Q z0dMDcty5>67_eWnkDyg~LaUZ{?9dwY8ZyzW32mq+`|EVD^@?|1lPb~pq9!@hZS zrg!9MLD$3CVt-WQo956jB_X0&yG8t+Io$&^s5qo&|Gdpqain}6zfONV$uY!YKr%nN zka{={I_NK8TOu_-(;rm<+pC4hz2}(-8BXMpQBE+<{RgQ9EVabc0Qr?EFg}<_epv4q zMAYH5+I|K$wYsE@?{`%pX;C$ofFc$NAecPQ&JjQaLEcQ&GE4;cE}dt}7a*2ZAmK6# z=LI}@)6mkM9aldlz+bHZ=mGUJeF?y2Z)O2}?GK?|G?8d0gHiiZd(|&RJ_UeuA$g`| z+S853#R=j7vMUi>@AMegD&hO4?JT=uomxu|zP&I&FdD&c?g>Y2$a_)$j2`&zygyx* z{3t+0HQ;!Rc5uVgdJ@!nz2bYelOHf#WzzGYM>{-Y@uxr5b%4lo5Rekf;o{zV1AM^9 zvmxad4>AUP6IK2PRq!t%h~Xlt-mjIB(b2kz^w0^}4|Ali2+NVd(XP5Y9JCr6$`dRn z@(v-t!~dKC!ksTrUiw#O;?MF{ShAi!qd@^Dw7P(TzR`HO!bI0z5c1&m3`Q&goH`F9 zu!9UINvq$EAr@$6|4uR2eJm`8Q1S4Zv9YuHM(2j#W{TpT@0Hs1YVqV zo*XrNLDOcEoT%X{V5m6c{}MwLpy=!C^B}tJb2DpmtDNNLl0cwPy>l}*X#9YwVCeR# zFK$M67q-mJao7*ptj*S_-##lg5#}fQ*%ahsPi}=%JkqLY%+WqU#YQ(HaMP6SA0BzEy6xF zYjmTI9H{JgvJEAOMFdBe(@F+%O3FT;0`Jw=*z_lrvF&jY7A>Gorw}J_rJdbgs#LXJ zwM7<6s97=|F|(rA;!(Z?JgJ=Dr>h4BQ5~ysYzPJf-B*>0{j4cAZpm3s-|(&%P0mG( z-;&Mu6pSa9+FwJ(^`0(!uFhk_=^}&WjmgHw<_ZQD-|z%njA8K9Fa-m`2ymPX_74Fs zE3)f)D9%DR1iV;Z&Oy<8y&AB3)Z?=9Tw=f3h<2cqkQ)alP1?)-*@nzRP?Y;EiL>K4tILeq~2J7S;~Z5@UjP zel!8Cw5UbSz8QeZ(s}x+&pnsES^)y>4Dikt+#wI=@#W%`?RU1>)rEeJQ5)~FF5na? zWmZT5IyvGZ>=dDTljCd;Od^(g%%{{ZVBf+Ph}F$vA9_U9FmyH+Fy4Y+7aYZYhUWdn zpvB{fW1Tej7kpHrz7CqxGt`y;YR=5{Y=~-6xfGNy^p9H46`OWHXAgTrYgOpefNPv9 zumgZRYPwcN0gsoO)42gpwk{SwZ71{*OMD zG&ikagwNAbQtWX?>TG_sJ_A~3&EMVU-g7Wb`*tHxdCh*;QESev&2#u7pQ+lsLg8FF z@aGiY;+P}!qd|j74{W8%=?c)1E!m0Z&Zv5t`o#iPFKK* zwPb(k^?+GEcphpma`Z7~Jtie^<~=v;TRaYReHD{W5AT#`NA3x@os9?Gc;&uN^zjhy zY~#znyU3E)X(qZhMJ$-wH(Lhe!GqIro*lwtFt4|PnXo2b#_GXvekV=(wRfavw}8`9 z<0Rvvnc$+XT;1-({CMt)Ha3IqrT~}}+JJ{kchtKatPCpvwOaX}FK*=YHrBzkt={@9 zfpL-{YoeP;GQ>M}uG(Fvv`=PWT(IA`&RrZJK3n^lf4KMEoc?L+x2E^B$NnOSKwldj z9qo`u9{Yv06#px%PSta0A#0YDbF;+$f*od6cG?Roaxk124xM%J`~ z`?lKD@%(`M@8GuiUSBh6t6|y2gc`IS8qXG zf=M1Dk-+5}XN zx~X~C7b*l42Z+i^|5Ee`PGis;{#hW_r;f<7!=YxP&D0D%(C4ez$ejD9sI6Z6ETK@H zs%{Mqg>{3zE}~^*z3pmY5~&bVbK|-+g2$i@fNbEDk$+*w)J#y{%8dD6|b|0rD^VPKiqO z@h&5+Tz_k5$uu}FKXjy^kNj)sziCz0Bg3wXwwjLF#ptHxhy?s5P^Rh9h(9-~GrJJ@DP|ZHM)X_W79IS(@Lq*H@>cm_@EYJ#Fw}_tzFE zkTYSaqpVkiuM#p({q6qf^&Ve?hoD|Dc-0U9Gd1hC^B*<- zIY>bS&|2yYegTK{tmeuwc(iT@P80E3rc)r|;^A#|JnPD?1rlDmKUQP79 zFVt`cSgobFt(%IH_JHs}>mD1mM-3V3AFY4QcTXi8L?;rg{nDUu)kSSwq` z%yZBMiPL6!&`64{m?5-(BHhID`fEk=Dx(^^@1@&%iZ=^d@nlWATPQ!IEB8Aah(krr z5J!9P6>>+QkWx$`ultP)^LCZm=c?%V#-qte4t#h%t>&k7k6W2N_b~ND^}K zY&&rTl4Avwq2havsh>haRoty5Y$ppt0~`+2FPqRDg(F^GuRZ1Zie*eK$Wp#;d^lxi z4ccki$+Lr;-_4%M5a67$$UZ$#{Pkevd{{}ij8VV&EUs5>GPLg$=Mq5b_h8R_}5C+CAU z7840s;t|pkm-n*6PsMD`u{12)957VH7cr8uLJCs}_m5DIn_kC`m9BMH+TRze$J>ZF zQ7=`gY4xmnZI2V<|Gtj)Tn-FV^t85SUwoyS=_l;adt%=J6(^b-bW>Us_w}a}jPfxw z97-J)RIcbZD3@DvM%cfdJ>ny<3=h4#&C8ca0mH4<=A4MAh=|vslwIxy-dm|285&`{ z?|N!hK}Xb)jeDKwD1?v>+&f-KAO*mF@r`)Li=%WqbK8&L6tQxZ{^?V<_2<-3K_`8_ zN2I@IBB2vL{_$5$G*M#D%X(auf$8CVn8UL(o=$}}WO$R~?vy!@WzmJ6Rv=EvFzRxF z%;5|5Z)DhM@_7C?t_A)tx|9Dpi9zu3Md2@ZJi`vGoYC%T6FhO#{Okf>#-R@{X-$(V z`iU5tayuhUeGPJnO`LdS{foF`%z(AICL>Sni6@#D#`g;OIMZK!&QKn;uceQ( zN3uYEKo1kl=_4K=J}c1#%*VNzo&TI*z)US?GyQSx#Th}{Pxp5D+@4!-(iG`u5Lzsl z)eD#5l*sdV@=&jw`R#i<-6+_3cotXLp_Za)oavAf@f$}o*#=P7omc~jj#5peb-dmI zML|OEgZuGFRq>I-KsldQ&3jnRE>d9r)sp&oHsTO4zSN;M{EWp(H zIE#|MP-P<4k^$9+(zlnBe-3bo%KJKB&-9mis%qpjmBGv?-YoNp~xh|;3}E8TgQY1*ThNzKGdtJ2Wm&X?ZXVK9Mo1rYL< zwvoq&FrnBiN2!;`b!goj*6V*!o)zBGKfF1tdPJ%|JP_kG_j22d0qn;4Rhe%ov;7IQ z>54;4Nb5w}N+G&A`v?sJ9WW@@3PhGjLfLRl#82&~{RCTk&BpE?r9P8QN1YBvfK)!^ z=6o#KxM1Uwp`7 zm*?B@cy;docsc8#N9D)6%Yof8(ncg_aUgKy2YMSTfKbuIK5>2x&i^$7ZdZ|Y3RMr- zV=hBxz*Rn$c}9v)z%q9$Y9LkR#=SV3(w95oC1}lFA>@2|g1Yh+JGsi91huweNjWE~ zP#q;K|MO)G+FPqSwqy|wkO;$3_#F2Vzg>GH+@S~y*Bu>}{u7=M!ZaKg!IzJ|px^_a z_O5;rR9ycnPNWRMID_pstl!pBRi3|h3{u&KLL~NoN0Pje!aazxLHsKxE2MZAH2Eb< zFb!-8YoVj4W$XSscO-?oTbK-h^BRacLrfkszXo>&;%0<&$&wPJt6f zkrXSE;2%Zi20ZdS*8I}K-v3`Ep8W3@sQ!Djq6+^X%59=BFLzG89`Zqw4NA}!&2tz{ z+kb@7uzSl2{70yaD2R(G2R$=^kP*3m!f2jjbp9PibFRws@(Z4_%0@7s({s?z&2xUw zzp9jpXPQ3+o#`bcB%V0HtpE*DlkUEQoY+uyQ#zrRs|?nf%v+R7*@)@QzXe!s1M-tXU`Iy%&(T8f?3 z33x+ZO-)VU$cufS?tUa+il?eF>}RrW^Eur=uu=t5_CN@0f#mGC|u`d5u%C z-Sqi05)nql6x*ksFP@Y!vFlmt^JNi)I!{Qh4BBGbaeBM;DyKH#CEm->0(zbdaQ#Gx zO{py;|F~L7|IgH=kYOuD!`H?7jBc!CJ)NwPIhLXlWrUkVpl&fVq=oW7Z!u>OJjMB? ziA(&pnvDUZ6R8t}{}{;+Q&&;-@qgYM%=w`hu3K5Ma}h{@`uNwVh+0OWpB9FzNx_@yY-bNB@~MHLW(rJeWB7)C$e2>%sA zC7%3c8iYpuD|YGs#XP2e&rW%uR$VEg9Tgp2^68WCD6b7@4hLXJsL+lC!MrX8g0iHv zw0ANx?N=c8>&^4r5ldkz*m9OQc{X09Q)43sSZjqhrMaoJR>%kR6DzIp0SaTn`)qw< z2S17|{*IEWZZKR`%Fuf_u}I%F@E9ctls$29KzjTft7Px(is6m@UL560D}6tFb+i+f*JwJ0(tP)|>VY8rlb^nj!$bn3G)Xb| zvMI);V8Ayb>b2j!TWNJy<$toE$vX@5igKka>j6T99psI&Z^z%>C0XBoUwLwR*XYG2 zi>P9;nKlqnv9RksRizonJ~*D}qtm3y#iq4qzA1Yc^Pw!&F|Cfj7E@YMH?g?nCT+Ty zOh$$=k;08Wu50qUBpalvzLZ-AQ7)cPLF#a{fCW7`&Q3`s}kQ3o*km`=unx z{d)7kg|YdXcVE)%hn2Vn-Xdo}XZ~@GjYQ0Ayd)pL3;^OAzt4>{mz^@zOy@z#iDsm8 zf(3DZzI)dE@Flo*>bgRHJ1O7{(zz((tV7d3V&X`zqiRKY&S-rmcK!{8>??g%eJrDT zi2Z~mt3Wz=rsKM*^_XwV`c&uCtDN*jTz5u~X*r82ol!wcHoX5^u~?DD)XIZ{#f%j} zf#rGB?J5h~ck1&18V-T)pIrUogQf_rVa(<|L-J6{&ybi7zQ+cW7t(qKNVz`Z6mj0v9 zXB>ZKm;=h8CA#4$zqmHIbu+ zzqKv1JKRQBfAi?nkaGQ^*QxQBOT)yZTJ*DnqFqbLib8C>c@9RieWANf2`}NT%Kt}= z=4l`A^n3nJtuhTx`MMS2#G8)aQ5+fb%KUt}3^XkUWJ!~q4IMQKDlc&a%4GVc3MOrL zm+VX4bCQ)i^!%P`e&V&!2xzNg4}OPX5zlY{H$eTXw{lvUyd3>jX!Cs zG)+25Dr&|NUCFTe_Ai}y8x0;=`Bh?ySRR&=CV7kEyhrKB*P*iy$BD2yJglGg>G6JA zgdO~LsW_~Z?BKX7E%|~Cdz0m9?>x7K^Lcpt;r)C{x=EXx*^WmFI$O)I6C3YPR*th^h`m8s1m-`l5!>LKx*>pNO zI#VDVqs*YCp5axHL53>qabKX-7G{p@m=s+reoTXSYj(QqG=ToI-&OB>AE1iIj zYOI%>qEdBs8fLomWi=f8gXy_P9lxwCCi!GL8U9IEGTt^upx|3;1>V-6wh{~Lp zsT6oDvm)(JPB^zFvYz|Zi>ev#U?j>W=b}?ZVRBT}=068#2(NLDUn}3kR{6@2ujzjK z`0Yi|At()`-k$G_0I|-23$%=qQaUU${&NAwd=-%859|-s-oQ^BCQ1>X24L(i{P>21 z&prrIDBHCT)e>XbC^uh+ry)Mi@Jf%ziNAH!qRI@a(Tt*)9Z4!yS94ayczb|mzukuA zIGCCgd%M9B6t`+mq|#7MaqRMg!kvI3FXg>&&IVq-%xRU}2fYk2=Ek5eDoQd$?D7;G zzZ{FyN%)#SlGE3?{lc0mx^>-MbZkFZ^iI2Q{GrV{-GYrXczr8br=Yu}DmlJK@n_pJ zp==~;=sT9ew>C%)B6rE>Lei8|Qr(w3BZ0pM7k=11cQa}V%?{?OYfSs1K5ZbMou6CG z*I1l^6V?!LIM8%Jo?0-muxXm zP(y|?U-L=JkZUPia?BZ`_Wqd=I`2-`U%Ko?P?O+eQa|7cH0eWJ4n3yP|Eo-oTb+5)D*y$ zt+v47?*fmDr3!5DZh^>p3#9lZj%Cw4q=*+*RVjl63I&5=I#>qG_F~XU($=krHaD4FMB&NrGGw2m#PPmW8dBE zep8)Brp#ihd=>MGLPnN>{48sR6FW!8N>-Wf=xip+b7Noi;jl@y#=m0zC)3?SbY=BI zj#Mmf$*@<6#`2{rghkBaA@^GE)FTWXxusr*!kz8R-bbyTnE#k}yXkn!Co#uy1==+3 z`s}BRkxaJF{wQ|V4wgij|5t{PGthHOdd}0)4`2wNo>nKglj>*yyI`)n+&XB`tPX)Ti#gy3n!w;6w}U#f>t>>q zEa(tC+BG<_Zx(N_8L%11_Vbfbx^>hVavkW?m9Zw3gJ-$`HM^E`K7yBgs%DP>dMu); zRbgYuXy?4}wt^OUD_$>H~362b}O@~w(Sz9V(AOpWSUhgjl!eRM?z)kV$*nyI(IEMZSg={yNg zjTO(4bN;K7WkXPoXPZN;@Bb>(Etv+21KA_o2*{0Mpqa5=@t;Jjgf$k|I&D4fyT7j8 zFgT58OJ_dEh^FGxP~EePr0TnN{1nn9Jw*^fwG7W4&}B0+C`vx#8XG}J%a-HN)Wl*W z%ekF(NjL|A9)O2WzhGyEIx+_nzp*d2vzI5zk)T1`s(UNpidz(sh^h?z%(nRus+#XG z4&;bdNb!)7;Jok{?%VUUY)#U5gI4~Lu2z>8fn3fXBsBs88uKvdYFhJvHev}VcZl>6 ztR>Ey#S3hH{Z>&U8vnX|BZ^MFmGT9W@;UfHsTfYJ6JRel1bWVn63>!W`e)bR#r~|) z-gKE-)(e`0(*<-^f>AR)4DBDbsh8BX&XuyJ8w^WieSh+Cj+tho=^-OgT;|vt3}qsp zi>H||oW3*345O9()=y%U_u+tzO18YsPDx|P(;fS6HD3Ah@NScLH)*Lf%}ecM>2|W+ zQC5Q`vdnXY@~pqfX*k-d)$YEz%u7xle|mxJ%h6LqtMlFc+0`1C`Rq*e2bkyVO3Jb% zxVS@5R)KjI7z7~u2_*fKwMm*>Elf8K+Uv!R1`fF%J@=(H#EpsMQqSFe`z#mXs^ix4 zaZ;%_l&^c{%&LA4UORs849jm=R;&Zxigv7Z*Yt#_DaOuQ|ikx`b}q_jJA%7!XC~U*^BJ7upX;U2-1FY zbx>e9(deXPUNNQ7;Zc$87qoqI;JNkse)PS}SECU{bmNZffYyw`UH6!(uU7Q!n09Dn z7pZaY-DwW%~N{Cc}FpZ=Wc(freKC=vl^)ioi>hsIs$Pmlh z{)MT#&r+k_!4j`wc$8kGlbmGpyLNxUdS%0n7@fR1i?_TvNrIA&&c;Wevg7~80u;n- zXC68y?XrzKXW+H4zdb)qI#A@f!w&ZoVf}lB+fq}LclGXYkuF>hj{-S6$VjI`5M20 zz$7DwzkXy# zgm-BFuq6GiYRi$5H0t9)P#;QBKg3DK$j~CcM7mMKMo}W;=Y|A%D}d`0O3-&dTcmon zsIV>iTU%{dxYZzF(Aj%!j;X}#kH*sv4uYh*o*HEL&9Hc-G%SMso7s@hzULOMJq1Dw zG^q86=wI7R$pwWT>^#qh{G_T+uOI3qdo?OVYQ&0uyRNbr4J(8{T2mQnfxzxCkYN;u z&mr3mc;*nUM6fF!{4r~2_XTNA_pJvSEf7=HK7I=dOfOZ+7pwug5Lr#lv;YU|j0NLf z5Igb!gTdd(hNp4f35}Wp5-MKIb+4VGEKxByrMR7<6lD?9k!Yn~_nK(Gh!1n!2*&UcZ_UZ4MXgNQ+rfwEymu zd6RsLQ-nJs46BEU@BDpN_9#p6mjEt#?59T#rR;;9#9u~j%IysW$?MlWR9Aly@<%Si zQLZa#Ca%?dh9_0Rl_mSblu_`=90=~H=1{w5F+9?K8Sxp_i_c#(VTNC4&>9(?<`h$C zeD^IfQ8R2_y4pfG+i<#1ef?$nc~@0fq}(6g7_Q8yTQ>+9f+Lr5R5%T*j`fU8L=ubS znrn0^B9cHmt#&l{%k70o8ZrY86t?$PFc?lB2PNJD9gW+w%IR)aGvtta1sp1f)LUPM z5*sp%@4lOE>gl>ShH`i%$9xJKf!q2qM8x$-do5#}kBXE9Mwpts;j42l$nA~t^Y9Wt zH;z!-M#*6@3wwAtpE!F?j}62cB_IA#vkZ5LpVkss`az9 zfsV|NGylVxOqmySMY#L;8BnKa&C7jN$AJHW6>u;5?hDz9_AcFl{aszaA<1hY-(UL? zR4tJd^FfL3l&Yp7Iq5vBwzu1UZuJZ@3h7C8^808uNsjf^Qg!?Y?<~jGaYRBJ z7GC#XBy0H}9~)uhEiW>rP{H|`$He^%qQyqp=h0Z{IN`XxC`_lL&%a5(o(K>)sLgGw zsgB8>omrAAQ7BN28p{=USQe&XEYseRl>2iqcGit|EZYLx9%zC|Nm^=~hVnm#{tTr3 z3HCzfadF$3Wn1wR@2E+=mVyGi;J`jd=}Z!$5LURDwm#G&At zEjPO-gI=&I3eP4Q0hx3usVKC|pO3Q$MSei2$3SHdpWa)$5vD84rMmq7~NiP3mV?lOgyixV_ z!_9iQrakn4r>HzC_u7l>erdP}eyb;*qu@m5TxZQG3O{TjRC03ZJfH{Ue3fK&Lfm8} z&K8scqcU;-_VGN|s_)+O?jGGo4wrFX2N2-QDtiz}5#VNbJ%YvPZ{;I}Rlj=SACpZz zS)Y0ch$S`W(#R}NygA-GC2RU7PsR$#zQ{^Rbrf0*n6ite;`yC+>NT0P$Ij9{@}TIO z7VhtCnXN0?Xkkepu#^3IJlV9~Dj!O1JM(&=fIkn@%^sGM#q|4JdL^2*YJwR`a0dI? zSh1K>g-rB8!m>R7y%62hy(+p}?9`+E>3gnOy>EDMwJmh1C5=B&fOVk1fO$!Xn2+pbpMWewiQ1KxNj?C$LD7&uW9fBo5ReShm@VyBa<1_H0y zGigH4YQRdXoA!I4E2Hge@AhQ1n(V2WbYnGznes*}@=sZ- zB6mOXnFPjeyF=OI@_xw}67vWnMhC|V;_-dsv>NF&R4dlW9p*xu85?CIW0vvH3DZ*B z1Q_wT4vv@4#aBBwoS{(;@(;o8JmTZcSP1n>rGFa#H9I)HV6eST;dJ**rck=vgzRHV)^=SqRDFu8qf84fpEeb&(T3S@e_Fvyd)~?-%|F8X=mN z+UJx|YUP`sPNT@MPhe(sijRKrN!fP3rUc9IyJs{!2OYTBIRE)Kfw_%OIU#CmeK$vB$kW zzV)xQfB`PXgbV4eio=;;|07y=Datl-*GH?Lah)=K2&C`VnwJYQaJSP9Jah&pvC!Rc z4BjU~vQh8UdB`wO(fle18^gMS;leTbFDTt!$yvY?+F&@DRM~xD!Ywjt?n>Umwp@39 z{azP#gLKX7DnkAhb#j**Q`edS3GhHXIkltYk4KaqRms%9zO+Yoc#Z;nj&tAY{giYw zS0{6Z#3>ux_b{kbB1E8KC6padV=Jd>YaKiu>^#A4K>{7Go!GAn36zcB>Dfk`nU96? zSNHJZQ!0Ry%xJUZ*x!XOm z9g~p7()nET$>zTy4%E8S222LocVy4)(jXO^m*HH!$OWN}m1@){pNxyc^-eMxMVPr8 z#mG!11gMlqpzY!iaRPPQW}(LodTg-sdg@{2}qyKkiDP_&p~b@{5%dy*I)Z@_C_bV<>8}WK7F7glJ_AUW7aLs zpkH^vOPwr+ldJ_u+PLA42dj9C_}#_+*kj!fuNP?%*}t`@>{+HN^wkOkuioFH-w)M{ zC4RvxMIqZUO)S9hOtT>9=E~z7TA|-Lcnl}3Jr7WcJ`BLFOLRG3(4hG;J2af!k1)ri zs`4hv8ZioAz66Hw@#E4@A=<1&WR@xf1*C>oDA&g!^v%fboi9)?Dqx_^@~b4^TFv7I z-u;BeI$R?R6r1Cl4V`nw7`r4Dr)A5>pXd=lBwrEf#VY(q3{=F;ieKbQ(Eajy&J49_ z@9VekRte6(tgUA&av{mbw|#8p*n#~5%$+z2q%BPFREbO^JNLmo3IU+pV7lqAb>Nl}+OsK8$@v?6^-;P*o?Sfr*ll z>Z`F`-hO-URJVV-JpX@@_ZCcfblci)aDo%u-QC^Y-Q6KL1b27$V8J0lf=h6B*Wm8% zewugfwfCxh&JQ^CeJLtM)zkFTvwQTkG4AW8pKWH}OD5Q$XXuzLM|9DfVmRh_#2FAN zoZBLfvsucbJzgjtzUtFrwpZ87;Hb?h*G)8=FB(eCGNsC!@c8`-HjjqxPHG|YbH|3Z zV`u;S$`)$DIDe(jBb|jxlRjRZ*qvL;dLvEh#3zL4eS{{x<^91|K+VaM*z z>7D@!&S=>kP~QF5nJCMy^|Ie7zo~`rG@)`Os74fwC@pqBb-z2jVEhM7z{T6){@|a_ zHsQd0CZ)^hll?W@g06`G;NHuRDB-a6Ifgx#Dy3j2N<$45%g4;+ zRGpaWup#=s*es$%UgR3*y#GbNvASB;$7e-Y*=9I5#`+a3ExpEhL|i4l^8Q_&a&2s+ zS7pnZm1@m&`qJ}#u+aq|O{d%UsZxU)OFww&ymB0a$uzQ02n2@YYLub&xb{BtR*|5?<&W;JUo9{NB zT19>{@3E2pW1UOkSs-QqiRC^`C@dNN3H>wp`nG*syuC!4eRv#1;!vkg%g8DQ9(A)v zh|?9FJyxFDRF0AMG!X_Keb^~pd&2udhs8*b{hKm*s_w~bb;gR>_)M{uPX?=3hVIJ- zML%SDdKDyIYm~0v{HJ%0q=~n5dJ+A{t{wRINa^YFIhaGgUd|-+VOXWWHuTX-I%o5_ z1PJF^v&Tf)r6#iG8}AjbRc^8AR^{Wg@#bOatqA3Qv$uDV9>Vm_5?7H7GQ3OTK23`l+@+Y^hWIP=~Ujf99oh-{dx z__P%ULb$>hXjp3WR1ktBbw3!r0#5P{0uky3`juBEHa!X)v{%lvWkS*!h6oBK-x7Ky zHVp2}LBRob#euUxq(9JEK1}ekA6DWwkw2DbQOy?o!S&#uPcSDx<$aLgVxtx&>DDgf znVtTrjKzVL=Q(AiNzJ~;fSZQK7Z=6zAqCsap1e=+_}E@=bHbN4^9ty$G!ED=h^$u^ z<@Akr#uQnr znzkTum!=_pVv&|6lLm6tj$Eto;vT;8^mqxRxF0D|oc3@Eo+K#N2z8ad1ZD+(^SBxi zq0yTzU(uAyrQ>3fyH(AUgf9Y0>HI($K zQk$8nEmc(`DL(KRaHaF*YGoZuZa8oiJ!RDAN62bzWxkOr9nLvLthhhK?Wx>Y;RxOK z1?!PE{{HGREdcv>IB1=K{vMu(c<1V*3TimC^=cL6!Y<}lckuGmRX>5(lE)r(PhkEG#x(fXe@e7{BLxs4R06e<=q1!J*zJQ7BAzIc9{pF4Km8DL_G{#|aVVqsA{jQsjX z<|ne_GOpj90keS4nnznf^z)y9a=kQi%RiEYd+O!#X$F4Q#8rF?YV6obW{8+(3Cfx~ z2?k*tDUY=W6G!V*!#*h4z_SsQg}A%SKmXD*GeEtCWD=cxfIpVlJMn|q4s(26ZiA4t z%W#dWk+Qd7pqglsCL}z3C&6gT!p(pqb{99yL27@o^K#{S7b1x3Apdq2V(TmuQdZ4j z)n#ER?@T0wlNg=GnO}VL+gVJPn0sYP$UWWx(>H}RxxdDe?}CVBU_Ouqw^^Hfqjm4*1_<#67NTba_y|tu75hdQ@9cKISt~Q10zE?S%}LKM zCK9>d7=lJyEOYVqeSK8@me=Xg*|EDGTaj}L5y>vg3@BvAv72-zZ8S7>i51#;x(k)w zBR}oYYSWpJL&Uj$T`muRTltjnq)-}*JhaJw;(_x<;W`6vwP7^R57 zl9AdE&>hR&4p#rTNnS!mq0_+}f{;e1iGz!pGhT>qWqj+UjDZiBF>DfRN_2NGw}`np zrQ0{Vu5e~NU-Z76mX*|Jg!Sld6C->vZ@ff*e1|+i`DXDa%}*(r`5?;WoI_%nYv>C+ z1zQPO+TgWCJIyIhs#vpV{$lvgV*!3C_C1LMdu_VplwBQ|>7DbCQ&Sax4f^<0m-!JO ztw_O;_2%2WTkpJWbIUR<3d?|3PvGJI@W?5@8644pb zzUDw@-zy$e=+PeRzg3H4mqGox(ko&>Dw^e0>ivZ*FDN=VTq6dAXyx_0|9Sa4AngYD zu>=epbpl9{;TTkt@x=-#D#)CM-hS6(9Cy4ox!XMZ6s8{{<%ZX+l^pC|Y$JDsaPTM1 zW+7E3|2{y1BqIi@PZL^gfhu}0nXbDINViYk_j>4@I~@?>>hyB#Q3aM(z5uv64?MZZXb7OWl?;f z{)4T6hhwil=V$`Dg{-!#a3MFVHr@U`!71r=X?k@HF!h+{syY0)byDrL702%{f#;Hpw#aKSe%uOm7T_k{!6jP#tc`h@lpp{?6#mKi2e7eNcE)dw z8sVxp$49|k)c zXDX+X2?AKv&TpCfnoQd?o<1H8y2UpYloIi3sW-{Qk4IM+g>EwIot6Uf$MkWCIG9SE zeC!7*OGR_SkTcjuK(b5JEu?ThO)x_L`gHmjR$#ls{N-sHDEqmLS@iKelCGIg(~ zhH8vW9-9xpp$-p{-nUJ?m%*LHl~f6+(-YJIMX{#rwnJ&w_|L*?FAez)NAs1{aDH#b zQg`caoXj&SMe?p0F`b5_ZXGW{yl>;-Fe*@_WF9+DGxtee40x1Q^l^VPMcz|k&)%D6 zAsq?jR-MKJoZffAd}v;xGwKBIIv)C_tsqu(eWd+7rg>q*c7JgU7cPy^>|}O{@W-Pa zJ5guVdOedno%e;wxT-&fp39M;?ELW)NQo2pN~LoZ=mo~Y5@~kcHb1BjrY@cR)Luz_ zykI(H4@~SH{EnsU!e76dr4H6N%BFJPVlqyW_(gvuH}F+%hgDZ$!q(=(M**sf? zbYo=Pxvb<8IhO$4ehGx7FGV^#>}+Fc#Jmm_iy~PIIhlP82&35ML@ZUp6; z5V(X}?)WwY@WCEVS;M0OP|z;Ia$q8e%^0ym&mx#acbZ6Es5c0_EWByKL)@V76WHC8 z%}~_PPnSfjQoTTSu`y`$!iEd#nTvQD*`ZY;=T8zO7b3ohuo?1MAx>+LZikBG=Haeh z%cgd&#~zlK{01kSdk-5jxr_mKV;Vgw^+p3cnE}2j_{w=ZL&lpGxUA7Q_=@tc5cg?s zlo>CT+Fq4q=q5_)-5-jsdzBb*S|G6}K& zHpQ!hPZ_=H*P^{1gsy2rsQ>ZY6m+ax72B=v?onj5xFLMYDV|nqzA^OKxZd{>3B+u= zY#ol8^$g!1?+_D1ek^H!&+U%p#^cxZ+jTu%zU~=plc0lOM9L5FlU}D)u{;iD2Z`RD z%!#)@c3j)~@pH^*yild_U$sPI>#gtTy-Q;NV2lw*3fe@3Rarc#)#Lo8MG}+&wqynY zd%9+!(1$P*1OxLDXn#ux+&art3X5z39qjMd_7k9$iRv;ANK$auz5r-MD*)*vIFE2U z*RdbV)pSI13!bqU3qRLEOyU!GkoAuVoaN9j_MIa62B>Rs-s!%j9w z0SN-1Z&vS3hJ@VdN#ijpjQybZp{0TwAz+(991%v3G&F0U;&AkpTb0lDyC zU!YS&S)2QC0;BicHUgkhm(%upEfdZnLApXMzI6wGo{GrEC{f|qkSaHU8YLP$z2wtN=30gsT~QwGvYHq|9pgH# zsM_)B(U;sdam$qSJ{?`t)UCy|eBn!U>}vJOZWbuFaV1Z?dk?fCVi}IvluQ8uA-k6y z(hQ7kq$}uP)dk@WR#9x&9CltMtV*(3@Gbeo7*|J>zOMj!KrKiWJB`l!6T{M|79tNO^M1T8F;X#d+3<-v<>f@TY^Nc(F| zz0DG?HjWJwG3ePZU>W5*7^fh-+aQ$mVVI7RZ9FZ5R6de!)5_=f~ zZGwc*Di_>bZ9pl63KxuDU`R~NiQmt+zv&E!tWUz@p&`026l)VUR>E0AGxr%LK#{n| z>xl<1TG#;9`5XG@ZXsmoa1>r8U4n(~gM?t{t;fyQGpQ~t=JkwN|NStI&yJ&kire>w z4hT>8mnU506JQV%kBe~*!lGnb+}P(zq@3(3u#77L753pAIamuP(4=oyYaSHQv_sb> z--Z;=u`gu_Bp|7XR+~ZyCJ-2~b3}4LNCKR^W5@XAEol#<_B(w@2&E>@thd%KhlvQ0 z1MQFxLvQ*Vk*r&J3=QU;dGG6B@&s?1b(DT^UuAp=z_O3Bxlxcx_&S_33dh`Z?xhXX zC}g^g-+xIOaOTM9Q)TwR8C^fXcSd49k!T*hbsIB$?E3b^qKpe($l}YIRFm zcC1w_s3cO+B30+@dj4w{YTjv(K{~|`qdwKZ;#*P}leJ7zMa`>WV(|-CV^FsL z>z|=E(yHHEpD+;I3>F98t(H|(u7PY>s!N;0#uMb(s@&6^&9(tbMqsxw&(N)3ZR+Df zaX_OwK|G7u$(p2pAy zSS-!Jw-3Y4%|%33Wpn=suRVSJi3&bdIhMIQWHdmVT6Z%^v`LzWZp%It+<`rmx)RB5 zB_v;Sv*cW1I!B!-23u?PG<&c=ru&CpPdWO|ddf5Eg{dY26-~pN!Ry|R0_tpPsc?T$ z%$V14pXJ12y5@r$Yz=KjnKe3QFy0g5V1)Vir9vK8q^%`(q?)t%8tGRgi6mzZn0~ed zzDMu7TsaiVL_FBUSb(*|IS#`9Uz4R0>UdWqz2ND@PagZ>@-Qx(8vJIi@`!=9$VhKLj!r`Q z>LX^|m{w>K8ZkSAG<;<41)T>69ye|F{7$}qOF)K)UCG|B^MLMKw3lR%Y~1$C{;fKW z_x|{;hAi5_c@k-O&?W|H*g>%|%X5(aYM!qI>zS}l&%dN{pDJ!B&ETygknJAw5#X`Q~g^cFk`CFs*IR2Q$(MRz$#Ilag;a)B}nhD5sxJYV;wqc?LAj zLj#|AYBmp*p zC_20&c<;7}lp;9Ho{3pV)a-aiFkH|E(Mis79u1RUczhyW*vu0AY&~30r)Gi|qfh-Mz$;fwXN_Jr5utp&!6#?1rds)YfXcNg z7W}ZuY^LR`LTsh=yTECOf3--f$L76}Svvak;c3Cd>?ZiJ#>iPrsOKOJBeyOs7-nh( zy|8G55Zae7lS6`n;djP$L*auq7wE?1VOiI)3RE!#=RxPLG7Zav>!=(?uTYcT575f* zU6MV^JAzxLb>dIf5Z!|bqcr=$YM?%y9|9#cM-6H-Hn2yQvO>kgD~?{XOVr)(&JPOa zj^ktNglx(Aq9A#0qf~b*;ULrZUCTQ!jZip5ld?{*DlwZH&T!liA}c2ABdjDV^xpTl z&+R#Hx1Tc1)(Fm(q-3FE%Cl4Oy@zDeqU#RUY-d92cW*@8;7cjI8HIO~&g1G_3ve22 zmzrX4j@4>~yJp7tO2VK8bgOCgV1k0_AiKkBYAvvYaBNgeur203^X;aKT&r_AJ!Qa> zvYGLyeI-%^!GJ>G-0*iIj%`=5Z@W#+{)2Fi3)>yjyqwj=ujKPqkch;=4hS`>8Lm$rjvqAa#aQ0`O?8Am<#?2HwT55#3$Iekf z`Ct_IYsJ&qtol*&UW79t9AXVjsQ|W^1j|Az#dDjq06zTaPns`c}@3JW|*^ zb3A;7l;m$woFci3w3)KYIA>Rqb8OlH@+&%g*%3wv&B4)Mi05P9b=hKg>;p~}|3C|i zK~KxU`Pru?9XhjJ9Z-oPr^P1b6H`$QrhO@fpL5urSd6#V*{ryy+h@KCHA36(b4dZu-|qc> z0gTuw!KXV*z4p!NiBR|(0c&z=^%;NUa@MQi?xm%x#{pYeei(hOyET`#LW^L;yl)#{ z?pEAFzE#hlKWNzPTYg5Nz$AZ9)v>Dv^Arv2o#1u(bJp?;&{XMMrG>mWQHLIJRdR`{SR-IHS@oMi-&&gCH!#JyRq~@V`jl7C?;yu3j{^1wuU|~;pqh7QP>Sy3|Lh*@(J91)A)+u>QDwf zGG8oK&@w#%oomVK3YYHM_v1}naGv$Z)M6N)Z&^~^v+qJF=V+yQ>1u5x+iWe`^KIV- z{TgGvZ|5>*XB+YF$dOAI)V0*5yIC%nI_5Ly)4fWgejcv-g_E;hLJxf-RKqYUC=YY6 z0(1Ch6shYpsiFX1yHVr;=%`T&^s${?>8-pOXd4{qb{=KkYhTJaxnivm84oL^?#Q`*Kg)9c20-vLb^)xTqX~bpJQA}L7|MpG>g~?MQPC+z z{t?1u5?MRf?&Lz)p1ybsD4dehu+Oc1HH!iY;WG?ppaoz5VkKsy9e`~0J+`yRU*x^E z49{@jGhm4a2WN^C&b+J@*_ymJUfi6nY3sMV0zCN**~~`eCxeUf@^w|mH8zehoVip5 ze13_$BfPgIyC^Gg!_{J4uDzCp-MG(h9vVTNDE6zY2HFkFw0%Iae$9_mTAd7ejK&`u zLdVN)6}W!Gyi!-k^NKUjrkDX74~bgS?aSov*i|nqKg+z|#AL2%yJ1c5SwUr|+b36e zRCpQJ^`BGWPl6BO@N4TMT(Lm_D8m;C)lOTS)+7T)vNyRcDi{MUJGxfX_SFfriS`K` zYmqR-DuA~j){SYZA7@&!FSB1vQ)CV3%gTmetkx}K>MA?rwK+6t=*HGhSl0`$d5F0n ztZGd4e6)IN8rny7Rwd0VzEZQKaU^Kbd>DH9ZNsKLbU;AmxopMQ4>uN+JTIt%7rf2}xusgz^YfRwJJnWz0!q9rFQ3vhkc?;QjsQj}qmXV%Uu0 z9EIuJRt98Xj{yE%gWl_*_J;~#udu6mbr!2-z_(?OHd3-QJqb<_w;W`LQvl+?&u%N3 zG9h3?p;9vcS<8(F)b4G%{_2qd#q8-N#c5iIXJl0D*QiV3h=B_F@Gm!nD`4CJXb+TZ zpFX0+PqkBxqko6$Qc_wuqRy&G-LqbodNumTF`$(|b{#1c*;boow2oq;j_$qGk+`d^ zW|AI*YkwB#2lLW?j;pE$CrKn43J~ztZzP+!lMyK$V-&W1iriy1lXBKF?A11^mZFe4 z!J7D0&P}~RoKy;0+0-QZlymKOwLWV0#;0;L z)9Z}E$Y1#8xBBXY99`mlxEzXX&k7cgB-xYqE~F@^@y9=g>b8#;ZLT%sq5?b;oiW+1 zf_wB@Bpt7O5>GcabTj*v@jZ8Eh5*KeG7idi|4Rbwn=@TT_TOCaV|8f7*k#kZ}D`$8C~bjcudSrrN}4?arG@uE;I&G zUXnE>i{5wG&u{_z&m%^7%Ta#mGBFcmWKzmD{1_fdF+Pc=P|#G=@~LzoR5e@zBJ8_n zgHO1;=JhrL^=z57W6Cn>cI9-x*ymE=_uQiDeK?K9NJ`07cxcq-<2V=IZ9mplz!lna zJeE0ab}TD*c1Z}lQCO_r2EcDS2X*k%{;E@Ob{@eLRE45-=&V ziM`4;HA9v8bI0X$&{;H{e`;P^Y|J0ij;M>pJ1yX6OTO;2EDRGA{p}FC=`%~(pn(d; zkU-*s-{zr|urimc%}eMfdEQ}5J! z@71`v`+eCCPdx1xb=WY&Y`)!MtTh;*dm+`yvEVagxla6`s` z*`uKKA&^FXO#;8;&hou+@*B@4vX;O9wV&I;a&3zDEjTFsOb0n{ltyKmQ9;Sjy@~l? z>HFT$_gnX!QKeNV_QyuwRbi_ItJ<4`YW=@%tCaOUvVUJK%KT|^H<2CowLu!EfO6_b zkD;s65?iwq1Nax~;ik9f*K&WIKTJTH-?*Gst6J#oJal`9heUWlPM;F$_LMjbQ>d~2 zgW3FFFW?uZT%xTtEOr_YK1b=+u4~u%&T%&BRw21@3z~Vm#2Bu}J5S?0SM^Z5Lu4>4 z#OYy|Ow*pdpYXnFG$eA`Y!&q4AzD|-I6%1>F|489K z`skPcq_jX}F+frO7bzp3k`4dAK|}eJpZ_l-0VM|;12P+Cmm}2=h;kKBz_{f2(R~4T zN4onrAf@qmIxP99_`f=-osbq13V8nqnGE({w~Q1Hx&eSI4*G+Dk+QL=!N9`4b^w2S zf?4X%GWp%@{q_EXK21L~G^AXm&-Vc--USMK(*QKcyZUnKRDr|@P_0D{@Ut$e%DDBb zG{|Ms2^aLeDsB#@de(US>{~CzGX(tF{eB&l6c&p5J{?j#0-CB!sd@f!e0 zv-tCU5PjKmuuDbcN9kh(!rt505Bl$aGyGqL?L5mSE=i|WahfbQm$P;?1>G~ zD=j)`cn}J)rV*?L13dkmzIOyDYOFdA;5EE?0##|YT*o_zcc1HnrE?3LljOOjwClf; z0XOQp3Lbs|>*OIIDoLZqRzdFtpzJT-5Jbn~@eBjX(UZ3E3>FE%4jQ0FZtj#Q4wQ@= zw0tY`J7y+c4SV)b+ruYLu^Nu;Ybfmj9YL-i`jUjF-I-tsAeVg(XG-GaT0fY$FAv*s z0ADwE5$+aHHZ8)yiCBA)2nZP9dz~;X2R7R*F?&@ByuZ}y0(`kEplE;O1Evj-zB2ue zk05$BfWNFm`<;IhPPq7ijD3ErXk1GK$k0m@0gCvq!1XqiD<>-|rmlHkk2UZ=f5^@( zvK&K6$x_tLXKfMr9}FjV(@kO%Zf?H4rOyU%r;nevDK0LV-C61hzDn|n=k6=W}Xb!Qx?FVO2kW9YpLd)Njj*GKH-;wDz1)DiV%W zfZ|XjWF0W_*g2gl2)hULa@Y7ygG6`OtM_BV*}g#WdNA)mZv@JzPqst%=Q}L!;~qVY z4do24@EhH)SNXSEw+w7~2+qUbZ9U*ylIhO8L)k?NlyV^GdCH+;0oXOQTz);BX+}oI zo5Ao8w6pvu`kFi{9Cn+Fs@ssCtpLlK{pK?VtDk}AVbSz&iZIK@54g7N(lLOaXSG{d z4DA9+?FkyhSTswc!Sw5Za3b8g1k-BwZUzz)JjSBmv%If{fcmUFp&&@WpaDasLc89^ zy)1}{-PXMb#ct?UA2_jV8<fB0{r(%?qUWlo}uVAyn+1y8hS#~*p5()HWHkRRwf zc7f`PB}|_;y5a6$XFz9=sSgya_+=JbCAN!P)ov}sL+7E0!9?hb6S^#DbnB309RQ7(9Q3BFS`>YN*9bl zs_ub;Rzd^s%aE7H{oJ?uIf+6bq%|W@3x0sBJ%Bgn)Quz9vZf6gYFC)DXUTMG z@i%Ue&u^^{t+_H!YRi-@4)xpw0?h_yTcq>`kg9Az6QUgFyVvyqvilVv5Tply*IQpG z0kJQ%qcC`)!)LV3d(Fl_z^dFf7bvjcSofa64%N_eQCk$wh5NQ_+o)9#yN%M&^$&e} z`h6A0m+tM_)(l12Ku|XPUnIC^+~AQA!UuA(+H)4*S6|zbO;g zj42963t)?WU8p=Y*bcJB9s5s1O#X;3DHy9|SV0m8%2~L}e}MBw%_5h#Oa7SS4(X zTc^MGv|Xjye)I^><;Y{Irr{#3&Nm_l>m42HcG8*`#a#O9ogud`Xa0@@zqc@5;Dkke ziQ$RcOBVXN=*e>Nciwy2M*LmiE zU#sFg5fQ~`k<5gBd$%RmD(Q*;`(b2U!oIyd+aFsw6DCHkXY+28C(EH%z{4&kf7GU& zUQ1;U-7SW)MgxSt#f}u^)sx?#v7o7`eS+tiELR$aWD>^^cqK!ksy0ALOPC*`K`A($ zhbnP!h9XFR4PGc2y(JVinl`U+Kzz4XxJLYMuTKl7f9sz)1Jkdq;4$ zcLS_Y1X93((67OEdH!9_W)0-3kflb?)wQ9{etivw=?cH%2+ms(aK%`jJ_Pveq$1y; z)U`1)RV_7i9YfesttJ=0Z(0uDK^Zj^T46&X0Yl5Z?&9Z)9(d!eN-uw2(POSWP5EtX zcxbp~f-yt#YX1aDzP(E82$;Ma(sw5VyEu^jeiZ-4POJy0%ScD5HWWL?BtyedNR*DQ z$|$nM@a*cfkEo|*Cy4o8$k8Z)EhM37QrcAmkc8)5(jd;bQ2>gM1&#)Q{x~%#Q03q$ z_Xnf?*%^-nqYuYd;?#5CL|9F%K8PEew;!L7V8ntkAd)Uti?_V7*U#B2`E91oN@e+- z5VH<-+Bp0NF?jOuh}H!vl6;^9t!fl;cRgT!rA$QPK7AoN$sE)esK}yihUdmN5qoUN zN0(U#Ipr6QBr8@1w}T;^fn~f{-6Fi4ol!)BNNCOi zQyXpi^J*l0T+K>D@FXb91k#-tMpR*$hJh*DlnSslj@AwUm!Amq6@Meawfg{r9B$1w zqddp)0B#T=P|U9gtfNHVk)+Oir_}mBBNo$>^rk;GIPZ@)fW*g}V?(bx*i{BtJ74iV zqEvMO^3>D%0p%(seei3fVC)d)|G1k5wdP|s+fgXuhrtRh*RrJf^)9O@2Y-%b&5nQf zz;ME-!ST6S^Zt$aQ8ElVB*75+qMC==qJVpwm|(U(i0xHQUMn-(T(%S;x*TxP%+VSfj!G zJec{=s%}0JjmSiM=q1H-M1PMs&H?SQE#+49pX@-e2P5KWnsQu4 z#N)2hjf%-v)-KA3;IA9oS ztkkd}02>?oV^QATy1PFZc-93YrqpXRX4hFLnx?l@m+@VcfRTL|>DQRVt0)Qh>gTMBAw^`puJHMJ_+3)P1_a;^kgzZwk?0da z?m?>`Zn_Y|bqC0b%HX7c<0az(f+WhOGA#CVq$tgX0{@b&Z$B8oQVMxyN8mG}CPk?U zv_)1S3F%70sk^O(1ER7)a6WuRc$7(V1UY(DxA{okGKLf3WY$5A^QgeH9GH6vgvSFc zBy&llKQ}Upg4a+=w>n~*^FN=|I(cAEfQl2mf?3*D!N7To5`2o5aIO}4gL~pE^@FI}V7YTm26DUo!kD*pIRMVFKM5iPd8~!#} zyMNjNtD-_jF9u(FchLv}cSE`OlizVgqm8kJKS-;*>Zcp~JYtc~>KK(ymPo{c2_{wk z?zmKeyB;Vf>8DS1aMJ!}WR8wy8 z5fcOP^=+0E{sDJ@Ic2)h833p%Z>OC^XkdfVkPy08G;JjOV7p}iXNv9V*fUjaUWz>& zBT20s64*Qn_cM2VRJc0D8bnAo2wBR!pOn>GtfP#RAlDTMZ<40H>=%~ z&LX})Y!K@w36d0P#0|;d;^&_QIiYT`MN&SBchz9tnp{%l-1AG^p zL}i9GMRek!9c~i?Hfgq35d%&T?vySt!I{rqhD9s6kf$7RBWx~#?}O2Y%Lt9l zL@jgZ_XShRKXiWYxoy{07D`kP|)c=l4?JR3FkZXRBb5p!|{;NQ2PQ2 z)#CMw03}_RBIxJB#&_?ewjHPD)ExvaZvi}~KaIe@KlT>w9`jWSC5@W~UZScA~k$OZ}8))5EBP+MPnXQYx}?$6TOHX{$&IuU%H z`*&c6g`{fjRY$<17>kdCk>+RotaYzGXwB*CDUm4vVRLF)8V+!}C2R&E=es zf${Z=C5zlcS9sf@zvpJJw*Z@7yMhJ}FU@0U(0-$SlUrhc*N$E}x4E6OM(O&=-!I$t zIwT~7jH)_i_K(|nA;9i0^q9EfGX5pECyO&b&oVUcNfjH_oPCLXq1Eqfr`7s^xSCIU zMEtd=YQ9LSf^Nm*fNsNd?Zm@EA!j3l5jO=N#}i*JuVh%TZydZUBs}|b;4?B03i!Q= zodsO3n(*RJs9OO%VGoW@n^-s#)?A&?O=46B+09{6iafBlGpuXMl-tN}MhJ}!5Y z6FWA*3m6!0G{3*SK3i~^^Gy;1qWc|ATbrg6&xbVM!6}mtY?fm@ji*bi0 z-uTyhf zWhp}LRU`VpPH=ZJn)nT$6W!k4J~%j-kez+rT>>dMT5vfJekzqVM(O{3IJQ1AGP0C6 zAS?{-?G+^@B_-tM#x^oCYVx=yAI}xYHI82qM*=@ij~-iJCjg9lB!GR=1R4!`a7f7R zfdL5|G9ajfM@jypwXWtegGI;=w5;OT`l_m^#3UrQ29+!v!c3Avl6@;p58oj%P*H<7 zHVj|WLapqAjV%J>9Y5Oo`cGSj!m~5%t>v*nES^TzQ2UK)F6xUmBZy#xsZ;y=%Ec2_#$`SlQ4V^`e&Z1KvR* z>;Jfy(b-xgG5#cbp%ncT&=~`!(ZvgUA0J!)&lv}Dvb7TJutEMiv^URs&7q4Pt*<+P zGxyi*_iEcm0*}rV{+~w&G-JaztD41HQ_k3!I`wjtoHh+&vm?4FAR~H$HzwAR%S6%`8Gl%#gO>Wyu*aBP)avAAth- zKi_7vlrc0oOesi+evl`d8QU|AX%3pSKF;_s9Fh&W`>SPdbI# z%!U|R*qqCG4-P2Af%Nn9bN8aSPa`EKj|2h__EF=6mI(jiPN}{$ty&oA%=WDR=>)(u zb#3)UAz@)zl@DRQ&~XXKG~tHlc$zdki?{SFlD;M}fD0 zorE?u9O|DwK8uVQ=F)nea4$?MJevhf3S;6|id&@;DYP&%Adlw)w!~AcF2yPNfl(&{ z7{3i)pKjCHZLt9}Gh4u78oa85A2*3Yp&u~(a(KS8-9`~szZlWNguuQFu{T5jE@g$c z6@~+`f-yBKEE^6M<1Gf*&BnIQ3c8;gPNAv)(Sn2guSgk>PY!;F!9jj}dk)XJV9RgE zj7eW7Cz8T;3v?LJYOpE09KKcjeY)0$qf+t~tyI9@c`#MLveSlalT@I~f?B$~`cq>F zKf-aUsrAmQS69HS+%a~hM1cmRQBeZR9NaTQr~YdUKzGNsKBNB_9-&YOP~vC?sy0?~ zoaIxly(UO*bSby;x1;$MDVT_m)8h0RS{SaKw!7imD)=y4x@^B}S75k>xh_82NerAp z`{pbVvooVJHJTv#&}d_{@3lNEx|5QbU)-0Cp=_a?Np5m6hII`5bv>ih;=6ZCZsH_8 zS&8X|#i*a4qaugA#P#Z>aBy>GLpc+z-Tr% z3}#4qq?K@<0XRimTHH3IUjr55-rriQ=b1p7!4#`q#AZE85r)~RqXNxfx|at$4G?l+ zpEb?~@~5I4%xADj`?af@UIJexciZSK@3nbMn^Loy5JrQlJk2)S83j2HS@Z4D=@u*+&5l%J6W8+6 z2KM3M>A<fL|wm-9@lg*7gu`y#xfpmAZ49Hx!6lRiNxd=bE zlSL6OHvMbH2sKGGFZXu><*lk2h0gFE8pudU{s2JA_+o=-FoUI}nw^gsa{LLC z)7dWfZ?sBnC<#6BtfmQ=n!==337+fEhbO3f%}kAiKdub!9?#_VCPCW+aV8HJ3l49* zBCFY9_xC`C@e-B?@M6$Vu$w9f6b#zTl z=fuJjp`+&@Q zJ<8Jm{U+SsnRerg39*CKoJLcP4|!rb>M-`t*o}(f+o-q9UbR8>I+#e87bFk}n&Yu@ zAvqQxWs$u@UeJ+wf7Rr;?P8qC6H^O1?ovT2gmgyf%~@TYt@Xv^i=)vp1g)<|U)^aJ~J`~v|+P`uW(>4ebTewdzh{jqpa7Z(mDG4WWSu*o7_PjDf1(0Lpl zYn&=kU+lgK7SYl$?eC(+{r2%AKANWA2D55*+=e7q$SG{=!3xN!gu-|KM5kT%`{=eM z^JAre4>J%scdCv-7up~=kj9gLpT359H^TU0KmLBP6HRfsI6E-8pUH2@TgY^!J{U!M zJ6Q|GXZgH$(vv(;yiuj(w>f{S)6Zj7Hdl=f>CN(qOXhFv`vmP@lZK<;^3hu`?6OP# zBTx)DG8rUS=7spyocFiQe}neEj-PQxSxw0Hgc5LIT7N82G=vnSkLPWtQvm#CzEXjB zsd7?WM$p*uA zs}UyqQ}4j+G(hII8!+|VdTN;p@5pBVNI`4oC)ry!-x)e@cjoJ|2{k1ym%9%YL3$0 zGe_7O1IOPv{eRQ7xXP**&k;-n(|t^LolJ$u7&zyVF>r$5emr#yT6>g4vCY(vujM zh{N*_8=ICDNEKaiC0#{LJ zR_iDSck%9Ni_ zv}jS|la4ua<}{8SJ8CT58!+pl<@w5NaTfpF#Zb8UF!%-p8bZxNys0W8d0D;I9N4!X@4xpRwr$;tKK=UQgVCdLEBqF^cJ9m;8wL*@g5G`lAV-cI#$Ig09-pwV zFeIwuwInDwSf6}*4GU{fG;p6h7E6Ma63)y8Wv@=0I-i#K`v<6f2J0RXo^N*&L?cqi z9(FTKUEjkC;w&y?t=q>BW~ZuZFQeryLuHDFI?2|%F)qLdjCPn zfb-_di=xGf=?N5hP5GeZ_c`N^=XK=0Yz-bSdp4Ze58?4fM@1t_9UJcr^Sgt`!+qJ~ zU-!V-fa2u9ssq?MZL1#B6&r5*KFnDvCm#O%Vbp!7ZaNV{VYBw9wV40bd_;#vyTk)$ zqQQU$81&;Hc>6kct;{*Q^DKVo_XDmTyK1blkEI8W0&3UXU6}UNG@Z`h78D*usue-q zUUgBSc?A@%To~E&E3sx=Yn~r^0x#^nfP*UzV#kafxP8@j?=G)r+55|4=(M58T{O4N z``Db18hnI{doLPGX<=%9dNSL*+(zx(SZM`*ulcp*cs$QzKo7dt**Kcd9i$PYfyGog=s=N+tF^ABEn`DHxw{Qt0X#|}K)vzHb}10Q`9FTV0J{Cxco zuY?90$F@!)8~a~;_8D$iF|Ic4+Mz?oju}$^dt=Et z_)wcTVFC^xI%KR#rAEzK=-I26j?vw>XOFSgJ+yAq2CY>2^pml;5|XvCK(R4wcqbv7 z5>thX6hY<6RZy`~MU+q#1O=8ocyC?3a#icEVAivFfgWofD%oz1uSBy1jDN?j39_iZ`B}$aAxes$_56hP=!-{3h zWR!?RZ7b>x^$+KR`RvOtk~g%0J1e}%@JJk9dsv6OaSDKfB@1fL3DfyIOH^1C_AK3l z=`T!2bV#(ZHa*#kW=Ef|`k-FVdg;zvOk@oHc>ND-nYhKJ0*=D%wRNwdbi<4_jCnp5 zeY6P6K3V2;*rTO@<9u$v4E+U%mmf}fwpoKBrROI-QLubLheDGRdSBds5kC+986k&4 zjJ4^Zm}xt_ExLW!%~)ffQFTVy6>wcgb+tP`UuFq88iG-^M%ff_H6E;ifs+R!M*+v3 za#=+LoFJrS`}Q5!vv(gJAM|L7O=Ok-ihyGX<>#M&jzx)O?c8EHk6le6frHZ%YzlMP0=~g1nTNNL_00dNh5^qh~kDWVrChsf* z)KKlrFO>$!nKPHM=HKIv3OE|j{(bvZgVtX)=qn*QIvS7m>yN!koHS_I5ECbTkNCKF zy!-ZBShi#-KKC?` zNXw;*7q##^e=dEZgQwJu_pB0!js%4z6qEfPe#Gj-;Pp4(#8c1Op6|mN^ym#yN}ht% zl6D=cIeruIjv=FxI*T3UprYuH7C|BKIK_DqPs= zO-8}JWBYb2UA#mIp=G#v^QQ4m#wc8*C|b5|r388hE$FO?bcW(~YTM3c>@$twu_1%; z{0lE2S1!kf%7@;BapN&@eERv#_*+k&vp-1IofRG@k^a8d_c}=jeS-<=KaQg+T&*xF zwXB31?P{QCm7+S7jpK`~T^kb-HzF`;&?KB#e!}LY;C^}i@ce@3QNB_6bo*jf#<%Of zjnR8X8*9>&Q2|G4b*qKJzYWgN_$UgC@twxw`l;*28pl`)xNXz7;g`X`XfbFFdfqCw zs)(oOK8=82w&b1X`kCwaw%fOQV)HaET{8l*2jKl9@9W9U9U%hF!vq}bTlL4DJqqbz z8%55U^K9L^4SV-dz&)nxJy-Bw5pcZMX3m_6Z@!tJ+W_|9JxQ@Mbm-7D5lJH8GDlKk z0z$X`fROd7&E=H{CM!u!XhI=J;bszV$%2mS^|E?2B>CmQ^S}7u+%;ooPC_p##ao>^ zb&WOuF3t+LWDCp!Y%p(E4~nrLe~eGZe4;&06i+X_{E}{9U%q?^dz6q13JyXbV_Vfh zaALd!b&-}YU8=orO`6`1+I8#b&rVii@3XOEF+nw$W1<}!rtt5ete?@&X1EP zTlJ=$QzConPd{VS$oDa4)~rm6H*)#XMf~yGZy5Q`yO{FBkGOU%gI20wC&l#Nr(yP= zGxh6x1!dmwRsSVLCE?QcOIY*i8jR~O4r5!6#UC&Ifz8u5BlNs&Z)dhV*)Z&yV*n;7M z>wjEtQ@oK<4NBqhpC8Y#cq8u4O*c-sbc}P0-9g@NisNK2t z8JCzChmc$QaP0aL+=xD_exmE9zThv9Q(_<@UbQK%hhY4n-FRl>YIIpL9}g^;hh~4x zNBjAU@YL#!_-5Y`9K0Te$hdgKCuZ@SDOi_n+_(v+PM^l&#Y-^u)6Xz|!Z$c{_=x(= zsr%=4t5&bZ#EIYGyYDAq+xG3cvzFYtbqmW^ti-;3`!Rj`481MGxc-{I0AoHGi!Z+X z8f(|BPZ1payLs~#eEHSa_~_#?n4#*VkdV~K5hh|da^xsZoH&6QGycTSQ-9I>HZ{Pn zUB8Z>ewvC;#(s*?AC1AJ$v@!y1$7?@S%&S@(xpo~UYIiE!MB;CYo9dE+l*ZF4xuXX$O?NO~-bz_a^kYxeK zhtRHFyRd)XK5W>q0pCyj79p1})8LtE{+&Fdvn}67h>Ca5Zgzgit=(9z* z9cq0H7Q-+*zSF^?A9L*vDacT^Oc!6x+(pj?OX-35PD#vV?FFb7+~45bm)p8`K3MR89TYb_5f&l$C^LY961YRZW;x8@2a$|g0f9*OkQem zboEg^ak$`lB;CA*`1OAw`pc&g_0B_xnD!xJw=G2cnf-{rb_VfRP9y%5y05p*LiDV+ z5Hr3D;^w@H#3L(}kvX3MFAWJXNZS8C61Km9xP2cZZp&jxyrTB!A}H2&pPht7`=7^( zBV#aa-%uR7zT9{>YlO!}W5eas_;l|E^jtX;kN&d&| z>UGRIdIE2+-;PGp7o+9O75He&L2SNs9SKo;uFbsGiTiWMucYxizcs8~Vwobz>z zYR?QAJQ)4^_0i+e=(*zlmMvG?spp%!nzKWCw2v8L+nWYK1L-3;+&5_1%9UaBymNtr_N=v+4nK5D%)a5xRe zFVfrPX=GWzQJA%E(;8is2+I~6jFsvEHcmCH$)oBa7^v2^X8EcdlIeGn+PU z)DN;&>a!Gaix&KiC5skmuSmZE{j~=#ATZ;-+rqy?cvv{*&YqK!w1r`lKm9lb3+B$l zP4(Iud^na@6XWjcH2Ed22OB;3wK zg>^*t1g2#gmC;!oEn!1v^Us^LFmnV&+q}2tAu`l@=E?ef>+5Wh zj$rttJ?|T{N|tL0K4VGn2-N+Wcz7qGzkLhQ zKa51&-gQWfVFD!MeM^k>iBY!@zi$y@=f95lm1B?;c0T!y43HFm3rUw(&=9z!^js(s zZtOOevra-%431oyqr^(`bA#i>_g`J2UuT8wSI=O~ZY9KauEg9^`*0~D+d(;coU|+oNigDrnNUG4kinkJ`0sp;gP4N@y+DA%eSi z@6mNx`*!Vgwz^`)ilI}-4to5&Da_8-*AFcocmVb4*3~`x-m0a^PHuYR3KS@SQl(1j zFgvz~_V3@1)~#EiTD58@Uc9*eE+*|-xpI}>7w?U5wcSHqAJoT1LCMb_G{)6aV3a6P zTz5!DMn<7rxw5!@J3@a($T}uZr!#mn#?P7FLBVPK3sgex>OJ7&$E=hl5vSfT#=Ip9 zIsQ!+Zd%YO5$EfhfX;OyQL#w+d={3leaR^%Dp#&-tnn-|E#P=_a}q;N6HrP0_!nP# z5l=kzq)u@0*W9`I>dP4(f?g9hRIAAit^BndhEE`SRs;PuHB;vr~Gc?j?Mb!S^3HKna$ycFSXfAJ@Xy8lAdy!RSxM zXyIe4v}ZyW8yPfnDlz@s3(sTPpEGQh-)Bt6lwYRei?6@J&?ko>pQ_kwv3}h;M61e* z72#$j&}Yw@Wph3i{-)<%cmdp zv7XL3U)g+U@JIuzqSYL}e{lIhV<|mslohX89QjJ7pBpcuaEgGS+_tmZa{fM!a}+!T zWk(4Iw&{;`=m--_9SgmMaa#|g&z#NJcO@+O zjSRx?D_ppcv6KvxbTJDt1>D)Q=X9Irv(LZKA&)GRfA~>}v)GjAlEAjvPqLC3(a4oE7pDF=MK`XQMC0A}-`Apz*$QjbuB{(P?K^b9*AvF;X6LsP zzR`yA;>$1VeNZGU{`+tA?%q>>$IKajCV%NY1daYaC188>>S?z$Y1&j*3f5rO9A>k$ zSE^O7o@QXJIex0b;Eel?8aK9C8aHWz2U@g1m#$s)^_e|y9`fe3-gu>0V{aovV8@Re zhcFxC-RK_Yee#47gjK3k(MH5Uq&!X*j*H~nEEOtNL?0#a-+uQURI6bAD}w0fl#s`spp{8jQJQ9aZg3A%)c1pM>mKlo|L zPdK>xV7eY}HZE&*uBG>#=#{;Aa)J^EZ5yWe4?S&r{G6AMm!31t$3N?pHA68gwybC@ zrH9vq^Vr45%Ge4|ASvWLVtyQhgfqwV^WCRl2?Vt6i{Q8ZK+yOd2zqZ3f`-pT(1^bg z^x+l+y*LX2%^!tVZqBD?c0VP=BjMOGB<%V{?bNkn*j$g59v3HwPKd*_qwDa;kqx*N zOF=@z^n#zN8%ku$g?<&PV_K^&*xs!l{_4;LKR(bBQ(8WN`JLKhf8Xv{)}tMss#O`K zbLD`aF$D!fC^wwFisu&X!r^O~@Ioo!V>w3q^r_SEQS~InRgRoFbjRcH*IvblH{a0Z z?YBl?*sv$erawxXY#@8KY-r!Uo!+i_v!+D-6S59Fp^qIq=2SQ;uMsmBihy%HzB!5^ zca47V^$Sqvndh2C8G!NEw5-D@Q#^6>!X=??tw=O1Z#%O#ZTjh_pV1vGo=T<#+-22p zo;P={&KmjblTYEr|2?lBcBiy3qt}KWGC`4@2L-~pWuew%7!n+E>gOYod9?$%zR$kKPK6&Ma??K9@Z-b01g z!kNNeJqjyk#VJ*~6xw&{XzU>^{5>q&wro?^F8%ZgCLVqCQ9RVGn-*>M;CHiUq(#e? z=-a|S5ZrV zcp4*jjl?IXKSAfwosp|-F8e}G{oJz~&*J;u-{a8Q^w*0S3>)=ngkE3wLJ}XE7ElDv zdTEx*uwwe|hvi13hb zn^`ysZ6SXd+g|95z~iaXri!s-1$D|PD4EbixVuSG^;68xUn2hKK6nRbgMX`@N}zoW z|3M?+U7?;5{uZ5RYT2t~E%7&aV)ld0F{DO$ zT*3w>gk8IK9TzWN)b&i!qD6G(IQY@nU-X+(osI@bFvudrzM} zgVwED8GEn|_wB8olM#_odTd@;Sh!B8!q?0nkX)B}=|pf*QMMbgf^|3qWbfWRPI=(i z&f@jFmjv9ABS$cQ{`@QzCf{)dL1jB2XQi(Ceoq!|EQWZa=S}@h4yoj8{$)L#1V8^I zJlWwUisrTc9BAIWxelN7WHK$_H~}-gH#u|W)Ms?r(xvFrtGCWJ$VT^Hix#3zUB@Xo z*@*Ab^+7$y*GHp1#Nfvs!`984^*$J9-c>d9`#(C+*jl(7$g3V!*0`^6BYiz5eg9o@ zZD<8wB?LJKpfinW6Wo^56mUukHlQtGSj)i!2OK88;RDv%=WpBy#h=rsW7W!4y64T4 z;JIPp++h^-zW%<*Q#=p4zS$KY?)wl!XAec?E|n2nK#64=r#$vXEXH>nj}5|E34ylp{#I9coub^NAbRUX<$KHT< zxf*Oq7+ctY_FJ?Xd^-$>PomNdqmp@;=G!6N% zu$x%Bc0H!g_!G;PE!T-iiWMuW>!D5^JLto*E3-J5&Kk(8kFQY}P zmMEwM7sDpUeB}Im8-xv}g9i@kEQTD++q=&r`0UFsv3$*Hd^LU?+O})wu`#Om8tj>( zaa%&+)_>qYZ4k$fS|5i*8Z>B-DVH@vRG8GnR$5>7gkHODKhZ^%DpgUYY+0*4WgHLp z(+@xBY^wK4Wyd+;8gy@fCx3eq{lDvv3hgRb4TmI2F-e&F`dn8lx5*ko`M_-9KW3Pf?WW7ZD+*PKg-E@A6$TdfK&imfJt znrLsLGuTUBVj?3Gr8~kj@0=|XViL6XQMh|aV(9pvQ;?L90KfY?!ME$<>VcRsk?)nO zD12K!5APglpN)wj`;c^Qldyw2K}48_dwI&IOqSa}5aWZbrL2l(NQ zMwRhst#T=SRD2T5*nI}Ot#sU)Bf3l5I#FZ{x3XeTD z5HCFcKV6vpv2x`~`h7+eJ^l2PDUl`IzFfI-=+oz63>`8Uk3T+0?~me(6ey4%4?WZs z|9kFPwXf&&erwdIuFnUlSGNv&bbm;{Hqo(T2XyY#QSZA}t(tiC)tB-7|DHqt{(VuW zPHnvW(u-=F_S!2{tY}dky4k3a^^Q*=J?y}pGiMII{PL@mv4)N)s?IZ=8TS%Qy2Ia} zmEy{26}$uUAXmMY;Nu^ZY~xzIsXzL)QIyr|{x2=+l7$>|%QUPMg;upvql9P+ZQ8U! zrAo&ax=syWddd!C9a!NrV$6TkmnCn#Yy!9IQa8e2IBA4U!CYlx3O9gC^I|Ar4f z{1DAsJfIqq@wj~XGR~YneNQGb`M05gUA}Z#$9mf=oQvl0VeH$x7wgxp$5)?!fdl*Y z8+%A5I5-;~eCQ$6SK^GJBDS(l5@syTEal3V(_x&};8-|o^U#@7wR%+y9y$bNm5Ad# zXUXPGoACRuzv++{dtMk%BPeGO>UOJ(r>8%St{-)US77?zPHtbmjoGiy#>E5aCxtFr zxhR@G(G-9tjeEhO9uhi}o6@b5W9*_Zv$a9em4C=Z_+ zU05L+Yk12iA@MX*^?UyFC?+8mE6;94XjH1G@==YcAyu29L4lISn#}WU``V~eByY;; zyAd6WNt;h1)(LxMqmI)DFcrUWFJk=}N}&=K7KT%&PGR-xRTwd11jdg21g1=@j$k{2 z$@(lkrGo46FHi^B%5+loq0t$ugj$M_OZjUm$;%{LU=ki|5P=f;lB0{tlqsX`0@C0(qv4Z_6PRt+N}rY)>94njKWY%CY4ybW{sW?Zp!2z@Xh!M z_(FZh$D>DM!C!yfQ;q7o0VeL4J!>Z3fA?LRW#qf>;oZ01#v88<$Fom9iC=#HImJW8 zhZn1QWJ(PUOjmgAxA@S{p@u_ghtpD7ra)%xq{K@)lPWyhewgSr1dbR+ZS$Q zV*iN<3rX8^O<~pSv1TaTps=yV4E2@EK3j&!FzX^JqvR@*3w65H(Vjm`Vk2U)dcx`y zFX7>phjDb3b&qeM%7yfNZPvuPEjX^w-u8S3-M*H|dAh7{^P>HU?Spa!IrP#AcLC~_ z@$b?f-o@?D?PnJ+Z}`;dqZ%k?oh0ghOS*B2H@>mLbGdl?D)wACoKla|%IXy=i>{?> z>E*1#5Xu)DR?uVQ2*W3LUATdh>bd9$)+_XmF~m{0_c(d-B)vg6^ zjxpne-6j|+S-DCTJ=fne|N9?Gmnp3uVw|+p(s;}X%t8+*D}GxNK3+BGof*Hzo>op( zz;I7fVjXihzufP?{ig2k#dm#&LFm~~{Pg%w__F<%_@?JK_<7jRm^X4BcFfyhbB;EO zqWd1X4-b!j7&%K?pCaJK!5jGX`Ckzok#_LAUw|J5eK*Le2hS_U3o}N{FqX0o!yY-` znl<+Sy&uQc9z)En7;O4wlhyHeet~{y`)XS~uU$sSSvaTNaLc5GBwRm}Jm$p}9AAIj zcx453ni&Ebn8Pf&g>YBmldlkb+xJx0%-++_F*Munlo16$c zi_qxQwt=sY4_X$hlBuvsThuO`2W9i7hOh3wd<%OohCB3MvhHFCq@%=ICYaoUNlvmF zCdMp>GiT0nX}Fn?o|7j}VCvML&B|0voAx{A&6}r1+mR!Ob*F7c>-;WVx_ImyVxEX^ zp6bX}v7feAK5F^2HGkylWz!2Ye0`G8vuOl6G%JCgJ$v3W31>1b;A+&WiTm$wicVcR zqtAf;+5_|ai!bVF15#7!Iu9Gx%r^Mu+izjSJ8$EEFTSAX2J6hMdOw4X$0i zroF%0x4R}R&|NU&V&wHm+&ptrPtv__&OR&~vkbpK@jJ$M9glye{F81x8=;7+)42|Y zO&^BfBGwx$hZi2k`d`)?OQ{sATnx>hZEh^3W!sP2j8|DFAV&cDecR8fnBwpL>z%)F zeB*H(`{!6n2b?)&8ka%UwpEQK=b>QAS;V@qCG14vm^*9PE;9-f#vEE^J6kL5h#o-- zXW{OJPY~aR&EOSmeSB_)@b-aM=|*W9AW5;efJo0DLa^e@I#wLU5>O#`QB+dJm6XmC zjHV^>rJTN)gan+qaa-A~Ydho4pFgh?on)r7+MJu}^Ltet%Fa~5eXza44qo$;6^8(y z&6O)x!Amc_EbUScf^8pr?Nd)9(|6uSW5(dUQSamR5hL)@%P-^UXP?2KA%oGXbt|1> zH>2<~=g*x(!GeYGP`B=Q;>jnKAbbI@zA+qcz5A|CJAW^ocu$8thMYEH@uEfe^0Uuz zV1IIUK!!QmnmC+X&&0i9;{3U@S~OaN->WmCItk}3{ND@DZcnGRaqNqE!o7t;wjhD4UXuRQvE)d&7L(}(EAN5*6OAKP*D z)Kz0mGPJ?QeHz0zXZl|Aq&U|$lFBzJkBY4-rfJNYxVZNsHvY5`VVBZ3pcrykyIXDK zF6w+XI}^;}S+Rxvx!Er4yMXY^);*jVfkM#Uet0bTO69Zq`-Qs*)qwM<+r(JNI$n7# zvalw^0I{wM+x<6)iI2gRNb@nuUMQM94+;c1|IBuVU_W0J${wgLNwU$fPTF%J4Dk*G z9P6{2H^Wk*IQW;t0n9yPTuw+x)ca)p(W84e{nx}bWBbt1(3B2M!gZG~hv@8(e9sx- zKtadx|NIOShw(d2@HOYFJAUE>PMkccUz^_NUq;HDIB`;^aSv6WmB=O6!a9)S=wDXt zUTaZ2g8PWSu^$mfr|J8gw>CG}0bY11aWKXN1nKQ;&-e>mFM%6XV5W72ouVZ`gNW6*#B82!O0tY5np-iEOh zDr{x!jSHbs+7gIy@viM1)8A!OF#jgJZi5C7#?;?`$1l_Dmnl=H;$3xp3s_{3q#>O+ zeq3KaRyw|_0=3-6oONs08Lu)1DA4>B7nl6R;OBMJfyMJ;9@Px{0)oDXj2terZf(Gt%4R<~E(Oxsw4z76aqGr4r&5>9VB zt?&KJ!sI3Ee@O4)G$%N3FsgN|W-JMJD_#LgFch;s^+l!#%%%P@yLS^4kfa2VCvzg~ zOqxa&lb7TR%%SUdSHitz5AaKwQh@AM0?tt)6VB4cO`A3ymo8n_%M@fw zmoC?T+4$SDXD{Z>`zxhGk?W@aF$0@7Z^4WiGxg^QLnN8vf6YJt;ONn#SiEGZo-V*1 zF)^|D>#zCvN7a>_k89pv3$Sa~?i3+LVK;xl-#B#WFb=D-b?bI@{K@0|c>I5Z%QVuVQKs+bbqaug(9Ozwas%V{Rhi$RCKibv{+J z>A%Kz@wIC6Bqgenye5T%XZ^8wM|Sv8rn@kHhKDd@FUW`{>d=cF`cfqSK zzJyuRXXp%mb?etNwsIb3FnsR$=TWp+G5w&MGiw&!dUFK2s(ruo{0lgHMpXcAg!7Zx z@*JH%ciwpAJi2jbc^4UE%NC66HnJ(+N$C17U!gpjHfyRqQTECMEnDdM_@@0iBV8g9 zRxDdKZ%!E_*Q{AH{nr{x)jeZAmd;rirn!IbUaJ#pGM>3aiIUn2ml61osZpZ_UVQ0g zJ^sNGUPF!noI88AvBvXIk5b$`c~egjuzK=pV{Iw{*#gk^g|^66EdBjRkJ^gyE7A!S zX0>FNNS{FW$B~z5PzIITR<>Rt*s!lQFSS9=LYeQ`FWaariq^Cp+C>lN`XARLE-LdK zJ14iC#DOITj3w(7u38uc%1CyzyB)9KY`VA#FK<6{`KIo3Hrzeii?ZqBHf64<2J7eT zgR;5Y(NWq^j3gm5X3Rjm5`_Fr zyLN5S^}){Av2&-M6r51Fak_&}9Xp^?r;cdTrWJDJNDkjUaq<+hWzUArojdC31lqT6 zhupc?At1N{j(3|dVFDUAZhTLB-vtY1`9YaO_{NPkY@Jo_fIyA0K$vRrh0`J_zisN{>G9 z@6;V$nHcwN3nnu0tp8r_bQ8Q)e*m zTu<{?!Mp2yzHH<4ggzr-@^(Rgi(K=thB1lyd%oA!P2 z(j_#gUmyGS@7HackdP}VQ#LtK7vbOZmg$8Dnm5y7m3*HODf{;wK;C?LaplTYT)1#i zd+PlBd~x<{>QpG^FpP7@&Rr<4>R5g!Kf?}BiiOb7WC2&Wa3Ne&>-X*3kINw;X|g_Z zqHlf|y?mEc8$`y|WgWH=*zS4isi*P%_mfbhNYO0SFP;FS-deV7rAo_8ml7pP7<+vMe~M}n|wPihAn z?Roj~W!h2X>16uJUbsjRwC&IyIdkPgvlb8NIp9Wp^pT#V@}0Nd#;Et-$M@fUs~;e2 z0J9-%4%P3~D_8U&;6sNG;kT(%b;^3q$I+u#FDzTN60g1S1}avng!>vd)`=&afe&9^ zpp2Hh>Y>~4zWa>5q~+YXvsmV&V(0fWCZUp3S2l04-XzGG3*^`2U+X zLiO*@U!VY5wP|hhH%^@JjTZh4TjBP$;B{eq|L?#4idldDsfC;+JfC&y)ffdqL__QAdpSB;ur?o20-cjqEc)t1N1Zs?eSIv*%UE^c;)))<+A|2HZT_fOpyaUjr z{7AH@@IKn8za7fIhbBb_8}Fua@Y)Xiyk`P_**h6O@0)@td#B?1?X;O&qvB)n%c0d6 zyK_E1+OZHHY+sCbw=Khnt;;Zc+j6|TbroLRyc#cTT7&;>_y_-6w-(R*vlhcvufr27 z*JJSV^%%5t0|qYMgnoZ-LZ1by%u{8~CQROb9MN$ZALBv+$KH9?^C6c*w3mf-6z8C& zP%2cgki&YL-Y*k;CWPM#$0a2O*;r&bcKjGhl`3U(yf!<1*=9O-{zA%UPM$igL_-ne z&Ye8ou6%_GXw>LFoH~74HAGin*|O!T9hDr_QnF+TJskogp;oL|iN%YT=rMFc#^7~k zR8G^T&G78A&*Fm*M&Yr?9!r^oUAS{3CB`Fc`xl70c2wCESJ1XtDB>v6ybZyYyiN zvvKO=DLoLGo+LhKc?0l)&lI#*uU^HGLx(YW(j+Wi__tNTQL1z)eXcy2Cigex6V6yl zhD$e$!PEgtY(MwH3+lI~_E?(3`TpL0`)!^4WasuB`o5s2m0~_j?Slzzm^@^gx_3VP zWUQV%+%z`99Nr^Ut5rkKM;^{p=2LT6K|c2Q5xu>9-gxOFY9SJB7U z2W_5dqZ+8j3NylvhvCwZOU6<%+`f8;J2M7=-l%S)yE*j2RqI?0rR%$9EK=T5d2#=s z`|W-wMeVAuS7GYlso3=MrgZ1joGZt!V9A&zm@;q*cFx$TK5V>Ug#r}|pu-y-j3q(v z92h2fG2$#D?M z`aK?h<;s=Nv1123*rhXKRNLg}(PR3(|1xZc*ee`{kvn&8J*1+0_a1mpwfDaHW}==S zu1}x7+5>m5q@MRQqE0SB_>uWam?hhGvYAu7slOC!>TjYJa(vCd?ELpaT&x$qUy>Ua zZ&+{7tzG*MR>h0s{h6V|yz;{9qK8FI|d)>ftoOR>m8+QYTCQq z^%uU?p6U zzgbtZ>Ve2H0u0k;)~Be$6If_fv41a)71TA zD}=FV(PHSUu3z1{3=_&aRjO9ioe50N!~6SQgpHnq3lHM#?)2F;OVurfVzr7HOQ|Hr zCSvo9&BjtHP5L&`4YoT$$vP#`a6m(2DLvWqXGgtW^;|Y*8U1h~Qc_hAU`z2<+I#W+6jIKW#qw9^v!uJ>I?3wmp2F;ewwM5aX zu1&8lxCpiuqN0(c8qKR6XP}5a*0#h$K5oFdYzDV zF2}5qgxL67h%rjEDzOP^lS--PFskLNhXUCOBiKJ1viY$DtLLd@o$7@^UqAI;fG*it z{Q1{kzs{lRmz?T#PP4zc_?mwMSp1M%6|T#zmian&pk?t#{@?&S+q5il209+E#7^mw zB}(A8-+tF2jO@s}ze!`&&|9lt=gMW~HPd=9eJ7?l6jS^>$H5&rdQ^8@(^Gc(^eJ6O z5(-NS(p$GU)j=Y%De>01b0@4?y#~jQ9Yd75*BC;1{`>`Su2T|nDeeHf)H=lYdB4e*AF?e*N_~%$_|*dohCTN1uNEJiaG~ z^R02dH%sVMHKk{zSZ&v#gBC<3N|sEiu*{*@qv!3PHEXoc|4xaw?7+69-=2?k->t*>UFQ)S5vxNe?LkjtlOauT|Ks<|*tfeCRq4k~ zn~uoALlF6-D$fo^%=@n(=~{BuL06D;emml3JchWLs?2#5@e7Bkb@nrsYGQ97Y0G0s z+|(cOn+G6v>p&!&ou+o^8XJvQVsO==SFl8tzYo2Nd52%a>a!D!cT*{uvpj~?c@0n1 zeF4wZ{~w-j@C=GtBrNgw@kNh{P4L!zJ@8(mhcK$qgZQ{n7kt*F6TWQR0ppsq$M~jg z@lCTfnD{^&eBZJ)Cbw>hAKSLX&+S{{*N!dlTbCA?-nBXY?A8o3d#KV&m4{X7+Y~eV zH^r=h_hZ?U58$mfmEh-OSoMAP-G^$`s-t%8T6&pYw?>T`p>CZzT9BE@nIi}4)vKE_ zw1ag}9kmUIVwhl^SEELCv}xTMJ9qBFEF~2FUbqma&m@ntBlNh@W421QlT1?&Fa)w~ zo7ULAV<+a!ou@NSp1*KGwJVaZV{A+ecJJAZg=$~RSFF_85X%|k(%C58xBr0po<&%+ zcnK<2tc0pnWTI(?tk$eqGfm-nt5~t3&MHY~gS9q_@b8h75QFeTa}gDCC{?Jb!sfMh zywl@mh&KJ&2%Aid_rm$Bept987*VnIuPMiw&YnHnYP6gu;F6Xv9~yl4um|hmK98Vl zw{D1vjKt4B{;0(a2SifXMMp*J6UBx(MGPka4-31ArAwCJtP*W)+qK1j#~wwIqD9f^ z!Ol7(<(-7b)a3pqdj7fE77y&Kz=wN>P962+G%iuqjJ?M#!@PqHf~5`2&|lX zKk|s4Z;j{E6`_f|M$C#ie8dR!?cdL4vXsoi{Xh6nS9Kpg=CbR5H*$E{Vcfo+HpvbJ z3k8yofBFuF$g7cv3`^VFPA^r?qIWJCPrlOm(D|LtC{!hNEQ(){pZ1b<9?@A(dhKes z&;AqpqjvY&I=P9vpy#dat8MYnsE71#67Et|OCpKMA@=P<+yPY%?L+*DBWzV08)Tit zs4yg6+KTifD*Q3s*h!Jyg_~%0L9HmeqdkK`tQ36%+q@AjTp8FiW0jQO?1Zw9kjym~@ zp-Zi$x}%`Q6gqm0cAeT7pz5kVk35XNefwx}W_o@xT(W1+9y;8SuL<94*suZmscj$b z-3$Hu^+i+FE-`%6Zrp=q{htn*%G zd^l>?u5G;I*-(r>*!975g6z!cGwHmhyoL{U?TU9szK0=CJb{v>N@c1!L19%@-A6CH z^b+2D`z>@<6<%htPC5gn3Y9 z$3Ms)T|exKPH%T|SqP}`U&KqRBZs zznwe@uf6^{nmzD<&aUVP&L>w&?dzd#-7#kDrx^R$XBha{qiOb^5$O7_QM0BN$(E&1 z2Bub^h_?KE9rYXY=g+u6<@urbe)RD{sMnxD%KmtrIVjX}mV3^x$L|{U^ivo=aU$Lu z`99jVYlp%`3fm2TWaY&Bi5{^JKmG_~KmQDm4;`Y%O4_O@xxR{e-~4x)N(ptp@z|Y# z!t=$KUqbgDJ*@7Fg(0z?3McvA{?~S6DU|}{3cxRW`g0UJ)v*0(_P6HsUPn zGDmP4fQ){IutVG0&_^6g!_cA-PZ zj`(`QcpN-%KxcB?zHJ-&4j6#qJ^Zj!qJ+nJ{n~Z)*HtM% zy>wWht%;!dV84Cqw)z{P2MJSva!wYC(uDW~CE9%TM4AN(7DUmaMRnt!qBeVu9Psz^ z&r;)(o=c7!OtA8Br?|7$b7u`^$c(p0nM$$4eV9TwG6Jt1r%T{DFi&JbuNc$s+7eiV zp|rPdtIw+Yi~~r|srw~VUC$^j@RJY}6pR8&SQal?T#rQyR=?c6bx@UG-|(vf64IbF zNP~3e25F@c1f*djA+hNW=?0bV5)kR`?(Xi8?rzWGci-=OKkswS%=zogJahhGnBf}s zzGAIwt#5oj-*to!<`zAW51|KRCso+)a^oKP=NhvVXD)OG=Dt$a&3uu^(`)_l95;l= z({cvUn}aVM9M0Q6${uhKY&vM9DtBYm^lr4qL7}5bV}V3iS62@a*85hoUJV|6_eWb% zQX^?na3C2sXJpwH5pnlRm*dPvRON<^jiz#FI$SsP>SF|A*-z0v6#TnBJ&=QItoak3 zFXUY`>-L#3iyF3F?*vliA_X{Ua6^M>_MbaPw2sSmEBf&0?BLLHcTnN;hvQ?ru@UzE0YWWk5M z80`Xz{a!zCV^7&herSI`@l6Sh=wtnhd5pgrsk!ckT=eqT4#}he?wiJx@(^Z&hWd}k zqF!1P!&SXb0YjhJU3RN@-S&xBBi*oaq>qUujtv$96#2WFgg-&%a6x%rK!ya$F58js z8np8Fg>+q|1yCR2sKqdU{>F_WST}ES95`c5;Ufb~%4r+g=E?f?u ziI3zoo8%lQM$;_+IAwLV6%m>x00$93wf&sfPFkJ}wlqW-LemrKYou{)=@KD`#H1L` zfE~(eGQ8F~noPaA$DQ=|*1mlKUFi~kSMnTHMUETx@UgGQGcxpY9I<}f?4#S=Gfc(v z_w`(+!VU-X@%3e>%}6yv!hb3H2nMvhYLZJDdR^ORIVLv&*qP9@ULMQ(EGgcgnFG#G zX37#HUHkUpmr|KODV593I6W|VU{4k5jBUHjIc}w4gu}g$pU$+7q>;y<`s3qsT43d> z_R^KYUx&y;*OIz_iy&XI#QS2N?N}NBp~sh8`0veYPX1Ip8OS0$7y06;LC5>R=b(=- z&P~mqPhu-cdR9-sD6GJ?mM_(dtwSr1_e~pF?bQ85kiXU+w_cER5h|EU{7&{z`~La) zN7FQ2wR{z&2*3WU$`zq~znr)>i~M`lSLnP|o5y96idNqy+)Iuw1-$7~2Ex8o_YU4Y zyR%bYo7p%19*>;Gr>n7xr?FdNPNPon_jFA-u$J(1{(JvtnMSb*bUKjzdBgNgv6B5u zi<|qD)C*a8Zv&qrhLvjP2O68$?t-qe_*m=;nJp@+L8M>(tyGlcU81AzJTmFheu`DD z+5tV%?-UDPM_H;9ewr*7mKgSY7+0})I*BpKYrDayny;1H)e+xzy($Xx&G7xgz;~+s z_myM+VA6U{YWCKl13ZWHBcETrmg7ikAn7`%#E>YZ3zVwt-u5RKE8l|leeRgOHV3A% z(k0VX%EGnY8yJ?emn|JbdS~=qK!S2|&bq+hS-zrYjZjdZ4$_-RT<0C3fHxDnyVe$i zNUpZLOO%@?g;}!~gMvfzL0}9F^sbdS$XXr)fZDgvI#EZ*} z8gB8+56q;sfmd5+iyGX$Wq;&$kHb(RMs1V+Fec$R2QOT+qM10zf)z~#;I>0TvBwY1UrlNkm-5vXm_ryN9Tlx(7 zt=Uhj$2OqE%z znkN%bZ-Xt2hK&eH8fPOPL@4hi@!M-S%9mORl57^);;nkwk{&mI#+;pRMBr`Of}^4l z&$}60w^rT^EQ-cTa9uqsEm;)S)LA#X5b~C;0BNyjfd3s~I`*Jg*RS5ioh+B?aTEXQ z?k~-t%kmEJ+sv3n*%F0C0{y5VVYHj{Kt~sHqW;&CF-fg zU^qh(>A`+ySM^B ze}6h@H0i?$5!`<2opvwXNcFS}e&~x8I~q!;z~}LJc0tCQCV{F?={%6&S?qeL!<5fE zH2rXoXqOrPEMTtG(Yk{nM;CiWl=L;*=Uz2PQZYf)KZ_~mod^4ZQAv*91w`1}mF@BY? zgokDMWcGcaajDp6`P{Yn(5aoxxXy-cdbleM1(IdI<F=jkpWWO% z-n|Pft+>9qbAGn^$au1;I#);e%j`CIklcjlDR_(6$~-gJiuaA_NrA3q81tdSd)iXJ zvp~eb3r#HM%O%?6evs&Kcv0?szg?w z1JPVBs9Nd(MNVJuu1~fl7q=oh^`>#elt5afA;NLdT|wd50CIi9IG;wD5vU7m3O`sN z9bGJM1670DrJtQvuK#Y|$AgGjM9NX(7}`-CItjmTe3{YOcPUcI4wRQ?ymz{ux7rvy zCKIK89^ta_Cz0~mA9Ea-NJypFdKBw#&UXl1?IDj(wK2rXcO{bGz4Rv1pzT!MWgGn^ z)5`r$K4PJ4JiAM6aDFSFkMPwpTmXSmB=)Mc(V*2lZ?)j{)%cn(vi-IC`?a7`_RB`< zzi4k1NhL_{NE>IAamYqr{k?(`0_51<_`N=Qz&WV7q$82OhLJzgd420pg?!`ZGsgsy z&!%}b+r``Uu5yBoSu5SN1;~!SY{#lv`s#gE4>8;0t6}R&iyJ)(%pP~vE&2|5>M?AhC zaxKyn0-|vnsM5Gxg4q z%9kEngTmHp!@c9R^7kwC=UU@9PG+z~mx2ZxnnR-fX0q#qhG@xpEQc6Ys_1KO_Fhl{ zzy|HKgKi&4KGK|9NqiwwVPs~Z+$>ck0)t_I&Ei)1Ou*=X6@ZB0>fx5PH`tss4U=U< zBSf#fBCwqA%&5duL_R#@$k8y{qs{f#<+b$RZ$G$aSrP5F+4t*yiAS=sXOO#_7KrYD4T_Iu|FzqX@6%^XilDBeYxP~>) zOgm6vibT2Vz#EP`6NK8X?k`Q_jt0co#W{{wZ(4wSF^76e_U zjWg`6pGsyK7((=0tvx4VwVa1{yIr`Pbyd$(m0QymIYP}SN@n~!J*wpGqbXK+90bUX>KOrJ|z1J>}!>{ME6wlyJGBtu) zx?e5V%4Tz&M0bvH31q0qpQ1*+uYQT+1G|8=e6tTZdKi?uTG@Ka$m>c-#H`y{Xf{>C za`xOo_eDpxj9STAKea5x3HuN>8zB->x%{B3wp#GfwfMf(UJE^5Hi>$FdkN|pNGhrZK5a#>O;6@?qi#IdwU`|d0-C%oOKOJ86uw5%4Aq9_5=M{7vJJUCO~9^riffX31Coj0d9 zo-)(=qTNXg?UPsBv|>X;lP>G;Sa(vtQ|^yle`+_pBWBfny#E|&EHIns)c@#cjI^R( zoRL>X&Zf5` zcE{Ap-dV!)vN0izq;OETU+m3fZkmqmGBQ7dYy!?yC#V)XpU=9zIA{Yq1aA)Gp`FV} zqM#T&`2x1nZZ=eQkqRNu|CX`Y^JkjP;QEt?W3BxcK4N27~l8uJvb|^)AhG zetp@s07W4Mk*kDMH3Tt+HUp@Y5^}Bb$_g*(EB`f~HY@%+Ze}TQatdalFrE<7LbsVa zwt9(=cU;9-cWy~$tE@euJ~!uzKdn}x-q*#)8Ey@?@I!?8TrY?zC@6rF(b4W;9vSrG zd{iJW_PBq(03C7{6xu4`?4gT8K5Su5R&K4PTP7vpeY?Q?)MfUGvP}SJ3`>IsIM6S3 z93D)k`=kVnx8KVpq-%tjlOeM?xHWq&qEe)T7V?TEhB3-p>RS|k z#A%4Iq?Wji@ptX0pqx-2WopfpFWuT@buNW73gM#CvCLt>FEy-B@ls~%k50vxm*`|d z=J_?Z|7PY7rV&2%D9~&zl4H2SWqGeZCouVSoc z5QZ@cWCJ=;33vRSpR%LPXj0SL>TTZeCC6VrdZ%7!oiV@?zR~l6Rn+%KNOfa$HyZ z^oz2ryQF>uIR2Qyh0-q>vfX%SuRp)rzCk0hK3W#po36ph8A=i<$&i>hrt(xPF_0MZ zRk2y*4)0*Yt3i12N<4A0Kru`DPt*{b=IhNgc8f0P@GUz|xnFcOqvGppA<%T}xAKEQ zHNV{p4zXg>>M*6Dwj|;EH+cYg(h+R2P3zh}>1DyPpOD=8NpffVQlw;%a^=siRt|yR z92`b@_qh1>=1^)EU~Z4eQrqy5k0Bv>6MZ*@2cX({B>||r^EnFkP^ypGVA2uur-TCc zH90&i691oxIUmuI;fvgUudF2Q1obe!KgN0&$RBp~tM6T4lb2IPQXGDWcnV9Z`O$G7 zTvKx^lH#+0DW2Hl+;S=!>Sf7wYw?Z%%0$nn>&P|(#|N~nRlDmlpy+Dry494s9?*_4=Qi=fF;oJ4Rm<{0K4)`0e?#} z6#x5%PIouwvqB1B_E0Z1%x%=|M;5Ln%-L+rN|7Mw!;u^K` ztzP$+ExYJqy)&EX=(_CZ@_xoadCEui!+aoM)>;W07{_~^dt{KpvNRCC%rvahG8DUK zlqX7((Gzj@gg6o_YX)^4!CdQpL`Z0h*|Q(Cm&}JbT!0Oe-tnr0OQE1<;MY1=c&-1S z%2(0M^X-Y=~h_Kp5{pPxUe zpcd?oyUsX*WV6)~pq-}5GAsm-h0tsa`{heN;DnWgb?q}DV_IfyTCl>;(s~ zm{@3T+&$bLx)4eEEk%B}Kxm1M+iNYwrBi%@0Xh5TA>Y;2wQC`F(b76qSq#it=Cz*5 z3lR+IVG{+Kcz`_$%2fS!uZ5~`4&2#omzH`qi?**yKj?n;7tqfGnu|DAebHK%Gxm&OYoECjuLjWt>M6*@pw ziD12}qZQMG3J{Xf;VFNPK^mH?oRht4jsvgIg>!J*KdI)(0K7p?z)*S&J|NSu=f_P_ z(@8HwU`R+?8-{S`qb+lPfA}vU#j+qV3N4}J!`*d>+qF&o!_983aX4Z0N`6_pMXQj(H8=kv5}NYf@iLlT#yVypgK z`uY2>0+W>3#P5&FmDg(#!lN~NM~R0VQsOuIhug6-#rcK4?|9&w+0XkYg1!7|WKM zRn4Ce%f1QrD`F}EbqgM5BN=`klb73LQd626$-#q@JfAx3WV2nmeGAURcX(QCi|v=d z)l5*~l;ST+WLf4|Tw(4)+Mz#%G$F0uF(4LT{J-SuXdz`N=c$o;}B6G|wu^ zXUuR%87CA=j!W)wJg^iUZZ_u=!-dD4+n!Y$t#n0T9NfZ3`qa6=JB#O&H_I z$cU*&X2t2oo;8wp8K=dp%2HK~KE2!5R6g71XhhtclT>XeZs3yFdK>)8WKut7Ew=$R zOY|?3Upuk8J(QNL?e69gnbPov#}D0;LR_{|PC@l4Uw%N6*U&u{rDPgxsgWK!qK;uc zU#-;SP}x{B#;e?foZ?e(yGU$Y=nS}&XAM0LOMv@##y^RHYlT{8+!a8{uAz%%d?-~( zZrZjN^^>IRA%kZRJP)0(wtK!Lg2ccVUQm4TDl10pbMPysV9=emniZ?VK55ur$v8Jf zeDc=ov-1J(;`wm8nA5TsVmTZo!%_?P-9k80HR{{%DvfT=3mhVFTzGQ%TllAQ>0*d> zyOVORbs#=iIG@z4#q*(vT#4b-P{s(Nf1U`*fI~zdpF4iqoDvLjhC5K> zbj_qxo_onxgves_a!n(%&bAl4@GYOzKOHZYpvtVcYk_rN)SHjJ>XE2oCesW(L)W*r zL}p`cI+`vB>Lm&F)85Ou?VX(lE#3$>pER31?m1I7-L6jn|FLoG9Mg5+Q_e^>UH)`) zA0rQJaXdNDoVYYRj??sqrD4f~9?6QVI}}5*Hcrqn2D}+OB&{nwa0KC8FB_1Ji}(&1 z{7r23*yk+jJ%w-gINTL%iQ_oh-hrFJ=V~sCD}^O0unNl}QRY=r|DZQRk!Mi~X29LG zt=Wpai&kqg+=zT*rc{!_hgUc;`(~BoGeX?Lj(6zj&U&{An=E-tBAZ>{4)Ef&NP9=8 z27%VlFJi#ll5^n}pBA`+HkjfC)F}97UeD18f3m52`1j$ORJz;~*t9A>sr{8q;ur(O zOAylV;~qDp_+{&n7a~gUeDpG-3k_Av?RduA2Xz=0mt|vG*T8Eg>A-$fh`>P%YM}r5 zaC0-qH5;@z!ra6(k|5|JR5=b|{Kyj7(- z<0>kV+^Xl6v?4-Z?w8nJpJD$q_yxew8{>5^r?R#l#q0l`Xq9)@dr&#fn2-u z488QyAM$aYWnqIjM-jC8EM&5oPqQ{On94pw^N(f{)0x(U>X>xnmV{@cS=kYLf5XC` zH6V>?mB(Jx)q;fpj0b*EzSvww*a(dvdA=y7tU3<2+o0t_?Z*Ha@GV-V+2i}i#Ri+# zip{^V%diuvn}6j~UYc&x7)4QDagj>qoV1d-r@0d_JZt3nK4LaV_3=#gROoYzO!(72 zB!b+cBnY>1-Dv6l`<5-)`{-zH0s;PQ_qAiIq}R!)sL4PDT>Z+PSf=Y{{AFbarivw$ z-w`#{P@TF6`I=?A2#FU1AQjZryunIsl&!Hc?KbYcIQ}!qHT}x&1TM2L7-V89CcpMb zvTm2)8LTf7ZU>S+SAhG!mZ)>vJ!N?RYAk9p+`J1W)5 zPOGny(zXn*sESyo5^Y)Lz)5k$G0)crcOMJ4l}bhO zFlvtxSDT`73pxAP?xBYTQG}mrTYqJ6C7WjbqxX@J0WnXb^)T(J2hfo7KTk0DCON47 zMYVCSC!U=nLjD!H5**ut5Smuoo!+GLzyy1F+@)2nHf&+B?v1fjsHT~yUKA3KgD}{1vu6v+K<8<@O(R;tzU&RrOkPO&)p7<>}0py zkcE7UIxr(7FVnM0dlQ)#*Y$n;UqL9dxTx!`*qTrI~}8O zL)6LwME|lbawG!3725p#IQ00H@3=aXdPRRJBLp+rs^Vg%?TShRVxsX8`M~;C)Afl- zUwtZqNH?#n>7Zl^vcGBxsUbTufF7QQt$q|-Fal9MDd5{Sb2sobp*tUgPa`jptSJ6)o%lqrS5Kdm$} zU=SrdgyUGZcven$wmtverpgBZqdBHK4jVN?+0t-eTv*>sOX0=x6Qw~-F*yB7a#9X4MJF*1hxj!l zUL`h!BvaSL@lXt77*^Gv&6)yde44J;#V3YZJUPN?49PPwai0Q%KGD6M8EYAqy|7|6 z!?^5P>M@s^kFmXv6aW3^pSKu!=1~f??c3#>LuFTFR3|c!Us0u{UE%n8F|_#vhoSf~ z{E|br%w{9a9;n^Wafnz);E0@XE!_sF;PO+aBM)TV*WM%*9CCbULcV-HsAC(9u*iYS zWIkrShnLf>m)vshw)E*=*RhgerqOUN(z6B)0!_O3I)*b%!C=8oN+iIKEFcj7{YPq+ zoYFkI7fLc7jjLm{#jzG!dn9z$34K8L^j}vqQ*RkzLy$A3BdYo9j7^+Sd;{*Dy|Ds3 zOsEfk2H+`n`RGi{lUf#ajF_`Ivc4Haz`+a%6+!oNiPNz`Z20La8`~NNOjR{}Pb57Z zQ}qK7z$Y9GQ0*lX@K`22$gCN)ML}5h3Rc1fbUqLg66N7G^pm!G>r|F?Xh+J8Eo+VXr{&1bg;p)>CyDIBP27*Lxgz2 zMw4Wzb{ciz{n`-y!l{Hd5QFey6sz-VNU}4!8#jA>@R~KAhao@aTR^e+k16x2>i;!Pv24S;5%xY$-S93E z#4>jOe=OYm|4twJj&`}ezCKjV8}#&=aZ>}YWue5QvoPXOkw|82F8$A~d5jgk2IfFAJvmGFiI zZ4B_}fMs*-KbFl%ECG(C$t2EagJ-b~^r3BrD`qlnIAXZIDvV(veS3Vz=jRh(%Ubnl z5Dg{&-v&_y$-RCXO&t1X;c=*;o^lPe8@uo##St|vC2^N32@k-QC_4FHt|fs$nIz#; zseUwtPz6l(h#q{oqJc-x=G5!|{U0MB0%cSSoX`Gs*TB!#g`fdU>C>qVPXv+Tu+Kw5 z6^poF1a=etWAXe~ln`n>S)9<`uEMw$FQ?A@qScc?29ARu)%iJjpM|VL$K$mMFDWOF z)PB6VyT$+KZ=cFI#&l&yk$9EUpR&WDD1Wjs26F=HET%F&2T^Q|2=?Q6NBhsGO_y>+ z7JT~A*(>Yc%~_lzE+49h$&P3dz%8!HJ`;`n-;XZiFf)08|H~nLgN*MtLE%EndE8vg<1>Qz_;Db@3xcb}Gcgk*+2GOme;y9h zdH<0Ci&Wnh^8>jc{FMGc_T%OLFJEIVR4(XWO3BA*?|*2y2C`w!IILRjCeg925l*HwuJ7eXbw_j$D%NQ$J13nkjgB`u)gO+|RY8>nw*+`MX4 znDIlCycW9;4&lgPEKmj!t8jn_daYS$esp;mP~~Xj`QE@_6#_N(w&MBsliSbL-rlk1 zpu%V=)w$fCrMSmyLmD8j+Zps?%IZKGU$l)9vuJR44+AmX-bcDK$M-MFhQr3%8XOA? zi#B7;CVbKjK{fs>A2{;wymr(mQbm$0dtvzE?KD(*{D|t7*jt-n(g@zeX@=?Jb`Ev=J1c(A*P}KJ9w<%z;Gt&lbX-fHf9*deu2!xw-4}t6PH> zS~2nFd&Fx6lABM~V4w6(nNu~VI31Pq=Ec5WF927Hyng!7%V#se~~5Ql?*-p|!ML4n~N z8*s))R{@@B=YMGo&cmTD{fPyY^biIPn(t z|Lj^5qnl|=E#JmAI;b(qf8Q~x!y8jzcJB5I@Gy(9Z9Eq3WC&qzqI8;n{V}#aviVuk z$M?CSyK`GvvmJu(K_+OP!IX}` zv%+gNIPSfR-mZ^9mU)|tRC&LSQzOz+?Ti3PC#+UXEeVZG^ zQtEcz$KkE3Wt`(d4ZWE*fK_&lj9@#Iim@6TeQM#IvmB<{F;bz?xc*XA-FlB^QPYL+ zy?^H5EIms!Ehd@6o3NAXXJK3^0V+=5Oy*6EL3Do_bkK5J&IdASRxY;(;Q&3Pe<6qM zdbbxa@Mh>VxPU?t|-kNrTT4?YVhAmb|32?skc|-k;)|l1i^9$e9 zqKFc|*LMN%^Gk?g$NT;={PTm@wn}eVrV|T7GSv&8K;<;9t~ZTRn}c>oItk}+he*C! zFZ8V{zN1>y_$eM(q61AAM=`u?ixPVIopO1Rb+~PZ2P4_Jzp#7Y<$4H9ZA=Bd#`R1( zN5sylF~9Dl!&I)cPW{PSYo<8J%iJT7A+2s3R44N|9pG`=&Wa7?3fHHetAgpu#<4yH zeB5%4ZKIcy6H`UHT_Dn547L!!HXIiY19_xc>%-o)wQI|X-!E$&55w_voLba(`BR)$ zk=r}64tW_B)1GvO5@Ztb7g7lck%HuIb014n=s(6fRnKGaE{Ybeg zU#L@g@+-?lcIs$7L}P@c&C1Z1=4YuWVUn`mYsaePc$)E)^mN=QC{?>3*;h8#{K7w; zY?atG9oB!zuRw1KOOF)sAazd|J36loap)j2Rc8;t?tSR|;3B^e*lCTZpCf3DyC6c! z2$u3kvY=)t0K{aB-qm9T=b4UWn-(ETpb}LU8iMULAaX;%(kw;)SW>o2EkhicseG%% zmJ8N+o&5^=j#;gB?Ez%f#-eL%H@{%5NDF}&rAnSEns%*S&o?JZT-7D_OA?@m&KNpw z@j(o8)auHXODvb#2UleX{2i2g(m&Q3OzC=(C@Ed+>bD5R7W%C%ybn2UiOSk;Cz|s_ zI5m`Nsysj14DeyQTx+Q=(s+*r^X@w`f12|KiKf*mJEfWsD<_02uEpj&h;=^v{gSHz z`nhhh!OX~SH_POHao2Bq&3x-`7_e4dBk;|;_tlAO4gT`nGI;f^v*K1)sypw|tm5qH zme;p5nkZ7#c_5(-w)paskFfFD2t2E6wbaj1a`0PfO*xw5eT5aY`&(pt)+qY*ye0@V zBY<%{o^BKW2b3nyx^@lsLR>HK%{VaH)~Am^DIA|_p_b_cPBhlEm2R<~*Q=*X9=A*x znIuvvyf4IFfc>Hj6K1JPk|Uvi2ACAo$pktqRK-mU4Sx!C$t7^M;OV-h7wM~}E8c^2 z-bjtD@!mom&sZx+UFB)~v=KhQjQfiqDhgYLws-p8wH5Lb{ z-J6#m1Y;@#!$P{^`*@12mrU;(R$dzw&W80(wS_q%kV8m8hrjV|tQm{mN{b>GkNso~ zPa79qv?k&HjCN(i_}yQS74U|rQ@=46E^JV2u;Q+tD}=W}yj~8ez$?-FYhSUr%3<`P zBG{-ckegbGF5tdDCZ*Lek$GimUkw12Slc$clBF|4!>rAc6y4i1#-<$(R8LUW+&-mT zN@m$CpB8?Gcr-#B1Co;JNW%7~EDbvTm`yvcy~_Bp`jk2&a$Jqtw959xK`=zHOGjt1 z`_EyBq_a(?+(Z4ZQ~}zPn+2lFoTH_Vj}D!kY+|U|$PvZ^30&2GA8sFU=v_-?#tMwO zjq~bjE~L)uSa-lbjf7Mz)ZR?s)JP!u{xB;DXvx<=cGst<2$-r)k8U(@5*flnv_0(`oN@!cB1jSjw-^KV}^9~Pv*&r?uO4&6;J14{tKO^)}e z%EA*y$y_(q<-c`Du6&PlTOMCmWN?>r;S9B`+D+6kW-gQc5&B^$yR7eQ4c2f-hKO?H zbq1fn+_CxmnxUS=9bEe(Wz5Ud4j#WGJ?e+^O-@iS0#XKf@KQQs%KVQ?pEejl>!Q4x zT&~oiWjA+H41>A0#=frzHv3LA+>aIcIDyx0%v#OQ@jw-~^N=rS0u-5NYEO>?Xmkyo#alYQ~-)hM{W^C=YKa3Y@&#A8w zpXq@tvnJ z^OsGAy1)Zd>#xY2YQpdU;IW1LR>?KUF9F$!<<<$-dUi|Fjk_`o~-+wLH(h%FM8r~8av|xwZ zmeGvUamP8eSt82{1sUF6&!HobK|Ic*<7(8V9qB(r@Ysn5ym7V&=8-3{+WUfQDfww= z*pR}LTX_7!4A-P!?f=w7M%bRj&7X(@y2OZd?q(%l`3n?qxw& z;Z(kWS436%OHA=;I#;mPK^Yg_BrI4y;`H)BEQ_u*`JDW&1nb|OGon`CvfF-W!;7Kc`fnBmC{o^`SP^US%hv-+w z4-Vh?;5M&5tj-S`%hLr+#+KylgAYzMf+8L!>aC}ccfJuZI8&KckkQ7Y6Q!3@k)xGF zs7aRk3q7n%>w6*!xwOV9cC@D$x~*Fih~ID(tYh9x*N^i}7z~&vJO23H-B7v_)wGL+e6Z$IoE&krW@ zU|7sm%Ej`hq^3(nte8F~k^SqLaid^O_myqOwz%Jt-YkO>o^fb~ldLSg!bn%trL2|6 zkaUqwLy|OEllct0dw!w+?fzszA}Ns*&oOu7NGE046qe z#t_SkGLaKfV7-bE6X`OgOL0A@K@G!LzkEzjg14*_nbi%7P95O2QwR!b;VtIKp0yrn zn~^Hg%;{THpkDw8=;xTVO)HINE3%?AIPE}ts2{IeRz_64(cUYBbj>d6S3bsVoT zTx}B{LS_zgR}7r-n4fk++igJAx5g0hDP)JED^;5Z^G3RuIfqU$TN6&@s?SV<8`9#B zJ3pmP^Za5FpAR!*eqUVPoW)r{Ts2+I_Kx+=-MUSUYkiNloQUg;mO{FGS(q5sxntmM z<(C56rza325fYidWF1f0XlD`66#pJ@S6|9z+u$q=dDiIR(g3+S_)6l6l};ETx>23z z@&VL___)~bWLOr=UKtM%LJRc5fV`EcXteG5#O+~I(dH*;TjC-Q&RHSkYT1XMl4|oU zRN4`TQZjT{^i5_8k7)W8s6rr=nN0tsmw}7I2Z(?qiC6%M`zX{9)C5I}plf#Z4fG>(Y(b#NBm6K;Mugvxu()&KAu9O2mJZ^R zxgrQpTw7oG9K0bhl|6BELdnE>(=+%>{r9(j?WGWiVGpESCqy#ge9jQUoTRwj0YdWx z67--}eZ+Wt`gwoOdPmg5TI_;b=9$ZWE5?rzgq5@hG@kny6VUCw7J4KUqsX6xlX1lt zbZHRSHbSCKPJ`8Zqz^`r3TF~G+augBjOroU>H1d%`W9bv7QF&P6lB{Kj5%KIwD*QJ z^q`6uti%QWrI3ms?ErSpuq+kX&5oJY>l~z^%cZALwkbTIN_M2c+N?7g*Y zi#di%3kn`{J|u^DbK_mLLW*y&{V4P7{$8<63k%gm4FgAK zJ1A_a=__1Qlh+IxBwQiI9g{`qq1YA~AHR{uIQ2X~Iy?>5$0kAnV{=E66ll7%{mq&*vMSo-^w>bTN1u=8DMy$(jCDX(LGDv@!Y zCM2iJEU$cSpj(D1XwEz(0A$JELo6FBDfAHS4kNri2E1q?4g8 z==o4zLIXLAQ?#1pKtZl?NiC!4VA83O{la;T4Xg?7*P!FHE`%F z(P0xKQBR%K8a#cd6|E*FNaRb=VCj_aU8voNF0LbaXy% zu$Jx7u9K7lKHT=J_J%&IiRFjuoYb>J1zA}Zw;G|$jw{0*eg;VOg&y2l8-~LdWq>A5q341#N zE%FI^FIUQ+41Y+U0(ITKrf=tSU}Ja(U253lcOmu6pAL$v9=`~Qa4BpYtwOk|EO?=t zI_`i0fi*GBcsVxe29}X;ce!p15QJg`*tf0@1*~iz$SIb&DP7527?VfdF+2-j#5b}w z{P2Q%r)V*}?d3=2fG!QDSaVz&>L6~>6T;ZuAajqw`y_@P+r+_Xg#cJsrdp;|UyI?= zwfDUuZ-9jYZr6%6Gv|ZlgVBS7JkeOwT&D^D0vga<^DYx{=%v`YX-}stE<^ z1tPpZmoH*N)h4eUR@*s|U(w;lMGwr6iIHE<143T?m#D=)R=>)iE2os{v*Pccb^1## zbyT;;idfJ|`9ewR9MeHfVy8w6AuwXI>SP6o`V){J!e7vAZ#1-szZ&$FL0u1Kwnb2aSVGK7buwVMo&a$RyZ90R4% z|D()~^Md~+EU3PwGKZ>64~ zawa7yqp}s<*NJ5Q+;2jA5qzvPxf+|)ZJ+*4dM8(wcwpO!Lbyz6e*(2?6Lr~(;TUE( zzMGHab($IF`0nC1Kjt%Wva(GhfoVuiL*U@Nrx8Ue|KVn3@wHI{uc2lG>e$u;v z_BOh8Mv+G0Yul3W-Qn1@Rjx-3yc1fg)h$w5?e)b{39|Nb5=EUUa-Tq@J(obbk3GIMCkIuf^j$YPhE)+CDdjIANwDwd8TX$6bq@YC85eE_@ilvED0ZYGRx8wWCW^{7yTg~V<;GSFh24aaW5q+cBh&+NUkxnBh32<3iG4Qg^vOk$Uaii<_@orE_@^!=i@n6aTDU{RVfr z2vB9@iG|a3g~uC+NXZ1%o3b~@?e>LkPhKR`f1ff+;78QwX*+YY-{_eW82Bv~e$G>MTFDxRC-dz(+63Crn|##^%uJz3 zlJlJ@vD8L)EOQ5;B=S55Zu=dT;jt~>>sl|X%vw#Edp$V)v?r;*HX({)+Hm_B2Lcr{&dc-6 z%Ujm7b~mqL$Qp7xh#(3|*2uN`$c{oi#G&xl7QE#2jd4f4lL&rooK`cpzOGTGB%8 zC(af~EFM$K1YLI6IGU_x>tAmW*naBkCz4h#Iq0-tB(i8C*{xL^)7IVGN4luJ-LBt; zSk(sz>DZ%tlD!jf7;%G9!HUU@AgU5e7<Wx_%86s$8Zb_YHkFsCjnO^;Mdg%yTms z<#0TD4qwn3g55Cj-R%2@AD)0phgd~0gHl3sJ&hBzN5@>4?DrCf+#*TZ;C6z?z=+3z zLuegCr9 zpWzY8Z9+6e4C1$+sG0k_B8u7muH&=;yJcNYiid`AedcSX{O{&YA_R2(=T!#~ zm!G~;2|&t6wn)tO3~gt!jL83dpgqWBsl=xw3Z)Cw@Xm`LMn1F5!OpIiVl2V6(gooL zQnHsu--DAcUrL-^!DxQYLx0ZsXxpm_vY!Q|JBg8SdFa`T2ne z8AscVXWLb!gncr2y9W`?O~rVl>`sf@K3_rToN6!`5)cmnz*FaY6E9NT_*I)|II@R^ zKd~1Km`NNBdiNC zxPkb*_Vmm)<`jV^_Q6>wtd>JRX?-4ee~TdzbQ&`}MQOX+3Mxm~J*%K?Wwn}Bx+~fJ zE#KuSgWT4iB;z8+M}G+uDAe2geaanS+OtB$f8GSKd47`y{UgZTLDUqIo-2~Ke@Qd~-Lw*;301&X@`cZ$2qyRZBC6K}qxH<7!X+tWH%Qy`@kfco}IN*PCH3xH3KC@Hy-xLz}yv^tWu z2NSnfo7Xwg9Qs<*nfpl-3ryZ#0oY9Frd1!1o{QxhaqmHxou*aDc~nXh<~22yXQ(xRHRshH@$XjjziPrs+1na>4tSks1i*aK z_qpGG<*m{Asa~sN&7cl4f6oz%Gsc&dW5@|#=z%`a zr6dv`UhOAeH*t9)>v;Y`9mVInFZu`)@S;%8_K8YTd0AfQiyDOT)2=OM z4r4tt6*Sk`Y=4f+hfq*fyd=B?@Vx4Fp++hj$#^)B;Y>^EA-`F!Oq=g+e_Lys8ksP= zBECAO8a!VbauJKz##c2T)kLZ2l@Xm8?Y0{`Ne5+d(+7o9M6XJ4@7-$-6Si z+dS9Q&c>g>!B!4uh^&PKKhi+Wcix3lTVN}JSS?AFI=1e6kJR}^qbb}pRWKh79|FJn zP~vWB{2bt@^W{h--0bxA2mlh2xqEH-f-!`^h$5iy#$}2tYZOw?KR*$|Zy?PB4TXHn zy=*g*wdOON|1>^23;2y}!tE?QcTRO}t!CHo#cV%<@71MO8)z@#wxQKFVTOs{YSqsT z5=^wvf70L$mq=y#c3Xt^W-L`=AL0$iOAoRYf~Nd4mddF}@JIXf-}AUx%YM-Wwu92W zvL{!;73@gV++%)>PRm3S{R2`DDEM)-8%MQWy}w^!>r7w$SFzj?oCYaYbRxSWq!rRl zhb}FNxNcKY2XINrV+mQOgZ#vv+4siYP)D?Deq(Bs6d~v4@VKYlB4pm1O@DRJO(*#_ z0A8aEyv7F7t9*)?k6aOG>>aXwTHDD~d1OLTu8)9Q`v7YS07p+P zCwT$scu(d#o~G5pJT=Arn`ko?(=MDEVZxNa`_J?cb`Fb24@3U^dVwYew-LsR%WV~n zYf{tpBKaw*#KZEZ3V=fCuke@HZmVhkEoclJB5N*96?8i_V_2M;ejghT6?fauQiJPwQut(6Nb`yAuB zU3;)|*8sg=-;ZCd$VX!`8u$?q7wChTo?Mlzjpf@Jk)r|2YML*%cj0iAh)PU7ufzaPd{DXYl0(NQt|IdgtD zMEtl_nQc6^(QQ9s=xbHa{cC`lIVvAH_btWrJ^)D}nENQDZxO9k@UJ;&yr-*W%t;Pii0AXgz{4q-2<78EOW9zpUMMDE z!j${YzoNfY8l#VkTom*9?d0@WW!en%)+1x$DJe5=?mR#F72%xDNg#Wt%z|EZ%kBPi^&69 zAG_oHjPAg0gJY?4w3Uw=8AV^m>a+hzh4A<^>U_cqyMw0BZaIjrzPHg~|NYssKt@xaluZFOG^ful21L%#R)i_##*5umFp!0EY}d*J->OW=jv81ZzO6BP)-?0n_A z{pn(_IeeQTg7VInirjMXSe?bBt%!||f4A?#{+8Z&|E5NrpMTXbShiMr>#x>R%G9Oa zC|b#@x2sGUjpyru9#H9w)%kdSb-10u2}aiLoFB(;& z{}>&*J$qY!y>TiSjwA35t18fXMDK{hYmh_HK_53;a5ZrX!n8Se5{Wg4fgZyjnE04a z*$gkeaH+*KRqsyEVx{O(h1mBHfgbSwh2mdmB`^VVRump@>1)7jyIKOd_>@VQ`nQp6 zTf4x#pIc;?K7n!}Xd*`-)~C-x(u{L=Kfj6&!ITq;PTm!+)8YEHm4co^mEhbz{`G*` zf)U>;y{LsJNey$7skSRi*`@(|xx@(AOMyDFqk(RdW5@e8(Q7Pdle05(aqpAaY#HXX zkGvAPg>m{8t-4>`Q-$9SU{snlb1eD3kXG#wdEH7Hq3$;Q)pw2)B^;*H=6VajZj$=B zkLdN~HFFgH;w;pKW(YJ%Bypjfp+@8gqG-{M4lLDM$d(H468EhASyB|=QO=MWBA6ez z)hl#ZYIt1vY86L%rV_!%Wjls-bL=_i*}3pj7Ye!-RyI)=6C|su&kcUpeu74w5&&8K^ev0owt4z;3+yKnlobn25WcwnWfY@y+{;XFf+@YWlex zMzr<29fW<_8(pP5RPAs(ZIy>V19JKuvnuTICJK2n?=rgf-~G2PA^KtBkwmdO8Si~XzHcFN!u?yGqo?vyT1W3hZtIAwze1(r`R<(o~z)< zPN)%2>$jGj*XdV`oi|^%P&P(#dnRYEdcSyyvBQlkG>;>?RA?OfI8X`R8O`)vQ$k0L zk-S>-^Br;MQOt6Lmc)oV*Y3EFt+U@YPN>(Q4_+$~iBTkC5Jd^{*cU{$na~AtO`ZPf zKaI#kNs#rsU9#r-(U5Pv2@G=EJCl)))*MiE>-a8#D7h+kQQ107tLo7Q!#R$V-3MAx z4A%I}N4gBdJNK1jYS?fORFN==*u@j8M;Lq^&L+f;lu^92k!;PrY}Zf|V&UgP3vlo@ zBD)-qid%srU*Mx3m(!ZE*1v-2(IC*rE4@Mjp?z5EHAjc)4 z2(PuS>95@0ry4!x4|_v!!geW@$W1gkGiiP&ztccTor7(D(uzL%bAc(*?p|nXC7$TG z%ci?D=E)q1vXT6hV>#_xuezpgp{Z9;$?Z5UC$>;xZE zmdtNoPhc+q=oZOwmKL}O-PEL$d&HYlb!1vaWCj|8-eqDaiCTR=bt>g zKidRzs4t@3$UPn|dl<$r^rTK1t@Jy3a=F}GrJ5_6FK~P!Wd}<<5+&glNX0Z1LgAyAGBltu}65w*Gw;TTqE*MF$;rk5^ z|0Cp#9a}FWp)a{p{Pi83KtnR+OyCklttgxQjl@@+kkz=Vl;Fz+xctS&*}BP^VW3l{6~o2)V5iE(`w zLuI%uk1R!;M#J(G2@fmR_iP;SF(KA0M?Z##RQJ)Jryk)wfnHF~@8vZzr~U%S-klCpj*{QI>iRP`8rCp@Q?!i^=d`Cquh*u& zIvwLeVB<(F`}2u(LywB#H=nd2Eg@tY3t&f&=+?dtNYk5MLM@0Ee{z>xcVhM#X!qp5 zWc~bH-*NA;YUZPn^Y7QG@JPU6K*oG|L^iCMdG*xb_x>r$prS#ZOqU>7s-+E7JjEMv zN-?idA4_s8Rqz6-N)En@w_8pc&nV^$Iev?6x&uv?g!+R>yZ6a-t>fnli{4AaG!`9# zl%QuAp}XUGWRq9GX=4Kg+s5dcep!0U1I-_D!+PT+*yad%*L0b5z1$jz$V@IheVZg5 zya*HWqEXQ>17NI>@Xkpw9hX;sD4Opce~Ld1*MEsa!PW?B24exQaeQRQ10#s%3j@Jw zbZ321CjJnf8(R5S`kq0`pqx$aSO18+!?`1q=EnAD;Na`J<8a7tw8jP*A(RYRv8}}p zCn_ajFHM7}GYL(EC!x{<)Dkt^2{(N2<+H_W+ywBwEfa!(;c- z^9rYC@F`RBY^PkIVA1p1Kg;cUMTGqna{dse`is_k}WnnyD z?FmPm<`m>=Hd@JB&!POb`W3VX;@n#)p@DRkt47aM{qf_y_??SS{fvWbd#evy! z9+|QH2pIb)*BW;zguLYKG4z2;aU(!GGM^2xDIZj&-^r9ej< zS5dpW))OD{jHqw8dbN;ES~*UI;H{$+*Rya7^)uBJ8%CoF5Bcx`b}B3LMuhNXzHRyz z#p8^-KzE75Aly5omYslU)WzqJy+@u+epv&HR1Bt`l=-Wy`QZ$tUEmb{=*zJ=g38tv zoFDW8PulFDOq?u!5eLWRVzwPFh0xpm^9*M2m+%9_76jn-jH!eT(D2*p_Yt z6P{EpR{|RQC+&d<&Zf`zSC7;by!UF$GN|000+;Q9O;Y4YbpS*Atd!0N)@_b`^1PkSNmKRr7E2h%BCbQaDe}SjY->b|2$3b7<-a3-hJ^rx+0q*AQxX6P)_nQv=3JSqXO*}UV@Ir)4OVxiV_caW-nx|ztn zdsaP=m%QnXN&JFHg83w<^kCa#6}Fx*U_tqsWJNhIeoFJlh7_9F&G%S^UyiK&!9HRiKW<-0PDJQ( z716$)F5tQQ@yP_1@Xdew{-m@3Klqa{)HgC^d5^5k>&xwi}2G!RqqkP)X4lVm;_1AQT2 zGd^a|E)+Ri1F$h%xvSIIL2ob3V^~TtxubEgqezN&5^Moh1MZdxbfyjc85|aIIyK>5 zW-4LX)&XNEXg9m5SU~A?;ak%B(9^J*ENlu=Yh%jqltuSr|EzuQ1k<3@PsuvoquHkO zL+(_z%lu2B@6TN-p+H>hHoqfoJ?-p^4&UTn_AEpE+l?JU5be(Mi~li!*j%6yWSd|6 zL{W_8&quUkO`LsHn-vtRBu(BB3 z%6yLayY>u2j1duoN*sg2fE0yNhJ=J6C6!tpJd6i_QTmHhIt^_}LA>Nxr0w7R`u_FW zEw9ovjglSg#DWS<>yoY0X~3z#zDT{>KYb6V=TkzSTxA*PK_>1wU@M1UXZnmXV~|f$ zXS$D`ZFr5^8oazR>LQf$X140YQ%W8$I>nLg0LO0LJu6EaJGwGcU#HI`?;Qgn=k#H7neaZ9Kt%?~Mpr*8v zgCFPDK$!DE&VZu}YTo%RkHzMP@5`I~m(JgtR^P>)qqHgAA>Pr zh+nh99vhna0##8tRN1)`D^#QFoeH68Nuua6P_jFS=|234?B{M%$#VY?_3E;dh1*&J z!^Et%6sUUpSK}f3!Y3M(-k&@?Oe*$fvmFzm=?QYfjh|ND=T_i*9EkS?u`u%|rZPK_ zs1$X|QU58p@VJbP|9aglRJ>`>J)8rHEOznv4)^eQXvTKagbf4IEFR-aKTU4a&>lLA zoEXu;oYP()-QHK1G(0>Z#RGTEM4%5G>ov}_sca$MRP(K;LV3=94F5PywbMFH(?P({ zT=)!-%|ET5$uUCh-+>8CYxu5wiYZpx6y}DticSKmi{%Ps)SH#bAN~ue*ssFw4#u-l z$@eU@Hm5x@SH-_vagU(431z@)l}{J%erBm7V!zXy8Bi+vyz1)~wCv@k0y&(n4?t{n zduV`i)O*~n2xLC=d)#Co&EGZKGM!NRe-R}S(njchZR2*DRIRb6!eo!6)W>$?(uDp; z#hM#ui%DCw$lK~A@mK&us1qkDfmo(pfIB^0b4ba}+;obHXnFn>^Jr2N0II$r_5rY4 ztJm(Cfc4b?82@P2F$tL4dv?F?v|D5kV3qUk_0jNH@3%IPeL88h?vKHLY+YVnE41b! z;~yu*p`nfadQ9tiupWy;njUD1)$O6hgqLCt@3;W3TW6k4kj6>Lz(|Ds(c-BoebRLB zHdC$T>+sf%+n)Iz$LoPr%kCMj+lJ3!7gL)@1f1o{!m{6Wj$sE$aT7knZ}U8&=h0#Y zfzsyRREq^izlgtC_58hN%->^KuZii>4Xr7w^d{5ZKVvr1MRhIaJa#tI2wwm}XU$t& z&s=X_M-SJH^gpG7MZ9jj_JAc#6tY?4kKvj4{N!Kd-*yYmI$zSt`096k8MG@-fR71N z{D{cSQbPB$2@%HTO<)iq!9un;9%$V*G0dYf#cC7x%$VdY_IE>#4+zTJPZ70GiR)4wPDW+KKE7a7rNo?sWvhn#Rq3&k zuXKiDrX>nEJrAnacG)JoopAMstpgq3FHCSrI#1aaPW+)syOWO`uERUvGmI{_ComSj z=Ov!r^=dCzTL|2tl*xM|Mrm?@M8*%G;TizknqoYC1H8ri6MWPVAYXjA&)N49?(5QBa+jL^Q0~6I1#5wdS2`*3{G4bOFho@Mq zCpB!9&Z6yGSpc!)KJQTt$q0tw7^nEV;e)#}Bi#rvLwRq;g;V!hmgkXCaCf5J?vu|L zV`b@uy-NA_WPA@k|C_r zk@=Cn@5PsupF!bmyzQ7<|4aVNnq)HwkwqDiGpB7r?86A)*-XYI(dw2IOUIx(y(}g| zM!D8;@1_bfBQtzG7jG?rx4|Dix1M|zai`TD#-Hr1@gn|QUDl)~KO4vZ>!Q~kkG=J* z;a}t*_A38xCaf`!`o;)z+-{34KU4Crf4_LXG@IcWe5~(x3o;{mME&Ks#Q~EpoNpTj6Z2 zldD<`!2`8_u`EZk*~v%?N^wQ%ixRVIih^gr-ZfCJx=@=x_MZ8Ed>QrRH0*oS zH(}kuDp#+yUW2)Qdt%H8i(`Y=j(^LNn#sbT*BF|N;WrDhyDn(ceuC6S`!9HEg@e4; zf4w~Fh0Bq|%GHl5NuEN92hH-4F43TMr!4(3=6<@B)Rg_23fOFHsJ*k%zVeL-*Uxrp z{V^QKLbr@x0-AmIt-*JFb}6t6embu(AR*&JL|qv3;la}wwkbg29pl) z2$r!^YHQ2&sm4S9N5m3xiPT>R@st>Tm z(7p`UiHm9BP*^}>WZ=qBLNW@`f;`~(;^c%VJvA_lqkf23o6mk-X`)!rP54L-QHZEy zL_PgP_A6lOn#m!mHIwFy`Gs%f0%X(g*XBiKtHr@7NhpK2VZ-I95TqK!-z~Q&@dbUj6_VT z{H)vBkk=`=sj_sVGVn{#r`W>SiD7PdPvb|P3)p2ZCY00p#!Rk9x%;*~{a3)71~^F8 zyM_^cm@wJqWKc+$LA=D_S$5A+sHUdR@q+E~>utHlXJ1WEe?*jO6nDBj*P+W4c@)@M3_ViFxrEC0oCsTUB(X9W8qB*mEnfx=2>ue2nVR<0rFM1 z=7o@KYo&!u*aSxzy8RP>NME(zo+F*PSu0_EYQg-K`TR9*g5s{2O{de{Mc|c}BHeZScK0rL9G$s2b1S?DAplP#$>_>dQUS^Kw}4GO}A} z5^bhHDKHHb@S9=VxRUY~@>^w5lf&-UtO{r*3s1g1P`A!om_Q4P1zqixBM*^>Pmpfc zIzV=a*41PIo`rEdi^o?(0=gU7AF9?}eD^{dFU!74(y;_TfT0M5dh>a|kNk%K6;{K& zhWIF(cGNn0BUNMjt**;Vr2e)`4IW=w^>~QoD3e|qOi`g9f#!I)${4$T#ux%q-ia_N zKj*!2$LT&)0`6uS=qA6nDqAs|Qn$W8ZijH5&rmu9Vvb@VAPUbbt_5B6fa~DWA*K&I zQA-VOu_yCFMBiKZD$BfS6?5iON%UK?9?k}{-Rwi!svqo z^Y4=t4LUv}1l)FmUlvL5V}^D5+P>Ue_G?x`TR`0-QJD&JpeQ8NWHS-y-|viK#qm9T zY3(NaC6{XJx6rd6wQ-q^@@rX5G<{vJk#yc8*99uW&ZKW?PG_S-=W9@&vMOWykSJ9i z(8CP1b>%YgGx%(yaI8|Hb*tQn8PZJWS@~0%)!VysA_gSVcP9L5#jiKqvFk-0*LgTo zS9Wc$Zg|Q_aX6V{V?(@j>6|*c>h)RlVH@n4q`0{-UZ%z+cJp zq47?7;7JfAWYs9!NFpe6?$^{PIXly{tr{9xpY^Lr2+=+9RgfM-2iy?*$U!#FB z-Z3j5SjEegt^mIm^>HQuyh>uQI6RBKkgE#8UPW>I%tT)!8y%=)dU*jbPb*uY4kzI4 z_F&ffw4?Mw)kTCmK8)$F@U`0~&Ff9X`XZ~Ia|Whg-p@;J_Z+hmr5f(6Px;na1|1R%G>~q)#7APwPn2(16HbBbsyF+aH1Gsld>JTO-+7su?RaC#TrR){n0I%nUO zhVzwqTFZgz%RBLsvSGpP8=8B6vfSD-Fw3dk@ z+D(L2Zft1pFJX&EmpBLlLMDDv)#JIXR3*?rAgY^)MTxMkF`(@Jh6QafFY53PvgJXB zK;+{-gZ$@oTb8l+<}0;68h&gLXeJprn`0x-vyBPO!6^@KUeHfXZGw6aCc$C~6gbqo zu>8@QVJq&dZ1?3+<~kkg&WT=pepPk=rrf@XoNo^_-z|6kCfPaU zPz!C?L`$I3=-&a1W(z(;&x6U&huMS4f=KI-v#uv){QDm7MwNWgPjSnB0WA~%ji}J6 zHutY_6Evr!Q#dih3i2rk6}(oaG^4P^{ry%u|W|(r* zo>+eMKrOM`1tpHuA`=LZ<2@5%HMq_HTjxwkM)ZL^Yca2RDtLnLq==xbzZpK!s{o7u zgS_1^Pu|JeJ}xz~dmt4p|0Q`b?5kWI_V~3}tkzVfIjE1_bFkpjfgwMM~5 z;d@t9)bk$=@ZXA^%Gu{2*RyjzCtPvr;fCPaG<;&hEgVrb&EgJ)CsJ*;EU0<=okQHS z=Vin#xm+5q&ep6Z_Q0alHnxjcnAP*iWWd~~0M{t5_D9Q0{p#eHb>ui(=4>(5!yn_7 zPT5AMr*7Y<&6%2Wk`woiRgbD@A#g@?kWzIc`U|$MN_{#m)>7kqs3p#b3(=sg-?|26 zC5t!V#fqu#W#C{UdAc@>eo?^Pd9Kq+QMAXw;%5{BW(Gh}p+PkvoVT}>C$bygyf(`Q zifEtbQj89E3MGQIH10lMJZQV0R$wcC_jQyu!l}ZBv6p6;fz!M;`-A@IG2}606H>^u z8-|Q~O*?P4k6`(l&F5UR_u7CqjFbzE=Er_wN<58{30Lt0O5t?>O)RnrVAL)d88M8V z^Z5e>?44}quBr5|JMx6`ux%9>B-~%Rzmis1Y@HD3(FF?<)e+YgbgFqFG4zyt*`Xop1M6bkN?IDpu`w>Y8){xV)Wj)$ zHF6luCMU>pb5MqhjYP45{fC-RPO5$7lN|zvxm{@4zeJFCbu1;*qO0yuxpW(ZoZoJq(OV9dGk9XX zi=&JZr0s)j5V5{3yUUGrS_3TrxaT`ch9gy1BJcBAgtO6NqKC^;*alzwJruXY>a6$r z5m=>cgv<-dU9Vl!<;u4Iv}>z&++E%Wwso30i6~BNL_I>4cds+Kgx9iCR&UUnvjsgX zP|R&W^@ybE9+7_{@M-rig!Mkx^}vmc1BlKa2(%mI;iq8f7(BIn*k0O!wxwm;=CDt| zu*kS(esQBP{>HmiFGNfVW?Xu!ykIdQ7>vaK;3N;g{BZSYi$U)?#rC#*Dnmir%a%x? zC&o-9(Y99Ow}oiXg6g|rVxRa7tTp2Wo!{Q?5H;Ru5NQiLmr&V$}!l@e2VFTS*v)Mw8D0aR&)5TcS&3oq=le>icxGwnC}s4h0>0o5aK#ab7CYQzTmRy8>MEOg%N zjj=aI&%G7qZ%3!vzTti&jY-?Uw7c$;PS$xUfWBevy$9#2fW!1aCLXAtO-b7yO&)B| zEN^nzH&))XqBLhDOyA#4ji{8x`Sg(>M6n^?0SRbpXQ(A15xfVJbJOyFxOjlIq*>S32a(yDmLWqt~u$mEp z5&Ci-noDp1UKk6U;VTo1+z_QiC7_wz)X! z$NnrH;29noMh^J1&V4(nA1pbtTxF&r&%`2JqLB0q$`yO}BiqL)n7RSJx-oGKns2Kg zc?tjRtNy-xMdkKFP;UCa?D4V<=O0Z(V)E{7D~!9~r`^67%Hjn9ZJQW-pTXp#R#hdRVxv|OJx zUvoxTYh*9wQ}M@Qha~nP!hv&{(@zs|0M{bv~tyK0htLyh4ff!Dx4L6-9Kz80g5ULM(0061pzInvB_k zm?ur$D#KGV>73!sGIJvp(=P+%^G38>9h57tPdx22d%gB*mA_J%jKAbIYd$7?;h`H> z;}8Cx6IX7duENNu=WW}q#r|8Zrg>9$FXV3b3Q=zEW5K7moX)%tPZQR8G&5*gv?ey+ zL7WaBKf>x`O-_ez>>iStE}9Ky`~q12<*o(^nt7ur5@d9%GE^vGA;(?{(pdh0f8i@o zQ?vVM>)kPs6GkAXi6Z^ZlQ4{5lU&+02^h_8Ek0r^vG~0>9F9uZR_ux5j$EVHC=-E3 zoXk5uhEFsY`6=f=g2CFi&v{`JMDU`_Oj7h2b8?Tt7B$8ed0yoW9k(I;s!VXVB~w%Tk?6&EeG$G1e>{ z+5wGwq*$&O$+eU_a8&aVKUgUyZSc5)@uawFgOq!xm#;G8=e2$Q!KvP)3lo66)W@2d zT4@xklbTByD$J_1{RV7^B^&@bw4*$a1#>{q}f`t2%QV6 zUR(lL9^fAAdwX1-?@k$qE_J5@R%~sQ#yEqA7stxtV4cjIAVR_aRMcki%dNPy_adH@ zZ}#pumaR*5-BT7w>?{1|7>LQ`k6q?kFlxamrkj1WT{VV?h4P%e*=eN`3UaGTi<7xt zSv6e09vBD&33$d3D1?BXcOp4Niam{vSUL~d_$^(1)o8$f(fvCz!IdU@0fU5Z^#*io z#-%SxGu>yQ?cikIn=?%#Cb6t``T0}9UnVIi76*aP=K}ce(U&oElkd8XsR(N={d2v4 zeENusDDq2Uy&QF6XUTit`anBN*6&3GI`?DHU|zGCqwgy5(Jk}&#@qKI#iPaX(0-ux zM}B9~xF3^jyDBHJJiQ+EzX@O7h`y+|(d~ZR8PlfKT6!DYKh|k(EH>{k1(iD5GG>eI zx2w2?%I<*j3@85MsOVd&Mo_YtMq*NGUzM3>1YC8tU@uSwlR@)%4 zXZVm^&6(*bD#kquW6?~agvCT=@S)LbPImm*I16!g8CYLkWrzG3&lS?z{qp?6VFX9_ z$UAUdaKxf-c+u=UT@?La^Td;7z?*}91G4cH%^qt`b1%$l+)-B3w@#9;wA$u5XWw^+ zhtF-)f0B80?gvPmb1yIN&vrWq_I}U7GWaknAPFb()>2)q|0`jma_)a8+YG13G<6y) zlBvHTFM}{lS}k9WytaP7Yg2NBo6)x5^=&k(6hs>g3#jK; zNv}$=3YGFnI0w!`-@_3fg z0*zf(pcX;um4l0kymiaJQvJL8f}1INjr>p)B+E$j?;a1%!`=)V_Y#F@pCIKsO(fgE z(3xz_P*zaQP$nrd4%r4ZPfUc|wi~v3qiz`<${2Nl{`V1<6DP83r5%vrM3iviQKmHw zE$zrIDTmwEF}_~oRg}Z(TP>N0pInXXV03qWEs&^CsYqj@d-HaNpM$1}Ie&?0c=51z z1xF#nk|iO!{ewa1-6-z zcz%AS;p9}O@Y#UT1KTA10mM|zIMMO(KL}0y)pA`#+v5gFdF)c>Mx^r6l9NAX^ExI2 zti#X4KbT6p-n1$-lNOBqlmLok$@*b;dQJ{AKsx*maFx`Ix;*|RuK?kg!yBm3s^3|s z+s1DgJwR&H78i8ldhI3EnpC*V`D=Ly;5pAUAvY=C-nQ3?{B8pSGo=A?g4=Dt(C9Fb zvAHQOPfkwW8RJ{mJlgorJXzeDSijo0|25){%lF!*+^D^&txL5);u4@M`~}9OYMO?= zQo`m$0&Xwm|0qDj0&YbB{8oO=?@WZ4+vfX92S|g$@0f#F(4$OBR`#fwuFdm9#~45Y z0-}`EfIlvY)u^rPOw8-Gt}ikeY&AymGXGW$cr^Y32}ou>rxn$7SXkKNfT!ETo2{^$ zX0<|@!?3G?O8|i>CnXiuhB;Lox6JwRXgHlMze%Z1SmgD7`;hnb_4UxiemISV31T;w z-2M8fV3O=~wKs-CM#lVfdy)d|gbo!nq@|^Y0ErOw-HLnS&Pck7$szge**aUI=wsPv zj)2DDLT#;RCBUN_iN>Wb(kNBtbXwt;Q&gOFR+?x34O#?joXkRA$KQb+z%Y=Jpar}& zX#jd>2%uY)bhYq1{%7*xnX29fz6&n_9;3oSjnQ0^G+D@>J4)Ge?3GWN0917%UMK)y zklI;%TZqN=0?ND970Ue1%#`e=!7HwV~v6o?Cr3cdo&E~$AUe)53zNh^a($bB!aO$9J9X`-ez z|K>dhY9wm=>CAe+ZjsMHW|T11&Wi))>+jCu9V~*c*txOSJr|7Z1}8=r7IaSA&qO_E zErA#QIIU?T_zJYt=j0QA{Ktkn9B~41zZXMW9Fl6}Ql0=tSdZ5_4Z3;lxvWON9KE-S zpo|AJM`b0B;9pp+TaPzKZkxUE5LZ+Hu99Bz36NSG-nt%4QhAb!^P&1v@;NMiaPGWG zdsHr5{;&Q+Lq{jW;=I}h(WiIW!4OKZ0{DTohgs3ASuNX|E7y8q{pCwn+zTMKnYS+gws-71LC8@)XBg-nwy5V)}`{e)5y8epKVtFuMTc_3+95 zw_s!%-?e$Uc6HX{Yc}i}z}%dB^xn_)t{BmCgvG_d*_qQZ(R%XU8-#JVs$+ThFR^1; zk~%9g$D%TaDb{jS-ukYy_A!O5%WGfp53zI$>RqcoPS(41wRv@?R)UkkfXl zv{|>_yuoc(o|xa|XBW-f+v}Ye#QpZX53{l`TUVf7@(%b^9W;Mgy^tj2<}TQ_VNiB; zbv=xA?#dMbwqV+C`n=19nG~ev4vGQ({@2A)e79Wf2kA8oJbb=&0%8;GyX%u`2*f9yH$_>xH=^Yg7X3KkwLZ;OE_$OG3)3fj(OCHk>XM7(Vj>1 z<*d2)hJuNyW5it_ z&>KkHbrl>OK5}0@__+U<7wa?jvQSlD+A2pqWfW{RKNhTwtt{%tZ>9AtpEIP=RXu`6 z6Mp{uFb9j6j~St}(V4L+Vg>?qVK=m2(SduZnI#A~f6T@NQ>QPwN-;>@aRLRX0`ZXd zoB(Fy-&$w8U_*4_sV=Bqtry@s2cLdg%qYM|mtA)4l|i2X?gPy2LU9Pk{CoiNG5oXp z6dw513|1d-B?TZST}{$c_Monco*PM+N{II)@+SP&3;lkv5sF!1I@jcA;Q0qrY>Od` z6F3_}F2s+Pzf(!hHUJ6A%BM|^dZ#!_F=L}dnDrL)c|JHI_rIU{rI30D(2uZqb%FOz zOP07^ttSNCf!iqL2D6s@h(!W-?F|6go=`xzyRBC+g?fBefr6UKa)t$UTn*i#ZU2q# z`h^~^&EyA3_PzvW3fOAUJz;*oEM-8EmoK(qwBhb(6Pi$Le!y*}Ad-zk;ki)`;D$Ry zv2r90zad=~UbzLLYig@;Q@v;jq7(xYru%q^LN4cs-LSfZlf#j@DzZ(nQcEG)79Pld zV7`R$`-_pQwRza$Ut<}*TScpnmt2tdsISs2Pe##|Im045mppO@5O%Y-!&8nQ=8c32 zZ?Di6-}(YS7&G|}nr3qX1U$&;#zmYXvVQ;6&)}eoN5k+4n5ujqm4kv%Px4iIom%7E zO;e_&!FT&+x|=TCa7ydCFm==s@hSj2X7}y&>`il5XrwUnp3_2hI+$F6EaY>Fu>MjV zS5&S9p2~0eC^ivgBp*ObH&nN6jhR*9>VZQl>~Q~1YhdyRypisC2k^%Phn&%2eA#G^ zF5E^^yno&9fTRgHNX5ad-}BMXNCBYgLSDfLKp9y9i!~qvcZ8?^F0O_K!Q(;VPGRI! zgTHq_dfxKbGz+nE`=LHbT1&xi^o;m??L0eV*c_>SF{$5ivcErgJql!ewHzbE$L*57Ix*qJiDm z!{<-&XiwW0tzK5F!ODZIrXudHBGqxndC$Guf zYf)&Kuu4BfUZ)WrczvH|5s4fSo4%B_7DmXvmZv~<^^L6DH{hJ~bnh;+AvfOJWR(umR}-Q6uIAR#3PlG5$>kNx62b*E;1d77cZ+DXD51HbOS`6**I(s zxp`Qb4{+FW5+4{oJxKFdL`ypp5wD}2CtkimvJkZX-c+xLJ7$LbNaR`ctL4EcTCOBU zVZ!Kg0Yq8V@o&Kd#E%4o8GJd1png(&0hBhN==%{>bW64uy<2G|Ce(9Qjbw*V0<%F; z1n$BJZ6cbslyw)4n!X?TPVzB@&GC3T7IZf{I*Q&Yp|QqnF8~s1H@=k}U>tVfxRn|} zuIkeJib&xQKi2{shbGHCta|!+t{R#|f}_Ogb5k_T1~nQB>N=B5EXfB$(61>R*fS<% zWTmpp1DQ`-8%9!XsrEvW1gk!hEHYM%hE5A2b+cz@2JY50p5j*Qy*Pv5NaS2H zlM->_Jh*S)_XJhofrKJbG`n$eINxl}DZliFW0mJgyEjJx?S6wE`o@(G3y1rP)5 zSOl!zQ6l90YRfYJB|!4F6iNWq(;8+pE9Kq9AUo)NAnNw5{7bMlLZC!ss^hVkj9oB0 ztA%psxCWUaUuUe^V1?~0WYrxd=nOLKOm)?@En{ABWIhGIdC-*`X=OjN?Um)l6&rLJQ<8mh~X3dcWco zMZtz@@NSpE_bV>yL1VOQ=bSB71I3Rvk%kT0kJMN>FdTn8$l5+x&lstB78-G(U@pOU z94-X+o@4p=x2|=+X}4dGsMsnW<=J96Pz2I)2>@Dw&EaF`VMJ)|+7?HY=wOmz?yq(a z0UM&pq=krzM6Ud2og0V58NAxMqd)x_3UzPj! z*;SB{WlPQ&Mdr4vOXgbB>y<2wH=G)EiWAz{j6aDkH9_?b29dk9-VCvKPZ~8b1uwlO^Jtspbe?%(nnu=>nKOc*x7G%Rh~i=A zO9wF|0tVcq$h~+!pAKL(Y^Jy5Kw+4`b=qJA~YO?poXQb{{rXjd!1VK0W!@u%U+?b zcrdmK=Wuc{=RpZ=G9O`^R>(AGGb&5Xi%o)gDHmHw1pFfvmRO;z6AWp^{Y42yzSkMX z&E>+`a&B~ zl6_pKx7<()lYd&I_^r+>>&Ea7GaaIecj2}opQhVW+_frsai(d8Oi2EiR19}Cgh@yW ztO(mhT&pRuzQ8F@zy)DbwkpqtYo`KsexRARqFzsF^iv6PDE8XD}I#0*;#(*)9BWWL*rrduTaFXN-SpWQhPs=M83tZqqxuC zmS{^A+;3cm)qy`vc_d0ZVhSdWblyCWOIUQoR6oFVoG_4A__5xv3Y-^(xMaoMzk|=S zurnTBA`8q0WV}T4637<7K;-&kHs@rQsp{h5g7IF!u_GB%7Hv$)ib6oXR@6mA+yuRt z_Y8shRO~S#@{1~O;zd$UnZO;AdL%u{a)ho}4I57TXa6EPfJf#nr&9ZXRO1hINZ?OG z{jojk6UTHaF3nX%ld>7#`8wiWNRj})AT>b6=8=#VY_SX(#?!HV5;liFeD3KJ;30e$ z>V%3@nuUqeQ~%=XK~G!uJZVEn_J@x=1lIug>1*U_TkIJ-LHLSlYz}djc8tVQ-wO=S zAojXPeESu&ZQ_Wz41HFWoRcbiDr0cMt{7PS@Vo8*0lL&6b|vUk;vYCg4BmJyM}== z@^XNW5gd=*+-gXmZgWJTJH$F_=st<;;{jjdNdf&U`gNkS(rP!&&guZiGxggSsh;#}81e}tO-?Jq+I7pz z#^BDt|L*233k0{6)(al=2>zfMJUgl9HLpkz(+)VIKV?xe0A!|q)lr&G!MxsSg*!U! z?DDec;fVhCU~om}e}~5TmB^Q-5^89he?`EG(T}Y#PZHcY^XDrL+>`~pH|wuu5=8zT zBf-iH-FWIYf6wk81F~!2!FWf25CaMZzm%s;AO2@|0|XyRt~OjxG;L8+l&WePCvw_> zmSTPv@CTK5%i63ugT%%yn>X|Et)f(oE-L?(b$uipB<+J~N2Cxl4&Dr*4h~Ay@(dck z3gmZ2q5i?&(|U)Kmi9rGBLQ{Kl9iP}?f(D;cw#hu5F zxoa=w>%Ar8IG)FMMP1T6cG}5*J1wK56rp7%Dh&grQELYeGbM0W_5a`;G&V-AS!%(q ztd^Ktf@r!K-_-pH_c^{#J8&IM`IJ37I~xxIq4M?p=B|HvvwAmGYvF0=4q@_fb(f(F zS4*$u4Qijoxn0pRKg&60{j#CSS#~y6Om(ANs3;Z^y!uSq%eLkFqDScHB`^f9wEW5d zoncS}+3gwt^)^PXp}~Jq^o9N(2<+=J#a%e(26%*}^4PW(X3mM3WoH`?l7$WRcY^YNQqL-ADV-#ez;0!SgupyUFHA`b4FfV!hLPcnckY#KPIZkM%k8OoFA` ztRTnEU~G7&L%@{v^~rexSr;8hCok}v@c)tm_3^TZ28VO3c$YmyPR<9f4xtgP~k zuZRKEl_d$2lPY9{Jc_uLJee|U{RHc-)s{Yhx`&)e1zK1_k5udjrkLbY4n9b^`xt4A znA;2wlmH7FW`KH+bC|jPn&pC{$sB(PIoB!t-R#4vQYf|N z{ZRV?nu~NNM(+{O3;fGB81DPW_UuoukK?yhQoqh3ZuH<0acCrub&& zRUe<%*2zk2HpT8VYO;f3Euc(gIygAEh|>a4hPt$p+5)oBrC=1o$o2oMl7)xMadGtY z3@U4{;^>U(So(WMqgW3KtSOuA?R0edz1kG30f`n9)2RlI9uOEm7UPZ!-`#vSHSqZC4Cz$ZBlZbg5% zerb=$gp_s(_E7K>D^yS4);+4v>`3WN%gi|P8MXeG5`po?4(-k9RNNTzb$9JC`9;Uj zD&q|mSDCVAiHE3(INlSl{58uZwN`$2iJkcd*PS&0|7&H6Z1_nc!~-nvnh=nH|9QYl z!!Ek@3D>YkH`R2%MUuXePZ+K!&-V`>iHV?JZ@V_GU|;Fa;I&ptPkBZrYA&)I=s8sY z>uSElHhueMdhiCshfw;?=IsaPFah*E{?TK*eDro_yzmlZmcX5vvZDRpc4#$d4IVg6 zvZ{pc?WRV}uVLx?sd(_`TbG%$_W#vn#5ISBl>K7y`y+uV9)V?h(#4kmlG zy%b(bu6b4TEG!DJ!YiVUi&@ zc~SCR_&6S;V6TeP4{x-1xHd6BDixu9X z?$d)J4H+W~%0dkpk!+M4TJvLiJ>rP~quJHiZ>DuRkhkPBs|B8rIOb3V9(E! zk7w9E1Q2JQ@AXA0B3Zz9;3k)6)atA4e#nJz_Bqp+C4z`U8>&&WeE!6B`(p>O#0WsQ z>44&JvP>CH0t~eHTpla_K_bH_y8`zwO{Y{q9xfvbUfM)+qxD z2L4!?Bi8?Ho;!w2@p1GAmwbiqKMEBj)smD6l6~L5QWxo!KO6_54K464DolL3zM%gd zqMdV}!b1$@f!7K)M1zw=Kq92Me4R=7JJ4w`fsr0QJb*Moc*j@IHa>tuepfPr=99L^ zdg8Oml(yoIM^qlv__R!`uHD_;KWAoxOH;s7NjA^$lnJT>zs(Y&M_hko-$V_{*OKEI zRkvp#cZ*jz8Oh*qeaFeUQ}i>d!nd{DpxQz+y5lb`tNJ}RDUVU0!67RBCwk5Fo~P=r zf=EO1r@PxjgUwiaGUJKm=A#)-8$%=^9oWlmw^v8+T!2;`<>RlmBN{PMLo9NC!fG&K z@=Gl~Q)j~ZV<^W3pEKe>A?OqQ4mRpQy=`G95MW-w(cbW+Zk7JgGTgwQ|<&N5?MG8HVC@RVJUM;;=uDf5A^WQlZmSo| zmSQC&lWJVGl`+iP?I&ijVpvr2hgO?O4#5XM3f0i)iZr!>`*NOg2WgqdY|MPeBKxwCzvlz?mn1um%XhV75RX!cv^Na-Ajg z1MtK^oej%Z6TdkANeicKtO{6l%CmfxWjutdN^NJ$#f{=H2Gt`s!0NAj^j*yb#gOjc zoH`3!Yh+u8PnaUSjw<2X#fQ7;_6*-au)DUqHu!Uv#7;x4i5hBD?R0^6$yu{2b7>M+ z_0!(gUeCxP!p-NZH{uv`MAD<9j@j!o;Hnf7SzyOrFN0}_C4;eIK|k;k7R z0f^xdN*Ths{Py43;F7oYAL;JjV?oJ#f0O5tRxF;oM9KwNuv%GUfMqR24hvufw5tY; z3zx1$JXtc3iD#i#dyp#NVS_KbQH#ON&87X6v=i{DVd2f*8n{pCZCu?q0%k`mpZCpk zu;3DbaiLB+>Algf2pj`qFpqBz0+^?S)x zNBO=Zt}Tg*@>^Gr=~W}%gGY|zSc=*5kEc60CBKL(rnepR#eJZULn&IU7X(QuZjZjT zE^GeO@H-nL`wORXTb^R_u_B4dfFGRfgbiS!qmL(l>Nv{r>rY_T%uf&)RT_wl;RLQTQyQivL0EK@38 z_i<6vg0v~BT;>A86M)g>42N0pX0Ny9hUa&Kpz@=OowB0J>m>gHEJJ47W(Iujc5^c^ z%xu2Xr8lisD4k8#N&AAi^iaST`+S<-@avrARq`|z8_cYpeymis54hB#Bw_2{=$TJ5 z?k}?A;zRA;N>Y}9D>!@tLqXOq1za->d48Rc1P zcNEj!hP5oB6QJXAtBrLKBIC*qZt1@*K(rxX7f0*mpS3;G!<#GM{>z?(|erzPLm5}ScUcD8c~ z{q#%VOxPu$@AU5t$L&BJ!b)LBN6_bL60fl0 zgm)ipD^|uNuvOR(#RN+49HX#1n9?h_Aa$kiyBvIhks%`>na+ThC6B|ALa*2c_5|dG zG2krPO8zBz=45u2t_wUzVHJe3z`S()mHW71qejHY6>y5uMp6&Mqn}LSX*(fK4h|dU zqQEhH%tepTz)KLISV4LV7Fs4 zA|oqW%!#kWfFM90TDgKXlp7q+m*2HzQLFUy=~KSv>_THfW8*CEJw%a}tRuy&SkIP4 zOi1r3X@75UwT9uNn2?~TLN0y{KMWZa1wlj52EXmMd^5CHH8(2${{Ds15NHHiddd=Z zyqD>ou-8%7N(=Ebonjp>0>ry)(x4|L6r3JO25T1TjeW z#L{2Ch$>B-mLMPl;fh|hRbGP$P1RgOI!On9&qa1QdV|gjQUt9cDX}jn4dXDp5*v#t zNq707>SThd>8Vmfeb*H!zup~5HAeU`G$3DkzfNLWU$^+6pOT$g&ja(<1cukag@#bo zJqV9gQrDY<2scr$70GJ+i_5%>(O@8R(S&(d@v&nIoy}Ir-VJXh0HVd#o8RGfL!42sG<4AaD@pl)@x0G|D+-(%UT-;fJ4Ev5fU(%igSl(*tV{ z3E9MH8x-4UacY_j-zCjH{-oFu*iCaVS4Qt461@%sR)sltAsWhpDv%~7N;gsoQRa(`m|Dt@<8 z>m$J2w|+Og5S*V(seN}Tr&e~qy4m!8h61LIqbZTxLf5UOyoynhGJBc6vM}Z|u1%zX zkA@JaDciG7U6uWTb|Xd~RDWK9&8`rWHN1<;X2sJ+glL(EH#}$dAqh!A&v14n6T^v& ztNKuOi02k$cAt#+!=2FW*a!p!>z3M*mzn9)pO){Tlg=3;dQ~|VW11?2cllhEbiKRQ zrsLnKP{kRSFdfSpxj@;Zj>%G71CFJ((FmP%V<6dZwk&<@rKZvP`egeeZ;12$9594E z1`crRwq2pPDp5vXP){?3Ju5HBezmVnqlryth0`~XlX7AQKrK)tAyNc#$ZE(h@v*pw zWW`T=Yrnr%RKBn+V_o|)Dr)^jku_i*D0(KZT!=@5OXEO(mWeUa;{*A=p8**o-<5%* zv{Gx#rS92m&y=nPh`w^@qM^rS%}~T=ZBjbquS4mdsH}=!Xuk;YfigedeW&OYY(FQ| z#n|!saaVEHI=0!)xZ$YYJqb*;&noF!x?ZxZyiYIy{EdUwfF!YYA4EcKbu&2MxM&T8Rn@^fT3Q694~CT z$1R&xecw5PI)Ms*wIylsc0VQMv>4Rl3?A-h@T8NZ;ljfwtG9J%+8+)Wv|lrNUYkww61hg1cutrO1^kd zT~)@788npZbO&V>x0K_>34=KsP>CmP#TZ0c!I(-a$Dl3 ziYRV2-V=9_s6SIRS|y9*@&byYT~sRbtV0^N4crMbYzIF`a9g0@@WOE7y-l_%m7EVW zdcWDI$$ogfVm%W?%A6TW<$gI*B!p$?7mm2%ka6=T>>!I52%+>zRyqYB^T zDwQu96db*~FT^fGy4$9Z`OSl4@6?{ELf{#QYV|q0<1$uZTL3}PmF31fEpg^*(020y*x8sJH$yTv_ z)A;gAIaSo>buc31h{Q}USTyp$i_=Dwh~ZhUun!2Kr;-I8l;o$}$8jF?2#Xk-gziGABeSDbj|UuCZ2+f zNpyvJHJ-!38$&wWz5;R8-JPWWNQwvjytwRaX884cBzyk@<6N024FqJ7kj8^Hi^4i{HRgv*+B%|`I2*7z+RGtzoLS;p@(bp zw$jhvjH15ZRJ_G9CMB4}5ms|i_|eBD^mWS5WP&5sMp;`>s`o!#@5Dbf5TY#DNOw*K znO;!`@g6Bs$O~Gg>v!60_&r;oeefqPhdP`eOK{n25jARU@5fc(*qzQ&>3?9IKOMSC zRwoi54WQG>6hW=Nt~!oD(#W|E0ba!v)7^Rd)+vs>5t5L?=p;_cM;(v8cLE!1Ghhb1 zea_YI3MvF%+XX7Q1WDDu!GBYro*V3I-OH$(~5$HBSR84OULq8<1d4 zdfCjBrnAJX$}RJBF5$n6a)@wrJR?(0f0D;yu=)g9C{QgDx9bNj8HlfV2mLItd;f#5 zWfS)gC+gF0XV+FW?@v+%TP1jYn@L{A73r##aI97uxBHN@o3EHd_?R_20fd1sN2>Wrwi-2G4X+|@pf^!lU_w}42St8L`(4Hri zo4(WL(gF?}Zy51!+IA%SeXg1zPsTI?3E9#jwP~8(0eP`b$;&Xo$+1dvG2+gIdr1)C z{S+A@1#j8?i-RiS>XpZ0@PY7g8SD}kPh^ch_kfFnI9~|4=5gu zzCDdTLhC&HwSVTJ>cK9nw{2tcKVY${->}BW5lR)wqPgPmru2j@wUDU9GyV2aR76 z&Imd38$@#+eSFuN`O#vgh+GeuPb~VY(K`$ef$XbBLM%p1V5O1jKO7G`q} z`H3*hIPbM$!eu-|I_7uq?4m#lk*NhtfpJ}?4CNQKRqV#IY3{-%KCYSGT$Z{!(~$7 z(2<9~eJTy!Rr?X%*!eV;{nIpqGk>O6^LrXBtF+up1m93=`vt>tI89pgUR+Q#0S>3uMl+cWEG zL*;qu!wmS1rOFjsxTqAvAIJy#L5h$E@DXm1R?fBfG)@_#qOIhooB^b6wn;gjLD@$8 zGu<6>v(SGHbztZ>=NKH$kE2pwfUygEoY+^BCeNMf$j%| zRX0FGeM}Fh2y!KeS(I~DY4s-5v|fU={tyJh10YIR2LR4R36F$HGyvt~^Ar+~>DQS5 z2UtI}@Z-rkle^kfq}y8*jB^s1uBQ6DdwfAS#OmMKD8Kw=SK~8LT~Ky+2{;Ym{^dSl zURgWq06>Jt5EpodbbpH)oIaDGI|L`}j5{4A567ML;>cfI8UgfQUW486D`E!Wwpv}6 z-#~(=WY=f0z_TPKCSRCB*zN69g0Ss&erbFyQbNre5Ou8sO%d$PLw7O1t+DL4L+L`# zP%sXK|GfmZgJo5DlD9ghoq&mzU%rlfJsCaox1&qsvk29fDeudLpj`xgMG}j$B@mY+CwOk!47U)#uv;+!A{X;4JY(;liOTd1_F9(pebxzQhpmIID&idqqm5P5X8JjrbZqfbu3QYj;m6s zeAa}WcNIOc$)=cTMD0skAbxfF^GjE^8MM1LY}8ifaaip_pIV$)S~3bJVEt66NON3m zJFg99i4h>6#zmuV0-<*mBx@?*AgAd1TPL#D#h!2%qDlR~R#x(VstQ=GL3aR|6cNay z8$ZK~PF|Rrezl8ulfJTjExZCSZ6`Qc2D-2w)2FUHSwO9&|NHj*2A>0{R}z4hN4E=r z(si33%}7*KbXyo6q~WUDInN{4fsr2o#2Qcf>*FnII5q|{iUPtqz>io?=Es7%p@KJe z33jMc#QbY40~eR7ND2`>z=TnQRnnqWYM=|PohCR0GX28pQiFOYjeIp~&z+Lmmmo=T z0#VqjZSFAkusVv`B8|Lor4&wakz`$O?rBFdt`EPaannS*L&)k(&t$HJ_(fHFu-db;Y?^N>`B#H`V&_LaLzIUN~j5g z-@fTUMo+B0wJws&0+huh0pPRn4WQmMJ+Dt@zIJ5f);bdje+SCym%nB!wmre&%K##) zL{L;=_FA;*_p|4o(sm1V*`l3*9w{8P0501slPACCG~pb_vr+?fXuA1Gn)>PB&(ph= z>%v+OMKVxO2=~6+tu36g1+Vwvtm(Ts^2-{RpBlQLYmikcDJTaJ-fu(V2FEnqwp)*r zOR{!|qvcHEuEfKuihbq0#&-TwsVpWz045@JDPUadFHS+JNGyQI2YR9&r8lIDfQ}n# zB?i+fbt726oq?hmE{Hx6hU(X7t=LW%>sC6fbYOwez6N5bfjY+@T76z8Q_u3iCJm>b z;n|ZgD9$N7!S;s?0*3QJ)?g?rJnG8G}}wZOo@{7tX~ zbj1_po!beI&}T>@Pv;6@tRP~HZqd>zFEMIuA$?G1HO0cmKjt^u<&k+tREB;EHEgKT zLk09y0=L;f{$>4+$lcmrEVXOA=f1ZfICZF1pg})1^+0@x!lhd3aeJlNpn{H0N((P6 zmPY!cOcd!wo)#J-_z?JS9cUb}J~dkhhb?COwDUtKUdvTGkuc_>U@krcQT6uN?&|95 zv3W%)sAWpUs3m4JFc)j|G_8T4#CwXas2%Ivpiu*#;$ zf8V`CTW+rUC<2jX9UhOXZkjIl8d?0zP})(YA1_dIa&khCjkg2x^O;W0l0JhnA*CP& zbGuDtyY9b3p77A_!##6$y?|9(neP5#Di;Da9kpl*kwS@(IdG!FI|qSBun@0oB4?)* z_0|zlad0Sy*GS=z31k6lBXl%0hb{1m8HXi3#y1jIOe4|j+HZk)e0f;vbn(xqS=mM0 zGuN#`?&c#48uRBqC2oC%PmO@wVf5x2=32%p>G*)H_r!1Wd5d>F*%KGJC|XH1D`1Xo zv)BNOV(vT%26R!;!7ETt&OYQ7lu-srbGnKF&TyB|t6xE-^$rxv z(I2+OT^_qQ0mMZgx%)zPgL;nBD(DH#ovsF*s64?^n??F8>T}Tt)CMrB&f|M2Ds6b- zDRA1YFzp6jwKcGQ`g_U9^$~A)7F9kmik4a)u4#wb%3-Q`Ml2vh3$~+27Df!=g=^rA zkK4@fQfXEdrhNVe^vl)SlJ4&NHSk~WBqCXlcD6KDi81kIuxDR!y6@G2Y|)R!!;N89 zO7DGt;ln;!tH^0hRgt`KA2?mnD*XiivQ5|uV48_wH+&w{iGYOq5oRN`l5khh#GtTg zI$k+Ww*@U@V8`_W4MbFW>#bMO{V9#IGS9*@gzyS{|&cjF3PXWRkCcJz0m zs;z{3D`phr5Jc8G0S^c7ABH@l8R*E@pnKOt|uu{8j_kf^5QjTk}XIdtgy&>>XhH} z%ELKJIO}K+D)hk5njn?~PCl$CiOrx+E&ca+ZU~&3RLKx#1{Hbnj2RvQhO2k?$|UQ6 z1+Ngrx-0&yf2jYzT@E@|?s@cz8B~`EUu@+%tq&0R6j-wx)MYUsjqd&M2w&fc__xyX z@9;R?M-^b@%_i zF<`@26ch8q5J2M^k(hlA4wk&<&94tTA3V(;1U)-Qe@+&}OM&W1c-f=oC#Bf0pH&q@ zaLCu*J-tz@HcJ$?=HaCI*}Y+qF&x>jQ}}h!iHVUXB9^!Y=?wYC^TZ zj-m#AiKc>P1fupt&;vpq#346m1q{mX=>Qr1to9N2%TRdZYz?4e@PvoXEth+m{-xj` zD>P$gXICjTYGnt7xf+9T7fZ{z$~+9?hm0y(Cbj@e+;%K!7FC?{Y_8h{CJZ@B%F^-jY8kPxNLAY}J)bT! z;sB-p)5+nJ1)8!7$?PRYcz04o8eyQ&C^`bN0F4?OU3mosjS_pbOfUz@0L3e^i$oQc zg>yQ6ownq$i!%|I%ZI5v=G-m^cX<%aCTCk}R^HDUqcg6(2*y>inNt6@R_nW9I<;2qsWD{DSXdNrhqZnk|?YXSsW)wDJ}M4 z*9P~Lk%-@Dee^EX&7=4$&n=IR8( z%tH_hDHt1PweXVbm3*tV{v4iq1@;RoD}S)s@&`d!tGUPuKu-=(9B@ZspDGYgP?Q#m zzF@C{pMSkzJ!irjV8dH!=-sHT-U7*j#`=Z&#S&SAfqIjNBkcSDT>Y8mgDe7E=Oz;@ zGQF=uY(|-RX&%jY1De2xDuAt*2vHd8a03zawjxjGBqEbTrQ5C^YzUeD5TNFTdJ9k7 z_lotJ0QIY9Hk~DoM1Z9}LTSh~K8!~|$0dW^Ee?<8eJ+mGH3_pq!-abCv(W|tyu00Csy4G>SxgRQ;;%sud;Cp=i4ddev4nB!H^2dG zMT$~2OFAW}n;j3|`whAY|2GpFptfOMgG-v3;qR9d(O+#3F6aeNgb)p+rT;2NnI-*x zj4;HSJN%Eb^#8A0zT4O9B|i}gq>6rd;~GnT1g`LkZKV4jWh3Jg~~o?;iXR0^2V@6$6n`P{J}Z zjr3CPROe2x;CtqQ0?0N0(IX2sarNQm@q}=v$*N)E;3UPwKu$Oi$@T9}#XfRwWPu+> zRUIZ*I6#0*G7B_x6S~3N1~L&JT3cJ6tTZ`3`kTcML|5IrzX36?YDi7SjrG~c3XZRz zr~8{Wn>`B#zsu?wlUaM>Y;5XrcPs9F_{&&W_EK}b%QEn_viFkbpN0zRdvTUD_IDc> z%|QD9S|{doVM`W4*_WnZ0q$ku>rU93fgADkmmrP67p~tdKlg*()ePuO zvw4s`K}nt&$bSCRHSRSi!hs9RZ&d}tSNok4&0g+B#p!^RpU$oMCk6m50KV?s&M)0C z0L-F#U<#bb<^&bsc=~p90q%NXfuk30zO6l%heZGz`Fp$g^bB&yajB`6BCp!gionj+ z|4}~PDV?~>1Z>EMfJN;GFFyzHOu*bFD55&qThwm>6;8Tf7q=+u17Ry0NNcdJRp~@T z7JYMw!AabAV+h`Hc-I9o0f)BN!)k1@k;K&Q-#tHLLtU0hLDg+F23c>9gx&AtrTts* z>A)aELM<&p&A<1PVACdf3@)iZssRoh2`_2D05HQtG@Ta^eA zH~gFu6B6(*mZ%pLh?W=`DcS$G3A&owKn8U!9d9ZLK} z;ha_N-$~993#F9jMw@~+z5tQ1pbc$_Fa?wSiz*t6F`$9d4ASFhGehcr=>o<8+C}RY~$!NrQ2;iOS$Nbkd{NmcZdE%^N z1m0Gi?Ck=0u*!Kd-b$jlm&j)Q*ZO$TF^e&f0Tx(c z;GYdFZsyfp+OYf2#qKw~b>0|4zvkeN?z$$V`_EW2K}O&w)=y9*j}O)3tvZExd>9cv zQu;u=25|Z6pC>{tTgJC<0G2-&)BEQ3atWTM_oFkWK`vnMYKGZgp~WZFAm;HZePmZB z4s=>nAR?b^8EfY5M{)(bal9p*XGS){a7?qe?J9 z@|@u@z#ys>D4BQB-_ae%Dn$&T(~kE5ENA__b!mm=rMz5RsGtcQuP5+1++`eiXq~g7 z+A^xYcj326sFM>3yKt}~QYEC(7ZRvm17 z8^NdNvJlctXp)^z%~=!KbK7`Xp*5O%w~pXTNmMM6=kC=9D>%X1@85^XVLHF}?`tNl zeu(hz>I=_VLyGIi0H0mmx7LJTpyIzS@SQ&|lqLf5Pn(Q<(VOUBEx| z`@mXpTTnAWxNULP1p;pHzkcOd3l-Xov;}8ewjsD2^0%0;8BOqubY<--sUK3=@-OZ1KZ}(JX+l@4hQ~H51mJcikp^a|4m6^Pv?nqyIJ~4$jkP_`bj$(#Fl_$P_ z-DQj#u{0LD%vQDpPceJ<>320e(*0hmmga8rEk0iNSkWBc*<9_~YJKa&Ux)0;pk7f% x$Yx6f{pwlzZfYS3zp7hRI literal 0 HcmV?d00001 From 2a58bf06c1f972155f4ab261145946e58bcf7340 Mon Sep 17 00:00:00 2001 From: rhadamantys <46837767+rhadamantys@users.noreply.github.com> Date: Mon, 1 Aug 2022 17:42:47 +0200 Subject: [PATCH 3048/3516] Fix invalid enocean unique_id (#74508) Co-authored-by: Martin Hjelmare --- homeassistant/components/enocean/switch.py | 43 +++++++++++-- tests/components/enocean/test_switch.py | 73 ++++++++++++++++++++++ 2 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 tests/components/enocean/test_switch.py diff --git a/homeassistant/components/enocean/switch.py b/homeassistant/components/enocean/switch.py index a53f691df19..5edd2bb6155 100644 --- a/homeassistant/components/enocean/switch.py +++ b/homeassistant/components/enocean/switch.py @@ -5,12 +5,14 @@ from enocean.utils import combine_hex import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import CONF_ID, CONF_NAME +from homeassistant.const import CONF_ID, CONF_NAME, Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import DOMAIN, LOGGER from .device import EnOceanEntity CONF_CHANNEL = "channel" @@ -25,10 +27,40 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( +def generate_unique_id(dev_id: list[int], channel: int) -> str: + """Generate a valid unique id.""" + return f"{combine_hex(dev_id)}-{channel}" + + +def _migrate_to_new_unique_id(hass: HomeAssistant, dev_id, channel) -> None: + """Migrate old unique ids to new unique ids.""" + old_unique_id = f"{combine_hex(dev_id)}" + + ent_reg = entity_registry.async_get(hass) + entity_id = ent_reg.async_get_entity_id(Platform.SWITCH, DOMAIN, old_unique_id) + + if entity_id is not None: + new_unique_id = generate_unique_id(dev_id, channel) + try: + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) + except ValueError: + LOGGER.warning( + "Skip migration of id [%s] to [%s] because it already exists", + old_unique_id, + new_unique_id, + ) + else: + LOGGER.debug( + "Migrating unique_id from [%s] to [%s]", + old_unique_id, + new_unique_id, + ) + + +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the EnOcean switch platform.""" @@ -36,7 +68,8 @@ def setup_platform( dev_id = config.get(CONF_ID) dev_name = config.get(CONF_NAME) - add_entities([EnOceanSwitch(dev_id, dev_name, channel)]) + _migrate_to_new_unique_id(hass, dev_id, channel) + async_add_entities([EnOceanSwitch(dev_id, dev_name, channel)]) class EnOceanSwitch(EnOceanEntity, SwitchEntity): @@ -49,7 +82,7 @@ class EnOceanSwitch(EnOceanEntity, SwitchEntity): self._on_state = False self._on_state2 = False self.channel = channel - self._attr_unique_id = f"{combine_hex(dev_id)}" + self._attr_unique_id = generate_unique_id(dev_id, channel) @property def is_on(self): diff --git a/tests/components/enocean/test_switch.py b/tests/components/enocean/test_switch.py new file mode 100644 index 00000000000..a7aafa6fc73 --- /dev/null +++ b/tests/components/enocean/test_switch.py @@ -0,0 +1,73 @@ +"""Tests for the EnOcean switch platform.""" + +from enocean.utils import combine_hex + +from homeassistant.components.enocean import DOMAIN as ENOCEAN_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, assert_setup_component + +SWITCH_CONFIG = { + "switch": [ + { + "platform": ENOCEAN_DOMAIN, + "id": [0xDE, 0xAD, 0xBE, 0xEF], + "channel": 1, + "name": "room0", + }, + ] +} + + +async def test_unique_id_migration(hass: HomeAssistant) -> None: + """Test EnOcean switch ID migration.""" + + entity_name = SWITCH_CONFIG["switch"][0]["name"] + switch_entity_id = f"{SWITCH_DOMAIN}.{entity_name}" + dev_id = SWITCH_CONFIG["switch"][0]["id"] + channel = SWITCH_CONFIG["switch"][0]["channel"] + + ent_reg = er.async_get(hass) + + old_unique_id = f"{combine_hex(dev_id)}" + + entry = MockConfigEntry(domain=ENOCEAN_DOMAIN, data={"device": "/dev/null"}) + + entry.add_to_hass(hass) + + # Add a switch with an old unique_id to the entity registry + entity_entry = ent_reg.async_get_or_create( + SWITCH_DOMAIN, + ENOCEAN_DOMAIN, + old_unique_id, + suggested_object_id=entity_name, + config_entry=entry, + original_name=entity_name, + ) + + assert entity_entry.entity_id == switch_entity_id + assert entity_entry.unique_id == old_unique_id + + # Now add the sensor to check, whether the old unique_id is migrated + + with assert_setup_component(1, SWITCH_DOMAIN): + assert await async_setup_component( + hass, + SWITCH_DOMAIN, + SWITCH_CONFIG, + ) + + await hass.async_block_till_done() + + # Check that new entry has a new unique_id + entity_entry = ent_reg.async_get(switch_entity_id) + new_unique_id = f"{combine_hex(dev_id)}-{channel}" + + assert entity_entry.unique_id == new_unique_id + assert ( + ent_reg.async_get_entity_id(SWITCH_DOMAIN, ENOCEAN_DOMAIN, old_unique_id) + is None + ) From 990975e9083df99f29a686cb1215af997be7129a Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Mon, 1 Aug 2022 16:56:08 +0200 Subject: [PATCH 3049/3516] =?UTF-8?q?Convert=20fj=C3=A4r=C3=A5skupan=20to?= =?UTF-8?q?=20built=20in=20bluetooth=20(#75380)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add bluetooth discovery * Use home assistant standard api * Fixup manufacture data * Adjust config flow to use standard features * Fixup tests * Mock bluetooth * Simplify device check * Fix missing typing Co-authored-by: Martin Hjelmare --- .../components/fjaraskupan/__init__.py | 87 +++++++++---------- .../components/fjaraskupan/config_flow.py | 29 ++----- .../components/fjaraskupan/manifest.json | 9 +- homeassistant/generated/bluetooth.py | 12 +++ tests/components/fjaraskupan/__init__.py | 10 +++ tests/components/fjaraskupan/conftest.py | 46 +--------- .../fjaraskupan/test_config_flow.py | 48 +++++----- 7 files changed, 109 insertions(+), 132 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 36608fb026d..fbd2f13d2b4 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -5,14 +5,20 @@ from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta import logging -from typing import TYPE_CHECKING -from bleak import BleakScanner -from fjaraskupan import Device, State, device_filter +from fjaraskupan import Device, State +from homeassistant.components.bluetooth import ( + BluetoothCallbackMatcher, + BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfoBleak, + async_address_present, + async_register_callback, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -23,11 +29,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DISPATCH_DETECTION, DOMAIN -if TYPE_CHECKING: - from bleak.backends.device import BLEDevice - from bleak.backends.scanner import AdvertisementData - - PLATFORMS = [ Platform.BINARY_SENSOR, Platform.FAN, @@ -70,16 +71,18 @@ class Coordinator(DataUpdateCoordinator[State]): async def _async_update_data(self) -> State: """Handle an explicit update request.""" if self._refresh_was_scheduled: - raise UpdateFailed("No data received within schedule.") + if async_address_present(self.hass, self.device.address): + return self.device.state + raise UpdateFailed( + "No data received within schedule, and device is no longer present" + ) await self.device.update() return self.device.state - def detection_callback( - self, ble_device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: + def detection_callback(self, service_info: BluetoothServiceInfoBleak) -> None: """Handle a new announcement of data.""" - self.device.detection_callback(ble_device, advertisement_data) + self.device.detection_callback(service_info.device, service_info.advertisement) self.async_set_updated_data(self.device.state) @@ -87,59 +90,52 @@ class Coordinator(DataUpdateCoordinator[State]): class EntryState: """Store state of config entry.""" - scanner: BleakScanner coordinators: dict[str, Coordinator] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Fjäråskupan from a config entry.""" - scanner = BleakScanner(filters={"DuplicateData": True}) - - state = EntryState(scanner, {}) + state = EntryState({}) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = state - async def detection_callback( - ble_device: BLEDevice, advertisement_data: AdvertisementData + def detection_callback( + service_info: BluetoothServiceInfoBleak, change: BluetoothChange ) -> None: - if data := state.coordinators.get(ble_device.address): - _LOGGER.debug( - "Update: %s %s - %s", ble_device.name, ble_device, advertisement_data - ) - - data.detection_callback(ble_device, advertisement_data) + if change != BluetoothChange.ADVERTISEMENT: + return + if data := state.coordinators.get(service_info.address): + _LOGGER.debug("Update: %s", service_info) + data.detection_callback(service_info) else: - if not device_filter(ble_device, advertisement_data): - return + _LOGGER.debug("Detected: %s", service_info) - _LOGGER.debug( - "Detected: %s %s - %s", ble_device.name, ble_device, advertisement_data - ) - - device = Device(ble_device) + device = Device(service_info.device) device_info = DeviceInfo( - identifiers={(DOMAIN, ble_device.address)}, + identifiers={(DOMAIN, service_info.address)}, manufacturer="Fjäråskupan", name="Fjäråskupan", ) coordinator: Coordinator = Coordinator(hass, device, device_info) - coordinator.detection_callback(ble_device, advertisement_data) + coordinator.detection_callback(service_info) - state.coordinators[ble_device.address] = coordinator + state.coordinators[service_info.address] = coordinator async_dispatcher_send( hass, f"{DISPATCH_DETECTION}.{entry.entry_id}", coordinator ) - scanner.register_detection_callback(detection_callback) - await scanner.start() - - async def on_hass_stop(event: Event) -> None: - await scanner.stop() - entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + async_register_callback( + hass, + detection_callback, + BluetoothCallbackMatcher( + manufacturer_id=20296, + manufacturer_data_start=[79, 68, 70, 74, 65, 82], + ), + BluetoothScanningMode.ACTIVE, + ) ) hass.config_entries.async_setup_platforms(entry, PLATFORMS) @@ -177,7 +173,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) if unload_ok: - entry_state: EntryState = hass.data[DOMAIN].pop(entry.entry_id) - await entry_state.scanner.stop() + hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/fjaraskupan/config_flow.py b/homeassistant/components/fjaraskupan/config_flow.py index ffac366500b..dd1dc03d3ad 100644 --- a/homeassistant/components/fjaraskupan/config_flow.py +++ b/homeassistant/components/fjaraskupan/config_flow.py @@ -1,42 +1,25 @@ """Config flow for Fjäråskupan integration.""" from __future__ import annotations -import asyncio - -import async_timeout -from bleak import BleakScanner -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData from fjaraskupan import device_filter +from homeassistant.components.bluetooth import async_discovered_service_info from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_flow import register_discovery_flow from .const import DOMAIN -CONST_WAIT_TIME = 5.0 - async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" - event = asyncio.Event() + service_infos = async_discovered_service_info(hass) - def detection(device: BLEDevice, advertisement_data: AdvertisementData): - if device_filter(device, advertisement_data): - event.set() + for service_info in service_infos: + if device_filter(service_info.device, service_info.advertisement): + return True - async with BleakScanner( - detection_callback=detection, - filters={"DuplicateData": True}, - ): - try: - async with async_timeout.timeout(CONST_WAIT_TIME): - await event.wait() - except asyncio.TimeoutError: - return False - - return True + return False register_discovery_flow(DOMAIN, "Fjäråskupan", _async_has_devices) diff --git a/homeassistant/components/fjaraskupan/manifest.json b/homeassistant/components/fjaraskupan/manifest.json index 3ff6e599a6b..bf7956d297d 100644 --- a/homeassistant/components/fjaraskupan/manifest.json +++ b/homeassistant/components/fjaraskupan/manifest.json @@ -6,5 +6,12 @@ "requirements": ["fjaraskupan==1.0.2"], "codeowners": ["@elupus"], "iot_class": "local_polling", - "loggers": ["bleak", "fjaraskupan"] + "loggers": ["bleak", "fjaraskupan"], + "dependencies": ["bluetooth"], + "bluetooth": [ + { + "manufacturer_id": 20296, + "manufacturer_data_start": [79, 68, 70, 74, 65, 82] + } + ] } diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 2cbaebb6074..ef8193dad28 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -7,6 +7,18 @@ from __future__ import annotations # fmt: off BLUETOOTH: list[dict[str, str | int | list[int]]] = [ + { + "domain": "fjaraskupan", + "manufacturer_id": 20296, + "manufacturer_data_start": [ + 79, + 68, + 70, + 74, + 65, + 82 + ] + }, { "domain": "govee_ble", "local_name": "Govee*" diff --git a/tests/components/fjaraskupan/__init__.py b/tests/components/fjaraskupan/__init__.py index 26a5ecd6605..35c69f98d65 100644 --- a/tests/components/fjaraskupan/__init__.py +++ b/tests/components/fjaraskupan/__init__.py @@ -1 +1,11 @@ """Tests for the Fjäråskupan integration.""" + + +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from homeassistant.components.bluetooth import SOURCE_LOCAL, BluetoothServiceInfoBleak + +COOKER_SERVICE_INFO = BluetoothServiceInfoBleak.from_advertisement( + BLEDevice("1.1.1.1", "COOKERHOOD_FJAR"), AdvertisementData(), source=SOURCE_LOCAL +) diff --git a/tests/components/fjaraskupan/conftest.py b/tests/components/fjaraskupan/conftest.py index 4e06b2ad046..46ff5ae167a 100644 --- a/tests/components/fjaraskupan/conftest.py +++ b/tests/components/fjaraskupan/conftest.py @@ -1,47 +1,9 @@ """Standard fixtures for the Fjäråskupan integration.""" from __future__ import annotations -from unittest.mock import patch - -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData, BaseBleakScanner -from pytest import fixture +import pytest -@fixture(name="scanner", autouse=True) -def fixture_scanner(hass): - """Fixture for scanner.""" - - devices = [BLEDevice("1.1.1.1", "COOKERHOOD_FJAR")] - - class MockScanner(BaseBleakScanner): - """Mock Scanner.""" - - def __init__(self, *args, **kwargs) -> None: - """Initialize the scanner.""" - super().__init__( - detection_callback=kwargs.pop("detection_callback"), service_uuids=[] - ) - - async def start(self): - """Start scanning for devices.""" - for device in devices: - self._callback(device, AdvertisementData()) - - async def stop(self): - """Stop scanning for devices.""" - - @property - def discovered_devices(self) -> list[BLEDevice]: - """Return discovered devices.""" - return devices - - def set_scanning_filter(self, **kwargs): - """Set the scanning filter.""" - - with patch( - "homeassistant.components.fjaraskupan.config_flow.BleakScanner", new=MockScanner - ), patch( - "homeassistant.components.fjaraskupan.config_flow.CONST_WAIT_TIME", new=0.01 - ): - yield devices +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/fjaraskupan/test_config_flow.py b/tests/components/fjaraskupan/test_config_flow.py index a51b8c6f9fa..bef53e18073 100644 --- a/tests/components/fjaraskupan/test_config_flow.py +++ b/tests/components/fjaraskupan/test_config_flow.py @@ -3,7 +3,6 @@ from __future__ import annotations from unittest.mock import patch -from bleak.backends.device import BLEDevice from pytest import fixture from homeassistant import config_entries @@ -11,6 +10,8 @@ from homeassistant.components.fjaraskupan.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from . import COOKER_SERVICE_INFO + @fixture(name="mock_setup_entry", autouse=True) async def fixture_mock_setup_entry(hass): @@ -24,31 +25,38 @@ async def fixture_mock_setup_entry(hass): async def test_configure(hass: HomeAssistant, mock_setup_entry) -> None: """Test we get the form.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + with patch( + "homeassistant.components.fjaraskupan.config_flow.async_discovered_service_info", + return_value=[COOKER_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) - assert result["type"] == FlowResultType.FORM - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Fjäråskupan" - assert result["data"] == {} + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Fjäråskupan" + assert result["data"] == {} - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 1 + await hass.async_block_till_done() + assert len(mock_setup_entry.mock_calls) == 1 -async def test_scan_no_devices(hass: HomeAssistant, scanner: list[BLEDevice]) -> None: +async def test_scan_no_devices(hass: HomeAssistant) -> None: """Test we get the form.""" - scanner.clear() - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + with patch( + "homeassistant.components.fjaraskupan.config_flow.async_discovered_service_info", + return_value=[], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) - assert result["type"] == FlowResultType.FORM - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == FlowResultType.FORM + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "no_devices_found" + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" From c6038380d68f1745759070d1b98aca14a2716eb6 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 1 Aug 2022 18:20:20 -0700 Subject: [PATCH 3050/3516] Add repair issues for nest app auth removal and yaml deprecation (#75974) * Add repair issues for nest app auth removal and yaml deprecation * Apply PR feedback * Re-apply suggestion that i force pushed over * Update criticality level --- homeassistant/components/nest/__init__.py | 33 +++++++++++++++---- homeassistant/components/nest/manifest.json | 2 +- homeassistant/components/nest/strings.json | 10 ++++++ .../components/nest/translations/en.json | 18 +++++----- 4 files changed, 48 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 72759ac0f52..44658558b62 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -29,6 +29,11 @@ from homeassistant.components.application_credentials import ( from homeassistant.components.camera import Image, img_util from homeassistant.components.http.const import KEY_HASS_USER from homeassistant.components.http.view import HomeAssistantView +from homeassistant.components.repairs import ( + IssueSeverity, + async_create_issue, + async_delete_issue, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_BINARY_SENSORS, @@ -187,6 +192,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, unique_id=entry.data[CONF_PROJECT_ID] ) + async_delete_issue(hass, DOMAIN, "removed_app_auth") + subscriber = await api.new_subscriber(hass, entry) if not subscriber: return False @@ -255,6 +262,18 @@ async def async_import_config(hass: HomeAssistant, entry: ConfigEntry) -> None: if entry.data["auth_implementation"] == INSTALLED_AUTH_DOMAIN: # App Auth credentials have been deprecated and must be re-created # by the user in the config flow + async_create_issue( + hass, + DOMAIN, + "removed_app_auth", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="removed_app_auth", + translation_placeholders={ + "more_info_url": "https://www.home-assistant.io/more-info/nest-auth-deprecation", + "documentation_url": "https://www.home-assistant.io/integrations/nest/", + }, + ) raise ConfigEntryAuthFailed( "Google has deprecated App Auth credentials, and the integration " "must be reconfigured in the UI to restore access to Nest Devices." @@ -271,12 +290,14 @@ async def async_import_config(hass: HomeAssistant, entry: ConfigEntry) -> None: WEB_AUTH_DOMAIN, ) - _LOGGER.warning( - "Configuration of Nest integration in YAML is deprecated and " - "will be removed in a future release; Your existing configuration " - "(including OAuth Application Credentials) has been imported into " - "the UI automatically and can be safely removed from your " - "configuration.yaml file" + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.10.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", ) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 72e0aed8420..d826272b207 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -2,7 +2,7 @@ "domain": "nest", "name": "Nest", "config_flow": true, - "dependencies": ["ffmpeg", "http", "application_credentials"], + "dependencies": ["ffmpeg", "http", "application_credentials", "repairs"], "after_dependencies": ["media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.0.0"], diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index 0a13de41511..07ba63ac479 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -88,5 +88,15 @@ "camera_sound": "Sound detected", "doorbell_chime": "Doorbell pressed" } + }, + "issues": { + "deprecated_yaml": { + "title": "The Nest YAML configuration is being removed", + "description": "Configuring Nest in configuration.yaml is being removed in Home Assistant 2022.10.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + }, + "removed_app_auth": { + "title": "Nest Authentication Credentials must be updated", + "description": "To improve security and reduce phishing risk Google has deprecated the authentication method used by Home Assistant.\n\n**This requires action by you to resolve** ([more info]({more_info_url}))\n\n1. Visit the integrations page\n1. Click Reconfigure on the Nest integration.\n1. Home Assistant will walk you through the steps to upgrade to Web Authentication.\n\nSee the Nest [integration instructions]({documentation_url}) for troubleshooting information." + } } } diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index 5f026e55f31..cd8274d635a 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -10,7 +10,6 @@ "missing_configuration": "The component is not configured. Please follow the documentation.", "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", "reauth_successful": "Re-authentication was successful", - "single_instance_allowed": "Already configured. Only a single configuration possible.", "unknown_authorize_url_generation": "Unknown error generating an authorize URL." }, "create_entry": { @@ -26,13 +25,6 @@ "wrong_project_id": "Please enter a valid Cloud Project ID (was same as Device Access Project ID)" }, "step": { - "auth": { - "data": { - "code": "Access Token" - }, - "description": "To link your Google account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided Auth Token code below.", - "title": "Link Google Account" - }, "auth_upgrade": { "description": "App Auth has been deprecated by Google to improve security, and you need to take action by creating new application credentials.\n\nOpen the [documentation]({more_info_url}) to follow along as the next steps will guide you through the steps you need to take to restore access to your Nest devices.", "title": "Nest: App Auth Deprecation" @@ -96,5 +88,15 @@ "camera_sound": "Sound detected", "doorbell_chime": "Doorbell pressed" } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Nest in configuration.yaml is being removed in Home Assistant 2022.10.\n\nYour existing OAuth Application Credentials and access settings have been imported into the UI automatically. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Nest YAML configuration is being removed" + }, + "removed_app_auth": { + "description": "To improve security and reduce phishing risk Google has deprecated the authentication method used by Home Assistant.\n\n**This requires action by you to resolve** ([more info]({more_info_url}))\n\n1. Visit the integrations page\n1. Click Reconfigure on the Nest integration.\n1. Home Assistant will walk you through the steps to upgrade to Web Authentication.\n\nSee the Nest [integration instructions]({documentation_url}) for troubleshooting information.", + "title": "Nest Authentication Credentials must be updated" + } } } \ No newline at end of file From 75747ce319532c68a4897875a8b2ebace9b8a98b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 1 Aug 2022 14:22:24 +0200 Subject: [PATCH 3051/3516] Support MWh for gas consumption sensors (#76016) --- homeassistant/components/energy/validate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index 02549ddfe96..9d6b3bd53c7 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -40,7 +40,11 @@ GAS_USAGE_DEVICE_CLASSES = ( sensor.SensorDeviceClass.GAS, ) GAS_USAGE_UNITS = { - sensor.SensorDeviceClass.ENERGY: (ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR), + sensor.SensorDeviceClass.ENERGY: ( + ENERGY_WATT_HOUR, + ENERGY_KILO_WATT_HOUR, + ENERGY_MEGA_WATT_HOUR, + ), sensor.SensorDeviceClass.GAS: (VOLUME_CUBIC_METERS, VOLUME_CUBIC_FEET), } GAS_PRICE_UNITS = tuple( From a332eb154c3fc6a25364a4f88a2bbdedc302565c Mon Sep 17 00:00:00 2001 From: Jc2k Date: Mon, 1 Aug 2022 22:04:16 +0100 Subject: [PATCH 3052/3516] Add reauth flow to xiaomi_ble, fixes problem adding LYWSD03MMC (#76028) --- .../components/xiaomi_ble/config_flow.py | 136 +++++--- .../components/xiaomi_ble/manifest.json | 2 +- homeassistant/components/xiaomi_ble/sensor.py | 45 ++- .../components/xiaomi_ble/strings.json | 4 + .../xiaomi_ble/translations/en.json | 6 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/xiaomi_ble/test_config_flow.py | 293 +++++++++++++++++- 8 files changed, 439 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index aa1ffc24895..092c60e9713 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping import dataclasses from typing import Any @@ -12,7 +13,7 @@ from xiaomi_ble.parser import EncryptionScheme from homeassistant.components import onboarding from homeassistant.components.bluetooth import ( BluetoothScanningMode, - BluetoothServiceInfoBleak, + BluetoothServiceInfo, async_discovered_service_info, async_process_advertisements, ) @@ -31,11 +32,11 @@ class Discovery: """A discovered bluetooth device.""" title: str - discovery_info: BluetoothServiceInfoBleak + discovery_info: BluetoothServiceInfo device: DeviceData -def _title(discovery_info: BluetoothServiceInfoBleak, device: DeviceData) -> str: +def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str: return device.title or device.get_device_name() or discovery_info.name @@ -46,19 +47,19 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the config flow.""" - self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovery_info: BluetoothServiceInfo | None = None self._discovered_device: DeviceData | None = None self._discovered_devices: dict[str, Discovery] = {} async def _async_wait_for_full_advertisement( - self, discovery_info: BluetoothServiceInfoBleak, device: DeviceData - ) -> BluetoothServiceInfoBleak: + self, discovery_info: BluetoothServiceInfo, device: DeviceData + ) -> BluetoothServiceInfo: """Sometimes first advertisement we receive is blank or incomplete. Wait until we get a useful one.""" if not device.pending: return discovery_info def _process_more_advertisements( - service_info: BluetoothServiceInfoBleak, + service_info: BluetoothServiceInfo, ) -> bool: device.update(service_info) return not device.pending @@ -72,7 +73,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): ) async def async_step_bluetooth( - self, discovery_info: BluetoothServiceInfoBleak + self, discovery_info: BluetoothServiceInfo ) -> FlowResult: """Handle the bluetooth discovery step.""" await self.async_set_unique_id(discovery_info.address) @@ -81,20 +82,21 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): if not device.supported(discovery_info): return self.async_abort(reason="not_supported") + title = _title(discovery_info, device) + self.context["title_placeholders"] = {"name": title} + + self._discovered_device = device + # Wait until we have received enough information about this device to detect its encryption type try: - discovery_info = await self._async_wait_for_full_advertisement( + self._discovery_info = await self._async_wait_for_full_advertisement( discovery_info, device ) except asyncio.TimeoutError: - # If we don't see a valid packet within the timeout then this device is not supported. - return self.async_abort(reason="not_supported") - - self._discovery_info = discovery_info - self._discovered_device = device - - title = _title(discovery_info, device) - self.context["title_placeholders"] = {"name": title} + # This device might have a really long advertising interval + # So create a config entry for it, and if we discover it has encryption later + # We can do a reauth + return await self.async_step_confirm_slow() if device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: return await self.async_step_get_encryption_key_legacy() @@ -107,6 +109,8 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Enter a legacy bindkey for a v2/v3 MiBeacon device.""" assert self._discovery_info + assert self._discovered_device + errors = {} if user_input is not None: @@ -115,18 +119,15 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): if len(bindkey) != 24: errors["bindkey"] = "expected_24_characters" else: - device = DeviceData(bindkey=bytes.fromhex(bindkey)) + self._discovered_device.bindkey = bytes.fromhex(bindkey) # If we got this far we already know supported will # return true so we don't bother checking that again # We just want to retry the decryption - device.supported(self._discovery_info) + self._discovered_device.supported(self._discovery_info) - if device.bindkey_verified: - return self.async_create_entry( - title=self.context["title_placeholders"]["name"], - data={"bindkey": bindkey}, - ) + if self._discovered_device.bindkey_verified: + return self._async_get_or_create_entry(bindkey) errors["bindkey"] = "decryption_failed" @@ -142,6 +143,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Enter a bindkey for a v4/v5 MiBeacon device.""" assert self._discovery_info + assert self._discovered_device errors = {} @@ -151,18 +153,15 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): if len(bindkey) != 32: errors["bindkey"] = "expected_32_characters" else: - device = DeviceData(bindkey=bytes.fromhex(bindkey)) + self._discovered_device.bindkey = bytes.fromhex(bindkey) # If we got this far we already know supported will # return true so we don't bother checking that again # We just want to retry the decryption - device.supported(self._discovery_info) + self._discovered_device.supported(self._discovery_info) - if device.bindkey_verified: - return self.async_create_entry( - title=self.context["title_placeholders"]["name"], - data={"bindkey": bindkey}, - ) + if self._discovered_device.bindkey_verified: + return self._async_get_or_create_entry(bindkey) errors["bindkey"] = "decryption_failed" @@ -178,10 +177,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Confirm discovery.""" if user_input is not None or not onboarding.async_is_onboarded(self.hass): - return self.async_create_entry( - title=self.context["title_placeholders"]["name"], - data={}, - ) + return self._async_get_or_create_entry() self._set_confirm_only() return self.async_show_form( @@ -189,6 +185,19 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): description_placeholders=self.context["title_placeholders"], ) + async def async_step_confirm_slow( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Ack that device is slow.""" + if user_input is not None or not onboarding.async_is_onboarded(self.hass): + return self._async_get_or_create_entry() + + self._set_confirm_only() + return self.async_show_form( + step_id="confirm_slow", + description_placeholders=self.context["title_placeholders"], + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -198,24 +207,28 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(address, raise_on_progress=False) discovery = self._discovered_devices[address] + self.context["title_placeholders"] = {"name": discovery.title} + # Wait until we have received enough information about this device to detect its encryption type try: self._discovery_info = await self._async_wait_for_full_advertisement( discovery.discovery_info, discovery.device ) except asyncio.TimeoutError: - # If we don't see a valid packet within the timeout then this device is not supported. - return self.async_abort(reason="not_supported") + # This device might have a really long advertising interval + # So create a config entry for it, and if we discover it has encryption later + # We can do a reauth + return await self.async_step_confirm_slow() + + self._discovered_device = discovery.device if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: - self.context["title_placeholders"] = {"name": discovery.title} return await self.async_step_get_encryption_key_legacy() if discovery.device.encryption_scheme == EncryptionScheme.MIBEACON_4_5: - self.context["title_placeholders"] = {"name": discovery.title} return await self.async_step_get_encryption_key_4_5() - return self.async_create_entry(title=discovery.title, data={}) + return self._async_get_or_create_entry() current_addresses = self._async_current_ids() for discovery_info in async_discovered_service_info(self.hass): @@ -241,3 +254,46 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema({vol.Required(CONF_ADDRESS): vol.In(titles)}), ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle a flow initialized by a reauth event.""" + entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert entry is not None + + device: DeviceData = self.context["device"] + self._discovered_device = device + + self._discovery_info = device.last_service_info + + if device.encryption_scheme == EncryptionScheme.MIBEACON_LEGACY: + return await self.async_step_get_encryption_key_legacy() + + if device.encryption_scheme == EncryptionScheme.MIBEACON_4_5: + return await self.async_step_get_encryption_key_4_5() + + # Otherwise there wasn't actually encryption so abort + return self.async_abort(reason="reauth_successful") + + def _async_get_or_create_entry(self, bindkey=None): + data = {} + + if bindkey: + data["bindkey"] = bindkey + + if entry_id := self.context.get("entry_id"): + entry = self.hass.config_entries.async_get_entry(entry_id) + assert entry is not None + + self.hass.config_entries.async_update_entry(entry, data=data) + + # Reload the config entry to notify of updated config + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry( + title=self.context["title_placeholders"]["name"], + data=data, + ) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 0d97dcbedf8..a901439b2c9 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.6.2"], + "requirements": ["xiaomi-ble==0.6.4"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index a96722620a9..b3cd5126967 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -11,8 +11,10 @@ from xiaomi_ble import ( Units, XiaomiBluetoothDeviceData, ) +from xiaomi_ble.parser import EncryptionScheme from homeassistant import config_entries +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, @@ -163,6 +165,45 @@ def sensor_update_to_bluetooth_data_update( ) +def process_service_info( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + data: XiaomiBluetoothDeviceData, + service_info: BluetoothServiceInfoBleak, +) -> PassiveBluetoothDataUpdate: + """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data.""" + update = data.update(service_info) + + # If device isn't pending we know it has seen at least one broadcast with a payload + # If that payload was encrypted and the bindkey was not verified then we need to reauth + if ( + not data.pending + and data.encryption_scheme != EncryptionScheme.NONE + and not data.bindkey_verified + ): + flow_context = { + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "title_placeholders": {"name": entry.title}, + "unique_id": entry.unique_id, + "device": data, + } + + for flow in hass.config_entries.flow.async_progress_by_handler(DOMAIN): + if flow["context"] == flow_context: + break + else: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context=flow_context, + data=entry.data, + ) + ) + + return sensor_update_to_bluetooth_data_update(update) + + async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry, @@ -177,9 +218,7 @@ async def async_setup_entry( kwargs["bindkey"] = bytes.fromhex(bindkey) data = XiaomiBluetoothDeviceData(**kwargs) processor = PassiveBluetoothDataProcessor( - lambda service_info: sensor_update_to_bluetooth_data_update( - data.update(service_info) - ) + lambda service_info: process_service_info(hass, entry, data, service_info) ) entry.async_on_unload( processor.async_add_entities_listener( diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index e12a15a0671..48d5c3a87f7 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -11,6 +11,9 @@ "bluetooth_confirm": { "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" }, + "slow_confirm": { + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if its needed." + }, "get_encryption_key_legacy": { "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey.", "data": { @@ -25,6 +28,7 @@ } }, "abort": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json index b9f4e024f92..836ccc51637 100644 --- a/homeassistant/components/xiaomi_ble/translations/en.json +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -6,7 +6,8 @@ "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", "expected_32_characters": "Expected a 32 character hexadecimal bindkey.", - "no_devices_found": "No devices found on the network" + "no_devices_found": "No devices found on the network", + "reauth_successful": "Re-authentication was successful" }, "flow_title": "{name}", "step": { @@ -25,6 +26,9 @@ }, "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey." }, + "slow_confirm": { + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if its needed." + }, "user": { "data": { "address": "Device" diff --git a/requirements_all.txt b/requirements_all.txt index aeaf75b987e..4d3e28cce46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2470,7 +2470,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.2 +xiaomi-ble==0.6.4 # homeassistant.components.knx xknx==0.22.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03cf9488f37..431483efd32 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1662,7 +1662,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.2 +xiaomi-ble==0.6.4 # homeassistant.components.knx xknx==0.22.1 diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index 86fda21aaa0..0d123f0cd54 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -3,7 +3,10 @@ import asyncio from unittest.mock import patch +from xiaomi_ble import XiaomiBluetoothDeviceData as DeviceData + from homeassistant import config_entries +from homeassistant.components.bluetooth import BluetoothChange from homeassistant.components.xiaomi_ble.const import DOMAIN from homeassistant.data_entry_flow import FlowResultType @@ -52,8 +55,19 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload(hass): context={"source": config_entries.SOURCE_BLUETOOTH}, data=MISSING_PAYLOAD_ENCRYPTED, ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "not_supported" + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "confirm_slow" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "LYWSD02MMC" + assert result2["data"] == {} + assert result2["result"].unique_id == "A4:C1:38:56:53:84" async def test_async_step_bluetooth_valid_device_but_missing_payload_then_full(hass): @@ -318,6 +332,24 @@ async def test_async_step_user_no_devices_found(hass): assert result["reason"] == "no_devices_found" +async def test_async_step_user_no_devices_found_2(hass): + """ + Test setup from service info cache with no devices found. + + This variant tests with a non-Xiaomi device known to us. + """ + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[NOT_SENSOR_PUSH_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + async def test_async_step_user_with_found_devices(hass): """Test setup from service info cache with devices found.""" with patch( @@ -363,8 +395,19 @@ async def test_async_step_user_short_payload(hass): result["flow_id"], user_input={"address": "A4:C1:38:56:53:84"}, ) - assert result2["type"] == FlowResultType.ABORT - assert result2["reason"] == "not_supported" + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "confirm_slow" + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "LYWSD02MMC" + assert result3["data"] == {} + assert result3["result"].unique_id == "A4:C1:38:56:53:84" async def test_async_step_user_short_payload_then_full(hass): @@ -755,3 +798,245 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): # Verify the original one was aborted assert not hass.config_entries.flow.async_progress(DOMAIN) + + +async def test_async_step_reauth_legacy(hass): + """Test reauth with a legacy key.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="F8:24:41:C5:98:8B", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "F8:24:41:C5:98:8B", + b"X0\xb6\x03\xd2\x8b\x98\xc5A$\xf8\xc3I\x14vu~\x00\x00\x00\x99", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_legacy" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_legacy_wrong_key(hass): + """Test reauth with a bad legacy key, and that we can recover.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="F8:24:41:C5:98:8B", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "F8:24:41:C5:98:8B", + b"X0\xb6\x03\xd2\x8b\x98\xc5A$\xf8\xc3I\x14vu~\x00\x00\x00\x99", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_legacy" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b85307515a487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result["step_id"] == "get_encryption_key_legacy" + assert result2["errors"]["bindkey"] == "decryption_failed" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "b853075158487ca39a5b5ea9"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_v4(hass): + """Test reauth with a v4 key.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "54:EF:44:E3:9C:BC", + b"XY\x97\tf\xbc\x9c\xe3D\xefT\x01" b"\x08\x12\x05\x00\x00\x00q^\xbe\x90", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_4_5" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_v4_wrong_key(hass): + """Test reauth for v4 with a bad key, and that we can recover.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + ) + entry.add_to_hass(hass) + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + + # WARNING: This test data is synthetic, rather than captured from a real device + # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 + saved_callback( + make_advertisement( + "54:EF:44:E3:9C:BC", + b"XY\x97\tf\xbc\x9c\xe3D\xefT\x01" b"\x08\x12\x05\x00\x00\x00q^\xbe\x90", + ), + BluetoothChange.ADVERTISEMENT, + ) + + await hass.async_block_till_done() + + results = hass.config_entries.flow.async_progress() + assert len(results) == 1 + result = results[0] + + assert result["step_id"] == "get_encryption_key_4_5" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dada143a58"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "get_encryption_key_4_5" + assert result2["errors"]["bindkey"] == "decryption_failed" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + +async def test_async_step_reauth_abort_early(hass): + """ + Test we can abort the reauth if there is no encryption. + + (This can't currently happen in practice). + """ + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + ) + entry.add_to_hass(hass) + + device = DeviceData() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "title_placeholders": {"name": entry.title}, + "unique_id": entry.unique_id, + "device": device, + }, + data=entry.data, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" From b962a6e7677aa36c443db1004604355f896a7803 Mon Sep 17 00:00:00 2001 From: krazos Date: Mon, 1 Aug 2022 12:45:18 -0400 Subject: [PATCH 3053/3516] Fix capitalization of Sonos "Status light" entity name (#76035) Tweak capitalization of "Status light" entity name Tweak capitalization of "Status light" entity name for consistency with blog post guidance, which states that entity names should start with a capital letter, with the rest of the words lower case --- homeassistant/components/sonos/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sonos/switch.py b/homeassistant/components/sonos/switch.py index 8dcf47fd0a2..a348b40cb0f 100644 --- a/homeassistant/components/sonos/switch.py +++ b/homeassistant/components/sonos/switch.py @@ -72,7 +72,7 @@ FRIENDLY_NAMES = { ATTR_MUSIC_PLAYBACK_FULL_VOLUME: "Surround music full volume", ATTR_NIGHT_SOUND: "Night sound", ATTR_SPEECH_ENHANCEMENT: "Speech enhancement", - ATTR_STATUS_LIGHT: "Status Light", + ATTR_STATUS_LIGHT: "Status light", ATTR_SUB_ENABLED: "Subwoofer enabled", ATTR_SURROUND_ENABLED: "Surround enabled", ATTR_TOUCH_CONTROLS: "Touch controls", From 6b588d41ffc9a6ecab655cde672d7a91c8ce7a20 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 1 Aug 2022 11:43:07 -0400 Subject: [PATCH 3054/3516] Enhance logging for ZHA device trigger validation (#76036) * Enhance logging for ZHA device trigger validation * use IntegrationError --- homeassistant/components/zha/core/gateway.py | 4 +++- homeassistant/components/zha/core/helpers.py | 18 ++++++++++++++++-- homeassistant/components/zha/device_trigger.py | 4 ++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index f2fd226249b..14fbf2cf701 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -142,6 +142,7 @@ class ZHAGateway: self._log_relay_handler = LogRelayHandler(hass, self) self.config_entry = config_entry self._unsubs: list[Callable[[], None]] = [] + self.initialized: bool = False async def async_initialize(self) -> None: """Initialize controller and connect radio.""" @@ -183,6 +184,7 @@ class ZHAGateway: self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str(self.coordinator_ieee) self.async_load_devices() self.async_load_groups() + self.initialized = True @callback def async_load_devices(self) -> None: @@ -217,7 +219,7 @@ class ZHAGateway: async def async_initialize_devices_and_entities(self) -> None: """Initialize devices and load entities.""" - _LOGGER.debug("Loading all devices") + _LOGGER.debug("Initializing all devices from Zigpy cache") await asyncio.gather( *(dev.async_initialize(from_cache=True) for dev in self.devices.values()) ) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index b60f61b1e8e..7fd789ac3f5 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -26,6 +26,7 @@ import zigpy.zdo.types as zdo_types from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, State, callback +from homeassistant.exceptions import IntegrationError from homeassistant.helpers import device_registry as dr from .const import ( @@ -42,6 +43,7 @@ if TYPE_CHECKING: from .gateway import ZHAGateway _T = TypeVar("_T") +_LOGGER = logging.getLogger(__name__) @dataclass @@ -170,10 +172,22 @@ def async_get_zha_device(hass: HomeAssistant, device_id: str) -> ZHADevice: device_registry = dr.async_get(hass) registry_device = device_registry.async_get(device_id) if not registry_device: + _LOGGER.error("Device id `%s` not found in registry", device_id) raise KeyError(f"Device id `{device_id}` not found in registry.") zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ieee_address = list(list(registry_device.identifiers)[0])[1] - ieee = zigpy.types.EUI64.convert(ieee_address) + if not zha_gateway.initialized: + _LOGGER.error("Attempting to get a ZHA device when ZHA is not initialized") + raise IntegrationError("ZHA is not initialized yet") + try: + ieee_address = list(list(registry_device.identifiers)[0])[1] + ieee = zigpy.types.EUI64.convert(ieee_address) + except (IndexError, ValueError) as ex: + _LOGGER.error( + "Unable to determine device IEEE for device with device id `%s`", device_id + ) + raise KeyError( + f"Unable to determine device IEEE for device with device id `{device_id}`." + ) from ex return zha_gateway.devices[ieee] diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 44682aaa559..cdd98110f83 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -13,7 +13,7 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, IntegrationError from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -39,7 +39,7 @@ async def async_validate_trigger_config( trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) try: zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) - except (KeyError, AttributeError) as err: + except (KeyError, AttributeError, IntegrationError) as err: raise InvalidDeviceAutomationConfig from err if ( zha_device.device_automation_triggers is None From 4f671bccbcca0175d377b785e7594eded83fb9a3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Aug 2022 08:54:28 +0200 Subject: [PATCH 3055/3516] Support multiple trigger instances for a single webhook (#76037) --- homeassistant/components/webhook/trigger.py | 65 +++++++++++++++------ tests/components/mobile_app/test_webhook.py | 2 +- tests/components/webhook/test_trigger.py | 63 ++++++++++++++++++-- 3 files changed, 107 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/webhook/trigger.py b/homeassistant/components/webhook/trigger.py index 3f790b1ec42..498a7363a61 100644 --- a/homeassistant/components/webhook/trigger.py +++ b/homeassistant/components/webhook/trigger.py @@ -1,5 +1,7 @@ """Offer webhook triggered automation rules.""" -from functools import partial +from __future__ import annotations + +from dataclasses import dataclass from aiohttp import hdrs import voluptuous as vol @@ -13,7 +15,7 @@ from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from . import async_register, async_unregister +from . import DOMAIN, async_register, async_unregister # mypy: allow-untyped-defs @@ -26,20 +28,35 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( } ) +WEBHOOK_TRIGGERS = f"{DOMAIN}_triggers" -async def _handle_webhook(job, trigger_data, hass, webhook_id, request): + +@dataclass +class TriggerInstance: + """Attached trigger settings.""" + + automation_info: AutomationTriggerInfo + job: HassJob + + +async def _handle_webhook(hass, webhook_id, request): """Handle incoming webhook.""" - result = {"platform": "webhook", "webhook_id": webhook_id} + base_result = {"platform": "webhook", "webhook_id": webhook_id} if "json" in request.headers.get(hdrs.CONTENT_TYPE, ""): - result["json"] = await request.json() + base_result["json"] = await request.json() else: - result["data"] = await request.post() + base_result["data"] = await request.post() - result["query"] = request.query - result["description"] = "webhook" - result.update(**trigger_data) - hass.async_run_hass_job(job, {"trigger": result}) + base_result["query"] = request.query + base_result["description"] = "webhook" + + triggers: dict[str, list[TriggerInstance]] = hass.data.setdefault( + WEBHOOK_TRIGGERS, {} + ) + for trigger in triggers[webhook_id]: + result = {**base_result, **trigger.automation_info["trigger_data"]} + hass.async_run_hass_job(trigger.job, {"trigger": result}) async def async_attach_trigger( @@ -49,20 +66,32 @@ async def async_attach_trigger( automation_info: AutomationTriggerInfo, ) -> CALLBACK_TYPE: """Trigger based on incoming webhooks.""" - trigger_data = automation_info["trigger_data"] webhook_id: str = config[CONF_WEBHOOK_ID] job = HassJob(action) - async_register( - hass, - automation_info["domain"], - automation_info["name"], - webhook_id, - partial(_handle_webhook, job, trigger_data), + + triggers: dict[str, list[TriggerInstance]] = hass.data.setdefault( + WEBHOOK_TRIGGERS, {} ) + if webhook_id not in triggers: + async_register( + hass, + automation_info["domain"], + automation_info["name"], + webhook_id, + _handle_webhook, + ) + triggers[webhook_id] = [] + + trigger_instance = TriggerInstance(automation_info, job) + triggers[webhook_id].append(trigger_instance) + @callback def unregister(): """Unregister webhook.""" - async_unregister(hass, webhook_id) + triggers[webhook_id].remove(trigger_instance) + if not triggers[webhook_id]: + async_unregister(hass, webhook_id) + triggers.pop(webhook_id) return unregister diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 0bc237b1c11..b7b95dff392 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -840,7 +840,7 @@ async def test_webhook_handle_scan_tag(hass, create_registrations, webhook_clien @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("tag_scanned", store_event) diff --git a/tests/components/webhook/test_trigger.py b/tests/components/webhook/test_trigger.py index 2deac022b1e..e8d88845f5a 100644 --- a/tests/components/webhook/test_trigger.py +++ b/tests/components/webhook/test_trigger.py @@ -23,7 +23,7 @@ async def test_webhook_json(hass, hass_client_no_auth): @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) @@ -62,7 +62,7 @@ async def test_webhook_post(hass, hass_client_no_auth): @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) @@ -97,7 +97,7 @@ async def test_webhook_query(hass, hass_client_no_auth): @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) @@ -126,13 +126,68 @@ async def test_webhook_query(hass, hass_client_no_auth): assert events[0].data["hello"] == "yo world" +async def test_webhook_multiple(hass, hass_client_no_auth): + """Test triggering multiple triggers with a POST webhook.""" + events1 = [] + events2 = [] + + @callback + def store_event1(event): + """Help store events.""" + events1.append(event) + + @callback + def store_event2(event): + """Help store events.""" + events2.append(event) + + hass.bus.async_listen("test_success1", store_event1) + hass.bus.async_listen("test_success2", store_event2) + + assert await async_setup_component( + hass, + "automation", + { + "automation": [ + { + "trigger": {"platform": "webhook", "webhook_id": "post_webhook"}, + "action": { + "event": "test_success1", + "event_data_template": {"hello": "yo {{ trigger.data.hello }}"}, + }, + }, + { + "trigger": {"platform": "webhook", "webhook_id": "post_webhook"}, + "action": { + "event": "test_success2", + "event_data_template": { + "hello": "yo2 {{ trigger.data.hello }}" + }, + }, + }, + ] + }, + ) + await hass.async_block_till_done() + + client = await hass_client_no_auth() + + await client.post("/api/webhook/post_webhook", data={"hello": "world"}) + await hass.async_block_till_done() + + assert len(events1) == 1 + assert events1[0].data["hello"] == "yo world" + assert len(events2) == 1 + assert events2[0].data["hello"] == "yo2 world" + + async def test_webhook_reload(hass, hass_client_no_auth): """Test reloading a webhook.""" events = [] @callback def store_event(event): - """Helepr to store events.""" + """Help store events.""" events.append(event) hass.bus.async_listen("test_success", store_event) From 7140a9d025496488d5feae17b3dc7f356a5abb4c Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 2 Aug 2022 02:56:50 -0400 Subject: [PATCH 3056/3516] Bump AIOAladdinConnect to 0.1.37 (#76046) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index a142e838f3e..008b8f81c89 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.33"], + "requirements": ["AIOAladdinConnect==0.1.37"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 4d3e28cce46..bd55f88f2df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.33 +AIOAladdinConnect==0.1.37 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 431483efd32..31e5e646ad8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.33 +AIOAladdinConnect==0.1.37 # homeassistant.components.adax Adax-local==0.1.4 From 23488f392b0a7a4071051eed40020d2e9f6ecb12 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Aug 2022 20:46:22 -1000 Subject: [PATCH 3057/3516] Lower bluetooth startup timeout to 9s to avoid warning (#76050) --- homeassistant/components/bluetooth/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 9adaac84333..ba079a426fe 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -46,7 +46,7 @@ _LOGGER = logging.getLogger(__name__) UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 -START_TIMEOUT = 15 +START_TIMEOUT = 9 SOURCE_LOCAL: Final = "local" From 66afd1e696e42853653b96e5ef86b3bfa1651ddf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Aug 2022 20:36:27 -1000 Subject: [PATCH 3058/3516] Bump bluetooth-adapters to 0.1.3 (#76052) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index f215e8fa161..40e63ec7180 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.15.0", "bluetooth-adapters==0.1.2"], + "requirements": ["bleak==0.15.0", "bluetooth-adapters==0.1.3"], "codeowners": ["@bdraco"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c9d424daa33..bea9b749445 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 bleak==0.15.0 -bluetooth-adapters==0.1.2 +bluetooth-adapters==0.1.3 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==36.0.2 diff --git a/requirements_all.txt b/requirements_all.txt index bd55f88f2df..229a8ad90ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -424,7 +424,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.1.2 +bluetooth-adapters==0.1.3 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 31e5e646ad8..b29ccb60cff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ blebox_uniapi==2.0.2 blinkpy==0.19.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.1.2 +bluetooth-adapters==0.1.3 # homeassistant.components.bond bond-async==0.1.22 From 30cd087f6f0e36f1fcfe13780ae28625ac609921 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Aug 2022 20:34:48 -1000 Subject: [PATCH 3059/3516] Fix govee H5074 data (#76057) --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index c7909d3e1af..624a38ebe9d 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -24,7 +24,7 @@ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.12.5"], + "requirements": ["govee-ble==0.12.6"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 229a8ad90ec..2b38d1155e7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.5 +govee-ble==0.12.6 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b29ccb60cff..7ef74a1c6dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,7 +561,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.5 +govee-ble==0.12.6 # homeassistant.components.gree greeclimate==1.2.0 From da00f5ba1e019867d9ba17cba1baf6955a9cbdfe Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Aug 2022 09:38:21 +0200 Subject: [PATCH 3060/3516] Bumped version to 2022.8.0b5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9ef45865665..385db6904e4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 284585734dc..fff0c129cec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0b4" +version = "2022.8.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 033e3b7e85fc1310868060a47ec02e5d710c78ee Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Aug 2022 11:42:57 +0200 Subject: [PATCH 3061/3516] Small title adjustment to the Home Assistant Alerts integration (#76070) --- homeassistant/components/homeassistant_alerts/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant_alerts/manifest.json b/homeassistant/components/homeassistant_alerts/manifest.json index 0d276c6f3ae..7c9ddf4f905 100644 --- a/homeassistant/components/homeassistant_alerts/manifest.json +++ b/homeassistant/components/homeassistant_alerts/manifest.json @@ -1,6 +1,6 @@ { "domain": "homeassistant_alerts", - "name": "Home Assistant alerts", + "name": "Home Assistant Alerts", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/homeassistant_alerts", "codeowners": ["@home-assistant/core"], From 786780bc8ca3d25de59b3f7a2161adc8ab697161 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Aug 2022 12:35:24 +0200 Subject: [PATCH 3062/3516] Use attributes in limitlessled light (#76066) --- .../components/limitlessled/light.py | 160 +++++++----------- 1 file changed, 62 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/limitlessled/light.py b/homeassistant/components/limitlessled/light.py index 5073e3b4a7d..801f104bd3b 100644 --- a/homeassistant/components/limitlessled/light.py +++ b/homeassistant/components/limitlessled/light.py @@ -2,9 +2,11 @@ from __future__ import annotations import logging +from typing import Any from limitlessled import Color from limitlessled.bridge import Bridge +from limitlessled.group import Group from limitlessled.group.dimmer import DimmerGroup from limitlessled.group.rgbw import RgbwGroup from limitlessled.group.rgbww import RgbwwGroup @@ -56,7 +58,7 @@ EFFECT_NIGHT = "night" MIN_SATURATION = 10 -WHITE = [0, 0] +WHITE = (0, 0) COLOR_MODES_LIMITLESS_WHITE = {ColorMode.COLOR_TEMP} SUPPORT_LIMITLESSLED_WHITE = LightEntityFeature.EFFECT | LightEntityFeature.TRANSITION @@ -104,7 +106,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def rewrite_legacy(config): +def rewrite_legacy(config: ConfigType) -> ConfigType: """Rewrite legacy configuration to new format.""" bridges = config.get(CONF_BRIDGES, [config]) new_bridges = [] @@ -151,13 +153,15 @@ def setup_platform( # Use the expanded configuration format. lights = [] + bridge_conf: dict[str, Any] + group_conf: dict[str, Any] for bridge_conf in config[CONF_BRIDGES]: bridge = Bridge( bridge_conf.get(CONF_HOST), port=bridge_conf.get(CONF_PORT, DEFAULT_PORT), version=bridge_conf.get(CONF_VERSION, DEFAULT_VERSION), ) - for group_conf in bridge_conf.get(CONF_GROUPS): + for group_conf in bridge_conf[CONF_GROUPS]: group = bridge.add_group( group_conf.get(CONF_NUMBER), group_conf.get(CONF_NAME), @@ -176,22 +180,22 @@ def state(new_state): def decorator(function): """Set up the decorator function.""" - def wrapper(self, **kwargs): + def wrapper(self: LimitlessLEDGroup, **kwargs: Any) -> None: """Wrap a group state change.""" # pylint: disable=protected-access pipeline = Pipeline() transition_time = DEFAULT_TRANSITION - if self._effect == EFFECT_COLORLOOP: + if self.effect == EFFECT_COLORLOOP: self.group.stop() - self._effect = None + self._attr_effect = None # Set transition time. if ATTR_TRANSITION in kwargs: transition_time = int(kwargs[ATTR_TRANSITION]) # Do group type-specific work. function(self, transition_time, pipeline, **kwargs) # Update state. - self._is_on = new_state + self._attr_is_on = new_state self.group.enqueue(pipeline) self.schedule_update_ha_state() @@ -203,29 +207,34 @@ def state(new_state): class LimitlessLEDGroup(LightEntity, RestoreEntity): """Representation of a LimitessLED group.""" - def __init__(self, group, config): + _attr_assumed_state = True + _attr_max_mireds = 370 + _attr_min_mireds = 154 + _attr_should_poll = False + + def __init__(self, group: Group, config: dict[str, Any]) -> None: """Initialize a group.""" if isinstance(group, WhiteGroup): self._attr_supported_color_modes = COLOR_MODES_LIMITLESS_WHITE self._attr_supported_features = SUPPORT_LIMITLESSLED_WHITE - self._effect_list = [EFFECT_NIGHT] + self._attr_effect_list = [EFFECT_NIGHT] elif isinstance(group, DimmerGroup): self._attr_supported_color_modes = COLOR_MODES_LIMITLESS_DIMMER self._attr_supported_features = SUPPORT_LIMITLESSLED_DIMMER - self._effect_list = [] + self._attr_effect_list = [] elif isinstance(group, RgbwGroup): self._attr_supported_color_modes = COLOR_MODES_LIMITLESS_RGB self._attr_supported_features = SUPPORT_LIMITLESSLED_RGB - self._effect_list = [EFFECT_COLORLOOP, EFFECT_NIGHT, EFFECT_WHITE] + self._attr_effect_list = [EFFECT_COLORLOOP, EFFECT_NIGHT, EFFECT_WHITE] elif isinstance(group, RgbwwGroup): self._attr_supported_color_modes = COLOR_MODES_LIMITLESS_RGBWW self._attr_supported_features = SUPPORT_LIMITLESSLED_RGBWW - self._effect_list = [EFFECT_COLORLOOP, EFFECT_NIGHT, EFFECT_WHITE] + self._attr_effect_list = [EFFECT_COLORLOOP, EFFECT_NIGHT, EFFECT_WHITE] self._fixed_color_mode = None - if len(self._attr_supported_color_modes) == 1: - self._fixed_color_mode = next(iter(self._attr_supported_color_modes)) + if self.supported_color_modes and len(self.supported_color_modes) == 1: + self._fixed_color_mode = next(iter(self.supported_color_modes)) else: assert self._attr_supported_color_modes == { ColorMode.COLOR_TEMP, @@ -233,59 +242,26 @@ class LimitlessLEDGroup(LightEntity, RestoreEntity): } self.group = group + self._attr_name = group.name self.config = config - self._is_on = False - self._brightness = None - self._temperature = None - self._color = None - self._effect = None + self._attr_is_on = False async def async_added_to_hass(self) -> None: """Handle entity about to be added to hass event.""" await super().async_added_to_hass() if last_state := await self.async_get_last_state(): - self._is_on = last_state.state == STATE_ON - self._brightness = last_state.attributes.get("brightness") - self._temperature = last_state.attributes.get("color_temp") - self._color = last_state.attributes.get("hs_color") + self._attr_is_on = last_state.state == STATE_ON + self._attr_brightness = last_state.attributes.get("brightness") + self._attr_color_temp = last_state.attributes.get("color_temp") + self._attr_hs_color = last_state.attributes.get("hs_color") @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def assumed_state(self): - """Return True because unable to access real state of the entity.""" - return True - - @property - def name(self): - """Return the name of the group.""" - return self.group.name - - @property - def is_on(self): - """Return true if device is on.""" - return self._is_on - - @property - def brightness(self): + def brightness(self) -> int | None: """Return the brightness property.""" - if self._effect == EFFECT_NIGHT: + if self.effect == EFFECT_NIGHT: return 1 - return self._brightness - - @property - def min_mireds(self): - """Return the coldest color_temp that this light supports.""" - return 154 - - @property - def max_mireds(self): - """Return the warmest color_temp that this light supports.""" - return 370 + return self._attr_brightness @property def color_mode(self) -> str | None: @@ -294,33 +270,17 @@ class LimitlessLEDGroup(LightEntity, RestoreEntity): return self._fixed_color_mode # The light supports both hs and white with adjustable color temperature - if self._effect == EFFECT_NIGHT or self._color is None or self._color[1] == 0: + if ( + self.effect == EFFECT_NIGHT + or self.hs_color is None + or self.hs_color[1] == 0 + ): return ColorMode.COLOR_TEMP return ColorMode.HS - @property - def color_temp(self): - """Return the temperature property.""" - return self._temperature - - @property - def hs_color(self): - """Return the color property.""" - return self._color - - @property - def effect(self): - """Return the current effect for this light.""" - return self._effect - - @property - def effect_list(self): - """Return the list of supported effects for this light.""" - return self._effect_list - # pylint: disable=arguments-differ @state(False) - def turn_off(self, transition_time, pipeline, **kwargs): + def turn_off(self, transition_time: int, pipeline: Pipeline, **kwargs: Any) -> None: """Turn off a group.""" if self.config[CONF_FADE]: pipeline.transition(transition_time, brightness=0.0) @@ -328,40 +288,42 @@ class LimitlessLEDGroup(LightEntity, RestoreEntity): # pylint: disable=arguments-differ @state(True) - def turn_on(self, transition_time, pipeline, **kwargs): + def turn_on(self, transition_time: int, pipeline: Pipeline, **kwargs: Any) -> None: """Turn on (or adjust property of) a group.""" # The night effect does not need a turned on light if kwargs.get(ATTR_EFFECT) == EFFECT_NIGHT: - if EFFECT_NIGHT in self._effect_list: + if self.effect_list and EFFECT_NIGHT in self.effect_list: pipeline.night_light() - self._effect = EFFECT_NIGHT + self._attr_effect = EFFECT_NIGHT return pipeline.on() # Set up transition. args = {} - if self.config[CONF_FADE] and not self.is_on and self._brightness: + if self.config[CONF_FADE] and not self.is_on and self.brightness: args["brightness"] = self.limitlessled_brightness() if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] + self._attr_brightness = kwargs[ATTR_BRIGHTNESS] args["brightness"] = self.limitlessled_brightness() if ATTR_HS_COLOR in kwargs: - self._color = kwargs[ATTR_HS_COLOR] + self._attr_hs_color = kwargs[ATTR_HS_COLOR] # White is a special case. - if self._color[1] < MIN_SATURATION: + assert self.hs_color is not None + if self.hs_color[1] < MIN_SATURATION: pipeline.white() - self._color = WHITE + self._attr_hs_color = WHITE else: args["color"] = self.limitlessled_color() if ATTR_COLOR_TEMP in kwargs: + assert self.supported_color_modes if ColorMode.HS in self.supported_color_modes: pipeline.white() - self._color = WHITE - self._temperature = kwargs[ATTR_COLOR_TEMP] + self._attr_hs_color = WHITE + self._attr_color_temp = kwargs[ATTR_COLOR_TEMP] args["temperature"] = self.limitlessled_temperature() if args: @@ -375,28 +337,30 @@ class LimitlessLEDGroup(LightEntity, RestoreEntity): pipeline.flash(duration=duration) # Add effects. - if ATTR_EFFECT in kwargs and self._effect_list: + if ATTR_EFFECT in kwargs and self.effect_list: if kwargs[ATTR_EFFECT] == EFFECT_COLORLOOP: - self._effect = EFFECT_COLORLOOP + self._attr_effect = EFFECT_COLORLOOP pipeline.append(COLORLOOP) if kwargs[ATTR_EFFECT] == EFFECT_WHITE: pipeline.white() - self._color = WHITE + self._attr_hs_color = WHITE - def limitlessled_temperature(self): + def limitlessled_temperature(self) -> float: """Convert Home Assistant color temperature units to percentage.""" max_kelvin = color_temperature_mired_to_kelvin(self.min_mireds) min_kelvin = color_temperature_mired_to_kelvin(self.max_mireds) width = max_kelvin - min_kelvin - kelvin = color_temperature_mired_to_kelvin(self._temperature) + assert self.color_temp is not None + kelvin = color_temperature_mired_to_kelvin(self.color_temp) temperature = (kelvin - min_kelvin) / width return max(0, min(1, temperature)) - def limitlessled_brightness(self): + def limitlessled_brightness(self) -> float: """Convert Home Assistant brightness units to percentage.""" - return self._brightness / 255 + assert self.brightness is not None + return self.brightness / 255 - def limitlessled_color(self): + def limitlessled_color(self) -> Color: """Convert Home Assistant HS list to RGB Color tuple.""" - - return Color(*color_hs_to_RGB(*tuple(self._color))) + assert self.hs_color is not None + return Color(*color_hs_to_RGB(*self.hs_color)) From fe6d6b81e312fe9877e866f83edc2d05d2a9cd96 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 00:38:31 -1000 Subject: [PATCH 3063/3516] Add support for switchbot motion sensors (#76059) --- homeassistant/components/switchbot/__init__.py | 2 ++ homeassistant/components/switchbot/const.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index e32252a7615..7eec785233f 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -22,6 +22,7 @@ from .const import ( ATTR_CONTACT, ATTR_CURTAIN, ATTR_HYGROMETER, + ATTR_MOTION, ATTR_PLUG, CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, @@ -35,6 +36,7 @@ PLATFORMS_BY_TYPE = { ATTR_CURTAIN: [Platform.COVER, Platform.BINARY_SENSOR, Platform.SENSOR], ATTR_HYGROMETER: [Platform.SENSOR], ATTR_CONTACT: [Platform.BINARY_SENSOR, Platform.SENSOR], + ATTR_MOTION: [Platform.BINARY_SENSOR, Platform.SENSOR], } CLASS_BY_DEVICE = { ATTR_CURTAIN: switchbot.SwitchbotCurtain, diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index 9cc2acebbf8..a8ec3433f84 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -8,13 +8,16 @@ ATTR_CURTAIN = "curtain" ATTR_HYGROMETER = "hygrometer" ATTR_CONTACT = "contact" ATTR_PLUG = "plug" +ATTR_MOTION = "motion" DEFAULT_NAME = "Switchbot" + SUPPORTED_MODEL_TYPES = { "WoHand": ATTR_BOT, "WoCurtain": ATTR_CURTAIN, "WoSensorTH": ATTR_HYGROMETER, "WoContact": ATTR_CONTACT, "WoPlug": ATTR_PLUG, + "WoPresence": ATTR_MOTION, } # Config Defaults From 320b264d03c2c391eca87656798fbbf8c78a915e Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 2 Aug 2022 12:40:14 +0200 Subject: [PATCH 3064/3516] Use `SourceType.ROUTER` in Tractive integration (#76071) Use SourceType.ROUTER --- homeassistant/components/tractive/device_tracker.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/tractive/device_tracker.py b/homeassistant/components/tractive/device_tracker.py index 19d30aa9856..f92a8e71df3 100644 --- a/homeassistant/components/tractive/device_tracker.py +++ b/homeassistant/components/tractive/device_tracker.py @@ -57,6 +57,8 @@ class TractiveDeviceTracker(TractiveEntity, TrackerEntity): """Return the source type, eg gps or router, of the device.""" if self._source_type == "PHONE": return SourceType.BLUETOOTH + if self._source_type == "KNOWN_WIFI": + return SourceType.ROUTER return SourceType.GPS @property From 48a34756f0e93d9d8bb2e84a15ebfe016c6c5871 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Aug 2022 13:03:34 +0200 Subject: [PATCH 3065/3516] Remove Somfy from Overkiz title in manifest (#76073) --- homeassistant/components/overkiz/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index a7595065224..6e6e57f12e5 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -1,6 +1,6 @@ { "domain": "overkiz", - "name": "Overkiz (by Somfy)", + "name": "Overkiz", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", "requirements": ["pyoverkiz==1.4.2"], From d69d7a8761b5b335bc8f26e04de319897743c0a0 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 2 Aug 2022 13:11:06 +0100 Subject: [PATCH 3066/3516] Fix typo in new xiaomi_ble string (#76076) --- homeassistant/components/xiaomi_ble/strings.json | 2 +- homeassistant/components/xiaomi_ble/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index 48d5c3a87f7..9d2a0ae40d8 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -12,7 +12,7 @@ "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" }, "slow_confirm": { - "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if its needed." + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." }, "get_encryption_key_legacy": { "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey.", diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json index 836ccc51637..4648b28cc93 100644 --- a/homeassistant/components/xiaomi_ble/translations/en.json +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -27,7 +27,7 @@ "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey." }, "slow_confirm": { - "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if its needed." + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." }, "user": { "data": { From 404d530b5fa45de8e9cd4f9efbf9267dad0cc5c2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Aug 2022 14:13:07 +0200 Subject: [PATCH 3067/3516] Handle missing attributes in meater objects (#76072) --- homeassistant/components/meater/sensor.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index 84ef3a2e2a9..a2753a42307 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -43,14 +43,18 @@ class MeaterSensorEntityDescription( def _elapsed_time_to_timestamp(probe: MeaterProbe) -> datetime | None: """Convert elapsed time to timestamp.""" - if not probe.cook: + if not probe.cook or not hasattr(probe.cook, "time_elapsed"): return None return dt_util.utcnow() - timedelta(seconds=probe.cook.time_elapsed) def _remaining_time_to_timestamp(probe: MeaterProbe) -> datetime | None: """Convert remaining time to timestamp.""" - if not probe.cook or probe.cook.time_remaining < 0: + if ( + not probe.cook + or not hasattr(probe.cook, "time_remaining") + or probe.cook.time_remaining < 0 + ): return None return dt_util.utcnow() + timedelta(seconds=probe.cook.time_remaining) @@ -99,7 +103,9 @@ SENSOR_TYPES = ( native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, available=lambda probe: probe is not None and probe.cook is not None, - value=lambda probe: probe.cook.target_temperature if probe.cook else None, + value=lambda probe: probe.cook.target_temperature + if probe.cook and hasattr(probe.cook, "target_temperature") + else None, ), # Peak temperature MeaterSensorEntityDescription( @@ -109,7 +115,9 @@ SENSOR_TYPES = ( native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, available=lambda probe: probe is not None and probe.cook is not None, - value=lambda probe: probe.cook.peak_temperature if probe.cook else None, + value=lambda probe: probe.cook.peak_temperature + if probe.cook and hasattr(probe.cook, "peak_temperature") + else None, ), # Remaining time in seconds. When unknown/calculating default is used. Default: -1 # Exposed as a TIMESTAMP sensor where the timestamp is current time + remaining time. From cfe6c8939c889f702df2dadeb881ccfe822a1fb5 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 2 Aug 2022 14:49:46 +0200 Subject: [PATCH 3068/3516] Add Open Exchange Rates coordinator (#76017) * Add Open Exchange Rates coordinator * Move debug log * Fix update interval calculation --- .coveragerc | 2 +- CODEOWNERS | 1 + .../components/openexchangerates/const.py | 7 + .../openexchangerates/coordinator.py | 47 ++++++ .../openexchangerates/manifest.json | 3 +- .../components/openexchangerates/sensor.py | 139 +++++++++--------- requirements_all.txt | 3 + 7 files changed, 133 insertions(+), 69 deletions(-) create mode 100644 homeassistant/components/openexchangerates/const.py create mode 100644 homeassistant/components/openexchangerates/coordinator.py diff --git a/.coveragerc b/.coveragerc index d529cdbd9ca..3c587e11cf6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -850,7 +850,7 @@ omit = homeassistant/components/open_meteo/weather.py homeassistant/components/opencv/* homeassistant/components/openevse/sensor.py - homeassistant/components/openexchangerates/sensor.py + homeassistant/components/openexchangerates/* homeassistant/components/opengarage/__init__.py homeassistant/components/opengarage/binary_sensor.py homeassistant/components/opengarage/cover.py diff --git a/CODEOWNERS b/CODEOWNERS index ce31d6e8dd3..16523cafa81 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -766,6 +766,7 @@ build.json @home-assistant/supervisor /tests/components/open_meteo/ @frenck /homeassistant/components/openerz/ @misialq /tests/components/openerz/ @misialq +/homeassistant/components/openexchangerates/ @MartinHjelmare /homeassistant/components/opengarage/ @danielhiversen /tests/components/opengarage/ @danielhiversen /homeassistant/components/openhome/ @bazwilliams diff --git a/homeassistant/components/openexchangerates/const.py b/homeassistant/components/openexchangerates/const.py new file mode 100644 index 00000000000..2c037887489 --- /dev/null +++ b/homeassistant/components/openexchangerates/const.py @@ -0,0 +1,7 @@ +"""Provide common constants for Open Exchange Rates.""" +from datetime import timedelta +import logging + +DOMAIN = "openexchangerates" +LOGGER = logging.getLogger(__package__) +BASE_UPDATE_INTERVAL = timedelta(hours=2) diff --git a/homeassistant/components/openexchangerates/coordinator.py b/homeassistant/components/openexchangerates/coordinator.py new file mode 100644 index 00000000000..0106edcd751 --- /dev/null +++ b/homeassistant/components/openexchangerates/coordinator.py @@ -0,0 +1,47 @@ +"""Provide an OpenExchangeRates data coordinator.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta + +from aiohttp import ClientSession +from aioopenexchangerates import Client, Latest, OpenExchangeRatesClientError +import async_timeout + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER + +TIMEOUT = 10 + + +class OpenexchangeratesCoordinator(DataUpdateCoordinator[Latest]): + """Represent a coordinator for Open Exchange Rates API.""" + + def __init__( + self, + hass: HomeAssistant, + session: ClientSession, + api_key: str, + base: str, + update_interval: timedelta, + ) -> None: + """Initialize the coordinator.""" + super().__init__( + hass, LOGGER, name=f"{DOMAIN} base {base}", update_interval=update_interval + ) + self.base = base + self.client = Client(api_key, session) + self.setup_lock = asyncio.Lock() + + async def _async_update_data(self) -> Latest: + """Update data from Open Exchange Rates.""" + try: + async with async_timeout.timeout(TIMEOUT): + latest = await self.client.get_latest(base=self.base) + except (OpenExchangeRatesClientError) as err: + raise UpdateFailed(err) from err + + LOGGER.debug("Result: %s", latest) + return latest diff --git a/homeassistant/components/openexchangerates/manifest.json b/homeassistant/components/openexchangerates/manifest.json index 43c45b6b665..a795eaf8d5e 100644 --- a/homeassistant/components/openexchangerates/manifest.json +++ b/homeassistant/components/openexchangerates/manifest.json @@ -2,6 +2,7 @@ "domain": "openexchangerates", "name": "Open Exchange Rates", "documentation": "https://www.home-assistant.io/integrations/openexchangerates", - "codeowners": [], + "requirements": ["aioopenexchangerates==0.3.0"], + "codeowners": ["@MartinHjelmare"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/openexchangerates/sensor.py b/homeassistant/components/openexchangerates/sensor.py index 318cae4ae0e..337cd3050ac 100644 --- a/homeassistant/components/openexchangerates/sensor.py +++ b/homeassistant/components/openexchangerates/sensor.py @@ -1,32 +1,28 @@ """Support for openexchangerates.org exchange rates service.""" from __future__ import annotations -from datetime import timedelta -from http import HTTPStatus -import logging -from typing import Any +from dataclasses import dataclass, field -import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_API_KEY, CONF_BASE, CONF_NAME, CONF_QUOTE from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import Throttle +from homeassistant.helpers.update_coordinator import CoordinatorEntity -_LOGGER = logging.getLogger(__name__) -_RESOURCE = "https://openexchangerates.org/api/latest.json" +from .const import BASE_UPDATE_INTERVAL, DOMAIN, LOGGER +from .coordinator import OpenexchangeratesCoordinator ATTRIBUTION = "Data provided by openexchangerates.org" DEFAULT_BASE = "USD" DEFAULT_NAME = "Exchange Rate Sensor" -MIN_TIME_BETWEEN_UPDATES = timedelta(hours=2) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, @@ -37,10 +33,19 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform( +@dataclass +class DomainData: + """Data structure to hold data for this domain.""" + + coordinators: dict[tuple[str, str], OpenexchangeratesCoordinator] = field( + default_factory=dict, init=False + ) + + +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Open Exchange Rates sensor.""" @@ -49,75 +54,75 @@ def setup_platform( base: str = config[CONF_BASE] quote: str = config[CONF_QUOTE] - parameters = {"base": base, "app_id": api_key} + integration_data: DomainData = hass.data.setdefault(DOMAIN, DomainData()) + coordinators = integration_data.coordinators - rest = OpenexchangeratesData(_RESOURCE, parameters, quote) - response = requests.get(_RESOURCE, params=parameters, timeout=10) + if (api_key, base) not in coordinators: + # Create one coordinator per base currency per API key. + update_interval = BASE_UPDATE_INTERVAL * ( + len( + { + coordinator_base + for coordinator_api_key, coordinator_base in coordinators + if coordinator_api_key == api_key + } + ) + + 1 + ) + coordinator = coordinators[api_key, base] = OpenexchangeratesCoordinator( + hass, + async_get_clientsession(hass), + api_key, + base, + update_interval, + ) - if response.status_code != HTTPStatus.OK: - _LOGGER.error("Check your OpenExchangeRates API key") - return + LOGGER.debug( + "Coordinator update interval set to: %s", coordinator.update_interval + ) - rest.update() - add_entities([OpenexchangeratesSensor(rest, name, quote)], True) + # Set new interval on all coordinators for this API key. + for ( + coordinator_api_key, + _, + ), coordinator in coordinators.items(): + if coordinator_api_key == api_key: + coordinator.update_interval = update_interval + + coordinator = coordinators[api_key, base] + async with coordinator.setup_lock: + # We need to make sure that the coordinator data is ready. + if not coordinator.data: + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise PlatformNotReady + + async_add_entities([OpenexchangeratesSensor(coordinator, name, quote)]) -class OpenexchangeratesSensor(SensorEntity): +class OpenexchangeratesSensor( + CoordinatorEntity[OpenexchangeratesCoordinator], SensorEntity +): """Representation of an Open Exchange Rates sensor.""" _attr_attribution = ATTRIBUTION - def __init__(self, rest: OpenexchangeratesData, name: str, quote: str) -> None: + def __init__( + self, coordinator: OpenexchangeratesCoordinator, name: str, quote: str + ) -> None: """Initialize the sensor.""" - self.rest = rest - self._name = name + super().__init__(coordinator) + self._attr_name = name self._quote = quote - self._state: float | None = None + self._attr_native_unit_of_measurement = quote @property - def name(self) -> str: - """Return the name of the sensor.""" - return self._name - - @property - def native_value(self) -> float | None: + def native_value(self) -> float: """Return the state of the sensor.""" - return self._state + return round(self.coordinator.data.rates[self._quote], 4) @property - def extra_state_attributes(self) -> dict[str, Any] | None: + def extra_state_attributes(self) -> dict[str, float]: """Return other attributes of the sensor.""" - attr = self.rest.data - - return attr - - def update(self) -> None: - """Update current conditions.""" - self.rest.update() - if (value := self.rest.data) is None: - self._attr_available = False - return - - self._attr_available = True - self._state = round(value[self._quote], 4) - - -class OpenexchangeratesData: - """Get data from Openexchangerates.org.""" - - def __init__(self, resource: str, parameters: dict[str, str], quote: str) -> None: - """Initialize the data object.""" - self._resource = resource - self._parameters = parameters - self._quote = quote - self.data: dict[str, Any] | None = None - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self) -> None: - """Get the latest data from openexchangerates.org.""" - try: - result = requests.get(self._resource, params=self._parameters, timeout=10) - self.data = result.json()["rates"] - except requests.exceptions.HTTPError: - _LOGGER.error("Check the Openexchangerates API key") - self.data = None + return self.coordinator.data.rates diff --git a/requirements_all.txt b/requirements_all.txt index df7bb65f9da..11136a0702b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -219,6 +219,9 @@ aionotion==3.0.2 # homeassistant.components.oncue aiooncue==0.3.4 +# homeassistant.components.openexchangerates +aioopenexchangerates==0.3.0 + # homeassistant.components.acmeda aiopulse==0.4.3 From fbe22d4fe7242bd67c17003ab9459d896b91485a Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 2 Aug 2022 10:10:20 -0400 Subject: [PATCH 3069/3516] Bump AIOAladdinConnect to 0.1.39 (#76082) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 008b8f81c89..5e55f391aa6 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.37"], + "requirements": ["AIOAladdinConnect==0.1.39"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 11136a0702b..3f8351a97be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.37 +AIOAladdinConnect==0.1.39 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 196cca0c3ba..f6dfbe5e4ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.37 +AIOAladdinConnect==0.1.39 # homeassistant.components.adax Adax-local==0.1.4 From be4f9598f965007b6f09b7278ec5fd6298d85203 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 2 Aug 2022 16:28:41 +0200 Subject: [PATCH 3070/3516] Improve type hints in blinksticklight lights (#75999) --- homeassistant/components/blinksticklight/light.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/blinksticklight/light.py b/homeassistant/components/blinksticklight/light.py index 621bd01f874..8373d2990a1 100644 --- a/homeassistant/components/blinksticklight/light.py +++ b/homeassistant/components/blinksticklight/light.py @@ -1,6 +1,8 @@ """Support for Blinkstick lights.""" from __future__ import annotations +from typing import Any + from blinkstick import blinkstick import voluptuous as vol @@ -57,15 +59,15 @@ class BlinkStickLight(LightEntity): self._stick = stick self._attr_name = name - def update(self): + def update(self) -> None: """Read back the device state.""" rgb_color = self._stick.get_color() hsv = color_util.color_RGB_to_hsv(*rgb_color) self._attr_hs_color = hsv[:2] - self._attr_brightness = hsv[2] - self._attr_is_on = self.brightness > 0 + self._attr_brightness = int(hsv[2]) + self._attr_is_on = self.brightness is not None and self.brightness > 0 - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if ATTR_HS_COLOR in kwargs: self._attr_hs_color = kwargs[ATTR_HS_COLOR] @@ -73,13 +75,15 @@ class BlinkStickLight(LightEntity): self._attr_brightness = kwargs[ATTR_BRIGHTNESS] else: self._attr_brightness = 255 + assert self.brightness is not None self._attr_is_on = self.brightness > 0 + assert self.hs_color rgb_color = color_util.color_hsv_to_RGB( self.hs_color[0], self.hs_color[1], self.brightness / 255 * 100 ) self._stick.set_color(red=rgb_color[0], green=rgb_color[1], blue=rgb_color[2]) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._stick.turn_off() From 67cef0dc942b3ab0d3a653eb4a989e4691ea3fe6 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 2 Aug 2022 11:29:32 -0400 Subject: [PATCH 3071/3516] Ensure ZHA devices load before validating device triggers (#76084) --- homeassistant/components/zha/__init__.py | 5 ++++- homeassistant/components/zha/core/const.py | 1 + homeassistant/components/zha/device_trigger.py | 9 +++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 70b9dfd9b46..0a7d43120f7 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -35,6 +35,7 @@ from .core.const import ( DOMAIN, PLATFORMS, SIGNAL_ADD_ENTITIES, + ZHA_DEVICES_LOADED_EVENT, RadioType, ) from .core.discovery import GROUP_PROBE @@ -75,7 +76,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up ZHA from config.""" - hass.data[DATA_ZHA] = {} + hass.data[DATA_ZHA] = {ZHA_DEVICES_LOADED_EVENT: asyncio.Event()} if DOMAIN in config: conf = config[DOMAIN] @@ -109,6 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() + hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].set() device_registry = dr.async_get(hass) device_registry.async_get_or_create( @@ -141,6 +143,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> """Unload ZHA config entry.""" zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] await zha_gateway.shutdown() + hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].clear() GROUP_PROBE.cleanup() api.async_unload_api(hass) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 4c8c6e03c79..dfa5f608cfe 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -394,6 +394,7 @@ ZHA_GW_MSG_GROUP_REMOVED = "group_removed" ZHA_GW_MSG_LOG_ENTRY = "log_entry" ZHA_GW_MSG_LOG_OUTPUT = "log_output" ZHA_GW_MSG_RAW_INIT = "raw_device_initialized" +ZHA_DEVICES_LOADED_EVENT = "zha_devices_loaded_event" EFFECT_BLINK = 0x00 EFFECT_BREATHE = 0x01 diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index cdd98110f83..4ad8eccea1d 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -16,8 +16,8 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError, IntegrationError from homeassistant.helpers.typing import ConfigType -from . import DOMAIN -from .core.const import ZHA_EVENT +from . import DOMAIN as ZHA_DOMAIN +from .core.const import DATA_ZHA, ZHA_DEVICES_LOADED_EVENT, ZHA_EVENT from .core.helpers import async_get_zha_device CONF_SUBTYPE = "subtype" @@ -35,7 +35,8 @@ async def async_validate_trigger_config( """Validate config.""" config = TRIGGER_SCHEMA(config) - if "zha" in hass.config.components: + if ZHA_DOMAIN in hass.config.components: + await hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].wait() trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) try: zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) @@ -100,7 +101,7 @@ async def async_get_triggers( triggers.append( { CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, + CONF_DOMAIN: ZHA_DOMAIN, CONF_PLATFORM: DEVICE, CONF_TYPE: trigger, CONF_SUBTYPE: subtype, From a1d495a25b1c1c85ec7539b02bb4920aa89e2ec1 Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 2 Aug 2022 11:08:33 -0500 Subject: [PATCH 3072/3516] Bump Frontend to 20220802.0 (#76087) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 45331491aa0..ed9b381ee9d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220728.0"], + "requirements": ["home-assistant-frontend==20220802.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 860d1538727..81182a7d8ba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.1 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220728.0 +home-assistant-frontend==20220802.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 3f8351a97be..70169ffa4f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -842,7 +842,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220728.0 +home-assistant-frontend==20220802.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f6dfbe5e4ae..bdfa620a64a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,7 +616,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220728.0 +home-assistant-frontend==20220802.0 # homeassistant.components.home_connect homeconnect==0.7.1 From f043203b566fbd834c4450b2e3ccb7acf31ce3d5 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 2 Aug 2022 17:20:37 +0100 Subject: [PATCH 3073/3516] Add optional context parameter to async_start_reauth (#76077) --- homeassistant/components/xiaomi_ble/sensor.py | 20 +------------- homeassistant/config_entries.py | 7 ++++- tests/test_config_entries.py | 27 +++++++++++++++++++ 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index b3cd5126967..fef9b334e75 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -181,25 +181,7 @@ def process_service_info( and data.encryption_scheme != EncryptionScheme.NONE and not data.bindkey_verified ): - flow_context = { - "source": config_entries.SOURCE_REAUTH, - "entry_id": entry.entry_id, - "title_placeholders": {"name": entry.title}, - "unique_id": entry.unique_id, - "device": data, - } - - for flow in hass.config_entries.flow.async_progress_by_handler(DOMAIN): - if flow["context"] == flow_context: - break - else: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context=flow_context, - data=entry.data, - ) - ) + entry.async_start_reauth(hass, context={"device": data}) return sensor_update_to_bluetooth_data_update(update) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b0c04323005..638aa0b8110 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -640,7 +640,9 @@ class ConfigEntry: await asyncio.gather(*pending) @callback - def async_start_reauth(self, hass: HomeAssistant) -> None: + def async_start_reauth( + self, hass: HomeAssistant, context: dict[str, Any] | None = None + ) -> None: """Start a reauth flow.""" flow_context = { "source": SOURCE_REAUTH, @@ -649,6 +651,9 @@ class ConfigEntry: "unique_id": self.unique_id, } + if context: + flow_context.update(context) + for flow in hass.config_entries.flow.async_progress_by_handler(self.domain): if flow["context"] == flow_context: return diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 3e7245ed73a..66f51508441 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3273,3 +3273,30 @@ async def test_disallow_entry_reload_with_setup_in_progresss(hass, manager): with pytest.raises(config_entries.OperationNotAllowed): assert await manager.async_reload(entry.entry_id) assert entry.state is config_entries.ConfigEntryState.SETUP_IN_PROGRESS + + +async def test_reauth(hass): + """Test the async_reauth_helper.""" + entry = MockConfigEntry(title="test_title", domain="test") + + mock_setup_entry = AsyncMock(return_value=True) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + + entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["entry_id"] == entry.entry_id + assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH + assert flows[0]["context"]["title_placeholders"] == {"name": "test_title"} + assert flows[0]["context"]["extra_context"] == "some_extra_context" + + # Check we can't start duplicate flows + entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + assert len(flows) == 1 From 17fbee7dd37e7939baf7ff34fb408285ffff5042 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Aug 2022 19:05:09 +0200 Subject: [PATCH 3074/3516] Refresh homeassistant_alerts when hass has started (#76083) --- homeassistant/components/homeassistant_alerts/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index d405b9e257d..60386e3d080 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -14,6 +14,7 @@ from homeassistant.components.repairs.models import IssueSeverity from homeassistant.const import __version__ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.start import async_at_start from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.yaml import parse_yaml @@ -100,7 +101,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: coordinator = AlertUpdateCoordinator(hass) coordinator.async_add_listener(async_schedule_update_alerts) - await coordinator.async_refresh() + + async def initial_refresh(hass: HomeAssistant) -> None: + await coordinator.async_refresh() + + async_at_start(hass, initial_refresh) return True From 9f31be8f01e0382df2ab7362b2d9b0df122f2d71 Mon Sep 17 00:00:00 2001 From: lunmay <28674102+lunmay@users.noreply.github.com> Date: Tue, 2 Aug 2022 19:22:29 +0200 Subject: [PATCH 3075/3516] Fix capitalization in mitemp_bt strings (#76063) Co-authored-by: Franck Nijhof --- homeassistant/components/mitemp_bt/strings.json | 2 +- homeassistant/components/mitemp_bt/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mitemp_bt/strings.json b/homeassistant/components/mitemp_bt/strings.json index d36c25eafec..1f9f031a3bb 100644 --- a/homeassistant/components/mitemp_bt/strings.json +++ b/homeassistant/components/mitemp_bt/strings.json @@ -2,7 +2,7 @@ "issues": { "replaced": { "title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced", - "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity Sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." } } } diff --git a/homeassistant/components/mitemp_bt/translations/en.json b/homeassistant/components/mitemp_bt/translations/en.json index cd5113ee02f..78ec041405b 100644 --- a/homeassistant/components/mitemp_bt/translations/en.json +++ b/homeassistant/components/mitemp_bt/translations/en.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity Sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", "title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced" } } From b9ee81dfc33704577d13305665eedf2184073f7f Mon Sep 17 00:00:00 2001 From: lunmay <28674102+lunmay@users.noreply.github.com> Date: Tue, 2 Aug 2022 19:22:29 +0200 Subject: [PATCH 3076/3516] Fix capitalization in mitemp_bt strings (#76063) Co-authored-by: Franck Nijhof --- homeassistant/components/mitemp_bt/strings.json | 2 +- homeassistant/components/mitemp_bt/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mitemp_bt/strings.json b/homeassistant/components/mitemp_bt/strings.json index d36c25eafec..1f9f031a3bb 100644 --- a/homeassistant/components/mitemp_bt/strings.json +++ b/homeassistant/components/mitemp_bt/strings.json @@ -2,7 +2,7 @@ "issues": { "replaced": { "title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced", - "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity Sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." } } } diff --git a/homeassistant/components/mitemp_bt/translations/en.json b/homeassistant/components/mitemp_bt/translations/en.json index cd5113ee02f..78ec041405b 100644 --- a/homeassistant/components/mitemp_bt/translations/en.json +++ b/homeassistant/components/mitemp_bt/translations/en.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "description": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration stopped working in Home Assistant 2022.7 and was replaced by the Xiaomi BLE integration in the 2022.8 release.\n\nThere is no migration path possible, therefore, you have to add your Xiaomi Mijia BLE device using the new integration manually.\n\nYour existing Xiaomi Mijia BLE Temperature and Humidity Sensor YAML configuration is no longer used by Home Assistant. Remove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", "title": "The Xiaomi Mijia BLE Temperature and Humidity Sensor integration has been replaced" } } From ed579515712ced262c88af69eeb9df031df5bdb1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Aug 2022 11:42:57 +0200 Subject: [PATCH 3077/3516] Small title adjustment to the Home Assistant Alerts integration (#76070) --- homeassistant/components/homeassistant_alerts/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant_alerts/manifest.json b/homeassistant/components/homeassistant_alerts/manifest.json index 0d276c6f3ae..7c9ddf4f905 100644 --- a/homeassistant/components/homeassistant_alerts/manifest.json +++ b/homeassistant/components/homeassistant_alerts/manifest.json @@ -1,6 +1,6 @@ { "domain": "homeassistant_alerts", - "name": "Home Assistant alerts", + "name": "Home Assistant Alerts", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/homeassistant_alerts", "codeowners": ["@home-assistant/core"], From 676664022d5716b2347044e2d42112412e31136c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Aug 2022 14:13:07 +0200 Subject: [PATCH 3078/3516] Handle missing attributes in meater objects (#76072) --- homeassistant/components/meater/sensor.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index 84ef3a2e2a9..a2753a42307 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -43,14 +43,18 @@ class MeaterSensorEntityDescription( def _elapsed_time_to_timestamp(probe: MeaterProbe) -> datetime | None: """Convert elapsed time to timestamp.""" - if not probe.cook: + if not probe.cook or not hasattr(probe.cook, "time_elapsed"): return None return dt_util.utcnow() - timedelta(seconds=probe.cook.time_elapsed) def _remaining_time_to_timestamp(probe: MeaterProbe) -> datetime | None: """Convert remaining time to timestamp.""" - if not probe.cook or probe.cook.time_remaining < 0: + if ( + not probe.cook + or not hasattr(probe.cook, "time_remaining") + or probe.cook.time_remaining < 0 + ): return None return dt_util.utcnow() + timedelta(seconds=probe.cook.time_remaining) @@ -99,7 +103,9 @@ SENSOR_TYPES = ( native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, available=lambda probe: probe is not None and probe.cook is not None, - value=lambda probe: probe.cook.target_temperature if probe.cook else None, + value=lambda probe: probe.cook.target_temperature + if probe.cook and hasattr(probe.cook, "target_temperature") + else None, ), # Peak temperature MeaterSensorEntityDescription( @@ -109,7 +115,9 @@ SENSOR_TYPES = ( native_unit_of_measurement=TEMP_CELSIUS, state_class=SensorStateClass.MEASUREMENT, available=lambda probe: probe is not None and probe.cook is not None, - value=lambda probe: probe.cook.peak_temperature if probe.cook else None, + value=lambda probe: probe.cook.peak_temperature + if probe.cook and hasattr(probe.cook, "peak_temperature") + else None, ), # Remaining time in seconds. When unknown/calculating default is used. Default: -1 # Exposed as a TIMESTAMP sensor where the timestamp is current time + remaining time. From 654e26052bd8c94cef527ca85e36db500701524b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Aug 2022 13:03:34 +0200 Subject: [PATCH 3079/3516] Remove Somfy from Overkiz title in manifest (#76073) --- homeassistant/components/overkiz/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index a7595065224..6e6e57f12e5 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -1,6 +1,6 @@ { "domain": "overkiz", - "name": "Overkiz (by Somfy)", + "name": "Overkiz", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", "requirements": ["pyoverkiz==1.4.2"], From 2eddbf2381613b39db88c5c6063cf8e0b4064904 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 2 Aug 2022 13:11:06 +0100 Subject: [PATCH 3080/3516] Fix typo in new xiaomi_ble string (#76076) --- homeassistant/components/xiaomi_ble/strings.json | 2 +- homeassistant/components/xiaomi_ble/translations/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index 48d5c3a87f7..9d2a0ae40d8 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -12,7 +12,7 @@ "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" }, "slow_confirm": { - "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if its needed." + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." }, "get_encryption_key_legacy": { "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey.", diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json index 836ccc51637..4648b28cc93 100644 --- a/homeassistant/components/xiaomi_ble/translations/en.json +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -27,7 +27,7 @@ "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey." }, "slow_confirm": { - "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if its needed." + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." }, "user": { "data": { From c90a223cb6e9f06782eca0ee4abeca67137376a3 Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 2 Aug 2022 10:10:20 -0400 Subject: [PATCH 3081/3516] Bump AIOAladdinConnect to 0.1.39 (#76082) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 008b8f81c89..5e55f391aa6 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.37"], + "requirements": ["AIOAladdinConnect==0.1.39"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 2b38d1155e7..52b514d7b0c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.37 +AIOAladdinConnect==0.1.39 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ef74a1c6dd..fd332772724 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.37 +AIOAladdinConnect==0.1.39 # homeassistant.components.adax Adax-local==0.1.4 From cc9a130f580fe25422ede49a5e36d647484cf763 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 2 Aug 2022 19:05:09 +0200 Subject: [PATCH 3082/3516] Refresh homeassistant_alerts when hass has started (#76083) --- homeassistant/components/homeassistant_alerts/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homeassistant_alerts/__init__.py b/homeassistant/components/homeassistant_alerts/__init__.py index d405b9e257d..60386e3d080 100644 --- a/homeassistant/components/homeassistant_alerts/__init__.py +++ b/homeassistant/components/homeassistant_alerts/__init__.py @@ -14,6 +14,7 @@ from homeassistant.components.repairs.models import IssueSeverity from homeassistant.const import __version__ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.start import async_at_start from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.yaml import parse_yaml @@ -100,7 +101,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: coordinator = AlertUpdateCoordinator(hass) coordinator.async_add_listener(async_schedule_update_alerts) - await coordinator.async_refresh() + + async def initial_refresh(hass: HomeAssistant) -> None: + await coordinator.async_refresh() + + async_at_start(hass, initial_refresh) return True From c4906414ea807502ae2cf5c5a89e813fd77478f1 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 2 Aug 2022 11:29:32 -0400 Subject: [PATCH 3083/3516] Ensure ZHA devices load before validating device triggers (#76084) --- homeassistant/components/zha/__init__.py | 5 ++++- homeassistant/components/zha/core/const.py | 1 + homeassistant/components/zha/device_trigger.py | 9 +++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 70b9dfd9b46..0a7d43120f7 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -35,6 +35,7 @@ from .core.const import ( DOMAIN, PLATFORMS, SIGNAL_ADD_ENTITIES, + ZHA_DEVICES_LOADED_EVENT, RadioType, ) from .core.discovery import GROUP_PROBE @@ -75,7 +76,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up ZHA from config.""" - hass.data[DATA_ZHA] = {} + hass.data[DATA_ZHA] = {ZHA_DEVICES_LOADED_EVENT: asyncio.Event()} if DOMAIN in config: conf = config[DOMAIN] @@ -109,6 +110,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() + hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].set() device_registry = dr.async_get(hass) device_registry.async_get_or_create( @@ -141,6 +143,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> """Unload ZHA config entry.""" zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] await zha_gateway.shutdown() + hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].clear() GROUP_PROBE.cleanup() api.async_unload_api(hass) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 4c8c6e03c79..dfa5f608cfe 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -394,6 +394,7 @@ ZHA_GW_MSG_GROUP_REMOVED = "group_removed" ZHA_GW_MSG_LOG_ENTRY = "log_entry" ZHA_GW_MSG_LOG_OUTPUT = "log_output" ZHA_GW_MSG_RAW_INIT = "raw_device_initialized" +ZHA_DEVICES_LOADED_EVENT = "zha_devices_loaded_event" EFFECT_BLINK = 0x00 EFFECT_BREATHE = 0x01 diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index cdd98110f83..4ad8eccea1d 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -16,8 +16,8 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError, IntegrationError from homeassistant.helpers.typing import ConfigType -from . import DOMAIN -from .core.const import ZHA_EVENT +from . import DOMAIN as ZHA_DOMAIN +from .core.const import DATA_ZHA, ZHA_DEVICES_LOADED_EVENT, ZHA_EVENT from .core.helpers import async_get_zha_device CONF_SUBTYPE = "subtype" @@ -35,7 +35,8 @@ async def async_validate_trigger_config( """Validate config.""" config = TRIGGER_SCHEMA(config) - if "zha" in hass.config.components: + if ZHA_DOMAIN in hass.config.components: + await hass.data[DATA_ZHA][ZHA_DEVICES_LOADED_EVENT].wait() trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) try: zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) @@ -100,7 +101,7 @@ async def async_get_triggers( triggers.append( { CONF_DEVICE_ID: device_id, - CONF_DOMAIN: DOMAIN, + CONF_DOMAIN: ZHA_DOMAIN, CONF_PLATFORM: DEVICE, CONF_TYPE: trigger, CONF_SUBTYPE: subtype, From e073f6b4391bc5177ebda1aa9bf6a2f80a42cb4d Mon Sep 17 00:00:00 2001 From: Zack Barett Date: Tue, 2 Aug 2022 11:08:33 -0500 Subject: [PATCH 3084/3516] Bump Frontend to 20220802.0 (#76087) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 45331491aa0..ed9b381ee9d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220728.0"], + "requirements": ["home-assistant-frontend==20220802.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bea9b749445..f2140a9eca7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.1 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220728.0 +home-assistant-frontend==20220802.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index 52b514d7b0c..71aee1700ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -839,7 +839,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220728.0 +home-assistant-frontend==20220802.0 # homeassistant.components.home_connect homeconnect==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd332772724..61a4b29b3b7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,7 +616,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220728.0 +home-assistant-frontend==20220802.0 # homeassistant.components.home_connect homeconnect==0.7.1 From 19b0961084068c7e8865ed0db5a20d2f69518a14 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Aug 2022 19:37:52 +0200 Subject: [PATCH 3085/3516] Bumped version to 2022.8.0b6 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 385db6904e4..3e7204e1746 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index fff0c129cec..2fa072c6d30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0b5" +version = "2022.8.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From cf849c59a4cf15893742a0a9e460b591b1d9ccd6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 09:11:50 -1000 Subject: [PATCH 3086/3516] Bump pyatv to 0.10.3 (#76091) --- homeassistant/components/apple_tv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index dec195fddee..5717f851b81 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -3,7 +3,7 @@ "name": "Apple TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", - "requirements": ["pyatv==0.10.2"], + "requirements": ["pyatv==0.10.3"], "dependencies": ["zeroconf"], "zeroconf": [ "_mediaremotetv._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 70169ffa4f4..253dee5445a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1398,7 +1398,7 @@ pyatmo==6.2.4 pyatome==0.1.1 # homeassistant.components.apple_tv -pyatv==0.10.2 +pyatv==0.10.3 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bdfa620a64a..8a8adbdf846 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -971,7 +971,7 @@ pyatag==0.3.5.3 pyatmo==6.2.4 # homeassistant.components.apple_tv -pyatv==0.10.2 +pyatv==0.10.3 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 From a628be4db8c5aebd09a097f0ab8e71b8fd672414 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 10:38:01 -1000 Subject: [PATCH 3087/3516] Only stat the .dockerenv file once (#76097) --- homeassistant/util/package.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 18ab43967ec..49ab3c10f8c 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from functools import cache from importlib.metadata import PackageNotFoundError, version import logging import os @@ -23,6 +24,7 @@ def is_virtual_env() -> bool: ) +@cache def is_docker_env() -> bool: """Return True if we run in a docker env.""" return Path("/.dockerenv").exists() From a0adfb9e62564615c1de8ea619634ddc3e102936 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 2 Aug 2022 21:38:38 +0100 Subject: [PATCH 3088/3516] Fix serialization of Xiaomi BLE reauth flow (#76095) * Use data instead of context to fix serialisation bug * Test change to async_start_reauth --- homeassistant/components/xiaomi_ble/config_flow.py | 2 +- homeassistant/components/xiaomi_ble/sensor.py | 2 +- homeassistant/config_entries.py | 7 +++++-- tests/components/xiaomi_ble/test_config_flow.py | 3 +-- tests/test_config_entries.py | 12 ++++++++++-- 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index 092c60e9713..725c513914f 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -260,7 +260,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) assert entry is not None - device: DeviceData = self.context["device"] + device: DeviceData = entry_data["device"] self._discovered_device = device self._discovery_info = device.last_service_info diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index fef9b334e75..dcb95422609 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -181,7 +181,7 @@ def process_service_info( and data.encryption_scheme != EncryptionScheme.NONE and not data.bindkey_verified ): - entry.async_start_reauth(hass, context={"device": data}) + entry.async_start_reauth(hass, data={"device": data}) return sensor_update_to_bluetooth_data_update(update) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 638aa0b8110..7c2f1c84ff9 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -641,7 +641,10 @@ class ConfigEntry: @callback def async_start_reauth( - self, hass: HomeAssistant, context: dict[str, Any] | None = None + self, + hass: HomeAssistant, + context: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, ) -> None: """Start a reauth flow.""" flow_context = { @@ -662,7 +665,7 @@ class ConfigEntry: hass.config_entries.flow.async_init( self.domain, context=flow_context, - data=self.data, + data=self.data | (data or {}), ) ) diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index 0d123f0cd54..32ba6be3322 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -1033,9 +1033,8 @@ async def test_async_step_reauth_abort_early(hass): "entry_id": entry.entry_id, "title_placeholders": {"name": entry.title}, "unique_id": entry.unique_id, - "device": device, }, - data=entry.data, + data=entry.data | {"device": device}, ) assert result["type"] == FlowResultType.ABORT diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 66f51508441..b923e37b636 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3286,8 +3286,14 @@ async def test_reauth(hass): await entry.async_setup(hass) await hass.async_block_till_done() - entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) - await hass.async_block_till_done() + flow = hass.config_entries.flow + with patch.object(flow, "async_init", wraps=flow.async_init) as mock_init: + entry.async_start_reauth( + hass, + context={"extra_context": "some_extra_context"}, + data={"extra_data": 1234}, + ) + await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -3296,6 +3302,8 @@ async def test_reauth(hass): assert flows[0]["context"]["title_placeholders"] == {"name": "test_title"} assert flows[0]["context"]["extra_context"] == "some_extra_context" + assert mock_init.call_args.kwargs["data"]["extra_data"] == 1234 + # Check we can't start duplicate flows entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) await hass.async_block_till_done() From fbf3c1a5d4572fe94db0ac53310690f4e491f290 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 2 Aug 2022 22:05:36 +0100 Subject: [PATCH 3089/3516] Fix Xiaomi BLE UI string issues (#76099) --- homeassistant/components/xiaomi_ble/config_flow.py | 2 +- homeassistant/components/xiaomi_ble/strings.json | 12 +++++++----- .../components/xiaomi_ble/translations/en.json | 14 ++++++++------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index 725c513914f..a05e703db6a 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -189,7 +189,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Ack that device is slow.""" - if user_input is not None or not onboarding.async_is_onboarded(self.hass): + if user_input is not None: return self._async_get_or_create_entry() self._set_confirm_only() diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index 9d2a0ae40d8..5ecbb8e1b88 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -11,7 +11,7 @@ "bluetooth_confirm": { "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" }, - "slow_confirm": { + "confirm_slow": { "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." }, "get_encryption_key_legacy": { @@ -27,14 +27,16 @@ } } }, + "error": { + "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", + "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", + "expected_32_characters": "Expected a 32 character hexadecimal bindkey." + }, "abort": { "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", - "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", - "expected_32_characters": "Expected a 32 character hexadecimal bindkey." + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } } diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json index 4648b28cc93..2cb77dd2c07 100644 --- a/homeassistant/components/xiaomi_ble/translations/en.json +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -3,17 +3,22 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", - "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", - "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", - "expected_32_characters": "Expected a 32 character hexadecimal bindkey.", "no_devices_found": "No devices found on the network", "reauth_successful": "Re-authentication was successful" }, + "error": { + "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", + "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", + "expected_32_characters": "Expected a 32 character hexadecimal bindkey." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, + "confirm_slow": { + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindkey" @@ -26,9 +31,6 @@ }, "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey." }, - "slow_confirm": { - "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." - }, "user": { "data": { "address": "Device" From bf931f1225ee82b143b4472ac94281941c1c3d9e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 13:46:43 -1000 Subject: [PATCH 3090/3516] Handle additional bluetooth start exceptions (#76096) --- .../components/bluetooth/__init__.py | 33 +++++- tests/components/bluetooth/test_init.py | 110 ++++++++++++++++++ 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 39629ab6d85..c91563d7729 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Final import async_timeout from bleak import BleakError +from dbus_next import InvalidMessageError from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -26,6 +27,7 @@ from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.loader import async_get_bluetooth +from homeassistant.util.package import is_docker_env from . import models from .const import CONF_ADAPTER, DEFAULT_ADAPTERS, DOMAIN @@ -341,13 +343,42 @@ class BluetoothManager: try: async with async_timeout.timeout(START_TIMEOUT): await self.scanner.start() # type: ignore[no-untyped-call] + except InvalidMessageError as ex: + self._cancel_device_detected() + _LOGGER.debug("Invalid DBus message received: %s", ex, exc_info=True) + raise ConfigEntryNotReady( + f"Invalid DBus message received: {ex}; try restarting `dbus`" + ) from ex + except BrokenPipeError as ex: + self._cancel_device_detected() + _LOGGER.debug("DBus connection broken: %s", ex, exc_info=True) + if is_docker_env(): + raise ConfigEntryNotReady( + f"DBus connection broken: {ex}; try restarting `bluetooth`, `dbus`, and finally the docker container" + ) from ex + raise ConfigEntryNotReady( + f"DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`" + ) from ex + except FileNotFoundError as ex: + self._cancel_device_detected() + _LOGGER.debug( + "FileNotFoundError while starting bluetooth: %s", ex, exc_info=True + ) + if is_docker_env(): + raise ConfigEntryNotReady( + f"DBus service not found; docker config may be missing `-v /run/dbus:/run/dbus:ro`: {ex}" + ) from ex + raise ConfigEntryNotReady( + f"DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}" + ) from ex except asyncio.TimeoutError as ex: self._cancel_device_detected() raise ConfigEntryNotReady( f"Timed out starting Bluetooth after {START_TIMEOUT} seconds" ) from ex - except (FileNotFoundError, BleakError) as ex: + except BleakError as ex: self._cancel_device_detected() + _LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True) raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex self.async_setup_unavailable_tracking() self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index a47916506df..edc5eb024a6 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch from bleak import BleakError from bleak.backends.scanner import AdvertisementData, BLEDevice +from dbus_next import InvalidMessageError import pytest from homeassistant.components import bluetooth @@ -1409,3 +1410,112 @@ async def test_changing_the_adapter_at_runtime(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() + + +async def test_dbus_socket_missing_in_container(hass, caplog): + """Test we handle dbus being missing in the container.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=True + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=FileNotFoundError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "/run/dbus" in caplog.text + assert "docker" in caplog.text + + +async def test_dbus_socket_missing(hass, caplog): + """Test we handle dbus being missing.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=False + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=FileNotFoundError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "DBus" in caplog.text + assert "docker" not in caplog.text + + +async def test_dbus_broken_pipe_in_container(hass, caplog): + """Test we handle dbus broken pipe in the container.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=True + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=BrokenPipeError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "dbus" in caplog.text + assert "restarting" in caplog.text + assert "container" in caplog.text + + +async def test_dbus_broken_pipe(hass, caplog): + """Test we handle dbus broken pipe.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=False + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=BrokenPipeError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "DBus" in caplog.text + assert "restarting" in caplog.text + assert "container" not in caplog.text + + +async def test_invalid_dbus_message(hass, caplog): + """Test we handle invalid dbus message.""" + + with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=InvalidMessageError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "dbus" in caplog.text From e09bbc749c06f55de0437ff882f9c2e17065c438 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 3 Aug 2022 00:28:23 +0000 Subject: [PATCH 3091/3516] [ci skip] Translation update --- .../ambiclimate/translations/hu.json | 2 +- .../components/govee_ble/translations/no.json | 6 +++ .../here_travel_time/translations/no.json | 6 ++- .../homeassistant/translations/no.json | 1 + .../homeassistant_alerts/translations/no.json | 8 ++++ .../homekit_controller/translations/no.json | 4 +- .../components/inkbird/translations/no.json | 1 + .../lg_soundbar/translations/id.json | 2 +- .../lg_soundbar/translations/no.json | 2 +- .../mitemp_bt/translations/pt-BR.json | 2 +- .../components/moat/translations/no.json | 13 +++++- .../components/nest/translations/ca.json | 8 ++++ .../components/nest/translations/de.json | 10 +++++ .../components/nest/translations/en.json | 8 ++++ .../components/nest/translations/fr.json | 5 +++ .../components/nest/translations/hu.json | 10 +++++ .../components/nest/translations/id.json | 10 +++++ .../components/nest/translations/it.json | 8 ++++ .../components/nest/translations/no.json | 10 +++++ .../components/nest/translations/pt-BR.json | 10 +++++ .../components/nest/translations/zh-Hant.json | 10 +++++ .../components/nextdns/translations/no.json | 10 +++++ .../openalpr_local/translations/no.json | 8 ++++ .../opentherm_gw/translations/id.json | 3 +- .../opentherm_gw/translations/no.json | 3 +- .../components/plugwise/translations/no.json | 3 +- .../radiotherm/translations/no.json | 6 +++ .../simplepush/translations/no.json | 3 +- .../simplisafe/translations/no.json | 7 +++- .../smartthings/translations/hu.json | 2 +- .../soundtouch/translations/no.json | 6 +++ .../components/spotify/translations/no.json | 6 +++ .../steam_online/translations/no.json | 6 +++ .../components/switchbot/translations/no.json | 3 +- .../unifiprotect/translations/hu.json | 2 +- .../components/uscis/translations/no.json | 8 ++++ .../components/verisure/translations/no.json | 15 ++++++- .../components/withings/translations/no.json | 1 + .../components/xbox/translations/no.json | 6 +++ .../xiaomi_ble/translations/ca.json | 3 +- .../xiaomi_ble/translations/de.json | 6 ++- .../xiaomi_ble/translations/en.json | 6 +++ .../xiaomi_ble/translations/fr.json | 3 +- .../xiaomi_ble/translations/hu.json | 6 ++- .../xiaomi_ble/translations/id.json | 6 ++- .../xiaomi_ble/translations/no.json | 40 +++++++++++++++++++ .../xiaomi_ble/translations/pt-BR.json | 6 ++- .../xiaomi_ble/translations/zh-Hant.json | 6 ++- .../components/zha/translations/no.json | 3 ++ 49 files changed, 293 insertions(+), 26 deletions(-) create mode 100644 homeassistant/components/homeassistant_alerts/translations/no.json create mode 100644 homeassistant/components/openalpr_local/translations/no.json create mode 100644 homeassistant/components/uscis/translations/no.json create mode 100644 homeassistant/components/xiaomi_ble/translations/no.json diff --git a/homeassistant/components/ambiclimate/translations/hu.json b/homeassistant/components/ambiclimate/translations/hu.json index 92ea617393e..421d692bbba 100644 --- a/homeassistant/components/ambiclimate/translations/hu.json +++ b/homeassistant/components/ambiclimate/translations/hu.json @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "K\u00e9rem, k\u00f6vesse ezt a [link]({authorization_url}}) \u00e9s **Enged\u00e9lyezze** a hozz\u00e1f\u00e9r\u00e9st Ambiclimate -fi\u00f3kj\u00e1hoz, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi **Mehet** gombot.\n(Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a megadott visszah\u00edv\u00e1si URL {cb_url})", + "description": "K\u00e9rem, k\u00f6vesse a [linket]({authorization_url}) \u00e9s **Enged\u00e9lyezze** a hozz\u00e1f\u00e9r\u00e9st Ambiclimate -fi\u00f3kj\u00e1hoz, majd t\u00e9rjen vissza, \u00e9s nyomja meg az al\u00e1bbi **Mehet** gombot.\n(Gy\u0151z\u0151dj\u00f6n meg arr\u00f3l, hogy a visszah\u00edv\u00e1si URL {cb_url})", "title": "Ambiclimate hiteles\u00edt\u00e9se" } } diff --git a/homeassistant/components/govee_ble/translations/no.json b/homeassistant/components/govee_ble/translations/no.json index 4fd1e1d0c9d..28ec4582177 100644 --- a/homeassistant/components/govee_ble/translations/no.json +++ b/homeassistant/components/govee_ble/translations/no.json @@ -9,6 +9,12 @@ "step": { "bluetooth_confirm": { "description": "Vil du konfigurere {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" } } } diff --git a/homeassistant/components/here_travel_time/translations/no.json b/homeassistant/components/here_travel_time/translations/no.json index e4282933051..4af798a78fc 100644 --- a/homeassistant/components/here_travel_time/translations/no.json +++ b/homeassistant/components/here_travel_time/translations/no.json @@ -41,8 +41,10 @@ }, "origin_menu": { "menu_options": { - "origin_coordinates": "Bruk kartplassering" - } + "origin_coordinates": "Bruk kartplassering", + "origin_entity": "Bruke en enhet" + }, + "title": "Velg Opprinnelse" }, "user": { "data": { diff --git a/homeassistant/components/homeassistant/translations/no.json b/homeassistant/components/homeassistant/translations/no.json index 675c02a6b66..0932c2e75a6 100644 --- a/homeassistant/components/homeassistant/translations/no.json +++ b/homeassistant/components/homeassistant/translations/no.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "CPU-arkitektur", + "config_dir": "Konfigurasjonskatalog", "dev": "Utvikling", "docker": "", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant_alerts/translations/no.json b/homeassistant/components/homeassistant_alerts/translations/no.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/no.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/no.json b/homeassistant/components/homekit_controller/translations/no.json index c383dddb763..70b74d51f64 100644 --- a/homeassistant/components/homekit_controller/translations/no.json +++ b/homeassistant/components/homekit_controller/translations/no.json @@ -18,7 +18,7 @@ "unable_to_pair": "Kunne ikke koble til, vennligst pr\u00f8v igjen.", "unknown_error": "Enheten rapporterte en ukjent feil. Sammenkobling mislyktes." }, - "flow_title": "{name}", + "flow_title": "{name} ( {category} )", "step": { "busy_error": { "description": "Avbryt sammenkobling p\u00e5 alle kontrollere, eller pr\u00f8v \u00e5 starte enheten p\u00e5 nytt, og fortsett deretter med \u00e5 fortsette sammenkoblingen.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Tillat sammenkobling med usikre oppsettkoder.", "pairing_code": "Sammenkoblingskode" }, - "description": "HomeKit Controller kommuniserer med {name} over lokalnettverket ved hjelp av en sikker kryptert tilkobling uten en separat HomeKit-kontroller eller iCloud. Skriv inn HomeKit-paringskoden (i formatet XXX-XX-XXX) for \u00e5 bruke dette tilbeh\u00f8ret. Denne koden finnes vanligvis p\u00e5 selve enheten eller i emballasjen.", + "description": "HomeKit-kontrolleren kommuniserer med {name} ( {category} ) over det lokale nettverket ved hjelp av en sikker kryptert tilkobling uten en separat HomeKit-kontroller eller iCloud. Skriv inn HomeKit-paringskoden (i formatet XXX-XX-XXX) for \u00e5 bruke dette tilbeh\u00f8ret. Denne koden finnes vanligvis p\u00e5 selve enheten eller i emballasjen.", "title": "Par med en enhet via HomeKit Accessory Protocol" }, "protocol_error": { diff --git a/homeassistant/components/inkbird/translations/no.json b/homeassistant/components/inkbird/translations/no.json index 3cf7f2b76c8..28ec4582177 100644 --- a/homeassistant/components/inkbird/translations/no.json +++ b/homeassistant/components/inkbird/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" }, diff --git a/homeassistant/components/lg_soundbar/translations/id.json b/homeassistant/components/lg_soundbar/translations/id.json index 74d380f3a1a..3f6d9ea8f81 100644 --- a/homeassistant/components/lg_soundbar/translations/id.json +++ b/homeassistant/components/lg_soundbar/translations/id.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Layanan sudah dikonfigurasi", + "already_configured": "Perangkat sudah dikonfigurasi", "existing_instance_updated": "Memperbarui konfigurasi yang ada." }, "error": { diff --git a/homeassistant/components/lg_soundbar/translations/no.json b/homeassistant/components/lg_soundbar/translations/no.json index 41eef0e4c16..58d4c11916b 100644 --- a/homeassistant/components/lg_soundbar/translations/no.json +++ b/homeassistant/components/lg_soundbar/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Tjenesten er allerede konfigurert", + "already_configured": "Enheten er allerede konfigurert", "existing_instance_updated": "Oppdatert eksisterende konfigurasjon." }, "error": { diff --git a/homeassistant/components/mitemp_bt/translations/pt-BR.json b/homeassistant/components/mitemp_bt/translations/pt-BR.json index 634f5dd71fd..991a749a729 100644 --- a/homeassistant/components/mitemp_bt/translations/pt-BR.json +++ b/homeassistant/components/mitemp_bt/translations/pt-BR.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "A integra\u00e7\u00e3o do sensor de temperatura e umidade do Xiaomi Mijia BLE parou de funcionar no Home Assistant 2022.7 e foi substitu\u00edda pela integra\u00e7\u00e3o do Xiaomi BLE na vers\u00e3o 2022.8. \n\n N\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o poss\u00edvel, portanto, voc\u00ea deve adicionar seu dispositivo Xiaomi Mijia BLE usando a nova integra\u00e7\u00e3o manualmente. \n\n Sua configura\u00e7\u00e3o YAML existente do sensor de temperatura e umidade Xiaomi Mijia BLE n\u00e3o \u00e9 mais usada pelo Home Assistant. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "description": "A integra\u00e7\u00e3o do sensor de temperatura e umidade Xiaomi Mijia BLE parou de funcionar no Home Assistant 2022.7 e foi substitu\u00edda pela integra\u00e7\u00e3o Xiaomi BLE na vers\u00e3o 2022.8. \n\n N\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o poss\u00edvel, portanto, voc\u00ea deve adicionar seu dispositivo Xiaomi Mijia BLE usando a nova integra\u00e7\u00e3o manualmente. \n\n Sua configura\u00e7\u00e3o YAML existente do sensor de temperatura e umidade Xiaomi Mijia BLE n\u00e3o \u00e9 mais usada pelo Home Assistant. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", "title": "A integra\u00e7\u00e3o do sensor de temperatura e umidade Xiaomi Mijia BLE foi substitu\u00edda" } } diff --git a/homeassistant/components/moat/translations/no.json b/homeassistant/components/moat/translations/no.json index bce03ad33d7..28ec4582177 100644 --- a/homeassistant/components/moat/translations/no.json +++ b/homeassistant/components/moat/translations/no.json @@ -5,6 +5,17 @@ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" }, - "flow_title": "{name}" + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index 791be9975eb..33d0179208b 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -96,5 +96,13 @@ "camera_sound": "So detectat", "doorbell_chime": "Timbre premut" } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuraci\u00f3 YAML de Nest est\u00e0 sent eliminada" + }, + "removed_app_auth": { + "title": "Les credencials d'autenticaci\u00f3 de Nest s'han d'actualitzar" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index 933231fd1e8..675b9087b6c 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -96,5 +96,15 @@ "camera_sound": "Ger\u00e4usch erkannt", "doorbell_chime": "T\u00fcrklingel gedr\u00fcckt" } + }, + "issues": { + "deprecated_yaml": { + "description": "Das Konfigurieren von Nest in configuration.yaml wird in Home Assistant 2022.10 entfernt. \n\nDeine bestehenden OAuth-Anwendungsdaten und Zugriffseinstellungen wurden automatisch in die Benutzeroberfl\u00e4che importiert. Entferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Nest-YAML-Konfiguration wird entfernt" + }, + "removed_app_auth": { + "description": "Um die Sicherheit zu verbessern und das Phishing-Risiko zu verringern, hat Google die von Home Assistant verwendete Authentifizierungsmethode eingestellt. \n\n **Zur L\u00f6sung sind Ma\u00dfnahmen deinerseits erforderlich** ([more info]( {more_info_url} )) \n\n 1. Besuche die Integrationsseite\n 1. Klicke in der Nest-Integration auf Neu konfigurieren.\n 1. Home Assistant f\u00fchrt dich durch die Schritte zum Upgrade auf die Webauthentifizierung. \n\n Informationen zur Fehlerbehebung findest du in der Nest [Integrationsanleitung]( {documentation_url} ).", + "title": "Nest-Authentifizierungsdaten m\u00fcssen aktualisiert werden" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index cd8274d635a..e0c0b8e67a5 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -10,6 +10,7 @@ "missing_configuration": "The component is not configured. Please follow the documentation.", "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", "reauth_successful": "Re-authentication was successful", + "single_instance_allowed": "Already configured. Only a single configuration possible.", "unknown_authorize_url_generation": "Unknown error generating an authorize URL." }, "create_entry": { @@ -25,6 +26,13 @@ "wrong_project_id": "Please enter a valid Cloud Project ID (was same as Device Access Project ID)" }, "step": { + "auth": { + "data": { + "code": "Access Token" + }, + "description": "To link your Google account, [authorize your account]({url}).\n\nAfter authorization, copy-paste the provided Auth Token code below.", + "title": "Link Google Account" + }, "auth_upgrade": { "description": "App Auth has been deprecated by Google to improve security, and you need to take action by creating new application credentials.\n\nOpen the [documentation]({more_info_url}) to follow along as the next steps will guide you through the steps you need to take to restore access to your Nest devices.", "title": "Nest: App Auth Deprecation" diff --git a/homeassistant/components/nest/translations/fr.json b/homeassistant/components/nest/translations/fr.json index 16990b93193..7cbed195e0c 100644 --- a/homeassistant/components/nest/translations/fr.json +++ b/homeassistant/components/nest/translations/fr.json @@ -88,5 +88,10 @@ "camera_sound": "Son d\u00e9tect\u00e9", "doorbell_chime": "Sonnette enfonc\u00e9e" } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuration YAML pour Nest est en cours de suppression" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json index 228f4f5e745..f453fdef150 100644 --- a/homeassistant/components/nest/translations/hu.json +++ b/homeassistant/components/nest/translations/hu.json @@ -96,5 +96,15 @@ "camera_sound": "Hang \u00e9szlelve", "doorbell_chime": "Cseng\u0151 megnyomva" } + }, + "issues": { + "deprecated_yaml": { + "description": "Nest konfigur\u00e1l\u00e1sa a configuration.yaml f\u00e1jl \u00e1ltal a 2022.10-es Home Assistantban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 OAuth alkalmaz\u00e1s hiteles\u00edt\u0151 adatai \u00e9s hozz\u00e1f\u00e9r\u00e9si be\u00e1ll\u00edt\u00e1sai automatikusan import\u00e1l\u00e1sra ker\u00fclnek a felhaszn\u00e1l\u00f3i fel\u00fcletre. A probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Nest YAML konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + }, + "removed_app_auth": { + "description": "A biztons\u00e1g meger\u0151s\u00edt\u00e9se \u00e9s az adathal\u00e1sz kock\u00e1zat cs\u00f6kkent\u00e9se \u00e9rdek\u00e9ben a Google megsz\u00fcntette a Home Assistant \u00e1ltal haszn\u00e1lt hiteles\u00edt\u00e9si m\u00f3dszert.\n\n**Ez az \u00d6n r\u00e9sz\u00e9r\u0151l int\u00e9zked\u00e9st ig\u00e9nyel a megold\u00e1shoz** ([b\u0151vebb inform\u00e1ci\u00f3]({more_info_url}))\n\n1. L\u00e1togasson el az integr\u00e1ci\u00f3k oldalra\n2. Kattintson az \u00dajrakonfigur\u00e1l\u00e1s gombra a Nest integr\u00e1ci\u00f3ban.\n3. A rendszer v\u00e9gigvezeti \u00d6nt a webes hiteles\u00edt\u00e9sre val\u00f3 friss\u00edt\u00e9s l\u00e9p\u00e9sein.\n\nA hibaelh\u00e1r\u00edt\u00e1ssal kapcsolatos inform\u00e1ci\u00f3k a Nest [integr\u00e1ci\u00f3s utas\u00edt\u00e1sok]({documentation_url}) dokumentumban tal\u00e1lhat\u00f3k.", + "title": "A Nest hiteles\u00edt\u0151 adatait friss\u00edteni sz\u00fcks\u00e9ges" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/id.json b/homeassistant/components/nest/translations/id.json index 6a45ccaee8c..1d69f9f48bd 100644 --- a/homeassistant/components/nest/translations/id.json +++ b/homeassistant/components/nest/translations/id.json @@ -96,5 +96,15 @@ "camera_sound": "Suara terdeteksi", "doorbell_chime": "Bel pintu ditekan" } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Nest di configuration.yaml sedang dihapus di Home Assistant 2022.10. \n\nKredensial Aplikasi OAuth yang Anda dan setelan akses telah diimpor ke antarmuka secara otomatis. Hapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Nest dalam proses penghapusan" + }, + "removed_app_auth": { + "description": "Untuk meningkatkan keamanan dan mengurangi risiko phishing, Google telah menghentikan metode autentikasi yang digunakan oleh Home Assistant.\n\n**Tindakan berikut diperlukan untuk diselesaikan** ([info lebih lanjut]({more_info_url}))\n\n1. Kunjungi halaman integrasi\n1. Klik Konfigurasi Ulang pada integrasi Nest.\n1. Home Assistant akan memandu Anda melalui langkah-langkah untuk meningkatkan ke Autentikasi Web.\n\nLihat [instruksi integrasi]({documentation_url}) Nest untuk informasi pemecahan masalah.", + "title": "Kredensial Autentikasi Nest harus diperbarui" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 8979631dec0..6771cd00431 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -96,5 +96,13 @@ "camera_sound": "Suono rilevato", "doorbell_chime": "Campanello premuto" } + }, + "issues": { + "deprecated_yaml": { + "title": "La configurazione YAML di Nest sar\u00e0 rimossa" + }, + "removed_app_auth": { + "title": "Le credenziali di autenticazione Nest devono essere aggiornate" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json index ce6663ec22e..89ba7bc8b7c 100644 --- a/homeassistant/components/nest/translations/no.json +++ b/homeassistant/components/nest/translations/no.json @@ -96,5 +96,15 @@ "camera_sound": "Lyd oppdaget", "doorbell_chime": "Ringeklokke trykket" } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Nest i configuration.yaml blir fjernet i Home Assistant 2022.10. \n\n Din eksisterende OAuth-applikasjonslegitimasjon og tilgangsinnstillinger er automatisk importert til brukergrensesnittet. Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Nest YAML-konfigurasjonen fjernes" + }, + "removed_app_auth": { + "description": "For \u00e5 forbedre sikkerheten og redusere phishing-risikoen har Google avviklet autentiseringsmetoden som brukes av Home Assistant. \n\n **Dette krever handling fra deg for \u00e5 l\u00f8se det** ([mer info]( {more_info_url} )) \n\n 1. G\u00e5 til integreringssiden\n 1. Klikk p\u00e5 Reconfigure p\u00e5 Nest-integrasjonen.\n 1. Home Assistant vil lede deg gjennom trinnene for \u00e5 oppgradere til webautentisering. \n\n Se Nest [integrasjonsinstruksjoner]( {documentation_url} ) for feils\u00f8kingsinformasjon.", + "title": "Nest-autentiseringslegitimasjonen m\u00e5 oppdateres" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/pt-BR.json b/homeassistant/components/nest/translations/pt-BR.json index d96387aee82..973a3cf3b69 100644 --- a/homeassistant/components/nest/translations/pt-BR.json +++ b/homeassistant/components/nest/translations/pt-BR.json @@ -96,5 +96,15 @@ "camera_sound": "Som detectado", "doorbell_chime": "Campainha pressionada" } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Nest em configuration.yaml est\u00e1 sendo removida no Home Assistant 2022.10. \n\n Suas credenciais de aplicativo OAuth e configura\u00e7\u00f5es de acesso existentes foram importadas para a interface do usu\u00e1rio automaticamente. Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML de Nest est\u00e1 sendo removida" + }, + "removed_app_auth": { + "description": "Para melhorar a seguran\u00e7a e reduzir o risco de phishing, o Google desativou o m\u00e9todo de autentica\u00e7\u00e3o usado pelo Home Assistant. \n\n **Isso requer uma a\u00e7\u00e3o sua para resolver** ([mais informa\u00e7\u00f5es]( {more_info_url} )) \n\n 1. Visite a p\u00e1gina de integra\u00e7\u00f5es\n 1. Clique em Reconfigurar na integra\u00e7\u00e3o Nest.\n 1. O Home Assistant o guiar\u00e1 pelas etapas para atualizar para a autentica\u00e7\u00e3o da Web. \n\n Consulte as [instru\u00e7\u00f5es de integra\u00e7\u00e3o]( {documentation_url} ) do Nest para obter informa\u00e7\u00f5es sobre solu\u00e7\u00e3o de problemas.", + "title": "As credenciais de autentica\u00e7\u00e3o Nest precisam ser atualizadas" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/zh-Hant.json b/homeassistant/components/nest/translations/zh-Hant.json index 11a4823d77a..77f271518a5 100644 --- a/homeassistant/components/nest/translations/zh-Hant.json +++ b/homeassistant/components/nest/translations/zh-Hant.json @@ -96,5 +96,15 @@ "camera_sound": "\u5075\u6e2c\u5230\u8072\u97f3", "doorbell_chime": "\u9580\u9234\u6309\u4e0b" } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Nest \u5373\u5c07\u65bc Home Assistant 2022.10 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 OAuth \u61c9\u7528\u6191\u8b49\u8207\u5b58\u53d6\u6b0a\u9650\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Nest YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + }, + "removed_app_auth": { + "description": "\u70ba\u4e86\u6539\u5584\u8cc7\u5b89\u8207\u964d\u4f4e\u7db2\u8def\u91e3\u9b5a\u98a8\u96aa\u3001Google \u5df2\u7d93\u68c4\u7528 Home Assistant \u6240\u4f7f\u7528\u7684\u8a8d\u8b49\u6a21\u5f0f\u3002\n\n**\u5c07\u9700\u8981\u60a8\u9032\u884c\u6392\u9664** ([\u66f4\u591a\u8cc7\u8a0a]({more_info_url}))\n\n1. \u700f\u89bd\u6574\u5408\u9801\u9762\n1. \u65bc Nest \u6574\u5408\u9ede\u9078\u91cd\u65b0\u8a2d\u5b9a\n1. Home Assistant \u5c07\u6703\u5f15\u5c0e\u9032\u884c\u66f4\u65b0\u81f3 Web \u8a8d\u8b49\u6b65\u9a5f\u3002\n\n\u8acb\u53c3\u95b1 Nest [\u6574\u5408\u6307\u5f15]({documentation_url}) \u4ee5\u7372\u5f97\u554f\u984c\u6392\u9664\u8cc7\u8a0a\u3002", + "title": "Nest \u8a8d\u8b49\u6191\u8b49\u9700\u8981\u66f4\u65b0" + } } } \ No newline at end of file diff --git a/homeassistant/components/nextdns/translations/no.json b/homeassistant/components/nextdns/translations/no.json index fb4d6616587..a7685d0caa2 100644 --- a/homeassistant/components/nextdns/translations/no.json +++ b/homeassistant/components/nextdns/translations/no.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "Denne NextDNS-profilen er allerede konfigurert." + }, "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_api_key": "Ugyldig API-n\u00f8kkel", "unknown": "Totalt uventet feil" }, "step": { @@ -15,5 +20,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "N\u00e5 serveren" + } } } \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/no.json b/homeassistant/components/openalpr_local/translations/no.json new file mode 100644 index 00000000000..92e6841ef94 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/no.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "OpenALPR Local-integrasjonen venter p\u00e5 fjerning fra Home Assistant og vil ikke lenger v\u00e6re tilgjengelig fra og med Home Assistant 2022.10. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "OpenALPR Local-integrasjonen blir fjernet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/id.json b/homeassistant/components/opentherm_gw/translations/id.json index 5f87fd0ed4b..c41035d1800 100644 --- a/homeassistant/components/opentherm_gw/translations/id.json +++ b/homeassistant/components/opentherm_gw/translations/id.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Perangkat sudah dikonfigurasi", "cannot_connect": "Gagal terhubung", - "id_exists": "ID gateway sudah ada" + "id_exists": "ID gateway sudah ada", + "timeout_connect": "Tenggang waktu membuat koneksi habis" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/no.json b/homeassistant/components/opentherm_gw/translations/no.json index 5cf2e9732e9..cc29595fdc4 100644 --- a/homeassistant/components/opentherm_gw/translations/no.json +++ b/homeassistant/components/opentherm_gw/translations/no.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Enheten er allerede konfigurert", "cannot_connect": "Tilkobling mislyktes", - "id_exists": "Gateway ID finnes allerede" + "id_exists": "Gateway ID finnes allerede", + "timeout_connect": "Tidsavbrudd oppretter forbindelse" }, "step": { "init": { diff --git a/homeassistant/components/plugwise/translations/no.json b/homeassistant/components/plugwise/translations/no.json index d9fbb15b2e8..ad95ab8e4ee 100644 --- a/homeassistant/components/plugwise/translations/no.json +++ b/homeassistant/components/plugwise/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Tjenesten er allerede konfigurert" + "already_configured": "Tjenesten er allerede konfigurert", + "anna_with_adam": "B\u00e5de Anna og Adam oppdaget. Legg til din Adam i stedet for din Anna" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/radiotherm/translations/no.json b/homeassistant/components/radiotherm/translations/no.json index fc05e672cbe..4ba3f323973 100644 --- a/homeassistant/components/radiotherm/translations/no.json +++ b/homeassistant/components/radiotherm/translations/no.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av radiotermostatklimaplattformen ved hjelp av YAML blir fjernet i Home Assistant 2022.9. \n\n Din eksisterende konfigurasjon har blitt importert til brukergrensesnittet automatisk. Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Radiotermostat YAML-konfigurasjonen blir fjernet" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/simplepush/translations/no.json b/homeassistant/components/simplepush/translations/no.json index 5c2e447b098..af15a931acc 100644 --- a/homeassistant/components/simplepush/translations/no.json +++ b/homeassistant/components/simplepush/translations/no.json @@ -20,7 +20,8 @@ }, "issues": { "deprecated_yaml": { - "description": "Konfigurering av Simplepush med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Simplepush YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet." + "description": "Konfigurering av Simplepush med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Simplepush YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Simplepush YAML-konfigurasjonen blir fjernet" } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json index 7e6875a2590..a0335228b2d 100644 --- a/homeassistant/components/simplisafe/translations/no.json +++ b/homeassistant/components/simplisafe/translations/no.json @@ -3,9 +3,11 @@ "abort": { "already_configured": "Denne SimpliSafe-kontoen er allerede i bruk.", "email_2fa_timed_out": "Tidsavbrudd mens du ventet p\u00e5 e-postbasert tofaktorautentisering.", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "wrong_account": "Oppgitt brukerlegitimasjon samsvarer ikke med denne SimpliSafe-kontoen." }, "error": { + "identifier_exists": "Konto er allerede registrert", "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, @@ -28,10 +30,11 @@ }, "user": { "data": { + "auth_code": "Autorisasjonskode", "password": "Passord", "username": "Brukernavn" }, - "description": "Skriv inn brukernavn og passord." + "description": "SimpliSafe autentiserer brukere via sin nettapp. P\u00e5 grunn av tekniske begrensninger er det et manuelt trinn p\u00e5 slutten av denne prosessen; s\u00f8rg for at du leser [dokumentasjonen](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) f\u00f8r du starter. \n\n N\u00e5r du er klar, klikk [her]( {url} ) for \u00e5 \u00e5pne SimpliSafe-nettappen og angi legitimasjonen din. N\u00e5r prosessen er fullf\u00f8rt, g\u00e5 tilbake hit og skriv inn autorisasjonskoden fra SimpliSafe-nettappens URL." } } }, diff --git a/homeassistant/components/smartthings/translations/hu.json b/homeassistant/components/smartthings/translations/hu.json index 47698ef528c..6487e12962d 100644 --- a/homeassistant/components/smartthings/translations/hu.json +++ b/homeassistant/components/smartthings/translations/hu.json @@ -30,7 +30,7 @@ "title": "Hely kiv\u00e1laszt\u00e1sa" }, "user": { - "description": "K\u00e9rem adja meg a SmartThings [Personal Access Tokent]({token_url}), amit az [instrukci\u00f3k]({component_url}) alapj\u00e1n hozott l\u00e9tre.", + "description": "A SmartThings \u00fagy lesz be\u00e1ll\u00edtva, hogy push friss\u00edt\u00e9seket k\u00fcldj\u00f6n a Home Assistantnak a k\u00f6vetkez\u0151 c\u00edmen:\n> {webhook_url}\n\nHa ez \u00edgy nem megfelel\u0151, friss\u00edtse a konfigur\u00e1ci\u00f3t, ind\u00edtsa \u00fajra Home Assistantot, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", "title": "Callback URL meger\u0151s\u00edt\u00e9se" } } diff --git a/homeassistant/components/soundtouch/translations/no.json b/homeassistant/components/soundtouch/translations/no.json index bdaee03da54..3761bc55ad7 100644 --- a/homeassistant/components/soundtouch/translations/no.json +++ b/homeassistant/components/soundtouch/translations/no.json @@ -17,5 +17,11 @@ "title": "Bekreft \u00e5 legge til Bose SoundTouch-enhet" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Bose SoundTouch med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Bose SoundTouch YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Bose SoundTouch YAML-konfigurasjonen fjernes" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/no.json b/homeassistant/components/spotify/translations/no.json index 54e3ca1f8b4..d8172e1a9bd 100644 --- a/homeassistant/components/spotify/translations/no.json +++ b/homeassistant/components/spotify/translations/no.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Konfigurering av Spotify med YAML er fjernet. \n\n Din eksisterende YAML-konfigurasjon brukes ikke av Home Assistant. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Spotify YAML-konfigurasjonen er fjernet" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Spotify API-endepunkt n\u00e5s" diff --git a/homeassistant/components/steam_online/translations/no.json b/homeassistant/components/steam_online/translations/no.json index 1b30669fad4..08defe9e2be 100644 --- a/homeassistant/components/steam_online/translations/no.json +++ b/homeassistant/components/steam_online/translations/no.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Konfigurering av Steam med YAML er fjernet. \n\n Din eksisterende YAML-konfigurasjon brukes ikke av Home Assistant. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Steam YAML-konfigurasjonen er fjernet" + } + }, "options": { "error": { "unauthorized": "Begrenset venneliste: Se dokumentasjonen for hvordan du ser alle andre venner" diff --git a/homeassistant/components/switchbot/translations/no.json b/homeassistant/components/switchbot/translations/no.json index 1d7836f6776..6c8d877551f 100644 --- a/homeassistant/components/switchbot/translations/no.json +++ b/homeassistant/components/switchbot/translations/no.json @@ -7,10 +7,11 @@ "switchbot_unsupported_type": "Switchbot-type st\u00f8ttes ikke.", "unknown": "Uventet feil" }, - "flow_title": "{name}", + "flow_title": "{name} ( {address} )", "step": { "user": { "data": { + "address": "Enhetsadresse", "mac": "Enhetens MAC -adresse", "name": "Navn", "password": "Passord" diff --git a/homeassistant/components/unifiprotect/translations/hu.json b/homeassistant/components/unifiprotect/translations/hu.json index cbef293c7e9..645ec1f8c90 100644 --- a/homeassistant/components/unifiprotect/translations/hu.json +++ b/homeassistant/components/unifiprotect/translations/hu.json @@ -16,7 +16,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({ipaddress})? A bejelentkez\u00e9shez egy helyi felhaszn\u00e1l\u00f3ra lesz sz\u00fcks\u00e9g, amelyet az UniFi OS Console-ban hoztak l\u00e9tre. Az Ubiquiti Cloud Users nem fog m\u0171k\u00f6dni. Tov\u00e1bbi inform\u00e1ci\u00f3: {local_user_documentation_url}", + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({ip_address})? A bejelentkez\u00e9shez egy helyi felhaszn\u00e1l\u00f3ra lesz sz\u00fcks\u00e9g, amelyet az UniFi OS Console-ban hoztak l\u00e9tre. Az Ubiquiti Cloud Users nem fog m\u0171k\u00f6dni. Tov\u00e1bbi inform\u00e1ci\u00f3: {local_user_documentation_url}", "title": "UniFi Protect felfedezve" }, "reauth_confirm": { diff --git a/homeassistant/components/uscis/translations/no.json b/homeassistant/components/uscis/translations/no.json new file mode 100644 index 00000000000..a4db40941ed --- /dev/null +++ b/homeassistant/components/uscis/translations/no.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integrasjonen med US Citizenship and Immigration Services (USCIS) venter p\u00e5 fjerning fra Home Assistant og vil ikke lenger v\u00e6re tilgjengelig fra og med Home Assistant 2022.10. \n\n Integrasjonen blir fjernet, fordi den er avhengig av webscraping, noe som ikke er tillatt. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "USCIS-integrasjonen fjernes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/translations/no.json b/homeassistant/components/verisure/translations/no.json index 0bd5529c51d..195f8aadd3d 100644 --- a/homeassistant/components/verisure/translations/no.json +++ b/homeassistant/components/verisure/translations/no.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Ugyldig godkjenning", - "unknown": "Uventet feil" + "unknown": "Uventet feil", + "unknown_mfa": "Ukjent feil oppstod under MFA-oppsett" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant fant flere Verisure-installasjoner i Mine sider-kontoen din. Velg installasjonen du vil legge til i Home Assistant." }, + "mfa": { + "data": { + "code": "Bekreftelseskode", + "description": "Kontoen din har 2-trinns bekreftelse aktivert. Vennligst skriv inn bekreftelseskoden Verisure sender til deg." + } + }, "reauth_confirm": { "data": { "description": "Autentiser p\u00e5 nytt med Verisure Mine sider-kontoen din.", @@ -22,6 +29,12 @@ "password": "Passord" } }, + "reauth_mfa": { + "data": { + "code": "Bekreftelseskode", + "description": "Kontoen din har 2-trinns bekreftelse aktivert. Vennligst skriv inn bekreftelseskoden Verisure sender til deg." + } + }, "user": { "data": { "description": "Logg p\u00e5 med Verisure Mine sider-kontoen din.", diff --git a/homeassistant/components/withings/translations/no.json b/homeassistant/components/withings/translations/no.json index 40ba24111bf..488a43592a5 100644 --- a/homeassistant/components/withings/translations/no.json +++ b/homeassistant/components/withings/translations/no.json @@ -29,6 +29,7 @@ "title": "Godkjenne integrering p\u00e5 nytt" }, "reauth_confirm": { + "description": "Profilen {profile} m\u00e5 godkjennes p\u00e5 nytt for \u00e5 kunne fortsette \u00e5 motta Withings-data.", "title": "Re-autentiser integrasjon" } } diff --git a/homeassistant/components/xbox/translations/no.json b/homeassistant/components/xbox/translations/no.json index 4736fc91bf0..130905fc3d7 100644 --- a/homeassistant/components/xbox/translations/no.json +++ b/homeassistant/components/xbox/translations/no.json @@ -13,5 +13,11 @@ "title": "Velg godkjenningsmetode" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Xbox i configuration.yaml blir fjernet i Home Assistant 2022.9. \n\n Din eksisterende OAuth-applikasjonslegitimasjon og tilgangsinnstillinger er automatisk importert til brukergrensesnittet. Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Xbox YAML-konfigurasjonen blir fjernet" + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/ca.json b/homeassistant/components/xiaomi_ble/translations/ca.json index 1c24ab7172e..873bcb3f3bb 100644 --- a/homeassistant/components/xiaomi_ble/translations/ca.json +++ b/homeassistant/components/xiaomi_ble/translations/ca.json @@ -6,7 +6,8 @@ "decryption_failed": "La clau d'enlla\u00e7 proporcionada no ha funcionat, les dades del sensor no s'han pogut desxifrar. Comprova-la i torna-ho a provar.", "expected_24_characters": "S'espera una clau d'enlla\u00e7 de 24 car\u00e0cters hexadecimals.", "expected_32_characters": "S'espera una clau d'enlla\u00e7 de 32 car\u00e0cters hexadecimals.", - "no_devices_found": "No s'han trobat dispositius a la xarxa" + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/xiaomi_ble/translations/de.json b/homeassistant/components/xiaomi_ble/translations/de.json index 0d3e2c542d2..6fc2c74af75 100644 --- a/homeassistant/components/xiaomi_ble/translations/de.json +++ b/homeassistant/components/xiaomi_ble/translations/de.json @@ -6,7 +6,8 @@ "decryption_failed": "Der bereitgestellte Bindkey funktionierte nicht, Sensordaten konnten nicht entschl\u00fcsselt werden. Bitte \u00fcberpr\u00fcfe es und versuche es erneut.", "expected_24_characters": "Erwartet wird ein 24-stelliger hexadezimaler Bindkey.", "expected_32_characters": "Erwartet wird ein 32-stelliger hexadezimaler Bindkey.", - "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "flow_title": "{name}", "step": { @@ -25,6 +26,9 @@ }, "description": "Die vom Sensor \u00fcbertragenen Sensordaten sind verschl\u00fcsselt. Um sie zu entschl\u00fcsseln, ben\u00f6tigen wir einen 24-stelligen hexadezimalen Bindungsschl\u00fcssel." }, + "slow_confirm": { + "description": "Von diesem Ger\u00e4t wurde in der letzten Minute kein Broadcast gesendet, so dass wir nicht sicher sind, ob dieses Ger\u00e4t Verschl\u00fcsselung verwendet oder nicht. Dies kann daran liegen, dass das Ger\u00e4t ein langsames Sendeintervall verwendet. Best\u00e4tige, dass du das Ger\u00e4t trotzdem hinzuf\u00fcgen m\u00f6chtest. Wenn das n\u00e4chste Mal ein Broadcast empfangen wird, wirst du aufgefordert, den Bindkey einzugeben, falls er ben\u00f6tigt wird." + }, "user": { "data": { "address": "Ger\u00e4t" diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json index 2cb77dd2c07..be75cc007b2 100644 --- a/homeassistant/components/xiaomi_ble/translations/en.json +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", + "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", + "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", + "expected_32_characters": "Expected a 32 character hexadecimal bindkey.", "no_devices_found": "No devices found on the network", "reauth_successful": "Re-authentication was successful" }, @@ -31,6 +34,9 @@ }, "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey." }, + "slow_confirm": { + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." + }, "user": { "data": { "address": "Device" diff --git a/homeassistant/components/xiaomi_ble/translations/fr.json b/homeassistant/components/xiaomi_ble/translations/fr.json index c8a1af034cf..4c9b9b980ed 100644 --- a/homeassistant/components/xiaomi_ble/translations/fr.json +++ b/homeassistant/components/xiaomi_ble/translations/fr.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", - "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau" + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/xiaomi_ble/translations/hu.json b/homeassistant/components/xiaomi_ble/translations/hu.json index b03a41f60a6..3962e890d4f 100644 --- a/homeassistant/components/xiaomi_ble/translations/hu.json +++ b/homeassistant/components/xiaomi_ble/translations/hu.json @@ -6,7 +6,8 @@ "decryption_failed": "A megadott kulcs nem m\u0171k\u00f6d\u00f6tt, az \u00e9rz\u00e9kel\u0151adatokat nem lehetett kiolvasni. K\u00e9rj\u00fck, ellen\u0151rizze \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", "expected_24_characters": "24 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g.", "expected_32_characters": "32 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g.", - "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton" + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "flow_title": "{name}", "step": { @@ -25,6 +26,9 @@ }, "description": "Az \u00e9rz\u00e9kel\u0151 adatai titkos\u00edtva vannak. A visszafejt\u00e9shez egy 24 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g." }, + "slow_confirm": { + "description": "Az elm\u00falt egy percben nem \u00e9rkezett ad\u00e1sjel az eszk\u00f6zt\u0151l, \u00edgy nem az nem \u00e1llap\u00edthat\u00f3 meg egy\u00e9rtelm\u0171en, hogy ez a k\u00e9sz\u00fcl\u00e9k haszn\u00e1l-e titkos\u00edt\u00e1st vagy sem. Ez az\u00e9rt lehet, mert az eszk\u00f6z ritka jelad\u00e1si intervallumot haszn\u00e1l. Meger\u0151s\u00edtheti most az eszk\u00f6z hozz\u00e1ad\u00e1s\u00e1t, de a k\u00f6vetkez\u0151 ad\u00e1sjel fogad\u00e1sakor a rendszer k\u00e9rni fogja, hogy adja meg az eszk\u00f6z kulcs\u00e1t (bindkeyt), ha az sz\u00fcks\u00e9ges." + }, "user": { "data": { "address": "Eszk\u00f6z" diff --git a/homeassistant/components/xiaomi_ble/translations/id.json b/homeassistant/components/xiaomi_ble/translations/id.json index ea45a7ba9c3..5d01dcab709 100644 --- a/homeassistant/components/xiaomi_ble/translations/id.json +++ b/homeassistant/components/xiaomi_ble/translations/id.json @@ -6,7 +6,8 @@ "decryption_failed": "Bindkey yang disediakan tidak berfungsi, data sensor tidak dapat didekripsi. Silakan periksa dan coba lagi.", "expected_24_characters": "Diharapkan bindkey berupa 24 karakter heksadesimal.", "expected_32_characters": "Diharapkan bindkey berupa 32 karakter heksadesimal.", - "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan" + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "reauth_successful": "Autentikasi ulang berhasil" }, "flow_title": "{name}", "step": { @@ -25,6 +26,9 @@ }, "description": "Data sensor yang disiarkan oleh sensor telah dienkripsi. Untuk mendekripsinya, diperlukan 24 karakter bindkey heksadesimal ." }, + "slow_confirm": { + "description": "Belum ada siaran dari perangkat ini dalam menit terakhir jadi kami tidak yakin apakah perangkat ini menggunakan enkripsi atau tidak. Ini mungkin terjadi karena perangkat menggunakan interval siaran yang lambat. Konfirmasikan sekarang untuk menambahkan perangkat ini, dan ketika siaran diterima nanti, Anda akan diminta untuk memasukkan kunci bind jika diperlukan." + }, "user": { "data": { "address": "Perangkat" diff --git a/homeassistant/components/xiaomi_ble/translations/no.json b/homeassistant/components/xiaomi_ble/translations/no.json new file mode 100644 index 00000000000..6c63f53c6aa --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/no.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "decryption_failed": "Den oppgitte bindingsn\u00f8kkelen fungerte ikke, sensordata kunne ikke dekrypteres. Vennligst sjekk det og pr\u00f8v igjen.", + "expected_24_characters": "Forventet en heksadesimal bindingsn\u00f8kkel p\u00e5 24 tegn.", + "expected_32_characters": "Forventet en heksadesimal bindingsn\u00f8kkel p\u00e5 32 tegn.", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Sensordataene som sendes av sensoren er kryptert. For \u00e5 dekryptere den trenger vi en heksadesimal bindn\u00f8kkel p\u00e5 32 tegn." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Sensordataene som sendes av sensoren er kryptert. For \u00e5 dekryptere den trenger vi en heksadesimal bindn\u00f8kkel p\u00e5 24 tegn." + }, + "slow_confirm": { + "description": "Det har ikke v\u00e6rt en sending fra denne enheten det siste minuttet, s\u00e5 vi er ikke sikre p\u00e5 om denne enheten bruker kryptering eller ikke. Dette kan skyldes at enheten bruker et tregt kringkastingsintervall. Bekreft \u00e5 legge til denne enheten uansett, s\u00e5 neste gang en kringkasting mottas, vil du bli bedt om \u00e5 angi bindn\u00f8kkelen hvis den er n\u00f8dvendig." + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/pt-BR.json b/homeassistant/components/xiaomi_ble/translations/pt-BR.json index 21c251bf0eb..b0776a217cd 100644 --- a/homeassistant/components/xiaomi_ble/translations/pt-BR.json +++ b/homeassistant/components/xiaomi_ble/translations/pt-BR.json @@ -6,7 +6,8 @@ "decryption_failed": "A bindkey fornecida n\u00e3o funcionou, os dados do sensor n\u00e3o puderam ser descriptografados. Por favor verifique e tente novamente.", "expected_24_characters": "Espera-se uma bindkey hexadecimal de 24 caracteres.", "expected_32_characters": "Esperado um bindkey hexadecimal de 32 caracteres.", - "no_devices_found": "Nenhum dispositivo encontrado na rede" + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "flow_title": "{name}", "step": { @@ -25,6 +26,9 @@ }, "description": "Os dados do sensor transmitidos pelo sensor s\u00e3o criptografados. Para decifr\u00e1-lo, precisamos de uma bindkey hexadecimal de 24 caracteres." }, + "slow_confirm": { + "description": "N\u00e3o houve uma transmiss\u00e3o deste dispositivo no \u00faltimo minuto, por isso n\u00e3o temos certeza se este dispositivo usa criptografia ou n\u00e3o. Isso pode ocorrer porque o dispositivo usa um intervalo de transmiss\u00e3o lento. Confirme para adicionar este dispositivo de qualquer maneira e, na pr\u00f3xima vez que uma transmiss\u00e3o for recebida, voc\u00ea ser\u00e1 solicitado a inserir sua bindkey, se necess\u00e1rio." + }, "user": { "data": { "address": "Dispositivo" diff --git a/homeassistant/components/xiaomi_ble/translations/zh-Hant.json b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json index 81f7e2050af..13d0a986b79 100644 --- a/homeassistant/components/xiaomi_ble/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json @@ -6,7 +6,8 @@ "decryption_failed": "\u6240\u63d0\u4f9b\u7684\u7d81\u5b9a\u78bc\u7121\u6cd5\u4f7f\u7528\u3001\u50b3\u611f\u5668\u8cc7\u6599\u7121\u6cd5\u89e3\u5bc6\u3002\u8acb\u4fee\u6b63\u5f8c\u3001\u518d\u8a66\u4e00\u6b21\u3002", "expected_24_characters": "\u9700\u8981 24 \u500b\u5b57\u5143\u4e4b\u5341\u516d\u9032\u4f4d\u7d81\u5b9a\u78bc\u3002", "expected_32_characters": "\u9700\u8981 32 \u500b\u5b57\u5143\u4e4b\u5341\u516d\u9032\u4f4d\u7d81\u5b9a\u78bc\u3002", - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "flow_title": "{name}", "step": { @@ -25,6 +26,9 @@ }, "description": "\u7531\u50b3\u611f\u5668\u6240\u5ee3\u64ad\u4e4b\u8cc7\u6599\u70ba\u52a0\u5bc6\u8cc7\u6599\u3002\u82e5\u8981\u89e3\u78bc\u3001\u9700\u8981 24 \u500b\u5b57\u5143\u4e4b\u7d81\u5b9a\u78bc\u3002" }, + "slow_confirm": { + "description": "\u8a72\u88dd\u7f6e\u65bc\u904e\u53bb\u4e00\u5206\u9418\u5167\u3001\u672a\u9032\u884c\u4efb\u4f55\u72c0\u614b\u5ee3\u64ad\uff0c\u56e0\u6b64\u7121\u6cd5\u78ba\u5b9a\u88dd\u7f6e\u662f\u5426\u4f7f\u7528\u52a0\u5bc6\u901a\u8a0a\u3002\u4e5f\u53ef\u80fd\u56e0\u70ba\u88dd\u7f6e\u7684\u66f4\u65b0\u983b\u7387\u8f03\u6162\u3002\u78ba\u8a8d\u9084\u662f\u8981\u65b0\u589e\u6b64\u88dd\u7f6e\u3001\u65bc\u4e0b\u6b21\u6536\u5230\u88dd\u7f6e\u5ee3\u64ad\u6642\uff0c\u5982\u679c\u9700\u8981\u3001\u5c07\u63d0\u793a\u60a8\u8f38\u5165\u7d81\u5b9a\u78bc\u3002" + }, "user": { "data": { "address": "\u88dd\u7f6e" diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json index 4e719e63ae1..47ae51b89f0 100644 --- a/homeassistant/components/zha/translations/no.json +++ b/homeassistant/components/zha/translations/no.json @@ -46,10 +46,13 @@ "title": "Alternativer for alarmkontrollpanel" }, "zha_options": { + "always_prefer_xy_color_mode": "Foretrekker alltid XY-fargemodus", "consider_unavailable_battery": "Vurder batteridrevne enheter som utilgjengelige etter (sekunder)", "consider_unavailable_mains": "Tenk p\u00e5 str\u00f8mnettet som ikke er tilgjengelig etter (sekunder)", "default_light_transition": "Standard lysovergangstid (sekunder)", "enable_identify_on_join": "Aktiver identifiseringseffekt n\u00e5r enheter blir med i nettverket", + "enhanced_light_transition": "Aktiver forbedret lysfarge/temperaturovergang fra en off-tilstand", + "light_transitioning_flag": "Aktiver skyveknappen for forbedret lysstyrke under lysovergang", "title": "Globale alternativer" } }, From 0dbb119677c15d0a430bc6683ebd377a790e916a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 20:34:46 -1000 Subject: [PATCH 3092/3516] Bump pySwitchbot to 0.17.3 to fix hang at startup (#76103) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 41d0d7efda6..f01eae4a938 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.17.1"], + "requirements": ["PySwitchbot==0.17.3"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 253dee5445a..511ec048104 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.17.1 +PySwitchbot==0.17.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a8adbdf846..00abfb8c115 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.17.1 +PySwitchbot==0.17.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 6006fc7e30dbeb974de8684ddb17f20ef7aa9772 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 20:35:41 -1000 Subject: [PATCH 3093/3516] Bump aiohomekit to 1.2.3 to fix hang at startup (#76102) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 2de2a915d41..ac1be576906 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.2"], + "requirements": ["aiohomekit==1.2.3"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 511ec048104..db7a13b7357 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.2 +aiohomekit==1.2.3 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 00abfb8c115..0fb93106711 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.2 +aiohomekit==1.2.3 # homeassistant.components.emulated_hue # homeassistant.components.http From 213812f087f78d3997526097cf8e9a73e5822b1a Mon Sep 17 00:00:00 2001 From: Eloston Date: Tue, 2 Aug 2022 02:29:44 +0000 Subject: [PATCH 3094/3516] Add support for SwitchBot Plug Mini (#76056) --- CODEOWNERS | 4 ++-- homeassistant/components/switchbot/__init__.py | 3 +++ homeassistant/components/switchbot/const.py | 2 ++ homeassistant/components/switchbot/manifest.json | 10 ++++++++-- homeassistant/components/switchbot/sensor.py | 9 ++++++++- homeassistant/components/switchbot/switch.py | 6 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 28 insertions(+), 10 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index bd39fd68590..5853186d0bb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1044,8 +1044,8 @@ build.json @home-assistant/supervisor /tests/components/switch/ @home-assistant/core /homeassistant/components/switch_as_x/ @home-assistant/core /tests/components/switch_as_x/ @home-assistant/core -/homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas -/tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas +/homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston +/tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston /homeassistant/components/switcher_kis/ @tomerfi @thecode /tests/components/switcher_kis/ @tomerfi @thecode /homeassistant/components/switchmate/ @danielhiversen @qiz-li diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 42ca0856b02..e32252a7615 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -22,6 +22,7 @@ from .const import ( ATTR_CONTACT, ATTR_CURTAIN, ATTR_HYGROMETER, + ATTR_PLUG, CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, @@ -30,6 +31,7 @@ from .coordinator import SwitchbotDataUpdateCoordinator PLATFORMS_BY_TYPE = { ATTR_BOT: [Platform.SWITCH, Platform.SENSOR], + ATTR_PLUG: [Platform.SWITCH, Platform.SENSOR], ATTR_CURTAIN: [Platform.COVER, Platform.BINARY_SENSOR, Platform.SENSOR], ATTR_HYGROMETER: [Platform.SENSOR], ATTR_CONTACT: [Platform.BINARY_SENSOR, Platform.SENSOR], @@ -37,6 +39,7 @@ PLATFORMS_BY_TYPE = { CLASS_BY_DEVICE = { ATTR_CURTAIN: switchbot.SwitchbotCurtain, ATTR_BOT: switchbot.Switchbot, + ATTR_PLUG: switchbot.SwitchbotPlugMini, } _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index dc5abc139e6..9cc2acebbf8 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -7,12 +7,14 @@ ATTR_BOT = "bot" ATTR_CURTAIN = "curtain" ATTR_HYGROMETER = "hygrometer" ATTR_CONTACT = "contact" +ATTR_PLUG = "plug" DEFAULT_NAME = "Switchbot" SUPPORTED_MODEL_TYPES = { "WoHand": ATTR_BOT, "WoCurtain": ATTR_CURTAIN, "WoSensorTH": ATTR_HYGROMETER, "WoContact": ATTR_CONTACT, + "WoPlug": ATTR_PLUG, } # Config Defaults diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index dcb33c03882..41d0d7efda6 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,10 +2,16 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.16.0"], + "requirements": ["PySwitchbot==0.17.1"], "config_flow": true, "dependencies": ["bluetooth"], - "codeowners": ["@bdraco", "@danielhiversen", "@RenierM26", "@murtas"], + "codeowners": [ + "@bdraco", + "@danielhiversen", + "@RenierM26", + "@murtas", + "@Eloston" + ], "bluetooth": [ { "service_data_uuid": "0000fd3d-0000-1000-8000-00805f9b34fb" diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index f796ea05e7b..fb24ae22679 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -33,6 +33,13 @@ SENSOR_TYPES: dict[str, SensorEntityDescription] = { entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ), + "wifi_rssi": SensorEntityDescription( + key="wifi_rssi", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), "battery": SensorEntityDescription( key="battery", native_unit_of_measurement=PERCENTAGE, @@ -98,7 +105,7 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity): super().__init__(coordinator, unique_id, address, name=switchbot_name) self._sensor = sensor self._attr_unique_id = f"{unique_id}-{sensor}" - self._attr_name = f"{switchbot_name} {sensor.title()}" + self._attr_name = f"{switchbot_name} {sensor.replace('_', ' ').title()}" self.entity_description = SENSOR_TYPES[sensor] @property diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index e6ba77fa164..65c7588acbd 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -33,7 +33,7 @@ async def async_setup_entry( assert unique_id is not None async_add_entities( [ - SwitchBotBotEntity( + SwitchBotSwitch( coordinator, unique_id, entry.data[CONF_ADDRESS], @@ -44,8 +44,8 @@ async def async_setup_entry( ) -class SwitchBotBotEntity(SwitchbotEntity, SwitchEntity, RestoreEntity): - """Representation of a Switchbot.""" +class SwitchBotSwitch(SwitchbotEntity, SwitchEntity, RestoreEntity): + """Representation of a Switchbot switch.""" _attr_device_class = SwitchDeviceClass.SWITCH diff --git a/requirements_all.txt b/requirements_all.txt index 71aee1700ab..04fc820bd9f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.16.0 +PySwitchbot==0.17.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 61a4b29b3b7..077121e7b22 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.16.0 +PySwitchbot==0.17.1 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From c22cb13bd0800e9abcf023b178ea2bf336661b3b Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 2 Aug 2022 17:20:37 +0100 Subject: [PATCH 3095/3516] Add optional context parameter to async_start_reauth (#76077) --- homeassistant/components/xiaomi_ble/sensor.py | 20 +------------- homeassistant/config_entries.py | 7 ++++- tests/test_config_entries.py | 27 +++++++++++++++++++ 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index b3cd5126967..fef9b334e75 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -181,25 +181,7 @@ def process_service_info( and data.encryption_scheme != EncryptionScheme.NONE and not data.bindkey_verified ): - flow_context = { - "source": config_entries.SOURCE_REAUTH, - "entry_id": entry.entry_id, - "title_placeholders": {"name": entry.title}, - "unique_id": entry.unique_id, - "device": data, - } - - for flow in hass.config_entries.flow.async_progress_by_handler(DOMAIN): - if flow["context"] == flow_context: - break - else: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context=flow_context, - data=entry.data, - ) - ) + entry.async_start_reauth(hass, context={"device": data}) return sensor_update_to_bluetooth_data_update(update) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b0c04323005..638aa0b8110 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -640,7 +640,9 @@ class ConfigEntry: await asyncio.gather(*pending) @callback - def async_start_reauth(self, hass: HomeAssistant) -> None: + def async_start_reauth( + self, hass: HomeAssistant, context: dict[str, Any] | None = None + ) -> None: """Start a reauth flow.""" flow_context = { "source": SOURCE_REAUTH, @@ -649,6 +651,9 @@ class ConfigEntry: "unique_id": self.unique_id, } + if context: + flow_context.update(context) + for flow in hass.config_entries.flow.async_progress_by_handler(self.domain): if flow["context"] == flow_context: return diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 3e7245ed73a..66f51508441 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3273,3 +3273,30 @@ async def test_disallow_entry_reload_with_setup_in_progresss(hass, manager): with pytest.raises(config_entries.OperationNotAllowed): assert await manager.async_reload(entry.entry_id) assert entry.state is config_entries.ConfigEntryState.SETUP_IN_PROGRESS + + +async def test_reauth(hass): + """Test the async_reauth_helper.""" + entry = MockConfigEntry(title="test_title", domain="test") + + mock_setup_entry = AsyncMock(return_value=True) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + + entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["entry_id"] == entry.entry_id + assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH + assert flows[0]["context"]["title_placeholders"] == {"name": "test_title"} + assert flows[0]["context"]["extra_context"] == "some_extra_context" + + # Check we can't start duplicate flows + entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) + await hass.async_block_till_done() + assert len(flows) == 1 From 690f051a87c11d63b9cdc06366f3cd6f8c17cda3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 09:11:50 -1000 Subject: [PATCH 3096/3516] Bump pyatv to 0.10.3 (#76091) --- homeassistant/components/apple_tv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index dec195fddee..5717f851b81 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -3,7 +3,7 @@ "name": "Apple TV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", - "requirements": ["pyatv==0.10.2"], + "requirements": ["pyatv==0.10.3"], "dependencies": ["zeroconf"], "zeroconf": [ "_mediaremotetv._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 04fc820bd9f..87ad8ffc67e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1392,7 +1392,7 @@ pyatmo==6.2.4 pyatome==0.1.1 # homeassistant.components.apple_tv -pyatv==0.10.2 +pyatv==0.10.3 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 077121e7b22..058971731c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -968,7 +968,7 @@ pyatag==0.3.5.3 pyatmo==6.2.4 # homeassistant.components.apple_tv -pyatv==0.10.2 +pyatv==0.10.3 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 From a78da6a000a0f37f3921f1107b367bbf605d9300 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 2 Aug 2022 21:38:38 +0100 Subject: [PATCH 3097/3516] Fix serialization of Xiaomi BLE reauth flow (#76095) * Use data instead of context to fix serialisation bug * Test change to async_start_reauth --- homeassistant/components/xiaomi_ble/config_flow.py | 2 +- homeassistant/components/xiaomi_ble/sensor.py | 2 +- homeassistant/config_entries.py | 7 +++++-- tests/components/xiaomi_ble/test_config_flow.py | 3 +-- tests/test_config_entries.py | 12 ++++++++++-- 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index 092c60e9713..725c513914f 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -260,7 +260,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) assert entry is not None - device: DeviceData = self.context["device"] + device: DeviceData = entry_data["device"] self._discovered_device = device self._discovery_info = device.last_service_info diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index fef9b334e75..dcb95422609 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -181,7 +181,7 @@ def process_service_info( and data.encryption_scheme != EncryptionScheme.NONE and not data.bindkey_verified ): - entry.async_start_reauth(hass, context={"device": data}) + entry.async_start_reauth(hass, data={"device": data}) return sensor_update_to_bluetooth_data_update(update) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 638aa0b8110..7c2f1c84ff9 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -641,7 +641,10 @@ class ConfigEntry: @callback def async_start_reauth( - self, hass: HomeAssistant, context: dict[str, Any] | None = None + self, + hass: HomeAssistant, + context: dict[str, Any] | None = None, + data: dict[str, Any] | None = None, ) -> None: """Start a reauth flow.""" flow_context = { @@ -662,7 +665,7 @@ class ConfigEntry: hass.config_entries.flow.async_init( self.domain, context=flow_context, - data=self.data, + data=self.data | (data or {}), ) ) diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index 0d123f0cd54..32ba6be3322 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -1033,9 +1033,8 @@ async def test_async_step_reauth_abort_early(hass): "entry_id": entry.entry_id, "title_placeholders": {"name": entry.title}, "unique_id": entry.unique_id, - "device": device, }, - data=entry.data, + data=entry.data | {"device": device}, ) assert result["type"] == FlowResultType.ABORT diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 66f51508441..b923e37b636 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -3286,8 +3286,14 @@ async def test_reauth(hass): await entry.async_setup(hass) await hass.async_block_till_done() - entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) - await hass.async_block_till_done() + flow = hass.config_entries.flow + with patch.object(flow, "async_init", wraps=flow.async_init) as mock_init: + entry.async_start_reauth( + hass, + context={"extra_context": "some_extra_context"}, + data={"extra_data": 1234}, + ) + await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -3296,6 +3302,8 @@ async def test_reauth(hass): assert flows[0]["context"]["title_placeholders"] == {"name": "test_title"} assert flows[0]["context"]["extra_context"] == "some_extra_context" + assert mock_init.call_args.kwargs["data"]["extra_data"] == 1234 + # Check we can't start duplicate flows entry.async_start_reauth(hass, {"extra_context": "some_extra_context"}) await hass.async_block_till_done() From d7a418a219b1732c43c60165fcecc5b93a35247a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 1 Aug 2022 17:54:06 +0200 Subject: [PATCH 3098/3516] Guard imports for type hinting in Bluetooth (#75984) --- homeassistant/components/bluetooth/__init__.py | 12 ++++++++---- homeassistant/components/bluetooth/config_flow.py | 6 ++++-- homeassistant/components/bluetooth/match.py | 12 ++++++++---- homeassistant/components/bluetooth/models.py | 7 +++++-- .../bluetooth/passive_update_coordinator.py | 11 +++++++---- .../components/bluetooth/passive_update_processor.py | 12 ++++++++---- 6 files changed, 40 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index ba079a426fe..39629ab6d85 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -8,12 +8,10 @@ from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum import logging -from typing import Final +from typing import TYPE_CHECKING, Final import async_timeout from bleak import BleakError -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -27,7 +25,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo -from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_bluetooth from . import models @@ -42,6 +39,13 @@ from .models import HaBleakScanner, HaBleakScannerWrapper from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher from .util import async_get_bluetooth_adapters +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + from bleak.backends.scanner import AdvertisementData + + from homeassistant.helpers.typing import ConfigType + + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index bbba5f411b2..1a0be8706bf 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -1,18 +1,20 @@ """Config flow to configure the Bluetooth integration.""" from __future__ import annotations -from typing import Any +from typing import TYPE_CHECKING, Any import voluptuous as vol from homeassistant.components import onboarding from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from .const import CONF_ADAPTER, DEFAULT_NAME, DOMAIN from .util import async_get_bluetooth_adapters +if TYPE_CHECKING: + from homeassistant.data_entry_flow import FlowResult + class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): """Config flow for Bluetooth.""" diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 000f39eefd4..2cd4f62ae5e 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -1,17 +1,21 @@ """The bluetooth integration matchers.""" from __future__ import annotations -from collections.abc import Mapping from dataclasses import dataclass import fnmatch -from typing import Final, TypedDict +from typing import TYPE_CHECKING, Final, TypedDict -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData from lru import LRU # pylint: disable=no-name-in-module from homeassistant.loader import BluetoothMatcher, BluetoothMatcherOptional +if TYPE_CHECKING: + from collections.abc import Mapping + + from bleak.backends.device import BLEDevice + from bleak.backends.scanner import AdvertisementData + + MAX_REMEMBER_ADDRESSES: Final = 2048 diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 6f814c7b66b..51704a2f530 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -4,10 +4,9 @@ from __future__ import annotations import asyncio import contextlib import logging -from typing import Any, Final +from typing import TYPE_CHECKING, Any, Final from bleak import BleakScanner -from bleak.backends.device import BLEDevice from bleak.backends.scanner import ( AdvertisementData, AdvertisementDataCallback, @@ -16,6 +15,10 @@ from bleak.backends.scanner import ( from homeassistant.core import CALLBACK_TYPE, callback as hass_callback +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + + _LOGGER = logging.getLogger(__name__) FILTER_UUIDS: Final = "UUIDs" diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 31a6b065830..5c6b5b79509 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -1,16 +1,19 @@ """Passive update coordinator for the Bluetooth integration.""" from __future__ import annotations -from collections.abc import Callable, Generator -import logging -from typing import Any +from typing import TYPE_CHECKING, Any from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .update_coordinator import BasePassiveBluetoothCoordinator +if TYPE_CHECKING: + from collections.abc import Callable, Generator + import logging + + from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak + class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): """Class to manage passive bluetooth advertisements. diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 1f2047c02cb..78966d9b7ab 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -1,20 +1,24 @@ """Passive update processors for the Bluetooth integration.""" from __future__ import annotations -from collections.abc import Callable, Mapping import dataclasses import logging -from typing import Any, Generic, TypeVar +from typing import TYPE_CHECKING, Any, Generic, TypeVar from homeassistant.const import ATTR_IDENTIFIERS, ATTR_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .const import DOMAIN from .update_coordinator import BasePassiveBluetoothCoordinator +if TYPE_CHECKING: + from collections.abc import Callable, Mapping + + from homeassistant.helpers.entity_platform import AddEntitiesCallback + + from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak + @dataclasses.dataclass(frozen=True) class PassiveBluetoothEntityKey: From d2dc83c4c7fc9b7375a7f7adc72d9ccab7347f62 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 13:46:43 -1000 Subject: [PATCH 3099/3516] Handle additional bluetooth start exceptions (#76096) --- .../components/bluetooth/__init__.py | 33 +++++- tests/components/bluetooth/test_init.py | 110 ++++++++++++++++++ 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 39629ab6d85..c91563d7729 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Final import async_timeout from bleak import BleakError +from dbus_next import InvalidMessageError from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP @@ -26,6 +27,7 @@ from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.loader import async_get_bluetooth +from homeassistant.util.package import is_docker_env from . import models from .const import CONF_ADAPTER, DEFAULT_ADAPTERS, DOMAIN @@ -341,13 +343,42 @@ class BluetoothManager: try: async with async_timeout.timeout(START_TIMEOUT): await self.scanner.start() # type: ignore[no-untyped-call] + except InvalidMessageError as ex: + self._cancel_device_detected() + _LOGGER.debug("Invalid DBus message received: %s", ex, exc_info=True) + raise ConfigEntryNotReady( + f"Invalid DBus message received: {ex}; try restarting `dbus`" + ) from ex + except BrokenPipeError as ex: + self._cancel_device_detected() + _LOGGER.debug("DBus connection broken: %s", ex, exc_info=True) + if is_docker_env(): + raise ConfigEntryNotReady( + f"DBus connection broken: {ex}; try restarting `bluetooth`, `dbus`, and finally the docker container" + ) from ex + raise ConfigEntryNotReady( + f"DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`" + ) from ex + except FileNotFoundError as ex: + self._cancel_device_detected() + _LOGGER.debug( + "FileNotFoundError while starting bluetooth: %s", ex, exc_info=True + ) + if is_docker_env(): + raise ConfigEntryNotReady( + f"DBus service not found; docker config may be missing `-v /run/dbus:/run/dbus:ro`: {ex}" + ) from ex + raise ConfigEntryNotReady( + f"DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}" + ) from ex except asyncio.TimeoutError as ex: self._cancel_device_detected() raise ConfigEntryNotReady( f"Timed out starting Bluetooth after {START_TIMEOUT} seconds" ) from ex - except (FileNotFoundError, BleakError) as ex: + except BleakError as ex: self._cancel_device_detected() + _LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True) raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex self.async_setup_unavailable_tracking() self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index a47916506df..edc5eb024a6 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch from bleak import BleakError from bleak.backends.scanner import AdvertisementData, BLEDevice +from dbus_next import InvalidMessageError import pytest from homeassistant.components import bluetooth @@ -1409,3 +1410,112 @@ async def test_changing_the_adapter_at_runtime(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() + + +async def test_dbus_socket_missing_in_container(hass, caplog): + """Test we handle dbus being missing in the container.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=True + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=FileNotFoundError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "/run/dbus" in caplog.text + assert "docker" in caplog.text + + +async def test_dbus_socket_missing(hass, caplog): + """Test we handle dbus being missing.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=False + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=FileNotFoundError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "DBus" in caplog.text + assert "docker" not in caplog.text + + +async def test_dbus_broken_pipe_in_container(hass, caplog): + """Test we handle dbus broken pipe in the container.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=True + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=BrokenPipeError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "dbus" in caplog.text + assert "restarting" in caplog.text + assert "container" in caplog.text + + +async def test_dbus_broken_pipe(hass, caplog): + """Test we handle dbus broken pipe.""" + + with patch( + "homeassistant.components.bluetooth.is_docker_env", return_value=False + ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=BrokenPipeError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "DBus" in caplog.text + assert "restarting" in caplog.text + assert "container" not in caplog.text + + +async def test_invalid_dbus_message(hass, caplog): + """Test we handle invalid dbus message.""" + + with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=InvalidMessageError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "dbus" in caplog.text From 51a6899a60c6944ec9b66f778cd9eb7a7065cdee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 10:38:01 -1000 Subject: [PATCH 3100/3516] Only stat the .dockerenv file once (#76097) --- homeassistant/util/package.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 18ab43967ec..49ab3c10f8c 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from functools import cache from importlib.metadata import PackageNotFoundError, version import logging import os @@ -23,6 +24,7 @@ def is_virtual_env() -> bool: ) +@cache def is_docker_env() -> bool: """Return True if we run in a docker env.""" return Path("/.dockerenv").exists() From ad14b5f3d7db2cf6d5debff8a295516eae8938b4 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 2 Aug 2022 22:05:36 +0100 Subject: [PATCH 3101/3516] Fix Xiaomi BLE UI string issues (#76099) --- homeassistant/components/xiaomi_ble/config_flow.py | 2 +- homeassistant/components/xiaomi_ble/strings.json | 12 +++++++----- .../components/xiaomi_ble/translations/en.json | 14 ++++++++------ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index 725c513914f..a05e703db6a 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -189,7 +189,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Ack that device is slow.""" - if user_input is not None or not onboarding.async_is_onboarded(self.hass): + if user_input is not None: return self._async_get_or_create_entry() self._set_confirm_only() diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index 9d2a0ae40d8..5ecbb8e1b88 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -11,7 +11,7 @@ "bluetooth_confirm": { "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" }, - "slow_confirm": { + "confirm_slow": { "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." }, "get_encryption_key_legacy": { @@ -27,14 +27,16 @@ } } }, + "error": { + "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", + "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", + "expected_32_characters": "Expected a 32 character hexadecimal bindkey." + }, "abort": { "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", - "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", - "expected_32_characters": "Expected a 32 character hexadecimal bindkey." + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } } diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json index 4648b28cc93..2cb77dd2c07 100644 --- a/homeassistant/components/xiaomi_ble/translations/en.json +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -3,17 +3,22 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", - "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", - "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", - "expected_32_characters": "Expected a 32 character hexadecimal bindkey.", "no_devices_found": "No devices found on the network", "reauth_successful": "Re-authentication was successful" }, + "error": { + "decryption_failed": "The provided bindkey did not work, sensor data could not be decrypted. Please check it and try again.", + "expected_24_characters": "Expected a 24 character hexadecimal bindkey.", + "expected_32_characters": "Expected a 32 character hexadecimal bindkey." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, + "confirm_slow": { + "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindkey" @@ -26,9 +31,6 @@ }, "description": "The sensor data broadcast by the sensor is encrypted. In order to decrypt it we need a 24 character hexadecimal bindkey." }, - "slow_confirm": { - "description": "There hasn't been a broadcast from this device in the last minute so we aren't sure if this device uses encryption or not. This may be because the device uses a slow broadcast interval. Confirm to add this device anyway, then the next time a broadcast is received you will be prompted to enter its bindkey if it's needed." - }, "user": { "data": { "address": "Device" From d85129c52753a152c0af26fd7eaf5eb61c7deefd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 20:35:41 -1000 Subject: [PATCH 3102/3516] Bump aiohomekit to 1.2.3 to fix hang at startup (#76102) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 2de2a915d41..ac1be576906 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.2"], + "requirements": ["aiohomekit==1.2.3"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 87ad8ffc67e..5e277e8e801 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.2 +aiohomekit==1.2.3 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 058971731c1..acf67deb250 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.2 +aiohomekit==1.2.3 # homeassistant.components.emulated_hue # homeassistant.components.http From 42a1f6ca20794f56b1b3afe09702e5ef884cf09f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Aug 2022 20:34:46 -1000 Subject: [PATCH 3103/3516] Bump pySwitchbot to 0.17.3 to fix hang at startup (#76103) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 41d0d7efda6..f01eae4a938 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.17.1"], + "requirements": ["PySwitchbot==0.17.3"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 5e277e8e801..78d5a3db456 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.17.1 +PySwitchbot==0.17.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index acf67deb250..01ba627194f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.17.1 +PySwitchbot==0.17.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From bc1e371cae25cad0491dd78879f51229dd474af2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Aug 2022 08:58:12 +0200 Subject: [PATCH 3104/3516] Bumped version to 2022.8.0b7 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 3e7204e1746..dd5a41a7aa5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0b7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 2fa072c6d30..8a65560b44a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0b6" +version = "2022.8.0b7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 98f0b24c42cf8557019bd4d749d5b27c13e96524 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 3 Aug 2022 09:41:00 +0200 Subject: [PATCH 3105/3516] Fix deconz group log warning (#76114) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 8019d0df2df..6384ebfcd5f 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==100"], + "requirements": ["pydeconz==101"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index db7a13b7357..5cdc66dec32 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1461,7 +1461,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==100 +pydeconz==101 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0fb93106711..24b0f4b52e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1004,7 +1004,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==100 +pydeconz==101 # homeassistant.components.dexcom pydexcom==0.2.3 From 1ba18f8df6fafbe17d83d439c079437fae68d448 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 3 Aug 2022 09:56:13 +0200 Subject: [PATCH 3106/3516] Improve type hints in vesync lights (#75998) * Improve type hints in vesync lights * Adjust import --- homeassistant/components/vesync/__init__.py | 2 +- homeassistant/components/vesync/common.py | 31 +++++++++++---------- homeassistant/components/vesync/light.py | 16 +++-------- 3 files changed, 22 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 10aa49514e5..55addd81066 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -21,7 +21,7 @@ from .const import ( VS_SWITCHES, ) -PLATFORMS = [Platform.SWITCH, Platform.FAN, Platform.LIGHT, Platform.SENSOR] +PLATFORMS = [Platform.FAN, Platform.LIGHT, Platform.SENSOR, Platform.SWITCH] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index e11897ea9ae..752a65ff051 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -1,7 +1,10 @@ """Common utilities for VeSync Component.""" import logging +from typing import Any -from homeassistant.helpers.entity import Entity, ToggleEntity +from pyvesync.vesyncbasedevice import VeSyncBaseDevice + +from homeassistant.helpers.entity import DeviceInfo, Entity, ToggleEntity from .const import DOMAIN, VS_FANS, VS_LIGHTS, VS_SENSORS, VS_SWITCHES @@ -48,7 +51,7 @@ async def async_process_devices(hass, manager): class VeSyncBaseEntity(Entity): """Base class for VeSync Entity Representations.""" - def __init__(self, device): + def __init__(self, device: VeSyncBaseDevice) -> None: """Initialize the VeSync device.""" self.device = device self._attr_unique_id = self.base_unique_id @@ -65,7 +68,7 @@ class VeSyncBaseEntity(Entity): return self.device.cid @property - def base_name(self): + def base_name(self) -> str: """Return the name of the device.""" # Same story here as `base_unique_id` above return self.device.device_name @@ -76,17 +79,17 @@ class VeSyncBaseEntity(Entity): return self.device.connection_status == "online" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device information.""" - return { - "identifiers": {(DOMAIN, self.base_unique_id)}, - "name": self.base_name, - "model": self.device.device_type, - "default_manufacturer": "VeSync", - "sw_version": self.device.current_firm_version, - } + return DeviceInfo( + identifiers={(DOMAIN, self.base_unique_id)}, + name=self.base_name, + model=self.device.device_type, + default_manufacturer="VeSync", + sw_version=self.device.current_firm_version, + ) - def update(self): + def update(self) -> None: """Update vesync device.""" self.device.update() @@ -100,10 +103,10 @@ class VeSyncDevice(VeSyncBaseEntity, ToggleEntity): return self.device.details @property - def is_on(self): + def is_on(self) -> bool: """Return True if device is on.""" return self.device.device_status == "on" - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self.device.turn_off() diff --git a/homeassistant/components/vesync/light.py b/homeassistant/components/vesync/light.py index 8727a770112..57329c0973e 100644 --- a/homeassistant/components/vesync/light.py +++ b/homeassistant/components/vesync/light.py @@ -67,7 +67,7 @@ class VeSyncBaseLight(VeSyncDevice, LightEntity): """Base class for VeSync Light Devices Representations.""" @property - def brightness(self): + def brightness(self) -> int: """Get light brightness.""" # get value from pyvesync library api, result = self.device.brightness @@ -141,10 +141,12 @@ class VeSyncTunableWhiteLightHA(VeSyncBaseLight, LightEntity): """Representation of a VeSync Tunable White Light device.""" _attr_color_mode = ColorMode.COLOR_TEMP + _attr_max_mireds = 370 # 1,000,000 divided by 2700 Kelvin = 370 Mireds + _attr_min_mireds = 154 # 1,000,000 divided by 6500 Kelvin = 154 Mireds _attr_supported_color_modes = {ColorMode.COLOR_TEMP} @property - def color_temp(self): + def color_temp(self) -> int: """Get device white temperature.""" # get value from pyvesync library api, result = self.device.color_temp_pct @@ -169,13 +171,3 @@ class VeSyncTunableWhiteLightHA(VeSyncBaseLight, LightEntity): ) # ensure value between minimum and maximum Mireds return max(self.min_mireds, min(color_temp_value, self.max_mireds)) - - @property - def min_mireds(self): - """Set device coldest white temperature.""" - return 154 # 154 Mireds ( 1,000,000 divided by 6500 Kelvin = 154 Mireds) - - @property - def max_mireds(self): - """Set device warmest white temperature.""" - return 370 # 370 Mireds ( 1,000,000 divided by 2700 Kelvin = 370 Mireds) From 1ee4445a7b8a5bda428b34f8746bcb97bf914ecb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 3 Aug 2022 10:15:41 +0200 Subject: [PATCH 3107/3516] Improve type hints in azure devops config flow (#75909) * Improve type hints in azure devops config flow * Improve --- .../components/azure_devops/config_flow.py | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/azure_devops/config_flow.py b/homeassistant/components/azure_devops/config_flow.py index 8fba3378886..a654dc2be61 100644 --- a/homeassistant/components/azure_devops/config_flow.py +++ b/homeassistant/components/azure_devops/config_flow.py @@ -1,4 +1,6 @@ """Config flow to configure the Azure DevOps integration.""" +from __future__ import annotations + from collections.abc import Mapping from typing import Any @@ -19,11 +21,13 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize config flow.""" - self._organization = None - self._project = None - self._pat = None + self._organization: str | None = None + self._project: str | None = None + self._pat: str | None = None - async def _show_setup_form(self, errors=None): + async def _show_setup_form( + self, errors: dict[str, str] | None = None + ) -> FlowResult: """Show the setup form to the user.""" return self.async_show_form( step_id="user", @@ -37,7 +41,7 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def _show_reauth_form(self, errors=None): + async def _show_reauth_form(self, errors: dict[str, str]) -> FlowResult: """Show the reauth form to the user.""" return self.async_show_form( step_id="reauth", @@ -48,9 +52,9 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors or {}, ) - async def _check_setup(self): + async def _check_setup(self) -> dict[str, str] | None: """Check the setup of the flow.""" - errors = {} + errors: dict[str, str] = {} client = DevOpsClient() @@ -69,10 +73,12 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): return errors return None - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" if user_input is None: - return await self._show_setup_form(user_input) + return await self._show_setup_form() self._organization = user_input[CONF_ORG] self._project = user_input[CONF_PROJECT] @@ -115,7 +121,7 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): ) return self.async_abort(reason="reauth_successful") - def _async_create_entry(self): + def _async_create_entry(self) -> FlowResult: """Handle create entry.""" return self.async_create_entry( title=f"{self._organization}/{self._project}", From 651928ee0cd3f85db2526e2809c416ab824c1bc0 Mon Sep 17 00:00:00 2001 From: Heine Furubotten Date: Wed, 3 Aug 2022 10:31:09 +0200 Subject: [PATCH 3108/3516] Bump `azure-servicebus` to support py3.10 (#76092) Bump azure-servicebus --- .../azure_service_bus/manifest.json | 2 +- .../components/azure_service_bus/notify.py | 24 ++++++++++--------- requirements_all.txt | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/azure_service_bus/manifest.json b/homeassistant/components/azure_service_bus/manifest.json index 6cf5e2bf406..26ccea446f2 100644 --- a/homeassistant/components/azure_service_bus/manifest.json +++ b/homeassistant/components/azure_service_bus/manifest.json @@ -2,7 +2,7 @@ "domain": "azure_service_bus", "name": "Azure Service Bus", "documentation": "https://www.home-assistant.io/integrations/azure_service_bus", - "requirements": ["azure-servicebus==0.50.3"], + "requirements": ["azure-servicebus==7.8.0"], "codeowners": ["@hfurubotten"], "iot_class": "cloud_push", "loggers": ["azure"] diff --git a/homeassistant/components/azure_service_bus/notify.py b/homeassistant/components/azure_service_bus/notify.py index 0d48ff6b2d6..53873373011 100644 --- a/homeassistant/components/azure_service_bus/notify.py +++ b/homeassistant/components/azure_service_bus/notify.py @@ -2,11 +2,12 @@ import json import logging -from azure.servicebus.aio import Message, ServiceBusClient -from azure.servicebus.common.errors import ( - MessageSendFailed, +from azure.servicebus import ServiceBusMessage +from azure.servicebus.aio import ServiceBusClient +from azure.servicebus.exceptions import ( + MessagingEntityNotFoundError, ServiceBusConnectionError, - ServiceBusResourceNotFound, + ServiceBusError, ) import voluptuous as vol @@ -60,10 +61,10 @@ def get_service(hass, config, discovery_info=None): try: if queue_name: - client = servicebus.get_queue(queue_name) + client = servicebus.get_queue_sender(queue_name) else: - client = servicebus.get_topic(topic_name) - except (ServiceBusConnectionError, ServiceBusResourceNotFound) as err: + client = servicebus.get_topic_sender(topic_name) + except (ServiceBusConnectionError, MessagingEntityNotFoundError) as err: _LOGGER.error( "Connection error while creating client for queue/topic '%s'. %s", queue_name or topic_name, @@ -93,11 +94,12 @@ class ServiceBusNotificationService(BaseNotificationService): if data := kwargs.get(ATTR_DATA): dto.update(data) - queue_message = Message(json.dumps(dto)) - queue_message.properties.content_type = CONTENT_TYPE_JSON + queue_message = ServiceBusMessage( + json.dumps(dto), content_type=CONTENT_TYPE_JSON + ) try: - await self._client.send(queue_message) - except MessageSendFailed as err: + await self._client.send_messages(queue_message) + except ServiceBusError as err: _LOGGER.error( "Could not send service bus notification to %s. %s", self._client.name, diff --git a/requirements_all.txt b/requirements_all.txt index 5cdc66dec32..80031eadf8c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -378,7 +378,7 @@ axis==44 azure-eventhub==5.7.0 # homeassistant.components.azure_service_bus -azure-servicebus==0.50.3 +azure-servicebus==7.8.0 # homeassistant.components.baidu baidu-aip==1.6.6 From 29f6d7818a09f60f5abd0a24a935d5fa06b214e2 Mon Sep 17 00:00:00 2001 From: Heine Furubotten Date: Wed, 3 Aug 2022 10:31:09 +0200 Subject: [PATCH 3109/3516] Bump `azure-servicebus` to support py3.10 (#76092) Bump azure-servicebus --- .../azure_service_bus/manifest.json | 2 +- .../components/azure_service_bus/notify.py | 24 ++++++++++--------- requirements_all.txt | 2 +- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/azure_service_bus/manifest.json b/homeassistant/components/azure_service_bus/manifest.json index 6cf5e2bf406..26ccea446f2 100644 --- a/homeassistant/components/azure_service_bus/manifest.json +++ b/homeassistant/components/azure_service_bus/manifest.json @@ -2,7 +2,7 @@ "domain": "azure_service_bus", "name": "Azure Service Bus", "documentation": "https://www.home-assistant.io/integrations/azure_service_bus", - "requirements": ["azure-servicebus==0.50.3"], + "requirements": ["azure-servicebus==7.8.0"], "codeowners": ["@hfurubotten"], "iot_class": "cloud_push", "loggers": ["azure"] diff --git a/homeassistant/components/azure_service_bus/notify.py b/homeassistant/components/azure_service_bus/notify.py index 0d48ff6b2d6..53873373011 100644 --- a/homeassistant/components/azure_service_bus/notify.py +++ b/homeassistant/components/azure_service_bus/notify.py @@ -2,11 +2,12 @@ import json import logging -from azure.servicebus.aio import Message, ServiceBusClient -from azure.servicebus.common.errors import ( - MessageSendFailed, +from azure.servicebus import ServiceBusMessage +from azure.servicebus.aio import ServiceBusClient +from azure.servicebus.exceptions import ( + MessagingEntityNotFoundError, ServiceBusConnectionError, - ServiceBusResourceNotFound, + ServiceBusError, ) import voluptuous as vol @@ -60,10 +61,10 @@ def get_service(hass, config, discovery_info=None): try: if queue_name: - client = servicebus.get_queue(queue_name) + client = servicebus.get_queue_sender(queue_name) else: - client = servicebus.get_topic(topic_name) - except (ServiceBusConnectionError, ServiceBusResourceNotFound) as err: + client = servicebus.get_topic_sender(topic_name) + except (ServiceBusConnectionError, MessagingEntityNotFoundError) as err: _LOGGER.error( "Connection error while creating client for queue/topic '%s'. %s", queue_name or topic_name, @@ -93,11 +94,12 @@ class ServiceBusNotificationService(BaseNotificationService): if data := kwargs.get(ATTR_DATA): dto.update(data) - queue_message = Message(json.dumps(dto)) - queue_message.properties.content_type = CONTENT_TYPE_JSON + queue_message = ServiceBusMessage( + json.dumps(dto), content_type=CONTENT_TYPE_JSON + ) try: - await self._client.send(queue_message) - except MessageSendFailed as err: + await self._client.send_messages(queue_message) + except ServiceBusError as err: _LOGGER.error( "Could not send service bus notification to %s. %s", self._client.name, diff --git a/requirements_all.txt b/requirements_all.txt index 78d5a3db456..5027b7df0cd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -375,7 +375,7 @@ axis==44 azure-eventhub==5.7.0 # homeassistant.components.azure_service_bus -azure-servicebus==0.50.3 +azure-servicebus==7.8.0 # homeassistant.components.baidu baidu-aip==1.6.6 From 81ee24738bbafccae01cf25c84060cb2da622ba1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 3 Aug 2022 09:41:00 +0200 Subject: [PATCH 3110/3516] Fix deconz group log warning (#76114) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 8019d0df2df..6384ebfcd5f 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==100"], + "requirements": ["pydeconz==101"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 5027b7df0cd..6e1850d33a7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1455,7 +1455,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==100 +pydeconz==101 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 01ba627194f..6f75b540b28 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1001,7 +1001,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==100 +pydeconz==101 # homeassistant.components.dexcom pydexcom==0.2.3 From 80a053a4cde0ec75c532f3a4870c742561579994 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Aug 2022 11:42:18 +0200 Subject: [PATCH 3111/3516] Bumped version to 2022.8.0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index dd5a41a7aa5..18561a8bd2e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,7 +7,7 @@ from .backports.enum import StrEnum MAJOR_VERSION: Final = 2022 MINOR_VERSION: Final = 8 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) diff --git a/pyproject.toml b/pyproject.toml index 8a65560b44a..c7e187e07f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2022.8.0b7" +version = "2022.8.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 7f83cba83ab0226e3dc9e261f941f65dff91f6be Mon Sep 17 00:00:00 2001 From: Erik Flodin Date: Wed, 3 Aug 2022 11:53:29 +0200 Subject: [PATCH 3112/3516] Bump pyTibber to 0.24.0 (#76098) To be able to add tibber sensors for production. --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index b07cebde680..0ac93c86668 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -3,7 +3,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.22.3"], + "requirements": ["pyTibber==0.24.0"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 80031eadf8c..1c2079d6857 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1359,7 +1359,7 @@ pyRFXtrx==0.30.0 pySwitchmate==0.5.1 # homeassistant.components.tibber -pyTibber==0.22.3 +pyTibber==0.24.0 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24b0f4b52e7..cfd8122bcff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -947,7 +947,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.tibber -pyTibber==0.22.3 +pyTibber==0.24.0 # homeassistant.components.nextbus py_nextbusnext==0.1.5 From 34b0e0d062e96f5424267d79fe2c12e41fab9b9f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Aug 2022 07:46:54 -1000 Subject: [PATCH 3113/3516] Bump bleak to 0.15.1 (#76136) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 40e63ec7180..f3828db5d10 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.15.0", "bluetooth-adapters==0.1.3"], + "requirements": ["bleak==0.15.1", "bluetooth-adapters==0.1.3"], "codeowners": ["@bdraco"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 81182a7d8ba..3ff9fd3c114 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 -bleak==0.15.0 +bleak==0.15.1 bluetooth-adapters==0.1.3 certifi>=2021.5.30 ciso8601==2.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 1c2079d6857..e7e6999053e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -408,7 +408,7 @@ bimmer_connected==0.10.1 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak==0.15.0 +bleak==0.15.1 # homeassistant.components.blebox blebox_uniapi==2.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfd8122bcff..18ef8e97b63 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -326,7 +326,7 @@ bellows==0.31.2 bimmer_connected==0.10.1 # homeassistant.components.bluetooth -bleak==0.15.0 +bleak==0.15.1 # homeassistant.components.blebox blebox_uniapi==2.0.2 From 84747ada668a2f07893e2b4093a586c4432b5e5f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 3 Aug 2022 21:22:30 +0200 Subject: [PATCH 3114/3516] Use attributes in decora light (#76047) --- homeassistant/components/decora/light.py | 66 ++++++++---------------- 1 file changed, 22 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index 9dbc031d476..c6fae73bc28 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -6,7 +6,7 @@ import copy from functools import wraps import logging import time -from typing import TypeVar +from typing import TYPE_CHECKING, Any, TypeVar from bluepy.btle import BTLEException # pylint: disable=import-error import decora # pylint: disable=import-error @@ -21,10 +21,13 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME -from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + from homeassistant.helpers.entity_platform import AddEntitiesCallback + from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + _DecoraLightT = TypeVar("_DecoraLightT", bound="DecoraLight") _R = TypeVar("_R") @@ -110,65 +113,40 @@ class DecoraLight(LightEntity): _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} - def __init__(self, device): + def __init__(self, device: dict[str, Any]) -> None: """Initialize the light.""" - self._name = device["name"] - self._address = device["address"] + self._attr_name = device["name"] + self._attr_unique_id = device["address"] self._key = device["key"] - self._switch = decora.decora(self._address, self._key) - self._brightness = 0 - self._state = False - - @property - def unique_id(self): - """Return the ID of this light.""" - return self._address - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def assumed_state(self): - """We can read the actual state.""" - return False + self._switch = decora.decora(device["address"], self._key) + self._attr_brightness = 0 + self._attr_is_on = False @retry - def set_state(self, brightness): + def set_state(self, brightness: int) -> None: """Set the state of this lamp to the provided brightness.""" self._switch.set_brightness(int(brightness / 2.55)) - self._brightness = brightness + self._attr_brightness = brightness @retry - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the specified or all lights on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) self._switch.on() - self._state = True + self._attr_is_on = True if brightness is not None: self.set_state(brightness) @retry - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the specified or all lights off.""" self._switch.off() - self._state = False + self._attr_is_on = False @retry - def update(self): + def update(self) -> None: """Synchronise internal state with the actual light state.""" - self._brightness = self._switch.get_brightness() * 2.55 - self._state = self._switch.get_on() + self._attr_brightness = self._switch.get_brightness() * 2.55 + self._attr_is_on = self._switch.get_on() From fbde347e6490292549fffdfe374561501dff633f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 3 Aug 2022 13:24:55 -0600 Subject: [PATCH 3115/3516] Move RainMachine utils to the correct location (#76051) * Move RainMachine utils to the correct location * Imports --- homeassistant/components/rainmachine/const.py | 17 ----------------- homeassistant/components/rainmachine/sensor.py | 4 +--- homeassistant/components/rainmachine/switch.py | 2 +- homeassistant/components/rainmachine/util.py | 17 +++++++++++++++++ 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/rainmachine/const.py b/homeassistant/components/rainmachine/const.py index f386341c161..56c1660a0ba 100644 --- a/homeassistant/components/rainmachine/const.py +++ b/homeassistant/components/rainmachine/const.py @@ -1,8 +1,6 @@ """Define constants for the SimpliSafe component.""" import logging -from homeassistant.backports.enum import StrEnum - LOGGER = logging.getLogger(__package__) DOMAIN = "rainmachine" @@ -19,18 +17,3 @@ DATA_ZONES = "zones" DEFAULT_PORT = 8080 DEFAULT_ZONE_RUN = 60 * 10 - - -class RunStates(StrEnum): - """Define an enum for program/zone run states.""" - - NOT_RUNNING = "Not Running" - QUEUED = "Queued" - RUNNING = "Running" - - -RUN_STATE_MAP = { - 0: RunStates.NOT_RUNNING, - 1: RunStates.RUNNING, - 2: RunStates.QUEUED, -} diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index e57386fe0ec..797420b460f 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -31,14 +31,12 @@ from .const import ( DATA_RESTRICTIONS_UNIVERSAL, DATA_ZONES, DOMAIN, - RUN_STATE_MAP, - RunStates, ) from .model import ( RainMachineDescriptionMixinApiCategory, RainMachineDescriptionMixinUid, ) -from .util import key_exists +from .util import RUN_STATE_MAP, RunStates, key_exists DEFAULT_ZONE_COMPLETION_TIME_WOBBLE_TOLERANCE = timedelta(seconds=5) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index aa91f529b5b..1fcdab49836 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -30,9 +30,9 @@ from .const import ( DATA_ZONES, DEFAULT_ZONE_RUN, DOMAIN, - RUN_STATE_MAP, ) from .model import RainMachineDescriptionMixinUid +from .util import RUN_STATE_MAP ATTR_AREA = "area" ATTR_CS_ON = "cs_on" diff --git a/homeassistant/components/rainmachine/util.py b/homeassistant/components/rainmachine/util.py index 27a0636688e..6bf15f2fb9c 100644 --- a/homeassistant/components/rainmachine/util.py +++ b/homeassistant/components/rainmachine/util.py @@ -3,6 +3,23 @@ from __future__ import annotations from typing import Any +from homeassistant.backports.enum import StrEnum + + +class RunStates(StrEnum): + """Define an enum for program/zone run states.""" + + NOT_RUNNING = "Not Running" + QUEUED = "Queued" + RUNNING = "Running" + + +RUN_STATE_MAP = { + 0: RunStates.NOT_RUNNING, + 1: RunStates.RUNNING, + 2: RunStates.QUEUED, +} + def key_exists(data: dict[str, Any], search_key: str) -> bool: """Return whether a key exists in a nested dict.""" From 1806172551236039306717edcc910ef3c17df258 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 3 Aug 2022 21:26:34 +0200 Subject: [PATCH 3116/3516] Improve type hints in hive lights (#76025) --- homeassistant/components/hive/__init__.py | 6 +++--- homeassistant/components/hive/light.py | 20 ++++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index a1a784162e8..bd74ecbff11 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -139,7 +139,7 @@ def refresh_system( class HiveEntity(Entity): """Initiate Hive Base Class.""" - def __init__(self, hive, hive_device): + def __init__(self, hive: Hive, hive_device: dict[str, Any]) -> None: """Initialize the instance.""" self.hive = hive self.device = hive_device @@ -153,9 +153,9 @@ class HiveEntity(Entity): sw_version=self.device["deviceData"]["version"], via_device=(DOMAIN, self.device["parentDevice"]), ) - self.attributes = {} + self.attributes: dict[str, Any] = {} - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """When entity is added to Home Assistant.""" self.async_on_remove( async_dispatcher_connect(self.hass, DOMAIN, self.async_write_ha_state) diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index 69345c430c7..a340aee0764 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +from typing import TYPE_CHECKING, Any from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -18,6 +19,9 @@ import homeassistant.util.color as color_util from . import HiveEntity, refresh_system from .const import ATTR_MODE, DOMAIN +if TYPE_CHECKING: + from apyhiveapi import Hive + PARALLEL_UPDATES = 0 SCAN_INTERVAL = timedelta(seconds=15) @@ -27,7 +31,7 @@ async def async_setup_entry( ) -> None: """Set up Hive thermostat based on a config entry.""" - hive = hass.data[DOMAIN][entry.entry_id] + hive: Hive = hass.data[DOMAIN][entry.entry_id] devices = hive.session.deviceList.get("light") entities = [] if devices: @@ -39,7 +43,7 @@ async def async_setup_entry( class HiveDeviceLight(HiveEntity, LightEntity): """Hive Active Light Device.""" - def __init__(self, hive, hive_device): + def __init__(self, hive: Hive, hive_device: dict[str, Any]) -> None: """Initialise hive light.""" super().__init__(hive, hive_device) if self.device["hiveType"] == "warmwhitelight": @@ -55,22 +59,22 @@ class HiveDeviceLight(HiveEntity, LightEntity): self._attr_max_mireds = 370 @refresh_system - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" new_brightness = None new_color_temp = None new_color = None if ATTR_BRIGHTNESS in kwargs: - tmp_new_brightness = kwargs.get(ATTR_BRIGHTNESS) + tmp_new_brightness = kwargs[ATTR_BRIGHTNESS] percentage_brightness = (tmp_new_brightness / 255) * 100 new_brightness = int(round(percentage_brightness / 5.0) * 5.0) if new_brightness == 0: new_brightness = 5 if ATTR_COLOR_TEMP in kwargs: - tmp_new_color_temp = kwargs.get(ATTR_COLOR_TEMP) + tmp_new_color_temp = kwargs[ATTR_COLOR_TEMP] new_color_temp = round(1000000 / tmp_new_color_temp) if ATTR_HS_COLOR in kwargs: - get_new_color = kwargs.get(ATTR_HS_COLOR) + get_new_color = kwargs[ATTR_HS_COLOR] hue = int(get_new_color[0]) saturation = int(get_new_color[1]) new_color = (hue, saturation, 100) @@ -80,11 +84,11 @@ class HiveDeviceLight(HiveEntity, LightEntity): ) @refresh_system - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" await self.hive.light.turnOff(self.device) - async def async_update(self): + async def async_update(self) -> None: """Update all Node data from Hive.""" await self.hive.session.updateData(self.device) self.device = await self.hive.light.getLight(self.device) From dd862595a3dc3c0715cb7527eb45ef5136228de1 Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Wed, 3 Aug 2022 23:19:10 +0300 Subject: [PATCH 3117/3516] New binary sensors for Ukraine Alarm (#76155) new alert types for ukraine alarm --- .../components/ukraine_alarm/binary_sensor.py | 14 ++++++++++++++ homeassistant/components/ukraine_alarm/const.py | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/homeassistant/components/ukraine_alarm/binary_sensor.py b/homeassistant/components/ukraine_alarm/binary_sensor.py index 10d66e0cb64..3cfe79ef5fb 100644 --- a/homeassistant/components/ukraine_alarm/binary_sensor.py +++ b/homeassistant/components/ukraine_alarm/binary_sensor.py @@ -18,6 +18,8 @@ from . import UkraineAlarmDataUpdateCoordinator from .const import ( ALERT_TYPE_AIR, ALERT_TYPE_ARTILLERY, + ALERT_TYPE_CHEMICAL, + ALERT_TYPE_NUCLEAR, ALERT_TYPE_UNKNOWN, ALERT_TYPE_URBAN_FIGHTS, ATTRIBUTION, @@ -49,6 +51,18 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( device_class=BinarySensorDeviceClass.SAFETY, icon="mdi:tank", ), + BinarySensorEntityDescription( + key=ALERT_TYPE_CHEMICAL, + name="Chemical", + device_class=BinarySensorDeviceClass.SAFETY, + icon="mdi:chemical-weapon", + ), + BinarySensorEntityDescription( + key=ALERT_TYPE_NUCLEAR, + name="Nuclear", + device_class=BinarySensorDeviceClass.SAFETY, + icon="mdi:nuke", + ), ) diff --git a/homeassistant/components/ukraine_alarm/const.py b/homeassistant/components/ukraine_alarm/const.py index cc1ae352967..bb0902293d4 100644 --- a/homeassistant/components/ukraine_alarm/const.py +++ b/homeassistant/components/ukraine_alarm/const.py @@ -10,10 +10,14 @@ ALERT_TYPE_UNKNOWN = "UNKNOWN" ALERT_TYPE_AIR = "AIR" ALERT_TYPE_ARTILLERY = "ARTILLERY" ALERT_TYPE_URBAN_FIGHTS = "URBAN_FIGHTS" +ALERT_TYPE_CHEMICAL = "CHEMICAL" +ALERT_TYPE_NUCLEAR = "NUCLEAR" ALERT_TYPES = { ALERT_TYPE_UNKNOWN, ALERT_TYPE_AIR, ALERT_TYPE_ARTILLERY, ALERT_TYPE_URBAN_FIGHTS, + ALERT_TYPE_CHEMICAL, + ALERT_TYPE_NUCLEAR, } PLATFORMS = [Platform.BINARY_SENSOR] From 842cc060f80a632032dacbe1e2eaa8ca6421eda0 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Wed, 3 Aug 2022 22:33:05 +0200 Subject: [PATCH 3118/3516] Fix zwave_js addon info (#76044) * Add add-on store info command * Use add-on store info command in zwave_js * Fix init tests * Update tests * Fix method for addon store info * Fix response parsing * Fix store addon installed response parsing * Remove addon info log that can contain network keys * Add supervisor store addon info test * Default to version None if add-on not installed Co-authored-by: Mike Degatano Co-authored-by: Mike Degatano --- homeassistant/components/hassio/__init__.py | 12 ++++ homeassistant/components/zwave_js/addon.py | 22 +++++-- tests/components/hassio/test_init.py | 20 ++++++- tests/components/zwave_js/conftest.py | 60 +++++++++++++++++-- tests/components/zwave_js/test_config_flow.py | 18 +++--- tests/components/zwave_js/test_init.py | 22 ++++--- 6 files changed, 122 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 46592cbc20c..8535a0c3cc6 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -223,12 +223,24 @@ HARDWARE_INTEGRATIONS = { async def async_get_addon_info(hass: HomeAssistant, slug: str) -> dict: """Return add-on info. + The add-on must be installed. The caller of the function should handle HassioAPIError. """ hassio = hass.data[DOMAIN] return await hassio.get_addon_info(slug) +@api_data +async def async_get_addon_store_info(hass: HomeAssistant, slug: str) -> dict: + """Return add-on store info. + + The caller of the function should handle HassioAPIError. + """ + hassio: HassIO = hass.data[DOMAIN] + command = f"/store/addons/{slug}" + return await hassio.send_command(command, method="get") + + @bind_hass async def async_update_diagnostics(hass: HomeAssistant, diagnostics: bool) -> dict: """Update Supervisor diagnostics toggle. diff --git a/homeassistant/components/zwave_js/addon.py b/homeassistant/components/zwave_js/addon.py index 7552ee117cc..610fc850e90 100644 --- a/homeassistant/components/zwave_js/addon.py +++ b/homeassistant/components/zwave_js/addon.py @@ -14,6 +14,7 @@ from homeassistant.components.hassio import ( async_create_backup, async_get_addon_discovery_info, async_get_addon_info, + async_get_addon_store_info, async_install_addon, async_restart_addon, async_set_addon_options, @@ -136,7 +137,17 @@ class AddonManager: @api_error("Failed to get the Z-Wave JS add-on info") async def async_get_addon_info(self) -> AddonInfo: """Return and cache Z-Wave JS add-on info.""" - addon_info: dict = await async_get_addon_info(self._hass, ADDON_SLUG) + addon_store_info = await async_get_addon_store_info(self._hass, ADDON_SLUG) + LOGGER.debug("Add-on store info: %s", addon_store_info) + if not addon_store_info["installed"]: + return AddonInfo( + options={}, + state=AddonState.NOT_INSTALLED, + update_available=False, + version=None, + ) + + addon_info = await async_get_addon_info(self._hass, ADDON_SLUG) addon_state = self.async_get_addon_state(addon_info) return AddonInfo( options=addon_info["options"], @@ -148,10 +159,8 @@ class AddonManager: @callback def async_get_addon_state(self, addon_info: dict[str, Any]) -> AddonState: """Return the current state of the Z-Wave JS add-on.""" - addon_state = AddonState.NOT_INSTALLED + addon_state = AddonState.NOT_RUNNING - if addon_info["version"] is not None: - addon_state = AddonState.NOT_RUNNING if addon_info["state"] == "started": addon_state = AddonState.RUNNING if self._install_task and not self._install_task.done(): @@ -226,7 +235,7 @@ class AddonManager: """Update the Z-Wave JS add-on if needed.""" addon_info = await self.async_get_addon_info() - if addon_info.version is None: + if addon_info.state is AddonState.NOT_INSTALLED: raise AddonError("Z-Wave JS add-on is not installed") if not addon_info.update_available: @@ -301,6 +310,9 @@ class AddonManager: """Configure and start Z-Wave JS add-on.""" addon_info = await self.async_get_addon_info() + if addon_info.state is AddonState.NOT_INSTALLED: + raise AddonError("Z-Wave JS add-on is not installed") + new_addon_options = { CONF_ADDON_DEVICE: usb_path, CONF_ADDON_S0_LEGACY_KEY: s0_legacy_key, diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 60fec517aa9..41b679e448a 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -8,7 +8,12 @@ import pytest from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components import frontend from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.hassio import ADDONS_COORDINATOR, DOMAIN, STORAGE_KEY +from homeassistant.components.hassio import ( + ADDONS_COORDINATOR, + DOMAIN, + STORAGE_KEY, + async_get_addon_store_info, +) from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.helpers.device_registry import async_get @@ -748,3 +753,16 @@ async def test_setup_hardware_integration(hass, aioclient_mock, integration): assert aioclient_mock.call_count == 15 assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_get_store_addon_info(hass, hassio_stubs, aioclient_mock): + """Test get store add-on info from Supervisor API.""" + aioclient_mock.clear_requests() + aioclient_mock.get( + "http://127.0.0.1/store/addons/test", + json={"result": "ok", "data": {"name": "bla"}}, + ) + + data = await async_get_addon_store_info(hass, "test") + assert data["name"] == "bla" + assert aioclient_mock.call_count == 1 diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index 7cf7ebd7ea2..1524aca719e 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -38,18 +38,56 @@ def mock_addon_info(addon_info_side_effect): yield addon_info +@pytest.fixture(name="addon_store_info_side_effect") +def addon_store_info_side_effect_fixture(): + """Return the add-on store info side effect.""" + return None + + +@pytest.fixture(name="addon_store_info") +def mock_addon_store_info(addon_store_info_side_effect): + """Mock Supervisor add-on info.""" + with patch( + "homeassistant.components.zwave_js.addon.async_get_addon_store_info", + side_effect=addon_store_info_side_effect, + ) as addon_store_info: + addon_store_info.return_value = { + "installed": None, + "state": None, + "version": "1.0.0", + } + yield addon_store_info + + @pytest.fixture(name="addon_running") -def mock_addon_running(addon_info): +def mock_addon_running(addon_store_info, addon_info): """Mock add-on already running.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "started", + "version": "1.0.0", + } addon_info.return_value["state"] = "started" + addon_info.return_value["version"] = "1.0.0" return addon_info @pytest.fixture(name="addon_installed") -def mock_addon_installed(addon_info): +def mock_addon_installed(addon_store_info, addon_info): """Mock add-on already installed but not running.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "stopped", + "version": "1.0.0", + } addon_info.return_value["state"] = "stopped" - addon_info.return_value["version"] = "1.0" + addon_info.return_value["version"] = "1.0.0" + return addon_info + + +@pytest.fixture(name="addon_not_installed") +def mock_addon_not_installed(addon_store_info, addon_info): + """Mock add-on not installed.""" return addon_info @@ -81,13 +119,18 @@ def mock_set_addon_options(set_addon_options_side_effect): @pytest.fixture(name="install_addon_side_effect") -def install_addon_side_effect_fixture(addon_info): +def install_addon_side_effect_fixture(addon_store_info, addon_info): """Return the install add-on side effect.""" async def install_addon(hass, slug): """Mock install add-on.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "stopped", + "version": "1.0.0", + } addon_info.return_value["state"] = "stopped" - addon_info.return_value["version"] = "1.0" + addon_info.return_value["version"] = "1.0.0" return install_addon @@ -112,11 +155,16 @@ def mock_update_addon(): @pytest.fixture(name="start_addon_side_effect") -def start_addon_side_effect_fixture(addon_info): +def start_addon_side_effect_fixture(addon_store_info, addon_info): """Return the start add-on options side effect.""" async def start_addon(hass, slug): """Mock start add-on.""" + addon_store_info.return_value = { + "installed": "1.0.0", + "state": "started", + "version": "1.0.0", + } addon_info.return_value["state"] = "started" return start_addon diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index f107a5fd8e2..a8a2c6c7191 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -422,7 +422,7 @@ async def test_abort_discovery_with_existing_entry( async def test_abort_hassio_discovery_with_existing_flow( - hass, supervisor, addon_options + hass, supervisor, addon_installed, addon_options ): """Test hassio discovery flow is aborted when another discovery has happened.""" result = await hass.config_entries.flow.async_init( @@ -701,15 +701,13 @@ async def test_discovery_addon_not_running( async def test_discovery_addon_not_installed( hass, supervisor, - addon_installed, + addon_not_installed, install_addon, addon_options, set_addon_options, start_addon, ): """Test discovery with add-on not installed.""" - addon_installed.return_value["version"] = None - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_HASSIO}, @@ -1443,7 +1441,7 @@ async def test_addon_installed_already_configured( async def test_addon_not_installed( hass, supervisor, - addon_installed, + addon_not_installed, install_addon, addon_options, set_addon_options, @@ -1451,8 +1449,6 @@ async def test_addon_not_installed( get_addon_discovery_info, ): """Test add-on not installed.""" - addon_installed.return_value["version"] = None - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -1533,9 +1529,10 @@ async def test_addon_not_installed( assert len(mock_setup_entry.mock_calls) == 1 -async def test_install_addon_failure(hass, supervisor, addon_installed, install_addon): +async def test_install_addon_failure( + hass, supervisor, addon_not_installed, install_addon +): """Test add-on install failure.""" - addon_installed.return_value["version"] = None install_addon.side_effect = HassioAPIError() result = await hass.config_entries.flow.async_init( @@ -2292,7 +2289,7 @@ async def test_options_addon_not_installed( hass, client, supervisor, - addon_installed, + addon_not_installed, install_addon, integration, addon_options, @@ -2306,7 +2303,6 @@ async def test_options_addon_not_installed( disconnect_calls, ): """Test options flow and add-on not installed on Supervisor.""" - addon_installed.return_value["version"] = None addon_options.update(old_addon_options) entry = integration entry.unique_id = "1234" diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index a2962261ac3..202088bb481 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -432,10 +432,14 @@ async def test_start_addon( async def test_install_addon( - hass, addon_installed, install_addon, addon_options, set_addon_options, start_addon + hass, + addon_not_installed, + install_addon, + addon_options, + set_addon_options, + start_addon, ): """Test install and start the Z-Wave JS add-on during entry setup.""" - addon_installed.return_value["version"] = None device = "/test" s0_legacy_key = "s0_legacy" s2_access_control_key = "s2_access_control" @@ -583,10 +587,10 @@ async def test_addon_options_changed( "addon_version, update_available, update_calls, backup_calls, " "update_addon_side_effect, create_backup_side_effect", [ - ("1.0", True, 1, 1, None, None), - ("1.0", False, 0, 0, None, None), - ("1.0", True, 1, 1, HassioAPIError("Boom"), None), - ("1.0", True, 0, 1, None, HassioAPIError("Boom")), + ("1.0.0", True, 1, 1, None, None), + ("1.0.0", False, 0, 0, None, None), + ("1.0.0", True, 1, 1, HassioAPIError("Boom"), None), + ("1.0.0", True, 0, 1, None, HassioAPIError("Boom")), ], ) async def test_update_addon( @@ -720,7 +724,7 @@ async def test_remove_entry( assert create_backup.call_count == 1 assert create_backup.call_args == call( hass, - {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, + {"name": "addon_core_zwave_js_1.0.0", "addons": ["core_zwave_js"]}, partial=True, ) assert uninstall_addon.call_count == 1 @@ -762,7 +766,7 @@ async def test_remove_entry( assert create_backup.call_count == 1 assert create_backup.call_args == call( hass, - {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, + {"name": "addon_core_zwave_js_1.0.0", "addons": ["core_zwave_js"]}, partial=True, ) assert uninstall_addon.call_count == 0 @@ -786,7 +790,7 @@ async def test_remove_entry( assert create_backup.call_count == 1 assert create_backup.call_args == call( hass, - {"name": "addon_core_zwave_js_1.0", "addons": ["core_zwave_js"]}, + {"name": "addon_core_zwave_js_1.0.0", "addons": ["core_zwave_js"]}, partial=True, ) assert uninstall_addon.call_count == 1 From 72a0ca4871c7a639e3a6be73792aeb26ebe25dc6 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 3 Aug 2022 22:03:10 +0100 Subject: [PATCH 3119/3516] Add homekit_controller thread node capabilties diagnostic sensor (#76120) --- .../components/homekit_controller/const.py | 1 + .../components/homekit_controller/sensor.py | 53 ++++++++++++++++++- .../homekit_controller/strings.sensor.json | 12 +++++ .../translations/sensor.en.json | 12 +++++ .../test_nanoleaf_strip_nl55.py | 7 +++ .../homekit_controller/test_sensor.py | 20 +++++++ 6 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/homekit_controller/strings.sensor.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.en.json diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index c27527d3638..5ea8205260e 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -88,6 +88,7 @@ CHARACTERISTIC_PLATFORMS = { CharacteristicsTypes.DENSITY_SO2: "sensor", CharacteristicsTypes.DENSITY_VOC: "sensor", CharacteristicsTypes.IDENTIFY: "button", + CharacteristicsTypes.THREAD_NODE_CAPABILITIES: "sensor", } diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index cddcbc59cde..ecfad477d00 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -5,6 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes +from aiohomekit.model.characteristics.const import ThreadNodeCapabilities from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.sensor import ( @@ -27,6 +28,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType @@ -40,6 +42,45 @@ class HomeKitSensorEntityDescription(SensorEntityDescription): """Describes Homekit sensor.""" probe: Callable[[Characteristic], bool] | None = None + format: Callable[[Characteristic], str] | None = None + + +def thread_node_capability_to_str(char: Characteristic) -> str: + """ + Return the thread device type as a string. + + The underlying value is a bitmask, but we want to turn that to + a human readable string. Some devices will have multiple capabilities. + For example, an NL55 is SLEEPY | MINIMAL. In that case we return the + "best" capability. + + https://openthread.io/guides/thread-primer/node-roles-and-types + """ + + val = ThreadNodeCapabilities(char.value) + + if val & ThreadNodeCapabilities.BORDER_ROUTER_CAPABLE: + # can act as a bridge between thread network and e.g. WiFi + return "border_router_capable" + + if val & ThreadNodeCapabilities.ROUTER_ELIGIBLE: + # radio always on, can be a router + return "router_eligible" + + if val & ThreadNodeCapabilities.FULL: + # radio always on, but can't be a router + return "full" + + if val & ThreadNodeCapabilities.MINIMAL: + # transceiver always on, does not need to poll for messages from its parent + return "minimal" + + if val & ThreadNodeCapabilities.SLEEPY: + # normally disabled, wakes on occasion to poll for messages from its parent + return "sleepy" + + # Device has no known thread capabilities + return "none" SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { @@ -195,6 +236,13 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, ), + CharacteristicsTypes.THREAD_NODE_CAPABILITIES: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.THREAD_NODE_CAPABILITIES, + name="Thread Capabilities", + device_class="homekit_controller__thread_node_capabilities", + entity_category=EntityCategory.DIAGNOSTIC, + format=thread_node_capability_to_str, + ), } @@ -399,7 +447,10 @@ class SimpleSensor(CharacteristicEntity, SensorEntity): @property def native_value(self) -> str | int | float: """Return the current sensor value.""" - return self._char.value + val = self._char.value + if self.entity_description.format: + return self.entity_description.format(val) + return val ENTITY_TYPES = { diff --git a/homeassistant/components/homekit_controller/strings.sensor.json b/homeassistant/components/homekit_controller/strings.sensor.json new file mode 100644 index 00000000000..d7d8e888a98 --- /dev/null +++ b/homeassistant/components/homekit_controller/strings.sensor.json @@ -0,0 +1,12 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Border Router Capable", + "router_eligible": "Router Eligible End Device", + "full": "Full End Device", + "minimal": "Minimal End Device", + "sleepy": "Sleepy End Device", + "none": "None" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.en.json b/homeassistant/components/homekit_controller/translations/sensor.en.json new file mode 100644 index 00000000000..b1f8a0a8128 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.en.json @@ -0,0 +1,12 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Border Router Capable", + "full": "Full End Device", + "minimal": "Minimal End Device", + "none": "None", + "router_eligible": "Router Eligible End Device", + "sleepy": "Sleepy End Device" + } + } +} \ No newline at end of file diff --git a/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py index 7e6a9bb672b..086027f2427 100644 --- a/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py +++ b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py @@ -50,6 +50,13 @@ async def test_nanoleaf_nl55_setup(hass): entity_category=EntityCategory.DIAGNOSTIC, state="unknown", ), + EntityTestInfo( + entity_id="sensor.nanoleaf_strip_3b32_thread_capabilities", + friendly_name="Nanoleaf Strip 3B32 Thread Capabilities", + unique_id="homekit-AAAA011111111111-aid:1-sid:31-cid:115", + entity_category=EntityCategory.DIAGNOSTIC, + state="border_router_capable", + ), ], ), ) diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index 836da1e466f..c2a466d3997 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -1,8 +1,12 @@ """Basic checks for HomeKit sensor.""" from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.characteristics.const import ThreadNodeCapabilities from aiohomekit.model.services import ServicesTypes from aiohomekit.protocol.statuscodes import HapStatusCode +from homeassistant.components.homekit_controller.sensor import ( + thread_node_capability_to_str, +) from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from tests.components.homekit_controller.common import Helper, setup_test_component @@ -315,3 +319,19 @@ async def test_sensor_unavailable(hass, utcnow): # Energy sensor has non-responsive characteristics so should be unavailable state = await energy_helper.poll_and_get_state() assert state.state == "unavailable" + + +def test_thread_node_caps_to_str(): + """Test all values of this enum get a translatable string.""" + assert ( + thread_node_capability_to_str(ThreadNodeCapabilities.BORDER_ROUTER_CAPABLE) + == "border_router_capable" + ) + assert ( + thread_node_capability_to_str(ThreadNodeCapabilities.ROUTER_ELIGIBLE) + == "router_eligible" + ) + assert thread_node_capability_to_str(ThreadNodeCapabilities.FULL) == "full" + assert thread_node_capability_to_str(ThreadNodeCapabilities.MINIMAL) == "minimal" + assert thread_node_capability_to_str(ThreadNodeCapabilities.SLEEPY) == "sleepy" + assert thread_node_capability_to_str(ThreadNodeCapabilities(128)) == "none" From 3388248eb52f5eb62cfe892f670e35dc058500c6 Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Thu, 4 Aug 2022 00:58:30 +0300 Subject: [PATCH 3120/3516] Fix prettier on HomeKit Controller (#76168) --- .../homekit_controller/strings.sensor.json | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/homekit_controller/strings.sensor.json b/homeassistant/components/homekit_controller/strings.sensor.json index d7d8e888a98..4a77fa1668a 100644 --- a/homeassistant/components/homekit_controller/strings.sensor.json +++ b/homeassistant/components/homekit_controller/strings.sensor.json @@ -1,12 +1,12 @@ { - "state": { - "homekit_controller__thread_node_capabilities": { - "border_router_capable": "Border Router Capable", - "router_eligible": "Router Eligible End Device", - "full": "Full End Device", - "minimal": "Minimal End Device", - "sleepy": "Sleepy End Device", - "none": "None" - } + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Border Router Capable", + "router_eligible": "Router Eligible End Device", + "full": "Full End Device", + "minimal": "Minimal End Device", + "sleepy": "Sleepy End Device", + "none": "None" } -} \ No newline at end of file + } +} From 847f150a78a38b5623a2d71dfe835698237f1ac2 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 3 Aug 2022 16:23:42 -0600 Subject: [PATCH 3121/3516] Modify RainMachine to store a single dataclass in `hass.data` (#75460) * Modify RainMachine to store a single dataclass in `hass.data` * Pass one object around instead of multiple --- .../components/rainmachine/__init__.py | 61 +++++++------ .../components/rainmachine/binary_sensor.py | 28 +++--- homeassistant/components/rainmachine/const.py | 2 - .../components/rainmachine/diagnostics.py | 20 ++--- homeassistant/components/rainmachine/model.py | 23 ++++- .../components/rainmachine/sensor.py | 90 ++++++++----------- .../components/rainmachine/switch.py | 62 ++++++------- 7 files changed, 142 insertions(+), 144 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 3feeac7a827..c30ce81dc6d 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass from datetime import timedelta from functools import partial -from typing import Any, cast +from typing import Any from regenmaschine import Client from regenmaschine.controller import Controller @@ -28,7 +29,7 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry as er, ) -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -39,8 +40,6 @@ from homeassistant.util.network import is_ip_address from .config_flow import get_client_controller from .const import ( CONF_ZONE_RUN_TIME, - DATA_CONTROLLER, - DATA_COORDINATOR, DATA_PROGRAMS, DATA_PROVISION_SETTINGS, DATA_RESTRICTIONS_CURRENT, @@ -49,6 +48,7 @@ from .const import ( DOMAIN, LOGGER, ) +from .model import RainMachineEntityDescription DEFAULT_SSL = True @@ -135,6 +135,14 @@ SERVICE_RESTRICT_WATERING_SCHEMA = SERVICE_SCHEMA.extend( ) +@dataclass +class RainMachineData: + """Define an object to be stored in `hass.data`.""" + + controller: Controller + coordinators: dict[str, DataUpdateCoordinator] + + @callback def async_get_controller_for_service_call( hass: HomeAssistant, call: ServiceCall @@ -146,9 +154,8 @@ def async_get_controller_for_service_call( if device_entry := device_registry.async_get(device_id): for entry in hass.config_entries.async_entries(DOMAIN): if entry.entry_id in device_entry.config_entries: - return cast( - Controller, hass.data[DOMAIN][entry.entry_id][DATA_CONTROLLER] - ) + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + return data.controller raise ValueError(f"No controller for device ID: {device_id}") @@ -161,14 +168,12 @@ async def async_update_programs_and_zones( Program and zone updates always go together because of how linked they are: programs affect zones and certain combinations of zones affect programs. """ + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + await asyncio.gather( *[ - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][ - DATA_PROGRAMS - ].async_refresh(), - hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR][ - DATA_ZONES - ].async_refresh(), + data.coordinators[DATA_PROGRAMS].async_refresh(), + data.coordinators[DATA_ZONES].async_refresh(), ] ) @@ -250,10 +255,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await asyncio.gather(*controller_init_tasks) hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = { - DATA_CONTROLLER: controller, - DATA_COORDINATOR: coordinators, - } + hass.data[DOMAIN][entry.entry_id] = RainMachineData( + controller=controller, coordinators=coordinators + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -406,28 +410,27 @@ class RainMachineEntity(CoordinatorEntity): def __init__( self, entry: ConfigEntry, - coordinator: DataUpdateCoordinator, - controller: Controller, - description: EntityDescription, + data: RainMachineData, + description: RainMachineEntityDescription, ) -> None: """Initialize.""" - super().__init__(coordinator) + super().__init__(data.coordinators[description.api_category]) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, controller.mac)}, + identifiers={(DOMAIN, data.controller.mac)}, configuration_url=f"https://{entry.data[CONF_IP_ADDRESS]}:{entry.data[CONF_PORT]}", - connections={(dr.CONNECTION_NETWORK_MAC, controller.mac)}, - name=str(controller.name).capitalize(), + connections={(dr.CONNECTION_NETWORK_MAC, data.controller.mac)}, + name=str(data.controller.name).capitalize(), manufacturer="RainMachine", model=( - f"Version {controller.hardware_version} " - f"(API: {controller.api_version})" + f"Version {data.controller.hardware_version} " + f"(API: {data.controller.api_version})" ), - sw_version=controller.software_version, + sw_version=data.controller.software_version, ) self._attr_extra_state_attributes = {} - self._attr_unique_id = f"{controller.mac}_{description.key}" - self._controller = controller + self._attr_unique_id = f"{data.controller.mac}_{description.key}" + self._data = data self.entity_description = description @callback diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 6ba374a28ba..d9448d68f9d 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -10,16 +10,17 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import RainMachineEntity +from . import RainMachineData, RainMachineEntity from .const import ( - DATA_CONTROLLER, - DATA_COORDINATOR, DATA_PROVISION_SETTINGS, DATA_RESTRICTIONS_CURRENT, DATA_RESTRICTIONS_UNIVERSAL, DOMAIN, ) -from .model import RainMachineDescriptionMixinApiCategory +from .model import ( + RainMachineEntityDescription, + RainMachineEntityDescriptionMixinDataKey, +) from .util import key_exists TYPE_FLOW_SENSOR = "flow_sensor" @@ -35,7 +36,9 @@ TYPE_WEEKDAY = "weekday" @dataclass class RainMachineBinarySensorDescription( - BinarySensorEntityDescription, RainMachineDescriptionMixinApiCategory + BinarySensorEntityDescription, + RainMachineEntityDescription, + RainMachineEntityDescriptionMixinDataKey, ): """Describe a RainMachine binary sensor.""" @@ -124,8 +127,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up RainMachine binary sensors based on a config entry.""" - controller = hass.data[DOMAIN][entry.entry_id][DATA_CONTROLLER] - coordinators = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] api_category_sensor_map = { DATA_PROVISION_SETTINGS: ProvisionSettingsBinarySensor, @@ -135,12 +137,10 @@ async def async_setup_entry( async_add_entities( [ - api_category_sensor_map[description.api_category]( - entry, coordinator, controller, description - ) + api_category_sensor_map[description.api_category](entry, data, description) for description in BINARY_SENSOR_DESCRIPTIONS if ( - (coordinator := coordinators[description.api_category]) is not None + (coordinator := data.coordinators[description.api_category]) is not None and coordinator.data and key_exists(coordinator.data, description.data_key) ) @@ -151,6 +151,8 @@ async def async_setup_entry( class CurrentRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity): """Define a binary sensor that handles current restrictions data.""" + entity_description: RainMachineBinarySensorDescription + @callback def update_from_latest_data(self) -> None: """Update the state.""" @@ -171,6 +173,8 @@ class CurrentRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity): class ProvisionSettingsBinarySensor(RainMachineEntity, BinarySensorEntity): """Define a binary sensor that handles provisioning data.""" + entity_description: RainMachineBinarySensorDescription + @callback def update_from_latest_data(self) -> None: """Update the state.""" @@ -181,6 +185,8 @@ class ProvisionSettingsBinarySensor(RainMachineEntity, BinarySensorEntity): class UniversalRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity): """Define a binary sensor that handles universal restrictions data.""" + entity_description: RainMachineBinarySensorDescription + @callback def update_from_latest_data(self) -> None: """Update the state.""" diff --git a/homeassistant/components/rainmachine/const.py b/homeassistant/components/rainmachine/const.py index 56c1660a0ba..f94e7011dce 100644 --- a/homeassistant/components/rainmachine/const.py +++ b/homeassistant/components/rainmachine/const.py @@ -7,8 +7,6 @@ DOMAIN = "rainmachine" CONF_ZONE_RUN_TIME = "zone_run_time" -DATA_CONTROLLER = "controller" -DATA_COORDINATOR = "coordinator" DATA_PROGRAMS = "programs" DATA_PROVISION_SETTINGS = "provision.settings" DATA_RESTRICTIONS_CURRENT = "restrictions.current" diff --git a/homeassistant/components/rainmachine/diagnostics.py b/homeassistant/components/rainmachine/diagnostics.py index e5e249d41f4..58b918c18ee 100644 --- a/homeassistant/components/rainmachine/diagnostics.py +++ b/homeassistant/components/rainmachine/diagnostics.py @@ -3,15 +3,13 @@ from __future__ import annotations from typing import Any -from regenmaschine.controller import Controller - from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_PASSWORD from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DATA_CONTROLLER, DATA_COORDINATOR, DOMAIN +from . import RainMachineData +from .const import DOMAIN TO_REDACT = { CONF_LATITUDE, @@ -24,9 +22,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data = hass.data[DOMAIN][entry.entry_id] - coordinators: dict[str, DataUpdateCoordinator] = data[DATA_COORDINATOR] - controller: Controller = data[DATA_CONTROLLER] + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] return { "entry": { @@ -38,15 +34,15 @@ async def async_get_config_entry_diagnostics( "coordinator": async_redact_data( { api_category: controller.data - for api_category, controller in coordinators.items() + for api_category, controller in data.coordinators.items() }, TO_REDACT, ), "controller": { - "api_version": controller.api_version, - "hardware_version": controller.hardware_version, - "name": controller.name, - "software_version": controller.software_version, + "api_version": data.controller.api_version, + "hardware_version": data.controller.hardware_version, + "name": data.controller.name, + "software_version": data.controller.software_version, }, }, } diff --git a/homeassistant/components/rainmachine/model.py b/homeassistant/components/rainmachine/model.py index 680a47c5d42..9ae99fe247a 100644 --- a/homeassistant/components/rainmachine/model.py +++ b/homeassistant/components/rainmachine/model.py @@ -1,17 +1,32 @@ """Define RainMachine data models.""" from dataclasses import dataclass +from homeassistant.helpers.entity import EntityDescription + @dataclass -class RainMachineDescriptionMixinApiCategory: - """Define an entity description mixin for binary and regular sensors.""" +class RainMachineEntityDescriptionMixinApiCategory: + """Define an entity description mixin to include an API category.""" api_category: str + + +@dataclass +class RainMachineEntityDescriptionMixinDataKey: + """Define an entity description mixin to include a data payload key.""" + data_key: str @dataclass -class RainMachineDescriptionMixinUid: - """Define an entity description mixin for switches.""" +class RainMachineEntityDescriptionMixinUid: + """Define an entity description mixin to include an activity UID.""" uid: int + + +@dataclass +class RainMachineEntityDescription( + EntityDescription, RainMachineEntityDescriptionMixinApiCategory +): + """Describe a RainMachine entity.""" diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 797420b460f..e2e602b945b 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -5,8 +5,6 @@ from dataclasses import dataclass from datetime import datetime, timedelta from typing import Any, cast -from regenmaschine.controller import Controller - from homeassistant.components.sensor import ( RestoreSensor, SensorDeviceClass, @@ -17,15 +15,12 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS, VOLUME_CUBIC_METERS from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import EntityCategory, EntityDescription +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.util.dt import utcnow -from . import RainMachineEntity +from . import RainMachineData, RainMachineEntity from .const import ( - DATA_CONTROLLER, - DATA_COORDINATOR, DATA_PROGRAMS, DATA_PROVISION_SETTINGS, DATA_RESTRICTIONS_UNIVERSAL, @@ -33,8 +28,9 @@ from .const import ( DOMAIN, ) from .model import ( - RainMachineDescriptionMixinApiCategory, - RainMachineDescriptionMixinUid, + RainMachineEntityDescription, + RainMachineEntityDescriptionMixinDataKey, + RainMachineEntityDescriptionMixinUid, ) from .util import RUN_STATE_MAP, RunStates, key_exists @@ -50,21 +46,25 @@ TYPE_ZONE_RUN_COMPLETION_TIME = "zone_run_completion_time" @dataclass -class RainMachineSensorDescriptionApiCategory( - SensorEntityDescription, RainMachineDescriptionMixinApiCategory +class RainMachineSensorDataDescription( + SensorEntityDescription, + RainMachineEntityDescription, + RainMachineEntityDescriptionMixinDataKey, ): """Describe a RainMachine sensor.""" @dataclass -class RainMachineSensorDescriptionUid( - SensorEntityDescription, RainMachineDescriptionMixinUid +class RainMachineSensorCompletionTimerDescription( + SensorEntityDescription, + RainMachineEntityDescription, + RainMachineEntityDescriptionMixinUid, ): """Describe a RainMachine sensor.""" SENSOR_DESCRIPTIONS = ( - RainMachineSensorDescriptionApiCategory( + RainMachineSensorDataDescription( key=TYPE_FLOW_SENSOR_CLICK_M3, name="Flow sensor clicks per cubic meter", icon="mdi:water-pump", @@ -75,7 +75,7 @@ SENSOR_DESCRIPTIONS = ( api_category=DATA_PROVISION_SETTINGS, data_key="flowSensorClicksPerCubicMeter", ), - RainMachineSensorDescriptionApiCategory( + RainMachineSensorDataDescription( key=TYPE_FLOW_SENSOR_CONSUMED_LITERS, name="Flow sensor consumed liters", icon="mdi:water-pump", @@ -86,7 +86,7 @@ SENSOR_DESCRIPTIONS = ( api_category=DATA_PROVISION_SETTINGS, data_key="flowSensorWateringClicks", ), - RainMachineSensorDescriptionApiCategory( + RainMachineSensorDataDescription( key=TYPE_FLOW_SENSOR_START_INDEX, name="Flow sensor start index", icon="mdi:water-pump", @@ -96,7 +96,7 @@ SENSOR_DESCRIPTIONS = ( api_category=DATA_PROVISION_SETTINGS, data_key="flowSensorStartIndex", ), - RainMachineSensorDescriptionApiCategory( + RainMachineSensorDataDescription( key=TYPE_FLOW_SENSOR_WATERING_CLICKS, name="Flow sensor clicks", icon="mdi:water-pump", @@ -107,7 +107,7 @@ SENSOR_DESCRIPTIONS = ( api_category=DATA_PROVISION_SETTINGS, data_key="flowSensorWateringClicks", ), - RainMachineSensorDescriptionApiCategory( + RainMachineSensorDataDescription( key=TYPE_FREEZE_TEMP, name="Freeze protect temperature", icon="mdi:thermometer", @@ -125,8 +125,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up RainMachine sensors based on a config entry.""" - controller = hass.data[DOMAIN][entry.entry_id][DATA_CONTROLLER] - coordinators = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] api_category_sensor_map = { DATA_PROVISION_SETTINGS: ProvisionSettingsSensor, @@ -134,32 +133,29 @@ async def async_setup_entry( } sensors = [ - api_category_sensor_map[description.api_category]( - entry, coordinator, controller, description - ) + api_category_sensor_map[description.api_category](entry, data, description) for description in SENSOR_DESCRIPTIONS if ( - (coordinator := coordinators[description.api_category]) is not None + (coordinator := data.coordinators[description.api_category]) is not None and coordinator.data and key_exists(coordinator.data, description.data_key) ) ] - program_coordinator = coordinators[DATA_PROGRAMS] - zone_coordinator = coordinators[DATA_ZONES] + program_coordinator = data.coordinators[DATA_PROGRAMS] + zone_coordinator = data.coordinators[DATA_ZONES] for uid, program in program_coordinator.data.items(): sensors.append( ProgramTimeRemainingSensor( entry, - program_coordinator, - zone_coordinator, - controller, - RainMachineSensorDescriptionUid( + data, + RainMachineSensorCompletionTimerDescription( key=f"{TYPE_PROGRAM_RUN_COMPLETION_TIME}_{uid}", name=f"{program['name']} Run Completion Time", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, + api_category=DATA_PROGRAMS, uid=uid, ), ) @@ -169,13 +165,13 @@ async def async_setup_entry( sensors.append( ZoneTimeRemainingSensor( entry, - zone_coordinator, - controller, - RainMachineSensorDescriptionUid( + data, + RainMachineSensorCompletionTimerDescription( key=f"{TYPE_ZONE_RUN_COMPLETION_TIME}_{uid}", name=f"{zone['name']} Run Completion Time", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, + api_category=DATA_ZONES, uid=uid, ), ) @@ -187,17 +183,16 @@ async def async_setup_entry( class TimeRemainingSensor(RainMachineEntity, RestoreSensor): """Define a sensor that shows the amount of time remaining for an activity.""" - entity_description: RainMachineSensorDescriptionUid + entity_description: RainMachineSensorCompletionTimerDescription def __init__( self, entry: ConfigEntry, - coordinator: DataUpdateCoordinator, - controller: Controller, - description: EntityDescription, + data: RainMachineData, + description: RainMachineSensorCompletionTimerDescription, ) -> None: """Initialize.""" - super().__init__(entry, coordinator, controller, description) + super().__init__(entry, data, description) self._current_run_state: RunStates | None = None self._previous_run_state: RunStates | None = None @@ -256,19 +251,6 @@ class TimeRemainingSensor(RainMachineEntity, RestoreSensor): class ProgramTimeRemainingSensor(TimeRemainingSensor): """Define a sensor that shows the amount of time remaining for a program.""" - def __init__( - self, - entry: ConfigEntry, - program_coordinator: DataUpdateCoordinator, - zone_coordinator: DataUpdateCoordinator, - controller: Controller, - description: EntityDescription, - ) -> None: - """Initialize.""" - super().__init__(entry, program_coordinator, controller, description) - - self._zone_coordinator = zone_coordinator - @property def status_key(self) -> str: """Return the data key that contains the activity status.""" @@ -277,7 +259,7 @@ class ProgramTimeRemainingSensor(TimeRemainingSensor): def calculate_seconds_remaining(self) -> int: """Calculate the number of seconds remaining.""" return sum( - self._zone_coordinator.data[zone["id"]]["remaining"] + self._data.coordinators[DATA_ZONES].data[zone["id"]]["remaining"] for zone in [z for z in self.activity_data["wateringTimes"] if z["active"]] ) @@ -285,6 +267,8 @@ class ProgramTimeRemainingSensor(TimeRemainingSensor): class ProvisionSettingsSensor(RainMachineEntity, SensorEntity): """Define a sensor that handles provisioning data.""" + entity_description: RainMachineSensorDataDescription + @callback def update_from_latest_data(self) -> None: """Update the state.""" @@ -315,6 +299,8 @@ class ProvisionSettingsSensor(RainMachineEntity, SensorEntity): class UniversalRestrictionsSensor(RainMachineEntity, SensorEntity): """Define a sensor that handles universal restrictions data.""" + entity_description: RainMachineSensorDataDescription + @callback def update_from_latest_data(self) -> None: """Update the state.""" diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 1fcdab49836..ee6ac670840 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -7,7 +7,6 @@ from dataclasses import dataclass from datetime import datetime from typing import Any -from regenmaschine.controller import Controller from regenmaschine.errors import RequestError import voluptuous as vol @@ -19,19 +18,16 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import RainMachineEntity, async_update_programs_and_zones +from . import RainMachineData, RainMachineEntity, async_update_programs_and_zones from .const import ( CONF_ZONE_RUN_TIME, - DATA_CONTROLLER, - DATA_COORDINATOR, DATA_PROGRAMS, DATA_ZONES, DEFAULT_ZONE_RUN, DOMAIN, ) -from .model import RainMachineDescriptionMixinUid +from .model import RainMachineEntityDescription, RainMachineEntityDescriptionMixinUid from .util import RUN_STATE_MAP ATTR_AREA = "area" @@ -110,7 +106,9 @@ VEGETATION_MAP = { @dataclass class RainMachineSwitchDescription( - SwitchEntityDescription, RainMachineDescriptionMixinUid + SwitchEntityDescription, + RainMachineEntityDescription, + RainMachineEntityDescriptionMixinUid, ): """Describe a RainMachine switch.""" @@ -137,30 +135,27 @@ async def async_setup_entry( ): platform.async_register_entity_service(service_name, schema, method) - data = hass.data[DOMAIN][entry.entry_id] - controller = data[DATA_CONTROLLER] - program_coordinator = data[DATA_COORDINATOR][DATA_PROGRAMS] - zone_coordinator = data[DATA_COORDINATOR][DATA_ZONES] + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] entities: list[RainMachineActivitySwitch | RainMachineEnabledSwitch] = [] - - for kind, coordinator, switch_class, switch_enabled_class in ( - ("program", program_coordinator, RainMachineProgram, RainMachineProgramEnabled), - ("zone", zone_coordinator, RainMachineZone, RainMachineZoneEnabled), + for kind, api_category, switch_class, switch_enabled_class in ( + ("program", DATA_PROGRAMS, RainMachineProgram, RainMachineProgramEnabled), + ("zone", DATA_ZONES, RainMachineZone, RainMachineZoneEnabled), ): - for uid, data in coordinator.data.items(): - name = data["name"].capitalize() + coordinator = data.coordinators[api_category] + for uid, activity in coordinator.data.items(): + name = activity["name"].capitalize() # Add a switch to start/stop the program or zone: entities.append( switch_class( entry, - coordinator, - controller, + data, RainMachineSwitchDescription( key=f"{kind}_{uid}", name=name, icon="mdi:water", + api_category=api_category, uid=uid, ), ) @@ -170,13 +165,13 @@ async def async_setup_entry( entities.append( switch_enabled_class( entry, - coordinator, - controller, + data, RainMachineSwitchDescription( key=f"{kind}_{uid}_enabled", name=f"{name} enabled", entity_category=EntityCategory.CONFIG, icon="mdi:cog", + api_category=api_category, uid=uid, ), ) @@ -193,12 +188,11 @@ class RainMachineBaseSwitch(RainMachineEntity, SwitchEntity): def __init__( self, entry: ConfigEntry, - coordinator: DataUpdateCoordinator, - controller: Controller, + data: RainMachineData, description: RainMachineSwitchDescription, ) -> None: """Initialize.""" - super().__init__(entry, coordinator, controller, description) + super().__init__(entry, data, description) self._attr_is_on = False self._entry = entry @@ -299,13 +293,13 @@ class RainMachineProgram(RainMachineActivitySwitch): async def async_turn_off_when_active(self, **kwargs: Any) -> None: """Turn the switch off when its associated activity is active.""" await self._async_run_api_coroutine( - self._controller.programs.stop(self.entity_description.uid) + self._data.controller.programs.stop(self.entity_description.uid) ) async def async_turn_on_when_active(self, **kwargs: Any) -> None: """Turn the switch on when its associated activity is active.""" await self._async_run_api_coroutine( - self._controller.programs.start(self.entity_description.uid) + self._data.controller.programs.start(self.entity_description.uid) ) @callback @@ -342,10 +336,10 @@ class RainMachineProgramEnabled(RainMachineEnabledSwitch): """Disable the program.""" tasks = [ self._async_run_api_coroutine( - self._controller.programs.stop(self.entity_description.uid) + self._data.controller.programs.stop(self.entity_description.uid) ), self._async_run_api_coroutine( - self._controller.programs.disable(self.entity_description.uid) + self._data.controller.programs.disable(self.entity_description.uid) ), ] @@ -354,7 +348,7 @@ class RainMachineProgramEnabled(RainMachineEnabledSwitch): async def async_turn_on(self, **kwargs: Any) -> None: """Enable the program.""" await self._async_run_api_coroutine( - self._controller.programs.enable(self.entity_description.uid) + self._data.controller.programs.enable(self.entity_description.uid) ) @@ -372,13 +366,13 @@ class RainMachineZone(RainMachineActivitySwitch): async def async_turn_off_when_active(self, **kwargs: Any) -> None: """Turn the switch off when its associated activity is active.""" await self._async_run_api_coroutine( - self._controller.zones.stop(self.entity_description.uid) + self._data.controller.zones.stop(self.entity_description.uid) ) async def async_turn_on_when_active(self, **kwargs: Any) -> None: """Turn the switch on when its associated activity is active.""" await self._async_run_api_coroutine( - self._controller.zones.start( + self._data.controller.zones.start( self.entity_description.uid, kwargs.get("duration", self._entry.options[CONF_ZONE_RUN_TIME]), ) @@ -426,10 +420,10 @@ class RainMachineZoneEnabled(RainMachineEnabledSwitch): """Disable the zone.""" tasks = [ self._async_run_api_coroutine( - self._controller.zones.stop(self.entity_description.uid) + self._data.controller.zones.stop(self.entity_description.uid) ), self._async_run_api_coroutine( - self._controller.zones.disable(self.entity_description.uid) + self._data.controller.zones.disable(self.entity_description.uid) ), ] @@ -438,5 +432,5 @@ class RainMachineZoneEnabled(RainMachineEnabledSwitch): async def async_turn_on(self, **kwargs: Any) -> None: """Enable the zone.""" await self._async_run_api_coroutine( - self._controller.zones.enable(self.entity_description.uid) + self._data.controller.zones.enable(self.entity_description.uid) ) From 1ff7686160ee24f6e413c521a912beb017002719 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Aug 2022 01:15:56 +0200 Subject: [PATCH 3122/3516] Use attributes in zengge light (#75994) --- homeassistant/components/zengge/light.py | 86 ++++++++---------------- 1 file changed, 29 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/zengge/light.py b/homeassistant/components/zengge/light.py index 6939ecb276b..2d4ba4614e6 100644 --- a/homeassistant/components/zengge/light.py +++ b/homeassistant/components/zengge/light.py @@ -53,49 +53,28 @@ def setup_platform( class ZenggeLight(LightEntity): """Representation of a Zengge light.""" + _attr_supported_color_modes = {ColorMode.HS, ColorMode.WHITE} + def __init__(self, device): """Initialize the light.""" - self._name = device["name"] - self._address = device["address"] + self._attr_name = device["name"] + self._attr_unique_id = device["address"] self.is_valid = True - self._bulb = zengge(self._address) + self._bulb = zengge(device["address"]) self._white = 0 - self._brightness = 0 - self._hs_color = (0, 0) - self._state = False + self._attr_brightness = 0 + self._attr_hs_color = (0, 0) + self._attr_is_on = False if self._bulb.connect() is False: self.is_valid = False - _LOGGER.error("Failed to connect to bulb %s, %s", self._address, self._name) + _LOGGER.error( + "Failed to connect to bulb %s, %s", device["address"], device["name"] + ) return @property - def unique_id(self): - """Return the ID of this light.""" - return self._address - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - @property - def brightness(self): - """Return the brightness property.""" - return self._brightness - - @property - def hs_color(self): - """Return the color property.""" - return self._hs_color - - @property - def white_value(self): + def white_value(self) -> int: """Return the white property.""" return self._white @@ -106,19 +85,9 @@ class ZenggeLight(LightEntity): return ColorMode.WHITE return ColorMode.HS - @property - def supported_color_modes(self) -> set[ColorMode | str]: - """Flag supported color modes.""" - return {ColorMode.HS, ColorMode.WHITE} - - @property - def assumed_state(self): - """We can report the actual state.""" - return False - - def _set_rgb(self, red, green, blue): + def _set_rgb(self, red: int, green: int, blue: int) -> None: """Set the rgb state.""" - return self._bulb.set_rgb(red, green, blue) + self._bulb.set_rgb(red, green, blue) def _set_white(self, white): """Set the white state.""" @@ -126,7 +95,7 @@ class ZenggeLight(LightEntity): def turn_on(self, **kwargs: Any) -> None: """Turn the specified light on.""" - self._state = True + self._attr_is_on = True self._bulb.on() hs_color = kwargs.get(ATTR_HS_COLOR) @@ -135,37 +104,40 @@ class ZenggeLight(LightEntity): if white is not None: # Change the bulb to white - self._brightness = self._white = white - self._hs_color = (0, 0) + self._attr_brightness = white + self._white = white + self._attr_hs_color = (0, 0) if hs_color is not None: # Change the bulb to hs self._white = 0 - self._hs_color = hs_color + self._attr_hs_color = hs_color if brightness is not None: - self._brightness = brightness + self._attr_brightness = brightness if self._white != 0: - self._set_white(self._brightness) + self._set_white(self.brightness) else: + assert self.hs_color is not None + assert self.brightness is not None rgb = color_util.color_hsv_to_RGB( - self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100 + self.hs_color[0], self.hs_color[1], self.brightness / 255 * 100 ) self._set_rgb(*rgb) def turn_off(self, **kwargs: Any) -> None: """Turn the specified light off.""" - self._state = False + self._attr_is_on = False self._bulb.off() def update(self) -> None: """Synchronise internal state with the actual light state.""" rgb = self._bulb.get_colour() hsv = color_util.color_RGB_to_hsv(*rgb) - self._hs_color = hsv[:2] - self._brightness = (hsv[2] / 100) * 255 + self._attr_hs_color = hsv[:2] + self._attr_brightness = int((hsv[2] / 100) * 255) self._white = self._bulb.get_white() if self._white: - self._brightness = self._white - self._state = self._bulb.get_on() + self._attr_brightness = self._white + self._attr_is_on = self._bulb.get_on() From e2e277490bb2064a40f684975b42c370cc26716e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 4 Aug 2022 00:27:38 +0000 Subject: [PATCH 3123/3516] [ci skip] Translation update --- .../alarm_control_panel/translations/uk.json | 1 + .../components/ambee/translations/ru.json | 6 ++++++ .../components/anthemav/translations/ja.json | 1 + .../components/anthemav/translations/ru.json | 6 ++++++ .../components/google/translations/ja.json | 1 + .../lacrosse_view/translations/ru.json | 20 +++++++++++++++++++ .../lg_soundbar/translations/it.json | 2 +- .../lg_soundbar/translations/pl.json | 2 +- .../components/lyric/translations/ja.json | 1 + .../components/mitemp_bt/translations/hu.json | 2 +- .../components/mitemp_bt/translations/it.json | 2 +- .../components/mitemp_bt/translations/ja.json | 7 +++++++ .../components/mitemp_bt/translations/no.json | 2 +- .../components/nest/translations/it.json | 2 ++ .../components/nest/translations/ja.json | 9 +++++++++ .../components/nest/translations/pl.json | 10 ++++++++++ .../components/nest/translations/ru.json | 10 ++++++++++ .../openalpr_local/translations/ja.json | 1 + .../openalpr_local/translations/ru.json | 8 ++++++++ .../opentherm_gw/translations/ja.json | 3 ++- .../opentherm_gw/translations/ru.json | 3 ++- .../radiotherm/translations/ja.json | 1 + .../simplepush/translations/ja.json | 1 + .../simplepush/translations/ru.json | 6 ++++++ .../soundtouch/translations/ja.json | 1 + .../soundtouch/translations/ru.json | 6 ++++++ .../components/spotify/translations/ja.json | 1 + .../steam_online/translations/ja.json | 1 + .../components/xbox/translations/ja.json | 1 + .../components/xbox/translations/ru.json | 6 ++++++ .../xiaomi_ble/translations/ca.json | 5 +++++ .../xiaomi_ble/translations/de.json | 8 ++++++++ .../xiaomi_ble/translations/hu.json | 8 ++++++++ .../xiaomi_ble/translations/it.json | 14 ++++++++++++- .../xiaomi_ble/translations/ja.json | 8 +++++++- .../xiaomi_ble/translations/no.json | 10 +++++++++- .../xiaomi_ble/translations/pl.json | 14 ++++++++++++- .../xiaomi_ble/translations/pt-BR.json | 10 +++++++++- .../xiaomi_ble/translations/ru.json | 14 ++++++++++++- .../xiaomi_ble/translations/zh-Hant.json | 8 ++++++++ 40 files changed, 209 insertions(+), 13 deletions(-) create mode 100644 homeassistant/components/lacrosse_view/translations/ru.json create mode 100644 homeassistant/components/mitemp_bt/translations/ja.json create mode 100644 homeassistant/components/openalpr_local/translations/ru.json diff --git a/homeassistant/components/alarm_control_panel/translations/uk.json b/homeassistant/components/alarm_control_panel/translations/uk.json index b50fd9f459d..de28faca361 100644 --- a/homeassistant/components/alarm_control_panel/translations/uk.json +++ b/homeassistant/components/alarm_control_panel/translations/uk.json @@ -29,6 +29,7 @@ "armed_custom_bypass": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 \u0437 \u0432\u0438\u043d\u044f\u0442\u043a\u0430\u043c\u0438", "armed_home": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u0412\u0434\u043e\u043c\u0430)", "armed_night": "\u041d\u0456\u0447\u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430", + "armed_vacation": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u0432\u0456\u0434\u043f\u0443\u0441\u0442\u043a\u0430)", "arming": "\u0421\u0442\u0430\u0432\u043b\u044e \u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0443", "disarmed": "\u0417\u043d\u044f\u0442\u043e \u0437 \u043e\u0445\u043e\u0440\u043e\u043d\u0438", "disarming": "\u0417\u043d\u044f\u0442\u0442\u044f", diff --git a/homeassistant/components/ambee/translations/ru.json b/homeassistant/components/ambee/translations/ru.json index c229c2d6020..11b3cbbf9d2 100644 --- a/homeassistant/components/ambee/translations/ru.json +++ b/homeassistant/components/ambee/translations/ru.json @@ -24,5 +24,11 @@ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Ambee." } } + }, + "issues": { + "pending_removal": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Ambee \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10. \n\n\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e Ambee \u0443\u0434\u0430\u043b\u0438\u043b\u0430 \u0441\u0432\u043e\u0438 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u044b\u0435 (\u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u043d\u044b\u0435) \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u044b \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u043e\u0431\u044b\u0447\u043d\u044b\u043c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f\u043c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u043f\u043e\u0434\u043f\u0438\u0441\u0430\u0442\u044c\u0441\u044f \u043d\u0430 \u043f\u043b\u0430\u0442\u043d\u044b\u0439 \u0442\u0430\u0440\u0438\u0444\u043d\u044b\u0439 \u043f\u043b\u0430\u043d.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u0443\u044e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Ambee \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/ja.json b/homeassistant/components/anthemav/translations/ja.json index b55e8b2b030..2081a17914d 100644 --- a/homeassistant/components/anthemav/translations/ja.json +++ b/homeassistant/components/anthemav/translations/ja.json @@ -18,6 +18,7 @@ }, "issues": { "deprecated_yaml": { + "description": "Anthem A/V Receivers\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Anthem A/V Receivers\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Anthem A/V Receivers YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/anthemav/translations/ru.json b/homeassistant/components/anthemav/translations/ru.json index 0f343609e4c..e55ad7100e7 100644 --- a/homeassistant/components/anthemav/translations/ru.json +++ b/homeassistant/components/anthemav/translations/ru.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AV-\u0440\u0435\u0441\u0438\u0432\u0435\u0440\u043e\u0432 Anthem \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AV-\u0440\u0435\u0441\u0438\u0432\u0435\u0440\u043e\u0432 Anthem \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/ja.json b/homeassistant/components/google/translations/ja.json index 9507f32812d..7ab3209ac1c 100644 --- a/homeassistant/components/google/translations/ja.json +++ b/homeassistant/components/google/translations/ja.json @@ -35,6 +35,7 @@ }, "issues": { "deprecated_yaml": { + "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Google\u30ab\u30ec\u30f3\u30c0\u30fc\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Google\u30ab\u30ec\u30f3\u30c0\u30fcyaml\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" }, "removed_track_new_yaml": { diff --git a/homeassistant/components/lacrosse_view/translations/ru.json b/homeassistant/components/lacrosse_view/translations/ru.json new file mode 100644 index 00000000000..931b4c32274 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "no_locations": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/it.json b/homeassistant/components/lg_soundbar/translations/it.json index c9f58f15aa2..30a7b328038 100644 --- a/homeassistant/components/lg_soundbar/translations/it.json +++ b/homeassistant/components/lg_soundbar/translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "existing_instance_updated": "Configurazione esistente aggiornata." }, "error": { diff --git a/homeassistant/components/lg_soundbar/translations/pl.json b/homeassistant/components/lg_soundbar/translations/pl.json index 1b84366cfa4..4a6b3d077df 100644 --- a/homeassistant/components/lg_soundbar/translations/pl.json +++ b/homeassistant/components/lg_soundbar/translations/pl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119" }, "error": { diff --git a/homeassistant/components/lyric/translations/ja.json b/homeassistant/components/lyric/translations/ja.json index 5394e978c27..bd8b4b93442 100644 --- a/homeassistant/components/lyric/translations/ja.json +++ b/homeassistant/components/lyric/translations/ja.json @@ -20,6 +20,7 @@ }, "issues": { "removed_yaml": { + "description": "Honeywell Lyric\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Honeywell Lyric YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/mitemp_bt/translations/hu.json b/homeassistant/components/mitemp_bt/translations/hu.json index 7e6b52a25ee..c970c4b4bd8 100644 --- a/homeassistant/components/mitemp_bt/translations/hu.json +++ b/homeassistant/components/mitemp_bt/translations/hu.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "A Xiaomi Mijia BLE h\u0151m\u00e9rs\u00e9klet- \u00e9s p\u00e1ratartalom-\u00e9rz\u00e9kel\u0151 integr\u00e1ci\u00f3ja nem le\u00e1llt a 2022.7-es Home Assistantban, \u00e9s a 2022.8-as kiad\u00e1sban a Xiaomi BLE integr\u00e1ci\u00f3val v\u00e1ltott\u00e1k fel.\n\nNincs lehet\u0151s\u00e9g migr\u00e1ci\u00f3ra, ez\u00e9rt a Xiaomi Mijia BLE eszk\u00f6zt az \u00faj integr\u00e1ci\u00f3 haszn\u00e1lat\u00e1val manu\u00e1lisan \u00fajra be kell \u00e1ll\u00edtani.\n\nA megl\u00e9v\u0151 Xiaomi Mijia BLE h\u0151m\u00e9rs\u00e9klet- \u00e9s p\u00e1ratartalom \u00e9rz\u00e9kel\u0151 YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant m\u00e1r nem haszn\u00e1lja. A probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "description": "A Xiaomi Mijia BLE h\u0151m\u00e9rs\u00e9klet- \u00e9s p\u00e1ratartalom-\u00e9rz\u00e9kel\u0151 integr\u00e1ci\u00f3ja le\u00e1llt a 2022.7-es Home Assistantban, \u00e9s a 2022.8-as kiad\u00e1sban a Xiaomi BLE integr\u00e1ci\u00f3val v\u00e1ltott\u00e1k fel.\n\nNincs lehet\u0151s\u00e9g migr\u00e1ci\u00f3ra, ez\u00e9rt a Xiaomi Mijia BLE eszk\u00f6zt az \u00faj integr\u00e1ci\u00f3 haszn\u00e1lat\u00e1val manu\u00e1lisan \u00fajra be kell \u00e1ll\u00edtani.\n\nA megl\u00e9v\u0151 Xiaomi Mijia BLE h\u0151m\u00e9rs\u00e9klet- \u00e9s p\u00e1ratartalom \u00e9rz\u00e9kel\u0151 YAML konfigur\u00e1ci\u00f3j\u00e1t a Home Assistant m\u00e1r nem haszn\u00e1lja. A probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", "title": "A Xiaomi Mijia BLE h\u0151m\u00e9rs\u00e9klet- \u00e9s p\u00e1ratartalom-\u00e9rz\u00e9kel\u0151 integr\u00e1ci\u00f3ja lecser\u00e9l\u0151d\u00f6tt" } } diff --git a/homeassistant/components/mitemp_bt/translations/it.json b/homeassistant/components/mitemp_bt/translations/it.json index 0fe7ab58919..68738725251 100644 --- a/homeassistant/components/mitemp_bt/translations/it.json +++ b/homeassistant/components/mitemp_bt/translations/it.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor ha smesso di funzionare in Home Assistant 2022.7 ed \u00e8 stata sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Xiaomi Mijia BLE utilizzando la nuova integrazione. \n\nLa configurazione YAML di Xiaomi Mijia BLE Temperature and Humidity Sensor esistente non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "description": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor ha smesso di funzionare in Home Assistant 2022.7 ed \u00e8 stata sostituita dall'integrazione Xiaomi BLE nella versione 2022.8. \n\nNon esiste un percorso di migrazione possibile, quindi devi aggiungere manualmente il tuo dispositivo Xiaomi Mijia BLE utilizzando la nuova integrazione. \n\nLa configurazione YAML esistente di Xiaomi Mijia BLE Temperature and Humidity Sensor non \u00e8 pi\u00f9 utilizzata da Home Assistant. Rimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "L'integrazione Xiaomi Mijia BLE Temperature and Humidity Sensor \u00e8 stata sostituita" } } diff --git a/homeassistant/components/mitemp_bt/translations/ja.json b/homeassistant/components/mitemp_bt/translations/ja.json new file mode 100644 index 00000000000..11212382f1f --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/ja.json @@ -0,0 +1,7 @@ +{ + "issues": { + "replaced": { + "title": "Xiaomi Mijia BLE\u6e29\u5ea6\u304a\u3088\u3073\u6e7f\u5ea6\u30bb\u30f3\u30b5\u30fc\u306e\u7d71\u5408\u306f\u3001\u30ea\u30d7\u30ec\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/no.json b/homeassistant/components/mitemp_bt/translations/no.json index 248f44b29af..de80b592212 100644 --- a/homeassistant/components/mitemp_bt/translations/no.json +++ b/homeassistant/components/mitemp_bt/translations/no.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "Xiaomi Mijia BLE temperatur- og fuktighetssensorintegrasjonen sluttet \u00e5 fungere i Home Assistant 2022.7 og ble erstattet av Xiaomi BLE-integrasjonen i 2022.8-utgivelsen. \n\n Det er ingen migreringsbane mulig, derfor m\u00e5 du legge til Xiaomi Mijia BLE-enheten ved \u00e5 bruke den nye integrasjonen manuelt. \n\n Din eksisterende Xiaomi Mijia BLE temperatur- og fuktighetssensor YAML-konfigurasjon brukes ikke lenger av Home Assistant. Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "description": "Xiaomi Mijia BLE Temperatur- og Fuktighetssensorintegrasjon sluttet \u00e5 fungere i Home Assistant 2022.7 og ble erstattet av Xiaomi BLE-integrasjonen i 2022.8-utgivelsen.\n\nDet er ingen migreringsvei mulig, derfor m\u00e5 du legge til Xiaomi Mijia BLE-enheten din ved hjelp av den nye integrasjonen manuelt.\n\nDin eksisterende Xiaomi Mijia BLE-temperatur- og fuktighetssensor YAML-konfigurasjon brukes ikke lenger av Home Assistant. Fjern YAML-konfigurasjonen fra configuration.yaml-filen, og start Home Assistant p\u00e5 nytt for \u00e5 l\u00f8se dette problemet.", "title": "Xiaomi Mijia BLE temperatur- og fuktighetssensorintegrasjon er erstattet" } } diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 6771cd00431..73095bf0930 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -99,9 +99,11 @@ }, "issues": { "deprecated_yaml": { + "description": "La configurazione di Nest in configuration.yaml sar\u00e0 rimossa in Home Assistant 2022.10. \n\nLe credenziali dell'applicazione OAuth esistenti e le impostazioni di accesso sono state importate automaticamente nell'interfaccia utente. Rimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "La configurazione YAML di Nest sar\u00e0 rimossa" }, "removed_app_auth": { + "description": "Per migliorare la sicurezza e ridurre il rischio di phishing, Google ha deprecato il metodo di autenticazione utilizzato da Home Assistant. \n\n **Ci\u00f2 richiede un'azione da parte tua per risolverlo** ([maggiori informazioni]({more_info_url})) \n\n 1. Visita la pagina delle integrazioni\n 1. Fare clic su Riconfigura sull'integrazione Nest.\n 1. Home Assistant ti guider\u00e0 attraverso i passaggi per l'aggiornamento all'autenticazione Web. \n\n Consulta le [istruzioni per l'integrazione]({documentation_url}) di Nest per informazioni sulla risoluzione dei problemi.", "title": "Le credenziali di autenticazione Nest devono essere aggiornate" } } diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index 55d9b9a0348..5509cd497eb 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -89,5 +89,14 @@ "camera_sound": "\u97f3\u304c\u691c\u51fa\u3055\u308c\u307e\u3057\u305f", "doorbell_chime": "\u30c9\u30a2\u30d9\u30eb\u304c\u62bc\u3055\u308c\u305f" } + }, + "issues": { + "deprecated_yaml": { + "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Nest\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.10\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "title": "Nest YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "removed_app_auth": { + "title": "Nest\u8a8d\u8a3c\u8cc7\u683c\u60c5\u5831\u3092\u66f4\u65b0\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json index c7a5c88b120..a0b879ccc90 100644 --- a/homeassistant/components/nest/translations/pl.json +++ b/homeassistant/components/nest/translations/pl.json @@ -96,5 +96,15 @@ "camera_sound": "nast\u0105pi wykrycie d\u017awi\u0119ku", "doorbell_chime": "dzwonek zostanie wci\u015bni\u0119ty" } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Nest w configuration.yaml zostanie usuni\u0119ta w Home Assistant 2022.10. \n\nTwoje istniej\u0105ce po\u015bwiadczenia aplikacji OAuth i ustawienia dost\u0119pu zosta\u0142y automatycznie zaimportowane do interfejsu u\u017cytkownika. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Nest zostanie usuni\u0119ta" + }, + "removed_app_auth": { + "description": "Aby poprawi\u0107 bezpiecze\u0144stwo i zmniejszy\u0107 ryzyko phishingu, Google wycofa\u0142o metod\u0119 uwierzytelniania u\u017cywan\u0105 przez Home Assistanta.\n\n **Wymaga to podj\u0119cia przez Ciebie dzia\u0142ania** ([wi\u0119cej informacji]({more_info_url})) \n\n1. Odwied\u017a stron\u0119 integracji\n2. Kliknij Zmie\u0144 konfiguracj\u0119 w integracji Nest.\n3. Home Assistant przeprowadzi Ci\u0119 przez kolejne etapy aktualizacji do uwierzytelniania internetowego. \n\nZobacz [instrukcj\u0119 integracji] Nest ({documentation_url}), aby uzyska\u0107 informacje na temat rozwi\u0105zywania problem\u00f3w.", + "title": "Dane uwierzytelniaj\u0105ce Nest musz\u0105 zosta\u0107 zaktualizowane" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index e071637713f..71721cda936 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -96,5 +96,15 @@ "camera_sound": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0437\u0432\u0443\u043a", "doorbell_chime": "\u041d\u0430\u0436\u0430\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430 \u0434\u0432\u0435\u0440\u043d\u043e\u0433\u043e \u0437\u0432\u043e\u043d\u043a\u0430" } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Nest \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10.\n\n\u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u044b. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Nest \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + }, + "removed_app_auth": { + "description": "\u0414\u043b\u044f \u043f\u043e\u0432\u044b\u0448\u0435\u043d\u0438\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u0438 \u0438 \u0441\u043d\u0438\u0436\u0435\u043d\u0438\u044f \u0440\u0438\u0441\u043a\u0430 \u0444\u0438\u0448\u0438\u043d\u0433\u0430 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u044f Google \u043e\u0442\u043a\u0430\u0437\u0430\u043b\u0430\u0441\u044c \u043e\u0442 \u043c\u0435\u0442\u043e\u0434\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e Home Assistant.\n\n**\u0414\u043b\u044f \u0443\u0441\u0442\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0412\u0430\u0448\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435** ([\u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435]({more_info_url}))\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Nest.\n3. \u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u043d\u0430 \u0432\u0435\u0431-\u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e.\n\n\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e\u0431 \u0443\u0441\u0442\u0440\u0430\u043d\u0435\u043d\u0438\u0438 \u043d\u0435\u043f\u043e\u043b\u0430\u0434\u043e\u043a \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0438 \u043f\u043e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Nest]({documentation_url}).", + "title": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 Nest Authentication." + } } } \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/ja.json b/homeassistant/components/openalpr_local/translations/ja.json index dbdd930cd10..2353cd9e60f 100644 --- a/homeassistant/components/openalpr_local/translations/ja.json +++ b/homeassistant/components/openalpr_local/translations/ja.json @@ -1,6 +1,7 @@ { "issues": { "pending_removal": { + "description": "OpenALPR \u30ed\u30fc\u30ab\u30eb\u7d71\u5408\u306f\u3001Home Assistant\u304b\u3089\u306e\u524a\u9664\u304c\u4fdd\u7559\u3055\u308c\u3066\u304a\u308a\u3001Home Assistant 2022.10\u4ee5\u964d\u306f\u5229\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002 \n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "OpenALPR Local\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/openalpr_local/translations/ru.json b/homeassistant/components/openalpr_local/translations/ru.json new file mode 100644 index 00000000000..180151e5fd5 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/ru.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f OpenALPR Local \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e YAML \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f OpenALPR Local \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/ja.json b/homeassistant/components/opentherm_gw/translations/ja.json index 16f4d55eac0..ca99567681b 100644 --- a/homeassistant/components/opentherm_gw/translations/ja.json +++ b/homeassistant/components/opentherm_gw/translations/ja.json @@ -3,7 +3,8 @@ "error": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "id_exists": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4ID\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059" + "id_exists": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4ID\u306f\u3059\u3067\u306b\u5b58\u5728\u3057\u307e\u3059", + "timeout_connect": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/ru.json b/homeassistant/components/opentherm_gw/translations/ru.json index e743830624a..448af518cc8 100644 --- a/homeassistant/components/opentherm_gw/translations/ru.json +++ b/homeassistant/components/opentherm_gw/translations/ru.json @@ -3,7 +3,8 @@ "error": { "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "id_exists": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442." + "id_exists": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.", + "timeout_connect": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." }, "step": { "init": { diff --git a/homeassistant/components/radiotherm/translations/ja.json b/homeassistant/components/radiotherm/translations/ja.json index 67f667bacc1..e3cf6571357 100644 --- a/homeassistant/components/radiotherm/translations/ja.json +++ b/homeassistant/components/radiotherm/translations/ja.json @@ -21,6 +21,7 @@ }, "issues": { "deprecated_yaml": { + "description": "YAML\u3092\u4f7f\u7528\u3057\u305f\u3001Radio Thermostat climate(\u6c17\u5019)\u30d7\u30e9\u30c3\u30c8\u30d5\u30a9\u30fc\u30e0\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Radio Thermostat YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } }, diff --git a/homeassistant/components/simplepush/translations/ja.json b/homeassistant/components/simplepush/translations/ja.json index fd22d7dfef5..398fa3d63b0 100644 --- a/homeassistant/components/simplepush/translations/ja.json +++ b/homeassistant/components/simplepush/translations/ja.json @@ -20,6 +20,7 @@ }, "issues": { "deprecated_yaml": { + "description": "Simplepush\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Simplepush\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Simplepush YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/simplepush/translations/ru.json b/homeassistant/components/simplepush/translations/ru.json index 4844f358c82..2ddcba76929 100644 --- a/homeassistant/components/simplepush/translations/ru.json +++ b/homeassistant/components/simplepush/translations/ru.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/ja.json b/homeassistant/components/soundtouch/translations/ja.json index 9bc5e427baf..a6417d9988a 100644 --- a/homeassistant/components/soundtouch/translations/ja.json +++ b/homeassistant/components/soundtouch/translations/ja.json @@ -20,6 +20,7 @@ }, "issues": { "deprecated_yaml": { + "description": "Bose SoundTouch\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Bose SoundTouch\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Bose SoundTouch YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/soundtouch/translations/ru.json b/homeassistant/components/soundtouch/translations/ru.json index d987f817a85..318fe8abef6 100644 --- a/homeassistant/components/soundtouch/translations/ru.json +++ b/homeassistant/components/soundtouch/translations/ru.json @@ -17,5 +17,11 @@ "title": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Bose SoundTouch" } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Bose SoundTouch \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Bose SoundTouch \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/ja.json b/homeassistant/components/spotify/translations/ja.json index ae5f52b68a2..6f1c48c8572 100644 --- a/homeassistant/components/spotify/translations/ja.json +++ b/homeassistant/components/spotify/translations/ja.json @@ -21,6 +21,7 @@ }, "issues": { "removed_yaml": { + "description": "Spotify\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Spotify YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } }, diff --git a/homeassistant/components/steam_online/translations/ja.json b/homeassistant/components/steam_online/translations/ja.json index 46c3eeb7d22..1524e2afc5a 100644 --- a/homeassistant/components/steam_online/translations/ja.json +++ b/homeassistant/components/steam_online/translations/ja.json @@ -26,6 +26,7 @@ }, "issues": { "removed_yaml": { + "description": "Steam\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Steam YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } }, diff --git a/homeassistant/components/xbox/translations/ja.json b/homeassistant/components/xbox/translations/ja.json index f1c31b6c64c..530299b0b24 100644 --- a/homeassistant/components/xbox/translations/ja.json +++ b/homeassistant/components/xbox/translations/ja.json @@ -16,6 +16,7 @@ }, "issues": { "deprecated_yaml": { + "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Xbox\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Xbox YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/xbox/translations/ru.json b/homeassistant/components/xbox/translations/ru.json index 5719a5d9d8a..ca47fd08c8c 100644 --- a/homeassistant/components/xbox/translations/ru.json +++ b/homeassistant/components/xbox/translations/ru.json @@ -13,5 +13,11 @@ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" } } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Xbox \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u044b. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Xbox \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/ca.json b/homeassistant/components/xiaomi_ble/translations/ca.json index 873bcb3f3bb..6dde2ede685 100644 --- a/homeassistant/components/xiaomi_ble/translations/ca.json +++ b/homeassistant/components/xiaomi_ble/translations/ca.json @@ -9,6 +9,11 @@ "no_devices_found": "No s'han trobat dispositius a la xarxa", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, + "error": { + "decryption_failed": "La clau d'enlla\u00e7 proporcionada no ha funcionat, les dades del sensor no s'han pogut desxifrar. Comprova-la i torna-ho a provar.", + "expected_24_characters": "S'espera una clau d'enlla\u00e7 de 24 car\u00e0cters hexadecimals.", + "expected_32_characters": "S'espera una clau d'enlla\u00e7 de 32 car\u00e0cters hexadecimals." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { diff --git a/homeassistant/components/xiaomi_ble/translations/de.json b/homeassistant/components/xiaomi_ble/translations/de.json index 6fc2c74af75..c21a653c4dc 100644 --- a/homeassistant/components/xiaomi_ble/translations/de.json +++ b/homeassistant/components/xiaomi_ble/translations/de.json @@ -9,11 +9,19 @@ "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, + "error": { + "decryption_failed": "Der bereitgestellte Bindkey funktionierte nicht, Sensordaten konnten nicht entschl\u00fcsselt werden. Bitte \u00fcberpr\u00fcfe es und versuche es erneut.", + "expected_24_characters": "Erwartet wird ein 24-stelliger hexadezimaler Bindkey.", + "expected_32_characters": "Erwartet wird ein 32-stelliger hexadezimaler Bindkey." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "M\u00f6chtest du {name} einrichten?" }, + "confirm_slow": { + "description": "Von diesem Ger\u00e4t wurde in der letzten Minute kein Broadcast gesendet, so dass wir nicht sicher sind, ob dieses Ger\u00e4t Verschl\u00fcsselung verwendet oder nicht. Dies kann daran liegen, dass das Ger\u00e4t ein langsames Sendeintervall verwendet. Best\u00e4tige, dass du das Ger\u00e4t trotzdem hinzuf\u00fcgen m\u00f6chtest. Wenn das n\u00e4chste Mal ein Broadcast empfangen wird, wirst du aufgefordert, den Bindkey einzugeben, falls er ben\u00f6tigt wird." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindungsschl\u00fcssel" diff --git a/homeassistant/components/xiaomi_ble/translations/hu.json b/homeassistant/components/xiaomi_ble/translations/hu.json index 3962e890d4f..044f970038b 100644 --- a/homeassistant/components/xiaomi_ble/translations/hu.json +++ b/homeassistant/components/xiaomi_ble/translations/hu.json @@ -9,11 +9,19 @@ "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, + "error": { + "decryption_failed": "A megadott kulcs nem m\u0171k\u00f6d\u00f6tt, az \u00e9rz\u00e9kel\u0151adatokat nem lehetett kiolvasni. K\u00e9rj\u00fck, ellen\u0151rizze \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", + "expected_24_characters": "24 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g.", + "expected_32_characters": "32 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" }, + "confirm_slow": { + "description": "Az elm\u00falt egy percben nem \u00e9rkezett ad\u00e1sjel az eszk\u00f6zt\u0151l, \u00edgy nem az nem \u00e1llap\u00edthat\u00f3 meg egy\u00e9rtelm\u0171en, hogy ez a k\u00e9sz\u00fcl\u00e9k haszn\u00e1l-e titkos\u00edt\u00e1st vagy sem. Ez az\u00e9rt lehet, mert az eszk\u00f6z ritka jelad\u00e1si intervallumot haszn\u00e1l. Meger\u0151s\u00edtheti most az eszk\u00f6z hozz\u00e1ad\u00e1s\u00e1t, de a k\u00f6vetkez\u0151 ad\u00e1sjel fogad\u00e1sakor a rendszer k\u00e9rni fogja, hogy adja meg az eszk\u00f6z kulcs\u00e1t (bindkeyt), ha az sz\u00fcks\u00e9ges." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Kulcs (bindkey)" diff --git a/homeassistant/components/xiaomi_ble/translations/it.json b/homeassistant/components/xiaomi_ble/translations/it.json index 018829bfbd2..bf5ee87b949 100644 --- a/homeassistant/components/xiaomi_ble/translations/it.json +++ b/homeassistant/components/xiaomi_ble/translations/it.json @@ -6,13 +6,22 @@ "decryption_failed": "La chiave di collegamento fornita non funziona, i dati del sensore non possono essere decifrati. Controlla e riprova.", "expected_24_characters": "Prevista una chiave di collegamento esadecimale di 24 caratteri.", "expected_32_characters": "Prevista una chiave di collegamento esadecimale di 32 caratteri.", - "no_devices_found": "Nessun dispositivo trovato sulla rete" + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "decryption_failed": "La chiave di collegamento fornita non funziona, i dati del sensore non possono essere decifrati. Controlla e riprova.", + "expected_24_characters": "Prevista una chiave di collegamento esadecimale di 24 caratteri.", + "expected_32_characters": "Prevista una chiave di collegamento esadecimale di 32 caratteri." }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Vuoi configurare {name}?" }, + "confirm_slow": { + "description": "Non c'\u00e8 stata una trasmissione da questo dispositivo nell'ultimo minuto, quindi non siamo sicuri se questo dispositivo utilizzi la crittografia o meno. Ci\u00f2 potrebbe essere dovuto al fatto che il dispositivo utilizza un intervallo di trasmissione lento. Conferma per aggiungere comunque questo dispositivo, la prossima volta che viene ricevuta una trasmissione ti verr\u00e0 chiesto di inserire la sua chiave di collegamento se necessario." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Chiave di collegamento" @@ -25,6 +34,9 @@ }, "description": "I dati trasmessi dal sensore sono criptati. Per decifrarli \u00e8 necessaria una chiave di collegamento esadecimale di 24 caratteri." }, + "slow_confirm": { + "description": "Non c'\u00e8 stata una trasmissione da questo dispositivo nell'ultimo minuto, quindi non siamo sicuri se questo dispositivo utilizzi la crittografia o meno. Ci\u00f2 potrebbe essere dovuto al fatto che il dispositivo utilizza un intervallo di trasmissione lento. Conferma per aggiungere comunque questo dispositivo, la prossima volta che viene ricevuta una trasmissione ti verr\u00e0 chiesto di inserire la sua chiave di collegamento se necessario." + }, "user": { "data": { "address": "Dispositivo" diff --git a/homeassistant/components/xiaomi_ble/translations/ja.json b/homeassistant/components/xiaomi_ble/translations/ja.json index d15f89bf9d3..597c0ecc3f2 100644 --- a/homeassistant/components/xiaomi_ble/translations/ja.json +++ b/homeassistant/components/xiaomi_ble/translations/ja.json @@ -6,7 +6,13 @@ "decryption_failed": "\u63d0\u4f9b\u3055\u308c\u305f\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u6a5f\u80fd\u305b\u305a\u3001\u30bb\u30f3\u30b5\u30fc \u30c7\u30fc\u30bf\u3092\u5fa9\u53f7\u5316\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u78ba\u8a8d\u306e\u4e0a\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "expected_24_characters": "24\u6587\u5b57\u306716\u9032\u6570\u306a\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002", "expected_32_characters": "32\u6587\u5b57\u304b\u3089\u306a\u308b16\u9032\u6570\u306e\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002", - "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "decryption_failed": "\u63d0\u4f9b\u3055\u308c\u305f\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u6a5f\u80fd\u305b\u305a\u3001\u30bb\u30f3\u30b5\u30fc \u30c7\u30fc\u30bf\u3092\u5fa9\u53f7\u5316\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u78ba\u8a8d\u306e\u4e0a\u3001\u3082\u3046\u4e00\u5ea6\u8a66\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "expected_24_characters": "24\u6587\u5b57\u306716\u9032\u6570\u306a\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002", + "expected_32_characters": "32\u6587\u5b57\u304b\u3089\u306a\u308b16\u9032\u6570\u306e\u30d0\u30a4\u30f3\u30c9\u30ad\u30fc\u304c\u5fc5\u8981\u3067\u3059\u3002" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/xiaomi_ble/translations/no.json b/homeassistant/components/xiaomi_ble/translations/no.json index 6c63f53c6aa..ff428d248d1 100644 --- a/homeassistant/components/xiaomi_ble/translations/no.json +++ b/homeassistant/components/xiaomi_ble/translations/no.json @@ -9,11 +9,19 @@ "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, + "error": { + "decryption_failed": "Den oppgitte bindingsn\u00f8kkelen fungerte ikke, sensordata kunne ikke dekrypteres. Vennligst sjekk det og pr\u00f8v igjen.", + "expected_24_characters": "Forventet en heksadesimal bindingsn\u00f8kkel p\u00e5 24 tegn.", + "expected_32_characters": "Forventet en heksadesimal bindingsn\u00f8kkel p\u00e5 32 tegn." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Vil du konfigurere {name}?" }, + "confirm_slow": { + "description": "Det har ikke v\u00e6rt en sending fra denne enheten det siste minuttet, s\u00e5 vi er ikke sikre p\u00e5 om denne enheten bruker kryptering eller ikke. Dette kan skyldes at enheten bruker et tregt kringkastingsintervall. Bekreft \u00e5 legge til denne enheten uansett, s\u00e5 neste gang en kringkasting mottas, vil du bli bedt om \u00e5 angi bindn\u00f8kkelen hvis det er n\u00f8dvendig." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindkey" @@ -27,7 +35,7 @@ "description": "Sensordataene som sendes av sensoren er kryptert. For \u00e5 dekryptere den trenger vi en heksadesimal bindn\u00f8kkel p\u00e5 24 tegn." }, "slow_confirm": { - "description": "Det har ikke v\u00e6rt en sending fra denne enheten det siste minuttet, s\u00e5 vi er ikke sikre p\u00e5 om denne enheten bruker kryptering eller ikke. Dette kan skyldes at enheten bruker et tregt kringkastingsintervall. Bekreft \u00e5 legge til denne enheten uansett, s\u00e5 neste gang en kringkasting mottas, vil du bli bedt om \u00e5 angi bindn\u00f8kkelen hvis den er n\u00f8dvendig." + "description": "Det har ikke v\u00e6rt en kringkasting fra denne enheten i siste \u00f8yeblikk, s\u00e5 vi er ikke sikre p\u00e5 om denne enheten bruker kryptering eller ikke. Dette kan skyldes at enheten bruker et sakte kringkastingsintervall. Bekreft \u00e5 legge til denne enheten uansett, s\u00e5 neste gang en kringkasting mottas, blir du bedt om \u00e5 angi bindingsn\u00f8kkelen hvis det er n\u00f8dvendig." }, "user": { "data": { diff --git a/homeassistant/components/xiaomi_ble/translations/pl.json b/homeassistant/components/xiaomi_ble/translations/pl.json index 2ca956019ef..7bb0c5da454 100644 --- a/homeassistant/components/xiaomi_ble/translations/pl.json +++ b/homeassistant/components/xiaomi_ble/translations/pl.json @@ -6,13 +6,22 @@ "decryption_failed": "Podany klucz (bindkey) nie zadzia\u0142a\u0142, dane czujnika nie mog\u0142y zosta\u0107 odszyfrowane. Sprawd\u017a go i spr\u00f3buj ponownie.", "expected_24_characters": "Oczekiwano 24-znakowego szesnastkowego klucza bindkey.", "expected_32_characters": "Oczekiwano 32-znakowego szesnastkowego klucza bindkey.", - "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "decryption_failed": "Podany klucz (bindkey) nie zadzia\u0142a\u0142, dane czujnika nie mog\u0142y zosta\u0107 odszyfrowane. Sprawd\u017a go i spr\u00f3buj ponownie.", + "expected_24_characters": "Oczekiwano 24-znakowego szesnastkowego klucza bindkey.", + "expected_32_characters": "Oczekiwano 32-znakowego szesnastkowego klucza bindkey." }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, + "confirm_slow": { + "description": "W ci\u0105gu ostatniej minuty nie by\u0142o transmisji z tego urz\u0105dzenia, wi\u0119c nie jeste\u015bmy pewni, czy to urz\u0105dzenie u\u017cywa szyfrowania, czy nie. Mo\u017ce to by\u0107 spowodowane tym, \u017ce urz\u0105dzenie u\u017cywa wolnego od\u015bwie\u017cania transmisji. Potwierd\u017a, aby mimo wszystko doda\u0107 to urz\u0105dzenie, a przy nast\u0119pnym odebraniu transmisji zostaniesz poproszony o wprowadzenie klucza bindkey, je\u015bli jest to konieczne." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindkey" @@ -25,6 +34,9 @@ }, "description": "Dane przesy\u0142ane przez sensor s\u0105 szyfrowane. Aby je odszyfrowa\u0107, potrzebujemy 24-znakowego szesnastkowego klucza bindkey." }, + "slow_confirm": { + "description": "W ci\u0105gu ostatniej minuty nie by\u0142o transmisji z tego urz\u0105dzenia, wi\u0119c nie jeste\u015bmy pewni, czy to urz\u0105dzenie u\u017cywa szyfrowania, czy nie. Mo\u017ce to by\u0107 spowodowane tym, \u017ce urz\u0105dzenie u\u017cywa wolnego od\u015bwie\u017cania transmisji. Potwierd\u017a, aby mimo wszystko doda\u0107 to urz\u0105dzenie, a przy nast\u0119pnym odebraniu transmisji zostaniesz poproszony o wprowadzenie klucza bindkey, je\u015bli jest to konieczne." + }, "user": { "data": { "address": "Urz\u0105dzenie" diff --git a/homeassistant/components/xiaomi_ble/translations/pt-BR.json b/homeassistant/components/xiaomi_ble/translations/pt-BR.json index b0776a217cd..4702ca14bcc 100644 --- a/homeassistant/components/xiaomi_ble/translations/pt-BR.json +++ b/homeassistant/components/xiaomi_ble/translations/pt-BR.json @@ -9,16 +9,24 @@ "no_devices_found": "Nenhum dispositivo encontrado na rede", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, + "error": { + "decryption_failed": "A bindkey fornecida n\u00e3o funcionou, os dados do sensor n\u00e3o puderam ser descriptografados. Por favor verifique e tente novamente.", + "expected_24_characters": "Esperado uma bindkey hexadecimal de 24 caracteres.", + "expected_32_characters": "Esperado uma bindkey hexadecimal de 32 caracteres." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Deseja configurar {name}?" }, + "confirm_slow": { + "description": "N\u00e3o houve uma transmiss\u00e3o deste dispositivo no \u00faltimo minuto, por isso n\u00e3o temos certeza se este dispositivo usa criptografia ou n\u00e3o. Isso pode ocorrer porque o dispositivo usa um intervalo de transmiss\u00e3o lento. Confirme para adicionar este dispositivo de qualquer maneira e, na pr\u00f3xima vez que uma transmiss\u00e3o for recebida, voc\u00ea ser\u00e1 solicitado a inserir sua bindkey, se necess\u00e1rio." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindkey" }, - "description": "Os dados do sensor transmitidos pelo sensor s\u00e3o criptografados. Para decifr\u00e1-lo, precisamos de uma chave de liga\u00e7\u00e3o hexadecimal de 32 caracteres." + "description": "Os dados do sensor transmitidos pelo sensor s\u00e3o criptografados. Para decifr\u00e1-lo, precisamos de uma bindkey hexadecimal de 32 caracteres." }, "get_encryption_key_legacy": { "data": { diff --git a/homeassistant/components/xiaomi_ble/translations/ru.json b/homeassistant/components/xiaomi_ble/translations/ru.json index a90da71d84e..3c0bf6cce78 100644 --- a/homeassistant/components/xiaomi_ble/translations/ru.json +++ b/homeassistant/components/xiaomi_ble/translations/ru.json @@ -6,13 +6,22 @@ "decryption_failed": "\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438 \u043d\u0435 \u0441\u0440\u0430\u0431\u043e\u0442\u0430\u043b, \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0435\u0433\u043e \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", "expected_24_characters": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 24-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438.", "expected_32_characters": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 32-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438.", - "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "decryption_failed": "\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438 \u043d\u0435 \u0441\u0440\u0430\u0431\u043e\u0442\u0430\u043b, \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430 \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0435\u0433\u043e \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", + "expected_24_characters": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 24-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438.", + "expected_32_characters": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 32-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438." }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" }, + "confirm_slow": { + "description": "\u0412 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0439 \u043c\u0438\u043d\u0443\u0442\u044b \u043e\u0442 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0431\u044b\u043b\u043e \u043d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0448\u0438\u0440\u043e\u043a\u043e\u0432\u0435\u0449\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438\u043b\u0438 \u043d\u0435\u0442. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0441\u0432\u044f\u0437\u0430\u043d\u043e \u0441 \u0442\u0435\u043c, \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043c\u0435\u0434\u043b\u0435\u043d\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0432\u0435\u0449\u0430\u043d\u0438\u044f. \u0415\u0441\u043b\u0438 \u0412\u044b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043f\u0440\u0438 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u043c \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u0448\u0438\u0440\u043e\u043a\u043e\u0432\u0435\u0449\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0412\u0430\u043c \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u043e \u0432\u0432\u0435\u0441\u0442\u0438 \u0435\u0433\u043e \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438, \u0435\u0441\u043b\u0438 \u043e\u043d \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c." + }, "get_encryption_key_4_5": { "data": { "bindkey": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438" @@ -25,6 +34,9 @@ }, "description": "\u041f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0435\u043c\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u043c \u0434\u0430\u043d\u043d\u044b\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u044b. \u0414\u043b\u044f \u0442\u043e\u0433\u043e \u0447\u0442\u043e\u0431\u044b \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0445, \u043d\u0443\u0436\u0435\u043d 24-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438." }, + "slow_confirm": { + "description": "\u0412 \u0442\u0435\u0447\u0435\u043d\u0438\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0439 \u043c\u0438\u043d\u0443\u0442\u044b \u043e\u0442 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0431\u044b\u043b\u043e \u043d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0448\u0438\u0440\u043e\u043a\u043e\u0432\u0435\u0449\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438\u043b\u0438 \u043d\u0435\u0442. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0441\u0432\u044f\u0437\u0430\u043d\u043e \u0441 \u0442\u0435\u043c, \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043c\u0435\u0434\u043b\u0435\u043d\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0432\u0435\u0449\u0430\u043d\u0438\u044f. \u0415\u0441\u043b\u0438 \u0412\u044b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u043f\u0440\u0438 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u043c \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u0448\u0438\u0440\u043e\u043a\u043e\u0432\u0435\u0449\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f \u0412\u0430\u043c \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u0435\u0434\u043b\u043e\u0436\u0435\u043d\u043e \u0432\u0432\u0435\u0441\u0442\u0438 \u0435\u0433\u043e \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0438, \u0435\u0441\u043b\u0438 \u043e\u043d \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c." + }, "user": { "data": { "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" diff --git a/homeassistant/components/xiaomi_ble/translations/zh-Hant.json b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json index 13d0a986b79..fdb720b2777 100644 --- a/homeassistant/components/xiaomi_ble/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_ble/translations/zh-Hant.json @@ -9,11 +9,19 @@ "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, + "error": { + "decryption_failed": "\u6240\u63d0\u4f9b\u7684\u7d81\u5b9a\u78bc\u7121\u6cd5\u4f7f\u7528\u3001\u50b3\u611f\u5668\u8cc7\u6599\u7121\u6cd5\u89e3\u5bc6\u3002\u8acb\u4fee\u6b63\u5f8c\u3001\u518d\u8a66\u4e00\u6b21\u3002", + "expected_24_characters": "\u9700\u8981 24 \u500b\u5b57\u5143\u4e4b\u5341\u516d\u9032\u4f4d\u7d81\u5b9a\u78bc\u3002", + "expected_32_characters": "\u9700\u8981 32 \u500b\u5b57\u5143\u4e4b\u5341\u516d\u9032\u4f4d\u7d81\u5b9a\u78bc\u3002" + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, + "confirm_slow": { + "description": "\u8a72\u88dd\u7f6e\u65bc\u904e\u53bb\u4e00\u5206\u9418\u5167\u3001\u672a\u9032\u884c\u4efb\u4f55\u72c0\u614b\u5ee3\u64ad\uff0c\u56e0\u6b64\u7121\u6cd5\u78ba\u5b9a\u88dd\u7f6e\u662f\u5426\u4f7f\u7528\u52a0\u5bc6\u901a\u8a0a\u3002\u4e5f\u53ef\u80fd\u56e0\u70ba\u88dd\u7f6e\u7684\u66f4\u65b0\u983b\u7387\u8f03\u6162\u3002\u78ba\u8a8d\u9084\u662f\u8981\u65b0\u589e\u6b64\u88dd\u7f6e\u3001\u65bc\u4e0b\u6b21\u6536\u5230\u88dd\u7f6e\u5ee3\u64ad\u6642\uff0c\u5982\u679c\u9700\u8981\u3001\u5c07\u63d0\u793a\u60a8\u8f38\u5165\u7d81\u5b9a\u78bc\u3002" + }, "get_encryption_key_4_5": { "data": { "bindkey": "\u7d81\u5b9a\u78bc" From 22eba6ce1ba3611421c526b61432ac90592eff08 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Aug 2022 08:13:20 +0200 Subject: [PATCH 3124/3516] Remove attribution from extra state attributes (#76172) --- homeassistant/components/aemet/sensor.py | 3 +-- homeassistant/components/agent_dvr/camera.py | 3 +-- homeassistant/components/airly/sensor.py | 4 ++-- homeassistant/components/airnow/sensor.py | 5 +++-- homeassistant/components/alpha_vantage/sensor.py | 8 +++++--- homeassistant/components/aurora/__init__.py | 2 +- homeassistant/components/co2signal/sensor.py | 4 ++-- homeassistant/components/worldtidesinfo/sensor.py | 12 ++++-------- homeassistant/components/wsdot/sensor.py | 4 ++-- homeassistant/components/zamg/sensor.py | 3 +-- 10 files changed, 22 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/aemet/sensor.py b/homeassistant/components/aemet/sensor.py index e34583148e1..42cc005dcdd 100644 --- a/homeassistant/components/aemet/sensor.py +++ b/homeassistant/components/aemet/sensor.py @@ -3,7 +3,6 @@ from __future__ import annotations from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -63,7 +62,7 @@ async def async_setup_entry( class AbstractAemetSensor(CoordinatorEntity[WeatherUpdateCoordinator], SensorEntity): """Abstract class for an AEMET OpenData sensor.""" - _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} + _attr_attribution = ATTRIBUTION def __init__( self, diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py index 9aab33efd5a..e99f1ecf223 100644 --- a/homeassistant/components/agent_dvr/camera.py +++ b/homeassistant/components/agent_dvr/camera.py @@ -7,7 +7,6 @@ from agent import AgentError from homeassistant.components.camera import CameraEntityFeature from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import ( @@ -70,6 +69,7 @@ async def async_setup_entry( class AgentCamera(MjpegCamera): """Representation of an Agent Device Stream.""" + _attr_attribution = ATTRIBUTION _attr_supported_features = CameraEntityFeature.ON_OFF def __init__(self, device): @@ -108,7 +108,6 @@ class AgentCamera(MjpegCamera): self._attr_icon = "mdi:camcorder" self._attr_available = self.device.client.is_available self._attr_extra_state_attributes = { - ATTR_ATTRIBUTION: ATTRIBUTION, "editable": False, "enabled": self.is_on, "connected": self.connected, diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index eeb0037c814..a1c9f8a3057 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -13,7 +13,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ATTRIBUTION, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONF_NAME, PERCENTAGE, @@ -136,6 +135,7 @@ async def async_setup_entry( class AirlySensor(CoordinatorEntity[AirlyDataUpdateCoordinator], SensorEntity): """Define an Airly sensor.""" + _attr_attribution = ATTRIBUTION _attr_has_entity_name = True entity_description: AirlySensorEntityDescription @@ -159,7 +159,7 @@ class AirlySensor(CoordinatorEntity[AirlyDataUpdateCoordinator], SensorEntity): self._attr_unique_id = ( f"{coordinator.latitude}-{coordinator.longitude}-{description.key}".lower() ) - self._attrs: dict[str, Any] = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attrs: dict[str, Any] = {} self.entity_description = description @property diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py index 780e40ed2ba..decec74ee47 100644 --- a/homeassistant/components/airnow/sensor.py +++ b/homeassistant/components/airnow/sensor.py @@ -8,7 +8,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_ATTRIBUTION, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, ) @@ -73,6 +72,8 @@ async def async_setup_entry( class AirNowSensor(CoordinatorEntity[AirNowDataUpdateCoordinator], SensorEntity): """Define an AirNow sensor.""" + _attr_attribution = ATTRIBUTION + def __init__( self, coordinator: AirNowDataUpdateCoordinator, @@ -82,7 +83,7 @@ class AirNowSensor(CoordinatorEntity[AirNowDataUpdateCoordinator], SensorEntity) super().__init__(coordinator) self.entity_description = description self._state = None - self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attrs: dict[str, str] = {} self._attr_name = f"AirNow {description.name}" self._attr_unique_id = ( f"{coordinator.latitude}-{coordinator.longitude}-{description.key.lower()}" diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index fb2c7f01fea..534383f0bbf 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.components import persistent_notification from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_CURRENCY, CONF_NAME +from homeassistant.const import CONF_API_KEY, CONF_CURRENCY, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -118,6 +118,8 @@ def setup_platform( class AlphaVantageSensor(SensorEntity): """Representation of a Alpha Vantage sensor.""" + _attr_attribution = ATTRIBUTION + def __init__(self, timeseries, symbol): """Initialize the sensor.""" self._symbol = symbol[CONF_SYMBOL] @@ -137,7 +139,6 @@ class AlphaVantageSensor(SensorEntity): self._attr_native_value = None self._attr_extra_state_attributes = ( { - ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_CLOSE: values["4. close"], ATTR_HIGH: values["2. high"], ATTR_LOW: values["3. low"], @@ -151,6 +152,8 @@ class AlphaVantageSensor(SensorEntity): class AlphaVantageForeignExchange(SensorEntity): """Sensor for foreign exchange rates.""" + _attr_attribution = ATTRIBUTION + def __init__(self, foreign_exchange, config): """Initialize the sensor.""" self._foreign_exchange = foreign_exchange @@ -180,7 +183,6 @@ class AlphaVantageForeignExchange(SensorEntity): self._attr_native_value = None self._attr_extra_state_attributes = ( { - ATTR_ATTRIBUTION: ATTRIBUTION, CONF_FROM: self._from_currency, CONF_TO: self._to_currency, } diff --git a/homeassistant/components/aurora/__init__.py b/homeassistant/components/aurora/__init__.py index b8d0589d007..bac402fe633 100644 --- a/homeassistant/components/aurora/__init__.py +++ b/homeassistant/components/aurora/__init__.py @@ -121,7 +121,7 @@ class AuroraDataUpdateCoordinator(DataUpdateCoordinator): class AuroraEntity(CoordinatorEntity[AuroraDataUpdateCoordinator]): """Implementation of the base Aurora Entity.""" - _attr_extra_state_attributes = {"attribution": ATTRIBUTION} + _attr_attribution = ATTRIBUTION def __init__( self, diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 841848621ec..680220371b4 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, PERCENTAGE +from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -59,6 +59,7 @@ class CO2Sensor(CoordinatorEntity[CO2SignalCoordinator], SensorEntity): """Implementation of the CO2Signal sensor.""" entity_description: CO2SensorEntityDescription + _attr_attribution = ATTRIBUTION _attr_has_entity_name = True _attr_icon = "mdi:molecule-co2" _attr_state_class = SensorStateClass.MEASUREMENT @@ -72,7 +73,6 @@ class CO2Sensor(CoordinatorEntity[CO2SignalCoordinator], SensorEntity): self._attr_extra_state_attributes = { "country_code": coordinator.data["countryCode"], - ATTR_ATTRIBUTION: ATTRIBUTION, } self._attr_device_info = DeviceInfo( configuration_url="https://www.electricitymap.org/", diff --git a/homeassistant/components/worldtidesinfo/sensor.py b/homeassistant/components/worldtidesinfo/sensor.py index 533328490c8..2433a9f678e 100644 --- a/homeassistant/components/worldtidesinfo/sensor.py +++ b/homeassistant/components/worldtidesinfo/sensor.py @@ -9,13 +9,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_API_KEY, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, -) +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -67,6 +61,8 @@ def setup_platform( class WorldTidesInfoSensor(SensorEntity): """Representation of a WorldTidesInfo sensor.""" + _attr_attribution = ATTRIBUTION + def __init__(self, name, lat, lon, key): """Initialize the sensor.""" self._name = name @@ -83,7 +79,7 @@ class WorldTidesInfoSensor(SensorEntity): @property def extra_state_attributes(self): """Return the state attributes of this device.""" - attr = {ATTR_ATTRIBUTION: ATTRIBUTION} + attr = {} if "High" in str(self.data["extremes"][0]["type"]): attr["high_tide_time_utc"] = self.data["extremes"][0]["date"] diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py index 309b9a6a758..76d1b92b476 100644 --- a/homeassistant/components/wsdot/sensor.py +++ b/homeassistant/components/wsdot/sensor.py @@ -11,7 +11,6 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_NAME, CONF_API_KEY, CONF_ID, @@ -106,6 +105,7 @@ class WashingtonStateTransportSensor(SensorEntity): class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): """Travel time sensor from WSDOT.""" + _attr_attribution = ATTRIBUTION _attr_native_unit_of_measurement = TIME_MINUTES def __init__(self, name, access_code, travel_time_id): @@ -131,7 +131,7 @@ class WashingtonStateTravelTimeSensor(WashingtonStateTransportSensor): def extra_state_attributes(self): """Return other details about the sensor state.""" if self._data is not None: - attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + attrs = {} for key in ( ATTR_AVG_TIME, ATTR_NAME, diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index c32aa942625..8452841520b 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -20,7 +20,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.const import ( AREA_SQUARE_METERS, - ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, @@ -243,6 +242,7 @@ def setup_platform( class ZamgSensor(SensorEntity): """Implementation of a ZAMG sensor.""" + _attr_attribution = ATTRIBUTION entity_description: ZamgSensorEntityDescription def __init__(self, probe, name, description: ZamgSensorEntityDescription): @@ -260,7 +260,6 @@ class ZamgSensor(SensorEntity): def extra_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_STATION: self.probe.get_data("station_name"), ATTR_UPDATED: self.probe.last_update.isoformat(), } From e6e5b98bc711c2b3f02df2622815f12a0d220155 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Thu, 4 Aug 2022 09:13:20 +0200 Subject: [PATCH 3125/3516] Allow climate operation mode fan_only as custom mode in Alexa (#76148) * Add support for FAN_ONLY mode * Tests for fan_only as custom mode --- homeassistant/components/alexa/const.py | 7 +++++-- tests/components/alexa/test_capabilities.py | 19 ++++++++++++++++++- tests/components/alexa/test_smart_home.py | 16 ++++++++++++++-- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index c6ac3071d94..d51409a5a1c 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -73,11 +73,14 @@ API_THERMOSTAT_MODES = OrderedDict( (climate.HVACMode.HEAT_COOL, "AUTO"), (climate.HVACMode.AUTO, "AUTO"), (climate.HVACMode.OFF, "OFF"), - (climate.HVACMode.FAN_ONLY, "OFF"), + (climate.HVACMode.FAN_ONLY, "CUSTOM"), (climate.HVACMode.DRY, "CUSTOM"), ] ) -API_THERMOSTAT_MODES_CUSTOM = {climate.HVACMode.DRY: "DEHUMIDIFY"} +API_THERMOSTAT_MODES_CUSTOM = { + climate.HVACMode.DRY: "DEHUMIDIFY", + climate.HVACMode.FAN_ONLY: "FAN", +} API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} # AlexaModeController does not like a single mode for the fan preset, we add PRESET_MODE_NA if a fan has only one preset_mode diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index ea6c96bbaef..10ad5f7ebd2 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -590,7 +590,7 @@ async def test_report_climate_state(hass): {"value": 34.0, "scale": "CELSIUS"}, ) - for off_modes in (climate.HVAC_MODE_OFF, climate.HVAC_MODE_FAN_ONLY): + for off_modes in [climate.HVAC_MODE_OFF]: hass.states.async_set( "climate.downstairs", off_modes, @@ -626,6 +626,23 @@ async def test_report_climate_state(hass): "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} ) + # assert fan_only is reported as CUSTOM + hass.states.async_set( + "climate.downstairs", + "fan_only", + { + "friendly_name": "Climate Downstairs", + "supported_features": 91, + climate.ATTR_CURRENT_TEMPERATURE: 31, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + }, + ) + properties = await reported_properties(hass, "climate.downstairs") + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "CUSTOM") + properties.assert_equal( + "Alexa.TemperatureSensor", "temperature", {"value": 31.0, "scale": "CELSIUS"} + ) + hass.states.async_set( "climate.heat", "heat", diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 0169eeff9d5..df45d90358b 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -2030,7 +2030,7 @@ async def test_thermostat(hass): "current_temperature": 75.0, "friendly_name": "Test Thermostat", "supported_features": 1 | 2 | 4 | 128, - "hvac_modes": ["off", "heat", "cool", "auto", "dry"], + "hvac_modes": ["off", "heat", "cool", "auto", "dry", "fan_only"], "preset_mode": None, "preset_modes": ["eco"], "min_temp": 50, @@ -2220,7 +2220,7 @@ async def test_thermostat(hass): properties = ReportedProperties(msg["context"]["properties"]) properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "HEAT") - # Assert we can call custom modes + # Assert we can call custom modes for dry and fan_only call, msg = await assert_request_calls_service( "Alexa.ThermostatController", "SetThermostatMode", @@ -2233,6 +2233,18 @@ async def test_thermostat(hass): properties = ReportedProperties(msg["context"]["properties"]) properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "CUSTOM") + call, msg = await assert_request_calls_service( + "Alexa.ThermostatController", + "SetThermostatMode", + "climate#test_thermostat", + "climate.set_hvac_mode", + hass, + payload={"thermostatMode": {"value": "CUSTOM", "customName": "FAN"}}, + ) + assert call.data["hvac_mode"] == "fan_only" + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal("Alexa.ThermostatController", "thermostatMode", "CUSTOM") + # assert unsupported custom mode msg = await assert_request_fails( "Alexa.ThermostatController", From b7b965c9c9575e3717eb4bef39809c55c700b7d4 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Aug 2022 09:41:07 +0200 Subject: [PATCH 3126/3516] Use attributes in yeelightsunflower light (#75995) --- .../components/yeelightsunflower/light.py | 37 +++++-------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/yeelightsunflower/light.py b/homeassistant/components/yeelightsunflower/light.py index 8b51dad40f7..29a6118fc03 100644 --- a/homeassistant/components/yeelightsunflower/light.py +++ b/homeassistant/components/yeelightsunflower/light.py @@ -49,42 +49,23 @@ class SunflowerBulb(LightEntity): _attr_color_mode = ColorMode.HS _attr_supported_color_modes = {ColorMode.HS} - def __init__(self, light): + def __init__(self, light: yeelightsunflower.Bulb) -> None: """Initialize a Yeelight Sunflower bulb.""" self._light = light - self._available = light.available + self._attr_available = light.available self._brightness = light.brightness - self._is_on = light.is_on + self._attr_is_on = light.is_on self._rgb_color = light.rgb_color - self._unique_id = light.zid + self._attr_unique_id = light.zid + self._attr_name = f"sunflower_{self._light.zid}" @property - def name(self): - """Return the display name of this light.""" - return f"sunflower_{self._light.zid}" - - @property - def unique_id(self): - """Return the unique ID of this light.""" - return self._unique_id - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def is_on(self): - """Return true if light is on.""" - return self._is_on - - @property - def brightness(self): + def brightness(self) -> int: """Return the brightness is 0-255; Yeelight's brightness is 0-100.""" return int(self._brightness / 100 * 255) @property - def hs_color(self): + def hs_color(self) -> tuple[float, float]: """Return the color property.""" return color_util.color_RGB_to_hs(*self._rgb_color) @@ -112,7 +93,7 @@ class SunflowerBulb(LightEntity): def update(self) -> None: """Fetch new state data for this light and update local values.""" self._light.update() - self._available = self._light.available + self._attr_available = self._light.available self._brightness = self._light.brightness - self._is_on = self._light.is_on + self._attr_is_on = self._light.is_on self._rgb_color = self._light.rgb_color From d5695a2d8656d2f9cb4d549c80cad331c914af1f Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 4 Aug 2022 11:30:37 +0100 Subject: [PATCH 3127/3516] Fix some homekit_controller pylint warnings and (local only) test failures (#76122) --- .../homekit_controller/config_flow.py | 60 ++++++++++++++----- .../components/homekit_controller/light.py | 4 +- .../homekit_controller/test_sensor.py | 8 ++- 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 31677e37b20..d5ce8c37e7c 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -1,14 +1,17 @@ """Config flow to configure homekit_controller.""" from __future__ import annotations -from collections.abc import Awaitable import logging import re from typing import TYPE_CHECKING, Any, cast import aiohomekit from aiohomekit import Controller, const as aiohomekit_const -from aiohomekit.controller.abstract import AbstractDiscovery, AbstractPairing +from aiohomekit.controller.abstract import ( + AbstractDiscovery, + AbstractPairing, + FinishPairing, +) from aiohomekit.exceptions import AuthenticationError from aiohomekit.model.categories import Categories from aiohomekit.model.status_flags import StatusFlags @@ -17,7 +20,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import zeroconf -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import device_registry as dr @@ -78,7 +81,9 @@ def formatted_category(category: Categories) -> str: @callback -def find_existing_host(hass, serial: str) -> config_entries.ConfigEntry | None: +def find_existing_host( + hass: HomeAssistant, serial: str +) -> config_entries.ConfigEntry | None: """Return a set of the configured hosts.""" for entry in hass.config_entries.async_entries(DOMAIN): if entry.data.get("AccessoryPairingID") == serial: @@ -115,15 +120,17 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.category: Categories | None = None self.devices: dict[str, AbstractDiscovery] = {} self.controller: Controller | None = None - self.finish_pairing: Awaitable[AbstractPairing] | None = None + self.finish_pairing: FinishPairing | None = None - async def _async_setup_controller(self): + async def _async_setup_controller(self) -> None: """Create the controller.""" self.controller = await async_get_controller(self.hass) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow start.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: key = user_input["device"] @@ -142,6 +149,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if self.controller is None: await self._async_setup_controller() + assert self.controller + self.devices = {} async for discovery in self.controller.async_discover(): @@ -167,7 +176,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ), ) - async def async_step_unignore(self, user_input): + async def async_step_unignore(self, user_input: dict[str, Any]) -> FlowResult: """Rediscover a previously ignored discover.""" unique_id = user_input["unique_id"] await self.async_set_unique_id(unique_id) @@ -175,19 +184,21 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if self.controller is None: await self._async_setup_controller() + assert self.controller + try: discovery = await self.controller.async_find(unique_id) except aiohomekit.AccessoryNotFoundError: return self.async_abort(reason="accessory_not_found_error") self.name = discovery.description.name - self.model = discovery.description.model + self.model = getattr(discovery.description, "model", BLE_DEFAULT_NAME) self.category = discovery.description.category self.hkid = discovery.description.id return self._async_step_pair_show_form() - async def _hkid_is_homekit(self, hkid): + async def _hkid_is_homekit(self, hkid: str) -> bool: """Determine if the device is a homekit bridge or accessory.""" dev_reg = dr.async_get(self.hass) device = dev_reg.async_get_device( @@ -410,7 +421,9 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self._async_step_pair_show_form() - async def async_step_pair(self, pair_info=None): + async def async_step_pair( + self, pair_info: dict[str, Any] | None = None + ) -> FlowResult: """Pair with a new HomeKit accessory.""" # If async_step_pair is called with no pairing code then we do the M1 # phase of pairing. If this is successful the device enters pairing @@ -428,11 +441,16 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # callable. We call the callable with the pin that the user has typed # in. + # Should never call this step without setting self.hkid + assert self.hkid + errors = {} if self.controller is None: await self._async_setup_controller() + assert self.controller + if pair_info and self.finish_pairing: self.context["pairing"] = True code = pair_info["pairing_code"] @@ -507,21 +525,27 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self._async_step_pair_show_form(errors) - async def async_step_busy_error(self, user_input=None): + async def async_step_busy_error( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Retry pairing after the accessory is busy.""" if user_input is not None: return await self.async_step_pair() return self.async_show_form(step_id="busy_error") - async def async_step_max_tries_error(self, user_input=None): + async def async_step_max_tries_error( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Retry pairing after the accessory has reached max tries.""" if user_input is not None: return await self.async_step_pair() return self.async_show_form(step_id="max_tries_error") - async def async_step_protocol_error(self, user_input=None): + async def async_step_protocol_error( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Retry pairing after the accessory has a protocol error.""" if user_input is not None: return await self.async_step_pair() @@ -529,7 +553,11 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="protocol_error") @callback - def _async_step_pair_show_form(self, errors=None): + def _async_step_pair_show_form( + self, errors: dict[str, str] | None = None + ) -> FlowResult: + assert self.category + placeholders = self.context["title_placeholders"] = { "name": self.name, "category": formatted_category(self.category), diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index df691ac3f6f..d882f6790f7 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -107,9 +107,9 @@ class HomeKitLight(HomeKitEntity, LightEntity): return ColorMode.ONOFF @property - def supported_color_modes(self) -> set[ColorMode | str] | None: + def supported_color_modes(self) -> set[ColorMode]: """Flag supported color modes.""" - color_modes: set[ColorMode | str] = set() + color_modes: set[ColorMode] = set() if self.service.has(CharacteristicsTypes.HUE) or self.service.has( CharacteristicsTypes.SATURATION diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index c2a466d3997..79ae29c7434 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -92,10 +92,12 @@ async def test_temperature_sensor_not_added_twice(hass, utcnow): hass, create_temperature_sensor_service, suffix="temperature" ) + created_sensors = set() for state in hass.states.async_all(): - if state.entity_id.startswith("button"): - continue - assert state.entity_id == helper.entity_id + if state.attributes.get("device_class") == SensorDeviceClass.TEMPERATURE: + created_sensors.add(state.entity_id) + + assert created_sensors == {helper.entity_id} async def test_humidity_sensor_read_state(hass, utcnow): From aa3097a3be267292812fa968848a39739727225f Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 4 Aug 2022 11:55:29 +0100 Subject: [PATCH 3128/3516] Add a Thread network status sensor to homekit_controller (#76209) --- .../components/homekit_controller/sensor.py | 57 ++++++++++++++++++- .../homekit_controller/strings.sensor.json | 9 +++ .../translations/sensor.en.json | 9 +++ .../test_nanoleaf_strip_nl55.py | 7 +++ .../homekit_controller/test_sensor.py | 14 ++++- 5 files changed, 94 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index ecfad477d00..a6810c10d99 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -5,7 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes -from aiohomekit.model.characteristics.const import ThreadNodeCapabilities +from aiohomekit.model.characteristics.const import ThreadNodeCapabilities, ThreadStatus from aiohomekit.model.services import Service, ServicesTypes from homeassistant.components.sensor import ( @@ -83,6 +83,54 @@ def thread_node_capability_to_str(char: Characteristic) -> str: return "none" +def thread_status_to_str(char: Characteristic) -> str: + """ + Return the thread status as a string. + + The underlying value is a bitmask, but we want to turn that to + a human readable string. So we check the flags in order. E.g. BORDER_ROUTER implies + ROUTER, so its more important to show that value. + """ + + val = ThreadStatus(char.value) + + if val & ThreadStatus.BORDER_ROUTER: + # Device has joined the Thread network and is participating + # in routing between mesh nodes. + # It's also the border router - bridging the thread network + # to WiFI/Ethernet/etc + return "border_router" + + if val & ThreadStatus.LEADER: + # Device has joined the Thread network and is participating + # in routing between mesh nodes. + # It's also the leader. There's only one leader and it manages + # which nodes are routers. + return "leader" + + if val & ThreadStatus.ROUTER: + # Device has joined the Thread network and is participating + # in routing between mesh nodes. + return "router" + + if val & ThreadStatus.CHILD: + # Device has joined the Thread network as a child + # It's not participating in routing between mesh nodes + return "child" + + if val & ThreadStatus.JOINING: + # Device is currently joining its Thread network + return "joining" + + if val & ThreadStatus.DETACHED: + # Device is currently unable to reach its Thread network + return "detached" + + # Must be ThreadStatus.DISABLED + # Device is not currently connected to Thread and will not try to. + return "disabled" + + SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_WATT: HomeKitSensorEntityDescription( key=CharacteristicsTypes.VENDOR_CONNECTSENSE_ENERGY_WATT, @@ -243,6 +291,13 @@ SIMPLE_SENSOR: dict[str, HomeKitSensorEntityDescription] = { entity_category=EntityCategory.DIAGNOSTIC, format=thread_node_capability_to_str, ), + CharacteristicsTypes.THREAD_STATUS: HomeKitSensorEntityDescription( + key=CharacteristicsTypes.THREAD_STATUS, + name="Thread Status", + device_class="homekit_controller__thread_status", + entity_category=EntityCategory.DIAGNOSTIC, + format=thread_status_to_str, + ), } diff --git a/homeassistant/components/homekit_controller/strings.sensor.json b/homeassistant/components/homekit_controller/strings.sensor.json index 4a77fa1668a..ceede708572 100644 --- a/homeassistant/components/homekit_controller/strings.sensor.json +++ b/homeassistant/components/homekit_controller/strings.sensor.json @@ -7,6 +7,15 @@ "minimal": "Minimal End Device", "sleepy": "Sleepy End Device", "none": "None" + }, + "homekit_controller__thread_status": { + "border_router": "Border Router", + "leader": "Leader", + "router": "Router", + "child": "Child", + "joining": "Joining", + "detached": "Detached", + "disabled": "Disabled" } } } diff --git a/homeassistant/components/homekit_controller/translations/sensor.en.json b/homeassistant/components/homekit_controller/translations/sensor.en.json index b1f8a0a8128..acfa6ab2824 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.en.json +++ b/homeassistant/components/homekit_controller/translations/sensor.en.json @@ -7,6 +7,15 @@ "none": "None", "router_eligible": "Router Eligible End Device", "sleepy": "Sleepy End Device" + }, + "homekit_controller__thread_status": { + "border_router": "Border Router", + "child": "Child", + "detached": "Detached", + "disabled": "Disabled", + "joining": "Joining", + "leader": "Leader", + "router": "Router" } } } \ No newline at end of file diff --git a/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py index 086027f2427..550c3a328d0 100644 --- a/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py +++ b/tests/components/homekit_controller/specific_devices/test_nanoleaf_strip_nl55.py @@ -57,6 +57,13 @@ async def test_nanoleaf_nl55_setup(hass): entity_category=EntityCategory.DIAGNOSTIC, state="border_router_capable", ), + EntityTestInfo( + entity_id="sensor.nanoleaf_strip_3b32_thread_status", + friendly_name="Nanoleaf Strip 3B32 Thread Status", + unique_id="homekit-AAAA011111111111-aid:1-sid:31-cid:117", + entity_category=EntityCategory.DIAGNOSTIC, + state="border_router", + ), ], ), ) diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index 79ae29c7434..21112937939 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -1,11 +1,12 @@ """Basic checks for HomeKit sensor.""" from aiohomekit.model.characteristics import CharacteristicsTypes -from aiohomekit.model.characteristics.const import ThreadNodeCapabilities +from aiohomekit.model.characteristics.const import ThreadNodeCapabilities, ThreadStatus from aiohomekit.model.services import ServicesTypes from aiohomekit.protocol.statuscodes import HapStatusCode from homeassistant.components.homekit_controller.sensor import ( thread_node_capability_to_str, + thread_status_to_str, ) from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass @@ -337,3 +338,14 @@ def test_thread_node_caps_to_str(): assert thread_node_capability_to_str(ThreadNodeCapabilities.MINIMAL) == "minimal" assert thread_node_capability_to_str(ThreadNodeCapabilities.SLEEPY) == "sleepy" assert thread_node_capability_to_str(ThreadNodeCapabilities(128)) == "none" + + +def test_thread_status_to_str(): + """Test all values of this enum get a translatable string.""" + assert thread_status_to_str(ThreadStatus.BORDER_ROUTER) == "border_router" + assert thread_status_to_str(ThreadStatus.LEADER) == "leader" + assert thread_status_to_str(ThreadStatus.ROUTER) == "router" + assert thread_status_to_str(ThreadStatus.CHILD) == "child" + assert thread_status_to_str(ThreadStatus.JOINING) == "joining" + assert thread_status_to_str(ThreadStatus.DETACHED) == "detached" + assert thread_status_to_str(ThreadStatus.DISABLED) == "disabled" From 726eb82758ba6ceec8e31611b2854e5f4c0cee72 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Aug 2022 13:21:37 +0200 Subject: [PATCH 3129/3516] Mark RPI Power binary sensor as diagnostic (#76198) --- homeassistant/components/rpi_power/binary_sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/rpi_power/binary_sensor.py b/homeassistant/components/rpi_power/binary_sensor.py index f70581a8075..08535daf970 100644 --- a/homeassistant/components/rpi_power/binary_sensor.py +++ b/homeassistant/components/rpi_power/binary_sensor.py @@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback _LOGGER = logging.getLogger(__name__) @@ -35,6 +36,7 @@ class RaspberryChargerBinarySensor(BinarySensorEntity): """Binary sensor representing the rpi power status.""" _attr_device_class = BinarySensorDeviceClass.PROBLEM + _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_icon = "mdi:raspberry-pi" _attr_name = "RPi Power status" _attr_unique_id = "rpi_power" # only one sensor possible From 88a5ab1e1eeec1975362c15102ca083d5e4b2489 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 4 Aug 2022 14:01:26 +0200 Subject: [PATCH 3130/3516] Bump NextDNS library (#76207) --- homeassistant/components/nextdns/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nextdns/test_diagnostics.py | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json index a427f930db8..3e2d3ebb3d0 100644 --- a/homeassistant/components/nextdns/manifest.json +++ b/homeassistant/components/nextdns/manifest.json @@ -3,7 +3,7 @@ "name": "NextDNS", "documentation": "https://www.home-assistant.io/integrations/nextdns", "codeowners": ["@bieniu"], - "requirements": ["nextdns==1.0.1"], + "requirements": ["nextdns==1.0.2"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["nextdns"] diff --git a/requirements_all.txt b/requirements_all.txt index e7e6999053e..8a1ef4cd991 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1109,7 +1109,7 @@ nextcloudmonitor==1.1.0 nextcord==2.0.0a8 # homeassistant.components.nextdns -nextdns==1.0.1 +nextdns==1.0.2 # homeassistant.components.niko_home_control niko-home-control==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18ef8e97b63..a67eb65f2e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -787,7 +787,7 @@ nexia==2.0.2 nextcord==2.0.0a8 # homeassistant.components.nextdns -nextdns==1.0.1 +nextdns==1.0.2 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 diff --git a/tests/components/nextdns/test_diagnostics.py b/tests/components/nextdns/test_diagnostics.py index 85dbceafff9..0702a2fa1d8 100644 --- a/tests/components/nextdns/test_diagnostics.py +++ b/tests/components/nextdns/test_diagnostics.py @@ -48,11 +48,13 @@ async def test_entry_diagnostics( } assert result["protocols_coordinator_data"] == { "doh_queries": 20, + "doh3_queries": 0, "doq_queries": 10, "dot_queries": 30, "tcp_queries": 0, "udp_queries": 40, "doh_queries_ratio": 20.0, + "doh3_queries_ratio": 0.0, "doq_queries_ratio": 10.0, "dot_queries_ratio": 30.0, "tcp_queries_ratio": 0.0, From 9af64b1c3b1a712d24fc7b86ed2cc5e1fa613f26 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Aug 2022 14:02:07 +0200 Subject: [PATCH 3131/3516] Improve type hints in zha light (#75947) --- homeassistant/components/zha/light.py | 37 ++++++++++++++++----------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 9fc089e2241..88bd5299ff7 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -28,7 +28,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, Platform, ) -from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, State, callback from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -390,7 +390,7 @@ class BaseLight(LogMixin, light.LightEntity): self.debug("turned on: %s", t_log) self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" transition = kwargs.get(light.ATTR_TRANSITION) supports_level = brightness_supported(self._attr_supported_color_modes) @@ -567,7 +567,7 @@ class Light(BaseLight, ZhaEntity): if self._color_channel: self._attr_min_mireds: int = self._color_channel.min_mireds self._attr_max_mireds: int = self._color_channel.max_mireds - self._cancel_refresh_handle = None + self._cancel_refresh_handle: CALLBACK_TYPE | None = None effect_list = [] self._zha_config_always_prefer_xy_color_mode = async_get_zha_config_value( @@ -675,7 +675,7 @@ class Light(BaseLight, ZhaEntity): self._off_brightness = None self.async_write_ha_state() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() self.async_accept_signal( @@ -755,7 +755,7 @@ class Light(BaseLight, ZhaEntity): if "effect" in last_state.attributes: self._attr_effect = last_state.attributes["effect"] - async def async_get_state(self): + async def async_get_state(self) -> None: """Attempt to retrieve the state from the light.""" if not self._attr_available: return @@ -843,7 +843,7 @@ class Light(BaseLight, ZhaEntity): else: self._attr_effect = None - async def async_update(self): + async def async_update(self) -> None: """Update to the latest state.""" if self._transitioning: self.debug("skipping async_update while transitioning") @@ -906,7 +906,12 @@ class LightGroup(BaseLight, ZhaGroupEntity): """Representation of a light group.""" def __init__( - self, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs + self, + entity_ids: list[str], + unique_id: str, + group_id: int, + zha_device: ZHADevice, + **kwargs: Any, ) -> None: """Initialize a light group.""" super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) @@ -919,7 +924,7 @@ class LightGroup(BaseLight, ZhaGroupEntity): self._level_channel = group.endpoint[LevelControl.cluster_id] self._color_channel = group.endpoint[Color.cluster_id] self._identify_channel = group.endpoint[Identify.cluster_id] - self._debounced_member_refresh = None + self._debounced_member_refresh: Debouncer | None = None self._zha_config_transition = async_get_zha_config_value( zha_device.gateway.config_entry, ZHA_OPTIONS, @@ -947,7 +952,7 @@ class LightGroup(BaseLight, ZhaGroupEntity): """Return entity availability.""" return self._attr_available - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() if self._debounced_member_refresh is None: @@ -960,22 +965,24 @@ class LightGroup(BaseLight, ZhaGroupEntity): ) self._debounced_member_refresh = force_refresh_debouncer - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" await super().async_turn_on(**kwargs) if self._transitioning: return - await self._debounced_member_refresh.async_call() + if self._debounced_member_refresh: + await self._debounced_member_refresh.async_call() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" await super().async_turn_off(**kwargs) if self._transitioning: return - await self._debounced_member_refresh.async_call() + if self._debounced_member_refresh: + await self._debounced_member_refresh.async_call() @callback - def async_state_changed_listener(self, event: Event): + def async_state_changed_listener(self, event: Event) -> None: """Handle child updates.""" if self._transitioning: self.debug("skipping group entity state update during transition") @@ -1073,7 +1080,7 @@ class LightGroup(BaseLight, ZhaGroupEntity): # so that we don't break in the future when a new feature is added. self._attr_supported_features &= SUPPORT_GROUP_LIGHT - async def _force_member_updates(self): + async def _force_member_updates(self) -> None: """Force the update of member entities to ensure the states are correct for bulbs that don't report their state.""" async_dispatcher_send( self.hass, From 8793cf4996b194680e6a2ef89fe814e7fc5b9930 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Aug 2022 15:57:00 +0200 Subject: [PATCH 3132/3516] Fix spelling of OpenWrt in luci integration manifest (#76219) --- homeassistant/components/luci/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index 2d61852689a..b24c0234de9 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -1,6 +1,6 @@ { "domain": "luci", - "name": "OpenWRT (luci)", + "name": "OpenWrt (luci)", "documentation": "https://www.home-assistant.io/integrations/luci", "requirements": ["openwrt-luci-rpc==1.1.11"], "codeowners": ["@mzdrale"], From ff255feddae0879a923854ba91a25689f0461353 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 4 Aug 2022 17:04:12 +0300 Subject: [PATCH 3133/3516] Fix nullable ip_address in mikrotik (#76197) --- homeassistant/components/mikrotik/device_tracker.py | 2 +- homeassistant/components/mikrotik/hub.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index d02bf69b5ab..856521f019d 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -116,7 +116,7 @@ class MikrotikDataUpdateCoordinatorTracker( return self.device.mac @property - def ip_address(self) -> str: + def ip_address(self) -> str | None: """Return the mac address of the client.""" return self.device.ip_address diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 66fe7226d9b..914911ee5cc 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -60,9 +60,9 @@ class Device: return self._params.get("host-name", self.mac) @property - def ip_address(self) -> str: + def ip_address(self) -> str | None: """Return device primary ip address.""" - return self._params["address"] + return self._params.get("address") @property def mac(self) -> str: From 63b454c9ed7354665a3f0efbb0c45b75754b1beb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Aug 2022 05:38:55 -1000 Subject: [PATCH 3134/3516] BLE pairing reliablity fixes for HomeKit Controller (#76199) - Remove the cached map from memory when unpairing so we do not reuse it again if they unpair/repair - Fixes for accessories that use a config number of 0 - General reliablity improvements to the pairing process under the hood of aiohomekit --- .../components/homekit_controller/__init__.py | 7 ++++--- .../homekit_controller/config_flow.py | 2 +- .../homekit_controller/manifest.json | 2 +- .../components/homekit_controller/storage.py | 19 ++++++++++++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../homekit_controller/test_config_flow.py | 6 ++++++ 7 files changed, 28 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 37dd648dedb..b2ccad9a457 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -31,7 +31,7 @@ from homeassistant.helpers.typing import ConfigType from .config_flow import normalize_hkid from .connection import HKDevice, valid_serial_number from .const import ENTITY_MAP, KNOWN_DEVICES, TRIGGERS -from .storage import async_get_entity_storage +from .storage import EntityMapStorage, async_get_entity_storage from .utils import async_get_controller, folded_name _LOGGER = logging.getLogger(__name__) @@ -269,7 +269,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hkid = entry.data["AccessoryPairingID"] if hkid in hass.data[KNOWN_DEVICES]: - connection = hass.data[KNOWN_DEVICES][hkid] + connection: HKDevice = hass.data[KNOWN_DEVICES][hkid] await connection.async_unload() return True @@ -280,7 +280,8 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: hkid = entry.data["AccessoryPairingID"] # Remove cached type data from .storage/homekit_controller-entity-map - hass.data[ENTITY_MAP].async_delete_map(hkid) + entity_map_storage: EntityMapStorage = hass.data[ENTITY_MAP] + entity_map_storage.async_delete_map(hkid) controller = await async_get_controller(hass) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index d5ce8c37e7c..eba531b917c 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -597,7 +597,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): entity_storage = await async_get_entity_storage(self.hass) assert self.unique_id is not None entity_storage.async_create_or_update_map( - self.unique_id, + pairing.id, accessories_state.config_num, accessories_state.accessories.serialize(), ) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index ac1be576906..5aff66fe757 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.3"], + "requirements": ["aiohomekit==1.2.4"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index 8c0628c97f6..51d8ce4ffd3 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -2,6 +2,7 @@ from __future__ import annotations +import logging from typing import Any, TypedDict from homeassistant.core import HomeAssistant, callback @@ -12,6 +13,7 @@ from .const import DOMAIN, ENTITY_MAP ENTITY_MAP_STORAGE_KEY = f"{DOMAIN}-entity-map" ENTITY_MAP_STORAGE_VERSION = 1 ENTITY_MAP_SAVE_DELAY = 10 +_LOGGER = logging.getLogger(__name__) class Pairing(TypedDict): @@ -68,6 +70,7 @@ class EntityMapStorage: self, homekit_id: str, config_num: int, accessories: list[Any] ) -> Pairing: """Create a new pairing cache.""" + _LOGGER.debug("Creating or updating entity map for %s", homekit_id) data = Pairing(config_num=config_num, accessories=accessories) self.storage_data[homekit_id] = data self._async_schedule_save() @@ -76,11 +79,17 @@ class EntityMapStorage: @callback def async_delete_map(self, homekit_id: str) -> None: """Delete pairing cache.""" - if homekit_id not in self.storage_data: - return - - self.storage_data.pop(homekit_id) - self._async_schedule_save() + removed_one = False + # Previously there was a bug where a lowercase homekit_id was stored + # in the storage. We need to account for that. + for hkid in (homekit_id, homekit_id.lower()): + if hkid not in self.storage_data: + continue + _LOGGER.debug("Deleting entity map for %s", hkid) + self.storage_data.pop(hkid) + removed_one = True + if removed_one: + self._async_schedule_save() @callback def _async_schedule_save(self) -> None: diff --git a/requirements_all.txt b/requirements_all.txt index 8a1ef4cd991..2893d0e943c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.3 +aiohomekit==1.2.4 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a67eb65f2e9..df4ee525846 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.3 +aiohomekit==1.2.4 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 78d3c609a9c..e72d9452e52 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -14,6 +14,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import KNOWN_DEVICES +from homeassistant.components.homekit_controller.storage import async_get_entity_storage from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, RESULT_TYPE_FORM, @@ -1071,6 +1072,8 @@ async def test_bluetooth_valid_device_discovery_paired(hass, controller): async def test_bluetooth_valid_device_discovery_unpaired(hass, controller): """Test bluetooth discovery with a homekit device and discovery works.""" setup_mock_accessory(controller) + storage = await async_get_entity_storage(hass) + with patch( "homeassistant.components.homekit_controller.config_flow.aiohomekit_const.BLE_TRANSPORT_SUPPORTED", True, @@ -1083,6 +1086,7 @@ async def test_bluetooth_valid_device_discovery_unpaired(hass, controller): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "pair" + assert storage.get_map("00:00:00:00:00:00") is None assert get_flow_context(hass, result) == { "source": config_entries.SOURCE_BLUETOOTH, @@ -1098,3 +1102,5 @@ async def test_bluetooth_valid_device_discovery_unpaired(hass, controller): assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Koogeek-LS1-20833F" assert result3["data"] == {} + + assert storage.get_map("00:00:00:00:00:00") is not None From 5d7cef64168673163986f6c3f31bb6a2cba32608 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Aug 2022 05:58:15 -1000 Subject: [PATCH 3135/3516] Fix race in bluetooth async_process_advertisements (#76176) --- homeassistant/components/bluetooth/__init__.py | 2 +- tests/components/bluetooth/test_init.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index c91563d7729..0b81472f838 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -188,7 +188,7 @@ async def async_process_advertisements( def _async_discovered_device( service_info: BluetoothServiceInfoBleak, change: BluetoothChange ) -> None: - if callback(service_info): + if not done.done() and callback(service_info): done.set_result(service_info) unload = async_register_callback(hass, _async_discovered_device, match_dict, mode) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index edc5eb024a6..ba315b1f380 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -856,6 +856,9 @@ async def test_process_advertisements_bail_on_good_advertisement( ) _get_underlying_scanner()._callback(device, adv) + _get_underlying_scanner()._callback(device, adv) + _get_underlying_scanner()._callback(device, adv) + await asyncio.sleep(0) result = await handle From 0ce44150fd1ef65ec832acab0a7e1a342c7d71b9 Mon Sep 17 00:00:00 2001 From: mkmer Date: Thu, 4 Aug 2022 12:01:58 -0400 Subject: [PATCH 3136/3516] Bump AIOAladdin Connect to 0.1.41 (#76217) --- homeassistant/components/aladdin_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 5e55f391aa6..febba16170a 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.39"], + "requirements": ["AIOAladdinConnect==0.1.41"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/requirements_all.txt b/requirements_all.txt index 2893d0e943c..903ac347c5a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.39 +AIOAladdinConnect==0.1.41 # homeassistant.components.adax Adax-local==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df4ee525846..2504705b55d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.39 +AIOAladdinConnect==0.1.41 # homeassistant.components.adax Adax-local==0.1.4 From 5e75bed9296e6131513da20d8da4349ebd5defc8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Aug 2022 18:04:54 +0200 Subject: [PATCH 3137/3516] Update sentry-sdk to 1.9.0 (#76192) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index f6567fbd04d..0400424a7fc 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.8.0"], + "requirements": ["sentry-sdk==1.9.0"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 903ac347c5a..1a79d1ee3ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2169,7 +2169,7 @@ sense_energy==0.10.4 sensorpush-ble==1.5.1 # homeassistant.components.sentry -sentry-sdk==1.8.0 +sentry-sdk==1.9.0 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2504705b55d..841af5b021b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1457,7 +1457,7 @@ sense_energy==0.10.4 sensorpush-ble==1.5.1 # homeassistant.components.sentry -sentry-sdk==1.8.0 +sentry-sdk==1.9.0 # homeassistant.components.sharkiq sharkiq==0.0.1 From 8810d3320ca34e1a69db3ef748d9000200fe3821 Mon Sep 17 00:00:00 2001 From: MosheTzvi <109089618+MosheTzvi@users.noreply.github.com> Date: Thu, 4 Aug 2022 19:08:23 +0300 Subject: [PATCH 3138/3516] added Hanetz Hachama (#76216) added missing variable --- homeassistant/components/jewish_calendar/sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 827a35587f8..085dbcd8c98 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -63,6 +63,11 @@ TIME_SENSORS = ( name="Talit and Tefillin", icon="mdi:calendar-clock", ), + SensorEntityDescription( + key="sunrise", + name="Hanetz Hachama", + icon="mdi:calendar-clock", + ), SensorEntityDescription( key="gra_end_shma", name='Latest time for Shma Gr"a', From 91486f2d61cf9f9f9abff349da0dee28bda2fae7 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 4 Aug 2022 17:41:47 +0100 Subject: [PATCH 3139/3516] Enable strict typing for HomeKit Controller config flow module (#76233) --- .strict-typing | 1 + .../components/homekit_controller/manifest.json | 2 +- mypy.ini | 11 +++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.strict-typing b/.strict-typing index 37dcdd7623f..cc1af9926bd 100644 --- a/.strict-typing +++ b/.strict-typing @@ -129,6 +129,7 @@ homeassistant.components.homekit.util homeassistant.components.homekit_controller homeassistant.components.homekit_controller.alarm_control_panel homeassistant.components.homekit_controller.button +homeassistant.components.homekit_controller.config_flow homeassistant.components.homekit_controller.const homeassistant.components.homekit_controller.lock homeassistant.components.homekit_controller.select diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 5aff66fe757..5f6b3f92220 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.4"], + "requirements": ["aiohomekit==1.2.5"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/mypy.ini b/mypy.ini index 8c85b550f71..26f76c24a02 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1142,6 +1142,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.homekit_controller.config_flow] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.homekit_controller.const] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 1a79d1ee3ef..48d2e47086a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.4 +aiohomekit==1.2.5 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 841af5b021b..08e47a7aded 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.4 +aiohomekit==1.2.5 # homeassistant.components.emulated_hue # homeassistant.components.http From 8ca5b5d4a4f78531093ce081e061445f009628d1 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Thu, 4 Aug 2022 18:36:37 +0100 Subject: [PATCH 3140/3516] Remove icon attribute if device class is set (#76161) --- homeassistant/components/integration/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 5d0dde3e4de..b3b8a2a2b9d 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -223,6 +223,7 @@ class IntegrationSensor(RestoreEntity, SensorEntity): == SensorDeviceClass.POWER ): self._attr_device_class = SensorDeviceClass.ENERGY + self._attr_icon = None update_state = True if update_state: From b2dc810ea4d2d7a9a89c33c15169781f0661651b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 4 Aug 2022 11:43:02 -0600 Subject: [PATCH 3141/3516] More explicitly call out special cases with SimpliSafe authorization code (#76232) --- .../components/simplisafe/config_flow.py | 23 +++++++- .../components/simplisafe/strings.json | 3 +- .../simplisafe/translations/en.json | 24 +------- .../components/simplisafe/test_config_flow.py | 55 ++++++++++++++++--- 4 files changed, 71 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 0b92871ccb2..7ae363c3be3 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -81,17 +81,34 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): description_placeholders={CONF_URL: self._oauth_values.auth_url}, ) + auth_code = user_input[CONF_AUTH_CODE] + + if auth_code.startswith("="): + # Sometimes, users may include the "=" from the URL query param; in that + # case, strip it off and proceed: + LOGGER.debug('Stripping "=" from the start of the authorization code') + auth_code = auth_code[1:] + + if len(auth_code) != 45: + # SimpliSafe authorization codes are 45 characters in length; if the user + # provides something different, stop them here: + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_SCHEMA, + errors={CONF_AUTH_CODE: "invalid_auth_code_length"}, + description_placeholders={CONF_URL: self._oauth_values.auth_url}, + ) + errors = {} session = aiohttp_client.async_get_clientsession(self.hass) - try: simplisafe = await API.async_from_auth( - user_input[CONF_AUTH_CODE], + auth_code, self._oauth_values.code_verifier, session=session, ) except InvalidCredentialsError: - errors = {"base": "invalid_auth"} + errors = {CONF_AUTH_CODE: "invalid_auth"} except SimplipyError as err: LOGGER.error("Unknown error while logging into SimpliSafe: %s", err) errors = {"base": "unknown"} diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 16ae7111abf..618c21566f7 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL.", + "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. If you've already logged into SimpliSafe in your browser, you may want to open a new tab, then copy/paste the above URL into that tab.\n\nWhen the process is complete, return here and input the authorization code from the `com.simplisafe.mobile` URL.", "data": { "auth_code": "Authorization Code" } @@ -11,6 +11,7 @@ "error": { "identifier_exists": "Account already registered", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_auth_code_length": "SimpliSafe authorization codes are 45 characters in length", "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 70b0cc15383..245bb18351e 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -2,39 +2,21 @@ "config": { "abort": { "already_configured": "This SimpliSafe account is already in use.", - "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", "reauth_successful": "Re-authentication was successful", "wrong_account": "The user credentials provided do not match this SimpliSafe account." }, "error": { "identifier_exists": "Account already registered", "invalid_auth": "Invalid authentication", + "invalid_auth_code_length": "SimpliSafe authorization codes are 45 characters in length", "unknown": "Unexpected error" }, - "progress": { - "email_2fa": "Check your email for a verification link from Simplisafe." - }, "step": { - "reauth_confirm": { - "data": { - "password": "Password" - }, - "description": "Please re-enter the password for {username}.", - "title": "Reauthenticate Integration" - }, - "sms_2fa": { - "data": { - "code": "Code" - }, - "description": "Input the two-factor authentication code sent to you via SMS." - }, "user": { "data": { - "auth_code": "Authorization Code", - "password": "Password", - "username": "Username" + "auth_code": "Authorization Code" }, - "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. When the process is complete, return here and input the authorization code from the SimpliSafe web app URL." + "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. If you've already logged into SimpliSafe in your browser, you may want to open a new tab, then copy/paste the above URL into that tab.\n\nWhen the process is complete, return here and input the authorization code from the `com.simplisafe.mobile` URL." } } }, diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 6e6f99ad4bb..cf92ed94d41 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -1,4 +1,5 @@ """Define tests for the SimpliSafe config flow.""" +import logging from unittest.mock import patch import pytest @@ -10,6 +11,8 @@ from homeassistant.components.simplisafe.config_flow import CONF_AUTH_CODE from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_CODE, CONF_TOKEN, CONF_USERNAME +VALID_AUTH_CODE = "code12345123451234512345123451234512345123451" + async def test_duplicate_error(config_entry, hass, setup_simplisafe): """Test that errors are shown when duplicates are added.""" @@ -23,12 +26,27 @@ async def test_duplicate_error(config_entry, hass, setup_simplisafe): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" +async def test_invalid_auth_code_length(hass): + """Test that an invalid auth code length show the correct error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_AUTH_CODE: "too_short_code"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_AUTH_CODE: "invalid_auth_code_length"} + + async def test_invalid_credentials(hass): """Test that invalid credentials show the correct error.""" with patch( @@ -42,10 +60,11 @@ async def test_invalid_credentials(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + result["flow_id"], + user_input={CONF_AUTH_CODE: VALID_AUTH_CODE}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_auth"} + assert result["errors"] == {CONF_AUTH_CODE: "invalid_auth"} async def test_options_flow(config_entry, hass): @@ -80,7 +99,7 @@ async def test_step_reauth(config_entry, hass, setup_simplisafe): "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "reauth_successful" @@ -104,14 +123,29 @@ async def test_step_reauth_wrong_account(config_entry, hass, setup_simplisafe): "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "wrong_account" -async def test_step_user(hass, setup_simplisafe): - """Test the user step.""" +@pytest.mark.parametrize( + "auth_code,log_statement", + [ + ( + VALID_AUTH_CODE, + None, + ), + ( + f"={VALID_AUTH_CODE}", + 'Stripping "=" from the start of the authorization code', + ), + ], +) +async def test_step_user(auth_code, caplog, hass, log_statement, setup_simplisafe): + """Test successfully completion of the user step.""" + caplog.set_level = logging.DEBUG + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -121,10 +155,13 @@ async def test_step_user(hass, setup_simplisafe): "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch("homeassistant.config_entries.ConfigEntries.async_reload"): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + result["flow_id"], user_input={CONF_AUTH_CODE: auth_code} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + if log_statement: + assert any(m for m in caplog.messages if log_statement in m) + assert len(hass.config_entries.async_entries()) == 1 [config_entry] = hass.config_entries.async_entries(DOMAIN) assert config_entry.data == {CONF_USERNAME: "12345", CONF_TOKEN: "token123"} @@ -143,7 +180,7 @@ async def test_unknown_error(hass, setup_simplisafe): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_AUTH_CODE: "code123"} + result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "unknown"} From 02ad4843b8d2c3fdb87c9e98043c14a457ca41c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Aug 2022 07:44:22 -1000 Subject: [PATCH 3142/3516] Fix flux_led ignored entries not being respected (#76173) --- .../components/flux_led/config_flow.py | 44 +++++++++++-------- tests/components/flux_led/test_config_flow.py | 27 ++++++++++++ 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index 61395d744b3..b245c0c2bc2 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -105,27 +105,33 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): assert mac_address is not None mac = dr.format_mac(mac_address) await self.async_set_unique_id(mac) - for entry in self._async_current_entries(include_ignore=False): - if entry.data[CONF_HOST] == device[ATTR_IPADDR] or ( - entry.unique_id - and ":" in entry.unique_id - and mac_matches_by_one(entry.unique_id, mac) + for entry in self._async_current_entries(include_ignore=True): + if not ( + entry.data.get(CONF_HOST) == device[ATTR_IPADDR] + or ( + entry.unique_id + and ":" in entry.unique_id + and mac_matches_by_one(entry.unique_id, mac) + ) ): - if ( - async_update_entry_from_discovery( - self.hass, entry, device, None, allow_update_mac - ) - or entry.state == config_entries.ConfigEntryState.SETUP_RETRY - ): - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) - ) - else: - async_dispatcher_send( - self.hass, - FLUX_LED_DISCOVERY_SIGNAL.format(entry_id=entry.entry_id), - ) + continue + if entry.source == config_entries.SOURCE_IGNORE: raise AbortFlow("already_configured") + if ( + async_update_entry_from_discovery( + self.hass, entry, device, None, allow_update_mac + ) + or entry.state == config_entries.ConfigEntryState.SETUP_RETRY + ): + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + else: + async_dispatcher_send( + self.hass, + FLUX_LED_DISCOVERY_SIGNAL.format(entry_id=entry.entry_id), + ) + raise AbortFlow("already_configured") async def _async_handle_discovery(self) -> FlowResult: """Handle any discovery.""" diff --git a/tests/components/flux_led/test_config_flow.py b/tests/components/flux_led/test_config_flow.py index 8abdb8e955b..3f1704f7e8c 100644 --- a/tests/components/flux_led/test_config_flow.py +++ b/tests/components/flux_led/test_config_flow.py @@ -695,3 +695,30 @@ async def test_options(hass: HomeAssistant): assert result2["data"] == user_input assert result2["data"] == config_entry.options assert hass.states.get("light.bulb_rgbcw_ddeeff") is not None + + +@pytest.mark.parametrize( + "source, data", + [ + (config_entries.SOURCE_DHCP, DHCP_DISCOVERY), + (config_entries.SOURCE_INTEGRATION_DISCOVERY, FLUX_DISCOVERY), + ], +) +async def test_discovered_can_be_ignored(hass, source, data): + """Test we abort if the mac was already ignored.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={}, + unique_id=MAC_ADDRESS, + source=config_entries.SOURCE_IGNORE, + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(), _patch_wifibulb(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": source}, data=data + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" From b5a6ee3c567aa50633ef47d342af685fb75e5219 Mon Sep 17 00:00:00 2001 From: y34hbuddy <47507530+y34hbuddy@users.noreply.github.com> Date: Thu, 4 Aug 2022 13:44:39 -0400 Subject: [PATCH 3143/3516] Refactor volvooncall to (mostly) use DataUpdateCoordinator (#75885) Co-authored-by: Martin Hjelmare --- .../components/volvooncall/__init__.py | 228 ++++++++++-------- .../components/volvooncall/binary_sensor.py | 38 ++- .../components/volvooncall/device_tracker.py | 10 +- homeassistant/components/volvooncall/lock.py | 18 +- .../components/volvooncall/sensor.py | 32 ++- .../components/volvooncall/switch.py | 19 +- 6 files changed, 209 insertions(+), 136 deletions(-) diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index 0f969d785df..cf385d320ca 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -2,8 +2,10 @@ from datetime import timedelta import logging +import async_timeout import voluptuous as vol from volvooncall import Connection +from volvooncall.dashboard import Instrument from homeassistant.const import ( CONF_NAME, @@ -17,14 +19,13 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType -from homeassistant.util.dt import utcnow +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) DOMAIN = "volvooncall" @@ -32,7 +33,6 @@ DATA_KEY = DOMAIN _LOGGER = logging.getLogger(__name__) -MIN_UPDATE_INTERVAL = timedelta(minutes=1) DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1) CONF_SERVICE_URL = "service_url" @@ -92,24 +92,31 @@ RESOURCES = [ CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_UPDATE_INTERVAL - ): vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL)), - vol.Optional(CONF_NAME, default={}): cv.schema_with_slug_keys( - cv.string - ), - vol.Optional(CONF_RESOURCES): vol.All( - cv.ensure_list, [vol.In(RESOURCES)] - ), - vol.Optional(CONF_REGION): cv.string, - vol.Optional(CONF_SERVICE_URL): cv.string, - vol.Optional(CONF_MUTABLE, default=True): cv.boolean, - vol.Optional(CONF_SCANDINAVIAN_MILES, default=False): cv.boolean, - } + DOMAIN: vol.All( + cv.deprecated(CONF_SCAN_INTERVAL), + cv.deprecated(CONF_NAME), + cv.deprecated(CONF_RESOURCES), + vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_UPDATE_INTERVAL + ): vol.All( + cv.time_period, vol.Clamp(min=DEFAULT_UPDATE_INTERVAL) + ), # ignored, using DataUpdateCoordinator instead + vol.Optional(CONF_NAME, default={}): cv.schema_with_slug_keys( + cv.string + ), # ignored, users can modify names of entities in the UI + vol.Optional(CONF_RESOURCES): vol.All( + cv.ensure_list, [vol.In(RESOURCES)] + ), # ignored, users can disable entities in the UI + vol.Optional(CONF_REGION): cv.string, + vol.Optional(CONF_SERVICE_URL): cv.string, + vol.Optional(CONF_MUTABLE, default=True): cv.boolean, + vol.Optional(CONF_SCANDINAVIAN_MILES, default=False): cv.boolean, + } + ), ) }, extra=vol.ALLOW_EXTRA, @@ -128,34 +135,70 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: region=config[DOMAIN].get(CONF_REGION), ) - interval = config[DOMAIN][CONF_SCAN_INTERVAL] + hass.data[DATA_KEY] = {} - data = hass.data[DATA_KEY] = VolvoData(config) + volvo_data = VolvoData(hass, connection, config) - def is_enabled(attr): - """Return true if the user has enabled the resource.""" - return attr in config[DOMAIN].get(CONF_RESOURCES, [attr]) + hass.data[DATA_KEY] = VolvoUpdateCoordinator(hass, volvo_data) - def discover_vehicle(vehicle): + return await volvo_data.update() + + +class VolvoData: + """Hold component state.""" + + def __init__( + self, + hass: HomeAssistant, + connection: Connection, + config: ConfigType, + ) -> None: + """Initialize the component state.""" + self.hass = hass + self.vehicles: set[str] = set() + self.instruments: set[Instrument] = set() + self.config = config + self.connection = connection + + def instrument(self, vin, component, attr, slug_attr): + """Return corresponding instrument.""" + return next( + instrument + for instrument in self.instruments + if instrument.vehicle.vin == vin + and instrument.component == component + and instrument.attr == attr + and instrument.slug_attr == slug_attr + ) + + def vehicle_name(self, vehicle): + """Provide a friendly name for a vehicle.""" + if vehicle.registration_number and vehicle.registration_number != "UNKNOWN": + return vehicle.registration_number + if vehicle.vin: + return vehicle.vin + return "Volvo" + + def discover_vehicle(self, vehicle): """Load relevant platforms.""" - data.vehicles.add(vehicle.vin) + self.vehicles.add(vehicle.vin) dashboard = vehicle.dashboard( - mutable=config[DOMAIN][CONF_MUTABLE], - scandinavian_miles=config[DOMAIN][CONF_SCANDINAVIAN_MILES], + mutable=self.config[DOMAIN][CONF_MUTABLE], + scandinavian_miles=self.config[DOMAIN][CONF_SCANDINAVIAN_MILES], ) for instrument in ( instrument for instrument in dashboard.instruments - if instrument.component in PLATFORMS and is_enabled(instrument.slug_attr) + if instrument.component in PLATFORMS ): - data.instruments.add(instrument) + self.instruments.add(instrument) - hass.async_create_task( + self.hass.async_create_task( discovery.async_load_platform( - hass, + self.hass, PLATFORMS[instrument.component], DOMAIN, ( @@ -164,93 +207,71 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: instrument.attr, instrument.slug_attr, ), - config, + self.config, ) ) - async def update(now): + async def update(self): """Update status from the online service.""" - try: - if not await connection.update(journal=True): - _LOGGER.warning("Could not query server") - return False + if not await self.connection.update(journal=True): + return False - for vehicle in connection.vehicles: - if vehicle.vin not in data.vehicles: - discover_vehicle(vehicle) + for vehicle in self.connection.vehicles: + if vehicle.vin not in self.vehicles: + self.discover_vehicle(vehicle) - async_dispatcher_send(hass, SIGNAL_STATE_UPDATED) + # this is currently still needed for device_tracker, which isn't using the update coordinator yet + async_dispatcher_send(self.hass, SIGNAL_STATE_UPDATED) - return True - finally: - async_track_point_in_utc_time(hass, update, utcnow() + interval) - - _LOGGER.info("Logging in to service") - return await update(utcnow()) + return True -class VolvoData: - """Hold component state.""" +class VolvoUpdateCoordinator(DataUpdateCoordinator): + """Volvo coordinator.""" - def __init__(self, config): - """Initialize the component state.""" - self.vehicles = set() - self.instruments = set() - self.config = config[DOMAIN] - self.names = self.config.get(CONF_NAME) + def __init__(self, hass: HomeAssistant, volvo_data: VolvoData) -> None: + """Initialize the data update coordinator.""" - def instrument(self, vin, component, attr, slug_attr): - """Return corresponding instrument.""" - return next( - ( - instrument - for instrument in self.instruments - if instrument.vehicle.vin == vin - and instrument.component == component - and instrument.attr == attr - and instrument.slug_attr == slug_attr - ), - None, + super().__init__( + hass, + _LOGGER, + name="volvooncall", + update_interval=DEFAULT_UPDATE_INTERVAL, ) - def vehicle_name(self, vehicle): - """Provide a friendly name for a vehicle.""" - if ( - vehicle.registration_number and vehicle.registration_number.lower() - ) in self.names: - return self.names[vehicle.registration_number.lower()] - if vehicle.vin and vehicle.vin.lower() in self.names: - return self.names[vehicle.vin.lower()] - if vehicle.registration_number: - return vehicle.registration_number - if vehicle.vin: - return vehicle.vin - return "" + self.volvo_data = volvo_data + + async def _async_update_data(self): + """Fetch data from API endpoint.""" + + async with async_timeout.timeout(10): + if not await self.volvo_data.update(): + raise UpdateFailed("Error communicating with API") -class VolvoEntity(Entity): +class VolvoEntity(CoordinatorEntity): """Base class for all VOC entities.""" - def __init__(self, data, vin, component, attribute, slug_attr): + def __init__( + self, + vin: str, + component: str, + attribute: str, + slug_attr: str, + coordinator: VolvoUpdateCoordinator, + ) -> None: """Initialize the entity.""" - self.data = data + super().__init__(coordinator) + self.vin = vin self.component = component self.attribute = attribute self.slug_attr = slug_attr - async def async_added_to_hass(self): - """Register update dispatcher.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_STATE_UPDATED, self.async_write_ha_state - ) - ) - @property def instrument(self): """Return corresponding instrument.""" - return self.data.instrument( + return self.coordinator.volvo_data.instrument( self.vin, self.component, self.attribute, self.slug_attr ) @@ -270,18 +291,13 @@ class VolvoEntity(Entity): @property def _vehicle_name(self): - return self.data.vehicle_name(self.vehicle) + return self.coordinator.volvo_data.vehicle_name(self.vehicle) @property def name(self): """Return full name of the entity.""" return f"{self._vehicle_name} {self._entity_name}" - @property - def should_poll(self): - """Return the polling state.""" - return False - @property def assumed_state(self): """Return true if unable to access real state of entity.""" diff --git a/homeassistant/components/volvooncall/binary_sensor.py b/homeassistant/components/volvooncall/binary_sensor.py index c8a0105128d..2aeaeff93e4 100644 --- a/homeassistant/components/volvooncall/binary_sensor.py +++ b/homeassistant/components/volvooncall/binary_sensor.py @@ -1,12 +1,19 @@ """Support for VOC.""" from __future__ import annotations -from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorEntity +from contextlib import suppress + +import voluptuous as vol + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES_SCHEMA, + BinarySensorEntity, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DATA_KEY, VolvoEntity +from . import DATA_KEY, VolvoEntity, VolvoUpdateCoordinator async def async_setup_platform( @@ -24,16 +31,25 @@ async def async_setup_platform( class VolvoSensor(VolvoEntity, BinarySensorEntity): """Representation of a Volvo sensor.""" + def __init__( + self, + coordinator: VolvoUpdateCoordinator, + vin: str, + component: str, + attribute: str, + slug_attr: str, + ) -> None: + """Initialize the sensor.""" + super().__init__(vin, component, attribute, slug_attr, coordinator) + + with suppress(vol.Invalid): + self._attr_device_class = DEVICE_CLASSES_SCHEMA( + self.instrument.device_class + ) + @property - def is_on(self): - """Return True if the binary sensor is on, but invert for the 'Door lock'.""" + def is_on(self) -> bool | None: + """Fetch from update coordinator.""" if self.instrument.attr == "is_locked": return not self.instrument.is_on return self.instrument.is_on - - @property - def device_class(self): - """Return the class of this sensor, from DEVICE_CLASSES.""" - if self.instrument.device_class in DEVICE_CLASSES: - return self.instrument.device_class - return None diff --git a/homeassistant/components/volvooncall/device_tracker.py b/homeassistant/components/volvooncall/device_tracker.py index 4f9300fd021..ffed8005f36 100644 --- a/homeassistant/components/volvooncall/device_tracker.py +++ b/homeassistant/components/volvooncall/device_tracker.py @@ -7,7 +7,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import slugify -from . import DATA_KEY, SIGNAL_STATE_UPDATED +from . import DATA_KEY, SIGNAL_STATE_UPDATED, VolvoUpdateCoordinator async def async_setup_scanner( @@ -21,8 +21,12 @@ async def async_setup_scanner( return False vin, component, attr, slug_attr = discovery_info - data = hass.data[DATA_KEY] - instrument = data.instrument(vin, component, attr, slug_attr) + coordinator: VolvoUpdateCoordinator = hass.data[DATA_KEY] + volvo_data = coordinator.volvo_data + instrument = volvo_data.instrument(vin, component, attr, slug_attr) + + if instrument is None: + return False async def see_vehicle() -> None: """Handle the reporting of the vehicle position.""" diff --git a/homeassistant/components/volvooncall/lock.py b/homeassistant/components/volvooncall/lock.py index c341627eef4..da36ca2e573 100644 --- a/homeassistant/components/volvooncall/lock.py +++ b/homeassistant/components/volvooncall/lock.py @@ -1,4 +1,5 @@ """Support for Volvo On Call locks.""" + from __future__ import annotations from typing import Any @@ -10,7 +11,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DATA_KEY, VolvoEntity +from . import DATA_KEY, VolvoEntity, VolvoUpdateCoordinator async def async_setup_platform( @@ -31,15 +32,28 @@ class VolvoLock(VolvoEntity, LockEntity): instrument: Lock + def __init__( + self, + coordinator: VolvoUpdateCoordinator, + vin: str, + component: str, + attribute: str, + slug_attr: str, + ) -> None: + """Initialize the lock.""" + super().__init__(vin, component, attribute, slug_attr, coordinator) + @property def is_locked(self) -> bool | None: - """Return true if lock is locked.""" + """Determine if car is locked.""" return self.instrument.is_locked async def async_lock(self, **kwargs: Any) -> None: """Lock the car.""" await self.instrument.lock() + await self.coordinator.async_request_refresh() async def async_unlock(self, **kwargs: Any) -> None: """Unlock the car.""" await self.instrument.unlock() + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/volvooncall/sensor.py b/homeassistant/components/volvooncall/sensor.py index bdc1b57f588..41426ff878a 100644 --- a/homeassistant/components/volvooncall/sensor.py +++ b/homeassistant/components/volvooncall/sensor.py @@ -2,11 +2,11 @@ from __future__ import annotations from homeassistant.components.sensor import SensorEntity -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DATA_KEY, VolvoEntity +from . import DATA_KEY, VolvoEntity, VolvoUpdateCoordinator async def async_setup_platform( @@ -24,12 +24,24 @@ async def async_setup_platform( class VolvoSensor(VolvoEntity, SensorEntity): """Representation of a Volvo sensor.""" - @property - def native_value(self): - """Return the state.""" - return self.instrument.state + def __init__( + self, + coordinator: VolvoUpdateCoordinator, + vin: str, + component: str, + attribute: str, + slug_attr: str, + ) -> None: + """Initialize the sensor.""" + super().__init__(vin, component, attribute, slug_attr, coordinator) + self._update_value_and_unit() - @property - def native_unit_of_measurement(self): - """Return the unit of measurement.""" - return self.instrument.unit + def _update_value_and_unit(self) -> None: + self._attr_native_value = self.instrument.state + self._attr_native_unit_of_measurement = self.instrument.unit + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_value_and_unit() + self.async_write_ha_state() diff --git a/homeassistant/components/volvooncall/switch.py b/homeassistant/components/volvooncall/switch.py index 63758ac010b..6c8519f12e8 100644 --- a/homeassistant/components/volvooncall/switch.py +++ b/homeassistant/components/volvooncall/switch.py @@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DATA_KEY, VolvoEntity +from . import DATA_KEY, VolvoEntity, VolvoUpdateCoordinator async def async_setup_platform( @@ -24,17 +24,28 @@ async def async_setup_platform( class VolvoSwitch(VolvoEntity, SwitchEntity): """Representation of a Volvo switch.""" + def __init__( + self, + coordinator: VolvoUpdateCoordinator, + vin: str, + component: str, + attribute: str, + slug_attr: str, + ) -> None: + """Initialize the switch.""" + super().__init__(vin, component, attribute, slug_attr, coordinator) + @property def is_on(self): - """Return true if switch is on.""" + """Determine if switch is on.""" return self.instrument.state async def async_turn_on(self, **kwargs): """Turn the switch on.""" await self.instrument.turn_on() - self.async_write_ha_state() + await self.coordinator.async_request_refresh() async def async_turn_off(self, **kwargs): """Turn the switch off.""" await self.instrument.turn_off() - self.async_write_ha_state() + await self.coordinator.async_request_refresh() From 639a522caa7fa8f8801be50f2ae12fbba7ac77e4 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 4 Aug 2022 11:45:28 -0600 Subject: [PATCH 3144/3516] Add repair item to remove no-longer-functioning Flu Near You integration (#76177) Co-authored-by: Franck Nijhof --- .coveragerc | 1 + .../components/flunearyou/__init__.py | 10 +++++ .../components/flunearyou/manifest.json | 1 + .../components/flunearyou/repairs.py | 42 +++++++++++++++++++ .../components/flunearyou/strings.json | 13 ++++++ .../flunearyou/translations/en.json | 13 ++++++ 6 files changed, 80 insertions(+) create mode 100644 homeassistant/components/flunearyou/repairs.py diff --git a/.coveragerc b/.coveragerc index 3c587e11cf6..341494b1424 100644 --- a/.coveragerc +++ b/.coveragerc @@ -388,6 +388,7 @@ omit = homeassistant/components/flume/__init__.py homeassistant/components/flume/sensor.py homeassistant/components/flunearyou/__init__.py + homeassistant/components/flunearyou/repairs.py homeassistant/components/flunearyou/sensor.py homeassistant/components/folder/sensor.py homeassistant/components/folder_watcher/* diff --git a/homeassistant/components/flunearyou/__init__.py b/homeassistant/components/flunearyou/__init__.py index 5e48e1561b5..75349002ec0 100644 --- a/homeassistant/components/flunearyou/__init__.py +++ b/homeassistant/components/flunearyou/__init__.py @@ -9,6 +9,7 @@ from typing import Any from pyflunearyou import Client from pyflunearyou.errors import FluNearYouError +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform from homeassistant.core import HomeAssistant @@ -26,6 +27,15 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Flu Near You as config entry.""" + async_create_issue( + hass, + DOMAIN, + "integration_removal", + is_fixable=True, + severity=IssueSeverity.ERROR, + translation_key="integration_removal", + ) + websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) diff --git a/homeassistant/components/flunearyou/manifest.json b/homeassistant/components/flunearyou/manifest.json index ee69961d1b0..fa98bf2e01e 100644 --- a/homeassistant/components/flunearyou/manifest.json +++ b/homeassistant/components/flunearyou/manifest.json @@ -3,6 +3,7 @@ "name": "Flu Near You", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/flunearyou", + "dependencies": ["repairs"], "requirements": ["pyflunearyou==2.0.2"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", diff --git a/homeassistant/components/flunearyou/repairs.py b/homeassistant/components/flunearyou/repairs.py new file mode 100644 index 00000000000..f48085ba623 --- /dev/null +++ b/homeassistant/components/flunearyou/repairs.py @@ -0,0 +1,42 @@ +"""Repairs platform for the Flu Near You integration.""" +from __future__ import annotations + +import asyncio + +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components.repairs import RepairsFlow +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + + +class FluNearYouFixFlow(RepairsFlow): + """Handler for an issue fixing flow.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + return await self.async_step_confirm() + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + removal_tasks = [ + self.hass.config_entries.async_remove(entry.entry_id) + for entry in self.hass.config_entries.async_entries(DOMAIN) + ] + await asyncio.gather(*removal_tasks) + return self.async_create_entry(title="Fixed issue", data={}) + return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) + + +async def async_create_fix_flow( + hass: HomeAssistant, issue_id: str +) -> FluNearYouFixFlow: + """Create flow.""" + return FluNearYouFixFlow() diff --git a/homeassistant/components/flunearyou/strings.json b/homeassistant/components/flunearyou/strings.json index 4df0326fc3b..59ec6125a34 100644 --- a/homeassistant/components/flunearyou/strings.json +++ b/homeassistant/components/flunearyou/strings.json @@ -16,5 +16,18 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" } + }, + "issues": { + "integration_removal": { + "title": "Flu Near You is no longer available", + "fix_flow": { + "step": { + "confirm": { + "title": "Remove Flu Near You", + "description": "The external data source powering the Flu Near You integration is no longer available; thus, the integration no longer works.\n\nPress SUBMIT to remove Flu Near You from your Home Assistant instance." + } + } + } + } } } diff --git a/homeassistant/components/flunearyou/translations/en.json b/homeassistant/components/flunearyou/translations/en.json index 29af5b2b288..3dcbfa2a628 100644 --- a/homeassistant/components/flunearyou/translations/en.json +++ b/homeassistant/components/flunearyou/translations/en.json @@ -16,5 +16,18 @@ "title": "Configure Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "The data source that powered the Flu Near You integration is no longer available. Press SUBMIT to remove all configured instances of the integration from Home Assistant.", + "title": "Remove Flu Near You" + } + } + }, + "title": "Flu Near You is no longer available" + } } } \ No newline at end of file From b2ceb2043b00545414d09b12f264f6baa61402fe Mon Sep 17 00:00:00 2001 From: On Freund Date: Thu, 4 Aug 2022 21:57:53 +0300 Subject: [PATCH 3145/3516] Fix arm away in Risco (#76188) --- homeassistant/components/risco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index a7c07af3e18..fb4b8203aac 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -3,7 +3,7 @@ "name": "Risco", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/risco", - "requirements": ["pyrisco==0.5.0"], + "requirements": ["pyrisco==0.5.2"], "codeowners": ["@OnFreund"], "quality_scale": "platinum", "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 48d2e47086a..c1badeb18de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1790,7 +1790,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.5.0 +pyrisco==0.5.2 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 08e47a7aded..7ef2f0ff1b0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1231,7 +1231,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.risco -pyrisco==0.5.0 +pyrisco==0.5.2 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 From 31d9425e4941b4b7b55804f8fc2aa605eab2791a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Aug 2022 20:58:12 +0200 Subject: [PATCH 3146/3516] Add entity category to Wiz number entities (#76191) --- homeassistant/components/wiz/number.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/wiz/number.py b/homeassistant/components/wiz/number.py index 9fd700f8f9c..fc855e410fe 100644 --- a/homeassistant/components/wiz/number.py +++ b/homeassistant/components/wiz/number.py @@ -14,6 +14,7 @@ from homeassistant.components.number import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -56,6 +57,7 @@ NUMBERS: tuple[WizNumberEntityDescription, ...] = ( value_fn=lambda device: cast(Optional[int], device.state.get_speed()), set_value_fn=_async_set_speed, required_feature="effect", + entity_category=EntityCategory.CONFIG, ), WizNumberEntityDescription( key="dual_head_ratio", @@ -67,6 +69,7 @@ NUMBERS: tuple[WizNumberEntityDescription, ...] = ( value_fn=lambda device: cast(Optional[int], device.state.get_ratio()), set_value_fn=_async_set_ratio, required_feature="dual_head", + entity_category=EntityCategory.CONFIG, ), ) From 0ffdf9fb6e60e37e480c4c3da9f839af8a42274f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Aug 2022 21:03:13 +0200 Subject: [PATCH 3147/3516] Add device_tracker checks to pylint plugin (#76228) --- pylint/plugins/hass_enforce_type_hints.py | 73 +++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index d0d20cedd7c..527c9358971 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1135,6 +1135,79 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "device_tracker": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="BaseTrackerEntity", + matches=[ + TypeHintMatch( + function_name="battery_level", + return_type=["int", None], + ), + TypeHintMatch( + function_name="source_type", + return_type=["SourceType", "str"], + ), + ], + ), + ClassTypeHintMatch( + base_class="TrackerEntity", + matches=[ + TypeHintMatch( + function_name="force_update", + return_type="bool", + ), + TypeHintMatch( + function_name="location_accuracy", + return_type="int", + ), + TypeHintMatch( + function_name="location_name", + return_type=["str", None], + ), + TypeHintMatch( + function_name="latitude", + return_type=["float", None], + ), + TypeHintMatch( + function_name="longitude", + return_type=["float", None], + ), + TypeHintMatch( + function_name="state", + return_type=["str", None], + ), + ], + ), + ClassTypeHintMatch( + base_class="ScannerEntity", + matches=[ + TypeHintMatch( + function_name="ip_address", + return_type=["str", None], + ), + TypeHintMatch( + function_name="mac_address", + return_type=["str", None], + ), + TypeHintMatch( + function_name="hostname", + return_type=["str", None], + ), + TypeHintMatch( + function_name="state", + return_type="str", + ), + TypeHintMatch( + function_name="is_connected", + return_type="bool", + ), + ], + ), + ], "fan": [ ClassTypeHintMatch( base_class="Entity", From 0df4642b62b52fe3f0dd0a627fc4387aba729495 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Aug 2022 21:03:26 +0200 Subject: [PATCH 3148/3516] Remove YAML configuration from Simplepush (#76175) --- .../components/simplepush/config_flow.py | 13 --------- homeassistant/components/simplepush/notify.py | 27 +++++-------------- .../components/simplepush/strings.json | 6 ++--- .../simplepush/translations/en.json | 6 ++--- .../components/simplepush/test_config_flow.py | 13 --------- 5 files changed, 12 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/simplepush/config_flow.py b/homeassistant/components/simplepush/config_flow.py index cf08a341114..0f0073c5099 100644 --- a/homeassistant/components/simplepush/config_flow.py +++ b/homeassistant/components/simplepush/config_flow.py @@ -1,7 +1,6 @@ """Config flow for simplepush integration.""" from __future__ import annotations -import logging from typing import Any from simplepush import UnknownError, send, send_encrypted @@ -13,8 +12,6 @@ from homeassistant.data_entry_flow import FlowResult from .const import ATTR_ENCRYPTED, CONF_DEVICE_KEY, CONF_SALT, DEFAULT_NAME, DOMAIN -_LOGGER = logging.getLogger(__name__) - def validate_input(entry: dict[str, str]) -> dict[str, str] | None: """Validate user input.""" @@ -76,13 +73,3 @@ class SimplePushFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ), errors=errors, ) - - async def async_step_import(self, import_config: dict[str, str]) -> FlowResult: - """Import a config entry from configuration.yaml.""" - _LOGGER.warning( - "Configuration of the simplepush integration in YAML is deprecated and " - "will be removed in a future release; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) - return await self.async_step_user(import_config) diff --git a/homeassistant/components/simplepush/notify.py b/homeassistant/components/simplepush/notify.py index 2e58748f323..8bcf166ad25 100644 --- a/homeassistant/components/simplepush/notify.py +++ b/homeassistant/components/simplepush/notify.py @@ -5,34 +5,24 @@ import logging from typing import Any from simplepush import BadRequest, UnknownError, send, send_encrypted -import voluptuous as vol from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.components.notify.const import ATTR_DATA from homeassistant.components.repairs.issue_handler import async_create_issue from homeassistant.components.repairs.models import IssueSeverity -from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_EVENT, CONF_PASSWORD from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import ATTR_ENCRYPTED, ATTR_EVENT, CONF_DEVICE_KEY, CONF_SALT, DOMAIN +from .const import ATTR_EVENT, CONF_DEVICE_KEY, CONF_SALT, DOMAIN -# Configuring simplepush under the notify platform will be removed in 2022.9.0 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_DEVICE_KEY): cv.string, - vol.Optional(CONF_EVENT): cv.string, - vol.Inclusive(CONF_PASSWORD, ATTR_ENCRYPTED): cv.string, - vol.Inclusive(CONF_SALT, ATTR_ENCRYPTED): cv.string, - } -) +# Configuring Simplepush under the notify has been removed in 2022.9.0 +PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -47,16 +37,11 @@ async def async_get_service( async_create_issue( hass, DOMAIN, - "deprecated_yaml", + "removed_yaml", breaks_in_ha_version="2022.9.0", is_fixable=False, severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) + translation_key="removed_yaml", ) return None diff --git a/homeassistant/components/simplepush/strings.json b/homeassistant/components/simplepush/strings.json index 77ed05c4b48..68ee5c7a9ed 100644 --- a/homeassistant/components/simplepush/strings.json +++ b/homeassistant/components/simplepush/strings.json @@ -19,9 +19,9 @@ } }, "issues": { - "deprecated_yaml": { - "title": "The Simplepush YAML configuration is being removed", - "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + "removed_yaml": { + "title": "The Simplepush YAML configuration has been removed", + "description": "Configuring Simplepush using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." } } } diff --git a/homeassistant/components/simplepush/translations/en.json b/homeassistant/components/simplepush/translations/en.json index 8674616dda1..205d3549a52 100644 --- a/homeassistant/components/simplepush/translations/en.json +++ b/homeassistant/components/simplepush/translations/en.json @@ -19,9 +19,9 @@ } }, "issues": { - "deprecated_yaml": { - "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", - "title": "The Simplepush YAML configuration is being removed" + "removed_yaml": { + "description": "Configuring Simplepush using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Simplepush YAML configuration has been removed" } } } \ No newline at end of file diff --git a/tests/components/simplepush/test_config_flow.py b/tests/components/simplepush/test_config_flow.py index 6c37fb7ffe6..6f0d6a73aa4 100644 --- a/tests/components/simplepush/test_config_flow.py +++ b/tests/components/simplepush/test_config_flow.py @@ -129,16 +129,3 @@ async def test_error_on_connection_failure(hass: HomeAssistant) -> None: ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "cannot_connect"} - - -async def test_flow_import(hass: HomeAssistant) -> None: - """Test an import flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=MOCK_CONFIG, - ) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "simplepush" - assert result["data"] == MOCK_CONFIG From 3d42c4ca87bd8bb23d4a65c7e7792f04c8dd0e4f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 4 Aug 2022 13:22:10 -0600 Subject: [PATCH 3149/3516] Add reboot button to RainMachine (#75227) --- .coveragerc | 1 + .../components/rainmachine/__init__.py | 58 +++++------- .../components/rainmachine/button.py | 90 ++++++++++++++++++ .../components/rainmachine/manifest.json | 2 +- homeassistant/components/rainmachine/util.py | 93 +++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 212 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/rainmachine/button.py diff --git a/.coveragerc b/.coveragerc index 341494b1424..20c6fd2c60e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -976,6 +976,7 @@ omit = homeassistant/components/raincloud/* homeassistant/components/rainmachine/__init__.py homeassistant/components/rainmachine/binary_sensor.py + homeassistant/components/rainmachine/button.py homeassistant/components/rainmachine/model.py homeassistant/components/rainmachine/sensor.py homeassistant/components/rainmachine/switch.py diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index c30ce81dc6d..dccdaaba74c 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -30,11 +30,7 @@ from homeassistant.helpers import ( entity_registry as er, ) from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity, UpdateFailed from homeassistant.util.network import is_ip_address from .config_flow import get_client_controller @@ -49,20 +45,13 @@ from .const import ( LOGGER, ) from .model import RainMachineEntityDescription +from .util import RainMachineDataUpdateCoordinator DEFAULT_SSL = True CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] - -UPDATE_INTERVALS = { - DATA_PROVISION_SETTINGS: timedelta(minutes=1), - DATA_PROGRAMS: timedelta(seconds=30), - DATA_RESTRICTIONS_CURRENT: timedelta(minutes=1), - DATA_RESTRICTIONS_UNIVERSAL: timedelta(minutes=1), - DATA_ZONES: timedelta(seconds=15), -} +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] CONF_CONDITION = "condition" CONF_DEWPOINT = "dewpoint" @@ -134,13 +123,21 @@ SERVICE_RESTRICT_WATERING_SCHEMA = SERVICE_SCHEMA.extend( } ) +COORDINATOR_UPDATE_INTERVAL_MAP = { + DATA_PROVISION_SETTINGS: timedelta(minutes=1), + DATA_PROGRAMS: timedelta(seconds=30), + DATA_RESTRICTIONS_CURRENT: timedelta(minutes=1), + DATA_RESTRICTIONS_UNIVERSAL: timedelta(minutes=1), + DATA_ZONES: timedelta(seconds=15), +} + @dataclass class RainMachineData: """Define an object to be stored in `hass.data`.""" controller: Controller - coordinators: dict[str, DataUpdateCoordinator] + coordinators: dict[str, RainMachineDataUpdateCoordinator] @callback @@ -233,24 +230,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return data + async def async_init_coordinator( + coordinator: RainMachineDataUpdateCoordinator, + ) -> None: + """Initialize a RainMachineDataUpdateCoordinator.""" + await coordinator.async_initialize() + await coordinator.async_config_entry_first_refresh() + controller_init_tasks = [] coordinators = {} - - for api_category in ( - DATA_PROGRAMS, - DATA_PROVISION_SETTINGS, - DATA_RESTRICTIONS_CURRENT, - DATA_RESTRICTIONS_UNIVERSAL, - DATA_ZONES, - ): - coordinator = coordinators[api_category] = DataUpdateCoordinator( + for api_category, update_interval in COORDINATOR_UPDATE_INTERVAL_MAP.items(): + coordinator = coordinators[api_category] = RainMachineDataUpdateCoordinator( hass, - LOGGER, + entry=entry, name=f'{controller.name} ("{api_category}")', - update_interval=UPDATE_INTERVALS[api_category], + api_category=api_category, + update_interval=update_interval, update_method=partial(async_update, api_category), ) - controller_init_tasks.append(coordinator.async_refresh()) + controller_init_tasks.append(async_init_coordinator(coordinator)) await asyncio.gather(*controller_init_tasks) @@ -439,12 +437,6 @@ class RainMachineEntity(CoordinatorEntity): self.update_from_latest_data() self.async_write_ha_state() - async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" - await super().async_added_to_hass() - self.update_from_latest_data() - @callback def update_from_latest_data(self) -> None: """Update the state.""" - raise NotImplementedError diff --git a/homeassistant/components/rainmachine/button.py b/homeassistant/components/rainmachine/button.py new file mode 100644 index 00000000000..14bfb878642 --- /dev/null +++ b/homeassistant/components/rainmachine/button.py @@ -0,0 +1,90 @@ +"""Buttons for the RainMachine integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from regenmaschine.controller import Controller +from regenmaschine.errors import RainMachineError + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import RainMachineData, RainMachineEntity +from .const import DATA_PROVISION_SETTINGS, DOMAIN +from .model import RainMachineEntityDescription + + +@dataclass +class RainMachineButtonDescriptionMixin: + """Define an entity description mixin for RainMachine buttons.""" + + push_action: Callable[[Controller], Awaitable] + + +@dataclass +class RainMachineButtonDescription( + ButtonEntityDescription, + RainMachineEntityDescription, + RainMachineButtonDescriptionMixin, +): + """Describe a RainMachine button description.""" + + +BUTTON_KIND_REBOOT = "reboot" + + +async def _async_reboot(controller: Controller) -> None: + """Reboot the RainMachine.""" + await controller.machine.reboot() + + +BUTTON_DESCRIPTIONS = ( + RainMachineButtonDescription( + key=BUTTON_KIND_REBOOT, + name="Reboot", + api_category=DATA_PROVISION_SETTINGS, + push_action=_async_reboot, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up RainMachine buttons based on a config entry.""" + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + RainMachineButton(entry, data, description) + for description in BUTTON_DESCRIPTIONS + ) + + +class RainMachineButton(RainMachineEntity, ButtonEntity): + """Define a RainMachine button.""" + + _attr_device_class = ButtonDeviceClass.RESTART + _attr_entity_category = EntityCategory.CONFIG + + entity_description: RainMachineButtonDescription + + async def async_press(self) -> None: + """Send out a restart command.""" + try: + await self.entity_description.push_action(self._data.controller) + except RainMachineError as err: + raise HomeAssistantError( + f'Error while pressing button "{self.entity_id}": {err}' + ) from err + + async_dispatcher_send(self.hass, self.coordinator.signal_reboot_requested) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index b318ef7f295..4d60730ba6c 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.07.1"], + "requirements": ["regenmaschine==2022.07.3"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/homeassistant/components/rainmachine/util.py b/homeassistant/components/rainmachine/util.py index 6bf15f2fb9c..dc772690ec5 100644 --- a/homeassistant/components/rainmachine/util.py +++ b/homeassistant/components/rainmachine/util.py @@ -1,9 +1,23 @@ """Define RainMachine utilities.""" from __future__ import annotations +from collections.abc import Awaitable, Callable +from datetime import timedelta from typing import Any from homeassistant.backports.enum import StrEnum +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import LOGGER + +SIGNAL_REBOOT_COMPLETED = "rainmachine_reboot_completed_{0}" +SIGNAL_REBOOT_REQUESTED = "rainmachine_reboot_requested_{0}" class RunStates(StrEnum): @@ -29,3 +43,82 @@ def key_exists(data: dict[str, Any], search_key: str) -> bool: if isinstance(value, dict): return key_exists(value, search_key) return False + + +class RainMachineDataUpdateCoordinator(DataUpdateCoordinator[dict]): + """Define an extended DataUpdateCoordinator.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + *, + entry: ConfigEntry, + name: str, + api_category: str, + update_interval: timedelta, + update_method: Callable[..., Awaitable], + ) -> None: + """Initialize.""" + super().__init__( + hass, + LOGGER, + name=name, + update_interval=update_interval, + update_method=update_method, + ) + + self._rebooting = False + self._signal_handler_unsubs: list[Callable[..., None]] = [] + self.config_entry = entry + self.signal_reboot_completed = SIGNAL_REBOOT_COMPLETED.format( + self.config_entry.entry_id + ) + self.signal_reboot_requested = SIGNAL_REBOOT_REQUESTED.format( + self.config_entry.entry_id + ) + + async def async_initialize(self) -> None: + """Initialize the coordinator.""" + + @callback + def async_reboot_completed() -> None: + """Respond to a reboot completed notification.""" + LOGGER.debug("%s responding to reboot complete", self.name) + self._rebooting = False + self.last_update_success = True + self.async_update_listeners() + + @callback + def async_reboot_requested() -> None: + """Respond to a reboot request.""" + LOGGER.debug("%s responding to reboot request", self.name) + self._rebooting = True + self.last_update_success = False + self.async_update_listeners() + + for signal, func in ( + (self.signal_reboot_completed, async_reboot_completed), + (self.signal_reboot_requested, async_reboot_requested), + ): + self._signal_handler_unsubs.append( + async_dispatcher_connect(self.hass, signal, func) + ) + + @callback + def async_check_reboot_complete() -> None: + """Check whether an active reboot has been completed.""" + if self._rebooting and self.last_update_success: + LOGGER.debug("%s discovered reboot complete", self.name) + async_dispatcher_send(self.hass, self.signal_reboot_completed) + + self.async_add_listener(async_check_reboot_complete) + + @callback + def async_teardown() -> None: + """Tear the coordinator down appropriately.""" + for unsub in self._signal_handler_unsubs: + unsub() + + self.config_entry.async_on_unload(async_teardown) diff --git a/requirements_all.txt b/requirements_all.txt index c1badeb18de..3abb34ff5da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2081,7 +2081,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.07.1 +regenmaschine==2022.07.3 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ef2f0ff1b0..96845b12579 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1402,7 +1402,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rainmachine -regenmaschine==2022.07.1 +regenmaschine==2022.07.3 # homeassistant.components.renault renault-api==0.1.11 From 343508a0151378ec4958bd04fa87ca772aaf0e4e Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Thu, 4 Aug 2022 14:28:59 -0500 Subject: [PATCH 3150/3516] Fix Life360 recovery from server errors (#76231) --- .../components/life360/coordinator.py | 4 +- .../components/life360/device_tracker.py | 86 +++++++++++-------- 2 files changed, 53 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/life360/coordinator.py b/homeassistant/components/life360/coordinator.py index 05eecd43cdc..ed774bba8ca 100644 --- a/homeassistant/components/life360/coordinator.py +++ b/homeassistant/components/life360/coordinator.py @@ -91,9 +91,11 @@ class Life360Data: members: dict[str, Life360Member] = field(init=False, default_factory=dict) -class Life360DataUpdateCoordinator(DataUpdateCoordinator): +class Life360DataUpdateCoordinator(DataUpdateCoordinator[Life360Data]): """Life360 data update coordinator.""" + config_entry: ConfigEntry + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: """Initialize data update coordinator.""" super().__init__( diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index ebb179beba2..1fa63a7659a 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -11,10 +11,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_BATTERY_CHARGING from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( ATTR_ADDRESS, @@ -31,6 +28,7 @@ from .const import ( LOGGER, SHOW_DRIVING, ) +from .coordinator import Life360DataUpdateCoordinator, Life360Member _LOC_ATTRS = ( "address", @@ -95,23 +93,27 @@ async def async_setup_entry( entry.async_on_unload(coordinator.async_add_listener(process_data)) -class Life360DeviceTracker(CoordinatorEntity, TrackerEntity): +class Life360DeviceTracker( + CoordinatorEntity[Life360DataUpdateCoordinator], TrackerEntity +): """Life360 Device Tracker.""" _attr_attribution = ATTRIBUTION + _attr_unique_id: str - def __init__(self, coordinator: DataUpdateCoordinator, member_id: str) -> None: + def __init__( + self, coordinator: Life360DataUpdateCoordinator, member_id: str + ) -> None: """Initialize Life360 Entity.""" super().__init__(coordinator) self._attr_unique_id = member_id - self._data = coordinator.data.members[self.unique_id] + self._data: Life360Member | None = coordinator.data.members[member_id] + self._prev_data = self._data self._attr_name = self._data.name self._attr_entity_picture = self._data.entity_picture - self._prev_data = self._data - @property def _options(self) -> Mapping[str, Any]: """Shortcut to config entry options.""" @@ -120,16 +122,15 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity): @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - # Get a shortcut to this member's data. Can't guarantee it's the same dict every - # update, or that there is even data for this member every update, so need to - # update shortcut each time. - self._data = self.coordinator.data.members.get(self.unique_id) - + # Get a shortcut to this Member's data. This needs to be updated each time since + # coordinator provides a new Life360Member object each time, and it's possible + # that there is no data for this Member on some updates. if self.available: - # If nothing important has changed, then skip the update altogether. - if self._data == self._prev_data: - return + self._data = self.coordinator.data.members.get(self._attr_unique_id) + else: + self._data = None + if self._data: # Check if we should effectively throw out new location data. last_seen = self._data.last_seen prev_seen = self._prev_data.last_seen @@ -168,27 +169,21 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity): """Return True if state updates should be forced.""" return False - @property - def available(self) -> bool: - """Return if entity is available.""" - # Guard against member not being in last update for some reason. - return super().available and self._data is not None - @property def entity_picture(self) -> str | None: """Return the entity picture to use in the frontend, if any.""" - if self.available: + if self._data: self._attr_entity_picture = self._data.entity_picture return super().entity_picture - # All of the following will only be called if self.available is True. - @property def battery_level(self) -> int | None: """Return the battery level of the device. Percentage from 0-100. """ + if not self._data: + return None return self._data.battery_level @property @@ -202,11 +197,15 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity): Value in meters. """ + if not self._data: + return 0 return self._data.gps_accuracy @property def driving(self) -> bool: """Return if driving.""" + if not self._data: + return False if (driving_speed := self._options.get(CONF_DRIVING_SPEED)) is not None: if self._data.speed >= driving_speed: return True @@ -222,23 +221,38 @@ class Life360DeviceTracker(CoordinatorEntity, TrackerEntity): @property def latitude(self) -> float | None: """Return latitude value of the device.""" + if not self._data: + return None return self._data.latitude @property def longitude(self) -> float | None: """Return longitude value of the device.""" + if not self._data: + return None return self._data.longitude @property def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return entity specific state attributes.""" - attrs = {} - attrs[ATTR_ADDRESS] = self._data.address - attrs[ATTR_AT_LOC_SINCE] = self._data.at_loc_since - attrs[ATTR_BATTERY_CHARGING] = self._data.battery_charging - attrs[ATTR_DRIVING] = self.driving - attrs[ATTR_LAST_SEEN] = self._data.last_seen - attrs[ATTR_PLACE] = self._data.place - attrs[ATTR_SPEED] = self._data.speed - attrs[ATTR_WIFI_ON] = self._data.wifi_on - return attrs + if not self._data: + return { + ATTR_ADDRESS: None, + ATTR_AT_LOC_SINCE: None, + ATTR_BATTERY_CHARGING: None, + ATTR_DRIVING: None, + ATTR_LAST_SEEN: None, + ATTR_PLACE: None, + ATTR_SPEED: None, + ATTR_WIFI_ON: None, + } + return { + ATTR_ADDRESS: self._data.address, + ATTR_AT_LOC_SINCE: self._data.at_loc_since, + ATTR_BATTERY_CHARGING: self._data.battery_charging, + ATTR_DRIVING: self.driving, + ATTR_LAST_SEEN: self._data.last_seen, + ATTR_PLACE: self._data.place, + ATTR_SPEED: self._data.speed, + ATTR_WIFI_ON: self._data.wifi_on, + } From d76ebbbb0b0ddb6b6cef320227b5e2a77af8fce1 Mon Sep 17 00:00:00 2001 From: Jonathan Keslin Date: Thu, 4 Aug 2022 12:37:20 -0700 Subject: [PATCH 3151/3516] Remove @decompil3d as maintainer on volvooncall (#76153) * Remove @decompil3d as maintainer on volvooncall * Run hassfest Signed-off-by: Franck Nijhof Co-authored-by: Franck Nijhof --- CODEOWNERS | 2 +- homeassistant/components/volvooncall/manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 16523cafa81..1a322b09981 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1176,7 +1176,7 @@ build.json @home-assistant/supervisor /tests/components/vlc_telnet/ @rodripf @MartinHjelmare /homeassistant/components/volumio/ @OnFreund /tests/components/volumio/ @OnFreund -/homeassistant/components/volvooncall/ @molobrakos @decompil3d +/homeassistant/components/volvooncall/ @molobrakos /homeassistant/components/vulcan/ @Antoni-Czaplicki /tests/components/vulcan/ @Antoni-Czaplicki /homeassistant/components/wake_on_lan/ @ntilley905 diff --git a/homeassistant/components/volvooncall/manifest.json b/homeassistant/components/volvooncall/manifest.json index 93b7642425c..fe7e384f72a 100644 --- a/homeassistant/components/volvooncall/manifest.json +++ b/homeassistant/components/volvooncall/manifest.json @@ -3,7 +3,7 @@ "name": "Volvo On Call", "documentation": "https://www.home-assistant.io/integrations/volvooncall", "requirements": ["volvooncall==0.10.0"], - "codeowners": ["@molobrakos", "@decompil3d"], + "codeowners": ["@molobrakos"], "iot_class": "cloud_polling", "loggers": ["geopy", "hbmqtt", "volvooncall"] } From 33bf94c4b2d541edf155ffa6cdae0c0459e9eb74 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 4 Aug 2022 21:37:57 +0200 Subject: [PATCH 3152/3516] Update orjson to 3.7.11 (#76171) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3ff9fd3c114..c0221647abf 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 -orjson==3.7.8 +orjson==3.7.11 paho-mqtt==1.6.1 pillow==9.2.0 pip>=21.0,<22.3 diff --git a/pyproject.toml b/pyproject.toml index 288bcc5225a..c0d96aa5ee0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dependencies = [ "PyJWT==2.4.0", # PyJWT has loose dependency. We want the latest one. "cryptography==36.0.2", - "orjson==3.7.8", + "orjson==3.7.11", "pip>=21.0,<22.3", "python-slugify==4.0.1", "pyyaml==6.0", diff --git a/requirements.txt b/requirements.txt index ce77253b752..fce57620224 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,7 @@ jinja2==3.1.2 lru-dict==1.1.8 PyJWT==2.4.0 cryptography==36.0.2 -orjson==3.7.8 +orjson==3.7.11 pip>=21.0,<22.3 python-slugify==4.0.1 pyyaml==6.0 From bb58ad0f54db39d8d1e9b0de0181f3586cfc71d7 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Thu, 4 Aug 2022 22:08:24 +0200 Subject: [PATCH 3153/3516] Add ability to specify user(s) when sending DMs using the Twitter integration (#71310) --- homeassistant/components/twitter/notify.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/twitter/notify.py b/homeassistant/components/twitter/notify.py index fd97303a360..d64f8ec3fbf 100644 --- a/homeassistant/components/twitter/notify.py +++ b/homeassistant/components/twitter/notify.py @@ -12,6 +12,7 @@ import voluptuous as vol from homeassistant.components.notify import ( ATTR_DATA, + ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, ) @@ -63,7 +64,7 @@ class TwitterNotificationService(BaseNotificationService): username, ): """Initialize the service.""" - self.user = username + self.default_user = username self.hass = hass self.api = TwitterAPI( consumer_key, consumer_secret, access_token_key, access_token_secret @@ -72,6 +73,7 @@ class TwitterNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Tweet a message, optionally with media.""" data = kwargs.get(ATTR_DATA) + targets = kwargs.get(ATTR_TARGET) media = None if data: @@ -79,15 +81,19 @@ class TwitterNotificationService(BaseNotificationService): if not self.hass.config.is_allowed_path(media): _LOGGER.warning("'%s' is not a whitelisted directory", media) return + + if targets: + for target in targets: + callback = partial(self.send_message_callback, message, target) + self.upload_media_then_callback(callback, media) + else: + callback = partial(self.send_message_callback, message, self.default_user) + self.upload_media_then_callback(callback, media) - callback = partial(self.send_message_callback, message) - - self.upload_media_then_callback(callback, media) - - def send_message_callback(self, message, media_id=None): + def send_message_callback(self, message, user, media_id=None): """Tweet a message, optionally with media.""" - if self.user: - user_resp = self.api.request("users/lookup", {"screen_name": self.user}) + if user: + user_resp = self.api.request("users/lookup", {"screen_name": user}) user_id = user_resp.json()[0]["id"] if user_resp.status_code != HTTPStatus.OK: self.log_error_resp(user_resp) From a987cad973e41f7d9a64fb9ebd93d513ec16ed2e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Aug 2022 23:10:27 +0200 Subject: [PATCH 3154/3516] Use attributes in unifiled light (#76019) --- homeassistant/components/unifiled/light.py | 52 ++++++---------------- 1 file changed, 14 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/unifiled/light.py b/homeassistant/components/unifiled/light.py index 8ba9fc2b6f9..f1d3ad15a02 100644 --- a/homeassistant/components/unifiled/light.py +++ b/homeassistant/components/unifiled/light.py @@ -63,58 +63,34 @@ class UnifiLedLight(LightEntity): _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} - def __init__(self, light, api): + def __init__(self, light: dict[str, Any], api: unifiled) -> None: """Init Unifi LED Light.""" self._api = api self._light = light - self._name = light["name"] - self._unique_id = light["id"] - self._state = light["status"]["output"] - self._available = light["isOnline"] - self._brightness = self._api.convertfrom100to255(light["status"]["led"]) - - @property - def name(self): - """Return the display name of this light.""" - return self._name - - @property - def available(self): - """Return the available state of this light.""" - return self._available - - @property - def brightness(self): - """Return the brightness name of this light.""" - return self._brightness - - @property - def unique_id(self): - """Return the unique id of this light.""" - return self._unique_id - - @property - def is_on(self): - """Return true if light is on.""" - return self._state + self._attr_name = light["name"] + self._light_id = light["id"] + self._attr_unique_id = light["id"] + self._attr_is_on = light["status"]["output"] + self._attr_available = light["isOnline"] + self._attr_brightness = self._api.convertfrom100to255(light["status"]["led"]) def turn_on(self, **kwargs: Any) -> None: """Instruct the light to turn on.""" self._api.setdevicebrightness( - self._unique_id, + self._light_id, str(self._api.convertfrom255to100(kwargs.get(ATTR_BRIGHTNESS, 255))), ) - self._api.setdeviceoutput(self._unique_id, 1) + self._api.setdeviceoutput(self._light_id, 1) def turn_off(self, **kwargs: Any) -> None: """Instruct the light to turn off.""" - self._api.setdeviceoutput(self._unique_id, 0) + self._api.setdeviceoutput(self._light_id, 0) def update(self) -> None: """Update the light states.""" - self._state = self._api.getlightstate(self._unique_id) - self._brightness = self._api.convertfrom100to255( - self._api.getlightbrightness(self._unique_id) + self._attr_is_on = self._api.getlightstate(self._light_id) + self._attr_brightness = self._api.convertfrom100to255( + self._api.getlightbrightness(self._light_id) ) - self._available = self._api.getlightavailable(self._unique_id) + self._attr_available = self._api.getlightavailable(self._light_id) From fa9d0b9ff746e86fd334aa2ef2ab7bbc03673210 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 4 Aug 2022 23:11:37 +0200 Subject: [PATCH 3155/3516] Use attributes in tikteck light (#76022) --- homeassistant/components/tikteck/light.py | 67 ++++++----------------- 1 file changed, 18 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/tikteck/light.py b/homeassistant/components/tikteck/light.py index 28daf4baac1..7022138d147 100644 --- a/homeassistant/components/tikteck/light.py +++ b/homeassistant/components/tikteck/light.py @@ -55,82 +55,51 @@ def setup_platform( class TikteckLight(LightEntity): """Representation of a Tikteck light.""" + _attr_assumed_state = True _attr_color_mode = ColorMode.HS + _attr_should_poll = False _attr_supported_color_modes = {ColorMode.HS} + hs_color: tuple[float, float] + brightness: int def __init__(self, device): """Initialize the light.""" - self._name = device["name"] - self._address = device["address"] - self._password = device["password"] - self._brightness = 255 - self._hs = [0, 0] - self._state = False + address = device["address"] + self._attr_unique_id = address + self._attr_name = device["name"] + self._attr_brightness = 255 + self._attr_hs_color = [0, 0] + self._attr_is_on = False self.is_valid = True - self._bulb = tikteck.tikteck(self._address, "Smart Light", self._password) + self._bulb = tikteck.tikteck(address, "Smart Light", device["password"]) if self._bulb.connect() is False: self.is_valid = False - _LOGGER.error("Failed to connect to bulb %s, %s", self._address, self._name) + _LOGGER.error("Failed to connect to bulb %s, %s", address, self.name) - @property - def unique_id(self): - """Return the ID of this light.""" - return self._address - - @property - def name(self): - """Return the name of the device if any.""" - return self._name - - @property - def is_on(self): - """Return true if device is on.""" - return self._state - - @property - def brightness(self): - """Return the brightness of this light between 0..255.""" - return self._brightness - - @property - def hs_color(self): - """Return the color property.""" - return self._hs - - @property - def should_poll(self): - """Return the polling state.""" - return False - - @property - def assumed_state(self): - """Return the assumed state.""" - return True - - def set_state(self, red, green, blue, brightness): + def set_state(self, red: int, green: int, blue: int, brightness: int) -> bool: """Set the bulb state.""" return self._bulb.set_state(red, green, blue, brightness) def turn_on(self, **kwargs: Any) -> None: """Turn the specified light on.""" - self._state = True + self._attr_is_on = True hs_color = kwargs.get(ATTR_HS_COLOR) brightness = kwargs.get(ATTR_BRIGHTNESS) if hs_color is not None: - self._hs = hs_color + self._attr_hs_color = hs_color if brightness is not None: - self._brightness = brightness + self._attr_brightness = brightness - rgb = color_util.color_hs_to_RGB(*self._hs) + rgb = color_util.color_hs_to_RGB(self.hs_color[0], self.hs_color[1]) self.set_state(rgb[0], rgb[1], rgb[2], self.brightness) self.schedule_update_ha_state() def turn_off(self, **kwargs: Any) -> None: """Turn the specified light off.""" - self._state = False + self._attr_is_on = False self.set_state(0, 0, 0, 0) self.schedule_update_ha_state() From ca4b7cca1aa08ba4c145570f2e60fe38d31c5348 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Aug 2022 11:45:35 -1000 Subject: [PATCH 3156/3516] Run black on twitter to fix CI (#76254) --- homeassistant/components/twitter/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/twitter/notify.py b/homeassistant/components/twitter/notify.py index d64f8ec3fbf..d89755969a1 100644 --- a/homeassistant/components/twitter/notify.py +++ b/homeassistant/components/twitter/notify.py @@ -81,7 +81,7 @@ class TwitterNotificationService(BaseNotificationService): if not self.hass.config.is_allowed_path(media): _LOGGER.warning("'%s' is not a whitelisted directory", media) return - + if targets: for target in targets: callback = partial(self.send_message_callback, message, target) From ce871835b23413524b87a34dbf3917836b88f622 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 5 Aug 2022 01:04:10 +0200 Subject: [PATCH 3157/3516] Update pyupgrade to v2.37.3 (#76257) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8e259c1d063..4867f712b8e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.37.2 + rev: v2.37.3 hooks: - id: pyupgrade args: [--py39-plus] diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index c7f5d559c38..a6cb45b22b2 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -12,5 +12,5 @@ mccabe==0.6.1 pycodestyle==2.8.0 pydocstyle==6.1.1 pyflakes==2.4.0 -pyupgrade==2.37.2 +pyupgrade==2.37.3 yamllint==1.27.1 From cb46441b7493260a7e0929854dc361d5400c79d2 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 5 Aug 2022 00:28:51 +0000 Subject: [PATCH 3158/3516] [ci skip] Translation update --- .../advantage_air/translations/sv.json | 20 +++++++ .../components/airly/translations/sv.json | 5 ++ .../components/airtouch4/translations/sv.json | 19 ++++++ .../airvisual/translations/sensor.sv.json | 15 ++++- .../components/airvisual/translations/sv.json | 25 ++++++-- .../amberelectric/translations/sv.json | 12 ++++ .../components/anthemav/translations/he.json | 18 ++++++ .../components/apple_tv/translations/sv.json | 59 +++++++++++++++++++ .../components/arcam_fmj/translations/sv.json | 3 + .../components/atag/translations/sv.json | 3 + .../components/august/translations/sv.json | 9 ++- .../aurora_abb_powerone/translations/sv.json | 9 ++- .../components/awair/translations/he.json | 6 ++ .../binary_sensor/translations/sv.json | 28 ++++++++- .../components/bluetooth/translations/he.json | 32 ++++++++++ .../components/broadlink/translations/sv.json | 21 ++++++- .../components/bsblan/translations/sv.json | 5 +- .../components/cloud/translations/sv.json | 16 +++++ .../components/co2signal/translations/sv.json | 5 ++ .../components/coinbase/translations/sv.json | 2 + .../components/daikin/translations/sv.json | 4 +- .../components/deconz/translations/sv.json | 4 +- .../demo/translations/select.sv.json | 9 +++ .../components/denonavr/translations/sv.json | 10 +++- .../devolo_home_control/translations/sv.json | 2 + .../components/discord/translations/sv.json | 12 ++++ .../components/dsmr/translations/sv.json | 37 +++++++++++- .../components/econet/translations/sv.json | 22 +++++++ .../eight_sleep/translations/he.json | 15 +++++ .../components/esphome/translations/sv.json | 14 +++++ .../evil_genius_labs/translations/sv.json | 15 +++++ .../components/ezviz/translations/sv.json | 9 +++ .../fjaraskupan/translations/sv.json | 13 ++++ .../flunearyou/translations/en.json | 2 +- .../flunearyou/translations/fr.json | 5 ++ .../flunearyou/translations/id.json | 13 ++++ .../flunearyou/translations/pt-BR.json | 13 ++++ .../components/flux_led/translations/sv.json | 36 +++++++++++ .../components/freebox/translations/sl.json | 2 +- .../components/freebox/translations/sv.json | 1 + .../freedompro/translations/sv.json | 1 + .../components/fritz/translations/sv.json | 24 ++++++-- .../components/fritzbox/translations/sv.json | 7 ++- .../components/fronius/translations/sv.json | 18 ++++++ .../garages_amsterdam/translations/sv.json | 18 ++++++ .../components/generic/translations/he.json | 10 ++++ .../components/goalzero/translations/sv.json | 26 ++++++++ .../components/google/translations/he.json | 4 +- .../components/govee_ble/translations/he.json | 21 +++++++ .../homeassistant/translations/sv.json | 12 +++- .../homeassistant_alerts/translations/he.json | 8 +++ .../components/homekit/translations/sv.json | 4 ++ .../homekit_controller/translations/he.json | 2 +- .../translations/sensor.ca.json | 19 ++++++ .../translations/sensor.fr.json | 7 +++ .../translations/sensor.id.json | 11 ++++ .../translations/sensor.no.json | 12 ++++ .../translations/sensor.pt-BR.json | 21 +++++++ .../translations/sensor.ru.json | 7 +++ .../translations/sensor.zh-Hant.json | 12 ++++ .../homekit_controller/translations/sv.json | 1 + .../components/honeywell/translations/sv.json | 7 ++- .../components/hue/translations/sv.json | 14 ++++- .../translations/sv.json | 1 + .../hvv_departures/translations/sv.json | 23 +++++++- .../components/hyperion/translations/sv.json | 1 + .../components/iaqualink/translations/sl.json | 2 +- .../components/icloud/translations/sv.json | 10 +++- .../components/inkbird/translations/he.json | 21 +++++++ .../components/insteon/translations/sv.json | 24 +++++++- .../islamic_prayer_times/translations/sv.json | 18 +++++- .../components/isy994/translations/sv.json | 11 +++- .../components/izone/translations/sl.json | 2 +- .../components/jellyfin/translations/sv.json | 10 ++++ .../keenetic_ndms2/translations/sv.json | 2 + .../components/kmtronic/translations/sv.json | 9 +++ .../components/knx/translations/sv.json | 29 ++++++++- .../components/kodi/translations/sv.json | 30 +++++++++- .../components/konnected/translations/he.json | 2 +- .../components/konnected/translations/sv.json | 3 +- .../components/kraken/translations/sv.json | 22 +++++++ .../lacrosse_view/translations/he.json | 19 ++++++ .../lg_soundbar/translations/he.json | 17 ++++++ .../components/life360/translations/he.json | 9 +++ .../components/life360/translations/sv.json | 1 + .../components/lifx/translations/he.json | 13 ++++ .../components/litejet/translations/sv.json | 7 +++ .../litterrobot/translations/sv.json | 9 +++ .../components/local_ip/translations/sl.json | 2 +- .../logi_circle/translations/sv.json | 8 ++- .../components/lookin/translations/sv.json | 31 ++++++++++ .../components/lovelace/translations/sv.json | 2 + .../lutron_caseta/translations/sv.json | 23 ++++++++ .../components/mazda/translations/sv.json | 10 ++++ .../media_player/translations/sv.json | 3 + .../components/met/translations/sl.json | 2 +- .../components/metoffice/translations/sv.json | 15 ++++- .../components/mikrotik/translations/sv.json | 1 + .../components/mill/translations/sv.json | 3 + .../components/moat/translations/he.json | 21 +++++++ .../mobile_app/translations/sv.json | 5 ++ .../modem_callerid/translations/sv.json | 1 + .../components/monoprice/translations/sl.json | 2 +- .../motion_blinds/translations/he.json | 2 +- .../motion_blinds/translations/sv.json | 9 +++ .../components/motioneye/translations/sv.json | 16 ++++- .../components/nam/translations/sv.json | 2 + .../components/nest/translations/he.json | 1 + .../components/nest/translations/sv.json | 8 ++- .../components/netatmo/translations/sv.json | 5 ++ .../components/nexia/translations/sv.json | 1 + .../components/nextdns/translations/he.json | 21 +++++++ .../nfandroidtv/translations/sv.json | 3 +- .../nightscout/translations/sv.json | 6 +- .../components/nina/translations/he.json | 6 ++ .../components/nuheat/translations/sl.json | 2 +- .../components/octoprint/translations/sv.json | 6 ++ .../components/omnilogic/translations/sv.json | 7 ++- .../opentherm_gw/translations/he.json | 3 +- .../openweathermap/translations/sv.json | 3 +- .../components/owntracks/translations/he.json | 9 +++ .../p1_monitor/translations/sv.json | 16 +++++ .../panasonic_viera/translations/sv.json | 8 ++- .../philips_js/translations/sv.json | 1 + .../components/pi_hole/translations/sv.json | 1 + .../components/picnic/translations/sv.json | 5 +- .../components/plaato/translations/sl.json | 2 +- .../components/plaato/translations/sv.json | 31 ++++++++++ .../components/plex/translations/sv.json | 7 ++- .../components/plugwise/translations/he.json | 4 ++ .../components/powerwall/translations/sv.json | 1 + .../progettihwsw/translations/sv.json | 23 +++++++- .../components/ps4/translations/sv.json | 2 + .../pvpc_hourly_pricing/translations/sv.json | 13 ++++ .../components/qnap_qsw/translations/he.json | 6 ++ .../components/rachio/translations/sl.json | 2 +- .../radiotherm/translations/he.json | 13 +++- .../rainforest_eagle/translations/sv.json | 12 +++- .../components/rdw/translations/sv.json | 15 +++++ .../components/rfxtrx/translations/sv.json | 6 +- .../components/rhasspy/translations/he.json | 7 +++ .../components/risco/translations/sv.json | 14 +++++ .../components/roku/translations/sl.json | 2 +- .../components/roomba/translations/sv.json | 2 + .../ruckus_unleashed/translations/sv.json | 9 +++ .../components/samsungtv/translations/sv.json | 7 +++ .../components/scrape/translations/he.json | 23 ++++++++ .../screenlogic/translations/sv.json | 17 +++++- .../components/select/translations/sv.json | 9 ++- .../components/sensor/translations/he.json | 27 ++++++++- .../components/sensor/translations/sv.json | 6 ++ .../sensorpush/translations/he.json | 21 +++++++ .../components/shelly/translations/sv.json | 18 ++++++ .../simplepush/translations/en.json | 4 ++ .../simplepush/translations/fr.json | 3 + .../simplepush/translations/he.json | 17 ++++++ .../simplepush/translations/id.json | 4 ++ .../simplepush/translations/pt-BR.json | 4 ++ .../simplisafe/translations/en.json | 21 ++++++- .../simplisafe/translations/he.json | 4 +- .../simplisafe/translations/id.json | 1 + .../simplisafe/translations/pt-BR.json | 3 +- .../simplisafe/translations/sv.json | 3 +- .../components/skybell/translations/he.json | 11 +++- .../smartthings/translations/sv.json | 1 + .../components/smarttub/translations/sv.json | 4 ++ .../components/solarlog/translations/sl.json | 2 +- .../somfy_mylink/translations/sv.json | 12 ++++ .../components/sonarr/translations/sv.json | 19 +++++- .../soundtouch/translations/he.json | 17 ++++++ .../components/sql/translations/he.json | 12 ++++ .../components/subaru/translations/sv.json | 20 ++++++- .../components/switchbot/translations/he.json | 2 +- .../components/switchbot/translations/sv.json | 24 +++++++- .../switcher_kis/translations/sv.json | 13 ++++ .../components/syncthing/translations/sv.json | 4 ++ .../synology_dsm/translations/sl.json | 2 +- .../synology_dsm/translations/sv.json | 3 +- .../tankerkoenig/translations/he.json | 8 ++- .../tellduslive/translations/sv.json | 1 + .../totalconnect/translations/sv.json | 15 ++++- .../transmission/translations/he.json | 9 ++- .../components/tuya/translations/he.json | 2 + .../tuya/translations/select.sv.json | 13 +++- .../tuya/translations/sensor.sv.json | 15 +++++ .../components/tuya/translations/sv.json | 5 ++ .../twentemilieu/translations/sv.json | 4 ++ .../components/unifi/translations/sv.json | 4 +- .../components/upnp/translations/sv.json | 14 +++++ .../components/velbus/translations/sv.json | 7 +++ .../components/venstar/translations/sv.json | 14 ++++- .../components/verisure/translations/sv.json | 28 +++++++++ .../vlc_telnet/translations/sv.json | 8 +++ .../components/wallbox/translations/sv.json | 5 +- .../water_heater/translations/sv.json | 4 +- .../components/watttime/translations/sv.json | 24 ++++++++ .../components/wemo/translations/sv.json | 5 ++ .../components/withings/translations/he.json | 3 + .../components/withings/translations/sl.json | 2 +- .../wled/translations/select.sv.json | 4 +- .../wolflink/translations/sensor.sv.json | 29 +++++++++ .../components/xbox/translations/sv.json | 1 + .../xiaomi_ble/translations/he.json | 22 +++++++ .../xiaomi_ble/translations/id.json | 8 +++ .../xiaomi_miio/translations/sv.json | 18 +++++- .../components/yeelight/translations/sv.json | 3 + .../components/zwave_js/translations/sv.json | 25 +++++++- 207 files changed, 2087 insertions(+), 103 deletions(-) create mode 100644 homeassistant/components/advantage_air/translations/sv.json create mode 100644 homeassistant/components/airtouch4/translations/sv.json create mode 100644 homeassistant/components/amberelectric/translations/sv.json create mode 100644 homeassistant/components/anthemav/translations/he.json create mode 100644 homeassistant/components/apple_tv/translations/sv.json create mode 100644 homeassistant/components/bluetooth/translations/he.json create mode 100644 homeassistant/components/cloud/translations/sv.json create mode 100644 homeassistant/components/demo/translations/select.sv.json create mode 100644 homeassistant/components/econet/translations/sv.json create mode 100644 homeassistant/components/eight_sleep/translations/he.json create mode 100644 homeassistant/components/evil_genius_labs/translations/sv.json create mode 100644 homeassistant/components/fjaraskupan/translations/sv.json create mode 100644 homeassistant/components/flux_led/translations/sv.json create mode 100644 homeassistant/components/fronius/translations/sv.json create mode 100644 homeassistant/components/garages_amsterdam/translations/sv.json create mode 100644 homeassistant/components/goalzero/translations/sv.json create mode 100644 homeassistant/components/govee_ble/translations/he.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/he.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.ca.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.fr.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.id.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.no.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.pt-BR.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.ru.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.zh-Hant.json create mode 100644 homeassistant/components/inkbird/translations/he.json create mode 100644 homeassistant/components/kraken/translations/sv.json create mode 100644 homeassistant/components/lacrosse_view/translations/he.json create mode 100644 homeassistant/components/lg_soundbar/translations/he.json create mode 100644 homeassistant/components/litejet/translations/sv.json create mode 100644 homeassistant/components/lookin/translations/sv.json create mode 100644 homeassistant/components/moat/translations/he.json create mode 100644 homeassistant/components/nextdns/translations/he.json create mode 100644 homeassistant/components/p1_monitor/translations/sv.json create mode 100644 homeassistant/components/rdw/translations/sv.json create mode 100644 homeassistant/components/rhasspy/translations/he.json create mode 100644 homeassistant/components/sensorpush/translations/he.json create mode 100644 homeassistant/components/simplepush/translations/he.json create mode 100644 homeassistant/components/soundtouch/translations/he.json create mode 100644 homeassistant/components/switcher_kis/translations/sv.json create mode 100644 homeassistant/components/tuya/translations/sensor.sv.json create mode 100644 homeassistant/components/xiaomi_ble/translations/he.json diff --git a/homeassistant/components/advantage_air/translations/sv.json b/homeassistant/components/advantage_air/translations/sv.json new file mode 100644 index 00000000000..99ecd87448c --- /dev/null +++ b/homeassistant/components/advantage_air/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-adress", + "port": "Port" + }, + "description": "Anslut till API:et f\u00f6r din Advantage Air v\u00e4ggmonterade surfplatta.", + "title": "Anslut" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/translations/sv.json b/homeassistant/components/airly/translations/sv.json index ac976224924..05d56e9d88a 100644 --- a/homeassistant/components/airly/translations/sv.json +++ b/homeassistant/components/airly/translations/sv.json @@ -18,5 +18,10 @@ "description": "Konfigurera integration av luftkvalitet. F\u00f6r att skapa API-nyckel, g\u00e5 till https://developer.airly.eu/register" } } + }, + "system_health": { + "info": { + "requests_remaining": "\u00c5terst\u00e5ende till\u00e5tna f\u00f6rfr\u00e5gningar" + } } } \ No newline at end of file diff --git a/homeassistant/components/airtouch4/translations/sv.json b/homeassistant/components/airtouch4/translations/sv.json new file mode 100644 index 00000000000..34b6a755ace --- /dev/null +++ b/homeassistant/components/airtouch4/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "no_units": "Det gick inte att hitta n\u00e5gra AirTouch 4-grupper." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + }, + "title": "St\u00e4ll in dina AirTouch 4-anslutningsdetaljer." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sensor.sv.json b/homeassistant/components/airvisual/translations/sensor.sv.json index f1fa0bbdcd8..85a1def80fa 100644 --- a/homeassistant/components/airvisual/translations/sensor.sv.json +++ b/homeassistant/components/airvisual/translations/sensor.sv.json @@ -1,7 +1,20 @@ { "state": { "airvisual__pollutant_label": { - "co": "Kolmonoxid" + "co": "Kolmonoxid", + "n2": "Kv\u00e4vedioxid", + "o3": "Ozon", + "p1": "PM10", + "p2": "PM2,5", + "s2": "Svaveldioxid" + }, + "airvisual__pollutant_level": { + "good": "Bra", + "hazardous": "Farlig", + "moderate": "M\u00e5ttlig", + "unhealthy": "Oh\u00e4lsosam", + "unhealthy_sensitive": "Oh\u00e4lsosamt f\u00f6r k\u00e4nsliga grupper", + "very_unhealthy": "Mycket oh\u00e4lsosamt" } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/sv.json b/homeassistant/components/airvisual/translations/sv.json index d3559f89aa0..9e32b698eaf 100644 --- a/homeassistant/components/airvisual/translations/sv.json +++ b/homeassistant/components/airvisual/translations/sv.json @@ -7,19 +7,36 @@ "error": { "cannot_connect": "Det gick inte att ansluta.", "general_error": "Ett ok\u00e4nt fel intr\u00e4ffade.", - "invalid_api_key": "Ogiltig API-nyckel" + "invalid_api_key": "Ogiltig API-nyckel", + "location_not_found": "Platsen hittades inte" }, "step": { + "geography_by_coords": { + "data": { + "api_key": "API-nyckel", + "latitude": "Latitud", + "longitude": "Longitud" + }, + "description": "Anv\u00e4nd AirVisuals moln-API f\u00f6r att \u00f6vervaka en latitud/longitud.", + "title": "Konfigurera en geografi" + }, "geography_by_name": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "city": "Stad", + "country": "Land", + "state": "stat" + }, + "description": "Anv\u00e4nd AirVisuals moln-API f\u00f6r att \u00f6vervaka en stad/stat/land.", + "title": "Konfigurera en geografi" }, "node_pro": { "data": { "ip_address": "Enhets IP-adress / v\u00e4rdnamn", "password": "Enhetsl\u00f6senord" - } + }, + "description": "\u00d6vervaka en personlig AirVisual-enhet. L\u00f6senordet kan h\u00e4mtas fr\u00e5n enhetens anv\u00e4ndargr\u00e4nssnitt.", + "title": "Konfigurera en AirVisual Node/Pro" }, "reauth_confirm": { "data": { diff --git a/homeassistant/components/amberelectric/translations/sv.json b/homeassistant/components/amberelectric/translations/sv.json new file mode 100644 index 00000000000..7458627ef0a --- /dev/null +++ b/homeassistant/components/amberelectric/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "site_id": "Plats-ID" + }, + "description": "G\u00e5 till {api_url} f\u00f6r att skapa en API-nyckel" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/he.json b/homeassistant/components/anthemav/translations/he.json new file mode 100644 index 00000000000..c3a67844fdd --- /dev/null +++ b/homeassistant/components/anthemav/translations/he.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "port": "\u05e4\u05ea\u05d7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/sv.json b/homeassistant/components/apple_tv/translations/sv.json new file mode 100644 index 00000000000..28b6e2ed67b --- /dev/null +++ b/homeassistant/components/apple_tv/translations/sv.json @@ -0,0 +1,59 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "backoff": "Enheten accepterar inte parningsf\u00f6rfr\u00e5gningar f\u00f6r n\u00e4rvarande (du kan ha angett en ogiltig PIN-kod f\u00f6r m\u00e5nga g\u00e5nger), f\u00f6rs\u00f6k igen senare.", + "device_did_not_pair": "Inget f\u00f6rs\u00f6k att avsluta parningsprocessen gjordes fr\u00e5n enheten.", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "invalid_auth": "Ogiltig autentisering", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name} ({type})", + "step": { + "confirm": { + "description": "Du h\u00e5ller p\u00e5 att l\u00e4gga till ` {name} ` av typen ` {type} ` till Home Assistant. \n\n **F\u00f6r att slutf\u00f6ra processen kan du beh\u00f6va ange flera PIN-koder.** \n\n Observera att du *inte* kommer att kunna st\u00e4nga av din Apple TV med denna integration. Endast mediaspelaren i Home Assistant kommer att st\u00e4ngas av!", + "title": "Bekr\u00e4fta att du l\u00e4gger till Apple TV" + }, + "pair_no_pin": { + "description": "Parkoppling kr\u00e4vs f\u00f6r tj\u00e4nsten {protocol}. Ange PIN-kod {pin} p\u00e5 din enhet f\u00f6r att forts\u00e4tta.", + "title": "Parkoppling" + }, + "pair_with_pin": { + "data": { + "pin": "Pin-kod" + }, + "description": "Parning kr\u00e4vs f\u00f6r protokollet ` {protocol} `. V\u00e4nligen ange PIN-koden som visas p\u00e5 sk\u00e4rmen. Inledande nollor ska utel\u00e4mnas, dvs ange 123 om den visade koden \u00e4r 0123.", + "title": "Parkoppling" + }, + "reconfigure": { + "description": "Konfigurera om enheten f\u00f6r att \u00e5terst\u00e4lla dess funktionalitet.", + "title": "Omkonfigurering av enheten" + }, + "service_problem": { + "description": "Ett problem uppstod vid koppling av protokoll ` {protocol} `. Det kommer att ignoreras.", + "title": "Det gick inte att l\u00e4gga till tj\u00e4nsten" + }, + "user": { + "data": { + "device_input": "Enhet" + }, + "description": "B\u00f6rja med att ange enhetsnamnet (t.ex. k\u00f6k eller sovrum) eller IP-adressen f\u00f6r den Apple TV du vill l\u00e4gga till. \n\n Om du inte kan se din enhet eller har n\u00e5gra problem, f\u00f6rs\u00f6k att ange enhetens IP-adress.", + "title": "Konfigurera en ny Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Sl\u00e5 inte p\u00e5 enheten n\u00e4r du startar Home Assistant" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/sv.json b/homeassistant/components/arcam_fmj/translations/sv.json index 42d58bfc929..f29aa5579cc 100644 --- a/homeassistant/components/arcam_fmj/translations/sv.json +++ b/homeassistant/components/arcam_fmj/translations/sv.json @@ -7,6 +7,9 @@ }, "flow_title": "{host}", "step": { + "confirm": { + "description": "Vill du l\u00e4gga till Arcam FMJ p\u00e5 ` {host} ` till Home Assistant?" + }, "user": { "data": { "host": "V\u00e4rd", diff --git a/homeassistant/components/atag/translations/sv.json b/homeassistant/components/atag/translations/sv.json index 480da89cb4a..ae7adf0a365 100644 --- a/homeassistant/components/atag/translations/sv.json +++ b/homeassistant/components/atag/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { "cannot_connect": "Det gick inte att ansluta.", "unauthorized": "Parning nekad, kontrollera enheten f\u00f6r autentiseringsbeg\u00e4ran" diff --git a/homeassistant/components/august/translations/sv.json b/homeassistant/components/august/translations/sv.json index b4d4e8835fa..f3b6cf8d552 100644 --- a/homeassistant/components/august/translations/sv.json +++ b/homeassistant/components/august/translations/sv.json @@ -12,13 +12,18 @@ "reauth_validate": { "data": { "password": "L\u00f6senord" - } + }, + "description": "Ange l\u00f6senordet f\u00f6r {username} .", + "title": "Autentisera ett augustikonto igen" }, "user_validate": { "data": { + "login_method": "Inloggningsmetod", "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Om inloggningsmetoden \u00e4r \"e-post\" \u00e4r anv\u00e4ndarnamnet e-postadressen. Om inloggningsmetoden \u00e4r \"telefon\" \u00e4r anv\u00e4ndarnamnet telefonnumret i formatet \"+ NNNNNNNN\".", + "title": "St\u00e4ll in ett August-konto" }, "validation": { "data": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/sv.json b/homeassistant/components/aurora_abb_powerone/translations/sv.json index 361fc8bbbb7..469047bc3ba 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/sv.json +++ b/homeassistant/components/aurora_abb_powerone/translations/sv.json @@ -1,16 +1,21 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "no_serial_ports": "Inga com portar funna. M\u00e5ste ha en RS485 enhet f\u00f6r att kommunicera" }, "error": { - "cannot_open_serial_port": "Kan inte \u00f6ppna serieporten, kontrollera och f\u00f6rs\u00f6k igen." + "cannot_connect": "Det g\u00e5r inte att ansluta, kontrollera seriell port, adress, elektrisk anslutning och att v\u00e4xelriktaren \u00e4r p\u00e5 (i dagsljus)", + "cannot_open_serial_port": "Kan inte \u00f6ppna serieporten, kontrollera och f\u00f6rs\u00f6k igen.", + "invalid_serial_port": "Serieporten \u00e4r inte en giltig enhet eller kunde inte \u00f6ppnas" }, "step": { "user": { "data": { + "address": "V\u00e4xelriktarens adress", "port": "RS485 eller USB-RS485 adapter port" - } + }, + "description": "V\u00e4xelriktaren m\u00e5ste anslutas via en RS485-adapter, v\u00e4lj seriell port och v\u00e4xelriktarens adress som konfigurerats p\u00e5 LCD-panelen" } } } diff --git a/homeassistant/components/awair/translations/he.json b/homeassistant/components/awair/translations/he.json index 55e8b21a52b..2494d0bbd28 100644 --- a/homeassistant/components/awair/translations/he.json +++ b/homeassistant/components/awair/translations/he.json @@ -16,6 +16,12 @@ "email": "\u05d3\u05d5\u05d0\"\u05dc" } }, + "reauth_confirm": { + "data": { + "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4", + "email": "\u05d3\u05d5\u05d0\"\u05dc" + } + }, "user": { "data": { "access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4", diff --git a/homeassistant/components/binary_sensor/translations/sv.json b/homeassistant/components/binary_sensor/translations/sv.json index 8b05d4b024e..eeaac07d691 100644 --- a/homeassistant/components/binary_sensor/translations/sv.json +++ b/homeassistant/components/binary_sensor/translations/sv.json @@ -17,6 +17,7 @@ "is_no_problem": "{entity_name} uppt\u00e4cker inte problem", "is_no_smoke": "{entity_name} detekterar inte r\u00f6k", "is_no_sound": "{entity_name} uppt\u00e4cker inte ljud", + "is_no_update": "{entity_name} \u00e4r uppdaterad", "is_no_vibration": "{entity_name} uppt\u00e4cker inte vibrationer", "is_not_bat_low": "{entity_name} batteri \u00e4r normalt", "is_not_cold": "{entity_name} \u00e4r inte kall", @@ -43,6 +44,7 @@ "is_smoke": "{entity_name} detekterar r\u00f6k", "is_sound": "{entity_name} uppt\u00e4cker ljud", "is_unsafe": "{entity_name} \u00e4r os\u00e4ker", + "is_update": "{entity_name} har en uppdatering tillg\u00e4nglig", "is_vibration": "{entity_name} uppt\u00e4cker vibrationer" }, "trigger_type": { @@ -62,6 +64,7 @@ "no_problem": "{entity_name} slutade uppt\u00e4cka problem", "no_smoke": "{entity_name} slutade detektera r\u00f6k", "no_sound": "{entity_name} slutade uppt\u00e4cka ljud", + "no_update": "{entity_name} blev uppdaterad", "no_vibration": "{entity_name} slutade uppt\u00e4cka vibrationer", "not_bat_low": "{entity_name} batteri normalt", "not_cold": "{entity_name} blev inte kall", @@ -87,10 +90,12 @@ "turned_off": "{entity_name} st\u00e4ngdes av", "turned_on": "{entity_name} slogs p\u00e5", "unsafe": "{entity_name} blev os\u00e4ker", + "update": "{entity_name} har en uppdatering tillg\u00e4nglig", "vibration": "{entity_name} b\u00f6rjade detektera vibrationer" } }, "device_class": { + "cold": "Kyla", "heat": "v\u00e4rme", "motion": "r\u00f6relse", "power": "effekt" @@ -105,7 +110,8 @@ "on": "L\u00e5g" }, "battery_charging": { - "off": "Laddar inte" + "off": "Laddar inte", + "on": "Laddar" }, "cold": { "off": "Normal", @@ -131,6 +137,10 @@ "off": "Normal", "on": "Varmt" }, + "light": { + "off": "Inget ljus", + "on": "Ljus uppt\u00e4ckt" + }, "lock": { "off": "L\u00e5st", "on": "Ol\u00e5st" @@ -143,6 +153,10 @@ "off": "Klart", "on": "Detekterad" }, + "moving": { + "off": "R\u00f6r sig inte", + "on": "R\u00f6r p\u00e5 sig" + }, "occupancy": { "off": "Tomt", "on": "Detekterad" @@ -151,6 +165,10 @@ "off": "St\u00e4ngd", "on": "\u00d6ppen" }, + "plug": { + "off": "Urkopplad", + "on": "Inkopplad" + }, "presence": { "off": "Borta", "on": "Hemma" @@ -159,6 +177,10 @@ "off": "Ok", "on": "Problem" }, + "running": { + "off": "K\u00f6r inte", + "on": "K\u00f6rs" + }, "safety": { "off": "S\u00e4ker", "on": "Os\u00e4ker" @@ -171,6 +193,10 @@ "off": "Klart", "on": "Detekterad" }, + "update": { + "off": "Uppdaterad", + "on": "Uppdatering tillg\u00e4nglig" + }, "vibration": { "off": "Rensa", "on": "Detekterad" diff --git a/homeassistant/components/bluetooth/translations/he.json b/homeassistant/components/bluetooth/translations/he.json new file mode 100644 index 00000000000..b5740956a9d --- /dev/null +++ b/homeassistant/components/bluetooth/translations/he.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", + "no_adapters": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05ea\u05d0\u05de\u05d9 \u05e9\u05df \u05db\u05d7\u05d5\u05dc\u05d4" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "enable_bluetooth": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05e9\u05df \u05db\u05d7\u05d5\u05dc\u05d4?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "\u05de\u05ea\u05d0\u05dd \u05d4\u05e9\u05df \u05d4\u05db\u05d7\u05d5\u05dc\u05d4 \u05dc\u05e9\u05d9\u05de\u05d5\u05e9 \u05dc\u05e1\u05e8\u05d9\u05e7\u05d4" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/sv.json b/homeassistant/components/broadlink/translations/sv.json index bc621dfaa6a..5f48a0f975f 100644 --- a/homeassistant/components/broadlink/translations/sv.json +++ b/homeassistant/components/broadlink/translations/sv.json @@ -21,7 +21,26 @@ "finish": { "data": { "name": "Namn" - } + }, + "title": "V\u00e4lj ett namn f\u00f6r enheten" + }, + "reset": { + "description": "{name} ( {model} p\u00e5 {host} ) \u00e4r l\u00e5st. Du m\u00e5ste l\u00e5sa upp enheten f\u00f6r att autentisera och slutf\u00f6ra konfigurationen. Instruktioner:\n 1. \u00d6ppna Broadlink-appen.\n 2. Klicka p\u00e5 enheten.\n 3. Klicka p\u00e5 `...` uppe till h\u00f6ger.\n 4. Bl\u00e4ddra till botten av sidan.\n 5. Inaktivera l\u00e5set.", + "title": "L\u00e5s upp enheten" + }, + "unlock": { + "data": { + "unlock": "Ja, g\u00f6r det." + }, + "description": "{name} ( {model} p\u00e5 {host} ) \u00e4r l\u00e5st. Detta kan leda till autentiseringsproblem i Home Assistant. Vill du l\u00e5sa upp den?", + "title": "L\u00e5s upp enheten (valfritt)" + }, + "user": { + "data": { + "host": "V\u00e4rd", + "timeout": "Timeout" + }, + "title": "Anslut till enheten" } } } diff --git a/homeassistant/components/bsblan/translations/sv.json b/homeassistant/components/bsblan/translations/sv.json index f96cbe0051f..96ec59f86dc 100644 --- a/homeassistant/components/bsblan/translations/sv.json +++ b/homeassistant/components/bsblan/translations/sv.json @@ -13,9 +13,12 @@ "data": { "host": "V\u00e4rd", "passkey": "Nyckelstr\u00e4ng", + "password": "L\u00f6senord", "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "St\u00e4ll in din BSB-Lan-enhet f\u00f6r att integreras med Home Assistant.", + "title": "Anslut till BSB-Lan-enheten" } } } diff --git a/homeassistant/components/cloud/translations/sv.json b/homeassistant/components/cloud/translations/sv.json new file mode 100644 index 00000000000..528e996272f --- /dev/null +++ b/homeassistant/components/cloud/translations/sv.json @@ -0,0 +1,16 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "Alexa Aktiverad", + "can_reach_cert_server": "N\u00e5 certifikatserver", + "can_reach_cloud": "N\u00e5 Home Assistant Cloud", + "can_reach_cloud_auth": "N\u00e5 autentiseringsserver", + "google_enabled": "Google Aktiverad", + "logged_in": "Inloggad", + "relayer_connected": "Vidarebefodrare Ansluten", + "remote_connected": "Fj\u00e4rransluten", + "remote_enabled": "Fj\u00e4rr\u00e5tkomst Aktiverad", + "subscription_expiration": "Prenumerationens utg\u00e5ng" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/co2signal/translations/sv.json b/homeassistant/components/co2signal/translations/sv.json index 0abdae46923..1544555f8f3 100644 --- a/homeassistant/components/co2signal/translations/sv.json +++ b/homeassistant/components/co2signal/translations/sv.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "api_ratelimit": "API-hastighetsgr\u00e4nsen har \u00f6verskridits", + "unknown": "Ov\u00e4ntat fel" + }, "error": { "api_ratelimit": "API-hastighetsgr\u00e4nsen har \u00f6verskridits", "invalid_auth": "Ogiltig autentisering", diff --git a/homeassistant/components/coinbase/translations/sv.json b/homeassistant/components/coinbase/translations/sv.json index 7d6a4a507aa..c63ebf1c09f 100644 --- a/homeassistant/components/coinbase/translations/sv.json +++ b/homeassistant/components/coinbase/translations/sv.json @@ -11,6 +11,7 @@ "api_key": "API-nyckel", "api_token": "API-hemlighet" }, + "description": "Ange uppgifterna om din API-nyckel som tillhandah\u00e5lls av Coinbase.", "title": "Coinbase API-nyckeldetaljer" } } @@ -25,6 +26,7 @@ "init": { "data": { "account_balance_currencies": "Balans i pl\u00e5nboken att rapportera.", + "exchange_base": "Basvaluta f\u00f6r v\u00e4xelkurssensorer.", "exchange_rate_currencies": "Valutakurser att rapportera.", "exchnage_rate_precision": "Antal decimaler f\u00f6r v\u00e4xelkurser." }, diff --git a/homeassistant/components/daikin/translations/sv.json b/homeassistant/components/daikin/translations/sv.json index 1ea73051e9b..f12d1d4d71f 100644 --- a/homeassistant/components/daikin/translations/sv.json +++ b/homeassistant/components/daikin/translations/sv.json @@ -5,8 +5,10 @@ "cannot_connect": "Det gick inte att ansluta." }, "error": { + "api_password": "Ogiltig autentisering , anv\u00e4nd antingen API-nyckel eller l\u00f6senord.", "cannot_connect": "Det gick inte att ansluta.", - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { diff --git a/homeassistant/components/deconz/translations/sv.json b/homeassistant/components/deconz/translations/sv.json index 774144c5ba3..6097d4ef3b9 100644 --- a/homeassistant/components/deconz/translations/sv.json +++ b/homeassistant/components/deconz/translations/sv.json @@ -4,6 +4,7 @@ "already_configured": "Bryggan \u00e4r redan konfigurerad", "already_in_progress": "Konfigurations fl\u00f6det f\u00f6r bryggan p\u00e5g\u00e5r redan.", "no_bridges": "Inga deCONZ-bryggor uppt\u00e4cktes", + "no_hardware_available": "Ingen radioh\u00e5rdvara ansluten till deCONZ", "updated_instance": "Uppdaterad deCONZ-instans med ny v\u00e4rdadress" }, "error": { @@ -97,7 +98,8 @@ "deconz_devices": { "data": { "allow_clip_sensor": "Till\u00e5t deCONZ CLIP-sensorer", - "allow_deconz_groups": "Till\u00e5t deCONZ ljusgrupper" + "allow_deconz_groups": "Till\u00e5t deCONZ ljusgrupper", + "allow_new_devices": "Till\u00e5t automatiskt till\u00e4gg av nya enheter" }, "description": "Konfigurera synlighet f\u00f6r deCONZ-enhetstyper", "title": "deCONZ-inst\u00e4llningar" diff --git a/homeassistant/components/demo/translations/select.sv.json b/homeassistant/components/demo/translations/select.sv.json new file mode 100644 index 00000000000..68d8d995563 --- /dev/null +++ b/homeassistant/components/demo/translations/select.sv.json @@ -0,0 +1,9 @@ +{ + "state": { + "demo__speed": { + "light_speed": "Ljushastighet", + "ludicrous_speed": "Ludicrous hastighet", + "ridiculous_speed": "L\u00f6jlig hastighet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/sv.json b/homeassistant/components/denonavr/translations/sv.json index f27ff32b05c..db692b7f835 100644 --- a/homeassistant/components/denonavr/translations/sv.json +++ b/homeassistant/components/denonavr/translations/sv.json @@ -1,8 +1,16 @@ { "config": { "abort": { - "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen, att koppla bort n\u00e4t- och ethernetkablar och \u00e5teransluta dem kan hj\u00e4lpa" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen, att koppla bort n\u00e4t- och ethernetkablar och \u00e5teransluta dem kan hj\u00e4lpa", + "not_denonavr_manufacturer": "Inte en Denon AVR-n\u00e4tverksreceiver, uppt\u00e4ckt tillverkare matchade inte", + "not_denonavr_missing": "Inte en Denon AVR-n\u00e4tverksreceiver, uppt\u00e4cktsinformationen \u00e4r inte fullst\u00e4ndig" }, + "error": { + "discovery_error": "Det gick inte att hitta en Denon AVR-n\u00e4tverksreceiver" + }, + "flow_title": "{name}", "step": { "confirm": { "description": "Bekr\u00e4fta att du l\u00e4gger till receivern" diff --git a/homeassistant/components/devolo_home_control/translations/sv.json b/homeassistant/components/devolo_home_control/translations/sv.json index 6727e5c5107..3081604f882 100644 --- a/homeassistant/components/devolo_home_control/translations/sv.json +++ b/homeassistant/components/devolo_home_control/translations/sv.json @@ -13,6 +13,8 @@ }, "zeroconf_confirm": { "data": { + "mydevolo_url": "mydevolo URL", + "password": "L\u00f6senord", "username": "E-postadress / devolo-id" } } diff --git a/homeassistant/components/discord/translations/sv.json b/homeassistant/components/discord/translations/sv.json index 1b52e7816d2..38370827354 100644 --- a/homeassistant/components/discord/translations/sv.json +++ b/homeassistant/components/discord/translations/sv.json @@ -8,6 +8,18 @@ "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "reauth_confirm": { + "data": { + "api_token": "API Token" + } + }, + "user": { + "data": { + "api_token": "API Token" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/sv.json b/homeassistant/components/dsmr/translations/sv.json index 38b86e964b6..7ad8f5f0b09 100644 --- a/homeassistant/components/dsmr/translations/sv.json +++ b/homeassistant/components/dsmr/translations/sv.json @@ -1,7 +1,42 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_communicate": "Misslyckades med att kommunicera", + "cannot_connect": "Det gick inte att ansluta." + }, + "error": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_communicate": "Misslyckades med att kommunicera", + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "setup_network": { + "data": { + "dsmr_version": "V\u00e4lj DSMR-version", + "host": "V\u00e4rd", + "port": "Port" + }, + "title": "V\u00e4lj anslutningsadress" + }, + "setup_serial": { + "data": { + "dsmr_version": "V\u00e4lj DSMR-version", + "port": "V\u00e4lj enhet" + }, + "title": "Enhet" + }, + "setup_serial_manual_path": { + "data": { + "port": "USB-enhetens s\u00f6kv\u00e4g" + }, + "title": "S\u00f6kv\u00e4g" + }, + "user": { + "data": { + "type": "Anslutningstyp" + } + } } }, "options": { diff --git a/homeassistant/components/econet/translations/sv.json b/homeassistant/components/econet/translations/sv.json new file mode 100644 index 00000000000..5fbfb53944b --- /dev/null +++ b/homeassistant/components/econet/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, + "step": { + "user": { + "data": { + "email": "E-post", + "password": "L\u00f6senord" + }, + "title": "Konfigurera Rheem EcoNet-konto" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/translations/he.json b/homeassistant/components/eight_sleep/translations/he.json new file mode 100644 index 00000000000..e428d0009ae --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/he.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/sv.json b/homeassistant/components/esphome/translations/sv.json index 88a9ca92377..1d4eb2a6ff3 100644 --- a/homeassistant/components/esphome/translations/sv.json +++ b/homeassistant/components/esphome/translations/sv.json @@ -7,6 +7,8 @@ }, "error": { "connection_error": "Kan inte ansluta till ESP. Se till att din YAML-fil inneh\u00e5ller en 'api:' line.", + "invalid_auth": "Ogiltig autentisering", + "invalid_psk": "Transportkrypteringsnyckeln \u00e4r ogiltig. Se till att den matchar det du har i din konfiguration", "resolve_error": "Det g\u00e5r inte att hitta IP-adressen f\u00f6r ESP med DNS-namnet. Om det h\u00e4r felet kvarst\u00e5r anger du en statisk IP-adress: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "ESPHome: {name}", @@ -21,6 +23,18 @@ "description": "Vill du l\u00e4gga till ESPHome noden ` {name} ` till Home Assistant?", "title": "Uppt\u00e4ckt ESPHome-nod" }, + "encryption_key": { + "data": { + "noise_psk": "Krypteringsnyckel" + }, + "description": "Ange krypteringsnyckeln som du st\u00e4llt in i din konfiguration f\u00f6r {name} ." + }, + "reauth_confirm": { + "data": { + "noise_psk": "Krypteringsnyckel" + }, + "description": "ESPHome-enheten {name} aktiverade transportkryptering eller \u00e4ndrade krypteringsnyckeln. V\u00e4nligen ange den uppdaterade nyckeln." + }, "user": { "data": { "host": "V\u00e4rddatorn", diff --git a/homeassistant/components/evil_genius_labs/translations/sv.json b/homeassistant/components/evil_genius_labs/translations/sv.json new file mode 100644 index 00000000000..d51f98c6100 --- /dev/null +++ b/homeassistant/components/evil_genius_labs/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ezviz/translations/sv.json b/homeassistant/components/ezviz/translations/sv.json index 4c047d75573..971e996a103 100644 --- a/homeassistant/components/ezviz/translations/sv.json +++ b/homeassistant/components/ezviz/translations/sv.json @@ -26,5 +26,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Timeout f\u00f6r beg\u00e4ran (sekunder)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/fjaraskupan/translations/sv.json b/homeassistant/components/fjaraskupan/translations/sv.json new file mode 100644 index 00000000000..baa8ed1ba40 --- /dev/null +++ b/homeassistant/components/fjaraskupan/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "confirm": { + "description": "Vill du s\u00e4tta upp Fj\u00e4r\u00e5skupan?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/en.json b/homeassistant/components/flunearyou/translations/en.json index 3dcbfa2a628..7e76b54b18a 100644 --- a/homeassistant/components/flunearyou/translations/en.json +++ b/homeassistant/components/flunearyou/translations/en.json @@ -22,7 +22,7 @@ "fix_flow": { "step": { "confirm": { - "description": "The data source that powered the Flu Near You integration is no longer available. Press SUBMIT to remove all configured instances of the integration from Home Assistant.", + "description": "The external data source powering the Flu Near You integration is no longer available; thus, the integration no longer works.\n\nPress SUBMIT to remove Flu Near You from your Home Assistant instance.", "title": "Remove Flu Near You" } } diff --git a/homeassistant/components/flunearyou/translations/fr.json b/homeassistant/components/flunearyou/translations/fr.json index a9d8064d865..ebc2ccc385a 100644 --- a/homeassistant/components/flunearyou/translations/fr.json +++ b/homeassistant/components/flunearyou/translations/fr.json @@ -16,5 +16,10 @@ "title": "Configurer Flu Near You" } } + }, + "issues": { + "integration_removal": { + "title": "Flu Near You n'est plus disponible" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/id.json b/homeassistant/components/flunearyou/translations/id.json index 86afc7bb5fd..72fcfefc78d 100644 --- a/homeassistant/components/flunearyou/translations/id.json +++ b/homeassistant/components/flunearyou/translations/id.json @@ -16,5 +16,18 @@ "title": "Konfigurasikan Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "Sumber data eksternal yang mendukung integrasi Flu Near You tidak lagi tersedia, sehingga integrasi tidak lagi berfungsi. \n\nTekan KIRIM untuk menghapus integrasi Flu Near You dari instans Home Assistant Anda.", + "title": "Hapus Integrasi Flu Near You" + } + } + }, + "title": "Integrasi Flu Year You tidak lagi tersedia" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/pt-BR.json b/homeassistant/components/flunearyou/translations/pt-BR.json index dc63fa1baf8..eeca693be89 100644 --- a/homeassistant/components/flunearyou/translations/pt-BR.json +++ b/homeassistant/components/flunearyou/translations/pt-BR.json @@ -16,5 +16,18 @@ "title": "Configurar Flue Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "A fonte de dados externa que alimenta a integra\u00e7\u00e3o do Flu Near You n\u00e3o est\u00e1 mais dispon\u00edvel; assim, a integra\u00e7\u00e3o n\u00e3o funciona mais. \n\n Pressione ENVIAR para remover Flu Near You da sua inst\u00e2ncia do Home Assistant.", + "title": "Remova Flu Near You" + } + } + }, + "title": "Flu Near You n\u00e3o est\u00e1 mais dispon\u00edvel" + } } } \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/sv.json b/homeassistant/components/flux_led/translations/sv.json new file mode 100644 index 00000000000..ec732eb1897 --- /dev/null +++ b/homeassistant/components/flux_led/translations/sv.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{model} {id} ({ipaddr})", + "step": { + "discovery_confirm": { + "description": "Vill du konfigurera {modell} {id} ({ipaddr})?" + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Om du l\u00e4mnar v\u00e4rden tomt anv\u00e4nds discovery f\u00f6r att hitta enheter." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "custom_effect_colors": "Anpassad effekt: Lista med 1 till 16 [R,G,B] f\u00e4rger. Exempel: [255,0,255],[60,128,0]", + "custom_effect_speed_pct": "Anpassad effekt: Hastighet i procent f\u00f6r effekten som byter f\u00e4rg.", + "custom_effect_transition": "Anpassad effekt: Typ av \u00f6verg\u00e5ng mellan f\u00e4rgerna.", + "mode": "Det valda l\u00e4get f\u00f6r ljusstyrka." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/sl.json b/homeassistant/components/freebox/translations/sl.json index 4dc79b32256..0a52dd187b3 100644 --- a/homeassistant/components/freebox/translations/sl.json +++ b/homeassistant/components/freebox/translations/sl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Gostitelj je \u017ee konfiguriran" + "already_configured": "Naprava je \u017ee konfigurirana" }, "error": { "cannot_connect": "Povezava ni uspela, poskusite znova", diff --git a/homeassistant/components/freebox/translations/sv.json b/homeassistant/components/freebox/translations/sv.json index ed8ebe67ae9..dbc060b4d27 100644 --- a/homeassistant/components/freebox/translations/sv.json +++ b/homeassistant/components/freebox/translations/sv.json @@ -10,6 +10,7 @@ }, "step": { "link": { + "description": "Klicka p\u00e5 \"Skicka\" och tryck sedan p\u00e5 h\u00f6gerpilen p\u00e5 routern f\u00f6r att registrera Freebox med Home Assistant. \n\n ![Plats f\u00f6r knappen p\u00e5 routern](/static/images/config_freebox.png)", "title": "L\u00e4nka Freebox-router" }, "user": { diff --git a/homeassistant/components/freedompro/translations/sv.json b/homeassistant/components/freedompro/translations/sv.json index 83f1c6b3dac..e4b85f92bba 100644 --- a/homeassistant/components/freedompro/translations/sv.json +++ b/homeassistant/components/freedompro/translations/sv.json @@ -12,6 +12,7 @@ "data": { "api_key": "API-nyckel" }, + "description": "Ange API-nyckeln fr\u00e5n https://home.freedompro.eu", "title": "Freedompro API-nyckel" } } diff --git a/homeassistant/components/fritz/translations/sv.json b/homeassistant/components/fritz/translations/sv.json index 5afcb156965..f29f7f0ba78 100644 --- a/homeassistant/components/fritz/translations/sv.json +++ b/homeassistant/components/fritz/translations/sv.json @@ -1,28 +1,44 @@ { "config": { "abort": { - "ignore_ip6_link_local": "IPv6-l\u00e4nkens lokala adress st\u00f6ds inte." + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "ignore_ip6_link_local": "IPv6-l\u00e4nkens lokala adress st\u00f6ds inte.", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { - "cannot_connect": "Det gick inte att ansluta." + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" }, + "flow_title": "{name}", "step": { "confirm": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Uppt\u00e4ckte FRITZ!Box: {name} \n\n St\u00e4ll in FRITZ!Box-verktyg f\u00f6r att styra ditt {name}", + "title": "St\u00e4ll in FRITZ!Box Tools" }, "reauth_confirm": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" }, + "description": "Uppdatera FRITZ! Box Tools autentiseringsuppgifter f\u00f6r: {host}.\n\nFRITZ! Box Tools kan inte logga in p\u00e5 din FRITZ!Box.", "title": "Uppdaterar FRITZ!Box Tools - referenser" }, "user": { "data": { "host": "V\u00e4rd", + "password": "L\u00f6senord", + "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Konfigurera FRITZ!Box Tools f\u00f6r att styra din FRITZ!Box.\nMinimikrav: anv\u00e4ndarnamn och l\u00f6senord.", + "title": "St\u00e4ll in FRITZ!Box Tools" } } }, diff --git a/homeassistant/components/fritzbox/translations/sv.json b/homeassistant/components/fritzbox/translations/sv.json index a028ec5f217..296f7244c1e 100644 --- a/homeassistant/components/fritzbox/translations/sv.json +++ b/homeassistant/components/fritzbox/translations/sv.json @@ -1,13 +1,17 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", "ignore_ip6_link_local": "IPv6-l\u00e4nkens lokala adress st\u00f6ds inte.", "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "not_supported": "Ansluten till AVM FRITZ!Box men den kan inte styra smarta hemenheter.", "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "invalid_auth": "Ogiltig autentisering" }, + "flow_title": "{name}", "step": { "confirm": { "data": { @@ -28,7 +32,8 @@ "host": "V\u00e4rd eller IP-adress", "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange din AVM FRITZ!Box-information." } } } diff --git a/homeassistant/components/fronius/translations/sv.json b/homeassistant/components/fronius/translations/sv.json new file mode 100644 index 00000000000..f341a6314ee --- /dev/null +++ b/homeassistant/components/fronius/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garages_amsterdam/translations/sv.json b/homeassistant/components/garages_amsterdam/translations/sv.json new file mode 100644 index 00000000000..e9209b99a82 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "garage_name": "Garagenamn" + }, + "title": "V\u00e4lj ett garage att \u00f6vervaka" + } + } + }, + "title": "Garage Amsterdam" +} \ No newline at end of file diff --git a/homeassistant/components/generic/translations/he.json b/homeassistant/components/generic/translations/he.json index 3d2f8cdca4e..2a3458cd3e7 100644 --- a/homeassistant/components/generic/translations/he.json +++ b/homeassistant/components/generic/translations/he.json @@ -7,7 +7,9 @@ "error": { "already_exists": "\u05de\u05e6\u05dc\u05de\u05d4 \u05e2\u05dd \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05d5 \u05db\u05d1\u05e8 \u05e7\u05d9\u05d9\u05de\u05ea.", "invalid_still_image": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05dc\u05d0 \u05d4\u05d7\u05d6\u05d9\u05e8\u05d4 \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 \u05d7\u05d5\u05e7\u05d9\u05ea", + "malformed_url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05d2\u05d5\u05d9\u05d4", "no_still_image_or_stream_url": "\u05d9\u05e9 \u05dc\u05e6\u05d9\u05d9\u05df \u05dc\u05e4\u05d7\u05d5\u05ea \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05d4\u05d6\u05e8\u05de\u05d4", + "relative_url": "\u05db\u05ea\u05d5\u05d1\u05d5\u05ea \u05d0\u05ea\u05e8\u05d9\u05dd \u05d9\u05d7\u05e1\u05d9\u05d5\u05ea \u05d0\u05d9\u05e0\u05df \u05de\u05d5\u05ea\u05e8\u05d5\u05ea", "stream_file_not_found": "\u05d4\u05e7\u05d5\u05d1\u05e5 \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4 (\u05d4\u05d0\u05dd ffmpeg \u05de\u05d5\u05ea\u05e7\u05df?)", "stream_http_not_found": "HTTP 404 \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", "stream_io_error": "\u05e9\u05d2\u05d9\u05d0\u05ea \u05e7\u05dc\u05d8/\u05e4\u05dc\u05d8 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4. \u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 \u05e9\u05d2\u05d5\u05d9 \u05e9\u05dc RTSP?", @@ -15,6 +17,7 @@ "stream_no_video": "\u05d0\u05d9\u05df \u05d5\u05d9\u05d3\u05d9\u05d0\u05d5 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", "stream_not_permitted": "\u05d4\u05e4\u05e2\u05d5\u05dc\u05d4 \u05d0\u05d9\u05e0\u05d4 \u05de\u05d5\u05ea\u05e8\u05ea \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4. \u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 \u05e9\u05d2\u05d5\u05d9 \u05e9\u05dc RTSP?", "stream_unauthorised": "\u05d4\u05d4\u05e8\u05e9\u05d0\u05d4 \u05e0\u05db\u05e9\u05dc\u05d4 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", + "template_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05e2\u05d9\u05d1\u05d5\u05d3 \u05d4\u05ea\u05d1\u05e0\u05d9\u05ea. \u05e2\u05d9\u05d9\u05df \u05d1\u05d9\u05d5\u05de\u05df \u05dc\u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.", "timeout": "\u05d6\u05de\u05df \u05e7\u05e6\u05d5\u05d1 \u05d1\u05e2\u05ea \u05d8\u05e2\u05d9\u05e0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", "unable_still_load": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d8\u05e2\u05d5\u05df \u05ea\u05de\u05d5\u05e0\u05d4 \u05d7\u05d5\u05e7\u05d9\u05ea \u05de\u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05e9\u05dc \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, \u05db\u05e9\u05dc \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9 \u05d1\u05de\u05d7\u05e9\u05d1 \u05de\u05d0\u05e8\u05d7, \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d0\u05d5 \u05d0\u05d9\u05de\u05d5\u05ea). \u05e0\u05d0 \u05dc\u05e2\u05d9\u05d9\u05df \u05d1\u05d9\u05d5\u05de\u05df \u05d4\u05e8\u05d9\u05e9\u05d5\u05dd \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" @@ -49,7 +52,9 @@ "error": { "already_exists": "\u05de\u05e6\u05dc\u05de\u05d4 \u05e2\u05dd \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05d5 \u05db\u05d1\u05e8 \u05e7\u05d9\u05d9\u05de\u05ea.", "invalid_still_image": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05dc\u05d0 \u05d4\u05d7\u05d6\u05d9\u05e8\u05d4 \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 \u05d7\u05d5\u05e7\u05d9\u05ea", + "malformed_url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05d2\u05d5\u05d9\u05d4", "no_still_image_or_stream_url": "\u05d9\u05e9 \u05dc\u05e6\u05d9\u05d9\u05df \u05dc\u05e4\u05d7\u05d5\u05ea \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05d4\u05d6\u05e8\u05de\u05d4", + "relative_url": "\u05db\u05ea\u05d5\u05d1\u05d5\u05ea \u05d0\u05ea\u05e8\u05d9\u05dd \u05d9\u05d7\u05e1\u05d9\u05d5\u05ea \u05d0\u05d9\u05e0\u05df \u05de\u05d5\u05ea\u05e8\u05d5\u05ea", "stream_file_not_found": "\u05d4\u05e7\u05d5\u05d1\u05e5 \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4 (\u05d4\u05d0\u05dd ffmpeg \u05de\u05d5\u05ea\u05e7\u05df?)", "stream_http_not_found": "HTTP 404 \u05dc\u05d0 \u05e0\u05de\u05e6\u05d0 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", "stream_io_error": "\u05e9\u05d2\u05d9\u05d0\u05ea \u05e7\u05dc\u05d8/\u05e4\u05dc\u05d8 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4. \u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 \u05e9\u05d2\u05d5\u05d9 \u05e9\u05dc RTSP?", @@ -57,6 +62,7 @@ "stream_no_video": "\u05d0\u05d9\u05df \u05d5\u05d9\u05d3\u05d9\u05d0\u05d5 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", "stream_not_permitted": "\u05d4\u05e4\u05e2\u05d5\u05dc\u05d4 \u05d0\u05d9\u05e0\u05d4 \u05de\u05d5\u05ea\u05e8\u05ea \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4. \u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 \u05e9\u05d2\u05d5\u05d9 \u05e9\u05dc RTSP?", "stream_unauthorised": "\u05d4\u05d4\u05e8\u05e9\u05d0\u05d4 \u05e0\u05db\u05e9\u05dc\u05d4 \u05d1\u05e2\u05ea \u05e0\u05d9\u05e1\u05d9\u05d5\u05df \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05dc\u05d4\u05d6\u05e8\u05de\u05d4", + "template_error": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05e2\u05d9\u05d1\u05d5\u05d3 \u05d4\u05ea\u05d1\u05e0\u05d9\u05ea. \u05e2\u05d9\u05d9\u05df \u05d1\u05d9\u05d5\u05de\u05df \u05dc\u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.", "timeout": "\u05d6\u05de\u05df \u05e7\u05e6\u05d5\u05d1 \u05d1\u05e2\u05ea \u05d8\u05e2\u05d9\u05e0\u05ea \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8", "unable_still_load": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d8\u05e2\u05d5\u05df \u05ea\u05de\u05d5\u05e0\u05d4 \u05d7\u05d5\u05e7\u05d9\u05ea \u05de\u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05e9\u05dc \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, \u05db\u05e9\u05dc \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9 \u05d1\u05de\u05d7\u05e9\u05d1 \u05de\u05d0\u05e8\u05d7, \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d0\u05d5 \u05d0\u05d9\u05de\u05d5\u05ea). \u05e0\u05d0 \u05dc\u05e2\u05d9\u05d9\u05df \u05d1\u05d9\u05d5\u05de\u05df \u05d4\u05e8\u05d9\u05e9\u05d5\u05dd \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3.", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" @@ -77,8 +83,12 @@ "rtsp_transport": "\u05e4\u05e8\u05d5\u05d8\u05d5\u05e7\u05d5\u05dc \u05ea\u05e2\u05d1\u05d5\u05e8\u05d4 RTSP", "still_image_url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05ea\u05de\u05d5\u05e0\u05ea \u05e1\u05d8\u05d9\u05dc\u05e1 (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, https://...)", "stream_source": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05e9\u05dc \u05de\u05e7\u05d5\u05e8 \u05d6\u05e8\u05dd (\u05dc\u05d3\u05d5\u05d2\u05de\u05d4, rtsp://...)", + "use_wallclock_as_timestamps": "\u05e9\u05d9\u05de\u05d5\u05e9 \u05d1\u05e9\u05e2\u05d5\u05df \u05e7\u05d9\u05e8 \u05db\u05d7\u05d5\u05ea\u05de\u05d5\u05ea \u05d6\u05de\u05df", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + }, + "data_description": { + "use_wallclock_as_timestamps": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05d6\u05d5 \u05e2\u05e9\u05d5\u05d9\u05d4 \u05dc\u05ea\u05e7\u05df \u05d1\u05e2\u05d9\u05d5\u05ea \u05e4\u05d9\u05dc\u05d5\u05d7 \u05d0\u05d5 \u05e7\u05e8\u05d9\u05e1\u05d4 \u05d4\u05e0\u05d5\u05d1\u05e2\u05d5\u05ea \u05de\u05d9\u05d9\u05e9\u05d5\u05de\u05d9 \u05d7\u05d5\u05ea\u05de\u05ea \u05d6\u05de\u05df \u05d1\u05d0\u05d2\u05d9\u05dd \u05d1\u05de\u05e6\u05dc\u05de\u05d5\u05ea \u05de\u05e1\u05d5\u05d9\u05de\u05d5\u05ea" } } } diff --git a/homeassistant/components/goalzero/translations/sv.json b/homeassistant/components/goalzero/translations/sv.json new file mode 100644 index 00000000000..bc0c0d03eca --- /dev/null +++ b/homeassistant/components/goalzero/translations/sv.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "confirm_discovery": { + "description": "DHCP-reservation p\u00e5 din router rekommenderas. Om den inte \u00e4r konfigurerad kan enheten bli otillg\u00e4nglig tills Home Assistant uppt\u00e4cker den nya IP-adressen. Se din routers anv\u00e4ndarmanual." + }, + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn" + }, + "description": "Se dokumentationen f\u00f6r att s\u00e4kerst\u00e4lla att alla krav \u00e4r uppfyllda." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/he.json b/homeassistant/components/google/translations/he.json index 191450ed8eb..691ae9547c0 100644 --- a/homeassistant/components/google/translations/he.json +++ b/homeassistant/components/google/translations/he.json @@ -3,10 +3,12 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", "oauth_error": "\u05d4\u05ea\u05e7\u05d1\u05dc\u05d5 \u05e0\u05ea\u05d5\u05e0\u05d9 \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd.", - "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", + "timeout_connect": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8" }, "create_entry": { "default": "\u05d0\u05d5\u05de\u05ea \u05d1\u05d4\u05e6\u05dc\u05d7\u05d4" diff --git a/homeassistant/components/govee_ble/translations/he.json b/homeassistant/components/govee_ble/translations/he.json new file mode 100644 index 00000000000..de780eb221a --- /dev/null +++ b/homeassistant/components/govee_ble/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/sv.json b/homeassistant/components/homeassistant/translations/sv.json index b0f67a5754e..9e6855c5e87 100644 --- a/homeassistant/components/homeassistant/translations/sv.json +++ b/homeassistant/components/homeassistant/translations/sv.json @@ -1,9 +1,19 @@ { "system_health": { "info": { + "arch": "CPU-arkitektur", "config_dir": "Konfigurationskatalog", + "dev": "Utveckling", + "docker": "Docker", "hassio": "Supervisor", - "user": "Anv\u00e4ndare" + "installation_type": "Installationstyp", + "os_name": "Operativsystemfamilj", + "os_version": "Operativsystemversion", + "python_version": "Python-version", + "timezone": "Tidszon", + "user": "Anv\u00e4ndare", + "version": "Version", + "virtualenv": "Virtuell milj\u00f6" } } } \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/he.json b/homeassistant/components/homeassistant_alerts/translations/he.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/he.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/sv.json b/homeassistant/components/homekit/translations/sv.json index 250bb634736..caddbeaf7d2 100644 --- a/homeassistant/components/homekit/translations/sv.json +++ b/homeassistant/components/homekit/translations/sv.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "port_name_in_use": "Ett tillbeh\u00f6r eller brygga med samma namn eller port \u00e4r redan konfigurerat." + }, "step": { "pairing": { + "description": "F\u00f6r att slutf\u00f6ra ihopparningen f\u00f6lj instruktionerna i \"Meddelanden\" under \"HomeKit-parning\".", "title": "Para HomeKit" }, "user": { diff --git a/homeassistant/components/homekit_controller/translations/he.json b/homeassistant/components/homekit_controller/translations/he.json index 9593bbd90e4..c5ec291569d 100644 --- a/homeassistant/components/homekit_controller/translations/he.json +++ b/homeassistant/components/homekit_controller/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea" }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "user": { "data": { diff --git a/homeassistant/components/homekit_controller/translations/sensor.ca.json b/homeassistant/components/homekit_controller/translations/sensor.ca.json new file mode 100644 index 00000000000..d8abac44cac --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.ca.json @@ -0,0 +1,19 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "full": "Dispositiu final complet", + "minimal": "Dispositiu final redu\u00eft", + "none": "Cap", + "router_eligible": "Dispositiu final apte per ser encaminador (router)", + "sleepy": "Dispositiu final dorment" + }, + "homekit_controller__thread_status": { + "child": "Fill", + "detached": "Desconnectat", + "disabled": "Desactivat", + "joining": "Unint-se", + "leader": "L\u00edder", + "router": "Encaminador (router)" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.fr.json b/homeassistant/components/homekit_controller/translations/sensor.fr.json new file mode 100644 index 00000000000..5855bbc94ec --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.fr.json @@ -0,0 +1,7 @@ +{ + "state": { + "homekit_controller__thread_status": { + "child": "Enfant" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.id.json b/homeassistant/components/homekit_controller/translations/sensor.id.json new file mode 100644 index 00000000000..5697598ef54 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.id.json @@ -0,0 +1,11 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "none": "Tidak Ada" + }, + "homekit_controller__thread_status": { + "disabled": "Dinonaktifkan", + "router": "Router" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.no.json b/homeassistant/components/homekit_controller/translations/sensor.no.json new file mode 100644 index 00000000000..f0323c0326a --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.no.json @@ -0,0 +1,12 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Kan med kantruter", + "full": "Full End-enhet", + "minimal": "Minimal sluttenhet", + "none": "Ingen", + "router_eligible": "Ruterkvalifisert sluttenhet", + "sleepy": "S\u00f8vnig sluttenhet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.pt-BR.json b/homeassistant/components/homekit_controller/translations/sensor.pt-BR.json new file mode 100644 index 00000000000..f4a5edf80ee --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.pt-BR.json @@ -0,0 +1,21 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Capacidade de roteador de borda", + "full": "Dispositivo final completo", + "minimal": "Dispositivo final m\u00ednimo", + "none": "Nenhum", + "router_eligible": "Dispositivo final qualificado para roteador", + "sleepy": "Dispositivo final sonolento" + }, + "homekit_controller__thread_status": { + "border_router": "Roteador de borda", + "child": "Filho", + "detached": "Separado", + "disabled": "Desabilitado", + "joining": "Juntar", + "leader": "L\u00edder", + "router": "Roteador" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.ru.json b/homeassistant/components/homekit_controller/translations/sensor.ru.json new file mode 100644 index 00000000000..c6e67b645e3 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.ru.json @@ -0,0 +1,7 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "sleepy": "\u0421\u043f\u044f\u0449\u0435\u0435 \u043a\u043e\u043d\u0435\u0447\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.zh-Hant.json b/homeassistant/components/homekit_controller/translations/sensor.zh-Hant.json new file mode 100644 index 00000000000..123469d79cf --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.zh-Hant.json @@ -0,0 +1,12 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "\u7db2\u8def\u6838\u5fc3\u80fd\u529b", + "full": "\u6240\u6709\u7d42\u7aef\u88dd\u7f6e", + "minimal": "\u6700\u4f4e\u7d42\u7aef\u88dd\u7f6e", + "none": "\u7121", + "router_eligible": "\u4e2d\u9593\u5c64\u7d42\u7aef\u88dd\u7f6e", + "sleepy": "\u5f85\u547d\u7d42\u7aef\u88dd\u7f6e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sv.json b/homeassistant/components/homekit_controller/translations/sv.json index e5a05b62b84..1766689ca69 100644 --- a/homeassistant/components/homekit_controller/translations/sv.json +++ b/homeassistant/components/homekit_controller/translations/sv.json @@ -24,6 +24,7 @@ "title": "Enheten paras redan med en annan styrenhet" }, "max_tries_error": { + "description": "Enheten har f\u00e5tt mer \u00e4n 100 misslyckade autentiseringsf\u00f6rs\u00f6k. Testa att starta om enheten och forts\u00e4tt sedan att \u00e5teruppta ihopkopplingen.", "title": "Maximalt antal autentiseringsf\u00f6rs\u00f6k har \u00f6verskridits" }, "pair": { diff --git a/homeassistant/components/honeywell/translations/sv.json b/homeassistant/components/honeywell/translations/sv.json index 23c825f256f..287e24372ba 100644 --- a/homeassistant/components/honeywell/translations/sv.json +++ b/homeassistant/components/honeywell/translations/sv.json @@ -1,10 +1,15 @@ { "config": { + "error": { + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange de autentiseringsuppgifter som anv\u00e4nds f\u00f6r att logga in p\u00e5 mytotalconnectcomfort.com." } } } diff --git a/homeassistant/components/hue/translations/sv.json b/homeassistant/components/hue/translations/sv.json index 1cf157f02e5..f09232f92ab 100644 --- a/homeassistant/components/hue/translations/sv.json +++ b/homeassistant/components/hue/translations/sv.json @@ -30,6 +30,10 @@ }, "device_automation": { "trigger_subtype": { + "1": "F\u00f6rsta knappen", + "2": "Andra knappen", + "3": "Tredje knappen", + "4": "Fj\u00e4rde knappen", "button_1": "F\u00f6rsta knappen", "button_2": "Andra knappen", "button_3": "Tredje knappen", @@ -42,18 +46,24 @@ "turn_on": "Starta" }, "trigger_type": { + "double_short_release": "B\u00e5da \"{subtyp}\" sl\u00e4pptes", + "initial_press": "Knappen \" {subtype} \" trycktes f\u00f6rst", + "long_release": "Knappen \" {subtype} \" sl\u00e4pps efter l\u00e5ng tryckning", "remote_button_long_release": "\"{subtype}\" knappen sl\u00e4pptes efter ett l\u00e5ngt tryck", "remote_button_short_press": "\"{subtype}\" knappen nedtryckt", "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt", "remote_double_button_long_press": "B\u00e5da \"{subtype}\" sl\u00e4pptes efter en l\u00e5ngtryckning", - "remote_double_button_short_press": "B\u00e5da \"{subtyp}\" sl\u00e4pptes" + "remote_double_button_short_press": "B\u00e5da \"{subtyp}\" sl\u00e4pptes", + "repeat": "Knappen \" {subtype} \" h\u00f6lls nedtryckt", + "short_release": "Knappen \" {subtype} \" sl\u00e4pps efter kort tryckning" } }, "options": { "step": { "init": { "data": { - "allow_hue_groups": "Till\u00e5t Hue-grupper" + "allow_hue_groups": "Till\u00e5t Hue-grupper", + "allow_hue_scenes": "Till\u00e5t Hue-scener" } } } diff --git a/homeassistant/components/hunterdouglas_powerview/translations/sv.json b/homeassistant/components/hunterdouglas_powerview/translations/sv.json index e572ec2c4a7..3ec2c552711 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/sv.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/sv.json @@ -7,6 +7,7 @@ "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{name} ({host})", "step": { "link": { "description": "Vill du konfigurera {name} ({host})?", diff --git a/homeassistant/components/hvv_departures/translations/sv.json b/homeassistant/components/hvv_departures/translations/sv.json index c1621e16f2c..0aa84f03b95 100644 --- a/homeassistant/components/hvv_departures/translations/sv.json +++ b/homeassistant/components/hvv_departures/translations/sv.json @@ -9,10 +9,25 @@ "no_results": "Inga resultat. F\u00f6rs\u00f6k med en annan station/adress" }, "step": { + "station": { + "data": { + "station": "Station/adress" + }, + "title": "Ange station/adress" + }, + "station_select": { + "data": { + "station": "Station/adress" + }, + "title": "V\u00e4lj station/adress" + }, "user": { "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Anslut till HVV API" } } }, @@ -20,8 +35,12 @@ "step": { "init": { "data": { + "filter": "V\u00e4lj linjer", + "offset": "Offset (minuter)", "real_time": "Anv\u00e4nd realtidsdata" - } + }, + "description": "\u00c4ndra alternativ f\u00f6r denna avg\u00e5ngssensor", + "title": "Alternativ" } } } diff --git a/homeassistant/components/hyperion/translations/sv.json b/homeassistant/components/hyperion/translations/sv.json index 56cee0c4ad2..9de6a5673c0 100644 --- a/homeassistant/components/hyperion/translations/sv.json +++ b/homeassistant/components/hyperion/translations/sv.json @@ -23,6 +23,7 @@ "description": "Konfigurera auktorisering till din Hyperion Ambilight-server" }, "confirm": { + "description": "Vill du l\u00e4gga till denna Hyperion Ambilight till Home Assistant? \n\n **V\u00e4rd:** {host}\n **Port:** {port}\n **ID**: {id}", "title": "Bekr\u00e4fta till\u00e4gg av Hyperion Ambilight-tj\u00e4nst" }, "create_token": { diff --git a/homeassistant/components/iaqualink/translations/sl.json b/homeassistant/components/iaqualink/translations/sl.json index 3b92f653e7f..eb3e1a4dfdd 100644 --- a/homeassistant/components/iaqualink/translations/sl.json +++ b/homeassistant/components/iaqualink/translations/sl.json @@ -4,7 +4,7 @@ "user": { "data": { "password": "Geslo", - "username": "Uporabni\u0161ko ime / e-po\u0161tni naslov" + "username": "Uporabni\u0161ko ime" }, "description": "Prosimo, vnesite uporabni\u0161ko ime in geslo za iAqualink ra\u010dun.", "title": "Pove\u017eite se z iAqualink" diff --git a/homeassistant/components/icloud/translations/sv.json b/homeassistant/components/icloud/translations/sv.json index 5aa6e9574f8..b0a052ecef4 100644 --- a/homeassistant/components/icloud/translations/sv.json +++ b/homeassistant/components/icloud/translations/sv.json @@ -2,13 +2,21 @@ "config": { "abort": { "already_configured": "Kontot har redan konfigurerats", - "no_device": "Ingen av dina enheter har \"Hitta min iPhone\" aktiverat" + "no_device": "Ingen av dina enheter har \"Hitta min iPhone\" aktiverat", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "send_verification_code": "Det gick inte att skicka verifieringskod", "validate_verification_code": "Det gick inte att verifiera verifieringskoden, v\u00e4lj en betrodd enhet och starta verifieringen igen" }, "step": { + "reauth": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Ditt tidigare angivna l\u00f6senord f\u00f6r {username} fungerar inte l\u00e4ngre. Uppdatera ditt l\u00f6senord f\u00f6r att forts\u00e4tta anv\u00e4nda denna integration.", + "title": "\u00c5terautenticera integration" + }, "trusted_device": { "data": { "trusted_device": "Betrodd enhet" diff --git a/homeassistant/components/inkbird/translations/he.json b/homeassistant/components/inkbird/translations/he.json new file mode 100644 index 00000000000..de780eb221a --- /dev/null +++ b/homeassistant/components/inkbird/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/sv.json b/homeassistant/components/insteon/translations/sv.json index 218b8d6d748..7b1692e7ec9 100644 --- a/homeassistant/components/insteon/translations/sv.json +++ b/homeassistant/components/insteon/translations/sv.json @@ -1,17 +1,37 @@ { "config": { "abort": { - "not_insteon_device": "Uppt\u00e4ckt enhet \u00e4r inte en Insteon-enhet" + "not_insteon_device": "Uppt\u00e4ckt enhet \u00e4r inte en Insteon-enhet", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, "flow_title": "{name}", "step": { "confirm_usb": { "description": "Vill du konfigurera {name}?" }, + "hubv1": { + "data": { + "host": "IP-adress", + "port": "Port" + }, + "description": "Konfigurera Insteon Hub version 1 (f\u00f6re 2014).", + "title": "Insteon Hub version 1" + }, "hubv2": { "data": { + "host": "IP-adress", + "password": "L\u00f6senord", + "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Konfigurera Insteon Hub version 2.", + "title": "Insteon Hub version 2" + }, + "user": { + "data": { + "modem_type": "Modemtyp." + }, + "description": "V\u00e4lj Insteon-modemtyp." } } }, diff --git a/homeassistant/components/islamic_prayer_times/translations/sv.json b/homeassistant/components/islamic_prayer_times/translations/sv.json index f865c5a2c6a..8d9c224e8f2 100644 --- a/homeassistant/components/islamic_prayer_times/translations/sv.json +++ b/homeassistant/components/islamic_prayer_times/translations/sv.json @@ -2,6 +2,22 @@ "config": { "abort": { "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du s\u00e4tta upp islamiska b\u00f6netider?", + "title": "St\u00e4ll in islamiska b\u00f6netider" + } } - } + }, + "options": { + "step": { + "init": { + "data": { + "calculation_method": "Ber\u00e4kningsmetod f\u00f6r b\u00f6n" + } + } + } + }, + "title": "Islamiska b\u00f6netider" } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/sv.json b/homeassistant/components/isy994/translations/sv.json index 334c3e0fb0d..a3b9ccb3d2f 100644 --- a/homeassistant/components/isy994/translations/sv.json +++ b/homeassistant/components/isy994/translations/sv.json @@ -38,11 +38,20 @@ "data": { "ignore_string": "Ignorera str\u00e4ng", "restore_light_state": "\u00c5terst\u00e4ll ljusstyrkan", - "sensor_string": "Nodsensorstr\u00e4ng" + "sensor_string": "Nodsensorstr\u00e4ng", + "variable_sensor_string": "Variabel sensorstr\u00e4ng" }, "description": "St\u00e4ll in alternativen f\u00f6r ISY-integration:\n \u2022 Nodsensorstr\u00e4ng: Alla enheter eller mappar som inneh\u00e5ller 'Nodsensorstr\u00e4ng' i namnet kommer att behandlas som en sensor eller bin\u00e4r sensor.\n \u2022 Ignorera str\u00e4ng: Alla enheter med 'Ignorera str\u00e4ng' i namnet kommer att ignoreras.\n \u2022 Variabel sensorstr\u00e4ng: Varje variabel som inneh\u00e5ller 'Variabel sensorstr\u00e4ng' kommer att l\u00e4ggas till som en sensor.\n \u2022 \u00c5terst\u00e4ll ljusstyrka: Om den \u00e4r aktiverad kommer den tidigare ljusstyrkan att \u00e5terst\u00e4llas n\u00e4r du sl\u00e5r p\u00e5 en lampa ist\u00e4llet f\u00f6r enhetens inbyggda On-Level.", "title": "ISY alternativ" } } + }, + "system_health": { + "info": { + "device_connected": "ISY ansluten", + "host_reachable": "V\u00e4rden kan n\u00e5s", + "last_heartbeat": "Tid f\u00f6r senaste hj\u00e4rtslag", + "websocket_status": "Status f\u00f6r h\u00e4ndelseuttag" + } } } \ No newline at end of file diff --git a/homeassistant/components/izone/translations/sl.json b/homeassistant/components/izone/translations/sl.json index 6ce860a74af..5e42482248e 100644 --- a/homeassistant/components/izone/translations/sl.json +++ b/homeassistant/components/izone/translations/sl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "V omre\u017eju ni najdenih naprav iZone.", + "no_devices_found": "V omre\u017eju ni mogo\u010de najti nobene naprave", "single_instance_allowed": "Potrebna je samo ena konfiguracija iZone." }, "step": { diff --git a/homeassistant/components/jellyfin/translations/sv.json b/homeassistant/components/jellyfin/translations/sv.json index 23c825f256f..c48e92fa5f9 100644 --- a/homeassistant/components/jellyfin/translations/sv.json +++ b/homeassistant/components/jellyfin/translations/sv.json @@ -1,8 +1,18 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", + "url": "URL", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/keenetic_ndms2/translations/sv.json b/homeassistant/components/keenetic_ndms2/translations/sv.json index df37b421209..1e263250636 100644 --- a/homeassistant/components/keenetic_ndms2/translations/sv.json +++ b/homeassistant/components/keenetic_ndms2/translations/sv.json @@ -22,9 +22,11 @@ "step": { "user": { "data": { + "consider_home": "Intervall f\u00f6r att betraktas som hemma", "include_arp": "Anv\u00e4nd ARP-data (ignoreras om hotspot-data anv\u00e4nds)", "include_associated": "Anv\u00e4nd WiFi AP-associationsdata (ignoreras om hotspotdata anv\u00e4nds)", "interfaces": "V\u00e4lj gr\u00e4nssnitt att skanna", + "scan_interval": "Skanningsintervall", "try_hotspot": "Anv\u00e4nd \"ip hotspot\"-data (mest korrekt)" } } diff --git a/homeassistant/components/kmtronic/translations/sv.json b/homeassistant/components/kmtronic/translations/sv.json index 8ec6cc04dc9..c5d6f4a0b82 100644 --- a/homeassistant/components/kmtronic/translations/sv.json +++ b/homeassistant/components/kmtronic/translations/sv.json @@ -1,9 +1,18 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { "host": "V\u00e4rd", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/knx/translations/sv.json b/homeassistant/components/knx/translations/sv.json index f5986f966d0..2c2d6f7d14d 100644 --- a/homeassistant/components/knx/translations/sv.json +++ b/homeassistant/components/knx/translations/sv.json @@ -18,10 +18,15 @@ } }, "routing": { + "data": { + "multicast_group": "Multicast-grupp", + "multicast_port": "Multicast-port" + }, "data_description": { "individual_address": "KNX-adress som ska anv\u00e4ndas av Home Assistant, t.ex. `0.0.4`", "local_ip": "L\u00e4mna tomt f\u00f6r att anv\u00e4nda automatisk uppt\u00e4ckt." - } + }, + "description": "Konfigurera routningsalternativen." }, "secure_knxkeys": { "data": { @@ -53,12 +58,32 @@ "secure_knxkeys": "Anv\u00e4nd en fil `.knxkeys` som inneh\u00e5ller s\u00e4kra IP-nycklar.", "secure_manual": "Konfigurera s\u00e4kra IP nycklar manuellt" } + }, + "tunnel": { + "data": { + "gateway": "KNX-tunnelanslutning" + }, + "description": "V\u00e4lj en gateway fr\u00e5n listan." + }, + "type": { + "data": { + "connection_type": "KNX anslutningstyp" + }, + "description": "Ange vilken anslutningstyp vi ska anv\u00e4nda f\u00f6r din KNX-anslutning.\n AUTOMATISK - Integrationen tar hand om anslutningen till din KNX Bus genom att utf\u00f6ra en gateway-skanning.\n TUNNELING - Integrationen kommer att ansluta till din KNX-buss via tunnling.\n ROUTING - Integrationen kommer att ansluta till din KNX-buss via routing." } } }, "options": { "step": { "init": { + "data": { + "connection_type": "KNX anslutningstyp", + "individual_address": "Enskild standardadress", + "multicast_group": "Multicast-grupp", + "multicast_port": "Multicast-port", + "rate_limit": "Hastighetsgr\u00e4ns", + "state_updater": "Tillst\u00e5ndsuppdaterare" + }, "data_description": { "individual_address": "KNX-adress som ska anv\u00e4ndas av Home Assistant, t.ex. `0.0.4`", "local_ip": "Anv\u00e4nd `0.0.0.0.0` f\u00f6r automatisk identifiering.", @@ -70,6 +95,8 @@ }, "tunnel": { "data": { + "host": "V\u00e4rd", + "port": "Port", "tunneling_type": "KNX tunneltyp" }, "data_description": { diff --git a/homeassistant/components/kodi/translations/sv.json b/homeassistant/components/kodi/translations/sv.json index 9102efc653e..b65fb101fd2 100644 --- a/homeassistant/components/kodi/translations/sv.json +++ b/homeassistant/components/kodi/translations/sv.json @@ -1,16 +1,44 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name}", "step": { "credentials": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "V\u00e4nligen ange ditt Kodi-anv\u00e4ndarnamn och l\u00f6senord. Dessa finns i System/Inst\u00e4llningar/N\u00e4tverk/Tj\u00e4nster." + }, + "discovery_confirm": { + "description": "Vill du l\u00e4gga till Kodi (` {name} `) till Home Assistant?", + "title": "Uppt\u00e4ckte Kodi" }, "user": { "data": { "host": "V\u00e4rd" } + }, + "ws_port": { + "data": { + "ws_port": "Port" + }, + "description": "WebSocket-porten (kallas ibland TCP-port i Kodi). F\u00f6r att ansluta \u00f6ver WebSocket m\u00e5ste du aktivera \"Till\u00e5t program ... att styra Kodi\" i System/Inst\u00e4llningar/N\u00e4tverk/Tj\u00e4nster. Om WebSocket inte \u00e4r aktiverat, ta bort porten och l\u00e4mna tom." } } + }, + "device_automation": { + "trigger_type": { + "turn_off": "{entity_name} ombads att st\u00e4ngas av", + "turn_on": "{entity_name} har ombetts att sl\u00e5 p\u00e5" + } } } \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/he.json b/homeassistant/components/konnected/translations/he.json index 622c73e2e61..ab4184fa777 100644 --- a/homeassistant/components/konnected/translations/he.json +++ b/homeassistant/components/konnected/translations/he.json @@ -31,7 +31,7 @@ }, "options_digital": { "data": { - "name": "\u05e9\u05dd (\u05d0\u05d5\u05e4\u05e6\u05d9\u05d5\u05e0\u05dc\u05d9)" + "name": "\u05e9\u05dd" } }, "options_io": { diff --git a/homeassistant/components/konnected/translations/sv.json b/homeassistant/components/konnected/translations/sv.json index 3e236f05952..e8130afd424 100644 --- a/homeassistant/components/konnected/translations/sv.json +++ b/homeassistant/components/konnected/translations/sv.json @@ -87,7 +87,8 @@ "data": { "api_host": "\u00c5sidos\u00e4tt API-v\u00e4rdens URL", "blink": "Blinka p\u00e5 panel-LED n\u00e4r du skickar tillst\u00e5nds\u00e4ndring", - "discovery": "Svara p\u00e5 uppt\u00e4cktsf\u00f6rfr\u00e5gningar i ditt n\u00e4tverk" + "discovery": "Svara p\u00e5 uppt\u00e4cktsf\u00f6rfr\u00e5gningar i ditt n\u00e4tverk", + "override_api_host": "\u00c5sidos\u00e4tt standardwebbadress f\u00f6r Home Assistant API-v\u00e4rdpanel" }, "description": "V\u00e4lj \u00f6nskat beteende f\u00f6r din panel", "title": "Konfigurera \u00d6vrigt" diff --git a/homeassistant/components/kraken/translations/sv.json b/homeassistant/components/kraken/translations/sv.json new file mode 100644 index 00000000000..7476ca4b29a --- /dev/null +++ b/homeassistant/components/kraken/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du starta konfigurationen?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Uppdateringsintervall", + "tracked_asset_pairs": "Sp\u00e5rade tillg\u00e5ngspar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/he.json b/homeassistant/components/lacrosse_view/translations/he.json new file mode 100644 index 00000000000..fe6357d0150 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/he.json b/homeassistant/components/lg_soundbar/translations/he.json new file mode 100644 index 00000000000..25fe66938d7 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/he.json b/homeassistant/components/life360/translations/he.json index e6fa6cd1db9..e4998f86963 100644 --- a/homeassistant/components/life360/translations/he.json +++ b/homeassistant/components/life360/translations/he.json @@ -1,16 +1,25 @@ { "config": { "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "error": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "invalid_username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/life360/translations/sv.json b/homeassistant/components/life360/translations/sv.json index ac104f24f39..77bb8815340 100644 --- a/homeassistant/components/life360/translations/sv.json +++ b/homeassistant/components/life360/translations/sv.json @@ -11,6 +11,7 @@ "error": { "already_configured": "Konto har redan konfigurerats", "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", "invalid_username": "Ogiltigt anv\u00e4ndarnmn", "unknown": "Ov\u00e4ntat fel" }, diff --git a/homeassistant/components/lifx/translations/he.json b/homeassistant/components/lifx/translations/he.json index 380dbc5d7fc..0ea2e6e551b 100644 --- a/homeassistant/components/lifx/translations/he.json +++ b/homeassistant/components/lifx/translations/he.json @@ -1,8 +1,21 @@ { "config": { "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "flow_title": "{label} ({host}) {serial}", + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/translations/sv.json b/homeassistant/components/litejet/translations/sv.json new file mode 100644 index 00000000000..f865c5a2c6a --- /dev/null +++ b/homeassistant/components/litejet/translations/sv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/translations/sv.json b/homeassistant/components/litterrobot/translations/sv.json index 23c825f256f..939b543adea 100644 --- a/homeassistant/components/litterrobot/translations/sv.json +++ b/homeassistant/components/litterrobot/translations/sv.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/local_ip/translations/sl.json b/homeassistant/components/local_ip/translations/sl.json index 06c5f4182d0..0294e61515c 100644 --- a/homeassistant/components/local_ip/translations/sl.json +++ b/homeassistant/components/local_ip/translations/sl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Dovoljena je samo ena konfiguracija lokalnega IP-ja." + "single_instance_allowed": "\u017de konfigurirano. Mo\u017ena je samo ena konfiguracija." }, "step": { "user": { diff --git a/homeassistant/components/logi_circle/translations/sv.json b/homeassistant/components/logi_circle/translations/sv.json index 0d4d01c062b..232b9448ce1 100644 --- a/homeassistant/components/logi_circle/translations/sv.json +++ b/homeassistant/components/logi_circle/translations/sv.json @@ -1,11 +1,15 @@ { "config": { "abort": { + "already_configured": "Konto har redan konfigurerats", "external_error": "Undantag intr\u00e4ffade fr\u00e5n ett annat fl\u00f6de.", - "external_setup": "Logi Circle har konfigurerats fr\u00e5n ett annat fl\u00f6de." + "external_setup": "Logi Circle har konfigurerats fr\u00e5n ett annat fl\u00f6de.", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen." }, "error": { - "follow_link": "V\u00e4nligen f\u00f6lj l\u00e4nken och autentisera innan du trycker p\u00e5 Skicka." + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "follow_link": "V\u00e4nligen f\u00f6lj l\u00e4nken och autentisera innan du trycker p\u00e5 Skicka.", + "invalid_auth": "Ogiltig autentisering" }, "step": { "auth": { diff --git a/homeassistant/components/lookin/translations/sv.json b/homeassistant/components/lookin/translations/sv.json new file mode 100644 index 00000000000..6f9d5f8cc71 --- /dev/null +++ b/homeassistant/components/lookin/translations/sv.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta.", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name} ({host})", + "step": { + "device_name": { + "data": { + "name": "Namn" + } + }, + "discovery_confirm": { + "description": "Vill du konfigurera {name} ({host})?" + }, + "user": { + "data": { + "ip_address": "IP-adress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/sv.json b/homeassistant/components/lovelace/translations/sv.json index c0b5bf9f948..60b790b184f 100644 --- a/homeassistant/components/lovelace/translations/sv.json +++ b/homeassistant/components/lovelace/translations/sv.json @@ -2,6 +2,8 @@ "system_health": { "info": { "dashboards": "Kontrollpaneler", + "mode": "L\u00e4ge", + "resources": "Resurser", "views": "Vyer" } } diff --git a/homeassistant/components/lutron_caseta/translations/sv.json b/homeassistant/components/lutron_caseta/translations/sv.json index 1d512e32d47..3f8e2de7d0f 100644 --- a/homeassistant/components/lutron_caseta/translations/sv.json +++ b/homeassistant/components/lutron_caseta/translations/sv.json @@ -1,6 +1,29 @@ { + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "not_lutron_device": "Uppt\u00e4ckt enhet \u00e4r inte en Lutron-enhet" + }, + "flow_title": "{name} ({host})", + "step": { + "link": { + "description": "F\u00f6r att para med {name} ( {host} ), efter att ha skickat in detta formul\u00e4r, tryck p\u00e5 den svarta knappen p\u00e5 baksidan av bryggan.", + "title": "Para ihop med bryggan" + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Ange enhetens IP-adress.", + "title": "Automatisk anslutning till bryggan" + } + } + }, "device_automation": { "trigger_subtype": { + "button_1": "F\u00f6rsta knappen", + "button_2": "Andra knappen", "close_1": "St\u00e4ng 1", "close_2": "St\u00e4ng 2", "close_3": "St\u00e4ng 3", diff --git a/homeassistant/components/mazda/translations/sv.json b/homeassistant/components/mazda/translations/sv.json index 24f538688bd..c71d7b4faa4 100644 --- a/homeassistant/components/mazda/translations/sv.json +++ b/homeassistant/components/mazda/translations/sv.json @@ -1,5 +1,15 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "account_locked": "Kontot \u00e4r l\u00e5st. V\u00e4nligen f\u00f6rs\u00f6k igen senare.", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/media_player/translations/sv.json b/homeassistant/components/media_player/translations/sv.json index 159ff201fba..e84edf74bb0 100644 --- a/homeassistant/components/media_player/translations/sv.json +++ b/homeassistant/components/media_player/translations/sv.json @@ -10,6 +10,9 @@ }, "trigger_type": { "buffering": "{entity_name} b\u00f6rjar buffra", + "idle": "{entity_name} blir inaktiv", + "paused": "{entity_name} \u00e4r pausad", + "playing": "{entity_name} b\u00f6rjar spela", "turned_off": "{entity_name} st\u00e4ngdes av", "turned_on": "{entity_name} slogs p\u00e5" } diff --git a/homeassistant/components/met/translations/sl.json b/homeassistant/components/met/translations/sl.json index 09e48c1238e..ac64e2d8e53 100644 --- a/homeassistant/components/met/translations/sl.json +++ b/homeassistant/components/met/translations/sl.json @@ -6,7 +6,7 @@ "elevation": "Nadmorska vi\u0161ina", "latitude": "Zemljepisna \u0161irina", "longitude": "Zemljepisna dol\u017eina", - "name": "Ime" + "name": "Naziv" }, "description": "Meteorolo\u0161ki institut", "title": "Lokacija" diff --git a/homeassistant/components/metoffice/translations/sv.json b/homeassistant/components/metoffice/translations/sv.json index f4a63bb449d..b16a23db7f2 100644 --- a/homeassistant/components/metoffice/translations/sv.json +++ b/homeassistant/components/metoffice/translations/sv.json @@ -1,10 +1,21 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "latitude": "Latitud", + "longitude": "Longitud" + }, + "description": "Latitud och longitud kommer att anv\u00e4ndas f\u00f6r att hitta n\u00e4rmaste v\u00e4derstation.", + "title": "Anslut till UK Met Office" } } } diff --git a/homeassistant/components/mikrotik/translations/sv.json b/homeassistant/components/mikrotik/translations/sv.json index 39e645a4c03..bc93490db14 100644 --- a/homeassistant/components/mikrotik/translations/sv.json +++ b/homeassistant/components/mikrotik/translations/sv.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Anslutningen misslyckades", + "invalid_auth": "Ogiltig autentisering", "name_exists": "Namnet finns" }, "step": { diff --git a/homeassistant/components/mill/translations/sv.json b/homeassistant/components/mill/translations/sv.json index 5c7362eeb7b..a7c8f4e0ea3 100644 --- a/homeassistant/components/mill/translations/sv.json +++ b/homeassistant/components/mill/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, "error": { "cannot_connect": "Det gick inte att ansluta." }, diff --git a/homeassistant/components/moat/translations/he.json b/homeassistant/components/moat/translations/he.json new file mode 100644 index 00000000000..de780eb221a --- /dev/null +++ b/homeassistant/components/moat/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/sv.json b/homeassistant/components/mobile_app/translations/sv.json index 4fd209e10cf..8757116010a 100644 --- a/homeassistant/components/mobile_app/translations/sv.json +++ b/homeassistant/components/mobile_app/translations/sv.json @@ -9,5 +9,10 @@ } } }, + "device_automation": { + "action_type": { + "notify": "Skicka ett meddelande" + } + }, "title": "Mobilapp" } \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/sv.json b/homeassistant/components/modem_callerid/translations/sv.json index 584cc33c277..531a1029e21 100644 --- a/homeassistant/components/modem_callerid/translations/sv.json +++ b/homeassistant/components/modem_callerid/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", "no_devices_found": "Inga \u00e5terst\u00e5ende enheter hittades" }, "error": { diff --git a/homeassistant/components/monoprice/translations/sl.json b/homeassistant/components/monoprice/translations/sl.json index 02713888762..3f3b3cbb325 100644 --- a/homeassistant/components/monoprice/translations/sl.json +++ b/homeassistant/components/monoprice/translations/sl.json @@ -4,7 +4,7 @@ "already_configured": "Naprava je \u017ee konfigurirana" }, "error": { - "cannot_connect": "Povezava ni uspela, poskusite znova", + "cannot_connect": "Povezava ni uspela", "unknown": "Nepri\u010dakovana napaka" }, "step": { diff --git a/homeassistant/components/motion_blinds/translations/he.json b/homeassistant/components/motion_blinds/translations/he.json index 3cf199985e5..e41857361b9 100644 --- a/homeassistant/components/motion_blinds/translations/he.json +++ b/homeassistant/components/motion_blinds/translations/he.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4 \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d4\u05d7\u05d9\u05d1\u05d5\u05e8 \u05e2\u05d5\u05d3\u05db\u05e0\u05d5", + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", "connection_error": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, diff --git a/homeassistant/components/motion_blinds/translations/sv.json b/homeassistant/components/motion_blinds/translations/sv.json index 5cee7f232c8..1deca42bc26 100644 --- a/homeassistant/components/motion_blinds/translations/sv.json +++ b/homeassistant/components/motion_blinds/translations/sv.json @@ -20,5 +20,14 @@ "description": "K\u00f6r installationen igen om du vill ansluta ytterligare Motion Gateways" } } + }, + "options": { + "step": { + "init": { + "data": { + "wait_for_push": "V\u00e4nta p\u00e5 multicast push vid uppdatering" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/motioneye/translations/sv.json b/homeassistant/components/motioneye/translations/sv.json index 75bf4f2e2f9..38517e7571e 100644 --- a/homeassistant/components/motioneye/translations/sv.json +++ b/homeassistant/components/motioneye/translations/sv.json @@ -1,9 +1,23 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "invalid_url": "Ogiltig URL", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { - "admin_username": "Admin Anv\u00e4ndarnamn" + "admin_password": "Admin L\u00f6senord", + "admin_username": "Admin Anv\u00e4ndarnamn", + "surveillance_password": "\u00d6vervakning L\u00f6senord", + "surveillance_username": "\u00d6vervakning [%key:common::config_flow::data::anv\u00e4ndarnamn%]", + "url": "URL" } } } diff --git a/homeassistant/components/nam/translations/sv.json b/homeassistant/components/nam/translations/sv.json index ffa62b8fae2..55d985814df 100644 --- a/homeassistant/components/nam/translations/sv.json +++ b/homeassistant/components/nam/translations/sv.json @@ -2,10 +2,12 @@ "config": { "abort": { "device_unsupported": "Enheten st\u00f6ds ej", + "reauth_successful": "\u00c5terautentisering lyckades", "reauth_unsuccessful": "\u00c5terautentiseringen misslyckades. Ta bort integrationen och konfigurera den igen." }, "error": { "cannot_connect": "Det gick inte att ansluta ", + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, "step": { diff --git a/homeassistant/components/nest/translations/he.json b/homeassistant/components/nest/translations/he.json index 743b80f69a1..7c41202e85e 100644 --- a/homeassistant/components/nest/translations/he.json +++ b/homeassistant/components/nest/translations/he.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "authorize_url_timeout": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05db\u05ea\u05d5\u05d1\u05ea URL \u05dc\u05d0\u05d9\u05e9\u05d5\u05e8.", "invalid_access_token": "\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d2\u05d9\u05e9\u05d4 \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", diff --git a/homeassistant/components/nest/translations/sv.json b/homeassistant/components/nest/translations/sv.json index 5cc9d3d68c0..1e2082c4a32 100644 --- a/homeassistant/components/nest/translations/sv.json +++ b/homeassistant/components/nest/translations/sv.json @@ -6,14 +6,20 @@ "abort": { "already_configured": "Konto har redan konfigurerats", "authorize_url_timeout": "Timeout vid generering av en autentisieringsadress.", - "reauth_successful": "\u00c5terautentisering lyckades" + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "reauth_successful": "\u00c5terautentisering lyckades", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, "error": { "internal_error": "Internt fel vid validering av kod", + "invalid_pin": "Ogiltig Pin-kod", "timeout": "Timeout vid valididering av kod", "unknown": "Ok\u00e4nt fel vid validering av kod" }, "step": { + "auth": { + "title": "L\u00e4nka Google-konto" + }, "auth_upgrade": { "description": "App Auth har fasats ut av Google f\u00f6r att f\u00f6rb\u00e4ttra s\u00e4kerheten, och du m\u00e5ste vidta \u00e5tg\u00e4rder genom att skapa nya applikationsuppgifter. \n\n \u00d6ppna [dokumentationen]( {more_info_url} ) f\u00f6r att f\u00f6lja med eftersom n\u00e4sta steg guidar dig genom stegen du beh\u00f6ver ta f\u00f6r att \u00e5terst\u00e4lla \u00e5tkomsten till dina Nest-enheter.", "title": "Nest: Utfasning av appautentisering" diff --git a/homeassistant/components/netatmo/translations/sv.json b/homeassistant/components/netatmo/translations/sv.json index 2fe3a86b0c8..cd91ea65c81 100644 --- a/homeassistant/components/netatmo/translations/sv.json +++ b/homeassistant/components/netatmo/translations/sv.json @@ -4,6 +4,7 @@ "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", + "reauth_successful": "\u00c5terautentisering lyckades", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, "create_entry": { @@ -12,6 +13,10 @@ "step": { "pick_implementation": { "title": "V\u00e4lj autentiseringsmetod" + }, + "reauth_confirm": { + "description": "Netatmo-integrationen m\u00e5ste autentisera ditt konto igen", + "title": "\u00c5terautenticera integration" } } }, diff --git a/homeassistant/components/nexia/translations/sv.json b/homeassistant/components/nexia/translations/sv.json index b00dc6e93b2..554d73169c6 100644 --- a/homeassistant/components/nexia/translations/sv.json +++ b/homeassistant/components/nexia/translations/sv.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "brand": "Varum\u00e4rke", "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } diff --git a/homeassistant/components/nextdns/translations/he.json b/homeassistant/components/nextdns/translations/he.json new file mode 100644 index 00000000000..f4563189497 --- /dev/null +++ b/homeassistant/components/nextdns/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_api_key": "\u05de\u05e4\u05ea\u05d7 API \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "profiles": { + "data": { + "profile": "\u05e4\u05e8\u05d5\u05e4\u05d9\u05dc" + } + }, + "user": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nfandroidtv/translations/sv.json b/homeassistant/components/nfandroidtv/translations/sv.json index c832c7a8880..f545dbda351 100644 --- a/homeassistant/components/nfandroidtv/translations/sv.json +++ b/homeassistant/components/nfandroidtv/translations/sv.json @@ -12,7 +12,8 @@ "data": { "host": "V\u00e4rd", "name": "Namn" - } + }, + "description": "Se dokumentationen f\u00f6r att s\u00e4kerst\u00e4lla att alla krav \u00e4r uppfyllda." } } } diff --git a/homeassistant/components/nightscout/translations/sv.json b/homeassistant/components/nightscout/translations/sv.json index d51243a77f1..ec8da0ee70f 100644 --- a/homeassistant/components/nightscout/translations/sv.json +++ b/homeassistant/components/nightscout/translations/sv.json @@ -5,13 +5,17 @@ }, "error": { "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "api_key": "API-nyckel", "url": "URL" - } + }, + "description": "- URL: adressen till din nightscout-instans. Det vill s\u00e4ga: https://myhomeassistant.duckdns.org:5423\n- API-nyckel (valfritt): Anv\u00e4nd endast om din instans \u00e4r skyddad (auth_default_roles != readable).", + "title": "Ange din Nightscout-serverinformation." } } } diff --git a/homeassistant/components/nina/translations/he.json b/homeassistant/components/nina/translations/he.json index 22681442909..d1c0f783903 100644 --- a/homeassistant/components/nina/translations/he.json +++ b/homeassistant/components/nina/translations/he.json @@ -7,5 +7,11 @@ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" } + }, + "options": { + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + } } } \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/sl.json b/homeassistant/components/nuheat/translations/sl.json index e64f7d1d381..ac3d3716357 100644 --- a/homeassistant/components/nuheat/translations/sl.json +++ b/homeassistant/components/nuheat/translations/sl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Termostat je \u017ee konfiguriran" + "already_configured": "Naprava je \u017ee konfigurirana" }, "error": { "cannot_connect": "Povezava ni uspela, poskusite znova", diff --git a/homeassistant/components/octoprint/translations/sv.json b/homeassistant/components/octoprint/translations/sv.json index 08b58e15cc6..af10a29c982 100644 --- a/homeassistant/components/octoprint/translations/sv.json +++ b/homeassistant/components/octoprint/translations/sv.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "auth_failed": "Det gick inte att h\u00e4mta applikationens API-nyckel", + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/omnilogic/translations/sv.json b/homeassistant/components/omnilogic/translations/sv.json index 70e9ad8a483..407893a7018 100644 --- a/homeassistant/components/omnilogic/translations/sv.json +++ b/homeassistant/components/omnilogic/translations/sv.json @@ -1,8 +1,12 @@ { "config": { + "error": { + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } @@ -12,7 +16,8 @@ "step": { "init": { "data": { - "ph_offset": "pH-f\u00f6rskjutning (positiv eller negativ)" + "ph_offset": "pH-f\u00f6rskjutning (positiv eller negativ)", + "polling_interval": "Pollingintervall (i sekunder)" } } } diff --git a/homeassistant/components/opentherm_gw/translations/he.json b/homeassistant/components/opentherm_gw/translations/he.json index eddeffa2ed0..f1089662414 100644 --- a/homeassistant/components/opentherm_gw/translations/he.json +++ b/homeassistant/components/opentherm_gw/translations/he.json @@ -3,7 +3,8 @@ "error": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "id_exists": "\u05de\u05d6\u05d4\u05d4 \u05d4\u05e9\u05e2\u05e8 \u05db\u05d1\u05e8 \u05e7\u05d9\u05d9\u05dd" + "id_exists": "\u05de\u05d6\u05d4\u05d4 \u05d4\u05e9\u05e2\u05e8 \u05db\u05d1\u05e8 \u05e7\u05d9\u05d9\u05dd", + "timeout_connect": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8" }, "step": { "init": { diff --git a/homeassistant/components/openweathermap/translations/sv.json b/homeassistant/components/openweathermap/translations/sv.json index 64212920aa7..a0e156ae448 100644 --- a/homeassistant/components/openweathermap/translations/sv.json +++ b/homeassistant/components/openweathermap/translations/sv.json @@ -4,7 +4,8 @@ "already_configured": "OpenWeatherMap-integrationen f\u00f6r dessa koordinater \u00e4r redan konfigurerad." }, "error": { - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "invalid_api_key": "Ogiltig API-nyckel" }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/he.json b/homeassistant/components/owntracks/translations/he.json index 10bd4cb9a41..82dddbc7034 100644 --- a/homeassistant/components/owntracks/translations/he.json +++ b/homeassistant/components/owntracks/translations/he.json @@ -3,6 +3,15 @@ "abort": { "cloud_not_connected": "\u05dc\u05d0 \u05de\u05d7\u05d5\u05d1\u05e8 \u05dc\u05e2\u05e0\u05df Home Assistant.", "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "create_entry": { + "default": "\n\u05d1\u05d0\u05e0\u05d3\u05e8\u05d5\u05d0\u05d9\u05d3, \u05d9\u05e9 \u05dc\u05e4\u05ea\u05d5\u05d7 \u05d0\u05ea [\u05d9\u05d9\u05e9\u05d5\u05dd OwnTracks]({android_url}), \u05dc\u05e2\u05d1\u05d5\u05e8 \u05d0\u05dc \u05d4\u05e2\u05d3\u05e4\u05d5\u05ea -> \u05d7\u05d9\u05d1\u05d5\u05e8. \u05d5\u05dc\u05e9\u05e0\u05d5\u05ea \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea:\n - \u05de\u05e6\u05d1: HTTP \u05e4\u05e8\u05d8\u05d9\n - \u05de\u05d0\u05e8\u05d7: {webhook_url}\n - \u05d6\u05d9\u05d4\u05d5\u05d9:\n - \u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9: `'<\u05d4\u05e9\u05dd \u05e9\u05dc\u05da>'`\n - \u05de\u05d6\u05d4\u05d4 \u05d4\u05ea\u05e7\u05df: `'<\u05e9\u05dd \u05d4\u05d4\u05ea\u05e7\u05df \u05e9\u05dc\u05da>'`\n\n\u05d1\u05de\u05d5\u05e6\u05e8\u05d9 \u05d0\u05e4\u05dc, \u05d9\u05e9 \u05dc\u05e4\u05ea\u05d5\u05d7 \u05d0\u05ea [\u05d9\u05d9\u05e9\u05d5\u05dd OwnTracks]({ios_url}), \u05dc\u05d4\u05e7\u05d9\u05e9 \u05e2\u05dc \u05d4\u05e1\u05de\u05dc (i) \u05d1\u05e4\u05d9\u05e0\u05d4 \u05d4\u05d9\u05de\u05e0\u05d9\u05ea \u05d4\u05e2\u05dc\u05d9\u05d5\u05e0\u05d4 -> \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea. \u05d5\u05dc\u05e9\u05e0\u05d5\u05ea \u05d0\u05ea \u05d4\u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea:\n - \u05de\u05e6\u05d1: HTTP\n - \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8: {webhook_url}\n - \u05d4\u05e4\u05e2\u05dc\u05ea \u05d0\u05d9\u05de\u05d5\u05ea\n - \u05de\u05d6\u05d4\u05d4 \u05de\u05e9\u05ea\u05de\u05e9: `'<\u05d4\u05e9\u05dd \u05e9\u05dc\u05da>'`\n\n{secret}\n\n\u05e0\u05d9\u05ea\u05df \u05dc\u05e7\u05e8\u05d5\u05d0 \u05d0\u05ea [\u05d4\u05ea\u05d9\u05e2\u05d5\u05d3]({docs_url}) \u05dc\u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e0\u05d5\u05e1\u05e3." + }, + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05d5\u05d5\u05d3\u05d0\u05d5\u05ea \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea OwnTracks?", + "title": "\u05d4\u05d2\u05d3\u05e8\u05ea OwnTracks" + } } } } \ No newline at end of file diff --git a/homeassistant/components/p1_monitor/translations/sv.json b/homeassistant/components/p1_monitor/translations/sv.json new file mode 100644 index 00000000000..2dcba36c76a --- /dev/null +++ b/homeassistant/components/p1_monitor/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn" + }, + "description": "Konfigurera P1 Monitor f\u00f6r att integrera med Home Assistant." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/sv.json b/homeassistant/components/panasonic_viera/translations/sv.json index 326c8c57dc5..a35794646b7 100644 --- a/homeassistant/components/panasonic_viera/translations/sv.json +++ b/homeassistant/components/panasonic_viera/translations/sv.json @@ -11,13 +11,17 @@ "pairing": { "data": { "pin": "Pin-kod" - } + }, + "description": "Ange Pin-kod som visas p\u00e5 din TV", + "title": "Ihopkoppling" }, "user": { "data": { "host": "IP-adress", "name": "Namn" - } + }, + "description": "Ange din Panasonic Viera TV:s IP-adress", + "title": "St\u00e4ll in din TV" } } } diff --git a/homeassistant/components/philips_js/translations/sv.json b/homeassistant/components/philips_js/translations/sv.json index 47b456c1ff7..290b8a9d82b 100644 --- a/homeassistant/components/philips_js/translations/sv.json +++ b/homeassistant/components/philips_js/translations/sv.json @@ -13,6 +13,7 @@ "data": { "pin": "PIN-kod" }, + "description": "Ange PIN-koden som visas p\u00e5 din TV", "title": "Para ihop" }, "user": { diff --git a/homeassistant/components/pi_hole/translations/sv.json b/homeassistant/components/pi_hole/translations/sv.json index a1a0a54f9af..589fe66fa9b 100644 --- a/homeassistant/components/pi_hole/translations/sv.json +++ b/homeassistant/components/pi_hole/translations/sv.json @@ -20,6 +20,7 @@ "name": "Namn", "port": "Port", "ssl": "Anv\u00e4nd ett SSL certifikat", + "statistics_only": "Endast statistik", "verify_ssl": "Verifiera SSL-certifikat" } } diff --git a/homeassistant/components/picnic/translations/sv.json b/homeassistant/components/picnic/translations/sv.json index 60959ec71fb..bff94714036 100644 --- a/homeassistant/components/picnic/translations/sv.json +++ b/homeassistant/components/picnic/translations/sv.json @@ -5,11 +5,14 @@ }, "error": { "cannot_connect": "Det gick inte att ansluta.", - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "country_code": "Landskod", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/plaato/translations/sl.json b/homeassistant/components/plaato/translations/sl.json index af6dd165225..2a8f1836def 100644 --- a/homeassistant/components/plaato/translations/sl.json +++ b/homeassistant/components/plaato/translations/sl.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Ali ste prepri\u010dani, da \u017eelite nastaviti Plaato Webhook?", + "description": "Ali \u017eelite za\u010deti z nastavitvijo?", "title": "Nastavite Plaato Webhook" } } diff --git a/homeassistant/components/plaato/translations/sv.json b/homeassistant/components/plaato/translations/sv.json index 6368ff7222e..fe6b9055d06 100644 --- a/homeassistant/components/plaato/translations/sv.json +++ b/homeassistant/components/plaato/translations/sv.json @@ -14,9 +14,40 @@ "no_auth_token": "Du m\u00e5ste l\u00e4gga till en autentiseringstoken" }, "step": { + "api_method": { + "data": { + "token": "Klistra in Auth Token h\u00e4r", + "use_webhook": "Anv\u00e4nd webhook" + }, + "description": "F\u00f6r att kunna fr\u00e5ga API:et kr\u00e4vs en `auth_token` som kan erh\u00e5llas genom att f\u00f6lja [dessa](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instruktioner \n\n Vald enhet: ** {device_type} ** \n\n Om du hellre anv\u00e4nder den inbyggda webhook-metoden (endast Airlock) markera rutan nedan och l\u00e4mna Auth Token tom", + "title": "V\u00e4lj API-metod" + }, "user": { + "data": { + "device_name": "Namnge din enhet", + "device_type": "Typ av Plaato-enhet" + }, "description": "\u00c4r du s\u00e4ker p\u00e5 att du vill konfigurera Plaato Webhook?", "title": "Konfigurera Plaato Webhook" + }, + "webhook": { + "description": "F\u00f6r att skicka h\u00e4ndelser till Home Assistant m\u00e5ste du konfigurera webhook funktionen i Plaato Airlock.\n\n Fyll i f\u00f6ljande information:\n \n- URL: `{webhook_url}`\n- Method: POST\n\nSe [dokumentation]({docs_url}) f\u00f6r mer information.", + "title": "Webhook att anv\u00e4nda" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "Uppdateringsintervall (minuter)" + }, + "description": "St\u00e4ll in uppdateringsintervallet (minuter)", + "title": "Alternativ f\u00f6r Plaato" + }, + "webhook": { + "description": "Webhook info: \n\n - URL: ` {webhook_url} `\n - Metod: POST \n\n", + "title": "Alternativ f\u00f6r Plaato Airlock" } } } diff --git a/homeassistant/components/plex/translations/sv.json b/homeassistant/components/plex/translations/sv.json index bd5b5570ac0..c79fdd881fc 100644 --- a/homeassistant/components/plex/translations/sv.json +++ b/homeassistant/components/plex/translations/sv.json @@ -4,6 +4,7 @@ "all_configured": "Alla l\u00e4nkade servrar har redan konfigurerats", "already_configured": "Denna Plex-server \u00e4r redan konfigurerad", "already_in_progress": "Plex konfigureras", + "reauth_successful": "\u00c5terautentisering lyckades", "token_request_timeout": "Timeout att erh\u00e5lla token", "unknown": "Misslyckades av ok\u00e4nd anledning" }, @@ -22,7 +23,8 @@ "ssl": "Anv\u00e4nd ett SSL certifikat", "token": "Token (valfritt)", "verify_ssl": "Verifiera SSL-certifikat" - } + }, + "title": "Manuell Plex-konfiguration" }, "select_server": { "data": { @@ -31,6 +33,9 @@ "description": "V\u00e4lj flera servrar tillg\u00e4ngliga, v\u00e4lj en:", "title": "V\u00e4lj Plex-server" }, + "user": { + "description": "Forts\u00e4tt till [plex.tv](https://plex.tv) f\u00f6r att l\u00e4nka en Plex-server." + }, "user_advanced": { "data": { "setup_method": "Inst\u00e4llningsmetod" diff --git a/homeassistant/components/plugwise/translations/he.json b/homeassistant/components/plugwise/translations/he.json index db3eeef2d53..f8a4c722a8f 100644 --- a/homeassistant/components/plugwise/translations/he.json +++ b/homeassistant/components/plugwise/translations/he.json @@ -11,6 +11,10 @@ "flow_title": "{name}", "step": { "user": { + "data": { + "host": "\u05db\u05ea\u05d5\u05d1\u05ea IP", + "port": "\u05e4\u05ea\u05d7\u05d4" + }, "description": "\u05de\u05d5\u05e6\u05e8:" }, "user_gateway": { diff --git a/homeassistant/components/powerwall/translations/sv.json b/homeassistant/components/powerwall/translations/sv.json index fd0d0eb86c8..bc4dcc606ff 100644 --- a/homeassistant/components/powerwall/translations/sv.json +++ b/homeassistant/components/powerwall/translations/sv.json @@ -10,6 +10,7 @@ "unknown": "Ov\u00e4ntat fel", "wrong_version": "Powerwall anv\u00e4nder en programvaruversion som inte st\u00f6ds. T\u00e4nk p\u00e5 att uppgradera eller rapportera det h\u00e4r problemet s\u00e5 att det kan l\u00f6sas." }, + "flow_title": "{name} ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/progettihwsw/translations/sv.json b/homeassistant/components/progettihwsw/translations/sv.json index c7c31766d08..2c4573174a5 100644 --- a/homeassistant/components/progettihwsw/translations/sv.json +++ b/homeassistant/components/progettihwsw/translations/sv.json @@ -14,8 +14,27 @@ "relay_10": "Rel\u00e4 10", "relay_11": "Rel\u00e4 11", "relay_12": "Rel\u00e4 12", - "relay_13": "Rel\u00e4 13" - } + "relay_13": "Rel\u00e4 13", + "relay_14": "Rel\u00e4 14", + "relay_15": "Rel\u00e4 15", + "relay_16": "Rel\u00e4 16", + "relay_2": "Rel\u00e4 2", + "relay_3": "Rel\u00e4 3", + "relay_4": "Rel\u00e4 4", + "relay_5": "Rel\u00e4 5", + "relay_6": "Rel\u00e4 6", + "relay_7": "Rel\u00e4 7", + "relay_8": "Rel\u00e4 8", + "relay_9": "Rel\u00e4 9" + }, + "title": "St\u00e4ll in rel\u00e4er" + }, + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + }, + "title": "St\u00e4ll in kort" } } } diff --git a/homeassistant/components/ps4/translations/sv.json b/homeassistant/components/ps4/translations/sv.json index 7435865c5d0..26af627749d 100644 --- a/homeassistant/components/ps4/translations/sv.json +++ b/homeassistant/components/ps4/translations/sv.json @@ -1,12 +1,14 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "credential_error": "Fel n\u00e4r f\u00f6rs\u00f6ker h\u00e4mta autentiseringsuppgifter.", "no_devices_found": "Inga PlayStation 4 enheter hittades p\u00e5 n\u00e4tverket.", "port_987_bind_error": "Kunde inte binda till port 987.", "port_997_bind_error": "Kunde inte binda till port 997." }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "credential_timeout": "Autentiseringstj\u00e4nsten orsakade timeout. Tryck p\u00e5 Skicka f\u00f6r att starta om.", "login_failed": "Misslyckades med att para till PlayStation 4. Verifiera PIN-koden \u00e4r korrekt.", "no_ipaddress": "Ange IP-adressen f\u00f6r PlayStation 4 du vill konfigurera." diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/sv.json b/homeassistant/components/pvpc_hourly_pricing/translations/sv.json index e45b374e5da..fe05a9cbfd5 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/sv.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/sv.json @@ -7,6 +7,19 @@ "user": { "data": { "name": "Sensornamn", + "power": "Kontrakterad effekt (kW)", + "power_p3": "Kontrakterad effekt f\u00f6r dalperiod P3 (kW)", + "tariff": "Till\u00e4mplig taxa per geografisk zon" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "power": "Kontrakterad effekt (kW)", + "power_p3": "Kontrakterad effekt f\u00f6r dalperiod P3 (kW)", "tariff": "Till\u00e4mplig taxa per geografisk zon" } } diff --git a/homeassistant/components/qnap_qsw/translations/he.json b/homeassistant/components/qnap_qsw/translations/he.json index fbe984e0b32..5d4ffba2b2b 100644 --- a/homeassistant/components/qnap_qsw/translations/he.json +++ b/homeassistant/components/qnap_qsw/translations/he.json @@ -8,6 +8,12 @@ "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { + "discovered_connection": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", diff --git a/homeassistant/components/rachio/translations/sl.json b/homeassistant/components/rachio/translations/sl.json index a7febddee05..20f523d8d5a 100644 --- a/homeassistant/components/rachio/translations/sl.json +++ b/homeassistant/components/rachio/translations/sl.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "api_key": "Klju\u010d API za ra\u010dun Rachio." + "api_key": "API Klju\u010d" }, "description": "Potrebovali boste API klju\u010d iz https://app.rach.io/. Izberite ' nastavitve ra\u010duna in kliknite 'get API KEY'.", "title": "Pove\u017eite se z napravo Rachio" diff --git a/homeassistant/components/radiotherm/translations/he.json b/homeassistant/components/radiotherm/translations/he.json index 77232a68dd2..bdf76e7c217 100644 --- a/homeassistant/components/radiotherm/translations/he.json +++ b/homeassistant/components/radiotherm/translations/he.json @@ -3,6 +3,17 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, - "flow_title": "{name} {model} ({host})" + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "flow_title": "{name} {model} ({host})", + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/rainforest_eagle/translations/sv.json b/homeassistant/components/rainforest_eagle/translations/sv.json index eba844f6c03..80a0ca5600a 100644 --- a/homeassistant/components/rainforest_eagle/translations/sv.json +++ b/homeassistant/components/rainforest_eagle/translations/sv.json @@ -1,9 +1,19 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { - "host": "V\u00e4rd" + "cloud_id": "Moln-ID", + "host": "V\u00e4rd", + "install_code": "Installationskod" } } } diff --git a/homeassistant/components/rdw/translations/sv.json b/homeassistant/components/rdw/translations/sv.json new file mode 100644 index 00000000000..cdb6aeb44be --- /dev/null +++ b/homeassistant/components/rdw/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown_license_plate": "Ok\u00e4nd registreringsskylt" + }, + "step": { + "user": { + "data": { + "license_plate": "Registreringsskylt" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/sv.json b/homeassistant/components/rfxtrx/translations/sv.json index 304513880e6..be9e6e944a7 100644 --- a/homeassistant/components/rfxtrx/translations/sv.json +++ b/homeassistant/components/rfxtrx/translations/sv.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + "already_configured": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "cannot_connect": "Det gick inte att ansluta." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." }, "step": { "setup_network": { diff --git a/homeassistant/components/rhasspy/translations/he.json b/homeassistant/components/rhasspy/translations/he.json new file mode 100644 index 00000000000..d0c3523da94 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/he.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/sv.json b/homeassistant/components/risco/translations/sv.json index 1f7f95730d1..662583561d0 100644 --- a/homeassistant/components/risco/translations/sv.json +++ b/homeassistant/components/risco/translations/sv.json @@ -1,8 +1,18 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", + "pin": "Pin-kod", "username": "Anv\u00e4ndarnamn" } } @@ -10,8 +20,12 @@ }, "options": { "step": { + "ha_to_risco": { + "title": "Mappa Home Assistant tillst\u00e5nd till Risco tillst\u00e5nd" + }, "risco_to_ha": { "data": { + "A": "Grupp A", "B": "Grupp B", "C": "Grupp C", "D": "Grupp D", diff --git a/homeassistant/components/roku/translations/sl.json b/homeassistant/components/roku/translations/sl.json index bb39edf9753..6960298d192 100644 --- a/homeassistant/components/roku/translations/sl.json +++ b/homeassistant/components/roku/translations/sl.json @@ -5,7 +5,7 @@ "unknown": "Nepri\u010dakovana napaka" }, "error": { - "cannot_connect": "Povezava ni uspela, poskusite znova" + "cannot_connect": "Povezava ni uspela" }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/roomba/translations/sv.json b/homeassistant/components/roomba/translations/sv.json index 1c731cd2af9..349721d149a 100644 --- a/homeassistant/components/roomba/translations/sv.json +++ b/homeassistant/components/roomba/translations/sv.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen" }, + "flow_title": "{name} ({host})", "step": { "link": { "description": "Tryck och h\u00e5ll hemknappen p\u00e5 {name} tills enheten genererar ett ljud (cirka tv\u00e5 sekunder), skicka sedan inom 30 sekunder.", @@ -18,6 +19,7 @@ "data": { "password": "L\u00f6senord" }, + "description": "L\u00f6senordet kunde inte h\u00e4mtas fr\u00e5n enheten automatiskt. F\u00f6lj stegen som beskrivs i dokumentationen p\u00e5: {auth_help_url}", "title": "Ange l\u00f6senord" }, "manual": { diff --git a/homeassistant/components/ruckus_unleashed/translations/sv.json b/homeassistant/components/ruckus_unleashed/translations/sv.json index a265d988aaa..f85a02855d6 100644 --- a/homeassistant/components/ruckus_unleashed/translations/sv.json +++ b/homeassistant/components/ruckus_unleashed/translations/sv.json @@ -1,9 +1,18 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { "host": "V\u00e4rd", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/samsungtv/translations/sv.json b/homeassistant/components/samsungtv/translations/sv.json index feff1c2fc86..67c8373d0e3 100644 --- a/homeassistant/components/samsungtv/translations/sv.json +++ b/homeassistant/components/samsungtv/translations/sv.json @@ -6,8 +6,12 @@ "auth_missing": "Home Assistant har inte beh\u00f6righet att ansluta till denna Samsung TV. Kontrollera tv:ns inst\u00e4llningar f\u00f6r att godk\u00e4nna Home Assistant.", "id_missing": "Denna Samsung-enhet har inget serienummer.", "not_supported": "Denna Samsung enhet st\u00f6ds f\u00f6r n\u00e4rvarande inte.", + "reauth_successful": "\u00c5terautentisering lyckades", "unknown": "Ov\u00e4ntat fel" }, + "error": { + "auth_missing": "Home Assistant har inte beh\u00f6righet att ansluta till denna Samsung TV. Kontrollera tv:ns inst\u00e4llningar f\u00f6r att godk\u00e4nna Home Assistant." + }, "flow_title": "{device}", "step": { "confirm": { @@ -16,6 +20,9 @@ "pairing": { "description": "Vill du st\u00e4lla in Samsung TV {device}? Om du aldrig har anslutit Home Assistant innan du ska se ett popup-f\u00f6nster p\u00e5 tv:n och be om auktorisering. Manuella konfigurationer f\u00f6r den h\u00e4r TV:n skrivs \u00f6ver." }, + "reauth_confirm": { + "description": "N\u00e4r du har skickat, acceptera popup-f\u00f6nstret p\u00e5 {enheten} som beg\u00e4r auktorisering inom 30 sekunder eller ange PIN-koden." + }, "user": { "data": { "host": "V\u00e4rdnamn eller IP-adress", diff --git a/homeassistant/components/scrape/translations/he.json b/homeassistant/components/scrape/translations/he.json index 463ce9035f4..6dd1e6845de 100644 --- a/homeassistant/components/scrape/translations/he.json +++ b/homeassistant/components/scrape/translations/he.json @@ -1,10 +1,33 @@ { "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, "step": { "user": { "data": { "name": "\u05e9\u05dd", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + }, + "data_description": { + "state_class": "\u05d4-state_class \u05e9\u05dc \u05d4\u05d7\u05d9\u05d9\u05e9\u05df" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "\u05e9\u05dd", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9", + "verify_ssl": "\u05d0\u05d9\u05de\u05d5\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8 SSL" + }, + "data_description": { + "state_class": "\u05d4-state_class \u05e9\u05dc \u05d4\u05d7\u05d9\u05d9\u05e9\u05df" } } } diff --git a/homeassistant/components/screenlogic/translations/sv.json b/homeassistant/components/screenlogic/translations/sv.json index 54ae7f2b212..251a00731d3 100644 --- a/homeassistant/components/screenlogic/translations/sv.json +++ b/homeassistant/components/screenlogic/translations/sv.json @@ -6,17 +6,32 @@ "error": { "cannot_connect": "Kunde inte ansluta" }, + "flow_title": "{name}", "step": { "gateway_entry": { "data": { + "ip_address": "IP-adress", "port": "Port" - } + }, + "description": "Ange din ScreenLogic Gateway-information.", + "title": "ScreenLogic" + }, + "gateway_select": { + "data": { + "selected_gateway": "Gateway" + }, + "description": "F\u00f6ljande ScreenLogic-gateways uppt\u00e4cktes. V\u00e4lj en att konfigurera, eller v\u00e4lj att manuellt konfigurera en ScreenLogic-gateway.", + "title": "ScreenLogic" } } }, "options": { "step": { "init": { + "data": { + "scan_interval": "Sekunder mellan skanningar" + }, + "description": "Ange inst\u00e4llningar f\u00f6r {gateway_name}", "title": "ScreenLogic" } } diff --git a/homeassistant/components/select/translations/sv.json b/homeassistant/components/select/translations/sv.json index d388cb6c622..6c5c5126002 100644 --- a/homeassistant/components/select/translations/sv.json +++ b/homeassistant/components/select/translations/sv.json @@ -1,7 +1,14 @@ { "device_automation": { + "action_type": { + "select_option": "\u00c4ndra alternativet {entity_name}" + }, "condition_type": { "selected_option": "Nuvarande {entity_name} markerad option" + }, + "trigger_type": { + "current_option_changed": "Alternativet {entity_name} har \u00e4ndrats" } - } + }, + "title": "V\u00e4lj" } \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/he.json b/homeassistant/components/sensor/translations/he.json index 7f2dd33a023..fc0ba9b48c4 100644 --- a/homeassistant/components/sensor/translations/he.json +++ b/homeassistant/components/sensor/translations/he.json @@ -3,12 +3,35 @@ "condition_type": { "is_apparent_power": "\u05d4\u05e2\u05d5\u05e6\u05de\u05d4 \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name} \u05de\u05e1\u05ea\u05de\u05e0\u05ea", "is_battery_level": "\u05e8\u05de\u05ea \u05d4\u05e1\u05d5\u05dc\u05dc\u05d4 \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea \u05e9\u05dc {entity_name}", - "is_reactive_power": "\u05d4\u05e1\u05e4\u05e7 \u05ea\u05d2\u05d5\u05d1\u05ea\u05d9 \u05e0\u05d5\u05db\u05d7\u05d9 {entity_name}" + "is_current": "\u05db\u05e2\u05ea {entity_name}", + "is_energy": "\u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name}", + "is_gas": "\u05db\u05e2\u05ea {entity_name} \u05d2\u05d6", + "is_illuminance": "\u05e2\u05d5\u05e6\u05de\u05ea \u05d4\u05d0\u05e8\u05d4 {entity_name} \u05e0\u05d5\u05db\u05d7\u05d9\u05ea", + "is_pm1": "\u05e8\u05de\u05ea \u05d4\u05e8\u05d9\u05db\u05d5\u05d6 \u05d4\u05e0\u05d5\u05db\u05d7\u05d9\u05ea {entity_name} PM1", + "is_reactive_power": "\u05d4\u05e1\u05e4\u05e7 \u05ea\u05d2\u05d5\u05d1\u05ea\u05d9 \u05e0\u05d5\u05db\u05d7\u05d9 {entity_name}", + "is_temperature": "\u05db\u05e2\u05ea {entity_name} \u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4" }, "trigger_type": { "apparent_power": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d4\u05e1\u05e4\u05e7 \u05dc\u05db\u05d0\u05d5\u05e8\u05d4", "battery_level": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05de\u05ea \u05d4\u05e1\u05d5\u05dc\u05dc\u05d4", - "reactive_power": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d4\u05e1\u05e4\u05e7 \u05ea\u05d2\u05d5\u05d1\u05ea\u05d9" + "current": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05e0\u05d5\u05db\u05d7\u05d9\u05d9\u05dd", + "energy": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d0\u05e0\u05e8\u05d2\u05d9\u05d4", + "frequency": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05ea\u05d3\u05e8\u05d9\u05dd", + "gas": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d2\u05d6", + "humidity": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05dc\u05d7\u05d5\u05ea", + "illuminance": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05e2\u05d5\u05e6\u05de\u05ea \u05d4\u05d0\u05e8\u05d4", + "nitrogen_dioxide": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 \u05d4\u05d7\u05e0\u05e7\u05df \u05d4\u05d3\u05d5-\u05d7\u05de\u05e6\u05e0\u05d9", + "ozone": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 \u05d4\u05d0\u05d5\u05d6\u05d5\u05df", + "pm1": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 PM1", + "pm10": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 PM10", + "pm25": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 PM2.5", + "power": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d4\u05e1\u05e4\u05e7", + "power_factor": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05d2\u05d5\u05e8\u05dd \u05d4\u05d4\u05e1\u05e4\u05e7", + "pressure": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05dc\u05d7\u05e5", + "reactive_power": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d4\u05e1\u05e4\u05e7 \u05ea\u05d2\u05d5\u05d1\u05ea\u05d9", + "temperature": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d8\u05de\u05e4\u05e8\u05d8\u05d5\u05e8\u05d4", + "value": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05e2\u05e8\u05da", + "voltage": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05de\u05ea\u05d7" } }, "state": { diff --git a/homeassistant/components/sensor/translations/sv.json b/homeassistant/components/sensor/translations/sv.json index 0fae4fe1047..24b137a420b 100644 --- a/homeassistant/components/sensor/translations/sv.json +++ b/homeassistant/components/sensor/translations/sv.json @@ -33,15 +33,21 @@ "carbon_dioxide": "{entity_name} f\u00f6r\u00e4ndringar av koldioxidkoncentrationen", "carbon_monoxide": "{entity_name} f\u00f6r\u00e4ndringar av kolmonoxidkoncentrationen", "energy": "Energif\u00f6r\u00e4ndringar", + "frequency": "{entity_name} frekvens\u00e4ndringar", "gas": "{entity_name} gasf\u00f6r\u00e4ndringar", "humidity": "{entity_name} fuktighet \u00e4ndras", "illuminance": "{entity_name} belysning \u00e4ndras", "nitrogen_dioxide": "{entity_name} kv\u00e4vedioxidkoncentrationen f\u00f6r\u00e4ndras.", + "ozone": "{entity_name} ozonkoncentrationen f\u00f6r\u00e4ndras", + "pm1": "{entity_name} PM1-koncentrationsf\u00f6r\u00e4ndringar", + "pm10": "{entity_name} PM10-koncentrations\u00e4ndringar", + "pm25": "{entity_name} PM2.5-koncentrationsf\u00f6r\u00e4ndringar", "power": "{entity_name} effektf\u00f6r\u00e4ndringar", "power_factor": "effektfaktorf\u00f6r\u00e4ndringar", "pressure": "{entity_name} tryckf\u00f6r\u00e4ndringar", "reactive_power": "{entity_name} reaktiv effekt\u00e4ndring", "signal_strength": "{entity_name} signalstyrka \u00e4ndras", + "sulphur_dioxide": "{entity_name} f\u00f6r\u00e4ndringar av koncentrationen av svaveldioxid", "temperature": "{entity_name} temperaturf\u00f6r\u00e4ndringar", "value": "{entity_name} v\u00e4rde \u00e4ndras", "volatile_organic_compounds": "{entity_name} koncentrations\u00e4ndringar av flyktiga organiska \u00e4mnen", diff --git a/homeassistant/components/sensorpush/translations/he.json b/homeassistant/components/sensorpush/translations/he.json new file mode 100644 index 00000000000..de780eb221a --- /dev/null +++ b/homeassistant/components/sensorpush/translations/he.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/sv.json b/homeassistant/components/shelly/translations/sv.json index 458f4be3b24..d4f3f6f400e 100644 --- a/homeassistant/components/shelly/translations/sv.json +++ b/homeassistant/components/shelly/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "unsupported_firmware": "Enheten anv\u00e4nder en firmwareversion som inte st\u00f6ds." + }, "error": { "firmware_not_fully_provisioned": "Enheten \u00e4r inte helt etablerad. Kontakta Shellys support", "invalid_auth": "Ogiltig autentisering" @@ -10,7 +13,22 @@ "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } + }, + "user": { + "description": "F\u00f6re installationen m\u00e5ste batteridrivna enheter v\u00e4ckas, du kan nu v\u00e4cka enheten med en knapp p\u00e5 den." } } + }, + "device_automation": { + "trigger_subtype": { + "button4": "Fj\u00e4rde knappen" + }, + "trigger_type": { + "btn_down": "\"{subtype}\" knappen nedtryckt", + "btn_up": "\"{subtype}\" knappen uppsl\u00e4ppt", + "double_push": "{subtype} dubbeltryck", + "long_push": "{subtype} l\u00e5ngtryck", + "single_push": "{subtyp} enkeltryck" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/en.json b/homeassistant/components/simplepush/translations/en.json index 205d3549a52..cb294813b88 100644 --- a/homeassistant/components/simplepush/translations/en.json +++ b/homeassistant/components/simplepush/translations/en.json @@ -19,6 +19,10 @@ } }, "issues": { + "deprecated_yaml": { + "description": "Configuring Simplepush using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Simplepush YAML configuration is being removed" + }, "removed_yaml": { "description": "Configuring Simplepush using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the Simplepush YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", "title": "The Simplepush YAML configuration has been removed" diff --git a/homeassistant/components/simplepush/translations/fr.json b/homeassistant/components/simplepush/translations/fr.json index 49356553426..e92ef0263d0 100644 --- a/homeassistant/components/simplepush/translations/fr.json +++ b/homeassistant/components/simplepush/translations/fr.json @@ -21,6 +21,9 @@ "issues": { "deprecated_yaml": { "title": "La configuration YAML pour Simplepush est en cours de suppression" + }, + "removed_yaml": { + "title": "La configuration YAML pour Simplepush a \u00e9t\u00e9 supprim\u00e9e" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/he.json b/homeassistant/components/simplepush/translations/he.json new file mode 100644 index 00000000000..880c7074257 --- /dev/null +++ b/homeassistant/components/simplepush/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "name": "\u05e9\u05dd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/id.json b/homeassistant/components/simplepush/translations/id.json index 715cb3c893b..de54996d014 100644 --- a/homeassistant/components/simplepush/translations/id.json +++ b/homeassistant/components/simplepush/translations/id.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "Proses konfigurasi Simplepush lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Simplepush dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", "title": "Konfigurasi YAML Simplepush dalam proses penghapusan" + }, + "removed_yaml": { + "description": "Proses konfigurasi Simplepush lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML Simplepush dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Simplepush telah dihapus" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/pt-BR.json b/homeassistant/components/simplepush/translations/pt-BR.json index f0a330ff1d3..90d21e1c44c 100644 --- a/homeassistant/components/simplepush/translations/pt-BR.json +++ b/homeassistant/components/simplepush/translations/pt-BR.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "A configura\u00e7\u00e3o do Simplepush usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o Simplepush YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", "title": "A configura\u00e7\u00e3o Simplepush YAML est\u00e1 sendo removida" + }, + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do Simplepush usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do Simplepush do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o Simplepush YAML foi removida" } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 245bb18351e..baa167ca951 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "This SimpliSafe account is already in use.", + "email_2fa_timed_out": "Timed out while waiting for email-based two-factor authentication.", "reauth_successful": "Re-authentication was successful", "wrong_account": "The user credentials provided do not match this SimpliSafe account." }, @@ -11,10 +12,28 @@ "invalid_auth_code_length": "SimpliSafe authorization codes are 45 characters in length", "unknown": "Unexpected error" }, + "progress": { + "email_2fa": "Check your email for a verification link from Simplisafe." + }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "Please re-enter the password for {username}.", + "title": "Reauthenticate Integration" + }, + "sms_2fa": { + "data": { + "code": "Code" + }, + "description": "Input the two-factor authentication code sent to you via SMS." + }, "user": { "data": { - "auth_code": "Authorization Code" + "auth_code": "Authorization Code", + "password": "Password", + "username": "Username" }, "description": "SimpliSafe authenticates users via its web app. Due to technical limitations, there is a manual step at the end of this process; please ensure that you read the [documentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) before starting.\n\nWhen you are ready, click [here]({url}) to open the SimpliSafe web app and input your credentials. If you've already logged into SimpliSafe in your browser, you may want to open a new tab, then copy/paste the above URL into that tab.\n\nWhen the process is complete, return here and input the authorization code from the `com.simplisafe.mobile` URL." } diff --git a/homeassistant/components/simplisafe/translations/he.json b/homeassistant/components/simplisafe/translations/he.json index 85a9e002b77..70ab1cff6ac 100644 --- a/homeassistant/components/simplisafe/translations/he.json +++ b/homeassistant/components/simplisafe/translations/he.json @@ -12,7 +12,7 @@ "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" }, - "description": "\u05ea\u05d5\u05e7\u05e3 \u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d4\u05d2\u05d9\u05e9\u05d4 \u05e9\u05dc\u05da \u05e4\u05d2 \u05d0\u05d5 \u05d1\u05d5\u05d8\u05dc. \u05d9\u05e9 \u05dc\u05d4\u05d6\u05d9\u05df \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05e7\u05e9\u05e8 \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05e9\u05dc\u05da.", + "description": "\u05e0\u05d0 \u05dc\u05d4\u05d6\u05d9\u05df \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05e2\u05d1\u05d5\u05e8 {username}.", "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" }, "sms_2fa": { @@ -23,7 +23,7 @@ "user": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4", - "username": "\u05d3\u05d5\u05d0\"\u05dc" + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" } } } diff --git a/homeassistant/components/simplisafe/translations/id.json b/homeassistant/components/simplisafe/translations/id.json index e9919dc734b..4d5ddf18ed6 100644 --- a/homeassistant/components/simplisafe/translations/id.json +++ b/homeassistant/components/simplisafe/translations/id.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "Akun sudah terdaftar", "invalid_auth": "Autentikasi tidak valid", + "invalid_auth_code_length": "Panjang kode otorisasi SimpliSafe adalah 45 karakter", "unknown": "Kesalahan yang tidak diharapkan" }, "progress": { diff --git a/homeassistant/components/simplisafe/translations/pt-BR.json b/homeassistant/components/simplisafe/translations/pt-BR.json index 74b30d2a9ef..87e3c6f0303 100644 --- a/homeassistant/components/simplisafe/translations/pt-BR.json +++ b/homeassistant/components/simplisafe/translations/pt-BR.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "Conta j\u00e1 cadastrada", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_auth_code_length": "Os c\u00f3digos de autoriza\u00e7\u00e3o SimpliSafe t\u00eam 45 caracteres", "unknown": "Erro inesperado" }, "progress": { @@ -34,7 +35,7 @@ "password": "Senha", "username": "Usu\u00e1rio" }, - "description": "O SimpliSafe autentica os usu\u00e1rios por meio de seu aplicativo da web. Por limita\u00e7\u00f5es t\u00e9cnicas, existe uma etapa manual ao final deste processo; certifique-se de ler a [documenta\u00e7\u00e3o](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de come\u00e7ar. \n\n Quando estiver pronto, clique [aqui]( {url} ) para abrir o aplicativo Web SimpliSafe e insira suas credenciais. Quando o processo estiver conclu\u00eddo, retorne aqui e insira o c\u00f3digo de autoriza\u00e7\u00e3o da URL do aplicativo Web SimpliSafe." + "description": "O SimpliSafe autentica os usu\u00e1rios por meio de seu aplicativo da web. Por limita\u00e7\u00f5es t\u00e9cnicas, existe uma etapa manual ao final deste processo; certifique-se de ler a [documenta\u00e7\u00e3o](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de come\u00e7ar. \n\n Quando estiver pronto, clique [aqui]( {url} ) para abrir o aplicativo da web SimpliSafe e insira suas credenciais. Se voc\u00ea j\u00e1 fez login no SimpliSafe em seu navegador, voc\u00ea pode querer abrir uma nova guia e copiar/colar o URL acima nessa guia. \n\n Quando o processo estiver conclu\u00eddo, retorne aqui e insira o c\u00f3digo de autoriza\u00e7\u00e3o da URL `com.simplisafe.mobile`." } } }, diff --git a/homeassistant/components/simplisafe/translations/sv.json b/homeassistant/components/simplisafe/translations/sv.json index e7e8ec28715..2e24eb856bd 100644 --- a/homeassistant/components/simplisafe/translations/sv.json +++ b/homeassistant/components/simplisafe/translations/sv.json @@ -32,7 +32,8 @@ "auth_code": "Auktoriseringskod", "password": "L\u00f6senord", "username": "E-postadress" - } + }, + "description": "SimpliSafe autentiserar anv\u00e4ndare via sin webbapp. P\u00e5 grund av tekniska begr\u00e4nsningar finns det ett manuellt steg i slutet av denna process; se till att du l\u00e4ser [dokumentationen](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) innan du b\u00f6rjar. \n\n N\u00e4r du \u00e4r redo klickar du [h\u00e4r]( {url} ) f\u00f6r att \u00f6ppna webbappen SimpliSafe och ange dina referenser. N\u00e4r processen \u00e4r klar, \u00e5terv\u00e4nd hit och mata in auktoriseringskoden fr\u00e5n SimpliSafe-webbappens URL." } } }, diff --git a/homeassistant/components/skybell/translations/he.json b/homeassistant/components/skybell/translations/he.json index 28e8ddd34c9..21b9822e248 100644 --- a/homeassistant/components/skybell/translations/he.json +++ b/homeassistant/components/skybell/translations/he.json @@ -1,12 +1,19 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, "step": { "user": { "data": { - "email": "\u05d3\u05d5\u05d0\"\u05dc" + "email": "\u05d3\u05d5\u05d0\"\u05dc", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" } } } diff --git a/homeassistant/components/smartthings/translations/sv.json b/homeassistant/components/smartthings/translations/sv.json index 21591e7c256..309ef659d56 100644 --- a/homeassistant/components/smartthings/translations/sv.json +++ b/homeassistant/components/smartthings/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "invalid_webhook_url": "Home Assistant \u00e4r inte korrekt konfigurerad f\u00f6r att ta emot uppdateringar fr\u00e5n SmartThings. Webhook-adressen \u00e4r ogiltig:\n > {webhook_url} \n\n Uppdatera din konfiguration enligt [instruktionerna]( {component_url} ), starta om Home Assistant och f\u00f6rs\u00f6k igen.", "no_available_locations": "Det finns inga tillg\u00e4ngliga SmartThings-platser att st\u00e4lla in i Home Assistant." }, "error": { diff --git a/homeassistant/components/smarttub/translations/sv.json b/homeassistant/components/smarttub/translations/sv.json index bfa44298487..9ebde76e717 100644 --- a/homeassistant/components/smarttub/translations/sv.json +++ b/homeassistant/components/smarttub/translations/sv.json @@ -8,6 +8,10 @@ "invalid_auth": "Ogiltig autentisering" }, "step": { + "reauth_confirm": { + "description": "SmartTub-integrationen m\u00e5ste autentisera ditt konto igen", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "email": "E-post", diff --git a/homeassistant/components/solarlog/translations/sl.json b/homeassistant/components/solarlog/translations/sl.json index 8f2682b3fb1..2e90b93f1ca 100644 --- a/homeassistant/components/solarlog/translations/sl.json +++ b/homeassistant/components/solarlog/translations/sl.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "Ime gostitelja ali ip naslov va\u0161e naprave Solar-Log", + "host": "Gostitelj", "name": "Predpona, ki jo \u017eelite uporabiti za senzorje Solar-log" }, "title": "Dolo\u010dite povezavo Solar-Log" diff --git a/homeassistant/components/somfy_mylink/translations/sv.json b/homeassistant/components/somfy_mylink/translations/sv.json index 9ab3fa7ec0c..6cacc18d1a9 100644 --- a/homeassistant/components/somfy_mylink/translations/sv.json +++ b/homeassistant/components/somfy_mylink/translations/sv.json @@ -3,6 +3,11 @@ "flow_title": "{mac} ({ip})", "step": { "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port", + "system_id": "System-ID" + }, "description": "System-ID kan erh\u00e5llas i MyLink-appen under Integration genom att v\u00e4lja vilken tj\u00e4nst som helst som inte kommer fr\u00e5n molnet." } } @@ -13,7 +18,14 @@ }, "step": { "init": { + "data": { + "target_id": "Konfigurera alternativ f\u00f6r ett skydd." + }, "title": "Konfigurera MyLink-alternativ" + }, + "target_config": { + "description": "Konfigurera alternativ f\u00f6r ` {target_name} `", + "title": "Konfigurera MyLink Cover" } } } diff --git a/homeassistant/components/sonarr/translations/sv.json b/homeassistant/components/sonarr/translations/sv.json index 9128ea57a38..c17fa818ed8 100644 --- a/homeassistant/components/sonarr/translations/sv.json +++ b/homeassistant/components/sonarr/translations/sv.json @@ -2,16 +2,33 @@ "config": { "abort": { "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades", "unknown": "Ov\u00e4ntat fel" }, "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering" }, + "flow_title": "{name}", "step": { + "reauth_confirm": { + "description": "Sonarr-integrationen m\u00e5ste autentiseras manuellt med Sonarr API:n som finns p\u00e5: {url}", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { - "api_key": "API nyckel" + "api_key": "API nyckel", + "verify_ssl": "Verifiera SSL-certifikat" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "Antal kommande dagar att visa", + "wanted_max_items": "Max antal \u00f6nskade objekt att visa" } } } diff --git a/homeassistant/components/soundtouch/translations/he.json b/homeassistant/components/soundtouch/translations/he.json new file mode 100644 index 00000000000..25fe66938d7 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/he.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "error": { + "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/translations/he.json b/homeassistant/components/sql/translations/he.json index 9b9bda1ed3f..0e44d72da00 100644 --- a/homeassistant/components/sql/translations/he.json +++ b/homeassistant/components/sql/translations/he.json @@ -3,19 +3,31 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d7\u05e9\u05d1\u05d5\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" }, + "error": { + "db_url_invalid": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05e9\u05dc \u05de\u05e1\u05d3 \u05d4\u05e0\u05ea\u05d5\u05e0\u05d9\u05dd \u05d0\u05d9\u05e0\u05d4 \u05d7\u05d5\u05e7\u05d9\u05ea" + }, "step": { "user": { "data": { "name": "\u05e9\u05dd" + }, + "data_description": { + "name": "\u05e9\u05dd \u05e9\u05d9\u05e9\u05de\u05e9 \u05dc\u05e2\u05e8\u05da Config \u05d5\u05d2\u05dd \u05dc\u05d7\u05d9\u05d9\u05e9\u05df" } } } }, "options": { + "error": { + "db_url_invalid": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05e9\u05dc \u05de\u05e1\u05d3 \u05d4\u05e0\u05ea\u05d5\u05e0\u05d9\u05dd \u05d0\u05d9\u05e0\u05d4 \u05d7\u05d5\u05e7\u05d9\u05ea" + }, "step": { "init": { "data": { "name": "\u05e9\u05dd" + }, + "data_description": { + "name": "\u05e9\u05dd \u05e9\u05d9\u05e9\u05de\u05e9 \u05dc\u05e2\u05e8\u05da Config \u05d5\u05d2\u05dd \u05dc\u05d7\u05d9\u05d9\u05e9\u05df" } } } diff --git a/homeassistant/components/subaru/translations/sv.json b/homeassistant/components/subaru/translations/sv.json index 38d552ff4aa..e89f29fc04a 100644 --- a/homeassistant/components/subaru/translations/sv.json +++ b/homeassistant/components/subaru/translations/sv.json @@ -17,7 +17,8 @@ "data": { "pin": "PIN-kod" }, - "description": "Ange din MySubaru PIN-kod\n OBS: Alla fordon p\u00e5 kontot m\u00e5ste ha samma PIN-kod" + "description": "Ange din MySubaru PIN-kod\n OBS: Alla fordon p\u00e5 kontot m\u00e5ste ha samma PIN-kod", + "title": "Subaru Starlink-konfiguration" }, "two_factor": { "description": "Tv\u00e5faktorautentisering kr\u00e4vs", @@ -31,8 +32,23 @@ }, "user": { "data": { + "country": "V\u00e4lj land", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "V\u00e4nligen ange dina MySubaru-uppgifter\n OBS: Initial installation kan ta upp till 30 sekunder", + "title": "Subaru Starlink-konfiguration" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "update_enabled": "Aktivera fordonsavl\u00e4sning" + }, + "description": "N\u00e4r den \u00e4r aktiverad skickar fordonsavl\u00e4sning ett fj\u00e4rrkommando till ditt fordon varannan timme f\u00f6r att f\u00e5 nya sensordata. Utan fordonsavl\u00e4sning tas nya sensordata endast emot n\u00e4r fordonet automatiskt skickar data (normalt efter motoravst\u00e4ngning).", + "title": "Subaru Starlink-alternativ" } } } diff --git a/homeassistant/components/switchbot/translations/he.json b/homeassistant/components/switchbot/translations/he.json index 836cd8b06b4..7f7974024b1 100644 --- a/homeassistant/components/switchbot/translations/he.json +++ b/homeassistant/components/switchbot/translations/he.json @@ -5,7 +5,7 @@ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "user": { "data": { diff --git a/homeassistant/components/switchbot/translations/sv.json b/homeassistant/components/switchbot/translations/sv.json index 6b1608c9da6..f2c86841487 100644 --- a/homeassistant/components/switchbot/translations/sv.json +++ b/homeassistant/components/switchbot/translations/sv.json @@ -1,9 +1,31 @@ { "config": { + "abort": { + "already_configured_device": "Enheten \u00e4r redan konfigurerad", + "switchbot_unsupported_type": "Switchbot-typ som inte st\u00f6ds.", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name} ({address})", "step": { "user": { "data": { - "address": "Enhetsadress" + "address": "Enhetsadress", + "mac": "Enhetens MAC-adress", + "name": "Namn", + "password": "L\u00f6senord" + }, + "title": "Konfigurera Switchbot-enhet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "retry_count": "Antal ompr\u00f6vningar", + "retry_timeout": "Timeout mellan \u00e5terf\u00f6rs\u00f6k", + "scan_timeout": "Hur l\u00e4nge ska man s\u00f6ka efter annonsdata", + "update_time": "Tid mellan uppdateringar (sekunder)" } } } diff --git a/homeassistant/components/switcher_kis/translations/sv.json b/homeassistant/components/switcher_kis/translations/sv.json new file mode 100644 index 00000000000..18a80850e45 --- /dev/null +++ b/homeassistant/components/switcher_kis/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "confirm": { + "description": "Vill du starta konfigurationen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthing/translations/sv.json b/homeassistant/components/syncthing/translations/sv.json index 9be5ea00256..a77f97f91e7 100644 --- a/homeassistant/components/syncthing/translations/sv.json +++ b/homeassistant/components/syncthing/translations/sv.json @@ -1,8 +1,12 @@ { "config": { + "error": { + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { + "title": "St\u00e4ll in Syncthing-integration", "token": "Token", "url": "URL", "verify_ssl": "Verifiera SSL-certifikat" diff --git a/homeassistant/components/synology_dsm/translations/sl.json b/homeassistant/components/synology_dsm/translations/sl.json index 91d32273e62..d458c167bfa 100644 --- a/homeassistant/components/synology_dsm/translations/sl.json +++ b/homeassistant/components/synology_dsm/translations/sl.json @@ -6,7 +6,7 @@ "error": { "missing_data": "Manjkajo\u010di podatki: poskusite pozneje ali v drugi konfiguraciji", "otp_failed": "Dvostopenjska avtentikacija ni uspela. Poskusite z novim geslom", - "unknown": "Neznana napaka: za ve\u010d podrobnosti preverite dnevnike" + "unknown": "Nepri\u010dakovana napaka" }, "flow_title": "Synology DSM {name} ({host})", "step": { diff --git a/homeassistant/components/synology_dsm/translations/sv.json b/homeassistant/components/synology_dsm/translations/sv.json index acfc243382d..95a8af99f63 100644 --- a/homeassistant/components/synology_dsm/translations/sv.json +++ b/homeassistant/components/synology_dsm/translations/sv.json @@ -9,7 +9,8 @@ "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", "missing_data": "Saknade data: f\u00f6rs\u00f6k igen senare eller en annan konfiguration", - "otp_failed": "Tv\u00e5stegsautentisering misslyckades, f\u00f6rs\u00f6k igen med ett nytt l\u00f6senord" + "otp_failed": "Tv\u00e5stegsautentisering misslyckades, f\u00f6rs\u00f6k igen med ett nytt l\u00f6senord", + "unknown": "Ov\u00e4ntat fel" }, "flow_title": "{name} ({host})", "step": { diff --git a/homeassistant/components/tankerkoenig/translations/he.json b/homeassistant/components/tankerkoenig/translations/he.json index 9ccb9aecfe6..3f5da8cd8d5 100644 --- a/homeassistant/components/tankerkoenig/translations/he.json +++ b/homeassistant/components/tankerkoenig/translations/he.json @@ -1,12 +1,18 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05d9\u05e7\u05d5\u05dd \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "\u05de\u05e4\u05ea\u05d7 API" + } + }, "user": { "data": { "api_key": "\u05de\u05e4\u05ea\u05d7 API", diff --git a/homeassistant/components/tellduslive/translations/sv.json b/homeassistant/components/tellduslive/translations/sv.json index 98fa56f928f..25d33f3afdf 100644 --- a/homeassistant/components/tellduslive/translations/sv.json +++ b/homeassistant/components/tellduslive/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", "authorize_url_timeout": "Timeout n\u00e4r genererar auktorisera url.", "unknown": "Ok\u00e4nt fel intr\u00e4ffade" }, diff --git a/homeassistant/components/totalconnect/translations/sv.json b/homeassistant/components/totalconnect/translations/sv.json index 064a6b2f9d3..fb9e0feb4e0 100644 --- a/homeassistant/components/totalconnect/translations/sv.json +++ b/homeassistant/components/totalconnect/translations/sv.json @@ -1,12 +1,23 @@ { "config": { "abort": { - "already_configured": "Kontot har redan konfigurerats" + "already_configured": "Kontot har redan konfigurerats", + "no_locations": "Inga platser \u00e4r tillg\u00e4ngliga f\u00f6r den h\u00e4r anv\u00e4ndaren, kontrollera TotalConnect-inst\u00e4llningarna", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "usercode": "Anv\u00e4ndarkoden \u00e4r inte giltig f\u00f6r denna anv\u00e4ndare p\u00e5 denna plats" }, "step": { + "locations": { + "description": "Ange anv\u00e4ndarkoden f\u00f6r denna anv\u00e4ndare p\u00e5 plats {location_id}", + "title": "Anv\u00e4ndarkoder f\u00f6r plats" + }, + "reauth_confirm": { + "description": "Total Connect m\u00e5ste autentisera ditt konto igen", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "password": "L\u00f6senord", diff --git a/homeassistant/components/transmission/translations/he.json b/homeassistant/components/transmission/translations/he.json index 6f8286290d4..73c5f6c7384 100644 --- a/homeassistant/components/transmission/translations/he.json +++ b/homeassistant/components/transmission/translations/he.json @@ -1,13 +1,20 @@ { "config": { "abort": { - "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4" + }, + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", diff --git a/homeassistant/components/tuya/translations/he.json b/homeassistant/components/tuya/translations/he.json index 737ca754202..713f8eafc02 100644 --- a/homeassistant/components/tuya/translations/he.json +++ b/homeassistant/components/tuya/translations/he.json @@ -7,6 +7,8 @@ "step": { "user": { "data": { + "access_id": "\u05de\u05d6\u05d4\u05d4 \u05d2\u05d9\u05e9\u05d4 \u05e9\u05dc Tuya IoT", + "access_secret": "\u05e1\u05d5\u05d3 \u05d4\u05d2\u05d9\u05e9\u05d4 \u05e9\u05dc Tuya IoT", "country_code": "\u05de\u05d3\u05d9\u05e0\u05d4", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05d7\u05e9\u05d1\u05d5\u05df" diff --git a/homeassistant/components/tuya/translations/select.sv.json b/homeassistant/components/tuya/translations/select.sv.json index 05092fe808c..c34c40ebacd 100644 --- a/homeassistant/components/tuya/translations/select.sv.json +++ b/homeassistant/components/tuya/translations/select.sv.json @@ -3,11 +3,22 @@ "tuya__humidifier_spray_mode": { "humidity": "Luftfuktighet" }, + "tuya__led_type": { + "halogen": "Halogen", + "incandescent": "Gl\u00f6dlampa", + "led": "LED" + }, "tuya__light_mode": { - "none": "Av" + "none": "Av", + "pos": "Ange omkopplarens plats", + "relay": "Indikera p\u00e5/av-l\u00e4ge" }, "tuya__relay_status": { + "last": "Kom ih\u00e5g senaste tillst\u00e5ndet", + "memory": "Kom ih\u00e5g senaste tillst\u00e5ndet", + "off": "Av", "on": "P\u00e5", + "power_off": "Av", "power_on": "P\u00e5" }, "tuya__vacuum_mode": { diff --git a/homeassistant/components/tuya/translations/sensor.sv.json b/homeassistant/components/tuya/translations/sensor.sv.json new file mode 100644 index 00000000000..a3dd5ff28df --- /dev/null +++ b/homeassistant/components/tuya/translations/sensor.sv.json @@ -0,0 +1,15 @@ +{ + "state": { + "tuya__status": { + "boiling_temp": "Koktemperatur", + "cooling": "Kyler", + "heating": "V\u00e4rmer", + "heating_temp": "Uppv\u00e4rmningstemperatur", + "reserve_1": "Reserv 1", + "reserve_2": "Reserv 2", + "reserve_3": "Reserv 3", + "standby": "Standby", + "warm": "Bevarande av v\u00e4rme" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sv.json b/homeassistant/components/tuya/translations/sv.json index 344aea60f6f..1add4cb2e2d 100644 --- a/homeassistant/components/tuya/translations/sv.json +++ b/homeassistant/components/tuya/translations/sv.json @@ -1,8 +1,13 @@ { "config": { + "error": { + "login_error": "Inloggningsfel ( {code} ): {msg}" + }, "step": { "user": { "data": { + "access_id": "Tuya IoT Access ID", + "access_secret": "Tuya IoT Access Secret", "country_code": "Landskod f\u00f6r ditt konto (t.ex. 1 f\u00f6r USA eller 86 f\u00f6r Kina)", "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" diff --git a/homeassistant/components/twentemilieu/translations/sv.json b/homeassistant/components/twentemilieu/translations/sv.json index 4e8bb592d05..b5f18b85ca3 100644 --- a/homeassistant/components/twentemilieu/translations/sv.json +++ b/homeassistant/components/twentemilieu/translations/sv.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "Platsen \u00e4r redan konfigurerad" + }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "invalid_address": "Adress hittades inte i serviceomr\u00e5det Twente Milieu." }, "step": { diff --git a/homeassistant/components/unifi/translations/sv.json b/homeassistant/components/unifi/translations/sv.json index 3c7e9dcc110..cdc66ea5126 100644 --- a/homeassistant/components/unifi/translations/sv.json +++ b/homeassistant/components/unifi/translations/sv.json @@ -2,13 +2,15 @@ "config": { "abort": { "already_configured": "Controller-platsen \u00e4r redan konfigurerad", - "configuration_updated": "Konfigurationen uppdaterad" + "configuration_updated": "Konfigurationen uppdaterad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "faulty_credentials": "Felaktiga anv\u00e4ndaruppgifter", "service_unavailable": "Ingen tj\u00e4nst tillg\u00e4nglig", "unknown_client_mac": "Ingen klient tillg\u00e4nglig p\u00e5 den MAC-adressen" }, + "flow_title": "{site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/upnp/translations/sv.json b/homeassistant/components/upnp/translations/sv.json index a067f819bba..1b1e98e987e 100644 --- a/homeassistant/components/upnp/translations/sv.json +++ b/homeassistant/components/upnp/translations/sv.json @@ -13,6 +13,20 @@ "step": { "ssdp_confirm": { "description": "Vill du konfigurera denna UPnP/IGD enhet?" + }, + "user": { + "data": { + "unique_id": "Enhet" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Uppdateringsintervall (sekunder, minst 30)" + } } } } diff --git a/homeassistant/components/velbus/translations/sv.json b/homeassistant/components/velbus/translations/sv.json index 6850c445703..eaff18c4195 100644 --- a/homeassistant/components/velbus/translations/sv.json +++ b/homeassistant/components/velbus/translations/sv.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/venstar/translations/sv.json b/homeassistant/components/venstar/translations/sv.json index 23c825f256f..61e6843a66f 100644 --- a/homeassistant/components/venstar/translations/sv.json +++ b/homeassistant/components/venstar/translations/sv.json @@ -1,10 +1,22 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "pin": "Pin-kod", + "ssl": "Anv\u00e4nd ett SSL certifikat", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Anslut till Venstar-termostaten" } } } diff --git a/homeassistant/components/verisure/translations/sv.json b/homeassistant/components/verisure/translations/sv.json index 1113a387339..f47e55d4df3 100644 --- a/homeassistant/components/verisure/translations/sv.json +++ b/homeassistant/components/verisure/translations/sv.json @@ -1,10 +1,21 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, "error": { + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel", "unknown_mfa": "Ett ok\u00e4nt fel har intr\u00e4ffat under konfiguration av MFA" }, "step": { + "installation": { + "data": { + "giid": "Installation" + }, + "description": "Home Assistant hittade flera Verisure-installationer i ditt Mina sidor-konto. V\u00e4lj installationen som du vill l\u00e4gga till i Home Assistant." + }, "mfa": { "data": { "code": "Verifieringskod", @@ -13,6 +24,8 @@ }, "reauth_confirm": { "data": { + "description": "Autentisera p\u00e5 nytt med ditt Verisure mina sidor-konto.", + "email": "E-post", "password": "L\u00f6senord" } }, @@ -24,9 +37,24 @@ }, "user": { "data": { + "description": "Logga in med ditt Verisure My Pages-konto.", + "email": "E-post", "password": "L\u00f6senord" } } } + }, + "options": { + "error": { + "code_format_mismatch": "Standard-PIN-koden matchar inte det antal siffror som kr\u00e4vs" + }, + "step": { + "init": { + "data": { + "lock_code_digits": "Antal siffror i PIN-kod f\u00f6r l\u00e5s", + "lock_default_code": "Standard PIN-kod f\u00f6r l\u00e5s, anv\u00e4nds om ingen anges" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/vlc_telnet/translations/sv.json b/homeassistant/components/vlc_telnet/translations/sv.json index eba844f6c03..f8a77ff3086 100644 --- a/homeassistant/components/vlc_telnet/translations/sv.json +++ b/homeassistant/components/vlc_telnet/translations/sv.json @@ -1,6 +1,14 @@ { "config": { + "abort": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { + "hassio_confirm": { + "description": "Vill du ansluta till till\u00e4gget {addon} ?" + }, "user": { "data": { "host": "V\u00e4rd" diff --git a/homeassistant/components/wallbox/translations/sv.json b/homeassistant/components/wallbox/translations/sv.json index 79a05e057b8..65f079ab79f 100644 --- a/homeassistant/components/wallbox/translations/sv.json +++ b/homeassistant/components/wallbox/translations/sv.json @@ -1,16 +1,19 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", + "reauth_invalid": "\u00c5terautentisering misslyckades; Serienumret st\u00e4mmer inte \u00f6verens med originalet", "unknown": "Ov\u00e4ntat fel" }, "step": { "reauth_confirm": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } }, diff --git a/homeassistant/components/water_heater/translations/sv.json b/homeassistant/components/water_heater/translations/sv.json index 9b45fec830e..99980e832c0 100644 --- a/homeassistant/components/water_heater/translations/sv.json +++ b/homeassistant/components/water_heater/translations/sv.json @@ -11,7 +11,9 @@ "electric": "Elektrisk", "gas": "Gas", "heat_pump": "V\u00e4rmepump", - "off": "Av" + "high_demand": "H\u00f6g efterfr\u00e5gan", + "off": "Av", + "performance": "Prestanda" } } } \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/sv.json b/homeassistant/components/watttime/translations/sv.json index dadc8b53d2d..f8a3d0b7fcf 100644 --- a/homeassistant/components/watttime/translations/sv.json +++ b/homeassistant/components/watttime/translations/sv.json @@ -1,6 +1,20 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel", + "unknown_coordinates": "Inga data f\u00f6r latitud/longitud" + }, "step": { + "coordinates": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + } + }, "location": { "data": { "location_type": "Plats" @@ -15,5 +29,15 @@ "description": "Ange ditt anv\u00e4ndarnamn och l\u00f6senord:" } } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Visa \u00f6vervakad plats p\u00e5 kartan" + }, + "title": "Konfigurera WattTime" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/sv.json b/homeassistant/components/wemo/translations/sv.json index be479aa1333..c489579828f 100644 --- a/homeassistant/components/wemo/translations/sv.json +++ b/homeassistant/components/wemo/translations/sv.json @@ -9,5 +9,10 @@ "description": "Vill du konfigurera Wemo?" } } + }, + "device_automation": { + "trigger_type": { + "long_press": "Wemo-knappen trycktes in i 2 sekunder" + } } } \ No newline at end of file diff --git a/homeassistant/components/withings/translations/he.json b/homeassistant/components/withings/translations/he.json index 6bcd8bf9a9d..5624b795692 100644 --- a/homeassistant/components/withings/translations/he.json +++ b/homeassistant/components/withings/translations/he.json @@ -15,6 +15,9 @@ }, "reauth": { "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" + }, + "reauth_confirm": { + "title": "\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05e9\u05dc \u05e9\u05d9\u05dc\u05d5\u05d1" } } } diff --git a/homeassistant/components/withings/translations/sl.json b/homeassistant/components/withings/translations/sl.json index 0b1ef34bf7c..301bceff9db 100644 --- a/homeassistant/components/withings/translations/sl.json +++ b/homeassistant/components/withings/translations/sl.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", - "missing_configuration": "Integracija Withings ni konfigurirana. Prosimo, upo\u0161tevajte dokumentacijo." + "missing_configuration": "Komponenta Netatmo ni konfigurirana. Prosimo, upo\u0161tevajte dokumentacijo." }, "create_entry": { "default": "Uspe\u0161no overjen z Withings." diff --git a/homeassistant/components/wled/translations/select.sv.json b/homeassistant/components/wled/translations/select.sv.json index 1c3bb4c1ff4..7743fe99020 100644 --- a/homeassistant/components/wled/translations/select.sv.json +++ b/homeassistant/components/wled/translations/select.sv.json @@ -1,7 +1,9 @@ { "state": { "wled__live_override": { - "1": "P\u00e5" + "0": "Av", + "1": "P\u00e5", + "2": "Tills enheten startar om" } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.sv.json b/homeassistant/components/wolflink/translations/sensor.sv.json index 797a7d6a230..faf7a2b9673 100644 --- a/homeassistant/components/wolflink/translations/sensor.sv.json +++ b/homeassistant/components/wolflink/translations/sensor.sv.json @@ -11,6 +11,15 @@ "at_frostschutz": "OT frostskydd", "aus": "Inaktiverad", "auto": "Automatiskt", + "auto_on_cool": "AutoOnCool", + "automatik_aus": "Automatisk avst\u00e4ngning", + "automatik_ein": "Automatiskt p\u00e5", + "bereit_keine_ladung": "Redo, laddas inte", + "betrieb_ohne_brenner": "Arbetar utan br\u00e4nnare", + "cooling": "Kyler", + "deaktiviert": "Inaktiv", + "eco": "Eco", + "ein": "Aktiverad", "estrichtrocknung": "Avj\u00e4mningstorkning", "externe_deaktivierung": "Extern avaktivering", "fernschalter_ein": "Fj\u00e4rrkontroll aktiverad", @@ -41,8 +50,28 @@ "perm_cooling": "PermCooling", "permanent": "Permanent", "permanentbetrieb": "Permanent l\u00e4ge", + "reduzierter_betrieb": "Begr\u00e4nsat l\u00e4ge", + "rt_abschaltung": "RT avst\u00e4ngning", + "rt_frostschutz": "RT frostskydd", + "ruhekontakt": "Kontakt f\u00f6r vila", + "schornsteinfeger": "Utsl\u00e4ppstest", + "smart_grid": "Smarta eln\u00e4t", + "smart_home": "Smart hem", + "softstart": "Mjukstart", + "solarbetrieb": "Solcellsl\u00e4ge", + "sparbetrieb": "Ekonomil\u00e4ge", "sparen": "Ekonomi", + "spreizung_hoch": "dT f\u00f6r bred", + "spreizung_kf": "Spridning KF", + "stabilisierung": "Stabilisering", + "standby": "Standby", + "start": "Starta", + "storung": "Fel", + "taktsperre": "Anti-cykel", + "telefonfernschalter": "Telefonfj\u00e4rrbrytare", "test": "Test", + "tpw": "TPW", + "urlaubsmodus": "Semesterl\u00e4ge", "vorspulen": "Ing\u00e5ngssk\u00f6ljning", "warmwasser": "Varmvatten", "warmwasser_schnellstart": "Snabbstart f\u00f6r varmvatten", diff --git a/homeassistant/components/xbox/translations/sv.json b/homeassistant/components/xbox/translations/sv.json index cb98fb350b7..e9c8dbb2610 100644 --- a/homeassistant/components/xbox/translations/sv.json +++ b/homeassistant/components/xbox/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, diff --git a/homeassistant/components/xiaomi_ble/translations/he.json b/homeassistant/components/xiaomi_ble/translations/he.json new file mode 100644 index 00000000000..b90a366130a --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/he.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05db\u05e9\u05d9\u05e8\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea {name}?" + }, + "user": { + "data": { + "address": "\u05d4\u05ea\u05e7\u05df" + }, + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/id.json b/homeassistant/components/xiaomi_ble/translations/id.json index 5d01dcab709..e6e29966bc7 100644 --- a/homeassistant/components/xiaomi_ble/translations/id.json +++ b/homeassistant/components/xiaomi_ble/translations/id.json @@ -9,11 +9,19 @@ "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", "reauth_successful": "Autentikasi ulang berhasil" }, + "error": { + "decryption_failed": "Bindkey yang disediakan tidak berfungsi, data sensor tidak dapat didekripsi. Silakan periksa dan coba lagi.", + "expected_24_characters": "Diharapkan bindkey berupa 24 karakter heksadesimal.", + "expected_32_characters": "Diharapkan bindkey berupa 32 karakter heksadesimal." + }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Ingin menyiapkan {name}?" }, + "confirm_slow": { + "description": "Belum ada siaran dari perangkat ini dalam menit terakhir jadi kami tidak yakin apakah perangkat ini menggunakan enkripsi atau tidak. Ini mungkin terjadi karena perangkat menggunakan interval siaran yang lambat. Konfirmasikan sekarang untuk menambahkan perangkat ini, dan ketika siaran diterima nanti, Anda akan diminta untuk memasukkan kunci bind jika diperlukan." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindkey" diff --git a/homeassistant/components/xiaomi_miio/translations/sv.json b/homeassistant/components/xiaomi_miio/translations/sv.json index a52c1ecbf98..37452a61e79 100644 --- a/homeassistant/components/xiaomi_miio/translations/sv.json +++ b/homeassistant/components/xiaomi_miio/translations/sv.json @@ -32,8 +32,10 @@ }, "manual": { "data": { - "host": "IP-adress" - } + "host": "IP-adress", + "token": "API Token" + }, + "description": "Du beh\u00f6ver de 32 tecknen API Token , se https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token f\u00f6r instruktioner. Observera att denna API Token skiljer sig fr\u00e5n nyckeln som anv\u00e4nds av Xiaomi Aqara-integrationen." }, "reauth_confirm": { "description": "Xiaomi Miio-integrationen m\u00e5ste autentisera ditt konto igen f\u00f6r att uppdatera tokens eller l\u00e4gga till saknade molnuppgifter.", @@ -46,5 +48,17 @@ "description": "V\u00e4lj Xiaomi Miio-enheten f\u00f6r att st\u00e4lla in." } } + }, + "options": { + "error": { + "cloud_credentials_incomplete": "Molnuppgifter \u00e4r ofullst\u00e4ndiga, v\u00e4nligen fyll i anv\u00e4ndarnamn, l\u00f6senord och land" + }, + "step": { + "init": { + "data": { + "cloud_subdevices": "Anv\u00e4nd moln f\u00f6r att f\u00e5 anslutna underenheter" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/sv.json b/homeassistant/components/yeelight/translations/sv.json index 8575090ff7c..7a8780c0bf1 100644 --- a/homeassistant/components/yeelight/translations/sv.json +++ b/homeassistant/components/yeelight/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { "cannot_connect": "Det gick inte att ansluta." }, diff --git a/homeassistant/components/zwave_js/translations/sv.json b/homeassistant/components/zwave_js/translations/sv.json index 8f8777f92b0..65ddbb3dce8 100644 --- a/homeassistant/components/zwave_js/translations/sv.json +++ b/homeassistant/components/zwave_js/translations/sv.json @@ -12,6 +12,14 @@ }, "flow_title": "{name}", "step": { + "configure_addon": { + "data": { + "s0_legacy_key": "S0-nyckel (\u00e4ldre)", + "s2_access_control_key": "S2-\u00e5tkomstkontrollnyckel", + "s2_authenticated_key": "S2 Autentiserad nyckel", + "s2_unauthenticated_key": "S2 oautentiserad nyckel" + } + }, "usb_confirm": { "description": "Vill du konfigurera {name} med Z-Wave JS till\u00e4gget?" }, @@ -22,6 +30,10 @@ }, "device_automation": { "action_type": { + "clear_lock_usercode": "Rensa anv\u00e4ndarkoden p\u00e5 {entity_name}", + "ping": "Pinga enhet", + "refresh_value": "Uppdatera v\u00e4rdet/v\u00e4rdena f\u00f6r {entity_name}", + "reset_meter": "\u00c5terst\u00e4ll m\u00e4tare p\u00e5 {subtype}", "set_config_parameter": "Ange v\u00e4rde f\u00f6r konfigurationsparametern {subtype}", "set_lock_usercode": "Ange en anv\u00e4ndarkod p\u00e5 {entity_name}", "set_value": "St\u00e4ll in v\u00e4rdet f\u00f6r ett Z-Wave-v\u00e4rde" @@ -32,7 +44,14 @@ "value": "Nuvarande v\u00e4rde f\u00f6r ett Z-Wave v\u00e4rde" }, "trigger_type": { - "zwave_js.value_updated.config_parameter": "V\u00e4rde\u00e4ndring p\u00e5 konfigurationsparameter {subtype}" + "event.notification.entry_control": "Skickat ett meddelande om kontroll av intr\u00e4de", + "event.notification.notification": "Skickade ett meddelande", + "event.value_notification.basic": "Grundl\u00e4ggande CC-h\u00e4ndelse p\u00e5 {subtype}", + "event.value_notification.central_scene": "Central scen\u00e5tg\u00e4rd p\u00e5 {subtype}", + "event.value_notification.scene_activation": "Scenaktivering p\u00e5 {subtype}", + "state.node_status": "Nodstatus \u00e4ndrad", + "zwave_js.value_updated.config_parameter": "V\u00e4rde\u00e4ndring p\u00e5 konfigurationsparameter {subtype}", + "zwave_js.value_updated.value": "V\u00e4rdef\u00f6r\u00e4ndring p\u00e5 ett Z-Wave JS-v\u00e4rde" } }, "options": { @@ -56,6 +75,10 @@ "data": { "emulate_hardware": "Emulera h\u00e5rdvara", "log_level": "Loggniv\u00e5", + "s0_legacy_key": "S0-nyckel (\u00e4ldre)", + "s2_access_control_key": "S2-\u00e5tkomstkontrollnyckel", + "s2_authenticated_key": "S2 Autentiserad nyckel", + "s2_unauthenticated_key": "S2 oautentiserad nyckel", "usb_path": "USB-enhetens s\u00f6kv\u00e4g" } }, From 3b29cbcd61fdf9007b426bad0d4fd73590144cff Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 5 Aug 2022 10:11:20 +0200 Subject: [PATCH 3159/3516] Support creating persistent repairs issues (#76211) --- .../components/repairs/issue_handler.py | 4 + .../components/repairs/issue_registry.py | 115 ++++++++++++----- .../components/repairs/websocket_api.py | 2 +- tests/components/repairs/test_init.py | 8 ++ .../components/repairs/test_issue_registry.py | 117 ++++++++++++++++-- .../components/repairs/test_websocket_api.py | 5 +- 6 files changed, 209 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index c139026ec48..a08fff29598 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -89,6 +89,7 @@ def async_create_issue( issue_domain: str | None = None, breaks_in_ha_version: str | None = None, is_fixable: bool, + is_persistent: bool = False, learn_more_url: str | None = None, severity: IssueSeverity, translation_key: str, @@ -110,6 +111,7 @@ def async_create_issue( issue_domain=issue_domain, breaks_in_ha_version=breaks_in_ha_version, is_fixable=is_fixable, + is_persistent=is_persistent, learn_more_url=learn_more_url, severity=severity, translation_key=translation_key, @@ -124,6 +126,7 @@ def create_issue( *, breaks_in_ha_version: str | None = None, is_fixable: bool, + is_persistent: bool = False, learn_more_url: str | None = None, severity: IssueSeverity, translation_key: str, @@ -139,6 +142,7 @@ def create_issue( issue_id, breaks_in_ha_version=breaks_in_ha_version, is_fixable=is_fixable, + is_persistent=is_persistent, learn_more_url=learn_more_url, severity=severity, translation_key=translation_key, diff --git a/homeassistant/components/repairs/issue_registry.py b/homeassistant/components/repairs/issue_registry.py index bd201f1007c..c7502ecf397 100644 --- a/homeassistant/components/repairs/issue_registry.py +++ b/homeassistant/components/repairs/issue_registry.py @@ -3,7 +3,7 @@ from __future__ import annotations import dataclasses from datetime import datetime -from typing import Optional, cast +from typing import Any, cast from homeassistant.const import __version__ as ha_version from homeassistant.core import HomeAssistant, callback @@ -15,7 +15,8 @@ from .models import IssueSeverity DATA_REGISTRY = "issue_registry" EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED = "repairs_issue_registry_updated" STORAGE_KEY = "repairs.issue_registry" -STORAGE_VERSION = 1 +STORAGE_VERSION_MAJOR = 1 +STORAGE_VERSION_MINOR = 2 SAVE_DELAY = 10 @@ -29,14 +30,53 @@ class IssueEntry: dismissed_version: str | None domain: str is_fixable: bool | None - issue_id: str + is_persistent: bool # Used if an integration creates issues for other integrations (ie alerts) issue_domain: str | None + issue_id: str learn_more_url: str | None severity: IssueSeverity | None translation_key: str | None translation_placeholders: dict[str, str] | None + def to_json(self) -> dict[str, Any]: + """Return a JSON serializable representation for storage.""" + result = { + "created": self.created.isoformat(), + "dismissed_version": self.dismissed_version, + "domain": self.domain, + "is_persistent": False, + "issue_id": self.issue_id, + } + if not self.is_persistent: + return result + return { + **result, + "breaks_in_ha_version": self.breaks_in_ha_version, + "is_fixable": self.is_fixable, + "is_persistent": True, + "issue_domain": self.issue_domain, + "issue_id": self.issue_id, + "learn_more_url": self.learn_more_url, + "severity": self.severity, + "translation_key": self.translation_key, + "translation_placeholders": self.translation_placeholders, + } + + +class IssueRegistryStore(Store[dict[str, list[dict[str, Any]]]]): + """Store entity registry data.""" + + async def _async_migrate_func( + self, old_major_version: int, old_minor_version: int, old_data: dict[str, Any] + ) -> dict[str, Any]: + """Migrate to the new version.""" + if old_major_version == 1 and old_minor_version < 2: + # Version 1.2 adds is_persistent + for issue in old_data["issues"]: + issue["is_persistent"] = False + return old_data + class IssueRegistry: """Class to hold a registry of issues.""" @@ -45,8 +85,12 @@ class IssueRegistry: """Initialize the issue registry.""" self.hass = hass self.issues: dict[tuple[str, str], IssueEntry] = {} - self._store = Store[dict[str, list[dict[str, Optional[str]]]]]( - hass, STORAGE_VERSION, STORAGE_KEY, atomic_writes=True + self._store = IssueRegistryStore( + hass, + STORAGE_VERSION_MAJOR, + STORAGE_KEY, + atomic_writes=True, + minor_version=STORAGE_VERSION_MINOR, ) @callback @@ -63,6 +107,7 @@ class IssueRegistry: issue_domain: str | None = None, breaks_in_ha_version: str | None = None, is_fixable: bool, + is_persistent: bool, learn_more_url: str | None = None, severity: IssueSeverity, translation_key: str, @@ -78,6 +123,7 @@ class IssueRegistry: dismissed_version=None, domain=domain, is_fixable=is_fixable, + is_persistent=is_persistent, issue_domain=issue_domain, issue_id=issue_id, learn_more_url=learn_more_url, @@ -97,6 +143,7 @@ class IssueRegistry: active=True, breaks_in_ha_version=breaks_in_ha_version, is_fixable=is_fixable, + is_persistent=is_persistent, issue_domain=issue_domain, learn_more_url=learn_more_url, severity=severity, @@ -151,21 +198,39 @@ class IssueRegistry: if isinstance(data, dict): for issue in data["issues"]: - assert issue["created"] and issue["domain"] and issue["issue_id"] - issues[(issue["domain"], issue["issue_id"])] = IssueEntry( - active=False, - breaks_in_ha_version=None, - created=cast(datetime, dt_util.parse_datetime(issue["created"])), - dismissed_version=issue["dismissed_version"], - domain=issue["domain"], - is_fixable=None, - issue_id=issue["issue_id"], - issue_domain=None, - learn_more_url=None, - severity=None, - translation_key=None, - translation_placeholders=None, - ) + created = cast(datetime, dt_util.parse_datetime(issue["created"])) + if issue["is_persistent"]: + issues[(issue["domain"], issue["issue_id"])] = IssueEntry( + active=True, + breaks_in_ha_version=issue["breaks_in_ha_version"], + created=created, + dismissed_version=issue["dismissed_version"], + domain=issue["domain"], + is_fixable=issue["is_fixable"], + is_persistent=issue["is_persistent"], + issue_id=issue["issue_id"], + issue_domain=issue["issue_domain"], + learn_more_url=issue["learn_more_url"], + severity=issue["severity"], + translation_key=issue["translation_key"], + translation_placeholders=issue["translation_placeholders"], + ) + else: + issues[(issue["domain"], issue["issue_id"])] = IssueEntry( + active=False, + breaks_in_ha_version=None, + created=created, + dismissed_version=issue["dismissed_version"], + domain=issue["domain"], + is_fixable=None, + is_persistent=issue["is_persistent"], + issue_id=issue["issue_id"], + issue_domain=None, + learn_more_url=None, + severity=None, + translation_key=None, + translation_placeholders=None, + ) self.issues = issues @@ -179,15 +244,7 @@ class IssueRegistry: """Return data of issue registry to store in a file.""" data = {} - data["issues"] = [ - { - "created": entry.created.isoformat(), - "dismissed_version": entry.dismissed_version, - "domain": entry.domain, - "issue_id": entry.issue_id, - } - for entry in self.issues.values() - ] + data["issues"] = [entry.to_json() for entry in self.issues.values()] return data diff --git a/homeassistant/components/repairs/websocket_api.py b/homeassistant/components/repairs/websocket_api.py index 2e9fcc5f8e4..ff0ac5ba8f9 100644 --- a/homeassistant/components/repairs/websocket_api.py +++ b/homeassistant/components/repairs/websocket_api.py @@ -64,7 +64,7 @@ def ws_list_issues( """Return a list of issues.""" def ws_dict(kv_pairs: list[tuple[Any, Any]]) -> dict[Any, Any]: - result = {k: v for k, v in kv_pairs if k not in ("active")} + result = {k: v for k, v in kv_pairs if k not in ("active", "is_persistent")} result["ignored"] = result["dismissed_version"] is not None result["created"] = result["created"].isoformat() return result diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py index d70f6c6e11d..1fc8367e4c3 100644 --- a/tests/components/repairs/test_init.py +++ b/tests/components/repairs/test_init.py @@ -68,6 +68,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], is_fixable=issue["is_fixable"], + is_persistent=False, learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -98,6 +99,7 @@ async def test_create_update_issue(hass: HomeAssistant, hass_ws_client) -> None: issues[0]["issue_id"], breaks_in_ha_version=issues[0]["breaks_in_ha_version"], is_fixable=issues[0]["is_fixable"], + is_persistent=False, issue_domain="my_issue_domain", learn_more_url="blablabla", severity=issues[0]["severity"], @@ -146,6 +148,7 @@ async def test_create_issue_invalid_version( issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], is_fixable=issue["is_fixable"], + is_persistent=False, learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -192,6 +195,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], is_fixable=issue["is_fixable"], + is_persistent=False, learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -283,6 +287,7 @@ async def test_ignore_issue(hass: HomeAssistant, hass_ws_client) -> None: issues[0]["issue_id"], breaks_in_ha_version=issues[0]["breaks_in_ha_version"], is_fixable=issues[0]["is_fixable"], + is_persistent=False, learn_more_url="blablabla", severity=issues[0]["severity"], translation_key=issues[0]["translation_key"], @@ -351,6 +356,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], is_fixable=issue["is_fixable"], + is_persistent=False, learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -422,6 +428,7 @@ async def test_delete_issue(hass: HomeAssistant, hass_ws_client, freezer) -> Non issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], is_fixable=issue["is_fixable"], + is_persistent=False, learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -492,6 +499,7 @@ async def test_sync_methods( "sync_issue", breaks_in_ha_version="2022.9", is_fixable=True, + is_persistent=False, learn_more_url="https://theuselessweb.com", severity=IssueSeverity.ERROR, translation_key="abc_123", diff --git a/tests/components/repairs/test_issue_registry.py b/tests/components/repairs/test_issue_registry.py index 523f75bfdc2..ff6c4b996da 100644 --- a/tests/components/repairs/test_issue_registry.py +++ b/tests/components/repairs/test_issue_registry.py @@ -21,6 +21,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: "domain": "test", "issue_id": "issue_1", "is_fixable": True, + "is_persistent": False, "learn_more_url": "https://theuselessweb.com", "severity": "error", "translation_key": "abc_123", @@ -31,6 +32,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: "domain": "test", "issue_id": "issue_2", "is_fixable": True, + "is_persistent": False, "learn_more_url": "https://theuselessweb.com/abc", "severity": "other", "translation_key": "even_worse", @@ -41,11 +43,23 @@ async def test_load_issues(hass: HomeAssistant) -> None: "domain": "test", "issue_id": "issue_3", "is_fixable": True, + "is_persistent": False, "learn_more_url": "https://checkboxrace.com", "severity": "other", "translation_key": "even_worse", "translation_placeholders": {"def": "789"}, }, + { + "breaks_in_ha_version": "2022.6", + "domain": "test", + "issue_id": "issue_4", + "is_fixable": True, + "is_persistent": True, + "learn_more_url": "https://checkboxrace.com/blah", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"xyz": "abc"}, + }, ] events = async_capture_events( @@ -59,6 +73,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], is_fixable=issue["is_fixable"], + is_persistent=issue["is_persistent"], learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -67,7 +82,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: await hass.async_block_till_done() - assert len(events) == 3 + assert len(events) == 4 assert events[0].data == { "action": "create", "domain": "test", @@ -83,12 +98,17 @@ async def test_load_issues(hass: HomeAssistant) -> None: "domain": "test", "issue_id": "issue_3", } + assert events[3].data == { + "action": "create", + "domain": "test", + "issue_id": "issue_4", + } async_ignore_issue(hass, issues[0]["domain"], issues[0]["issue_id"], True) await hass.async_block_till_done() - assert len(events) == 4 - assert events[3].data == { + assert len(events) == 5 + assert events[4].data == { "action": "update", "domain": "test", "issue_id": "issue_1", @@ -97,17 +117,18 @@ async def test_load_issues(hass: HomeAssistant) -> None: async_delete_issue(hass, issues[2]["domain"], issues[2]["issue_id"]) await hass.async_block_till_done() - assert len(events) == 5 - assert events[4].data == { + assert len(events) == 6 + assert events[5].data == { "action": "remove", "domain": "test", "issue_id": "issue_3", } registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] - assert len(registry.issues) == 2 + assert len(registry.issues) == 3 issue1 = registry.async_get_issue("test", "issue_1") issue2 = registry.async_get_issue("test", "issue_2") + issue4 = registry.async_get_issue("test", "issue_4") registry2 = issue_registry.IssueRegistry(hass) await flush_store(registry._store) @@ -116,17 +137,91 @@ async def test_load_issues(hass: HomeAssistant) -> None: assert list(registry.issues) == list(registry2.issues) issue1_registry2 = registry2.async_get_issue("test", "issue_1") - assert issue1_registry2.created == issue1.created - assert issue1_registry2.dismissed_version == issue1.dismissed_version + assert issue1_registry2 == issue_registry.IssueEntry( + active=False, + breaks_in_ha_version=None, + created=issue1.created, + dismissed_version=issue1.dismissed_version, + domain=issue1.domain, + is_fixable=None, + is_persistent=issue1.is_persistent, + issue_domain=None, + issue_id=issue1.issue_id, + learn_more_url=None, + severity=None, + translation_key=None, + translation_placeholders=None, + ) issue2_registry2 = registry2.async_get_issue("test", "issue_2") - assert issue2_registry2.created == issue2.created - assert issue2_registry2.dismissed_version == issue2.dismissed_version + assert issue2_registry2 == issue_registry.IssueEntry( + active=False, + breaks_in_ha_version=None, + created=issue2.created, + dismissed_version=issue2.dismissed_version, + domain=issue2.domain, + is_fixable=None, + is_persistent=issue2.is_persistent, + issue_domain=None, + issue_id=issue2.issue_id, + learn_more_url=None, + severity=None, + translation_key=None, + translation_placeholders=None, + ) + issue4_registry2 = registry2.async_get_issue("test", "issue_4") + assert issue4_registry2 == issue4 async def test_loading_issues_from_storage(hass: HomeAssistant, hass_storage) -> None: """Test loading stored issues on start.""" hass_storage[issue_registry.STORAGE_KEY] = { - "version": issue_registry.STORAGE_VERSION, + "version": issue_registry.STORAGE_VERSION_MAJOR, + "minor_version": issue_registry.STORAGE_VERSION_MINOR, + "data": { + "issues": [ + { + "created": "2022-07-19T09:41:13.746514+00:00", + "dismissed_version": "2022.7.0.dev0", + "domain": "test", + "is_persistent": False, + "issue_id": "issue_1", + }, + { + "created": "2022-07-19T19:41:13.746514+00:00", + "dismissed_version": None, + "domain": "test", + "is_persistent": False, + "issue_id": "issue_2", + }, + { + "breaks_in_ha_version": "2022.6", + "created": "2022-07-19T19:41:13.746514+00:00", + "dismissed_version": None, + "domain": "test", + "issue_domain": "blubb", + "issue_id": "issue_4", + "is_fixable": True, + "is_persistent": True, + "learn_more_url": "https://checkboxrace.com/blah", + "severity": "other", + "translation_key": "even_worse", + "translation_placeholders": {"xyz": "abc"}, + }, + ] + }, + } + + assert await async_setup_component(hass, DOMAIN, {}) + + registry: issue_registry.IssueRegistry = hass.data[issue_registry.DATA_REGISTRY] + assert len(registry.issues) == 3 + + +async def test_migration_1_1(hass: HomeAssistant, hass_storage) -> None: + """Test migration from version 1.1.""" + hass_storage[issue_registry.STORAGE_KEY] = { + "version": 1, + "minor_version": 1, "data": { "issues": [ { diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index d778b043832..359024f9fe5 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -44,6 +44,7 @@ async def create_issues(hass, ws_client): issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], is_fixable=issue["is_fixable"], + is_persistent=False, learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], @@ -379,13 +380,14 @@ async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> # Add an inactive issue, this should not be exposed in the list hass_storage[issue_registry.STORAGE_KEY] = { - "version": issue_registry.STORAGE_VERSION, + "version": issue_registry.STORAGE_VERSION_MAJOR, "data": { "issues": [ { "created": "2022-07-19T09:41:13.746514+00:00", "dismissed_version": None, "domain": "test", + "is_persistent": False, "issue_id": "issue_3_inactive", "issue_domain": None, }, @@ -435,6 +437,7 @@ async def test_list_issues(hass: HomeAssistant, hass_storage, hass_ws_client) -> issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], is_fixable=issue["is_fixable"], + is_persistent=False, learn_more_url=issue["learn_more_url"], severity=issue["severity"], translation_key=issue["translation_key"], From babb3d10a1cc113522e43a75563301676dc1accc Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 5 Aug 2022 10:59:43 +0200 Subject: [PATCH 3160/3516] Deprecate the Deutsche Bahn (#76286) --- .../components/deutsche_bahn/manifest.json | 1 + .../components/deutsche_bahn/sensor.py | 17 ++++++++++++++++- .../components/deutsche_bahn/strings.json | 8 ++++++++ .../deutsche_bahn/translations/en.json | 8 ++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/deutsche_bahn/strings.json create mode 100644 homeassistant/components/deutsche_bahn/translations/en.json diff --git a/homeassistant/components/deutsche_bahn/manifest.json b/homeassistant/components/deutsche_bahn/manifest.json index 1eeb2241db5..dd69a940de0 100644 --- a/homeassistant/components/deutsche_bahn/manifest.json +++ b/homeassistant/components/deutsche_bahn/manifest.json @@ -3,6 +3,7 @@ "name": "Deutsche Bahn", "documentation": "https://www.home-assistant.io/integrations/deutsche_bahn", "requirements": ["schiene==0.23"], + "dependencies": ["repairs"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["schiene"] diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py index 6c784fa9a89..07c330fd402 100644 --- a/homeassistant/components/deutsche_bahn/sensor.py +++ b/homeassistant/components/deutsche_bahn/sensor.py @@ -2,10 +2,12 @@ from __future__ import annotations from datetime import timedelta +import logging import schiene import voluptuous as vol +from homeassistant.components.repairs import IssueSeverity, create_issue from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.const import CONF_OFFSET from homeassistant.core import HomeAssistant @@ -33,6 +35,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +_LOGGER = logging.getLogger(__name__) + def setup_platform( hass: HomeAssistant, @@ -45,7 +49,18 @@ def setup_platform( destination = config[CONF_DESTINATION] offset = config[CONF_OFFSET] only_direct = config[CONF_ONLY_DIRECT] - + create_issue( + hass, + "deutsche_bahn", + "pending_removal", + breaks_in_ha_version="2022.11.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="pending_removal", + ) + _LOGGER.warning( + "The Deutsche Bahn sensor component is deprecated and will be removed in Home Assistant 2022.11" + ) add_entities([DeutscheBahnSensor(start, destination, offset, only_direct)], True) diff --git a/homeassistant/components/deutsche_bahn/strings.json b/homeassistant/components/deutsche_bahn/strings.json new file mode 100644 index 00000000000..c88668862da --- /dev/null +++ b/homeassistant/components/deutsche_bahn/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "title": "The Deutsche Bahn integration is being removed", + "description": "The Deutsche Bahn integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.11.\n\nThe integration is being removed, because it relies on webscraping, which is not allowed.\n\nRemove the Deutsche Bahn YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/deutsche_bahn/translations/en.json b/homeassistant/components/deutsche_bahn/translations/en.json new file mode 100644 index 00000000000..0ad1f14d1cc --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "The Deutsche Bahn integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.11.\n\nThe integration is being removed, because it relies on webscraping, which is not allowed.\n\nRemove the Deutsche Bahn YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Deutsche Bahn integration is being removed" + } + } +} \ No newline at end of file From 34dcc74491048b64ae847d48591402c8a3b0623d Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 5 Aug 2022 05:06:40 -0400 Subject: [PATCH 3161/3516] Bump ZHA dependencies (#76275) --- homeassistant/components/zha/manifest.json | 6 +++--- requirements_all.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 179302af1cd..bad84054f1f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,14 +4,14 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.31.2", + "bellows==0.31.3", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.78", "zigpy-deconz==0.18.0", - "zigpy==0.48.0", + "zigpy==0.49.0", "zigpy-xbee==0.15.0", - "zigpy-zigate==0.9.0", + "zigpy-zigate==0.9.1", "zigpy-znp==0.8.1" ], "usb": [ diff --git a/requirements_all.txt b/requirements_all.txt index 3abb34ff5da..1b6db67f18c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -399,7 +399,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.31.2 +bellows==0.31.3 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.1 @@ -2535,13 +2535,13 @@ zigpy-deconz==0.18.0 zigpy-xbee==0.15.0 # homeassistant.components.zha -zigpy-zigate==0.9.0 +zigpy-zigate==0.9.1 # homeassistant.components.zha zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.48.0 +zigpy==0.49.0 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 96845b12579..8e063b257ff 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -320,7 +320,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.31.2 +bellows==0.31.3 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.1 @@ -1706,13 +1706,13 @@ zigpy-deconz==0.18.0 zigpy-xbee==0.15.0 # homeassistant.components.zha -zigpy-zigate==0.9.0 +zigpy-zigate==0.9.1 # homeassistant.components.zha zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.48.0 +zigpy==0.49.0 # homeassistant.components.zwave_js zwave-js-server-python==0.40.0 From 44aa49dde8d9db962a50875b16a1e8fe7647cdf1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 5 Aug 2022 11:22:55 +0200 Subject: [PATCH 3162/3516] Bump pydeconz to v102 (#76287) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 6384ebfcd5f..e4e412056e6 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==101"], + "requirements": ["pydeconz==102"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 1b6db67f18c..e6b42839383 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1461,7 +1461,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==101 +pydeconz==102 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8e063b257ff..b128bc80b21 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1004,7 +1004,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==101 +pydeconz==102 # homeassistant.components.dexcom pydexcom==0.2.3 From 861b694cffbeb0813dc75320924307cfea3acfde Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 5 Aug 2022 11:39:51 +0200 Subject: [PATCH 3163/3516] Use attributes in litejet light (#76031) --- homeassistant/components/litejet/light.py | 67 ++++++++--------------- 1 file changed, 24 insertions(+), 43 deletions(-) diff --git a/homeassistant/components/litejet/light.py b/homeassistant/components/litejet/light.py index 74395117c46..a41a34016d9 100644 --- a/homeassistant/components/litejet/light.py +++ b/homeassistant/components/litejet/light.py @@ -1,7 +1,11 @@ """Support for LiteJet lights.""" +from __future__ import annotations + import logging from typing import Any +from pylitejet import LiteJet + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_TRANSITION, @@ -27,13 +31,13 @@ async def async_setup_entry( ) -> None: """Set up entry.""" - system = hass.data[DOMAIN] + system: LiteJet = hass.data[DOMAIN] - def get_entities(system): + def get_entities(system: LiteJet) -> list[LiteJetLight]: entities = [] - for i in system.loads(): - name = system.get_load_name(i) - entities.append(LiteJetLight(config_entry, system, i, name)) + for index in system.loads(): + name = system.get_load_name(index) + entities.append(LiteJetLight(config_entry, system, index, name)) return entities async_add_entities(await hass.async_add_executor_job(get_entities, system), True) @@ -43,16 +47,22 @@ class LiteJetLight(LightEntity): """Representation of a single LiteJet light.""" _attr_color_mode = ColorMode.BRIGHTNESS + _attr_should_poll = False _attr_supported_color_modes = {ColorMode.BRIGHTNESS} _attr_supported_features = LightEntityFeature.TRANSITION - def __init__(self, config_entry, lj, i, name): # pylint: disable=invalid-name + def __init__( + self, config_entry: ConfigEntry, litejet: LiteJet, index: int, name: str + ) -> None: """Initialize a LiteJet light.""" self._config_entry = config_entry - self._lj = lj - self._index = i - self._brightness = 0 - self._name = name + self._lj = litejet + self._index = index + self._attr_brightness = 0 + self._attr_is_on = False + self._attr_name = name + self._attr_unique_id = f"{config_entry.entry_id}_{index}" + self._attr_extra_state_attributes = {ATTR_NUMBER: self._index} async def async_added_to_hass(self) -> None: """Run when this Entity has been added to HA.""" @@ -63,41 +73,11 @@ class LiteJetLight(LightEntity): """Entity being removed from hass.""" self._lj.unsubscribe(self._on_load_changed) - def _on_load_changed(self): + def _on_load_changed(self) -> None: """Handle state changes.""" - _LOGGER.debug("Updating due to notification for %s", self._name) + _LOGGER.debug("Updating due to notification for %s", self.name) self.schedule_update_ha_state(True) - @property - def name(self): - """Return the light's name.""" - return self._name - - @property - def unique_id(self): - """Return a unique identifier for this light.""" - return f"{self._config_entry.entry_id}_{self._index}" - - @property - def brightness(self): - """Return the light's brightness.""" - return self._brightness - - @property - def is_on(self): - """Return if the light is on.""" - return self._brightness != 0 - - @property - def should_poll(self): - """Return that lights do not require polling.""" - return False - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - return {ATTR_NUMBER: self._index} - def turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" @@ -129,4 +109,5 @@ class LiteJetLight(LightEntity): def update(self) -> None: """Retrieve the light's brightness from the LiteJet system.""" - self._brightness = int(self._lj.get_load_level(self._index) / 99 * 255) + self._attr_brightness = int(self._lj.get_load_level(self._index) / 99 * 255) + self._attr_is_on = self.brightness != 0 From b3660901757d3a953cda3a7d70175ac5cb5d9cf8 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 5 Aug 2022 12:07:51 +0200 Subject: [PATCH 3164/3516] Allow creating fixable repairs issues without flows (#76224) * Allow creating fixable repairs issues without flows * Add test * Adjust test --- homeassistant/components/demo/__init__.py | 10 +++ homeassistant/components/demo/repairs.py | 9 ++- homeassistant/components/demo/strings.json | 13 +++- .../components/demo/translations/en.json | 13 +++- homeassistant/components/repairs/__init__.py | 2 + .../components/repairs/issue_handler.py | 37 +++++++--- tests/components/demo/test_init.py | 30 +++++++- .../components/repairs/test_websocket_api.py | 68 +++++++++++-------- 8 files changed, 140 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 3f0bb09cdbd..2e01e2c3c6b 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -211,6 +211,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: translation_key="unfixable_problem", ) + async_create_issue( + hass, + DOMAIN, + "bad_psu", + is_fixable=True, + learn_more_url="https://www.youtube.com/watch?v=b9rntRxLlbU", + severity=IssueSeverity.CRITICAL, + translation_key="bad_psu", + ) + return True diff --git a/homeassistant/components/demo/repairs.py b/homeassistant/components/demo/repairs.py index e5d31c18971..2d7c8b4cbcc 100644 --- a/homeassistant/components/demo/repairs.py +++ b/homeassistant/components/demo/repairs.py @@ -5,7 +5,7 @@ from __future__ import annotations import voluptuous as vol from homeassistant import data_entry_flow -from homeassistant.components.repairs import RepairsFlow +from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow class DemoFixFlow(RepairsFlow): @@ -23,11 +23,16 @@ class DemoFixFlow(RepairsFlow): ) -> data_entry_flow.FlowResult: """Handle the confirm step of a fix flow.""" if user_input is not None: - return self.async_create_entry(title="Fixed issue", data={}) + return self.async_create_entry(title="", data={}) return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) async def async_create_fix_flow(hass, issue_id): """Create flow.""" + if issue_id == "bad_psu": + # The bad_psu issue doesn't have its own flow + return ConfirmRepairFlow() + + # Other issues have a custom flow return DemoFixFlow() diff --git a/homeassistant/components/demo/strings.json b/homeassistant/components/demo/strings.json index a3a5b11f336..e02d64f157f 100644 --- a/homeassistant/components/demo/strings.json +++ b/homeassistant/components/demo/strings.json @@ -1,13 +1,24 @@ { "title": "Demo", "issues": { + "bad_psu": { + "title": "The power supply is not stable", + "fix_flow": { + "step": { + "confirm": { + "title": "The power supply needs to be replaced", + "description": "Press SUBMIT to confirm the power supply has been replaced" + } + } + } + }, "out_of_blinker_fluid": { "title": "The blinker fluid is empty and needs to be refilled", "fix_flow": { "step": { "confirm": { "title": "Blinker fluid needs to be refilled", - "description": "Press OK when blinker fluid has been refilled" + "description": "Press SUBMIT when blinker fluid has been refilled" } } } diff --git a/homeassistant/components/demo/translations/en.json b/homeassistant/components/demo/translations/en.json index 11378fb94d4..a98f0d3c28d 100644 --- a/homeassistant/components/demo/translations/en.json +++ b/homeassistant/components/demo/translations/en.json @@ -1,10 +1,21 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Press SUBMIT to confirm the power supply has been replaced", + "title": "The power supply needs to be replaced" + } + } + }, + "title": "The power supply is not stable" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { "confirm": { - "description": "Press OK when blinker fluid has been refilled", + "description": "Press SUBMIT when blinker fluid has been refilled", "title": "Blinker fluid needs to be refilled" } } diff --git a/homeassistant/components/repairs/__init__.py b/homeassistant/components/repairs/__init__.py index 4471def0dcd..5014baff834 100644 --- a/homeassistant/components/repairs/__init__.py +++ b/homeassistant/components/repairs/__init__.py @@ -7,6 +7,7 @@ from homeassistant.helpers.typing import ConfigType from . import issue_handler, websocket_api from .const import DOMAIN from .issue_handler import ( + ConfirmRepairFlow, async_create_issue, async_delete_issue, create_issue, @@ -21,6 +22,7 @@ __all__ = [ "create_issue", "delete_issue", "DOMAIN", + "ConfirmRepairFlow", "IssueSeverity", "RepairsFlow", ] diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index a08fff29598..23f37754ffe 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -5,6 +5,7 @@ import functools as ft from typing import Any from awesomeversion import AwesomeVersion, AwesomeVersionStrategy +import voluptuous as vol from homeassistant import data_entry_flow from homeassistant.core import HomeAssistant, callback @@ -19,6 +20,26 @@ from .issue_registry import async_get as async_get_issue_registry from .models import IssueSeverity, RepairsFlow, RepairsProtocol +class ConfirmRepairFlow(RepairsFlow): + """Handler for an issue fixing flow without any side effects.""" + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + + return await (self.async_step_confirm()) + + async def async_step_confirm( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the confirm step of a fix flow.""" + if user_input is not None: + return self.async_create_entry(title="", data={}) + + return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) + + class RepairsFlowManager(data_entry_flow.FlowManager): """Manage repairs flows.""" @@ -30,14 +51,6 @@ class RepairsFlowManager(data_entry_flow.FlowManager): data: dict[str, Any] | None = None, ) -> RepairsFlow: """Create a flow. platform is a repairs module.""" - if "platforms" not in self.hass.data[DOMAIN]: - await async_process_repairs_platforms(self.hass) - - platforms: dict[str, RepairsProtocol] = self.hass.data[DOMAIN]["platforms"] - if handler_key not in platforms: - raise data_entry_flow.UnknownHandler - platform = platforms[handler_key] - assert data and "issue_id" in data issue_id = data["issue_id"] @@ -46,6 +59,14 @@ class RepairsFlowManager(data_entry_flow.FlowManager): if issue is None or not issue.is_fixable: raise data_entry_flow.UnknownStep + if "platforms" not in self.hass.data[DOMAIN]: + await async_process_repairs_platforms(self.hass) + + platforms: dict[str, RepairsProtocol] = self.hass.data[DOMAIN]["platforms"] + if handler_key not in platforms: + return ConfirmRepairFlow() + platform = platforms[handler_key] + return await platform.async_create_fix_flow(self.hass, issue_id) async def async_finish_flow( diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 5b322cb776f..ba8baa0d487 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -129,6 +129,20 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "translation_key": "unfixable_problem", "translation_placeholders": None, }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "ignored": False, + "is_fixable": True, + "issue_domain": None, + "issue_id": "bad_psu", + "learn_more_url": "https://www.youtube.com/watch?v=b9rntRxLlbU", + "severity": "critical", + "translation_key": "bad_psu", + "translation_placeholders": None, + }, ] } @@ -164,7 +178,7 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "description_placeholders": None, "flow_id": flow_id, "handler": "demo", - "title": "Fixed issue", + "title": "", "type": "create_entry", "version": 1, } @@ -203,5 +217,19 @@ async def test_issues_created(hass, hass_client, hass_ws_client): "translation_key": "unfixable_problem", "translation_placeholders": None, }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": "demo", + "ignored": False, + "is_fixable": True, + "issue_domain": None, + "issue_id": "bad_psu", + "learn_more_url": "https://www.youtube.com/watch?v=b9rntRxLlbU", + "severity": "critical", + "translation_key": "bad_psu", + "translation_placeholders": None, + }, ] } diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index 359024f9fe5..a47a7a899ea 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -21,21 +21,24 @@ from homeassistant.setup import async_setup_component from tests.common import mock_platform +DEFAULT_ISSUES = [ + { + "breaks_in_ha_version": "2022.9", + "domain": "fake_integration", + "issue_id": "issue_1", + "is_fixable": True, + "learn_more_url": "https://theuselessweb.com", + "severity": "error", + "translation_key": "abc_123", + "translation_placeholders": {"abc": "123"}, + } +] -async def create_issues(hass, ws_client): + +async def create_issues(hass, ws_client, issues=None): """Create issues.""" - issues = [ - { - "breaks_in_ha_version": "2022.9", - "domain": "fake_integration", - "issue_id": "issue_1", - "is_fixable": True, - "learn_more_url": "https://theuselessweb.com", - "severity": "error", - "translation_key": "abc_123", - "translation_placeholders": {"abc": "123"}, - }, - ] + if issues is None: + issues = DEFAULT_ISSUES for issue in issues: async_create_issue( @@ -79,23 +82,22 @@ class MockFixFlow(RepairsFlow): ) -> data_entry_flow.FlowResult: """Handle the first step of a fix flow.""" - return await (self.async_step_confirm()) + return await (self.async_step_custom_step()) - async def async_step_confirm( + async def async_step_custom_step( self, user_input: dict[str, str] | None = None ) -> data_entry_flow.FlowResult: - """Handle the confirm step of a fix flow.""" + """Handle a custom_step step of a fix flow.""" if user_input is not None: - return self.async_create_entry(title=None, data=None) + return self.async_create_entry(title="", data={}) - return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) + return self.async_show_form(step_id="custom_step", data_schema=vol.Schema({})) @pytest.fixture(autouse=True) async def mock_repairs_integration(hass): """Mock a repairs integration.""" hass.config.components.add("fake_integration") - hass.config.components.add("integration_without_diagnostics") def async_create_fix_flow(hass, issue_id): return MockFixFlow() @@ -107,7 +109,7 @@ async def mock_repairs_integration(hass): ) mock_platform( hass, - "integration_without_diagnostics.repairs", + "integration_without_repairs.repairs", Mock(spec=[]), ) @@ -237,7 +239,16 @@ async def test_fix_non_existing_issue( } -async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> None: +@pytest.mark.parametrize( + "domain, step", + ( + ("fake_integration", "custom_step"), + ("fake_integration_default_handler", "confirm"), + ), +) +async def test_fix_issue( + hass: HomeAssistant, hass_client, hass_ws_client, domain, step +) -> None: """Test we can fix an issue.""" assert await async_setup_component(hass, "http", {}) assert await async_setup_component(hass, DOMAIN, {}) @@ -245,12 +256,11 @@ async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> No ws_client = await hass_ws_client(hass) client = await hass_client() - await create_issues(hass, ws_client) + issues = [{**DEFAULT_ISSUES[0], "domain": domain}] + await create_issues(hass, ws_client, issues=issues) url = "/api/repairs/issues/fix" - resp = await client.post( - url, json={"handler": "fake_integration", "issue_id": "issue_1"} - ) + resp = await client.post(url, json={"handler": domain, "issue_id": "issue_1"}) assert resp.status == HTTPStatus.OK data = await resp.json() @@ -261,9 +271,9 @@ async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> No "description_placeholders": None, "errors": None, "flow_id": ANY, - "handler": "fake_integration", + "handler": domain, "last_step": None, - "step_id": "confirm", + "step_id": step, "type": "form", } @@ -286,8 +296,8 @@ async def test_fix_issue(hass: HomeAssistant, hass_client, hass_ws_client) -> No "description": None, "description_placeholders": None, "flow_id": flow_id, - "handler": "fake_integration", - "title": None, + "handler": domain, + "title": "", "type": "create_entry", "version": 1, } From 9aa88384794186d46a07f14201329b58cd5f267b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 5 Aug 2022 13:16:29 +0200 Subject: [PATCH 3165/3516] Allow storing arbitrary data in repairs issues (#76288) --- homeassistant/components/demo/repairs.py | 7 ++- .../components/flunearyou/repairs.py | 6 ++- .../components/repairs/issue_handler.py | 14 ++++-- .../components/repairs/issue_registry.py | 7 +++ homeassistant/components/repairs/models.py | 8 ++- .../components/repairs/websocket_api.py | 3 +- .../components/repairs/test_issue_registry.py | 4 ++ .../components/repairs/test_websocket_api.py | 50 +++++++++++++------ 8 files changed, 76 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/demo/repairs.py b/homeassistant/components/demo/repairs.py index 2d7c8b4cbcc..cddc937a71a 100644 --- a/homeassistant/components/demo/repairs.py +++ b/homeassistant/components/demo/repairs.py @@ -6,6 +6,7 @@ import voluptuous as vol from homeassistant import data_entry_flow from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow +from homeassistant.core import HomeAssistant class DemoFixFlow(RepairsFlow): @@ -28,7 +29,11 @@ class DemoFixFlow(RepairsFlow): return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) -async def async_create_fix_flow(hass, issue_id): +async def async_create_fix_flow( + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None, +) -> RepairsFlow: """Create flow.""" if issue_id == "bad_psu": # The bad_psu issue doesn't have its own flow diff --git a/homeassistant/components/flunearyou/repairs.py b/homeassistant/components/flunearyou/repairs.py index f48085ba623..df81a1ae576 100644 --- a/homeassistant/components/flunearyou/repairs.py +++ b/homeassistant/components/flunearyou/repairs.py @@ -36,7 +36,9 @@ class FluNearYouFixFlow(RepairsFlow): async def async_create_fix_flow( - hass: HomeAssistant, issue_id: str -) -> FluNearYouFixFlow: + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None, +) -> RepairsFlow: """Create flow.""" return FluNearYouFixFlow() diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index 23f37754ffe..5695e99998b 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -64,10 +64,14 @@ class RepairsFlowManager(data_entry_flow.FlowManager): platforms: dict[str, RepairsProtocol] = self.hass.data[DOMAIN]["platforms"] if handler_key not in platforms: - return ConfirmRepairFlow() - platform = platforms[handler_key] + flow: RepairsFlow = ConfirmRepairFlow() + else: + platform = platforms[handler_key] + flow = await platform.async_create_fix_flow(self.hass, issue_id, issue.data) - return await platform.async_create_fix_flow(self.hass, issue_id) + flow.issue_id = issue_id + flow.data = issue.data + return flow async def async_finish_flow( self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult @@ -109,6 +113,7 @@ def async_create_issue( *, issue_domain: str | None = None, breaks_in_ha_version: str | None = None, + data: dict[str, str | int | float | None] | None = None, is_fixable: bool, is_persistent: bool = False, learn_more_url: str | None = None, @@ -131,6 +136,7 @@ def async_create_issue( issue_id, issue_domain=issue_domain, breaks_in_ha_version=breaks_in_ha_version, + data=data, is_fixable=is_fixable, is_persistent=is_persistent, learn_more_url=learn_more_url, @@ -146,6 +152,7 @@ def create_issue( issue_id: str, *, breaks_in_ha_version: str | None = None, + data: dict[str, str | int | float | None] | None = None, is_fixable: bool, is_persistent: bool = False, learn_more_url: str | None = None, @@ -162,6 +169,7 @@ def create_issue( domain, issue_id, breaks_in_ha_version=breaks_in_ha_version, + data=data, is_fixable=is_fixable, is_persistent=is_persistent, learn_more_url=learn_more_url, diff --git a/homeassistant/components/repairs/issue_registry.py b/homeassistant/components/repairs/issue_registry.py index c7502ecf397..f9a15e0f165 100644 --- a/homeassistant/components/repairs/issue_registry.py +++ b/homeassistant/components/repairs/issue_registry.py @@ -27,6 +27,7 @@ class IssueEntry: active: bool breaks_in_ha_version: str | None created: datetime + data: dict[str, str | int | float | None] | None dismissed_version: str | None domain: str is_fixable: bool | None @@ -53,6 +54,7 @@ class IssueEntry: return { **result, "breaks_in_ha_version": self.breaks_in_ha_version, + "data": self.data, "is_fixable": self.is_fixable, "is_persistent": True, "issue_domain": self.issue_domain, @@ -106,6 +108,7 @@ class IssueRegistry: *, issue_domain: str | None = None, breaks_in_ha_version: str | None = None, + data: dict[str, str | int | float | None] | None = None, is_fixable: bool, is_persistent: bool, learn_more_url: str | None = None, @@ -120,6 +123,7 @@ class IssueRegistry: active=True, breaks_in_ha_version=breaks_in_ha_version, created=dt_util.utcnow(), + data=data, dismissed_version=None, domain=domain, is_fixable=is_fixable, @@ -142,6 +146,7 @@ class IssueRegistry: issue, active=True, breaks_in_ha_version=breaks_in_ha_version, + data=data, is_fixable=is_fixable, is_persistent=is_persistent, issue_domain=issue_domain, @@ -204,6 +209,7 @@ class IssueRegistry: active=True, breaks_in_ha_version=issue["breaks_in_ha_version"], created=created, + data=issue["data"], dismissed_version=issue["dismissed_version"], domain=issue["domain"], is_fixable=issue["is_fixable"], @@ -220,6 +226,7 @@ class IssueRegistry: active=False, breaks_in_ha_version=None, created=created, + data=None, dismissed_version=issue["dismissed_version"], domain=issue["domain"], is_fixable=None, diff --git a/homeassistant/components/repairs/models.py b/homeassistant/components/repairs/models.py index 2a6eeb15269..1022c50e1f2 100644 --- a/homeassistant/components/repairs/models.py +++ b/homeassistant/components/repairs/models.py @@ -19,11 +19,17 @@ class IssueSeverity(StrEnum): class RepairsFlow(data_entry_flow.FlowHandler): """Handle a flow for fixing an issue.""" + issue_id: str + data: dict[str, str | int | float | None] | None + class RepairsProtocol(Protocol): """Define the format of repairs platforms.""" async def async_create_fix_flow( - self, hass: HomeAssistant, issue_id: str + self, + hass: HomeAssistant, + issue_id: str, + data: dict[str, str | int | float | None] | None, ) -> RepairsFlow: """Create a flow to fix a fixable issue.""" diff --git a/homeassistant/components/repairs/websocket_api.py b/homeassistant/components/repairs/websocket_api.py index ff0ac5ba8f9..192c9f5ac66 100644 --- a/homeassistant/components/repairs/websocket_api.py +++ b/homeassistant/components/repairs/websocket_api.py @@ -64,7 +64,8 @@ def ws_list_issues( """Return a list of issues.""" def ws_dict(kv_pairs: list[tuple[Any, Any]]) -> dict[Any, Any]: - result = {k: v for k, v in kv_pairs if k not in ("active", "is_persistent")} + excluded_keys = ("active", "data", "is_persistent") + result = {k: v for k, v in kv_pairs if k not in excluded_keys} result["ignored"] = result["dismissed_version"] is not None result["created"] = result["created"].isoformat() return result diff --git a/tests/components/repairs/test_issue_registry.py b/tests/components/repairs/test_issue_registry.py index ff6c4b996da..76faafce1c7 100644 --- a/tests/components/repairs/test_issue_registry.py +++ b/tests/components/repairs/test_issue_registry.py @@ -51,6 +51,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: }, { "breaks_in_ha_version": "2022.6", + "data": {"entry_id": "123"}, "domain": "test", "issue_id": "issue_4", "is_fixable": True, @@ -141,6 +142,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: active=False, breaks_in_ha_version=None, created=issue1.created, + data=None, dismissed_version=issue1.dismissed_version, domain=issue1.domain, is_fixable=None, @@ -157,6 +159,7 @@ async def test_load_issues(hass: HomeAssistant) -> None: active=False, breaks_in_ha_version=None, created=issue2.created, + data=None, dismissed_version=issue2.dismissed_version, domain=issue2.domain, is_fixable=None, @@ -196,6 +199,7 @@ async def test_loading_issues_from_storage(hass: HomeAssistant, hass_storage) -> { "breaks_in_ha_version": "2022.6", "created": "2022-07-19T19:41:13.746514+00:00", + "data": {"entry_id": "123"}, "dismissed_version": None, "domain": "test", "issue_domain": "blubb", diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index a47a7a899ea..1cb83d81b06 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -37,6 +37,17 @@ DEFAULT_ISSUES = [ async def create_issues(hass, ws_client, issues=None): """Create issues.""" + + def api_issue(issue): + excluded_keys = ("data",) + return dict( + {key: issue[key] for key in issue if key not in excluded_keys}, + created=ANY, + dismissed_version=None, + ignored=False, + issue_domain=None, + ) + if issues is None: issues = DEFAULT_ISSUES @@ -46,6 +57,7 @@ async def create_issues(hass, ws_client, issues=None): issue["domain"], issue["issue_id"], breaks_in_ha_version=issue["breaks_in_ha_version"], + data=issue.get("data"), is_fixable=issue["is_fixable"], is_persistent=False, learn_more_url=issue["learn_more_url"], @@ -58,22 +70,17 @@ async def create_issues(hass, ws_client, issues=None): msg = await ws_client.receive_json() assert msg["success"] - assert msg["result"] == { - "issues": [ - dict( - issue, - created=ANY, - dismissed_version=None, - ignored=False, - issue_domain=None, - ) - for issue in issues - ] - } + assert msg["result"] == {"issues": [api_issue(issue) for issue in issues]} return issues +EXPECTED_DATA = { + "issue_1": None, + "issue_2": {"blah": "bleh"}, +} + + class MockFixFlow(RepairsFlow): """Handler for an issue fixing flow.""" @@ -82,6 +89,9 @@ class MockFixFlow(RepairsFlow): ) -> data_entry_flow.FlowResult: """Handle the first step of a fix flow.""" + assert self.issue_id in EXPECTED_DATA + assert self.data == EXPECTED_DATA[self.issue_id] + return await (self.async_step_custom_step()) async def async_step_custom_step( @@ -99,7 +109,10 @@ async def mock_repairs_integration(hass): """Mock a repairs integration.""" hass.config.components.add("fake_integration") - def async_create_fix_flow(hass, issue_id): + def async_create_fix_flow(hass, issue_id, data): + assert issue_id in EXPECTED_DATA + assert data == EXPECTED_DATA[issue_id] + return MockFixFlow() mock_platform( @@ -256,11 +269,18 @@ async def test_fix_issue( ws_client = await hass_ws_client(hass) client = await hass_client() - issues = [{**DEFAULT_ISSUES[0], "domain": domain}] + issues = [ + { + **DEFAULT_ISSUES[0], + "data": {"blah": "bleh"}, + "domain": domain, + "issue_id": "issue_2", + } + ] await create_issues(hass, ws_client, issues=issues) url = "/api/repairs/issues/fix" - resp = await client.post(url, json={"handler": domain, "issue_id": "issue_1"}) + resp = await client.post(url, json={"handler": domain, "issue_id": "issue_2"}) assert resp.status == HTTPStatus.OK data = await resp.json() From 741efb89d50fab37e8ed27301543f11cbc559f39 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 5 Aug 2022 13:17:46 +0200 Subject: [PATCH 3166/3516] Remove deprecated `send_if_off` option for MQTT climate (#76293) * Remove `send_if_off` option for mqtt climate * Use cv.remove() --- homeassistant/components/mqtt/climate.py | 38 +++----- tests/components/mqtt/test_climate.py | 116 ----------------------- 2 files changed, 11 insertions(+), 143 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 30263798740..bf53544b491 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -99,7 +99,7 @@ CONF_PRESET_MODE_COMMAND_TOPIC = "preset_mode_command_topic" CONF_PRESET_MODE_VALUE_TEMPLATE = "preset_mode_value_template" CONF_PRESET_MODE_COMMAND_TEMPLATE = "preset_mode_command_template" CONF_PRESET_MODES_LIST = "preset_modes" -# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 +# Support CONF_SEND_IF_OFF is removed with release 2022.9 CONF_SEND_IF_OFF = "send_if_off" CONF_SWING_MODE_COMMAND_TEMPLATE = "swing_mode_command_template" CONF_SWING_MODE_COMMAND_TOPIC = "swing_mode_command_topic" @@ -284,8 +284,6 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] ), vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - vol.Optional(CONF_SEND_IF_OFF): cv.boolean, vol.Optional(CONF_ACTION_TEMPLATE): cv.template, vol.Optional(CONF_ACTION_TOPIC): valid_subscribe_topic, # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together @@ -334,8 +332,8 @@ PLATFORM_SCHEMA_MODERN = vol.All( # Configuring MQTT Climate under the climate platform key is deprecated in HA Core 2022.6 PLATFORM_SCHEMA = vol.All( cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - cv.deprecated(CONF_SEND_IF_OFF), + # Support CONF_SEND_IF_OFF is removed with release 2022.9 + cv.removed(CONF_SEND_IF_OFF), # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 cv.deprecated(CONF_AWAY_MODE_COMMAND_TOPIC), cv.deprecated(CONF_AWAY_MODE_STATE_TEMPLATE), @@ -353,8 +351,8 @@ _DISCOVERY_SCHEMA_BASE = _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA DISCOVERY_SCHEMA = vol.All( _DISCOVERY_SCHEMA_BASE, - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - cv.deprecated(CONF_SEND_IF_OFF), + # Support CONF_SEND_IF_OFF is removed with release 2022.9 + cv.removed(CONF_SEND_IF_OFF), # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 cv.deprecated(CONF_AWAY_MODE_COMMAND_TOPIC), cv.deprecated(CONF_AWAY_MODE_STATE_TEMPLATE), @@ -437,8 +435,6 @@ class MqttClimate(MqttEntity, ClimateEntity): self._feature_preset_mode = False self._optimistic_preset_mode = None - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - self._send_if_off = True # AWAY and HOLD mode topics and templates are deprecated, # support will be removed with release 2022.9 self._hold_list = [] @@ -511,10 +507,6 @@ class MqttClimate(MqttEntity, ClimateEntity): self._command_templates = command_templates - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if CONF_SEND_IF_OFF in config: - self._send_if_off = config[CONF_SEND_IF_OFF] - # AWAY and HOLD mode topics and templates are deprecated, # support will be removed with release 2022.9 if CONF_HOLD_LIST in config: @@ -871,10 +863,8 @@ class MqttClimate(MqttEntity, ClimateEntity): # optimistic mode setattr(self, attr, temp) - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if self._send_if_off or self._current_operation != HVACMode.OFF: - payload = self._command_templates[cmnd_template](temp) - await self._publish(cmnd_topic, payload) + payload = self._command_templates[cmnd_template](temp) + await self._publish(cmnd_topic, payload) async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" @@ -910,12 +900,8 @@ class MqttClimate(MqttEntity, ClimateEntity): async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new swing mode.""" - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if self._send_if_off or self._current_operation != HVACMode.OFF: - payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE]( - swing_mode - ) - await self._publish(CONF_SWING_MODE_COMMAND_TOPIC, payload) + payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE](swing_mode) + await self._publish(CONF_SWING_MODE_COMMAND_TOPIC, payload) if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: self._current_swing_mode = swing_mode @@ -923,10 +909,8 @@ class MqttClimate(MqttEntity, ClimateEntity): async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target temperature.""" - # CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 - if self._send_if_off or self._current_operation != HVACMode.OFF: - payload = self._command_templates[CONF_FAN_MODE_COMMAND_TEMPLATE](fan_mode) - await self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload) + payload = self._command_templates[CONF_FAN_MODE_COMMAND_TEMPLATE](fan_mode) + await self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload) if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: self._current_fan_mode = fan_mode diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index aec83a85227..679f853a3a8 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -349,44 +349,6 @@ async def test_set_fan_mode(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("fan_mode") == "high" -# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 -@pytest.mark.parametrize( - "send_if_off,assert_async_publish", - [ - ({}, [call("fan-mode-topic", "low", 0, False)]), - ({"send_if_off": True}, [call("fan-mode-topic", "low", 0, False)]), - ({"send_if_off": False}, []), - ], -) -async def test_set_fan_mode_send_if_off( - hass, mqtt_mock_entry_with_yaml_config, send_if_off, assert_async_publish -): - """Test setting of fan mode if the hvac is off.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config[CLIMATE_DOMAIN].update(send_if_off) - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(ENTITY_CLIMATE) is not None - - # Turn on HVAC - await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) - mqtt_mock.async_publish.reset_mock() - # Updates for fan_mode should be sent when the device is turned on - await common.async_set_fan_mode(hass, "high", ENTITY_CLIMATE) - mqtt_mock.async_publish.assert_called_once_with("fan-mode-topic", "high", 0, False) - - # Turn off HVAC - await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE) - state = hass.states.get(ENTITY_CLIMATE) - assert state.state == "off" - - # Updates for fan_mode should be sent if SEND_IF_OFF is not set or is True - mqtt_mock.async_publish.reset_mock() - await common.async_set_fan_mode(hass, "low", ENTITY_CLIMATE) - mqtt_mock.async_publish.assert_has_calls(assert_async_publish) - - async def test_set_swing_mode_bad_attr(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test setting swing mode without required attribute.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) @@ -442,44 +404,6 @@ async def test_set_swing(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("swing_mode") == "on" -# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 -@pytest.mark.parametrize( - "send_if_off,assert_async_publish", - [ - ({}, [call("swing-mode-topic", "on", 0, False)]), - ({"send_if_off": True}, [call("swing-mode-topic", "on", 0, False)]), - ({"send_if_off": False}, []), - ], -) -async def test_set_swing_mode_send_if_off( - hass, mqtt_mock_entry_with_yaml_config, send_if_off, assert_async_publish -): - """Test setting of swing mode if the hvac is off.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config[CLIMATE_DOMAIN].update(send_if_off) - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(ENTITY_CLIMATE) is not None - - # Turn on HVAC - await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) - mqtt_mock.async_publish.reset_mock() - # Updates for swing_mode should be sent when the device is turned on - await common.async_set_swing_mode(hass, "off", ENTITY_CLIMATE) - mqtt_mock.async_publish.assert_called_once_with("swing-mode-topic", "off", 0, False) - - # Turn off HVAC - await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE) - state = hass.states.get(ENTITY_CLIMATE) - assert state.state == "off" - - # Updates for swing_mode should be sent if SEND_IF_OFF is not set or is True - mqtt_mock.async_publish.reset_mock() - await common.async_set_swing_mode(hass, "on", ENTITY_CLIMATE) - mqtt_mock.async_publish.assert_has_calls(assert_async_publish) - - async def test_set_target_temperature(hass, mqtt_mock_entry_with_yaml_config): """Test setting the target temperature.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) @@ -517,46 +441,6 @@ async def test_set_target_temperature(hass, mqtt_mock_entry_with_yaml_config): mqtt_mock.async_publish.reset_mock() -# CONF_SEND_IF_OFF is deprecated, support will be removed with release 2022.9 -@pytest.mark.parametrize( - "send_if_off,assert_async_publish", - [ - ({}, [call("temperature-topic", "21.0", 0, False)]), - ({"send_if_off": True}, [call("temperature-topic", "21.0", 0, False)]), - ({"send_if_off": False}, []), - ], -) -async def test_set_target_temperature_send_if_off( - hass, mqtt_mock_entry_with_yaml_config, send_if_off, assert_async_publish -): - """Test setting of target temperature if the hvac is off.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config[CLIMATE_DOMAIN].update(send_if_off) - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - assert hass.states.get(ENTITY_CLIMATE) is not None - - # Turn on HVAC - await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) - mqtt_mock.async_publish.reset_mock() - # Updates for target temperature should be sent when the device is turned on - await common.async_set_temperature(hass, 16.0, ENTITY_CLIMATE) - mqtt_mock.async_publish.assert_called_once_with( - "temperature-topic", "16.0", 0, False - ) - - # Turn off HVAC - await common.async_set_hvac_mode(hass, "off", ENTITY_CLIMATE) - state = hass.states.get(ENTITY_CLIMATE) - assert state.state == "off" - - # Updates for target temperature sent should be if SEND_IF_OFF is not set or is True - mqtt_mock.async_publish.reset_mock() - await common.async_set_temperature(hass, 21.0, ENTITY_CLIMATE) - mqtt_mock.async_publish.assert_has_calls(assert_async_publish) - - async def test_set_target_temperature_pessimistic( hass, mqtt_mock_entry_with_yaml_config ): From cdde4f9925665fde2ad41fba8830878eb4e3b20b Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 5 Aug 2022 14:49:34 +0200 Subject: [PATCH 3167/3516] Add bluetooth API to allow rediscovery of address (#76005) * Add API to allow rediscovery of domains * Switch to clearing per device * Drop unneded change --- .../components/bluetooth/__init__.py | 12 ++++++ homeassistant/components/bluetooth/match.py | 10 +++-- .../components/fjaraskupan/__init__.py | 10 +++++ tests/components/bluetooth/test_init.py | 40 +++++++++++++++++++ 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 0b81472f838..ed04ca401ed 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -214,6 +214,13 @@ def async_track_unavailable( return manager.async_track_unavailable(callback, address) +@hass_callback +def async_rediscover_address(hass: HomeAssistant, address: str) -> None: + """Trigger discovery of devices which have already been seen.""" + manager: BluetoothManager = hass.data[DOMAIN] + manager.async_rediscover_address(address) + + async def _async_has_bluetooth_adapter() -> bool: """Return if the device has a bluetooth adapter.""" return bool(await async_get_bluetooth_adapters()) @@ -545,3 +552,8 @@ class BluetoothManager: # change the bluetooth dongle. _LOGGER.error("Error stopping scanner: %s", ex) uninstall_multiple_bleak_catcher() + + @hass_callback + def async_rediscover_address(self, address: str) -> None: + """Trigger discovery of devices which have already been seen.""" + self._integration_matcher.async_clear_address(address) diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 2cd4f62ae5e..9c942d9f411 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -10,7 +10,7 @@ from lru import LRU # pylint: disable=no-name-in-module from homeassistant.loader import BluetoothMatcher, BluetoothMatcherOptional if TYPE_CHECKING: - from collections.abc import Mapping + from collections.abc import MutableMapping from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData @@ -70,7 +70,7 @@ class IntegrationMatcher: self._integration_matchers = integration_matchers # Some devices use a random address so we need to use # an LRU to avoid memory issues. - self._matched: Mapping[str, IntegrationMatchHistory] = LRU( + self._matched: MutableMapping[str, IntegrationMatchHistory] = LRU( MAX_REMEMBER_ADDRESSES ) @@ -78,6 +78,10 @@ class IntegrationMatcher: """Clear the history.""" self._matched = {} + def async_clear_address(self, address: str) -> None: + """Clear the history matches for a set of domains.""" + self._matched.pop(address, None) + def match_domains(self, device: BLEDevice, adv_data: AdvertisementData) -> set[str]: """Return the domains that are matched.""" matched_domains: set[str] = set() @@ -98,7 +102,7 @@ class IntegrationMatcher: previous_match.service_data |= bool(adv_data.service_data) previous_match.service_uuids |= bool(adv_data.service_uuids) else: - self._matched[device.address] = IntegrationMatchHistory( # type: ignore[index] + self._matched[device.address] = IntegrationMatchHistory( manufacturer_data=bool(adv_data.manufacturer_data), service_data=bool(adv_data.service_data), service_uuids=bool(adv_data.service_uuids), diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index fbd2f13d2b4..28032b3f997 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -14,11 +14,13 @@ from homeassistant.components.bluetooth import ( BluetoothScanningMode, BluetoothServiceInfoBleak, async_address_present, + async_rediscover_address, async_register_callback, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -113,6 +115,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device = Device(service_info.device) device_info = DeviceInfo( + connections={(dr.CONNECTION_BLUETOOTH, service_info.address)}, identifiers={(DOMAIN, service_info.address)}, manufacturer="Fjäråskupan", name="Fjäråskupan", @@ -175,4 +178,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) + for device_entry in dr.async_entries_for_config_entry( + dr.async_get(hass), entry.entry_id + ): + for conn in device_entry.connections: + if conn[0] == dr.CONNECTION_BLUETOOTH: + async_rediscover_address(hass, conn[1]) + return unload_ok diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index ba315b1f380..6cd22505dc4 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -16,6 +16,7 @@ from homeassistant.components.bluetooth import ( BluetoothScanningMode, BluetoothServiceInfo, async_process_advertisements, + async_rediscover_address, async_track_unavailable, models, ) @@ -560,6 +561,45 @@ async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( assert len(mock_config_flow.mock_calls) == 0 +async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth): + """Test bluetooth discovery can be re-enabled for a given domain.""" + mock_bt = [ + {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} + ] + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] + ) + + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "switchbot" + + async_rediscover_address(hass, "44:44:33:11:23:45") + + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(mock_config_flow.mock_calls) == 2 + assert mock_config_flow.mock_calls[1][1][0] == "switchbot" + + async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): """Test the async_discovered_device API.""" mock_bt = [] From df67a8cd4f8df91a153778009a74be1e3876ca53 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 5 Aug 2022 09:34:21 -0400 Subject: [PATCH 3168/3516] Fix ZHA light color temp support (#76305) --- .../components/zha/core/channels/lighting.py | 39 +++++++------------ 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index 36bb0beb17d..1754b9aff68 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -1,7 +1,7 @@ """Lighting channels module for Zigbee Home Automation.""" from __future__ import annotations -from contextlib import suppress +from functools import cached_property from zigpy.zcl.clusters import lighting @@ -46,17 +46,8 @@ class ColorChannel(ZigbeeChannel): "color_loop_active": False, } - @property - def color_capabilities(self) -> int: - """Return color capabilities of the light.""" - with suppress(KeyError): - return self.cluster["color_capabilities"] - if self.cluster.get("color_temperature") is not None: - return self.CAPABILITIES_COLOR_XY | self.CAPABILITIES_COLOR_TEMP - return self.CAPABILITIES_COLOR_XY - - @property - def zcl_color_capabilities(self) -> lighting.Color.ColorCapabilities: + @cached_property + def color_capabilities(self) -> lighting.Color.ColorCapabilities: """Return ZCL color capabilities of the light.""" color_capabilities = self.cluster.get("color_capabilities") if color_capabilities is None: @@ -117,43 +108,41 @@ class ColorChannel(ZigbeeChannel): def hs_supported(self) -> bool: """Return True if the channel supports hue and saturation.""" return ( - self.zcl_color_capabilities is not None + self.color_capabilities is not None and lighting.Color.ColorCapabilities.Hue_and_saturation - in self.zcl_color_capabilities + in self.color_capabilities ) @property def enhanced_hue_supported(self) -> bool: """Return True if the channel supports enhanced hue and saturation.""" return ( - self.zcl_color_capabilities is not None - and lighting.Color.ColorCapabilities.Enhanced_hue - in self.zcl_color_capabilities + self.color_capabilities is not None + and lighting.Color.ColorCapabilities.Enhanced_hue in self.color_capabilities ) @property def xy_supported(self) -> bool: """Return True if the channel supports xy.""" return ( - self.zcl_color_capabilities is not None + self.color_capabilities is not None and lighting.Color.ColorCapabilities.XY_attributes - in self.zcl_color_capabilities + in self.color_capabilities ) @property def color_temp_supported(self) -> bool: """Return True if the channel supports color temperature.""" return ( - self.zcl_color_capabilities is not None + self.color_capabilities is not None and lighting.Color.ColorCapabilities.Color_temperature - in self.zcl_color_capabilities - ) + in self.color_capabilities + ) or self.color_temperature is not None @property def color_loop_supported(self) -> bool: """Return True if the channel supports color loop.""" return ( - self.zcl_color_capabilities is not None - and lighting.Color.ColorCapabilities.Color_loop - in self.zcl_color_capabilities + self.color_capabilities is not None + and lighting.Color.ColorCapabilities.Color_loop in self.color_capabilities ) From a0ef3ad21b88b2c4c692682afaf64b493d96f682 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Fri, 5 Aug 2022 16:06:19 +0200 Subject: [PATCH 3169/3516] Use stored philips_js system data on start (#75981) Co-authored-by: Martin Hjelmare --- homeassistant/components/philips_js/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 9e574e69f90..24b3f9a91e0 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -38,15 +38,22 @@ LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Philips TV from a config entry.""" + system: SystemType | None = entry.data.get(CONF_SYSTEM) tvapi = PhilipsTV( entry.data[CONF_HOST], entry.data[CONF_API_VERSION], username=entry.data.get(CONF_USERNAME), password=entry.data.get(CONF_PASSWORD), + system=system, ) coordinator = PhilipsTVDataUpdateCoordinator(hass, tvapi, entry.options) await coordinator.async_refresh() + + if (actual_system := tvapi.system) and actual_system != system: + data = {**entry.data, CONF_SYSTEM: actual_system} + hass.config_entries.async_update_entry(entry, data=data) + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator From 742877f79b47b7805d90e4b17cf3e168fc3308ce Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 5 Aug 2022 16:28:52 +0200 Subject: [PATCH 3170/3516] Revert "Disable Spotify Media Player entity by default (#69372)" (#76250) --- homeassistant/components/spotify/media_player.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 04f523c2d4b..ea41067e7e9 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -40,7 +40,7 @@ from .util import fetch_image_url _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=1) +SCAN_INTERVAL = timedelta(seconds=30) SUPPORT_SPOTIFY = ( MediaPlayerEntityFeature.BROWSE_MEDIA @@ -107,7 +107,6 @@ def spotify_exception_handler(func): class SpotifyMediaPlayer(MediaPlayerEntity): """Representation of a Spotify controller.""" - _attr_entity_registry_enabled_default = False _attr_has_entity_name = True _attr_icon = "mdi:spotify" _attr_media_content_type = MEDIA_TYPE_MUSIC From c2f026d0a7feb801f36a04f26fe29996d73c20be Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 6 Aug 2022 01:34:27 +0200 Subject: [PATCH 3171/3516] Minor deCONZ clean up (#76323) * Rename secondary_temperature with internal_temperature * Prefix binary and sensor descriptions matching on all sensor devices with COMMON_ * Always create entities in the same order Its been reported previously that if the integration is removed and setup again that entity IDs can change if not sorted in the numerical order * Rename alarmsystems to alarm_systems * Use websocket enums * Don't use legacy pydeconz constants * Bump pydeconz to v103 * unsub -> unsubscribe --- .../components/deconz/alarm_control_panel.py | 10 +++++----- .../components/deconz/binary_sensor.py | 9 +++++---- .../components/deconz/diagnostics.py | 4 ++-- homeassistant/components/deconz/gateway.py | 2 +- homeassistant/components/deconz/manifest.json | 2 +- homeassistant/components/deconz/number.py | 4 ++-- homeassistant/components/deconz/sensor.py | 20 ++++++++++--------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/conftest.py | 6 +++--- tests/components/deconz/test_diagnostics.py | 6 +++--- tests/components/deconz/test_gateway.py | 6 +++--- 12 files changed, 38 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/deconz/alarm_control_panel.py b/homeassistant/components/deconz/alarm_control_panel.py index e1fb0757b12..59b4b9e4f8e 100644 --- a/homeassistant/components/deconz/alarm_control_panel.py +++ b/homeassistant/components/deconz/alarm_control_panel.py @@ -48,7 +48,7 @@ def get_alarm_system_id_for_unique_id( gateway: DeconzGateway, unique_id: str ) -> str | None: """Retrieve alarm system ID the unique ID is registered to.""" - for alarm_system in gateway.api.alarmsystems.values(): + for alarm_system in gateway.api.alarm_systems.values(): if unique_id in alarm_system.devices: return alarm_system.resource_id return None @@ -122,27 +122,27 @@ class DeconzAlarmControlPanel(DeconzDevice[AncillaryControl], AlarmControlPanelE async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" if code: - await self.gateway.api.alarmsystems.arm( + await self.gateway.api.alarm_systems.arm( self.alarm_system_id, AlarmSystemArmAction.AWAY, code ) async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" if code: - await self.gateway.api.alarmsystems.arm( + await self.gateway.api.alarm_systems.arm( self.alarm_system_id, AlarmSystemArmAction.STAY, code ) async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" if code: - await self.gateway.api.alarmsystems.arm( + await self.gateway.api.alarm_systems.arm( self.alarm_system_id, AlarmSystemArmAction.NIGHT, code ) async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" if code: - await self.gateway.api.alarmsystems.arm( + await self.gateway.api.alarm_systems.arm( self.alarm_system_id, AlarmSystemArmAction.DISARM, code ) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index a7dbc2eacff..08cb8753bb6 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -159,7 +159,7 @@ ENTITY_DESCRIPTIONS = { ], } -BINARY_SENSOR_DESCRIPTIONS = [ +COMMON_BINARY_SENSOR_DESCRIPTIONS = [ DeconzBinarySensorDescription( key="tampered", value_fn=lambda device: device.tampered, @@ -215,7 +215,8 @@ async def async_setup_entry( sensor = gateway.api.sensors[sensor_id] for description in ( - ENTITY_DESCRIPTIONS.get(type(sensor), []) + BINARY_SENSOR_DESCRIPTIONS + ENTITY_DESCRIPTIONS.get(type(sensor), []) + + COMMON_BINARY_SENSOR_DESCRIPTIONS ): if ( not hasattr(sensor, description.key) @@ -283,8 +284,8 @@ class DeconzBinarySensor(DeconzDevice[SensorResources], BinarySensorEntity): if self._device.on is not None: attr[ATTR_ON] = self._device.on - if self._device.secondary_temperature is not None: - attr[ATTR_TEMPERATURE] = self._device.secondary_temperature + if self._device.internal_temperature is not None: + attr[ATTR_TEMPERATURE] = self._device.internal_temperature if isinstance(self._device, Presence): diff --git a/homeassistant/components/deconz/diagnostics.py b/homeassistant/components/deconz/diagnostics.py index 11854421512..5b7986fc4c9 100644 --- a/homeassistant/components/deconz/diagnostics.py +++ b/homeassistant/components/deconz/diagnostics.py @@ -26,7 +26,7 @@ async def async_get_config_entry_diagnostics( gateway.api.config.raw, REDACT_DECONZ_CONFIG ) diag["websocket_state"] = ( - gateway.api.websocket.state if gateway.api.websocket else "Unknown" + gateway.api.websocket.state.value if gateway.api.websocket else "Unknown" ) diag["deconz_ids"] = gateway.deconz_ids diag["entities"] = gateway.entities @@ -37,7 +37,7 @@ async def async_get_config_entry_diagnostics( } for event in gateway.events } - diag["alarm_systems"] = {k: v.raw for k, v in gateway.api.alarmsystems.items()} + diag["alarm_systems"] = {k: v.raw for k, v in gateway.api.alarm_systems.items()} diag["groups"] = {k: v.raw for k, v in gateway.api.groups.items()} diag["lights"] = {k: v.raw for k, v in gateway.api.lights.items()} diag["scenes"] = {k: v.raw for k, v in gateway.api.scenes.items()} diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index f8e4548cf91..6f29cef5190 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -169,7 +169,7 @@ class DeconzGateway: ) ) - for device_id in deconz_device_interface: + for device_id in sorted(deconz_device_interface, key=int): async_add_device(EventType.ADDED, device_id) initializing = False diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index e4e412056e6..51de538324f 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -3,7 +3,7 @@ "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": ["pydeconz==102"], + "requirements": ["pydeconz==103"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/homeassistant/components/deconz/number.py b/homeassistant/components/deconz/number.py index 636711d609d..b4a4ba415c0 100644 --- a/homeassistant/components/deconz/number.py +++ b/homeassistant/components/deconz/number.py @@ -6,7 +6,7 @@ from collections.abc import Callable from dataclasses import dataclass from pydeconz.models.event import EventType -from pydeconz.models.sensor.presence import PRESENCE_DELAY, Presence +from pydeconz.models.sensor.presence import Presence from homeassistant.components.number import ( DOMAIN, @@ -42,7 +42,7 @@ ENTITY_DESCRIPTIONS = { key="delay", value_fn=lambda device: device.delay, suffix="Delay", - update_key=PRESENCE_DELAY, + update_key="delay", native_max_value=65535, native_min_value=0, native_step=1, diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 941729ac8c2..5c1fe61c7a7 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -209,7 +209,7 @@ ENTITY_DESCRIPTIONS = { } -SENSOR_DESCRIPTIONS = [ +COMMON_SENSOR_DESCRIPTIONS = [ DeconzSensorDescription( key="battery", value_fn=lambda device: device.battery, @@ -221,8 +221,8 @@ SENSOR_DESCRIPTIONS = [ entity_category=EntityCategory.DIAGNOSTIC, ), DeconzSensorDescription( - key="secondary_temperature", - value_fn=lambda device: device.secondary_temperature, + key="internal_temperature", + value_fn=lambda device: device.internal_temperature, suffix="Temperature", update_key="temperature", device_class=SensorDeviceClass.TEMPERATURE, @@ -253,7 +253,7 @@ async def async_setup_entry( known_entities = set(gateway.entities[DOMAIN]) for description in ( - ENTITY_DESCRIPTIONS.get(type(sensor), []) + SENSOR_DESCRIPTIONS + ENTITY_DESCRIPTIONS.get(type(sensor), []) + COMMON_SENSOR_DESCRIPTIONS ): if ( not hasattr(sensor, description.key) @@ -341,8 +341,8 @@ class DeconzSensor(DeconzDevice[SensorResources], SensorEntity): if self._device.on is not None: attr[ATTR_ON] = self._device.on - if self._device.secondary_temperature is not None: - attr[ATTR_TEMPERATURE] = self._device.secondary_temperature + if self._device.internal_temperature is not None: + attr[ATTR_TEMPERATURE] = self._device.internal_temperature if isinstance(self._device, Consumption): attr[ATTR_POWER] = self._device.power @@ -383,14 +383,16 @@ class DeconzBatteryTracker: self.sensor = gateway.api.sensors[sensor_id] self.gateway = gateway self.async_add_entities = async_add_entities - self.unsub = self.sensor.subscribe(self.async_update_callback) + self.unsubscribe = self.sensor.subscribe(self.async_update_callback) @callback def async_update_callback(self) -> None: """Update the device's state.""" if "battery" in self.sensor.changed_keys: - self.unsub() + self.unsubscribe() known_entities = set(self.gateway.entities[DOMAIN]) - entity = DeconzSensor(self.sensor, self.gateway, SENSOR_DESCRIPTIONS[0]) + entity = DeconzSensor( + self.sensor, self.gateway, COMMON_SENSOR_DESCRIPTIONS[0] + ) if entity.unique_id not in known_entities: self.async_add_entities([entity]) diff --git a/requirements_all.txt b/requirements_all.txt index e6b42839383..b638e66be56 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1461,7 +1461,7 @@ pydaikin==2.7.0 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==102 +pydeconz==103 # homeassistant.components.delijn pydelijn==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b128bc80b21..1f4cd646e4e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1004,7 +1004,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.7.0 # homeassistant.components.deconz -pydeconz==102 +pydeconz==103 # homeassistant.components.dexcom pydexcom==0.2.3 diff --git a/tests/components/deconz/conftest.py b/tests/components/deconz/conftest.py index 8b92e94416a..44411ca40cf 100644 --- a/tests/components/deconz/conftest.py +++ b/tests/components/deconz/conftest.py @@ -3,7 +3,7 @@ from __future__ import annotations from unittest.mock import patch -from pydeconz.websocket import SIGNAL_CONNECTION_STATE, SIGNAL_DATA +from pydeconz.websocket import Signal import pytest from tests.components.light.conftest import mock_light_profiles # noqa: F401 @@ -20,10 +20,10 @@ def mock_deconz_websocket(): if data: mock.return_value.data = data - await pydeconz_gateway_session_handler(signal=SIGNAL_DATA) + await pydeconz_gateway_session_handler(signal=Signal.DATA) elif state: mock.return_value.state = state - await pydeconz_gateway_session_handler(signal=SIGNAL_CONNECTION_STATE) + await pydeconz_gateway_session_handler(signal=Signal.CONNECTION_STATE) else: raise NotImplementedError diff --git a/tests/components/deconz/test_diagnostics.py b/tests/components/deconz/test_diagnostics.py index d0905f5ba5f..459e0e910ab 100644 --- a/tests/components/deconz/test_diagnostics.py +++ b/tests/components/deconz/test_diagnostics.py @@ -1,6 +1,6 @@ """Test deCONZ diagnostics.""" -from pydeconz.websocket import STATE_RUNNING +from pydeconz.websocket import State from homeassistant.components.deconz.const import CONF_MASTER_GATEWAY from homeassistant.components.diagnostics import REDACTED @@ -17,7 +17,7 @@ async def test_entry_diagnostics( """Test config entry diagnostics.""" config_entry = await setup_deconz_integration(hass, aioclient_mock) - await mock_deconz_websocket(state=STATE_RUNNING) + await mock_deconz_websocket(state=State.RUNNING) await hass.async_block_till_done() assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { @@ -44,7 +44,7 @@ async def test_entry_diagnostics( "uuid": "1234", "websocketport": 1234, }, - "websocket_state": STATE_RUNNING, + "websocket_state": State.RUNNING.value, "deconz_ids": {}, "entities": { str(Platform.ALARM_CONTROL_PANEL): [], diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index e6ded3981c4..9471752eb8d 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -5,7 +5,7 @@ from copy import deepcopy from unittest.mock import patch import pydeconz -from pydeconz.websocket import STATE_RETRYING, STATE_RUNNING +from pydeconz.websocket import State import pytest from homeassistant.components import ssdp @@ -223,12 +223,12 @@ async def test_connection_status_signalling( assert hass.states.get("binary_sensor.presence").state == STATE_OFF - await mock_deconz_websocket(state=STATE_RETRYING) + await mock_deconz_websocket(state=State.RETRYING) await hass.async_block_till_done() assert hass.states.get("binary_sensor.presence").state == STATE_UNAVAILABLE - await mock_deconz_websocket(state=STATE_RUNNING) + await mock_deconz_websocket(state=State.RUNNING) await hass.async_block_till_done() assert hass.states.get("binary_sensor.presence").state == STATE_OFF From 32a2999b8523dcbffea03992318c1403f7a78477 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 6 Aug 2022 00:24:46 +0000 Subject: [PATCH 3172/3516] [ci skip] Translation update --- .../accuweather/translations/sv.json | 3 + .../components/acmeda/translations/sv.json | 3 + .../components/adax/translations/sv.json | 26 ++++- .../components/airly/translations/sv.json | 2 + .../components/airnow/translations/sv.json | 12 +- .../components/airzone/translations/sv.json | 13 +++ .../aladdin_connect/translations/sv.json | 1 + .../alarmdecoder/translations/sv.json | 3 + .../components/almond/translations/sv.json | 3 +- .../ambee/translations/sensor.sv.json | 1 + .../components/ambee/translations/sv.json | 12 +- .../amberelectric/translations/sv.json | 8 ++ .../ambiclimate/translations/sv.json | 4 +- .../components/androidtv/translations/sv.json | 63 ++++++++++ .../components/apple_tv/translations/sv.json | 17 ++- .../aseko_pool_live/translations/sv.json | 20 ++++ .../components/august/translations/sv.json | 3 +- .../aussie_broadband/translations/sv.json | 7 ++ .../components/auth/translations/de.json | 4 +- .../components/awair/translations/sv.json | 3 +- .../azure_event_hub/translations/sv.json | 49 ++++++++ .../components/balboa/translations/sv.json | 28 +++++ .../binary_sensor/translations/sv.json | 25 +++- .../components/blebox/translations/sv.json | 1 + .../components/blink/translations/sv.json | 2 + .../components/bond/translations/sv.json | 5 +- .../components/braviatv/translations/sv.json | 3 + .../components/brunt/translations/sv.json | 20 +++- .../components/button/translations/sv.json | 11 ++ .../components/canary/translations/sv.json | 20 ++++ .../components/cast/translations/sv.json | 14 ++- .../components/climacell/translations/sv.json | 13 +++ .../components/cloud/translations/sv.json | 1 + .../cloudflare/translations/sv.json | 42 +++++++ .../components/coinbase/translations/sv.json | 5 + .../components/cpuspeed/translations/sv.json | 15 +++ .../components/deluge/translations/sv.json | 15 ++- .../components/demo/translations/ca.json | 11 ++ .../components/demo/translations/hu.json | 11 ++ .../components/demo/translations/id.json | 13 ++- .../components/demo/translations/no.json | 13 ++- .../components/demo/translations/pt-BR.json | 11 ++ .../components/demo/translations/sv.json | 11 ++ .../components/demo/translations/zh-Hant.json | 13 ++- .../components/denonavr/translations/sv.json | 3 + .../derivative/translations/sv.json | 9 +- .../deutsche_bahn/translations/ca.json | 7 ++ .../deutsche_bahn/translations/de.json | 8 ++ .../deutsche_bahn/translations/hu.json | 8 ++ .../deutsche_bahn/translations/id.json | 8 ++ .../deutsche_bahn/translations/no.json | 8 ++ .../deutsche_bahn/translations/pt-BR.json | 8 ++ .../deutsche_bahn/translations/sv.json | 8 ++ .../deutsche_bahn/translations/zh-Hant.json | 8 ++ .../devolo_home_control/translations/sv.json | 7 +- .../devolo_home_network/translations/sv.json | 25 ++++ .../components/dexcom/translations/sv.json | 19 ++- .../diagnostics/translations/sv.json | 3 + .../dialogflow/translations/sv.json | 1 + .../components/discord/translations/sv.json | 6 +- .../components/dlna_dmr/translations/sv.json | 49 +++++++- .../components/dlna_dms/translations/sv.json | 24 ++++ .../components/dnsip/translations/sv.json | 29 +++++ .../components/doorbird/translations/sv.json | 3 + .../components/dsmr/translations/sv.json | 3 +- .../components/eafm/translations/sv.json | 17 +++ .../components/ecobee/translations/sv.json | 3 + .../components/efergy/translations/sv.json | 7 +- .../components/elkm1/translations/sv.json | 21 +++- .../components/elmax/translations/sv.json | 22 +++- .../enphase_envoy/translations/sv.json | 3 +- .../environment_canada/translations/sv.json | 23 ++++ .../components/epson/translations/sv.json | 3 +- .../evil_genius_labs/translations/sv.json | 1 + .../components/ezviz/translations/sv.json | 21 +++- .../faa_delays/translations/sv.json | 8 +- .../components/fan/translations/sv.json | 1 + .../components/fibaro/translations/sv.json | 2 + .../components/filesize/translations/sv.json | 14 ++- .../fireservicerota/translations/sv.json | 18 +++ .../components/fivem/translations/sv.json | 21 ++++ .../components/flipr/translations/sv.json | 3 +- .../components/flo/translations/sv.json | 5 +- .../flunearyou/translations/de.json | 13 +++ .../flunearyou/translations/hu.json | 13 +++ .../flunearyou/translations/ja.json | 12 ++ .../flunearyou/translations/no.json | 13 +++ .../flunearyou/translations/ru.json | 13 +++ .../flunearyou/translations/sv.json | 16 +++ .../flunearyou/translations/zh-Hant.json | 13 +++ .../forecast_solar/translations/sv.json | 10 +- .../forked_daapd/translations/sv.json | 9 ++ .../components/foscam/translations/sv.json | 2 + .../components/fritz/translations/sv.json | 6 +- .../components/fronius/translations/sv.json | 11 +- .../components/generic/translations/sv.json | 23 +++- .../components/github/translations/de.json | 2 +- .../components/github/translations/sv.json | 19 +++ .../components/goodwe/translations/sv.json | 4 +- .../components/google/translations/sv.json | 24 ++++ .../components/gpslogger/translations/sv.json | 1 + .../components/group/translations/sv.json | 69 ++++++++++- .../components/heos/translations/sv.json | 3 + .../components/hive/translations/de.json | 2 +- .../components/homekit/translations/sv.json | 23 ++++ .../translations/select.sv.json | 9 ++ .../translations/sensor.de.json | 21 ++++ .../translations/sensor.hu.json | 21 ++++ .../translations/sensor.ja.json | 21 ++++ .../translations/sensor.no.json | 9 ++ .../translations/sensor.sv.json | 21 ++++ .../translations/sensor.zh-Hant.json | 13 ++- .../homekit_controller/translations/sv.json | 2 + .../homewizard/translations/sv.json | 15 +++ .../components/honeywell/translations/sv.json | 11 ++ .../huawei_lte/translations/sv.json | 1 + .../components/hue/translations/sv.json | 10 +- .../huisbaasje/translations/sv.json | 8 +- .../humidifier/translations/sv.json | 1 + .../components/iaqualink/translations/sv.json | 6 +- .../components/icloud/translations/sv.json | 1 + .../components/ifttt/translations/sv.json | 1 + .../components/insteon/translations/sv.json | 18 +++ .../integration/translations/sv.json | 15 ++- .../intellifire/translations/sv.json | 14 +++ .../components/iotawatt/translations/sv.json | 10 +- .../components/iss/translations/sv.json | 22 ++++ .../kaleidescape/translations/sv.json | 22 +++- .../keenetic_ndms2/translations/sv.json | 5 +- .../components/knx/translations/sv.json | 14 ++- .../components/kodi/translations/sv.json | 12 +- .../components/konnected/translations/sv.json | 1 + .../kostal_plenticore/translations/sv.json | 2 + .../launch_library/translations/sv.json | 12 ++ .../components/lcn/translations/sv.json | 6 +- .../components/life360/translations/sv.json | 3 +- .../components/light/translations/sv.json | 1 + .../components/litejet/translations/sv.json | 22 ++++ .../components/local_ip/translations/sv.json | 1 + .../components/locative/translations/sv.json | 1 + .../components/luftdaten/translations/sv.json | 2 + .../lutron_caseta/translations/sv.json | 19 ++- .../components/mailgun/translations/sv.json | 1 + .../components/mazda/translations/sv.json | 4 +- .../media_player/translations/sv.json | 1 + .../components/met/translations/sv.json | 6 + .../met_eireann/translations/sv.json | 2 + .../meteoclimatic/translations/sv.json | 21 ++++ .../components/mill/translations/sv.json | 10 +- .../components/min_max/translations/sv.json | 14 ++- .../components/mjpeg/translations/sv.json | 23 +++- .../modem_callerid/translations/sv.json | 1 + .../modern_forms/translations/sv.json | 24 ++++ .../moehlenhoff_alpha2/translations/sv.json | 19 +++ .../components/moon/translations/sv.json | 13 +++ .../motion_blinds/translations/sv.json | 14 ++- .../components/motioneye/translations/sv.json | 5 + .../components/mqtt/translations/sv.json | 13 ++- .../components/mullvad/translations/sv.json | 5 + .../components/mysensors/translations/sv.json | 44 ++++++- .../components/nam/translations/sv.json | 16 ++- .../components/nanoleaf/translations/sv.json | 8 ++ .../components/nest/translations/de.json | 12 +- .../components/nest/translations/sv.json | 37 +++++- .../components/netatmo/translations/sv.json | 17 +++ .../components/netgear/translations/sv.json | 21 +++- .../components/nina/translations/sv.json | 25 ++++ .../nmap_tracker/translations/sv.json | 34 +++++- .../components/notion/translations/sv.json | 1 + .../components/nuki/translations/sv.json | 7 +- .../components/number/translations/sv.json | 8 ++ .../components/octoprint/translations/sv.json | 13 ++- .../components/omnilogic/translations/sv.json | 5 + .../ondilo_ico/translations/sv.json | 16 +++ .../components/onewire/translations/sv.json | 23 +++- .../components/onvif/translations/sv.json | 3 + .../open_meteo/translations/sv.json | 12 ++ .../opentherm_gw/translations/sv.json | 4 +- .../overkiz/translations/select.sv.json | 13 +++ .../overkiz/translations/sensor.sv.json | 37 ++++++ .../components/overkiz/translations/sv.json | 15 ++- .../ovo_energy/translations/sv.json | 9 +- .../components/owntracks/translations/de.json | 2 +- .../components/owntracks/translations/sv.json | 1 + .../panasonic_viera/translations/sv.json | 2 + .../components/peco/translations/sv.json | 14 +++ .../philips_js/translations/sv.json | 10 ++ .../components/picnic/translations/sv.json | 4 +- .../components/plaato/translations/de.json | 2 +- .../components/plaato/translations/sv.json | 1 + .../components/plex/translations/sv.json | 1 + .../components/plugwise/translations/sv.json | 2 + .../plum_lightpad/translations/sv.json | 7 ++ .../components/point/translations/sv.json | 3 +- .../components/poolsense/translations/sv.json | 18 +++ .../components/powerwall/translations/sv.json | 12 ++ .../components/profiler/translations/sv.json | 12 ++ .../components/prosegur/translations/sv.json | 12 ++ .../components/ps4/translations/sv.json | 6 + .../pure_energie/translations/sv.json | 23 ++++ .../components/pvoutput/translations/sv.json | 16 ++- .../radio_browser/translations/sv.json | 12 ++ .../rainmachine/translations/sv.json | 4 + .../components/remote/translations/sv.json | 1 + .../components/renault/translations/sv.json | 21 ++++ .../components/rfxtrx/translations/sv.json | 6 +- .../components/ridwell/translations/sv.json | 19 ++- .../components/risco/translations/sv.json | 15 +++ .../components/roku/translations/sv.json | 3 + .../rtsp_to_webrtc/translations/sv.json | 27 +++++ .../components/samsungtv/translations/sv.json | 10 +- .../components/season/translations/sv.json | 7 ++ .../components/sense/translations/sv.json | 19 ++- .../components/senseme/translations/sv.json | 23 ++++ .../components/sensibo/translations/sv.json | 11 ++ .../components/sensor/translations/sv.json | 5 + .../components/shelly/translations/sv.json | 24 +++- .../components/sia/translations/sv.json | 12 +- .../simplepush/translations/de.json | 4 + .../simplepush/translations/hu.json | 4 + .../simplepush/translations/ja.json | 3 + .../simplepush/translations/no.json | 4 + .../simplepush/translations/ru.json | 4 + .../simplepush/translations/sv.json | 4 + .../simplepush/translations/zh-Hant.json | 4 + .../simplisafe/translations/ca.json | 1 + .../simplisafe/translations/de.json | 3 +- .../simplisafe/translations/hu.json | 1 + .../simplisafe/translations/id.json | 2 +- .../simplisafe/translations/no.json | 3 +- .../simplisafe/translations/ru.json | 1 + .../simplisafe/translations/sv.json | 2 + .../simplisafe/translations/zh-Hant.json | 3 +- .../components/sleepiq/translations/sv.json | 16 +++ .../components/smhi/translations/sv.json | 3 + .../components/solaredge/translations/sv.json | 8 +- .../somfy_mylink/translations/sv.json | 11 ++ .../components/sonarr/translations/sv.json | 1 + .../components/songpal/translations/sv.json | 3 + .../components/spider/translations/sv.json | 7 +- .../components/spotify/translations/sv.json | 7 +- .../srp_energy/translations/sv.json | 4 + .../components/steamist/translations/sv.json | 18 ++- .../stookalert/translations/sv.json | 14 +++ .../components/subaru/translations/sv.json | 7 +- .../components/sun/translations/sv.json | 10 ++ .../surepetcare/translations/sv.json | 9 ++ .../components/switch/translations/sv.json | 1 + .../switch_as_x/translations/sv.json | 4 +- .../components/switchbot/translations/sv.json | 2 + .../components/syncthing/translations/sv.json | 4 + .../synology_dsm/translations/sv.json | 4 +- .../system_bridge/translations/sv.json | 21 +++- .../components/tag/translations/sv.json | 3 + .../components/tailscale/translations/sv.json | 16 ++- .../tellduslive/translations/sv.json | 3 +- .../tesla_wall_connector/translations/sv.json | 20 ++++ .../components/threshold/translations/sv.json | 20 +++- .../components/tile/translations/sv.json | 12 +- .../components/tod/translations/sv.json | 23 ++++ .../tolo/translations/select.sv.json | 8 ++ .../components/tolo/translations/sv.json | 22 ++++ .../tomorrowio/translations/sensor.sv.json | 27 +++++ .../tomorrowio/translations/sv.json | 23 +++- .../components/toon/translations/sv.json | 3 +- .../totalconnect/translations/sv.json | 3 + .../components/tplink/translations/sv.json | 6 + .../components/traccar/translations/sv.json | 1 + .../tractive/translations/sensor.sv.json | 10 ++ .../components/tradfri/translations/sv.json | 1 + .../translations/sv.json | 12 +- .../transmission/translations/sv.json | 1 + .../tuya/translations/select.sv.json | 109 +++++++++++++++++- .../tuya/translations/sensor.sv.json | 6 + .../components/tuya/translations/sv.json | 1 + .../components/twilio/translations/sv.json | 1 + .../components/twinkly/translations/sv.json | 3 + .../ukraine_alarm/translations/sv.json | 1 + .../components/unifi/translations/sv.json | 3 +- .../unifiprotect/translations/sv.json | 40 ++++++- .../components/update/translations/sv.json | 3 +- .../components/uptime/translations/sv.json | 13 +++ .../uptimerobot/translations/sensor.sv.json | 11 ++ .../uptimerobot/translations/sv.json | 1 + .../components/vallox/translations/sv.json | 22 ++++ .../components/vera/translations/sv.json | 3 + .../components/version/translations/sv.json | 26 +++++ .../components/vicare/translations/sv.json | 13 ++- .../components/vizio/translations/sv.json | 1 + .../vlc_telnet/translations/sv.json | 19 ++- .../components/vulcan/translations/id.json | 2 +- .../components/vulcan/translations/sv.json | 2 + .../components/watttime/translations/sv.json | 13 ++- .../waze_travel_time/translations/sv.json | 18 ++- .../components/webostv/translations/sv.json | 47 ++++++++ .../components/whirlpool/translations/sv.json | 5 +- .../components/whois/translations/sv.json | 9 ++ .../components/wiffi/translations/sv.json | 12 +- .../components/wilight/translations/sv.json | 15 +++ .../components/wiz/translations/sv.json | 33 ++++++ .../wolflink/translations/sensor.sv.json | 3 + .../xiaomi_ble/translations/sv.json | 14 ++- .../xiaomi_miio/translations/sv.json | 3 +- .../yale_smart_alarm/translations/sv.json | 27 +++++ .../translations/select.sv.json | 52 +++++++++ .../components/yeelight/translations/sv.json | 3 +- .../components/youless/translations/sv.json | 15 +++ .../components/zerproc/translations/sv.json | 13 +++ .../components/zha/translations/sv.json | 2 + .../zodiac/translations/sensor.sv.json | 18 +++ .../zoneminder/translations/sv.json | 17 ++- .../components/zwave_js/translations/sv.json | 54 ++++++++- .../components/zwave_me/translations/sv.json | 20 ++++ 313 files changed, 3494 insertions(+), 184 deletions(-) create mode 100644 homeassistant/components/androidtv/translations/sv.json create mode 100644 homeassistant/components/aseko_pool_live/translations/sv.json create mode 100644 homeassistant/components/azure_event_hub/translations/sv.json create mode 100644 homeassistant/components/balboa/translations/sv.json create mode 100644 homeassistant/components/button/translations/sv.json create mode 100644 homeassistant/components/climacell/translations/sv.json create mode 100644 homeassistant/components/cloudflare/translations/sv.json create mode 100644 homeassistant/components/cpuspeed/translations/sv.json create mode 100644 homeassistant/components/deutsche_bahn/translations/ca.json create mode 100644 homeassistant/components/deutsche_bahn/translations/de.json create mode 100644 homeassistant/components/deutsche_bahn/translations/hu.json create mode 100644 homeassistant/components/deutsche_bahn/translations/id.json create mode 100644 homeassistant/components/deutsche_bahn/translations/no.json create mode 100644 homeassistant/components/deutsche_bahn/translations/pt-BR.json create mode 100644 homeassistant/components/deutsche_bahn/translations/sv.json create mode 100644 homeassistant/components/deutsche_bahn/translations/zh-Hant.json create mode 100644 homeassistant/components/devolo_home_network/translations/sv.json create mode 100644 homeassistant/components/diagnostics/translations/sv.json create mode 100644 homeassistant/components/dlna_dms/translations/sv.json create mode 100644 homeassistant/components/dnsip/translations/sv.json create mode 100644 homeassistant/components/eafm/translations/sv.json create mode 100644 homeassistant/components/environment_canada/translations/sv.json create mode 100644 homeassistant/components/fivem/translations/sv.json create mode 100644 homeassistant/components/github/translations/sv.json create mode 100644 homeassistant/components/homekit_controller/translations/select.sv.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.de.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.hu.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.ja.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.sv.json create mode 100644 homeassistant/components/iss/translations/sv.json create mode 100644 homeassistant/components/launch_library/translations/sv.json create mode 100644 homeassistant/components/meteoclimatic/translations/sv.json create mode 100644 homeassistant/components/modern_forms/translations/sv.json create mode 100644 homeassistant/components/moehlenhoff_alpha2/translations/sv.json create mode 100644 homeassistant/components/moon/translations/sv.json create mode 100644 homeassistant/components/number/translations/sv.json create mode 100644 homeassistant/components/ondilo_ico/translations/sv.json create mode 100644 homeassistant/components/open_meteo/translations/sv.json create mode 100644 homeassistant/components/overkiz/translations/select.sv.json create mode 100644 homeassistant/components/peco/translations/sv.json create mode 100644 homeassistant/components/poolsense/translations/sv.json create mode 100644 homeassistant/components/profiler/translations/sv.json create mode 100644 homeassistant/components/pure_energie/translations/sv.json create mode 100644 homeassistant/components/radio_browser/translations/sv.json create mode 100644 homeassistant/components/rtsp_to_webrtc/translations/sv.json create mode 100644 homeassistant/components/stookalert/translations/sv.json create mode 100644 homeassistant/components/tag/translations/sv.json create mode 100644 homeassistant/components/tesla_wall_connector/translations/sv.json create mode 100644 homeassistant/components/tolo/translations/select.sv.json create mode 100644 homeassistant/components/tolo/translations/sv.json create mode 100644 homeassistant/components/tomorrowio/translations/sensor.sv.json create mode 100644 homeassistant/components/tractive/translations/sensor.sv.json create mode 100644 homeassistant/components/uptime/translations/sv.json create mode 100644 homeassistant/components/uptimerobot/translations/sensor.sv.json create mode 100644 homeassistant/components/vallox/translations/sv.json create mode 100644 homeassistant/components/version/translations/sv.json create mode 100644 homeassistant/components/webostv/translations/sv.json create mode 100644 homeassistant/components/wilight/translations/sv.json create mode 100644 homeassistant/components/wiz/translations/sv.json create mode 100644 homeassistant/components/yamaha_musiccast/translations/select.sv.json create mode 100644 homeassistant/components/youless/translations/sv.json create mode 100644 homeassistant/components/zerproc/translations/sv.json create mode 100644 homeassistant/components/zodiac/translations/sensor.sv.json create mode 100644 homeassistant/components/zwave_me/translations/sv.json diff --git a/homeassistant/components/accuweather/translations/sv.json b/homeassistant/components/accuweather/translations/sv.json index a87b6736271..4dea8a74f47 100644 --- a/homeassistant/components/accuweather/translations/sv.json +++ b/homeassistant/components/accuweather/translations/sv.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, + "create_entry": { + "default": "Vissa sensorer \u00e4r inte aktiverade som standard. Du kan aktivera dem i entitetsregistret efter integrationskonfigurationen.\n V\u00e4derprognos \u00e4r inte aktiverat som standard. Du kan aktivera det i integrationsalternativen." + }, "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_api_key": "Ogiltig API-nyckel", diff --git a/homeassistant/components/acmeda/translations/sv.json b/homeassistant/components/acmeda/translations/sv.json index 487295ecade..a968c430178 100644 --- a/homeassistant/components/acmeda/translations/sv.json +++ b/homeassistant/components/acmeda/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_devices_found": "Inga enheter hittades i n\u00e4tverket" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/adax/translations/sv.json b/homeassistant/components/adax/translations/sv.json index be36fec5fe3..ef22bdd620f 100644 --- a/homeassistant/components/adax/translations/sv.json +++ b/homeassistant/components/adax/translations/sv.json @@ -1,10 +1,34 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "heater_not_available": "V\u00e4rmare inte tillg\u00e4nglig. F\u00f6rs\u00f6k att \u00e5terst\u00e4lla v\u00e4rmaren genom att trycka p\u00e5 + och OK i n\u00e5gra sekunder.", + "heater_not_found": "V\u00e4rmare hittades inte. F\u00f6rs\u00f6k att flytta v\u00e4rmaren n\u00e4rmare Home Assistant-datorn.", + "invalid_auth": "Ogiltig autentisering" }, "error": { "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "cloud": { + "data": { + "account_id": "Konto-ID", + "password": "L\u00f6senord" + } + }, + "local": { + "data": { + "wifi_pswd": "Wi-Fi l\u00f6senord", + "wifi_ssid": "Wi-Fi SSID" + }, + "description": "\u00c5terst\u00e4ll v\u00e4rmaren genom att trycka p\u00e5 + och OK tills displayen visar 'Reset'. Tryck sedan och h\u00e5ll ner OK-knappen p\u00e5 v\u00e4rmaren tills den bl\u00e5 lysdioden b\u00f6rjar blinka innan du trycker p\u00e5 Skicka. Det kan ta n\u00e5gra minuter att konfigurera v\u00e4rmaren." + }, + "user": { + "data": { + "connection_type": "V\u00e4lj anslutningstyp" + }, + "description": "V\u00e4lj anslutningstyp. Lokalt kr\u00e4ver v\u00e4rmare med bluetooth" + } } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/sv.json b/homeassistant/components/airly/translations/sv.json index 05d56e9d88a..ac8ae0728df 100644 --- a/homeassistant/components/airly/translations/sv.json +++ b/homeassistant/components/airly/translations/sv.json @@ -21,6 +21,8 @@ }, "system_health": { "info": { + "can_reach_server": "N\u00e5 Airly-servern", + "requests_per_day": "Till\u00e5tna f\u00f6rfr\u00e5gningar per dag", "requests_remaining": "\u00c5terst\u00e5ende till\u00e5tna f\u00f6rfr\u00e5gningar" } } diff --git a/homeassistant/components/airnow/translations/sv.json b/homeassistant/components/airnow/translations/sv.json index 138764f44d9..320291a913f 100644 --- a/homeassistant/components/airnow/translations/sv.json +++ b/homeassistant/components/airnow/translations/sv.json @@ -5,13 +5,19 @@ }, "error": { "cannot_connect": "Det gick inte att ansluta.", - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "invalid_location": "Inga resultat hittades f\u00f6r den platsen", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "latitude": "Latitud", + "longitude": "Longitud", + "radius": "Stationsradie (miles; valfritt)" + }, + "description": "F\u00f6r att generera API-nyckel g\u00e5 till https://docs.airnowapi.org/account/request/" } } } diff --git a/homeassistant/components/airzone/translations/sv.json b/homeassistant/components/airzone/translations/sv.json index daa1cb38499..1fe6a415693 100644 --- a/homeassistant/components/airzone/translations/sv.json +++ b/homeassistant/components/airzone/translations/sv.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "invalid_system_id": "Ogiltigt Airzone System ID" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "port": "Port" + }, + "description": "St\u00e4ll in Airzone-integration." + } } } } \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/sv.json b/homeassistant/components/aladdin_connect/translations/sv.json index 867d5d1c5c7..0f9de2eb48c 100644 --- a/homeassistant/components/aladdin_connect/translations/sv.json +++ b/homeassistant/components/aladdin_connect/translations/sv.json @@ -13,6 +13,7 @@ "data": { "password": "L\u00f6senord" }, + "description": "Aladdin Connect-integrationen m\u00e5ste autentisera ditt konto igen", "title": "\u00c5terautenticera integration" }, "user": { diff --git a/homeassistant/components/alarmdecoder/translations/sv.json b/homeassistant/components/alarmdecoder/translations/sv.json index 0e8f0208b6c..4ff8037c318 100644 --- a/homeassistant/components/alarmdecoder/translations/sv.json +++ b/homeassistant/components/alarmdecoder/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "create_entry": { "default": "Ansluten till AlarmDecoder." }, diff --git a/homeassistant/components/almond/translations/sv.json b/homeassistant/components/almond/translations/sv.json index 6cccf60b2c2..c11af204012 100644 --- a/homeassistant/components/almond/translations/sv.json +++ b/homeassistant/components/almond/translations/sv.json @@ -3,7 +3,8 @@ "abort": { "cannot_connect": "Det g\u00e5r inte att ansluta till Almond-servern.", "missing_configuration": "Kontrollera dokumentationen f\u00f6r hur du st\u00e4ller in Almond.", - "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/ambee/translations/sensor.sv.json b/homeassistant/components/ambee/translations/sensor.sv.json index a7c17b93906..d3280d4ebf4 100644 --- a/homeassistant/components/ambee/translations/sensor.sv.json +++ b/homeassistant/components/ambee/translations/sensor.sv.json @@ -1,6 +1,7 @@ { "state": { "ambee__risk": { + "high": "H\u00f6g", "low": "L\u00e5g", "moderate": "M\u00e5ttlig", "very high": "V\u00e4ldigt h\u00f6gt" diff --git a/homeassistant/components/ambee/translations/sv.json b/homeassistant/components/ambee/translations/sv.json index 77149dc7889..e31205118b9 100644 --- a/homeassistant/components/ambee/translations/sv.json +++ b/homeassistant/components/ambee/translations/sv.json @@ -3,6 +3,10 @@ "abort": { "reauth_successful": "\u00c5terautentisering lyckades" }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_api_key": "Ogiltig API-nyckel" + }, "step": { "reauth_confirm": { "data": { @@ -12,8 +16,12 @@ }, "user": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Namn" + }, + "description": "Konfigurera Ambee f\u00f6r att integrera med Home Assistant." } } }, diff --git a/homeassistant/components/amberelectric/translations/sv.json b/homeassistant/components/amberelectric/translations/sv.json index 7458627ef0a..fdf3161483f 100644 --- a/homeassistant/components/amberelectric/translations/sv.json +++ b/homeassistant/components/amberelectric/translations/sv.json @@ -1,8 +1,16 @@ { "config": { "step": { + "site": { + "data": { + "site_name": "Namn p\u00e5 platsen", + "site_nmi": "Plats NMI" + }, + "description": "V\u00e4lj NMI f\u00f6r den plats du vill l\u00e4gga till." + }, "user": { "data": { + "api_token": "API Token", "site_id": "Plats-ID" }, "description": "G\u00e5 till {api_url} f\u00f6r att skapa en API-nyckel" diff --git a/homeassistant/components/ambiclimate/translations/sv.json b/homeassistant/components/ambiclimate/translations/sv.json index e6d06553d77..02f3b80022f 100644 --- a/homeassistant/components/ambiclimate/translations/sv.json +++ b/homeassistant/components/ambiclimate/translations/sv.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "access_token": "Ok\u00e4nt fel vid generering av \u00e5tkomsttoken." + "access_token": "Ok\u00e4nt fel vid generering av \u00e5tkomsttoken.", + "already_configured": "Konto har redan konfigurerats", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen." }, "create_entry": { "default": "Lyckad autentisering med Ambiclimate" diff --git a/homeassistant/components/androidtv/translations/sv.json b/homeassistant/components/androidtv/translations/sv.json new file mode 100644 index 00000000000..297daf9cc02 --- /dev/null +++ b/homeassistant/components/androidtv/translations/sv.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "invalid_unique_id": "Om\u00f6jligt att fastst\u00e4lla ett giltigt unikt ID f\u00f6r enheten" + }, + "error": { + "adbkey_not_file": "ADB-nyckelfil hittades inte", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress", + "key_and_server": "Ange endast ADB-nyckel eller ADB-server", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "adb_server_ip": "ADB-serverns IP-adress (l\u00e4mna tomt om du inte vill anv\u00e4nda den).", + "adb_server_port": "Port f\u00f6r ADB-servern", + "adbkey": "S\u00f6kv\u00e4g till din ADB-nyckelfil (l\u00e5t vara tomt f\u00f6r att automatiskt generera)", + "device_class": "Typ av enhet", + "host": "V\u00e4rd", + "port": "Port" + } + } + } + }, + "options": { + "error": { + "invalid_det_rules": "Ogiltiga regler f\u00f6r tillst\u00e5ndsidentifiering" + }, + "step": { + "apps": { + "data": { + "app_delete": "Markera f\u00f6r att radera denna applikation", + "app_id": "Program-ID", + "app_name": "Programmets namn" + }, + "description": "Konfigurera program-ID {app_id}", + "title": "Konfigurera Android TV-appar" + }, + "init": { + "data": { + "apps": "Konfigurera applikationslista", + "exclude_unnamed_apps": "Undanta appar med ok\u00e4nt namn fr\u00e5n k\u00e4llistan", + "get_sources": "H\u00e4mta de appar som k\u00f6rs som en lista \u00f6ver k\u00e4llor", + "screencap": "Anv\u00e4nd sk\u00e4rmdump f\u00f6r albumomslag", + "state_detection_rules": "Konfigurera regler f\u00f6r tillst\u00e5ndsdetektering", + "turn_off_command": "Kommando f\u00f6r att st\u00e4nga av ADB-skalet (l\u00e4mna tomt som standard)", + "turn_on_command": "Kommando f\u00f6r att aktivera ADB-skalet (l\u00e4mna tomt som standard)" + } + }, + "rules": { + "data": { + "rule_delete": "Markera f\u00f6r att ta bort denna regel", + "rule_id": "Program-ID", + "rule_values": "F\u00f6rteckning \u00f6ver regler f\u00f6r detektering av tillst\u00e5nd (se dokumentationen)" + }, + "description": "Konfigurera identifieringsregel f\u00f6r app-id {rule_id}", + "title": "Konfigurera regler f\u00f6r detektering av Android TV-tillst\u00e5nd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/sv.json b/homeassistant/components/apple_tv/translations/sv.json index 28b6e2ed67b..2e421f154b0 100644 --- a/homeassistant/components/apple_tv/translations/sv.json +++ b/homeassistant/components/apple_tv/translations/sv.json @@ -1,10 +1,16 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", "backoff": "Enheten accepterar inte parningsf\u00f6rfr\u00e5gningar f\u00f6r n\u00e4rvarande (du kan ha angett en ogiltig PIN-kod f\u00f6r m\u00e5nga g\u00e5nger), f\u00f6rs\u00f6k igen senare.", "device_did_not_pair": "Inget f\u00f6rs\u00f6k att avsluta parningsprocessen gjordes fr\u00e5n enheten.", + "device_not_found": "Enheten hittades inte under uppt\u00e4ckten, f\u00f6rs\u00f6k att l\u00e4gga till den igen.", + "inconsistent_device": "F\u00f6rv\u00e4ntade protokoll hittades inte under uppt\u00e4ckten. Detta indikerar normalt ett problem med multicast DNS (Zeroconf). F\u00f6rs\u00f6k att l\u00e4gga till enheten igen.", + "ipv6_not_supported": "IPv6 st\u00f6ds inte.", "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "reauth_successful": "\u00c5terautentisering lyckades", + "setup_failed": "Det gick inte att konfigurera enheten.", "unknown": "Ov\u00e4ntat fel" }, "error": { @@ -30,6 +36,14 @@ "description": "Parning kr\u00e4vs f\u00f6r protokollet ` {protocol} `. V\u00e4nligen ange PIN-koden som visas p\u00e5 sk\u00e4rmen. Inledande nollor ska utel\u00e4mnas, dvs ange 123 om den visade koden \u00e4r 0123.", "title": "Parkoppling" }, + "password": { + "description": "Ett l\u00f6senord kr\u00e4vs av ` {protocol} `. Detta st\u00f6ds inte \u00e4nnu, inaktivera l\u00f6senordet f\u00f6r att forts\u00e4tta.", + "title": "L\u00f6senord kr\u00e4vs" + }, + "protocol_disabled": { + "description": "Parkoppling kr\u00e4vs f\u00f6r ` {protocol} ` men det \u00e4r inaktiverat p\u00e5 enheten. Granska potentiella \u00e5tkomstbegr\u00e4nsningar (t.ex. till\u00e5t alla enheter i det lokala n\u00e4tverket att ansluta) p\u00e5 enheten. \n\n Du kan forts\u00e4tta utan att para detta protokoll, men vissa funktioner kommer att vara begr\u00e4nsade.", + "title": "Det g\u00e5r inte att koppla ihop" + }, "reconfigure": { "description": "Konfigurera om enheten f\u00f6r att \u00e5terst\u00e4lla dess funktionalitet.", "title": "Omkonfigurering av enheten" @@ -52,7 +66,8 @@ "init": { "data": { "start_off": "Sl\u00e5 inte p\u00e5 enheten n\u00e4r du startar Home Assistant" - } + }, + "description": "Konfigurera allm\u00e4nna enhetsinst\u00e4llningar" } } } diff --git a/homeassistant/components/aseko_pool_live/translations/sv.json b/homeassistant/components/aseko_pool_live/translations/sv.json new file mode 100644 index 00000000000..34edaf71822 --- /dev/null +++ b/homeassistant/components/aseko_pool_live/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "email": "E-post", + "password": "L\u00f6senord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/sv.json b/homeassistant/components/august/translations/sv.json index f3b6cf8d552..0243aef828a 100644 --- a/homeassistant/components/august/translations/sv.json +++ b/homeassistant/components/august/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Kontot har redan konfigurerats" + "already_configured": "Kontot har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", diff --git a/homeassistant/components/aussie_broadband/translations/sv.json b/homeassistant/components/aussie_broadband/translations/sv.json index b159fb71b87..535bb55e74b 100644 --- a/homeassistant/components/aussie_broadband/translations/sv.json +++ b/homeassistant/components/aussie_broadband/translations/sv.json @@ -11,6 +11,13 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Uppdatera l\u00f6senord f\u00f6r {username}", + "title": "\u00c5terautenticera integration" + }, "service": { "data": { "services": "Tj\u00e4nster" diff --git a/homeassistant/components/auth/translations/de.json b/homeassistant/components/auth/translations/de.json index 93cbf1073cc..e33536bcc1c 100644 --- a/homeassistant/components/auth/translations/de.json +++ b/homeassistant/components/auth/translations/de.json @@ -9,11 +9,11 @@ }, "step": { "init": { - "description": "Bitte w\u00e4hlen Sie einen der Benachrichtigungsdienste:", + "description": "Bitte w\u00e4hle einen der Benachrichtigungsdienste:", "title": "Einmal Passwort f\u00fcr Notify einrichten" }, "setup": { - "description": "Ein Einmal-Passwort wurde per **notify.{notify_service}** gesendet. Bitte geben Sie es unten ein:", + "description": "Ein Einmal-Passwort wurde per **notify.{notify_service}** gesendet. Bitte gib es unten ein:", "title": "\u00dcberpr\u00fcfe das Setup" } }, diff --git a/homeassistant/components/awair/translations/sv.json b/homeassistant/components/awair/translations/sv.json index 4823ac2df6a..017247b4e1d 100644 --- a/homeassistant/components/awair/translations/sv.json +++ b/homeassistant/components/awair/translations/sv.json @@ -28,7 +28,8 @@ "data": { "access_token": "\u00c5tkomstnyckel", "email": "E-post" - } + }, + "description": "Du m\u00e5ste registrera dig f\u00f6r en Awair-utvecklar\u00e5tkomsttoken p\u00e5: https://developer.getawair.com/onboard/login" } } } diff --git a/homeassistant/components/azure_event_hub/translations/sv.json b/homeassistant/components/azure_event_hub/translations/sv.json new file mode 100644 index 00000000000..a01f17029e8 --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/sv.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta med referenserna fr\u00e5n configuration.yaml, ta bort fr\u00e5n yaml och anv\u00e4nd konfigurationsfl\u00f6det.", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "unknown": "Att ansluta med referenserna fr\u00e5n configuration.yaml misslyckades med ett ok\u00e4nt fel, ta bort fr\u00e5n yaml och anv\u00e4nd konfigurationsfl\u00f6det." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "conn_string": { + "data": { + "event_hub_connection_string": "Event Hub Connection String" + }, + "description": "Ange anslutningsstr\u00e4ngen f\u00f6r: {event_hub_instance_name}", + "title": "Metod f\u00f6r anslutningsstr\u00e4ng" + }, + "sas": { + "data": { + "event_hub_namespace": "Event Hub Namnutrymme", + "event_hub_sas_key": "Event Hub SAS-nyckel", + "event_hub_sas_policy": "Event Hub SAS Policy" + }, + "description": "V\u00e4nligen ange SAS-uppgifterna (delad \u00e5tkomstsignatur) f\u00f6r: {event_hub_instance_name}", + "title": "Metod f\u00f6r SAS-autentiseringsuppgifter" + }, + "user": { + "data": { + "event_hub_instance_name": "Event Hub-instansnamn", + "use_connection_string": "Anv\u00e4nd anslutningsstr\u00e4ng" + }, + "title": "Konfigurera din Azure Event Hub-integration" + } + } + }, + "options": { + "step": { + "options": { + "data": { + "send_interval": "Intervall mellan att skicka batcher till hubben." + }, + "title": "Alternativ f\u00f6r Azure Event Hub." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/balboa/translations/sv.json b/homeassistant/components/balboa/translations/sv.json new file mode 100644 index 00000000000..d74e7805e44 --- /dev/null +++ b/homeassistant/components/balboa/translations/sv.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + }, + "title": "Anslut till Balboa Wi-Fi-enhet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "sync_time": "H\u00e5ll din Balboa Spa Clients tid synkroniserad med Home Assistant" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/sv.json b/homeassistant/components/binary_sensor/translations/sv.json index eeaac07d691..b3f2529b766 100644 --- a/homeassistant/components/binary_sensor/translations/sv.json +++ b/homeassistant/components/binary_sensor/translations/sv.json @@ -2,6 +2,7 @@ "device_automation": { "condition_type": { "is_bat_low": "{entity_name}-batteriet \u00e4r l\u00e5gt", + "is_co": "{entity_name} uppt\u00e4cker kolmonoxid", "is_cold": "{entity_name} \u00e4r kall", "is_connected": "{entity_name} \u00e4r ansluten", "is_gas": "{entity_name} detekterar gas", @@ -11,6 +12,7 @@ "is_moist": "{entity_name} \u00e4r fuktig", "is_motion": "{entity_name} detekterar r\u00f6relse", "is_moving": "{entity_name} r\u00f6r sig", + "is_no_co": "{entity_name} uppt\u00e4cker inte kolmonoxid", "is_no_gas": "{entity_name} uppt\u00e4cker inte gas", "is_no_light": "{entity_name} uppt\u00e4cker inte ljus", "is_no_motion": "{entity_name} detekterar inte r\u00f6relse", @@ -31,6 +33,8 @@ "is_not_plugged_in": "{entity_name} \u00e4r urkopplad", "is_not_powered": "{entity_name} \u00e4r inte str\u00f6mf\u00f6rd", "is_not_present": "{entity_name} finns inte", + "is_not_running": "{entity_name} k\u00f6rs inte", + "is_not_tampered": "{entity_name} uppt\u00e4cker inte manipulering", "is_not_unsafe": "{entity_name} \u00e4r s\u00e4ker", "is_occupied": "{entity_name} \u00e4r upptagen", "is_off": "{entity_name} \u00e4r avst\u00e4ngd", @@ -43,12 +47,14 @@ "is_running": "{entity_name} k\u00f6rs", "is_smoke": "{entity_name} detekterar r\u00f6k", "is_sound": "{entity_name} uppt\u00e4cker ljud", + "is_tampered": "{entity_name} uppt\u00e4cker manipulering", "is_unsafe": "{entity_name} \u00e4r os\u00e4ker", "is_update": "{entity_name} har en uppdatering tillg\u00e4nglig", "is_vibration": "{entity_name} uppt\u00e4cker vibrationer" }, "trigger_type": { "bat_low": "{entity_name} batteri l\u00e5gt", + "co": "{entity_name} b\u00f6rjade detektera kolmonoxid", "cold": "{entity_name} blev kall", "connected": "{entity_name} ansluten", "gas": "{entity_name} b\u00f6rjade detektera gas", @@ -58,6 +64,7 @@ "moist": "{entity_name} blev fuktig", "motion": "{entity_name} b\u00f6rjade detektera r\u00f6relse", "moving": "{entity_name} b\u00f6rjade r\u00f6ra sig", + "no_co": "{entity_name} slutade detektera kolmonoxid", "no_gas": "{entity_name} slutade uppt\u00e4cka gas", "no_light": "{entity_name} slutade uppt\u00e4cka ljus", "no_motion": "{entity_name} slutade uppt\u00e4cka r\u00f6relse", @@ -78,6 +85,8 @@ "not_plugged_in": "{entity_name} urkopplad", "not_powered": "{entity_name} inte p\u00e5slagen", "not_present": "{entity_name} inte n\u00e4rvarande", + "not_running": "{entity_name} k\u00f6rs inte l\u00e4ngre", + "not_tampered": "{entity_name} slutade uppt\u00e4cka manipulering", "not_unsafe": "{entity_name} blev s\u00e4ker", "occupied": "{entity_name} blev upptagen", "opened": "{entity_name} \u00f6ppnades", @@ -85,8 +94,10 @@ "powered": "{entity_name} p\u00e5slagen", "present": "{entity_name} n\u00e4rvarande", "problem": "{entity_name} b\u00f6rjade uppt\u00e4cka problem", + "running": "{entity_name} b\u00f6rjade k\u00f6ras", "smoke": "{entity_name} b\u00f6rjade detektera r\u00f6k", "sound": "{entity_name} b\u00f6rjade uppt\u00e4cka ljud", + "tampered": "{entity_name} b\u00f6rjade uppt\u00e4cka manipulering", "turned_off": "{entity_name} st\u00e4ngdes av", "turned_on": "{entity_name} slogs p\u00e5", "unsafe": "{entity_name} blev os\u00e4ker", @@ -95,10 +106,18 @@ } }, "device_class": { + "co": "kolmonoxid", "cold": "Kyla", + "gas": "gas", "heat": "v\u00e4rme", + "moisture": "fukt", "motion": "r\u00f6relse", - "power": "effekt" + "occupancy": "n\u00e4rvaro", + "power": "effekt", + "problem": "problem", + "smoke": "r\u00f6k", + "sound": "ljud", + "vibration": "vibration" }, "state": { "_": { @@ -113,6 +132,10 @@ "off": "Laddar inte", "on": "Laddar" }, + "carbon_monoxide": { + "off": "Rensa", + "on": "Detekterad" + }, "cold": { "off": "Normal", "on": "Kallt" diff --git a/homeassistant/components/blebox/translations/sv.json b/homeassistant/components/blebox/translations/sv.json index e521f4c6f4a..2f22cc7d372 100644 --- a/homeassistant/components/blebox/translations/sv.json +++ b/homeassistant/components/blebox/translations/sv.json @@ -13,6 +13,7 @@ "step": { "user": { "data": { + "host": "IP-adress", "port": "Port" }, "description": "St\u00e4ll in din BleBox f\u00f6r att integrera med Home Assistant.", diff --git a/homeassistant/components/blink/translations/sv.json b/homeassistant/components/blink/translations/sv.json index 282dc374e67..9d71443234e 100644 --- a/homeassistant/components/blink/translations/sv.json +++ b/homeassistant/components/blink/translations/sv.json @@ -4,6 +4,8 @@ "already_configured": "Enheten \u00e4r redan konfigurerad" }, "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_access_token": "Ogiltig \u00e5tkomstnyckel", "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, diff --git a/homeassistant/components/bond/translations/sv.json b/homeassistant/components/bond/translations/sv.json index e745283a84a..9d5223555b6 100644 --- a/homeassistant/components/bond/translations/sv.json +++ b/homeassistant/components/bond/translations/sv.json @@ -4,7 +4,10 @@ "already_configured": "Enheten \u00e4r redan konfigurerad" }, "error": { - "old_firmware": "Gamla firmware som inte st\u00f6ds p\u00e5 Bond-enheten - uppgradera innan du forts\u00e4tter." + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "old_firmware": "Gamla firmware som inte st\u00f6ds p\u00e5 Bond-enheten - uppgradera innan du forts\u00e4tter.", + "unknown": "Ov\u00e4ntat fel" }, "flow_title": "{name} ({host})", "step": { diff --git a/homeassistant/components/braviatv/translations/sv.json b/homeassistant/components/braviatv/translations/sv.json index f545bdcabfb..f47ee5c3a77 100644 --- a/homeassistant/components/braviatv/translations/sv.json +++ b/homeassistant/components/braviatv/translations/sv.json @@ -11,6 +11,9 @@ }, "step": { "authorize": { + "data": { + "pin": "Pin-kod" + }, "description": "Ange PIN-koden som visas p\u00e5 Sony Bravia TV. \n\n Om PIN-koden inte visas m\u00e5ste du avregistrera Home Assistant p\u00e5 din TV, g\u00e5 till: Inst\u00e4llningar - > N\u00e4tverk - > Inst\u00e4llningar f\u00f6r fj\u00e4rrenhet - > Avregistrera fj\u00e4rrenhet.", "title": "Auktorisera Sony Bravia TV" }, diff --git a/homeassistant/components/brunt/translations/sv.json b/homeassistant/components/brunt/translations/sv.json index 23c825f256f..8db2db05e9a 100644 --- a/homeassistant/components/brunt/translations/sv.json +++ b/homeassistant/components/brunt/translations/sv.json @@ -1,10 +1,28 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "V\u00e4nligen ange l\u00f6senordet igen f\u00f6r: {username}", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Konfigurera din Brunt-integration" } } } diff --git a/homeassistant/components/button/translations/sv.json b/homeassistant/components/button/translations/sv.json new file mode 100644 index 00000000000..4f512af45d0 --- /dev/null +++ b/homeassistant/components/button/translations/sv.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "press": "Tryck p\u00e5 knappen {entity_name}" + }, + "trigger_type": { + "pressed": "{entity_name} har tryckts" + } + }, + "title": "Knapp" +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/sv.json b/homeassistant/components/canary/translations/sv.json index 23c825f256f..7b57017ad3b 100644 --- a/homeassistant/components/canary/translations/sv.json +++ b/homeassistant/components/canary/translations/sv.json @@ -1,9 +1,29 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{name}", "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" + }, + "title": "Anslut till Canary" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "Argument som skickas till ffmpeg f\u00f6r kameror", + "timeout": "Timeout f\u00f6r beg\u00e4ran (sekunder)" } } } diff --git a/homeassistant/components/cast/translations/sv.json b/homeassistant/components/cast/translations/sv.json index 49ec796088a..1bcc2ec4f72 100644 --- a/homeassistant/components/cast/translations/sv.json +++ b/homeassistant/components/cast/translations/sv.json @@ -26,8 +26,18 @@ "step": { "advanced_options": { "data": { - "ignore_cec": "Ignorera CEC" - } + "ignore_cec": "Ignorera CEC", + "uuid": "Till\u00e5tna UUID" + }, + "description": "Till\u00e5tna UUID - En kommaseparerad lista \u00f6ver UUID f\u00f6r Cast-enheter att l\u00e4gga till i Home Assistant. Anv\u00e4nd endast om du inte vill l\u00e4gga till alla tillg\u00e4ngliga cast-enheter.\n Ignorera CEC \u2013 En kommaseparerad lista \u00f6ver Chromecast-enheter som ska ignorera CEC-data f\u00f6r att fastst\u00e4lla den aktiva ing\u00e5ngen. Detta skickas till pychromecast.IGNORE_CEC.", + "title": "Avancerad Google Cast-konfiguration" + }, + "basic_options": { + "data": { + "known_hosts": "K\u00e4nad v\u00e4rdar" + }, + "description": "K\u00e4nda v\u00e4rdar - En kommaseparerad lista \u00f6ver v\u00e4rdnamn eller IP-adresser f\u00f6r cast-enheter, anv\u00e4nd om mDNS-uppt\u00e4ckt inte fungerar.", + "title": "Google Cast-konfiguration" } } } diff --git a/homeassistant/components/climacell/translations/sv.json b/homeassistant/components/climacell/translations/sv.json new file mode 100644 index 00000000000..2382ec64324 --- /dev/null +++ b/homeassistant/components/climacell/translations/sv.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "timestep": "Min. Mellan NowCast-prognoser" + }, + "description": "Om du v\u00e4ljer att aktivera \"nowcast\"-prognosentiteten kan du konfigurera antalet minuter mellan varje prognos. Antalet prognoser som tillhandah\u00e5lls beror p\u00e5 antalet minuter som v\u00e4ljs mellan prognoserna.", + "title": "Uppdatera ClimaCell-alternativ" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/sv.json b/homeassistant/components/cloud/translations/sv.json index 528e996272f..4711a333f49 100644 --- a/homeassistant/components/cloud/translations/sv.json +++ b/homeassistant/components/cloud/translations/sv.json @@ -10,6 +10,7 @@ "relayer_connected": "Vidarebefodrare Ansluten", "remote_connected": "Fj\u00e4rransluten", "remote_enabled": "Fj\u00e4rr\u00e5tkomst Aktiverad", + "remote_server": "Fj\u00e4rrserver", "subscription_expiration": "Prenumerationens utg\u00e5ng" } } diff --git a/homeassistant/components/cloudflare/translations/sv.json b/homeassistant/components/cloudflare/translations/sv.json new file mode 100644 index 00000000000..4d9f671a20d --- /dev/null +++ b/homeassistant/components/cloudflare/translations/sv.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u00c5terautentisering lyckades", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "invalid_zone": "Ogiltig zon" + }, + "flow_title": "{name}", + "step": { + "reauth_confirm": { + "data": { + "api_token": "API Token", + "description": "Autentisera p\u00e5 nytt med ditt Cloudflare-konto." + } + }, + "records": { + "data": { + "records": "Poster" + }, + "title": "V\u00e4lj de poster som ska uppdateras" + }, + "user": { + "data": { + "api_token": "API Token" + }, + "description": "Den h\u00e4r integrationen kr\u00e4ver en API-token som skapats med beh\u00f6righeter f\u00f6r Zone:Zone:Read och Zone:DNS:Edit f\u00f6r alla zoner i ditt konto.", + "title": "Anslut till Cloudflare" + }, + "zone": { + "data": { + "zone": "Zon" + }, + "title": "V\u00e4lj den zon som ska uppdateras" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/translations/sv.json b/homeassistant/components/coinbase/translations/sv.json index c63ebf1c09f..83c5e7c3f0e 100644 --- a/homeassistant/components/coinbase/translations/sv.json +++ b/homeassistant/components/coinbase/translations/sv.json @@ -1,8 +1,13 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", + "invalid_auth_key": "API-inloggningsuppgifter avvisades av Coinbase p\u00e5 grund av en ogiltig API-nyckel.", + "invalid_auth_secret": "API-inloggningsuppgifter avvisades av Coinbase p\u00e5 grund av en ogiltig API-hemlighet.", "unknown": "Ov\u00e4ntat fel" }, "step": { diff --git a/homeassistant/components/cpuspeed/translations/sv.json b/homeassistant/components/cpuspeed/translations/sv.json new file mode 100644 index 00000000000..532d7a14fe9 --- /dev/null +++ b/homeassistant/components/cpuspeed/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "not_compatible": "Det g\u00e5r inte att f\u00e5 CPU-information, denna integration \u00e4r inte kompatibel med ditt system" + }, + "step": { + "user": { + "description": "Vill du starta konfigurationen?", + "title": "CPU klockfrekvens" + } + } + }, + "title": "CPU klockfrekvens" +} \ No newline at end of file diff --git a/homeassistant/components/deluge/translations/sv.json b/homeassistant/components/deluge/translations/sv.json index 1a65fe29a6f..f77b1f5c552 100644 --- a/homeassistant/components/deluge/translations/sv.json +++ b/homeassistant/components/deluge/translations/sv.json @@ -1,11 +1,22 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { + "host": "V\u00e4rd", + "password": "L\u00f6senord", "port": "Port", - "username": "Anv\u00e4ndarnamn" - } + "username": "Anv\u00e4ndarnamn", + "web_port": "Webbport (f\u00f6r bes\u00f6kstj\u00e4nst)" + }, + "description": "F\u00f6r att kunna anv\u00e4nda denna integration m\u00e5ste du aktivera f\u00f6ljande alternativ i deluge-inst\u00e4llningarna: Daemon > Till\u00e5t fj\u00e4rrkontroller" } } } diff --git a/homeassistant/components/demo/translations/ca.json b/homeassistant/components/demo/translations/ca.json index 4ffda91d97c..19fc5a86e0b 100644 --- a/homeassistant/components/demo/translations/ca.json +++ b/homeassistant/components/demo/translations/ca.json @@ -1,5 +1,16 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Prem ENVIAR per confirmar que s'ha canviat la font d'alimentaci\u00f3", + "title": "La font d'alimentaci\u00f3 s'ha de canviar" + } + } + }, + "title": "La font d'alimentaci\u00f3 no \u00e9s estable" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/hu.json b/homeassistant/components/demo/translations/hu.json index 1ff83e4ed06..a5aa872fc5c 100644 --- a/homeassistant/components/demo/translations/hu.json +++ b/homeassistant/components/demo/translations/hu.json @@ -1,5 +1,16 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Nyomja meg a MEHET gombot a t\u00e1pegys\u00e9g cser\u00e9j\u00e9nek meger\u0151s\u00edt\u00e9s\u00e9hez.", + "title": "A t\u00e1pegys\u00e9get ki kell cser\u00e9lni" + } + } + }, + "title": "A t\u00e1pegys\u00e9g nem m\u0171k\u00f6dik megb\u00edzhat\u00f3 m\u00f3don" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/id.json b/homeassistant/components/demo/translations/id.json index e8f827e2b86..7acf127caa8 100644 --- a/homeassistant/components/demo/translations/id.json +++ b/homeassistant/components/demo/translations/id.json @@ -1,10 +1,21 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Tekan KIRIM untuk mengonfirmasi bahwa catu daya telah diganti", + "title": "Catu daya perlu diganti" + } + } + }, + "title": "Catu daya tidak stabil" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { "confirm": { - "description": "Tekan Oke saat cairan blinker telah diisi ulang", + "description": "Tekan KIRIM saat cairan blinker telah diisi ulang", "title": "Cairan blinker perlu diisi ulang" } } diff --git a/homeassistant/components/demo/translations/no.json b/homeassistant/components/demo/translations/no.json index 7cbf50d76bb..396c3a366f6 100644 --- a/homeassistant/components/demo/translations/no.json +++ b/homeassistant/components/demo/translations/no.json @@ -1,10 +1,21 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Trykk SUBMIT for \u00e5 bekrefte at str\u00f8mforsyningen er byttet ut", + "title": "Str\u00f8mforsyningen m\u00e5 skiftes" + } + } + }, + "title": "Str\u00f8mforsyningen er ikke stabil" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { "confirm": { - "description": "Trykk OK n\u00e5r blinklysv\u00e6ske er fylt p\u00e5 igjen", + "description": "Trykk SUBMIT n\u00e5r blinklysv\u00e6ske er fylt p\u00e5 igjen", "title": "Blinkerv\u00e6ske m\u00e5 etterfylles" } } diff --git a/homeassistant/components/demo/translations/pt-BR.json b/homeassistant/components/demo/translations/pt-BR.json index 26e903f345d..e38a9a1c7aa 100644 --- a/homeassistant/components/demo/translations/pt-BR.json +++ b/homeassistant/components/demo/translations/pt-BR.json @@ -1,5 +1,16 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Pressione ENVIAR para confirmar que a fonte de alimenta\u00e7\u00e3o foi substitu\u00edda", + "title": "A fonte de alimenta\u00e7\u00e3o precisa ser substitu\u00edda" + } + } + }, + "title": "A fonte de alimenta\u00e7\u00e3o n\u00e3o \u00e9 est\u00e1vel" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/sv.json b/homeassistant/components/demo/translations/sv.json index d88f06a3f99..f192cb8b544 100644 --- a/homeassistant/components/demo/translations/sv.json +++ b/homeassistant/components/demo/translations/sv.json @@ -1,5 +1,16 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Tryck p\u00e5 SKICKA f\u00f6r att bekr\u00e4fta att str\u00f6mf\u00f6rs\u00f6rjningen har bytts ut", + "title": "Str\u00f6mf\u00f6rs\u00f6rjningen m\u00e5ste bytas ut" + } + } + }, + "title": "Str\u00f6mf\u00f6rs\u00f6rjningen \u00e4r inte stabil" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/zh-Hant.json b/homeassistant/components/demo/translations/zh-Hant.json index a984c9c62ca..60f465316a0 100644 --- a/homeassistant/components/demo/translations/zh-Hant.json +++ b/homeassistant/components/demo/translations/zh-Hant.json @@ -1,10 +1,21 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u6309\u4e0b\u300c\u50b3\u9001\u300d\u4ee5\u78ba\u8a8d\u96fb\u6e90\u4f9b\u61c9\u5df2\u7d93\u66f4\u63db", + "title": "\u96fb\u6e90\u4f9b\u61c9\u9700\u8981\u66f4\u63db" + } + } + }, + "title": "\u96fb\u6e90\u4f9b\u61c9\u4e0d\u7a69\u5b9a" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { "confirm": { - "description": "\u65bc\u8a0a\u865f\u5291\u88dc\u5145\u5f8c\u6309\u4e0b OK", + "description": "\u65bc\u88dc\u5145\u8a0a\u865f\u5291\u5f8c\u6309\u4e0b\u300c\u50b3\u9001\u300d", "title": "\u8a0a\u865f\u5291\u9700\u8981\u88dc\u5145" } } diff --git a/homeassistant/components/denonavr/translations/sv.json b/homeassistant/components/denonavr/translations/sv.json index db692b7f835..288b5df5357 100644 --- a/homeassistant/components/denonavr/translations/sv.json +++ b/homeassistant/components/denonavr/translations/sv.json @@ -25,6 +25,9 @@ "user": { "data": { "host": "IP-adress" + }, + "data_description": { + "host": "L\u00e4mna tomt f\u00f6r att anv\u00e4nda automatisk uppt\u00e4ckt" } } } diff --git a/homeassistant/components/derivative/translations/sv.json b/homeassistant/components/derivative/translations/sv.json index 0f36236b1ae..5ea8260f731 100644 --- a/homeassistant/components/derivative/translations/sv.json +++ b/homeassistant/components/derivative/translations/sv.json @@ -5,12 +5,15 @@ "data": { "name": "Namn", "round": "Precision", + "source": "Ing\u00e5ngssensor", "time_window": "Tidsf\u00f6nster", "unit_prefix": "Metriskt prefix", "unit_time": "Tidsenhet" }, "data_description": { - "round": "Anger antal decimaler i resultatet." + "round": "Anger antal decimaler i resultatet.", + "time_window": "Om den \u00e4r inst\u00e4lld \u00e4r sensorns v\u00e4rde ett tidsviktat glidande medelv\u00e4rde av derivat inom detta f\u00f6nster.", + "unit_prefix": "Utdata kommer att skalas enligt det valda metriska prefixet och tidsenheten f\u00f6r derivatan." }, "description": "Skapa en sensor som ber\u00e4knar derivatan av en sensor", "title": "L\u00e4gg till derivatasensor" @@ -23,13 +26,15 @@ "data": { "name": "Namn", "round": "Precision", + "source": "Ing\u00e5ngssensor", "time_window": "Tidsf\u00f6nster", "unit_prefix": "Metriskt prefix", "unit_time": "Tidsenhet" }, "data_description": { "round": "Anger antal decimaler i resultatet.", - "unit_prefix": "." + "time_window": "Om den \u00e4r inst\u00e4lld \u00e4r sensorns v\u00e4rde ett tidsviktat glidande medelv\u00e4rde av derivat inom detta f\u00f6nster.", + "unit_prefix": "Utdata kommer att skalas enligt det valda metriska prefixet och tidsenheten f\u00f6r derivatan.." } } } diff --git a/homeassistant/components/deutsche_bahn/translations/ca.json b/homeassistant/components/deutsche_bahn/translations/ca.json new file mode 100644 index 00000000000..a4dc0724423 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/ca.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "La integraci\u00f3 Deutsche Bahn est\u00e0 sent eliminada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/de.json b/homeassistant/components/deutsche_bahn/translations/de.json new file mode 100644 index 00000000000..986aebfa7a6 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Die Deutsche Bahn-Integration wird derzeit aus Home Assistant entfernt und wird ab Home Assistant 2022.11 nicht mehr verf\u00fcgbar sein.\n\nDie Integration wird entfernt, weil sie auf Webscraping beruht, was nicht erlaubt ist.\n\nEntferne die YAML-Konfiguration der Deutschen Bahn aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Deutsche-Bahn-Integration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/hu.json b/homeassistant/components/deutsche_bahn/translations/hu.json new file mode 100644 index 00000000000..5b1bf7e95e6 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/hu.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "A Deutsche Bahn integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra v\u00e1r a Home Assistantb\u00f3l, \u00e9s a 2022.11-es Home Assistant-t\u00f3l m\u00e1r nem lesz el\u00e9rhet\u0151.\n\nAz integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl, mivel webscrapingre t\u00e1maszkodik, ami nem megengedett.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a Deutsche Bahn YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Deutsche Bahn integr\u00e1ci\u00f3 megsz\u00fcnik" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/id.json b/homeassistant/components/deutsche_bahn/translations/id.json new file mode 100644 index 00000000000..2bd68daf49e --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/id.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integrasi Deutsche Bahn sedang menunggu penghapusan dari Home Assistant dan tidak akan lagi tersedia pada Home Assistant 2022.11.\n\nIntegrasi ini dalam proses penghapusan, karena bergantung pada proses webscraping, yang tidak diizinkan.\n\nHapus konfigurasi Deutsche Bahn YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Integrasi Deutsche Bahn dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/no.json b/homeassistant/components/deutsche_bahn/translations/no.json new file mode 100644 index 00000000000..5852c5b716c --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/no.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Deutsche Bahn-integrasjonen venter p\u00e5 fjerning fra Home Assistant og vil ikke lenger v\u00e6re tilgjengelig fra og med Home Assistant 2022.11. \n\n Integrasjonen blir fjernet, fordi den er avhengig av webscraping, noe som ikke er tillatt. \n\n Fjern Deutsche Bahn YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Deutsche Bahn-integrasjonen fjernes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/pt-BR.json b/homeassistant/components/deutsche_bahn/translations/pt-BR.json new file mode 100644 index 00000000000..abf4f29a0e5 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "A integra\u00e7\u00e3o da Deutsche Bahn est\u00e1 com remo\u00e7\u00e3o pendente do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.11. \n\n A integra\u00e7\u00e3o est\u00e1 sendo removida, pois depende de webscraping, o que n\u00e3o \u00e9 permitido. \n\n Remova a configura\u00e7\u00e3o YAML da Deutsche Bahn do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o da Deutsche Bahn est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/sv.json b/homeassistant/components/deutsche_bahn/translations/sv.json new file mode 100644 index 00000000000..7595e8bed60 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/sv.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Deutsche Bahn-integrationen v\u00e4ntar p\u00e5 borttagning fr\u00e5n Home Assistant och kommer inte l\u00e4ngre att vara tillg\u00e4nglig fr\u00e5n och med Home Assistant 2022.11. \n\n Integrationen tas bort, eftersom den f\u00f6rlitar sig p\u00e5 webbskrapning, vilket inte \u00e4r till\u00e5tet. \n\n Ta bort Deutsche Bahn YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Deutsche Bahn-integrationen tas bort" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/zh-Hant.json b/homeassistant/components/deutsche_bahn/translations/zh-Hant.json new file mode 100644 index 00000000000..fd60b2ae7fb --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Deutsche Bahn \u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.11 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531\u65bc\u4f7f\u7528\u4e86\u4e0d\u88ab\u5141\u8a31\u7684\u7db2\u8def\u8cc7\u6599\u64f7\u53d6\uff08webscraping\uff09\u65b9\u5f0f\u3001\u6574\u5408\u5373\u5c07\u79fb\u9664\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Deutsche Bahn YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Deutsche Bahn \u6574\u5408\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/sv.json b/homeassistant/components/devolo_home_control/translations/sv.json index 3081604f882..c2b34e47a0f 100644 --- a/homeassistant/components/devolo_home_control/translations/sv.json +++ b/homeassistant/components/devolo_home_control/translations/sv.json @@ -1,7 +1,12 @@ { "config": { "abort": { - "already_configured": "Konto har redan konfigurerats" + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "reauth_failed": "Anv\u00e4nd samma mydevolo-anv\u00e4ndare som tidigare." }, "step": { "user": { diff --git a/homeassistant/components/devolo_home_network/translations/sv.json b/homeassistant/components/devolo_home_network/translations/sv.json new file mode 100644 index 00000000000..097e9d826b9 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/sv.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "home_control": "Devolo Home Control Central Unit fungerar inte med denna integration." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{product} ({name})", + "step": { + "user": { + "data": { + "ip_address": "IP-adress" + }, + "description": "Vill du starta konfigurationen?" + }, + "zeroconf_confirm": { + "description": "Vill du l\u00e4gga till devolos hemn\u00e4tverksenhet med v\u00e4rdnamnet ` {host_name} ` till Home Assistant?", + "title": "Uppt\u00e4ckte devolo hemn\u00e4tverksenhet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/sv.json b/homeassistant/components/dexcom/translations/sv.json index d82f098eb83..3dc87b0aaff 100644 --- a/homeassistant/components/dexcom/translations/sv.json +++ b/homeassistant/components/dexcom/translations/sv.json @@ -1,13 +1,30 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, "error": { "cannot_connect": "Det gick inte att ansluta.", - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "password": "L\u00f6senord", + "server": "Server", "username": "Anv\u00e4ndarnamn" + }, + "description": "Ange Dexcom Share-uppgifter", + "title": "St\u00e4ll in Dexcom-integration" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "M\u00e5ttenhet" } } } diff --git a/homeassistant/components/diagnostics/translations/sv.json b/homeassistant/components/diagnostics/translations/sv.json new file mode 100644 index 00000000000..732e52ee843 --- /dev/null +++ b/homeassistant/components/diagnostics/translations/sv.json @@ -0,0 +1,3 @@ +{ + "title": "Diagnostik" +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/sv.json b/homeassistant/components/dialogflow/translations/sv.json index daf0f4f3ea6..bc2d5d27ecf 100644 --- a/homeassistant/components/dialogflow/translations/sv.json +++ b/homeassistant/components/dialogflow/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", "webhook_not_internet_accessible": "Din Home Assistant instans m\u00e5ste kunna n\u00e5s fr\u00e5n Internet f\u00f6r att ta emot webhook meddelanden" }, diff --git a/homeassistant/components/discord/translations/sv.json b/homeassistant/components/discord/translations/sv.json index 38370827354..4526346ddf6 100644 --- a/homeassistant/components/discord/translations/sv.json +++ b/homeassistant/components/discord/translations/sv.json @@ -13,12 +13,14 @@ "reauth_confirm": { "data": { "api_token": "API Token" - } + }, + "description": "Se dokumentationen om hur du skaffar din Discord-botnyckel. \n\n {url}" }, "user": { "data": { "api_token": "API Token" - } + }, + "description": "Se dokumentationen om hur du skaffar din Discord-botnyckel. \n\n {url}" } } } diff --git a/homeassistant/components/dlna_dmr/translations/sv.json b/homeassistant/components/dlna_dmr/translations/sv.json index 8f95dda75db..dd2a1fb7491 100644 --- a/homeassistant/components/dlna_dmr/translations/sv.json +++ b/homeassistant/components/dlna_dmr/translations/sv.json @@ -1,10 +1,55 @@ { + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "alternative_integration": "Enheten st\u00f6ds b\u00e4ttre av en annan integration", + "cannot_connect": "Det gick inte att ansluta.", + "discovery_error": "Det gick inte att uppt\u00e4cka en matchande DLNA-enhet", + "incomplete_config": "Konfigurationen saknar en n\u00f6dv\u00e4ndig variabel", + "non_unique_id": "Flera enheter hittades med samma unika ID", + "not_dmr": "Enheten \u00e4r inte en digital mediarenderare som st\u00f6ds" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "not_dmr": "Enheten \u00e4r inte en digital mediarenderare som st\u00f6ds" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vill du starta konfigurationen?" + }, + "import_turn_on": { + "description": "Sl\u00e5 p\u00e5 enheten och klicka p\u00e5 skicka f\u00f6r att forts\u00e4tta migreringen" + }, + "manual": { + "data": { + "url": "URL" + }, + "description": "URL till en XML-fil f\u00f6r enhetsbeskrivning", + "title": "Manuell DLNA DMR-enhetsanslutning" + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "V\u00e4lj en enhet att konfigurera eller l\u00e4mna tomt f\u00f6r att ange en URL", + "title": "Uppt\u00e4ckte DLNA DMR-enheter" + } + } + }, "options": { + "error": { + "invalid_url": "Ogiltig URL" + }, "step": { "init": { "data": { - "browse_unfiltered": "Visa inkompatibla media n\u00e4r du surfar" - } + "browse_unfiltered": "Visa inkompatibla media n\u00e4r du surfar", + "callback_url_override": "URL f\u00f6r \u00e5teruppringning av h\u00e4ndelseavlyssnare", + "listen_port": "H\u00e4ndelseavlyssnarport (slumpm\u00e4ssig om inte inst\u00e4llt)", + "poll_availability": "Fr\u00e5ga efter om en enhet \u00e4r tillg\u00e4nglig" + }, + "title": "Konfiguration av DLNA Digital Media Renderer" } } } diff --git a/homeassistant/components/dlna_dms/translations/sv.json b/homeassistant/components/dlna_dms/translations/sv.json new file mode 100644 index 00000000000..6bf1cda2bd8 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/sv.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "bad_ssdp": "SSDP-data saknar ett obligatoriskt v\u00e4rde", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "not_dms": "Enheten \u00e4r inte en mediaserver som st\u00f6ds" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vill du starta konfigurationen?" + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "V\u00e4lj en enhet att konfigurera", + "title": "Uppt\u00e4ckta DLNA DMA-enheter" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dnsip/translations/sv.json b/homeassistant/components/dnsip/translations/sv.json new file mode 100644 index 00000000000..f5cc073d7a3 --- /dev/null +++ b/homeassistant/components/dnsip/translations/sv.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "invalid_hostname": "Ogiltigt v\u00e4rdnamn" + }, + "step": { + "user": { + "data": { + "hostname": "V\u00e4rdnamnet f\u00f6r vilket DNS-fr\u00e5gan ska utf\u00f6ras", + "resolver": "Resolver f\u00f6r IPV4-s\u00f6kning", + "resolver_ipv6": "Resolver f\u00f6r IPV6-s\u00f6kning" + } + } + } + }, + "options": { + "error": { + "invalid_resolver": "Ogiltig IP-adress f\u00f6r resolver" + }, + "step": { + "init": { + "data": { + "resolver": "Resolver f\u00f6r IPV4-s\u00f6kning", + "resolver_ipv6": "Resolver f\u00f6r IPV6-s\u00f6kning" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/sv.json b/homeassistant/components/doorbird/translations/sv.json index d91cc55a3be..547af936d47 100644 --- a/homeassistant/components/doorbird/translations/sv.json +++ b/homeassistant/components/doorbird/translations/sv.json @@ -27,6 +27,9 @@ "init": { "data": { "events": "Kommaseparerad lista \u00f6ver h\u00e4ndelser." + }, + "data_description": { + "events": "L\u00e4gg till ett kommaseparerat h\u00e4ndelsenamn f\u00f6r varje h\u00e4ndelse du vill sp\u00e5ra. N\u00e4r du har angett dem h\u00e4r, anv\u00e4nd DoorBird-appen f\u00f6r att tilldela dem till en specifik h\u00e4ndelse. \n\n Exempel: n\u00e5gon_tryckte p\u00e5_knappen, r\u00f6relse" } } } diff --git a/homeassistant/components/dsmr/translations/sv.json b/homeassistant/components/dsmr/translations/sv.json index 7ad8f5f0b09..09eba96b2dc 100644 --- a/homeassistant/components/dsmr/translations/sv.json +++ b/homeassistant/components/dsmr/translations/sv.json @@ -35,7 +35,8 @@ "user": { "data": { "type": "Anslutningstyp" - } + }, + "title": "V\u00e4lj anslutningstyp" } } }, diff --git a/homeassistant/components/eafm/translations/sv.json b/homeassistant/components/eafm/translations/sv.json new file mode 100644 index 00000000000..8fdf2a9f064 --- /dev/null +++ b/homeassistant/components/eafm/translations/sv.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "no_stations": "Inga \u00f6vervakningsstationer f\u00f6r \u00f6versv\u00e4mningar hittades." + }, + "step": { + "user": { + "data": { + "station": "Station" + }, + "description": "V\u00e4lj den station du vill \u00f6vervaka", + "title": "Sp\u00e5ra en station f\u00f6r \u00f6vervakning av \u00f6versv\u00e4mningar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/translations/sv.json b/homeassistant/components/ecobee/translations/sv.json index d78d5ba521e..84d699cc928 100644 --- a/homeassistant/components/ecobee/translations/sv.json +++ b/homeassistant/components/ecobee/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, "error": { "pin_request_failed": "Fel vid beg\u00e4ran av PIN-kod fr\u00e5n ecobee. kontrollera API-nyckeln \u00e4r korrekt.", "token_request_failed": "Fel vid beg\u00e4ran av tokens fr\u00e5n ecobee; v\u00e4nligen f\u00f6rs\u00f6k igen." diff --git a/homeassistant/components/efergy/translations/sv.json b/homeassistant/components/efergy/translations/sv.json index b163c1a520b..623afec73af 100644 --- a/homeassistant/components/efergy/translations/sv.json +++ b/homeassistant/components/efergy/translations/sv.json @@ -1,10 +1,13 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { - "invalid_auth": "Ogiltig autentisering" + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { diff --git a/homeassistant/components/elkm1/translations/sv.json b/homeassistant/components/elkm1/translations/sv.json index 305329feff4..003f12b54b3 100644 --- a/homeassistant/components/elkm1/translations/sv.json +++ b/homeassistant/components/elkm1/translations/sv.json @@ -3,6 +3,8 @@ "abort": { "address_already_configured": "En ElkM1 med denna adress \u00e4r redan konfigurerad", "already_configured": "En ElkM1 med detta prefix \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, @@ -11,19 +13,34 @@ "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{mac_address} ({host})", "step": { "discovered_connection": { "data": { + "password": "L\u00f6senord", + "protocol": "Protokoll", + "temperature_unit": "Temperaturenheten ElkM1 anv\u00e4nder.", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Anslut till det uppt\u00e4ckta systemet: {mac_address} ( {host} )", + "title": "Anslut till Elk-M1 Control" }, "manual_connection": { "data": { + "address": "IP-adressen eller dom\u00e4nen eller seriell port om anslutning via seriell.", + "password": "L\u00f6senord", + "prefix": "Ett unikt prefix (l\u00e4mna tomt om du bara har en ElkM1).", "protocol": "Protokoll", + "temperature_unit": "Temperaturenheten ElkM1 anv\u00e4nder.", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Adressstr\u00e4ngen m\u00e5ste ha formen \"address[:port]\" f\u00f6r \"s\u00e4ker\" och \"icke-s\u00e4ker\". Exempel: '192.168.1.1'. Porten \u00e4r valfri och har som standard 2101 f\u00f6r \"icke-s\u00e4ker\" och 2601 f\u00f6r \"s\u00e4ker\". F\u00f6r det seriella protokollet m\u00e5ste adressen vara i formen 'tty[:baud]'. Exempel: '/dev/ttyS1'. Bauden \u00e4r valfri och \u00e4r som standard 115200.", + "title": "Anslut till Elk-M1 Control" }, "user": { + "data": { + "device": "Enhet" + }, "description": "V\u00e4lj ett uppt\u00e4ckt system eller \"Manuell inmatning\" om inga enheter har uppt\u00e4ckts.", "title": "Anslut till Elk-M1 Control" } diff --git a/homeassistant/components/elmax/translations/sv.json b/homeassistant/components/elmax/translations/sv.json index 23c825f256f..cd87c97b3e3 100644 --- a/homeassistant/components/elmax/translations/sv.json +++ b/homeassistant/components/elmax/translations/sv.json @@ -1,10 +1,30 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "invalid_pin": "Den angivna pin-koden \u00e4r ogiltig", + "network_error": "Ett n\u00e4tverksfel uppstod", + "no_panel_online": "Ingen Elmax kontrollpanel online hittades.", + "unknown": "Ov\u00e4ntat fel" + }, "step": { + "panels": { + "data": { + "panel_id": "Panel-ID", + "panel_name": "Panelnamn", + "panel_pin": "Pinkod" + }, + "description": "V\u00e4lj vilken panel du vill styra med denna integration. Observera att panelen m\u00e5ste vara p\u00e5 f\u00f6r att kunna konfigureras." + }, "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "V\u00e4nligen logga in p\u00e5 Elmax-molnet med dina uppgifter" } } } diff --git a/homeassistant/components/enphase_envoy/translations/sv.json b/homeassistant/components/enphase_envoy/translations/sv.json index 3889eae836b..5117a6fcd20 100644 --- a/homeassistant/components/enphase_envoy/translations/sv.json +++ b/homeassistant/components/enphase_envoy/translations/sv.json @@ -16,7 +16,8 @@ "host": "V\u00e4rd", "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "F\u00f6r nyare modeller, ange anv\u00e4ndarnamnet \"envoy\" utan l\u00f6senord. F\u00f6r \u00e4ldre modeller, ange anv\u00e4ndarnamnet `installer` utan l\u00f6senord. F\u00f6r alla andra modeller, ange ett giltigt anv\u00e4ndarnamn och l\u00f6senord." } } } diff --git a/homeassistant/components/environment_canada/translations/sv.json b/homeassistant/components/environment_canada/translations/sv.json new file mode 100644 index 00000000000..8bd3af75502 --- /dev/null +++ b/homeassistant/components/environment_canada/translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "bad_station_id": "Stations-ID \u00e4r ogiltigt, saknas eller finns inte i stations-ID-databasen", + "cannot_connect": "Det gick inte att ansluta.", + "error_response": "Svar fr\u00e5n Environment Canada felaktigt", + "too_many_attempts": "Anslutningar till Environment Canada \u00e4r hastighetsbegr\u00e4nsade; F\u00f6rs\u00f6k igen om 60 sekunder.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "language": "Spr\u00e5k f\u00f6r v\u00e4derinformation", + "latitude": "Latitud", + "longitude": "Longitud", + "station": "V\u00e4derstations-ID" + }, + "description": "Antingen ett stations-ID eller latitud/longitud m\u00e5ste anges. Standardlatitud/longitud som anv\u00e4nds \u00e4r de v\u00e4rden som konfigurerats i din Home Assistant-installation. Den v\u00e4derstation som ligger n\u00e4rmast koordinaterna kommer att anv\u00e4ndas om du anger koordinater. Om en stationskod anv\u00e4nds m\u00e5ste den f\u00f6lja formatet: PP/kod, d\u00e4r PP \u00e4r tv\u00e5bokstavsprovinsen och kod \u00e4r stations-ID. Listan \u00f6ver stations-ID:n finns h\u00e4r: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. V\u00e4derinformation kan h\u00e4mtas p\u00e5 antingen engelska eller franska.", + "title": "Environment Canada: v\u00e4derplats och spr\u00e5k" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/epson/translations/sv.json b/homeassistant/components/epson/translations/sv.json index 45224016263..b933bf9485e 100644 --- a/homeassistant/components/epson/translations/sv.json +++ b/homeassistant/components/epson/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "powered_off": "\u00c4r projektorn p\u00e5slagen? Du m\u00e5ste sl\u00e5 p\u00e5 projektorn f\u00f6r den f\u00f6rsta konfigurationen." }, "step": { "user": { diff --git a/homeassistant/components/evil_genius_labs/translations/sv.json b/homeassistant/components/evil_genius_labs/translations/sv.json index d51f98c6100..77cc9e74352 100644 --- a/homeassistant/components/evil_genius_labs/translations/sv.json +++ b/homeassistant/components/evil_genius_labs/translations/sv.json @@ -2,6 +2,7 @@ "config": { "error": { "cannot_connect": "Det gick inte att ansluta.", + "timeout": "Timeout uppr\u00e4ttar anslutning", "unknown": "Ov\u00e4ntat fel" }, "step": { diff --git a/homeassistant/components/ezviz/translations/sv.json b/homeassistant/components/ezviz/translations/sv.json index 971e996a103..2ea759456b7 100644 --- a/homeassistant/components/ezviz/translations/sv.json +++ b/homeassistant/components/ezviz/translations/sv.json @@ -1,29 +1,41 @@ { "config": { "abort": { + "already_configured_account": "Konto har redan konfigurerats", + "ezviz_cloud_account_missing": "Ezviz molnkonto saknas. V\u00e4nligen konfigurera om Ezviz molnkonto", "unknown": "Ov\u00e4ntat fel" }, "error": { - "cannot_connect": "Kunde inte ansluta" + "cannot_connect": "Kunde inte ansluta", + "invalid_auth": "Ogiltig autentisering", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress" }, + "flow_title": "{serial}", "step": { "confirm": { "data": { "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange RTSP-uppgifter f\u00f6r Ezviz-kamera {serial} med IP {ip_address}", + "title": "Uppt\u00e4ckte Ezviz Camera" }, "user": { "data": { "password": "L\u00f6senord", + "url": "URL", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Anslut till Ezviz Cloud" }, "user_custom_url": { "data": { "password": "L\u00f6senord", + "url": "URL", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange din regions URL manuellt", + "title": "Anslut till anpassad Ezviz URL" } } }, @@ -31,6 +43,7 @@ "step": { "init": { "data": { + "ffmpeg_arguments": "Argument som skickas till ffmpeg f\u00f6r kameror", "timeout": "Timeout f\u00f6r beg\u00e4ran (sekunder)" } } diff --git a/homeassistant/components/faa_delays/translations/sv.json b/homeassistant/components/faa_delays/translations/sv.json index bd797004301..cfa624c113d 100644 --- a/homeassistant/components/faa_delays/translations/sv.json +++ b/homeassistant/components/faa_delays/translations/sv.json @@ -1,14 +1,20 @@ { "config": { + "abort": { + "already_configured": "Denna flygplats \u00e4r redan konfigurerad." + }, "error": { "cannot_connect": "Kunde inte ansluta", + "invalid_airport": "Flygplatskoden \u00e4r inte giltig", "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { "id": "Flygplats" - } + }, + "description": "Ange en amerikansk flygplatskod i IATA-format", + "title": "FAA f\u00f6rseningar" } } } diff --git a/homeassistant/components/fan/translations/sv.json b/homeassistant/components/fan/translations/sv.json index 31df690b766..d061669532f 100644 --- a/homeassistant/components/fan/translations/sv.json +++ b/homeassistant/components/fan/translations/sv.json @@ -10,6 +10,7 @@ "is_on": "{entity_name} \u00e4r p\u00e5" }, "trigger_type": { + "changed_states": "{entity_name} slogs p\u00e5 eller av", "turned_off": "{entity_name} st\u00e4ngdes av", "turned_on": "{entity_name} aktiverades" } diff --git a/homeassistant/components/fibaro/translations/sv.json b/homeassistant/components/fibaro/translations/sv.json index 89cfc8f6c3b..e35b7b41d46 100644 --- a/homeassistant/components/fibaro/translations/sv.json +++ b/homeassistant/components/fibaro/translations/sv.json @@ -11,7 +11,9 @@ "step": { "user": { "data": { + "import_plugins": "Importera enheter fr\u00e5n fibaro plugin?", "password": "L\u00f6senord", + "url": "URL i formatet http://HOST/api/", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/filesize/translations/sv.json b/homeassistant/components/filesize/translations/sv.json index c0b662beebe..4bc5b562d6a 100644 --- a/homeassistant/components/filesize/translations/sv.json +++ b/homeassistant/components/filesize/translations/sv.json @@ -2,6 +2,18 @@ "config": { "abort": { "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "error": { + "not_allowed": "S\u00f6kv\u00e4gen \u00e4r inte till\u00e5ten", + "not_valid": "S\u00f6kv\u00e4gen \u00e4r inte giltig" + }, + "step": { + "user": { + "data": { + "file_path": "S\u00f6kv\u00e4g till filen" + } + } } - } + }, + "title": "Filstorlek" } \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/sv.json b/homeassistant/components/fireservicerota/translations/sv.json index 23c825f256f..79167d65334 100644 --- a/homeassistant/components/fireservicerota/translations/sv.json +++ b/homeassistant/components/fireservicerota/translations/sv.json @@ -1,8 +1,26 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "create_entry": { + "default": "Autentiserats" + }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, "step": { + "reauth": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Autentiseringstokens blev ogiltiga, logga in f\u00f6r att \u00e5terskapa dem." + }, "user": { "data": { + "password": "L\u00f6senord", + "url": "Webbplats", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/fivem/translations/sv.json b/homeassistant/components/fivem/translations/sv.json new file mode 100644 index 00000000000..01ce62450d5 --- /dev/null +++ b/homeassistant/components/fivem/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta. Kontrollera v\u00e4rden och porten och f\u00f6rs\u00f6k igen. Se ocks\u00e5 till att du k\u00f6r den senaste FiveM-servern.", + "invalid_game_name": "API:et f\u00f6r spelet du f\u00f6rs\u00f6ker ansluta till \u00e4r inte ett FiveM-spel.", + "unknown_error": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/sv.json b/homeassistant/components/flipr/translations/sv.json index ec835585137..84bdebc9ba9 100644 --- a/homeassistant/components/flipr/translations/sv.json +++ b/homeassistant/components/flipr/translations/sv.json @@ -22,7 +22,8 @@ "email": "E-post", "password": "L\u00f6senord" }, - "description": "Anslut med ditt Flipr-konto." + "description": "Anslut med ditt Flipr-konto.", + "title": "Anslut till Flipr" } } } diff --git a/homeassistant/components/flo/translations/sv.json b/homeassistant/components/flo/translations/sv.json index a07a2c509bc..f85a02855d6 100644 --- a/homeassistant/components/flo/translations/sv.json +++ b/homeassistant/components/flo/translations/sv.json @@ -4,11 +4,14 @@ "already_configured": "Enheten \u00e4r redan konfigurerad" }, "error": { - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "host": "V\u00e4rd", "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } diff --git a/homeassistant/components/flunearyou/translations/de.json b/homeassistant/components/flunearyou/translations/de.json index 61dd2cd4ce7..72109df534c 100644 --- a/homeassistant/components/flunearyou/translations/de.json +++ b/homeassistant/components/flunearyou/translations/de.json @@ -16,5 +16,18 @@ "title": "Konfiguriere Grippe in deiner N\u00e4he" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "Die externe Datenquelle, aus der die Integration von Flu Near You gespeist wird, ist nicht mehr verf\u00fcgbar; daher funktioniert die Integration nicht mehr.\n\nDr\u00fccke SUBMIT, um Flu Near You aus deiner Home Assistant-Instanz zu entfernen.", + "title": "Flu Near You entfernen" + } + } + }, + "title": "Flu Near You ist nicht mehr verf\u00fcgbar" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/hu.json b/homeassistant/components/flunearyou/translations/hu.json index a67bc91a2a1..efda42723b4 100644 --- a/homeassistant/components/flunearyou/translations/hu.json +++ b/homeassistant/components/flunearyou/translations/hu.json @@ -16,5 +16,18 @@ "title": "Flu Near You weboldal konfigur\u00e1l\u00e1sa" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "A Flu Near You integr\u00e1ci\u00f3t m\u0171k\u00f6dtet\u0151 k\u00fcls\u0151 adatforr\u00e1s m\u00e1r nem el\u00e9rhet\u0151, \u00edgy az integr\u00e1ci\u00f3 m\u00e1r nem m\u0171k\u00f6dik.\n\nNyomja meg a MEHET gombot a Flu Near You elt\u00e1vol\u00edt\u00e1s\u00e1hoz a Home Assistantb\u00f3l.", + "title": "Flu Near You elt\u00e1vol\u00edt\u00e1sa" + } + } + }, + "title": "Flu Near You m\u00e1r nem el\u00e9rhet\u0151" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/ja.json b/homeassistant/components/flunearyou/translations/ja.json index 23df88d984b..116800656a6 100644 --- a/homeassistant/components/flunearyou/translations/ja.json +++ b/homeassistant/components/flunearyou/translations/ja.json @@ -16,5 +16,17 @@ "title": "\u8fd1\u304f\u306eFlu\u3092\u8a2d\u5b9a" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "title": "\u8fd1\u304f\u306eFlu Near You\u3092\u524a\u9664" + } + } + }, + "title": "Flu Near You\u306f\u3001\u5229\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3057\u305f" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/no.json b/homeassistant/components/flunearyou/translations/no.json index b958aea2ab5..898889bc348 100644 --- a/homeassistant/components/flunearyou/translations/no.json +++ b/homeassistant/components/flunearyou/translations/no.json @@ -16,5 +16,18 @@ "title": "Konfigurere influensa i n\u00e6rheten av deg" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "Den eksterne datakilden som driver Flu Near You-integrasjonen er ikke lenger tilgjengelig; dermed fungerer ikke integreringen lenger. \n\n Trykk SUBMIT for \u00e5 fjerne Flu Near You fra Home Assistant-forekomsten.", + "title": "Fjern influensa n\u00e6r deg" + } + } + }, + "title": "Flu Near You er ikke lenger tilgjengelig" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/ru.json b/homeassistant/components/flunearyou/translations/ru.json index e4694cf31b9..d9f184e05a6 100644 --- a/homeassistant/components/flunearyou/translations/ru.json +++ b/homeassistant/components/flunearyou/translations/ru.json @@ -16,5 +16,18 @@ "title": "Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0412\u043d\u0435\u0448\u043d\u0438\u0439 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u0434\u0430\u043d\u043d\u044b\u0445, \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u044e\u0449\u0438\u0439 \u0440\u0430\u0431\u043e\u0442\u0443 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Flu Near You, \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d.\n\n\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c, \u0447\u0442\u043e\u0431\u044b \u0443\u0434\u0430\u043b\u0438\u0442\u044c Flu Near You \u0438\u0437 \u0412\u0430\u0448\u0435\u0433\u043e Home Assistant.", + "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Flu Near You" + } + } + }, + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Flu Near You \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/sv.json b/homeassistant/components/flunearyou/translations/sv.json index eb41d4ff78f..0ce55468d86 100644 --- a/homeassistant/components/flunearyou/translations/sv.json +++ b/homeassistant/components/flunearyou/translations/sv.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Dessa koordinater \u00e4r redan registrerade." }, + "error": { + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { @@ -13,5 +16,18 @@ "title": "Konfigurera influensa i n\u00e4rheten av dig" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "Den externa datak\u00e4llan som driver Flu Near You-integrationen \u00e4r inte l\u00e4ngre tillg\u00e4nglig; allts\u00e5 fungerar inte integrationen l\u00e4ngre. \n\n Tryck p\u00e5 SUBMIT f\u00f6r att ta bort Flu Near You fr\u00e5n din Home Assistant-instans.", + "title": "Ta bort influensa n\u00e4ra dig" + } + } + }, + "title": "Influensa n\u00e4ra dig \u00e4r inte l\u00e4ngre tillg\u00e4nglig" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/zh-Hant.json b/homeassistant/components/flunearyou/translations/zh-Hant.json index f3273349f1f..42b975c451b 100644 --- a/homeassistant/components/flunearyou/translations/zh-Hant.json +++ b/homeassistant/components/flunearyou/translations/zh-Hant.json @@ -16,5 +16,18 @@ "title": "\u8a2d\u5b9a Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "The external data source powering the Flu Near You \u6574\u5408\u6240\u4f7f\u7528\u7684\u5916\u90e8\u8cc7\u6599\u4f86\u6e90\u5df2\u7d93\u7121\u6cd5\u4f7f\u7528\uff0c\u6574\u5408\u7121\u6cd5\u4f7f\u7528\u3002\n\n\u6309\u4e0b\u300c\u50b3\u9001\u300d\u4ee5\u79fb\u9664\u7531 Home Assistant \u79fb\u9664 Flu Near You\u3002", + "title": "\u79fb\u9664 Flu Near You" + } + } + }, + "title": "Flu Near You \u5df2\u7d93\u7121\u6cd5\u4f7f\u7528" + } } } \ No newline at end of file diff --git a/homeassistant/components/forecast_solar/translations/sv.json b/homeassistant/components/forecast_solar/translations/sv.json index 8a1e0911c8d..e21c5cc38fb 100644 --- a/homeassistant/components/forecast_solar/translations/sv.json +++ b/homeassistant/components/forecast_solar/translations/sv.json @@ -9,7 +9,8 @@ "longitude": "Longitud", "modules power": "Total maxeffekt (Watt) p\u00e5 dina solpaneler", "name": "Namn" - } + }, + "description": "Fyll i data f\u00f6r dina solpaneler. Se dokumentationen om ett f\u00e4lt \u00e4r otydligt." } } }, @@ -20,8 +21,11 @@ "api_key": "API-nyckel f\u00f6r Forecast.Solar (valfritt)", "azimuth": "Azimuth (360 grader, 0 = norr, 90 = \u00f6st, 180 = s\u00f6der, 270 = v\u00e4ster)", "damping": "D\u00e4mpningsfaktor: justerar resultaten p\u00e5 morgonen och kv\u00e4llen", - "declination": "Deklination (0 = horisontell, 90 = vertikal)" - } + "declination": "Deklination (0 = horisontell, 90 = vertikal)", + "inverter_size": "V\u00e4xelriktarens storlek (Watt)", + "modules power": "Total maxeffekt (Watt) p\u00e5 dina solpaneler" + }, + "description": "Dessa v\u00e4rden till\u00e5ter justering av Solar.Forecast-resultatet. Se dokumentationen om ett f\u00e4lt \u00e4r otydligt." } } } diff --git a/homeassistant/components/forked_daapd/translations/sv.json b/homeassistant/components/forked_daapd/translations/sv.json index 3e36427c525..e155ee1bd20 100644 --- a/homeassistant/components/forked_daapd/translations/sv.json +++ b/homeassistant/components/forked_daapd/translations/sv.json @@ -1,6 +1,15 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "not_forked_daapd": "Enheten \u00e4r inte en forked-daapd-server." + }, "error": { + "forbidden": "Kan inte ansluta. Kontrollera dina forked-daapd-n\u00e4tverksbeh\u00f6righeter.", + "unknown_error": "Ov\u00e4ntat fel", + "websocket_not_enabled": "forked-daapd server websocket inte aktiverat.", + "wrong_host_or_port": "Kan inte ansluta. Kontrollera v\u00e4rd och port.", + "wrong_password": "Felaktigt l\u00f6senord.", "wrong_server_type": "Forked-daapd-integrationen kr\u00e4ver en forked-daapd-server med version > = 27.0." }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/foscam/translations/sv.json b/homeassistant/components/foscam/translations/sv.json index 9de4793eabe..10974a23d65 100644 --- a/homeassistant/components/foscam/translations/sv.json +++ b/homeassistant/components/foscam/translations/sv.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", + "invalid_response": "Ogiltigt svar fr\u00e5n enheten", "unknown": "Ov\u00e4ntat fel" }, "step": { @@ -14,6 +15,7 @@ "host": "V\u00e4rd", "password": "L\u00f6senord", "port": "Port", + "rtsp_port": "RTSP-port", "stream": "Str\u00f6m", "username": "Anv\u00e4ndarnamn" } diff --git a/homeassistant/components/fritz/translations/sv.json b/homeassistant/components/fritz/translations/sv.json index f29f7f0ba78..6f1fd326587 100644 --- a/homeassistant/components/fritz/translations/sv.json +++ b/homeassistant/components/fritz/translations/sv.json @@ -10,7 +10,8 @@ "already_configured": "Enheten \u00e4r redan konfigurerad", "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", "cannot_connect": "Det gick inte att ansluta.", - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "upnp_not_configured": "UPnP-inst\u00e4llningar saknas p\u00e5 enheten." }, "flow_title": "{name}", "step": { @@ -46,7 +47,8 @@ "step": { "init": { "data": { - "consider_home": "Sekunder att \u00f6verv\u00e4ga en enhet hemma" + "consider_home": "Sekunder att \u00f6verv\u00e4ga en enhet hemma", + "old_discovery": "Aktivera gammal uppt\u00e4cktsmetod" } } } diff --git a/homeassistant/components/fronius/translations/sv.json b/homeassistant/components/fronius/translations/sv.json index f341a6314ee..43d6170132b 100644 --- a/homeassistant/components/fronius/translations/sv.json +++ b/homeassistant/components/fronius/translations/sv.json @@ -1,17 +1,24 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress" }, "error": { "cannot_connect": "Det gick inte att ansluta.", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{device}", "step": { + "confirm_discovery": { + "description": "Vill du l\u00e4gga till {device} i Home Assistant?" + }, "user": { "data": { "host": "V\u00e4rd" - } + }, + "description": "Konfigurera IP-adressen eller det lokala v\u00e4rdnamnet f\u00f6r din Fronius-enhet.", + "title": "Fronius SolarNet" } } } diff --git a/homeassistant/components/generic/translations/sv.json b/homeassistant/components/generic/translations/sv.json index 9a4067bbcfc..616931824b9 100644 --- a/homeassistant/components/generic/translations/sv.json +++ b/homeassistant/components/generic/translations/sv.json @@ -5,9 +5,15 @@ "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, "error": { + "already_exists": "Det finns redan en kamera med dessa URL-inst\u00e4llningar.", + "invalid_still_image": "URL returnerade inte en giltig stillbild", "malformed_url": "Ogiltig URL", + "no_still_image_or_stream_url": "Du m\u00e5ste ange \u00e5tminstone en stillbilds- eller stream-URL", "relative_url": "Relativa URL:er \u00e4r inte till\u00e5tna", + "stream_file_not_found": "Filen hittades inte n\u00e4r du f\u00f6rs\u00f6kte ansluta till str\u00f6m (\u00e4r ffmpeg installerat?)", + "stream_http_not_found": "HTTP 404 Ej funnen n\u00e4r du f\u00f6rs\u00f6ker ansluta till str\u00f6mmen", "stream_io_error": "Inmatnings-/utg\u00e5ngsfel vid f\u00f6rs\u00f6k att ansluta till stream. Fel RTSP-transportprotokoll?", + "stream_no_route_to_host": "Kunde inte hitta v\u00e4rddatorn n\u00e4r jag f\u00f6rs\u00f6kte ansluta till str\u00f6mmen", "stream_no_video": "Str\u00f6mmen har ingen video", "stream_not_permitted": "\u00c5tg\u00e4rden \u00e4r inte till\u00e5ten n\u00e4r du f\u00f6rs\u00f6ker ansluta till streamen. Fel RTSP-transportprotokoll?", "stream_unauthorised": "Auktoriseringen misslyckades n\u00e4r du f\u00f6rs\u00f6kte ansluta till str\u00f6mmen", @@ -44,10 +50,22 @@ }, "options": { "error": { + "already_exists": "Det finns redan en kamera med dessa URL-inst\u00e4llningar.", + "invalid_still_image": "URL returnerade inte en giltig stillbild", "malformed_url": "Ogiltig URL", + "no_still_image_or_stream_url": "Du m\u00e5ste ange \u00e5tminstone en stillbilds- eller stream-URL", "relative_url": "Relativa URL:er \u00e4r inte till\u00e5tet", + "stream_file_not_found": "Filen hittades inte n\u00e4r du f\u00f6rs\u00f6kte ansluta till str\u00f6m (\u00e4r ffmpeg installerat?)", + "stream_http_not_found": "HTTP 404 Ej funnen n\u00e4r du f\u00f6rs\u00f6ker ansluta till str\u00f6mmen", + "stream_io_error": "Inmatnings-/utg\u00e5ngsfel vid f\u00f6rs\u00f6k att ansluta till stream. Fel RTSP-transportprotokoll?", + "stream_no_route_to_host": "Kunde inte hitta v\u00e4rddatorn n\u00e4r jag f\u00f6rs\u00f6kte ansluta till str\u00f6mmen", + "stream_no_video": "Str\u00f6mmen har ingen video", + "stream_not_permitted": "\u00c5tg\u00e4rden \u00e4r inte till\u00e5ten n\u00e4r du f\u00f6rs\u00f6ker ansluta till streamen. Fel RTSP-transportprotokoll?", + "stream_unauthorised": "Auktoriseringen misslyckades n\u00e4r du f\u00f6rs\u00f6kte ansluta till str\u00f6mmen", "template_error": "Problem att rendera mall. Kolla i loggen f\u00f6r mer information.", - "timeout": "Timeout vid h\u00e4mtning fr\u00e5n URL" + "timeout": "Timeout vid h\u00e4mtning fr\u00e5n URL", + "unable_still_load": "Det g\u00e5r inte att ladda giltig bild fr\u00e5n stillbilds-URL (t.ex. ogiltig v\u00e4rd, URL eller autentiseringsfel). Granska loggen f\u00f6r mer information.", + "unknown": "Ov\u00e4ntat fel" }, "step": { "content_type": { @@ -59,6 +77,9 @@ "init": { "data": { "authentication": "Autentiseringen", + "framerate": "Bildfrekvens (Hz)", + "limit_refetch_to_url_change": "Begr\u00e4nsa \u00e5terh\u00e4mtning till \u00e4ndring av webbadress", + "password": "L\u00f6senord", "rtsp_transport": "RTSP transportprotokoll", "still_image_url": "URL f\u00f6r stillbild (t.ex. http://...)", "stream_source": "URL f\u00f6r str\u00f6mk\u00e4lla (t.ex. rtsp://...)", diff --git a/homeassistant/components/github/translations/de.json b/homeassistant/components/github/translations/de.json index 904d15774d9..0d195210744 100644 --- a/homeassistant/components/github/translations/de.json +++ b/homeassistant/components/github/translations/de.json @@ -5,7 +5,7 @@ "could_not_register": "Integration konnte nicht mit GitHub registriert werden" }, "progress": { - "wait_for_device": "1. \u00d6ffne {url}\n 2. F\u00fcge den folgenden Schl\u00fcssel ein, um die Integration zu autorisieren:\n ```\n {code}\n ```\n" + "wait_for_device": "1. \u00d6ffne {url}\n 2. F\u00fcge den folgenden Schl\u00fcssel ein, um die Integration zu autorisieren:\n ```\n {code}\n ```" }, "step": { "repositories": { diff --git a/homeassistant/components/github/translations/sv.json b/homeassistant/components/github/translations/sv.json new file mode 100644 index 00000000000..9f7a956f729 --- /dev/null +++ b/homeassistant/components/github/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "could_not_register": "Det gick inte att registrera integration med GitHub" + }, + "progress": { + "wait_for_device": "1. \u00d6ppna {url}\n 2.Klistra in f\u00f6ljande nyckel f\u00f6r att auktorisera integrationen:\n ```\n {code}\n ```\n" + }, + "step": { + "repositories": { + "data": { + "repositories": "V\u00e4lj de arkiv som ska sp\u00e5ras." + }, + "title": "Konfigurera arkiv" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goodwe/translations/sv.json b/homeassistant/components/goodwe/translations/sv.json index 360862118d4..717b6bfaffc 100644 --- a/homeassistant/components/goodwe/translations/sv.json +++ b/homeassistant/components/goodwe/translations/sv.json @@ -11,7 +11,9 @@ "user": { "data": { "host": "IP-adress" - } + }, + "description": "Anslut till v\u00e4xelriktare", + "title": "GoodWe v\u00e4xelriktare" } } } diff --git a/homeassistant/components/google/translations/sv.json b/homeassistant/components/google/translations/sv.json index d1e92e9b8a0..499ea375547 100644 --- a/homeassistant/components/google/translations/sv.json +++ b/homeassistant/components/google/translations/sv.json @@ -5,8 +5,32 @@ "config": { "abort": { "already_configured": "Konto har redan konfigurerats", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", "cannot_connect": "Det gick inte att ansluta.", + "code_expired": "Autentiseringskoden har l\u00f6pt ut eller autentiseringsinst\u00e4llningarna \u00e4r ogiltig, f\u00f6rs\u00f6k igen.", + "invalid_access_token": "Ogiltig \u00e5tkomstnyckel", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "oauth_error": "Mottog ogiltiga tokendata.", + "reauth_successful": "\u00c5terautentisering lyckades", "timeout_connect": "Timeout vid anslutningsf\u00f6rs\u00f6k" + }, + "create_entry": { + "default": "Autentiserats" + }, + "progress": { + "exchange": "F\u00f6r att l\u00e4nka ditt Google-konto bes\u00f6ker du [ {url} ]( {url} ) och anger koden: \n\n {user_code}" + }, + "step": { + "auth": { + "title": "L\u00e4nka Google-konto" + }, + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + }, + "reauth_confirm": { + "description": "Google Kalender-integrationen m\u00e5ste autentisera ditt konto igen", + "title": "\u00c5terautenticera integration" + } } }, "issues": { diff --git a/homeassistant/components/gpslogger/translations/sv.json b/homeassistant/components/gpslogger/translations/sv.json index f73ba63337d..794d5959541 100644 --- a/homeassistant/components/gpslogger/translations/sv.json +++ b/homeassistant/components/gpslogger/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." }, diff --git a/homeassistant/components/group/translations/sv.json b/homeassistant/components/group/translations/sv.json index 93cbf568052..3382dc7a5ee 100644 --- a/homeassistant/components/group/translations/sv.json +++ b/homeassistant/components/group/translations/sv.json @@ -5,17 +5,34 @@ "data": { "all": "Alla entiteter", "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar", "name": "Namn" }, + "description": "Om \"alla enheter\" \u00e4r aktiverat \u00e4r gruppens status endast aktiverad om alla medlemmar \u00e4r p\u00e5. Om \"alla enheter\" \u00e4r inaktiverat \u00e4r gruppens status p\u00e5 om n\u00e5gon medlem \u00e4r p\u00e5.", "title": "Ny grupp" }, "cover": { + "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar", + "name": "Namn" + }, "title": "L\u00e4gg till grupp" }, "fan": { + "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar", + "name": "Namn" + }, "title": "L\u00e4gg till grupp" }, "light": { + "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar", + "name": "Namn" + }, "title": "L\u00e4gg till grupp" }, "lock": { @@ -27,17 +44,31 @@ "title": "L\u00e4gg till grupp" }, "media_player": { + "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar", + "name": "Namn" + }, "title": "L\u00e4gg till grupp" }, "switch": { "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar", "name": "Namn" }, "title": "L\u00e4gg till grupp" }, "user": { + "description": "Med grupper kan du skapa en ny enhet som representerar flera enheter av samma typ.", "menu_options": { - "lock": "L\u00e5sgrupp" + "binary_sensor": "Bin\u00e4r sensorgrupp", + "cover": "T\u00e4ckningsgrupp", + "fan": "Fl\u00e4ktgrupp", + "light": "Ljusgrupp", + "lock": "L\u00e5sgrupp", + "media_player": "Grupp av mediespelare", + "switch": "Brytargrupp" }, "title": "L\u00e4gg till grupp" } @@ -48,20 +79,50 @@ "binary_sensor": { "data": { "all": "Alla entiteter", - "entities": "Medlemmar" + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar" + }, + "description": "Om \"alla enheter\" \u00e4r aktiverat \u00e4r gruppens status endast aktiverad om alla medlemmar \u00e4r p\u00e5. Om \"alla enheter\" \u00e4r inaktiverat \u00e4r gruppens status p\u00e5 om n\u00e5gon medlem \u00e4r p\u00e5." + }, + "cover": { + "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar" } }, + "fan": { + "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar" + } + }, + "light": { + "data": { + "all": "Alla entiteter", + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar" + }, + "description": "Om \"alla enheter\" \u00e4r aktiverat \u00e4r gruppens status endast aktiverad om alla medlemmar \u00e4r p\u00e5. Om \"alla enheter\" \u00e4r inaktiverat \u00e4r gruppens status p\u00e5 om n\u00e5gon medlem \u00e4r p\u00e5." + }, "lock": { "data": { "entities": "Medlemmar", "hide_members": "D\u00f6lj medlemmar" } }, + "media_player": { + "data": { + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar" + } + }, "switch": { "data": { "all": "Alla entiteter", - "entities": "Medlemmar" - } + "entities": "Medlemmar", + "hide_members": "D\u00f6lj medlemmar" + }, + "description": "Om \"alla enheter\" \u00e4r aktiverat \u00e4r gruppens status endast aktiverad om alla medlemmar \u00e4r p\u00e5. Om \"alla enheter\" \u00e4r inaktiverat \u00e4r gruppens status p\u00e5 om n\u00e5gon medlem \u00e4r p\u00e5." } } }, diff --git a/homeassistant/components/heos/translations/sv.json b/homeassistant/components/heos/translations/sv.json index 100f8fd83a5..f1538d3b780 100644 --- a/homeassistant/components/heos/translations/sv.json +++ b/homeassistant/components/heos/translations/sv.json @@ -3,6 +3,9 @@ "abort": { "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/hive/translations/de.json b/homeassistant/components/hive/translations/de.json index e40fd0f499f..e40c7a1adcb 100644 --- a/homeassistant/components/hive/translations/de.json +++ b/homeassistant/components/hive/translations/de.json @@ -24,7 +24,7 @@ "data": { "device_name": "Ger\u00e4tename" }, - "description": "Gib deine Hive-Konfiguration ein ", + "description": "Gib deine Hive-Konfiguration ein", "title": "Hive-Konfiguration." }, "reauth": { diff --git a/homeassistant/components/homekit/translations/sv.json b/homeassistant/components/homekit/translations/sv.json index caddbeaf7d2..474e595769f 100644 --- a/homeassistant/components/homekit/translations/sv.json +++ b/homeassistant/components/homekit/translations/sv.json @@ -19,6 +19,12 @@ }, "options": { "step": { + "accessory": { + "data": { + "entities": "Entitet" + }, + "title": "V\u00e4lj entiteten f\u00f6r tillbeh\u00f6ret" + }, "advanced": { "data": { "devices": "Enheter (utl\u00f6sare)" @@ -28,13 +34,30 @@ }, "cameras": { "data": { + "camera_audio": "Kameror som st\u00f6der ljud", "camera_copy": "Kameror som st\u00f6der inbyggda H.264-str\u00f6mmar" }, "description": "Kontrollera alla kameror som st\u00f6der inbyggda H.264-str\u00f6mmar. Om kameran inte skickar ut en H.264-str\u00f6m kodar systemet videon till H.264 f\u00f6r HomeKit. Transkodning kr\u00e4ver h\u00f6g prestanda och kommer troligtvis inte att fungera p\u00e5 enkortsdatorer.", "title": "V\u00e4lj kamerans videoavkodare." }, + "exclude": { + "data": { + "entities": "Entiteter" + }, + "description": "Alla \" {domains} \"-enheter kommer att inkluderas f\u00f6rutom de exkluderade enheterna och kategoriserade enheterna.", + "title": "V\u00e4lj de enheter som ska exkluderas" + }, + "include": { + "data": { + "entities": "Entiteter" + }, + "description": "Alla \" {domains} \"-enheter kommer att inkluderas om inte specifika enheter har valts.", + "title": "V\u00e4lj de entiteter som ska inkluderas" + }, "init": { "data": { + "domains": "Dom\u00e4ner att inkludera", + "include_exclude_mode": "Inkluderingsl\u00e4ge", "mode": "HomeKit-l\u00e4ge" }, "description": "HomeKit kan konfigureras f\u00f6r att exponera en bro eller ett enskilt tillbeh\u00f6r. I tillbeh\u00f6rsl\u00e4get kan endast en enda enhet anv\u00e4ndas. Tillbeh\u00f6rsl\u00e4ge kr\u00e4vs f\u00f6r att mediaspelare med enhetsklassen TV ska fungera korrekt. Enheter i \"Dom\u00e4ner att inkludera\" kommer att inkluderas i HomeKit. Du kommer att kunna v\u00e4lja vilka enheter som ska inkluderas eller uteslutas fr\u00e5n listan p\u00e5 n\u00e4sta sk\u00e4rm.", diff --git a/homeassistant/components/homekit_controller/translations/select.sv.json b/homeassistant/components/homekit_controller/translations/select.sv.json new file mode 100644 index 00000000000..a97da943fda --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/select.sv.json @@ -0,0 +1,9 @@ +{ + "state": { + "homekit_controller__ecobee_mode": { + "away": "Borta", + "home": "Hemma", + "sleep": "Vilol\u00e4ge" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.de.json b/homeassistant/components/homekit_controller/translations/sensor.de.json new file mode 100644 index 00000000000..2aef45f5303 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.de.json @@ -0,0 +1,21 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Border Router-f\u00e4hig", + "full": "Vollst\u00e4ndiges Endger\u00e4t", + "minimal": "Minimales Endger\u00e4t", + "none": "Keine", + "router_eligible": "Router-f\u00e4higes Endger\u00e4t", + "sleepy": "Sleepy Endger\u00e4t" + }, + "homekit_controller__thread_status": { + "border_router": "Border-Router", + "child": "Kind", + "detached": "Freistehend", + "disabled": "Deaktiviert", + "joining": "Beitreten", + "leader": "Anf\u00fchrer", + "router": "Router" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.hu.json b/homeassistant/components/homekit_controller/translations/sensor.hu.json new file mode 100644 index 00000000000..9c6de41a56b --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.hu.json @@ -0,0 +1,21 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Hat\u00e1rol\u00f3 \u00fatv\u00e1laszt\u00f3 k\u00e9pess\u00e9g", + "full": "Teljes k\u00e9pess\u00e9g\u0171 v\u00e9gberendez\u00e9s", + "minimal": "Minim\u00e1lis k\u00e9pess\u00e9g\u0171 v\u00e9gberendez\u00e9s", + "none": "Nincs", + "router_eligible": "\u00datv\u00e1laszt\u00f3ra alkalmas v\u00e9geszk\u00f6z", + "sleepy": "Alv\u00f3 v\u00e9gberendez\u00e9s" + }, + "homekit_controller__thread_status": { + "border_router": "Hat\u00e1rol\u00f3 \u00fatv\u00e1laszt\u00f3", + "child": "Gyermek", + "detached": "Lev\u00e1lasztva", + "disabled": "Letiltva", + "joining": "Csatlakoz\u00e1s", + "leader": "Vezet\u0151", + "router": "\u00datv\u00e1laszt\u00f3" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.ja.json b/homeassistant/components/homekit_controller/translations/sensor.ja.json new file mode 100644 index 00000000000..a2f246f11b3 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.ja.json @@ -0,0 +1,21 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "\u5883\u754c(Border)\u30eb\u30fc\u30bf\u30fc\u5bfe\u5fdc", + "full": "\u30d5\u30eb\u30a8\u30f3\u30c9\u30c7\u30d0\u30a4\u30b9", + "minimal": "\u6700\u5c0f\u9650\u306e\u30a8\u30f3\u30c9 \u30c7\u30d0\u30a4\u30b9", + "none": "\u306a\u3057", + "router_eligible": "\u30eb\u30fc\u30bf\u30fc\u306e\u9069\u683c\u306a\u30a8\u30f3\u30c9 \u30c7\u30d0\u30a4\u30b9", + "sleepy": "\u30b9\u30ea\u30fc\u30d4\u30fc \u30a8\u30f3\u30c9 \u30c7\u30d0\u30a4\u30b9" + }, + "homekit_controller__thread_status": { + "border_router": "\u5883\u754c(Border)\u30eb\u30fc\u30bf\u30fc", + "child": "\u5b50(Child)", + "detached": "\u5207\u308a\u96e2\u3055\u308c\u305f", + "disabled": "\u7121\u52b9", + "joining": "\u63a5\u5408(Joining)", + "leader": "\u30ea\u30fc\u30c0\u30fc", + "router": "\u30eb\u30fc\u30bf\u30fc" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.no.json b/homeassistant/components/homekit_controller/translations/sensor.no.json index f0323c0326a..997b3fe34e9 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.no.json +++ b/homeassistant/components/homekit_controller/translations/sensor.no.json @@ -7,6 +7,15 @@ "none": "Ingen", "router_eligible": "Ruterkvalifisert sluttenhet", "sleepy": "S\u00f8vnig sluttenhet" + }, + "homekit_controller__thread_status": { + "border_router": "Border Router", + "child": "Barn", + "detached": "Frakoblet", + "disabled": "Deaktivert", + "joining": "Blir med", + "leader": "Leder", + "router": "Ruter" } } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.sv.json b/homeassistant/components/homekit_controller/translations/sensor.sv.json new file mode 100644 index 00000000000..61058201e61 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.sv.json @@ -0,0 +1,21 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Gr\u00e4nsrouter kapabel", + "full": "Fullst\u00e4ndig slutenhet", + "minimal": "Minimal slutenhet", + "none": "Ingen", + "router_eligible": "Routerkvalificerad slutenhet", + "sleepy": "S\u00f6mnig slutenhet" + }, + "homekit_controller__thread_status": { + "border_router": "Gr\u00e4nsrouter", + "child": "Barn", + "detached": "Frist\u00e5ende", + "disabled": "Inaktiverad", + "joining": "Ansluter sig till", + "leader": "Ledare", + "router": "Router" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.zh-Hant.json b/homeassistant/components/homekit_controller/translations/sensor.zh-Hant.json index 123469d79cf..78925b1ff0c 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.zh-Hant.json +++ b/homeassistant/components/homekit_controller/translations/sensor.zh-Hant.json @@ -1,12 +1,21 @@ { "state": { "homekit_controller__thread_node_capabilities": { - "border_router_capable": "\u7db2\u8def\u6838\u5fc3\u80fd\u529b", + "border_router_capable": "Border Router \u80fd\u529b", "full": "\u6240\u6709\u7d42\u7aef\u88dd\u7f6e", "minimal": "\u6700\u4f4e\u7d42\u7aef\u88dd\u7f6e", "none": "\u7121", - "router_eligible": "\u4e2d\u9593\u5c64\u7d42\u7aef\u88dd\u7f6e", + "router_eligible": "Router \u7d42\u7aef\u88dd\u7f6e", "sleepy": "\u5f85\u547d\u7d42\u7aef\u88dd\u7f6e" + }, + "homekit_controller__thread_status": { + "border_router": "Border Router", + "child": "Child", + "detached": "Detached", + "disabled": "\u95dc\u9589", + "joining": "\u52a0\u5165", + "leader": "Leader", + "router": "Router" } } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sv.json b/homeassistant/components/homekit_controller/translations/sv.json index 1766689ca69..aea1202b352 100644 --- a/homeassistant/components/homekit_controller/translations/sv.json +++ b/homeassistant/components/homekit_controller/translations/sv.json @@ -12,6 +12,7 @@ }, "error": { "authentication_error": "Felaktig HomeKit-kod. V\u00e4nligen kontrollera och f\u00f6rs\u00f6k igen.", + "insecure_setup_code": "Den beg\u00e4rda installationskoden \u00e4r os\u00e4ker p\u00e5 grund av dess triviala natur. Detta tillbeh\u00f6r uppfyller inte grundl\u00e4ggande s\u00e4kerhetskrav.", "max_peers_error": "Enheten nekade parningsf\u00f6rs\u00f6ket d\u00e5 det inte finns n\u00e5got parningsminnesutrymme kvar", "pairing_failed": "Ett ok\u00e4nt fel uppstod n\u00e4r parningsf\u00f6rs\u00f6ket gjordes med den h\u00e4r enheten. Det h\u00e4r kan vara ett tillf\u00e4lligt fel, eller s\u00e5 st\u00f6ds inte din enhet i nul\u00e4get.", "unable_to_pair": "Det g\u00e5r inte att para ihop, f\u00f6rs\u00f6k igen.", @@ -29,6 +30,7 @@ }, "pair": { "data": { + "allow_insecure_setup_codes": "Till\u00e5t parning med os\u00e4kra installationskoder.", "pairing_code": "Parningskod" }, "description": "HomeKit Controller kommunicerar med {name} \u00f6ver det lokala n\u00e4tverket med hj\u00e4lp av en s\u00e4ker krypterad anslutning utan en separat HomeKit-kontroller eller iCloud. Ange din HomeKit-kopplingskod (i formatet XXX-XX-XXX) f\u00f6r att anv\u00e4nda detta tillbeh\u00f6r. Denna kod finns vanligtvis p\u00e5 sj\u00e4lva enheten eller i f\u00f6rpackningen.", diff --git a/homeassistant/components/homewizard/translations/sv.json b/homeassistant/components/homewizard/translations/sv.json index c9bc9cd66a9..554f347eee1 100644 --- a/homeassistant/components/homewizard/translations/sv.json +++ b/homeassistant/components/homewizard/translations/sv.json @@ -1,7 +1,22 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "api_not_enabled": "API:et \u00e4r inte aktiverat. Aktivera API i HomeWizard Energy App under inst\u00e4llningar", + "device_not_supported": "Den h\u00e4r enheten st\u00f6ds inte", + "invalid_discovery_parameters": "Uppt\u00e4ckte en API-version som inte st\u00f6ds", + "unknown_error": "Ov\u00e4ntat fel" + }, "step": { + "discovery_confirm": { + "description": "Vill du konfigurera {product_type} ( {serial} ) p\u00e5 {ip_address} ?", + "title": "Bekr\u00e4fta" + }, "user": { + "data": { + "ip_address": "IP-adress" + }, + "description": "Ange IP-adressen f\u00f6r din HomeWizard Energy-enhet f\u00f6r att integrera med Home Assistant.", "title": "Konfigurera enhet" } } diff --git a/homeassistant/components/honeywell/translations/sv.json b/homeassistant/components/honeywell/translations/sv.json index 287e24372ba..6629effa6b6 100644 --- a/homeassistant/components/honeywell/translations/sv.json +++ b/homeassistant/components/honeywell/translations/sv.json @@ -12,5 +12,16 @@ "description": "Ange de autentiseringsuppgifter som anv\u00e4nds f\u00f6r att logga in p\u00e5 mytotalconnectcomfort.com." } } + }, + "options": { + "step": { + "init": { + "data": { + "away_cool_temperature": "Borta kylningstemperatur", + "away_heat_temperature": "Borta uppv\u00e4rmingstemperatur" + }, + "description": "Ytterligare Honeywell-konfigurationsalternativ. Temperaturen \u00e4r inst\u00e4lld i Fahrenheit." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/sv.json b/homeassistant/components/huawei_lte/translations/sv.json index 4efe112d468..b58dcb07da2 100644 --- a/homeassistant/components/huawei_lte/translations/sv.json +++ b/homeassistant/components/huawei_lte/translations/sv.json @@ -18,6 +18,7 @@ "user": { "data": { "password": "L\u00f6senord", + "url": "URL", "username": "Anv\u00e4ndarnamn" }, "description": "Ange information om enhets\u00e5tkomst. Det \u00e4r valfritt att ange anv\u00e4ndarnamn och l\u00f6senord, men st\u00f6djer d\u00e5 fler integrationsfunktioner. \u00c5 andra sidan kan anv\u00e4ndning av en auktoriserad anslutning orsaka problem med att komma \u00e5t enhetens webbgr\u00e4nssnitt utanf\u00f6r Home Assistant medan integrationen \u00e4r aktiv och tv\u00e4rtom.", diff --git a/homeassistant/components/hue/translations/sv.json b/homeassistant/components/hue/translations/sv.json index f09232f92ab..519c35370fa 100644 --- a/homeassistant/components/hue/translations/sv.json +++ b/homeassistant/components/hue/translations/sv.json @@ -25,6 +25,12 @@ "link": { "description": "Tryck p\u00e5 knappen p\u00e5 bryggan f\u00f6r att registrera Philips Hue med Home Assistant. \n\n![Placering av knapp p\u00e5 brygga](/static/images/config_philips_hue.jpg)", "title": "L\u00e4nka hub" + }, + "manual": { + "data": { + "host": "V\u00e4rd" + }, + "title": "Manuellt konfigurera en Hue-brygga" } } }, @@ -63,7 +69,9 @@ "init": { "data": { "allow_hue_groups": "Till\u00e5t Hue-grupper", - "allow_hue_scenes": "Till\u00e5t Hue-scener" + "allow_hue_scenes": "Till\u00e5t Hue-scener", + "allow_unreachable": "Till\u00e5t o\u00e5tkomliga gl\u00f6dlampor att rapportera sitt tillst\u00e5nd korrekt", + "ignore_availability": "Ignorera anslutningsstatus f\u00f6r de givna enheterna" } } } diff --git a/homeassistant/components/huisbaasje/translations/sv.json b/homeassistant/components/huisbaasje/translations/sv.json index 4a6100815d6..6cc1e2b35ee 100644 --- a/homeassistant/components/huisbaasje/translations/sv.json +++ b/homeassistant/components/huisbaasje/translations/sv.json @@ -1,11 +1,17 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { - "cannot_connect": "Kunde inte ansluta" + "cannot_connect": "Kunde inte ansluta", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/humidifier/translations/sv.json b/homeassistant/components/humidifier/translations/sv.json index a500d9dc18e..985bb8eb820 100644 --- a/homeassistant/components/humidifier/translations/sv.json +++ b/homeassistant/components/humidifier/translations/sv.json @@ -13,6 +13,7 @@ "is_on": "{entity_name} \u00e4r p\u00e5" }, "trigger_type": { + "changed_states": "{entity_name} slogs p\u00e5 eller av", "target_humidity_changed": "{entity_name} m\u00e5lfuktighet har \u00e4ndrats", "turned_off": "{entity_name} st\u00e4ngdes av", "turned_on": "{entity_name} slogs p\u00e5" diff --git a/homeassistant/components/iaqualink/translations/sv.json b/homeassistant/components/iaqualink/translations/sv.json index e697b5a02b9..bf65527243d 100644 --- a/homeassistant/components/iaqualink/translations/sv.json +++ b/homeassistant/components/iaqualink/translations/sv.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, "error": { - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" }, "step": { "user": { diff --git a/homeassistant/components/icloud/translations/sv.json b/homeassistant/components/icloud/translations/sv.json index b0a052ecef4..bb07cc8291e 100644 --- a/homeassistant/components/icloud/translations/sv.json +++ b/homeassistant/components/icloud/translations/sv.json @@ -6,6 +6,7 @@ "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { + "invalid_auth": "Ogiltig autentisering", "send_verification_code": "Det gick inte att skicka verifieringskod", "validate_verification_code": "Det gick inte att verifiera verifieringskoden, v\u00e4lj en betrodd enhet och starta verifieringen igen" }, diff --git a/homeassistant/components/ifttt/translations/sv.json b/homeassistant/components/ifttt/translations/sv.json index 16ec51d62c2..0559bc82afa 100644 --- a/homeassistant/components/ifttt/translations/sv.json +++ b/homeassistant/components/ifttt/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." }, diff --git a/homeassistant/components/insteon/translations/sv.json b/homeassistant/components/insteon/translations/sv.json index 7b1692e7ec9..fa4c69f369a 100644 --- a/homeassistant/components/insteon/translations/sv.json +++ b/homeassistant/components/insteon/translations/sv.json @@ -1,9 +1,14 @@ { "config": { "abort": { + "cannot_connect": "Det gick inte att ansluta.", "not_insteon_device": "Uppt\u00e4ckt enhet \u00e4r inte en Insteon-enhet", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "select_single": "V\u00e4lj ett alternativ." + }, "flow_title": "{name}", "step": { "confirm_usb": { @@ -27,6 +32,13 @@ "description": "Konfigurera Insteon Hub version 2.", "title": "Insteon Hub version 2" }, + "plm": { + "data": { + "device": "USB-enhetens s\u00f6kv\u00e4g" + }, + "description": "Konfigurera Insteon PowerLink-modemet (PLM).", + "title": "Insteon PLM" + }, "user": { "data": { "modem_type": "Modemtyp." @@ -36,9 +48,15 @@ } }, "options": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "input_error": "Ogiltiga poster, kontrollera dina v\u00e4rden.", + "select_single": "V\u00e4lj ett alternativ." + }, "step": { "add_override": { "data": { + "address": "Enhetsadress (dvs. 1a2b3c)", "cat": "Enhetskategori (dvs. 0x10)", "subcat": "Enhetsunderkategori (dvs. 0x0a)" }, diff --git a/homeassistant/components/integration/translations/sv.json b/homeassistant/components/integration/translations/sv.json index e0c12be775f..edba53d6310 100644 --- a/homeassistant/components/integration/translations/sv.json +++ b/homeassistant/components/integration/translations/sv.json @@ -2,17 +2,30 @@ "config": { "step": { "user": { + "data": { + "method": "Integrationsmetod", + "name": "Namn", + "round": "Precision", + "source": "Ing\u00e5ngssensor", + "unit_prefix": "Metriskt prefix", + "unit_time": "Tidsenhet" + }, "data_description": { "round": "Anger antal decimaler i resultatet.", "unit_prefix": "Utdata kommer att skalas enligt det valda metriska prefixet.", "unit_time": "Utg\u00e5ngen kommer att skalas enligt den valda tidsenheten." - } + }, + "description": "Skapa en sensor som ber\u00e4knar en Riemanns summa f\u00f6r att uppskatta integralen av en sensor.", + "title": "L\u00e4gg till Riemann summa integral sensor" } } }, "options": { "step": { "init": { + "data": { + "round": "Precision" + }, "data_description": { "round": "Anger antal decimaler i resultatet." } diff --git a/homeassistant/components/intellifire/translations/sv.json b/homeassistant/components/intellifire/translations/sv.json index 05685de670b..cb50a5ca6a0 100644 --- a/homeassistant/components/intellifire/translations/sv.json +++ b/homeassistant/components/intellifire/translations/sv.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad", + "not_intellifire_device": "Inte en IntelliFire-enhet.", "reauth_successful": "Omautentiseringen lyckades" }, "error": { @@ -9,6 +10,7 @@ "cannot_connect": "Det gick inte att ansluta.", "iftapi_connect": "Fel vid anslutning till iftapi.net" }, + "flow_title": "{serial} ({host})", "step": { "api_config": { "data": { @@ -16,7 +18,19 @@ "username": "E-postadress" } }, + "dhcp_confirm": { + "description": "Vill du konfigurera {host}\nSerie: {serial}?" + }, + "manual_device_entry": { + "data": { + "host": "V\u00e4rd (IP-adress)" + }, + "description": "Lokal konfiguration" + }, "pick_device": { + "data": { + "host": "V\u00e4rd" + }, "description": "F\u00f6ljande IntelliFire-enheter uppt\u00e4cktes. V\u00e4lj vilken du vill konfigurera.", "title": "Val av enhet" } diff --git a/homeassistant/components/iotawatt/translations/sv.json b/homeassistant/components/iotawatt/translations/sv.json index 500a65ce603..acf1f77a107 100644 --- a/homeassistant/components/iotawatt/translations/sv.json +++ b/homeassistant/components/iotawatt/translations/sv.json @@ -2,12 +2,20 @@ "config": { "error": { "cannot_connect": "Det gick inte att ansluta.", - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "auth": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" + }, + "description": "IoTawatt-enheten kr\u00e4ver autentisering. Ange anv\u00e4ndarnamn och l\u00f6senord och klicka p\u00e5 knappen Skicka." + }, + "user": { + "data": { + "host": "V\u00e4rd" } } } diff --git a/homeassistant/components/iss/translations/sv.json b/homeassistant/components/iss/translations/sv.json new file mode 100644 index 00000000000..0962f763548 --- /dev/null +++ b/homeassistant/components/iss/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "latitude_longitude_not_defined": "Latitud och longitud definieras inte i Home Assistant.", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du konfigurera den internationella rymdstationen (ISS)?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Visa p\u00e5 karta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/sv.json b/homeassistant/components/kaleidescape/translations/sv.json index c5cfbbb662a..6adb8993b5d 100644 --- a/homeassistant/components/kaleidescape/translations/sv.json +++ b/homeassistant/components/kaleidescape/translations/sv.json @@ -1,5 +1,25 @@ { "config": { - "flow_title": "{model} ({name})" + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "unknown": "Ov\u00e4ntat fel", + "unsupported": "Enhet som inte st\u00f6ds" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unsupported": "Enhet som inte st\u00f6ds" + }, + "flow_title": "{model} ({name})", + "step": { + "discovery_confirm": { + "description": "Vill du st\u00e4lla in {model} -spelaren med namnet {name} ?" + }, + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/translations/sv.json b/homeassistant/components/keenetic_ndms2/translations/sv.json index 1e263250636..e0bce87762a 100644 --- a/homeassistant/components/keenetic_ndms2/translations/sv.json +++ b/homeassistant/components/keenetic_ndms2/translations/sv.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "Konto har redan konfigurerats" + "already_configured": "Konto har redan konfigurerats", + "no_udn": "SSDP-uppt\u00e4cktsinformation har ingen UDN", + "not_keenetic_ndms2": "Den uppt\u00e4ckta enheten \u00e4r inte en Keenetic-router" }, "error": { "cannot_connect": "Det gick inte att ansluta." }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/knx/translations/sv.json b/homeassistant/components/knx/translations/sv.json index 2c2d6f7d14d..8abd3264e18 100644 --- a/homeassistant/components/knx/translations/sv.json +++ b/homeassistant/components/knx/translations/sv.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "file_not_found": "Den angivna `.knxkeys`-filen hittades inte i s\u00f6kv\u00e4gen config/.storage/knx/", "invalid_individual_address": "V\u00e4rdet matchar inte m\u00f6nstret f\u00f6r en individuell adress i KNX.\n'area.line.device'", "invalid_ip_address": "Ogiltig IPv4-adress.", @@ -9,16 +14,22 @@ "step": { "manual_tunnel": { "data": { + "host": "V\u00e4rd", + "local_ip": "Lokal IP f\u00f6r Home Assistant", + "port": "Port", "tunneling_type": "KNX tunneltyp" }, "data_description": { "host": "IP-adressen f\u00f6r KNX/IP-tunnelenheten.", "local_ip": "L\u00e4mna tomt f\u00f6r att anv\u00e4nda automatisk uppt\u00e4ckt.", "port": "Port p\u00e5 KNX/IP-tunnelenheten." - } + }, + "description": "Ange anslutningsinformationen f\u00f6r din tunnelenhet." }, "routing": { "data": { + "individual_address": "Individuell adress", + "local_ip": "Lokal IP f\u00f6r Home Assistant", "multicast_group": "Multicast-grupp", "multicast_port": "Multicast-port" }, @@ -79,6 +90,7 @@ "data": { "connection_type": "KNX anslutningstyp", "individual_address": "Enskild standardadress", + "local_ip": "Lokal IP f\u00f6r Home Assistant", "multicast_group": "Multicast-grupp", "multicast_port": "Multicast-port", "rate_limit": "Hastighetsgr\u00e4ns", diff --git a/homeassistant/components/kodi/translations/sv.json b/homeassistant/components/kodi/translations/sv.json index b65fb101fd2..8d242b673d8 100644 --- a/homeassistant/components/kodi/translations/sv.json +++ b/homeassistant/components/kodi/translations/sv.json @@ -2,7 +2,10 @@ "config": { "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad", - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "no_uuid": "Kodi-instansen har inte ett unikt ID. Detta beror troligen p\u00e5 en gammal Kodi-version (17.x eller l\u00e4gre). Du kan konfigurera integrationen manuellt eller uppgradera till en nyare Kodi-version.", + "unknown": "Ov\u00e4ntat fel" }, "error": { "cannot_connect": "Det gick inte att ansluta.", @@ -24,8 +27,11 @@ }, "user": { "data": { - "host": "V\u00e4rd" - } + "host": "V\u00e4rd", + "port": "Port", + "ssl": "Anv\u00e4nd ett SSL certifikat" + }, + "description": "Kodi anslutningsinformation. Se till att aktivera \"Till\u00e5t kontroll av Kodi via HTTP\" i System/Inst\u00e4llningar/N\u00e4tverk/Tj\u00e4nster." }, "ws_port": { "data": { diff --git a/homeassistant/components/konnected/translations/sv.json b/homeassistant/components/konnected/translations/sv.json index e8130afd424..a52dcb52cce 100644 --- a/homeassistant/components/konnected/translations/sv.json +++ b/homeassistant/components/konnected/translations/sv.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad", "already_in_progress": "Konfigurationsfl\u00f6de f\u00f6r enhet p\u00e5g\u00e5r redan.", + "cannot_connect": "Det gick inte att ansluta.", "not_konn_panel": "Inte en erk\u00e4nd Konnected.io-enhet", "unknown": "Ett ok\u00e4nt fel har intr\u00e4ffat" }, diff --git a/homeassistant/components/kostal_plenticore/translations/sv.json b/homeassistant/components/kostal_plenticore/translations/sv.json index 70aba340c35..c0980850c66 100644 --- a/homeassistant/components/kostal_plenticore/translations/sv.json +++ b/homeassistant/components/kostal_plenticore/translations/sv.json @@ -5,11 +5,13 @@ }, "error": { "cannot_connect": "Kunde inte ansluta", + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "host": "V\u00e4rd", "password": "L\u00f6senord" } } diff --git a/homeassistant/components/launch_library/translations/sv.json b/homeassistant/components/launch_library/translations/sv.json new file mode 100644 index 00000000000..a4d7bc415a6 --- /dev/null +++ b/homeassistant/components/launch_library/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du konfigurera Launch Library?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lcn/translations/sv.json b/homeassistant/components/lcn/translations/sv.json index 59b06895f48..a6f6f40edce 100644 --- a/homeassistant/components/lcn/translations/sv.json +++ b/homeassistant/components/lcn/translations/sv.json @@ -1,7 +1,11 @@ { "device_automation": { "trigger_type": { - "codelock": "kodl\u00e5skod mottagen" + "codelock": "kodl\u00e5skod mottagen", + "fingerprint": "Mottagen fingeravtryckskod.", + "send_keys": "skicka mottagna nycklar", + "transmitter": "Mottagen s\u00e4ndarkod", + "transponder": "transponderkod mottagen" } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/sv.json b/homeassistant/components/life360/translations/sv.json index 77bb8815340..9f9168abdd2 100644 --- a/homeassistant/components/life360/translations/sv.json +++ b/homeassistant/components/life360/translations/sv.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Konto har redan konfigurerats", "invalid_auth": "Ogiltig autentisering", - "reauth_successful": "\u00c5terautentisering lyckades" + "reauth_successful": "\u00c5terautentisering lyckades", + "unknown": "Ov\u00e4ntat fel" }, "create_entry": { "default": "F\u00f6r att st\u00e4lla in avancerade alternativ, se [Life360 documentation]({docs_url})." diff --git a/homeassistant/components/light/translations/sv.json b/homeassistant/components/light/translations/sv.json index 1f32324d707..a68004c041e 100644 --- a/homeassistant/components/light/translations/sv.json +++ b/homeassistant/components/light/translations/sv.json @@ -13,6 +13,7 @@ "is_on": "{entity_name} \u00e4r p\u00e5" }, "trigger_type": { + "changed_states": "{entity_name} slogs p\u00e5 eller av", "turned_off": "{entity_name} avst\u00e4ngd", "turned_on": "{entity_name} slogs p\u00e5" } diff --git a/homeassistant/components/litejet/translations/sv.json b/homeassistant/components/litejet/translations/sv.json index f865c5a2c6a..e0ffccbc1df 100644 --- a/homeassistant/components/litejet/translations/sv.json +++ b/homeassistant/components/litejet/translations/sv.json @@ -2,6 +2,28 @@ "config": { "abort": { "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "open_failed": "Kan inte \u00f6ppna den angivna seriella porten." + }, + "step": { + "user": { + "data": { + "port": "Port" + }, + "description": "Anslut LiteJets RS232-2-port till din dator och ange s\u00f6kv\u00e4gen till den seriella portenheten. \n\n LiteJet MCP m\u00e5ste konfigureras f\u00f6r 19,2 K baud, 8 databitar, 1 stoppbit, ingen paritet och att s\u00e4nda ett \"CR\" efter varje svar.", + "title": "Anslut till LiteJet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "Standard\u00f6verg\u00e5ng (sekunder)" + }, + "title": "Konfigurera LiteJet" + } } } } \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/sv.json b/homeassistant/components/local_ip/translations/sv.json index 7f1b7ce637a..cb5a9eed725 100644 --- a/homeassistant/components/local_ip/translations/sv.json +++ b/homeassistant/components/local_ip/translations/sv.json @@ -5,6 +5,7 @@ }, "step": { "user": { + "description": "Vill du starta konfigurationen?", "title": "Lokal IP-adress" } } diff --git a/homeassistant/components/locative/translations/sv.json b/homeassistant/components/locative/translations/sv.json index 80da7825d40..905c85b7481 100644 --- a/homeassistant/components/locative/translations/sv.json +++ b/homeassistant/components/locative/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." }, diff --git a/homeassistant/components/luftdaten/translations/sv.json b/homeassistant/components/luftdaten/translations/sv.json index 63de146c6d7..de83a1788ee 100644 --- a/homeassistant/components/luftdaten/translations/sv.json +++ b/homeassistant/components/luftdaten/translations/sv.json @@ -1,6 +1,8 @@ { "config": { "error": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", "invalid_sensor": "Sensor saknas eller \u00e4r ogiltig" }, "step": { diff --git a/homeassistant/components/lutron_caseta/translations/sv.json b/homeassistant/components/lutron_caseta/translations/sv.json index 3f8e2de7d0f..d72f71cef04 100644 --- a/homeassistant/components/lutron_caseta/translations/sv.json +++ b/homeassistant/components/lutron_caseta/translations/sv.json @@ -5,8 +5,15 @@ "cannot_connect": "Det gick inte att ansluta.", "not_lutron_device": "Uppt\u00e4ckt enhet \u00e4r inte en Lutron-enhet" }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "flow_title": "{name} ({host})", "step": { + "import_failed": { + "description": "Det gick inte att st\u00e4lla in bryggan (v\u00e4rd: {host} ) importerad fr\u00e5n configuration.yaml.", + "title": "Det gick inte att importera Cas\u00e9ta-bryggkonfigurationen." + }, "link": { "description": "F\u00f6r att para med {name} ( {host} ), efter att ha skickat in detta formul\u00e4r, tryck p\u00e5 den svarta knappen p\u00e5 baksidan av bryggan.", "title": "Para ihop med bryggan" @@ -24,6 +31,8 @@ "trigger_subtype": { "button_1": "F\u00f6rsta knappen", "button_2": "Andra knappen", + "button_3": "Tredje knappen", + "button_4": "Fj\u00e4rde knappen", "close_1": "St\u00e4ng 1", "close_2": "St\u00e4ng 2", "close_3": "St\u00e4ng 3", @@ -53,7 +62,15 @@ "raise_4": "H\u00f6j 4", "raise_all": "H\u00f6j alla", "stop": "Stoppa (favorit)", - "stop_1": "Stoppa 1" + "stop_1": "Stoppa 1", + "stop_2": "Stoppa 2", + "stop_3": "Stoppa 3", + "stop_4": "Stoppa 4", + "stop_all": "Stoppa alla" + }, + "trigger_type": { + "press": "\" {subtype} \" tryckt", + "release": "\"{subtyp}\" sl\u00e4pptes" } } } \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/sv.json b/homeassistant/components/mailgun/translations/sv.json index 3ca0139183c..68a8c5d5878 100644 --- a/homeassistant/components/mailgun/translations/sv.json +++ b/homeassistant/components/mailgun/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." }, diff --git a/homeassistant/components/mazda/translations/sv.json b/homeassistant/components/mazda/translations/sv.json index c71d7b4faa4..ec3c6339a6f 100644 --- a/homeassistant/components/mazda/translations/sv.json +++ b/homeassistant/components/mazda/translations/sv.json @@ -13,9 +13,11 @@ "step": { "user": { "data": { + "email": "E-post", "password": "L\u00f6senord", "region": "Region" - } + }, + "description": "Ange den e-postadress och det l\u00f6senord som du anv\u00e4nder f\u00f6r att logga in i MyMazda-mobilappen." } } } diff --git a/homeassistant/components/media_player/translations/sv.json b/homeassistant/components/media_player/translations/sv.json index e84edf74bb0..56fba12edf6 100644 --- a/homeassistant/components/media_player/translations/sv.json +++ b/homeassistant/components/media_player/translations/sv.json @@ -10,6 +10,7 @@ }, "trigger_type": { "buffering": "{entity_name} b\u00f6rjar buffra", + "changed_states": "{entity_name} \u00e4ndrade tillst\u00e5nd", "idle": "{entity_name} blir inaktiv", "paused": "{entity_name} \u00e4r pausad", "playing": "{entity_name} b\u00f6rjar spela", diff --git a/homeassistant/components/met/translations/sv.json b/homeassistant/components/met/translations/sv.json index fbea0183395..6ce86cca663 100644 --- a/homeassistant/components/met/translations/sv.json +++ b/homeassistant/components/met/translations/sv.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "no_home": "Inga hemkoordinater \u00e4r inst\u00e4llda i Home Assistant-konfigurationen" + }, + "error": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/met_eireann/translations/sv.json b/homeassistant/components/met_eireann/translations/sv.json index 754fa336a08..950802826ef 100644 --- a/homeassistant/components/met_eireann/translations/sv.json +++ b/homeassistant/components/met_eireann/translations/sv.json @@ -6,10 +6,12 @@ "step": { "user": { "data": { + "elevation": "H\u00f6jd", "latitude": "Latitud", "longitude": "Longitud", "name": "Namn" }, + "description": "Ange din plats f\u00f6r att anv\u00e4nda v\u00e4derdata fr\u00e5n Met \u00c9ireann Public Weather Forecast API", "title": "Plats" } } diff --git a/homeassistant/components/meteoclimatic/translations/sv.json b/homeassistant/components/meteoclimatic/translations/sv.json new file mode 100644 index 00000000000..8d6f17f635a --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/sv.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "not_found": "Inga enheter hittades i n\u00e4tverket" + }, + "step": { + "user": { + "data": { + "code": "Stationskod" + }, + "data_description": { + "code": "Ser ut som ESCAT4300000043206B" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/sv.json b/homeassistant/components/mill/translations/sv.json index a7c8f4e0ea3..72ec5c09ed6 100644 --- a/homeassistant/components/mill/translations/sv.json +++ b/homeassistant/components/mill/translations/sv.json @@ -9,13 +9,21 @@ "step": { "cloud": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } }, + "local": { + "data": { + "ip_address": "IP-adress" + }, + "description": "Lokal IP-adress f\u00f6r enheten." + }, "user": { "data": { "connection_type": "V\u00e4lj anslutningstyp" - } + }, + "description": "V\u00e4lj anslutningstyp. Lokalt kr\u00e4ver generation 3 v\u00e4rmare" } } } diff --git a/homeassistant/components/min_max/translations/sv.json b/homeassistant/components/min_max/translations/sv.json index 7a48b0e631b..882dcd786b0 100644 --- a/homeassistant/components/min_max/translations/sv.json +++ b/homeassistant/components/min_max/translations/sv.json @@ -2,9 +2,16 @@ "config": { "step": { "user": { + "data": { + "entity_ids": "Indataentiteter", + "name": "Namn", + "round_digits": "Precision", + "type": "Statistisk egenskap" + }, "data_description": { "round_digits": "Styr antalet decimalsiffror i utg\u00e5ngen n\u00e4r statistikegenskapen \u00e4r medelv\u00e4rde eller median." }, + "description": "Skapa en sensor som ber\u00e4knar ett min-, max-, medelv\u00e4rde eller medianv\u00e4rde fr\u00e5n en lista med ing\u00e5ngssensorer.", "title": "L\u00e4gg till min / max / medelv\u00e4rde / mediansensor" } } @@ -13,12 +20,15 @@ "step": { "init": { "data": { - "round_digits": "Precision" + "entity_ids": "Indataentiteter", + "round_digits": "Precision", + "type": "Statistisk egenskap" }, "data_description": { "round_digits": "Styr antalet decimalsiffror i utg\u00e5ngen n\u00e4r statistikegenskapen \u00e4r medelv\u00e4rde eller median." } } } - } + }, + "title": "Min / max / medelv\u00e4rde / mediansensor" } \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/sv.json b/homeassistant/components/mjpeg/translations/sv.json index dd590fec4ba..74d669f60b2 100644 --- a/homeassistant/components/mjpeg/translations/sv.json +++ b/homeassistant/components/mjpeg/translations/sv.json @@ -1,21 +1,40 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "user": { "data": { - "username": "Anv\u00e4ndarnamn" + "mjpeg_url": "MJPEG URL", + "name": "Namn", + "password": "L\u00f6senord", + "still_image_url": "URL f\u00f6r stillbild", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera SSL-certifikat" } } } }, "options": { "error": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering" }, "step": { "init": { "data": { - "username": "Anv\u00e4ndarnamn" + "mjpeg_url": "MJPEG URL", + "name": "Namn", + "password": "L\u00f6senord", + "still_image_url": "URL f\u00f6r stillbild", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera SSL-certifikat" } } } diff --git a/homeassistant/components/modem_callerid/translations/sv.json b/homeassistant/components/modem_callerid/translations/sv.json index 531a1029e21..4021e3f2e63 100644 --- a/homeassistant/components/modem_callerid/translations/sv.json +++ b/homeassistant/components/modem_callerid/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", "no_devices_found": "Inga \u00e5terst\u00e5ende enheter hittades" }, diff --git a/homeassistant/components/modern_forms/translations/sv.json b/homeassistant/components/modern_forms/translations/sv.json new file mode 100644 index 00000000000..9058899ce38 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/sv.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "St\u00e4ll in din Modern Forms-fl\u00e4kt f\u00f6r att integreras med Home Assistant." + }, + "zeroconf_confirm": { + "description": "Vill du l\u00e4gga till Modern Forms-fanen som heter ` {name} ` till Home Assistant?", + "title": "Uppt\u00e4ckte Modern Forms fl\u00e4ktenhet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/sv.json b/homeassistant/components/moehlenhoff_alpha2/translations/sv.json new file mode 100644 index 00000000000..4b2c8fb1ce3 --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/translations/sv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + }, + "title": "M\u00f6hlenhoff Alpha2" +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/sv.json b/homeassistant/components/moon/translations/sv.json new file mode 100644 index 00000000000..38aaa7157e6 --- /dev/null +++ b/homeassistant/components/moon/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du starta konfigurationen?" + } + } + }, + "title": "M\u00e5nen" +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/sv.json b/homeassistant/components/motion_blinds/translations/sv.json index 1deca42bc26..10ed4870d6e 100644 --- a/homeassistant/components/motion_blinds/translations/sv.json +++ b/homeassistant/components/motion_blinds/translations/sv.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "connection_error": "Det gick inte att ansluta." }, "error": { "discovery_error": "Det gick inte att uppt\u00e4cka en Motion Gateway" }, + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { @@ -17,7 +20,14 @@ "data": { "select_ip": "IP-adress" }, - "description": "K\u00f6r installationen igen om du vill ansluta ytterligare Motion Gateways" + "description": "K\u00f6r installationen igen om du vill ansluta ytterligare Motion Gateways", + "title": "V\u00e4lj den Motion Gateway som du vill ansluta" + }, + "user": { + "data": { + "host": "IP-adress" + }, + "description": "Anslut till din Motion Gateway, om IP-adressen inte \u00e4r inst\u00e4lld anv\u00e4nds automatisk uppt\u00e4ckt" } } }, diff --git a/homeassistant/components/motioneye/translations/sv.json b/homeassistant/components/motioneye/translations/sv.json index 38517e7571e..e8cbab545a7 100644 --- a/homeassistant/components/motioneye/translations/sv.json +++ b/homeassistant/components/motioneye/translations/sv.json @@ -11,6 +11,10 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "hassio_confirm": { + "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till motionEye-tj\u00e4nsten som tillhandah\u00e5lls av till\u00e4gget: {addon} ?", + "title": "motionEye via Home Assistant-till\u00e4gget" + }, "user": { "data": { "admin_password": "Admin L\u00f6senord", @@ -26,6 +30,7 @@ "step": { "init": { "data": { + "stream_url_template": "Mall f\u00f6r webbadress f\u00f6r str\u00f6m", "webhook_set": "Konfigurera motionEye webhooks f\u00f6r att rapportera h\u00e4ndelser till Home Assistant", "webhook_set_overwrite": "Skriv \u00f6ver ok\u00e4nda webhooks" } diff --git a/homeassistant/components/mqtt/translations/sv.json b/homeassistant/components/mqtt/translations/sv.json index 30acc3c1098..46e3325f990 100644 --- a/homeassistant/components/mqtt/translations/sv.json +++ b/homeassistant/components/mqtt/translations/sv.json @@ -63,7 +63,8 @@ "port": "Port", "username": "Anv\u00e4ndarnamn" }, - "description": "V\u00e4nligen ange anslutningsinformationen f\u00f6r din MQTT broker." + "description": "V\u00e4nligen ange anslutningsinformationen f\u00f6r din MQTT broker.", + "title": "M\u00e4klaralternativ" }, "options": { "data": { @@ -72,9 +73,15 @@ "birth_qos": "F\u00f6delsemeddelande QoS", "birth_retain": "F\u00f6delsemeddelande beh\u00e5lls", "birth_topic": "\u00c4mne f\u00f6r f\u00f6delsemeddelande", - "discovery": "Aktivera uppt\u00e4ckt" + "discovery": "Aktivera uppt\u00e4ckt", + "will_enable": "Aktivera testament", + "will_payload": "Testamentets nyttolast", + "will_qos": "Testamentets QoS", + "will_retain": "Testamentets tid f\u00f6r sparande", + "will_topic": "Testamentets \u00e4mne" }, - "description": "Uppt\u00e4ckt - Om uppt\u00e4ckt \u00e4r aktiverat (rekommenderas), kommer Home Assistant automatiskt att uppt\u00e4cka enheter och enheter som publicerar sin konfiguration p\u00e5 MQTT-brokern. Om uppt\u00e4ckten \u00e4r inaktiverad m\u00e5ste all konfiguration g\u00f6ras manuellt.\n F\u00f6delsemeddelande - F\u00f6delsemeddelandet kommer att skickas varje g\u00e5ng Home Assistant (\u00e5ter)ansluter till MQTT-brokern.\n Kommer meddelande - Viljemeddelandet kommer att skickas varje g\u00e5ng Home Assistant f\u00f6rlorar sin anslutning till brokern, b\u00e5de i h\u00e4ndelse av en reng\u00f6ring (t.ex. Home Assistant st\u00e4ngs av) och i h\u00e4ndelse av en oren (t.ex. Home Assistant kraschar eller f\u00f6rlorar sin n\u00e4tverksanslutning) koppla ifr\u00e5n." + "description": "Uppt\u00e4ckt - Om uppt\u00e4ckt \u00e4r aktiverat (rekommenderas), kommer Home Assistant automatiskt att uppt\u00e4cka enheter och enheter som publicerar sin konfiguration p\u00e5 MQTT-brokern. Om uppt\u00e4ckten \u00e4r inaktiverad m\u00e5ste all konfiguration g\u00f6ras manuellt.\n F\u00f6delsemeddelande - F\u00f6delsemeddelandet kommer att skickas varje g\u00e5ng Home Assistant (\u00e5ter)ansluter till MQTT-brokern.\n Kommer meddelande - Viljemeddelandet kommer att skickas varje g\u00e5ng Home Assistant f\u00f6rlorar sin anslutning till brokern, b\u00e5de i h\u00e4ndelse av en reng\u00f6ring (t.ex. Home Assistant st\u00e4ngs av) och i h\u00e4ndelse av en oren (t.ex. Home Assistant kraschar eller f\u00f6rlorar sin n\u00e4tverksanslutning) koppla ifr\u00e5n.", + "title": "MQTT-alternativ" } } } diff --git a/homeassistant/components/mullvad/translations/sv.json b/homeassistant/components/mullvad/translations/sv.json index c5ad71d784d..4138bf60b8c 100644 --- a/homeassistant/components/mullvad/translations/sv.json +++ b/homeassistant/components/mullvad/translations/sv.json @@ -6,6 +6,11 @@ "error": { "cannot_connect": "Kunde inte ansluta", "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "description": "Konfigurera Mullvad VPN-integration?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/sv.json b/homeassistant/components/mysensors/translations/sv.json index daebe65e30e..c41dc508691 100644 --- a/homeassistant/components/mysensors/translations/sv.json +++ b/homeassistant/components/mysensors/translations/sv.json @@ -31,7 +31,49 @@ "invalid_port": "Ogiltigt portnummer", "invalid_publish_topic": "Ogiltigt \u00e4mne f\u00f6r publicering", "invalid_serial": "Ogiltig serieport", - "mqtt_required": "MQTT-integrationen \u00e4r inte konfigurerad" + "invalid_subscribe_topic": "Ogiltigt \u00e4mne f\u00f6r prenumeration", + "invalid_version": "Ogiltig version av MySensors", + "mqtt_required": "MQTT-integrationen \u00e4r inte konfigurerad", + "not_a_number": "Ange ett nummer", + "port_out_of_range": "Portnummer m\u00e5ste vara minst 1 och h\u00f6gst 65535", + "same_topic": "\u00c4mnen f\u00f6r prenumeration och publicering \u00e4r desamma", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "gw_mqtt": { + "data": { + "persistence_file": "persistensfil (l\u00e4mna tom f\u00f6r att automatiskt generera)", + "retain": "mqtt beh\u00e5ller", + "topic_in_prefix": "prefix f\u00f6r inmatnings\u00e4mnen (topic_in_prefix)", + "topic_out_prefix": "prefix f\u00f6r utdata\u00e4mnen (topic_out_prefix)", + "version": "MySensors version" + }, + "description": "MQTT-gateway-inst\u00e4llning" + }, + "gw_serial": { + "data": { + "baud_rate": "baudhastighet", + "device": "Serieport", + "persistence_file": "persistensfil (l\u00e4mna tom f\u00f6r att automatiskt generera)", + "version": "MySensors version" + }, + "description": "Seriell gateway-inst\u00e4llning" + }, + "gw_tcp": { + "data": { + "device": "IP-adress f\u00f6r gatewayen", + "persistence_file": "persistensfil (l\u00e4mna tom f\u00f6r att automatiskt generera)", + "tcp_port": "port", + "version": "MySensors version" + }, + "description": "Ethernet-gateway-inst\u00e4llning" + }, + "user": { + "data": { + "gateway_type": "Gateway typ" + }, + "description": "V\u00e4lj anslutningsmetod till gatewayen" + } } } } \ No newline at end of file diff --git a/homeassistant/components/nam/translations/sv.json b/homeassistant/components/nam/translations/sv.json index 55d985814df..45b4410da7b 100644 --- a/homeassistant/components/nam/translations/sv.json +++ b/homeassistant/components/nam/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "device_unsupported": "Enheten st\u00f6ds ej", "reauth_successful": "\u00c5terautentisering lyckades", "reauth_unsuccessful": "\u00c5terautentiseringen misslyckades. Ta bort integrationen och konfigurera den igen." @@ -10,21 +11,30 @@ "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{host}", "step": { + "confirm_discovery": { + "description": "Vill du st\u00e4lla in Nettigo Air Monitor p\u00e5 {host} ?" + }, "credentials": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange anv\u00e4ndarnamn och l\u00f6senord." }, "reauth_confirm": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange r\u00e4tt anv\u00e4ndarnamn och l\u00f6senord f\u00f6r v\u00e4rden: {host}" }, "user": { "data": { "host": "V\u00e4rd" - } + }, + "description": "Konfigurera Nettigo Air Monitor integrationen." } } } diff --git a/homeassistant/components/nanoleaf/translations/sv.json b/homeassistant/components/nanoleaf/translations/sv.json index bbea90511da..6198fce87c6 100644 --- a/homeassistant/components/nanoleaf/translations/sv.json +++ b/homeassistant/components/nanoleaf/translations/sv.json @@ -24,5 +24,13 @@ } } } + }, + "device_automation": { + "trigger_type": { + "swipe_down": "Svep ned\u00e5t", + "swipe_left": "Svep \u00e5t v\u00e4nster", + "swipe_right": "Svep \u00e5t h\u00f6ger", + "swipe_up": "Svep upp\u00e5t" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index 675b9087b6c..0836cc974bc 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -1,6 +1,6 @@ { "application_credentials": { - "description": "Folgen Sie den [Anweisungen]({more_info_url}), um die Cloud-Konsole zu konfigurieren:\n\n1. Gehen Sie zum [OAuth-Zustimmungsbildschirm]({oauth_consent_url}) und konfigurieren Sie\n1. Gehen Sie zu [Credentials]({oauth_creds_url}) und klicken Sie auf **Create Credentials**.\n1. W\u00e4hlen Sie in der Dropdown-Liste **OAuth-Client-ID**.\n1. W\u00e4hlen Sie **Webanwendung** f\u00fcr den Anwendungstyp.\n1. F\u00fcgen Sie `{redirect_url}` unter *Authorized redirect URI* hinzu." + "description": "Folge den [Anweisungen]({more_info_url}), um die Cloud-Konsole zu konfigurieren:\n\n1. Gehe zum [OAuth-Zustimmungsbildschirm]({oauth_consent_url}) und konfiguriere\n1. Gehe zu [Credentials]({oauth_creds_url}) und klicke auf **Create Credentials**.\n1. W\u00e4hle in der Dropdown-Liste **OAuth-Client-ID**.\n1. W\u00e4hle **Webanwendung** f\u00fcr den Anwendungstyp.\n1. F\u00fcge `{redirect_url}` unter *Authorized redirect URI* hinzu." }, "config": { "abort": { @@ -34,29 +34,29 @@ "title": "Google-Konto verkn\u00fcpfen" }, "auth_upgrade": { - "description": "App Auth wurde von Google abgeschafft, um die Sicherheit zu verbessern, und Sie m\u00fcssen Ma\u00dfnahmen ergreifen, indem Sie neue Anmeldedaten f\u00fcr die Anwendung erstellen.\n\n\u00d6ffnen Sie die [Dokumentation]({more_info_url}) und folgen Sie den n\u00e4chsten Schritten, die Sie durchf\u00fchren m\u00fcssen, um den Zugriff auf Ihre Nest-Ger\u00e4te wiederherzustellen.", + "description": "App Auth wurde von Google abgeschafft, um die Sicherheit zu verbessern, und du musst Ma\u00dfnahmen ergreifen, indem du neue Anmeldedaten f\u00fcr die Anwendung erstellst.\n\n\u00d6ffnen die [Dokumentation]({more_info_url}) und folge den n\u00e4chsten Schritten, die du durchf\u00fchren musst, um den Zugriff auf deine Nest-Ger\u00e4te wiederherzustellen.", "title": "Nest: Einstellung der App-Authentifizierung" }, "cloud_project": { "data": { "cloud_project_id": "Google Cloud Projekt-ID" }, - "description": "Geben Sie unten die Cloud-Projekt-ID ein, z. B. *example-project-12345*. Siehe die [Google Cloud Console]({cloud_console_url}) oder die Dokumentation f\u00fcr [weitere Informationen]({more_info_url}).", + "description": "Gib unten die Cloud-Projekt-ID ein, z. B. *example-project-12345*. Siehe die [Google Cloud Console]({cloud_console_url}) oder die Dokumentation f\u00fcr [weitere Informationen]({more_info_url}).", "title": "Nest: Cloud-Projekt-ID eingeben" }, "create_cloud_project": { - "description": "Die Nest-Integration erm\u00f6glicht es Ihnen, Ihre Nest-Thermostate, -Kameras und -T\u00fcrklingeln \u00fcber die Smart Device Management API zu integrieren. Die SDM API **erfordert eine einmalige Einrichtungsgeb\u00fchr von US $5**. Siehe Dokumentation f\u00fcr [weitere Informationen]({more_info_url}).\n\n1. Rufen Sie die [Google Cloud Console]({cloud_console_url}) auf.\n1. Wenn dies Ihr erstes Projekt ist, klicken Sie auf **Projekt erstellen** und dann auf **Neues Projekt**.\n1. Geben Sie Ihrem Cloud-Projekt einen Namen und klicken Sie dann auf **Erstellen**.\n1. Speichern Sie die Cloud Project ID, z. B. *example-project-12345*, da Sie diese sp\u00e4ter ben\u00f6tigen.\n1. Gehen Sie zur API-Bibliothek f\u00fcr [Smart Device Management API]({sdm_api_url}) und klicken Sie auf **Aktivieren**.\n1. Wechseln Sie zur API-Bibliothek f\u00fcr [Cloud Pub/Sub API]({pubsub_api_url}) und klicken Sie auf **Aktivieren**.\n\nFahren Sie fort, wenn Ihr Cloud-Projekt eingerichtet ist.", + "description": "Die Nest-Integration erm\u00f6glicht es dir, deine Nest-Thermostate, -Kameras und -T\u00fcrklingeln \u00fcber die Smart Device Management API zu integrieren. Die SDM API **erfordert eine einmalige Einrichtungsgeb\u00fchr von US $5**. Siehe Dokumentation f\u00fcr [weitere Informationen]({more_info_url}).\n\n1. Rufe die [Google Cloud Console]({cloud_console_url}) auf.\n1. Wenn dies dein erstes Projekt ist, klicke auf **Projekt erstellen** und dann auf **Neues Projekt**.\n1. Gib deinem Cloud-Projekt einen Namen und klicke dann auf **Erstellen**.\n1. Speichere die Cloud Project ID, z. B. *example-project-12345*, da du diese sp\u00e4ter ben\u00f6tigst.\n1. Gehe zur API-Bibliothek f\u00fcr [Smart Device Management API]({sdm_api_url}) und klicke auf **Aktivieren**.\n1. Wechsele zur API-Bibliothek f\u00fcr [Cloud Pub/Sub API]({pubsub_api_url}) und klicke auf **Aktivieren**.\n\nFahre fort, wenn dein Cloud-Projekt eingerichtet ist.", "title": "Nest: Cloud-Projekt erstellen und konfigurieren" }, "device_project": { "data": { "project_id": "Ger\u00e4tezugriffsprojekt ID" }, - "description": "Erstellen Sie ein Nest Ger\u00e4tezugriffsprojekt, f\u00fcr dessen Einrichtung **eine Geb\u00fchr von 5 US-Dollar** anf\u00e4llt.\n1. Gehen Sie zur [Device Access Console]({device_access_console_url}) und durchlaufen Sie den Zahlungsablauf.\n1. Klicken Sie auf **Projekt erstellen**.\n1. Geben Sie Ihrem Device Access-Projekt einen Namen und klicken Sie auf **Weiter**.\n1. Geben Sie Ihre OAuth-Client-ID ein\n1. Aktivieren Sie Ereignisse, indem Sie auf **Aktivieren** und **Projekt erstellen** klicken.\n\nGeben Sie unten Ihre Ger\u00e4tezugriffsprojekt ID ein ([more info]({more_info_url})).\n", + "description": "Erstelle ein Nest Ger\u00e4tezugriffsprojekt, f\u00fcr dessen Einrichtung **eine Geb\u00fchr von 5 US-Dollar** anf\u00e4llt.\n1. Gehe zur [Device Access Console]({device_access_console_url}) und durchlaufe den Zahlungsablauf.\n1. Klicke auf **Projekt erstellen**.\n1. Gib deinem Device Access-Projekt einen Namen und klicke auf **Weiter**.\n1. Gib deine OAuth-Client-ID ein\n1. Aktiviere Ereignisse, indem du auf **Aktivieren** und **Projekt erstellen** klickst.\n\nGib unten deine Ger\u00e4tezugriffsprojekt ID ein ([more info]({more_info_url})).", "title": "Nest: Erstelle ein Ger\u00e4tezugriffsprojekt" }, "device_project_upgrade": { - "description": "Aktualisieren Sie das Nest Ger\u00e4tezugriffsprojekt mit Ihrer neuen OAuth Client ID ([more info]({more_info_url}))\n1. Gehen Sie zur [Ger\u00e4tezugriffskonsole]({device_access_console_url}).\n1. Klicken Sie auf das Papierkorbsymbol neben *OAuth Client ID*.\n1. Klicken Sie auf das \u00dcberlaufmen\u00fc und *Client ID hinzuf\u00fcgen*.\n1. Geben Sie Ihre neue OAuth-Client-ID ein und klicken Sie auf **Hinzuf\u00fcgen**.\n\nIhre OAuth-Client-ID lautet: `{client_id}`", + "description": "Aktualisiere das Nest Ger\u00e4tezugriffsprojekt mit deiner neuen OAuth Client ID ([more info]({more_info_url}))\n1. Gehe zur [Ger\u00e4tezugriffskonsole]({device_access_console_url}).\n1. Klicke auf das Papierkorbsymbol neben *OAuth Client ID*.\n1. Klicke auf das \u00dcberlaufmen\u00fc und *Client ID hinzuf\u00fcgen*.\n1. Gib deine neue OAuth-Client-ID ein und klicke auf **Hinzuf\u00fcgen**.\n\nDeine OAuth-Client-ID lautet: `{client_id}`", "title": "Nest: Aktualisiere das Ger\u00e4tezugriffsprojekt" }, "init": { diff --git a/homeassistant/components/nest/translations/sv.json b/homeassistant/components/nest/translations/sv.json index 1e2082c4a32..3eae7e1c547 100644 --- a/homeassistant/components/nest/translations/sv.json +++ b/homeassistant/components/nest/translations/sv.json @@ -6,18 +6,31 @@ "abort": { "already_configured": "Konto har redan konfigurerats", "authorize_url_timeout": "Timeout vid generering av en autentisieringsadress.", + "invalid_access_token": "Ogiltig \u00e5tkomstnyckel", "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", "reauth_successful": "\u00c5terautentisering lyckades", - "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "unknown_authorize_url_generation": "Ok\u00e4nt fel vid generering av en auktoriserad URL." + }, + "create_entry": { + "default": "Autentiserats" }, "error": { + "bad_project_id": "Ange ett giltigt Cloud Project ID (kontrollera Cloud Console)", "internal_error": "Internt fel vid validering av kod", "invalid_pin": "Ogiltig Pin-kod", + "subscriber_error": "Ok\u00e4nt abonnentfel, se loggar", "timeout": "Timeout vid valididering av kod", - "unknown": "Ok\u00e4nt fel vid validering av kod" + "unknown": "Ok\u00e4nt fel vid validering av kod", + "wrong_project_id": "Ange ett giltigt Cloud Project ID (var samma som Device Access Project ID)" }, "step": { "auth": { + "data": { + "code": "\u00c5tkomstnyckel" + }, + "description": "F\u00f6r att l\u00e4nka ditt Google-konto, [auktorisera ditt konto]( {url} ). \n\n Efter auktorisering, kopiera och klistra in den medf\u00f6ljande Auth Token-koden nedan.", "title": "L\u00e4nka Google-konto" }, "auth_upgrade": { @@ -60,6 +73,16 @@ "description": "F\u00f6r att l\u00e4nka ditt Nest-konto, [autentisiera ditt konto]({url}). \n\nEfter autentisiering, klipp och klistra in den angivna pin-koden nedan.", "title": "L\u00e4nka Nest-konto" }, + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + }, + "pubsub": { + "data": { + "cloud_project_id": "Projekt-ID f\u00f6r Google Cloud" + }, + "description": "Bes\u00f6k [Cloud Console]( {url} ) f\u00f6r att hitta ditt Google Cloud Project ID.", + "title": "Konfigurera Google Cloud" + }, "reauth_confirm": { "description": "Nest-integreringen m\u00e5ste autentisera ditt konto igen", "title": "\u00c5terautenticera integration" @@ -73,5 +96,15 @@ "camera_sound": "Ljud uppt\u00e4ckt", "doorbell_chime": "D\u00f6rrklockan tryckt" } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Nest i configuration.yaml tas bort i Home Assistant 2022.10. \n\n Dina befintliga OAuth-applikationsuppgifter och \u00e5tkomstinst\u00e4llningar har importerats till anv\u00e4ndargr\u00e4nssnittet automatiskt. Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Nest YAML-konfigurationen tas bort" + }, + "removed_app_auth": { + "description": "F\u00f6r att f\u00f6rb\u00e4ttra s\u00e4kerheten och minska risken f\u00f6r n\u00e4tfiske har Google fasat ut den autentiseringsmetod som anv\u00e4nds av Home Assistant. \n\n **Detta kr\u00e4ver \u00e5tg\u00e4rd fr\u00e5n dig f\u00f6r att l\u00f6sa** ([mer info]( {more_info_url} )) \n\n 1. Bes\u00f6k integrationssidan\n 1. Klicka p\u00e5 Konfigurera om p\u00e5 Nest-integreringen.\n 1. Home Assistant leder dig genom stegen f\u00f6r att uppgradera till webbautentisering. \n\n Se Nest [integreringsinstruktioner]( {documentation_url} ) f\u00f6r fels\u00f6kningsinformation.", + "title": "Nest-autentiseringsuppgifterna m\u00e5ste uppdateras" + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/sv.json b/homeassistant/components/netatmo/translations/sv.json index cd91ea65c81..54e1a56ed11 100644 --- a/homeassistant/components/netatmo/translations/sv.json +++ b/homeassistant/components/netatmo/translations/sv.json @@ -22,7 +22,24 @@ }, "device_automation": { "trigger_subtype": { + "away": "borta", + "hg": "frostvakt", "schedule": "schema" + }, + "trigger_type": { + "alarm_started": "{entity_name} uppt\u00e4ckte ett larm", + "animal": "{entity_name} uppt\u00e4ckte ett djur", + "cancel_set_point": "{entity_name} har \u00e5terupptagit sitt schema", + "human": "{entity_name} uppt\u00e4ckte en m\u00e4nniska", + "movement": "{entity_name} uppt\u00e4ckte r\u00f6relse", + "outdoor": "{entity_name} uppt\u00e4ckte en utomhush\u00e4ndelse", + "person": "{entity_name} uppt\u00e4ckte en person", + "person_away": "{entity_name} uppt\u00e4ckte att en person har l\u00e4mnat", + "set_point": "M\u00e5ltemperaturen {entity_name} har st\u00e4llts in manuellt.", + "therm_mode": "{entity_name} bytte till \" {subtype} \"", + "turned_off": "{entity_name} st\u00e4ngdes av", + "turned_on": "{entity_name} slogs p\u00e5", + "vehicle": "{entity_name} uppt\u00e4ckt ett fordon" } }, "options": { diff --git a/homeassistant/components/netgear/translations/sv.json b/homeassistant/components/netgear/translations/sv.json index 2672bc03eef..5575242e6e8 100644 --- a/homeassistant/components/netgear/translations/sv.json +++ b/homeassistant/components/netgear/translations/sv.json @@ -1,10 +1,29 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "config": "Anslutnings- eller inloggningsfel: kontrollera din konfiguration" + }, "step": { "user": { "data": { + "host": "V\u00e4rd (Valfritt)", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn (frivilligt)" - } + }, + "description": "Standardv\u00e4rd: {host}\n Standardanv\u00e4ndarnamn: {username}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "consider_home": "Tid f\u00f6r att betraktas som hemma (sekunder)" + }, + "description": "Ange valfria inst\u00e4llningar" } } } diff --git a/homeassistant/components/nina/translations/sv.json b/homeassistant/components/nina/translations/sv.json index 11f2e28deac..028b843fdb5 100644 --- a/homeassistant/components/nina/translations/sv.json +++ b/homeassistant/components/nina/translations/sv.json @@ -1,4 +1,29 @@ { + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "no_selection": "V\u00e4lj minst en stad/l\u00e4n", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "_a_to_d": "Stad/l\u00e4n (A-D)", + "_e_to_h": "Stad/l\u00e4n (E-H)", + "_i_to_l": "Stad/l\u00e4n (I-L)", + "_m_to_q": "Stad/l\u00e4n (M-Q)", + "_r_to_u": "Stad/l\u00e4n (R-U)", + "_v_to_z": "Stad/l\u00e4n (V-Z)", + "corona_filter": "Ta bort Corona-varningar", + "slots": "Maximala varningar per stad/l\u00e4n" + }, + "title": "V\u00e4lj stad/l\u00e4n" + } + } + }, "options": { "error": { "cannot_connect": "Misslyckades att ansluta", diff --git a/homeassistant/components/nmap_tracker/translations/sv.json b/homeassistant/components/nmap_tracker/translations/sv.json index b69cd27fe52..e9bdf8dbdf9 100644 --- a/homeassistant/components/nmap_tracker/translations/sv.json +++ b/homeassistant/components/nmap_tracker/translations/sv.json @@ -1,12 +1,40 @@ { + "config": { + "abort": { + "already_configured": "Platsen \u00e4r redan konfigurerad" + }, + "error": { + "invalid_hosts": "Ogiltiga v\u00e4rdar" + }, + "step": { + "user": { + "data": { + "exclude": "N\u00e4tverksadresser (kommaseparerade) f\u00f6r att utesluta fr\u00e5n skanning", + "home_interval": "Minsta antal minuter mellan skanningar av aktiva enheter (spar p\u00e5 batteri)", + "hosts": "N\u00e4tverksadresser (kommaseparerade) att skanna", + "scan_options": "R\u00e5konfigurerbara skanningsalternativ f\u00f6r Nmap" + }, + "description": "Konfigurera v\u00e4rdar som ska skannas av Nmap. N\u00e4tverksadresser och exkluderingar kan vara IP-adresser (192.168.1.1), IP-n\u00e4tverk (192.168.0.0/24) eller IP-intervall (192.168.1.0-32)." + } + } + }, "options": { + "error": { + "invalid_hosts": "Ogiltiga v\u00e4rdar" + }, "step": { "init": { "data": { "consider_home": "Sekunder att v\u00e4nta tills en enhetssp\u00e5rare markeras som inte hemma efter att den inte setts.", - "interval_seconds": "Skanningsintervall" - } + "exclude": "N\u00e4tverksadresser (kommaseparerade) f\u00f6r att utesluta fr\u00e5n skanning", + "home_interval": "Minsta antal minuter mellan skanningar av aktiva enheter (spar p\u00e5 batteri)", + "hosts": "N\u00e4tverksadresser (kommaseparerade) att skanna", + "interval_seconds": "Skanningsintervall", + "scan_options": "R\u00e5konfigurerbara skanningsalternativ f\u00f6r Nmap" + }, + "description": "Konfigurera v\u00e4rdar som ska skannas av Nmap. N\u00e4tverksadresser och exkluderingar kan vara IP-adresser (192.168.1.1), IP-n\u00e4tverk (192.168.0.0/24) eller IP-intervall (192.168.1.0-32)." } } - } + }, + "title": "Nmap sp\u00e5rare" } \ No newline at end of file diff --git a/homeassistant/components/notion/translations/sv.json b/homeassistant/components/notion/translations/sv.json index b565872aacc..39fc56dbf66 100644 --- a/homeassistant/components/notion/translations/sv.json +++ b/homeassistant/components/notion/translations/sv.json @@ -5,6 +5,7 @@ "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, "step": { diff --git a/homeassistant/components/nuki/translations/sv.json b/homeassistant/components/nuki/translations/sv.json index 9a4ec6946b7..0d73cff0543 100644 --- a/homeassistant/components/nuki/translations/sv.json +++ b/homeassistant/components/nuki/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "reauth_successful": "\u00c5terautentisering lyckades" + }, "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", @@ -9,7 +12,9 @@ "reauth_confirm": { "data": { "token": "\u00c5tkomstnyckel" - } + }, + "description": "Nuki-integrationen m\u00e5ste autentiseras p\u00e5 nytt med din brygga.", + "title": "\u00c5terautenticera integration" }, "user": { "data": { diff --git a/homeassistant/components/number/translations/sv.json b/homeassistant/components/number/translations/sv.json new file mode 100644 index 00000000000..02af44b1e7b --- /dev/null +++ b/homeassistant/components/number/translations/sv.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "Ange v\u00e4rde f\u00f6r {entity_name}" + } + }, + "title": "Nummer" +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/sv.json b/homeassistant/components/octoprint/translations/sv.json index af10a29c982..f3b9760aca2 100644 --- a/homeassistant/components/octoprint/translations/sv.json +++ b/homeassistant/components/octoprint/translations/sv.json @@ -6,12 +6,23 @@ "cannot_connect": "Det gick inte att ansluta.", "unknown": "Ov\u00e4ntat fel" }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "OctoPrint-skrivare: {host}", + "progress": { + "get_api_key": "\u00d6ppna OctoPrint UI och klicka p\u00e5 \"Till\u00e5t\" p\u00e5 \u00e5tkomstbeg\u00e4ran f\u00f6r \"Home Assistant\"." + }, "step": { "user": { "data": { "host": "V\u00e4rd", + "path": "S\u00f6kv\u00e4g till program", + "port": "Portnummer", "ssl": "Anv\u00e4nd SSL", - "username": "Anv\u00e4ndarnamn" + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera SSL-certifikat" } } } diff --git a/homeassistant/components/omnilogic/translations/sv.json b/homeassistant/components/omnilogic/translations/sv.json index 407893a7018..c572f6264b5 100644 --- a/homeassistant/components/omnilogic/translations/sv.json +++ b/homeassistant/components/omnilogic/translations/sv.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, "step": { diff --git a/homeassistant/components/ondilo_ico/translations/sv.json b/homeassistant/components/ondilo_ico/translations/sv.json new file mode 100644 index 00000000000..732124944cd --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/sv.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", + "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen." + }, + "create_entry": { + "default": "Autentiserats" + }, + "step": { + "pick_implementation": { + "title": "V\u00e4lj autentiseringsmetod" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/sv.json b/homeassistant/components/onewire/translations/sv.json index e4ce4947ca9..b1f66853ca5 100644 --- a/homeassistant/components/onewire/translations/sv.json +++ b/homeassistant/components/onewire/translations/sv.json @@ -1,21 +1,40 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "user": { "data": { "host": "V\u00e4rd", "port": "Port" - } + }, + "title": "St\u00e4ll in serverdetaljer" } } }, "options": { + "error": { + "device_not_selected": "V\u00e4lj enheter att konfigurera" + }, "step": { + "configure_device": { + "data": { + "precision": "Sensorprecision" + }, + "description": "V\u00e4lj sensorprecision f\u00f6r {sensor_id}", + "title": "OneWire-sensorprecision" + }, "device_selection": { "data": { "clear_device_options": "Rensa alla enhetskonfigurationer", "device_selection": "V\u00e4lj enheter att konfigurera" - } + }, + "description": "V\u00e4lj vilka konfigurationssteg som ska bearbetas", + "title": "OneWire-enhetsalternativ" } } } diff --git a/homeassistant/components/onvif/translations/sv.json b/homeassistant/components/onvif/translations/sv.json index 579922f31e6..45fe62d156d 100644 --- a/homeassistant/components/onvif/translations/sv.json +++ b/homeassistant/components/onvif/translations/sv.json @@ -7,6 +7,9 @@ "no_mac": "Det gick inte att konfigurera unikt ID f\u00f6r ONVIF-enhet.", "onvif_error": "Problem med att konfigurera ONVIF enheten. Kolla loggarna f\u00f6r mer information." }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "configure": { "data": { diff --git a/homeassistant/components/open_meteo/translations/sv.json b/homeassistant/components/open_meteo/translations/sv.json new file mode 100644 index 00000000000..8a7e60fcaa4 --- /dev/null +++ b/homeassistant/components/open_meteo/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zone": "Zon" + }, + "description": "V\u00e4lj plats att anv\u00e4nda f\u00f6r v\u00e4derprognoser" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/sv.json b/homeassistant/components/opentherm_gw/translations/sv.json index 95ed092fece..1231a8343a7 100644 --- a/homeassistant/components/opentherm_gw/translations/sv.json +++ b/homeassistant/components/opentherm_gw/translations/sv.json @@ -2,7 +2,9 @@ "config": { "error": { "already_configured": "Gateway redan konfigurerad", - "id_exists": "Gateway-id finns redan" + "cannot_connect": "Det gick inte att ansluta.", + "id_exists": "Gateway-id finns redan", + "timeout_connect": "Timeout uppr\u00e4ttar anslutning" }, "step": { "init": { diff --git a/homeassistant/components/overkiz/translations/select.sv.json b/homeassistant/components/overkiz/translations/select.sv.json new file mode 100644 index 00000000000..bf0cd5dc9aa --- /dev/null +++ b/homeassistant/components/overkiz/translations/select.sv.json @@ -0,0 +1,13 @@ +{ + "state": { + "overkiz__memorized_simple_volume": { + "highest": "H\u00f6gsta", + "standard": "Standard" + }, + "overkiz__open_closed_pedestrian": { + "closed": "St\u00e4ngd", + "open": "\u00d6ppen", + "pedestrian": "Fotg\u00e4ngare" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/sensor.sv.json b/homeassistant/components/overkiz/translations/sensor.sv.json index 024e5fe1037..f7cfb3eb94f 100644 --- a/homeassistant/components/overkiz/translations/sensor.sv.json +++ b/homeassistant/components/overkiz/translations/sensor.sv.json @@ -1,5 +1,42 @@ { "state": { + "overkiz__battery": { + "full": "Full", + "low": "L\u00e5gt", + "normal": "Normal", + "verylow": "V\u00e4ldigt l\u00e5gt" + }, + "overkiz__discrete_rssi_level": { + "good": "Bra", + "low": "L\u00e5g", + "normal": "Normal", + "verylow": "Mycket l\u00e5g" + }, + "overkiz__priority_lock_originator": { + "external_gateway": "Extern gateway", + "local_user": "Lokal anv\u00e4ndare", + "lsc": "LSC", + "myself": "Jag sj\u00e4lv", + "rain": "Regn", + "saac": "SAAC", + "security": "S\u00e4kerhet", + "sfc": "SFC", + "temperature": "Temperatur", + "timer": "Timer", + "ups": "UPS", + "user": "Anv\u00e4ndare", + "wind": "Vind" + }, + "overkiz__sensor_defect": { + "dead": "D\u00f6d", + "low_battery": "L\u00e5g batteriniv\u00e5", + "maintenance_required": "Underh\u00e5ll kr\u00e4vs", + "no_defect": "Ingen defekt" + }, + "overkiz__sensor_room": { + "clean": "Ren", + "dirty": "Smutsig" + }, "overkiz__three_way_handle_direction": { "closed": "St\u00e4ngd", "open": "\u00d6ppen", diff --git a/homeassistant/components/overkiz/translations/sv.json b/homeassistant/components/overkiz/translations/sv.json index d88861d15e0..b173a0bef99 100644 --- a/homeassistant/components/overkiz/translations/sv.json +++ b/homeassistant/components/overkiz/translations/sv.json @@ -1,17 +1,28 @@ { "config": { "abort": { + "already_configured": "Konto har redan konfigurerats", "reauth_successful": "\u00c5terautentisering lyckades", "reauth_wrong_account": "Du kan bara \u00e5terautentisera denna post med samma Overkiz-konto och hub" }, "error": { - "too_many_attempts": "F\u00f6r m\u00e5nga f\u00f6rs\u00f6k med en ogiltig token, tillf\u00e4lligt avst\u00e4ngd" + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "server_in_maintenance": "Servern ligger nere f\u00f6r underh\u00e5ll", + "too_many_attempts": "F\u00f6r m\u00e5nga f\u00f6rs\u00f6k med en ogiltig token, tillf\u00e4lligt avst\u00e4ngd", + "too_many_requests": "F\u00f6r m\u00e5nga f\u00f6rfr\u00e5gningar, f\u00f6rs\u00f6k igen senare", + "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "Gateway: {gateway_id}", "step": { "user": { "data": { + "host": "V\u00e4rd", + "hub": "Hubb", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Overkiz-plattformen anv\u00e4nds av olika leverant\u00f6rer som Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) och Atlantic (Cozytouch). Ange dina applikationsuppgifter och v\u00e4lj din hubb." } } } diff --git a/homeassistant/components/ovo_energy/translations/sv.json b/homeassistant/components/ovo_energy/translations/sv.json index bd443851b14..46fafdc3429 100644 --- a/homeassistant/components/ovo_energy/translations/sv.json +++ b/homeassistant/components/ovo_energy/translations/sv.json @@ -5,17 +5,22 @@ "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering" }, + "flow_title": "{username}", "step": { "reauth": { "data": { "password": "L\u00f6senord" - } + }, + "description": "Autentisering misslyckades f\u00f6r OVO Energy. Ange dina aktuella uppgifter.", + "title": "\u00c5terautentisering" }, "user": { "data": { "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "St\u00e4ll in en OVO Energy-instans f\u00f6r att komma \u00e5t din energianv\u00e4ndning.", + "title": "L\u00e4gg till OVO Energikonto" } } } diff --git a/homeassistant/components/owntracks/translations/de.json b/homeassistant/components/owntracks/translations/de.json index d02571a6a50..f3a7542cdd5 100644 --- a/homeassistant/components/owntracks/translations/de.json +++ b/homeassistant/components/owntracks/translations/de.json @@ -5,7 +5,7 @@ "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "create_entry": { - "default": "\n\nUnter Android \u00f6ffne [die OwnTracks App]({android_url}), gehe zu Einstellungen \u2192 Verbindung. \u00c4ndere die folgenden Einstellungen:\n - Modus: Privat HTTP\n - Host: {webhook_url}\n - Identifikation:\n - Benutzername: `''`\n - Ger\u00e4te-ID: `''`\n\nUnter iOS \u00f6ffne [die OwnTracks App]({ios_url}), tippe auf das (i)-Symbol oben links \u2192 Einstellungen. \u00c4ndere die folgenden Einstellungen:\n - Modus: HTTP\n - URL: {webhook_url}\n - Authentifizierung einschalten\n - UserID: `''`\n\n{secret}\n\nWeitere Informationen findest du in [der Dokumentation]({docs_url})." + "default": "Unter Android \u00f6ffne [die OwnTracks App]({android_url}), gehe zu Einstellungen \u2192 Verbindung. \u00c4ndere die folgenden Einstellungen:\n - Modus: Privat HTTP\n - Host: {webhook_url}\n - Identifikation:\n - Benutzername: `''`\n - Ger\u00e4te-ID: `''`\n\nUnter iOS \u00f6ffne [die OwnTracks App]({ios_url}), tippe auf das (i)-Symbol oben links \u2192 Einstellungen. \u00c4ndere die folgenden Einstellungen:\n - Modus: HTTP\n - URL: {webhook_url}\n - Authentifizierung einschalten\n - UserID: `''`\n\n{secret}\n\nWeitere Informationen findest du in [der Dokumentation]({docs_url})." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/sv.json b/homeassistant/components/owntracks/translations/sv.json index 3c9146e49f2..0f3c8cade9c 100644 --- a/homeassistant/components/owntracks/translations/sv.json +++ b/homeassistant/components/owntracks/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, "create_entry": { diff --git a/homeassistant/components/panasonic_viera/translations/sv.json b/homeassistant/components/panasonic_viera/translations/sv.json index a35794646b7..7375897d482 100644 --- a/homeassistant/components/panasonic_viera/translations/sv.json +++ b/homeassistant/components/panasonic_viera/translations/sv.json @@ -2,9 +2,11 @@ "config": { "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", "unknown": "Ett ov\u00e4ntat fel intr\u00e4ffade. Kontrollera loggarna f\u00f6r mer information." }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "invalid_pin_code": "Pin-kod du angav \u00e4r ogiltig" }, "step": { diff --git a/homeassistant/components/peco/translations/sv.json b/homeassistant/components/peco/translations/sv.json new file mode 100644 index 00000000000..b821234481e --- /dev/null +++ b/homeassistant/components/peco/translations/sv.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "step": { + "user": { + "data": { + "county": "L\u00e4n" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/translations/sv.json b/homeassistant/components/philips_js/translations/sv.json index 290b8a9d82b..82c0206995b 100644 --- a/homeassistant/components/philips_js/translations/sv.json +++ b/homeassistant/components/philips_js/translations/sv.json @@ -6,6 +6,7 @@ "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_pin": "Ogiltig PIN-kod", + "pairing_failure": "Det g\u00e5r inte att para ihop: {error_id}", "unknown": "Ov\u00e4ntat fel" }, "step": { @@ -28,5 +29,14 @@ "trigger_type": { "turn_on": "Enheten uppmanas att sl\u00e5s p\u00e5" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_notify": "Till\u00e5t anv\u00e4ndning av dataaviseringstj\u00e4nst." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/sv.json b/homeassistant/components/picnic/translations/sv.json index bff94714036..db0cc6e420a 100644 --- a/homeassistant/components/picnic/translations/sv.json +++ b/homeassistant/components/picnic/translations/sv.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta.", + "different_account": "Kontot b\u00f6r vara detsamma som anv\u00e4ndes f\u00f6r att st\u00e4lla in integrationen", "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, diff --git a/homeassistant/components/plaato/translations/de.json b/homeassistant/components/plaato/translations/de.json index d3e39738b02..fadf91bdbe8 100644 --- a/homeassistant/components/plaato/translations/de.json +++ b/homeassistant/components/plaato/translations/de.json @@ -47,7 +47,7 @@ "title": "Optionen f\u00fcr Plaato" }, "webhook": { - "description": "Webhook-Informationen:\n\n- URL: `{webhook_url}`\n- Methode: POST\n\n", + "description": "Webhook-Informationen:\n\n- URL: `{webhook_url}`\n- Methode: POST", "title": "Optionen f\u00fcr Plaato Airlock" } } diff --git a/homeassistant/components/plaato/translations/sv.json b/homeassistant/components/plaato/translations/sv.json index fe6b9055d06..ea08973ab07 100644 --- a/homeassistant/components/plaato/translations/sv.json +++ b/homeassistant/components/plaato/translations/sv.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Konto har redan konfigurerats", + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." }, diff --git a/homeassistant/components/plex/translations/sv.json b/homeassistant/components/plex/translations/sv.json index c79fdd881fc..81460b66c09 100644 --- a/homeassistant/components/plex/translations/sv.json +++ b/homeassistant/components/plex/translations/sv.json @@ -15,6 +15,7 @@ "not_found": "Plex-server hittades inte", "ssl_error": "Problem med SSL-certifikat" }, + "flow_title": "{name} ({host})", "step": { "manual_setup": { "data": { diff --git a/homeassistant/components/plugwise/translations/sv.json b/homeassistant/components/plugwise/translations/sv.json index 072006dd32e..379e1eb8a8c 100644 --- a/homeassistant/components/plugwise/translations/sv.json +++ b/homeassistant/components/plugwise/translations/sv.json @@ -7,8 +7,10 @@ "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", + "invalid_setup": "L\u00e4gg till din Adam ist\u00e4llet f\u00f6r din Anna, se Home Assistant Plugwise integrationsdokumentation f\u00f6r mer information", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plum_lightpad/translations/sv.json b/homeassistant/components/plum_lightpad/translations/sv.json index 26e9f2d6a49..4416244e366 100644 --- a/homeassistant/components/plum_lightpad/translations/sv.json +++ b/homeassistant/components/plum_lightpad/translations/sv.json @@ -1,8 +1,15 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "E-postadress" } } diff --git a/homeassistant/components/point/translations/sv.json b/homeassistant/components/point/translations/sv.json index 51627c78f3d..7fbf6d35c9f 100644 --- a/homeassistant/components/point/translations/sv.json +++ b/homeassistant/components/point/translations/sv.json @@ -4,7 +4,8 @@ "already_setup": "Du kan endast konfigurera ett Point-konto.", "authorize_url_timeout": "Timeout n\u00e4r genererar url f\u00f6r auktorisering.", "external_setup": "Point har lyckats med konfigurering ifr\u00e5n ett annat fl\u00f6de.", - "no_flows": "Du beh\u00f6ver konfigurera Point innan de kan autentisera med den. [L\u00e4s instruktioner](https://www.home-assistant.io/components/point/)." + "no_flows": "Du beh\u00f6ver konfigurera Point innan de kan autentisera med den. [L\u00e4s instruktioner](https://www.home-assistant.io/components/point/).", + "unknown_authorize_url_generation": "Ok\u00e4nt fel vid generering av en auktoriserad URL." }, "create_entry": { "default": "Lyckad autentisering med Minut f\u00f6r din(a) Point-enhet(er)" diff --git a/homeassistant/components/poolsense/translations/sv.json b/homeassistant/components/poolsense/translations/sv.json new file mode 100644 index 00000000000..6d40f08dd1f --- /dev/null +++ b/homeassistant/components/poolsense/translations/sv.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, + "step": { + "user": { + "data": { + "email": "E-post", + "password": "L\u00f6senord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/sv.json b/homeassistant/components/powerwall/translations/sv.json index bc4dcc606ff..578eba75a05 100644 --- a/homeassistant/components/powerwall/translations/sv.json +++ b/homeassistant/components/powerwall/translations/sv.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { @@ -12,6 +13,17 @@ }, "flow_title": "{name} ({ip_address})", "step": { + "confirm_discovery": { + "description": "Vill du konfigurera {name} ({ip_address})?", + "title": "Anslut till powerwall" + }, + "reauth_confim": { + "data": { + "password": "L\u00f6senord" + }, + "description": "L\u00f6senordet \u00e4r vanligtvis de sista 5 tecknen i serienumret f\u00f6r Backup Gateway och kan hittas i Tesla-appen eller de sista 5 tecknen i l\u00f6senordet som finns innanf\u00f6r d\u00f6rren f\u00f6r Backup Gateway 2.", + "title": "Autentisera powerwall igen" + }, "user": { "data": { "ip_address": "IP-adress", diff --git a/homeassistant/components/profiler/translations/sv.json b/homeassistant/components/profiler/translations/sv.json new file mode 100644 index 00000000000..dda1c5ef70f --- /dev/null +++ b/homeassistant/components/profiler/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du starta konfigurationen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/sv.json b/homeassistant/components/prosegur/translations/sv.json index 4ae0ae62971..6c129cc5a64 100644 --- a/homeassistant/components/prosegur/translations/sv.json +++ b/homeassistant/components/prosegur/translations/sv.json @@ -1,13 +1,25 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "reauth_confirm": { "data": { + "description": "Autentisera p\u00e5 nytt med Prosegur-konto.", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } }, "user": { "data": { + "country": "Land", "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } diff --git a/homeassistant/components/ps4/translations/sv.json b/homeassistant/components/ps4/translations/sv.json index 26af627749d..f3382628633 100644 --- a/homeassistant/components/ps4/translations/sv.json +++ b/homeassistant/components/ps4/translations/sv.json @@ -23,12 +23,18 @@ "ip_address": "IP-adress", "name": "Namn", "region": "Region" + }, + "data_description": { + "code": "Navigera till \"Inst\u00e4llningar\" p\u00e5 din PlayStation 4-konsol. Navigera sedan till \"Mobile App Connection Settings\" och v\u00e4lj \"Add Device\" f\u00f6r att f\u00e5 fram PIN-koden." } }, "mode": { "data": { "ip_address": "IP-adress (l\u00e4mna tom om du anv\u00e4nder automatisk uppt\u00e4ckt).", "mode": "Konfigureringsl\u00e4ge" + }, + "data_description": { + "ip_address": "L\u00e4mna tomt om du v\u00e4ljer automatisk uppt\u00e4ckt." } } } diff --git a/homeassistant/components/pure_energie/translations/sv.json b/homeassistant/components/pure_energie/translations/sv.json new file mode 100644 index 00000000000..d85762bb0b4 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/sv.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{model} ({host})", + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + }, + "zeroconf_confirm": { + "description": "Vill du l\u00e4gga till Pure Energie Meter (` {model} `) till Home Assistant?", + "title": "Uppt\u00e4ckte Pure Energie Meter-enhet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvoutput/translations/sv.json b/homeassistant/components/pvoutput/translations/sv.json index 5ad5b5b6db4..75a03895a40 100644 --- a/homeassistant/components/pvoutput/translations/sv.json +++ b/homeassistant/components/pvoutput/translations/sv.json @@ -1,15 +1,25 @@ { "config": { + "abort": { + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "reauth_confirm": { "data": { "api_key": "API-nyckel" - } + }, + "description": "F\u00f6r att autentisera p\u00e5 nytt med PVOutput m\u00e5ste du h\u00e4mta API-nyckeln p\u00e5 {account_url} ." }, "user": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "system_id": "System-ID" + }, + "description": "F\u00f6r att autentisera med PVOutput m\u00e5ste du h\u00e4mta API-nyckeln p\u00e5 {account_url} . \n\n System-ID:n f\u00f6r registrerade system listas p\u00e5 samma sida." } } } diff --git a/homeassistant/components/radio_browser/translations/sv.json b/homeassistant/components/radio_browser/translations/sv.json new file mode 100644 index 00000000000..69d8658d2ac --- /dev/null +++ b/homeassistant/components/radio_browser/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du l\u00e4gga till Radio Browser till Home Assistant?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/sv.json b/homeassistant/components/rainmachine/translations/sv.json index 40295676501..10e06693207 100644 --- a/homeassistant/components/rainmachine/translations/sv.json +++ b/homeassistant/components/rainmachine/translations/sv.json @@ -3,6 +3,10 @@ "abort": { "already_configured": "Denna RainMachine-enhet \u00e4r redan konfigurerad" }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, + "flow_title": "{ip}", "step": { "user": { "data": { diff --git a/homeassistant/components/remote/translations/sv.json b/homeassistant/components/remote/translations/sv.json index ef7a2ab0ad2..2fff0f39aa4 100644 --- a/homeassistant/components/remote/translations/sv.json +++ b/homeassistant/components/remote/translations/sv.json @@ -10,6 +10,7 @@ "is_on": "{entity_name} \u00e4r p\u00e5" }, "trigger_type": { + "changed_states": "{entity_name} slogs p\u00e5 eller av", "turned_off": "{entity_name} st\u00e4ngdes av", "turned_on": "{entity_name} slogs p\u00e5" } diff --git a/homeassistant/components/renault/translations/sv.json b/homeassistant/components/renault/translations/sv.json index a3bea645b0f..484daf9b774 100644 --- a/homeassistant/components/renault/translations/sv.json +++ b/homeassistant/components/renault/translations/sv.json @@ -1,6 +1,27 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "kamereon_no_account": "Det gick inte att hitta Kamereon-kontot", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "invalid_credentials": "Ogiltig autentisering" + }, "step": { + "kamereon": { + "data": { + "kamereon_account_id": "Kamereon konto-id" + }, + "title": "V\u00e4lj Kamereon-konto-id" + }, + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Uppdatera ditt l\u00f6senord f\u00f6r {username}", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "locale": "Plats", diff --git a/homeassistant/components/rfxtrx/translations/sv.json b/homeassistant/components/rfxtrx/translations/sv.json index be9e6e944a7..c35b96e9f44 100644 --- a/homeassistant/components/rfxtrx/translations/sv.json +++ b/homeassistant/components/rfxtrx/translations/sv.json @@ -60,7 +60,8 @@ "automatic_add": "Aktivera automatisk till\u00e4gg av enheter", "debug": "Aktivera fels\u00f6kning", "device": "V\u00e4lj enhet att konfigurera", - "event_code": "Ange h\u00e4ndelsekod att l\u00e4gga till" + "event_code": "Ange h\u00e4ndelsekod att l\u00e4gga till", + "protocols": "Protokoll" }, "title": "Rfxtrx-alternativ" }, @@ -71,7 +72,8 @@ "data_bit": "Antal databitar", "off_delay": "Avst\u00e4ngningsf\u00f6rdr\u00f6jning", "off_delay_enabled": "Aktivera avst\u00e4ngningsf\u00f6rdr\u00f6jning", - "replace_device": "V\u00e4lj enhet att ers\u00e4tta" + "replace_device": "V\u00e4lj enhet att ers\u00e4tta", + "venetian_blind_mode": "L\u00e4ge f\u00f6r persienner" }, "title": "Konfigurera enhetsalternativ" } diff --git a/homeassistant/components/ridwell/translations/sv.json b/homeassistant/components/ridwell/translations/sv.json index 23c825f256f..b62664b842d 100644 --- a/homeassistant/components/ridwell/translations/sv.json +++ b/homeassistant/components/ridwell/translations/sv.json @@ -1,10 +1,27 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Ange l\u00f6senordet f\u00f6r {anv\u00e4ndarnamn} p\u00e5 nytt:", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "description": "Ange ditt anv\u00e4ndarnamn och l\u00f6senord:" } } } diff --git a/homeassistant/components/risco/translations/sv.json b/homeassistant/components/risco/translations/sv.json index 662583561d0..f0194b4edf5 100644 --- a/homeassistant/components/risco/translations/sv.json +++ b/homeassistant/components/risco/translations/sv.json @@ -21,8 +21,23 @@ "options": { "step": { "ha_to_risco": { + "data": { + "armed_away": "Bortalarmat", + "armed_custom_bypass": "Larmat anpassad f\u00f6rbikoppling", + "armed_home": "Hemmalarmat", + "armed_night": "Nattlarmat" + }, + "description": "V\u00e4lj vilket l\u00e4ge du vill st\u00e4lla in ditt Risco-larm p\u00e5 n\u00e4r Home Assistant-larmet aktiveras", "title": "Mappa Home Assistant tillst\u00e5nd till Risco tillst\u00e5nd" }, + "init": { + "data": { + "code_arm_required": "Kr\u00e4v Pin-kod f\u00f6r att aktivera", + "code_disarm_required": "Kr\u00e4v Pin-kod f\u00f6r att avaktivera", + "scan_interval": "Hur ofta ska man fr\u00e5ga Risco (i sekunder)" + }, + "title": "Konfigurera alternativ" + }, "risco_to_ha": { "data": { "A": "Grupp A", diff --git a/homeassistant/components/roku/translations/sv.json b/homeassistant/components/roku/translations/sv.json index 0acdb41359d..a470c9455ad 100644 --- a/homeassistant/components/roku/translations/sv.json +++ b/homeassistant/components/roku/translations/sv.json @@ -10,6 +10,9 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "description": "Vill du konfigurera {name}?" + }, "user": { "data": { "host": "V\u00e4rd" diff --git a/homeassistant/components/rtsp_to_webrtc/translations/sv.json b/homeassistant/components/rtsp_to_webrtc/translations/sv.json new file mode 100644 index 00000000000..c748d2feb9c --- /dev/null +++ b/homeassistant/components/rtsp_to_webrtc/translations/sv.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "server_failure": "RTSPtoWebRTC-servern returnerade ett fel. Kontrollera loggar f\u00f6r mer information.", + "server_unreachable": "Det g\u00e5r inte att kommunicera med RTSPtoWebRTC-servern. Kontrollera loggar f\u00f6r mer information.", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "error": { + "invalid_url": "M\u00e5ste vara en giltig RTSPtoWebRTC-serveradress, t.ex. https://example.com", + "server_failure": "RTSPtoWebRTC-servern returnerade ett fel. Kontrollera loggar f\u00f6r mer information.", + "server_unreachable": "Det g\u00e5r inte att kommunicera med RTSPtoWebRTC-servern. Kontrollera loggar f\u00f6r mer information." + }, + "step": { + "hassio_confirm": { + "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till RTSPtoWebRTC-servern som tillhandah\u00e5lls av till\u00e4gget: {addon} ?", + "title": "RTSPtoWebRTC via Home Assistant-till\u00e4gget" + }, + "user": { + "data": { + "server_url": "RTSPtoWebRTC server URL t.ex. https://example.com" + }, + "description": "RTSPtoWebRTC-integrationen kr\u00e4ver en server f\u00f6r att \u00f6vers\u00e4tta RTSP-str\u00f6mmar till WebRTC. Ange URL:en till RTSPtoWebRTC-servern.", + "title": "Konfigurera RTSPtoWebRTC" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/sv.json b/homeassistant/components/samsungtv/translations/sv.json index 67c8373d0e3..7de006b6a11 100644 --- a/homeassistant/components/samsungtv/translations/sv.json +++ b/homeassistant/components/samsungtv/translations/sv.json @@ -4,25 +4,33 @@ "already_configured": "Denna Samsung TV \u00e4r redan konfigurerad.", "already_in_progress": "Samsung TV-konfiguration p\u00e5g\u00e5r redan.", "auth_missing": "Home Assistant har inte beh\u00f6righet att ansluta till denna Samsung TV. Kontrollera tv:ns inst\u00e4llningar f\u00f6r att godk\u00e4nna Home Assistant.", + "cannot_connect": "Det gick inte att ansluta.", "id_missing": "Denna Samsung-enhet har inget serienummer.", "not_supported": "Denna Samsung enhet st\u00f6ds f\u00f6r n\u00e4rvarande inte.", "reauth_successful": "\u00c5terautentisering lyckades", "unknown": "Ov\u00e4ntat fel" }, "error": { - "auth_missing": "Home Assistant har inte beh\u00f6righet att ansluta till denna Samsung TV. Kontrollera tv:ns inst\u00e4llningar f\u00f6r att godk\u00e4nna Home Assistant." + "auth_missing": "Home Assistant har inte beh\u00f6righet att ansluta till denna Samsung TV. Kontrollera tv:ns inst\u00e4llningar f\u00f6r att godk\u00e4nna Home Assistant.", + "invalid_pin": "PIN-koden \u00e4r ogiltig, f\u00f6rs\u00f6k igen." }, "flow_title": "{device}", "step": { "confirm": { "description": "Vill du st\u00e4lla in Samsung TV {device}? Om du aldrig har anslutit Home Assistant innan du ska se ett popup-f\u00f6nster p\u00e5 tv:n och be om auktorisering. Manuella konfigurationer f\u00f6r den h\u00e4r TV:n skrivs \u00f6ver." }, + "encrypted_pairing": { + "description": "Ange PIN-koden som visas p\u00e5 {device} ." + }, "pairing": { "description": "Vill du st\u00e4lla in Samsung TV {device}? Om du aldrig har anslutit Home Assistant innan du ska se ett popup-f\u00f6nster p\u00e5 tv:n och be om auktorisering. Manuella konfigurationer f\u00f6r den h\u00e4r TV:n skrivs \u00f6ver." }, "reauth_confirm": { "description": "N\u00e4r du har skickat, acceptera popup-f\u00f6nstret p\u00e5 {enheten} som beg\u00e4r auktorisering inom 30 sekunder eller ange PIN-koden." }, + "reauth_confirm_encrypted": { + "description": "Ange PIN-koden som visas p\u00e5 {device} ." + }, "user": { "data": { "host": "V\u00e4rdnamn eller IP-adress", diff --git a/homeassistant/components/season/translations/sv.json b/homeassistant/components/season/translations/sv.json index c0b662beebe..649789e560e 100644 --- a/homeassistant/components/season/translations/sv.json +++ b/homeassistant/components/season/translations/sv.json @@ -2,6 +2,13 @@ "config": { "abort": { "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "step": { + "user": { + "data": { + "type": "Typ av s\u00e4songsdefinition" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/sense/translations/sv.json b/homeassistant/components/sense/translations/sv.json index 76e41ccb1ba..f805dd483d8 100644 --- a/homeassistant/components/sense/translations/sv.json +++ b/homeassistant/components/sense/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta, f\u00f6rs\u00f6k igen", @@ -9,12 +10,26 @@ "unknown": "Ov\u00e4ntat fel" }, "step": { + "reauth_validate": { + "data": { + "password": "L\u00f6senord" + }, + "description": "The Sense integration needs to re-authenticate your account {email}.", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "email": "E-postadress", - "password": "L\u00f6senord" + "password": "L\u00f6senord", + "timeout": "Timeout" }, "title": "Anslut till din Sense Energy Monitor" + }, + "validation": { + "data": { + "code": "Verifieringskod" + }, + "title": "Sense Multifaktorsautentisering" } } } diff --git a/homeassistant/components/senseme/translations/sv.json b/homeassistant/components/senseme/translations/sv.json index 46631acc69a..ac7e8f6f76a 100644 --- a/homeassistant/components/senseme/translations/sv.json +++ b/homeassistant/components/senseme/translations/sv.json @@ -1,7 +1,30 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "cannot_connect": "Det gick inte att ansluta." + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress" + }, + "flow_title": "{name} - {model} ({host})", + "step": { + "discovery_confirm": { + "description": "Vill du konfigurera {name} - {model} ({host})?" + }, + "manual": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Ange en IP-adress." + }, + "user": { + "data": { + "device": "Enhet" + }, + "description": "V\u00e4lj en enhet eller v\u00e4lj \"IP-adress\" f\u00f6r att manuellt ange en IP-adress." + } } } } \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sv.json b/homeassistant/components/sensibo/translations/sv.json index 5ad5b5b6db4..04f12a04cd4 100644 --- a/homeassistant/components/sensibo/translations/sv.json +++ b/homeassistant/components/sensibo/translations/sv.json @@ -1,5 +1,16 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "incorrect_api_key": "Ogiltig API-nyckel f\u00f6r valt konto", + "invalid_auth": "Ogiltig autentisering", + "no_devices": "Inga enheter uppt\u00e4cktes", + "no_username": "Kunde inte h\u00e4mta anv\u00e4ndarnamn" + }, "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/sensor/translations/sv.json b/homeassistant/components/sensor/translations/sv.json index 24b137a420b..91f79c0c703 100644 --- a/homeassistant/components/sensor/translations/sv.json +++ b/homeassistant/components/sensor/translations/sv.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_apparent_power": "Nuvarande {entity_name} skenbar effekt", "is_battery_level": "Nuvarande {entity_name} batteriniv\u00e5", "is_carbon_dioxide": "Nuvarande {entity_name} koncentration av koldioxid", "is_carbon_monoxide": "Nuvarande {entity_name} koncentration av kolmonoxid", @@ -29,15 +30,19 @@ "is_voltage": "Nuvarande {entity_name} sp\u00e4nning" }, "trigger_type": { + "apparent_power": "{entity_name} uppenbara effektf\u00f6r\u00e4ndringar", "battery_level": "{entity_name} batteriniv\u00e5 \u00e4ndras", "carbon_dioxide": "{entity_name} f\u00f6r\u00e4ndringar av koldioxidkoncentrationen", "carbon_monoxide": "{entity_name} f\u00f6r\u00e4ndringar av kolmonoxidkoncentrationen", + "current": "{entity_name} aktuella \u00e4ndringar", "energy": "Energif\u00f6r\u00e4ndringar", "frequency": "{entity_name} frekvens\u00e4ndringar", "gas": "{entity_name} gasf\u00f6r\u00e4ndringar", "humidity": "{entity_name} fuktighet \u00e4ndras", "illuminance": "{entity_name} belysning \u00e4ndras", "nitrogen_dioxide": "{entity_name} kv\u00e4vedioxidkoncentrationen f\u00f6r\u00e4ndras.", + "nitrogen_monoxide": "{entity_name} koncentrationen av kv\u00e4vemonoxid \u00e4ndras", + "nitrous_oxide": "{entity_name} f\u00f6r\u00e4ndringar i koncentrationen av lustgas", "ozone": "{entity_name} ozonkoncentrationen f\u00f6r\u00e4ndras", "pm1": "{entity_name} PM1-koncentrationsf\u00f6r\u00e4ndringar", "pm10": "{entity_name} PM10-koncentrations\u00e4ndringar", diff --git a/homeassistant/components/shelly/translations/sv.json b/homeassistant/components/shelly/translations/sv.json index d4f3f6f400e..62262c8558c 100644 --- a/homeassistant/components/shelly/translations/sv.json +++ b/homeassistant/components/shelly/translations/sv.json @@ -1,13 +1,20 @@ { "config": { "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", "unsupported_firmware": "Enheten anv\u00e4nder en firmwareversion som inte st\u00f6ds." }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "firmware_not_fully_provisioned": "Enheten \u00e4r inte helt etablerad. Kontakta Shellys support", - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{name}", "step": { + "confirm_discovery": { + "description": "Vill du konfigurera {model} p\u00e5 {host} ? \n\n Batteridrivna enheter som \u00e4r l\u00f6senordsskyddade m\u00e5ste v\u00e4ckas innan du forts\u00e4tter med installationen.\n Batteridrivna enheter som inte \u00e4r l\u00f6senordsskyddade kommer att l\u00e4ggas till n\u00e4r enheten vaknar, du kan nu manuellt v\u00e4cka enheten med en knapp p\u00e5 den eller v\u00e4nta p\u00e5 n\u00e4sta datauppdatering fr\u00e5n enheten." + }, "credentials": { "data": { "password": "L\u00f6senord", @@ -15,20 +22,33 @@ } }, "user": { + "data": { + "host": "V\u00e4rd" + }, "description": "F\u00f6re installationen m\u00e5ste batteridrivna enheter v\u00e4ckas, du kan nu v\u00e4cka enheten med en knapp p\u00e5 den." } } }, "device_automation": { "trigger_subtype": { + "button": "Knapp", + "button1": "F\u00f6rsta knappen", + "button2": "Andra knappen", + "button3": "Tredje knappen", "button4": "Fj\u00e4rde knappen" }, "trigger_type": { "btn_down": "\"{subtype}\" knappen nedtryckt", "btn_up": "\"{subtype}\" knappen uppsl\u00e4ppt", + "double": "\"{subtyp}\" dubbelklickad", "double_push": "{subtype} dubbeltryck", + "long": "{subtype} l\u00e5ngklickad", "long_push": "{subtype} l\u00e5ngtryck", - "single_push": "{subtyp} enkeltryck" + "long_single": "{subtype} l\u00e5ngklickad och sedan enkelklickad", + "single": "{subtype} enkelklickad", + "single_long": "{subtype} enkelklickad och sedan l\u00e5ngklickad", + "single_push": "{subtyp} enkeltryck", + "triple": "{subtype} trippelklickad" } } } \ No newline at end of file diff --git a/homeassistant/components/sia/translations/sv.json b/homeassistant/components/sia/translations/sv.json index fe4c471f302..9fac789fcfb 100644 --- a/homeassistant/components/sia/translations/sv.json +++ b/homeassistant/components/sia/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "error": { + "invalid_account_format": "Kontot \u00e4r inte ett hexadecimalt v\u00e4rde, anv\u00e4nd endast 0-9 och AF.", "invalid_account_length": "Kontot har inte r\u00e4tt l\u00e4ngd, det m\u00e5ste vara mellan 3 och 16 tecken.", "invalid_key_format": "Nyckeln \u00e4r inte ett hexadecimalt v\u00e4rde, anv\u00e4nd endast 0-9 och AF.", "invalid_key_length": "Nyckeln har inte r\u00e4tt l\u00e4ngd, den m\u00e5ste vara 16, 24 eller 32 hexadecken.", @@ -12,18 +13,22 @@ "additional_account": { "data": { "account": "Konto-ID", + "additional_account": "Ytterligare konton", "encryption_key": "Krypteringsnyckel", - "ping_interval": "Pingintervall (min)" + "ping_interval": "Pingintervall (min)", + "zones": "Antal zoner f\u00f6r kontot" }, "title": "L\u00e4gg till ett annat konto till den aktuella porten." }, "user": { "data": { "account": "Konto-ID", + "additional_account": "Ytterligare konton", "encryption_key": "Krypteringsnyckel", "ping_interval": "Pingintervall (min)", "port": "Port", - "protocol": "Protokoll" + "protocol": "Protokoll", + "zones": "Antal zoner f\u00f6r kontot" }, "title": "Skapa en anslutning f\u00f6r SIA-baserade larmsystem." } @@ -33,7 +38,8 @@ "step": { "options": { "data": { - "ignore_timestamps": "Ignorera tidsst\u00e4mpelkontrollen f\u00f6r SIA-h\u00e4ndelserna" + "ignore_timestamps": "Ignorera tidsst\u00e4mpelkontrollen f\u00f6r SIA-h\u00e4ndelserna", + "zones": "Antal zoner f\u00f6r kontot" }, "description": "St\u00e4ll in alternativen f\u00f6r kontot: {account}", "title": "Alternativ f\u00f6r SIA-installationen." diff --git a/homeassistant/components/simplepush/translations/de.json b/homeassistant/components/simplepush/translations/de.json index 523ffda32bf..16e916f6d2f 100644 --- a/homeassistant/components/simplepush/translations/de.json +++ b/homeassistant/components/simplepush/translations/de.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "Die Konfiguration von Simplepush mittels YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die Simplepush-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", "title": "Die Simplepush YAML-Konfiguration wird entfernt" + }, + "removed_yaml": { + "description": "Die Konfiguration von Simplepush mittels YAML wurde entfernt.\n\nDeine bestehende YAML-Konfiguration wird vom Home Assistant nicht verwendet.\n\nEntferne die Simplepush-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Simplepush YAML-Konfiguration wurde entfernt" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/hu.json b/homeassistant/components/simplepush/translations/hu.json index b1f7b519dc5..b5809898fb1 100644 --- a/homeassistant/components/simplepush/translations/hu.json +++ b/homeassistant/components/simplepush/translations/hu.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "A Simplepush YAML-ben megadott konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a Simplepush YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", "title": "A Simplepush YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + }, + "removed_yaml": { + "description": "A Simplepush YAML-ban t\u00f6rt\u00e9n\u0151 konfigur\u00e1l\u00e1sa elt\u00e1vol\u00edt\u00e1sra ker\u00fclt.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3t a Home Assistant nem haszn\u00e1lja.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a Simplepush YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Simplepush YAML konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fclt" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/ja.json b/homeassistant/components/simplepush/translations/ja.json index 398fa3d63b0..c407990aa4d 100644 --- a/homeassistant/components/simplepush/translations/ja.json +++ b/homeassistant/components/simplepush/translations/ja.json @@ -22,6 +22,9 @@ "deprecated_yaml": { "description": "Simplepush\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Simplepush\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Simplepush YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "removed_yaml": { + "title": "Simplepush YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/no.json b/homeassistant/components/simplepush/translations/no.json index af15a931acc..453632f348e 100644 --- a/homeassistant/components/simplepush/translations/no.json +++ b/homeassistant/components/simplepush/translations/no.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "Konfigurering av Simplepush med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Simplepush YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", "title": "Simplepush YAML-konfigurasjonen blir fjernet" + }, + "removed_yaml": { + "description": "Konfigurering av Simplepush med YAML er fjernet. \n\n Din eksisterende YAML-konfigurasjon brukes ikke av Home Assistant. \n\n Fjern Simplepush YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Simplepush YAML-konfigurasjonen er fjernet" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/ru.json b/homeassistant/components/simplepush/translations/ru.json index 2ddcba76929..e615a4c4c72 100644 --- a/homeassistant/components/simplepush/translations/ru.json +++ b/homeassistant/components/simplepush/translations/ru.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + }, + "removed_yaml": { + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Simplepush \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/sv.json b/homeassistant/components/simplepush/translations/sv.json index 9ed6908f0c3..2572b2cce75 100644 --- a/homeassistant/components/simplepush/translations/sv.json +++ b/homeassistant/components/simplepush/translations/sv.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "Konfigurering av Simplepush med YAML tas bort. \n\n Din befintliga YAML-konfiguration har automatiskt importerats till anv\u00e4ndargr\u00e4nssnittet. \n\n Ta bort Simplepush YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", "title": "Simplepush YAML-konfigurationen tas bort" + }, + "removed_yaml": { + "description": "Konfigurering av Simplepush med YAML har tagits bort. \n\n Din befintliga YAML-konfiguration anv\u00e4nds inte av Home Assistant. \n\n Ta bort Simplepush YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", + "title": "Simplepush YAML-konfigurationen har tagits bort" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/zh-Hant.json b/homeassistant/components/simplepush/translations/zh-Hant.json index 15cd0bedb37..2e5b3576135 100644 --- a/homeassistant/components/simplepush/translations/zh-Hant.json +++ b/homeassistant/components/simplepush/translations/zh-Hant.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Simplepush \u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Simplepush YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "Simplepush YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + }, + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Simplepush \u7684\u529f\u80fd\u5df2\u7d93\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Simplepush YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Simplepush YAML \u8a2d\u5b9a\u5df2\u7d93\u79fb\u9664" } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json index dbb3d0e4207..9f44ba9ade9 100644 --- a/homeassistant/components/simplisafe/translations/ca.json +++ b/homeassistant/components/simplisafe/translations/ca.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "Compte ja est\u00e0 registrat", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_auth_code_length": "Els codis d'autoritzaci\u00f3 de SimpliSafe tenen una longitud de 45 car\u00e0cters", "unknown": "Error inesperat" }, "progress": { diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index 4788f2201b4..3f9eae5f187 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "Konto bereits registriert", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_auth_code_length": "SimpliSafe Autorisierungscodes sind 45 Zeichen lang", "unknown": "Unerwarteter Fehler" }, "progress": { @@ -34,7 +35,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "SimpliSafe authentifiziert Benutzer \u00fcber seine Web-App. Aufgrund technischer Einschr\u00e4nkungen gibt es am Ende dieses Prozesses einen manuellen Schritt; Bitte stelle sicher, dass Du die [Dokumentation](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) lesen, bevor Sie beginnen. \n\n Wenn Sie fertig sind, klicke [hier]({url}), um die SimpliSafe-Web-App zu \u00f6ffnen und Ihre Anmeldeinformationen einzugeben. Wenn der Vorgang abgeschlossen ist, kehre hierher zur\u00fcck und gebe den Autorisierungscode von der SimpliSafe-Web-App-URL ein." + "description": "SimpliSafe authentifiziert die Benutzer \u00fcber seine Web-App. Aufgrund technischer Beschr\u00e4nkungen gibt es am Ende dieses Prozesses einen manuellen Schritt; bitte stelle sicher, dass du die [Dokumentation] (http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) liest, bevor du beginnst.\n\nWenn du bereit bist, klicke [hier]({url}), um die SimpliSafe-Webanwendung zu \u00f6ffnen und deine Anmeldedaten einzugeben. Wenn du dich bereits bei SimpliSafe in deinem Browser angemeldet hast, kannst du eine neue Registerkarte \u00f6ffnen und dann die oben genannte URL in diese Registerkarte kopieren/einf\u00fcgen.\n\nWenn der Vorgang abgeschlossen ist, kehre hierher zur\u00fcck und gib den Autorisierungscode von der URL \"com.simplisafe.mobile\" ein." } } }, diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index 5b2e898a1e5..78c32522a3d 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "A fi\u00f3k m\u00e1r regisztr\u00e1lva van", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_auth_code_length": "A SimpliSafe enged\u00e9lyez\u00e9si k\u00f3dok 45 karakter hossz\u00faak.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "progress": { diff --git a/homeassistant/components/simplisafe/translations/id.json b/homeassistant/components/simplisafe/translations/id.json index 4d5ddf18ed6..4bb637e3f08 100644 --- a/homeassistant/components/simplisafe/translations/id.json +++ b/homeassistant/components/simplisafe/translations/id.json @@ -35,7 +35,7 @@ "password": "Kata Sandi", "username": "Nama Pengguna" }, - "description": "SimpliSafe mengautentikasi pengguna melalui aplikasi webnya. Karena keterbatasan teknis, ada langkah manual di akhir proses ini; pastikan bahwa Anda membaca [dokumentasi] (http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) sebelum memulai.\n\nJika sudah siap, klik [di sini]({url}) untuk membuka aplikasi web SimpliSafe dan memasukkan kredensial Anda. Setelah proses selesai, kembali ke sini dan masukkan kode otorisasi dari URL aplikasi web SimpliSafe." + "description": "SimpliSafe mengautentikasi pengguna melalui aplikasi webnya. Karena keterbatasan teknis, ada langkah manual di akhir proses ini; pastikan bahwa Anda membaca [dokumentasi] (http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) sebelum memulai.\n\nJika sudah siap, klik [di sini]({url}) untuk membuka aplikasi web SimpliSafe dan memasukkan kredensial Anda. Jika Anda sedang masuk ke SimpliSafe di browser, Anda mungkin harus membuka tab baru, kemudian menyalin-tempel URL di atas di tab baru tersebut.\n\nSetelah proses selesai, kembali ke sini dan masukkan kode otorisasi dari URL `com.simplisafe.mobile`." } } }, diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json index a0335228b2d..2997ca5dba5 100644 --- a/homeassistant/components/simplisafe/translations/no.json +++ b/homeassistant/components/simplisafe/translations/no.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "Konto er allerede registrert", "invalid_auth": "Ugyldig godkjenning", + "invalid_auth_code_length": "SimpliSafe-autorisasjonskoder er p\u00e5 45 tegn", "unknown": "Uventet feil" }, "progress": { @@ -34,7 +35,7 @@ "password": "Passord", "username": "Brukernavn" }, - "description": "SimpliSafe autentiserer brukere via sin nettapp. P\u00e5 grunn av tekniske begrensninger er det et manuelt trinn p\u00e5 slutten av denne prosessen; s\u00f8rg for at du leser [dokumentasjonen](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) f\u00f8r du starter. \n\n N\u00e5r du er klar, klikk [her]( {url} ) for \u00e5 \u00e5pne SimpliSafe-nettappen og angi legitimasjonen din. N\u00e5r prosessen er fullf\u00f8rt, g\u00e5 tilbake hit og skriv inn autorisasjonskoden fra SimpliSafe-nettappens URL." + "description": "SimpliSafe autentiserer brukere via sin nettapp. P\u00e5 grunn av tekniske begrensninger er det et manuelt trinn p\u00e5 slutten av denne prosessen; s\u00f8rg for at du leser [dokumentasjonen](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) f\u00f8r du starter. \n\n N\u00e5r du er klar, klikk [her]( {url} ) for \u00e5 \u00e5pne SimpliSafe-nettappen og angi legitimasjonen din. Hvis du allerede har logget p\u00e5 SimpliSafe i nettleseren din, kan det v\u00e6re lurt \u00e5 \u00e5pne en ny fane, og deretter kopiere/lime inn URL-en ovenfor i den fanen. \n\n N\u00e5r prosessen er fullf\u00f8rt, g\u00e5 tilbake hit og skriv inn autorisasjonskoden fra `com.simplisafe.mobile` URL." } } }, diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index 1b88417dd27..863a67c3b89 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_auth_code_length": "\u041a\u043e\u0434\u044b \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 SimpliSafe \u0441\u043e\u0441\u0442\u043e\u044f\u0442 \u0438\u0437 45 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "progress": { diff --git a/homeassistant/components/simplisafe/translations/sv.json b/homeassistant/components/simplisafe/translations/sv.json index 2e24eb856bd..61e00432950 100644 --- a/homeassistant/components/simplisafe/translations/sv.json +++ b/homeassistant/components/simplisafe/translations/sv.json @@ -8,6 +8,8 @@ }, "error": { "identifier_exists": "Kontot \u00e4r redan registrerat", + "invalid_auth": "Ogiltig autentisering", + "invalid_auth_code_length": "SimpliSafe auktoriseringskoder \u00e4r 45 tecken l\u00e5nga", "unknown": "Ov\u00e4ntat fel" }, "progress": { diff --git a/homeassistant/components/simplisafe/translations/zh-Hant.json b/homeassistant/components/simplisafe/translations/zh-Hant.json index 99f08eb14d4..fe6a3fa5ce0 100644 --- a/homeassistant/components/simplisafe/translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/translations/zh-Hant.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "\u5e33\u865f\u5df2\u8a3b\u518a", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_auth_code_length": "SimpliSafe \u8a8d\u8b49\u78bc\u70ba 45 \u500b\u5b57\u5143\u9577", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "progress": { @@ -34,7 +35,7 @@ "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "SimpliSafe \u70ba\u900f\u904e Web App \u65b9\u5f0f\u7684\u8a8d\u8b49\u5176\u4f7f\u7528\u8005\u3002\u7531\u65bc\u6280\u8853\u9650\u5236\u3001\u65bc\u6b64\u904e\u7a0b\u7d50\u675f\u6642\u5c07\u6703\u6709\u4e00\u6b65\u624b\u52d5\u968e\u6bb5\uff1b\u65bc\u958b\u59cb\u524d\u3001\u8acb\u78ba\u5b9a\u53c3\u95b1 [\u76f8\u95dc\u6587\u4ef6](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3002\n\n\u6e96\u5099\u5c31\u7dd2\u5f8c\u3001\u9ede\u9078 [\u6b64\u8655]({url}) \u4ee5\u958b\u555f SimpliSafe Web App \u4e26\u8f38\u5165\u9a57\u8b49\u3002\u5b8c\u6210\u5f8c\u56de\u5230\u9019\u88e1\u4e26\u8f38\u5165\u7531 SimpliSafe Web App \u6240\u53d6\u7684\u8a8d\u8b49\u78bc\u3002" + "description": "SimpliSafe \u70ba\u900f\u904e Web App \u65b9\u5f0f\u7684\u8a8d\u8b49\u5176\u4f7f\u7528\u8005\u3002\u7531\u65bc\u6280\u8853\u9650\u5236\u3001\u65bc\u6b64\u904e\u7a0b\u7d50\u675f\u6642\u5c07\u6703\u6709\u4e00\u6b65\u624b\u52d5\u968e\u6bb5\uff1b\u65bc\u958b\u59cb\u524d\u3001\u8acb\u78ba\u5b9a\u53c3\u95b1 [\u76f8\u95dc\u6587\u4ef6](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code)\u3002\n\n\u6e96\u5099\u5c31\u7dd2\u5f8c\u3001\u9ede\u9078 [\u6b64\u8655]({url}) \u4ee5\u958b\u555f SimpliSafe Web App \u4e26\u8f38\u5165\u9a57\u8b49\u3002\u5047\u5982\u5df2\u7d93\u65bc\u700f\u89bd\u5668\u4e2d\u767b\u5165 SimpliSafe\uff0c\u53ef\u80fd\u9700\u8981\u958b\u555f\u65b0\u9801\u9762\u3001\u7136\u5f8c\u65bc\u9801\u9762\u7db2\u5740\u8907\u88fd/\u8cbc\u4e0a\u4e0a\u65b9 URL\u3002\n\n\u5b8c\u6210\u5f8c\u56de\u5230\u9019\u88e1\u4e26\u8f38\u5165\u7531 `com.simplisafe.mobile` \u6240\u53d6\u5f97\u7684\u8a8d\u8b49\u78bc\u3002" } } }, diff --git a/homeassistant/components/sleepiq/translations/sv.json b/homeassistant/components/sleepiq/translations/sv.json index 23c825f256f..e22bee20046 100644 --- a/homeassistant/components/sleepiq/translations/sv.json +++ b/homeassistant/components/sleepiq/translations/sv.json @@ -1,8 +1,24 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "SleepIQ-integrationen m\u00e5ste autentisera ditt konto {username} igen.", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/smhi/translations/sv.json b/homeassistant/components/smhi/translations/sv.json index 0bb597e393b..f9aac9e7d2b 100644 --- a/homeassistant/components/smhi/translations/sv.json +++ b/homeassistant/components/smhi/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, "error": { "wrong_location": "Plats i Sverige endast" }, diff --git a/homeassistant/components/solaredge/translations/sv.json b/homeassistant/components/solaredge/translations/sv.json index df7408b43c2..e956870d013 100644 --- a/homeassistant/components/solaredge/translations/sv.json +++ b/homeassistant/components/solaredge/translations/sv.json @@ -1,7 +1,13 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, "error": { - "invalid_api_key": "Ogiltig API-nyckel" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "could_not_connect": "Det gick inte att ansluta till solaredge API", + "invalid_api_key": "Ogiltig API-nyckel", + "site_not_active": "Webbplatsen \u00e4r inte aktiv" }, "step": { "user": { diff --git a/homeassistant/components/somfy_mylink/translations/sv.json b/homeassistant/components/somfy_mylink/translations/sv.json index 6cacc18d1a9..75b4d6abd88 100644 --- a/homeassistant/components/somfy_mylink/translations/sv.json +++ b/homeassistant/components/somfy_mylink/translations/sv.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "flow_title": "{mac} ({ip})", "step": { "user": { @@ -24,6 +32,9 @@ "title": "Konfigurera MyLink-alternativ" }, "target_config": { + "data": { + "reverse": "Locket \u00e4r omv\u00e4nt" + }, "description": "Konfigurera alternativ f\u00f6r ` {target_name} `", "title": "Konfigurera MyLink Cover" } diff --git a/homeassistant/components/sonarr/translations/sv.json b/homeassistant/components/sonarr/translations/sv.json index c17fa818ed8..01dfc35be20 100644 --- a/homeassistant/components/sonarr/translations/sv.json +++ b/homeassistant/components/sonarr/translations/sv.json @@ -18,6 +18,7 @@ "user": { "data": { "api_key": "API nyckel", + "url": "URL", "verify_ssl": "Verifiera SSL-certifikat" } } diff --git a/homeassistant/components/songpal/translations/sv.json b/homeassistant/components/songpal/translations/sv.json index ea2b5e2d715..d67bad837e9 100644 --- a/homeassistant/components/songpal/translations/sv.json +++ b/homeassistant/components/songpal/translations/sv.json @@ -4,6 +4,9 @@ "already_configured": "Enheten \u00e4r redan konfigurerad", "not_songpal_device": "Inte en Songpal-enhet" }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, "flow_title": "Sony Songpal {name} ({host})", "step": { "init": { diff --git a/homeassistant/components/spider/translations/sv.json b/homeassistant/components/spider/translations/sv.json index d078c6d4110..678d8895de8 100644 --- a/homeassistant/components/spider/translations/sv.json +++ b/homeassistant/components/spider/translations/sv.json @@ -4,13 +4,16 @@ "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." }, "error": { - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Logga in med mijn.ithodaalderop.nl-konto" } } } diff --git a/homeassistant/components/spotify/translations/sv.json b/homeassistant/components/spotify/translations/sv.json index 08c7e415edf..3dbcb9dc85a 100644 --- a/homeassistant/components/spotify/translations/sv.json +++ b/homeassistant/components/spotify/translations/sv.json @@ -3,7 +3,8 @@ "abort": { "authorize_url_timeout": "Skapandet av en auktoriseringsadress \u00f6verskred tidsgr\u00e4nsen.", "missing_configuration": "Spotify-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen.", - "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", + "reauth_account_mismatch": "Spotify-kontot som autentiserats med matchar inte kontot som kr\u00e4vs f\u00f6r omautentisering." }, "create_entry": { "default": "Lyckad autentisering med Spotify." @@ -11,6 +12,10 @@ "step": { "pick_implementation": { "title": "V\u00e4lj autentiseringsmetod." + }, + "reauth_confirm": { + "description": "Spotify-integrationen m\u00e5ste autentiseras p\u00e5 nytt med Spotify f\u00f6r konto: {account}", + "title": "\u00c5terautenticera integration" } } }, diff --git a/homeassistant/components/srp_energy/translations/sv.json b/homeassistant/components/srp_energy/translations/sv.json index b2068a4d12d..3f501296ccb 100644 --- a/homeassistant/components/srp_energy/translations/sv.json +++ b/homeassistant/components/srp_energy/translations/sv.json @@ -5,11 +5,15 @@ }, "error": { "cannot_connect": "Kan inte ansluta", + "invalid_account": "Konto-ID ska vara ett 9-siffrigt nummer", + "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "id": "Konto-id", + "is_tou": "\u00c4r plan f\u00f6r anv\u00e4ndningstid", "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } diff --git a/homeassistant/components/steamist/translations/sv.json b/homeassistant/components/steamist/translations/sv.json index c20adb3f64d..a37f088921f 100644 --- a/homeassistant/components/steamist/translations/sv.json +++ b/homeassistant/components/steamist/translations/sv.json @@ -1,17 +1,31 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta.", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "not_steamist_device": "Inte en steamist enhet" }, "error": { "cannot_connect": "Kunde inte ansluta", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{name} ({ipaddress})", "step": { + "discovery_confirm": { + "description": "Vill du konfigurera {name} ({ipaddress})?" + }, + "pick_device": { + "data": { + "device": "Enhet" + } + }, "user": { "data": { "host": "V\u00e4rd" - } + }, + "description": "Om du l\u00e4mnar v\u00e4rden tomt anv\u00e4nds discovery f\u00f6r att hitta enheter." } } } diff --git a/homeassistant/components/stookalert/translations/sv.json b/homeassistant/components/stookalert/translations/sv.json new file mode 100644 index 00000000000..505ccef6044 --- /dev/null +++ b/homeassistant/components/stookalert/translations/sv.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "step": { + "user": { + "data": { + "province": "Provins" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/subaru/translations/sv.json b/homeassistant/components/subaru/translations/sv.json index e89f29fc04a..69a01c5ea25 100644 --- a/homeassistant/components/subaru/translations/sv.json +++ b/homeassistant/components/subaru/translations/sv.json @@ -10,7 +10,8 @@ "cannot_connect": "Det gick inte att ansluta.", "incorrect_pin": "Felaktig PIN-kod", "incorrect_validation_code": "Felaktig valideringkod", - "invalid_auth": "Ogiltig autentisering" + "invalid_auth": "Ogiltig autentisering", + "two_factor_request_failed": "Beg\u00e4ran om 2FA-kod misslyckades, f\u00f6rs\u00f6k igen" }, "step": { "pin": { @@ -21,6 +22,9 @@ "title": "Subaru Starlink-konfiguration" }, "two_factor": { + "data": { + "contact_method": "V\u00e4lj en kontaktmetod:" + }, "description": "Tv\u00e5faktorautentisering kr\u00e4vs", "title": "Subaru Starlink-konfiguration" }, @@ -28,6 +32,7 @@ "data": { "validation_code": "Valideringskod" }, + "description": "Ange mottagen valideringskod", "title": "Subaru Starlink-konfiguration" }, "user": { diff --git a/homeassistant/components/sun/translations/sv.json b/homeassistant/components/sun/translations/sv.json index 7494630ac88..ffc1bacf65a 100644 --- a/homeassistant/components/sun/translations/sv.json +++ b/homeassistant/components/sun/translations/sv.json @@ -1,4 +1,14 @@ { + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du starta konfigurationen?" + } + } + }, "state": { "_": { "above_horizon": "Ovanf\u00f6r horisonten", diff --git a/homeassistant/components/surepetcare/translations/sv.json b/homeassistant/components/surepetcare/translations/sv.json index 23c825f256f..939b543adea 100644 --- a/homeassistant/components/surepetcare/translations/sv.json +++ b/homeassistant/components/surepetcare/translations/sv.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/switch/translations/sv.json b/homeassistant/components/switch/translations/sv.json index a2cd74434eb..6a87682ae5d 100644 --- a/homeassistant/components/switch/translations/sv.json +++ b/homeassistant/components/switch/translations/sv.json @@ -10,6 +10,7 @@ "is_on": "{entity_name} \u00e4r p\u00e5" }, "trigger_type": { + "changed_states": "{entity_name} slogs p\u00e5 eller av", "turned_off": "{entity_name} st\u00e4ngdes av", "turned_on": "{entity_name} slogs p\u00e5" } diff --git a/homeassistant/components/switch_as_x/translations/sv.json b/homeassistant/components/switch_as_x/translations/sv.json index d21eefa5a1a..95ea5abe410 100644 --- a/homeassistant/components/switch_as_x/translations/sv.json +++ b/homeassistant/components/switch_as_x/translations/sv.json @@ -3,8 +3,10 @@ "step": { "user": { "data": { + "entity_id": "Brytare", "target_domain": "Typ" - } + }, + "description": "V\u00e4lj en str\u00f6mbrytare som du vill visa i Home Assistant som lampa, skal eller n\u00e5got annat. Den ursprungliga switchen kommer att d\u00f6ljas." } } }, diff --git a/homeassistant/components/switchbot/translations/sv.json b/homeassistant/components/switchbot/translations/sv.json index f2c86841487..717b00d3d6c 100644 --- a/homeassistant/components/switchbot/translations/sv.json +++ b/homeassistant/components/switchbot/translations/sv.json @@ -2,6 +2,8 @@ "config": { "abort": { "already_configured_device": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "no_unconfigured_devices": "Inga okonfigurerade enheter hittades.", "switchbot_unsupported_type": "Switchbot-typ som inte st\u00f6ds.", "unknown": "Ov\u00e4ntat fel" }, diff --git a/homeassistant/components/syncthing/translations/sv.json b/homeassistant/components/syncthing/translations/sv.json index a77f97f91e7..1ba91de2f70 100644 --- a/homeassistant/components/syncthing/translations/sv.json +++ b/homeassistant/components/syncthing/translations/sv.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, "error": { + "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering" }, "step": { diff --git a/homeassistant/components/synology_dsm/translations/sv.json b/homeassistant/components/synology_dsm/translations/sv.json index 95a8af99f63..5813be31352 100644 --- a/homeassistant/components/synology_dsm/translations/sv.json +++ b/homeassistant/components/synology_dsm/translations/sv.json @@ -32,8 +32,10 @@ }, "reauth_confirm": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "Synology DSM \u00c5terautenticera integration" }, "user": { "data": { diff --git a/homeassistant/components/system_bridge/translations/sv.json b/homeassistant/components/system_bridge/translations/sv.json index 5fa635734cb..fdd4c4c0b67 100644 --- a/homeassistant/components/system_bridge/translations/sv.json +++ b/homeassistant/components/system_bridge/translations/sv.json @@ -1,15 +1,30 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name}", "step": { "authenticate": { "data": { "api_key": "API-nyckel" - } + }, + "description": "Ange API-nyckeln som du st\u00e4llt in i din konfiguration f\u00f6r {name} ." }, "user": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "host": "V\u00e4rd", + "port": "Port" + }, + "description": "Ange dina anslutningsuppgifter." } } } diff --git a/homeassistant/components/tag/translations/sv.json b/homeassistant/components/tag/translations/sv.json new file mode 100644 index 00000000000..421db629ef8 --- /dev/null +++ b/homeassistant/components/tag/translations/sv.json @@ -0,0 +1,3 @@ +{ + "title": "Tagg" +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/translations/sv.json b/homeassistant/components/tailscale/translations/sv.json index 5ad5b5b6db4..40780c30f16 100644 --- a/homeassistant/components/tailscale/translations/sv.json +++ b/homeassistant/components/tailscale/translations/sv.json @@ -1,15 +1,25 @@ { "config": { + "abort": { + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "reauth_confirm": { "data": { "api_key": "API-nyckel" - } + }, + "description": "Tailscale API-tokens \u00e4r giltiga i 90 dagar. Du kan skapa en ny Tailscale API-nyckel p\u00e5 https://login.tailscale.com/admin/settings/authkeys." }, "user": { "data": { - "api_key": "API-nyckel" - } + "api_key": "API-nyckel", + "tailnet": "Tailnet" + }, + "description": "Denna integration \u00f6vervakar ditt Tailscale-n\u00e4tverk, den **G\u00d6R INTE** din Home Assistant tillg\u00e4nglig via Tailscale VPN. \n\n F\u00f6r att autentisera med Tailscale m\u00e5ste du skapa en API-nyckel p\u00e5 {authkeys_url} . \n\n Ett Tailnet \u00e4r namnet p\u00e5 ditt Tailscale-n\u00e4tverk. Du hittar den i det \u00f6vre v\u00e4nstra h\u00f6rnet i Tailscale Admin Panel (bredvid Tailscale-logotypen)." } } } diff --git a/homeassistant/components/tellduslive/translations/sv.json b/homeassistant/components/tellduslive/translations/sv.json index 25d33f3afdf..709100f1b57 100644 --- a/homeassistant/components/tellduslive/translations/sv.json +++ b/homeassistant/components/tellduslive/translations/sv.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", "authorize_url_timeout": "Timeout n\u00e4r genererar auktorisera url.", - "unknown": "Ok\u00e4nt fel intr\u00e4ffade" + "unknown": "Ok\u00e4nt fel intr\u00e4ffade", + "unknown_authorize_url_generation": "Ok\u00e4nt fel vid generering av en auktoriserad URL." }, "error": { "invalid_auth": "Ogiltig autentisering" diff --git a/homeassistant/components/tesla_wall_connector/translations/sv.json b/homeassistant/components/tesla_wall_connector/translations/sv.json new file mode 100644 index 00000000000..099c5aecafb --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{serial_number} ({host})", + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + }, + "title": "Konfigurera Tesla Wall Connector" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/threshold/translations/sv.json b/homeassistant/components/threshold/translations/sv.json index dd247031a4e..aaef886e1a6 100644 --- a/homeassistant/components/threshold/translations/sv.json +++ b/homeassistant/components/threshold/translations/sv.json @@ -5,16 +5,32 @@ }, "step": { "user": { + "data": { + "entity_id": "Ing\u00e5ngssensor", + "hysteresis": "Hysteres", + "lower": "L\u00e4gre gr\u00e4ns", + "name": "Namn", + "upper": "\u00d6vre gr\u00e4ns" + }, + "description": "Skapa en bin\u00e4r sensor som sl\u00e5s p\u00e5 och av beroende p\u00e5 v\u00e4rdet p\u00e5 en sensor \n\n Endast nedre gr\u00e4ns konfigurerad - Sl\u00e5 p\u00e5 n\u00e4r ing\u00e5ngssensorns v\u00e4rde \u00e4r mindre \u00e4n den nedre gr\u00e4nsen.\n Endast \u00f6vre gr\u00e4ns konfigurerad - Sl\u00e5 p\u00e5 n\u00e4r ing\u00e5ngssensorns v\u00e4rde \u00e4r st\u00f6rre \u00e4n den \u00f6vre gr\u00e4nsen.\n B\u00e5de undre och \u00f6vre gr\u00e4ns konfigurerad - Sl\u00e5 p\u00e5 n\u00e4r ing\u00e5ngssensorns v\u00e4rde \u00e4r inom omr\u00e5det [nedre gr\u00e4ns .. \u00f6vre gr\u00e4ns].", "title": "L\u00e4gg till gr\u00e4nsv\u00e4rdessensor" } } }, "options": { + "error": { + "need_lower_upper": "Undre och \u00f6vre gr\u00e4ns kan inte vara tomma" + }, "step": { "init": { "data": { - "lower": "Undre gr\u00e4ns" - } + "entity_id": "Ing\u00e5ngssensor", + "hysteresis": "Hysteres", + "lower": "Undre gr\u00e4ns", + "name": "Namn", + "upper": "\u00d6vre gr\u00e4ns" + }, + "description": "Endast nedre gr\u00e4ns konfigurerad - Sl\u00e5 p\u00e5 n\u00e4r ing\u00e5ngssensorns v\u00e4rde \u00e4r mindre \u00e4n den nedre gr\u00e4nsen.\n Endast \u00f6vre gr\u00e4ns konfigurerad - Sl\u00e5 p\u00e5 n\u00e4r ing\u00e5ngssensorns v\u00e4rde \u00e4r st\u00f6rre \u00e4n den \u00f6vre gr\u00e4nsen.\n B\u00e5de undre och \u00f6vre gr\u00e4ns konfigurerad - Sl\u00e5 p\u00e5 n\u00e4r ing\u00e5ngssensorns v\u00e4rde \u00e4r inom omr\u00e5det [nedre gr\u00e4ns .. \u00f6vre gr\u00e4ns]." } } }, diff --git a/homeassistant/components/tile/translations/sv.json b/homeassistant/components/tile/translations/sv.json index b3a1bc0e169..e9a1392a842 100644 --- a/homeassistant/components/tile/translations/sv.json +++ b/homeassistant/components/tile/translations/sv.json @@ -1,9 +1,19 @@ { "config": { "abort": { - "already_configured": "Konto har redan konfigurerats" + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "invalid_auth": "Ogiltig autentisering" }, "step": { + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "title": "Autentisera panelen igen" + }, "user": { "data": { "password": "L\u00f6senord", diff --git a/homeassistant/components/tod/translations/sv.json b/homeassistant/components/tod/translations/sv.json index 79e74cf3935..4441f7ed5da 100644 --- a/homeassistant/components/tod/translations/sv.json +++ b/homeassistant/components/tod/translations/sv.json @@ -1,3 +1,26 @@ { + "config": { + "step": { + "user": { + "data": { + "after_time": "P\u00e5 tid", + "before_time": "Av tid", + "name": "Namn" + }, + "description": "Skapa en bin\u00e4r sensor som sl\u00e5s p\u00e5 eller av beroende p\u00e5 tiden.", + "title": "L\u00e4gg till sensorer f\u00f6r tider p\u00e5 dagen" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "after_time": "P\u00e5 tid", + "before_time": "Av tid" + } + } + } + }, "title": "Tider p\u00e5 dagen sensor" } \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/select.sv.json b/homeassistant/components/tolo/translations/select.sv.json new file mode 100644 index 00000000000..cab1d17ca34 --- /dev/null +++ b/homeassistant/components/tolo/translations/select.sv.json @@ -0,0 +1,8 @@ +{ + "state": { + "tolo__lamp_mode": { + "automatic": "Automatisk", + "manual": "Manuell" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/sv.json b/homeassistant/components/tolo/translations/sv.json new file mode 100644 index 00000000000..415e3da5dbf --- /dev/null +++ b/homeassistant/components/tolo/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "Vill du starta konfigurationen?" + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Ange v\u00e4rdnamnet eller IP-adressen f\u00f6r din TOLO Sauna-enhet." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sensor.sv.json b/homeassistant/components/tomorrowio/translations/sensor.sv.json new file mode 100644 index 00000000000..04886f124dd --- /dev/null +++ b/homeassistant/components/tomorrowio/translations/sensor.sv.json @@ -0,0 +1,27 @@ +{ + "state": { + "tomorrowio__health_concern": { + "good": "Bra", + "hazardous": "Farlig", + "moderate": "M\u00e5ttlig", + "unhealthy": "Oh\u00e4lsosam", + "unhealthy_for_sensitive_groups": "Oh\u00e4lsosamt f\u00f6r k\u00e4nsliga grupper", + "very_unhealthy": "Mycket oh\u00e4lsosamt" + }, + "tomorrowio__pollen_index": { + "high": "H\u00f6gt", + "low": "L\u00e5g", + "medium": "Medium", + "none": "Inget", + "very_high": "V\u00e4ldigt h\u00f6gt", + "very_low": "V\u00e4ldigt l\u00e5gt" + }, + "tomorrowio__precipitation_type": { + "freezing_rain": "Underkylt regn", + "ice_pellets": "Hagel", + "none": "Inget", + "rain": "Regn", + "snow": "Sn\u00f6" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/translations/sv.json b/homeassistant/components/tomorrowio/translations/sv.json index 15498795844..64851f5d0ae 100644 --- a/homeassistant/components/tomorrowio/translations/sv.json +++ b/homeassistant/components/tomorrowio/translations/sv.json @@ -1,11 +1,30 @@ { "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_api_key": "Ogiltig API-nyckel", + "rate_limited": "F\u00f6r n\u00e4rvarande \u00e4r hastigheten begr\u00e4nsad, v\u00e4nligen f\u00f6rs\u00f6k igen senare.", + "unknown": "Ov\u00e4ntat fel" + }, "step": { "user": { "data": { "api_key": "API-nyckel", - "location": "Plats" - } + "location": "Plats", + "name": "Namn" + }, + "description": "F\u00f6r att f\u00e5 en API-nyckel, registrera dig p\u00e5 [Tomorrow.io](https://app.tomorrow.io/signup)." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timestep": "Minuter mellan NowCast-prognoser" + }, + "description": "Om du v\u00e4ljer att aktivera \"nowcast\"-prognosentiteten kan du konfigurera antalet minuter mellan varje prognos. Antalet prognoser som tillhandah\u00e5lls beror p\u00e5 antalet minuter som v\u00e4ljs mellan prognoserna.", + "title": "Uppdatera Tomorrow.io-alternativen" } } } diff --git a/homeassistant/components/toon/translations/sv.json b/homeassistant/components/toon/translations/sv.json index 95864e9bbe3..fd177e7bcfc 100644 --- a/homeassistant/components/toon/translations/sv.json +++ b/homeassistant/components/toon/translations/sv.json @@ -5,7 +5,8 @@ "authorize_url_timeout": "Timeout vid generering av en auktoriserings-URL.", "missing_configuration": "Komponenten har inte konfigurerats. F\u00f6lj dokumentationen.", "no_agreements": "Det h\u00e4r kontot har inga Toon-sk\u00e4rmar.", - "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})" + "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", + "unknown_authorize_url_generation": "Ok\u00e4nt fel vid generering av en auktoriserad URL." }, "step": { "agreement": { diff --git a/homeassistant/components/totalconnect/translations/sv.json b/homeassistant/components/totalconnect/translations/sv.json index fb9e0feb4e0..b06dc765a4b 100644 --- a/homeassistant/components/totalconnect/translations/sv.json +++ b/homeassistant/components/totalconnect/translations/sv.json @@ -11,6 +11,9 @@ }, "step": { "locations": { + "data": { + "usercode": "Anv\u00e4ndarkod" + }, "description": "Ange anv\u00e4ndarkoden f\u00f6r denna anv\u00e4ndare p\u00e5 plats {location_id}", "title": "Anv\u00e4ndarkoder f\u00f6r plats" }, diff --git a/homeassistant/components/tplink/translations/sv.json b/homeassistant/components/tplink/translations/sv.json index 923647633d9..2bcc7d3137a 100644 --- a/homeassistant/components/tplink/translations/sv.json +++ b/homeassistant/components/tplink/translations/sv.json @@ -16,6 +16,12 @@ "data": { "device": "Enhet" } + }, + "user": { + "data": { + "host": "V\u00e4rd" + }, + "description": "Om du l\u00e4mnar v\u00e4rden tomt anv\u00e4nds discovery f\u00f6r att hitta enheter." } } } diff --git a/homeassistant/components/traccar/translations/sv.json b/homeassistant/components/traccar/translations/sv.json index b6858100aa4..0b35f9aab56 100644 --- a/homeassistant/components/traccar/translations/sv.json +++ b/homeassistant/components/traccar/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." }, diff --git a/homeassistant/components/tractive/translations/sensor.sv.json b/homeassistant/components/tractive/translations/sensor.sv.json new file mode 100644 index 00000000000..7fde66fac96 --- /dev/null +++ b/homeassistant/components/tractive/translations/sensor.sv.json @@ -0,0 +1,10 @@ +{ + "state": { + "tractive__tracker_state": { + "not_reporting": "Rapporterar inte", + "operational": "Operativ", + "system_shutdown_user": "Anv\u00e4ndare av systemavst\u00e4ngning", + "system_startup": "Systemstart" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/translations/sv.json b/homeassistant/components/tradfri/translations/sv.json index 69d7f7ba3c5..610cc12014c 100644 --- a/homeassistant/components/tradfri/translations/sv.json +++ b/homeassistant/components/tradfri/translations/sv.json @@ -5,6 +5,7 @@ "already_in_progress": "Konfigurations fl\u00f6det f\u00f6r bryggan p\u00e5g\u00e5r redan." }, "error": { + "cannot_authenticate": "Det g\u00e5r inte att autentisera, \u00e4r Gateway kopplad till en annan server, t.ex. Homekit?", "cannot_connect": "Det gick inte att ansluta till gatewayen.", "invalid_key": "Misslyckades med att registrera den angivna nyckeln. Om det h\u00e4r h\u00e4nder, f\u00f6rs\u00f6k starta om gatewayen igen.", "timeout": "Timeout vid valididering av kod" diff --git a/homeassistant/components/trafikverket_weatherstation/translations/sv.json b/homeassistant/components/trafikverket_weatherstation/translations/sv.json index f4a63bb449d..f1a5b2dfb41 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/sv.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/sv.json @@ -1,9 +1,19 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "invalid_station": "Det gick inte att hitta en v\u00e4derstation med det angivna namnet", + "more_stations": "Hittade flera v\u00e4derstationer med det angivna namnet" + }, "step": { "user": { "data": { - "api_key": "API-nyckel" + "api_key": "API-nyckel", + "station": "Station" } } } diff --git a/homeassistant/components/transmission/translations/sv.json b/homeassistant/components/transmission/translations/sv.json index 79b40b78ff5..859d042dbbc 100644 --- a/homeassistant/components/transmission/translations/sv.json +++ b/homeassistant/components/transmission/translations/sv.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "Det g\u00e5r inte att ansluta till v\u00e4rden", + "invalid_auth": "Ogiltig autentisering", "name_exists": "Namnet finns redan" }, "step": { diff --git a/homeassistant/components/tuya/translations/select.sv.json b/homeassistant/components/tuya/translations/select.sv.json index c34c40ebacd..ec08db5924b 100644 --- a/homeassistant/components/tuya/translations/select.sv.json +++ b/homeassistant/components/tuya/translations/select.sv.json @@ -1,7 +1,74 @@ { "state": { + "tuya__basic_anti_flickr": { + "0": "Inaktiverad", + "1": "50 Hz", + "2": "60 Hz" + }, + "tuya__basic_nightvision": { + "0": "Automatisk", + "1": "Av", + "2": "P\u00e5" + }, + "tuya__countdown": { + "1h": "1 timme", + "2h": "2 timmar", + "3h": "3 timmar", + "4h": "4 timmar", + "5h": "5 timmar", + "6h": "6 timmar", + "cancel": "Avbryt" + }, + "tuya__curtain_mode": { + "morning": "Morgon", + "night": "Natt" + }, + "tuya__curtain_motor_mode": { + "back": "Tillbaka", + "forward": "Fram\u00e5t" + }, + "tuya__decibel_sensitivity": { + "0": "L\u00e5g k\u00e4nslighet", + "1": "H\u00f6g k\u00e4nslighet" + }, + "tuya__fan_angle": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + }, + "tuya__fingerbot_mode": { + "click": "Tryck", + "switch": "Knapp" + }, + "tuya__humidifier_level": { + "level_1": "Niv\u00e5 1", + "level_10": "Niv\u00e5 10", + "level_2": "Niv\u00e5 2", + "level_3": "Niv\u00e5 3", + "level_4": "Niv\u00e5 4", + "level_5": "Niv\u00e5 5", + "level_6": "Niv\u00e5 6", + "level_7": "Niv\u00e5 7", + "level_8": "Niv\u00e5 8", + "level_9": "Niv\u00e5 9" + }, + "tuya__humidifier_moodlighting": { + "1": "Hum\u00f6r 1", + "2": "Hum\u00f6r 2", + "3": "Hum\u00f6r 3", + "4": "Hum\u00f6r 4", + "5": "Hum\u00f6r 5" + }, "tuya__humidifier_spray_mode": { - "humidity": "Luftfuktighet" + "auto": "Automatisk", + "health": "H\u00e4lsa", + "humidity": "Luftfuktighet", + "sleep": "S\u00f6mnl\u00e4ge", + "work": "Arbetsl\u00e4ge" + }, + "tuya__ipc_work_mode": { + "0": "L\u00e5geffektl\u00e4ge", + "1": "Kontinuerligt arbetsl\u00e4ge" }, "tuya__led_type": { "halogen": "Halogen", @@ -13,6 +80,15 @@ "pos": "Ange omkopplarens plats", "relay": "Indikera p\u00e5/av-l\u00e4ge" }, + "tuya__motion_sensitivity": { + "0": "L\u00e5g k\u00e4nslighet", + "1": "Medium k\u00e4nslighet", + "2": "H\u00f6g k\u00e4nslighet" + }, + "tuya__record_mode": { + "1": "Spela bara in h\u00e4ndelser", + "2": "Kontinuerlig inspelning" + }, "tuya__relay_status": { "last": "Kom ih\u00e5g senaste tillst\u00e5ndet", "memory": "Kom ih\u00e5g senaste tillst\u00e5ndet", @@ -21,8 +97,37 @@ "power_off": "Av", "power_on": "P\u00e5" }, + "tuya__vacuum_cistern": { + "closed": "St\u00e4ngd", + "high": "H\u00f6gt", + "low": "L\u00e5gt", + "middle": "Mitten" + }, + "tuya__vacuum_collection": { + "large": "Stor", + "middle": "Mitten", + "small": "Liten" + }, "tuya__vacuum_mode": { - "standby": "Standby" + "bow": "B\u00e5ge", + "chargego": "\u00c5terg\u00e5 till dockan", + "left_bow": "B\u00e5ge v\u00e4nster", + "left_spiral": "Spiral v\u00e4nster", + "mop": "Moppa", + "part": "Del", + "partial_bow": "B\u00e5ge delvis", + "pick_zone": "V\u00e4lj zon", + "point": "Punkt", + "pose": "Pose", + "random": "Slumpm\u00e4ssig", + "right_bow": "B\u00e5ge \u00e5t h\u00f6ger", + "right_spiral": "Spiral h\u00f6ger", + "single": "Enkel", + "smart": "Smart", + "spiral": "Spiral", + "standby": "Standby", + "wall_follow": "F\u00f6lj v\u00e4ggen", + "zone": "Zon" } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/sensor.sv.json b/homeassistant/components/tuya/translations/sensor.sv.json index a3dd5ff28df..8bc39f828d5 100644 --- a/homeassistant/components/tuya/translations/sensor.sv.json +++ b/homeassistant/components/tuya/translations/sensor.sv.json @@ -1,5 +1,11 @@ { "state": { + "tuya__air_quality": { + "good": "Bra", + "great": "Mycket bra", + "mild": "Mild", + "severe": "Allvarlig" + }, "tuya__status": { "boiling_temp": "Koktemperatur", "cooling": "Kyler", diff --git a/homeassistant/components/tuya/translations/sv.json b/homeassistant/components/tuya/translations/sv.json index 1add4cb2e2d..665f781d726 100644 --- a/homeassistant/components/tuya/translations/sv.json +++ b/homeassistant/components/tuya/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "error": { + "invalid_auth": "Ogiltig autentisering", "login_error": "Inloggningsfel ( {code} ): {msg}" }, "step": { diff --git a/homeassistant/components/twilio/translations/sv.json b/homeassistant/components/twilio/translations/sv.json index 8bb70f9cd64..c4e94b50741 100644 --- a/homeassistant/components/twilio/translations/sv.json +++ b/homeassistant/components/twilio/translations/sv.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cloud_not_connected": "Ej ansluten till Home Assistant Cloud.", "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", "webhook_not_internet_accessible": "Din Home Assistant-instans m\u00e5ste vara tillg\u00e4nglig fr\u00e5n internet f\u00f6r att ta emot webhook-meddelanden." }, diff --git a/homeassistant/components/twinkly/translations/sv.json b/homeassistant/components/twinkly/translations/sv.json index 9b541d7ceac..e997d3520bf 100644 --- a/homeassistant/components/twinkly/translations/sv.json +++ b/homeassistant/components/twinkly/translations/sv.json @@ -7,6 +7,9 @@ "cannot_connect": "Det gick inte att ansluta." }, "step": { + "discovery_confirm": { + "description": "Vill du konfigurera {name} - {model} ({host})?" + }, "user": { "data": { "host": "V\u00e4rd" diff --git a/homeassistant/components/ukraine_alarm/translations/sv.json b/homeassistant/components/ukraine_alarm/translations/sv.json index 3e8c6255251..433b8d25211 100644 --- a/homeassistant/components/ukraine_alarm/translations/sv.json +++ b/homeassistant/components/ukraine_alarm/translations/sv.json @@ -5,6 +5,7 @@ "cannot_connect": "Det gick inte att ansluta.", "max_regions": "Max 5 regioner kan konfigureras", "rate_limit": "F\u00f6r mycket f\u00f6rfr\u00e5gningar", + "timeout": "Timeout uppr\u00e4ttar anslutning", "unknown": "Ov\u00e4ntat fel" }, "step": { diff --git a/homeassistant/components/unifi/translations/sv.json b/homeassistant/components/unifi/translations/sv.json index cdc66ea5126..8ab224c7ab7 100644 --- a/homeassistant/components/unifi/translations/sv.json +++ b/homeassistant/components/unifi/translations/sv.json @@ -67,7 +67,8 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Skapa bandbreddsanv\u00e4ndningssensorer f\u00f6r n\u00e4tverksklienter" + "allow_bandwidth_sensors": "Skapa bandbreddsanv\u00e4ndningssensorer f\u00f6r n\u00e4tverksklienter", + "allow_uptime_sensors": "Upptidssensorer f\u00f6r n\u00e4tverksklienter" }, "description": "Konfigurera statistiksensorer", "title": "UniFi-inst\u00e4llningar 2/3" diff --git a/homeassistant/components/unifiprotect/translations/sv.json b/homeassistant/components/unifiprotect/translations/sv.json index 702dcbecdb7..81afcc7c14b 100644 --- a/homeassistant/components/unifiprotect/translations/sv.json +++ b/homeassistant/components/unifiprotect/translations/sv.json @@ -1,22 +1,56 @@ { "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "discovery_started": "Uppt\u00e4ckten startades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "protect_version": "Minsta n\u00f6dv\u00e4ndiga version \u00e4r v1.20.0. Uppgradera UniFi Protect och f\u00f6rs\u00f6k sedan igen." + }, + "flow_title": "{name} ({ip_address})", "step": { "discovery_confirm": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" }, + "description": "Vill du konfigurera {name} ({ip_address})? Du beh\u00f6ver en lokal anv\u00e4ndare skapad i din UniFi OS-konsol f\u00f6r att logga in med. Ubiquiti Cloud-anv\u00e4ndare kommer inte att fungera. F\u00f6r mer information: {local_user_documentation_url}", "title": "UniFi Protect uppt\u00e4ckt" }, "reauth_confirm": { "data": { + "host": "IP/v\u00e4rd f\u00f6r UniFi Protect Server", + "password": "L\u00f6senord", + "port": "Port", "username": "Anv\u00e4ndarnamn" - } + }, + "title": "UniFi Protect Reauth" }, "user": { "data": { - "username": "Anv\u00e4ndarnamn" + "host": "V\u00e4rd", + "password": "L\u00f6senord", + "port": "Port", + "username": "Anv\u00e4ndarnamn", + "verify_ssl": "Verifiera SSL-certifikat" }, - "description": "Du beh\u00f6ver en lokal anv\u00e4ndare skapad i din UniFi OS-konsol f\u00f6r att logga in med. Ubiquiti Cloud-anv\u00e4ndare kommer inte att fungera. F\u00f6r mer information: {local_user_documentation_url}" + "description": "Du beh\u00f6ver en lokal anv\u00e4ndare skapad i din UniFi OS-konsol f\u00f6r att logga in med. Ubiquiti Cloud-anv\u00e4ndare kommer inte att fungera. F\u00f6r mer information: {local_user_documentation_url}", + "title": "Installation av UniFi Protect" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "all_updates": "Realtidsm\u00e4tningar (VARNING: \u00d6kar CPU-anv\u00e4ndningen avsev\u00e4rt)", + "disable_rtsp": "Inaktivera RTSP-str\u00f6mmen", + "override_connection_host": "\u00c5sidos\u00e4tt anslutningsv\u00e4rd" + }, + "description": "Alternativet Realtidsm\u00e4tv\u00e4rden b\u00f6r endast aktiveras om du har aktiverat diagnostiksensorerna och vill att de ska uppdateras i realtid. Om det inte \u00e4r aktiverat kommer de bara att uppdateras en g\u00e5ng var 15:e minut.", + "title": "UniFi Protect-alternativ" } } } diff --git a/homeassistant/components/update/translations/sv.json b/homeassistant/components/update/translations/sv.json index 53ecdb21377..c68c6d0f1c3 100644 --- a/homeassistant/components/update/translations/sv.json +++ b/homeassistant/components/update/translations/sv.json @@ -5,5 +5,6 @@ "turned_off": "{entity_name} blev uppdaterad", "turned_on": "{entity_name} har en uppdatering tillg\u00e4nglig" } - } + }, + "title": "Uppdatera" } \ No newline at end of file diff --git a/homeassistant/components/uptime/translations/sv.json b/homeassistant/components/uptime/translations/sv.json new file mode 100644 index 00000000000..0d9e03ec575 --- /dev/null +++ b/homeassistant/components/uptime/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "user": { + "description": "Vill du starta konfigurationen?" + } + } + }, + "title": "Upptid" +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sensor.sv.json b/homeassistant/components/uptimerobot/translations/sensor.sv.json new file mode 100644 index 00000000000..b0d99120a3c --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.sv.json @@ -0,0 +1,11 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "down": "Nere", + "not_checked_yet": "Inte kontrollerat \u00e4n", + "pause": "Pausa", + "seems_down": "Verkar nere", + "up": "Uppe" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sv.json b/homeassistant/components/uptimerobot/translations/sv.json index d9db1fc1deb..b0bb7b6e72a 100644 --- a/homeassistant/components/uptimerobot/translations/sv.json +++ b/homeassistant/components/uptimerobot/translations/sv.json @@ -9,6 +9,7 @@ "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_api_key": "Ogiltig API-nyckel", + "not_main_key": "Fel API-nyckeltyp uppt\u00e4ckt, anv\u00e4nd \"huvud\" API-nyckeln", "reauth_failed_matching_account": "API-nyckeln du angav matchar inte konto-id:t f\u00f6r befintlig konfiguration.", "unknown": "Ov\u00e4ntat fel" }, diff --git a/homeassistant/components/vallox/translations/sv.json b/homeassistant/components/vallox/translations/sv.json new file mode 100644 index 00000000000..b3e55ae100f --- /dev/null +++ b/homeassistant/components/vallox/translations/sv.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_host": "Ogiltigt v\u00e4rdnamn eller IP-adress", + "unknown": "Ov\u00e4ntat fel" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/translations/sv.json b/homeassistant/components/vera/translations/sv.json index 8c7335c2b13..5e28c1db392 100644 --- a/homeassistant/components/vera/translations/sv.json +++ b/homeassistant/components/vera/translations/sv.json @@ -9,6 +9,9 @@ "exclude": "Vera enhets-id att utesluta fr\u00e5n Home Assistant.", "lights": "Vera switch enhets-ID att behandla som lampor i Home Assistant.", "vera_controller_url": "Controller-URL" + }, + "data_description": { + "vera_controller_url": "Det ska se ut s\u00e5 h\u00e4r: http://192.168.1.161:3480" } } } diff --git a/homeassistant/components/version/translations/sv.json b/homeassistant/components/version/translations/sv.json new file mode 100644 index 00000000000..3db6f23dbfc --- /dev/null +++ b/homeassistant/components/version/translations/sv.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad" + }, + "step": { + "user": { + "data": { + "version_source": "Versionsk\u00e4lla" + }, + "description": "V\u00e4lj k\u00e4llan du vill sp\u00e5ra versioner fr\u00e5n", + "title": "V\u00e4lj installationstyp" + }, + "version_source": { + "data": { + "beta": "Inkludera betaversioner", + "board": "Vilket kort som ska sp\u00e5ras", + "channel": "Vilken kanal ska sp\u00e5ras", + "image": "Vilken bild ska sp\u00e5ras" + }, + "description": "Konfigurera {version_source} versionssp\u00e5rning", + "title": "Konfigurera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/translations/sv.json b/homeassistant/components/vicare/translations/sv.json index 80588906063..cd9c40f60c1 100644 --- a/homeassistant/components/vicare/translations/sv.json +++ b/homeassistant/components/vicare/translations/sv.json @@ -1,11 +1,22 @@ { "config": { + "abort": { + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig.", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { + "invalid_auth": "Ogiltig autentisering" + }, + "flow_title": "{name} ({host})", "step": { "user": { "data": { "client_id": "API-nyckel", + "heating_type": "V\u00e4rmel\u00e4ge", + "password": "L\u00f6senord", "username": "E-postadress" - } + }, + "description": "Konfigurera ViCare-integration. F\u00f6r att generera API-nyckel g\u00e5 till https://developer.viessmann.com" } } } diff --git a/homeassistant/components/vizio/translations/sv.json b/homeassistant/components/vizio/translations/sv.json index dc1f5514d80..62e74a67ed1 100644 --- a/homeassistant/components/vizio/translations/sv.json +++ b/homeassistant/components/vizio/translations/sv.json @@ -7,6 +7,7 @@ }, "error": { "cannot_connect": "Det gick inte att ansluta.", + "complete_pairing_failed": "Det gick inte att slutf\u00f6ra ihopkopplingen. Se till att PIN-koden du angav \u00e4r korrekt och att TV:n fortfarande \u00e4r str\u00f6msatt och ansluten till n\u00e4tverket innan du skickar in igen.", "existing_config_entry_found": "En befintlig St\u00e4ll in Vizio SmartCast-klient konfigurationspost med samma serienummer har redan konfigurerats. Du m\u00e5ste ta bort den befintliga posten f\u00f6r att konfigurera denna." }, "step": { diff --git a/homeassistant/components/vlc_telnet/translations/sv.json b/homeassistant/components/vlc_telnet/translations/sv.json index f8a77ff3086..bc13817a21a 100644 --- a/homeassistant/components/vlc_telnet/translations/sv.json +++ b/homeassistant/components/vlc_telnet/translations/sv.json @@ -1,17 +1,34 @@ { "config": { "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "reauth_successful": "\u00c5terautentisering lyckades", + "unknown": "Ov\u00e4ntat fel" + }, + "error": { "cannot_connect": "Det gick inte att ansluta.", "invalid_auth": "Ogiltig autentisering", "unknown": "Ov\u00e4ntat fel" }, + "flow_title": "{host}", "step": { "hassio_confirm": { "description": "Vill du ansluta till till\u00e4gget {addon} ?" }, + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Ange r\u00e4tt l\u00f6senord f\u00f6r v\u00e4rden: {host}" + }, "user": { "data": { - "host": "V\u00e4rd" + "host": "V\u00e4rd", + "name": "Namn", + "password": "L\u00f6senord", + "port": "Port" } } } diff --git a/homeassistant/components/vulcan/translations/id.json b/homeassistant/components/vulcan/translations/id.json index 6aa1e0d7fa6..eab39d53811 100644 --- a/homeassistant/components/vulcan/translations/id.json +++ b/homeassistant/components/vulcan/translations/id.json @@ -3,7 +3,7 @@ "abort": { "all_student_already_configured": "Semua siswa telah ditambahkan.", "already_configured": "Siswa tersebut telah ditambahkan.", - "no_matching_entries": "Tidak ditemukan entri yang cocok, harap gunakan akun lain atau hapus integrasi dengan siswa yang ketinggalan zaman..", + "no_matching_entries": "Tidak ditemukan entri yang cocok. Gunakan akun lain atau hapus integrasi dengan data siswa yang usang.", "reauth_successful": "Otorisasi ulang berhasil" }, "error": { diff --git a/homeassistant/components/vulcan/translations/sv.json b/homeassistant/components/vulcan/translations/sv.json index cbe2d7174b1..29bedcf709c 100644 --- a/homeassistant/components/vulcan/translations/sv.json +++ b/homeassistant/components/vulcan/translations/sv.json @@ -7,6 +7,8 @@ "reauth_successful": "Reautentisering lyckades" }, "error": { + "cannot_connect": "Anslutningsfel - kontrollera din internetanslutning", + "expired_credentials": "Utg\u00e5ngna uppgifter - v\u00e4nligen skapa nya p\u00e5 Vulcans mobilapps registreringssida", "expired_token": "Token som har upph\u00f6rt att g\u00e4lla \u2013 generera en ny token", "invalid_pin": "Ogiltig pin", "invalid_symbol": "Ogiltig symbol", diff --git a/homeassistant/components/watttime/translations/sv.json b/homeassistant/components/watttime/translations/sv.json index f8a3d0b7fcf..6df3193bbea 100644 --- a/homeassistant/components/watttime/translations/sv.json +++ b/homeassistant/components/watttime/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "invalid_auth": "Ogiltig autentisering", @@ -13,7 +14,8 @@ "data": { "latitude": "Latitud", "longitude": "Longitud" - } + }, + "description": "Ange latitud och longitud f\u00f6r att \u00f6vervaka:" }, "location": { "data": { @@ -21,6 +23,13 @@ }, "description": "V\u00e4lj en plats att \u00f6vervaka:" }, + "reauth_confirm": { + "data": { + "password": "L\u00f6senord" + }, + "description": "Ange l\u00f6senordet f\u00f6r {anv\u00e4ndarnamn} p\u00e5 nytt:", + "title": "\u00c5terautenticera integration" + }, "user": { "data": { "password": "L\u00f6senord", diff --git a/homeassistant/components/waze_travel_time/translations/sv.json b/homeassistant/components/waze_travel_time/translations/sv.json index 05407519a87..4b3da3f69d8 100644 --- a/homeassistant/components/waze_travel_time/translations/sv.json +++ b/homeassistant/components/waze_travel_time/translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Platsen \u00e4r redan konfigurerad" + }, "error": { "cannot_connect": "Kunde inte ansluta" }, @@ -10,7 +13,8 @@ "name": "Namn", "origin": "Ursprung", "region": "Region" - } + }, + "description": "F\u00f6r Ursprung och Destination anger du adressen eller GPS-koordinaterna f\u00f6r platsen (GPS-koordinaterna m\u00e5ste avgr\u00e4nsas med kommatecken). Du kan ocks\u00e5 ange ett enhets-id som tillhandah\u00e5ller denna information i dess tillst\u00e5nd, ett enhets-ID med latitud- och longitudattribut eller zonv\u00e4nligt namn." } } }, @@ -18,10 +22,18 @@ "step": { "init": { "data": { + "avoid_ferries": "Undvik f\u00e4rjor?", + "avoid_subscription_roads": "Undvik v\u00e4gar som beh\u00f6ver en vinjett / prenumeration?", + "avoid_toll_roads": "Undvika avgiftsbelagda v\u00e4gar?", + "excl_filter": "Delstr\u00e4ng INTE i beskrivning av vald rutt", + "incl_filter": "Delstr\u00e4ng i beskrivningen av den valda rutten", + "realtime": "Restid i realtid?", "units": "Enheter", "vehicle_type": "Fordonstyp" - } + }, + "description": "Med \"delstr\u00e4ng\"-ing\u00e5ngarna kan du tvinga integrationen att anv\u00e4nda en viss rutt eller undvika en viss rutt i dess tidsreseber\u00e4kning." } } - } + }, + "title": "Waze restid" } \ No newline at end of file diff --git a/homeassistant/components/webostv/translations/sv.json b/homeassistant/components/webostv/translations/sv.json new file mode 100644 index 00000000000..dcd01faf6a8 --- /dev/null +++ b/homeassistant/components/webostv/translations/sv.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "error_pairing": "Ansluten till LG webOS TV men inte ihopkopplad" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta. Sl\u00e5 p\u00e5 din TV eller kontrollera ip-adressen" + }, + "flow_title": "LG webOS Smart TV", + "step": { + "pairing": { + "description": "Klicka p\u00e5 skicka och acceptera parningsf\u00f6rfr\u00e5gan p\u00e5 din TV. \n\n ![Image](/static/images/config_webos.png)", + "title": "Koppling av webOS TV" + }, + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn" + }, + "description": "Sl\u00e5 p\u00e5 TV:n, fyll i f\u00f6ljande f\u00e4lt och klicka p\u00e5 skicka.", + "title": "Anslut till webOS TV" + } + } + }, + "device_automation": { + "trigger_type": { + "webostv.turn_on": "Enheten uppmanas att sl\u00e5s p\u00e5" + } + }, + "options": { + "error": { + "cannot_retrieve": "Det gick inte att h\u00e4mta k\u00e4lllistan. Se till att enheten \u00e4r p\u00e5slagen", + "script_not_found": "Skriptet hittades inte" + }, + "step": { + "init": { + "data": { + "sources": "K\u00e4lllista" + }, + "description": "V\u00e4lj aktiverade k\u00e4llor", + "title": "Alternativ f\u00f6r webOS Smart TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/sv.json b/homeassistant/components/whirlpool/translations/sv.json index f7461922566..72e401bd4f0 100644 --- a/homeassistant/components/whirlpool/translations/sv.json +++ b/homeassistant/components/whirlpool/translations/sv.json @@ -1,11 +1,14 @@ { "config": { "error": { - "cannot_connect": "Det gick inte att ansluta." + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering", + "unknown": "Ov\u00e4ntat fel" }, "step": { "user": { "data": { + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } diff --git a/homeassistant/components/whois/translations/sv.json b/homeassistant/components/whois/translations/sv.json index c5d4a425a5b..524f5261475 100644 --- a/homeassistant/components/whois/translations/sv.json +++ b/homeassistant/components/whois/translations/sv.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "error": { + "unexpected_response": "Ov\u00e4ntat svar fr\u00e5n whois server", + "unknown_date_format": "Ok\u00e4nt datumformat i whois-serverns svar", + "unknown_tld": "Den givna toppdom\u00e4nen \u00e4r ok\u00e4nd eller inte tillg\u00e4nglig f\u00f6r denna integration", + "whois_command_failed": "Whois-kommandot misslyckades: kunde inte h\u00e4mta whois-information" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/wiffi/translations/sv.json b/homeassistant/components/wiffi/translations/sv.json index 1fdd2ec4aa7..8916c3a34cd 100644 --- a/homeassistant/components/wiffi/translations/sv.json +++ b/homeassistant/components/wiffi/translations/sv.json @@ -1,7 +1,17 @@ { "config": { "abort": { - "addr_in_use": "Serverporten \u00e4r upptagen." + "addr_in_use": "Serverporten \u00e4r upptagen.", + "already_configured": "Serverporten \u00e4r redan konfigurerad.", + "start_server_failed": "Det gick inte att starta servern." + }, + "step": { + "user": { + "data": { + "port": "Port" + }, + "title": "Konfigurera TCP-server f\u00f6r WIFFI-enheter" + } } }, "options": { diff --git a/homeassistant/components/wilight/translations/sv.json b/homeassistant/components/wilight/translations/sv.json new file mode 100644 index 00000000000..5e2fe1c7707 --- /dev/null +++ b/homeassistant/components/wilight/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "not_supported_device": "Denna WiLight st\u00f6ds f\u00f6r n\u00e4rvarande inte", + "not_wilight_device": "Denna enhet \u00e4r inte en WiLight" + }, + "flow_title": "{name}", + "step": { + "confirm": { + "description": "F\u00f6ljande komponenter st\u00f6ds: {components}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiz/translations/sv.json b/homeassistant/components/wiz/translations/sv.json new file mode 100644 index 00000000000..05fe0b3ece1 --- /dev/null +++ b/homeassistant/components/wiz/translations/sv.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "cannot_connect": "Det gick inte att ansluta.", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket" + }, + "error": { + "bulb_time_out": "Kan inte ansluta till lampan. Kanske \u00e4r lampan offline eller s\u00e5 har du angett fel IP-nummer. Sl\u00e5 p\u00e5 lampan och f\u00f6rs\u00f6k igen!", + "cannot_connect": "Det gick inte att ansluta.", + "no_ip": "Inte en giltig IP-adress.", + "no_wiz_light": "Lampan kan inte anslutas via WiZ Platform-integration.", + "unknown": "Ov\u00e4ntat fel" + }, + "flow_title": "{name} ({host})", + "step": { + "discovery_confirm": { + "description": "Vill du konfigurera {name} ({host})?" + }, + "pick_device": { + "data": { + "device": "Enhet" + } + }, + "user": { + "data": { + "host": "IP-adress" + }, + "description": "Om du l\u00e4mnar IP-adressen tom kommer uppt\u00e4ckten att anv\u00e4ndas f\u00f6r att hitta enheter." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.sv.json b/homeassistant/components/wolflink/translations/sensor.sv.json index faf7a2b9673..7a10945fc91 100644 --- a/homeassistant/components/wolflink/translations/sensor.sv.json +++ b/homeassistant/components/wolflink/translations/sensor.sv.json @@ -11,6 +11,7 @@ "at_frostschutz": "OT frostskydd", "aus": "Inaktiverad", "auto": "Automatiskt", + "auto_off_cool": "AutoOffCool", "auto_on_cool": "AutoOnCool", "automatik_aus": "Automatisk avst\u00e4ngning", "automatik_ein": "Automatiskt p\u00e5", @@ -18,6 +19,7 @@ "betrieb_ohne_brenner": "Arbetar utan br\u00e4nnare", "cooling": "Kyler", "deaktiviert": "Inaktiv", + "dhw_prior": "DHWPrior", "eco": "Eco", "ein": "Aktiverad", "estrichtrocknung": "Avj\u00e4mningstorkning", @@ -72,6 +74,7 @@ "test": "Test", "tpw": "TPW", "urlaubsmodus": "Semesterl\u00e4ge", + "ventilprufung": "Test av ventiler", "vorspulen": "Ing\u00e5ngssk\u00f6ljning", "warmwasser": "Varmvatten", "warmwasser_schnellstart": "Snabbstart f\u00f6r varmvatten", diff --git a/homeassistant/components/xiaomi_ble/translations/sv.json b/homeassistant/components/xiaomi_ble/translations/sv.json index 68373aca702..85dd1bdf557 100644 --- a/homeassistant/components/xiaomi_ble/translations/sv.json +++ b/homeassistant/components/xiaomi_ble/translations/sv.json @@ -6,13 +6,22 @@ "decryption_failed": "Den tillhandah\u00e5llna bindningsnyckeln fungerade inte, sensordata kunde inte dekrypteras. Kontrollera den och f\u00f6rs\u00f6k igen.", "expected_24_characters": "F\u00f6rv\u00e4ntade ett hexadecimalt bindningsnyckel med 24 tecken.", "expected_32_characters": "F\u00f6rv\u00e4ntade ett hexadecimalt bindningsnyckel med 32 tecken.", - "no_devices_found": "Inga enheter hittades p\u00e5 n\u00e4tverket" + "no_devices_found": "Inga enheter hittades p\u00e5 n\u00e4tverket", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "decryption_failed": "Den tillhandah\u00e5llna bindningsnyckeln fungerade inte, sensordata kunde inte dekrypteras. Kontrollera den och f\u00f6rs\u00f6k igen.", + "expected_24_characters": "F\u00f6rv\u00e4ntade ett hexadecimalt bindningsnyckel med 24 tecken.", + "expected_32_characters": "F\u00f6rv\u00e4ntade ett hexadecimalt bindningsnyckel med 32 tecken." }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Vill du s\u00e4tta upp {name}?" }, + "confirm_slow": { + "description": "Det har inte s\u00e4nts fr\u00e5n den h\u00e4r enheten under den senaste minuten s\u00e5 vi \u00e4r inte s\u00e4kra p\u00e5 om den h\u00e4r enheten anv\u00e4nder kryptering eller inte. Det kan bero p\u00e5 att enheten anv\u00e4nder ett l\u00e5ngsamt s\u00e4ndningsintervall. Bekr\u00e4fta att l\u00e4gga till den h\u00e4r enheten \u00e4nd\u00e5, n\u00e4sta g\u00e5ng en s\u00e4ndning tas emot kommer du att uppmanas att ange dess bindnyckel om det beh\u00f6vs." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindningsnyckel" @@ -25,6 +34,9 @@ }, "description": "De sensordata som s\u00e4nds av sensorn \u00e4r krypterade. F\u00f6r att dekryptera dem beh\u00f6ver vi en hexadecimal bindningsnyckel med 24 tecken." }, + "slow_confirm": { + "description": "Det har inte s\u00e4nts fr\u00e5n den h\u00e4r enheten under den senaste minuten s\u00e5 vi \u00e4r inte s\u00e4kra p\u00e5 om den h\u00e4r enheten anv\u00e4nder kryptering eller inte. Det kan bero p\u00e5 att enheten anv\u00e4nder ett l\u00e5ngsamt s\u00e4ndningsintervall. Bekr\u00e4fta att l\u00e4gga till den h\u00e4r enheten \u00e4nd\u00e5, n\u00e4sta g\u00e5ng en s\u00e4ndning tas emot kommer du att uppmanas att ange dess bindnyckel om det beh\u00f6vs." + }, "user": { "data": { "address": "Enhet" diff --git a/homeassistant/components/xiaomi_miio/translations/sv.json b/homeassistant/components/xiaomi_miio/translations/sv.json index 37452a61e79..83e3ba16b32 100644 --- a/homeassistant/components/xiaomi_miio/translations/sv.json +++ b/homeassistant/components/xiaomi_miio/translations/sv.json @@ -12,7 +12,8 @@ "cloud_credentials_incomplete": "Cloud-uppgifterna \u00e4r ofullst\u00e4ndiga, v\u00e4nligen fyll i anv\u00e4ndarnamn, l\u00f6senord och land", "cloud_login_error": "Kunde inte logga in p\u00e5 Xiaomi Miio Cloud, kontrollera anv\u00e4ndaruppgifterna.", "cloud_no_devices": "Inga enheter hittades i detta Xiaomi Miio molnkonto.", - "unknown_device": "Enhetsmodellen \u00e4r inte k\u00e4nd, kan inte st\u00e4lla in enheten med hj\u00e4lp av konfigurationsfl\u00f6de." + "unknown_device": "Enhetsmodellen \u00e4r inte k\u00e4nd, kan inte st\u00e4lla in enheten med hj\u00e4lp av konfigurationsfl\u00f6de.", + "wrong_token": "Kontrollsummafel, fel token" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/yale_smart_alarm/translations/sv.json b/homeassistant/components/yale_smart_alarm/translations/sv.json index 8a60ea1a5dc..316c0587b3e 100644 --- a/homeassistant/components/yale_smart_alarm/translations/sv.json +++ b/homeassistant/components/yale_smart_alarm/translations/sv.json @@ -1,16 +1,43 @@ { "config": { + "abort": { + "already_configured": "Konto har redan konfigurerats", + "reauth_successful": "\u00c5terautentisering lyckades" + }, + "error": { + "cannot_connect": "Det gick inte att ansluta.", + "invalid_auth": "Ogiltig autentisering" + }, "step": { "reauth_confirm": { "data": { + "area_id": "Omr\u00e5des-ID", + "name": "Namn", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } }, "user": { "data": { + "area_id": "Omr\u00e5des-ID", + "name": "Namn", + "password": "L\u00f6senord", "username": "Anv\u00e4ndarnamn" } } } + }, + "options": { + "error": { + "code_format_mismatch": "Koden matchar inte det antal siffror som kr\u00e4vs" + }, + "step": { + "init": { + "data": { + "code": "Standardkod f\u00f6r l\u00e5s, anv\u00e4nds om inget anges", + "lock_code_digits": "Antal siffror i PIN-kod f\u00f6r l\u00e5s" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/select.sv.json b/homeassistant/components/yamaha_musiccast/translations/select.sv.json new file mode 100644 index 00000000000..2c1897b9903 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.sv.json @@ -0,0 +1,52 @@ +{ + "state": { + "yamaha_musiccast__dimmer": { + "auto": "Automatiskt" + }, + "yamaha_musiccast__zone_equalizer_mode": { + "auto": "Automatiskt", + "bypass": "F\u00f6rbikoppling", + "manual": "Manuell" + }, + "yamaha_musiccast__zone_link_audio_delay": { + "audio_sync": "Ljudsynkronisering", + "audio_sync_off": "Ljudsynkronisering av", + "audio_sync_on": "Ljudsynkronisering p\u00e5", + "balanced": "Balanserad", + "lip_sync": "L\u00e4ppsynkronisering" + }, + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "Komprimerad", + "uncompressed": "Okomprimerad" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "Hastighet", + "stability": "Stabilitet", + "standard": "Standard" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 minuter", + "30 min": "30 minuter", + "60 min": "60 minuter", + "90 min": "90 minuter", + "off": "Av" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "Automatiskt", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x-spel", + "dolby_pl2x_movie": "Dolby ProLogic 2x film", + "dolby_pl2x_music": "Dolby ProLogic 2x Musik", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 Cinema", + "dts_neo6_music": "DTS Neo:6 Musik", + "dts_neural_x": "DTS Neural:X", + "toggle": "V\u00e4xla" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "Automatiskt", + "bypass": "F\u00f6rbikoppling", + "manual": "Manuell" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/sv.json b/homeassistant/components/yeelight/translations/sv.json index 7a8780c0bf1..18b5f3ce4a9 100644 --- a/homeassistant/components/yeelight/translations/sv.json +++ b/homeassistant/components/yeelight/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten \u00e4r redan konfigurerad" + "already_configured": "Enheten \u00e4r redan konfigurerad", + "no_devices_found": "Inga enheter hittades i n\u00e4tverket" }, "error": { "cannot_connect": "Det gick inte att ansluta." diff --git a/homeassistant/components/youless/translations/sv.json b/homeassistant/components/youless/translations/sv.json new file mode 100644 index 00000000000..45224016263 --- /dev/null +++ b/homeassistant/components/youless/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Det gick inte att ansluta." + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd", + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/sv.json b/homeassistant/components/zerproc/translations/sv.json new file mode 100644 index 00000000000..18a80850e45 --- /dev/null +++ b/homeassistant/components/zerproc/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Inga enheter hittades i n\u00e4tverket", + "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." + }, + "step": { + "confirm": { + "description": "Vill du starta konfigurationen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/sv.json b/homeassistant/components/zha/translations/sv.json index ee189ef1463..57be69d4f9f 100644 --- a/homeassistant/components/zha/translations/sv.json +++ b/homeassistant/components/zha/translations/sv.json @@ -47,6 +47,8 @@ }, "zha_options": { "always_prefer_xy_color_mode": "F\u00f6redrar alltid XY-f\u00e4rgl\u00e4ge", + "consider_unavailable_battery": "\u00d6verv\u00e4g att batteridrivna enheter inte \u00e4r tillg\u00e4ngliga efter (sekunder)", + "consider_unavailable_mains": "Anse att n\u00e4tdrivna enheter inte \u00e4r tillg\u00e4ngliga efter (sekunder)", "default_light_transition": "Standard ljus\u00f6verg\u00e5ngstid (sekunder)", "enable_identify_on_join": "Aktivera identifieringseffekt n\u00e4r enheter ansluter till n\u00e4tverket", "enhanced_light_transition": "Aktivera f\u00f6rb\u00e4ttrad ljusf\u00e4rg/temperatur\u00f6verg\u00e5ng fr\u00e5n ett avst\u00e4ngt l\u00e4ge", diff --git a/homeassistant/components/zodiac/translations/sensor.sv.json b/homeassistant/components/zodiac/translations/sensor.sv.json new file mode 100644 index 00000000000..ca874c92c00 --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.sv.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Vattumannen", + "aries": "V\u00e4duren", + "cancer": "Kr\u00e4ftan", + "capricorn": "Stenbocken", + "gemini": "Tvillingarna", + "leo": "Lejonet", + "libra": "V\u00e5gen", + "pisces": "Fiskarna", + "sagittarius": "Skytten", + "scorpio": "Skorpionen", + "taurus": "Oxen", + "virgo": "Jungfrun" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/sv.json b/homeassistant/components/zoneminder/translations/sv.json index f3e7d2891bd..07a162a321f 100644 --- a/homeassistant/components/zoneminder/translations/sv.json +++ b/homeassistant/components/zoneminder/translations/sv.json @@ -2,21 +2,32 @@ "config": { "abort": { "auth_fail": "Anv\u00e4ndarnamn eller l\u00f6senord \u00e4r felaktigt.", - "connection_error": "Det gick inte att ansluta till en ZoneMinder-server." + "cannot_connect": "Det gick inte att ansluta.", + "connection_error": "Det gick inte att ansluta till en ZoneMinder-server.", + "invalid_auth": "Ogiltig autentisering" }, "create_entry": { "default": "ZoneMinder-server har lagts till." }, "error": { "auth_fail": "Anv\u00e4ndarnamn eller l\u00f6senord \u00e4r felaktigt.", - "connection_error": "Det gick inte att ansluta till en ZoneMinder-server." + "cannot_connect": "Det gick inte att ansluta.", + "connection_error": "Det gick inte att ansluta till en ZoneMinder-server.", + "invalid_auth": "Ogiltig autentisering" }, + "flow_title": "ZoneMinder", "step": { "user": { "data": { + "host": "V\u00e4rd och port (ex 10.10.0.4:8010)", + "password": "L\u00f6senord", + "path": "ZM-s\u00f6kv\u00e4g", + "path_zms": "ZMS-s\u00f6kv\u00e4g", + "ssl": "Anv\u00e4nd ett SSL certifikat", "username": "Anv\u00e4ndarnamn", "verify_ssl": "Verifiera SSL-certifikat" - } + }, + "title": "L\u00e4gg till ZoneMinder Server." } } } diff --git a/homeassistant/components/zwave_js/translations/sv.json b/homeassistant/components/zwave_js/translations/sv.json index 65ddbb3dce8..b619c54026b 100644 --- a/homeassistant/components/zwave_js/translations/sv.json +++ b/homeassistant/components/zwave_js/translations/sv.json @@ -1,29 +1,66 @@ { "config": { "abort": { + "addon_get_discovery_info_failed": "Det gick inte att h\u00e4mta Z-Wave JS-till\u00e4ggsuppt\u00e4cktsinformation.", + "addon_info_failed": "Det gick inte att h\u00e4mta Z-Wave JS-till\u00e4ggsinformation.", + "addon_install_failed": "Det gick inte att installera Z-Wave JS-till\u00e4gget.", + "addon_set_config_failed": "Det gick inte att st\u00e4lla in Z-Wave JS-konfiguration.", + "addon_start_failed": "Det gick inte att starta Z-Wave JS-till\u00e4gget.", "already_configured": "Enheten \u00e4r redan konfigurerad", + "already_in_progress": "Konfigurationsfl\u00f6det p\u00e5g\u00e5r redan", + "cannot_connect": "Det gick inte att ansluta.", "discovery_requires_supervisor": "Uppt\u00e4ckt kr\u00e4ver \u00f6vervakaren.", "not_zwave_device": "Uppt\u00e4ckt enhet \u00e4r inte en Z-Wave-enhet." }, "error": { + "addon_start_failed": "Det gick inte att starta Z-Wave JS-till\u00e4gget. Kontrollera konfigurationen.", "cannot_connect": "Det gick inte att ansluta.", "invalid_ws_url": "Ogiltig websocket-URL", "unknown": "Ov\u00e4ntat fel" }, "flow_title": "{name}", + "progress": { + "install_addon": "V\u00e4nta medan installationen av Z-Wave JS-till\u00e4gget slutf\u00f6rs. Detta kan ta flera minuter.", + "start_addon": "V\u00e4nta medan Z-Wave JS-till\u00e4ggsstarten slutf\u00f6rs. Detta kan ta n\u00e5gra sekunder." + }, "step": { "configure_addon": { "data": { "s0_legacy_key": "S0-nyckel (\u00e4ldre)", "s2_access_control_key": "S2-\u00e5tkomstkontrollnyckel", "s2_authenticated_key": "S2 Autentiserad nyckel", - "s2_unauthenticated_key": "S2 oautentiserad nyckel" + "s2_unauthenticated_key": "S2 oautentiserad nyckel", + "usb_path": "USB-enhetens s\u00f6kv\u00e4g" + }, + "description": "Till\u00e4gget genererar s\u00e4kerhetsnycklar om dessa f\u00e4lt l\u00e4mnas tomma.", + "title": "G\u00e5 in i konfigurationen f\u00f6r till\u00e4gget Z-Wave JS" + }, + "hassio_confirm": { + "title": "Konfigurera Z-Wave JS-integration med till\u00e4gget Z-Wave JS" + }, + "install_addon": { + "title": "Z-Wave JS-till\u00e4ggsinstallationen har startat" + }, + "manual": { + "data": { + "url": "URL" } }, + "on_supervisor": { + "data": { + "use_addon": "Anv\u00e4nd Z-Wave JS Supervisor-till\u00e4gget" + }, + "description": "Vill du anv\u00e4nda Z-Wave JS Supervisor-till\u00e4gget?", + "title": "V\u00e4lj anslutningsmetod" + }, + "start_addon": { + "title": "Z-Wave JS-till\u00e4gget startar." + }, "usb_confirm": { "description": "Vill du konfigurera {name} med Z-Wave JS till\u00e4gget?" }, "zeroconf_confirm": { + "description": "Vill du l\u00e4gga till Z-Wave JS Server med hem-ID {home_id} finns p\u00e5 {url} till Home Assistant?", "title": "Uppt\u00e4ckte Z-Wave JS Server" } } @@ -56,7 +93,11 @@ }, "options": { "abort": { + "addon_get_discovery_info_failed": "Det gick inte att h\u00e4mta Z-Wave JS-till\u00e4ggsuppt\u00e4cktsinformation.", + "addon_info_failed": "Det gick inte att h\u00e4mta Z-Wave JS-till\u00e4ggsinformation.", "addon_install_failed": "Det gick inte att installera Z-Wave JS-till\u00e4gget.", + "addon_set_config_failed": "Det gick inte att st\u00e4lla in Z-Wave JS-konfiguration.", + "addon_start_failed": "Det gick inte att starta Z-Wave JS-till\u00e4gget.", "already_configured": "Enheten \u00e4r redan konfigurerad", "cannot_connect": "Det gick inte att ansluta.", "different_device": "Den anslutna USB-enheten \u00e4r inte densamma som tidigare konfigurerats f\u00f6r den h\u00e4r konfigurationsposten. Skapa ist\u00e4llet en ny konfigurationspost f\u00f6r den nya enheten." @@ -80,7 +121,9 @@ "s2_authenticated_key": "S2 Autentiserad nyckel", "s2_unauthenticated_key": "S2 oautentiserad nyckel", "usb_path": "USB-enhetens s\u00f6kv\u00e4g" - } + }, + "description": "Till\u00e4gget genererar s\u00e4kerhetsnycklar om dessa f\u00e4lt l\u00e4mnas tomma.", + "title": "G\u00e5 in i konfigurationen f\u00f6r till\u00e4gget Z-Wave JS" }, "install_addon": { "title": "Z-Wave JS-till\u00e4ggsinstallationen har startat" @@ -91,7 +134,14 @@ } }, "on_supervisor": { + "data": { + "use_addon": "Anv\u00e4nd Z-Wave JS Supervisor-till\u00e4gget" + }, + "description": "Vill du anv\u00e4nda Z-Wave JS Supervisor-till\u00e4gget?", "title": "V\u00e4lj anslutningsmetod" + }, + "start_addon": { + "title": "Z-Wave JS-till\u00e4gget startar." } } } diff --git a/homeassistant/components/zwave_me/translations/sv.json b/homeassistant/components/zwave_me/translations/sv.json new file mode 100644 index 00000000000..3012a7f3ef6 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/sv.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten \u00e4r redan konfigurerad", + "no_valid_uuid_set": "Ingen giltig UUID-upps\u00e4ttning" + }, + "error": { + "no_valid_uuid_set": "Ingen giltig UUID-upps\u00e4ttning" + }, + "step": { + "user": { + "data": { + "token": "API Token", + "url": "URL" + }, + "description": "Mata in IP-adress med port och \u00e5tkomsttoken f\u00f6r Z-Way-servern. F\u00f6r att f\u00e5 token, g\u00e5 till Z-Way-anv\u00e4ndargr\u00e4nssnittet Smart Home UI > Meny > Inst\u00e4llningar > Anv\u00e4ndare > Administrat\u00f6r > API-token. \n\n Exempel p\u00e5 anslutning till Z-Way i det lokala n\u00e4tverket:\n URL: {local_url}\n Token: {local_token} \n\n Exempel p\u00e5 anslutning till Z-Way via fj\u00e4rr\u00e5tkomst find.z-wave.me:\n URL: {find_url}\n Token: {find_token} \n\n Exempel p\u00e5 anslutning till Z-Way med en statisk offentlig IP-adress:\n URL: {remote_url}\n Token: {local_token} \n\n N\u00e4r du ansluter via find.z-wave.me m\u00e5ste du anv\u00e4nda en token med ett globalt scope (logga in p\u00e5 Z-Way via find.z-wave.me f\u00f6r detta)." + } + } + } +} \ No newline at end of file From 7c6a64c348e541f48c50d0d504aa37844c14a743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roy?= Date: Fri, 5 Aug 2022 17:30:45 -0700 Subject: [PATCH 3173/3516] Bump aiobafi6 to 0.7.2 to unblock #76328 (#76330) --- homeassistant/components/baf/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/baf/manifest.json b/homeassistant/components/baf/manifest.json index 15e0272b2b0..7462a64e770 100644 --- a/homeassistant/components/baf/manifest.json +++ b/homeassistant/components/baf/manifest.json @@ -3,7 +3,7 @@ "name": "Big Ass Fans", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/baf", - "requirements": ["aiobafi6==0.7.0"], + "requirements": ["aiobafi6==0.7.2"], "codeowners": ["@bdraco", "@jfroy"], "iot_class": "local_push", "zeroconf": [ diff --git a/requirements_all.txt b/requirements_all.txt index b638e66be56..c3f23dfba96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -128,7 +128,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.7.0 +aiobafi6==0.7.2 # homeassistant.components.aws aiobotocore==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f4cd646e4e..f8e23870756 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -115,7 +115,7 @@ aioasuswrt==1.4.0 aioazuredevops==1.3.5 # homeassistant.components.baf -aiobafi6==0.7.0 +aiobafi6==0.7.2 # homeassistant.components.aws aiobotocore==2.1.0 From 76f137eb758910bced2ae3a59651951e88b48479 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Aug 2022 00:39:33 -1000 Subject: [PATCH 3174/3516] Bump yalexs to 1.2.1 (#76339) Changelog: https://github.com/bdraco/yalexs/compare/v1.1.25...v1.2.1 --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 34bd4843f32..418fa6920ad 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.1.25"], + "requirements": ["yalexs==1.2.1"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index c3f23dfba96..e654833389e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2496,7 +2496,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.august -yalexs==1.1.25 +yalexs==1.2.1 # homeassistant.components.yeelight yeelight==0.7.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8e23870756..1c490f24ea7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1682,7 +1682,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.8 # homeassistant.components.august -yalexs==1.1.25 +yalexs==1.2.1 # homeassistant.components.yeelight yeelight==0.7.10 From adce55b4dbe1926a4f6d2e545c487565969b9858 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Aug 2022 00:46:27 -1000 Subject: [PATCH 3175/3516] Bump pySwitchbot to 0.18.4 (#76322) * Bump pySwitchbot to 0.18.3 Fixes #76321 Changelog: https://github.com/Danielhiversen/pySwitchbot/compare/0.17.3...0.18.3 * bump --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index f01eae4a938..b413b44d605 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.17.3"], + "requirements": ["PySwitchbot==0.18.4"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index e654833389e..7abfad4db5e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.17.3 +PySwitchbot==0.18.4 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c490f24ea7..75ce10222f1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.17.3 +PySwitchbot==0.18.4 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From c580bce879b6c2f68c4ea45707b5a05ee88c6ecc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Aug 2022 08:10:26 -1000 Subject: [PATCH 3176/3516] Move HKC entity classes into entity.py (#76333) --- .../components/homekit_controller/__init__.py | 198 +---------------- .../homekit_controller/alarm_control_panel.py | 3 +- .../homekit_controller/binary_sensor.py | 3 +- .../components/homekit_controller/button.py | 3 +- .../components/homekit_controller/camera.py | 3 +- .../components/homekit_controller/climate.py | 3 +- .../components/homekit_controller/cover.py | 3 +- .../components/homekit_controller/entity.py | 203 ++++++++++++++++++ .../components/homekit_controller/fan.py | 3 +- .../homekit_controller/humidifier.py | 3 +- .../components/homekit_controller/light.py | 3 +- .../components/homekit_controller/lock.py | 3 +- .../homekit_controller/media_player.py | 3 +- .../components/homekit_controller/number.py | 3 +- .../components/homekit_controller/select.py | 3 +- .../components/homekit_controller/sensor.py | 3 +- .../components/homekit_controller/switch.py | 3 +- 17 files changed, 235 insertions(+), 211 deletions(-) create mode 100644 homeassistant/components/homekit_controller/entity.py diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index b2ccad9a457..3a5ba42848c 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio import logging -from typing import Any import aiohomekit from aiohomekit.exceptions import ( @@ -11,216 +10,23 @@ from aiohomekit.exceptions import ( AccessoryNotFoundError, EncryptionError, ) -from aiohomekit.model import Accessory -from aiohomekit.model.characteristics import ( - Characteristic, - CharacteristicPermissions, - CharacteristicsTypes, -) -from aiohomekit.model.services import Service, ServicesTypes from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_IDENTIFIERS, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.typing import ConfigType from .config_flow import normalize_hkid -from .connection import HKDevice, valid_serial_number +from .connection import HKDevice from .const import ENTITY_MAP, KNOWN_DEVICES, TRIGGERS from .storage import EntityMapStorage, async_get_entity_storage -from .utils import async_get_controller, folded_name +from .utils import async_get_controller _LOGGER = logging.getLogger(__name__) -class HomeKitEntity(Entity): - """Representation of a Home Assistant HomeKit device.""" - - _attr_should_poll = False - - def __init__(self, accessory: HKDevice, devinfo: ConfigType) -> None: - """Initialise a generic HomeKit device.""" - self._accessory = accessory - self._aid = devinfo["aid"] - self._iid = devinfo["iid"] - self._char_name: str | None = None - self._features = 0 - self.setup() - - super().__init__() - - @property - def accessory(self) -> Accessory: - """Return an Accessory model that this entity is attached to.""" - return self._accessory.entity_map.aid(self._aid) - - @property - def accessory_info(self) -> Service: - """Information about the make and model of an accessory.""" - return self.accessory.services.first( - service_type=ServicesTypes.ACCESSORY_INFORMATION - ) - - @property - def service(self) -> Service: - """Return a Service model that this entity is attached to.""" - return self.accessory.services.iid(self._iid) - - async def async_added_to_hass(self) -> None: - """Entity added to hass.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self._accessory.signal_state_updated, - self.async_write_ha_state, - ) - ) - - self._accessory.add_pollable_characteristics(self.pollable_characteristics) - await self._accessory.add_watchable_characteristics( - self.watchable_characteristics - ) - - async def async_will_remove_from_hass(self) -> None: - """Prepare to be removed from hass.""" - self._accessory.remove_pollable_characteristics(self._aid) - self._accessory.remove_watchable_characteristics(self._aid) - - async def async_put_characteristics(self, characteristics: dict[str, Any]) -> None: - """ - Write characteristics to the device. - - A characteristic type is unique within a service, but in order to write - to a named characteristic on a bridge we need to turn its type into - an aid and iid, and send it as a list of tuples, which is what this - helper does. - - E.g. you can do: - - await entity.async_put_characteristics({ - CharacteristicsTypes.ON: True - }) - """ - payload = self.service.build_update(characteristics) - return await self._accessory.put_characteristics(payload) - - def setup(self) -> None: - """Configure an entity based on its HomeKit characteristics metadata.""" - self.pollable_characteristics: list[tuple[int, int]] = [] - self.watchable_characteristics: list[tuple[int, int]] = [] - - char_types = self.get_characteristic_types() - - # Setup events and/or polling for characteristics directly attached to this entity - for char in self.service.characteristics.filter(char_types=char_types): - self._setup_characteristic(char) - - # Setup events and/or polling for characteristics attached to sub-services of this - # entity (like an INPUT_SOURCE). - for service in self.accessory.services.filter(parent_service=self.service): - for char in service.characteristics.filter(char_types=char_types): - self._setup_characteristic(char) - - def _setup_characteristic(self, char: Characteristic) -> None: - """Configure an entity based on a HomeKit characteristics metadata.""" - # Build up a list of (aid, iid) tuples to poll on update() - if CharacteristicPermissions.paired_read in char.perms: - self.pollable_characteristics.append((self._aid, char.iid)) - - # Build up a list of (aid, iid) tuples to subscribe to - if CharacteristicPermissions.events in char.perms: - self.watchable_characteristics.append((self._aid, char.iid)) - - if self._char_name is None: - self._char_name = char.service.value(CharacteristicsTypes.NAME) - - @property - def unique_id(self) -> str: - """Return the ID of this device.""" - info = self.accessory_info - serial = info.value(CharacteristicsTypes.SERIAL_NUMBER) - if valid_serial_number(serial): - return f"homekit-{serial}-{self._iid}" - # Some accessories do not have a serial number - return f"homekit-{self._accessory.unique_id}-{self._aid}-{self._iid}" - - @property - def default_name(self) -> str | None: - """Return the default name of the device.""" - return None - - @property - def name(self) -> str | None: - """Return the name of the device if any.""" - accessory_name = self.accessory.name - # If the service has a name char, use that, if not - # fallback to the default name provided by the subclass - device_name = self._char_name or self.default_name - folded_device_name = folded_name(device_name or "") - folded_accessory_name = folded_name(accessory_name) - if device_name: - # Sometimes the device name includes the accessory - # name already like My ecobee Occupancy / My ecobee - if folded_device_name.startswith(folded_accessory_name): - return device_name - if ( - folded_accessory_name not in folded_device_name - and folded_device_name not in folded_accessory_name - ): - return f"{accessory_name} {device_name}" - return accessory_name - - @property - def available(self) -> bool: - """Return True if entity is available.""" - return self._accessory.available and self.service.available - - @property - def device_info(self) -> DeviceInfo: - """Return the device info.""" - return self._accessory.device_info_for_accessory(self.accessory) - - def get_characteristic_types(self) -> list[str]: - """Define the homekit characteristics the entity cares about.""" - raise NotImplementedError - - -class AccessoryEntity(HomeKitEntity): - """A HomeKit entity that is related to an entire accessory rather than a specific service or characteristic.""" - - @property - def unique_id(self) -> str: - """Return the ID of this device.""" - serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) - return f"homekit-{serial}-aid:{self._aid}" - - -class CharacteristicEntity(HomeKitEntity): - """ - A HomeKit entity that is related to an single characteristic rather than a whole service. - - This is typically used to expose additional sensor, binary_sensor or number entities that don't belong with - the service entity. - """ - - def __init__( - self, accessory: HKDevice, devinfo: ConfigType, char: Characteristic - ) -> None: - """Initialise a generic single characteristic HomeKit entity.""" - self._char = char - super().__init__(accessory, devinfo) - - @property - def unique_id(self) -> str: - """Return the ID of this device.""" - serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) - return f"homekit-{serial}-aid:{self._aid}-sid:{self._char.service.iid}-cid:{self._char.iid}" - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a HomeKit connection on a config entry.""" conn = HKDevice(hass, entry, entry.data) diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index c7e499b6e89..204fa1bb3f8 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -22,7 +22,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES +from .entity import HomeKitEntity ICON = "mdi:security" diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 5efd0915cb0..11c81e7e251 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -12,7 +12,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES +from .entity import HomeKitEntity class HomeKitMotionSensor(HomeKitEntity, BinarySensorEntity): diff --git a/homeassistant/components/homekit_controller/button.py b/homeassistant/components/homekit_controller/button.py index e9c85dbe876..d5a8bc733ad 100644 --- a/homeassistant/components/homekit_controller/button.py +++ b/homeassistant/components/homekit_controller/button.py @@ -21,8 +21,9 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType -from . import KNOWN_DEVICES, CharacteristicEntity +from . import KNOWN_DEVICES from .connection import HKDevice +from .entity import CharacteristicEntity @dataclass diff --git a/homeassistant/components/homekit_controller/camera.py b/homeassistant/components/homekit_controller/camera.py index 0f0dd4f9050..510c0c2f522 100644 --- a/homeassistant/components/homekit_controller/camera.py +++ b/homeassistant/components/homekit_controller/camera.py @@ -9,7 +9,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, AccessoryEntity +from . import KNOWN_DEVICES +from .entity import AccessoryEntity class HomeKitCamera(AccessoryEntity, Camera): diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index b76ed1ea6a9..7254363e835 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -38,7 +38,8 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES +from .entity import HomeKitEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index 4e8af03bba0..6cbc623596e 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -18,7 +18,8 @@ from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_O from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES +from .entity import HomeKitEntity STATE_STOPPED = "stopped" diff --git a/homeassistant/components/homekit_controller/entity.py b/homeassistant/components/homekit_controller/entity.py new file mode 100644 index 00000000000..ad99e65f2d8 --- /dev/null +++ b/homeassistant/components/homekit_controller/entity.py @@ -0,0 +1,203 @@ +"""Homekit Controller entities.""" +from __future__ import annotations + +from typing import Any + +from aiohomekit.model import Accessory +from aiohomekit.model.characteristics import ( + Characteristic, + CharacteristicPermissions, + CharacteristicsTypes, +) +from aiohomekit.model.services import Service, ServicesTypes + +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.typing import ConfigType + +from .connection import HKDevice, valid_serial_number +from .utils import folded_name + + +class HomeKitEntity(Entity): + """Representation of a Home Assistant HomeKit device.""" + + _attr_should_poll = False + + def __init__(self, accessory: HKDevice, devinfo: ConfigType) -> None: + """Initialise a generic HomeKit device.""" + self._accessory = accessory + self._aid = devinfo["aid"] + self._iid = devinfo["iid"] + self._char_name: str | None = None + self._features = 0 + self.setup() + + super().__init__() + + @property + def accessory(self) -> Accessory: + """Return an Accessory model that this entity is attached to.""" + return self._accessory.entity_map.aid(self._aid) + + @property + def accessory_info(self) -> Service: + """Information about the make and model of an accessory.""" + return self.accessory.services.first( + service_type=ServicesTypes.ACCESSORY_INFORMATION + ) + + @property + def service(self) -> Service: + """Return a Service model that this entity is attached to.""" + return self.accessory.services.iid(self._iid) + + async def async_added_to_hass(self) -> None: + """Entity added to hass.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self._accessory.signal_state_updated, + self.async_write_ha_state, + ) + ) + + self._accessory.add_pollable_characteristics(self.pollable_characteristics) + await self._accessory.add_watchable_characteristics( + self.watchable_characteristics + ) + + async def async_will_remove_from_hass(self) -> None: + """Prepare to be removed from hass.""" + self._accessory.remove_pollable_characteristics(self._aid) + self._accessory.remove_watchable_characteristics(self._aid) + + async def async_put_characteristics(self, characteristics: dict[str, Any]) -> None: + """ + Write characteristics to the device. + + A characteristic type is unique within a service, but in order to write + to a named characteristic on a bridge we need to turn its type into + an aid and iid, and send it as a list of tuples, which is what this + helper does. + + E.g. you can do: + + await entity.async_put_characteristics({ + CharacteristicsTypes.ON: True + }) + """ + payload = self.service.build_update(characteristics) + return await self._accessory.put_characteristics(payload) + + def setup(self) -> None: + """Configure an entity based on its HomeKit characteristics metadata.""" + self.pollable_characteristics: list[tuple[int, int]] = [] + self.watchable_characteristics: list[tuple[int, int]] = [] + + char_types = self.get_characteristic_types() + + # Setup events and/or polling for characteristics directly attached to this entity + for char in self.service.characteristics.filter(char_types=char_types): + self._setup_characteristic(char) + + # Setup events and/or polling for characteristics attached to sub-services of this + # entity (like an INPUT_SOURCE). + for service in self.accessory.services.filter(parent_service=self.service): + for char in service.characteristics.filter(char_types=char_types): + self._setup_characteristic(char) + + def _setup_characteristic(self, char: Characteristic) -> None: + """Configure an entity based on a HomeKit characteristics metadata.""" + # Build up a list of (aid, iid) tuples to poll on update() + if CharacteristicPermissions.paired_read in char.perms: + self.pollable_characteristics.append((self._aid, char.iid)) + + # Build up a list of (aid, iid) tuples to subscribe to + if CharacteristicPermissions.events in char.perms: + self.watchable_characteristics.append((self._aid, char.iid)) + + if self._char_name is None: + self._char_name = char.service.value(CharacteristicsTypes.NAME) + + @property + def unique_id(self) -> str: + """Return the ID of this device.""" + info = self.accessory_info + serial = info.value(CharacteristicsTypes.SERIAL_NUMBER) + if valid_serial_number(serial): + return f"homekit-{serial}-{self._iid}" + # Some accessories do not have a serial number + return f"homekit-{self._accessory.unique_id}-{self._aid}-{self._iid}" + + @property + def default_name(self) -> str | None: + """Return the default name of the device.""" + return None + + @property + def name(self) -> str | None: + """Return the name of the device if any.""" + accessory_name = self.accessory.name + # If the service has a name char, use that, if not + # fallback to the default name provided by the subclass + device_name = self._char_name or self.default_name + folded_device_name = folded_name(device_name or "") + folded_accessory_name = folded_name(accessory_name) + if device_name: + # Sometimes the device name includes the accessory + # name already like My ecobee Occupancy / My ecobee + if folded_device_name.startswith(folded_accessory_name): + return device_name + if ( + folded_accessory_name not in folded_device_name + and folded_device_name not in folded_accessory_name + ): + return f"{accessory_name} {device_name}" + return accessory_name + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._accessory.available and self.service.available + + @property + def device_info(self) -> DeviceInfo: + """Return the device info.""" + return self._accessory.device_info_for_accessory(self.accessory) + + def get_characteristic_types(self) -> list[str]: + """Define the homekit characteristics the entity cares about.""" + raise NotImplementedError + + +class AccessoryEntity(HomeKitEntity): + """A HomeKit entity that is related to an entire accessory rather than a specific service or characteristic.""" + + @property + def unique_id(self) -> str: + """Return the ID of this device.""" + serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) + return f"homekit-{serial}-aid:{self._aid}" + + +class CharacteristicEntity(HomeKitEntity): + """ + A HomeKit entity that is related to an single characteristic rather than a whole service. + + This is typically used to expose additional sensor, binary_sensor or number entities that don't belong with + the service entity. + """ + + def __init__( + self, accessory: HKDevice, devinfo: ConfigType, char: Characteristic + ) -> None: + """Initialise a generic single characteristic HomeKit entity.""" + self._char = char + super().__init__(accessory, devinfo) + + @property + def unique_id(self) -> str: + """Return the ID of this device.""" + serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) + return f"homekit-{serial}-aid:{self._aid}-sid:{self._char.service.iid}-cid:{self._char.iid}" diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index 159a1d936fa..03f4dade674 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -20,7 +20,8 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES +from .entity import HomeKitEntity # 0 is clockwise, 1 is counter-clockwise. The match to forward and reverse is so that # its consistent with homeassistant.components.homekit. diff --git a/homeassistant/components/homekit_controller/humidifier.py b/homeassistant/components/homekit_controller/humidifier.py index 1676999ad78..ebba525e0c9 100644 --- a/homeassistant/components/homekit_controller/humidifier.py +++ b/homeassistant/components/homekit_controller/humidifier.py @@ -21,7 +21,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES +from .entity import HomeKitEntity HK_MODE_TO_HA = { 0: "off", diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index d882f6790f7..010411c60d0 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -17,7 +17,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES +from .entity import HomeKitEntity async def async_setup_entry( diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index 248bb93a68f..8e8919ae4f8 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -17,7 +17,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES +from .entity import HomeKitEntity CURRENT_STATE_MAP = { 0: STATE_UNLOCKED, diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py index fbdf800edf8..092652ed17d 100644 --- a/homeassistant/components/homekit_controller/media_player.py +++ b/homeassistant/components/homekit_controller/media_player.py @@ -28,7 +28,8 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES +from .entity import HomeKitEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/homekit_controller/number.py b/homeassistant/components/homekit_controller/number.py index 7a6d0a01ab6..2987c82e829 100644 --- a/homeassistant/components/homekit_controller/number.py +++ b/homeassistant/components/homekit_controller/number.py @@ -20,8 +20,9 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType -from . import KNOWN_DEVICES, CharacteristicEntity +from . import KNOWN_DEVICES from .connection import HKDevice +from .entity import CharacteristicEntity NUMBER_ENTITIES: dict[str, NumberEntityDescription] = { CharacteristicsTypes.VENDOR_VOCOLINC_HUMIDIFIER_SPRAY_LEVEL: NumberEntityDescription( diff --git a/homeassistant/components/homekit_controller/select.py b/homeassistant/components/homekit_controller/select.py index 681f24b9ab8..a22f79d675b 100644 --- a/homeassistant/components/homekit_controller/select.py +++ b/homeassistant/components/homekit_controller/select.py @@ -8,8 +8,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import KNOWN_DEVICES, CharacteristicEntity +from . import KNOWN_DEVICES from .const import DEVICE_CLASS_ECOBEE_MODE +from .entity import CharacteristicEntity _ECOBEE_MODE_TO_TEXT = { 0: "home", diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index a6810c10d99..04856a60347 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -32,8 +32,9 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType -from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity +from . import KNOWN_DEVICES from .connection import HKDevice +from .entity import CharacteristicEntity, HomeKitEntity from .utils import folded_name diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index be6c3b8bfe0..c537233de7e 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -19,8 +19,9 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType -from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity +from . import KNOWN_DEVICES from .connection import HKDevice +from .entity import CharacteristicEntity, HomeKitEntity OUTLET_IN_USE = "outlet_in_use" From 1aa0e64354d1dffd5e948da803e4d379ee39443c Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 6 Aug 2022 21:45:44 +0100 Subject: [PATCH 3177/3516] Update gree to use the network component to set discovery interfaces (#75812) --- homeassistant/components/gree/__init__.py | 4 +++- homeassistant/components/gree/config_flow.py | 6 +++++- homeassistant/components/gree/manifest.json | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/gree/common.py | 2 +- 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index d4a929f1642..ff3438ed53f 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from homeassistant.components.network import async_get_ipv4_broadcast_addresses from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -32,7 +33,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async def _async_scan_update(_=None): - await gree_discovery.discovery.scan() + bcast_addr = list(await async_get_ipv4_broadcast_addresses(hass)) + await gree_discovery.discovery.scan(0, bcast_ifaces=bcast_addr) _LOGGER.debug("Scanning network for Gree devices") await _async_scan_update() diff --git a/homeassistant/components/gree/config_flow.py b/homeassistant/components/gree/config_flow.py index d317fe6d873..58f83cd4486 100644 --- a/homeassistant/components/gree/config_flow.py +++ b/homeassistant/components/gree/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Gree.""" from greeclimate.discovery import Discovery +from homeassistant.components.network import async_get_ipv4_broadcast_addresses from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_flow @@ -10,7 +11,10 @@ from .const import DISCOVERY_TIMEOUT, DOMAIN async def _async_has_devices(hass: HomeAssistant) -> bool: """Return if there are devices that can be discovered.""" gree_discovery = Discovery(DISCOVERY_TIMEOUT) - devices = await gree_discovery.scan(wait_for=DISCOVERY_TIMEOUT) + bcast_addr = list(await async_get_ipv4_broadcast_addresses(hass)) + devices = await gree_discovery.scan( + wait_for=DISCOVERY_TIMEOUT, bcast_ifaces=bcast_addr + ) return len(devices) > 0 diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index 1b2c8dd6a2a..97c0ec1780c 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,7 +3,8 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==1.2.0"], + "requirements": ["greeclimate==1.3.0"], + "dependencies": ["network"], "codeowners": ["@cmroche"], "iot_class": "local_polling", "loggers": ["greeclimate"] diff --git a/requirements_all.txt b/requirements_all.txt index 7abfad4db5e..ead73586d7a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -772,7 +772,7 @@ gpiozero==1.6.2 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.2.0 +greeclimate==1.3.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75ce10222f1..e27714f81f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -564,7 +564,7 @@ googlemaps==2.5.1 govee-ble==0.12.6 # homeassistant.components.gree -greeclimate==1.2.0 +greeclimate==1.3.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/tests/components/gree/common.py b/tests/components/gree/common.py index c7db03b118f..cd8a2d6ee28 100644 --- a/tests/components/gree/common.py +++ b/tests/components/gree/common.py @@ -28,7 +28,7 @@ class FakeDiscovery: """Add an event listener.""" self._listeners.append(listener) - async def scan(self, wait_for: int = 0): + async def scan(self, wait_for: int = 0, bcast_ifaces=None): """Search for devices, return mocked data.""" self.scan_count += 1 _LOGGER.info("CALLED SCAN %d TIMES", self.scan_count) From e864b82c036d38b8d793c7094d7f19ccfff6a63f Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 7 Aug 2022 01:22:50 +0200 Subject: [PATCH 3178/3516] Improve mysensors config flow (#75122) * Improve mysensors config flow * Improve form input order * Update flow tests --- .../components/mysensors/config_flow.py | 112 ++++++++--------- homeassistant/components/mysensors/const.py | 5 - .../components/mysensors/strings.json | 10 +- .../components/mysensors/translations/en.json | 10 +- .../components/mysensors/test_config_flow.py | 113 +++++++----------- 5 files changed, 113 insertions(+), 137 deletions(-) diff --git a/homeassistant/components/mysensors/config_flow.py b/homeassistant/components/mysensors/config_flow.py index 5409e3c9a85..04e95f1dad3 100644 --- a/homeassistant/components/mysensors/config_flow.py +++ b/homeassistant/components/mysensors/config_flow.py @@ -20,13 +20,13 @@ from homeassistant.components.mqtt import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import selector import homeassistant.helpers.config_validation as cv from .const import ( CONF_BAUD_RATE, CONF_DEVICE, CONF_GATEWAY_TYPE, - CONF_GATEWAY_TYPE_ALL, CONF_GATEWAY_TYPE_MQTT, CONF_GATEWAY_TYPE_SERIAL, CONF_GATEWAY_TYPE_TCP, @@ -45,6 +45,15 @@ DEFAULT_BAUD_RATE = 115200 DEFAULT_TCP_PORT = 5003 DEFAULT_VERSION = "1.4" +_PORT_SELECTOR = vol.All( + selector.NumberSelector( + selector.NumberSelectorConfig( + min=1, max=65535, mode=selector.NumberSelectorMode.BOX + ), + ), + vol.Coerce(int), +) + def is_persistence_file(value: str) -> str: """Validate that persistence file path ends in either .pickle or .json.""" @@ -119,51 +128,34 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, str] | None = None ) -> FlowResult: """Create a config entry from frontend user input.""" - schema = {vol.Required(CONF_GATEWAY_TYPE): vol.In(CONF_GATEWAY_TYPE_ALL)} - schema = vol.Schema(schema) - errors = {} - - if user_input is not None: - gw_type = self._gw_type = user_input[CONF_GATEWAY_TYPE] - input_pass = user_input if CONF_DEVICE in user_input else None - if gw_type == CONF_GATEWAY_TYPE_MQTT: - # Naive check that doesn't consider config entry state. - if MQTT_DOMAIN in self.hass.config.components: - return await self.async_step_gw_mqtt(input_pass) - - errors["base"] = "mqtt_required" - if gw_type == CONF_GATEWAY_TYPE_TCP: - return await self.async_step_gw_tcp(input_pass) - if gw_type == CONF_GATEWAY_TYPE_SERIAL: - return await self.async_step_gw_serial(input_pass) - - return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + return self.async_show_menu( + step_id="select_gateway_type", + menu_options=["gw_serial", "gw_tcp", "gw_mqtt"], + ) async def async_step_gw_serial( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Create config entry for a serial gateway.""" + gw_type = self._gw_type = CONF_GATEWAY_TYPE_SERIAL errors: dict[str, str] = {} + if user_input is not None: - errors.update( - await self.validate_common(CONF_GATEWAY_TYPE_SERIAL, errors, user_input) - ) + errors.update(await self.validate_common(gw_type, errors, user_input)) if not errors: return self._async_create_entry(user_input) user_input = user_input or {} - schema = _get_schema_common(user_input) - schema[ + schema = { + vol.Required( + CONF_DEVICE, default=user_input.get(CONF_DEVICE, "/dev/ttyACM0") + ): str, vol.Required( CONF_BAUD_RATE, default=user_input.get(CONF_BAUD_RATE, DEFAULT_BAUD_RATE), - ) - ] = cv.positive_int - schema[ - vol.Required( - CONF_DEVICE, default=user_input.get(CONF_DEVICE, "/dev/ttyACM0") - ) - ] = str + ): cv.positive_int, + } + schema.update(_get_schema_common(user_input)) schema = vol.Schema(schema) return self.async_show_form( @@ -174,30 +166,24 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Create a config entry for a tcp gateway.""" - errors = {} - if user_input is not None: - if CONF_TCP_PORT in user_input: - port: int = user_input[CONF_TCP_PORT] - if not (0 < port <= 65535): - errors[CONF_TCP_PORT] = "port_out_of_range" + gw_type = self._gw_type = CONF_GATEWAY_TYPE_TCP + errors: dict[str, str] = {} - errors.update( - await self.validate_common(CONF_GATEWAY_TYPE_TCP, errors, user_input) - ) + if user_input is not None: + errors.update(await self.validate_common(gw_type, errors, user_input)) if not errors: return self._async_create_entry(user_input) user_input = user_input or {} - schema = _get_schema_common(user_input) - schema[ - vol.Required(CONF_DEVICE, default=user_input.get(CONF_DEVICE, "127.0.0.1")) - ] = str - # Don't use cv.port as that would show a slider *facepalm* - schema[ + schema = { + vol.Required( + CONF_DEVICE, default=user_input.get(CONF_DEVICE, "127.0.0.1") + ): str, vol.Optional( CONF_TCP_PORT, default=user_input.get(CONF_TCP_PORT, DEFAULT_TCP_PORT) - ) - ] = vol.Coerce(int) + ): _PORT_SELECTOR, + } + schema.update(_get_schema_common(user_input)) schema = vol.Schema(schema) return self.async_show_form(step_id="gw_tcp", data_schema=schema, errors=errors) @@ -214,7 +200,13 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Create a config entry for a mqtt gateway.""" - errors = {} + # Naive check that doesn't consider config entry state. + if MQTT_DOMAIN not in self.hass.config.components: + return self.async_abort(reason="mqtt_required") + + gw_type = self._gw_type = CONF_GATEWAY_TYPE_MQTT + errors: dict[str, str] = {} + if user_input is not None: user_input[CONF_DEVICE] = MQTT_COMPONENT @@ -239,27 +231,21 @@ class MySensorsConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): elif self._check_topic_exists(user_input[CONF_TOPIC_OUT_PREFIX]): errors[CONF_TOPIC_OUT_PREFIX] = "duplicate_topic" - errors.update( - await self.validate_common(CONF_GATEWAY_TYPE_MQTT, errors, user_input) - ) + errors.update(await self.validate_common(gw_type, errors, user_input)) if not errors: return self._async_create_entry(user_input) user_input = user_input or {} - schema = _get_schema_common(user_input) - schema[ - vol.Required(CONF_RETAIN, default=user_input.get(CONF_RETAIN, True)) - ] = bool - schema[ + schema = { vol.Required( CONF_TOPIC_IN_PREFIX, default=user_input.get(CONF_TOPIC_IN_PREFIX, "") - ) - ] = str - schema[ + ): str, vol.Required( CONF_TOPIC_OUT_PREFIX, default=user_input.get(CONF_TOPIC_OUT_PREFIX, "") - ) - ] = str + ): str, + vol.Required(CONF_RETAIN, default=user_input.get(CONF_RETAIN, True)): bool, + } + schema.update(_get_schema_common(user_input)) schema = vol.Schema(schema) return self.async_show_form( diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index 32e2110dd95..42df81ae526 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -22,11 +22,6 @@ ConfGatewayType = Literal["Serial", "TCP", "MQTT"] CONF_GATEWAY_TYPE_SERIAL: ConfGatewayType = "Serial" CONF_GATEWAY_TYPE_TCP: ConfGatewayType = "TCP" CONF_GATEWAY_TYPE_MQTT: ConfGatewayType = "MQTT" -CONF_GATEWAY_TYPE_ALL: list[str] = [ - CONF_GATEWAY_TYPE_MQTT, - CONF_GATEWAY_TYPE_SERIAL, - CONF_GATEWAY_TYPE_TCP, -] DOMAIN: Final = "mysensors" MYSENSORS_GATEWAY_START_TASK: str = "mysensors_gateway_start_task_{}" diff --git a/homeassistant/components/mysensors/strings.json b/homeassistant/components/mysensors/strings.json index d7722e565cb..dc5dc76c7ae 100644 --- a/homeassistant/components/mysensors/strings.json +++ b/homeassistant/components/mysensors/strings.json @@ -7,6 +7,14 @@ }, "description": "Choose connection method to the gateway" }, + "select_gateway_type": { + "description": "Select which gateway to configure.", + "menu_options": { + "gw_mqtt": "Configure an MQTT gateway", + "gw_serial": "Configure a serial gateway", + "gw_tcp": "Configure a TCP gateway" + } + }, "gw_tcp": { "description": "Ethernet gateway setup", "data": { @@ -51,7 +59,6 @@ "invalid_serial": "Invalid serial port", "invalid_device": "Invalid device", "invalid_version": "Invalid MySensors version", - "mqtt_required": "The MQTT integration is not set up", "not_a_number": "Please enter a number", "port_out_of_range": "Port number must be at least 1 and at most 65535", "unknown": "[%key:common::config_flow::error::unknown%]" @@ -71,6 +78,7 @@ "invalid_serial": "Invalid serial port", "invalid_device": "Invalid device", "invalid_version": "Invalid MySensors version", + "mqtt_required": "The MQTT integration is not set up", "not_a_number": "Please enter a number", "port_out_of_range": "Port number must be at least 1 and at most 65535", "unknown": "[%key:common::config_flow::error::unknown%]" diff --git a/homeassistant/components/mysensors/translations/en.json b/homeassistant/components/mysensors/translations/en.json index 5ec81c22186..b85a28fb7d3 100644 --- a/homeassistant/components/mysensors/translations/en.json +++ b/homeassistant/components/mysensors/translations/en.json @@ -14,6 +14,7 @@ "invalid_serial": "Invalid serial port", "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_version": "Invalid MySensors version", + "mqtt_required": "The MQTT integration is not set up", "not_a_number": "Please enter a number", "port_out_of_range": "Port number must be at least 1 and at most 65535", "same_topic": "Subscribe and publish topics are the same", @@ -33,7 +34,6 @@ "invalid_serial": "Invalid serial port", "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_version": "Invalid MySensors version", - "mqtt_required": "The MQTT integration is not set up", "not_a_number": "Please enter a number", "port_out_of_range": "Port number must be at least 1 and at most 65535", "same_topic": "Subscribe and publish topics are the same", @@ -68,6 +68,14 @@ }, "description": "Ethernet gateway setup" }, + "select_gateway_type": { + "description": "Select which gateway to configure.", + "menu_options": { + "gw_mqtt": "Configure an MQTT gateway", + "gw_serial": "Configure a serial gateway", + "gw_tcp": "Configure a TCP gateway" + } + }, "user": { "data": { "gateway_type": "Gateway type" diff --git a/tests/components/mysensors/test_config_flow.py b/tests/components/mysensors/test_config_flow.py index e7808162043..e14059c4e4f 100644 --- a/tests/components/mysensors/test_config_flow.py +++ b/tests/components/mysensors/test_config_flow.py @@ -24,28 +24,32 @@ from homeassistant.components.mysensors.const import ( ConfGatewayType, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import FlowResult, FlowResultType from tests.common import MockConfigEntry +GATEWAY_TYPE_TO_STEP = { + CONF_GATEWAY_TYPE_TCP: "gw_tcp", + CONF_GATEWAY_TYPE_SERIAL: "gw_serial", + CONF_GATEWAY_TYPE_MQTT: "gw_mqtt", +} + async def get_form( - hass: HomeAssistant, gatway_type: ConfGatewayType, expected_step_id: str + hass: HomeAssistant, gateway_type: ConfGatewayType, expected_step_id: str ) -> FlowResult: """Get a form for the given gateway type.""" - stepuser = await hass.config_entries.flow.async_init( + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert stepuser["type"] == "form" - assert not stepuser["errors"] + assert result["type"] == FlowResultType.MENU result = await hass.config_entries.flow.async_configure( - stepuser["flow_id"], - {CONF_GATEWAY_TYPE: gatway_type}, + result["flow_id"], {"next_step_id": GATEWAY_TYPE_TO_STEP[gateway_type]} ) await hass.async_block_till_done() - assert result["type"] == "form" + assert result["type"] == FlowResultType.FORM assert result["step_id"] == expected_step_id return result @@ -62,7 +66,7 @@ async def test_config_mqtt(hass: HomeAssistant, mqtt: None) -> None: "homeassistant.components.mysensors.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( flow_id, { CONF_RETAIN: True, @@ -73,11 +77,11 @@ async def test_config_mqtt(hass: HomeAssistant, mqtt: None) -> None: ) await hass.async_block_till_done() - if "errors" in result2: - assert not result2["errors"] - assert result2["type"] == "create_entry" - assert result2["title"] == "mqtt" - assert result2["data"] == { + if "errors" in result: + assert not result["errors"] + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "mqtt" + assert result["data"] == { CONF_DEVICE: "mqtt", CONF_RETAIN: True, CONF_TOPIC_IN_PREFIX: "bla", @@ -91,20 +95,19 @@ async def test_config_mqtt(hass: HomeAssistant, mqtt: None) -> None: async def test_missing_mqtt(hass: HomeAssistant) -> None: """Test configuring a mqtt gateway without mqtt integration setup.""" - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" - assert not result["errors"] + assert result["type"] == FlowResultType.MENU result = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_GATEWAY_TYPE: CONF_GATEWAY_TYPE_MQTT}, + {"next_step_id": GATEWAY_TYPE_TO_STEP[CONF_GATEWAY_TYPE_MQTT]}, ) - assert result["step_id"] == "user" - assert result["type"] == "form" - assert result["errors"] == {"base": "mqtt_required"} + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "mqtt_required" async def test_config_serial(hass: HomeAssistant) -> None: @@ -123,7 +126,7 @@ async def test_config_serial(hass: HomeAssistant) -> None: "homeassistant.components.mysensors.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( flow_id, { CONF_BAUD_RATE: 115200, @@ -133,11 +136,11 @@ async def test_config_serial(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - if "errors" in result2: - assert not result2["errors"] - assert result2["type"] == "create_entry" - assert result2["title"] == "/dev/ttyACM0" - assert result2["data"] == { + if "errors" in result: + assert not result["errors"] + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "/dev/ttyACM0" + assert result["data"] == { CONF_DEVICE: "/dev/ttyACM0", CONF_BAUD_RATE: 115200, CONF_VERSION: "2.4", @@ -160,7 +163,7 @@ async def test_config_tcp(hass: HomeAssistant) -> None: "homeassistant.components.mysensors.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( flow_id, { CONF_TCP_PORT: 5003, @@ -170,11 +173,11 @@ async def test_config_tcp(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - if "errors" in result2: - assert not result2["errors"] - assert result2["type"] == "create_entry" - assert result2["title"] == "127.0.0.1" - assert result2["data"] == { + if "errors" in result: + assert not result["errors"] + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "127.0.0.1" + assert result["data"] == { CONF_DEVICE: "127.0.0.1", CONF_TCP_PORT: 5003, CONF_VERSION: "2.4", @@ -197,7 +200,7 @@ async def test_fail_to_connect(hass: HomeAssistant) -> None: "homeassistant.components.mysensors.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( flow_id, { CONF_TCP_PORT: 5003, @@ -207,9 +210,9 @@ async def test_fail_to_connect(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == "form" - assert "errors" in result2 - errors = result2["errors"] + assert result["type"] == FlowResultType.FORM + assert "errors" in result + errors = result["errors"] assert errors assert errors.get("base") == "cannot_connect" assert len(mock_setup.mock_calls) == 0 @@ -219,28 +222,6 @@ async def test_fail_to_connect(hass: HomeAssistant) -> None: @pytest.mark.parametrize( "gateway_type, expected_step_id, user_input, err_field, err_string", [ - ( - CONF_GATEWAY_TYPE_TCP, - "gw_tcp", - { - CONF_TCP_PORT: 600_000, - CONF_DEVICE: "127.0.0.1", - CONF_VERSION: "2.4", - }, - CONF_TCP_PORT, - "port_out_of_range", - ), - ( - CONF_GATEWAY_TYPE_TCP, - "gw_tcp", - { - CONF_TCP_PORT: 0, - CONF_DEVICE: "127.0.0.1", - CONF_VERSION: "2.4", - }, - CONF_TCP_PORT, - "port_out_of_range", - ), ( CONF_GATEWAY_TYPE_TCP, "gw_tcp", @@ -382,15 +363,15 @@ async def test_config_invalid( "homeassistant.components.mysensors.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( flow_id, user_input, ) await hass.async_block_till_done() - assert result2["type"] == "form" - assert "errors" in result2 - errors = result2["errors"] + assert result["type"] == FlowResultType.FORM + assert "errors" in result + errors = result["errors"] assert errors assert err_field in errors assert errors[err_field] == err_string @@ -681,10 +662,8 @@ async def test_duplicate( MockConfigEntry(domain=DOMAIN, data=first_input).add_to_hass(hass) second_gateway_type = second_input.pop(CONF_GATEWAY_TYPE) - result = await hass.config_entries.flow.async_init( - DOMAIN, - data={CONF_GATEWAY_TYPE: second_gateway_type}, - context={"source": config_entries.SOURCE_USER}, + result = await get_form( + hass, second_gateway_type, GATEWAY_TYPE_TO_STEP[second_gateway_type] ) result = await hass.config_entries.flow.async_configure( result["flow_id"], From 953d9eb9c86f5065067d35a60b50e78650077d29 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 7 Aug 2022 01:23:18 +0200 Subject: [PATCH 3179/3516] Bump aioopenexchangerates to 0.4.0 (#76356) --- homeassistant/components/openexchangerates/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openexchangerates/manifest.json b/homeassistant/components/openexchangerates/manifest.json index a795eaf8d5e..f2377478c5f 100644 --- a/homeassistant/components/openexchangerates/manifest.json +++ b/homeassistant/components/openexchangerates/manifest.json @@ -2,7 +2,7 @@ "domain": "openexchangerates", "name": "Open Exchange Rates", "documentation": "https://www.home-assistant.io/integrations/openexchangerates", - "requirements": ["aioopenexchangerates==0.3.0"], + "requirements": ["aioopenexchangerates==0.4.0"], "codeowners": ["@MartinHjelmare"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index ead73586d7a..5dbce196182 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -220,7 +220,7 @@ aionotion==3.0.2 aiooncue==0.3.4 # homeassistant.components.openexchangerates -aioopenexchangerates==0.3.0 +aioopenexchangerates==0.4.0 # homeassistant.components.acmeda aiopulse==0.4.3 From 4b53b920cb4c086fe8eb47f96f8fa3dafb9d9fa1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 7 Aug 2022 00:29:25 +0000 Subject: [PATCH 3180/3516] [ci skip] Translation update --- homeassistant/components/demo/translations/de.json | 13 ++++++++++++- homeassistant/components/demo/translations/fr.json | 10 ++++++++++ homeassistant/components/demo/translations/ru.json | 11 +++++++++++ .../components/deutsche_bahn/translations/fr.json | 7 +++++++ .../components/deutsche_bahn/translations/ru.json | 8 ++++++++ .../components/mysensors/translations/en.json | 1 + 6 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/deutsche_bahn/translations/fr.json create mode 100644 homeassistant/components/deutsche_bahn/translations/ru.json diff --git a/homeassistant/components/demo/translations/de.json b/homeassistant/components/demo/translations/de.json index ab06043d52c..8f5950f49f8 100644 --- a/homeassistant/components/demo/translations/de.json +++ b/homeassistant/components/demo/translations/de.json @@ -1,10 +1,21 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Dr\u00fccke SENDEN, um zu best\u00e4tigen, dass das Netzteil ausgetauscht wurde", + "title": "Das Netzteil muss ausgetauscht werden" + } + } + }, + "title": "Das Netzteil ist nicht stabil" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { "confirm": { - "description": "Dr\u00fccke OK, wenn die Blinkerfl\u00fcssigkeit nachgef\u00fcllt wurde.", + "description": "Dr\u00fccke SENDEN, wenn die Blinkerfl\u00fcssigkeit nachgef\u00fcllt wurde", "title": "Blinkerfl\u00fcssigkeit muss nachgef\u00fcllt werden" } } diff --git a/homeassistant/components/demo/translations/fr.json b/homeassistant/components/demo/translations/fr.json index c5a150d8cb6..a09a7a1fd2f 100644 --- a/homeassistant/components/demo/translations/fr.json +++ b/homeassistant/components/demo/translations/fr.json @@ -1,5 +1,15 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "title": "L'alimentation \u00e9lectrique doit \u00eatre remplac\u00e9e" + } + } + }, + "title": "L'alimentation \u00e9lectrique n'est pas stable" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/ru.json b/homeassistant/components/demo/translations/ru.json index 3c919e12d84..29b4229dacb 100644 --- a/homeassistant/components/demo/translations/ru.json +++ b/homeassistant/components/demo/translations/ru.json @@ -1,5 +1,16 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\" \u0434\u043b\u044f \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f \u0437\u0430\u043c\u0435\u043d\u044b \u0431\u043b\u043e\u043a\u0430 \u043f\u0438\u0442\u0430\u043d\u0438\u044f", + "title": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u043f\u0438\u0442\u0430\u043d\u0438\u044f" + } + } + }, + "title": "\u0418\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u043f\u0438\u0442\u0430\u043d\u0438\u044f \u043d\u0435 \u0441\u0442\u0430\u0431\u0438\u043b\u0435\u043d" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/deutsche_bahn/translations/fr.json b/homeassistant/components/deutsche_bahn/translations/fr.json new file mode 100644 index 00000000000..a3e3b26fa78 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/fr.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "L'int\u00e9gration Deutsche Bahn est en cours de suppression" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/ru.json b/homeassistant/components/deutsche_bahn/translations/ru.json new file mode 100644 index 00000000000..2cfbb695fc3 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/ru.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Deutsche Bahn \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.11. \n\n\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0430 \u043d\u0430 \u0432\u0435\u0431-\u0441\u043a\u0440\u0430\u043f\u0438\u043d\u0433\u0435, \u0447\u0442\u043e \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043e. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e YAML \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Deutsche Bahn \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/en.json b/homeassistant/components/mysensors/translations/en.json index b85a28fb7d3..081ae3a2b95 100644 --- a/homeassistant/components/mysensors/translations/en.json +++ b/homeassistant/components/mysensors/translations/en.json @@ -34,6 +34,7 @@ "invalid_serial": "Invalid serial port", "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_version": "Invalid MySensors version", + "mqtt_required": "The MQTT integration is not set up", "not_a_number": "Please enter a number", "port_out_of_range": "Port number must be at least 1 and at most 65535", "same_topic": "Subscribe and publish topics are the same", From db3e21df86d299d59e0c8ea4a3aeea1627837abc Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Sun, 7 Aug 2022 11:02:32 +1000 Subject: [PATCH 3181/3516] Update aiolifx to version 0.8.2 (#76367) --- homeassistant/components/lifx/__init__.py | 2 +- homeassistant/components/lifx/config_flow.py | 2 +- homeassistant/components/lifx/coordinator.py | 2 +- homeassistant/components/lifx/manifest.json | 6 +----- requirements_all.txt | 5 +---- requirements_test_all.txt | 5 +---- 6 files changed, 6 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 3faf69483d5..9d4e2d5facf 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -8,7 +8,7 @@ import socket from typing import Any from aiolifx.aiolifx import Light -from aiolifx_connection import LIFXConnection +from aiolifx.connection import LIFXConnection import voluptuous as vol from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN diff --git a/homeassistant/components/lifx/config_flow.py b/homeassistant/components/lifx/config_flow.py index 30b42e640f8..daa917dc847 100644 --- a/homeassistant/components/lifx/config_flow.py +++ b/homeassistant/components/lifx/config_flow.py @@ -6,7 +6,7 @@ import socket from typing import Any from aiolifx.aiolifx import Light -from aiolifx_connection import LIFXConnection +from aiolifx.connection import LIFXConnection import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index 87ba46e94d1..bb3dd60b326 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -7,7 +7,7 @@ from functools import partial from typing import cast from aiolifx.aiolifx import Light -from aiolifx_connection import LIFXConnection +from aiolifx.connection import LIFXConnection from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index ebc4d73ce5d..83408f87bb5 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -3,11 +3,7 @@ "name": "LIFX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lifx", - "requirements": [ - "aiolifx==0.8.1", - "aiolifx_effects==0.2.2", - "aiolifx-connection==1.0.0" - ], + "requirements": ["aiolifx==0.8.2", "aiolifx_effects==0.2.2"], "quality_scale": "platinum", "dependencies": ["network"], "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 5dbce196182..8dbcab9d520 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -187,10 +187,7 @@ aiokafka==0.7.2 aiokef==0.2.16 # homeassistant.components.lifx -aiolifx-connection==1.0.0 - -# homeassistant.components.lifx -aiolifx==0.8.1 +aiolifx==0.8.2 # homeassistant.components.lifx aiolifx_effects==0.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e27714f81f8..eba56137019 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -165,10 +165,7 @@ aiohue==4.4.2 aiokafka==0.7.2 # homeassistant.components.lifx -aiolifx-connection==1.0.0 - -# homeassistant.components.lifx -aiolifx==0.8.1 +aiolifx==0.8.2 # homeassistant.components.lifx aiolifx_effects==0.2.2 From 74cfdc6c1fa1e9fb4fa7acb6b4d06401f7ae66b6 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Sun, 7 Aug 2022 13:28:30 +1000 Subject: [PATCH 3182/3516] Add identify and restart button entities to the LIFX integration (#75568) --- homeassistant/components/lifx/__init__.py | 3 +- homeassistant/components/lifx/button.py | 77 +++++++++++ homeassistant/components/lifx/const.py | 15 +++ homeassistant/components/lifx/coordinator.py | 29 +++- homeassistant/components/lifx/entity.py | 28 ++++ homeassistant/components/lifx/light.py | 30 +---- tests/components/lifx/__init__.py | 5 +- tests/components/lifx/test_button.py | 132 +++++++++++++++++++ 8 files changed, 292 insertions(+), 27 deletions(-) create mode 100644 homeassistant/components/lifx/button.py create mode 100644 homeassistant/components/lifx/entity.py create mode 100644 tests/components/lifx/test_button.py diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index 9d4e2d5facf..ec54382ec40 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -57,7 +57,7 @@ CONFIG_SCHEMA = vol.All( ) -PLATFORMS = [Platform.LIGHT] +PLATFORMS = [Platform.BUTTON, Platform.LIGHT] DISCOVERY_INTERVAL = timedelta(minutes=15) MIGRATION_INTERVAL = timedelta(minutes=5) @@ -173,7 +173,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up LIFX from a config entry.""" - if async_entry_is_legacy(entry): return True diff --git a/homeassistant/components/lifx/button.py b/homeassistant/components/lifx/button.py new file mode 100644 index 00000000000..6d3f4fe51bf --- /dev/null +++ b/homeassistant/components/lifx/button.py @@ -0,0 +1,77 @@ +"""Button entity for LIFX devices..""" +from __future__ import annotations + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, IDENTIFY, RESTART +from .coordinator import LIFXUpdateCoordinator +from .entity import LIFXEntity + +RESTART_BUTTON_DESCRIPTION = ButtonEntityDescription( + key=RESTART, + name="Restart", + device_class=ButtonDeviceClass.RESTART, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, +) + +IDENTIFY_BUTTON_DESCRIPTION = ButtonEntityDescription( + key=IDENTIFY, + name="Identify", + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up LIFX from a config entry.""" + domain_data = hass.data[DOMAIN] + coordinator: LIFXUpdateCoordinator = domain_data[entry.entry_id] + async_add_entities( + cls(coordinator) for cls in (LIFXRestartButton, LIFXIdentifyButton) + ) + + +class LIFXButton(LIFXEntity, ButtonEntity): + """Base LIFX button.""" + + _attr_has_entity_name: bool = True + + def __init__(self, coordinator: LIFXUpdateCoordinator) -> None: + """Initialise a LIFX button.""" + super().__init__(coordinator) + self._attr_unique_id = ( + f"{coordinator.serial_number}_{self.entity_description.key}" + ) + + +class LIFXRestartButton(LIFXButton): + """LIFX restart button.""" + + entity_description = RESTART_BUTTON_DESCRIPTION + + async def async_press(self) -> None: + """Restart the bulb on button press.""" + self.bulb.set_reboot() + + +class LIFXIdentifyButton(LIFXButton): + """LIFX identify button.""" + + entity_description = IDENTIFY_BUTTON_DESCRIPTION + + async def async_press(self) -> None: + """Identify the bulb by flashing it when the button is pressed.""" + await self.coordinator.async_identify_bulb() diff --git a/homeassistant/components/lifx/const.py b/homeassistant/components/lifx/const.py index ec756c2091f..f6ec653c994 100644 --- a/homeassistant/components/lifx/const.py +++ b/homeassistant/components/lifx/const.py @@ -14,6 +14,21 @@ UNAVAILABLE_GRACE = 90 CONF_SERIAL = "serial" +IDENTIFY_WAVEFORM = { + "transient": True, + "color": [0, 0, 1, 3500], + "skew_ratio": 0, + "period": 1000, + "cycles": 3, + "waveform": 1, + "set_hue": True, + "set_saturation": True, + "set_brightness": True, + "set_kelvin": True, +} +IDENTIFY = "identify" +RESTART = "restart" + DATA_LIFX_MANAGER = "lifx_manager" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index bb3dd60b326..1f3f49368ca 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from datetime import timedelta from functools import partial -from typing import cast +from typing import Any, cast from aiolifx.aiolifx import Light from aiolifx.connection import LIFXConnection @@ -15,6 +15,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import ( _LOGGER, + IDENTIFY_WAVEFORM, MESSAGE_RETRIES, MESSAGE_TIMEOUT, TARGET_ANY, @@ -75,6 +76,24 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): self.device.host_firmware_version, ) + @property + def label(self) -> str: + """Return the label of the bulb.""" + return cast(str, self.device.label) + + async def async_identify_bulb(self) -> None: + """Identify the device by flashing it three times.""" + bulb: Light = self.device + if bulb.power_level: + # just flash the bulb for three seconds + await self.async_set_waveform_optional(value=IDENTIFY_WAVEFORM) + return + # Turn the bulb on first, flash for 3 seconds, then turn off + await self.async_set_power(state=True, duration=1) + await self.async_set_waveform_optional(value=IDENTIFY_WAVEFORM) + await asyncio.sleep(3) + await self.async_set_power(state=False, duration=1) + async def _async_update_data(self) -> None: """Fetch all device data from the api.""" async with self.lock: @@ -119,6 +138,14 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator): if zone == top - 1: zone -= 1 + async def async_set_waveform_optional( + self, value: dict[str, Any], rapid: bool = False + ) -> None: + """Send a set_waveform_optional message to the device.""" + await async_execute_lifx( + partial(self.device.set_waveform_optional, value=value, rapid=rapid) + ) + async def async_get_color(self) -> None: """Send a get color message to the device.""" await async_execute_lifx(self.device.get_color) diff --git a/homeassistant/components/lifx/entity.py b/homeassistant/components/lifx/entity.py new file mode 100644 index 00000000000..0007ab998a9 --- /dev/null +++ b/homeassistant/components/lifx/entity.py @@ -0,0 +1,28 @@ +"""Support for LIFX lights.""" +from __future__ import annotations + +from aiolifx import products + +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import LIFXUpdateCoordinator + + +class LIFXEntity(CoordinatorEntity[LIFXUpdateCoordinator]): + """Representation of a LIFX entity with a coordinator.""" + + def __init__(self, coordinator: LIFXUpdateCoordinator) -> None: + """Initialise the light.""" + super().__init__(coordinator) + self.bulb = coordinator.device + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.serial_number)}, + connections={(dr.CONNECTION_NETWORK_MAC, coordinator.mac_address)}, + manufacturer="LIFX", + name=coordinator.label, + model=products.product_map.get(self.bulb.product, "LIFX Bulb"), + sw_version=self.bulb.host_firmware_version, + ) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 28a678d5e8f..67bb3e91748 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -6,7 +6,6 @@ from datetime import datetime, timedelta import math from typing import Any -from aiolifx import products import aiolifx_effects as aiolifx_effects_module import voluptuous as vol @@ -20,20 +19,18 @@ from homeassistant.components.light import ( LightEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODEL, ATTR_SW_VERSION +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv -import homeassistant.helpers.device_registry as dr -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.update_coordinator import CoordinatorEntity import homeassistant.util.color as color_util from .const import DATA_LIFX_MANAGER, DOMAIN from .coordinator import LIFXUpdateCoordinator +from .entity import LIFXEntity from .manager import ( SERVICE_EFFECT_COLORLOOP, SERVICE_EFFECT_PULSE, @@ -92,7 +89,7 @@ async def async_setup_entry( async_add_entities([entity]) -class LIFXLight(CoordinatorEntity[LIFXUpdateCoordinator], LightEntity): +class LIFXLight(LIFXEntity, LightEntity): """Representation of a LIFX light.""" _attr_supported_features = LightEntityFeature.TRANSITION | LightEntityFeature.EFFECT @@ -105,10 +102,9 @@ class LIFXLight(CoordinatorEntity[LIFXUpdateCoordinator], LightEntity): ) -> None: """Initialize the light.""" super().__init__(coordinator) - bulb = coordinator.device - self.mac_addr = bulb.mac_addr - self.bulb = bulb - bulb_features = lifx_features(bulb) + + self.mac_addr = self.bulb.mac_addr + bulb_features = lifx_features(self.bulb) self.manager = manager self.effects_conductor: aiolifx_effects_module.Conductor = ( manager.effects_conductor @@ -116,25 +112,13 @@ class LIFXLight(CoordinatorEntity[LIFXUpdateCoordinator], LightEntity): self.postponed_update: CALLBACK_TYPE | None = None self.entry = entry self._attr_unique_id = self.coordinator.serial_number - self._attr_name = bulb.label + self._attr_name = self.bulb.label self._attr_min_mireds = math.floor( color_util.color_temperature_kelvin_to_mired(bulb_features["max_kelvin"]) ) self._attr_max_mireds = math.ceil( color_util.color_temperature_kelvin_to_mired(bulb_features["min_kelvin"]) ) - info = DeviceInfo( - identifiers={(DOMAIN, coordinator.serial_number)}, - connections={(dr.CONNECTION_NETWORK_MAC, coordinator.mac_address)}, - manufacturer="LIFX", - name=self.name, - ) - _map = products.product_map - if (model := (_map.get(bulb.product) or bulb.product)) is not None: - info[ATTR_MODEL] = str(model) - if (version := bulb.host_firmware_version) is not None: - info[ATTR_SW_VERSION] = version - self._attr_device_info = info if bulb_features["min_kelvin"] != bulb_features["max_kelvin"]: color_mode = ColorMode.COLOR_TEMP else: diff --git a/tests/components/lifx/__init__.py b/tests/components/lifx/__init__.py index fdea992c87d..8259314e77c 100644 --- a/tests/components/lifx/__init__.py +++ b/tests/components/lifx/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio from contextlib import contextmanager -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch from aiolifx.aiolifx import Light @@ -72,6 +72,8 @@ def _mocked_bulb() -> Light: bulb.label = LABEL bulb.color = [1, 2, 3, 4] bulb.power_level = 0 + bulb.fire_and_forget = AsyncMock() + bulb.set_reboot = Mock() bulb.try_sending = AsyncMock() bulb.set_infrared = MockLifxCommand(bulb) bulb.get_color = MockLifxCommand(bulb) @@ -79,6 +81,7 @@ def _mocked_bulb() -> Light: bulb.set_color = MockLifxCommand(bulb) bulb.get_hostfirmware = MockLifxCommand(bulb) bulb.get_version = MockLifxCommand(bulb) + bulb.set_waveform_optional = MockLifxCommand(bulb) bulb.product = 1 # LIFX Original 1000 return bulb diff --git a/tests/components/lifx/test_button.py b/tests/components/lifx/test_button.py new file mode 100644 index 00000000000..a485c882100 --- /dev/null +++ b/tests/components/lifx/test_button.py @@ -0,0 +1,132 @@ +"""Tests for button platform.""" +from homeassistant.components import lifx +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN +from homeassistant.components.lifx.const import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from . import ( + DEFAULT_ENTRY_TITLE, + IP_ADDRESS, + MAC_ADDRESS, + SERIAL, + _mocked_bulb, + _patch_config_flow_try_connect, + _patch_device, + _patch_discovery, +) + +from tests.common import MockConfigEntry + + +async def test_button_restart(hass: HomeAssistant) -> None: + """Test that a bulb can be restarted.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_ENTRY_TITLE, + data={CONF_HOST: IP_ADDRESS}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + unique_id = f"{SERIAL}_restart" + entity_id = "button.my_bulb_restart" + + entity_registry = er.async_get(hass) + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.disabled + assert entity.unique_id == unique_id + + enabled_entity = entity_registry.async_update_entity(entity_id, disabled_by=None) + assert not enabled_entity.disabled + + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + await hass.services.async_call( + BUTTON_DOMAIN, "press", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + + bulb.set_reboot.assert_called_once() + + +async def test_button_identify(hass: HomeAssistant) -> None: + """Test that a bulb can be identified.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title=DEFAULT_ENTRY_TITLE, + data={CONF_HOST: IP_ADDRESS}, + unique_id=MAC_ADDRESS, + ) + config_entry.add_to_hass(hass) + bulb = _mocked_bulb() + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) + await hass.async_block_till_done() + + unique_id = f"{SERIAL}_identify" + entity_id = "button.my_bulb_identify" + + entity_registry = er.async_get(hass) + entity = entity_registry.async_get(entity_id) + assert entity + assert entity.disabled + assert entity.unique_id == unique_id + + enabled_entity = entity_registry.async_update_entity(entity_id, disabled_by=None) + assert not enabled_entity.disabled + + with _patch_discovery(device=bulb), _patch_config_flow_try_connect( + device=bulb + ), _patch_device(device=bulb): + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + await hass.services.async_call( + BUTTON_DOMAIN, "press", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + + assert len(bulb.set_power.calls) == 2 + + waveform_call_dict = bulb.set_waveform_optional.calls[0][1] + waveform_call_dict.pop("callb") + assert waveform_call_dict == { + "rapid": False, + "value": { + "transient": True, + "color": [0, 0, 1, 3500], + "skew_ratio": 0, + "period": 1000, + "cycles": 3, + "waveform": 1, + "set_hue": True, + "set_saturation": True, + "set_brightness": True, + "set_kelvin": True, + }, + } + + bulb.set_power.reset_mock() + bulb.set_waveform_optional.reset_mock() + bulb.power_level = 65535 + + await hass.services.async_call( + BUTTON_DOMAIN, "press", {ATTR_ENTITY_ID: entity_id}, blocking=True + ) + + assert len(bulb.set_waveform_optional.calls) == 1 + assert len(bulb.set_power.calls) == 0 From 34984a8af8efc5ef6d1d204404c517e7f7c2d1bb Mon Sep 17 00:00:00 2001 From: Leonardo Figueiro Date: Sun, 7 Aug 2022 06:07:01 -0300 Subject: [PATCH 3183/3516] Add switch to wilight (#62873) * Created switch.py and support * updated support.py * test for wilight switch * Update for Test * Updated test_switch.py * Trigger service with index * Updated support.py and switch.py * Updated support.py * Updated switch.py as PR#63614 * Updated switch.py * add type hints * Updated support.py * Updated switch.py * Updated switch.py and services.yaml * Updated pywilight * Update homeassistant/components/wilight/switch.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/wilight/switch.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/wilight/switch.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/wilight/switch.py Co-authored-by: Martin Hjelmare * Update ci.yaml * Update ci.yaml * Updated as pywilight Renamed Device as PyWiLightDevice in pywilight. * Updated as pywilight Renamed Device as PyWiLightDevice in pywilight. * Updated as pywilight Renamed Device as PyWiLightDevice in pywilight. * Updated as pywilight Renamed Device as PyWiLightDevice in pywilight. * Update switch.py * Update homeassistant/components/wilight/support.py Co-authored-by: Martin Hjelmare * Update support.py * Update switch.py * Update support.py * Update support.py * Update switch.py * Update switch.py * Update services.yaml * Update switch.py * Update services.yaml * Update switch.py * Update homeassistant/components/wilight/switch.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/wilight/switch.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/wilight/switch.py Co-authored-by: Martin Hjelmare * Update switch.py * Update switch.py * Update switch.py * Update test_switch.py * Update test_switch.py * Update test_switch.py * Decrease exception scope * Clean up Co-authored-by: Martin Hjelmare --- homeassistant/components/wilight/__init__.py | 4 +- .../components/wilight/config_flow.py | 2 +- homeassistant/components/wilight/fan.py | 2 +- homeassistant/components/wilight/light.py | 2 +- .../components/wilight/manifest.json | 2 +- .../components/wilight/parent_device.py | 2 +- .../components/wilight/services.yaml | 24 ++ homeassistant/components/wilight/support.py | 87 +++++ homeassistant/components/wilight/switch.py | 322 ++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wilight/__init__.py | 1 + tests/components/wilight/test_switch.py | 264 ++++++++++++++ 13 files changed, 707 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/wilight/services.yaml create mode 100644 homeassistant/components/wilight/support.py create mode 100644 homeassistant/components/wilight/switch.py create mode 100644 tests/components/wilight/test_switch.py diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index fefde1644ad..326265b8b3f 100644 --- a/homeassistant/components/wilight/__init__.py +++ b/homeassistant/components/wilight/__init__.py @@ -2,7 +2,7 @@ from typing import Any -from pywilight.wilight_device import Device as PyWiLightDevice +from pywilight.wilight_device import PyWiLightDevice from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -15,7 +15,7 @@ from .parent_device import WiLightParent DOMAIN = "wilight" # List the platforms that you want to support. -PLATFORMS = [Platform.COVER, Platform.FAN, Platform.LIGHT] +PLATFORMS = [Platform.COVER, Platform.FAN, Platform.LIGHT, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/wilight/config_flow.py b/homeassistant/components/wilight/config_flow.py index dc8e5fc39cc..b7df1932cab 100644 --- a/homeassistant/components/wilight/config_flow.py +++ b/homeassistant/components/wilight/config_flow.py @@ -16,7 +16,7 @@ CONF_MODEL_NAME = "model_name" WILIGHT_MANUFACTURER = "All Automacao Ltda" # List the components supported by this integration. -ALLOWED_WILIGHT_COMPONENTS = ["cover", "fan", "light"] +ALLOWED_WILIGHT_COMPONENTS = ["cover", "fan", "light", "switch"] class WiLightFlowHandler(ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/wilight/fan.py b/homeassistant/components/wilight/fan.py index c598e6db397..3d0c6d0ff39 100644 --- a/homeassistant/components/wilight/fan.py +++ b/homeassistant/components/wilight/fan.py @@ -13,7 +13,7 @@ from pywilight.const import ( WL_SPEED_LOW, WL_SPEED_MEDIUM, ) -from pywilight.wilight_device import Device as PyWiLightDevice +from pywilight.wilight_device import PyWiLightDevice from homeassistant.components.fan import DIRECTION_FORWARD, FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/wilight/light.py b/homeassistant/components/wilight/light.py index ea9e19dcb30..2509dc50737 100644 --- a/homeassistant/components/wilight/light.py +++ b/homeassistant/components/wilight/light.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from pywilight.const import ITEM_LIGHT, LIGHT_COLOR, LIGHT_DIMMER, LIGHT_ON_OFF -from pywilight.wilight_device import Device as PyWiLightDevice +from pywilight.wilight_device import PyWiLightDevice from homeassistant.components.light import ( ATTR_BRIGHTNESS, diff --git a/homeassistant/components/wilight/manifest.json b/homeassistant/components/wilight/manifest.json index 972de72a9c9..b1be0c80122 100644 --- a/homeassistant/components/wilight/manifest.json +++ b/homeassistant/components/wilight/manifest.json @@ -3,7 +3,7 @@ "name": "WiLight", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wilight", - "requirements": ["pywilight==0.0.70"], + "requirements": ["pywilight==0.0.74"], "ssdp": [ { "manufacturer": "All Automacao Ltda" diff --git a/homeassistant/components/wilight/parent_device.py b/homeassistant/components/wilight/parent_device.py index 17a33fef633..8091e78cc76 100644 --- a/homeassistant/components/wilight/parent_device.py +++ b/homeassistant/components/wilight/parent_device.py @@ -5,7 +5,7 @@ import asyncio import logging import pywilight -from pywilight.wilight_device import Device as PyWiLightDevice +from pywilight.wilight_device import PyWiLightDevice import requests from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/wilight/services.yaml b/homeassistant/components/wilight/services.yaml new file mode 100644 index 00000000000..07a545bd5d7 --- /dev/null +++ b/homeassistant/components/wilight/services.yaml @@ -0,0 +1,24 @@ +set_watering_time: + description: Set watering time + target: + fields: + watering_time: + description: Duration for this irrigation to be turned on + example: 30 +set_pause_time: + description: Set pause time + target: + fields: + pause_time: + description: Duration for this irrigation to be paused + example: 24 +set_trigger: + description: Set trigger + target: + fields: + trigger_index: + description: Index of Trigger from 1 to 4 + example: "1" + trigger: + description: Configuration of trigger + example: "'12707001'" diff --git a/homeassistant/components/wilight/support.py b/homeassistant/components/wilight/support.py new file mode 100644 index 00000000000..6a03a854c70 --- /dev/null +++ b/homeassistant/components/wilight/support.py @@ -0,0 +1,87 @@ +"""Support for config validation using voluptuous and Translate Trigger.""" +from __future__ import annotations + +import calendar +import locale +import re +from typing import Any + +import voluptuous as vol + + +def wilight_trigger(value: Any) -> str | None: + """Check rules for WiLight Trigger.""" + step = 1 + err_desc = "Value is None" + result_128 = False + result_24 = False + result_60 = False + result_2 = False + + if value is not None: + step = 2 + err_desc = "Expected a string" + + if (step == 2) & isinstance(value, str): + step = 3 + err_desc = "String should only contain 8 decimals character" + if re.search(r"^([0-9]{8})$", value) is not None: + step = 4 + err_desc = "First 3 character should be less than 128" + result_128 = int(value[0:3]) < 128 + result_24 = int(value[3:5]) < 24 + result_60 = int(value[5:7]) < 60 + result_2 = int(value[7:8]) < 2 + + if (step == 4) & result_128: + step = 5 + err_desc = "Hour part should be less than 24" + + if (step == 5) & result_24: + step = 6 + err_desc = "Minute part should be less than 60" + + if (step == 6) & result_60: + step = 7 + err_desc = "Active part shoul be less than 2" + + if (step == 7) & result_2: + return value + + raise vol.Invalid(err_desc) + + +def wilight_to_hass_trigger(value: str | None) -> str | None: + """Convert wilight trigger to hass description. + + Ex: "12719001" -> "sun mon tue wed thu fri sat 19:00 On" + "00000000" -> "00:00 Off" + """ + if value is None: + return value + + locale.setlocale(locale.LC_ALL, "") + week_days = list(calendar.day_abbr) + days = bin(int(value[0:3]))[2:].zfill(8) + desc = "" + if int(days[7:8]) == 1: + desc += f"{week_days[6]} " + if int(days[6:7]) == 1: + desc += f"{week_days[0]} " + if int(days[5:6]) == 1: + desc += f"{week_days[1]} " + if int(days[4:5]) == 1: + desc += f"{week_days[2]} " + if int(days[3:4]) == 1: + desc += f"{week_days[3]} " + if int(days[2:3]) == 1: + desc += f"{week_days[4]} " + if int(days[1:2]) == 1: + desc += f"{week_days[5]} " + desc += f"{value[3:5]}:{value[5:7]} " + if int(value[7:8]) == 1: + desc += "On" + else: + desc += "Off" + + return desc diff --git a/homeassistant/components/wilight/switch.py b/homeassistant/components/wilight/switch.py new file mode 100644 index 00000000000..920fb66908c --- /dev/null +++ b/homeassistant/components/wilight/switch.py @@ -0,0 +1,322 @@ +"""Support for WiLight switches.""" +from __future__ import annotations + +from typing import Any + +from pywilight.const import ITEM_SWITCH, SWITCH_PAUSE_VALVE, SWITCH_VALVE +from pywilight.wilight_device import PyWiLightDevice +import voluptuous as vol + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_platform +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import DOMAIN, WiLightDevice +from .parent_device import WiLightParent +from .support import wilight_to_hass_trigger, wilight_trigger as wl_trigger + +# Attr of features supported by the valve switch entities +ATTR_WATERING_TIME = "watering_time" +ATTR_PAUSE_TIME = "pause_time" +ATTR_TRIGGER_1 = "trigger_1" +ATTR_TRIGGER_2 = "trigger_2" +ATTR_TRIGGER_3 = "trigger_3" +ATTR_TRIGGER_4 = "trigger_4" +ATTR_TRIGGER_1_DESC = "trigger_1_description" +ATTR_TRIGGER_2_DESC = "trigger_2_description" +ATTR_TRIGGER_3_DESC = "trigger_3_description" +ATTR_TRIGGER_4_DESC = "trigger_4_description" + +# Attr of services data supported by the valve switch entities +ATTR_TRIGGER = "trigger" +ATTR_TRIGGER_INDEX = "trigger_index" + +# Service of features supported by the valve switch entities +SERVICE_SET_WATERING_TIME = "set_watering_time" +SERVICE_SET_PAUSE_TIME = "set_pause_time" +SERVICE_SET_TRIGGER = "set_trigger" + +# Range of features supported by the valve switch entities +RANGE_WATERING_TIME = 1800 +RANGE_PAUSE_TIME = 24 +RANGE_TRIGGER_INDEX = 4 + +# Service call validation schemas +VALID_WATERING_TIME = vol.All( + vol.Coerce(int), vol.Range(min=1, max=RANGE_WATERING_TIME) +) +VALID_PAUSE_TIME = vol.All(vol.Coerce(int), vol.Range(min=1, max=RANGE_PAUSE_TIME)) +VALID_TRIGGER_INDEX = vol.All( + vol.Coerce(int), vol.Range(min=1, max=RANGE_TRIGGER_INDEX) +) + +# Descriptions of the valve switch entities +DESC_WATERING = "watering" +DESC_PAUSE = "pause" + +# Icons of the valve switch entities +ICON_WATERING = "mdi:water" +ICON_PAUSE = "mdi:pause-circle-outline" + + +def entities_from_discovered_wilight(api_device: PyWiLightDevice) -> tuple[Any]: + """Parse configuration and add WiLight switch entities.""" + entities: Any = [] + for item in api_device.items: + if item["type"] == ITEM_SWITCH: + index = item["index"] + item_name = item["name"] + if item["sub_type"] == SWITCH_VALVE: + entities.append(WiLightValveSwitch(api_device, index, item_name)) + elif item["sub_type"] == SWITCH_PAUSE_VALVE: + entities.append(WiLightValvePauseSwitch(api_device, index, item_name)) + + return entities + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up WiLight switches from a config entry.""" + parent: WiLightParent = hass.data[DOMAIN][entry.entry_id] + + # Handle a discovered WiLight device. + assert parent.api + entities = entities_from_discovered_wilight(parent.api) + async_add_entities(entities) + + # Handle services for a discovered WiLight device. + async def set_watering_time(entity, service: Any) -> None: + if not isinstance(entity, WiLightValveSwitch): + raise ValueError("Entity is not a WiLight valve switch") + watering_time = service.data[ATTR_WATERING_TIME] + await entity.async_set_watering_time(watering_time=watering_time) + + async def set_trigger(entity, service: Any) -> None: + if not isinstance(entity, WiLightValveSwitch): + raise ValueError("Entity is not a WiLight valve switch") + trigger_index = service.data[ATTR_TRIGGER_INDEX] + trigger = service.data[ATTR_TRIGGER] + await entity.async_set_trigger(trigger_index=trigger_index, trigger=trigger) + + async def set_pause_time(entity, service: Any) -> None: + if not isinstance(entity, WiLightValvePauseSwitch): + raise ValueError("Entity is not a WiLight valve pause switch") + pause_time = service.data[ATTR_PAUSE_TIME] + await entity.async_set_pause_time(pause_time=pause_time) + + platform = entity_platform.async_get_current_platform() + + platform.async_register_entity_service( + SERVICE_SET_WATERING_TIME, + { + vol.Required(ATTR_WATERING_TIME): VALID_WATERING_TIME, + }, + set_watering_time, + ) + + platform.async_register_entity_service( + SERVICE_SET_TRIGGER, + { + vol.Required(ATTR_TRIGGER_INDEX): VALID_TRIGGER_INDEX, + vol.Required(ATTR_TRIGGER): wl_trigger, + }, + set_trigger, + ) + + platform.async_register_entity_service( + SERVICE_SET_PAUSE_TIME, + { + vol.Required(ATTR_PAUSE_TIME): VALID_PAUSE_TIME, + }, + set_pause_time, + ) + + +def wilight_to_hass_pause_time(value: int) -> int: + """Convert wilight pause_time seconds to hass hour.""" + return round(value / 3600) + + +def hass_to_wilight_pause_time(value: int) -> int: + """Convert hass pause_time hours to wilight seconds.""" + return round(value * 3600) + + +class WiLightValveSwitch(WiLightDevice, SwitchEntity): + """Representation of a WiLights Valve switch.""" + + @property + def name(self) -> str: + """Return the name of the switch.""" + return f"{self._attr_name} {DESC_WATERING}" + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return self._status.get("on", False) + + @property + def watering_time(self) -> int | None: + """Return watering time of valve switch. + + None is unknown, 1 is minimum, 1800 is maximum. + """ + return self._status.get("timer_target") + + @property + def trigger_1(self) -> str | None: + """Return trigger_1 of valve switch.""" + return self._status.get("trigger_1") + + @property + def trigger_2(self) -> str | None: + """Return trigger_2 of valve switch.""" + return self._status.get("trigger_2") + + @property + def trigger_3(self) -> str | None: + """Return trigger_3 of valve switch.""" + return self._status.get("trigger_3") + + @property + def trigger_4(self) -> str | None: + """Return trigger_4 of valve switch.""" + return self._status.get("trigger_4") + + @property + def trigger_1_description(self) -> str | None: + """Return trigger_1_description of valve switch.""" + return wilight_to_hass_trigger(self._status.get("trigger_1")) + + @property + def trigger_2_description(self) -> str | None: + """Return trigger_2_description of valve switch.""" + return wilight_to_hass_trigger(self._status.get("trigger_2")) + + @property + def trigger_3_description(self) -> str | None: + """Return trigger_3_description of valve switch.""" + return wilight_to_hass_trigger(self._status.get("trigger_3")) + + @property + def trigger_4_description(self) -> str | None: + """Return trigger_4_description of valve switch.""" + return wilight_to_hass_trigger(self._status.get("trigger_4")) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes.""" + attr: dict[str, Any] = {} + + if self.watering_time is not None: + attr[ATTR_WATERING_TIME] = self.watering_time + + if self.trigger_1 is not None: + attr[ATTR_TRIGGER_1] = self.trigger_1 + + if self.trigger_2 is not None: + attr[ATTR_TRIGGER_2] = self.trigger_2 + + if self.trigger_3 is not None: + attr[ATTR_TRIGGER_3] = self.trigger_3 + + if self.trigger_4 is not None: + attr[ATTR_TRIGGER_4] = self.trigger_4 + + if self.trigger_1_description is not None: + attr[ATTR_TRIGGER_1_DESC] = self.trigger_1_description + + if self.trigger_2_description is not None: + attr[ATTR_TRIGGER_2_DESC] = self.trigger_2_description + + if self.trigger_3_description is not None: + attr[ATTR_TRIGGER_3_DESC] = self.trigger_3_description + + if self.trigger_4_description is not None: + attr[ATTR_TRIGGER_4_DESC] = self.trigger_4_description + + return attr + + @property + def icon(self) -> str: + """Return the icon to use in the frontend.""" + return ICON_WATERING + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + await self._client.turn_on(self._index) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + await self._client.turn_off(self._index) + + async def async_set_watering_time(self, watering_time: int) -> None: + """Set the watering time.""" + await self._client.set_switch_time(self._index, watering_time) + + async def async_set_trigger(self, trigger_index: int, trigger: str) -> None: + """Set the trigger according to index.""" + if trigger_index == 1: + await self._client.set_switch_trigger_1(self._index, trigger) + if trigger_index == 2: + await self._client.set_switch_trigger_2(self._index, trigger) + if trigger_index == 3: + await self._client.set_switch_trigger_3(self._index, trigger) + if trigger_index == 4: + await self._client.set_switch_trigger_4(self._index, trigger) + + +class WiLightValvePauseSwitch(WiLightDevice, SwitchEntity): + """Representation of a WiLights Valve Pause switch.""" + + @property + def name(self) -> str: + """Return the name of the switch.""" + return f"{self._attr_name} {DESC_PAUSE}" + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return self._status.get("on", False) + + @property + def pause_time(self) -> int | None: + """Return pause time of valve switch. + + None is unknown, 1 is minimum, 24 is maximum. + """ + pause_time = self._status.get("timer_target") + if pause_time is not None: + return wilight_to_hass_pause_time(pause_time) + return pause_time + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes.""" + attr: dict[str, Any] = {} + + if self.pause_time is not None: + attr[ATTR_PAUSE_TIME] = self.pause_time + + return attr + + @property + def icon(self) -> str: + """Return the icon to use in the frontend.""" + return ICON_PAUSE + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + await self._client.turn_on(self._index) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + await self._client.turn_off(self._index) + + async def async_set_pause_time(self, pause_time: int) -> None: + """Set the pause time.""" + target_time = hass_to_wilight_pause_time(pause_time) + await self._client.set_switch_time(self._index, target_time) diff --git a/requirements_all.txt b/requirements_all.txt index 8dbcab9d520..44075fdf14a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2039,7 +2039,7 @@ pywebpush==1.9.2 pywemo==0.9.1 # homeassistant.components.wilight -pywilight==0.0.70 +pywilight==0.0.74 # homeassistant.components.wiz pywizlight==0.5.14 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eba56137019..5470bf91d73 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1378,7 +1378,7 @@ pywebpush==1.9.2 pywemo==0.9.1 # homeassistant.components.wilight -pywilight==0.0.70 +pywilight==0.0.74 # homeassistant.components.wiz pywizlight==0.5.14 diff --git a/tests/components/wilight/__init__.py b/tests/components/wilight/__init__.py index dbcfbbaaa8c..acaf2aef2a8 100644 --- a/tests/components/wilight/__init__.py +++ b/tests/components/wilight/__init__.py @@ -27,6 +27,7 @@ UPNP_MODEL_NAME_DIMMER = "WiLight 0100001700020009-10010010" UPNP_MODEL_NAME_COLOR = "WiLight 0107001800020009-11010" UPNP_MODEL_NAME_LIGHT_FAN = "WiLight 0104001800010009-10" UPNP_MODEL_NAME_COVER = "WiLight 0103001800010009-10" +UPNP_MODEL_NAME_SWITCH = "WiLight 0105001900010011-00000000000010" UPNP_MODEL_NUMBER = "123456789012345678901234567890123456" UPNP_SERIAL = "000000000099" UPNP_MAC_ADDRESS = "5C:CF:7F:8B:CA:56" diff --git a/tests/components/wilight/test_switch.py b/tests/components/wilight/test_switch.py new file mode 100644 index 00000000000..035f5b37be5 --- /dev/null +++ b/tests/components/wilight/test_switch.py @@ -0,0 +1,264 @@ +"""Tests for the WiLight integration.""" +from unittest.mock import patch + +import pytest +import pywilight + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.wilight import DOMAIN as WILIGHT_DOMAIN +from homeassistant.components.wilight.switch import ( + ATTR_PAUSE_TIME, + ATTR_TRIGGER, + ATTR_TRIGGER_1, + ATTR_TRIGGER_2, + ATTR_TRIGGER_3, + ATTR_TRIGGER_4, + ATTR_TRIGGER_INDEX, + ATTR_WATERING_TIME, + SERVICE_SET_PAUSE_TIME, + SERVICE_SET_TRIGGER, + SERVICE_SET_WATERING_TIME, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import ( + HOST, + UPNP_MAC_ADDRESS, + UPNP_MODEL_NAME_SWITCH, + UPNP_MODEL_NUMBER, + UPNP_SERIAL, + WILIGHT_ID, + setup_integration, +) + + +@pytest.fixture(name="dummy_device_from_host_switch") +def mock_dummy_device_from_host_switch(): + """Mock a valid api_devce.""" + + device = pywilight.wilight_from_discovery( + f"http://{HOST}:45995/wilight.xml", + UPNP_MAC_ADDRESS, + UPNP_MODEL_NAME_SWITCH, + UPNP_SERIAL, + UPNP_MODEL_NUMBER, + ) + + device.set_dummy(True) + + with patch( + "pywilight.device_from_host", + return_value=device, + ): + yield device + + +async def test_loading_switch( + hass: HomeAssistant, + dummy_device_from_host_switch, +) -> None: + """Test the WiLight configuration entry loading.""" + + entry = await setup_integration(hass) + assert entry + assert entry.unique_id == WILIGHT_ID + + entity_registry = er.async_get(hass) + + # First segment of the strip + state = hass.states.get("switch.wl000000000099_1_watering") + assert state + assert state.state == STATE_OFF + + entry = entity_registry.async_get("switch.wl000000000099_1_watering") + assert entry + assert entry.unique_id == "WL000000000099_0" + + # Seconnd segment of the strip + state = hass.states.get("switch.wl000000000099_2_pause") + assert state + assert state.state == STATE_OFF + + entry = entity_registry.async_get("switch.wl000000000099_2_pause") + assert entry + assert entry.unique_id == "WL000000000099_1" + + +async def test_on_off_switch_state( + hass: HomeAssistant, dummy_device_from_host_switch +) -> None: + """Test the change of state of the switch.""" + await setup_integration(hass) + + # On - watering + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wl000000000099_1_watering"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_1_watering") + assert state + assert state.state == STATE_ON + + # Off - watering + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wl000000000099_1_watering"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_1_watering") + assert state + assert state.state == STATE_OFF + + # On - pause + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wl000000000099_2_pause"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_2_pause") + assert state + assert state.state == STATE_ON + + # Off - pause + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wl000000000099_2_pause"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_2_pause") + assert state + assert state.state == STATE_OFF + + +async def test_switch_services( + hass: HomeAssistant, dummy_device_from_host_switch +) -> None: + """Test the services of the switch.""" + await setup_integration(hass) + + # Set watering time + await hass.services.async_call( + WILIGHT_DOMAIN, + SERVICE_SET_WATERING_TIME, + {ATTR_WATERING_TIME: 30, ATTR_ENTITY_ID: "switch.wl000000000099_1_watering"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_1_watering") + assert state + assert state.attributes.get(ATTR_WATERING_TIME) == 30 + + # Set pause time + await hass.services.async_call( + WILIGHT_DOMAIN, + SERVICE_SET_PAUSE_TIME, + {ATTR_PAUSE_TIME: 18, ATTR_ENTITY_ID: "switch.wl000000000099_2_pause"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_2_pause") + assert state + assert state.attributes.get(ATTR_PAUSE_TIME) == 18 + + # Set trigger_1 + await hass.services.async_call( + WILIGHT_DOMAIN, + SERVICE_SET_TRIGGER, + { + ATTR_TRIGGER_INDEX: "1", + ATTR_TRIGGER: "12715301", + ATTR_ENTITY_ID: "switch.wl000000000099_1_watering", + }, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_1_watering") + assert state + assert state.attributes.get(ATTR_TRIGGER_1) == "12715301" + + # Set trigger_2 + await hass.services.async_call( + WILIGHT_DOMAIN, + SERVICE_SET_TRIGGER, + { + ATTR_TRIGGER_INDEX: "2", + ATTR_TRIGGER: "12707301", + ATTR_ENTITY_ID: "switch.wl000000000099_1_watering", + }, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_1_watering") + assert state + assert state.attributes.get(ATTR_TRIGGER_2) == "12707301" + + # Set trigger_3 + await hass.services.async_call( + WILIGHT_DOMAIN, + SERVICE_SET_TRIGGER, + { + ATTR_TRIGGER_INDEX: "3", + ATTR_TRIGGER: "00015301", + ATTR_ENTITY_ID: "switch.wl000000000099_1_watering", + }, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_1_watering") + assert state + assert state.attributes.get(ATTR_TRIGGER_3) == "00015301" + + # Set trigger_4 + await hass.services.async_call( + WILIGHT_DOMAIN, + SERVICE_SET_TRIGGER, + { + ATTR_TRIGGER_INDEX: "4", + ATTR_TRIGGER: "00008300", + ATTR_ENTITY_ID: "switch.wl000000000099_1_watering", + }, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.wl000000000099_1_watering") + assert state + assert state.attributes.get(ATTR_TRIGGER_4) == "00008300" + + # Set watering time using WiLight Pause Switch to raise + with pytest.raises(ValueError) as exc_info: + await hass.services.async_call( + WILIGHT_DOMAIN, + SERVICE_SET_WATERING_TIME, + {ATTR_WATERING_TIME: 30, ATTR_ENTITY_ID: "switch.wl000000000099_2_pause"}, + blocking=True, + ) + + await hass.async_block_till_done() + assert str(exc_info.value) == "Entity is not a WiLight valve switch" From cd1227d8b9c4ae677f1218aeabc9dea497393db9 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 7 Aug 2022 15:04:04 +0200 Subject: [PATCH 3184/3516] Fix default sensor names in NextDNS integration (#76264) --- homeassistant/components/nextdns/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index 168c0be8cd0..422bc2a237b 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -135,7 +135,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:dns", - name="TCP Queries", + name="TCP queries", native_unit_of_measurement="queries", state_class=SensorStateClass.TOTAL, value=lambda data: data.tcp_queries, @@ -190,7 +190,7 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:dns", - name="TCP Queries Ratio", + name="TCP queries ratio", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.tcp_queries_ratio, From 4aeaeeda0dce5bd1a839cdf92b747cf6e8e6a90e Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 7 Aug 2022 15:42:47 +0200 Subject: [PATCH 3185/3516] Postpone broadlink platform switch until config entry is ready (#76371) --- homeassistant/components/broadlink/switch.py | 24 +++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 6a015748bd0..d38898a513f 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -26,11 +26,13 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from . import BroadlinkDevice from .const import DOMAIN from .entity import BroadlinkEntity from .helpers import data_packet, import_device, mac_address @@ -80,8 +82,18 @@ async def async_setup_platform( host = config.get(CONF_HOST) if switches := config.get(CONF_SWITCHES): - platform_data = hass.data[DOMAIN].platforms.setdefault(Platform.SWITCH, {}) - platform_data.setdefault(mac_addr, []).extend(switches) + platform_data = hass.data[DOMAIN].platforms.get(Platform.SWITCH, {}) + async_add_entities_config_entry: AddEntitiesCallback + device: BroadlinkDevice + async_add_entities_config_entry, device = platform_data.get( + mac_addr, (None, None) + ) + if not async_add_entities_config_entry: + raise PlatformNotReady + + async_add_entities_config_entry( + BroadlinkRMSwitch(device, config) for config in switches + ) else: _LOGGER.warning( @@ -104,12 +116,8 @@ async def async_setup_entry( switches: list[BroadlinkSwitch] = [] if device.api.type in {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"}: - platform_data = hass.data[DOMAIN].platforms.get(Platform.SWITCH, {}) - user_defined_switches = platform_data.get(device.api.mac, {}) - switches.extend( - BroadlinkRMSwitch(device, config) for config in user_defined_switches - ) - + platform_data = hass.data[DOMAIN].platforms.setdefault(Platform.SWITCH, {}) + platform_data[device.api.mac] = async_add_entities, device elif device.api.type == "SP1": switches.append(BroadlinkSP1Switch(device)) From 1fe44d0997bc56dabd3b44b94b715032e53141b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Aug 2022 05:03:56 -1000 Subject: [PATCH 3186/3516] Ensure bluetooth recovers if Dbus gets restarted (#76249) --- .../components/bluetooth/__init__.py | 68 ++++++++++++++++--- tests/components/bluetooth/test_init.py | 56 +++++++++++++++ 2 files changed, 116 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index ed04ca401ed..9492642e0e0 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -8,6 +8,7 @@ from dataclasses import dataclass from datetime import datetime, timedelta from enum import Enum import logging +import time from typing import TYPE_CHECKING, Final import async_timeout @@ -56,6 +57,10 @@ START_TIMEOUT = 9 SOURCE_LOCAL: Final = "local" +SCANNER_WATCHDOG_TIMEOUT: Final = 60 * 5 +SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=SCANNER_WATCHDOG_TIMEOUT) +MONOTONIC_TIME = time.monotonic + @dataclass class BluetoothServiceInfoBleak(BluetoothServiceInfo): @@ -259,9 +264,10 @@ async def async_setup_entry( ) -> bool: """Set up the bluetooth integration from a config entry.""" manager: BluetoothManager = hass.data[DOMAIN] - await manager.async_start( - BluetoothScanningMode.ACTIVE, entry.options.get(CONF_ADAPTER) - ) + async with manager.start_stop_lock: + await manager.async_start( + BluetoothScanningMode.ACTIVE, entry.options.get(CONF_ADAPTER) + ) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True @@ -270,8 +276,6 @@ async def _async_update_listener( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> None: """Handle options update.""" - manager: BluetoothManager = hass.data[DOMAIN] - manager.async_start_reload() await hass.config_entries.async_reload(entry.entry_id) @@ -280,7 +284,9 @@ async def async_unload_entry( ) -> bool: """Unload a config entry.""" manager: BluetoothManager = hass.data[DOMAIN] - await manager.async_stop() + async with manager.start_stop_lock: + manager.async_start_reload() + await manager.async_stop() return True @@ -296,13 +302,19 @@ class BluetoothManager: self.hass = hass self._integration_matcher = integration_matcher self.scanner: HaBleakScanner | None = None + self.start_stop_lock = asyncio.Lock() self._cancel_device_detected: CALLBACK_TYPE | None = None self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None + self._cancel_stop: CALLBACK_TYPE | None = None + self._cancel_watchdog: CALLBACK_TYPE | None = None self._unavailable_callbacks: dict[str, list[Callable[[str], None]]] = {} self._callbacks: list[ tuple[BluetoothCallback, BluetoothCallbackMatcher | None] ] = [] + self._last_detection = 0.0 self._reloading = False + self._adapter: str | None = None + self._scanning_mode = BluetoothScanningMode.ACTIVE @hass_callback def async_setup(self) -> None: @@ -324,6 +336,8 @@ class BluetoothManager: ) -> None: """Set up BT Discovery.""" assert self.scanner is not None + self._adapter = adapter + self._scanning_mode = scanning_mode if self._reloading: # On reload, we need to reset the scanner instance # since the devices in its history may not be reachable @@ -388,7 +402,32 @@ class BluetoothManager: _LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True) raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex self.async_setup_unavailable_tracking() - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.async_stop) + self._async_setup_scanner_watchdog() + self._cancel_stop = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping + ) + + @hass_callback + def _async_setup_scanner_watchdog(self) -> None: + """If Dbus gets restarted or updated, we need to restart the scanner.""" + self._last_detection = MONOTONIC_TIME() + self._cancel_watchdog = async_track_time_interval( + self.hass, self._async_scanner_watchdog, SCANNER_WATCHDOG_INTERVAL + ) + + async def _async_scanner_watchdog(self, now: datetime) -> None: + """Check if the scanner is running.""" + time_since_last_detection = MONOTONIC_TIME() - self._last_detection + if time_since_last_detection < SCANNER_WATCHDOG_TIMEOUT: + return + _LOGGER.info( + "Bluetooth scanner has gone quiet for %s, restarting", + SCANNER_WATCHDOG_INTERVAL, + ) + async with self.start_stop_lock: + self.async_start_reload() + await self.async_stop() + await self.async_start(self._scanning_mode, self._adapter) @hass_callback def async_setup_unavailable_tracking(self) -> None: @@ -423,6 +462,7 @@ class BluetoothManager: self, device: BLEDevice, advertisement_data: AdvertisementData ) -> None: """Handle a detected device.""" + self._last_detection = MONOTONIC_TIME() matched_domains = self._integration_matcher.match_domains( device, advertisement_data ) @@ -535,14 +575,26 @@ class BluetoothManager: for device_adv in self.scanner.history.values() ] - async def async_stop(self, event: Event | None = None) -> None: + async def _async_hass_stopping(self, event: Event) -> None: + """Stop the Bluetooth integration at shutdown.""" + self._cancel_stop = None + await self.async_stop() + + async def async_stop(self) -> None: """Stop bluetooth discovery.""" + _LOGGER.debug("Stopping bluetooth discovery") + if self._cancel_watchdog: + self._cancel_watchdog() + self._cancel_watchdog = None if self._cancel_device_detected: self._cancel_device_detected() self._cancel_device_detected = None if self._cancel_unavailable_tracking: self._cancel_unavailable_tracking() self._cancel_unavailable_tracking = None + if self._cancel_stop: + self._cancel_stop() + self._cancel_stop = None if self.scanner: try: await self.scanner.stop() # type: ignore[no-untyped-call] diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 6cd22505dc4..45babd05748 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -10,6 +10,8 @@ import pytest from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( + SCANNER_WATCHDOG_INTERVAL, + SCANNER_WATCHDOG_TIMEOUT, SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, BluetoothChange, @@ -1562,3 +1564,57 @@ async def test_invalid_dbus_message(hass, caplog): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() assert "dbus" in caplog.text + + +async def test_recovery_from_dbus_restart( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test we can recover when DBus gets restarted out from under us.""" + assert await async_setup_component(hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}) + await hass.async_block_till_done() + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + start_time_monotonic = 1000 + scanner = _get_underlying_scanner() + mock_discovered = [MagicMock()] + type(scanner).discovered_devices = mock_discovered + + # Ensure we don't restart the scanner if we don't need to + with patch( + "homeassistant.components.bluetooth.MONOTONIC_TIME", + return_value=start_time_monotonic + 10, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + # Fire a callback to reset the timer + with patch( + "homeassistant.components.bluetooth.MONOTONIC_TIME", + return_value=start_time_monotonic, + ): + scanner._callback( + BLEDevice("44:44:33:11:23:42", "any_name"), + AdvertisementData(local_name="any_name"), + ) + + # Ensure we don't restart the scanner if we don't need to + with patch( + "homeassistant.components.bluetooth.MONOTONIC_TIME", + return_value=start_time_monotonic + 20, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + # We hit the timer, so we restart the scanner + with patch( + "homeassistant.components.bluetooth.MONOTONIC_TIME", + return_value=start_time_monotonic + SCANNER_WATCHDOG_TIMEOUT, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert len(mock_bleak_scanner_start.mock_calls) == 2 From a6963e6a38cb53f8d4caa3d9d0e69976dbc348d2 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 7 Aug 2022 17:06:03 +0200 Subject: [PATCH 3187/3516] Add zwave_js usb port selection (#76385) --- .../components/zwave_js/config_flow.py | 39 ++++++++++++-- .../components/zwave_js/manifest.json | 2 +- requirements_all.txt | 1 + requirements_test_all.txt | 1 + tests/components/zwave_js/test_config_flow.py | 52 +++++++++++++++++-- 5 files changed, 87 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 60807471e03..3da785cdcf2 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -8,6 +8,7 @@ from typing import Any import aiohttp from async_timeout import timeout +from serial.tools import list_ports import voluptuous as vol from zwave_js_server.version import VersionInfo, get_server_version @@ -119,6 +120,30 @@ async def async_get_version_info(hass: HomeAssistant, ws_address: str) -> Versio return version_info +def get_usb_ports() -> dict[str, str]: + """Return a dict of USB ports and their friendly names.""" + ports = list_ports.comports() + port_descriptions = {} + for port in ports: + usb_device = usb.usb_device_from_port(port) + dev_path = usb.get_serial_by_id(usb_device.device) + human_name = usb.human_readable_device_name( + dev_path, + usb_device.serial_number, + usb_device.manufacturer, + usb_device.description, + usb_device.vid, + usb_device.pid, + ) + port_descriptions[dev_path] = human_name + return port_descriptions + + +async def async_get_usb_ports(hass: HomeAssistant) -> dict[str, str]: + """Return a dict of USB ports and their friendly names.""" + return await hass.async_add_executor_job(get_usb_ports) + + class BaseZwaveJSFlow(FlowHandler): """Represent the base config flow for Z-Wave JS.""" @@ -402,7 +427,9 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): vid, pid, ) - self.context["title_placeholders"] = {CONF_NAME: self._title} + self.context["title_placeholders"] = { + CONF_NAME: self._title.split(" - ")[0].strip() + } return await self.async_step_usb_confirm() async def async_step_usb_confirm( @@ -579,7 +606,11 @@ class ConfigFlow(BaseZwaveJSFlow, config_entries.ConfigFlow, domain=DOMAIN): } if not self._usb_discovery: - schema = {vol.Required(CONF_USB_PATH, default=usb_path): str, **schema} + ports = await async_get_usb_ports(self.hass) + schema = { + vol.Required(CONF_USB_PATH, default=usb_path): vol.In(ports), + **schema, + } data_schema = vol.Schema(schema) @@ -801,9 +832,11 @@ class OptionsFlowHandler(BaseZwaveJSFlow, config_entries.OptionsFlow): log_level = addon_config.get(CONF_ADDON_LOG_LEVEL, "info") emulate_hardware = addon_config.get(CONF_ADDON_EMULATE_HARDWARE, False) + ports = await async_get_usb_ports(self.hass) + data_schema = vol.Schema( { - vol.Required(CONF_USB_PATH, default=usb_path): str, + vol.Required(CONF_USB_PATH, default=usb_path): vol.In(ports), vol.Optional(CONF_S0_LEGACY_KEY, default=s0_legacy_key): str, vol.Optional( CONF_S2_ACCESS_CONTROL_KEY, default=s2_access_control_key diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 29be66cd024..9ee08b0505d 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.40.0"], + "requirements": ["pyserial==3.5", "zwave-js-server-python==0.40.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["usb", "http", "websocket_api"], "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 44075fdf14a..7e07889d05f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1815,6 +1815,7 @@ pyserial-asyncio==0.6 # homeassistant.components.crownstone # homeassistant.components.usb # homeassistant.components.zha +# homeassistant.components.zwave_js pyserial==3.5 # homeassistant.components.sesame diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5470bf91d73..e04b0d7cea5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1250,6 +1250,7 @@ pyserial-asyncio==0.6 # homeassistant.components.crownstone # homeassistant.components.usb # homeassistant.components.zha +# homeassistant.components.zwave_js pyserial==3.5 # homeassistant.components.sia diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py index a8a2c6c7191..6c4b18e8dc3 100644 --- a/tests/components/zwave_js/test_config_flow.py +++ b/tests/components/zwave_js/test_config_flow.py @@ -1,9 +1,12 @@ """Test the Z-Wave JS config flow.""" import asyncio -from unittest.mock import DEFAULT, call, patch +from collections.abc import Generator +from copy import copy +from unittest.mock import DEFAULT, MagicMock, call, patch import aiohttp import pytest +from serial.tools.list_ports_common import ListPortInfo from zwave_js_server.version import VersionInfo from homeassistant import config_entries @@ -134,6 +137,45 @@ def mock_addon_setup_time(): yield addon_setup_time +@pytest.fixture(name="serial_port") +def serial_port_fixture() -> ListPortInfo: + """Return a mock serial port.""" + port = ListPortInfo("/test", skip_link_detection=True) + port.serial_number = "1234" + port.manufacturer = "Virtual serial port" + port.device = "/test" + port.description = "Some serial port" + port.pid = 9876 + port.vid = 5678 + + return port + + +@pytest.fixture(name="mock_list_ports", autouse=True) +def mock_list_ports_fixture(serial_port) -> Generator[MagicMock, None, None]: + """Mock list ports.""" + with patch( + "homeassistant.components.zwave_js.config_flow.list_ports.comports" + ) as mock_list_ports: + another_port = copy(serial_port) + another_port.device = "/new" + another_port.description = "New serial port" + another_port.serial_number = "5678" + another_port.pid = 8765 + mock_list_ports.return_value = [serial_port, another_port] + yield mock_list_ports + + +@pytest.fixture(name="mock_usb_serial_by_id", autouse=True) +def mock_usb_serial_by_id_fixture() -> Generator[MagicMock, None, None]: + """Mock usb serial by id.""" + with patch( + "homeassistant.components.zwave_js.config_flow.usb.get_serial_by_id" + ) as mock_usb_serial_by_id: + mock_usb_serial_by_id.side_effect = lambda x: x + yield mock_usb_serial_by_id + + async def test_manual(hass): """Test we create an entry with manual step.""" @@ -1397,7 +1439,7 @@ async def test_addon_installed_already_configured( result = await hass.config_entries.flow.async_configure( result["flow_id"], { - "usb_path": "/test_new", + "usb_path": "/new", "s0_legacy_key": "new123", "s2_access_control_key": "new456", "s2_authenticated_key": "new789", @@ -1410,7 +1452,7 @@ async def test_addon_installed_already_configured( "core_zwave_js", { "options": { - "device": "/test_new", + "device": "/new", "s0_legacy_key": "new123", "s2_access_control_key": "new456", "s2_authenticated_key": "new789", @@ -1430,7 +1472,7 @@ async def test_addon_installed_already_configured( assert result["type"] == "abort" assert result["reason"] == "already_configured" assert entry.data["url"] == "ws://host1:3001" - assert entry.data["usb_path"] == "/test_new" + assert entry.data["usb_path"] == "/new" assert entry.data["s0_legacy_key"] == "new123" assert entry.data["s2_access_control_key"] == "new456" assert entry.data["s2_authenticated_key"] == "new789" @@ -2380,8 +2422,10 @@ async def test_import_addon_installed( set_addon_options, start_addon, get_addon_discovery_info, + serial_port, ): """Test import step while add-on already installed on Supervisor.""" + serial_port.device = "/test/imported" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, From ddf3d23c27739b64baee589e2f97ef6b00886d24 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sun, 7 Aug 2022 18:10:32 +0200 Subject: [PATCH 3188/3516] Fix opentherm_gw startup failure handling (#76376) --- homeassistant/components/opentherm_gw/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index cdf360c8795..51071c9a0a1 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -117,6 +117,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b timeout=CONNECTION_TIMEOUT, ) except (asyncio.TimeoutError, ConnectionError, SerialException) as ex: + await gateway.cleanup() raise ConfigEntryNotReady( f"Could not connect to gateway at {gateway.device_path}: {ex}" ) from ex From c7838c347f97d5da1c18b009ff6d226ceb04a3b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Aug 2022 06:11:11 -1000 Subject: [PATCH 3189/3516] Bump zeroconf to 0.39.0 (#76328) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 8061be2cf8a..4438a22040d 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.38.7"], + "requirements": ["zeroconf==0.39.0"], "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c0221647abf..f15daf093a3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -38,7 +38,7 @@ typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 yarl==1.7.2 -zeroconf==0.38.7 +zeroconf==0.39.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index 7e07889d05f..54606de114a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2515,7 +2515,7 @@ youtube_dl==2021.12.17 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.38.7 +zeroconf==0.39.0 # homeassistant.components.zha zha-quirks==0.0.78 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e04b0d7cea5..c3fc6b31256 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1692,7 +1692,7 @@ yolink-api==0.0.9 youless-api==0.16 # homeassistant.components.zeroconf -zeroconf==0.38.7 +zeroconf==0.39.0 # homeassistant.components.zha zha-quirks==0.0.78 From 36808a0db42e0157e936cd806eadb9afe7c57699 Mon Sep 17 00:00:00 2001 From: rlippmann <70883373+rlippmann@users.noreply.github.com> Date: Sun, 7 Aug 2022 12:27:17 -0400 Subject: [PATCH 3190/3516] Add ecobee Smart Premium thermostat (#76365) Update const.py Add device model constant for ecobee Smart Premium thermostat --- homeassistant/components/ecobee/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index 50dd606ad25..b4e0c485e45 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -36,6 +36,7 @@ ECOBEE_MODEL_TO_NAME = { "nikeEms": "ecobee3 lite EMS", "apolloSmart": "ecobee4 Smart", "vulcanSmart": "ecobee4 Smart", + "aresSmart": "ecobee Smart Premium", } PLATFORMS = [ From ea88f229a32dd2435dd661869f3d8e4ee02ec056 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 7 Aug 2022 12:41:12 -0500 Subject: [PATCH 3191/3516] Bump plexapi to 4.12.1 (#76393) --- homeassistant/components/plex/manifest.json | 2 +- homeassistant/components/plex/server.py | 34 ++++++++++----------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 912732efe98..1875b0e05dc 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.11.2", + "plexapi==4.12.1", "plexauth==0.0.6", "plexwebsocket==0.0.13" ], diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index b136bec73e9..058e8abbecd 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -462,24 +462,24 @@ class PlexServer: continue session_username = next(iter(session.usernames), None) - for player in session.players: - unique_id = f"{self.machine_identifier}:{player.machineIdentifier}" - if unique_id not in self.active_sessions: - _LOGGER.debug("Creating new Plex session: %s", session) - self.active_sessions[unique_id] = PlexSession(self, session) - if session_username and session_username not in monitored_users: - ignored_clients.add(player.machineIdentifier) - _LOGGER.debug( - "Ignoring %s client owned by '%s'", - player.product, - session_username, - ) - continue + player = session.player + unique_id = f"{self.machine_identifier}:{player.machineIdentifier}" + if unique_id not in self.active_sessions: + _LOGGER.debug("Creating new Plex session: %s", session) + self.active_sessions[unique_id] = PlexSession(self, session) + if session_username and session_username not in monitored_users: + ignored_clients.add(player.machineIdentifier) + _LOGGER.debug( + "Ignoring %s client owned by '%s'", + player.product, + session_username, + ) + continue - process_device("session", player) - available_clients[player.machineIdentifier][ - "session" - ] = self.active_sessions[unique_id] + process_device("session", player) + available_clients[player.machineIdentifier][ + "session" + ] = self.active_sessions[unique_id] for device in devices: process_device("PMS", device) diff --git a/requirements_all.txt b/requirements_all.txt index 54606de114a..48f802fa367 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1256,7 +1256,7 @@ pillow==9.2.0 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.11.2 +plexapi==4.12.1 # homeassistant.components.plex plexauth==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c3fc6b31256..1ffcf08ea57 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -874,7 +874,7 @@ pilight==0.1.1 pillow==9.2.0 # homeassistant.components.plex -plexapi==4.11.2 +plexapi==4.12.1 # homeassistant.components.plex plexauth==0.0.6 From 89e0db354859fa49d5c4489c6cf576f7033a0cb8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 7 Aug 2022 14:36:30 -0400 Subject: [PATCH 3192/3516] Remove Z-Wave JS trigger uart USB id (#76391) --- homeassistant/components/zwave_js/manifest.json | 9 --------- homeassistant/generated/usb.py | 5 ----- 2 files changed, 14 deletions(-) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index 9ee08b0505d..5f9c8df7afc 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -18,15 +18,6 @@ "pid": "8A2A", "description": "*z-wave*", "known_devices": ["Nortek HUSBZB-1"] - }, - { - "vid": "10C4", - "pid": "EA60", - "known_devices": [ - "Aeotec Z-Stick 7", - "Silicon Labs UZB-7", - "Zooz ZST10 700" - ] } ], "zeroconf": ["_zwave-js-server._tcp.local."], diff --git a/homeassistant/generated/usb.py b/homeassistant/generated/usb.py index ef75fbef4d1..3583f96dc2c 100644 --- a/homeassistant/generated/usb.py +++ b/homeassistant/generated/usb.py @@ -105,10 +105,5 @@ USB = [ "vid": "10C4", "pid": "8A2A", "description": "*z-wave*" - }, - { - "domain": "zwave_js", - "vid": "10C4", - "pid": "EA60" } ] From d14b76e7fcf37048953398b54e552b44824a4063 Mon Sep 17 00:00:00 2001 From: Avi Miller Date: Mon, 8 Aug 2022 04:45:26 +1000 Subject: [PATCH 3193/3516] Enable the LIFX diagnostic buttons by default (#76389) --- homeassistant/components/lifx/button.py | 2 -- tests/components/lifx/test_button.py | 22 ++-------------------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/lifx/button.py b/homeassistant/components/lifx/button.py index 6d3f4fe51bf..3a4c73d2889 100644 --- a/homeassistant/components/lifx/button.py +++ b/homeassistant/components/lifx/button.py @@ -19,14 +19,12 @@ RESTART_BUTTON_DESCRIPTION = ButtonEntityDescription( key=RESTART, name="Restart", device_class=ButtonDeviceClass.RESTART, - entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ) IDENTIFY_BUTTON_DESCRIPTION = ButtonEntityDescription( key=IDENTIFY, name="Identify", - entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, ) diff --git a/tests/components/lifx/test_button.py b/tests/components/lifx/test_button.py index a485c882100..abc91128e25 100644 --- a/tests/components/lifx/test_button.py +++ b/tests/components/lifx/test_button.py @@ -43,18 +43,9 @@ async def test_button_restart(hass: HomeAssistant) -> None: entity_registry = er.async_get(hass) entity = entity_registry.async_get(entity_id) assert entity - assert entity.disabled + assert not entity.disabled assert entity.unique_id == unique_id - enabled_entity = entity_registry.async_update_entity(entity_id, disabled_by=None) - assert not enabled_entity.disabled - - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=bulb): - await hass.config_entries.async_reload(config_entry.entry_id) - await hass.async_block_till_done() - await hass.services.async_call( BUTTON_DOMAIN, "press", {ATTR_ENTITY_ID: entity_id}, blocking=True ) @@ -84,18 +75,9 @@ async def test_button_identify(hass: HomeAssistant) -> None: entity_registry = er.async_get(hass) entity = entity_registry.async_get(entity_id) assert entity - assert entity.disabled + assert not entity.disabled assert entity.unique_id == unique_id - enabled_entity = entity_registry.async_update_entity(entity_id, disabled_by=None) - assert not enabled_entity.disabled - - with _patch_discovery(device=bulb), _patch_config_flow_try_connect( - device=bulb - ), _patch_device(device=bulb): - await hass.config_entries.async_reload(config_entry.entry_id) - await hass.async_block_till_done() - await hass.services.async_call( BUTTON_DOMAIN, "press", {ATTR_ENTITY_ID: entity_id}, blocking=True ) From 7deeea02c2c0c6a8312293c713ca0edab78acab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Sun, 7 Aug 2022 21:29:07 +0200 Subject: [PATCH 3194/3516] Update aioairzone to v0.4.8 (#76404) --- homeassistant/components/airzone/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/airzone/manifest.json b/homeassistant/components/airzone/manifest.json index e4f94327708..bd8ed6b9920 100644 --- a/homeassistant/components/airzone/manifest.json +++ b/homeassistant/components/airzone/manifest.json @@ -3,7 +3,7 @@ "name": "Airzone", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airzone", - "requirements": ["aioairzone==0.4.6"], + "requirements": ["aioairzone==0.4.8"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioairzone"] diff --git a/requirements_all.txt b/requirements_all.txt index 48f802fa367..50a4e5860bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -113,7 +113,7 @@ aio_geojson_usgs_earthquakes==0.1 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.6 +aioairzone==0.4.8 # homeassistant.components.ambient_station aioambient==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ffcf08ea57..8207b8edfd5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -100,7 +100,7 @@ aio_geojson_usgs_earthquakes==0.1 aio_georss_gdacs==0.7 # homeassistant.components.airzone -aioairzone==0.4.6 +aioairzone==0.4.8 # homeassistant.components.ambient_station aioambient==2021.11.0 From e89459453b5414ed9f4e9e6434698ad5bf00f202 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 7 Aug 2022 13:44:27 -0600 Subject: [PATCH 3195/3516] Add more controller-related RainMachine diagnostics (#76409) --- .../components/rainmachine/diagnostics.py | 35 +++- tests/components/rainmachine/conftest.py | 16 ++ .../fixtures/api_versions_data.json | 5 + .../fixtures/diagnostics_current_data.json | 21 ++ .../rainmachine/test_diagnostics.py | 198 ++++++++++-------- 5 files changed, 182 insertions(+), 93 deletions(-) create mode 100644 tests/components/rainmachine/fixtures/api_versions_data.json create mode 100644 tests/components/rainmachine/fixtures/diagnostics_current_data.json diff --git a/homeassistant/components/rainmachine/diagnostics.py b/homeassistant/components/rainmachine/diagnostics.py index 58b918c18ee..131f2e130e7 100644 --- a/homeassistant/components/rainmachine/diagnostics.py +++ b/homeassistant/components/rainmachine/diagnostics.py @@ -1,20 +1,38 @@ """Diagnostics support for RainMachine.""" from __future__ import annotations +import asyncio from typing import Any +from regenmaschine.errors import RequestError + from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_PASSWORD +from homeassistant.const import ( + CONF_ELEVATION, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_PASSWORD, +) from homeassistant.core import HomeAssistant from . import RainMachineData from .const import DOMAIN +CONF_STATION_ID = "stationID" +CONF_STATION_NAME = "stationName" +CONF_STATION_SOURCE = "stationSource" +CONF_TIMEZONE = "timezone" + TO_REDACT = { + CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_PASSWORD, + CONF_STATION_ID, + CONF_STATION_NAME, + CONF_STATION_SOURCE, + CONF_TIMEZONE, } @@ -24,6 +42,14 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + controller_tasks = { + "versions": data.controller.api.versions(), + "current_diagnostics": data.controller.diagnostics.current(), + } + controller_results = await asyncio.gather( + *controller_tasks.values(), return_exceptions=True + ) + return { "entry": { "title": entry.title, @@ -39,10 +65,9 @@ async def async_get_config_entry_diagnostics( TO_REDACT, ), "controller": { - "api_version": data.controller.api_version, - "hardware_version": data.controller.hardware_version, - "name": data.controller.name, - "software_version": data.controller.software_version, + category: result + for category, result in zip(controller_tasks, controller_results) + if not isinstance(result, RequestError) }, }, } diff --git a/tests/components/rainmachine/conftest.py b/tests/components/rainmachine/conftest.py index 457df4a3ef2..72b4a5d4293 100644 --- a/tests/components/rainmachine/conftest.py +++ b/tests/components/rainmachine/conftest.py @@ -39,6 +39,8 @@ def config_entry_fixture(hass, config, controller_mac): @pytest.fixture(name="controller") def controller_fixture( controller_mac, + data_api_versions, + data_diagnostics_current, data_programs, data_provision_settings, data_restrictions_current, @@ -55,6 +57,8 @@ def controller_fixture( controller.mac = controller_mac controller.software_version = "4.0.925" + controller.api.versions.return_value = data_api_versions + controller.diagnostics.current.return_value = data_diagnostics_current controller.programs.all.return_value = data_programs controller.provisioning.settings.return_value = data_provision_settings controller.restrictions.current.return_value = data_restrictions_current @@ -70,6 +74,18 @@ def controller_mac_fixture(): return "aa:bb:cc:dd:ee:ff" +@pytest.fixture(name="data_api_versions", scope="session") +def data_api_versions_fixture(): + """Define API version data.""" + return json.loads(load_fixture("api_versions_data.json", "rainmachine")) + + +@pytest.fixture(name="data_diagnostics_current", scope="session") +def data_diagnostics_current_fixture(): + """Define current diagnostics data.""" + return json.loads(load_fixture("diagnostics_current_data.json", "rainmachine")) + + @pytest.fixture(name="data_programs", scope="session") def data_programs_fixture(): """Define program data.""" diff --git a/tests/components/rainmachine/fixtures/api_versions_data.json b/tests/components/rainmachine/fixtures/api_versions_data.json new file mode 100644 index 00000000000..d4ec1fb80e9 --- /dev/null +++ b/tests/components/rainmachine/fixtures/api_versions_data.json @@ -0,0 +1,5 @@ +{ + "apiVer": "4.6.1", + "hwVer": 3, + "swVer": "4.0.1144" +} diff --git a/tests/components/rainmachine/fixtures/diagnostics_current_data.json b/tests/components/rainmachine/fixtures/diagnostics_current_data.json new file mode 100644 index 00000000000..40afd504911 --- /dev/null +++ b/tests/components/rainmachine/fixtures/diagnostics_current_data.json @@ -0,0 +1,21 @@ +{ + "hasWifi": true, + "uptime": "3 days, 18:14:14", + "uptimeSeconds": 324854, + "memUsage": 16196, + "networkStatus": true, + "bootCompleted": true, + "lastCheckTimestamp": 1659895175, + "wizardHasRun": true, + "standaloneMode": false, + "cpuUsage": 1, + "lastCheck": "2022-08-07 11:59:35", + "softwareVersion": "4.0.1144", + "internetStatus": true, + "locationStatus": true, + "timeStatus": true, + "wifiMode": null, + "gatewayAddress": "172.16.20.1", + "cloudStatus": 0, + "weatherStatus": true +} diff --git a/tests/components/rainmachine/test_diagnostics.py b/tests/components/rainmachine/test_diagnostics.py index d770d01fd36..b0876a2f597 100644 --- a/tests/components/rainmachine/test_diagnostics.py +++ b/tests/components/rainmachine/test_diagnostics.py @@ -19,6 +19,90 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_rainmach }, "data": { "coordinator": { + "provision.settings": { + "system": { + "httpEnabled": True, + "rainSensorSnoozeDuration": 0, + "uiUnitsMetric": False, + "programZonesShowInactive": False, + "programSingleSchedule": False, + "standaloneMode": False, + "masterValveAfter": 0, + "touchSleepTimeout": 10, + "selfTest": False, + "useSoftwareRainSensor": False, + "defaultZoneWateringDuration": 300, + "maxLEDBrightness": 40, + "simulatorHistorySize": 0, + "vibration": False, + "masterValveBefore": 0, + "touchProgramToRun": None, + "useRainSensor": False, + "wizardHasRun": True, + "waterLogHistorySize": 365, + "netName": "Home", + "softwareRainSensorMinQPF": 5, + "touchAdvanced": False, + "useBonjourService": True, + "hardwareVersion": 3, + "touchLongPressTimeout": 3, + "showRestrictionsOnLed": False, + "parserDataSizeInDays": 6, + "programListShowInactive": True, + "parserHistorySize": 365, + "allowAlexaDiscovery": False, + "automaticUpdates": True, + "minLEDBrightness": 0, + "minWateringDurationThreshold": 0, + "localValveCount": 12, + "touchAuthAPSeconds": 60, + "useCommandLineArguments": False, + "databasePath": "/rainmachine-app/DB/Default", + "touchCyclePrograms": True, + "zoneListShowInactive": True, + "rainSensorRainStart": None, + "zoneDuration": [ + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + ], + "rainSensorIsNormallyClosed": True, + "useCorrectionForPast": True, + "useMasterValve": False, + "runParsersBeforePrograms": True, + "maxWateringCoef": 2, + "mixerHistorySize": 365, + }, + "location": { + "elevation": REDACTED, + "doyDownloaded": True, + "zip": None, + "windSensitivity": 0.5, + "krs": 0.16, + "stationID": REDACTED, + "stationSource": REDACTED, + "et0Average": 6.578, + "latitude": REDACTED, + "state": "Default", + "stationName": REDACTED, + "wsDays": 2, + "stationDownloaded": True, + "address": "Default", + "rainSensitivity": 0.8, + "timezone": REDACTED, + "longitude": REDACTED, + "name": "Home", + }, + }, "programs": [ { "uid": 1, @@ -297,90 +381,6 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_rainmach ], }, ], - "provision.settings": { - "system": { - "httpEnabled": True, - "rainSensorSnoozeDuration": 0, - "uiUnitsMetric": False, - "programZonesShowInactive": False, - "programSingleSchedule": False, - "standaloneMode": False, - "masterValveAfter": 0, - "touchSleepTimeout": 10, - "selfTest": False, - "useSoftwareRainSensor": False, - "defaultZoneWateringDuration": 300, - "maxLEDBrightness": 40, - "simulatorHistorySize": 0, - "vibration": False, - "masterValveBefore": 0, - "touchProgramToRun": None, - "useRainSensor": False, - "wizardHasRun": True, - "waterLogHistorySize": 365, - "netName": "Home", - "softwareRainSensorMinQPF": 5, - "touchAdvanced": False, - "useBonjourService": True, - "hardwareVersion": 3, - "touchLongPressTimeout": 3, - "showRestrictionsOnLed": False, - "parserDataSizeInDays": 6, - "programListShowInactive": True, - "parserHistorySize": 365, - "allowAlexaDiscovery": False, - "automaticUpdates": True, - "minLEDBrightness": 0, - "minWateringDurationThreshold": 0, - "localValveCount": 12, - "touchAuthAPSeconds": 60, - "useCommandLineArguments": False, - "databasePath": "/rainmachine-app/DB/Default", - "touchCyclePrograms": True, - "zoneListShowInactive": True, - "rainSensorRainStart": None, - "zoneDuration": [ - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - ], - "rainSensorIsNormallyClosed": True, - "useCorrectionForPast": True, - "useMasterValve": False, - "runParsersBeforePrograms": True, - "maxWateringCoef": 2, - "mixerHistorySize": 365, - }, - "location": { - "elevation": 1593.45141602, - "doyDownloaded": True, - "zip": None, - "windSensitivity": 0.5, - "krs": 0.16, - "stationID": 9172, - "stationSource": "station", - "et0Average": 6.578, - "latitude": REDACTED, - "state": "Default", - "stationName": "MY STATION", - "wsDays": 2, - "stationDownloaded": True, - "address": "Default", - "rainSensitivity": 0.8, - "timezone": "America/Los Angeles", - "longitude": REDACTED, - "name": "Home", - }, - }, "restrictions.current": { "hourly": False, "freeze": False, @@ -583,10 +583,32 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_rainmach ], }, "controller": { - "api_version": "4.5.0", - "hardware_version": 3, - "name": 12345, - "software_version": "4.0.925", + "versions": { + "apiVer": "4.6.1", + "hwVer": 3, + "swVer": "4.0.1144", + }, + "current_diagnostics": { + "hasWifi": True, + "uptime": "3 days, 18:14:14", + "uptimeSeconds": 324854, + "memUsage": 16196, + "networkStatus": True, + "bootCompleted": True, + "lastCheckTimestamp": 1659895175, + "wizardHasRun": True, + "standaloneMode": False, + "cpuUsage": 1, + "lastCheck": "2022-08-07 11:59:35", + "softwareVersion": "4.0.1144", + "internetStatus": True, + "locationStatus": True, + "timeStatus": True, + "wifiMode": None, + "gatewayAddress": "172.16.20.1", + "cloudStatus": 0, + "weatherStatus": True, + }, }, }, } From 9552250f3662e6b4d88081ca324fc3d18d0c51b0 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 7 Aug 2022 13:44:50 -0600 Subject: [PATCH 3196/3516] Fix bug where RainMachine entity states don't populate on startup (#76412) --- homeassistant/components/rainmachine/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index dccdaaba74c..f46b1aa0f4c 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -437,6 +437,11 @@ class RainMachineEntity(CoordinatorEntity): self.update_from_latest_data() self.async_write_ha_state() + async def async_added_to_hass(self) -> None: + """When entity is added to hass.""" + await super().async_added_to_hass() + self.update_from_latest_data() + @callback def update_from_latest_data(self) -> None: """Update the state.""" From 714d46b15333cf5b3b0e9bbf6c5d9b4beda73e14 Mon Sep 17 00:00:00 2001 From: Alex Yao <33379584+alexyao2015@users.noreply.github.com> Date: Sun, 7 Aug 2022 14:47:49 -0500 Subject: [PATCH 3197/3516] Silence Yeelight Discovery Log Errors (#76373) --- homeassistant/components/yeelight/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index b4afedd6c51..23a2a131913 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -269,7 +269,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await bulb.async_get_properties() await bulb.async_stop_listening() except (asyncio.TimeoutError, yeelight.BulbException) as err: - _LOGGER.error("Failed to get properties from %s: %s", host, err) + _LOGGER.debug("Failed to get properties from %s: %s", host, err) raise CannotConnect from err _LOGGER.debug("Get properties: %s", bulb.last_properties) return MODEL_UNKNOWN From 33b194e48d0c003f5a97f661b5d55f68eac9141f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Aug 2022 10:01:32 -1000 Subject: [PATCH 3198/3516] Switch a few recent merges to use FlowResultType (#76416) --- .../homekit_controller/test_config_flow.py | 18 ++++++--------- .../lacrosse_view/test_config_flow.py | 16 ++++++------- tests/components/lifx/test_config_flow.py | 16 ++++++------- .../components/simplisafe/test_config_flow.py | 23 ++++++++++--------- 4 files changed, 35 insertions(+), 38 deletions(-) diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index e72d9452e52..ff9b89473d6 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -15,11 +15,7 @@ from homeassistant.components import zeroconf from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import KNOWN_DEVICES from homeassistant.components.homekit_controller.storage import async_get_entity_storage -from homeassistant.data_entry_flow import ( - RESULT_TYPE_ABORT, - RESULT_TYPE_FORM, - FlowResultType, -) +from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo @@ -1017,7 +1013,7 @@ async def test_discovery_no_bluetooth_support(hass, controller): context={"source": config_entries.SOURCE_BLUETOOTH}, data=HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ignored_model" @@ -1032,7 +1028,7 @@ async def test_bluetooth_not_homekit(hass, controller): context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_HK_BLUETOOTH_SERVICE_INFO, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "ignored_model" @@ -1047,7 +1043,7 @@ async def test_bluetooth_valid_device_no_discovery(hass, controller): context={"source": config_entries.SOURCE_BLUETOOTH}, data=HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "accessory_not_found_error" @@ -1065,7 +1061,7 @@ async def test_bluetooth_valid_device_discovery_paired(hass, controller): data=HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_PAIRED, ) - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_paired" @@ -1084,7 +1080,7 @@ async def test_bluetooth_valid_device_discovery_unpaired(hass, controller): data=HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_UNPAIRED, ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "pair" assert storage.get_map("00:00:00:00:00:00") is None @@ -1095,7 +1091,7 @@ async def test_bluetooth_valid_device_discovery_unpaired(hass, controller): } result2 = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input={"pairing_code": "111-22-333"} ) diff --git a/tests/components/lacrosse_view/test_config_flow.py b/tests/components/lacrosse_view/test_config_flow.py index 82178f2801b..dc55f02bff8 100644 --- a/tests/components/lacrosse_view/test_config_flow.py +++ b/tests/components/lacrosse_view/test_config_flow.py @@ -6,7 +6,7 @@ from lacrosse_view import Location, LoginError from homeassistant import config_entries from homeassistant.components.lacrosse_view.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, FlowResultType async def test_form(hass: HomeAssistant) -> None: @@ -14,7 +14,7 @@ async def test_form(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with patch("lacrosse_view.LaCrosse.login", return_value=True,), patch( @@ -30,7 +30,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "location" assert result2["errors"] is None @@ -75,7 +75,7 @@ async def test_form_auth_false(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -94,7 +94,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -115,7 +115,7 @@ async def test_form_login_first(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} @@ -137,7 +137,7 @@ async def test_form_no_locations(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "no_locations"} @@ -159,5 +159,5 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: }, ) - assert result2["type"] == RESULT_TYPE_FORM + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/lifx/test_config_flow.py b/tests/components/lifx/test_config_flow.py index f007e9ee0e8..b346233874a 100644 --- a/tests/components/lifx/test_config_flow.py +++ b/tests/components/lifx/test_config_flow.py @@ -10,7 +10,7 @@ from homeassistant.components.lifx import DOMAIN from homeassistant.components.lifx.const import CONF_SERIAL from homeassistant.const import CONF_DEVICE, CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import FlowResultType from . import ( DEFAULT_ENTRY_TITLE, @@ -332,7 +332,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): data={CONF_HOST: IP_ADDRESS, CONF_SERIAL: SERIAL}, ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_config_flow_try_connect(): @@ -344,7 +344,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_ABORT + assert result2["type"] == FlowResultType.ABORT assert result2["reason"] == "already_in_progress" with _patch_discovery(), _patch_config_flow_try_connect(): @@ -356,7 +356,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_in_progress" with _patch_discovery(no_device=True), _patch_config_flow_try_connect( @@ -370,7 +370,7 @@ async def test_discovered_by_discovery_and_dhcp(hass): ), ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_ABORT + assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "cannot_connect" @@ -408,7 +408,7 @@ async def test_discovered_by_dhcp_or_discovery(hass, source, data): ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] is None with _patch_discovery(), _patch_config_flow_try_connect(), patch( @@ -462,7 +462,7 @@ async def test_discovered_by_dhcp_or_discovery_failed_to_get_device(hass, source DOMAIN, context={"source": source}, data=data ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "cannot_connect" @@ -483,7 +483,7 @@ async def test_discovered_by_dhcp_updates_ip(hass): ), ) await hass.async_block_till_done() - assert result["type"] == RESULT_TYPE_ABORT + assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert config_entry.data[CONF_HOST] == IP_ADDRESS diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index cf92ed94d41..fe803ba187e 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -10,6 +10,7 @@ from homeassistant.components.simplisafe import DOMAIN from homeassistant.components.simplisafe.config_flow import CONF_AUTH_CODE from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_CODE, CONF_TOKEN, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResultType VALID_AUTH_CODE = "code12345123451234512345123451234512345123451" @@ -23,12 +24,12 @@ async def test_duplicate_error(config_entry, hass, setup_simplisafe): DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -38,12 +39,12 @@ async def test_invalid_auth_code_length(hass): DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: "too_short_code"} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {CONF_AUTH_CODE: "invalid_auth_code_length"} @@ -57,13 +58,13 @@ async def test_invalid_credentials(hass): DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {CONF_AUTH_CODE: "invalid_auth"} @@ -75,7 +76,7 @@ async def test_options_flow(config_entry, hass): await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( @@ -101,7 +102,7 @@ async def test_step_reauth(config_entry, hass, setup_simplisafe): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 @@ -125,7 +126,7 @@ async def test_step_reauth_wrong_account(config_entry, hass, setup_simplisafe): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "wrong_account" @@ -177,10 +178,10 @@ async def test_unknown_error(hass, setup_simplisafe): DOMAIN, context={"source": SOURCE_USER} ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_AUTH_CODE: VALID_AUTH_CODE} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == FlowResultType.FORM assert result["errors"] == {"base": "unknown"} From 56acb665148d09f17c18ecf69d3493d17cd86b90 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Aug 2022 10:11:34 -1000 Subject: [PATCH 3199/3516] Fix Govee 5185 Meat Thermometers with older firmware not being discovered (#76414) --- homeassistant/components/govee_ble/manifest.json | 6 +++++- homeassistant/generated/bluetooth.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 624a38ebe9d..eb33df867e1 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -15,6 +15,10 @@ "manufacturer_id": 18994, "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" }, + { + "manufacturer_id": 818, + "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" + }, { "manufacturer_id": 14474, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" @@ -24,7 +28,7 @@ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.12.6"], + "requirements": ["govee-ble==0.12.7"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index ef8193dad28..d704d00ab8a 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -41,6 +41,11 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "manufacturer_id": 18994, "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" }, + { + "domain": "govee_ble", + "manufacturer_id": 818, + "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" + }, { "domain": "govee_ble", "manufacturer_id": 14474, diff --git a/requirements_all.txt b/requirements_all.txt index 50a4e5860bd..3a755023ac7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.6 +govee-ble==0.12.7 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8207b8edfd5..0fbc84fc0a8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -558,7 +558,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.6 +govee-ble==0.12.7 # homeassistant.components.gree greeclimate==1.3.0 From 27f1955f2887716b36f3b45f46afaa9021d58a30 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 7 Aug 2022 14:27:52 -0600 Subject: [PATCH 3200/3516] Automatically enable common RainMachine restriction entities (#76405) Automatically enable common delay-related RainMachine entities --- homeassistant/components/rainmachine/binary_sensor.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index d9448d68f9d..3db64240788 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -80,7 +80,6 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Hourly restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, data_key="hourly", ), @@ -89,7 +88,6 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Month restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, data_key="month", ), @@ -98,7 +96,6 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Rain delay restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, data_key="rainDelay", ), @@ -116,7 +113,6 @@ BINARY_SENSOR_DESCRIPTIONS = ( name="Weekday restrictions", icon="mdi:cancel", entity_category=EntityCategory.DIAGNOSTIC, - entity_registry_enabled_default=False, api_category=DATA_RESTRICTIONS_CURRENT, data_key="weekDay", ), From 8ea9f975fdae4188399210dcb4b11f5f789cea50 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 7 Aug 2022 14:50:49 -0600 Subject: [PATCH 3201/3516] Fix bug potential in RainMachine switches by simplifying architecture (#76417) * Fix bug potential in RainMachine switches by simplifying architecture * Better typing (per code review) * Broader error catch --- .../components/rainmachine/switch.py | 111 +++++++++--------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index ee6ac670840..029c8c06771 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -2,12 +2,13 @@ from __future__ import annotations import asyncio -from collections.abc import Coroutine +from collections.abc import Awaitable, Callable, Coroutine from dataclasses import dataclass from datetime import datetime -from typing import Any +from typing import Any, TypeVar -from regenmaschine.errors import RequestError +from regenmaschine.errors import RainMachineError +from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription @@ -104,6 +105,27 @@ VEGETATION_MAP = { } +_T = TypeVar("_T", bound="RainMachineBaseSwitch") +_P = ParamSpec("_P") + + +def raise_on_request_error( + func: Callable[Concatenate[_T, _P], Awaitable[None]] +) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: + """Define a decorator to raise on a request error.""" + + async def decorator(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: + """Decorate.""" + try: + await func(self, *args, **kwargs) + except RainMachineError as err: + raise HomeAssistantError( + f"Error while executing {func.__name__}: {err}", + ) from err + + return decorator + + @dataclass class RainMachineSwitchDescription( SwitchEntityDescription, @@ -197,22 +219,9 @@ class RainMachineBaseSwitch(RainMachineEntity, SwitchEntity): self._attr_is_on = False self._entry = entry - async def _async_run_api_coroutine(self, api_coro: Coroutine) -> None: - """Await an API coroutine, handle any errors, and update as appropriate.""" - try: - resp = await api_coro - except RequestError as err: - raise HomeAssistantError( - f'Error while executing {api_coro.__name__} on "{self.name}": {err}', - ) from err - - if resp["statusCode"] != 0: - raise HomeAssistantError( - f'Error while executing {api_coro.__name__} on "{self.name}": {resp["message"]}', - ) - - # Because of how inextricably linked programs and zones are, anytime one is - # toggled, we make sure to update the data of both coordinators: + @callback + def _update_activities(self) -> None: + """Update all activity data.""" self.hass.async_create_task( async_update_programs_and_zones(self.hass, self._entry) ) @@ -250,6 +259,7 @@ class RainMachineActivitySwitch(RainMachineBaseSwitch): await self.async_turn_off_when_active(**kwargs) + @raise_on_request_error async def async_turn_off_when_active(self, **kwargs: Any) -> None: """Turn the switch off when its associated activity is active.""" raise NotImplementedError @@ -265,6 +275,7 @@ class RainMachineActivitySwitch(RainMachineBaseSwitch): await self.async_turn_on_when_active(**kwargs) + @raise_on_request_error async def async_turn_on_when_active(self, **kwargs: Any) -> None: """Turn the switch on when its associated activity is active.""" raise NotImplementedError @@ -290,17 +301,17 @@ class RainMachineProgram(RainMachineActivitySwitch): """Stop the program.""" await self.async_turn_off() + @raise_on_request_error async def async_turn_off_when_active(self, **kwargs: Any) -> None: """Turn the switch off when its associated activity is active.""" - await self._async_run_api_coroutine( - self._data.controller.programs.stop(self.entity_description.uid) - ) + await self._data.controller.programs.stop(self.entity_description.uid) + self._update_activities() + @raise_on_request_error async def async_turn_on_when_active(self, **kwargs: Any) -> None: """Turn the switch on when its associated activity is active.""" - await self._async_run_api_coroutine( - self._data.controller.programs.start(self.entity_description.uid) - ) + await self._data.controller.programs.start(self.entity_description.uid) + self._update_activities() @callback def update_from_latest_data(self) -> None: @@ -332,24 +343,21 @@ class RainMachineProgram(RainMachineActivitySwitch): class RainMachineProgramEnabled(RainMachineEnabledSwitch): """Define a switch to enable/disable a RainMachine program.""" + @raise_on_request_error async def async_turn_off(self, **kwargs: Any) -> None: """Disable the program.""" tasks = [ - self._async_run_api_coroutine( - self._data.controller.programs.stop(self.entity_description.uid) - ), - self._async_run_api_coroutine( - self._data.controller.programs.disable(self.entity_description.uid) - ), + self._data.controller.programs.stop(self.entity_description.uid), + self._data.controller.programs.disable(self.entity_description.uid), ] - await asyncio.gather(*tasks) + self._update_activities() + @raise_on_request_error async def async_turn_on(self, **kwargs: Any) -> None: """Enable the program.""" - await self._async_run_api_coroutine( - self._data.controller.programs.enable(self.entity_description.uid) - ) + await self._data.controller.programs.enable(self.entity_description.uid) + self._update_activities() class RainMachineZone(RainMachineActivitySwitch): @@ -363,20 +371,20 @@ class RainMachineZone(RainMachineActivitySwitch): """Stop a zone.""" await self.async_turn_off() + @raise_on_request_error async def async_turn_off_when_active(self, **kwargs: Any) -> None: """Turn the switch off when its associated activity is active.""" - await self._async_run_api_coroutine( - self._data.controller.zones.stop(self.entity_description.uid) - ) + await self._data.controller.zones.stop(self.entity_description.uid) + self._update_activities() + @raise_on_request_error async def async_turn_on_when_active(self, **kwargs: Any) -> None: """Turn the switch on when its associated activity is active.""" - await self._async_run_api_coroutine( - self._data.controller.zones.start( - self.entity_description.uid, - kwargs.get("duration", self._entry.options[CONF_ZONE_RUN_TIME]), - ) + await self._data.controller.zones.start( + self.entity_description.uid, + kwargs.get("duration", self._entry.options[CONF_ZONE_RUN_TIME]), ) + self._update_activities() @callback def update_from_latest_data(self) -> None: @@ -416,21 +424,18 @@ class RainMachineZone(RainMachineActivitySwitch): class RainMachineZoneEnabled(RainMachineEnabledSwitch): """Define a switch to enable/disable a RainMachine zone.""" + @raise_on_request_error async def async_turn_off(self, **kwargs: Any) -> None: """Disable the zone.""" tasks = [ - self._async_run_api_coroutine( - self._data.controller.zones.stop(self.entity_description.uid) - ), - self._async_run_api_coroutine( - self._data.controller.zones.disable(self.entity_description.uid) - ), + self._data.controller.zones.stop(self.entity_description.uid), + self._data.controller.zones.disable(self.entity_description.uid), ] - await asyncio.gather(*tasks) + self._update_activities() + @raise_on_request_error async def async_turn_on(self, **kwargs: Any) -> None: """Enable the zone.""" - await self._async_run_api_coroutine( - self._data.controller.zones.enable(self.entity_description.uid) - ) + await self._data.controller.zones.enable(self.entity_description.uid) + self._update_activities() From dc30d9793898fff130c6a78bc46294bc889b3b8f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 7 Aug 2022 14:51:00 -0600 Subject: [PATCH 3202/3516] Add debug logging for unknown Notion errors (#76395) * Add debug logging for unknown Notion errors * Remove unused constant * Code review --- homeassistant/components/notion/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 2a73d12d946..eaa3f55e56c 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -3,6 +3,8 @@ from __future__ import annotations import asyncio from datetime import timedelta +import logging +import traceback from typing import Any from aionotion import async_get_client @@ -31,7 +33,6 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] ATTR_SYSTEM_MODE = "system_mode" ATTR_SYSTEM_NAME = "system_name" -DEFAULT_ATTRIBUTION = "Data provided by Notion" DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @@ -75,6 +76,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"There was a Notion error while updating {attr}: {result}" ) from result if isinstance(result, Exception): + if LOGGER.isEnabledFor(logging.DEBUG): + LOGGER.debug("".join(traceback.format_tb(result.__traceback__))) raise UpdateFailed( f"There was an unknown error while updating {attr}: {result}" ) from result From ceecab95596076e4fca41577dc91a5592e51da4b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 7 Aug 2022 15:21:49 -0600 Subject: [PATCH 3203/3516] Add update entity to RainMachine (#76100) * Add update entity to RainMachine * Fix tests * Cleanup * Test missing controller diagnostics * Code review --- .coveragerc | 1 + .../components/rainmachine/__init__.py | 58 +- homeassistant/components/rainmachine/const.py | 2 + .../components/rainmachine/diagnostics.py | 23 +- .../components/rainmachine/update.py | 102 +++ tests/components/rainmachine/conftest.py | 12 + .../machine_firmware_update_status_data.json | 7 + .../rainmachine/test_diagnostics.py | 817 +++++++++++++++--- 8 files changed, 882 insertions(+), 140 deletions(-) create mode 100644 homeassistant/components/rainmachine/update.py create mode 100644 tests/components/rainmachine/fixtures/machine_firmware_update_status_data.json diff --git a/.coveragerc b/.coveragerc index 20c6fd2c60e..4c11fe46120 100644 --- a/.coveragerc +++ b/.coveragerc @@ -980,6 +980,7 @@ omit = homeassistant/components/rainmachine/model.py homeassistant/components/rainmachine/sensor.py homeassistant/components/rainmachine/switch.py + homeassistant/components/rainmachine/update.py homeassistant/components/rainmachine/util.py homeassistant/components/raspyrfm/* homeassistant/components/recollect_waste/__init__.py diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index f46b1aa0f4c..1ad1fb734fa 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -36,6 +36,8 @@ from homeassistant.util.network import is_ip_address from .config_flow import get_client_controller from .const import ( CONF_ZONE_RUN_TIME, + DATA_API_VERSIONS, + DATA_MACHINE_FIRMWARE_UPDATE_STATUS, DATA_PROGRAMS, DATA_PROVISION_SETTINGS, DATA_RESTRICTIONS_CURRENT, @@ -51,7 +53,13 @@ DEFAULT_SSL = True CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.SENSOR, + Platform.SWITCH, + Platform.UPDATE, +] CONF_CONDITION = "condition" CONF_DEWPOINT = "dewpoint" @@ -124,8 +132,10 @@ SERVICE_RESTRICT_WATERING_SCHEMA = SERVICE_SCHEMA.extend( ) COORDINATOR_UPDATE_INTERVAL_MAP = { - DATA_PROVISION_SETTINGS: timedelta(minutes=1), + DATA_API_VERSIONS: timedelta(minutes=1), + DATA_MACHINE_FIRMWARE_UPDATE_STATUS: timedelta(seconds=15), DATA_PROGRAMS: timedelta(seconds=30), + DATA_PROVISION_SETTINGS: timedelta(minutes=1), DATA_RESTRICTIONS_CURRENT: timedelta(minutes=1), DATA_RESTRICTIONS_UNIVERSAL: timedelta(minutes=1), DATA_ZONES: timedelta(seconds=15), @@ -215,7 +225,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data: dict = {} try: - if api_category == DATA_PROGRAMS: + if api_category == DATA_API_VERSIONS: + data = await controller.api.versions() + elif api_category == DATA_MACHINE_FIRMWARE_UPDATE_STATUS: + data = await controller.machine.get_firmware_update_status() + elif api_category == DATA_PROGRAMS: data = await controller.programs.all(include_inactive=True) elif api_category == DATA_PROVISION_SETTINGS: data = await controller.provisioning.settings() @@ -414,23 +428,32 @@ class RainMachineEntity(CoordinatorEntity): """Initialize.""" super().__init__(data.coordinators[description.api_category]) - self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, data.controller.mac)}, - configuration_url=f"https://{entry.data[CONF_IP_ADDRESS]}:{entry.data[CONF_PORT]}", - connections={(dr.CONNECTION_NETWORK_MAC, data.controller.mac)}, - name=str(data.controller.name).capitalize(), - manufacturer="RainMachine", - model=( - f"Version {data.controller.hardware_version} " - f"(API: {data.controller.api_version})" - ), - sw_version=data.controller.software_version, - ) self._attr_extra_state_attributes = {} self._attr_unique_id = f"{data.controller.mac}_{description.key}" + self._entry = entry self._data = data + self._version_coordinator = data.coordinators[DATA_API_VERSIONS] self.entity_description = description + @property + def device_info(self) -> DeviceInfo: + """Return device information about this controller.""" + return DeviceInfo( + identifiers={(DOMAIN, self._data.controller.mac)}, + configuration_url=( + f"https://{self._entry.data[CONF_IP_ADDRESS]}:" + f"{self._entry.data[CONF_PORT]}" + ), + connections={(dr.CONNECTION_NETWORK_MAC, self._data.controller.mac)}, + name=str(self._data.controller.name).capitalize(), + manufacturer="RainMachine", + model=( + f"Version {self._version_coordinator.data['hwVer']} " + f"(API: {self._version_coordinator.data['apiVer']})" + ), + sw_version=self._version_coordinator.data["swVer"], + ) + @callback def _handle_coordinator_update(self) -> None: """Respond to a DataUpdateCoordinator update.""" @@ -440,6 +463,11 @@ class RainMachineEntity(CoordinatorEntity): async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() + self.async_on_remove( + self._version_coordinator.async_add_listener( + self._handle_coordinator_update, self.coordinator_context + ) + ) self.update_from_latest_data() @callback diff --git a/homeassistant/components/rainmachine/const.py b/homeassistant/components/rainmachine/const.py index f94e7011dce..d1b5bd9bd52 100644 --- a/homeassistant/components/rainmachine/const.py +++ b/homeassistant/components/rainmachine/const.py @@ -7,6 +7,8 @@ DOMAIN = "rainmachine" CONF_ZONE_RUN_TIME = "zone_run_time" +DATA_API_VERSIONS = "api.versions" +DATA_MACHINE_FIRMWARE_UPDATE_STATUS = "machine.firmware_update_status" DATA_PROGRAMS = "programs" DATA_PROVISION_SETTINGS = "provision.settings" DATA_RESTRICTIONS_CURRENT = "restrictions.current" diff --git a/homeassistant/components/rainmachine/diagnostics.py b/homeassistant/components/rainmachine/diagnostics.py index 131f2e130e7..47ded7990c6 100644 --- a/homeassistant/components/rainmachine/diagnostics.py +++ b/homeassistant/components/rainmachine/diagnostics.py @@ -1,10 +1,9 @@ """Diagnostics support for RainMachine.""" from __future__ import annotations -import asyncio from typing import Any -from regenmaschine.errors import RequestError +from regenmaschine.errors import RainMachineError from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry @@ -17,7 +16,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from . import RainMachineData -from .const import DOMAIN +from .const import DOMAIN, LOGGER CONF_STATION_ID = "stationID" CONF_STATION_NAME = "stationName" @@ -42,13 +41,11 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" data: RainMachineData = hass.data[DOMAIN][entry.entry_id] - controller_tasks = { - "versions": data.controller.api.versions(), - "current_diagnostics": data.controller.diagnostics.current(), - } - controller_results = await asyncio.gather( - *controller_tasks.values(), return_exceptions=True - ) + try: + controller_diagnostics = await data.controller.diagnostics.current() + except RainMachineError: + LOGGER.warning("Unable to download controller-specific diagnostics") + controller_diagnostics = None return { "entry": { @@ -64,10 +61,6 @@ async def async_get_config_entry_diagnostics( }, TO_REDACT, ), - "controller": { - category: result - for category, result in zip(controller_tasks, controller_results) - if not isinstance(result, RequestError) - }, + "controller_diagnostics": controller_diagnostics, }, } diff --git a/homeassistant/components/rainmachine/update.py b/homeassistant/components/rainmachine/update.py new file mode 100644 index 00000000000..b191f2695a0 --- /dev/null +++ b/homeassistant/components/rainmachine/update.py @@ -0,0 +1,102 @@ +"""Support for RainMachine updates.""" +from __future__ import annotations + +from enum import Enum +from typing import Any + +from regenmaschine.errors import RequestError + +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import RainMachineData, RainMachineEntity +from .const import DATA_MACHINE_FIRMWARE_UPDATE_STATUS, DOMAIN +from .model import RainMachineEntityDescription + + +class UpdateStates(Enum): + """Define an enum for update states.""" + + IDLE = 1 + CHECKING = 2 + DOWNLOADING = 3 + UPGRADING = 4 + ERROR = 5 + REBOOT = 6 + + +UPDATE_STATE_MAP = { + 1: UpdateStates.IDLE, + 2: UpdateStates.CHECKING, + 3: UpdateStates.DOWNLOADING, + 4: UpdateStates.UPGRADING, + 5: UpdateStates.ERROR, + 6: UpdateStates.REBOOT, +} + + +UPDATE_DESCRIPTION = RainMachineEntityDescription( + key="update", + name="Firmware", + api_category=DATA_MACHINE_FIRMWARE_UPDATE_STATUS, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up WLED update based on a config entry.""" + data: RainMachineData = hass.data[DOMAIN][entry.entry_id] + + async_add_entities([RainMachineUpdateEntity(entry, data, UPDATE_DESCRIPTION)]) + + +class RainMachineUpdateEntity(RainMachineEntity, UpdateEntity): + """Define a RainMachine update entity.""" + + _attr_device_class = UpdateDeviceClass.FIRMWARE + _attr_supported_features = ( + UpdateEntityFeature.INSTALL + | UpdateEntityFeature.PROGRESS + | UpdateEntityFeature.SPECIFIC_VERSION + ) + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update.""" + try: + await self._data.controller.machine.update_firmware() + except RequestError as err: + raise HomeAssistantError(f"Error while updating firmware: {err}") from err + + await self.coordinator.async_refresh() + + @callback + def update_from_latest_data(self) -> None: + """Update the state.""" + if version := self._version_coordinator.data["swVer"]: + self._attr_installed_version = version + else: + self._attr_installed_version = None + + data = self.coordinator.data + + if not data["update"]: + self._attr_in_progress = False + self._attr_latest_version = self._attr_installed_version + return + + self._attr_in_progress = UPDATE_STATE_MAP[data["updateStatus"]] in ( + UpdateStates.DOWNLOADING, + UpdateStates.UPGRADING, + UpdateStates.REBOOT, + ) + self._attr_latest_version = data["packageDetails"]["newVersion"] diff --git a/tests/components/rainmachine/conftest.py b/tests/components/rainmachine/conftest.py index 72b4a5d4293..1dfef7e399c 100644 --- a/tests/components/rainmachine/conftest.py +++ b/tests/components/rainmachine/conftest.py @@ -41,6 +41,7 @@ def controller_fixture( controller_mac, data_api_versions, data_diagnostics_current, + data_machine_firmare_update_status, data_programs, data_provision_settings, data_restrictions_current, @@ -59,6 +60,9 @@ def controller_fixture( controller.api.versions.return_value = data_api_versions controller.diagnostics.current.return_value = data_diagnostics_current + controller.machine.get_firmware_update_status.return_value = ( + data_machine_firmare_update_status + ) controller.programs.all.return_value = data_programs controller.provisioning.settings.return_value = data_provision_settings controller.restrictions.current.return_value = data_restrictions_current @@ -86,6 +90,14 @@ def data_diagnostics_current_fixture(): return json.loads(load_fixture("diagnostics_current_data.json", "rainmachine")) +@pytest.fixture(name="data_machine_firmare_update_status", scope="session") +def data_machine_firmare_update_status_fixture(): + """Define machine firmware update status data.""" + return json.loads( + load_fixture("machine_firmware_update_status_data.json", "rainmachine") + ) + + @pytest.fixture(name="data_programs", scope="session") def data_programs_fixture(): """Define program data.""" diff --git a/tests/components/rainmachine/fixtures/machine_firmware_update_status_data.json b/tests/components/rainmachine/fixtures/machine_firmware_update_status_data.json new file mode 100644 index 00000000000..01dd95e6136 --- /dev/null +++ b/tests/components/rainmachine/fixtures/machine_firmware_update_status_data.json @@ -0,0 +1,7 @@ +{ + "lastUpdateCheckTimestamp": 1657825288, + "packageDetails": [], + "update": false, + "lastUpdateCheck": "2022-07-14 13:01:28", + "updateStatus": 1 +} diff --git a/tests/components/rainmachine/test_diagnostics.py b/tests/components/rainmachine/test_diagnostics.py index b0876a2f597..a600c5f7c34 100644 --- a/tests/components/rainmachine/test_diagnostics.py +++ b/tests/components/rainmachine/test_diagnostics.py @@ -1,4 +1,6 @@ """Test RainMachine diagnostics.""" +from regenmaschine.errors import RainMachineError + from homeassistant.components.diagnostics import REDACTED from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -19,89 +21,13 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_rainmach }, "data": { "coordinator": { - "provision.settings": { - "system": { - "httpEnabled": True, - "rainSensorSnoozeDuration": 0, - "uiUnitsMetric": False, - "programZonesShowInactive": False, - "programSingleSchedule": False, - "standaloneMode": False, - "masterValveAfter": 0, - "touchSleepTimeout": 10, - "selfTest": False, - "useSoftwareRainSensor": False, - "defaultZoneWateringDuration": 300, - "maxLEDBrightness": 40, - "simulatorHistorySize": 0, - "vibration": False, - "masterValveBefore": 0, - "touchProgramToRun": None, - "useRainSensor": False, - "wizardHasRun": True, - "waterLogHistorySize": 365, - "netName": "Home", - "softwareRainSensorMinQPF": 5, - "touchAdvanced": False, - "useBonjourService": True, - "hardwareVersion": 3, - "touchLongPressTimeout": 3, - "showRestrictionsOnLed": False, - "parserDataSizeInDays": 6, - "programListShowInactive": True, - "parserHistorySize": 365, - "allowAlexaDiscovery": False, - "automaticUpdates": True, - "minLEDBrightness": 0, - "minWateringDurationThreshold": 0, - "localValveCount": 12, - "touchAuthAPSeconds": 60, - "useCommandLineArguments": False, - "databasePath": "/rainmachine-app/DB/Default", - "touchCyclePrograms": True, - "zoneListShowInactive": True, - "rainSensorRainStart": None, - "zoneDuration": [ - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - ], - "rainSensorIsNormallyClosed": True, - "useCorrectionForPast": True, - "useMasterValve": False, - "runParsersBeforePrograms": True, - "maxWateringCoef": 2, - "mixerHistorySize": 365, - }, - "location": { - "elevation": REDACTED, - "doyDownloaded": True, - "zip": None, - "windSensitivity": 0.5, - "krs": 0.16, - "stationID": REDACTED, - "stationSource": REDACTED, - "et0Average": 6.578, - "latitude": REDACTED, - "state": "Default", - "stationName": REDACTED, - "wsDays": 2, - "stationDownloaded": True, - "address": "Default", - "rainSensitivity": 0.8, - "timezone": REDACTED, - "longitude": REDACTED, - "name": "Home", - }, + "api.versions": {"apiVer": "4.6.1", "hwVer": 3, "swVer": "4.0.1144"}, + "machine.firmware_update_status": { + "lastUpdateCheckTimestamp": 1657825288, + "packageDetails": [], + "update": False, + "lastUpdateCheck": "2022-07-14 13:01:28", + "updateStatus": 1, }, "programs": [ { @@ -381,6 +307,90 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_rainmach ], }, ], + "provision.settings": { + "system": { + "httpEnabled": True, + "rainSensorSnoozeDuration": 0, + "uiUnitsMetric": False, + "programZonesShowInactive": False, + "programSingleSchedule": False, + "standaloneMode": False, + "masterValveAfter": 0, + "touchSleepTimeout": 10, + "selfTest": False, + "useSoftwareRainSensor": False, + "defaultZoneWateringDuration": 300, + "maxLEDBrightness": 40, + "simulatorHistorySize": 0, + "vibration": False, + "masterValveBefore": 0, + "touchProgramToRun": None, + "useRainSensor": False, + "wizardHasRun": True, + "waterLogHistorySize": 365, + "netName": "Home", + "softwareRainSensorMinQPF": 5, + "touchAdvanced": False, + "useBonjourService": True, + "hardwareVersion": 3, + "touchLongPressTimeout": 3, + "showRestrictionsOnLed": False, + "parserDataSizeInDays": 6, + "programListShowInactive": True, + "parserHistorySize": 365, + "allowAlexaDiscovery": False, + "automaticUpdates": True, + "minLEDBrightness": 0, + "minWateringDurationThreshold": 0, + "localValveCount": 12, + "touchAuthAPSeconds": 60, + "useCommandLineArguments": False, + "databasePath": "/rainmachine-app/DB/Default", + "touchCyclePrograms": True, + "zoneListShowInactive": True, + "rainSensorRainStart": None, + "zoneDuration": [ + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + ], + "rainSensorIsNormallyClosed": True, + "useCorrectionForPast": True, + "useMasterValve": False, + "runParsersBeforePrograms": True, + "maxWateringCoef": 2, + "mixerHistorySize": 365, + }, + "location": { + "elevation": REDACTED, + "doyDownloaded": True, + "zip": None, + "windSensitivity": 0.5, + "krs": 0.16, + "stationID": REDACTED, + "stationSource": REDACTED, + "et0Average": 6.578, + "latitude": REDACTED, + "state": "Default", + "stationName": REDACTED, + "wsDays": 2, + "stationDownloaded": True, + "address": "Default", + "rainSensitivity": 0.8, + "timezone": REDACTED, + "longitude": REDACTED, + "name": "Home", + }, + }, "restrictions.current": { "hourly": False, "freeze": False, @@ -582,33 +592,620 @@ async def test_entry_diagnostics(hass, config_entry, hass_client, setup_rainmach }, ], }, - "controller": { - "versions": { - "apiVer": "4.6.1", - "hwVer": 3, - "swVer": "4.0.1144", - }, - "current_diagnostics": { - "hasWifi": True, - "uptime": "3 days, 18:14:14", - "uptimeSeconds": 324854, - "memUsage": 16196, - "networkStatus": True, - "bootCompleted": True, - "lastCheckTimestamp": 1659895175, - "wizardHasRun": True, - "standaloneMode": False, - "cpuUsage": 1, - "lastCheck": "2022-08-07 11:59:35", - "softwareVersion": "4.0.1144", - "internetStatus": True, - "locationStatus": True, - "timeStatus": True, - "wifiMode": None, - "gatewayAddress": "172.16.20.1", - "cloudStatus": 0, - "weatherStatus": True, - }, + "controller_diagnostics": { + "hasWifi": True, + "uptime": "3 days, 18:14:14", + "uptimeSeconds": 324854, + "memUsage": 16196, + "networkStatus": True, + "bootCompleted": True, + "lastCheckTimestamp": 1659895175, + "wizardHasRun": True, + "standaloneMode": False, + "cpuUsage": 1, + "lastCheck": "2022-08-07 11:59:35", + "softwareVersion": "4.0.1144", + "internetStatus": True, + "locationStatus": True, + "timeStatus": True, + "wifiMode": None, + "gatewayAddress": "172.16.20.1", + "cloudStatus": 0, + "weatherStatus": True, }, }, } + + +async def test_entry_diagnostics_failed_controller_diagnostics( + hass, config_entry, controller, hass_client, setup_rainmachine +): + """Test config entry diagnostics when the controller diagnostics API call fails.""" + controller.diagnostics.current.side_effect = RainMachineError + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { + "entry": { + "title": "Mock Title", + "data": { + "ip_address": "192.168.1.100", + "password": REDACTED, + "port": 8080, + "ssl": True, + }, + "options": {}, + }, + "data": { + "coordinator": { + "api.versions": {"apiVer": "4.6.1", "hwVer": 3, "swVer": "4.0.1144"}, + "machine.firmware_update_status": { + "lastUpdateCheckTimestamp": 1657825288, + "packageDetails": [], + "update": False, + "lastUpdateCheck": "2022-07-14 13:01:28", + "updateStatus": 1, + }, + "programs": [ + { + "uid": 1, + "name": "Morning", + "active": True, + "startTime": "06:00", + "cycles": 0, + "soak": 0, + "cs_on": False, + "delay": 0, + "delay_on": False, + "status": 0, + "startTimeParams": { + "offsetSign": 0, + "type": 0, + "offsetMinutes": 0, + }, + "frequency": {"type": 0, "param": "0"}, + "coef": 0, + "ignoreInternetWeather": False, + "futureField1": 0, + "freq_modified": 0, + "useWaterSense": False, + "nextRun": "2018-06-04", + "startDate": "2018-04-28", + "endDate": None, + "yearlyRecurring": True, + "simulationExpired": False, + "wateringTimes": [ + { + "id": 1, + "order": -1, + "name": "Landscaping", + "duration": 0, + "active": True, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 2, + "order": -1, + "name": "Flower Box", + "duration": 0, + "active": True, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 3, + "order": -1, + "name": "TEST", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 4, + "order": -1, + "name": "Zone 4", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 5, + "order": -1, + "name": "Zone 5", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 6, + "order": -1, + "name": "Zone 6", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 7, + "order": -1, + "name": "Zone 7", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 8, + "order": -1, + "name": "Zone 8", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 9, + "order": -1, + "name": "Zone 9", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 10, + "order": -1, + "name": "Zone 10", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 11, + "order": -1, + "name": "Zone 11", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 12, + "order": -1, + "name": "Zone 12", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + ], + }, + { + "uid": 2, + "name": "Evening", + "active": False, + "startTime": "06:00", + "cycles": 0, + "soak": 0, + "cs_on": False, + "delay": 0, + "delay_on": False, + "status": 0, + "startTimeParams": { + "offsetSign": 0, + "type": 0, + "offsetMinutes": 0, + }, + "frequency": {"type": 0, "param": "0"}, + "coef": 0, + "ignoreInternetWeather": False, + "futureField1": 0, + "freq_modified": 0, + "useWaterSense": False, + "nextRun": "2018-06-04", + "startDate": "2018-04-28", + "endDate": None, + "yearlyRecurring": True, + "simulationExpired": False, + "wateringTimes": [ + { + "id": 1, + "order": -1, + "name": "Landscaping", + "duration": 0, + "active": True, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 2, + "order": -1, + "name": "Flower Box", + "duration": 0, + "active": True, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 3, + "order": -1, + "name": "TEST", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 4, + "order": -1, + "name": "Zone 4", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 5, + "order": -1, + "name": "Zone 5", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 6, + "order": -1, + "name": "Zone 6", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 7, + "order": -1, + "name": "Zone 7", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 8, + "order": -1, + "name": "Zone 8", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 9, + "order": -1, + "name": "Zone 9", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 10, + "order": -1, + "name": "Zone 10", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 11, + "order": -1, + "name": "Zone 11", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + { + "id": 12, + "order": -1, + "name": "Zone 12", + "duration": 0, + "active": False, + "userPercentage": 1, + "minRuntimeCoef": 1, + }, + ], + }, + ], + "provision.settings": { + "system": { + "httpEnabled": True, + "rainSensorSnoozeDuration": 0, + "uiUnitsMetric": False, + "programZonesShowInactive": False, + "programSingleSchedule": False, + "standaloneMode": False, + "masterValveAfter": 0, + "touchSleepTimeout": 10, + "selfTest": False, + "useSoftwareRainSensor": False, + "defaultZoneWateringDuration": 300, + "maxLEDBrightness": 40, + "simulatorHistorySize": 0, + "vibration": False, + "masterValveBefore": 0, + "touchProgramToRun": None, + "useRainSensor": False, + "wizardHasRun": True, + "waterLogHistorySize": 365, + "netName": "Home", + "softwareRainSensorMinQPF": 5, + "touchAdvanced": False, + "useBonjourService": True, + "hardwareVersion": 3, + "touchLongPressTimeout": 3, + "showRestrictionsOnLed": False, + "parserDataSizeInDays": 6, + "programListShowInactive": True, + "parserHistorySize": 365, + "allowAlexaDiscovery": False, + "automaticUpdates": True, + "minLEDBrightness": 0, + "minWateringDurationThreshold": 0, + "localValveCount": 12, + "touchAuthAPSeconds": 60, + "useCommandLineArguments": False, + "databasePath": "/rainmachine-app/DB/Default", + "touchCyclePrograms": True, + "zoneListShowInactive": True, + "rainSensorRainStart": None, + "zoneDuration": [ + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + 300, + ], + "rainSensorIsNormallyClosed": True, + "useCorrectionForPast": True, + "useMasterValve": False, + "runParsersBeforePrograms": True, + "maxWateringCoef": 2, + "mixerHistorySize": 365, + }, + "location": { + "elevation": REDACTED, + "doyDownloaded": True, + "zip": None, + "windSensitivity": 0.5, + "krs": 0.16, + "stationID": REDACTED, + "stationSource": REDACTED, + "et0Average": 6.578, + "latitude": REDACTED, + "state": "Default", + "stationName": REDACTED, + "wsDays": 2, + "stationDownloaded": True, + "address": "Default", + "rainSensitivity": 0.8, + "timezone": REDACTED, + "longitude": REDACTED, + "name": "Home", + }, + }, + "restrictions.current": { + "hourly": False, + "freeze": False, + "month": False, + "weekDay": False, + "rainDelay": False, + "rainDelayCounter": -1, + "rainSensor": False, + }, + "restrictions.universal": { + "hotDaysExtraWatering": False, + "freezeProtectEnabled": True, + "freezeProtectTemp": 2, + "noWaterInWeekDays": "0000000", + "noWaterInMonths": "000000000000", + "rainDelayStartTime": 1524854551, + "rainDelayDuration": 0, + }, + "zones": [ + { + "uid": 1, + "name": "Landscaping", + "state": 0, + "active": True, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 4, + "master": False, + "waterSense": False, + }, + { + "uid": 2, + "name": "Flower Box", + "state": 0, + "active": True, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 5, + "master": False, + "waterSense": False, + }, + { + "uid": 3, + "name": "TEST", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 9, + "master": False, + "waterSense": False, + }, + { + "uid": 4, + "name": "Zone 4", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 2, + "master": False, + "waterSense": False, + }, + { + "uid": 5, + "name": "Zone 5", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 2, + "master": False, + "waterSense": False, + }, + { + "uid": 6, + "name": "Zone 6", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 2, + "master": False, + "waterSense": False, + }, + { + "uid": 7, + "name": "Zone 7", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 2, + "master": False, + "waterSense": False, + }, + { + "uid": 8, + "name": "Zone 8", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 2, + "master": False, + "waterSense": False, + }, + { + "uid": 9, + "name": "Zone 9", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 2, + "master": False, + "waterSense": False, + }, + { + "uid": 10, + "name": "Zone 10", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 2, + "master": False, + "waterSense": False, + }, + { + "uid": 11, + "name": "Zone 11", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 2, + "master": False, + "waterSense": False, + }, + { + "uid": 12, + "name": "Zone 12", + "state": 0, + "active": False, + "userDuration": 0, + "machineDuration": 0, + "remaining": 0, + "cycle": 0, + "noOfCycles": 0, + "restriction": False, + "type": 2, + "master": False, + "waterSense": False, + }, + ], + }, + "controller_diagnostics": None, + }, + } From d1ab93fbaf731e3966cd80392178f9e5684f560b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 7 Aug 2022 23:45:32 +0200 Subject: [PATCH 3204/3516] Add openexchangerates config flow (#76390) --- .coveragerc | 4 +- CODEOWNERS | 1 + .../components/openexchangerates/__init__.py | 62 +++- .../openexchangerates/config_flow.py | 132 +++++++++ .../components/openexchangerates/const.py | 2 + .../openexchangerates/coordinator.py | 20 +- .../openexchangerates/manifest.json | 4 +- .../components/openexchangerates/sensor.py | 140 +++++---- .../components/openexchangerates/strings.json | 33 +++ .../openexchangerates/translations/en.json | 33 +++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + .../components/openexchangerates/__init__.py | 1 + .../components/openexchangerates/conftest.py | 39 +++ .../openexchangerates/test_config_flow.py | 268 ++++++++++++++++++ 15 files changed, 661 insertions(+), 82 deletions(-) create mode 100644 homeassistant/components/openexchangerates/config_flow.py create mode 100644 homeassistant/components/openexchangerates/strings.json create mode 100644 homeassistant/components/openexchangerates/translations/en.json create mode 100644 tests/components/openexchangerates/__init__.py create mode 100644 tests/components/openexchangerates/conftest.py create mode 100644 tests/components/openexchangerates/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 4c11fe46120..5068574df78 100644 --- a/.coveragerc +++ b/.coveragerc @@ -851,7 +851,9 @@ omit = homeassistant/components/open_meteo/weather.py homeassistant/components/opencv/* homeassistant/components/openevse/sensor.py - homeassistant/components/openexchangerates/* + homeassistant/components/openexchangerates/__init__.py + homeassistant/components/openexchangerates/coordinator.py + homeassistant/components/openexchangerates/sensor.py homeassistant/components/opengarage/__init__.py homeassistant/components/opengarage/binary_sensor.py homeassistant/components/opengarage/cover.py diff --git a/CODEOWNERS b/CODEOWNERS index 1a322b09981..e10a8a0b26c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -767,6 +767,7 @@ build.json @home-assistant/supervisor /homeassistant/components/openerz/ @misialq /tests/components/openerz/ @misialq /homeassistant/components/openexchangerates/ @MartinHjelmare +/tests/components/openexchangerates/ @MartinHjelmare /homeassistant/components/opengarage/ @danielhiversen /tests/components/opengarage/ @danielhiversen /homeassistant/components/openhome/ @bazwilliams diff --git a/homeassistant/components/openexchangerates/__init__.py b/homeassistant/components/openexchangerates/__init__.py index 93d53614bdb..1b6ab4e65f1 100644 --- a/homeassistant/components/openexchangerates/__init__.py +++ b/homeassistant/components/openexchangerates/__init__.py @@ -1 +1,61 @@ -"""The openexchangerates component.""" +"""The Open Exchange Rates integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_BASE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import BASE_UPDATE_INTERVAL, DOMAIN, LOGGER +from .coordinator import OpenexchangeratesCoordinator + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Open Exchange Rates from a config entry.""" + api_key: str = entry.data[CONF_API_KEY] + base: str = entry.data[CONF_BASE] + + # Create one coordinator per base currency per API key. + existing_coordinators: dict[str, OpenexchangeratesCoordinator] = hass.data.get( + DOMAIN, {} + ) + existing_coordinator_for_api_key = { + existing_coordinator + for config_entry_id, existing_coordinator in existing_coordinators.items() + if (config_entry := hass.config_entries.async_get_entry(config_entry_id)) + and config_entry.data[CONF_API_KEY] == api_key + } + + # Adjust update interval by coordinators per API key. + update_interval = BASE_UPDATE_INTERVAL * (len(existing_coordinator_for_api_key) + 1) + coordinator = OpenexchangeratesCoordinator( + hass, + async_get_clientsession(hass), + api_key, + base, + update_interval, + ) + + LOGGER.debug("Coordinator update interval set to: %s", update_interval) + + # Set new interval on all coordinators for this API key. + for existing_coordinator in existing_coordinator_for_api_key: + existing_coordinator.update_interval = update_interval + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/openexchangerates/config_flow.py b/homeassistant/components/openexchangerates/config_flow.py new file mode 100644 index 00000000000..3c22f3e0fe0 --- /dev/null +++ b/homeassistant/components/openexchangerates/config_flow.py @@ -0,0 +1,132 @@ +"""Config flow for Open Exchange Rates integration.""" +from __future__ import annotations + +import asyncio +from collections.abc import Mapping +from typing import Any + +from aioopenexchangerates import ( + Client, + OpenExchangeRatesAuthError, + OpenExchangeRatesClientError, +) +import async_timeout +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_BASE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CLIENT_TIMEOUT, DEFAULT_BASE, DOMAIN, LOGGER + + +def get_data_schema( + currencies: dict[str, str], existing_data: Mapping[str, str] +) -> vol.Schema: + """Return a form schema.""" + return vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Optional( + CONF_BASE, default=existing_data.get(CONF_BASE) or DEFAULT_BASE + ): vol.In(currencies), + } + ) + + +async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, str]: + """Validate the user input allows us to connect.""" + client = Client(data[CONF_API_KEY], async_get_clientsession(hass)) + + async with async_timeout.timeout(CLIENT_TIMEOUT): + await client.get_latest(base=data[CONF_BASE]) + + return {"title": data[CONF_BASE]} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Open Exchange Rates.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self.currencies: dict[str, str] = {} + self._reauth_entry: config_entries.ConfigEntry | None = None + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + currencies = await self.async_get_currencies() + + if user_input is None: + existing_data: Mapping[str, str] | dict[str, str] = ( + self._reauth_entry.data if self._reauth_entry else {} + ) + return self.async_show_form( + step_id="user", data_schema=get_data_schema(currencies, existing_data) + ) + + errors = {} + + try: + info = await validate_input(self.hass, user_input) + except OpenExchangeRatesAuthError: + errors["base"] = "invalid_auth" + except OpenExchangeRatesClientError: + errors["base"] = "cannot_connect" + except asyncio.TimeoutError: + errors["base"] = "timeout_connect" + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + self._async_abort_entries_match( + { + CONF_API_KEY: user_input[CONF_API_KEY], + CONF_BASE: user_input[CONF_BASE], + } + ) + + if self._reauth_entry is not None: + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=self._reauth_entry.data | user_input + ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=get_data_schema(currencies, user_input), + description_placeholders={"signup": "https://openexchangerates.org/signup"}, + errors=errors, + ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle reauth.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_user() + + async def async_get_currencies(self) -> dict[str, str]: + """Get the available currencies.""" + if not self.currencies: + client = Client("dummy-api-key", async_get_clientsession(self.hass)) + try: + async with async_timeout.timeout(CLIENT_TIMEOUT): + self.currencies = await client.get_currencies() + except OpenExchangeRatesClientError as err: + raise AbortFlow("cannot_connect") from err + except asyncio.TimeoutError as err: + raise AbortFlow("timeout_connect") from err + return self.currencies + + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + """Handle import from yaml/configuration.""" + return await self.async_step_user(import_config) diff --git a/homeassistant/components/openexchangerates/const.py b/homeassistant/components/openexchangerates/const.py index 2c037887489..146919cfe44 100644 --- a/homeassistant/components/openexchangerates/const.py +++ b/homeassistant/components/openexchangerates/const.py @@ -5,3 +5,5 @@ import logging DOMAIN = "openexchangerates" LOGGER = logging.getLogger(__package__) BASE_UPDATE_INTERVAL = timedelta(hours=2) +CLIENT_TIMEOUT = 10 +DEFAULT_BASE = "USD" diff --git a/homeassistant/components/openexchangerates/coordinator.py b/homeassistant/components/openexchangerates/coordinator.py index 0106edcd751..3795f33aec5 100644 --- a/homeassistant/components/openexchangerates/coordinator.py +++ b/homeassistant/components/openexchangerates/coordinator.py @@ -1,19 +1,22 @@ """Provide an OpenExchangeRates data coordinator.""" from __future__ import annotations -import asyncio from datetime import timedelta from aiohttp import ClientSession -from aioopenexchangerates import Client, Latest, OpenExchangeRatesClientError +from aioopenexchangerates import ( + Client, + Latest, + OpenExchangeRatesAuthError, + OpenExchangeRatesClientError, +) import async_timeout from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN, LOGGER - -TIMEOUT = 10 +from .const import CLIENT_TIMEOUT, DOMAIN, LOGGER class OpenexchangeratesCoordinator(DataUpdateCoordinator[Latest]): @@ -33,14 +36,15 @@ class OpenexchangeratesCoordinator(DataUpdateCoordinator[Latest]): ) self.base = base self.client = Client(api_key, session) - self.setup_lock = asyncio.Lock() async def _async_update_data(self) -> Latest: """Update data from Open Exchange Rates.""" try: - async with async_timeout.timeout(TIMEOUT): + async with async_timeout.timeout(CLIENT_TIMEOUT): latest = await self.client.get_latest(base=self.base) - except (OpenExchangeRatesClientError) as err: + except OpenExchangeRatesAuthError as err: + raise ConfigEntryAuthFailed(err) from err + except OpenExchangeRatesClientError as err: raise UpdateFailed(err) from err LOGGER.debug("Result: %s", latest) diff --git a/homeassistant/components/openexchangerates/manifest.json b/homeassistant/components/openexchangerates/manifest.json index f2377478c5f..efa67ff39e9 100644 --- a/homeassistant/components/openexchangerates/manifest.json +++ b/homeassistant/components/openexchangerates/manifest.json @@ -3,6 +3,8 @@ "name": "Open Exchange Rates", "documentation": "https://www.home-assistant.io/integrations/openexchangerates", "requirements": ["aioopenexchangerates==0.4.0"], + "dependencies": ["repairs"], "codeowners": ["@MartinHjelmare"], - "iot_class": "cloud_polling" + "iot_class": "cloud_polling", + "config_flow": true } diff --git a/homeassistant/components/openexchangerates/sensor.py b/homeassistant/components/openexchangerates/sensor.py index 337cd3050ac..7f7681b6887 100644 --- a/homeassistant/components/openexchangerates/sensor.py +++ b/homeassistant/components/openexchangerates/sensor.py @@ -1,26 +1,26 @@ """Support for openexchangerates.org exchange rates service.""" from __future__ import annotations -from dataclasses import dataclass, field - import voluptuous as vol +from homeassistant.components.repairs.issue_handler import async_create_issue +from homeassistant.components.repairs.models import IssueSeverity from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_BASE, CONF_NAME, CONF_QUOTE from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import BASE_UPDATE_INTERVAL, DOMAIN, LOGGER +from .const import DEFAULT_BASE, DOMAIN, LOGGER from .coordinator import OpenexchangeratesCoordinator ATTRIBUTION = "Data provided by openexchangerates.org" -DEFAULT_BASE = "USD" DEFAULT_NAME = "Exchange Rate Sensor" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -33,15 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -@dataclass -class DomainData: - """Data structure to hold data for this domain.""" - - coordinators: dict[tuple[str, str], OpenexchangeratesCoordinator] = field( - default_factory=dict, init=False - ) - - async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -49,56 +40,48 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Open Exchange Rates sensor.""" - name: str = config[CONF_NAME] - api_key: str = config[CONF_API_KEY] - base: str = config[CONF_BASE] - quote: str = config[CONF_QUOTE] - - integration_data: DomainData = hass.data.setdefault(DOMAIN, DomainData()) - coordinators = integration_data.coordinators - - if (api_key, base) not in coordinators: - # Create one coordinator per base currency per API key. - update_interval = BASE_UPDATE_INTERVAL * ( - len( - { - coordinator_base - for coordinator_api_key, coordinator_base in coordinators - if coordinator_api_key == api_key - } - ) - + 1 + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.11.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, ) - coordinator = coordinators[api_key, base] = OpenexchangeratesCoordinator( - hass, - async_get_clientsession(hass), - api_key, - base, - update_interval, + ) + + LOGGER.warning( + "Configuration of Open Exchange Rates integration in YAML is deprecated and " + "will be removed in Home Assistant 2022.11.; Your existing configuration " + "has been imported into the UI automatically and can be safely removed from" + " your configuration.yaml file" + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Open Exchange Rates sensor.""" + # Only YAML imported configs have name and quote in config entry data. + name: str | None = config_entry.data.get(CONF_NAME) + quote: str = config_entry.data.get(CONF_QUOTE, "EUR") + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + OpenexchangeratesSensor( + config_entry, coordinator, name, rate_quote, rate_quote == quote ) - - LOGGER.debug( - "Coordinator update interval set to: %s", coordinator.update_interval - ) - - # Set new interval on all coordinators for this API key. - for ( - coordinator_api_key, - _, - ), coordinator in coordinators.items(): - if coordinator_api_key == api_key: - coordinator.update_interval = update_interval - - coordinator = coordinators[api_key, base] - async with coordinator.setup_lock: - # We need to make sure that the coordinator data is ready. - if not coordinator.data: - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise PlatformNotReady - - async_add_entities([OpenexchangeratesSensor(coordinator, name, quote)]) + for rate_quote in coordinator.data.rates + ) class OpenexchangeratesSensor( @@ -109,20 +92,35 @@ class OpenexchangeratesSensor( _attr_attribution = ATTRIBUTION def __init__( - self, coordinator: OpenexchangeratesCoordinator, name: str, quote: str + self, + config_entry: ConfigEntry, + coordinator: OpenexchangeratesCoordinator, + name: str | None, + quote: str, + enabled: bool, ) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self._attr_name = name - self._quote = quote + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, config_entry.entry_id)}, + manufacturer="Open Exchange Rates", + name=f"Open Exchange Rates {coordinator.base}", + ) + self._attr_entity_registry_enabled_default = enabled + if name and enabled: + # name is legacy imported from YAML config + # this block can be removed when removing import from YAML + self._attr_name = name + self._attr_has_entity_name = False + else: + self._attr_name = quote + self._attr_has_entity_name = True self._attr_native_unit_of_measurement = quote + self._attr_unique_id = f"{config_entry.entry_id}_{quote}" + self._quote = quote @property def native_value(self) -> float: """Return the state of the sensor.""" return round(self.coordinator.data.rates[self._quote], 4) - - @property - def extra_state_attributes(self) -> dict[str, float]: - """Return other attributes of the sensor.""" - return self.coordinator.data.rates diff --git a/homeassistant/components/openexchangerates/strings.json b/homeassistant/components/openexchangerates/strings.json new file mode 100644 index 00000000000..57180e367aa --- /dev/null +++ b/homeassistant/components/openexchangerates/strings.json @@ -0,0 +1,33 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "base": "Base currency" + }, + "data_description": { + "base": "Using another base currency than USD requires a [paid plan]({signup})." + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" + } + }, + "issues": { + "deprecated_yaml": { + "title": "The Open Exchange Rates YAML configuration is being removed", + "description": "Configuring Open Exchange Rates using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Open Exchange Rates YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/openexchangerates/translations/en.json b/homeassistant/components/openexchangerates/translations/en.json new file mode 100644 index 00000000000..011953904ff --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/en.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "cannot_connect": "Failed to connect", + "reauth_successful": "Re-authentication was successful", + "timeout_connect": "Timeout establishing connection" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "timeout_connect": "Timeout establishing connection", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "base": "Base currency" + }, + "data_description": { + "base": "Using another base currency than USD requires a [paid plan]({signup})." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Open Exchange Rates using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Open Exchange Rates YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Open Exchange Rates YAML configuration is being removed" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6d92f7cf7e7..327781c2562 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -258,6 +258,7 @@ FLOWS = { "onewire", "onvif", "open_meteo", + "openexchangerates", "opengarage", "opentherm_gw", "openuv", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0fbc84fc0a8..7211bb3f08a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -191,6 +191,9 @@ aionotion==3.0.2 # homeassistant.components.oncue aiooncue==0.3.4 +# homeassistant.components.openexchangerates +aioopenexchangerates==0.4.0 + # homeassistant.components.acmeda aiopulse==0.4.3 diff --git a/tests/components/openexchangerates/__init__.py b/tests/components/openexchangerates/__init__.py new file mode 100644 index 00000000000..4547f25f4bc --- /dev/null +++ b/tests/components/openexchangerates/__init__.py @@ -0,0 +1 @@ +"""Tests for the Open Exchange Rates integration.""" diff --git a/tests/components/openexchangerates/conftest.py b/tests/components/openexchangerates/conftest.py new file mode 100644 index 00000000000..a1512442fd1 --- /dev/null +++ b/tests/components/openexchangerates/conftest.py @@ -0,0 +1,39 @@ +"""Provide common fixtures for tests.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.openexchangerates.const import DOMAIN + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + domain=DOMAIN, data={"api_key": "test-api-key", "base": "USD"} + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.openexchangerates.async_setup_entry", + return_value=True, + ) as mock_setup: + yield mock_setup + + +@pytest.fixture +def mock_latest_rates_config_flow( + request: pytest.FixtureRequest, +) -> Generator[AsyncMock, None, None]: + """Return a mocked WLED client.""" + with patch( + "homeassistant.components.openexchangerates.config_flow.Client.get_latest", + ) as mock_latest: + mock_latest.return_value = {"EUR": 1.0} + yield mock_latest diff --git a/tests/components/openexchangerates/test_config_flow.py b/tests/components/openexchangerates/test_config_flow.py new file mode 100644 index 00000000000..ee4ba57de2c --- /dev/null +++ b/tests/components/openexchangerates/test_config_flow.py @@ -0,0 +1,268 @@ +"""Test the Open Exchange Rates config flow.""" +import asyncio +from collections.abc import Generator +from typing import Any +from unittest.mock import AsyncMock, patch + +from aioopenexchangerates import ( + OpenExchangeRatesAuthError, + OpenExchangeRatesClientError, +) +import pytest + +from homeassistant import config_entries +from homeassistant.components.openexchangerates.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="currencies", autouse=True) +def currencies_fixture(hass: HomeAssistant) -> Generator[AsyncMock, None, None]: + """Mock currencies.""" + with patch( + "homeassistant.components.openexchangerates.config_flow.Client.get_currencies", + return_value={"USD": "United States Dollar", "EUR": "Euro"}, + ) as mock_currencies: + yield mock_currencies + + +async def test_user_create_entry( + hass: HomeAssistant, + mock_latest_rates_config_flow: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"api_key": "test-api-key"}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "USD" + assert result["data"] == { + "api_key": "test-api-key", + "base": "USD", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth( + hass: HomeAssistant, + mock_latest_rates_config_flow: AsyncMock, +) -> None: + """Test we handle invalid auth.""" + mock_latest_rates_config_flow.side_effect = OpenExchangeRatesAuthError() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"api_key": "bad-api-key"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect( + hass: HomeAssistant, + mock_latest_rates_config_flow: AsyncMock, +) -> None: + """Test we handle cannot connect error.""" + mock_latest_rates_config_flow.side_effect = OpenExchangeRatesClientError() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"api_key": "test-api-key"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_form_unknown_error( + hass: HomeAssistant, + mock_latest_rates_config_flow: AsyncMock, +) -> None: + """Test we handle unknown error.""" + mock_latest_rates_config_flow.side_effect = Exception() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"api_key": "test-api-key"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "unknown"} + + +async def test_already_configured_service( + hass: HomeAssistant, + mock_latest_rates_config_flow: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we abort if the service is already configured.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"api_key": "test-api-key"}, + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_no_currencies(hass: HomeAssistant, currencies: AsyncMock) -> None: + """Test we abort if the service fails to retrieve currencies.""" + currencies.side_effect = OpenExchangeRatesClientError() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "cannot_connect" + + +async def test_currencies_timeout(hass: HomeAssistant, currencies: AsyncMock) -> None: + """Test we abort if the service times out retrieving currencies.""" + + async def currencies_side_effect(): + await asyncio.sleep(1) + return {"USD": "United States Dollar", "EUR": "Euro"} + + currencies.side_effect = currencies_side_effect + + with patch( + "homeassistant.components.openexchangerates.config_flow.CLIENT_TIMEOUT", 0 + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "timeout_connect" + + +async def test_latest_rates_timeout( + hass: HomeAssistant, + mock_latest_rates_config_flow: AsyncMock, +) -> None: + """Test we abort if the service times out retrieving latest rates.""" + + async def latest_rates_side_effect(*args: Any, **kwargs: Any) -> dict[str, float]: + await asyncio.sleep(1) + return {"EUR": 1.0} + + mock_latest_rates_config_flow.side_effect = latest_rates_side_effect + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.openexchangerates.config_flow.CLIENT_TIMEOUT", 0 + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"api_key": "test-api-key"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "timeout_connect"} + + +async def test_reauth( + hass: HomeAssistant, + mock_latest_rates_config_flow: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we can reauthenticate the config entry.""" + mock_config_entry.add_to_hass(hass) + flow_context = { + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + "title_placeholders": {"name": mock_config_entry.title}, + "unique_id": mock_config_entry.unique_id, + } + + result = await hass.config_entries.flow.async_init( + DOMAIN, context=flow_context, data=mock_config_entry.data + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + mock_latest_rates_config_flow.side_effect = OpenExchangeRatesAuthError() + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": "invalid-test-api-key", + }, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + mock_latest_rates_config_flow.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": "new-test-api-key", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "reauth_successful" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_create_entry( + hass: HomeAssistant, + mock_latest_rates_config_flow: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test we can import data from configuration.yaml.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "api_key": "test-api-key", + "base": "USD", + "quote": "EUR", + "name": "test", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "USD" + assert result["data"] == { + "api_key": "test-api-key", + "base": "USD", + "quote": "EUR", + "name": "test", + } + assert len(mock_setup_entry.mock_calls) == 1 From f11fbf298919c306acd0ec5a4d6cb0485e9489b7 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 8 Aug 2022 00:22:41 +0000 Subject: [PATCH 3205/3516] [ci skip] Translation update --- .../components/asuswrt/translations/sl.json | 11 ++++++++ .../automation/translations/sl.json | 2 +- .../binary_sensor/translations/sl.json | 2 +- .../components/calendar/translations/sl.json | 2 +- .../components/climate/translations/sl.json | 2 +- .../components/fan/translations/sl.json | 2 +- .../flunearyou/translations/ca.json | 12 +++++++++ .../components/group/translations/sl.json | 2 +- .../translations/sensor.ca.json | 1 + .../input_boolean/translations/sl.json | 2 +- .../components/light/translations/sl.json | 2 +- .../media_player/translations/sl.json | 2 +- .../components/mysensors/translations/ca.json | 8 ++++++ .../components/mysensors/translations/de.json | 9 +++++++ .../components/mysensors/translations/fr.json | 9 +++++++ .../components/mysensors/translations/id.json | 8 ++++++ .../mysensors/translations/pt-BR.json | 9 +++++++ .../components/mysensors/translations/ru.json | 9 +++++++ .../mysensors/translations/zh-Hant.json | 9 +++++++ .../openexchangerates/translations/fr.json | 27 +++++++++++++++++++ .../components/remote/translations/sl.json | 2 +- .../components/script/translations/sl.json | 2 +- .../components/sensor/translations/sl.json | 2 +- .../simplepush/translations/ca.json | 3 +++ .../components/switch/translations/sl.json | 2 +- .../tuya/translations/select.sl.json | 8 +++--- .../components/update/translations/sl.json | 7 +++++ .../components/vacuum/translations/sl.json | 2 +- .../wled/translations/select.sl.json | 2 +- 29 files changed, 141 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/asuswrt/translations/sl.json create mode 100644 homeassistant/components/openexchangerates/translations/fr.json create mode 100644 homeassistant/components/update/translations/sl.json diff --git a/homeassistant/components/asuswrt/translations/sl.json b/homeassistant/components/asuswrt/translations/sl.json new file mode 100644 index 00000000000..fc65c3c714a --- /dev/null +++ b/homeassistant/components/asuswrt/translations/sl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Naziv" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/automation/translations/sl.json b/homeassistant/components/automation/translations/sl.json index 9045a3f3d36..bb5966be95f 100644 --- a/homeassistant/components/automation/translations/sl.json +++ b/homeassistant/components/automation/translations/sl.json @@ -1,7 +1,7 @@ { "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" } }, diff --git a/homeassistant/components/binary_sensor/translations/sl.json b/homeassistant/components/binary_sensor/translations/sl.json index 6b47f4e9e7e..e787ff1eb25 100644 --- a/homeassistant/components/binary_sensor/translations/sl.json +++ b/homeassistant/components/binary_sensor/translations/sl.json @@ -106,7 +106,7 @@ }, "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" }, "battery": { diff --git a/homeassistant/components/calendar/translations/sl.json b/homeassistant/components/calendar/translations/sl.json index bd917673e78..b8583925c7d 100644 --- a/homeassistant/components/calendar/translations/sl.json +++ b/homeassistant/components/calendar/translations/sl.json @@ -1,7 +1,7 @@ { "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" } }, diff --git a/homeassistant/components/climate/translations/sl.json b/homeassistant/components/climate/translations/sl.json index 037f807b42d..30ce6018c2e 100644 --- a/homeassistant/components/climate/translations/sl.json +++ b/homeassistant/components/climate/translations/sl.json @@ -22,7 +22,7 @@ "fan_only": "Samo ventilator", "heat": "Toplo", "heat_cool": "Gretje/Hlajenje", - "off": "Izklju\u010den" + "off": "Izklopljen" } }, "title": "Klimat" diff --git a/homeassistant/components/fan/translations/sl.json b/homeassistant/components/fan/translations/sl.json index c987bd921c8..28fe83150a2 100644 --- a/homeassistant/components/fan/translations/sl.json +++ b/homeassistant/components/fan/translations/sl.json @@ -15,7 +15,7 @@ }, "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" } }, diff --git a/homeassistant/components/flunearyou/translations/ca.json b/homeassistant/components/flunearyou/translations/ca.json index 9ae2ebd9a26..912f88d8b28 100644 --- a/homeassistant/components/flunearyou/translations/ca.json +++ b/homeassistant/components/flunearyou/translations/ca.json @@ -16,5 +16,17 @@ "title": "Configuraci\u00f3 Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "title": "Elimina Flu Near You" + } + } + }, + "title": "Flu Near You ja no est\u00e0 disponible" + } } } \ No newline at end of file diff --git a/homeassistant/components/group/translations/sl.json b/homeassistant/components/group/translations/sl.json index f810bbc6d2d..1295f420c92 100644 --- a/homeassistant/components/group/translations/sl.json +++ b/homeassistant/components/group/translations/sl.json @@ -5,7 +5,7 @@ "home": "Doma", "locked": "Zaklenjeno", "not_home": "Odsoten", - "off": "Izklju\u010den", + "off": "Izklopljen", "ok": "OK", "on": "Vklopljen", "open": "Odprto", diff --git a/homeassistant/components/homekit_controller/translations/sensor.ca.json b/homeassistant/components/homekit_controller/translations/sensor.ca.json index d8abac44cac..dde4926406c 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.ca.json +++ b/homeassistant/components/homekit_controller/translations/sensor.ca.json @@ -8,6 +8,7 @@ "sleepy": "Dispositiu final dorment" }, "homekit_controller__thread_status": { + "border_router": "Encaminador (router) frontera", "child": "Fill", "detached": "Desconnectat", "disabled": "Desactivat", diff --git a/homeassistant/components/input_boolean/translations/sl.json b/homeassistant/components/input_boolean/translations/sl.json index e0f7eb97344..535b2a2a3c6 100644 --- a/homeassistant/components/input_boolean/translations/sl.json +++ b/homeassistant/components/input_boolean/translations/sl.json @@ -1,7 +1,7 @@ { "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" } }, diff --git a/homeassistant/components/light/translations/sl.json b/homeassistant/components/light/translations/sl.json index 9bb6af3c7d5..c4662e582fe 100644 --- a/homeassistant/components/light/translations/sl.json +++ b/homeassistant/components/light/translations/sl.json @@ -19,7 +19,7 @@ }, "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" } }, diff --git a/homeassistant/components/media_player/translations/sl.json b/homeassistant/components/media_player/translations/sl.json index 6d24f4e923f..3ea9be6cab7 100644 --- a/homeassistant/components/media_player/translations/sl.json +++ b/homeassistant/components/media_player/translations/sl.json @@ -11,7 +11,7 @@ "state": { "_": { "idle": "V pripravljenosti", - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen", "paused": "Na pavzi", "playing": "Predvajanje", diff --git a/homeassistant/components/mysensors/translations/ca.json b/homeassistant/components/mysensors/translations/ca.json index 99cf16939ad..1527f2b3307 100644 --- a/homeassistant/components/mysensors/translations/ca.json +++ b/homeassistant/components/mysensors/translations/ca.json @@ -14,6 +14,7 @@ "invalid_serial": "Port s\u00e8rie inv\u00e0lid", "invalid_subscribe_topic": "Topic de subscripci\u00f3 inv\u00e0lid", "invalid_version": "Versi\u00f3 de MySensors inv\u00e0lida", + "mqtt_required": "La integraci\u00f3 MQTT no est\u00e0 configurada", "not_a_number": "Introdueix un n\u00famero", "port_out_of_range": "El n\u00famero de port ha d'estar entre 1 i 65535", "same_topic": "Els topics de publicaci\u00f3 i subscripci\u00f3 son els mateixos", @@ -68,6 +69,13 @@ }, "description": "Configuraci\u00f3 de passarel\u00b7la Ethernet" }, + "select_gateway_type": { + "menu_options": { + "gw_mqtt": "Configura passarel\u00b7la (gateway) MQTT", + "gw_serial": "Configura passarel\u00b7la (gateway) s\u00e8rie", + "gw_tcp": "Configura passarel\u00b7la (gateway) TCP" + } + }, "user": { "data": { "gateway_type": "Tipus de passarel\u00b7la" diff --git a/homeassistant/components/mysensors/translations/de.json b/homeassistant/components/mysensors/translations/de.json index 0b90165fa5a..2ab3da62431 100644 --- a/homeassistant/components/mysensors/translations/de.json +++ b/homeassistant/components/mysensors/translations/de.json @@ -14,6 +14,7 @@ "invalid_serial": "Ung\u00fcltiger Serieller Port", "invalid_subscribe_topic": "Ung\u00fcltiges Abonnementthema", "invalid_version": "Ung\u00fcltige MySensors Version", + "mqtt_required": "Die MQTT-Integration ist nicht eingerichtet", "not_a_number": "Bitte eine Nummer eingeben", "port_out_of_range": "Die Portnummer muss mindestens 1 und darf h\u00f6chstens 65535 sein", "same_topic": "Themen zum Abonnieren und Ver\u00f6ffentlichen sind gleich", @@ -68,6 +69,14 @@ }, "description": "Einrichtung des Ethernet-Gateways" }, + "select_gateway_type": { + "description": "W\u00e4hle das zu konfigurierende Gateway aus.", + "menu_options": { + "gw_mqtt": "Konfiguriere ein MQTT-Gateway", + "gw_serial": "Konfiguriere ein serielles Gateway", + "gw_tcp": "Konfiguriere ein TCP-Gateway" + } + }, "user": { "data": { "gateway_type": "Gateway-Typ" diff --git a/homeassistant/components/mysensors/translations/fr.json b/homeassistant/components/mysensors/translations/fr.json index 722bf639111..0c691e64f2f 100644 --- a/homeassistant/components/mysensors/translations/fr.json +++ b/homeassistant/components/mysensors/translations/fr.json @@ -14,6 +14,7 @@ "invalid_serial": "Port s\u00e9rie non valide", "invalid_subscribe_topic": "Sujet d'abonnement non valide", "invalid_version": "Version de MySensors non valide", + "mqtt_required": "L'int\u00e9gration MQTT n'est pas configur\u00e9e", "not_a_number": "Veuillez saisir un nombre", "port_out_of_range": "Le num\u00e9ro de port doit \u00eatre au moins 1 et au plus 65535", "same_topic": "Les sujets de souscription et de publication sont identiques", @@ -68,6 +69,14 @@ }, "description": "Configuration de la passerelle Ethernet" }, + "select_gateway_type": { + "description": "S\u00e9lectionnez la passerelle \u00e0 configurer.", + "menu_options": { + "gw_mqtt": "Configurer une passerelle MQTT", + "gw_serial": "Configurer une passerelle s\u00e9rie", + "gw_tcp": "Configurer une passerelle TCP" + } + }, "user": { "data": { "gateway_type": "Type de passerelle" diff --git a/homeassistant/components/mysensors/translations/id.json b/homeassistant/components/mysensors/translations/id.json index e6256bd8757..418da20676e 100644 --- a/homeassistant/components/mysensors/translations/id.json +++ b/homeassistant/components/mysensors/translations/id.json @@ -14,6 +14,7 @@ "invalid_serial": "Port serial tidak valid", "invalid_subscribe_topic": "Topik langganan tidak valid", "invalid_version": "Versi MySensors tidak valid", + "mqtt_required": "Integrasi MQTT belum disiapkan", "not_a_number": "Masukkan angka", "port_out_of_range": "Nilai port minimal 1 dan maksimal 65535", "same_topic": "Topik subscribe dan publish sama", @@ -68,6 +69,13 @@ }, "description": "Pengaturan gateway Ethernet" }, + "select_gateway_type": { + "menu_options": { + "gw_mqtt": "Konfigurasikan gateway MQTT", + "gw_serial": "Konfigurasikan gateway serial", + "gw_tcp": "Konfigurasikan gateway TCP" + } + }, "user": { "data": { "gateway_type": "Jenis gateway" diff --git a/homeassistant/components/mysensors/translations/pt-BR.json b/homeassistant/components/mysensors/translations/pt-BR.json index 2c0f312da72..29b8a9e9372 100644 --- a/homeassistant/components/mysensors/translations/pt-BR.json +++ b/homeassistant/components/mysensors/translations/pt-BR.json @@ -14,6 +14,7 @@ "invalid_serial": "Porta serial inv\u00e1lida", "invalid_subscribe_topic": "T\u00f3pico de inscri\u00e7\u00e3o inv\u00e1lido", "invalid_version": "Vers\u00e3o MySensors inv\u00e1lida", + "mqtt_required": "A integra\u00e7\u00e3o do MQTT n\u00e3o est\u00e1 configurada", "not_a_number": "Por favor, digite um n\u00famero", "port_out_of_range": "O n\u00famero da porta deve ser no m\u00ednimo 1 e no m\u00e1ximo 65535", "same_topic": "Subscrever e publicar t\u00f3picos s\u00e3o os mesmos", @@ -68,6 +69,14 @@ }, "description": "Configura\u00e7\u00e3o do gateway Ethernet" }, + "select_gateway_type": { + "description": "Selecione qual gateway configurar.", + "menu_options": { + "gw_mqtt": "Configurar um gateway MQTT", + "gw_serial": "Configurar um gateway serial", + "gw_tcp": "Configurar um gateway TCP" + } + }, "user": { "data": { "gateway_type": "Tipo de gateway" diff --git a/homeassistant/components/mysensors/translations/ru.json b/homeassistant/components/mysensors/translations/ru.json index 2801b8e8c00..8d24debaf33 100644 --- a/homeassistant/components/mysensors/translations/ru.json +++ b/homeassistant/components/mysensors/translations/ru.json @@ -14,6 +14,7 @@ "invalid_serial": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442.", "invalid_subscribe_topic": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u0434\u043b\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438.", "invalid_version": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u0432\u0435\u0440\u0441\u0438\u044f MySensors.", + "mqtt_required": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f MQTT \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430.", "not_a_number": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0447\u0438\u0441\u043b\u043e.", "port_out_of_range": "\u041d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u043e\u0442 1 \u0434\u043e 65535.", "same_topic": "\u0422\u043e\u043f\u0438\u043a\u0438 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438 \u0438 \u043f\u0443\u0431\u043b\u0438\u043a\u0430\u0446\u0438\u0438 \u0441\u043e\u0432\u043f\u0430\u0434\u0430\u044e\u0442.", @@ -68,6 +69,14 @@ }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 Ethernet" }, + "select_gateway_type": { + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", + "menu_options": { + "gw_mqtt": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0448\u043b\u044e\u0437 MQTT", + "gw_serial": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u0448\u043b\u044e\u0437", + "gw_tcp": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c TCP-\u0448\u043b\u044e\u0437\u0430" + } + }, "user": { "data": { "gateway_type": "\u0422\u0438\u043f \u0448\u043b\u044e\u0437\u0430" diff --git a/homeassistant/components/mysensors/translations/zh-Hant.json b/homeassistant/components/mysensors/translations/zh-Hant.json index a3ec7ba2dd7..378624f65b0 100644 --- a/homeassistant/components/mysensors/translations/zh-Hant.json +++ b/homeassistant/components/mysensors/translations/zh-Hant.json @@ -14,6 +14,7 @@ "invalid_serial": "\u5e8f\u5217\u57e0\u7121\u6548", "invalid_subscribe_topic": "\u8a02\u95b1\u4e3b\u984c\u7121\u6548", "invalid_version": "MySensors \u7248\u672c\u7121\u6548", + "mqtt_required": "MQTT \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a", "not_a_number": "\u8acb\u8f38\u5165\u6578\u5b57", "port_out_of_range": "\u8acb\u8f38\u5165\u4ecb\u65bc 1 \u81f3 65535 \u4e4b\u9593\u7684\u865f\u78bc", "same_topic": "\u8a02\u95b1\u8207\u767c\u4f48\u4e3b\u984c\u76f8\u540c", @@ -68,6 +69,14 @@ }, "description": "\u9598\u9053\u5668\u4e59\u592a\u7db2\u8def\u8a2d\u5b9a" }, + "select_gateway_type": { + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u9598\u9053\u5668\u3002", + "menu_options": { + "gw_mqtt": "\u8a2d\u5b9a MQTT \u9598\u9053\u5668", + "gw_serial": "\u8a2d\u5b9a\u5e8f\u5217\u9598\u9053\u5668", + "gw_tcp": "\u8a2d\u5b9a TCP \u9598\u9053\u5668" + } + }, "user": { "data": { "gateway_type": "\u9598\u9053\u5668\u985e\u5225" diff --git a/homeassistant/components/openexchangerates/translations/fr.json b/homeassistant/components/openexchangerates/translations/fr.json new file mode 100644 index 00000000000..a6b5929245a --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de connexion", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "timeout_connect": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "timeout_connect": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "base": "Devise de r\u00e9f\u00e9rence" + }, + "data_description": { + "base": "L'utilisation d'une devise de r\u00e9f\u00e9rence autre que l'USD n\u00e9cessite un [forfait payant]({signup})." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/remote/translations/sl.json b/homeassistant/components/remote/translations/sl.json index 5d2864f5b89..115d50127b0 100644 --- a/homeassistant/components/remote/translations/sl.json +++ b/homeassistant/components/remote/translations/sl.json @@ -1,7 +1,7 @@ { "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" } }, diff --git a/homeassistant/components/script/translations/sl.json b/homeassistant/components/script/translations/sl.json index cd38e49dcb5..f57b7a5d6b7 100644 --- a/homeassistant/components/script/translations/sl.json +++ b/homeassistant/components/script/translations/sl.json @@ -1,7 +1,7 @@ { "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" } }, diff --git a/homeassistant/components/sensor/translations/sl.json b/homeassistant/components/sensor/translations/sl.json index a83b273946c..f544b44ecfd 100644 --- a/homeassistant/components/sensor/translations/sl.json +++ b/homeassistant/components/sensor/translations/sl.json @@ -23,7 +23,7 @@ }, "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" } }, diff --git a/homeassistant/components/simplepush/translations/ca.json b/homeassistant/components/simplepush/translations/ca.json index d4e449d8a35..8755c00a076 100644 --- a/homeassistant/components/simplepush/translations/ca.json +++ b/homeassistant/components/simplepush/translations/ca.json @@ -22,6 +22,9 @@ "deprecated_yaml": { "description": "La configuraci\u00f3 de Simplepush mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML de Simplepush del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", "title": "La configuraci\u00f3 YAML de Simplepush est\u00e0 sent eliminada" + }, + "removed_yaml": { + "title": "La configuraci\u00f3 YAML de Simplepush s'ha eliminat" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/translations/sl.json b/homeassistant/components/switch/translations/sl.json index 7995033d1c2..565d84b56c2 100644 --- a/homeassistant/components/switch/translations/sl.json +++ b/homeassistant/components/switch/translations/sl.json @@ -16,7 +16,7 @@ }, "state": { "_": { - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen" } }, diff --git a/homeassistant/components/tuya/translations/select.sl.json b/homeassistant/components/tuya/translations/select.sl.json index cad127241aa..4929c0e2e76 100644 --- a/homeassistant/components/tuya/translations/select.sl.json +++ b/homeassistant/components/tuya/translations/select.sl.json @@ -5,7 +5,7 @@ }, "tuya__basic_nightvision": { "0": "Samodejno", - "1": "Izklju\u010den", + "1": "Izklopljen", "2": "Vklopljen" }, "tuya__led_type": { @@ -14,7 +14,7 @@ "led": "LED" }, "tuya__light_mode": { - "none": "Izklju\u010den", + "none": "Izklopljen", "pos": "Navedite lokacijo stikala", "relay": "Navedite stanje vklopa/izklopa" }, @@ -25,9 +25,9 @@ "tuya__relay_status": { "last": "Zapomni si zadnje stanje", "memory": "Zapomni si zadnje stanje", - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen", - "power_off": "Izklju\u010den", + "power_off": "Izklopljen", "power_on": "Vklopljen" } } diff --git a/homeassistant/components/update/translations/sl.json b/homeassistant/components/update/translations/sl.json new file mode 100644 index 00000000000..30291553a42 --- /dev/null +++ b/homeassistant/components/update/translations/sl.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "trigger_type": { + "turned_off": "{entity_name} je posodobljen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/translations/sl.json b/homeassistant/components/vacuum/translations/sl.json index faa94ef07cf..defef95f6f8 100644 --- a/homeassistant/components/vacuum/translations/sl.json +++ b/homeassistant/components/vacuum/translations/sl.json @@ -19,7 +19,7 @@ "docked": "Priklju\u010den", "error": "Napaka", "idle": "V pripravljenosti", - "off": "Izklju\u010den", + "off": "Izklopljen", "on": "Vklopljen", "paused": "Na pavzi", "returning": "Vra\u010danje na postajo" diff --git a/homeassistant/components/wled/translations/select.sl.json b/homeassistant/components/wled/translations/select.sl.json index b752ae28d89..339c39fde77 100644 --- a/homeassistant/components/wled/translations/select.sl.json +++ b/homeassistant/components/wled/translations/select.sl.json @@ -1,7 +1,7 @@ { "state": { "wled__live_override": { - "0": "Izklju\u010den", + "0": "Izklopljen", "1": "Vklopljen", "2": "Dokler se naprava znova ne za\u017eene" } From 42dd0cabb35920732bc1ceb52f37d5ae875df74c Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Mon, 8 Aug 2022 07:52:39 +0300 Subject: [PATCH 3206/3516] Fix Shelly H&T sensors rounding (#76426) --- homeassistant/components/shelly/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 5a36b5e99bf..c37bbbd5ff8 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -362,6 +362,7 @@ RPC_SENSORS: Final = { sub_key="tC", name="Temperature", native_unit_of_measurement=TEMP_CELSIUS, + value=lambda status, _: round(status, 1), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=True, @@ -392,6 +393,7 @@ RPC_SENSORS: Final = { sub_key="rh", name="Humidity", native_unit_of_measurement=PERCENTAGE, + value=lambda status, _: round(status, 1), device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=True, From bcc2be344a1c5405e12cc3de47ffbd9538b1ff8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Aug 2022 11:27:36 +0200 Subject: [PATCH 3207/3516] Bump actions/cache from 3.0.5 to 3.0.6 (#76432) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fd3909ca23a..ae7d8cfd48d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -172,7 +172,7 @@ jobs: cache: "pip" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -185,7 +185,7 @@ jobs: pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -211,7 +211,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -222,7 +222,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -260,7 +260,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -271,7 +271,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -312,7 +312,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -323,7 +323,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -353,7 +353,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -364,7 +364,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -480,7 +480,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: >- @@ -488,7 +488,7 @@ jobs: needs.info.outputs.python_cache_key }} - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: ${{ env.PIP_CACHE }} key: >- @@ -538,7 +538,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: >- @@ -570,7 +570,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: >- @@ -603,7 +603,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: >- @@ -647,7 +647,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: >- @@ -695,7 +695,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: >- @@ -749,7 +749,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.5 + uses: actions/cache@v3.0.6 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ From a1d5a4bc793cd85dbe6fd12380f7cef0b4b37d9e Mon Sep 17 00:00:00 2001 From: Laz <87186949+lazdavila@users.noreply.github.com> Date: Mon, 8 Aug 2022 19:18:42 +0930 Subject: [PATCH 3208/3516] Add Escea fireplace integration (#56039) Co-authored-by: Teemu R. Co-authored-by: J. Nick Koston Co-authored-by: Martin Hjelmare --- .coveragerc | 3 + CODEOWNERS | 2 + homeassistant/components/escea/__init__.py | 22 ++ homeassistant/components/escea/climate.py | 221 ++++++++++++++++++ homeassistant/components/escea/config_flow.py | 52 +++++ homeassistant/components/escea/const.py | 15 ++ homeassistant/components/escea/discovery.py | 75 ++++++ homeassistant/components/escea/manifest.json | 12 + homeassistant/components/escea/strings.json | 13 ++ .../components/escea/translations/en.json | 13 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/escea/__init__.py | 1 + tests/components/escea/test_config_flow.py | 117 ++++++++++ 16 files changed, 554 insertions(+) create mode 100644 homeassistant/components/escea/__init__.py create mode 100644 homeassistant/components/escea/climate.py create mode 100644 homeassistant/components/escea/config_flow.py create mode 100644 homeassistant/components/escea/const.py create mode 100644 homeassistant/components/escea/discovery.py create mode 100644 homeassistant/components/escea/manifest.json create mode 100644 homeassistant/components/escea/strings.json create mode 100644 homeassistant/components/escea/translations/en.json create mode 100644 tests/components/escea/__init__.py create mode 100644 tests/components/escea/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 5068574df78..9d839cdb5f6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -309,6 +309,9 @@ omit = homeassistant/components/epson/media_player.py homeassistant/components/epsonworkforce/sensor.py homeassistant/components/eq3btsmart/climate.py + homeassistant/components/escea/climate.py + homeassistant/components/escea/discovery.py + homeassistant/components/escea/__init__.py homeassistant/components/esphome/__init__.py homeassistant/components/esphome/binary_sensor.py homeassistant/components/esphome/button.py diff --git a/CODEOWNERS b/CODEOWNERS index e10a8a0b26c..906119e9006 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -309,6 +309,8 @@ build.json @home-assistant/supervisor /tests/components/epson/ @pszafer /homeassistant/components/epsonworkforce/ @ThaStealth /homeassistant/components/eq3btsmart/ @rytilahti +/homeassistant/components/escea/ @lazdavila +/tests/components/escea/ @lazdavila /homeassistant/components/esphome/ @OttoWinter @jesserockz /tests/components/esphome/ @OttoWinter @jesserockz /homeassistant/components/evil_genius_labs/ @balloob diff --git a/homeassistant/components/escea/__init__.py b/homeassistant/components/escea/__init__.py new file mode 100644 index 00000000000..95e6765fa95 --- /dev/null +++ b/homeassistant/components/escea/__init__.py @@ -0,0 +1,22 @@ +"""Platform for the Escea fireplace.""" + +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .discovery import async_start_discovery_service, async_stop_discovery_service + +PLATFORMS = [CLIMATE_DOMAIN] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up from a config entry.""" + await async_start_discovery_service(hass) + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload the config entry and stop discovery process.""" + await async_stop_discovery_service(hass) + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/escea/climate.py b/homeassistant/components/escea/climate.py new file mode 100644 index 00000000000..7bd7d54353c --- /dev/null +++ b/homeassistant/components/escea/climate.py @@ -0,0 +1,221 @@ +"""Support for the Escea Fireplace.""" +from __future__ import annotations + +from collections.abc import Coroutine +import logging +from typing import Any + +from pescea import Controller + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import ( + DATA_DISCOVERY_SERVICE, + DISPATCH_CONTROLLER_DISCONNECTED, + DISPATCH_CONTROLLER_DISCOVERED, + DISPATCH_CONTROLLER_RECONNECTED, + DISPATCH_CONTROLLER_UPDATE, + DOMAIN, + ESCEA_FIREPLACE, + ESCEA_MANUFACTURER, + ICON, +) + +_LOGGER = logging.getLogger(__name__) + +_ESCEA_FAN_TO_HA = { + Controller.Fan.FLAME_EFFECT: FAN_LOW, + Controller.Fan.FAN_BOOST: FAN_HIGH, + Controller.Fan.AUTO: FAN_AUTO, +} +_HA_FAN_TO_ESCEA = {v: k for k, v in _ESCEA_FAN_TO_HA.items()} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize an Escea Controller.""" + discovery_service = hass.data[DATA_DISCOVERY_SERVICE] + + @callback + def init_controller(ctrl: Controller) -> None: + """Register the controller device.""" + + _LOGGER.debug("Controller UID=%s discovered", ctrl.device_uid) + + entity = ControllerEntity(ctrl) + async_add_entities([entity]) + + # create any components not yet created + for controller in discovery_service.controllers.values(): + init_controller(controller) + + # connect to register any further components + config_entry.async_on_unload( + async_dispatcher_connect(hass, DISPATCH_CONTROLLER_DISCOVERED, init_controller) + ) + + +class ControllerEntity(ClimateEntity): + """Representation of Escea Controller.""" + + _attr_fan_modes = list(_HA_FAN_TO_ESCEA) + _attr_has_entity_name = True + _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] + _attr_icon = ICON + _attr_precision = PRECISION_WHOLE + _attr_should_poll = False + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ) + _attr_target_temperature_step = PRECISION_WHOLE + _attr_temperature_unit = TEMP_CELSIUS + + def __init__(self, controller: Controller) -> None: + """Initialise ControllerDevice.""" + self._controller = controller + + self._attr_min_temp = controller.min_temp + self._attr_max_temp = controller.max_temp + + self._attr_unique_id = controller.device_uid + + # temporary assignment to get past mypy checker + unique_id: str = controller.device_uid + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer=ESCEA_MANUFACTURER, + name=ESCEA_FIREPLACE, + ) + + self._attr_available = True + + async def async_added_to_hass(self) -> None: + """Call on adding to hass. + + Registers for connect/disconnect/update events + """ + + @callback + def controller_disconnected(ctrl: Controller, ex: Exception) -> None: + """Disconnected from controller.""" + if ctrl is not self._controller: + return + self.set_available(False, ex) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, DISPATCH_CONTROLLER_DISCONNECTED, controller_disconnected + ) + ) + + @callback + def controller_reconnected(ctrl: Controller) -> None: + """Reconnected to controller.""" + if ctrl is not self._controller: + return + self.set_available(True) + + self.async_on_remove( + async_dispatcher_connect( + self.hass, DISPATCH_CONTROLLER_RECONNECTED, controller_reconnected + ) + ) + + @callback + def controller_update(ctrl: Controller) -> None: + """Handle controller data updates.""" + if ctrl is not self._controller: + return + self.async_write_ha_state() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, DISPATCH_CONTROLLER_UPDATE, controller_update + ) + ) + + @callback + def set_available(self, available: bool, ex: Exception = None) -> None: + """Set availability for the controller.""" + if self._attr_available == available: + return + + if available: + _LOGGER.debug("Reconnected controller %s ", self._controller.device_uid) + else: + _LOGGER.debug( + "Controller %s disconnected due to exception: %s", + self._controller.device_uid, + ex, + ) + + self._attr_available = available + self.async_write_ha_state() + + @property + def hvac_mode(self) -> HVACMode: + """Return current operation ie. heat, cool, idle.""" + return HVACMode.HEAT if self._controller.is_on else HVACMode.OFF + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + return self._controller.current_temp + + @property + def target_temperature(self) -> float | None: + """Return the temperature we try to reach.""" + return self._controller.desired_temp + + @property + def fan_mode(self) -> str | None: + """Return the fan setting.""" + return _ESCEA_FAN_TO_HA[self._controller.fan] + + async def wrap_and_catch(self, coro: Coroutine) -> None: + """Catch any connection errors and set unavailable.""" + try: + await coro + except ConnectionError as ex: + self.set_available(False, ex) + else: + self.set_available(True) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is not None: + await self.wrap_and_catch(self._controller.set_desired_temp(temp)) + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + await self.wrap_and_catch(self._controller.set_fan(_HA_FAN_TO_ESCEA[fan_mode])) + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target operation mode.""" + await self.wrap_and_catch(self._controller.set_on(hvac_mode == HVACMode.HEAT)) + + async def async_turn_on(self) -> None: + """Turn the entity on.""" + await self.wrap_and_catch(self._controller.set_on(True)) + + async def async_turn_off(self) -> None: + """Turn the entity off.""" + await self.wrap_and_catch(self._controller.set_on(False)) diff --git a/homeassistant/components/escea/config_flow.py b/homeassistant/components/escea/config_flow.py new file mode 100644 index 00000000000..5d0dfea1157 --- /dev/null +++ b/homeassistant/components/escea/config_flow.py @@ -0,0 +1,52 @@ +"""Config flow for escea.""" +import asyncio +from contextlib import suppress +import logging + +from async_timeout import timeout + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_entry_flow +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import ( + DISPATCH_CONTROLLER_DISCOVERED, + DOMAIN, + ESCEA_FIREPLACE, + TIMEOUT_DISCOVERY, +) +from .discovery import async_start_discovery_service, async_stop_discovery_service + +_LOGGER = logging.getLogger(__name__) + + +async def _async_has_devices(hass: HomeAssistant) -> bool: + + controller_ready = asyncio.Event() + + @callback + def dispatch_discovered(_): + controller_ready.set() + + remove_handler = async_dispatcher_connect( + hass, DISPATCH_CONTROLLER_DISCOVERED, dispatch_discovered + ) + + discovery_service = await async_start_discovery_service(hass) + + with suppress(asyncio.TimeoutError): + async with timeout(TIMEOUT_DISCOVERY): + await controller_ready.wait() + + remove_handler() + + if not discovery_service.controllers: + await async_stop_discovery_service(hass) + _LOGGER.debug("No controllers found") + return False + + _LOGGER.debug("Controllers %s", discovery_service.controllers) + return True + + +config_entry_flow.register_discovery_flow(DOMAIN, ESCEA_FIREPLACE, _async_has_devices) diff --git a/homeassistant/components/escea/const.py b/homeassistant/components/escea/const.py new file mode 100644 index 00000000000..c35e77e2719 --- /dev/null +++ b/homeassistant/components/escea/const.py @@ -0,0 +1,15 @@ +"""Constants used by the escea component.""" + +DOMAIN = "escea" +ESCEA_MANUFACTURER = "Escea" +ESCEA_FIREPLACE = "Escea Fireplace" +ICON = "mdi:fire" + +DATA_DISCOVERY_SERVICE = "escea_discovery" + +DISPATCH_CONTROLLER_DISCOVERED = "escea_controller_discovered" +DISPATCH_CONTROLLER_DISCONNECTED = "escea_controller_disconnected" +DISPATCH_CONTROLLER_RECONNECTED = "escea_controller_reconnected" +DISPATCH_CONTROLLER_UPDATE = "escea_controller_update" + +TIMEOUT_DISCOVERY = 20 diff --git a/homeassistant/components/escea/discovery.py b/homeassistant/components/escea/discovery.py new file mode 100644 index 00000000000..0d7f3024bfc --- /dev/null +++ b/homeassistant/components/escea/discovery.py @@ -0,0 +1,75 @@ +"""Internal discovery service for Escea Fireplace.""" +from __future__ import annotations + +from pescea import ( + AbstractDiscoveryService, + Controller, + Listener, + discovery_service as pescea_discovery_service, +) + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import ( + DATA_DISCOVERY_SERVICE, + DISPATCH_CONTROLLER_DISCONNECTED, + DISPATCH_CONTROLLER_DISCOVERED, + DISPATCH_CONTROLLER_RECONNECTED, + DISPATCH_CONTROLLER_UPDATE, +) + + +class DiscoveryServiceListener(Listener): + """Discovery data and interfacing with pescea library.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialise discovery service.""" + super().__init__() + self.hass = hass + + # Listener interface + def controller_discovered(self, ctrl: Controller) -> None: + """Handle new controller discoverery.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_DISCOVERED, ctrl) + + def controller_disconnected(self, ctrl: Controller, ex: Exception) -> None: + """On disconnect from controller.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_DISCONNECTED, ctrl, ex) + + def controller_reconnected(self, ctrl: Controller) -> None: + """On reconnect to controller.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_RECONNECTED, ctrl) + + def controller_update(self, ctrl: Controller) -> None: + """System update message is received from the controller.""" + async_dispatcher_send(self.hass, DISPATCH_CONTROLLER_UPDATE, ctrl) + + +async def async_start_discovery_service( + hass: HomeAssistant, +) -> AbstractDiscoveryService: + """Set up the pescea internal discovery.""" + discovery_service = hass.data.get(DATA_DISCOVERY_SERVICE) + if discovery_service: + # Already started + return discovery_service + + # discovery local services + listener = DiscoveryServiceListener(hass) + discovery_service = pescea_discovery_service(listener) + hass.data[DATA_DISCOVERY_SERVICE] = discovery_service + + await discovery_service.start_discovery() + + return discovery_service + + +async def async_stop_discovery_service(hass: HomeAssistant) -> None: + """Stop the discovery service.""" + discovery_service = hass.data.get(DATA_DISCOVERY_SERVICE) + if not discovery_service: + return + + await discovery_service.close() + del hass.data[DATA_DISCOVERY_SERVICE] diff --git a/homeassistant/components/escea/manifest.json b/homeassistant/components/escea/manifest.json new file mode 100644 index 00000000000..4feb28e982c --- /dev/null +++ b/homeassistant/components/escea/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "escea", + "name": "Escea", + "documentation": "https://www.home-assistant.io/integrations/escea", + "codeowners": ["@lazdavila"], + "requirements": ["pescea==1.0.12"], + "config_flow": true, + "homekit": { + "models": ["Escea"] + }, + "iot_class": "local_push" +} diff --git a/homeassistant/components/escea/strings.json b/homeassistant/components/escea/strings.json new file mode 100644 index 00000000000..8744e74fd3f --- /dev/null +++ b/homeassistant/components/escea/strings.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "confirm": { + "description": "Do you want to set up an Escea fireplace?" + } + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + } +} diff --git a/homeassistant/components/escea/translations/en.json b/homeassistant/components/escea/translations/en.json new file mode 100644 index 00000000000..7d4e7b6fa8b --- /dev/null +++ b/homeassistant/components/escea/translations/en.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "No devices found on the network", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "step": { + "confirm": { + "description": "Do you want to set up an Escea fireplace?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 327781c2562..597cb10fd7f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -99,6 +99,7 @@ FLOWS = { "enphase_envoy", "environment_canada", "epson", + "escea", "esphome", "evil_genius_labs", "ezviz", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 5284eef02a7..d59d37f4579 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -426,6 +426,7 @@ HOMEKIT = { "C105X": "roku", "C135X": "roku", "EB-*": "ecobee", + "Escea": "escea", "HHKBridge*": "hive", "Healty Home Coach": "netatmo", "Iota": "abode", diff --git a/requirements_all.txt b/requirements_all.txt index 3a755023ac7..ddf22e985c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1227,6 +1227,9 @@ peco==0.0.29 # homeassistant.components.pencom pencompy==0.0.3 +# homeassistant.components.escea +pescea==1.0.12 + # homeassistant.components.aruba # homeassistant.components.cisco_ios # homeassistant.components.pandora diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7211bb3f08a..a9c55603282 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -854,6 +854,9 @@ pdunehd==1.3.2 # homeassistant.components.peco peco==0.0.29 +# homeassistant.components.escea +pescea==1.0.12 + # homeassistant.components.aruba # homeassistant.components.cisco_ios # homeassistant.components.pandora diff --git a/tests/components/escea/__init__.py b/tests/components/escea/__init__.py new file mode 100644 index 00000000000..b31616f7e58 --- /dev/null +++ b/tests/components/escea/__init__.py @@ -0,0 +1 @@ +"""Escea tests.""" diff --git a/tests/components/escea/test_config_flow.py b/tests/components/escea/test_config_flow.py new file mode 100644 index 00000000000..c4a5b323d22 --- /dev/null +++ b/tests/components/escea/test_config_flow.py @@ -0,0 +1,117 @@ +"""Tests for Escea.""" + +from collections.abc import Callable, Coroutine +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.escea.const import DOMAIN, ESCEA_FIREPLACE +from homeassistant.components.escea.discovery import DiscoveryServiceListener +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="mock_discovery_service") +def mock_discovery_service_fixture() -> AsyncMock: + """Mock discovery service.""" + discovery_service = AsyncMock() + discovery_service.controllers = {} + return discovery_service + + +@pytest.fixture(name="mock_controller") +def mock_controller_fixture() -> MagicMock: + """Mock controller.""" + controller = MagicMock() + return controller + + +def _mock_start_discovery( + discovery_service: MagicMock, controller: MagicMock +) -> Callable[[], Coroutine[None, None, None]]: + """Mock start discovery service.""" + + async def do_discovered() -> None: + """Call the listener callback.""" + listener: DiscoveryServiceListener = discovery_service.call_args[0][0] + listener.controller_discovered(controller) + + return do_discovered + + +async def test_not_found( + hass: HomeAssistant, mock_discovery_service: MagicMock +) -> None: + """Test not finding any Escea controllers.""" + + with patch( + "homeassistant.components.escea.discovery.pescea_discovery_service" + ) as discovery_service, patch( + "homeassistant.components.escea.config_flow.TIMEOUT_DISCOVERY", 0 + ): + discovery_service.return_value = mock_discovery_service + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + # Confirmation form + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_devices_found" + assert discovery_service.return_value.close.call_count == 1 + + +async def test_found( + hass: HomeAssistant, mock_controller: MagicMock, mock_discovery_service: AsyncMock +) -> None: + """Test finding an Escea controller.""" + mock_discovery_service.controllers["test-uid"] = mock_controller + + with patch( + "homeassistant.components.escea.async_setup_entry", + return_value=True, + ) as mock_setup, patch( + "homeassistant.components.escea.discovery.pescea_discovery_service" + ) as discovery_service: + discovery_service.return_value = mock_discovery_service + mock_discovery_service.start_discovery.side_effect = _mock_start_discovery( + discovery_service, mock_controller + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + # Confirmation form + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert mock_setup.call_count == 1 + + +async def test_single_instance_allowed(hass: HomeAssistant) -> None: + """Test single instance allowed.""" + config_entry = MockConfigEntry(domain=DOMAIN, title=ESCEA_FIREPLACE) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.escea.discovery.pescea_discovery_service" + ) as discovery_service: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + assert discovery_service.call_count == 0 From 7cd4be1310b3f76398b4404d3f4ecb26b9533cee Mon Sep 17 00:00:00 2001 From: Pieter Mulder Date: Mon, 8 Aug 2022 13:47:05 +0200 Subject: [PATCH 3209/3516] Add tests for the HDMI-CEC integration (#75094) * Add basic tests to the HDMI-CEC component * Add tests for the HDMI-CEC switch component * Add test for watchdog code * Start adding tests for the HDMI-CEC media player platform Also some cleanup and code move. * Add more tests for media_player And cleanup some switch tests. * Improve xfail message for features * Align test pyCEC dependency with main dependency * Make fixtures snake_case * Cleanup call asserts * Cleanup service tests * fix issues with media player tests * Cleanup MockHDMIDevice class * Cleanup watchdog tests * Add myself as code owner for the HDMI-CEC integration * Fix async fire time changed time jump * Fix event api sync context * Delint tests * Parametrize watchdog test Co-authored-by: Martin Hjelmare --- .coveragerc | 1 - CODEOWNERS | 2 + homeassistant/components/hdmi_cec/__init__.py | 2 +- .../components/hdmi_cec/manifest.json | 2 +- requirements_test_all.txt | 3 + tests/components/hdmi_cec/__init__.py | 49 ++ tests/components/hdmi_cec/conftest.py | 59 +++ tests/components/hdmi_cec/test_init.py | 469 +++++++++++++++++ .../components/hdmi_cec/test_media_player.py | 493 ++++++++++++++++++ tests/components/hdmi_cec/test_switch.py | 252 +++++++++ 10 files changed, 1329 insertions(+), 3 deletions(-) create mode 100644 tests/components/hdmi_cec/__init__.py create mode 100644 tests/components/hdmi_cec/conftest.py create mode 100644 tests/components/hdmi_cec/test_init.py create mode 100644 tests/components/hdmi_cec/test_media_player.py create mode 100644 tests/components/hdmi_cec/test_switch.py diff --git a/.coveragerc b/.coveragerc index 9d839cdb5f6..0a1430cce91 100644 --- a/.coveragerc +++ b/.coveragerc @@ -473,7 +473,6 @@ omit = homeassistant/components/harmony/remote.py homeassistant/components/harmony/util.py homeassistant/components/haveibeenpwned/sensor.py - homeassistant/components/hdmi_cec/* homeassistant/components/heatmiser/climate.py homeassistant/components/hikvision/binary_sensor.py homeassistant/components/hikvisioncam/switch.py diff --git a/CODEOWNERS b/CODEOWNERS index 906119e9006..292879aabea 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -436,6 +436,8 @@ build.json @home-assistant/supervisor /tests/components/harmony/ @ehendrix23 @bramkragten @bdraco @mkeesey @Aohzan /homeassistant/components/hassio/ @home-assistant/supervisor /tests/components/hassio/ @home-assistant/supervisor +/homeassistant/components/hdmi_cec/ @inytar +/tests/components/hdmi_cec/ @inytar /homeassistant/components/heatmiser/ @andylockran /homeassistant/components/heos/ @andrewsayre /tests/components/heos/ @andrewsayre diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 056eacb6a5b..fa64b3dac41 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -219,7 +219,7 @@ def setup(hass: HomeAssistant, base_config: ConfigType) -> bool: # noqa: C901 def _adapter_watchdog(now=None): _LOGGER.debug("Reached _adapter_watchdog") - event.async_call_later(hass, WATCHDOG_INTERVAL, _adapter_watchdog) + event.call_later(hass, WATCHDOG_INTERVAL, _adapter_watchdog) if not adapter.initialized: _LOGGER.info("Adapter not initialized; Trying to restart") hass.bus.fire(EVENT_HDMI_CEC_UNAVAILABLE) diff --git a/homeassistant/components/hdmi_cec/manifest.json b/homeassistant/components/hdmi_cec/manifest.json index 8ea56a51fa9..46e7f61719f 100644 --- a/homeassistant/components/hdmi_cec/manifest.json +++ b/homeassistant/components/hdmi_cec/manifest.json @@ -3,7 +3,7 @@ "name": "HDMI-CEC", "documentation": "https://www.home-assistant.io/integrations/hdmi_cec", "requirements": ["pyCEC==0.5.2"], - "codeowners": [], + "codeowners": ["@inytar"], "iot_class": "local_push", "loggers": ["pycec"] } diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a9c55603282..0326765cf74 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -936,6 +936,9 @@ py-synologydsm-api==1.0.8 # homeassistant.components.seventeentrack py17track==2021.12.2 +# homeassistant.components.hdmi_cec +pyCEC==0.5.2 + # homeassistant.components.control4 pyControl4==0.0.6 diff --git a/tests/components/hdmi_cec/__init__.py b/tests/components/hdmi_cec/__init__.py new file mode 100644 index 00000000000..c131bf96b41 --- /dev/null +++ b/tests/components/hdmi_cec/__init__.py @@ -0,0 +1,49 @@ +"""Tests for the HDMI-CEC component.""" +from unittest.mock import AsyncMock, Mock + +from homeassistant.components.hdmi_cec import KeyPressCommand, KeyReleaseCommand + + +class MockHDMIDevice: + """Mock of a HDMIDevice.""" + + def __init__(self, *, logical_address, **values): + """Mock of a HDMIDevice.""" + self.set_update_callback = Mock(side_effect=self._set_update_callback) + self.logical_address = logical_address + self.name = f"hdmi_{logical_address:x}" + if "power_status" not in values: + # Default to invalid state. + values["power_status"] = -1 + self._values = values + self.turn_on = Mock() + self.turn_off = Mock() + self.send_command = Mock() + self.async_send_command = AsyncMock() + + def __getattr__(self, name): + """Get attribute from `_values` if not explicitly set.""" + return self._values.get(name) + + def __setattr__(self, name, value): + """Set attributes in `_values` if not one of the known attributes.""" + if name in ("power_status", "status"): + self._values[name] = value + self._update() + else: + super().__setattr__(name, value) + + def _set_update_callback(self, update): + self._update = update + + +def assert_key_press_release(fnc, count=0, *, dst, key): + """Assert that correct KeyPressCommand & KeyReleaseCommand where sent.""" + assert fnc.call_count >= count * 2 + 1 + press_arg = fnc.call_args_list[count * 2].args[0] + release_arg = fnc.call_args_list[count * 2 + 1].args[0] + assert isinstance(press_arg, KeyPressCommand) + assert press_arg.key == key + assert press_arg.dst == dst + assert isinstance(release_arg, KeyReleaseCommand) + assert release_arg.dst == dst diff --git a/tests/components/hdmi_cec/conftest.py b/tests/components/hdmi_cec/conftest.py new file mode 100644 index 00000000000..c5f82c04e19 --- /dev/null +++ b/tests/components/hdmi_cec/conftest.py @@ -0,0 +1,59 @@ +"""Tests for the HDMI-CEC component.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.hdmi_cec import DOMAIN +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.setup import async_setup_component + + +@pytest.fixture(name="mock_cec_adapter", autouse=True) +def mock_cec_adapter_fixture(): + """Mock CecAdapter. + + Always mocked as it imports the `cec` library which is part of `libcec`. + """ + with patch( + "homeassistant.components.hdmi_cec.CecAdapter", autospec=True + ) as mock_cec_adapter: + yield mock_cec_adapter + + +@pytest.fixture(name="mock_hdmi_network") +def mock_hdmi_network_fixture(): + """Mock HDMINetwork.""" + with patch( + "homeassistant.components.hdmi_cec.HDMINetwork", autospec=True + ) as mock_hdmi_network: + yield mock_hdmi_network + + +@pytest.fixture +def create_hdmi_network(hass, mock_hdmi_network): + """Create an initialized mock hdmi_network.""" + + async def hdmi_network(config=None): + if not config: + config = {} + await async_setup_component(hass, DOMAIN, {DOMAIN: config}) + + mock_hdmi_network_instance = mock_hdmi_network.return_value + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + return mock_hdmi_network_instance + + return hdmi_network + + +@pytest.fixture +def create_cec_entity(hass): + """Create a CecEntity.""" + + async def cec_entity(hdmi_network, device): + new_device_callback = hdmi_network.set_new_device_callback.call_args.args[0] + new_device_callback(device) + await hass.async_block_till_done() + + return cec_entity diff --git a/tests/components/hdmi_cec/test_init.py b/tests/components/hdmi_cec/test_init.py new file mode 100644 index 00000000000..751c9b051f0 --- /dev/null +++ b/tests/components/hdmi_cec/test_init.py @@ -0,0 +1,469 @@ +"""Tests for the HDMI-CEC component.""" +from datetime import timedelta +from unittest.mock import ANY, PropertyMock, call, patch + +import pytest +import voluptuous as vol + +from homeassistant.components.hdmi_cec import ( + DOMAIN, + EVENT_HDMI_CEC_UNAVAILABLE, + SERVICE_POWER_ON, + SERVICE_SELECT_DEVICE, + SERVICE_SEND_COMMAND, + SERVICE_STANDBY, + SERVICE_UPDATE_DEVICES, + SERVICE_VOLUME, + WATCHDOG_INTERVAL, + CecCommand, + KeyPressCommand, + KeyReleaseCommand, + PhysicalAddress, + parse_mapping, +) +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow + +from . import assert_key_press_release + +from tests.common import ( + MockEntity, + MockEntityPlatform, + async_capture_events, + async_fire_time_changed, +) + + +@pytest.fixture(name="mock_tcp_adapter") +def mock_tcp_adapter_fixture(): + """Mock TcpAdapter.""" + with patch( + "homeassistant.components.hdmi_cec.TcpAdapter", autospec=True + ) as mock_tcp_adapter: + yield mock_tcp_adapter + + +@pytest.mark.parametrize( + "mapping,expected", + [ + ({}, []), + ( + { + "TV": "0.0.0.0", + "Pi Zero": "1.0.0.0", + "Fire TV Stick": "2.1.0.0", + "Chromecast": "2.2.0.0", + "Another Device": "2.3.0.0", + "BlueRay player": "3.0.0.0", + }, + [ + ("TV", "0.0.0.0"), + ("Pi Zero", "1.0.0.0"), + ("Fire TV Stick", "2.1.0.0"), + ("Chromecast", "2.2.0.0"), + ("Another Device", "2.3.0.0"), + ("BlueRay player", "3.0.0.0"), + ], + ), + ( + { + 1: "Pi Zero", + 2: { + 1: "Fire TV Stick", + 2: "Chromecast", + 3: "Another Device", + }, + 3: "BlueRay player", + }, + [ + ("Pi Zero", [1, 0, 0, 0]), + ("Fire TV Stick", [2, 1, 0, 0]), + ("Chromecast", [2, 2, 0, 0]), + ("Another Device", [2, 3, 0, 0]), + ("BlueRay player", [3, 0, 0, 0]), + ], + ), + ], +) +def test_parse_mapping_physical_address(mapping, expected): + """Test the device config mapping function.""" + result = parse_mapping(mapping) + result = [ + (r[0], str(r[1]) if isinstance(r[1], PhysicalAddress) else r[1]) for r in result + ] + assert result == expected + + +# Test Setup + + +async def test_setup_cec_adapter(hass, mock_cec_adapter, mock_hdmi_network): + """Test the general setup of this component.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + mock_cec_adapter.assert_called_once_with(name="HA", activate_source=False) + mock_hdmi_network.assert_called_once() + call_args = mock_hdmi_network.call_args + assert call_args == call(mock_cec_adapter.return_value, loop=ANY) + assert call_args.kwargs["loop"] in (None, hass.loop) + + mock_hdmi_network_instance = mock_hdmi_network.return_value + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + mock_hdmi_network_instance.start.assert_called_once_with() + mock_hdmi_network_instance.set_new_device_callback.assert_called_once() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + mock_hdmi_network_instance.stop.assert_called_once_with() + + +@pytest.mark.parametrize("osd_name", ["test", "test_a_long_name"]) +async def test_setup_set_osd_name(hass, osd_name, mock_cec_adapter): + """Test the setup of this component with the `osd_name` config setting.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {"osd_name": osd_name}}) + + mock_cec_adapter.assert_called_once_with(name=osd_name[:12], activate_source=False) + + +async def test_setup_tcp_adapter(hass, mock_tcp_adapter, mock_hdmi_network): + """Test the setup of this component with the TcpAdapter (`host` config setting).""" + host = "0.0.0.0" + + await async_setup_component(hass, DOMAIN, {DOMAIN: {"host": host}}) + + mock_tcp_adapter.assert_called_once_with(host, name="HA", activate_source=False) + mock_hdmi_network.assert_called_once() + call_args = mock_hdmi_network.call_args + assert call_args == call(mock_tcp_adapter.return_value, loop=ANY) + assert call_args.kwargs["loop"] in (None, hass.loop) + + mock_hdmi_network_instance = mock_hdmi_network.return_value + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + mock_hdmi_network_instance.start.assert_called_once_with() + mock_hdmi_network_instance.set_new_device_callback.assert_called_once() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + mock_hdmi_network_instance.stop.assert_called_once_with() + + +# Test services + + +async def test_service_power_on(hass, create_hdmi_network): + """Test the power on service call.""" + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_POWER_ON, + {}, + blocking=True, + ) + + mock_hdmi_network_instance.power_on.assert_called_once_with() + + +async def test_service_standby(hass, create_hdmi_network): + """Test the standby service call.""" + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_STANDBY, + {}, + blocking=True, + ) + + mock_hdmi_network_instance.standby.assert_called_once_with() + + +async def test_service_select_device_alias(hass, create_hdmi_network): + """Test the select device service call with a known alias.""" + mock_hdmi_network_instance = await create_hdmi_network( + {"devices": {"Chromecast": "1.0.0.0"}} + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_DEVICE, + {"device": "Chromecast"}, + blocking=True, + ) + + mock_hdmi_network_instance.active_source.assert_called_once() + physical_address = mock_hdmi_network_instance.active_source.call_args.args[0] + assert isinstance(physical_address, PhysicalAddress) + assert str(physical_address) == "1.0.0.0" + + +class MockCecEntity(MockEntity): + """Mock CEC entity.""" + + @property + def extra_state_attributes(self): + """Set the physical address in the attributes.""" + return {"physical_address": self._values["physical_address"]} + + +async def test_service_select_device_entity(hass, create_hdmi_network): + """Test the select device service call with an existing entity.""" + platform = MockEntityPlatform(hass) + await platform.async_add_entities( + [MockCecEntity(name="hdmi_3", physical_address="3.0.0.0")] + ) + + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_DEVICE, + {"device": "test_domain.hdmi_3"}, + blocking=True, + ) + + mock_hdmi_network_instance.active_source.assert_called_once() + physical_address = mock_hdmi_network_instance.active_source.call_args.args[0] + assert isinstance(physical_address, PhysicalAddress) + assert str(physical_address) == "3.0.0.0" + + +async def test_service_select_device_physical_address(hass, create_hdmi_network): + """Test the select device service call with a raw physical address.""" + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_SELECT_DEVICE, + {"device": "1.1.0.0"}, + blocking=True, + ) + + mock_hdmi_network_instance.active_source.assert_called_once() + physical_address = mock_hdmi_network_instance.active_source.call_args.args[0] + assert isinstance(physical_address, PhysicalAddress) + assert str(physical_address) == "1.1.0.0" + + +async def test_service_update_devices(hass, create_hdmi_network): + """Test the update devices service call.""" + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_UPDATE_DEVICES, + {}, + blocking=True, + ) + + mock_hdmi_network_instance.scan.assert_called_once_with() + + +@pytest.mark.parametrize( + "count,calls", + [ + (3, 3), + (1, 1), + (0, 0), + pytest.param( + "", + 1, + marks=pytest.mark.xfail( + reason="While the code allows for an empty string the schema doesn't allow it", + raises=vol.MultipleInvalid, + ), + ), + ], +) +@pytest.mark.parametrize("direction,key", [("up", 65), ("down", 66)]) +async def test_service_volume_x_times( + hass, create_hdmi_network, count, calls, direction, key +): + """Test the volume service call with steps.""" + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_VOLUME, + {direction: count}, + blocking=True, + ) + + assert mock_hdmi_network_instance.send_command.call_count == calls * 2 + for i in range(calls): + assert_key_press_release( + mock_hdmi_network_instance.send_command, i, dst=5, key=key + ) + + +@pytest.mark.parametrize("direction,key", [("up", 65), ("down", 66)]) +async def test_service_volume_press(hass, create_hdmi_network, direction, key): + """Test the volume service call with press attribute.""" + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_VOLUME, + {direction: "press"}, + blocking=True, + ) + + mock_hdmi_network_instance.send_command.assert_called_once() + arg = mock_hdmi_network_instance.send_command.call_args.args[0] + assert isinstance(arg, KeyPressCommand) + assert arg.key == key + assert arg.dst == 5 + + +@pytest.mark.parametrize("direction,key", [("up", 65), ("down", 66)]) +async def test_service_volume_release(hass, create_hdmi_network, direction, key): + """Test the volume service call with release attribute.""" + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_VOLUME, + {direction: "release"}, + blocking=True, + ) + + mock_hdmi_network_instance.send_command.assert_called_once() + arg = mock_hdmi_network_instance.send_command.call_args.args[0] + assert isinstance(arg, KeyReleaseCommand) + assert arg.dst == 5 + + +@pytest.mark.parametrize( + "attr,key", + [ + ("toggle", 67), + ("on", 101), + ("off", 102), + pytest.param( + "", + 101, + marks=pytest.mark.xfail( + reason="The documentation mention it's allowed to pass an empty string, but the schema does not allow this", + raises=vol.MultipleInvalid, + ), + ), + ], +) +async def test_service_volume_mute(hass, create_hdmi_network, attr, key): + """Test the volume service call with mute.""" + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_VOLUME, + {"mute": attr}, + blocking=True, + ) + + assert mock_hdmi_network_instance.send_command.call_count == 2 + assert_key_press_release(mock_hdmi_network_instance.send_command, key=key, dst=5) + + +@pytest.mark.parametrize( + "data,expected", + [ + ({"raw": "20:0D"}, "20:0d"), + pytest.param( + {"cmd": "36"}, + "ff:36", + marks=pytest.mark.xfail( + reason="String is converted in hex value, the final result looks like 'ff:24', not what you'd expect." + ), + ), + ({"cmd": 54}, "ff:36"), + pytest.param( + {"cmd": "36", "src": "1", "dst": "0"}, + "10:36", + marks=pytest.mark.xfail( + reason="String is converted in hex value, the final result looks like 'ff:24', not what you'd expect." + ), + ), + ({"cmd": 54, "src": "1", "dst": "0"}, "10:36"), + pytest.param( + {"cmd": "64", "src": "1", "dst": "0", "att": "4f:44"}, + "10:64:4f:44", + marks=pytest.mark.xfail( + reason="`att` only accepts a int or a HEX value, it seems good to allow for raw data here.", + raises=vol.MultipleInvalid, + ), + ), + pytest.param( + {"cmd": "0A", "src": "1", "dst": "0", "att": "1B"}, + "10:0a:1b", + marks=pytest.mark.xfail( + reason="The code tries to run `reduce` on this string and fails.", + raises=TypeError, + ), + ), + pytest.param( + {"cmd": "0A", "src": "1", "dst": "0", "att": "01"}, + "10:0a:1b", + marks=pytest.mark.xfail( + reason="The code tries to run `reduce` on this as an `int` and fails.", + raises=TypeError, + ), + ), + pytest.param( + {"cmd": "0A", "src": "1", "dst": "0", "att": ["1B", "44"]}, + "10:0a:1b:44", + marks=pytest.mark.xfail( + reason="While the code shows that it's possible to passthrough a list, the call schema does not allow it.", + raises=(vol.MultipleInvalid, TypeError), + ), + ), + ], +) +async def test_service_send_command(hass, create_hdmi_network, data, expected): + """Test the send command service call.""" + mock_hdmi_network_instance = await create_hdmi_network() + + await hass.services.async_call( + DOMAIN, + SERVICE_SEND_COMMAND, + data, + blocking=True, + ) + + mock_hdmi_network_instance.send_command.assert_called_once() + command = mock_hdmi_network_instance.send_command.call_args.args[0] + assert isinstance(command, CecCommand) + assert str(command) == expected + + +@pytest.mark.parametrize( + "adapter_initialized_value, watchdog_actions", [(False, 1), (True, 0)] +) +async def test_watchdog( + hass, + create_hdmi_network, + mock_cec_adapter, + adapter_initialized_value, + watchdog_actions, +): + """Test the watchdog when adapter is down/up.""" + adapter_initialized = PropertyMock(return_value=adapter_initialized_value) + events = async_capture_events(hass, EVENT_HDMI_CEC_UNAVAILABLE) + + mock_cec_adapter_instance = mock_cec_adapter.return_value + type(mock_cec_adapter_instance).initialized = adapter_initialized + + mock_hdmi_network_instance = await create_hdmi_network() + + mock_hdmi_network_instance.set_initialized_callback.assert_called_once() + callback = mock_hdmi_network_instance.set_initialized_callback.call_args.args[0] + callback() + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=WATCHDOG_INTERVAL)) + await hass.async_block_till_done() + + adapter_initialized.assert_called_once_with() + assert len(events) == watchdog_actions + assert mock_cec_adapter_instance.init.call_count == watchdog_actions diff --git a/tests/components/hdmi_cec/test_media_player.py b/tests/components/hdmi_cec/test_media_player.py new file mode 100644 index 00000000000..861134e2715 --- /dev/null +++ b/tests/components/hdmi_cec/test_media_player.py @@ -0,0 +1,493 @@ +"""Tests for the HDMI-CEC media player platform.""" +from pycec.const import ( + DEVICE_TYPE_NAMES, + KEY_BACKWARD, + KEY_FORWARD, + KEY_MUTE_TOGGLE, + KEY_PAUSE, + KEY_PLAY, + KEY_STOP, + KEY_VOLUME_DOWN, + KEY_VOLUME_UP, + POWER_OFF, + POWER_ON, + STATUS_PLAY, + STATUS_STILL, + STATUS_STOP, + TYPE_AUDIO, + TYPE_PLAYBACK, + TYPE_RECORDER, + TYPE_TUNER, + TYPE_TV, + TYPE_UNKNOWN, +) +import pytest + +from homeassistant.components.hdmi_cec import EVENT_HDMI_CEC_UNAVAILABLE +from homeassistant.components.media_player import ( + DOMAIN as MEDIA_PLAYER_DOMAIN, + MediaPlayerEntityFeature as MPEF, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_MEDIA_NEXT_TRACK, + SERVICE_MEDIA_PAUSE, + SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PLAY_PAUSE, + SERVICE_MEDIA_PREVIOUS_TRACK, + SERVICE_MEDIA_STOP, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_UP, + STATE_IDLE, + STATE_OFF, + STATE_ON, + STATE_PAUSED, + STATE_PLAYING, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) + +from . import MockHDMIDevice, assert_key_press_release + + +@pytest.fixture( + name="assert_state", + params=[ + False, + pytest.param( + True, + marks=pytest.mark.xfail( + reason="""State isn't updated because the function is missing the + `schedule_update_ha_state` for a correct push entity. Would still + update once the data comes back from the device.""" + ), + ), + ], + ids=["skip_assert_state", "run_assert_state"], +) +def assert_state_fixture(hass, request): + """Allow for skipping the assert state changes. + + This is broken in this entity, but we still want to test that + the rest of the code works as expected. + """ + + def test_state(state, expected): + if request.param: + assert state == expected + else: + assert True + + return test_state + + +async def test_load_platform(hass, create_hdmi_network, create_cec_entity): + """Test that media_player entity is loaded.""" + hdmi_network = await create_hdmi_network(config={"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice(logical_address=3) + await create_cec_entity(hdmi_network, mock_hdmi_device) + mock_hdmi_device.set_update_callback.assert_called_once() + state = hass.states.get("media_player.hdmi_3") + assert state is not None + + state = hass.states.get("switch.hdmi_3") + assert state is None + + +@pytest.mark.parametrize("platform", [{}, {"platform": "switch"}]) +async def test_load_types(hass, create_hdmi_network, create_cec_entity, platform): + """Test that media_player entity is loaded when types is set.""" + config = platform | {"types": {"hdmi_cec.hdmi_4": "media_player"}} + hdmi_network = await create_hdmi_network(config=config) + mock_hdmi_device = MockHDMIDevice(logical_address=3) + await create_cec_entity(hdmi_network, mock_hdmi_device) + mock_hdmi_device.set_update_callback.assert_called_once() + state = hass.states.get("media_player.hdmi_3") + assert state is None + + state = hass.states.get("switch.hdmi_3") + assert state is not None + + mock_hdmi_device = MockHDMIDevice(logical_address=4) + await create_cec_entity(hdmi_network, mock_hdmi_device) + mock_hdmi_device.set_update_callback.assert_called_once() + state = hass.states.get("media_player.hdmi_4") + assert state is not None + + state = hass.states.get("switch.hdmi_4") + assert state is None + + +async def test_service_on(hass, create_hdmi_network, create_cec_entity, assert_state): + """Test that media_player triggers on `on` service.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice(logical_address=3) + await create_cec_entity(hdmi_network, mock_hdmi_device) + state = hass.states.get("media_player.hdmi_3") + assert state.state != STATE_ON + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "media_player.hdmi_3"}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_hdmi_device.turn_on.assert_called_once_with() + + state = hass.states.get("media_player.hdmi_3") + assert_state(state.state, STATE_ON) + + +async def test_service_off(hass, create_hdmi_network, create_cec_entity, assert_state): + """Test that media_player triggers on `off` service.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice(logical_address=3) + await create_cec_entity(hdmi_network, mock_hdmi_device) + state = hass.states.get("media_player.hdmi_3") + assert state.state != STATE_OFF + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "media_player.hdmi_3"}, + blocking=True, + ) + + mock_hdmi_device.turn_off.assert_called_once_with() + + state = hass.states.get("media_player.hdmi_3") + assert_state(state.state, STATE_OFF) + + +@pytest.mark.parametrize( + "type_id,expected_features", + [ + (TYPE_TV, (MPEF.TURN_ON, MPEF.TURN_OFF)), + ( + TYPE_RECORDER, + ( + MPEF.TURN_ON, + MPEF.TURN_OFF, + MPEF.PAUSE, + MPEF.STOP, + MPEF.PREVIOUS_TRACK, + MPEF.NEXT_TRACK, + ), + ), + pytest.param( + TYPE_RECORDER, + (MPEF.PLAY,), + marks=pytest.mark.xfail( + reason="The feature is wrongly set to PLAY_MEDIA, but should be PLAY." + ), + ), + (TYPE_UNKNOWN, (MPEF.TURN_ON, MPEF.TURN_OFF)), + pytest.param( + TYPE_TUNER, + ( + MPEF.TURN_ON, + MPEF.TURN_OFF, + MPEF.PAUSE, + MPEF.STOP, + ), + marks=pytest.mark.xfail( + reason="Checking for the wrong attribute, should be checking `type_id`, is checking `type`." + ), + ), + pytest.param( + TYPE_TUNER, + (MPEF.PLAY,), + marks=pytest.mark.xfail( + reason="The feature is wrongly set to PLAY_MEDIA, but should be PLAY." + ), + ), + pytest.param( + TYPE_PLAYBACK, + ( + MPEF.TURN_ON, + MPEF.TURN_OFF, + MPEF.PAUSE, + MPEF.STOP, + MPEF.PREVIOUS_TRACK, + MPEF.NEXT_TRACK, + ), + marks=pytest.mark.xfail( + reason="Checking for the wrong attribute, should be checking `type_id`, is checking `type`." + ), + ), + pytest.param( + TYPE_PLAYBACK, + (MPEF.PLAY,), + marks=pytest.mark.xfail( + reason="The feature is wrongly set to PLAY_MEDIA, but should be PLAY." + ), + ), + ( + TYPE_AUDIO, + ( + MPEF.TURN_ON, + MPEF.TURN_OFF, + MPEF.VOLUME_STEP, + MPEF.VOLUME_MUTE, + ), + ), + ], +) +async def test_supported_features( + hass, create_hdmi_network, create_cec_entity, type_id, expected_features +): + """Test that features load as expected.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice( + logical_address=3, type=type_id, type_name=DEVICE_TYPE_NAMES[type_id] + ) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + state = hass.states.get("media_player.hdmi_3") + supported_features = state.attributes["supported_features"] + for feature in expected_features: + assert supported_features & feature + + +@pytest.mark.parametrize( + "service,extra_data,key", + [ + (SERVICE_VOLUME_DOWN, None, KEY_VOLUME_DOWN), + (SERVICE_VOLUME_UP, None, KEY_VOLUME_UP), + (SERVICE_VOLUME_MUTE, {"is_volume_muted": True}, KEY_MUTE_TOGGLE), + (SERVICE_VOLUME_MUTE, {"is_volume_muted": False}, KEY_MUTE_TOGGLE), + ], +) +async def test_volume_services( + hass, create_hdmi_network, create_cec_entity, service, extra_data, key +): + """Test volume related commands.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice(logical_address=3, type=TYPE_AUDIO) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + data = {ATTR_ENTITY_ID: "media_player.hdmi_3"} + if extra_data: + data |= extra_data + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + service, + data, + blocking=True, + ) + await hass.async_block_till_done() + + assert mock_hdmi_device.send_command.call_count == 2 + assert_key_press_release(mock_hdmi_device.send_command, dst=3, key=key) + + +@pytest.mark.parametrize( + "service,key", + [ + (SERVICE_MEDIA_NEXT_TRACK, KEY_FORWARD), + (SERVICE_MEDIA_PREVIOUS_TRACK, KEY_BACKWARD), + ], +) +async def test_track_change_services( + hass, create_hdmi_network, create_cec_entity, service, key +): + """Test track change related commands.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice(logical_address=3, type=TYPE_RECORDER) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + service, + {ATTR_ENTITY_ID: "media_player.hdmi_3"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert mock_hdmi_device.send_command.call_count == 2 + assert_key_press_release(mock_hdmi_device.send_command, dst=3, key=key) + + +@pytest.mark.parametrize( + "service,key,expected_state", + [ + pytest.param( + SERVICE_MEDIA_PLAY, + KEY_PLAY, + STATE_PLAYING, + marks=pytest.mark.xfail( + reason="The wrong feature is defined, should be PLAY, not PLAY_MEDIA" + ), + ), + (SERVICE_MEDIA_PAUSE, KEY_PAUSE, STATE_PAUSED), + (SERVICE_MEDIA_STOP, KEY_STOP, STATE_IDLE), + ], +) +async def test_playback_services( + hass, + create_hdmi_network, + create_cec_entity, + assert_state, + service, + key, + expected_state, +): + """Test playback related commands.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice(logical_address=3, type=TYPE_RECORDER) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + service, + {ATTR_ENTITY_ID: "media_player.hdmi_3"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert mock_hdmi_device.send_command.call_count == 2 + assert_key_press_release(mock_hdmi_device.send_command, dst=3, key=key) + + state = hass.states.get("media_player.hdmi_3") + assert_state(state.state, expected_state) + + +@pytest.mark.xfail(reason="PLAY feature isn't enabled") +async def test_play_pause_service( + hass, + create_hdmi_network, + create_cec_entity, + assert_state, +): + """Test play pause service.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice( + logical_address=3, type=TYPE_RECORDER, status=STATUS_PLAY + ) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_MEDIA_PLAY_PAUSE, + {ATTR_ENTITY_ID: "media_player.hdmi_3"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert mock_hdmi_device.send_command.call_count == 2 + assert_key_press_release(mock_hdmi_device.send_command, dst=3, key=KEY_PAUSE) + + state = hass.states.get("media_player.hdmi_3") + assert_state(state.state, STATE_PAUSED) + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_MEDIA_PLAY_PAUSE, + {ATTR_ENTITY_ID: "media_player.hdmi_3"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert mock_hdmi_device.send_command.call_count == 4 + assert_key_press_release(mock_hdmi_device.send_command, 1, dst=3, key=KEY_PLAY) + + +@pytest.mark.parametrize( + "type_id,update_data,expected_state", + [ + (TYPE_TV, {"power_status": POWER_OFF}, STATE_OFF), + (TYPE_TV, {"power_status": 3}, STATE_OFF), + (TYPE_TV, {"power_status": POWER_ON}, STATE_ON), + (TYPE_TV, {"power_status": 4}, STATE_ON), + (TYPE_TV, {"power_status": POWER_ON, "status": STATUS_PLAY}, STATE_ON), + (TYPE_RECORDER, {"power_status": POWER_OFF, "status": STATUS_PLAY}, STATE_OFF), + ( + TYPE_RECORDER, + {"power_status": POWER_ON, "status": STATUS_PLAY}, + STATE_PLAYING, + ), + (TYPE_RECORDER, {"power_status": POWER_ON, "status": STATUS_STOP}, STATE_IDLE), + ( + TYPE_RECORDER, + {"power_status": POWER_ON, "status": STATUS_STILL}, + STATE_PAUSED, + ), + (TYPE_RECORDER, {"power_status": POWER_ON, "status": None}, STATE_UNKNOWN), + ], +) +async def test_update_state( + hass, create_hdmi_network, create_cec_entity, type_id, update_data, expected_state +): + """Test state updates work as expected.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice(logical_address=3, type=type_id) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + for att, val in update_data.items(): + setattr(mock_hdmi_device, att, val) + await hass.async_block_till_done() + + state = hass.states.get("media_player.hdmi_3") + assert state.state == expected_state + + +@pytest.mark.parametrize( + "data,expected_state", + [ + ({"power_status": POWER_OFF}, STATE_OFF), + ({"power_status": 3}, STATE_OFF), + ({"power_status": POWER_ON, "type": TYPE_TV}, STATE_ON), + ({"power_status": 4, "type": TYPE_TV}, STATE_ON), + ({"power_status": POWER_ON, "type": TYPE_TV, "status": STATUS_PLAY}, STATE_ON), + ( + {"power_status": POWER_OFF, "type": TYPE_RECORDER, "status": STATUS_PLAY}, + STATE_OFF, + ), + ( + {"power_status": POWER_ON, "type": TYPE_RECORDER, "status": STATUS_PLAY}, + STATE_PLAYING, + ), + ( + {"power_status": POWER_ON, "type": TYPE_RECORDER, "status": STATUS_STOP}, + STATE_IDLE, + ), + ( + {"power_status": POWER_ON, "type": TYPE_RECORDER, "status": STATUS_STILL}, + STATE_PAUSED, + ), + ( + {"power_status": POWER_ON, "type": TYPE_RECORDER, "status": None}, + STATE_UNKNOWN, + ), + ], +) +async def test_starting_state( + hass, create_hdmi_network, create_cec_entity, data, expected_state +): + """Test starting states are set as expected.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice(logical_address=3, **data) + await create_cec_entity(hdmi_network, mock_hdmi_device) + state = hass.states.get("media_player.hdmi_3") + assert state.state == expected_state + + +@pytest.mark.xfail( + reason="The code only sets the state to unavailable, doesn't set the `_attr_available` to false." +) +async def test_unavailable_status(hass, create_hdmi_network, create_cec_entity): + """Test entity goes into unavailable status when expected.""" + hdmi_network = await create_hdmi_network({"platform": "media_player"}) + mock_hdmi_device = MockHDMIDevice(logical_address=3) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + hass.bus.async_fire(EVENT_HDMI_CEC_UNAVAILABLE) + + state = hass.states.get("media_player.hdmi_3") + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/hdmi_cec/test_switch.py b/tests/components/hdmi_cec/test_switch.py new file mode 100644 index 00000000000..61e1e03e4a6 --- /dev/null +++ b/tests/components/hdmi_cec/test_switch.py @@ -0,0 +1,252 @@ +"""Tests for the HDMI-CEC switch platform.""" +import pytest + +from homeassistant.components.hdmi_cec import ( + EVENT_HDMI_CEC_UNAVAILABLE, + POWER_OFF, + POWER_ON, + STATUS_PLAY, + STATUS_STILL, + STATUS_STOP, + PhysicalAddress, +) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) + +from tests.components.hdmi_cec import MockHDMIDevice + + +@pytest.mark.parametrize("config", [{}, {"platform": "switch"}]) +async def test_load_platform(hass, create_hdmi_network, create_cec_entity, config): + """Test that switch entity is loaded.""" + hdmi_network = await create_hdmi_network(config=config) + mock_hdmi_device = MockHDMIDevice(logical_address=3) + await create_cec_entity(hdmi_network, mock_hdmi_device) + mock_hdmi_device.set_update_callback.assert_called_once() + state = hass.states.get("media_player.hdmi_3") + assert state is None + + state = hass.states.get("switch.hdmi_3") + assert state is not None + + +async def test_load_types(hass, create_hdmi_network, create_cec_entity): + """Test that switch entity is loaded when types is set.""" + config = {"platform": "media_player", "types": {"hdmi_cec.hdmi_3": "switch"}} + hdmi_network = await create_hdmi_network(config=config) + mock_hdmi_device = MockHDMIDevice(logical_address=3) + await create_cec_entity(hdmi_network, mock_hdmi_device) + mock_hdmi_device.set_update_callback.assert_called_once() + state = hass.states.get("media_player.hdmi_3") + assert state is None + + state = hass.states.get("switch.hdmi_3") + assert state is not None + + mock_hdmi_device = MockHDMIDevice(logical_address=4) + await create_cec_entity(hdmi_network, mock_hdmi_device) + mock_hdmi_device.set_update_callback.assert_called_once() + state = hass.states.get("media_player.hdmi_4") + assert state is not None + + state = hass.states.get("switch.hdmi_4") + assert state is None + + +async def test_service_on(hass, create_hdmi_network, create_cec_entity): + """Test that switch triggers on `on` service.""" + hdmi_network = await create_hdmi_network() + mock_hdmi_device = MockHDMIDevice(logical_address=3, power_status=3) + await create_cec_entity(hdmi_network, mock_hdmi_device) + state = hass.states.get("switch.hdmi_3") + assert state.state != STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "switch.hdmi_3"}, blocking=True + ) + + mock_hdmi_device.turn_on.assert_called_once_with() + + state = hass.states.get("switch.hdmi_3") + assert state.state == STATE_ON + + +async def test_service_off(hass, create_hdmi_network, create_cec_entity): + """Test that switch triggers on `off` service.""" + hdmi_network = await create_hdmi_network() + mock_hdmi_device = MockHDMIDevice(logical_address=3, power_status=4) + await create_cec_entity(hdmi_network, mock_hdmi_device) + state = hass.states.get("switch.hdmi_3") + assert state.state != STATE_OFF + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.hdmi_3"}, + blocking=True, + ) + + mock_hdmi_device.turn_off.assert_called_once_with() + + state = hass.states.get("switch.hdmi_3") + assert state.state == STATE_OFF + + +@pytest.mark.parametrize( + "power_status,expected_state", + [(3, STATE_OFF), (POWER_OFF, STATE_OFF), (4, STATE_ON), (POWER_ON, STATE_ON)], +) +@pytest.mark.parametrize( + "status", + [ + None, + STATUS_PLAY, + STATUS_STOP, + STATUS_STILL, + ], +) +async def test_device_status_change( + hass, create_hdmi_network, create_cec_entity, power_status, expected_state, status +): + """Test state change on device status change.""" + hdmi_network = await create_hdmi_network() + mock_hdmi_device = MockHDMIDevice(logical_address=3, status=status) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + mock_hdmi_device.power_status = power_status + await hass.async_block_till_done() + + state = hass.states.get("switch.hdmi_3") + if power_status in (POWER_ON, 4) and status is not None: + pytest.xfail( + reason="`CecSwitchEntity.is_on` returns `False` here instead of `true` as expected." + ) + assert state.state == expected_state + + +@pytest.mark.parametrize( + "device_values, expected", + [ + ({"osd_name": "Switch", "vendor": "Nintendo"}, "Nintendo Switch"), + ({"type_name": "TV"}, "TV 3"), + ({"type_name": "Playback", "osd_name": "Switch"}, "Playback 3 (Switch)"), + ({"type_name": "TV", "vendor": "Samsung"}, "TV 3"), + ( + {"type_name": "Playback", "osd_name": "Super PC", "vendor": "Unknown"}, + "Playback 3 (Super PC)", + ), + ], +) +async def test_friendly_name( + hass, create_hdmi_network, create_cec_entity, device_values, expected +): + """Test friendly name setup.""" + hdmi_network = await create_hdmi_network() + mock_hdmi_device = MockHDMIDevice(logical_address=3, **device_values) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + state = hass.states.get("switch.hdmi_3") + assert state.attributes["friendly_name"] == expected + + +@pytest.mark.parametrize( + "device_values,expected_attributes", + [ + ( + {"physical_address": PhysicalAddress("3.0.0.0")}, + {"physical_address": "3.0.0.0"}, + ), + pytest.param( + {}, + {}, + marks=pytest.mark.xfail( + reason="physical address logic returns a string 'None' instead of not being set." + ), + ), + ( + {"physical_address": PhysicalAddress("3.0.0.0"), "vendor_id": 5}, + {"physical_address": "3.0.0.0", "vendor_id": 5, "vendor_name": None}, + ), + ( + { + "physical_address": PhysicalAddress("3.0.0.0"), + "vendor_id": 5, + "vendor": "Samsung", + }, + {"physical_address": "3.0.0.0", "vendor_id": 5, "vendor_name": "Samsung"}, + ), + ( + {"physical_address": PhysicalAddress("3.0.0.0"), "type": 1}, + {"physical_address": "3.0.0.0", "type_id": 1, "type": None}, + ), + ( + { + "physical_address": PhysicalAddress("3.0.0.0"), + "type": 1, + "type_name": "TV", + }, + {"physical_address": "3.0.0.0", "type_id": 1, "type": "TV"}, + ), + ], +) +async def test_extra_state_attributes( + hass, create_hdmi_network, create_cec_entity, device_values, expected_attributes +): + """Test extra state attributes.""" + hdmi_network = await create_hdmi_network() + mock_hdmi_device = MockHDMIDevice(logical_address=3, **device_values) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + state = hass.states.get("switch.hdmi_3") + attributes = state.attributes + # We don't care about these attributes, so just copy them to the expected attributes + for att in ("friendly_name", "icon"): + expected_attributes[att] = attributes[att] + assert attributes == expected_attributes + + +@pytest.mark.parametrize( + "device_type,expected_icon", + [ + (None, "mdi:help"), + (0, "mdi:television"), + (1, "mdi:microphone"), + (2, "mdi:help"), + (3, "mdi:radio"), + (4, "mdi:play"), + (5, "mdi:speaker"), + ], +) +async def test_icon( + hass, create_hdmi_network, create_cec_entity, device_type, expected_icon +): + """Test icon selection.""" + hdmi_network = await create_hdmi_network() + mock_hdmi_device = MockHDMIDevice(logical_address=3, type=device_type) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + state = hass.states.get("switch.hdmi_3") + assert state.attributes["icon"] == expected_icon + + +@pytest.mark.xfail( + reason="The code only sets the state to unavailable, doesn't set the `_attr_available` to false." +) +async def test_unavailable_status(hass, create_hdmi_network, create_cec_entity): + """Test entity goes into unavailable status when expected.""" + hdmi_network = await create_hdmi_network() + mock_hdmi_device = MockHDMIDevice(logical_address=3) + await create_cec_entity(hdmi_network, mock_hdmi_device) + + hass.bus.async_fire(EVENT_HDMI_CEC_UNAVAILABLE) + await hass.async_block_till_done() + + state = hass.states.get("switch.hdmi_3") + assert state.state == STATE_UNAVAILABLE From 3026a70f961438546705c35756b0c1771a967e49 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 8 Aug 2022 14:55:23 +0200 Subject: [PATCH 3210/3516] Improve type hints in zwave_js select entity (#76449) --- homeassistant/components/zwave_js/select.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave_js/select.py b/homeassistant/components/zwave_js/select.py index f61149e5de7..81e60582764 100644 --- a/homeassistant/components/zwave_js/select.py +++ b/homeassistant/components/zwave_js/select.py @@ -82,7 +82,7 @@ class ZwaveSelectEntity(ZWaveBaseEntity, SelectEntity): ) ) - async def async_select_option(self, option: str | int) -> None: + async def async_select_option(self, option: str) -> None: """Change the selected option.""" key = next( key @@ -133,7 +133,7 @@ class ZwaveDefaultToneSelectEntity(ZWaveBaseEntity, SelectEntity): ) ) - async def async_select_option(self, option: str | int) -> None: + async def async_select_option(self, option: str) -> None: """Change the selected option.""" # We know we can assert because this value is part of the discovery schema assert self._tones_value From fe9c101817b392137814d578a5372c6932d7093a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 8 Aug 2022 15:22:22 +0200 Subject: [PATCH 3211/3516] Improve select type hints (#76446) --- homeassistant/components/advantage_air/select.py | 4 ++-- homeassistant/components/xiaomi_miio/select.py | 2 +- homeassistant/components/yamaha_musiccast/select.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/advantage_air/select.py b/homeassistant/components/advantage_air/select.py index 9cfece25b24..97ec0f9705c 100644 --- a/homeassistant/components/advantage_air/select.py +++ b/homeassistant/components/advantage_air/select.py @@ -46,11 +46,11 @@ class AdvantageAirMyZone(AdvantageAirAcEntity, SelectEntity): self._attr_options.append(zone["name"]) @property - def current_option(self): + def current_option(self) -> str: """Return the current MyZone.""" return self._number_to_name[self._ac["myZone"]] - async def async_select_option(self, option): + async def async_select_option(self, option: str) -> None: """Set the MyZone.""" await self.async_change( {self.ac_key: {"info": {"myZone": self._name_to_number[option]}}} diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index b7e6a65775e..7f6f8ce1cb1 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -164,7 +164,7 @@ class XiaomiAirHumidifierSelector(XiaomiSelector): self.async_write_ha_state() @property - def current_option(self): + def current_option(self) -> str: """Return the current option.""" return self.led_brightness.lower() diff --git a/homeassistant/components/yamaha_musiccast/select.py b/homeassistant/components/yamaha_musiccast/select.py index d57d1c07f68..475d32311ed 100644 --- a/homeassistant/components/yamaha_musiccast/select.py +++ b/homeassistant/components/yamaha_musiccast/select.py @@ -52,11 +52,11 @@ class SelectableCapapility(MusicCastCapabilityEntity, SelectEntity): return DEVICE_CLASS_MAPPING.get(self.capability.id) @property - def options(self): + def options(self) -> list[str]: """Return the list possible options.""" return list(self.capability.options.values()) @property - def current_option(self): + def current_option(self) -> str | None: """Return the currently selected option.""" return self.capability.options.get(self.capability.current) From 3fcdad74fc4616f13d4b6254823e9cb1cbd1ed66 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 8 Aug 2022 15:35:45 +0200 Subject: [PATCH 3212/3516] Fix iCloud listeners (#76437) --- homeassistant/components/icloud/account.py | 4 +++- homeassistant/components/icloud/device_tracker.py | 2 +- homeassistant/components/icloud/sensor.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 4dc3c07aba7..d0e3b8059a4 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -17,7 +17,7 @@ from pyicloud.services.findmyiphone import AppleDevice from homeassistant.components.zone import async_active_zone from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import ATTR_ATTRIBUTION, CONF_USERNAME -from homeassistant.core import HomeAssistant +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import track_point_in_utc_time @@ -104,6 +104,8 @@ class IcloudAccount: self._retried_fetch = False self._config_entry = config_entry + self.listeners: list[CALLBACK_TYPE] = [] + def setup(self) -> None: """Set up an iCloud account.""" try: diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index eaebb0b7717..35a411ff11c 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -34,7 +34,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up device tracker for iCloud component.""" - account = hass.data[DOMAIN][entry.unique_id] + account: IcloudAccount = hass.data[DOMAIN][entry.unique_id] tracked = set[str]() @callback diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py index 6e415aa3350..3feb30f078f 100644 --- a/homeassistant/components/icloud/sensor.py +++ b/homeassistant/components/icloud/sensor.py @@ -20,7 +20,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up device tracker for iCloud component.""" - account = hass.data[DOMAIN][entry.unique_id] + account: IcloudAccount = hass.data[DOMAIN][entry.unique_id] tracked = set[str]() @callback From f67a45f643f504fd22f490dad39b4b0b71c7fdab Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 8 Aug 2022 16:16:40 +0200 Subject: [PATCH 3213/3516] Update coverage to 6.4.3 (#76443) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 2331e971711..6685d06c3b0 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt codecov==2.1.12 -coverage==6.4.2 +coverage==6.4.3 freezegun==1.2.1 mock-open==1.4.0 mypy==0.971 From 9f240d5bab97b144f4e467684dced5c4f04433c8 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 8 Aug 2022 16:52:36 +0200 Subject: [PATCH 3214/3516] Bump NextDNS backend library (#76300) * Bump NextDNS backend library * Update tests * Update diagnostics tests * Use fixtures --- .../components/nextdns/diagnostics.py | 3 + .../components/nextdns/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nextdns/__init__.py | 42 ++++++++++++ .../components/nextdns/fixtures/settings.json | 67 +++++++++++++++++++ tests/components/nextdns/test_diagnostics.py | 5 ++ 7 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 tests/components/nextdns/fixtures/settings.json diff --git a/homeassistant/components/nextdns/diagnostics.py b/homeassistant/components/nextdns/diagnostics.py index 4be22684395..077f2ca2988 100644 --- a/homeassistant/components/nextdns/diagnostics.py +++ b/homeassistant/components/nextdns/diagnostics.py @@ -13,6 +13,7 @@ from .const import ( ATTR_ENCRYPTION, ATTR_IP_VERSIONS, ATTR_PROTOCOLS, + ATTR_SETTINGS, ATTR_STATUS, CONF_PROFILE_ID, DOMAIN, @@ -31,6 +32,7 @@ async def async_get_config_entry_diagnostics( encryption_coordinator = coordinators[ATTR_ENCRYPTION] ip_versions_coordinator = coordinators[ATTR_IP_VERSIONS] protocols_coordinator = coordinators[ATTR_PROTOCOLS] + settings_coordinator = coordinators[ATTR_SETTINGS] status_coordinator = coordinators[ATTR_STATUS] diagnostics_data = { @@ -39,6 +41,7 @@ async def async_get_config_entry_diagnostics( "encryption_coordinator_data": asdict(encryption_coordinator.data), "ip_versions_coordinator_data": asdict(ip_versions_coordinator.data), "protocols_coordinator_data": asdict(protocols_coordinator.data), + "settings_coordinator_data": asdict(settings_coordinator.data), "status_coordinator_data": asdict(status_coordinator.data), } diff --git a/homeassistant/components/nextdns/manifest.json b/homeassistant/components/nextdns/manifest.json index 3e2d3ebb3d0..eb5d71173bf 100644 --- a/homeassistant/components/nextdns/manifest.json +++ b/homeassistant/components/nextdns/manifest.json @@ -3,7 +3,7 @@ "name": "NextDNS", "documentation": "https://www.home-assistant.io/integrations/nextdns", "codeowners": ["@bieniu"], - "requirements": ["nextdns==1.0.2"], + "requirements": ["nextdns==1.1.0"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["nextdns"] diff --git a/requirements_all.txt b/requirements_all.txt index ddf22e985c7..6564512a494 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1106,7 +1106,7 @@ nextcloudmonitor==1.1.0 nextcord==2.0.0a8 # homeassistant.components.nextdns -nextdns==1.0.2 +nextdns==1.1.0 # homeassistant.components.niko_home_control niko-home-control==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0326765cf74..d28ad4ac97f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -787,7 +787,7 @@ nexia==2.0.2 nextcord==2.0.0a8 # homeassistant.components.nextdns -nextdns==1.0.2 +nextdns==1.1.0 # homeassistant.components.nfandroidtv notifications-android-tv==0.1.5 diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py index 3b513b811b8..32b8bed76fa 100644 --- a/tests/components/nextdns/__init__.py +++ b/tests/components/nextdns/__init__.py @@ -54,6 +54,48 @@ SETTINGS = Settings( typosquatting_protection=True, web3=True, youtube_restricted_mode=False, + block_9gag=True, + block_amazon=True, + block_blizzard=True, + block_dailymotion=True, + block_discord=True, + block_disneyplus=True, + block_ebay=True, + block_facebook=True, + block_fortnite=True, + block_hulu=True, + block_imgur=True, + block_instagram=True, + block_leagueoflegends=True, + block_messenger=True, + block_minecraft=True, + block_netflix=True, + block_pinterest=True, + block_primevideo=True, + block_reddit=True, + block_roblox=True, + block_signal=True, + block_skype=True, + block_snapchat=True, + block_spotify=True, + block_steam=True, + block_telegram=True, + block_tiktok=True, + block_tinder=True, + block_tumblr=True, + block_twitch=True, + block_twitter=True, + block_vimeo=True, + block_vk=True, + block_whatsapp=True, + block_xboxlive=True, + block_youtube=True, + block_zoom=True, + block_dating=True, + block_gambling=True, + block_piracy=True, + block_porn=True, + block_social_networks=True, ) diff --git a/tests/components/nextdns/fixtures/settings.json b/tests/components/nextdns/fixtures/settings.json new file mode 100644 index 00000000000..9593ca6325e --- /dev/null +++ b/tests/components/nextdns/fixtures/settings.json @@ -0,0 +1,67 @@ +{ + "block_page": false, + "cache_boost": true, + "cname_flattening": true, + "anonymized_ecs": true, + "logs": true, + "web3": true, + "allow_affiliate": true, + "block_disguised_trackers": true, + "ai_threat_detection": true, + "block_csam": true, + "block_ddns": true, + "block_nrd": true, + "block_parked_domains": true, + "cryptojacking_protection": true, + "dga_protection": true, + "dns_rebinding_protection": true, + "google_safe_browsing": false, + "idn_homograph_attacks_protection": true, + "threat_intelligence_feeds": true, + "typosquatting_protection": true, + "block_bypass_methods": true, + "safesearch": false, + "youtube_restricted_mode": false, + "block_9gag": true, + "block_amazon": true, + "block_blizzard": true, + "block_dailymotion": true, + "block_discord": true, + "block_disneyplus": true, + "block_ebay": true, + "block_facebook": true, + "block_fortnite": true, + "block_hulu": true, + "block_imgur": true, + "block_instagram": true, + "block_leagueoflegends": true, + "block_messenger": true, + "block_minecraft": true, + "block_netflix": true, + "block_pinterest": true, + "block_primevideo": true, + "block_reddit": true, + "block_roblox": true, + "block_signal": true, + "block_skype": true, + "block_snapchat": true, + "block_spotify": true, + "block_steam": true, + "block_telegram": true, + "block_tiktok": true, + "block_tinder": true, + "block_tumblr": true, + "block_twitch": true, + "block_twitter": true, + "block_vimeo": true, + "block_vk": true, + "block_whatsapp": true, + "block_xboxlive": true, + "block_youtube": true, + "block_zoom": true, + "block_dating": true, + "block_gambling": true, + "block_piracy": true, + "block_porn": true, + "block_social_networks": true +} diff --git a/tests/components/nextdns/test_diagnostics.py b/tests/components/nextdns/test_diagnostics.py index 0702a2fa1d8..ec4ffa10aaf 100644 --- a/tests/components/nextdns/test_diagnostics.py +++ b/tests/components/nextdns/test_diagnostics.py @@ -1,11 +1,13 @@ """Test NextDNS diagnostics.""" from collections.abc import Awaitable, Callable +import json from aiohttp import ClientSession from homeassistant.components.diagnostics import REDACTED from homeassistant.core import HomeAssistant +from tests.common import load_fixture from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.components.nextdns import init_integration @@ -14,6 +16,8 @@ async def test_entry_diagnostics( hass: HomeAssistant, hass_client: Callable[..., Awaitable[ClientSession]] ) -> None: """Test config entry diagnostics.""" + settings = json.loads(load_fixture("settings.json", "nextdns")) + entry = await init_integration(hass) result = await get_diagnostics_for_config_entry(hass, hass_client, entry) @@ -60,6 +64,7 @@ async def test_entry_diagnostics( "tcp_queries_ratio": 0.0, "udp_queries_ratio": 40.0, } + assert result["settings_coordinator_data"] == settings assert result["status_coordinator_data"] == { "all_queries": 100, "allowed_queries": 30, From 6540bed59d433f812af712642a2793bff21eaaba Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Mon, 8 Aug 2022 23:15:31 +0800 Subject: [PATCH 3215/3516] Defer preload stream start on startup (#75801) --- homeassistant/components/camera/__init__.py | 4 ++-- tests/components/camera/test_init.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 77bd0b57f1c..5aa348c9fb8 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -39,7 +39,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, CONF_FILENAME, CONTENT_TYPE_MULTIPART, - EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, SERVICE_TURN_OFF, SERVICE_TURN_ON, ) @@ -374,7 +374,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: stream.add_provider("hls") await stream.start() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, preload_stream) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, preload_stream) @callback def update_tokens(time: datetime) -> None: diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index f800a4ac2bd..cea9c527946 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -13,7 +13,7 @@ from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( ATTR_ENTITY_ID, - EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STARTED, STATE_UNAVAILABLE, ) from homeassistant.exceptions import HomeAssistantError @@ -374,7 +374,7 @@ async def test_no_preload_stream(hass, mock_stream): ) as mock_stream_source: mock_stream_source.return_value = io.BytesIO() await async_setup_component(hass, "camera", {DOMAIN: {"platform": "demo"}}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert not mock_request_stream.called @@ -396,7 +396,7 @@ async def test_preload_stream(hass, mock_stream): hass, "camera", {DOMAIN: {"platform": "demo"}} ) await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert mock_create_stream.called From ccf7b8fbb97569298607e84185d3fc391fe13e8f Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Mon, 8 Aug 2022 13:50:41 -0400 Subject: [PATCH 3216/3516] Bump version of pyunifiprotect to 4.0.12 (#76465) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 815b5250e1d..ad18be3dba9 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.11", "unifi-discovery==1.1.5"], + "requirements": ["pyunifiprotect==4.0.12", "unifi-discovery==1.1.5"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 6564512a494..7d67df172ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2010,7 +2010,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.11 +pyunifiprotect==4.0.12 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d28ad4ac97f..c9c6ec04051 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1364,7 +1364,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.11 +pyunifiprotect==4.0.12 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From e199b243747209db9c75a80c9a46d2507e5b58a5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 8 Aug 2022 20:00:05 +0200 Subject: [PATCH 3217/3516] Update sentry-sdk to 1.9.2 (#76444) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 0400424a7fc..0cbe9eb636f 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.9.0"], + "requirements": ["sentry-sdk==1.9.2"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 7d67df172ed..46884ba53bd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2170,7 +2170,7 @@ sense_energy==0.10.4 sensorpush-ble==1.5.1 # homeassistant.components.sentry -sentry-sdk==1.9.0 +sentry-sdk==1.9.2 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9c6ec04051..6acb6d88822 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1464,7 +1464,7 @@ sense_energy==0.10.4 sensorpush-ble==1.5.1 # homeassistant.components.sentry -sentry-sdk==1.9.0 +sentry-sdk==1.9.2 # homeassistant.components.sharkiq sharkiq==0.0.1 From 8456a25f86a9849cd737a780d064c59f88b44091 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 8 Aug 2022 20:03:21 +0200 Subject: [PATCH 3218/3516] Update apprise to 1.0.0 (#76441) --- homeassistant/components/apprise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index 450e0a964df..a1b2efcad89 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -2,7 +2,7 @@ "domain": "apprise", "name": "Apprise", "documentation": "https://www.home-assistant.io/integrations/apprise", - "requirements": ["apprise==0.9.9"], + "requirements": ["apprise==1.0.0"], "codeowners": ["@caronc"], "iot_class": "cloud_push", "loggers": ["apprise"] diff --git a/requirements_all.txt b/requirements_all.txt index 46884ba53bd..1cd3875344c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -319,7 +319,7 @@ anthemav==1.4.1 apcaccess==0.0.13 # homeassistant.components.apprise -apprise==0.9.9 +apprise==1.0.0 # homeassistant.components.aprs aprslib==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6acb6d88822..d387fd49e1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -282,7 +282,7 @@ androidtv[async]==0.0.67 anthemav==1.4.1 # homeassistant.components.apprise -apprise==0.9.9 +apprise==1.0.0 # homeassistant.components.aprs aprslib==0.7.0 From d139d1e175558e4422a5f8b72be0ddfc3868e95b Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Mon, 8 Aug 2022 16:00:50 -0400 Subject: [PATCH 3219/3516] Add UniFi Protect media source (#73244) --- .../components/unifiprotect/config_flow.py | 9 + .../components/unifiprotect/const.py | 2 + homeassistant/components/unifiprotect/data.py | 7 + .../components/unifiprotect/media_source.py | 862 ++++++++++++++++++ .../components/unifiprotect/strings.json | 3 +- .../unifiprotect/translations/en.json | 1 + .../components/unifiprotect/views.py | 4 +- .../unifiprotect/test_config_flow.py | 2 + .../unifiprotect/test_media_source.py | 793 ++++++++++++++++ tests/components/unifiprotect/test_views.py | 2 +- 10 files changed, 1681 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/unifiprotect/media_source.py create mode 100644 tests/components/unifiprotect/test_media_source.py diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 330a5e530a1..f07ca923a53 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -35,7 +35,9 @@ from homeassistant.util.network import is_ip_address from .const import ( CONF_ALL_UPDATES, CONF_DISABLE_RTSP, + CONF_MAX_MEDIA, CONF_OVERRIDE_CHOST, + DEFAULT_MAX_MEDIA, DEFAULT_PORT, DEFAULT_VERIFY_SSL, DOMAIN, @@ -221,6 +223,7 @@ class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): CONF_DISABLE_RTSP: False, CONF_ALL_UPDATES: False, CONF_OVERRIDE_CHOST: False, + CONF_MAX_MEDIA: DEFAULT_MAX_MEDIA, }, ) @@ -383,6 +386,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_OVERRIDE_CHOST, False ), ): bool, + vol.Optional( + CONF_MAX_MEDIA, + default=self.config_entry.options.get( + CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA + ), + ): vol.All(vol.Coerce(int), vol.Range(min=100, max=10000)), } ), ) diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 3c29d0c9972..9710279a7c4 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -19,6 +19,7 @@ ATTR_ANONYMIZE = "anonymize" CONF_DISABLE_RTSP = "disable_rtsp" CONF_ALL_UPDATES = "all_updates" CONF_OVERRIDE_CHOST = "override_connection_host" +CONF_MAX_MEDIA = "max_media" CONFIG_OPTIONS = [ CONF_ALL_UPDATES, @@ -31,6 +32,7 @@ DEFAULT_ATTRIBUTION = "Powered by UniFi Protect Server" DEFAULT_BRAND = "Ubiquiti" DEFAULT_SCAN_INTERVAL = 5 DEFAULT_VERIFY_SSL = False +DEFAULT_MAX_MEDIA = 1000 DEVICES_THAT_ADOPT = { ModelType.CAMERA, diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index d4140759a7b..74ef6ab37f8 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -26,6 +26,8 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( CONF_DISABLE_RTSP, + CONF_MAX_MEDIA, + DEFAULT_MAX_MEDIA, DEVICES_THAT_ADOPT, DISPATCH_ADOPT, DISPATCH_CHANNELS, @@ -82,6 +84,11 @@ class ProtectData: """Check if RTSP is disabled.""" return self._entry.options.get(CONF_DISABLE_RTSP, False) + @property + def max_events(self) -> int: + """Max number of events to load at once.""" + return self._entry.options.get(CONF_MAX_MEDIA, DEFAULT_MAX_MEDIA) + def get_by_types( self, device_types: Iterable[ModelType] ) -> Generator[ProtectAdoptableDeviceModel, None, None]: diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py new file mode 100644 index 00000000000..17db4db5c3c --- /dev/null +++ b/homeassistant/components/unifiprotect/media_source.py @@ -0,0 +1,862 @@ +"""UniFi Protect media sources.""" + +from __future__ import annotations + +import asyncio +from datetime import date, datetime, timedelta +from enum import Enum +from typing import Any, cast + +from pyunifiprotect.data import Camera, Event, EventType, SmartDetectObjectType +from pyunifiprotect.exceptions import NvrError +from pyunifiprotect.utils import from_js_time +from yarl import URL + +from homeassistant.components.camera import CameraImageView +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_IMAGE, + MEDIA_CLASS_VIDEO, +) +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source.models import ( + BrowseMediaSource, + MediaSource, + MediaSourceItem, + PlayMedia, +) +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util + +from .const import DOMAIN +from .data import ProtectData +from .views import async_generate_event_video_url, async_generate_thumbnail_url + +VIDEO_FORMAT = "video/mp4" +THUMBNAIL_WIDTH = 185 +THUMBNAIL_HEIGHT = 185 + + +class SimpleEventType(str, Enum): + """Enum to Camera Video events.""" + + ALL = "all" + RING = "ring" + MOTION = "motion" + SMART = "smart" + + +class IdentifierType(str, Enum): + """UniFi Protect identifier type.""" + + EVENT = "event" + EVENT_THUMB = "eventthumb" + BROWSE = "browse" + + +class IdentifierTimeType(str, Enum): + """UniFi Protect identifier subtype.""" + + RECENT = "recent" + RANGE = "range" + + +EVENT_MAP = { + SimpleEventType.ALL: None, + SimpleEventType.RING: EventType.RING, + SimpleEventType.MOTION: EventType.MOTION, + SimpleEventType.SMART: EventType.SMART_DETECT, +} +EVENT_NAME_MAP = { + SimpleEventType.ALL: "All Events", + SimpleEventType.RING: "Ring Events", + SimpleEventType.MOTION: "Motion Events", + SimpleEventType.SMART: "Smart Detections", +} + + +def get_ufp_event(event_type: SimpleEventType) -> EventType | None: + """Get UniFi Protect event type from SimpleEventType.""" + + return EVENT_MAP[event_type] + + +async def async_get_media_source(hass: HomeAssistant) -> MediaSource: + """Set up UniFi Protect media source.""" + + data_sources: dict[str, ProtectData] = {} + for data in hass.data.get(DOMAIN, {}).values(): + if isinstance(data, ProtectData): + data_sources[data.api.bootstrap.nvr.id] = data + + return ProtectMediaSource(hass, data_sources) + + +@callback +def _get_start_end(hass: HomeAssistant, start: datetime) -> tuple[datetime, datetime]: + start = dt_util.as_local(start) + end = dt_util.now() + + start = start.replace(day=1, hour=1, minute=0, second=0, microsecond=0) + end = end.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + + return start, end + + +@callback +def _bad_identifier(identifier: str, err: Exception | None = None) -> BrowseMediaSource: + msg = f"Unexpected identifier: {identifier}" + if err is None: + raise BrowseError(msg) + raise BrowseError(msg) from err + + +@callback +def _bad_identifier_media(identifier: str, err: Exception | None = None) -> PlayMedia: + return cast(PlayMedia, _bad_identifier(identifier, err)) + + +@callback +def _format_duration(duration: timedelta) -> str: + formatted = "" + seconds = int(duration.total_seconds()) + if seconds > 3600: + hours = seconds // 3600 + formatted += f"{hours}h " + seconds -= hours * 3600 + if seconds > 60: + minutes = seconds // 60 + formatted += f"{minutes}m " + seconds -= minutes * 60 + if seconds > 0: + formatted += f"{seconds}s " + + return formatted.strip() + + +class ProtectMediaSource(MediaSource): + """Represents all UniFi Protect NVRs.""" + + name: str = "UniFi Protect" + _registry: er.EntityRegistry | None + + def __init__( + self, hass: HomeAssistant, data_sources: dict[str, ProtectData] + ) -> None: + """Initialize the UniFi Protect media source.""" + + super().__init__(DOMAIN) + self.hass = hass + self.data_sources = data_sources + self._registry = None + + async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: + """Return a streamable URL and associated mime type for a UniFi Protect event. + + Accepted identifier format are + + * {nvr_id}:event:{event_id} - MP4 video clip for specific event + * {nvr_id}:eventthumb:{event_id} - Thumbnail JPEG for specific event + """ + + parts = item.identifier.split(":") + if len(parts) != 3 or parts[1] not in ("event", "eventthumb"): + return _bad_identifier_media(item.identifier) + + thumbnail_only = parts[1] == "eventthumb" + try: + data = self.data_sources[parts[0]] + except (KeyError, IndexError) as err: + return _bad_identifier_media(item.identifier, err) + + event = data.api.bootstrap.events.get(parts[2]) + if event is None: + try: + event = await data.api.get_event(parts[2]) + except NvrError as err: + return _bad_identifier_media(item.identifier, err) + else: + # cache the event for later + data.api.bootstrap.events[event.id] = event + + nvr = data.api.bootstrap.nvr + if thumbnail_only: + return PlayMedia( + async_generate_thumbnail_url(event.id, nvr.id), "image/jpeg" + ) + return PlayMedia(async_generate_event_video_url(event), "video/mp4") + + async def async_browse_media(self, item: MediaSourceItem) -> BrowseMediaSource: + """Return a browsable UniFi Protect media source. + + Identifier formatters for UniFi Protect media sources are all in IDs from + the UniFi Protect instance since events may not always map 1:1 to a Home + Assistant device or entity. It also drasically speeds up resolution. + + The UniFi Protect Media source is timebased for the events recorded by the NVR. + So its structure is a bit different then many other media players. All browsable + media is a video clip. The media source could be greatly cleaned up if/when the + frontend has filtering supporting. + + * ... Each NVR Console (hidden if there is only one) + * All Cameras + * ... Camera X + * All Events + * ... Event Type X + * Last 24 Hours -> Events + * Last 7 Days -> Events + * Last 30 Days -> Events + * ... This Month - X + * Whole Month -> Events + * ... Day X -> Events + + Accepted identifier formats: + + * {nvr_id}:event:{event_id} + Specific Event for NVR + * {nvr_id}:eventthumb:{event_id} + Specific Event Thumbnail for NVR + * {nvr_id}:browse + Root NVR browse source + * {nvr_id}:browse:all|{camera_id} + Root Camera(s) browse source + * {nvr_id}:browse:all|{camera_id}:all|{event_type} + Root Camera(s) Event Type(s) browse source + * {nvr_id}:browse:all|{camera_id}:all|{event_type}:recent:{day_count} + Listing of all events in last {day_count}, sorted in reverse chronological order + * {nvr_id}:browse:all|{camera_id}:all|{event_type}:range:{year}:{month} + List of folders for each day in month + all events for month + * {nvr_id}:browse:all|{camera_id}:all|{event_type}:range:{year}:{month}:all|{day} + Listing of all events for give {day} + {month} + {year} combination in chronological order + """ + + if not item.identifier: + return await self._build_sources() + + parts = item.identifier.split(":") + + try: + data = self.data_sources[parts[0]] + except (KeyError, IndexError) as err: + return _bad_identifier(item.identifier, err) + + if len(parts) < 2: + return _bad_identifier(item.identifier) + + try: + identifier_type = IdentifierType(parts[1]) + except ValueError as err: + return _bad_identifier(item.identifier, err) + + if identifier_type in (IdentifierType.EVENT, IdentifierType.EVENT_THUMB): + thumbnail_only = identifier_type == IdentifierType.EVENT_THUMB + return await self._resolve_event(data, parts[2], thumbnail_only) + + # rest are params for browse + parts = parts[2:] + + # {nvr_id}:browse + if len(parts) == 0: + return await self._build_console(data) + + # {nvr_id}:browse:all|{camera_id} + camera_id = parts.pop(0) + if len(parts) == 0: + return await self._build_camera(data, camera_id, build_children=True) + + # {nvr_id}:browse:all|{camera_id}:all|{event_type} + try: + event_type = SimpleEventType(parts.pop(0).lower()) + except (IndexError, ValueError) as err: + return _bad_identifier(item.identifier, err) + + if len(parts) == 0: + return await self._build_events_type( + data, camera_id, event_type, build_children=True + ) + + try: + time_type = IdentifierTimeType(parts.pop(0)) + except ValueError as err: + return _bad_identifier(item.identifier, err) + + if len(parts) == 0: + return _bad_identifier(item.identifier) + + # {nvr_id}:browse:all|{camera_id}:all|{event_type}:recent:{day_count} + if time_type == IdentifierTimeType.RECENT: + try: + days = int(parts.pop(0)) + except (IndexError, ValueError) as err: + return _bad_identifier(item.identifier, err) + + return await self._build_recent( + data, camera_id, event_type, days, build_children=True + ) + + # {nvr_id}:all|{camera_id}:all|{event_type}:range:{year}:{month} + # {nvr_id}:all|{camera_id}:all|{event_type}:range:{year}:{month}:all|{day} + try: + start, is_month, is_all = self._parse_range(parts) + except (IndexError, ValueError) as err: + return _bad_identifier(item.identifier, err) + + if is_month: + return await self._build_month( + data, camera_id, event_type, start, build_children=True + ) + return await self._build_days( + data, camera_id, event_type, start, build_children=True, is_all=is_all + ) + + def _parse_range(self, parts: list[str]) -> tuple[date, bool, bool]: + day = 1 + is_month = True + is_all = True + year = int(parts[0]) + month = int(parts[1]) + if len(parts) == 3: + is_month = False + if parts[2] != "all": + is_all = False + day = int(parts[2]) + + start = date(year=year, month=month, day=day) + return start, is_month, is_all + + async def _resolve_event( + self, data: ProtectData, event_id: str, thumbnail_only: bool = False + ) -> BrowseMediaSource: + """Resolve a specific event.""" + + subtype = "eventthumb" if thumbnail_only else "event" + try: + event = await data.api.get_event(event_id) + except NvrError as err: + return _bad_identifier( + f"{data.api.bootstrap.nvr.id}:{subtype}:{event_id}", err + ) + + if event.start is None or event.end is None: + raise BrowseError("Event is still ongoing") + + return await self._build_event(data, event, thumbnail_only) + + async def get_registry(self) -> er.EntityRegistry: + """Get or return Entity Registry.""" + + if self._registry is None: + self._registry = await er.async_get_registry(self.hass) + return self._registry + + def _breadcrumb( + self, + data: ProtectData, + base_title: str, + camera: Camera | None = None, + event_type: SimpleEventType | None = None, + count: int | None = None, + ) -> str: + title = base_title + if count is not None: + if count == data.max_events: + title = f"{title} ({count} TRUNCATED)" + else: + title = f"{title} ({count})" + + if event_type is not None: + title = f"{EVENT_NAME_MAP[event_type].title()} > {title}" + + if camera is not None: + title = f"{camera.display_name} > {title}" + title = f"{data.api.bootstrap.nvr.display_name} > {title}" + + return title + + async def _build_event( + self, + data: ProtectData, + event: dict[str, Any] | Event, + thumbnail_only: bool = False, + ) -> BrowseMediaSource: + """Build media source for an individual event.""" + + if isinstance(event, Event): + event_id = event.id + event_type = event.type + start = event.start + end = event.end + else: + event_id = event["id"] + event_type = event["type"] + start = from_js_time(event["start"]) + end = from_js_time(event["end"]) + + assert end is not None + + title = dt_util.as_local(start).strftime("%x %X") + duration = end - start + title += f" {_format_duration(duration)}" + if event_type == EventType.RING.value: + event_text = "Ring Event" + elif event_type == EventType.MOTION.value: + event_text = "Motion Event" + elif event_type == EventType.SMART_DETECT.value: + if isinstance(event, Event): + smart_type = event.smart_detect_types[0] + else: + smart_type = SmartDetectObjectType(event["smartDetectTypes"][0]) + event_text = f"Smart Detection - {smart_type.name.title()}" + title += f" {event_text}" + + nvr = data.api.bootstrap.nvr + if thumbnail_only: + return BrowseMediaSource( + domain=DOMAIN, + identifier=f"{nvr.id}:eventthumb:{event_id}", + media_class=MEDIA_CLASS_IMAGE, + media_content_type="image/jpeg", + title=title, + can_play=True, + can_expand=False, + thumbnail=async_generate_thumbnail_url( + event_id, nvr.id, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT + ), + ) + + return BrowseMediaSource( + domain=DOMAIN, + identifier=f"{nvr.id}:event:{event_id}", + media_class=MEDIA_CLASS_VIDEO, + media_content_type="video/mp4", + title=title, + can_play=True, + can_expand=False, + thumbnail=async_generate_thumbnail_url( + event_id, nvr.id, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT + ), + ) + + async def _build_events( + self, + data: ProtectData, + start: datetime, + end: datetime, + camera_id: str | None = None, + event_type: EventType | None = None, + reserve: bool = False, + ) -> list[BrowseMediaSource]: + """Build media source for a given range of time and event type.""" + + if event_type is None: + types = [ + EventType.RING, + EventType.MOTION, + EventType.SMART_DETECT, + ] + else: + types = [event_type] + + sources: list[BrowseMediaSource] = [] + events = await data.api.get_events_raw( + start=start, end=end, types=types, limit=data.max_events + ) + events = sorted(events, key=lambda e: cast(int, e["start"]), reverse=reserve) + for event in events: + # do not process ongoing events + if event.get("start") is None or event.get("end") is None: + continue + + if camera_id is not None and event.get("camera") != camera_id: + continue + + # smart detect events have a paired motion event + if ( + event.get("type") == EventType.MOTION.value + and len(event.get("smartDetectEvents", [])) > 0 + ): + continue + + sources.append(await self._build_event(data, event)) + + return sources + + async def _build_recent( + self, + data: ProtectData, + camera_id: str, + event_type: SimpleEventType, + days: int, + build_children: bool = False, + ) -> BrowseMediaSource: + """Build media source for events in relative days.""" + + base_id = f"{data.api.bootstrap.nvr.id}:browse:{camera_id}:{event_type.value}" + title = f"Last {days} Days" + if days == 1: + title = "Last 24 Hours" + + source = BrowseMediaSource( + domain=DOMAIN, + identifier=f"{base_id}:recent:{days}", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type="video/mp4", + title=title, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_VIDEO, + ) + + if not build_children: + return source + + now = dt_util.now() + + args = { + "data": data, + "start": now - timedelta(days=days), + "end": now, + "reserve": True, + } + if event_type != SimpleEventType.ALL: + args["event_type"] = get_ufp_event(event_type) + + camera: Camera | None = None + if camera_id != "all": + camera = data.api.bootstrap.cameras.get(camera_id) + args["camera_id"] = camera_id + + events = await self._build_events(**args) # type: ignore[arg-type] + source.children = events # type: ignore[assignment] + source.title = self._breadcrumb( + data, + title, + camera=camera, + event_type=event_type, + count=len(events), + ) + return source + + async def _build_month( + self, + data: ProtectData, + camera_id: str, + event_type: SimpleEventType, + start: date, + build_children: bool = False, + ) -> BrowseMediaSource: + """Build media source for selectors for a given month.""" + + base_id = f"{data.api.bootstrap.nvr.id}:browse:{camera_id}:{event_type.value}" + + title = f"{start.strftime('%B %Y')}" + source = BrowseMediaSource( + domain=DOMAIN, + identifier=f"{base_id}:range:{start.year}:{start.month}", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=VIDEO_FORMAT, + title=title, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_VIDEO, + ) + + if not build_children: + return source + + month = start.month + children = [self._build_days(data, camera_id, event_type, start, is_all=True)] + while start.month == month: + children.append( + self._build_days(data, camera_id, event_type, start, is_all=False) + ) + start = start + timedelta(hours=24) + + camera: Camera | None = None + if camera_id != "all": + camera = data.api.bootstrap.cameras.get(camera_id) + + source.children = await asyncio.gather(*children) + source.title = self._breadcrumb( + data, + title, + camera=camera, + event_type=event_type, + ) + + return source + + async def _build_days( + self, + data: ProtectData, + camera_id: str, + event_type: SimpleEventType, + start: date, + is_all: bool = True, + build_children: bool = False, + ) -> BrowseMediaSource: + """Build media source for events for a given day or whole month.""" + + base_id = f"{data.api.bootstrap.nvr.id}:browse:{camera_id}:{event_type.value}" + + if is_all: + title = "Whole Month" + identifier = f"{base_id}:range:{start.year}:{start.month}:all" + else: + title = f"{start.strftime('%x')}" + identifier = f"{base_id}:range:{start.year}:{start.month}:{start.day}" + source = BrowseMediaSource( + domain=DOMAIN, + identifier=identifier, + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=VIDEO_FORMAT, + title=title, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_VIDEO, + ) + + if not build_children: + return source + + start_dt = datetime( + year=start.year, + month=start.month, + day=start.day, + hour=0, + minute=0, + second=0, + tzinfo=dt_util.DEFAULT_TIME_ZONE, + ) + if is_all: + if start_dt.month < 12: + end_dt = start_dt.replace(month=start_dt.month + 1) + else: + end_dt = start_dt.replace(year=start_dt.year + 1, month=1) + else: + end_dt = start_dt + timedelta(hours=24) + + args = { + "data": data, + "start": start_dt, + "end": end_dt, + "reserve": False, + } + if event_type != SimpleEventType.ALL: + args["event_type"] = get_ufp_event(event_type) + + camera: Camera | None = None + if camera_id != "all": + camera = data.api.bootstrap.cameras.get(camera_id) + args["camera_id"] = camera_id + + title = f"{start.strftime('%B %Y')} > {title}" + events = await self._build_events(**args) # type: ignore[arg-type] + source.children = events # type: ignore[assignment] + source.title = self._breadcrumb( + data, + title, + camera=camera, + event_type=event_type, + count=len(events), + ) + + return source + + async def _build_events_type( + self, + data: ProtectData, + camera_id: str, + event_type: SimpleEventType, + build_children: bool = False, + ) -> BrowseMediaSource: + """Build folder media source for a selectors for a given event type.""" + + base_id = f"{data.api.bootstrap.nvr.id}:browse:{camera_id}:{event_type.value}" + + title = EVENT_NAME_MAP[event_type].title() + source = BrowseMediaSource( + domain=DOMAIN, + identifier=base_id, + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=VIDEO_FORMAT, + title=title, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_VIDEO, + ) + + if not build_children or data.api.bootstrap.recording_start is None: + return source + + children = [ + self._build_recent(data, camera_id, event_type, 1), + self._build_recent(data, camera_id, event_type, 7), + self._build_recent(data, camera_id, event_type, 30), + ] + + start, end = _get_start_end(self.hass, data.api.bootstrap.recording_start) + while end > start: + children.append(self._build_month(data, camera_id, event_type, end.date())) + end = (end - timedelta(days=1)).replace(day=1) + + camera: Camera | None = None + if camera_id != "all": + camera = data.api.bootstrap.cameras.get(camera_id) + source.children = await asyncio.gather(*children) + source.title = self._breadcrumb(data, title, camera=camera) + + return source + + async def _get_camera_thumbnail_url(self, camera: Camera) -> str | None: + """Get camera thumbnail URL using the first available camera entity.""" + + if not camera.is_connected or camera.is_privacy_on: + return None + + entity_id: str | None = None + entity_registry = await self.get_registry() + for channel in camera.channels: + # do not use the package camera + if channel.id == 3: + continue + + base_id = f"{camera.mac}_{channel.id}" + entity_id = entity_registry.async_get_entity_id( + Platform.CAMERA, DOMAIN, base_id + ) + if entity_id is None: + entity_id = entity_registry.async_get_entity_id( + Platform.CAMERA, DOMAIN, f"{base_id}_insecure" + ) + + if entity_id: + # verify entity is available + entry = entity_registry.async_get(entity_id) + if entry and not entry.disabled: + break + entity_id = None + + if entity_id is not None: + url = URL(CameraImageView.url.format(entity_id=entity_id)) + return str( + url.update_query({"width": THUMBNAIL_WIDTH, "height": THUMBNAIL_HEIGHT}) + ) + return None + + async def _build_camera( + self, data: ProtectData, camera_id: str, build_children: bool = False + ) -> BrowseMediaSource: + """Build media source for selectors for a UniFi Protect camera.""" + + name = "All Cameras" + is_doorbell = data.api.bootstrap.has_doorbell + has_smart = data.api.bootstrap.has_smart_detections + camera: Camera | None = None + if camera_id != "all": + camera = data.api.bootstrap.cameras.get(camera_id) + if camera is None: + raise BrowseError(f"Unknown Camera ID: {camera_id}") + name = camera.name or camera.market_name or camera.type + is_doorbell = camera.feature_flags.has_chime + has_smart = camera.feature_flags.has_smart_detect + + thumbnail_url: str | None = None + if camera is not None: + thumbnail_url = await self._get_camera_thumbnail_url(camera) + source = BrowseMediaSource( + domain=DOMAIN, + identifier=f"{data.api.bootstrap.nvr.id}:browse:{camera_id}", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=VIDEO_FORMAT, + title=name, + can_play=False, + can_expand=True, + thumbnail=thumbnail_url, + children_media_class=MEDIA_CLASS_VIDEO, + ) + + if not build_children: + return source + + source.children = [ + await self._build_events_type(data, camera_id, SimpleEventType.MOTION), + ] + + if is_doorbell: + source.children.insert( + 0, + await self._build_events_type(data, camera_id, SimpleEventType.RING), + ) + + if has_smart: + source.children.append( + await self._build_events_type(data, camera_id, SimpleEventType.SMART) + ) + + if is_doorbell or has_smart: + source.children.insert( + 0, + await self._build_events_type(data, camera_id, SimpleEventType.ALL), + ) + + source.title = self._breadcrumb(data, name) + + return source + + async def _build_cameras(self, data: ProtectData) -> list[BrowseMediaSource]: + """Build media source for a single UniFi Protect NVR.""" + + cameras: list[BrowseMediaSource] = [await self._build_camera(data, "all")] + + for camera in data.api.bootstrap.cameras.values(): + if not camera.can_read_media(data.api.bootstrap.auth_user): + continue + cameras.append(await self._build_camera(data, camera.id)) + + return cameras + + async def _build_console(self, data: ProtectData) -> BrowseMediaSource: + """Build media source for a single UniFi Protect NVR.""" + + base = BrowseMediaSource( + domain=DOMAIN, + identifier=f"{data.api.bootstrap.nvr.id}:browse", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=VIDEO_FORMAT, + title=data.api.bootstrap.nvr.name, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_VIDEO, + children=await self._build_cameras(data), + ) + + return base + + async def _build_sources(self) -> BrowseMediaSource: + """Return all media source for all UniFi Protect NVRs.""" + + consoles: list[BrowseMediaSource] = [] + print(len(self.data_sources.values())) + for data_source in self.data_sources.values(): + if not data_source.api.bootstrap.has_media: + continue + console_source = await self._build_console(data_source) + consoles.append(console_source) + + if len(consoles) == 1: + return consoles[0] + + return BrowseMediaSource( + domain=DOMAIN, + identifier=None, + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type=VIDEO_FORMAT, + title=self.name, + can_play=False, + can_expand=True, + children_media_class=MEDIA_CLASS_VIDEO, + children=consoles, + ) diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index d789459960b..d3cfe24abd2 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -49,7 +49,8 @@ "data": { "disable_rtsp": "Disable the RTSP stream", "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", - "override_connection_host": "Override Connection Host" + "override_connection_host": "Override Connection Host", + "max_media": "Max number of event to load for Media Browser (increases RAM usage)" } } } diff --git a/homeassistant/components/unifiprotect/translations/en.json b/homeassistant/components/unifiprotect/translations/en.json index b9d787b382e..5d690e3fd3e 100644 --- a/homeassistant/components/unifiprotect/translations/en.json +++ b/homeassistant/components/unifiprotect/translations/en.json @@ -47,6 +47,7 @@ "data": { "all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)", "disable_rtsp": "Disable the RTSP stream", + "max_media": "Max number of event to load for Media Browser (increases RAM usage)", "override_connection_host": "Override Connection Host" }, "description": "Realtime metrics option should only be enabled if you have enabled the diagnostics sensors and want them updated in realtime. If not enabled, they will only update once every 15 minutes.", diff --git a/homeassistant/components/unifiprotect/views.py b/homeassistant/components/unifiprotect/views.py index ea523d36dd2..a8a767c8879 100644 --- a/homeassistant/components/unifiprotect/views.py +++ b/homeassistant/components/unifiprotect/views.py @@ -53,8 +53,8 @@ def async_generate_event_video_url(event: Event) -> str: url = url_format.format( nvr_id=event.api.bootstrap.nvr.id, camera_id=event.camera_id, - start=event.start.isoformat(), - end=event.end.isoformat(), + start=event.start.replace(microsecond=0).isoformat(), + end=event.end.replace(microsecond=0).isoformat(), ) return url diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index 5304e05fe13..d0fb0dba9f2 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -238,6 +238,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - "id": "UnifiProtect", "port": 443, "verify_ssl": False, + "max_media": 1000, }, version=2, unique_id=dr.format_mac(MAC_ADDR), @@ -268,6 +269,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - "all_updates": True, "disable_rtsp": True, "override_connection_host": True, + "max_media": 1000, } diff --git a/tests/components/unifiprotect/test_media_source.py b/tests/components/unifiprotect/test_media_source.py new file mode 100644 index 00000000000..e1e7f3cacde --- /dev/null +++ b/tests/components/unifiprotect/test_media_source.py @@ -0,0 +1,793 @@ +"""Tests for unifiprotect.media_source.""" + +from datetime import datetime, timedelta +from ipaddress import IPv4Address +from unittest.mock import AsyncMock, Mock, patch + +import pytest +from pyunifiprotect.data import ( + Bootstrap, + Camera, + Event, + EventType, + Permission, + SmartDetectObjectType, +) +from pyunifiprotect.exceptions import NvrError + +from homeassistant.components.media_player.const import ( + MEDIA_CLASS_IMAGE, + MEDIA_CLASS_VIDEO, +) +from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.media_source import MediaSourceItem +from homeassistant.components.unifiprotect.const import DOMAIN +from homeassistant.components.unifiprotect.media_source import ( + ProtectMediaSource, + async_get_media_source, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .conftest import MockUFPFixture +from .utils import init_entry + +from tests.common import MockConfigEntry + + +async def test_get_media_source(hass: HomeAssistant) -> None: + """Test the async_get_media_source function and ProtectMediaSource constructor.""" + source = await async_get_media_source(hass) + assert isinstance(source, ProtectMediaSource) + assert source.domain == DOMAIN + + +@pytest.mark.parametrize( + "identifier", + [ + "test_id:bad_type:test_id", + "bad_id:event:test_id", + "test_id:event:bad_id", + "test_id", + ], +) +async def test_resolve_media_bad_identifier( + hass: HomeAssistant, ufp: MockUFPFixture, identifier: str +): + """Test resolving bad identifiers.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + ufp.api.get_event = AsyncMock(side_effect=NvrError) + await init_entry(hass, ufp, [], regenerate_ids=False) + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, identifier, None) + with pytest.raises(BrowseError): + await source.async_resolve_media(media_item) + + +async def test_resolve_media_thumbnail( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test resolving event thumbnails.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event = Event( + id="test_event_id", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event._api = ufp.api + ufp.api.bootstrap.events = {"test_event_id": event} + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, "test_id:eventthumb:test_event_id", None) + play_media = await source.async_resolve_media(media_item) + + assert play_media.mime_type == "image/jpeg" + assert play_media.url.startswith( + "/api/unifiprotect/thumbnail/test_id/test_event_id" + ) + + +async def test_resolve_media_event( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test resolving event clips.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event = Event( + id="test_event_id", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event._api = ufp.api + ufp.api.get_event = AsyncMock(return_value=event) + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, "test_id:event:test_event_id", None) + play_media = await source.async_resolve_media(media_item) + + start = event.start.replace(microsecond=0).isoformat() + end = event.end.replace(microsecond=0).isoformat() + + assert play_media.mime_type == "video/mp4" + assert play_media.url.startswith( + f"/api/unifiprotect/video/test_id/{event.camera_id}/{start}/{end}" + ) + + +@pytest.mark.parametrize( + "identifier", + [ + "bad_id:event:test_id", + "test_id", + "test_id:bad_type", + "test_id:browse:all:all:bad_type", + "test_id:browse:all:bad_event", + "test_id:browse:all:all:recent", + "test_id:browse:all:all:recent:not_a_num", + "test_id:browse:all:all:range", + "test_id:browse:all:all:range:not_a_num", + "test_id:browse:all:all:range:2022:not_a_num", + "test_id:browse:all:all:range:2022:1:not_a_num", + "test_id:browse:all:all:range:2022:1:50", + "test_id:browse:all:all:invalid", + "test_id:event:bad_event_id", + "test_id:browse:bad_camera_id", + ], +) +async def test_browse_media_bad_identifier( + hass: HomeAssistant, ufp: MockUFPFixture, identifier: str +): + """Test browsing media with bad identifiers.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + ufp.api.get_event = AsyncMock(side_effect=NvrError) + await init_entry(hass, ufp, [], regenerate_ids=False) + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, identifier, None) + with pytest.raises(BrowseError): + await source.async_browse_media(media_item) + + +async def test_browse_media_event_ongoing( + hass: HomeAssistant, ufp: MockUFPFixture, fixed_now: datetime, doorbell: Camera +): + """Test browsing event that is still ongoing.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event = Event( + id="test_event_id", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=None, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event._api = ufp.api + ufp.api.get_event = AsyncMock(return_value=event) + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, f"test_id:event:{event.id}", None) + with pytest.raises(BrowseError): + await source.async_browse_media(media_item) + + +async def test_browse_media_root_multiple_consoles( + hass: HomeAssistant, ufp: MockUFPFixture, bootstrap: Bootstrap +): + """Test browsing root level media with multiple consoles.""" + + ufp.api.bootstrap._has_media = True + + await hass.config_entries.async_setup(ufp.entry.entry_id) + await hass.async_block_till_done() + + bootstrap2 = bootstrap.copy() + bootstrap2._has_media = True + bootstrap2.nvr = bootstrap.nvr.copy() + bootstrap2.nvr.id = "test_id2" + bootstrap2.nvr.mac = "A2E00C826924" + bootstrap2.nvr.name = "UnifiProtect2" + + api2 = Mock() + bootstrap2.nvr._api = api2 + bootstrap2._api = api2 + + api2.bootstrap = bootstrap2 + api2._bootstrap = bootstrap2 + api2.api_path = "/api" + api2.base_url = "https://127.0.0.2" + api2.connection_host = IPv4Address("127.0.0.2") + api2.get_nvr = AsyncMock(return_value=bootstrap2.nvr) + api2.update = AsyncMock(return_value=bootstrap2) + api2.async_disconnect_ws = AsyncMock() + + with patch("homeassistant.components.unifiprotect.ProtectApiClient") as mock_api: + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.1.1.2", + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect2", + "port": 443, + "verify_ssl": False, + }, + version=2, + ) + mock_config.add_to_hass(hass) + + mock_api.return_value = api2 + + await hass.config_entries.async_setup(mock_config.entry_id) + await hass.async_block_till_done() + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, None, None) + + browse = await source.async_browse_media(media_item) + + assert browse.title == "UniFi Protect" + assert len(browse.children) == 2 + assert browse.children[0].title.startswith("UnifiProtect") + assert browse.children[0].identifier.startswith("test_id") + assert browse.children[1].title.startswith("UnifiProtect") + assert browse.children[0].identifier.startswith("test_id") + + +async def test_browse_media_root_multiple_consoles_only_one_media( + hass: HomeAssistant, ufp: MockUFPFixture, bootstrap: Bootstrap +): + """Test browsing root level media with multiple consoles.""" + + ufp.api.bootstrap._has_media = True + + await hass.config_entries.async_setup(ufp.entry.entry_id) + await hass.async_block_till_done() + + bootstrap2 = bootstrap.copy() + bootstrap2._has_media = False + bootstrap2.nvr = bootstrap.nvr.copy() + bootstrap2.nvr.id = "test_id2" + bootstrap2.nvr.mac = "A2E00C826924" + bootstrap2.nvr.name = "UnifiProtect2" + + api2 = Mock() + bootstrap2.nvr._api = api2 + bootstrap2._api = api2 + + api2.bootstrap = bootstrap2 + api2._bootstrap = bootstrap2 + api2.api_path = "/api" + api2.base_url = "https://127.0.0.2" + api2.connection_host = IPv4Address("127.0.0.2") + api2.get_nvr = AsyncMock(return_value=bootstrap2.nvr) + api2.update = AsyncMock(return_value=bootstrap2) + api2.async_disconnect_ws = AsyncMock() + + with patch("homeassistant.components.unifiprotect.ProtectApiClient") as mock_api: + mock_config = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.1.1.2", + "username": "test-username", + "password": "test-password", + "id": "UnifiProtect2", + "port": 443, + "verify_ssl": False, + }, + version=2, + ) + mock_config.add_to_hass(hass) + + mock_api.return_value = api2 + + await hass.config_entries.async_setup(mock_config.entry_id) + await hass.async_block_till_done() + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, None, None) + + browse = await source.async_browse_media(media_item) + + assert browse.title == "UnifiProtect" + assert browse.identifier == "test_id:browse" + assert len(browse.children) == 1 + assert browse.children[0].title == "All Cameras" + assert browse.children[0].identifier == "test_id:browse:all" + + +async def test_browse_media_root_single_console( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera +): + """Test browsing root level media with a single console.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, None, None) + + browse = await source.async_browse_media(media_item) + + assert browse.title == "UnifiProtect" + assert browse.identifier == "test_id:browse" + assert len(browse.children) == 2 + assert browse.children[0].title == "All Cameras" + assert browse.children[0].identifier == "test_id:browse:all" + assert browse.children[1].title == doorbell.name + assert browse.children[1].identifier == f"test_id:browse:{doorbell.id}" + assert browse.children[1].thumbnail is not None + + +async def test_browse_media_camera( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, camera: Camera +): + """Test browsing camera selector level media.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell, camera]) + + ufp.api.bootstrap.auth_user.all_permissions = [ + Permission.unifi_dict_to_dict( + {"rawPermission": "camera:create,read,write,delete,deletemedia:*"} + ), + Permission.unifi_dict_to_dict( + {"rawPermission": f"camera:readmedia:{doorbell.id}"} + ), + ] + + entity_registry = er.async_get(hass) + entity_registry.async_update_entity( + "camera.test_camera_high", disabled_by=er.RegistryEntryDisabler("user") + ) + await hass.async_block_till_done() + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, "test_id:browse", None) + + browse = await source.async_browse_media(media_item) + + assert browse.title == "UnifiProtect" + assert browse.identifier == "test_id:browse" + assert len(browse.children) == 2 + assert browse.children[0].title == "All Cameras" + assert browse.children[0].identifier == "test_id:browse:all" + assert browse.children[1].title == doorbell.name + assert browse.children[1].identifier == f"test_id:browse:{doorbell.id}" + assert browse.children[1].thumbnail is None + + +async def test_browse_media_camera_offline( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera +): + """Test browsing camera selector level media when camera is offline.""" + + doorbell.is_connected = False + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell]) + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, "test_id:browse", None) + + browse = await source.async_browse_media(media_item) + + assert browse.title == "UnifiProtect" + assert browse.identifier == "test_id:browse" + assert len(browse.children) == 2 + assert browse.children[0].title == "All Cameras" + assert browse.children[0].identifier == "test_id:browse:all" + assert browse.children[1].title == doorbell.name + assert browse.children[1].identifier == f"test_id:browse:{doorbell.id}" + assert browse.children[1].thumbnail is None + + +async def test_browse_media_event_type( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera +): + """Test browsing event type selector level media.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, "test_id:browse:all", None) + + browse = await source.async_browse_media(media_item) + + assert browse.title == "UnifiProtect > All Cameras" + assert browse.identifier == "test_id:browse:all" + assert len(browse.children) == 4 + assert browse.children[0].title == "All Events" + assert browse.children[0].identifier == "test_id:browse:all:all" + assert browse.children[1].title == "Ring Events" + assert browse.children[1].identifier == "test_id:browse:all:ring" + assert browse.children[2].title == "Motion Events" + assert browse.children[2].identifier == "test_id:browse:all:motion" + assert browse.children[3].title == "Smart Detections" + assert browse.children[3].identifier == "test_id:browse:all:smart" + + +async def test_browse_media_time( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test browsing time selector level media.""" + + last_month = fixed_now.replace(day=1) - timedelta(days=1) + ufp.api.bootstrap._recording_start = last_month + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + base_id = f"test_id:browse:{doorbell.id}:all" + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + assert browse.title == f"UnifiProtect > {doorbell.name} > All Events" + assert browse.identifier == base_id + assert len(browse.children) == 4 + assert browse.children[0].title == "Last 24 Hours" + assert browse.children[0].identifier == f"{base_id}:recent:1" + assert browse.children[1].title == "Last 7 Days" + assert browse.children[1].identifier == f"{base_id}:recent:7" + assert browse.children[2].title == "Last 30 Days" + assert browse.children[2].identifier == f"{base_id}:recent:30" + assert browse.children[3].title == f"{fixed_now.strftime('%B %Y')}" + assert ( + browse.children[3].identifier + == f"{base_id}:range:{fixed_now.year}:{fixed_now.month}" + ) + + +async def test_browse_media_recent( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test browsing event selector level media for recent days.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event = Event( + id="test_event_id", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event._api = ufp.api + ufp.api.get_events_raw = AsyncMock(return_value=[event.unifi_dict()]) + + base_id = f"test_id:browse:{doorbell.id}:motion:recent:1" + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + assert ( + browse.title + == f"UnifiProtect > {doorbell.name} > Motion Events > Last 24 Hours (1)" + ) + assert browse.identifier == base_id + assert len(browse.children) == 1 + assert browse.children[0].identifier == "test_id:event:test_event_id" + + +async def test_browse_media_recent_truncated( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test browsing event selector level media for recent days.""" + + ufp.entry.options = {"max_media": 1} + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event = Event( + id="test_event_id", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event._api = ufp.api + ufp.api.get_events_raw = AsyncMock(return_value=[event.unifi_dict()]) + + base_id = f"test_id:browse:{doorbell.id}:motion:recent:1" + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + assert ( + browse.title + == f"UnifiProtect > {doorbell.name} > Motion Events > Last 24 Hours (1 TRUNCATED)" + ) + assert browse.identifier == base_id + assert len(browse.children) == 1 + assert browse.children[0].identifier == "test_id:event:test_event_id" + + +async def test_browse_media_event( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test browsing specific event.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event = Event( + id="test_event_id", + type=EventType.RING, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event._api = ufp.api + ufp.api.get_event = AsyncMock(return_value=event) + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, "test_id:event:test_event_id", None) + + browse = await source.async_browse_media(media_item) + + assert browse.identifier == "test_id:event:test_event_id" + assert browse.children is None + assert browse.media_class == MEDIA_CLASS_VIDEO + + +async def test_browse_media_eventthumb( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test browsing specific event.""" + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event = Event( + id="test_event_id", + type=EventType.SMART_DETECT, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[SmartDetectObjectType.PERSON], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event._api = ufp.api + ufp.api.get_event = AsyncMock(return_value=event) + + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, "test_id:eventthumb:test_event_id", None) + + browse = await source.async_browse_media(media_item) + + assert browse.identifier == "test_id:eventthumb:test_event_id" + assert browse.children is None + assert browse.media_class == MEDIA_CLASS_IMAGE + + +async def test_browse_media_day( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test browsing day selector level media.""" + + last_month = fixed_now.replace(day=1) - timedelta(days=1) + ufp.api.bootstrap._recording_start = last_month + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + base_id = ( + f"test_id:browse:{doorbell.id}:all:range:{fixed_now.year}:{fixed_now.month}" + ) + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + assert ( + browse.title + == f"UnifiProtect > {doorbell.name} > All Events > {fixed_now.strftime('%B %Y')}" + ) + assert browse.identifier == base_id + assert len(browse.children) in (29, 30, 31, 32) + assert browse.children[0].title == "Whole Month" + assert browse.children[0].identifier == f"{base_id}:all" + + +async def test_browse_media_browse_day( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test events for a specific day.""" + + last_month = fixed_now.replace(day=1) - timedelta(days=1) + ufp.api.bootstrap._recording_start = last_month + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event = Event( + id="test_event_id", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event._api = ufp.api + ufp.api.get_events_raw = AsyncMock(return_value=[event.unifi_dict()]) + + base_id = f"test_id:browse:{doorbell.id}:motion:range:{fixed_now.year}:{fixed_now.month}:1" + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + start = fixed_now.replace(day=1) + assert ( + browse.title + == f"UnifiProtect > {doorbell.name} > Motion Events > {fixed_now.strftime('%B %Y')} > {start.strftime('%x')} (1)" + ) + assert browse.identifier == base_id + assert len(browse.children) == 1 + assert browse.children[0].identifier == "test_id:event:test_event_id" + + +async def test_browse_media_browse_whole_month( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test events for a specific day.""" + + fixed_now = fixed_now.replace(month=11) + last_month = fixed_now.replace(day=1) - timedelta(days=1) + ufp.api.bootstrap._recording_start = last_month + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event = Event( + id="test_event_id", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event._api = ufp.api + ufp.api.get_events_raw = AsyncMock(return_value=[event.unifi_dict()]) + + base_id = ( + f"test_id:browse:{doorbell.id}:all:range:{fixed_now.year}:{fixed_now.month}:all" + ) + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + assert ( + browse.title + == f"UnifiProtect > {doorbell.name} > All Events > {fixed_now.strftime('%B %Y')} > Whole Month (1)" + ) + assert browse.identifier == base_id + assert len(browse.children) == 1 + assert browse.children[0].identifier == "test_id:event:test_event_id" + + +async def test_browse_media_browse_whole_month_december( + hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera, fixed_now: datetime +): + """Test events for a specific day.""" + + fixed_now = fixed_now.replace(month=12) + last_month = fixed_now.replace(day=1) - timedelta(days=1) + ufp.api.bootstrap._recording_start = last_month + + ufp.api.get_bootstrap = AsyncMock(return_value=ufp.api.bootstrap) + await init_entry(hass, ufp, [doorbell], regenerate_ids=False) + + event1 = Event( + id="test_event_id", + type=EventType.SMART_DETECT, + start=fixed_now - timedelta(seconds=3663), + end=fixed_now, + score=100, + smart_detect_types=[SmartDetectObjectType.PERSON], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event1._api = ufp.api + event2 = Event( + id="test_event_id2", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[], + smart_detect_event_ids=["test_event_id"], + camera_id=doorbell.id, + ) + event2._api = ufp.api + event3 = Event( + id="test_event_id3", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=fixed_now, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id="other_camera", + ) + event3._api = ufp.api + event4 = Event( + id="test_event_id4", + type=EventType.MOTION, + start=fixed_now - timedelta(seconds=20), + end=None, + score=100, + smart_detect_types=[], + smart_detect_event_ids=[], + camera_id=doorbell.id, + ) + event4._api = ufp.api + + ufp.api.get_events_raw = AsyncMock( + return_value=[ + event1.unifi_dict(), + event2.unifi_dict(), + event3.unifi_dict(), + event4.unifi_dict(), + ] + ) + + base_id = ( + f"test_id:browse:{doorbell.id}:all:range:{fixed_now.year}:{fixed_now.month}:all" + ) + source = await async_get_media_source(hass) + media_item = MediaSourceItem(hass, DOMAIN, base_id, None) + + browse = await source.async_browse_media(media_item) + + assert ( + browse.title + == f"UnifiProtect > {doorbell.name} > All Events > {fixed_now.strftime('%B %Y')} > Whole Month (1)" + ) + assert browse.identifier == base_id + assert len(browse.children) == 1 + assert browse.children[0].identifier == "test_id:event:test_event_id" diff --git a/tests/components/unifiprotect/test_views.py b/tests/components/unifiprotect/test_views.py index e64a0a87377..0768252f6c9 100644 --- a/tests/components/unifiprotect/test_views.py +++ b/tests/components/unifiprotect/test_views.py @@ -341,7 +341,7 @@ async def test_video_bad_params( url = async_generate_event_video_url(event) from_value = event_start if start is not None else fixed_now to_value = start if start is not None else end - url = url.replace(from_value.isoformat(), to_value) + url = url.replace(from_value.replace(microsecond=0).isoformat(), to_value) http_client = await hass_client() response = cast(ClientResponse, await http_client.get(url)) From 36d6ef6228d2b0b5ce2af175d8679965ff3d8888 Mon Sep 17 00:00:00 2001 From: rlippmann <70883373+rlippmann@users.noreply.github.com> Date: Mon, 8 Aug 2022 16:34:38 -0400 Subject: [PATCH 3220/3516] Add ecobee CO2, VOC, and AQI sensors (#76366) * Add support for CO2, VOC, and AQI sensors * Update sensor.py * Formatting changes * Use class thermostat member in async_update * Remove thermostat attribute, add mixin class * Add docstrings * Add blank line * Sort imports Co-authored-by: Martin Hjelmare --- homeassistant/components/ecobee/sensor.py | 68 ++++++++++++++++++++--- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 250aac68a04..38671189132 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -1,6 +1,8 @@ """Support for Ecobee sensors.""" from __future__ import annotations +from dataclasses import dataclass + from pyecobee.const import ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN from homeassistant.components.sensor import ( @@ -10,27 +12,73 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, TEMP_FAHRENHEIT +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + PERCENTAGE, + TEMP_FAHRENHEIT, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( + +@dataclass +class EcobeeSensorEntityDescriptionMixin: + """Represent the required ecobee entity description attributes.""" + + runtime_key: str + + +@dataclass +class EcobeeSensorEntityDescription( + SensorEntityDescription, EcobeeSensorEntityDescriptionMixin +): + """Represent the ecobee sensor entity description.""" + + +SENSOR_TYPES: tuple[EcobeeSensorEntityDescription, ...] = ( + EcobeeSensorEntityDescription( key="temperature", name="Temperature", native_unit_of_measurement=TEMP_FAHRENHEIT, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, + runtime_key="actualTemperature", ), - SensorEntityDescription( + EcobeeSensorEntityDescription( key="humidity", name="Humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, + runtime_key="actualHumidity", + ), + EcobeeSensorEntityDescription( + key="co2PPM", + name="CO2", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=SensorDeviceClass.CO2, + state_class=SensorStateClass.MEASUREMENT, + runtime_key="actualCO2", + ), + EcobeeSensorEntityDescription( + key="vocPPM", + name="VOC", + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + runtime_key="actualVOC", + ), + EcobeeSensorEntityDescription( + key="airQuality", + name="Air Quality Index", + device_class=SensorDeviceClass.AQI, + native_unit_of_measurement=None, + state_class=SensorStateClass.MEASUREMENT, + runtime_key="actualAQScore", ), ) @@ -40,7 +88,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up ecobee (temperature and humidity) sensors.""" + """Set up ecobee sensors.""" data = hass.data[DOMAIN] entities = [ EcobeeSensor(data, sensor["name"], index, description) @@ -58,7 +106,11 @@ class EcobeeSensor(SensorEntity): """Representation of an Ecobee sensor.""" def __init__( - self, data, sensor_name, sensor_index, description: SensorEntityDescription + self, + data, + sensor_name, + sensor_index, + description: EcobeeSensorEntityDescription, ): """Initialize the sensor.""" self.entity_description = description @@ -66,7 +118,6 @@ class EcobeeSensor(SensorEntity): self.sensor_name = sensor_name self.index = sensor_index self._state = None - self._attr_name = f"{sensor_name} {description.name}" @property @@ -141,5 +192,6 @@ class EcobeeSensor(SensorEntity): for item in sensor["capability"]: if item["type"] != self.entity_description.key: continue - self._state = item["value"] + thermostat = self.data.ecobee.get_thermostat(self.index) + self._state = thermostat["runtime"][self.entity_description.runtime_key] break From acbeb8c881cdbe0c67119cf924df955e765bd9f5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 8 Aug 2022 14:53:27 -0600 Subject: [PATCH 3221/3516] Bump `regenmaschine` to 2022.08.0 (#76483) --- homeassistant/components/rainmachine/__init__.py | 16 ++++++++++++++-- .../components/rainmachine/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 1ad1fb734fa..a5426552ae2 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -31,6 +31,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity, UpdateFailed +from homeassistant.util.dt import as_timestamp, utcnow from homeassistant.util.network import is_ip_address from .config_flow import get_client_controller @@ -299,7 +300,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_restrict_watering(call: ServiceCall) -> None: """Restrict watering for a time period.""" controller = async_get_controller_for_service_call(hass, call) - await controller.restrictions.restrict(call.data[CONF_DURATION]) + duration = call.data[CONF_DURATION] + await controller.restrictions.set_universal( + { + "rainDelayStartTime": round(as_timestamp(utcnow())), + "rainDelayDuration": duration.total_seconds(), + }, + ) await async_update_programs_and_zones(hass, entry) async def async_stop_all(call: ServiceCall) -> None: @@ -317,7 +324,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unrestrict_watering(call: ServiceCall) -> None: """Unrestrict watering.""" controller = async_get_controller_for_service_call(hass, call) - await controller.restrictions.unrestrict() + await controller.restrictions.set_universal( + { + "rainDelayStartTime": round(as_timestamp(utcnow())), + "rainDelayDuration": 0, + }, + ) await async_update_programs_and_zones(hass, entry) for service_name, schema, method in ( diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 4d60730ba6c..b183fc1b24f 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -3,7 +3,7 @@ "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": ["regenmaschine==2022.07.3"], + "requirements": ["regenmaschine==2022.08.0"], "codeowners": ["@bachya"], "iot_class": "local_polling", "homekit": { diff --git a/requirements_all.txt b/requirements_all.txt index 1cd3875344c..d2777a873f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2082,7 +2082,7 @@ raincloudy==0.0.7 raspyrfm-client==1.2.8 # homeassistant.components.rainmachine -regenmaschine==2022.07.3 +regenmaschine==2022.08.0 # homeassistant.components.renault renault-api==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d387fd49e1d..75a0c06acc9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1409,7 +1409,7 @@ radios==0.1.1 radiotherm==2.1.0 # homeassistant.components.rainmachine -regenmaschine==2022.07.3 +regenmaschine==2022.08.0 # homeassistant.components.renault renault-api==0.1.11 From cefc535edba7ae15aaf10606e756b0222f26b85f Mon Sep 17 00:00:00 2001 From: Koen van Zuijlen Date: Mon, 8 Aug 2022 23:35:05 +0200 Subject: [PATCH 3222/3516] Add JustNimbus integration (#75718) Co-authored-by: Franck Nijhof --- .coveragerc | 4 + CODEOWNERS | 2 + .../components/justnimbus/__init__.py | 25 +++ .../components/justnimbus/config_flow.py | 60 ++++++ homeassistant/components/justnimbus/const.py | 13 ++ .../components/justnimbus/coordinator.py | 34 +++ homeassistant/components/justnimbus/entity.py | 37 ++++ .../components/justnimbus/manifest.json | 9 + homeassistant/components/justnimbus/sensor.py | 193 ++++++++++++++++++ .../components/justnimbus/strings.json | 19 ++ .../justnimbus/translations/en.json | 19 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/justnimbus/__init__.py | 1 + .../components/justnimbus/test_config_flow.py | 84 ++++++++ 16 files changed, 507 insertions(+) create mode 100644 homeassistant/components/justnimbus/__init__.py create mode 100644 homeassistant/components/justnimbus/config_flow.py create mode 100644 homeassistant/components/justnimbus/const.py create mode 100644 homeassistant/components/justnimbus/coordinator.py create mode 100644 homeassistant/components/justnimbus/entity.py create mode 100644 homeassistant/components/justnimbus/manifest.json create mode 100644 homeassistant/components/justnimbus/sensor.py create mode 100644 homeassistant/components/justnimbus/strings.json create mode 100644 homeassistant/components/justnimbus/translations/en.json create mode 100644 tests/components/justnimbus/__init__.py create mode 100644 tests/components/justnimbus/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 0a1430cce91..8a83b98873e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -602,6 +602,10 @@ omit = homeassistant/components/juicenet/number.py homeassistant/components/juicenet/sensor.py homeassistant/components/juicenet/switch.py + homeassistant/components/justnimbus/const.py + homeassistant/components/justnimbus/coordinator.py + homeassistant/components/justnimbus/entity.py + homeassistant/components/justnimbus/sensor.py homeassistant/components/kaiterra/* homeassistant/components/kankun/switch.py homeassistant/components/keba/* diff --git a/CODEOWNERS b/CODEOWNERS index 292879aabea..c92ad3b5ba9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -555,6 +555,8 @@ build.json @home-assistant/supervisor /tests/components/jewish_calendar/ @tsvi /homeassistant/components/juicenet/ @jesserockz /tests/components/juicenet/ @jesserockz +/homeassistant/components/justnimbus/ @kvanzuijlen +/tests/components/justnimbus/ @kvanzuijlen /homeassistant/components/kaiterra/ @Michsior14 /homeassistant/components/kaleidescape/ @SteveEasley /tests/components/kaleidescape/ @SteveEasley diff --git a/homeassistant/components/justnimbus/__init__.py b/homeassistant/components/justnimbus/__init__.py new file mode 100644 index 00000000000..695faa4f529 --- /dev/null +++ b/homeassistant/components/justnimbus/__init__.py @@ -0,0 +1,25 @@ +"""The JustNimbus integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN, PLATFORMS +from .coordinator import JustNimbusCoordinator + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up JustNimbus from a config entry.""" + coordinator = JustNimbusCoordinator(hass=hass, entry=entry) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/justnimbus/config_flow.py b/homeassistant/components/justnimbus/config_flow.py new file mode 100644 index 00000000000..bb55b1852b8 --- /dev/null +++ b/homeassistant/components/justnimbus/config_flow.py @@ -0,0 +1,60 @@ +"""Config flow for JustNimbus integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import justnimbus +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_CLIENT_ID +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + }, +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for JustNimbus.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + await self.async_set_unique_id(user_input[CONF_CLIENT_ID]) + self._abort_if_unique_id_configured() + + client = justnimbus.JustNimbusClient(client_id=user_input[CONF_CLIENT_ID]) + try: + await self.hass.async_add_executor_job(client.get_data) + except justnimbus.InvalidClientID: + errors["base"] = "invalid_auth" + except justnimbus.JustNimbusError: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title="JustNimbus", data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/justnimbus/const.py b/homeassistant/components/justnimbus/const.py new file mode 100644 index 00000000000..cf3d4ef825f --- /dev/null +++ b/homeassistant/components/justnimbus/const.py @@ -0,0 +1,13 @@ +"""Constants for the JustNimbus integration.""" + +from typing import Final + +from homeassistant.const import Platform + +DOMAIN = "justnimbus" + +VOLUME_FLOW_RATE_LITERS_PER_MINUTE: Final = "L/min" + +PLATFORMS = [ + Platform.SENSOR, +] diff --git a/homeassistant/components/justnimbus/coordinator.py b/homeassistant/components/justnimbus/coordinator.py new file mode 100644 index 00000000000..606cea0e922 --- /dev/null +++ b/homeassistant/components/justnimbus/coordinator.py @@ -0,0 +1,34 @@ +"""JustNimbus coordinator.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +import justnimbus + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_CLIENT_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class JustNimbusCoordinator(DataUpdateCoordinator[justnimbus.JustNimbusModel]): + """Data update coordinator.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(minutes=1), + ) + self._client = justnimbus.JustNimbusClient(client_id=entry.data[CONF_CLIENT_ID]) + + async def _async_update_data(self) -> justnimbus.JustNimbusModel: + """Fetch the latest data from the source.""" + return await self.hass.async_add_executor_job(self._client.get_data) diff --git a/homeassistant/components/justnimbus/entity.py b/homeassistant/components/justnimbus/entity.py new file mode 100644 index 00000000000..f9ea5ba1151 --- /dev/null +++ b/homeassistant/components/justnimbus/entity.py @@ -0,0 +1,37 @@ +"""Base Entity for JustNimbus sensors.""" +from __future__ import annotations + +import justnimbus + +from homeassistant.components.sensor import SensorEntity +from homeassistant.helpers import update_coordinator +from homeassistant.helpers.entity import DeviceInfo + +from . import JustNimbusCoordinator +from .const import DOMAIN + + +class JustNimbusEntity( + update_coordinator.CoordinatorEntity[justnimbus.JustNimbusModel], + SensorEntity, +): + """Defines a base JustNimbus entity.""" + + def __init__( + self, + *, + device_id: str, + coordinator: JustNimbusCoordinator, + ) -> None: + """Initialize the JustNimbus entity.""" + super().__init__(coordinator=coordinator) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device_id)}, + name="JustNimbus Sensor", + manufacturer="JustNimbus", + ) + + @property + def available(self) -> bool: + """Return device availability.""" + return super().available and getattr(self.coordinator.data, "error_code") == 0 diff --git a/homeassistant/components/justnimbus/manifest.json b/homeassistant/components/justnimbus/manifest.json new file mode 100644 index 00000000000..ca25832df00 --- /dev/null +++ b/homeassistant/components/justnimbus/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "justnimbus", + "name": "JustNimbus", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/justnimbus", + "requirements": ["justnimbus==0.6.0"], + "codeowners": ["@kvanzuijlen"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/justnimbus/sensor.py b/homeassistant/components/justnimbus/sensor.py new file mode 100644 index 00000000000..6041f84e25a --- /dev/null +++ b/homeassistant/components/justnimbus/sensor.py @@ -0,0 +1,193 @@ +"""Support for the JustNimbus platform.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_CLIENT_ID, + PRESSURE_BAR, + TEMP_CELSIUS, + VOLUME_LITERS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from . import JustNimbusCoordinator +from .const import DOMAIN, VOLUME_FLOW_RATE_LITERS_PER_MINUTE +from .entity import JustNimbusEntity + + +@dataclass +class JustNimbusEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[JustNimbusCoordinator], Any] + + +@dataclass +class JustNimbusEntityDescription( + SensorEntityDescription, JustNimbusEntityDescriptionMixin +): + """Describes JustNimbus sensor entity.""" + + +SENSOR_TYPES = ( + JustNimbusEntityDescription( + key="pump_flow", + name="Pump flow", + icon="mdi:pump", + native_unit_of_measurement=VOLUME_FLOW_RATE_LITERS_PER_MINUTE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.pump_flow, + ), + JustNimbusEntityDescription( + key="drink_flow", + name="Drink flow", + icon="mdi:water-pump", + native_unit_of_measurement=VOLUME_FLOW_RATE_LITERS_PER_MINUTE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.drink_flow, + ), + JustNimbusEntityDescription( + key="pump_pressure", + name="Pump pressure", + native_unit_of_measurement=PRESSURE_BAR, + device_class=SensorDeviceClass.PRESSURE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.pump_pressure, + ), + JustNimbusEntityDescription( + key="pump_starts", + name="Pump starts", + icon="mdi:restart", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.pump_starts, + ), + JustNimbusEntityDescription( + key="pump_hours", + name="Pump hours", + icon="mdi:clock", + device_class=SensorDeviceClass.DURATION, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.pump_hours, + ), + JustNimbusEntityDescription( + key="reservoir_temp", + name="Reservoir Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.reservoir_temp, + ), + JustNimbusEntityDescription( + key="reservoir_content", + name="Reservoir content", + icon="mdi:car-coolant-level", + native_unit_of_measurement=VOLUME_LITERS, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.reservoir_content, + ), + JustNimbusEntityDescription( + key="total_saved", + name="Total saved", + icon="mdi:water-opacity", + native_unit_of_measurement=VOLUME_LITERS, + state_class=SensorStateClass.TOTAL_INCREASING, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.total_saved, + ), + JustNimbusEntityDescription( + key="total_replenished", + name="Total replenished", + icon="mdi:water", + native_unit_of_measurement=VOLUME_LITERS, + state_class=SensorStateClass.TOTAL_INCREASING, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.total_replenished, + ), + JustNimbusEntityDescription( + key="error_code", + name="Error code", + icon="mdi:bug", + entity_registry_enabled_default=False, + native_unit_of_measurement="", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.error_code, + ), + JustNimbusEntityDescription( + key="totver", + name="Total use", + icon="mdi:chart-donut", + native_unit_of_measurement=VOLUME_LITERS, + state_class=SensorStateClass.TOTAL_INCREASING, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.totver, + ), + JustNimbusEntityDescription( + key="reservoir_content_max", + name="Max reservoir content", + icon="mdi:waves", + native_unit_of_measurement=VOLUME_LITERS, + state_class=SensorStateClass.TOTAL, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda coordinator: coordinator.data.reservoir_content_max, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the JustNimbus sensor.""" + coordinator: JustNimbusCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + JustNimbusSensor( + device_id=entry.data[CONF_CLIENT_ID], + description=description, + coordinator=coordinator, + ) + for description in SENSOR_TYPES + ) + + +class JustNimbusSensor( + JustNimbusEntity, +): + """Implementation of the JustNimbus sensor.""" + + def __init__( + self, + *, + device_id: str, + description: JustNimbusEntityDescription, + coordinator: JustNimbusCoordinator, + ) -> None: + """Initialize the sensor.""" + self.entity_description: JustNimbusEntityDescription = description + super().__init__( + device_id=device_id, + coordinator=coordinator, + ) + self._attr_unique_id = f"{device_id}_{description.key}" + + @property + def native_value(self) -> StateType: + """Return sensor state.""" + return self.entity_description.value_fn(self.coordinator) diff --git a/homeassistant/components/justnimbus/strings.json b/homeassistant/components/justnimbus/strings.json new file mode 100644 index 00000000000..609b1425e93 --- /dev/null +++ b/homeassistant/components/justnimbus/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "data": { + "client_id": "Client ID" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/justnimbus/translations/en.json b/homeassistant/components/justnimbus/translations/en.json new file mode 100644 index 00000000000..31443841e8a --- /dev/null +++ b/homeassistant/components/justnimbus/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "client_id": "Client ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 597cb10fd7f..d9da1ff2057 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -183,6 +183,7 @@ FLOWS = { "izone", "jellyfin", "juicenet", + "justnimbus", "kaleidescape", "keenetic_ndms2", "kmtronic", diff --git a/requirements_all.txt b/requirements_all.txt index d2777a873f3..6b67daab6fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -925,6 +925,9 @@ jellyfin-apiclient-python==1.8.1 # homeassistant.components.rest jsonpath==0.82 +# homeassistant.components.justnimbus +justnimbus==0.6.0 + # homeassistant.components.kaiterra kaiterra-async-client==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 75a0c06acc9..0c80e40f5e9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -675,6 +675,9 @@ jellyfin-apiclient-python==1.8.1 # homeassistant.components.rest jsonpath==0.82 +# homeassistant.components.justnimbus +justnimbus==0.6.0 + # homeassistant.components.konnected konnected==1.2.0 diff --git a/tests/components/justnimbus/__init__.py b/tests/components/justnimbus/__init__.py new file mode 100644 index 00000000000..46d872e08c6 --- /dev/null +++ b/tests/components/justnimbus/__init__.py @@ -0,0 +1 @@ +"""Tests for the JustNimbus integration.""" diff --git a/tests/components/justnimbus/test_config_flow.py b/tests/components/justnimbus/test_config_flow.py new file mode 100644 index 00000000000..d2cfb64d4c7 --- /dev/null +++ b/tests/components/justnimbus/test_config_flow.py @@ -0,0 +1,84 @@ +"""Test the JustNimbus config flow.""" +from unittest.mock import patch + +from justnimbus.exceptions import InvalidClientID, JustNimbusError +import pytest + +from homeassistant import config_entries +from homeassistant.components.justnimbus.const import DOMAIN +from homeassistant.const import CONF_CLIENT_ID +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + await _set_up_justnimbus(hass=hass, flow_id=result["flow_id"]) + + +@pytest.mark.parametrize( + "side_effect,errors", + ( + ( + InvalidClientID(client_id="test_id"), + {"base": "invalid_auth"}, + ), + ( + JustNimbusError(), + {"base": "cannot_connect"}, + ), + ), +) +async def test_form_errors( + hass: HomeAssistant, + side_effect: JustNimbusError, + errors: dict, +) -> None: + """Test we handle errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "justnimbus.JustNimbusClient.get_data", + side_effect=side_effect, + ): + result2 = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], + user_input={ + CONF_CLIENT_ID: "test_id", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == errors + + await _set_up_justnimbus(hass=hass, flow_id=result["flow_id"]) + + +async def _set_up_justnimbus(hass: HomeAssistant, flow_id: str) -> None: + """Reusable successful setup of JustNimbus sensor.""" + with patch("justnimbus.JustNimbusClient.get_data"), patch( + "homeassistant.components.justnimbus.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + flow_id=flow_id, + user_input={ + CONF_CLIENT_ID: "test_id", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "JustNimbus" + assert result2["data"] == { + CONF_CLIENT_ID: "test_id", + } + assert len(mock_setup_entry.mock_calls) == 1 From 01de1c6304d2b496e3117ee4126609a47557a880 Mon Sep 17 00:00:00 2001 From: Sarabveer Singh <4297171+sarabveer@users.noreply.github.com> Date: Mon, 8 Aug 2022 17:49:07 -0400 Subject: [PATCH 3223/3516] Update HomeKit PM2.5 mappings to US AQI (#76358) --- .../components/homekit/type_sensors.py | 3 +-- homeassistant/components/homekit/util.py | 25 +++++-------------- tests/components/homekit/test_type_sensors.py | 8 +++--- tests/components/homekit/test_util.py | 15 ++++++----- 4 files changed, 20 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 7e9b4cbbb31..e877ffff07a 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -56,7 +56,6 @@ from .util import ( convert_to_float, density_to_air_quality, density_to_air_quality_pm10, - density_to_air_quality_pm25, temperature_to_homekit, ) @@ -239,7 +238,7 @@ class PM25Sensor(AirQualitySensor): if self.char_density.value != density: self.char_density.set_value(density) _LOGGER.debug("%s: Set density to %d", self.entity_id, density) - air_quality = density_to_air_quality_pm25(density) + air_quality = density_to_air_quality(density) if self.char_quality.value != air_quality: self.char_quality.set_value(air_quality) _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 34df1008e76..b7af7d516dd 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -400,16 +400,16 @@ def temperature_to_states(temperature: float | int, unit: str) -> float: def density_to_air_quality(density: float) -> int: - """Map PM2.5 density to HomeKit AirQuality level.""" - if density <= 35: + """Map PM2.5 µg/m3 density to HomeKit AirQuality level.""" + if density <= 12: # US AQI 0-50 (HomeKit: Excellent) return 1 - if density <= 75: + if density <= 35.4: # US AQI 51-100 (HomeKit: Good) return 2 - if density <= 115: + if density <= 55.4: # US AQI 101-150 (HomeKit: Fair) return 3 - if density <= 150: + if density <= 150.4: # US AQI 151-200 (HomeKit: Inferior) return 4 - return 5 + return 5 # US AQI 201+ (HomeKit: Poor) def density_to_air_quality_pm10(density: float) -> int: @@ -425,19 +425,6 @@ def density_to_air_quality_pm10(density: float) -> int: return 5 -def density_to_air_quality_pm25(density: float) -> int: - """Map PM2.5 density to HomeKit AirQuality level.""" - if density <= 25: - return 1 - if density <= 50: - return 2 - if density <= 100: - return 3 - if density <= 300: - return 4 - return 5 - - def get_persist_filename_for_entry_id(entry_id: str) -> str: """Determine the filename of the homekit state file.""" return f"{DOMAIN}.{entry_id}.state" diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 75069ce9467..b916d447d12 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -126,7 +126,7 @@ async def test_air_quality(hass, hk_driver): hass.states.async_set(entity_id, "34") await hass.async_block_till_done() assert acc.char_density.value == 34 - assert acc.char_quality.value == 1 + assert acc.char_quality.value == 2 hass.states.async_set(entity_id, "200") await hass.async_block_till_done() @@ -205,7 +205,7 @@ async def test_pm25(hass, hk_driver): hass.states.async_set(entity_id, "23") await hass.async_block_till_done() assert acc.char_density.value == 23 - assert acc.char_quality.value == 1 + assert acc.char_quality.value == 2 hass.states.async_set(entity_id, "34") await hass.async_block_till_done() @@ -215,12 +215,12 @@ async def test_pm25(hass, hk_driver): hass.states.async_set(entity_id, "90") await hass.async_block_till_done() assert acc.char_density.value == 90 - assert acc.char_quality.value == 3 + assert acc.char_quality.value == 4 hass.states.async_set(entity_id, "200") await hass.async_block_till_done() assert acc.char_density.value == 200 - assert acc.char_quality.value == 4 + assert acc.char_quality.value == 5 hass.states.async_set(entity_id, "400") await hass.async_block_till_done() diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 3dd30af2056..f3811ce34c5 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -224,12 +224,15 @@ def test_temperature_to_states(): def test_density_to_air_quality(): """Test map PM2.5 density to HomeKit AirQuality level.""" assert density_to_air_quality(0) == 1 - assert density_to_air_quality(35) == 1 - assert density_to_air_quality(35.1) == 2 - assert density_to_air_quality(75) == 2 - assert density_to_air_quality(115) == 3 - assert density_to_air_quality(150) == 4 - assert density_to_air_quality(300) == 5 + assert density_to_air_quality(12) == 1 + assert density_to_air_quality(12.1) == 2 + assert density_to_air_quality(35.4) == 2 + assert density_to_air_quality(35.5) == 3 + assert density_to_air_quality(55.4) == 3 + assert density_to_air_quality(55.5) == 4 + assert density_to_air_quality(150.4) == 4 + assert density_to_air_quality(150.5) == 5 + assert density_to_air_quality(200) == 5 async def test_async_show_setup_msg(hass, hk_driver, mock_get_source_ip): From 759503f8633adb6a6f601ee2d6964a7e6923eb75 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 8 Aug 2022 16:46:48 -0600 Subject: [PATCH 3224/3516] Ensure ConfirmRepairFlow can make use of translation placeholders (#76336) * Ensure ConfirmRepairFlow can make use of translation placeholders * Automatically determine the issue * Fix tests * Update homeassistant/components/repairs/issue_handler.py Co-authored-by: Franck Nijhof Co-authored-by: Franck Nijhof --- homeassistant/components/repairs/issue_handler.py | 12 ++++++++++-- tests/components/repairs/test_websocket_api.py | 15 ++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index 5695e99998b..b37d4c10e06 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -27,7 +27,6 @@ class ConfirmRepairFlow(RepairsFlow): self, user_input: dict[str, str] | None = None ) -> data_entry_flow.FlowResult: """Handle the first step of a fix flow.""" - return await (self.async_step_confirm()) async def async_step_confirm( @@ -37,7 +36,16 @@ class ConfirmRepairFlow(RepairsFlow): if user_input is not None: return self.async_create_entry(title="", data={}) - return self.async_show_form(step_id="confirm", data_schema=vol.Schema({})) + issue_registry = async_get_issue_registry(self.hass) + description_placeholders = None + if issue := issue_registry.async_get_issue(self.handler, self.issue_id): + description_placeholders = issue.translation_placeholders + + return self.async_show_form( + step_id="confirm", + data_schema=vol.Schema({}), + description_placeholders=description_placeholders, + ) class RepairsFlowManager(data_entry_flow.FlowManager): diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index 1cb83d81b06..2a506c7a248 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -253,14 +253,19 @@ async def test_fix_non_existing_issue( @pytest.mark.parametrize( - "domain, step", + "domain, step, description_placeholders", ( - ("fake_integration", "custom_step"), - ("fake_integration_default_handler", "confirm"), + ("fake_integration", "custom_step", None), + ("fake_integration_default_handler", "confirm", {"abc": "123"}), ), ) async def test_fix_issue( - hass: HomeAssistant, hass_client, hass_ws_client, domain, step + hass: HomeAssistant, + hass_client, + hass_ws_client, + domain, + step, + description_placeholders, ) -> None: """Test we can fix an issue.""" assert await async_setup_component(hass, "http", {}) @@ -288,7 +293,7 @@ async def test_fix_issue( flow_id = data["flow_id"] assert data == { "data_schema": [], - "description_placeholders": None, + "description_placeholders": description_placeholders, "errors": None, "flow_id": ANY, "handler": domain, From 2d431453033d209f5f56455824a3b584c3c989b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Aug 2022 13:06:49 -1000 Subject: [PATCH 3225/3516] Bump aiohomekit to 1.2.6 (#76488) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 5f6b3f92220..fa7bf39d385 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.5"], + "requirements": ["aiohomekit==1.2.6"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 6b67daab6fd..aed97e37a0e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.5 +aiohomekit==1.2.6 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c80e40f5e9..7f612cf9526 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.5 +aiohomekit==1.2.6 # homeassistant.components.emulated_hue # homeassistant.components.http From 12721da063c5edeadb42118559dbb0f3ba274b28 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 9 Aug 2022 00:28:50 +0000 Subject: [PATCH 3226/3516] [ci skip] Translation update --- .../anthemav/translations/zh-Hant.json | 2 +- .../components/demo/translations/ja.json | 9 +++++ .../deutsche_bahn/translations/ja.json | 7 ++++ .../components/escea/translations/ca.json | 8 +++++ .../components/escea/translations/de.json | 13 ++++++++ .../components/escea/translations/fr.json | 13 ++++++++ .../components/escea/translations/id.json | 13 ++++++++ .../components/escea/translations/ja.json | 8 +++++ .../escea/translations/zh-Hant.json | 13 ++++++++ .../justnimbus/translations/fr.json | 19 +++++++++++ .../components/mysensors/translations/id.json | 1 + .../components/mysensors/translations/ja.json | 7 ++++ .../components/mysensors/translations/no.json | 9 +++++ .../openexchangerates/translations/ca.json | 23 +++++++++++++ .../openexchangerates/translations/de.json | 33 +++++++++++++++++++ .../openexchangerates/translations/id.json | 33 +++++++++++++++++++ .../openexchangerates/translations/ja.json | 25 ++++++++++++++ .../openexchangerates/translations/no.json | 33 +++++++++++++++++++ .../openexchangerates/translations/pt-BR.json | 33 +++++++++++++++++++ .../translations/zh-Hant.json | 33 +++++++++++++++++++ .../simplepush/translations/zh-Hant.json | 2 +- .../soundtouch/translations/zh-Hant.json | 2 +- .../unifiprotect/translations/fr.json | 1 + 23 files changed, 337 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/deutsche_bahn/translations/ja.json create mode 100644 homeassistant/components/escea/translations/ca.json create mode 100644 homeassistant/components/escea/translations/de.json create mode 100644 homeassistant/components/escea/translations/fr.json create mode 100644 homeassistant/components/escea/translations/id.json create mode 100644 homeassistant/components/escea/translations/ja.json create mode 100644 homeassistant/components/escea/translations/zh-Hant.json create mode 100644 homeassistant/components/justnimbus/translations/fr.json create mode 100644 homeassistant/components/openexchangerates/translations/ca.json create mode 100644 homeassistant/components/openexchangerates/translations/de.json create mode 100644 homeassistant/components/openexchangerates/translations/id.json create mode 100644 homeassistant/components/openexchangerates/translations/ja.json create mode 100644 homeassistant/components/openexchangerates/translations/no.json create mode 100644 homeassistant/components/openexchangerates/translations/pt-BR.json create mode 100644 homeassistant/components/openexchangerates/translations/zh-Hant.json diff --git a/homeassistant/components/anthemav/translations/zh-Hant.json b/homeassistant/components/anthemav/translations/zh-Hant.json index acba04f49e6..d68bd690c48 100644 --- a/homeassistant/components/anthemav/translations/zh-Hant.json +++ b/homeassistant/components/anthemav/translations/zh-Hant.json @@ -18,7 +18,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Anthem A/V \u63a5\u6536\u5668\u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Anthem A/V \u63a5\u6536\u5668\u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "Anthem A/V \u63a5\u6536\u5668 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" } } diff --git a/homeassistant/components/demo/translations/ja.json b/homeassistant/components/demo/translations/ja.json index 30467e3df5b..97eb4866bfa 100644 --- a/homeassistant/components/demo/translations/ja.json +++ b/homeassistant/components/demo/translations/ja.json @@ -1,5 +1,14 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "SUBMIT(\u9001\u4fe1)\u3092\u62bc\u3057\u3066\u3001\u96fb\u6e90\u304c\u4ea4\u63db\u3055\u308c\u305f\u3053\u3068\u3092\u78ba\u8a8d\u3057\u307e\u3059" + } + } + } + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/deutsche_bahn/translations/ja.json b/homeassistant/components/deutsche_bahn/translations/ja.json new file mode 100644 index 00000000000..191ec874abf --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/ja.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "\u30c9\u30a4\u30c4\u9244\u9053(Deutsche Bahn)\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/ca.json b/homeassistant/components/escea/translations/ca.json new file mode 100644 index 00000000000..d28b9050682 --- /dev/null +++ b/homeassistant/components/escea/translations/ca.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/de.json b/homeassistant/components/escea/translations/de.json new file mode 100644 index 00000000000..9ff884c6073 --- /dev/null +++ b/homeassistant/components/escea/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "confirm": { + "description": "M\u00f6chtest du einen Escea-Kamin einrichten?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/fr.json b/homeassistant/components/escea/translations/fr.json new file mode 100644 index 00000000000..68cd0cce6dc --- /dev/null +++ b/homeassistant/components/escea/translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "step": { + "confirm": { + "description": "Voulez-vous configurer une chemin\u00e9e Escea\u00a0?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/id.json b/homeassistant/components/escea/translations/id.json new file mode 100644 index 00000000000..66ad19d2a15 --- /dev/null +++ b/homeassistant/components/escea/translations/id.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "step": { + "confirm": { + "description": "Ingin menyiapkan tungku Escea?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/ja.json b/homeassistant/components/escea/translations/ja.json new file mode 100644 index 00000000000..a64c00ee212 --- /dev/null +++ b/homeassistant/components/escea/translations/ja.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/zh-Hant.json b/homeassistant/components/escea/translations/zh-Hant.json new file mode 100644 index 00000000000..403184e5a23 --- /dev/null +++ b/homeassistant/components/escea/translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Escea fireplace\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/fr.json b/homeassistant/components/justnimbus/translations/fr.json new file mode 100644 index 00000000000..a8c26f118e3 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "client_id": "ID du client" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/id.json b/homeassistant/components/mysensors/translations/id.json index 418da20676e..67a7469b48a 100644 --- a/homeassistant/components/mysensors/translations/id.json +++ b/homeassistant/components/mysensors/translations/id.json @@ -70,6 +70,7 @@ "description": "Pengaturan gateway Ethernet" }, "select_gateway_type": { + "description": "Pilih gateway mana yang akan dikonfigurasi.", "menu_options": { "gw_mqtt": "Konfigurasikan gateway MQTT", "gw_serial": "Konfigurasikan gateway serial", diff --git a/homeassistant/components/mysensors/translations/ja.json b/homeassistant/components/mysensors/translations/ja.json index 4a73363bf16..2732a622511 100644 --- a/homeassistant/components/mysensors/translations/ja.json +++ b/homeassistant/components/mysensors/translations/ja.json @@ -68,6 +68,13 @@ }, "description": "\u30a4\u30fc\u30b5\u30cd\u30c3\u30c8 \u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7" }, + "select_gateway_type": { + "description": "\u8a2d\u5b9a\u3059\u308b\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3092\u9078\u629e\u3057\u307e\u3059\u3002", + "menu_options": { + "gw_mqtt": "MQTT\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u8a2d\u5b9a", + "gw_tcp": "TCP\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u8a2d\u5b9a" + } + }, "user": { "data": { "gateway_type": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u7a2e\u985e" diff --git a/homeassistant/components/mysensors/translations/no.json b/homeassistant/components/mysensors/translations/no.json index 14fb39910fe..1e45899d981 100644 --- a/homeassistant/components/mysensors/translations/no.json +++ b/homeassistant/components/mysensors/translations/no.json @@ -14,6 +14,7 @@ "invalid_serial": "Ugyldig serieport", "invalid_subscribe_topic": "Ugyldig abonnementsemne", "invalid_version": "Ugyldig MySensors-versjon", + "mqtt_required": "MQTT-integrasjonen er ikke satt opp", "not_a_number": "Vennligst skriv inn et nummer", "port_out_of_range": "Portnummer m\u00e5 v\u00e6re minst 1 og maksimalt 65535", "same_topic": "Abonner og publiser emner er de samme", @@ -68,6 +69,14 @@ }, "description": "Ethernet gateway-oppsett" }, + "select_gateway_type": { + "description": "Velg hvilken gateway som skal konfigureres.", + "menu_options": { + "gw_mqtt": "Konfigurer en MQTT-gateway", + "gw_serial": "Konfigurer en seriell gateway", + "gw_tcp": "Konfigurer en TCP-gateway" + } + }, "user": { "data": { "gateway_type": "" diff --git a/homeassistant/components/openexchangerates/translations/ca.json b/homeassistant/components/openexchangerates/translations/ca.json new file mode 100644 index 00000000000..f5a93caa71d --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "timeout_connect": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "timeout_connect": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/de.json b/homeassistant/components/openexchangerates/translations/de.json new file mode 100644 index 00000000000..51a0294c816 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/de.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "timeout_connect": "Zeit\u00fcberschreitung beim Verbindungsaufbau" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "timeout_connect": "Zeit\u00fcberschreitung beim Verbindungsaufbau", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "base": "Basisw\u00e4hrung" + }, + "data_description": { + "base": "Die Verwendung einer anderen Basisw\u00e4hrung als USD erfordert einen [kostenpflichtigen Plan]({signup})." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Open Exchange Rates mittels YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die YAML-Konfiguration f\u00fcr Open Exchange Rates aus deiner configuration.yaml und starte den Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Open Exchange Rates YAML-Konfiguration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/id.json b/homeassistant/components/openexchangerates/translations/id.json new file mode 100644 index 00000000000..0b5f0183e2b --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/id.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi", + "cannot_connect": "Gagal terhubung", + "reauth_successful": "Autentikasi ulang berhasil", + "timeout_connect": "Tenggang waktu membuat koneksi habis" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "timeout_connect": "Tenggang waktu membuat koneksi habis", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "api_key": "Kunci API", + "base": "Mata uang dasar" + }, + "data_description": { + "base": "Menggunakan mata uang dasar selain USD memerlukan [paket berbayar]({daftar})." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Open Exchange Rates lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Open Exchange Rates dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Open Exchange Rates dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/ja.json b/homeassistant/components/openexchangerates/translations/ja.json new file mode 100644 index 00000000000..35a212e6d82 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/ja.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "timeout_connect": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "timeout_connect": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "api_key": "API\u30ad\u30fc" + }, + "data_description": { + "base": "\u7c73\u30c9\u30eb\u4ee5\u5916\u306e\u57fa\u672c\u901a\u8ca8\u3092\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001[\u6709\u6599\u30d7\u30e9\u30f3] ({signup}) \u304c\u5fc5\u8981\u3067\u3059\u3002" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/no.json b/homeassistant/components/openexchangerates/translations/no.json new file mode 100644 index 00000000000..93a85a767c8 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/no.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "timeout_connect": "Tidsavbrudd oppretter forbindelse" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "timeout_connect": "Tidsavbrudd oppretter forbindelse", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "base": "Grunnvaluta" + }, + "data_description": { + "base": "Bruk av en annen basisvaluta enn USD krever en [betalt plan]( {signup} )." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av \u00e5pne valutakurser ved hjelp av YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Open Exchange Rates YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Open Exchange Rates YAML-konfigurasjonen blir fjernet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/pt-BR.json b/homeassistant/components/openexchangerates/translations/pt-BR.json new file mode 100644 index 00000000000..b7a8bc33614 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/pt-BR.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falhou ao conectar", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "timeout_connect": "Tempo limite estabelecendo conex\u00e3o" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "timeout_connect": "Tempo limite estabelecendo conex\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Chave API", + "base": "Moeda base" + }, + "data_description": { + "base": "Usar outra moeda base que n\u00e3o seja USD requer um [plano pago]( {signup} )." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o de Open Exchange Rates usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML do Open Exchange Rates do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o de YAML de Open Exchange Rates est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/zh-Hant.json b/homeassistant/components/openexchangerates/translations/zh-Hant.json new file mode 100644 index 00000000000..c9f8df654e4 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/zh-Hant.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "timeout_connect": "\u5efa\u7acb\u9023\u7dda\u903e\u6642" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "timeout_connect": "\u5efa\u7acb\u9023\u7dda\u903e\u6642", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "base": "\u57fa\u6e96\u8ca8\u5e63" + }, + "data_description": { + "base": "\u4f7f\u7528\u5176\u4ed6\u8ca8\u5e63\u53d6\u4ee3\u7f8e\u91d1\u505a\u70ba\u57fa\u6e96\u8ca8\u5e63\u70ba [\u4ed8\u8cbb]({signup}) \u670d\u52d9\u3002" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Open Exchange Rates \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Open Exchange Rates YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Open Exchange Rates YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/zh-Hant.json b/homeassistant/components/simplepush/translations/zh-Hant.json index 2e5b3576135..ea51e58e648 100644 --- a/homeassistant/components/simplepush/translations/zh-Hant.json +++ b/homeassistant/components/simplepush/translations/zh-Hant.json @@ -20,7 +20,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Simplepush \u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Simplepush YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Simplepush \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Simplepush YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "Simplepush YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" }, "removed_yaml": { diff --git a/homeassistant/components/soundtouch/translations/zh-Hant.json b/homeassistant/components/soundtouch/translations/zh-Hant.json index 80d78ba9d20..4387df76e05 100644 --- a/homeassistant/components/soundtouch/translations/zh-Hant.json +++ b/homeassistant/components/soundtouch/translations/zh-Hant.json @@ -20,7 +20,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Bose SoundTouch \u5373\u5c07\u65bc Home Assistant 2022.9 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Bose SoundTouch YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Bose SoundTouch \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Bose SoundTouch YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "Bose SoundTouch YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" } } diff --git a/homeassistant/components/unifiprotect/translations/fr.json b/homeassistant/components/unifiprotect/translations/fr.json index e1f189a55d5..c6527d39a39 100644 --- a/homeassistant/components/unifiprotect/translations/fr.json +++ b/homeassistant/components/unifiprotect/translations/fr.json @@ -47,6 +47,7 @@ "data": { "all_updates": "M\u00e9triques en temps r\u00e9el (AVERTISSEMENT\u00a0: augmente consid\u00e9rablement l'utilisation du processeur)", "disable_rtsp": "D\u00e9sactiver le flux RTSP", + "max_media": "Nombre maximal d'\u00e9v\u00e9nements \u00e0 charger pour le navigateur multim\u00e9dia (augmente l'utilisation de la RAM)", "override_connection_host": "Ignorer l'h\u00f4te de connexion" }, "description": "L'option Mesures en temps r\u00e9el ne doit \u00eatre activ\u00e9e que si vous avez activ\u00e9 les capteurs de diagnostic et souhaitez qu'ils soient mis \u00e0 jour en temps r\u00e9el. S'ils ne sont pas activ\u00e9s, ils ne seront mis \u00e0 jour qu'une fois toutes les 15 minutes.", From 7d427ddbd4b5eca09b0dfa6f7523e2efd2af5afc Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 9 Aug 2022 06:36:39 +0100 Subject: [PATCH 3227/3516] Allow parsing to happen in PassiveBluetoothProcessorCoordinator (#76384) --- .../bluetooth/passive_update_processor.py | 49 +++-- .../components/govee_ble/__init__.py | 4 + homeassistant/components/govee_ble/sensor.py | 16 +- homeassistant/components/inkbird/__init__.py | 9 +- homeassistant/components/inkbird/sensor.py | 16 +- homeassistant/components/moat/__init__.py | 9 +- homeassistant/components/moat/sensor.py | 16 +- .../components/sensorpush/__init__.py | 9 +- homeassistant/components/sensorpush/sensor.py | 16 +- .../components/xiaomi_ble/__init__.py | 44 +++- homeassistant/components/xiaomi_ble/sensor.py | 40 +--- .../test_passive_update_processor.py | 190 ++++++++++++++++-- 12 files changed, 288 insertions(+), 130 deletions(-) diff --git a/homeassistant/components/bluetooth/passive_update_processor.py b/homeassistant/components/bluetooth/passive_update_processor.py index 78966d9b7ab..bb9e82a7dbe 100644 --- a/homeassistant/components/bluetooth/passive_update_processor.py +++ b/homeassistant/components/bluetooth/passive_update_processor.py @@ -52,11 +52,17 @@ class PassiveBluetoothDataUpdate(Generic[_T]): ) -class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): +class PassiveBluetoothProcessorCoordinator( + Generic[_T], BasePassiveBluetoothCoordinator +): """Passive bluetooth processor coordinator for bluetooth advertisements. The coordinator is responsible for dispatching the bluetooth data, to each processor, and tracking devices. + + The update_method should return the data that is dispatched to each processor. + This is normally a parsed form of the data, but you can just forward the + BluetoothServiceInfoBleak if needed. """ def __init__( @@ -65,10 +71,18 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): logger: logging.Logger, address: str, mode: BluetoothScanningMode, + update_method: Callable[[BluetoothServiceInfoBleak], _T], ) -> None: """Initialize the coordinator.""" super().__init__(hass, logger, address, mode) self._processors: list[PassiveBluetoothDataProcessor] = [] + self._update_method = update_method + self.last_update_success = True + + @property + def available(self) -> bool: + """Return if the device is available.""" + return super().available and self.last_update_success @callback def async_register_processor( @@ -102,8 +116,22 @@ class PassiveBluetoothProcessorCoordinator(BasePassiveBluetoothCoordinator): super()._async_handle_bluetooth_event(service_info, change) if self.hass.is_stopping: return + + try: + update = self._update_method(service_info) + except Exception as err: # pylint: disable=broad-except + self.last_update_success = False + self.logger.exception( + "Unexpected error updating %s data: %s", self.name, err + ) + return + + if not self.last_update_success: + self.last_update_success = True + self.logger.info("Coordinator %s recovered", self.name) + for processor in self._processors: - processor.async_handle_bluetooth_event(service_info, change) + processor.async_handle_update(update) _PassiveBluetoothDataProcessorT = TypeVar( @@ -123,9 +151,8 @@ class PassiveBluetoothDataProcessor(Generic[_T]): the appropriate format. The processor will call the update_method every time the bluetooth device - receives a new advertisement data from the coordinator with the following signature: - - update_method(service_info: BluetoothServiceInfoBleak) -> PassiveBluetoothDataUpdate + receives a new advertisement data from the coordinator with the data + returned by he update_method of the coordinator. As the size of each advertisement is limited, the update_method should return a PassiveBluetoothDataUpdate object that contains only data that @@ -138,9 +165,7 @@ class PassiveBluetoothDataProcessor(Generic[_T]): def __init__( self, - update_method: Callable[ - [BluetoothServiceInfoBleak], PassiveBluetoothDataUpdate[_T] - ], + update_method: Callable[[_T], PassiveBluetoothDataUpdate[_T]], ) -> None: """Initialize the coordinator.""" self.coordinator: PassiveBluetoothProcessorCoordinator @@ -244,14 +269,10 @@ class PassiveBluetoothDataProcessor(Generic[_T]): update_callback(data) @callback - def async_handle_bluetooth_event( - self, - service_info: BluetoothServiceInfoBleak, - change: BluetoothChange, - ) -> None: + def async_handle_update(self, update: _T) -> None: """Handle a Bluetooth event.""" try: - new_data = self.update_method(service_info) + new_data = self.update_method(update) except Exception as err: # pylint: disable=broad-except self.last_update_success = False self.coordinator.logger.exception( diff --git a/homeassistant/components/govee_ble/__init__.py b/homeassistant/components/govee_ble/__init__.py index 7a134e43ace..a2dbecf85a2 100644 --- a/homeassistant/components/govee_ble/__init__.py +++ b/homeassistant/components/govee_ble/__init__.py @@ -3,6 +3,8 @@ from __future__ import annotations import logging +from govee_ble import GoveeBluetoothDeviceData + from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, @@ -22,6 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Govee BLE device from a config entry.""" address = entry.unique_id assert address is not None + data = GoveeBluetoothDeviceData() coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( @@ -29,6 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER, address=address, mode=BluetoothScanningMode.ACTIVE, + update_method=data.update, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/govee_ble/sensor.py b/homeassistant/components/govee_ble/sensor.py index d0b9447d9e3..4faa6befa06 100644 --- a/homeassistant/components/govee_ble/sensor.py +++ b/homeassistant/components/govee_ble/sensor.py @@ -3,14 +3,7 @@ from __future__ import annotations from typing import Optional, Union -from govee_ble import ( - DeviceClass, - DeviceKey, - GoveeBluetoothDeviceData, - SensorDeviceInfo, - SensorUpdate, - Units, -) +from govee_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( @@ -129,12 +122,7 @@ async def async_setup_entry( coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] - data = GoveeBluetoothDeviceData() - processor = PassiveBluetoothDataProcessor( - lambda service_info: sensor_update_to_bluetooth_data_update( - data.update(service_info) - ) - ) + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) entry.async_on_unload( processor.async_add_entities_listener( GoveeBluetoothSensorEntity, async_add_entities diff --git a/homeassistant/components/inkbird/__init__.py b/homeassistant/components/inkbird/__init__.py index 0272114b83c..5ed0d6fb367 100644 --- a/homeassistant/components/inkbird/__init__.py +++ b/homeassistant/components/inkbird/__init__.py @@ -3,6 +3,8 @@ from __future__ import annotations import logging +from inkbird_ble import INKBIRDBluetoothDeviceData + from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, @@ -22,10 +24,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up INKBIRD BLE device from a config entry.""" address = entry.unique_id assert address is not None + data = INKBIRDBluetoothDeviceData() coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, address=address, mode=BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.ACTIVE, + update_method=data.update, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/inkbird/sensor.py b/homeassistant/components/inkbird/sensor.py index 0648ca80383..71d6f00ea40 100644 --- a/homeassistant/components/inkbird/sensor.py +++ b/homeassistant/components/inkbird/sensor.py @@ -3,14 +3,7 @@ from __future__ import annotations from typing import Optional, Union -from inkbird_ble import ( - DeviceClass, - DeviceKey, - INKBIRDBluetoothDeviceData, - SensorDeviceInfo, - SensorUpdate, - Units, -) +from inkbird_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( @@ -129,12 +122,7 @@ async def async_setup_entry( coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] - data = INKBIRDBluetoothDeviceData() - processor = PassiveBluetoothDataProcessor( - lambda service_info: sensor_update_to_bluetooth_data_update( - data.update(service_info) - ) - ) + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) entry.async_on_unload( processor.async_add_entities_listener( INKBIRDBluetoothSensorEntity, async_add_entities diff --git a/homeassistant/components/moat/__init__.py b/homeassistant/components/moat/__init__.py index 237948a8ff6..ed360f53b65 100644 --- a/homeassistant/components/moat/__init__.py +++ b/homeassistant/components/moat/__init__.py @@ -3,6 +3,8 @@ from __future__ import annotations import logging +from moat_ble import MoatBluetoothDeviceData + from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, @@ -22,10 +24,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Moat BLE device from a config entry.""" address = entry.unique_id assert address is not None + data = MoatBluetoothDeviceData() coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/moat/sensor.py b/homeassistant/components/moat/sensor.py index 295e2877aed..c5e02a38dcd 100644 --- a/homeassistant/components/moat/sensor.py +++ b/homeassistant/components/moat/sensor.py @@ -3,14 +3,7 @@ from __future__ import annotations from typing import Optional, Union -from moat_ble import ( - DeviceClass, - DeviceKey, - MoatBluetoothDeviceData, - SensorDeviceInfo, - SensorUpdate, - Units, -) +from moat_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( @@ -136,12 +129,7 @@ async def async_setup_entry( coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] - data = MoatBluetoothDeviceData() - processor = PassiveBluetoothDataProcessor( - lambda service_info: sensor_update_to_bluetooth_data_update( - data.update(service_info) - ) - ) + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) entry.async_on_unload( processor.async_add_entities_listener( MoatBluetoothSensorEntity, async_add_entities diff --git a/homeassistant/components/sensorpush/__init__.py b/homeassistant/components/sensorpush/__init__.py index d4a0872ba3f..7828a581d07 100644 --- a/homeassistant/components/sensorpush/__init__.py +++ b/homeassistant/components/sensorpush/__init__.py @@ -3,6 +3,8 @@ from __future__ import annotations import logging +from sensorpush_ble import SensorPushBluetoothDeviceData + from homeassistant.components.bluetooth import BluetoothScanningMode from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, @@ -22,10 +24,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SensorPush BLE device from a config entry.""" address = entry.unique_id assert address is not None + data = SensorPushBluetoothDeviceData() coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/sensorpush/sensor.py b/homeassistant/components/sensorpush/sensor.py index 9bfa59e3876..8a4db7aff14 100644 --- a/homeassistant/components/sensorpush/sensor.py +++ b/homeassistant/components/sensorpush/sensor.py @@ -3,14 +3,7 @@ from __future__ import annotations from typing import Optional, Union -from sensorpush_ble import ( - DeviceClass, - DeviceKey, - SensorDeviceInfo, - SensorPushBluetoothDeviceData, - SensorUpdate, - Units, -) +from sensorpush_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( @@ -130,12 +123,7 @@ async def async_setup_entry( coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] - data = SensorPushBluetoothDeviceData() - processor = PassiveBluetoothDataProcessor( - lambda service_info: sensor_update_to_bluetooth_data_update( - data.update(service_info) - ) - ) + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) entry.async_on_unload( processor.async_add_entities_listener( SensorPushBluetoothSensorEntity, async_add_entities diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 791ac1447ad..e3e30e0c79e 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -3,7 +3,14 @@ from __future__ import annotations import logging -from homeassistant.components.bluetooth import BluetoothScanningMode +from xiaomi_ble import SensorUpdate, XiaomiBluetoothDeviceData +from xiaomi_ble.parser import EncryptionScheme + +from homeassistant import config_entries +from homeassistant.components.bluetooth import ( + BluetoothScanningMode, + BluetoothServiceInfoBleak, +) from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorCoordinator, ) @@ -18,14 +25,47 @@ PLATFORMS: list[Platform] = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) +def process_service_info( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + data: XiaomiBluetoothDeviceData, + service_info: BluetoothServiceInfoBleak, +) -> SensorUpdate: + """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data.""" + update = data.update(service_info) + + # If device isn't pending we know it has seen at least one broadcast with a payload + # If that payload was encrypted and the bindkey was not verified then we need to reauth + if ( + not data.pending + and data.encryption_scheme != EncryptionScheme.NONE + and not data.bindkey_verified + ): + entry.async_start_reauth(hass, data={"device": data}) + + return update + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Xiaomi BLE device from a config entry.""" address = entry.unique_id assert address is not None + + kwargs = {} + if bindkey := entry.data.get("bindkey"): + kwargs["bindkey"] = bytes.fromhex(bindkey) + data = XiaomiBluetoothDeviceData(**kwargs) + coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=lambda service_info: process_service_info( + hass, entry, data, service_info + ), ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index dcb95422609..d22ed46dd83 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -3,18 +3,9 @@ from __future__ import annotations from typing import Optional, Union -from xiaomi_ble import ( - DeviceClass, - DeviceKey, - SensorDeviceInfo, - SensorUpdate, - Units, - XiaomiBluetoothDeviceData, -) -from xiaomi_ble.parser import EncryptionScheme +from xiaomi_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units from homeassistant import config_entries -from homeassistant.components.bluetooth import BluetoothServiceInfoBleak from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, @@ -165,27 +156,6 @@ def sensor_update_to_bluetooth_data_update( ) -def process_service_info( - hass: HomeAssistant, - entry: config_entries.ConfigEntry, - data: XiaomiBluetoothDeviceData, - service_info: BluetoothServiceInfoBleak, -) -> PassiveBluetoothDataUpdate: - """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data.""" - update = data.update(service_info) - - # If device isn't pending we know it has seen at least one broadcast with a payload - # If that payload was encrypted and the bindkey was not verified then we need to reauth - if ( - not data.pending - and data.encryption_scheme != EncryptionScheme.NONE - and not data.bindkey_verified - ): - entry.async_start_reauth(hass, data={"device": data}) - - return sensor_update_to_bluetooth_data_update(update) - - async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry, @@ -195,13 +165,7 @@ async def async_setup_entry( coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ entry.entry_id ] - kwargs = {} - if bindkey := entry.data.get("bindkey"): - kwargs["bindkey"] = bytes.fromhex(bindkey) - data = XiaomiBluetoothDeviceData(**kwargs) - processor = PassiveBluetoothDataProcessor( - lambda service_info: process_service_info(hass, entry, data, service_info) - ) + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) entry.async_on_unload( processor.async_add_entities_listener( XiaomiBluetoothSensorEntity, async_add_entities diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 6a092746a68..5653b938ada 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -84,14 +84,25 @@ async def test_basic_usage(hass, mock_bleak_scanner_start): await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @callback - def _async_generate_mock_data( + def _mock_update_method( service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + + @callback + def _async_generate_mock_data( + data: dict[str, str], ) -> PassiveBluetoothDataUpdate: """Generate mock data.""" + assert data == {"test": "data"} return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, ) assert coordinator.available is False # no data yet saved_callback = None @@ -186,14 +197,24 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): await hass.async_block_till_done() @callback - def _async_generate_mock_data( + def _mock_update_method( service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + + @callback + def _async_generate_mock_data( + data: dict[str, str], ) -> PassiveBluetoothDataUpdate: """Generate mock data.""" return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, ) assert coordinator.available is False # no data yet saved_callback = None @@ -271,14 +292,24 @@ async def test_no_updates_once_stopping(hass, mock_bleak_scanner_start): await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @callback - def _async_generate_mock_data( + def _mock_update_method( service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + + @callback + def _async_generate_mock_data( + data: dict[str, str], ) -> PassiveBluetoothDataUpdate: """Generate mock data.""" return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, ) assert coordinator.available is False # no data yet saved_callback = None @@ -326,8 +357,14 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta run_count = 0 @callback - def _async_generate_mock_data( + def _mock_update_method( service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + + @callback + def _async_generate_mock_data( + data: dict[str, str], ) -> PassiveBluetoothDataUpdate: """Generate mock data.""" nonlocal run_count @@ -337,7 +374,11 @@ async def test_exception_from_update_method(hass, caplog, mock_bleak_scanner_sta return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, ) assert coordinator.available is False # no data yet saved_callback = None @@ -379,8 +420,14 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): run_count = 0 @callback - def _async_generate_mock_data( + def _mock_update_method( service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + + @callback + def _async_generate_mock_data( + data: dict[str, str], ) -> PassiveBluetoothDataUpdate: """Generate mock data.""" nonlocal run_count @@ -390,7 +437,11 @@ async def test_bad_data_from_update_method(hass, mock_bleak_scanner_start): return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, ) assert coordinator.available is False # no data yet saved_callback = None @@ -721,8 +772,14 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): update_count = 0 @callback - def _async_generate_mock_data( + def _mock_update_method( service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + + @callback + def _async_generate_mock_data( + data: dict[str, str], ) -> PassiveBluetoothDataUpdate: """Generate mock data.""" nonlocal update_count @@ -732,7 +789,11 @@ async def test_integration_with_entity(hass, mock_bleak_scanner_start): return GOVEE_B5178_REMOTE_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, ) assert coordinator.available is False # no data yet saved_callback = None @@ -835,14 +896,24 @@ async def test_integration_with_entity_without_a_device(hass, mock_bleak_scanner await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) @callback - def _async_generate_mock_data( + def _mock_update_method( service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + + @callback + def _async_generate_mock_data( + data: dict[str, str], ) -> PassiveBluetoothDataUpdate: """Generate mock data.""" return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, ) assert coordinator.available is False # no data yet saved_callback = None @@ -899,14 +970,24 @@ async def test_passive_bluetooth_entity_with_entity_platform( entity_platform = MockEntityPlatform(hass) @callback - def _async_generate_mock_data( + def _mock_update_method( service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + + @callback + def _async_generate_mock_data( + data: dict[str, str], ) -> PassiveBluetoothDataUpdate: """Generate mock data.""" return NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, ) assert coordinator.available is False # no data yet saved_callback = None @@ -992,8 +1073,18 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st """Test integration of PassiveBluetoothProcessorCoordinator with multiple platforms.""" await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + @callback + def _mock_update_method( + service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + return {"test": "data"} + coordinator = PassiveBluetoothProcessorCoordinator( - hass, _LOGGER, "aa:bb:cc:dd:ee:ff", BluetoothScanningMode.ACTIVE + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, ) assert coordinator.available is False # no data yet saved_callback = None @@ -1075,3 +1166,68 @@ async def test_integration_multiple_entity_platforms(hass, mock_bleak_scanner_st key="motion", device_id=None ) cancel_coordinator() + + +async def test_exception_from_coordinator_update_method( + hass, caplog, mock_bleak_scanner_start +): + """Test we handle exceptions from the update method.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + run_count = 0 + + @callback + def _mock_update_method( + service_info: BluetoothServiceInfo, + ) -> dict[str, str]: + nonlocal run_count + run_count += 1 + if run_count == 2: + raise Exception("Test exception") + return {"test": "data"} + + @callback + def _async_generate_mock_data( + data: dict[str, str], + ) -> PassiveBluetoothDataUpdate: + """Generate mock data.""" + return GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE + + coordinator = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + "aa:bb:cc:dd:ee:ff", + BluetoothScanningMode.ACTIVE, + _mock_update_method, + ) + assert coordinator.available is False # no data yet + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + processor = PassiveBluetoothDataProcessor(_async_generate_mock_data) + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + unregister_processor = coordinator.async_register_processor(processor) + cancel_coordinator = coordinator.async_start() + + processor.async_add_listener(MagicMock()) + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert processor.available is True + + # We should go unavailable once we get an exception + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert "Test exception" in caplog.text + assert processor.available is False + + # We should go available again once we get data again + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + assert processor.available is True + unregister_processor() + cancel_coordinator() From f90d007e73b52cd06b2a450b2f9a215b4b0b384d Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Tue, 9 Aug 2022 15:08:46 +0300 Subject: [PATCH 3228/3516] Add config flow to `android_ip_webcam` (#76222) Co-authored-by: Martin Hjelmare --- .coveragerc | 4 +- CODEOWNERS | 2 + .../components/android_ip_webcam/__init__.py | 394 ++++-------------- .../android_ip_webcam/binary_sensor.py | 61 +-- .../components/android_ip_webcam/camera.py | 60 ++- .../android_ip_webcam/config_flow.py | 84 ++++ .../components/android_ip_webcam/const.py | 41 ++ .../android_ip_webcam/coordinator.py | 42 ++ .../components/android_ip_webcam/entity.py | 32 ++ .../android_ip_webcam/manifest.json | 4 +- .../components/android_ip_webcam/sensor.py | 211 +++++++--- .../components/android_ip_webcam/strings.json | 26 ++ .../components/android_ip_webcam/switch.py | 214 +++++++--- .../android_ip_webcam/translations/en.json | 26 ++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + .../components/android_ip_webcam/__init__.py | 1 + .../components/android_ip_webcam/conftest.py | 25 ++ .../fixtures/sensor_data.json | 57 +++ .../fixtures/status_data.json | 62 +++ .../android_ip_webcam/test_config_flow.py | 122 ++++++ .../components/android_ip_webcam/test_init.py | 82 ++++ 22 files changed, 1076 insertions(+), 478 deletions(-) create mode 100644 homeassistant/components/android_ip_webcam/config_flow.py create mode 100644 homeassistant/components/android_ip_webcam/const.py create mode 100644 homeassistant/components/android_ip_webcam/coordinator.py create mode 100644 homeassistant/components/android_ip_webcam/entity.py create mode 100644 homeassistant/components/android_ip_webcam/strings.json create mode 100644 homeassistant/components/android_ip_webcam/translations/en.json create mode 100644 tests/components/android_ip_webcam/__init__.py create mode 100644 tests/components/android_ip_webcam/conftest.py create mode 100644 tests/components/android_ip_webcam/fixtures/sensor_data.json create mode 100644 tests/components/android_ip_webcam/fixtures/status_data.json create mode 100644 tests/components/android_ip_webcam/test_config_flow.py create mode 100644 tests/components/android_ip_webcam/test_init.py diff --git a/.coveragerc b/.coveragerc index 8a83b98873e..a94b2a8babc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -56,7 +56,9 @@ omit = homeassistant/components/ambient_station/sensor.py homeassistant/components/amcrest/* homeassistant/components/ampio/* - homeassistant/components/android_ip_webcam/* + homeassistant/components/android_ip_webcam/binary_sensor.py + homeassistant/components/android_ip_webcam/sensor.py + homeassistant/components/android_ip_webcam/switch.py homeassistant/components/androidtv/diagnostics.py homeassistant/components/anel_pwrctrl/switch.py homeassistant/components/anthemav/media_player.py diff --git a/CODEOWNERS b/CODEOWNERS index c92ad3b5ba9..59835aed315 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -72,6 +72,8 @@ build.json @home-assistant/supervisor /homeassistant/components/amcrest/ @flacjacket /homeassistant/components/analytics/ @home-assistant/core @ludeeus /tests/components/analytics/ @home-assistant/core @ludeeus +/homeassistant/components/android_ip_webcam/ @engrbm87 +/tests/components/android_ip_webcam/ @engrbm87 /homeassistant/components/androidtv/ @JeffLIrion @ollo69 /tests/components/androidtv/ @JeffLIrion @ollo69 /homeassistant/components/anthemav/ @hyralex diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py index cc24e6f4182..12885db6375 100644 --- a/homeassistant/components/android_ip_webcam/__init__.py +++ b/homeassistant/components/android_ip_webcam/__init__.py @@ -1,13 +1,12 @@ -"""Support for Android IP Webcam.""" +"""The Android IP Webcam integration.""" from __future__ import annotations -import asyncio -from datetime import timedelta - from pydroid_ipcam import PyDroidIPCam import voluptuous as vol -from homeassistant.components.mjpeg import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL +from homeassistant.components.repairs.issue_handler import async_create_issue +from homeassistant.components.repairs.models import IssueSeverity +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -20,168 +19,64 @@ from homeassistant.const import ( CONF_USERNAME, Platform, ) -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import discovery +from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.typing import ConfigType -from homeassistant.util.dt import utcnow -ATTR_AUD_CONNS = "Audio Connections" -ATTR_HOST = "host" -ATTR_VID_CONNS = "Video Connections" +from .const import ( + CONF_MOTION_SENSOR, + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_TIMEOUT, + DOMAIN, + SCAN_INTERVAL, + SENSORS, + SWITCHES, +) +from .coordinator import AndroidIPCamDataUpdateCoordinator -CONF_MOTION_SENSOR = "motion_sensor" - -DATA_IP_WEBCAM = "android_ip_webcam" -DEFAULT_NAME = "IP Webcam" -DEFAULT_PORT = 8080 -DEFAULT_TIMEOUT = 10 -DOMAIN = "android_ip_webcam" - -SCAN_INTERVAL = timedelta(seconds=10) -SIGNAL_UPDATE_DATA = "android_ip_webcam_update" - -KEY_MAP = { - "audio_connections": "Audio Connections", - "adet_limit": "Audio Trigger Limit", - "antibanding": "Anti-banding", - "audio_only": "Audio Only", - "battery_level": "Battery Level", - "battery_temp": "Battery Temperature", - "battery_voltage": "Battery Voltage", - "coloreffect": "Color Effect", - "exposure": "Exposure Level", - "exposure_lock": "Exposure Lock", - "ffc": "Front-facing Camera", - "flashmode": "Flash Mode", - "focus": "Focus", - "focus_homing": "Focus Homing", - "focus_region": "Focus Region", - "focusmode": "Focus Mode", - "gps_active": "GPS Active", - "idle": "Idle", - "ip_address": "IPv4 Address", - "ipv6_address": "IPv6 Address", - "ivideon_streaming": "Ivideon Streaming", - "light": "Light Level", - "mirror_flip": "Mirror Flip", - "motion": "Motion", - "motion_active": "Motion Active", - "motion_detect": "Motion Detection", - "motion_event": "Motion Event", - "motion_limit": "Motion Limit", - "night_vision": "Night Vision", - "night_vision_average": "Night Vision Average", - "night_vision_gain": "Night Vision Gain", - "orientation": "Orientation", - "overlay": "Overlay", - "photo_size": "Photo Size", - "pressure": "Pressure", - "proximity": "Proximity", - "quality": "Quality", - "scenemode": "Scene Mode", - "sound": "Sound", - "sound_event": "Sound Event", - "sound_timeout": "Sound Timeout", - "torch": "Torch", - "video_connections": "Video Connections", - "video_chunk_len": "Video Chunk Length", - "video_recording": "Video Recording", - "video_size": "Video Size", - "whitebalance": "White Balance", - "whitebalance_lock": "White Balance Lock", - "zoom": "Zoom", -} - -ICON_MAP = { - "audio_connections": "mdi:speaker", - "battery_level": "mdi:battery", - "battery_temp": "mdi:thermometer", - "battery_voltage": "mdi:battery-charging-100", - "exposure_lock": "mdi:camera", - "ffc": "mdi:camera-front-variant", - "focus": "mdi:image-filter-center-focus", - "gps_active": "mdi:crosshairs-gps", - "light": "mdi:flashlight", - "motion": "mdi:run", - "night_vision": "mdi:weather-night", - "overlay": "mdi:monitor", - "pressure": "mdi:gauge", - "proximity": "mdi:map-marker-radius", - "quality": "mdi:quality-high", - "sound": "mdi:speaker", - "sound_event": "mdi:speaker", - "sound_timeout": "mdi:speaker", - "torch": "mdi:white-balance-sunny", - "video_chunk_len": "mdi:video", - "video_connections": "mdi:eye", - "video_recording": "mdi:record-rec", - "whitebalance_lock": "mdi:white-balance-auto", -} - -SWITCHES = [ - "exposure_lock", - "ffc", - "focus", - "gps_active", - "motion_detect", - "night_vision", - "overlay", - "torch", - "whitebalance_lock", - "video_recording", +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.CAMERA, + Platform.SENSOR, + Platform.SWITCH, ] -SENSORS = [ - "audio_connections", - "battery_level", - "battery_temp", - "battery_voltage", - "light", - "motion", - "pressure", - "proximity", - "sound", - "video_connections", -] CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional( - CONF_TIMEOUT, default=DEFAULT_TIMEOUT - ): cv.positive_int, - vol.Optional( - CONF_SCAN_INTERVAL, default=SCAN_INTERVAL - ): cv.time_period, - vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, - vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, - vol.Optional(CONF_SWITCHES): vol.All( - cv.ensure_list, [vol.In(SWITCHES)] - ), - vol.Optional(CONF_SENSORS): vol.All( - cv.ensure_list, [vol.In(SENSORS)] - ), - vol.Optional(CONF_MOTION_SENSOR): cv.boolean, - } - ) - ], - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional( + CONF_TIMEOUT, default=DEFAULT_TIMEOUT + ): cv.positive_int, + vol.Optional( + CONF_SCAN_INTERVAL, default=SCAN_INTERVAL + ): cv.time_period, + vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, + vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, + vol.Optional(CONF_SWITCHES): vol.All( + cv.ensure_list, [vol.In(SWITCHES)] + ), + vol.Optional(CONF_SENSORS): vol.All( + cv.ensure_list, [vol.In(SENSORS)] + ), + vol.Optional(CONF_MOTION_SENSOR): cv.boolean, + } + ) + ], + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -189,165 +84,52 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the IP Webcam component.""" - webcams = hass.data[DATA_IP_WEBCAM] = {} - websession = async_get_clientsession(hass) - - async def async_setup_ipcamera(cam_config): - """Set up an IP camera.""" - host = cam_config[CONF_HOST] - username: str | None = cam_config.get(CONF_USERNAME) - password: str | None = cam_config.get(CONF_PASSWORD) - name: str = cam_config[CONF_NAME] - interval = cam_config[CONF_SCAN_INTERVAL] - switches = cam_config.get(CONF_SWITCHES) - sensors = cam_config.get(CONF_SENSORS) - motion = cam_config.get(CONF_MOTION_SENSOR) - - # Init ip webcam - cam = PyDroidIPCam( - websession, - host, - cam_config[CONF_PORT], - username=username, - password=password, - timeout=cam_config[CONF_TIMEOUT], - ssl=False, - ) - - if switches is None: - switches = [ - setting for setting in cam.enabled_settings if setting in SWITCHES - ] - - if sensors is None: - sensors = [sensor for sensor in cam.enabled_sensors if sensor in SENSORS] - sensors.extend(["audio_connections", "video_connections"]) - - if motion is None: - motion = "motion_active" in cam.enabled_sensors - - async def async_update_data(now): - """Update data from IP camera in SCAN_INTERVAL.""" - await cam.update() - async_dispatcher_send(hass, SIGNAL_UPDATE_DATA, host) - - async_track_point_in_utc_time(hass, async_update_data, utcnow() + interval) - - await async_update_data(None) - - # Load platforms - webcams[host] = cam - - mjpeg_camera = { - CONF_MJPEG_URL: cam.mjpeg_url, - CONF_STILL_IMAGE_URL: cam.image_url, - } - if username and password: - mjpeg_camera.update({CONF_USERNAME: username, CONF_PASSWORD: password}) - - # Remove incorrect config entry setup via mjpeg platform discovery. - mjpeg_config_entry = next( - ( - config_entry - for config_entry in hass.config_entries.async_entries("mjpeg") - if all( - config_entry.options.get(key) == val - for key, val in mjpeg_camera.items() - ) - ), - None, - ) - if mjpeg_config_entry: - await hass.config_entries.async_remove(mjpeg_config_entry.entry_id) - - mjpeg_camera[CONF_NAME] = name + if DOMAIN not in config: + return True + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.11.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + for entry in config[DOMAIN]: hass.async_create_task( - discovery.async_load_platform( - hass, Platform.CAMERA, DOMAIN, mjpeg_camera, config + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry ) ) - if sensors: - hass.async_create_task( - discovery.async_load_platform( - hass, - Platform.SENSOR, - DOMAIN, - {CONF_NAME: name, CONF_HOST: host, CONF_SENSORS: sensors}, - config, - ) - ) - - if switches: - hass.async_create_task( - discovery.async_load_platform( - hass, - Platform.SWITCH, - DOMAIN, - {CONF_NAME: name, CONF_HOST: host, CONF_SWITCHES: switches}, - config, - ) - ) - - if motion: - hass.async_create_task( - discovery.async_load_platform( - hass, - Platform.BINARY_SENSOR, - DOMAIN, - {CONF_HOST: host, CONF_NAME: name}, - config, - ) - ) - - tasks = [async_setup_ipcamera(conf) for conf in config[DOMAIN]] - if tasks: - await asyncio.wait(tasks) - return True -class AndroidIPCamEntity(Entity): - """The Android device running IP Webcam.""" +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Android IP Webcam from a config entry.""" + websession = async_get_clientsession(hass) + cam = PyDroidIPCam( + websession, + entry.data[CONF_HOST], + entry.data[CONF_PORT], + username=entry.data.get(CONF_USERNAME), + password=entry.data.get(CONF_PASSWORD), + ssl=False, + ) + coordinator = AndroidIPCamDataUpdateCoordinator(hass, entry, cam) + await coordinator.async_config_entry_first_refresh() - def __init__(self, host, ipcam): - """Initialize the data object.""" - self._host = host - self._ipcam = ipcam + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - async def async_added_to_hass(self): - """Register update dispatcher.""" + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - @callback - def async_ipcam_update(host): - """Update callback.""" - if self._host != host: - return - self.async_schedule_update_ha_state(True) + return True - self.async_on_remove( - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_DATA, async_ipcam_update) - ) - @property - def should_poll(self): - """Return True if entity has to be polled for state.""" - return False +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) - @property - def available(self): - """Return True if entity is available.""" - return self._ipcam.available - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - state_attr = {ATTR_HOST: self._host} - if self._ipcam.status_data is None: - return state_attr - - state_attr[ATTR_VID_CONNS] = self._ipcam.status_data.get("video_connections") - state_attr[ATTR_AUD_CONNS] = self._ipcam.status_data.get("audio_connections") - - return state_attr + return unload_ok diff --git a/homeassistant/components/android_ip_webcam/binary_sensor.py b/homeassistant/components/android_ip_webcam/binary_sensor.py index c5a2bb25cfe..a2dc25d825b 100644 --- a/homeassistant/components/android_ip_webcam/binary_sensor.py +++ b/homeassistant/components/android_ip_webcam/binary_sensor.py @@ -4,46 +4,57 @@ from __future__ import annotations from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, + BinarySensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import CONF_HOST, CONF_NAME, DATA_IP_WEBCAM, KEY_MAP, AndroidIPCamEntity +from .const import DOMAIN, MOTION_ACTIVE +from .coordinator import AndroidIPCamDataUpdateCoordinator +from .entity import AndroidIPCamBaseEntity + +BINARY_SENSOR_DESCRIPTION = BinarySensorEntityDescription( + key="motion_active", + name="Motion active", + device_class=BinarySensorDeviceClass.MOTION, +) -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the IP Webcam binary sensors.""" - if discovery_info is None: - return + """Set up the IP Webcam sensors from config entry.""" - host = discovery_info[CONF_HOST] - name = discovery_info[CONF_NAME] - ipcam = hass.data[DATA_IP_WEBCAM][host] + coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] - async_add_entities([IPWebcamBinarySensor(name, host, ipcam, "motion_active")], True) + async_add_entities([IPWebcamBinarySensor(coordinator)]) -class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorEntity): +class IPWebcamBinarySensor(AndroidIPCamBaseEntity, BinarySensorEntity): """Representation of an IP Webcam binary sensor.""" - _attr_device_class = BinarySensorDeviceClass.MOTION - - def __init__(self, name, host, ipcam, sensor): + def __init__( + self, + coordinator: AndroidIPCamDataUpdateCoordinator, + ) -> None: """Initialize the binary sensor.""" - super().__init__(host, ipcam) + self.entity_description = BINARY_SENSOR_DESCRIPTION + self._attr_unique_id = ( + f"{coordinator.config_entry.entry_id}-{BINARY_SENSOR_DESCRIPTION.key}" + ) + super().__init__(coordinator) - self._sensor = sensor - self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) - self._attr_name = f"{name} {self._mapped_name}" - self._attr_is_on = None + @property + def available(self) -> bool: + """Return avaibility if setting is enabled.""" + return MOTION_ACTIVE in self.cam.enabled_sensors and super().available - async def async_update(self): - """Retrieve latest state.""" - state, _ = self._ipcam.export_sensor(self._sensor) - self._attr_is_on = state == 1.0 + @property + def is_on(self) -> bool: + """Return if motion is detected.""" + return self.cam.export_sensor(MOTION_ACTIVE)[0] == 1.0 diff --git a/homeassistant/components/android_ip_webcam/camera.py b/homeassistant/components/android_ip_webcam/camera.py index de1223c7f5f..db6548411a9 100644 --- a/homeassistant/components/android_ip_webcam/camera.py +++ b/homeassistant/components/android_ip_webcam/camera.py @@ -2,43 +2,59 @@ from __future__ import annotations from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging -from homeassistant.const import HTTP_BASIC_AUTHENTICATION +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + HTTP_BASIC_AUTHENTICATION, +) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .const import DOMAIN +from .coordinator import AndroidIPCamDataUpdateCoordinator -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the IP Webcam camera.""" - if discovery_info is None: - return - + """Set up the IP Webcam camera from config entry.""" filter_urllib3_logging() - async_add_entities([IPWebcamCamera(**discovery_info)]) + coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + async_add_entities([IPWebcamCamera(coordinator)]) class IPWebcamCamera(MjpegCamera): """Representation of a IP Webcam camera.""" - def __init__( - self, - name: str, - mjpeg_url: str, - still_image_url: str, - username: str | None = None, - password: str = "", - ) -> None: + _attr_has_entity_name = True + + def __init__(self, coordinator: AndroidIPCamDataUpdateCoordinator) -> None: """Initialize the camera.""" + name = None + # keep imported name until YAML is removed + if CONF_NAME in coordinator.config_entry.data: + name = coordinator.config_entry.data[CONF_NAME] + self._attr_has_entity_name = False + super().__init__( name=name, - mjpeg_url=mjpeg_url, - still_image_url=still_image_url, + mjpeg_url=coordinator.cam.mjpeg_url, + still_image_url=coordinator.cam.image_url, authentication=HTTP_BASIC_AUTHENTICATION, - username=username, - password=password, + username=coordinator.config_entry.data.get(CONF_USERNAME), + password=coordinator.config_entry.data.get(CONF_PASSWORD, ""), + ) + self._attr_unique_id = f"{coordinator.config_entry.entry_id}-camera" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + name=name or coordinator.config_entry.data[CONF_HOST], ) diff --git a/homeassistant/components/android_ip_webcam/config_flow.py b/homeassistant/components/android_ip_webcam/config_flow.py new file mode 100644 index 00000000000..09f0fdaa3a2 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/config_flow.py @@ -0,0 +1,84 @@ +"""Config flow for Android IP Webcam integration.""" +from __future__ import annotations + +from typing import Any + +from pydroid_ipcam import PyDroidIPCam +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_TIMEOUT, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DEFAULT_PORT, DOMAIN + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Inclusive(CONF_USERNAME, "authentication"): str, + vol.Inclusive(CONF_PASSWORD, "authentication"): str, + } +) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool: + """Validate the user input allows us to connect.""" + + websession = async_get_clientsession(hass) + cam = PyDroidIPCam( + websession, + data[CONF_HOST], + data[CONF_PORT], + username=data.get(CONF_USERNAME), + password=data.get(CONF_PASSWORD), + ssl=False, + ) + await cam.update() + return cam.available + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Android IP Webcam.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + self._async_abort_entries_match( + {CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]} + ) + # to be removed when YAML import is removed + title = user_input.get(CONF_NAME) or user_input[CONF_HOST] + if await validate_input(self.hass, user_input): + return self.async_create_entry(title=title, data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_DATA_SCHEMA, + errors={"base": "cannot_connect"}, + ) + + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + """Import a config entry from configuration.yaml.""" + import_config.pop(CONF_SCAN_INTERVAL) + import_config.pop(CONF_TIMEOUT) + return await self.async_step_user(import_config) diff --git a/homeassistant/components/android_ip_webcam/const.py b/homeassistant/components/android_ip_webcam/const.py new file mode 100644 index 00000000000..6672628d977 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/const.py @@ -0,0 +1,41 @@ +"""Constants for the Android IP Webcam integration.""" + +from datetime import timedelta +from typing import Final + +DOMAIN: Final = "android_ip_webcam" +DEFAULT_NAME: Final = "IP Webcam" +DEFAULT_PORT: Final = 8080 +DEFAULT_TIMEOUT: Final = 10 + +CONF_MOTION_SENSOR: Final = "motion_sensor" + +MOTION_ACTIVE: Final = "motion_active" +SCAN_INTERVAL: Final = timedelta(seconds=10) + + +SWITCHES = [ + "exposure_lock", + "ffc", + "focus", + "gps_active", + "motion_detect", + "night_vision", + "overlay", + "torch", + "whitebalance_lock", + "video_recording", +] + +SENSORS = [ + "audio_connections", + "battery_level", + "battery_temp", + "battery_voltage", + "light", + "motion", + "pressure", + "proximity", + "sound", + "video_connections", +] diff --git a/homeassistant/components/android_ip_webcam/coordinator.py b/homeassistant/components/android_ip_webcam/coordinator.py new file mode 100644 index 00000000000..3940c6df7e4 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/coordinator.py @@ -0,0 +1,42 @@ +"""Coordinator object for the Android IP Webcam integration.""" + +from datetime import timedelta +import logging + +from pydroid_ipcam import PyDroidIPCam + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class AndroidIPCamDataUpdateCoordinator(DataUpdateCoordinator[None]): + """Coordinator class for the Android IP Webcam.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + cam: PyDroidIPCam, + ) -> None: + """Initialize the Android IP Webcam.""" + self.hass = hass + self.config_entry: ConfigEntry = config_entry + self.cam = cam + super().__init__( + self.hass, + _LOGGER, + name=f"{DOMAIN} {config_entry.data[CONF_HOST]}", + update_interval=timedelta(seconds=10), + ) + + async def _async_update_data(self) -> None: + """Update Android IP Webcam entities.""" + await self.cam.update() + if not self.cam.available: + raise UpdateFailed diff --git a/homeassistant/components/android_ip_webcam/entity.py b/homeassistant/components/android_ip_webcam/entity.py new file mode 100644 index 00000000000..025132e4bfb --- /dev/null +++ b/homeassistant/components/android_ip_webcam/entity.py @@ -0,0 +1,32 @@ +"""Base class for Android IP Webcam entities.""" + +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import AndroidIPCamDataUpdateCoordinator + + +class AndroidIPCamBaseEntity(CoordinatorEntity[AndroidIPCamDataUpdateCoordinator]): + """Base class for Android IP Webcam entities.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: AndroidIPCamDataUpdateCoordinator, + ) -> None: + """Initialize the base entity.""" + super().__init__(coordinator) + if CONF_NAME in coordinator.config_entry.data: + # name is legacy imported from YAML config + # this block can be removed when removing import from YAML + self._attr_name = f"{coordinator.config_entry.data[CONF_NAME]} {self.entity_description.name}" + self._attr_has_entity_name = False + self.cam = coordinator.cam + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + name=coordinator.config_entry.data.get(CONF_NAME) + or coordinator.config_entry.data[CONF_HOST], + ) diff --git a/homeassistant/components/android_ip_webcam/manifest.json b/homeassistant/components/android_ip_webcam/manifest.json index 39223e6636d..0023454728a 100644 --- a/homeassistant/components/android_ip_webcam/manifest.json +++ b/homeassistant/components/android_ip_webcam/manifest.json @@ -1,8 +1,10 @@ { "domain": "android_ip_webcam", "name": "Android IP Webcam", + "config_flow": true, + "dependencies": ["repairs"], "documentation": "https://www.home-assistant.io/integrations/android_ip_webcam", "requirements": ["pydroid-ipcam==1.3.1"], - "codeowners": [], + "codeowners": ["@engrbm87"], "iot_class": "local_polling" } diff --git a/homeassistant/components/android_ip_webcam/sensor.py b/homeassistant/components/android_ip_webcam/sensor.py index b3d43a51217..d699121d6c9 100644 --- a/homeassistant/components/android_ip_webcam/sensor.py +++ b/homeassistant/components/android_ip_webcam/sensor.py @@ -1,75 +1,172 @@ """Support for Android IP Webcam sensors.""" from __future__ import annotations -from homeassistant.components.sensor import SensorEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.icon import icon_for_battery_level -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from collections.abc import Callable +from dataclasses import dataclass -from . import ( - CONF_HOST, - CONF_NAME, - CONF_SENSORS, - DATA_IP_WEBCAM, - ICON_MAP, - KEY_MAP, - AndroidIPCamEntity, +from pydroid_ipcam import PyDroidIPCam + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .const import DOMAIN +from .coordinator import AndroidIPCamDataUpdateCoordinator +from .entity import AndroidIPCamBaseEntity + + +@dataclass +class AndroidIPWebcamSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[PyDroidIPCam], StateType] + + +@dataclass +class AndroidIPWebcamSensorEntityDescription( + SensorEntityDescription, AndroidIPWebcamSensorEntityDescriptionMixin +): + """Entity description class for Android IP Webcam sensors.""" + + unit_fn: Callable[[PyDroidIPCam], str | None] = lambda _: None + + +SENSOR_TYPES: tuple[AndroidIPWebcamSensorEntityDescription, ...] = ( + AndroidIPWebcamSensorEntityDescription( + key="audio_connections", + name="Audio connections", + icon="mdi:speaker", + state_class=SensorStateClass.TOTAL, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda ipcam: ipcam.status_data.get("audio_connections"), + ), + AndroidIPWebcamSensorEntityDescription( + key="battery_level", + name="Battery level", + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda ipcam: ipcam.export_sensor("battery_level")[0], + unit_fn=lambda ipcam: ipcam.export_sensor("battery_level")[1], + ), + AndroidIPWebcamSensorEntityDescription( + key="battery_temp", + name="Battery temperature", + icon="mdi:thermometer", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda ipcam: ipcam.export_sensor("battery_temp")[0], + unit_fn=lambda ipcam: ipcam.export_sensor("battery_temp")[1], + ), + AndroidIPWebcamSensorEntityDescription( + key="battery_voltage", + name="Battery voltage", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda ipcam: ipcam.export_sensor("battery_voltage")[0], + unit_fn=lambda ipcam: ipcam.export_sensor("battery_voltage")[1], + ), + AndroidIPWebcamSensorEntityDescription( + key="light", + name="Light level", + icon="mdi:flashlight", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda ipcam: ipcam.export_sensor("light")[0], + unit_fn=lambda ipcam: ipcam.export_sensor("light")[1], + ), + AndroidIPWebcamSensorEntityDescription( + key="motion", + name="Motion", + icon="mdi:run", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda ipcam: ipcam.export_sensor("motion")[0], + unit_fn=lambda ipcam: ipcam.export_sensor("motion")[1], + ), + AndroidIPWebcamSensorEntityDescription( + key="pressure", + name="Pressure", + icon="mdi:gauge", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda ipcam: ipcam.export_sensor("pressure")[0], + unit_fn=lambda ipcam: ipcam.export_sensor("pressure")[1], + ), + AndroidIPWebcamSensorEntityDescription( + key="proximity", + name="Proximity", + icon="mdi:map-marker-radius", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda ipcam: ipcam.export_sensor("proximity")[0], + unit_fn=lambda ipcam: ipcam.export_sensor("proximity")[1], + ), + AndroidIPWebcamSensorEntityDescription( + key="sound", + name="Sound", + icon="mdi:speaker", + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda ipcam: ipcam.export_sensor("sound")[0], + unit_fn=lambda ipcam: ipcam.export_sensor("sound")[1], + ), + AndroidIPWebcamSensorEntityDescription( + key="video_connections", + name="Video connections", + icon="mdi:eye", + state_class=SensorStateClass.TOTAL, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda ipcam: ipcam.status_data.get("video_connections"), + ), ) -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the IP Webcam Sensor.""" - if discovery_info is None: - return + """Set up the IP Webcam sensors from config entry.""" - host = discovery_info[CONF_HOST] - name = discovery_info[CONF_NAME] - sensors = discovery_info[CONF_SENSORS] - ipcam = hass.data[DATA_IP_WEBCAM][host] - - all_sensors = [] - - for sensor in sensors: - all_sensors.append(IPWebcamSensor(name, host, ipcam, sensor)) - - async_add_entities(all_sensors, True) + coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + sensor_types = [ + sensor + for sensor in SENSOR_TYPES + if sensor.key + in coordinator.cam.enabled_sensors + ["audio_connections", "video_connections"] + ] + async_add_entities( + IPWebcamSensor(coordinator, description) for description in sensor_types + ) -class IPWebcamSensor(AndroidIPCamEntity, SensorEntity): +class IPWebcamSensor(AndroidIPCamBaseEntity, SensorEntity): """Representation of a IP Webcam sensor.""" - def __init__(self, name, host, ipcam, sensor): + entity_description: AndroidIPWebcamSensorEntityDescription + + def __init__( + self, + coordinator: AndroidIPCamDataUpdateCoordinator, + description: AndroidIPWebcamSensorEntityDescription, + ) -> None: """Initialize the sensor.""" - super().__init__(host, ipcam) - - self._sensor = sensor - self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) - self._attr_name = f"{name} {self._mapped_name}" - self._attr_native_value = None - self._attr_native_unit_of_measurement = None - - async def async_update(self): - """Retrieve latest state.""" - if self._sensor in ("audio_connections", "video_connections"): - if not self._ipcam.status_data: - return - self._attr_native_value = self._ipcam.status_data.get(self._sensor) - self._attr_native_unit_of_measurement = "Connections" - else: - ( - self._attr_native_value, - self._attr_native_unit_of_measurement, - ) = self._ipcam.export_sensor(self._sensor) + self._attr_unique_id = f"{coordinator.config_entry.entry_id}-{description.key}" + self.entity_description = description + super().__init__(coordinator) @property - def icon(self): - """Return the icon for the sensor.""" - if self._sensor == "battery_level" and self._attr_native_value is not None: - return icon_for_battery_level(int(self._attr_native_value)) - return ICON_MAP.get(self._sensor, "mdi:eye") + def native_value(self) -> StateType: + """Return native value of sensor.""" + return self.entity_description.value_fn(self.cam) + + @property + def native_unit_of_measurement(self) -> str | None: + """Return native unit of measurement of sensor.""" + return self.entity_description.unit_fn(self.cam) diff --git a/homeassistant/components/android_ip_webcam/strings.json b/homeassistant/components/android_ip_webcam/strings.json new file mode 100644 index 00000000000..a9ade78a413 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "issues": { + "deprecated_yaml": { + "title": "The Android IP Webcam YAML configuration is being removed", + "description": "Configuring Android IP Webcam using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Android IP Webcam YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/android_ip_webcam/switch.py b/homeassistant/components/android_ip_webcam/switch.py index 0e49990236a..57f78b20e3d 100644 --- a/homeassistant/components/android_ip_webcam/switch.py +++ b/homeassistant/components/android_ip_webcam/switch.py @@ -1,88 +1,170 @@ """Support for Android IP Webcam settings.""" from __future__ import annotations -from homeassistant.components.switch import SwitchEntity -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from collections.abc import Callable +from dataclasses import dataclass -from . import ( - CONF_HOST, - CONF_NAME, - CONF_SWITCHES, - DATA_IP_WEBCAM, - ICON_MAP, - KEY_MAP, - AndroidIPCamEntity, +from pydroid_ipcam import PyDroidIPCam + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import AndroidIPCamDataUpdateCoordinator +from .entity import AndroidIPCamBaseEntity + + +@dataclass +class AndroidIPWebcamSwitchEntityDescriptionMixin: + """Mixin for required keys.""" + + on_func: Callable[[PyDroidIPCam], None] + off_func: Callable[[PyDroidIPCam], None] + + +@dataclass +class AndroidIPWebcamSwitchEntityDescription( + SwitchEntityDescription, AndroidIPWebcamSwitchEntityDescriptionMixin +): + """Entity description class for Android IP Webcam switches.""" + + +SWITCH_TYPES: tuple[AndroidIPWebcamSwitchEntityDescription, ...] = ( + AndroidIPWebcamSwitchEntityDescription( + key="exposure_lock", + name="Exposure lock", + icon="mdi:camera", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.change_setting("exposure_lock", True), + off_func=lambda ipcam: ipcam.change_setting("exposure_lock", False), + ), + AndroidIPWebcamSwitchEntityDescription( + key="ffc", + name="Front-facing camera", + icon="mdi:camera-front-variant", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.change_setting("ffc", True), + off_func=lambda ipcam: ipcam.change_setting("ffc", False), + ), + AndroidIPWebcamSwitchEntityDescription( + key="focus", + name="Focus", + icon="mdi:image-filter-center-focus", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.torch(activate=True), + off_func=lambda ipcam: ipcam.torch(activate=False), + ), + AndroidIPWebcamSwitchEntityDescription( + key="gps_active", + name="GPS active", + icon="mdi:crosshairs-gps", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.change_setting("gps_active", True), + off_func=lambda ipcam: ipcam.change_setting("gps_active", False), + ), + AndroidIPWebcamSwitchEntityDescription( + key="motion_detect", + name="Motion detection", + icon="mdi:flash", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.change_setting("motion_detect", True), + off_func=lambda ipcam: ipcam.change_setting("motion_detect", False), + ), + AndroidIPWebcamSwitchEntityDescription( + key="night_vision", + name="Night vision", + icon="mdi:weather-night", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.change_setting("night_vision", True), + off_func=lambda ipcam: ipcam.change_setting("night_vision", False), + ), + AndroidIPWebcamSwitchEntityDescription( + key="overlay", + name="Overlay", + icon="mdi:monitor", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.change_setting("overlay", True), + off_func=lambda ipcam: ipcam.change_setting("overlay", False), + ), + AndroidIPWebcamSwitchEntityDescription( + key="torch", + name="Torch", + icon="mdi:white-balance-sunny", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.torch(activate=True), + off_func=lambda ipcam: ipcam.torch(activate=False), + ), + AndroidIPWebcamSwitchEntityDescription( + key="whitebalance_lock", + name="White balance lock", + icon="mdi:white-balance-auto", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.change_setting("whitebalance_lock", True), + off_func=lambda ipcam: ipcam.change_setting("whitebalance_lock", False), + ), + AndroidIPWebcamSwitchEntityDescription( + key="video_recording", + name="Video recording", + icon="mdi:record-rec", + entity_category=EntityCategory.CONFIG, + on_func=lambda ipcam: ipcam.record(activate=True), + off_func=lambda ipcam: ipcam.record(activate=False), + ), ) -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the IP Webcam switch platform.""" - if discovery_info is None: - return + """Set up the IP Webcam switches from config entry.""" - host = discovery_info[CONF_HOST] - name = discovery_info[CONF_NAME] - switches = discovery_info[CONF_SWITCHES] - ipcam = hass.data[DATA_IP_WEBCAM][host] - - all_switches = [] - - for setting in switches: - all_switches.append(IPWebcamSettingsSwitch(name, host, ipcam, setting)) - - async_add_entities(all_switches, True) + coordinator: AndroidIPCamDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + switch_types = [ + switch + for switch in SWITCH_TYPES + if switch.key in coordinator.cam.enabled_settings + ] + async_add_entities( + [ + IPWebcamSettingSwitch(coordinator, description) + for description in switch_types + ] + ) -class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchEntity): - """An abstract class for an IP Webcam setting.""" +class IPWebcamSettingSwitch(AndroidIPCamBaseEntity, SwitchEntity): + """Representation of a IP Webcam setting.""" - def __init__(self, name, host, ipcam, setting): - """Initialize the settings switch.""" - super().__init__(host, ipcam) + entity_description: AndroidIPWebcamSwitchEntityDescription - self._setting = setting - self._mapped_name = KEY_MAP.get(self._setting, self._setting) - self._attr_name = f"{name} {self._mapped_name}" - self._attr_is_on = False + def __init__( + self, + coordinator: AndroidIPCamDataUpdateCoordinator, + description: AndroidIPWebcamSwitchEntityDescription, + ) -> None: + """Initialize the sensor.""" + self.entity_description = description + super().__init__(coordinator) + self._attr_unique_id = f"{coordinator.config_entry.entry_id}-{description.key}" - async def async_update(self): - """Get the updated status of the switch.""" - self._attr_is_on = bool(self._ipcam.current_settings.get(self._setting)) + @property + def is_on(self) -> bool: + """Return if settings is on or off.""" + return bool(self.cam.current_settings.get(self.entity_description.key)) async def async_turn_on(self, **kwargs): """Turn device on.""" - if self._setting == "torch": - await self._ipcam.torch(activate=True) - elif self._setting == "focus": - await self._ipcam.focus(activate=True) - elif self._setting == "video_recording": - await self._ipcam.record(record=True) - else: - await self._ipcam.change_setting(self._setting, True) - self._attr_is_on = True - self.async_write_ha_state() + await self.entity_description.on_func(self.cam) + await self.coordinator.async_request_refresh() async def async_turn_off(self, **kwargs): """Turn device off.""" - if self._setting == "torch": - await self._ipcam.torch(activate=False) - elif self._setting == "focus": - await self._ipcam.focus(activate=False) - elif self._setting == "video_recording": - await self._ipcam.record(record=False) - else: - await self._ipcam.change_setting(self._setting, False) - self._attr_is_on = False - self.async_write_ha_state() - - @property - def icon(self): - """Return the icon for the switch.""" - return ICON_MAP.get(self._setting, "mdi:flash") + await self.entity_description.off_func(self.cam) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/android_ip_webcam/translations/en.json b/homeassistant/components/android_ip_webcam/translations/en.json new file mode 100644 index 00000000000..43cd63356b4 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "username": "Username" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Android IP Webcam using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Android IP Webcam YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Android IP Webcamepush YAML configuration is being removed" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index d9da1ff2057..a582f2b719c 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -28,6 +28,7 @@ FLOWS = { "amberelectric", "ambiclimate", "ambient_station", + "android_ip_webcam", "androidtv", "anthemav", "apple_tv", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7f612cf9526..c1c224f4812 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1018,6 +1018,9 @@ pydeconz==103 # homeassistant.components.dexcom pydexcom==0.2.3 +# homeassistant.components.android_ip_webcam +pydroid-ipcam==1.3.1 + # homeassistant.components.econet pyeconet==0.1.15 diff --git a/tests/components/android_ip_webcam/__init__.py b/tests/components/android_ip_webcam/__init__.py new file mode 100644 index 00000000000..331547929ea --- /dev/null +++ b/tests/components/android_ip_webcam/__init__.py @@ -0,0 +1 @@ +"""Tests for the Android IP Webcam integration.""" diff --git a/tests/components/android_ip_webcam/conftest.py b/tests/components/android_ip_webcam/conftest.py new file mode 100644 index 00000000000..83a040222f8 --- /dev/null +++ b/tests/components/android_ip_webcam/conftest.py @@ -0,0 +1,25 @@ +"""Fixtures for tests.""" +from http import HTTPStatus + +import pytest + +from homeassistant.const import CONTENT_TYPE_JSON + +from tests.common import load_fixture + + +@pytest.fixture +def aioclient_mock_fixture(aioclient_mock) -> None: + """Fixture to provide a aioclient mocker.""" + aioclient_mock.get( + "http://1.1.1.1:8080/status.json?show_avail=1", + text=load_fixture("android_ip_webcam/status_data.json"), + status=HTTPStatus.OK, + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + aioclient_mock.get( + "http://1.1.1.1:8080/sensors.json", + text=load_fixture("android_ip_webcam/sensor_data.json"), + status=HTTPStatus.OK, + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) diff --git a/tests/components/android_ip_webcam/fixtures/sensor_data.json b/tests/components/android_ip_webcam/fixtures/sensor_data.json new file mode 100644 index 00000000000..341e8e13931 --- /dev/null +++ b/tests/components/android_ip_webcam/fixtures/sensor_data.json @@ -0,0 +1,57 @@ +{ + "light": { + "unit": "lx", + "data": [ + [1659602753873, [708.0]], + [1659602773708, [991.0]] + ] + }, + "proximity": { "unit": "cm", "data": [[1659602773709, [5.0]]] }, + "motion": { + "unit": "", + "data": [ + [1659602752922, [8.0]], + [1659602773709, [12.0]] + ] + }, + "motion_active": { "unit": "", "data": [[1659602773710, [0.0]]] }, + "battery_voltage": { + "unit": "V", + "data": [ + [1659602769351, [4.077]], + [1659602771352, [4.077]], + [1659602773353, [4.077]] + ] + }, + "battery_level": { + "unit": "%", + "data": [ + [1659602769351, [87.0]], + [1659602771352, [87.0]], + [1659602773353, [87.0]] + ] + }, + "battery_temp": { + "unit": "℃", + "data": [ + [1659602769351, [33.9]], + [1659602771352, [33.9]], + [1659602773353, [33.9]] + ] + }, + "sound": { + "unit": "dB", + "data": [ + [1659602768710, [181.0]], + [1659602769200, [191.0]], + [1659602769690, [186.0]], + [1659602770181, [186.0]], + [1659602770670, [188.0]], + [1659602771145, [192.0]], + [1659602771635, [192.0]], + [1659602772125, [179.0]], + [1659602772615, [186.0]], + [1659602773261, [178.0]] + ] + } +} diff --git a/tests/components/android_ip_webcam/fixtures/status_data.json b/tests/components/android_ip_webcam/fixtures/status_data.json new file mode 100644 index 00000000000..7d30b67c4ca --- /dev/null +++ b/tests/components/android_ip_webcam/fixtures/status_data.json @@ -0,0 +1,62 @@ +{ + "video_connections": 0, + "audio_connections": 0, + "video_status": { "result": "status", "enabled": "True", "mode": "none" }, + "curvals": { + "orientation": "landscape", + "idle": "off", + "audio_only": "off", + "overlay": "off", + "quality": "49", + "focus_homing": "off", + "ip_address": "192.168.3.88", + "motion_limit": "250", + "adet_limit": "200", + "night_vision": "off", + "night_vision_average": "2", + "night_vision_gain": "1.0", + " ": "off", + "motion_detect": "on", + "motion_display": "off", + "video_chunk_len": "60", + "gps_active": "off", + "video_size": "1920x1080", + "mirror_flip": "none", + "ffc": "off", + "rtsp_video_formats": "", + "rtsp_audio_formats": "", + "video_connections": "0", + "audio_connections": "0", + "ivideon_streaming": "off", + "zoom": "100", + "crop_x": "50", + "crop_y": "50", + "coloreffect": "none", + "scenemode": "auto", + "focusmode": "continuous-video", + "whitebalance": "auto", + "flashmode": "off", + "antibanding": "off", + "torch": "off", + "focus_distance": "0.0", + "focal_length": "4.25", + "aperture": "1.7", + "filter_density": "0.0", + "exposure_ns": "9384", + "frame_duration": "33333333", + "iso": "100", + "manual_sensor": "off", + "photo_size": "1920x1080", + "photo_rotation": "-1" + }, + "idle": ["on", "off"], + "audio_only": ["on", "off"], + "overlay": ["on", "off"], + "focus_homing": ["on", "off"], + "night_vision": ["on", "off"], + "motion_detect": ["on", "off"], + "motion_display": ["on", "off"], + "gps_active": ["on", "off"], + "ffc": ["on", "off"], + "torch": ["on", "off"] +} diff --git a/tests/components/android_ip_webcam/test_config_flow.py b/tests/components/android_ip_webcam/test_config_flow.py new file mode 100644 index 00000000000..1ede523ecd2 --- /dev/null +++ b/tests/components/android_ip_webcam/test_config_flow.py @@ -0,0 +1,122 @@ +"""Test the Android IP Webcam config flow.""" +from datetime import timedelta +from unittest.mock import patch + +import aiohttp + +from homeassistant import config_entries +from homeassistant.components.android_ip_webcam.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .test_init import MOCK_CONFIG_DATA + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_form(hass: HomeAssistant, aioclient_mock_fixture) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.android_ip_webcam.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 8080, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "1.1.1.1" + assert result2["data"] == { + "host": "1.1.1.1", + "port": 8080, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_flow_success(hass: HomeAssistant, aioclient_mock_fixture) -> None: + """Test a successful import of yaml.""" + with patch( + "homeassistant.components.android_ip_webcam.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "name": "IP Webcam", + "host": "1.1.1.1", + "port": 8080, + "timeout": 10, + "scan_interval": timedelta(seconds=30), + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "IP Webcam" + assert result2["data"] == { + "name": "IP Webcam", + "host": "1.1.1.1", + "port": 8080, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_device_already_configured( + hass: HomeAssistant, aioclient_mock_fixture +) -> None: + """Test aborting if the device is already configured.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "port": 8080, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_form_cannot_connect( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + aioclient_mock.get( + "http://1.1.1.1:8080/status.json?show_avail=1", + exc=aiohttp.ClientError, + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/android_ip_webcam/test_init.py b/tests/components/android_ip_webcam/test_init.py new file mode 100644 index 00000000000..e0c21445d71 --- /dev/null +++ b/tests/components/android_ip_webcam/test_init.py @@ -0,0 +1,82 @@ +"""Tests for the Android IP Webcam integration.""" + + +from collections.abc import Awaitable +from typing import Callable + +import aiohttp + +from homeassistant.components.android_ip_webcam.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry +from tests.components.repairs import get_repairs +from tests.test_util.aiohttp import AiohttpClientMocker + +MOCK_CONFIG_DATA = { + "name": "IP Webcam", + "host": "1.1.1.1", + "port": 8080, +} + + +async def test_setup( + hass: HomeAssistant, + aioclient_mock_fixture, + hass_ws_client: Callable[ + [HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse] + ], +) -> None: + """Test integration failed due to an error.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: [MOCK_CONFIG_DATA]}) + assert hass.config_entries.async_entries(DOMAIN) + issues = await get_repairs(hass, hass_ws_client) + assert len(issues) == 1 + assert issues[0]["issue_id"] == "deprecated_yaml" + + +async def test_successful_config_entry( + hass: HomeAssistant, aioclient_mock_fixture +) -> None: + """Test settings up integration from config entry.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state == ConfigEntryState.LOADED + + +async def test_setup_failed( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test integration failed due to an error.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) + entry.add_to_hass(hass) + aioclient_mock.get( + "http://1.1.1.1:8080/status.json?show_avail=1", + exc=aiohttp.ClientError, + ) + + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_unload_entry(hass: HomeAssistant, aioclient_mock_fixture) -> None: + """Test removing integration.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + assert entry.entry_id not in hass.data[DOMAIN] From 46a8f191970cc40cb144038c184a07a16e7fa899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Tue, 9 Aug 2022 15:24:53 +0200 Subject: [PATCH 3229/3516] Update aioqsw to v0.2.0 (#76509) --- .../components/qnap_qsw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/qnap_qsw/test_coordinator.py | 12 + tests/components/qnap_qsw/util.py | 335 ++++++++++++++++++ 5 files changed, 350 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qnap_qsw/manifest.json b/homeassistant/components/qnap_qsw/manifest.json index 83c9423f0f4..690297d69bc 100644 --- a/homeassistant/components/qnap_qsw/manifest.json +++ b/homeassistant/components/qnap_qsw/manifest.json @@ -3,7 +3,7 @@ "name": "QNAP QSW", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/qnap_qsw", - "requirements": ["aioqsw==0.1.1"], + "requirements": ["aioqsw==0.2.0"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioqsw"], diff --git a/requirements_all.txt b/requirements_all.txt index aed97e37a0e..f47af097bcc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -232,7 +232,7 @@ aiopvpc==3.0.0 aiopyarr==22.7.0 # homeassistant.components.qnap_qsw -aioqsw==0.1.1 +aioqsw==0.2.0 # homeassistant.components.recollect_waste aiorecollect==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c1c224f4812..56019b553a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -207,7 +207,7 @@ aiopvpc==3.0.0 aiopyarr==22.7.0 # homeassistant.components.qnap_qsw -aioqsw==0.1.1 +aioqsw==0.2.0 # homeassistant.components.recollect_waste aiorecollect==1.0.8 diff --git a/tests/components/qnap_qsw/test_coordinator.py b/tests/components/qnap_qsw/test_coordinator.py index 107cfa580b7..125b333c8d6 100644 --- a/tests/components/qnap_qsw/test_coordinator.py +++ b/tests/components/qnap_qsw/test_coordinator.py @@ -18,6 +18,8 @@ from .util import ( FIRMWARE_CONDITION_MOCK, FIRMWARE_INFO_MOCK, FIRMWARE_UPDATE_CHECK_MOCK, + PORTS_STATISTICS_MOCK, + PORTS_STATUS_MOCK, SYSTEM_BOARD_MOCK, SYSTEM_SENSOR_MOCK, SYSTEM_TIME_MOCK, @@ -44,6 +46,12 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_update_check", return_value=FIRMWARE_UPDATE_CHECK_MOCK, ) as mock_firmware_update_check, patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_statistics", + return_value=PORTS_STATISTICS_MOCK, + ) as mock_ports_statistics, patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_status", + return_value=PORTS_STATUS_MOCK, + ) as mock_ports_status, patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_system_board", return_value=SYSTEM_BOARD_MOCK, ) as mock_system_board, patch( @@ -65,6 +73,8 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: mock_firmware_condition.assert_called_once() mock_firmware_info.assert_called_once() mock_firmware_update_check.assert_called_once() + mock_ports_statistics.assert_called_once() + mock_ports_status.assert_called_once() mock_system_board.assert_called_once() mock_system_sensor.assert_called_once() mock_system_time.assert_called_once() @@ -74,6 +84,8 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: mock_firmware_condition.reset_mock() mock_firmware_info.reset_mock() mock_firmware_update_check.reset_mock() + mock_ports_statistics.reset_mock() + mock_ports_status.reset_mock() mock_system_board.reset_mock() mock_system_sensor.reset_mock() mock_system_time.reset_mock() diff --git a/tests/components/qnap_qsw/util.py b/tests/components/qnap_qsw/util.py index a057dfbe3ac..d3a62d413fa 100644 --- a/tests/components/qnap_qsw/util.py +++ b/tests/components/qnap_qsw/util.py @@ -18,6 +18,10 @@ from aioqsw.const import ( API_ERROR_MESSAGE, API_FAN1_SPEED, API_FAN2_SPEED, + API_FCS_ERRORS, + API_FULL_DUPLEX, + API_KEY, + API_LINK, API_MAC_ADDR, API_MAX_SWITCH_TEMP, API_MESSAGE, @@ -28,10 +32,15 @@ from aioqsw.const import ( API_PRODUCT, API_PUB_DATE, API_RESULT, + API_RX_ERRORS, + API_RX_OCTETS, API_SERIAL, + API_SPEED, API_SWITCH_TEMP, API_TRUNK_NUM, + API_TX_OCTETS, API_UPTIME, + API_VAL, API_VERSION, ) @@ -111,6 +120,326 @@ FIRMWARE_UPDATE_CHECK_MOCK = { }, } +PORTS_STATISTICS_MOCK = { + API_ERROR_CODE: 200, + API_ERROR_MESSAGE: "OK", + API_RESULT: [ + { + API_KEY: "1", + API_VAL: { + API_RX_OCTETS: 20000, + API_RX_ERRORS: 20, + API_TX_OCTETS: 10000, + API_FCS_ERRORS: 10, + }, + }, + { + API_KEY: "2", + API_VAL: { + API_RX_OCTETS: 2000, + API_RX_ERRORS: 2, + API_TX_OCTETS: 1000, + API_FCS_ERRORS: 1, + }, + }, + { + API_KEY: "3", + API_VAL: { + API_RX_OCTETS: 200, + API_RX_ERRORS: 0, + API_TX_OCTETS: 100, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "4", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "5", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "6", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "7", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "8", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "9", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "10", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "11", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "12", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "29", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "30", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "31", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "32", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "33", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + { + API_KEY: "34", + API_VAL: { + API_RX_OCTETS: 0, + API_RX_ERRORS: 0, + API_TX_OCTETS: 0, + API_FCS_ERRORS: 0, + }, + }, + ], +} + +PORTS_STATUS_MOCK = { + API_ERROR_CODE: 200, + API_ERROR_MESSAGE: "OK", + API_RESULT: [ + { + API_KEY: "1", + API_VAL: { + API_LINK: True, + API_FULL_DUPLEX: True, + API_SPEED: "10000", + }, + }, + { + API_KEY: "2", + API_VAL: { + API_LINK: True, + API_FULL_DUPLEX: True, + API_SPEED: "1000", + }, + }, + { + API_KEY: "3", + API_VAL: { + API_LINK: True, + API_FULL_DUPLEX: False, + API_SPEED: "100", + }, + }, + { + API_KEY: "4", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "1000", + }, + }, + { + API_KEY: "5", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "1000", + }, + }, + { + API_KEY: "6", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "1000", + }, + }, + { + API_KEY: "7", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "1000", + }, + }, + { + API_KEY: "8", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "1000", + }, + }, + { + API_KEY: "9", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "1000", + }, + }, + { + API_KEY: "10", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "1000", + }, + }, + { + API_KEY: "11", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "1000", + }, + }, + { + API_KEY: "12", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "1000", + }, + }, + { + API_KEY: "29", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "0", + }, + }, + { + API_KEY: "30", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "0", + }, + }, + { + API_KEY: "31", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "0", + }, + }, + { + API_KEY: "32", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "0", + }, + }, + { + API_KEY: "33", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "0", + }, + }, + { + API_KEY: "34", + API_VAL: { + API_LINK: False, + API_FULL_DUPLEX: False, + API_SPEED: "0", + }, + }, + ], +} + SYSTEM_COMMAND_MOCK = { API_ERROR_CODE: 200, API_ERROR_MESSAGE: "OK", @@ -170,6 +499,12 @@ async def async_init_integration( ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_update_check", return_value=FIRMWARE_UPDATE_CHECK_MOCK, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_statistics", + return_value=PORTS_STATISTICS_MOCK, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_status", + return_value=PORTS_STATUS_MOCK, ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_system_board", return_value=SYSTEM_BOARD_MOCK, From a0ceb38f5f38b2bbe471e15e2ffcf8f9270da09d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Aug 2022 03:35:08 -1000 Subject: [PATCH 3230/3516] Bump govee-ble to 0.14.0 to fix H5052 sensors (#76497) --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index eb33df867e1..2ba97b95d08 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -28,7 +28,7 @@ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.12.7"], + "requirements": ["govee-ble==0.14.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index f47af097bcc..cde7071920d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -760,7 +760,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.7 +govee-ble==0.14.0 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 56019b553a3..e5cef1cf7f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -561,7 +561,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.12.7 +govee-ble==0.14.0 # homeassistant.components.gree greeclimate==1.3.0 From 4e3db5bb5ce8cb5e1e814a06c13907b84942e187 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 9 Aug 2022 15:35:22 +0200 Subject: [PATCH 3231/3516] Update sqlalchemy to 1.4.40 (#76505) --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index f0c1f81689a..9486d2eaf1e 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.38", "fnvhash==0.1.0"], + "requirements": ["sqlalchemy==1.4.40", "fnvhash==0.1.0"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 0e6c68071cc..3e3faaa7372 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,7 +2,7 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.4.38"], + "requirements": ["sqlalchemy==1.4.40"], "codeowners": ["@dgomes", "@gjohansson-ST"], "config_flow": true, "iot_class": "local_polling" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f15daf093a3..b7c2fd8cbb5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,7 +33,7 @@ pyudev==0.23.2 pyyaml==6.0 requests==2.28.1 scapy==2.4.5 -sqlalchemy==1.4.38 +sqlalchemy==1.4.40 typing-extensions>=3.10.0.2,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 diff --git a/requirements_all.txt b/requirements_all.txt index cde7071920d..93120e76d30 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2243,7 +2243,7 @@ spotipy==2.20.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.38 +sqlalchemy==1.4.40 # homeassistant.components.srp_energy srpenergy==1.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e5cef1cf7f8..c15cb4fc568 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1522,7 +1522,7 @@ spotipy==2.20.0 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.38 +sqlalchemy==1.4.40 # homeassistant.components.srp_energy srpenergy==1.3.6 From 2b2ea3dd73bfeb49bf50cf5cd16645221606e057 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 9 Aug 2022 15:35:38 +0200 Subject: [PATCH 3232/3516] Update flake8-noqa to 1.2.8 (#76506) --- .pre-commit-config.yaml | 2 +- requirements_test_pre_commit.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4867f712b8e..cb9a5ed04dc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: - flake8-docstrings==1.6.0 - pydocstyle==6.1.1 - flake8-comprehensions==3.10.0 - - flake8-noqa==1.2.5 + - flake8-noqa==1.2.8 - mccabe==0.6.1 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index a6cb45b22b2..115e826537a 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -5,7 +5,7 @@ black==22.6.0 codespell==2.1.0 flake8-comprehensions==3.10.0 flake8-docstrings==1.6.0 -flake8-noqa==1.2.5 +flake8-noqa==1.2.8 flake8==4.0.1 isort==5.10.1 mccabe==0.6.1 From 891158f332cccca2942f55037df983bb22641d65 Mon Sep 17 00:00:00 2001 From: uvjustin <46082645+uvjustin@users.noreply.github.com> Date: Tue, 9 Aug 2022 22:41:19 +0800 Subject: [PATCH 3233/3516] Use stream to generate fallback image for onvif (#75584) --- homeassistant/components/onvif/camera.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 5aa49f68aa6..528b9605bbc 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -136,13 +136,16 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a still image response from the camera.""" - image = None + + if self.stream and self.stream.keepalive: + return await self.stream.async_get_image(width, height) if self.device.capabilities.snapshot: try: image = await self.device.device.get_snapshot( self.profile.token, self._basic_auth ) + return image except ONVIFError as err: LOGGER.error( "Fetch snapshot image failed from %s, falling back to FFmpeg; %s", @@ -150,17 +153,14 @@ class ONVIFCameraEntity(ONVIFBaseEntity, Camera): err, ) - if image is None: - assert self._stream_uri - return await ffmpeg.async_get_image( - self.hass, - self._stream_uri, - extra_cmd=self.device.config_entry.options.get(CONF_EXTRA_ARGUMENTS), - width=width, - height=height, - ) - - return image + assert self._stream_uri + return await ffmpeg.async_get_image( + self.hass, + self._stream_uri, + extra_cmd=self.device.config_entry.options.get(CONF_EXTRA_ARGUMENTS), + width=width, + height=height, + ) async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" From 6bae03c14b17b929d43fc682fdf7856e7da14432 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Aug 2022 04:49:17 -1000 Subject: [PATCH 3234/3516] Fix inkbird ibbq2s that identify with xbbq (#76492) --- homeassistant/components/inkbird/manifest.json | 3 ++- homeassistant/generated/bluetooth.py | 4 ++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index 686c9bada2d..b0ef08143c2 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -7,9 +7,10 @@ { "local_name": "sps" }, { "local_name": "Inkbird*" }, { "local_name": "iBBQ*" }, + { "local_name": "xBBQ*" }, { "local_name": "tps" } ], - "requirements": ["inkbird-ble==0.5.1"], + "requirements": ["inkbird-ble==0.5.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index d704d00ab8a..15a822599c6 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -75,6 +75,10 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "domain": "inkbird", "local_name": "iBBQ*" }, + { + "domain": "inkbird", + "local_name": "xBBQ*" + }, { "domain": "inkbird", "local_name": "tps" diff --git a/requirements_all.txt b/requirements_all.txt index 93120e76d30..5eca3255533 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -902,7 +902,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.1 +inkbird-ble==0.5.2 # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c15cb4fc568..4d9e3611c4d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -655,7 +655,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.1 +inkbird-ble==0.5.2 # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 From 929eeac1e4cfc4d6d7d7d9ff6a13a63212f9660e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Aug 2022 04:53:21 -1000 Subject: [PATCH 3235/3516] Add support for Govee 5184 BBQ sensors (#76490) --- homeassistant/components/govee_ble/manifest.json | 4 ++++ homeassistant/generated/bluetooth.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 2ba97b95d08..e8df2af3abb 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -7,6 +7,10 @@ { "local_name": "Govee*" }, { "local_name": "GVH5*" }, { "local_name": "B5178*" }, + { + "manufacturer_id": 6966, + "service_uuid": "00008451-0000-1000-8000-00805f9b34fb" + }, { "manufacturer_id": 26589, "service_uuid": "00008351-0000-1000-8000-00805f9b34fb" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 15a822599c6..d7af6e6ee11 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -31,6 +31,11 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "domain": "govee_ble", "local_name": "B5178*" }, + { + "domain": "govee_ble", + "manufacturer_id": 6966, + "service_uuid": "00008451-0000-1000-8000-00805f9b34fb" + }, { "domain": "govee_ble", "manufacturer_id": 26589, From b19ace912469b076a2527f25cefab404edfcc6e9 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Tue, 9 Aug 2022 16:54:07 +0200 Subject: [PATCH 3236/3516] Use constructor instead of factory method for sensors in here_travel_time (#76471) --- .../components/here_travel_time/sensor.py | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index c4be60d5569..e7bc88f2210 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -153,38 +153,6 @@ def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...] ) -def create_origin_sensor( - config_entry: ConfigEntry, hass: HomeAssistant -) -> OriginSensor: - """Create a origin sensor.""" - return OriginSensor( - config_entry.entry_id, - config_entry.data[CONF_NAME], - SensorEntityDescription( - name="Origin", - icon="mdi:store-marker", - key=ATTR_ORIGIN_NAME, - ), - hass.data[DOMAIN][config_entry.entry_id], - ) - - -def create_destination_sensor( - config_entry: ConfigEntry, hass: HomeAssistant -) -> DestinationSensor: - """Create a destination sensor.""" - return DestinationSensor( - config_entry.entry_id, - config_entry.data[CONF_NAME], - SensorEntityDescription( - name="Destination", - icon="mdi:store-marker", - key=ATTR_DESTINATION_NAME, - ), - hass.data[DOMAIN][config_entry.entry_id], - ) - - async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -214,18 +182,22 @@ async def async_setup_entry( ) -> None: """Add HERE travel time entities from a config_entry.""" + entry_id = config_entry.entry_id + name = config_entry.data[CONF_NAME] + coordinator = hass.data[DOMAIN][entry_id] + sensors: list[HERETravelTimeSensor] = [] for sensor_description in sensor_descriptions(config_entry.data[CONF_MODE]): sensors.append( HERETravelTimeSensor( - config_entry.entry_id, - config_entry.data[CONF_NAME], + entry_id, + name, sensor_description, - hass.data[DOMAIN][config_entry.entry_id], + coordinator, ) ) - sensors.append(create_origin_sensor(config_entry, hass)) - sensors.append(create_destination_sensor(config_entry, hass)) + sensors.append(OriginSensor(entry_id, name, coordinator)) + sensors.append(DestinationSensor(entry_id, name, coordinator)) async_add_entities(sensors) @@ -278,6 +250,20 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity): class OriginSensor(HERETravelTimeSensor): """Sensor holding information about the route origin.""" + def __init__( + self, + unique_id_prefix: str, + name: str, + coordinator: HereTravelTimeDataUpdateCoordinator, + ) -> None: + """Initialize the sensor.""" + sensor_description = SensorEntityDescription( + name="Origin", + icon="mdi:store-marker", + key=ATTR_ORIGIN_NAME, + ) + super().__init__(unique_id_prefix, name, sensor_description, coordinator) + @property def extra_state_attributes(self) -> Mapping[str, Any] | None: """GPS coordinates.""" @@ -292,6 +278,20 @@ class OriginSensor(HERETravelTimeSensor): class DestinationSensor(HERETravelTimeSensor): """Sensor holding information about the route destination.""" + def __init__( + self, + unique_id_prefix: str, + name: str, + coordinator: HereTravelTimeDataUpdateCoordinator, + ) -> None: + """Initialize the sensor.""" + sensor_description = SensorEntityDescription( + name="Destination", + icon="mdi:store-marker", + key=ATTR_DESTINATION_NAME, + ) + super().__init__(unique_id_prefix, name, sensor_description, coordinator) + @property def extra_state_attributes(self) -> Mapping[str, Any] | None: """GPS coordinates.""" From bcdf880364faa93cdd94c10f08b84f8887cc87a3 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 9 Aug 2022 16:55:52 +0200 Subject: [PATCH 3237/3516] Add siren checks to pylint plugin (#76460) --- pylint/plugins/hass_enforce_type_hints.py | 52 +++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 527c9358971..f7a109507d8 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1477,6 +1477,58 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "select": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="SelectEntity", + matches=[ + TypeHintMatch( + function_name="capability_attributes", + return_type="dict[str, Any]", + ), + TypeHintMatch( + function_name="options", + return_type="list[str]", + ), + TypeHintMatch( + function_name="current_option", + return_type=["str", None], + ), + TypeHintMatch( + function_name="select_option", + return_type=None, + ), + TypeHintMatch( + function_name="select_option", + arg_types={1: "str"}, + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ], + "siren": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="ToggleEntity", + matches=_TOGGLE_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="SirenEntity", + matches=[ + TypeHintMatch( + function_name="available_tones", + return_type=["dict[int, str]", "list[int | str]", None], + ), + ], + ), + ], } From bd795be0e985d0a6e907a61ea6f920cda42c5010 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 9 Aug 2022 16:56:15 +0200 Subject: [PATCH 3238/3516] Cleanup device_class checks in pylint plugin (#76458) --- pylint/plugins/hass_enforce_type_hints.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index f7a109507d8..257f4fb3613 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -611,10 +611,6 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ClassTypeHintMatch( base_class="AlarmControlPanelEntity", matches=[ - TypeHintMatch( - function_name="device_class", - return_type=["str", None], - ), TypeHintMatch( function_name="code_format", return_type=["CodeFormat", None], @@ -1220,10 +1216,6 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ClassTypeHintMatch( base_class="FanEntity", matches=[ - TypeHintMatch( - function_name="device_class", - return_type=["str", None], - ), TypeHintMatch( function_name="percentage", return_type=["int", None], @@ -1428,10 +1420,6 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ClassTypeHintMatch( base_class="LockEntity", matches=[ - TypeHintMatch( - function_name="device_class", - return_type=["str", None], - ), TypeHintMatch( function_name="changed_by", return_type=["str", None], From 753a3c0921d50d7778d9bb184fe4e957e3dbf412 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 9 Aug 2022 17:45:48 +0200 Subject: [PATCH 3239/3516] Add new sensors to NextDNS integration (#76262) * Add DNS-over-HTTP/3 sensors * Update tests --- homeassistant/components/nextdns/sensor.py | 22 ++++++++++ tests/components/nextdns/__init__.py | 1 + tests/components/nextdns/test_diagnostics.py | 12 +++--- tests/components/nextdns/test_sensor.py | 42 ++++++++++++++++++-- 4 files changed, 67 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/nextdns/sensor.py b/homeassistant/components/nextdns/sensor.py index 422bc2a237b..62cd671835b 100644 --- a/homeassistant/components/nextdns/sensor.py +++ b/homeassistant/components/nextdns/sensor.py @@ -107,6 +107,17 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.TOTAL, value=lambda data: data.doh_queries, ), + NextDnsSensorEntityDescription[AnalyticsProtocols]( + key="doh3_queries", + coordinator_type=ATTR_PROTOCOLS, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + icon="mdi:dns", + name="DNS-over-HTTP/3 queries", + native_unit_of_measurement="queries", + state_class=SensorStateClass.TOTAL, + value=lambda data: data.doh3_queries, + ), NextDnsSensorEntityDescription[AnalyticsProtocols]( key="dot_queries", coordinator_type=ATTR_PROTOCOLS, @@ -162,6 +173,17 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value=lambda data: data.doh_queries_ratio, ), + NextDnsSensorEntityDescription[AnalyticsProtocols]( + key="doh3_queries_ratio", + coordinator_type=ATTR_PROTOCOLS, + entity_registry_enabled_default=False, + icon="mdi:dns", + entity_category=EntityCategory.DIAGNOSTIC, + name="DNS-over-HTTP/3 queries ratio", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value=lambda data: data.doh3_queries_ratio, + ), NextDnsSensorEntityDescription[AnalyticsProtocols]( key="dot_queries_ratio", coordinator_type=ATTR_PROTOCOLS, diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py index 32b8bed76fa..04d838f8f58 100644 --- a/tests/components/nextdns/__init__.py +++ b/tests/components/nextdns/__init__.py @@ -25,6 +25,7 @@ ENCRYPTION = AnalyticsEncryption(encrypted_queries=60, unencrypted_queries=40) IP_VERSIONS = AnalyticsIpVersions(ipv4_queries=90, ipv6_queries=10) PROTOCOLS = AnalyticsProtocols( doh_queries=20, + doh3_queries=15, doq_queries=10, dot_queries=30, tcp_queries=0, diff --git a/tests/components/nextdns/test_diagnostics.py b/tests/components/nextdns/test_diagnostics.py index ec4ffa10aaf..21c030274cc 100644 --- a/tests/components/nextdns/test_diagnostics.py +++ b/tests/components/nextdns/test_diagnostics.py @@ -52,17 +52,17 @@ async def test_entry_diagnostics( } assert result["protocols_coordinator_data"] == { "doh_queries": 20, - "doh3_queries": 0, + "doh3_queries": 15, "doq_queries": 10, "dot_queries": 30, "tcp_queries": 0, "udp_queries": 40, - "doh_queries_ratio": 20.0, - "doh3_queries_ratio": 0.0, - "doq_queries_ratio": 10.0, - "dot_queries_ratio": 30.0, + "doh_queries_ratio": 17.4, + "doh3_queries_ratio": 13.0, + "doq_queries_ratio": 8.7, + "dot_queries_ratio": 26.1, "tcp_queries_ratio": 0.0, - "udp_queries_ratio": 40.0, + "udp_queries_ratio": 34.8, } assert result["settings_coordinator_data"] == settings assert result["status_coordinator_data"] == { diff --git a/tests/components/nextdns/test_sensor.py b/tests/components/nextdns/test_sensor.py index a90999a592b..c3c7577bd83 100644 --- a/tests/components/nextdns/test_sensor.py +++ b/tests/components/nextdns/test_sensor.py @@ -31,6 +31,13 @@ async def test_sensor(hass: HomeAssistant) -> None: suggested_object_id="fake_profile_dns_over_https_queries", disabled_by=None, ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_doh3_queries", + suggested_object_id="fake_profile_dns_over_http_3_queries", + disabled_by=None, + ) registry.async_get_or_create( SENSOR_DOMAIN, DOMAIN, @@ -38,6 +45,13 @@ async def test_sensor(hass: HomeAssistant) -> None: suggested_object_id="fake_profile_dns_over_https_queries_ratio", disabled_by=None, ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "xyz12_doh3_queries_ratio", + suggested_object_id="fake_profile_dns_over_http_3_queries_ratio", + disabled_by=None, + ) registry.async_get_or_create( SENSOR_DOMAIN, DOMAIN, @@ -212,7 +226,7 @@ async def test_sensor(hass: HomeAssistant) -> None: state = hass.states.get("sensor.fake_profile_dns_over_https_queries_ratio") assert state - assert state.state == "20.0" + assert state.state == "17.4" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -220,6 +234,26 @@ async def test_sensor(hass: HomeAssistant) -> None: assert entry assert entry.unique_id == "xyz12_doh_queries_ratio" + state = hass.states.get("sensor.fake_profile_dns_over_http_3_queries") + assert state + assert state.state == "15" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "queries" + + entry = registry.async_get("sensor.fake_profile_dns_over_http_3_queries") + assert entry + assert entry.unique_id == "xyz12_doh3_queries" + + state = hass.states.get("sensor.fake_profile_dns_over_http_3_queries_ratio") + assert state + assert state.state == "13.0" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.fake_profile_dns_over_http_3_queries_ratio") + assert entry + assert entry.unique_id == "xyz12_doh3_queries_ratio" + state = hass.states.get("sensor.fake_profile_dns_over_quic_queries") assert state assert state.state == "10" @@ -232,7 +266,7 @@ async def test_sensor(hass: HomeAssistant) -> None: state = hass.states.get("sensor.fake_profile_dns_over_quic_queries_ratio") assert state - assert state.state == "10.0" + assert state.state == "8.7" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -252,7 +286,7 @@ async def test_sensor(hass: HomeAssistant) -> None: state = hass.states.get("sensor.fake_profile_dns_over_tls_queries_ratio") assert state - assert state.state == "30.0" + assert state.state == "26.1" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE @@ -382,7 +416,7 @@ async def test_sensor(hass: HomeAssistant) -> None: state = hass.states.get("sensor.fake_profile_udp_queries_ratio") assert state - assert state.state == "40.0" + assert state.state == "34.8" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE From 6eb1dbdb74a578a2872bc91450d141c8c1fc3e6d Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 9 Aug 2022 17:51:04 +0200 Subject: [PATCH 3240/3516] Add NextDNS binary sensor platform (#75266) * Add binary_sensor platform * Add tests * Add quality scale * Sort coordinators * Remove quality scale * Fix docstring --- homeassistant/components/nextdns/__init__.py | 14 ++- .../components/nextdns/binary_sensor.py | 103 ++++++++++++++++++ homeassistant/components/nextdns/const.py | 2 + tests/components/nextdns/__init__.py | 5 + .../components/nextdns/test_binary_sensor.py | 86 +++++++++++++++ 5 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/nextdns/binary_sensor.py create mode 100644 tests/components/nextdns/test_binary_sensor.py diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index 2f68abee847..a92186f6f14 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -15,6 +15,7 @@ from nextdns import ( AnalyticsProtocols, AnalyticsStatus, ApiError, + ConnectionStatus, InvalidApiKeyError, NextDns, Settings, @@ -31,6 +32,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( + ATTR_CONNECTION, ATTR_DNSSEC, ATTR_ENCRYPTION, ATTR_IP_VERSIONS, @@ -40,6 +42,7 @@ from .const import ( CONF_PROFILE_ID, DOMAIN, UPDATE_INTERVAL_ANALYTICS, + UPDATE_INTERVAL_CONNECTION, UPDATE_INTERVAL_SETTINGS, ) @@ -131,10 +134,19 @@ class NextDnsSettingsUpdateCoordinator(NextDnsUpdateCoordinator[Settings]): return await self.nextdns.get_settings(self.profile_id) +class NextDnsConnectionUpdateCoordinator(NextDnsUpdateCoordinator[ConnectionStatus]): + """Class to manage fetching NextDNS connection data from API.""" + + async def _async_update_data_internal(self) -> ConnectionStatus: + """Update data via library.""" + return await self.nextdns.connection_status(self.profile_id) + + _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] COORDINATORS = [ + (ATTR_CONNECTION, NextDnsConnectionUpdateCoordinator, UPDATE_INTERVAL_CONNECTION), (ATTR_DNSSEC, NextDnsDnssecUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), (ATTR_ENCRYPTION, NextDnsEncryptionUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), (ATTR_IP_VERSIONS, NextDnsIpVersionsUpdateCoordinator, UPDATE_INTERVAL_ANALYTICS), diff --git a/homeassistant/components/nextdns/binary_sensor.py b/homeassistant/components/nextdns/binary_sensor.py new file mode 100644 index 00000000000..af80d14a89b --- /dev/null +++ b/homeassistant/components/nextdns/binary_sensor.py @@ -0,0 +1,103 @@ +"""Support for the NextDNS service.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Generic + +from nextdns import ConnectionStatus + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import NextDnsConnectionUpdateCoordinator, TCoordinatorData +from .const import ATTR_CONNECTION, DOMAIN + +PARALLEL_UPDATES = 1 + + +@dataclass +class NextDnsBinarySensorRequiredKeysMixin(Generic[TCoordinatorData]): + """Mixin for required keys.""" + + state: Callable[[TCoordinatorData, str], bool] + + +@dataclass +class NextDnsBinarySensorEntityDescription( + BinarySensorEntityDescription, + NextDnsBinarySensorRequiredKeysMixin[TCoordinatorData], +): + """NextDNS binary sensor entity description.""" + + +SENSORS = ( + NextDnsBinarySensorEntityDescription[ConnectionStatus]( + key="this_device_nextdns_connection_status", + entity_category=EntityCategory.DIAGNOSTIC, + name="This device NextDNS connection status", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + state=lambda data, _: data.connected, + ), + NextDnsBinarySensorEntityDescription[ConnectionStatus]( + key="this_device_profile_connection_status", + entity_category=EntityCategory.DIAGNOSTIC, + name="This device profile connection status", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + state=lambda data, profile_id: profile_id == data.profile_id, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add NextDNS entities from a config_entry.""" + coordinator: NextDnsConnectionUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + ATTR_CONNECTION + ] + + sensors: list[NextDnsBinarySensor] = [] + for description in SENSORS: + sensors.append(NextDnsBinarySensor(coordinator, description)) + + async_add_entities(sensors) + + +class NextDnsBinarySensor( + CoordinatorEntity[NextDnsConnectionUpdateCoordinator], BinarySensorEntity +): + """Define an NextDNS binary sensor.""" + + _attr_has_entity_name = True + entity_description: NextDnsBinarySensorEntityDescription + + def __init__( + self, + coordinator: NextDnsConnectionUpdateCoordinator, + description: NextDnsBinarySensorEntityDescription, + ) -> None: + """Initialize.""" + super().__init__(coordinator) + self._attr_device_info = coordinator.device_info + self._attr_unique_id = f"{coordinator.profile_id}_{description.key}" + self._attr_is_on = description.state(coordinator.data, coordinator.profile_id) + self.entity_description = description + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_is_on = self.entity_description.state( + self.coordinator.data, self.coordinator.profile_id + ) + self.async_write_ha_state() diff --git a/homeassistant/components/nextdns/const.py b/homeassistant/components/nextdns/const.py index d455dd79635..8cac556c87c 100644 --- a/homeassistant/components/nextdns/const.py +++ b/homeassistant/components/nextdns/const.py @@ -1,6 +1,7 @@ """Constants for NextDNS integration.""" from datetime import timedelta +ATTR_CONNECTION = "connection" ATTR_DNSSEC = "dnssec" ATTR_ENCRYPTION = "encryption" ATTR_IP_VERSIONS = "ip_versions" @@ -11,6 +12,7 @@ ATTR_STATUS = "status" CONF_PROFILE_ID = "profile_id" CONF_PROFILE_NAME = "profile_name" +UPDATE_INTERVAL_CONNECTION = timedelta(minutes=5) UPDATE_INTERVAL_ANALYTICS = timedelta(minutes=10) UPDATE_INTERVAL_SETTINGS = timedelta(minutes=1) diff --git a/tests/components/nextdns/__init__.py b/tests/components/nextdns/__init__.py index 04d838f8f58..8c80db1fdff 100644 --- a/tests/components/nextdns/__init__.py +++ b/tests/components/nextdns/__init__.py @@ -7,6 +7,7 @@ from nextdns import ( AnalyticsIpVersions, AnalyticsProtocols, AnalyticsStatus, + ConnectionStatus, Settings, ) @@ -16,6 +17,7 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +CONNECTION_STATUS = ConnectionStatus(connected=True, profile_id="abcdef") PROFILES = [{"id": "xyz12", "fingerprint": "aabbccdd123", "name": "Fake Profile"}] STATUS = AnalyticsStatus( default_queries=40, allowed_queries=30, blocked_queries=20, relayed_queries=10 @@ -129,6 +131,9 @@ async def init_integration(hass: HomeAssistant) -> MockConfigEntry: ), patch( "homeassistant.components.nextdns.NextDns.get_settings", return_value=SETTINGS, + ), patch( + "homeassistant.components.nextdns.NextDns.connection_status", + return_value=CONNECTION_STATUS, ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/nextdns/test_binary_sensor.py b/tests/components/nextdns/test_binary_sensor.py new file mode 100644 index 00000000000..eb1478a5809 --- /dev/null +++ b/tests/components/nextdns/test_binary_sensor.py @@ -0,0 +1,86 @@ +"""Test binary sensor of NextDNS integration.""" +from datetime import timedelta +from unittest.mock import patch + +from nextdns import ApiError + +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.util.dt import utcnow + +from . import CONNECTION_STATUS, init_integration + +from tests.common import async_fire_time_changed + + +async def test_binary_Sensor(hass: HomeAssistant) -> None: + """Test states of the binary sensors.""" + registry = er.async_get(hass) + + await init_integration(hass) + + state = hass.states.get( + "binary_sensor.fake_profile_this_device_nextdns_connection_status" + ) + assert state + assert state.state == STATE_ON + + entry = registry.async_get( + "binary_sensor.fake_profile_this_device_nextdns_connection_status" + ) + assert entry + assert entry.unique_id == "xyz12_this_device_nextdns_connection_status" + + state = hass.states.get( + "binary_sensor.fake_profile_this_device_profile_connection_status" + ) + assert state + assert state.state == STATE_OFF + + entry = registry.async_get( + "binary_sensor.fake_profile_this_device_profile_connection_status" + ) + assert entry + assert entry.unique_id == "xyz12_this_device_profile_connection_status" + + +async def test_availability(hass: HomeAssistant) -> None: + """Ensure that we mark the entities unavailable correctly when service causes an error.""" + await init_integration(hass) + + state = hass.states.get( + "binary_sensor.fake_profile_this_device_nextdns_connection_status" + ) + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == STATE_ON + + future = utcnow() + timedelta(minutes=10) + with patch( + "homeassistant.components.nextdns.NextDns.connection_status", + side_effect=ApiError("API Error"), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get( + "binary_sensor.fake_profile_this_device_nextdns_connection_status" + ) + assert state + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=20) + with patch( + "homeassistant.components.nextdns.NextDns.connection_status", + return_value=CONNECTION_STATUS, + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get( + "binary_sensor.fake_profile_this_device_nextdns_connection_status" + ) + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == STATE_ON From 7d0a4ee00a5af7bbb1ecc22a031285557e9be76b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 9 Aug 2022 17:54:33 +0200 Subject: [PATCH 3241/3516] Improve type hints in rfxtrx siren entity (#76459) --- homeassistant/components/rfxtrx/siren.py | 45 ++++++++++++++++-------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/rfxtrx/siren.py b/homeassistant/components/rfxtrx/siren.py index 282933f7f85..acf06518959 100644 --- a/homeassistant/components/rfxtrx/siren.py +++ b/homeassistant/components/rfxtrx/siren.py @@ -1,6 +1,7 @@ """Support for RFXtrx sirens.""" from __future__ import annotations +from datetime import datetime from typing import Any import RFXtrx as rfxtrxmod @@ -26,7 +27,7 @@ SECURITY_PANIC_OFF = "End Panic" SECURITY_PANIC_ALL = {SECURITY_PANIC_ON, SECURITY_PANIC_OFF} -def supported(event: rfxtrxmod.RFXtrxEvent): +def supported(event: rfxtrxmod.RFXtrxEvent) -> bool: """Return whether an event supports sirens.""" device = event.device @@ -104,16 +105,16 @@ class RfxtrxOffDelayMixin(Entity): _timeout: CALLBACK_TYPE | None = None _off_delay: float | None = None - def _setup_timeout(self): + def _setup_timeout(self) -> None: @callback - def _done(_): + def _done(_: datetime) -> None: self._timeout = None self.async_write_ha_state() if self._off_delay: self._timeout = async_call_later(self.hass, self._off_delay, _done) - def _cancel_timeout(self): + def _cancel_timeout(self) -> None: if self._timeout: self._timeout() self._timeout = None @@ -125,7 +126,13 @@ class RfxtrxChime(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin): _attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TONES _device: rfxtrxmod.ChimeDevice - def __init__(self, device, device_id, off_delay=None, event=None): + def __init__( + self, + device: rfxtrxmod.RFXtrxDevice, + device_id: DeviceTuple, + off_delay: float | None = None, + event: rfxtrxmod.RFXtrxEvent | None = None, + ) -> None: """Initialize the entity.""" super().__init__(device, device_id, event) self._attr_available_tones = list(self._device.COMMANDS.values()) @@ -133,11 +140,11 @@ class RfxtrxChime(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin): self._off_delay = off_delay @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._timeout is not None - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._cancel_timeout() @@ -152,7 +159,7 @@ class RfxtrxChime(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin): self.async_write_ha_state() - def _apply_event(self, event: rfxtrxmod.ControlEvent): + def _apply_event(self, event: rfxtrxmod.ControlEvent) -> None: """Apply a received event.""" super()._apply_event(event) @@ -162,7 +169,9 @@ class RfxtrxChime(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin): self._setup_timeout() @callback - def _handle_event(self, event, device_id): + def _handle_event( + self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple + ) -> None: """Check if event applies to me and update.""" if self._event_applies(event, device_id): self._apply_event(event) @@ -176,7 +185,13 @@ class RfxtrxSecurityPanic(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin) _attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF _device: rfxtrxmod.SecurityDevice - def __init__(self, device, device_id, off_delay=None, event=None): + def __init__( + self, + device: rfxtrxmod.RFXtrxDevice, + device_id: DeviceTuple, + off_delay: float | None = None, + event: rfxtrxmod.RFXtrxEvent | None = None, + ) -> None: """Initialize the entity.""" super().__init__(device, device_id, event) self._on_value = get_first_key(self._device.STATUS, SECURITY_PANIC_ON) @@ -184,11 +199,11 @@ class RfxtrxSecurityPanic(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin) self._off_delay = off_delay @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._timeout is not None - async def async_turn_on(self, **kwargs: Any): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._cancel_timeout() @@ -206,7 +221,7 @@ class RfxtrxSecurityPanic(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin) self.async_write_ha_state() - def _apply_event(self, event: rfxtrxmod.SensorEvent): + def _apply_event(self, event: rfxtrxmod.SensorEvent) -> None: """Apply a received event.""" super()._apply_event(event) @@ -219,7 +234,9 @@ class RfxtrxSecurityPanic(RfxtrxCommandEntity, SirenEntity, RfxtrxOffDelayMixin) self._cancel_timeout() @callback - def _handle_event(self, event, device_id): + def _handle_event( + self, event: rfxtrxmod.RFXtrxEvent, device_id: DeviceTuple + ) -> None: """Check if event applies to me and update.""" if self._event_applies(event, device_id): self._apply_event(event) From 38c57944fa1353e6bb1f0e3ba3187b86010f1e9d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 9 Aug 2022 18:32:36 +0200 Subject: [PATCH 3242/3516] Improve type hints in zha number entity (#76468) --- homeassistant/components/zha/number.py | 30 +++++++++++++++----------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 4252bf0e14c..14967c1b91c 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -278,12 +278,18 @@ async def async_setup_entry( class ZhaNumber(ZhaEntity, NumberEntity): """Representation of a ZHA Number entity.""" - def __init__(self, unique_id, zha_device, channels, **kwargs): + def __init__( + self, + unique_id: str, + zha_device: ZHADevice, + channels: list[ZigbeeChannel], + **kwargs: Any, + ) -> None: """Init this entity.""" super().__init__(unique_id, zha_device, channels, **kwargs) - self._analog_output_channel = self.cluster_channels.get(CHANNEL_ANALOG_OUTPUT) + self._analog_output_channel = self.cluster_channels[CHANNEL_ANALOG_OUTPUT] - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() self.async_accept_signal( @@ -291,12 +297,12 @@ class ZhaNumber(ZhaEntity, NumberEntity): ) @property - def native_value(self): + def native_value(self) -> float | None: """Return the current value.""" return self._analog_output_channel.present_value @property - def native_min_value(self): + def native_min_value(self) -> float: """Return the minimum value.""" min_present_value = self._analog_output_channel.min_present_value if min_present_value is not None: @@ -304,7 +310,7 @@ class ZhaNumber(ZhaEntity, NumberEntity): return 0 @property - def native_max_value(self): + def native_max_value(self) -> float: """Return the maximum value.""" max_present_value = self._analog_output_channel.max_present_value if max_present_value is not None: @@ -312,7 +318,7 @@ class ZhaNumber(ZhaEntity, NumberEntity): return 1023 @property - def native_step(self): + def native_step(self) -> float | None: """Return the value step.""" resolution = self._analog_output_channel.resolution if resolution is not None: @@ -320,7 +326,7 @@ class ZhaNumber(ZhaEntity, NumberEntity): return super().native_step @property - def name(self): + def name(self) -> str: """Return the name of the number entity.""" description = self._analog_output_channel.description if description is not None and len(description) > 0: @@ -328,7 +334,7 @@ class ZhaNumber(ZhaEntity, NumberEntity): return super().name @property - def icon(self): + def icon(self) -> str | None: """Return the icon to be used for this entity.""" application_type = self._analog_output_channel.application_type if application_type is not None: @@ -336,7 +342,7 @@ class ZhaNumber(ZhaEntity, NumberEntity): return super().icon @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str | None: """Return the unit the value is expressed in.""" engineering_units = self._analog_output_channel.engineering_units return UNITS.get(engineering_units) @@ -346,13 +352,13 @@ class ZhaNumber(ZhaEntity, NumberEntity): """Handle value update from channel.""" self.async_write_ha_state() - async def async_set_native_value(self, value): + async def async_set_native_value(self, value: float) -> None: """Update the current value from HA.""" num_value = float(value) if await self._analog_output_channel.async_set_present_value(num_value): self.async_write_ha_state() - async def async_update(self): + async def async_update(self) -> None: """Attempt to retrieve the state of the entity.""" await super().async_update() _LOGGER.debug("polling current state") From 23fc15115076dbf592f75454e77cd051a9e6dcf0 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Tue, 9 Aug 2022 14:41:02 -0400 Subject: [PATCH 3243/3516] Bump version of pyunifiprotect to 4.0.13 (#76523) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index ad18be3dba9..246a4643412 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.12", "unifi-discovery==1.1.5"], + "requirements": ["pyunifiprotect==4.0.13", "unifi-discovery==1.1.5"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 5eca3255533..a779d2ada82 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2013,7 +2013,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.12 +pyunifiprotect==4.0.13 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4d9e3611c4d..142247940a8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1370,7 +1370,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.12 +pyunifiprotect==4.0.13 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From ad361b8fc227f5636030ae40ab7c8453d3449f4f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Aug 2022 09:48:48 -1000 Subject: [PATCH 3244/3516] Fix pairing with HK accessories that do not provide format for vendor chars (#76502) --- .../homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../fixtures/schlage_sense.json | 356 ++++++++++++++++++ .../specific_devices/test_schlage_sense.py | 40 ++ 5 files changed, 399 insertions(+), 3 deletions(-) create mode 100644 tests/components/homekit_controller/fixtures/schlage_sense.json create mode 100644 tests/components/homekit_controller/specific_devices/test_schlage_sense.py diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index fa7bf39d385..3107f5efbb2 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.6"], + "requirements": ["aiohomekit==1.2.7"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index a779d2ada82..1949dd01ed2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.6 +aiohomekit==1.2.7 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 142247940a8..7483bdc31d1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.6 +aiohomekit==1.2.7 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/fixtures/schlage_sense.json b/tests/components/homekit_controller/fixtures/schlage_sense.json new file mode 100644 index 00000000000..04e1923da55 --- /dev/null +++ b/tests/components/homekit_controller/fixtures/schlage_sense.json @@ -0,0 +1,356 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000020-0000-1000-8000-0026BB765291", + "iid": 4, + "perms": ["pr"], + "format": "string", + "value": "Schlage ", + "description": "Manufacturer", + "maxLen": 64 + }, + { + "type": "00000021-0000-1000-8000-0026BB765291", + "iid": 5, + "perms": ["pr"], + "format": "string", + "value": "BE479CAM619", + "description": "Model", + "maxLen": 64 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 6, + "perms": ["pr"], + "format": "string", + "value": "SENSE ", + "description": "Name", + "maxLen": 64 + }, + { + "type": "00000030-0000-1000-8000-0026BB765291", + "iid": 7, + "perms": ["pr"], + "format": "string", + "value": "AAAAAAA000", + "description": "Serial Number", + "maxLen": 64 + }, + { + "type": "00000014-0000-1000-8000-0026BB765291", + "iid": 3, + "perms": ["pw"], + "format": "bool", + "description": "Identify" + }, + { + "type": "00000052-0000-1000-8000-0026BB765291", + "iid": 8, + "perms": ["pr"], + "format": "string", + "value": "004.027.000", + "description": "Firmware Revision", + "maxLen": 64 + }, + { + "type": "00000053-0000-1000-8000-0026BB765291", + "iid": 51, + "perms": ["pr"], + "format": "string", + "value": "1.3.0", + "description": "Hardware Revision", + "maxLen": 64 + }, + { + "type": "00000054-0000-1000-8000-0026BB765291", + "iid": 50, + "perms": ["pr"], + "format": "string", + "value": "002.001.000", + "maxLen": 64 + } + ] + }, + { + "iid": 10, + "type": "7F0DEE73-4A3F-4103-98E6-A46CD301BDFB", + "characteristics": [ + { + "type": "44FF6853-58DB-4956-B298-5F6650DD61F6", + "iid": 25, + "perms": ["pw"], + "format": "data" + }, + { + "type": "CF68C40F-DC6F-4F7E-918C-4C536B643A2B", + "iid": 26, + "perms": ["pr", "pw"], + "format": "uint8", + "value": 0, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "type": "4058C2B8-4545-4E77-B6B7-157C38F9718B", + "iid": 27, + "perms": ["pr", "pw"], + "format": "uint8", + "value": 0, + "minValue": 1, + "maxValue": 5, + "minStep": 1 + }, + { + "type": "B498F4B5-6364-4F79-B5CC-1563ADE070DF", + "iid": 28, + "perms": ["pr", "pw"], + "format": "uint8", + "value": 1, + "minValue": 0, + "maxValue": 1 + }, + { + "type": "AFAE7AD2-8DD3-4B20-BAE0-C0B18B79EDB5", + "iid": 29, + "perms": ["pw"], + "format": "data" + }, + { + "type": "87D91EC6-C508-4CAD-89F1-A21B0BF179A0", + "iid": 30, + "perms": ["pr"], + "format": "data", + "value": "000a00000000000000000000" + }, + { + "type": "4C3E2641-F57F-11E3-A3AC-0800200C9A66", + "iid": 31, + "perms": ["pr"], + "format": "uint64", + "value": 3468600224 + }, + { + "type": "EEC26990-F628-11E3-A3AC-0800200C9A66", + "iid": 32, + "perms": ["pr", "pw"], + "format": "uint8", + "value": 4, + "minValue": 4, + "maxValue": 8, + "minStep": 1 + }, + { + "type": "BCDE3B9E-3963-4123-B24D-42ECCBB3A9C4", + "iid": 33, + "perms": ["pr"], + "format": "data", + "value": "4e6f6e65" + }, + { + "type": "A9464D14-6806-4375-BA53-E14F7E0A6BEE", + "iid": 34, + "perms": ["pr", "pw"], + "format": null, + "value": "ff" + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 35, + "perms": ["pr"], + "format": "string", + "value": "Additional Settings", + "description": "Name", + "maxLen": 64 + }, + { + "type": "63D23C2F-2FBB-45E8-8540-47CC26C517D0", + "iid": 36, + "perms": ["pr"], + "format": "uint8", + "value": 100 + } + ] + }, + { + "iid": 23, + "type": "00000044-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000019-0000-1000-8000-0026BB765291", + "iid": 16, + "perms": ["pw"], + "format": "data", + "description": "Lock Control Point" + }, + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 17, + "perms": ["pr"], + "format": "string", + "value": "02.00.00", + "description": "Version", + "maxLen": 64 + }, + { + "type": "0000001F-0000-1000-8000-0026BB765291", + "iid": 18, + "perms": ["pr"], + "format": "data", + "value": "012431443133423434392d423941312d334135392d463042412d3245393030304233453430450208000000000000000003010404030001ff", + "description": "Logs" + }, + { + "type": "00000005-0000-1000-8000-0026BB765291", + "iid": 19, + "perms": ["pr", "pw"], + "format": "bool", + "value": true, + "description": "Audio Feedback" + }, + { + "type": "0000001A-0000-1000-8000-0026BB765291", + "iid": 20, + "perms": ["pr", "pw"], + "format": "uint32", + "value": 0, + "description": "Lock Management Auto Security Timeout", + "unit": "seconds" + }, + { + "type": "00000001-0000-1000-8000-0026BB765291", + "iid": 21, + "perms": ["pr", "pw"], + "format": "bool", + "value": false, + "description": "Administrator Only Access" + } + ] + }, + { + "iid": 30, + "type": "00000045-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000001D-0000-1000-8000-0026BB765291", + "iid": 11, + "perms": ["pr", "ev"], + "format": "uint8", + "value": 3, + "description": "Lock Current State", + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "type": "0000001E-0000-1000-8000-0026BB765291", + "iid": 12, + "perms": ["pr", "pw", "ev"], + "format": "uint8", + "value": 1, + "description": "Lock Target State", + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "type": "00000023-0000-1000-8000-0026BB765291", + "iid": 13, + "perms": ["pr"], + "format": "string", + "value": "Lock Mechanism", + "description": "Name", + "maxLen": 64 + } + ] + }, + { + "iid": 34, + "type": "1F6B43AA-94DE-4BA9-981C-DA38823117BD", + "characteristics": [ + { + "type": "048D8799-695B-4A7F-A7F7-A4A1301587FE", + "iid": 39, + "perms": ["pw"], + "format": "data" + }, + { + "type": "66B7C7FD-95A7-4F89-B0AD-38073A67C46C", + "iid": 40, + "perms": ["pw"], + "format": "data" + }, + { + "type": "507EFC3F-9231-438C-976A-FA04427F1F8F", + "iid": 41, + "perms": ["pw"], + "format": "data" + }, + { + "type": "1DC15719-0882-4BAD-AB0F-9AEAB0600C90", + "iid": 42, + "perms": ["pr"], + "format": "data", + "value": "03" + } + ] + }, + { + "iid": 39, + "type": "00000055-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "0000004C-0000-1000-8000-0026BB765291", + "iid": 45, + "perms": [], + "format": "data", + "description": "Pair Setup" + }, + { + "type": "0000004E-0000-1000-8000-0026BB765291", + "iid": 46, + "perms": [], + "format": "data", + "description": "Pair Verify" + }, + { + "type": "0000004F-0000-1000-8000-0026BB765291", + "iid": 47, + "perms": [], + "format": "uint8", + "description": "Pairing Features" + }, + { + "type": "00000050-0000-1000-8000-0026BB765291", + "iid": 48, + "perms": ["pr", "pw"], + "format": "data", + "value": null, + "description": "Pairing Pairings" + } + ] + }, + { + "iid": 44, + "type": "000000A2-0000-1000-8000-0026BB765291", + "characteristics": [ + { + "type": "00000037-0000-1000-8000-0026BB765291", + "iid": 62, + "perms": ["pr"], + "format": "string", + "value": "02.00.00", + "description": "Version", + "maxLen": 64 + } + ] + } + ] + } +] diff --git a/tests/components/homekit_controller/specific_devices/test_schlage_sense.py b/tests/components/homekit_controller/specific_devices/test_schlage_sense.py new file mode 100644 index 00000000000..d572989e345 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_schlage_sense.py @@ -0,0 +1,40 @@ +"""Make sure that Schlage Sense is enumerated properly.""" + + +from tests.components.homekit_controller.common import ( + HUB_TEST_ACCESSORY_ID, + DeviceTestInfo, + EntityTestInfo, + assert_devices_and_entities_created, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_schlage_sense_setup(hass): + """Test that the accessory can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "schlage_sense.json") + await setup_test_accessories(hass, accessories) + + await assert_devices_and_entities_created( + hass, + DeviceTestInfo( + unique_id=HUB_TEST_ACCESSORY_ID, + name="SENSE ", + model="BE479CAM619", + manufacturer="Schlage ", + sw_version="004.027.000", + hw_version="1.3.0", + serial_number="AAAAAAA000", + devices=[], + entities=[ + EntityTestInfo( + entity_id="lock.sense_lock_mechanism", + friendly_name="SENSE Lock Mechanism", + unique_id="homekit-AAAAAAA000-30", + supported_features=0, + state="unknown", + ), + ], + ), + ) From 70aeaa3c76a7e67dfbae3deb01105db9050bbdd3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 9 Aug 2022 22:10:26 +0200 Subject: [PATCH 3245/3516] Use Callback protocol for AutomationActionType (#76054) --- .../components/automation/__init__.py | 25 +++++++++++++++---- homeassistant/components/calendar/trigger.py | 3 ++- .../components/homekit/type_triggers.py | 2 +- .../components/philips_js/__init__.py | 6 +++-- homeassistant/components/webostv/__init__.py | 7 ++++-- 5 files changed, 32 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index c743e1f83fd..c3a669511bc 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -1,9 +1,9 @@ """Allow to set up simple automation rules via the config file.""" from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Callable import logging -from typing import Any, TypedDict, cast +from typing import Any, Protocol, TypedDict, cast import voluptuous as vol from voluptuous.humanize import humanize_error @@ -110,7 +110,17 @@ ATTR_VARIABLES = "variables" SERVICE_TRIGGER = "trigger" _LOGGER = logging.getLogger(__name__) -AutomationActionType = Callable[[HomeAssistant, TemplateVarsType], Awaitable[None]] + + +class AutomationActionType(Protocol): + """Protocol type for automation action callback.""" + + async def __call__( + self, + run_variables: dict[str, Any], + context: Context | None = None, + ) -> None: + """Define action callback type.""" class AutomationTriggerData(TypedDict): @@ -437,7 +447,12 @@ class AutomationEntity(ToggleEntity, RestoreEntity): else: await self.async_disable() - async def async_trigger(self, run_variables, context=None, skip_condition=False): + async def async_trigger( + self, + run_variables: dict[str, Any], + context: Context | None = None, + skip_condition: bool = False, + ) -> None: """Trigger automation. This method is a coroutine. @@ -462,7 +477,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): this = None if state := self.hass.states.get(self.entity_id): this = state.as_dict() - variables = {"this": this, **(run_variables or {})} + variables: dict[str, Any] = {"this": this, **(run_variables or {})} if self._variables: try: variables = self._variables.async_render(self.hass, variables) diff --git a/homeassistant/components/calendar/trigger.py b/homeassistant/components/calendar/trigger.py index bb6e874b47f..7845037f896 100644 --- a/homeassistant/components/calendar/trigger.py +++ b/homeassistant/components/calendar/trigger.py @@ -1,6 +1,7 @@ """Offer calendar automation rules.""" from __future__ import annotations +from collections.abc import Coroutine import datetime import logging from typing import Any @@ -47,7 +48,7 @@ class CalendarEventListener: def __init__( self, hass: HomeAssistant, - job: HassJob, + job: HassJob[..., Coroutine[Any, Any, None]], trigger_data: dict[str, Any], entity: CalendarEntity, event_type: str, diff --git a/homeassistant/components/homekit/type_triggers.py b/homeassistant/components/homekit/type_triggers.py index 4b3a7e73cac..776fe6f3110 100644 --- a/homeassistant/components/homekit/type_triggers.py +++ b/homeassistant/components/homekit/type_triggers.py @@ -66,7 +66,7 @@ class DeviceTriggerAccessory(HomeAccessory): async def async_trigger( self, - run_variables: dict, + run_variables: dict[str, Any], context: Context | None = None, skip_condition: bool = False, ) -> None: diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 24b3f9a91e0..154df3ed214 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Callable, Mapping +from collections.abc import Callable, Coroutine, Mapping from datetime import timedelta import logging from typing import Any @@ -84,7 +84,9 @@ class PluggableAction: def __init__(self, update: Callable[[], None]) -> None: """Initialize.""" self._update = update - self._actions: dict[Any, tuple[HassJob, dict[str, Any]]] = {} + self._actions: dict[ + Any, tuple[HassJob[..., Coroutine[Any, Any, None]], dict[str, Any]] + ] = {} def __bool__(self): """Return if we have something attached.""" diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index d9b2acb1836..32161e6bad6 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -1,7 +1,7 @@ """Support for LG webOS Smart TV.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Coroutine from contextlib import suppress import logging from typing import Any @@ -170,7 +170,10 @@ class PluggableAction: def __init__(self) -> None: """Initialize.""" - self._actions: dict[Callable[[], None], tuple[HassJob, dict[str, Any]]] = {} + self._actions: dict[ + Callable[[], None], + tuple[HassJob[..., Coroutine[Any, Any, None]], dict[str, Any]], + ] = {} def __bool__(self) -> bool: """Return if we have something attached.""" From dc47121f2cb9cb5146048df39977093e6bd7690a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 9 Aug 2022 22:12:33 +0200 Subject: [PATCH 3246/3516] Better type hass_job method calls (#76053) --- homeassistant/core.py | 20 +++++++++++-------- homeassistant/helpers/discovery.py | 6 ++++-- homeassistant/helpers/event.py | 32 ++++++++++++++++-------------- homeassistant/helpers/start.py | 6 ++++-- 4 files changed, 37 insertions(+), 27 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 7b41fe476aa..fcd41ddc856 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -816,7 +816,7 @@ class Event: class _FilterableJob(NamedTuple): """Event listener job to be executed with optional filter.""" - job: HassJob[[Event], None | Awaitable[None]] + job: HassJob[[Event], Coroutine[Any, Any, None] | None] event_filter: Callable[[Event], bool] | None run_immediately: bool @@ -907,7 +907,7 @@ class EventBus: def listen( self, event_type: str, - listener: Callable[[Event], None | Awaitable[None]], + listener: Callable[[Event], Coroutine[Any, Any, None] | None], ) -> CALLBACK_TYPE: """Listen for all events or events of a specific type. @@ -928,7 +928,7 @@ class EventBus: def async_listen( self, event_type: str, - listener: Callable[[Event], None | Awaitable[None]], + listener: Callable[[Event], Coroutine[Any, Any, None] | None], event_filter: Callable[[Event], bool] | None = None, run_immediately: bool = False, ) -> CALLBACK_TYPE: @@ -968,7 +968,9 @@ class EventBus: return remove_listener def listen_once( - self, event_type: str, listener: Callable[[Event], None | Awaitable[None]] + self, + event_type: str, + listener: Callable[[Event], Coroutine[Any, Any, None] | None], ) -> CALLBACK_TYPE: """Listen once for event of a specific type. @@ -989,7 +991,9 @@ class EventBus: @callback def async_listen_once( - self, event_type: str, listener: Callable[[Event], None | Awaitable[None]] + self, + event_type: str, + listener: Callable[[Event], Coroutine[Any, Any, None] | None], ) -> CALLBACK_TYPE: """Listen once for event of a specific type. @@ -1463,7 +1467,7 @@ class Service: def __init__( self, - func: Callable[[ServiceCall], None | Awaitable[None]], + func: Callable[[ServiceCall], Coroutine[Any, Any, None] | None], schema: vol.Schema | None, context: Context | None = None, ) -> None: @@ -1533,7 +1537,7 @@ class ServiceRegistry: self, domain: str, service: str, - service_func: Callable[[ServiceCall], Awaitable[None] | None], + service_func: Callable[[ServiceCall], Coroutine[Any, Any, None] | None], schema: vol.Schema | None = None, ) -> None: """ @@ -1550,7 +1554,7 @@ class ServiceRegistry: self, domain: str, service: str, - service_func: Callable[[ServiceCall], Awaitable[None] | None], + service_func: Callable[[ServiceCall], Coroutine[Any, Any, None] | None], schema: vol.Schema | None = None, ) -> None: """ diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 61117fb7d04..375c3b09c2e 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -7,7 +7,7 @@ There are two different types of discoveries that can be fired/listened for. """ from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Coroutine from typing import Any, TypedDict from homeassistant import core, setup @@ -36,7 +36,9 @@ class DiscoveryDict(TypedDict): def async_listen( hass: core.HomeAssistant, service: str, - callback: Callable[[str, DiscoveryInfoType | None], Awaitable[None] | None], + callback: Callable[ + [str, DiscoveryInfoType | None], Coroutine[Any, Any, None] | None + ], ) -> None: """Set up listener for discovery of specific service. diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index d18af953ec6..2a34773a413 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -2,7 +2,7 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable, Iterable, Sequence +from collections.abc import Callable, Coroutine, Iterable, Sequence import copy from dataclasses import dataclass from datetime import datetime, timedelta @@ -141,7 +141,7 @@ def threaded_listener_factory( def async_track_state_change( hass: HomeAssistant, entity_ids: str | Iterable[str], - action: Callable[[str, State | None, State], Awaitable[None] | None], + action: Callable[[str, State | None, State], Coroutine[Any, Any, None] | None], from_state: None | str | Iterable[str] = None, to_state: None | str | Iterable[str] = None, ) -> CALLBACK_TYPE: @@ -714,7 +714,9 @@ def async_track_state_change_filtered( def async_track_template( hass: HomeAssistant, template: Template, - action: Callable[[str, State | None, State | None], Awaitable[None] | None], + action: Callable[ + [str, State | None, State | None], Coroutine[Any, Any, None] | None + ], variables: TemplateVarsType | None = None, ) -> CALLBACK_TYPE: """Add a listener that fires when a a template evaluates to 'true'. @@ -1188,7 +1190,7 @@ def async_track_template_result( def async_track_same_state( hass: HomeAssistant, period: timedelta, - action: Callable[[], Awaitable[None] | None], + action: Callable[[], Coroutine[Any, Any, None] | None], async_check_same_func: Callable[[str, State | None, State | None], bool], entity_ids: str | Iterable[str] = MATCH_ALL, ) -> CALLBACK_TYPE: @@ -1257,8 +1259,8 @@ track_same_state = threaded_listener_factory(async_track_same_state) @bind_hass def async_track_point_in_time( hass: HomeAssistant, - action: HassJob[[datetime], Awaitable[None] | None] - | Callable[[datetime], Awaitable[None] | None], + action: HassJob[[datetime], Coroutine[Any, Any, None] | None] + | Callable[[datetime], Coroutine[Any, Any, None] | None], point_in_time: datetime, ) -> CALLBACK_TYPE: """Add a listener that fires once after a specific point in time.""" @@ -1279,8 +1281,8 @@ track_point_in_time = threaded_listener_factory(async_track_point_in_time) @bind_hass def async_track_point_in_utc_time( hass: HomeAssistant, - action: HassJob[[datetime], Awaitable[None] | None] - | Callable[[datetime], Awaitable[None] | None], + action: HassJob[[datetime], Coroutine[Any, Any, None] | None] + | Callable[[datetime], Coroutine[Any, Any, None] | None], point_in_time: datetime, ) -> CALLBACK_TYPE: """Add a listener that fires once after a specific point in UTC time.""" @@ -1293,7 +1295,7 @@ def async_track_point_in_utc_time( cancel_callback: asyncio.TimerHandle | None = None @callback - def run_action(job: HassJob[[datetime], Awaitable[None] | None]) -> None: + def run_action(job: HassJob[[datetime], Coroutine[Any, Any, None] | None]) -> None: """Call the action.""" nonlocal cancel_callback # Depending on the available clock support (including timer hardware @@ -1330,8 +1332,8 @@ track_point_in_utc_time = threaded_listener_factory(async_track_point_in_utc_tim def async_call_later( hass: HomeAssistant, delay: float | timedelta, - action: HassJob[[datetime], Awaitable[None] | None] - | Callable[[datetime], Awaitable[None] | None], + action: HassJob[[datetime], Coroutine[Any, Any, None] | None] + | Callable[[datetime], Coroutine[Any, Any, None] | None], ) -> CALLBACK_TYPE: """Add a listener that is called in .""" if not isinstance(delay, timedelta): @@ -1346,7 +1348,7 @@ call_later = threaded_listener_factory(async_call_later) @bind_hass def async_track_time_interval( hass: HomeAssistant, - action: Callable[[datetime], Awaitable[None] | None], + action: Callable[[datetime], Coroutine[Any, Any, None] | None], interval: timedelta, ) -> CALLBACK_TYPE: """Add a listener that fires repetitively at every timedelta interval.""" @@ -1388,7 +1390,7 @@ class SunListener: """Helper class to help listen to sun events.""" hass: HomeAssistant = attr.ib() - job: HassJob[[], Awaitable[None] | None] = attr.ib() + job: HassJob[[], Coroutine[Any, Any, None] | None] = attr.ib() event: str = attr.ib() offset: timedelta | None = attr.ib() _unsub_sun: CALLBACK_TYPE | None = attr.ib(default=None) @@ -1479,7 +1481,7 @@ time_tracker_timestamp = time.time @bind_hass def async_track_utc_time_change( hass: HomeAssistant, - action: Callable[[datetime], Awaitable[None] | None], + action: Callable[[datetime], Coroutine[Any, Any, None] | None], hour: Any | None = None, minute: Any | None = None, second: Any | None = None, @@ -1544,7 +1546,7 @@ track_utc_time_change = threaded_listener_factory(async_track_utc_time_change) @bind_hass def async_track_time_change( hass: HomeAssistant, - action: Callable[[datetime], Awaitable[None] | None], + action: Callable[[datetime], Coroutine[Any, Any, None] | None], hour: Any | None = None, minute: Any | None = None, second: Any | None = None, diff --git a/homeassistant/helpers/start.py b/homeassistant/helpers/start.py index 6c17ae5be3a..f6c9a536a23 100644 --- a/homeassistant/helpers/start.py +++ b/homeassistant/helpers/start.py @@ -1,7 +1,8 @@ """Helpers to help during startup.""" from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Coroutine +from typing import Any from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback @@ -9,7 +10,8 @@ from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, cal @callback def async_at_start( - hass: HomeAssistant, at_start_cb: Callable[[HomeAssistant], Awaitable[None] | None] + hass: HomeAssistant, + at_start_cb: Callable[[HomeAssistant], Coroutine[Any, Any, None] | None], ) -> CALLBACK_TYPE: """Execute something when Home Assistant is started. From 596b7fed34cc4e64bc99167dc36672e052f157d8 Mon Sep 17 00:00:00 2001 From: Oscar Calvo <2091582+ocalvo@users.noreply.github.com> Date: Tue, 9 Aug 2022 14:46:22 -0600 Subject: [PATCH 3247/3516] Fix #76283 (#76531) --- homeassistant/components/sms/notify.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sms/notify.py b/homeassistant/components/sms/notify.py index d076f3625ba..21b48946f55 100644 --- a/homeassistant/components/sms/notify.py +++ b/homeassistant/components/sms/notify.py @@ -20,31 +20,32 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the SMS notification service.""" - if SMS_GATEWAY not in hass.data[DOMAIN]: - _LOGGER.error("SMS gateway not found, cannot initialize service") - return - - gateway = hass.data[DOMAIN][SMS_GATEWAY][GATEWAY] - if discovery_info is None: number = config[CONF_RECIPIENT] else: number = discovery_info[CONF_RECIPIENT] - return SMSNotificationService(gateway, number) + return SMSNotificationService(hass, number) class SMSNotificationService(BaseNotificationService): """Implement the notification service for SMS.""" - def __init__(self, gateway, number): + def __init__(self, hass, number): """Initialize the service.""" - self.gateway = gateway + + self.hass = hass self.number = number async def async_send_message(self, message="", **kwargs): """Send SMS message.""" + if SMS_GATEWAY not in self.hass.data[DOMAIN]: + _LOGGER.error("SMS gateway not found, cannot send message") + return + + gateway = self.hass.data[DOMAIN][SMS_GATEWAY][GATEWAY] + targets = kwargs.get(CONF_TARGET, [self.number]) smsinfo = { "Class": -1, @@ -67,6 +68,6 @@ class SMSNotificationService(BaseNotificationService): encoded_message["Number"] = target try: # Actually send the message - await self.gateway.send_sms_async(encoded_message) + await gateway.send_sms_async(encoded_message) except gammu.GSMError as exc: _LOGGER.error("Sending to %s failed: %s", target, exc) From b04352e7456a271dfc42e8aff1c597df4549eb0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Aug 2022 11:08:38 -1000 Subject: [PATCH 3248/3516] Bump aiohomekit to 1.2.8 (#76532) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 3107f5efbb2..cf3069e3b0d 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.7"], + "requirements": ["aiohomekit==1.2.8"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 1949dd01ed2..dcc87172176 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.7 +aiohomekit==1.2.8 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7483bdc31d1..1df383e4ea9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.7 +aiohomekit==1.2.8 # homeassistant.components.emulated_hue # homeassistant.components.http From 4a938ec33ee8df17e619fc64a519ef7c2ff856d8 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 10 Aug 2022 00:23:36 +0000 Subject: [PATCH 3249/3516] [ci skip] Translation update --- .../android_ip_webcam/translations/en.json | 2 +- .../android_ip_webcam/translations/fr.json | 20 +++++++++++ .../android_ip_webcam/translations/hu.json | 26 +++++++++++++++ .../android_ip_webcam/translations/it.json | 20 +++++++++++ .../translations/zh-Hant.json | 26 +++++++++++++++ .../components/anthemav/translations/ru.json | 2 +- .../components/demo/translations/it.json | 13 +++++++- .../components/demo/translations/ru.json | 2 +- .../deutsche_bahn/translations/it.json | 7 ++++ .../deutsche_bahn/translations/ru.json | 2 +- .../components/escea/translations/hu.json | 13 ++++++++ .../components/escea/translations/it.json | 8 +++++ .../components/escea/translations/no.json | 13 ++++++++ .../components/escea/translations/ru.json | 13 ++++++++ .../flunearyou/translations/it.json | 12 +++++++ .../components/google/translations/ru.json | 2 +- .../justnimbus/translations/hu.json | 19 +++++++++++ .../justnimbus/translations/it.json | 19 +++++++++++ .../justnimbus/translations/no.json | 19 +++++++++++ .../justnimbus/translations/ru.json | 19 +++++++++++ .../justnimbus/translations/zh-Hant.json | 19 +++++++++++ .../lg_soundbar/translations/ru.json | 2 +- .../components/lyric/translations/ru.json | 2 +- .../components/miflora/translations/ru.json | 2 +- .../components/mitemp_bt/translations/ru.json | 2 +- .../components/mysensors/translations/hu.json | 9 +++++ .../components/mysensors/translations/it.json | 9 +++++ .../components/nest/translations/ru.json | 2 +- .../openalpr_local/translations/ru.json | 2 +- .../openexchangerates/translations/hu.json | 33 +++++++++++++++++++ .../openexchangerates/translations/it.json | 29 ++++++++++++++++ .../openexchangerates/translations/ru.json | 33 +++++++++++++++++++ .../radiotherm/translations/ru.json | 2 +- .../components/recorder/translations/es.json | 2 +- .../components/senz/translations/ru.json | 2 +- .../simplepush/translations/it.json | 4 +++ .../simplepush/translations/ru.json | 4 +-- .../simplisafe/translations/it.json | 1 + .../simplisafe/translations/ru.json | 2 +- .../components/solaredge/translations/it.json | 2 +- .../soundtouch/translations/ru.json | 2 +- .../steam_online/translations/ru.json | 2 +- .../unifiprotect/translations/hu.json | 1 + .../unifiprotect/translations/it.json | 1 + .../unifiprotect/translations/no.json | 1 + .../unifiprotect/translations/ru.json | 1 + .../unifiprotect/translations/zh-Hant.json | 1 + .../components/uscis/translations/ru.json | 2 +- .../components/xbox/translations/ru.json | 2 +- 49 files changed, 410 insertions(+), 23 deletions(-) create mode 100644 homeassistant/components/android_ip_webcam/translations/fr.json create mode 100644 homeassistant/components/android_ip_webcam/translations/hu.json create mode 100644 homeassistant/components/android_ip_webcam/translations/it.json create mode 100644 homeassistant/components/android_ip_webcam/translations/zh-Hant.json create mode 100644 homeassistant/components/deutsche_bahn/translations/it.json create mode 100644 homeassistant/components/escea/translations/hu.json create mode 100644 homeassistant/components/escea/translations/it.json create mode 100644 homeassistant/components/escea/translations/no.json create mode 100644 homeassistant/components/escea/translations/ru.json create mode 100644 homeassistant/components/justnimbus/translations/hu.json create mode 100644 homeassistant/components/justnimbus/translations/it.json create mode 100644 homeassistant/components/justnimbus/translations/no.json create mode 100644 homeassistant/components/justnimbus/translations/ru.json create mode 100644 homeassistant/components/justnimbus/translations/zh-Hant.json create mode 100644 homeassistant/components/openexchangerates/translations/hu.json create mode 100644 homeassistant/components/openexchangerates/translations/it.json create mode 100644 homeassistant/components/openexchangerates/translations/ru.json diff --git a/homeassistant/components/android_ip_webcam/translations/en.json b/homeassistant/components/android_ip_webcam/translations/en.json index 43cd63356b4..775263225ea 100644 --- a/homeassistant/components/android_ip_webcam/translations/en.json +++ b/homeassistant/components/android_ip_webcam/translations/en.json @@ -20,7 +20,7 @@ "issues": { "deprecated_yaml": { "description": "Configuring Android IP Webcam using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Android IP Webcam YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", - "title": "The Android IP Webcamepush YAML configuration is being removed" + "title": "The Android IP Webcam YAML configuration is being removed" } } } \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/fr.json b/homeassistant/components/android_ip_webcam/translations/fr.json new file mode 100644 index 00000000000..0e83b0feaf7 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/hu.json b/homeassistant/components/android_ip_webcam/translations/hu.json new file mode 100644 index 00000000000..e728b9eee54 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/hu.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "password": "Jelsz\u00f3", + "port": "Port", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Az Android IP webkamera konfigur\u00e1l\u00e1sa YAML haszn\u00e1lat\u00e1val elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA hiba kijav\u00edt\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el az Android IP Webkamera YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "Az Android IP webkamera YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/it.json b/homeassistant/components/android_ip_webcam/translations/it.json new file mode 100644 index 00000000000..7c04ebfdaef --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/zh-Hant.json b/homeassistant/components/android_ip_webcam/translations/zh-Hant.json new file mode 100644 index 00000000000..523c5a8b0b3 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Android IP Webcam \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Android IP Webcam YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Android IP Webcam YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/ru.json b/homeassistant/components/anthemav/translations/ru.json index e55ad7100e7..f56475d331d 100644 --- a/homeassistant/components/anthemav/translations/ru.json +++ b/homeassistant/components/anthemav/translations/ru.json @@ -18,7 +18,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AV-\u0440\u0435\u0441\u0438\u0432\u0435\u0440\u043e\u0432 Anthem \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AV-\u0440\u0435\u0441\u0438\u0432\u0435\u0440\u043e\u0432 Anthem \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AV-\u0440\u0435\u0441\u0438\u0432\u0435\u0440\u043e\u0432 Anthem \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } } diff --git a/homeassistant/components/demo/translations/it.json b/homeassistant/components/demo/translations/it.json index 80281a62703..e8265baebf7 100644 --- a/homeassistant/components/demo/translations/it.json +++ b/homeassistant/components/demo/translations/it.json @@ -1,10 +1,21 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Premere INVIA per confermare che l'alimentatore \u00e8 stato sostituito", + "title": "L'alimentatore deve essere sostituito" + } + } + }, + "title": "L'alimentazione non \u00e8 stabile" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { "confirm": { - "description": "Premere OK quando il liquido delle frecce \u00e8 stato riempito", + "description": "Premere INVIA quando il liquido delle frecce \u00e8 stato riempito", "title": "Il liquido delle frecce deve essere rabboccato" } } diff --git a/homeassistant/components/demo/translations/ru.json b/homeassistant/components/demo/translations/ru.json index 29b4229dacb..b7f3ddb20a7 100644 --- a/homeassistant/components/demo/translations/ru.json +++ b/homeassistant/components/demo/translations/ru.json @@ -15,7 +15,7 @@ "fix_flow": { "step": { "confirm": { - "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 OK, \u043a\u043e\u0433\u0434\u0430 \u0436\u0438\u0434\u043a\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u043f\u043e\u0432\u043e\u0440\u043e\u0442\u043d\u0438\u043a\u043e\u0432 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0430.", + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \"\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c\", \u043a\u043e\u0433\u0434\u0430 \u0436\u0438\u0434\u043a\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u043f\u043e\u0432\u043e\u0440\u043e\u0442\u043d\u0438\u043a\u043e\u0432 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0430.", "title": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0434\u043e\u043b\u0438\u0442\u044c \u0436\u0438\u0434\u043a\u043e\u0441\u0442\u044c \u0434\u043b\u044f \u043f\u043e\u0432\u043e\u0440\u043e\u0442\u043d\u0438\u043a\u043e\u0432" } } diff --git a/homeassistant/components/deutsche_bahn/translations/it.json b/homeassistant/components/deutsche_bahn/translations/it.json new file mode 100644 index 00000000000..d390f84ff0a --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/it.json @@ -0,0 +1,7 @@ +{ + "issues": { + "pending_removal": { + "title": "L'integrazione Deutsche Bahn sar\u00e0 rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/ru.json b/homeassistant/components/deutsche_bahn/translations/ru.json index 2cfbb695fc3..b5fe6857a73 100644 --- a/homeassistant/components/deutsche_bahn/translations/ru.json +++ b/homeassistant/components/deutsche_bahn/translations/ru.json @@ -1,7 +1,7 @@ { "issues": { "pending_removal": { - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Deutsche Bahn \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.11. \n\n\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0430 \u043d\u0430 \u0432\u0435\u0431-\u0441\u043a\u0440\u0430\u043f\u0438\u043d\u0433\u0435, \u0447\u0442\u043e \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043e. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e YAML \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Deutsche Bahn \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.11. \n\n\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0430 \u043d\u0430 \u0432\u0435\u0431-\u0441\u043a\u0440\u0430\u043f\u0438\u043d\u0433\u0435, \u0447\u0442\u043e \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043e. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Deutsche Bahn \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } } diff --git a/homeassistant/components/escea/translations/hu.json b/homeassistant/components/escea/translations/hu.json new file mode 100644 index 00000000000..015057fd92d --- /dev/null +++ b/homeassistant/components/escea/translations/hu.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "step": { + "confirm": { + "description": "Szeretne Escea kandall\u00f3t be\u00e1ll\u00edtani?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/it.json b/homeassistant/components/escea/translations/it.json new file mode 100644 index 00000000000..047fc1d0ff1 --- /dev/null +++ b/homeassistant/components/escea/translations/it.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/no.json b/homeassistant/components/escea/translations/no.json new file mode 100644 index 00000000000..1058194709f --- /dev/null +++ b/homeassistant/components/escea/translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "step": { + "confirm": { + "description": "\u00d8nsker du \u00e5 sette opp en Escea peis?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/ru.json b/homeassistant/components/escea/translations/ru.json new file mode 100644 index 00000000000..eda098cfc2b --- /dev/null +++ b/homeassistant/components/escea/translations/ru.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "step": { + "confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043a\u0430\u043c\u0438\u043d Escea?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/it.json b/homeassistant/components/flunearyou/translations/it.json index af43ada9596..4b3bd589325 100644 --- a/homeassistant/components/flunearyou/translations/it.json +++ b/homeassistant/components/flunearyou/translations/it.json @@ -16,5 +16,17 @@ "title": "Configurare Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "title": "Rimuovi Flu Near You" + } + } + }, + "title": "Flu Near You non \u00e8 pi\u00f9 disponibile" + } } } \ No newline at end of file diff --git a/homeassistant/components/google/translations/ru.json b/homeassistant/components/google/translations/ru.json index 57a6791cedf..be7b92a707c 100644 --- a/homeassistant/components/google/translations/ru.json +++ b/homeassistant/components/google/translations/ru.json @@ -35,7 +35,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Google Calendar \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u044b. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Google Calendar \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Google Calendar \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" }, "removed_track_new_yaml": { diff --git a/homeassistant/components/justnimbus/translations/hu.json b/homeassistant/components/justnimbus/translations/hu.json new file mode 100644 index 00000000000..8b141e6a2f5 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "client_id": "\u00dcgyf\u00e9lazonos\u00edt\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/it.json b/homeassistant/components/justnimbus/translations/it.json new file mode 100644 index 00000000000..0824bb1be49 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore inaspettato" + }, + "step": { + "user": { + "data": { + "client_id": "ID Cliente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/no.json b/homeassistant/components/justnimbus/translations/no.json new file mode 100644 index 00000000000..e8a0a650d99 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "client_id": "Klient ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/ru.json b/homeassistant/components/justnimbus/translations/ru.json new file mode 100644 index 00000000000..719ec98c326 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "client_id": "ID \u043a\u043b\u0438\u0435\u043d\u0442\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/zh-Hant.json b/homeassistant/components/justnimbus/translations/zh-Hant.json new file mode 100644 index 00000000000..27986db1a84 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "client_id": "\u5ba2\u6236\u7aef ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/ru.json b/homeassistant/components/lg_soundbar/translations/ru.json index f7961eb2e7e..38f8ad9f92a 100644 --- a/homeassistant/components/lg_soundbar/translations/ru.json +++ b/homeassistant/components/lg_soundbar/translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430." }, "error": { diff --git a/homeassistant/components/lyric/translations/ru.json b/homeassistant/components/lyric/translations/ru.json index 536f1a9c0cc..98fd17ed407 100644 --- a/homeassistant/components/lyric/translations/ru.json +++ b/homeassistant/components/lyric/translations/ru.json @@ -20,7 +20,7 @@ }, "issues": { "removed_yaml": { - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"Honeywell Lyric\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"Honeywell Lyric\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Honeywell Lyric \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } } diff --git a/homeassistant/components/miflora/translations/ru.json b/homeassistant/components/miflora/translations/ru.json index b5bf7bfd3c1..8c3a3fcbdb5 100644 --- a/homeassistant/components/miflora/translations/ru.json +++ b/homeassistant/components/miflora/translations/ru.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Mi Flora\" \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u043b\u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.7 \u0438 \u0431\u044b\u043b\u0430 \u0437\u0430\u043c\u0435\u043d\u0435\u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439 \"Xiaomi BLE\" \u0432 \u0432\u0435\u0440\u0441\u0438\u0438 2022.8.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Mi Flora \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043d\u043e\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.\n\n\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \"Mi Flora\" \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Mi Flora\" \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u043b\u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.7 \u0438 \u0431\u044b\u043b\u0430 \u0437\u0430\u043c\u0435\u043d\u0435\u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439 \"Xiaomi BLE\" \u0432 \u0432\u0435\u0440\u0441\u0438\u0438 2022.8.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Mi Flora \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043d\u043e\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.\n\n\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Mi Flora\" \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } } diff --git a/homeassistant/components/mitemp_bt/translations/ru.json b/homeassistant/components/mitemp_bt/translations/ru.json index e25532777b8..34620d5d689 100644 --- a/homeassistant/components/mitemp_bt/translations/ru.json +++ b/homeassistant/components/mitemp_bt/translations/ru.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Xiaomi Mijia BLE Temperature and Humidity Sensor\" \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u043b\u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.7 \u0438 \u0437\u0430\u043c\u0435\u043d\u0435\u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439 \"Xiaomi BLE\" \u0432 \u0432\u0435\u0440\u0441\u0438\u0438 2022.8.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Xiaomi Mijia BLE \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043d\u043e\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.\n\n\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \"Xiaomi Mijia BLE Temperature and Humidity Sensor\" \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Xiaomi Mijia BLE Temperature and Humidity Sensor\" \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u043b\u0430 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.7 \u0438 \u0437\u0430\u043c\u0435\u043d\u0435\u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0435\u0439 \"Xiaomi BLE\" \u0432 \u0432\u0435\u0440\u0441\u0438\u0438 2022.8.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Xiaomi Mijia BLE \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043d\u043e\u0432\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.\n\n\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Xiaomi Mijia BLE Temperature and Humidity Sensor\" \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } } diff --git a/homeassistant/components/mysensors/translations/hu.json b/homeassistant/components/mysensors/translations/hu.json index e1d9c10b053..5253dd7b427 100644 --- a/homeassistant/components/mysensors/translations/hu.json +++ b/homeassistant/components/mysensors/translations/hu.json @@ -14,6 +14,7 @@ "invalid_serial": "\u00c9rv\u00e9nytelen soros port", "invalid_subscribe_topic": "\u00c9rv\u00e9nytelen feliratkoz\u00e1si (subscribe) topik", "invalid_version": "\u00c9rv\u00e9nytelen MySensors verzi\u00f3", + "mqtt_required": "Az MQTT integr\u00e1ci\u00f3 nincs be\u00e1ll\u00edtva", "not_a_number": "Adj meg egy sz\u00e1mot.", "port_out_of_range": "A portsz\u00e1mnak legal\u00e1bb 1-nek \u00e9s legfeljebb 65535-nek kell lennie", "same_topic": "A feliratkoz\u00e1s \u00e9s a k\u00f6zz\u00e9t\u00e9tel t\u00e9m\u00e1i ugyanazok", @@ -68,6 +69,14 @@ }, "description": "Ethernet \u00e1tj\u00e1r\u00f3 be\u00e1ll\u00edt\u00e1sa" }, + "select_gateway_type": { + "description": "V\u00e1lassza ki a konfigur\u00e1land\u00f3 \u00e1tj\u00e1r\u00f3t.", + "menu_options": { + "gw_mqtt": "MQTT \u00e1tj\u00e1r\u00f3 konfigur\u00e1l\u00e1sa", + "gw_serial": "Soros-port \u00e1tj\u00e1r\u00f3 konfigur\u00e1l\u00e1sa", + "gw_tcp": "TCP-\u00e1tj\u00e1r\u00f3 konfigur\u00e1l\u00e1sa" + } + }, "user": { "data": { "gateway_type": "\u00c1tj\u00e1r\u00f3 t\u00edpusa" diff --git a/homeassistant/components/mysensors/translations/it.json b/homeassistant/components/mysensors/translations/it.json index 0a16a4f045c..0f4a2746790 100644 --- a/homeassistant/components/mysensors/translations/it.json +++ b/homeassistant/components/mysensors/translations/it.json @@ -14,6 +14,7 @@ "invalid_serial": "Porta seriale non valida", "invalid_subscribe_topic": "Argomento di sottoscrizione non valido", "invalid_version": "Versione di MySensors non valida", + "mqtt_required": "L'integrazione MQTT non \u00e8 configurata", "not_a_number": "Digita un numero", "port_out_of_range": "Il numero di porta deve essere almeno 1 e al massimo 65535", "same_topic": "Gli argomenti di sottoscrizione e pubblicazione sono gli stessi", @@ -68,6 +69,14 @@ }, "description": "Configurazione del gateway Ethernet" }, + "select_gateway_type": { + "description": "Seleziona quale gateway configurare.", + "menu_options": { + "gw_mqtt": "Configura un gateway MQTT", + "gw_serial": "Configura un gateway seriale", + "gw_tcp": "Configura un gateway TCP" + } + }, "user": { "data": { "gateway_type": "Tipo di gateway" diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index 71721cda936..44295514840 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -99,7 +99,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Nest \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10.\n\n\u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u044b. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Nest \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Nest \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" }, "removed_app_auth": { diff --git a/homeassistant/components/openalpr_local/translations/ru.json b/homeassistant/components/openalpr_local/translations/ru.json index 180151e5fd5..171aaa8a5c9 100644 --- a/homeassistant/components/openalpr_local/translations/ru.json +++ b/homeassistant/components/openalpr_local/translations/ru.json @@ -1,7 +1,7 @@ { "issues": { "pending_removal": { - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f OpenALPR Local \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e YAML \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f OpenALPR Local \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f OpenALPR Local \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } } diff --git a/homeassistant/components/openexchangerates/translations/hu.json b/homeassistant/components/openexchangerates/translations/hu.json new file mode 100644 index 00000000000..83f3ebae2b3 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/hu.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "timeout_connect": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "timeout_connect": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "api_key": "API kulcs", + "base": "Alapdeviza" + }, + "data_description": { + "base": "Az USD-n k\u00edv\u00fcli m\u00e1sik alapdeviza haszn\u00e1lat\u00e1hoz [fizet\u0151s csomag]({signup}) sz\u00fcks\u00e9ges." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Az Open Exchange Rates konfigur\u00e1l\u00e1sa YAML haszn\u00e1lat\u00e1val elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "Az Open Exchange Rates YAML-konfigur\u00e1ci\u00f3ja elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/it.json b/homeassistant/components/openexchangerates/translations/it.json new file mode 100644 index 00000000000..d42f7122593 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/it.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "timeout_connect": "Tempo scaduto per stabile la connessione." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "timeout_connect": "Tempo scaduto per stabile la connessione.", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "base": "Valuta di base" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Open Exchange Rates tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Open Exchange Rates dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/ru.json b/homeassistant/components/openexchangerates/translations/ru.json new file mode 100644 index 00000000000..1707c8e8646 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/ru.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", + "timeout_connect": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "timeout_connect": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "base": "\u0411\u0430\u0437\u043e\u0432\u0430\u044f \u0432\u0430\u043b\u044e\u0442\u0430" + }, + "data_description": { + "base": "\u0414\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0431\u0430\u0437\u043e\u0432\u043e\u0439 \u0432\u0430\u043b\u044e\u0442\u044b, \u043e\u0442\u043b\u0438\u0447\u043d\u043e\u0439 \u043e\u0442 USD, \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f [\u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0430]({signup})." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Open Exchange Rates \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Open Exchange Rates \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/ru.json b/homeassistant/components/radiotherm/translations/ru.json index 6d4615f9820..e052a1653ea 100644 --- a/homeassistant/components/radiotherm/translations/ru.json +++ b/homeassistant/components/radiotherm/translations/ru.json @@ -21,7 +21,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Radio Thermostat \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Radio Thermostat \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Radio Thermostat \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } }, diff --git a/homeassistant/components/recorder/translations/es.json b/homeassistant/components/recorder/translations/es.json index 86bdd0abec8..e15c071240e 100644 --- a/homeassistant/components/recorder/translations/es.json +++ b/homeassistant/components/recorder/translations/es.json @@ -4,7 +4,7 @@ "current_recorder_run": "Hora de inicio de la ejecuci\u00f3n actual", "database_engine": "Motor de la base de datos", "database_version": "Versi\u00f3n de la base de datos", - "estimated_db_size": "Mida estimada de la base de datos (MiB)", + "estimated_db_size": "Tama\u00f1o estimado de la base de datos (MiB)", "oldest_recorder_run": "Hora de inicio de ejecuci\u00f3n m\u00e1s antigua" } } diff --git a/homeassistant/components/senz/translations/ru.json b/homeassistant/components/senz/translations/ru.json index 21c6b830841..0c8f912d2fe 100644 --- a/homeassistant/components/senz/translations/ru.json +++ b/homeassistant/components/senz/translations/ru.json @@ -19,7 +19,7 @@ }, "issues": { "removed_yaml": { - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"nVent RAYCHEM SENZ\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \"nVent RAYCHEM SENZ\" \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 nVent RAYCHEM SENZ \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } } diff --git a/homeassistant/components/simplepush/translations/it.json b/homeassistant/components/simplepush/translations/it.json index b3a9b44f938..be311f7e0c3 100644 --- a/homeassistant/components/simplepush/translations/it.json +++ b/homeassistant/components/simplepush/translations/it.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "La configurazione di Simplepush tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Simplepush dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "La configurazione YAML di Simplepush sar\u00e0 rimossa" + }, + "removed_yaml": { + "description": "La configurazione di Simplepush tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non \u00e8 utilizzata da Home Assistant. \n\nRimuovi la configurazione YAML di Simplepush dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Simplepush \u00e8 stata rimossa" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/ru.json b/homeassistant/components/simplepush/translations/ru.json index e615a4c4c72..1e7a61fb1cb 100644 --- a/homeassistant/components/simplepush/translations/ru.json +++ b/homeassistant/components/simplepush/translations/ru.json @@ -20,11 +20,11 @@ }, "issues": { "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" }, "removed_yaml": { - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Simplepush \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Simplepush \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Simplepush \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } } diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index ba75954ce71..6f2a61e2df4 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "Account gi\u00e0 registrato", "invalid_auth": "Autenticazione non valida", + "invalid_auth_code_length": "I codici di autorizzazione SimpliSafe sono lunghi 45 caratteri", "unknown": "Errore imprevisto" }, "progress": { diff --git a/homeassistant/components/simplisafe/translations/ru.json b/homeassistant/components/simplisafe/translations/ru.json index 863a67c3b89..0d09bcd0faf 100644 --- a/homeassistant/components/simplisafe/translations/ru.json +++ b/homeassistant/components/simplisafe/translations/ru.json @@ -35,7 +35,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" }, - "description": "SimpliSafe \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u0442 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0447\u0435\u0440\u0435\u0437 \u0441\u0432\u043e\u0435 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. \u0418\u0437-\u0437\u0430 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0439, \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u043e\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u043c \u0432\u0440\u0443\u0447\u043d\u0443\u044e. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) \u043f\u0435\u0440\u0435\u0434 \u0437\u0430\u043f\u0443\u0441\u043a\u043e\u043c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \n\n\u041a\u043e\u0433\u0434\u0430 \u0412\u044b \u0431\u0443\u0434\u0435\u0442\u0435 \u0433\u043e\u0442\u043e\u0432\u044b, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 [\u0441\u044e\u0434\u0430]({url}), \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 SimpliSafe \u0438 \u0432\u0432\u0435\u0441\u0442\u0438 \u0441\u0432\u043e\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435. \u041a\u043e\u0433\u0434\u0430 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d, \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f SimpliSafe." + "description": "SimpliSafe \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u0442 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0447\u0435\u0440\u0435\u0437 \u0441\u0432\u043e\u0435 \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435. \u0418\u0437-\u0437\u0430 \u0442\u0435\u0445\u043d\u0438\u0447\u0435\u0441\u043a\u0438\u0445 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0439, \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u0435 \u044d\u0442\u043e\u0433\u043e \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u043e\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u043c \u0432\u0440\u0443\u0447\u043d\u0443\u044e. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) \u043f\u0435\u0440\u0435\u0434 \u0437\u0430\u043f\u0443\u0441\u043a\u043e\u043c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438. \n\n\u041a\u043e\u0433\u0434\u0430 \u0412\u044b \u0431\u0443\u0434\u0435\u0442\u0435 \u0433\u043e\u0442\u043e\u0432\u044b, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 [\u0441\u044e\u0434\u0430]({url}), \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u0432\u0435\u0431-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 SimpliSafe \u0438 \u0432\u0432\u0435\u0441\u0442\u0438 \u0441\u0432\u043e\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435. \u0415\u0441\u043b\u0438 \u0412\u044b \u0443\u0436\u0435 \u0432\u043e\u0448\u043b\u0438 \u0432 SimpliSafe \u0432 \u0441\u0432\u043e\u0435\u043c \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0435, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043e\u0442\u043a\u0440\u044b\u0442\u044c \u043d\u043e\u0432\u0443\u044e \u0432\u043a\u043b\u0430\u0434\u043a\u0443, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0432\u044b\u0448\u0435\u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.\n\n\u041a\u043e\u0433\u0434\u0430 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0431\u0443\u0434\u0435\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d, \u0432\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441 URL-\u0430\u0434\u0440\u0435\u0441\u0430 `com.simplisafe.mobile`." } } }, diff --git a/homeassistant/components/solaredge/translations/it.json b/homeassistant/components/solaredge/translations/it.json index f89c85eeaee..8bf3efce908 100644 --- a/homeassistant/components/solaredge/translations/it.json +++ b/homeassistant/components/solaredge/translations/it.json @@ -14,7 +14,7 @@ "data": { "api_key": "Chiave API", "name": "Il nome di questa installazione", - "site_id": "Il sito-id di SolarEdge" + "site_id": "Il site-id di SolarEdge" }, "title": "Definisci i parametri API per questa installazione" } diff --git a/homeassistant/components/soundtouch/translations/ru.json b/homeassistant/components/soundtouch/translations/ru.json index 318fe8abef6..62fa8df84ad 100644 --- a/homeassistant/components/soundtouch/translations/ru.json +++ b/homeassistant/components/soundtouch/translations/ru.json @@ -20,7 +20,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Bose SoundTouch \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Bose SoundTouch \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Bose SoundTouch \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } } diff --git a/homeassistant/components/steam_online/translations/ru.json b/homeassistant/components/steam_online/translations/ru.json index 828b66f1dc8..48ec5e0c944 100644 --- a/homeassistant/components/steam_online/translations/ru.json +++ b/homeassistant/components/steam_online/translations/ru.json @@ -26,7 +26,7 @@ }, "issues": { "removed_yaml": { - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Steam \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f YAML \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Steam \u0442\u0435\u043f\u0435\u0440\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f Home Assistant. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0423\u0434\u0430\u043b\u0435\u043d\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Steam \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML" } }, diff --git a/homeassistant/components/unifiprotect/translations/hu.json b/homeassistant/components/unifiprotect/translations/hu.json index 645ec1f8c90..d9162f74a91 100644 --- a/homeassistant/components/unifiprotect/translations/hu.json +++ b/homeassistant/components/unifiprotect/translations/hu.json @@ -47,6 +47,7 @@ "data": { "all_updates": "Val\u00f3s idej\u0171 m\u00e9r\u0151sz\u00e1mok (FIGYELEM: nagym\u00e9rt\u00e9kben n\u00f6veli a CPU terhel\u00e9st)", "disable_rtsp": "Az RTSP adatfolyam letilt\u00e1sa", + "max_media": "A m\u00e9diab\u00f6ng\u00e9sz\u0151be bet\u00f6ltend\u0151 esem\u00e9nyek maxim\u00e1lis sz\u00e1ma (n\u00f6veli a RAM-haszn\u00e1latot)", "override_connection_host": "Kapcsolat c\u00edm\u00e9nek fel\u00fclb\u00edr\u00e1l\u00e1sa" }, "description": "A Val\u00f3s idej\u0171 m\u00e9r\u0151sz\u00e1mokat csak akkor javasolt haszn\u00e1lni, ha enged\u00e9lyezte a diagnosztikai \u00e9rz\u00e9kel\u0151ket, \u00e9s szeretn\u00e9, hogy azok val\u00f3s id\u0151ben friss\u00fcljenek. Ha nincs enged\u00e9lyezve, akkor csak 15 percenk\u00e9nt friss\u00fclnek.", diff --git a/homeassistant/components/unifiprotect/translations/it.json b/homeassistant/components/unifiprotect/translations/it.json index 8ea65342e78..00592e72ea1 100644 --- a/homeassistant/components/unifiprotect/translations/it.json +++ b/homeassistant/components/unifiprotect/translations/it.json @@ -47,6 +47,7 @@ "data": { "all_updates": "Metriche in tempo reale (ATTENZIONE: aumenta notevolmente l'utilizzo della CPU)", "disable_rtsp": "Disabilita il flusso RTSP", + "max_media": "Numero massimo di eventi da caricare per Media Browser (aumenta l'utilizzo della RAM)", "override_connection_host": "Sostituisci host di connessione" }, "description": "L'opzione delle metriche in tempo reale dovrebbe essere abilitata solo se hai abilitato i sensori di diagnostica e desideri che vengano aggiornati in tempo reale. Se non sono abilitati, si aggiorneranno solo una volta ogni 15 minuti.", diff --git a/homeassistant/components/unifiprotect/translations/no.json b/homeassistant/components/unifiprotect/translations/no.json index e11ed432313..947d5c76887 100644 --- a/homeassistant/components/unifiprotect/translations/no.json +++ b/homeassistant/components/unifiprotect/translations/no.json @@ -47,6 +47,7 @@ "data": { "all_updates": "Sanntidsm\u00e5linger (ADVARSEL: \u00d8ker CPU-bruken betraktelig)", "disable_rtsp": "Deaktiver RTSP-str\u00f8mmen", + "max_media": "Maks antall hendelser som skal lastes for medienettleseren (\u00f8ker RAM-bruken)", "override_connection_host": "Overstyr tilkoblingsvert" }, "description": "Alternativet sanntidsm\u00e5linger b\u00f8r bare aktiveres hvis du har aktivert diagnostikksensorene og vil ha dem oppdatert i sanntid. Hvis den ikke er aktivert, vil de bare oppdatere en gang hvert 15. minutt.", diff --git a/homeassistant/components/unifiprotect/translations/ru.json b/homeassistant/components/unifiprotect/translations/ru.json index 2e06e887cf4..e81404f2a95 100644 --- a/homeassistant/components/unifiprotect/translations/ru.json +++ b/homeassistant/components/unifiprotect/translations/ru.json @@ -47,6 +47,7 @@ "data": { "all_updates": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0438 \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0438 (\u0412\u041d\u0418\u041c\u0410\u041d\u0418\u0415: \u0437\u043d\u0430\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0443 \u043d\u0430 \u0426\u041f)", "disable_rtsp": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u043e\u0442\u043e\u043a RTSP", + "max_media": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u043c\u044b\u0445 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0434\u043b\u044f \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430 \u043c\u0443\u043b\u044c\u0442\u0438\u043c\u0435\u0434\u0438\u0430 (\u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043e\u043f\u0435\u0440\u0430\u0442\u0438\u0432\u043d\u043e\u0439 \u043f\u0430\u043c\u044f\u0442\u0438)", "override_connection_host": "\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0443\u0437\u0435\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" }, "description": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 '\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0438 \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0438' \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0432\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u0432 \u0442\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435, \u0435\u0441\u043b\u0438 \u0412\u044b \u0432\u043a\u043b\u044e\u0447\u0438\u043b\u0438 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0434\u0438\u0430\u0433\u043d\u043e\u0441\u0442\u0438\u043a\u0438 \u0438 \u0445\u043e\u0442\u0438\u0442\u0435, \u0447\u0442\u043e\u0431\u044b \u043e\u043d\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u043b\u0438\u0441\u044c \u0432 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0432\u0440\u0435\u043c\u0435\u043d\u0438. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u0434\u0435\u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u043d, \u043e\u043d\u0438 \u0431\u0443\u0434\u0443\u0442 \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u0440\u0430\u0437 \u0432 15 \u043c\u0438\u043d\u0443\u0442.", diff --git a/homeassistant/components/unifiprotect/translations/zh-Hant.json b/homeassistant/components/unifiprotect/translations/zh-Hant.json index d0c23849e12..ba996c5e123 100644 --- a/homeassistant/components/unifiprotect/translations/zh-Hant.json +++ b/homeassistant/components/unifiprotect/translations/zh-Hant.json @@ -47,6 +47,7 @@ "data": { "all_updates": "\u5373\u6642\u6307\u6a19\uff08\u8b66\u544a\uff1a\u5927\u91cf\u63d0\u5347 CPU \u4f7f\u7528\u7387\uff09", "disable_rtsp": "\u95dc\u9589 RTSP \u4e32\u6d41", + "max_media": "\u5a92\u9ad4\u700f\u89bd\u5668\u6700\u9ad8\u8f09\u5165\u4e8b\u4ef6\u6578\uff08\u589e\u52a0\u8a18\u61b6\u9ad4\u4f7f\u7528\uff09", "override_connection_host": "\u7f6e\u63db\u9023\u7dda\u4e3b\u6a5f\u7aef" }, "description": "\u50c5\u6709\u7576\u958b\u555f\u8a3a\u65b7\u611f\u6e2c\u5668\u3001\u4e26\u9700\u8981\u5373\u6642\u66f4\u65b0\u6642\uff0c\u624d\u5efa\u8b70\u958b\u555f\u5373\u6642\u6307\u6a19\u9078\u9805\u3002\u672a\u555f\u7528\u72c0\u6cc1\u4e0b\u70ba\u6bcf 15 \u5206\u9418\u66f4\u65b0\u4e00\u6b21\u3002", diff --git a/homeassistant/components/uscis/translations/ru.json b/homeassistant/components/uscis/translations/ru.json index f3b70020245..d6cd9da954e 100644 --- a/homeassistant/components/uscis/translations/ru.json +++ b/homeassistant/components/uscis/translations/ru.json @@ -1,7 +1,7 @@ { "issues": { "pending_removal": { - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0421\u043b\u0443\u0436\u0431\u044b \u0433\u0440\u0430\u0436\u0434\u0430\u043d\u0441\u0442\u0432\u0430 \u0438 \u0438\u043c\u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0421\u0428\u0410 (USCIS) \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10. \n\n\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0430 \u043d\u0430 \u0432\u0435\u0431-\u0441\u043a\u0440\u0430\u043f\u0438\u043d\u0433\u0435, \u0447\u0442\u043e \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043e. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e YAML \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0421\u043b\u0443\u0436\u0431\u044b \u0433\u0440\u0430\u0436\u0434\u0430\u043d\u0441\u0442\u0432\u0430 \u0438 \u0438\u043c\u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0421\u0428\u0410 (USCIS) \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10. \n\n\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0430 \u043d\u0430 \u0432\u0435\u0431-\u0441\u043a\u0440\u0430\u043f\u0438\u043d\u0433\u0435, \u0447\u0442\u043e \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d\u043e. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f USCIS \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } } diff --git a/homeassistant/components/xbox/translations/ru.json b/homeassistant/components/xbox/translations/ru.json index ca47fd08c8c..1fddc4e3ade 100644 --- a/homeassistant/components/xbox/translations/ru.json +++ b/homeassistant/components/xbox/translations/ru.json @@ -16,7 +16,7 @@ }, "issues": { "deprecated_yaml": { - "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Xbox \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u044b. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Xbox \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.9.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Xbox \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" } } From 19295d33ba60bad0a9dfbc4d512e156a09a010fc Mon Sep 17 00:00:00 2001 From: Artem Draft Date: Wed, 10 Aug 2022 14:11:49 +0300 Subject: [PATCH 3250/3516] Migrate BraviaTV to new async backend (#75727) --- .coveragerc | 1 + homeassistant/components/braviatv/__init__.py | 251 +---------------- .../components/braviatv/config_flow.py | 123 ++++----- homeassistant/components/braviatv/const.py | 1 - .../components/braviatv/coordinator.py | 258 ++++++++++++++++++ homeassistant/components/braviatv/entity.py | 2 +- .../components/braviatv/manifest.json | 4 +- .../components/braviatv/media_player.py | 13 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/braviatv/test_config_flow.py | 83 +++--- 11 files changed, 385 insertions(+), 363 deletions(-) create mode 100644 homeassistant/components/braviatv/coordinator.py diff --git a/.coveragerc b/.coveragerc index a94b2a8babc..7e60c9ae891 100644 --- a/.coveragerc +++ b/.coveragerc @@ -139,6 +139,7 @@ omit = homeassistant/components/bosch_shc/switch.py homeassistant/components/braviatv/__init__.py homeassistant/components/braviatv/const.py + homeassistant/components/braviatv/coordinator.py homeassistant/components/braviatv/entity.py homeassistant/components/braviatv/media_player.py homeassistant/components/braviatv/remote.py diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index e1d90681d2a..539dd980ffc 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -1,27 +1,20 @@ -"""The Bravia TV component.""" +"""The Bravia TV integration.""" from __future__ import annotations -import asyncio -from collections.abc import Iterable -from datetime import timedelta -import logging from typing import Final -from bravia_tv import BraviaRC -from bravia_tv.braviarc import NoIPControl +from aiohttp import CookieJar +from pybravia import BraviaTV from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.aiohttp_client import async_create_clientsession -from .const import CLIENTID_PREFIX, CONF_IGNORED_SOURCES, DOMAIN, NICKNAME - -_LOGGER = logging.getLogger(__name__) +from .const import CONF_IGNORED_SOURCES, DOMAIN +from .coordinator import BraviaTVCoordinator PLATFORMS: Final[list[Platform]] = [Platform.MEDIA_PLAYER, Platform.REMOTE] -SCAN_INTERVAL: Final = timedelta(seconds=10) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: @@ -31,7 +24,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b pin = config_entry.data[CONF_PIN] ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES, []) - coordinator = BraviaTVCoordinator(hass, host, mac, pin, ignored_sources) + session = async_create_clientsession( + hass, cookie_jar=CookieJar(unsafe=True, quote_cookie=False) + ) + client = BraviaTV(host, mac, session=session) + coordinator = BraviaTVCoordinator(hass, client, pin, ignored_sources) config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) await coordinator.async_config_entry_first_refresh() @@ -59,229 +56,3 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(config_entry.entry_id) - - -class BraviaTVCoordinator(DataUpdateCoordinator[None]): - """Representation of a Bravia TV Coordinator. - - An instance is used per device to share the same power state between - several platforms. - """ - - def __init__( - self, - hass: HomeAssistant, - host: str, - mac: str, - pin: str, - ignored_sources: list[str], - ) -> None: - """Initialize Bravia TV Client.""" - - self.braviarc = BraviaRC(host, mac) - self.pin = pin - self.ignored_sources = ignored_sources - self.muted: bool = False - self.channel_name: str | None = None - self.media_title: str | None = None - self.source: str | None = None - self.source_list: list[str] = [] - self.original_content_list: list[str] = [] - self.content_mapping: dict[str, str] = {} - self.duration: int | None = None - self.content_uri: str | None = None - self.program_media_type: str | None = None - self.audio_output: str | None = None - self.min_volume: int | None = None - self.max_volume: int | None = None - self.volume_level: float | None = None - self.is_on = False - # Assume that the TV is in Play mode - self.playing = True - self.state_lock = asyncio.Lock() - - super().__init__( - hass, - _LOGGER, - name=DOMAIN, - update_interval=SCAN_INTERVAL, - request_refresh_debouncer=Debouncer( - hass, _LOGGER, cooldown=1.0, immediate=False - ), - ) - - def _send_command(self, command: Iterable[str], repeats: int = 1) -> None: - """Send a command to the TV.""" - for _ in range(repeats): - for cmd in command: - self.braviarc.send_command(cmd) - - def _get_source(self) -> str | None: - """Return the name of the source.""" - for key, value in self.content_mapping.items(): - if value == self.content_uri: - return key - return None - - def _refresh_volume(self) -> bool: - """Refresh volume information.""" - volume_info = self.braviarc.get_volume_info(self.audio_output) - if volume_info is not None: - volume = volume_info.get("volume") - self.volume_level = volume / 100 if volume is not None else None - self.audio_output = volume_info.get("target") - self.min_volume = volume_info.get("minVolume") - self.max_volume = volume_info.get("maxVolume") - self.muted = volume_info.get("mute", False) - return True - return False - - def _refresh_channels(self) -> bool: - """Refresh source and channels list.""" - if not self.source_list: - self.content_mapping = self.braviarc.load_source_list() - self.source_list = [] - if not self.content_mapping: - return False - for key in self.content_mapping: - if key not in self.ignored_sources: - self.source_list.append(key) - return True - - def _refresh_playing_info(self) -> None: - """Refresh playing information.""" - playing_info = self.braviarc.get_playing_info() - program_name = playing_info.get("programTitle") - self.channel_name = playing_info.get("title") - self.program_media_type = playing_info.get("programMediaType") - self.content_uri = playing_info.get("uri") - self.source = self._get_source() - self.duration = playing_info.get("durationSec") - if not playing_info: - self.channel_name = "App" - if self.channel_name is not None: - self.media_title = self.channel_name - if program_name is not None: - self.media_title = f"{self.media_title}: {program_name}" - else: - self.media_title = None - - def _update_tv_data(self) -> None: - """Connect and update TV info.""" - power_status = self.braviarc.get_power_status() - - if power_status != "off": - connected = self.braviarc.is_connected() - if not connected: - try: - connected = self.braviarc.connect( - self.pin, CLIENTID_PREFIX, NICKNAME - ) - except NoIPControl: - _LOGGER.error("IP Control is disabled in the TV settings") - if not connected: - power_status = "off" - - if power_status == "active": - self.is_on = True - if self._refresh_volume() and self._refresh_channels(): - self._refresh_playing_info() - return - - self.is_on = False - - async def _async_update_data(self) -> None: - """Fetch the latest data.""" - if self.state_lock.locked(): - return - - await self.hass.async_add_executor_job(self._update_tv_data) - - async def async_turn_on(self) -> None: - """Turn the device on.""" - async with self.state_lock: - await self.hass.async_add_executor_job(self.braviarc.turn_on) - await self.async_request_refresh() - - async def async_turn_off(self) -> None: - """Turn off device.""" - async with self.state_lock: - await self.hass.async_add_executor_job(self.braviarc.turn_off) - await self.async_request_refresh() - - async def async_set_volume_level(self, volume: float) -> None: - """Set volume level, range 0..1.""" - async with self.state_lock: - await self.hass.async_add_executor_job( - self.braviarc.set_volume_level, volume, self.audio_output - ) - await self.async_request_refresh() - - async def async_volume_up(self) -> None: - """Send volume up command to device.""" - async with self.state_lock: - await self.hass.async_add_executor_job( - self.braviarc.volume_up, self.audio_output - ) - await self.async_request_refresh() - - async def async_volume_down(self) -> None: - """Send volume down command to device.""" - async with self.state_lock: - await self.hass.async_add_executor_job( - self.braviarc.volume_down, self.audio_output - ) - await self.async_request_refresh() - - async def async_volume_mute(self, mute: bool) -> None: - """Send mute command to device.""" - async with self.state_lock: - await self.hass.async_add_executor_job(self.braviarc.mute_volume, mute) - await self.async_request_refresh() - - async def async_media_play(self) -> None: - """Send play command to device.""" - async with self.state_lock: - await self.hass.async_add_executor_job(self.braviarc.media_play) - self.playing = True - await self.async_request_refresh() - - async def async_media_pause(self) -> None: - """Send pause command to device.""" - async with self.state_lock: - await self.hass.async_add_executor_job(self.braviarc.media_pause) - self.playing = False - await self.async_request_refresh() - - async def async_media_stop(self) -> None: - """Send stop command to device.""" - async with self.state_lock: - await self.hass.async_add_executor_job(self.braviarc.media_stop) - self.playing = False - await self.async_request_refresh() - - async def async_media_next_track(self) -> None: - """Send next track command.""" - async with self.state_lock: - await self.hass.async_add_executor_job(self.braviarc.media_next_track) - await self.async_request_refresh() - - async def async_media_previous_track(self) -> None: - """Send previous track command.""" - async with self.state_lock: - await self.hass.async_add_executor_job(self.braviarc.media_previous_track) - await self.async_request_refresh() - - async def async_select_source(self, source: str) -> None: - """Set the input source.""" - if source in self.content_mapping: - uri = self.content_mapping[source] - async with self.state_lock: - await self.hass.async_add_executor_job(self.braviarc.play_content, uri) - await self.async_request_refresh() - - async def async_send_command(self, command: Iterable[str], repeats: int) -> None: - """Send command to device.""" - async with self.state_lock: - await self.hass.async_add_executor_job(self._send_command, command, repeats) - await self.async_request_refresh() diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index 8e59033ffc8..f89880caf89 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -1,4 +1,4 @@ -"""Adds config flow for Bravia TV integration.""" +"""Config flow to configure the Bravia TV integration.""" from __future__ import annotations from contextlib import suppress @@ -6,17 +6,19 @@ import ipaddress import re from typing import Any -from bravia_tv import BraviaRC -from bravia_tv.braviarc import NoIPControl +from aiohttp import CookieJar +from pybravia import BraviaTV, BraviaTVError, BraviaTVNotSupported import voluptuous as vol -from homeassistant import config_entries, exceptions +from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv +from . import BraviaTVCoordinator from .const import ( ATTR_CID, ATTR_MAC, @@ -38,39 +40,15 @@ def host_valid(host: str) -> bool: class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for BraviaTV integration.""" + """Handle a config flow for Bravia TV integration.""" VERSION = 1 + client: BraviaTV + def __init__(self) -> None: - """Initialize.""" - self.braviarc: BraviaRC | None = None - self.host: str | None = None - self.title = "" - self.mac: str | None = None - - async def init_device(self, pin: str) -> None: - """Initialize Bravia TV device.""" - assert self.braviarc is not None - await self.hass.async_add_executor_job( - self.braviarc.connect, pin, CLIENTID_PREFIX, NICKNAME - ) - - connected = await self.hass.async_add_executor_job(self.braviarc.is_connected) - if not connected: - raise CannotConnect() - - system_info = await self.hass.async_add_executor_job( - self.braviarc.get_system_info - ) - if not system_info: - raise ModelNotSupported() - - await self.async_set_unique_id(system_info[ATTR_CID].lower()) - self._abort_if_unique_id_configured() - - self.title = system_info[ATTR_MODEL] - self.mac = system_info[ATTR_MAC] + """Initialize config flow.""" + self.device_config: dict[str, Any] = {} @staticmethod @callback @@ -78,6 +56,24 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Bravia TV options callback.""" return BraviaTVOptionsFlowHandler(config_entry) + async def async_init_device(self) -> FlowResult: + """Initialize and create Bravia TV device from config.""" + pin = self.device_config[CONF_PIN] + + await self.client.connect(pin=pin, clientid=CLIENTID_PREFIX, nickname=NICKNAME) + await self.client.set_wol_mode(True) + + system_info = await self.client.get_system_info() + cid = system_info[ATTR_CID].lower() + title = system_info[ATTR_MODEL] + + self.device_config[CONF_MAC] = system_info[ATTR_MAC] + + await self.async_set_unique_id(cid) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=title, data=self.device_config) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -85,9 +81,14 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: - if host_valid(user_input[CONF_HOST]): - self.host = user_input[CONF_HOST] - self.braviarc = BraviaRC(self.host) + host = user_input[CONF_HOST] + if host_valid(host): + session = async_create_clientsession( + self.hass, + cookie_jar=CookieJar(unsafe=True, quote_cookie=False), + ) + self.client = BraviaTV(host=host, session=session) + self.device_config[CONF_HOST] = host return await self.async_step_authorize() @@ -106,23 +107,17 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: + self.device_config[CONF_PIN] = user_input[CONF_PIN] try: - await self.init_device(user_input[CONF_PIN]) - except CannotConnect: - errors["base"] = "cannot_connect" - except ModelNotSupported: + return await self.async_init_device() + except BraviaTVNotSupported: errors["base"] = "unsupported_model" - else: - user_input[CONF_HOST] = self.host - user_input[CONF_MAC] = self.mac - return self.async_create_entry(title=self.title, data=user_input) - # Connecting with th PIN "0000" to start the pairing process on the TV. + except BraviaTVError: + errors["base"] = "cannot_connect" + try: - assert self.braviarc is not None - await self.hass.async_add_executor_job( - self.braviarc.connect, "0000", CLIENTID_PREFIX, NICKNAME - ) - except NoIPControl: + await self.client.pair(CLIENTID_PREFIX, NICKNAME) + except BraviaTVError: return self.async_abort(reason="no_ip_control") return self.async_show_form( @@ -138,26 +133,20 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry: ConfigEntry) -> None: """Initialize Bravia TV options flow.""" self.config_entry = config_entry - self.pin = config_entry.data[CONF_PIN] self.ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES) - self.source_list: dict[str, str] = {} + self.source_list: list[str] = [] async def async_step_init( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the options.""" - coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id] - braviarc = coordinator.braviarc - connected = await self.hass.async_add_executor_job(braviarc.is_connected) - if not connected: - await self.hass.async_add_executor_job( - braviarc.connect, self.pin, CLIENTID_PREFIX, NICKNAME - ) + coordinator: BraviaTVCoordinator = self.hass.data[DOMAIN][ + self.config_entry.entry_id + ] - content_mapping = await self.hass.async_add_executor_job( - braviarc.load_source_list - ) - self.source_list = {item: item for item in content_mapping} + await coordinator.async_update_sources() + sources = coordinator.source_map.values() + self.source_list = [item["title"] for item in sources] return await self.async_step_user() async def async_step_user( @@ -177,11 +166,3 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow): } ), ) - - -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" - - -class ModelNotSupported(exceptions.HomeAssistantError): - """Error to indicate not supported model.""" diff --git a/homeassistant/components/braviatv/const.py b/homeassistant/components/braviatv/const.py index 4aa44992cbf..6ed8efd3739 100644 --- a/homeassistant/components/braviatv/const.py +++ b/homeassistant/components/braviatv/const.py @@ -10,7 +10,6 @@ ATTR_MODEL: Final = "model" CONF_IGNORED_SOURCES: Final = "ignored_sources" -BRAVIA_CONFIG_FILE: Final = "bravia.conf" CLIENTID_PREFIX: Final = "HomeAssistant" DOMAIN: Final = "braviatv" NICKNAME: Final = "Home Assistant" diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py new file mode 100644 index 00000000000..b5d91263b34 --- /dev/null +++ b/homeassistant/components/braviatv/coordinator.py @@ -0,0 +1,258 @@ +"""Update coordinator for Bravia TV integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable, Coroutine, Iterable +from datetime import timedelta +from functools import wraps +import logging +from typing import Any, Final, TypeVar + +from pybravia import BraviaTV, BraviaTVError +from typing_extensions import Concatenate, ParamSpec + +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_APP, + MEDIA_TYPE_CHANNEL, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import CLIENTID_PREFIX, DOMAIN, NICKNAME + +_BraviaTVCoordinatorT = TypeVar("_BraviaTVCoordinatorT", bound="BraviaTVCoordinator") +_P = ParamSpec("_P") +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL: Final = timedelta(seconds=10) + + +def catch_braviatv_errors( + func: Callable[Concatenate[_BraviaTVCoordinatorT, _P], Awaitable[None]] +) -> Callable[Concatenate[_BraviaTVCoordinatorT, _P], Coroutine[Any, Any, None]]: + """Catch BraviaTV errors.""" + + @wraps(func) + async def wrapper( + self: _BraviaTVCoordinatorT, + *args: _P.args, + **kwargs: _P.kwargs, + ) -> None: + """Catch BraviaTV errors and log message.""" + try: + await func(self, *args, **kwargs) + except BraviaTVError as err: + _LOGGER.error("Command error: %s", err) + await self.async_request_refresh() + + return wrapper + + +class BraviaTVCoordinator(DataUpdateCoordinator[None]): + """Representation of a Bravia TV Coordinator.""" + + def __init__( + self, + hass: HomeAssistant, + client: BraviaTV, + pin: str, + ignored_sources: list[str], + ) -> None: + """Initialize Bravia TV Client.""" + + self.client = client + self.pin = pin + self.ignored_sources = ignored_sources + self.source: str | None = None + self.source_list: list[str] = [] + self.source_map: dict[str, dict] = {} + self.media_title: str | None = None + self.media_content_id: str | None = None + self.media_content_type: str | None = None + self.media_uri: str | None = None + self.media_duration: int | None = None + self.volume_level: float | None = None + self.volume_target: str | None = None + self.volume_muted = False + self.is_on = False + self.is_channel = False + self.connected = False + # Assume that the TV is in Play mode + self.playing = True + + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + request_refresh_debouncer=Debouncer( + hass, _LOGGER, cooldown=1.0, immediate=False + ), + ) + + def _sources_extend(self, sources: list[dict], source_type: str) -> None: + """Extend source map and source list.""" + for item in sources: + item["type"] = source_type + title = item.get("title") + uri = item.get("uri") + if not title or not uri: + continue + self.source_map[uri] = item + if title not in self.ignored_sources: + self.source_list.append(title) + + async def _async_update_data(self) -> None: + """Connect and fetch data.""" + try: + if not self.connected: + await self.client.connect( + pin=self.pin, clientid=CLIENTID_PREFIX, nickname=NICKNAME + ) + self.connected = True + + power_status = await self.client.get_power_status() + self.is_on = power_status == "active" + + if self.is_on is False: + return + + if not self.source_map: + await self.async_update_sources() + await self.async_update_volume() + await self.async_update_playing() + except BraviaTVError as err: + self.is_on = False + self.connected = False + raise UpdateFailed("Error communicating with device") from err + + async def async_update_sources(self) -> None: + """Update sources.""" + self.source_list = [] + self.source_map = {} + + externals = await self.client.get_external_status() + self._sources_extend(externals, "input") + + apps = await self.client.get_app_list() + self._sources_extend(apps, "app") + + channels = await self.client.get_content_list_all("tv") + self._sources_extend(channels, "channel") + + async def async_update_volume(self) -> None: + """Update volume information.""" + volume_info = await self.client.get_volume_info() + volume_level = volume_info.get("volume") + if volume_level is not None: + self.volume_level = volume_level / 100 + self.volume_muted = volume_info.get("mute", False) + self.volume_target = volume_info.get("target") + + async def async_update_playing(self) -> None: + """Update current playing information.""" + playing_info = await self.client.get_playing_info() + self.media_title = playing_info.get("title") + self.media_uri = playing_info.get("uri") + self.media_duration = playing_info.get("durationSec") + if program_title := playing_info.get("programTitle"): + self.media_title = f"{self.media_title}: {program_title}" + if self.media_uri: + source = self.source_map.get(self.media_uri, {}) + self.source = source.get("title") + self.is_channel = self.media_uri[:2] == "tv" + if self.is_channel: + self.media_content_id = playing_info.get("dispNum") + self.media_content_type = MEDIA_TYPE_CHANNEL + else: + self.media_content_id = self.media_uri + self.media_content_type = None + else: + self.source = None + self.is_channel = False + self.media_content_id = None + self.media_content_type = None + if not playing_info: + self.media_title = "Smart TV" + self.media_content_type = MEDIA_TYPE_APP + + @catch_braviatv_errors + async def async_turn_on(self) -> None: + """Turn the device on.""" + await self.client.turn_on() + + @catch_braviatv_errors + async def async_turn_off(self) -> None: + """Turn off device.""" + await self.client.turn_off() + + @catch_braviatv_errors + async def async_set_volume_level(self, volume: float) -> None: + """Set volume level, range 0..1.""" + await self.client.volume_level(round(volume * 100)) + + @catch_braviatv_errors + async def async_volume_up(self) -> None: + """Send volume up command to device.""" + await self.client.volume_up() + + @catch_braviatv_errors + async def async_volume_down(self) -> None: + """Send volume down command to device.""" + await self.client.volume_down() + + @catch_braviatv_errors + async def async_volume_mute(self, mute: bool) -> None: + """Send mute command to device.""" + await self.client.volume_mute() + + @catch_braviatv_errors + async def async_media_play(self) -> None: + """Send play command to device.""" + await self.client.play() + self.playing = True + + @catch_braviatv_errors + async def async_media_pause(self) -> None: + """Send pause command to device.""" + await self.client.pause() + self.playing = False + + @catch_braviatv_errors + async def async_media_stop(self) -> None: + """Send stop command to device.""" + await self.client.stop() + + @catch_braviatv_errors + async def async_media_next_track(self) -> None: + """Send next track command.""" + if self.is_channel: + await self.client.channel_up() + else: + await self.client.next_track() + + @catch_braviatv_errors + async def async_media_previous_track(self) -> None: + """Send previous track command.""" + if self.is_channel: + await self.client.channel_down() + else: + await self.client.previous_track() + + @catch_braviatv_errors + async def async_select_source(self, source: str) -> None: + """Set the input source.""" + for uri, item in self.source_map.items(): + if item.get("title") == source: + if item.get("type") == "app": + await self.client.set_active_app(uri) + else: + await self.client.set_play_content(uri) + break + + @catch_braviatv_errors + async def async_send_command(self, command: Iterable[str], repeats: int) -> None: + """Send command to device.""" + for _ in range(repeats): + for cmd in command: + await self.client.send_command(cmd) diff --git a/homeassistant/components/braviatv/entity.py b/homeassistant/components/braviatv/entity.py index ad896ae8c5a..a947513e713 100644 --- a/homeassistant/components/braviatv/entity.py +++ b/homeassistant/components/braviatv/entity.py @@ -1,4 +1,4 @@ -"""A entity class for BraviaTV integration.""" +"""A entity class for Bravia TV integration.""" from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index 4ce465abc36..8a18cac5a99 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -2,9 +2,9 @@ "domain": "braviatv", "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": ["bravia-tv==1.0.11"], + "requirements": ["pybravia==0.2.0"], "codeowners": ["@bieniu", "@Drafteed"], "config_flow": true, "iot_class": "local_polling", - "loggers": ["bravia_tv"] + "loggers": ["pybravia"] } diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 5d812788563..525e265d415 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -1,4 +1,4 @@ -"""Support for interface with a Bravia TV.""" +"""Media player support for Bravia TV integration.""" from __future__ import annotations from homeassistant.components.media_player import ( @@ -74,7 +74,7 @@ class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): @property def is_volume_muted(self) -> bool: """Boolean if volume is currently muted.""" - return self.coordinator.muted + return self.coordinator.volume_muted @property def media_title(self) -> str | None: @@ -84,12 +84,17 @@ class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): @property def media_content_id(self) -> str | None: """Content ID of current playing media.""" - return self.coordinator.channel_name + return self.coordinator.media_content_id + + @property + def media_content_type(self) -> str | None: + """Content type of current playing media.""" + return self.coordinator.media_content_type @property def media_duration(self) -> int | None: """Duration of current playing media in seconds.""" - return self.coordinator.duration + return self.coordinator.media_duration async def async_turn_on(self) -> None: """Turn the device on.""" diff --git a/requirements_all.txt b/requirements_all.txt index dcc87172176..d6dec9582f0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -436,9 +436,6 @@ boschshcpy==0.2.30 # homeassistant.components.route53 boto3==1.20.24 -# homeassistant.components.braviatv -bravia-tv==1.0.11 - # homeassistant.components.broadlink broadlink==0.18.2 @@ -1421,6 +1418,9 @@ pyblackbird==0.5 # homeassistant.components.neato pybotvac==0.0.23 +# homeassistant.components.braviatv +pybravia==0.2.0 + # homeassistant.components.nissan_leaf pycarwings2==2.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1df383e4ea9..185a68ddf04 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -343,9 +343,6 @@ bond-async==0.1.22 # homeassistant.components.bosch_shc boschshcpy==0.2.30 -# homeassistant.components.braviatv -bravia-tv==1.0.11 - # homeassistant.components.broadlink broadlink==0.18.2 @@ -994,6 +991,9 @@ pyblackbird==0.5 # homeassistant.components.neato pybotvac==0.0.23 +# homeassistant.components.braviatv +pybravia==0.2.0 + # homeassistant.components.cloudflare pycfdns==1.2.2 diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index f61a8d312b2..a105f20d3ee 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -1,7 +1,7 @@ """Define tests for the Bravia TV config flow.""" from unittest.mock import patch -from bravia_tv.braviarc import NoIPControl +from pybravia import BraviaTVConnectionError, BraviaTVNotSupported from homeassistant import data_entry_flow from homeassistant.components.braviatv.const import CONF_IGNORED_SOURCES, DOMAIN @@ -23,13 +23,13 @@ BRAVIA_SYSTEM_INFO = { "cid": "very_unique_string", } -BRAVIA_SOURCE_LIST = { - "HDMI 1": "extInput:hdmi?port=1", - "HDMI 2": "extInput:hdmi?port=2", - "HDMI 3/ARC": "extInput:hdmi?port=3", - "HDMI 4": "extInput:hdmi?port=4", - "AV/Component": "extInput:component?port=1", -} +BRAVIA_SOURCES = [ + {"title": "HDMI 1", "uri": "extInput:hdmi?port=1"}, + {"title": "HDMI 2", "uri": "extInput:hdmi?port=2"}, + {"title": "HDMI 3/ARC", "uri": "extInput:hdmi?port=3"}, + {"title": "HDMI 4", "uri": "extInput:hdmi?port=4"}, + {"title": "AV/Component", "uri": "extInput:component?port=1"}, +] async def test_show_form(hass): @@ -53,9 +53,10 @@ async def test_user_invalid_host(hass): async def test_authorize_cannot_connect(hass): """Test that errors are shown when cannot connect to host at the authorize step.""" - with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( - "bravia_tv.BraviaRC.is_connected", return_value=False - ): + with patch( + "pybravia.BraviaTV.connect", + side_effect=BraviaTVConnectionError, + ), patch("pybravia.BraviaTV.pair"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) @@ -68,12 +69,14 @@ async def test_authorize_cannot_connect(hass): async def test_authorize_model_unsupported(hass): """Test that errors are shown when the TV is not supported at the authorize step.""" - with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( - "bravia_tv.BraviaRC.is_connected", return_value=True - ), patch("bravia_tv.BraviaRC.get_system_info", return_value={}): + with patch( + "pybravia.BraviaTV.connect", + side_effect=BraviaTVNotSupported, + ), patch("pybravia.BraviaTV.pair"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "10.10.10.12"} ) + result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PIN: "1234"} ) @@ -83,13 +86,12 @@ async def test_authorize_model_unsupported(hass): async def test_authorize_no_ip_control(hass): """Test that errors are shown when IP Control is disabled on the TV.""" - with patch("bravia_tv.BraviaRC.connect", side_effect=NoIPControl("No IP Control")): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} + ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "no_ip_control" + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "no_ip_control" async def test_duplicate_error(hass): @@ -106,9 +108,12 @@ async def test_duplicate_error(hass): ) config_entry.add_to_hass(hass) - with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( - "bravia_tv.BraviaRC.is_connected", return_value=True - ), patch("bravia_tv.BraviaRC.get_system_info", return_value=BRAVIA_SYSTEM_INFO): + with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( + "pybravia.BraviaTV.set_wol_mode" + ), patch( + "pybravia.BraviaTV.get_system_info", + return_value=BRAVIA_SYSTEM_INFO, + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} @@ -123,10 +128,11 @@ async def test_duplicate_error(hass): async def test_create_entry(hass): """Test that the user step works.""" - with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( - "bravia_tv.BraviaRC.is_connected", return_value=True + with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( + "pybravia.BraviaTV.set_wol_mode" ), patch( - "bravia_tv.BraviaRC.get_system_info", return_value=BRAVIA_SYSTEM_INFO + "pybravia.BraviaTV.get_system_info", + return_value=BRAVIA_SYSTEM_INFO, ), patch( "homeassistant.components.braviatv.async_setup_entry", return_value=True ): @@ -154,10 +160,11 @@ async def test_create_entry(hass): async def test_create_entry_with_ipv6_address(hass): """Test that the user step works with device IPv6 address.""" - with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( - "bravia_tv.BraviaRC.is_connected", return_value=True + with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( + "pybravia.BraviaTV.set_wol_mode" ), patch( - "bravia_tv.BraviaRC.get_system_info", return_value=BRAVIA_SYSTEM_INFO + "pybravia.BraviaTV.get_system_info", + return_value=BRAVIA_SYSTEM_INFO, ), patch( "homeassistant.components.braviatv.async_setup_entry", return_value=True ): @@ -199,19 +206,19 @@ async def test_options_flow(hass): ) config_entry.add_to_hass(hass) - with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( - "bravia_tv.BraviaRC.is_connected", return_value=True - ), patch("bravia_tv.BraviaRC.get_power_status"), patch( - "bravia_tv.BraviaRC.get_system_info", return_value=BRAVIA_SYSTEM_INFO + with patch("pybravia.BraviaTV.connect"), patch( + "pybravia.BraviaTV.get_power_status", + return_value="active", + ), patch( + "pybravia.BraviaTV.get_external_status", + return_value=BRAVIA_SOURCES, + ), patch( + "pybravia.BraviaTV.send_rest_req", + return_value={}, ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( - "bravia_tv.BraviaRC.is_connected", return_value=False - ), patch("bravia_tv.BraviaRC.get_power_status"), patch( - "bravia_tv.BraviaRC.load_source_list", return_value=BRAVIA_SOURCE_LIST - ): result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.FlowResultType.FORM From acaa20cabe316a9b325c3dd641fefb1aa59c8e43 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 10 Aug 2022 15:38:40 +0200 Subject: [PATCH 3251/3516] Improve MQTT warning message on illegal discovery topic (#76545) --- homeassistant/components/mqtt/discovery.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index ebc3a170aeb..8a4c4d0c542 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -106,7 +106,10 @@ async def async_start( # noqa: C901 if not (match := TOPIC_MATCHER.match(topic_trimmed)): if topic_trimmed.endswith("config"): _LOGGER.warning( - "Received message on illegal discovery topic '%s'", topic + "Received message on illegal discovery topic '%s'. The topic contains " + "not allowed characters. For more information see " + "https://www.home-assistant.io/docs/mqtt/discovery/#discovery-topic", + topic, ) return From eeff766078c94e25dc21afc928b236991e7a37ed Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 10 Aug 2022 16:25:03 +0200 Subject: [PATCH 3252/3516] Improve type hints in xiaomi_miio number entity (#76466) --- .../components/xiaomi_miio/number.py | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/number.py b/homeassistant/components/xiaomi_miio/number.py index 364bd59772c..2d3106ddd4e 100644 --- a/homeassistant/components/xiaomi_miio/number.py +++ b/homeassistant/components/xiaomi_miio/number.py @@ -4,6 +4,8 @@ from __future__ import annotations import dataclasses from dataclasses import dataclass +from miio import Device + from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.components.number.const import DOMAIN as PLATFORM_DOMAIN from homeassistant.config_entries import ConfigEntry @@ -12,6 +14,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( CONF_DEVICE, @@ -91,11 +94,17 @@ ATTR_VOLUME = "volume" @dataclass -class XiaomiMiioNumberDescription(NumberEntityDescription): +class XiaomiMiioNumberMixin: + """A class that describes number entities.""" + + method: str + + +@dataclass +class XiaomiMiioNumberDescription(NumberEntityDescription, XiaomiMiioNumberMixin): """A class that describes number entities.""" available_with_device_off: bool = True - method: str | None = None @dataclass @@ -325,7 +334,16 @@ async def async_setup_entry( class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): """Representation of a generic Xiaomi attribute selector.""" - def __init__(self, device, entry, unique_id, coordinator, description): + entity_description: XiaomiMiioNumberDescription + + def __init__( + self, + device: Device, + entry: ConfigEntry, + unique_id: str, + coordinator: DataUpdateCoordinator, + description: XiaomiMiioNumberDescription, + ) -> None: """Initialize the generic Xiaomi attribute selector.""" super().__init__(device, entry, unique_id, coordinator) @@ -335,7 +353,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): self.entity_description = description @property - def available(self): + def available(self) -> bool: """Return the number controller availability.""" if ( super().available @@ -345,7 +363,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): return False return super().available - async def async_set_native_value(self, value): + async def async_set_native_value(self, value: float) -> None: """Set an option of the miio device.""" method = getattr(self, self.entity_description.method) if await method(int(value)): @@ -353,7 +371,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): self.async_write_ha_state() @callback - def _handle_coordinator_update(self): + def _handle_coordinator_update(self) -> None: """Fetch state from the device.""" # On state change the device doesn't provide the new state immediately. self._attr_native_value = self._extract_value_from_attribute( @@ -407,7 +425,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): delay_off_countdown * 60, ) - async def async_set_led_brightness_level(self, level: int): + async def async_set_led_brightness_level(self, level: int) -> bool: """Set the led brightness level.""" return await self._try_command( "Setting the led brightness level of the miio device failed.", @@ -415,7 +433,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): level, ) - async def async_set_led_brightness(self, level: int): + async def async_set_led_brightness(self, level: int) -> bool: """Set the led brightness level.""" return await self._try_command( "Setting the led brightness level of the miio device failed.", @@ -423,7 +441,7 @@ class XiaomiNumberEntity(XiaomiCoordinatedMiioEntity, NumberEntity): level, ) - async def async_set_favorite_rpm(self, rpm: int): + async def async_set_favorite_rpm(self, rpm: int) -> bool: """Set the target motor speed.""" return await self._try_command( "Setting the favorite rpm of the miio device failed.", From 982d197ff3c493024846f38d8688985b6acd3707 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 10 Aug 2022 16:30:58 +0200 Subject: [PATCH 3253/3516] Add number checks to pylint plugin (#76457) * Add number checks to pylint plugin * Adjust ancestor checks * Add tests * Add comments in tests --- pylint/plugins/hass_enforce_type_hints.py | 65 ++++++++++++++++++++++- tests/pylint/test_enforce_type_hints.py | 37 +++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 257f4fb3613..852c5b544c4 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1465,6 +1465,55 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "number": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="NumberEntity", + matches=[ + TypeHintMatch( + function_name="device_class", + return_type=["NumberDeviceClass", "str", None], + ), + TypeHintMatch( + function_name="capability_attributes", + return_type="dict[str, Any]", + ), + TypeHintMatch( + function_name="native_min_value", + return_type="float", + ), + TypeHintMatch( + function_name="native_max_value", + return_type="float", + ), + TypeHintMatch( + function_name="native_step", + return_type=["float", None], + ), + TypeHintMatch( + function_name="mode", + return_type="NumberMode", + ), + TypeHintMatch( + function_name="native_unit_of_measurement", + return_type=["str", None], + ), + TypeHintMatch( + function_name="native_value", + return_type=["float", None], + ), + TypeHintMatch( + function_name="set_native_value", + arg_types={1: "float"}, + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ], "select": [ ClassTypeHintMatch( base_class="Entity", @@ -1599,6 +1648,15 @@ def _is_valid_type( and _is_valid_type(match.group(2), node.slice) ) + # Special case for float in return type + if ( + expected_type == "float" + and in_return + and isinstance(node, nodes.Name) + and node.name in ("float", "int") + ): + return True + # Name occurs when a namespace is not used, eg. "HomeAssistant" if isinstance(node, nodes.Name) and node.name == expected_type: return True @@ -1737,12 +1795,15 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] ): self._class_matchers.extend(property_matches) + self._class_matchers.reverse() + def visit_classdef(self, node: nodes.ClassDef) -> None: """Called when a ClassDef node is visited.""" ancestor: nodes.ClassDef checked_class_methods: set[str] = set() - for ancestor in node.ancestors(): - for class_matches in self._class_matchers: + ancestors = list(node.ancestors()) # cache result for inside loop + for class_matches in self._class_matchers: + for ancestor in ancestors: if ancestor.name == class_matches.base_class: self._visit_class_functions( node, class_matches.matches, checked_class_methods diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index d9edde9fdee..b3c233d1c3b 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -900,3 +900,40 @@ def test_invalid_device_class( ), ): type_hint_checker.visit_classdef(class_node) + + +def test_number_entity(linter: UnittestLinter, type_hint_checker: BaseChecker) -> None: + """Ensure valid hints are accepted for number entity.""" + # Set bypass option + type_hint_checker.config.ignore_missing_annotations = False + + # Ensure that device class is valid despite Entity inheritance + # Ensure that `int` is valid for `float` return type + class_node = astroid.extract_node( + """ + class Entity(): + pass + + class RestoreEntity(Entity): + pass + + class NumberEntity(Entity): + pass + + class MyNumber( #@ + RestoreEntity, NumberEntity + ): + @property + def device_class(self) -> NumberDeviceClass: + pass + + @property + def native_value(self) -> int: + pass + """, + "homeassistant.components.pylint_test.number", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_no_messages(linter): + type_hint_checker.visit_classdef(class_node) From 0639681991a0d8cd0235ddc023ea5cb5c883c57a Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 10 Aug 2022 18:56:34 +0100 Subject: [PATCH 3254/3516] Add new Bluetooth coordinator helper for polling mostly passive devices (#76549) --- .../bluetooth/active_update_coordinator.py | 141 +++++++ .../test_active_update_coordinator.py | 348 ++++++++++++++++++ 2 files changed, 489 insertions(+) create mode 100644 homeassistant/components/bluetooth/active_update_coordinator.py create mode 100644 tests/components/bluetooth/test_active_update_coordinator.py diff --git a/homeassistant/components/bluetooth/active_update_coordinator.py b/homeassistant/components/bluetooth/active_update_coordinator.py new file mode 100644 index 00000000000..1ebd26f8203 --- /dev/null +++ b/homeassistant/components/bluetooth/active_update_coordinator.py @@ -0,0 +1,141 @@ +"""A Bluetooth passive coordinator that collects data from advertisements but can also poll.""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine +import logging +import time +from typing import Any, Generic, TypeVar + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.debounce import Debouncer + +from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak +from .passive_update_processor import PassiveBluetoothProcessorCoordinator + +POLL_DEFAULT_COOLDOWN = 10 +POLL_DEFAULT_IMMEDIATE = True + +_T = TypeVar("_T") + + +class ActiveBluetoothProcessorCoordinator( + Generic[_T], PassiveBluetoothProcessorCoordinator[_T] +): + """ + A coordinator that parses passive data from advertisements but can also poll. + + Every time an advertisement is received, needs_poll_method is called to work + out if a poll is needed. This should return True if it is and False if it is + not needed. + + def needs_poll_method(svc_info: BluetoothServiceInfoBleak, last_poll: float | None) -> bool: + return True + + If there has been no poll since HA started, `last_poll` will be None. Otherwise it is + the number of seconds since one was last attempted. + + If a poll is needed, the coordinator will call poll_method. This is a coroutine. + It should return the same type of data as your update_method. The expectation is that + data from advertisements and from polling are being parsed and fed into a shared + object that represents the current state of the device. + + async def poll_method(svc_info: BluetoothServiceInfoBleak) -> YourDataType: + return YourDataType(....) + + BluetoothServiceInfoBleak.device contains a BLEDevice. You should use this in + your poll function, as it is the most efficient way to get a BleakClient. + """ + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + *, + address: str, + mode: BluetoothScanningMode, + update_method: Callable[[BluetoothServiceInfoBleak], _T], + needs_poll_method: Callable[[BluetoothServiceInfoBleak, float | None], bool], + poll_method: Callable[ + [BluetoothServiceInfoBleak], + Coroutine[Any, Any, _T], + ] + | None = None, + poll_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None, + ) -> None: + """Initialize the processor.""" + super().__init__(hass, logger, address, mode, update_method) + + self._needs_poll_method = needs_poll_method + self._poll_method = poll_method + self._last_poll: float | None = None + self.last_poll_successful = True + + # We keep the last service info in case the poller needs to refer to + # e.g. its BLEDevice + self._last_service_info: BluetoothServiceInfoBleak | None = None + + if poll_debouncer is None: + poll_debouncer = Debouncer( + hass, + logger, + cooldown=POLL_DEFAULT_COOLDOWN, + immediate=POLL_DEFAULT_IMMEDIATE, + function=self._async_poll, + ) + else: + poll_debouncer.function = self._async_poll + + self._debounced_poll = poll_debouncer + + def needs_poll(self, service_info: BluetoothServiceInfoBleak) -> bool: + """Return true if time to try and poll.""" + poll_age: float | None = None + if self._last_poll: + poll_age = time.monotonic() - self._last_poll + return self._needs_poll_method(service_info, poll_age) + + async def _async_poll_data( + self, last_service_info: BluetoothServiceInfoBleak + ) -> _T: + """Fetch the latest data from the source.""" + if self._poll_method is None: + raise NotImplementedError("Poll method not implemented") + return await self._poll_method(last_service_info) + + async def _async_poll(self) -> None: + """Poll the device to retrieve any extra data.""" + assert self._last_service_info + + try: + update = await self._async_poll_data(self._last_service_info) + except Exception: # pylint: disable=broad-except + if self.last_poll_successful: + self.logger.exception("%s: Failure while polling", self.address) + self.last_poll_successful = False + return + finally: + self._last_poll = time.monotonic() + + if not self.last_poll_successful: + self.logger.debug("%s: Polling recovered") + self.last_poll_successful = True + + for processor in self._processors: + processor.async_handle_update(update) + + @callback + def _async_handle_bluetooth_event( + self, + service_info: BluetoothServiceInfoBleak, + change: BluetoothChange, + ) -> None: + """Handle a Bluetooth event.""" + super()._async_handle_bluetooth_event(service_info, change) + + self._last_service_info = service_info + + # See if its time to poll + # We use bluetooth events to trigger the poll so that we scan as soon as + # possible after a device comes online or back in range, if a poll is due + if self.needs_poll(service_info): + self.hass.async_create_task(self._debounced_poll.async_call()) diff --git a/tests/components/bluetooth/test_active_update_coordinator.py b/tests/components/bluetooth/test_active_update_coordinator.py new file mode 100644 index 00000000000..24ad96c523e --- /dev/null +++ b/tests/components/bluetooth/test_active_update_coordinator.py @@ -0,0 +1,348 @@ +"""Tests for the Bluetooth integration PassiveBluetoothDataUpdateCoordinator.""" +from __future__ import annotations + +import asyncio +import logging +from unittest.mock import MagicMock, call, patch + +from homeassistant.components.bluetooth import ( + DOMAIN, + BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfoBleak, +) +from homeassistant.components.bluetooth.active_update_coordinator import ( + ActiveBluetoothProcessorCoordinator, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from homeassistant.setup import async_setup_component + +_LOGGER = logging.getLogger(__name__) + + +GENERIC_BLUETOOTH_SERVICE_INFO = BluetoothServiceInfo( + name="Generic", + address="aa:bb:cc:dd:ee:ff", + rssi=-95, + manufacturer_data={ + 1: b"\x01\x01\x01\x01\x01\x01\x01\x01", + }, + service_data={}, + service_uuids=[], + source="local", +) + + +async def test_basic_usage(hass: HomeAssistant, mock_bleak_scanner_start): + """Test basic usage of the ActiveBluetoothProcessorCoordinator.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + def _update_method(service_info: BluetoothServiceInfoBleak): + return {"testdata": 0} + + def _poll_needed(*args, **kwargs): + return True + + async def _poll(*args, **kwargs): + return {"testdata": 1} + + coordinator = ActiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address="aa:bb:cc:dd:ee:ff", + mode=BluetoothScanningMode.ACTIVE, + update_method=_update_method, + needs_poll_method=_poll_needed, + poll_method=_poll, + ) + assert coordinator.available is False # no data yet + saved_callback = None + + processor = MagicMock() + coordinator.async_register_processor(processor) + async_handle_update = processor.async_handle_update + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel = coordinator.async_start() + + assert saved_callback is not None + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + + assert coordinator.available is True + + # async_handle_update should have been called twice + # The first time, it was passed the data from parsing the advertisement + # The second time, it was passed the data from polling + assert len(async_handle_update.mock_calls) == 2 + assert async_handle_update.mock_calls[0] == call({"testdata": 0}) + assert async_handle_update.mock_calls[1] == call({"testdata": 1}) + + cancel() + + +async def test_poll_can_be_skipped(hass: HomeAssistant, mock_bleak_scanner_start): + """Test need_poll callback works and can skip a poll if its not needed.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + flag = True + + def _update_method(service_info: BluetoothServiceInfoBleak): + return {"testdata": None} + + def _poll_needed(*args, **kwargs): + nonlocal flag + return flag + + async def _poll(*args, **kwargs): + return {"testdata": flag} + + coordinator = ActiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address="aa:bb:cc:dd:ee:ff", + mode=BluetoothScanningMode.ACTIVE, + update_method=_update_method, + needs_poll_method=_poll_needed, + poll_method=_poll, + poll_debouncer=Debouncer( + hass, + _LOGGER, + cooldown=0, + immediate=True, + ), + ) + assert coordinator.available is False # no data yet + saved_callback = None + + processor = MagicMock() + coordinator.async_register_processor(processor) + async_handle_update = processor.async_handle_update + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel = coordinator.async_start() + + assert saved_callback is not None + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": True}) + + flag = False + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": None}) + + flag = True + + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": True}) + + cancel() + + +async def test_poll_failure_and_recover(hass: HomeAssistant, mock_bleak_scanner_start): + """Test error handling and recovery.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + flag = True + + def _update_method(service_info: BluetoothServiceInfoBleak): + return {"testdata": None} + + def _poll_needed(*args, **kwargs): + return True + + async def _poll(*args, **kwargs): + nonlocal flag + if flag: + raise RuntimeError("Poll failure") + return {"testdata": flag} + + coordinator = ActiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address="aa:bb:cc:dd:ee:ff", + mode=BluetoothScanningMode.ACTIVE, + update_method=_update_method, + needs_poll_method=_poll_needed, + poll_method=_poll, + poll_debouncer=Debouncer( + hass, + _LOGGER, + cooldown=0, + immediate=True, + ), + ) + assert coordinator.available is False # no data yet + saved_callback = None + + processor = MagicMock() + coordinator.async_register_processor(processor) + async_handle_update = processor.async_handle_update + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel = coordinator.async_start() + + assert saved_callback is not None + + # First poll fails + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": None}) + + # Second poll works + flag = False + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": False}) + + cancel() + + +async def test_second_poll_needed(hass: HomeAssistant, mock_bleak_scanner_start): + """If a poll is queued, by the time it starts it may no longer be needed.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + count = 0 + + def _update_method(service_info: BluetoothServiceInfoBleak): + return {"testdata": None} + + # Only poll once + def _poll_needed(*args, **kwargs): + nonlocal count + return count == 0 + + async def _poll(*args, **kwargs): + nonlocal count + count += 1 + return {"testdata": count} + + coordinator = ActiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address="aa:bb:cc:dd:ee:ff", + mode=BluetoothScanningMode.ACTIVE, + update_method=_update_method, + needs_poll_method=_poll_needed, + poll_method=_poll, + ) + assert coordinator.available is False # no data yet + saved_callback = None + + processor = MagicMock() + coordinator.async_register_processor(processor) + async_handle_update = processor.async_handle_update + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel = coordinator.async_start() + + assert saved_callback is not None + + # First poll gets queued + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Second poll gets stuck behind first poll + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": 1}) + + cancel() + + +async def test_rate_limit(hass: HomeAssistant, mock_bleak_scanner_start): + """Test error handling and recovery.""" + await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + count = 0 + + def _update_method(service_info: BluetoothServiceInfoBleak): + return {"testdata": None} + + def _poll_needed(*args, **kwargs): + return True + + async def _poll(*args, **kwargs): + nonlocal count + count += 1 + await asyncio.sleep(0) + return {"testdata": count} + + coordinator = ActiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address="aa:bb:cc:dd:ee:ff", + mode=BluetoothScanningMode.ACTIVE, + update_method=_update_method, + needs_poll_method=_poll_needed, + poll_method=_poll, + ) + assert coordinator.available is False # no data yet + saved_callback = None + + processor = MagicMock() + coordinator.async_register_processor(processor) + async_handle_update = processor.async_handle_update + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + cancel = coordinator.async_start() + + assert saved_callback is not None + + # First poll gets queued + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Second poll gets stuck behind first poll + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + # Third poll gets stuck behind first poll doesn't get queued + saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + + await hass.async_block_till_done() + assert async_handle_update.mock_calls[-1] == call({"testdata": 1}) + + cancel() From 54fc17e10de0752c03d6b95153c3d8168f76ea44 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 10 Aug 2022 20:40:38 +0200 Subject: [PATCH 3255/3516] Improve type hints in xiaomi_miio vacuum entities (#76563) Co-authored-by: Teemu R. --- .../components/xiaomi_miio/vacuum.py | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 7b866d5ff71..7df38109d16 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -3,6 +3,7 @@ from __future__ import annotations from functools import partial import logging +from typing import Any from miio import DeviceException import voluptuous as vol @@ -214,7 +215,7 @@ class MiroboVacuum( self._handle_coordinator_update() @property - def state(self): + def state(self) -> str | None: """Return the status of the vacuum cleaner.""" # The vacuum reverts back to an idle state after erroring out. # We want to keep returning an error until it has been cleared. @@ -247,7 +248,7 @@ class MiroboVacuum( return [] @property - def timers(self): + def timers(self) -> list[dict[str, Any]]: """Get the list of added timers of the vacuum cleaner.""" return [ { @@ -259,9 +260,9 @@ class MiroboVacuum( ] @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the specific state attributes of this vacuum cleaner.""" - attrs = {} + attrs: dict[str, Any] = {} attrs[ATTR_STATUS] = str(self.coordinator.data.status.state) if self.coordinator.data.status.got_error: @@ -281,27 +282,27 @@ class MiroboVacuum( _LOGGER.error(mask_error, exc) return False - async def async_start(self): + async def async_start(self) -> None: """Start or resume the cleaning task.""" await self._try_command( "Unable to start the vacuum: %s", self._device.resume_or_start ) - async def async_pause(self): + async def async_pause(self) -> None: """Pause the cleaning task.""" await self._try_command("Unable to set start/pause: %s", self._device.pause) - async def async_stop(self, **kwargs): + async def async_stop(self, **kwargs: Any) -> None: """Stop the vacuum cleaner.""" await self._try_command("Unable to stop: %s", self._device.stop) - async def async_set_fan_speed(self, fan_speed, **kwargs): + async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set fan speed.""" if fan_speed in self.coordinator.data.fan_speeds: - fan_speed = self.coordinator.data.fan_speeds[fan_speed] + fan_speed_int = self.coordinator.data.fan_speeds[fan_speed] else: try: - fan_speed = int(fan_speed) + fan_speed_int = int(fan_speed) except ValueError as exc: _LOGGER.error( "Fan speed step not recognized (%s). Valid speeds are: %s", @@ -310,24 +311,26 @@ class MiroboVacuum( ) return await self._try_command( - "Unable to set fan speed: %s", self._device.set_fan_speed, fan_speed + "Unable to set fan speed: %s", self._device.set_fan_speed, fan_speed_int ) - async def async_return_to_base(self, **kwargs): + async def async_return_to_base(self, **kwargs: Any) -> None: """Set the vacuum cleaner to return to the dock.""" await self._try_command("Unable to return home: %s", self._device.home) - async def async_clean_spot(self, **kwargs): + async def async_clean_spot(self, **kwargs: Any) -> None: """Perform a spot clean-up.""" await self._try_command( "Unable to start the vacuum for a spot clean-up: %s", self._device.spot ) - async def async_locate(self, **kwargs): + async def async_locate(self, **kwargs: Any) -> None: """Locate the vacuum cleaner.""" await self._try_command("Unable to locate the botvac: %s", self._device.find) - async def async_send_command(self, command, params=None, **kwargs): + async def async_send_command( + self, command: str, params: dict | list | None = None, **kwargs: Any + ) -> None: """Send raw command.""" await self._try_command( "Unable to send command to the vacuum: %s", @@ -336,13 +339,13 @@ class MiroboVacuum( params, ) - async def async_remote_control_start(self): + async def async_remote_control_start(self) -> None: """Start remote control mode.""" await self._try_command( "Unable to start remote control the vacuum: %s", self._device.manual_start ) - async def async_remote_control_stop(self): + async def async_remote_control_stop(self) -> None: """Stop remote control mode.""" await self._try_command( "Unable to stop remote control the vacuum: %s", self._device.manual_stop @@ -350,7 +353,7 @@ class MiroboVacuum( async def async_remote_control_move( self, rotation: int = 0, velocity: float = 0.3, duration: int = 1500 - ): + ) -> None: """Move vacuum with remote control mode.""" await self._try_command( "Unable to move with remote control the vacuum: %s", @@ -362,7 +365,7 @@ class MiroboVacuum( async def async_remote_control_move_step( self, rotation: int = 0, velocity: float = 0.2, duration: int = 1500 - ): + ) -> None: """Move vacuum one step with remote control mode.""" await self._try_command( "Unable to remote control the vacuum: %s", @@ -372,7 +375,7 @@ class MiroboVacuum( duration=duration, ) - async def async_goto(self, x_coord: int, y_coord: int): + async def async_goto(self, x_coord: int, y_coord: int) -> None: """Goto the specified coordinates.""" await self._try_command( "Unable to send the vacuum cleaner to the specified coordinates: %s", @@ -381,7 +384,7 @@ class MiroboVacuum( y_coord=y_coord, ) - async def async_clean_segment(self, segments): + async def async_clean_segment(self, segments) -> None: """Clean the specified segments(s).""" if isinstance(segments, int): segments = [segments] @@ -392,7 +395,7 @@ class MiroboVacuum( segments=segments, ) - async def async_clean_zone(self, zone, repeats=1): + async def async_clean_zone(self, zone: list[Any], repeats: int = 1) -> None: """Clean selected area for the number of repeats indicated.""" for _zone in zone: _zone.append(repeats) From b1497b08579e3a35cd3a478418adb200633cc8cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Aug 2022 09:02:08 -1000 Subject: [PATCH 3256/3516] Simplify switchbot config flow (#76272) --- .../components/switchbot/__init__.py | 44 ++--- .../components/switchbot/binary_sensor.py | 24 +-- .../components/switchbot/config_flow.py | 171 ++++++++++++----- homeassistant/components/switchbot/const.py | 38 ++-- .../components/switchbot/coordinator.py | 4 + homeassistant/components/switchbot/cover.py | 30 +-- homeassistant/components/switchbot/entity.py | 17 +- homeassistant/components/switchbot/sensor.py | 31 +-- .../components/switchbot/strings.json | 12 +- homeassistant/components/switchbot/switch.py | 31 +-- .../components/switchbot/translations/en.json | 21 ++- tests/components/switchbot/__init__.py | 58 +++--- .../components/switchbot/test_config_flow.py | 177 +++++++++++++++--- 13 files changed, 412 insertions(+), 246 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 7eec785233f..4d9fd2af7b6 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -9,6 +9,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_ADDRESS, CONF_MAC, + CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE, Platform, @@ -17,31 +18,26 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr -from .const import ( - ATTR_BOT, - ATTR_CONTACT, - ATTR_CURTAIN, - ATTR_HYGROMETER, - ATTR_MOTION, - ATTR_PLUG, - CONF_RETRY_COUNT, - DEFAULT_RETRY_COUNT, - DOMAIN, -) +from .const import CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, SupportedModels from .coordinator import SwitchbotDataUpdateCoordinator PLATFORMS_BY_TYPE = { - ATTR_BOT: [Platform.SWITCH, Platform.SENSOR], - ATTR_PLUG: [Platform.SWITCH, Platform.SENSOR], - ATTR_CURTAIN: [Platform.COVER, Platform.BINARY_SENSOR, Platform.SENSOR], - ATTR_HYGROMETER: [Platform.SENSOR], - ATTR_CONTACT: [Platform.BINARY_SENSOR, Platform.SENSOR], - ATTR_MOTION: [Platform.BINARY_SENSOR, Platform.SENSOR], + SupportedModels.BULB.value: [Platform.SENSOR], + SupportedModels.BOT.value: [Platform.SWITCH, Platform.SENSOR], + SupportedModels.PLUG.value: [Platform.SWITCH, Platform.SENSOR], + SupportedModels.CURTAIN.value: [ + Platform.COVER, + Platform.BINARY_SENSOR, + Platform.SENSOR, + ], + SupportedModels.HYGROMETER.value: [Platform.SENSOR], + SupportedModels.CONTACT.value: [Platform.BINARY_SENSOR, Platform.SENSOR], + SupportedModels.MOTION.value: [Platform.BINARY_SENSOR, Platform.SENSOR], } CLASS_BY_DEVICE = { - ATTR_CURTAIN: switchbot.SwitchbotCurtain, - ATTR_BOT: switchbot.Switchbot, - ATTR_PLUG: switchbot.SwitchbotPlugMini, + SupportedModels.CURTAIN.value: switchbot.SwitchbotCurtain, + SupportedModels.BOT.value: switchbot.Switchbot, + SupportedModels.PLUG.value: switchbot.SwitchbotPlugMini, } _LOGGER = logging.getLogger(__name__) @@ -49,6 +45,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Switchbot from a config entry.""" + assert entry.unique_id is not None hass.data.setdefault(DOMAIN, {}) if CONF_ADDRESS not in entry.data and CONF_MAC in entry.data: # Bleak uses addresses not mac addresses which are are actually @@ -81,7 +78,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: retry_count=entry.options[CONF_RETRY_COUNT], ) coordinator = hass.data[DOMAIN][entry.entry_id] = SwitchbotDataUpdateCoordinator( - hass, _LOGGER, ble_device, device + hass, + _LOGGER, + ble_device, + device, + entry.unique_id, + entry.data.get(CONF_NAME, entry.title), ) entry.async_on_unload(coordinator.async_start()) if not await coordinator.async_wait_ready(): diff --git a/homeassistant/components/switchbot/binary_sensor.py b/homeassistant/components/switchbot/binary_sensor.py index 4da4ed531b0..bf071d64a2d 100644 --- a/homeassistant/components/switchbot/binary_sensor.py +++ b/homeassistant/components/switchbot/binary_sensor.py @@ -7,7 +7,6 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -53,20 +52,10 @@ async def async_setup_entry( ) -> None: """Set up Switchbot curtain based on a config entry.""" coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - unique_id = entry.unique_id - assert unique_id is not None async_add_entities( - [ - SwitchBotBinarySensor( - coordinator, - unique_id, - binary_sensor, - entry.data[CONF_ADDRESS], - entry.data[CONF_NAME], - ) - for binary_sensor in coordinator.data["data"] - if binary_sensor in BINARY_SENSOR_TYPES - ] + SwitchBotBinarySensor(coordinator, binary_sensor) + for binary_sensor in coordinator.data["data"] + if binary_sensor in BINARY_SENSOR_TYPES ) @@ -78,15 +67,12 @@ class SwitchBotBinarySensor(SwitchbotEntity, BinarySensorEntity): def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, - unique_id: str, binary_sensor: str, - mac: str, - switchbot_name: str, ) -> None: """Initialize the Switchbot sensor.""" - super().__init__(coordinator, unique_id, mac, name=switchbot_name) + super().__init__(coordinator) self._sensor = binary_sensor - self._attr_unique_id = f"{unique_id}-{binary_sensor}" + self._attr_unique_id = f"{coordinator.base_unique_id}-{binary_sensor}" self.entity_description = BINARY_SENSOR_TYPES[binary_sensor] self._attr_name = self.entity_description.name diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index eaad573d370..0d7e91648f2 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -12,9 +12,9 @@ from homeassistant.components.bluetooth import ( async_discovered_service_info, ) from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow -from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SENSOR_TYPE +from homeassistant.const import CONF_ADDRESS, CONF_PASSWORD, CONF_SENSOR_TYPE from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import AbortFlow, FlowResult from .const import CONF_RETRY_COUNT, DEFAULT_RETRY_COUNT, DOMAIN, SUPPORTED_MODEL_TYPES @@ -26,6 +26,17 @@ def format_unique_id(address: str) -> str: return address.replace(":", "").lower() +def short_address(address: str) -> str: + """Convert a Bluetooth address to a short address.""" + results = address.replace("-", ":").split(":") + return f"{results[-2].upper()}{results[-1].upper()}"[-4:] + + +def name_from_discovery(discovery: SwitchBotAdvertisement) -> str: + """Get the name from a discovery.""" + return f'{discovery.data["modelFriendlyName"]} {short_address(discovery.address)}' + + class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Switchbot.""" @@ -59,62 +70,128 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): self._discovered_adv = parsed data = parsed.data self.context["title_placeholders"] = { - "name": data["modelName"], - "address": discovery_info.address, + "name": data["modelFriendlyName"], + "address": short_address(discovery_info.address), } - return await self.async_step_user() + if self._discovered_adv.data["isEncrypted"]: + return await self.async_step_password() + return await self.async_step_confirm() + + async def _async_create_entry_from_discovery( + self, user_input: dict[str, Any] + ) -> FlowResult: + """Create an entry from a discovery.""" + assert self._discovered_adv is not None + discovery = self._discovered_adv + name = name_from_discovery(discovery) + model_name = discovery.data["modelName"] + return self.async_create_entry( + title=name, + data={ + **user_input, + CONF_ADDRESS: discovery.address, + CONF_SENSOR_TYPE: str(SUPPORTED_MODEL_TYPES[model_name]), + }, + ) + + async def async_step_confirm(self, user_input: dict[str, Any] = None) -> FlowResult: + """Confirm a single device.""" + assert self._discovered_adv is not None + if user_input is not None: + return await self._async_create_entry_from_discovery(user_input) + + self._set_confirm_only() + return self.async_show_form( + step_id="confirm", + data_schema=vol.Schema({}), + description_placeholders={ + "name": name_from_discovery(self._discovered_adv) + }, + ) + + async def async_step_password( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the password step.""" + assert self._discovered_adv is not None + if user_input is not None: + # There is currently no api to validate the password + # that does not operate the device so we have + # to accept it as-is + return await self._async_create_entry_from_discovery(user_input) + + return self.async_show_form( + step_id="password", + data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), + description_placeholders={ + "name": name_from_discovery(self._discovered_adv) + }, + ) + + @callback + def _async_discover_devices(self) -> None: + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if ( + format_unique_id(address) in current_addresses + or address in self._discovered_advs + ): + continue + parsed = parse_advertisement_data( + discovery_info.device, discovery_info.advertisement + ) + if parsed and parsed.data.get("modelName") in SUPPORTED_MODEL_TYPES: + self._discovered_advs[address] = parsed + + if not self._discovered_advs: + raise AbortFlow("no_unconfigured_devices") + + async def _async_set_device(self, discovery: SwitchBotAdvertisement) -> None: + """Set the device to work with.""" + self._discovered_adv = discovery + address = discovery.address + await self.async_set_unique_id( + format_unique_id(address), raise_on_progress=False + ) + self._abort_if_unique_id_configured() async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the user step to pick discovered device.""" errors: dict[str, str] = {} - + device_adv: SwitchBotAdvertisement | None = None if user_input is not None: - address = user_input[CONF_ADDRESS] - await self.async_set_unique_id( - format_unique_id(address), raise_on_progress=False - ) - self._abort_if_unique_id_configured() - user_input[CONF_SENSOR_TYPE] = SUPPORTED_MODEL_TYPES[ - self._discovered_advs[address].data["modelName"] - ] - return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) + device_adv = self._discovered_advs[user_input[CONF_ADDRESS]] + await self._async_set_device(device_adv) + if device_adv.data["isEncrypted"]: + return await self.async_step_password() + return await self._async_create_entry_from_discovery(user_input) - if discovery := self._discovered_adv: - self._discovered_advs[discovery.address] = discovery - else: - current_addresses = self._async_current_ids() - for discovery_info in async_discovered_service_info(self.hass): - address = discovery_info.address - if ( - format_unique_id(address) in current_addresses - or address in self._discovered_advs - ): - continue - parsed = parse_advertisement_data( - discovery_info.device, discovery_info.advertisement - ) - if parsed and parsed.data.get("modelName") in SUPPORTED_MODEL_TYPES: - self._discovered_advs[address] = parsed + self._async_discover_devices() + if len(self._discovered_advs) == 1: + # If there is only one device we can ask for a password + # or simply confirm it + device_adv = list(self._discovered_advs.values())[0] + await self._async_set_device(device_adv) + if device_adv.data["isEncrypted"]: + return await self.async_step_password() + return await self.async_step_confirm() - if not self._discovered_advs: - return self.async_abort(reason="no_unconfigured_devices") - - data_schema = vol.Schema( - { - vol.Required(CONF_ADDRESS): vol.In( - { - address: f"{parsed.data['modelName']} ({address})" - for address, parsed in self._discovered_advs.items() - } - ), - vol.Required(CONF_NAME): str, - vol.Optional(CONF_PASSWORD): str, - } - ) return self.async_show_form( - step_id="user", data_schema=data_schema, errors=errors + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_ADDRESS): vol.In( + { + address: name_from_discovery(parsed) + for address, parsed in self._discovered_advs.items() + } + ), + } + ), + errors=errors, ) diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index a8ec3433f84..6463b9fb4a3 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -1,25 +1,39 @@ """Constants for the switchbot integration.""" +from switchbot import SwitchbotModel + +from homeassistant.backports.enum import StrEnum + DOMAIN = "switchbot" MANUFACTURER = "switchbot" # Config Attributes -ATTR_BOT = "bot" -ATTR_CURTAIN = "curtain" -ATTR_HYGROMETER = "hygrometer" -ATTR_CONTACT = "contact" -ATTR_PLUG = "plug" -ATTR_MOTION = "motion" + DEFAULT_NAME = "Switchbot" + +class SupportedModels(StrEnum): + """Supported Switchbot models.""" + + BOT = "bot" + BULB = "bulb" + CURTAIN = "curtain" + HYGROMETER = "hygrometer" + CONTACT = "contact" + PLUG = "plug" + MOTION = "motion" + + SUPPORTED_MODEL_TYPES = { - "WoHand": ATTR_BOT, - "WoCurtain": ATTR_CURTAIN, - "WoSensorTH": ATTR_HYGROMETER, - "WoContact": ATTR_CONTACT, - "WoPlug": ATTR_PLUG, - "WoPresence": ATTR_MOTION, + SwitchbotModel.BOT: SupportedModels.BOT, + SwitchbotModel.CURTAIN: SupportedModels.CURTAIN, + SwitchbotModel.METER: SupportedModels.HYGROMETER, + SwitchbotModel.CONTACT_SENSOR: SupportedModels.CONTACT, + SwitchbotModel.PLUG_MINI: SupportedModels.PLUG, + SwitchbotModel.MOTION_SENSOR: SupportedModels.MOTION, + SwitchbotModel.COLOR_BULB: SupportedModels.BULB, } + # Config Defaults DEFAULT_RETRY_COUNT = 3 diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 43c576249df..ad9aff8c53b 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -37,6 +37,8 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): logger: logging.Logger, ble_device: BLEDevice, device: switchbot.SwitchbotDevice, + base_unique_id: str, + device_name: str, ) -> None: """Initialize global switchbot data updater.""" super().__init__( @@ -45,6 +47,8 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): self.ble_device = ble_device self.device = device self.data: dict[str, Any] = {} + self.device_name = device_name + self.base_unique_id = base_unique_id self._ready_event = asyncio.Event() @callback diff --git a/homeassistant/components/switchbot/cover.py b/homeassistant/components/switchbot/cover.py index 0ae225f55d7..c2b6cb1a4c7 100644 --- a/homeassistant/components/switchbot/cover.py +++ b/homeassistant/components/switchbot/cover.py @@ -4,8 +4,6 @@ from __future__ import annotations import logging from typing import Any -from switchbot import SwitchbotCurtain - from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_POSITION, @@ -14,7 +12,6 @@ from homeassistant.components.cover import ( CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ADDRESS, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -33,19 +30,7 @@ async def async_setup_entry( ) -> None: """Set up Switchbot curtain based on a config entry.""" coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - unique_id = entry.unique_id - assert unique_id is not None - async_add_entities( - [ - SwitchBotCurtainEntity( - coordinator, - unique_id, - entry.data[CONF_ADDRESS], - entry.data[CONF_NAME], - coordinator.device, - ) - ] - ) + async_add_entities([SwitchBotCurtainEntity(coordinator)]) class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): @@ -59,19 +44,10 @@ class SwitchBotCurtainEntity(SwitchbotEntity, CoverEntity, RestoreEntity): | CoverEntityFeature.SET_POSITION ) - def __init__( - self, - coordinator: SwitchbotDataUpdateCoordinator, - unique_id: str, - address: str, - name: str, - device: SwitchbotCurtain, - ) -> None: + def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: """Initialize the Switchbot.""" - super().__init__(coordinator, unique_id, address, name) - self._attr_unique_id = unique_id + super().__init__(coordinator) self._attr_is_closed = None - self._device = device async def async_added_to_hass(self) -> None: """Run when entity about to be added.""" diff --git a/homeassistant/components/switchbot/entity.py b/homeassistant/components/switchbot/entity.py index 4e69da4ec11..2e5ba78dcc8 100644 --- a/homeassistant/components/switchbot/entity.py +++ b/homeassistant/components/switchbot/entity.py @@ -20,24 +20,19 @@ class SwitchbotEntity(PassiveBluetoothCoordinatorEntity): coordinator: SwitchbotDataUpdateCoordinator - def __init__( - self, - coordinator: SwitchbotDataUpdateCoordinator, - unique_id: str, - address: str, - name: str, - ) -> None: + def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: """Initialize the entity.""" super().__init__(coordinator) + self._device = coordinator.device self._last_run_success: bool | None = None - self._unique_id = unique_id - self._address = address - self._attr_name = name + self._address = coordinator.ble_device.address + self._attr_unique_id = coordinator.base_unique_id + self._attr_name = coordinator.device_name self._attr_device_info = DeviceInfo( connections={(dr.CONNECTION_BLUETOOTH, self._address)}, manufacturer=MANUFACTURER, model=self.data["modelName"], - name=name, + name=coordinator.device_name, ) if ":" not in self._address: # MacOS Bluetooth addresses are not mac addresses diff --git a/homeassistant/components/switchbot/sensor.py b/homeassistant/components/switchbot/sensor.py index fb24ae22679..886da1051b7 100644 --- a/homeassistant/components/switchbot/sensor.py +++ b/homeassistant/components/switchbot/sensor.py @@ -9,8 +9,6 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_ADDRESS, - CONF_NAME, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, TEMP_CELSIUS, @@ -73,20 +71,13 @@ async def async_setup_entry( ) -> None: """Set up Switchbot sensor based on a config entry.""" coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - unique_id = entry.unique_id - assert unique_id is not None async_add_entities( - [ - SwitchBotSensor( - coordinator, - unique_id, - sensor, - entry.data[CONF_ADDRESS], - entry.data[CONF_NAME], - ) - for sensor in coordinator.data["data"] - if sensor in SENSOR_TYPES - ] + SwitchBotSensor( + coordinator, + sensor, + ) + for sensor in coordinator.data["data"] + if sensor in SENSOR_TYPES ) @@ -96,16 +87,14 @@ class SwitchBotSensor(SwitchbotEntity, SensorEntity): def __init__( self, coordinator: SwitchbotDataUpdateCoordinator, - unique_id: str, sensor: str, - address: str, - switchbot_name: str, ) -> None: """Initialize the Switchbot sensor.""" - super().__init__(coordinator, unique_id, address, name=switchbot_name) + super().__init__(coordinator) self._sensor = sensor - self._attr_unique_id = f"{unique_id}-{sensor}" - self._attr_name = f"{switchbot_name} {sensor.replace('_', ' ').title()}" + self._attr_unique_id = f"{coordinator.base_unique_id}-{sensor}" + name = coordinator.device_name + self._attr_name = f"{name} {sensor.replace('_', ' ').title()}" self.entity_description = SENSOR_TYPES[sensor] @property diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index 797d1d7613c..c7b0744c579 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -3,10 +3,16 @@ "flow_title": "{name} ({address})", "step": { "user": { - "title": "Setup Switchbot device", "data": { - "address": "Device address", - "name": "[%key:common::config_flow::data::name%]", + "address": "Device address" + } + }, + "confirm": { + "description": "Do you want to setup {name}?" + }, + "password": { + "description": "The {name} device requires a password", + "data": { "password": "[%key:common::config_flow::data::password%]" } } diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 65c7588acbd..17235135cfa 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -4,11 +4,9 @@ from __future__ import annotations import logging from typing import Any -from switchbot import Switchbot - from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ADDRESS, CONF_NAME, STATE_ON +from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_platform from homeassistant.helpers.restore_state import RestoreEntity @@ -29,19 +27,7 @@ async def async_setup_entry( ) -> None: """Set up Switchbot based on a config entry.""" coordinator: SwitchbotDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - unique_id = entry.unique_id - assert unique_id is not None - async_add_entities( - [ - SwitchBotSwitch( - coordinator, - unique_id, - entry.data[CONF_ADDRESS], - entry.data[CONF_NAME], - coordinator.device, - ) - ] - ) + async_add_entities([SwitchBotSwitch(coordinator)]) class SwitchBotSwitch(SwitchbotEntity, SwitchEntity, RestoreEntity): @@ -49,18 +35,9 @@ class SwitchBotSwitch(SwitchbotEntity, SwitchEntity, RestoreEntity): _attr_device_class = SwitchDeviceClass.SWITCH - def __init__( - self, - coordinator: SwitchbotDataUpdateCoordinator, - unique_id: str, - address: str, - name: str, - device: Switchbot, - ) -> None: + def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: """Initialize the Switchbot.""" - super().__init__(coordinator, unique_id, address, name) - self._attr_unique_id = unique_id - self._device = device + super().__init__(coordinator) self._attr_is_on = False async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index b583c60061b..7e58d169856 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -7,16 +7,22 @@ "switchbot_unsupported_type": "Unsupported Switchbot Type.", "unknown": "Unexpected error" }, + "error": {}, "flow_title": "{name} ({address})", "step": { - "user": { + "confirm": { + "description": "Do you want to setup {name}?" + }, + "password": { "data": { - "address": "Device address", - "mac": "Device MAC address", - "name": "Name", "password": "Password" }, - "title": "Setup Switchbot device" + "description": "The {name} device requires a password" + }, + "user": { + "data": { + "address": "Device address" + } } } }, @@ -24,10 +30,7 @@ "step": { "init": { "data": { - "retry_count": "Retry count", - "retry_timeout": "Timeout between retries", - "scan_timeout": "How long to scan for advertisement data", - "update_time": "Time between updates (seconds)" + "retry_count": "Retry count" } } } diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index b4b2e56b39c..fbf764fa4eb 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -5,7 +5,7 @@ from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData from homeassistant.components.bluetooth import BluetoothServiceInfoBleak -from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PASSWORD +from homeassistant.const import CONF_ADDRESS from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -13,38 +13,18 @@ from tests.common import MockConfigEntry DOMAIN = "switchbot" ENTRY_CONFIG = { - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", CONF_ADDRESS: "e7:89:43:99:99:99", } USER_INPUT = { - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", - CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", -} - -USER_INPUT_CURTAIN = { - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", - CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", -} - -USER_INPUT_SENSOR = { - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", } USER_INPUT_UNSUPPORTED_DEVICE = { - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", CONF_ADDRESS: "test", } USER_INPUT_INVALID = { - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", CONF_ADDRESS: "invalid-mac", } @@ -90,6 +70,42 @@ WOHAND_SERVICE_INFO = BluetoothServiceInfoBleak( ), device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoHand"), ) + + +WOHAND_ENCRYPTED_SERVICE_INFO = BluetoothServiceInfoBleak( + name="WoHand", + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"\xc8\x10\xcf"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="798A8547-2A3D-C609-55FF-73FA824B923B", + rssi=-60, + source="local", + advertisement=AdvertisementData( + local_name="WoHand", + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"\xc8\x10\xcf"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=BLEDevice("798A8547-2A3D-C609-55FF-73FA824B923B", "WoHand"), +) + + +WOHAND_SERVICE_ALT_ADDRESS_INFO = BluetoothServiceInfoBleak( + name="WoHand", + manufacturer_data={89: b"\xfd`0U\x92W"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + address="cc:cc:cc:cc:cc:cc", + rssi=-60, + source="local", + advertisement=AdvertisementData( + local_name="WoHand", + manufacturer_data={89: b"\xfd`0U\x92W"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x90\xd9"}, + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + ), + device=BLEDevice("aa:bb:cc:dd:ee:ff", "WoHand"), +) WOCURTAIN_SERVICE_INFO = BluetoothServiceInfoBleak( name="WoCurtain", address="aa:bb:cc:dd:ee:ff", diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index 0ae3430eeb1..7ad863cc355 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -10,9 +10,9 @@ from homeassistant.data_entry_flow import FlowResultType from . import ( NOT_SWITCHBOT_INFO, USER_INPUT, - USER_INPUT_CURTAIN, - USER_INPUT_SENSOR, WOCURTAIN_SERVICE_INFO, + WOHAND_ENCRYPTED_SERVICE_INFO, + WOHAND_SERVICE_ALT_ADDRESS_INFO, WOHAND_SERVICE_INFO, WOSENSORTH_SERVICE_INFO, init_integration, @@ -32,27 +32,53 @@ async def test_bluetooth_discovery(hass): data=WOHAND_SERVICE_INFO, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" + assert result["step_id"] == "confirm" with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], - USER_INPUT, + {}, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "test-name" + assert result["title"] == "Bot EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "bot", } assert len(mock_setup_entry.mock_calls) == 1 +async def test_bluetooth_discovery_requires_password(hass): + """Test discovery via bluetooth with a valid device that needs a password.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_BLUETOOTH}, + data=WOHAND_ENCRYPTED_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "password" + + with patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "abc123"}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Bot 923B" + assert result["data"] == { + CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B", + CONF_SENSOR_TYPE: "bot", + CONF_PASSWORD: "abc123", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_bluetooth_discovery_already_setup(hass): """Test discovery via bluetooth with a valid device when already setup.""" entry = MockConfigEntry( @@ -97,22 +123,20 @@ async def test_user_setup_wohand(hass): DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} + assert result["step_id"] == "confirm" + assert result["errors"] is None with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], - USER_INPUT, + {}, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "test-name" + assert result["title"] == "Bot EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "bot", } @@ -154,28 +178,129 @@ async def test_user_setup_wocurtain(hass): DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "confirm" + assert result["errors"] is None + + with patch_async_setup_entry() as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Curtain EEFF" + assert result["data"] == { + CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", + CONF_SENSOR_TYPE: "curtain", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_setup_wocurtain_or_bot(hass): + """Test the user initiated form with valid address.""" + + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOCURTAIN_SERVICE_INFO, WOHAND_SERVICE_ALT_ADDRESS_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], - USER_INPUT_CURTAIN, + USER_INPUT, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "test-name" + assert result["title"] == "Curtain EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "curtain", } assert len(mock_setup_entry.mock_calls) == 1 +async def test_user_setup_wocurtain_or_bot_with_password(hass): + """Test the user initiated form and valid address and a bot with a password.""" + + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOCURTAIN_SERVICE_INFO, WOHAND_ENCRYPTED_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B"}, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "password" + assert result2["errors"] is None + + with patch_async_setup_entry() as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {CONF_PASSWORD: "abc123"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == "Bot 923B" + assert result3["data"] == { + CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B", + CONF_PASSWORD: "abc123", + CONF_SENSOR_TYPE: "bot", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_setup_single_bot_with_password(hass): + """Test the user initiated form for a bot with a password.""" + + with patch( + "homeassistant.components.switchbot.config_flow.async_discovered_service_info", + return_value=[WOHAND_ENCRYPTED_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "password" + assert result["errors"] is None + + with patch_async_setup_entry() as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "abc123"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Bot 923B" + assert result2["data"] == { + CONF_ADDRESS: "798A8547-2A3D-C609-55FF-73FA824B923B", + CONF_PASSWORD: "abc123", + CONF_SENSOR_TYPE: "bot", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_user_setup_wosensor(hass): """Test the user initiated form with password and valid mac.""" with patch( @@ -186,22 +311,20 @@ async def test_user_setup_wosensor(hass): DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] == {} + assert result["step_id"] == "confirm" + assert result["errors"] is None with patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], - USER_INPUT_SENSOR, + {}, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "test-name" + assert result["title"] == "Meter EEFF" assert result["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "hygrometer", } @@ -229,7 +352,7 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): data=WOCURTAIN_SERVICE_INFO, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" + assert result["step_id"] == "confirm" with patch( "homeassistant.components.switchbot.config_flow.async_discovered_service_info", @@ -244,15 +367,13 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): with patch_async_setup_entry() as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=USER_INPUT, + user_input={}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "test-name" + assert result2["title"] == "Curtain EEFF" assert result2["data"] == { CONF_ADDRESS: "aa:bb:cc:dd:ee:ff", - CONF_NAME: "test-name", - CONF_PASSWORD: "test-password", CONF_SENSOR_TYPE: "curtain", } From 2f99d6a32d641d1f476e765528e827c79b22302c Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 10 Aug 2022 13:51:31 -0600 Subject: [PATCH 3257/3516] Bump ZHA dependencies (#76565) --- homeassistant/components/zha/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index bad84054f1f..1eb2536fed6 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,12 +4,12 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.31.3", + "bellows==0.32.0", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.78", "zigpy-deconz==0.18.0", - "zigpy==0.49.0", + "zigpy==0.49.1", "zigpy-xbee==0.15.0", "zigpy-zigate==0.9.1", "zigpy-znp==0.8.1" diff --git a/requirements_all.txt b/requirements_all.txt index d6dec9582f0..7a8fae075f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -396,7 +396,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.31.3 +bellows==0.32.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.1 @@ -2545,7 +2545,7 @@ zigpy-zigate==0.9.1 zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.49.0 +zigpy==0.49.1 # homeassistant.components.zoneminder zm-py==0.5.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 185a68ddf04..d029efd2880 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -320,7 +320,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.31.3 +bellows==0.32.0 # homeassistant.components.bmw_connected_drive bimmer_connected==0.10.1 @@ -1725,7 +1725,7 @@ zigpy-zigate==0.9.1 zigpy-znp==0.8.1 # homeassistant.components.zha -zigpy==0.49.0 +zigpy==0.49.1 # homeassistant.components.zwave_js zwave-js-server-python==0.40.0 From bb0038319dd467ac519e8d2de1e78c3e7c092241 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Aug 2022 10:08:02 -1000 Subject: [PATCH 3258/3516] Add Yale Access Bluetooth integration (#76182) --- .coveragerc | 4 + CODEOWNERS | 2 + .../components/yalexs_ble/__init__.py | 91 +++ .../components/yalexs_ble/config_flow.py | 245 ++++++ homeassistant/components/yalexs_ble/const.py | 9 + homeassistant/components/yalexs_ble/entity.py | 76 ++ homeassistant/components/yalexs_ble/lock.py | 62 ++ .../components/yalexs_ble/manifest.json | 11 + homeassistant/components/yalexs_ble/models.py | 14 + .../components/yalexs_ble/strings.json | 30 + .../yalexs_ble/translations/en.json | 30 + homeassistant/components/yalexs_ble/util.py | 69 ++ homeassistant/generated/bluetooth.py | 4 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/yalexs_ble/__init__.py | 67 ++ tests/components/yalexs_ble/conftest.py | 8 + .../components/yalexs_ble/test_config_flow.py | 754 ++++++++++++++++++ 19 files changed, 1483 insertions(+) create mode 100644 homeassistant/components/yalexs_ble/__init__.py create mode 100644 homeassistant/components/yalexs_ble/config_flow.py create mode 100644 homeassistant/components/yalexs_ble/const.py create mode 100644 homeassistant/components/yalexs_ble/entity.py create mode 100644 homeassistant/components/yalexs_ble/lock.py create mode 100644 homeassistant/components/yalexs_ble/manifest.json create mode 100644 homeassistant/components/yalexs_ble/models.py create mode 100644 homeassistant/components/yalexs_ble/strings.json create mode 100644 homeassistant/components/yalexs_ble/translations/en.json create mode 100644 homeassistant/components/yalexs_ble/util.py create mode 100644 tests/components/yalexs_ble/__init__.py create mode 100644 tests/components/yalexs_ble/conftest.py create mode 100644 tests/components/yalexs_ble/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 7e60c9ae891..2616f4b6e16 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1483,6 +1483,10 @@ omit = homeassistant/components/xiaomi_tv/media_player.py homeassistant/components/xmpp/notify.py homeassistant/components/xs1/* + homeassistant/components/yalexs_ble/__init__.py + homeassistant/components/yalexs_ble/entity.py + homeassistant/components/yalexs_ble/lock.py + homeassistant/components/yalexs_ble/util.py homeassistant/components/yale_smart_alarm/__init__.py homeassistant/components/yale_smart_alarm/alarm_control_panel.py homeassistant/components/yale_smart_alarm/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 59835aed315..8de29fd5ede 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1246,6 +1246,8 @@ build.json @home-assistant/supervisor /homeassistant/components/xmpp/ @fabaff @flowolf /homeassistant/components/yale_smart_alarm/ @gjohansson-ST /tests/components/yale_smart_alarm/ @gjohansson-ST +/homeassistant/components/yalexs_ble/ @bdraco +/tests/components/yalexs_ble/ @bdraco /homeassistant/components/yamaha_musiccast/ @vigonotion @micha91 /tests/components/yamaha_musiccast/ @vigonotion @micha91 /homeassistant/components/yandex_transport/ @rishatik92 @devbis diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py new file mode 100644 index 00000000000..5a1cf461e5d --- /dev/null +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -0,0 +1,91 @@ +"""The Yale Access Bluetooth integration.""" +from __future__ import annotations + +import asyncio + +import async_timeout +from yalexs_ble import PushLock, local_name_is_unique + +from homeassistant.components import bluetooth +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS, Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DEVICE_TIMEOUT, DOMAIN +from .models import YaleXSBLEData +from .util import async_find_existing_service_info, bluetooth_callback_matcher + +PLATFORMS: list[Platform] = [Platform.LOCK] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Yale Access Bluetooth from a config entry.""" + local_name = entry.data[CONF_LOCAL_NAME] + address = entry.data[CONF_ADDRESS] + key = entry.data[CONF_KEY] + slot = entry.data[CONF_SLOT] + has_unique_local_name = local_name_is_unique(local_name) + push_lock = PushLock(local_name, address, None, key, slot) + id_ = local_name if has_unique_local_name else address + push_lock.set_name(f"{entry.title} ({id_})") + + startup_event = asyncio.Event() + + @callback + def _async_update_ble( + service_info: bluetooth.BluetoothServiceInfoBleak, + change: bluetooth.BluetoothChange, + ) -> None: + """Update from a ble callback.""" + push_lock.update_advertisement(service_info.device, service_info.advertisement) + + cancel_first_update = push_lock.register_callback(lambda *_: startup_event.set()) + entry.async_on_unload(await push_lock.start()) + + # We may already have the advertisement, so check for it. + if service_info := async_find_existing_service_info(hass, local_name, address): + push_lock.update_advertisement(service_info.device, service_info.advertisement) + + entry.async_on_unload( + bluetooth.async_register_callback( + hass, + _async_update_ble, + bluetooth_callback_matcher(local_name, push_lock.address), + bluetooth.BluetoothScanningMode.PASSIVE, + ) + ) + + try: + async with async_timeout.timeout(DEVICE_TIMEOUT): + await startup_event.wait() + except asyncio.TimeoutError as ex: + raise ConfigEntryNotReady( + f"{push_lock.last_error}; " + f"Try moving the Bluetooth adapter closer to {local_name}" + ) from ex + finally: + cancel_first_update() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = YaleXSBLEData( + entry.title, push_lock + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + return True + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + data: YaleXSBLEData = hass.data[DOMAIN][entry.entry_id] + if entry.title != data.title: + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/yalexs_ble/config_flow.py b/homeassistant/components/yalexs_ble/config_flow.py new file mode 100644 index 00000000000..7f632ebfab0 --- /dev/null +++ b/homeassistant/components/yalexs_ble/config_flow.py @@ -0,0 +1,245 @@ +"""Config flow for Yale Access Bluetooth integration.""" +from __future__ import annotations + +import asyncio +import logging +from typing import Any + +from bleak_retry_connector import BleakError, BLEDevice +import voluptuous as vol +from yalexs_ble import ( + AuthError, + DisconnectedError, + PushLock, + ValidatedLockConfig, + local_name_is_unique, +) +from yalexs_ble.const import YALE_MFR_ID + +from homeassistant import config_entries +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.typing import DiscoveryInfoType +from homeassistant.loader import async_get_integration + +from .const import CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DOMAIN +from .util import async_get_service_info, human_readable_name + +_LOGGER = logging.getLogger(__name__) + + +async def validate_lock( + local_name: str, device: BLEDevice, key: str, slot: int +) -> None: + """Validate a lock.""" + if len(key) != 32: + raise InvalidKeyFormat + try: + bytes.fromhex(key) + except ValueError as ex: + raise InvalidKeyFormat from ex + if not isinstance(slot, int) or slot < 0 or slot > 255: + raise InvalidKeyIndex + await PushLock(local_name, device.address, device, key, slot).validate() + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Yale Access Bluetooth.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_devices: dict[str, BluetoothServiceInfoBleak] = {} + self._lock_cfg: ValidatedLockConfig | None = None + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + self.context["local_name"] = discovery_info.name + self._discovery_info = discovery_info + self.context["title_placeholders"] = { + "name": human_readable_name( + None, discovery_info.name, discovery_info.address + ), + } + return await self.async_step_user() + + async def async_step_integration_discovery( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: + """Handle a discovered integration.""" + lock_cfg = ValidatedLockConfig( + discovery_info["name"], + discovery_info["address"], + discovery_info["serial"], + discovery_info["key"], + discovery_info["slot"], + ) + # We do not want to raise on progress as integration_discovery takes + # precedence over other discovery flows since we already have the keys. + await self.async_set_unique_id(lock_cfg.address, raise_on_progress=False) + new_data = {CONF_KEY: lock_cfg.key, CONF_SLOT: lock_cfg.slot} + self._abort_if_unique_id_configured(updates=new_data) + for entry in self._async_current_entries(): + if entry.data.get(CONF_LOCAL_NAME) == lock_cfg.local_name: + if self.hass.config_entries.async_update_entry( + entry, data={**entry.data, **new_data} + ): + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + raise AbortFlow(reason="already_configured") + + try: + self._discovery_info = await async_get_service_info( + self.hass, lock_cfg.local_name, lock_cfg.address + ) + except asyncio.TimeoutError: + return self.async_abort(reason="no_devices_found") + + for progress in self._async_in_progress(include_uninitialized=True): + # Integration discovery should abort other discovery types + # since it already has the keys and slots, and the other + # discovery types do not. + context = progress["context"] + if ( + not context.get("active") + and context.get("local_name") == lock_cfg.local_name + or context.get("unique_id") == lock_cfg.address + ): + self.hass.config_entries.flow.async_abort(progress["flow_id"]) + + self._lock_cfg = lock_cfg + self.context["title_placeholders"] = { + "name": human_readable_name( + lock_cfg.name, lock_cfg.local_name, self._discovery_info.address + ) + } + return await self.async_step_integration_discovery_confirm() + + async def async_step_integration_discovery_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a confirmation of discovered integration.""" + assert self._discovery_info is not None + assert self._lock_cfg is not None + if user_input is not None: + return self.async_create_entry( + title=self._lock_cfg.name, + data={ + CONF_LOCAL_NAME: self._discovery_info.name, + CONF_ADDRESS: self._discovery_info.address, + CONF_KEY: self._lock_cfg.key, + CONF_SLOT: self._lock_cfg.slot, + }, + ) + + self._set_confirm_only() + return self.async_show_form( + step_id="integration_discovery_confirm", + description_placeholders={ + "name": self._lock_cfg.name, + "address": self._discovery_info.address, + }, + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + errors: dict[str, str] = {} + + if user_input is not None: + self.context["active"] = True + address = user_input[CONF_ADDRESS] + discovery_info = self._discovered_devices[address] + local_name = discovery_info.name + key = user_input[CONF_KEY] + slot = user_input[CONF_SLOT] + await self.async_set_unique_id( + discovery_info.address, raise_on_progress=False + ) + self._abort_if_unique_id_configured() + try: + await validate_lock(local_name, discovery_info.device, key, slot) + except InvalidKeyFormat: + errors[CONF_KEY] = "invalid_key_format" + except InvalidKeyIndex: + errors[CONF_SLOT] = "invalid_key_index" + except (DisconnectedError, AuthError, ValueError): + errors[CONF_KEY] = "invalid_auth" + except BleakError: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected error") + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=local_name, + data={ + CONF_LOCAL_NAME: discovery_info.name, + CONF_ADDRESS: discovery_info.address, + CONF_KEY: key, + CONF_SLOT: slot, + }, + ) + + if discovery := self._discovery_info: + self._discovered_devices[discovery.address] = discovery + else: + current_addresses = self._async_current_ids() + current_unique_names = { + entry.data.get(CONF_LOCAL_NAME) + for entry in self._async_current_entries() + if local_name_is_unique(entry.data.get(CONF_LOCAL_NAME)) + } + for discovery in async_discovered_service_info(self.hass): + if ( + discovery.address in current_addresses + or discovery.name in current_unique_names + or discovery.address in self._discovered_devices + or YALE_MFR_ID not in discovery.manufacturer_data + ): + continue + self._discovered_devices[discovery.address] = discovery + + if not self._discovered_devices: + return self.async_abort(reason="no_unconfigured_devices") + + data_schema = vol.Schema( + { + vol.Required(CONF_ADDRESS): vol.In( + { + service_info.address: f"{service_info.name} ({service_info.address})" + for service_info in self._discovered_devices.values() + } + ), + vol.Required(CONF_KEY): str, + vol.Required(CONF_SLOT): int, + } + ) + integration = await async_get_integration(self.hass, DOMAIN) + return self.async_show_form( + step_id="user", + data_schema=data_schema, + errors=errors, + description_placeholders={"docs_url": integration.documentation}, + ) + + +class InvalidKeyFormat(HomeAssistantError): + """Invalid key format.""" + + +class InvalidKeyIndex(HomeAssistantError): + """Invalid key index.""" diff --git a/homeassistant/components/yalexs_ble/const.py b/homeassistant/components/yalexs_ble/const.py new file mode 100644 index 00000000000..f38a376a717 --- /dev/null +++ b/homeassistant/components/yalexs_ble/const.py @@ -0,0 +1,9 @@ +"""Constants for the Yale Access Bluetooth integration.""" + +DOMAIN = "yalexs_ble" + +CONF_LOCAL_NAME = "local_name" +CONF_KEY = "key" +CONF_SLOT = "slot" + +DEVICE_TIMEOUT = 55 diff --git a/homeassistant/components/yalexs_ble/entity.py b/homeassistant/components/yalexs_ble/entity.py new file mode 100644 index 00000000000..fa80698831b --- /dev/null +++ b/homeassistant/components/yalexs_ble/entity.py @@ -0,0 +1,76 @@ +"""The yalexs_ble integration entities.""" +from __future__ import annotations + +from yalexs_ble import ConnectionInfo, LockInfo, LockState + +from homeassistant.components import bluetooth +from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import DOMAIN +from .models import YaleXSBLEData + + +class YALEXSBLEEntity(Entity): + """Base class for yale xs ble entities.""" + + _attr_should_poll = False + + def __init__(self, data: YaleXSBLEData) -> None: + """Initialize the entity.""" + self._data = data + self._device = device = data.lock + self._attr_available = False + lock_state = device.lock_state + lock_info = device.lock_info + connection_info = device.connection_info + assert lock_state is not None + assert connection_info is not None + assert lock_info is not None + self._attr_unique_id = device.address + self._attr_device_info = DeviceInfo( + name=data.title, + manufacturer=lock_info.manufacturer, + model=lock_info.model, + connections={(dr.CONNECTION_BLUETOOTH, device.address)}, + identifiers={(DOMAIN, lock_info.serial)}, + sw_version=lock_info.firmware, + ) + if device.lock_state: + self._async_update_state(lock_state, lock_info, connection_info) + + @callback + def _async_update_state( + self, new_state: LockState, lock_info: LockInfo, connection_info: ConnectionInfo + ) -> None: + """Update the state.""" + self._attr_available = True + + @callback + def _async_state_changed( + self, new_state: LockState, lock_info: LockInfo, connection_info: ConnectionInfo + ) -> None: + """Handle state changed.""" + self._async_update_state(new_state, lock_info, connection_info) + self.async_write_ha_state() + + @callback + def _async_device_unavailable(self, _address: str) -> None: + """Handle device not longer being seen by the bluetooth stack.""" + self._attr_available = False + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + self.async_on_remove( + bluetooth.async_track_unavailable( + self.hass, self._async_device_unavailable, self._device.address + ) + ) + self.async_on_remove(self._device.register_callback(self._async_state_changed)) + return await super().async_added_to_hass() + + async def async_update(self) -> None: + """Request a manual update.""" + await self._device.update() diff --git a/homeassistant/components/yalexs_ble/lock.py b/homeassistant/components/yalexs_ble/lock.py new file mode 100644 index 00000000000..3f75a282f67 --- /dev/null +++ b/homeassistant/components/yalexs_ble/lock.py @@ -0,0 +1,62 @@ +"""Support for Yale Access Bluetooth locks.""" +from __future__ import annotations + +from typing import Any + +from yalexs_ble import ConnectionInfo, LockInfo, LockState, LockStatus + +from homeassistant.components.lock import LockEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import YALEXSBLEEntity +from .models import YaleXSBLEData + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up locks.""" + data: YaleXSBLEData = hass.data[DOMAIN][entry.entry_id] + async_add_entities([YaleXSBLELock(data)]) + + +class YaleXSBLELock(YALEXSBLEEntity, LockEntity): + """A yale xs ble lock.""" + + _attr_has_entity_name = True + + @callback + def _async_update_state( + self, new_state: LockState, lock_info: LockInfo, connection_info: ConnectionInfo + ) -> None: + """Update the state.""" + self._attr_is_locked = False + self._attr_is_locking = False + self._attr_is_unlocking = False + self._attr_is_jammed = False + lock_state = new_state.lock + if lock_state == LockStatus.LOCKED: + self._attr_is_locked = True + elif lock_state == LockStatus.LOCKING: + self._attr_is_locking = True + elif lock_state == LockStatus.UNLOCKING: + self._attr_is_unlocking = True + elif lock_state in ( + LockStatus.UNKNOWN_01, + LockStatus.UNKNOWN_06, + ): + self._attr_is_jammed = True + super()._async_update_state(new_state, lock_info, connection_info) + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock the lock.""" + return await self._device.unlock() + + async def async_lock(self, **kwargs: Any) -> None: + """Lock the lock.""" + return await self._device.lock() diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json new file mode 100644 index 00000000000..8f7838792ff --- /dev/null +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "yalexs_ble", + "name": "Yale Access Bluetooth", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", + "requirements": ["yalexs-ble==1.1.2"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "bluetooth": [{ "manufacturer_id": 465 }], + "iot_class": "local_push" +} diff --git a/homeassistant/components/yalexs_ble/models.py b/homeassistant/components/yalexs_ble/models.py new file mode 100644 index 00000000000..d79668f1c70 --- /dev/null +++ b/homeassistant/components/yalexs_ble/models.py @@ -0,0 +1,14 @@ +"""The yalexs_ble integration models.""" +from __future__ import annotations + +from dataclasses import dataclass + +from yalexs_ble import PushLock + + +@dataclass +class YaleXSBLEData: + """Data for the yale xs ble integration.""" + + title: str + lock: PushLock diff --git a/homeassistant/components/yalexs_ble/strings.json b/homeassistant/components/yalexs_ble/strings.json new file mode 100644 index 00000000000..4d867474dbe --- /dev/null +++ b/homeassistant/components/yalexs_ble/strings.json @@ -0,0 +1,30 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "description": "Check the documentation at {docs_url} for how to find the offline key.", + "data": { + "address": "Bluetooth address", + "key": "Offline Key (32-byte hex string)", + "slot": "Offline Key Slot (Integer between 0 and 255)" + } + }, + "integration_discovery_confirm": { + "description": "Do you want to setup {name} over Bluetooth with address {address}?" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "invalid_key_format": "The offline key must be a 32-byte hex string.", + "invalid_key_index": "The offline key slot must be an integer between 0 and 255." + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_unconfigured_devices": "No unconfigured devices found.", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + } +} diff --git a/homeassistant/components/yalexs_ble/translations/en.json b/homeassistant/components/yalexs_ble/translations/en.json new file mode 100644 index 00000000000..6d817499270 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/en.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "no_devices_found": "No devices found on the network", + "no_unconfigured_devices": "No unconfigured devices found." + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_key_format": "The offline key must be a 32-byte hex string.", + "invalid_key_index": "The offline key slot must be an integer between 0 and 255.", + "unknown": "Unexpected error" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "Do you want to setup {name} over Bluetooth with address {address}?" + }, + "user": { + "data": { + "address": "Bluetooth address", + "key": "Offline Key (32-byte hex string)", + "slot": "Offline Key Slot (Integer between 0 and 255)" + }, + "description": "Check the documentation at {docs_url} for how to find the offline key." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/util.py b/homeassistant/components/yalexs_ble/util.py new file mode 100644 index 00000000000..a1c6fdf7d32 --- /dev/null +++ b/homeassistant/components/yalexs_ble/util.py @@ -0,0 +1,69 @@ +"""The yalexs_ble integration models.""" +from __future__ import annotations + +from yalexs_ble import local_name_is_unique + +from homeassistant.components.bluetooth import ( + BluetoothScanningMode, + BluetoothServiceInfoBleak, + async_discovered_service_info, + async_process_advertisements, +) +from homeassistant.components.bluetooth.match import ( + ADDRESS, + LOCAL_NAME, + BluetoothCallbackMatcher, +) +from homeassistant.core import HomeAssistant, callback + +from .const import DEVICE_TIMEOUT + + +def bluetooth_callback_matcher( + local_name: str, address: str +) -> BluetoothCallbackMatcher: + """Return a BluetoothCallbackMatcher for the given local_name and address.""" + if local_name_is_unique(local_name): + return BluetoothCallbackMatcher({LOCAL_NAME: local_name}) + return BluetoothCallbackMatcher({ADDRESS: address}) + + +@callback +def async_find_existing_service_info( + hass: HomeAssistant, local_name: str, address: str +) -> BluetoothServiceInfoBleak | None: + """Return the service info for the given local_name and address.""" + has_unique_local_name = local_name_is_unique(local_name) + for service_info in async_discovered_service_info(hass): + device = service_info.device + if ( + has_unique_local_name and device.name == local_name + ) or device.address == address: + return service_info + return None + + +async def async_get_service_info( + hass: HomeAssistant, local_name: str, address: str +) -> BluetoothServiceInfoBleak: + """Wait for the service info for the given local_name and address.""" + if service_info := async_find_existing_service_info(hass, local_name, address): + return service_info + return await async_process_advertisements( + hass, + lambda service_info: True, + bluetooth_callback_matcher(local_name, address), + BluetoothScanningMode.ACTIVE, + DEVICE_TIMEOUT, + ) + + +def short_address(address: str) -> str: + """Convert a Bluetooth address to a short address.""" + split_address = address.replace("-", ":").split(":") + return f"{split_address[-2].upper()}{split_address[-1].upper()}"[-4:] + + +def human_readable_name(name: str | None, local_name: str, address: str) -> str: + """Return a human readable name for the given name, local_name, and address.""" + return f"{name or local_name} ({short_address(address)})" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index d7af6e6ee11..7f0696f1a76 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -107,5 +107,9 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ { "domain": "xiaomi_ble", "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" + }, + { + "domain": "yalexs_ble", + "manufacturer_id": 465 } ] diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a582f2b719c..a20f1229a39 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -430,6 +430,7 @@ FLOWS = { "xiaomi_ble", "xiaomi_miio", "yale_smart_alarm", + "yalexs_ble", "yamaha_musiccast", "yeelight", "yolink", diff --git a/requirements_all.txt b/requirements_all.txt index 7a8fae075f4..1927739987c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2499,6 +2499,9 @@ xs1-api-client==3.0.0 # homeassistant.components.yale_smart_alarm yalesmartalarmclient==0.3.8 +# homeassistant.components.yalexs_ble +yalexs-ble==1.1.2 + # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d029efd2880..72bf0741f25 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1694,6 +1694,9 @@ xmltodict==0.13.0 # homeassistant.components.yale_smart_alarm yalesmartalarmclient==0.3.8 +# homeassistant.components.yalexs_ble +yalexs-ble==1.1.2 + # homeassistant.components.august yalexs==1.2.1 diff --git a/tests/components/yalexs_ble/__init__.py b/tests/components/yalexs_ble/__init__.py new file mode 100644 index 00000000000..eb6800ff83a --- /dev/null +++ b/tests/components/yalexs_ble/__init__.py @@ -0,0 +1,67 @@ +"""Tests for the Yale Access Bluetooth integration.""" +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData + +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak + +YALE_ACCESS_LOCK_DISCOVERY_INFO = BluetoothServiceInfoBleak( + name="M1012LU", + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + manufacturer_data={ + 465: b"\x00\x00\xd1\xf0b;\xd8\x1dE\xd6\xba\xeeL\xdd]\xf5\xb2\xe9", + 76: b"\x061\x00Z\x8f\x93\xb2\xec\x85\x06\x00i\x00\x02\x02Q\xed\x1d\xf0", + }, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="M1012LU"), + advertisement=AdvertisementData(), +) + + +LOCK_DISCOVERY_INFO_UUID_ADDRESS = BluetoothServiceInfoBleak( + name="M1012LU", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-60, + manufacturer_data={ + 465: b"\x00\x00\xd1\xf0b;\xd8\x1dE\xd6\xba\xeeL\xdd]\xf5\xb2\xe9", + 76: b"\x061\x00Z\x8f\x93\xb2\xec\x85\x06\x00i\x00\x02\x02Q\xed\x1d\xf0", + }, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="M1012LU"), + advertisement=AdvertisementData(), +) + +OLD_FIRMWARE_LOCK_DISCOVERY_INFO = BluetoothServiceInfoBleak( + name="Aug", + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + manufacturer_data={ + 465: b"\x00\x00\xd1\xf0b;\xd8\x1dE\xd6\xba\xeeL\xdd]\xf5\xb2\xe9", + 76: b"\x061\x00Z\x8f\x93\xb2\xec\x85\x06\x00i\x00\x02\x02Q\xed\x1d\xf0", + }, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="Aug"), + advertisement=AdvertisementData(), +) + + +NOT_YALE_DISCOVERY_INFO = BluetoothServiceInfoBleak( + name="Not", + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + manufacturer_data={ + 33: b"\x00\x00\xd1\xf0b;\xd8\x1dE\xd6\xba\xeeL\xdd]\xf5\xb2\xe9", + 21: b"\x061\x00Z\x8f\x93\xb2\xec\x85\x06\x00i\x00\x02\x02Q\xed\x1d\xf0", + }, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="Aug"), + advertisement=AdvertisementData(), +) diff --git a/tests/components/yalexs_ble/conftest.py b/tests/components/yalexs_ble/conftest.py new file mode 100644 index 00000000000..c2b947cc863 --- /dev/null +++ b/tests/components/yalexs_ble/conftest.py @@ -0,0 +1,8 @@ +"""yalexs_ble session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/yalexs_ble/test_config_flow.py b/tests/components/yalexs_ble/test_config_flow.py new file mode 100644 index 00000000000..7607b710934 --- /dev/null +++ b/tests/components/yalexs_ble/test_config_flow.py @@ -0,0 +1,754 @@ +"""Test the Yale Access Bluetooth config flow.""" +import asyncio +from unittest.mock import patch + +from bleak import BleakError +from yalexs_ble import AuthError + +from homeassistant import config_entries +from homeassistant.components.yalexs_ble.const import ( + CONF_KEY, + CONF_LOCAL_NAME, + CONF_SLOT, + DOMAIN, +) +from homeassistant.const import CONF_ADDRESS +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import ( + LOCK_DISCOVERY_INFO_UUID_ADDRESS, + NOT_YALE_DISCOVERY_INFO, + OLD_FIRMWARE_LOCK_DISCOVERY_INFO, + YALE_ACCESS_LOCK_DISCOVERY_INFO, +) + +from tests.common import MockConfigEntry + + +async def test_user_step_success(hass: HomeAssistant) -> None: + """Test user step success path.""" + with patch( + "homeassistant.components.yalexs_ble.config_flow.async_discovered_service_info", + return_value=[NOT_YALE_DISCOVERY_INFO, YALE_ACCESS_LOCK_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name + assert result2["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert result2["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_step_no_devices_found(hass: HomeAssistant) -> None: + """Test user step with no devices found.""" + with patch( + "homeassistant.components.yalexs_ble.config_flow.async_discovered_service_info", + return_value=[NOT_YALE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_unconfigured_devices" + + +async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None: + """Test user step with only existing devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + unique_id=YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.yalexs_ble.config_flow.async_discovered_service_info", + return_value=[YALE_ACCESS_LOCK_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_unconfigured_devices" + + +async def test_user_step_invalid_keys(hass: HomeAssistant) -> None: + """Test user step with invalid keys tried first.""" + with patch( + "homeassistant.components.yalexs_ble.config_flow.async_discovered_service_info", + return_value=[YALE_ACCESS_LOCK_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "dog", + CONF_SLOT: 66, + }, + ) + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {CONF_KEY: "invalid_key_format"} + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "qfd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + assert result3["type"] == FlowResultType.FORM + assert result3["step_id"] == "user" + assert result3["errors"] == {CONF_KEY: "invalid_key_format"} + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 999, + }, + ) + assert result4["type"] == FlowResultType.FORM + assert result4["step_id"] == "user" + assert result4["errors"] == {CONF_SLOT: "invalid_key_index"} + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result5 = await hass.config_entries.flow.async_configure( + result4["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + await hass.async_block_till_done() + + assert result5["type"] == FlowResultType.CREATE_ENTRY + assert result5["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name + assert result5["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert result5["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: + """Test user step and we cannot connect.""" + with patch( + "homeassistant.components.yalexs_ble.config_flow.async_discovered_service_info", + return_value=[YALE_ACCESS_LOCK_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + side_effect=BleakError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name + assert result3["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert result3["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_step_auth_exception(hass: HomeAssistant) -> None: + """Test user step with an authentication exception.""" + with patch( + "homeassistant.components.yalexs_ble.config_flow.async_discovered_service_info", + return_value=[YALE_ACCESS_LOCK_DISCOVERY_INFO, NOT_YALE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + side_effect=AuthError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {CONF_KEY: "invalid_auth"} + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name + assert result3["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert result3["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: + """Test user step with an unknown exception.""" + with patch( + "homeassistant.components.yalexs_ble.config_flow.async_discovered_service_info", + return_value=[NOT_YALE_DISCOVERY_INFO, YALE_ACCESS_LOCK_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + side_effect=RuntimeError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "unknown"} + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name + assert result3["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert result3["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_bluetooth_step_success(hass: HomeAssistant) -> None: + """Test bluetooth step success path.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=YALE_ACCESS_LOCK_DISCOVERY_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + ), patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name + assert result2["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert result2["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_integration_discovery_success(hass: HomeAssistant) -> None: + """Test integration discovery step success path.""" + with patch( + "homeassistant.components.yalexs_ble.util.async_process_advertisements", + return_value=YALE_ACCESS_LOCK_DISCOVERY_INFO, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "integration_discovery_confirm" + assert result["errors"] is None + + with patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Front Door" + assert result2["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert result2["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_integration_discovery_device_not_found(hass: HomeAssistant) -> None: + """Test integration discovery when the device is not found.""" + with patch( + "homeassistant.components.yalexs_ble.util.async_process_advertisements", + side_effect=asyncio.TimeoutError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_integration_discovery_takes_precedence_over_bluetooth( + hass: HomeAssistant, +) -> None: + """Test integration discovery dismisses bluetooth discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=YALE_ACCESS_LOCK_DISCOVERY_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + flows = [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN + ] + assert len(flows) == 1 + assert flows[0]["context"]["unique_id"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert flows[0]["context"]["local_name"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name + + with patch( + "homeassistant.components.yalexs_ble.util.async_process_advertisements", + return_value=YALE_ACCESS_LOCK_DISCOVERY_INFO, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "integration_discovery_confirm" + assert result["errors"] is None + + # the bluetooth flow should get dismissed in favor + # of the integration discovery flow since the integration + # discovery flow will have the keys and the bluetooth + # flow will not + flows = [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN + ] + assert len(flows) == 1 + + with patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Front Door" + assert result2["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert result2["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + flows = [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN + ] + assert len(flows) == 0 + + +async def test_integration_discovery_updates_key_unique_local_name( + hass: HomeAssistant, +) -> None: + """Test integration discovery updates the key with a unique local name.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LOCAL_NAME: LOCK_DISCOVERY_INFO_UUID_ADDRESS.name, + CONF_ADDRESS: "61DE521B-F0BF-9F44-64D4-75BBE1738105", + CONF_KEY: "5fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 11, + }, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.yalexs_ble.util.async_process_advertisements", + return_value=LOCK_DISCOVERY_INFO_UUID_ADDRESS, + ), patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": "AA:BB:CC:DD:EE:FF", + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_KEY] == "2fd51b8621c6a139eaffbedcb846b60f" + assert entry.data[CONF_SLOT] == 66 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_integration_discovery_updates_key_without_unique_local_name( + hass: HomeAssistant, +) -> None: + """Test integration discovery updates the key without a unique local name.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LOCAL_NAME: OLD_FIRMWARE_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: OLD_FIRMWARE_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "5fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 11, + }, + unique_id=OLD_FIRMWARE_LOCK_DISCOVERY_INFO.address, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.yalexs_ble.util.async_process_advertisements", + return_value=LOCK_DISCOVERY_INFO_UUID_ADDRESS, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": OLD_FIRMWARE_LOCK_DISCOVERY_INFO.address, + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_KEY] == "2fd51b8621c6a139eaffbedcb846b60f" + assert entry.data[CONF_SLOT] == 66 + + +async def test_integration_discovery_takes_precedence_over_bluetooth_uuid_address( + hass: HomeAssistant, +) -> None: + """Test integration discovery dismisses bluetooth discovery with a uuid address.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=LOCK_DISCOVERY_INFO_UUID_ADDRESS, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + flows = [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN + ] + assert len(flows) == 1 + assert flows[0]["context"]["unique_id"] == LOCK_DISCOVERY_INFO_UUID_ADDRESS.address + assert flows[0]["context"]["local_name"] == LOCK_DISCOVERY_INFO_UUID_ADDRESS.name + + with patch( + "homeassistant.components.yalexs_ble.util.async_process_advertisements", + return_value=LOCK_DISCOVERY_INFO_UUID_ADDRESS, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": "AA:BB:CC:DD:EE:FF", + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "integration_discovery_confirm" + assert result["errors"] is None + + # the bluetooth flow should get dismissed in favor + # of the integration discovery flow since the integration + # discovery flow will have the keys and the bluetooth + # flow will not + flows = [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN + ] + assert len(flows) == 1 + + with patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Front Door" + assert result2["data"] == { + CONF_LOCAL_NAME: LOCK_DISCOVERY_INFO_UUID_ADDRESS.name, + CONF_ADDRESS: LOCK_DISCOVERY_INFO_UUID_ADDRESS.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert result2["result"].unique_id == OLD_FIRMWARE_LOCK_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + flows = [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN + ] + assert len(flows) == 0 + + +async def test_integration_discovery_takes_precedence_over_bluetooth_non_unique_local_name( + hass: HomeAssistant, +) -> None: + """Test integration discovery dismisses bluetooth discovery with a non unique local name.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=OLD_FIRMWARE_LOCK_DISCOVERY_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + flows = [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN + ] + assert len(flows) == 1 + assert flows[0]["context"]["unique_id"] == OLD_FIRMWARE_LOCK_DISCOVERY_INFO.address + assert flows[0]["context"]["local_name"] == OLD_FIRMWARE_LOCK_DISCOVERY_INFO.name + + with patch( + "homeassistant.components.yalexs_ble.util.async_process_advertisements", + return_value=OLD_FIRMWARE_LOCK_DISCOVERY_INFO, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": OLD_FIRMWARE_LOCK_DISCOVERY_INFO.address, + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "integration_discovery_confirm" + assert result["errors"] is None + + # the bluetooth flow should get dismissed in favor + # of the integration discovery flow since the integration + # discovery flow will have the keys and the bluetooth + # flow will not + flows = [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN + ] + assert len(flows) == 1 From 9555df88c812863084f85116772c44157407f1f8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 10 Aug 2022 23:49:02 +0200 Subject: [PATCH 3259/3516] Improve type hints in zwave_me number entity (#76469) --- homeassistant/components/zwave_me/number.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zwave_me/number.py b/homeassistant/components/zwave_me/number.py index 2fa82514626..4e9acc1f76b 100644 --- a/homeassistant/components/zwave_me/number.py +++ b/homeassistant/components/zwave_me/number.py @@ -40,7 +40,7 @@ class ZWaveMeNumber(ZWaveMeEntity, NumberEntity): """Representation of a ZWaveMe Multilevel Switch.""" @property - def native_value(self): + def native_value(self) -> float: """Return the unit of measurement.""" if self.device.level == 99: # Scale max value return 100 From 58ac3eee3b8c69b8f54b8d40187a94ecbbfa7e4c Mon Sep 17 00:00:00 2001 From: Brandon Rothweiler Date: Wed, 10 Aug 2022 17:56:20 -0400 Subject: [PATCH 3260/3516] Always round down for Mazda odometer entity (#76500) --- homeassistant/components/mazda/sensor.py | 3 ++- tests/components/mazda/test_sensor.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py index c688ac62637..715b274b6f5 100644 --- a/homeassistant/components/mazda/sensor.py +++ b/homeassistant/components/mazda/sensor.py @@ -116,7 +116,8 @@ def _fuel_distance_remaining_value(data, unit_system): def _odometer_value(data, unit_system): """Get the odometer value.""" - return round(unit_system.length(data["status"]["odometerKm"], LENGTH_KILOMETERS)) + # In order to match the behavior of the Mazda mobile app, we always round down + return int(unit_system.length(data["status"]["odometerKm"], LENGTH_KILOMETERS)) def _front_left_tire_pressure_value(data, unit_system): diff --git a/tests/components/mazda/test_sensor.py b/tests/components/mazda/test_sensor.py index 763e1490e89..2284101fa84 100644 --- a/tests/components/mazda/test_sensor.py +++ b/tests/components/mazda/test_sensor.py @@ -63,7 +63,7 @@ async def test_sensors(hass): assert state.attributes.get(ATTR_ICON) == "mdi:speedometer" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING - assert state.state == "2796" + assert state.state == "2795" entry = entity_registry.async_get("sensor.my_mazda3_odometer") assert entry assert entry.unique_id == "JM000000000000000_odometer" From 2bab6447a924d63d253b210f9a6ab3ea3ca67e7d Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 10 Aug 2022 23:57:58 +0200 Subject: [PATCH 3261/3516] Replaces aiohttp.hdrs CONTENT_TYPE with plain string for the Swisscom integration (#76568) --- homeassistant/components/swisscom/device_tracker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/swisscom/device_tracker.py b/homeassistant/components/swisscom/device_tracker.py index 3c8e7341be2..d95067e6b33 100644 --- a/homeassistant/components/swisscom/device_tracker.py +++ b/homeassistant/components/swisscom/device_tracker.py @@ -4,7 +4,6 @@ from __future__ import annotations from contextlib import suppress import logging -from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol @@ -79,7 +78,7 @@ class SwisscomDeviceScanner(DeviceScanner): def get_swisscom_data(self): """Retrieve data from Swisscom and return parsed result.""" url = f"http://{self.host}/ws" - headers = {CONTENT_TYPE: "application/x-sah-ws-4-call+json"} + headers = {"Content-Type": "application/x-sah-ws-4-call+json"} data = """ {"service":"Devices", "method":"get", "parameters":{"expression":"lan and not self"}}""" From d81298a2d655493b31b43b0fde3062b5322696c2 Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Wed, 10 Aug 2022 17:59:50 -0400 Subject: [PATCH 3262/3516] Add sensor state class for SleepIQ sensors (#76372) Co-authored-by: Inca --- homeassistant/components/sleepiq/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sleepiq/sensor.py b/homeassistant/components/sleepiq/sensor.py index b101aee8a6e..71618dab056 100644 --- a/homeassistant/components/sleepiq/sensor.py +++ b/homeassistant/components/sleepiq/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from asyncsleepiq import SleepIQBed, SleepIQSleeper -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -45,6 +45,7 @@ class SleepIQSensorEntity(SleepIQSleeperEntity, SensorEntity): ) -> None: """Initialize the sensor.""" self.sensor_type = sensor_type + self._attr_state_class = SensorStateClass.MEASUREMENT super().__init__(coordinator, bed, sleeper, sensor_type) @callback From 52fd63acbce6d5b31f751cf82bbdca5ae39b8803 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 10 Aug 2022 18:05:32 -0400 Subject: [PATCH 3263/3516] Use generators for async_add_entities in Accuweather (#76574) --- .../components/accuweather/sensor.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 72182d4d635..c13dedcdceb 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -320,19 +320,19 @@ async def async_setup_entry( coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - sensors: list[AccuWeatherSensor] = [] - for description in SENSOR_TYPES: - sensors.append(AccuWeatherSensor(coordinator, description)) + sensors = [ + AccuWeatherSensor(coordinator, description) for description in SENSOR_TYPES + ] if coordinator.forecast: - for description in FORECAST_SENSOR_TYPES: - for day in range(MAX_FORECAST_DAYS + 1): - # Some air quality/allergy sensors are only available for certain - # locations. - if description.key in coordinator.data[ATTR_FORECAST][0]: - sensors.append( - AccuWeatherSensor(coordinator, description, forecast_day=day) - ) + # Some air quality/allergy sensors are only available for certain + # locations. + sensors.extend( + AccuWeatherSensor(coordinator, description, forecast_day=day) + for description in FORECAST_SENSOR_TYPES + for day in range(MAX_FORECAST_DAYS + 1) + if description.key in coordinator.data[ATTR_FORECAST][0] + ) async_add_entities(sensors) From ca3033b84cd5a982ac50388c7a2828a08c14862f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 10 Aug 2022 18:09:05 -0400 Subject: [PATCH 3264/3516] Use generators for async_add_entities in Abode (#76569) --- .../components/abode/binary_sensor.py | 10 ++++------ homeassistant/components/abode/camera.py | 9 ++++----- homeassistant/components/abode/cover.py | 10 ++++------ homeassistant/components/abode/light.py | 10 ++++------ homeassistant/components/abode/lock.py | 10 ++++------ homeassistant/components/abode/sensor.py | 19 ++++++------------- homeassistant/components/abode/switch.py | 16 +++++++++------- 7 files changed, 35 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index 58bb101cb5d..4f7af8af640 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -30,12 +30,10 @@ async def async_setup_entry( CONST.TYPE_OPENING, ] - entities = [] - - for device in data.abode.get_devices(generic_type=device_types): - entities.append(AbodeBinarySensor(data, device)) - - async_add_entities(entities) + async_add_entities( + AbodeBinarySensor(data, device) + for device in data.abode.get_devices(generic_type=device_types) + ) class AbodeBinarySensor(AbodeDevice, BinarySensorEntity): diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index 9885ccb54ef..d0f428a45fa 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -28,12 +28,11 @@ async def async_setup_entry( ) -> None: """Set up Abode camera devices.""" data: AbodeSystem = hass.data[DOMAIN] - entities = [] - for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA): - entities.append(AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE)) - - async_add_entities(entities) + async_add_entities( + AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE) + for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA) + ) class AbodeCamera(AbodeDevice, Camera): diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index 7943056f8ac..b48f00209ec 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -19,12 +19,10 @@ async def async_setup_entry( """Set up Abode cover devices.""" data: AbodeSystem = hass.data[DOMAIN] - entities = [] - - for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER): - entities.append(AbodeCover(data, device)) - - async_add_entities(entities) + async_add_entities( + AbodeCover(data, device) + for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER) + ) class AbodeCover(AbodeDevice, CoverEntity): diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index 1bb9d41f461..030e5744ce8 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -32,12 +32,10 @@ async def async_setup_entry( """Set up Abode light devices.""" data: AbodeSystem = hass.data[DOMAIN] - entities = [] - - for device in data.abode.get_devices(generic_type=CONST.TYPE_LIGHT): - entities.append(AbodeLight(data, device)) - - async_add_entities(entities) + async_add_entities( + AbodeLight(data, device) + for device in data.abode.get_devices(generic_type=CONST.TYPE_LIGHT) + ) class AbodeLight(AbodeDevice, LightEntity): diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index 368769003b0..12258a45aaf 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -19,12 +19,10 @@ async def async_setup_entry( """Set up Abode lock devices.""" data: AbodeSystem = hass.data[DOMAIN] - entities = [] - - for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK): - entities.append(AbodeLock(data, device)) - - async_add_entities(entities) + async_add_entities( + AbodeLock(data, device) + for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK) + ) class AbodeLock(AbodeDevice, LockEntity): diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index 2564b775cf9..da854153293 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -42,19 +42,12 @@ async def async_setup_entry( """Set up Abode sensor devices.""" data: AbodeSystem = hass.data[DOMAIN] - entities = [] - - for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR): - conditions = device.get_value(CONST.STATUSES_KEY) - entities.extend( - [ - AbodeSensor(data, device, description) - for description in SENSOR_TYPES - if description.key in conditions - ] - ) - - async_add_entities(entities) + async_add_entities( + AbodeSensor(data, device, description) + for description in SENSOR_TYPES + for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR) + if description.key in device.get_value(CONST.STATUSES_KEY) + ) class AbodeSensor(AbodeDevice, SensorEntity): diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index 5ed32b4f83c..f472a5028c0 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -25,14 +25,16 @@ async def async_setup_entry( """Set up Abode switch devices.""" data: AbodeSystem = hass.data[DOMAIN] - entities: list[SwitchEntity] = [] + entities: list[SwitchEntity] = [ + AbodeSwitch(data, device) + for device_type in DEVICE_TYPES + for device in data.abode.get_devices(generic_type=device_type) + ] - for device_type in DEVICE_TYPES: - for device in data.abode.get_devices(generic_type=device_type): - entities.append(AbodeSwitch(data, device)) - - for automation in data.abode.get_automations(): - entities.append(AbodeAutomationSwitch(data, automation)) + entities.extend( + AbodeAutomationSwitch(data, automation) + for automation in data.abode.get_automations() + ) async_add_entities(entities) From f7c23fe19363ab16a2a9c33e1eb9baf1e08e4eb9 Mon Sep 17 00:00:00 2001 From: Alastair D'Silva Date: Thu, 11 Aug 2022 08:09:37 +1000 Subject: [PATCH 3265/3516] Handle EmonCMS feeds that return NULL gracefully (#76074) --- homeassistant/components/emoncms/sensor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index af684067ec8..0aab21458f4 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -192,8 +192,10 @@ class EmonCmsSensor(SensorEntity): self._state = self._value_template.render_with_possible_json_value( elem["value"], STATE_UNKNOWN ) - else: + elif elem["value"] is not None: self._state = round(float(elem["value"]), DECIMALS) + else: + self._state = None @property def name(self): @@ -255,8 +257,10 @@ class EmonCmsSensor(SensorEntity): self._state = self._value_template.render_with_possible_json_value( elem["value"], STATE_UNKNOWN ) - else: + elif elem["value"] is not None: self._state = round(float(elem["value"]), DECIMALS) + else: + self._state = None class EmonCmsData: From c8f11d65d2a4ddf729107c9a6dabec5bfaabc173 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 11 Aug 2022 00:10:28 +0200 Subject: [PATCH 3266/3516] Improve type hints in demo and mqtt number entity (#76464) --- homeassistant/components/demo/number.py | 2 +- homeassistant/components/mqtt/number.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py index 17382ab9962..9613aa247fb 100644 --- a/homeassistant/components/demo/number.py +++ b/homeassistant/components/demo/number.py @@ -131,7 +131,7 @@ class DemoNumber(NumberEntity): name=self.name, ) - async def async_set_native_value(self, value): + async def async_set_native_value(self, value: float) -> None: """Update the current value.""" self._attr_native_value = value self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index fbadd653df7..4e9f237431a 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -272,7 +272,7 @@ class MqttNumber(MqttEntity, RestoreNumber): return self._config.get(CONF_UNIT_OF_MEASUREMENT) @property - def native_value(self): + def native_value(self) -> float | None: """Return the current value.""" return self._current_number From 46d3f2e14c90026c0c99f1ebd0ef7d350d8d44ba Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 11 Aug 2022 00:12:47 +0200 Subject: [PATCH 3267/3516] Improve type hints in freedompro lights (#76045) --- homeassistant/components/freedompro/light.py | 32 ++++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/freedompro/light.py b/homeassistant/components/freedompro/light.py index 7659a136927..d3f99cbd4e0 100644 --- a/homeassistant/components/freedompro/light.py +++ b/homeassistant/components/freedompro/light.py @@ -1,5 +1,8 @@ """Support for Freedompro light.""" +from __future__ import annotations + import json +from typing import Any from pyfreedompro import put_state @@ -17,6 +20,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import FreedomproDataUpdateCoordinator from .const import DOMAIN @@ -24,8 +28,8 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Freedompro light.""" - api_key = entry.data[CONF_API_KEY] - coordinator = hass.data[DOMAIN][entry.entry_id] + api_key: str = entry.data[CONF_API_KEY] + coordinator: FreedomproDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( Device(hass, api_key, device, coordinator) for device in coordinator.data @@ -36,7 +40,13 @@ async def async_setup_entry( class Device(CoordinatorEntity, LightEntity): """Representation of an Freedompro light.""" - def __init__(self, hass, api_key, device, coordinator): + def __init__( + self, + hass: HomeAssistant, + api_key: str, + device: dict[str, Any], + coordinator: FreedomproDataUpdateCoordinator, + ) -> None: """Initialize the Freedompro light.""" super().__init__(coordinator) self._session = aiohttp_client.async_get_clientsession(hass) @@ -44,9 +54,7 @@ class Device(CoordinatorEntity, LightEntity): self._attr_name = device["name"] self._attr_unique_id = device["uid"] self._attr_device_info = DeviceInfo( - identifiers={ - (DOMAIN, self.unique_id), - }, + identifiers={(DOMAIN, device["uid"])}, manufacturer="Freedompro", model=device["type"], name=self.name, @@ -87,31 +95,29 @@ class Device(CoordinatorEntity, LightEntity): await super().async_added_to_hass() self._handle_coordinator_update() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Async function to set on to light.""" - payload = {"on": True} + payload: dict[str, Any] = {"on": True} if ATTR_BRIGHTNESS in kwargs: payload["brightness"] = round(kwargs[ATTR_BRIGHTNESS] / 255 * 100) if ATTR_HS_COLOR in kwargs: payload["saturation"] = round(kwargs[ATTR_HS_COLOR][1]) payload["hue"] = round(kwargs[ATTR_HS_COLOR][0]) - payload = json.dumps(payload) await put_state( self._session, self._api_key, self._attr_unique_id, - payload, + json.dumps(payload), ) await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Async function to set off to light.""" payload = {"on": False} - payload = json.dumps(payload) await put_state( self._session, self._api_key, self._attr_unique_id, - payload, + json.dumps(payload), ) await self.coordinator.async_request_refresh() From 0edf82fcb4fd38d31d4b6b871fe5fd85e17f2e69 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 11 Aug 2022 00:16:38 +0200 Subject: [PATCH 3268/3516] Improve type hints in yamaha_musiccast number (#76467) --- homeassistant/components/yamaha_musiccast/number.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yamaha_musiccast/number.py b/homeassistant/components/yamaha_musiccast/number.py index b05c47ce279..98cda92ffea 100644 --- a/homeassistant/components/yamaha_musiccast/number.py +++ b/homeassistant/components/yamaha_musiccast/number.py @@ -1,4 +1,5 @@ """Number entities for musiccast.""" +from __future__ import annotations from aiomusiccast.capabilities import NumberSetter @@ -50,10 +51,10 @@ class NumberCapability(MusicCastCapabilityEntity, NumberEntity): self._attr_native_step = capability.value_range.step @property - def native_value(self): + def native_value(self) -> float | None: """Return the current value.""" return self.capability.current - async def async_set_native_value(self, value: float): + async def async_set_native_value(self, value: float) -> None: """Set a new value.""" await self.capability.set(value) From e0d8f0cc950509ee666cf0b8b3774f76bc76a397 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 10 Aug 2022 16:18:11 -0600 Subject: [PATCH 3269/3516] Add persistent repair items for deprecated Guardian services (#76312) Co-authored-by: Franck Nijhof --- homeassistant/components/guardian/__init__.py | 29 +++++++++++++++++-- .../components/guardian/manifest.json | 1 + .../components/guardian/strings.json | 13 +++++++++ .../components/guardian/translations/en.json | 13 +++++++++ 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 58d70667cdf..909752b5b33 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -10,6 +10,7 @@ from aioguardian import Client from aioguardian.errors import GuardianError import voluptuous as vol +from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ( ATTR_DEVICE_ID, @@ -116,14 +117,34 @@ def async_log_deprecated_service_call( call: ServiceCall, alternate_service: str, alternate_target: str, + breaks_in_ha_version: str, ) -> None: """Log a warning about a deprecated service call.""" + deprecated_service = f"{call.domain}.{call.service}" + + async_create_issue( + hass, + DOMAIN, + f"deprecated_service_{deprecated_service}", + breaks_in_ha_version=breaks_in_ha_version, + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key="deprecated_service", + translation_placeholders={ + "alternate_service": alternate_service, + "alternate_target": alternate_target, + "deprecated_service": deprecated_service, + }, + ) + LOGGER.warning( ( - 'The "%s" service is deprecated and will be removed in a future version; ' - 'use the "%s" service and pass it a target entity ID of "%s"' + 'The "%s" service is deprecated and will be removed in %s; use the "%s" ' + 'service and pass it a target entity ID of "%s"' ), - f"{call.domain}.{call.service}", + deprecated_service, + breaks_in_ha_version, alternate_service, alternate_target, ) @@ -235,6 +256,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: call, "button.press", f"button.guardian_valve_controller_{data.entry.data[CONF_UID]}_reboot", + "2022.10.0", ) await data.client.system.reboot() @@ -248,6 +270,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: call, "button.press", f"button.guardian_valve_controller_{data.entry.data[CONF_UID]}_reset_valve_diagnostics", + "2022.10.0", ) await data.client.valve.reset() diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index 7fab487563c..24dfbad13fe 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -3,6 +3,7 @@ "name": "Elexa Guardian", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/guardian", + "dependencies": ["repairs"], "requirements": ["aioguardian==2022.07.0"], "zeroconf": ["_api._udp.local."], "codeowners": ["@bachya"], diff --git a/homeassistant/components/guardian/strings.json b/homeassistant/components/guardian/strings.json index 4c60bfe4572..1665cf9f678 100644 --- a/homeassistant/components/guardian/strings.json +++ b/homeassistant/components/guardian/strings.json @@ -17,5 +17,18 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } + }, + "issues": { + "deprecated_service": { + "title": "The {deprecated_service} service is being removed", + "fix_flow": { + "step": { + "confirm": { + "title": "The {deprecated_service} service is being removed", + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`. Then, click SUBMIT below to mark this issue as resolved." + } + } + } + } } } diff --git a/homeassistant/components/guardian/translations/en.json b/homeassistant/components/guardian/translations/en.json index 310f550bcc1..ad6d0a4b7dc 100644 --- a/homeassistant/components/guardian/translations/en.json +++ b/homeassistant/components/guardian/translations/en.json @@ -17,5 +17,18 @@ "description": "Configure a local Elexa Guardian device." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`. Then, click SUBMIT below to mark this issue as resolved.", + "title": "The {deprecated_service} service is being removed" + } + } + }, + "title": "The {deprecated_service} service is being removed" + } } } \ No newline at end of file From 519d478d612dabfd4fe77705cee8103fbcf81bf0 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 11 Aug 2022 00:26:23 +0000 Subject: [PATCH 3270/3516] [ci skip] Translation update --- .../accuweather/translations/es.json | 2 +- .../components/adguard/translations/es.json | 4 +- .../components/agent_dvr/translations/es.json | 2 +- .../components/airzone/translations/es.json | 2 +- .../aladdin_connect/translations/es.json | 8 +- .../components/ambee/translations/es.json | 6 ++ .../components/ambee/translations/et.json | 6 ++ .../components/ambee/translations/tr.json | 6 ++ .../android_ip_webcam/translations/ca.json | 26 +++++++ .../android_ip_webcam/translations/de.json | 26 +++++++ .../android_ip_webcam/translations/el.json | 26 +++++++ .../android_ip_webcam/translations/es.json | 26 +++++++ .../android_ip_webcam/translations/et.json | 26 +++++++ .../android_ip_webcam/translations/no.json | 26 +++++++ .../android_ip_webcam/translations/pt-BR.json | 26 +++++++ .../android_ip_webcam/translations/tr.json | 26 +++++++ .../components/anthemav/translations/es.json | 25 +++++++ .../components/anthemav/translations/et.json | 6 ++ .../components/anthemav/translations/tr.json | 6 ++ .../components/apple_tv/translations/es.json | 2 +- .../components/arcam_fmj/translations/es.json | 2 +- .../components/asuswrt/translations/es.json | 4 +- .../components/august/translations/es.json | 2 +- .../aussie_broadband/translations/es.json | 2 +- .../components/awair/translations/es.json | 7 ++ .../components/axis/translations/es.json | 2 +- .../components/baf/translations/es.json | 6 +- .../components/blink/translations/es.json | 2 +- .../components/bluetooth/translations/es.json | 32 ++++++++ .../components/bluetooth/translations/tr.json | 32 ++++++++ .../components/broadlink/translations/es.json | 2 +- .../components/bsblan/translations/es.json | 2 +- .../components/canary/translations/es.json | 2 +- .../components/coinbase/translations/es.json | 2 +- .../components/deconz/translations/es.json | 4 +- .../components/deluge/translations/es.json | 2 +- .../components/demo/translations/el.json | 11 +++ .../components/demo/translations/es.json | 32 ++++++++ .../components/demo/translations/et.json | 13 +++- .../components/demo/translations/tr.json | 13 +++- .../components/denonavr/translations/es.json | 4 +- .../derivative/translations/es.json | 6 +- .../deutsche_bahn/translations/el.json | 8 ++ .../deutsche_bahn/translations/es.json | 8 ++ .../deutsche_bahn/translations/et.json | 8 ++ .../deutsche_bahn/translations/tr.json | 8 ++ .../components/dexcom/translations/es.json | 2 +- .../components/discord/translations/es.json | 8 +- .../components/dlna_dmr/translations/es.json | 4 +- .../components/doorbird/translations/es.json | 4 +- .../enphase_envoy/translations/es.json | 2 +- .../environment_canada/translations/es.json | 2 +- .../components/escea/translations/ca.json | 5 ++ .../components/escea/translations/el.json | 13 ++++ .../components/escea/translations/es.json | 13 ++++ .../components/escea/translations/et.json | 13 ++++ .../components/escea/translations/pt-BR.json | 13 ++++ .../components/escea/translations/tr.json | 13 ++++ .../components/esphome/translations/es.json | 4 +- .../components/ezviz/translations/es.json | 6 +- .../components/fan/translations/es.json | 2 +- .../components/fibaro/translations/es.json | 2 +- .../fireservicerota/translations/es.json | 2 +- .../flick_electric/translations/es.json | 2 +- .../components/flume/translations/es.json | 2 +- .../flunearyou/translations/el.json | 13 ++++ .../flunearyou/translations/es.json | 13 ++++ .../flunearyou/translations/et.json | 13 ++++ .../flunearyou/translations/tr.json | 13 ++++ .../components/flux_led/translations/es.json | 2 +- .../components/foscam/translations/es.json | 2 +- .../components/fritz/translations/es.json | 12 +-- .../components/fritzbox/translations/es.json | 10 +-- .../fritzbox_callmonitor/translations/es.json | 2 +- .../components/generic/translations/es.json | 74 ++++++++++--------- .../geocaching/translations/es.json | 12 +-- .../components/glances/translations/es.json | 4 +- .../components/gogogate2/translations/es.json | 2 +- .../components/google/translations/es.json | 16 +++- .../components/google/translations/et.json | 10 +++ .../components/google/translations/tr.json | 10 +++ .../components/govee_ble/translations/es.json | 21 ++++++ .../components/govee_ble/translations/tr.json | 21 ++++++ .../components/group/translations/es.json | 6 +- .../growatt_server/translations/es.json | 2 +- .../components/guardian/translations/es.json | 13 ++++ .../here_travel_time/translations/es.json | 23 ++++-- .../components/hive/translations/es.json | 13 +++- .../components/hlk_sw16/translations/es.json | 2 +- .../home_plus_control/translations/es.json | 2 +- .../homeassistant/translations/es.json | 1 + .../homeassistant_alerts/translations/es.json | 8 ++ .../homeassistant_alerts/translations/et.json | 8 ++ .../homeassistant_alerts/translations/tr.json | 8 ++ .../homekit_controller/translations/es.json | 4 +- .../translations/sensor.ca.json | 1 + .../translations/sensor.el.json | 21 ++++++ .../translations/sensor.es.json | 21 ++++++ .../translations/sensor.et.json | 21 ++++++ .../translations/sensor.tr.json | 21 ++++++ .../components/honeywell/translations/es.json | 2 +- .../components/hue/translations/es.json | 2 +- .../huisbaasje/translations/es.json | 2 +- .../hvv_departures/translations/es.json | 2 +- .../components/hyperion/translations/es.json | 2 +- .../components/iaqualink/translations/es.json | 2 +- .../components/inkbird/translations/es.json | 21 ++++++ .../components/inkbird/translations/tr.json | 21 ++++++ .../components/insteon/translations/es.json | 4 +- .../integration/translations/es.json | 2 +- .../intellifire/translations/es.json | 4 +- .../components/ipp/translations/es.json | 2 +- .../components/isy994/translations/es.json | 8 +- .../isy994/translations/zh-Hant.json | 2 +- .../components/jellyfin/translations/es.json | 2 +- .../justnimbus/translations/ca.json | 19 +++++ .../justnimbus/translations/de.json | 19 +++++ .../justnimbus/translations/el.json | 19 +++++ .../justnimbus/translations/es.json | 19 +++++ .../justnimbus/translations/et.json | 19 +++++ .../justnimbus/translations/pt-BR.json | 19 +++++ .../justnimbus/translations/tr.json | 19 +++++ .../components/knx/translations/es.json | 42 +++++------ .../components/konnected/translations/es.json | 2 +- .../lacrosse_view/translations/es.json | 20 +++++ .../lacrosse_view/translations/et.json | 20 +++++ .../lacrosse_view/translations/tr.json | 20 +++++ .../components/laundrify/translations/es.json | 12 +-- .../components/lcn/translations/es.json | 1 + .../lg_soundbar/translations/es.json | 18 +++++ .../lg_soundbar/translations/et.json | 2 +- .../lg_soundbar/translations/tr.json | 2 +- .../components/life360/translations/es.json | 27 ++++++- .../components/lifx/translations/es.json | 20 +++++ .../litterrobot/translations/sensor.es.json | 8 +- .../components/lookin/translations/es.json | 2 +- .../components/lyric/translations/es.json | 6 ++ .../components/lyric/translations/et.json | 6 ++ .../components/lyric/translations/tr.json | 6 ++ .../components/meater/translations/es.json | 6 +- .../media_player/translations/es.json | 6 +- .../components/miflora/translations/es.json | 8 ++ .../components/miflora/translations/et.json | 8 ++ .../components/miflora/translations/tr.json | 8 ++ .../components/mikrotik/translations/es.json | 2 +- .../components/min_max/translations/es.json | 6 +- .../components/mitemp_bt/translations/es.json | 8 ++ .../components/mitemp_bt/translations/et.json | 8 ++ .../components/mitemp_bt/translations/tr.json | 8 ++ .../components/moat/translations/es.json | 21 ++++++ .../components/moat/translations/tr.json | 21 ++++++ .../modem_callerid/translations/es.json | 2 +- .../motion_blinds/translations/es.json | 2 +- .../components/motioneye/translations/es.json | 4 +- .../components/mqtt/translations/es.json | 4 +- .../components/myq/translations/es.json | 2 +- .../components/mysensors/translations/ca.json | 1 + .../components/mysensors/translations/el.json | 9 +++ .../components/mysensors/translations/es.json | 9 +++ .../components/mysensors/translations/et.json | 9 +++ .../components/mysensors/translations/tr.json | 9 +++ .../components/nam/translations/es.json | 4 +- .../components/nest/translations/el.json | 10 +++ .../components/nest/translations/es.json | 40 ++++++++++ .../components/nest/translations/et.json | 10 +++ .../components/nest/translations/tr.json | 10 +++ .../components/netgear/translations/es.json | 2 +- .../components/nextdns/translations/es.json | 29 ++++++++ .../components/nina/translations/es.json | 22 ++++++ .../components/notion/translations/es.json | 2 +- .../components/nuheat/translations/es.json | 2 +- .../components/nut/translations/es.json | 2 +- .../components/nzbget/translations/es.json | 4 +- .../components/octoprint/translations/es.json | 2 +- .../components/omnilogic/translations/es.json | 2 +- .../components/onvif/translations/es.json | 2 +- .../openalpr_local/translations/es.json | 8 ++ .../openalpr_local/translations/et.json | 8 ++ .../openalpr_local/translations/tr.json | 8 ++ .../openexchangerates/translations/ca.json | 11 ++- .../openexchangerates/translations/el.json | 33 +++++++++ .../openexchangerates/translations/es.json | 33 +++++++++ .../openexchangerates/translations/et.json | 33 +++++++++ .../openexchangerates/translations/tr.json | 33 +++++++++ .../opengarage/translations/es.json | 2 +- .../opentherm_gw/translations/es.json | 3 +- .../opentherm_gw/translations/et.json | 3 +- .../opentherm_gw/translations/tr.json | 3 +- .../components/overkiz/translations/es.json | 2 +- .../overkiz/translations/sensor.es.json | 5 ++ .../ovo_energy/translations/es.json | 2 +- .../components/picnic/translations/es.json | 2 +- .../components/plex/translations/es.json | 4 +- .../components/plugwise/translations/es.json | 7 +- .../components/prosegur/translations/es.json | 4 +- .../components/ps4/translations/es.json | 4 +- .../components/qnap_qsw/translations/es.json | 12 ++- .../radiotherm/translations/es.json | 10 ++- .../radiotherm/translations/tr.json | 6 ++ .../remote/translations/zh-Hant.json | 2 +- .../components/rhasspy/translations/es.json | 12 +++ .../components/ring/translations/es.json | 2 +- .../components/roku/translations/es.json | 2 +- .../components/roon/translations/es.json | 2 +- .../ruckus_unleashed/translations/es.json | 2 +- .../components/sabnzbd/translations/es.json | 4 +- .../components/samsungtv/translations/es.json | 2 +- .../components/scrape/translations/es.json | 60 ++++++++++++++- .../script/translations/zh-Hant.json | 2 +- .../sensor/translations/zh-Hant.json | 2 +- .../sensorpush/translations/es.json | 21 ++++++ .../sensorpush/translations/tr.json | 21 ++++++ .../components/senz/translations/es.json | 14 +++- .../components/senz/translations/et.json | 6 ++ .../components/senz/translations/tr.json | 6 ++ .../components/sharkiq/translations/es.json | 4 +- .../components/shelly/translations/es.json | 4 +- .../simplepush/translations/ca.json | 1 + .../simplepush/translations/el.json | 4 + .../simplepush/translations/es.json | 31 ++++++++ .../simplepush/translations/et.json | 10 +++ .../simplepush/translations/tr.json | 10 +++ .../simplisafe/translations/el.json | 1 + .../simplisafe/translations/es.json | 14 ++-- .../simplisafe/translations/et.json | 3 +- .../simplisafe/translations/tr.json | 8 +- .../components/skybell/translations/es.json | 6 +- .../components/slack/translations/es.json | 6 +- .../components/slimproto/translations/es.json | 2 +- .../components/sma/translations/es.json | 4 +- .../smart_meter_texas/translations/es.json | 2 +- .../components/sms/translations/es.json | 2 +- .../components/sonarr/translations/es.json | 2 +- .../soundtouch/translations/es.json | 27 +++++++ .../soundtouch/translations/et.json | 6 ++ .../soundtouch/translations/tr.json | 6 ++ .../components/spider/translations/es.json | 2 +- .../components/spotify/translations/es.json | 6 ++ .../components/spotify/translations/et.json | 6 ++ .../components/spotify/translations/tr.json | 6 ++ .../components/sql/translations/es.json | 14 ++-- .../squeezebox/translations/es.json | 2 +- .../srp_energy/translations/es.json | 2 +- .../components/starline/translations/es.json | 2 +- .../steam_online/translations/es.json | 20 +++-- .../steam_online/translations/et.json | 6 ++ .../steam_online/translations/tr.json | 6 ++ .../components/subaru/translations/es.json | 6 +- .../surepetcare/translations/es.json | 2 +- .../switch/translations/zh-Hant.json | 2 +- .../switch_as_x/translations/es.json | 2 +- .../components/switchbot/translations/ca.json | 9 +++ .../components/switchbot/translations/en.json | 14 +++- .../components/switchbot/translations/es.json | 12 ++- .../components/switchbot/translations/fr.json | 9 +++ .../components/switchbot/translations/hu.json | 9 +++ .../switchbot/translations/pt-BR.json | 9 +++ .../components/switchbot/translations/tr.json | 12 ++- .../components/syncthing/translations/es.json | 2 +- .../synology_dsm/translations/es.json | 8 +- .../components/tado/translations/es.json | 2 +- .../tankerkoenig/translations/es.json | 8 +- .../components/tautulli/translations/es.json | 10 +-- .../components/tod/translations/es.json | 2 +- .../totalconnect/translations/es.json | 4 +- .../components/tradfri/translations/es.json | 2 +- .../trafikverket_ferry/translations/es.json | 8 +- .../trafikverket_train/translations/es.json | 8 +- .../transmission/translations/es.json | 12 ++- .../components/tuya/translations/es.json | 2 +- .../ukraine_alarm/translations/es.json | 10 +-- .../components/unifi/translations/es.json | 2 +- .../unifiprotect/translations/de.json | 1 + .../unifiprotect/translations/el.json | 1 + .../unifiprotect/translations/es.json | 3 +- .../unifiprotect/translations/et.json | 1 + .../unifiprotect/translations/pt-BR.json | 1 + .../unifiprotect/translations/tr.json | 1 + .../components/upcloud/translations/es.json | 2 +- .../components/update/translations/es.json | 4 +- .../components/uscis/translations/es.json | 8 ++ .../components/uscis/translations/tr.json | 8 ++ .../utility_meter/translations/es.json | 12 +-- .../components/venstar/translations/es.json | 2 +- .../components/verisure/translations/es.json | 15 +++- .../components/vulcan/translations/es.json | 18 ++--- .../components/wallbox/translations/es.json | 4 +- .../components/whirlpool/translations/es.json | 2 +- .../components/withings/translations/es.json | 4 + .../components/ws66i/translations/es.json | 6 +- .../components/xbox/translations/es.json | 6 ++ .../components/xbox/translations/et.json | 6 ++ .../components/xbox/translations/tr.json | 6 ++ .../xiaomi_aqara/translations/es.json | 2 +- .../xiaomi_ble/translations/el.json | 14 +++- .../xiaomi_ble/translations/es.json | 48 ++++++++++++ .../xiaomi_ble/translations/et.json | 14 +++- .../xiaomi_ble/translations/tr.json | 48 ++++++++++++ .../xiaomi_miio/translations/es.json | 2 +- .../yale_smart_alarm/translations/es.json | 4 +- .../yalexs_ble/translations/ca.json | 27 +++++++ .../yalexs_ble/translations/es.json | 30 ++++++++ .../yalexs_ble/translations/fr.json | 22 ++++++ .../yalexs_ble/translations/hu.json | 29 ++++++++ .../yalexs_ble/translations/pt-BR.json | 30 ++++++++ .../components/yolink/translations/es.json | 20 ++--- .../components/zha/translations/es.json | 3 + .../components/zha/translations/et.json | 2 + .../components/zha/translations/tr.json | 3 + .../zoneminder/translations/es.json | 4 +- .../components/zwave_js/translations/es.json | 4 +- 311 files changed, 2578 insertions(+), 391 deletions(-) create mode 100644 homeassistant/components/android_ip_webcam/translations/ca.json create mode 100644 homeassistant/components/android_ip_webcam/translations/de.json create mode 100644 homeassistant/components/android_ip_webcam/translations/el.json create mode 100644 homeassistant/components/android_ip_webcam/translations/es.json create mode 100644 homeassistant/components/android_ip_webcam/translations/et.json create mode 100644 homeassistant/components/android_ip_webcam/translations/no.json create mode 100644 homeassistant/components/android_ip_webcam/translations/pt-BR.json create mode 100644 homeassistant/components/android_ip_webcam/translations/tr.json create mode 100644 homeassistant/components/anthemav/translations/es.json create mode 100644 homeassistant/components/bluetooth/translations/es.json create mode 100644 homeassistant/components/bluetooth/translations/tr.json create mode 100644 homeassistant/components/deutsche_bahn/translations/el.json create mode 100644 homeassistant/components/deutsche_bahn/translations/es.json create mode 100644 homeassistant/components/deutsche_bahn/translations/et.json create mode 100644 homeassistant/components/deutsche_bahn/translations/tr.json create mode 100644 homeassistant/components/escea/translations/el.json create mode 100644 homeassistant/components/escea/translations/es.json create mode 100644 homeassistant/components/escea/translations/et.json create mode 100644 homeassistant/components/escea/translations/pt-BR.json create mode 100644 homeassistant/components/escea/translations/tr.json create mode 100644 homeassistant/components/govee_ble/translations/es.json create mode 100644 homeassistant/components/govee_ble/translations/tr.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/es.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/et.json create mode 100644 homeassistant/components/homeassistant_alerts/translations/tr.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.el.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.es.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.et.json create mode 100644 homeassistant/components/homekit_controller/translations/sensor.tr.json create mode 100644 homeassistant/components/inkbird/translations/es.json create mode 100644 homeassistant/components/inkbird/translations/tr.json create mode 100644 homeassistant/components/justnimbus/translations/ca.json create mode 100644 homeassistant/components/justnimbus/translations/de.json create mode 100644 homeassistant/components/justnimbus/translations/el.json create mode 100644 homeassistant/components/justnimbus/translations/es.json create mode 100644 homeassistant/components/justnimbus/translations/et.json create mode 100644 homeassistant/components/justnimbus/translations/pt-BR.json create mode 100644 homeassistant/components/justnimbus/translations/tr.json create mode 100644 homeassistant/components/lacrosse_view/translations/es.json create mode 100644 homeassistant/components/lacrosse_view/translations/et.json create mode 100644 homeassistant/components/lacrosse_view/translations/tr.json create mode 100644 homeassistant/components/lg_soundbar/translations/es.json create mode 100644 homeassistant/components/miflora/translations/es.json create mode 100644 homeassistant/components/miflora/translations/et.json create mode 100644 homeassistant/components/miflora/translations/tr.json create mode 100644 homeassistant/components/mitemp_bt/translations/es.json create mode 100644 homeassistant/components/mitemp_bt/translations/et.json create mode 100644 homeassistant/components/mitemp_bt/translations/tr.json create mode 100644 homeassistant/components/moat/translations/es.json create mode 100644 homeassistant/components/moat/translations/tr.json create mode 100644 homeassistant/components/nextdns/translations/es.json create mode 100644 homeassistant/components/openalpr_local/translations/es.json create mode 100644 homeassistant/components/openalpr_local/translations/et.json create mode 100644 homeassistant/components/openalpr_local/translations/tr.json create mode 100644 homeassistant/components/openexchangerates/translations/el.json create mode 100644 homeassistant/components/openexchangerates/translations/es.json create mode 100644 homeassistant/components/openexchangerates/translations/et.json create mode 100644 homeassistant/components/openexchangerates/translations/tr.json create mode 100644 homeassistant/components/rhasspy/translations/es.json create mode 100644 homeassistant/components/sensorpush/translations/es.json create mode 100644 homeassistant/components/sensorpush/translations/tr.json create mode 100644 homeassistant/components/simplepush/translations/es.json create mode 100644 homeassistant/components/soundtouch/translations/es.json create mode 100644 homeassistant/components/uscis/translations/es.json create mode 100644 homeassistant/components/uscis/translations/tr.json create mode 100644 homeassistant/components/xiaomi_ble/translations/es.json create mode 100644 homeassistant/components/xiaomi_ble/translations/tr.json create mode 100644 homeassistant/components/yalexs_ble/translations/ca.json create mode 100644 homeassistant/components/yalexs_ble/translations/es.json create mode 100644 homeassistant/components/yalexs_ble/translations/fr.json create mode 100644 homeassistant/components/yalexs_ble/translations/hu.json create mode 100644 homeassistant/components/yalexs_ble/translations/pt-BR.json diff --git a/homeassistant/components/accuweather/translations/es.json b/homeassistant/components/accuweather/translations/es.json index ef91348a727..36ccc3f0cca 100644 --- a/homeassistant/components/accuweather/translations/es.json +++ b/homeassistant/components/accuweather/translations/es.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "create_entry": { - "default": "Algunos sensores no est\u00e1n habilitados de forma predeterminada. Puede habilitarlos en el registro de la entidad despu\u00e9s de la configuraci\u00f3n de la integraci\u00f3n.\n El pron\u00f3stico del tiempo no est\u00e1 habilitado de forma predeterminada. Puedes habilitarlo en las opciones de integraci\u00f3n." + "default": "Algunos sensores no est\u00e1n habilitados de forma predeterminada. Puedes habilitarlos en el registro de la entidad despu\u00e9s de la configuraci\u00f3n de la integraci\u00f3n.\nEl pron\u00f3stico del tiempo no est\u00e1 habilitado de forma predeterminada. Puedes habilitarlo en las opciones de integraci\u00f3n." }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/adguard/translations/es.json b/homeassistant/components/adguard/translations/es.json index 7fc2f972fa1..f7bfb1203d7 100644 --- a/homeassistant/components/adguard/translations/es.json +++ b/homeassistant/components/adguard/translations/es.json @@ -18,8 +18,8 @@ "password": "Contrase\u00f1a", "port": "Puerto", "ssl": "Utiliza un certificado SSL", - "username": "Usuario", - "verify_ssl": "Verificar certificado SSL" + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL" }, "description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control." } diff --git a/homeassistant/components/agent_dvr/translations/es.json b/homeassistant/components/agent_dvr/translations/es.json index c1771d7e80b..996e006898d 100644 --- a/homeassistant/components/agent_dvr/translations/es.json +++ b/homeassistant/components/agent_dvr/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar" }, "step": { diff --git a/homeassistant/components/airzone/translations/es.json b/homeassistant/components/airzone/translations/es.json index fbb00d71bf0..1fb525c9851 100644 --- a/homeassistant/components/airzone/translations/es.json +++ b/homeassistant/components/airzone/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Fall\u00f3 la conexi\u00f3n", - "invalid_system_id": "ID de sistema Airzone inv\u00e1lido" + "invalid_system_id": "ID del sistema Airzone no v\u00e1lido" }, "step": { "user": { diff --git a/homeassistant/components/aladdin_connect/translations/es.json b/homeassistant/components/aladdin_connect/translations/es.json index ac10503ab3c..5621a1c69e9 100644 --- a/homeassistant/components/aladdin_connect/translations/es.json +++ b/homeassistant/components/aladdin_connect/translations/es.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { @@ -13,8 +13,8 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "La integraci\u00f3n de Aladdin Connect necesita volver a autenticar su cuenta", - "title": "Reautenticaci\u00f3n de la integraci\u00f3n" + "description": "La integraci\u00f3n Aladdin Connect necesita volver a autenticar tu cuenta", + "title": "Volver a autenticar la integraci\u00f3n" }, "user": { "data": { diff --git a/homeassistant/components/ambee/translations/es.json b/homeassistant/components/ambee/translations/es.json index 7f4f8b75de5..6484ac8c3a0 100644 --- a/homeassistant/components/ambee/translations/es.json +++ b/homeassistant/components/ambee/translations/es.json @@ -24,5 +24,11 @@ "description": "Configure Ambee para que se integre con Home Assistant." } } + }, + "issues": { + "pending_removal": { + "description": "La integraci\u00f3n Ambee est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nLa integraci\u00f3n se elimina porque Ambee elimin\u00f3 sus cuentas gratuitas (limitadas) y ya no proporciona una forma para que los usuarios regulares se registren en un plan pago. \n\nElimina la entrada de la integraci\u00f3n Ambee de tu instancia para solucionar este problema.", + "title": "Se va a eliminar la integraci\u00f3n Ambee" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/et.json b/homeassistant/components/ambee/translations/et.json index 085f13d6926..abb41497581 100644 --- a/homeassistant/components/ambee/translations/et.json +++ b/homeassistant/components/ambee/translations/et.json @@ -24,5 +24,11 @@ "description": "Seadista Ambee sidumine Home Assistantiga." } } + }, + "issues": { + "pending_removal": { + "description": "Ambee integratsioon on Home Assistantist eemaldamisel ja ei ole enam saadaval alates Home Assistant 2022.10.\n\nIntegratsioon eemaldatakse, sest Ambee eemaldas oma tasuta (piiratud) kontod ja ei paku tavakasutajatele enam v\u00f5imalust tasulisele plaanile registreeruda.\n\nSelle probleemi lahendamiseks eemaldage Ambee integratsiooni kirje oma instantsist.", + "title": "Ambee integratsioon eemaldatakse" + } } } \ No newline at end of file diff --git a/homeassistant/components/ambee/translations/tr.json b/homeassistant/components/ambee/translations/tr.json index 45eacf30987..0163ea40bae 100644 --- a/homeassistant/components/ambee/translations/tr.json +++ b/homeassistant/components/ambee/translations/tr.json @@ -24,5 +24,11 @@ "description": "Ambee'yi Home Assistant ile entegre olacak \u015fekilde ayarlay\u0131n." } } + }, + "issues": { + "pending_removal": { + "description": "Ambee entegrasyonu Home Assistant'tan kald\u0131r\u0131lmay\u0131 beklemektedir ve Home Assistant 2022.10'dan itibaren art\u0131k kullan\u0131lamayacakt\u0131r.\n\nEntegrasyon kald\u0131r\u0131l\u0131yor \u00e7\u00fcnk\u00fc Ambee \u00fccretsiz (s\u0131n\u0131rl\u0131) hesaplar\u0131n\u0131 kald\u0131rd\u0131 ve art\u0131k normal kullan\u0131c\u0131lar\u0131n \u00fccretli bir plana kaydolmas\u0131 i\u00e7in bir yol sa\u011flam\u0131yor.\n\nBu sorunu gidermek i\u00e7in Ambee entegrasyon giri\u015fini \u00f6rne\u011finizden kald\u0131r\u0131n.", + "title": "Ambee entegrasyonu kald\u0131r\u0131l\u0131yor" + } } } \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/ca.json b/homeassistant/components/android_ip_webcam/translations/ca.json new file mode 100644 index 00000000000..daebd1c3cd6 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/ca.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 d'Android IP Webcam mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML d'Android IP Webcam del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML d'Android IP Webcam est\u00e0 sent eliminada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/de.json b/homeassistant/components/android_ip_webcam/translations/de.json new file mode 100644 index 00000000000..3fe34f4a259 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration der Android IP Webcam mit YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die Android IP Webcam YAML-Konfiguration aus deiner configuration.yaml-Datei und starte den Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Android IP Webcam YAML Konfiguration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/el.json b/homeassistant/components/android_ip_webcam/translations/el.json new file mode 100644 index 00000000000..a4ae676b8a0 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/el.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1\u03c2 Web Android IP \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03b7\u03c2 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1\u03c2 Web Android IP \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03b7\u03c2 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1\u03c2 Web Android IP \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/es.json b/homeassistant/components/android_ip_webcam/translations/es.json new file mode 100644 index 00000000000..71c0d6cc5bc --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se eliminar\u00e1 la configuraci\u00f3n de la c\u00e1mara web IP de Android mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de la c\u00e1mara web IP de Android de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de la c\u00e1mara web IP de Android" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/et.json b/homeassistant/components/android_ip_webcam/translations/et.json new file mode 100644 index 00000000000..1b2d03a2d0a --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/et.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na", + "port": "Port", + "username": "Kasutajanimi" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Android IP veebikaamera konfigureerimine YAML-i abil eemaldatakse.\n\nOlemasolev YAML-konfiguratsioon on automaatselt kasutajaliidesesse imporditud.\n\nProbleemi lahendamiseks eemalda Android IP Webcam YAML-konfiguratsioon failist configuration.yaml ja k\u00e4ivita Home Assistant uuesti.", + "title": "Android IP veebikaamera YAML konfiguratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/no.json b/homeassistant/components/android_ip_webcam/translations/no.json new file mode 100644 index 00000000000..e9d7be1d409 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Android IP Webcam med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Android IP Webcam YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Android IP Webcam YAML-konfigurasjonen fjernes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/pt-BR.json b/homeassistant/components/android_ip_webcam/translations/pt-BR.json new file mode 100644 index 00000000000..95b5ffa7166 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/pt-BR.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Senha", + "port": "Porta", + "username": "Nome de usu\u00e1rio" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Android IP Webcam usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML do Android IP Webcam do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o de YAML do Android IP Webcam est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/tr.json b/homeassistant/components/android_ip_webcam/translations/tr.json new file mode 100644 index 00000000000..cac43d82490 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/tr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Sunucu", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Android IP Web Kameras\u0131n\u0131 YAML kullanarak yap\u0131land\u0131rma kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. \n\n Android IP Webcam YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Android IP Webcam YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/es.json b/homeassistant/components/anthemav/translations/es.json new file mode 100644 index 00000000000..7759dbd8607 --- /dev/null +++ b/homeassistant/components/anthemav/translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "cannot_receive_deviceinfo": "No se pudo recuperar la direcci\u00f3n MAC. Aseg\u00farate de que el dispositivo est\u00e9 encendido" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Puerto" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de los Receptores A/V Anthem mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de los Receptores A/V Anthem de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de los Receptores A/V Anthem" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/et.json b/homeassistant/components/anthemav/translations/et.json index 4ec356c8902..d5b9d4f224f 100644 --- a/homeassistant/components/anthemav/translations/et.json +++ b/homeassistant/components/anthemav/translations/et.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Anthem A/V-vastuv\u00f5tjate konfigureerimine YAML-i abil eemaldatakse. \n\n Teie olemasolev YAML-i konfiguratsioon imporditi kasutajaliidesesse automaatselt. \n\n Selle probleemi lahendamiseks eemaldage failist configuration.yaml konfiguratsioon Anthem A/V Receivers YAML ja taask\u00e4ivitage Home Assistant.", + "title": "Anthem A/V-vastuv\u00f5tjate YAML-konfiguratsioon eemaldatakse" + } } } \ No newline at end of file diff --git a/homeassistant/components/anthemav/translations/tr.json b/homeassistant/components/anthemav/translations/tr.json index cbe85a5319c..c77f5a1f14a 100644 --- a/homeassistant/components/anthemav/translations/tr.json +++ b/homeassistant/components/anthemav/translations/tr.json @@ -15,5 +15,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Anthem A/V Al\u0131c\u0131lar\u0131n\u0131 YAML kullanarak yap\u0131land\u0131rma kald\u0131r\u0131l\u0131yor.\n\nMevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131.\n\nAnthem A/V Al\u0131c\u0131lar\u0131 YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Anthem A/V Al\u0131c\u0131lar\u0131 YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/es.json b/homeassistant/components/apple_tv/translations/es.json index 3fe2345d6e1..6b136463aa0 100644 --- a/homeassistant/components/apple_tv/translations/es.json +++ b/homeassistant/components/apple_tv/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "backoff": "El dispositivo no acepta solicitudes de emparejamiento en este momento (es posible que hayas introducido un c\u00f3digo PIN no v\u00e1lido demasiadas veces), int\u00e9ntalo de nuevo m\u00e1s tarde.", "device_did_not_pair": "No se ha intentado finalizar el proceso de emparejamiento desde el dispositivo.", "device_not_found": "No se ha encontrado el dispositivo durante la detecci\u00f3n, por favor, intente a\u00f1adirlo de nuevo.", diff --git a/homeassistant/components/arcam_fmj/translations/es.json b/homeassistant/components/arcam_fmj/translations/es.json index 6959ee85ab1..33c0558a4ce 100644 --- a/homeassistant/components/arcam_fmj/translations/es.json +++ b/homeassistant/components/arcam_fmj/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar" }, "flow_title": "Arcam FMJ en {host}", diff --git a/homeassistant/components/asuswrt/translations/es.json b/homeassistant/components/asuswrt/translations/es.json index a2e899ef113..c714c26129a 100644 --- a/homeassistant/components/asuswrt/translations/es.json +++ b/homeassistant/components/asuswrt/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "invalid_unique_id": "No se pudo determinar ning\u00fan identificador \u00fanico v\u00e1lido del dispositivo", - "no_unique_id": "Un dispositivo sin una identificaci\u00f3n \u00fanica v\u00e1lida ya est\u00e1 configurado. La configuraci\u00f3n de una instancia m\u00faltiple no es posible" + "invalid_unique_id": "Imposible determinar una identificaci\u00f3n \u00fanica v\u00e1lida para el dispositivo", + "no_unique_id": "Ya se configur\u00f3 un dispositivo sin una identificaci\u00f3n \u00fanica v\u00e1lida. La configuraci\u00f3n de varias instancias no es posible" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/august/translations/es.json b/homeassistant/components/august/translations/es.json index cd50020bb2a..f2c7a10d0e0 100644 --- a/homeassistant/components/august/translations/es.json +++ b/homeassistant/components/august/translations/es.json @@ -21,7 +21,7 @@ "data": { "login_method": "M\u00e9todo de inicio de sesi\u00f3n", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Si el m\u00e9todo de inicio de sesi\u00f3n es \"correo electr\u00f3nico\", el nombre de usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el m\u00e9todo de inicio de sesi\u00f3n es \"tel\u00e9fono\", el nombre de usuario es el n\u00famero de tel\u00e9fono en el formato \"+NNNNNNN\".", "title": "Configurar una cuenta de August" diff --git a/homeassistant/components/aussie_broadband/translations/es.json b/homeassistant/components/aussie_broadband/translations/es.json index 497215525cb..e1ae211ca8b 100644 --- a/homeassistant/components/aussie_broadband/translations/es.json +++ b/homeassistant/components/aussie_broadband/translations/es.json @@ -27,7 +27,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/awair/translations/es.json b/homeassistant/components/awair/translations/es.json index 1a3240d3802..5b203948a7c 100644 --- a/homeassistant/components/awair/translations/es.json +++ b/homeassistant/components/awair/translations/es.json @@ -17,6 +17,13 @@ }, "description": "Por favor, vuelve a introducir tu token de acceso de desarrollador Awair." }, + "reauth_confirm": { + "data": { + "access_token": "Token de acceso", + "email": "Correo electr\u00f3nico" + }, + "description": "Por favor, vuelve a introducir tu token de acceso de desarrollador Awair." + }, "user": { "data": { "access_token": "Token de acceso", diff --git a/homeassistant/components/axis/translations/es.json b/homeassistant/components/axis/translations/es.json index 4a47b17c528..6a406614b9f 100644 --- a/homeassistant/components/axis/translations/es.json +++ b/homeassistant/components/axis/translations/es.json @@ -18,7 +18,7 @@ "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Configurar dispositivo Axis" } diff --git a/homeassistant/components/baf/translations/es.json b/homeassistant/components/baf/translations/es.json index 4e2800090d2..472c2e22c2c 100644 --- a/homeassistant/components/baf/translations/es.json +++ b/homeassistant/components/baf/translations/es.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "ipv6_not_supported": "IPv6 no est\u00e1 soportado." + "ipv6_not_supported": "IPv6 no es compatible." }, "error": { - "cannot_connect": "Error al conectar", - "unknown": "Error Inesperado" + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" }, "flow_title": "{name} - {model} ({ip_address})", "step": { diff --git a/homeassistant/components/blink/translations/es.json b/homeassistant/components/blink/translations/es.json index 1b25e1a52e2..7ff0b349555 100644 --- a/homeassistant/components/blink/translations/es.json +++ b/homeassistant/components/blink/translations/es.json @@ -20,7 +20,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Iniciar sesi\u00f3n con cuenta Blink" } diff --git a/homeassistant/components/bluetooth/translations/es.json b/homeassistant/components/bluetooth/translations/es.json new file mode 100644 index 00000000000..ec970720228 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/es.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", + "no_adapters": "No se encontraron adaptadores Bluetooth" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "enable_bluetooth": { + "description": "\u00bfQuieres configurar Bluetooth?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "El adaptador Bluetooth que se utilizar\u00e1 para escanear" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/tr.json b/homeassistant/components/bluetooth/translations/tr.json new file mode 100644 index 00000000000..a464d65dd93 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/tr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "no_adapters": "Bluetooth adapt\u00f6r\u00fc bulunamad\u0131" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "enable_bluetooth": { + "description": "Bluetooth'u kurmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "Tarama i\u00e7in kullan\u0131lacak Bluetooth Adapt\u00f6r\u00fc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/es.json b/homeassistant/components/broadlink/translations/es.json index d0020c55bca..96f791979d4 100644 --- a/homeassistant/components/broadlink/translations/es.json +++ b/homeassistant/components/broadlink/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", "not_supported": "Dispositivo no compatible", diff --git a/homeassistant/components/bsblan/translations/es.json b/homeassistant/components/bsblan/translations/es.json index 6e533b5916b..4b36080dd7c 100644 --- a/homeassistant/components/bsblan/translations/es.json +++ b/homeassistant/components/bsblan/translations/es.json @@ -15,7 +15,7 @@ "passkey": "Clave de acceso", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Configura tu dispositivo BSB-Lan para integrarse con Home Assistant.", "title": "Conectar con el dispositivo BSB-Lan" diff --git a/homeassistant/components/canary/translations/es.json b/homeassistant/components/canary/translations/es.json index 76a439da49d..2a1b3333b3d 100644 --- a/homeassistant/components/canary/translations/es.json +++ b/homeassistant/components/canary/translations/es.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Conectar a Canary" } diff --git a/homeassistant/components/coinbase/translations/es.json b/homeassistant/components/coinbase/translations/es.json index 7ecd0a753c9..32eb2af4d5c 100644 --- a/homeassistant/components/coinbase/translations/es.json +++ b/homeassistant/components/coinbase/translations/es.json @@ -33,7 +33,7 @@ "account_balance_currencies": "Saldos de la cartera para informar.", "exchange_base": "Moneda base para sensores de tipo de cambio.", "exchange_rate_currencies": "Tipos de cambio a informar.", - "exchnage_rate_precision": "N\u00famero de posiciones decimales para los tipos de cambio." + "exchnage_rate_precision": "N\u00famero de decimales para los tipos de cambio." }, "description": "Ajustar las opciones de Coinbase" } diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index 2921dc2f2bf..0974be9ff9d 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "La pasarela ya est\u00e1 configurada", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "no_bridges": "No se han descubierto pasarelas deCONZ", "no_hardware_available": "No hay hardware de radio conectado a deCONZ", "updated_instance": "Instancia deCONZ actualizada con nueva direcci\u00f3n de host" }, "error": { - "linking_not_possible": "No se ha podido enlazar con la pasarela", + "linking_not_possible": "No se pudo vincular con la puerta de enlace", "no_key": "No se pudo obtener una clave API" }, "flow_title": "{host}", diff --git a/homeassistant/components/deluge/translations/es.json b/homeassistant/components/deluge/translations/es.json index 1724791221f..c09a1ef5ef5 100644 --- a/homeassistant/components/deluge/translations/es.json +++ b/homeassistant/components/deluge/translations/es.json @@ -13,7 +13,7 @@ "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario", + "username": "Nombre de usuario", "web_port": "Puerto web (para el servicio de visita)" }, "description": "Para poder usar esta integraci\u00f3n, debe habilitar la siguiente opci\u00f3n en la configuraci\u00f3n de diluvio: Daemon > Permitir controles remotos" diff --git a/homeassistant/components/demo/translations/el.json b/homeassistant/components/demo/translations/el.json index c4c539034bd..ea1787ab8f0 100644 --- a/homeassistant/components/demo/translations/el.json +++ b/homeassistant/components/demo/translations/el.json @@ -1,5 +1,16 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u03a0\u03b9\u03ad\u03c3\u03c4\u03b5 \u03a5\u03a0\u039f\u0392\u039f\u039b\u0397 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03b9\u03ce\u03c3\u03b5\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c4\u03bf \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03b9\u03ba\u03cc \u03ad\u03c7\u03b5\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b1\u03b8\u03b5\u03af.", + "title": "\u03a4\u03bf \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b1\u03b8\u03b5\u03af" + } + } + }, + "title": "\u03a4\u03bf \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03b9\u03ba\u03cc \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03cc" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/es.json b/homeassistant/components/demo/translations/es.json index 19ebc05d089..ba1e31265b9 100644 --- a/homeassistant/components/demo/translations/es.json +++ b/homeassistant/components/demo/translations/es.json @@ -1,4 +1,36 @@ { + "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Pulsa ENVIAR para confirmar que la fuente de alimentaci\u00f3n ha sido sustituida", + "title": "La fuente de alimentaci\u00f3n necesita ser reemplazada" + } + } + }, + "title": "La fuente de alimentaci\u00f3n no es estable." + }, + "out_of_blinker_fluid": { + "fix_flow": { + "step": { + "confirm": { + "description": "Pulsa ENVIAR cuando se haya rellenado el l\u00edquido de las luces intermitentes.", + "title": "Es necesario rellenar el l\u00edquido de los intermitentes" + } + } + }, + "title": "El l\u00edquido de la luz intermitente est\u00e1 vac\u00edo y necesita ser rellenado" + }, + "transmogrifier_deprecated": { + "description": "El componente transfigurador ahora est\u00e1 obsoleto debido a la falta de control local disponible en la nueva API", + "title": "El componente transfigurador est\u00e1 obsoleto" + }, + "unfixable_problem": { + "description": "Este problema no se va a rendir nunca.", + "title": "Este no es un problema solucionable" + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/demo/translations/et.json b/homeassistant/components/demo/translations/et.json index 0e4c89dba01..ad7d7e361b6 100644 --- a/homeassistant/components/demo/translations/et.json +++ b/homeassistant/components/demo/translations/et.json @@ -1,10 +1,21 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Vajuta nuppu ESITA, et kinnitada toiteallika v\u00e4ljavahetamist", + "title": "Toiteallikas tuleb v\u00e4lja vahetada" + } + } + }, + "title": "Toiteallikas ei ole stabiilne" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { "confirm": { - "description": "Vajuta OK kui Blinkeri vedelik on uuesti t\u00e4idetud", + "description": "Vajuta ESITA kui Blinkeri vedelik on uuesti t\u00e4idetud", "title": "Blinkeri vedelikku on vaja uuesti t\u00e4ita" } } diff --git a/homeassistant/components/demo/translations/tr.json b/homeassistant/components/demo/translations/tr.json index b7ff98f9cd4..2c3f9a44200 100644 --- a/homeassistant/components/demo/translations/tr.json +++ b/homeassistant/components/demo/translations/tr.json @@ -1,10 +1,21 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "G\u00fc\u00e7 kayna\u011f\u0131n\u0131n de\u011fi\u015ftirildi\u011fini onaylamak i\u00e7in G\u00d6NDER'e bas\u0131n", + "title": "G\u00fc\u00e7 kayna\u011f\u0131n\u0131n de\u011fi\u015ftirilmesi gerekiyor" + } + } + }, + "title": "G\u00fc\u00e7 kayna\u011f\u0131 stabil de\u011fil" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { "confirm": { - "description": "Fla\u015f\u00f6r yeniden dolduruldu\u011funda Tamam'a bas\u0131n", + "description": "Fla\u015f\u00f6r s\u0131v\u0131s\u0131 yeniden dolduruldu\u011funda G\u00d6NDER d\u00fc\u011fmesine bas\u0131n", "title": "Fla\u015f\u00f6r\u00fcn yeniden doldurulmas\u0131 gerekiyor" } } diff --git a/homeassistant/components/denonavr/translations/es.json b/homeassistant/components/denonavr/translations/es.json index f5993f46cf7..d601c6a9ff0 100644 --- a/homeassistant/components/denonavr/translations/es.json +++ b/homeassistant/components/denonavr/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar, int\u00e9ntelo de nuevo, desconectar los cables de alimentaci\u00f3n y Ethernet y volver a conectarlos puede ayudar", "not_denonavr_manufacturer": "No es un Receptor AVR Denon AVR en Red, el fabricante detectado no concuerda", "not_denonavr_missing": "No es un Receptor AVR Denon AVR en Red, la informaci\u00f3n detectada no est\u00e1 completa" @@ -27,7 +27,7 @@ "host": "Direcci\u00f3n IP" }, "data_description": { - "host": "D\u00e9jelo en blanco para usar el descubrimiento autom\u00e1tico" + "host": "D\u00e9jalo en blanco para usar el descubrimiento autom\u00e1tico" } } } diff --git a/homeassistant/components/derivative/translations/es.json b/homeassistant/components/derivative/translations/es.json index e9b0919f06f..d0722b70400 100644 --- a/homeassistant/components/derivative/translations/es.json +++ b/homeassistant/components/derivative/translations/es.json @@ -33,11 +33,11 @@ }, "data_description": { "round": "Controla el n\u00famero de d\u00edgitos decimales en la salida.", - "time_window": "Si se establece, el valor del sensor es una media m\u00f3vil ponderada en el tiempo de las derivadas dentro de esta ventana.", - "unit_prefix": "a salida se escalar\u00e1 seg\u00fan el prefijo m\u00e9trico y la unidad de tiempo de la derivada seleccionados." + "time_window": "Si se establece, el valor del sensor es un promedio m\u00f3vil ponderado en el tiempo de las derivadas dentro de esta ventana.", + "unit_prefix": "La salida se escalar\u00e1 seg\u00fan el prefijo m\u00e9trico seleccionado y la unidad de tiempo de la derivada." } } } }, - "title": "Sensor derivatiu" + "title": "Sensor derivado" } \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/el.json b/homeassistant/components/deutsche_bahn/translations/el.json new file mode 100644 index 00000000000..cc1cd6eb001 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/el.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 Deutsche Bahn \u03b5\u03ba\u03ba\u03c1\u03b5\u03bc\u03b5\u03af \u03c0\u03c1\u03bf\u03c2 \u03b1\u03c6\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant 2022.11.\n\n\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03af\u03c4\u03b1\u03b9, \u03b5\u03c0\u03b5\u03b9\u03b4\u03ae \u03b2\u03b1\u03c3\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 webscraping, \u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03b4\u03b5\u03bd \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9.\n\n\u0391\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd YAML \u03c4\u03b7\u03c2 Deutsche Bahn \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 Deutsche Bahn \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/es.json b/homeassistant/components/deutsche_bahn/translations/es.json new file mode 100644 index 00000000000..32aaf5a3893 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/es.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "La integraci\u00f3n Deutsche Bahn est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.11. \n\nLa integraci\u00f3n se elimina porque se basa en webscraping, que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de Deutsche Bahn de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la integraci\u00f3n Deutsche Bahn" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/et.json b/homeassistant/components/deutsche_bahn/translations/et.json new file mode 100644 index 00000000000..a6029593a7b --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/et.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Deutsche Bahni integratsioon on Home Assistantist eemaldamisel ja see ei ole enam saadaval alates Home Assistant 2022.11.\n\nIntegratsioon eemaldatakse, sest see p\u00f5hineb veebiotsingul, mis on keelatud.\n\nProbleemi lahendamiseks eemaldage Deutsche Bahni YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivitage Home Assistant uuesti.", + "title": "Deutsche Bahni integratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/tr.json b/homeassistant/components/deutsche_bahn/translations/tr.json new file mode 100644 index 00000000000..1aeda9c4d18 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/tr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Deutsche Bahn entegrasyonu, Home Assistant'tan kald\u0131r\u0131lmay\u0131 bekliyor ve Home Assistant 2022.11'den itibaren art\u0131k kullan\u0131lamayacak. \n\n Entegrasyon kald\u0131r\u0131l\u0131yor, \u00e7\u00fcnk\u00fc izin verilmeyen web taramaya dayan\u0131yor. \n\n Deutsche Bahn YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Deutsche Bahn entegrasyonu kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/es.json b/homeassistant/components/dexcom/translations/es.json index 24d06db3ee6..58b4a014f50 100644 --- a/homeassistant/components/dexcom/translations/es.json +++ b/homeassistant/components/dexcom/translations/es.json @@ -13,7 +13,7 @@ "data": { "password": "Contrase\u00f1a", "server": "Servidor", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Introducir las credenciales de Dexcom Share", "title": "Configurar integraci\u00f3n de Dexcom" diff --git a/homeassistant/components/discord/translations/es.json b/homeassistant/components/discord/translations/es.json index f4f7bd49fc5..df4bc5a7fa3 100644 --- a/homeassistant/components/discord/translations/es.json +++ b/homeassistant/components/discord/translations/es.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -14,13 +14,13 @@ "data": { "api_token": "Token API" }, - "description": "Consulta la documentaci\u00f3n para obtener tu clave de bote de Discord.\n\n{url}" + "description": "Consulta la documentaci\u00f3n sobre c\u00f3mo obtener tu clave de bot de Discord. \n\n{url}" }, "user": { "data": { "api_token": "Token API" }, - "description": "Consulte la documentaci\u00f3n sobre c\u00f3mo obtener su clave de bot de Discord. \n\n {url}" + "description": "Consulta la documentaci\u00f3n sobre c\u00f3mo obtener tu clave de bot de Discord. \n\n{url}" } } } diff --git a/homeassistant/components/dlna_dmr/translations/es.json b/homeassistant/components/dlna_dmr/translations/es.json index 6bb9dd83dbd..6d16766de03 100644 --- a/homeassistant/components/dlna_dmr/translations/es.json +++ b/homeassistant/components/dlna_dmr/translations/es.json @@ -32,7 +32,7 @@ "data": { "host": "Host" }, - "description": "Elija un dispositivo para configurar o d\u00e9jelo en blanco para introducir una URL", + "description": "Elige un dispositivo para configurar o d\u00e9jalo en blanco para introducir una URL", "title": "Dispositivos DLNA DMR descubiertos" } } @@ -44,7 +44,7 @@ "step": { "init": { "data": { - "browse_unfiltered": "Muestra archivos multimedia incompatibles mientras navega", + "browse_unfiltered": "Mostrar medios incompatibles al navegar", "callback_url_override": "URL de devoluci\u00f3n de llamada del detector de eventos", "listen_port": "Puerto de escucha de eventos (aleatorio si no se establece)", "poll_availability": "Sondeo para la disponibilidad del dispositivo" diff --git a/homeassistant/components/doorbird/translations/es.json b/homeassistant/components/doorbird/translations/es.json index 355a48e9191..83ec7ed7a6a 100644 --- a/homeassistant/components/doorbird/translations/es.json +++ b/homeassistant/components/doorbird/translations/es.json @@ -17,7 +17,7 @@ "host": "Host", "name": "Nombre del dispositivo", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } @@ -29,7 +29,7 @@ "events": "Lista de eventos separados por comas." }, "data_description": { - "events": "A\u00f1ade un nombre de evento separado por comas para cada evento que desee rastrear. Despu\u00e9s de ingresarlos aqu\u00ed, use la aplicaci\u00f3n DoorBird para asignarlos a un evento espec\u00edfico. \n\n Ejemplo: alguien_puls\u00f3_el_bot\u00f3n, movimiento" + "events": "A\u00f1ade un nombre de evento separado por comas para cada evento que desees rastrear. Despu\u00e9s de introducirlos aqu\u00ed, usa la aplicaci\u00f3n DoorBird para asignarlos a un evento espec\u00edfico. \n\nEjemplo: alguien_puls\u00f3_el_bot\u00f3n, movimiento" } } } diff --git a/homeassistant/components/enphase_envoy/translations/es.json b/homeassistant/components/enphase_envoy/translations/es.json index 8bf1359cefa..24a63fa6c73 100644 --- a/homeassistant/components/enphase_envoy/translations/es.json +++ b/homeassistant/components/enphase_envoy/translations/es.json @@ -15,7 +15,7 @@ "data": { "host": "Host", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Para los modelos m\u00e1s nuevos, introduzca el nombre de usuario `envoy` sin contrase\u00f1a. Para los modelos m\u00e1s antiguos, introduzca el nombre de usuario `installer` sin contrase\u00f1a. Para todos los dem\u00e1s modelos, introduzca un nombre de usuario y una contrase\u00f1a v\u00e1lidos." } diff --git a/homeassistant/components/environment_canada/translations/es.json b/homeassistant/components/environment_canada/translations/es.json index 2c476c6c90f..7cbfb8caed9 100644 --- a/homeassistant/components/environment_canada/translations/es.json +++ b/homeassistant/components/environment_canada/translations/es.json @@ -15,7 +15,7 @@ "longitude": "Longitud", "station": "ID de la estaci\u00f3n meteorol\u00f3gica" }, - "description": "Se debe especificar un ID de estaci\u00f3n o una latitud/longitud. La latitud/longitud utilizada por defecto son los valores configurados en su instalaci\u00f3n de Home Assistant. Si se especifican coordenadas, se utilizar\u00e1 la estaci\u00f3n meteorol\u00f3gica m\u00e1s cercana a las mismas. Si se utiliza un c\u00f3digo de estaci\u00f3n debe seguir el formato PP/c\u00f3digo, donde PP es la provincia de dos letras y c\u00f3digo es el ID de la estaci\u00f3n. La lista de IDs de estaciones se puede encontrar aqu\u00ed: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. La informaci\u00f3n meteorol\u00f3gica puede obtenerse en ingl\u00e9s o en franc\u00e9s.", + "description": "Se debe especificar un ID de estaci\u00f3n o una latitud/longitud. La latitud/longitud utilizada por defecto son los valores configurados en tu instalaci\u00f3n de Home Assistant. Si se especifican coordenadas, se utilizar\u00e1 la estaci\u00f3n meteorol\u00f3gica m\u00e1s cercana a las mismas. Si se utiliza un c\u00f3digo de estaci\u00f3n debe seguir el formato PP/c\u00f3digo, donde PP es la provincia de dos letras y c\u00f3digo es el ID de la estaci\u00f3n. La lista de IDs de estaciones se puede encontrar aqu\u00ed: https://dd.weather.gc.ca/citypage_weather/docs/site_list_towns_en.csv. La informaci\u00f3n meteorol\u00f3gica puede obtenerse en ingl\u00e9s o en franc\u00e9s.", "title": "Environment Canada: ubicaci\u00f3n meteorol\u00f3gica e idioma" } } diff --git a/homeassistant/components/escea/translations/ca.json b/homeassistant/components/escea/translations/ca.json index d28b9050682..dcc35592fff 100644 --- a/homeassistant/components/escea/translations/ca.json +++ b/homeassistant/components/escea/translations/ca.json @@ -3,6 +3,11 @@ "abort": { "no_devices_found": "No s'han trobat dispositius a la xarxa", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "confirm": { + "description": "Vols configurar la llar de foc Escea?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/escea/translations/el.json b/homeassistant/components/escea/translations/el.json new file mode 100644 index 00000000000..340d28fe43a --- /dev/null +++ b/homeassistant/components/escea/translations/el.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + }, + "step": { + "confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03b3\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03c4\u03b6\u03ac\u03ba\u03b9 Escea;" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/es.json b/homeassistant/components/escea/translations/es.json new file mode 100644 index 00000000000..0f208dac321 --- /dev/null +++ b/homeassistant/components/escea/translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos en la red", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "step": { + "confirm": { + "description": "\u00bfQuieres configurar una chimenea Escea?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/et.json b/homeassistant/components/escea/translations/et.json new file mode 100644 index 00000000000..a52a0d2c22c --- /dev/null +++ b/homeassistant/components/escea/translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "V\u00f5rgust seadmeid ei leitud", + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks sidumine" + }, + "step": { + "confirm": { + "description": "Kas luua Escea kamina sidumine?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/pt-BR.json b/homeassistant/components/escea/translations/pt-BR.json new file mode 100644 index 00000000000..5bcd3a25634 --- /dev/null +++ b/homeassistant/components/escea/translations/pt-BR.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 foi configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "confirm": { + "description": "Quer montar uma lareira Escea?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/escea/translations/tr.json b/homeassistant/components/escea/translations/tr.json new file mode 100644 index 00000000000..1e2ba7839a0 --- /dev/null +++ b/homeassistant/components/escea/translations/tr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Escea \u015f\u00f6minesi kurmak ister misiniz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/es.json b/homeassistant/components/esphome/translations/es.json index c682ddab294..68e53160322 100644 --- a/homeassistant/components/esphome/translations/es.json +++ b/homeassistant/components/esphome/translations/es.json @@ -9,7 +9,7 @@ "connection_error": "No se puede conectar a ESP. Aseg\u00farate de que tu archivo YAML contenga una l\u00ednea 'api:'.", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_psk": "La clave de transporte cifrado no es v\u00e1lida. Por favor, aseg\u00farese de que coincide con la que tiene en su configuraci\u00f3n", - "resolve_error": "No se puede resolver la direcci\u00f3n de ESP. Si el error persiste, configura una direcci\u00f3n IP est\u00e1tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "No se puede resolver la direcci\u00f3n del ESP. Si este error persiste, por favor, configura una direcci\u00f3n IP est\u00e1tica" }, "flow_title": "{name}", "step": { @@ -40,7 +40,7 @@ "host": "Host", "port": "Puerto" }, - "description": "Introduce la configuraci\u00f3n de la conexi\u00f3n de tu nodo [ESPHome](https://esphomelib.com/)." + "description": "Por favor, introduce la configuraci\u00f3n de conexi\u00f3n de tu nodo [ESPHome]({esphome_url})." } } } diff --git a/homeassistant/components/ezviz/translations/es.json b/homeassistant/components/ezviz/translations/es.json index eef8343c17a..f8a80614fb0 100644 --- a/homeassistant/components/ezviz/translations/es.json +++ b/homeassistant/components/ezviz/translations/es.json @@ -15,7 +15,7 @@ "confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Introduce las credenciales RTSP para la c\u00e1mara Ezviz {serial} con IP {ip_address}", "title": "Descubierta c\u00e1mara Ezviz" @@ -24,7 +24,7 @@ "data": { "password": "Contrase\u00f1a", "url": "URL", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Conectar con Ezviz Cloud" }, @@ -32,7 +32,7 @@ "data": { "password": "Contrase\u00f1a", "url": "URL", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Especificar manualmente la URL de tu regi\u00f3n", "title": "Conectar con la URL personalizada de Ezviz" diff --git a/homeassistant/components/fan/translations/es.json b/homeassistant/components/fan/translations/es.json index b66ca0b0256..e2bc0b1611d 100644 --- a/homeassistant/components/fan/translations/es.json +++ b/homeassistant/components/fan/translations/es.json @@ -1,7 +1,7 @@ { "device_automation": { "action_type": { - "toggle": "Cambiar {entity_name}", + "toggle": "Alternar {entity_name}", "turn_off": "Desactivar {entity_name}", "turn_on": "Activar {entity_name}" }, diff --git a/homeassistant/components/fibaro/translations/es.json b/homeassistant/components/fibaro/translations/es.json index 99f29f3bee5..567ae7e59a2 100644 --- a/homeassistant/components/fibaro/translations/es.json +++ b/homeassistant/components/fibaro/translations/es.json @@ -14,7 +14,7 @@ "import_plugins": "\u00bfImportar entidades desde los plugins de fibaro?", "password": "Contrase\u00f1a", "url": "URL en el format http://HOST/api/", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/fireservicerota/translations/es.json b/homeassistant/components/fireservicerota/translations/es.json index 085bce8f343..b86ba3b2164 100644 --- a/homeassistant/components/fireservicerota/translations/es.json +++ b/homeassistant/components/fireservicerota/translations/es.json @@ -21,7 +21,7 @@ "data": { "password": "Contrase\u00f1a", "url": "Sitio web", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/flick_electric/translations/es.json b/homeassistant/components/flick_electric/translations/es.json index 424c73da24f..10b77911016 100644 --- a/homeassistant/components/flick_electric/translations/es.json +++ b/homeassistant/components/flick_electric/translations/es.json @@ -14,7 +14,7 @@ "client_id": "ID de cliente (Opcional)", "client_secret": "Secreto de Cliente (Opcional)", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Credenciales de Inicio de Sesi\u00f3n de Flick" } diff --git a/homeassistant/components/flume/translations/es.json b/homeassistant/components/flume/translations/es.json index e8baedf243c..5789a2eccc0 100644 --- a/homeassistant/components/flume/translations/es.json +++ b/homeassistant/components/flume/translations/es.json @@ -22,7 +22,7 @@ "client_id": "Client ID", "client_secret": "Client Secret", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Para acceder a la API Personal de Flume, tendr\u00e1s que solicitar un 'Client ID' y un 'Client Secret' en https://portal.flumetech.com/settings#token", "title": "Conectar con tu cuenta de Flume" diff --git a/homeassistant/components/flunearyou/translations/el.json b/homeassistant/components/flunearyou/translations/el.json index 972611ff6b1..ca6fae97190 100644 --- a/homeassistant/components/flunearyou/translations/el.json +++ b/homeassistant/components/flunearyou/translations/el.json @@ -16,5 +16,18 @@ "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0397 \u03b5\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03ae \u03c0\u03b7\u03b3\u03ae \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd \u03c0\u03bf\u03c5 \u03c4\u03c1\u03bf\u03c6\u03bf\u03b4\u03bf\u03c4\u03b5\u03af \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Flu Near You \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7. \u0395\u03c0\u03bf\u03bc\u03ad\u03bd\u03c9\u03c2, \u03b7 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03c0\u03bb\u03ad\u03bf\u03bd. \n\n \u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 SUBMIT \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7 \u03b3\u03c1\u03af\u03c0\u03b7 \u03ba\u03bf\u03bd\u03c4\u03ac \u03c3\u03b1\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 Home Assistant.", + "title": "\u0391\u03c6\u03b1\u03b9\u03c1\u03ad\u03c3\u03c4\u03b5 \u03c4\u03bf Flu Near You" + } + } + }, + "title": "\u03a4\u03bf Flu Near You \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03bf" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/es.json b/homeassistant/components/flunearyou/translations/es.json index 5d7b8fb6a6f..b205be8270c 100644 --- a/homeassistant/components/flunearyou/translations/es.json +++ b/homeassistant/components/flunearyou/translations/es.json @@ -16,5 +16,18 @@ "title": "Configurar Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "La fuente de datos externa que alimenta la integraci\u00f3n Flu Near You ya no est\u00e1 disponible; por lo tanto, la integraci\u00f3n ya no funciona. \n\nPulsar ENVIAR para eliminar Flu Near You de tu instancia Home Assistant.", + "title": "Eliminar Flu Near You" + } + } + }, + "title": "Flu Near You ya no est\u00e1 disponible" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/et.json b/homeassistant/components/flunearyou/translations/et.json index 9d79d166ef7..447ad54c25a 100644 --- a/homeassistant/components/flunearyou/translations/et.json +++ b/homeassistant/components/flunearyou/translations/et.json @@ -16,5 +16,18 @@ "title": "Seadista Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "Flu Near You integratsiooni k\u00e4ivitav v\u00e4line andmeallikas ei ole enam saadaval; seega ei t\u00f6\u00f6ta integratsioon enam.\n\nVajutage SUBMIT, et eemaldada Flu Near You oma Home Assistant'i instantsist.", + "title": "Eemalda Flu Near You" + } + } + }, + "title": "Flu Near You pole enam saadaval" + } } } \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/tr.json b/homeassistant/components/flunearyou/translations/tr.json index 3a21364502e..1e762c75f7a 100644 --- a/homeassistant/components/flunearyou/translations/tr.json +++ b/homeassistant/components/flunearyou/translations/tr.json @@ -16,5 +16,18 @@ "title": "Flu Near You'yu Yap\u0131land\u0131r\u0131n" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "Flu Near You entegrasyonuna g\u00fc\u00e7 sa\u011flayan harici veri kayna\u011f\u0131 art\u0131k mevcut de\u011fil; bu nedenle, entegrasyon art\u0131k \u00e7al\u0131\u015fm\u0131yor. \n\n Home Assistant \u00f6rne\u011finden Flu Near You'yu kald\u0131rmak i\u00e7in G\u00d6NDER'e bas\u0131n.", + "title": "Yak\u0131n\u0131n\u0131zdaki Flu Near You'yu Kald\u0131r\u0131n" + } + } + }, + "title": "Flu Near You art\u0131k mevcut de\u011fil" + } } } \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/es.json b/homeassistant/components/flux_led/translations/es.json index e4431b7a1d1..54fef29c371 100644 --- a/homeassistant/components/flux_led/translations/es.json +++ b/homeassistant/components/flux_led/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "no_devices_found": "No se encontraron dispositivos en la red" }, "error": { diff --git a/homeassistant/components/foscam/translations/es.json b/homeassistant/components/foscam/translations/es.json index f80ef3335d1..93e01fabbc9 100644 --- a/homeassistant/components/foscam/translations/es.json +++ b/homeassistant/components/foscam/translations/es.json @@ -17,7 +17,7 @@ "port": "Puerto", "rtsp_port": "Puerto RTSP", "stream": "Stream", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/fritz/translations/es.json b/homeassistant/components/fritz/translations/es.json index f1386a9e87f..21dafaec367 100644 --- a/homeassistant/components/fritz/translations/es.json +++ b/homeassistant/components/fritz/translations/es.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", - "ignore_ip6_link_local": "El enlace con direcciones IPv6 locales no est\u00e1 permitido", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "ignore_ip6_link_local": "La direcci\u00f3n local del enlace IPv6 no es compatible.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "upnp_not_configured": "Falta la configuraci\u00f3n de UPnP en el dispositivo." @@ -18,7 +18,7 @@ "confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Descubierto FRITZ!Box: {name}\n\nConfigurar FRITZ!Box Tools para controlar tu {name}", "title": "Configurar FRITZ!Box Tools" @@ -26,7 +26,7 @@ "reauth_confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Actualizar credenciales de FRITZ!Box Tools para: {host}.\n\n FRITZ!Box Tools no puede iniciar sesi\u00f3n en tu FRITZ!Box.", "title": "Actualizando FRITZ!Box Tools - credenciales" @@ -36,7 +36,7 @@ "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Configure las herramientas de FRITZ! Box para controlar su FRITZ! Box.\n M\u00ednimo necesario: nombre de usuario, contrase\u00f1a.", "title": "Configurar las herramientas de FRITZ! Box" diff --git a/homeassistant/components/fritzbox/translations/es.json b/homeassistant/components/fritzbox/translations/es.json index 77c8bb64ee5..33dfc59d543 100644 --- a/homeassistant/components/fritzbox/translations/es.json +++ b/homeassistant/components/fritzbox/translations/es.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_configured": "Este AVM FRITZ!Box ya est\u00e1 configurado.", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", - "ignore_ip6_link_local": "El enlace con direcciones IPv6 locales no est\u00e1 permitido", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "ignore_ip6_link_local": "La direcci\u00f3n local del enlace IPv6 no es compatible.", "no_devices_found": "No se encontraron dispositivos en la red", "not_supported": "Conectado a AVM FRITZ!Box pero no es capaz de controlar dispositivos Smart Home.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" @@ -16,14 +16,14 @@ "confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "\u00bfQuieres configurar {name}?" }, "reauth_confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Actualice la informaci\u00f3n de inicio de sesi\u00f3n para {name}." }, @@ -31,7 +31,7 @@ "data": { "host": "Host", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Introduce tu informaci\u00f3n de AVM FRITZ!Box." } diff --git a/homeassistant/components/fritzbox_callmonitor/translations/es.json b/homeassistant/components/fritzbox_callmonitor/translations/es.json index 61e295d3b99..3be51f3bb6d 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/es.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/es.json @@ -20,7 +20,7 @@ "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/generic/translations/es.json b/homeassistant/components/generic/translations/es.json index 8e1189f30f1..ac5c16c3120 100644 --- a/homeassistant/components/generic/translations/es.json +++ b/homeassistant/components/generic/translations/es.json @@ -5,62 +5,66 @@ "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { - "already_exists": "Ya hay una c\u00e1mara con esa URL de configuraci\u00f3n.", - "invalid_still_image": "La URL no ha devuelto una imagen fija v\u00e1lida", - "no_still_image_or_stream_url": "Tienes que especificar al menos una imagen una URL de flujo", + "already_exists": "Ya existe una c\u00e1mara con esta configuraci\u00f3n de URL.", + "invalid_still_image": "La URL no devolvi\u00f3 una imagen fija v\u00e1lida", + "malformed_url": "URL con formato incorrecto", + "no_still_image_or_stream_url": "Debes especificar al menos una imagen fija o URL de transmisi\u00f3n", + "relative_url": "No se permiten URLs relativas", "stream_file_not_found": "No se encontr\u00f3 el archivo al intentar conectarse a la transmisi\u00f3n (\u00bfest\u00e1 instalado ffmpeg?)", - "stream_http_not_found": "HTTP 404 'Not found' al intentar conectarse al flujo de datos ('stream')", + "stream_http_not_found": "HTTP 404 Not found al intentar conectarse a la transmisi\u00f3n", "stream_io_error": "Error de entrada/salida al intentar conectarse a la transmisi\u00f3n. \u00bfProtocolo de transporte RTSP incorrecto?", - "stream_no_route_to_host": "No se pudo encontrar el anfitri\u00f3n mientras intentaba conectar al flujo de datos", - "stream_no_video": "El flujo no contiene v\u00eddeo", + "stream_no_route_to_host": "No se pudo encontrar el host al intentar conectarse a la transmisi\u00f3n", + "stream_no_video": "La transmisi\u00f3n no tiene video", "stream_not_permitted": "Operaci\u00f3n no permitida al intentar conectarse a la transmisi\u00f3n. \u00bfProtocolo de transporte RTSP incorrecto?", - "stream_unauthorised": "La autorizaci\u00f3n ha fallado mientras se intentaba conectar con el flujo de datos", - "template_error": "Error al renderizar la plantilla. Revise el registro para m\u00e1s informaci\u00f3n.", - "timeout": "El tiempo m\u00e1ximo de carga de la URL ha expirado", - "unable_still_load": "No se puede cargar una imagen v\u00e1lida desde la URL de la imagen fija (p. ej., host no v\u00e1lido, URL o error de autenticaci\u00f3n). Revise el registro para obtener m\u00e1s informaci\u00f3n.", + "stream_unauthorised": "La autorizaci\u00f3n fall\u00f3 al intentar conectarse a la transmisi\u00f3n", + "template_error": "Error al renderizar la plantilla. Revisa el registro para obtener m\u00e1s informaci\u00f3n.", + "timeout": "Tiempo de espera al cargar la URL", + "unable_still_load": "No se puede cargar una imagen v\u00e1lida desde la URL de la imagen fija (p. ej., host no v\u00e1lido, URL o error de autenticaci\u00f3n). Revisa el registro para obtener m\u00e1s informaci\u00f3n.", "unknown": "Error inesperado" }, "step": { "confirm": { - "description": "\u00bfQuiere empezar a configurar?" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" }, "content_type": { "data": { "content_type": "Tipos de contenido" }, - "description": "Especifique el tipo de contenido para el flujo de datos (stream)." + "description": "Especifica el tipo de contenido para el flujo." }, "user": { "data": { "authentication": "Autenticaci\u00f3n", - "framerate": "Frecuencia de visualizaci\u00f3n (Hz)", - "limit_refetch_to_url_change": "Limita la lectura al cambio de URL", + "framerate": "Velocidad de fotogramas (Hz)", + "limit_refetch_to_url_change": "Limitar recuperaci\u00f3n al cambio de URL", "password": "Contrase\u00f1a", "rtsp_transport": "Protocolo de transporte RTSP", - "still_image_url": "URL de imagen fija (ej. http://...)", - "stream_source": "URL origen del flux (p. ex. rtsp://...)", - "username": "Usuario", - "verify_ssl": "Verifica el certificat SSL" + "still_image_url": "URL de la imagen fija (p. ej., http://...)", + "stream_source": "URL de origen de la transmisi\u00f3n (p. ej., rtsp://...)", + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL" }, - "description": "Introduzca los ajustes para conectarse a la c\u00e1mara." + "description": "Introduce los ajustes para conectarte a la c\u00e1mara." } } }, "options": { "error": { - "already_exists": "Ya hay una c\u00e1mara con esa URL de configuraci\u00f3n.", + "already_exists": "Ya existe una c\u00e1mara con esta configuraci\u00f3n de URL.", "invalid_still_image": "La URL no devolvi\u00f3 una imagen fija v\u00e1lida", - "no_still_image_or_stream_url": "Debe especificar al menos una imagen fija o URL de transmisi\u00f3n", + "malformed_url": "URL con formato incorrecto", + "no_still_image_or_stream_url": "Debes especificar al menos una imagen fija o URL de transmisi\u00f3n", + "relative_url": "No se permiten URLs relativas", "stream_file_not_found": "No se encontr\u00f3 el archivo al intentar conectarse a la transmisi\u00f3n (\u00bfest\u00e1 instalado ffmpeg?)", - "stream_http_not_found": "HTTP 404 No encontrado al intentar conectarse a la transmisi\u00f3n", + "stream_http_not_found": "HTTP 404 Not found al intentar conectarse a la transmisi\u00f3n", "stream_io_error": "Error de entrada/salida al intentar conectarse a la transmisi\u00f3n. \u00bfProtocolo de transporte RTSP incorrecto?", - "stream_no_route_to_host": "No se pudo encontrar el anfitri\u00f3n mientras intentaba conectar al flujo de datos", + "stream_no_route_to_host": "No se pudo encontrar el host al intentar conectarse a la transmisi\u00f3n", "stream_no_video": "La transmisi\u00f3n no tiene video", "stream_not_permitted": "Operaci\u00f3n no permitida al intentar conectarse a la transmisi\u00f3n. \u00bfProtocolo de transporte RTSP incorrecto?", - "stream_unauthorised": "La autorizaci\u00f3n ha fallado mientras se intentaba conectar con el flujo de datos", - "template_error": "Error al renderizar la plantilla. Revise el registro para m\u00e1s informaci\u00f3n.", - "timeout": "El tiempo m\u00e1ximo de carga de la URL ha expirado", - "unable_still_load": "No se puede cargar una imagen v\u00e1lida desde la URL de la imagen fija (por ejemplo, host no v\u00e1lido, URL o error de autenticaci\u00f3n). Revise el registro para obtener m\u00e1s informaci\u00f3n.", + "stream_unauthorised": "La autorizaci\u00f3n fall\u00f3 al intentar conectarse a la transmisi\u00f3n", + "template_error": "Error al renderizar la plantilla. Revisa el registro para obtener m\u00e1s informaci\u00f3n.", + "timeout": "Tiempo de espera al cargar la URL", + "unable_still_load": "No se puede cargar una imagen v\u00e1lida desde la URL de la imagen fija (p. ej., host no v\u00e1lido, URL o error de autenticaci\u00f3n). Revisa el registro para obtener m\u00e1s informaci\u00f3n.", "unknown": "Error inesperado" }, "step": { @@ -68,23 +72,23 @@ "data": { "content_type": "Tipos de contenido" }, - "description": "Especifique el tipo de contenido para el flujo de datos (stream)." + "description": "Especifica el tipo de contenido para el flujo." }, "init": { "data": { "authentication": "Autenticaci\u00f3n", - "framerate": "Frecuencia de visualizaci\u00f3n (Hz)", - "limit_refetch_to_url_change": "Limita la lectura al cambio de URL", + "framerate": "Velocidad de fotogramas (Hz)", + "limit_refetch_to_url_change": "Limitar recuperaci\u00f3n al cambio de URL", "password": "Contrase\u00f1a", "rtsp_transport": "Protocolo de transporte RTSP", - "still_image_url": "URL de imagen fija (ej. http://...)", - "stream_source": "URL de origen de la transmisi\u00f3n (por ejemplo, rtsp://...)", + "still_image_url": "URL de la imagen fija (p. ej., http://...)", + "stream_source": "URL de origen de la transmisi\u00f3n (p. ej., rtsp://...)", "use_wallclock_as_timestamps": "Usar el reloj de pared como marca de tiempo", - "username": "Usuario", - "verify_ssl": "Verifica el certificado SSL" + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL" }, "data_description": { - "use_wallclock_as_timestamps": "Esta opci\u00f3n puede corregir los problemas de segmentaci\u00f3n o bloqueo que surgen de las implementaciones de marcas de tiempo defectuosas en algunas c\u00e1maras" + "use_wallclock_as_timestamps": "Esta opci\u00f3n puede corregir problemas de segmentaci\u00f3n o bloqueo que surgen de implementaciones de marcas de tiempo con errores en algunas c\u00e1maras." } } } diff --git a/homeassistant/components/geocaching/translations/es.json b/homeassistant/components/geocaching/translations/es.json index 8fd4800c588..712f554ac19 100644 --- a/homeassistant/components/geocaching/translations/es.json +++ b/homeassistant/components/geocaching/translations/es.json @@ -4,13 +4,13 @@ "already_configured": "La cuenta ya est\u00e1 configurada", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Mira su documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [compruebe la secci\u00f3n de ayuda]({docs_url})", - "oauth_error": "Se han recibido datos token inv\u00e1lidos.", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "create_entry": { - "default": "Autenticaci\u00f3n exitosa" + "default": "Autenticado correctamente" }, "step": { "pick_implementation": { @@ -18,7 +18,7 @@ }, "reauth_confirm": { "description": "La integraci\u00f3n Geocaching debe volver a autenticarse con tu cuenta", - "title": "Reautenticaci\u00f3n de la integraci\u00f3n" + "title": "Volver a autenticar la integraci\u00f3n" } } } diff --git a/homeassistant/components/glances/translations/es.json b/homeassistant/components/glances/translations/es.json index 5560276ff3d..01f2a132136 100644 --- a/homeassistant/components/glances/translations/es.json +++ b/homeassistant/components/glances/translations/es.json @@ -15,8 +15,8 @@ "password": "Contrase\u00f1a", "port": "Puerto", "ssl": "Utiliza un certificado SSL", - "username": "Usuario", - "verify_ssl": "Verificar certificado SSL", + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL", "version": "Versi\u00f3n API Glances (2 o 3)" }, "title": "Configurar Glances" diff --git a/homeassistant/components/gogogate2/translations/es.json b/homeassistant/components/gogogate2/translations/es.json index 65d14508ce6..877147e46a0 100644 --- a/homeassistant/components/gogogate2/translations/es.json +++ b/homeassistant/components/gogogate2/translations/es.json @@ -13,7 +13,7 @@ "data": { "ip_address": "Direcci\u00f3n IP", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Proporciona la informaci\u00f3n requerida a continuaci\u00f3n.", "title": "Configurar GotoGate2" diff --git a/homeassistant/components/google/translations/es.json b/homeassistant/components/google/translations/es.json index 9e777e6b377..5bd628e1f6e 100644 --- a/homeassistant/components/google/translations/es.json +++ b/homeassistant/components/google/translations/es.json @@ -1,16 +1,18 @@ { "application_credentials": { - "description": "Sigue las [instrucciones]({more_info_url}) para la [pantalla de consentimiento de OAuth]({oauth_consent_url}) para dar acceso a Home Assistant a tu Google Calendar. Tambi\u00e9n necesita crear credenciales de aplicaci\u00f3n vinculadas a su calendario:\n1. Vaya a [Credenciales]({oauth_creds_url}) y haga clic en **Crear credenciales**.\n1. En la lista desplegable, seleccione **ID de cliente de OAuth**.\n1. Seleccione **TV y dispositivos de entrada limitada** para el tipo de aplicaci\u00f3n." + "description": "Sigue las [instrucciones]({more_info_url}) para la [pantalla de consentimiento de OAuth]({oauth_consent_url}) para dar acceso a Home Assistant a tu Google Calendar. Tambi\u00e9n necesitas crear credenciales de aplicaci\u00f3n vinculadas a tu calendario:\n 1. Ve a [Credenciales]( {oauth_creds_url} ) y haz clic en **Crear credenciales**.\n 1. En la lista desplegable, selecciona **ID de cliente de OAuth**.\n 1. Selecciona **TV y dispositivos de entrada limitada** para el tipo de aplicaci\u00f3n." }, "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "already_in_progress": "El proceso de configuraci\u00f3n ya est\u00e1 en curso", + "cannot_connect": "No se pudo conectar", "code_expired": "El c\u00f3digo de autenticaci\u00f3n caduc\u00f3 o la configuraci\u00f3n de la credencial no es v\u00e1lida, int\u00e9ntelo de nuevo.", "invalid_access_token": "Token de acceso no v\u00e1lido", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "oauth_error": "Se han recibido datos token inv\u00e1lidos.", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente", + "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n" }, "create_entry": { "default": "Autenticaci\u00f3n exitosa" @@ -31,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3n de Google Calendar en configuration.yaml se eliminar\u00e1 en Home Assistant 2022.9. \n\nTs credenciales OAuth de aplicaci\u00f3n existentes y la configuraci\u00f3n de acceso se han importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Google Calendar" + }, + "removed_track_new_yaml": { + "description": "Has inhabilitado el seguimiento de entidades para Google Calendar en configuration.yaml, que ya no es compatible. Debes cambiar manualmente las opciones del sistema de integraci\u00f3n en la IU para deshabilitar las entidades reci\u00e9n descubiertas en el futuro. Elimina la configuraci\u00f3n track_new de configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "El seguimiento de entidades de Google Calendar ha cambiado" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/et.json b/homeassistant/components/google/translations/et.json index c516c9201e2..83dda4e151c 100644 --- a/homeassistant/components/google/translations/et.json +++ b/homeassistant/components/google/translations/et.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Google'i kalendri konfigureerimine failis configuration.yaml eemaldatakse versioonis Home Assistant 2022.9.\n\nTeie olemasolevad OAuth-rakenduse volitused ja juurdep\u00e4\u00e4su seaded on automaatselt kasutajaliidesesse imporditud. Probleemi lahendamiseks eemaldage YAML-konfiguratsioon failist configuration.yaml ja taask\u00e4ivitage Home Assistant.", + "title": "Google'i kalendri YAML-i konfiguratsioon eemaldatakse" + }, + "removed_track_new_yaml": { + "description": "Oled keelanud Google'i kalendri olemite j\u00e4lgimise rakenduses configuration.yaml, mida enam ei toetata. Peate kasutajaliideses integratsioonis\u00fcsteemi suvandeid k\u00e4sitsi muutma, et \u00e4sja avastatud olemid edaspidi keelata. Eemaldage saidilt configuration.yaml s\u00e4te track_new ja taask\u00e4ivitage home assistant selle probleemi lahendamiseks.", + "title": "Google'i kalendri olemi j\u00e4lgimine on muutunud" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/google/translations/tr.json b/homeassistant/components/google/translations/tr.json index 7d67018630f..a7074b69127 100644 --- a/homeassistant/components/google/translations/tr.json +++ b/homeassistant/components/google/translations/tr.json @@ -33,6 +33,16 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Google Takvim'in configuration.yaml dosyas\u0131nda yap\u0131land\u0131r\u0131lmas\u0131 Home Assistant 2022.9'da kald\u0131r\u0131l\u0131yor.\n\nMevcut OAuth Uygulama Kimlik Bilgileriniz ve eri\u015fim ayarlar\u0131n\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131lm\u0131\u015ft\u0131r. YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Google Takvim YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + }, + "removed_track_new_yaml": { + "description": "Art\u0131k desteklenmeyen configuration.yaml dosyas\u0131nda Google Takvim i\u00e7in varl\u0131k izlemeyi devre d\u0131\u015f\u0131 b\u0131rakt\u0131n\u0131z. \u0130leride yeni ke\u015ffedilen varl\u0131klar\u0131 devre d\u0131\u015f\u0131 b\u0131rakmak i\u00e7in kullan\u0131c\u0131 aray\u00fcz\u00fcndeki entegrasyon Sistem Se\u00e7eneklerini manuel olarak de\u011fi\u015ftirmeniz gerekir. Bu sorunu \u00e7\u00f6zmek i\u00e7in configuration.yaml dosyas\u0131ndan track_new ayar\u0131n\u0131 kald\u0131r\u0131n ve Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Google Takvim varl\u0131k takibi de\u011fi\u015fti" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/govee_ble/translations/es.json b/homeassistant/components/govee_ble/translations/es.json new file mode 100644 index 00000000000..76fb203eacd --- /dev/null +++ b/homeassistant/components/govee_ble/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/tr.json b/homeassistant/components/govee_ble/translations/tr.json new file mode 100644 index 00000000000..f63cee3493c --- /dev/null +++ b/homeassistant/components/govee_ble/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/es.json b/homeassistant/components/group/translations/es.json index c58cb744a92..bea5a398f8c 100644 --- a/homeassistant/components/group/translations/es.json +++ b/homeassistant/components/group/translations/es.json @@ -5,7 +5,7 @@ "data": { "all": "Todas las entidades", "entities": "Miembros", - "hide_members": "Esconde miembros", + "hide_members": "Ocultar miembros", "name": "Nombre" }, "description": "Si \"todas las entidades\" est\u00e1n habilitadas, el estado del grupo est\u00e1 activado solo si todos los miembros est\u00e1n activados. Si \"todas las entidades\" est\u00e1n deshabilitadas, el estado del grupo es activado si alg\u00fan miembro est\u00e1 activado.", @@ -38,10 +38,10 @@ "lock": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros", + "hide_members": "Ocultar miembros", "name": "Nombre" }, - "title": "Agregar grupo" + "title": "A\u00f1adir grupo" }, "media_player": { "data": { diff --git a/homeassistant/components/growatt_server/translations/es.json b/homeassistant/components/growatt_server/translations/es.json index 041f52b19a0..f26b7c1f173 100644 --- a/homeassistant/components/growatt_server/translations/es.json +++ b/homeassistant/components/growatt_server/translations/es.json @@ -18,7 +18,7 @@ "name": "Nombre", "password": "Contrase\u00f1a", "url": "URL", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Introduce tu informaci\u00f3n de Growatt." } diff --git a/homeassistant/components/guardian/translations/es.json b/homeassistant/components/guardian/translations/es.json index fe2367232a4..42c08aba617 100644 --- a/homeassistant/components/guardian/translations/es.json +++ b/homeassistant/components/guardian/translations/es.json @@ -17,5 +17,18 @@ "description": "Configurar un dispositivo local Elexa Guardian." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con una ID de entidad de destino de `{alternate_target}`. Luego, haz clic en ENVIAR a continuaci\u00f3n para marcar este problema como resuelto.", + "title": "El servicio {deprecated_service} ser\u00e1 eliminado" + } + } + }, + "title": "El servicio {deprecated_service} ser\u00e1 eliminado" + } } } \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/es.json b/homeassistant/components/here_travel_time/translations/es.json index c1a8d9cef11..04563358781 100644 --- a/homeassistant/components/here_travel_time/translations/es.json +++ b/homeassistant/components/here_travel_time/translations/es.json @@ -12,32 +12,39 @@ "data": { "destination": "Destino como coordenadas GPS" }, - "title": "Elija el destino" + "title": "Elige el destino" }, "destination_entity_id": { "data": { "destination_entity_id": "Destino usando una entidad" }, - "title": "Elija el destino" + "title": "Elige el destino" }, "destination_menu": { "menu_options": { "destination_coordinates": "Usando una ubicaci\u00f3n en el mapa", "destination_entity": "Usando una entidad" }, - "title": "Elija el destino" + "title": "Elige el destino" }, "origin_coordinates": { "data": { "origin": "Origen como coordenadas GPS" }, - "title": "Elija el origen" + "title": "Elige el origen" }, "origin_entity_id": { "data": { "origin_entity_id": "Origen usando una entidad" }, - "title": "Elija el origen" + "title": "Elige el origen" + }, + "origin_menu": { + "menu_options": { + "origin_coordinates": "Usando una ubicaci\u00f3n en el mapa", + "origin_entity": "Usando una entidad" + }, + "title": "Elige el origen" }, "user": { "data": { @@ -54,13 +61,13 @@ "data": { "arrival_time": "Hora de llegada" }, - "title": "Elija la hora de llegada" + "title": "Elige la hora de llegada" }, "departure_time": { "data": { "departure_time": "Hora de salida" }, - "title": "Elija la hora de salida" + "title": "Elige la hora de salida" }, "init": { "data": { @@ -75,7 +82,7 @@ "departure_time": "Configurar una hora de salida", "no_time": "No configurar una hora" }, - "title": "Elija el tipo de hora" + "title": "Elige el tipo de hora" } } } diff --git a/homeassistant/components/hive/translations/es.json b/homeassistant/components/hive/translations/es.json index 09acc273536..ccca2b3ea4d 100644 --- a/homeassistant/components/hive/translations/es.json +++ b/homeassistant/components/hive/translations/es.json @@ -20,10 +20,17 @@ "description": "Introduzca su c\u00f3digo de autentificaci\u00f3n Hive. \n \n Introduzca el c\u00f3digo 0000 para solicitar otro c\u00f3digo.", "title": "Autenticaci\u00f3n de dos factores de Hive." }, + "configuration": { + "data": { + "device_name": "Nombre del dispositivo" + }, + "description": "Introduce tu configuraci\u00f3n de Hive", + "title": "Configuraci\u00f3n de Hive." + }, "reauth": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Vuelva a introducir sus datos de acceso a Hive.", "title": "Inicio de sesi\u00f3n en Hive" @@ -32,9 +39,9 @@ "data": { "password": "Contrase\u00f1a", "scan_interval": "Intervalo de exploraci\u00f3n (segundos)", - "username": "Usuario" + "username": "Nombre de usuario" }, - "description": "Ingrese su configuraci\u00f3n e informaci\u00f3n de inicio de sesi\u00f3n de Hive.", + "description": "Introduce tus datos de acceso a Hive.", "title": "Inicio de sesi\u00f3n en Hive" } } diff --git a/homeassistant/components/hlk_sw16/translations/es.json b/homeassistant/components/hlk_sw16/translations/es.json index 2609ee07eaf..c1d3e57b02f 100644 --- a/homeassistant/components/hlk_sw16/translations/es.json +++ b/homeassistant/components/hlk_sw16/translations/es.json @@ -13,7 +13,7 @@ "data": { "host": "Host", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/home_plus_control/translations/es.json b/homeassistant/components/home_plus_control/translations/es.json index 194eff4bb8c..ff4e671fb65 100644 --- a/homeassistant/components/home_plus_control/translations/es.json +++ b/homeassistant/components/home_plus_control/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", diff --git a/homeassistant/components/homeassistant/translations/es.json b/homeassistant/components/homeassistant/translations/es.json index 6b00a1b3b51..db63c818f61 100644 --- a/homeassistant/components/homeassistant/translations/es.json +++ b/homeassistant/components/homeassistant/translations/es.json @@ -2,6 +2,7 @@ "system_health": { "info": { "arch": "Arquitectura de CPU", + "config_dir": "Directorio de configuraci\u00f3n", "dev": "Desarrollo", "docker": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant_alerts/translations/es.json b/homeassistant/components/homeassistant_alerts/translations/es.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/es.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/et.json b/homeassistant/components/homeassistant_alerts/translations/et.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/et.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_alerts/translations/tr.json b/homeassistant/components/homeassistant_alerts/translations/tr.json new file mode 100644 index 00000000000..33cafb8bba3 --- /dev/null +++ b/homeassistant/components/homeassistant_alerts/translations/tr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "alert": { + "description": "{description}", + "title": "{title}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/es.json b/homeassistant/components/homekit_controller/translations/es.json index a528d66ea06..54dd93f8a55 100644 --- a/homeassistant/components/homekit_controller/translations/es.json +++ b/homeassistant/components/homekit_controller/translations/es.json @@ -18,7 +18,7 @@ "unable_to_pair": "No se ha podido emparejar, por favor int\u00e9ntelo de nuevo.", "unknown_error": "El dispositivo report\u00f3 un error desconocido. La vinculaci\u00f3n ha fallado." }, - "flow_title": "{name}", + "flow_title": "{name} ({category})", "step": { "busy_error": { "description": "Interrumpe el emparejamiento en todos los controladores o intenta reiniciar el dispositivo y luego contin\u00faa con el emparejamiento.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "Permitir el emparejamiento con c\u00f3digos de configuraci\u00f3n inseguros.", "pairing_code": "C\u00f3digo de vinculaci\u00f3n" }, - "description": "El controlador de HomeKit se comunica con {name} a trav\u00e9s de la red de \u00e1rea local usando una conexi\u00f3n encriptada segura sin un controlador HomeKit separado o iCloud. Introduce el c\u00f3digo de vinculaci\u00f3n de tu HomeKit (con el formato XXX-XX-XXX) para usar este accesorio. Este c\u00f3digo suele encontrarse en el propio dispositivo o en el embalaje.", + "description": "El controlador HomeKit se comunica con {name} ({category}) a trav\u00e9s de la red de \u00e1rea local mediante una conexi\u00f3n cifrada segura sin un controlador HomeKit o iCloud por separado. Introduce tu c\u00f3digo de emparejamiento de HomeKit (en el formato XXX-XX-XXX) para usar este accesorio. Este c\u00f3digo suele encontrarse en el propio dispositivo o en el embalaje.", "title": "Vincular un dispositivo a trav\u00e9s del protocolo de accesorios HomeKit" }, "protocol_error": { diff --git a/homeassistant/components/homekit_controller/translations/sensor.ca.json b/homeassistant/components/homekit_controller/translations/sensor.ca.json index dde4926406c..5ee2100c59b 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.ca.json +++ b/homeassistant/components/homekit_controller/translations/sensor.ca.json @@ -1,6 +1,7 @@ { "state": { "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Pot funcionar com a encaminador (router) frontera", "full": "Dispositiu final complet", "minimal": "Dispositiu final redu\u00eft", "none": "Cap", diff --git a/homeassistant/components/homekit_controller/translations/sensor.el.json b/homeassistant/components/homekit_controller/translations/sensor.el.json new file mode 100644 index 00000000000..c1c02c08bff --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.el.json @@ -0,0 +1,21 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "\u0394\u03c5\u03bd\u03b1\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c3\u03c5\u03bd\u03cc\u03c1\u03c9\u03bd", + "full": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c0\u03bb\u03ae\u03c1\u03bf\u03c5\u03c2 \u03bb\u03ae\u03be\u03b7\u03c2", + "minimal": "\u0395\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03b7 \u03c4\u03b5\u03bb\u03b9\u03ba\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", + "none": "\u03a4\u03af\u03c0\u03bf\u03c4\u03b1", + "router_eligible": "\u039a\u03b1\u03c4\u03ac\u03bb\u03bb\u03b7\u03bb\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c4\u03b5\u03c1\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae", + "sleepy": "Sleepy End Device" + }, + "homekit_controller__thread_status": { + "border_router": "Border Router", + "child": "\u03a0\u03b1\u03b9\u03b4\u03af", + "detached": "\u0391\u03c0\u03bf\u03bc\u03bf\u03bd\u03c9\u03bc\u03ad\u03bd\u03bf", + "disabled": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf", + "joining": "\u03a3\u03c5\u03bc\u03bc\u03b5\u03c4\u03bf\u03c7\u03ae", + "leader": "\u0397\u03b3\u03ad\u03c4\u03b7\u03c2", + "router": "\u0394\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae\u03c2" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.es.json b/homeassistant/components/homekit_controller/translations/sensor.es.json new file mode 100644 index 00000000000..e3ba6322d3f --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.es.json @@ -0,0 +1,21 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Capacidad de router fronterizo", + "full": "Dispositivo final completo", + "minimal": "Dispositivo final m\u00ednimo", + "none": "Ninguna", + "router_eligible": "Dispositivo final elegible como router", + "sleepy": "Dispositivo final dormido" + }, + "homekit_controller__thread_status": { + "border_router": "Router fronterizo", + "child": "Hijo", + "detached": "Separado", + "disabled": "Deshabilitado", + "joining": "Uniendo", + "leader": "L\u00edder", + "router": "Router" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.et.json b/homeassistant/components/homekit_controller/translations/sensor.et.json new file mode 100644 index 00000000000..7594576edad --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.et.json @@ -0,0 +1,21 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "Piiriruuteri v\u00f5imekus", + "full": "T\u00e4ielik l\u00f5ppseade", + "minimal": "Minimaalne l\u00f5ppseade", + "none": "Puudub", + "router_eligible": "Ruuteriks sobiv l\u00f5ppseade", + "sleepy": "Unine l\u00f5ppseade" + }, + "homekit_controller__thread_status": { + "border_router": "Rajaruuter", + "child": "Alamseade", + "detached": "Eraldatud", + "disabled": "Keelatud", + "joining": "Liitun", + "leader": "Juhtseade", + "router": "Ruuter" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.tr.json b/homeassistant/components/homekit_controller/translations/sensor.tr.json new file mode 100644 index 00000000000..4f295f42ce1 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.tr.json @@ -0,0 +1,21 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "border_router_capable": "S\u0131n\u0131r Y\u00f6nlendirici \u00d6zelli\u011fi", + "full": "Tam Son Cihaz", + "minimal": "Minimal Son Cihaz", + "none": "Hi\u00e7biri", + "router_eligible": "Y\u00f6nlendiriciye Uygun Son Cihaz", + "sleepy": "Uykudaki Son Cihaz" + }, + "homekit_controller__thread_status": { + "border_router": "S\u0131n\u0131r Y\u00f6nlendirici", + "child": "\u00c7ocuk", + "detached": "Tarafs\u0131z", + "disabled": "Devre d\u0131\u015f\u0131", + "joining": "Kat\u0131l\u0131yor", + "leader": "Lider", + "router": "Y\u00f6nlendirici" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/es.json b/homeassistant/components/honeywell/translations/es.json index 98ad6871b81..c08c02c3632 100644 --- a/homeassistant/components/honeywell/translations/es.json +++ b/homeassistant/components/honeywell/translations/es.json @@ -7,7 +7,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Por favor, introduzca las credenciales utilizadas para iniciar sesi\u00f3n en mytotalconnectcomfort.com." } diff --git a/homeassistant/components/hue/translations/es.json b/homeassistant/components/hue/translations/es.json index 7017cca99c1..6b72df25f65 100644 --- a/homeassistant/components/hue/translations/es.json +++ b/homeassistant/components/hue/translations/es.json @@ -3,7 +3,7 @@ "abort": { "all_configured": "Ya se han configurado todas las pasarelas Philips Hue", "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", "discover_timeout": "Imposible encontrar pasarelas Philips Hue", "invalid_host": "Host inv\u00e1lido", diff --git a/homeassistant/components/huisbaasje/translations/es.json b/homeassistant/components/huisbaasje/translations/es.json index d537185eb68..942024cf167 100644 --- a/homeassistant/components/huisbaasje/translations/es.json +++ b/homeassistant/components/huisbaasje/translations/es.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/hvv_departures/translations/es.json b/homeassistant/components/hvv_departures/translations/es.json index d73002c5a4b..8cfa90d6367 100644 --- a/homeassistant/components/hvv_departures/translations/es.json +++ b/homeassistant/components/hvv_departures/translations/es.json @@ -25,7 +25,7 @@ "data": { "host": "Host", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Conectar con el API de HVV" } diff --git a/homeassistant/components/hyperion/translations/es.json b/homeassistant/components/hyperion/translations/es.json index 496064b1543..d96584ee532 100644 --- a/homeassistant/components/hyperion/translations/es.json +++ b/homeassistant/components/hyperion/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "auth_new_token_not_granted_error": "El token reci\u00e9n creado no se aprob\u00f3 en la interfaz de usuario de Hyperion", "auth_new_token_not_work_error": "Error al autenticarse con el token reci\u00e9n creado", "auth_required_error": "No se pudo determinar si se requiere autorizaci\u00f3n", diff --git a/homeassistant/components/iaqualink/translations/es.json b/homeassistant/components/iaqualink/translations/es.json index c95f9d51927..7587b393c3f 100644 --- a/homeassistant/components/iaqualink/translations/es.json +++ b/homeassistant/components/iaqualink/translations/es.json @@ -11,7 +11,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Introduce el nombre de usuario y contrase\u00f1a de tu cuenta de iAqualink.", "title": "Conexi\u00f3n con iAqualink" diff --git a/homeassistant/components/inkbird/translations/es.json b/homeassistant/components/inkbird/translations/es.json new file mode 100644 index 00000000000..76fb203eacd --- /dev/null +++ b/homeassistant/components/inkbird/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/tr.json b/homeassistant/components/inkbird/translations/tr.json new file mode 100644 index 00000000000..f63cee3493c --- /dev/null +++ b/homeassistant/components/inkbird/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/es.json b/homeassistant/components/insteon/translations/es.json index 5434bc77a8a..768c590151e 100644 --- a/homeassistant/components/insteon/translations/es.json +++ b/homeassistant/components/insteon/translations/es.json @@ -27,7 +27,7 @@ "host": "Direcci\u00f3n IP", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Configure el Insteon Hub versi\u00f3n 2.", "title": "Insteon Hub Versi\u00f3n 2" @@ -76,7 +76,7 @@ "host": "Direcci\u00f3n IP", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Cambiar la informaci\u00f3n de la conexi\u00f3n del Hub Insteon. Debes reiniciar el Home Assistant despu\u00e9s de hacer este cambio. Esto no cambia la configuraci\u00f3n del Hub en s\u00ed. Para cambiar la configuraci\u00f3n del Hub usa la aplicaci\u00f3n Hub." }, diff --git a/homeassistant/components/integration/translations/es.json b/homeassistant/components/integration/translations/es.json index 4b4f1306dc9..7fee4f392ce 100644 --- a/homeassistant/components/integration/translations/es.json +++ b/homeassistant/components/integration/translations/es.json @@ -32,5 +32,5 @@ } } }, - "title": "Integraci\u00f3n - Sensor integral de suma de Riemann" + "title": "Integraci\u00f3n - Sensor de suma integral de Riemann" } \ No newline at end of file diff --git a/homeassistant/components/intellifire/translations/es.json b/homeassistant/components/intellifire/translations/es.json index 4b61f7f4b3e..c44475be1a1 100644 --- a/homeassistant/components/intellifire/translations/es.json +++ b/homeassistant/components/intellifire/translations/es.json @@ -6,7 +6,7 @@ "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "api_error": "Ha Fallado el inicio de sesi\u00f3n", + "api_error": "Error de inicio de sesi\u00f3n", "cannot_connect": "Fall\u00f3 la conexi\u00f3n", "iftapi_connect": "Se ha producido un error al conectar a iftapi.net" }, @@ -31,7 +31,7 @@ "data": { "host": "Host" }, - "description": "Se han descubierto los siguientes dispositivos IntelliFire. Selecciona lo que quieras configurar.", + "description": "Se han descubierto los siguientes dispositivos IntelliFire. Por favor, selecciona el que quieras configurar.", "title": "Selecci\u00f3n de dispositivo" } } diff --git a/homeassistant/components/ipp/translations/es.json b/homeassistant/components/ipp/translations/es.json index be1b38f08db..f6948374561 100644 --- a/homeassistant/components/ipp/translations/es.json +++ b/homeassistant/components/ipp/translations/es.json @@ -21,7 +21,7 @@ "host": "Host", "port": "Puerto", "ssl": "Utiliza un certificado SSL", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar el certificado SSL" }, "description": "Configura tu impresora a trav\u00e9s del Protocolo de Impresi\u00f3n de Internet (IPP) para integrarla con Home Assistant.", "title": "Vincula tu impresora" diff --git a/homeassistant/components/isy994/translations/es.json b/homeassistant/components/isy994/translations/es.json index 7fd765fb437..dd5e97923d6 100644 --- a/homeassistant/components/isy994/translations/es.json +++ b/homeassistant/components/isy994/translations/es.json @@ -7,7 +7,7 @@ "cannot_connect": "Error al conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_host": "La entrada del host no estaba en formato URL completo, por ejemplo, http://192.168.10.100:80", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" }, "flow_title": "{name} ({host})", @@ -17,15 +17,15 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Las credenciales de {host} ya no son v\u00e1lidas.", - "title": "Re-autenticaci\u00f3n de ISY" + "description": "Las credenciales para {host} ya no son v\u00e1lidas.", + "title": "Vuelve a autenticar tu ISY" }, "user": { "data": { "host": "URL", "password": "Contrase\u00f1a", "tls": "La versi\u00f3n de TLS del controlador ISY.", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "La entrada del host debe estar en formato URL completo, por ejemplo, http://192.168.10.100:80", "title": "Conexi\u00f3n con ISY" diff --git a/homeassistant/components/isy994/translations/zh-Hant.json b/homeassistant/components/isy994/translations/zh-Hant.json index f6625c0bb60..b70d9601ba2 100644 --- a/homeassistant/components/isy994/translations/zh-Hant.json +++ b/homeassistant/components/isy994/translations/zh-Hant.json @@ -41,7 +41,7 @@ "sensor_string": "\u7bc0\u9ede\u611f\u6e2c\u5668\u5b57\u4e32", "variable_sensor_string": "\u53ef\u8b8a\u611f\u6e2c\u5668\u5b57\u4e32" }, - "description": "ISY \u6574\u5408\u8a2d\u5b9a\u9078\u9805\uff1a \n \u2022 \u7bc0\u9ede\u611f\u6e2c\u5668\u5b57\u4e32\uff08Node Sensor String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u6216\u8cc7\u6599\u593e\u5305\u542b\u300cNode Sensor String\u300d\u7684\u88dd\u7f6e\u90fd\u6703\u88ab\u8996\u70ba\u611f\u6e2c\u5668\u6216\u4e8c\u9032\u4f4d\u611f\u6e2c\u5668\u3002\n \u2022 \u5ffd\u7565\u5b57\u4e32\uff08Ignore String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u5305\u542b\u300cIgnore String\u300d\u7684\u88dd\u7f6e\u90fd\u6703\u88ab\u5ffd\u7565\u3002\n \u2022 \u53ef\u8b8a\u611f\u6e2c\u5668\u5b57\u4e32\uff08Variable Sensor String\uff09\uff1a\u4efb\u4f55\u5305\u542b\u300cVariable Sensor String\u300d\u7684\u8b8a\u6578\u90fd\u5c07\u65b0\u589e\u70ba\u611f\u6e2c\u5668\u3002 \n \u2022 \u56de\u5fa9\u4eae\u5ea6\uff08Restore Light Brightness\uff09\uff1a\u958b\u5553\u5f8c\u3001\u7576\u71c8\u5149\u958b\u555f\u6642\u6703\u56de\u5fa9\u5148\u524d\u7684\u4eae\u5ea6\uff0c\u800c\u4e0d\u662f\u4f7f\u7528\u88dd\u7f6e\u9810\u8a2d\u4eae\u5ea6\u3002", + "description": "ISY \u6574\u5408\u8a2d\u5b9a\u9078\u9805\uff1a \n \u2022 \u7bc0\u9ede\u611f\u6e2c\u5668\u5b57\u4e32\uff08Node Sensor String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u6216\u8cc7\u6599\u593e\u5305\u542b\u300cNode Sensor String\u300d\u7684\u88dd\u7f6e\u90fd\u6703\u88ab\u8996\u70ba\u611f\u6e2c\u5668\u6216\u4e8c\u9032\u4f4d\u611f\u6e2c\u5668\u3002\n \u2022 \u5ffd\u7565\u5b57\u4e32\uff08Ignore String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u5305\u542b\u300cIgnore String\u300d\u7684\u88dd\u7f6e\u90fd\u6703\u88ab\u5ffd\u7565\u3002\n \u2022 \u53ef\u8b8a\u611f\u6e2c\u5668\u5b57\u4e32\uff08Variable Sensor String\uff09\uff1a\u4efb\u4f55\u5305\u542b\u300cVariable Sensor String\u300d\u7684\u8b8a\u6578\u90fd\u5c07\u65b0\u589e\u70ba\u611f\u6e2c\u5668\u3002 \n \u2022 \u56de\u5fa9\u4eae\u5ea6\uff08Restore Light Brightness\uff09\uff1a\u958b\u555f\u5f8c\u3001\u7576\u71c8\u5149\u958b\u555f\u6642\u6703\u56de\u5fa9\u5148\u524d\u7684\u4eae\u5ea6\uff0c\u800c\u4e0d\u662f\u4f7f\u7528\u88dd\u7f6e\u9810\u8a2d\u4eae\u5ea6\u3002", "title": "ISY \u9078\u9805" } } diff --git a/homeassistant/components/jellyfin/translations/es.json b/homeassistant/components/jellyfin/translations/es.json index 1cc7ab64c75..b6e0c52c667 100644 --- a/homeassistant/components/jellyfin/translations/es.json +++ b/homeassistant/components/jellyfin/translations/es.json @@ -13,7 +13,7 @@ "data": { "password": "Contrase\u00f1a", "url": "URL", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/justnimbus/translations/ca.json b/homeassistant/components/justnimbus/translations/ca.json new file mode 100644 index 00000000000..679f8726b1b --- /dev/null +++ b/homeassistant/components/justnimbus/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "client_id": "ID de client" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/de.json b/homeassistant/components/justnimbus/translations/de.json new file mode 100644 index 00000000000..d60cbfd45d6 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "client_id": "Client-ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/el.json b/homeassistant/components/justnimbus/translations/el.json new file mode 100644 index 00000000000..a26b9f1a466 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/el.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "client_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/es.json b/homeassistant/components/justnimbus/translations/es.json new file mode 100644 index 00000000000..2b718ffd7bd --- /dev/null +++ b/homeassistant/components/justnimbus/translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "client_id": "ID de cliente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/et.json b/homeassistant/components/justnimbus/translations/et.json new file mode 100644 index 00000000000..327b1a41eda --- /dev/null +++ b/homeassistant/components/justnimbus/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "client_id": "Kliendi ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/pt-BR.json b/homeassistant/components/justnimbus/translations/pt-BR.json new file mode 100644 index 00000000000..c6fec98d719 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/pt-BR.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "client_id": "ID do Cliente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/tr.json b/homeassistant/components/justnimbus/translations/tr.json new file mode 100644 index 00000000000..017fbed5f8a --- /dev/null +++ b/homeassistant/components/justnimbus/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "client_id": "\u0130stemci Kimli\u011fi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/es.json b/homeassistant/components/knx/translations/es.json index d982d96b330..627c26ab72a 100644 --- a/homeassistant/components/knx/translations/es.json +++ b/homeassistant/components/knx/translations/es.json @@ -7,8 +7,8 @@ "error": { "cannot_connect": "Error al conectar", "file_not_found": "El archivo `.knxkeys` especificado no se encontr\u00f3 en la ruta config/.storage/knx/", - "invalid_individual_address": "El valor no coincide con el patr\u00f3n de direcci\u00f3n KNX individual. 'area.line.device'", - "invalid_ip_address": "Direcci\u00f3n IPv4 inv\u00e1lida.", + "invalid_individual_address": "El valor no coincide con el patr\u00f3n de la direcci\u00f3n KNX individual. 'area.line.device'", + "invalid_ip_address": "Direcci\u00f3n IPv4 no v\u00e1lida.", "invalid_signature": "La contrase\u00f1a para descifrar el archivo `.knxkeys` es incorrecta." }, "step": { @@ -20,9 +20,9 @@ "tunneling_type": "Tipo de t\u00fanel KNX" }, "data_description": { - "host": "Direcci\u00f3n IP del dispositivo de t\u00fanel KNX/IP.", + "host": "Direcci\u00f3n IP del dispositivo de tunelizaci\u00f3n KNX/IP.", "local_ip": "D\u00e9jalo en blanco para utilizar el descubrimiento autom\u00e1tico.", - "port": "Puerto del dispositivo de tunelizaci\u00f3n KNX/IP.Puerto del dispositivo de tunelizaci\u00f3n KNX/IP." + "port": "Puerto del dispositivo de tunelizaci\u00f3n KNX/IP." }, "description": "Introduzca la informaci\u00f3n de conexi\u00f3n de su dispositivo de tunelizaci\u00f3n." }, @@ -35,39 +35,39 @@ }, "data_description": { "individual_address": "Direcci\u00f3n KNX que usar\u00e1 Home Assistant, por ejemplo, `0.0.4`", - "local_ip": "D\u00e9jelo en blanco para usar el descubrimiento autom\u00e1tico." + "local_ip": "D\u00e9jalo en blanco para usar el descubrimiento autom\u00e1tico." }, "description": "Por favor, configure las opciones de enrutamiento." }, "secure_knxkeys": { "data": { - "knxkeys_filename": "El nombre de su archivo `.knxkeys` (incluyendo la extensi\u00f3n)", + "knxkeys_filename": "El nombre de tu archivo `.knxkeys` (incluyendo la extensi\u00f3n)", "knxkeys_password": "Contrase\u00f1a para descifrar el archivo `.knxkeys`." }, "data_description": { - "knxkeys_filename": "Se espera que el archivo se encuentre en su directorio de configuraci\u00f3n en `.storage/knx/`.\n En el sistema operativo Home Assistant, ser\u00eda `/config/.storage/knx/`\n Ejemplo: `mi_proyecto.knxkeys`", - "knxkeys_password": "Se ha definido durante la exportaci\u00f3n del archivo desde ETS.Se ha definido durante la exportaci\u00f3n del archivo desde ETS." + "knxkeys_filename": "Se espera que el archivo se encuentre en tu directorio de configuraci\u00f3n en `.storage/knx/`.\nEn Home Assistant OS ser\u00eda `/config/.storage/knx/`\nEjemplo: `mi_proyecto.knxkeys`", + "knxkeys_password": "Esto se configur\u00f3 al exportar el archivo desde ETS." }, - "description": "Introduce la informaci\u00f3n de tu archivo `.knxkeys`." + "description": "Por favor, introduce la informaci\u00f3n de tu archivo `.knxkeys`." }, "secure_manual": { "data": { "device_authentication": "Contrase\u00f1a de autenticaci\u00f3n del dispositivo", "user_id": "ID de usuario", - "user_password": "Contrase\u00f1a del usuario" + "user_password": "Contrase\u00f1a de usuario" }, "data_description": { "device_authentication": "Esto se configura en el panel 'IP' de la interfaz en ETS.", - "user_id": "A menudo, es el n\u00famero del t\u00fanel +1. Por tanto, 'T\u00fanel 2' tendr\u00eda el ID de usuario '3'.", - "user_password": "Contrase\u00f1a para la conexi\u00f3n espec\u00edfica del t\u00fanel establecida en el panel de \"Propiedades\" del t\u00fanel en ETS." + "user_id": "Este suele ser el n\u00famero de t\u00fanel +1. Por tanto, 'T\u00fanel 2' tendr\u00eda ID de usuario '3'.", + "user_password": "Contrase\u00f1a para la conexi\u00f3n de t\u00fanel espec\u00edfica establecida en el panel 'Propiedades' del t\u00fanel en ETS." }, - "description": "Introduce la informaci\u00f3n de seguridad IP (IP Secure)." + "description": "Introduce tu informaci\u00f3n de IP segura." }, "secure_tunneling": { "description": "Selecciona c\u00f3mo quieres configurar KNX/IP Secure.", "menu_options": { - "secure_knxkeys": "Use un archivo `.knxkeys` que contenga claves seguras de IP", - "secure_manual": "Configura manualmente las claves de seguridad IP (IP Secure)" + "secure_knxkeys": "Utilizar un archivo `.knxkeys` que contenga claves seguras de IP", + "secure_manual": "Configurar claves seguras de IP manualmente" } }, "tunnel": { @@ -97,12 +97,12 @@ "state_updater": "Actualizador de estado" }, "data_description": { - "individual_address": "Direcci\u00f3n KNX para utilizar con Home Assistant, ej. `0.0.4`", - "local_ip": "Usar `0.0.0.0` para el descubrimiento autom\u00e1tico.", - "multicast_group": "Se usa para el enrutamiento y el descubrimiento. Predeterminado: `224.0.23.12`", - "multicast_port": "Se usa para el enrutamiento y el descubrimiento. Predeterminado: `3671`", - "rate_limit": "Telegramas de salida m\u00e1ximos por segundo. \nRecomendado: de 20 a 40", - "state_updater": "Establece los valores predeterminados para leer los estados del bus KNX. Cuando est\u00e1 deshabilitado, Home Assistant no recuperar\u00e1 activamente estados de entidad del bus KNX. Puede ser anulado por las opciones de entidad `sync_state`." + "individual_address": "Direcci\u00f3n KNX que usar\u00e1 Home Assistant, por ejemplo, `0.0.4`", + "local_ip": "Usa `0.0.0.0` para el descubrimiento autom\u00e1tico.", + "multicast_group": "Se utiliza para el enrutamiento y el descubrimiento. Predeterminado: `224.0.23.12`", + "multicast_port": "Se utiliza para el enrutamiento y el descubrimiento. Predeterminado: `3671`", + "rate_limit": "N\u00famero m\u00e1ximo de telegramas salientes por segundo.\nRecomendado: 20 a 40", + "state_updater": "Establece los valores predeterminados para leer los estados del bus KNX. Cuando est\u00e1 deshabilitado, Home Assistant no recuperar\u00e1 activamente los estados de entidad del bus KNX. Puede ser anulado por las opciones de entidad `sync_state`." } }, "tunnel": { diff --git a/homeassistant/components/konnected/translations/es.json b/homeassistant/components/konnected/translations/es.json index 1074711901c..901c858b33c 100644 --- a/homeassistant/components/konnected/translations/es.json +++ b/homeassistant/components/konnected/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", "not_konn_panel": "No es un dispositivo Konnected.io reconocido", "unknown": "Se produjo un error desconocido" diff --git a/homeassistant/components/lacrosse_view/translations/es.json b/homeassistant/components/lacrosse_view/translations/es.json new file mode 100644 index 00000000000..1f341b0f44e --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "no_locations": "No se encontraron ubicaciones", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/et.json b/homeassistant/components/lacrosse_view/translations/et.json new file mode 100644 index 00000000000..8f21ccff5f6 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "invalid_auth": "Tuvastamine nurjus", + "no_locations": "Asukohti ei leitud", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/tr.json b/homeassistant/components/lacrosse_view/translations/tr.json new file mode 100644 index 00000000000..cf82d698150 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "no_locations": "Konum bulunamad\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/translations/es.json b/homeassistant/components/laundrify/translations/es.json index 05c019700bc..8c55a8cf441 100644 --- a/homeassistant/components/laundrify/translations/es.json +++ b/homeassistant/components/laundrify/translations/es.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { - "cannot_connect": "Fallo en la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "invalid_format": "Formato no v\u00e1lido. Por favor, especif\u00edquelo como xxx-xxx.", + "invalid_format": "Formato no v\u00e1lido. Por favor, especif\u00edcalo como xxx-xxx.", "unknown": "Error inesperado" }, "step": { @@ -14,11 +14,11 @@ "data": { "code": "C\u00f3digo de autenticaci\u00f3n (xxx-xxx)" }, - "description": "Por favor, introduzca su c\u00f3digo de autenticaci\u00f3n personal que se muestra en la aplicaci\u00f3n Laundrify." + "description": "Por favor, introduce tu c\u00f3digo de autenticaci\u00f3n personal que se muestra en la aplicaci\u00f3n laundrify." }, "reauth_confirm": { - "description": "La integraci\u00f3n de laundrify necesita volver a autentificarse.", - "title": "Integraci\u00f3n de la reautenticaci\u00f3n" + "description": "La integraci\u00f3n laundrify necesita volver a autentificarse.", + "title": "Volver a autenticar la integraci\u00f3n" } } } diff --git a/homeassistant/components/lcn/translations/es.json b/homeassistant/components/lcn/translations/es.json index cc9f91601c9..045f87a1927 100644 --- a/homeassistant/components/lcn/translations/es.json +++ b/homeassistant/components/lcn/translations/es.json @@ -1,6 +1,7 @@ { "device_automation": { "trigger_type": { + "codelock": "c\u00f3digo de bloqueo de c\u00f3digo recibido", "fingerprint": "c\u00f3digo de huella dactilar recibido", "send_keys": "enviar claves recibidas", "transmitter": "c\u00f3digo de transmisor recibido", diff --git a/homeassistant/components/lg_soundbar/translations/es.json b/homeassistant/components/lg_soundbar/translations/es.json new file mode 100644 index 00000000000..dc78afa232b --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente." + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/et.json b/homeassistant/components/lg_soundbar/translations/et.json index 227250382c0..f5c73126124 100644 --- a/homeassistant/components/lg_soundbar/translations/et.json +++ b/homeassistant/components/lg_soundbar/translations/et.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "existing_instance_updated": "V\u00e4rskendati olemasolevat konfiguratsiooni." }, "error": { diff --git a/homeassistant/components/lg_soundbar/translations/tr.json b/homeassistant/components/lg_soundbar/translations/tr.json index 5eb581847fb..c80f5540643 100644 --- a/homeassistant/components/lg_soundbar/translations/tr.json +++ b/homeassistant/components/lg_soundbar/translations/tr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "existing_instance_updated": "Mevcut yap\u0131land\u0131rma g\u00fcncellendi." }, "error": { diff --git a/homeassistant/components/life360/translations/es.json b/homeassistant/components/life360/translations/es.json index 02a4a349ee9..b8495b5e916 100644 --- a/homeassistant/components/life360/translations/es.json +++ b/homeassistant/components/life360/translations/es.json @@ -1,7 +1,9 @@ { "config": { "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" }, "create_entry": { @@ -9,18 +11,39 @@ }, "error": { "already_configured": "La cuenta ya est\u00e1 configurada", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "invalid_username": "Nombre de usuario no v\u00e1lido", "unknown": "Error inesperado" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Para configurar las opciones avanzadas, revisa la [documentaci\u00f3n de Life360]({docs_url}).\nDeber\u00edas hacerlo antes de a\u00f1adir alguna cuenta.", - "title": "Informaci\u00f3n de la cuenta de Life360" + "title": "Configurar la cuenta de Life360" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "driving": "Mostrar conducci\u00f3n como estado", + "driving_speed": "Velocidad de conducci\u00f3n", + "limit_gps_acc": "Limitar la precisi\u00f3n del GPS", + "max_gps_accuracy": "Precisi\u00f3n m\u00e1xima del GPS (metros)", + "set_drive_speed": "Establecer el umbral de velocidad de conducci\u00f3n" + }, + "title": "Opciones de la cuenta" } } } diff --git a/homeassistant/components/lifx/translations/es.json b/homeassistant/components/lifx/translations/es.json index 484d59ba55f..a308d5fbb9e 100644 --- a/homeassistant/components/lifx/translations/es.json +++ b/homeassistant/components/lifx/translations/es.json @@ -1,12 +1,32 @@ { "config": { "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "no_devices_found": "No se encontraron dispositivos en la red", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "flow_title": "{label} ({host}) {serial}", "step": { "confirm": { "description": "\u00bfQuieres configurar LIFX?" + }, + "discovery_confirm": { + "description": "\u00bfQuieres configurar {label} ({host}) {serial}?" + }, + "pick_device": { + "data": { + "device": "Dispositivo" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Si deja el host vac\u00edo, se usar\u00e1 el descubrimiento para encontrar dispositivos." } } } diff --git a/homeassistant/components/litterrobot/translations/sensor.es.json b/homeassistant/components/litterrobot/translations/sensor.es.json index ca5695d1347..a67a15c6820 100644 --- a/homeassistant/components/litterrobot/translations/sensor.es.json +++ b/homeassistant/components/litterrobot/translations/sensor.es.json @@ -6,18 +6,18 @@ "ccp": "Ciclo de limpieza en curso", "csf": "Fallo del sensor de gatos", "csi": "Sensor de gatos interrumpido", - "cst": "Tiempo del sensor de gatos", + "cst": "Sincronizaci\u00f3n del sensor de gatos", "df1": "Caj\u00f3n casi lleno - Quedan 2 ciclos", "df2": "Caj\u00f3n casi lleno - Queda 1 ciclo", "dfs": "Caj\u00f3n lleno", - "dhf": "Error de posici\u00f3n de vertido + inicio", + "dhf": "Fallo de volcado + posici\u00f3n inicial", "dpf": "Fallo de posici\u00f3n de descarga", "ec": "Ciclo vac\u00edo", "hpf": "Fallo de posici\u00f3n inicial", "off": "Apagado", - "offline": "Desconectado", + "offline": "Sin conexi\u00f3n", "otf": "Fallo de par excesivo", - "p": "Pausada", + "p": "En pausa", "pd": "Detecci\u00f3n de pellizcos", "rdy": "Listo", "scf": "Fallo del sensor de gatos al inicio", diff --git a/homeassistant/components/lookin/translations/es.json b/homeassistant/components/lookin/translations/es.json index c1e443bddcf..d160513d0ff 100644 --- a/homeassistant/components/lookin/translations/es.json +++ b/homeassistant/components/lookin/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", "no_devices_found": "No se encontraron dispositivos en la red" }, diff --git a/homeassistant/components/lyric/translations/es.json b/homeassistant/components/lyric/translations/es.json index 12692849ce3..5405ca19ffa 100644 --- a/homeassistant/components/lyric/translations/es.json +++ b/homeassistant/components/lyric/translations/es.json @@ -17,5 +17,11 @@ "title": "Volver a autenticar la integraci\u00f3n" } } + }, + "issues": { + "removed_yaml": { + "description": "Se elimin\u00f3 la configuraci\u00f3n de Honeywell Lyric mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se elimin\u00f3 la configuraci\u00f3n YAML de Honeywell Lyric" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/et.json b/homeassistant/components/lyric/translations/et.json index b3e19a93b26..e5e7c970e13 100644 --- a/homeassistant/components/lyric/translations/et.json +++ b/homeassistant/components/lyric/translations/et.json @@ -17,5 +17,11 @@ "title": "Taastuvastamine" } } + }, + "issues": { + "removed_yaml": { + "description": "Honeywell Lyric'i konfigureerimine YAML-i abil on eemaldatud.\n\nTeie olemasolevat YAML-konfiguratsiooni ei kasuta Home Assistant.\n\nProbleemi lahendamiseks eemaldage YAML-konfiguratsioon failist configuration.yaml ja k\u00e4ivitage Home Assistant uuesti.", + "title": "Honeywell Lyric YAML-i konfiguratsioon on eemaldatud" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/tr.json b/homeassistant/components/lyric/translations/tr.json index b910327ed7e..b49ffa53e18 100644 --- a/homeassistant/components/lyric/translations/tr.json +++ b/homeassistant/components/lyric/translations/tr.json @@ -17,5 +17,11 @@ "title": "Entegrasyonu Yeniden Do\u011frula" } } + }, + "issues": { + "removed_yaml": { + "description": "Honeywell Lyric'in YAML kullan\u0131larak yap\u0131land\u0131r\u0131lmas\u0131 kald\u0131r\u0131ld\u0131.\n\nMevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lmaz.\n\nYAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Honeywell Lyric YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" + } } } \ No newline at end of file diff --git a/homeassistant/components/meater/translations/es.json b/homeassistant/components/meater/translations/es.json index 44e5f3984f4..8c4be00c610 100644 --- a/homeassistant/components/meater/translations/es.json +++ b/homeassistant/components/meater/translations/es.json @@ -2,7 +2,7 @@ "config": { "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "service_unavailable_error": "La API no est\u00e1 disponible actualmente, vuelva a intentarlo m\u00e1s tarde.", + "service_unavailable_error": "La API no est\u00e1 disponible en este momento, por favor int\u00e9ntalo m\u00e1s tarde.", "unknown_auth_error": "Error inesperado" }, "step": { @@ -15,12 +15,12 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "data_description": { "username": "Nombre de usuario de Meater Cloud, normalmente una direcci\u00f3n de correo electr\u00f3nico." }, - "description": "Configure su cuenta de Meater Cloud." + "description": "Configura tu cuenta de Meater Cloud." } } } diff --git a/homeassistant/components/media_player/translations/es.json b/homeassistant/components/media_player/translations/es.json index fd60d09f562..ca2a633eb21 100644 --- a/homeassistant/components/media_player/translations/es.json +++ b/homeassistant/components/media_player/translations/es.json @@ -1,7 +1,7 @@ { "device_automation": { "condition_type": { - "is_buffering": "{entity_name} se est\u00e1 cargando en memoria", + "is_buffering": "{entity_name} est\u00e1 almacenando en b\u00fafer", "is_idle": "{entity_name} est\u00e1 inactivo", "is_off": "{entity_name} est\u00e1 apagado", "is_on": "{entity_name} est\u00e1 activado", @@ -9,7 +9,7 @@ "is_playing": "{entity_name} est\u00e1 reproduciendo" }, "trigger_type": { - "buffering": "{entity_name} comienza a cargarse en memoria", + "buffering": "{entity_name} comienza a almacenar en b\u00fafer", "changed_states": "{entity_name} ha cambiado de estado", "idle": "{entity_name} est\u00e1 inactivo", "paused": "{entity_name} est\u00e1 en pausa", @@ -20,7 +20,7 @@ }, "state": { "_": { - "buffering": "Cargando", + "buffering": "almacenamiento en b\u00fafer", "idle": "Inactivo", "off": "Apagado", "on": "Encendido", diff --git a/homeassistant/components/miflora/translations/es.json b/homeassistant/components/miflora/translations/es.json new file mode 100644 index 00000000000..93a1e82aed6 --- /dev/null +++ b/homeassistant/components/miflora/translations/es.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "La integraci\u00f3n Mi Flora dej\u00f3 de funcionar en Home Assistant 2022.7 y se reemplaz\u00f3 por la integraci\u00f3n de Xiaomi BLE en la versi\u00f3n 2022.8. \n\nNo hay una ruta de migraci\u00f3n posible, por lo tanto, debes agregar tu dispositivo Mi Flora usando la nueva integraci\u00f3n manualmente. \n\nHome Assistant ya no usa la configuraci\u00f3n YAML existente de Mi Flora. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "La integraci\u00f3n Mi Flora ha sido reemplazada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/et.json b/homeassistant/components/miflora/translations/et.json new file mode 100644 index 00000000000..f340d3263ca --- /dev/null +++ b/homeassistant/components/miflora/translations/et.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Mi Flora integratsioon lakkas t\u00f6\u00f6tamast versioonis Home Assistant 2022.7 ja asendati Xiaomi BLE integratsiooniga versioonis 2022.8.\n\nMigratsiooniteed ei ole v\u00f5imalik, seega peate oma Mi Flora seadme uue integratsiooni abil k\u00e4sitsi lisama.\n\nTeie olemasolevat Mi Flora YAML-konfiguratsiooni ei kasuta Home Assistant enam. Probleemi lahendamiseks eemaldage YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivitage Home Assistant uuesti.", + "title": "Mi Flora sidumine on asendatud" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/tr.json b/homeassistant/components/miflora/translations/tr.json new file mode 100644 index 00000000000..8d6fe8a625d --- /dev/null +++ b/homeassistant/components/miflora/translations/tr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Mi Flora entegrasyonu Home Assistant 2022.7'de \u00e7al\u0131\u015fmay\u0131 durdurdu ve 2022.8 s\u00fcr\u00fcm\u00fcnde Xiaomi BLE entegrasyonu ile de\u011fi\u015ftirildi.\n\nGe\u00e7i\u015f yolu m\u00fcmk\u00fcn de\u011fildir, bu nedenle Mi Flora cihaz\u0131n\u0131z\u0131 yeni entegrasyonu kullanarak manuel olarak eklemeniz gerekir.\n\nMevcut Mi Flora YAML yap\u0131land\u0131rman\u0131z art\u0131k Home Assistant taraf\u0131ndan kullan\u0131lm\u0131yor. Bu sorunu gidermek i\u00e7in YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Mi Flora entegrasyonu de\u011fi\u015ftirildi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/es.json b/homeassistant/components/mikrotik/translations/es.json index e252c492c47..d4751c19a9a 100644 --- a/homeassistant/components/mikrotik/translations/es.json +++ b/homeassistant/components/mikrotik/translations/es.json @@ -15,7 +15,7 @@ "name": "Nombre", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario", + "username": "Nombre de usuario", "verify_ssl": "Usar ssl" }, "title": "Configurar el router Mikrotik" diff --git a/homeassistant/components/min_max/translations/es.json b/homeassistant/components/min_max/translations/es.json index fadad650eaa..2be7203d0d4 100644 --- a/homeassistant/components/min_max/translations/es.json +++ b/homeassistant/components/min_max/translations/es.json @@ -9,10 +9,10 @@ "type": "Caracter\u00edstica estad\u00edstica" }, "data_description": { - "round_digits": "Controla el n\u00famero de d\u00edgitos decimales cuando la caracter\u00edstica estad\u00edstica es la media o mediana." + "round_digits": "Controla el n\u00famero de d\u00edgitos decimales en la salida cuando la caracter\u00edstica estad\u00edstica es media o mediana." }, "description": "Cree un sensor que calcule un valor m\u00ednimo, m\u00e1ximo, medio o mediano a partir de una lista de sensores de entrada.", - "title": "Agregar sensor m\u00edn / m\u00e1x / media / mediana" + "title": "A\u00f1adir sensor m\u00edn / m\u00e1x / media / mediana" } } }, @@ -25,7 +25,7 @@ "type": "Caracter\u00edstica estad\u00edstica" }, "data_description": { - "round_digits": "Controla el n\u00famero de d\u00edgitos decimales cuando la caracter\u00edstica estad\u00edstica es la media o mediana." + "round_digits": "Controla el n\u00famero de d\u00edgitos decimales en la salida cuando la caracter\u00edstica estad\u00edstica es media o mediana." } } } diff --git a/homeassistant/components/mitemp_bt/translations/es.json b/homeassistant/components/mitemp_bt/translations/es.json new file mode 100644 index 00000000000..6ae1e300961 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/es.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "La integraci\u00f3n del sensor de temperatura y humedad Xiaomi Mijia BLE dej\u00f3 de funcionar en Home Assistant 2022.7 y fue reemplazada por la integraci\u00f3n Xiaomi BLE en la versi\u00f3n 2022.8. \n\nNo hay una ruta de migraci\u00f3n posible, por lo tanto, debes agregar tu dispositivo Xiaomi Mijia BLE utilizando la nueva integraci\u00f3n manualmente. \n\nHome Assistant ya no utiliza la configuraci\u00f3n YAML del sensor de temperatura y humedad BLE de Xiaomi Mijia. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "La integraci\u00f3n del sensor de temperatura y humedad Xiaomi Mijia BLE ha sido reemplazada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/et.json b/homeassistant/components/mitemp_bt/translations/et.json new file mode 100644 index 00000000000..2dd28b9cc4e --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/et.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Xiaomi Mijia BLE temperatuuri- ja niiskusanduri integratsioon lakkas t\u00f6\u00f6tamast Home Assistant 2022.7 versioonis ja asendati Xiaomi BLE integratsiooniga 2022.8 versioonis.\n\nMigratsiooniteed ei ole v\u00f5imalik, seega peate oma Xiaomi Mijia BLE-seadme lisama uue integratsiooni abil k\u00e4sitsi.\n\nTeie olemasolevat Xiaomi Mijia BLE temperatuuri ja \u00f5huniiskuse anduri YAML-konfiguratsiooni ei kasuta enam Home Assistant. Eemaldage YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivitage Home Assistant uuesti, et see probleem lahendada.", + "title": "Xiaomi Mijia BLE temperatuuri ja niiskusanduri sidumine on asendatud" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mitemp_bt/translations/tr.json b/homeassistant/components/mitemp_bt/translations/tr.json new file mode 100644 index 00000000000..5e7c18a9544 --- /dev/null +++ b/homeassistant/components/mitemp_bt/translations/tr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "replaced": { + "description": "Xiaomi Mijia BLE S\u0131cakl\u0131k ve Nem Sens\u00f6r\u00fc entegrasyonu Home Assistant 2022.7'de \u00e7al\u0131\u015fmay\u0131 durdurdu ve 2022.8 s\u00fcr\u00fcm\u00fcnde Xiaomi BLE entegrasyonu ile de\u011fi\u015ftirildi.\n\nGe\u00e7i\u015f yolu m\u00fcmk\u00fcn de\u011fildir, bu nedenle Xiaomi Mijia BLE cihaz\u0131n\u0131z\u0131 yeni entegrasyonu kullanarak manuel olarak eklemeniz gerekir.\n\nMevcut Xiaomi Mijia BLE S\u0131cakl\u0131k ve Nem Sens\u00f6r\u00fc YAML yap\u0131land\u0131rman\u0131z art\u0131k Home Assistant taraf\u0131ndan kullan\u0131lm\u0131yor. YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Xiaomi Mijia BLE S\u0131cakl\u0131k ve Nem Sens\u00f6r\u00fc entegrasyonu de\u011fi\u015ftirildi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/es.json b/homeassistant/components/moat/translations/es.json new file mode 100644 index 00000000000..76fb203eacd --- /dev/null +++ b/homeassistant/components/moat/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/tr.json b/homeassistant/components/moat/translations/tr.json new file mode 100644 index 00000000000..f63cee3493c --- /dev/null +++ b/homeassistant/components/moat/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/translations/es.json b/homeassistant/components/modem_callerid/translations/es.json index 6c62f64a1d6..bc1b20cbbe0 100644 --- a/homeassistant/components/modem_callerid/translations/es.json +++ b/homeassistant/components/modem_callerid/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "no_devices_found": "No se encontraron dispositivos restantes" }, "error": { diff --git a/homeassistant/components/motion_blinds/translations/es.json b/homeassistant/components/motion_blinds/translations/es.json index 1a4312d4fe0..11e8c2a2e71 100644 --- a/homeassistant/components/motion_blinds/translations/es.json +++ b/homeassistant/components/motion_blinds/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "connection_error": "No se pudo conectar" }, "error": { diff --git a/homeassistant/components/motioneye/translations/es.json b/homeassistant/components/motioneye/translations/es.json index e9480019bb6..881972cf5c4 100644 --- a/homeassistant/components/motioneye/translations/es.json +++ b/homeassistant/components/motioneye/translations/es.json @@ -18,9 +18,9 @@ "user": { "data": { "admin_password": "Contrase\u00f1a administrador", - "admin_username": "Usuario administrador", + "admin_username": "Nombre de usuario administrador", "surveillance_password": "Contrase\u00f1a vigilancia", - "surveillance_username": "Usuario vigilancia", + "surveillance_username": "Nombre de usuario vigilancia", "url": "URL" } } diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index 89a5ce04d97..93fe4b53933 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -14,7 +14,7 @@ "discovery": "Habilitar descubrimiento", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Por favor, introduzca la informaci\u00f3n de conexi\u00f3n de su br\u00f3ker MQTT." }, @@ -61,7 +61,7 @@ "broker": "Br\u00f3ker", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Por favor, introduzca la informaci\u00f3n de conexi\u00f3n de su br\u00f3ker MQTT.", "title": "Opciones del br\u00f3ker" diff --git a/homeassistant/components/myq/translations/es.json b/homeassistant/components/myq/translations/es.json index 98d02b56280..d8520cd2b6f 100644 --- a/homeassistant/components/myq/translations/es.json +++ b/homeassistant/components/myq/translations/es.json @@ -20,7 +20,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Conectar con el Gateway " } diff --git a/homeassistant/components/mysensors/translations/ca.json b/homeassistant/components/mysensors/translations/ca.json index 1527f2b3307..94ba8b80c2f 100644 --- a/homeassistant/components/mysensors/translations/ca.json +++ b/homeassistant/components/mysensors/translations/ca.json @@ -70,6 +70,7 @@ "description": "Configuraci\u00f3 de passarel\u00b7la Ethernet" }, "select_gateway_type": { + "description": "Selecciona la passarel\u00b7la (gateway) a configurar.", "menu_options": { "gw_mqtt": "Configura passarel\u00b7la (gateway) MQTT", "gw_serial": "Configura passarel\u00b7la (gateway) s\u00e8rie", diff --git a/homeassistant/components/mysensors/translations/el.json b/homeassistant/components/mysensors/translations/el.json index 81ba72eea7e..a761f148c27 100644 --- a/homeassistant/components/mysensors/translations/el.json +++ b/homeassistant/components/mysensors/translations/el.json @@ -14,6 +14,7 @@ "invalid_serial": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1", "invalid_subscribe_topic": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b8\u03ad\u03bc\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2", "invalid_version": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 MySensors", + "mqtt_required": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 MQTT \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", "not_a_number": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc", "port_out_of_range": "\u039f \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03b8\u03cd\u03c1\u03b1\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03bf\u03c5\u03bb\u03ac\u03c7\u03b9\u03c3\u03c4\u03bf\u03bd 1 \u03ba\u03b1\u03b9 \u03c4\u03bf \u03c0\u03bf\u03bb\u03cd 65535", "same_topic": "\u03a4\u03b1 \u03b8\u03ad\u03bc\u03b1\u03c4\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03ba\u03b1\u03b9 \u03b4\u03b7\u03bc\u03bf\u03c3\u03af\u03b5\u03c5\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03b1 \u03af\u03b4\u03b9\u03b1", @@ -68,6 +69,14 @@ }, "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03cd\u03bb\u03b7\u03c2 Ethernet" }, + "select_gateway_type": { + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03bf\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 \u03b8\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5.", + "menu_options": { + "gw_mqtt": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bc\u03b9\u03b1\u03c2 \u03c0\u03cd\u03bb\u03b7\u03c2 MQTT", + "gw_serial": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bc\u03b9\u03b1\u03c2 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae\u03c2 \u03c0\u03cd\u03bb\u03b7\u03c2", + "gw_tcp": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bc\u03b9\u03b1\u03c2 \u03c0\u03cd\u03bb\u03b7\u03c2 TCP" + } + }, "user": { "data": { "gateway_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03cd\u03bb\u03b7\u03c2" diff --git a/homeassistant/components/mysensors/translations/es.json b/homeassistant/components/mysensors/translations/es.json index 91f2b7f0d1e..15234537136 100644 --- a/homeassistant/components/mysensors/translations/es.json +++ b/homeassistant/components/mysensors/translations/es.json @@ -14,6 +14,7 @@ "invalid_serial": "Puerto serie no v\u00e1lido", "invalid_subscribe_topic": "Tema de suscripci\u00f3n no v\u00e1lido", "invalid_version": "Versi\u00f3n inv\u00e1lida de MySensors", + "mqtt_required": "La integraci\u00f3n MQTT no est\u00e1 configurada", "not_a_number": "Por favor, introduzca un n\u00famero", "port_out_of_range": "El n\u00famero de puerto debe ser como m\u00ednimo 1 y como m\u00e1ximo 65535", "same_topic": "Los temas de suscripci\u00f3n y publicaci\u00f3n son los mismos", @@ -68,6 +69,14 @@ }, "description": "Configuraci\u00f3n de la pasarela Ethernet" }, + "select_gateway_type": { + "description": "Selecciona qu\u00e9 puerta de enlace configurar.", + "menu_options": { + "gw_mqtt": "Configurar una puerta de enlace MQTT", + "gw_serial": "Configurar una puerta de enlace serie", + "gw_tcp": "Configurar una puerta de enlace TCP" + } + }, "user": { "data": { "gateway_type": "Tipo de pasarela" diff --git a/homeassistant/components/mysensors/translations/et.json b/homeassistant/components/mysensors/translations/et.json index 00614e57252..a1bcacc1852 100644 --- a/homeassistant/components/mysensors/translations/et.json +++ b/homeassistant/components/mysensors/translations/et.json @@ -14,6 +14,7 @@ "invalid_serial": "Sobimatu jadaport", "invalid_subscribe_topic": "Kehtetu tellimisteema", "invalid_version": "Sobimatu MySensors versioon", + "mqtt_required": "MQTT sidumine on loomata", "not_a_number": "Sisesta number", "port_out_of_range": "Pordi number peab olema v\u00e4hemalt 1 ja k\u00f5ige rohkem 65535", "same_topic": "Tellimise ja avaldamise teemad kattuvad", @@ -68,6 +69,14 @@ }, "description": "Etherneti l\u00fc\u00fcsi seadistamine" }, + "select_gateway_type": { + "description": "Vali seadistatav l\u00fc\u00fcs", + "menu_options": { + "gw_mqtt": "Seadista MQTT l\u00fc\u00fcs", + "gw_serial": "Seadista jadal\u00fc\u00fcs", + "gw_tcp": "TCP l\u00fc\u00fcsi seadistamine" + } + }, "user": { "data": { "gateway_type": "L\u00fc\u00fcsi t\u00fc\u00fcp" diff --git a/homeassistant/components/mysensors/translations/tr.json b/homeassistant/components/mysensors/translations/tr.json index 9f99525c7b2..9fc26a7119b 100644 --- a/homeassistant/components/mysensors/translations/tr.json +++ b/homeassistant/components/mysensors/translations/tr.json @@ -14,6 +14,7 @@ "invalid_serial": "Ge\u00e7ersiz seri ba\u011flant\u0131 noktas\u0131", "invalid_subscribe_topic": "Ge\u00e7ersiz abone konusu", "invalid_version": "Ge\u00e7ersiz MySensors s\u00fcr\u00fcm\u00fc", + "mqtt_required": "MQTT entegrasyonu kurulmam\u0131\u015f", "not_a_number": "L\u00fctfen bir numara giriniz", "port_out_of_range": "Port numaras\u0131 en az 1, en fazla 65535 olmal\u0131d\u0131r", "same_topic": "Abone olma ve yay\u0131nlama konular\u0131 ayn\u0131", @@ -68,6 +69,14 @@ }, "description": "Ethernet a\u011f ge\u00e7idi kurulumu" }, + "select_gateway_type": { + "description": "Hangi a\u011f ge\u00e7idinin yap\u0131land\u0131r\u0131laca\u011f\u0131n\u0131 se\u00e7in.", + "menu_options": { + "gw_mqtt": "Bir MQTT a\u011f ge\u00e7idini yap\u0131land\u0131r\u0131n", + "gw_serial": "Bir seri a\u011f ge\u00e7idi yap\u0131land\u0131r\u0131n", + "gw_tcp": "Bir TCP a\u011f ge\u00e7idi yap\u0131land\u0131r\u0131n" + } + }, "user": { "data": { "gateway_type": "A\u011f ge\u00e7idi t\u00fcr\u00fc" diff --git a/homeassistant/components/nam/translations/es.json b/homeassistant/components/nam/translations/es.json index feeff2015f9..b6494327077 100644 --- a/homeassistant/components/nam/translations/es.json +++ b/homeassistant/components/nam/translations/es.json @@ -19,14 +19,14 @@ "credentials": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Por favor, introduzca el nombre de usuario y la contrase\u00f1a." }, "reauth_confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Por favor, introduzca el nombre de usuario y la contrase\u00f1a correctos para el host: {host}" }, diff --git a/homeassistant/components/nest/translations/el.json b/homeassistant/components/nest/translations/el.json index b94ec0ee9df..69b6f096d8a 100644 --- a/homeassistant/components/nest/translations/el.json +++ b/homeassistant/components/nest/translations/el.json @@ -96,5 +96,15 @@ "camera_sound": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ae\u03c7\u03bf\u03c2", "doorbell_chime": "\u03a4\u03bf \u03ba\u03bf\u03c5\u03b4\u03bf\u03cd\u03bd\u03b9 \u03c4\u03b7\u03c2 \u03c0\u03cc\u03c1\u03c4\u03b1\u03c2 \u03c0\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5" } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Nest \u03c3\u03c4\u03bf configuration.yaml \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant 2022.10. \n\n \u03a4\u03b1 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03bd\u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 OAuth \u03ba\u03b1\u03b9 \u03bf\u03b9 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Nest YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + }, + "removed_app_auth": { + "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03b2\u03b5\u03bb\u03c4\u03b9\u03ce\u03c3\u03b5\u03b9 \u03c4\u03b7\u03bd \u03b1\u03c3\u03c6\u03ac\u03bb\u03b5\u03b9\u03b1 \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03bc\u03b5\u03b9\u03ce\u03c3\u03b5\u03b9 \u03c4\u03bf\u03bd \u03ba\u03af\u03bd\u03b4\u03c5\u03bd\u03bf \u03b7\u03bb\u03b5\u03ba\u03c4\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03c8\u03b1\u03c1\u03ad\u03bc\u03b1\u03c4\u03bf\u03c2, \u03b7 Google \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03b5\u03b9 \u03c4\u03b7 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n **\u0391\u03c5\u03c4\u03cc \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1 \u03b1\u03c0\u03cc \u03b5\u03c3\u03ac\u03c2 \u03b3\u03b9\u03b1 \u03b5\u03c0\u03af\u03bb\u03c5\u03c3\u03b7** ([\u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2]( {more_info_url} )) \n\n 1. \u0395\u03c0\u03b9\u03c3\u03ba\u03b5\u03c6\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03b5\u03bb\u03af\u03b4\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03ce\u03c3\u03b5\u03c9\u03bd\n 1. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u0395\u03c0\u03b1\u03bd\u03b1\u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c3\u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Nest.\n 1. \u03a4\u03bf Home Assistant \u03b8\u03b1 \u03c3\u03b1\u03c2 \u03ba\u03b1\u03b8\u03bf\u03b4\u03b7\u03b3\u03ae\u03c3\u03b5\u03b9 \u03c3\u03c4\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03b2\u03ac\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c3\u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 Web. \n\n \u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03bf Nest [\u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2]( {documentation_url} ) \u03b3\u03b9\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b1\u03bd\u03c4\u03b9\u03bc\u03b5\u03c4\u03ce\u03c0\u03b9\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b2\u03bb\u03b7\u03bc\u03ac\u03c4\u03c9\u03bd.", + "title": "\u03a4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 Nest \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03b8\u03bf\u03cd\u03bd" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index ea65d2fc78a..a04dc0baab6 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -1,6 +1,10 @@ { + "application_credentials": { + "description": "Sigue las [instrucciones]({more_info_url}) para configurar Cloud Console: \n\n 1. Ve a la [pantalla de consentimiento de OAuth]( {oauth_consent_url} ) y configura\n 1. Ve a [Credenciales]( {oauth_creds_url} ) y haz clic en **Crear credenciales**.\n 1. En la lista desplegable, selecciona **ID de cliente de OAuth**.\n 1. Selecciona **Aplicaci\u00f3n web** para el Tipo de aplicaci\u00f3n.\n 1. A\u00f1ade `{redirect_url}` debajo de *URI de redirecci\u00f3n autorizada*." + }, "config": { "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", "invalid_access_token": "Token de acceso no v\u00e1lido", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", @@ -29,6 +33,32 @@ "description": "Para vincular tu cuenta de Google, [autoriza tu cuenta]({url}).\n\nDespu\u00e9s de la autorizaci\u00f3n, copie y pegue el c\u00f3digo Auth Token proporcionado a continuaci\u00f3n.", "title": "Vincular cuenta de Google" }, + "auth_upgrade": { + "description": "Google ha dejado de usar App Auth para mejorar la seguridad, y debes tomar medidas creando nuevas credenciales de aplicaci\u00f3n. \n\nAbre la [documentaci\u00f3n]({more_info_url}) para seguir, ya que los siguientes pasos te guiar\u00e1n a trav\u00e9s de los pasos que debes seguir para restaurar el acceso a tus dispositivos Nest.", + "title": "Nest: desactivaci\u00f3n de App Auth" + }, + "cloud_project": { + "data": { + "cloud_project_id": "ID de proyecto de Google Cloud" + }, + "description": "Introduce el ID del Cloud Project a continuaci\u00f3n, por ejemplo, *example-project-12345*. Consulta la [Consola de Google Cloud]({cloud_console_url}) o la documentaci\u00f3n para obtener [m\u00e1s informaci\u00f3n]({more_info_url}).", + "title": "Nest: Introduce el ID del Cloud Project" + }, + "create_cloud_project": { + "description": "La integraci\u00f3n Nest te permite integrar tus termostatos, c\u00e1maras y timbres Nest mediante la API de administraci\u00f3n de dispositivos inteligentes (SDM). La API de SDM **requiere una tarifa de configuraci\u00f3n \u00fanica de 5$**. Consulta la documentaci\u00f3n para obtener [m\u00e1s informaci\u00f3n]({more_info_url}).\n\n1. Ve a [Google Cloud Console]({cloud_console_url}).\n1. Si este es tu primer proyecto, haz clic en **Crear proyecto** y luego en **Nuevo proyecto**.\n1. Asigna un nombre a tu Cloud Project y, a continuaci\u00f3n, haz clic en **Crear**.\n1. Guarda el ID del Cloud Project, por ejemplo, *example-project-12345* ya que lo necesitar\u00e1s m\u00e1s adelante\n1. Ve a la Biblioteca de API de [API de administraci\u00f3n de dispositivos inteligentes]({sdm_api_url}) y haz clic en **Habilitar**.\n1. Ve a la biblioteca de API de [Cloud Pub/Sub API]({pubsub_api_url}) y haz clic en **Habilitar**.\n\nContin\u00faa cuando tu proyecto en la nube est\u00e9 configurado.", + "title": "Nest: Crear y configurar un Cloud Project" + }, + "device_project": { + "data": { + "project_id": "ID de proyecto de acceso a dispositivos" + }, + "description": "Crea un proyecto de acceso a dispositivos Nest que **requiere una tarifa de 5$** para configurarlo.\n 1. Ve a la [Consola de acceso al dispositivo] ({device_access_console_url}) y sigue el flujo de pago.\n 1. Haz clic en **Crear proyecto**\n 1. Asigna un nombre a tu proyecto de acceso a dispositivos y haz clic en **Siguiente**.\n 1. Introduce tu ID de cliente de OAuth\n 1. Habilita los eventos haciendo clic en **Habilitar** y **Crear proyecto**. \n\n Introduce tu ID de proyecto de acceso a dispositivos a continuaci\u00f3n ([m\u00e1s informaci\u00f3n]({more_info_url})).", + "title": "Nest: Crear un proyecto de acceso a dispositivos" + }, + "device_project_upgrade": { + "description": "Actualiza el Proyecto de acceso a dispositivos Nest con tu nuevo ID de cliente de OAuth ([m\u00e1s informaci\u00f3n]({more_info_url}))\n 1. Ve a la [Consola de acceso al dispositivo]({device_access_console_url}).\n 1. Haz clic en el icono de la papelera junto a *ID de cliente de OAuth*.\n 1. Haz clic en el men\u00fa adicional `...` y *A\u00f1adir ID de cliente*.\n 1. Introduce tu nuevo ID de cliente de OAuth y haz clic en **A\u00f1adir**. \n\n Tu ID de cliente de OAuth es: `{client_id}`", + "title": "Nest: Actualizar el proyecto de acceso a dispositivos" + }, "init": { "data": { "flow_impl": "Proveedor" @@ -66,5 +96,15 @@ "camera_sound": "Sonido detectado", "doorbell_chime": "Timbre pulsado" } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3n de Nest en configuration.yaml se eliminar\u00e1 en Home Assistant 2022.10. \n\nTus credenciales OAuth de aplicaci\u00f3n existentes y la configuraci\u00f3n de acceso se han importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Nest" + }, + "removed_app_auth": { + "description": "Para mejorar la seguridad y reducir el riesgo de phishing, Google ha dejado de utilizar el m\u00e9todo de autenticaci\u00f3n utilizado por Home Assistant. \n\n **Esto requiere una acci\u00f3n de su parte para resolverlo** ([m\u00e1s informaci\u00f3n]({more_info_url})) \n\n 1. Visita la p\u00e1gina de integraciones\n 1. Haz clic en Reconfigurar en la integraci\u00f3n de Nest.\n 1. Home Assistant te guiar\u00e1 a trav\u00e9s de los pasos para actualizar a la autenticaci\u00f3n web. \n\nConsulta las [instrucciones de integraci\u00f3n]({documentation_url}) de Nest para obtener informaci\u00f3n sobre la soluci\u00f3n de problemas.", + "title": "Las credenciales de autenticaci\u00f3n de Nest deben actualizarse" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/et.json b/homeassistant/components/nest/translations/et.json index b07845f7dab..b79c320a35b 100644 --- a/homeassistant/components/nest/translations/et.json +++ b/homeassistant/components/nest/translations/et.json @@ -96,5 +96,15 @@ "camera_sound": "Tuvastati heli", "doorbell_chime": "Uksekell helises" } + }, + "issues": { + "deprecated_yaml": { + "description": "Nesti seadistamine rakenduses configuration.yaml eemaldatakse koduabilisest 2022.10.\n\nTeie olemasolevad OAuthi rakenduse identimis- ja juurdep\u00e4\u00e4sus\u00e4tted on automaatselt kasutajaliidesesse imporditud. Eemaldage FAILIST CONFIGURATION.yaml YAML-konfiguratsioon ja taask\u00e4ivitage selle probleemi lahendamiseks Home Assistant.", + "title": "Nesti YAML-i konfiguratsioon eemaldatakse" + }, + "removed_app_auth": { + "description": "Turvalisuse parandamiseks ja andmep\u00fc\u00fcgiriski v\u00e4hendamiseks katkestas Google Home Assistanti autentimismeetodi. \n\n **Selle lahendamiseks peate midagi ette v\u00f5tma** ([rohkem teavet]( {more_info_url} )) \n\n 1. K\u00fclastage integreerimise lehte\n 1. Kl\u00f5psake Nesti integratsioonil nuppu Konfigureeri uuesti.\n 1. Koduassistent juhendab teid veebiautentimisele \u00fcleminekuks. \n\n Veaotsingu teabe saamiseks vaadake Nesti [integreerimisjuhiseid]( {documentation_url} ).", + "title": "Nesti autentimise mandaate tuleb v\u00e4rskendada" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/tr.json b/homeassistant/components/nest/translations/tr.json index bc69b928ba1..986412798dc 100644 --- a/homeassistant/components/nest/translations/tr.json +++ b/homeassistant/components/nest/translations/tr.json @@ -96,5 +96,15 @@ "camera_sound": "Ses alg\u0131land\u0131", "doorbell_chime": "Kap\u0131 zili bas\u0131ld\u0131" } + }, + "issues": { + "deprecated_yaml": { + "description": "Nest'i configuration.yaml'de yap\u0131land\u0131rma, Home Assistant 2022.10'da kald\u0131r\u0131l\u0131yor. \n\n Mevcut OAuth Uygulama Kimlik Bilgileriniz ve eri\u015fim ayarlar\u0131n\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Nest YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + }, + "removed_app_auth": { + "description": "G\u00fcvenli\u011fi art\u0131rmak ve kimlik av\u0131 riskini azaltmak i\u00e7in Google, Home Assistant taraf\u0131ndan kullan\u0131lan kimlik do\u011frulama y\u00f6ntemini kullan\u0131mdan kald\u0131rm\u0131\u015ft\u0131r. \n\n **Bu, \u00e7\u00f6zmek i\u00e7in sizin taraf\u0131n\u0131zdan bir i\u015flem yap\u0131lmas\u0131n\u0131 gerektirir** ([daha fazla bilgi]( {more_info_url} )) \n\n 1. Entegrasyon sayfas\u0131n\u0131 ziyaret edin\n 1. Nest entegrasyonunda Yeniden Yap\u0131land\u0131r'\u0131 t\u0131klay\u0131n.\n 1. Home Assistant, Web Kimlik Do\u011frulamas\u0131na y\u00fckseltme ad\u0131mlar\u0131nda size yol g\u00f6sterecektir. \n\n Sorun giderme bilgileri i\u00e7in Nest [entegrasyon talimatlar\u0131na]( {documentation_url} ) bak\u0131n.", + "title": "Nest Kimlik Do\u011frulama Kimlik Bilgileri g\u00fcncellenmelidir" + } } } \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/es.json b/homeassistant/components/netgear/translations/es.json index 9edbd1101d0..69bea9a5de6 100644 --- a/homeassistant/components/netgear/translations/es.json +++ b/homeassistant/components/netgear/translations/es.json @@ -11,7 +11,7 @@ "data": { "host": "Host (Opcional)", "password": "Contrase\u00f1a", - "username": "Usuario (Opcional)" + "username": "Nombre de usuario (Opcional)" }, "description": "Host predeterminado: {host} \nNombre de usuario predeterminado: {username}" } diff --git a/homeassistant/components/nextdns/translations/es.json b/homeassistant/components/nextdns/translations/es.json new file mode 100644 index 00000000000..428b25128d8 --- /dev/null +++ b/homeassistant/components/nextdns/translations/es.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Este perfil de NextDNS ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_api_key": "Clave API no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "profiles": { + "data": { + "profile": "Perfil" + } + }, + "user": { + "data": { + "api_key": "Clave API" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "Llegar al servidor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nina/translations/es.json b/homeassistant/components/nina/translations/es.json index e7e20c0908a..a605bfb7a9a 100644 --- a/homeassistant/components/nina/translations/es.json +++ b/homeassistant/components/nina/translations/es.json @@ -23,5 +23,27 @@ "title": "Seleccionar ciudad/pa\u00eds" } } + }, + "options": { + "error": { + "cannot_connect": "No se pudo conectar", + "no_selection": "Por favor, selecciona al menos una ciudad/condado", + "unknown": "Error inesperado" + }, + "step": { + "init": { + "data": { + "_a_to_d": "Ciudad/condado (A-D)", + "_e_to_h": "Ciudad/condado (E-H)", + "_i_to_l": "Ciudad/condado (I-L)", + "_m_to_q": "Ciudad/condado (M-Q)", + "_r_to_u": "Ciudad/condado (R-U)", + "_v_to_z": "Ciudad/condado (V-Z)", + "corona_filter": "Eliminar las advertencias de Corona", + "slots": "Advertencias m\u00e1ximas por ciudad/condado" + }, + "title": "Opciones" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/notion/translations/es.json b/homeassistant/components/notion/translations/es.json index 62a21b06022..3eb17fa606d 100644 --- a/homeassistant/components/notion/translations/es.json +++ b/homeassistant/components/notion/translations/es.json @@ -19,7 +19,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Completa tu informaci\u00f3n" } diff --git a/homeassistant/components/nuheat/translations/es.json b/homeassistant/components/nuheat/translations/es.json index 088978d4c50..a64a68e2e70 100644 --- a/homeassistant/components/nuheat/translations/es.json +++ b/homeassistant/components/nuheat/translations/es.json @@ -14,7 +14,7 @@ "data": { "password": "Contrase\u00f1a", "serial_number": "N\u00famero de serie del termostato.", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Necesitas obtener el n\u00famero de serie o el ID de tu termostato iniciando sesi\u00f3n en https://MyNuHeat.com y seleccionando tu(s) termostato(s).", "title": "ConectarNuHeat" diff --git a/homeassistant/components/nut/translations/es.json b/homeassistant/components/nut/translations/es.json index 898bf1f027d..edb49ebcbcd 100644 --- a/homeassistant/components/nut/translations/es.json +++ b/homeassistant/components/nut/translations/es.json @@ -19,7 +19,7 @@ "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Conectar con el servidor NUT" } diff --git a/homeassistant/components/nzbget/translations/es.json b/homeassistant/components/nzbget/translations/es.json index e3b1a595c90..eeb2b46ba8b 100644 --- a/homeassistant/components/nzbget/translations/es.json +++ b/homeassistant/components/nzbget/translations/es.json @@ -16,8 +16,8 @@ "password": "Contrase\u00f1a", "port": "Puerto", "ssl": "Utiliza un certificado SSL", - "username": "Usuario", - "verify_ssl": "Verificar certificado SSL" + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL" }, "title": "Conectarse a NZBGet" } diff --git a/homeassistant/components/octoprint/translations/es.json b/homeassistant/components/octoprint/translations/es.json index 241b3a4111e..e9135b25be8 100644 --- a/homeassistant/components/octoprint/translations/es.json +++ b/homeassistant/components/octoprint/translations/es.json @@ -21,7 +21,7 @@ "path": "Ruta de aplicaci\u00f3n", "port": "N\u00famero de puerto", "ssl": "Usar SSL", - "username": "Usuario", + "username": "Nombre de usuario", "verify_ssl": "Verificar certificado SSL" } } diff --git a/homeassistant/components/omnilogic/translations/es.json b/homeassistant/components/omnilogic/translations/es.json index 54aef5f1892..1755942e5df 100644 --- a/homeassistant/components/omnilogic/translations/es.json +++ b/homeassistant/components/omnilogic/translations/es.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/onvif/translations/es.json b/homeassistant/components/onvif/translations/es.json index 6ba7dbe0ee8..7858a4561fe 100644 --- a/homeassistant/components/onvif/translations/es.json +++ b/homeassistant/components/onvif/translations/es.json @@ -17,7 +17,7 @@ "name": "Nombre", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Configurar dispositivo ONVIF" }, diff --git a/homeassistant/components/openalpr_local/translations/es.json b/homeassistant/components/openalpr_local/translations/es.json new file mode 100644 index 00000000000..4c1b1de6e05 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/es.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "La integraci\u00f3n OpenALPR Local est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la integraci\u00f3n OpenALPR Local" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/et.json b/homeassistant/components/openalpr_local/translations/et.json new file mode 100644 index 00000000000..aca98183950 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/et.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "OpenALPRi kohalik integratsioon on Home Assistantist eemaldamisel ja see ei ole enam saadaval alates Home Assistant 2022.10.\n\nProbleemi lahendamiseks eemaldage YAML-konfiguratsioon failist configuration.yaml ja k\u00e4ivitage Home Assistant uuesti.", + "title": "OpenALPR Locali integratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/tr.json b/homeassistant/components/openalpr_local/translations/tr.json new file mode 100644 index 00000000000..479e5385980 --- /dev/null +++ b/homeassistant/components/openalpr_local/translations/tr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "OpenALPR Yerel entegrasyonu Home Assistant'tan kald\u0131r\u0131lmay\u0131 beklemektedir ve Home Assistant 2022.10'dan itibaren art\u0131k kullan\u0131lamayacakt\u0131r.\n\nYAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "OpenALPR Yerel entegrasyonu kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/ca.json b/homeassistant/components/openexchangerates/translations/ca.json index f5a93caa71d..98c74ae55b3 100644 --- a/homeassistant/components/openexchangerates/translations/ca.json +++ b/homeassistant/components/openexchangerates/translations/ca.json @@ -15,9 +15,18 @@ "step": { "user": { "data": { - "api_key": "Clau API" + "api_key": "Clau API", + "base": "Moneda base" + }, + "data_description": { + "base": "L'\u00fas d'una moneda base que no sigui USD requereix un [pla de pagament]({signup})." } } } + }, + "issues": { + "deprecated_yaml": { + "title": "La configuraci\u00f3 YAML d'Open Exchange Rates est\u00e0 sent eliminada" + } } } \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/el.json b/homeassistant/components/openexchangerates/translations/el.json new file mode 100644 index 00000000000..dee7e836d01 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/el.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "timeout_connect": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "timeout_connect": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "base": "\u0392\u03b1\u03c3\u03b9\u03ba\u03cc \u03bd\u03cc\u03bc\u03b9\u03c3\u03bc\u03b1" + }, + "data_description": { + "base": "\u0393\u03b9\u03b1 \u03c4\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03ac\u03bb\u03bb\u03bf\u03c5 \u03b2\u03b1\u03c3\u03b9\u03ba\u03bf\u03cd \u03bd\u03bf\u03bc\u03af\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b1\u03c0\u03cc \u03c4\u03bf USD \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03ad\u03bd\u03b1 [\u03b5\u03c0\u03af \u03c0\u03bb\u03b7\u03c1\u03c9\u03bc\u03ae \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1]({\u03c3\u03c5\u03bd\u03b4\u03c1\u03bf\u03bc\u03ae})." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \u03b1\u03bd\u03bf\u03b9\u03ba\u03c4\u03ce\u03bd \u03b9\u03c3\u03bf\u03c4\u03b9\u03bc\u03b9\u03ce\u03bd \u03c3\u03c5\u03bd\u03b1\u03bb\u03bb\u03ac\u03b3\u03bc\u03b1\u03c4\u03bf\u03c2 \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Open Exchange Rates YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Open Exchange Rates YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/es.json b/homeassistant/components/openexchangerates/translations/es.json new file mode 100644 index 00000000000..fb5897846db --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/es.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "base": "Moneda base" + }, + "data_description": { + "base": "El uso de una moneda base distinta al USD requiere un [plan de pago]({signup})." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de Open Exchange Rates mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Open Exchange Rates de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Open Exchange Rates" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/et.json b/homeassistant/components/openexchangerates/translations/et.json new file mode 100644 index 00000000000..fbeed4f8443 --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/et.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "timeout_connect": "\u00dchenduse loomise ajal\u00f5pp" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "timeout_connect": "\u00dchenduse loomise ajal\u00f5pp", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "base": "P\u00f5hivaluuta" + }, + "data_description": { + "base": "Teise baasvaluuta kui USD kasutamine n\u00f5uab [tasulist plaani]({signup})." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Open Exchange Rates konfigureerimine YAML-i abil eemaldatakse.\n\nOlemasolev YAML-konfiguratsioon on automaatselt kasutajaliidesesse imporditud.\n\nEemalda Open Exchange Rates YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivita Home Assistant uuesti, et see probleem lahendada.", + "title": "Open Exchange Rates YAML-konfiguratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openexchangerates/translations/tr.json b/homeassistant/components/openexchangerates/translations/tr.json new file mode 100644 index 00000000000..436e6bbb07b --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/tr.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "timeout_connect": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "timeout_connect": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "base": "Temel para birimi" + }, + "data_description": { + "base": "USD'den ba\u015fka bir temel para birimi kullanmak, bir [\u00fccretli plan]( {signup} ) gerektirir." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "YAML kullanarak A\u00e7\u0131k D\u00f6viz Kurlar\u0131n\u0131 yap\u0131land\u0131rma kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. \n\n Open Exchange Rates YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "A\u00e7\u0131k D\u00f6viz Kurlar\u0131 YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/es.json b/homeassistant/components/opengarage/translations/es.json index d282e706afc..cc8a51dc8fc 100644 --- a/homeassistant/components/opengarage/translations/es.json +++ b/homeassistant/components/opengarage/translations/es.json @@ -14,7 +14,7 @@ "device_key": "Clave del dispositivo", "host": "Host", "port": "Puerto", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar el certificado SSL" } } } diff --git a/homeassistant/components/opentherm_gw/translations/es.json b/homeassistant/components/opentherm_gw/translations/es.json index 2b6cc0afd7f..b464549ed71 100644 --- a/homeassistant/components/opentherm_gw/translations/es.json +++ b/homeassistant/components/opentherm_gw/translations/es.json @@ -3,7 +3,8 @@ "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "id_exists": "El ID del Gateway ya existe" + "id_exists": "El ID del Gateway ya existe", + "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/et.json b/homeassistant/components/opentherm_gw/translations/et.json index 2458c8f1d52..27c44ccd620 100644 --- a/homeassistant/components/opentherm_gw/translations/et.json +++ b/homeassistant/components/opentherm_gw/translations/et.json @@ -3,7 +3,8 @@ "error": { "already_configured": "L\u00fc\u00fcs on juba m\u00e4\u00e4ratud", "cannot_connect": "\u00dchendamine nurjus", - "id_exists": "L\u00fc\u00fcsi ID on juba olemas" + "id_exists": "L\u00fc\u00fcsi ID on juba olemas", + "timeout_connect": "\u00dchenduse loomise ajal\u00f5pp" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/tr.json b/homeassistant/components/opentherm_gw/translations/tr.json index 72a603cc827..11b1db771ad 100644 --- a/homeassistant/components/opentherm_gw/translations/tr.json +++ b/homeassistant/components/opentherm_gw/translations/tr.json @@ -3,7 +3,8 @@ "error": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "cannot_connect": "Ba\u011flanma hatas\u0131", - "id_exists": "A\u011f ge\u00e7idi kimli\u011fi zaten var" + "id_exists": "A\u011f ge\u00e7idi kimli\u011fi zaten var", + "timeout_connect": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131" }, "step": { "init": { diff --git a/homeassistant/components/overkiz/translations/es.json b/homeassistant/components/overkiz/translations/es.json index 817864deba8..ac9d3f72ebc 100644 --- a/homeassistant/components/overkiz/translations/es.json +++ b/homeassistant/components/overkiz/translations/es.json @@ -9,7 +9,7 @@ "cannot_connect": "Error al conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "server_in_maintenance": "El servidor est\u00e1 inactivo por mantenimiento", - "too_many_attempts": "Demasiados intentos con un 'token' inv\u00e1lido, bloqueado temporalmente", + "too_many_attempts": "Demasiados intentos con un token no v\u00e1lido, prohibido temporalmente", "too_many_requests": "Demasiadas solicitudes, int\u00e9ntalo de nuevo m\u00e1s tarde.", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/overkiz/translations/sensor.es.json b/homeassistant/components/overkiz/translations/sensor.es.json index 8d0c475586b..523ddf7fe7b 100644 --- a/homeassistant/components/overkiz/translations/sensor.es.json +++ b/homeassistant/components/overkiz/translations/sensor.es.json @@ -36,6 +36,11 @@ "overkiz__sensor_room": { "clean": "Limpiar", "dirty": "Sucio" + }, + "overkiz__three_way_handle_direction": { + "closed": "Cerrado", + "open": "Abierto", + "tilt": "Inclinaci\u00f3n" } } } \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/es.json b/homeassistant/components/ovo_energy/translations/es.json index 549f3af16ca..a060f0f9552 100644 --- a/homeassistant/components/ovo_energy/translations/es.json +++ b/homeassistant/components/ovo_energy/translations/es.json @@ -17,7 +17,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Configurar una instancia de OVO Energy para acceder a su consumo de energ\u00eda.", "title": "A\u00f1adir cuenta de OVO Energy" diff --git a/homeassistant/components/picnic/translations/es.json b/homeassistant/components/picnic/translations/es.json index 5054ae22f5b..7ecfc37d97d 100644 --- a/homeassistant/components/picnic/translations/es.json +++ b/homeassistant/components/picnic/translations/es.json @@ -15,7 +15,7 @@ "data": { "country_code": "C\u00f3digo del pa\u00eds", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/plex/translations/es.json b/homeassistant/components/plex/translations/es.json index 7099f63a9e8..8071e9e11fe 100644 --- a/homeassistant/components/plex/translations/es.json +++ b/homeassistant/components/plex/translations/es.json @@ -3,7 +3,7 @@ "abort": { "all_configured": "Todos los servidores vinculados ya configurados", "already_configured": "Este servidor Plex ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "token_request_timeout": "Tiempo de espera agotado para la obtenci\u00f3n del token", "unknown": "Error inesperado" @@ -23,7 +23,7 @@ "port": "Puerto", "ssl": "Utiliza un certificado SSL", "token": "Token (Opcional)", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar el certificado SSL" }, "title": "Configuraci\u00f3n Manual de Plex" }, diff --git a/homeassistant/components/plugwise/translations/es.json b/homeassistant/components/plugwise/translations/es.json index 16fae70586d..284bfe97943 100644 --- a/homeassistant/components/plugwise/translations/es.json +++ b/homeassistant/components/plugwise/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El servicio ya est\u00e1 configurado" + "already_configured": "El servicio ya est\u00e1 configurado", + "anna_with_adam": "Tanto Anna como Adam han sido detectados. A\u00f1ade tu Adam en lugar de tu Anna" }, "error": { "cannot_connect": "Fall\u00f3 la conexi\u00f3n", @@ -15,9 +16,9 @@ "data": { "flow_type": "Tipo de conexi\u00f3n", "host": "Direcci\u00f3n IP", - "password": "ID Smile", + "password": "ID de Smile", "port": "Puerto", - "username": "Nombre de usuario de la sonrisa" + "username": "Nombre de usuario de Smile" }, "description": "Producto:", "title": "Conectarse a Smile" diff --git a/homeassistant/components/prosegur/translations/es.json b/homeassistant/components/prosegur/translations/es.json index af4f61d6fdc..1cbb1e25dc9 100644 --- a/homeassistant/components/prosegur/translations/es.json +++ b/homeassistant/components/prosegur/translations/es.json @@ -14,14 +14,14 @@ "data": { "description": "Vuelva a autenticarse con su cuenta Prosegur.", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } }, "user": { "data": { "country": "Pa\u00eds", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/ps4/translations/es.json b/homeassistant/components/ps4/translations/es.json index 4eb7636d0a2..3655b72110c 100644 --- a/homeassistant/components/ps4/translations/es.json +++ b/homeassistant/components/ps4/translations/es.json @@ -25,7 +25,7 @@ "region": "Regi\u00f3n" }, "data_description": { - "code": "Vaya a 'Configuraci\u00f3n' en su consola PlayStation 4. Luego navegue hasta 'Configuraci\u00f3n de conexi\u00f3n de la aplicaci\u00f3n m\u00f3vil' y seleccione 'Agregar dispositivo' para obtener el pin." + "code": "Ve a 'Configuraci\u00f3n' en tu consola PlayStation 4. Luego navega hasta 'Configuraci\u00f3n de conexi\u00f3n de la aplicaci\u00f3n m\u00f3vil' y selecciona 'Agregar dispositivo' para obtener el PIN." } }, "mode": { @@ -34,7 +34,7 @@ "mode": "Modo configuraci\u00f3n" }, "data_description": { - "ip_address": "D\u00e9jelo en blanco si selecciona la detecci\u00f3n autom\u00e1tica." + "ip_address": "D\u00e9jalo en blanco si seleccionas la detecci\u00f3n autom\u00e1tica." } } } diff --git a/homeassistant/components/qnap_qsw/translations/es.json b/homeassistant/components/qnap_qsw/translations/es.json index b58fcb71fc7..e149b08b403 100644 --- a/homeassistant/components/qnap_qsw/translations/es.json +++ b/homeassistant/components/qnap_qsw/translations/es.json @@ -2,13 +2,19 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "invalid_id": "El dispositivo ha devuelto un ID \u00fanico inv\u00e1lido" + "invalid_id": "El dispositivo devolvi\u00f3 un ID \u00fanico no v\u00e1lido" }, "error": { - "cannot_connect": "Ha fallado la conexi\u00f3n", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { + "discovered_connection": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + } + }, "user": { "data": { "password": "Contrase\u00f1a", diff --git a/homeassistant/components/radiotherm/translations/es.json b/homeassistant/components/radiotherm/translations/es.json index dbb84376ff9..165068f38fa 100644 --- a/homeassistant/components/radiotherm/translations/es.json +++ b/homeassistant/components/radiotherm/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "flow_title": "{name} {model} ({host})", @@ -19,11 +19,17 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3n de la plataforma clim\u00e1tica Radio Thermostat mediante YAML se eliminar\u00e1 en Home Assistant 2022.9. \n\nTu configuraci\u00f3n existente se ha importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Radio Thermostat" + } + }, "options": { "step": { "init": { "data": { - "hold_temp": "Establezca una retenci\u00f3n permanente al ajustar la temperatura." + "hold_temp": "Establecer una retenci\u00f3n permanente al ajustar la temperatura." } } } diff --git a/homeassistant/components/radiotherm/translations/tr.json b/homeassistant/components/radiotherm/translations/tr.json index f8e6b4f7a6d..99e57c1bf6b 100644 --- a/homeassistant/components/radiotherm/translations/tr.json +++ b/homeassistant/components/radiotherm/translations/tr.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "deprecated_yaml": { + "description": "Radyo Termostat iklim platformunun YAML kullan\u0131larak yap\u0131land\u0131r\u0131lmas\u0131 Home Assistant 2022.9'da kald\u0131r\u0131l\u0131yor.\n\nMevcut yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131lm\u0131\u015ft\u0131r. YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Radyo Termostat\u0131 YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/remote/translations/zh-Hant.json b/homeassistant/components/remote/translations/zh-Hant.json index 259121d7a4f..a10eb3b8f61 100644 --- a/homeassistant/components/remote/translations/zh-Hant.json +++ b/homeassistant/components/remote/translations/zh-Hant.json @@ -18,7 +18,7 @@ "state": { "_": { "off": "\u95dc\u9589", - "on": "\u958b\u5553" + "on": "\u958b\u555f" } }, "title": "\u9059\u63a7\u5668" diff --git a/homeassistant/components/rhasspy/translations/es.json b/homeassistant/components/rhasspy/translations/es.json new file mode 100644 index 00000000000..5510c9e38f4 --- /dev/null +++ b/homeassistant/components/rhasspy/translations/es.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "step": { + "user": { + "description": "\u00bfQuieres habilitar el soporte de Rhasspy?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/translations/es.json b/homeassistant/components/ring/translations/es.json index d98a61474cc..4a3947c5efc 100644 --- a/homeassistant/components/ring/translations/es.json +++ b/homeassistant/components/ring/translations/es.json @@ -17,7 +17,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Iniciar sesi\u00f3n con cuenta de Ring" } diff --git a/homeassistant/components/roku/translations/es.json b/homeassistant/components/roku/translations/es.json index 817f1d970cc..015360d76c9 100644 --- a/homeassistant/components/roku/translations/es.json +++ b/homeassistant/components/roku/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/roon/translations/es.json b/homeassistant/components/roon/translations/es.json index 098ba60234a..d63856b8f8d 100644 --- a/homeassistant/components/roon/translations/es.json +++ b/homeassistant/components/roon/translations/es.json @@ -13,7 +13,7 @@ "host": "Host", "port": "Puerto" }, - "description": "No se ha podrido descubrir el servidor Roon, introduce el anfitri\u00f3n y el puerto." + "description": "No se pudo descubrir el servidor Roon, por favor, introduce su nombre de host y el puerto." }, "link": { "description": "Debes autorizar Home Assistant en Roon. Despu\u00e9s de pulsar en Enviar, ve a la aplicaci\u00f3n Roon Core, abre Configuraci\u00f3n y activa HomeAssistant en la pesta\u00f1a Extensiones.", diff --git a/homeassistant/components/ruckus_unleashed/translations/es.json b/homeassistant/components/ruckus_unleashed/translations/es.json index 2609ee07eaf..c1d3e57b02f 100644 --- a/homeassistant/components/ruckus_unleashed/translations/es.json +++ b/homeassistant/components/ruckus_unleashed/translations/es.json @@ -13,7 +13,7 @@ "data": { "host": "Host", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/sabnzbd/translations/es.json b/homeassistant/components/sabnzbd/translations/es.json index f38e28a3287..c3e690941bb 100644 --- a/homeassistant/components/sabnzbd/translations/es.json +++ b/homeassistant/components/sabnzbd/translations/es.json @@ -1,8 +1,8 @@ { "config": { "error": { - "cannot_connect": "Ha fallado la conexi\u00f3n", - "invalid_api_key": "Clave API inv\u00e1lida" + "cannot_connect": "No se pudo conectar", + "invalid_api_key": "Clave API no v\u00e1lida" }, "step": { "user": { diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json index 29b2a97027d..ebcaa398949 100644 --- a/homeassistant/components/samsungtv/translations/es.json +++ b/homeassistant/components/samsungtv/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este televisor Samsung. Revisa la configuraci\u00f3n de tu televisor para autorizar a Home Assistant.", "cannot_connect": "No se pudo conectar", "id_missing": "Este dispositivo Samsung no tiene un n\u00famero de serie.", diff --git a/homeassistant/components/scrape/translations/es.json b/homeassistant/components/scrape/translations/es.json index 660d687344c..f5c07aa30b4 100644 --- a/homeassistant/components/scrape/translations/es.json +++ b/homeassistant/components/scrape/translations/es.json @@ -1,10 +1,68 @@ { + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada" + }, + "step": { + "user": { + "data": { + "attribute": "Atributo", + "authentication": "Autenticaci\u00f3n", + "device_class": "Clase de dispositivo", + "headers": "Cabeceras", + "index": "\u00cdndice", + "name": "Nombre", + "password": "Contrase\u00f1a", + "resource": "Recurso", + "select": "Seleccionar", + "state_class": "Clase de estado", + "unit_of_measurement": "Unidad de medida", + "username": "Nombre de usuario", + "value_template": "Plantilla de valor", + "verify_ssl": "Verificar el certificado SSL" + }, + "data_description": { + "attribute": "Obtener el valor de un atributo en la etiqueta seleccionada", + "authentication": "Tipo de autenticaci\u00f3n HTTP. Puede ser basic o digest", + "device_class": "El tipo/clase del sensor para establecer el icono en el frontend", + "headers": "Cabeceras a utilizar para la petici\u00f3n web", + "index": "Define cu\u00e1l de los elementos devueltos por el selector CSS usar", + "resource": "La URL del sitio web que contiene el valor.", + "select": "Define qu\u00e9 etiqueta buscar. Consulta los selectores CSS de Beautifulsoup para obtener m\u00e1s informaci\u00f3n.", + "state_class": "El state_class del sensor", + "value_template": "Define una plantilla para obtener el estado del sensor", + "verify_ssl": "Habilita/deshabilita la verificaci\u00f3n del certificado SSL/TLS, por ejemplo, si est\u00e1 autofirmado" + } + } + } + }, "options": { "step": { "init": { + "data": { + "attribute": "Atributo", + "authentication": "Autenticaci\u00f3n", + "device_class": "Clase de dispositivo", + "headers": "Cabeceras", + "index": "\u00cdndice", + "name": "Nombre", + "password": "Contrase\u00f1a", + "resource": "Recurso", + "select": "Seleccionar", + "state_class": "Clase de estado", + "unit_of_measurement": "Unidad de medida", + "username": "Nombre de usuario", + "value_template": "Plantilla de valor", + "verify_ssl": "Verificar el certificado SSL" + }, "data_description": { + "attribute": "Obtener el valor de un atributo en la etiqueta seleccionada", + "authentication": "Tipo de autenticaci\u00f3n HTTP. Puede ser basic o digest", + "device_class": "El tipo/clase del sensor para establecer el icono en el frontend", + "headers": "Cabeceras a utilizar para la petici\u00f3n web", + "index": "Define cu\u00e1l de los elementos devueltos por el selector CSS usar", "resource": "La URL del sitio web que contiene el valor.", - "select": "Define qu\u00e9 etiqueta buscar. Consulte los selectores de CSS de Beautifulsoup para obtener m\u00e1s informaci\u00f3n.", + "select": "Define qu\u00e9 etiqueta buscar. Consulta los selectores CSS de Beautifulsoup para obtener m\u00e1s informaci\u00f3n.", "state_class": "El state_class del sensor", "value_template": "Define una plantilla para obtener el estado del sensor", "verify_ssl": "Habilita/deshabilita la verificaci\u00f3n del certificado SSL/TLS, por ejemplo, si est\u00e1 autofirmado" diff --git a/homeassistant/components/script/translations/zh-Hant.json b/homeassistant/components/script/translations/zh-Hant.json index 4840b6f8ab0..9e117e082d6 100644 --- a/homeassistant/components/script/translations/zh-Hant.json +++ b/homeassistant/components/script/translations/zh-Hant.json @@ -2,7 +2,7 @@ "state": { "_": { "off": "\u95dc\u9589", - "on": "\u958b\u5553" + "on": "\u958b\u555f" } }, "title": "\u8173\u672c" diff --git a/homeassistant/components/sensor/translations/zh-Hant.json b/homeassistant/components/sensor/translations/zh-Hant.json index 1dcb63d4052..344f9d6119e 100644 --- a/homeassistant/components/sensor/translations/zh-Hant.json +++ b/homeassistant/components/sensor/translations/zh-Hant.json @@ -62,7 +62,7 @@ "state": { "_": { "off": "\u95dc\u9589", - "on": "\u958b\u5553" + "on": "\u958b\u555f" } }, "title": "\u611f\u6e2c\u5668" diff --git a/homeassistant/components/sensorpush/translations/es.json b/homeassistant/components/sensorpush/translations/es.json new file mode 100644 index 00000000000..76fb203eacd --- /dev/null +++ b/homeassistant/components/sensorpush/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/tr.json b/homeassistant/components/sensorpush/translations/tr.json new file mode 100644 index 00000000000..f63cee3493c --- /dev/null +++ b/homeassistant/components/sensorpush/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/es.json b/homeassistant/components/senz/translations/es.json index f81af28f4d9..fc1c1ae2d13 100644 --- a/homeassistant/components/senz/translations/es.json +++ b/homeassistant/components/senz/translations/es.json @@ -2,19 +2,25 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", - "no_url_available": "No hay ninguna URL disponible. Para m\u00e1s informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", - "oauth_error": "Se han recibido datos token inv\u00e1lidos." + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "oauth_error": "Se han recibido datos de token no v\u00e1lidos." }, "create_entry": { - "default": "Autenticaci\u00f3n exitosa" + "default": "Autenticado correctamente" }, "step": { "pick_implementation": { "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" } } + }, + "issues": { + "removed_yaml": { + "description": "Se elimin\u00f3 la configuraci\u00f3n de nVent RAYCHEM SENZ mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se elimin\u00f3 la configuraci\u00f3n YAML de nVent RAYCHEM SENZ" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/et.json b/homeassistant/components/senz/translations/et.json index 1ea0537a1b7..7424b6e2967 100644 --- a/homeassistant/components/senz/translations/et.json +++ b/homeassistant/components/senz/translations/et.json @@ -16,5 +16,11 @@ "title": "Vali tuvastusmeetod" } } + }, + "issues": { + "removed_yaml": { + "description": "nVent RAYCHEM SENZi konfigureerimine YAMLi abil on eemaldatud.\n\nTeie olemasolevat YAML-konfiguratsiooni ei kasuta Home Assistant.\n\nProbleemi lahendamiseks eemaldage YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivitage Home Assistant uuesti.", + "title": "NVent RAYCHEM SENZ YAML konfiguratsioon on eemaldatud" + } } } \ No newline at end of file diff --git a/homeassistant/components/senz/translations/tr.json b/homeassistant/components/senz/translations/tr.json index 3f6fa6f27ba..a9f7ea9b718 100644 --- a/homeassistant/components/senz/translations/tr.json +++ b/homeassistant/components/senz/translations/tr.json @@ -16,5 +16,11 @@ "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" } } + }, + "issues": { + "removed_yaml": { + "description": "nVent RAYCHEM SENZ'in YAML kullan\u0131larak yap\u0131land\u0131r\u0131lmas\u0131 kald\u0131r\u0131ld\u0131.\n\nMevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lmaz.\n\nYAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "nVent RAYCHEM SENZ YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" + } } } \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/es.json b/homeassistant/components/sharkiq/translations/es.json index 951537484bc..976840e0a9f 100644 --- a/homeassistant/components/sharkiq/translations/es.json +++ b/homeassistant/components/sharkiq/translations/es.json @@ -15,13 +15,13 @@ "reauth": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } }, "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index 0c0011f7297..876d64093a0 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/translations/es.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "firmware_not_fully_provisioned": "El dispositivo no est\u00e1 completamente aprovisionado. P\u00f3ngase en contacto con el servicio de asistencia de Shelly", + "firmware_not_fully_provisioned": "El dispositivo no est\u00e1 completamente aprovisionado. Por favor, ponte en contacto con el servicio de asistencia de Shelly", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -18,7 +18,7 @@ "credentials": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } }, "user": { diff --git a/homeassistant/components/simplepush/translations/ca.json b/homeassistant/components/simplepush/translations/ca.json index 8755c00a076..4252be0764e 100644 --- a/homeassistant/components/simplepush/translations/ca.json +++ b/homeassistant/components/simplepush/translations/ca.json @@ -24,6 +24,7 @@ "title": "La configuraci\u00f3 YAML de Simplepush est\u00e0 sent eliminada" }, "removed_yaml": { + "description": "La configuraci\u00f3 de Simplepush mitjan\u00e7ant YAML s'ha eliminat de Home Assistant.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML existent.\n\nElimina la configuraci\u00f3 YAML de Simplepush del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", "title": "La configuraci\u00f3 YAML de Simplepush s'ha eliminat" } } diff --git a/homeassistant/components/simplepush/translations/el.json b/homeassistant/components/simplepush/translations/el.json index 8f8c4691045..13a961c6aff 100644 --- a/homeassistant/components/simplepush/translations/el.json +++ b/homeassistant/components/simplepush/translations/el.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + }, + "removed_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03bf\u03c5 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03b8\u03b7\u03ba\u03b5. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b4\u03b5\u03bd \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Simplepush YAML \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/es.json b/homeassistant/components/simplepush/translations/es.json new file mode 100644 index 00000000000..acf2da9e5d2 --- /dev/null +++ b/homeassistant/components/simplepush/translations/es.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "device_key": "La clave de dispositivo de tu dispositivo", + "event": "El evento para los eventos.", + "name": "Nombre", + "password": "La contrase\u00f1a de cifrado utilizada por tu dispositivo", + "salt": "La semilla utilizada por tu dispositivo." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de Simplepush mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Simplepush de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Simplepush" + }, + "removed_yaml": { + "description": "Se ha eliminado la configuraci\u00f3n de Simplepush mediante YAML.\n\nTu configuraci\u00f3n YAML existente no es utilizada por Home Assistant.\n\nElimina la configuraci\u00f3n YAML de Simplepush de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se ha eliminado la configuraci\u00f3n YAML de Simplepush" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/et.json b/homeassistant/components/simplepush/translations/et.json index 2501d992c83..7cae6c69edf 100644 --- a/homeassistant/components/simplepush/translations/et.json +++ b/homeassistant/components/simplepush/translations/et.json @@ -17,5 +17,15 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Simplepushi konfigureerimine YAML-i abil eemaldatakse.\n\nTeie olemasolev YAML-konfiguratsioon on automaatselt kasutajaliidesesse imporditud.\n\nEemaldage Simplepushi YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivitage Home Assistant uuesti, et see probleem lahendada.", + "title": "Simplepush YAML-i konfiguratsioon eemaldatakse" + }, + "removed_yaml": { + "description": "Simplepushi konfigureerimine YAMLi abil on eemaldatud.\n\nTeie olemasolevat YAML-konfiguratsiooni ei kasuta Home Assistant.\n\nEemaldage Simplepushi YAML-konfiguratsioon oma configuration.yaml-failist ja k\u00e4ivitage Home Assistant uuesti, et see probleem lahendada.", + "title": "Simplepush YAML-i konfiguratsioon on eemaldatud" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/tr.json b/homeassistant/components/simplepush/translations/tr.json index 0c969465da9..0a3183d7c90 100644 --- a/homeassistant/components/simplepush/translations/tr.json +++ b/homeassistant/components/simplepush/translations/tr.json @@ -17,5 +17,15 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Simplepush'un YAML kullan\u0131larak yap\u0131land\u0131r\u0131lmas\u0131 kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. \n\n Simplepush YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Simplepush YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + }, + "removed_yaml": { + "description": "Simplepush'u YAML kullanarak yap\u0131land\u0131rma kald\u0131r\u0131ld\u0131. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lm\u0131yor. \n\n Simplepush YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Simplepush YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/el.json b/homeassistant/components/simplisafe/translations/el.json index ecc263e6821..d9c9123b391 100644 --- a/homeassistant/components/simplisafe/translations/el.json +++ b/homeassistant/components/simplisafe/translations/el.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "\u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ae\u03b4\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "invalid_auth_code_length": "\u039f\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03af \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 SimpliSafe \u03ad\u03c7\u03bf\u03c5\u03bd \u03bc\u03ae\u03ba\u03bf\u03c2 45 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03b5\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "progress": { diff --git a/homeassistant/components/simplisafe/translations/es.json b/homeassistant/components/simplisafe/translations/es.json index 25d8de8bbb3..db73c6ce042 100644 --- a/homeassistant/components/simplisafe/translations/es.json +++ b/homeassistant/components/simplisafe/translations/es.json @@ -2,15 +2,18 @@ "config": { "abort": { "already_configured": "Esta cuenta SimpliSafe ya est\u00e1 en uso.", - "email_2fa_timed_out": "Se ha agotado el tiempo de espera de la autenticaci\u00f3n de dos factores basada en el correo electr\u00f3nico.", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "email_2fa_timed_out": "Se agot\u00f3 el tiempo de espera para la autenticaci\u00f3n de dos factores basada en correo electr\u00f3nico.", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "wrong_account": "Las credenciales de usuario proporcionadas no coinciden con esta cuenta de SimpliSafe." }, "error": { + "identifier_exists": "Cuenta ya registrada", "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth_code_length": "Los c\u00f3digos de autorizaci\u00f3n de SimpliSafe tienen 45 caracteres de longitud", "unknown": "Error inesperado" }, "progress": { - "email_2fa": "Mira el correo electr\u00f3nico donde deber\u00edas encontrar el enlace de verificaci\u00f3n de Simplisafe." + "email_2fa": "Revisa tu correo electr\u00f3nico para obtener un enlace de verificaci\u00f3n de Simplisafe." }, "step": { "reauth_confirm": { @@ -24,14 +27,15 @@ "data": { "code": "C\u00f3digo" }, - "description": "Introduce el c\u00f3digo de autenticaci\u00f3n de dos factores enviado por SMS." + "description": "Introduce el c\u00f3digo de autenticaci\u00f3n de dos factores que se te envi\u00f3 por SMS." }, "user": { "data": { + "auth_code": "C\u00f3digo de Autorizaci\u00f3n", "password": "Contrase\u00f1a", "username": "Correo electr\u00f3nico" }, - "description": "SimpliSafe se autentifica con Home Assistant a trav\u00e9s de la aplicaci\u00f3n web de SimpliSafe. Debido a limitaciones t\u00e9cnicas, hay un paso manual al final de este proceso; aseg\u00farese de leer la [documentaci\u00f3n]({docs_url}) antes de empezar.\n\n1. Haga clic en [aqu\u00ed]({url}) para abrir la aplicaci\u00f3n web de SimpliSafe e introduzca sus credenciales.\n\n2. Cuando el proceso de inicio de sesi\u00f3n haya finalizado, vuelva aqu\u00ed e introduzca el c\u00f3digo de autorizaci\u00f3n que aparece a continuaci\u00f3n." + "description": "SimpliSafe autentica a los usuarios a trav\u00e9s de su aplicaci\u00f3n web. Debido a limitaciones t\u00e9cnicas, existe un paso manual al final de este proceso; aseg\u00farate de leer la [documentaci\u00f3n](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de comenzar. \n\nCuando est\u00e9s listo, haz clic [aqu\u00ed]({url}) para abrir la aplicaci\u00f3n web SimpliSafe e introduce tus credenciales. Si ya iniciaste sesi\u00f3n en SimpliSafe en tu navegador, es posible que desees abrir una nueva pesta\u00f1a y luego copiar/pegar la URL anterior en esa pesta\u00f1a. \n\n Cuando se complete el proceso, regresa aqu\u00ed e introduce el c\u00f3digo de autorizaci\u00f3n de la URL `com.simplisafe.mobile`." } } }, diff --git a/homeassistant/components/simplisafe/translations/et.json b/homeassistant/components/simplisafe/translations/et.json index 073d20555c1..03356fc7d6e 100644 --- a/homeassistant/components/simplisafe/translations/et.json +++ b/homeassistant/components/simplisafe/translations/et.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "Konto on juba registreeritud", "invalid_auth": "Tuvastamise viga", + "invalid_auth_code_length": "SimpliSafe autoriseerimiskoodid on 45 t\u00e4hem\u00e4rki pikad.", "unknown": "Tundmatu viga" }, "progress": { @@ -34,7 +35,7 @@ "password": "Salas\u00f5na", "username": "Kasutajanimi" }, - "description": "SimpliSafe autendib kasutajaid oma veebirakenduse kaudu. Tehniliste piirangute t\u00f5ttu on selle protsessi l\u00f5pus k\u00e4sitsi samm; palun veendu, et loed enne alustamist l\u00e4bi [dokumendid](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code).\n\nKui oled valmis, kl\u00f5psa veebirakenduse SimpliSafe avamiseks ja mandaadi sisestamiseks kl\u00f5psa [here]({url}). Kui protsess on l\u00f5pule j\u00f5udnud, naase siia ja sisesta autoriseerimiskood SimpliSafe veebirakenduse URL-ist." + "description": "SimpliSafe autentib kasutajad veebirakenduse kaudu. Tehniliste piirangute t\u00f5ttu on selle protsessi l\u00f5pus manuaalne samm; palun lugege enne alustamist kindlasti [dokumentatsiooni](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code).\n\nKui olete valmis, kl\u00f5psake [siin]({url}), et avada SimpliSafe veebirakendus ja sisestada oma volitused. Kui olete juba SimpliSafe'ile oma brauseris sisse loginud, siis avage uus vahekaart ja kopeerige/liidke \u00fclaltoodud URL sellesse vahekaarti.\n\nKui protsess on l\u00f5pule viidud, naaske siia ja sisestage autoriseerimiskood `com.simplisafe.mobile` URL-i." } } }, diff --git a/homeassistant/components/simplisafe/translations/tr.json b/homeassistant/components/simplisafe/translations/tr.json index 74c260e2d3d..02153b0b90c 100644 --- a/homeassistant/components/simplisafe/translations/tr.json +++ b/homeassistant/components/simplisafe/translations/tr.json @@ -3,10 +3,13 @@ "abort": { "already_configured": "Bu SimpliSafe hesab\u0131 zaten kullan\u0131mda.", "email_2fa_timed_out": "E-posta tabanl\u0131 iki fakt\u00f6rl\u00fc kimlik do\u011frulama i\u00e7in beklerken zaman a\u015f\u0131m\u0131na u\u011frad\u0131.", - "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "wrong_account": "Sa\u011flanan kullan\u0131c\u0131 kimlik bilgileri bu SimpliSafe hesab\u0131yla e\u015fle\u015fmiyor." }, "error": { + "identifier_exists": "Hesap zaten kay\u0131tl\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_auth_code_length": "SimpliSafe yetkilendirme kodlar\u0131 45 karakter uzunlu\u011fundad\u0131r", "unknown": "Beklenmeyen hata" }, "progress": { @@ -28,10 +31,11 @@ }, "user": { "data": { + "auth_code": "Yetkilendirme Kodu", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "Kullan\u0131c\u0131 ad\u0131n\u0131z\u0131 ve \u015fifrenizi girin." + "description": "SimpliSafe, web uygulamas\u0131 arac\u0131l\u0131\u011f\u0131yla kullan\u0131c\u0131lar\u0131n kimli\u011fini do\u011frular. Teknik s\u0131n\u0131rlamalar nedeniyle bu i\u015flemin sonunda manuel bir ad\u0131m vard\u0131r; l\u00fctfen ba\u015flamadan \u00f6nce [belgeleri](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) okudu\u011funuzdan emin olun. \n\n Haz\u0131r oldu\u011funuzda SimpliSafe web uygulamas\u0131n\u0131 a\u00e7mak ve kimlik bilgilerinizi girmek i\u00e7in [buray\u0131]( {url} ) t\u0131klay\u0131n. Taray\u0131c\u0131n\u0131zda SimpliSafe'e zaten giri\u015f yapt\u0131ysan\u0131z, yeni bir sekme a\u00e7mak ve ard\u0131ndan yukar\u0131daki URL'yi o sekmeye kopyalay\u0131p/yap\u0131\u015ft\u0131rmak isteyebilirsiniz. \n\n \u0130\u015flem tamamland\u0131\u011f\u0131nda buraya d\u00f6n\u00fcn ve \"com.simplisafe.mobile\" URL'sinden yetkilendirme kodunu girin." } } }, diff --git a/homeassistant/components/skybell/translations/es.json b/homeassistant/components/skybell/translations/es.json index cc93b536c38..68633c72180 100644 --- a/homeassistant/components/skybell/translations/es.json +++ b/homeassistant/components/skybell/translations/es.json @@ -2,17 +2,17 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { "user": { "data": { - "email": "Correo electronico", + "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a" } } diff --git a/homeassistant/components/slack/translations/es.json b/homeassistant/components/slack/translations/es.json index 52aee38b4f9..e4680a3e7ae 100644 --- a/homeassistant/components/slack/translations/es.json +++ b/homeassistant/components/slack/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -19,10 +19,10 @@ "data_description": { "api_key": "El token de la API de Slack que se usar\u00e1 para enviar mensajes de Slack.", "default_channel": "Canal al que publicar en caso de que no se especifique ning\u00fan canal al enviar un mensaje.", - "icon": "Utilice uno de los emojis de Slack como icono para el nombre de usuario proporcionado.", + "icon": "Utiliza uno de los emojis de Slack como icono para el nombre de usuario proporcionado.", "username": "Home Assistant publicar\u00e1 en Slack con el nombre de usuario especificado." }, - "description": "Consulte la documentaci\u00f3n sobre c\u00f3mo obtener su clave API de Slack." + "description": "Consulta la documentaci\u00f3n sobre c\u00f3mo obtener tu clave API de Slack." } } } diff --git a/homeassistant/components/slimproto/translations/es.json b/homeassistant/components/slimproto/translations/es.json index 352f82f605f..ac3c9b2bb41 100644 --- a/homeassistant/components/slimproto/translations/es.json +++ b/homeassistant/components/slimproto/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Ya configurado. S\u00f3lo es posible una sola configuraci\u00f3n." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." } } } \ No newline at end of file diff --git a/homeassistant/components/sma/translations/es.json b/homeassistant/components/sma/translations/es.json index 76edc25241c..216745647fb 100644 --- a/homeassistant/components/sma/translations/es.json +++ b/homeassistant/components/sma/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso" + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso" }, "error": { "cannot_connect": "No se pudo conectar", @@ -17,7 +17,7 @@ "host": "Host", "password": "Contrase\u00f1a", "ssl": "Utiliza un certificado SSL", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar el certificado SSL" }, "description": "Introduce la informaci\u00f3n de tu dispositivo SMA.", "title": "Configurar SMA Solar" diff --git a/homeassistant/components/smart_meter_texas/translations/es.json b/homeassistant/components/smart_meter_texas/translations/es.json index d537185eb68..942024cf167 100644 --- a/homeassistant/components/smart_meter_texas/translations/es.json +++ b/homeassistant/components/smart_meter_texas/translations/es.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/sms/translations/es.json b/homeassistant/components/sms/translations/es.json index 27669a2b52f..660fb0db4fa 100644 --- a/homeassistant/components/sms/translations/es.json +++ b/homeassistant/components/sms/translations/es.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "baud_speed": "Velocidad en Baudis", + "baud_speed": "Velocidad en baudios", "device": "Dispositivo" }, "title": "Conectar con el m\u00f3dem" diff --git a/homeassistant/components/sonarr/translations/es.json b/homeassistant/components/sonarr/translations/es.json index 3c1b7c0fa9a..440ab7bf21f 100644 --- a/homeassistant/components/sonarr/translations/es.json +++ b/homeassistant/components/sonarr/translations/es.json @@ -19,7 +19,7 @@ "data": { "api_key": "Clave API", "url": "URL", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar el certificado SSL" } } } diff --git a/homeassistant/components/soundtouch/translations/es.json b/homeassistant/components/soundtouch/translations/es.json new file mode 100644 index 00000000000..0ee2968bf09 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + }, + "zeroconf_confirm": { + "description": "Est\u00e1s a punto de a\u00f1adir el dispositivo SoundTouch llamado `{name}` a Home Assistant.", + "title": "Confirma la adici\u00f3n del dispositivo Bose SoundTouch" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de Bose SoundTouch mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimine la configuraci\u00f3n YAML de Bose SoundTouch de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Bose SoundTouch" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/et.json b/homeassistant/components/soundtouch/translations/et.json index 0adb9ddba8b..fdbc2b9090f 100644 --- a/homeassistant/components/soundtouch/translations/et.json +++ b/homeassistant/components/soundtouch/translations/et.json @@ -17,5 +17,11 @@ "title": "Kinnita Bose SoundTouchi seadme lisamine" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Bose SoundTouchi seadistamine YAML-i abil eemaldatakse.\n\nTeie olemasolev YAML-i konfiguratsioon imporditakse kasutajaliidesesse automaatselt.\n\nEemaldage bose SoundTouch YAML-i konfiguratsioon failist configuration.yaml ja taask\u00e4ivitage selle probleemi lahendamiseks Home Assistant.", + "title": "Bose SoundTouchi YAML-konfiguratsioon eemaldatakse" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/tr.json b/homeassistant/components/soundtouch/translations/tr.json index 957215dd8b3..a3a176e58d8 100644 --- a/homeassistant/components/soundtouch/translations/tr.json +++ b/homeassistant/components/soundtouch/translations/tr.json @@ -17,5 +17,11 @@ "title": "Bose SoundTouch cihaz\u0131 eklemeyi onaylay\u0131n" } } + }, + "issues": { + "deprecated_yaml": { + "description": "YAML kullanarak Bose SoundTouch'\u0131 yap\u0131land\u0131rmak kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. \n\n Bu sorunu \u00e7\u00f6zmek i\u00e7in Bose SoundTouch YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Bose SoundTouch YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } } } \ No newline at end of file diff --git a/homeassistant/components/spider/translations/es.json b/homeassistant/components/spider/translations/es.json index c538d4fa5b5..12fe8db0e73 100644 --- a/homeassistant/components/spider/translations/es.json +++ b/homeassistant/components/spider/translations/es.json @@ -11,7 +11,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Iniciar sesi\u00f3n con la cuenta de mijn.ithodaalderop.nl" } diff --git a/homeassistant/components/spotify/translations/es.json b/homeassistant/components/spotify/translations/es.json index ce1966a8edd..09de377d195 100644 --- a/homeassistant/components/spotify/translations/es.json +++ b/homeassistant/components/spotify/translations/es.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Se elimin\u00f3 la configuraci\u00f3n de Spotify usando YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se ha eliminado la configuraci\u00f3n YAML de Spotify" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Se puede acceder al punto de conexi\u00f3n de la API de Spotify" diff --git a/homeassistant/components/spotify/translations/et.json b/homeassistant/components/spotify/translations/et.json index c5cee44acca..5365a66df1c 100644 --- a/homeassistant/components/spotify/translations/et.json +++ b/homeassistant/components/spotify/translations/et.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Spotify seadistamine YAML-i abil on eemaldatud.\n\nHome Assistant ei kasuta teie olemasolevat YAML-i konfiguratsiooni.\n\nEemaldage FAILIST CONFIGURATION.yaml YAML-konfiguratsioon ja taask\u00e4ivitage selle probleemi lahendamiseks Home Assistant.", + "title": "Spotify YAML konfiguratsioon on eemaldatud" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Spotify API l\u00f5pp-punkt on k\u00e4ttesaadav" diff --git a/homeassistant/components/spotify/translations/tr.json b/homeassistant/components/spotify/translations/tr.json index 32c21899d52..ba52631e96d 100644 --- a/homeassistant/components/spotify/translations/tr.json +++ b/homeassistant/components/spotify/translations/tr.json @@ -19,6 +19,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Spotify'\u0131 YAML kullanarak yap\u0131land\u0131rma kald\u0131r\u0131ld\u0131.\n\nMevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lmaz.\n\nYAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Spotify YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" + } + }, "system_health": { "info": { "api_endpoint_reachable": "Spotify API u\u00e7 noktas\u0131na ula\u015f\u0131labilir" diff --git a/homeassistant/components/sql/translations/es.json b/homeassistant/components/sql/translations/es.json index 6811fc498f9..322485ae446 100644 --- a/homeassistant/components/sql/translations/es.json +++ b/homeassistant/components/sql/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "La cuenta ya est\u00e1 configurada" }, "error": { - "db_url_invalid": "URL de la base de datos inv\u00e1lido", + "db_url_invalid": "URL de la base de datos no v\u00e1lida", "query_invalid": "Consulta SQL no v\u00e1lida" }, "step": { @@ -12,7 +12,7 @@ "data": { "column": "Columna", "db_url": "URL de la base de datos", - "name": "Nombre", + "name": "[%key:component::sql::config::step::user::data::name%]", "query": "Selecciona la consulta", "unit_of_measurement": "Unidad de medida", "value_template": "Plantilla de valor" @@ -20,7 +20,7 @@ "data_description": { "column": "Columna de respuesta de la consulta para presentar como estado", "db_url": "URL de la base de datos, d\u00e9jalo en blanco para utilizar la predeterminada de HA", - "name": "Nombre que se utilizar\u00e1 para la entrada de configuraci\u00f3n y tambi\u00e9n para el sensor", + "name": "Nombre que se usar\u00e1 para la entrada de configuraci\u00f3n y tambi\u00e9n para el sensor", "query": "Consulta a ejecutar, debe empezar por 'SELECT'", "unit_of_measurement": "Unidad de medida (opcional)", "value_template": "Plantilla de valor (opcional)" @@ -30,15 +30,15 @@ }, "options": { "error": { - "db_url_invalid": "URL de la base de datos inv\u00e1lido", - "query_invalid": "Consulta SQL inv\u00e1lida" + "db_url_invalid": "URL de la base de datos no v\u00e1lida", + "query_invalid": "Consulta SQL no v\u00e1lida" }, "step": { "init": { "data": { "column": "Columna", "db_url": "URL de la base de datos", - "name": "Nombre", + "name": "[%key:component::sql::config::step::user::data::name%]", "query": "Selecciona la consulta", "unit_of_measurement": "Unidad de medida", "value_template": "Plantilla de valor" @@ -46,7 +46,7 @@ "data_description": { "column": "Columna de respuesta de la consulta para presentar como estado", "db_url": "URL de la base de datos, d\u00e9jalo en blanco para utilizar la predeterminada de HA", - "name": "Nombre que se utilizar\u00e1 para la entrada de configuraci\u00f3n y tambi\u00e9n para el sensor", + "name": "Nombre que se usar\u00e1 para la entrada de configuraci\u00f3n y tambi\u00e9n para el sensor", "query": "Consulta a ejecutar, debe empezar por 'SELECT'", "unit_of_measurement": "Unidad de medida (opcional)", "value_template": "Plantilla de valor (opcional)" diff --git a/homeassistant/components/squeezebox/translations/es.json b/homeassistant/components/squeezebox/translations/es.json index 7bf247c2976..b6745920acd 100644 --- a/homeassistant/components/squeezebox/translations/es.json +++ b/homeassistant/components/squeezebox/translations/es.json @@ -17,7 +17,7 @@ "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Editar la informaci\u00f3n de conexi\u00f3n" }, diff --git a/homeassistant/components/srp_energy/translations/es.json b/homeassistant/components/srp_energy/translations/es.json index ebd4583fe43..b82a4e6f6f6 100644 --- a/homeassistant/components/srp_energy/translations/es.json +++ b/homeassistant/components/srp_energy/translations/es.json @@ -15,7 +15,7 @@ "id": "ID de la cuenta", "is_tou": "Es el plan de tiempo de uso", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/starline/translations/es.json b/homeassistant/components/starline/translations/es.json index 6df1cdf8f01..eff1da9773a 100644 --- a/homeassistant/components/starline/translations/es.json +++ b/homeassistant/components/starline/translations/es.json @@ -31,7 +31,7 @@ "auth_user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Correo electr\u00f3nico y contrase\u00f1a de la cuenta StarLine", "title": "Credenciales de usuario" diff --git a/homeassistant/components/steam_online/translations/es.json b/homeassistant/components/steam_online/translations/es.json index 26ee994acde..7aeca102cc4 100644 --- a/homeassistant/components/steam_online/translations/es.json +++ b/homeassistant/components/steam_online/translations/es.json @@ -2,17 +2,17 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", - "invalid_account": "ID de la cuenta inv\u00e1lida", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "cannot_connect": "No se pudo conectar", + "invalid_account": "ID de cuenta inv\u00e1lido", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { "reauth_confirm": { - "description": "La integraci\u00f3n de Steam debe volver a autenticarse manualmente \n\n Puede encontrar su clave aqu\u00ed: {api_key_url}", + "description": "La integraci\u00f3n de Steam debe volver a autenticarse manualmente \n\nPuedes encontrar tu clave aqu\u00ed: {api_key_url}", "title": "Volver a autenticar la integraci\u00f3n" }, "user": { @@ -24,14 +24,20 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Se elimin\u00f3 la configuraci\u00f3n de Steam usando YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se ha eliminado la configuraci\u00f3n YAML de Steam" + } + }, "options": { "error": { - "unauthorized": "Lista de amigos restringida: Por favor, consulte la documentaci\u00f3n sobre c\u00f3mo ver a todos los dem\u00e1s amigos" + "unauthorized": "Lista de amigos restringida: Por favor, consulta la documentaci\u00f3n sobre c\u00f3mo ver a todos los dem\u00e1s amigos" }, "step": { "init": { "data": { - "accounts": "Nombres de las cuentas a controlar" + "accounts": "Nombres de las cuentas a supervisar" } } } diff --git a/homeassistant/components/steam_online/translations/et.json b/homeassistant/components/steam_online/translations/et.json index 62e38eb4088..b33e994f3e8 100644 --- a/homeassistant/components/steam_online/translations/et.json +++ b/homeassistant/components/steam_online/translations/et.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Steami seadistamine YAML-i abil on eemaldatud.\n\nHome Assistant ei kasuta teie olemasolevat YAML-i konfiguratsiooni.\n\nEemaldage FAILIST CONFIGURATION.yaml YAML-konfiguratsioon ja taask\u00e4ivitage selle probleemi lahendamiseks Home Assistant.", + "title": "Steam YAML konfiguratsioon on eemaldatud" + } + }, "options": { "error": { "unauthorized": "S\u00f5prade nimekiri on piiratud: vaata dokumentatsiooni kuidas n\u00e4ha k\u00f5iki teisi s\u00f5pru" diff --git a/homeassistant/components/steam_online/translations/tr.json b/homeassistant/components/steam_online/translations/tr.json index a1717857080..eabdabc24a0 100644 --- a/homeassistant/components/steam_online/translations/tr.json +++ b/homeassistant/components/steam_online/translations/tr.json @@ -24,6 +24,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Steam'i YAML kullanarak yap\u0131land\u0131rma kald\u0131r\u0131ld\u0131.\n\nMevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lmaz.\n\nYAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Steam YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" + } + }, "options": { "error": { "unauthorized": "Arkada\u015f listesi k\u0131s\u0131tland\u0131: L\u00fctfen di\u011fer t\u00fcm arkada\u015flar\u0131 nas\u0131l g\u00f6rece\u011finizle ilgili belgelere bak\u0131n" diff --git a/homeassistant/components/subaru/translations/es.json b/homeassistant/components/subaru/translations/es.json index 7b3a32c0213..fe02d508241 100644 --- a/homeassistant/components/subaru/translations/es.json +++ b/homeassistant/components/subaru/translations/es.json @@ -25,21 +25,21 @@ "data": { "contact_method": "Selecciona un m\u00e9todo de contacto:" }, - "description": "Autenticaci\u00f3n de dos factores requerida", + "description": "Se requiere autenticaci\u00f3n de dos factores", "title": "Configuraci\u00f3n de Subaru Starlink" }, "two_factor_validate": { "data": { "validation_code": "C\u00f3digo de validaci\u00f3n" }, - "description": "Introduce el c\u00f3digo de validaci\u00f3n recibido", + "description": "Por favor, introduce el c\u00f3digo de validaci\u00f3n recibido", "title": "Configuraci\u00f3n de Subaru Starlink" }, "user": { "data": { "country": "Seleccionar pa\u00eds", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Por favor, introduzca sus credenciales de MySubaru\nNOTA: La configuraci\u00f3n inicial puede tardar hasta 30 segundos", "title": "Configuraci\u00f3n de Subaru Starlink" diff --git a/homeassistant/components/surepetcare/translations/es.json b/homeassistant/components/surepetcare/translations/es.json index 13f2eb38bef..f92417d76a0 100644 --- a/homeassistant/components/surepetcare/translations/es.json +++ b/homeassistant/components/surepetcare/translations/es.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/switch/translations/zh-Hant.json b/homeassistant/components/switch/translations/zh-Hant.json index a1f38544f67..c8c6f688cab 100644 --- a/homeassistant/components/switch/translations/zh-Hant.json +++ b/homeassistant/components/switch/translations/zh-Hant.json @@ -18,7 +18,7 @@ "state": { "_": { "off": "\u95dc\u9589", - "on": "\u958b\u5553" + "on": "\u958b\u555f" } }, "title": "\u958b\u95dc" diff --git a/homeassistant/components/switch_as_x/translations/es.json b/homeassistant/components/switch_as_x/translations/es.json index ad0f9f52bbf..0228d86d0fe 100644 --- a/homeassistant/components/switch_as_x/translations/es.json +++ b/homeassistant/components/switch_as_x/translations/es.json @@ -6,7 +6,7 @@ "entity_id": "Conmutador", "target_domain": "Nuevo tipo" }, - "description": "Elija un interruptor que desee que aparezca en Home Assistant como luz, cubierta o cualquier otra cosa. El interruptor original se ocultar\u00e1." + "description": "Elige un interruptor que desees que aparezca en Home Assistant como luz, cubierta o cualquier otra cosa. El interruptor original se ocultar\u00e1." } } }, diff --git a/homeassistant/components/switchbot/translations/ca.json b/homeassistant/components/switchbot/translations/ca.json index a3ba38e2f24..04c1af6d69f 100644 --- a/homeassistant/components/switchbot/translations/ca.json +++ b/homeassistant/components/switchbot/translations/ca.json @@ -9,6 +9,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "Vols configurar {name}?" + }, + "password": { + "data": { + "password": "Contrasenya" + }, + "description": "El dispositiu {name} necessita una contrasenya" + }, "user": { "data": { "address": "Adre\u00e7a del dispositiu", diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index 7e58d169856..6a0231c9bed 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -7,7 +7,6 @@ "switchbot_unsupported_type": "Unsupported Switchbot Type.", "unknown": "Unexpected error" }, - "error": {}, "flow_title": "{name} ({address})", "step": { "confirm": { @@ -21,8 +20,12 @@ }, "user": { "data": { - "address": "Device address" - } + "address": "Device address", + "mac": "Device MAC address", + "name": "Name", + "password": "Password" + }, + "title": "Setup Switchbot device" } } }, @@ -30,7 +33,10 @@ "step": { "init": { "data": { - "retry_count": "Retry count" + "retry_count": "Retry count", + "retry_timeout": "Timeout between retries", + "scan_timeout": "How long to scan for advertisement data", + "update_time": "Time between updates (seconds)" } } } diff --git a/homeassistant/components/switchbot/translations/es.json b/homeassistant/components/switchbot/translations/es.json index 6fc4d59f693..d0f908df646 100644 --- a/homeassistant/components/switchbot/translations/es.json +++ b/homeassistant/components/switchbot/translations/es.json @@ -7,10 +7,20 @@ "switchbot_unsupported_type": "Tipo de Switchbot no compatible.", "unknown": "Error inesperado" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "password": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "El dispositivo {name} requiere una contrase\u00f1a" + }, "user": { "data": { + "address": "Direcci\u00f3n del dispositivo", "mac": "Direcci\u00f3n MAC del dispositivo", "name": "Nombre", "password": "Contrase\u00f1a" diff --git a/homeassistant/components/switchbot/translations/fr.json b/homeassistant/components/switchbot/translations/fr.json index ea070f9f3c2..2b35ec9634e 100644 --- a/homeassistant/components/switchbot/translations/fr.json +++ b/homeassistant/components/switchbot/translations/fr.json @@ -9,6 +9,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "password": { + "data": { + "password": "Mot de passe" + }, + "description": "L'appareil {name} requiert un mot de passe" + }, "user": { "data": { "address": "Adresse de l'appareil", diff --git a/homeassistant/components/switchbot/translations/hu.json b/homeassistant/components/switchbot/translations/hu.json index bd363de0738..88f5756c5fa 100644 --- a/homeassistant/components/switchbot/translations/hu.json +++ b/homeassistant/components/switchbot/translations/hu.json @@ -13,6 +13,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "password": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "A {name} eszk\u00f6z jelsz\u00f3t k\u00e9r" + }, "user": { "data": { "address": "Eszk\u00f6z c\u00edme", diff --git a/homeassistant/components/switchbot/translations/pt-BR.json b/homeassistant/components/switchbot/translations/pt-BR.json index 3edc04ba6f3..bcc97a120af 100644 --- a/homeassistant/components/switchbot/translations/pt-BR.json +++ b/homeassistant/components/switchbot/translations/pt-BR.json @@ -9,6 +9,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "Deseja configurar {name}?" + }, + "password": { + "data": { + "password": "Senha" + }, + "description": "O dispositivo {name} requer uma senha" + }, "user": { "data": { "address": "Endere\u00e7o do dispositivo", diff --git a/homeassistant/components/switchbot/translations/tr.json b/homeassistant/components/switchbot/translations/tr.json index 40b80dc4a3c..d60be4c0d73 100644 --- a/homeassistant/components/switchbot/translations/tr.json +++ b/homeassistant/components/switchbot/translations/tr.json @@ -11,10 +11,20 @@ "one": "Bo\u015f", "other": "Bo\u015f" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "password": { + "data": { + "password": "Parola" + }, + "description": "{name} cihaz\u0131 bir \u015fifre gerektiriyor" + }, "user": { "data": { + "address": "Cihaz adresi", "mac": "Cihaz MAC adresi", "name": "Ad", "password": "Parola" diff --git a/homeassistant/components/syncthing/translations/es.json b/homeassistant/components/syncthing/translations/es.json index e39a1436c2a..dd156f7edb4 100644 --- a/homeassistant/components/syncthing/translations/es.json +++ b/homeassistant/components/syncthing/translations/es.json @@ -13,7 +13,7 @@ "title": "Configurar integraci\u00f3n de Syncthing", "token": "Token", "url": "URL", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar el certificado SSL" } } } diff --git a/homeassistant/components/synology_dsm/translations/es.json b/homeassistant/components/synology_dsm/translations/es.json index 779996d7023..d7f1f16197c 100644 --- a/homeassistant/components/synology_dsm/translations/es.json +++ b/homeassistant/components/synology_dsm/translations/es.json @@ -25,15 +25,15 @@ "password": "Contrase\u00f1a", "port": "Puerto", "ssl": "Usar SSL/TLS para conectar con tu NAS", - "username": "Usuario", - "verify_ssl": "Verificar certificado SSL" + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL" }, "description": "\u00bfQuieres configurar {name} ({host})?" }, "reauth_confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Volver a autenticar la integraci\u00f3n Synology DSM" }, @@ -44,7 +44,7 @@ "port": "Puerto", "ssl": "Usar SSL/TLS para conectar con tu NAS", "username": "Usuario", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar el certificado SSL" } } } diff --git a/homeassistant/components/tado/translations/es.json b/homeassistant/components/tado/translations/es.json index db71d41f789..58745783774 100644 --- a/homeassistant/components/tado/translations/es.json +++ b/homeassistant/components/tado/translations/es.json @@ -13,7 +13,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Conectar con tu cuenta de Tado" } diff --git a/homeassistant/components/tankerkoenig/translations/es.json b/homeassistant/components/tankerkoenig/translations/es.json index bda6c43ce6e..ec0f079cbea 100644 --- a/homeassistant/components/tankerkoenig/translations/es.json +++ b/homeassistant/components/tankerkoenig/translations/es.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", - "no_stations": "No se pudo encontrar ninguna estaci\u00f3n al alcance." + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "no_stations": "No se pudo encontrar ninguna estaci\u00f3n dentro del rango." }, "step": { "reauth_confirm": { @@ -18,7 +18,7 @@ "data": { "stations": "Estaciones" }, - "description": "encontr\u00f3 {stations_count} estaciones en el radio", + "description": "se encontraron {stations_count} estaciones en el radio", "title": "Selecciona las estaciones a a\u00f1adir" }, "user": { diff --git a/homeassistant/components/tautulli/translations/es.json b/homeassistant/components/tautulli/translations/es.json index 295683bf358..2d9a09d3432 100644 --- a/homeassistant/components/tautulli/translations/es.json +++ b/homeassistant/components/tautulli/translations/es.json @@ -5,8 +5,8 @@ "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { - "cannot_connect": "Fallo en la conexi\u00f3n", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { @@ -14,16 +14,16 @@ "data": { "api_key": "Clave API" }, - "description": "Para encontrar su clave API, abra la p\u00e1gina web de Tautulli y navegue a Configuraci\u00f3n y luego a la interfaz web. La clave API estar\u00e1 en la parte inferior de esa p\u00e1gina.", + "description": "Para encontrar tu clave API, abre la p\u00e1gina web de Tautulli y navega a Configuraci\u00f3n y luego a Interfaz web. La clave API estar\u00e1 en la parte inferior de esa p\u00e1gina.", "title": "Re-autenticaci\u00f3n de Tautulli" }, "user": { "data": { "api_key": "Clave API", "url": "URL", - "verify_ssl": "Verifica el certificat SSL" + "verify_ssl": "Verificar el certificado SSL" }, - "description": "Para encontrar su clave de API, abra la p\u00e1gina web de Tautulli y navegue hasta Configuraci\u00f3n y luego hasta Interfaz web. La clave API estar\u00e1 en la parte inferior de esa p\u00e1gina.\n\nEjemplo de la URL: ```http://192.168.0.10:8181`` con 8181 como puerto predeterminado." + "description": "Para encontrar tu clave API, abre la p\u00e1gina web de Tautulli y navega a Configuraci\u00f3n y luego a Interfaz web. La clave API estar\u00e1 en la parte inferior de esa p\u00e1gina. \n\nEjemplo de URL: ```http://192.168.0.10:8181``` siendo 8181 el puerto predeterminado." } } } diff --git a/homeassistant/components/tod/translations/es.json b/homeassistant/components/tod/translations/es.json index c1ea525e10c..a5451faf145 100644 --- a/homeassistant/components/tod/translations/es.json +++ b/homeassistant/components/tod/translations/es.json @@ -22,5 +22,5 @@ } } }, - "title": "Sensor tiempo del d\u00eda" + "title": "Sensor de horas del d\u00eda" } \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/es.json b/homeassistant/components/totalconnect/translations/es.json index 66f5be9ebc2..a25a204eec4 100644 --- a/homeassistant/components/totalconnect/translations/es.json +++ b/homeassistant/components/totalconnect/translations/es.json @@ -24,7 +24,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } @@ -35,7 +35,7 @@ "data": { "auto_bypass_low_battery": "Ignorar autom\u00e1ticamente bater\u00eda baja" }, - "description": "Ignorar autom\u00e1ticamente las zonas en el momento en que informan de que la bater\u00eda est\u00e1 baja.", + "description": "Omite zonas autom\u00e1ticamente en el momento en que informan que la bater\u00eda est\u00e1 baja.", "title": "Opciones de TotalConnect" } } diff --git a/homeassistant/components/tradfri/translations/es.json b/homeassistant/components/tradfri/translations/es.json index 531b11cc0a1..caadd9b7903 100644 --- a/homeassistant/components/tradfri/translations/es.json +++ b/homeassistant/components/tradfri/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso" + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso" }, "error": { "cannot_authenticate": "No se puede autenticar, \u00bfGateway est\u00e1 emparejado con otro servidor como, por ejemplo, Homekit?", diff --git a/homeassistant/components/trafikverket_ferry/translations/es.json b/homeassistant/components/trafikverket_ferry/translations/es.json index 0ab89ac0fa8..93996b1923c 100644 --- a/homeassistant/components/trafikverket_ferry/translations/es.json +++ b/homeassistant/components/trafikverket_ferry/translations/es.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Fallo en la conexi\u00f3n", - "incorrect_api_key": "Clave API inv\u00e1lida para la cuenta seleccionada", + "cannot_connect": "No se pudo conectar", + "incorrect_api_key": "Clave de API no v\u00e1lida para la cuenta seleccionada", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_route": "No se pudo encontrar la ruta con la informaci\u00f3n proporcionada" }, @@ -19,7 +19,7 @@ "user": { "data": { "api_key": "Clave API", - "from": "Des del puerto", + "from": "Desde el puerto", "time": "Hora", "to": "Al puerto", "weekday": "D\u00edas entre semana" diff --git a/homeassistant/components/trafikverket_train/translations/es.json b/homeassistant/components/trafikverket_train/translations/es.json index 2aca5e59745..9f546bd1676 100644 --- a/homeassistant/components/trafikverket_train/translations/es.json +++ b/homeassistant/components/trafikverket_train/translations/es.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", - "incorrect_api_key": "Clave API inv\u00e1lida para la cuenta seleccionada", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "cannot_connect": "No se pudo conectar", + "incorrect_api_key": "Clave API no v\u00e1lida para la cuenta seleccionada", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_station": "No se pudo encontrar una estaci\u00f3n con el nombre especificado", "invalid_time": "Hora proporcionada no v\u00e1lida", "more_stations": "Se encontraron varias estaciones con el nombre especificado" diff --git a/homeassistant/components/transmission/translations/es.json b/homeassistant/components/transmission/translations/es.json index c83904bf5db..6c293a59d4c 100644 --- a/homeassistant/components/transmission/translations/es.json +++ b/homeassistant/components/transmission/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado." + "already_configured": "El dispositivo ya est\u00e1 configurado.", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", @@ -9,13 +10,20 @@ "name_exists": "El nombre ya existe" }, "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "La contrase\u00f1a para {username} no es v\u00e1lida.", + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "host": "Host", "name": "Nombre", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Configuraci\u00f3n del cliente de transmisi\u00f3n" } diff --git a/homeassistant/components/tuya/translations/es.json b/homeassistant/components/tuya/translations/es.json index a15408bf96d..d51024c5640 100644 --- a/homeassistant/components/tuya/translations/es.json +++ b/homeassistant/components/tuya/translations/es.json @@ -11,7 +11,7 @@ "access_secret": "Tuya IoT Access Secret", "country_code": "C\u00f3digo de pais de tu cuenta (por ejemplo, 1 para USA o 86 para China)", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "Introduce tus credencial de Tuya" } diff --git a/homeassistant/components/ukraine_alarm/translations/es.json b/homeassistant/components/ukraine_alarm/translations/es.json index 3e1b8f322bb..6a4e2d806a5 100644 --- a/homeassistant/components/ukraine_alarm/translations/es.json +++ b/homeassistant/components/ukraine_alarm/translations/es.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada", - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "max_regions": "Se pueden configurar un m\u00e1ximo de 5 regiones", "rate_limit": "Demasiadas peticiones", - "timeout": "Tiempo m\u00e1ximo de espera para establecer la conexi\u00f3n agotado", + "timeout": "Tiempo de espera agotado para establecer la conexi\u00f3n", "unknown": "Error inesperado" }, "step": { @@ -13,19 +13,19 @@ "data": { "region": "Regi\u00f3n" }, - "description": "Si desea monitorear no solo el estado y el distrito, elija su comunidad espec\u00edfica" + "description": "Si quieres supervisar no solo el estado y el distrito, elige tu comunidad espec\u00edfica" }, "district": { "data": { "region": "Regi\u00f3n" }, - "description": "Si quieres monitorear no s\u00f3lo el estado, elige el distrito espec\u00edfico" + "description": "Si quieres supervisar no s\u00f3lo el estado, elige el distrito espec\u00edfico" }, "user": { "data": { "region": "Regi\u00f3n" }, - "description": "Escoja el estado a monitorear" + "description": "Elige el estado para supervisar" } } } diff --git a/homeassistant/components/unifi/translations/es.json b/homeassistant/components/unifi/translations/es.json index ebaf1d3c127..c6d17b369b4 100644 --- a/homeassistant/components/unifi/translations/es.json +++ b/homeassistant/components/unifi/translations/es.json @@ -18,7 +18,7 @@ "password": "Contrase\u00f1a", "port": "Puerto", "site": "ID del sitio", - "username": "Usuario", + "username": "Nombre de usuario", "verify_ssl": "Controlador usando el certificado adecuado" }, "title": "Configuraci\u00f3n de UniFi Network" diff --git a/homeassistant/components/unifiprotect/translations/de.json b/homeassistant/components/unifiprotect/translations/de.json index 0f6ffd77793..01867c2b76d 100644 --- a/homeassistant/components/unifiprotect/translations/de.json +++ b/homeassistant/components/unifiprotect/translations/de.json @@ -47,6 +47,7 @@ "data": { "all_updates": "Echtzeitmetriken (WARNUNG: Erh\u00f6ht die CPU-Auslastung erheblich)", "disable_rtsp": "RTSP-Stream deaktivieren", + "max_media": "Maximale Anzahl von Ereignissen, die f\u00fcr den Medienbrowser geladen werden (erh\u00f6ht die RAM-Nutzung)", "override_connection_host": "Verbindungshost \u00fcberschreiben" }, "description": "Die Option Echtzeit-Metriken sollte nur aktiviert werden, wenn du die Diagnosesensoren aktiviert hast und diese in Echtzeit aktualisiert werden sollen. Wenn sie nicht aktiviert ist, werden sie nur einmal alle 15 Minuten aktualisiert.", diff --git a/homeassistant/components/unifiprotect/translations/el.json b/homeassistant/components/unifiprotect/translations/el.json index 8cede72d32b..4ad61ad47bb 100644 --- a/homeassistant/components/unifiprotect/translations/el.json +++ b/homeassistant/components/unifiprotect/translations/el.json @@ -47,6 +47,7 @@ "data": { "all_updates": "\u039c\u03b5\u03c4\u03c1\u03ae\u03c3\u03b5\u03b9\u03c2 \u03c3\u03b5 \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03cc \u03c7\u03c1\u03cc\u03bd\u03bf (\u03a0\u03a1\u039f\u0395\u0399\u0394\u039f\u03a0\u039f\u0399\u0397\u03a3\u0397: \u0391\u03c5\u03be\u03ac\u03bd\u03b5\u03b9 \u03c3\u03b7\u03bc\u03b1\u03bd\u03c4\u03b9\u03ba\u03ac \u03c4\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03b7\u03c2 CPU)", "disable_rtsp": "\u0391\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c1\u03bf\u03ae RTSP", + "max_media": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03c9\u03bd \u03c0\u03c1\u03bf\u03c2 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03c0\u03b5\u03c1\u03b9\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd (\u03b1\u03c5\u03be\u03ac\u03bd\u03b5\u03b9 \u03c4\u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 RAM)", "override_connection_host": "\u03a0\u03b1\u03c1\u03ac\u03ba\u03b1\u03bc\u03c8\u03b7 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "description": "\u0397 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03c4\u03c1\u03ae\u03c3\u03b5\u03c9\u03bd \u03c3\u03b5 \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03cc \u03c7\u03c1\u03cc\u03bd\u03bf \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03bc\u03cc\u03bd\u03bf \u03b5\u03ac\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03c4\u03bf\u03c5\u03c2 \u03b4\u03b9\u03b1\u03b3\u03bd\u03c9\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03ba\u03b1\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03b8\u03bf\u03cd\u03bd \u03c3\u03b5 \u03c0\u03c1\u03b1\u03b3\u03bc\u03b1\u03c4\u03b9\u03ba\u03cc \u03c7\u03c1\u03cc\u03bd\u03bf. \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b1, \u03b8\u03b1 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03bd\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03c6\u03bf\u03c1\u03ac \u03ba\u03ac\u03b8\u03b5 15 \u03bb\u03b5\u03c0\u03c4\u03ac.", diff --git a/homeassistant/components/unifiprotect/translations/es.json b/homeassistant/components/unifiprotect/translations/es.json index eb52d0222fc..8a1cdd9afcd 100644 --- a/homeassistant/components/unifiprotect/translations/es.json +++ b/homeassistant/components/unifiprotect/translations/es.json @@ -14,7 +14,7 @@ "discovery_confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "description": "\u00bfQuieres configurar {name} ({ip_address})? Necesitar\u00e1 un usuario local creado en su consola UniFi OS para iniciar sesi\u00f3n. Los usuarios de Ubiquiti Cloud no funcionar\u00e1n. Para m\u00e1s informaci\u00f3n: {local_user_documentation_url}", "title": "UniFi Protect descubierto" @@ -47,6 +47,7 @@ "data": { "all_updates": "M\u00e9tricas en tiempo real (ADVERTENCIA: Aumenta en gran medida el uso de la CPU)", "disable_rtsp": "Deshabilitar la transmisi\u00f3n RTSP", + "max_media": "N\u00famero m\u00e1ximo de eventos a cargar para el Navegador de Medios (aumenta el uso de RAM)", "override_connection_host": "Anular la conexi\u00f3n del host" }, "description": "La opci\u00f3n de m\u00e9tricas en tiempo real s\u00f3lo debe estar activada si ha habilitado los sensores de diagn\u00f3stico y quiere que se actualicen en tiempo real. Si no est\u00e1 activada, s\u00f3lo se actualizar\u00e1n una vez cada 15 minutos.", diff --git a/homeassistant/components/unifiprotect/translations/et.json b/homeassistant/components/unifiprotect/translations/et.json index 9abd63559ee..31b70f41dec 100644 --- a/homeassistant/components/unifiprotect/translations/et.json +++ b/homeassistant/components/unifiprotect/translations/et.json @@ -47,6 +47,7 @@ "data": { "all_updates": "Reaalajas m\u00f5\u00f5dikud (HOIATUS: suurendab oluliselt CPU kasutust)", "disable_rtsp": "Keela RTSP voog", + "max_media": "Meediumibrauserisse laaditavate s\u00fcndmuste maksimaalne arv (suurendab RAM-i kasutamist)", "override_connection_host": "\u00dchenduse hosti alistamine" }, "description": "Reaalajas m\u00f5\u00f5dikute valik tuleks lubada ainult siis kui oled diagnostikaandurid sisse l\u00fclitanud ja soovid, et neid uuendatakse reaalajas. Kui see ei ole lubatud, uuendatakse neid ainult \u00fcks kord 15 minuti tagant.", diff --git a/homeassistant/components/unifiprotect/translations/pt-BR.json b/homeassistant/components/unifiprotect/translations/pt-BR.json index 2c0c1270add..e2951d47938 100644 --- a/homeassistant/components/unifiprotect/translations/pt-BR.json +++ b/homeassistant/components/unifiprotect/translations/pt-BR.json @@ -47,6 +47,7 @@ "data": { "all_updates": "M\u00e9tricas em tempo real (AVISO: aumenta muito o uso da CPU)", "disable_rtsp": "Desativar o fluxo RTSP", + "max_media": "N\u00famero m\u00e1ximo de eventos a serem carregados para o Media Browser (aumenta o uso de RAM)", "override_connection_host": "Anular o host de conex\u00e3o" }, "description": "A op\u00e7\u00e3o de m\u00e9tricas em tempo real s\u00f3 deve ser habilitada se voc\u00ea tiver habilitado os sensores de diagn\u00f3stico e quiser que eles sejam atualizados em tempo real. Se n\u00e3o estiver ativado, eles ser\u00e3o atualizados apenas uma vez a cada 15 minutos.", diff --git a/homeassistant/components/unifiprotect/translations/tr.json b/homeassistant/components/unifiprotect/translations/tr.json index 869c13608ce..d26f6af41ce 100644 --- a/homeassistant/components/unifiprotect/translations/tr.json +++ b/homeassistant/components/unifiprotect/translations/tr.json @@ -47,6 +47,7 @@ "data": { "all_updates": "Ger\u00e7ek zamanl\u0131 \u00f6l\u00e7\u00fcmler (UYARI: CPU kullan\u0131m\u0131n\u0131 b\u00fcy\u00fck \u00f6l\u00e7\u00fcde art\u0131r\u0131r)", "disable_rtsp": "RTSP ak\u0131\u015f\u0131n\u0131 devre d\u0131\u015f\u0131 b\u0131rak\u0131n", + "max_media": "Medya Taray\u0131c\u0131 i\u00e7in y\u00fcklenecek maksimum olay say\u0131s\u0131 (RAM kullan\u0131m\u0131n\u0131 art\u0131r\u0131r)", "override_connection_host": "Ba\u011flant\u0131 Ana Bilgisayar\u0131n\u0131 Ge\u00e7ersiz K\u0131l" }, "description": "Ger\u00e7ek zamanl\u0131 \u00f6l\u00e7\u00fcmler se\u00e7ene\u011fi, yaln\u0131zca tan\u0131lama sens\u00f6rlerini etkinle\u015ftirdiyseniz ve bunlar\u0131n ger\u00e7ek zamanl\u0131 olarak g\u00fcncellenmesini istiyorsan\u0131z etkinle\u015ftirilmelidir. Etkinle\u015ftirilmezlerse, yaln\u0131zca her 15 dakikada bir g\u00fcncellenirler.", diff --git a/homeassistant/components/upcloud/translations/es.json b/homeassistant/components/upcloud/translations/es.json index 96c659ccad4..f4cccb3f0e2 100644 --- a/homeassistant/components/upcloud/translations/es.json +++ b/homeassistant/components/upcloud/translations/es.json @@ -8,7 +8,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/update/translations/es.json b/homeassistant/components/update/translations/es.json index ee92c01c938..49012350f47 100644 --- a/homeassistant/components/update/translations/es.json +++ b/homeassistant/components/update/translations/es.json @@ -2,8 +2,8 @@ "device_automation": { "trigger_type": { "changed_states": "La disponibilidad de la actualizaci\u00f3n de {entity_name} cambie", - "turned_off": "{entity_name} se actualice", - "turned_on": "{entity_name} tenga una actualizaci\u00f3n disponible" + "turned_off": "{entity_name} se actualiz\u00f3", + "turned_on": "{entity_name} tiene una actualizaci\u00f3n disponible" } }, "title": "Actualizar" diff --git a/homeassistant/components/uscis/translations/es.json b/homeassistant/components/uscis/translations/es.json new file mode 100644 index 00000000000..8dd3ad76874 --- /dev/null +++ b/homeassistant/components/uscis/translations/es.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "La integraci\u00f3n de los Servicios de Inmigraci\u00f3n y Ciudadan\u00eda de los EE.UU. (USCIS) est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nLa integraci\u00f3n se elimina porque se basa en webscraping, que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la integraci\u00f3n USCIS" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uscis/translations/tr.json b/homeassistant/components/uscis/translations/tr.json new file mode 100644 index 00000000000..0e27f5bdac5 --- /dev/null +++ b/homeassistant/components/uscis/translations/tr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "ABD Vatanda\u015fl\u0131k ve G\u00f6\u00e7menlik Hizmetleri (USCIS) entegrasyonu, Home Assistant'tan kald\u0131r\u0131lmay\u0131 bekliyor ve Home Assistant 2022.10'dan itibaren art\u0131k kullan\u0131lamayacak. \n\n Entegrasyon kald\u0131r\u0131l\u0131yor, \u00e7\u00fcnk\u00fc izin verilmeyen web taramaya dayan\u0131yor. \n\n YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "USCIS entegrasyonu kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/es.json b/homeassistant/components/utility_meter/translations/es.json index a38687741d8..f4a3ca04104 100644 --- a/homeassistant/components/utility_meter/translations/es.json +++ b/homeassistant/components/utility_meter/translations/es.json @@ -12,13 +12,13 @@ "tariffs": "Tarifas soportadas" }, "data_description": { - "delta_values": "Habilitar si los valores de origen son valores delta desde la \u00faltima lectura en lugar de valores absolutos.", - "net_consumption": "Act\u00edvalo si es un contador limpio, es decir, puede aumentar y disminuir.", - "offset": "Desplaza el d\u00eda de restablecimiento mensual del contador.", - "tariffs": "Lista de tarifas admitidas, d\u00e9jala en blanco si utilizas una \u00fanica tarifa." + "delta_values": "Habil\u00edtalo si los valores de origen son valores delta desde la \u00faltima lectura en lugar de valores absolutos.", + "net_consumption": "Habil\u00edtalo si la fuente es un medidor neto, lo que significa que puede aumentar y disminuir.", + "offset": "Compensar el d\u00eda de reinicio del medidor mensual.", + "tariffs": "Una lista de tarifas admitidas, d\u00e9jala en blanco si solo necesitas una tarifa." }, - "description": "Cree un sensor que rastree el consumo de varios servicios p\u00fablicos (p. ej., energ\u00eda, gas, agua, calefacci\u00f3n) durante un per\u00edodo de tiempo configurado, generalmente mensual. El sensor del medidor de servicios admite opcionalmente dividir el consumo por tarifas, en ese caso se crea un sensor para cada tarifa, as\u00ed como una entidad de selecci\u00f3n para elegir la tarifa actual.", - "title": "A\u00f1adir medidor de utilidades" + "description": "Crea un sensor que rastree el consumo de varios servicios p\u00fablicos (p. ej., energ\u00eda, gas, agua, calefacci\u00f3n) durante un per\u00edodo de tiempo configurado, generalmente mensual. El sensor del medidor de servicios admite opcionalmente dividir el consumo por tarifas, en ese caso se crea un sensor para cada tarifa, as\u00ed como una entidad de selecci\u00f3n para elegir la tarifa actual.", + "title": "A\u00f1adir contador" } } }, diff --git a/homeassistant/components/venstar/translations/es.json b/homeassistant/components/venstar/translations/es.json index 0401a0365d3..90d63f5aa0d 100644 --- a/homeassistant/components/venstar/translations/es.json +++ b/homeassistant/components/venstar/translations/es.json @@ -14,7 +14,7 @@ "password": "Contrase\u00f1a", "pin": "C\u00f3digo PIN", "ssl": "Utiliza un certificado SSL", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Conectar con el termostato Venstar" } diff --git a/homeassistant/components/verisure/translations/es.json b/homeassistant/components/verisure/translations/es.json index 19517bb7ed0..c7ea0af6f41 100644 --- a/homeassistant/components/verisure/translations/es.json +++ b/homeassistant/components/verisure/translations/es.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "unknown": "Error inesperado" + "unknown": "Error inesperado", + "unknown_mfa": "Se produjo un error desconocido durante la configuraci\u00f3n MFA" }, "step": { "installation": { @@ -15,6 +16,12 @@ }, "description": "Home Assistant encontr\u00f3 varias instalaciones de Verisure en su cuenta de Mis p\u00e1ginas. Por favor, seleccione la instalaci\u00f3n para agregar a Home Assistant." }, + "mfa": { + "data": { + "code": "C\u00f3digo de verificaci\u00f3n", + "description": "Tu cuenta tiene activada la verificaci\u00f3n en dos pasos. Por favor, introduce el c\u00f3digo de verificaci\u00f3n que te env\u00eda Verisure." + } + }, "reauth_confirm": { "data": { "description": "Vuelva a autenticarse con su cuenta Verisure My Pages.", @@ -22,6 +29,12 @@ "password": "Contrase\u00f1a" } }, + "reauth_mfa": { + "data": { + "code": "C\u00f3digo de verificaci\u00f3n", + "description": "Tu cuenta tiene activada la verificaci\u00f3n en dos pasos. Por favor, introduce el c\u00f3digo de verificaci\u00f3n que te env\u00eda Verisure." + } + }, "user": { "data": { "description": "Inicia sesi\u00f3n con tu cuenta Verisure My Pages.", diff --git a/homeassistant/components/vulcan/translations/es.json b/homeassistant/components/vulcan/translations/es.json index 7538c6411df..7e8b3b8b067 100644 --- a/homeassistant/components/vulcan/translations/es.json +++ b/homeassistant/components/vulcan/translations/es.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "all_student_already_configured": "Ya se han a\u00f1adido todos los estudiantes.", - "already_configured": "Ya se ha a\u00f1adido a este alumno.", - "no_matching_entries": "No se encontraron entradas que coincidan, use una cuenta diferente o elimine la integraci\u00f3n con el estudiante obsoleto.", + "all_student_already_configured": "Todos los estudiantes ya han sido a\u00f1adidos.", + "already_configured": "Ese estudiante ya ha sido a\u00f1adido.", + "no_matching_entries": "No se encontraron entradas que coincidan, usa una cuenta diferente o elimina la integraci\u00f3n con el estudiante obsoleto.", "reauth_successful": "Re-autenticaci\u00f3n exitosa" }, "error": { - "cannot_connect": "Error de conexi\u00f3n - compruebe su conexi\u00f3n a Internet", - "expired_credentials": "Credenciales caducadas - cree nuevas en la p\u00e1gina de registro de la aplicaci\u00f3n m\u00f3vil de Vulcan", - "expired_token": "Token caducado, genera un nuevo token", + "cannot_connect": "Error de conexi\u00f3n - por favor, comprueba tu conexi\u00f3n a Internet", + "expired_credentials": "Credenciales caducadas - por favor, crea unas nuevas en la p\u00e1gina de registro de la aplicaci\u00f3n m\u00f3vil de Vulcan", + "expired_token": "Token caducado - por favor, genera un token nuevo", "invalid_pin": "Pin no v\u00e1lido", "invalid_symbol": "S\u00edmbolo inv\u00e1lido", "invalid_token": "Token inv\u00e1lido", @@ -28,7 +28,7 @@ "region": "S\u00edmbolo", "token": "Token" }, - "description": "Acceda a su cuenta de Vulcan a trav\u00e9s de la p\u00e1gina de registro de la aplicaci\u00f3n m\u00f3vil." + "description": "Inicia sesi\u00f3n en tu cuenta de Vulcan utilizando la p\u00e1gina de registro de la aplicaci\u00f3n m\u00f3vil." }, "reauth_confirm": { "data": { @@ -36,7 +36,7 @@ "region": "S\u00edmbolo", "token": "Token" }, - "description": "Acceda a su cuenta de Vulcan a trav\u00e9s de la p\u00e1gina de registro de la aplicaci\u00f3n m\u00f3vil." + "description": "Inicie sesi\u00f3n en tu cuenta de Vulcan utilizando la p\u00e1gina de registro de la aplicaci\u00f3n m\u00f3vil." }, "select_saved_credentials": { "data": { @@ -48,7 +48,7 @@ "data": { "student_name": "Selecciona al alumno" }, - "description": "Seleccione el estudiante, puede a\u00f1adir m\u00e1s estudiantes a\u00f1adiendo de nuevo la integraci\u00f3n." + "description": "Selecciona el estudiante, puedes a\u00f1adir m\u00e1s estudiantes a\u00f1adiendo de nuevo la integraci\u00f3n." } } } diff --git a/homeassistant/components/wallbox/translations/es.json b/homeassistant/components/wallbox/translations/es.json index 1c5315d6745..451e55ed41e 100644 --- a/homeassistant/components/wallbox/translations/es.json +++ b/homeassistant/components/wallbox/translations/es.json @@ -14,14 +14,14 @@ "reauth_confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } }, "user": { "data": { "password": "Contrase\u00f1a", "station": "N\u00famero de serie de la estaci\u00f3n", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/whirlpool/translations/es.json b/homeassistant/components/whirlpool/translations/es.json index d26c25c3548..b5ec4d85cb0 100644 --- a/homeassistant/components/whirlpool/translations/es.json +++ b/homeassistant/components/whirlpool/translations/es.json @@ -9,7 +9,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/withings/translations/es.json b/homeassistant/components/withings/translations/es.json index d3a3f60b12f..1303ca27690 100644 --- a/homeassistant/components/withings/translations/es.json +++ b/homeassistant/components/withings/translations/es.json @@ -27,6 +27,10 @@ "reauth": { "description": "El perfil \"{profile}\" debe volver a autenticarse para continuar recibiendo datos de Withings.", "title": "Re-autentificar el perfil" + }, + "reauth_confirm": { + "description": "El perfil \"{profile}\" debe volver a autenticarse para continuar recibiendo datos de Withings.", + "title": "Volver a autenticar la integraci\u00f3n" } } } diff --git a/homeassistant/components/ws66i/translations/es.json b/homeassistant/components/ws66i/translations/es.json index d19017fdb95..075dd41ed56 100644 --- a/homeassistant/components/ws66i/translations/es.json +++ b/homeassistant/components/ws66i/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Ha fallado la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "step": { @@ -12,7 +12,7 @@ "data": { "ip_address": "Direcci\u00f3n IP" }, - "title": "Conexi\u00f3n con el dispositivo" + "title": "Conectar al dispositivo" } } }, @@ -27,7 +27,7 @@ "source_5": "Nombre de la fuente #5", "source_6": "Nombre de la fuente #6" }, - "title": "Configuraci\u00f3n de las fuentes" + "title": "Configurar fuentes" } } } diff --git a/homeassistant/components/xbox/translations/es.json b/homeassistant/components/xbox/translations/es.json index 27cbeaec139..7b7fe2d32fe 100644 --- a/homeassistant/components/xbox/translations/es.json +++ b/homeassistant/components/xbox/translations/es.json @@ -13,5 +13,11 @@ "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3n de Xbox en configuration.yaml se eliminar\u00e1 en Home Assistant 2022.9. \n\nTus credenciales OAuth de aplicaci\u00f3n existentes y la configuraci\u00f3n de acceso se han importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Xbox" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/et.json b/homeassistant/components/xbox/translations/et.json index d7bb66c6cf1..2b14aa56762 100644 --- a/homeassistant/components/xbox/translations/et.json +++ b/homeassistant/components/xbox/translations/et.json @@ -13,5 +13,11 @@ "title": "Vali tuvastusmeetod" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Xboxi konfigureerimine failis configuration.yaml eemaldatakse versioonis Home Assistant 2022.9.\n\nTeie olemasolevad OAuth-rakenduse volitused ja juurdep\u00e4\u00e4su seaded on automaatselt kasutajaliidesesse imporditud. Probleemi lahendamiseks eemaldage YAML-konfiguratsioon failist configuration.yaml ja k\u00e4ivitage Home Assistant uuesti.", + "title": "Xboxi YAML-i konfiguratsioon eemaldatakse" + } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/tr.json b/homeassistant/components/xbox/translations/tr.json index 3f4025ade5d..39a9c199371 100644 --- a/homeassistant/components/xbox/translations/tr.json +++ b/homeassistant/components/xbox/translations/tr.json @@ -13,5 +13,11 @@ "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" } } + }, + "issues": { + "deprecated_yaml": { + "description": "Xbox'\u0131 configuration.yaml'de yap\u0131land\u0131rma, Home Assistant 2022.9'da kald\u0131r\u0131l\u0131yor. \n\n Mevcut OAuth Uygulama Kimlik Bilgileriniz ve eri\u015fim ayarlar\u0131n\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Xbox YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/es.json b/homeassistant/components/xiaomi_aqara/translations/es.json index e06806bbd9a..d8a36306af7 100644 --- a/homeassistant/components/xiaomi_aqara/translations/es.json +++ b/homeassistant/components/xiaomi_aqara/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "not_xiaomi_aqara": "No es un Xiaomi Aqara Gateway, el dispositivo descubierto no coincide con los gateways conocidos" }, "error": { diff --git a/homeassistant/components/xiaomi_ble/translations/el.json b/homeassistant/components/xiaomi_ble/translations/el.json index 30dc61cb5dc..e6c5efce91f 100644 --- a/homeassistant/components/xiaomi_ble/translations/el.json +++ b/homeassistant/components/xiaomi_ble/translations/el.json @@ -6,13 +6,22 @@ "decryption_failed": "\u03a4\u03bf \u03c0\u03b1\u03c1\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03cd\u03c1\u03b3\u03b7\u03c3\u03b5, \u03c4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03c3\u03b1\u03bd \u03bd\u03b1 \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03b8\u03bf\u03cd\u03bd. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", "expected_24_characters": "\u0391\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03c4\u03b1\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 24 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd.", "expected_32_characters": "\u0391\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03c4\u03b1\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd.", - "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf" + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + }, + "error": { + "decryption_failed": "\u03a4\u03bf \u03c0\u03b1\u03c1\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03bb\u03b5\u03b9\u03c4\u03bf\u03cd\u03c1\u03b3\u03b7\u03c3\u03b5, \u03c4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03bf\u03cd\u03c3\u03b1\u03bd \u03bd\u03b1 \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03b8\u03bf\u03cd\u03bd. \u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.", + "expected_24_characters": "\u0391\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03c4\u03b1\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 24 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd.", + "expected_32_characters": "\u0391\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03c4\u03b1\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03cc \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af 32 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd." }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" }, + "confirm_slow": { + "description": "\u0394\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b3\u03af\u03bd\u03b5\u03b9 \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7 \u03b1\u03c0\u03cc \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c4\u03b7\u03bd \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1 \u03c3\u03c4\u03b9\u03b3\u03bc\u03ae, \u03b5\u03c0\u03bf\u03bc\u03ad\u03bd\u03c9\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bc\u03b1\u03c3\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03bf\u03b9 \u03b1\u03bd \u03b1\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03ae \u03cc\u03c7\u03b9. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03bf\u03c6\u03b5\u03af\u03bb\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf \u03cc\u03c4\u03b9 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ad\u03bd\u03b1 \u03b1\u03c1\u03b3\u03cc \u03b4\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7\u03c2. \u0395\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03b9\u03ce\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bf\u03cd\u03c4\u03c9\u03c2 \u03ae \u03ac\u03bb\u03bb\u03c9\u03c2, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03b7 \u03c6\u03bf\u03c1\u03ac \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03bb\u03b7\u03c6\u03b8\u03b5\u03af \u03bc\u03b9\u03b1 \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03b1\u03c2 \u03b6\u03b7\u03c4\u03b7\u03b8\u03b5\u03af \u03bd\u03b1 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c4\u03b7\u03c2, \u03b5\u03ac\u03bd \u03c7\u03c1\u03b5\u03b9\u03ac\u03b6\u03b5\u03c4\u03b1\u03b9." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindkey" @@ -25,6 +34,9 @@ }, "description": "\u03a4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03bc\u03b5\u03c4\u03b1\u03b4\u03af\u03b4\u03bf\u03bd\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03b1\u03c6\u03b7\u03bc\u03ad\u03bd\u03b1. \u0393\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03ae \u03c4\u03bf\u03c5\u03c2 \u03c7\u03c1\u03b5\u03b9\u03b1\u03b6\u03cc\u03bc\u03b1\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03ad\u03c3\u03bc\u03b5\u03c5\u03c3\u03b7\u03c2 24 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03bf\u03cd \u03b1\u03c1\u03b9\u03b8\u03bc\u03bf\u03cd." }, + "slow_confirm": { + "description": "\u0394\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b3\u03af\u03bd\u03b5\u03b9 \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7 \u03b1\u03c0\u03cc \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c4\u03b7\u03bd \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1 \u03c3\u03c4\u03b9\u03b3\u03bc\u03ae, \u03b5\u03c0\u03bf\u03bc\u03ad\u03bd\u03c9\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bc\u03b1\u03c3\u03c4\u03b5 \u03c3\u03af\u03b3\u03bf\u03c5\u03c1\u03bf\u03b9 \u03b1\u03bd \u03b1\u03c5\u03c4\u03ae \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03ae \u03cc\u03c7\u03b9. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03bf\u03c6\u03b5\u03af\u03bb\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf \u03cc\u03c4\u03b9 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03ad\u03bd\u03b1 \u03b1\u03c1\u03b3\u03cc \u03b4\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7\u03c2. \u0395\u03c0\u03b9\u03b2\u03b5\u03b2\u03b1\u03b9\u03ce\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bf\u03cd\u03c4\u03c9\u03c2 \u03ae \u03ac\u03bb\u03bb\u03c9\u03c2, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03b7 \u03c6\u03bf\u03c1\u03ac \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03bb\u03b7\u03c6\u03b8\u03b5\u03af \u03bc\u03b9\u03b1 \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03b1\u03c2 \u03b6\u03b7\u03c4\u03b7\u03b8\u03b5\u03af \u03bd\u03b1 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b4\u03b5\u03c3\u03bc\u03b5\u03c5\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c4\u03b7\u03c2, \u03b5\u03ac\u03bd \u03c7\u03c1\u03b5\u03b9\u03ac\u03b6\u03b5\u03c4\u03b1\u03b9." + }, "user": { "data": { "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" diff --git a/homeassistant/components/xiaomi_ble/translations/es.json b/homeassistant/components/xiaomi_ble/translations/es.json new file mode 100644 index 00000000000..504c7bc845c --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/es.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "decryption_failed": "La clave de enlace proporcionada no funcion\u00f3, los datos del sensor no se pudieron descifrar. Por favor, compru\u00e9balo e int\u00e9ntalo de nuevo.", + "expected_24_characters": "Se esperaba una clave de enlace hexadecimal de 24 caracteres.", + "expected_32_characters": "Se esperaba una clave de enlace hexadecimal de 32 caracteres.", + "no_devices_found": "No se encontraron dispositivos en la red", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "decryption_failed": "La clave de enlace proporcionada no funcion\u00f3, los datos del sensor no se pudieron descifrar. Por favor, compru\u00e9balo e int\u00e9ntalo de nuevo.", + "expected_24_characters": "Se esperaba una clave de enlace hexadecimal de 24 caracteres.", + "expected_32_characters": "Se esperaba una clave de enlace hexadecimal de 32 caracteres." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "confirm_slow": { + "description": "No ha habido una transmisi\u00f3n desde este dispositivo en el \u00faltimo minuto, por lo que no estamos seguros de si este dispositivo usa cifrado o no. Esto puede deberse a que el dispositivo utiliza un intervalo de transmisi\u00f3n lento. Confirma para agregar este dispositivo de todos modos, luego, la pr\u00f3xima vez que se reciba una transmisi\u00f3n, se te pedir\u00e1 que ingreses su clave de enlace si es necesario." + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Clave de enlace" + }, + "description": "Los datos del sensor transmitidos por el sensor est\u00e1n cifrados. Para descifrarlos necesitamos una clave de enlace hexadecimal de 32 caracteres." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Clave de enlace" + }, + "description": "Los datos del sensor transmitidos por el sensor est\u00e1n cifrados. Para descifrarlos necesitamos una clave de enlace hexadecimal de 24 caracteres." + }, + "slow_confirm": { + "description": "No ha habido una transmisi\u00f3n desde este dispositivo en el \u00faltimo minuto, por lo que no estamos seguros de si este dispositivo usa cifrado o no. Esto puede deberse a que el dispositivo utiliza un intervalo de transmisi\u00f3n lento. Confirma para agregar este dispositivo de todos modos, luego, la pr\u00f3xima vez que se reciba una transmisi\u00f3n, se te pedir\u00e1 que ingreses su clave de enlace si es necesario." + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/et.json b/homeassistant/components/xiaomi_ble/translations/et.json index 1895097e7b1..41bf99207e2 100644 --- a/homeassistant/components/xiaomi_ble/translations/et.json +++ b/homeassistant/components/xiaomi_ble/translations/et.json @@ -6,13 +6,22 @@ "decryption_failed": "Esitatud sidumisv\u00f5ti ei t\u00f6\u00f6tanud, sensori andmeid ei saanud dekr\u00fcpteerida. Palun kontrolli seda ja proovi uuesti.", "expected_24_characters": "Eeldati 24-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit.", "expected_32_characters": "Eeldati 32-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit.", - "no_devices_found": "V\u00f6rgust seadmeid ei leitud" + "no_devices_found": "V\u00f6rgust seadmeid ei leitud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "decryption_failed": "Esitatud sidumisv\u00f5ti ei t\u00f6\u00f6tanud, sensori andmeid ei saanud dekr\u00fcpteerida. Palun kontrolli seda ja proovi uuesti.", + "expected_24_characters": "Eeldati 24-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit.", + "expected_32_characters": "Eeldati 32-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit." }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Kas seadistada {name}?" }, + "confirm_slow": { + "description": "Sellest seadmest ei ole viimasel minutil \u00fchtegi saadet olnud, nii et me ei ole kindlad, kas see seade kasutab kr\u00fcpteerimist v\u00f5i mitte. See v\u00f5ib olla tingitud sellest, et seade kasutab aeglast saateintervalli. Kinnita, et lisate selle seadme ikkagi, siis j\u00e4rgmisel korral, kui saade saabub, palutakse sisestada selle sidumisv\u00f5ti, kui seda on vaja." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Sidumisv\u00f5ti" @@ -25,6 +34,9 @@ }, "description": "Anduri edastatavad andmed on kr\u00fcpteeritud. Selle dekr\u00fcpteerimiseks vajame 24-m\u00e4rgilist kuueteistk\u00fcmnends\u00fcsteemi sidumisv\u00f5tit." }, + "slow_confirm": { + "description": "Sellest seadmest ei ole viimasel minutil \u00fchtegi saadet olnud, nii et me ei ole kindlad, kas see seade kasutab kr\u00fcpteerimist v\u00f5i mitte. See v\u00f5ib olla tingitud sellest, et seade kasutab aeglast saateintervalli. Kinnita, et lisate selle seadme ikkagi, siis j\u00e4rgmisel korral, kui saade saabub, palutakse sisestada selle sidumisv\u00f5ti, kui seda on vaja." + }, "user": { "data": { "address": "Seade" diff --git a/homeassistant/components/xiaomi_ble/translations/tr.json b/homeassistant/components/xiaomi_ble/translations/tr.json new file mode 100644 index 00000000000..9d1b1931e79 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/tr.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "decryption_failed": "Sa\u011flanan ba\u011flama anahtar\u0131 \u00e7al\u0131\u015fmad\u0131, sens\u00f6r verilerinin \u015fifresi \u00e7\u00f6z\u00fclemedi. L\u00fctfen kontrol edin ve tekrar deneyin.", + "expected_24_characters": "24 karakterlik onalt\u0131l\u0131k bir ba\u011flama anahtar\u0131 bekleniyor.", + "expected_32_characters": "32 karakterlik onalt\u0131l\u0131k bir ba\u011flama anahtar\u0131 bekleniyor.", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "decryption_failed": "Sa\u011flanan ba\u011flama anahtar\u0131 \u00e7al\u0131\u015fmad\u0131, sens\u00f6r verilerinin \u015fifresi \u00e7\u00f6z\u00fclemedi. L\u00fctfen kontrol edin ve tekrar deneyin.", + "expected_24_characters": "24 karakterlik onalt\u0131l\u0131k bir ba\u011flama anahtar\u0131 bekleniyor.", + "expected_32_characters": "32 karakterlik onalt\u0131l\u0131k bir ba\u011flama anahtar\u0131 bekleniyor." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "confirm_slow": { + "description": "Son dakikada bu cihazdan bir yay\u0131n olmad\u0131\u011f\u0131 i\u00e7in bu cihaz\u0131n \u015fifreleme kullan\u0131p kullanmad\u0131\u011f\u0131ndan emin de\u011filiz. Bunun nedeni, cihaz\u0131n yava\u015f bir yay\u0131n aral\u0131\u011f\u0131 kullanmas\u0131 olabilir. Yine de bu cihaz\u0131 eklemeyi onaylay\u0131n, ard\u0131ndan bir sonraki yay\u0131n al\u0131nd\u0131\u011f\u0131nda gerekirse bindkey'i girmeniz istenecektir." + }, + "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Sens\u00f6r taraf\u0131ndan yay\u0131nlanan sens\u00f6r verileri \u015fifrelenmi\u015ftir. \u015eifreyi \u00e7\u00f6zmek i\u00e7in 32 karakterlik onalt\u0131l\u0131k bir ba\u011flama anahtar\u0131na ihtiyac\u0131m\u0131z var." + }, + "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, + "description": "Sens\u00f6r taraf\u0131ndan yay\u0131nlanan sens\u00f6r verileri \u015fifrelenmi\u015ftir. \u015eifreyi \u00e7\u00f6zmek i\u00e7in 24 karakterlik onalt\u0131l\u0131k bir ba\u011flama anahtar\u0131na ihtiyac\u0131m\u0131z var." + }, + "slow_confirm": { + "description": "Son dakikada bu cihazdan bir yay\u0131n olmad\u0131\u011f\u0131 i\u00e7in bu cihaz\u0131n \u015fifreleme kullan\u0131p kullanmad\u0131\u011f\u0131ndan emin de\u011filiz. Bunun nedeni, cihaz\u0131n yava\u015f bir yay\u0131n aral\u0131\u011f\u0131 kullanmas\u0131 olabilir. Yine de bu cihaz\u0131 eklemeyi onaylay\u0131n, ard\u0131ndan bir sonraki yay\u0131n al\u0131nd\u0131\u011f\u0131nda gerekirse bindkey'i girmeniz istenecektir." + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/es.json b/homeassistant/components/xiaomi_miio/translations/es.json index c1a3eae784b..6152da62e47 100644 --- a/homeassistant/components/xiaomi_miio/translations/es.json +++ b/homeassistant/components/xiaomi_miio/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "incomplete_info": "Informaci\u00f3n incompleta para configurar el dispositivo, no se ha suministrado ning\u00fan host o token.", "not_xiaomi_miio": "El dispositivo no es (todav\u00eda) compatible con Xiaomi Miio.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" diff --git a/homeassistant/components/yale_smart_alarm/translations/es.json b/homeassistant/components/yale_smart_alarm/translations/es.json index e9c24aab7f2..59824bfdb3b 100644 --- a/homeassistant/components/yale_smart_alarm/translations/es.json +++ b/homeassistant/components/yale_smart_alarm/translations/es.json @@ -14,7 +14,7 @@ "area_id": "ID de \u00e1rea", "name": "Nombre", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } }, "user": { @@ -22,7 +22,7 @@ "area_id": "ID de \u00e1rea", "name": "Nombre", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/yalexs_ble/translations/ca.json b/homeassistant/components/yalexs_ble/translations/ca.json new file mode 100644 index 00000000000..5b4b014c4ac --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "no_unconfigured_devices": "No s'han trobat dispositius no configurats." + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "Vols configurar {name} per Bluetooth amb l'adre\u00e7a {address}?" + }, + "user": { + "data": { + "address": "Adre\u00e7a Bluetooth", + "key": "Clau fora de l\u00ednia (cadena hexadecimal de 32 bytes)" + }, + "description": "Consulta la documentaci\u00f3 a {docs_url} per saber com trobar la clau de fora de l\u00ednia." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/es.json b/homeassistant/components/yalexs_ble/translations/es.json new file mode 100644 index 00000000000..32863d750d2 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/es.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "no_devices_found": "No se encontraron dispositivos en la red", + "no_unconfigured_devices": "No se encontraron dispositivos no configurados." + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_key_format": "La clave sin conexi\u00f3n debe ser una cadena hexadecimal de 32 bytes.", + "invalid_key_index": "La ranura de la clave sin conexi\u00f3n debe ser un n\u00famero entero entre 0 y 255.", + "unknown": "Error inesperado" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "\u00bfQuieres configurar {name} a trav\u00e9s de Bluetooth con la direcci\u00f3n {address}?" + }, + "user": { + "data": { + "address": "Direcci\u00f3n Bluetooth", + "key": "Clave sin conexi\u00f3n (cadena hexadecimal de 32 bytes)", + "slot": "Ranura de clave sin conexi\u00f3n (entero entre 0 y 255)" + }, + "description": "Consulta la documentaci\u00f3n en {docs_url} para saber c\u00f3mo encontrar la clave sin conexi\u00f3n." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/fr.json b/homeassistant/components/yalexs_ble/translations/fr.json new file mode 100644 index 00000000000..7ec2edfc86e --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "no_unconfigured_devices": "Aucun appareil non configur\u00e9 n'a \u00e9t\u00e9 trouv\u00e9." + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Adresse Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/hu.json b/homeassistant/components/yalexs_ble/translations/hu.json new file mode 100644 index 00000000000..44e14971cc7 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/hu.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "no_unconfigured_devices": "Nem tal\u00e1lhat\u00f3 konfigur\u00e1latlan eszk\u00f6z." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_key_format": "Az offline kulcsnak 32 b\u00e1jtos hexadecim\u00e1lis karakterl\u00e1ncnak kell lennie.", + "invalid_key_index": "Az offline kulcshelynek 0 \u00e9s 255 k\u00f6z\u00f6tti eg\u00e9sz sz\u00e1mnak kell lennie.", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "integration_discovery_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {nane}, Bluetooth-on kereszt\u00fcl, {address} c\u00edmmel?" + }, + "user": { + "data": { + "address": "Bluetooth-c\u00edm", + "key": "Offline kulcs (32 b\u00e1jtos hexa karakterl\u00e1nc)", + "slot": "Offline kulcshely (eg\u00e9sz sz\u00e1m 0 \u00e9s 255 k\u00f6z\u00f6tt)" + }, + "description": "Tekintse meg a {docs_url} c\u00edmen tal\u00e1lhat\u00f3 dokument\u00e1ci\u00f3t, hogy hogyan szerezze meg az offline kulcsot." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/pt-BR.json b/homeassistant/components/yalexs_ble/translations/pt-BR.json new file mode 100644 index 00000000000..68a52803cbe --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/pt-BR.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "no_unconfigured_devices": "Nenhum dispositivo n\u00e3o configurado encontrado." + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_key_format": "A chave offline deve ser uma string hexadecimal de 32 bytes.", + "invalid_key_index": "O slot de chave offline deve ser um n\u00famero inteiro entre 0 e 255.", + "unknown": "Erro inesperado" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "Deseja configurar {name} por Bluetooth com o endere\u00e7o {address}?" + }, + "user": { + "data": { + "address": "Endere\u00e7o Bluetooth", + "key": "Chave offline (sequ\u00eancia hexadecimal de 32 bytes)", + "slot": "Slot de chave offline (inteiro entre 0 e 255)" + }, + "description": "Verifique a documenta\u00e7\u00e3o em {docs_url} para saber como encontrar a chave off-line." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yolink/translations/es.json b/homeassistant/components/yolink/translations/es.json index 71df6ac31e3..c4bcfa6e75b 100644 --- a/homeassistant/components/yolink/translations/es.json +++ b/homeassistant/components/yolink/translations/es.json @@ -2,23 +2,23 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en progreso", - "authorize_url_timeout": "Tiempo de espera para generar la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Por favor, siga la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [compruebe la secci\u00f3n de ayuda]({docs_url})", - "oauth_error": "Se han recibido datos no v\u00e1lidos del token.", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "create_entry": { - "default": "Autentificado con \u00e9xito" + "default": "Autenticado correctamente" }, "step": { "pick_implementation": { - "title": "Elija el m\u00e9todo de autenticaci\u00f3n" + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" }, "reauth_confirm": { - "description": "La integraci\u00f3n de yolink necesita volver a autenticar su cuenta", - "title": "Integraci\u00f3n de la reautenticaci\u00f3n" + "description": "La integraci\u00f3n yolink necesita volver a autenticar tu cuenta", + "title": "Volver a autenticar la integraci\u00f3n" } } } diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index d8162401f3d..9056708fd96 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -46,10 +46,13 @@ "title": "Opciones del panel de control de la alarma" }, "zha_options": { + "always_prefer_xy_color_mode": "Preferir siempre el modo de color XY", "consider_unavailable_battery": "Considere que los dispositivos alimentados por bater\u00eda no est\u00e1n disponibles despu\u00e9s de (segundos)", "consider_unavailable_mains": "Considere que los dispositivos alimentados por la red el\u00e9ctrica no est\u00e1n disponibles despu\u00e9s de (segundos)", "default_light_transition": "Tiempo de transici\u00f3n de la luz por defecto (segundos)", "enable_identify_on_join": "Activar el efecto de identificaci\u00f3n cuando los dispositivos se unen a la red", + "enhanced_light_transition": "Habilitar la transici\u00f3n mejorada de color de luz/temperatura desde un estado apagado", + "light_transitioning_flag": "Habilitar el control deslizante de brillo mejorado durante la transici\u00f3n de luz", "title": "Opciones globales" } }, diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index 567ee5f359c..cbcc3adfa51 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -46,11 +46,13 @@ "title": "Valvekeskuse juhtpaneeli s\u00e4tted" }, "zha_options": { + "always_prefer_xy_color_mode": "Eelista alati XY v\u00e4rvire\u017eiimi", "consider_unavailable_battery": "Arvesta, et patareitoitega seadmed pole p\u00e4rast (sekundit) saadaval", "consider_unavailable_mains": "Arvesta, et v\u00f5rgutoitega seadmed pole p\u00e4rast (sekundit) saadaval", "default_light_transition": "Heleduse vaike\u00fclemineku aeg (sekundites)", "enable_identify_on_join": "Luba tuvastamine kui seadmed liituvad v\u00f5rguga", "enhanced_light_transition": "Luba t\u00e4iustatud valguse v\u00e4rvi/temperatuuri \u00fcleminek v\u00e4ljal\u00fclitatud olekust", + "light_transitioning_flag": "Luba t\u00e4iustatud heleduse liugur valguse \u00fclemineku ajal", "title": "\u00dcldised valikud" } }, diff --git a/homeassistant/components/zha/translations/tr.json b/homeassistant/components/zha/translations/tr.json index 39eb68d8d53..391b9315b48 100644 --- a/homeassistant/components/zha/translations/tr.json +++ b/homeassistant/components/zha/translations/tr.json @@ -46,10 +46,13 @@ "title": "Alarm Kontrol Paneli Se\u00e7enekleri" }, "zha_options": { + "always_prefer_xy_color_mode": "Her zaman XY renk modunu tercih edin", "consider_unavailable_battery": "Pille \u00e7al\u0131\u015fan ayg\u0131tlar\u0131n kullan\u0131lamad\u0131\u011f\u0131n\u0131 g\u00f6z \u00f6n\u00fcnde bulundurun (saniye)", "consider_unavailable_mains": "\u015eebekeyle \u00e7al\u0131\u015fan ayg\u0131tlar\u0131n kullan\u0131lamad\u0131\u011f\u0131n\u0131 g\u00f6z \u00f6n\u00fcnde bulundurun (saniye)", "default_light_transition": "Varsay\u0131lan \u0131\u015f\u0131k ge\u00e7i\u015f s\u00fcresi (saniye)", "enable_identify_on_join": "Cihazlar a\u011fa kat\u0131ld\u0131\u011f\u0131nda tan\u0131mlama efektini etkinle\u015ftir", + "enhanced_light_transition": "Kapal\u0131 durumdan geli\u015fmi\u015f \u0131\u015f\u0131k rengi/s\u0131cakl\u0131\u011f\u0131 ge\u00e7i\u015fini etkinle\u015ftirme", + "light_transitioning_flag": "I\u015f\u0131k ge\u00e7i\u015fi s\u0131ras\u0131nda geli\u015fmi\u015f parlakl\u0131k kayd\u0131r\u0131c\u0131s\u0131n\u0131 etkinle\u015ftirin", "title": "Genel Se\u00e7enekler" } }, diff --git a/homeassistant/components/zoneminder/translations/es.json b/homeassistant/components/zoneminder/translations/es.json index 6e12f9fd8fe..4425b179ccb 100644 --- a/homeassistant/components/zoneminder/translations/es.json +++ b/homeassistant/components/zoneminder/translations/es.json @@ -24,8 +24,8 @@ "path": "Ruta ZM", "path_zms": "Ruta ZMS", "ssl": "Utiliza un certificado SSL", - "username": "Usuario", - "verify_ssl": "Verificar certificado SSL" + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL" }, "title": "A\u00f1adir Servidor ZoneMinder" } diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json index 5d3efb0e7f4..3101d804b43 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -7,7 +7,7 @@ "addon_set_config_failed": "Fallo en la configuraci\u00f3n de Z-Wave JS.", "addon_start_failed": "No se ha podido iniciar el complemento Z-Wave JS.", "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", "discovery_requires_supervisor": "El descubrimiento requiere del supervisor.", "not_zwave_device": "El dispositivo descubierto no es un dispositivo Z-Wave." @@ -60,7 +60,7 @@ "description": "\u00bfQuieres configurar {name} con el complemento Z-Wave JS?" }, "zeroconf_confirm": { - "description": "\u00bfQuieres a\u00f1adir el servidor Z-Wave JS con ID {home_id} que se encuentra en {url} en Home Assistant?", + "description": "\u00bfQuieres a\u00f1adir el servidor Z-Wave JS con el ID de casa {home_id} que se encuentra en {url} a Home Assistant?", "title": "Servidor Z-Wave JS descubierto" } } From 8ecbb858524145dd7bef9be74a3d8fc9d8366c1c Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 10 Aug 2022 21:05:09 -0400 Subject: [PATCH 3271/3516] Use generators for async_add_entities in Ambient Station (#76586) --- .../components/ambient_station/binary_sensor.py | 14 ++++++-------- homeassistant/components/ambient_station/sensor.py | 10 ++++------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index 4380e1839f2..bcc2ae60404 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -333,14 +333,12 @@ async def async_setup_entry( ambient = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - AmbientWeatherBinarySensor( - ambient, mac_address, station[ATTR_NAME], description - ) - for mac_address, station in ambient.stations.items() - for description in BINARY_SENSOR_DESCRIPTIONS - if description.key in station[ATTR_LAST_DATA] - ] + AmbientWeatherBinarySensor( + ambient, mac_address, station[ATTR_NAME], description + ) + for mac_address, station in ambient.stations.items() + for description in BINARY_SENSOR_DESCRIPTIONS + if description.key in station[ATTR_LAST_DATA] ) diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 4eae53b6a03..1a51e57bfa3 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -646,12 +646,10 @@ async def async_setup_entry( ambient = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - AmbientWeatherSensor(ambient, mac_address, station[ATTR_NAME], description) - for mac_address, station in ambient.stations.items() - for description in SENSOR_DESCRIPTIONS - if description.key in station[ATTR_LAST_DATA] - ] + AmbientWeatherSensor(ambient, mac_address, station[ATTR_NAME], description) + for mac_address, station in ambient.stations.items() + for description in SENSOR_DESCRIPTIONS + if description.key in station[ATTR_LAST_DATA] ) From dbfba3a951c505372594b4a7cf8f91ff6c84f3db Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 11 Aug 2022 03:24:12 +0200 Subject: [PATCH 3272/3516] Remove attribution from extra state attributes (#76580) --- .../components/brottsplatskartan/sensor.py | 14 ++++---------- homeassistant/components/fixer/sensor.py | 5 +++-- homeassistant/components/gitlab_ci/sensor.py | 11 +++-------- homeassistant/components/ring/camera.py | 4 ++-- homeassistant/components/rmvtransport/sensor.py | 5 +++-- homeassistant/components/speedtestdotnet/sensor.py | 4 ++-- 6 files changed, 17 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index 1a321b3f173..171986ffd1c 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -10,12 +10,7 @@ import brottsplatskartan import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, -) +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -90,6 +85,8 @@ def setup_platform( class BrottsplatskartanSensor(SensorEntity): """Representation of a Brottsplatskartan Sensor.""" + _attr_attribution = brottsplatskartan.ATTRIBUTION + def __init__(self, bpk, name): """Initialize the Brottsplatskartan sensor.""" self._brottsplatskartan = bpk @@ -109,8 +106,5 @@ class BrottsplatskartanSensor(SensorEntity): incident_type = incident.get("title_type") incident_counts[incident_type] += 1 - self._attr_extra_state_attributes = { - ATTR_ATTRIBUTION: brottsplatskartan.ATTRIBUTION - } - self._attr_extra_state_attributes.update(incident_counts) + self._attr_extra_state_attributes = incident_counts self._attr_native_value = len(incidents) diff --git a/homeassistant/components/fixer/sensor.py b/homeassistant/components/fixer/sensor.py index a4260efeda7..c05bd5a756f 100644 --- a/homeassistant/components/fixer/sensor.py +++ b/homeassistant/components/fixer/sensor.py @@ -9,7 +9,7 @@ from fixerio.exceptions import FixerioException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME, CONF_TARGET +from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_TARGET from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -61,6 +61,8 @@ def setup_platform( class ExchangeRateSensor(SensorEntity): """Representation of a Exchange sensor.""" + _attr_attribution = ATTRIBUTION + def __init__(self, data, name, target): """Initialize the sensor.""" self.data = data @@ -88,7 +90,6 @@ class ExchangeRateSensor(SensorEntity): """Return the state attributes.""" if self.data.rate is not None: return { - ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_EXCHANGE_RATE: self.data.rate["rates"][self._target], ATTR_TARGET: self._target, } diff --git a/homeassistant/components/gitlab_ci/sensor.py b/homeassistant/components/gitlab_ci/sensor.py index e9dd4991d71..4206a1184ad 100644 --- a/homeassistant/components/gitlab_ci/sensor.py +++ b/homeassistant/components/gitlab_ci/sensor.py @@ -8,13 +8,7 @@ from gitlab import Gitlab, GitlabAuthenticationError, GitlabGetError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_NAME, - CONF_SCAN_INTERVAL, - CONF_TOKEN, - CONF_URL, -) +from homeassistant.const import CONF_NAME, CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_URL from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -78,6 +72,8 @@ def setup_platform( class GitLabSensor(SensorEntity): """Representation of a GitLab sensor.""" + _attr_attribution = ATTRIBUTION + def __init__(self, gitlab_data, name): """Initialize the GitLab sensor.""" self._available = False @@ -111,7 +107,6 @@ class GitLabSensor(SensorEntity): def extra_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_BUILD_STATUS: self._state, ATTR_BUILD_STARTED: self._started_at, ATTR_BUILD_FINISHED: self._finished_at, diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index da2e447869a..72d8a51f01e 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -11,7 +11,6 @@ import requests from homeassistant.components import ffmpeg from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -49,6 +48,8 @@ async def async_setup_entry( class RingCam(RingEntityMixin, Camera): """An implementation of a Ring Door Bell camera.""" + _attr_attribution = ATTRIBUTION + def __init__(self, config_entry_id, ffmpeg_manager, device): """Initialize a Ring Door Bell camera.""" super().__init__(config_entry_id, device) @@ -105,7 +106,6 @@ class RingCam(RingEntityMixin, Camera): def extra_state_attributes(self): """Return the state attributes.""" return { - ATTR_ATTRIBUTION: ATTRIBUTION, "video_url": self._video_url, "last_video_id": self._last_video_id, } diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index 92455329dea..3394c4ef4d3 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -13,7 +13,7 @@ from RMVtransport.rmvtransport import ( import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_TIMEOUT, TIME_MINUTES +from homeassistant.const import CONF_NAME, CONF_TIMEOUT, TIME_MINUTES from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -116,6 +116,8 @@ async def async_setup_platform( class RMVDepartureSensor(SensorEntity): """Implementation of an RMV departure sensor.""" + _attr_attribution = ATTRIBUTION + def __init__( self, station, @@ -170,7 +172,6 @@ class RMVDepartureSensor(SensorEntity): "minutes": self.data.departures[0].get("minutes"), "departure_time": self.data.departures[0].get("departure_time"), "product": self.data.departures[0].get("product"), - ATTR_ATTRIBUTION: ATTRIBUTION, } except IndexError: return {} diff --git a/homeassistant/components/speedtestdotnet/sensor.py b/homeassistant/components/speedtestdotnet/sensor.py index 44b018e1e19..e1ba0f560bb 100644 --- a/homeassistant/components/speedtestdotnet/sensor.py +++ b/homeassistant/components/speedtestdotnet/sensor.py @@ -5,7 +5,6 @@ from typing import Any, cast from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo @@ -49,6 +48,7 @@ class SpeedtestSensor( """Implementation of a speedtest.net sensor.""" entity_description: SpeedtestSensorEntityDescription + _attr_attribution = ATTRIBUTION _attr_has_entity_name = True _attr_icon = ICON @@ -62,7 +62,7 @@ class SpeedtestSensor( self.entity_description = description self._attr_unique_id = description.key self._state: StateType = None - self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + self._attrs: dict[str, Any] = {} self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)}, name=DEFAULT_NAME, From 6e65cb4928301a8cf5ccc4e49d7ccdcd392f5f9f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 11 Aug 2022 03:27:52 +0200 Subject: [PATCH 3273/3516] Fix Spotify deviding None value in current progress (#76581) --- homeassistant/components/spotify/media_player.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index ea41067e7e9..3db8ae7de08 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -179,7 +179,10 @@ class SpotifyMediaPlayer(MediaPlayerEntity): @property def media_position(self) -> int | None: """Position of current playing media in seconds.""" - if not self._currently_playing: + if ( + not self._currently_playing + or self._currently_playing.get("progress_ms") is None + ): return None return self._currently_playing["progress_ms"] / 1000 From 828ea99c61c170bb096e6d64cb3fb6042d6ec920 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Aug 2022 15:34:48 -1000 Subject: [PATCH 3274/3516] Add door sensors to Yale Access Bluetooth (#76571) --- .coveragerc | 1 + .../components/yalexs_ble/__init__.py | 2 +- .../components/yalexs_ble/binary_sensor.py | 41 +++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/yalexs_ble/binary_sensor.py diff --git a/.coveragerc b/.coveragerc index 2616f4b6e16..4e88d171d57 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1484,6 +1484,7 @@ omit = homeassistant/components/xmpp/notify.py homeassistant/components/xs1/* homeassistant/components/yalexs_ble/__init__.py + homeassistant/components/yalexs_ble/binary_sensor.py homeassistant/components/yalexs_ble/entity.py homeassistant/components/yalexs_ble/lock.py homeassistant/components/yalexs_ble/util.py diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index 5a1cf461e5d..e38dba00ef8 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -16,7 +16,7 @@ from .const import CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DEVICE_TIMEOUT, DOMAIN from .models import YaleXSBLEData from .util import async_find_existing_service_info, bluetooth_callback_matcher -PLATFORMS: list[Platform] = [Platform.LOCK] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.LOCK] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/yalexs_ble/binary_sensor.py b/homeassistant/components/yalexs_ble/binary_sensor.py new file mode 100644 index 00000000000..3ee88dbaa5e --- /dev/null +++ b/homeassistant/components/yalexs_ble/binary_sensor.py @@ -0,0 +1,41 @@ +"""Support for yalexs ble binary sensors.""" +from __future__ import annotations + +from yalexs_ble import ConnectionInfo, DoorStatus, LockInfo, LockState + +from homeassistant import config_entries +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import YALEXSBLEEntity +from .models import YaleXSBLEData + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up YALE XS binary sensors.""" + data: YaleXSBLEData = hass.data[DOMAIN][entry.entry_id] + async_add_entities([YaleXSBLEDoorSensor(data)]) + + +class YaleXSBLEDoorSensor(YALEXSBLEEntity, BinarySensorEntity): + """Yale XS BLE binary sensor.""" + + _attr_device_class = BinarySensorDeviceClass.DOOR + _attr_has_entity_name = True + + @callback + def _async_update_state( + self, new_state: LockState, lock_info: LockInfo, connection_info: ConnectionInfo + ) -> None: + """Update the state.""" + self._attr_is_on = new_state.door == DoorStatus.OPENED + super()._async_update_state(new_state, lock_info, connection_info) From 5523b6fc9fdeea80bbd0dcd437fc3d4c9e5c9214 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 11 Aug 2022 02:35:05 +0100 Subject: [PATCH 3275/3516] Fix homekit_controller not noticing ip and port changes that zeroconf has found (#76570) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index cf3069e3b0d..4bd9a0b70f9 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.8"], + "requirements": ["aiohomekit==1.2.9"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 1927739987c..b793c357631 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.8 +aiohomekit==1.2.9 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72bf0741f25..5bb38db054b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.8 +aiohomekit==1.2.9 # homeassistant.components.emulated_hue # homeassistant.components.http From 420084f6f17c8229b96fb69760d1daf5e39e2d3b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 11 Aug 2022 03:35:13 +0200 Subject: [PATCH 3276/3516] Update sentry-sdk to 1.9.3 (#76573) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 0cbe9eb636f..2360180d1cf 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.9.2"], + "requirements": ["sentry-sdk==1.9.3"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index b793c357631..c5653e757b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2173,7 +2173,7 @@ sense_energy==0.10.4 sensorpush-ble==1.5.1 # homeassistant.components.sentry -sentry-sdk==1.9.2 +sentry-sdk==1.9.3 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5bb38db054b..67d73001634 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1470,7 +1470,7 @@ sense_energy==0.10.4 sensorpush-ble==1.5.1 # homeassistant.components.sentry -sentry-sdk==1.9.2 +sentry-sdk==1.9.3 # homeassistant.components.sharkiq sharkiq==0.0.1 From 4c701294270995044f6b003dde6b67df3b424945 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 11 Aug 2022 03:36:13 +0200 Subject: [PATCH 3277/3516] Improve state attributes of CityBikes (#76578) --- homeassistant/components/citybikes/sensor.py | 21 ++++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index 5d833a113f2..418e206fc36 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -15,7 +15,6 @@ from homeassistant.components.sensor import ( SensorEntity, ) from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_ID, ATTR_LATITUDE, ATTR_LOCATION, @@ -275,6 +274,7 @@ class CityBikesNetwork: class CityBikesStation(SensorEntity): """CityBikes API Sensor.""" + _attr_attribution = CITYBIKES_ATTRIBUTION _attr_native_unit_of_measurement = "bikes" _attr_icon = "mdi:bike" @@ -292,15 +292,10 @@ class CityBikesStation(SensorEntity): break self._attr_name = station_data.get(ATTR_NAME) self._attr_native_value = station_data.get(ATTR_FREE_BIKES) - self._attr_extra_state_attributes = ( - { - ATTR_ATTRIBUTION: CITYBIKES_ATTRIBUTION, - ATTR_UID: station_data.get(ATTR_EXTRA, {}).get(ATTR_UID), - ATTR_LATITUDE: station_data[ATTR_LATITUDE], - ATTR_LONGITUDE: station_data[ATTR_LONGITUDE], - ATTR_EMPTY_SLOTS: station_data[ATTR_EMPTY_SLOTS], - ATTR_TIMESTAMP: station_data[ATTR_TIMESTAMP], - } - if station_data - else {ATTR_ATTRIBUTION: CITYBIKES_ATTRIBUTION} - ) + self._attr_extra_state_attributes = { + ATTR_UID: station_data.get(ATTR_EXTRA, {}).get(ATTR_UID), + ATTR_LATITUDE: station_data.get(ATTR_LATITUDE), + ATTR_LONGITUDE: station_data.get(ATTR_LONGITUDE), + ATTR_EMPTY_SLOTS: station_data.get(ATTR_EMPTY_SLOTS), + ATTR_TIMESTAMP: station_data.get(ATTR_TIMESTAMP), + } From bf899101ce756e2b62936ed3b5d7a410c61b7e3a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Aug 2022 16:21:41 -1000 Subject: [PATCH 3278/3516] Update offline keys from august cloud for august branded yale locks (#76577) --- homeassistant/components/august/__init__.py | 34 ++++++ homeassistant/components/august/manifest.json | 3 +- .../components/yalexs_ble/__init__.py | 25 ++++- .../fixtures/get_lock.doorsense_init.json | 10 +- .../fixtures/get_lock.low_keypad_battery.json | 10 +- .../august/fixtures/get_lock.offline.json | 10 +- .../august/fixtures/get_lock.online.json | 10 +- .../fixtures/get_lock.online_with_keys.json | 100 ++++++++++++++++++ tests/components/august/mocks.py | 4 + tests/components/august/test_init.py | 26 +++++ 10 files changed, 194 insertions(+), 38 deletions(-) create mode 100644 tests/components/august/fixtures/get_lock.online_with_keys.json diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 81842f995e8..f4a0f57eb76 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -13,6 +13,7 @@ from yalexs.lock import Lock, LockDetail from yalexs.pubnub_activity import activities_from_pubnub_message from yalexs.pubnub_async import AugustPubNub, async_create_pubnub +from homeassistant.components import yalexs_ble from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant, callback @@ -93,6 +94,26 @@ async def async_setup_august( return True +@callback +def _async_trigger_ble_lock_discovery( + hass: HomeAssistant, locks_with_offline_keys: list[LockDetail] +): + """Update keys for the yalexs-ble integration if available.""" + for lock_detail in locks_with_offline_keys: + yalexs_ble.async_discovery( + hass, + yalexs_ble.YaleXSBLEDiscovery( + { + "name": lock_detail.device_name, + "address": lock_detail.mac_address, + "serial": lock_detail.serial_number, + "key": lock_detail.offline_key, + "slot": lock_detail.offline_slot, + } + ), + ) + + class AugustData(AugustSubscriberMixin): """August data object.""" @@ -133,6 +154,19 @@ class AugustData(AugustSubscriberMixin): # detail as we cannot determine if they are usable. # This also allows us to avoid checking for # detail being None all over the place + + # Currently we know how to feed data to yalexe_ble + # but we do not know how to send it to homekit_controller + # yet + _async_trigger_ble_lock_discovery( + self._hass, + [ + lock_detail + for lock_detail in self._device_detail_by_id.values() + if isinstance(lock_detail, LockDetail) and lock_detail.offline_key + ], + ) + self._remove_inoperative_locks() self._remove_inoperative_doorbells() diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 418fa6920ad..c688aa1a775 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -24,5 +24,6 @@ ], "config_flow": true, "iot_class": "cloud_push", - "loggers": ["pubnub", "yalexs"] + "loggers": ["pubnub", "yalexs"], + "after_dependencies": ["yalexs_ble"] } diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index e38dba00ef8..265b10a502b 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -2,12 +2,13 @@ from __future__ import annotations import asyncio +from typing import TypedDict import async_timeout from yalexs_ble import PushLock, local_name_is_unique from homeassistant.components import bluetooth -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry from homeassistant.const import CONF_ADDRESS, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -19,6 +20,28 @@ from .util import async_find_existing_service_info, bluetooth_callback_matcher PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.LOCK] +class YaleXSBLEDiscovery(TypedDict): + """A validated discovery of a Yale XS BLE device.""" + + name: str + address: str + serial: str + key: str + slot: int + + +@callback +def async_discovery(hass: HomeAssistant, discovery: YaleXSBLEDiscovery) -> None: + """Update keys for the yalexs-ble integration if available.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + "yalexs_ble", + context={"source": SOURCE_INTEGRATION_DISCOVERY}, + data=discovery, + ) + ) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Yale Access Bluetooth from a config entry.""" local_name = entry.data[CONF_LOCAL_NAME] diff --git a/tests/components/august/fixtures/get_lock.doorsense_init.json b/tests/components/august/fixtures/get_lock.doorsense_init.json index d85ca3b153f..1132cc61a8d 100644 --- a/tests/components/august/fixtures/get_lock.doorsense_init.json +++ b/tests/components/august/fixtures/get_lock.doorsense_init.json @@ -40,15 +40,7 @@ }, "OfflineKeys": { "created": [], - "loaded": [ - { - "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", - "slot": 1, - "key": "kkk01d4300c1dcxxx1c330f794941111", - "created": "2017-12-10T03:12:09.215Z", - "loaded": "2017-12-10T03:12:54.391Z" - } - ], + "loaded": [], "deleted": [], "loadedhk": [ { diff --git a/tests/components/august/fixtures/get_lock.low_keypad_battery.json b/tests/components/august/fixtures/get_lock.low_keypad_battery.json index b10c3f2600f..08bdfaa76ed 100644 --- a/tests/components/august/fixtures/get_lock.low_keypad_battery.json +++ b/tests/components/august/fixtures/get_lock.low_keypad_battery.json @@ -40,15 +40,7 @@ }, "OfflineKeys": { "created": [], - "loaded": [ - { - "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", - "slot": 1, - "key": "kkk01d4300c1dcxxx1c330f794941111", - "created": "2017-12-10T03:12:09.215Z", - "loaded": "2017-12-10T03:12:54.391Z" - } - ], + "loaded": [], "deleted": [], "loadedhk": [ { diff --git a/tests/components/august/fixtures/get_lock.offline.json b/tests/components/august/fixtures/get_lock.offline.json index 753a1081918..50d3d345ef8 100644 --- a/tests/components/august/fixtures/get_lock.offline.json +++ b/tests/components/august/fixtures/get_lock.offline.json @@ -19,15 +19,7 @@ } ], "deleted": [], - "loaded": [ - { - "UserID": "userid", - "created": "2000-00-00T00:00:00.447Z", - "key": "key", - "loaded": "2000-00-00T00:00:00.447Z", - "slot": 1 - } - ] + "loaded": [] }, "SerialNumber": "ABC", "Type": 3, diff --git a/tests/components/august/fixtures/get_lock.online.json b/tests/components/august/fixtures/get_lock.online.json index 7fa12fa8bcb..7abadeef4b6 100644 --- a/tests/components/august/fixtures/get_lock.online.json +++ b/tests/components/august/fixtures/get_lock.online.json @@ -40,15 +40,7 @@ }, "OfflineKeys": { "created": [], - "loaded": [ - { - "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", - "slot": 1, - "key": "kkk01d4300c1dcxxx1c330f794941111", - "created": "2017-12-10T03:12:09.215Z", - "loaded": "2017-12-10T03:12:54.391Z" - } - ], + "loaded": [], "deleted": [], "loadedhk": [ { diff --git a/tests/components/august/fixtures/get_lock.online_with_keys.json b/tests/components/august/fixtures/get_lock.online_with_keys.json new file mode 100644 index 00000000000..7fa12fa8bcb --- /dev/null +++ b/tests/components/august/fixtures/get_lock.online_with_keys.json @@ -0,0 +1,100 @@ +{ + "LockName": "Front Door Lock", + "Type": 2, + "Created": "2017-12-10T03:12:09.210Z", + "Updated": "2017-12-10T03:12:09.210Z", + "LockID": "A6697750D607098BAE8D6BAA11EF8063", + "HouseID": "000000000000", + "HouseName": "My House", + "Calibrated": false, + "skuNumber": "AUG-SL02-M02-S02", + "timeZone": "America/Vancouver", + "battery": 0.88, + "SerialNumber": "X2FSW05DGA", + "LockStatus": { + "status": "locked", + "doorState": "closed", + "dateTime": "2017-12-10T04:48:30.272Z", + "isLockStatusChanged": true, + "valid": true + }, + "currentFirmwareVersion": "109717e9-3.0.44-3.0.30", + "homeKitEnabled": false, + "zWaveEnabled": false, + "isGalileo": false, + "Bridge": { + "_id": "aaacab87f7efxa0015884999", + "mfgBridgeID": "AAGPP102XX", + "deviceModel": "august-doorbell", + "firmwareVersion": "2.3.0-RC153+201711151527", + "operative": true + }, + "keypad": { + "_id": "5bc65c24e6ef2a263e1450a8", + "serialNumber": "K1GXB0054Z", + "lockID": "92412D1B44004595B5DEB134E151A8D3", + "currentFirmwareVersion": "2.27.0", + "battery": {}, + "batteryLevel": "Medium", + "batteryRaw": 170 + }, + "OfflineKeys": { + "created": [], + "loaded": [ + { + "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", + "slot": 1, + "key": "kkk01d4300c1dcxxx1c330f794941111", + "created": "2017-12-10T03:12:09.215Z", + "loaded": "2017-12-10T03:12:54.391Z" + } + ], + "deleted": [], + "loadedhk": [ + { + "key": "kkk01d4300c1dcxxx1c330f794941222", + "slot": 256, + "UserID": "cccca94e-373e-aaaa-bbbb-333396827777", + "created": "2017-12-10T03:12:09.218Z", + "loaded": "2017-12-10T03:12:55.563Z" + } + ] + }, + "parametersToSet": {}, + "users": { + "cccca94e-373e-aaaa-bbbb-333396827777": { + "UserType": "superuser", + "FirstName": "Foo", + "LastName": "Bar", + "identifiers": ["email:foo@bar.com", "phone:+177777777777"], + "imageInfo": { + "original": { + "width": 948, + "height": 949, + "format": "jpg", + "url": "http://www.image.com/foo.jpeg", + "secure_url": "https://www.image.com/foo.jpeg" + }, + "thumbnail": { + "width": 128, + "height": 128, + "format": "jpg", + "url": "http://www.image.com/foo.jpeg", + "secure_url": "https://www.image.com/foo.jpeg" + } + } + } + }, + "pubsubChannel": "3333a674-ffff-aaaa-b351-b3a4473f3333", + "ruleHash": {}, + "cameras": [], + "geofenceLimits": { + "ios": { + "debounceInterval": 90, + "gpsAccuracyMultiplier": 2.5, + "maximumGeofence": 5000, + "minimumGeofence": 100, + "minGPSAccuracyRequired": 80 + } + } +} diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 932065f37da..2fa59fe964c 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -302,6 +302,10 @@ async def _mock_operative_august_lock_detail(hass): return await _mock_lock_from_fixture(hass, "get_lock.online.json") +async def _mock_lock_with_offline_key(hass): + return await _mock_lock_from_fixture(hass, "get_lock.online_with_keys.json") + + async def _mock_inoperative_august_lock_detail(hass): return await _mock_lock_from_fixture(hass, "get_lock.offline.json") diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index 56113832d23..ab3269e9ac8 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -29,6 +29,7 @@ from tests.components.august.mocks import ( _mock_doorsense_missing_august_lock_detail, _mock_get_config, _mock_inoperative_august_lock_detail, + _mock_lock_with_offline_key, _mock_operative_august_lock_detail, ) @@ -323,6 +324,31 @@ async def test_load_unload(hass): await hass.async_block_till_done() +async def test_load_triggers_ble_discovery(hass): + """Test that loading a lock that supports offline ble operation passes the keys to yalexe_ble.""" + + august_lock_with_key = await _mock_lock_with_offline_key(hass) + august_lock_without_key = await _mock_operative_august_lock_detail(hass) + + with patch( + "homeassistant.components.august.yalexs_ble.async_discovery" + ) as mock_discovery: + config_entry = await _create_august_with_devices( + hass, [august_lock_with_key, august_lock_without_key] + ) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.LOADED + + assert len(mock_discovery.mock_calls) == 1 + assert mock_discovery.mock_calls[0][1][1] == { + "name": "Front Door Lock", + "address": None, + "serial": "X2FSW05DGA", + "key": "kkk01d4300c1dcxxx1c330f794941111", + "slot": 1, + } + + async def remove_device(ws_client, device_id, config_entry_id): """Remove config entry from a device.""" await ws_client.send_json( From 8f0ade7a68969a281a4cd2aa81d68d2dc8f8438f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Aug 2022 21:34:23 -1000 Subject: [PATCH 3279/3516] Bump yalexs-ble to 1.1.3 (#76595) --- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 8f7838792ff..0bf861e4d44 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.1.2"], + "requirements": ["yalexs-ble==1.1.3"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], diff --git a/requirements_all.txt b/requirements_all.txt index c5653e757b0..2cccec261c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2500,7 +2500,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.1.2 +yalexs-ble==1.1.3 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 67d73001634..7222a1778bd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1695,7 +1695,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.1.2 +yalexs-ble==1.1.3 # homeassistant.components.august yalexs==1.2.1 From 7fc2a73c8806ce13d970c9f155a8c43bfba08590 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 11 Aug 2022 09:50:35 +0200 Subject: [PATCH 3280/3516] Improve type hints in harmony (#76445) --- homeassistant/components/harmony/select.py | 27 ++++++++++--------- .../components/harmony/subscriber.py | 20 +++++++------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/harmony/select.py b/homeassistant/components/harmony/select.py index e19f6c3bc9c..3728c4b17a4 100644 --- a/homeassistant/components/harmony/select.py +++ b/homeassistant/components/harmony/select.py @@ -40,7 +40,7 @@ class HarmonyActivitySelect(HarmonyEntity, SelectEntity): self._attr_name = name @property - def icon(self): + def icon(self) -> str: """Return a representative icon.""" if not self.available or self.current_option == ACTIVITY_POWER_OFF: return "mdi:remote-tv-off" @@ -52,7 +52,7 @@ class HarmonyActivitySelect(HarmonyEntity, SelectEntity): return [ACTIVITY_POWER_OFF] + sorted(self._data.activity_names) @property - def current_option(self): + def current_option(self) -> str | None: """Return the current activity.""" _, activity_name = self._data.current_activity return activity_name @@ -61,18 +61,19 @@ class HarmonyActivitySelect(HarmonyEntity, SelectEntity): """Change the current activity.""" await self._data.async_start_activity(option) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity is added to hass.""" - - callbacks = { - "connected": self.async_got_connected, - "disconnected": self.async_got_disconnected, - "activity_starting": self._async_activity_update, - "activity_started": self._async_activity_update, - "config_updated": None, - } - - self.async_on_remove(self._data.async_subscribe(HarmonyCallback(**callbacks))) + self.async_on_remove( + self._data.async_subscribe( + HarmonyCallback( + connected=self.async_got_connected, + disconnected=self.async_got_disconnected, + activity_starting=self._async_activity_update, + activity_started=self._async_activity_update, + config_updated=None, + ) + ) + ) @callback def _async_activity_update(self, activity_info: tuple): diff --git a/homeassistant/components/harmony/subscriber.py b/homeassistant/components/harmony/subscriber.py index 2731d8555f0..efb46f5c6e6 100644 --- a/homeassistant/components/harmony/subscriber.py +++ b/homeassistant/components/harmony/subscriber.py @@ -7,12 +7,12 @@ import logging # https://bugs.python.org/issue42965 from typing import Any, Callable, NamedTuple, Optional -from homeassistant.core import callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback _LOGGER = logging.getLogger(__name__) -NoParamCallback = Optional[Callable[[object], Any]] -ActivityCallback = Optional[Callable[[object, tuple], Any]] +NoParamCallback = Optional[Callable[[], Any]] +ActivityCallback = Optional[Callable[[tuple], Any]] class HarmonyCallback(NamedTuple): @@ -28,11 +28,11 @@ class HarmonyCallback(NamedTuple): class HarmonySubscriberMixin: """Base implementation for a subscriber.""" - def __init__(self, hass): + def __init__(self, hass: HomeAssistant) -> None: """Initialize an subscriber.""" super().__init__() self._hass = hass - self._subscriptions = [] + self._subscriptions: list[HarmonyCallback] = [] self._activity_lock = asyncio.Lock() async def async_lock_start_activity(self): @@ -40,23 +40,23 @@ class HarmonySubscriberMixin: await self._activity_lock.acquire() @callback - def async_unlock_start_activity(self): + def async_unlock_start_activity(self) -> None: """Release the lock.""" if self._activity_lock.locked(): self._activity_lock.release() @callback - def async_subscribe(self, update_callbacks: HarmonyCallback) -> Callable: + def async_subscribe(self, update_callbacks: HarmonyCallback) -> CALLBACK_TYPE: """Add a callback subscriber.""" self._subscriptions.append(update_callbacks) - def _unsubscribe(): + def _unsubscribe() -> None: self.async_unsubscribe(update_callbacks) return _unsubscribe @callback - def async_unsubscribe(self, update_callback: HarmonyCallback): + def async_unsubscribe(self, update_callback: HarmonyCallback) -> None: """Remove a callback subscriber.""" self._subscriptions.remove(update_callback) @@ -85,7 +85,7 @@ class HarmonySubscriberMixin: self.async_unlock_start_activity() self._call_callbacks("activity_started", activity_info) - def _call_callbacks(self, callback_func_name: str, argument: tuple = None): + def _call_callbacks(self, callback_func_name: str, argument: tuple = None) -> None: for subscription in self._subscriptions: current_callback = getattr(subscription, callback_func_name) if current_callback: From 6ad27089468e551ce4fb33021684c5366ce1d4f9 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 11 Aug 2022 09:10:18 +0100 Subject: [PATCH 3281/3516] Support polling the MiFlora battery (#76342) --- .../components/xiaomi_ble/__init__.py | 18 +++++-- .../components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/xiaomi_ble/__init__.py | 34 ++++++++++---- tests/components/xiaomi_ble/conftest.py | 47 +++++++++++++++++++ tests/components/xiaomi_ble/test_sensor.py | 15 ++++-- 7 files changed, 101 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index e3e30e0c79e..031490d6d68 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -11,8 +11,8 @@ from homeassistant.components.bluetooth import ( BluetoothScanningMode, BluetoothServiceInfoBleak, ) -from homeassistant.components.bluetooth.passive_update_processor import ( - PassiveBluetoothProcessorCoordinator, +from homeassistant.components.bluetooth.active_update_coordinator import ( + ActiveBluetoothProcessorCoordinator, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -56,9 +56,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: kwargs["bindkey"] = bytes.fromhex(bindkey) data = XiaomiBluetoothDeviceData(**kwargs) + def _needs_poll( + service_info: BluetoothServiceInfoBleak, last_poll: float | None + ) -> bool: + return data.poll_needed(service_info, last_poll) + + async def _async_poll(service_info: BluetoothServiceInfoBleak): + # BluetoothServiceInfoBleak is defined in HA, otherwise would just pass it + # directly to the Xiaomi code + return await data.async_poll(service_info.device) + coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( + ] = ActiveBluetoothProcessorCoordinator( hass, _LOGGER, address=address, @@ -66,6 +76,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=lambda service_info: process_service_info( hass, entry, data, service_info ), + needs_poll_method=_needs_poll, + poll_method=_async_poll, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index a901439b2c9..cdcac07b5c9 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.6.4"], + "requirements": ["xiaomi-ble==0.8.1"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 2cccec261c5..21fa1488f48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2480,7 +2480,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.4 +xiaomi-ble==0.8.1 # homeassistant.components.knx xknx==0.22.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7222a1778bd..35f845c8046 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1678,7 +1678,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.6.4 +xiaomi-ble==0.8.1 # homeassistant.components.knx xknx==0.22.1 diff --git a/tests/components/xiaomi_ble/__init__.py b/tests/components/xiaomi_ble/__init__.py index 1dd1eeed65a..c4424236082 100644 --- a/tests/components/xiaomi_ble/__init__.py +++ b/tests/components/xiaomi_ble/__init__.py @@ -1,21 +1,26 @@ """Tests for the SensorPush integration.""" +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak -NOT_SENSOR_PUSH_SERVICE_INFO = BluetoothServiceInfo( +NOT_SENSOR_PUSH_SERVICE_INFO = BluetoothServiceInfoBleak( name="Not it", address="00:00:00:00:00:00", + device=BLEDevice("00:00:00:00:00:00", None), rssi=-63, manufacturer_data={3234: b"\x00\x01"}, service_data={}, service_uuids=[], source="local", + advertisement=AdvertisementData(local_name="Not it"), ) -LYWSDCGQ_SERVICE_INFO = BluetoothServiceInfo( +LYWSDCGQ_SERVICE_INFO = BluetoothServiceInfoBleak( name="LYWSDCGQ", address="58:2D:34:35:93:21", + device=BLEDevice("00:00:00:00:00:00", None), rssi=-63, manufacturer_data={}, service_data={ @@ -23,11 +28,13 @@ LYWSDCGQ_SERVICE_INFO = BluetoothServiceInfo( }, service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", + advertisement=AdvertisementData(local_name="Not it"), ) -MMC_T201_1_SERVICE_INFO = BluetoothServiceInfo( +MMC_T201_1_SERVICE_INFO = BluetoothServiceInfoBleak( name="MMC_T201_1", address="00:81:F9:DD:6F:C1", + device=BLEDevice("00:00:00:00:00:00", None), rssi=-56, manufacturer_data={}, service_data={ @@ -35,11 +42,13 @@ MMC_T201_1_SERVICE_INFO = BluetoothServiceInfo( }, service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", + advertisement=AdvertisementData(local_name="Not it"), ) -JTYJGD03MI_SERVICE_INFO = BluetoothServiceInfo( +JTYJGD03MI_SERVICE_INFO = BluetoothServiceInfoBleak( name="JTYJGD03MI", address="54:EF:44:E3:9C:BC", + device=BLEDevice("00:00:00:00:00:00", None), rssi=-56, manufacturer_data={}, service_data={ @@ -47,11 +56,13 @@ JTYJGD03MI_SERVICE_INFO = BluetoothServiceInfo( }, service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", + advertisement=AdvertisementData(local_name="Not it"), ) -YLKG07YL_SERVICE_INFO = BluetoothServiceInfo( +YLKG07YL_SERVICE_INFO = BluetoothServiceInfoBleak( name="YLKG07YL", address="F8:24:41:C5:98:8B", + device=BLEDevice("00:00:00:00:00:00", None), rssi=-56, manufacturer_data={}, service_data={ @@ -59,11 +70,13 @@ YLKG07YL_SERVICE_INFO = BluetoothServiceInfo( }, service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", + advertisement=AdvertisementData(local_name="Not it"), ) -MISSING_PAYLOAD_ENCRYPTED = BluetoothServiceInfo( +MISSING_PAYLOAD_ENCRYPTED = BluetoothServiceInfoBleak( name="LYWSD02MMC", address="A4:C1:38:56:53:84", + device=BLEDevice("00:00:00:00:00:00", None), rssi=-56, manufacturer_data={}, service_data={ @@ -71,14 +84,16 @@ MISSING_PAYLOAD_ENCRYPTED = BluetoothServiceInfo( }, service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", + advertisement=AdvertisementData(local_name="Not it"), ) -def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfo: +def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfoBleak: """Make a dummy advertisement.""" - return BluetoothServiceInfo( + return BluetoothServiceInfoBleak( name="Test Device", address=address, + device=BLEDevice(address, None), rssi=-56, manufacturer_data={}, service_data={ @@ -86,4 +101,5 @@ def make_advertisement(address: str, payload: bytes) -> BluetoothServiceInfo: }, service_uuids=["0000fe95-0000-1000-8000-00805f9b34fb"], source="local", + advertisement=AdvertisementData(local_name="Test Device"), ) diff --git a/tests/components/xiaomi_ble/conftest.py b/tests/components/xiaomi_ble/conftest.py index 9fce8e85ea8..2997943468f 100644 --- a/tests/components/xiaomi_ble/conftest.py +++ b/tests/components/xiaomi_ble/conftest.py @@ -1,8 +1,55 @@ """Session fixtures.""" +from unittest import mock + import pytest +class MockServices: + """Mock GATTServicesCollection.""" + + def get_characteristic(self, key: str) -> str: + """Mock GATTServicesCollection.get_characteristic.""" + return key + + +class MockBleakClient: + """Mock BleakClient.""" + + services = MockServices() + + def __init__(self, *args, **kwargs): + """Mock BleakClient.""" + pass + + async def __aenter__(self, *args, **kwargs): + """Mock BleakClient.__aenter__.""" + return self + + async def __aexit__(self, *args, **kwargs): + """Mock BleakClient.__aexit__.""" + pass + + async def connect(self, *args, **kwargs): + """Mock BleakClient.connect.""" + pass + + async def disconnect(self, *args, **kwargs): + """Mock BleakClient.disconnect.""" + pass + + +class MockBleakClientBattery5(MockBleakClient): + """Mock BleakClient that returns a battery level of 5.""" + + async def read_gatt_char(self, *args, **kwargs) -> bytes: + """Mock BleakClient.read_gatt_char.""" + return b"\x05\x001.2.3" + + @pytest.fixture(autouse=True) def mock_bluetooth(enable_bluetooth): """Auto mock bluetooth.""" + + with mock.patch("xiaomi_ble.parser.BleakClient", MockBleakClientBattery5): + yield diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 011c6daecae..063e4a22c2d 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -78,7 +78,7 @@ async def test_xiaomi_formaldeyhde(hass): # obj type is 0x1010, payload len is 0x2 and payload is 0xf400 saved_callback( make_advertisement( - "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x10\x10\x02\xf4\x00" + "C4:7C:8D:6A:3E:7A", b"q \x5d\x01iz>j\x8d|\xc4\r\x10\x10\x02\xf4\x00" ), BluetoothChange.ADVERTISEMENT, ) @@ -125,7 +125,7 @@ async def test_xiaomi_consumable(hass): # obj type is 0x1310, payload len is 0x2 and payload is 0x6000 saved_callback( make_advertisement( - "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x13\x10\x02\x60\x00" + "C4:7C:8D:6A:3E:7A", b"q \x5d\x01iz>j\x8d|\xc4\r\x13\x10\x02\x60\x00" ), BluetoothChange.ADVERTISEMENT, ) @@ -172,7 +172,7 @@ async def test_xiaomi_battery_voltage(hass): # obj type is 0x0a10, payload len is 0x2 and payload is 0x6400 saved_callback( make_advertisement( - "C4:7C:8D:6A:3E:7A", b"q \x98\x00iz>j\x8d|\xc4\r\x0a\x10\x02\x64\x00" + "C4:7C:8D:6A:3E:7A", b"q \x5d\x01iz>j\x8d|\xc4\r\x0a\x10\x02\x64\x00" ), BluetoothChange.ADVERTISEMENT, ) @@ -246,7 +246,7 @@ async def test_xiaomi_HHCCJCY01(hass): BluetoothChange.ADVERTISEMENT, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 illum_sensor = hass.states.get("sensor.test_device_illuminance") illum_sensor_attr = illum_sensor.attributes @@ -276,6 +276,13 @@ async def test_xiaomi_HHCCJCY01(hass): assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + batt_sensor = hass.states.get("sensor.test_device_battery") + batt_sensor_attribtes = batt_sensor.attributes + assert batt_sensor.state == "5" + assert batt_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Battery" + assert batt_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" + assert batt_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" + assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() From a30dfd9f3e117d4834eb6fe7f09b66b97d01c80e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 11 Aug 2022 10:34:58 +0200 Subject: [PATCH 3282/3516] Add class attribute for capability attributes in entity base class (#76599) --- homeassistant/helpers/entity.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index cb71cfd9edf..2f2588367b6 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -274,6 +274,7 @@ class Entity(ABC): _attr_assumed_state: bool = False _attr_attribution: str | None = None _attr_available: bool = True + _attr_capability_attributes: Mapping[str, Any] | None = None _attr_context_recent_time: timedelta = timedelta(seconds=5) _attr_device_class: str | None _attr_device_info: DeviceInfo | None = None @@ -337,7 +338,7 @@ class Entity(ABC): Implemented by component base class. Convention for attribute names is lowercase snake_case. """ - return None + return self._attr_capability_attributes @property def state_attributes(self) -> dict[str, Any] | None: From 9919dd500dda5d5248dd42bac3c4c5ccd90d8be2 Mon Sep 17 00:00:00 2001 From: Antonino Piazza Date: Thu, 11 Aug 2022 11:03:12 +0200 Subject: [PATCH 3283/3516] Improve code quality in huawei_lte (#76583) Co-authored-by: Martin Hjelmare --- .../components/huawei_lte/__init__.py | 9 +- homeassistant/components/huawei_lte/switch.py | 4 +- tests/components/huawei_lte/test_switches.py | 182 ++++++++++-------- 3 files changed, 107 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index bace633f128..565286c4505 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -279,11 +279,12 @@ class Router: self._get_data( KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH, lambda: next( - filter( - lambda ssid: ssid.get("wifiisguestnetwork") == "1", - self.client.wlan.multi_basic_settings() + ( + ssid + for ssid in self.client.wlan.multi_basic_settings() .get("Ssids", {}) - .get("Ssid", []), + .get("Ssid", []) + if isinstance(ssid, dict) and ssid.get("wifiisguestnetwork") == "1" ), {}, ), diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index 78579d62698..261b77987cf 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -37,7 +37,7 @@ async def async_setup_entry( if router.data.get(KEY_DIALUP_MOBILE_DATASWITCH): switches.append(HuaweiLteMobileDataSwitch(router)) - if router.data.get(KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH).get("WifiEnable"): + if router.data.get(KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH, {}).get("WifiEnable"): switches.append(HuaweiLteWifiGuestNetworkSwitch(router)) async_add_entities(switches, True) @@ -151,6 +151,6 @@ class HuaweiLteWifiGuestNetworkSwitch(HuaweiLteBaseSwitch): return "mdi:wifi" if self.is_on else "mdi:wifi-off" @property - def extra_state_attributes(self) -> dict[str, str]: + def extra_state_attributes(self) -> dict[str, str | None]: """Return the state attributes.""" return {"ssid": self.router.data[self.key].get("WifiSsid")} diff --git a/tests/components/huawei_lte/test_switches.py b/tests/components/huawei_lte/test_switches.py index 5bafed27e70..4b0b81a86cd 100644 --- a/tests/components/huawei_lte/test_switches.py +++ b/tests/components/huawei_lte/test_switches.py @@ -2,7 +2,6 @@ from unittest.mock import MagicMock, patch from huawei_lte_api.enums.cradle import ConnectionStatusEnum -from pytest import fixture from homeassistant.components.huawei_lte.const import DOMAIN from homeassistant.components.switch import ( @@ -20,94 +19,70 @@ from tests.common import MockConfigEntry SWITCH_WIFI_GUEST_NETWORK = "switch.lte_wifi_guest_network" -@fixture +def magic_client(multi_basic_settings_value: dict) -> MagicMock: + """Mock huawei_lte.Client.""" + information = MagicMock(return_value={"SerialNumber": "test-serial-number"}) + check_notifications = MagicMock(return_value={"SmsStorageFull": 0}) + status = MagicMock( + return_value={"ConnectionStatus": ConnectionStatusEnum.CONNECTED.value} + ) + multi_basic_settings = MagicMock(return_value=multi_basic_settings_value) + wifi_feature_switch = MagicMock(return_value={"wifi24g_switch_enable": 1}) + device = MagicMock(information=information) + monitoring = MagicMock(check_notifications=check_notifications, status=status) + wlan = MagicMock( + multi_basic_settings=multi_basic_settings, + wifi_feature_switch=wifi_feature_switch, + ) + return MagicMock(device=device, monitoring=monitoring, wlan=wlan) + + @patch("homeassistant.components.huawei_lte.Connection", MagicMock()) -@patch( - "homeassistant.components.huawei_lte.Client", - return_value=MagicMock( - device=MagicMock( - information=MagicMock(return_value={"SerialNumber": "test-serial-number"}) - ), - monitoring=MagicMock( - check_notifications=MagicMock(return_value={"SmsStorageFull": 0}), - status=MagicMock( - return_value={"ConnectionStatus": ConnectionStatusEnum.CONNECTED.value} - ), - ), - wlan=MagicMock( - multi_basic_settings=MagicMock( - return_value={ - "Ssids": {"Ssid": [{"wifiisguestnetwork": "1", "WifiEnable": "0"}]} - } - ), - wifi_feature_switch=MagicMock(return_value={"wifi24g_switch_enable": 1}), - ), - ), -) -async def setup_component_with_wifi_guest_network( - client: MagicMock, hass: HomeAssistant -) -> None: - """Initialize huawei_lte components.""" - assert client - huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) - huawei_lte.add_to_hass(hass) - assert await hass.config_entries.async_setup(huawei_lte.entry_id) - await hass.async_block_till_done() - - -@fixture -@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) -@patch( - "homeassistant.components.huawei_lte.Client", - return_value=MagicMock( - device=MagicMock( - information=MagicMock(return_value={"SerialNumber": "test-serial-number"}) - ), - monitoring=MagicMock( - check_notifications=MagicMock(return_value={"SmsStorageFull": 0}), - status=MagicMock( - return_value={"ConnectionStatus": ConnectionStatusEnum.CONNECTED.value} - ), - ), - wlan=MagicMock( - multi_basic_settings=MagicMock(return_value={}), - wifi_feature_switch=MagicMock(return_value={"wifi24g_switch_enable": 1}), - ), - ), -) -async def setup_component_without_wifi_guest_network( - client: MagicMock, hass: HomeAssistant -) -> None: - """Initialize huawei_lte components.""" - assert client - huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) - huawei_lte.add_to_hass(hass) - assert await hass.config_entries.async_setup(huawei_lte.entry_id) - await hass.async_block_till_done() - - -def test_huawei_lte_wifi_guest_network_config_entry_when_network_is_not_present( +@patch("homeassistant.components.huawei_lte.Client", return_value=magic_client({})) +async def test_huawei_lte_wifi_guest_network_config_entry_when_network_is_not_present( + client, hass: HomeAssistant, - setup_component_without_wifi_guest_network, ) -> None: """Test switch wifi guest network config entry when network is not present.""" + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() entity_registry: EntityRegistry = er.async_get(hass) assert not entity_registry.async_is_registered(SWITCH_WIFI_GUEST_NETWORK) -def test_huawei_lte_wifi_guest_network_config_entry_when_network_is_present( +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch( + "homeassistant.components.huawei_lte.Client", + return_value=magic_client( + {"Ssids": {"Ssid": [{"wifiisguestnetwork": "1", "WifiEnable": "0"}]}} + ), +) +async def test_huawei_lte_wifi_guest_network_config_entry_when_network_is_present( + client, hass: HomeAssistant, - setup_component_with_wifi_guest_network, ) -> None: """Test switch wifi guest network config entry when network is present.""" + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() entity_registry: EntityRegistry = er.async_get(hass) assert entity_registry.async_is_registered(SWITCH_WIFI_GUEST_NETWORK) -async def test_turn_on_switch_wifi_guest_network( - hass: HomeAssistant, setup_component_with_wifi_guest_network -) -> None: +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch("homeassistant.components.huawei_lte.Client") +async def test_turn_on_switch_wifi_guest_network(client, hass: HomeAssistant) -> None: """Test switch wifi guest network turn on method.""" + client.return_value = magic_client( + {"Ssids": {"Ssid": [{"wifiisguestnetwork": "1", "WifiEnable": "0"}]}} + ) + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -116,15 +91,20 @@ async def test_turn_on_switch_wifi_guest_network( ) await hass.async_block_till_done() assert hass.states.is_state(SWITCH_WIFI_GUEST_NETWORK, STATE_ON) - hass.data[DOMAIN].routers[ - "test-serial-number" - ].client.wlan.wifi_guest_network_switch.assert_called_once_with(True) + client.return_value.wlan.wifi_guest_network_switch.assert_called_once_with(True) -async def test_turn_off_switch_wifi_guest_network( - hass: HomeAssistant, setup_component_with_wifi_guest_network -) -> None: +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch("homeassistant.components.huawei_lte.Client") +async def test_turn_off_switch_wifi_guest_network(client, hass: HomeAssistant) -> None: """Test switch wifi guest network turn off method.""" + client.return_value = magic_client( + {"Ssids": {"Ssid": [{"wifiisguestnetwork": "1", "WifiEnable": "1"}]}} + ) + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, @@ -133,6 +113,44 @@ async def test_turn_off_switch_wifi_guest_network( ) await hass.async_block_till_done() assert hass.states.is_state(SWITCH_WIFI_GUEST_NETWORK, STATE_OFF) - hass.data[DOMAIN].routers[ - "test-serial-number" - ].client.wlan.wifi_guest_network_switch.assert_called_with(False) + client.return_value.wlan.wifi_guest_network_switch.assert_called_with(False) + + +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch( + "homeassistant.components.huawei_lte.Client", + return_value=magic_client({"Ssids": {"Ssid": "str"}}), +) +async def test_huawei_lte_wifi_guest_network_config_entry_when_ssid_is_str( + client, hass: HomeAssistant +): + """Test switch wifi guest network config entry when ssid is a str. + + Issue #76244. Huawai models: H312-371, E5372 and E8372. + """ + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() + entity_registry: EntityRegistry = er.async_get(hass) + assert not entity_registry.async_is_registered(SWITCH_WIFI_GUEST_NETWORK) + + +@patch("homeassistant.components.huawei_lte.Connection", MagicMock()) +@patch( + "homeassistant.components.huawei_lte.Client", + return_value=magic_client({"Ssids": {"Ssid": None}}), +) +async def test_huawei_lte_wifi_guest_network_config_entry_when_ssid_is_none( + client, hass: HomeAssistant +): + """Test switch wifi guest network config entry when ssid is a None. + + Issue #76244. + """ + huawei_lte = MockConfigEntry(domain=DOMAIN, data={CONF_URL: "http://huawei-lte"}) + huawei_lte.add_to_hass(hass) + await hass.config_entries.async_setup(huawei_lte.entry_id) + await hass.async_block_till_done() + entity_registry: EntityRegistry = er.async_get(hass) + assert not entity_registry.async_is_registered(SWITCH_WIFI_GUEST_NETWORK) From b5787fe8fa83c3ce2b1324542f6e14a0441ecc88 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Aug 2022 23:05:58 -1000 Subject: [PATCH 3284/3516] Add RSSI sensors to Yale Access Bluetooth (#76590) --- .coveragerc | 1 + .../components/yalexs_ble/__init__.py | 2 +- homeassistant/components/yalexs_ble/sensor.py | 44 +++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/yalexs_ble/sensor.py diff --git a/.coveragerc b/.coveragerc index 4e88d171d57..e326f5f44c8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1487,6 +1487,7 @@ omit = homeassistant/components/yalexs_ble/binary_sensor.py homeassistant/components/yalexs_ble/entity.py homeassistant/components/yalexs_ble/lock.py + homeassistant/components/yalexs_ble/sensor.py homeassistant/components/yalexs_ble/util.py homeassistant/components/yale_smart_alarm/__init__.py homeassistant/components/yale_smart_alarm/alarm_control_panel.py diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index 265b10a502b..3b9481b6982 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -17,7 +17,7 @@ from .const import CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DEVICE_TIMEOUT, DOMAIN from .models import YaleXSBLEData from .util import async_find_existing_service_info, bluetooth_callback_matcher -PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.LOCK] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.LOCK, Platform.SENSOR] class YaleXSBLEDiscovery(TypedDict): diff --git a/homeassistant/components/yalexs_ble/sensor.py b/homeassistant/components/yalexs_ble/sensor.py new file mode 100644 index 00000000000..19fee1924e6 --- /dev/null +++ b/homeassistant/components/yalexs_ble/sensor.py @@ -0,0 +1,44 @@ +"""Support for yalexs ble sensors.""" +from __future__ import annotations + +from yalexs_ble import ConnectionInfo, LockInfo, LockState + +from homeassistant import config_entries +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import YALEXSBLEEntity +from .models import YaleXSBLEData + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up YALE XS Bluetooth sensors.""" + data: YaleXSBLEData = hass.data[DOMAIN][entry.entry_id] + async_add_entities([YaleXSBLERSSISensor(data)]) + + +class YaleXSBLERSSISensor(YALEXSBLEEntity, SensorEntity): + """Yale XS Bluetooth RSSI sensor.""" + + _attr_device_class = SensorDeviceClass.SIGNAL_STRENGTH + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_entity_registry_enabled_default = False + _attr_has_entity_name = True + _attr_name = "Signal strength" + _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT + + @callback + def _async_update_state( + self, new_state: LockState, lock_info: LockInfo, connection_info: ConnectionInfo + ) -> None: + """Update the state.""" + self._attr_native_value = connection_info.rssi + super()._async_update_state(new_state, lock_info, connection_info) From eb0b6f3d75ee97dd50e33be84fc99f30d0c31413 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Aug 2022 23:08:03 -1000 Subject: [PATCH 3285/3516] Fix Govee 5181 with old firmware (#76600) --- homeassistant/components/govee_ble/manifest.json | 6 +++++- homeassistant/generated/bluetooth.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index e8df2af3abb..7c65fe35b5d 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -23,6 +23,10 @@ "manufacturer_id": 818, "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" }, + { + "manufacturer_id": 59970, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + }, { "manufacturer_id": 14474, "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" @@ -32,7 +36,7 @@ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.14.0"], + "requirements": ["govee-ble==0.14.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 7f0696f1a76..2575be9aa69 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -51,6 +51,11 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "manufacturer_id": 818, "service_uuid": "00008551-0000-1000-8000-00805f9b34fb" }, + { + "domain": "govee_ble", + "manufacturer_id": 59970, + "service_uuid": "00008151-0000-1000-8000-00805f9b34fb" + }, { "domain": "govee_ble", "manufacturer_id": 14474, diff --git a/requirements_all.txt b/requirements_all.txt index 21fa1488f48..3054c425ecc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -757,7 +757,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.14.0 +govee-ble==0.14.1 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 35f845c8046..8a16718a237 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -558,7 +558,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.14.0 +govee-ble==0.14.1 # homeassistant.components.gree greeclimate==1.3.0 From 66b742f110025013e60ca8cac7aeb3247bac8f47 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 11 Aug 2022 12:41:24 +0200 Subject: [PATCH 3286/3516] Improve type hints in yeelight lights (#76018) Co-authored-by: Franck Nijhof --- homeassistant/components/yeelight/light.py | 94 ++++++++++------------ 1 file changed, 43 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 9bb0ed21989..af1c3de74e7 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -63,6 +63,7 @@ from .const import ( MODELS_WITH_DELAYED_ON_TRANSITION, POWER_STATE_CHANGE_TIME, ) +from .device import YeelightDevice from .entity import YeelightEntity _LOGGER = logging.getLogger(__name__) @@ -218,7 +219,7 @@ def _transitions_config_parser(transitions): @callback -def _parse_custom_effects(effects_config): +def _parse_custom_effects(effects_config) -> dict[str, dict[str, Any]]: effects = {} for config in effects_config: params = config[CONF_FLOW_PARAMS] @@ -414,18 +415,23 @@ class YeelightGenericLight(YeelightEntity, LightEntity): ) _attr_should_poll = False - def __init__(self, device, entry, custom_effects=None): + def __init__( + self, + device: YeelightDevice, + entry: ConfigEntry, + custom_effects: dict[str, dict[str, Any]] | None = None, + ) -> None: """Initialize the Yeelight light.""" super().__init__(device, entry) self.config = device.config - self._color_temp = None + self._color_temp: int | None = None self._effect = None model_specs = self._bulb.get_model_specs() - self._min_mireds = kelvin_to_mired(model_specs["color_temp"]["max"]) - self._max_mireds = kelvin_to_mired(model_specs["color_temp"]["min"]) + self._attr_min_mireds = kelvin_to_mired(model_specs["color_temp"]["max"]) + self._attr_max_mireds = kelvin_to_mired(model_specs["color_temp"]["min"]) self._light_type = LightType.Main @@ -437,7 +443,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): self._unexpected_state_check = None @callback - def async_state_changed(self): + def async_state_changed(self) -> None: """Call when the device changes state.""" if not self._device.available: self._async_cancel_pending_state_check() @@ -455,12 +461,12 @@ class YeelightGenericLight(YeelightEntity, LightEntity): await super().async_added_to_hass() @property - def effect_list(self): + def effect_list(self) -> list[str]: """Return the list of supported effects.""" return self._predefined_effects + self.custom_effects_names @property - def color_temp(self) -> int: + def color_temp(self) -> int | None: """Return the color temperature.""" if temp_in_k := self._get_property("ct"): self._color_temp = kelvin_to_mired(int(temp_in_k)) @@ -489,32 +495,22 @@ class YeelightGenericLight(YeelightEntity, LightEntity): return round(255 * (int(brightness) / 100)) @property - def min_mireds(self): - """Return minimum supported color temperature.""" - return self._min_mireds - - @property - def max_mireds(self): - """Return maximum supported color temperature.""" - return self._max_mireds - - @property - def custom_effects(self): + def custom_effects(self) -> dict[str, dict[str, Any]]: """Return dict with custom effects.""" return self._custom_effects @property - def custom_effects_names(self): + def custom_effects_names(self) -> list[str]: """Return list with custom effects names.""" return list(self.custom_effects) @property - def light_type(self): + def light_type(self) -> LightType: """Return light type.""" return self._light_type @property - def hs_color(self) -> tuple[int, int] | None: + def hs_color(self) -> tuple[float, float] | None: """Return the color property.""" hue = self._get_property("hue") sat = self._get_property("sat") @@ -537,7 +533,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): return (red, green, blue) @property - def effect(self): + def effect(self) -> str | None: """Return the current effect.""" return self._effect if self.device.is_color_flow_enabled else None @@ -549,27 +545,27 @@ class YeelightGenericLight(YeelightEntity, LightEntity): def _properties(self) -> dict: return self._bulb.last_properties if self._bulb else {} - def _get_property(self, prop, default=None): + def _get_property(self, prop: str, default=None): return self._properties.get(prop, default) @property - def _brightness_property(self): + def _brightness_property(self) -> str: return "bright" @property - def _power_property(self): + def _power_property(self) -> str: return "power" @property - def _turn_on_power_mode(self): + def _turn_on_power_mode(self) -> PowerMode: return PowerMode.LAST @property - def _predefined_effects(self): + def _predefined_effects(self) -> list[str]: return YEELIGHT_MONO_EFFECT_LIST @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device specific state attributes.""" attributes = { "flowing": self.device.is_color_flow_enabled, @@ -582,7 +578,7 @@ class YeelightGenericLight(YeelightEntity, LightEntity): return attributes @property - def device(self): + def device(self) -> YeelightDevice: """Return yeelight device.""" return self._device @@ -890,7 +886,7 @@ class YeelightColorLightSupport(YeelightGenericLight): return ColorMode.UNKNOWN @property - def _predefined_effects(self): + def _predefined_effects(self) -> list[str]: return YEELIGHT_COLOR_EFFECT_LIST @@ -901,7 +897,7 @@ class YeelightWhiteTempLightSupport(YeelightGenericLight): _attr_supported_color_modes = {ColorMode.COLOR_TEMP} @property - def _predefined_effects(self): + def _predefined_effects(self) -> list[str]: return YEELIGHT_TEMP_ONLY_EFFECT_LIST @@ -909,7 +905,7 @@ class YeelightNightLightSupport: """Representation of a Yeelight nightlight support.""" @property - def _turn_on_power_mode(self): + def _turn_on_power_mode(self) -> PowerMode: return PowerMode.NORMAL @@ -917,7 +913,7 @@ class YeelightWithoutNightlightSwitchMixIn(YeelightGenericLight): """A mix-in for yeelights without a nightlight switch.""" @property - def _brightness_property(self): + def _brightness_property(self) -> str: # If the nightlight is not active, we do not # want to "current_brightness" since it will check # "bg_power" and main light could still be on @@ -926,11 +922,11 @@ class YeelightWithoutNightlightSwitchMixIn(YeelightGenericLight): return super()._brightness_property @property - def color_temp(self) -> int: + def color_temp(self) -> int | None: """Return the color temperature.""" if self.device.is_nightlight_enabled: # Enabling the nightlight locks the colortemp to max - return self._max_mireds + return self.max_mireds return super().color_temp @@ -978,6 +974,7 @@ class YeelightNightLightMode(YeelightGenericLight): """Representation of a Yeelight when in nightlight mode.""" _attr_color_mode = ColorMode.BRIGHTNESS + _attr_icon = "mdi:weather-night" _attr_supported_color_modes = {ColorMode.BRIGHTNESS} @property @@ -991,26 +988,21 @@ class YeelightNightLightMode(YeelightGenericLight): """Return the name of the device if any.""" return f"{self.device.name} Nightlight" - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return "mdi:weather-night" - @property def is_on(self) -> bool: """Return true if device is on.""" return super().is_on and self.device.is_nightlight_enabled @property - def _brightness_property(self): + def _brightness_property(self) -> str: return "nl_br" @property - def _turn_on_power_mode(self): + def _turn_on_power_mode(self) -> PowerMode: return PowerMode.MOONLIGHT @property - def supported_features(self): + def supported_features(self) -> int: """Flag no supported features.""" return 0 @@ -1019,7 +1011,7 @@ class YeelightNightLightModeWithAmbientSupport(YeelightNightLightMode): """Representation of a Yeelight, with ambient support, when in nightlight mode.""" @property - def _power_property(self): + def _power_property(self) -> str: return "main_power" @@ -1040,7 +1032,7 @@ class YeelightWithAmbientWithoutNightlight(YeelightWhiteTempWithoutNightlightSwi """ @property - def _power_property(self): + def _power_property(self) -> str: return "main_power" @@ -1051,7 +1043,7 @@ class YeelightWithAmbientAndNightlight(YeelightWithNightLight): """ @property - def _power_property(self): + def _power_property(self) -> str: return "main_power" @@ -1063,8 +1055,8 @@ class YeelightAmbientLight(YeelightColorLightWithoutNightlightSwitch): def __init__(self, *args, **kwargs): """Initialize the Yeelight Ambient light.""" super().__init__(*args, **kwargs) - self._min_mireds = kelvin_to_mired(6500) - self._max_mireds = kelvin_to_mired(1700) + self._attr_min_mireds = kelvin_to_mired(6500) + self._attr_max_mireds = kelvin_to_mired(1700) self._light_type = LightType.Ambient @@ -1080,10 +1072,10 @@ class YeelightAmbientLight(YeelightColorLightWithoutNightlightSwitch): return f"{self.device.name} Ambilight" @property - def _brightness_property(self): + def _brightness_property(self) -> str: return "bright" - def _get_property(self, prop, default=None): + def _get_property(self, prop: str, default=None): if not (bg_prop := self.PROPERTIES_MAPPING.get(prop)): bg_prop = f"bg_{prop}" From 078a4974e143d14e307f50201d088f3fbbce6967 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 11 Aug 2022 14:53:40 +0200 Subject: [PATCH 3287/3516] Fix evohome preset modes (#76606) --- homeassistant/components/evohome/climate.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index c1a630d0d05..e9eab8c2ae3 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -128,26 +128,17 @@ class EvoClimateEntity(EvoDevice, ClimateEntity): _attr_temperature_unit = TEMP_CELSIUS - def __init__(self, evo_broker, evo_device) -> None: - """Initialize a Climate device.""" - super().__init__(evo_broker, evo_device) - - self._preset_modes = None - @property def hvac_modes(self) -> list[str]: """Return a list of available hvac operation modes.""" return list(HA_HVAC_TO_TCS) - @property - def preset_modes(self) -> list[str] | None: - """Return a list of available preset modes.""" - return self._preset_modes - class EvoZone(EvoChild, EvoClimateEntity): """Base for a Honeywell TCC Zone.""" + _attr_preset_modes = list(HA_PRESET_TO_EVO) + def __init__(self, evo_broker, evo_device) -> None: """Initialize a Honeywell TCC Zone.""" super().__init__(evo_broker, evo_device) @@ -233,7 +224,7 @@ class EvoZone(EvoChild, EvoClimateEntity): """ return self._evo_device.setpointCapabilities["maxHeatSetpoint"] - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set a new target temperature.""" temperature = kwargs["temperature"] @@ -249,7 +240,7 @@ class EvoZone(EvoChild, EvoClimateEntity): self._evo_device.set_temperature(temperature, until=until) ) - async def async_set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set a Zone to one of its native EVO_* operating modes. Zones inherit their _effective_ operating mode from their Controller. @@ -387,7 +378,7 @@ class EvoController(EvoClimateEntity): """Return the current preset mode, e.g., home, away, temp.""" return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"]) - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Raise exception as Controllers don't have a target temperature.""" raise NotImplementedError("Evohome Controllers don't have target temperatures.") From ebbff7b60e43f17d65ead811d314602b9daddfc4 Mon Sep 17 00:00:00 2001 From: Zach Berger Date: Thu, 11 Aug 2022 06:01:35 -0700 Subject: [PATCH 3288/3516] Add Awair Local API support (#75535) --- homeassistant/components/awair/__init__.py | 84 ++++-- homeassistant/components/awair/config_flow.py | 124 ++++++++- homeassistant/components/awair/const.py | 7 +- homeassistant/components/awair/manifest.json | 10 +- homeassistant/components/awair/sensor.py | 4 +- homeassistant/components/awair/strings.json | 32 ++- homeassistant/config_entries.py | 4 +- homeassistant/generated/zeroconf.py | 4 + tests/components/awair/conftest.py | 73 +++++ tests/components/awair/const.py | 31 ++- .../awair/fixtures/awair-local.json | 17 ++ .../{devices.json => cloud_devices.json} | 0 .../awair/fixtures/local_devices.json | 16 ++ tests/components/awair/test_config_flow.py | 251 ++++++++++++++---- tests/components/awair/test_sensor.py | 89 ++++--- 15 files changed, 603 insertions(+), 143 deletions(-) create mode 100644 tests/components/awair/conftest.py create mode 100644 tests/components/awair/fixtures/awair-local.json rename tests/components/awair/fixtures/{devices.json => cloud_devices.json} (100%) create mode 100644 tests/components/awair/fixtures/local_devices.json diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index ef39e488001..b0a5d39814c 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -2,20 +2,27 @@ from __future__ import annotations from asyncio import gather -from typing import Any from async_timeout import timeout -from python_awair import Awair -from python_awair.exceptions import AuthError +from python_awair import Awair, AwairLocal +from python_awair.devices import AwairBaseDevice, AwairLocalDevice +from python_awair.exceptions import AuthError, AwairError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ACCESS_TOKEN, Platform +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import API_TIMEOUT, DOMAIN, LOGGER, UPDATE_INTERVAL, AwairResult +from .const import ( + API_TIMEOUT, + DOMAIN, + LOGGER, + UPDATE_INTERVAL_CLOUD, + UPDATE_INTERVAL_LOCAL, + AwairResult, +) PLATFORMS = [Platform.SENSOR] @@ -23,7 +30,13 @@ PLATFORMS = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Awair integration from a config entry.""" session = async_get_clientsession(hass) - coordinator = AwairDataUpdateCoordinator(hass, config_entry, session) + + coordinator: AwairDataUpdateCoordinator + + if CONF_HOST in config_entry.data: + coordinator = AwairLocalDataUpdateCoordinator(hass, config_entry, session) + else: + coordinator = AwairCloudDataUpdateCoordinator(hass, config_entry, session) await coordinator.async_config_entry_first_refresh() @@ -50,15 +63,31 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> class AwairDataUpdateCoordinator(DataUpdateCoordinator): """Define a wrapper class to update Awair data.""" - def __init__(self, hass, config_entry, session) -> None: + def __init__(self, hass, config_entry, update_interval) -> None: """Set up the AwairDataUpdateCoordinator class.""" - access_token = config_entry.data[CONF_ACCESS_TOKEN] - self._awair = Awair(access_token=access_token, session=session) self._config_entry = config_entry - super().__init__(hass, LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) + super().__init__(hass, LOGGER, name=DOMAIN, update_interval=update_interval) - async def _async_update_data(self) -> Any | None: + async def _fetch_air_data(self, device: AwairBaseDevice): + """Fetch latest air quality data.""" + LOGGER.debug("Fetching data for %s", device.uuid) + air_data = await device.air_data_latest() + LOGGER.debug(air_data) + return AwairResult(device=device, air_data=air_data) + + +class AwairCloudDataUpdateCoordinator(AwairDataUpdateCoordinator): + """Define a wrapper class to update Awair data from Cloud API.""" + + def __init__(self, hass, config_entry, session) -> None: + """Set up the AwairCloudDataUpdateCoordinator class.""" + access_token = config_entry.data[CONF_ACCESS_TOKEN] + self._awair = Awair(access_token=access_token, session=session) + + super().__init__(hass, config_entry, UPDATE_INTERVAL_CLOUD) + + async def _async_update_data(self) -> dict[str, AwairResult] | None: """Update data via Awair client library.""" async with timeout(API_TIMEOUT): try: @@ -74,9 +103,30 @@ class AwairDataUpdateCoordinator(DataUpdateCoordinator): except Exception as err: raise UpdateFailed(err) from err - async def _fetch_air_data(self, device): - """Fetch latest air quality data.""" - LOGGER.debug("Fetching data for %s", device.uuid) - air_data = await device.air_data_latest() - LOGGER.debug(air_data) - return AwairResult(device=device, air_data=air_data) + +class AwairLocalDataUpdateCoordinator(AwairDataUpdateCoordinator): + """Define a wrapper class to update Awair data from the local API.""" + + _device: AwairLocalDevice | None = None + + def __init__(self, hass, config_entry, session) -> None: + """Set up the AwairLocalDataUpdateCoordinator class.""" + self._awair = AwairLocal( + session=session, device_addrs=[config_entry.data[CONF_HOST]] + ) + + super().__init__(hass, config_entry, UPDATE_INTERVAL_LOCAL) + + async def _async_update_data(self) -> dict[str, AwairResult] | None: + """Update data via Awair client library.""" + async with timeout(API_TIMEOUT): + try: + if self._device is None: + LOGGER.debug("Fetching devices") + devices = await self._awair.devices() + self._device = devices[0] + result = await self._fetch_air_data(self._device) + return {result.device.uuid: result} + except AwairError as err: + LOGGER.error("Unexpected API error: %s", err) + raise UpdateFailed(err) from err diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index 1e83144945d..3fb822ab4fe 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -4,12 +4,14 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any -from python_awair import Awair +from aiohttp.client_exceptions import ClientConnectorError +from python_awair import Awair, AwairLocal, AwairLocalDevice from python_awair.exceptions import AuthError, AwairError import voluptuous as vol +from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -21,20 +23,76 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 + _device: AwairLocalDevice + + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: + """Handle zeroconf discovery.""" + + host = discovery_info.host + LOGGER.debug("Discovered device: %s", host) + + self._device, _ = await self._check_local_connection(host) + + if self._device is not None: + await self.async_set_unique_id(self._device.mac_address) + self._abort_if_unique_id_configured(error="already_configured_device") + self.context.update( + { + "title_placeholders": { + "model": self._device.model, + "device_id": self._device.device_id, + } + } + ) + else: + return self.async_abort(reason="unreachable") + return await self.async_step_discovery_confirm() + + async def async_step_discovery_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + if user_input is not None: + title = f"{self._device.model} ({self._device.device_id})" + return self.async_create_entry( + title=title, + data={CONF_HOST: self._device.device_addr}, + ) + + self._set_confirm_only() + placeholders = { + "model": self._device.model, + "device_id": self._device.device_id, + } + return self.async_show_form( + step_id="discovery_confirm", + description_placeholders=placeholders, + ) + async def async_step_user( self, user_input: dict[str, str] | None = None ) -> FlowResult: """Handle a flow initialized by the user.""" + + return self.async_show_menu(step_id="user", menu_options=["local", "cloud"]) + + async def async_step_cloud(self, user_input: Mapping[str, Any]) -> FlowResult: + """Handle collecting and verifying Awair Cloud API credentials.""" + errors = {} if user_input is not None: - user, error = await self._check_connection(user_input[CONF_ACCESS_TOKEN]) + user, error = await self._check_cloud_connection( + user_input[CONF_ACCESS_TOKEN] + ) if user is not None: await self.async_set_unique_id(user.email) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured(error="already_configured_account") - title = f"{user.email} ({user.user_id})" + title = user.email return self.async_create_entry(title=title, data=user_input) if error != "invalid_access_token": @@ -43,8 +101,39 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): errors = {CONF_ACCESS_TOKEN: "invalid_access_token"} return self.async_show_form( - step_id="user", - data_schema=vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}), + step_id="cloud", + data_schema=vol.Schema({vol.Optional(CONF_ACCESS_TOKEN): str}), + description_placeholders={ + "url": "https://developer.getawair.com/onboard/login" + }, + errors=errors, + ) + + async def async_step_local(self, user_input: Mapping[str, Any]) -> FlowResult: + """Handle collecting and verifying Awair Local API hosts.""" + + errors = {} + + if user_input is not None: + self._device, error = await self._check_local_connection( + user_input[CONF_HOST] + ) + + if self._device is not None: + await self.async_set_unique_id(self._device.mac_address) + self._abort_if_unique_id_configured(error="already_configured_device") + title = f"{self._device.model} ({self._device.device_id})" + return self.async_create_entry(title=title, data=user_input) + + if error is not None: + errors = {CONF_HOST: error} + + return self.async_show_form( + step_id="local", + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + description_placeholders={ + "url": "https://support.getawair.com/hc/en-us/articles/360049221014-Awair-Element-Local-API-Feature" + }, errors=errors, ) @@ -60,7 +149,7 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): if user_input is not None: access_token = user_input[CONF_ACCESS_TOKEN] - _, error = await self._check_connection(access_token) + _, error = await self._check_cloud_connection(access_token) if error is None: entry = await self.async_set_unique_id(self.unique_id) @@ -79,7 +168,24 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def _check_connection(self, access_token: str): + async def _check_local_connection(self, device_address: str): + """Check the access token is valid.""" + session = async_get_clientsession(self.hass) + awair = AwairLocal(session=session, device_addrs=[device_address]) + + try: + devices = await awair.devices() + return (devices[0], None) + + except ClientConnectorError as err: + LOGGER.error("Unable to connect error: %s", err) + return (None, "unreachable") + + except AwairError as err: + LOGGER.error("Unexpected API error: %s", err) + return (None, "unknown") + + async def _check_cloud_connection(self, access_token: str): """Check the access token is valid.""" session = async_get_clientsession(self.hass) awair = Awair(access_token=access_token, session=session) diff --git a/homeassistant/components/awair/const.py b/homeassistant/components/awair/const.py index 6fcf63abb4d..133cf03fdbe 100644 --- a/homeassistant/components/awair/const.py +++ b/homeassistant/components/awair/const.py @@ -6,7 +6,7 @@ from datetime import timedelta import logging from python_awair.air_data import AirData -from python_awair.devices import AwairDevice +from python_awair.devices import AwairBaseDevice from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription from homeassistant.const import ( @@ -39,7 +39,8 @@ DUST_ALIASES = [API_PM25, API_PM10] LOGGER = logging.getLogger(__package__) -UPDATE_INTERVAL = timedelta(minutes=5) +UPDATE_INTERVAL_CLOUD = timedelta(minutes=5) +UPDATE_INTERVAL_LOCAL = timedelta(seconds=30) @dataclass @@ -129,5 +130,5 @@ SENSOR_TYPES_DUST: tuple[AwairSensorEntityDescription, ...] = ( class AwairResult: """Wrapper class to hold an awair device and set of air data.""" - device: AwairDevice + device: AwairBaseDevice air_data: AirData diff --git a/homeassistant/components/awair/manifest.json b/homeassistant/components/awair/manifest.json index 57b3c242620..cea5d01bfab 100644 --- a/homeassistant/components/awair/manifest.json +++ b/homeassistant/components/awair/manifest.json @@ -5,6 +5,12 @@ "requirements": ["python_awair==0.2.3"], "codeowners": ["@ahayworth", "@danielsjf"], "config_flow": true, - "iot_class": "cloud_polling", - "loggers": ["python_awair"] + "iot_class": "local_polling", + "loggers": ["python_awair"], + "zeroconf": [ + { + "type": "_http._tcp.local.", + "name": "awair*" + } + ] } diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index ddf76c0e93d..cda7f31095e 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from python_awair.air_data import AirData -from python_awair.devices import AwairDevice +from python_awair.devices import AwairBaseDevice from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry @@ -76,7 +76,7 @@ class AwairSensor(CoordinatorEntity[AwairDataUpdateCoordinator], SensorEntity): def __init__( self, - device: AwairDevice, + device: AwairBaseDevice, coordinator: AwairDataUpdateCoordinator, description: AwairSensorEntityDescription, ) -> None: diff --git a/homeassistant/components/awair/strings.json b/homeassistant/components/awair/strings.json index 5ed7c0e715e..fc95fc861f1 100644 --- a/homeassistant/components/awair/strings.json +++ b/homeassistant/components/awair/strings.json @@ -1,29 +1,49 @@ { "config": { "step": { - "user": { - "description": "You must register for an Awair developer access token at: https://developer.getawair.com/onboard/login", + "cloud": { + "description": "You must register for an Awair developer access token at: {url}", "data": { "access_token": "[%key:common::config_flow::data::access_token%]", "email": "[%key:common::config_flow::data::email%]" } }, + "local": { + "data": { + "host": "[%key:common::config_flow::data::ip%]" + }, + "description": "Awair Local API must be enabled following these steps: {url}" + }, "reauth_confirm": { "description": "Please re-enter your Awair developer access token.", "data": { "access_token": "[%key:common::config_flow::data::access_token%]", "email": "[%key:common::config_flow::data::email%]" } + }, + "discovery_confirm": { + "description": "Do you want to setup {model} ({device_id})?" + }, + "user": { + "menu_options": { + "cloud": "Connect via the cloud", + "local": "Connect locally (preferred)" + }, + "description": "Pick local for the best experience. Only use cloud if the device is not connected to the same network as Home Assistant, or if you have a legacy device." } }, "error": { "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "unreachable": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]", + "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" - } + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "unreachable": "[%key:common::config_flow::error::cannot_connect%]" + }, + "flow_title": "{model} ({device_id})" } } diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 7c2f1c84ff9..f37efb6c627 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1297,6 +1297,8 @@ class ConfigFlow(data_entry_flow.FlowHandler): self, updates: dict[str, Any] | None = None, reload_on_update: bool = True, + *, + error: str = "already_configured", ) -> None: """Abort if the unique ID is already configured.""" if self.unique_id is None: @@ -1332,7 +1334,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) ) - raise data_entry_flow.AbortFlow("already_configured") + raise data_entry_flow.AbortFlow(error) async def async_set_unique_id( self, unique_id: str | None = None, *, raise_on_progress: bool = True diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index d59d37f4579..b6237a36cd7 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -183,6 +183,10 @@ ZEROCONF = { } ], "_http._tcp.local.": [ + { + "domain": "awair", + "name": "awair*" + }, { "domain": "bosch_shc", "name": "bosch shc*" diff --git a/tests/components/awair/conftest.py b/tests/components/awair/conftest.py new file mode 100644 index 00000000000..ec15561cc05 --- /dev/null +++ b/tests/components/awair/conftest.py @@ -0,0 +1,73 @@ +"""Fixtures for testing Awair integration.""" + +import json + +import pytest + +from tests.common import load_fixture + + +@pytest.fixture(name="cloud_devices", scope="session") +def cloud_devices_fixture(): + """Fixture representing devices returned by Awair Cloud API.""" + return json.loads(load_fixture("awair/cloud_devices.json")) + + +@pytest.fixture(name="local_devices", scope="session") +def local_devices_fixture(): + """Fixture representing devices returned by Awair local API.""" + return json.loads(load_fixture("awair/local_devices.json")) + + +@pytest.fixture(name="gen1_data", scope="session") +def gen1_data_fixture(): + """Fixture representing data returned from Gen1 Awair device.""" + return json.loads(load_fixture("awair/awair.json")) + + +@pytest.fixture(name="gen2_data", scope="session") +def gen2_data_fixture(): + """Fixture representing data returned from Gen2 Awair device.""" + return json.loads(load_fixture("awair/awair-r2.json")) + + +@pytest.fixture(name="glow_data", scope="session") +def glow_data_fixture(): + """Fixture representing data returned from Awair glow device.""" + return json.loads(load_fixture("awair/glow.json")) + + +@pytest.fixture(name="mint_data", scope="session") +def mint_data_fixture(): + """Fixture representing data returned from Awair mint device.""" + return json.loads(load_fixture("awair/mint.json")) + + +@pytest.fixture(name="no_devices", scope="session") +def no_devicess_fixture(): + """Fixture representing when no devices are found in Awair's cloud API.""" + return json.loads(load_fixture("awair/no_devices.json")) + + +@pytest.fixture(name="awair_offline", scope="session") +def awair_offline_fixture(): + """Fixture representing when Awair devices are offline.""" + return json.loads(load_fixture("awair/awair-offline.json")) + + +@pytest.fixture(name="omni_data", scope="session") +def omni_data_fixture(): + """Fixture representing data returned from Awair omni device.""" + return json.loads(load_fixture("awair/omni.json")) + + +@pytest.fixture(name="user", scope="session") +def user_fixture(): + """Fixture representing the User object returned from Awair's Cloud API.""" + return json.loads(load_fixture("awair/user.json")) + + +@pytest.fixture(name="local_data", scope="session") +def local_data_fixture(): + """Fixture representing data returned from Awair local device.""" + return json.loads(load_fixture("awair/awair-local.json")) diff --git a/tests/components/awair/const.py b/tests/components/awair/const.py index 94c07e9e9fd..cead20d10af 100644 --- a/tests/components/awair/const.py +++ b/tests/components/awair/const.py @@ -1,20 +1,19 @@ """Constants used in Awair tests.""" -import json - -from homeassistant.const import CONF_ACCESS_TOKEN - -from tests.common import load_fixture +from homeassistant.components import zeroconf +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST AWAIR_UUID = "awair_24947" -CONFIG = {CONF_ACCESS_TOKEN: "12345"} -UNIQUE_ID = "foo@bar.com" -DEVICES_FIXTURE = json.loads(load_fixture("awair/devices.json")) -GEN1_DATA_FIXTURE = json.loads(load_fixture("awair/awair.json")) -GEN2_DATA_FIXTURE = json.loads(load_fixture("awair/awair-r2.json")) -GLOW_DATA_FIXTURE = json.loads(load_fixture("awair/glow.json")) -MINT_DATA_FIXTURE = json.loads(load_fixture("awair/mint.json")) -NO_DEVICES_FIXTURE = json.loads(load_fixture("awair/no_devices.json")) -OFFLINE_FIXTURE = json.loads(load_fixture("awair/awair-offline.json")) -OMNI_DATA_FIXTURE = json.loads(load_fixture("awair/omni.json")) -USER_FIXTURE = json.loads(load_fixture("awair/user.json")) +CLOUD_CONFIG = {CONF_ACCESS_TOKEN: "12345"} +LOCAL_CONFIG = {CONF_HOST: "192.0.2.5"} +CLOUD_UNIQUE_ID = "foo@bar.com" +LOCAL_UNIQUE_ID = "00:B0:D0:63:C2:26" +ZEROCONF_DISCOVERY = zeroconf.ZeroconfServiceInfo( + host="192.0.2.5", + addresses=["192.0.2.5"], + hostname="mock_hostname", + name="awair12345", + port=None, + type="_http._tcp.local.", + properties={}, +) diff --git a/tests/components/awair/fixtures/awair-local.json b/tests/components/awair/fixtures/awair-local.json new file mode 100644 index 00000000000..d793b8f4017 --- /dev/null +++ b/tests/components/awair/fixtures/awair-local.json @@ -0,0 +1,17 @@ +{ + "timestamp": "2022-08-11T05:04:12.108Z", + "score": 94, + "dew_point": 14.47, + "temp": 23.64, + "humid": 56.45, + "abs_humid": 12.0, + "co2": 426, + "co2_est": 489, + "co2_est_baseline": 37021, + "voc": 149, + "voc_baseline": 37783, + "voc_h2_raw": 26, + "voc_ethanol_raw": 37, + "pm25": 2, + "pm10_est": 3 +} diff --git a/tests/components/awair/fixtures/devices.json b/tests/components/awair/fixtures/cloud_devices.json similarity index 100% rename from tests/components/awair/fixtures/devices.json rename to tests/components/awair/fixtures/cloud_devices.json diff --git a/tests/components/awair/fixtures/local_devices.json b/tests/components/awair/fixtures/local_devices.json new file mode 100644 index 00000000000..d657020df96 --- /dev/null +++ b/tests/components/awair/fixtures/local_devices.json @@ -0,0 +1,16 @@ +{ + "device_uuid": "awair-element_24947", + "wifi_mac": "00:B0:D0:63:C2:26", + "ssid": "Internet of Things", + "ip": "192.0.2.5", + "netmask": "255.255.255.0", + "gateway": "none", + "fw_version": "1.2.8", + "timezone": "America/Los_Angeles", + "display": "score", + "led": { + "mode": "auto", + "brightness": 179 + }, + "voc_feature_set": 34 +} diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index b5bc5f23eaa..f6513321dfb 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -1,99 +1,143 @@ """Define tests for the Awair config flow.""" +from unittest.mock import Mock, patch -from unittest.mock import patch - +from aiohttp.client_exceptions import ClientConnectorError from python_awair.exceptions import AuthError, AwairError from homeassistant import data_entry_flow from homeassistant.components.awair.const import DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.core import HomeAssistant -from .const import CONFIG, DEVICES_FIXTURE, NO_DEVICES_FIXTURE, UNIQUE_ID, USER_FIXTURE +from .const import ( + CLOUD_CONFIG, + CLOUD_UNIQUE_ID, + LOCAL_CONFIG, + LOCAL_UNIQUE_ID, + ZEROCONF_DISCOVERY, +) from tests.common import MockConfigEntry -async def test_show_form(hass): +async def test_show_form(hass: HomeAssistant): """Test that the form is served with no input.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["type"] == data_entry_flow.FlowResultType.MENU assert result["step_id"] == SOURCE_USER -async def test_invalid_access_token(hass): +async def test_invalid_access_token(hass: HomeAssistant): """Test that errors are shown when the access token is invalid.""" with patch("python_awair.AwairClient.query", side_effect=AuthError()): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + menu_step = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CLOUD_CONFIG + ) + + form_step = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "cloud"}, + ) + + result = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + CLOUD_CONFIG, ) assert result["errors"] == {CONF_ACCESS_TOKEN: "invalid_access_token"} -async def test_unexpected_api_error(hass): +async def test_unexpected_api_error(hass: HomeAssistant): """Test that we abort on generic errors.""" with patch("python_awair.AwairClient.query", side_effect=AwairError()): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + menu_step = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CLOUD_CONFIG ) - assert result["type"] == "abort" + form_step = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "cloud"}, + ) + + result = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + CLOUD_CONFIG, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" -async def test_duplicate_error(hass): +async def test_duplicate_error(hass: HomeAssistant, user, cloud_devices): """Test that errors are shown when adding a duplicate config.""" with patch( - "python_awair.AwairClient.query", side_effect=[USER_FIXTURE, DEVICES_FIXTURE] - ), patch( - "homeassistant.components.awair.sensor.async_setup_entry", - return_value=True, + "python_awair.AwairClient.query", + side_effect=[user, cloud_devices], ): - MockConfigEntry(domain=DOMAIN, unique_id=UNIQUE_ID, data=CONFIG).add_to_hass( - hass + MockConfigEntry( + domain=DOMAIN, unique_id=CLOUD_UNIQUE_ID, data=CLOUD_CONFIG + ).add_to_hass(hass) + + menu_step = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CLOUD_CONFIG ) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + form_step = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "cloud"}, ) - assert result["type"] == "abort" - assert result["reason"] == "already_configured" + result = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + CLOUD_CONFIG, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured_account" -async def test_no_devices_error(hass): +async def test_no_devices_error(hass: HomeAssistant, user, no_devices): """Test that errors are shown when the API returns no devices.""" - with patch( - "python_awair.AwairClient.query", side_effect=[USER_FIXTURE, NO_DEVICES_FIXTURE] - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + with patch("python_awair.AwairClient.query", side_effect=[user, no_devices]): + menu_step = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CLOUD_CONFIG ) - assert result["type"] == "abort" + form_step = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "cloud"}, + ) + + result = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + CLOUD_CONFIG, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" -async def test_reauth(hass: HomeAssistant) -> None: +async def test_reauth(hass: HomeAssistant, user, cloud_devices) -> None: """Test reauth flow.""" mock_config = MockConfigEntry( - domain=DOMAIN, unique_id=UNIQUE_ID, data={**CONFIG, CONF_ACCESS_TOKEN: "blah"} + domain=DOMAIN, + unique_id=CLOUD_UNIQUE_ID, + data={**CLOUD_CONFIG, CONF_ACCESS_TOKEN: "blah"}, ) mock_config.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_REAUTH, "unique_id": UNIQUE_ID}, - data={**CONFIG, CONF_ACCESS_TOKEN: "blah"}, + context={"source": SOURCE_REAUTH, "unique_id": CLOUD_UNIQUE_ID}, + data={**CLOUD_CONFIG, CONF_ACCESS_TOKEN: "blah"}, ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -102,7 +146,7 @@ async def test_reauth(hass: HomeAssistant) -> None: with patch("python_awair.AwairClient.query", side_effect=AuthError()): result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=CONFIG, + user_input=CLOUD_CONFIG, ) assert result["type"] == data_entry_flow.FlowResultType.FORM @@ -110,11 +154,12 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result["errors"] == {CONF_ACCESS_TOKEN: "invalid_access_token"} with patch( - "python_awair.AwairClient.query", side_effect=[USER_FIXTURE, DEVICES_FIXTURE] + "python_awair.AwairClient.query", + side_effect=[user, cloud_devices], ), patch("homeassistant.components.awair.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=CONFIG, + user_input=CLOUD_CONFIG, ) assert result["type"] == data_entry_flow.FlowResultType.ABORT @@ -124,14 +169,16 @@ async def test_reauth(hass: HomeAssistant) -> None: async def test_reauth_error(hass: HomeAssistant) -> None: """Test reauth flow.""" mock_config = MockConfigEntry( - domain=DOMAIN, unique_id=UNIQUE_ID, data={**CONFIG, CONF_ACCESS_TOKEN: "blah"} + domain=DOMAIN, + unique_id=CLOUD_UNIQUE_ID, + data={**CLOUD_CONFIG, CONF_ACCESS_TOKEN: "blah"}, ) mock_config.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": SOURCE_REAUTH, "unique_id": UNIQUE_ID}, - data={**CONFIG, CONF_ACCESS_TOKEN: "blah"}, + context={"source": SOURCE_REAUTH, "unique_id": CLOUD_UNIQUE_ID}, + data={**CLOUD_CONFIG, CONF_ACCESS_TOKEN: "blah"}, ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -140,27 +187,127 @@ async def test_reauth_error(hass: HomeAssistant) -> None: with patch("python_awair.AwairClient.query", side_effect=AwairError()): result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=CONFIG, + user_input=CLOUD_CONFIG, ) assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "unknown" -async def test_create_entry(hass): - """Test overall flow.""" +async def test_create_cloud_entry(hass: HomeAssistant, user, cloud_devices): + """Test overall flow when using cloud api.""" with patch( - "python_awair.AwairClient.query", side_effect=[USER_FIXTURE, DEVICES_FIXTURE] + "python_awair.AwairClient.query", + side_effect=[user, cloud_devices], ), patch( - "homeassistant.components.awair.sensor.async_setup_entry", + "homeassistant.components.awair.async_setup_entry", return_value=True, ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + menu_step = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CLOUD_CONFIG + ) + + form_step = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "cloud"}, + ) + + result = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + CLOUD_CONFIG, ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "foo@bar.com (32406)" - assert result["data"][CONF_ACCESS_TOKEN] == CONFIG[CONF_ACCESS_TOKEN] - assert result["result"].unique_id == UNIQUE_ID + assert result["title"] == "foo@bar.com" + assert result["data"][CONF_ACCESS_TOKEN] == CLOUD_CONFIG[CONF_ACCESS_TOKEN] + assert result["result"].unique_id == CLOUD_UNIQUE_ID + + +async def test_create_local_entry(hass: HomeAssistant, local_devices): + """Test overall flow when using local API.""" + + with patch("python_awair.AwairClient.query", side_effect=[local_devices]), patch( + "homeassistant.components.awair.async_setup_entry", + return_value=True, + ): + menu_step = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=LOCAL_CONFIG + ) + + form_step = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "local"}, + ) + + result = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + LOCAL_CONFIG, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "Awair Element (24947)" + assert result["data"][CONF_HOST] == LOCAL_CONFIG[CONF_HOST] + assert result["result"].unique_id == LOCAL_UNIQUE_ID + + +async def test_create_local_entry_awair_error(hass: HomeAssistant): + """Test overall flow when using local API and device is returns error.""" + + with patch( + "python_awair.AwairClient.query", + side_effect=AwairError(), + ): + menu_step = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=LOCAL_CONFIG + ) + + form_step = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "local"}, + ) + + result = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + LOCAL_CONFIG, + ) + + # User is returned to form to try again + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "local" + + +async def test_create_zeroconf_entry(hass: HomeAssistant, local_devices): + """Test overall flow when using discovery.""" + + with patch("python_awair.AwairClient.query", side_effect=[local_devices]), patch( + "homeassistant.components.awair.async_setup_entry", + return_value=True, + ): + confirm_step = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=ZEROCONF_DISCOVERY + ) + + result = await hass.config_entries.flow.async_configure( + confirm_step["flow_id"], + {}, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "Awair Element (24947)" + assert result["data"][CONF_HOST] == ZEROCONF_DISCOVERY.host + assert result["result"].unique_id == LOCAL_UNIQUE_ID + + +async def test_unsuccessful_create_zeroconf_entry(hass: HomeAssistant): + """Test overall flow when using discovery and device is unreachable.""" + + with patch( + "python_awair.AwairClient.query", + side_effect=ClientConnectorError(Mock(), OSError()), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=ZEROCONF_DISCOVERY + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index 07b2f9ba00f..87b931a3f7f 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -26,21 +26,16 @@ from homeassistant.const import ( STATE_UNAVAILABLE, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_component import async_update_entity from .const import ( AWAIR_UUID, - CONFIG, - DEVICES_FIXTURE, - GEN1_DATA_FIXTURE, - GEN2_DATA_FIXTURE, - GLOW_DATA_FIXTURE, - MINT_DATA_FIXTURE, - OFFLINE_FIXTURE, - OMNI_DATA_FIXTURE, - UNIQUE_ID, - USER_FIXTURE, + CLOUD_CONFIG, + CLOUD_UNIQUE_ID, + LOCAL_CONFIG, + LOCAL_UNIQUE_ID, ) from tests.common import MockConfigEntry @@ -50,10 +45,10 @@ SENSOR_TYPES_MAP = { } -async def setup_awair(hass, fixtures): +async def setup_awair(hass: HomeAssistant, fixtures, unique_id, data): """Add Awair devices to hass, using specified fixtures for data.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=UNIQUE_ID, data=CONFIG) + entry = MockConfigEntry(domain=DOMAIN, unique_id=unique_id, data=data) with patch("python_awair.AwairClient.query", side_effect=fixtures): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -61,7 +56,12 @@ async def setup_awair(hass, fixtures): def assert_expected_properties( - hass, registry, name, unique_id, state_value, attributes + hass: HomeAssistant, + registry: er.RegistryEntry, + name, + unique_id, + state_value, + attributes: dict, ): """Assert expected properties from a dict.""" @@ -74,11 +74,11 @@ def assert_expected_properties( assert state.attributes.get(attr) == value -async def test_awair_gen1_sensors(hass): +async def test_awair_gen1_sensors(hass: HomeAssistant, user, cloud_devices, gen1_data): """Test expected sensors on a 1st gen Awair.""" - fixtures = [USER_FIXTURE, DEVICES_FIXTURE, GEN1_DATA_FIXTURE] - await setup_awair(hass, fixtures) + fixtures = [user, cloud_devices, gen1_data] + await setup_awair(hass, fixtures, CLOUD_UNIQUE_ID, CLOUD_CONFIG) registry = er.async_get(hass) assert_expected_properties( @@ -166,11 +166,11 @@ async def test_awair_gen1_sensors(hass): assert hass.states.get("sensor.living_room_illuminance") is None -async def test_awair_gen2_sensors(hass): +async def test_awair_gen2_sensors(hass: HomeAssistant, user, cloud_devices, gen2_data): """Test expected sensors on a 2nd gen Awair.""" - fixtures = [USER_FIXTURE, DEVICES_FIXTURE, GEN2_DATA_FIXTURE] - await setup_awair(hass, fixtures) + fixtures = [user, cloud_devices, gen2_data] + await setup_awair(hass, fixtures, CLOUD_UNIQUE_ID, CLOUD_CONFIG) registry = er.async_get(hass) assert_expected_properties( @@ -199,11 +199,28 @@ async def test_awair_gen2_sensors(hass): assert hass.states.get("sensor.living_room_pm10") is None -async def test_awair_mint_sensors(hass): +async def test_local_awair_sensors(hass: HomeAssistant, local_devices, local_data): + """Test expected sensors on a local Awair.""" + + fixtures = [local_devices, local_data] + await setup_awair(hass, fixtures, LOCAL_UNIQUE_ID, LOCAL_CONFIG) + registry = er.async_get(hass) + + assert_expected_properties( + hass, + registry, + "sensor.awair_score", + f"{local_devices['device_uuid']}_{SENSOR_TYPES_MAP[API_SCORE].unique_id_tag}", + "94", + {}, + ) + + +async def test_awair_mint_sensors(hass: HomeAssistant, user, cloud_devices, mint_data): """Test expected sensors on an Awair mint.""" - fixtures = [USER_FIXTURE, DEVICES_FIXTURE, MINT_DATA_FIXTURE] - await setup_awair(hass, fixtures) + fixtures = [user, cloud_devices, mint_data] + await setup_awair(hass, fixtures, CLOUD_UNIQUE_ID, CLOUD_CONFIG) registry = er.async_get(hass) assert_expected_properties( @@ -240,11 +257,11 @@ async def test_awair_mint_sensors(hass): assert hass.states.get("sensor.living_room_carbon_dioxide") is None -async def test_awair_glow_sensors(hass): +async def test_awair_glow_sensors(hass: HomeAssistant, user, cloud_devices, glow_data): """Test expected sensors on an Awair glow.""" - fixtures = [USER_FIXTURE, DEVICES_FIXTURE, GLOW_DATA_FIXTURE] - await setup_awair(hass, fixtures) + fixtures = [user, cloud_devices, glow_data] + await setup_awair(hass, fixtures, CLOUD_UNIQUE_ID, CLOUD_CONFIG) registry = er.async_get(hass) assert_expected_properties( @@ -260,11 +277,11 @@ async def test_awair_glow_sensors(hass): assert hass.states.get("sensor.living_room_pm2_5") is None -async def test_awair_omni_sensors(hass): +async def test_awair_omni_sensors(hass: HomeAssistant, user, cloud_devices, omni_data): """Test expected sensors on an Awair omni.""" - fixtures = [USER_FIXTURE, DEVICES_FIXTURE, OMNI_DATA_FIXTURE] - await setup_awair(hass, fixtures) + fixtures = [user, cloud_devices, omni_data] + await setup_awair(hass, fixtures, CLOUD_UNIQUE_ID, CLOUD_CONFIG) registry = er.async_get(hass) assert_expected_properties( @@ -295,11 +312,11 @@ async def test_awair_omni_sensors(hass): ) -async def test_awair_offline(hass): +async def test_awair_offline(hass: HomeAssistant, user, cloud_devices, awair_offline): """Test expected behavior when an Awair is offline.""" - fixtures = [USER_FIXTURE, DEVICES_FIXTURE, OFFLINE_FIXTURE] - await setup_awair(hass, fixtures) + fixtures = [user, cloud_devices, awair_offline] + await setup_awair(hass, fixtures, CLOUD_UNIQUE_ID, CLOUD_CONFIG) # The expected behavior is that we won't have any sensors # if the device is not online when we set it up. python_awair @@ -313,11 +330,13 @@ async def test_awair_offline(hass): assert hass.states.get("sensor.living_room_awair_score") is None -async def test_awair_unavailable(hass): +async def test_awair_unavailable( + hass: HomeAssistant, user, cloud_devices, gen1_data, awair_offline +): """Test expected behavior when an Awair becomes offline later.""" - fixtures = [USER_FIXTURE, DEVICES_FIXTURE, GEN1_DATA_FIXTURE] - await setup_awair(hass, fixtures) + fixtures = [user, cloud_devices, gen1_data] + await setup_awair(hass, fixtures, CLOUD_UNIQUE_ID, CLOUD_CONFIG) registry = er.async_get(hass) assert_expected_properties( @@ -329,7 +348,7 @@ async def test_awair_unavailable(hass): {}, ) - with patch("python_awair.AwairClient.query", side_effect=OFFLINE_FIXTURE): + with patch("python_awair.AwairClient.query", side_effect=awair_offline): await async_update_entity(hass, "sensor.living_room_awair_score") assert_expected_properties( hass, From f0827a20c3c0014de7e28dbeba76fc3f2e74fc70 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 11 Aug 2022 16:14:01 +0200 Subject: [PATCH 3289/3516] Add schedule helper (#76566) Co-authored-by: Paulus Schoutsen --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/default_config/manifest.json | 1 + homeassistant/components/schedule/__init__.py | 314 +++++++++++++++ homeassistant/components/schedule/const.py | 37 ++ .../components/schedule/manifest.json | 8 + homeassistant/components/schedule/recorder.py | 16 + .../components/schedule/services.yaml | 3 + .../components/schedule/strings.json | 9 + .../components/schedule/translations/en.json | 9 + mypy.ini | 11 + script/hassfest/manifest.py | 1 + tests/components/schedule/__init__.py | 1 + tests/components/schedule/test_init.py | 376 ++++++++++++++++++ tests/components/schedule/test_recorder.py | 70 ++++ 15 files changed, 859 insertions(+) create mode 100644 homeassistant/components/schedule/__init__.py create mode 100644 homeassistant/components/schedule/const.py create mode 100644 homeassistant/components/schedule/manifest.json create mode 100644 homeassistant/components/schedule/recorder.py create mode 100644 homeassistant/components/schedule/services.yaml create mode 100644 homeassistant/components/schedule/strings.json create mode 100644 homeassistant/components/schedule/translations/en.json create mode 100644 tests/components/schedule/__init__.py create mode 100644 tests/components/schedule/test_init.py create mode 100644 tests/components/schedule/test_recorder.py diff --git a/.strict-typing b/.strict-typing index cc1af9926bd..1574ea09f2e 100644 --- a/.strict-typing +++ b/.strict-typing @@ -213,6 +213,7 @@ homeassistant.components.rpi_power.* homeassistant.components.rtsp_to_webrtc.* homeassistant.components.samsungtv.* homeassistant.components.scene.* +homeassistant.components.schedule.* homeassistant.components.select.* homeassistant.components.sensibo.* homeassistant.components.sensor.* diff --git a/CODEOWNERS b/CODEOWNERS index 8de29fd5ede..5de44d30fb3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -921,6 +921,8 @@ build.json @home-assistant/supervisor /tests/components/samsungtv/ @chemelli74 @epenet /homeassistant/components/scene/ @home-assistant/core /tests/components/scene/ @home-assistant/core +/homeassistant/components/schedule/ @home-assistant/core +/tests/components/schedule/ @home-assistant/core /homeassistant/components/schluter/ @prairieapps /homeassistant/components/scrape/ @fabaff /tests/components/scrape/ @fabaff diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index f790292c27a..593ac26dbc9 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -27,6 +27,7 @@ "network", "person", "scene", + "schedule", "script", "ssdp", "sun", diff --git a/homeassistant/components/schedule/__init__.py b/homeassistant/components/schedule/__init__.py new file mode 100644 index 00000000000..023cfef99e1 --- /dev/null +++ b/homeassistant/components/schedule/__init__.py @@ -0,0 +1,314 @@ +"""Support for schedules in Home Assistant.""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime, timedelta +import itertools +import logging +from typing import Literal + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_EDITABLE, + CONF_ICON, + CONF_ID, + CONF_NAME, + SERVICE_RELOAD, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.helpers.collection import ( + IDManager, + StorageCollection, + StorageCollectionWebsocket, + YamlCollection, + sync_entity_lifecycle, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.integration_platform import ( + async_process_integration_platform_for_component, +) +from homeassistant.helpers.service import async_register_admin_service +from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType +from homeassistant.util import dt as dt_util + +from .const import ( + ATTR_NEXT_EVENT, + CONF_ALL_DAYS, + CONF_FROM, + CONF_TO, + DOMAIN, + LOGGER, + WEEKDAY_TO_CONF, +) + +STORAGE_VERSION = 1 +STORAGE_VERSION_MINOR = 1 + + +def valid_schedule(schedule: list[dict[str, str]]) -> list[dict[str, str]]: + """Validate the schedule of time ranges. + + Ensure they have no overlap and the end time is greater than the start time. + """ + # Emtpty schedule is valid + if not schedule: + return schedule + + # Sort the schedule by start times + schedule = sorted(schedule, key=lambda time_range: time_range[CONF_FROM]) + + # Check if the start time of the next event is before the end time of the previous event + previous_to = None + for time_range in schedule: + if time_range[CONF_FROM] >= time_range[CONF_TO]: + raise vol.Invalid( + f"Invalid time range, from {time_range[CONF_FROM]} is after {time_range[CONF_TO]}" + ) + + # Check if the from time of the event is after the to time of the previous event + if previous_to is not None and previous_to > time_range[CONF_FROM]: # type: ignore[unreachable] + raise vol.Invalid("Overlapping times found in schedule") + + previous_to = time_range[CONF_TO] + + return schedule + + +BASE_SCHEMA = { + vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), + vol.Optional(CONF_ICON): cv.icon, +} + +TIME_RANGE_SCHEMA = { + vol.Required(CONF_FROM): cv.time, + vol.Required(CONF_TO): cv.time, +} +STORAGE_TIME_RANGE_SCHEMA = vol.Schema( + { + vol.Required(CONF_FROM): vol.All(cv.time, vol.Coerce(str)), + vol.Required(CONF_TO): vol.All(cv.time, vol.Coerce(str)), + } +) + +SCHEDULE_SCHEMA = { + vol.Optional(day, default=[]): vol.All( + cv.ensure_list, [TIME_RANGE_SCHEMA], valid_schedule + ) + for day in CONF_ALL_DAYS +} +STORAGE_SCHEDULE_SCHEMA = { + vol.Optional(day, default=[]): vol.All( + cv.ensure_list, [TIME_RANGE_SCHEMA], valid_schedule, [STORAGE_TIME_RANGE_SCHEMA] + ) + for day in CONF_ALL_DAYS +} + + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: cv.schema_with_slug_keys(vol.All(BASE_SCHEMA | SCHEDULE_SCHEMA))}, + extra=vol.ALLOW_EXTRA, +) +STORAGE_SCHEMA = vol.Schema( + {vol.Required(CONF_ID): cv.string} | BASE_SCHEMA | SCHEDULE_SCHEMA +) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up an input select.""" + component = EntityComponent(LOGGER, DOMAIN, hass) + + # Process integration platforms right away since + # we will create entities before firing EVENT_COMPONENT_LOADED + await async_process_integration_platform_for_component(hass, DOMAIN) + + id_manager = IDManager() + + yaml_collection = YamlCollection(LOGGER, id_manager) + sync_entity_lifecycle( + hass, DOMAIN, DOMAIN, component, yaml_collection, Schedule.from_yaml + ) + + storage_collection = ScheduleStorageCollection( + Store( + hass, + key=DOMAIN, + version=STORAGE_VERSION, + minor_version=STORAGE_VERSION_MINOR, + ), + logging.getLogger(f"{__name__}.storage_collection"), + id_manager, + ) + sync_entity_lifecycle(hass, DOMAIN, DOMAIN, component, storage_collection, Schedule) + + await yaml_collection.async_load( + [{CONF_ID: id_, **cfg} for id_, cfg in config.get(DOMAIN, {}).items()] + ) + await storage_collection.async_load() + + StorageCollectionWebsocket( + storage_collection, + DOMAIN, + DOMAIN, + BASE_SCHEMA | STORAGE_SCHEDULE_SCHEMA, + BASE_SCHEMA | STORAGE_SCHEDULE_SCHEMA, + ).async_setup(hass) + + async def reload_service_handler(service_call: ServiceCall) -> None: + """Reload yaml entities.""" + conf = await component.async_prepare_reload(skip_reset=True) + if conf is None: + conf = {DOMAIN: {}} + await yaml_collection.async_load( + [{CONF_ID: id_, **cfg} for id_, cfg in conf.get(DOMAIN, {}).items()] + ) + + async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + ) + + return True + + +class ScheduleStorageCollection(StorageCollection): + """Schedules stored in storage.""" + + SCHEMA = vol.Schema(BASE_SCHEMA | STORAGE_SCHEDULE_SCHEMA) + + async def _process_create_data(self, data: dict) -> dict: + """Validate the config is valid.""" + self.SCHEMA(data) + return data + + @callback + def _get_suggested_id(self, info: dict) -> str: + """Suggest an ID based on the config.""" + name: str = info[CONF_NAME] + return name + + async def _update_data(self, data: dict, update_data: dict) -> dict: + """Return a new updated data object.""" + self.SCHEMA(update_data) + return data | update_data + + async def _async_load_data(self) -> dict | None: + """Load the data.""" + if data := await super()._async_load_data(): + data["items"] = [STORAGE_SCHEMA(item) for item in data["items"]] + return data + + +class Schedule(Entity): + """Schedule entity.""" + + _attr_has_entity_name = True + _attr_should_poll = False + _attr_state: Literal["on", "off"] + _config: ConfigType + _next: datetime + _unsub_update: Callable[[], None] | None = None + + def __init__(self, config: ConfigType, editable: bool = True) -> None: + """Initialize a schedule.""" + self._config = STORAGE_SCHEMA(config) + self._attr_capability_attributes = {ATTR_EDITABLE: editable} + self._attr_icon = self._config.get(CONF_ICON) + self._attr_name = self._config[CONF_NAME] + self._attr_unique_id = self._config[CONF_ID] + + @classmethod + def from_yaml(cls, config: ConfigType) -> Schedule: + """Return entity instance initialized from yaml storage.""" + schedule = cls(config, editable=False) + schedule.entity_id = f"{DOMAIN}.{config[CONF_ID]}" + return schedule + + async def async_update_config(self, config: ConfigType) -> None: + """Handle when the config is updated.""" + self._config = STORAGE_SCHEMA(config) + self._attr_icon = config.get(CONF_ICON) + self._attr_name = config[CONF_NAME] + self._clean_up_listener() + self._update() + + @callback + def _clean_up_listener(self) -> None: + """Remove the update timer.""" + if self._unsub_update is not None: + self._unsub_update() + self._unsub_update = None + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + self.async_on_remove(self._clean_up_listener) + self._update() + + @callback + def _update(self, _: datetime | None = None) -> None: + """Update the states of the schedule.""" + now = dt_util.now() + todays_schedule = self._config.get(WEEKDAY_TO_CONF[now.weekday()], []) + + # Determine current schedule state + self._attr_state = next( + ( + STATE_ON + for time_range in todays_schedule + if time_range[CONF_FROM] <= now.time() <= time_range[CONF_TO] + ), + STATE_OFF, + ) + + # Find next event in the schedule, loop over each day (starting with + # the current day) until the next event has been found. + next_event = None + for day in range(7): + day_schedule = self._config.get( + WEEKDAY_TO_CONF[(now.weekday() + day) % 7], [] + ) + times = sorted( + itertools.chain( + *[ + [time_range[CONF_FROM], time_range[CONF_TO]] + for time_range in day_schedule + ] + ) + ) + + if next_event := next( + ( + possible_next_event + for time in times + if ( + possible_next_event := ( + datetime.combine(now.date(), time, tzinfo=now.tzinfo) + + timedelta(days=day) + ) + ) + > now + ), + None, + ): + # We have found the next event in this day, stop searching. + break + + self._attr_extra_state_attributes = { + ATTR_NEXT_EVENT: next_event, + } + self.async_write_ha_state() + + if next_event: + self._unsub_update = async_track_point_in_utc_time( + self.hass, + self._update, + next_event, + ) diff --git a/homeassistant/components/schedule/const.py b/homeassistant/components/schedule/const.py new file mode 100644 index 00000000000..e044a614e4d --- /dev/null +++ b/homeassistant/components/schedule/const.py @@ -0,0 +1,37 @@ +"""Constants for the schedule integration.""" +import logging +from typing import Final + +DOMAIN: Final = "schedule" +LOGGER = logging.getLogger(__package__) + +CONF_FRIDAY: Final = "friday" +CONF_FROM: Final = "from" +CONF_MONDAY: Final = "monday" +CONF_SATURDAY: Final = "saturday" +CONF_SUNDAY: Final = "sunday" +CONF_THURSDAY: Final = "thursday" +CONF_TO: Final = "to" +CONF_TUESDAY: Final = "tuesday" +CONF_WEDNESDAY: Final = "wednesday" +CONF_ALL_DAYS: Final = { + CONF_MONDAY, + CONF_TUESDAY, + CONF_WEDNESDAY, + CONF_THURSDAY, + CONF_FRIDAY, + CONF_SATURDAY, + CONF_SUNDAY, +} + +ATTR_NEXT_EVENT: Final = "next_event" + +WEEKDAY_TO_CONF: Final = { + 0: CONF_MONDAY, + 1: CONF_TUESDAY, + 2: CONF_WEDNESDAY, + 3: CONF_THURSDAY, + 4: CONF_FRIDAY, + 5: CONF_SATURDAY, + 6: CONF_SUNDAY, +} diff --git a/homeassistant/components/schedule/manifest.json b/homeassistant/components/schedule/manifest.json new file mode 100644 index 00000000000..f36185e7ba7 --- /dev/null +++ b/homeassistant/components/schedule/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "schedule", + "integration_type": "helper", + "name": "Schedule", + "documentation": "https://www.home-assistant.io/integrations/schedule", + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" +} diff --git a/homeassistant/components/schedule/recorder.py b/homeassistant/components/schedule/recorder.py new file mode 100644 index 00000000000..b9911e0544b --- /dev/null +++ b/homeassistant/components/schedule/recorder.py @@ -0,0 +1,16 @@ +"""Integration platform for recorder.""" +from __future__ import annotations + +from homeassistant.const import ATTR_EDITABLE +from homeassistant.core import HomeAssistant, callback + +from .const import ATTR_NEXT_EVENT + + +@callback +def exclude_attributes(hass: HomeAssistant) -> set[str]: + """Exclude configuration to be recorded in the database.""" + return { + ATTR_EDITABLE, + ATTR_NEXT_EVENT, + } diff --git a/homeassistant/components/schedule/services.yaml b/homeassistant/components/schedule/services.yaml new file mode 100644 index 00000000000..b34dd5e83da --- /dev/null +++ b/homeassistant/components/schedule/services.yaml @@ -0,0 +1,3 @@ +reload: + name: Reload + description: Reload the schedule configuration diff --git a/homeassistant/components/schedule/strings.json b/homeassistant/components/schedule/strings.json new file mode 100644 index 00000000000..fdcb8c4ffdc --- /dev/null +++ b/homeassistant/components/schedule/strings.json @@ -0,0 +1,9 @@ +{ + "title": "Schedule", + "state": { + "_": { + "off": "[%key:common::state::off%]", + "on": "[%key:common::state::on%]" + } + } +} diff --git a/homeassistant/components/schedule/translations/en.json b/homeassistant/components/schedule/translations/en.json new file mode 100644 index 00000000000..7b161cddd18 --- /dev/null +++ b/homeassistant/components/schedule/translations/en.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Off", + "on": "On" + } + }, + "title": "Schedule" +} \ No newline at end of file diff --git a/mypy.ini b/mypy.ini index 26f76c24a02..700d96a4982 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2066,6 +2066,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.schedule.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.select.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 5ce67b59198..1e6bd03f457 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -83,6 +83,7 @@ NO_IOT_CLASS = [ "raspberry_pi", "repairs", "safe_mode", + "schedule", "script", "search", "system_health", diff --git a/tests/components/schedule/__init__.py b/tests/components/schedule/__init__.py new file mode 100644 index 00000000000..86e5cb9a27d --- /dev/null +++ b/tests/components/schedule/__init__.py @@ -0,0 +1 @@ +"""Tests for the schedule integration.""" diff --git a/tests/components/schedule/test_init.py b/tests/components/schedule/test_init.py new file mode 100644 index 00000000000..d559dc27a9a --- /dev/null +++ b/tests/components/schedule/test_init.py @@ -0,0 +1,376 @@ +"""Test for the Schedule integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable, Coroutine +from typing import Any +from unittest.mock import patch + +from aiohttp import ClientWebSocketResponse +from freezegun import freeze_time +import pytest + +from homeassistant.components.schedule import STORAGE_VERSION, STORAGE_VERSION_MINOR +from homeassistant.components.schedule.const import ( + ATTR_NEXT_EVENT, + CONF_FRIDAY, + CONF_FROM, + CONF_MONDAY, + CONF_SATURDAY, + CONF_SUNDAY, + CONF_THURSDAY, + CONF_TO, + CONF_TUESDAY, + CONF_WEDNESDAY, + DOMAIN, +) +from homeassistant.const import ( + ATTR_EDITABLE, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_NAME, + CONF_ICON, + CONF_ID, + CONF_NAME, + SERVICE_RELOAD, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from tests.common import MockUser, async_fire_time_changed + + +@pytest.fixture +def schedule_setup( + hass: HomeAssistant, hass_storage: dict[str, Any] +) -> Callable[..., Coroutine[Any, Any, bool]]: + """Schedule setup.""" + + async def _schedule_setup( + items: dict[str, Any] | None = None, + config: dict[str, Any] | None = None, + ) -> bool: + if items is None: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": STORAGE_VERSION, + "minor_version": STORAGE_VERSION_MINOR, + "data": { + "items": [ + { + CONF_ID: "from_storage", + CONF_NAME: "from storage", + CONF_ICON: "mdi:party-popper", + CONF_FRIDAY: [ + {CONF_FROM: "17:00:00", CONF_TO: "23:59:59"}, + ], + CONF_SATURDAY: [ + {CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}, + ], + CONF_SUNDAY: [ + {CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}, + ], + } + ] + }, + } + else: + hass_storage[DOMAIN] = { + "key": DOMAIN, + "version": 1, + "minor_version": STORAGE_VERSION_MINOR, + "data": {"items": items}, + } + if config is None: + config = { + DOMAIN: { + "from_yaml": { + CONF_NAME: "from yaml", + CONF_ICON: "mdi:party-pooper", + CONF_MONDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}], + CONF_TUESDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}], + CONF_WEDNESDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}], + CONF_THURSDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}], + CONF_FRIDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}], + CONF_SATURDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}], + CONF_SUNDAY: [{CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}], + } + } + } + return await async_setup_component(hass, DOMAIN, config) + + return _schedule_setup + + +async def test_invalid_config(hass: HomeAssistant) -> None: + """Test invalid configs.""" + invalid_configs = [ + None, + {}, + {"name with space": None}, + ] + + for cfg in invalid_configs: + assert not await async_setup_component(hass, DOMAIN, {DOMAIN: cfg}) + + +@pytest.mark.parametrize( + "schedule,error", + ( + ( + [ + {CONF_FROM: "00:00:00", CONF_TO: "23:59:59"}, + {CONF_FROM: "07:00:00", CONF_TO: "08:00:00"}, + ], + "Overlapping times found in schedule", + ), + ( + [ + {CONF_FROM: "07:00:00", CONF_TO: "08:00:00"}, + {CONF_FROM: "07:00:00", CONF_TO: "08:00:00"}, + ], + "Overlapping times found in schedule", + ), + ( + [ + {CONF_FROM: "07:59:00", CONF_TO: "09:00:00"}, + {CONF_FROM: "07:00:00", CONF_TO: "08:00:00"}, + ], + "Overlapping times found in schedule", + ), + ( + [ + {CONF_FROM: "06:00:00", CONF_TO: "07:00:00"}, + {CONF_FROM: "06:59:00", CONF_TO: "08:00:00"}, + ], + "Overlapping times found in schedule", + ), + ( + [ + {CONF_FROM: "06:00:00", CONF_TO: "05:00:00"}, + ], + "Invalid time range, from 06:00:00 is after 05:00:00", + ), + ), +) +async def test_invalid_schedules( + hass: HomeAssistant, + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], + caplog: pytest.LogCaptureFixture, + schedule: list[dict[str, str]], + error: str, +) -> None: + """Test overlapping time ranges invalidate.""" + assert not await schedule_setup( + config={ + DOMAIN: { + "from_yaml": { + CONF_NAME: "from yaml", + CONF_ICON: "mdi:party-pooper", + CONF_SUNDAY: schedule, + } + } + } + ) + assert error in caplog.text + + +async def test_setup_no_config(hass: HomeAssistant, hass_admin_user: MockUser) -> None: + """Test component setup with no config.""" + count_start = len(hass.states.async_entity_ids()) + assert await async_setup_component(hass, DOMAIN, {}) + + with patch( + "homeassistant.config.load_yaml_config_file", autospec=True, return_value={} + ): + await hass.services.async_call( + DOMAIN, + SERVICE_RELOAD, + blocking=True, + context=Context(user_id=hass_admin_user.id), + ) + await hass.async_block_till_done() + + assert count_start == len(hass.states.async_entity_ids()) + + +@pytest.mark.freeze_time("2022-08-10 20:10:00-07:00") +async def test_load( + hass: HomeAssistant, + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], +) -> None: + """Test set up from storage and YAML.""" + assert await schedule_setup() + + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_FRIENDLY_NAME] == "from storage" + assert state.attributes[ATTR_EDITABLE] is True + assert state.attributes[ATTR_ICON] == "mdi:party-popper" + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-08-12T17:00:00-07:00" + + state = hass.states.get(f"{DOMAIN}.from_yaml") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_FRIENDLY_NAME] == "from yaml" + assert state.attributes[ATTR_EDITABLE] is False + assert state.attributes[ATTR_ICON] == "mdi:party-pooper" + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-08-10T23:59:59-07:00" + + +async def test_schedule_updates( + hass: HomeAssistant, + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], +) -> None: + """Test the schedule updates when time changes.""" + with freeze_time("2022-08-10 20:10:00-07:00"): + assert await schedule_setup() + + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-08-12T17:00:00-07:00" + + with freeze_time(state.attributes[ATTR_NEXT_EVENT]): + async_fire_time_changed(hass, state.attributes[ATTR_NEXT_EVENT]) + await hass.async_block_till_done() + + state = hass.states.get(f"{DOMAIN}.from_storage") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-08-12T23:59:59-07:00" + + +async def test_ws_list( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], +) -> None: + """Test listing via WS.""" + assert await schedule_setup() + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": f"{DOMAIN}/list"}) + resp = await client.receive_json() + assert resp["success"] + + result = {item["id"]: item for item in resp["result"]} + + assert len(result) == 1 + assert result["from_storage"][ATTR_NAME] == "from storage" + assert "from_yaml" not in result + + +async def test_ws_delete( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], +) -> None: + """Test WS delete cleans up entity registry.""" + ent_reg = er.async_get(hass) + + assert await schedule_setup() + + state = hass.states.get("schedule.from_storage") + assert state is not None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "from_storage") is not None + + client = await hass_ws_client(hass) + await client.send_json( + {"id": 1, "type": f"{DOMAIN}/delete", f"{DOMAIN}_id": "from_storage"} + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get("schedule.from_storage") + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "from_storage") is None + + +@pytest.mark.freeze_time("2022-08-10 20:10:00-07:00") +async def test_update( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], +) -> None: + """Test updating the schedule.""" + ent_reg = er.async_get(hass) + + assert await schedule_setup() + + state = hass.states.get("schedule.from_storage") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_FRIENDLY_NAME] == "from storage" + assert state.attributes[ATTR_ICON] == "mdi:party-popper" + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-08-12T17:00:00-07:00" + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "from_storage") is not None + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 1, + "type": f"{DOMAIN}/update", + f"{DOMAIN}_id": "from_storage", + CONF_NAME: "Party pooper", + CONF_ICON: "mdi:party-pooper", + CONF_MONDAY: [], + CONF_TUESDAY: [], + CONF_WEDNESDAY: [{CONF_FROM: "17:00:00", CONF_TO: "23:59:59"}], + CONF_THURSDAY: [], + CONF_FRIDAY: [], + CONF_SATURDAY: [], + CONF_SUNDAY: [], + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get("schedule.from_storage") + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_FRIENDLY_NAME] == "Party pooper" + assert state.attributes[ATTR_ICON] == "mdi:party-pooper" + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-08-10T23:59:59-07:00" + + +@pytest.mark.freeze_time("2022-08-11 8:52:00-07:00") +async def test_ws_create( + hass: HomeAssistant, + hass_ws_client: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]], + schedule_setup: Callable[..., Coroutine[Any, Any, bool]], +) -> None: + """Test create WS.""" + ent_reg = er.async_get(hass) + + assert await schedule_setup(items=[]) + + state = hass.states.get("schedule.party_mode") + assert state is None + assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "party_mode") is None + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": f"{DOMAIN}/create", + "name": "Party mode", + "icon": "mdi:party-popper", + "monday": [{"from": "12:00:00", "to": "14:00:00"}], + } + ) + resp = await client.receive_json() + assert resp["success"] + + state = hass.states.get("schedule.party_mode") + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_FRIENDLY_NAME] == "Party mode" + assert state.attributes[ATTR_EDITABLE] is True + assert state.attributes[ATTR_ICON] == "mdi:party-popper" + assert state.attributes[ATTR_NEXT_EVENT].isoformat() == "2022-08-15T12:00:00-07:00" diff --git a/tests/components/schedule/test_recorder.py b/tests/components/schedule/test_recorder.py new file mode 100644 index 00000000000..a105f388fc2 --- /dev/null +++ b/tests/components/schedule/test_recorder.py @@ -0,0 +1,70 @@ +"""The tests for recorder platform.""" +from __future__ import annotations + +from datetime import timedelta + +from homeassistant.components.recorder.db_schema import StateAttributes, States +from homeassistant.components.recorder.util import session_scope +from homeassistant.components.schedule.const import ATTR_NEXT_EVENT, DOMAIN +from homeassistant.const import ATTR_EDITABLE, ATTR_FRIENDLY_NAME, ATTR_ICON +from homeassistant.core import HomeAssistant, State +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import async_fire_time_changed +from tests.components.recorder.common import async_wait_recording_done + + +async def test_exclude_attributes( + hass: HomeAssistant, + recorder_mock: None, + enable_custom_integrations: None, +) -> None: + """Test attributes to be excluded.""" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "test": { + "name": "Party mode", + "icon": "mdi:party-popper", + "monday": [{"from": "1:00", "to": "2:00"}], + "tuesday": [{"from": "2:00", "to": "3:00"}], + "wednesday": [{"from": "3:00", "to": "4:00"}], + "thursday": [{"from": "5:00", "to": "6:00"}], + "friday": [{"from": "7:00", "to": "8:00"}], + "saturday": [{"from": "9:00", "to": "10:00"}], + "sunday": [{"from": "11:00", "to": "12:00"}], + } + } + }, + ) + + state = hass.states.get("schedule.test") + assert state + assert state.attributes[ATTR_EDITABLE] is False + assert state.attributes[ATTR_FRIENDLY_NAME] + assert state.attributes[ATTR_ICON] + assert state.attributes[ATTR_NEXT_EVENT] + + await hass.async_block_till_done() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) + await hass.async_block_till_done() + await async_wait_recording_done(hass) + + def _fetch_states() -> list[State]: + with session_scope(hass=hass) as session: + native_states = [] + for db_state, db_state_attributes in session.query(States, StateAttributes): + state = db_state.to_native() + state.attributes = db_state_attributes.to_native() + native_states.append(state) + return native_states + + states: list[State] = await hass.async_add_executor_job(_fetch_states) + assert len(states) == 1 + assert ATTR_EDITABLE not in states[0].attributes + assert ATTR_FRIENDLY_NAME in states[0].attributes + assert ATTR_ICON in states[0].attributes + assert ATTR_NEXT_EVENT not in states[0].attributes From 51ca74b3d12aed9c6384cb6a5a9c3cc1d3ff6c7c Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 11 Aug 2022 23:24:55 +0100 Subject: [PATCH 3290/3516] Fix titles for discoveries and device names in xiaomi_ble (#76618) --- .../components/xiaomi_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/xiaomi_ble/test_config_flow.py | 38 ++++++------- tests/components/xiaomi_ble/test_sensor.py | 56 +++++++++++-------- 5 files changed, 56 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index cdcac07b5c9..8c1d47ee423 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.8.1"], + "requirements": ["xiaomi-ble==0.8.2"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 3054c425ecc..0e4e9b2c021 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2480,7 +2480,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.8.1 +xiaomi-ble==0.8.2 # homeassistant.components.knx xknx==0.22.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a16718a237..762cdd16472 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1678,7 +1678,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.8.1 +xiaomi-ble==0.8.2 # homeassistant.components.knx xknx==0.22.1 diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index 32ba6be3322..1871b0ae387 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -39,7 +39,7 @@ async def test_async_step_bluetooth_valid_device(hass): result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "MMC_T201_1" + assert result2["title"] == "Baby Thermometer DD6FC1 (MMC-T201-1)" assert result2["data"] == {} assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" @@ -65,7 +65,7 @@ async def test_async_step_bluetooth_valid_device_but_missing_payload(hass): result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "LYWSD02MMC" + assert result2["title"] == "Temperature/Humidity Sensor 565384 (LYWSD03MMC)" assert result2["data"] == {} assert result2["result"].unique_id == "A4:C1:38:56:53:84" @@ -123,7 +123,7 @@ async def test_async_step_bluetooth_during_onboarding(hass): ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "MMC_T201_1" + assert result["title"] == "Baby Thermometer DD6FC1 (MMC-T201-1)" assert result["data"] == {} assert result["result"].unique_id == "00:81:F9:DD:6F:C1" assert len(mock_setup_entry.mock_calls) == 1 @@ -148,7 +148,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption(hass): user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "YLKG07YL" + assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -180,7 +180,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key(has user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "YLKG07YL" + assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -214,7 +214,7 @@ async def test_async_step_bluetooth_valid_device_legacy_encryption_wrong_key_len user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "YLKG07YL" + assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -238,7 +238,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "JTYJGD03MI" + assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -272,7 +272,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "JTYJGD03MI" + assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -306,7 +306,7 @@ async def test_async_step_bluetooth_valid_device_v4_encryption_wrong_key_length( ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "JTYJGD03MI" + assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -370,7 +370,7 @@ async def test_async_step_user_with_found_devices(hass): user_input={"address": "58:2D:34:35:93:21"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "LYWSDCGQ" + assert result2["title"] == "Temperature/Humidity Sensor 359321 (LYWSDCGQ)" assert result2["data"] == {} assert result2["result"].unique_id == "58:2D:34:35:93:21" @@ -405,7 +405,7 @@ async def test_async_step_user_short_payload(hass): result["flow_id"], user_input={} ) assert result3["type"] == FlowResultType.CREATE_ENTRY - assert result3["title"] == "LYWSD02MMC" + assert result3["title"] == "Temperature/Humidity Sensor 565384 (LYWSD03MMC)" assert result3["data"] == {} assert result3["result"].unique_id == "A4:C1:38:56:53:84" @@ -453,7 +453,7 @@ async def test_async_step_user_short_payload_then_full(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "LYWSD02MMC" + assert result2["title"] == "Temperature/Humidity Sensor 565384 (LYWSD03MMC)" assert result2["data"] == {"bindkey": "a115210eed7a88e50ad52662e732a9fb"} @@ -486,7 +486,7 @@ async def test_async_step_user_with_found_devices_v4_encryption(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "JTYJGD03MI" + assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -532,7 +532,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key(hass): ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "JTYJGD03MI" + assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -580,7 +580,7 @@ async def test_async_step_user_with_found_devices_v4_encryption_wrong_key_length ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "JTYJGD03MI" + assert result2["title"] == "Thermometer E39CBC (JTYJGD03MI)" assert result2["data"] == {"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"} assert result2["result"].unique_id == "54:EF:44:E3:9C:BC" @@ -613,7 +613,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption(hass): user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "YLKG07YL" + assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -658,7 +658,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key( user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "YLKG07YL" + assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -703,7 +703,7 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key_le user_input={"bindkey": "b853075158487ca39a5b5ea9"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "YLKG07YL" + assert result2["title"] == "Dimmer Switch C5988B (YLKG07YL/YLKG08YL)" assert result2["data"] == {"bindkey": "b853075158487ca39a5b5ea9"} assert result2["result"].unique_id == "F8:24:41:C5:98:8B" @@ -792,7 +792,7 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): user_input={"address": "00:81:F9:DD:6F:C1"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "MMC_T201_1" + assert result2["title"] == "Baby Thermometer DD6FC1 (MMC-T201-1)" assert result2["data"] == {} assert result2["result"].unique_id == "00:81:F9:DD:6F:C1" diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 063e4a22c2d..b49d65f58ae 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -39,10 +39,13 @@ async def test_sensors(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 2 - temp_sensor = hass.states.get("sensor.mmc_t201_1_temperature") + temp_sensor = hass.states.get("sensor.baby_thermometer_dd6fc1_temperature") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "36.8719980616822" - assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "MMC_T201_1 Temperature" + assert ( + temp_sensor_attribtes[ATTR_FRIENDLY_NAME] + == "Baby Thermometer DD6FC1 Temperature" + ) assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" @@ -86,10 +89,10 @@ async def test_xiaomi_formaldeyhde(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 - sensor = hass.states.get("sensor.test_device_formaldehyde") + sensor = hass.states.get("sensor.smart_flower_pot_6a3e7a_formaldehyde") sensor_attr = sensor.attributes assert sensor.state == "2.44" - assert sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Formaldehyde" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Formaldehyde" assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "mg/m³" assert sensor_attr[ATTR_STATE_CLASS] == "measurement" @@ -133,10 +136,10 @@ async def test_xiaomi_consumable(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 - sensor = hass.states.get("sensor.test_device_consumable") + sensor = hass.states.get("sensor.smart_flower_pot_6a3e7a_consumable") sensor_attr = sensor.attributes assert sensor.state == "96" - assert sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Consumable" + assert sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Consumable" assert sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "%" assert sensor_attr[ATTR_STATE_CLASS] == "measurement" @@ -180,17 +183,17 @@ async def test_xiaomi_battery_voltage(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 2 - volt_sensor = hass.states.get("sensor.test_device_voltage") + volt_sensor = hass.states.get("sensor.smart_flower_pot_6a3e7a_voltage") volt_sensor_attr = volt_sensor.attributes assert volt_sensor.state == "3.1" - assert volt_sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Voltage" + assert volt_sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Voltage" assert volt_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "V" assert volt_sensor_attr[ATTR_STATE_CLASS] == "measurement" - bat_sensor = hass.states.get("sensor.test_device_battery") + bat_sensor = hass.states.get("sensor.smart_flower_pot_6a3e7a_battery") bat_sensor_attr = bat_sensor.attributes assert bat_sensor.state == "100" - assert bat_sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Battery" + assert bat_sensor_attr[ATTR_FRIENDLY_NAME] == "Smart Flower Pot 6A3E7A Battery" assert bat_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "%" assert bat_sensor_attr[ATTR_STATE_CLASS] == "measurement" @@ -248,38 +251,42 @@ async def test_xiaomi_HHCCJCY01(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 5 - illum_sensor = hass.states.get("sensor.test_device_illuminance") + illum_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_illuminance") illum_sensor_attr = illum_sensor.attributes assert illum_sensor.state == "0" - assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Test Device Illuminance" + assert illum_sensor_attr[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Illuminance" assert illum_sensor_attr[ATTR_UNIT_OF_MEASUREMENT] == "lx" assert illum_sensor_attr[ATTR_STATE_CLASS] == "measurement" - cond_sensor = hass.states.get("sensor.test_device_conductivity") + cond_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_conductivity") cond_sensor_attribtes = cond_sensor.attributes assert cond_sensor.state == "599" - assert cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Conductivity" + assert ( + cond_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Conductivity" + ) assert cond_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "µS/cm" assert cond_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - moist_sensor = hass.states.get("sensor.test_device_moisture") + moist_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_moisture") moist_sensor_attribtes = moist_sensor.attributes assert moist_sensor.state == "64" - assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Moisture" + assert moist_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Moisture" assert moist_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" assert moist_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - temp_sensor = hass.states.get("sensor.test_device_temperature") + temp_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_temperature") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "24.4" - assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Temperature" + assert ( + temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Temperature" + ) assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" - batt_sensor = hass.states.get("sensor.test_device_battery") + batt_sensor = hass.states.get("sensor.plant_sensor_6a3e7a_battery") batt_sensor_attribtes = batt_sensor.attributes assert batt_sensor.state == "5" - assert batt_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Battery" + assert batt_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Plant Sensor 6A3E7A Battery" assert batt_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" assert batt_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" @@ -321,10 +328,15 @@ async def test_xiaomi_CGDK2(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 - temp_sensor = hass.states.get("sensor.test_device_temperature") + temp_sensor = hass.states.get( + "sensor.temperature_humidity_sensor_122089_temperature" + ) temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "22.6" - assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Test Device Temperature" + assert ( + temp_sensor_attribtes[ATTR_FRIENDLY_NAME] + == "Temperature/Humidity Sensor 122089 Temperature" + ) assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" From 4f064268b01cd7626596e711219515cf9241e665 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Aug 2022 13:55:52 -1000 Subject: [PATCH 3291/3516] Add missing _abort_if_unique_id_configured to ble integrations (#76624) --- .../components/govee_ble/config_flow.py | 1 + .../components/inkbird/config_flow.py | 1 + homeassistant/components/moat/config_flow.py | 1 + .../components/sensorpush/config_flow.py | 1 + .../components/xiaomi_ble/config_flow.py | 1 + .../components/govee_ble/test_config_flow.py | 30 +++++++++++++++++++ tests/components/inkbird/test_config_flow.py | 28 +++++++++++++++++ tests/components/moat/test_config_flow.py | 28 +++++++++++++++++ .../components/sensorpush/test_config_flow.py | 30 +++++++++++++++++++ .../components/xiaomi_ble/test_config_flow.py | 30 +++++++++++++++++++ 10 files changed, 151 insertions(+) diff --git a/homeassistant/components/govee_ble/config_flow.py b/homeassistant/components/govee_ble/config_flow.py index 1e3a5566bfd..47d73f2779a 100644 --- a/homeassistant/components/govee_ble/config_flow.py +++ b/homeassistant/components/govee_ble/config_flow.py @@ -67,6 +67,7 @@ class GoveeConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: address = user_input[CONF_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() return self.async_create_entry( title=self._discovered_devices[address], data={} ) diff --git a/homeassistant/components/inkbird/config_flow.py b/homeassistant/components/inkbird/config_flow.py index 21ed85e117e..524471bbcc7 100644 --- a/homeassistant/components/inkbird/config_flow.py +++ b/homeassistant/components/inkbird/config_flow.py @@ -67,6 +67,7 @@ class INKBIRDConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: address = user_input[CONF_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() return self.async_create_entry( title=self._discovered_devices[address], data={} ) diff --git a/homeassistant/components/moat/config_flow.py b/homeassistant/components/moat/config_flow.py index 6f51b62d110..0e1b4f89568 100644 --- a/homeassistant/components/moat/config_flow.py +++ b/homeassistant/components/moat/config_flow.py @@ -67,6 +67,7 @@ class MoatConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: address = user_input[CONF_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() return self.async_create_entry( title=self._discovered_devices[address], data={} ) diff --git a/homeassistant/components/sensorpush/config_flow.py b/homeassistant/components/sensorpush/config_flow.py index d10c2f481a6..63edd59a5b7 100644 --- a/homeassistant/components/sensorpush/config_flow.py +++ b/homeassistant/components/sensorpush/config_flow.py @@ -67,6 +67,7 @@ class SensorPushConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: address = user_input[CONF_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() return self.async_create_entry( title=self._discovered_devices[address], data={} ) diff --git a/homeassistant/components/xiaomi_ble/config_flow.py b/homeassistant/components/xiaomi_ble/config_flow.py index a05e703db6a..4ec3b66d0f9 100644 --- a/homeassistant/components/xiaomi_ble/config_flow.py +++ b/homeassistant/components/xiaomi_ble/config_flow.py @@ -205,6 +205,7 @@ class XiaomiConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: address = user_input[CONF_ADDRESS] await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() discovery = self._discovered_devices[address] self.context["title_placeholders"] = {"name": discovery.title} diff --git a/tests/components/govee_ble/test_config_flow.py b/tests/components/govee_ble/test_config_flow.py index a1b9fed3cd7..188672cdf18 100644 --- a/tests/components/govee_ble/test_config_flow.py +++ b/tests/components/govee_ble/test_config_flow.py @@ -78,6 +78,36 @@ async def test_async_step_user_with_found_devices(hass): assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.govee_ble.config_flow.async_discovered_service_info", + return_value=[GVH5177_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="4125DDBA-2774-4851-9889-6AADDD4CAC3D", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.govee_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "4125DDBA-2774-4851-9889-6AADDD4CAC3D"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + async def test_async_step_user_with_found_devices_already_setup(hass): """Test setup from service info cache with devices found.""" entry = MockConfigEntry( diff --git a/tests/components/inkbird/test_config_flow.py b/tests/components/inkbird/test_config_flow.py index c1f8b3ef545..fe210f75f4b 100644 --- a/tests/components/inkbird/test_config_flow.py +++ b/tests/components/inkbird/test_config_flow.py @@ -74,6 +74,34 @@ async def test_async_step_user_with_found_devices(hass): assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.inkbird.config_flow.async_discovered_service_info", + return_value=[SPS_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.inkbird.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + async def test_async_step_user_with_found_devices_already_setup(hass): """Test setup from service info cache with devices found.""" entry = MockConfigEntry( diff --git a/tests/components/moat/test_config_flow.py b/tests/components/moat/test_config_flow.py index 7ceeb2ad73f..6e92be703a2 100644 --- a/tests/components/moat/test_config_flow.py +++ b/tests/components/moat/test_config_flow.py @@ -74,6 +74,34 @@ async def test_async_step_user_with_found_devices(hass): assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.moat.config_flow.async_discovered_service_info", + return_value=[MOAT_S2_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.moat.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + async def test_async_step_user_with_found_devices_already_setup(hass): """Test setup from service info cache with devices found.""" entry = MockConfigEntry( diff --git a/tests/components/sensorpush/test_config_flow.py b/tests/components/sensorpush/test_config_flow.py index 1c825640603..244787eecb9 100644 --- a/tests/components/sensorpush/test_config_flow.py +++ b/tests/components/sensorpush/test_config_flow.py @@ -78,6 +78,36 @@ async def test_async_step_user_with_found_devices(hass): assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.sensorpush.config_flow.async_discovered_service_info", + return_value=[HTW_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.sensorpush.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + async def test_async_step_user_with_found_devices_already_setup(hass): """Test setup from service info cache with devices found.""" entry = MockConfigEntry( diff --git a/tests/components/xiaomi_ble/test_config_flow.py b/tests/components/xiaomi_ble/test_config_flow.py index 1871b0ae387..6a1c9c8e435 100644 --- a/tests/components/xiaomi_ble/test_config_flow.py +++ b/tests/components/xiaomi_ble/test_config_flow.py @@ -708,6 +708,36 @@ async def test_async_step_user_with_found_devices_legacy_encryption_wrong_key_le assert result2["result"].unique_id == "F8:24:41:C5:98:8B" +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.xiaomi_ble.config_flow.async_discovered_service_info", + return_value=[LYWSDCGQ_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="58:2D:34:35:93:21", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.xiaomi_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "58:2D:34:35:93:21"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + async def test_async_step_user_with_found_devices_already_setup(hass): """Test setup from service info cache with devices found.""" entry = MockConfigEntry( From 7c81f790a71a26bf33d7fda4b8f0a76f26cb0b00 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 12 Aug 2022 00:23:47 +0000 Subject: [PATCH 3292/3516] [ci skip] Translation update --- .../components/adax/translations/es.json | 10 +++--- .../components/adguard/translations/es.json | 2 +- .../components/aemet/translations/es.json | 2 +- .../components/airthings/translations/es.json | 2 +- .../airvisual/translations/sensor.es.json | 4 +-- .../components/airzone/translations/es.json | 2 +- .../alarm_control_panel/translations/es.json | 8 ++--- .../components/almond/translations/es.json | 2 +- .../components/ambee/translations/es.json | 4 +-- .../amberelectric/translations/es.json | 2 +- .../android_ip_webcam/translations/id.json | 26 ++++++++++++++ .../android_ip_webcam/translations/pl.json | 26 ++++++++++++++ .../components/androidtv/translations/es.json | 26 +++++++------- .../components/apple_tv/translations/es.json | 14 ++++---- .../components/asuswrt/translations/es.json | 4 +-- .../components/august/translations/es.json | 8 ++--- .../aurora_abb_powerone/translations/es.json | 10 +++--- .../aussie_broadband/translations/es.json | 16 ++++----- .../components/auth/translations/es.json | 2 +- .../components/awair/translations/ca.json | 25 +++++++++++-- .../components/awair/translations/de.json | 31 ++++++++++++++-- .../components/awair/translations/en.json | 31 ++++++++++++++-- .../components/awair/translations/es.json | 31 ++++++++++++++-- .../components/awair/translations/et.json | 31 ++++++++++++++-- .../components/awair/translations/id.json | 31 ++++++++++++++-- .../components/awair/translations/pl.json | 31 ++++++++++++++-- .../azure_event_hub/translations/es.json | 14 ++++---- .../components/balboa/translations/es.json | 4 +-- .../binary_sensor/translations/es.json | 12 +++---- .../components/bosch_shc/translations/es.json | 10 +++--- .../components/brother/translations/es.json | 2 +- .../components/brunt/translations/es.json | 4 +-- .../components/bsblan/translations/es.json | 2 +- .../components/cast/translations/es.json | 8 ++--- .../components/climacell/translations/es.json | 6 ++-- .../climacell/translations/sensor.es.json | 10 +++--- .../cloudflare/translations/es.json | 2 +- .../components/co2signal/translations/es.json | 6 ++-- .../components/coinbase/translations/es.json | 6 ++-- .../components/cpuspeed/translations/es.json | 6 ++-- .../crownstone/translations/es.json | 34 +++++++++--------- .../components/daikin/translations/es.json | 2 +- .../components/deconz/translations/es.json | 2 +- .../components/deluge/translations/es.json | 6 ++-- .../components/demo/translations/pl.json | 13 ++++++- .../derivative/translations/es.json | 6 ++-- .../deutsche_bahn/translations/pl.json | 8 +++++ .../devolo_home_network/translations/es.json | 10 +++--- .../components/dlna_dmr/translations/es.json | 14 ++++---- .../components/dlna_dms/translations/es.json | 10 +++--- .../components/dnsip/translations/es.json | 12 +++---- .../components/dsmr/translations/es.json | 12 +++---- .../components/efergy/translations/es.json | 4 +-- .../components/elgato/translations/es.json | 2 +- .../components/elkm1/translations/es.json | 18 +++++----- .../components/elmax/translations/es.json | 8 ++--- .../components/emonitor/translations/es.json | 2 +- .../enphase_envoy/translations/es.json | 4 +-- .../environment_canada/translations/es.json | 4 +-- .../components/escea/translations/pl.json | 13 +++++++ .../components/esphome/translations/es.json | 10 +++--- .../components/ezviz/translations/es.json | 8 ++--- .../faa_delays/translations/es.json | 4 +-- .../components/fan/translations/es.json | 2 +- .../components/fibaro/translations/es.json | 4 +-- .../components/filesize/translations/es.json | 2 +- .../components/fivem/translations/es.json | 2 +- .../components/flipr/translations/es.json | 2 +- .../components/flume/translations/es.json | 2 +- .../flunearyou/translations/pl.json | 13 +++++++ .../components/flux_led/translations/es.json | 10 +++--- .../forecast_solar/translations/es.json | 10 +++--- .../freedompro/translations/es.json | 2 +- .../components/fritz/translations/es.json | 12 +++---- .../components/fronius/translations/es.json | 4 +-- .../garages_amsterdam/translations/es.json | 4 +-- .../components/geofency/translations/es.json | 2 +- .../components/github/translations/es.json | 6 ++-- .../components/goalzero/translations/es.json | 2 +- .../components/google/translations/es.json | 18 +++++----- .../google_travel_time/translations/es.json | 6 ++-- .../components/group/translations/es.json | 36 +++++++++---------- .../components/guardian/translations/de.json | 13 +++++++ .../components/guardian/translations/et.json | 13 +++++++ .../components/guardian/translations/hu.json | 13 +++++++ .../components/guardian/translations/id.json | 13 +++++++ .../components/guardian/translations/no.json | 13 +++++++ .../components/guardian/translations/pl.json | 13 +++++++ .../guardian/translations/pt-BR.json | 13 +++++++ .../guardian/translations/zh-Hant.json | 13 +++++++ .../components/habitica/translations/es.json | 2 +- .../hisense_aehw4a1/translations/es.json | 2 +- .../components/hive/translations/es.json | 16 ++++----- .../components/homekit/translations/es.json | 14 ++++---- .../homekit_controller/translations/es.json | 2 +- .../translations/select.es.json | 4 +-- .../homewizard/translations/es.json | 8 ++--- .../components/honeywell/translations/es.json | 6 ++-- .../huawei_lte/translations/es.json | 4 +-- .../components/hue/translations/es.json | 10 +++--- .../humidifier/translations/es.json | 2 +- .../translations/es.json | 2 +- .../components/hyperion/translations/es.json | 2 +- .../components/ifttt/translations/es.json | 2 +- .../integration/translations/es.json | 2 +- .../intellifire/translations/es.json | 6 ++-- .../components/iotawatt/translations/es.json | 4 +-- .../components/iss/translations/es.json | 2 +- .../justnimbus/translations/id.json | 19 ++++++++++ .../justnimbus/translations/pl.json | 19 ++++++++++ .../kaleidescape/translations/es.json | 6 ++-- .../keenetic_ndms2/translations/es.json | 16 ++++----- .../components/kmtronic/translations/es.json | 6 ++-- .../components/knx/translations/es.json | 20 +++++------ .../components/kodi/translations/es.json | 2 +- .../components/kraken/translations/es.json | 4 --- .../launch_library/translations/es.json | 2 +- .../components/lcn/translations/es.json | 2 +- .../components/life360/translations/es.json | 2 +- .../components/light/translations/es.json | 2 +- .../components/litejet/translations/es.json | 4 +-- .../litterrobot/translations/es.json | 8 ++--- .../media_player/translations/es.json | 2 +- .../met_eireann/translations/es.json | 2 +- .../components/mill/translations/es.json | 4 +-- .../components/min_max/translations/es.json | 2 +- .../components/mjpeg/translations/es.json | 24 ++++++------- .../modem_callerid/translations/es.json | 4 +-- .../modern_forms/translations/es.json | 4 +-- .../moehlenhoff_alpha2/translations/es.json | 2 +- .../components/moon/translations/es.json | 4 +-- .../motion_blinds/translations/es.json | 2 +- .../components/motioneye/translations/es.json | 10 +++--- .../components/mqtt/translations/es.json | 2 +- .../components/mullvad/translations/es.json | 2 +- .../components/mutesync/translations/es.json | 2 +- .../components/myq/translations/es.json | 2 +- .../components/mysensors/translations/pl.json | 9 +++++ .../components/nam/translations/es.json | 8 ++--- .../components/nanoleaf/translations/es.json | 4 +-- .../components/nest/translations/es.json | 10 +++--- .../components/netatmo/translations/es.json | 14 ++++---- .../components/netgear/translations/es.json | 8 ++--- .../nfandroidtv/translations/es.json | 2 +- .../components/nina/translations/es.json | 6 ++-- .../nmap_tracker/translations/es.json | 14 ++++---- .../components/notion/translations/es.json | 2 +- .../components/nuki/translations/es.json | 2 +- .../components/octoprint/translations/es.json | 6 ++-- .../components/omnilogic/translations/es.json | 2 +- .../components/oncue/translations/es.json | 2 +- .../components/onewire/translations/es.json | 10 +++--- .../open_meteo/translations/es.json | 2 +- .../openexchangerates/translations/pl.json | 33 +++++++++++++++++ .../opentherm_gw/translations/es.json | 2 +- .../components/overkiz/translations/es.json | 14 ++++---- .../overkiz/translations/sensor.es.json | 12 +++---- .../p1_monitor/translations/es.json | 2 +- .../philips_js/translations/es.json | 4 +-- .../components/picnic/translations/es.json | 2 +- .../components/plugwise/translations/es.json | 2 +- .../components/powerwall/translations/es.json | 18 +++++----- .../components/prosegur/translations/es.json | 2 +- .../pure_energie/translations/es.json | 10 +++--- .../components/pvoutput/translations/es.json | 6 ++-- .../radio_browser/translations/es.json | 4 +-- .../rainforest_eagle/translations/es.json | 2 +- .../components/remote/translations/es.json | 2 +- .../components/ridwell/translations/es.json | 6 ++-- .../translations/es.json | 4 +-- .../rtsp_to_webrtc/translations/es.json | 18 +++++----- .../components/samsungtv/translations/es.json | 14 ++++---- .../components/schedule/translations/ca.json | 9 +++++ .../components/schedule/translations/de.json | 9 +++++ .../components/schedule/translations/es.json | 9 +++++ .../components/schedule/translations/et.json | 9 +++++ .../components/schedule/translations/id.json | 9 +++++ .../components/schedule/translations/pl.json | 9 +++++ .../screenlogic/translations/es.json | 8 ++--- .../components/select/translations/es.json | 6 ++-- .../components/sense/translations/es.json | 10 +++--- .../components/senseme/translations/es.json | 6 ++-- .../components/sensibo/translations/es.json | 8 ++--- .../components/sensor/translations/es.json | 32 ++++++++--------- .../components/shelly/translations/es.json | 4 +-- .../components/sia/translations/es.json | 18 +++++----- .../simplepush/translations/pl.json | 4 +++ .../simplisafe/translations/es.json | 2 +- .../simplisafe/translations/pl.json | 3 +- .../components/sleepiq/translations/es.json | 12 +++---- .../components/smappee/translations/es.json | 2 +- .../components/smarttub/translations/es.json | 2 +- .../components/solax/translations/es.json | 2 +- .../components/steamist/translations/es.json | 10 +++--- .../components/subaru/translations/es.json | 6 ++-- .../components/sun/translations/es.json | 4 +-- .../components/switch/translations/es.json | 2 +- .../switch_as_x/translations/es.json | 2 +- .../components/switchbot/translations/de.json | 9 +++++ .../components/switchbot/translations/es.json | 2 +- .../components/switchbot/translations/et.json | 9 +++++ .../components/switchbot/translations/id.json | 9 +++++ .../components/switchbot/translations/no.json | 9 +++++ .../components/switchbot/translations/pl.json | 9 +++++ .../switchbot/translations/zh-Hant.json | 9 +++++ .../components/syncthing/translations/es.json | 2 +- .../synology_dsm/translations/es.json | 2 +- .../system_bridge/translations/es.json | 2 +- .../components/tailscale/translations/es.json | 8 ++--- .../tesla_wall_connector/translations/es.json | 4 +-- .../components/threshold/translations/es.json | 4 +-- .../components/tile/translations/es.json | 2 +- .../components/tod/translations/es.json | 12 +++---- .../components/tolo/translations/es.json | 2 +- .../tomorrowio/translations/es.json | 8 ++--- .../tomorrowio/translations/sensor.es.json | 8 ++--- .../totalconnect/translations/es.json | 4 +-- .../components/tplink/translations/es.json | 2 +- .../components/traccar/translations/es.json | 2 +- .../components/tractive/translations/es.json | 2 +- .../tractive/translations/sensor.es.json | 6 ++-- .../components/tradfri/translations/es.json | 2 +- .../translations/es.json | 4 +-- .../components/tuya/translations/es.json | 4 +-- .../tuya/translations/select.es.json | 16 ++++----- .../tuya/translations/sensor.es.json | 4 +-- .../unifiprotect/translations/es.json | 12 +++---- .../unifiprotect/translations/id.json | 1 + .../unifiprotect/translations/pl.json | 1 + .../components/uptime/translations/es.json | 4 +-- .../uptimerobot/translations/es.json | 12 +++---- .../uptimerobot/translations/sensor.es.json | 10 +++--- .../components/vallox/translations/es.json | 2 +- .../components/verisure/translations/es.json | 10 +++--- .../components/version/translations/es.json | 6 ++-- .../components/vicare/translations/es.json | 6 ++-- .../vlc_telnet/translations/es.json | 6 ++-- .../components/wallbox/translations/es.json | 2 +- .../components/watttime/translations/es.json | 14 ++++---- .../waze_travel_time/translations/es.json | 4 +-- .../components/webostv/translations/es.json | 14 ++++---- .../components/whois/translations/es.json | 2 +- .../components/wiz/translations/es.json | 16 ++++----- .../components/wled/translations/es.json | 4 +-- .../xiaomi_miio/translations/es.json | 12 +++---- .../yale_smart_alarm/translations/es.json | 12 +++---- .../yalexs_ble/translations/de.json | 30 ++++++++++++++++ .../yalexs_ble/translations/et.json | 30 ++++++++++++++++ .../yalexs_ble/translations/id.json | 30 ++++++++++++++++ .../yalexs_ble/translations/no.json | 30 ++++++++++++++++ .../yalexs_ble/translations/pl.json | 30 ++++++++++++++++ .../yalexs_ble/translations/zh-Hant.json | 30 ++++++++++++++++ .../yamaha_musiccast/translations/es.json | 2 +- .../components/zha/translations/es.json | 18 +++++----- .../components/zwave_js/translations/es.json | 34 +++++++++--------- .../components/zwave_me/translations/es.json | 2 +- 256 files changed, 1465 insertions(+), 724 deletions(-) create mode 100644 homeassistant/components/android_ip_webcam/translations/id.json create mode 100644 homeassistant/components/android_ip_webcam/translations/pl.json create mode 100644 homeassistant/components/deutsche_bahn/translations/pl.json create mode 100644 homeassistant/components/escea/translations/pl.json create mode 100644 homeassistant/components/justnimbus/translations/id.json create mode 100644 homeassistant/components/justnimbus/translations/pl.json create mode 100644 homeassistant/components/openexchangerates/translations/pl.json create mode 100644 homeassistant/components/schedule/translations/ca.json create mode 100644 homeassistant/components/schedule/translations/de.json create mode 100644 homeassistant/components/schedule/translations/es.json create mode 100644 homeassistant/components/schedule/translations/et.json create mode 100644 homeassistant/components/schedule/translations/id.json create mode 100644 homeassistant/components/schedule/translations/pl.json create mode 100644 homeassistant/components/yalexs_ble/translations/de.json create mode 100644 homeassistant/components/yalexs_ble/translations/et.json create mode 100644 homeassistant/components/yalexs_ble/translations/id.json create mode 100644 homeassistant/components/yalexs_ble/translations/no.json create mode 100644 homeassistant/components/yalexs_ble/translations/pl.json create mode 100644 homeassistant/components/yalexs_ble/translations/zh-Hant.json diff --git a/homeassistant/components/adax/translations/es.json b/homeassistant/components/adax/translations/es.json index ea350d97f4e..c2d224aacb1 100644 --- a/homeassistant/components/adax/translations/es.json +++ b/homeassistant/components/adax/translations/es.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "heater_not_available": "Calentador no disponible. Intente restablecer el calentador pulsando + y OK durante algunos segundos.", - "heater_not_found": "No se encuentra el calefactor. Intente acercar el calefactor al ordenador del Asistente de Hogar.", + "heater_not_available": "Calefactor no disponible. Intenta reiniciar el calentador presionando + y OK durante unos segundos.", + "heater_not_found": "No se encontr\u00f3 el calentador. Intenta acercar el calentador al ordenador con Home Assistant.", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "error": { @@ -21,13 +21,13 @@ "wifi_pswd": "Contrase\u00f1a Wi-Fi", "wifi_ssid": "SSID Wi-Fi" }, - "description": "Reinicie el calentador presionando + y OK hasta que la pantalla muestre 'Reiniciar'. Luego presione y mantenga presionado el bot\u00f3n OK en el calentador hasta que el LED azul comience a parpadear antes de presionar Enviar. La configuraci\u00f3n del calentador puede llevar algunos minutos." + "description": "Reinicia el calentador presionando + y OK hasta que la pantalla muestre 'Restablecer'. Luego mant\u00e9n presionado el bot\u00f3n OK en el calentador hasta que el led azul comience a parpadear antes de presionar Enviar. La configuraci\u00f3n del calentador puede tardar algunos minutos." }, "user": { "data": { - "connection_type": "Seleccione el tipo de conexi\u00f3n" + "connection_type": "Selecciona el tipo de conexi\u00f3n" }, - "description": "Seleccione el tipo de conexi\u00f3n. Local requiere calentadores con bluetooth" + "description": "Selecciona el tipo de conexi\u00f3n. Local requiere calefactores con bluetooth" } } } diff --git a/homeassistant/components/adguard/translations/es.json b/homeassistant/components/adguard/translations/es.json index f7bfb1203d7..96a4546f735 100644 --- a/homeassistant/components/adguard/translations/es.json +++ b/homeassistant/components/adguard/translations/es.json @@ -9,7 +9,7 @@ }, "step": { "hassio_confirm": { - "description": "\u00bfDesea configurar Home Assistant para conectarse al AdGuard Home proporcionado por el complemento Supervisor: {addon} ?", + "description": "\u00bfQuieres configurar Home Assistant para conectarse a AdGuard Home proporcionado por el complemento: {addon} ?", "title": "AdGuard Home v\u00eda complemento de Home Assistant" }, "user": { diff --git a/homeassistant/components/aemet/translations/es.json b/homeassistant/components/aemet/translations/es.json index b46f7720789..be22291c270 100644 --- a/homeassistant/components/aemet/translations/es.json +++ b/homeassistant/components/aemet/translations/es.json @@ -14,7 +14,7 @@ "longitude": "Longitud", "name": "Nombre de la integraci\u00f3n" }, - "description": "Configurar la integraci\u00f3n de AEMET OpenData. Para generar la clave API, ve a https://opendata.aemet.es/centrodedescargas/altaUsuario" + "description": "Para generar la clave API, ve a https://opendata.aemet.es/centrodedescargas/altaUsuario" } } }, diff --git a/homeassistant/components/airthings/translations/es.json b/homeassistant/components/airthings/translations/es.json index 5feddd20875..c5b0d338a03 100644 --- a/homeassistant/components/airthings/translations/es.json +++ b/homeassistant/components/airthings/translations/es.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "description": "Inicie sesi\u00f3n en {url} para encontrar sus credenciales", + "description": "Inicia sesi\u00f3n en {url} para encontrar tus credenciales", "id": "ID", "secret": "Secreto" } diff --git a/homeassistant/components/airvisual/translations/sensor.es.json b/homeassistant/components/airvisual/translations/sensor.es.json index 113c17246ed..b5385066e52 100644 --- a/homeassistant/components/airvisual/translations/sensor.es.json +++ b/homeassistant/components/airvisual/translations/sensor.es.json @@ -12,8 +12,8 @@ "good": "Bueno", "hazardous": "Da\u00f1ino", "moderate": "Moderado", - "unhealthy": "Insalubre", - "unhealthy_sensitive": "Insalubre para grupos sensibles", + "unhealthy": "Poco saludable", + "unhealthy_sensitive": "Poco saludable para grupos sensibles", "very_unhealthy": "Muy poco saludable" } } diff --git a/homeassistant/components/airzone/translations/es.json b/homeassistant/components/airzone/translations/es.json index 1fb525c9851..20c05fa714f 100644 --- a/homeassistant/components/airzone/translations/es.json +++ b/homeassistant/components/airzone/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_system_id": "ID del sistema Airzone no v\u00e1lido" }, "step": { diff --git a/homeassistant/components/alarm_control_panel/translations/es.json b/homeassistant/components/alarm_control_panel/translations/es.json index 6255f8cc574..38c4c059b48 100644 --- a/homeassistant/components/alarm_control_panel/translations/es.json +++ b/homeassistant/components/alarm_control_panel/translations/es.json @@ -4,7 +4,7 @@ "arm_away": "Armar {entity_name} exterior", "arm_home": "Armar {entity_name} modo casa", "arm_night": "Armar {entity_name} por la noche", - "arm_vacation": "Armar las vacaciones de {entity_name}", + "arm_vacation": "Armar de vacaciones {entity_name}", "disarm": "Desarmar {entity_name}", "trigger": "Lanzar {entity_name}" }, @@ -12,7 +12,7 @@ "is_armed_away": "{entity_name} est\u00e1 armada ausente", "is_armed_home": "{entity_name} est\u00e1 armada en casa", "is_armed_night": "{entity_name} est\u00e1 armada noche", - "is_armed_vacation": "{entity_name} est\u00e1 armado de vacaciones", + "is_armed_vacation": "{entity_name} est\u00e1 armada de vacaciones", "is_disarmed": "{entity_name} est\u00e1 desarmada", "is_triggered": "{entity_name} est\u00e1 disparada" }, @@ -20,7 +20,7 @@ "armed_away": "{entity_name} armada ausente", "armed_home": "{entity_name} armada en casa", "armed_night": "{entity_name} armada noche", - "armed_vacation": "Vacaciones armadas de {entity_name}", + "armed_vacation": "{entity_name} en armada de vacaciones", "disarmed": "{entity_name} desarmada", "triggered": "{entity_name} activado" } @@ -32,7 +32,7 @@ "armed_custom_bypass": "Armada personalizada", "armed_home": "Armada en casa", "armed_night": "Armada noche", - "armed_vacation": "Vacaciones armadas", + "armed_vacation": "Armada de vacaciones", "arming": "Armando", "disarmed": "Desarmada", "disarming": "Desarmando", diff --git a/homeassistant/components/almond/translations/es.json b/homeassistant/components/almond/translations/es.json index 83e78317741..94c5e89be5d 100644 --- a/homeassistant/components/almond/translations/es.json +++ b/homeassistant/components/almond/translations/es.json @@ -8,7 +8,7 @@ }, "step": { "hassio_confirm": { - "description": "\u00bfDesea configurar Home Assistant para conectarse a Almond proporcionado por el complemento Supervisor: {addon} ?", + "description": "\u00bfQuieres configurar Home Assistant para conectarse a Almond proporcionado por el complemento: {addon} ?", "title": "Almond a trav\u00e9s del complemento Supervisor" }, "pick_implementation": { diff --git a/homeassistant/components/ambee/translations/es.json b/homeassistant/components/ambee/translations/es.json index 6484ac8c3a0..1bf2f5391ad 100644 --- a/homeassistant/components/ambee/translations/es.json +++ b/homeassistant/components/ambee/translations/es.json @@ -11,7 +11,7 @@ "reauth_confirm": { "data": { "api_key": "Clave API", - "description": "Vuelva a autenticarse con su cuenta de Ambee." + "description": "Vuelve a autenticarse con tu cuenta de Ambee." } }, "user": { @@ -21,7 +21,7 @@ "longitude": "Longitud", "name": "Nombre" }, - "description": "Configure Ambee para que se integre con Home Assistant." + "description": "Configura Ambee para que se integre con Home Assistant." } } }, diff --git a/homeassistant/components/amberelectric/translations/es.json b/homeassistant/components/amberelectric/translations/es.json index 18af819d5d1..e8c40903e27 100644 --- a/homeassistant/components/amberelectric/translations/es.json +++ b/homeassistant/components/amberelectric/translations/es.json @@ -6,7 +6,7 @@ "site_name": "Nombre del sitio", "site_nmi": "Sitio NMI" }, - "description": "Seleccione el NMI del sitio que le gustar\u00eda agregar" + "description": "Selecciona el NMI del sitio que te gustar\u00eda a\u00f1adir" }, "user": { "data": { diff --git a/homeassistant/components/android_ip_webcam/translations/id.json b/homeassistant/components/android_ip_webcam/translations/id.json new file mode 100644 index 00000000000..430ebe3645f --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/id.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "port": "Port", + "username": "Nama Pengguna" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Android IP Webcam lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Android IP Webcam dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Android IP Webcam dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/pl.json b/homeassistant/components/android_ip_webcam/translations/pl.json new file mode 100644 index 00000000000..8698af51c4b --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Android IP Webcam przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Android IP Webcam zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/translations/es.json b/homeassistant/components/androidtv/translations/es.json index 74c7484257c..b41827566ab 100644 --- a/homeassistant/components/androidtv/translations/es.json +++ b/homeassistant/components/androidtv/translations/es.json @@ -8,15 +8,15 @@ "adbkey_not_file": "No se ha encontrado el archivo de claves ADB", "cannot_connect": "No se pudo conectar", "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", - "key_and_server": "S\u00f3lo proporciona la clave ADB o el servidor ADB", + "key_and_server": "Solo proporcione la clave ADB o el servidor ADB", "unknown": "Error inesperado" }, "step": { "user": { "data": { - "adb_server_ip": "Direcci\u00f3n IP del servidor ADB (dejar vac\u00edo para no utilizarlo)", + "adb_server_ip": "Direcci\u00f3n IP del servidor ADB (d\u00e9jalo vac\u00edo para no utilizarlo)", "adb_server_port": "Puerto del servidor ADB", - "adbkey": "Ruta de acceso a su archivo de clave ADB (d\u00e9jelo vac\u00edo para que se genere autom\u00e1ticamente)", + "adbkey": "Ruta a tu archivo de clave ADB (d\u00e9jalo en blanco para generarlo autom\u00e1ticamente)", "device_class": "Tipo de dispositivo", "host": "Host", "port": "Puerto" @@ -31,31 +31,31 @@ "step": { "apps": { "data": { - "app_delete": "Marque para eliminar esta aplicaci\u00f3n", - "app_id": "ID de la aplicaci\u00f3n", + "app_delete": "Marcar para eliminar esta aplicaci\u00f3n", + "app_id": "ID de aplicaci\u00f3n", "app_name": "Nombre de la aplicaci\u00f3n" }, - "description": "Configurar el ID de la aplicaci\u00f3n {app_id}", + "description": "Configurar el ID de aplicaci\u00f3n {app_id}", "title": "Configurar aplicaciones de Android TV" }, "init": { "data": { "apps": "Configurar la lista de aplicaciones", "exclude_unnamed_apps": "Excluir aplicaciones con nombre desconocido de la lista de fuentes", - "get_sources": "Recupere las aplicaciones en ejecuci\u00f3n como lista de fuentes", + "get_sources": "Recuperar las aplicaciones en ejecuci\u00f3n como la lista de fuentes", "screencap": "Usar captura de pantalla para la car\u00e1tula del \u00e1lbum", "state_detection_rules": "Configurar reglas de detecci\u00f3n de estado", - "turn_off_command": "Comando de apagado del shell de ADB (dejar vac\u00edo por defecto)", - "turn_on_command": "Comando de activaci\u00f3n del shell ADB (dejar vac\u00edo por defecto)" + "turn_off_command": "Comando de apagado de shell ADB (d\u00e9jalo vac\u00edo para usar el comando por defecto)", + "turn_on_command": "Comando de encendido de shell ADB (d\u00e9jalo vac\u00edo para usar el comando por defecto)" } }, "rules": { "data": { - "rule_delete": "Marque para eliminar esta regla", - "rule_id": "ID de la aplicaci\u00f3n", - "rule_values": "Lista de reglas de detecci\u00f3n de estados (ver documentaci\u00f3n)" + "rule_delete": "Marcar para eliminar esta regla", + "rule_id": "ID de aplicaci\u00f3n", + "rule_values": "Lista de reglas de detecci\u00f3n de estado (ver documentaci\u00f3n)" }, - "description": "Configurar la regla de detecci\u00f3n del ID {rule_id} de la aplicaci\u00f3n", + "description": "Configura la regla de detecci\u00f3n para la identificaci\u00f3n de la aplicaci\u00f3n {rule_id}", "title": "Configurar las reglas de detecci\u00f3n de estado de Android TV" } } diff --git a/homeassistant/components/apple_tv/translations/es.json b/homeassistant/components/apple_tv/translations/es.json index 6b136463aa0..b73aa659ef6 100644 --- a/homeassistant/components/apple_tv/translations/es.json +++ b/homeassistant/components/apple_tv/translations/es.json @@ -5,12 +5,12 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "backoff": "El dispositivo no acepta solicitudes de emparejamiento en este momento (es posible que hayas introducido un c\u00f3digo PIN no v\u00e1lido demasiadas veces), int\u00e9ntalo de nuevo m\u00e1s tarde.", "device_did_not_pair": "No se ha intentado finalizar el proceso de emparejamiento desde el dispositivo.", - "device_not_found": "No se ha encontrado el dispositivo durante la detecci\u00f3n, por favor, intente a\u00f1adirlo de nuevo.", - "inconsistent_device": "No se encontraron los protocolos esperados durante el descubrimiento. Esto normalmente indica un problema con el DNS de multidifusi\u00f3n (Zeroconf). Por favor, intente a\u00f1adir el dispositivo de nuevo.", - "ipv6_not_supported": "IPv6 no est\u00e1 soportado.", + "device_not_found": "No se encontr\u00f3 el dispositivo durante el descubrimiento, por favor intenta a\u00f1adirlo nuevamente.", + "inconsistent_device": "No se encontraron los protocolos esperados durante el descubrimiento. Esto normalmente indica un problema con multicast DNS (Zeroconf). Por favor, intenta a\u00f1adir el dispositivo nuevamente.", + "ipv6_not_supported": "IPv6 no es compatible.", "no_devices_found": "No se encontraron dispositivos en la red", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", - "setup_failed": "No se ha podido configurar el dispositivo.", + "setup_failed": "No se pudo configurar el dispositivo.", "unknown": "Error inesperado" }, "error": { @@ -37,11 +37,11 @@ "title": "Emparejamiento" }, "password": { - "description": "Una contrase\u00f1a es requerida por '{protocolo}'. Esto a\u00fan no es compatible, deshabilite la contrase\u00f1a para continuar.", - "title": "Se requiere una contrase\u00f1a" + "description": "Se requiere una contrase\u00f1a por `{protocol}`. Esto a\u00fan no es compatible, por favor deshabilita la contrase\u00f1a para continuar.", + "title": "Se requiere contrase\u00f1a" }, "protocol_disabled": { - "description": "El emparejamiento es necesario para `{protocol}` pero est\u00e1 desactivado en el dispositivo. Revise las posibles restricciones de acceso (por ejemplo, permitir que se conecten todos los dispositivos de la red local) en el dispositivo.\n\nPuede continuar sin emparejar este protocolo, pero algunas funciones estar\u00e1n limitadas.", + "description": "Se requiere emparejamiento para `{protocol}` pero est\u00e1 deshabilitado en el dispositivo. Revisa las posibles restricciones de acceso (p. ej., permitir que todos los dispositivos de la red local se conecten) en el dispositivo. \n\nPuedes continuar sin emparejar este protocolo, pero algunas funciones estar\u00e1n limitadas.", "title": "No es posible el emparejamiento" }, "reconfigure": { diff --git a/homeassistant/components/asuswrt/translations/es.json b/homeassistant/components/asuswrt/translations/es.json index c714c26129a..f46bf499455 100644 --- a/homeassistant/components/asuswrt/translations/es.json +++ b/homeassistant/components/asuswrt/translations/es.json @@ -35,9 +35,9 @@ "data": { "consider_home": "Segundos de espera antes de considerar un dispositivo ausente", "dnsmasq": "La ubicaci\u00f3n en el router de los archivos dnsmasq.leases", - "interface": "La interfaz de la que desea obtener estad\u00edsticas (por ejemplo, eth0, eth1, etc.)", + "interface": "La interfaz de la que quieres estad\u00edsticas (por ejemplo, eth0, eth1, etc.)", "require_ip": "Los dispositivos deben tener IP (para el modo de punto de acceso)", - "track_unknown": "Seguimiento de dispositivos desconocidos / sin nombre" + "track_unknown": "Seguimiento de dispositivos desconocidos/sin nombre" }, "title": "Opciones de AsusWRT" } diff --git a/homeassistant/components/august/translations/es.json b/homeassistant/components/august/translations/es.json index f2c7a10d0e0..b9334d7b473 100644 --- a/homeassistant/components/august/translations/es.json +++ b/homeassistant/components/august/translations/es.json @@ -14,8 +14,8 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Introduzca la contrase\u00f1a de {username}.", - "title": "Reautorizar una cuenta de August" + "description": "Introduce la contrase\u00f1a de {username}.", + "title": "Volver a autenticar una cuenta August" }, "user_validate": { "data": { @@ -23,8 +23,8 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Si el m\u00e9todo de inicio de sesi\u00f3n es \"correo electr\u00f3nico\", el nombre de usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el m\u00e9todo de inicio de sesi\u00f3n es \"tel\u00e9fono\", el nombre de usuario es el n\u00famero de tel\u00e9fono en el formato \"+NNNNNNN\".", - "title": "Configurar una cuenta de August" + "description": "Si el m\u00e9todo de inicio de sesi\u00f3n es 'correo electr\u00f3nico', el nombre de usuario es la direcci\u00f3n de correo electr\u00f3nico. Si el m\u00e9todo de inicio de sesi\u00f3n es 'tel\u00e9fono', el nombre de usuario es el n\u00famero de tel\u00e9fono en el formato '+NNNNNNNNN'.", + "title": "Configurar una cuenta August" }, "validation": { "data": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/es.json b/homeassistant/components/aurora_abb_powerone/translations/es.json index 4e1d95a126d..537ee81a30e 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/es.json +++ b/homeassistant/components/aurora_abb_powerone/translations/es.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "no_serial_ports": "No se han encontrado puertos. Necesita un dispositivo RS485 v\u00e1lido para comunicarse." + "no_serial_ports": "No se encontraron puertos de comunicaciones. Necesitas un dispositivo RS485 v\u00e1lido para comunicarse." }, "error": { - "cannot_connect": "No se puede conectar, por favor, compruebe el puerto serie, la direcci\u00f3n, la conexi\u00f3n el\u00e9ctrica y que el inversor est\u00e1 encendido", - "cannot_open_serial_port": "No se puede abrir el puerto serie, por favor, compruebe y vuelva a intentarlo", - "invalid_serial_port": "El puerto serie no es v\u00e1lido para este dispositivo o no se ha podido abrir" + "cannot_connect": "No se puede conectar, por favor, verifica el puerto serie, la direcci\u00f3n, la conexi\u00f3n el\u00e9ctrica y que el inversor est\u00e9 encendido (durante la luz del d\u00eda)", + "cannot_open_serial_port": "No se puede abrir el puerto serie, por favor, verif\u00edcalo e int\u00e9ntalo de nuevo", + "invalid_serial_port": "El puerto serie no es un dispositivo v\u00e1lido o no se pudo abrir" }, "step": { "user": { @@ -15,7 +15,7 @@ "address": "Direcci\u00f3n del inversor", "port": "Puerto adaptador RS485 o USB-RS485" }, - "description": "El inversor debe conectarse a trav\u00e9s de un adaptador RS485, por favor, seleccione el puerto serie y la direcci\u00f3n del inversor tal y como se configura en el panel LCD" + "description": "El inversor debe estar conectado a trav\u00e9s de un adaptador RS485, por favor, selecciona el puerto serie y la direcci\u00f3n del inversor seg\u00fan lo configurado en el panel LCD" } } } diff --git a/homeassistant/components/aussie_broadband/translations/es.json b/homeassistant/components/aussie_broadband/translations/es.json index e1ae211ca8b..ce0f6022094 100644 --- a/homeassistant/components/aussie_broadband/translations/es.json +++ b/homeassistant/components/aussie_broadband/translations/es.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "no_services_found": "No se han encontrado servicios para esta cuenta", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + "no_services_found": "No se encontraron servicios para esta cuenta", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -15,14 +15,14 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Actualice la contrase\u00f1a para {username}", - "title": "Reautenticar Integraci\u00f3n" + "description": "Actualizar contrase\u00f1a para {username}", + "title": "Volver a autenticar la integraci\u00f3n" }, "service": { "data": { "services": "Servicios" }, - "title": "Seleccionar Servicios" + "title": "Seleccionar servicios" }, "user": { "data": { @@ -34,7 +34,7 @@ }, "options": { "abort": { - "cannot_connect": "Fallo en la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -43,7 +43,7 @@ "data": { "services": "Servicios" }, - "title": "Selecciona servicios" + "title": "Seleccionar servicios" } } } diff --git a/homeassistant/components/auth/translations/es.json b/homeassistant/components/auth/translations/es.json index 7495ffcfbc9..0279bca1dfa 100644 --- a/homeassistant/components/auth/translations/es.json +++ b/homeassistant/components/auth/translations/es.json @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "Para activar la autenticaci\u00f3n de dos factores utilizando contrase\u00f1as de un solo uso basadas en el tiempo, escanea el c\u00f3digo QR con tu aplicaci\u00f3n de autenticaci\u00f3n. Si no tienes una, te recomendamos el [Autenticador de Google](https://support.google.com/accounts/answer/1066447) o [Authy](https://authy.com/). \n\n {qr_code} \n \nDespu\u00e9s de escanear el c\u00f3digo, introduce el c\u00f3digo de seis d\u00edgitos de tu aplicaci\u00f3n para verificar la configuraci\u00f3n. Si tienes problemas para escanear el c\u00f3digo QR, realiza una configuraci\u00f3n manual con el c\u00f3digo **`{code}`**.", + "description": "Para activar la autenticaci\u00f3n de dos factores usando contrase\u00f1as de un solo uso basadas en el tiempo, escanea el c\u00f3digo QR con tu aplicaci\u00f3n de autenticaci\u00f3n. Si no tienes una, te recomendamos [Google Authenticator](https://support.google.com/accounts/answer/1066447) o [Authy](https://authy.com/). \n\n {qr_code}\n\nDespu\u00e9s de escanear el c\u00f3digo, introduce el c\u00f3digo de seis d\u00edgitos de tu aplicaci\u00f3n para verificar la configuraci\u00f3n. Si tienes problemas para escanear el c\u00f3digo QR, realiza una configuraci\u00f3n manual con el c\u00f3digo **`{code}`**.", "title": "Configurar la autenticaci\u00f3n de dos factores usando TOTP" } }, diff --git a/homeassistant/components/awair/translations/ca.json b/homeassistant/components/awair/translations/ca.json index 3510d3a3a8b..e52451bd108 100644 --- a/homeassistant/components/awair/translations/ca.json +++ b/homeassistant/components/awair/translations/ca.json @@ -2,14 +2,30 @@ "config": { "abort": { "already_configured": "El compte ja est\u00e0 configurat", + "already_configured_account": "El compte ja est\u00e0 configurat", + "already_configured_device": "El dispositiu ja est\u00e0 configurat", "no_devices_found": "No s'han trobat dispositius a la xarxa", - "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "unreachable": "Ha fallat la connexi\u00f3" }, "error": { "invalid_access_token": "Token d'acc\u00e9s inv\u00e0lid", - "unknown": "Error inesperat" + "unknown": "Error inesperat", + "unreachable": "Ha fallat la connexi\u00f3" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Token d'acc\u00e9s", + "email": "Correu electr\u00f2nic" + } + }, + "local": { + "data": { + "host": "Adre\u00e7a IP" + } + }, "reauth": { "data": { "access_token": "Token d'acc\u00e9s", @@ -29,7 +45,10 @@ "access_token": "Token d'acc\u00e9s", "email": "Correu electr\u00f2nic" }, - "description": "T'has de registrar a Awair per a obtenir un token d'acc\u00e9s de desenvolupador a trav\u00e9s de l'enlla\u00e7 seg\u00fcent: https://developer.getawair.com/onboard/login" + "description": "T'has de registrar a Awair per a obtenir un token d'acc\u00e9s de desenvolupador a trav\u00e9s de l'enlla\u00e7 seg\u00fcent: https://developer.getawair.com/onboard/login", + "menu_options": { + "local": "Connecta't localment (preferit)" + } } } } diff --git a/homeassistant/components/awair/translations/de.json b/homeassistant/components/awair/translations/de.json index c28ee6bc016..43b387e8286 100644 --- a/homeassistant/components/awair/translations/de.json +++ b/homeassistant/components/awair/translations/de.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "Konto wurde bereits konfiguriert", + "already_configured_account": "Konto wurde bereits konfiguriert", + "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", - "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "unreachable": "Verbindung fehlgeschlagen" }, "error": { "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", - "unknown": "Unerwarteter Fehler" + "unknown": "Unerwarteter Fehler", + "unreachable": "Verbindung fehlgeschlagen" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Zugangstoken", + "email": "E-Mail" + }, + "description": "Du musst dich f\u00fcr ein Awair-Entwicklerzugriffstoken registrieren unter: {url}" + }, + "discovery_confirm": { + "description": "M\u00f6chtest du {model} ({device_id}) einrichten?" + }, + "local": { + "data": { + "host": "IP-Adresse" + }, + "description": "Awair Local API muss wie folgt aktiviert werden: {url}" + }, "reauth": { "data": { "access_token": "Zugangstoken", @@ -29,7 +50,11 @@ "access_token": "Zugangstoken", "email": "E-Mail" }, - "description": "Du musst dich f\u00fcr ein Awair Entwickler-Zugangs-Token registrieren unter: https://developer.getawair.com/onboard/login" + "description": "W\u00e4hle lokal f\u00fcr die beste Erfahrung. Verwende die Cloud nur, wenn das Ger\u00e4t nicht mit demselben Netzwerk wie Home Assistant verbunden ist oder wenn du ein \u00e4lteres Ger\u00e4t hast.", + "menu_options": { + "cloud": "Verbindung \u00fcber die Cloud", + "local": "Lokal verbinden (bevorzugt)" + } } } } diff --git a/homeassistant/components/awair/translations/en.json b/homeassistant/components/awair/translations/en.json index caec592c527..ca572958c46 100644 --- a/homeassistant/components/awair/translations/en.json +++ b/homeassistant/components/awair/translations/en.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "Account is already configured", + "already_configured_account": "Account is already configured", + "already_configured_device": "Device is already configured", "no_devices_found": "No devices found on the network", - "reauth_successful": "Re-authentication was successful" + "reauth_successful": "Re-authentication was successful", + "unreachable": "Failed to connect" }, "error": { "invalid_access_token": "Invalid access token", - "unknown": "Unexpected error" + "unknown": "Unexpected error", + "unreachable": "Failed to connect" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Access Token", + "email": "Email" + }, + "description": "You must register for an Awair developer access token at: {url}" + }, + "discovery_confirm": { + "description": "Do you want to setup {model} ({device_id})?" + }, + "local": { + "data": { + "host": "IP Address" + }, + "description": "Awair Local API must be enabled following these steps: {url}" + }, "reauth": { "data": { "access_token": "Access Token", @@ -29,7 +50,11 @@ "access_token": "Access Token", "email": "Email" }, - "description": "You must register for an Awair developer access token at: https://developer.getawair.com/onboard/login" + "description": "Pick local for the best experience. Only use cloud if the device is not connected to the same network as Home Assistant, or if you have a legacy device.", + "menu_options": { + "cloud": "Connect via the cloud", + "local": "Connect locally (preferred)" + } } } } diff --git a/homeassistant/components/awair/translations/es.json b/homeassistant/components/awair/translations/es.json index 5b203948a7c..64d678003ac 100644 --- a/homeassistant/components/awair/translations/es.json +++ b/homeassistant/components/awair/translations/es.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", + "already_configured_account": "La cuenta ya est\u00e1 configurada", + "already_configured_device": "El dispositivo ya est\u00e1 configurado", "no_devices_found": "No se encontraron dispositivos en la red", - "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "unreachable": "No se pudo conectar" }, "error": { "invalid_access_token": "Token de acceso no v\u00e1lido", - "unknown": "Error inesperado" + "unknown": "Error inesperado", + "unreachable": "No se pudo conectar" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Token de acceso", + "email": "Correo electr\u00f3nico" + }, + "description": "Debes registrarte para obtener un token de acceso de desarrollador de Awair en: {url}" + }, + "discovery_confirm": { + "description": "\u00bfQuieres configurar {model} ({device_id})?" + }, + "local": { + "data": { + "host": "Direcci\u00f3n IP" + }, + "description": "La API local de Awair debe estar habilitada siguiendo estos pasos: {url}" + }, "reauth": { "data": { "access_token": "Token de acceso", @@ -29,7 +50,11 @@ "access_token": "Token de acceso", "email": "Correo electr\u00f3nico" }, - "description": "Debes registrarte para obtener un token de acceso de desarrollador Awair en: https://developer.getawair.com/onboard/login" + "description": "Debes registrarte para obtener un token de acceso de desarrollador Awair en: https://developer.getawair.com/onboard/login", + "menu_options": { + "cloud": "Conectar a trav\u00e9s de la nube", + "local": "Conectar localmente (preferido)" + } } } } diff --git a/homeassistant/components/awair/translations/et.json b/homeassistant/components/awair/translations/et.json index 70632b292e4..b741f5f8722 100644 --- a/homeassistant/components/awair/translations/et.json +++ b/homeassistant/components/awair/translations/et.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "Konto on juba seadistatud", + "already_configured_account": "Konto on juba seadistatud", + "already_configured_device": "Seade on juba h\u00e4\u00e4lestatud", "no_devices_found": "V\u00f5rgust ei leitud Awair seadmeid", - "reauth_successful": "Taastuvastamine \u00f5nnestus" + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "unreachable": "\u00dchendamine nurjus" }, "error": { "invalid_access_token": "Vigane juurdep\u00e4\u00e4sut\u00f5end", - "unknown": "Tundmatu viga" + "unknown": "Tundmatu viga", + "unreachable": "\u00dchendamine nurjus" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Juurdep\u00e4\u00e4su t\u00f5end", + "email": "E-posti aadress" + }, + "description": "Pead registreeruma Awairi arendaja juurdep\u00e4\u00e4suloa saamiseks aadressil: {url}" + }, + "discovery_confirm": { + "description": "Kas seadistada {model} ({device_id})?" + }, + "local": { + "data": { + "host": "IP aadress" + }, + "description": "Awairi kohalik API tuleb lubada j\u00e4rgmiste sammude abil: {url}" + }, "reauth": { "data": { "access_token": "Juurdep\u00e4\u00e4sut\u00f5end", @@ -29,7 +50,11 @@ "access_token": "Juurdep\u00e4\u00e4sut\u00f5end", "email": "E-post" }, - "description": "Pead registreerima Awair arendaja juurdep\u00e4\u00e4su loa aadressil: https://developer.getawair.com/onboard/login" + "description": "Pead registreerima Awair arendaja juurdep\u00e4\u00e4su loa aadressil: https://developer.getawair.com/onboard/login", + "menu_options": { + "cloud": "Pilve\u00fchendus", + "local": "Kohalik \u00fchendus (eelistatud)" + } } } } diff --git a/homeassistant/components/awair/translations/id.json b/homeassistant/components/awair/translations/id.json index 863b7982b2a..53c41584413 100644 --- a/homeassistant/components/awair/translations/id.json +++ b/homeassistant/components/awair/translations/id.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "Akun sudah dikonfigurasi", + "already_configured_account": "Akun sudah dikonfigurasi", + "already_configured_device": "Perangkat sudah dikonfigurasi", "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", - "reauth_successful": "Autentikasi ulang berhasil" + "reauth_successful": "Autentikasi ulang berhasil", + "unreachable": "Gagal terhubung" }, "error": { "invalid_access_token": "Token akses tidak valid", - "unknown": "Kesalahan yang tidak diharapkan" + "unknown": "Kesalahan yang tidak diharapkan", + "unreachable": "Gagal terhubung" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Token Akses", + "email": "Email" + }, + "description": "Anda harus mendaftar untuk mendapatkan token akses pengembang Awair di: {url}" + }, + "discovery_confirm": { + "description": "Ingin menyiapkan {model} ({device_id})?" + }, + "local": { + "data": { + "host": "Alamat IP" + }, + "description": "API Awair Local harus diaktifkan dengan mengikuti langkah-langkah berikut: {url}" + }, "reauth": { "data": { "access_token": "Token Akses", @@ -29,7 +50,11 @@ "access_token": "Token Akses", "email": "Email" }, - "description": "Anda harus mendaftar untuk mendapatkan token akses pengembang Awair di: https://developer.getawair.com/onboard/login" + "description": "Anda harus mendaftar untuk mendapatkan token akses pengembang Awair di: https://developer.getawair.com/onboard/login", + "menu_options": { + "cloud": "Terhubung melalui cloud", + "local": "Terhubung secara lokal (lebih disukai)" + } } } } diff --git a/homeassistant/components/awair/translations/pl.json b/homeassistant/components/awair/translations/pl.json index b0ee9e85adf..840612eff40 100644 --- a/homeassistant/components/awair/translations/pl.json +++ b/homeassistant/components/awair/translations/pl.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "Konto jest ju\u017c skonfigurowane", + "already_configured_account": "Konto jest ju\u017c skonfigurowane", + "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "unreachable": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "error": { "invalid_access_token": "Niepoprawny token dost\u0119pu", - "unknown": "Nieoczekiwany b\u0142\u0105d" + "unknown": "Nieoczekiwany b\u0142\u0105d", + "unreachable": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Token dost\u0119pu", + "email": "Adres e-mail" + }, + "description": "Aby uzyska\u0107 token dost\u0119pu programisty Awair, musisz zarejestrowa\u0107 si\u0119 pod adresem: {url}" + }, + "discovery_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {model} ({device_id})?" + }, + "local": { + "data": { + "host": "Adres IP" + }, + "description": "Lokalny interfejs API Awair musi by\u0107 w\u0142\u0105czony, wykonuj\u0105c nast\u0119puj\u0105ce czynno\u015bci: {url}" + }, "reauth": { "data": { "access_token": "Token dost\u0119pu", @@ -29,7 +50,11 @@ "access_token": "Token dost\u0119pu", "email": "Adres e-mail" }, - "description": "Aby uzyska\u0107 token dost\u0119pu programisty Awair, nale\u017cy zarejestrowa\u0107 si\u0119 pod adresem: https://developer.getawair.com/onboard/login" + "description": "Wybierz lokalny, aby uzyska\u0107 najlepsze efekty. Korzystaj z chmury tylko wtedy, gdy urz\u0105dzenie nie jest pod\u0142\u0105czone do tej samej sieci co Home Assistant lub je\u015bli masz starsze urz\u0105dzenie.", + "menu_options": { + "cloud": "Po\u0142\u0105cz si\u0119 przez chmur\u0119", + "local": "Po\u0142\u0105cz lokalnie (preferowane)" + } } } } diff --git a/homeassistant/components/azure_event_hub/translations/es.json b/homeassistant/components/azure_event_hub/translations/es.json index 7d9792ef371..25d5cfde92e 100644 --- a/homeassistant/components/azure_event_hub/translations/es.json +++ b/homeassistant/components/azure_event_hub/translations/es.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "cannot_connect": "La conexi\u00f3n con las credenciales del configuration.yaml ha fallado, por favor elim\u00ednelo de yaml y utilice el flujo de configuraci\u00f3n", - "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n.", - "unknown": "La conexi\u00f3n con las credenciales de configuration.yaml ha fallado con un error desconocido, por favor, elim\u00ednelo de yaml y utilice el flujo de configuraci\u00f3n." + "cannot_connect": "No se pudo conectar con las credenciales de configuration.yaml, por favor, elim\u00ednalo de yaml y usa el flujo de configuraci\u00f3n.", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", + "unknown": "La conexi\u00f3n con las credenciales de la configuraci\u00f3n.yaml fall\u00f3 con un error desconocido, por favor, elim\u00ednalo de yaml y usa el flujo de configuraci\u00f3n." }, "error": { "cannot_connect": "No se pudo conectar", @@ -15,7 +15,7 @@ "data": { "event_hub_connection_string": "Cadena de conexi\u00f3n de Event Hub" }, - "description": "Por favor, introduzca la cadena de conexi\u00f3n para: {event_hub_instance_name}", + "description": "Por favor, introduce la cadena de conexi\u00f3n para: {event_hub_instance_name}", "title": "M\u00e9todo de cadena de conexi\u00f3n" }, "sas": { @@ -24,7 +24,7 @@ "event_hub_sas_key": "Clave de SAS de Event Hub", "event_hub_sas_policy": "Directiva de SAS de Event Hub" }, - "description": "Por favor, introduzca las credenciales SAS (firma de acceso compartido) para: {event_hub_instance_name}", + "description": "Por favor, introduce las credenciales SAS (firma de acceso compartido) para: {event_hub_instance_name}", "title": "M\u00e9todo de credenciales SAS" }, "user": { @@ -32,7 +32,7 @@ "event_hub_instance_name": "Nombre de instancia de Event Hub", "use_connection_string": "Usar cadena de conexi\u00f3n" }, - "title": "Configure su integraci\u00f3n de Azure Event Hub" + "title": "Configura tu integraci\u00f3n Azure Event Hub" } } }, @@ -40,7 +40,7 @@ "step": { "options": { "data": { - "send_interval": "Intervalo entre el env\u00edo de lotes al hub." + "send_interval": "Intervalo entre el env\u00edo de lotes al concentrador." }, "title": "Opciones para Azure Event Hub." } diff --git a/homeassistant/components/balboa/translations/es.json b/homeassistant/components/balboa/translations/es.json index c71f279fd6c..33cee6c5196 100644 --- a/homeassistant/components/balboa/translations/es.json +++ b/homeassistant/components/balboa/translations/es.json @@ -12,7 +12,7 @@ "data": { "host": "Host" }, - "title": "Con\u00e9ctese al dispositivo Wi-Fi de Balboa" + "title": "Con\u00e9ctate al dispositivo Wi-Fi de Balboa" } } }, @@ -20,7 +20,7 @@ "step": { "init": { "data": { - "sync_time": "Mantenga sincronizada la hora de su cliente de Balboa Spa con Home Assistant" + "sync_time": "Mant\u00e9n sincronizada la hora de tu Cliente Balboa Spa con Home Assistant" } } } diff --git a/homeassistant/components/binary_sensor/translations/es.json b/homeassistant/components/binary_sensor/translations/es.json index 005f2669e50..4a30bed923d 100644 --- a/homeassistant/components/binary_sensor/translations/es.json +++ b/homeassistant/components/binary_sensor/translations/es.json @@ -86,7 +86,7 @@ "not_powered": "{entity_name} no est\u00e1 activado", "not_present": "{entity_name} no est\u00e1 presente", "not_running": "{entity_name} ya no se est\u00e1 ejecutando", - "not_tampered": "{entity_name} dej\u00f3 de detectar alteraciones", + "not_tampered": "{entity_name} dej\u00f3 de detectar manipulaci\u00f3n", "not_unsafe": "{entity_name} se volvi\u00f3 seguro", "occupied": "{entity_name} se convirti\u00f3 en ocupado", "opened": "{entity_name} abierto", @@ -97,7 +97,7 @@ "running": "{entity_name} comenz\u00f3 a ejecutarse", "smoke": "{entity_name} empez\u00f3 a detectar humo", "sound": "{entity_name} empez\u00f3 a detectar sonido", - "tampered": "{entity_name} comenz\u00f3 a detectar alteraciones", + "tampered": "{entity_name} comenz\u00f3 a detectar manipulaci\u00f3n", "turned_off": "{entity_name} desactivado", "turned_on": "{entity_name} activado", "unsafe": "{entity_name} se volvi\u00f3 inseguro", @@ -112,9 +112,9 @@ "heat": "calor", "moisture": "humedad", "motion": "movimiento", - "occupancy": "ocupaci\u00f3n", + "occupancy": "presencia", "power": "energ\u00eda", - "problem": "Problema", + "problem": "problema", "smoke": "humo", "sound": "sonido", "vibration": "vibraci\u00f3n" @@ -133,7 +133,7 @@ "on": "Cargando" }, "carbon_monoxide": { - "off": "Libre", + "off": "No detectado", "on": "Detectado" }, "cold": { @@ -202,7 +202,7 @@ }, "running": { "off": "No se est\u00e1 ejecutando", - "on": "Corriendo" + "on": "Ejecutando" }, "safety": { "off": "Seguro", diff --git a/homeassistant/components/bosch_shc/translations/es.json b/homeassistant/components/bosch_shc/translations/es.json index 0e3c536995d..7a019e277a4 100644 --- a/homeassistant/components/bosch_shc/translations/es.json +++ b/homeassistant/components/bosch_shc/translations/es.json @@ -7,22 +7,22 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "pairing_failed": "El emparejamiento ha fallado; compruebe que el Bosch Smart Home Controller est\u00e1 en modo de emparejamiento (el LED parpadea) y que su contrase\u00f1a es correcta.", - "session_error": "Error de sesi\u00f3n: La API devuelve un resultado no correcto.", + "pairing_failed": "Emparejamiento fallido; Verifica que el Smart Home Controller de Bosch est\u00e9 en modo de emparejamiento (el LED parpadea) y que tu contrase\u00f1a sea correcta.", + "session_error": "Error de sesi\u00f3n: la API devuelve un resultado No-OK.", "unknown": "Error inesperado" }, "flow_title": "Bosch SHC: {name}", "step": { "confirm_discovery": { - "description": "Pulse el bot\u00f3n frontal del Smart Home Controller de Bosch hasta que el LED empiece a parpadear.\n\u00bfPreparado para seguir configurando {model} @ {host} con Home Assistant?" + "description": "Pulsa el bot\u00f3n frontal del Smart Home Controller de Bosch hasta que el LED empiece a parpadear.\n\u00bfPreparado para seguir configurando {model} @ {host} con Home Assistant?" }, "credentials": { "data": { - "password": "Contrase\u00f1a del controlador smart home" + "password": "Contrase\u00f1a del Smart Home Controller" } }, "reauth_confirm": { - "description": "La integraci\u00f3n bosch_shc necesita volver a autentificar su cuenta", + "description": "La integraci\u00f3n bosch_shc necesita volver a autentificar tu cuenta", "title": "Volver a autenticar la integraci\u00f3n" }, "user": { diff --git a/homeassistant/components/brother/translations/es.json b/homeassistant/components/brother/translations/es.json index 9c327a2887a..921f2806f0e 100644 --- a/homeassistant/components/brother/translations/es.json +++ b/homeassistant/components/brother/translations/es.json @@ -21,7 +21,7 @@ "data": { "type": "Tipo de impresora" }, - "description": "\u00bfQuieres a\u00f1adir la Impresora Brother {model} con el n\u00famero de serie `{serial_number}` a Home Assistant?", + "description": "\u00bfQuieres a\u00f1adir la impresora {model} con n\u00famero de serie `{serial_number}` a Home Assistant?", "title": "Impresora Brother encontrada" } } diff --git a/homeassistant/components/brunt/translations/es.json b/homeassistant/components/brunt/translations/es.json index 362ef22b838..7a912e267f9 100644 --- a/homeassistant/components/brunt/translations/es.json +++ b/homeassistant/components/brunt/translations/es.json @@ -14,7 +14,7 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Por favor, vuelva a introducir la contrase\u00f1a de: {username}", + "description": "Por favor, vuelve a introducir la contrase\u00f1a de: {username}", "title": "Volver a autenticar la integraci\u00f3n" }, "user": { @@ -22,7 +22,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "title": "Configure su integraci\u00f3n con Brunt" + "title": "Configura tu integraci\u00f3n Brunt" } } } diff --git a/homeassistant/components/bsblan/translations/es.json b/homeassistant/components/bsblan/translations/es.json index 4b36080dd7c..5f1e55c8d0d 100644 --- a/homeassistant/components/bsblan/translations/es.json +++ b/homeassistant/components/bsblan/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "cannot_connect": "Fallo en la conexi\u00f3n" + "cannot_connect": "No se pudo conectar" }, "error": { "cannot_connect": "No se pudo conectar" diff --git a/homeassistant/components/cast/translations/es.json b/homeassistant/components/cast/translations/es.json index 39e34975af8..9c0847b52ce 100644 --- a/homeassistant/components/cast/translations/es.json +++ b/homeassistant/components/cast/translations/es.json @@ -9,9 +9,9 @@ "step": { "config": { "data": { - "known_hosts": "Anfitriones conocidos" + "known_hosts": "Hosts conocidos" }, - "description": "Introduce la configuraci\u00f3n de Google Cast.", + "description": "Hosts conocidos: una lista separada por comas de nombres de host o direcciones IP de dispositivos de transmisi\u00f3n, se usa si el descubrimiento de mDNS no funciona.", "title": "Configuraci\u00f3n de Google Cast" }, "confirm": { @@ -27,9 +27,9 @@ "advanced_options": { "data": { "ignore_cec": "Ignorar CEC", - "uuid": "UUID permitidos" + "uuid": "UUIDs permitidos" }, - "description": "UUID permitidos: lista separada por comas de UUID de dispositivos Cast para a\u00f1adir a Home Assistant. \u00dasalo solo si no deseas a\u00f1adir todos los dispositivos Cast disponibles.\nIgnorar CEC: lista separada por comas de Chromecasts que deben ignorar los datos CEC para determinar la entrada activa. Esto se pasar\u00e1 a pychromecast.IGNORE_CEC.", + "description": "UUID permitidos: una lista separada por comas de UUID de dispositivos Cast para a\u00f1adir a Home Assistant. \u00dasalo solo si no deseas a\u00f1adir todos los dispositivos de transmisi\u00f3n disponibles.\nIgnorar CEC: una lista separada por comas de Chromecasts que deben ignorar los datos de CEC para determinar la entrada activa. Esto se pasar\u00e1 a pychromecast.IGNORE_CEC.", "title": "Configuraci\u00f3n avanzada de Google Cast" }, "basic_options": { diff --git a/homeassistant/components/climacell/translations/es.json b/homeassistant/components/climacell/translations/es.json index 056d26b077d..270d72bd58c 100644 --- a/homeassistant/components/climacell/translations/es.json +++ b/homeassistant/components/climacell/translations/es.json @@ -3,10 +3,10 @@ "step": { "init": { "data": { - "timestep": "Min. Entre pron\u00f3sticos de NowCast" + "timestep": "Min. entre pron\u00f3sticos de NowCast" }, - "description": "Si elige habilitar la entidad de pron\u00f3stico \"nowcast\", puede configurar el n\u00famero de minutos entre cada pron\u00f3stico. El n\u00famero de pron\u00f3sticos proporcionados depende del n\u00famero de minutos elegidos entre los pron\u00f3sticos.", - "title": "Actualizaci\u00f3n de opciones de ClimaCell" + "description": "Si eliges habilitar la entidad de pron\u00f3stico `nowcast`, puedes configurar la cantidad de minutos entre cada pron\u00f3stico. La cantidad de pron\u00f3sticos proporcionados depende de la cantidad de minutos elegidos entre los pron\u00f3sticos.", + "title": "Actualizar opciones de ClimaCell" } } } diff --git a/homeassistant/components/climacell/translations/sensor.es.json b/homeassistant/components/climacell/translations/sensor.es.json index 4ce9ac05c1c..4cb1b34eb21 100644 --- a/homeassistant/components/climacell/translations/sensor.es.json +++ b/homeassistant/components/climacell/translations/sensor.es.json @@ -4,21 +4,21 @@ "good": "Bueno", "hazardous": "Peligroso", "moderate": "Moderado", - "unhealthy": "Insalubre", - "unhealthy_for_sensitive_groups": "Insalubre para grupos sensibles", + "unhealthy": "No saludable", + "unhealthy_for_sensitive_groups": "No es saludable para grupos sensibles", "very_unhealthy": "Muy poco saludable" }, "climacell__pollen_index": { "high": "Alto", "low": "Bajo", "medium": "Medio", - "none": "Ninguna", + "none": "Ninguno", "very_high": "Muy alto", "very_low": "Muy bajo" }, "climacell__precipitation_type": { - "freezing_rain": "Lluvia g\u00e9lida", - "ice_pellets": "Perdigones de hielo", + "freezing_rain": "Lluvia helada", + "ice_pellets": "Granizo", "none": "Ninguna", "rain": "Lluvia", "snow": "Nieve" diff --git a/homeassistant/components/cloudflare/translations/es.json b/homeassistant/components/cloudflare/translations/es.json index 0647609e4e8..eff36eefcfa 100644 --- a/homeassistant/components/cloudflare/translations/es.json +++ b/homeassistant/components/cloudflare/translations/es.json @@ -15,7 +15,7 @@ "reauth_confirm": { "data": { "api_token": "Token API", - "description": "Vuelva a autenticarse con su cuenta de Cloudflare." + "description": "Vuelve a autenticarte con tu cuenta de Cloudflare." } }, "records": { diff --git a/homeassistant/components/co2signal/translations/es.json b/homeassistant/components/co2signal/translations/es.json index 921dd22a76a..7029bd48e52 100644 --- a/homeassistant/components/co2signal/translations/es.json +++ b/homeassistant/components/co2signal/translations/es.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "api_ratelimit": "Se ha superado el l\u00edmite de velocidad de la API", + "api_ratelimit": "Se excedi\u00f3 el l\u00edmite de tasa de API", "unknown": "Error inesperado" }, "error": { - "api_ratelimit": "Excedida tasa l\u00edmite del API", + "api_ratelimit": "Se excedi\u00f3 el l\u00edmite de tasa de API", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -27,7 +27,7 @@ "api_key": "Token de acceso", "location": "Obtener datos para" }, - "description": "Visite https://co2signal.com/ para solicitar un token." + "description": "Visita https://co2signal.com/ para solicitar un token." } } } diff --git a/homeassistant/components/coinbase/translations/es.json b/homeassistant/components/coinbase/translations/es.json index 32eb2af4d5c..8d89b9b546b 100644 --- a/homeassistant/components/coinbase/translations/es.json +++ b/homeassistant/components/coinbase/translations/es.json @@ -7,7 +7,7 @@ "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_auth_key": "Credenciales de API rechazadas por Coinbase debido a una clave de API no v\u00e1lida.", - "invalid_auth_secret": "Credenciales de la API rechazadas por Coinbase debido a un secreto de API no v\u00e1lido.", + "invalid_auth_secret": "Credenciales de API rechazadas por Coinbase debido a un secreto de API no v\u00e1lido.", "unknown": "Error inesperado" }, "step": { @@ -23,8 +23,8 @@ }, "options": { "error": { - "currency_unavailable": "La API de Coinbase no proporciona uno o m\u00e1s de los saldos de divisas solicitados.", - "exchange_rate_unavailable": "El API de Coinbase no proporciona alguno/s de los tipos de cambio que has solicitado.", + "currency_unavailable": "Tu API de Coinbase no proporciona uno o m\u00e1s de los saldos de divisas solicitados.", + "exchange_rate_unavailable": "Coinbase no proporciona uno o m\u00e1s de los tipos de cambio solicitados.", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/cpuspeed/translations/es.json b/homeassistant/components/cpuspeed/translations/es.json index 87ff7b0c783..f345f62a3d4 100644 --- a/homeassistant/components/cpuspeed/translations/es.json +++ b/homeassistant/components/cpuspeed/translations/es.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n.", - "not_compatible": "No se puede obtener informaci\u00f3n de la CPU, esta integraci\u00f3n no es compatible con su sistema" + "already_configured": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", + "not_compatible": "No se puede obtener informaci\u00f3n de la CPU, esta integraci\u00f3n no es compatible con tu sistema" }, "step": { "user": { - "description": "\u00bfQuieres empezar a configurar?", + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?", "title": "Velocidad de la CPU" } } diff --git a/homeassistant/components/crownstone/translations/es.json b/homeassistant/components/crownstone/translations/es.json index b71c69db33c..dcb0e1af6e3 100644 --- a/homeassistant/components/crownstone/translations/es.json +++ b/homeassistant/components/crownstone/translations/es.json @@ -6,7 +6,7 @@ "usb_setup_unsuccessful": "La configuraci\u00f3n del USB de Crownstone no tuvo \u00e9xito." }, "error": { - "account_not_verified": "Cuenta no verificada. Por favor, active su cuenta a trav\u00e9s del correo electr\u00f3nico de activaci\u00f3n de Crownstone.", + "account_not_verified": "Cuenta no verificada. Por favor, activa tu cuenta a trav\u00e9s del correo electr\u00f3nico de activaci\u00f3n de Crownstone.", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -15,22 +15,22 @@ "data": { "usb_path": "Ruta del dispositivo USB" }, - "description": "Selecciona el puerto serie del dongle USB de Crownstone, o selecciona \"No usar USB\" si no quieres configurar un dongle USB.\n\nBusca un dispositivo con VID 10C4 y PID EA60.", - "title": "Configuraci\u00f3n del dispositivo USB Crownstone" + "description": "Selecciona el puerto serie del dongle USB de Crownstone o selecciona 'No usar USB' si no deseas configurar un dongle USB. \n\nBusca un dispositivo con VID 10C4 y PID EA60.", + "title": "Configuraci\u00f3n del dongle USB Crownstone" }, "usb_manual_config": { "data": { "usb_manual_path": "Ruta del dispositivo USB" }, - "description": "Introduzca manualmente la ruta de un dispositivo USB Crownstone.", - "title": "Ruta manual del dispositivo USB Crownstone" + "description": "Introduce manualmente la ruta de un dongle USB de Crownstone.", + "title": "Ruta manual del dongle USB Crownstone" }, "usb_sphere_config": { "data": { - "usb_sphere": "Esfera Crownstone" + "usb_sphere": "Crownstone Sphere" }, - "description": "Selecciona una Esfera Crownstone donde se encuentra el USB.", - "title": "USB de Esfera Crownstone" + "description": "Selecciona una Crownstone Sphere donde se encuentra el USB.", + "title": "USB de Crownstone Sphere" }, "user": { "data": { @@ -45,30 +45,30 @@ "step": { "init": { "data": { - "usb_sphere_option": "Esfera de Crownstone donde se encuentra el USB", - "use_usb_option": "Utilice una llave USB Crownstone para la transmisi\u00f3n de datos local" + "usb_sphere_option": "Crownstone Sphere donde se encuentra el USB", + "use_usb_option": "Utiliza un dongle USB de Crownstone para la transmisi\u00f3n local de datos" } }, "usb_config": { "data": { "usb_path": "Ruta del dispositivo USB" }, - "description": "Seleccione el puerto serie del dispositivo USB Crownstone.\n\nBusque un dispositivo con VID 10C4 y PID EA60.", - "title": "Configuraci\u00f3n del dispositivo USB Crownstone" + "description": "Selecciona el puerto serie del dongle USB Crownstone. \n\nBusca un dispositivo con VID 10C4 y PID EA60.", + "title": "Configuraci\u00f3n del dongle USB Crownstone" }, "usb_manual_config": { "data": { "usb_manual_path": "Ruta del dispositivo USB" }, - "description": "Introduzca manualmente la ruta de un dispositivo USB Crownstone.", - "title": "Ruta manual del dispositivo USB Crownstone" + "description": "Introduce manualmente la ruta de un dongle USB de Crownstone.", + "title": "Ruta manual del dongle USB Crownstone" }, "usb_sphere_config": { "data": { - "usb_sphere": "Esfera Crownstone" + "usb_sphere": "Crownstone Sphere" }, - "description": "Selecciona una Esfera Crownstone donde se encuentra el USB.", - "title": "USB de Esfera Crownstone" + "description": "Selecciona una Crownstone Sphere donde se encuentra el USB.", + "title": "USB de Crownstone Sphere" } } } diff --git a/homeassistant/components/daikin/translations/es.json b/homeassistant/components/daikin/translations/es.json index 6908e0af5f7..ce72440bf7d 100644 --- a/homeassistant/components/daikin/translations/es.json +++ b/homeassistant/components/daikin/translations/es.json @@ -5,7 +5,7 @@ "cannot_connect": "No se pudo conectar" }, "error": { - "api_password": "Autenticaci\u00f3n no v\u00e1lida, utilice la clave de la API o la contrase\u00f1a.", + "api_password": "Autenticaci\u00f3n no v\u00e1lida, utiliza la clave API o la contrase\u00f1a.", "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index 0974be9ff9d..3c4d8a89134 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -14,7 +14,7 @@ "flow_title": "{host}", "step": { "hassio_confirm": { - "description": "\u00bfQuieres configurar Home Assistant para que se conecte al gateway de deCONZ proporcionado por el add-on {addon} de Supervisor?", + "description": "\u00bfQuieres configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento {addon} ?", "title": "Pasarela de enlace de CONZ Zigbee v\u00eda complemento de Home Assistant" }, "link": { diff --git a/homeassistant/components/deluge/translations/es.json b/homeassistant/components/deluge/translations/es.json index c09a1ef5ef5..73029783168 100644 --- a/homeassistant/components/deluge/translations/es.json +++ b/homeassistant/components/deluge/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { @@ -14,9 +14,9 @@ "password": "Contrase\u00f1a", "port": "Puerto", "username": "Nombre de usuario", - "web_port": "Puerto web (para el servicio de visita)" + "web_port": "Puerto web (para visitar el servicio)" }, - "description": "Para poder usar esta integraci\u00f3n, debe habilitar la siguiente opci\u00f3n en la configuraci\u00f3n de diluvio: Daemon > Permitir controles remotos" + "description": "Para poder usar esta integraci\u00f3n, debes habilitar la siguiente opci\u00f3n en la configuraci\u00f3n de Deluge: Daemon > Allow Remote Connections" } } } diff --git a/homeassistant/components/demo/translations/pl.json b/homeassistant/components/demo/translations/pl.json index c57a1e4f619..6fa5f2509c4 100644 --- a/homeassistant/components/demo/translations/pl.json +++ b/homeassistant/components/demo/translations/pl.json @@ -1,10 +1,21 @@ { "issues": { + "bad_psu": { + "fix_flow": { + "step": { + "confirm": { + "description": "Naci\u015bnij ZATWIERD\u0179, aby potwierdzi\u0107, \u017ce zasilacz zosta\u0142 wymieniony", + "title": "Zasilacz wymaga wymiany" + } + } + }, + "title": "Zasilacz nie jest stabilny" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { "confirm": { - "description": "Naci\u015bnij OK po uzupe\u0142nieniu p\u0142ynu \u015bwiate\u0142ka", + "description": "Naci\u015bnij ZATWIERD\u0179 po uzupe\u0142nieniu p\u0142ynu \u015bwiate\u0142ka", "title": "P\u0142yn \u015bwiate\u0142ka nale\u017cy uzupe\u0142ni\u0107" } } diff --git a/homeassistant/components/derivative/translations/es.json b/homeassistant/components/derivative/translations/es.json index d0722b70400..8462540f9dd 100644 --- a/homeassistant/components/derivative/translations/es.json +++ b/homeassistant/components/derivative/translations/es.json @@ -15,8 +15,8 @@ "time_window": "Si se establece, el valor del sensor es un promedio m\u00f3vil ponderado en el tiempo de las derivadas dentro de esta ventana.", "unit_prefix": "La salida se escalar\u00e1 seg\u00fan el prefijo m\u00e9trico seleccionado y la unidad de tiempo de la derivada." }, - "description": "Crea un sensor que ama la derivada de otro sensor.", - "title": "A\u00f1ade sensor derivativo" + "description": "Crea un sensor que estima la derivada de un sensor.", + "title": "A\u00f1adir sensor de derivaci\u00f3n" } } }, @@ -39,5 +39,5 @@ } } }, - "title": "Sensor derivado" + "title": "Sensor de derivaci\u00f3n" } \ No newline at end of file diff --git a/homeassistant/components/deutsche_bahn/translations/pl.json b/homeassistant/components/deutsche_bahn/translations/pl.json new file mode 100644 index 00000000000..85cf97d0d17 --- /dev/null +++ b/homeassistant/components/deutsche_bahn/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integracja Deutsche Bahn oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.11. \n\nIntegracja jest usuwana, poniewa\u017c opiera si\u0119 na webscrapingu, co jest niedozwolone. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja Deutsche Bahn zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/es.json b/homeassistant/components/devolo_home_network/translations/es.json index df2435df665..645f0347554 100644 --- a/homeassistant/components/devolo_home_network/translations/es.json +++ b/homeassistant/components/devolo_home_network/translations/es.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "home_control": "La unidad central de devolo Home Control no funciona con esta integraci\u00f3n." + "home_control": "La unidad central Home Control de devolo no funciona con esta integraci\u00f3n." }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "flow_title": "{product} ({name})", @@ -14,11 +14,11 @@ "data": { "ip_address": "Direcci\u00f3n IP" }, - "description": "\u00bfDeseas iniciar la configuraci\u00f3n?" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" }, "zeroconf_confirm": { - "description": "\u00bfDesea a\u00f1adir el dispositivo de red dom\u00e9stica de devolo con el nombre de host `{host_name}` a Home Assistant?", - "title": "Dispositivo de red dom\u00e9stico devolo descubierto" + "description": "\u00bfQuieres a\u00f1adir el dispositivo de red dom\u00e9stica devolo con el nombre de host `{host_name}` a Home Assistant?", + "title": "Dispositivo de red dom\u00e9stica devolo descubierto" } } } diff --git a/homeassistant/components/dlna_dmr/translations/es.json b/homeassistant/components/dlna_dmr/translations/es.json index 6d16766de03..3459adca00a 100644 --- a/homeassistant/components/dlna_dmr/translations/es.json +++ b/homeassistant/components/dlna_dmr/translations/es.json @@ -2,16 +2,16 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "alternative_integration": "Dispositivo compatible con otra integraci\u00f3n", + "alternative_integration": "El dispositivo es m\u00e1s compatible con otra integraci\u00f3n", "cannot_connect": "No se pudo conectar", "discovery_error": "No se ha podido descubrir un dispositivo DLNA coincidente", - "incomplete_config": "A la configuraci\u00f3n le falta una variable necesaria", + "incomplete_config": "A la configuraci\u00f3n le falta una variable requerida", "non_unique_id": "Se han encontrado varios dispositivos con el mismo ID \u00fanico", - "not_dmr": "El dispositivo no es un procesador de medios digitales compatible" + "not_dmr": "El dispositivo no es un Digital Media Renderer compatible" }, "error": { "cannot_connect": "No se pudo conectar", - "not_dmr": "El dispositivo no es un procesador de medios digitales compatible" + "not_dmr": "El dispositivo no es un Digital Media Renderer compatible" }, "flow_title": "{name}", "step": { @@ -19,13 +19,13 @@ "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" }, "import_turn_on": { - "description": "Por favor, encienda el dispositivo y haga clic en enviar para continuar la migraci\u00f3n" + "description": "Por favor, enciende el dispositivo y haz clic en enviar para continuar la migraci\u00f3n" }, "manual": { "data": { "url": "URL" }, - "description": "URL de un archivo XML de descripci\u00f3n del dispositivo", + "description": "URL a un archivo XML de descripci\u00f3n de dispositivo", "title": "Conexi\u00f3n manual del dispositivo DLNA DMR" }, "user": { @@ -46,7 +46,7 @@ "data": { "browse_unfiltered": "Mostrar medios incompatibles al navegar", "callback_url_override": "URL de devoluci\u00f3n de llamada del detector de eventos", - "listen_port": "Puerto de escucha de eventos (aleatorio si no se establece)", + "listen_port": "Puerto de escucha de eventos (aleatorio si no est\u00e1 configurado)", "poll_availability": "Sondeo para la disponibilidad del dispositivo" }, "title": "Configuraci\u00f3n de DLNA Digital Media Renderer" diff --git a/homeassistant/components/dlna_dms/translations/es.json b/homeassistant/components/dlna_dms/translations/es.json index f9d08f90451..d93988479aa 100644 --- a/homeassistant/components/dlna_dms/translations/es.json +++ b/homeassistant/components/dlna_dms/translations/es.json @@ -2,21 +2,21 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El proceso de configuraci\u00f3n ya est\u00e1 en curso", - "bad_ssdp": "Falta un valor necesario en los datos SSDP", - "no_devices_found": "No se han encontrado dispositivos en la red", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "bad_ssdp": "Falta un valor requerido en los datos SSDP", + "no_devices_found": "No se encontraron dispositivos en la red", "not_dms": "El dispositivo no es un servidor multimedia compatible" }, "flow_title": "{name}", "step": { "confirm": { - "description": "\u00bfQuiere empezar a configurar?" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" }, "user": { "data": { "host": "Host" }, - "description": "Escoge un dispositivo a configurar", + "description": "Elige un dispositivo para configurar", "title": "Dispositivos DLNA DMA descubiertos" } } diff --git a/homeassistant/components/dnsip/translations/es.json b/homeassistant/components/dnsip/translations/es.json index a5a51b76746..de168346526 100644 --- a/homeassistant/components/dnsip/translations/es.json +++ b/homeassistant/components/dnsip/translations/es.json @@ -1,14 +1,14 @@ { "config": { "error": { - "invalid_hostname": "Nombre de host inv\u00e1lido" + "invalid_hostname": "Nombre de host no v\u00e1lido" }, "step": { "user": { "data": { - "hostname": "El nombre de host para el que se realiza la consulta DNS", - "resolver": "Conversor para la b\u00fasqueda de IPV4", - "resolver_ipv6": "Conversor para la b\u00fasqueda de IPV6" + "hostname": "El nombre de host para el que realizar la consulta de DNS", + "resolver": "Resolver para la b\u00fasqueda IPv4", + "resolver_ipv6": "Resolver para la b\u00fasqueda IPv6" } } } @@ -20,8 +20,8 @@ "step": { "init": { "data": { - "resolver": "Resolver para la b\u00fasqueda de IPV4", - "resolver_ipv6": "Resolver para la b\u00fasqueda de IPV6" + "resolver": "Resolver para la b\u00fasqueda IPv4", + "resolver_ipv6": "Resolver para la b\u00fasqueda IPv6" } } } diff --git a/homeassistant/components/dsmr/translations/es.json b/homeassistant/components/dsmr/translations/es.json index 880f378c7c3..91ee7c58952 100644 --- a/homeassistant/components/dsmr/translations/es.json +++ b/homeassistant/components/dsmr/translations/es.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "cannot_communicate": "No se ha podido comunicar", + "cannot_communicate": "No se pudo comunicar", "cannot_connect": "No se pudo conectar" }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "cannot_communicate": "No se ha podido comunicar", + "cannot_communicate": "No se pudo comunicar", "cannot_connect": "No se pudo conectar" }, "step": { @@ -19,12 +19,12 @@ "host": "Host", "port": "Puerto" }, - "title": "Seleccione la direcci\u00f3n de la conexi\u00f3n" + "title": "Selecciona la direcci\u00f3n de conexi\u00f3n" }, "setup_serial": { "data": { - "dsmr_version": "Seleccione la versi\u00f3n de DSMR", - "port": "Seleccione el dispositivo" + "dsmr_version": "Selecciona la versi\u00f3n de DSMR", + "port": "Selecciona el dispositivo" }, "title": "Dispositivo" }, @@ -38,7 +38,7 @@ "data": { "type": "Tipo de conexi\u00f3n" }, - "title": "Seleccione el tipo de conexi\u00f3n" + "title": "Selecciona el tipo de conexi\u00f3n" } } }, diff --git a/homeassistant/components/efergy/translations/es.json b/homeassistant/components/efergy/translations/es.json index ce25d3f3184..a93f2f42235 100644 --- a/homeassistant/components/efergy/translations/es.json +++ b/homeassistant/components/efergy/translations/es.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/elgato/translations/es.json b/homeassistant/components/elgato/translations/es.json index 2cdbd7e6ae1..a41872115fa 100644 --- a/homeassistant/components/elgato/translations/es.json +++ b/homeassistant/components/elgato/translations/es.json @@ -17,7 +17,7 @@ "description": "Configura la integraci\u00f3n de Elgato Light con Home Assistant." }, "zeroconf_confirm": { - "description": "\u00bfDesea a\u00f1adir Elgato Key Light con el n\u00famero de serie `{serial_number}` a Home Assistant?", + "description": "\u00bfQuieres a\u00f1adir el Key Light de Elgato con n\u00famero de serie `{serial_number}` a Home Assistant?", "title": "Descubierto dispositivo Elgato Key Light" } } diff --git a/homeassistant/components/elkm1/translations/es.json b/homeassistant/components/elkm1/translations/es.json index 9df8e1f4ddf..25c676caafc 100644 --- a/homeassistant/components/elkm1/translations/es.json +++ b/homeassistant/components/elkm1/translations/es.json @@ -3,8 +3,8 @@ "abort": { "address_already_configured": "Ya est\u00e1 configurado un Elk-M1 con esta direcci\u00f3n", "already_configured": "Ya est\u00e1 configurado un Elk-M1 con este prefijo", - "already_in_progress": "La configuraci\u00f3n ya se encuentra en proceso", - "cannot_connect": "Error al conectar", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -19,22 +19,22 @@ "data": { "password": "Contrase\u00f1a", "protocol": "Protocolo", - "temperature_unit": "La unidad de temperatura que el ElkM1 usa.", - "username": "Usuario" + "temperature_unit": "La unidad de temperatura que utiliza ElkM1.", + "username": "Nombre de usuario" }, - "description": "Con\u00e9ctate al sistema descubierto: {mac_address} ({host})", + "description": "Conectar al sistema descubierto: {mac_address} ({host})", "title": "Conectar con Control Elk-M1" }, "manual_connection": { "data": { - "address": "La direcci\u00f3n IP o el dominio o el puerto serie si se conecta a trav\u00e9s de serie.", + "address": "La direcci\u00f3n IP o dominio o puerto serie si se conecta a trav\u00e9s de serie.", "password": "Contrase\u00f1a", - "prefix": "Un prefijo \u00fanico (dejar en blanco si solo tiene un ElkM1).", + "prefix": "Un prefijo \u00fanico (d\u00e9jalo en blanco si solo tienes un ElkM1).", "protocol": "Protocolo", "temperature_unit": "La unidad de temperatura que utiliza ElkM1.", - "username": "Usuario" + "username": "Nombre de usuario" }, - "description": "Conecte un M\u00f3dulo de Interfaz Universal Powerline Bus Powerline (UPB PIM). La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n [: puerto]' para 'tcp'. El puerto es opcional y el valor predeterminado es 2101. Ejemplo: '192.168.1.42'. Para el protocolo serie, la direcci\u00f3n debe estar en la forma 'tty [: baudios]'. El baud es opcional y el valor predeterminado es 4800. Ejemplo: '/ dev / ttyS1'.", + "description": "La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n[:puerto]' para 'seguro' y 'no seguro'. Ejemplo: '192.168.1.1'. El puerto es opcional y el valor predeterminado es 2101 para 'no seguro' y 2601 para 'seguro'. Para el protocolo serial, la direcci\u00f3n debe tener el formato 'tty[:baud]'. Ejemplo: '/dev/ttyS1'. El baudio es opcional y el valor predeterminado es 115200.", "title": "Conectar con Control Elk-M1" }, "user": { diff --git a/homeassistant/components/elmax/translations/es.json b/homeassistant/components/elmax/translations/es.json index a8130c70d7e..8d6bccd03ab 100644 --- a/homeassistant/components/elmax/translations/es.json +++ b/homeassistant/components/elmax/translations/es.json @@ -6,8 +6,8 @@ "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_pin": "El pin proporcionado no es v\u00e1lido", - "network_error": "Se ha producido un error de red", - "no_panel_online": "No se encontr\u00f3 ning\u00fan panel de control de Elmax en l\u00ednea.", + "network_error": "Ocurri\u00f3 un error de red", + "no_panel_online": "No se encontr\u00f3 ning\u00fan panel de control Elmax en l\u00ednea.", "unknown": "Error inesperado" }, "step": { @@ -17,14 +17,14 @@ "panel_name": "Nombre del panel", "panel_pin": "C\u00f3digo PIN" }, - "description": "Seleccione el panel que desea controlar con esta integraci\u00f3n. Tenga en cuenta que el panel debe estar activado para poder configurarlo." + "description": "Selecciona qu\u00e9 panel te gustar\u00eda controlar con esta integraci\u00f3n. Ten en cuenta que el panel debe estar ENCENDIDO para poder configurarlo." }, "user": { "data": { "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Inicie sesi\u00f3n en Elmax Cloud con sus credenciales" + "description": "Por favor, inicia sesi\u00f3n en la nube de Elmax con tus credenciales" } } } diff --git a/homeassistant/components/emonitor/translations/es.json b/homeassistant/components/emonitor/translations/es.json index bef4b3b2329..8438338bbf9 100644 --- a/homeassistant/components/emonitor/translations/es.json +++ b/homeassistant/components/emonitor/translations/es.json @@ -7,7 +7,7 @@ "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, - "flow_title": "SiteSage {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "\u00bfQuieres configurar {name} ({host})?", diff --git a/homeassistant/components/enphase_envoy/translations/es.json b/homeassistant/components/enphase_envoy/translations/es.json index 24a63fa6c73..ab385d0a282 100644 --- a/homeassistant/components/enphase_envoy/translations/es.json +++ b/homeassistant/components/enphase_envoy/translations/es.json @@ -9,7 +9,7 @@ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, - "flow_title": "Envoy {serial} ({host})", + "flow_title": "{serial} ({host})", "step": { "user": { "data": { @@ -17,7 +17,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Para los modelos m\u00e1s nuevos, introduzca el nombre de usuario `envoy` sin contrase\u00f1a. Para los modelos m\u00e1s antiguos, introduzca el nombre de usuario `installer` sin contrase\u00f1a. Para todos los dem\u00e1s modelos, introduzca un nombre de usuario y una contrase\u00f1a v\u00e1lidos." + "description": "Para los modelos m\u00e1s nuevos, introduce el nombre de usuario `envoy` sin contrase\u00f1a. Para modelos m\u00e1s antiguos, introduce el nombre de usuario `installer` sin contrase\u00f1a. Para todos los dem\u00e1s modelos, introduce un nombre de usuario y contrase\u00f1a v\u00e1lidos." } } } diff --git a/homeassistant/components/environment_canada/translations/es.json b/homeassistant/components/environment_canada/translations/es.json index 7cbfb8caed9..5b491979072 100644 --- a/homeassistant/components/environment_canada/translations/es.json +++ b/homeassistant/components/environment_canada/translations/es.json @@ -1,10 +1,10 @@ { "config": { "error": { - "bad_station_id": "El ID de la estaci\u00f3n no es v\u00e1lido, falta o no se encuentra en la base de datos de ID de la estaci\u00f3n", + "bad_station_id": "El ID de la estaci\u00f3n no es v\u00e1lida, falta o no se encuentra en la base de datos de IDs de estaci\u00f3n", "cannot_connect": "No se pudo conectar", "error_response": "Error de respuesta de Environment Canada", - "too_many_attempts": "Las conexiones con Environment Canada son limitadas; Int\u00e9ntalo de nuevo en 60 segundos", + "too_many_attempts": "Las conexiones con Environment Canada tienen una frecuencia limitada; Int\u00e9ntalo de nuevo en 60 segundos", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/escea/translations/pl.json b/homeassistant/components/escea/translations/pl.json new file mode 100644 index 00000000000..cd9489661c0 --- /dev/null +++ b/homeassistant/components/escea/translations/pl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "confirm": { + "description": "Czy chcesz skonfigurowa\u0107 kominek Escea?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/es.json b/homeassistant/components/esphome/translations/es.json index 68e53160322..f3e73d97a52 100644 --- a/homeassistant/components/esphome/translations/es.json +++ b/homeassistant/components/esphome/translations/es.json @@ -3,12 +3,12 @@ "abort": { "already_configured": "ESP ya est\u00e1 configurado", "already_in_progress": "La configuraci\u00f3n del ESP ya est\u00e1 en marcha", - "reauth_successful": "La re-autenticaci\u00f3n ha funcionado" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "connection_error": "No se puede conectar a ESP. Aseg\u00farate de que tu archivo YAML contenga una l\u00ednea 'api:'.", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "invalid_psk": "La clave de transporte cifrado no es v\u00e1lida. Por favor, aseg\u00farese de que coincide con la que tiene en su configuraci\u00f3n", + "invalid_psk": "La clave de cifrado de transporte no es v\u00e1lida. Por favor, aseg\u00farate de que coincida con lo que tienes en tu configuraci\u00f3n", "resolve_error": "No se puede resolver la direcci\u00f3n del ESP. Si este error persiste, por favor, configura una direcci\u00f3n IP est\u00e1tica" }, "flow_title": "{name}", @@ -20,20 +20,20 @@ "description": "Escribe la contrase\u00f1a que hayas puesto en la configuraci\u00f3n para {name}." }, "discovery_confirm": { - "description": "\u00bfQuieres a\u00f1adir el nodo `{name}` de ESPHome a Home Assistant?", + "description": "\u00bfQuieres a\u00f1adir el nodo de ESPHome `{name}` a Home Assistant?", "title": "Nodo ESPHome descubierto" }, "encryption_key": { "data": { "noise_psk": "Clave de cifrado" }, - "description": "Por favor, introduzca la clave de cifrado que estableci\u00f3 en su configuraci\u00f3n para {name}." + "description": "Por favor, introduce la clave de cifrado que estableciste en tu configuraci\u00f3n para {name}." }, "reauth_confirm": { "data": { "noise_psk": "Clave de cifrado" }, - "description": "El dispositivo ESPHome {name} ha activado el transporte cifrado o ha cambiado la clave de cifrado. Por favor, introduzca la clave actualizada." + "description": "El dispositivo ESPHome {name} habilit\u00f3 el cifrado de transporte o cambi\u00f3 la clave de cifrado. Introduce la clave actualizada." }, "user": { "data": { diff --git a/homeassistant/components/ezviz/translations/es.json b/homeassistant/components/ezviz/translations/es.json index f8a80614fb0..a69cb8d5d24 100644 --- a/homeassistant/components/ezviz/translations/es.json +++ b/homeassistant/components/ezviz/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured_account": "La cuenta ya est\u00e1 configurada", - "ezviz_cloud_account_missing": "Falta la cuenta de Ezviz Cloud. Por favor, reconfigura la cuenta de Ezviz Cloud", + "ezviz_cloud_account_missing": "Falta la cuenta de Ezviz Cloud. Por favor, vuelve a configurar la cuenta de Ezviz Cloud", "unknown": "Error inesperado" }, "error": { @@ -35,7 +35,7 @@ "username": "Nombre de usuario" }, "description": "Especificar manualmente la URL de tu regi\u00f3n", - "title": "Conectar con la URL personalizada de Ezviz" + "title": "Conectar a la URL personalizada de Ezviz" } } }, @@ -43,8 +43,8 @@ "step": { "init": { "data": { - "ffmpeg_arguments": "Par\u00e1metros pasados a ffmpeg para c\u00e1maras", - "timeout": "Tiempo de espera de la solicitud (segundos)" + "ffmpeg_arguments": "Argumentos pasados a ffmpeg para las c\u00e1maras", + "timeout": "Tiempo de espera de solicitud (segundos)" } } } diff --git a/homeassistant/components/faa_delays/translations/es.json b/homeassistant/components/faa_delays/translations/es.json index 71f7fecef41..3108f3d30fe 100644 --- a/homeassistant/components/faa_delays/translations/es.json +++ b/homeassistant/components/faa_delays/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "Este aeropuerto ya est\u00e1 configurado." }, "error": { - "cannot_connect": "Fallo al conectar", + "cannot_connect": "No se pudo conectar", "invalid_airport": "El c\u00f3digo del aeropuerto no es v\u00e1lido", "unknown": "Error inesperado" }, @@ -13,7 +13,7 @@ "data": { "id": "Aeropuerto" }, - "description": "Introduzca un c\u00f3digo de aeropuerto estadounidense en formato IATA", + "description": "Introduce un c\u00f3digo de aeropuerto de EE.UU. en formato IATA", "title": "Retrasos de la FAA" } } diff --git a/homeassistant/components/fan/translations/es.json b/homeassistant/components/fan/translations/es.json index e2bc0b1611d..15486b23a48 100644 --- a/homeassistant/components/fan/translations/es.json +++ b/homeassistant/components/fan/translations/es.json @@ -10,7 +10,7 @@ "is_on": "{entity_name} est\u00e1 activado" }, "trigger_type": { - "changed_states": "{entity_name} activado o desactivado", + "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", "turned_off": "{entity_name} desactivado", "turned_on": "{entity_name} activado" } diff --git a/homeassistant/components/fibaro/translations/es.json b/homeassistant/components/fibaro/translations/es.json index 567ae7e59a2..0bf8f1aaa76 100644 --- a/homeassistant/components/fibaro/translations/es.json +++ b/homeassistant/components/fibaro/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -13,7 +13,7 @@ "data": { "import_plugins": "\u00bfImportar entidades desde los plugins de fibaro?", "password": "Contrase\u00f1a", - "url": "URL en el format http://HOST/api/", + "url": "URL en el formato http://HOST/api/", "username": "Nombre de usuario" } } diff --git a/homeassistant/components/filesize/translations/es.json b/homeassistant/components/filesize/translations/es.json index ee030e86cf9..c78a497b9d6 100644 --- a/homeassistant/components/filesize/translations/es.json +++ b/homeassistant/components/filesize/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { - "not_allowed": "Ruta no permitida", + "not_allowed": "La ruta no est\u00e1 permitida", "not_valid": "La ruta no es v\u00e1lida" }, "step": { diff --git a/homeassistant/components/fivem/translations/es.json b/homeassistant/components/fivem/translations/es.json index 273435a4e5e..3264888e71e 100644 --- a/homeassistant/components/fivem/translations/es.json +++ b/homeassistant/components/fivem/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Error al conectarse. Compruebe el host y el puerto e int\u00e9ntelo de nuevo. Aseg\u00farese tambi\u00e9n de que est\u00e1 ejecutando el servidor FiveM m\u00e1s reciente.", + "cannot_connect": "No se pudo conectar. Por favor, verifica el host y el puerto y vuelve a intentarlo. Tambi\u00e9n aseg\u00farate de estar ejecutando el servidor FiveM m\u00e1s reciente.", "invalid_game_name": "La API del juego al que intentas conectarte no es un juego de FiveM.", "unknown_error": "Error inesperado" }, diff --git a/homeassistant/components/flipr/translations/es.json b/homeassistant/components/flipr/translations/es.json index 0a066451b84..02af1fc3121 100644 --- a/homeassistant/components/flipr/translations/es.json +++ b/homeassistant/components/flipr/translations/es.json @@ -22,7 +22,7 @@ "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a" }, - "description": "Con\u00e9ctese usando su cuenta Flipr.", + "description": "Con\u00e9ctate usando tu cuenta Flipr.", "title": "Conectarse a Flipr" } } diff --git a/homeassistant/components/flume/translations/es.json b/homeassistant/components/flume/translations/es.json index 5789a2eccc0..b2ffbe162a8 100644 --- a/homeassistant/components/flume/translations/es.json +++ b/homeassistant/components/flume/translations/es.json @@ -15,7 +15,7 @@ "password": "Contrase\u00f1a" }, "description": "La contrase\u00f1a de {username} ya no es v\u00e1lida.", - "title": "Reautenticar tu cuenta de Flume" + "title": "Volver a autenticar tu cuenta Flume" }, "user": { "data": { diff --git a/homeassistant/components/flunearyou/translations/pl.json b/homeassistant/components/flunearyou/translations/pl.json index b250d220cf3..71d390992e4 100644 --- a/homeassistant/components/flunearyou/translations/pl.json +++ b/homeassistant/components/flunearyou/translations/pl.json @@ -16,5 +16,18 @@ "title": "Konfiguracja Flu Near You" } } + }, + "issues": { + "integration_removal": { + "fix_flow": { + "step": { + "confirm": { + "description": "Zewn\u0119trzne \u017ar\u00f3d\u0142o danych dla integracji Flu Near You nie jest ju\u017c dost\u0119pne; tym sposobem integracja ju\u017c nie dzia\u0142a. \n\nNaci\u015bnij ZATWIERD\u0179, aby usun\u0105\u0107 Flu Near You z Home Assistanta.", + "title": "Usu\u0144 Flu Near You" + } + } + }, + "title": "Flu Near You nie jest ju\u017c dost\u0119pna" + } } } \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/es.json b/homeassistant/components/flux_led/translations/es.json index 54fef29c371..89a4e02319e 100644 --- a/homeassistant/components/flux_led/translations/es.json +++ b/homeassistant/components/flux_led/translations/es.json @@ -11,13 +11,13 @@ "flow_title": "{model} {id} ({ipaddr})", "step": { "discovery_confirm": { - "description": "\u00bfQuieres configurar {model} {id} ( {ipaddr} )?" + "description": "\u00bfQuieres configurar {model} {id} ({ipaddr})?" }, "user": { "data": { "host": "Host" }, - "description": "Si deja el host vac\u00edo, la detecci\u00f3n se utilizar\u00e1 para buscar dispositivos." + "description": "Si dejas el host vac\u00edo, se usar\u00e1 el descubrimiento para encontrar dispositivos." } } }, @@ -25,10 +25,10 @@ "step": { "init": { "data": { - "custom_effect_colors": "Efecto personalizado: Lista de 1 a 16 colores [R, G, B]. Ejemplo: [255,0,255],[60,128,0]", + "custom_effect_colors": "Efecto personalizado: Lista de 1 a 16 colores [R,G,B]. Ejemplo: [255,0,255],[60,128,0]", "custom_effect_speed_pct": "Efecto personalizado: Velocidad en porcentajes para el efecto que cambia de color.", - "custom_effect_transition": "Efecto personalizado: Tipo de transici\u00f3n entre colores.", - "mode": "Modo de brillo elegido." + "custom_effect_transition": "Efecto personalizado: Tipo de transici\u00f3n entre los colores.", + "mode": "El modo de brillo elegido." } } } diff --git a/homeassistant/components/forecast_solar/translations/es.json b/homeassistant/components/forecast_solar/translations/es.json index d82bd944202..5567fa63ee2 100644 --- a/homeassistant/components/forecast_solar/translations/es.json +++ b/homeassistant/components/forecast_solar/translations/es.json @@ -3,14 +3,14 @@ "step": { "user": { "data": { - "azimuth": "Acimut (360 grados, 0 = Norte, 90 = Este, 180 = Sur, 270 = Oeste)", + "azimuth": "Azimut (360 grados, 0 = Norte, 90 = Este, 180 = Sur, 270 = Oeste)", "declination": "Declinaci\u00f3n (0 = Horizontal, 90 = Vertical)", "latitude": "Latitud", "longitude": "Longitud", - "modules power": "Potencia total en vatios pico de sus m\u00f3dulos solares", + "modules power": "Potencia pico total en vatios de tus m\u00f3dulos solares", "name": "Nombre" }, - "description": "Rellene los datos de sus paneles solares. Consulte la documentaci\u00f3n si alg\u00fan campo no est\u00e1 claro." + "description": "Rellena los datos de tus placas solares. Consulta la documentaci\u00f3n si un campo no est\u00e1 claro." } } }, @@ -22,10 +22,10 @@ "azimuth": "Azimut (360 grados, 0 = Norte, 90 = Este, 180 = Sur, 270 = Oeste)", "damping": "Factor de amortiguaci\u00f3n: ajusta los resultados por la ma\u00f1ana y por la noche", "declination": "Declinaci\u00f3n (0 = Horizontal, 90 = Vertical)", - "inverter_size": "Potencia del inversor (Watts)", + "inverter_size": "Tama\u00f1o del inversor (vatios)", "modules power": "Potencia pico total en vatios de tus m\u00f3dulos solares" }, - "description": "Estos valores permiten ajustar el resultado de Solar.Forecast. Consulte la documentaci\u00f3n si un campo no est\u00e1 claro." + "description": "Estos valores permiten modificar el resultado de Solar.Forecast. Por favor, consulta la documentaci\u00f3n si un campo no est\u00e1 claro." } } } diff --git a/homeassistant/components/freedompro/translations/es.json b/homeassistant/components/freedompro/translations/es.json index c08c30d64dc..1657aaa908e 100644 --- a/homeassistant/components/freedompro/translations/es.json +++ b/homeassistant/components/freedompro/translations/es.json @@ -12,7 +12,7 @@ "data": { "api_key": "Clave API" }, - "description": "Ingresa la clave API obtenida de https://home.freedompro.eu", + "description": "Por favor, introduce la clave API obtenida de https://home.freedompro.eu", "title": "Clave API de Freedompro" } } diff --git a/homeassistant/components/fritz/translations/es.json b/homeassistant/components/fritz/translations/es.json index 21dafaec367..a8f5168902f 100644 --- a/homeassistant/components/fritz/translations/es.json +++ b/homeassistant/components/fritz/translations/es.json @@ -13,14 +13,14 @@ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "upnp_not_configured": "Falta la configuraci\u00f3n de UPnP en el dispositivo." }, - "flow_title": "FRITZ!Box Tools: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Descubierto FRITZ!Box: {name}\n\nConfigurar FRITZ!Box Tools para controlar tu {name}", + "description": "Descubierto FRITZ!Box: {name}\n\nConfigura FRITZ!Box Tools para controlar tu {name}", "title": "Configurar FRITZ!Box Tools" }, "reauth_confirm": { @@ -28,7 +28,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Actualizar credenciales de FRITZ!Box Tools para: {host}.\n\n FRITZ!Box Tools no puede iniciar sesi\u00f3n en tu FRITZ!Box.", + "description": "Actualiza las credenciales de FRITZ!Box Tools para: {host}. \n\nFRITZ!Box Tools no puede iniciar sesi\u00f3n en tu FRITZ!Box.", "title": "Actualizando FRITZ!Box Tools - credenciales" }, "user": { @@ -38,8 +38,8 @@ "port": "Puerto", "username": "Nombre de usuario" }, - "description": "Configure las herramientas de FRITZ! Box para controlar su FRITZ! Box.\n M\u00ednimo necesario: nombre de usuario, contrase\u00f1a.", - "title": "Configurar las herramientas de FRITZ! Box" + "description": "Configura las herramientas de FRITZ!Box para controlar tu FRITZ!Box.\nM\u00ednimo necesario: nombre de usuario, contrase\u00f1a.", + "title": "Configurar las herramientas de FRITZ!Box" } } }, @@ -48,7 +48,7 @@ "init": { "data": { "consider_home": "Segundos para considerar un dispositivo en 'casa'", - "old_discovery": "Habilitar m\u00e9todo de descubrimiento antiguo" + "old_discovery": "Habilitar el m\u00e9todo de descubrimiento antiguo" } } } diff --git a/homeassistant/components/fronius/translations/es.json b/homeassistant/components/fronius/translations/es.json index e8df3b81a6e..98fdc7e1ebd 100644 --- a/homeassistant/components/fronius/translations/es.json +++ b/homeassistant/components/fronius/translations/es.json @@ -11,13 +11,13 @@ "flow_title": "{device}", "step": { "confirm_discovery": { - "description": "\u00bfQuieres agregar {device} a Home Assistant?" + "description": "\u00bfQuieres a\u00f1adir {device} a Home Assistant?" }, "user": { "data": { "host": "Host" }, - "description": "Configure la direcci\u00f3n IP o el nombre de host local de su dispositivo Fronius.", + "description": "Configura la direcci\u00f3n IP o el nombre de host local de tu dispositivo Fronius.", "title": "Fronius SolarNet" } } diff --git a/homeassistant/components/garages_amsterdam/translations/es.json b/homeassistant/components/garages_amsterdam/translations/es.json index 79433b6b854..dc4b9aa9c96 100644 --- a/homeassistant/components/garages_amsterdam/translations/es.json +++ b/homeassistant/components/garages_amsterdam/translations/es.json @@ -10,9 +10,9 @@ "data": { "garage_name": "Nombre del garaje" }, - "title": "Elige un garaje para vigilar" + "title": "Elige un garaje para supervisar" } } }, - "title": "Garajes Amsterdam" + "title": "Garages Amsterdam" } \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/es.json b/homeassistant/components/geofency/translations/es.json index 6726a4d2e3b..8e3c806f708 100644 --- a/homeassistant/components/geofency/translations/es.json +++ b/homeassistant/components/geofency/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "No est\u00e1 conectado a Home Assistant Cloud.", + "cloud_not_connected": "No conectado a Home Assistant Cloud.", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, diff --git a/homeassistant/components/github/translations/es.json b/homeassistant/components/github/translations/es.json index 37bf1d4689e..bf1dcf64ae4 100644 --- a/homeassistant/components/github/translations/es.json +++ b/homeassistant/components/github/translations/es.json @@ -5,14 +5,14 @@ "could_not_register": "No se pudo registrar la integraci\u00f3n con GitHub" }, "progress": { - "wait_for_device": "1. Abra {url}\n 2.Pegue la siguiente clave para autorizar la integraci\u00f3n:\n ```\n {code}\n ```\n" + "wait_for_device": "1. Abre {url}\n2.Pega la siguiente clave para autorizar la integraci\u00f3n:\n```\n{code}\n```\n" }, "step": { "repositories": { "data": { - "repositories": "Seleccione los repositorios a seguir." + "repositories": "Selecciona los repositorios a seguir." }, - "title": "Configuraci\u00f3n de repositorios" + "title": "Configura los repositorios" } } } diff --git a/homeassistant/components/goalzero/translations/es.json b/homeassistant/components/goalzero/translations/es.json index e02b06d32f5..9772f3f6d91 100644 --- a/homeassistant/components/goalzero/translations/es.json +++ b/homeassistant/components/goalzero/translations/es.json @@ -12,7 +12,7 @@ }, "step": { "confirm_discovery": { - "description": "Se recomienda reservar el DHCP en el router. Si no se configura, el dispositivo puede dejar de estar disponible hasta que el Home Assistant detecte la nueva direcci\u00f3n ip. Consulte el manual de usuario de su router." + "description": "Se recomienda la reserva de DHCP en tu router. Si no se configura, es posible que el dispositivo no est\u00e9 disponible hasta que Home Assistant detecte la nueva direcci\u00f3n IP. Consulta el manual de usuario de tu router." }, "user": { "data": { diff --git a/homeassistant/components/google/translations/es.json b/homeassistant/components/google/translations/es.json index 5bd628e1f6e..eaf84a11931 100644 --- a/homeassistant/components/google/translations/es.json +++ b/homeassistant/components/google/translations/es.json @@ -5,20 +5,20 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "already_in_progress": "El proceso de configuraci\u00f3n ya est\u00e1 en curso", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", - "code_expired": "El c\u00f3digo de autenticaci\u00f3n caduc\u00f3 o la configuraci\u00f3n de la credencial no es v\u00e1lida, int\u00e9ntelo de nuevo.", + "code_expired": "El c\u00f3digo de autenticaci\u00f3n caduc\u00f3 o la configuraci\u00f3n de la credencial no es v\u00e1lida, por favor, int\u00e9ntalo de nuevo.", "invalid_access_token": "Token de acceso no v\u00e1lido", - "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "oauth_error": "Se han recibido datos token inv\u00e1lidos.", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente", + "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n" }, "create_entry": { - "default": "Autenticaci\u00f3n exitosa" + "default": "Autenticado correctamente" }, "progress": { - "exchange": "Para vincular su cuenta de Google, visite [ {url} ]( {url} ) e ingrese el c\u00f3digo: \n\n {user_code}" + "exchange": "Para vincular tu cuenta de Google, visita la [{url}]({url}) e introduce el c\u00f3digo:\n\n{user_code}" }, "step": { "auth": { @@ -28,8 +28,8 @@ "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" }, "reauth_confirm": { - "description": "La integraci\u00f3n de Google Calendar necesita volver a autenticar su cuenta", - "title": "Integraci\u00f3n de la reautenticaci\u00f3n" + "description": "La integraci\u00f3n Google Calendar necesita volver a autenticar tu cuenta", + "title": "Volver a autenticar la integraci\u00f3n" } } }, diff --git a/homeassistant/components/google_travel_time/translations/es.json b/homeassistant/components/google_travel_time/translations/es.json index cecfc620e47..44d227554cb 100644 --- a/homeassistant/components/google_travel_time/translations/es.json +++ b/homeassistant/components/google_travel_time/translations/es.json @@ -14,7 +14,7 @@ "name": "Nombre", "origin": "Origen" }, - "description": "Al especificar el origen y el destino, puedes proporcionar una o m\u00e1s ubicaciones separadas por el car\u00e1cter de barra vertical, en forma de una direcci\u00f3n, coordenadas de latitud/longitud o un ID de lugar de Google. Al especificar la ubicaci\u00f3n utilizando un ID de lugar de Google, el ID debe tener el prefijo `place_id:`." + "description": "Al especificar el origen y el destino, puedes proporcionar una o m\u00e1s ubicaciones separadas por el car\u00e1cter de barra vertical, en forma de direcci\u00f3n, coordenadas de latitud/longitud o un ID de lugar de Google. Al especificar la ubicaci\u00f3n mediante un ID de lugar de Google, el ID debe tener el prefijo `place_id:`." } } }, @@ -28,10 +28,10 @@ "time": "Hora", "time_type": "Tipo de tiempo", "transit_mode": "Modo de tr\u00e1nsito", - "transit_routing_preference": "Preferencia de enrutamiento de tr\u00e1nsito", + "transit_routing_preference": "Preferencia de ruta de tr\u00e1nsito", "units": "Unidades" }, - "description": "Opcionalmente, puedes especificar una hora de salida o una hora de llegada. Si especifica una hora de salida, puedes introducir `ahora`, una marca de tiempo Unix o una cadena de tiempo 24 horas como `08:00:00`. Si especifica una hora de llegada, puede usar una marca de tiempo Unix o una cadena de tiempo 24 horas como `08:00:00`" + "description": "Opcionalmente, puedes especificar una Hora de salida o una Hora de llegada. Si especificas una hora de salida, puedes introducir \"ahora\", una marca de tiempo de Unix o una cadena de tiempo de 24 horas como \"08:00:00\". Si especificas una hora de llegada, puedes usar una marca de tiempo de Unix o una cadena de tiempo de 24 horas como `08:00:00`" } } }, diff --git a/homeassistant/components/group/translations/es.json b/homeassistant/components/group/translations/es.json index bea5a398f8c..c5fe0ede852 100644 --- a/homeassistant/components/group/translations/es.json +++ b/homeassistant/components/group/translations/es.json @@ -8,32 +8,32 @@ "hide_members": "Ocultar miembros", "name": "Nombre" }, - "description": "Si \"todas las entidades\" est\u00e1n habilitadas, el estado del grupo est\u00e1 activado solo si todos los miembros est\u00e1n activados. Si \"todas las entidades\" est\u00e1n deshabilitadas, el estado del grupo es activado si alg\u00fan miembro est\u00e1 activado.", - "title": "Agregar grupo" + "description": "Si \"todas las entidades\" est\u00e1 habilitado, el estado del grupo est\u00e1 activado solo si todos los miembros est\u00e1n activados. Si \"todas las entidades\" est\u00e1 deshabilitado, el estado del grupo es activado si alg\u00fan miembro est\u00e1 activado.", + "title": "A\u00f1adir grupo" }, "cover": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros", - "name": "Nombre del Grupo" + "hide_members": "Ocultar miembros", + "name": "Nombre" }, "title": "A\u00f1adir grupo" }, "fan": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros", + "hide_members": "Ocultar miembros", "name": "Nombre" }, - "title": "Agregar grupo" + "title": "A\u00f1adir grupo" }, "light": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros", + "hide_members": "Ocultar miembros", "name": "Nombre" }, - "title": "Agregar grupo" + "title": "A\u00f1adir grupo" }, "lock": { "data": { @@ -46,10 +46,10 @@ "media_player": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros", + "hide_members": "Ocultar miembros", "name": "Nombre" }, - "title": "Agregar grupo" + "title": "A\u00f1adir grupo" }, "switch": { "data": { @@ -60,12 +60,12 @@ "title": "A\u00f1adir grupo" }, "user": { - "description": "Los grupos permiten crear una nueva entidad que representa a varias entidades del mismo tipo.", + "description": "Los grupos te permiten crear una nueva entidad que representa varias entidades del mismo tipo.", "menu_options": { "binary_sensor": "Grupo de sensores binarios", - "cover": "Grupo de cubiertas", + "cover": "Grupo de persianas/cortinas", "fan": "Grupo de ventiladores", - "light": "Grupo de luz", + "light": "Grupo de luces", "lock": "Bloquear el grupo", "media_player": "Grupo de reproductores multimedia", "switch": "Grupo de conmutadores" @@ -80,27 +80,27 @@ "data": { "all": "Todas las entidades", "entities": "Miembros", - "hide_members": "Esconde miembros" + "hide_members": "Ocultar miembros" }, "description": "Si \"todas las entidades\" est\u00e1 habilitado, el estado del grupo est\u00e1 activado solo si todos los miembros est\u00e1n activados. Si \"todas las entidades\" est\u00e1 deshabilitado, el estado del grupo es activado si alg\u00fan miembro est\u00e1 activado." }, "cover": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros" + "hide_members": "Ocultar miembros" } }, "fan": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros" + "hide_members": "Ocultar miembros" } }, "light": { "data": { "all": "Todas las entidades", "entities": "Miembros", - "hide_members": "Esconde miembros" + "hide_members": "Ocultar miembros" }, "description": "Si \"todas las entidades\" est\u00e1 habilitado, el estado del grupo est\u00e1 activado solo si todos los miembros est\u00e1n activados. Si \"todas las entidades\" est\u00e1 deshabilitado, el estado del grupo es activado si alg\u00fan miembro est\u00e1 activado." }, @@ -113,7 +113,7 @@ "media_player": { "data": { "entities": "Miembros", - "hide_members": "Esconde miembros" + "hide_members": "Ocultar miembros" } }, "switch": { diff --git a/homeassistant/components/guardian/translations/de.json b/homeassistant/components/guardian/translations/de.json index 2078df1cae9..9b042a7a1c4 100644 --- a/homeassistant/components/guardian/translations/de.json +++ b/homeassistant/components/guardian/translations/de.json @@ -17,5 +17,18 @@ "description": "Konfiguriere ein lokales Elexa Guardian Ger\u00e4t." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst `{alternate_service}` mit einer Zielentit\u00e4ts-ID von `{alternate_target}` zu verwenden. Klicke dann unten auf SUBMIT, um dieses Problem als behoben zu markieren.", + "title": "Der Dienst {deprecated_service} wird entfernt" + } + } + }, + "title": "Der Dienst {deprecated_service} wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/et.json b/homeassistant/components/guardian/translations/et.json index 22ee0bf1300..49172263d9c 100644 --- a/homeassistant/components/guardian/translations/et.json +++ b/homeassistant/components/guardian/translations/et.json @@ -17,5 +17,18 @@ "description": "Seadista kohalik Elexa Guardiani seade." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Uuenda k\u00f5iki seda teenust kasutavaid automatiseerimisi v\u00f5i skripte, et need kasutaksid selle asemel teenust `{alternate_service}}, mille siht\u00fcksuse ID on `{alternate_target}}. Seej\u00e4rel kl\u00f5psa allpool nuppu ESITA, et m\u00e4rkida see probleem lahendatuks.", + "title": "Teenus {deprecated_service} eemaldatakse" + } + } + }, + "title": "Teenus {deprecated_service} eemaldatakse" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json index 1f8f9039707..25a0abb5526 100644 --- a/homeassistant/components/guardian/translations/hu.json +++ b/homeassistant/components/guardian/translations/hu.json @@ -17,5 +17,18 @@ "description": "Konfigur\u00e1lja a helyi Elexa Guardian eszk\u00f6zt." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Friss\u00edtsen minden olyan automatiz\u00e1l\u00e1st vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, hogy helyette az `{alternate_service}` szolg\u00e1ltat\u00e1st haszn\u00e1lja a `{alternate_target}` entit\u00e1ssal. Ezut\u00e1n kattintson az al\u00e1bbi MEHET gombra a probl\u00e9ma megoldottk\u00e9nt val\u00f3 megjel\u00f6l\u00e9s\u00e9hez.", + "title": "A {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } + }, + "title": "A {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/id.json b/homeassistant/components/guardian/translations/id.json index 77c46e95afc..0d06eb61729 100644 --- a/homeassistant/components/guardian/translations/id.json +++ b/homeassistant/components/guardian/translations/id.json @@ -17,5 +17,18 @@ "description": "Konfigurasikan perangkat Elexa Guardian lokal." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Perbarui semua otomasi atau skrip yang menggunakan layanan ini untuk menggunakan layanan `{alternate_service}` dengan ID entitas target `{alternate_target}`. Kemudian, klik KIRIM di bawah ini untuk menandai masalah ini sebagai terselesaikan.", + "title": "Layanan {deprecated_service} dalam proses penghapusan" + } + } + }, + "title": "Layanan {deprecated_service} dalam proses penghapusan" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/no.json b/homeassistant/components/guardian/translations/no.json index a61b43dcfea..9c2669fdeb2 100644 --- a/homeassistant/components/guardian/translations/no.json +++ b/homeassistant/components/guardian/translations/no.json @@ -17,5 +17,18 @@ "description": "Konfigurer en lokal Elexa Guardian-enhet." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten til i stedet \u00e5 bruke ` {alternate_service} `-tjenesten med en m\u00e5lenhets-ID p\u00e5 ` {alternate_target} `. Klikk deretter SEND nedenfor for \u00e5 merke dette problemet som l\u00f8st.", + "title": "{deprecated_service} -tjenesten blir fjernet" + } + } + }, + "title": "{deprecated_service} -tjenesten blir fjernet" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/pl.json b/homeassistant/components/guardian/translations/pl.json index 5e5b2d143e2..c86f98b3b8e 100644 --- a/homeassistant/components/guardian/translations/pl.json +++ b/homeassistant/components/guardian/translations/pl.json @@ -17,5 +17,18 @@ "description": "Skonfiguruj lokalne urz\u0105dzenie Elexa Guardian." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Zaktualizuj wszystkie automatyzacje lub skrypty, kt\u00f3re u\u017cywaj\u0105 tej us\u0142ugi, aby zamiast tego u\u017cywa\u0142y us\u0142ugi `{alternate_service}` z encj\u0105 docelow\u0105 `{alternate_target}`. Nast\u0119pnie kliknij ZATWIERD\u0179 poni\u017cej, aby oznaczy\u0107 ten problem jako rozwi\u0105zany.", + "title": "Us\u0142uga {deprecated_service} zostanie usuni\u0119ta" + } + } + }, + "title": "Us\u0142uga {deprecated_service} zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/pt-BR.json b/homeassistant/components/guardian/translations/pt-BR.json index f4d4273b4ab..2a4514f4968 100644 --- a/homeassistant/components/guardian/translations/pt-BR.json +++ b/homeassistant/components/guardian/translations/pt-BR.json @@ -17,5 +17,18 @@ "description": "Configure um dispositivo local Elexa Guardian." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam este servi\u00e7o para usar o servi\u00e7o `{alternate_service}` com um ID de entidade de destino de `{alternate_target}`. Em seguida, clique em ENVIAR abaixo para marcar este problema como resolvido.", + "title": "O servi\u00e7o {deprecated_service} est\u00e1 sendo removido" + } + } + }, + "title": "O servi\u00e7o {deprecated_service} est\u00e1 sendo removido" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/zh-Hant.json b/homeassistant/components/guardian/translations/zh-Hant.json index 40a7de81170..baa0e477b4c 100644 --- a/homeassistant/components/guardian/translations/zh-Hant.json +++ b/homeassistant/components/guardian/translations/zh-Hant.json @@ -17,5 +17,18 @@ "description": "\u8a2d\u5b9a\u5340\u57df Elexa Guardian \u88dd\u7f6e\u3002" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u4f7f\u7528\u6b64\u670d\u52d9\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\u3001\u4ee5\u53d6\u4ee3\u4f7f\u7528\u76ee\u6a19\u5be6\u9ad4 ID \u70ba `{alternate_target}` \u4e4b `{alternate_service}` \u670d\u52d9\uff0c\u7136\u5f8c\u9ede\u9078\u50b3\u9001\u4ee5\u6a19\u793a\u554f\u984c\u5df2\u89e3\u6c7a\u3002", + "title": "{deprecated_service} \u670d\u52d9\u5373\u5c07\u79fb\u9664" + } + } + }, + "title": "{deprecated_service} \u670d\u52d9\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/es.json b/homeassistant/components/habitica/translations/es.json index 55cf8eb7642..681336e553f 100644 --- a/homeassistant/components/habitica/translations/es.json +++ b/homeassistant/components/habitica/translations/es.json @@ -12,7 +12,7 @@ "name": "Anular el nombre de usuario de Habitica. Se utilizar\u00e1 para llamadas de servicio.", "url": "URL" }, - "description": "Conecta tu perfil de Habitica para permitir la supervisi\u00f3n del perfil y las tareas de tu usuario. Ten en cuenta que api_id y api_key deben obtenerse de https://habitica.com/user/settings/api" + "description": "Conecta tu perfil de Habitica para permitir el seguimiento del perfil y las tareas de tu usuario. Ten en cuenta que api_id y api_key deben obtenerse de https://habitica.com/user/settings/api" } } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/es.json b/homeassistant/components/hisense_aehw4a1/translations/es.json index 8eb81391ec4..3f6e58b792e 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/es.json +++ b/homeassistant/components/hisense_aehw4a1/translations/es.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "\u00bfDesea configurar Hisense AEH-W4A1?" + "description": "\u00bfQuieres configurar Hisense AEH-W4A1?" } } } diff --git a/homeassistant/components/hive/translations/es.json b/homeassistant/components/hive/translations/es.json index ccca2b3ea4d..ffba8558cac 100644 --- a/homeassistant/components/hive/translations/es.json +++ b/homeassistant/components/hive/translations/es.json @@ -6,9 +6,9 @@ "unknown_entry": "No se puede encontrar una entrada existente." }, "error": { - "invalid_code": "No se ha podido iniciar la sesi\u00f3n en Hive. Tu c\u00f3digo de autenticaci\u00f3n de dos factores era incorrecto.", - "invalid_password": "No se ha podido iniciar la sesi\u00f3n en Hive. Contrase\u00f1a incorrecta, por favor, int\u00e9ntelo de nuevo.", - "invalid_username": "No se ha podido iniciar la sesi\u00f3n en Hive. No se reconoce su direcci\u00f3n de correo electr\u00f3nico.", + "invalid_code": "Error al iniciar sesi\u00f3n en Hive. Tu c\u00f3digo de autenticaci\u00f3n de dos factores era incorrecto.", + "invalid_password": "Error al iniciar sesi\u00f3n en Hive. Contrase\u00f1a incorrecta, por favor, prueba de nuevo.", + "invalid_username": "Error al iniciar sesi\u00f3n en Hive. No se reconoce tu direcci\u00f3n de correo electr\u00f3nico.", "no_internet_available": "Se requiere una conexi\u00f3n a Internet para conectarse a Hive.", "unknown": "Error inesperado" }, @@ -17,7 +17,7 @@ "data": { "2fa": "C\u00f3digo de dos factores" }, - "description": "Introduzca su c\u00f3digo de autentificaci\u00f3n Hive. \n \n Introduzca el c\u00f3digo 0000 para solicitar otro c\u00f3digo.", + "description": "introduce tu c\u00f3digo de autenticaci\u00f3n de Hive. \n\nPor favor, introduce el c\u00f3digo 0000 para solicitar otro c\u00f3digo.", "title": "Autenticaci\u00f3n de dos factores de Hive." }, "configuration": { @@ -32,13 +32,13 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Vuelva a introducir sus datos de acceso a Hive.", + "description": "Vuelve a introducir tus datos de acceso a Hive.", "title": "Inicio de sesi\u00f3n en Hive" }, "user": { "data": { "password": "Contrase\u00f1a", - "scan_interval": "Intervalo de exploraci\u00f3n (segundos)", + "scan_interval": "Intervalo de escaneo (segundos)", "username": "Nombre de usuario" }, "description": "Introduce tus datos de acceso a Hive.", @@ -50,9 +50,9 @@ "step": { "user": { "data": { - "scan_interval": "Intervalo de exploraci\u00f3n (segundos)" + "scan_interval": "Intervalo de escaneo (segundos)" }, - "description": "Actualice el intervalo de escaneo para buscar datos m\u00e1s a menudo.", + "description": "Actualiza el intervalo de escaneo para buscar datos con m\u00e1s frecuencia.", "title": "Opciones para Hive" } } diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index 5076e7b8800..170df5c4a13 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -23,11 +23,11 @@ "data": { "entities": "Entidad" }, - "title": "Seleccione la entidad para el accesorio" + "title": "Selecciona la entidad para el accesorio" }, "advanced": { "data": { - "devices": "Dispositivos (disparadores)" + "devices": "Dispositivos (Disparadores)" }, "description": "Esta configuraci\u00f3n solo necesita ser ajustada si el puente HomeKit no es funcional.", "title": "Configuraci\u00f3n avanzada" @@ -37,26 +37,26 @@ "camera_audio": "C\u00e1maras que admiten audio", "camera_copy": "C\u00e1maras compatibles con transmisiones H.264 nativas" }, - "description": "Verifique todas las c\u00e1maras que admiten transmisiones H.264 nativas. Si la c\u00e1mara no emite una transmisi\u00f3n H.264, el sistema transcodificar\u00e1 el video a H.264 para HomeKit. La transcodificaci\u00f3n requiere una CPU de alto rendimiento y es poco probable que funcione en ordenadores de placa \u00fanica.", + "description": "Verifica todas las c\u00e1maras que admitan transmisiones H.264 nativas. Si la c\u00e1mara no emite una transmisi\u00f3n H.264, el sistema transcodificar\u00e1 el video a H.264 para HomeKit. La transcodificaci\u00f3n requiere una CPU de alto rendimiento y es poco probable que funcione en ordenadores de placa \u00fanica.", "title": "Seleccione el c\u00f3dec de video de la c\u00e1mara." }, "exclude": { "data": { "entities": "Entidades" }, - "description": "Se incluir\u00e1n todas las entidades de \" {domains} \" excepto las entidades excluidas y las entidades categorizadas.", + "description": "Se incluir\u00e1n todas las entidades de \"{domains}\" excepto las entidades excluidas y las entidades categorizadas.", "title": "Selecciona las entidades a excluir" }, "include": { "data": { "entities": "Entidades" }, - "description": "Se incluir\u00e1n todas las entidades de \" {domains} \" a menos que se seleccionen entidades espec\u00edficas.", - "title": "Seleccione las entidades a incluir" + "description": "Se incluir\u00e1n todas las entidades de \"{domains}\" a menos que se seleccionen entidades espec\u00edficas.", + "title": "Selecciona las entidades a incluir" }, "init": { "data": { - "domains": "Dominios a incluir", + "domains": "Dominios para incluir", "include_exclude_mode": "Modo de inclusi\u00f3n", "mode": "Mode de HomeKit" }, diff --git a/homeassistant/components/homekit_controller/translations/es.json b/homeassistant/components/homekit_controller/translations/es.json index 54dd93f8a55..48015d0418d 100644 --- a/homeassistant/components/homekit_controller/translations/es.json +++ b/homeassistant/components/homekit_controller/translations/es.json @@ -12,7 +12,7 @@ }, "error": { "authentication_error": "C\u00f3digo HomeKit incorrecto. Por favor, compru\u00e9belo e int\u00e9ntelo de nuevo.", - "insecure_setup_code": "El c\u00f3digo de configuraci\u00f3n solicitado es inseguro debido a su naturaleza trivial. Este accesorio no cumple con los requisitos b\u00e1sicos de seguridad.", + "insecure_setup_code": "El c\u00f3digo de configuraci\u00f3n solicitado no es seguro debido a su naturaleza trivial. Este accesorio no cumple con los requisitos b\u00e1sicos de seguridad.", "max_peers_error": "El dispositivo rechaz\u00f3 el emparejamiento ya que no tiene almacenamiento de emparejamientos libres.", "pairing_failed": "Se ha producido un error no controlado al intentar emparejarse con este dispositivo. Esto puede ser un fallo temporal o que tu dispositivo no est\u00e9 admitido en este momento.", "unable_to_pair": "No se ha podido emparejar, por favor int\u00e9ntelo de nuevo.", diff --git a/homeassistant/components/homekit_controller/translations/select.es.json b/homeassistant/components/homekit_controller/translations/select.es.json index 13c45f8e538..8ef5ae09895 100644 --- a/homeassistant/components/homekit_controller/translations/select.es.json +++ b/homeassistant/components/homekit_controller/translations/select.es.json @@ -1,9 +1,9 @@ { "state": { "homekit_controller__ecobee_mode": { - "away": "Afuera", + "away": "Ausente", "home": "Inicio", - "sleep": "Durmiendo" + "sleep": "Dormir" } } } \ No newline at end of file diff --git a/homeassistant/components/homewizard/translations/es.json b/homeassistant/components/homewizard/translations/es.json index 898d37fed09..c2fe80926da 100644 --- a/homeassistant/components/homewizard/translations/es.json +++ b/homeassistant/components/homewizard/translations/es.json @@ -2,21 +2,21 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "api_not_enabled": "La API no est\u00e1 habilitada. Habilite la API en la aplicaci\u00f3n HomeWizard Energy en configuraci\u00f3n", + "api_not_enabled": "La API no est\u00e1 habilitada. Habilita la API en la aplicaci\u00f3n HomeWizard Energy dentro de Configuraci\u00f3n", "device_not_supported": "Este dispositivo no es compatible", - "invalid_discovery_parameters": "Versi\u00f3n de API no compatible detectada", + "invalid_discovery_parameters": "Se ha detectado una versi\u00f3n de API no compatible", "unknown_error": "Error inesperado" }, "step": { "discovery_confirm": { - "description": "\u00bfDesea configurar {product_type} ({serial}) en {ip_address} ?", + "description": "\u00bfQuieres configurar {product_type} ({serial}) en {ip_address} ?", "title": "Confirmar" }, "user": { "data": { "ip_address": "Direcci\u00f3n IP" }, - "description": "Ingrese la direcci\u00f3n IP de su dispositivo HomeWizard Energy para integrarlo con Home Assistant.", + "description": "Introduce la direcci\u00f3n IP de tu dispositivo HomeWizard Energy para integrarlo con Home Assistant.", "title": "Configurar dispositivo" } } diff --git a/homeassistant/components/honeywell/translations/es.json b/homeassistant/components/honeywell/translations/es.json index c08c02c3632..c97e5aa3ddf 100644 --- a/homeassistant/components/honeywell/translations/es.json +++ b/homeassistant/components/honeywell/translations/es.json @@ -9,7 +9,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Por favor, introduzca las credenciales utilizadas para iniciar sesi\u00f3n en mytotalconnectcomfort.com." + "description": "Por favor, introduce las credenciales utilizadas para iniciar sesi\u00f3n en mytotalconnectcomfort.com." } } }, @@ -17,8 +17,8 @@ "step": { "init": { "data": { - "away_cool_temperature": "Temperatura fria, modo fuera", - "away_heat_temperature": "Temperatura del calor exterior" + "away_cool_temperature": "Temperatura en modo fr\u00edo cuando ausente", + "away_heat_temperature": "Temperatura en modo calor cuando ausente" }, "description": "Opciones de configuraci\u00f3n adicionales de Honeywell. Las temperaturas se establecen en Fahrenheit." } diff --git a/homeassistant/components/huawei_lte/translations/es.json b/homeassistant/components/huawei_lte/translations/es.json index 485d2e16f69..4d1ffba4608 100644 --- a/homeassistant/components/huawei_lte/translations/es.json +++ b/homeassistant/components/huawei_lte/translations/es.json @@ -32,8 +32,8 @@ "data": { "name": "Nombre del servicio de notificaci\u00f3n (el cambio requiere reiniciar)", "recipient": "Destinatarios de notificaciones por SMS", - "track_wired_clients": "Seguir clientes de red cableados", - "unauthenticated_mode": "Modo no autenticado (el cambio requiere recarga)" + "track_wired_clients": "Rastrear clientes de red cableados", + "unauthenticated_mode": "Modo no autenticado (el cambio requiere recargar)" } } } diff --git a/homeassistant/components/hue/translations/es.json b/homeassistant/components/hue/translations/es.json index 6b72df25f65..db5a72c7fe2 100644 --- a/homeassistant/components/hue/translations/es.json +++ b/homeassistant/components/hue/translations/es.json @@ -53,15 +53,15 @@ }, "trigger_type": { "double_short_release": "Ambos \"{subtype}\" soltados", - "initial_press": "Bot\u00f3n \"{subtype}\" pulsado inicialmente", - "long_release": "Bot\u00f3n \"{subtype}\" liberado tras una pulsaci\u00f3n larga", + "initial_press": "Bot\u00f3n \"{subtype}\" presionado inicialmente", + "long_release": "Bot\u00f3n \"{subtype}\" soltado tras una pulsaci\u00f3n larga", "remote_button_long_release": "Bot\u00f3n \"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", "remote_button_short_release": "Bot\u00f3n \"{subtype}\" soltado", "remote_double_button_long_press": "Ambos \"{subtype}\" soltados despu\u00e9s de pulsaci\u00f3n larga", "remote_double_button_short_press": "Ambos \"{subtype}\" soltados", - "repeat": "Bot\u00f3n \"{subtype}\" pulsado", - "short_release": "Bot\u00f3n \"{subtype}\" liberado tras una breve pulsaci\u00f3n" + "repeat": "Bot\u00f3n \"{subtype}\" presionado", + "short_release": "Bot\u00f3n \"{subtype}\" soltado tras una breve pulsaci\u00f3n" } }, "options": { @@ -71,7 +71,7 @@ "allow_hue_groups": "Permitir grupos de Hue", "allow_hue_scenes": "Permitir escenas Hue", "allow_unreachable": "Permitir que las bombillas inalcanzables informen su estado correctamente", - "ignore_availability": "Ignorar el estado de conectividad de los dispositivos dados" + "ignore_availability": "Ignorar el estado de conectividad de los siguientes dispositivos" } } } diff --git a/homeassistant/components/humidifier/translations/es.json b/homeassistant/components/humidifier/translations/es.json index 8506445b25c..944361ef35d 100644 --- a/homeassistant/components/humidifier/translations/es.json +++ b/homeassistant/components/humidifier/translations/es.json @@ -13,7 +13,7 @@ "is_on": "{entity_name} est\u00e1 activado" }, "trigger_type": { - "changed_states": "{entity_name} activado o desactivado", + "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", "target_humidity_changed": "La humedad objetivo ha cambiado en {entity_name}", "turned_off": "{entity_name} desactivado", "turned_on": "{entity_name} activado" diff --git a/homeassistant/components/hunterdouglas_powerview/translations/es.json b/homeassistant/components/hunterdouglas_powerview/translations/es.json index a5cf5303000..924edd5394f 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/es.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/es.json @@ -7,7 +7,7 @@ "cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo", "unknown": "Error inesperado" }, - "flow_title": "{name} ( {host} )", + "flow_title": "{name} ({host})", "step": { "link": { "description": "\u00bfQuieres configurar {name} ({host})?", diff --git a/homeassistant/components/hyperion/translations/es.json b/homeassistant/components/hyperion/translations/es.json index d96584ee532..45e41859c81 100644 --- a/homeassistant/components/hyperion/translations/es.json +++ b/homeassistant/components/hyperion/translations/es.json @@ -23,7 +23,7 @@ "description": "Configurar autorizaci\u00f3n a tu servidor Hyperion Ambilight" }, "confirm": { - "description": "\u00bfQuieres a\u00f1adir el siguiente Hyperion Ambilight en Home Assistant?\n\n**Host:** {host}\n**Puerto:** {port}\n**Identificaci\u00f3n**: {id}", + "description": "\u00bfQuieres a\u00f1adir el siguiente Ambilight de Hyperion a Home Assistant?\n\n**Host:** {host}\n**Puerto:** {port}\n**ID**: {id}", "title": "Confirmar la adici\u00f3n del servicio Hyperion Ambilight" }, "create_token": { diff --git a/homeassistant/components/ifttt/translations/es.json b/homeassistant/components/ifttt/translations/es.json index e65f12b6295..a296af6a6d6 100644 --- a/homeassistant/components/ifttt/translations/es.json +++ b/homeassistant/components/ifttt/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "No est\u00e1 conectado a Home Assistant Cloud.", + "cloud_not_connected": "No conectado a Home Assistant Cloud.", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, diff --git a/homeassistant/components/integration/translations/es.json b/homeassistant/components/integration/translations/es.json index 7fee4f392ce..59e2dacb2c7 100644 --- a/homeassistant/components/integration/translations/es.json +++ b/homeassistant/components/integration/translations/es.json @@ -15,7 +15,7 @@ "unit_prefix": "La salida se escalar\u00e1 seg\u00fan el prefijo m\u00e9trico seleccionado.", "unit_time": "La salida se escalar\u00e1 seg\u00fan la unidad de tiempo seleccionada." }, - "description": "Cree un sensor que calcule una suma de Riemann para estimar la integral de un sensor.", + "description": "Crea un sensor que calcule una suma de Riemann para estimar la integral de un sensor.", "title": "A\u00f1adir sensor integral de suma de Riemann" } } diff --git a/homeassistant/components/intellifire/translations/es.json b/homeassistant/components/intellifire/translations/es.json index c44475be1a1..3cf2cfc6938 100644 --- a/homeassistant/components/intellifire/translations/es.json +++ b/homeassistant/components/intellifire/translations/es.json @@ -7,7 +7,7 @@ }, "error": { "api_error": "Error de inicio de sesi\u00f3n", - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "iftapi_connect": "Se ha producido un error al conectar a iftapi.net" }, "flow_title": "{serial} ({host})", @@ -19,11 +19,11 @@ } }, "dhcp_confirm": { - "description": "\u00bfQuieres configurar {host} \nSerie: {serial}?" + "description": "\u00bfQuieres configurar {host} \nN\u00ba serie: {serial}?" }, "manual_device_entry": { "data": { - "host": "Host (direcci\u00f3n IP)" + "host": "Host (Direcci\u00f3n IP)" }, "description": "Configuraci\u00f3n local" }, diff --git a/homeassistant/components/iotawatt/translations/es.json b/homeassistant/components/iotawatt/translations/es.json index 4a8b29a183f..4370bfbe181 100644 --- a/homeassistant/components/iotawatt/translations/es.json +++ b/homeassistant/components/iotawatt/translations/es.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "La conexi\u00f3n ha fallado", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -11,7 +11,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "El dispositivo IoTawatt requiere autenticaci\u00f3n. Introduce el nombre de usuario y la contrase\u00f1a y haz clic en el bot\u00f3n Enviar." + "description": "El dispositivo IoTawatt requiere autenticaci\u00f3n. Por favor, introduce el nombre de usuario y la contrase\u00f1a y haz clic en el bot\u00f3n Enviar." }, "user": { "data": { diff --git a/homeassistant/components/iss/translations/es.json b/homeassistant/components/iss/translations/es.json index 02a03ee5995..aa50ad5ac0f 100644 --- a/homeassistant/components/iss/translations/es.json +++ b/homeassistant/components/iss/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "latitude_longitude_not_defined": "La latitud y la longitud no est\u00e1n definidas en Home Assistant.", - "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "step": { "user": { diff --git a/homeassistant/components/justnimbus/translations/id.json b/homeassistant/components/justnimbus/translations/id.json new file mode 100644 index 00000000000..74d2a8adb7c --- /dev/null +++ b/homeassistant/components/justnimbus/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "client_id": "ID Klien" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/pl.json b/homeassistant/components/justnimbus/translations/pl.json new file mode 100644 index 00000000000..bbc8bab8392 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "client_id": "Identyfikator klienta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/es.json b/homeassistant/components/kaleidescape/translations/es.json index 5cb7047f4f5..333a44fb123 100644 --- a/homeassistant/components/kaleidescape/translations/es.json +++ b/homeassistant/components/kaleidescape/translations/es.json @@ -4,16 +4,16 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "unknown": "Error inesperado", - "unsupported": "Dispositiu no compatible" + "unsupported": "Dispositivo no compatible" }, "error": { - "cannot_connect": "Fallo en la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "unsupported": "Dispositivo no compatible" }, "flow_title": "{model} ({name})", "step": { "discovery_confirm": { - "description": "\u00bfQuieres configurar el reproductor {name} modelo {model}?" + "description": "\u00bfQuieres configurar el reproductor {model} llamado {name}?" }, "user": { "data": { diff --git a/homeassistant/components/keenetic_ndms2/translations/es.json b/homeassistant/components/keenetic_ndms2/translations/es.json index 190caf9947b..84e39aed3c5 100644 --- a/homeassistant/components/keenetic_ndms2/translations/es.json +++ b/homeassistant/components/keenetic_ndms2/translations/es.json @@ -3,19 +3,19 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "no_udn": "La informaci\u00f3n de descubrimiento SSDP no tiene UDN", - "not_keenetic_ndms2": "El art\u00edculo descubierto no es un router Keenetic" + "not_keenetic_ndms2": "El elemento descubierto no es un router Keenetic" }, "error": { - "cannot_connect": "Fallo de conexi\u00f3n" + "cannot_connect": "No se pudo conectar" }, - "flow_title": "{name} ( {host} )", + "flow_title": "{name} ({host})", "step": { "user": { "data": { "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Configurar el router Keenetic NDMS2" } @@ -26,11 +26,11 @@ "user": { "data": { "consider_home": "Considerar el intervalo en casa", - "include_arp": "Usar datos ARP (ignorado si se usan datos de hotspot)", - "include_associated": "Utilizar los datos de las asociaciones WiFi AP (se ignora si se utilizan los datos del hotspot)", - "interfaces": "Elija las interfaces para escanear", + "include_arp": "Usar datos ARP (se ignora si se usan datos de puntos de acceso)", + "include_associated": "Usar datos de asociaciones de puntos de acceso WiFi (se ignoran si se usan datos de puntos de acceso)", + "interfaces": "Elige las interfaces para escanear", "scan_interval": "Intervalo de escaneo", - "try_hotspot": "Utilizar datos de 'punto de acceso ip' (m\u00e1s precisos)" + "try_hotspot": "Usar los datos de 'ip hotspot' (m\u00e1s precisos)" } } } diff --git a/homeassistant/components/kmtronic/translations/es.json b/homeassistant/components/kmtronic/translations/es.json index 822a37649fd..f8df877d1b5 100644 --- a/homeassistant/components/kmtronic/translations/es.json +++ b/homeassistant/components/kmtronic/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fallo al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -13,7 +13,7 @@ "data": { "host": "Host", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } @@ -22,7 +22,7 @@ "step": { "init": { "data": { - "reverse": "L\u00f3gica de conmutaci\u00f3n inversa (utilizar NC)" + "reverse": "L\u00f3gica de interruptor inverso (usar NC)" } } } diff --git a/homeassistant/components/knx/translations/es.json b/homeassistant/components/knx/translations/es.json index 627c26ab72a..cad83301d83 100644 --- a/homeassistant/components/knx/translations/es.json +++ b/homeassistant/components/knx/translations/es.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "file_not_found": "El archivo `.knxkeys` especificado no se encontr\u00f3 en la ruta config/.storage/knx/", "invalid_individual_address": "El valor no coincide con el patr\u00f3n de la direcci\u00f3n KNX individual. 'area.line.device'", "invalid_ip_address": "Direcci\u00f3n IPv4 no v\u00e1lida.", @@ -24,20 +24,20 @@ "local_ip": "D\u00e9jalo en blanco para utilizar el descubrimiento autom\u00e1tico.", "port": "Puerto del dispositivo de tunelizaci\u00f3n KNX/IP." }, - "description": "Introduzca la informaci\u00f3n de conexi\u00f3n de su dispositivo de tunelizaci\u00f3n." + "description": "Por favor, introduce la informaci\u00f3n de conexi\u00f3n de tu dispositivo de t\u00fanel." }, "routing": { "data": { "individual_address": "Direcci\u00f3n individual", "local_ip": "IP local de Home Assistant", - "multicast_group": "El grupo de multidifusi\u00f3n utilizado para el enrutamiento", - "multicast_port": "El puerto de multidifusi\u00f3n utilizado para el enrutamiento" + "multicast_group": "Grupo multicast", + "multicast_port": "Puerto multicast" }, "data_description": { "individual_address": "Direcci\u00f3n KNX que usar\u00e1 Home Assistant, por ejemplo, `0.0.4`", "local_ip": "D\u00e9jalo en blanco para usar el descubrimiento autom\u00e1tico." }, - "description": "Por favor, configure las opciones de enrutamiento." + "description": "Por favor, configura las opciones de enrutamiento." }, "secure_knxkeys": { "data": { @@ -74,13 +74,13 @@ "data": { "gateway": "Conexi\u00f3n de t\u00fanel KNX" }, - "description": "Seleccione una puerta de enlace de la lista." + "description": "Selecciona una puerta de enlace de la lista." }, "type": { "data": { "connection_type": "Tipo de conexi\u00f3n KNX" }, - "description": "Por favor, introduzca el tipo de conexi\u00f3n que debemos utilizar para su conexi\u00f3n KNX. \n AUTOM\u00c1TICO - La integraci\u00f3n se encarga de la conectividad a su bus KNX realizando una exploraci\u00f3n de la pasarela. \n TUNNELING - La integraci\u00f3n se conectar\u00e1 a su bus KNX mediante tunneling. \n ROUTING - La integraci\u00f3n se conectar\u00e1 a su bus KNX mediante routing." + "description": "Por favor, introduce el tipo de conexi\u00f3n que debemos usar para tu conexi\u00f3n KNX.\n AUTOM\u00c1TICO: la integraci\u00f3n se encarga de la conectividad con tu bus KNX mediante la realizaci\u00f3n de un escaneo de la puerta de enlace.\n T\u00daNELES: la integraci\u00f3n se conectar\u00e1 a tu bus KNX a trav\u00e9s de t\u00faneles.\n ENRUTAMIENTO: la integraci\u00f3n se conectar\u00e1 a tu bus KNX a trav\u00e9s del enrutamiento." } } }, @@ -91,8 +91,8 @@ "connection_type": "Tipo de conexi\u00f3n KNX", "individual_address": "Direcci\u00f3n individual predeterminada", "local_ip": "IP local de Home Assistant", - "multicast_group": "Grupo multidifusi\u00f3n", - "multicast_port": "Puerto multidifusi\u00f3n", + "multicast_group": "Grupo multicast", + "multicast_port": "Puerto multicast", "rate_limit": "Frecuencia m\u00e1xima", "state_updater": "Actualizador de estado" }, diff --git a/homeassistant/components/kodi/translations/es.json b/homeassistant/components/kodi/translations/es.json index b19d09a92d0..290d7aa53ee 100644 --- a/homeassistant/components/kodi/translations/es.json +++ b/homeassistant/components/kodi/translations/es.json @@ -22,7 +22,7 @@ "description": "Por favor, introduzca su nombre de usuario y contrase\u00f1a de Kodi. Estos se pueden encontrar en Sistema/Configuraci\u00f3n/Red/Servicios." }, "discovery_confirm": { - "description": "\u00bfQuieres agregar Kodi (`{name}`) a Home Assistant?", + "description": "\u00bfQuieres a\u00f1adir Kodi (`{name}`) a Home Assistant?", "title": "Descubierto Kodi" }, "user": { diff --git a/homeassistant/components/kraken/translations/es.json b/homeassistant/components/kraken/translations/es.json index 86df8397c15..2a473d78080 100644 --- a/homeassistant/components/kraken/translations/es.json +++ b/homeassistant/components/kraken/translations/es.json @@ -5,10 +5,6 @@ }, "step": { "user": { - "data": { - "one": "", - "other": "Otros" - }, "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" } } diff --git a/homeassistant/components/launch_library/translations/es.json b/homeassistant/components/launch_library/translations/es.json index c9dc0a00b92..0ca93e34e5e 100644 --- a/homeassistant/components/launch_library/translations/es.json +++ b/homeassistant/components/launch_library/translations/es.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "\u00bfDesea configurar la biblioteca de lanzamiento?" + "description": "\u00bfQuieres configurar Launch Library?" } } } diff --git a/homeassistant/components/lcn/translations/es.json b/homeassistant/components/lcn/translations/es.json index 045f87a1927..a5d0be12c8e 100644 --- a/homeassistant/components/lcn/translations/es.json +++ b/homeassistant/components/lcn/translations/es.json @@ -4,7 +4,7 @@ "codelock": "c\u00f3digo de bloqueo de c\u00f3digo recibido", "fingerprint": "c\u00f3digo de huella dactilar recibido", "send_keys": "enviar claves recibidas", - "transmitter": "c\u00f3digo de transmisor recibido", + "transmitter": "c\u00f3digo del transmisor recibido", "transponder": "c\u00f3digo de transpondedor recibido" } } diff --git a/homeassistant/components/life360/translations/es.json b/homeassistant/components/life360/translations/es.json index b8495b5e916..4645b9c88b9 100644 --- a/homeassistant/components/life360/translations/es.json +++ b/homeassistant/components/life360/translations/es.json @@ -28,7 +28,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Para configurar las opciones avanzadas, revisa la [documentaci\u00f3n de Life360]({docs_url}).\nDeber\u00edas hacerlo antes de a\u00f1adir alguna cuenta.", + "description": "Para configurar las opciones avanzadas, consulta la [documentaci\u00f3n de Life360]({docs_url}).\nEs posible que quieras hacerlo antes de a\u00f1adir cuentas.", "title": "Configurar la cuenta de Life360" } } diff --git a/homeassistant/components/light/translations/es.json b/homeassistant/components/light/translations/es.json index 53cf50215aa..94e28719702 100644 --- a/homeassistant/components/light/translations/es.json +++ b/homeassistant/components/light/translations/es.json @@ -13,7 +13,7 @@ "is_on": "{entity_name} est\u00e1 encendida" }, "trigger_type": { - "changed_states": "{entity_name} activado o desactivado", + "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", "turned_off": "{entity_name} apagada", "turned_on": "{entity_name} encendida" } diff --git a/homeassistant/components/litejet/translations/es.json b/homeassistant/components/litejet/translations/es.json index 41875da9e69..95e552560ec 100644 --- a/homeassistant/components/litejet/translations/es.json +++ b/homeassistant/components/litejet/translations/es.json @@ -11,8 +11,8 @@ "data": { "port": "Puerto" }, - "description": "Conecte el puerto RS232-2 del LiteJet a su computadora e ingrese la ruta al dispositivo del puerto serial. \n\nEl LiteJet MCP debe configurarse para 19,2 K baudios, 8 bits de datos, 1 bit de parada, sin paridad y para transmitir un 'CR' despu\u00e9s de cada respuesta.", - "title": "Conectarse a LiteJet" + "description": "Conecta el puerto RS232-2 del LiteJet a tu ordenador e introduce la ruta al dispositivo del puerto serie. \n\nEl LiteJet MCP debe configurarse para 19,2 K baudios, 8 bits de datos, 1 bit de parada, sin paridad y para transmitir un 'CR' despu\u00e9s de cada respuesta.", + "title": "Conectar a LiteJet" } } }, diff --git a/homeassistant/components/litterrobot/translations/es.json b/homeassistant/components/litterrobot/translations/es.json index 12a48f17c32..f92417d76a0 100644 --- a/homeassistant/components/litterrobot/translations/es.json +++ b/homeassistant/components/litterrobot/translations/es.json @@ -1,18 +1,18 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" + "already_configured": "La cuenta ya est\u00e1 configurada" }, "error": { - "cannot_connect": "Fallo al conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/media_player/translations/es.json b/homeassistant/components/media_player/translations/es.json index ca2a633eb21..0924f212179 100644 --- a/homeassistant/components/media_player/translations/es.json +++ b/homeassistant/components/media_player/translations/es.json @@ -10,7 +10,7 @@ }, "trigger_type": { "buffering": "{entity_name} comienza a almacenar en b\u00fafer", - "changed_states": "{entity_name} ha cambiado de estado", + "changed_states": "{entity_name} cambi\u00f3 de estado", "idle": "{entity_name} est\u00e1 inactivo", "paused": "{entity_name} est\u00e1 en pausa", "playing": "{entity_name} comienza a reproducirse", diff --git a/homeassistant/components/met_eireann/translations/es.json b/homeassistant/components/met_eireann/translations/es.json index 97b6518862c..5448f05a9bb 100644 --- a/homeassistant/components/met_eireann/translations/es.json +++ b/homeassistant/components/met_eireann/translations/es.json @@ -11,7 +11,7 @@ "longitude": "Longitud", "name": "Nombre" }, - "description": "Introduce tu ubicaci\u00f3n para utilizar los datos meteorol\u00f3gicos de la API p\u00fablica de previsi\u00f3n meteorol\u00f3gica de Met \u00c9ireann", + "description": "Introduce tu ubicaci\u00f3n para utilizar los datos meteorol\u00f3gicos de la API del pron\u00f3stico meteorol\u00f3gico p\u00fablico de Met \u00c9ireann", "title": "Ubicaci\u00f3n" } } diff --git a/homeassistant/components/mill/translations/es.json b/homeassistant/components/mill/translations/es.json index 280d4ad4ba9..8b56b8be89d 100644 --- a/homeassistant/components/mill/translations/es.json +++ b/homeassistant/components/mill/translations/es.json @@ -21,9 +21,9 @@ }, "user": { "data": { - "connection_type": "Seleccione el tipo de conexi\u00f3n" + "connection_type": "Selecciona el tipo de conexi\u00f3n" }, - "description": "Seleccione el tipo de conexi\u00f3n. Local requiere calentadores de generaci\u00f3n 3" + "description": "Selecciona el tipo de conexi\u00f3n. Local requiere calentadores de generaci\u00f3n 3" } } } diff --git a/homeassistant/components/min_max/translations/es.json b/homeassistant/components/min_max/translations/es.json index 2be7203d0d4..149f3f030d3 100644 --- a/homeassistant/components/min_max/translations/es.json +++ b/homeassistant/components/min_max/translations/es.json @@ -11,7 +11,7 @@ "data_description": { "round_digits": "Controla el n\u00famero de d\u00edgitos decimales en la salida cuando la caracter\u00edstica estad\u00edstica es media o mediana." }, - "description": "Cree un sensor que calcule un valor m\u00ednimo, m\u00e1ximo, medio o mediano a partir de una lista de sensores de entrada.", + "description": "Crea un sensor que calcula el valor m\u00ednimo, m\u00e1ximo, medio o mediano a partir de una lista de sensores de entrada.", "title": "A\u00f1adir sensor m\u00edn / m\u00e1x / media / mediana" } } diff --git a/homeassistant/components/mjpeg/translations/es.json b/homeassistant/components/mjpeg/translations/es.json index 113193e3832..72644dad83a 100644 --- a/homeassistant/components/mjpeg/translations/es.json +++ b/homeassistant/components/mjpeg/translations/es.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya se encuentra configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Error al conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "user": { @@ -13,18 +13,18 @@ "mjpeg_url": "URL MJPEG", "name": "Nombre", "password": "Contrase\u00f1a", - "still_image_url": "URL de imagen est\u00e1tica", - "username": "Usuario", - "verify_ssl": "Verifique el certificado SSL" + "still_image_url": "URL de imagen fija", + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL" } } } }, "options": { "error": { - "already_configured": "El dispositivo ya se encuentra configurado", - "cannot_connect": "Error al conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "init": { @@ -32,9 +32,9 @@ "mjpeg_url": "URL MJPEG", "name": "Nombre", "password": "Contrase\u00f1a", - "still_image_url": "URL de imagen est\u00e1tica", - "username": "Nombre de Usuario", - "verify_ssl": "Verifique el certificado SSL" + "still_image_url": "URL de imagen fija", + "username": "Nombre de usuario", + "verify_ssl": "Verificar el certificado SSL" } } } diff --git a/homeassistant/components/modem_callerid/translations/es.json b/homeassistant/components/modem_callerid/translations/es.json index bc1b20cbbe0..c9d551f2d7e 100644 --- a/homeassistant/components/modem_callerid/translations/es.json +++ b/homeassistant/components/modem_callerid/translations/es.json @@ -10,14 +10,14 @@ }, "step": { "usb_confirm": { - "description": "Se trata de una integraci\u00f3n para llamadas a tel\u00e9fonos fijos que utilizan un m\u00f3dem de voz CX93001. Puede recuperar la informaci\u00f3n del identificador de llamadas con una opci\u00f3n para rechazar una llamada entrante." + "description": "Esta es una integraci\u00f3n para llamadas de l\u00ednea fija usando un m\u00f3dem de voz CX93001. Esto puede recuperar informaci\u00f3n de identificaci\u00f3n de llamadas con una opci\u00f3n para rechazar una llamada entrante." }, "user": { "data": { "name": "Nombre", "port": "Puerto" }, - "description": "Se trata de una integraci\u00f3n para llamadas a tel\u00e9fonos fijos que utilizan un m\u00f3dem de voz CX93001. Puede recuperar la informaci\u00f3n del identificador de llamadas con una opci\u00f3n para rechazar una llamada entrante." + "description": "Esta es una integraci\u00f3n para llamadas de l\u00ednea fija usando un m\u00f3dem de voz CX93001. Esto puede recuperar informaci\u00f3n de identificaci\u00f3n de llamadas con una opci\u00f3n para rechazar una llamada entrante." } } } diff --git a/homeassistant/components/modern_forms/translations/es.json b/homeassistant/components/modern_forms/translations/es.json index 29b51c03cf1..3b6e1148e13 100644 --- a/homeassistant/components/modern_forms/translations/es.json +++ b/homeassistant/components/modern_forms/translations/es.json @@ -16,8 +16,8 @@ "description": "Configura tu ventilador de Modern Forms para que se integre con Home Assistant." }, "zeroconf_confirm": { - "description": "\u00bfQuieres a\u00f1adir el ventilador de Modern Forms llamado `{name}` a Home Assistant?", - "title": "Dispositivo de ventilador de Modern Forms descubierto" + "description": "\u00bfQuieres a\u00f1adir el ventilador de Modern Forms `{name}` a Home Assistant?", + "title": "Dispositivo ventilador de Modern Forms descubierto" } } } diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/es.json b/homeassistant/components/moehlenhoff_alpha2/translations/es.json index 12821ae906d..5a62e2517d2 100644 --- a/homeassistant/components/moehlenhoff_alpha2/translations/es.json +++ b/homeassistant/components/moehlenhoff_alpha2/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/moon/translations/es.json b/homeassistant/components/moon/translations/es.json index d23683ceb5d..4bd878ba9f3 100644 --- a/homeassistant/components/moon/translations/es.json +++ b/homeassistant/components/moon/translations/es.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Ya configurado. Solo una configuraci\u00f3n es posible." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "step": { "user": { - "description": "\u00bfQuieres empezar la configuraci\u00f3n?" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" } } }, diff --git a/homeassistant/components/motion_blinds/translations/es.json b/homeassistant/components/motion_blinds/translations/es.json index 11e8c2a2e71..e316f882712 100644 --- a/homeassistant/components/motion_blinds/translations/es.json +++ b/homeassistant/components/motion_blinds/translations/es.json @@ -35,7 +35,7 @@ "step": { "init": { "data": { - "wait_for_push": "Espere a que se realice la actualizaci\u00f3n de multidifusi\u00f3n" + "wait_for_push": "Esperar a que se active la actualizaci\u00f3n de multicast" } } } diff --git a/homeassistant/components/motioneye/translations/es.json b/homeassistant/components/motioneye/translations/es.json index 881972cf5c4..d7dc7f1a083 100644 --- a/homeassistant/components/motioneye/translations/es.json +++ b/homeassistant/components/motioneye/translations/es.json @@ -12,15 +12,15 @@ }, "step": { "hassio_confirm": { - "description": "\u00bfQuieres configurar Home Assistant para que se conecte al servicio motionEye proporcionado por el complemento: {addon}?", + "description": "\u00bfQuieres configurar Home Assistant para conectarse al servicio motionEye proporcionado por el complemento: {addon}?", "title": "motionEye a trav\u00e9s del complemento Home Assistant" }, "user": { "data": { - "admin_password": "Contrase\u00f1a administrador", + "admin_password": "Contrase\u00f1a de administrador", "admin_username": "Nombre de usuario administrador", - "surveillance_password": "Contrase\u00f1a vigilancia", - "surveillance_username": "Nombre de usuario vigilancia", + "surveillance_password": "Contrase\u00f1a de vigilancia", + "surveillance_username": "Nombre de usuario de vigilancia", "url": "URL" } } @@ -31,7 +31,7 @@ "init": { "data": { "stream_url_template": "Plantilla de URL de transmisi\u00f3n", - "webhook_set": "Configure los webhooks de motionEye para informar eventos a Home Assistant", + "webhook_set": "Configura los webhooks de motionEye para informar eventos a Home Assistant", "webhook_set_overwrite": "Sobrescribir webhooks no reconocidos" } } diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index 93fe4b53933..915af215a26 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -22,7 +22,7 @@ "data": { "discovery": "Habilitar descubrimiento" }, - "description": "\u00bfDesea configurar Home Assistant para conectar con el br\u00f3ker de MQTT proporcionado por el complemento {addon}?", + "description": "\u00bfQuieres configurar Home Assistant para conectarse al agente MQTT proporcionado por el complemento {addon} ?", "title": "Br\u00f3ker MQTT a trav\u00e9s de complemento de Home Assistant" } } diff --git a/homeassistant/components/mullvad/translations/es.json b/homeassistant/components/mullvad/translations/es.json index f7ad856ea91..2f6395282d3 100644 --- a/homeassistant/components/mullvad/translations/es.json +++ b/homeassistant/components/mullvad/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fallo al conectar", + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/mutesync/translations/es.json b/homeassistant/components/mutesync/translations/es.json index fb32193010e..f3a4eb12460 100644 --- a/homeassistant/components/mutesync/translations/es.json +++ b/homeassistant/components/mutesync/translations/es.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "No se pudo conectar", - "invalid_auth": "Activar la autenticaci\u00f3n en las Preferencias de m\u00fctesync > Autenticaci\u00f3n", + "invalid_auth": "Habilita la autenticaci\u00f3n en Preferencias de m\u00fctesync > Autenticaci\u00f3n", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/myq/translations/es.json b/homeassistant/components/myq/translations/es.json index d8520cd2b6f..a6b81fbcbbc 100644 --- a/homeassistant/components/myq/translations/es.json +++ b/homeassistant/components/myq/translations/es.json @@ -15,7 +15,7 @@ "password": "Contrase\u00f1a" }, "description": "La contrase\u00f1a de {username} ya no es v\u00e1lida.", - "title": "Reautenticar tu cuenta MyQ" + "title": "Volver a autenticar tu cuenta MyQ" }, "user": { "data": { diff --git a/homeassistant/components/mysensors/translations/pl.json b/homeassistant/components/mysensors/translations/pl.json index 3c4bed1ee86..ef473a6aff5 100644 --- a/homeassistant/components/mysensors/translations/pl.json +++ b/homeassistant/components/mysensors/translations/pl.json @@ -14,6 +14,7 @@ "invalid_serial": "Nieprawid\u0142owy port szeregowy", "invalid_subscribe_topic": "Nieprawid\u0142owy temat \"subscribe\"", "invalid_version": "Nieprawid\u0142owa wersja MySensors", + "mqtt_required": "Integracja MQTT nie jest skonfigurowana", "not_a_number": "Prosz\u0119 wpisa\u0107 numer", "port_out_of_range": "Numer portu musi by\u0107 pomi\u0119dzy 1 a 65535", "same_topic": "Tematy \"subscribe\" i \"publish\" s\u0105 takie same", @@ -68,6 +69,14 @@ }, "description": "Konfiguracja bramki LAN" }, + "select_gateway_type": { + "description": "Wybierz bramk\u0119 do skonfigurowania.", + "menu_options": { + "gw_mqtt": "Skonfiguruj bramk\u0119 MQTT", + "gw_serial": "Skonfiguruj bramk\u0119 szeregow\u0105", + "gw_tcp": "Skonfiguruj bramk\u0119 TCP" + } + }, "user": { "data": { "gateway_type": "Typ bramki" diff --git a/homeassistant/components/nam/translations/es.json b/homeassistant/components/nam/translations/es.json index b6494327077..0ba92e457ca 100644 --- a/homeassistant/components/nam/translations/es.json +++ b/homeassistant/components/nam/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "device_unsupported": "El dispositivo no es compatible.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", - "reauth_unsuccessful": "La reautenticaci\u00f3n no se realiz\u00f3 correctamente, elimine la integraci\u00f3n y vuelva a configurarla." + "reauth_unsuccessful": "No se pudo volver a autenticar, elimina la integraci\u00f3n y vuelve a configurarla." }, "error": { "cannot_connect": "No se pudo conectar", @@ -21,20 +21,20 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Por favor, introduzca el nombre de usuario y la contrase\u00f1a." + "description": "Por favor, introduce el nombre de usuario y la contrase\u00f1a." }, "reauth_confirm": { "data": { "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Por favor, introduzca el nombre de usuario y la contrase\u00f1a correctos para el host: {host}" + "description": "Por favor, introduce el nombre de usuario y la contrase\u00f1a correctos para el host: {host}" }, "user": { "data": { "host": "Host" }, - "description": "Configurar la integraci\u00f3n de Nettigo Air Monitor." + "description": "Configurar la integraci\u00f3n Nettigo Air Monitor." } } } diff --git a/homeassistant/components/nanoleaf/translations/es.json b/homeassistant/components/nanoleaf/translations/es.json index 9899d30d36f..00ba28474ac 100644 --- a/homeassistant/components/nanoleaf/translations/es.json +++ b/homeassistant/components/nanoleaf/translations/es.json @@ -15,8 +15,8 @@ "flow_title": "{name}", "step": { "link": { - "description": "Mantenga presionado el bot\u00f3n de encendido en su Nanoleaf durante 5 segundos hasta que los LED de los botones comiencen a parpadear, luego haga clic en ** ENVIAR ** dentro de los 30 segundos.", - "title": "Link Nanoleaf" + "description": "Mant\u00e9n presionado el bot\u00f3n de encendido de tu Nanoleaf durante 5 segundos hasta que los LED del bot\u00f3n comiencen a parpadear, luego haz clic en **ENVIAR** en los siguientes 30 segundos.", + "title": "Enlazar Nanoleaf" }, "user": { "data": { diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index a04dc0baab6..36b5d64e253 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -17,20 +17,20 @@ "default": "Autenticado con \u00e9xito" }, "error": { - "bad_project_id": "Por favor, introduzca un ID de proyecto Cloud v\u00e1lido (compruebe la consola de la nube)", + "bad_project_id": "Por favor introduce un ID de proyecto en la nube v\u00e1lido (verifica la Cloud Console)", "internal_error": "Error interno validando el c\u00f3digo", "invalid_pin": "C\u00f3digo PIN no v\u00e1lido", - "subscriber_error": "Error de abonado desconocido, ver registros", + "subscriber_error": "Error de suscriptor desconocido, mira los registros", "timeout": "Tiempo de espera agotado validando el c\u00f3digo", "unknown": "Error inesperado", - "wrong_project_id": "Por favor, introduzca un ID de proyecto Cloud v\u00e1lido (encontr\u00f3 el ID de proyecto de acceso al dispositivo)" + "wrong_project_id": "Por favor introduce un ID de proyecto en la nube v\u00e1lido (era el mismo que el ID del proyecto de acceso al dispositivo)" }, "step": { "auth": { "data": { "code": "Token de acceso" }, - "description": "Para vincular tu cuenta de Google, [autoriza tu cuenta]({url}).\n\nDespu\u00e9s de la autorizaci\u00f3n, copie y pegue el c\u00f3digo Auth Token proporcionado a continuaci\u00f3n.", + "description": "Para vincular tu cuenta de Google, [autoriza tu cuenta]({url}).\n\nDespu\u00e9s de la autorizaci\u00f3n, copia y pega el c\u00f3digo Auth Token proporcionado a continuaci\u00f3n.", "title": "Vincular cuenta de Google" }, "auth_upgrade": { @@ -80,7 +80,7 @@ "data": { "cloud_project_id": "ID de proyecto de Google Cloud" }, - "description": "Visite [Cloud Console] ({url}) para encontrar su ID de proyecto de Google Cloud.", + "description": "Visita [Cloud Console]({url}) para encontrar tu ID de proyecto de Google Cloud.", "title": "Configurar Google Cloud" }, "reauth_confirm": { diff --git a/homeassistant/components/netatmo/translations/es.json b/homeassistant/components/netatmo/translations/es.json index dd9fcf18d60..c658c0046a1 100644 --- a/homeassistant/components/netatmo/translations/es.json +++ b/homeassistant/components/netatmo/translations/es.json @@ -15,16 +15,16 @@ "title": "Selecciona un m\u00e9todo de autenticaci\u00f3n" }, "reauth_confirm": { - "description": "La integraci\u00f3n de Netatmo necesita volver a autentificar su cuenta", + "description": "La integraci\u00f3n Netatmo necesita volver a autenticar tu cuenta", "title": "Volver a autenticar la integraci\u00f3n" } } }, "device_automation": { "trigger_subtype": { - "away": "fuera", - "hg": "protector contra las heladas", - "schedule": "Horario" + "away": "ausente", + "hg": "protector contra heladas", + "schedule": "programaci\u00f3n" }, "trigger_type": { "alarm_started": "{entity_name} ha detectado una alarma", @@ -35,10 +35,10 @@ "outdoor": "{entity_name} ha detectado un evento en el exterior", "person": "{entity_name} ha detectado una persona", "person_away": "{entity_name} ha detectado que una persona se ha ido", - "set_point": "Temperatura objetivo {entity_name} fijada manualmente", + "set_point": "Temperatura objetivo de {entity_name} configurada manualmente", "therm_mode": "{entity_name} cambi\u00f3 a \" {subtype} \"", - "turned_off": "{entity_name} desactivado", - "turned_on": "{entity_name} activado", + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido", "vehicle": "{entity_name} ha detectado un veh\u00edculo" } }, diff --git a/homeassistant/components/netgear/translations/es.json b/homeassistant/components/netgear/translations/es.json index 69bea9a5de6..2ad6d0e6d16 100644 --- a/homeassistant/components/netgear/translations/es.json +++ b/homeassistant/components/netgear/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "config": "Error de conexi\u00f3n o de inicio de sesi\u00f3n: compruebe su configuraci\u00f3n" + "config": "Error de conexi\u00f3n o de inicio de sesi\u00f3n: verifica tu configuraci\u00f3n" }, "step": { "user": { @@ -13,7 +13,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario (Opcional)" }, - "description": "Host predeterminado: {host} \nNombre de usuario predeterminado: {username}" + "description": "Host por defecto: {host}\nNombre de usuario por defecto: {username}" } } }, @@ -21,9 +21,9 @@ "step": { "init": { "data": { - "consider_home": "Considere el tiempo en casa (segundos)" + "consider_home": "Considerar la hora local (segundos)" }, - "description": "Especifica los ajustes opcionales" + "description": "Especificar configuraciones opcionales" } } } diff --git a/homeassistant/components/nfandroidtv/translations/es.json b/homeassistant/components/nfandroidtv/translations/es.json index e382855479f..4522f36cb12 100644 --- a/homeassistant/components/nfandroidtv/translations/es.json +++ b/homeassistant/components/nfandroidtv/translations/es.json @@ -13,7 +13,7 @@ "host": "Host", "name": "Nombre" }, - "description": "Esta integraci\u00f3n requiere la aplicaci\u00f3n de Notificaciones para Android TV.\n\nPara Android TV: https://play.google.com/store/apps/details?id=de.cyberdream.androidtv.notifications.google\nPara Fire TV: https://www.amazon.com/Christian-Fees-Notifications-for-Fire/dp/B00OESCXEK\n\nDebes configurar una reserva DHCP en su router (consulta el manual de usuario de tu router) o una direcci\u00f3n IP est\u00e1tica en el dispositivo. Si no, el dispositivo acabar\u00e1 por no estar disponible." + "description": "Por favor, consulta la documentaci\u00f3n para asegurarte de que se cumplen todos los requisitos." } } } diff --git a/homeassistant/components/nina/translations/es.json b/homeassistant/components/nina/translations/es.json index a605bfb7a9a..a4cabb669d8 100644 --- a/homeassistant/components/nina/translations/es.json +++ b/homeassistant/components/nina/translations/es.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { "cannot_connect": "No se pudo conectar", - "no_selection": "Por favor, seleccione al menos una ciudad/condado", + "no_selection": "Por favor selecciona al menos una ciudad/condado", "unknown": "Error inesperado" }, "step": { @@ -20,7 +20,7 @@ "corona_filter": "Eliminar las advertencias de Corona", "slots": "Advertencias m\u00e1ximas por ciudad/condado" }, - "title": "Seleccionar ciudad/pa\u00eds" + "title": "Seleccionar ciudad/condado" } } }, diff --git a/homeassistant/components/nmap_tracker/translations/es.json b/homeassistant/components/nmap_tracker/translations/es.json index 9b943edb310..f978f0d18ad 100644 --- a/homeassistant/components/nmap_tracker/translations/es.json +++ b/homeassistant/components/nmap_tracker/translations/es.json @@ -10,11 +10,11 @@ "user": { "data": { "exclude": "Direcciones de red (separadas por comas) para excluir del escaneo", - "home_interval": "N\u00famero m\u00ednimo de minutos entre los escaneos de los dispositivos activos (preservar la bater\u00eda)", + "home_interval": "N\u00famero m\u00ednimo de minutos entre escaneos de dispositivos activos (conservar bater\u00eda)", "hosts": "Direcciones de red a escanear (separadas por comas)", - "scan_options": "Opciones de escaneo configurables sin procesar para Nmap" + "scan_options": "Opciones de escaneo configurables sin formato para Nmap" }, - "description": "Configure los hosts que ser\u00e1n escaneados por Nmap. Las direcciones de red y los excluidos pueden ser direcciones IP (192.168.1.1), redes IP (192.168.0.0/24) o rangos IP (192.168.1.0-32)." + "description": "Configura los hosts para que sean escaneados por Nmap. La direcci\u00f3n de red y las exclusiones pueden ser direcciones IP (192.168.1.1), redes IP (192.168.0.0/24) o rangos de IP (192.168.1.0-32)." } } }, @@ -25,14 +25,14 @@ "step": { "init": { "data": { - "consider_home": "Segundos de espera hasta que se marca un dispositivo de seguimiento como no en casa despu\u00e9s de no ser visto.", + "consider_home": "Segundos de espera hasta marcar un rastreador de dispositivo como ausente despu\u00e9s de no ser visto.", "exclude": "Direcciones de red (separadas por comas) para excluir del escaneo", - "home_interval": "N\u00famero m\u00ednimo de minutos entre los escaneos de los dispositivos activos (preservar la bater\u00eda)", + "home_interval": "N\u00famero m\u00ednimo de minutos entre escaneos de dispositivos activos (conservar bater\u00eda)", "hosts": "Direcciones de red a escanear (separadas por comas)", "interval_seconds": "Intervalo de exploraci\u00f3n", - "scan_options": "Opciones de escaneo configurables sin procesar para Nmap" + "scan_options": "Opciones de escaneo configurables sin formato para Nmap" }, - "description": "Configure los hosts que ser\u00e1n escaneados por Nmap. Las direcciones de red y los excluidos pueden ser direcciones IP (192.168.1.1), redes IP (192.168.0.0/24) o rangos IP (192.168.1.0-32)." + "description": "Configura los hosts para que sean escaneados por Nmap. La direcci\u00f3n de red y las exclusiones pueden ser direcciones IP (192.168.1.1), redes IP (192.168.0.0/24) o rangos de IP (192.168.1.0-32)." } } }, diff --git a/homeassistant/components/notion/translations/es.json b/homeassistant/components/notion/translations/es.json index 3eb17fa606d..f7a7a01f4d8 100644 --- a/homeassistant/components/notion/translations/es.json +++ b/homeassistant/components/notion/translations/es.json @@ -13,7 +13,7 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Por favor, vuelva a introducir la contrase\u00f1a de {username}.", + "description": "Por favor, vuelve a introducir la contrase\u00f1a de {username}.", "title": "Volver a autenticar la integraci\u00f3n" }, "user": { diff --git a/homeassistant/components/nuki/translations/es.json b/homeassistant/components/nuki/translations/es.json index 33fe3f462df..d21ef9dfdb6 100644 --- a/homeassistant/components/nuki/translations/es.json +++ b/homeassistant/components/nuki/translations/es.json @@ -13,7 +13,7 @@ "data": { "token": "Token de acceso" }, - "description": "La integraci\u00f3n de Nuki debe volver a autenticarse con tu bridge.", + "description": "La integraci\u00f3n Nuki debe volver a autenticarse con tu bridge.", "title": "Volver a autenticar la integraci\u00f3n" }, "user": { diff --git a/homeassistant/components/octoprint/translations/es.json b/homeassistant/components/octoprint/translations/es.json index e9135b25be8..827e29e0fde 100644 --- a/homeassistant/components/octoprint/translations/es.json +++ b/homeassistant/components/octoprint/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "auth_failed": "Error al recuperar la clave de API de la aplicaci\u00f3n", + "auth_failed": "No se pudo recuperar la clave API de la aplicaci\u00f3n", "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, @@ -12,7 +12,7 @@ }, "flow_title": "Impresora OctoPrint: {host}", "progress": { - "get_api_key": "Abra la interfaz de usuario de OctoPrint y haga clic en 'Permitir' en la solicitud de acceso para 'Home Assistant'." + "get_api_key": "Abre la interfaz de usuario de OctoPrint y haz clic en 'Permitir' en la solicitud de acceso para 'Home Assistant'." }, "step": { "user": { @@ -22,7 +22,7 @@ "port": "N\u00famero de puerto", "ssl": "Usar SSL", "username": "Nombre de usuario", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar el certificado SSL" } } } diff --git a/homeassistant/components/omnilogic/translations/es.json b/homeassistant/components/omnilogic/translations/es.json index 1755942e5df..a47b982d94f 100644 --- a/homeassistant/components/omnilogic/translations/es.json +++ b/homeassistant/components/omnilogic/translations/es.json @@ -21,7 +21,7 @@ "step": { "init": { "data": { - "ph_offset": "Desplazamiento del pH (positivo o negativo)", + "ph_offset": "Compensaci\u00f3n del pH (positivo o negativo)", "polling_interval": "Intervalo de sondeo (en segundos)" } } diff --git a/homeassistant/components/oncue/translations/es.json b/homeassistant/components/oncue/translations/es.json index 13f2eb38bef..f92417d76a0 100644 --- a/homeassistant/components/oncue/translations/es.json +++ b/homeassistant/components/oncue/translations/es.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/onewire/translations/es.json b/homeassistant/components/onewire/translations/es.json index 3617f4c8fd4..986a36a30d4 100644 --- a/homeassistant/components/onewire/translations/es.json +++ b/homeassistant/components/onewire/translations/es.json @@ -18,22 +18,22 @@ }, "options": { "error": { - "device_not_selected": "Seleccionar los dispositivos a configurar" + "device_not_selected": "Selecciona los dispositivos para configurar" }, "step": { "configure_device": { "data": { "precision": "Precisi\u00f3n del sensor" }, - "description": "Selecciona la precisi\u00f3n del sensor {sensor_id}", + "description": "Selecciona la precisi\u00f3n del sensor para {sensor_id}", "title": "Precisi\u00f3n del sensor OneWire" }, "device_selection": { "data": { - "clear_device_options": "Borra todas las configuraciones de dispositivo", - "device_selection": "Seleccionar los dispositivos a configurar" + "clear_device_options": "Borrar todas las configuraciones de dispositivo", + "device_selection": "Selecciona los dispositivos para configurar" }, - "description": "Seleccione los pasos de configuraci\u00f3n a procesar", + "description": "Selecciona qu\u00e9 pasos de configuraci\u00f3n procesar", "title": "Opciones de dispositivo OneWire" } } diff --git a/homeassistant/components/open_meteo/translations/es.json b/homeassistant/components/open_meteo/translations/es.json index 68e31e61ccd..87bc3b879be 100644 --- a/homeassistant/components/open_meteo/translations/es.json +++ b/homeassistant/components/open_meteo/translations/es.json @@ -5,7 +5,7 @@ "data": { "zone": "Zona" }, - "description": "Seleccionar la ubicaci\u00f3n que se utilizar\u00e1 para la previsi\u00f3n meteorol\u00f3gica" + "description": "Selecciona la ubicaci\u00f3n que se usar\u00e1 para el pron\u00f3stico del tiempo" } } } diff --git a/homeassistant/components/openexchangerates/translations/pl.json b/homeassistant/components/openexchangerates/translations/pl.json new file mode 100644 index 00000000000..e7de6c30cec --- /dev/null +++ b/homeassistant/components/openexchangerates/translations/pl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "timeout_connect": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "timeout_connect": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "base": "Waluta bazowa" + }, + "data_description": { + "base": "Korzystanie z innej waluty bazowej ni\u017c USD wymaga [p\u0142atnego abonamentu]({signup})." + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Open Exchange Rates przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Open Exchange Rates zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/es.json b/homeassistant/components/opentherm_gw/translations/es.json index b464549ed71..0310cdd8236 100644 --- a/homeassistant/components/opentherm_gw/translations/es.json +++ b/homeassistant/components/opentherm_gw/translations/es.json @@ -23,7 +23,7 @@ "floor_temperature": "Temperatura del suelo", "read_precision": "Leer precisi\u00f3n", "set_precision": "Establecer precisi\u00f3n", - "temporary_override_mode": "Modo de anulaci\u00f3n temporal del punto de ajuste" + "temporary_override_mode": "Modo de anulaci\u00f3n temporal de la consigna" } } } diff --git a/homeassistant/components/overkiz/translations/es.json b/homeassistant/components/overkiz/translations/es.json index ac9d3f72ebc..5503525ac58 100644 --- a/homeassistant/components/overkiz/translations/es.json +++ b/homeassistant/components/overkiz/translations/es.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente", - "reauth_wrong_account": "S\u00f3lo puedes volver a autenticar esta entrada con la misma cuenta y hub de Overkiz" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", + "reauth_wrong_account": "Solo puedes volver a autenticar esta entrada con la misma cuenta y concentrador de Overkiz" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "server_in_maintenance": "El servidor est\u00e1 inactivo por mantenimiento", + "server_in_maintenance": "El servidor est\u00e1 fuera de servicio por mantenimiento", "too_many_attempts": "Demasiados intentos con un token no v\u00e1lido, prohibido temporalmente", - "too_many_requests": "Demasiadas solicitudes, int\u00e9ntalo de nuevo m\u00e1s tarde.", + "too_many_requests": "Demasiadas solicitudes, vuelve a intentarlo m\u00e1s tarde", "unknown": "Error inesperado" }, "flow_title": "Puerta de enlace: {gateway_id}", @@ -18,11 +18,11 @@ "user": { "data": { "host": "Host", - "hub": "Hub", + "hub": "Concentrador", "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "La plataforma Overkiz es utilizada por varios proveedores como Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) y Atlantic (Cozytouch). Introduce las credenciales de tu aplicaci\u00f3n y selecciona tu hub." + "description": "La plataforma Overkiz es utilizada por varios proveedores como Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) y Atlantic (Cozytouch). Introduce las credenciales de tu aplicaci\u00f3n y selecciona tu concentrador." } } } diff --git a/homeassistant/components/overkiz/translations/sensor.es.json b/homeassistant/components/overkiz/translations/sensor.es.json index 523ddf7fe7b..794730c5c86 100644 --- a/homeassistant/components/overkiz/translations/sensor.es.json +++ b/homeassistant/components/overkiz/translations/sensor.es.json @@ -1,10 +1,10 @@ { "state": { "overkiz__battery": { - "full": "Completo", - "low": "Bajo", + "full": "Completa", + "low": "Baja", "normal": "Normal", - "verylow": "Muy bajo" + "verylow": "Muy baja" }, "overkiz__discrete_rssi_level": { "good": "Bien", @@ -30,11 +30,11 @@ "overkiz__sensor_defect": { "dead": "Muerto", "low_battery": "Bater\u00eda baja", - "maintenance_required": "Mantenimiento necesario", - "no_defect": "Ning\u00fan defecto" + "maintenance_required": "Requiere mantenimiento", + "no_defect": "Sin defectos" }, "overkiz__sensor_room": { - "clean": "Limpiar", + "clean": "Limpio", "dirty": "Sucio" }, "overkiz__three_way_handle_direction": { diff --git a/homeassistant/components/p1_monitor/translations/es.json b/homeassistant/components/p1_monitor/translations/es.json index 31976986ec4..28dcb186505 100644 --- a/homeassistant/components/p1_monitor/translations/es.json +++ b/homeassistant/components/p1_monitor/translations/es.json @@ -9,7 +9,7 @@ "host": "Host", "name": "Nombre" }, - "description": "Configurar el monitor P1 para que se integre con el asistente dom\u00e9stico." + "description": "Configura P1 Monitor para integrarlo con Home Assistant." } } } diff --git a/homeassistant/components/philips_js/translations/es.json b/homeassistant/components/philips_js/translations/es.json index 153512cb83b..a2842917b87 100644 --- a/homeassistant/components/philips_js/translations/es.json +++ b/homeassistant/components/philips_js/translations/es.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_pin": "PIN no v\u00e1lido", - "pairing_failure": "No se ha podido emparejar: {error_id}", + "pairing_failure": "No se puede emparejar: {error_id}", "unknown": "Error inesperado" }, "step": { @@ -14,7 +14,7 @@ "data": { "pin": "C\u00f3digo PIN" }, - "description": "Introduzca el PIN que se muestra en el televisor", + "description": "Introduce el PIN que se muestra en tu TV", "title": "Par" }, "user": { diff --git a/homeassistant/components/picnic/translations/es.json b/homeassistant/components/picnic/translations/es.json index 7ecfc37d97d..93024c5611c 100644 --- a/homeassistant/components/picnic/translations/es.json +++ b/homeassistant/components/picnic/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "Reauntenticaci\u00f3n exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/plugwise/translations/es.json b/homeassistant/components/plugwise/translations/es.json index 284bfe97943..1a0ae166aaa 100644 --- a/homeassistant/components/plugwise/translations/es.json +++ b/homeassistant/components/plugwise/translations/es.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Fall\u00f3 la conexi\u00f3n", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "invalid_setup": "Agregue su Adam en lugar de su Anna, consulte la documentaci\u00f3n de integraci\u00f3n de Home Assistant Plugwise para obtener m\u00e1s informaci\u00f3n", + "invalid_setup": "A\u00f1ade tu Adam en lugar de tu Anna, consulta la documentaci\u00f3n de la integraci\u00f3n Home Assistant Plugwise para m\u00e1s informaci\u00f3n", "unknown": "Error inesperado" }, "flow_title": "{name}", diff --git a/homeassistant/components/powerwall/translations/es.json b/homeassistant/components/powerwall/translations/es.json index ae578b37e50..baf6fbda838 100644 --- a/homeassistant/components/powerwall/translations/es.json +++ b/homeassistant/components/powerwall/translations/es.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "already_configured": "El powerwall ya est\u00e1 configurado", - "cannot_connect": "Fallo en la conexi\u00f3n", + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "No se pudo conectar, por favor int\u00e9ntelo de nuevo", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado", - "wrong_version": "Tu powerwall utiliza una versi\u00f3n de software que no es compatible. Considera actualizar o informar de este problema para que pueda resolverse." + "wrong_version": "Tu powerwall utiliza una versi\u00f3n de software que no es compatible. Por favor, considera actualizar o informar este problema para que pueda resolverse." }, - "flow_title": "Powerwall de Tesla ({ip_address})", + "flow_title": "{name} ({ip_address})", "step": { "confirm_discovery": { "description": "\u00bfQuieres configurar {name} ({ip_address})?", @@ -21,16 +21,16 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "La contrase\u00f1a suele ser los \u00faltimos 5 caracteres del n\u00famero de serie de Backup Gateway y se puede encontrar en la aplicaci\u00f3n de Tesla o los \u00faltimos 5 caracteres de la contrase\u00f1a que se encuentra dentro de la puerta de Backup Gateway 2.", - "title": "Reautorizar la powerwall" + "description": "La contrase\u00f1a suele ser los \u00faltimos 5 caracteres del n\u00famero de serie del Backup Gateway y se puede encontrar en la aplicaci\u00f3n Tesla o los \u00faltimos 5 caracteres de la contrase\u00f1a que se encuentran dentro de la puerta del Backup Gateway 2.", + "title": "Volver a autenticar el powerwall" }, "user": { "data": { "ip_address": "Direcci\u00f3n IP", "password": "Contrase\u00f1a" }, - "description": "La contrase\u00f1a suele ser los \u00faltimos 5 caracteres del n\u00famero de serie del Backup Gateway y se puede encontrar en la aplicaci\u00f3n Telsa; o los \u00faltimos 5 caracteres de la contrase\u00f1a que se encuentran dentro de la puerta del Backup Gateway 2.", - "title": "Conectarse al powerwall" + "description": "La contrase\u00f1a suele ser los \u00faltimos 5 caracteres del n\u00famero de serie del Backup Gateway y se puede encontrar en la aplicaci\u00f3n Tesla o los \u00faltimos 5 caracteres de la contrase\u00f1a que se encuentran dentro de la puerta del Backup Gateway 2.", + "title": "Conectar al powerwall" } } } diff --git a/homeassistant/components/prosegur/translations/es.json b/homeassistant/components/prosegur/translations/es.json index 1cbb1e25dc9..4447bbbc2e4 100644 --- a/homeassistant/components/prosegur/translations/es.json +++ b/homeassistant/components/prosegur/translations/es.json @@ -12,7 +12,7 @@ "step": { "reauth_confirm": { "data": { - "description": "Vuelva a autenticarse con su cuenta Prosegur.", + "description": "Vuelve a autenticarte con tu cuenta Prosegur.", "password": "Contrase\u00f1a", "username": "Nombre de usuario" } diff --git a/homeassistant/components/pure_energie/translations/es.json b/homeassistant/components/pure_energie/translations/es.json index 9725448be66..58d02574211 100644 --- a/homeassistant/components/pure_energie/translations/es.json +++ b/homeassistant/components/pure_energie/translations/es.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya se encuentra configurado", - "cannot_connect": "Error al conectar" + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar" }, "error": { - "cannot_connect": "Error al conectar" + "cannot_connect": "No se pudo conectar" }, "flow_title": "{model} ({host})", "step": { @@ -15,8 +15,8 @@ } }, "zeroconf_confirm": { - "description": "\u00bfQuieres a\u00f1adir el Medidor Pure Energie (`{name}`) a Home Assistant?", - "title": "Medidor Pure Energie encontrado" + "description": "\u00bfQuieres a\u00f1adir el medidor Pure Energie (`{model}`) a Home Assistant?", + "title": "Dispositivo medidor Pure Energie descubierto" } } } diff --git a/homeassistant/components/pvoutput/translations/es.json b/homeassistant/components/pvoutput/translations/es.json index 6ddf1dc9cfa..188a7e8d293 100644 --- a/homeassistant/components/pvoutput/translations/es.json +++ b/homeassistant/components/pvoutput/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", @@ -12,14 +12,14 @@ "data": { "api_key": "Clave API" }, - "description": "Para volver a autenticarse con PVOutput, deber\u00e1 obtener la clave API en {account_url} ." + "description": "Para volver a autenticarte con PVOutput, deber\u00e1s obtener la clave API en {account_url}." }, "user": { "data": { "api_key": "Clave API", "system_id": "ID del sistema" }, - "description": "Para autenticarse con PVOutput, deber\u00e1 obtener la clave API en {account_url} . \n\n Los ID de los sistemas registrados se enumeran en esa misma p\u00e1gina." + "description": "Para autenticarte con PVOutput, deber\u00e1s obtener la clave API en {account_url}. \n\nLos ID de sistema de los sistemas registrados se enumeran en esa misma p\u00e1gina." } } } diff --git a/homeassistant/components/radio_browser/translations/es.json b/homeassistant/components/radio_browser/translations/es.json index aa8e6336550..1a797d16af3 100644 --- a/homeassistant/components/radio_browser/translations/es.json +++ b/homeassistant/components/radio_browser/translations/es.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Ya est\u00e1 configurado. Solamente una configuraci\u00f3n es posible." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "step": { "user": { - "description": "\u00bfQuieres a\u00f1adir el navegador radio a Home Assistant?" + "description": "\u00bfQuieres a\u00f1adir Radio Browser a Home Assistant?" } } } diff --git a/homeassistant/components/rainforest_eagle/translations/es.json b/homeassistant/components/rainforest_eagle/translations/es.json index da6a6fd6936..d8501c80c89 100644 --- a/homeassistant/components/rainforest_eagle/translations/es.json +++ b/homeassistant/components/rainforest_eagle/translations/es.json @@ -13,7 +13,7 @@ "data": { "cloud_id": "ID de Cloud", "host": "Host", - "install_code": "Codigo de instalacion" + "install_code": "C\u00f3digo de instalaci\u00f3n" } } } diff --git a/homeassistant/components/remote/translations/es.json b/homeassistant/components/remote/translations/es.json index 31e68384b8b..b2c8aea25cc 100644 --- a/homeassistant/components/remote/translations/es.json +++ b/homeassistant/components/remote/translations/es.json @@ -10,7 +10,7 @@ "is_on": "{entity_name} est\u00e1 activado" }, "trigger_type": { - "changed_states": "{entity_name} activado o desactivado", + "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", "turned_off": "{entity_name} desactivado", "turned_on": "{entity_name} activado" } diff --git a/homeassistant/components/ridwell/translations/es.json b/homeassistant/components/ridwell/translations/es.json index 74171cb98f0..07037e942cf 100644 --- a/homeassistant/components/ridwell/translations/es.json +++ b/homeassistant/components/ridwell/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", @@ -13,7 +13,7 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Por favor, vuelva a introducir la contrase\u00f1a de: {username}", + "description": "Por favor, vuelve a introducir la contrase\u00f1a de {username}:", "title": "Volver a autenticar la integraci\u00f3n" }, "user": { @@ -21,7 +21,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Introduzca su nombre de usuario y contrase\u00f1a:" + "description": "Introduce tu nombre de usuario y contrase\u00f1a:" } } } diff --git a/homeassistant/components/rituals_perfume_genie/translations/es.json b/homeassistant/components/rituals_perfume_genie/translations/es.json index bdb1933eaf7..6be6c9d1e04 100644 --- a/homeassistant/components/rituals_perfume_genie/translations/es.json +++ b/homeassistant/components/rituals_perfume_genie/translations/es.json @@ -11,10 +11,10 @@ "step": { "user": { "data": { - "email": "email", + "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a" }, - "title": "Con\u00e9ctese a su cuenta de Rituals" + "title": "Con\u00e9ctate a tu cuenta Rituals" } } } diff --git a/homeassistant/components/rtsp_to_webrtc/translations/es.json b/homeassistant/components/rtsp_to_webrtc/translations/es.json index c74f0b6b34d..5a066d6645e 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/es.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/es.json @@ -1,25 +1,25 @@ { "config": { "abort": { - "server_failure": "El servidor RTSPtoWebRTC devolvi\u00f3 un error. Consulte los registros para obtener m\u00e1s informaci\u00f3n.", - "server_unreachable": "No se puede comunicar con el servidor RTSPtoWebRTC. Consulte los registros para obtener m\u00e1s informaci\u00f3n.", - "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n." + "server_failure": "El servidor RTSPtoWebRTC devolvi\u00f3 un error. Consulta los registros para obtener m\u00e1s informaci\u00f3n.", + "server_unreachable": "No se puede comunicar con el servidor RTSPtoWebRTC. Consulta los registros para obtener m\u00e1s informaci\u00f3n.", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { - "invalid_url": "Debe ser una URL de servidor RTSPtoWebRTC v\u00e1lida, por ejemplo, https://ejemplo.com", - "server_failure": "El servidor RTSPtoWebRTC devolvi\u00f3 un error. Consulte los registros para obtener m\u00e1s informaci\u00f3n.", - "server_unreachable": "No se puede comunicar con el servidor RTSPtoWebRTC. Consulte los registros para obtener m\u00e1s informaci\u00f3n." + "invalid_url": "Debe ser una URL de servidor RTSPtoWebRTC v\u00e1lida, por ejemplo, https://example.com", + "server_failure": "El servidor RTSPtoWebRTC devolvi\u00f3 un error. Consulta los registros para obtener m\u00e1s informaci\u00f3n.", + "server_unreachable": "No se puede comunicar con el servidor RTSPtoWebRTC. Consulta los registros para obtener m\u00e1s informaci\u00f3n." }, "step": { "hassio_confirm": { - "description": "\u00bfDesea configurar Home Assistant para conectarse al servidor RTSPtoWebRTC proporcionado por el complemento: {complemento}?", + "description": "\u00bfQuieres configurar Home Assistant para conectarse al servidor RTSPtoWebRTC proporcionado por el complemento: {addon}?", "title": "RTSPtoWebRTC a trav\u00e9s del complemento Home Assistant" }, "user": { "data": { - "server_url": "URL del servidor RTSPtoWebRTC, por ejemplo, https://ejemplo.com" + "server_url": "URL del servidor RTSPtoWebRTC, por ejemplo, https://example.com" }, - "description": "La integraci\u00f3n RTSPtoWebRTC requiere un servidor para traducir las transmisiones RTSP a WebRTC. Ingrese la URL del servidor RTSPtoWebRTC.", + "description": "La integraci\u00f3n RTSPtoWebRTC requiere un servidor para traducir flujos RTSP a WebRTC. Introduce la URL del servidor RTSPtoWebRTC.", "title": "Configurar RTSPtoWebRTC" } } diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json index ebcaa398949..70bd9f30b9b 100644 --- a/homeassistant/components/samsungtv/translations/es.json +++ b/homeassistant/components/samsungtv/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este televisor Samsung. Revisa la configuraci\u00f3n de tu televisor para autorizar a Home Assistant.", + "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este TV Samsung. Verifica la configuraci\u00f3n del Administrador de dispositivos externos de tu TV para autorizar a Home Assistant.", "cannot_connect": "No se pudo conectar", "id_missing": "Este dispositivo Samsung no tiene un n\u00famero de serie.", "not_supported": "Esta televisi\u00f3n Samsung actualmente no es compatible.", @@ -11,8 +11,8 @@ "unknown": "Error inesperado" }, "error": { - "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este televisor Samsung. Revisa la configuraci\u00f3n de tu televisor para autorizar a Home Assistant.", - "invalid_pin": "El PIN es inv\u00e1lido; vuelve a intentarlo." + "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este TV Samsung. Verifica la configuraci\u00f3n del Administrador de dispositivos externos de tu TV para autorizar a Home Assistant.", + "invalid_pin": "El PIN no es v\u00e1lido, por favor, int\u00e9ntalo de nuevo." }, "flow_title": "{device}", "step": { @@ -20,16 +20,16 @@ "description": "\u00bfQuieres configurar {device}? Si nunca la has conectado a Home Assistant antes deber\u00edas ver una ventana en tu TV pidiendo autorizaci\u00f3n." }, "encrypted_pairing": { - "description": "Introduce el PIN que se muestra en {device}." + "description": "Por favor, introduce el PIN que aparece en {device}." }, "pairing": { - "description": "\u00bfQuiere configurar {device}? Si nunca conect\u00f3 Home Assistant antes, deber\u00eda ver una ventana emergente en su televisor solicitando autorizaci\u00f3n." + "description": "\u00bfQuieres configurar {device}? Si nunca la has conectado a Home Assistant antes deber\u00edas ver una ventana en tu TV pidiendo autorizaci\u00f3n." }, "reauth_confirm": { - "description": "Despu\u00e9s de enviarlo, acepte la ventana emergente en {device} solicitando autorizaci\u00f3n dentro de los 30 segundos." + "description": "Despu\u00e9s de enviar, acepta la ventana emergente en {device} solicitando autorizaci\u00f3n dentro de los 30 segundos o introduce el PIN." }, "reauth_confirm_encrypted": { - "description": "Introduce el PIN que se muestra en {device}." + "description": "Por favor, introduce el PIN que aparece en {device}." }, "user": { "data": { diff --git a/homeassistant/components/schedule/translations/ca.json b/homeassistant/components/schedule/translations/ca.json new file mode 100644 index 00000000000..323bb583601 --- /dev/null +++ b/homeassistant/components/schedule/translations/ca.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "OFF", + "on": "ON" + } + }, + "title": "Horari" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/de.json b/homeassistant/components/schedule/translations/de.json new file mode 100644 index 00000000000..33e07d1c2ca --- /dev/null +++ b/homeassistant/components/schedule/translations/de.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Aus", + "on": "An" + } + }, + "title": "Zeitplan" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/es.json b/homeassistant/components/schedule/translations/es.json new file mode 100644 index 00000000000..b41c3ed8281 --- /dev/null +++ b/homeassistant/components/schedule/translations/es.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Apagado", + "on": "Encendido" + } + }, + "title": "Programaci\u00f3n" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/et.json b/homeassistant/components/schedule/translations/et.json new file mode 100644 index 00000000000..de5b11e6af7 --- /dev/null +++ b/homeassistant/components/schedule/translations/et.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "V\u00e4ljas", + "on": "Sees" + } + }, + "title": "Ajakava" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/id.json b/homeassistant/components/schedule/translations/id.json new file mode 100644 index 00000000000..f179781c3f0 --- /dev/null +++ b/homeassistant/components/schedule/translations/id.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Mati", + "on": "Nyala" + } + }, + "title": "Jadwal" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/pl.json b/homeassistant/components/schedule/translations/pl.json new file mode 100644 index 00000000000..9f02b8a0c51 --- /dev/null +++ b/homeassistant/components/schedule/translations/pl.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "wy\u0142.", + "on": "w\u0142." + } + }, + "title": "Harmonogram" +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/translations/es.json b/homeassistant/components/screenlogic/translations/es.json index c890d3bf10c..c5309c928db 100644 --- a/homeassistant/components/screenlogic/translations/es.json +++ b/homeassistant/components/screenlogic/translations/es.json @@ -6,21 +6,21 @@ "error": { "cannot_connect": "No se pudo conectar" }, - "flow_title": "ScreenLogic {name}", + "flow_title": "{name}", "step": { "gateway_entry": { "data": { "ip_address": "Direcci\u00f3n IP", "port": "Puerto" }, - "description": "Introduzca la informaci\u00f3n de su ScreenLogic Gateway.", + "description": "Introduce la informaci\u00f3n de tu puerta de enlace ScreenLogic.", "title": "ScreenLogic" }, "gateway_select": { "data": { "selected_gateway": "Puerta de enlace" }, - "description": "Se han descubierto las siguientes puertas de enlace ScreenLogic. Seleccione una para configurarla o elija configurar manualmente una puerta de enlace ScreenLogic.", + "description": "Se han descubierto las siguientes puertas de enlace ScreenLogic. Selecciona una para configurarla o elige configurar manualmente una puerta de enlace ScreenLogic.", "title": "ScreenLogic" } } @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "scan_interval": "Segundos entre exploraciones" + "scan_interval": "Segundos entre escaneos" }, "description": "Especificar la configuraci\u00f3n de {gateway_name}", "title": "ScreenLogic" diff --git a/homeassistant/components/select/translations/es.json b/homeassistant/components/select/translations/es.json index b6db30f0711..b2066b4606d 100644 --- a/homeassistant/components/select/translations/es.json +++ b/homeassistant/components/select/translations/es.json @@ -1,13 +1,13 @@ { "device_automation": { "action_type": { - "select_option": "Cambiar la opci\u00f3n {entity_name}" + "select_option": "Cambiar opci\u00f3n de {entity_name}" }, "condition_type": { - "selected_option": "Opci\u00f3n actual {entity_name} seleccionada" + "selected_option": "Opci\u00f3n de {entity_name} seleccionada actualmente" }, "trigger_type": { - "current_option_changed": "Opci\u00f3n {entity_name} cambiada" + "current_option_changed": "Opci\u00f3n de {entity_name} cambiada" } }, "title": "Seleccionar" diff --git a/homeassistant/components/sense/translations/es.json b/homeassistant/components/sense/translations/es.json index 3593b08f17c..6790c0841e3 100644 --- a/homeassistant/components/sense/translations/es.json +++ b/homeassistant/components/sense/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", @@ -14,14 +14,14 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "La integraci\u00f3n Sense debe volver a autenticar la cuenta {email}.", - "title": "Reautenticar la integraci\u00f3n" + "description": "La integraci\u00f3n Sense necesita volver a autenticar tu cuenta {email}.", + "title": "Volver a autenticar la integraci\u00f3n" }, "user": { "data": { "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a", - "timeout": "Timeout" + "timeout": "Tiempo de espera" }, "title": "Conectar a tu Sense Energy Monitor" }, @@ -29,7 +29,7 @@ "data": { "code": "C\u00f3digo de verificaci\u00f3n" }, - "title": "Detecci\u00f3n de autenticaci\u00f3n multifactor" + "title": "Autenticaci\u00f3n multifactor de Sense" } } } diff --git a/homeassistant/components/senseme/translations/es.json b/homeassistant/components/senseme/translations/es.json index a10e4422a82..33349bbbc69 100644 --- a/homeassistant/components/senseme/translations/es.json +++ b/homeassistant/components/senseme/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "cannot_connect": "Fallo en la conexi\u00f3n" + "cannot_connect": "No se pudo conectar" }, "error": { "cannot_connect": "No se pudo conectar", @@ -17,13 +17,13 @@ "data": { "host": "Host" }, - "description": "Introduzca una direcci\u00f3n IP." + "description": "Introduce una direcci\u00f3n IP." }, "user": { "data": { "device": "Dispositivo" }, - "description": "Seleccione un dispositivo o elija \"Direcci\u00f3n IP\" para introducir manualmente una direcci\u00f3n IP." + "description": "Selecciona un dispositivo o elige 'Direcci\u00f3n IP' para introducir manualmente una direcci\u00f3n IP." } } } diff --git a/homeassistant/components/sensibo/translations/es.json b/homeassistant/components/sensibo/translations/es.json index 43d9acc2645..beb6ed575a2 100644 --- a/homeassistant/components/sensibo/translations/es.json +++ b/homeassistant/components/sensibo/translations/es.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", - "incorrect_api_key": "Clave API inv\u00e1lida para la cuenta seleccionada", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", - "no_devices": "No se ha descubierto ning\u00fan dispositivo", + "incorrect_api_key": "Clave API no v\u00e1lida para la cuenta seleccionada", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "no_devices": "No se han descubierto dispositivos", "no_username": "No se pudo obtener el nombre de usuario" }, "step": { diff --git a/homeassistant/components/sensor/translations/es.json b/homeassistant/components/sensor/translations/es.json index 4190680944b..a99b59dceae 100644 --- a/homeassistant/components/sensor/translations/es.json +++ b/homeassistant/components/sensor/translations/es.json @@ -3,11 +3,11 @@ "condition_type": { "is_apparent_power": "Potencia aparente actual de {entity_name}", "is_battery_level": "Nivel de bater\u00eda actual de {entity_name}", - "is_carbon_dioxide": "Nivel actual de concentraci\u00f3n de di\u00f3xido de carbono {entity_name}", - "is_carbon_monoxide": "Nivel actual de concentraci\u00f3n de mon\u00f3xido de carbono {entity_name}", + "is_carbon_dioxide": "Nivel actual en {entity_name} de concentraci\u00f3n de di\u00f3xido de carbono", + "is_carbon_monoxide": "Nivel actual en {entity_name} de concentraci\u00f3n de mon\u00f3xido de carbono", "is_current": "Corriente actual de {entity_name}", "is_energy": "Energ\u00eda actual de {entity_name}", - "is_frequency": "Frecuencia actual de {entity_name}", + "is_frequency": "Frecuencia de {entity_name} actual", "is_gas": "Gas actual de {entity_name}", "is_humidity": "Humedad actual de {entity_name}", "is_illuminance": "Luminosidad actual de {entity_name}", @@ -30,32 +30,32 @@ "is_voltage": "Voltaje actual de {entity_name}" }, "trigger_type": { - "apparent_power": "Cambios de potencia aparente de {entity_name}", + "apparent_power": "{entity_name} ha cambiado de potencia aparente", "battery_level": "Cambios de nivel de bater\u00eda de {entity_name}", "carbon_dioxide": "{entity_name} cambios en la concentraci\u00f3n de di\u00f3xido de carbono", "carbon_monoxide": "{entity_name} cambios en la concentraci\u00f3n de mon\u00f3xido de carbono", "current": "Cambio de corriente en {entity_name}", "energy": "Cambio de energ\u00eda en {entity_name}", - "frequency": "Cambios de frecuencia de {entity_name}", - "gas": "Cambio de gas de {entity_name}", + "frequency": "{entity_name} ha cambiado de frecuencia", + "gas": "{entity_name} ha hambiado gas", "humidity": "Cambios de humedad de {entity_name}", "illuminance": "Cambios de luminosidad de {entity_name}", - "nitrogen_dioxide": "Cambios en la concentraci\u00f3n de di\u00f3xido de nitr\u00f3geno de {entity_name}", - "nitrogen_monoxide": "Cambios en la concentraci\u00f3n de mon\u00f3xido de nitr\u00f3geno de {entity_name}", - "nitrous_oxide": "Cambios en la concentraci\u00f3n de \u00f3xido nitroso de {entity_name}", - "ozone": "Cambios en la concentraci\u00f3n de ozono de {entity_name}", - "pm1": "Cambios en la concentraci\u00f3n de PM1 de {entity_name}", - "pm10": "Cambios en la concentraci\u00f3n de PM10 de {entity_name}", - "pm25": "Cambios en la concentraci\u00f3n de PM2.5 de {entity_name}", + "nitrogen_dioxide": "{entity_name} ha cambiado en la concentraci\u00f3n de di\u00f3xido de nitr\u00f3geno", + "nitrogen_monoxide": "{entity_name} ha cambiado en la concentraci\u00f3n de mon\u00f3xido de nitr\u00f3geno", + "nitrous_oxide": "{entity_name} ha cambiado en la concentraci\u00f3n de \u00f3xido nitroso", + "ozone": "{entity_name} ha cambiado en la concentraci\u00f3n de ozono", + "pm1": "{entity_name} ha cambiado en la concentraci\u00f3n de PM1", + "pm10": "{entity_name} ha cambiado en la concentraci\u00f3n de PM10", + "pm25": "{entity_name} ha cambiado en la concentraci\u00f3n de PM2.5", "power": "Cambios de potencia de {entity_name}", "power_factor": "Cambio de factor de potencia en {entity_name}", "pressure": "Cambios de presi\u00f3n de {entity_name}", - "reactive_power": "Cambios de potencia reactiva de {entity_name}", + "reactive_power": "{entity_name} ha cambiado de potencia reactiva", "signal_strength": "cambios de la intensidad de se\u00f1al de {entity_name}", - "sulphur_dioxide": "Cambios en la concentraci\u00f3n de di\u00f3xido de azufre de {entity_name}", + "sulphur_dioxide": "{entity_name} ha cambiado en la concentraci\u00f3n de di\u00f3xido de azufre", "temperature": "{entity_name} cambios de temperatura", "value": "Cambios de valor de la {entity_name}", - "volatile_organic_compounds": "Cambios en la concentraci\u00f3n de compuestos org\u00e1nicos vol\u00e1tiles de {entity_name}", + "volatile_organic_compounds": "{entity_name} ha cambiado la concentraci\u00f3n de compuestos org\u00e1nicos vol\u00e1tiles", "voltage": "Cambio de voltaje en {entity_name}" } }, diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index 876d64093a0..85394365c1a 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/translations/es.json @@ -13,7 +13,7 @@ "flow_title": "Shelly: {name}", "step": { "confirm_discovery": { - "description": "\u00bfQuieres configurar el {model} en {host}?\n\nAntes de configurarlo, el dispositivo que funciona con pilas debe ser despertado pulsando el bot\u00f3n situado en el dispositivo." + "description": "\u00bfQuieres configurar {model} en {host}? \n\nLos dispositivos alimentados por bater\u00eda que est\u00e1n protegidos con contrase\u00f1a deben despertarse antes de continuar con la configuraci\u00f3n.\nLos dispositivos que funcionan con bater\u00eda que no est\u00e1n protegidos con contrase\u00f1a se agregar\u00e1n cuando el dispositivo se despierte, puedes activar manualmente el dispositivo ahora con un bot\u00f3n o esperar la pr\u00f3xima actualizaci\u00f3n de datos del dispositivo." }, "credentials": { "data": { @@ -43,7 +43,7 @@ "double": "Pulsaci\u00f3n doble de {subtype}", "double_push": "Pulsaci\u00f3n doble de {subtype}", "long": "Pulsaci\u00f3n larga de {subtype}", - "long_push": "{subtype} pulsado durante un rato", + "long_push": "Pulsaci\u00f3n larga de {subtype}", "long_single": "Pulsaci\u00f3n larga de {subtype} seguida de una pulsaci\u00f3n simple", "single": "Pulsaci\u00f3n simple de {subtype}", "single_long": "Pulsaci\u00f3n simple de {subtype} seguida de una pulsaci\u00f3n larga", diff --git a/homeassistant/components/sia/translations/es.json b/homeassistant/components/sia/translations/es.json index 8b8b7e97f1f..60ad0a84f92 100644 --- a/homeassistant/components/sia/translations/es.json +++ b/homeassistant/components/sia/translations/es.json @@ -1,10 +1,10 @@ { "config": { "error": { - "invalid_account_format": "La cuenta no es un valor hexadecimal, por favor utilice s\u00f3lo 0-9 y A-F.", + "invalid_account_format": "La cuenta no es un valor hexadecimal, por favor, utiliza solo 0-9 y A-F.", "invalid_account_length": "La cuenta no tiene la longitud adecuada, tiene que tener entre 3 y 16 caracteres.", - "invalid_key_format": "La clave no es un valor hexadecimal, por favor utilice s\u00f3lo 0-9 y A-F.", - "invalid_key_length": "La clave no tiene la longitud correcta, tiene que ser de 16, 24 o 32 caracteres hexadecimales.", + "invalid_key_format": "La clave no es un valor hexadecimal, por favor, utiliza solo 0-9 y A-F.", + "invalid_key_length": "La clave no tiene la longitud correcta, debe tener 16, 24 o 32 caracteres hexadecimales.", "invalid_ping": "El intervalo de ping debe estar entre 1 y 1440 minutos.", "invalid_zones": "Tiene que haber al menos 1 zona.", "unknown": "Error inesperado" @@ -14,23 +14,23 @@ "data": { "account": "ID de la cuenta", "additional_account": "Cuentas adicionales", - "encryption_key": "Clave de encriptaci\u00f3n", + "encryption_key": "Clave de cifrado", "ping_interval": "Intervalo de ping (min)", "zones": "N\u00famero de zonas de la cuenta" }, - "title": "Agrega otra cuenta al puerto actual." + "title": "A\u00f1adir otra cuenta al puerto actual." }, "user": { "data": { "account": "ID de la cuenta", "additional_account": "Cuentas adicionales", - "encryption_key": "Clave de encriptaci\u00f3n", + "encryption_key": "Clave de cifrado", "ping_interval": "Intervalo de ping (min)", "port": "Puerto", "protocol": "Protocolo", "zones": "N\u00famero de zonas de la cuenta" }, - "title": "Cree una conexi\u00f3n para sistemas de alarma basados en SIA." + "title": "Crear una conexi\u00f3n para sistemas de alarma basados en SIA." } } }, @@ -38,10 +38,10 @@ "step": { "options": { "data": { - "ignore_timestamps": "Ignore la verificaci\u00f3n de la marca de tiempo de los eventos SIA", + "ignore_timestamps": "Ignorar la verificaci\u00f3n de marca de tiempo de los eventos SIA", "zones": "N\u00famero de zonas de la cuenta" }, - "description": "Configure las opciones para la cuenta: {account}", + "description": "Configura las opciones para la cuenta: {account}", "title": "Opciones para la configuraci\u00f3n de SIA." } } diff --git a/homeassistant/components/simplepush/translations/pl.json b/homeassistant/components/simplepush/translations/pl.json index 10f83b3401c..a3269fda96c 100644 --- a/homeassistant/components/simplepush/translations/pl.json +++ b/homeassistant/components/simplepush/translations/pl.json @@ -22,6 +22,10 @@ "deprecated_yaml": { "description": "Konfiguracja Simplepush przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", "title": "Konfiguracja YAML dla Simplepush zostanie usuni\u0119ta" + }, + "removed_yaml": { + "description": "Konfiguracja Simplepush za pomoc\u0105 YAML zosta\u0142a usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML nie jest u\u017cywana przez Home Assistant. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistant, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Simplepush zosta\u0142a usuni\u0119ta" } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/es.json b/homeassistant/components/simplisafe/translations/es.json index db73c6ce042..b47b3870d46 100644 --- a/homeassistant/components/simplisafe/translations/es.json +++ b/homeassistant/components/simplisafe/translations/es.json @@ -35,7 +35,7 @@ "password": "Contrase\u00f1a", "username": "Correo electr\u00f3nico" }, - "description": "SimpliSafe autentica a los usuarios a trav\u00e9s de su aplicaci\u00f3n web. Debido a limitaciones t\u00e9cnicas, existe un paso manual al final de este proceso; aseg\u00farate de leer la [documentaci\u00f3n](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de comenzar. \n\nCuando est\u00e9s listo, haz clic [aqu\u00ed]({url}) para abrir la aplicaci\u00f3n web SimpliSafe e introduce tus credenciales. Si ya iniciaste sesi\u00f3n en SimpliSafe en tu navegador, es posible que desees abrir una nueva pesta\u00f1a y luego copiar/pegar la URL anterior en esa pesta\u00f1a. \n\n Cuando se complete el proceso, regresa aqu\u00ed e introduce el c\u00f3digo de autorizaci\u00f3n de la URL `com.simplisafe.mobile`." + "description": "SimpliSafe autentica a los usuarios a trav\u00e9s de su aplicaci\u00f3n web. Debido a limitaciones t\u00e9cnicas, existe un paso manual al final de este proceso; aseg\u00farate de leer la [documentaci\u00f3n](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de comenzar. \n\nCuando est\u00e9s listo, haz clic [aqu\u00ed]({url}) para abrir la aplicaci\u00f3n web SimpliSafe e introduce tus credenciales. Si ya iniciaste sesi\u00f3n en SimpliSafe en tu navegador, es posible que desees abrir una nueva pesta\u00f1a y luego copiar/pegar la URL anterior en esa pesta\u00f1a. \n\nCuando se complete el proceso, regresa aqu\u00ed e introduce el c\u00f3digo de autorizaci\u00f3n de la URL `com.simplisafe.mobile`." } } }, diff --git a/homeassistant/components/simplisafe/translations/pl.json b/homeassistant/components/simplisafe/translations/pl.json index 04f6e8dd44b..c3eb5433359 100644 --- a/homeassistant/components/simplisafe/translations/pl.json +++ b/homeassistant/components/simplisafe/translations/pl.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane", "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_auth_code_length": "Kody autoryzacji SimpliSafe maj\u0105 d\u0142ugo\u015b\u0107 45 znak\u00f3w", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "progress": { @@ -34,7 +35,7 @@ "password": "Has\u0142o", "username": "Nazwa u\u017cytkownika" }, - "description": "SimpliSafe uwierzytelnia u\u017cytkownik\u00f3w za po\u015brednictwem swojej aplikacji internetowej. Ze wzgl\u0119du na ograniczenia techniczne na ko\u0144cu tego procesu znajduje si\u0119 r\u0119czny krok; upewnij si\u0119, \u017ce przeczyta\u0142e\u015b [dokumentacj\u0119](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) przed rozpocz\u0119ciem. \n\nGdy b\u0119dziesz ju\u017c gotowy, kliknij [tutaj]({url}), aby otworzy\u0107 aplikacj\u0119 internetow\u0105 SimpliSafe i wprowadzi\u0107 swoje dane uwierzytelniaj\u0105ce. Po zako\u0144czeniu procesu wr\u00f3\u0107 tutaj i wprowad\u017a kod autoryzacji z adresu URL aplikacji internetowej SimpliSafe." + "description": "SimpliSafe uwierzytelnia u\u017cytkownik\u00f3w za po\u015brednictwem swojej aplikacji internetowej. Ze wzgl\u0119du na ograniczenia techniczne na ko\u0144cu tego procesu znajduje si\u0119 r\u0119czny krok; upewnij si\u0119, \u017ce przeczyta\u0142e\u015b [dokumentacj\u0119](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) przed rozpocz\u0119ciem. \n\nGdy b\u0119dziesz ju\u017c gotowy, kliknij [tutaj]({url}), aby otworzy\u0107 aplikacj\u0119 internetow\u0105 SimpliSafe i wprowadzi\u0107 swoje dane uwierzytelniaj\u0105ce. Je\u015bli ju\u017c zalogowa\u0142e\u015b si\u0119 do Simplisafe w swojej przegl\u0105darce, mo\u017cesz otworzy\u0107 now\u0105 kart\u0119, a nast\u0119pnie skopiowa\u0107/wklei\u0107 powy\u017cszy adres URL.\n\nPo zako\u0144czeniu procesu wr\u00f3\u0107 tutaj i wprowad\u017a kod autoryzacji z adresu URL 'com.simplisafe.mobile'." } } }, diff --git a/homeassistant/components/sleepiq/translations/es.json b/homeassistant/components/sleepiq/translations/es.json index 033941b21d2..cdea85e7360 100644 --- a/homeassistant/components/sleepiq/translations/es.json +++ b/homeassistant/components/sleepiq/translations/es.json @@ -2,24 +2,24 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Error al conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida" + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "reauth_confirm": { "data": { "password": "Contrase\u00f1a" }, - "description": "La integraci\u00f3n de SleepIQ necesita volver a autenticar su cuenta {username} .", - "title": "Reautenticaci\u00f3n de la integraci\u00f3n" + "description": "La integraci\u00f3n SleepIQ necesita volver a autenticar tu cuenta {username}.", + "title": "Volver a autenticar la integraci\u00f3n" }, "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/smappee/translations/es.json b/homeassistant/components/smappee/translations/es.json index 891e9642d53..808eeffbbcd 100644 --- a/homeassistant/components/smappee/translations/es.json +++ b/homeassistant/components/smappee/translations/es.json @@ -27,7 +27,7 @@ "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" }, "zeroconf_confirm": { - "description": "\u00bfDesea agregar el dispositivo Smappee con el n\u00famero de serie `{serialnumber}` a Home Assistant?", + "description": "\u00bfQuieres a\u00f1adir el dispositivo Smappee con n\u00famero de serie `{serialnumber}` a Home Assistant?", "title": "Dispositivo Smappee descubierto" } } diff --git a/homeassistant/components/smarttub/translations/es.json b/homeassistant/components/smarttub/translations/es.json index 8f2eb153cb7..fa2b9d91e9b 100644 --- a/homeassistant/components/smarttub/translations/es.json +++ b/homeassistant/components/smarttub/translations/es.json @@ -17,7 +17,7 @@ "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a" }, - "description": "Introduzca su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a de SmartTub para iniciar sesi\u00f3n", + "description": "Introduce tu direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a de SmartTub para iniciar sesi\u00f3n", "title": "Inicio de sesi\u00f3n" } } diff --git a/homeassistant/components/solax/translations/es.json b/homeassistant/components/solax/translations/es.json index da4765b897d..c8a47a84150 100644 --- a/homeassistant/components/solax/translations/es.json +++ b/homeassistant/components/solax/translations/es.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/steamist/translations/es.json b/homeassistant/components/steamist/translations/es.json index 95592668ad8..d937ed49112 100644 --- a/homeassistant/components/steamist/translations/es.json +++ b/homeassistant/components/steamist/translations/es.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n se encuentra en progreso", - "cannot_connect": "Error en la conexi\u00f3n", - "no_devices_found": "No se han encontrado dispositivos en la red", - "not_steamist_device": "No es un dispositivo de vapor" + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "cannot_connect": "No se pudo conectar", + "no_devices_found": "No se encontraron dispositivos en la red", + "not_steamist_device": "No es un dispositivo de Steamist" }, "error": { "cannot_connect": "No se pudo conectar", @@ -25,7 +25,7 @@ "data": { "host": "Host" }, - "description": "Si deja el host vac\u00edo, la detecci\u00f3n se utilizar\u00e1 para buscar dispositivos." + "description": "Si dejas el host vac\u00edo, se usar\u00e1 el descubrimiento para encontrar dispositivos." } } } diff --git a/homeassistant/components/subaru/translations/es.json b/homeassistant/components/subaru/translations/es.json index fe02d508241..be4121cf747 100644 --- a/homeassistant/components/subaru/translations/es.json +++ b/homeassistant/components/subaru/translations/es.json @@ -18,7 +18,7 @@ "data": { "pin": "PIN" }, - "description": "Por favor, introduzca su PIN de MySubaru\nNOTA: Todos los veh\u00edculos de la cuenta deben tener el mismo PIN", + "description": "Por favor, introduce tu PIN de MySubaru\nNOTA: Todos los veh\u00edculos en la cuenta deben tener el mismo PIN", "title": "Configuraci\u00f3n de Subaru Starlink" }, "two_factor": { @@ -41,7 +41,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Por favor, introduzca sus credenciales de MySubaru\nNOTA: La configuraci\u00f3n inicial puede tardar hasta 30 segundos", + "description": "Por favor, introduce tus credenciales de MySubaru\nNOTA: La configuraci\u00f3n inicial puede tardar hasta 30 segundos", "title": "Configuraci\u00f3n de Subaru Starlink" } } @@ -52,7 +52,7 @@ "data": { "update_enabled": "Habilitar el sondeo de veh\u00edculos" }, - "description": "Cuando est\u00e1 habilitado, el sondeo de veh\u00edculos enviar\u00e1 un comando remoto a su veh\u00edculo cada 2 horas para obtener nuevos datos del sensor. Sin sondeo del veh\u00edculo, los nuevos datos del sensor solo se reciben cuando el veh\u00edculo env\u00eda datos autom\u00e1ticamente (normalmente despu\u00e9s de apagar el motor).", + "description": "Cuando est\u00e1 habilitado, el sondeo de veh\u00edculos enviar\u00e1 un comando remoto a tu veh\u00edculo cada 2 horas para obtener nuevos datos del sensor. Sin sondeo del veh\u00edculo, los nuevos datos del sensor solo se reciben cuando el veh\u00edculo env\u00eda datos autom\u00e1ticamente (normalmente despu\u00e9s de apagar el motor).", "title": "Opciones de Subaru Starlink" } } diff --git a/homeassistant/components/sun/translations/es.json b/homeassistant/components/sun/translations/es.json index 68db12462b1..9b30ae8a012 100644 --- a/homeassistant/components/sun/translations/es.json +++ b/homeassistant/components/sun/translations/es.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Ya configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "step": { "user": { - "description": "\u00bfQuiere empezar a configurar?" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" } } }, diff --git a/homeassistant/components/switch/translations/es.json b/homeassistant/components/switch/translations/es.json index e6190f32c8b..a605ceb238b 100644 --- a/homeassistant/components/switch/translations/es.json +++ b/homeassistant/components/switch/translations/es.json @@ -10,7 +10,7 @@ "is_on": "{entity_name} est\u00e1 encendida" }, "trigger_type": { - "changed_states": "{entity_name} activado o desactivado", + "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", "turned_off": "{entity_name} apagado", "turned_on": "{entity_name} encendido" } diff --git a/homeassistant/components/switch_as_x/translations/es.json b/homeassistant/components/switch_as_x/translations/es.json index 0228d86d0fe..eeeae5ede55 100644 --- a/homeassistant/components/switch_as_x/translations/es.json +++ b/homeassistant/components/switch_as_x/translations/es.json @@ -10,5 +10,5 @@ } } }, - "title": "Cambia el tipo de dispositivo de un conmutador" + "title": "Cambiar el tipo de dispositivo de un interruptor" } \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/de.json b/homeassistant/components/switchbot/translations/de.json index ca306d6fa2d..2b4fd5ba8cf 100644 --- a/homeassistant/components/switchbot/translations/de.json +++ b/homeassistant/components/switchbot/translations/de.json @@ -9,6 +9,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "password": { + "data": { + "password": "Passwort" + }, + "description": "F\u00fcr das Ger\u00e4t {name} ist ein Kennwort erforderlich." + }, "user": { "data": { "address": "Ger\u00e4teadresse", diff --git a/homeassistant/components/switchbot/translations/es.json b/homeassistant/components/switchbot/translations/es.json index d0f908df646..a4cca573f7d 100644 --- a/homeassistant/components/switchbot/translations/es.json +++ b/homeassistant/components/switchbot/translations/es.json @@ -35,7 +35,7 @@ "data": { "retry_count": "Recuento de reintentos", "retry_timeout": "Tiempo de espera entre reintentos", - "scan_timeout": "Cu\u00e1nto tiempo se debe buscar datos de anuncio", + "scan_timeout": "Cu\u00e1nto tiempo escanear en busca de datos de anuncio", "update_time": "Tiempo entre actualizaciones (segundos)" } } diff --git a/homeassistant/components/switchbot/translations/et.json b/homeassistant/components/switchbot/translations/et.json index a46ec376a96..0f5998dee99 100644 --- a/homeassistant/components/switchbot/translations/et.json +++ b/homeassistant/components/switchbot/translations/et.json @@ -9,6 +9,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "Kas seadistada {name} ?" + }, + "password": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Seade {name} n\u00f5uab salas\u00f5na" + }, "user": { "data": { "address": "Seadme aadress", diff --git a/homeassistant/components/switchbot/translations/id.json b/homeassistant/components/switchbot/translations/id.json index f7baed8c8db..d9d4ade13b1 100644 --- a/homeassistant/components/switchbot/translations/id.json +++ b/homeassistant/components/switchbot/translations/id.json @@ -9,6 +9,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "password": { + "data": { + "password": "Kata Sandi" + }, + "description": "Perangkat {name} memerlukan kata sandi" + }, "user": { "data": { "address": "Alamat perangkat", diff --git a/homeassistant/components/switchbot/translations/no.json b/homeassistant/components/switchbot/translations/no.json index 6c8d877551f..53767627d2d 100644 --- a/homeassistant/components/switchbot/translations/no.json +++ b/homeassistant/components/switchbot/translations/no.json @@ -9,6 +9,15 @@ }, "flow_title": "{name} ( {address} )", "step": { + "confirm": { + "description": "Vil du konfigurere {name}?" + }, + "password": { + "data": { + "password": "Passord" + }, + "description": "{name} -enheten krever et passord" + }, "user": { "data": { "address": "Enhetsadresse", diff --git a/homeassistant/components/switchbot/translations/pl.json b/homeassistant/components/switchbot/translations/pl.json index dc43a7f610d..5dbc87c07af 100644 --- a/homeassistant/components/switchbot/translations/pl.json +++ b/homeassistant/components/switchbot/translations/pl.json @@ -15,6 +15,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "password": { + "data": { + "password": "Has\u0142o" + }, + "description": "Urz\u0105dzenie {name} wymaga has\u0142a" + }, "user": { "data": { "address": "Adres urz\u0105dzenia", diff --git a/homeassistant/components/switchbot/translations/zh-Hant.json b/homeassistant/components/switchbot/translations/zh-Hant.json index 6d14e05aff7..082ad32f84c 100644 --- a/homeassistant/components/switchbot/translations/zh-Hant.json +++ b/homeassistant/components/switchbot/translations/zh-Hant.json @@ -9,6 +9,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "password": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u88dd\u7f6e {name} \u9700\u8981\u8f38\u5165\u5bc6\u78bc" + }, "user": { "data": { "address": "\u88dd\u7f6e\u4f4d\u5740", diff --git a/homeassistant/components/syncthing/translations/es.json b/homeassistant/components/syncthing/translations/es.json index dd156f7edb4..34e3a391e86 100644 --- a/homeassistant/components/syncthing/translations/es.json +++ b/homeassistant/components/syncthing/translations/es.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "title": "Configurar integraci\u00f3n de Syncthing", + "title": "Configurar la integraci\u00f3n Syncthing", "token": "Token", "url": "URL", "verify_ssl": "Verificar el certificado SSL" diff --git a/homeassistant/components/synology_dsm/translations/es.json b/homeassistant/components/synology_dsm/translations/es.json index d7f1f16197c..41d3d369115 100644 --- a/homeassistant/components/synology_dsm/translations/es.json +++ b/homeassistant/components/synology_dsm/translations/es.json @@ -54,7 +54,7 @@ "init": { "data": { "scan_interval": "Minutos entre escaneos", - "snap_profile_type": "Calidad de las fotos de la c\u00e1mara (0:alta, 1:media, 2:baja)", + "snap_profile_type": "Nivel de calidad de las instant\u00e1neas de la c\u00e1mara (0:alta 1:media 2:baja)", "timeout": "Tiempo de espera (segundos)" } } diff --git a/homeassistant/components/system_bridge/translations/es.json b/homeassistant/components/system_bridge/translations/es.json index d7fba5e7c32..b6df8d6f57c 100644 --- a/homeassistant/components/system_bridge/translations/es.json +++ b/homeassistant/components/system_bridge/translations/es.json @@ -16,7 +16,7 @@ "data": { "api_key": "Clave API" }, - "description": "Escribe la clave API que estableciste en la configuraci\u00f3n para {name}." + "description": "Por favor, escribe la clave API que estableciste en la configuraci\u00f3n para {name}." }, "user": { "data": { diff --git a/homeassistant/components/tailscale/translations/es.json b/homeassistant/components/tailscale/translations/es.json index 5e87412da4f..62f803025e8 100644 --- a/homeassistant/components/tailscale/translations/es.json +++ b/homeassistant/components/tailscale/translations/es.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { @@ -12,14 +12,14 @@ "data": { "api_key": "Clave API" }, - "description": "Los tokens de la API de Tailscale son v\u00e1lidos durante 90 d\u00edas. Puedes crear una nueva clave de API de Tailscale en https://login.tailscale.com/admin/settings/authkeys." + "description": "Los tokens de la API Tailscale son v\u00e1lidos durante 90 d\u00edas. Puedes crear una nueva clave API de Tailscale en https://login.tailscale.com/admin/settings/authkeys." }, "user": { "data": { "api_key": "Clave API", "tailnet": "Tailnet" }, - "description": "Para autenticarse con Tailscale, deber\u00e1 crear una clave API en https://login.tailscale.com/admin/settings/authkeys. \n\nTailnet es el nombre de su red Tailscale. Puede encontrarlo en la esquina superior izquierda en el Panel de administraci\u00f3n de Tailscale (al lado del logotipo de Tailscale)." + "description": "Esta integraci\u00f3n supervisa tu red Tailscale, **NO** hace que tu instancia de Home Assistant sea accesible a trav\u00e9s de Tailscale VPN. \n\nPara autenticarse con Tailscale, deber\u00e1s crear una clave de API en {authkeys_url}. \n\nTailnet es el nombre de tu red Tailscale. Puedes encontrarlo en la esquina superior izquierda del Panel de administraci\u00f3n de Tailscale (junto al logotipo de Tailscale)." } } } diff --git a/homeassistant/components/tesla_wall_connector/translations/es.json b/homeassistant/components/tesla_wall_connector/translations/es.json index 34cdf425528..c324ad42733 100644 --- a/homeassistant/components/tesla_wall_connector/translations/es.json +++ b/homeassistant/components/tesla_wall_connector/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "flow_title": "{serial_number} ({host})", @@ -13,7 +13,7 @@ "data": { "host": "Host" }, - "title": "Configurar el conector de pared Tesla" + "title": "Configurar el Tesla Wall Connector" } } } diff --git a/homeassistant/components/threshold/translations/es.json b/homeassistant/components/threshold/translations/es.json index 585a690108f..e35d539d2d2 100644 --- a/homeassistant/components/threshold/translations/es.json +++ b/homeassistant/components/threshold/translations/es.json @@ -12,7 +12,7 @@ "name": "Nombre", "upper": "L\u00edmite superior" }, - "description": "Cree un sensor binario que se encienda y apague dependiendo del valor de un sensor \n\n Solo l\u00edmite inferior configurado: se enciende cuando el valor del sensor de entrada es menor que el l\u00edmite inferior.\n Solo l\u00edmite superior configurado: se enciende cuando el valor del sensor de entrada es mayor que el l\u00edmite superior.\n Ambos l\u00edmites inferior y superior configurados: se activa cuando el valor del sensor de entrada est\u00e1 en el rango [l\u00edmite inferior ... l\u00edmite superior].", + "description": "Crea un sensor binario que se enciende y apaga dependiendo del valor de un sensor \n\nSolo l\u00edmite inferior configurado: se enciende cuando el valor del sensor de entrada es menor que el l\u00edmite inferior.\nSolo l\u00edmite superior configurado: se enciende cuando el valor del sensor de entrada es mayor que el l\u00edmite superior.\nAmbos l\u00edmites inferior y superior configurados: se activa cuando el valor del sensor de entrada est\u00e1 en el rango [l\u00edmite inferior ... l\u00edmite superior].", "title": "A\u00f1adir sensor de umbral" } } @@ -30,7 +30,7 @@ "name": "Nombre", "upper": "L\u00edmite superior" }, - "description": "Solo l\u00edmite inferior configurado: se enciende cuando el valor del sensor de entrada es menor que el l\u00edmite inferior.\n Solo l\u00edmite superior configurado: se enciende cuando el valor del sensor de entrada es mayor que el l\u00edmite superior.\n Ambos l\u00edmites inferior y superior configurados: se activa cuando el valor del sensor de entrada est\u00e1 en el rango [l\u00edmite inferior ... l\u00edmite superior]." + "description": "Solo l\u00edmite inferior configurado: se enciende cuando el valor del sensor de entrada es menor que el l\u00edmite inferior.\nSolo l\u00edmite superior configurado: se enciende cuando el valor del sensor de entrada es mayor que el l\u00edmite superior.\nAmbos l\u00edmites inferior y superior configurados: se activa cuando el valor del sensor de entrada est\u00e1 en el rango [l\u00edmite inferior ... l\u00edmite superior]." } } }, diff --git a/homeassistant/components/tile/translations/es.json b/homeassistant/components/tile/translations/es.json index c13488183d3..0b0979991bb 100644 --- a/homeassistant/components/tile/translations/es.json +++ b/homeassistant/components/tile/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" diff --git a/homeassistant/components/tod/translations/es.json b/homeassistant/components/tod/translations/es.json index a5451faf145..55b5c495f18 100644 --- a/homeassistant/components/tod/translations/es.json +++ b/homeassistant/components/tod/translations/es.json @@ -3,12 +3,12 @@ "step": { "user": { "data": { - "after_time": "Tiempo de activaci\u00f3n", - "before_time": "Tiempo de desactivaci\u00f3n", + "after_time": "Hora de encendido", + "before_time": "Hora de apagado", "name": "Nombre" }, - "description": "Crea un sensor binario que se activa o desactiva en funci\u00f3n de la hora.", - "title": "A\u00f1ade sensor tiempo del d\u00eda" + "description": "Crea un sensor binario que se enciende o se apaga dependiendo de la hora.", + "title": "A\u00f1adir sensor Horas del d\u00eda" } } }, @@ -16,8 +16,8 @@ "step": { "init": { "data": { - "after_time": "Tiempo de activaci\u00f3n", - "before_time": "Tiempo apagado" + "after_time": "Hora de encendido", + "before_time": "Hora de apagado" } } } diff --git a/homeassistant/components/tolo/translations/es.json b/homeassistant/components/tolo/translations/es.json index 76cb6c73275..9f08b482d3e 100644 --- a/homeassistant/components/tolo/translations/es.json +++ b/homeassistant/components/tolo/translations/es.json @@ -15,7 +15,7 @@ "data": { "host": "Host" }, - "description": "Introduzca el nombre de host o la direcci\u00f3n IP de su dispositivo TOLO Sauna." + "description": "Introduce el nombre de host o la direcci\u00f3n IP de tu dispositivo TOLO Sauna." } } } diff --git a/homeassistant/components/tomorrowio/translations/es.json b/homeassistant/components/tomorrowio/translations/es.json index bacc07bcdfc..48bac23d2ba 100644 --- a/homeassistant/components/tomorrowio/translations/es.json +++ b/homeassistant/components/tomorrowio/translations/es.json @@ -1,9 +1,9 @@ { "config": { "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", - "invalid_api_key": "Clave API inv\u00e1lida", - "rate_limited": "Actualmente la tarifa est\u00e1 limitada, por favor int\u00e9ntelo m\u00e1s tarde.", + "cannot_connect": "No se pudo conectar", + "invalid_api_key": "Clave API no v\u00e1lida", + "rate_limited": "Actualmente el ritmo de consultas est\u00e1 limitado, por favor int\u00e9ntalo m\u00e1s tarde.", "unknown": "Error inesperado" }, "step": { @@ -23,7 +23,7 @@ "data": { "timestep": "Min. entre previsiones de NowCast" }, - "description": "Si elige habilitar la entidad de pron\u00f3stico \"nowcast\", puede configurar el n\u00famero de minutos entre cada pron\u00f3stico. El n\u00famero de pron\u00f3sticos proporcionados depende del n\u00famero de minutos elegidos entre los pron\u00f3sticos.", + "description": "Si eliges habilitar la entidad de pron\u00f3stico del tiempo `nowcast`, puedes configurar la cantidad de minutos entre cada pron\u00f3stico. La cantidad de pron\u00f3sticos proporcionados depende de la cantidad de minutos elegidos entre los pron\u00f3sticos.", "title": "Actualizar las opciones de Tomorrow.io" } } diff --git a/homeassistant/components/tomorrowio/translations/sensor.es.json b/homeassistant/components/tomorrowio/translations/sensor.es.json index 3967aeba2e2..8be15843389 100644 --- a/homeassistant/components/tomorrowio/translations/sensor.es.json +++ b/homeassistant/components/tomorrowio/translations/sensor.es.json @@ -6,19 +6,19 @@ "moderate": "Moderado", "unhealthy": "Poco saludable", "unhealthy_for_sensitive_groups": "No saludable para grupos sensibles", - "very_unhealthy": "Nada saludable" + "very_unhealthy": "Muy poco saludable" }, "tomorrowio__pollen_index": { "high": "Alto", "low": "Bajo", "medium": "Medio", - "none": "Ninguna", + "none": "Ninguno", "very_high": "Muy alto", "very_low": "Muy bajo" }, "tomorrowio__precipitation_type": { - "freezing_rain": "Lluvia g\u00e9lida", - "ice_pellets": "Perdigones de hielo", + "freezing_rain": "Lluvia helada", + "ice_pellets": "Granizo", "none": "Ninguna", "rain": "Lluvia", "snow": "Nieve" diff --git a/homeassistant/components/totalconnect/translations/es.json b/homeassistant/components/totalconnect/translations/es.json index a25a204eec4..61930b53fb0 100644 --- a/homeassistant/components/totalconnect/translations/es.json +++ b/homeassistant/components/totalconnect/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "no_locations": "No hay ubicaciones disponibles para este usuario, compruebe la configuraci\u00f3n de TotalConnect", + "no_locations": "No hay ubicaciones disponibles para este usuario, verifica la configuraci\u00f3n de TotalConnect", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { @@ -12,7 +12,7 @@ "step": { "locations": { "data": { - "usercode": "Codigo de usuario" + "usercode": "C\u00f3digo de usuario" }, "description": "Introduce el c\u00f3digo de usuario para este usuario en la ubicaci\u00f3n {location_id}", "title": "C\u00f3digos de usuario de ubicaci\u00f3n" diff --git a/homeassistant/components/tplink/translations/es.json b/homeassistant/components/tplink/translations/es.json index e083acf61ed..964777e876e 100644 --- a/homeassistant/components/tplink/translations/es.json +++ b/homeassistant/components/tplink/translations/es.json @@ -21,7 +21,7 @@ "data": { "host": "Host" }, - "description": "Si dejas el host vac\u00edo, se usar\u00e1 descubrimiento para encontrar dispositivos." + "description": "Si dejas el host vac\u00edo, se usar\u00e1 el descubrimiento para encontrar dispositivos." } } } diff --git a/homeassistant/components/traccar/translations/es.json b/homeassistant/components/traccar/translations/es.json index d23aa678816..21e90275d17 100644 --- a/homeassistant/components/traccar/translations/es.json +++ b/homeassistant/components/traccar/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cloud_not_connected": "No est\u00e1 conectado a Home Assistant Cloud.", + "cloud_not_connected": "No conectado a Home Assistant Cloud.", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, diff --git a/homeassistant/components/tractive/translations/es.json b/homeassistant/components/tractive/translations/es.json index d1f10a1b293..0e242f535ee 100644 --- a/homeassistant/components/tractive/translations/es.json +++ b/homeassistant/components/tractive/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, elimine la integraci\u00f3n y config\u00farela nuevamente.", + "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, elimina la integraci\u00f3n y vuelve a configurarla.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/tractive/translations/sensor.es.json b/homeassistant/components/tractive/translations/sensor.es.json index 21a642c14d8..e36f70c9bf2 100644 --- a/homeassistant/components/tractive/translations/sensor.es.json +++ b/homeassistant/components/tractive/translations/sensor.es.json @@ -1,9 +1,9 @@ { "state": { "tractive__tracker_state": { - "not_reporting": "No reportando", - "operational": "Operacional", - "system_shutdown_user": "Usuario de cierre del sistema", + "not_reporting": "No informando", + "operational": "Operativo", + "system_shutdown_user": "Usuario de apagado del sistema", "system_startup": "Inicio del sistema" } } diff --git a/homeassistant/components/tradfri/translations/es.json b/homeassistant/components/tradfri/translations/es.json index caadd9b7903..500b357319c 100644 --- a/homeassistant/components/tradfri/translations/es.json +++ b/homeassistant/components/tradfri/translations/es.json @@ -5,7 +5,7 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso" }, "error": { - "cannot_authenticate": "No se puede autenticar, \u00bfGateway est\u00e1 emparejado con otro servidor como, por ejemplo, Homekit?", + "cannot_authenticate": "No se puede autenticar, \u00bfest\u00e1 la puerta de enlace emparejada con otro servidor como, por ejemplo, Homekit?", "cannot_connect": "No se puede conectar a la puerta de enlace.", "invalid_key": "No se ha podido registrar con la clave proporcionada. Si esto sigue ocurriendo, intenta reiniciar el gateway.", "timeout": "Tiempo de espera agotado validando el c\u00f3digo." diff --git a/homeassistant/components/trafikverket_weatherstation/translations/es.json b/homeassistant/components/trafikverket_weatherstation/translations/es.json index 9513ae65221..f32acf866f4 100644 --- a/homeassistant/components/trafikverket_weatherstation/translations/es.json +++ b/homeassistant/components/trafikverket_weatherstation/translations/es.json @@ -6,8 +6,8 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "invalid_station": "No se ha podido encontrar una estaci\u00f3n meteorol\u00f3gica con el nombre especificado", - "more_stations": "Se han encontrado varias estaciones meteorol\u00f3gicas con el nombre especificado" + "invalid_station": "No se pudo encontrar una estaci\u00f3n meteorol\u00f3gica con el nombre especificado", + "more_stations": "Se encontraron varias estaciones meteorol\u00f3gicas con el nombre especificado" }, "step": { "user": { diff --git a/homeassistant/components/tuya/translations/es.json b/homeassistant/components/tuya/translations/es.json index d51024c5640..ca46860a208 100644 --- a/homeassistant/components/tuya/translations/es.json +++ b/homeassistant/components/tuya/translations/es.json @@ -7,8 +7,8 @@ "step": { "user": { "data": { - "access_id": "ID de acceso de Tuya IoT", - "access_secret": "Tuya IoT Access Secret", + "access_id": "ID de acceso a Tuya IoT", + "access_secret": "Secreto de acceso a Tuya IoT", "country_code": "C\u00f3digo de pais de tu cuenta (por ejemplo, 1 para USA o 86 para China)", "password": "Contrase\u00f1a", "username": "Nombre de usuario" diff --git a/homeassistant/components/tuya/translations/select.es.json b/homeassistant/components/tuya/translations/select.es.json index b6ab1f8ce1b..0e64ced084e 100644 --- a/homeassistant/components/tuya/translations/select.es.json +++ b/homeassistant/components/tuya/translations/select.es.json @@ -55,8 +55,8 @@ "tuya__humidifier_moodlighting": { "1": "Estado de \u00e1nimo 1", "2": "Estado de \u00e1nimo 2", - "3": "Estado 3", - "4": "Estado 4", + "3": "Estado de \u00e1nimo 3", + "4": "Estado de \u00e1nimo 4", "5": "Estado de \u00e1nimo 5" }, "tuya__humidifier_spray_mode": { @@ -68,7 +68,7 @@ }, "tuya__ipc_work_mode": { "0": "Modo de bajo consumo", - "1": "Modo de trabajo continuo" + "1": "Modo de funcionamiento continuo" }, "tuya__led_type": { "halogen": "Hal\u00f3geno", @@ -78,7 +78,7 @@ "tuya__light_mode": { "none": "Apagado", "pos": "Indicar la ubicaci\u00f3n del interruptor", - "relay": "Indica el estado de encendido / apagado" + "relay": "Indicar el estado de encendido/apagado del interruptor" }, "tuya__motion_sensitivity": { "0": "Sensibilidad baja", @@ -90,15 +90,15 @@ "2": "Grabaci\u00f3n continua" }, "tuya__relay_status": { - "last": "Recuerda el \u00faltimo estado", - "memory": "Recuerda el \u00faltimo estado", + "last": "Recordar el \u00faltimo estado", + "memory": "Recordar el \u00faltimo estado", "off": "Apagado", "on": "Encendido", "power_off": "Apagado", "power_on": "Encendido" }, "tuya__vacuum_cistern": { - "closed": "Cerrado", + "closed": "Cerrada", "high": "Alto", "low": "Bajo", "middle": "Medio" @@ -125,7 +125,7 @@ "single": "\u00danico", "smart": "Inteligente", "spiral": "Espiral", - "standby": "Standby", + "standby": "En espera", "wall_follow": "Seguir Muro", "zone": "Zona" } diff --git a/homeassistant/components/tuya/translations/sensor.es.json b/homeassistant/components/tuya/translations/sensor.es.json index 7dad02bdf7d..da317023bb0 100644 --- a/homeassistant/components/tuya/translations/sensor.es.json +++ b/homeassistant/components/tuya/translations/sensor.es.json @@ -8,13 +8,13 @@ }, "tuya__status": { "boiling_temp": "Temperatura de ebullici\u00f3n", - "cooling": "Enfriamiento", + "cooling": "Refrigeraci\u00f3n", "heating": "Calefacci\u00f3n", "heating_temp": "Temperatura de calentamiento", "reserve_1": "Reserva 1", "reserve_2": "Reserva 2", "reserve_3": "Reserva 3", - "standby": "Standby", + "standby": "En espera", "warm": "Conservaci\u00f3n del calor" } } diff --git a/homeassistant/components/unifiprotect/translations/es.json b/homeassistant/components/unifiprotect/translations/es.json index 8a1cdd9afcd..684ee3f817f 100644 --- a/homeassistant/components/unifiprotect/translations/es.json +++ b/homeassistant/components/unifiprotect/translations/es.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "discovery_started": "Se ha iniciado el descubrimiento" + "discovery_started": "Descubrimiento iniciado" }, "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "protect_version": "La versi\u00f3n m\u00ednima requerida es v1.20.0. Actualice UniFi Protect y vuelva a intentarlo." + "protect_version": "La versi\u00f3n m\u00ednima requerida es v1.20.0. Actualiza UniFi Protect y vuelve a intentarlo." }, "flow_title": "{name} ({ip_address})", "step": { @@ -16,7 +16,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "\u00bfQuieres configurar {name} ({ip_address})? Necesitar\u00e1 un usuario local creado en su consola UniFi OS para iniciar sesi\u00f3n. Los usuarios de Ubiquiti Cloud no funcionar\u00e1n. Para m\u00e1s informaci\u00f3n: {local_user_documentation_url}", + "description": "\u00bfQuieres configurar {name} ({ip_address})? Necesitar\u00e1s un usuario local creado en tu consola UniFi OS para iniciar sesi\u00f3n. Los usuarios de Ubiquiti Cloud no funcionar\u00e1n. Para m\u00e1s informaci\u00f3n: {local_user_documentation_url}", "title": "UniFi Protect descubierto" }, "reauth_confirm": { @@ -36,7 +36,7 @@ "username": "Nombre de usuario", "verify_ssl": "Verificar el certificado SSL" }, - "description": "Necesitar\u00e1 un usuario local creado en su consola UniFi OS para iniciar sesi\u00f3n. Los usuarios de Ubiquiti Cloud no funcionar\u00e1n. Para m\u00e1s informaci\u00f3n: {local_user_documentation_url}", + "description": "Necesitar\u00e1s un usuario local creado en tu consola UniFi OS para iniciar sesi\u00f3n. Los usuarios de Ubiquiti Cloud no funcionar\u00e1n. Para m\u00e1s informaci\u00f3n: {local_user_documentation_url}", "title": "Configuraci\u00f3n de UniFi Protect" } } @@ -45,12 +45,12 @@ "step": { "init": { "data": { - "all_updates": "M\u00e9tricas en tiempo real (ADVERTENCIA: Aumenta en gran medida el uso de la CPU)", + "all_updates": "M\u00e9tricas en tiempo real (ADVERTENCIA: aumenta considerablemente el uso de la CPU)", "disable_rtsp": "Deshabilitar la transmisi\u00f3n RTSP", "max_media": "N\u00famero m\u00e1ximo de eventos a cargar para el Navegador de Medios (aumenta el uso de RAM)", "override_connection_host": "Anular la conexi\u00f3n del host" }, - "description": "La opci\u00f3n de m\u00e9tricas en tiempo real s\u00f3lo debe estar activada si ha habilitado los sensores de diagn\u00f3stico y quiere que se actualicen en tiempo real. Si no est\u00e1 activada, s\u00f3lo se actualizar\u00e1n una vez cada 15 minutos.", + "description": "La opci\u00f3n de m\u00e9tricas en tiempo real solo debe habilitarse si has habilitado los sensores de diagn\u00f3stico y deseas que se actualicen en tiempo real. Si no est\u00e1n habilitados, solo se actualizar\u00e1n una vez cada 15 minutos.", "title": "Opciones de UniFi Protect" } } diff --git a/homeassistant/components/unifiprotect/translations/id.json b/homeassistant/components/unifiprotect/translations/id.json index a0a3b9751d3..38f5a87cb05 100644 --- a/homeassistant/components/unifiprotect/translations/id.json +++ b/homeassistant/components/unifiprotect/translations/id.json @@ -47,6 +47,7 @@ "data": { "all_updates": "Metrik waktu nyata (PERINGATAN: Meningkatkan penggunaan CPU)", "disable_rtsp": "Nonaktifkan aliran RTSP", + "max_media": "Jumlah maksimum peristiwa yang akan dimuat untuk Browser Media (meningkatkan penggunaan RAM)", "override_connection_host": "Timpa Host Koneksi" }, "description": "Opsi metrik waktu nyata hanya boleh diaktifkan jika Anda telah mengaktifkan sensor diagnostik dan ingin memperbaruinya secara waktu nyata. Jika tidak diaktifkan, metrik hanya akan memperbarui setiap 15 menit sekali.", diff --git a/homeassistant/components/unifiprotect/translations/pl.json b/homeassistant/components/unifiprotect/translations/pl.json index 82aa3c91ee3..1752e40ac3c 100644 --- a/homeassistant/components/unifiprotect/translations/pl.json +++ b/homeassistant/components/unifiprotect/translations/pl.json @@ -47,6 +47,7 @@ "data": { "all_updates": "Metryki w czasie rzeczywistym (UWAGA: Znacznie zwi\u0119ksza u\u017cycie CPU)", "disable_rtsp": "Wy\u0142\u0105cz strumie\u0144 RTSP", + "max_media": "Maksymalna liczba zdarze\u0144 do za\u0142adowania dla przegl\u0105darki medi\u00f3w (zwi\u0119ksza u\u017cycie pami\u0119ci RAM)", "override_connection_host": "Zast\u0105p host po\u0142\u0105czenia" }, "description": "Opcja metryk w czasie rzeczywistym powinna by\u0107 w\u0142\u0105czona tylko wtedy, gdy w\u0142\u0105czono sensory diagnostyczne i chcesz je aktualizowa\u0107 w czasie rzeczywistym. Je\u015bli nie s\u0105 w\u0142\u0105czone, b\u0119d\u0105 aktualizowane tylko raz na 15 minut.", diff --git a/homeassistant/components/uptime/translations/es.json b/homeassistant/components/uptime/translations/es.json index 84e840f453f..e04b528f153 100644 --- a/homeassistant/components/uptime/translations/es.json +++ b/homeassistant/components/uptime/translations/es.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "step": { "user": { - "description": "\u00bfQuiere empezar a configurar?" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" } } }, diff --git a/homeassistant/components/uptimerobot/translations/es.json b/homeassistant/components/uptimerobot/translations/es.json index e78a90b4176..e1ede36a55a 100644 --- a/homeassistant/components/uptimerobot/translations/es.json +++ b/homeassistant/components/uptimerobot/translations/es.json @@ -2,30 +2,30 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, elimine la integraci\u00f3n y config\u00farela nuevamente.", + "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, eliminq la integraci\u00f3n y vuelve a configurarla.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" }, "error": { "cannot_connect": "No se pudo conectar", "invalid_api_key": "Clave API no v\u00e1lida", - "not_main_key": "Se ha detectado un tipo de clave API incorrecta, utiliza la clave API 'principal'", - "reauth_failed_matching_account": "La clave de API que has proporcionado no coincide con el ID de cuenta para la configuraci\u00f3n existente.", + "not_main_key": "Se detect\u00f3 un tipo de clave API incorrecto, utiliza la clave API 'principal'", + "reauth_failed_matching_account": "La clave de API que proporcionaste no coincide con el ID de cuenta para la configuraci\u00f3n existente.", "unknown": "Error inesperado" }, "step": { "reauth_confirm": { "data": { - "api_key": "API Key" + "api_key": "Clave API" }, - "description": "Debes proporcionar una nueva clave API de solo lectura de Uptime Robot", + "description": "Debes proporcionar una nueva clave API 'principal' de UptimeRobot", "title": "Volver a autenticar la integraci\u00f3n" }, "user": { "data": { "api_key": "Clave API" }, - "description": "Debes proporcionar una clave API de solo lectura de robot de tiempo de actividad/funcionamiento" + "description": "Debes proporcionar la clave API 'principal' de UptimeRobot" } } } diff --git a/homeassistant/components/uptimerobot/translations/sensor.es.json b/homeassistant/components/uptimerobot/translations/sensor.es.json index 2adb0ff18c5..5397e6eb54c 100644 --- a/homeassistant/components/uptimerobot/translations/sensor.es.json +++ b/homeassistant/components/uptimerobot/translations/sensor.es.json @@ -1,11 +1,11 @@ { "state": { "uptimerobot__monitor_status": { - "down": "No disponible", - "not_checked_yet": "No comprobado", - "pause": "En pausa", - "seems_down": "Parece no disponible", - "up": "Funcionante" + "down": "Ca\u00eddo", + "not_checked_yet": "A\u00fan no se ha comprobado", + "pause": "Pausa", + "seems_down": "Parece ca\u00eddo", + "up": "Arriba" } } } \ No newline at end of file diff --git a/homeassistant/components/vallox/translations/es.json b/homeassistant/components/vallox/translations/es.json index 373ae333ec3..088257647ad 100644 --- a/homeassistant/components/vallox/translations/es.json +++ b/homeassistant/components/vallox/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El servicio ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "invalid_host": "Nombre del host o direcci\u00f3n IP no v\u00e1lidos", + "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/verisure/translations/es.json b/homeassistant/components/verisure/translations/es.json index c7ea0af6f41..ed0d1328e79 100644 --- a/homeassistant/components/verisure/translations/es.json +++ b/homeassistant/components/verisure/translations/es.json @@ -14,7 +14,7 @@ "data": { "giid": "Instalaci\u00f3n" }, - "description": "Home Assistant encontr\u00f3 varias instalaciones de Verisure en su cuenta de Mis p\u00e1ginas. Por favor, seleccione la instalaci\u00f3n para agregar a Home Assistant." + "description": "Home Assistant encontr\u00f3 varias instalaciones Verisure en tu cuenta My Pages. Por favor, selecciona la instalaci\u00f3n a a\u00f1adir a Home Assistant." }, "mfa": { "data": { @@ -24,7 +24,7 @@ }, "reauth_confirm": { "data": { - "description": "Vuelva a autenticarse con su cuenta Verisure My Pages.", + "description": "Vuelva a autenticarse con tu cuenta Verisure My Pages.", "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a" } @@ -46,13 +46,13 @@ }, "options": { "error": { - "code_format_mismatch": "El c\u00f3digo PIN predeterminado no coincide con el n\u00famero necesario de d\u00edgitos" + "code_format_mismatch": "El c\u00f3digo PIN predeterminado no coincide con el n\u00famero requerido de d\u00edgitos" }, "step": { "init": { "data": { - "lock_code_digits": "N\u00famero de d\u00edgitos del c\u00f3digo PIN de las cerraduras", - "lock_default_code": "C\u00f3digo PIN por defecto para las cerraduras, utilizado si no se indica ninguno" + "lock_code_digits": "N\u00famero de d\u00edgitos en el c\u00f3digo PIN para cerraduras", + "lock_default_code": "C\u00f3digo PIN predeterminado para cerraduras, se usa si no se proporciona ninguno" } } } diff --git a/homeassistant/components/version/translations/es.json b/homeassistant/components/version/translations/es.json index e2cc5937f8d..cbcd4bcd511 100644 --- a/homeassistant/components/version/translations/es.json +++ b/homeassistant/components/version/translations/es.json @@ -8,8 +8,8 @@ "data": { "version_source": "Fuente de la versi\u00f3n" }, - "description": "Seleccione la fuente desde la que desea realizar el seguimiento de las versiones", - "title": "Seleccione el tipo de instalaci\u00f3n" + "description": "Selecciona la fuente de la que deseas realizar un seguimiento de las versiones", + "title": "Selecciona el tipo de instalaci\u00f3n" }, "version_source": { "data": { @@ -18,7 +18,7 @@ "channel": "Qu\u00e9 canal debe ser rastreado", "image": "Qu\u00e9 imagen debe ser rastreada" }, - "description": "Configurar el seguimiento de la versi\u00f3n {version_source}", + "description": "Configurar el seguimiento de versiones de {version_source}", "title": "Configurar" } } diff --git a/homeassistant/components/vicare/translations/es.json b/homeassistant/components/vicare/translations/es.json index 6653fa3198e..26f32b1e720 100644 --- a/homeassistant/components/vicare/translations/es.json +++ b/homeassistant/components/vicare/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n.", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "unknown": "Error inesperado" }, "error": { @@ -14,9 +14,9 @@ "client_id": "Clave API", "heating_type": "Tipo de calefacci\u00f3n", "password": "Contrase\u00f1a", - "username": "Email" + "username": "Correo electr\u00f3nico" }, - "description": "Configure la integraci\u00f3n de ViCare. Para generar la clave API, vaya a https://developer.viessmann.com" + "description": "Configura la integraci\u00f3n de ViCare. Para generar la clave API, ve a https://developer.viessmann.com" } } } diff --git a/homeassistant/components/vlc_telnet/translations/es.json b/homeassistant/components/vlc_telnet/translations/es.json index 64f3b72d223..4cdc7cb2154 100644 --- a/homeassistant/components/vlc_telnet/translations/es.json +++ b/homeassistant/components/vlc_telnet/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" @@ -15,13 +15,13 @@ "flow_title": "{host}", "step": { "hassio_confirm": { - "description": "\u00bfDesea conectarse al complemento {addon}?" + "description": "\u00bfQuieres conectarte al complemento {addon}?" }, "reauth_confirm": { "data": { "password": "Contrase\u00f1a" }, - "description": "Por favor, introduzca la contrase\u00f1a correcta para el host: {host}" + "description": "Por favor, introduce la contrase\u00f1a correcta para el host: {host}" }, "user": { "data": { diff --git a/homeassistant/components/wallbox/translations/es.json b/homeassistant/components/wallbox/translations/es.json index 451e55ed41e..e93a47ce3b3 100644 --- a/homeassistant/components/wallbox/translations/es.json +++ b/homeassistant/components/wallbox/translations/es.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "reauth_invalid": "Fallo en la reautenticaci\u00f3n; el n\u00famero de serie no coincide con el original", + "reauth_invalid": "La reautenticaci\u00f3n fall\u00f3; El n\u00famero de serie no coincide con el original", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/watttime/translations/es.json b/homeassistant/components/watttime/translations/es.json index 189ea8b70cb..9267314d960 100644 --- a/homeassistant/components/watttime/translations/es.json +++ b/homeassistant/components/watttime/translations/es.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya se ha configurado", + "already_configured": "El dispositivo ya est\u00e1 configurado", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado", - "unknown_coordinates": "No hay datos para esa latitud/longitud" + "unknown_coordinates": "No hay datos para latitud/longitud" }, "step": { "coordinates": { @@ -15,19 +15,19 @@ "latitude": "Latitud", "longitude": "Longitud" }, - "description": "Introduzca la latitud y longitud a monitorizar:" + "description": "Introduce la latitud y longitud a supervisar:" }, "location": { "data": { "location_type": "Ubicaci\u00f3n" }, - "description": "Escoja una ubicaci\u00f3n para monitorizar:" + "description": "Elige una ubicaci\u00f3n para supervisar:" }, "reauth_confirm": { "data": { "password": "Contrase\u00f1a" }, - "description": "Vuelva a ingresar la contrase\u00f1a de {username} :", + "description": "Por favor, vuelve a introducir la contrase\u00f1a de {username}:", "title": "Volver a autenticar la integraci\u00f3n" }, "user": { @@ -35,7 +35,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Introduzca su nombre de usuario y contrase\u00f1a:" + "description": "Introduce tu nombre de usuario y contrase\u00f1a:" } } }, @@ -43,7 +43,7 @@ "step": { "init": { "data": { - "show_on_map": "Mostrar la ubicaci\u00f3n en el mapa" + "show_on_map": "Mostrar la ubicaci\u00f3n supervisada en el mapa" }, "title": "Configurar WattTime" } diff --git a/homeassistant/components/waze_travel_time/translations/es.json b/homeassistant/components/waze_travel_time/translations/es.json index 62325233cab..118bf583d56 100644 --- a/homeassistant/components/waze_travel_time/translations/es.json +++ b/homeassistant/components/waze_travel_time/translations/es.json @@ -14,7 +14,7 @@ "origin": "Origen", "region": "Regi\u00f3n" }, - "description": "En Origen y Destino, introduce la direcci\u00f3n de las coordenadas GPS de la ubicaci\u00f3n (las coordenadas GPS deben estar separadas por una coma). Tambi\u00e9n puedes escribir un id de entidad que proporcione esta informaci\u00f3n en su estado, un id de entidad con atributos de latitud y longitud o un nombre descriptivo de zona." + "description": "Para Origen y Destino, introduce la direcci\u00f3n o las coordenadas GPS de la ubicaci\u00f3n (las coordenadas GPS deben estar separadas por una coma). Tambi\u00e9n puedes introducir un id de entidad que proporcione esta informaci\u00f3n en su estado, un id de entidad con atributos de latitud y longitud, o un nombre descriptivo de zona." } } }, @@ -31,7 +31,7 @@ "units": "Unidades", "vehicle_type": "Tipo de veh\u00edculo" }, - "description": "Las entradas `subcadena` te permitir\u00e1n forzar a la integraci\u00f3n a utilizar una ruta concreta o a evitar una ruta concreta en el c\u00e1lculo del tiempo del recorrido." + "description": "Las entradas `subcadena` te permitir\u00e1n forzar la integraci\u00f3n para usar una ruta en particular o evitar una ruta en particular en su c\u00e1lculo del tiempo de viaje." } } }, diff --git a/homeassistant/components/webostv/translations/es.json b/homeassistant/components/webostv/translations/es.json index db23caa048b..d4b3d2eecdc 100644 --- a/homeassistant/components/webostv/translations/es.json +++ b/homeassistant/components/webostv/translations/es.json @@ -6,32 +6,32 @@ "error_pairing": "Conectado a LG webOS TV pero no emparejado" }, "error": { - "cannot_connect": "No se ha podido conectar, por favor, encienda el televisor o compruebe la direcci\u00f3n IP" + "cannot_connect": "No se pudo conectar, por favor, enciende tu televisor o verifica la direcci\u00f3n IP" }, "flow_title": "LG webOS Smart TV", "step": { "pairing": { - "description": "Haz clic en enviar y acepta la solicitud de emparejamiento en tu televisor.\n\n![Image](/static/images/config_webos.png)", + "description": "Haz clic en enviar y acepta la solicitud de emparejamiento en tu televisor. \n\n ![Image](/static/images/config_webos.png)", "title": "Emparejamiento de webOS TV" }, "user": { "data": { - "host": "Anfitri\u00f3n", + "host": "Host", "name": "Nombre" }, - "description": "Encienda la televisi\u00f3n, rellene los siguientes campos y haga clic en enviar", + "description": "Enciende la TV, completa los siguientes campos y haz clic en enviar", "title": "Conectarse a webOS TV" } } }, "device_automation": { "trigger_type": { - "webostv.turn_on": "Se solicita el encendido del dispositivo" + "webostv.turn_on": "Se solicita que el dispositivo se encienda" } }, "options": { "error": { - "cannot_retrieve": "No se puede recuperar la lista de fuentes. Aseg\u00farese de que el dispositivo est\u00e1 encendido", + "cannot_retrieve": "No se puede recuperar la lista de fuentes. Aseg\u00farate de que el dispositivo est\u00e9 encendido", "script_not_found": "Script no encontrado" }, "step": { @@ -39,7 +39,7 @@ "data": { "sources": "Lista de fuentes" }, - "description": "Seleccionar fuentes habilitadas", + "description": "Selecciona las fuentes habilitadas", "title": "Opciones para webOS Smart TV" } } diff --git a/homeassistant/components/whois/translations/es.json b/homeassistant/components/whois/translations/es.json index e2803c251d6..ff7506bf0c5 100644 --- a/homeassistant/components/whois/translations/es.json +++ b/homeassistant/components/whois/translations/es.json @@ -7,7 +7,7 @@ "unexpected_response": "Respuesta inesperada del servidor whois", "unknown_date_format": "Formato de fecha desconocido en la respuesta del servidor whois", "unknown_tld": "El TLD dado es desconocido o no est\u00e1 disponible para esta integraci\u00f3n", - "whois_command_failed": "El comando whois ha fallado: no se pudo obtener la informaci\u00f3n whois" + "whois_command_failed": "Error en el comando whois: no se pudo recuperar la informaci\u00f3n whois" }, "step": { "user": { diff --git a/homeassistant/components/wiz/translations/es.json b/homeassistant/components/wiz/translations/es.json index b86f60649a2..d3d0469cd0b 100644 --- a/homeassistant/components/wiz/translations/es.json +++ b/homeassistant/components/wiz/translations/es.json @@ -1,21 +1,21 @@ { "config": { "abort": { - "already_configured": "Dispositivo ya configurado", - "cannot_connect": "Error al conectar", - "no_devices_found": "Ning\u00fan dispositivo encontrado en la red." + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", + "no_devices_found": "No se encontraron dispositivos en la red" }, "error": { - "bulb_time_out": "No se puede conectar a la bombilla. Tal vez la bombilla est\u00e1 desconectada o se ingres\u00f3 una IP incorrecta. \u00a1Por favor encienda la luz y vuelve a intentarlo!", - "cannot_connect": "Error al conectar", + "bulb_time_out": "No se puede conectar a la bombilla. Tal vez la bombilla est\u00e1 desconectada o se ha introducido una IP incorrecta. \u00a1Por favor, enciende la luz y vuelve a intentarlo!", + "cannot_connect": "No se pudo conectar", "no_ip": "No es una direcci\u00f3n IP v\u00e1lida.", - "no_wiz_light": "La bombilla no se puede conectar a trav\u00e9s de la integraci\u00f3n de WiZ Platform.", + "no_wiz_light": "La bombilla no se puede conectar a trav\u00e9s de la integraci\u00f3n WiZ Platform.", "unknown": "Error inesperado" }, "flow_title": "{name} ({host})", "step": { "discovery_confirm": { - "description": "\u00bfDesea configurar {name} ({host})?" + "description": "\u00bfQuieres configurar {name} ({host})?" }, "pick_device": { "data": { @@ -26,7 +26,7 @@ "data": { "host": "Direcci\u00f3n IP" }, - "description": "Si deja la direcci\u00f3n IP vac\u00eda, la detecci\u00f3n se utilizar\u00e1 para buscar dispositivos." + "description": "Si dejas la direcci\u00f3n IP vac\u00eda, se usar\u00e1 el descubrimiento para encontrar dispositivos." } } } diff --git a/homeassistant/components/wled/translations/es.json b/homeassistant/components/wled/translations/es.json index 07ebd6c2e02..7a428960d62 100644 --- a/homeassistant/components/wled/translations/es.json +++ b/homeassistant/components/wled/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Este dispositivo WLED ya est\u00e1 configurado.", "cannot_connect": "No se pudo conectar", - "cct_unsupported": "Este dispositivo WLED utiliza canales CCT, que no son compatibles con esta integraci\u00f3n." + "cct_unsupported": "Este dispositivo WLED utiliza canales CCT, que no son compatibles con esta integraci\u00f3n" }, "error": { "cannot_connect": "No se pudo conectar" @@ -26,7 +26,7 @@ "step": { "init": { "data": { - "keep_master_light": "Mantenga la luz principal, incluso con 1 segmento de LED." + "keep_master_light": "Mantener la luz principal, incluso con 1 segmento LED." } } } diff --git a/homeassistant/components/xiaomi_miio/translations/es.json b/homeassistant/components/xiaomi_miio/translations/es.json index 6152da62e47..8f692a87b2c 100644 --- a/homeassistant/components/xiaomi_miio/translations/es.json +++ b/homeassistant/components/xiaomi_miio/translations/es.json @@ -3,16 +3,16 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "incomplete_info": "Informaci\u00f3n incompleta para configurar el dispositivo, no se ha suministrado ning\u00fan host o token.", + "incomplete_info": "Informaci\u00f3n incompleta para configurar el dispositivo, no se proporcion\u00f3 host ni token.", "not_xiaomi_miio": "El dispositivo no es (todav\u00eda) compatible con Xiaomi Miio.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", - "cloud_credentials_incomplete": "Las credenciales de la nube est\u00e1n incompletas, por favor, rellene el nombre de usuario, la contrase\u00f1a y el pa\u00eds", - "cloud_login_error": "No se ha podido iniciar sesi\u00f3n en Xiaomi Miio Cloud, comprueba las credenciales.", - "cloud_no_devices": "No se han encontrado dispositivos en esta cuenta de Xiaomi Miio.", - "unknown_device": "No se conoce el modelo del dispositivo, no se puede configurar el dispositivo mediante el flujo de configuraci\u00f3n.", + "cloud_credentials_incomplete": "Credenciales de la nube incompletas, por favor, completa el nombre de usuario, la contrase\u00f1a y el pa\u00eds", + "cloud_login_error": "No se pudo iniciar sesi\u00f3n en Xiaomi Miio Cloud, verifica las credenciales.", + "cloud_no_devices": "No se encontraron dispositivos en esta cuenta en la nube de Xiaomi Miio.", + "unknown_device": "Se desconoce el modelo del dispositivo, no se puede configurar el dispositivo mediante el flujo de configuraci\u00f3n.", "wrong_token": "Error de suma de comprobaci\u00f3n, token err\u00f3neo" }, "flow_title": "{name}", @@ -24,7 +24,7 @@ "cloud_username": "Nombre de usuario de la nube", "manual": "Configurar manualmente (no recomendado)" }, - "description": "Inicie sesi\u00f3n en la nube de Xiaomi Miio, consulte https://www.openhab.org/addons/bindings/miio/#country-servers para conocer el servidor de la nube que debe utilizar." + "description": "Inicia sesi\u00f3n en la nube de Xiaomi Miio, consulta https://www.openhab.org/addons/bindings/miio/#country-servers para conocer el servidor de la nube que debes utilizar." }, "connect": { "data": { diff --git a/homeassistant/components/yale_smart_alarm/translations/es.json b/homeassistant/components/yale_smart_alarm/translations/es.json index 59824bfdb3b..94f54854126 100644 --- a/homeassistant/components/yale_smart_alarm/translations/es.json +++ b/homeassistant/components/yale_smart_alarm/translations/es.json @@ -2,16 +2,16 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "La reautenticaci\u00f3n fue exitosa" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "reauth_confirm": { "data": { - "area_id": "ID de \u00e1rea", + "area_id": "ID de \u00c1rea", "name": "Nombre", "password": "Contrase\u00f1a", "username": "Nombre de usuario" @@ -29,13 +29,13 @@ }, "options": { "error": { - "code_format_mismatch": "El c\u00f3digo no coincide con el n\u00famero de d\u00edgitos requerido" + "code_format_mismatch": "El c\u00f3digo no coincide con el n\u00famero requerido de d\u00edgitos" }, "step": { "init": { "data": { - "code": "C\u00f3digo predeterminado para cerraduras, utilizado si no se proporciona ninguno", - "lock_code_digits": "N\u00famero de d\u00edgitos del c\u00f3digo PIN de las cerraduras" + "code": "C\u00f3digo predeterminado para cerraduras, se usa si no se proporciona ninguno", + "lock_code_digits": "N\u00famero de d\u00edgitos en el c\u00f3digo PIN para cerraduras" } } } diff --git a/homeassistant/components/yalexs_ble/translations/de.json b/homeassistant/components/yalexs_ble/translations/de.json new file mode 100644 index 00000000000..cfd368aadef --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/de.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "no_unconfigured_devices": "Keine unkonfigurierten Ger\u00e4te gefunden." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_key_format": "Der Offline-Schl\u00fcssel muss eine 32-Byte-Hex-Zeichenfolge sein.", + "invalid_key_index": "Der Offline-Schl\u00fcssel-Slot muss eine ganze Zahl zwischen 0 und 255 sein.", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "M\u00f6chtest du {name} \u00fcber Bluetooth mit der Adresse {address} einrichten?" + }, + "user": { + "data": { + "address": "Bluetooth-Adresse", + "key": "Offline-Schl\u00fcssel (32-Byte-Hex-String)", + "slot": "Offline-Schl\u00fcssel-Slot (Ganzzahl zwischen 0 und 255)" + }, + "description": "Lies in der Dokumentation unter {docs_url} nach, wie du den Offline-Schl\u00fcssel finden kannst." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/et.json b/homeassistant/components/yalexs_ble/translations/et.json new file mode 100644 index 00000000000..564b6c32dd5 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/et.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud", + "no_unconfigured_devices": "H\u00e4\u00e4lestamata seadmeid ei leitud." + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "invalid_key_format": "V\u00f5rgu\u00fchenduseta v\u00f5ti peab olema 32-baidine kuueteistk\u00fcmnebaidine string.", + "invalid_key_index": "V\u00f5rgu\u00fchenduseta v\u00f5tmepesa peab olema t\u00e4isarv vahemikus 0 kuni 255.", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "Kas seadistada {name} Bluetoothi kaudu aadressiga {address} ?" + }, + "user": { + "data": { + "address": "Bluetoothi aadress", + "key": "V\u00f5rgu\u00fchenduseta v\u00f5ti (32-baidine kuueteistk\u00fcmnebaidine string)", + "slot": "V\u00f5rgu\u00fchenduseta v\u00f5tmepesa (t\u00e4isarv vahemikus 0 kuni 255)" + }, + "description": "V\u00f5rgu\u00fchenduseta v\u00f5tme leidmise kohta vaata dokumentatsiooni aadressil {docs_url} ." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/id.json b/homeassistant/components/yalexs_ble/translations/id.json new file mode 100644 index 00000000000..fda1e243e0c --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/id.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "no_unconfigured_devices": "Tidak ditemukan perangkat yang tidak dikonfigurasi." + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "invalid_key_format": "Kunci offline harus berupa string heksadesimal 32 byte.", + "invalid_key_index": "Slot kunci offline harus berupa bilangan bulat antara 0 dan 255.", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "Ingin menyiapkan {name} melalui Bluetooth dengan alamat {address} ?" + }, + "user": { + "data": { + "address": "Alamat Bluetooth", + "key": "Kunci Offline (string heksadesimal 32 byte)", + "slot": "Slot Kunci Offline (Bilangan Bulat antara 0 dan 255)" + }, + "description": "Lihat dokumentasi di {docs_url} untuk mengetahui cara menemukan kunci offline." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/no.json b/homeassistant/components/yalexs_ble/translations/no.json new file mode 100644 index 00000000000..6d2be535c73 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/no.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "no_unconfigured_devices": "Fant ingen ukonfigurerte enheter." + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "invalid_key_format": "Den frakoblede n\u00f8kkelen m\u00e5 v\u00e6re en 32-byte sekskantstreng.", + "invalid_key_index": "Frakoblet n\u00f8kkelspor m\u00e5 v\u00e6re et heltall mellom 0 og 255.", + "unknown": "Uventet feil" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "Vil du konfigurere {name} over Bluetooth med adressen {address} ?" + }, + "user": { + "data": { + "address": "Bluetooth-adresse", + "key": "Frakoblet n\u00f8kkel (32-byte sekskantstreng)", + "slot": "Frakoblet n\u00f8kkelspor (heltall mellom 0 og 255)" + }, + "description": "Se dokumentasjonen p\u00e5 {docs_url} for hvordan du finner frakoblet n\u00f8kkel." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/pl.json b/homeassistant/components/yalexs_ble/translations/pl.json new file mode 100644 index 00000000000..b178db81e8d --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/pl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "no_unconfigured_devices": "Nie znaleziono nieskonfigurowanych urz\u0105dze\u0144." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_key_format": "Klucz offline musi by\u0107 32-bajtowym ci\u0105giem szesnastkowym.", + "invalid_key_index": "Slot klucza offline musi by\u0107 liczb\u0105 ca\u0142kowit\u0105 z zakresu od 0 do 255.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name} przez Bluetooth z adresem {address}?" + }, + "user": { + "data": { + "address": "Adres Bluetooth", + "key": "Klucz offline (32-bajtowy ci\u0105g szesnastkowy)", + "slot": "Slot klucza offline (liczba ca\u0142kowita od 0 do 255)" + }, + "description": "Sprawd\u017a dokumentacj\u0119 na {docs_url}, aby dowiedzie\u0107 si\u0119, jak znale\u017a\u0107 klucz offline." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/zh-Hant.json b/homeassistant/components/yalexs_ble/translations/zh-Hant.json new file mode 100644 index 00000000000..f16fdcb0d07 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/zh-Hant.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "no_unconfigured_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u8a2d\u5b9a\u88dd\u7f6e\u3002" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_key_format": "\u96e2\u7dda\u91d1\u9470\u5fc5\u9808\u70ba 32 \u4f4d\u5143\u5341\u516d\u9032\u4f4d\u5b57\u4e32\u3002", + "invalid_key_index": "\u96e2\u7dda\u91d1\u9470\u5fc5\u9808\u70ba 0 \u81f3 255 \u4e4b\u9593\u7684\u6574\u6578\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u4f4d\u5740\u70ba {address}\u3001\u540d\u7a31\u70ba {name} \u4e4b\u85cd\u7259\u88dd\u7f6e\uff1f" + }, + "user": { + "data": { + "address": "\u85cd\u7259\u4f4d\u5740", + "key": "\u96e2\u7dda\u91d1\u9470\uff0832 \u4f4d\u5143 16 \u9032\u4f4d\u5b57\u4e32\uff09", + "slot": "\u96e2\u7dda\u91d1\u9470\uff080 \u81f3 255 \u9593\u7684\u6574\u6578\uff09" + }, + "description": "\u8acb\u53c3\u8003\u6587\u4ef6 {docs_url} \u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/es.json b/homeassistant/components/yamaha_musiccast/translations/es.json index a83d97db703..e5c24d474b8 100644 --- a/homeassistant/components/yamaha_musiccast/translations/es.json +++ b/homeassistant/components/yamaha_musiccast/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "yxc_control_url_missing": "La URL de control no se proporciona en la descripci\u00f3n del ssdp." + "yxc_control_url_missing": "La URL de control no se proporciona en la descripci\u00f3n de ssdp." }, "error": { "no_musiccast_device": "Este dispositivo no parece ser un dispositivo MusicCast." diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 9056708fd96..0378ee6cdd0 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -3,7 +3,7 @@ "abort": { "not_zha_device": "Este dispositivo no es un dispositivo zha", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", - "usb_probe_failed": "No se ha podido sondear el dispositivo usb" + "usb_probe_failed": "Error al sondear el dispositivo USB" }, "error": { "cannot_connect": "No se pudo conectar" @@ -40,17 +40,17 @@ }, "config_panel": { "zha_alarm_options": { - "alarm_arm_requires_code": "C\u00f3digo requerido para las acciones de armado", - "alarm_failed_tries": "El n\u00famero de entradas de c\u00f3digo fallidas consecutivas para activar una alarma", - "alarm_master_code": "C\u00f3digo maestro de la(s) central(es) de alarma", - "title": "Opciones del panel de control de la alarma" + "alarm_arm_requires_code": "C\u00f3digo requerido para acciones de armado", + "alarm_failed_tries": "El n\u00famero de entradas consecutivas de c\u00f3digos fallidos para disparar una alarma", + "alarm_master_code": "C\u00f3digo maestro para el(los) panel(es) de control de alarma", + "title": "Opciones del panel de control de alarma" }, "zha_options": { "always_prefer_xy_color_mode": "Preferir siempre el modo de color XY", - "consider_unavailable_battery": "Considere que los dispositivos alimentados por bater\u00eda no est\u00e1n disponibles despu\u00e9s de (segundos)", - "consider_unavailable_mains": "Considere que los dispositivos alimentados por la red el\u00e9ctrica no est\u00e1n disponibles despu\u00e9s de (segundos)", - "default_light_transition": "Tiempo de transici\u00f3n de la luz por defecto (segundos)", - "enable_identify_on_join": "Activar el efecto de identificaci\u00f3n cuando los dispositivos se unen a la red", + "consider_unavailable_battery": "Considerar que los dispositivos alimentados por bater\u00eda no est\u00e1n disponibles despu\u00e9s de (segundos)", + "consider_unavailable_mains": "Considerar que los dispositivos alimentados por la red no est\u00e1n disponibles despu\u00e9s de (segundos)", + "default_light_transition": "Tiempo de transici\u00f3n de luz predeterminado (segundos)", + "enable_identify_on_join": "Habilitar el efecto de identificaci\u00f3n cuando los dispositivos se unan a la red", "enhanced_light_transition": "Habilitar la transici\u00f3n mejorada de color de luz/temperatura desde un estado apagado", "light_transitioning_flag": "Habilitar el control deslizante de brillo mejorado durante la transici\u00f3n de luz", "title": "Opciones globales" diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json index 3101d804b43..9e3a2f154bc 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -5,7 +5,7 @@ "addon_info_failed": "No se pudo obtener la informaci\u00f3n del complemento Z-Wave JS.", "addon_install_failed": "No se ha podido instalar el complemento Z-Wave JS.", "addon_set_config_failed": "Fallo en la configuraci\u00f3n de Z-Wave JS.", - "addon_start_failed": "No se ha podido iniciar el complemento Z-Wave JS.", + "addon_start_failed": "No se pudo iniciar el complemento Z-Wave JS.", "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", @@ -21,7 +21,7 @@ "flow_title": "{name}", "progress": { "install_addon": "Espera mientras termina la instalaci\u00f3n del complemento Z-Wave JS. Puede tardar varios minutos.", - "start_addon": "Espere mientras se completa el inicio del complemento Z-Wave JS. Esto puede tardar unos segundos." + "start_addon": "Por favor, espera mientras se completa el inicio del complemento Z-Wave JS. Esto puede tardar unos segundos." }, "step": { "configure_addon": { @@ -54,7 +54,7 @@ "title": "Selecciona el m\u00e9todo de conexi\u00f3n" }, "start_addon": { - "title": "Se est\u00e1 iniciando el complemento Z-Wave JS." + "title": "El complemento Z-Wave JS se est\u00e1 iniciando." }, "usb_confirm": { "description": "\u00bfQuieres configurar {name} con el complemento Z-Wave JS?" @@ -68,12 +68,12 @@ "device_automation": { "action_type": { "clear_lock_usercode": "Borrar c\u00f3digo de usuario en {entity_name}", - "ping": "Ping del dispositivo", - "refresh_value": "Actualizar los valores de {entity_name}", + "ping": "Ping al dispositivo", + "refresh_value": "Actualizar el/los valor(es) para {entity_name}", "reset_meter": "Restablecer contadores en {subtype}", "set_config_parameter": "Establecer el valor del par\u00e1metro de configuraci\u00f3n {subtype}", "set_lock_usercode": "Establecer un c\u00f3digo de usuario en {entity_name}", - "set_value": "Establecer valor de un valor Z-Wave" + "set_value": "Establecer el valor de un valor de Z-Wave" }, "condition_type": { "config_parameter": "Valor del par\u00e1metro de configuraci\u00f3n {subtype}", @@ -88,19 +88,19 @@ "event.value_notification.scene_activation": "Activaci\u00f3n de escena en {subtype}", "state.node_status": "El estado del nodo ha cambiado", "zwave_js.value_updated.config_parameter": "Cambio de valor en el par\u00e1metro de configuraci\u00f3n {subtype}", - "zwave_js.value_updated.value": "Cambio de valor en un valor JS de Z-Wave" + "zwave_js.value_updated.value": "Cambio de valor en un valor Z-Wave JS" } }, "options": { "abort": { - "addon_get_discovery_info_failed": "Fallo en la obtenci\u00f3n de la informaci\u00f3n de descubrimiento del complemento Z-Wave JS.", - "addon_info_failed": "Fallo en la obtenci\u00f3n de la informaci\u00f3n del complemento Z-Wave JS.", - "addon_install_failed": "No se ha podido instalar el complemento Z-Wave JS.", - "addon_set_config_failed": "Fallo en la configuraci\u00f3n de Z-Wave JS.", - "addon_start_failed": "No se ha podido iniciar el complemento Z-Wave JS.", + "addon_get_discovery_info_failed": "No se pudo obtener la informaci\u00f3n de descubrimiento del complemento Z-Wave JS.", + "addon_info_failed": "No se pudo obtener la informaci\u00f3n adicional de Z-Wave JS.", + "addon_install_failed": "No se pudo instalar el complemento Z-Wave JS.", + "addon_set_config_failed": "No se pudo establecer la configuraci\u00f3n de Z-Wave JS.", + "addon_start_failed": "No se pudo iniciar el complemento Z-Wave JS.", "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "different_device": "El dispositivo USB conectado no es el mismo que el configurado anteriormente para esta entrada de configuraci\u00f3n. Por favor, crea una nueva entrada de configuraci\u00f3n para el nuevo dispositivo." + "different_device": "El dispositivo USB conectado no es el mismo que se configur\u00f3 previamente para esta entrada de configuraci\u00f3n. Por favor, en su lugar crea una nueva entrada de configuraci\u00f3n para el nuevo dispositivo." }, "error": { "cannot_connect": "No se pudo conectar", @@ -108,8 +108,8 @@ "unknown": "Error inesperado" }, "progress": { - "install_addon": "Por favor, espera mientras termina la instalaci\u00f3n del complemento Z-Wave JS. Esto puede tardar varios minutos.", - "start_addon": "Por favor, espera mientras se completa el inicio del complemento Z-Wave JS. Esto puede tardar algunos segundos." + "install_addon": "Por favor, espera mientras finaliza la instalaci\u00f3n del complemento Z-Wave JS. Esto puede tardar varios minutos.", + "start_addon": "Por favor, espera mientras se completa el inicio del complemento Z-Wave JS. Esto puede tardar unos segundos." }, "step": { "configure_addon": { @@ -135,9 +135,9 @@ }, "on_supervisor": { "data": { - "use_addon": "Usar el complemento Z-Wave JS Supervisor" + "use_addon": "Usar el complemento Supervisor Z-Wave JS" }, - "description": "\u00bfQuieres utilizar el complemento Z-Wave JS Supervisor?", + "description": "\u00bfQuieres utilizar el complemento Supervisor Z-Wave JS ?", "title": "Selecciona el m\u00e9todo de conexi\u00f3n" }, "start_addon": { diff --git a/homeassistant/components/zwave_me/translations/es.json b/homeassistant/components/zwave_me/translations/es.json index 2443547e59d..ff067220022 100644 --- a/homeassistant/components/zwave_me/translations/es.json +++ b/homeassistant/components/zwave_me/translations/es.json @@ -13,7 +13,7 @@ "token": "Token API", "url": "URL" }, - "description": "Direcci\u00f3n IP de entrada del servidor Z-Way y token de acceso Z-Way. La direcci\u00f3n IP se puede prefijar con wss:// si se debe usar HTTPS en lugar de HTTP. Para obtener el token, vaya a la interfaz de usuario de Z-Way > Configuraci\u00f3n de > de men\u00fa > token de API de > de usuario. Se sugiere crear un nuevo usuario para Home Assistant y conceder acceso a los dispositivos que necesita controlar desde Home Assistant. Tambi\u00e9n es posible utilizar el acceso remoto a trav\u00e9s de find.z-wave.me para conectar un Z-Way remoto. Ingrese wss://find.z-wave.me en el campo IP y copie el token con alcance global (inicie sesi\u00f3n en Z-Way a trav\u00e9s de find.z-wave.me para esto)." + "description": "Introduce la direcci\u00f3n IP con el puerto y el token de acceso del servidor Z-Way. Para obtener el token, ve a la interfaz de usuario de Z-Way Smart Home UI > Menu > Settings > Users > Administrator > API token. \n\nEjemplo de conexi\u00f3n a Z-Way en la red local:\nURL: {local_url}\nFicha: {local_token} \n\nEjemplo de conexi\u00f3n a Z-Way mediante acceso remoto find.z-wave.me:\nURL: {find_url}\nFicha: {find_token} \n\nEjemplo de conexi\u00f3n a Z-Way con una direcci\u00f3n IP p\u00fablica est\u00e1tica:\nURL: {remote_url}\nFicha: {local_token} \n\nAl conectarte a trav\u00e9s de find.z-wave.me, debes usar un token con un alcance global (inicia sesi\u00f3n en Z-Way a trav\u00e9s de find.z-wave.me para esto)." } } } From ae9ab48d05caaaacbd2962755f4506a314241f8b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Aug 2022 15:09:27 -1000 Subject: [PATCH 3293/3516] Downgrade bluetooth_le_tracker timeout message to debug (#76639) Fixes #76558 --- homeassistant/components/bluetooth_le_tracker/device_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index b416fe3e070..7e2c484e1a6 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -144,7 +144,7 @@ async def async_setup_scanner( # noqa: C901 bat_char = await client.read_gatt_char(BATTERY_CHARACTERISTIC_UUID) battery = ord(bat_char) except asyncio.TimeoutError: - _LOGGER.warning( + _LOGGER.debug( "Timeout when trying to get battery status for %s", service_info.name ) # Bleak currently has a few places where checking dbus attributes From 14e6c84104857e25b112c4a71e47d06111ce70f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Aug 2022 15:10:14 -1000 Subject: [PATCH 3294/3516] Bump yalexs-ble to 1.2.0 (#76631) Implements service caching Speeds up lock and unlock operations Changelog: https://github.com/bdraco/yalexs-ble/compare/v1.1.3...v1.2.0 --- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 0bf861e4d44..78abb0fce30 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.1.3"], + "requirements": ["yalexs-ble==1.2.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], diff --git a/requirements_all.txt b/requirements_all.txt index 0e4e9b2c021..72fcba303be 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2500,7 +2500,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.1.3 +yalexs-ble==1.2.0 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 762cdd16472..a8ad45e3251 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1695,7 +1695,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.1.3 +yalexs-ble==1.2.0 # homeassistant.components.august yalexs==1.2.1 From 4a5a0399846f18043a9fb75c685f4509a6c4adc2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Aug 2022 15:10:59 -1000 Subject: [PATCH 3295/3516] Use async_timeout instead of asyncio.wait_for in switchbot (#76630) --- homeassistant/components/switchbot/coordinator.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index ad9aff8c53b..e4e7c25dc70 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -2,9 +2,11 @@ from __future__ import annotations import asyncio +import contextlib import logging from typing import TYPE_CHECKING, Any +import async_timeout import switchbot from homeassistant.components import bluetooth @@ -71,8 +73,8 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator): async def async_wait_ready(self) -> bool: """Wait for the device to be ready.""" - try: - await asyncio.wait_for(self._ready_event.wait(), timeout=55) - except asyncio.TimeoutError: - return False - return True + with contextlib.suppress(asyncio.TimeoutError): + async with async_timeout.timeout(55): + await self._ready_event.wait() + return True + return False From 75ca80428d0eb7c86e61a5ea9fd6d5a476ea49b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Aug 2022 15:12:25 -1000 Subject: [PATCH 3296/3516] Add support for August locks to Yale Access Bluetooth (#76625) --- homeassistant/components/yalexs_ble/manifest.json | 5 ++++- homeassistant/components/yalexs_ble/util.py | 10 +++++++++- homeassistant/generated/supported_brands.py | 1 + 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 78abb0fce30..c4f5b139c86 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -7,5 +7,8 @@ "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], - "iot_class": "local_push" + "iot_class": "local_push", + "supported_brands": { + "august_ble": "August Bluetooth" + } } diff --git a/homeassistant/components/yalexs_ble/util.py b/homeassistant/components/yalexs_ble/util.py index a1c6fdf7d32..465f4487c0b 100644 --- a/homeassistant/components/yalexs_ble/util.py +++ b/homeassistant/components/yalexs_ble/util.py @@ -1,6 +1,8 @@ """The yalexs_ble integration models.""" from __future__ import annotations +import platform + from yalexs_ble import local_name_is_unique from homeassistant.components.bluetooth import ( @@ -23,8 +25,14 @@ def bluetooth_callback_matcher( local_name: str, address: str ) -> BluetoothCallbackMatcher: """Return a BluetoothCallbackMatcher for the given local_name and address.""" - if local_name_is_unique(local_name): + # On MacOS, coreblueooth uses UUIDs for addresses so we must + # have a unique local_name to match since the system + # hides the address from us. + if local_name_is_unique(local_name) and platform.system() == "Darwin": return BluetoothCallbackMatcher({LOCAL_NAME: local_name}) + # On every other platform we actually get the mac address + # which is needed for the older August locks that use the + # older version of the underlying protocol. return BluetoothCallbackMatcher({ADDRESS: address}) diff --git a/homeassistant/generated/supported_brands.py b/homeassistant/generated/supported_brands.py index 4e151f5578d..b4eaa9d8a06 100644 --- a/homeassistant/generated/supported_brands.py +++ b/homeassistant/generated/supported_brands.py @@ -12,5 +12,6 @@ HAS_SUPPORTED_BRANDS = ( "overkiz", "renault", "wemo", + "yalexs_ble", "zwave_js" ) From 3937ac2ca3cb408b67f78519fd6f93adc78de53e Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 11 Aug 2022 21:13:27 -0400 Subject: [PATCH 3297/3516] Track code coverage for ZHA sensor entities (#76617) * Track code coverage for ZHA sensor entities * remove correct entry --- .coveragerc | 1 - homeassistant/components/zha/sensor.py | 6 +++--- tests/components/zha/test_sensor.py | 23 +++++++++++++++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.coveragerc b/.coveragerc index e326f5f44c8..283adf0d3fc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1538,7 +1538,6 @@ omit = homeassistant/components/zha/core/registries.py homeassistant/components/zha/entity.py homeassistant/components/zha/light.py - homeassistant/components/zha/sensor.py homeassistant/components/zhong_hong/climate.py homeassistant/components/ziggo_mediabox_xl/media_player.py homeassistant/components/zoneminder/* diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index f42e88041ef..e0f5bb958cd 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -183,7 +183,7 @@ class Sensor(ZhaEntity, SensorEntity): """Handle state update from channel.""" self.async_write_ha_state() - def formatter(self, value: int) -> int | float: + def formatter(self, value: int) -> int | float | None: """Numeric pass-through formatter.""" if self._decimals > 0: return round( @@ -236,11 +236,11 @@ class Battery(Sensor): return cls(unique_id, zha_device, channels, **kwargs) @staticmethod - def formatter(value: int) -> int: # pylint: disable=arguments-differ + def formatter(value: int) -> int | None: # pylint: disable=arguments-differ """Return the state of the entity.""" # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ if not isinstance(value, numbers.Number) or value == -1: - return value + return None value = round(value / 2) return value diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index d2fc7c3ca73..0698c07db9e 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -255,6 +255,17 @@ async def async_test_powerconfiguration(hass, cluster, entity_id): assert hass.states.get(entity_id).attributes["battery_voltage"] == 2.0 +async def async_test_powerconfiguration2(hass, cluster, entity_id): + """Test powerconfiguration/battery sensor.""" + await send_attributes_report(hass, cluster, {33: -1}) + assert_state(hass, entity_id, STATE_UNKNOWN, "%") + assert hass.states.get(entity_id).attributes["battery_voltage"] == 2.9 + assert hass.states.get(entity_id).attributes["battery_quantity"] == 3 + assert hass.states.get(entity_id).attributes["battery_size"] == "AAA" + await send_attributes_report(hass, cluster, {32: 20}) + assert hass.states.get(entity_id).attributes["battery_voltage"] == 2.0 + + async def async_test_device_temperature(hass, cluster, entity_id): """Test temperature sensor.""" await send_attributes_report(hass, cluster, {0: 2900}) @@ -370,6 +381,18 @@ async def async_test_device_temperature(hass, cluster, entity_id): }, None, ), + ( + general.PowerConfiguration.cluster_id, + "battery", + async_test_powerconfiguration2, + 2, + { + "battery_size": 4, # AAA + "battery_voltage": 29, + "battery_quantity": 3, + }, + None, + ), ( general.DeviceTemperature.cluster_id, "device_temperature", From 828b97f46098c8d55ab46701b5c9d685ad7419ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Aug 2022 16:03:31 -1000 Subject: [PATCH 3298/3516] Bump pySwitchbot to 0.18.5 (#76640) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index b413b44d605..dd04829de15 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.18.4"], + "requirements": ["PySwitchbot==0.18.5"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 72fcba303be..b4d5fe6374a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.4 +PySwitchbot==0.18.5 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a8ad45e3251..8eb0066b32c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.4 +PySwitchbot==0.18.5 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 86b968bf79ffd0392b69fd937227f26444fe117d Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Fri, 12 Aug 2022 10:50:27 +0300 Subject: [PATCH 3299/3516] Migrate Glances to new entity naming style (#76651) * Migrate Glances to new entity naming style * minor fixes --- homeassistant/components/glances/__init__.py | 2 +- .../components/glances/config_flow.py | 12 +- homeassistant/components/glances/const.py | 206 --------------- homeassistant/components/glances/sensor.py | 235 +++++++++++++++++- homeassistant/components/glances/strings.json | 2 - .../components/glances/translations/en.json | 4 +- tests/components/glances/test_config_flow.py | 11 +- 7 files changed, 240 insertions(+), 232 deletions(-) diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index 2b52cdeef8b..0747db89cd2 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -129,7 +129,7 @@ class GlancesData: def get_api(hass, entry): """Return the api from glances_api.""" params = entry.copy() - params.pop(CONF_NAME) + params.pop(CONF_NAME, None) verify_ssl = params.pop(CONF_VERIFY_SSL, True) httpx_client = get_async_client(hass, verify_ssl=verify_ssl) return Glances(httpx_client=httpx_client, **params) diff --git a/homeassistant/components/glances/config_flow.py b/homeassistant/components/glances/config_flow.py index a4a345116eb..16c33182c25 100644 --- a/homeassistant/components/glances/config_flow.py +++ b/homeassistant/components/glances/config_flow.py @@ -1,13 +1,14 @@ """Config flow for Glances.""" from __future__ import annotations +from typing import Any + import glances_api import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import ( CONF_HOST, - CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, @@ -18,10 +19,10 @@ from homeassistant.const import ( from homeassistant.core import callback from . import get_api +from ...data_entry_flow import FlowResult from .const import ( CONF_VERSION, DEFAULT_HOST, - DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DEFAULT_VERSION, @@ -31,7 +32,6 @@ from .const import ( DATA_SCHEMA = vol.Schema( { - vol.Required(CONF_NAME, default=DEFAULT_NAME): str, vol.Required(CONF_HOST, default=DEFAULT_HOST): str, vol.Optional(CONF_USERNAME): str, vol.Optional(CONF_PASSWORD): str, @@ -67,7 +67,7 @@ class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return GlancesOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: """Handle the initial step.""" errors = {} if user_input is not None: @@ -75,7 +75,7 @@ class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: await validate_input(self.hass, user_input) return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input + title=user_input[CONF_HOST], data=user_input ) except CannotConnect: errors["base"] = "cannot_connect" @@ -94,7 +94,7 @@ class GlancesOptionsFlowHandler(config_entries.OptionsFlow): """Initialize Glances options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult: """Manage the Glances options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index d28c7395a43..92fe8ba91f6 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -1,21 +1,11 @@ """Constants for Glances component.""" -from __future__ import annotations -from dataclasses import dataclass import sys -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntityDescription, - SensorStateClass, -) -from homeassistant.const import DATA_GIBIBYTES, DATA_MEBIBYTES, PERCENTAGE, TEMP_CELSIUS - DOMAIN = "glances" CONF_VERSION = "version" DEFAULT_HOST = "localhost" -DEFAULT_NAME = "Glances" DEFAULT_PORT = 61208 DEFAULT_VERSION = 3 DEFAULT_SCAN_INTERVAL = 60 @@ -27,199 +17,3 @@ if sys.maxsize > 2**32: CPU_ICON = "mdi:cpu-64-bit" else: CPU_ICON = "mdi:cpu-32-bit" - - -@dataclass -class GlancesSensorEntityDescription(SensorEntityDescription): - """Describe Glances sensor entity.""" - - type: str | None = None - name_suffix: str | None = None - - -SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( - GlancesSensorEntityDescription( - key="disk_use_percent", - type="fs", - name_suffix="used percent", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:harddisk", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="disk_use", - type="fs", - name_suffix="used", - native_unit_of_measurement=DATA_GIBIBYTES, - icon="mdi:harddisk", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="disk_free", - type="fs", - name_suffix="free", - native_unit_of_measurement=DATA_GIBIBYTES, - icon="mdi:harddisk", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="memory_use_percent", - type="mem", - name_suffix="RAM used percent", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:memory", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="memory_use", - type="mem", - name_suffix="RAM used", - native_unit_of_measurement=DATA_MEBIBYTES, - icon="mdi:memory", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="memory_free", - type="mem", - name_suffix="RAM free", - native_unit_of_measurement=DATA_MEBIBYTES, - icon="mdi:memory", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="swap_use_percent", - type="memswap", - name_suffix="Swap used percent", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:memory", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="swap_use", - type="memswap", - name_suffix="Swap used", - native_unit_of_measurement=DATA_GIBIBYTES, - icon="mdi:memory", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="swap_free", - type="memswap", - name_suffix="Swap free", - native_unit_of_measurement=DATA_GIBIBYTES, - icon="mdi:memory", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="processor_load", - type="load", - name_suffix="CPU load", - icon=CPU_ICON, - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="process_running", - type="processcount", - name_suffix="Running", - icon=CPU_ICON, - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="process_total", - type="processcount", - name_suffix="Total", - icon=CPU_ICON, - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="process_thread", - type="processcount", - name_suffix="Thread", - icon=CPU_ICON, - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="process_sleeping", - type="processcount", - name_suffix="Sleeping", - icon=CPU_ICON, - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="cpu_use_percent", - type="cpu", - name_suffix="CPU used", - native_unit_of_measurement=PERCENTAGE, - icon=CPU_ICON, - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="temperature_core", - type="sensors", - name_suffix="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="temperature_hdd", - type="sensors", - name_suffix="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, - device_class=SensorDeviceClass.TEMPERATURE, - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="fan_speed", - type="sensors", - name_suffix="Fan speed", - native_unit_of_measurement="RPM", - icon="mdi:fan", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="battery", - type="sensors", - name_suffix="Charge", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:battery", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="docker_active", - type="docker", - name_suffix="Containers active", - icon="mdi:docker", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="docker_cpu_use", - type="docker", - name_suffix="Containers CPU used", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:docker", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="docker_memory_use", - type="docker", - name_suffix="Containers RAM used", - native_unit_of_measurement=DATA_MEBIBYTES, - icon="mdi:docker", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="used", - type="raid", - name_suffix="Raid used", - icon="mdi:harddisk", - state_class=SensorStateClass.MEASUREMENT, - ), - GlancesSensorEntityDescription( - key="available", - type="raid", - name_suffix="Raid available", - icon="mdi:harddisk", - state_class=SensorStateClass.MEASUREMENT, - ), -) diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index e37cfaca211..af6f307ef3a 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -1,7 +1,25 @@ """Support gathering system information of hosts which are running glances.""" -from homeassistant.components.sensor import SensorEntity +from __future__ import annotations + +from dataclasses import dataclass + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE, Platform +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + DATA_GIBIBYTES, + DATA_MEBIBYTES, + PERCENTAGE, + STATE_UNAVAILABLE, + TEMP_CELSIUS, + Platform, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -9,7 +27,203 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import GlancesData -from .const import DATA_UPDATED, DOMAIN, SENSOR_TYPES, GlancesSensorEntityDescription +from .const import CPU_ICON, DATA_UPDATED, DOMAIN + + +@dataclass +class GlancesSensorEntityDescription(SensorEntityDescription): + """Describe Glances sensor entity.""" + + type: str | None = None + name_suffix: str | None = None + + +SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( + GlancesSensorEntityDescription( + key="disk_use_percent", + type="fs", + name_suffix="used percent", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:harddisk", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="disk_use", + type="fs", + name_suffix="used", + native_unit_of_measurement=DATA_GIBIBYTES, + icon="mdi:harddisk", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="disk_free", + type="fs", + name_suffix="free", + native_unit_of_measurement=DATA_GIBIBYTES, + icon="mdi:harddisk", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="memory_use_percent", + type="mem", + name_suffix="RAM used percent", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="memory_use", + type="mem", + name_suffix="RAM used", + native_unit_of_measurement=DATA_MEBIBYTES, + icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="memory_free", + type="mem", + name_suffix="RAM free", + native_unit_of_measurement=DATA_MEBIBYTES, + icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="swap_use_percent", + type="memswap", + name_suffix="Swap used percent", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="swap_use", + type="memswap", + name_suffix="Swap used", + native_unit_of_measurement=DATA_GIBIBYTES, + icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="swap_free", + type="memswap", + name_suffix="Swap free", + native_unit_of_measurement=DATA_GIBIBYTES, + icon="mdi:memory", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="processor_load", + type="load", + name_suffix="CPU load", + icon=CPU_ICON, + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="process_running", + type="processcount", + name_suffix="Running", + icon=CPU_ICON, + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="process_total", + type="processcount", + name_suffix="Total", + icon=CPU_ICON, + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="process_thread", + type="processcount", + name_suffix="Thread", + icon=CPU_ICON, + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="process_sleeping", + type="processcount", + name_suffix="Sleeping", + icon=CPU_ICON, + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="cpu_use_percent", + type="cpu", + name_suffix="CPU used", + native_unit_of_measurement=PERCENTAGE, + icon=CPU_ICON, + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="temperature_core", + type="sensors", + name_suffix="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="temperature_hdd", + type="sensors", + name_suffix="Temperature", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="fan_speed", + type="sensors", + name_suffix="Fan speed", + native_unit_of_measurement="RPM", + icon="mdi:fan", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="battery", + type="sensors", + name_suffix="Charge", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:battery", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="docker_active", + type="docker", + name_suffix="Containers active", + icon="mdi:docker", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="docker_cpu_use", + type="docker", + name_suffix="Containers CPU used", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:docker", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="docker_memory_use", + type="docker", + name_suffix="Containers RAM used", + native_unit_of_measurement=DATA_MEBIBYTES, + icon="mdi:docker", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="used", + type="raid", + name_suffix="Raid used", + icon="mdi:harddisk", + state_class=SensorStateClass.MEASUREMENT, + ), + GlancesSensorEntityDescription( + key="available", + type="raid", + name_suffix="Raid available", + icon="mdi:harddisk", + state_class=SensorStateClass.MEASUREMENT, + ), +) async def async_setup_entry( @@ -20,7 +234,7 @@ async def async_setup_entry( """Set up the Glances sensors.""" client: GlancesData = hass.data[DOMAIN][config_entry.entry_id] - name = config_entry.data[CONF_NAME] + name = config_entry.data.get(CONF_NAME) dev = [] @callback @@ -102,11 +316,13 @@ class GlancesSensor(SensorEntity): """Implementation of a Glances sensor.""" entity_description: GlancesSensorEntityDescription + _attr_has_entity_name = True + _attr_should_poll = False def __init__( self, glances_data: GlancesData, - name: str, + name: str | None, sensor_name_prefix: str, description: GlancesSensorEntityDescription, ) -> None: @@ -117,11 +333,11 @@ class GlancesSensor(SensorEntity): self.unsub_update = None self.entity_description = description - self._attr_name = f"{name} {sensor_name_prefix} {description.name_suffix}" + self._attr_name = f"{sensor_name_prefix} {description.name_suffix}" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, glances_data.config_entry.entry_id)}, manufacturer="Glances", - name=name, + name=name or glances_data.config_entry.data[CONF_HOST], ) self._attr_unique_id = f"{self.glances_data.config_entry.entry_id}-{sensor_name_prefix}-{description.key}" @@ -135,11 +351,6 @@ class GlancesSensor(SensorEntity): """Return the state of the resources.""" return self._state - @property - def should_poll(self): - """Return the polling requirement for this sensor.""" - return False - async def async_added_to_hass(self): """Handle entity which will be added.""" self.unsub_update = async_dispatcher_connect( diff --git a/homeassistant/components/glances/strings.json b/homeassistant/components/glances/strings.json index 5d96b1ae57e..11c9792f364 100644 --- a/homeassistant/components/glances/strings.json +++ b/homeassistant/components/glances/strings.json @@ -2,9 +2,7 @@ "config": { "step": { "user": { - "title": "Setup Glances", "data": { - "name": "[%key:common::config_flow::data::name%]", "host": "[%key:common::config_flow::data::host%]", "username": "[%key:common::config_flow::data::username%]", "password": "[%key:common::config_flow::data::password%]", diff --git a/homeassistant/components/glances/translations/en.json b/homeassistant/components/glances/translations/en.json index 87c53c3cf48..aa7005bddea 100644 --- a/homeassistant/components/glances/translations/en.json +++ b/homeassistant/components/glances/translations/en.json @@ -11,15 +11,13 @@ "user": { "data": { "host": "Host", - "name": "Name", "password": "Password", "port": "Port", "ssl": "Uses an SSL certificate", "username": "Username", "verify_ssl": "Verify SSL certificate", "version": "Glances API Version (2 or 3)" - }, - "title": "Setup Glances" + } } } }, diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py index d996c3af533..8ee669ae84e 100644 --- a/tests/components/glances/test_config_flow.py +++ b/tests/components/glances/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import patch from glances_api import exceptions +import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import glances @@ -18,7 +19,6 @@ VERSION = 3 SCAN_INTERVAL = 10 DEMO_USER_INPUT = { - "name": NAME, "host": HOST, "username": USERNAME, "password": PASSWORD, @@ -29,6 +29,13 @@ DEMO_USER_INPUT = { } +@pytest.fixture(autouse=True) +def glances_setup_fixture(): + """Mock transmission entry setup.""" + with patch("homeassistant.components.glances.async_setup_entry", return_value=True): + yield + + async def test_form(hass): """Test config entry configured successfully.""" @@ -45,7 +52,7 @@ async def test_form(hass): ) assert result["type"] == "create_entry" - assert result["title"] == NAME + assert result["title"] == HOST assert result["data"] == DEMO_USER_INPUT From 46369b274b0309518e2dd555bf075379e97cd633 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 12 Aug 2022 09:25:24 +0100 Subject: [PATCH 3300/3516] Initial binary_sensor support for Xiaomi BLE (#76635) --- .../components/xiaomi_ble/__init__.py | 2 +- .../components/xiaomi_ble/binary_sensor.py | 99 +++++++++++++++++++ homeassistant/components/xiaomi_ble/device.py | 31 ++++++ .../components/xiaomi_ble/manifest.json | 2 +- homeassistant/components/xiaomi_ble/sensor.py | 37 ++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../xiaomi_ble/test_binary_sensor.py | 54 ++++++++++ 8 files changed, 194 insertions(+), 35 deletions(-) create mode 100644 homeassistant/components/xiaomi_ble/binary_sensor.py create mode 100644 homeassistant/components/xiaomi_ble/device.py create mode 100644 tests/components/xiaomi_ble/test_binary_sensor.py diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 031490d6d68..626b7325014 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -20,7 +20,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN -PLATFORMS: list[Platform] = [Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py new file mode 100644 index 00000000000..448b1f176e5 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -0,0 +1,99 @@ +"""Support for Xiaomi binary sensors.""" +from __future__ import annotations + +from typing import Optional + +from xiaomi_ble.parser import ( + BinarySensorDeviceClass as XiaomiBinarySensorDeviceClass, + SensorUpdate, +) + +from homeassistant import config_entries +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass + +BINARY_SENSOR_DESCRIPTIONS = { + XiaomiBinarySensorDeviceClass.MOTION: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.MOTION, + device_class=BinarySensorDeviceClass.MOTION, + ), + XiaomiBinarySensorDeviceClass.LIGHT: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.LIGHT, + device_class=BinarySensorDeviceClass.LIGHT, + ), + XiaomiBinarySensorDeviceClass.SMOKE: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.SMOKE, + device_class=BinarySensorDeviceClass.SMOKE, + ), +} + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + device_key_to_bluetooth_entity_key(device_key): BINARY_SENSOR_DESCRIPTIONS[ + description.device_class + ] + for device_key, description in sensor_update.binary_entity_descriptions.items() + if description.device_class + }, + entity_data={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.binary_entity_values.items() + }, + entity_names={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.binary_entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Xiaomi BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + entry.async_on_unload( + processor.async_add_entities_listener( + XiaomiBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class XiaomiBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[Optional[bool]]], + BinarySensorEntity, +): + """Representation of a Xiaomi binary sensor.""" + + @property + def is_on(self) -> bool | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/xiaomi_ble/device.py b/homeassistant/components/xiaomi_ble/device.py new file mode 100644 index 00000000000..4ddfc31ae51 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/device.py @@ -0,0 +1,31 @@ +"""Support for Xioami BLE devices.""" +from __future__ import annotations + +from xiaomi_ble import DeviceKey, SensorDeviceInfo + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothEntityKey, +) +from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME +from homeassistant.helpers.entity import DeviceInfo + + +def device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a sensor device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 8c1d47ee423..e93dace95c6 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -8,7 +8,7 @@ "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.8.2"], + "requirements": ["xiaomi-ble==0.9.0"], "dependencies": ["bluetooth"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index d22ed46dd83..1aa1b7f8f72 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -3,13 +3,12 @@ from __future__ import annotations from typing import Optional, Union -from xiaomi_ble import DeviceClass, DeviceKey, SensorDeviceInfo, SensorUpdate, Units +from xiaomi_ble import DeviceClass, SensorUpdate, Units from homeassistant import config_entries from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, - PassiveBluetoothEntityKey, PassiveBluetoothProcessorCoordinator, PassiveBluetoothProcessorEntity, ) @@ -20,9 +19,6 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.const import ( - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONDUCTIVITY, ELECTRIC_POTENTIAL_VOLT, @@ -33,10 +29,10 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN +from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass SENSOR_DESCRIPTIONS = { (DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription( @@ -108,49 +104,28 @@ SENSOR_DESCRIPTIONS = { } -def _device_key_to_bluetooth_entity_key( - device_key: DeviceKey, -) -> PassiveBluetoothEntityKey: - """Convert a device key to an entity key.""" - return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) - - -def _sensor_device_info_to_hass( - sensor_device_info: SensorDeviceInfo, -) -> DeviceInfo: - """Convert a sensor device info to a sensor device info.""" - hass_device_info = DeviceInfo({}) - if sensor_device_info.name is not None: - hass_device_info[ATTR_NAME] = sensor_device_info.name - if sensor_device_info.manufacturer is not None: - hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer - if sensor_device_info.model is not None: - hass_device_info[ATTR_MODEL] = sensor_device_info.model - return hass_device_info - - def sensor_update_to_bluetooth_data_update( sensor_update: SensorUpdate, ) -> PassiveBluetoothDataUpdate: """Convert a sensor update to a bluetooth data update.""" return PassiveBluetoothDataUpdate( devices={ - device_id: _sensor_device_info_to_hass(device_info) + device_id: sensor_device_info_to_hass(device_info) for device_id, device_info in sensor_update.devices.items() }, entity_descriptions={ - _device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ (description.device_class, description.native_unit_of_measurement) ] for device_key, description in sensor_update.entity_descriptions.items() if description.native_unit_of_measurement }, entity_data={ - _device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value for device_key, sensor_values in sensor_update.entity_values.items() }, entity_names={ - _device_key_to_bluetooth_entity_key(device_key): sensor_values.name + device_key_to_bluetooth_entity_key(device_key): sensor_values.name for device_key, sensor_values in sensor_update.entity_values.items() }, ) diff --git a/requirements_all.txt b/requirements_all.txt index b4d5fe6374a..a356cec0af6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2480,7 +2480,7 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.8.2 +xiaomi-ble==0.9.0 # homeassistant.components.knx xknx==0.22.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8eb0066b32c..7cbef52b7ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1678,7 +1678,7 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.8.2 +xiaomi-ble==0.9.0 # homeassistant.components.knx xknx==0.22.1 diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py new file mode 100644 index 00000000000..03e3d52d783 --- /dev/null +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -0,0 +1,54 @@ +"""Test Xiaomi binary sensors.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.xiaomi_ble.const import DOMAIN +from homeassistant.const import ATTR_FRIENDLY_NAME + +from . import make_advertisement + +from tests.common import MockConfigEntry + + +async def test_smoke_sensor(hass): + """Test setting up a smoke sensor.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + data={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + saved_callback( + make_advertisement( + "54:EF:44:E3:9C:BC", + b"XY\x97\tf\xbc\x9c\xe3D\xefT\x01" b"\x08\x12\x05\x00\x00\x00q^\xbe\x90", + ), + BluetoothChange.ADVERTISEMENT, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + smoke_sensor = hass.states.get("binary_sensor.thermometer_e39cbc_smoke") + smoke_sensor_attribtes = smoke_sensor.attributes + assert smoke_sensor.state == "on" + assert smoke_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Thermometer E39CBC Smoke" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From cafc6ca89511bd0a576b618216415f8068c1133e Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Fri, 12 Aug 2022 12:42:42 +0300 Subject: [PATCH 3301/3516] Fix typing in `glances` config flow (#76654) fix typing in config_flow --- homeassistant/components/glances/config_flow.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/glances/config_flow.py b/homeassistant/components/glances/config_flow.py index 16c33182c25..568586f177b 100644 --- a/homeassistant/components/glances/config_flow.py +++ b/homeassistant/components/glances/config_flow.py @@ -17,9 +17,9 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from . import get_api -from ...data_entry_flow import FlowResult from .const import ( CONF_VERSION, DEFAULT_HOST, @@ -67,7 +67,9 @@ class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return GlancesOptionsFlowHandler(config_entry) - async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" errors = {} if user_input is not None: @@ -94,7 +96,9 @@ class GlancesOptionsFlowHandler(config_entries.OptionsFlow): """Initialize Glances options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the Glances options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) From 795e7f570791c329383ce5a4e752ba9d7545da2c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 12 Aug 2022 02:58:48 -1000 Subject: [PATCH 3302/3516] Bump pySwitchbot to 0.18.6 to fix disconnect race (#76656) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index dd04829de15..e26100108c9 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.18.5"], + "requirements": ["PySwitchbot==0.18.6"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index a356cec0af6..0332c7c06e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.5 +PySwitchbot==0.18.6 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7cbef52b7ed..a20e82bffce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.5 +PySwitchbot==0.18.6 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 563e899d2e2bc8c97444396a55bd7f7329e480bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 12 Aug 2022 02:58:59 -1000 Subject: [PATCH 3303/3516] Bump yalexs_ble to 1.3.1 to fix disconnect race (#76657) --- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index c4f5b139c86..aa3cdcd592b 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.2.0"], + "requirements": ["yalexs-ble==1.3.1"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], diff --git a/requirements_all.txt b/requirements_all.txt index 0332c7c06e9..35a231a8cd4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2500,7 +2500,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.2.0 +yalexs-ble==1.3.1 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a20e82bffce..011c93291e8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1695,7 +1695,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.2.0 +yalexs-ble==1.3.1 # homeassistant.components.august yalexs==1.2.1 From a86397cc10212a9bf0acac442736909076acf8e3 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Fri, 12 Aug 2022 15:19:16 +0200 Subject: [PATCH 3304/3516] Fix non-awaited coroutine in BMW notify (#76664) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/notify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/notify.py b/homeassistant/components/bmw_connected_drive/notify.py index 14f6c94dff6..b48441ae5fe 100644 --- a/homeassistant/components/bmw_connected_drive/notify.py +++ b/homeassistant/components/bmw_connected_drive/notify.py @@ -56,7 +56,7 @@ class BMWNotificationService(BaseNotificationService): """Set up the notification service.""" self.targets: dict[str, MyBMWVehicle] = targets - def send_message(self, message: str = "", **kwargs: Any) -> None: + async def async_send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message or POI to the car.""" for vehicle in kwargs[ATTR_TARGET]: vehicle = cast(MyBMWVehicle, vehicle) @@ -81,6 +81,6 @@ class BMWNotificationService(BaseNotificationService): } ) - vehicle.remote_services.trigger_send_poi(location_dict) + await vehicle.remote_services.trigger_send_poi(location_dict) else: raise ValueError(f"'data.{ATTR_LOCATION}' is required.") From eeb9a9f0584c99eb74cc34dd7fe6fd666cc7f16f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 12 Aug 2022 03:25:23 -1000 Subject: [PATCH 3305/3516] Make sure all discovery flows are using the helper (#76641) --- .../components/bluetooth/__init__.py | 11 ++++---- .../components/discovery/__init__.py | 4 ++- homeassistant/components/elkm1/discovery.py | 13 +++++----- homeassistant/components/ezviz/camera.py | 23 +++++++++-------- .../components/flux_led/discovery.py | 13 +++++----- homeassistant/components/hassio/discovery.py | 4 ++- homeassistant/components/lifx/discovery.py | 12 ++++----- homeassistant/components/mqtt/__init__.py | 18 +++++++------ homeassistant/components/plex/config_flow.py | 4 ++- homeassistant/components/senseme/discovery.py | 12 ++++----- .../components/squeezebox/media_player.py | 25 +++++++++++-------- .../components/steamist/discovery.py | 23 ++++++++--------- homeassistant/components/tplink/__init__.py | 21 ++++++++-------- .../components/unifiprotect/discovery.py | 12 ++++----- homeassistant/components/wiz/discovery.py | 12 ++++----- .../components/yalexs_ble/__init__.py | 12 ++++----- homeassistant/components/yeelight/scanner.py | 22 ++++++++-------- tests/components/hassio/test_discovery.py | 7 ++++-- tests/components/plex/test_config_flow.py | 1 + 19 files changed, 132 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 9492642e0e0..a5204d50b68 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -249,12 +249,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) ) elif await _async_has_bluetooth_adapter(): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={}, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={}, ) return True diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index cc104cc2110..75016c28048 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -13,6 +13,7 @@ from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import discovery_flow import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_discover, async_load_platform from homeassistant.helpers.event import async_track_point_in_utc_time @@ -173,7 +174,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: already_discovered.add(discovery_hash) if service in CONFIG_ENTRY_HANDLERS: - await hass.config_entries.flow.async_init( + discovery_flow.async_create_flow( + hass, CONFIG_ENTRY_HANDLERS[service], context={"source": config_entries.SOURCE_DISCOVERY}, data=info, diff --git a/homeassistant/components/elkm1/discovery.py b/homeassistant/components/elkm1/discovery.py index 326698c3686..50db2840753 100644 --- a/homeassistant/components/elkm1/discovery.py +++ b/homeassistant/components/elkm1/discovery.py @@ -10,7 +10,7 @@ from elkm1_lib.discovery import AIOELKDiscovery, ElkSystem from homeassistant import config_entries from homeassistant.components import network from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, discovery_flow from .const import DISCOVER_SCAN_TIMEOUT, DOMAIN @@ -87,10 +87,9 @@ def async_trigger_discovery( ) -> None: """Trigger config flows for discovered devices.""" for device in discovered_devices: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data=asdict(device), - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=asdict(device), ) diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 91bab5f83af..307e1fac185 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -17,7 +17,11 @@ from homeassistant.config_entries import ( ) from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers import ( + config_validation as cv, + discovery_flow, + entity_platform, +) from .const import ( ATTR_DIRECTION, @@ -93,15 +97,14 @@ async def async_setup_entry( else: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_INTEGRATION_DISCOVERY}, - data={ - ATTR_SERIAL: camera, - CONF_IP_ADDRESS: value["local_ip"], - }, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": SOURCE_INTEGRATION_DISCOVERY}, + data={ + ATTR_SERIAL: camera, + CONF_IP_ADDRESS: value["local_ip"], + }, ) _LOGGER.warning( diff --git a/homeassistant/components/flux_led/discovery.py b/homeassistant/components/flux_led/discovery.py index 67dbbc74e2e..ef0c131993e 100644 --- a/homeassistant/components/flux_led/discovery.py +++ b/homeassistant/components/flux_led/discovery.py @@ -27,7 +27,7 @@ from homeassistant.components import network from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, discovery_flow from homeassistant.util.network import is_ip_address from .const import ( @@ -221,10 +221,9 @@ def async_trigger_discovery( ) -> None: """Trigger config flows for discovered devices.""" for device in discovered_devices: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={**device}, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={**device}, ) diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index 587457f2ca2..e8cbbfc6bf5 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -14,6 +14,7 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.const import ATTR_NAME, ATTR_SERVICE, EVENT_HOMEASSISTANT_START from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import BaseServiceInfo +from homeassistant.helpers import discovery_flow from .const import ATTR_ADDON, ATTR_CONFIG, ATTR_DISCOVERY, ATTR_UUID from .handler import HassioAPIError @@ -99,7 +100,8 @@ class HassIODiscovery(HomeAssistantView): config_data[ATTR_ADDON] = addon_info[ATTR_NAME] # Use config flow - await self.hass.config_entries.flow.async_init( + discovery_flow.async_create_flow( + self.hass, service, context={"source": config_entries.SOURCE_HASSIO}, data=HassioServiceInfo(config=config_data), diff --git a/homeassistant/components/lifx/discovery.py b/homeassistant/components/lifx/discovery.py index 1c6e9ab3060..6e1507c92ca 100644 --- a/homeassistant/components/lifx/discovery.py +++ b/homeassistant/components/lifx/discovery.py @@ -10,6 +10,7 @@ from homeassistant import config_entries from homeassistant.components import network from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import discovery_flow from .const import CONF_SERIAL, DOMAIN @@ -38,12 +39,11 @@ async def async_discover_devices(hass: HomeAssistant) -> Iterable[Light]: @callback def async_init_discovery_flow(hass: HomeAssistant, host: str, serial: str) -> None: """Start discovery of devices.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={CONF_HOST: host, CONF_SERIAL: serial}, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={CONF_HOST: host, CONF_SERIAL: serial}, ) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 2bea1a593d1..1121377a30e 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -22,7 +22,12 @@ from homeassistant.const import ( ) from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import TemplateError, Unauthorized -from homeassistant.helpers import config_validation as cv, event, template +from homeassistant.helpers import ( + config_validation as cv, + discovery_flow, + event, + template, +) from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import ( @@ -178,12 +183,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # Create an import flow if the user has yaml configured entities etc. # but no broker configuration. Note: The intention is not for this to # import broker configuration from YAML because that has been deprecated. - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={}, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={}, ) hass.data[DATA_MQTT_RELOAD_NEEDED] = True elif mqtt_entry_status is False: diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index 42d227154a6..e79b7e7ee04 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -29,6 +29,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import discovery_flow from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -76,7 +77,8 @@ async def async_discover(hass): gdm = GDM() await hass.async_add_executor_job(gdm.scan) for server_data in gdm.entries: - await hass.config_entries.flow.async_init( + discovery_flow.async_create_flow( + hass, DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=server_data, diff --git a/homeassistant/components/senseme/discovery.py b/homeassistant/components/senseme/discovery.py index 624b18a8761..d3924ef16c0 100644 --- a/homeassistant/components/senseme/discovery.py +++ b/homeassistant/components/senseme/discovery.py @@ -8,6 +8,7 @@ from aiosenseme import SensemeDevice, SensemeDiscovery from homeassistant import config_entries from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import discovery_flow from .const import DISCOVERY, DOMAIN @@ -55,10 +56,9 @@ def async_trigger_discovery( """Trigger config flows for discovered devices.""" for device in discovered_devices: if device.uuid: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={CONF_ID: device.uuid}, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={CONF_ID: device.uuid}, ) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index cd628a639c5..260228e4fdf 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -39,7 +39,11 @@ from homeassistant.const import ( STATE_PLAYING, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers import ( + config_validation as cv, + discovery_flow, + entity_platform, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.dispatcher import ( @@ -99,16 +103,15 @@ async def start_server_discovery(hass): """Start a server discovery task.""" def _discovered_server(server): - asyncio.create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_INTEGRATION_DISCOVERY}, - data={ - CONF_HOST: server.host, - CONF_PORT: int(server.port), - "uuid": server.uuid, - }, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": SOURCE_INTEGRATION_DISCOVERY}, + data={ + CONF_HOST: server.host, + CONF_PORT: int(server.port), + "uuid": server.uuid, + }, ) hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/steamist/discovery.py b/homeassistant/components/steamist/discovery.py index 7600503658f..cff97692979 100644 --- a/homeassistant/components/steamist/discovery.py +++ b/homeassistant/components/steamist/discovery.py @@ -11,7 +11,7 @@ from homeassistant import config_entries from homeassistant.components import network from homeassistant.const import CONF_MODEL, CONF_NAME from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, discovery_flow from homeassistant.util.network import is_ip_address from .const import DISCOVER_SCAN_TIMEOUT, DISCOVERY, DOMAIN @@ -122,15 +122,14 @@ def async_trigger_discovery( ) -> None: """Trigger config flows for discovered devices.""" for device in discovered_devices: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={ - "ipaddress": device.ipaddress, - "name": device.name, - "mac": device.mac, - "hostname": device.hostname, - }, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "ipaddress": device.ipaddress, + "name": device.name, + "mac": device.mac, + "hostname": device.hostname, + }, ) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 50c18000baa..9606dc29a44 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -19,7 +19,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import device_registry as dr, discovery_flow from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType @@ -36,16 +36,15 @@ def async_trigger_discovery( ) -> None: """Trigger config flows for discovered devices.""" for formatted_mac, device in discovered_devices.items(): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={ - CONF_NAME: device.alias, - CONF_HOST: device.host, - CONF_MAC: formatted_mac, - }, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + CONF_NAME: device.alias, + CONF_HOST: device.host, + CONF_MAC: formatted_mac, + }, ) diff --git a/homeassistant/components/unifiprotect/discovery.py b/homeassistant/components/unifiprotect/discovery.py index 537e2fa1121..d58cad4e40a 100644 --- a/homeassistant/components/unifiprotect/discovery.py +++ b/homeassistant/components/unifiprotect/discovery.py @@ -11,6 +11,7 @@ from unifi_discovery import AIOUnifiScanner, UnifiDevice, UnifiService from homeassistant import config_entries from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval from .const import DOMAIN @@ -54,10 +55,9 @@ def async_trigger_discovery( """Trigger config flows for discovered devices.""" for device in discovered_devices: if device.services[UnifiService.Protect] and device.hw_addr: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data=asdict(device), - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=asdict(device), ) diff --git a/homeassistant/components/wiz/discovery.py b/homeassistant/components/wiz/discovery.py index 0b7015643ff..0f4be1d873e 100644 --- a/homeassistant/components/wiz/discovery.py +++ b/homeassistant/components/wiz/discovery.py @@ -10,6 +10,7 @@ from pywizlight.discovery import DiscoveredBulb, find_wizlights from homeassistant import config_entries from homeassistant.components import network from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import discovery_flow from .const import DOMAIN @@ -46,10 +47,9 @@ def async_trigger_discovery( ) -> None: """Trigger config flows for discovered devices.""" for device in discovered_devices: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data=asdict(device), - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data=asdict(device), ) diff --git a/homeassistant/components/yalexs_ble/__init__.py b/homeassistant/components/yalexs_ble/__init__.py index 3b9481b6982..6073bf7a032 100644 --- a/homeassistant/components/yalexs_ble/__init__.py +++ b/homeassistant/components/yalexs_ble/__init__.py @@ -12,6 +12,7 @@ from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEnt from homeassistant.const import CONF_ADDRESS, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import discovery_flow from .const import CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DEVICE_TIMEOUT, DOMAIN from .models import YaleXSBLEData @@ -33,12 +34,11 @@ class YaleXSBLEDiscovery(TypedDict): @callback def async_discovery(hass: HomeAssistant, discovery: YaleXSBLEDiscovery) -> None: """Update keys for the yalexs-ble integration if available.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - "yalexs_ble", - context={"source": SOURCE_INTEGRATION_DISCOVERY}, - data=discovery, - ) + discovery_flow.async_create_flow( + hass, + DOMAIN, + context={"source": SOURCE_INTEGRATION_DISCOVERY}, + data=discovery, ) diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py index 088071244b3..0b33512e315 100644 --- a/homeassistant/components/yeelight/scanner.py +++ b/homeassistant/components/yeelight/scanner.py @@ -15,6 +15,7 @@ from async_upnp_client.utils import CaseInsensitiveDict from homeassistant import config_entries from homeassistant.components import network, ssdp from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_call_later, async_track_time_interval from .const import ( @@ -161,17 +162,16 @@ class YeelightScanner: def _async_discovered_by_ssdp(self, response: CaseInsensitiveDict) -> None: @callback def _async_start_flow(*_) -> None: - asyncio.create_task( - self._hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="", - ssdp_st=SSDP_ST, - ssdp_headers=response, - upnp={}, - ), - ) + discovery_flow.async_create_flow( + self._hass, + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=ssdp.SsdpServiceInfo( + ssdp_usn="", + ssdp_st=SSDP_ST, + ssdp_headers=response, + upnp={}, + ), ) # Delay starting the flow in case the discovery is the result diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index 71f5f8acf96..30013c34f21 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -4,7 +4,7 @@ from unittest.mock import Mock, patch from homeassistant.components.hassio import HassioServiceInfo from homeassistant.components.hassio.handler import HassioAPIError -from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED from homeassistant.setup import async_setup_component @@ -45,7 +45,8 @@ async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): ) as mock_mqtt: hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() assert aioclient_mock.call_count == 2 assert mock_mqtt.called mock_mqtt.assert_called_with( @@ -159,6 +160,8 @@ async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client): json={"addon": "mosquitto", "service": "mqtt", "uuid": "testuuid"}, ) await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() assert resp.status == HTTPStatus.OK assert aioclient_mock.call_count == 2 diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index f02abd834d7..fb5a0f06724 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -703,6 +703,7 @@ async def test_integration_discovery(hass): with patch("homeassistant.components.plex.config_flow.GDM", return_value=mock_gdm): await config_flow.async_discover(hass) + await hass.async_block_till_done() flows = hass.config_entries.flow.async_progress() From 0293db343fd364ed64d7b6c31d1683f7bdcc8087 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Fri, 12 Aug 2022 15:45:28 +0200 Subject: [PATCH 3306/3516] Allow only known attrs for BMW binary sensors (#76663) Co-authored-by: Paulus Schoutsen Co-authored-by: rikroe --- .../bmw_connected_drive/binary_sensor.py | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 94d8902fe23..96e8a1fc0e4 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -28,11 +28,32 @@ from .coordinator import BMWDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) +ALLOWED_CONDITION_BASED_SERVICE_KEYS = { + "BRAKE_FLUID", + "ENGINE_OIL", + "OIL", + "TIRE_WEAR_FRONT", + "TIRE_WEAR_REAR", + "VEHICLE_CHECK", + "VEHICLE_TUV", +} + +ALLOWED_CHECK_CONTROL_MESSAGE_KEYS = {"ENGINE_OIL", "TIRE_PRESSURE"} + + def _condition_based_services( vehicle: MyBMWVehicle, unit_system: UnitSystem ) -> dict[str, Any]: extra_attributes = {} for report in vehicle.condition_based_services.messages: + if report.service_type not in ALLOWED_CONDITION_BASED_SERVICE_KEYS: + _LOGGER.warning( + "'%s' not an allowed condition based service (%s)", + report.service_type, + report, + ) + continue + extra_attributes.update(_format_cbs_report(report, unit_system)) return extra_attributes @@ -40,7 +61,17 @@ def _condition_based_services( def _check_control_messages(vehicle: MyBMWVehicle) -> dict[str, Any]: extra_attributes: dict[str, Any] = {} for message in vehicle.check_control_messages.messages: - extra_attributes.update({message.description_short: message.state.value}) + if message.description_short not in ALLOWED_CHECK_CONTROL_MESSAGE_KEYS: + _LOGGER.warning( + "'%s' not an allowed check control message (%s)", + message.description_short, + message, + ) + continue + + extra_attributes.update( + {message.description_short.lower(): message.state.value} + ) return extra_attributes @@ -48,10 +79,10 @@ def _format_cbs_report( report: ConditionBasedService, unit_system: UnitSystem ) -> dict[str, Any]: result: dict[str, Any] = {} - service_type = report.service_type.lower().replace("_", " ") - result[f"{service_type} status"] = report.state.value + service_type = report.service_type.lower() + result[service_type] = report.state.value if report.due_date is not None: - result[f"{service_type} date"] = report.due_date.strftime("%Y-%m-%d") + result[f"{service_type}_date"] = report.due_date.strftime("%Y-%m-%d") if report.due_distance.value and report.due_distance.unit: distance = round( unit_system.length( @@ -59,7 +90,7 @@ def _format_cbs_report( UNIT_MAP.get(report.due_distance.unit, report.due_distance.unit), ) ) - result[f"{service_type} distance"] = f"{distance} {unit_system.length_unit}" + result[f"{service_type}_distance"] = f"{distance} {unit_system.length_unit}" return result From e033c8b38037438a98f4a6cbc07fb6efbccbb0ba Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 12 Aug 2022 13:14:41 -0400 Subject: [PATCH 3307/3516] Migrate Abode to new entity naming style (#76673) --- homeassistant/components/abode/__init__.py | 2 +- homeassistant/components/abode/sensor.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index bd66aa66c9c..092f9d36071 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -248,6 +248,7 @@ class AbodeEntity(entity.Entity): """Representation of an Abode entity.""" _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True def __init__(self, data: AbodeSystem) -> None: """Initialize Abode entity.""" @@ -283,7 +284,6 @@ class AbodeDevice(AbodeEntity): """Initialize Abode device.""" super().__init__(data) self._device = device - self._attr_name = device.name self._attr_unique_id = device.device_uuid async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index da854153293..7fd3a0280a1 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -64,7 +64,6 @@ class AbodeSensor(AbodeDevice, SensorEntity): """Initialize a sensor for an Abode device.""" super().__init__(data, device) self.entity_description = description - self._attr_name = f"{device.name} {description.name}" self._attr_unique_id = f"{device.device_uuid}-{description.key}" if description.key == CONST.TEMP_STATUS_KEY: self._attr_native_unit_of_measurement = device.temp_unit From 2e40fc72883ec366fdb17504e486aef8ea2ff563 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 13 Aug 2022 01:39:12 +0200 Subject: [PATCH 3308/3516] Bump motionblinds to 0.6.12 (#76665) --- homeassistant/components/motion_blinds/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 3499932c1d8..90ad330bd40 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,7 +3,7 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.6.11"], + "requirements": ["motionblinds==0.6.12"], "dependencies": ["network"], "dhcp": [ { "registered_devices": true }, diff --git a/requirements_all.txt b/requirements_all.txt index 35a231a8cd4..9ec008d1a45 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1055,7 +1055,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.11 +motionblinds==0.6.12 # homeassistant.components.motioneye motioneye-client==0.3.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 011c93291e8..09d654157e5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -751,7 +751,7 @@ moat-ble==0.1.1 moehlenhoff-alpha2==1.2.1 # homeassistant.components.motion_blinds -motionblinds==0.6.11 +motionblinds==0.6.12 # homeassistant.components.motioneye motioneye-client==0.3.12 From 2f29f38ec6f5620412442dabdf4ce7fdfbf498bc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 12 Aug 2022 19:42:41 -0400 Subject: [PATCH 3309/3516] Streamline discovery flow callback (#76666) --- homeassistant/helpers/discovery_flow.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/helpers/discovery_flow.py b/homeassistant/helpers/discovery_flow.py index 5bb0da2dc05..863fb58625c 100644 --- a/homeassistant/helpers/discovery_flow.py +++ b/homeassistant/helpers/discovery_flow.py @@ -60,17 +60,14 @@ class FlowDispatcher: @callback def async_setup(self) -> None: """Set up the flow disptcher.""" - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, self.async_start) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, self._async_start) - @callback - def async_start(self, event: Event) -> None: + async def _async_start(self, event: Event) -> None: """Start processing pending flows.""" self.hass.data.pop(DISCOVERY_FLOW_DISPATCHER) - self.hass.async_create_task(self._async_process_pending_flows()) - async def _async_process_pending_flows(self) -> None: - """Process any pending discovery flows.""" init_coros = [_async_init_flow(self.hass, *flow) for flow in self.pending_flows] + await gather_with_concurrency( FLOW_INIT_LIMIT, *[init_coro for init_coro in init_coros if init_coro is not None], From 6e03b12a93a9287b60bb8a52c0c590226637e3d1 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 13 Aug 2022 00:25:00 +0000 Subject: [PATCH 3310/3516] [ci skip] Translation update --- .../accuweather/translations/es.json | 2 +- .../components/airly/translations/es.json | 2 +- .../components/airnow/translations/es.json | 2 +- .../components/airvisual/translations/es.json | 2 +- .../components/almond/translations/es.json | 2 +- .../ambiclimate/translations/es.json | 4 +-- .../components/apple_tv/translations/es.json | 6 ++-- .../components/arcam_fmj/translations/es.json | 2 +- .../components/asuswrt/translations/es.json | 8 ++--- .../components/atag/translations/es.json | 2 +- .../components/auth/translations/es.json | 6 ++-- .../automation/translations/es.json | 2 +- .../components/awair/translations/ca.json | 3 ++ .../components/awair/translations/el.json | 16 +++++++++- .../components/awair/translations/es.json | 2 +- .../components/awair/translations/et.json | 2 +- .../components/awair/translations/fr.json | 31 +++++++++++++++++-- .../components/awair/translations/hu.json | 31 +++++++++++++++++-- .../components/awair/translations/pt-BR.json | 31 +++++++++++++++++-- .../awair/translations/zh-Hant.json | 31 +++++++++++++++++-- .../components/axis/translations/es.json | 6 ++-- .../azure_devops/translations/es.json | 2 +- .../components/blink/translations/es.json | 4 +-- .../components/braviatv/translations/es.json | 2 +- .../components/broadlink/translations/es.json | 8 ++--- .../components/bsblan/translations/es.json | 2 +- .../cloudflare/translations/es.json | 2 +- .../components/daikin/translations/es.json | 2 +- .../components/deconz/translations/es.json | 8 ++--- .../components/denonavr/translations/es.json | 2 +- .../dialogflow/translations/es.json | 2 +- .../components/directv/translations/es.json | 2 +- .../components/doorbird/translations/es.json | 2 +- .../components/dsmr/translations/es.json | 2 -- .../components/dunehd/translations/es.json | 2 +- .../components/ecobee/translations/es.json | 2 +- .../components/elgato/translations/es.json | 2 +- .../components/elkm1/translations/es.json | 2 +- .../emulated_roku/translations/es.json | 4 +-- .../components/esphome/translations/es.json | 6 ++-- .../fireservicerota/translations/es.json | 2 +- .../components/flume/translations/es.json | 2 +- .../flunearyou/translations/es.json | 2 +- .../forked_daapd/translations/es.json | 2 +- .../components/freebox/translations/es.json | 4 +-- .../components/fritz/translations/es.json | 2 +- .../components/fritzbox/translations/es.json | 6 ++-- .../components/geofency/translations/es.json | 2 +- .../components/glances/translations/en.json | 4 ++- .../components/goalzero/translations/es.json | 2 +- .../components/gogogate2/translations/es.json | 2 +- .../components/gpslogger/translations/es.json | 2 +- .../components/guardian/translations/el.json | 13 ++++++++ .../components/guardian/translations/es.json | 2 +- .../components/hangouts/translations/es.json | 10 +++--- .../components/heos/translations/es.json | 2 +- .../components/homekit/translations/es.json | 12 +++---- .../homekit_controller/translations/es.json | 18 +++++------ .../homematicip_cloud/translations/es.json | 14 ++++----- .../huawei_lte/translations/es.json | 2 +- .../components/hue/translations/es.json | 6 ++-- .../translations/es.json | 2 +- .../components/iaqualink/translations/es.json | 2 +- .../components/icloud/translations/es.json | 2 +- .../components/ifttt/translations/es.json | 4 +-- .../components/insteon/translations/es.json | 4 +-- .../components/ipp/translations/es.json | 4 +-- .../components/iqvia/translations/es.json | 2 +- .../components/isy994/translations/es.json | 4 +-- .../components/kodi/translations/es.json | 4 +-- .../components/konnected/translations/es.json | 26 ++++++++-------- .../components/locative/translations/es.json | 4 +-- .../logi_circle/translations/es.json | 8 ++--- .../lutron_caseta/translations/es.json | 2 +- .../components/mailgun/translations/es.json | 2 +- .../components/melcloud/translations/es.json | 2 +- .../meteo_france/translations/es.json | 2 +- .../components/mikrotik/translations/es.json | 2 +- .../minecraft_server/translations/es.json | 2 +- .../mobile_app/translations/es.json | 2 +- .../motion_blinds/translations/es.json | 2 +- .../components/mqtt/translations/es.json | 8 ++--- .../components/myq/translations/es.json | 4 +-- .../components/nanoleaf/translations/es.json | 2 +- .../components/nest/translations/es.json | 4 +-- .../components/netatmo/translations/es.json | 8 ++--- .../components/nexia/translations/es.json | 4 +-- .../nightscout/translations/es.json | 2 +- .../components/nws/translations/es.json | 2 +- .../components/onewire/translations/es.json | 2 +- .../components/onvif/translations/es.json | 4 +-- .../components/owntracks/translations/es.json | 2 +- .../components/pi_hole/translations/es.json | 4 +-- .../components/plaato/translations/es.json | 2 +- .../components/plant/translations/es.json | 2 +- .../components/plex/translations/es.json | 4 +-- .../components/plugwise/translations/es.json | 6 ++-- .../components/point/translations/es.json | 4 +-- .../components/profiler/translations/es.json | 2 +- .../components/ps4/translations/es.json | 18 +++++------ .../components/rachio/translations/es.json | 2 +- .../components/renault/translations/es.json | 2 +- .../components/rfxtrx/translations/es.json | 2 +- .../components/risco/translations/es.json | 2 +- .../components/roomba/translations/es.json | 10 +++--- .../components/samsungtv/translations/es.json | 2 +- .../components/schedule/translations/el.json | 3 ++ .../components/schedule/translations/fr.json | 9 ++++++ .../components/schedule/translations/hu.json | 9 ++++++ .../schedule/translations/pt-BR.json | 9 ++++++ .../schedule/translations/zh-Hant.json | 9 ++++++ .../components/sentry/translations/es.json | 2 +- .../components/shelly/translations/es.json | 6 ++-- .../simplisafe/translations/es.json | 2 +- .../components/smappee/translations/es.json | 2 +- .../smartthings/translations/es.json | 4 +-- .../components/soma/translations/es.json | 4 +-- .../somfy_mylink/translations/es.json | 4 +-- .../components/sonarr/translations/es.json | 4 +-- .../components/songpal/translations/es.json | 4 +-- .../speedtestdotnet/translations/es.json | 2 +- .../components/spotify/translations/es.json | 4 +-- .../components/sql/translations/es.json | 4 +-- .../components/starline/translations/es.json | 2 +- .../components/switchbot/translations/el.json | 6 ++++ .../synology_dsm/translations/es.json | 6 ++-- .../components/tado/translations/es.json | 4 +-- .../tellduslive/translations/es.json | 7 ++--- .../components/tibber/translations/es.json | 2 +- .../components/tradfri/translations/es.json | 8 ++--- .../transmission/translations/es.json | 4 +-- .../components/tuya/translations/es.json | 4 +-- .../components/twilio/translations/es.json | 4 +-- .../components/unifi/translations/es.json | 16 +++++----- .../components/upnp/translations/es.json | 6 +--- .../components/vera/translations/es.json | 2 +- .../components/vizio/translations/es.json | 10 +++--- .../components/withings/translations/es.json | 4 +-- .../components/wled/translations/es.json | 2 +- .../components/wolflink/translations/es.json | 4 +-- .../wolflink/translations/sensor.es.json | 6 ++-- .../xiaomi_aqara/translations/es.json | 6 ++-- .../yalexs_ble/translations/el.json | 24 ++++++++++++++ .../components/yeelight/translations/es.json | 2 +- 144 files changed, 475 insertions(+), 283 deletions(-) create mode 100644 homeassistant/components/schedule/translations/el.json create mode 100644 homeassistant/components/schedule/translations/fr.json create mode 100644 homeassistant/components/schedule/translations/hu.json create mode 100644 homeassistant/components/schedule/translations/pt-BR.json create mode 100644 homeassistant/components/schedule/translations/zh-Hant.json create mode 100644 homeassistant/components/yalexs_ble/translations/el.json diff --git a/homeassistant/components/accuweather/translations/es.json b/homeassistant/components/accuweather/translations/es.json index 36ccc3f0cca..df5b9c21494 100644 --- a/homeassistant/components/accuweather/translations/es.json +++ b/homeassistant/components/accuweather/translations/es.json @@ -28,7 +28,7 @@ "data": { "forecast": "Pron\u00f3stico del tiempo" }, - "description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilitas el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 64 minutos en lugar de cada 32 minutos." + "description": "Debido a las limitaciones de la versi\u00f3n gratuita de la clave API de AccuWeather, cuando habilitas el pron\u00f3stico del tiempo, las actualizaciones de datos se realizar\u00e1n cada 80 minutos en lugar de cada 40 minutos." } } }, diff --git a/homeassistant/components/airly/translations/es.json b/homeassistant/components/airly/translations/es.json index 9d0b254713b..1a4197b758b 100644 --- a/homeassistant/components/airly/translations/es.json +++ b/homeassistant/components/airly/translations/es.json @@ -15,7 +15,7 @@ "longitude": "Longitud", "name": "Nombre" }, - "description": "Establecer la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave de la API vaya a https://developer.airly.eu/register" + "description": "Para generar la clave API, ve a https://developer.airly.eu/register" } } }, diff --git a/homeassistant/components/airnow/translations/es.json b/homeassistant/components/airnow/translations/es.json index b5b915910c5..94ed5e64123 100644 --- a/homeassistant/components/airnow/translations/es.json +++ b/homeassistant/components/airnow/translations/es.json @@ -17,7 +17,7 @@ "longitude": "Longitud", "radius": "Radio de la estaci\u00f3n (millas; opcional)" }, - "description": "Configurar la integraci\u00f3n de calidad del aire de AirNow. Para generar una clave API, ve a https://docs.airnowapi.org/account/request/" + "description": "Para generar la clave API, ve a https://docs.airnowapi.org/account/request/" } } } diff --git a/homeassistant/components/airvisual/translations/es.json b/homeassistant/components/airvisual/translations/es.json index b51d3035fe5..5c38176bf22 100644 --- a/homeassistant/components/airvisual/translations/es.json +++ b/homeassistant/components/airvisual/translations/es.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "general_error": "Se ha producido un error desconocido.", + "general_error": "Error inesperado", "invalid_api_key": "Clave API no v\u00e1lida", "location_not_found": "Ubicaci\u00f3n no encontrada" }, diff --git a/homeassistant/components/almond/translations/es.json b/homeassistant/components/almond/translations/es.json index 94c5e89be5d..a3383cd4b5f 100644 --- a/homeassistant/components/almond/translations/es.json +++ b/homeassistant/components/almond/translations/es.json @@ -9,7 +9,7 @@ "step": { "hassio_confirm": { "description": "\u00bfQuieres configurar Home Assistant para conectarse a Almond proporcionado por el complemento: {addon} ?", - "title": "Almond a trav\u00e9s del complemento Supervisor" + "title": "Almond a trav\u00e9s del complemento Home Assistant" }, "pick_implementation": { "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" diff --git a/homeassistant/components/ambiclimate/translations/es.json b/homeassistant/components/ambiclimate/translations/es.json index 521234c972a..1ac7a371f82 100644 --- a/homeassistant/components/ambiclimate/translations/es.json +++ b/homeassistant/components/ambiclimate/translations/es.json @@ -6,7 +6,7 @@ "missing_configuration": "El componente no est\u00e1 configurado. Siga la documentaci\u00f3n." }, "create_entry": { - "default": "Autenticado correctamente con Ambiclimate" + "default": "Autenticado correctamente" }, "error": { "follow_link": "Accede al enlace e identif\u00edcate antes de pulsar Enviar.", @@ -14,7 +14,7 @@ }, "step": { "auth": { - "description": "Accede al siguiente [enlace]({authorization_url}) y permite el acceso a tu cuenta de Ambiclimate, despu\u00e9s vuelve y pulsa en enviar a continuaci\u00f3n.\n(Aseg\u00farate que la url de devoluci\u00f3n de llamada es {cb_url})", + "description": "Por favor, sigue este [enlace]({authorization_url}) y **Permite** el acceso a tu cuenta Ambiclimate, luego regresa y presiona **Enviar** a continuaci\u00f3n.\n(Aseg\u00farate de que la URL de devoluci\u00f3n de llamada especificada sea {cb_url})", "title": "Autenticaci\u00f3n de Ambiclimate" } } diff --git a/homeassistant/components/apple_tv/translations/es.json b/homeassistant/components/apple_tv/translations/es.json index b73aa659ef6..918a95de976 100644 --- a/homeassistant/components/apple_tv/translations/es.json +++ b/homeassistant/components/apple_tv/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "backoff": "El dispositivo no acepta solicitudes de emparejamiento en este momento (es posible que hayas introducido un c\u00f3digo PIN no v\u00e1lido demasiadas veces), int\u00e9ntalo de nuevo m\u00e1s tarde.", + "backoff": "El dispositivo no acepta solicitudes de emparejamiento en este momento (es posible que hayas introducido un c\u00f3digo PIN no v\u00e1lido demasiadas veces), intenta de nuevo m\u00e1s tarde.", "device_did_not_pair": "No se ha intentado finalizar el proceso de emparejamiento desde el dispositivo.", "device_not_found": "No se encontr\u00f3 el dispositivo durante el descubrimiento, por favor intenta a\u00f1adirlo nuevamente.", "inconsistent_device": "No se encontraron los protocolos esperados durante el descubrimiento. Esto normalmente indica un problema con multicast DNS (Zeroconf). Por favor, intenta a\u00f1adir el dispositivo nuevamente.", @@ -26,7 +26,7 @@ "title": "Confirma la adici\u00f3n del Apple TV" }, "pair_no_pin": { - "description": "El emparejamiento es necesario para el servicio `{protocol}`. Introduce el PIN en tu Apple TV para continuar.", + "description": "Se requiere emparejamiento para el servicio `{protocol}`. Por favor, introduce el PIN {pin} en tu dispositivo para continuar.", "title": "Emparejamiento" }, "pair_with_pin": { @@ -56,7 +56,7 @@ "data": { "device_input": "Dispositivo" }, - "description": "Empieza introduciendo el nombre del dispositivo (eje. Cocina o Dormitorio) o la direcci\u00f3n IP del Apple TV que quieres a\u00f1adir. Si se han econtrado dispositivos en tu red, se mostrar\u00e1n a continuaci\u00f3n.\n\nSi no puedes ver el dispositivo o experimentas alg\u00fan problema, intente especificar la direcci\u00f3n IP del dispositivo.\n\n{devices}", + "description": "Comienza introduciendo el nombre del dispositivo (por ejemplo, cocina o dormitorio) o la direcci\u00f3n IP del Apple TV que deseas a\u00f1adir. \n\n Si no puedes ver tu dispositivo o experimentas alg\u00fan problema, intenta especificar la direcci\u00f3n IP del dispositivo.", "title": "Configurar un nuevo Apple TV" } } diff --git a/homeassistant/components/arcam_fmj/translations/es.json b/homeassistant/components/arcam_fmj/translations/es.json index 33c0558a4ce..035749be792 100644 --- a/homeassistant/components/arcam_fmj/translations/es.json +++ b/homeassistant/components/arcam_fmj/translations/es.json @@ -5,7 +5,7 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar" }, - "flow_title": "Arcam FMJ en {host}", + "flow_title": "{host}", "step": { "confirm": { "description": "\u00bfQuieres a\u00f1adir el Arcam FMJ en `{host}` a Home Assistant?" diff --git a/homeassistant/components/asuswrt/translations/es.json b/homeassistant/components/asuswrt/translations/es.json index f46bf499455..f20f0faa3eb 100644 --- a/homeassistant/components/asuswrt/translations/es.json +++ b/homeassistant/components/asuswrt/translations/es.json @@ -19,12 +19,12 @@ "mode": "Modo", "name": "Nombre", "password": "Contrase\u00f1a", - "port": "Puerto (d\u00e9jalo vac\u00edo por el predeterminado del protocolo)", + "port": "Puerto (dejar vac\u00edo para el predeterminado del protocolo)", "protocol": "Protocolo de comunicaci\u00f3n a utilizar", - "ssh_key": "Ruta de acceso a su archivo de clave SSH (en lugar de la contrase\u00f1a)", + "ssh_key": "Ruta a tu archivo de clave SSH (en lugar de contrase\u00f1a)", "username": "Nombre de usuario" }, - "description": "Establezca los par\u00e1metros necesarios para conectarse a su router", + "description": "Establece el par\u00e1metro requerido para conectarte a tu router", "title": "AsusWRT" } } @@ -33,7 +33,7 @@ "step": { "init": { "data": { - "consider_home": "Segundos de espera antes de considerar un dispositivo ausente", + "consider_home": "Segundos de espera antes de considerar un dispositivo como ausente", "dnsmasq": "La ubicaci\u00f3n en el router de los archivos dnsmasq.leases", "interface": "La interfaz de la que quieres estad\u00edsticas (por ejemplo, eth0, eth1, etc.)", "require_ip": "Los dispositivos deben tener IP (para el modo de punto de acceso)", diff --git a/homeassistant/components/atag/translations/es.json b/homeassistant/components/atag/translations/es.json index ed89c8c385c..c2da8173e22 100644 --- a/homeassistant/components/atag/translations/es.json +++ b/homeassistant/components/atag/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Este dispositivo ya ha sido a\u00f1adido a HomeAssistant" + "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/auth/translations/es.json b/homeassistant/components/auth/translations/es.json index 0279bca1dfa..85f412f0814 100644 --- a/homeassistant/components/auth/translations/es.json +++ b/homeassistant/components/auth/translations/es.json @@ -5,7 +5,7 @@ "no_available_service": "No hay servicios de notificaci\u00f3n disponibles." }, "error": { - "invalid_code": "C\u00f3digo inv\u00e1lido, por favor int\u00e9ntelo de nuevo." + "invalid_code": "C\u00f3digo no v\u00e1lido, por favor, vuelve a intentarlo." }, "step": { "init": { @@ -13,7 +13,7 @@ "title": "Configurar una contrase\u00f1a de un solo uso entregada por el componente de notificaci\u00f3n" }, "setup": { - "description": "Se ha enviado una contrase\u00f1a de un solo uso a trav\u00e9s de ** notify.{notify_service} **. Por favor introd\u00facela a continuaci\u00f3n:", + "description": "Se ha enviado una contrase\u00f1a de un solo uso a trav\u00e9s de **notify.{notify_service}**. Por favor, introd\u00facela a continuaci\u00f3n:", "title": "Verificar la configuraci\u00f3n" } }, @@ -21,7 +21,7 @@ }, "totp": { "error": { - "invalid_code": "C\u00f3digo inv\u00e1lido, int\u00e9ntalo de nuevo. Si recibes este error de forma consistente, aseg\u00farate de que el reloj de tu sistema Home Assistant es correcto." + "invalid_code": "C\u00f3digo no v\u00e1lido, por favor, vuelve a intentarlo. Si recibes este error constantemente, aseg\u00farate de que el reloj de tu sistema Home Assistant sea exacto." }, "step": { "init": { diff --git a/homeassistant/components/automation/translations/es.json b/homeassistant/components/automation/translations/es.json index c20f1be7d1d..08d1cc7df07 100644 --- a/homeassistant/components/automation/translations/es.json +++ b/homeassistant/components/automation/translations/es.json @@ -1,7 +1,7 @@ { "state": { "_": { - "off": "Apagado", + "off": "Apagada", "on": "Encendida" } }, diff --git a/homeassistant/components/awair/translations/ca.json b/homeassistant/components/awair/translations/ca.json index e52451bd108..eaf255a0532 100644 --- a/homeassistant/components/awair/translations/ca.json +++ b/homeassistant/components/awair/translations/ca.json @@ -21,6 +21,9 @@ "email": "Correu electr\u00f2nic" } }, + "discovery_confirm": { + "description": "Vols configurar {model} ({device_id})?" + }, "local": { "data": { "host": "Adre\u00e7a IP" diff --git a/homeassistant/components/awair/translations/el.json b/homeassistant/components/awair/translations/el.json index e878c370d93..4cd1ac93d0a 100644 --- a/homeassistant/components/awair/translations/el.json +++ b/homeassistant/components/awair/translations/el.json @@ -9,7 +9,17 @@ "invalid_access_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03b5\u03af\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03c4\u03ae Awair \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: {url}" + }, + "discovery_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {model} ({device_id});" + }, + "local": { + "description": "\u03a4\u03bf Awair Local API \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ce\u03bd\u03c4\u03b1\u03c2 \u03b1\u03c5\u03c4\u03ac \u03c4\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1: {url}" + }, "reauth": { "data": { "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", @@ -29,7 +39,11 @@ "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "email": "Email" }, - "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03b5\u03af\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03c4\u03ae Awair \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: https://developer.getawair.com/onboard/login" + "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03b5\u03af\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03c4\u03ae Awair \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: https://developer.getawair.com/onboard/login", + "menu_options": { + "cloud": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03ad\u03c3\u03c9 cloud", + "local": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ac (\u03c0\u03c1\u03bf\u03c4\u03b9\u03bc\u03ac\u03c4\u03b1\u03b9)" + } } } } diff --git a/homeassistant/components/awair/translations/es.json b/homeassistant/components/awair/translations/es.json index 64d678003ac..df5667ff1f9 100644 --- a/homeassistant/components/awair/translations/es.json +++ b/homeassistant/components/awair/translations/es.json @@ -50,7 +50,7 @@ "access_token": "Token de acceso", "email": "Correo electr\u00f3nico" }, - "description": "Debes registrarte para obtener un token de acceso de desarrollador Awair en: https://developer.getawair.com/onboard/login", + "description": "Elige local para la mejor experiencia. Usa solo la nube si el dispositivo no est\u00e1 conectado a la misma red que Home Assistant, o si tienes un dispositivo heredado.", "menu_options": { "cloud": "Conectar a trav\u00e9s de la nube", "local": "Conectar localmente (preferido)" diff --git a/homeassistant/components/awair/translations/et.json b/homeassistant/components/awair/translations/et.json index b741f5f8722..c40f4be1f1c 100644 --- a/homeassistant/components/awair/translations/et.json +++ b/homeassistant/components/awair/translations/et.json @@ -50,7 +50,7 @@ "access_token": "Juurdep\u00e4\u00e4sut\u00f5end", "email": "E-post" }, - "description": "Pead registreerima Awair arendaja juurdep\u00e4\u00e4su loa aadressil: https://developer.getawair.com/onboard/login", + "description": "Parima kogemuse saamiseks vali kohalik \u00fchendus. Kasuta pilve ainult siis, kui seade ei ole \u00fchendatud samasse v\u00f5rku kui Home Assistant v\u00f5i kui on vanem seade.", "menu_options": { "cloud": "Pilve\u00fchendus", "local": "Kohalik \u00fchendus (eelistatud)" diff --git a/homeassistant/components/awair/translations/fr.json b/homeassistant/components/awair/translations/fr.json index fd915507762..2b4d572609d 100644 --- a/homeassistant/components/awair/translations/fr.json +++ b/homeassistant/components/awair/translations/fr.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "already_configured_account": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "already_configured_device": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", - "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "unreachable": "\u00c9chec de connexion" }, "error": { "invalid_access_token": "Jeton d'acc\u00e8s non valide", - "unknown": "Erreur inattendue" + "unknown": "Erreur inattendue", + "unreachable": "\u00c9chec de connexion" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Jeton d'acc\u00e8s", + "email": "Courriel" + }, + "description": "Vous devez r\u00e9aliser une demande de jeton d'acc\u00e8s de d\u00e9veloppeur Awair \u00e0 l'adresse suivante\u00a0: {url}" + }, + "discovery_confirm": { + "description": "Voulez-vous configurer {model} ({device_id})\u00a0?" + }, + "local": { + "data": { + "host": "Adresse IP" + }, + "description": "L'API locale Awair doit \u00eatre activ\u00e9e en suivant ces \u00e9tapes\u00a0: {url}" + }, "reauth": { "data": { "access_token": "Jeton d'acc\u00e8s", @@ -29,7 +50,11 @@ "access_token": "Jeton d'acc\u00e8s", "email": "Courriel" }, - "description": "Vous devez vous inscrire pour un jeton d'acc\u00e8s d\u00e9veloppeur Awair sur: https://developer.getawair.com/onboard/login" + "description": "S\u00e9lectionnez le mode local pour une exp\u00e9rience optimale. S\u00e9lectionnez le mode cloud uniquement si l'appareil n'est pas connect\u00e9 au m\u00eame r\u00e9seau que Home Assistant ou si vous disposez d'un appareil ancien.", + "menu_options": { + "cloud": "Connexion cloud", + "local": "Connexion locale (recommand\u00e9e)" + } } } } diff --git a/homeassistant/components/awair/translations/hu.json b/homeassistant/components/awair/translations/hu.json index 2e81b31f187..7ce394ccb4b 100644 --- a/homeassistant/components/awair/translations/hu.json +++ b/homeassistant/components/awair/translations/hu.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "already_configured_account": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "unreachable": "Sikertelen csatlakoz\u00e1s" }, "error": { "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token", - "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", + "unreachable": "Sikertelen csatlakoz\u00e1s" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", + "email": "E-mail" + }, + "description": "Regisztr\u00e1lnia kell egy Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si token\u00e9rt a k\u00f6vetkez\u0151 c\u00edmen: {url}" + }, + "discovery_confirm": { + "description": "Be\u00e1ll\u00edtja a k\u00f6vetkez\u0151t: {model} ({device_id})?" + }, + "local": { + "data": { + "host": "IP c\u00edm" + }, + "description": "Az Awair lok\u00e1lis API-t az al\u00e1bbi l\u00e9p\u00e9sekkel kell enged\u00e9lyezni: {url}" + }, "reauth": { "data": { "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", @@ -29,7 +50,11 @@ "access_token": "Hozz\u00e1f\u00e9r\u00e9si token", "email": "E-mail" }, - "description": "Regisztr\u00e1lnia kell az Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si tokenj\u00e9hez a k\u00f6vetkez\u0151 c\u00edmen: https://developer.getawair.com/onboard/login" + "description": "Regisztr\u00e1lnia kell az Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si tokenj\u00e9hez a k\u00f6vetkez\u0151 c\u00edmen: https://developer.getawair.com/onboard/login", + "menu_options": { + "cloud": "Csatlakoz\u00e1s a felh\u0151n kereszt\u00fcl", + "local": "Lok\u00e1lis csatlakoz\u00e1s (aj\u00e1nlott)" + } } } } diff --git a/homeassistant/components/awair/translations/pt-BR.json b/homeassistant/components/awair/translations/pt-BR.json index 7406bdf3ee0..e3c3e24f738 100644 --- a/homeassistant/components/awair/translations/pt-BR.json +++ b/homeassistant/components/awair/translations/pt-BR.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "A conta j\u00e1 foi configurada", + "already_configured_account": "A conta j\u00e1 est\u00e1 configurada", + "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", "no_devices_found": "Nenhum dispositivo encontrado na rede", - "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "unreachable": "Falhou ao conectar" }, "error": { "invalid_access_token": "Token de acesso inv\u00e1lido", - "unknown": "Erro inesperado" + "unknown": "Erro inesperado", + "unreachable": "Falhou ao conectar" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Token de acesso", + "email": "E-mail" + }, + "description": "Voc\u00ea deve se registrar para um token de acesso de desenvolvedor Awair em: {url}" + }, + "discovery_confirm": { + "description": "Deseja configurar {model} ({device_id})?" + }, + "local": { + "data": { + "host": "Endere\u00e7o IP" + }, + "description": "Awair Local API deve ser ativada seguindo estas etapas: {url}" + }, "reauth": { "data": { "access_token": "Token de acesso", @@ -29,7 +50,11 @@ "access_token": "Token de acesso", "email": "Email" }, - "description": "Voc\u00ea deve se registrar para um token de acesso de desenvolvedor Awair em: https://developer.getawair.com/onboard/login" + "description": "Escolha local para a melhor experi\u00eancia. Use a nuvem apenas se o dispositivo n\u00e3o estiver conectado \u00e0 mesma rede que o Home Assistant ou se voc\u00ea tiver um dispositivo legado.", + "menu_options": { + "cloud": "Conecte-se pela nuvem", + "local": "Conecte-se localmente (preferencial)" + } } } } diff --git a/homeassistant/components/awair/translations/zh-Hant.json b/homeassistant/components/awair/translations/zh-Hant.json index f14acef8550..e7953517823 100644 --- a/homeassistant/components/awair/translations/zh-Hant.json +++ b/homeassistant/components/awair/translations/zh-Hant.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured_account": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured_device": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", - "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "unreachable": "\u9023\u7dda\u5931\u6557" }, "error": { "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "unknown": "\u672a\u9810\u671f\u932f\u8aa4", + "unreachable": "\u9023\u7dda\u5931\u6557" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "\u5b58\u53d6\u6b0a\u6756", + "email": "\u96fb\u5b50\u90f5\u4ef6" + }, + "description": "\u5fc5\u9808\u5148\u8a3b\u518a\u53d6\u5f97 Awair \u958b\u767c\u8005\u5e33\u865f\u6b0a\u6756\uff1a{url}" + }, + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {model} ({device_id})\uff1f" + }, + "local": { + "data": { + "host": "IP \u4f4d\u5740" + }, + "description": "\u5fc5\u9808\u900f\u904e\u4ee5\u4e0b\u6b65\u9a5f\u4ee5\u555f\u7528 Awair \u672c\u5730\u7aef API\uff1a{url}" + }, "reauth": { "data": { "access_token": "\u5b58\u53d6\u6b0a\u6756", @@ -29,7 +50,11 @@ "access_token": "\u5b58\u53d6\u6b0a\u6756", "email": "\u96fb\u5b50\u90f5\u4ef6" }, - "description": "\u5fc5\u9808\u5148\u8a3b\u518a Awair \u958b\u767c\u8005\u5b58\u53d6\u6b0a\u6756\uff1ahttps://developer.getawair.com/onboard/login" + "description": "\u9078\u64c7\u672c\u5730\u7aef\u4ee5\u7372\u5f97\u6700\u4f73\u4f7f\u7528\u9ad4\u9a57\u3002\u50c5\u65bc\u88dd\u7f6e\u8207 Home Assistant \u4e26\u975e\u8655\u65bc\u76f8\u540c\u7db2\u8def\u4e0b\u3001\u6216\u8005\u4f7f\u7528\u820a\u8a2d\u5099\u7684\u60c5\u6cc1\u4e0b\u4f7f\u7528\u96f2\u7aef\u3002", + "menu_options": { + "cloud": "\u900f\u904e\u96f2\u7aef\u9023\u7dda", + "local": "\u672c\u5730\u7aef\u9023\u7dda\uff08\u9996\u9078\uff09" + } } } } diff --git a/homeassistant/components/axis/translations/es.json b/homeassistant/components/axis/translations/es.json index 6a406614b9f..87497280775 100644 --- a/homeassistant/components/axis/translations/es.json +++ b/homeassistant/components/axis/translations/es.json @@ -2,16 +2,16 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "link_local_address": "Las direcciones de enlace locales no son compatibles", + "link_local_address": "Las direcciones de enlace local no son compatibles", "not_axis_device": "El dispositivo descubierto no es un dispositivo de Axis" }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "El flujo de configuraci\u00f3n del dispositivo ya est\u00e1 en marcha.", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, - "flow_title": "Dispositivo Axis: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/azure_devops/translations/es.json b/homeassistant/components/azure_devops/translations/es.json index c71fc22b1bd..2795286e5c7 100644 --- a/homeassistant/components/azure_devops/translations/es.json +++ b/homeassistant/components/azure_devops/translations/es.json @@ -9,7 +9,7 @@ "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "project_error": "No se pudo obtener informaci\u00f3n del proyecto." }, - "flow_title": "Azure DevOps: {project_url}", + "flow_title": "{project_url}", "step": { "reauth": { "data": { diff --git a/homeassistant/components/blink/translations/es.json b/homeassistant/components/blink/translations/es.json index 7ff0b349555..72f68ab4fff 100644 --- a/homeassistant/components/blink/translations/es.json +++ b/homeassistant/components/blink/translations/es.json @@ -14,7 +14,7 @@ "data": { "2fa": "C\u00f3digo de dos factores" }, - "description": "Introduce el pin enviado a tu correo electr\u00f3nico. Si el correo electr\u00f3nico no contiene un pin, d\u00e9jalo en blanco", + "description": "Introduce el PIN enviado a su correo electr\u00f3nico", "title": "Autenticaci\u00f3n de dos factores" }, "user": { @@ -32,7 +32,7 @@ "data": { "scan_interval": "Intervalo de escaneo (segundos)" }, - "description": "Configurar la integraci\u00f3n de Blink", + "description": "Configurar la integraci\u00f3n Blink", "title": "Opciones de Blink" } } diff --git a/homeassistant/components/braviatv/translations/es.json b/homeassistant/components/braviatv/translations/es.json index 401a254eeaf..118125f63e0 100644 --- a/homeassistant/components/braviatv/translations/es.json +++ b/homeassistant/components/braviatv/translations/es.json @@ -21,7 +21,7 @@ "data": { "host": "Host" }, - "description": "Configura la integraci\u00f3n del televisor Sony Bravia. Si tienes problemas con la configuraci\u00f3n, ve a: https://www.home-assistant.io/integrations/braviatv\n\nAseg\u00farate de que tu televisor est\u00e1 encendido." + "description": "Aseg\u00farate de que tu TV est\u00e9 encendida antes de intentar configurarla." } } }, diff --git a/homeassistant/components/broadlink/translations/es.json b/homeassistant/components/broadlink/translations/es.json index 96f791979d4..97a71e2cf61 100644 --- a/homeassistant/components/broadlink/translations/es.json +++ b/homeassistant/components/broadlink/translations/es.json @@ -13,7 +13,7 @@ "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", "unknown": "Error inesperado" }, - "flow_title": "{name} ( {model} en {host} )", + "flow_title": "{name} ({model} en {host})", "step": { "auth": { "title": "Autenticarse en el dispositivo" @@ -22,17 +22,17 @@ "data": { "name": "Nombre" }, - "title": "Elija un nombre para el dispositivo" + "title": "Elige un nombre para el dispositivo" }, "reset": { - "description": "Su dispositivo est\u00e1 bloqueado para la autenticaci\u00f3n. Siga las instrucciones para desbloquearlo:\n1. Reinicie el dispositivo de f\u00e1brica.\n2. Use la aplicaci\u00f3n oficial para agregar el dispositivo a su red local.\n3. Pare. No termine la configuraci\u00f3n. Cierre la aplicaci\u00f3n.\n4. Haga clic en Enviar.", + "description": "{name} ({model} en {host}) est\u00e1 bloqueado. Debes desbloquear el dispositivo para autenticarse y completar la configuraci\u00f3n. Instrucciones:\n1. Abre la aplicaci\u00f3n Broadlink.\n2. Haz clic en el dispositivo.\n3. Haz clic en `...` en la parte superior derecha.\n4. Despl\u00e1zate hasta la parte inferior de la p\u00e1gina.\n5. Desactiva el bloqueo.", "title": "Desbloquear el dispositivo" }, "unlock": { "data": { "unlock": "S\u00ed, hazlo." }, - "description": "Tu dispositivo est\u00e1 bloqueado. Esto puede provocar problemas de autenticaci\u00f3n en Home Assistant. \u00bfQuieres desbloquearlo?", + "description": "{name} ({model} en {host}) est\u00e1 bloqueado. Esto puede generar problemas de autenticaci\u00f3n en Home Assistant. \u00bfTe gustar\u00eda desbloquearlo?", "title": "Desbloquear el dispositivo (opcional)" }, "user": { diff --git a/homeassistant/components/bsblan/translations/es.json b/homeassistant/components/bsblan/translations/es.json index 5f1e55c8d0d..1e1c29d24a4 100644 --- a/homeassistant/components/bsblan/translations/es.json +++ b/homeassistant/components/bsblan/translations/es.json @@ -17,7 +17,7 @@ "port": "Puerto", "username": "Nombre de usuario" }, - "description": "Configura tu dispositivo BSB-Lan para integrarse con Home Assistant.", + "description": "Configura tu dispositivo BSB-Lan para que se integre con Home Assistant.", "title": "Conectar con el dispositivo BSB-Lan" } } diff --git a/homeassistant/components/cloudflare/translations/es.json b/homeassistant/components/cloudflare/translations/es.json index eff36eefcfa..d3ea7a842c5 100644 --- a/homeassistant/components/cloudflare/translations/es.json +++ b/homeassistant/components/cloudflare/translations/es.json @@ -10,7 +10,7 @@ "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "invalid_zone": "Zona no v\u00e1lida" }, - "flow_title": "Cloudflare: {name}", + "flow_title": "{name}", "step": { "reauth_confirm": { "data": { diff --git a/homeassistant/components/daikin/translations/es.json b/homeassistant/components/daikin/translations/es.json index ce72440bf7d..8ddfb8241e0 100644 --- a/homeassistant/components/daikin/translations/es.json +++ b/homeassistant/components/daikin/translations/es.json @@ -17,7 +17,7 @@ "host": "Host", "password": "Contrase\u00f1a" }, - "description": "Introduce la direcci\u00f3n IP de tu aire acondicionado Daikin.\n\nTen en cuenta que la Clave API y la Contrase\u00f1a son usadas por los dispositivos BRP072Cxx y SKYFi respectivamente.", + "description": "Introduce la Direcci\u00f3n IP de tu aire acondicionado Daikin. \n\nTen en cuenta que Clave API y Contrase\u00f1a solo los utilizan los dispositivos BRP072Cxx y SKYFi respectivamente.", "title": "Configurar aire acondicionado Daikin" } } diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index 3c4d8a89134..6308bf7fed8 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -15,11 +15,11 @@ "step": { "hassio_confirm": { "description": "\u00bfQuieres configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento {addon} ?", - "title": "Pasarela de enlace de CONZ Zigbee v\u00eda complemento de Home Assistant" + "title": "Puerta de enlace Zigbee deCONZ a trav\u00e9s del complemento Home Assistant" }, "link": { - "description": "Desbloquea tu gateway de deCONZ para registrarte con Home Assistant.\n\n1. Dir\u00edgete a deCONZ Settings -> Gateway -> Advanced\n2. Pulsa el bot\u00f3n \"Authenticate app\"", - "title": "Enlazar con deCONZ" + "description": "Desbloquea tu puerta de enlace deCONZ para registrarlo con Home Assistant. \n\n 1. Ve a Configuraci\u00f3n deCONZ -> Puerta de enlace -> Avanzado\n 2. Presiona el bot\u00f3n \"Autenticar aplicaci\u00f3n\"", + "title": "Vincular con deCONZ" }, "manual_input": { "data": { @@ -71,7 +71,7 @@ "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" qu\u00edntuple pulsaci\u00f3n", "remote_button_rotated": "Bot\u00f3n \"{subtype}\" girado", "remote_button_rotated_fast": "Bot\u00f3n \"{subtype}\" girado r\u00e1pido", - "remote_button_rotation_stopped": "Bot\u00f3n rotativo \"{subtype}\" detenido", + "remote_button_rotation_stopped": "Se detuvo la rotaci\u00f3n del bot\u00f3n \"{subtype}\"", "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", "remote_button_short_release": "Bot\u00f3n \"{subtype}\" soltado", "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" triple pulsaci\u00f3n", diff --git a/homeassistant/components/denonavr/translations/es.json b/homeassistant/components/denonavr/translations/es.json index d601c6a9ff0..55936dee05d 100644 --- a/homeassistant/components/denonavr/translations/es.json +++ b/homeassistant/components/denonavr/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar, int\u00e9ntelo de nuevo, desconectar los cables de alimentaci\u00f3n y Ethernet y volver a conectarlos puede ayudar", - "not_denonavr_manufacturer": "No es un Receptor AVR Denon AVR en Red, el fabricante detectado no concuerda", + "not_denonavr_manufacturer": "No es un receptor de red Denon AVR, el fabricante descubierto no coincide", "not_denonavr_missing": "No es un Receptor AVR Denon AVR en Red, la informaci\u00f3n detectada no est\u00e1 completa" }, "error": { diff --git a/homeassistant/components/dialogflow/translations/es.json b/homeassistant/components/dialogflow/translations/es.json index 8c2c5b4993e..eb4c3b179a6 100644 --- a/homeassistant/components/dialogflow/translations/es.json +++ b/homeassistant/components/dialogflow/translations/es.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, "create_entry": { - "default": "Para enviar eventos a Home Assistant, necesitas configurar [Integraci\u00f3n de flujos de dialogo de webhook]({dialogflow_url}).\n\nRellena la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nVer [Documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." + "default": "Para enviar eventos a Home Assistant, necesitas configurar la [Integraci\u00f3n webhook de Dialogflow]({dialogflow_url}).\n\nCompleta la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nConsulta [la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." }, "step": { "user": { diff --git a/homeassistant/components/directv/translations/es.json b/homeassistant/components/directv/translations/es.json index 92cf160462c..2a268ac2148 100644 --- a/homeassistant/components/directv/translations/es.json +++ b/homeassistant/components/directv/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dispositivo ya configurado", + "already_configured": "El dispositivo ya est\u00e1 configurado", "unknown": "Error inesperado" }, "error": { diff --git a/homeassistant/components/doorbird/translations/es.json b/homeassistant/components/doorbird/translations/es.json index 83ec7ed7a6a..5aefd6eb3af 100644 --- a/homeassistant/components/doorbird/translations/es.json +++ b/homeassistant/components/doorbird/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "link_local_address": "No se admiten direcciones locales", + "link_local_address": "Las direcciones de enlace local no son compatibles", "not_doorbird_device": "Este dispositivo no es un DoorBird" }, "error": { diff --git a/homeassistant/components/dsmr/translations/es.json b/homeassistant/components/dsmr/translations/es.json index 91ee7c58952..a6363ebdea1 100644 --- a/homeassistant/components/dsmr/translations/es.json +++ b/homeassistant/components/dsmr/translations/es.json @@ -11,8 +11,6 @@ "cannot_connect": "No se pudo conectar" }, "step": { - "one": "Vac\u00edo", - "other": "Vac\u00edo", "setup_network": { "data": { "dsmr_version": "Seleccione la versi\u00f3n de DSMR", diff --git a/homeassistant/components/dunehd/translations/es.json b/homeassistant/components/dunehd/translations/es.json index 7e2c1282ca6..78a8fd1733d 100644 --- a/homeassistant/components/dunehd/translations/es.json +++ b/homeassistant/components/dunehd/translations/es.json @@ -13,7 +13,7 @@ "data": { "host": "Host" }, - "description": "Configura la integraci\u00f3n de Dune HD. Si tienes problemas con la configuraci\u00f3n, ve a: https://www.home-assistant.io/integrations/dunehd \n\n Aseg\u00farate de que tu reproductor est\u00e1 encendido." + "description": "Aseg\u00farate de que tu reproductor est\u00e9 encendido." } } } diff --git a/homeassistant/components/ecobee/translations/es.json b/homeassistant/components/ecobee/translations/es.json index f4980f46bbb..a7fdcfb2116 100644 --- a/homeassistant/components/ecobee/translations/es.json +++ b/homeassistant/components/ecobee/translations/es.json @@ -9,7 +9,7 @@ }, "step": { "authorize": { - "description": "Por favor, autorizar esta aplicaci\u00f3n en https://www.ecobee.com/consumerportal/index.html con c\u00f3digo pin:\n\n{pin}\n\nA continuaci\u00f3n, pulse Enviar.", + "description": "Por favor, autoriza esta aplicaci\u00f3n en https://www.ecobee.com/consumerportal/index.html con el c\u00f3digo PIN: \n\n{pin}\n\nLuego, presiona Enviar.", "title": "Autorizar aplicaci\u00f3n en ecobee.com" }, "user": { diff --git a/homeassistant/components/elgato/translations/es.json b/homeassistant/components/elgato/translations/es.json index a41872115fa..eb0d7fde9b4 100644 --- a/homeassistant/components/elgato/translations/es.json +++ b/homeassistant/components/elgato/translations/es.json @@ -18,7 +18,7 @@ }, "zeroconf_confirm": { "description": "\u00bfQuieres a\u00f1adir el Key Light de Elgato con n\u00famero de serie `{serial_number}` a Home Assistant?", - "title": "Descubierto dispositivo Elgato Key Light" + "title": "Dispositivo Elgato Light descubierto" } } } diff --git a/homeassistant/components/elkm1/translations/es.json b/homeassistant/components/elkm1/translations/es.json index 25c676caafc..1cbae31550f 100644 --- a/homeassistant/components/elkm1/translations/es.json +++ b/homeassistant/components/elkm1/translations/es.json @@ -41,7 +41,7 @@ "data": { "device": "Dispositivo" }, - "description": "La cadena de direcci\u00f3n debe estar en el formato 'direcci\u00f3n[:puerto]' para 'seguro' y 'no-seguro'. Ejemplo: '192.168.1.1'. El puerto es opcional y el valor predeterminado es 2101 para 'no-seguro' y 2601 para 'seguro'. Para el protocolo serie, la direcci\u00f3n debe tener la forma 'tty[:baudios]'. Ejemplo: '/dev/ttyS1'. Los baudios son opcionales y el valor predeterminado es 115200.", + "description": "Elige un sistema descubierto o 'Entrada manual' si no se han descubierto dispositivos.", "title": "Conectar con Control Elk-M1" } } diff --git a/homeassistant/components/emulated_roku/translations/es.json b/homeassistant/components/emulated_roku/translations/es.json index abbcac75479..8904c03ffe2 100644 --- a/homeassistant/components/emulated_roku/translations/es.json +++ b/homeassistant/components/emulated_roku/translations/es.json @@ -6,8 +6,8 @@ "step": { "user": { "data": { - "advertise_ip": "IP para anunciar", - "advertise_port": "Puerto de advertencias", + "advertise_ip": "Direcci\u00f3n IP anunciada", + "advertise_port": "Puerto anunciado", "host_ip": "Direcci\u00f3n IP del host", "listen_port": "Puerto de escucha", "name": "Nombre", diff --git a/homeassistant/components/esphome/translations/es.json b/homeassistant/components/esphome/translations/es.json index f3e73d97a52..f8e933f7711 100644 --- a/homeassistant/components/esphome/translations/es.json +++ b/homeassistant/components/esphome/translations/es.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "ESP ya est\u00e1 configurado", - "already_in_progress": "La configuraci\u00f3n del ESP ya est\u00e1 en marcha", + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "connection_error": "No se puede conectar a ESP. Aseg\u00farate de que tu archivo YAML contenga una l\u00ednea 'api:'.", + "connection_error": "No se puede conectar a ESP. Por favor, aseg\u00farate de que tu archivo YAML contiene una l\u00ednea 'api:'.", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_psk": "La clave de cifrado de transporte no es v\u00e1lida. Por favor, aseg\u00farate de que coincida con lo que tienes en tu configuraci\u00f3n", "resolve_error": "No se puede resolver la direcci\u00f3n del ESP. Si este error persiste, por favor, configura una direcci\u00f3n IP est\u00e1tica" diff --git a/homeassistant/components/fireservicerota/translations/es.json b/homeassistant/components/fireservicerota/translations/es.json index b86ba3b2164..19ba8da21dd 100644 --- a/homeassistant/components/fireservicerota/translations/es.json +++ b/homeassistant/components/fireservicerota/translations/es.json @@ -15,7 +15,7 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Los tokens de autenticaci\u00f3n ya no son v\u00e1lidos, inicia sesi\u00f3n para recrearlos." + "description": "Los tokens de autenticaci\u00f3n dejaron de ser v\u00e1lidos, inicia sesi\u00f3n para volver a crearlos." }, "user": { "data": { diff --git a/homeassistant/components/flume/translations/es.json b/homeassistant/components/flume/translations/es.json index b2ffbe162a8..12152dc0211 100644 --- a/homeassistant/components/flume/translations/es.json +++ b/homeassistant/components/flume/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Esta cuenta ya est\u00e1 configurada", + "already_configured": "La cuenta ya est\u00e1 configurada", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/flunearyou/translations/es.json b/homeassistant/components/flunearyou/translations/es.json index b205be8270c..3a2c41cc735 100644 --- a/homeassistant/components/flunearyou/translations/es.json +++ b/homeassistant/components/flunearyou/translations/es.json @@ -12,7 +12,7 @@ "latitude": "Latitud", "longitude": "Longitud" }, - "description": "Monitorizar reportes de usuarios y del CDC para un par de coordenadas", + "description": "Supervisa los informes del CDC y los basados en los usuarios para un par de coordenadas.", "title": "Configurar Flu Near You" } } diff --git a/homeassistant/components/forked_daapd/translations/es.json b/homeassistant/components/forked_daapd/translations/es.json index 54b1adb479f..af47865dd66 100644 --- a/homeassistant/components/forked_daapd/translations/es.json +++ b/homeassistant/components/forked_daapd/translations/es.json @@ -6,7 +6,7 @@ }, "error": { "forbidden": "No se puede conectar. Compruebe los permisos de red de bifurcaci\u00f3n.", - "unknown_error": "Error desconocido.", + "unknown_error": "Error inesperado", "websocket_not_enabled": "Websocket no activado en servidor forked-daapd.", "wrong_host_or_port": "No se ha podido conectar. Por favor comprueba host y puerto.", "wrong_password": "Contrase\u00f1a incorrecta.", diff --git a/homeassistant/components/freebox/translations/es.json b/homeassistant/components/freebox/translations/es.json index de176304d1f..9cae8cc2089 100644 --- a/homeassistant/components/freebox/translations/es.json +++ b/homeassistant/components/freebox/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado." + "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { "cannot_connect": "Fall\u00f3 la conexi\u00f3n", @@ -11,7 +11,7 @@ "step": { "link": { "description": "Pulsa \"Enviar\", despu\u00e9s pulsa en la flecha derecha en el router para registrar Freebox con Home Assistant\n\n![Localizaci\u00f3n del bot\u00f3n en el router](/static/images/config_freebox.png)", - "title": "Enlazar router Freebox" + "title": "Vincular router Freebox" }, "user": { "data": { diff --git a/homeassistant/components/fritz/translations/es.json b/homeassistant/components/fritz/translations/es.json index a8f5168902f..a5263d4e056 100644 --- a/homeassistant/components/fritz/translations/es.json +++ b/homeassistant/components/fritz/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "ignore_ip6_link_local": "La direcci\u00f3n local del enlace IPv6 no es compatible.", + "ignore_ip6_link_local": "La direcci\u00f3n de enlace local IPv6 no es compatible.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/fritzbox/translations/es.json b/homeassistant/components/fritzbox/translations/es.json index 33dfc59d543..71e5a2f5cf1 100644 --- a/homeassistant/components/fritzbox/translations/es.json +++ b/homeassistant/components/fritzbox/translations/es.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "Este AVM FRITZ!Box ya est\u00e1 configurado.", + "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "ignore_ip6_link_local": "La direcci\u00f3n local del enlace IPv6 no es compatible.", + "ignore_ip6_link_local": "La direcci\u00f3n de enlace local IPv6 no es compatible.", "no_devices_found": "No se encontraron dispositivos en la red", "not_supported": "Conectado a AVM FRITZ!Box pero no es capaz de controlar dispositivos Smart Home.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" @@ -11,7 +11,7 @@ "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, - "flow_title": "AVM FRITZ!Box: {name}", + "flow_title": "{name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/geofency/translations/es.json b/homeassistant/components/geofency/translations/es.json index 8e3c806f708..2462ec5bcc7 100644 --- a/homeassistant/components/geofency/translations/es.json +++ b/homeassistant/components/geofency/translations/es.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, "create_entry": { - "default": "Para enviar eventos a Home Assistant, necesitar\u00e1s configurar la funci\u00f3n de webhook en Geofency.\n\nRellene la siguiente informaci\u00f3n:\n\n- URL: ``{webhook_url}``\n- M\u00e9todo: POST\n\nVer[la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." + "default": "Para enviar eventos a Home Assistant, necesitar\u00e1s configurar la funci\u00f3n de webhook en Geofency.\n\nCompleta la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST\n\nConsulta [la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." }, "step": { "user": { diff --git a/homeassistant/components/glances/translations/en.json b/homeassistant/components/glances/translations/en.json index aa7005bddea..87c53c3cf48 100644 --- a/homeassistant/components/glances/translations/en.json +++ b/homeassistant/components/glances/translations/en.json @@ -11,13 +11,15 @@ "user": { "data": { "host": "Host", + "name": "Name", "password": "Password", "port": "Port", "ssl": "Uses an SSL certificate", "username": "Username", "verify_ssl": "Verify SSL certificate", "version": "Glances API Version (2 or 3)" - } + }, + "title": "Setup Glances" } } }, diff --git a/homeassistant/components/goalzero/translations/es.json b/homeassistant/components/goalzero/translations/es.json index 9772f3f6d91..c75fc143141 100644 --- a/homeassistant/components/goalzero/translations/es.json +++ b/homeassistant/components/goalzero/translations/es.json @@ -19,7 +19,7 @@ "host": "Host", "name": "Nombre" }, - "description": "Primero, tienes que descargar la aplicaci\u00f3n Goal Zero: https://www.goalzero.com/product-features/yeti-app/\n\nSigue las instrucciones para conectar tu Yeti a tu red Wifi. Luego obt\u00e9n la IP de tu router. El DHCP debe estar configurado en los ajustes de tu router para asegurar que la IP de host del dispositivo no cambie. Consulta el manual de usuario de tu router." + "description": "Por favor, consulta la documentaci\u00f3n para asegurarte de que se cumplen todos los requisitos." } } } diff --git a/homeassistant/components/gogogate2/translations/es.json b/homeassistant/components/gogogate2/translations/es.json index 877147e46a0..c585deb9958 100644 --- a/homeassistant/components/gogogate2/translations/es.json +++ b/homeassistant/components/gogogate2/translations/es.json @@ -16,7 +16,7 @@ "username": "Nombre de usuario" }, "description": "Proporciona la informaci\u00f3n requerida a continuaci\u00f3n.", - "title": "Configurar GotoGate2" + "title": "Configurar Gogogate2 o ismartgate" } } } diff --git a/homeassistant/components/gpslogger/translations/es.json b/homeassistant/components/gpslogger/translations/es.json index ea3221ee2f5..bf6f3052e23 100644 --- a/homeassistant/components/gpslogger/translations/es.json +++ b/homeassistant/components/gpslogger/translations/es.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, "create_entry": { - "default": "Para enviar eventos a Home Assistant, necesitar\u00e1s configurar la funci\u00f3n de webhook en GPSLogger.\n\nRellena la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nEcha un vistazo a [la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." + "default": "Para enviar eventos a Home Assistant, necesitar\u00e1s configurar la funci\u00f3n de webhook en GPSLogger.\n\nCompleta la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nConsulta [la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." }, "step": { "user": { diff --git a/homeassistant/components/guardian/translations/el.json b/homeassistant/components/guardian/translations/el.json index 8f6a77ac2ec..2a4963c8649 100644 --- a/homeassistant/components/guardian/translations/el.json +++ b/homeassistant/components/guardian/translations/el.json @@ -17,5 +17,18 @@ "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Elexa Guardian." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03c5\u03c7\u03cc\u03bd \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 `{alternate_service}` \u03bc\u03b5 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2-\u03c3\u03c4\u03cc\u03c7\u03bf\u03c5 `{alternate_target}`. \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03a5\u03a0\u039f\u0392\u039f\u039b\u0397 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03c3\u03b7\u03bc\u03ac\u03bd\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03b6\u03ae\u03c4\u03b7\u03bc\u03b1 \u03c9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03c5\u03bc\u03ad\u03bd\u03bf.", + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } + }, + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/es.json b/homeassistant/components/guardian/translations/es.json index 42c08aba617..c918a3fe583 100644 --- a/homeassistant/components/guardian/translations/es.json +++ b/homeassistant/components/guardian/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "already_in_progress": "La configuraci\u00f3n del dispositivo Guardian ya est\u00e1 en proceso.", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar" }, "step": { diff --git a/homeassistant/components/hangouts/translations/es.json b/homeassistant/components/hangouts/translations/es.json index a2aba99c24c..29fe36ea23c 100644 --- a/homeassistant/components/hangouts/translations/es.json +++ b/homeassistant/components/hangouts/translations/es.json @@ -2,19 +2,18 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "unknown": "Error desconocido" + "unknown": "Error inesperado" }, "error": { - "invalid_2fa": "Autenticaci\u00f3n de 2 factores no v\u00e1lida, por favor, int\u00e9ntelo de nuevo.", - "invalid_2fa_method": "M\u00e9todo 2FA inv\u00e1lido (verificar en el tel\u00e9fono).", + "invalid_2fa": "Autenticaci\u00f3n de 2 factores no v\u00e1lida, por favor, int\u00e9ntalo de nuevo.", + "invalid_2fa_method": "M\u00e9todo 2FA no v\u00e1lido (verificar en el tel\u00e9fono).", "invalid_login": "Inicio de sesi\u00f3n no v\u00e1lido, por favor, int\u00e9ntalo de nuevo." }, "step": { "2fa": { "data": { - "2fa": "Pin 2FA" + "2fa": "PIN 2FA" }, - "description": "Vac\u00edo", "title": "Autenticaci\u00f3n de 2 factores" }, "user": { @@ -23,7 +22,6 @@ "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a" }, - "description": "Vac\u00edo", "title": "Inicio de sesi\u00f3n de Google Chat" } } diff --git a/homeassistant/components/heos/translations/es.json b/homeassistant/components/heos/translations/es.json index dde95e24384..436e2089993 100644 --- a/homeassistant/components/heos/translations/es.json +++ b/homeassistant/components/heos/translations/es.json @@ -11,7 +11,7 @@ "data": { "host": "Host" }, - "description": "Introduce el nombre de host o direcci\u00f3n IP de un dispositivo Heos (preferiblemente conectado por cable a la red).", + "description": "Por favor, introduce el nombre de host o la direcci\u00f3n IP de un dispositivo Heos (preferiblemente uno conectado por cable a la red).", "title": "Conectar a Heos" } } diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index 170df5c4a13..1813b96dd7a 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -12,8 +12,8 @@ "data": { "include_domains": "Dominios para incluir" }, - "description": "Una pasarela Homekit permitir\u00e1 a Homekit acceder a sus entidades de Home Assistant. La pasarela Homekit est\u00e1 limitada a 150 accesorios por instancia incluyendo la propia pasarela. Si desea enlazar m\u00e1s del m\u00e1ximo n\u00famero de accesorios, se recomienda que use multiples pasarelas Homekit para diferentes dominios. Configuraci\u00f3n detallada de la entidad solo est\u00e1 disponible via YAML para la pasarela primaria.", - "title": "Activar pasarela Homekit" + "description": "Elige los dominios a incluir. Se incluir\u00e1n todas las entidades admitidas en el dominio, excepto las entidades categorizadas. Se crear\u00e1 una instancia separada de HomeKit en modo accesorio para cada reproductor multimedia de TV, control remoto basado en actividad, cerradura y c\u00e1mara.", + "title": "Selecciona los dominios que se incluir\u00e1n" } } }, @@ -29,7 +29,7 @@ "data": { "devices": "Dispositivos (Disparadores)" }, - "description": "Esta configuraci\u00f3n solo necesita ser ajustada si el puente HomeKit no es funcional.", + "description": "Se crean interruptores programables para cada dispositivo seleccionado. Cuando se dispara un dispositivo, HomeKit se puede configurar para ejecutar una automatizaci\u00f3n o una escena.", "title": "Configuraci\u00f3n avanzada" }, "cameras": { @@ -38,7 +38,7 @@ "camera_copy": "C\u00e1maras compatibles con transmisiones H.264 nativas" }, "description": "Verifica todas las c\u00e1maras que admitan transmisiones H.264 nativas. Si la c\u00e1mara no emite una transmisi\u00f3n H.264, el sistema transcodificar\u00e1 el video a H.264 para HomeKit. La transcodificaci\u00f3n requiere una CPU de alto rendimiento y es poco probable que funcione en ordenadores de placa \u00fanica.", - "title": "Seleccione el c\u00f3dec de video de la c\u00e1mara." + "title": "Configuraci\u00f3n de la c\u00e1mara" }, "exclude": { "data": { @@ -60,12 +60,12 @@ "include_exclude_mode": "Modo de inclusi\u00f3n", "mode": "Mode de HomeKit" }, - "description": "Las entidades de los \"Dominios que se van a incluir\" se establecer\u00e1n en HomeKit. Podr\u00e1 seleccionar qu\u00e9 entidades excluir de esta lista en la siguiente pantalla.", + "description": "HomeKit se puede configurar para exponer un puente o un solo accesorio. En el modo accesorio, solo se puede usar una sola entidad. El modo accesorio es necesario para que los reproductores multimedia con la clase de dispositivo TV funcionen correctamente. Las entidades en los \"Dominios para incluir\" se incluir\u00e1n en HomeKit. Podr\u00e1s seleccionar qu\u00e9 entidades incluir o excluir de esta lista en la siguiente pantalla.", "title": "Selecciona el modo y dominios." }, "yaml": { "description": "Esta entrada se controla a trav\u00e9s de YAML", - "title": "Ajustar las opciones del puente HomeKit" + "title": "Ajustar las opciones de HomeKit" } } } diff --git a/homeassistant/components/homekit_controller/translations/es.json b/homeassistant/components/homekit_controller/translations/es.json index 48015d0418d..b446c87e70f 100644 --- a/homeassistant/components/homekit_controller/translations/es.json +++ b/homeassistant/components/homekit_controller/translations/es.json @@ -3,20 +3,20 @@ "abort": { "accessory_not_found_error": "No se puede a\u00f1adir el emparejamiento porque ya no se puede encontrar el dispositivo.", "already_configured": "El accesorio ya est\u00e1 configurado con este controlador.", - "already_in_progress": "El flujo de configuraci\u00f3n del dispositivo ya est\u00e1 en marcha.", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicia el accesorio e int\u00e9ntalo de nuevo.", "ignored_model": "El soporte de HomeKit para este modelo est\u00e1 bloqueado ya que est\u00e1 disponible una integraci\u00f3n nativa m\u00e1s completa.", - "invalid_config_entry": "Este dispositivo se muestra como listo para vincular, pero ya existe una entrada que causa conflicto en Home Assistant y se debe eliminar primero.", + "invalid_config_entry": "Este dispositivo se muestra como listo para emparejarse, pero ya hay una entrada de configuraci\u00f3n conflictiva para \u00e9l en Home Assistant que primero debe eliminarse.", "invalid_properties": "Propiedades no v\u00e1lidas anunciadas por dispositivo.", "no_devices": "No se encontraron dispositivos no emparejados" }, "error": { - "authentication_error": "C\u00f3digo HomeKit incorrecto. Por favor, compru\u00e9belo e int\u00e9ntelo de nuevo.", + "authentication_error": "C\u00f3digo de HomeKit incorrecto. Por favor, rev\u00edsalo e int\u00e9ntalo de nuevo.", "insecure_setup_code": "El c\u00f3digo de configuraci\u00f3n solicitado no es seguro debido a su naturaleza trivial. Este accesorio no cumple con los requisitos b\u00e1sicos de seguridad.", "max_peers_error": "El dispositivo rechaz\u00f3 el emparejamiento ya que no tiene almacenamiento de emparejamientos libres.", - "pairing_failed": "Se ha producido un error no controlado al intentar emparejarse con este dispositivo. Esto puede ser un fallo temporal o que tu dispositivo no est\u00e9 admitido en este momento.", - "unable_to_pair": "No se ha podido emparejar, por favor int\u00e9ntelo de nuevo.", - "unknown_error": "El dispositivo report\u00f3 un error desconocido. La vinculaci\u00f3n ha fallado." + "pairing_failed": "Se produjo un error no controlado al intentar emparejar con este dispositivo. Esto puede ser un fallo temporal o que tu dispositivo no sea compatible actualmente.", + "unable_to_pair": "No se puede emparejar, int\u00e9ntalo de nuevo.", + "unknown_error": "El dispositivo report\u00f3 un error desconocido. El emparejamiento ha fallado." }, "flow_title": "{name} ({category})", "step": { @@ -31,10 +31,10 @@ "pair": { "data": { "allow_insecure_setup_codes": "Permitir el emparejamiento con c\u00f3digos de configuraci\u00f3n inseguros.", - "pairing_code": "C\u00f3digo de vinculaci\u00f3n" + "pairing_code": "C\u00f3digo de emparejamiento" }, "description": "El controlador HomeKit se comunica con {name} ({category}) a trav\u00e9s de la red de \u00e1rea local mediante una conexi\u00f3n cifrada segura sin un controlador HomeKit o iCloud por separado. Introduce tu c\u00f3digo de emparejamiento de HomeKit (en el formato XXX-XX-XXX) para usar este accesorio. Este c\u00f3digo suele encontrarse en el propio dispositivo o en el embalaje.", - "title": "Vincular un dispositivo a trav\u00e9s del protocolo de accesorios HomeKit" + "title": "Emparejar con un dispositivo a trav\u00e9s del protocolo de accesorios HomeKit" }, "protocol_error": { "description": "Es posible que el dispositivo no est\u00e9 en modo de emparejamiento y que requiera que se presione un bot\u00f3n f\u00edsico o virtual. Aseg\u00farate de que el dispositivo est\u00e1 en modo de emparejamiento o intenta reiniciar el dispositivo, luego contin\u00faa para reanudar el emparejamiento.", @@ -44,7 +44,7 @@ "data": { "device": "Dispositivo" }, - "description": "El controlador de HomeKit se comunica a trav\u00e9s de la red de \u00e1rea local usando una conexi\u00f3n encriptada segura sin un controlador HomeKit separado o iCloud. Selecciona el dispositivo que quieres vincular:", + "description": "El controlador HomeKit se comunica a trav\u00e9s de la red de \u00e1rea local mediante una conexi\u00f3n cifrada segura sin un controlador HomeKit o iCloud por separado. Selecciona el dispositivo que deseas emparejar:", "title": "Selecci\u00f3n del dispositivo" } } diff --git a/homeassistant/components/homematicip_cloud/translations/es.json b/homeassistant/components/homematicip_cloud/translations/es.json index 454fa8f9f2a..afc637bbaa7 100644 --- a/homeassistant/components/homematicip_cloud/translations/es.json +++ b/homeassistant/components/homematicip_cloud/translations/es.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "El punto de acceso ya est\u00e1 configurado", - "connection_aborted": "Fall\u00f3 la conexi\u00f3n", - "unknown": "Se ha producido un error desconocido." + "already_configured": "El dispositivo ya est\u00e1 configurado", + "connection_aborted": "No se pudo conectar", + "unknown": "Error inesperado" }, "error": { - "invalid_sgtin_or_pin": "PIN no v\u00e1lido, por favor int\u00e9ntalo de nuevo.", + "invalid_sgtin_or_pin": "SGTIN o C\u00f3digo PIN no v\u00e1lido, int\u00e9ntalo de nuevo.", "press_the_button": "Por favor, pulsa el bot\u00f3n azul", "register_failed": "No se pudo registrar, por favor int\u00e9ntalo de nuevo.", "timeout_button": "Se agot\u00f3 el tiempo de espera para presionar el bot\u00f3n azul. Vuelve a intentarlo." @@ -15,14 +15,14 @@ "init": { "data": { "hapid": "ID de punto de acceso (SGTIN)", - "name": "Nombre (opcional, utilizado como prefijo para todos los dispositivos)", - "pin": "C\u00f3digo PIN (opcional)" + "name": "Nombre (opcional, usado como prefijo de nombre para todos los dispositivos)", + "pin": "C\u00f3digo PIN" }, "title": "Elegir punto de acceso HomematicIP" }, "link": { "description": "Pulsa el bot\u00f3n azul en el punto de acceso y el bot\u00f3n de env\u00edo para registrar HomematicIP en Home Assistant.\n\n![Ubicaci\u00f3n del bot\u00f3n en la pasarela](/static/images/config_flows/config_homematicip_cloud.png)", - "title": "Enlazar punto de acceso" + "title": "Vincular punto de acceso" } } } diff --git a/homeassistant/components/huawei_lte/translations/es.json b/homeassistant/components/huawei_lte/translations/es.json index 4d1ffba4608..9bef5d09eec 100644 --- a/homeassistant/components/huawei_lte/translations/es.json +++ b/homeassistant/components/huawei_lte/translations/es.json @@ -21,7 +21,7 @@ "url": "URL", "username": "Nombre de usuario" }, - "description": "Introduzca los detalles de acceso al dispositivo. La especificaci\u00f3n del nombre de usuario y la contrase\u00f1a es opcional, pero permite admitir m\u00e1s funciones de integraci\u00f3n. Por otro lado, el uso de una conexi\u00f3n autorizada puede causar problemas para acceder a la interfaz web del dispositivo desde fuera de Home Assistant mientras la integraci\u00f3n est\u00e1 activa, y viceversa.", + "description": "Introduce los detalles de acceso del dispositivo.", "title": "Configurar Huawei LTE" } } diff --git a/homeassistant/components/hue/translations/es.json b/homeassistant/components/hue/translations/es.json index db5a72c7fe2..456668fec88 100644 --- a/homeassistant/components/hue/translations/es.json +++ b/homeassistant/components/hue/translations/es.json @@ -13,7 +13,7 @@ }, "error": { "linking": "Error inesperado", - "register_failed": "No se pudo registrar, intente de nuevo" + "register_failed": "No se pudo registrar, int\u00e9ntalo de nuevo" }, "step": { "init": { @@ -23,8 +23,8 @@ "title": "Elige la pasarela Hue" }, "link": { - "description": "Presione el bot\u00f3n en la pasarela para registrar Philips Hue con Home Assistant. \n\n![Ubicaci\u00f3n del bot\u00f3n en la pasarela](/static/images/config_philips_hue.jpg)", - "title": "Link Hub" + "description": "Presiona el bot\u00f3n en la pasarela para registrar Philips Hue con Home Assistant. \n\n![Ubicaci\u00f3n del bot\u00f3n en la pasarela](/static/images/config_philips_hue.jpg)", + "title": "Vincular pasarela" }, "manual": { "data": { diff --git a/homeassistant/components/hunterdouglas_powerview/translations/es.json b/homeassistant/components/hunterdouglas_powerview/translations/es.json index 924edd5394f..c0d2ad2e4e5 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/es.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo", + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/iaqualink/translations/es.json b/homeassistant/components/iaqualink/translations/es.json index 7587b393c3f..60f26b1c64d 100644 --- a/homeassistant/components/iaqualink/translations/es.json +++ b/homeassistant/components/iaqualink/translations/es.json @@ -13,7 +13,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Introduce el nombre de usuario y contrase\u00f1a de tu cuenta de iAqualink.", + "description": "Por favor, introduce el nombre de usuario y contrase\u00f1a de tu cuenta iAqualink.", "title": "Conexi\u00f3n con iAqualink" } } diff --git a/homeassistant/components/icloud/translations/es.json b/homeassistant/components/icloud/translations/es.json index 31db2283f66..9140c843483 100644 --- a/homeassistant/components/icloud/translations/es.json +++ b/homeassistant/components/icloud/translations/es.json @@ -16,7 +16,7 @@ "password": "Contrase\u00f1a" }, "description": "La contrase\u00f1a introducida anteriormente para {username} ya no funciona. Actualiza tu contrase\u00f1a para seguir usando esta integraci\u00f3n.", - "title": "Credenciales de iCloud" + "title": "Volver a autenticar la integraci\u00f3n" }, "trusted_device": { "data": { diff --git a/homeassistant/components/ifttt/translations/es.json b/homeassistant/components/ifttt/translations/es.json index a296af6a6d6..6ffb7b9e421 100644 --- a/homeassistant/components/ifttt/translations/es.json +++ b/homeassistant/components/ifttt/translations/es.json @@ -6,12 +6,12 @@ "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, "create_entry": { - "default": "Para enviar eventos a Home Assistant debes usar la acci\u00f3n \"Make a web request\" del [applet IFTTT Webhook]({applet_url}).\n\nCompleta la siguiente informaci\u00f3n: \n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST\n- Tipo de contenido: application/json\n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." + "default": "Para enviar eventos a Home Assistant, deber\u00e1s usar la acci\u00f3n \"Hacer una solicitud web\" del [applet IFTTT Webhook]({applet_url}). \n\nCompleta la siguiente informaci\u00f3n: \n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST\n- Tipo de contenido: application/json \n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar automatizaciones para manejar los datos entrantes." }, "step": { "user": { "description": "\u00bfEst\u00e1s seguro de que quieres configurar IFTTT?", - "title": "Configurar el applet de webhook IFTTT" + "title": "Configurar el applet IFTTT Webhook" } } } diff --git a/homeassistant/components/insteon/translations/es.json b/homeassistant/components/insteon/translations/es.json index 768c590151e..730277dd5c0 100644 --- a/homeassistant/components/insteon/translations/es.json +++ b/homeassistant/components/insteon/translations/es.json @@ -6,7 +6,7 @@ "single_instance_allowed": "Ya esta configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { - "cannot_connect": "No se conect\u00f3 al m\u00f3dem Insteon, por favor, int\u00e9ntelo de nuevo.", + "cannot_connect": "No se pudo conectar", "select_single": "Seleccione una opci\u00f3n." }, "flow_title": "{name}", @@ -36,7 +36,7 @@ "data": { "device": "Ruta del dispositivo USB" }, - "description": "Configure el M\u00f3dem Insteon PowerLink (PLM).", + "description": "Configura el M\u00f3dem Insteon PowerLink (PLM).", "title": "Insteon PLM" }, "user": { diff --git a/homeassistant/components/ipp/translations/es.json b/homeassistant/components/ipp/translations/es.json index f6948374561..a5a067b5ffd 100644 --- a/homeassistant/components/ipp/translations/es.json +++ b/homeassistant/components/ipp/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dispositivo ya configurado", + "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", "connection_upgrade": "No se pudo conectar con la impresora debido a que se requiere una actualizaci\u00f3n de la conexi\u00f3n.", "ipp_error": "Error IPP encontrado.", @@ -13,7 +13,7 @@ "cannot_connect": "No se pudo conectar", "connection_upgrade": "No se pudo conectar con la impresora. Int\u00e9ntalo de nuevo con la opci\u00f3n SSL/TLS marcada." }, - "flow_title": "Impresora: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/iqvia/translations/es.json b/homeassistant/components/iqvia/translations/es.json index cecbb7af591..dc26ca6c065 100644 --- a/homeassistant/components/iqvia/translations/es.json +++ b/homeassistant/components/iqvia/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Este c\u00f3digo postal ya ha sido configurado." + "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { "invalid_zip_code": "El c\u00f3digo postal no es v\u00e1lido" diff --git a/homeassistant/components/isy994/translations/es.json b/homeassistant/components/isy994/translations/es.json index dd5e97923d6..4c3c4475743 100644 --- a/homeassistant/components/isy994/translations/es.json +++ b/homeassistant/components/isy994/translations/es.json @@ -41,8 +41,8 @@ "sensor_string": "Cadena Nodo Sensor", "variable_sensor_string": "Cadena de Sensor Variable" }, - "description": "Configura las opciones para la integraci\u00f3n de ISY: \n \u2022 Cadena Nodo Sensor: Cualquier dispositivo o carpeta que contenga 'Cadena Nodo Sensor' en el nombre ser\u00e1 tratada como un sensor o un sensor binario. \n \u2022 Ignorar Cadena: Cualquier dispositivo con 'Ignorar Cadena' en el nombre ser\u00e1 ignorado. \n \u2022 Restaurar Intensidad de la Luz: Si se habilita, la intensidad anterior ser\u00e1 restaurada al encender una luz en lugar de usar el nivel predeterminado del dispositivo.", - "title": "Opciones ISY994" + "description": "Configura las opciones para la integraci\u00f3n ISY:\n\u2022 Cadena de sensor de nodo: Cualquier dispositivo o carpeta que contenga 'Cadena de sensor de nodo' en el nombre se tratar\u00e1 como un sensor o un sensor binario.\n\u2022 Ignorar Cadena: Cualquier dispositivo con 'Ignorar Cadena' en el nombre ser\u00e1 ignorado.\n\u2022 Cadena de sensor variable: cualquier variable que contenga 'Cadena de sensor variable' se a\u00f1adir\u00e1 como sensor.\n\u2022 Restaurar brillo de luz: si est\u00e1 habilitado, el brillo anterior se restaurar\u00e1 al encender una luz en lugar del nivel de encendido integrado del dispositivo.", + "title": "Opciones ISY" } } }, diff --git a/homeassistant/components/kodi/translations/es.json b/homeassistant/components/kodi/translations/es.json index 290d7aa53ee..0b7b37a6e11 100644 --- a/homeassistant/components/kodi/translations/es.json +++ b/homeassistant/components/kodi/translations/es.json @@ -12,7 +12,7 @@ "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "unknown": "Error inesperado" }, - "flow_title": "Kodi: {name}", + "flow_title": "{name}", "step": { "credentials": { "data": { @@ -37,7 +37,7 @@ "data": { "ws_port": "Puerto" }, - "description": "El puerto WebSocket (a veces llamado puerto TCP en Kodi). Para conectarse a trav\u00e9s de WebSocket, necesitas habilitar \"Permitir a los programas... controlar Kodi\" en Sistema/Configuraci\u00f3n/Red/Servicios. Si el WebSocket no est\u00e1 habilitado, elimine el puerto y d\u00e9jelo vac\u00edo." + "description": "El puerto WebSocket (a veces llamado puerto TCP en Kodi). Para conectar a trav\u00e9s de WebSocket, debes habilitar \"Permitir que los programas... controlen Kodi\" en Sistema/Configuraci\u00f3n/Red/Servicios. Si WebSocket no est\u00e1 habilitado, elimina el puerto y d\u00e9jalo vac\u00edo." } } }, diff --git a/homeassistant/components/konnected/translations/es.json b/homeassistant/components/konnected/translations/es.json index 901c858b33c..7ba726e8e6a 100644 --- a/homeassistant/components/konnected/translations/es.json +++ b/homeassistant/components/konnected/translations/es.json @@ -5,7 +5,7 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "cannot_connect": "No se pudo conectar", "not_konn_panel": "No es un dispositivo Konnected.io reconocido", - "unknown": "Se produjo un error desconocido" + "unknown": "Error inesperado" }, "error": { "cannot_connect": "No se pudo conectar" @@ -39,19 +39,19 @@ "options_binary": { "data": { "inverse": "Invertir el estado de apertura/cierre", - "name": "Nombre (opcional)", + "name": "Nombre", "type": "Tipo de sensor binario" }, - "description": "Seleccione las opciones para el sensor binario conectado a {zone}", + "description": "Opciones de {zone}", "title": "Configurar sensor binario" }, "options_digital": { "data": { - "name": "Nombre (opcional)", - "poll_interval": "Intervalo de sondeo (minutos) (opcional)", + "name": "Nombre", + "poll_interval": "Intervalo de sondeo (minutos)", "type": "Tipo de sensor" }, - "description": "Seleccione las opciones para el sensor digital conectado a {zone}", + "description": "Opciones de {zone}", "title": "Configurar el sensor digital" }, "options_io": { @@ -84,8 +84,8 @@ }, "options_misc": { "data": { - "api_host": "Sustituye la URL del host de la API (opcional)", - "blink": "Parpadea el LED del panel cuando se env\u00eda un cambio de estado", + "api_host": "Anular la URL de la API del host", + "blink": "Parpadear el LED del panel al enviar un cambio de estado", "discovery": "Responde a las solicitudes de descubrimiento en tu red", "override_api_host": "Reemplazar la URL predeterminada del panel host de la API de Home Assistant" }, @@ -95,13 +95,13 @@ "options_switch": { "data": { "activation": "Salida cuando est\u00e1 activada", - "momentary": "Duraci\u00f3n del pulso (ms) (opcional)", + "momentary": "Duraci\u00f3n del pulso (ms)", "more_states": "Configurar estados adicionales para esta zona", - "name": "Nombre (opcional)", - "pause": "Pausa entre pulsos (ms) (opcional)", - "repeat": "Tiempos de repetici\u00f3n (-1 = infinito) (opcional)" + "name": "Nombre", + "pause": "Pausa entre pulsos (ms)", + "repeat": "Veces que se repite (-1=infinito)" }, - "description": "Selecciona las opciones de salida para {zone}: state {state}", + "description": "Opciones de {zone}: estado {state}", "title": "Configurar la salida conmutable" } } diff --git a/homeassistant/components/locative/translations/es.json b/homeassistant/components/locative/translations/es.json index e3c63d7b35d..676c1863b2e 100644 --- a/homeassistant/components/locative/translations/es.json +++ b/homeassistant/components/locative/translations/es.json @@ -6,11 +6,11 @@ "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, "create_entry": { - "default": "Para enviar ubicaciones a Home Assistant, es necesario configurar la caracter\u00edstica webhook en la app de Locative.\n\nRellena la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST\n\nRevisa [la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." + "default": "Para enviar ubicaciones a Home Assistant, es necesario configurar la caracter\u00edstica webhook en la app de Locative.\n\nCompleta la siguiente informaci\u00f3n:\n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST\n\nConsulta [la documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." }, "step": { "user": { - "description": "\u00bfEst\u00e1s seguro de que quieres configurar el webhook de Locative?", + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?", "title": "Configurar el webhook de Locative" } } diff --git a/homeassistant/components/logi_circle/translations/es.json b/homeassistant/components/logi_circle/translations/es.json index 7fe62a28d05..ac1fdc8dc5a 100644 --- a/homeassistant/components/logi_circle/translations/es.json +++ b/homeassistant/components/logi_circle/translations/es.json @@ -2,18 +2,18 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "external_error": "Se produjo una excepci\u00f3n de otro flujo.", - "external_setup": "Logi Circle se ha configurado correctamente a partir de otro flujo.", + "external_error": "Ocurri\u00f3 una excepci\u00f3n de otro flujo.", + "external_setup": "Logi Circle se configur\u00f3 correctamente desde otro flujo.", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n." }, "error": { "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "follow_link": "Accede al enlace e identif\u00edcate antes de pulsar Enviar.", + "follow_link": "Por favor, sigue el enlace y autent\u00edcate antes de presionar Enviar.", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "auth": { - "description": "Accede al siguiente enlace y Acepta el acceso a tu cuenta Logi Circle, despu\u00e9s vuelve y pulsa en Enviar a continuaci\u00f3n.\n\n[Link]({authorization_url})", + "description": "Por favor, sigue el enlace a continuaci\u00f3n y **Acepta** el acceso a tu cuenta Logi Circle, luego regresa y presiona **Enviar** a continuaci\u00f3n. \n\n[Enlace]({authorization_url})", "title": "Autenticaci\u00f3n con Logi Circle" }, "user": { diff --git a/homeassistant/components/lutron_caseta/translations/es.json b/homeassistant/components/lutron_caseta/translations/es.json index d13fded562e..c6e90ec6364 100644 --- a/homeassistant/components/lutron_caseta/translations/es.json +++ b/homeassistant/components/lutron_caseta/translations/es.json @@ -15,7 +15,7 @@ "title": "Error al importar la configuraci\u00f3n del bridge Cas\u00e9ta." }, "link": { - "description": "Para emparejar con {name} ({host}), despu\u00e9s de enviar este formulario, presione el bot\u00f3n negro en la parte posterior del puente.", + "description": "Para emparejar con {name} ({host}), despu\u00e9s de enviar este formulario, presiona el bot\u00f3n negro en la parte posterior del puente.", "title": "Emparejar con el puente" }, "user": { diff --git a/homeassistant/components/mailgun/translations/es.json b/homeassistant/components/mailgun/translations/es.json index 1fcd54a5266..85007655134 100644 --- a/homeassistant/components/mailgun/translations/es.json +++ b/homeassistant/components/mailgun/translations/es.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, "create_entry": { - "default": "Para enviar eventos a Home Assistant debes configurar los [Webhooks en Mailgun]({mailgun_url}). \n\n Completa la siguiente informaci\u00f3n: \n\n - URL: `{webhook_url}` \n - M\u00e9todo: POST \n - Tipo de contenido: application/json \n\n Consulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." + "default": "Para enviar eventos a Home Assistant debes configurar los [Webhooks en Mailgun]({mailgun_url}). \n\n Completa la siguiente informaci\u00f3n: \n\n- URL: `{webhook_url}` \n- M\u00e9todo: POST \n- Tipo de contenido: application/json \n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." }, "step": { "user": { diff --git a/homeassistant/components/melcloud/translations/es.json b/homeassistant/components/melcloud/translations/es.json index be4f6cabe6c..94583ed2589 100644 --- a/homeassistant/components/melcloud/translations/es.json +++ b/homeassistant/components/melcloud/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "Fall\u00f3 la conexi\u00f3n", - "invalid_auth": "Autentificaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/meteo_france/translations/es.json b/homeassistant/components/meteo_france/translations/es.json index cd6d2d80812..a16c47e4aa9 100644 --- a/homeassistant/components/meteo_france/translations/es.json +++ b/homeassistant/components/meteo_france/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada", - "unknown": "Error desconocido: por favor, vuelva a intentarlo m\u00e1s tarde" + "unknown": "Error inesperado" }, "error": { "empty": "No hay resultado en la b\u00fasqueda de la ciudad: por favor, comprueba el campo de la ciudad" diff --git a/homeassistant/components/mikrotik/translations/es.json b/homeassistant/components/mikrotik/translations/es.json index d4751c19a9a..b1cd62a1763 100644 --- a/homeassistant/components/mikrotik/translations/es.json +++ b/homeassistant/components/mikrotik/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Conexi\u00f3n fallida", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "name_exists": "El nombre ya existe" }, diff --git a/homeassistant/components/minecraft_server/translations/es.json b/homeassistant/components/minecraft_server/translations/es.json index 1c3891d3fb5..a5c5ae531d9 100644 --- a/homeassistant/components/minecraft_server/translations/es.json +++ b/homeassistant/components/minecraft_server/translations/es.json @@ -15,7 +15,7 @@ "name": "Nombre" }, "description": "Configura tu instancia de Minecraft Server para permitir la supervisi\u00f3n.", - "title": "Enlace su servidor Minecraft" + "title": "Vincula tu servidor Minecraft" } } } diff --git a/homeassistant/components/mobile_app/translations/es.json b/homeassistant/components/mobile_app/translations/es.json index 8ac5c909e17..adb7b4212a9 100644 --- a/homeassistant/components/mobile_app/translations/es.json +++ b/homeassistant/components/mobile_app/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "install_app": "Abre la aplicaci\u00f3n en el m\u00f3vil para configurar la integraci\u00f3n con Home Assistant. Echa un vistazo a [la documentaci\u00f3n]({apps_url}) para ver una lista de apps compatibles." + "install_app": "Abre la aplicaci\u00f3n m\u00f3vil para configurar la integraci\u00f3n con Home Assistant. Consulta [la documentaci\u00f3n]({apps_url}) para obtener una lista de aplicaciones compatibles." }, "step": { "confirm": { diff --git a/homeassistant/components/motion_blinds/translations/es.json b/homeassistant/components/motion_blinds/translations/es.json index e316f882712..c7469a93820 100644 --- a/homeassistant/components/motion_blinds/translations/es.json +++ b/homeassistant/components/motion_blinds/translations/es.json @@ -8,7 +8,7 @@ "error": { "discovery_error": "No se pudo descubrir un detector de movimiento" }, - "flow_title": "Motion Blinds", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index 915af215a26..4c88a5a66f7 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo se permite una \u00fanica configuraci\u00f3n." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { - "cannot_connect": "No se puede conectar" + "cannot_connect": "No se pudo conectar" }, "step": { "broker": { @@ -16,13 +16,13 @@ "port": "Puerto", "username": "Nombre de usuario" }, - "description": "Por favor, introduzca la informaci\u00f3n de conexi\u00f3n de su br\u00f3ker MQTT." + "description": "Por favor, introduce la informaci\u00f3n de conexi\u00f3n de tu br\u00f3ker MQTT." }, "hassio_confirm": { "data": { "discovery": "Habilitar descubrimiento" }, - "description": "\u00bfQuieres configurar Home Assistant para conectarse al agente MQTT proporcionado por el complemento {addon} ?", + "description": "\u00bfQuieres configurar Home Assistant para conectarse al agente MQTT proporcionado por el complemento {addon}?", "title": "Br\u00f3ker MQTT a trav\u00e9s de complemento de Home Assistant" } } diff --git a/homeassistant/components/myq/translations/es.json b/homeassistant/components/myq/translations/es.json index a6b81fbcbbc..9f20a31ff15 100644 --- a/homeassistant/components/myq/translations/es.json +++ b/homeassistant/components/myq/translations/es.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "MyQ ya est\u00e1 configurado", + "already_configured": "El servicio ya est\u00e1 configurado", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/nanoleaf/translations/es.json b/homeassistant/components/nanoleaf/translations/es.json index 00ba28474ac..12ae1b235ba 100644 --- a/homeassistant/components/nanoleaf/translations/es.json +++ b/homeassistant/components/nanoleaf/translations/es.json @@ -16,7 +16,7 @@ "step": { "link": { "description": "Mant\u00e9n presionado el bot\u00f3n de encendido de tu Nanoleaf durante 5 segundos hasta que los LED del bot\u00f3n comiencen a parpadear, luego haz clic en **ENVIAR** en los siguientes 30 segundos.", - "title": "Enlazar Nanoleaf" + "title": "Vincular Nanoleaf" }, "user": { "data": { diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index 36b5d64e253..93dd7eb12a2 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -5,7 +5,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", "invalid_access_token": "Token de acceso no v\u00e1lido", "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", @@ -70,7 +70,7 @@ "data": { "code": "C\u00f3digo PIN" }, - "description": "Para vincular tu cuenta de Nest, [autoriza tu cuenta]({url}).\n\nDespu\u00e9s de la autorizaci\u00f3n, copia y pega el c\u00f3digo pin a continuaci\u00f3n.", + "description": "Para vincular tu cuenta Nest, [autoriza tu cuenta]({url}). \n\nDespu\u00e9s de la autorizaci\u00f3n, copia y pega el c\u00f3digo PIN proporcionado a continuaci\u00f3n.", "title": "Vincular cuenta de Nest" }, "pick_implementation": { diff --git a/homeassistant/components/netatmo/translations/es.json b/homeassistant/components/netatmo/translations/es.json index c658c0046a1..0d8e4167ab8 100644 --- a/homeassistant/components/netatmo/translations/es.json +++ b/homeassistant/components/netatmo/translations/es.json @@ -47,10 +47,10 @@ "public_weather": { "data": { "area_name": "Nombre del \u00e1rea", - "lat_ne": "Longitud esquina noreste", - "lat_sw": "Latitud esquina suroeste", - "lon_ne": "Longitud esquina noreste", - "lon_sw": "Longitud esquina Suroeste", + "lat_ne": "Latitud Esquina noreste", + "lat_sw": "Latitud Esquina suroeste", + "lon_ne": "Longitud Esquina noreste", + "lon_sw": "Longitud Esquina suroeste", "mode": "C\u00e1lculo", "show_on_map": "Mostrar en el mapa" }, diff --git a/homeassistant/components/nexia/translations/es.json b/homeassistant/components/nexia/translations/es.json index ee89d9db6f5..9e9e720dc15 100644 --- a/homeassistant/components/nexia/translations/es.json +++ b/homeassistant/components/nexia/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Este nexia home ya est\u00e1 configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { "cannot_connect": "Fall\u00f3 la conexi\u00f3n", @@ -13,7 +13,7 @@ "data": { "brand": "Marca", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" } } } diff --git a/homeassistant/components/nightscout/translations/es.json b/homeassistant/components/nightscout/translations/es.json index d98b8e345b6..ab9ce8baa02 100644 --- a/homeassistant/components/nightscout/translations/es.json +++ b/homeassistant/components/nightscout/translations/es.json @@ -14,7 +14,7 @@ "api_key": "Clave API", "url": "URL" }, - "description": "- URL: la direcci\u00f3n de tu instancia de nightscout. Por ejemplo: https://myhomeassistant.duckdns.org:5423 \n- Clave API (opcional): util\u00edzala s\u00f3lo si tu instancia est\u00e1 protegida (auth_default_roles! = readable).", + "description": "- URL: la direcci\u00f3n de tu instancia nightscout. Por ejemplo: https://myhomeassistant.duckdns.org:5423\n- Clave de API (opcional): usar solo si tu instancia est\u00e1 protegida (auth_default_roles != legible).", "title": "Introduce la informaci\u00f3n del servidor de Nightscout." } } diff --git a/homeassistant/components/nws/translations/es.json b/homeassistant/components/nws/translations/es.json index 4e7732c062d..522e601d050 100644 --- a/homeassistant/components/nws/translations/es.json +++ b/homeassistant/components/nws/translations/es.json @@ -15,7 +15,7 @@ "longitude": "Longitud", "station": "C\u00f3digo de estaci\u00f3n METAR" }, - "description": "Si no se especifica un c\u00f3digo de estaci\u00f3n METAR, se utilizar\u00e1n la latitud y la longitud para encontrar la estaci\u00f3n m\u00e1s cercana.", + "description": "Si no se especifica un c\u00f3digo de estaci\u00f3n METAR, la latitud y la longitud se utilizar\u00e1n para encontrar la estaci\u00f3n m\u00e1s cercana. Por ahora, una clave API puede ser cualquier cosa. Se recomienda utilizar una direcci\u00f3n de correo electr\u00f3nico v\u00e1lida.", "title": "Conectar con el National Weather Service" } } diff --git a/homeassistant/components/onewire/translations/es.json b/homeassistant/components/onewire/translations/es.json index 986a36a30d4..81a4d564d9c 100644 --- a/homeassistant/components/onewire/translations/es.json +++ b/homeassistant/components/onewire/translations/es.json @@ -12,7 +12,7 @@ "host": "Host", "port": "Puerto" }, - "title": "Configurar 1 cable" + "title": "Establecer detalles del servidor" } } }, diff --git a/homeassistant/components/onvif/translations/es.json b/homeassistant/components/onvif/translations/es.json index 7858a4561fe..aa1d40e3f14 100644 --- a/homeassistant/components/onvif/translations/es.json +++ b/homeassistant/components/onvif/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositivo ONVIF ya est\u00e1 configurado.", - "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ONVIF ya est\u00e1 en marcha.", + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "no_h264": "No hab\u00eda transmisiones H264 disponibles. Verifique la configuraci\u00f3n del perfil en su dispositivo.", "no_mac": "No se pudo configurar una identificaci\u00f3n \u00fanica para el dispositivo ONVIF.", "onvif_error": "Error de configuraci\u00f3n del dispositivo ONVIF. Comprueba el registro para m\u00e1s informaci\u00f3n." diff --git a/homeassistant/components/owntracks/translations/es.json b/homeassistant/components/owntracks/translations/es.json index dc420a806f4..55f5a45dd54 100644 --- a/homeassistant/components/owntracks/translations/es.json +++ b/homeassistant/components/owntracks/translations/es.json @@ -5,7 +5,7 @@ "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "create_entry": { - "default": "\n\nEn Android, abre [la aplicaci\u00f3n OwnTracks]({android_url}), ve a preferencias -> conexi\u00f3n. Cambia los siguientes ajustes:\n - Modo: HTTP privado\n - Host: {webhook_url}\n - Identificaci\u00f3n:\n - Nombre de usuario: \n - ID de dispositivo: \n\nEn iOS, abre [la aplicaci\u00f3n OwnTracks] ({ios_url}), pulsa el icono (i) en la parte superior izquierda -> configuraci\u00f3n. Cambia los siguientes ajustes:\n - Modo: HTTP\n - URL: {webhook_url}\n - Activar la autenticaci\u00f3n\n - UserID: \n\n{secret}\n\nConsulta [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s informaci\u00f3n." + "default": "\n\nEn Android, abre [la aplicaci\u00f3n OwnTracks]({android_url}), ve a preferencias -> conexi\u00f3n. Cambia los siguientes ajustes:\n - Modo: HTTP privado\n - Host: {webhook_url}\n - Identificaci\u00f3n:\n - Nombre de usuario: \n - ID de dispositivo: \n\nEn iOS, abre [la aplicaci\u00f3n OwnTracks]({ios_url}), pulsa el icono (i) en la parte superior izquierda -> configuraci\u00f3n. Cambia los siguientes ajustes:\n - Modo: HTTP\n - URL: {webhook_url}\n - Activar la autenticaci\u00f3n\n - UserID: \n\n{secret}\n\nConsulta [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s informaci\u00f3n." }, "step": { "user": { diff --git a/homeassistant/components/pi_hole/translations/es.json b/homeassistant/components/pi_hole/translations/es.json index 35597af49f2..dca8e8c7f98 100644 --- a/homeassistant/components/pi_hole/translations/es.json +++ b/homeassistant/components/pi_hole/translations/es.json @@ -19,9 +19,9 @@ "location": "Ubicaci\u00f3n", "name": "Nombre", "port": "Puerto", - "ssl": "Usar SSL", + "ssl": "Utiliza un certificado SSL", "statistics_only": "S\u00f3lo las estad\u00edsticas", - "verify_ssl": "Verificar certificado SSL" + "verify_ssl": "Verificar el certificado SSL" } } } diff --git a/homeassistant/components/plaato/translations/es.json b/homeassistant/components/plaato/translations/es.json index 693c6bf99c7..df1ffb99cb1 100644 --- a/homeassistant/components/plaato/translations/es.json +++ b/homeassistant/components/plaato/translations/es.json @@ -10,7 +10,7 @@ "default": "\u00a1El dispositivo Plaato {device_type} con nombre **{device_name}** se ha configurado correctamente!" }, "error": { - "invalid_webhook_device": "Has seleccionado un dispositivo que no admite el env\u00edo de datos a un webhook. Solo est\u00e1 disponible para Airlock", + "invalid_webhook_device": "Has seleccionado un dispositivo que no admite el env\u00edo de datos a un webhook. Solo est\u00e1 disponible para el Airlock", "no_api_method": "Necesitas a\u00f1adir un token de autenticaci\u00f3n o seleccionar un webhook", "no_auth_token": "Es necesario a\u00f1adir un token de autenticaci\u00f3n" }, diff --git a/homeassistant/components/plant/translations/es.json b/homeassistant/components/plant/translations/es.json index 957a07a1d51..da9470e5ea0 100644 --- a/homeassistant/components/plant/translations/es.json +++ b/homeassistant/components/plant/translations/es.json @@ -5,5 +5,5 @@ "problem": "Problema" } }, - "title": "Planta" + "title": "Monitor de planta" } \ No newline at end of file diff --git a/homeassistant/components/plex/translations/es.json b/homeassistant/components/plex/translations/es.json index 8071e9e11fe..0b030c2a958 100644 --- a/homeassistant/components/plex/translations/es.json +++ b/homeassistant/components/plex/translations/es.json @@ -22,7 +22,7 @@ "host": "Host", "port": "Puerto", "ssl": "Utiliza un certificado SSL", - "token": "Token (Opcional)", + "token": "Token (opcional)", "verify_ssl": "Verificar el certificado SSL" }, "title": "Configuraci\u00f3n Manual de Plex" @@ -35,7 +35,7 @@ "title": "Seleccione el servidor Plex" }, "user": { - "description": "Continuar hacia [plex.tv](https://plex.tv) para vincular un servidor Plex." + "description": "Contin\u00faa hacia [plex.tv](https://plex.tv) para vincular un servidor Plex." }, "user_advanced": { "data": { diff --git a/homeassistant/components/plugwise/translations/es.json b/homeassistant/components/plugwise/translations/es.json index 1a0ae166aaa..570184aaabf 100644 --- a/homeassistant/components/plugwise/translations/es.json +++ b/homeassistant/components/plugwise/translations/es.json @@ -20,8 +20,8 @@ "port": "Puerto", "username": "Nombre de usuario de Smile" }, - "description": "Producto:", - "title": "Conectarse a Smile" + "description": "Por favor, introduce", + "title": "Conectar a Smile" }, "user_gateway": { "data": { @@ -30,7 +30,7 @@ "port": "Puerto", "username": "Nombre de usuario Smile" }, - "description": "Por favor introduce:", + "description": "Por favor, introduce:", "title": "Conectarse a Smile" } } diff --git a/homeassistant/components/point/translations/es.json b/homeassistant/components/point/translations/es.json index 3d1f47c2ceb..2e1de2f1f6f 100644 --- a/homeassistant/components/point/translations/es.json +++ b/homeassistant/components/point/translations/es.json @@ -11,12 +11,12 @@ "default": "Autenticado correctamente" }, "error": { - "follow_link": "Accede al enlace e identif\u00edcate antes de pulsar Enviar.", + "follow_link": "Por favor, sigue el enlace y autent\u00edcate antes de presionar Enviar", "no_token": "Token de acceso no v\u00e1lido" }, "step": { "auth": { - "description": "Accede al siguiente enlace y Acepta el acceso a tu cuenta Minut, despu\u00e9s vuelve y pulsa en Enviar a continuaci\u00f3n.\n\n[Link]({authorization_url})", + "description": "Por favor, sigue el enlace a continuaci\u00f3n y **Acepta** el acceso a tu cuenta Minut, luego regresa y presiona **Enviar** a continuaci\u00f3n. \n\n[Enlace]({authorization_url})", "title": "Autenticaci\u00f3n con Point" }, "user": { diff --git a/homeassistant/components/profiler/translations/es.json b/homeassistant/components/profiler/translations/es.json index a39a0a24d2a..d5552505c73 100644 --- a/homeassistant/components/profiler/translations/es.json +++ b/homeassistant/components/profiler/translations/es.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "\u00bfEst\u00e1s seguro de que quieres configurar el Profiler?" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" } } } diff --git a/homeassistant/components/ps4/translations/es.json b/homeassistant/components/ps4/translations/es.json index 3655b72110c..fd1283ccfd8 100644 --- a/homeassistant/components/ps4/translations/es.json +++ b/homeassistant/components/ps4/translations/es.json @@ -3,19 +3,19 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "credential_error": "Error al obtener las credenciales.", - "no_devices_found": "No se encuentran dispositivos PlayStation 4 en la red.", - "port_987_bind_error": "No se ha podido unir al puerto 987. Consulta la [documentaci\u00f3n](https://www.home-assistant.io/components/ps4/) para m\u00e1s informaci\u00f3n.", - "port_997_bind_error": "No se ha podido unir al puerto 997. Consulta la [documentaci\u00f3n](https://www.home-assistant.io/components/ps4/) para m\u00e1s informaci\u00f3n." + "no_devices_found": "No se encontraron dispositivos en la red", + "port_987_bind_error": "No se pudo vincular al puerto 987. Consulta la [documentaci\u00f3n](https://www.home-assistant.io/components/ps4/) para obtener informaci\u00f3n adicional.", + "port_997_bind_error": "No se pudo vincular al puerto 997. Consulta la [documentaci\u00f3n](https://www.home-assistant.io/components/ps4/) para obtener informaci\u00f3n adicional." }, "error": { "cannot_connect": "No se pudo conectar", - "credential_timeout": "Se agot\u00f3 el tiempo para el servicio de credenciales. Pulsa enviar para reiniciar.", - "login_failed": "No se ha podido vincular con PlayStation 4. Verifica que el PIN sea correcto.", - "no_ipaddress": "Introduce la direcci\u00f3n IP de la PlayStation 4 que quieres configurar." + "credential_timeout": "Se agot\u00f3 el tiempo de espera del servicio de credenciales. Presiona enviar para reiniciar.", + "login_failed": "No se pudo emparejar con PlayStation 4. Verifica que el C\u00f3digo PIN sea correcto.", + "no_ipaddress": "Introduce la Direcci\u00f3n IP de la PlayStation 4 que deseas configurar." }, "step": { "creds": { - "description": "Credenciales necesarias. Pulsa 'Enviar' y, a continuaci\u00f3n, en la app de segunda pantalla de PS4, actualiza la lista de dispositivos y selecciona el dispositivo 'Home-Assistant' para continuar." + "description": "Credenciales necesarias. Presiona 'Enviar' y luego en la aplicaci\u00f3n PS4 Second Screen, actualiza los dispositivos y selecciona el dispositivo 'Home-Assistant' para continuar." }, "link": { "data": { @@ -25,12 +25,12 @@ "region": "Regi\u00f3n" }, "data_description": { - "code": "Ve a 'Configuraci\u00f3n' en tu consola PlayStation 4. Luego navega hasta 'Configuraci\u00f3n de conexi\u00f3n de la aplicaci\u00f3n m\u00f3vil' y selecciona 'Agregar dispositivo' para obtener el PIN." + "code": "Navega a 'Configuraci\u00f3n' en tu consola PlayStation 4. Luego navega hasta 'Configuraci\u00f3n de conexi\u00f3n de la aplicaci\u00f3n m\u00f3vil' y selecciona 'Agregar dispositivo' para obtener el PIN." } }, "mode": { "data": { - "ip_address": "Direcci\u00f3n IP (d\u00e9jalo en blanco si usas la detecci\u00f3n autom\u00e1tica).", + "ip_address": "Direcci\u00f3n IP (D\u00e9jalo en blanco si usas la detecci\u00f3n autom\u00e1tica).", "mode": "Modo configuraci\u00f3n" }, "data_description": { diff --git a/homeassistant/components/rachio/translations/es.json b/homeassistant/components/rachio/translations/es.json index f3821b7aa5c..671a00a334c 100644 --- a/homeassistant/components/rachio/translations/es.json +++ b/homeassistant/components/rachio/translations/es.json @@ -13,7 +13,7 @@ "data": { "api_key": "Clave API" }, - "description": "Necesitar\u00e1s la clave API de https://app.rach.io/. Selecciona 'Account Settings' y luego haz clic en 'GET API KEY'.", + "description": "Necesitar\u00e1s la clave API de https://app.rach.io/. Ve a Configuraci\u00f3n, luego haz clic en 'GET API KEY'.", "title": "Conectar a tu dispositivo Rachio" } } diff --git a/homeassistant/components/renault/translations/es.json b/homeassistant/components/renault/translations/es.json index 1d8b5f447e3..75a0ca557c5 100644 --- a/homeassistant/components/renault/translations/es.json +++ b/homeassistant/components/renault/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "kamereon_no_account": "No se pudo encontrar la cuenta de Kamereon.", + "kamereon_no_account": "No se puede encontrar la cuenta de Kamereon", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/rfxtrx/translations/es.json b/homeassistant/components/rfxtrx/translations/es.json index 997a6b0a0bf..aa567d0093d 100644 --- a/homeassistant/components/rfxtrx/translations/es.json +++ b/homeassistant/components/rfxtrx/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_configured": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "cannot_connect": "No se pudo conectar" }, "error": { diff --git a/homeassistant/components/risco/translations/es.json b/homeassistant/components/risco/translations/es.json index 6f85fe0c4ab..8d0c0460a3c 100644 --- a/homeassistant/components/risco/translations/es.json +++ b/homeassistant/components/risco/translations/es.json @@ -32,7 +32,7 @@ }, "init": { "data": { - "code_arm_required": "Requiere un c\u00f3digo PIN para armar", + "code_arm_required": "Requiere un C\u00f3digo PIN para armar", "code_disarm_required": "Requiere un c\u00f3digo PIN para desactivar", "scan_interval": "Con qu\u00e9 frecuencia sondear Risco (en segundos)" }, diff --git a/homeassistant/components/roomba/translations/es.json b/homeassistant/components/roomba/translations/es.json index f2664baee31..cc2954335b6 100644 --- a/homeassistant/components/roomba/translations/es.json +++ b/homeassistant/components/roomba/translations/es.json @@ -9,17 +9,17 @@ "error": { "cannot_connect": "No se pudo conectar" }, - "flow_title": "iRobot {name} ({host})", + "flow_title": "{name} ({host})", "step": { "link": { - "description": "Mant\u00e9n pulsado el bot\u00f3n Inicio en {name} hasta que el dispositivo genere un sonido (aproximadamente dos segundos).", - "title": "Recuperar la contrase\u00f1a" + "description": "Mant\u00e9n presionado el bot\u00f3n Inicio en {name} hasta que el dispositivo genere un sonido (alrededor de dos segundos), luego haz clic en enviar en los siguientes 30 segundos.", + "title": "Recuperar Contrase\u00f1a" }, "link_manual": { "data": { "password": "Contrase\u00f1a" }, - "description": "No se pudo recuperar la contrase\u00f1a desde el dispositivo de forma autom\u00e1tica. Por favor, sigue los pasos descritos en la documentaci\u00f3n en: {auth_help_url}", + "description": "La contrase\u00f1a no se pudo recuperar del dispositivo autom\u00e1ticamente. Sigue los pasos descritos en la documentaci\u00f3n en: {auth_help_url}", "title": "Escribe la contrase\u00f1a" }, "manual": { @@ -33,7 +33,7 @@ "data": { "host": "Host" }, - "description": "Actualmente recuperar el BLID y la contrase\u00f1a es un proceso manual. Sigue los pasos descritos en la documentaci\u00f3n en: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "description": "Seleccione una Roomba o Braava.", "title": "Conexi\u00f3n autom\u00e1tica con el dispositivo" } } diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json index 70bd9f30b9b..9a5c8c5fa51 100644 --- a/homeassistant/components/samsungtv/translations/es.json +++ b/homeassistant/components/samsungtv/translations/es.json @@ -6,7 +6,7 @@ "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este TV Samsung. Verifica la configuraci\u00f3n del Administrador de dispositivos externos de tu TV para autorizar a Home Assistant.", "cannot_connect": "No se pudo conectar", "id_missing": "Este dispositivo Samsung no tiene un n\u00famero de serie.", - "not_supported": "Esta televisi\u00f3n Samsung actualmente no es compatible.", + "not_supported": "Este dispositivo Samsung no es actualmente compatible.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/schedule/translations/el.json b/homeassistant/components/schedule/translations/el.json new file mode 100644 index 00000000000..2c6a17ab298 --- /dev/null +++ b/homeassistant/components/schedule/translations/el.json @@ -0,0 +1,3 @@ +{ + "title": "\u03a0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/fr.json b/homeassistant/components/schedule/translations/fr.json new file mode 100644 index 00000000000..e7327a3fffa --- /dev/null +++ b/homeassistant/components/schedule/translations/fr.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "D\u00e9sactiv\u00e9", + "on": "Activ\u00e9" + } + }, + "title": "Planification" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/hu.json b/homeassistant/components/schedule/translations/hu.json new file mode 100644 index 00000000000..44b70c0497b --- /dev/null +++ b/homeassistant/components/schedule/translations/hu.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Ki", + "on": "Be" + } + }, + "title": "Id\u0151z\u00edt\u00e9s" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/pt-BR.json b/homeassistant/components/schedule/translations/pt-BR.json new file mode 100644 index 00000000000..f6aa495fc69 --- /dev/null +++ b/homeassistant/components/schedule/translations/pt-BR.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Desligado", + "on": "Ligado" + } + }, + "title": "Hor\u00e1rio" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/zh-Hant.json b/homeassistant/components/schedule/translations/zh-Hant.json new file mode 100644 index 00000000000..84612eacc84 --- /dev/null +++ b/homeassistant/components/schedule/translations/zh-Hant.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u95dc\u9589", + "on": "\u958b\u555f" + } + }, + "title": "\u6392\u7a0b" +} \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/es.json b/homeassistant/components/sentry/translations/es.json index 0bb2f70720c..058cc27642f 100644 --- a/homeassistant/components/sentry/translations/es.json +++ b/homeassistant/components/sentry/translations/es.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "dsn": "DSN" + "dsn": "Sentry DSN" } } } diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index 85394365c1a..9d344f83992 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/translations/es.json @@ -10,7 +10,7 @@ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, - "flow_title": "Shelly: {name}", + "flow_title": "{name}", "step": { "confirm_discovery": { "description": "\u00bfQuieres configurar {model} en {host}? \n\nLos dispositivos alimentados por bater\u00eda que est\u00e1n protegidos con contrase\u00f1a deben despertarse antes de continuar con la configuraci\u00f3n.\nLos dispositivos que funcionan con bater\u00eda que no est\u00e1n protegidos con contrase\u00f1a se agregar\u00e1n cuando el dispositivo se despierte, puedes activar manualmente el dispositivo ahora con un bot\u00f3n o esperar la pr\u00f3xima actualizaci\u00f3n de datos del dispositivo." @@ -25,7 +25,7 @@ "data": { "host": "Host" }, - "description": "Antes de configurarlo, el dispositivo que funciona con bater\u00eda debe despertarse presionando el bot\u00f3n del dispositivo." + "description": "Antes de configurar, los dispositivos alimentados por bater\u00eda deben despertarse, puede despertar el dispositivo ahora con un bot\u00f3n." } } }, @@ -42,7 +42,7 @@ "btn_up": "Bot\u00f3n {subtype} soltado", "double": "Pulsaci\u00f3n doble de {subtype}", "double_push": "Pulsaci\u00f3n doble de {subtype}", - "long": "Pulsaci\u00f3n larga de {subtype}", + "long": "{subtype} con pulsaci\u00f3n larga", "long_push": "Pulsaci\u00f3n larga de {subtype}", "long_single": "Pulsaci\u00f3n larga de {subtype} seguida de una pulsaci\u00f3n simple", "single": "Pulsaci\u00f3n simple de {subtype}", diff --git a/homeassistant/components/simplisafe/translations/es.json b/homeassistant/components/simplisafe/translations/es.json index b47b3870d46..1558f96fa4f 100644 --- a/homeassistant/components/simplisafe/translations/es.json +++ b/homeassistant/components/simplisafe/translations/es.json @@ -33,7 +33,7 @@ "data": { "auth_code": "C\u00f3digo de Autorizaci\u00f3n", "password": "Contrase\u00f1a", - "username": "Correo electr\u00f3nico" + "username": "Nombre de usuario" }, "description": "SimpliSafe autentica a los usuarios a trav\u00e9s de su aplicaci\u00f3n web. Debido a limitaciones t\u00e9cnicas, existe un paso manual al final de este proceso; aseg\u00farate de leer la [documentaci\u00f3n](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de comenzar. \n\nCuando est\u00e9s listo, haz clic [aqu\u00ed]({url}) para abrir la aplicaci\u00f3n web SimpliSafe e introduce tus credenciales. Si ya iniciaste sesi\u00f3n en SimpliSafe en tu navegador, es posible que desees abrir una nueva pesta\u00f1a y luego copiar/pegar la URL anterior en esa pesta\u00f1a. \n\nCuando se complete el proceso, regresa aqu\u00ed e introduce el c\u00f3digo de autorizaci\u00f3n de la URL `com.simplisafe.mobile`." } diff --git a/homeassistant/components/smappee/translations/es.json b/homeassistant/components/smappee/translations/es.json index 808eeffbbcd..1a9e2ee28dd 100644 --- a/homeassistant/components/smappee/translations/es.json +++ b/homeassistant/components/smappee/translations/es.json @@ -9,7 +9,7 @@ "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" }, - "flow_title": "Smappee: {name}", + "flow_title": "{name}", "step": { "environment": { "data": { diff --git a/homeassistant/components/smartthings/translations/es.json b/homeassistant/components/smartthings/translations/es.json index f354e3914e0..8aee6337cef 100644 --- a/homeassistant/components/smartthings/translations/es.json +++ b/homeassistant/components/smartthings/translations/es.json @@ -9,7 +9,7 @@ "token_forbidden": "El token no tiene los \u00e1mbitos de OAuth necesarios.", "token_invalid_format": "El token debe estar en formato UID/GUID", "token_unauthorized": "El token no es v\u00e1lido o ya no est\u00e1 autorizado.", - "webhook_error": "SmartThings no ha podido validar el endpoint configurado en 'base_url'. Por favor, revisa los requisitos del componente." + "webhook_error": "SmartThings no pudo validar la URL del webhook. Aseg\u00farate de que la URL del webhook sea accesible desde Internet y vuelve a intentarlo." }, "step": { "authorize": { @@ -30,7 +30,7 @@ "title": "Seleccionar Ubicaci\u00f3n" }, "user": { - "description": "Los SmartThings se configurar\u00e1n para enviar las actualizaciones a Home Assistant en:\n> {webhook_url}\n\nSi esto no es correcto, por favor, actualiza tu configuraci\u00f3n, reinicia Home Assistant e int\u00e9ntalo de nuevo.", + "description": "SmartThings se configurar\u00e1 para enviar actualizaciones a Home Assistant en:\n> {webhook_url}\n\nSi esto no es correcto, por favor, actualiza tu configuraci\u00f3n, reinicia Home Assistant e int\u00e9ntalo de nuevo.", "title": "Confirmar URL de devoluci\u00f3n de llamada" } } diff --git a/homeassistant/components/soma/translations/es.json b/homeassistant/components/soma/translations/es.json index 0ae2d26adec..0e2312bd335 100644 --- a/homeassistant/components/soma/translations/es.json +++ b/homeassistant/components/soma/translations/es.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_setup": "S\u00f3lo puede configurar una cuenta de Soma.", + "already_setup": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "connection_error": "No se ha podido conectar a SOMA Connect.", + "connection_error": "No se pudo conectar", "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, leer la documentaci\u00f3n.", "result_error": "SOMA Connect respondi\u00f3 con un error." }, diff --git a/homeassistant/components/somfy_mylink/translations/es.json b/homeassistant/components/somfy_mylink/translations/es.json index 8647f7bb8fd..4e59c634e67 100644 --- a/homeassistant/components/somfy_mylink/translations/es.json +++ b/homeassistant/components/somfy_mylink/translations/es.json @@ -8,7 +8,7 @@ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, - "flow_title": "Somfy MyLink {mac} ({ip})", + "flow_title": "{mac} ({ip})", "step": { "user": { "data": { @@ -16,7 +16,7 @@ "port": "Puerto", "system_id": "ID del sistema" }, - "description": "El ID del sistema se puede obtener en la aplicaci\u00f3n MyLink en Integraci\u00f3n seleccionando cualquier servicio que no sea de la nube." + "description": "El ID del sistema se puede obtener en la aplicaci\u00f3n MyLink en Integraci\u00f3n seleccionando cualquier servicio que no sea en la nube." } } }, diff --git a/homeassistant/components/sonarr/translations/es.json b/homeassistant/components/sonarr/translations/es.json index 440ab7bf21f..3d8fc33de59 100644 --- a/homeassistant/components/sonarr/translations/es.json +++ b/homeassistant/components/sonarr/translations/es.json @@ -9,10 +9,10 @@ "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, - "flow_title": "Sonarr: {name}", + "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "La integraci\u00f3n de Sonarr necesita volver a autenticarse manualmente con la API de Sonarr alojada en: {host}", + "description": "La integraci\u00f3n de Sonarr debe volver a autenticarse manualmente con la API de Sonarr alojada en: {url}", "title": "Reautenticaci\u00f3n de la integraci\u00f3n" }, "user": { diff --git a/homeassistant/components/songpal/translations/es.json b/homeassistant/components/songpal/translations/es.json index fa62eb3ba1e..153d107eacd 100644 --- a/homeassistant/components/songpal/translations/es.json +++ b/homeassistant/components/songpal/translations/es.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "Dispositivo ya configurado", + "already_configured": "El dispositivo ya est\u00e1 configurado", "not_songpal_device": "No es un dispositivo Songpal" }, "error": { "cannot_connect": "No se pudo conectar" }, - "flow_title": "Sony Songpal {name} ({host})", + "flow_title": "{name} ({host})", "step": { "init": { "description": "\u00bfQuieres configurar {name} ({host})?" diff --git a/homeassistant/components/speedtestdotnet/translations/es.json b/homeassistant/components/speedtestdotnet/translations/es.json index 42f263971ba..90fe06ccf55 100644 --- a/homeassistant/components/speedtestdotnet/translations/es.json +++ b/homeassistant/components/speedtestdotnet/translations/es.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "\u00bfEst\u00e1s seguro de que quieres configurar SpeedTest?" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" } } }, diff --git a/homeassistant/components/spotify/translations/es.json b/homeassistant/components/spotify/translations/es.json index 09de377d195..377c2a9df58 100644 --- a/homeassistant/components/spotify/translations/es.json +++ b/homeassistant/components/spotify/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", "missing_configuration": "La integraci\u00f3n de Spotify no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "reauth_account_mismatch": "La cuenta de Spotify con la que est\u00e1s autenticado, no coincide con la cuenta necesaria para re-autenticaci\u00f3n." @@ -15,7 +15,7 @@ }, "reauth_confirm": { "description": "La integraci\u00f3n de Spotify necesita volver a autenticarse con Spotify para la cuenta: {account}", - "title": "Volver a autenticar con Spotify" + "title": "Volver a autenticar la integraci\u00f3n" } } }, diff --git a/homeassistant/components/sql/translations/es.json b/homeassistant/components/sql/translations/es.json index 322485ae446..427510a3f1d 100644 --- a/homeassistant/components/sql/translations/es.json +++ b/homeassistant/components/sql/translations/es.json @@ -12,7 +12,7 @@ "data": { "column": "Columna", "db_url": "URL de la base de datos", - "name": "[%key:component::sql::config::step::user::data::name%]", + "name": "Nombre", "query": "Selecciona la consulta", "unit_of_measurement": "Unidad de medida", "value_template": "Plantilla de valor" @@ -38,7 +38,7 @@ "data": { "column": "Columna", "db_url": "URL de la base de datos", - "name": "[%key:component::sql::config::step::user::data::name%]", + "name": "Nombre", "query": "Selecciona la consulta", "unit_of_measurement": "Unidad de medida", "value_template": "Plantilla de valor" diff --git a/homeassistant/components/starline/translations/es.json b/homeassistant/components/starline/translations/es.json index eff1da9773a..75ded3aaef2 100644 --- a/homeassistant/components/starline/translations/es.json +++ b/homeassistant/components/starline/translations/es.json @@ -11,7 +11,7 @@ "app_id": "ID de la aplicaci\u00f3n", "app_secret": "Secreto" }, - "description": "ID de la aplicaci\u00f3n y c\u00f3digo secreto de la cuenta de desarrollador de StarLine", + "description": "ID de aplicaci\u00f3n y c\u00f3digo secreto de [cuenta de desarrollador StarLine](https://my.starline.ru/developer)", "title": "Credenciales de la aplicaci\u00f3n" }, "auth_captcha": { diff --git a/homeassistant/components/switchbot/translations/el.json b/homeassistant/components/switchbot/translations/el.json index 585dd3ccbd4..d38179c0702 100644 --- a/homeassistant/components/switchbot/translations/el.json +++ b/homeassistant/components/switchbot/translations/el.json @@ -9,6 +9,12 @@ }, "flow_title": "{name}", "step": { + "confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "password": { + "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae {name} \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, "user": { "data": { "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", diff --git a/homeassistant/components/synology_dsm/translations/es.json b/homeassistant/components/synology_dsm/translations/es.json index 41d3d369115..af0d17d7d39 100644 --- a/homeassistant/components/synology_dsm/translations/es.json +++ b/homeassistant/components/synology_dsm/translations/es.json @@ -24,7 +24,7 @@ "data": { "password": "Contrase\u00f1a", "port": "Puerto", - "ssl": "Usar SSL/TLS para conectar con tu NAS", + "ssl": "Utiliza un certificado SSL", "username": "Nombre de usuario", "verify_ssl": "Verificar el certificado SSL" }, @@ -42,8 +42,8 @@ "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", - "ssl": "Usar SSL/TLS para conectar con tu NAS", - "username": "Usuario", + "ssl": "Utiliza un certificado SSL", + "username": "Nombre de usuario", "verify_ssl": "Verificar el certificado SSL" } } diff --git a/homeassistant/components/tado/translations/es.json b/homeassistant/components/tado/translations/es.json index 58745783774..42090cccb15 100644 --- a/homeassistant/components/tado/translations/es.json +++ b/homeassistant/components/tado/translations/es.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "no_homes": "No hay casas asociadas a esta cuenta de Tado", + "no_homes": "No hay casas vinculadas a esta cuenta Tado", "unknown": "Error inesperado" }, "step": { @@ -25,7 +25,7 @@ "data": { "fallback": "Elige el modo alternativo." }, - "description": "El modo de salvaguarda volver\u00e1 a la Planificaci\u00f3n Inteligente en el siguiente cambio de programaci\u00f3n despu\u00e9s de ajustar manualmente una zona.", + "description": "El modo de respaldo te permite elegir cu\u00e1ndo retroceder a Smart Schedule desde tu superposici\u00f3n de zona manual. (NEXT_TIME_BLOCK:= Cambiar en el pr\u00f3ximo cambio de Smart Schedule; MANUAL:= No cambiar hasta que canceles; TADO_DEFAULT:= Cambiar seg\u00fan tu configuraci\u00f3n en la aplicaci\u00f3n Tado).", "title": "Ajustar las opciones de Tado" } } diff --git a/homeassistant/components/tellduslive/translations/es.json b/homeassistant/components/tellduslive/translations/es.json index 5971c89f43e..fbda5550ac3 100644 --- a/homeassistant/components/tellduslive/translations/es.json +++ b/homeassistant/components/tellduslive/translations/es.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n", - "unknown": "Se produjo un error desconocido", + "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", + "unknown": "Error inesperado", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, "error": { @@ -11,14 +11,13 @@ }, "step": { "auth": { - "description": "Para vincular tu cuenta de Telldus Live:\n 1. Pulsa el siguiente enlace\n 2. Inicia sesi\u00f3n en Telldus Live\n 3. Autoriza **{app_name}** (pulsa en **Yes**).\n 4. Vuelve atr\u00e1s y pulsa **ENVIAR**.\n\n [Link TelldusLive account]({auth_url})", + "description": "Para vincular tu cuenta de Telldus Live:\n1. Pulsa el siguiente enlace\n2. Inicia sesi\u00f3n en Telldus Live\n3. Autoriza **{app_name}** (pulsa en **Yes**).\n4. Vuelve atr\u00e1s y pulsa **ENVIAR**.\n\n[Enlace a la cuenta TelldusLive]({auth_url})", "title": "Autenticaci\u00f3n contra TelldusLive" }, "user": { "data": { "host": "Host" }, - "description": "Vac\u00edo", "title": "Elige el punto final." } } diff --git a/homeassistant/components/tibber/translations/es.json b/homeassistant/components/tibber/translations/es.json index 0840339ac24..53c0c2aaab7 100644 --- a/homeassistant/components/tibber/translations/es.json +++ b/homeassistant/components/tibber/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Una cuenta de Tibber ya est\u00e1 configurada." + "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/tradfri/translations/es.json b/homeassistant/components/tradfri/translations/es.json index 500b357319c..6ed8b96a986 100644 --- a/homeassistant/components/tradfri/translations/es.json +++ b/homeassistant/components/tradfri/translations/es.json @@ -6,8 +6,8 @@ }, "error": { "cannot_authenticate": "No se puede autenticar, \u00bfest\u00e1 la puerta de enlace emparejada con otro servidor como, por ejemplo, Homekit?", - "cannot_connect": "No se puede conectar a la puerta de enlace.", - "invalid_key": "No se ha podido registrar con la clave proporcionada. Si esto sigue ocurriendo, intenta reiniciar el gateway.", + "cannot_connect": "No se pudo conectar", + "invalid_key": "No se pudo registrar con la clave proporcionada. Si esto sigue ocurriendo, intenta reiniciar la puerta de enlace.", "timeout": "Tiempo de espera agotado validando el c\u00f3digo." }, "step": { @@ -16,8 +16,8 @@ "host": "Host", "security_code": "C\u00f3digo de seguridad" }, - "description": "Puedes encontrar el c\u00f3digo de seguridad en la parte posterior de tu gateway.", - "title": "Introduzca el c\u00f3digo de seguridad" + "description": "Puedes encontrar el c\u00f3digo de seguridad en la parte posterior de tu puerta de enlace.", + "title": "Introduce el c\u00f3digo de seguridad" } } } diff --git a/homeassistant/components/transmission/translations/es.json b/homeassistant/components/transmission/translations/es.json index 6c293a59d4c..7ae9bfa87fa 100644 --- a/homeassistant/components/transmission/translations/es.json +++ b/homeassistant/components/transmission/translations/es.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado.", + "already_configured": "El dispositivo ya est\u00e1 configurado", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "name_exists": "El nombre ya existe" + "name_exists": "Nombre ya existente" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/tuya/translations/es.json b/homeassistant/components/tuya/translations/es.json index ca46860a208..55ab464c3c2 100644 --- a/homeassistant/components/tuya/translations/es.json +++ b/homeassistant/components/tuya/translations/es.json @@ -9,9 +9,9 @@ "data": { "access_id": "ID de acceso a Tuya IoT", "access_secret": "Secreto de acceso a Tuya IoT", - "country_code": "C\u00f3digo de pais de tu cuenta (por ejemplo, 1 para USA o 86 para China)", + "country_code": "Pa\u00eds", "password": "Contrase\u00f1a", - "username": "Nombre de usuario" + "username": "Cuenta" }, "description": "Introduce tus credencial de Tuya" } diff --git a/homeassistant/components/twilio/translations/es.json b/homeassistant/components/twilio/translations/es.json index 41057c5f4a0..3743b0ee163 100644 --- a/homeassistant/components/twilio/translations/es.json +++ b/homeassistant/components/twilio/translations/es.json @@ -6,11 +6,11 @@ "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, "create_entry": { - "default": "Para enviar eventos a Home Assistant debes configurar los [Webhooks en Twilio]({twilio_url}). \n\n Completa la siguiente informaci\u00f3n: \n\n - URL: `{webhook_url}` \n - M\u00e9todo: POST \n - Tipo de contenido: application/x-www-form-urlencoded \n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." + "default": "Para enviar eventos a Home Assistant debes configurar los [Webhooks en Twilio]({twilio_url}). \n\n Completa la siguiente informaci\u00f3n: \n\n- URL: `{webhook_url}` \n- M\u00e9todo: POST \n- Tipo de contenido: application/x-www-form-urlencoded \n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." }, "step": { "user": { - "description": "\u00bfQuieres empezar la configuraci\u00f3n?", + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?", "title": "Configurar el Webhook de Twilio" } } diff --git a/homeassistant/components/unifi/translations/es.json b/homeassistant/components/unifi/translations/es.json index c6d17b369b4..ff9b9575b78 100644 --- a/homeassistant/components/unifi/translations/es.json +++ b/homeassistant/components/unifi/translations/es.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_configured": "El sitio del controlador ya est\u00e1 configurado", - "configuration_updated": "Configuraci\u00f3n actualizada.", + "already_configured": "El sitio UniFi Network ya est\u00e1 configurado", + "configuration_updated": "Configuraci\u00f3n actualizada", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "faulty_credentials": "Autenticaci\u00f3n no v\u00e1lida", - "service_unavailable": "Error al conectar", + "service_unavailable": "No se pudo conectar", "unknown_client_mac": "Ning\u00fan cliente disponible en esa direcci\u00f3n MAC" }, - "flow_title": "Red UniFi {site} ({host})", + "flow_title": "{site} ({host})", "step": { "user": { "data": { @@ -19,9 +19,9 @@ "port": "Puerto", "site": "ID del sitio", "username": "Nombre de usuario", - "verify_ssl": "Controlador usando el certificado adecuado" + "verify_ssl": "Verificar el certificado SSL" }, - "title": "Configuraci\u00f3n de UniFi Network" + "title": "Configurar UniFi Network" } } }, @@ -37,7 +37,7 @@ "poe_clients": "Permitir control PoE de clientes" }, "description": "Configurar controles de cliente\n\nCrea conmutadores para los n\u00fameros de serie para los que deseas controlar el acceso a la red.", - "title": "Opciones UniFi 2/3" + "title": "Opciones de UniFi Network 2/3" }, "device_tracker": { "data": { @@ -49,7 +49,7 @@ "track_wired_clients": "Incluir clientes de red cableada" }, "description": "Configurar dispositivo de seguimiento", - "title": "Opciones UniFi 1/3" + "title": "Opciones de UniFi Network 1/3" }, "simple_options": { "data": { diff --git a/homeassistant/components/upnp/translations/es.json b/homeassistant/components/upnp/translations/es.json index 9a8a9e0160b..60e9ebaadf9 100644 --- a/homeassistant/components/upnp/translations/es.json +++ b/homeassistant/components/upnp/translations/es.json @@ -1,14 +1,10 @@ { "config": { "abort": { - "already_configured": "UPnP / IGD ya est\u00e1 configurado", + "already_configured": "El dispositivo ya est\u00e1 configurado", "incomplete_discovery": "Descubrimiento incompleto", "no_devices_found": "No se encontraron dispositivos en la red" }, - "error": { - "one": "UNO", - "other": "OTRO" - }, "flow_title": "{name}", "step": { "ssdp_confirm": { diff --git a/homeassistant/components/vera/translations/es.json b/homeassistant/components/vera/translations/es.json index 8299b2ea6c9..8d9686b0a13 100644 --- a/homeassistant/components/vera/translations/es.json +++ b/homeassistant/components/vera/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "cannot_connect": "No se pudo conectar con el controlador con url {base_url}" + "cannot_connect": "No se pudo conectar al controlador con URL {base_url}" }, "step": { "user": { diff --git a/homeassistant/components/vizio/translations/es.json b/homeassistant/components/vizio/translations/es.json index d3ba768e933..6ac88586cad 100644 --- a/homeassistant/components/vizio/translations/es.json +++ b/homeassistant/components/vizio/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured_device": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "updated_entry": "Esta entrada ya ha sido configurada pero el nombre y/o las opciones definidas en la configuraci\u00f3n no coinciden con la configuraci\u00f3n previamente importada, por lo que la entrada de la configuraci\u00f3n ha sido actualizada en consecuencia." + "updated_entry": "Esta entrada ya se configur\u00f3, pero el nombre, las aplicaciones y/o las opciones definidas en la configuraci\u00f3n no coinciden con la configuraci\u00f3n importada anteriormente, por lo que la entrada de configuraci\u00f3n se actualiz\u00f3 en consecuencia." }, "error": { "cannot_connect": "No se pudo conectar", @@ -19,11 +19,11 @@ "title": "Completar Proceso de Emparejamiento" }, "pairing_complete": { - "description": "El dispositivo VIZIO SmartCast ahora est\u00e1 conectado a Home Assistant.", + "description": "Tu VIZIO SmartCast Device ahora est\u00e1 conectado a Home Assistant.", "title": "Emparejamiento Completado" }, "pairing_complete_import": { - "description": "El dispositivo VIZIO SmartCast TV ahora est\u00e1 conectado a Home Assistant.\n\nEl token de acceso es '**{access_token}**'.", + "description": "Tu VIZIO SmartCast Device ahora est\u00e1 conectado a Home Assistant. \n\nTu Token de acceso es '**{access_token}**'.", "title": "Emparejamiento Completado" }, "user": { @@ -33,7 +33,7 @@ "host": "Host", "name": "Nombre" }, - "description": "El token de acceso solo se necesita para las televisiones. Si est\u00e1s configurando una televisi\u00f3n y a\u00fan no tienes un token de acceso, d\u00e9jalo en blanco para iniciar el proceso de sincronizaci\u00f3n.", + "description": "Solo se necesita un Token de acceso para TVs. Si est\u00e1s configurando una TV y a\u00fan no tienes un Token de acceso, d\u00e9jalo en blanco para pasar por un proceso de emparejamiento.", "title": "VIZIO SmartCast Device" } } @@ -47,7 +47,7 @@ "volume_step": "Tama\u00f1o del paso de volumen" }, "description": "Si tienes un Smart TV, opcionalmente puedes filtrar su lista de fuentes eligiendo qu\u00e9 aplicaciones incluir o excluir en su lista de fuentes.", - "title": "Actualizar las opciones de SmartCast de Vizo" + "title": "Actualizar las opciones de VIZIO SmartCast Device" } } } diff --git a/homeassistant/components/withings/translations/es.json b/homeassistant/components/withings/translations/es.json index 1303ca27690..d90f1ccacc3 100644 --- a/homeassistant/components/withings/translations/es.json +++ b/homeassistant/components/withings/translations/es.json @@ -21,12 +21,12 @@ "data": { "profile": "Nombre de perfil" }, - "description": "\u00bfQu\u00e9 perfil seleccion\u00f3 en el sitio web de Withings? Es importante que los perfiles coincidan, de lo contrario los datos se etiquetar\u00e1n incorrectamente.", + "description": "Proporciona un nombre de perfil \u00fanico para estos datos. Por lo general, este es el nombre del perfil que seleccionaste en el paso anterior.", "title": "Perfil de usuario." }, "reauth": { "description": "El perfil \"{profile}\" debe volver a autenticarse para continuar recibiendo datos de Withings.", - "title": "Re-autentificar el perfil" + "title": "Volver a autenticar la integraci\u00f3n" }, "reauth_confirm": { "description": "El perfil \"{profile}\" debe volver a autenticarse para continuar recibiendo datos de Withings.", diff --git a/homeassistant/components/wled/translations/es.json b/homeassistant/components/wled/translations/es.json index 7a428960d62..0547b1598e3 100644 --- a/homeassistant/components/wled/translations/es.json +++ b/homeassistant/components/wled/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Este dispositivo WLED ya est\u00e1 configurado.", + "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", "cct_unsupported": "Este dispositivo WLED utiliza canales CCT, que no son compatibles con esta integraci\u00f3n" }, diff --git a/homeassistant/components/wolflink/translations/es.json b/homeassistant/components/wolflink/translations/es.json index 359a2d0b27e..601026f784a 100644 --- a/homeassistant/components/wolflink/translations/es.json +++ b/homeassistant/components/wolflink/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { @@ -18,7 +18,7 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Nombre de usuario" }, "title": "Conexi\u00f3n WOLF SmartSet" } diff --git a/homeassistant/components/wolflink/translations/sensor.es.json b/homeassistant/components/wolflink/translations/sensor.es.json index 98c6b5bd242..e321ecb72f4 100644 --- a/homeassistant/components/wolflink/translations/sensor.es.json +++ b/homeassistant/components/wolflink/translations/sensor.es.json @@ -28,9 +28,9 @@ "frost_heizkreis": "Escarcha del circuito de calefacci\u00f3n", "frost_warmwasser": "Heladas de DHW", "frostschutz": "Protecci\u00f3n contra las heladas", - "gasdruck": "Presion del gas", + "gasdruck": "Presi\u00f3n del gas", "glt_betrieb": "Modo BMS", - "gradienten_uberwachung": "Monitoreo de gradiente", + "gradienten_uberwachung": "Supervisi\u00f3n de gradiente", "heizbetrieb": "Modo de calefacci\u00f3n", "heizgerat_mit_speicher": "Caldera con cilindro", "heizung": "Calefacci\u00f3n", @@ -59,7 +59,7 @@ "schornsteinfeger": "Prueba de emisiones", "smart_grid": "SmartGrid", "smart_home": "SmartHome", - "softstart": "Arranque suave.", + "softstart": "Arranque suave", "solarbetrieb": "Modo solar", "sparbetrieb": "Modo econ\u00f3mico", "sparen": "Econom\u00eda", diff --git a/homeassistant/components/xiaomi_aqara/translations/es.json b/homeassistant/components/xiaomi_aqara/translations/es.json index d8a36306af7..41d3ffa8207 100644 --- a/homeassistant/components/xiaomi_aqara/translations/es.json +++ b/homeassistant/components/xiaomi_aqara/translations/es.json @@ -12,13 +12,13 @@ "invalid_key": "Clave del gateway inv\u00e1lida", "invalid_mac": "Direcci\u00f3n Mac no v\u00e1lida" }, - "flow_title": "Xiaomi Aqara Gateway: {name}", + "flow_title": "{name}", "step": { "select": { "data": { "select_ip": "Direcci\u00f3n IP" }, - "description": "Ejecuta la configuraci\u00f3n de nuevo si deseas conectar gateways adicionales" + "description": "Selecciona la puerta de enlace Xiaomi Aqara que deseas conectar" }, "settings": { "data": { @@ -34,7 +34,7 @@ "interface": "La interfaz de la red a usar", "mac": "Direcci\u00f3n Mac (opcional)" }, - "description": "Con\u00e9ctate a tu Xiaomi Aqara Gateway, si las direcciones IP y mac se dejan vac\u00edas, se utiliza el auto-descubrimiento." + "description": "Si las direcciones IP y MAC se dejan vac\u00edas, se utiliza la detecci\u00f3n autom\u00e1tica" } } } diff --git a/homeassistant/components/yalexs_ble/translations/el.json b/homeassistant/components/yalexs_ble/translations/el.json new file mode 100644 index 00000000000..2a0243266be --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/el.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "no_unconfigured_devices": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2." + }, + "error": { + "invalid_key_format": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03ae \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac 32 byte.", + "invalid_key_index": "\u0397 \u03c5\u03c0\u03bf\u03b4\u03bf\u03c7\u03ae \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03b1\u03ba\u03ad\u03c1\u03b1\u03b9\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd 0 \u03ba\u03b1\u03b9 255." + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} \u03bc\u03ad\u03c3\u03c9 Bluetooth \u03bc\u03b5 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {address};" + }, + "user": { + "data": { + "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 Bluetooth", + "key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03ae \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac 32 byte)" + }, + "description": "\u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {docs_url} \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/es.json b/homeassistant/components/yeelight/translations/es.json index ef93c17e72c..6ec5b64a3fd 100644 --- a/homeassistant/components/yeelight/translations/es.json +++ b/homeassistant/components/yeelight/translations/es.json @@ -29,7 +29,7 @@ "step": { "init": { "data": { - "model": "Modelo (opcional)", + "model": "Modelo", "nightlight_switch": "Usar interruptor de luz nocturna", "save_on_change": "Guardar estado al cambiar", "transition": "Tiempo de transici\u00f3n (ms)", From 58883feaf69f5204c73812283fc7509ce5cb3c0c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 12 Aug 2022 21:55:48 -1000 Subject: [PATCH 3311/3516] Small cleanups to Yale Access Bluetooth (#76691) - Abort the discovery flow if the user has already started interacting with a user flow or bluetooth discovery - Remove docs_url from the flow - Fix useless return --- .../components/yalexs_ble/config_flow.py | 44 ++++++---- homeassistant/components/yalexs_ble/lock.py | 4 +- .../components/yalexs_ble/strings.json | 3 +- .../yalexs_ble/translations/en.json | 3 +- .../components/yalexs_ble/test_config_flow.py | 80 +++++++++++++++++++ 5 files changed, 113 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/yalexs_ble/config_flow.py b/homeassistant/components/yalexs_ble/config_flow.py index 7f632ebfab0..c7213eefbe9 100644 --- a/homeassistant/components/yalexs_ble/config_flow.py +++ b/homeassistant/components/yalexs_ble/config_flow.py @@ -16,7 +16,7 @@ from yalexs_ble import ( ) from yalexs_ble.const import YALE_MFR_ID -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.bluetooth import ( BluetoothServiceInfoBleak, async_discovered_service_info, @@ -25,7 +25,6 @@ from homeassistant.const import CONF_ADDRESS from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import DiscoveryInfoType -from homeassistant.loader import async_get_integration from .const import CONF_KEY, CONF_LOCAL_NAME, CONF_SLOT, DOMAIN from .util import async_get_service_info, human_readable_name @@ -85,39 +84,52 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): discovery_info["key"], discovery_info["slot"], ) + + address = lock_cfg.address + local_name = lock_cfg.local_name + hass = self.hass + # We do not want to raise on progress as integration_discovery takes # precedence over other discovery flows since we already have the keys. - await self.async_set_unique_id(lock_cfg.address, raise_on_progress=False) + # + # After we do discovery we will abort the flows that do not have the keys + # below unless the user is already setting them up. + await self.async_set_unique_id(address, raise_on_progress=False) new_data = {CONF_KEY: lock_cfg.key, CONF_SLOT: lock_cfg.slot} self._abort_if_unique_id_configured(updates=new_data) for entry in self._async_current_entries(): if entry.data.get(CONF_LOCAL_NAME) == lock_cfg.local_name: - if self.hass.config_entries.async_update_entry( + if hass.config_entries.async_update_entry( entry, data={**entry.data, **new_data} ): - self.hass.async_create_task( - self.hass.config_entries.async_reload(entry.entry_id) + hass.async_create_task( + hass.config_entries.async_reload(entry.entry_id) ) raise AbortFlow(reason="already_configured") try: self._discovery_info = await async_get_service_info( - self.hass, lock_cfg.local_name, lock_cfg.address + hass, local_name, address ) except asyncio.TimeoutError: return self.async_abort(reason="no_devices_found") + # Integration discovery should abort other flows unless they + # are already in the process of being set up since this discovery + # will already have all the keys and the user can simply confirm. for progress in self._async_in_progress(include_uninitialized=True): - # Integration discovery should abort other discovery types - # since it already has the keys and slots, and the other - # discovery types do not. context = progress["context"] if ( - not context.get("active") - and context.get("local_name") == lock_cfg.local_name - or context.get("unique_id") == lock_cfg.address - ): - self.hass.config_entries.flow.async_abort(progress["flow_id"]) + local_name_is_unique(local_name) + and context.get("local_name") == local_name + ) or context.get("unique_id") == address: + if context.get("active"): + # The user has already started interacting with this flow + # and entered the keys. We abort the discovery flow since + # we assume they do not want to use the discovered keys for + # some reason. + raise data_entry_flow.AbortFlow("already_in_progress") + hass.config_entries.flow.async_abort(progress["flow_id"]) self._lock_cfg = lock_cfg self.context["title_placeholders"] = { @@ -228,12 +240,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): vol.Required(CONF_SLOT): int, } ) - integration = await async_get_integration(self.hass, DOMAIN) return self.async_show_form( step_id="user", data_schema=data_schema, errors=errors, - description_placeholders={"docs_url": integration.documentation}, ) diff --git a/homeassistant/components/yalexs_ble/lock.py b/homeassistant/components/yalexs_ble/lock.py index 3f75a282f67..9e97c2f080f 100644 --- a/homeassistant/components/yalexs_ble/lock.py +++ b/homeassistant/components/yalexs_ble/lock.py @@ -55,8 +55,8 @@ class YaleXSBLELock(YALEXSBLEEntity, LockEntity): async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" - return await self._device.unlock() + await self._device.unlock() async def async_lock(self, **kwargs: Any) -> None: """Lock the lock.""" - return await self._device.lock() + await self._device.lock() diff --git a/homeassistant/components/yalexs_ble/strings.json b/homeassistant/components/yalexs_ble/strings.json index 4d867474dbe..df5ac713b07 100644 --- a/homeassistant/components/yalexs_ble/strings.json +++ b/homeassistant/components/yalexs_ble/strings.json @@ -3,7 +3,7 @@ "flow_title": "{name}", "step": { "user": { - "description": "Check the documentation at {docs_url} for how to find the offline key.", + "description": "Check the documentation for how to find the offline key.", "data": { "address": "Bluetooth address", "key": "Offline Key (32-byte hex string)", @@ -22,6 +22,7 @@ "invalid_key_index": "The offline key slot must be an integer between 0 and 255." }, "abort": { + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_unconfigured_devices": "No unconfigured devices found.", "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" diff --git a/homeassistant/components/yalexs_ble/translations/en.json b/homeassistant/components/yalexs_ble/translations/en.json index 6d817499270..f8ebc0737ac 100644 --- a/homeassistant/components/yalexs_ble/translations/en.json +++ b/homeassistant/components/yalexs_ble/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", "no_devices_found": "No devices found on the network", "no_unconfigured_devices": "No unconfigured devices found." }, @@ -23,7 +24,7 @@ "key": "Offline Key (32-byte hex string)", "slot": "Offline Key Slot (Integer between 0 and 255)" }, - "description": "Check the documentation at {docs_url} for how to find the offline key." + "description": "Check the documentation for how to find the offline key." } } } diff --git a/tests/components/yalexs_ble/test_config_flow.py b/tests/components/yalexs_ble/test_config_flow.py index 7607b710934..6ea1b4e8a63 100644 --- a/tests/components/yalexs_ble/test_config_flow.py +++ b/tests/components/yalexs_ble/test_config_flow.py @@ -752,3 +752,83 @@ async def test_integration_discovery_takes_precedence_over_bluetooth_non_unique_ if flow["handler"] == DOMAIN ] assert len(flows) == 1 + + +async def test_user_is_setting_up_lock_and_discovery_happens_in_the_middle( + hass: HomeAssistant, +) -> None: + """Test that the user is setting up the lock and waiting for validation and the keys get discovered. + + In this case the integration discovery should abort and let the user continue setting up the lock. + """ + with patch( + "homeassistant.components.yalexs_ble.config_flow.async_discovered_service_info", + return_value=[NOT_YALE_DISCOVERY_INFO, YALE_ACCESS_LOCK_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + user_flow_event = asyncio.Event() + valdidate_started = asyncio.Event() + + async def _wait_for_user_flow(): + valdidate_started.set() + await user_flow_event.wait() + + with patch( + "homeassistant.components.yalexs_ble.config_flow.PushLock.validate", + side_effect=_wait_for_user_flow, + ), patch( + "homeassistant.components.yalexs_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + user_flow_task = asyncio.create_task( + hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + }, + ) + ) + await valdidate_started.wait() + + with patch( + "homeassistant.components.yalexs_ble.util.async_process_advertisements", + return_value=LOCK_DISCOVERY_INFO_UUID_ADDRESS, + ): + discovery_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={ + "name": "Front Door", + "address": OLD_FIRMWARE_LOCK_DISCOVERY_INFO.address, + "key": "2fd51b8621c6a139eaffbedcb846b60f", + "slot": 66, + "serial": "M1XXX012LU", + }, + ) + await hass.async_block_till_done() + assert discovery_result["type"] == FlowResultType.ABORT + assert discovery_result["reason"] == "already_in_progress" + + user_flow_event.set() + user_flow_result = await user_flow_task + + assert user_flow_result["type"] == FlowResultType.CREATE_ENTRY + assert user_flow_result["title"] == YALE_ACCESS_LOCK_DISCOVERY_INFO.name + assert user_flow_result["data"] == { + CONF_LOCAL_NAME: YALE_ACCESS_LOCK_DISCOVERY_INFO.name, + CONF_ADDRESS: YALE_ACCESS_LOCK_DISCOVERY_INFO.address, + CONF_KEY: "2fd51b8621c6a139eaffbedcb846b60f", + CONF_SLOT: 66, + } + assert ( + user_flow_result["result"].unique_id == YALE_ACCESS_LOCK_DISCOVERY_INFO.address + ) + assert len(mock_setup_entry.mock_calls) == 1 From c9c84639ade69a8648fdff97f24d0b3ec9d243c0 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Sat, 13 Aug 2022 10:30:39 +0200 Subject: [PATCH 3312/3516] Remove `charging_time_label` sensor in BMW Connected Drive (#76616) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/sensor.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index f1046881ed3..26fbe19b5b1 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -72,11 +72,6 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { key_class="fuel_and_battery", device_class=SensorDeviceClass.TIMESTAMP, ), - "charging_time_label": BMWSensorEntityDescription( - key="charging_time_label", - key_class="fuel_and_battery", - entity_registry_enabled_default=False, - ), "charging_status": BMWSensorEntityDescription( key="charging_status", key_class="fuel_and_battery", From e44b1fa98c9cd1df051dd709ddbf88e0a126a473 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 13 Aug 2022 10:32:58 +0200 Subject: [PATCH 3313/3516] Bump nettigo-air-monitor to 1.4.2 (#76670) --- homeassistant/components/nam/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nam/fixtures/diagnostics_data.json | 4 ++++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nam/manifest.json b/homeassistant/components/nam/manifest.json index 88048b59162..b70c2054808 100644 --- a/homeassistant/components/nam/manifest.json +++ b/homeassistant/components/nam/manifest.json @@ -3,7 +3,7 @@ "name": "Nettigo Air Monitor", "documentation": "https://www.home-assistant.io/integrations/nam", "codeowners": ["@bieniu"], - "requirements": ["nettigo-air-monitor==1.3.0"], + "requirements": ["nettigo-air-monitor==1.4.2"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 9ec008d1a45..ce8a44fc23c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1091,7 +1091,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.3.0 +nettigo-air-monitor==1.4.2 # homeassistant.components.neurio_energy neurio==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 09d654157e5..02b8258cdca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -778,7 +778,7 @@ netdisco==3.0.0 netmap==0.7.0.2 # homeassistant.components.nam -nettigo-air-monitor==1.3.0 +nettigo-air-monitor==1.4.2 # homeassistant.components.nexia nexia==2.0.2 diff --git a/tests/components/nam/fixtures/diagnostics_data.json b/tests/components/nam/fixtures/diagnostics_data.json index b90e51f34b8..1506adaa824 100644 --- a/tests/components/nam/fixtures/diagnostics_data.json +++ b/tests/components/nam/fixtures/diagnostics_data.json @@ -11,11 +11,15 @@ "heca_humidity": 50.0, "heca_temperature": 8.0, "mhz14a_carbon_dioxide": 865, + "sds011_caqi": 19, + "sds011_caqi_level": "very low", "sds011_p1": 19, "sds011_p2": 11, "sht3x_humidity": 34.7, "sht3x_temperature": 6.3, "signal": -72, + "sps30_caqi": 54, + "sps30_caqi_level": "medium", "sps30_p0": 31, "sps30_p1": 21, "sps30_p2": 34, From 9181b0171a82c31d179dcad7c6e9bd8e1714df97 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sat, 13 Aug 2022 10:35:13 +0200 Subject: [PATCH 3314/3516] Bump pyoverkiz to 1.5.0 (#76682) --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index 6e6e57f12e5..f2d17bab3f9 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -3,7 +3,7 @@ "name": "Overkiz", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/overkiz", - "requirements": ["pyoverkiz==1.4.2"], + "requirements": ["pyoverkiz==1.5.0"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index ce8a44fc23c..a76e6aa0cc4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1742,7 +1742,7 @@ pyotgw==2.0.2 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.4.2 +pyoverkiz==1.5.0 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 02b8258cdca..2b1b691b9bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1213,7 +1213,7 @@ pyotgw==2.0.2 pyotp==2.6.0 # homeassistant.components.overkiz -pyoverkiz==1.4.2 +pyoverkiz==1.5.0 # homeassistant.components.openweathermap pyowm==3.2.0 From 93a80d8fc36e115fe33c1fa8aace43f3f56d08a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 12 Aug 2022 22:36:30 -1000 Subject: [PATCH 3315/3516] Bump yalexs-ble to 1.4.0 (#76685) --- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index aa3cdcd592b..4bc21e17d3a 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.3.1"], + "requirements": ["yalexs-ble==1.4.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], diff --git a/requirements_all.txt b/requirements_all.txt index a76e6aa0cc4..058218dcefb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2500,7 +2500,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.3.1 +yalexs-ble==1.4.0 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b1b691b9bc..875ad6e7faa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1695,7 +1695,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.3.1 +yalexs-ble==1.4.0 # homeassistant.components.august yalexs==1.2.1 From bcc0a92e2b447571f90b01b941427762b4827b17 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 13 Aug 2022 14:39:04 +0200 Subject: [PATCH 3316/3516] Motion Blinds fix OperationNotAllowed (#76712) fix OperationNotAllowed homeassistant.config_entries.OperationNotAllowed --- homeassistant/components/motion_blinds/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index dfbc6ab74a7..a023fc05d14 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -121,8 +121,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: multicast_interface = entry.data.get(CONF_INTERFACE, DEFAULT_INTERFACE) wait_for_push = entry.options.get(CONF_WAIT_FOR_PUSH, DEFAULT_WAIT_FOR_PUSH) - entry.async_on_unload(entry.add_update_listener(update_listener)) - # Create multicast Listener async with setup_lock: if KEY_MULTICAST_LISTENER not in hass.data[DOMAIN]: @@ -213,6 +211,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(update_listener)) + return True From b969ed00b97d9d752770cacccb0f9df12aedc45f Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 13 Aug 2022 06:02:32 -0700 Subject: [PATCH 3317/3516] Fix google calendar disabled entity handling (#76699) Fix google calendar disable entity handling --- homeassistant/components/google/calendar.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index ca98b3da087..77ed922e511 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -192,7 +192,6 @@ async def async_setup_entry( calendar_id, data.get(CONF_SEARCH), ) - await coordinator.async_config_entry_first_refresh() entities.append( GoogleCalendarEntity( coordinator, @@ -342,6 +341,9 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity): async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() + # We do not ask for an update with async_add_entities() + # because it will update disabled entities + await self.coordinator.async_request_refresh() self._apply_coordinator_update() async def async_get_events( From db03c273f1a84617f1cee62002561a329ff03ff6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 13 Aug 2022 15:17:49 +0200 Subject: [PATCH 3318/3516] Netgear skip devices withouth mac (#76626) skip devices withouth mac --- homeassistant/components/netgear/router.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index ae4186abbb5..8e370a6b5e0 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -198,6 +198,9 @@ class NetgearRouter: _LOGGER.debug("Netgear scan result: \n%s", ntg_devices) for ntg_device in ntg_devices: + if ntg_device.mac is None: + continue + device_mac = format_mac(ntg_device.mac) if not self.devices.get(device_mac): From a70252a24322129ccdb3595327c637b9df4efb42 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Sat, 13 Aug 2022 16:05:23 +0200 Subject: [PATCH 3319/3516] Log not allowed attributes only once in BMW binary sensors (#76708) * Log not allowed keys only once * Fix spelling Co-authored-by: Jan Bouwhuis Co-authored-by: rikroe Co-authored-by: Jan Bouwhuis --- .../bmw_connected_drive/binary_sensor.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 96e8a1fc0e4..d7e28eef41c 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -37,8 +37,10 @@ ALLOWED_CONDITION_BASED_SERVICE_KEYS = { "VEHICLE_CHECK", "VEHICLE_TUV", } +LOGGED_CONDITION_BASED_SERVICE_WARNINGS = set() ALLOWED_CHECK_CONTROL_MESSAGE_KEYS = {"ENGINE_OIL", "TIRE_PRESSURE"} +LOGGED_CHECK_CONTROL_MESSAGE_WARNINGS = set() def _condition_based_services( @@ -46,12 +48,16 @@ def _condition_based_services( ) -> dict[str, Any]: extra_attributes = {} for report in vehicle.condition_based_services.messages: - if report.service_type not in ALLOWED_CONDITION_BASED_SERVICE_KEYS: + if ( + report.service_type not in ALLOWED_CONDITION_BASED_SERVICE_KEYS + and report.service_type not in LOGGED_CONDITION_BASED_SERVICE_WARNINGS + ): _LOGGER.warning( "'%s' not an allowed condition based service (%s)", report.service_type, report, ) + LOGGED_CONDITION_BASED_SERVICE_WARNINGS.add(report.service_type) continue extra_attributes.update(_format_cbs_report(report, unit_system)) @@ -61,17 +67,19 @@ def _condition_based_services( def _check_control_messages(vehicle: MyBMWVehicle) -> dict[str, Any]: extra_attributes: dict[str, Any] = {} for message in vehicle.check_control_messages.messages: - if message.description_short not in ALLOWED_CHECK_CONTROL_MESSAGE_KEYS: + if ( + message.description_short not in ALLOWED_CHECK_CONTROL_MESSAGE_KEYS + and message.description_short not in LOGGED_CHECK_CONTROL_MESSAGE_WARNINGS + ): _LOGGER.warning( "'%s' not an allowed check control message (%s)", message.description_short, message, ) + LOGGED_CHECK_CONTROL_MESSAGE_WARNINGS.add(message.description_short) continue - extra_attributes.update( - {message.description_short.lower(): message.state.value} - ) + extra_attributes[message.description_short.lower()] = message.state.value return extra_attributes From cf7c716bda484a497573c65963f696be8c294f69 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 13 Aug 2022 18:46:08 +0200 Subject: [PATCH 3320/3516] Fix implicit Optional [core] (#76719) --- .../components/application_credentials/__init__.py | 2 +- homeassistant/components/mobile_app/helpers.py | 6 +++--- homeassistant/components/mqtt/mixins.py | 2 +- homeassistant/components/template/template_entity.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/application_credentials/__init__.py b/homeassistant/components/application_credentials/__init__.py index 14ae049cfca..6dd2d562307 100644 --- a/homeassistant/components/application_credentials/__init__.py +++ b/homeassistant/components/application_credentials/__init__.py @@ -168,7 +168,7 @@ async def async_import_client_credential( hass: HomeAssistant, domain: str, credential: ClientCredential, - auth_domain: str = None, + auth_domain: str | None = None, ) -> None: """Import an existing credential from configuration.yaml.""" if DOMAIN not in hass.data: diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index e1c0841984f..a116e6196f4 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -117,7 +117,7 @@ def registration_context(registration: dict) -> Context: def empty_okay_response( - headers: dict = None, status: HTTPStatus = HTTPStatus.OK + headers: dict | None = None, status: HTTPStatus = HTTPStatus.OK ) -> Response: """Return a Response with empty JSON object and a 200.""" return Response( @@ -129,7 +129,7 @@ def error_response( code: str, message: str, status: HTTPStatus = HTTPStatus.BAD_REQUEST, - headers: dict = None, + headers: dict | None = None, ) -> Response: """Return an error Response.""" return json_response( @@ -177,7 +177,7 @@ def webhook_response( *, registration: dict, status: HTTPStatus = HTTPStatus.OK, - headers: dict = None, + headers: dict | None = None, ) -> Response: """Return a encrypted response if registration supports it.""" data = json.dumps(data, cls=JSONEncoder) diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index f0cc0c4ad44..4b7ae92c6f4 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -287,7 +287,7 @@ async def async_discover_yaml_entities( async def async_get_platform_config_from_yaml( hass: HomeAssistant, platform_domain: str, - config_yaml: ConfigType = None, + config_yaml: ConfigType | None = None, ) -> list[ConfigType]: """Return a list of validated configurations for the domain.""" diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 901834237c6..87c8ec651d2 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -79,7 +79,7 @@ LEGACY_FIELDS = { def rewrite_common_legacy_to_modern_conf( - entity_cfg: dict[str, Any], extra_legacy_fields: dict[str, str] = None + entity_cfg: dict[str, Any], extra_legacy_fields: dict[str, str] | None = None ) -> dict[str, Any]: """Rewrite legacy config.""" entity_cfg = {**entity_cfg} From 5a046ae7be68f0e8c6e8e1aae8b82f030ba7959b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 13 Aug 2022 18:46:34 +0200 Subject: [PATCH 3321/3516] Fix implicit Optional [a-n] (#76720) --- homeassistant/components/climacell/config_flow.py | 4 +++- homeassistant/components/directv/config_flow.py | 2 +- homeassistant/components/escea/climate.py | 2 +- homeassistant/components/harmony/subscriber.py | 5 ++++- homeassistant/components/huisbaasje/sensor.py | 2 +- homeassistant/components/ipp/config_flow.py | 2 +- homeassistant/components/izone/climate.py | 2 +- homeassistant/components/minecraft_server/__init__.py | 2 +- homeassistant/components/motioneye/__init__.py | 2 +- homeassistant/components/motioneye/camera.py | 8 ++++---- homeassistant/components/netgear/router.py | 6 +++--- 11 files changed, 21 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/climacell/config_flow.py b/homeassistant/components/climacell/config_flow.py index ffc76479a4d..07b85e4a4ab 100644 --- a/homeassistant/components/climacell/config_flow.py +++ b/homeassistant/components/climacell/config_flow.py @@ -19,7 +19,9 @@ class ClimaCellOptionsConfigFlow(config_entries.OptionsFlow): """Initialize ClimaCell options flow.""" self._config_entry = config_entry - async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the ClimaCell options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/directv/config_flow.py b/homeassistant/components/directv/config_flow.py index 34a09a04811..b3209638012 100644 --- a/homeassistant/components/directv/config_flow.py +++ b/homeassistant/components/directv/config_flow.py @@ -100,7 +100,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): return await self.async_step_ssdp_confirm() async def async_step_ssdp_confirm( - self, user_input: dict[str, Any] = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a confirmation flow initiated by SSDP.""" if user_input is None: diff --git a/homeassistant/components/escea/climate.py b/homeassistant/components/escea/climate.py index 7bd7d54353c..1ddea9cb026 100644 --- a/homeassistant/components/escea/climate.py +++ b/homeassistant/components/escea/climate.py @@ -152,7 +152,7 @@ class ControllerEntity(ClimateEntity): ) @callback - def set_available(self, available: bool, ex: Exception = None) -> None: + def set_available(self, available: bool, ex: Exception | None = None) -> None: """Set availability for the controller.""" if self._attr_available == available: return diff --git a/homeassistant/components/harmony/subscriber.py b/homeassistant/components/harmony/subscriber.py index efb46f5c6e6..092eb0d6859 100644 --- a/homeassistant/components/harmony/subscriber.py +++ b/homeassistant/components/harmony/subscriber.py @@ -1,4 +1,5 @@ """Mixin class for handling harmony callback subscriptions.""" +from __future__ import annotations import asyncio import logging @@ -85,7 +86,9 @@ class HarmonySubscriberMixin: self.async_unlock_start_activity() self._call_callbacks("activity_started", activity_info) - def _call_callbacks(self, callback_func_name: str, argument: tuple = None) -> None: + def _call_callbacks( + self, callback_func_name: str, argument: tuple | None = None + ) -> None: for subscription in self._subscriptions: current_callback = getattr(subscription, callback_func_name) if current_callback: diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py index 2f7f607d493..c963f366323 100644 --- a/homeassistant/components/huisbaasje/sensor.py +++ b/homeassistant/components/huisbaasje/sensor.py @@ -42,7 +42,7 @@ class HuisbaasjeSensor(CoordinatorEntity, SensorEntity): user_id: str, name: str, source_type: str, - device_class: str = None, + device_class: str | None = None, sensor_type: str = SENSOR_TYPE_RATE, unit_of_measurement: str = POWER_WATT, icon: str = "mdi:lightning-bolt", diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index 432094eee8e..7f953c4cd9a 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -174,7 +174,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): return await self.async_step_zeroconf_confirm() async def async_step_zeroconf_confirm( - self, user_input: dict[str, Any] = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a confirmation flow initiated by zeroconf.""" if user_input is None: diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index a26490e78c8..58834f995dd 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -216,7 +216,7 @@ class ControllerDevice(ClimateEntity): return self._available @callback - def set_available(self, available: bool, ex: Exception = None) -> None: + def set_available(self, available: bool, ex: Exception | None = None) -> None: """ Set availability for the controller. diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index 4abfbca9a2f..b2f7698d969 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -150,7 +150,7 @@ class MinecraftServer: ) self.online = False - async def async_update(self, now: datetime = None) -> None: + async def async_update(self, now: datetime | None = None) -> None: """Get server data from 3rd party library and update properties.""" # Check connection status. server_online_old = self.online diff --git a/homeassistant/components/motioneye/__init__.py b/homeassistant/components/motioneye/__init__.py index 7c87dda1bd2..6a650142995 100644 --- a/homeassistant/components/motioneye/__init__.py +++ b/homeassistant/components/motioneye/__init__.py @@ -544,7 +544,7 @@ class MotionEyeEntity(CoordinatorEntity): client: MotionEyeClient, coordinator: DataUpdateCoordinator, options: MappingProxyType[str, Any], - entity_description: EntityDescription = None, + entity_description: EntityDescription | None = None, ) -> None: """Initialize a motionEye entity.""" self._camera_id = camera[KEY_ID] diff --git a/homeassistant/components/motioneye/camera.py b/homeassistant/components/motioneye/camera.py index e5e4f224fe6..ff825b43bf7 100644 --- a/homeassistant/components/motioneye/camera.py +++ b/homeassistant/components/motioneye/camera.py @@ -266,10 +266,10 @@ class MotionEyeMjpegCamera(MotionEyeEntity, MjpegCamera): async def async_set_text_overlay( self, - left_text: str = None, - right_text: str = None, - custom_left_text: str = None, - custom_right_text: str = None, + left_text: str | None = None, + right_text: str | None = None, + custom_left_text: str | None = None, + custom_right_text: str | None = None, ) -> None: """Set text overlay for a camera.""" # Fetch the very latest camera config to reduce the risk of updating with a diff --git a/homeassistant/components/netgear/router.py b/homeassistant/components/netgear/router.py index 8e370a6b5e0..f69e88e83e2 100644 --- a/homeassistant/components/netgear/router.py +++ b/homeassistant/components/netgear/router.py @@ -42,9 +42,9 @@ _LOGGER = logging.getLogger(__name__) def get_api( password: str, - host: str = None, - username: str = None, - port: int = None, + host: str | None = None, + username: str | None = None, + port: int | None = None, ssl: bool = False, ) -> Netgear: """Get the Netgear API and login to it.""" From 5db1fec99e7397ed8276a8f4b25c012638afc707 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 13 Aug 2022 18:46:49 +0200 Subject: [PATCH 3322/3516] Fix implicit Optional [p-s] (#76721) --- homeassistant/components/plaato/config_flow.py | 2 +- homeassistant/components/sia/config_flow.py | 14 ++++++++++---- homeassistant/components/sia/hub.py | 2 +- homeassistant/components/solax/config_flow.py | 6 +++++- homeassistant/components/switchbot/config_flow.py | 4 +++- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py index 637122b1d9c..654150ffa48 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -127,7 +127,7 @@ class PlaatoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def _show_api_method_form( - self, device_type: PlaatoDeviceType, errors: dict = None + self, device_type: PlaatoDeviceType, errors: dict | None = None ): data_schema = vol.Schema({vol.Optional(CONF_TOKEN, default=""): str}) diff --git a/homeassistant/components/sia/config_flow.py b/homeassistant/components/sia/config_flow.py index df03882e995..516018c43a7 100644 --- a/homeassistant/components/sia/config_flow.py +++ b/homeassistant/components/sia/config_flow.py @@ -105,7 +105,9 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._data: dict[str, Any] = {} self._options: Mapping[str, Any] = {CONF_ACCOUNTS: {}} - async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial user step.""" errors: dict[str, str] | None = None if user_input is not None: @@ -117,7 +119,7 @@ class SIAConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_handle_data_and_route(user_input) async def async_step_add_account( - self, user_input: dict[str, Any] = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle the additional accounts steps.""" errors: dict[str, str] | None = None @@ -179,7 +181,9 @@ class SIAOptionsFlowHandler(config_entries.OptionsFlow): self.hub: SIAHub | None = None self.accounts_todo: list = [] - async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the SIA options.""" self.hub = self.hass.data[DOMAIN][self.config_entry.entry_id] assert self.hub is not None @@ -187,7 +191,9 @@ class SIAOptionsFlowHandler(config_entries.OptionsFlow): self.accounts_todo = [a.account_id for a in self.hub.sia_accounts] return await self.async_step_options() - async def async_step_options(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_options( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Create the options step for a account.""" errors: dict[str, str] | None = None if user_input is not None: diff --git a/homeassistant/components/sia/hub.py b/homeassistant/components/sia/hub.py index 399da14c2ad..2c2fb0d2be9 100644 --- a/homeassistant/components/sia/hub.py +++ b/homeassistant/components/sia/hub.py @@ -68,7 +68,7 @@ class SIAHub: self._hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.async_shutdown) ) - async def async_shutdown(self, _: Event = None) -> None: + async def async_shutdown(self, _: Event | None = None) -> None: """Shutdown the SIA server.""" await self.sia_client.stop() diff --git a/homeassistant/components/solax/config_flow.py b/homeassistant/components/solax/config_flow.py index 56c6989cc7f..e3255a8e377 100644 --- a/homeassistant/components/solax/config_flow.py +++ b/homeassistant/components/solax/config_flow.py @@ -1,4 +1,6 @@ """Config flow for solax integration.""" +from __future__ import annotations + import logging from typing import Any @@ -40,7 +42,9 @@ async def validate_api(data) -> str: class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Solax.""" - async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" errors: dict[str, Any] = {} if user_input is None: diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 0d7e91648f2..af2f43bdaa0 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -94,7 +94,9 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): }, ) - async def async_step_confirm(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Confirm a single device.""" assert self._discovered_adv is not None if user_input is not None: From 67e339c67beb7e1d6e4613536edcfffd0281b23f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 13 Aug 2022 18:47:17 +0200 Subject: [PATCH 3323/3516] Fix implicit Optional [t-z] (#76722) --- .../components/tomorrowio/config_flow.py | 12 ++++++++--- homeassistant/components/toon/config_flow.py | 2 +- homeassistant/components/tuya/base.py | 2 +- homeassistant/components/vizio/config_flow.py | 20 ++++++++++++------- .../components/yamaha_musiccast/__init__.py | 2 +- .../components/yamaha_musiccast/number.py | 2 +- 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/tomorrowio/config_flow.py b/homeassistant/components/tomorrowio/config_flow.py index a0fa7b0b34c..71df06394ed 100644 --- a/homeassistant/components/tomorrowio/config_flow.py +++ b/homeassistant/components/tomorrowio/config_flow.py @@ -45,7 +45,9 @@ _LOGGER = logging.getLogger(__name__) def _get_config_schema( - hass: core.HomeAssistant, source: str | None, input_dict: dict[str, Any] = None + hass: core.HomeAssistant, + source: str | None, + input_dict: dict[str, Any] | None = None, ) -> vol.Schema: """ Return schema defaults for init step based on user input/config dict. @@ -99,7 +101,9 @@ class TomorrowioOptionsConfigFlow(config_entries.OptionsFlow): """Initialize Tomorrow.io options flow.""" self._config_entry = config_entry - async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the Tomorrow.io options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) @@ -134,7 +138,9 @@ class TomorrowioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return TomorrowioOptionsConfigFlow(config_entry) - async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/toon/config_flow.py b/homeassistant/components/toon/config_flow.py index e86d951069c..5c98e35bead 100644 --- a/homeassistant/components/toon/config_flow.py +++ b/homeassistant/components/toon/config_flow.py @@ -64,7 +64,7 @@ class ToonFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): return await self.async_step_user() async def async_step_agreement( - self, user_input: dict[str, Any] = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Select Toon agreement to add.""" if len(self.agreements) == 1: diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 624bfd80cd4..f27101e373d 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -189,7 +189,7 @@ class TuyaEntity(Entity): dpcodes: str | DPCode | tuple[DPCode, ...] | None, *, prefer_function: bool = False, - dptype: DPType = None, + dptype: DPType | None = None, ) -> DPCode | EnumTypeData | IntegerTypeData | None: """Find a matching DP code available on for this device.""" if dpcodes is None: diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index a80105579fe..54e4ef53fe1 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -49,7 +49,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def _get_config_schema(input_dict: dict[str, Any] = None) -> vol.Schema: +def _get_config_schema(input_dict: dict[str, Any] | None = None) -> vol.Schema: """ Return schema defaults for init step based on user input/config dict. @@ -81,7 +81,7 @@ def _get_config_schema(input_dict: dict[str, Any] = None) -> vol.Schema: ) -def _get_pairing_schema(input_dict: dict[str, Any] = None) -> vol.Schema: +def _get_pairing_schema(input_dict: dict[str, Any] | None = None) -> vol.Schema: """ Return schema defaults for pairing data based on user input. @@ -112,7 +112,9 @@ class VizioOptionsConfigFlow(config_entries.OptionsFlow): """Initialize vizio options flow.""" self.config_entry = config_entry - async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Manage the vizio options.""" if user_input is not None: if user_input.get(CONF_APPS_TO_INCLUDE_OR_EXCLUDE): @@ -204,7 +206,9 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=input_dict[CONF_NAME], data=input_dict) - async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" errors = {} @@ -381,7 +385,9 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } ) - async def async_step_pair_tv(self, user_input: dict[str, Any] = None) -> FlowResult: + async def async_step_pair_tv( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """ Start pairing process for TV. @@ -460,7 +466,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) async def async_step_pairing_complete( - self, user_input: dict[str, Any] = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """ Complete non-import sourced config flow. @@ -470,7 +476,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self._pairing_complete("pairing_complete") async def async_step_pairing_complete_import( - self, user_input: dict[str, Any] = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """ Complete import sourced config flow. diff --git a/homeassistant/components/yamaha_musiccast/__init__.py b/homeassistant/components/yamaha_musiccast/__init__.py index b8117df056a..639f0b69a41 100644 --- a/homeassistant/components/yamaha_musiccast/__init__.py +++ b/homeassistant/components/yamaha_musiccast/__init__.py @@ -216,7 +216,7 @@ class MusicCastCapabilityEntity(MusicCastDeviceEntity): self, coordinator: MusicCastDataUpdateCoordinator, capability: Capability, - zone_id: str = None, + zone_id: str | None = None, ) -> None: """Initialize a capability based entity.""" if zone_id is not None: diff --git a/homeassistant/components/yamaha_musiccast/number.py b/homeassistant/components/yamaha_musiccast/number.py index 98cda92ffea..105cb0edb3a 100644 --- a/homeassistant/components/yamaha_musiccast/number.py +++ b/homeassistant/components/yamaha_musiccast/number.py @@ -42,7 +42,7 @@ class NumberCapability(MusicCastCapabilityEntity, NumberEntity): self, coordinator: MusicCastDataUpdateCoordinator, capability: NumberSetter, - zone_id: str = None, + zone_id: str | None = None, ) -> None: """Initialize the number entity.""" super().__init__(coordinator, capability, zone_id) From 4fc1d59b74238c307f3f4059f795f11fd5f64274 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Aug 2022 18:55:09 +0200 Subject: [PATCH 3324/3516] Bump actions/cache from 3.0.6 to 3.0.7 (#76648) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ae7d8cfd48d..e4a47ca2b03 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -172,7 +172,7 @@ jobs: cache: "pip" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -185,7 +185,7 @@ jobs: pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -211,7 +211,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -222,7 +222,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -260,7 +260,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -271,7 +271,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -312,7 +312,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -323,7 +323,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -353,7 +353,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: ${{ runner.os }}-venv-${{ needs.info.outputs.pre-commit_cache_key }} @@ -364,7 +364,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: ${{ env.PRE_COMMIT_CACHE }} key: ${{ runner.os }}-pre-commit-${{ needs.info.outputs.pre-commit_cache_key }} @@ -480,7 +480,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: >- @@ -488,7 +488,7 @@ jobs: needs.info.outputs.python_cache_key }} - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: ${{ env.PIP_CACHE }} key: >- @@ -538,7 +538,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: >- @@ -570,7 +570,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: >- @@ -603,7 +603,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: >- @@ -647,7 +647,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: >- @@ -695,7 +695,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: >- @@ -749,7 +749,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache@v3.0.6 + uses: actions/cache@v3.0.7 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ From bac44cf473f84c1923a1181e4c5851257e873b83 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 13 Aug 2022 19:33:57 +0200 Subject: [PATCH 3325/3516] Enable no_implicit_optional globally [mypy] (#76723) --- mypy.ini | 241 +-------------------------------- script/hassfest/mypy_config.py | 2 +- 2 files changed, 2 insertions(+), 241 deletions(-) diff --git a/mypy.ini b/mypy.ini index 700d96a4982..7338d9a67f0 100644 --- a/mypy.ini +++ b/mypy.ini @@ -8,6 +8,7 @@ show_error_codes = true follow_imports = silent ignore_missing_imports = true strict_equality = true +no_implicit_optional = true warn_incomplete_stub = true warn_redundant_casts = true warn_unused_configs = true @@ -20,7 +21,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -124,7 +124,6 @@ disallow_subclassing_any = false disallow_untyped_calls = false disallow_untyped_decorators = false disallow_untyped_defs = false -no_implicit_optional = false warn_return_any = false warn_unreachable = false no_implicit_reexport = false @@ -136,7 +135,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true no_implicit_reexport = true @@ -148,7 +146,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -159,7 +156,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -170,7 +166,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -181,7 +176,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -192,7 +186,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -203,7 +196,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -214,7 +206,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -225,7 +216,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -236,7 +226,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -247,7 +236,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -258,7 +246,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -269,7 +256,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -280,7 +266,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -291,7 +276,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -302,7 +286,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -313,7 +296,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -324,7 +306,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -335,7 +316,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -346,7 +326,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -357,7 +336,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -368,7 +346,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -379,7 +356,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -390,7 +366,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -401,7 +376,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -412,7 +386,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -423,7 +396,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -434,7 +406,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -445,7 +416,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -456,7 +426,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -467,7 +436,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -478,7 +446,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -489,7 +456,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -500,7 +466,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -511,7 +476,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -522,7 +486,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -533,7 +496,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -544,7 +506,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -555,7 +516,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -566,7 +526,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -577,7 +536,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -588,7 +546,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -599,7 +556,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -610,7 +566,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -621,7 +576,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -632,7 +586,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -643,7 +596,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -654,7 +606,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -665,7 +616,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -676,7 +626,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -687,7 +636,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -698,7 +646,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -709,7 +656,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -720,7 +666,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -731,7 +676,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -742,7 +686,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -753,7 +696,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -764,7 +706,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -775,7 +716,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -786,7 +726,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -797,7 +736,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -808,7 +746,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -819,7 +756,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -830,7 +766,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -841,7 +776,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -852,7 +786,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -863,7 +796,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -874,7 +806,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -885,7 +816,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -896,7 +826,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -907,7 +836,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -918,7 +846,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -929,7 +856,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -940,7 +866,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -951,7 +876,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -962,7 +886,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -973,7 +896,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -984,7 +906,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -995,7 +916,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1006,7 +926,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1017,7 +936,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1028,7 +946,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1039,7 +956,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1050,7 +966,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1061,7 +976,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1072,7 +986,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1083,7 +996,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1094,7 +1006,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1105,7 +1016,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1116,7 +1026,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1127,7 +1036,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1138,7 +1046,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1149,7 +1056,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1160,7 +1066,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1171,7 +1076,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1182,7 +1086,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1193,7 +1096,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1204,7 +1106,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1215,7 +1116,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1226,7 +1126,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1237,7 +1136,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1248,7 +1146,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1259,7 +1156,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1270,7 +1166,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1281,7 +1176,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1292,7 +1186,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1303,7 +1196,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1314,7 +1206,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1325,7 +1216,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1336,7 +1226,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1347,7 +1236,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1358,7 +1246,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1369,7 +1256,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1380,7 +1266,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1391,7 +1276,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1402,7 +1286,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1413,7 +1296,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1424,7 +1306,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1435,7 +1316,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1446,7 +1326,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1457,7 +1336,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1468,7 +1346,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1479,7 +1356,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1490,7 +1366,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1501,7 +1376,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1512,7 +1386,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1523,7 +1396,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1534,7 +1406,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1545,7 +1416,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1556,7 +1426,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1567,7 +1436,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1578,7 +1446,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1589,7 +1456,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1600,7 +1466,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1611,7 +1476,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1622,7 +1486,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1633,7 +1496,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1644,7 +1506,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1655,7 +1516,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1666,7 +1526,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1677,7 +1536,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1688,7 +1546,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1699,7 +1556,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1710,7 +1566,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1721,7 +1576,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1732,7 +1586,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1743,7 +1596,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1754,7 +1606,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1765,7 +1616,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1776,7 +1626,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1787,7 +1636,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1798,7 +1646,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1809,7 +1656,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1820,7 +1666,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1831,7 +1676,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1842,7 +1686,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1853,7 +1696,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1864,7 +1706,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1875,7 +1716,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1886,7 +1726,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1897,7 +1736,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1908,7 +1746,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1919,7 +1756,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1930,7 +1766,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1941,7 +1776,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1952,7 +1786,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1963,7 +1796,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1974,7 +1806,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1985,7 +1816,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -1996,7 +1826,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2007,7 +1836,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2018,7 +1846,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2029,7 +1856,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2040,7 +1866,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2051,7 +1876,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2062,7 +1886,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2073,7 +1896,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2084,7 +1906,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2095,7 +1916,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2106,7 +1926,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2117,7 +1936,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2128,7 +1946,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2139,7 +1956,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2150,7 +1966,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2161,7 +1976,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2172,7 +1986,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2183,7 +1996,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2194,7 +2006,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2205,7 +2016,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2216,7 +2026,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2227,7 +2036,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2238,7 +2046,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true no_implicit_reexport = true @@ -2250,7 +2057,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2261,7 +2067,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2272,7 +2077,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2283,7 +2087,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2294,7 +2097,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2305,7 +2107,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2316,7 +2117,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2327,7 +2127,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2338,7 +2137,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2349,7 +2147,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2360,7 +2157,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2371,7 +2167,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2382,7 +2177,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2393,7 +2187,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2404,7 +2197,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2415,7 +2207,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2426,7 +2217,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2437,7 +2227,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2448,7 +2237,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2459,7 +2247,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2470,7 +2257,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2481,7 +2267,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2492,7 +2277,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true no_implicit_reexport = true @@ -2504,7 +2288,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2515,7 +2298,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2526,7 +2308,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2537,7 +2318,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2548,7 +2328,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2559,7 +2338,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2570,7 +2348,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2581,7 +2358,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2592,7 +2368,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2603,7 +2378,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2614,7 +2388,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2625,7 +2398,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2636,7 +2408,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2647,7 +2418,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2658,7 +2428,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2669,7 +2438,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2680,7 +2448,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2691,7 +2458,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2702,7 +2468,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2713,7 +2478,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2724,7 +2488,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2735,7 +2498,6 @@ disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true -no_implicit_optional = true warn_return_any = true warn_unreachable = true @@ -2755,7 +2517,6 @@ disallow_subclassing_any = false disallow_untyped_calls = false disallow_untyped_decorators = false disallow_untyped_defs = false -no_implicit_optional = false warn_return_any = false warn_unreachable = false diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 4bf8cfd68cf..b6c31751e12 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -54,6 +54,7 @@ GENERAL_SETTINGS: Final[dict[str, str]] = { # Enable some checks globally. "ignore_missing_imports": "true", "strict_equality": "true", + "no_implicit_optional": "true", "warn_incomplete_stub": "true", "warn_redundant_casts": "true", "warn_unused_configs": "true", @@ -74,7 +75,6 @@ STRICT_SETTINGS: Final[list[str]] = [ "disallow_untyped_calls", "disallow_untyped_decorators", "disallow_untyped_defs", - "no_implicit_optional", "warn_return_any", "warn_unreachable", # TODO: turn these on, address issues From b4a840c00dd680f8f674c660860ee0e159845a69 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Aug 2022 12:47:35 -1000 Subject: [PATCH 3326/3516] Avoid creating door sensor when it does no exist on older yalexs_ble locks (#76710) --- homeassistant/components/yalexs_ble/binary_sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/yalexs_ble/binary_sensor.py b/homeassistant/components/yalexs_ble/binary_sensor.py index 3ee88dbaa5e..32421f67fbb 100644 --- a/homeassistant/components/yalexs_ble/binary_sensor.py +++ b/homeassistant/components/yalexs_ble/binary_sensor.py @@ -23,7 +23,9 @@ async def async_setup_entry( ) -> None: """Set up YALE XS binary sensors.""" data: YaleXSBLEData = hass.data[DOMAIN][entry.entry_id] - async_add_entities([YaleXSBLEDoorSensor(data)]) + lock = data.lock + if lock.lock_info and lock.lock_info.door_sense: + async_add_entities([YaleXSBLEDoorSensor(data)]) class YaleXSBLEDoorSensor(YALEXSBLEEntity, BinarySensorEntity): From bec8e544f42e87ec6a79ed546414de84fc0a256a Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 14 Aug 2022 00:25:47 +0000 Subject: [PATCH 3327/3516] [ci skip] Translation update --- .../components/abode/translations/es.json | 6 +-- .../accuweather/translations/es.json | 2 +- .../components/adax/translations/es.json | 2 +- .../components/adguard/translations/es.json | 6 +-- .../components/agent_dvr/translations/es.json | 2 +- .../components/airly/translations/es.json | 4 +- .../components/airvisual/translations/es.json | 10 ++-- .../alarm_control_panel/translations/es.json | 10 ++-- .../alarmdecoder/translations/es.json | 22 ++++----- .../components/almond/translations/es.json | 4 +- .../ambiclimate/translations/es.json | 8 ++-- .../android_ip_webcam/translations/es.json | 2 +- .../android_ip_webcam/translations/it.json | 6 +++ .../components/apple_tv/translations/es.json | 6 +-- .../components/asuswrt/translations/es.json | 4 +- .../components/atag/translations/es.json | 4 +- .../components/aurora/translations/es.json | 2 +- .../components/awair/translations/hu.json | 2 +- .../components/awair/translations/id.json | 2 +- .../components/awair/translations/it.json | 31 ++++++++++-- .../components/axis/translations/es.json | 2 +- .../azure_devops/translations/es.json | 4 +- .../binary_sensor/translations/es.json | 48 +++++++++---------- .../components/blink/translations/es.json | 2 +- .../bmw_connected_drive/translations/es.json | 2 +- .../components/bond/translations/es.json | 2 +- .../components/braviatv/translations/es.json | 10 ++-- .../components/broadlink/translations/es.json | 2 +- .../components/brother/translations/es.json | 2 +- .../components/bsblan/translations/es.json | 2 +- .../components/button/translations/es.json | 2 +- .../cert_expiry/translations/es.json | 2 +- .../components/climate/translations/es.json | 12 ++--- .../components/cloud/translations/es.json | 8 ++-- .../cloudflare/translations/es.json | 12 ++--- .../components/control4/translations/es.json | 8 ++-- .../coolmaster/translations/es.json | 14 +++--- .../coronavirus/translations/es.json | 2 +- .../components/cover/translations/es.json | 10 ++-- .../components/deconz/translations/es.json | 30 ++++++------ .../components/demo/translations/es.json | 2 +- .../components/denonavr/translations/es.json | 10 ++-- .../deutsche_bahn/translations/it.json | 1 + .../components/dexcom/translations/es.json | 2 +- .../components/directv/translations/es.json | 2 +- .../components/doorbird/translations/es.json | 2 +- .../components/eafm/translations/es.json | 6 +-- .../components/ecobee/translations/es.json | 8 ++-- .../components/elgato/translations/es.json | 4 +- .../components/enocean/translations/es.json | 12 ++--- .../components/escea/translations/it.json | 5 ++ .../components/fan/translations/es.json | 12 ++--- .../flick_electric/translations/es.json | 4 +- .../components/flo/translations/es.json | 4 +- .../components/flume/translations/es.json | 2 +- .../forked_daapd/translations/es.json | 12 ++--- .../components/freebox/translations/es.json | 6 +-- .../components/fritzbox/translations/es.json | 2 +- .../fritzbox_callmonitor/translations/es.json | 6 +-- .../components/gdacs/translations/es.json | 2 +- .../geocaching/translations/es.json | 4 +- .../geonetnz_quakes/translations/es.json | 2 +- .../geonetnz_volcano/translations/es.json | 2 +- .../components/glances/translations/es.json | 2 +- .../components/google/translations/es.json | 2 +- .../components/guardian/translations/es.json | 2 +- .../components/guardian/translations/hu.json | 6 +-- .../components/guardian/translations/it.json | 13 +++++ .../components/harmony/translations/es.json | 2 +- .../components/hassio/translations/es.json | 2 +- .../home_connect/translations/es.json | 2 +- .../home_plus_control/translations/es.json | 4 +- .../homeassistant/translations/es.json | 2 +- .../components/homekit/translations/es.json | 12 ++--- .../homekit_controller/translations/es.json | 16 +++---- .../translations/sensor.it.json | 10 ++++ .../huawei_lte/translations/es.json | 2 +- .../components/hue/translations/es.json | 6 +-- .../humidifier/translations/es.json | 8 ++-- .../translations/es.json | 4 +- .../hvv_departures/translations/es.json | 10 ++-- .../components/hyperion/translations/es.json | 18 +++---- .../components/iaqualink/translations/es.json | 2 +- .../components/icloud/translations/es.json | 8 ++-- .../input_datetime/translations/es.json | 2 +- .../components/insteon/translations/es.json | 40 ++++++++-------- .../components/ipma/translations/es.json | 4 +- .../components/ipp/translations/es.json | 8 ++-- .../components/iqvia/translations/es.json | 2 +- .../components/isy994/translations/es.json | 6 +-- .../keenetic_ndms2/translations/es.json | 2 +- .../components/kodi/translations/es.json | 10 ++-- .../components/konnected/translations/es.json | 28 +++++------ .../components/life360/translations/es.json | 6 +-- .../components/light/translations/es.json | 4 +- .../components/lock/translations/es.json | 8 ++-- .../logi_circle/translations/es.json | 12 ++--- .../lutron_caseta/translations/es.json | 24 +++++----- .../components/lyric/translations/es.json | 4 +- .../components/mazda/translations/es.json | 4 +- .../media_player/translations/es.json | 8 ++-- .../components/melcloud/translations/es.json | 8 ++-- .../components/met/translations/es.json | 4 +- .../meteo_france/translations/es.json | 4 +- .../components/metoffice/translations/es.json | 2 +- .../components/mikrotik/translations/es.json | 8 ++-- .../minecraft_server/translations/es.json | 8 ++-- .../motion_blinds/translations/es.json | 10 ++-- .../components/mqtt/translations/es.json | 10 ++-- .../components/myq/translations/es.json | 4 +- .../components/mysensors/translations/es.json | 28 +++++------ .../components/nanoleaf/translations/es.json | 2 +- .../components/neato/translations/es.json | 4 +- .../components/nest/translations/es.json | 12 ++--- .../components/netatmo/translations/es.json | 20 ++++---- .../components/nexia/translations/es.json | 2 +- .../nightscout/translations/es.json | 2 +- .../components/nuheat/translations/es.json | 4 +- .../components/nut/translations/es.json | 2 +- .../components/nzbget/translations/es.json | 2 +- .../ondilo_ico/translations/es.json | 4 +- .../components/onvif/translations/es.json | 16 +++---- .../openexchangerates/translations/it.json | 6 ++- .../opentherm_gw/translations/es.json | 2 +- .../ovo_energy/translations/es.json | 4 +- .../panasonic_viera/translations/es.json | 8 ++-- .../philips_js/translations/es.json | 4 +- .../components/plaato/translations/es.json | 20 ++++---- .../components/plex/translations/es.json | 10 ++-- .../components/plugwise/translations/es.json | 6 +-- .../components/point/translations/es.json | 4 +- .../components/powerwall/translations/es.json | 2 +- .../progettihwsw/translations/es.json | 2 +- .../components/ps4/translations/es.json | 4 +- .../components/rachio/translations/es.json | 4 +- .../recollect_waste/translations/es.json | 2 +- .../components/remote/translations/es.json | 6 +-- .../components/rfxtrx/translations/es.json | 22 ++++----- .../components/ring/translations/es.json | 2 +- .../components/risco/translations/es.json | 10 ++-- .../components/roku/translations/es.json | 4 +- .../components/roomba/translations/es.json | 10 ++-- .../components/rpi_power/translations/es.json | 2 +- .../components/samsungtv/translations/es.json | 12 ++--- .../components/schedule/translations/hu.json | 2 +- .../components/schedule/translations/it.json | 9 ++++ .../components/sensor/translations/es.json | 16 +++---- .../components/sentry/translations/es.json | 16 +++---- .../components/senz/translations/es.json | 4 +- .../components/shelly/translations/es.json | 6 +-- .../simplisafe/translations/es.json | 8 ++-- .../components/smappee/translations/es.json | 10 ++-- .../smartthings/translations/es.json | 4 +- .../components/solaredge/translations/es.json | 2 +- .../components/solarlog/translations/es.json | 4 +- .../components/soma/translations/es.json | 10 ++-- .../components/sonarr/translations/es.json | 4 +- .../components/songpal/translations/es.json | 2 +- .../soundtouch/translations/es.json | 2 +- .../speedtestdotnet/translations/es.json | 2 +- .../components/spotify/translations/es.json | 8 ++-- .../squeezebox/translations/es.json | 2 +- .../srp_energy/translations/es.json | 2 +- .../components/switch/translations/es.json | 4 +- .../components/switchbot/translations/it.json | 9 ++++ .../components/syncthru/translations/es.json | 2 +- .../synology_dsm/translations/es.json | 6 +-- .../system_health/translations/es.json | 2 +- .../components/tado/translations/es.json | 4 +- .../tellduslive/translations/es.json | 2 +- .../components/tibber/translations/es.json | 2 +- .../components/toon/translations/es.json | 6 +-- .../components/traccar/translations/es.json | 4 +- .../transmission/translations/es.json | 6 +-- .../components/tuya/translations/es.json | 2 +- .../twentemilieu/translations/es.json | 4 +- .../components/unifi/translations/es.json | 18 +++---- .../components/upb/translations/es.json | 4 +- .../components/vacuum/translations/es.json | 4 +- .../components/velbus/translations/es.json | 4 +- .../components/vera/translations/es.json | 8 ++-- .../components/vesync/translations/es.json | 2 +- .../components/vizio/translations/es.json | 16 +++---- .../components/weather/translations/es.json | 2 +- .../components/wilight/translations/es.json | 4 +- .../components/withings/translations/es.json | 6 +-- .../components/wled/translations/es.json | 2 +- .../components/wolflink/translations/es.json | 2 +- .../wolflink/translations/sensor.es.json | 20 ++++---- .../components/xbox/translations/es.json | 4 +- .../xiaomi_aqara/translations/es.json | 16 +++---- .../yalexs_ble/translations/de.json | 3 +- .../yalexs_ble/translations/es.json | 3 +- .../yalexs_ble/translations/fr.json | 4 +- .../yalexs_ble/translations/hu.json | 1 + .../yalexs_ble/translations/id.json | 3 +- .../yalexs_ble/translations/it.json | 31 ++++++++++++ .../yalexs_ble/translations/pt-BR.json | 3 +- .../components/yolink/translations/es.json | 4 +- .../components/zha/translations/es.json | 18 +++---- .../components/zwave_js/translations/es.json | 14 +++--- 201 files changed, 756 insertions(+), 636 deletions(-) create mode 100644 homeassistant/components/homekit_controller/translations/sensor.it.json create mode 100644 homeassistant/components/schedule/translations/it.json create mode 100644 homeassistant/components/yalexs_ble/translations/it.json diff --git a/homeassistant/components/abode/translations/es.json b/homeassistant/components/abode/translations/es.json index 66cb5d13f22..c7db5e8db6a 100644 --- a/homeassistant/components/abode/translations/es.json +++ b/homeassistant/components/abode/translations/es.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "invalid_mfa_code": "C\u00f3digo MFA inv\u00e1lido" + "invalid_mfa_code": "C\u00f3digo MFA no v\u00e1lido" }, "step": { "mfa": { @@ -21,14 +21,14 @@ "password": "Contrase\u00f1a", "username": "Correo electr\u00f3nico" }, - "title": "Rellene su informaci\u00f3n de inicio de sesi\u00f3n de Abode" + "title": "Completa tu informaci\u00f3n de inicio de sesi\u00f3n de Abode" }, "user": { "data": { "password": "Contrase\u00f1a", "username": "Correo electr\u00f3nico" }, - "title": "Rellene la informaci\u00f3n de acceso Abode" + "title": "Completa tu informaci\u00f3n de inicio de sesi\u00f3n de Abode" } } } diff --git a/homeassistant/components/accuweather/translations/es.json b/homeassistant/components/accuweather/translations/es.json index df5b9c21494..9ec67fd88b8 100644 --- a/homeassistant/components/accuweather/translations/es.json +++ b/homeassistant/components/accuweather/translations/es.json @@ -34,7 +34,7 @@ }, "system_health": { "info": { - "can_reach_server": "Alcanzar el servidor AccuWeather", + "can_reach_server": "Se puede llegar al servidor AccuWeather", "remaining_requests": "Solicitudes permitidas restantes" } } diff --git a/homeassistant/components/adax/translations/es.json b/homeassistant/components/adax/translations/es.json index c2d224aacb1..90dc1ad05bc 100644 --- a/homeassistant/components/adax/translations/es.json +++ b/homeassistant/components/adax/translations/es.json @@ -21,7 +21,7 @@ "wifi_pswd": "Contrase\u00f1a Wi-Fi", "wifi_ssid": "SSID Wi-Fi" }, - "description": "Reinicia el calentador presionando + y OK hasta que la pantalla muestre 'Restablecer'. Luego mant\u00e9n presionado el bot\u00f3n OK en el calentador hasta que el led azul comience a parpadear antes de presionar Enviar. La configuraci\u00f3n del calentador puede tardar algunos minutos." + "description": "Reinicia el calentador pulsando + y OK hasta que la pantalla muestre 'Restablecer'. Luego mant\u00e9n pulsado el bot\u00f3n OK en el calentador hasta que el led azul comience a parpadear antes de pulsar Enviar. La configuraci\u00f3n del calentador puede tardar algunos minutos." }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/es.json b/homeassistant/components/adguard/translations/es.json index 96a4546f735..6cc1022f7ae 100644 --- a/homeassistant/components/adguard/translations/es.json +++ b/homeassistant/components/adguard/translations/es.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "\u00bfQuieres configurar Home Assistant para conectarse a AdGuard Home proporcionado por el complemento: {addon} ?", - "title": "AdGuard Home v\u00eda complemento de Home Assistant" + "description": "\u00bfQuieres configurar Home Assistant para conectarse al AdGuard Home proporcionado por el complemento: {addon}?", + "title": "AdGuard Home a trav\u00e9s del complemento Home Assistant" }, "user": { "data": { @@ -21,7 +21,7 @@ "username": "Nombre de usuario", "verify_ssl": "Verificar el certificado SSL" }, - "description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control." + "description": "Configura tu instancia AdGuard Home para permitir la supervisi\u00f3n y el control." } } } diff --git a/homeassistant/components/agent_dvr/translations/es.json b/homeassistant/components/agent_dvr/translations/es.json index 996e006898d..685f0c38045 100644 --- a/homeassistant/components/agent_dvr/translations/es.json +++ b/homeassistant/components/agent_dvr/translations/es.json @@ -13,7 +13,7 @@ "host": "Host", "port": "Puerto" }, - "title": "Configurar el Agente de DVR" + "title": "Configurar Agent DVR" } } } diff --git a/homeassistant/components/airly/translations/es.json b/homeassistant/components/airly/translations/es.json index 1a4197b758b..efe0228e715 100644 --- a/homeassistant/components/airly/translations/es.json +++ b/homeassistant/components/airly/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "invalid_api_key": "Clave API no v\u00e1lida", - "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta zona." + "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta \u00e1rea." }, "step": { "user": { @@ -21,7 +21,7 @@ }, "system_health": { "info": { - "can_reach_server": "Alcanzar el servidor Airly", + "can_reach_server": "Se puede llegar al servidor Airly", "requests_per_day": "Solicitudes permitidas por d\u00eda", "requests_remaining": "Solicitudes permitidas restantes" } diff --git a/homeassistant/components/airvisual/translations/es.json b/homeassistant/components/airvisual/translations/es.json index 5c38176bf22..739aaa818ed 100644 --- a/homeassistant/components/airvisual/translations/es.json +++ b/homeassistant/components/airvisual/translations/es.json @@ -17,7 +17,7 @@ "latitude": "Latitud", "longitude": "Longitud" }, - "description": "Utilice la API de la nube de AirVisual para supervisar una latitud/longitud.", + "description": "Utiliza la API de nube de AirVisual para supervisar una latitud/longitud.", "title": "Configurar una geograf\u00eda" }, "geography_by_name": { @@ -27,7 +27,7 @@ "country": "Pa\u00eds", "state": "estado" }, - "description": "Utilice la API de la nube de AirVisual para supervisar una ciudad/estado/pa\u00eds.", + "description": "Utiliza la API en la nube de AirVisual para supervisar una ciudad/estado/pa\u00eds.", "title": "Configurar una geograf\u00eda" }, "node_pro": { @@ -35,7 +35,7 @@ "ip_address": "Host", "password": "Contrase\u00f1a" }, - "description": "Monitorizar una unidad personal AirVisual. La contrase\u00f1a puede ser recuperada desde la interfaz de la unidad.", + "description": "Supervisar una unidad AirVisual personal. La contrase\u00f1a se puede recuperar desde la IU de la unidad.", "title": "Configurar un AirVisual Node/Pro" }, "reauth_confirm": { @@ -45,7 +45,7 @@ "title": "Volver a autenticar AirVisual" }, "user": { - "description": "Elige qu\u00e9 tipo de datos de AirVisual quieres monitorizar.", + "description": "Elige qu\u00e9 tipo de datos de AirVisual quieres supervisar.", "title": "Configurar AirVisual" } } @@ -54,7 +54,7 @@ "step": { "init": { "data": { - "show_on_map": "Mostrar geograf\u00eda monitorizada en el mapa" + "show_on_map": "Mostrar geograf\u00eda supervisada en el mapa" }, "title": "Configurar AirVisual" } diff --git a/homeassistant/components/alarm_control_panel/translations/es.json b/homeassistant/components/alarm_control_panel/translations/es.json index 38c4c059b48..212e31b9670 100644 --- a/homeassistant/components/alarm_control_panel/translations/es.json +++ b/homeassistant/components/alarm_control_panel/translations/es.json @@ -1,12 +1,12 @@ { "device_automation": { "action_type": { - "arm_away": "Armar {entity_name} exterior", - "arm_home": "Armar {entity_name} modo casa", - "arm_night": "Armar {entity_name} por la noche", + "arm_away": "Armar ausente en {entity_name}", + "arm_home": "Armar en casa en {entity_name}", + "arm_night": "Armar noche en {entity_name}", "arm_vacation": "Armar de vacaciones {entity_name}", "disarm": "Desarmar {entity_name}", - "trigger": "Lanzar {entity_name}" + "trigger": "Disparar {entity_name}" }, "condition_type": { "is_armed_away": "{entity_name} est\u00e1 armada ausente", @@ -22,7 +22,7 @@ "armed_night": "{entity_name} armada noche", "armed_vacation": "{entity_name} en armada de vacaciones", "disarmed": "{entity_name} desarmada", - "triggered": "{entity_name} activado" + "triggered": "{entity_name} disparada" } }, "state": { diff --git a/homeassistant/components/alarmdecoder/translations/es.json b/homeassistant/components/alarmdecoder/translations/es.json index 5dfd7ab5745..a568c853365 100644 --- a/homeassistant/components/alarmdecoder/translations/es.json +++ b/homeassistant/components/alarmdecoder/translations/es.json @@ -23,23 +23,23 @@ "data": { "protocol": "Protocolo" }, - "title": "Elige el protocolo del AlarmDecoder" + "title": "Elige el protocolo AlarmDecoder" } } }, "options": { "error": { - "int": "El campo siguiente debe ser un n\u00famero entero.", - "loop_range": "El bucle RF debe ser un n\u00famero entero entre 1 y 4.", - "loop_rfid": "El bucle de RF no puede utilizarse sin el serie RF.", - "relay_inclusive": "La direcci\u00f3n de retransmisi\u00f3n y el canal de retransmisi\u00f3n son codependientes y deben incluirse a la vez." + "int": "El siguiente campo debe ser un n\u00famero entero.", + "loop_range": "RF Loop debe ser un n\u00famero entero entre 1 y 4.", + "loop_rfid": "RF Loop no se puede utilizar sin RF Serial.", + "relay_inclusive": "La direcci\u00f3n de retransmisi\u00f3n y el canal de retransmisi\u00f3n son c\u00f3digopendientes y deben incluirse a la vez." }, "step": { "arm_settings": { "data": { - "alt_night_mode": "Modo noche alternativo", - "auto_bypass": "Desv\u00edo autom\u00e1tico al armar", - "code_arm_required": "C\u00f3digo requerido para el armado" + "alt_night_mode": "Modo nocturno alternativo", + "auto_bypass": "Anulaci\u00f3n autom\u00e1tica en armado", + "code_arm_required": "C\u00f3digo Requerido para Armar" }, "title": "Configurar AlarmDecoder" }, @@ -52,14 +52,14 @@ }, "zone_details": { "data": { - "zone_loop": "Bucle RF", + "zone_loop": "RF Loop", "zone_name": "Nombre de zona", "zone_relayaddr": "Direcci\u00f3n de retransmisi\u00f3n", "zone_relaychan": "Canal de retransmisi\u00f3n", - "zone_rfid": "Serie RF", + "zone_rfid": "RF Serial", "zone_type": "Tipo de zona" }, - "description": "Introduce los detalles para la zona {zona_number}. Para borrar la zona {zone_number}, deja el nombre de la zona en blanco.", + "description": "Introduce los detalles para la zona {zone_number}. Para eliminar la zona {zone_number}, deja el nombre de la zona en blanco.", "title": "Configurar AlarmDecoder" }, "zone_select": { diff --git a/homeassistant/components/almond/translations/es.json b/homeassistant/components/almond/translations/es.json index a3383cd4b5f..1a5b3ddf074 100644 --- a/homeassistant/components/almond/translations/es.json +++ b/homeassistant/components/almond/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "cannot_connect": "No se pudo conectar", - "missing_configuration": "El componente no est\u00e1 configurado. Mira su documentaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, @@ -12,7 +12,7 @@ "title": "Almond a trav\u00e9s del complemento Home Assistant" }, "pick_implementation": { - "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" } } } diff --git a/homeassistant/components/ambiclimate/translations/es.json b/homeassistant/components/ambiclimate/translations/es.json index 1ac7a371f82..d3c5433a289 100644 --- a/homeassistant/components/ambiclimate/translations/es.json +++ b/homeassistant/components/ambiclimate/translations/es.json @@ -3,19 +3,19 @@ "abort": { "access_token": "Error desconocido al generar un token de acceso.", "already_configured": "La cuenta ya est\u00e1 configurada", - "missing_configuration": "El componente no est\u00e1 configurado. Siga la documentaci\u00f3n." + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n." }, "create_entry": { "default": "Autenticado correctamente" }, "error": { - "follow_link": "Accede al enlace e identif\u00edcate antes de pulsar Enviar.", + "follow_link": "Por favor, sigue el enlace y autent\u00edcate antes de pulsar Enviar", "no_token": "No autenticado con Ambiclimate" }, "step": { "auth": { - "description": "Por favor, sigue este [enlace]({authorization_url}) y **Permite** el acceso a tu cuenta Ambiclimate, luego regresa y presiona **Enviar** a continuaci\u00f3n.\n(Aseg\u00farate de que la URL de devoluci\u00f3n de llamada especificada sea {cb_url})", - "title": "Autenticaci\u00f3n de Ambiclimate" + "description": "Por favor, sigue este [enlace]({authorization_url}) y **Permite** el acceso a tu cuenta Ambiclimate, luego regresa y pulsa **Enviar** a continuaci\u00f3n.\n(Aseg\u00farate de que la URL de devoluci\u00f3n de llamada especificada sea {cb_url})", + "title": "Autenticar Ambiclimate" } } } diff --git a/homeassistant/components/android_ip_webcam/translations/es.json b/homeassistant/components/android_ip_webcam/translations/es.json index 71c0d6cc5bc..d004be2aeeb 100644 --- a/homeassistant/components/android_ip_webcam/translations/es.json +++ b/homeassistant/components/android_ip_webcam/translations/es.json @@ -19,7 +19,7 @@ }, "issues": { "deprecated_yaml": { - "description": "Se eliminar\u00e1 la configuraci\u00f3n de la c\u00e1mara web IP de Android mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de la c\u00e1mara web IP de Android de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se eliminar\u00e1 la configuraci\u00f3n de la Android IP Webcam mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Android IP Webcam de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la configuraci\u00f3n YAML de la c\u00e1mara web IP de Android" } } diff --git a/homeassistant/components/android_ip_webcam/translations/it.json b/homeassistant/components/android_ip_webcam/translations/it.json index 7c04ebfdaef..db0cfc79d84 100644 --- a/homeassistant/components/android_ip_webcam/translations/it.json +++ b/homeassistant/components/android_ip_webcam/translations/it.json @@ -16,5 +16,11 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Android IP Webcam tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Android IP Webcam dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Android IP Webcam sar\u00e0 rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/es.json b/homeassistant/components/apple_tv/translations/es.json index 918a95de976..d1654272818 100644 --- a/homeassistant/components/apple_tv/translations/es.json +++ b/homeassistant/components/apple_tv/translations/es.json @@ -22,8 +22,8 @@ "flow_title": "{name} ({type})", "step": { "confirm": { - "description": "Est\u00e1s a punto de a\u00f1adir `{name}` con el tipo `{type}` en Home Assistant.\n\n**Para completar el proceso, puede que tengas que introducir varios c\u00f3digos PIN.**\n\nTen en cuenta que *no* podr\u00e1s apagar tu Apple TV con esta integraci\u00f3n. \u00a1S\u00f3lo se apagar\u00e1 el reproductor de medios de Home Assistant!", - "title": "Confirma la adici\u00f3n del Apple TV" + "description": "Est\u00e1s a punto de a\u00f1adir `{name}` con el tipo `{type}` en Home Assistant.\n\n**Para completar el proceso, puede que tengas que introducir varios c\u00f3digos PIN.**\n\nTen en cuenta que *no* podr\u00e1s apagar tu Apple TV con esta integraci\u00f3n. \u00a1S\u00f3lo se apagar\u00e1 el reproductor de medios en Home Assistant!", + "title": "Confirma para a\u00f1adir Apple TV" }, "pair_no_pin": { "description": "Se requiere emparejamiento para el servicio `{protocol}`. Por favor, introduce el PIN {pin} en tu dispositivo para continuar.", @@ -56,7 +56,7 @@ "data": { "device_input": "Dispositivo" }, - "description": "Comienza introduciendo el nombre del dispositivo (por ejemplo, cocina o dormitorio) o la direcci\u00f3n IP del Apple TV que deseas a\u00f1adir. \n\n Si no puedes ver tu dispositivo o experimentas alg\u00fan problema, intenta especificar la direcci\u00f3n IP del dispositivo.", + "description": "Comienza introduciendo el nombre del dispositivo (por ejemplo, cocina o dormitorio) o la direcci\u00f3n IP del Apple TV que deseas a\u00f1adir. \n\nSi no puedes ver tu dispositivo o experimentas alg\u00fan problema, intenta especificar la direcci\u00f3n IP del dispositivo.", "title": "Configurar un nuevo Apple TV" } } diff --git a/homeassistant/components/asuswrt/translations/es.json b/homeassistant/components/asuswrt/translations/es.json index f20f0faa3eb..57e7bb4cde6 100644 --- a/homeassistant/components/asuswrt/translations/es.json +++ b/homeassistant/components/asuswrt/translations/es.json @@ -7,8 +7,8 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", - "pwd_and_ssh": "S\u00f3lo proporcionar la contrase\u00f1a o el archivo de clave SSH", - "pwd_or_ssh": "Por favor, proporcione la contrase\u00f1a o el archivo de clave SSH", + "pwd_and_ssh": "Proporciona solo la contrase\u00f1a o el archivo de clave SSH", + "pwd_or_ssh": "Por favor, proporciona la contrase\u00f1a o el archivo de clave SSH", "ssh_not_file": "Archivo de clave SSH no encontrado", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/atag/translations/es.json b/homeassistant/components/atag/translations/es.json index c2da8173e22..c1bc879f64d 100644 --- a/homeassistant/components/atag/translations/es.json +++ b/homeassistant/components/atag/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "unauthorized": "Emparejamiento denegado, comprobar el dispositivo para la solicitud de autorizaci\u00f3n" + "unauthorized": "Emparejamiento denegado, verifica el dispositivo para la solicitud de autenticaci\u00f3n" }, "step": { "user": { @@ -13,7 +13,7 @@ "host": "Host", "port": "Puerto" }, - "title": "Conectarse al dispositivo" + "title": "Conectar al dispositivo" } } } diff --git a/homeassistant/components/aurora/translations/es.json b/homeassistant/components/aurora/translations/es.json index c722c95ef6f..c17fadbc0f9 100644 --- a/homeassistant/components/aurora/translations/es.json +++ b/homeassistant/components/aurora/translations/es.json @@ -22,5 +22,5 @@ } } }, - "title": "Sensor Aurora NOAA" + "title": "Sensor NOAA Aurora" } \ No newline at end of file diff --git a/homeassistant/components/awair/translations/hu.json b/homeassistant/components/awair/translations/hu.json index 7ce394ccb4b..eae3a375bdf 100644 --- a/homeassistant/components/awair/translations/hu.json +++ b/homeassistant/components/awair/translations/hu.json @@ -52,7 +52,7 @@ }, "description": "Regisztr\u00e1lnia kell az Awair fejleszt\u0151i hozz\u00e1f\u00e9r\u00e9si tokenj\u00e9hez a k\u00f6vetkez\u0151 c\u00edmen: https://developer.getawair.com/onboard/login", "menu_options": { - "cloud": "Csatlakoz\u00e1s a felh\u0151n kereszt\u00fcl", + "cloud": "Felh\u0151n kereszt\u00fcli csatlakoz\u00e1s", "local": "Lok\u00e1lis csatlakoz\u00e1s (aj\u00e1nlott)" } } diff --git a/homeassistant/components/awair/translations/id.json b/homeassistant/components/awair/translations/id.json index 53c41584413..835f0d7716a 100644 --- a/homeassistant/components/awair/translations/id.json +++ b/homeassistant/components/awair/translations/id.json @@ -50,7 +50,7 @@ "access_token": "Token Akses", "email": "Email" }, - "description": "Anda harus mendaftar untuk mendapatkan token akses pengembang Awair di: https://developer.getawair.com/onboard/login", + "description": "Pilih lokal untuk pengalaman terbaik. Hanya gunakan opsi cloud jika perangkat tidak terhubung ke jaringan yang sama dengan Home Assistant, atau jika Anda memiliki perangkat versi lawas.", "menu_options": { "cloud": "Terhubung melalui cloud", "local": "Terhubung secara lokal (lebih disukai)" diff --git a/homeassistant/components/awair/translations/it.json b/homeassistant/components/awair/translations/it.json index 27ec006fb06..d82934fceb9 100644 --- a/homeassistant/components/awair/translations/it.json +++ b/homeassistant/components/awair/translations/it.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "already_configured_account": "L'account \u00e8 gi\u00e0 configurato", + "already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato", "no_devices_found": "Nessun dispositivo trovato sulla rete", - "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "unreachable": "Impossibile connettersi" }, "error": { "invalid_access_token": "Token di accesso non valido", - "unknown": "Errore imprevisto" + "unknown": "Errore imprevisto", + "unreachable": "Impossibile connettersi" }, + "flow_title": "{model} ( {device_id} )", "step": { + "cloud": { + "data": { + "access_token": "Token di accesso", + "email": "Email" + }, + "description": "Devi registrarti per un token di accesso per sviluppatori Awair su: {url}" + }, + "discovery_confirm": { + "description": "Vuoi configurare {model} ({device_id})?" + }, + "local": { + "data": { + "host": "Indirizzo IP" + }, + "description": "Awair Local API deve essere abilitato seguendo questi passaggi: {url}" + }, "reauth": { "data": { "access_token": "Token di accesso", @@ -29,7 +50,11 @@ "access_token": "Token di accesso", "email": "Email" }, - "description": "\u00c8 necessario registrarsi per un token di accesso per sviluppatori Awair all'indirizzo: https://developer.getawair.com/onboard/login" + "description": "Scegli locale per la migliore esperienza. Utilizza il cloud solo se il dispositivo non \u00e8 connesso alla stessa rete di Home Assistant o se disponi di un dispositivo legacy.", + "menu_options": { + "cloud": "Connettiti tramite il cloud", + "local": "Connetti localmente (preferito)" + } } } } diff --git a/homeassistant/components/axis/translations/es.json b/homeassistant/components/axis/translations/es.json index 87497280775..262228bb2d4 100644 --- a/homeassistant/components/axis/translations/es.json +++ b/homeassistant/components/axis/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "link_local_address": "Las direcciones de enlace local no son compatibles", - "not_axis_device": "El dispositivo descubierto no es un dispositivo de Axis" + "not_axis_device": "El dispositivo descubierto no es un dispositivo Axis" }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", diff --git a/homeassistant/components/azure_devops/translations/es.json b/homeassistant/components/azure_devops/translations/es.json index 2795286e5c7..8414e03a727 100644 --- a/homeassistant/components/azure_devops/translations/es.json +++ b/homeassistant/components/azure_devops/translations/es.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_successful": "Re-autenticaci\u00f3n realizada correctamente" + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "project_error": "No se pudo obtener informaci\u00f3n del proyecto." }, "flow_title": "{project_url}", diff --git a/homeassistant/components/binary_sensor/translations/es.json b/homeassistant/components/binary_sensor/translations/es.json index 4a30bed923d..2d3f7c7d4f7 100644 --- a/homeassistant/components/binary_sensor/translations/es.json +++ b/homeassistant/components/binary_sensor/translations/es.json @@ -1,7 +1,7 @@ { "device_automation": { "condition_type": { - "is_bat_low": "{entity_name} la bater\u00eda est\u00e1 baja", + "is_bat_low": "La bater\u00eda de {entity_name} est\u00e1 baja", "is_co": "{entity_name} est\u00e1 detectando mon\u00f3xido de carbono", "is_cold": "{entity_name} est\u00e1 fr\u00edo", "is_connected": "{entity_name} est\u00e1 conectado", @@ -14,9 +14,9 @@ "is_moving": "{entity_name} se est\u00e1 moviendo", "is_no_co": "{entity_name} no detecta mon\u00f3xido de carbono", "is_no_gas": "{entity_name} no detecta gas", - "is_no_light": "{entity_name} no detecta la luz", + "is_no_light": "{entity_name} no detecta luz", "is_no_motion": "{entity_name} no detecta movimiento", - "is_no_problem": "{entity_name} no detecta el problema", + "is_no_problem": "{entity_name} no detecta problema alguno", "is_no_smoke": "{entity_name} no detecta humo", "is_no_sound": "{entity_name} no detecta sonido", "is_no_update": "{entity_name} est\u00e1 actualizado", @@ -30,7 +30,7 @@ "is_not_moving": "{entity_name} no se mueve", "is_not_occupied": "{entity_name} no est\u00e1 ocupado", "is_not_open": "{entity_name} est\u00e1 cerrado", - "is_not_plugged_in": "{entity_name} est\u00e1 desconectado", + "is_not_plugged_in": "{entity_name} est\u00e1 desenchufado", "is_not_powered": "{entity_name} no tiene alimentaci\u00f3n", "is_not_present": "{entity_name} no est\u00e1 presente", "is_not_running": "{entity_name} no se est\u00e1 ejecutando", @@ -40,8 +40,8 @@ "is_off": "{entity_name} est\u00e1 apagado", "is_on": "{entity_name} est\u00e1 activado", "is_open": "{entity_name} est\u00e1 abierto", - "is_plugged_in": "{entity_name} est\u00e1 conectado", - "is_powered": "{entity_name} est\u00e1 activado", + "is_plugged_in": "{entity_name} est\u00e1 enchufado", + "is_powered": "{entity_name} est\u00e1 alimentado", "is_present": "{entity_name} est\u00e1 presente", "is_problem": "{entity_name} est\u00e1 detectando un problema", "is_running": "{entity_name} se est\u00e1 ejecutando", @@ -50,56 +50,56 @@ "is_tampered": "{entity_name} est\u00e1 detectando manipulaci\u00f3n", "is_unsafe": "{entity_name} no es seguro", "is_update": "{entity_name} tiene una actualizaci\u00f3n disponible", - "is_vibration": "{entity_name} est\u00e1 detectando vibraciones" + "is_vibration": "{entity_name} est\u00e1 detectando vibraci\u00f3n" }, "trigger_type": { - "bat_low": "{entity_name} bater\u00eda baja", + "bat_low": "La bater\u00eda de {entity_name} es baja", "co": "{entity_name} comenz\u00f3 a detectar mon\u00f3xido de carbono", "cold": "{entity_name} se enfri\u00f3", "connected": "{entity_name} conectado", "gas": "{entity_name} empez\u00f3 a detectar gas", - "hot": "{entity_name} se est\u00e1 calentando", - "light": "{entity_name} empez\u00f3 a detectar la luz", + "hot": "{entity_name} se calent\u00f3", + "light": "{entity_name} empez\u00f3 a detectar luz", "locked": "{entity_name} bloqueado", - "moist": "{entity_name} se humedece", - "motion": "{entity_name} comenz\u00f3 a detectar movimiento", + "moist": "{entity_name} se humedeci\u00f3", + "motion": "{entity_name} empez\u00f3 a detectar movimiento", "moving": "{entity_name} empez\u00f3 a moverse", "no_co": "{entity_name} dej\u00f3 de detectar mon\u00f3xido de carbono", "no_gas": "{entity_name} dej\u00f3 de detectar gas", - "no_light": "{entity_name} dej\u00f3 de detectar la luz", + "no_light": "{entity_name} dej\u00f3 de detectar luz", "no_motion": "{entity_name} dej\u00f3 de detectar movimiento", - "no_problem": "{entity_name} dej\u00f3 de detectar el problema", + "no_problem": "{entity_name} dej\u00f3 de detectar alg\u00fan problema", "no_smoke": "{entity_name} dej\u00f3 de detectar humo", "no_sound": "{entity_name} dej\u00f3 de detectar sonido", "no_update": "{entity_name} se actualiz\u00f3", "no_vibration": "{entity_name} dej\u00f3 de detectar vibraci\u00f3n", "not_bat_low": "{entity_name} bater\u00eda normal", - "not_cold": "{entity_name} no se enfri\u00f3", + "not_cold": "{entity_name} dej\u00f3 de estar fr\u00edo", "not_connected": "{entity_name} desconectado", - "not_hot": "{entity_name} no se calent\u00f3", + "not_hot": "{entity_name} dej\u00f3 de estar caliente", "not_locked": "{entity_name} desbloqueado", "not_moist": "{entity_name} se sec\u00f3", "not_moving": "{entity_name} dej\u00f3 de moverse", "not_occupied": "{entity_name} no est\u00e1 ocupado", - "not_opened": "{entity_name} se cierra", - "not_plugged_in": "{entity_name} desconectado", - "not_powered": "{entity_name} no est\u00e1 activado", + "not_opened": "{entity_name} cerrado", + "not_plugged_in": "{entity_name} desenchufado", + "not_powered": "{entity_name} no alimentado", "not_present": "{entity_name} no est\u00e1 presente", "not_running": "{entity_name} ya no se est\u00e1 ejecutando", "not_tampered": "{entity_name} dej\u00f3 de detectar manipulaci\u00f3n", "not_unsafe": "{entity_name} se volvi\u00f3 seguro", - "occupied": "{entity_name} se convirti\u00f3 en ocupado", + "occupied": "{entity_name} se volvi\u00f3 ocupado", "opened": "{entity_name} abierto", - "plugged_in": "{entity_name} se ha enchufado", + "plugged_in": "{entity_name} enchufado", "powered": "{entity_name} alimentado", "present": "{entity_name} presente", - "problem": "{entity_name} empez\u00f3 a detectar problemas", + "problem": "{entity_name} empez\u00f3 a detectar un problema", "running": "{entity_name} comenz\u00f3 a ejecutarse", "smoke": "{entity_name} empez\u00f3 a detectar humo", "sound": "{entity_name} empez\u00f3 a detectar sonido", "tampered": "{entity_name} comenz\u00f3 a detectar manipulaci\u00f3n", - "turned_off": "{entity_name} desactivado", - "turned_on": "{entity_name} activado", + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido", "unsafe": "{entity_name} se volvi\u00f3 inseguro", "update": "{entity_name} tiene una actualizaci\u00f3n disponible", "vibration": "{entity_name} empez\u00f3 a detectar vibraciones" diff --git a/homeassistant/components/blink/translations/es.json b/homeassistant/components/blink/translations/es.json index 72f68ab4fff..17c724102eb 100644 --- a/homeassistant/components/blink/translations/es.json +++ b/homeassistant/components/blink/translations/es.json @@ -14,7 +14,7 @@ "data": { "2fa": "C\u00f3digo de dos factores" }, - "description": "Introduce el PIN enviado a su correo electr\u00f3nico", + "description": "Introduce el PIN enviado a tu correo electr\u00f3nico", "title": "Autenticaci\u00f3n de dos factores" }, "user": { diff --git a/homeassistant/components/bmw_connected_drive/translations/es.json b/homeassistant/components/bmw_connected_drive/translations/es.json index 2a4fd84f708..7d5c21534cb 100644 --- a/homeassistant/components/bmw_connected_drive/translations/es.json +++ b/homeassistant/components/bmw_connected_drive/translations/es.json @@ -21,7 +21,7 @@ "step": { "account_options": { "data": { - "read_only": "S\u00f3lo lectura (s\u00f3lo sensores y notificaci\u00f3n, sin ejecuci\u00f3n de servicios, sin bloqueo)" + "read_only": "S\u00f3lo lectura (s\u00f3lo sensores y notificaci\u00f3n, sin ejecuci\u00f3n de servicios, ni cerraduras)" } } } diff --git a/homeassistant/components/bond/translations/es.json b/homeassistant/components/bond/translations/es.json index 33d3dbb4408..d426fca5862 100644 --- a/homeassistant/components/bond/translations/es.json +++ b/homeassistant/components/bond/translations/es.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "old_firmware": "Firmware antiguo no compatible en el dispositivo Bond - actual\u00edzalo antes de continuar", + "old_firmware": "Firmware antiguo no compatible en el dispositivo Bond - por favor, actualiza antes de continuar", "unknown": "Error inesperado" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/braviatv/translations/es.json b/homeassistant/components/braviatv/translations/es.json index 118125f63e0..86436249717 100644 --- a/homeassistant/components/braviatv/translations/es.json +++ b/homeassistant/components/braviatv/translations/es.json @@ -2,20 +2,20 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "no_ip_control": "El Control de IP est\u00e1 desactivado en tu televisor o el televisor no es compatible." + "no_ip_control": "El Control IP est\u00e1 desactivado en tu TV o la TV no es compatible." }, "error": { "cannot_connect": "No se pudo conectar", "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", - "unsupported_model": "Tu modelo de televisor no es compatible." + "unsupported_model": "Tu modelo de TV no es compatible." }, "step": { "authorize": { "data": { "pin": "C\u00f3digo PIN" }, - "description": "Introduce el c\u00f3digo PIN que se muestra en el televisor Sony Bravia.\n\nSi no se muestra ning\u00fan c\u00f3digo PIN, necesitas eliminar el registro de Home Assistant de tu televisor, ve a: Configuraci\u00f3n -> Red -> Configuraci\u00f3n del dispositivo remoto -> Eliminar el dispositivo remoto.", - "title": "Autorizaci\u00f3n del televisor Sony Bravia" + "description": "Introduce el c\u00f3digo PIN que se muestra en Sony Bravia TV. \n\nSi no se muestra el c\u00f3digo PIN, debes cancelar el registro de Home Assistant en tu TV, ve a: Configuraci\u00f3n - > Red - > Configuraci\u00f3n del dispositivo remoto - > Cancelar el registro del dispositivo remoto.", + "title": "Autorizar Sony Bravia TV" }, "user": { "data": { @@ -31,7 +31,7 @@ "data": { "ignored_sources": "Lista de fuentes ignoradas" }, - "title": "Opciones para el televisor Sony Bravia" + "title": "Opciones para Sony Bravia TV" } } } diff --git a/homeassistant/components/broadlink/translations/es.json b/homeassistant/components/broadlink/translations/es.json index 97a71e2cf61..540675cd646 100644 --- a/homeassistant/components/broadlink/translations/es.json +++ b/homeassistant/components/broadlink/translations/es.json @@ -40,7 +40,7 @@ "host": "Host", "timeout": "L\u00edmite de tiempo" }, - "title": "Conectarse al dispositivo" + "title": "Conectar al dispositivo" } } } diff --git a/homeassistant/components/brother/translations/es.json b/homeassistant/components/brother/translations/es.json index 921f2806f0e..e56d922425e 100644 --- a/homeassistant/components/brother/translations/es.json +++ b/homeassistant/components/brother/translations/es.json @@ -22,7 +22,7 @@ "type": "Tipo de impresora" }, "description": "\u00bfQuieres a\u00f1adir la impresora {model} con n\u00famero de serie `{serial_number}` a Home Assistant?", - "title": "Impresora Brother encontrada" + "title": "Impresora Brother descubierta" } } } diff --git a/homeassistant/components/bsblan/translations/es.json b/homeassistant/components/bsblan/translations/es.json index 1e1c29d24a4..0bf6d3bad9d 100644 --- a/homeassistant/components/bsblan/translations/es.json +++ b/homeassistant/components/bsblan/translations/es.json @@ -12,7 +12,7 @@ "user": { "data": { "host": "Host", - "passkey": "Clave de acceso", + "passkey": "Cadena de clave de acceso", "password": "Contrase\u00f1a", "port": "Puerto", "username": "Nombre de usuario" diff --git a/homeassistant/components/button/translations/es.json b/homeassistant/components/button/translations/es.json index 69430e83f1b..7fd58a87159 100644 --- a/homeassistant/components/button/translations/es.json +++ b/homeassistant/components/button/translations/es.json @@ -4,7 +4,7 @@ "press": "Presiona el bot\u00f3n {entity_name}" }, "trigger_type": { - "pressed": "{entity_name} se ha presionado" + "pressed": "{entity_name} se ha pulsado" } }, "title": "Bot\u00f3n" diff --git a/homeassistant/components/cert_expiry/translations/es.json b/homeassistant/components/cert_expiry/translations/es.json index 1eaee01060f..9cad0193bb7 100644 --- a/homeassistant/components/cert_expiry/translations/es.json +++ b/homeassistant/components/cert_expiry/translations/es.json @@ -16,7 +16,7 @@ "name": "El nombre del certificado", "port": "Puerto" }, - "title": "Defina el certificado para probar" + "title": "Definir el certificado a probar" } } }, diff --git a/homeassistant/components/climate/translations/es.json b/homeassistant/components/climate/translations/es.json index 09806ddf5ef..bf7e37c71ff 100644 --- a/homeassistant/components/climate/translations/es.json +++ b/homeassistant/components/climate/translations/es.json @@ -2,22 +2,22 @@ "device_automation": { "action_type": { "set_hvac_mode": "Cambiar el modo HVAC de {entity_name}.", - "set_preset_mode": "Cambiar la configuraci\u00f3n prefijada de {entity_name}" + "set_preset_mode": "Cambiar la configuraci\u00f3n preestablecida de {entity_name}" }, "condition_type": { "is_hvac_mode": "{entity_name} est\u00e1 configurado en un modo HVAC espec\u00edfico", - "is_preset_mode": "{entity_name} se establece en un modo predeterminado espec\u00edfico" + "is_preset_mode": "{entity_name} se establece en un modo preestablecido espec\u00edfico" }, "trigger_type": { - "current_humidity_changed": "{entity_name} humedad medida cambi\u00f3", - "current_temperature_changed": "{entity_name} temperatura medida cambi\u00f3", - "hvac_mode_changed": "{entity_name} Modo HVAC cambiado" + "current_humidity_changed": "{entity_name} cambi\u00f3 la humedad medida", + "current_temperature_changed": "{entity_name} cambi\u00f3 la temperatura medida", + "hvac_mode_changed": "{entity_name} cambi\u00f3 el modo HVAC" } }, "state": { "_": { "auto": "Autom\u00e1tico", - "cool": "Enfr\u00eda", + "cool": "Fr\u00edo", "dry": "Seco", "fan_only": "Solo ventilador", "heat": "Calor", diff --git a/homeassistant/components/cloud/translations/es.json b/homeassistant/components/cloud/translations/es.json index f81c71e8292..7eddc5c4109 100644 --- a/homeassistant/components/cloud/translations/es.json +++ b/homeassistant/components/cloud/translations/es.json @@ -2,11 +2,11 @@ "system_health": { "info": { "alexa_enabled": "Alexa habilitada", - "can_reach_cert_server": "Servidor de Certificados accesible", - "can_reach_cloud": "Home Assistant Cloud accesible", - "can_reach_cloud_auth": "Servidor de Autenticaci\u00f3n accesible", + "can_reach_cert_server": "Se llega al Servidor de Certificados", + "can_reach_cloud": "Se llega a Home Assistant Cloud", + "can_reach_cloud_auth": "Se llega al Servidor de Autenticaci\u00f3n", "google_enabled": "Google habilitado", - "logged_in": "Iniciada sesi\u00f3n", + "logged_in": "Sesi\u00f3n iniciada", "relayer_connected": "Relayer conectado", "remote_connected": "Remoto conectado", "remote_enabled": "Remoto habilitado", diff --git a/homeassistant/components/cloudflare/translations/es.json b/homeassistant/components/cloudflare/translations/es.json index d3ea7a842c5..d47711bf0a5 100644 --- a/homeassistant/components/cloudflare/translations/es.json +++ b/homeassistant/components/cloudflare/translations/es.json @@ -2,12 +2,12 @@ "config": { "abort": { "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", - "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n.", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "unknown": "Error inesperado" }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_zone": "Zona no v\u00e1lida" }, "flow_title": "{name}", @@ -22,20 +22,20 @@ "data": { "records": "Registros" }, - "title": "Elija los registros que desea actualizar" + "title": "Elige los registros para actualizar" }, "user": { "data": { - "api_token": "Token de la API" + "api_token": "Token API" }, - "description": "Esta integraci\u00f3n requiere un token de API creado con los permisos Zone:Zone:Read y Zone:DNS:Edit para todas las zonas de su cuenta.", + "description": "Esta integraci\u00f3n requiere un token de API creado con los permisos Zone:Zone:Read y Zone:DNS:Edit para todas las zonas de tu cuenta.", "title": "Conectar con Cloudflare" }, "zone": { "data": { "zone": "Zona" }, - "title": "Elija la zona para actualizar" + "title": "Elige la Zona a Actualizar" } } } diff --git a/homeassistant/components/control4/translations/es.json b/homeassistant/components/control4/translations/es.json index c5d49e30680..900f87206e5 100644 --- a/homeassistant/components/control4/translations/es.json +++ b/homeassistant/components/control4/translations/es.json @@ -4,8 +4,8 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fallo al conectar", - "invalid_auth": "Autentificaci\u00f3n invalida", + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { @@ -13,9 +13,9 @@ "data": { "host": "Direcci\u00f3n IP", "password": "Contrase\u00f1a", - "username": "Usuario" + "username": "Volver a autenticar la integraci\u00f3n" }, - "description": "Por favor, introduzca su cuenta de Control4 y la direcci\u00f3n IP de su controlador local." + "description": "Por favor, introduce los detalles de tu cuenta Control4 y la direcci\u00f3n IP de tu controlador local." } } }, diff --git a/homeassistant/components/coolmaster/translations/es.json b/homeassistant/components/coolmaster/translations/es.json index a2c830756bd..14e9179f122 100644 --- a/homeassistant/components/coolmaster/translations/es.json +++ b/homeassistant/components/coolmaster/translations/es.json @@ -2,20 +2,20 @@ "config": { "error": { "cannot_connect": "No se pudo conectar", - "no_units": "No se ha encontrado ninguna unidad HVAC en el host CoolMasterNet." + "no_units": "No se pudo encontrar ninguna unidad HVAC en el host CoolMasterNet." }, "step": { "user": { "data": { - "cool": "Soporta el modo de enfriamiento", - "dry": "Soporta el modo seco", - "fan_only": "Soporta modo solo ventilador", - "heat": "Soporta modo calor", - "heat_cool": "Soporta el modo autom\u00e1tico de calor/fr\u00edo", + "cool": "Admite modo fr\u00edo", + "dry": "Admite modo seco", + "fan_only": "Admite modo solo ventilador", + "heat": "Admite modo de calor", + "heat_cool": "Admite modo autom\u00e1tico de calor/fr\u00edo", "host": "Host", "off": "Se puede apagar" }, - "title": "Configure los detalles de su conexi\u00f3n a CoolMasterNet." + "title": "Configura los detalles de tu conexi\u00f3n CoolMasterNet." } } } diff --git a/homeassistant/components/coronavirus/translations/es.json b/homeassistant/components/coronavirus/translations/es.json index 160bdc219a6..d6e05a3a560 100644 --- a/homeassistant/components/coronavirus/translations/es.json +++ b/homeassistant/components/coronavirus/translations/es.json @@ -9,7 +9,7 @@ "data": { "country": "Pa\u00eds" }, - "title": "Elige un pa\u00eds para monitorizar" + "title": "Elige un pa\u00eds para supervisar" } } } diff --git a/homeassistant/components/cover/translations/es.json b/homeassistant/components/cover/translations/es.json index 550c9d368e9..708c4a2dc7d 100644 --- a/homeassistant/components/cover/translations/es.json +++ b/homeassistant/components/cover/translations/es.json @@ -19,11 +19,11 @@ }, "trigger_type": { "closed": "{entity_name} cerrado", - "closing": "{entity_name} cerrando", - "opened": "abierto {entity_name}", - "opening": "abriendo {entity_name}", - "position": "Posici\u00f3n cambiada de {entity_name}", - "tilt_position": "Cambia la posici\u00f3n de inclinaci\u00f3n de {entity_name}" + "closing": "{entity_name} cerr\u00e1ndose", + "opened": "{entity_name} abierto", + "opening": "{entity_name} abri\u00e9ndose", + "position": "{entity_name} cambi\u00f3 de posici\u00f3n", + "tilt_position": "{entity_name} cambi\u00f3 de posici\u00f3n de inclinaci\u00f3n" } }, "state": { diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index 6308bf7fed8..4e0d9ee96fc 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -14,11 +14,11 @@ "flow_title": "{host}", "step": { "hassio_confirm": { - "description": "\u00bfQuieres configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento {addon} ?", + "description": "\u00bfQuieres configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento {addon}?", "title": "Puerta de enlace Zigbee deCONZ a trav\u00e9s del complemento Home Assistant" }, "link": { - "description": "Desbloquea tu puerta de enlace deCONZ para registrarlo con Home Assistant. \n\n 1. Ve a Configuraci\u00f3n deCONZ -> Puerta de enlace -> Avanzado\n 2. Presiona el bot\u00f3n \"Autenticar aplicaci\u00f3n\"", + "description": "Desbloquea tu puerta de enlace deCONZ para registrarlo con Home Assistant. \n\n 1. Ve a Configuraci\u00f3n deCONZ -> Puerta de enlace -> Avanzado\n 2. Pulsa el bot\u00f3n \"Autenticar aplicaci\u00f3n\"", "title": "Vincular con deCONZ" }, "manual_input": { @@ -29,7 +29,7 @@ }, "user": { "data": { - "host": "Seleccione la puerta de enlace descubierta deCONZ" + "host": "Selecciona la puerta de enlace deCONZ descubierta" } } } @@ -64,18 +64,18 @@ }, "trigger_type": { "remote_awakened": "Dispositivo despertado", - "remote_button_double_press": "Bot\u00f3n \"{subtype}\" doble pulsaci\u00f3n", + "remote_button_double_press": "Bot\u00f3n \"{subtype}\" pulsado dos veces", "remote_button_long_press": "Bot\u00f3n \"{subtype}\" pulsado continuamente", "remote_button_long_release": "Bot\u00f3n \"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", - "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" cu\u00e1druple pulsaci\u00f3n", - "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" qu\u00edntuple pulsaci\u00f3n", + "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces", + "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces", "remote_button_rotated": "Bot\u00f3n \"{subtype}\" girado", "remote_button_rotated_fast": "Bot\u00f3n \"{subtype}\" girado r\u00e1pido", "remote_button_rotation_stopped": "Se detuvo la rotaci\u00f3n del bot\u00f3n \"{subtype}\"", "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", "remote_button_short_release": "Bot\u00f3n \"{subtype}\" soltado", - "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" triple pulsaci\u00f3n", - "remote_double_tap": "Dispositivo \" {subtype} \" doble pulsaci\u00f3n", + "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado tres veces", + "remote_double_tap": "Doble toque en dispositivo \"{subtype}\"", "remote_double_tap_any_side": "Dispositivo con doble toque en cualquier lado", "remote_falling": "Dispositivo en ca\u00edda libre", "remote_flip_180_degrees": "Dispositivo volteado 180 grados", @@ -83,12 +83,12 @@ "remote_gyro_activated": "Dispositivo sacudido", "remote_moved": "Dispositivo movido con \"{subtype}\" hacia arriba", "remote_moved_any_side": "Dispositivo movido con cualquier lado hacia arriba", - "remote_rotate_from_side_1": "Dispositivo girado del \"lado 1\" al \" {subtype} \"", - "remote_rotate_from_side_2": "Dispositivo girado del \"lado 2\" al \" {subtype} \"", - "remote_rotate_from_side_3": "Dispositivo girado del \"lado 3\" al \" {subtype} \"", - "remote_rotate_from_side_4": "Dispositivo girado del \"lado 4\" al \" {subtype} \"", - "remote_rotate_from_side_5": "Dispositivo girado del \"lado 5\" al \" {subtype} \"", - "remote_rotate_from_side_6": "Dispositivo girado de \"lado 6\" a \" {subtype} \"", + "remote_rotate_from_side_1": "Dispositivo girado desde \"lado 1\" a \"{subtype}\"", + "remote_rotate_from_side_2": "Dispositivo girado desde \"lado 2\" a \"{subtype}\"", + "remote_rotate_from_side_3": "Dispositivo girado desde \"lado 3\" a \"{subtype}\"", + "remote_rotate_from_side_4": "Dispositivo girado desde \"lado 4\" a \"{subtype}\"", + "remote_rotate_from_side_5": "Dispositivo girado desde \"lado 5\" a \"{subtype}\"", + "remote_rotate_from_side_6": "Dispositivo girado desde \"lado 6\" a \"{subtype}\"", "remote_turned_clockwise": "Dispositivo girado en el sentido de las agujas del reloj", "remote_turned_counter_clockwise": "Dispositivo girado en sentido contrario a las agujas del reloj" } @@ -101,7 +101,7 @@ "allow_deconz_groups": "Permitir grupos de luz deCONZ", "allow_new_devices": "Permitir a\u00f1adir autom\u00e1ticamente nuevos dispositivos" }, - "description": "Configurar la visibilidad de los tipos de dispositivos deCONZ", + "description": "Configura la visibilidad de los tipos de dispositivos deCONZ", "title": "Opciones deCONZ" } } diff --git a/homeassistant/components/demo/translations/es.json b/homeassistant/components/demo/translations/es.json index ba1e31265b9..3fd439a1b05 100644 --- a/homeassistant/components/demo/translations/es.json +++ b/homeassistant/components/demo/translations/es.json @@ -42,7 +42,7 @@ }, "options_2": { "data": { - "multi": "Multiselecci\u00f3n", + "multi": "Selecci\u00f3n m\u00faltiple", "select": "Selecciona una opci\u00f3n", "string": "Valor de cadena" } diff --git a/homeassistant/components/denonavr/translations/es.json b/homeassistant/components/denonavr/translations/es.json index 55936dee05d..cf645e78ce4 100644 --- a/homeassistant/components/denonavr/translations/es.json +++ b/homeassistant/components/denonavr/translations/es.json @@ -3,17 +3,17 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "cannot_connect": "No se pudo conectar, int\u00e9ntelo de nuevo, desconectar los cables de alimentaci\u00f3n y Ethernet y volver a conectarlos puede ayudar", - "not_denonavr_manufacturer": "No es un receptor de red Denon AVR, el fabricante descubierto no coincide", - "not_denonavr_missing": "No es un Receptor AVR Denon AVR en Red, la informaci\u00f3n detectada no est\u00e1 completa" + "cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo. Desconectar los cables de la alimentaci\u00f3n el\u00e9ctrica y ethernet y volvi\u00e9ndolos a conectar puede ayudar", + "not_denonavr_manufacturer": "No es un Denon AVR Network Receiver, el fabricante descubierto no coincide", + "not_denonavr_missing": "No es un Denon AVR Network Receiver, la informaci\u00f3n de descubrimiento no est\u00e1 completa" }, "error": { - "discovery_error": "Error detectando un Receptor AVR Denon en Red" + "discovery_error": "No se pudo detectar ning\u00fan Denon AVR Network Receiver" }, "flow_title": "{name}", "step": { "confirm": { - "description": "Por favor confirma la adici\u00f3n del receptor" + "description": "Por favor, confirma la adici\u00f3n del receptor" }, "select": { "data": { diff --git a/homeassistant/components/deutsche_bahn/translations/it.json b/homeassistant/components/deutsche_bahn/translations/it.json index d390f84ff0a..22449ef7a17 100644 --- a/homeassistant/components/deutsche_bahn/translations/it.json +++ b/homeassistant/components/deutsche_bahn/translations/it.json @@ -1,6 +1,7 @@ { "issues": { "pending_removal": { + "description": "L'integrazione Deutsce Bahn \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.11. \n\nL'integrazione sar\u00e0 rimossa, perch\u00e9 si basa sul webscraping, che non \u00e8 consentito. \n\nRimuovi la configurazione YAML di Deutsce Bahn dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", "title": "L'integrazione Deutsche Bahn sar\u00e0 rimossa" } } diff --git a/homeassistant/components/dexcom/translations/es.json b/homeassistant/components/dexcom/translations/es.json index 58b4a014f50..0aefcd1c7f9 100644 --- a/homeassistant/components/dexcom/translations/es.json +++ b/homeassistant/components/dexcom/translations/es.json @@ -16,7 +16,7 @@ "username": "Nombre de usuario" }, "description": "Introducir las credenciales de Dexcom Share", - "title": "Configurar integraci\u00f3n de Dexcom" + "title": "Configurar la integraci\u00f3n Dexcom" } } }, diff --git a/homeassistant/components/directv/translations/es.json b/homeassistant/components/directv/translations/es.json index 2a268ac2148..81206472829 100644 --- a/homeassistant/components/directv/translations/es.json +++ b/homeassistant/components/directv/translations/es.json @@ -5,7 +5,7 @@ "unknown": "Error inesperado" }, "error": { - "cannot_connect": "Error al conectar" + "cannot_connect": "No se pudo conectar" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/doorbird/translations/es.json b/homeassistant/components/doorbird/translations/es.json index 5aefd6eb3af..509b6e7d8c7 100644 --- a/homeassistant/components/doorbird/translations/es.json +++ b/homeassistant/components/doorbird/translations/es.json @@ -6,7 +6,7 @@ "not_doorbird_device": "Este dispositivo no es un DoorBird" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/eafm/translations/es.json b/homeassistant/components/eafm/translations/es.json index 01dca9b3af3..8e38ac23e0a 100644 --- a/homeassistant/components/eafm/translations/es.json +++ b/homeassistant/components/eafm/translations/es.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "no_stations": "No se encontraron estaciones de monitoreo de inundaciones." + "no_stations": "No se encontraron estaciones de supervisi\u00f3n de inundaciones." }, "step": { "user": { "data": { "station": "Estaci\u00f3n" }, - "description": "Seleccione la estaci\u00f3n que desea monitorear", - "title": "Rastrear una estaci\u00f3n de monitoreo de inundaciones" + "description": "Selecciona la estaci\u00f3n que deseas supervisar", + "title": "Seguimiento de una estaci\u00f3n de supervisi\u00f3n de inundaciones" } } } diff --git a/homeassistant/components/ecobee/translations/es.json b/homeassistant/components/ecobee/translations/es.json index a7fdcfb2116..8b003c12e5f 100644 --- a/homeassistant/components/ecobee/translations/es.json +++ b/homeassistant/components/ecobee/translations/es.json @@ -4,19 +4,19 @@ "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { - "pin_request_failed": "Error al solicitar el PIN de ecobee; verifique que la clave API sea correcta.", - "token_request_failed": "Error al solicitar tokens de ecobee; Int\u00e9ntalo de nuevo." + "pin_request_failed": "Error al solicitar PIN de ecobee; por favor, verifica que la clave API sea correcta.", + "token_request_failed": "Error al solicitar tokens de ecobee; por favor, int\u00e9ntalo de nuevo." }, "step": { "authorize": { - "description": "Por favor, autoriza esta aplicaci\u00f3n en https://www.ecobee.com/consumerportal/index.html con el c\u00f3digo PIN: \n\n{pin}\n\nLuego, presiona Enviar.", + "description": "Por favor, autoriza esta aplicaci\u00f3n en https://www.ecobee.com/consumerportal/index.html con el c\u00f3digo PIN: \n\n{pin}\n\nLuego, pulsa Enviar.", "title": "Autorizar aplicaci\u00f3n en ecobee.com" }, "user": { "data": { "api_key": "Clave API" }, - "description": "Introduzca la clave de API obtenida de ecobee.com.", + "description": "Por favor, introduce la clave API obtenida de ecobee.com.", "title": "Clave API de ecobee" } } diff --git a/homeassistant/components/elgato/translations/es.json b/homeassistant/components/elgato/translations/es.json index eb0d7fde9b4..adef400e613 100644 --- a/homeassistant/components/elgato/translations/es.json +++ b/homeassistant/components/elgato/translations/es.json @@ -14,10 +14,10 @@ "host": "Host", "port": "Puerto" }, - "description": "Configura la integraci\u00f3n de Elgato Light con Home Assistant." + "description": "Configura tu Elgato Light para que se integre con Home Assistant." }, "zeroconf_confirm": { - "description": "\u00bfQuieres a\u00f1adir el Key Light de Elgato con n\u00famero de serie `{serial_number}` a Home Assistant?", + "description": "\u00bfQuieres a\u00f1adir el Light de Elgato con n\u00famero de serie `{serial_number}` a Home Assistant?", "title": "Dispositivo Elgato Light descubierto" } } diff --git a/homeassistant/components/enocean/translations/es.json b/homeassistant/components/enocean/translations/es.json index 2bcc1074a23..4a141857cc9 100644 --- a/homeassistant/components/enocean/translations/es.json +++ b/homeassistant/components/enocean/translations/es.json @@ -1,24 +1,24 @@ { "config": { "abort": { - "invalid_dongle_path": "Ruta a mochila no v\u00e1lida", + "invalid_dongle_path": "Ruta al dongle no v\u00e1lida", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { - "invalid_dongle_path": "No se ha encontrado ninguna mochila v\u00e1lida en esta ruta" + "invalid_dongle_path": "No se ha encontrado ning\u00fan dongle en esta ruta" }, "step": { "detect": { "data": { - "path": "Ruta a mochila USB" + "path": "Ruta al dongle USB" }, - "title": "Selecciona la ruta a su mochila ENOcean" + "title": "Selecciona la ruta a tu dongle ENOcean" }, "manual": { "data": { - "path": "Ruta a mochila USB" + "path": "Ruta al dongle USB" }, - "title": "Introduce la ruta a tu mochila ENOcean" + "title": "Introduce la ruta a tu dongle ENOcean" } } } diff --git a/homeassistant/components/escea/translations/it.json b/homeassistant/components/escea/translations/it.json index 047fc1d0ff1..9f21d9b917c 100644 --- a/homeassistant/components/escea/translations/it.json +++ b/homeassistant/components/escea/translations/it.json @@ -3,6 +3,11 @@ "abort": { "no_devices_found": "Nessun dispositivo trovato sulla rete", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "step": { + "confirm": { + "description": "Vuoi configurare un caminetto Escea?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/fan/translations/es.json b/homeassistant/components/fan/translations/es.json index 15486b23a48..45efee6d4ce 100644 --- a/homeassistant/components/fan/translations/es.json +++ b/homeassistant/components/fan/translations/es.json @@ -2,17 +2,17 @@ "device_automation": { "action_type": { "toggle": "Alternar {entity_name}", - "turn_off": "Desactivar {entity_name}", - "turn_on": "Activar {entity_name}" + "turn_off": "Apagar {entity_name}", + "turn_on": "Encender {entity_name}" }, "condition_type": { - "is_off": "{entity_name} est\u00e1 desactivado", - "is_on": "{entity_name} est\u00e1 activado" + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 encendido" }, "trigger_type": { "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", - "turned_off": "{entity_name} desactivado", - "turned_on": "{entity_name} activado" + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido" } }, "state": { diff --git a/homeassistant/components/flick_electric/translations/es.json b/homeassistant/components/flick_electric/translations/es.json index 10b77911016..01415c08678 100644 --- a/homeassistant/components/flick_electric/translations/es.json +++ b/homeassistant/components/flick_electric/translations/es.json @@ -11,8 +11,8 @@ "step": { "user": { "data": { - "client_id": "ID de cliente (Opcional)", - "client_secret": "Secreto de Cliente (Opcional)", + "client_id": "ID de cliente (opcional)", + "client_secret": "Secreto de Cliente (opcional)", "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, diff --git a/homeassistant/components/flo/translations/es.json b/homeassistant/components/flo/translations/es.json index 9267e363693..c1d3e57b02f 100644 --- a/homeassistant/components/flo/translations/es.json +++ b/homeassistant/components/flo/translations/es.json @@ -4,8 +4,8 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fallo al conectar", - "invalid_auth": "Autentificaci\u00f3n no valida", + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/flume/translations/es.json b/homeassistant/components/flume/translations/es.json index 12152dc0211..5de43c8dcd1 100644 --- a/homeassistant/components/flume/translations/es.json +++ b/homeassistant/components/flume/translations/es.json @@ -25,7 +25,7 @@ "username": "Nombre de usuario" }, "description": "Para acceder a la API Personal de Flume, tendr\u00e1s que solicitar un 'Client ID' y un 'Client Secret' en https://portal.flumetech.com/settings#token", - "title": "Conectar con tu cuenta de Flume" + "title": "Conectar con tu cuenta Flume" } } } diff --git a/homeassistant/components/forked_daapd/translations/es.json b/homeassistant/components/forked_daapd/translations/es.json index af47865dd66..2e6c986a61e 100644 --- a/homeassistant/components/forked_daapd/translations/es.json +++ b/homeassistant/components/forked_daapd/translations/es.json @@ -5,10 +5,10 @@ "not_forked_daapd": "El dispositivo no es un servidor forked-daapd." }, "error": { - "forbidden": "No se puede conectar. Compruebe los permisos de red de bifurcaci\u00f3n.", + "forbidden": "No se puede conectar. Comprueba los permisos de tu forked-daapd.", "unknown_error": "Error inesperado", - "websocket_not_enabled": "Websocket no activado en servidor forked-daapd.", - "wrong_host_or_port": "No se ha podido conectar. Por favor comprueba host y puerto.", + "websocket_not_enabled": "Websocket del servidor forked-daapd no habilitado.", + "wrong_host_or_port": "No se ha podido conectar. Por favor, comprueba host y puerto.", "wrong_password": "Contrase\u00f1a incorrecta.", "wrong_server_type": "La integraci\u00f3n forked-daapd requiere un servidor forked-daapd con versi\u00f3n >= 27.0." }, @@ -18,7 +18,7 @@ "data": { "host": "Host", "name": "Nombre amigable", - "password": "Contrase\u00f1a API (dejar en blanco si no hay contrase\u00f1a)", + "password": "Contrase\u00f1a API (d\u00e9jala en blanco si no hay contrase\u00f1a)", "port": "Puerto API" }, "title": "Configurar dispositivo forked-daapd" @@ -34,8 +34,8 @@ "tts_pause_time": "Segundos para pausar antes y despu\u00e9s del TTS", "tts_volume": "Volumen TTS (decimal en el rango [0,1])" }, - "description": "Ajustar varias opciones para la integraci\u00f3n de forked-daapd", - "title": "Configurar opciones para forked-daapd" + "description": "Establece varias opciones para la integraci\u00f3n de forked-daapd.", + "title": "Configurar opciones de forked-daapd" } } } diff --git a/homeassistant/components/freebox/translations/es.json b/homeassistant/components/freebox/translations/es.json index 9cae8cc2089..d7b0a6b492d 100644 --- a/homeassistant/components/freebox/translations/es.json +++ b/homeassistant/components/freebox/translations/es.json @@ -4,13 +4,13 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", - "register_failed": "No se pudo registrar, int\u00e9ntalo de nuevo", + "cannot_connect": "No se pudo conectar", + "register_failed": "No se pudo registrar, por favor, int\u00e9ntalo de nuevo", "unknown": "Error inesperado" }, "step": { "link": { - "description": "Pulsa \"Enviar\", despu\u00e9s pulsa en la flecha derecha en el router para registrar Freebox con Home Assistant\n\n![Localizaci\u00f3n del bot\u00f3n en el router](/static/images/config_freebox.png)", + "description": "Haz clic en \"Enviar\", luego toca la flecha derecha en el router para registrar Freebox con Home Assistant. \n\n![Ubicaci\u00f3n del bot\u00f3n en el enrutador](/static/images/config_freebox.png)", "title": "Vincular router Freebox" }, "user": { diff --git a/homeassistant/components/fritzbox/translations/es.json b/homeassistant/components/fritzbox/translations/es.json index 71e5a2f5cf1..ed8184d958a 100644 --- a/homeassistant/components/fritzbox/translations/es.json +++ b/homeassistant/components/fritzbox/translations/es.json @@ -25,7 +25,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Actualice la informaci\u00f3n de inicio de sesi\u00f3n para {name}." + "description": "Actualiza tu informaci\u00f3n de inicio de sesi\u00f3n para {name}." }, "user": { "data": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/es.json b/homeassistant/components/fritzbox_callmonitor/translations/es.json index 3be51f3bb6d..8e7dbbc1b8e 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/es.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "insufficient_permissions": "El usuario no tiene permisos suficientes para acceder a la configuraci\u00f3n de AVM FRITZ! Box y sus agendas telef\u00f3nicas.", + "insufficient_permissions": "El usuario no tiene suficientes permisos para acceder a la configuraci\u00f3n de AVM FRITZ!Box y sus agendas telef\u00f3nicas.", "no_devices_found": "No se encontraron dispositivos en la red" }, "error": { @@ -12,7 +12,7 @@ "step": { "phonebook": { "data": { - "phonebook": "Directorio telef\u00f3nico" + "phonebook": "Agenda telef\u00f3nica" } }, "user": { @@ -27,7 +27,7 @@ }, "options": { "error": { - "malformed_prefixes": "Los prefijos tienen un formato incorrecto, comprueba el formato." + "malformed_prefixes": "Los prefijos tienen un formato incorrecto, por favor, verifica el formato." }, "step": { "init": { diff --git a/homeassistant/components/gdacs/translations/es.json b/homeassistant/components/gdacs/translations/es.json index 9b7c2686d6f..f0c1ec6a2d1 100644 --- a/homeassistant/components/gdacs/translations/es.json +++ b/homeassistant/components/gdacs/translations/es.json @@ -8,7 +8,7 @@ "data": { "radius": "Radio" }, - "title": "Rellena los datos de tu filtro." + "title": "Completa los detalles de tu filtro." } } } diff --git a/homeassistant/components/geocaching/translations/es.json b/homeassistant/components/geocaching/translations/es.json index 712f554ac19..14b534271f9 100644 --- a/homeassistant/components/geocaching/translations/es.json +++ b/homeassistant/components/geocaching/translations/es.json @@ -3,8 +3,8 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" diff --git a/homeassistant/components/geonetnz_quakes/translations/es.json b/homeassistant/components/geonetnz_quakes/translations/es.json index 371dc967a82..90d2486b270 100644 --- a/homeassistant/components/geonetnz_quakes/translations/es.json +++ b/homeassistant/components/geonetnz_quakes/translations/es.json @@ -9,7 +9,7 @@ "mmi": "MMI", "radius": "Radio" }, - "title": "Complete todos los campos requeridos" + "title": "Completa los detalles de tu filtro." } } } diff --git a/homeassistant/components/geonetnz_volcano/translations/es.json b/homeassistant/components/geonetnz_volcano/translations/es.json index 1621a42eb4a..109eaf73729 100644 --- a/homeassistant/components/geonetnz_volcano/translations/es.json +++ b/homeassistant/components/geonetnz_volcano/translations/es.json @@ -8,7 +8,7 @@ "data": { "radius": "Radio" }, - "title": "Complete los detalles de su filtro." + "title": "Completa los detalles de tu filtro." } } } diff --git a/homeassistant/components/glances/translations/es.json b/homeassistant/components/glances/translations/es.json index 01f2a132136..22187e65793 100644 --- a/homeassistant/components/glances/translations/es.json +++ b/homeassistant/components/glances/translations/es.json @@ -17,7 +17,7 @@ "ssl": "Utiliza un certificado SSL", "username": "Nombre de usuario", "verify_ssl": "Verificar el certificado SSL", - "version": "Versi\u00f3n API Glances (2 o 3)" + "version": "Versi\u00f3n API de Glances (2 o 3)" }, "title": "Configurar Glances" } diff --git a/homeassistant/components/google/translations/es.json b/homeassistant/components/google/translations/es.json index eaf84a11931..be94117d1bd 100644 --- a/homeassistant/components/google/translations/es.json +++ b/homeassistant/components/google/translations/es.json @@ -9,7 +9,7 @@ "cannot_connect": "No se pudo conectar", "code_expired": "El c\u00f3digo de autenticaci\u00f3n caduc\u00f3 o la configuraci\u00f3n de la credencial no es v\u00e1lida, por favor, int\u00e9ntalo de nuevo.", "invalid_access_token": "Token de acceso no v\u00e1lido", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n" diff --git a/homeassistant/components/guardian/translations/es.json b/homeassistant/components/guardian/translations/es.json index c918a3fe583..a7f54c0a726 100644 --- a/homeassistant/components/guardian/translations/es.json +++ b/homeassistant/components/guardian/translations/es.json @@ -23,7 +23,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con una ID de entidad de destino de `{alternate_target}`. Luego, haz clic en ENVIAR a continuaci\u00f3n para marcar este problema como resuelto.", + "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con un ID de entidad de destino de `{alternate_target}`. A continuaci\u00f3n, haz clic en ENVIAR m\u00e1s abajo para marcar este problema como resuelto.", "title": "El servicio {deprecated_service} ser\u00e1 eliminado" } } diff --git a/homeassistant/components/guardian/translations/hu.json b/homeassistant/components/guardian/translations/hu.json index 25a0abb5526..35787e95524 100644 --- a/homeassistant/components/guardian/translations/hu.json +++ b/homeassistant/components/guardian/translations/hu.json @@ -23,12 +23,12 @@ "fix_flow": { "step": { "confirm": { - "description": "Friss\u00edtsen minden olyan automatiz\u00e1l\u00e1st vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, hogy helyette az `{alternate_service}` szolg\u00e1ltat\u00e1st haszn\u00e1lja a `{alternate_target}` entit\u00e1ssal. Ezut\u00e1n kattintson az al\u00e1bbi MEHET gombra a probl\u00e9ma megoldottk\u00e9nt val\u00f3 megjel\u00f6l\u00e9s\u00e9hez.", - "title": "A {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + "description": "Friss\u00edtsen minden olyan automatiz\u00e1l\u00e1st vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, hogy helyette a(z) `{alternate_service}` szolg\u00e1ltat\u00e1st haszn\u00e1lja a(z) `{alternate_target}` entit\u00e1ssal. Ezut\u00e1n kattintson az al\u00e1bbi MEHET gombra a probl\u00e9ma megoldottk\u00e9nt val\u00f3 megjel\u00f6l\u00e9s\u00e9hez.", + "title": "{deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } } }, - "title": "A {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + "title": "{deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/it.json b/homeassistant/components/guardian/translations/it.json index e6450dae21d..4f43ace1b71 100644 --- a/homeassistant/components/guardian/translations/it.json +++ b/homeassistant/components/guardian/translations/it.json @@ -17,5 +17,18 @@ "description": "Configura un dispositivo Elexa Guardian locale." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aggiorna tutte le automazioni o gli script che utilizzano questo servizio e utilizzare invece il servizio `{alternate_service}` con un ID entit\u00e0 di destinazione di `{alternate_target}`. Quindi, fai clic su INVIA di seguito per contrassegnare questo problema come risolto.", + "title": "Il servizio {deprecated_service} verr\u00e0 rimosso" + } + } + }, + "title": "Il servizio {deprecated_service} verr\u00e0 rimosso" + } } } \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/es.json b/homeassistant/components/harmony/translations/es.json index 527cd8433e8..3acc9abb07e 100644 --- a/homeassistant/components/harmony/translations/es.json +++ b/homeassistant/components/harmony/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "unknown": "Error inesperado" }, "flow_title": "{name}", diff --git a/homeassistant/components/hassio/translations/es.json b/homeassistant/components/hassio/translations/es.json index 4a6fda89d84..2d88b0d2252 100644 --- a/homeassistant/components/hassio/translations/es.json +++ b/homeassistant/components/hassio/translations/es.json @@ -7,7 +7,7 @@ "disk_used": "Disco usado", "docker_version": "Versi\u00f3n de Docker", "healthy": "Saludable", - "host_os": "Sistema operativo del Host", + "host_os": "Sistema Operativo del Host", "installed_addons": "Complementos instalados", "supervisor_api": "API del Supervisor", "supervisor_version": "Versi\u00f3n del Supervisor", diff --git a/homeassistant/components/home_connect/translations/es.json b/homeassistant/components/home_connect/translations/es.json index 9ee5769583b..a476741a8b1 100644 --- a/homeassistant/components/home_connect/translations/es.json +++ b/homeassistant/components/home_connect/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "missing_configuration": "El componente no est\u00e1 configurado. Mira su documentaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" }, "create_entry": { diff --git a/homeassistant/components/home_plus_control/translations/es.json b/homeassistant/components/home_plus_control/translations/es.json index ff4e671fb65..4796a9e1236 100644 --- a/homeassistant/components/home_plus_control/translations/es.json +++ b/homeassistant/components/home_plus_control/translations/es.json @@ -3,8 +3,8 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, diff --git a/homeassistant/components/homeassistant/translations/es.json b/homeassistant/components/homeassistant/translations/es.json index db63c818f61..5b447f7177f 100644 --- a/homeassistant/components/homeassistant/translations/es.json +++ b/homeassistant/components/homeassistant/translations/es.json @@ -7,7 +7,7 @@ "docker": "Docker", "hassio": "Supervisor", "installation_type": "Tipo de instalaci\u00f3n", - "os_name": "Familia del sistema operativo", + "os_name": "Familia de Sistema Operativo", "os_version": "Versi\u00f3n del Sistema Operativo", "python_version": "Versi\u00f3n de Python", "timezone": "Zona horaria", diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index 1813b96dd7a..3bca7c0f253 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "port_name_in_use": "Ya existe un enlace o accesorio configurado con ese nombre o puerto." + "port_name_in_use": "Ya est\u00e1 configurado un accesorio o puente con el mismo nombre o puerto." }, "step": { "pairing": { - "description": "Para completar el emparejamiento, sigue las instrucciones en \"Notificaciones\" en \"Emparejamiento HomeKit\".", - "title": "Vinculaci\u00f3n HomeKit" + "description": "Para completar el emparejamiento, sigue las instrucciones en \"Notificaciones\" bajo \"Emparejamiento HomeKit\".", + "title": "Emparejar HomeKit" }, "user": { "data": { @@ -38,7 +38,7 @@ "camera_copy": "C\u00e1maras compatibles con transmisiones H.264 nativas" }, "description": "Verifica todas las c\u00e1maras que admitan transmisiones H.264 nativas. Si la c\u00e1mara no emite una transmisi\u00f3n H.264, el sistema transcodificar\u00e1 el video a H.264 para HomeKit. La transcodificaci\u00f3n requiere una CPU de alto rendimiento y es poco probable que funcione en ordenadores de placa \u00fanica.", - "title": "Configuraci\u00f3n de la c\u00e1mara" + "title": "Configuraci\u00f3n de C\u00e1mara" }, "exclude": { "data": { @@ -58,10 +58,10 @@ "data": { "domains": "Dominios para incluir", "include_exclude_mode": "Modo de inclusi\u00f3n", - "mode": "Mode de HomeKit" + "mode": "Modo de HomeKit" }, "description": "HomeKit se puede configurar para exponer un puente o un solo accesorio. En el modo accesorio, solo se puede usar una sola entidad. El modo accesorio es necesario para que los reproductores multimedia con la clase de dispositivo TV funcionen correctamente. Las entidades en los \"Dominios para incluir\" se incluir\u00e1n en HomeKit. Podr\u00e1s seleccionar qu\u00e9 entidades incluir o excluir de esta lista en la siguiente pantalla.", - "title": "Selecciona el modo y dominios." + "title": "Selecciona el modo y los dominios." }, "yaml": { "description": "Esta entrada se controla a trav\u00e9s de YAML", diff --git a/homeassistant/components/homekit_controller/translations/es.json b/homeassistant/components/homekit_controller/translations/es.json index b446c87e70f..6185c432007 100644 --- a/homeassistant/components/homekit_controller/translations/es.json +++ b/homeassistant/components/homekit_controller/translations/es.json @@ -5,7 +5,7 @@ "already_configured": "El accesorio ya est\u00e1 configurado con este controlador.", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicia el accesorio e int\u00e9ntalo de nuevo.", - "ignored_model": "El soporte de HomeKit para este modelo est\u00e1 bloqueado ya que est\u00e1 disponible una integraci\u00f3n nativa m\u00e1s completa.", + "ignored_model": "El soporte HomeKit para este modelo est\u00e1 bloqueado ya que est\u00e1 disponible una integraci\u00f3n nativa m\u00e1s completa.", "invalid_config_entry": "Este dispositivo se muestra como listo para emparejarse, pero ya hay una entrada de configuraci\u00f3n conflictiva para \u00e9l en Home Assistant que primero debe eliminarse.", "invalid_properties": "Propiedades no v\u00e1lidas anunciadas por dispositivo.", "no_devices": "No se encontraron dispositivos no emparejados" @@ -13,20 +13,20 @@ "error": { "authentication_error": "C\u00f3digo de HomeKit incorrecto. Por favor, rev\u00edsalo e int\u00e9ntalo de nuevo.", "insecure_setup_code": "El c\u00f3digo de configuraci\u00f3n solicitado no es seguro debido a su naturaleza trivial. Este accesorio no cumple con los requisitos b\u00e1sicos de seguridad.", - "max_peers_error": "El dispositivo rechaz\u00f3 el emparejamiento ya que no tiene almacenamiento de emparejamientos libres.", + "max_peers_error": "El dispositivo se neg\u00f3 a a\u00f1adir el emparejamiento porque no tiene almacenamiento disponible para emparejamientos.", "pairing_failed": "Se produjo un error no controlado al intentar emparejar con este dispositivo. Esto puede ser un fallo temporal o que tu dispositivo no sea compatible actualmente.", - "unable_to_pair": "No se puede emparejar, int\u00e9ntalo de nuevo.", + "unable_to_pair": "No se puede emparejar, por favor, int\u00e9ntalo de nuevo.", "unknown_error": "El dispositivo report\u00f3 un error desconocido. El emparejamiento ha fallado." }, "flow_title": "{name} ({category})", "step": { "busy_error": { - "description": "Interrumpe el emparejamiento en todos los controladores o intenta reiniciar el dispositivo y luego contin\u00faa con el emparejamiento.", + "description": "Cancela el emparejamiento en todos los controladores, o intenta reiniciar el dispositivo, luego contin\u00faa con el emparejamiento.", "title": "El dispositivo ya est\u00e1 emparejando con otro controlador" }, "max_tries_error": { - "description": "El dispositivo ha recibido m\u00e1s de 100 intentos de autenticaci\u00f3n fallidos. Intenta reiniciar el dispositivo, luego contin\u00faa para reanudar el emparejamiento.", - "title": "M\u00e1ximo n\u00famero de intentos de autenticaci\u00f3n superados" + "description": "El dispositivo ha recibido m\u00e1s de 100 intentos de autenticaci\u00f3n fallidos. Intenta reiniciar el dispositivo, luego contin\u00faa con el emparejamiento.", + "title": "Se excedieron los intentos m\u00e1ximos de autenticaci\u00f3n" }, "pair": { "data": { @@ -37,8 +37,8 @@ "title": "Emparejar con un dispositivo a trav\u00e9s del protocolo de accesorios HomeKit" }, "protocol_error": { - "description": "Es posible que el dispositivo no est\u00e9 en modo de emparejamiento y que requiera que se presione un bot\u00f3n f\u00edsico o virtual. Aseg\u00farate de que el dispositivo est\u00e1 en modo de emparejamiento o intenta reiniciar el dispositivo, luego contin\u00faa para reanudar el emparejamiento.", - "title": "Error al comunicarse con el accesorio" + "description": "Es posible que el dispositivo no est\u00e9 en modo de emparejamiento y que se requiera pulsar un bot\u00f3n f\u00edsico o virtual. Aseg\u00farate de que el dispositivo est\u00e9 en modo de emparejamiento o intenta reiniciar el dispositivo, luego contin\u00faa con el emparejamiento.", + "title": "Error de comunicaci\u00f3n con el accesorio" }, "user": { "data": { diff --git a/homeassistant/components/homekit_controller/translations/sensor.it.json b/homeassistant/components/homekit_controller/translations/sensor.it.json new file mode 100644 index 00000000000..acfff28f0b2 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/sensor.it.json @@ -0,0 +1,10 @@ +{ + "state": { + "homekit_controller__thread_node_capabilities": { + "none": "Nessuna" + }, + "homekit_controller__thread_status": { + "disabled": "Disabilitato" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/es.json b/homeassistant/components/huawei_lte/translations/es.json index 9bef5d09eec..03b159989f2 100644 --- a/homeassistant/components/huawei_lte/translations/es.json +++ b/homeassistant/components/huawei_lte/translations/es.json @@ -9,7 +9,7 @@ "incorrect_username": "Nombre de usuario incorrecto", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_url": "URL no v\u00e1lida", - "login_attempts_exceeded": "Se han superado los intentos de inicio de sesi\u00f3n m\u00e1ximos, int\u00e9ntelo de nuevo m\u00e1s tarde.", + "login_attempts_exceeded": "Se han superado los intentos de inicio de sesi\u00f3n m\u00e1ximos, por favor, int\u00e9ntalo de nuevo m\u00e1s tarde.", "response_error": "Error desconocido del dispositivo", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/hue/translations/es.json b/homeassistant/components/hue/translations/es.json index 456668fec88..e49438144b7 100644 --- a/homeassistant/components/hue/translations/es.json +++ b/homeassistant/components/hue/translations/es.json @@ -8,7 +8,7 @@ "discover_timeout": "Imposible encontrar pasarelas Philips Hue", "invalid_host": "Host inv\u00e1lido", "no_bridges": "No se han encontrado pasarelas Philips Hue.", - "not_hue_bridge": "No es un enlace Hue", + "not_hue_bridge": "No es un puente Hue", "unknown": "Error inesperado" }, "error": { @@ -53,7 +53,7 @@ }, "trigger_type": { "double_short_release": "Ambos \"{subtype}\" soltados", - "initial_press": "Bot\u00f3n \"{subtype}\" presionado inicialmente", + "initial_press": "Bot\u00f3n \"{subtype}\" pulsado inicialmente", "long_release": "Bot\u00f3n \"{subtype}\" soltado tras una pulsaci\u00f3n larga", "remote_button_long_release": "Bot\u00f3n \"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", @@ -68,7 +68,7 @@ "step": { "init": { "data": { - "allow_hue_groups": "Permitir grupos de Hue", + "allow_hue_groups": "Permitir grupos Hue", "allow_hue_scenes": "Permitir escenas Hue", "allow_unreachable": "Permitir que las bombillas inalcanzables informen su estado correctamente", "ignore_availability": "Ignorar el estado de conectividad de los siguientes dispositivos" diff --git a/homeassistant/components/humidifier/translations/es.json b/homeassistant/components/humidifier/translations/es.json index 944361ef35d..7a03cf901fc 100644 --- a/homeassistant/components/humidifier/translations/es.json +++ b/homeassistant/components/humidifier/translations/es.json @@ -10,13 +10,13 @@ "condition_type": { "is_mode": "{entity_name} est\u00e1 configurado en un modo espec\u00edfico", "is_off": "{entity_name} est\u00e1 apagado", - "is_on": "{entity_name} est\u00e1 activado" + "is_on": "{entity_name} est\u00e1 encendido" }, "trigger_type": { "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", - "target_humidity_changed": "La humedad objetivo ha cambiado en {entity_name}", - "turned_off": "{entity_name} desactivado", - "turned_on": "{entity_name} activado" + "target_humidity_changed": "{entity_name} cambi\u00f3 su humedad objetivo", + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido" } }, "state": { diff --git a/homeassistant/components/hunterdouglas_powerview/translations/es.json b/homeassistant/components/hunterdouglas_powerview/translations/es.json index c0d2ad2e4e5..7e880c09441 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/es.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/es.json @@ -11,13 +11,13 @@ "step": { "link": { "description": "\u00bfQuieres configurar {name} ({host})?", - "title": "Conectar con el PowerView Hub" + "title": "Conectar al PowerView Hub" }, "user": { "data": { "host": "Direcci\u00f3n IP" }, - "title": "Conectar con el PowerView Hub" + "title": "Conectar al PowerView Hub" } } } diff --git a/homeassistant/components/hvv_departures/translations/es.json b/homeassistant/components/hvv_departures/translations/es.json index 8cfa90d6367..38a929c3575 100644 --- a/homeassistant/components/hvv_departures/translations/es.json +++ b/homeassistant/components/hvv_departures/translations/es.json @@ -11,13 +11,13 @@ "step": { "station": { "data": { - "station": "Estacion/Direccion" + "station": "Estaci\u00f3n/Direcci\u00f3n" }, - "title": "Introducir Estaci\u00f3n/Direcci\u00f3n" + "title": "Introduce la Estaci\u00f3n/Direcci\u00f3n" }, "station_select": { "data": { - "station": "Estacion/Direccion" + "station": "Estaci\u00f3n/Direcci\u00f3n" }, "title": "Seleccionar Estaci\u00f3n/Direcci\u00f3n" }, @@ -27,7 +27,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "title": "Conectar con el API de HVV" + "title": "Conectar con la API de HVV" } } }, @@ -36,7 +36,7 @@ "init": { "data": { "filter": "Seleccionar l\u00edneas", - "offset": "Desfase (minutos)", + "offset": "Compensaci\u00f3n (minutos)", "real_time": "Usar datos en tiempo real" }, "description": "Cambiar opciones para este sensor de salidas", diff --git a/homeassistant/components/hyperion/translations/es.json b/homeassistant/components/hyperion/translations/es.json index 45e41859c81..91143ec96a8 100644 --- a/homeassistant/components/hyperion/translations/es.json +++ b/homeassistant/components/hyperion/translations/es.json @@ -3,11 +3,11 @@ "abort": { "already_configured": "El servicio ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "auth_new_token_not_granted_error": "El token reci\u00e9n creado no se aprob\u00f3 en la interfaz de usuario de Hyperion", - "auth_new_token_not_work_error": "Error al autenticarse con el token reci\u00e9n creado", + "auth_new_token_not_granted_error": "El token reci\u00e9n creado no se aprob\u00f3 en la IU de Hyperion", + "auth_new_token_not_work_error": "No se pudo autenticar usando un token reci\u00e9n creado", "auth_required_error": "No se pudo determinar si se requiere autorizaci\u00f3n", "cannot_connect": "No se pudo conectar", - "no_id": "La instancia de Hyperion Ambilight no inform\u00f3 su identificaci\u00f3n", + "no_id": "La instancia de Hyperion Ambilight no inform\u00f3 su id", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { @@ -18,20 +18,20 @@ "auth": { "data": { "create_token": "Crea un nuevo token autom\u00e1ticamente", - "token": "O proporcionar un token preexistente" + "token": "O proporciona un token preexistente" }, - "description": "Configurar autorizaci\u00f3n a tu servidor Hyperion Ambilight" + "description": "Configura la autorizaci\u00f3n a tu servidor Hyperion Ambilight" }, "confirm": { - "description": "\u00bfQuieres a\u00f1adir el siguiente Ambilight de Hyperion a Home Assistant?\n\n**Host:** {host}\n**Puerto:** {port}\n**ID**: {id}", - "title": "Confirmar la adici\u00f3n del servicio Hyperion Ambilight" + "description": "\u00bfQuieres a\u00f1adir el siguiente Hyperion Ambilight a Home Assistant?\n\n**Host:** {host}\n**Puerto:** {port}\n**ID**: {id}", + "title": "Confirmar para a\u00f1adir el servicio Hyperion Ambilight" }, "create_token": { - "description": "Elige ** Enviar ** a continuaci\u00f3n para solicitar un nuevo token de autenticaci\u00f3n. Se te redirigir\u00e1 a la interfaz de usuario de Hyperion para aprobar la solicitud. Verifica que la identificaci\u00f3n que se muestra sea \"{auth_id}\"", + "description": "Elige **Enviar** a continuaci\u00f3n para solicitar un nuevo token de autenticaci\u00f3n. Ser\u00e1s redirigido a la IU de Hyperion para aprobar la solicitud. Verifica que la identificaci\u00f3n que se muestra sea \"{auth_id}\"", "title": "Crear autom\u00e1ticamente un nuevo token de autenticaci\u00f3n" }, "create_token_external": { - "title": "Aceptar nuevo token en la interfaz de usuario de Hyperion" + "title": "Aceptar nuevo token en la IU de Hyperion" }, "user": { "data": { diff --git a/homeassistant/components/iaqualink/translations/es.json b/homeassistant/components/iaqualink/translations/es.json index 60f26b1c64d..967d52b7319 100644 --- a/homeassistant/components/iaqualink/translations/es.json +++ b/homeassistant/components/iaqualink/translations/es.json @@ -14,7 +14,7 @@ "username": "Nombre de usuario" }, "description": "Por favor, introduce el nombre de usuario y contrase\u00f1a de tu cuenta iAqualink.", - "title": "Conexi\u00f3n con iAqualink" + "title": "Conectar a iAqualink" } } } diff --git a/homeassistant/components/icloud/translations/es.json b/homeassistant/components/icloud/translations/es.json index 9140c843483..e5e6a8927a9 100644 --- a/homeassistant/components/icloud/translations/es.json +++ b/homeassistant/components/icloud/translations/es.json @@ -7,7 +7,7 @@ }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "send_verification_code": "Error al enviar el c\u00f3digo de verificaci\u00f3n", + "send_verification_code": "No se pudo enviar el c\u00f3digo de verificaci\u00f3n", "validate_verification_code": "No se ha podido verificar el c\u00f3digo de verificaci\u00f3n, vuelve a intentarlo" }, "step": { @@ -22,7 +22,7 @@ "data": { "trusted_device": "Dispositivo de confianza" }, - "description": "Seleccione su dispositivo de confianza", + "description": "Selecciona tu dispositivo de confianza", "title": "Dispositivo de confianza iCloud" }, "user": { @@ -31,14 +31,14 @@ "username": "Correo electr\u00f3nico", "with_family": "Con la familia" }, - "description": "Ingrese sus credenciales", + "description": "Introduce tus credenciales", "title": "Credenciales iCloud" }, "verification_code": { "data": { "verification_code": "C\u00f3digo de verificaci\u00f3n" }, - "description": "Por favor, introduzca el c\u00f3digo de verificaci\u00f3n que acaba de recibir de iCloud", + "description": "Por favor, introduce el c\u00f3digo de verificaci\u00f3n que acabas de recibir de iCloud", "title": "C\u00f3digo de verificaci\u00f3n de iCloud" } } diff --git a/homeassistant/components/input_datetime/translations/es.json b/homeassistant/components/input_datetime/translations/es.json index 570c7b17592..e7824db9ac1 100644 --- a/homeassistant/components/input_datetime/translations/es.json +++ b/homeassistant/components/input_datetime/translations/es.json @@ -1,3 +1,3 @@ { - "title": "Entrada de data i hora" + "title": "Entrada de fecha y hora" } \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/es.json b/homeassistant/components/insteon/translations/es.json index 730277dd5c0..8dbe09d635c 100644 --- a/homeassistant/components/insteon/translations/es.json +++ b/homeassistant/components/insteon/translations/es.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "not_insteon_device": "El dispositivo descubierto no es un dispositivo Insteon", - "single_instance_allowed": "Ya esta configurado. Solo es posible una \u00fanica configuraci\u00f3n." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { "cannot_connect": "No se pudo conectar", - "select_single": "Seleccione una opci\u00f3n." + "select_single": "Selecciona una opci\u00f3n." }, "flow_title": "{name}", "step": { @@ -19,7 +19,7 @@ "host": "Direcci\u00f3n IP", "port": "Puerto" }, - "description": "Configure el Insteon Hub Versi\u00f3n 1 (anterior a 2014).", + "description": "Configura el Insteon Hub Versi\u00f3n 1 (anterior a 2014).", "title": "Insteon Hub Versi\u00f3n 1" }, "hubv2": { @@ -29,29 +29,29 @@ "port": "Puerto", "username": "Nombre de usuario" }, - "description": "Configure el Insteon Hub versi\u00f3n 2.", + "description": "Configura el Insteon Hub Versi\u00f3n 2.", "title": "Insteon Hub Versi\u00f3n 2" }, "plm": { "data": { "device": "Ruta del dispositivo USB" }, - "description": "Configura el M\u00f3dem Insteon PowerLink (PLM).", + "description": "Configura el Insteon PowerLink Modem (PLM).", "title": "Insteon PLM" }, "user": { "data": { "modem_type": "Tipo de m\u00f3dem." }, - "description": "Seleccione el tipo de m\u00f3dem Insteon." + "description": "Selecciona el tipo de m\u00f3dem Insteon." } } }, "options": { "error": { "cannot_connect": "No se pudo conectar", - "input_error": "Entradas no v\u00e1lidas, compruebe sus valores.", - "select_single": "Selecciona una opci\u00f3n" + "input_error": "Entradas no v\u00e1lidas, por favor, verifica tus valores.", + "select_single": "Selecciona una opci\u00f3n." }, "step": { "add_override": { @@ -60,16 +60,16 @@ "cat": "Categor\u00eda del dispositivo (es decir, 0x10)", "subcat": "Subcategor\u00eda del dispositivo (es decir, 0x0a)" }, - "description": "Agregue una anulaci\u00f3n del dispositivo." + "description": "A\u00f1ade una anulaci\u00f3n de dispositivo." }, "add_x10": { "data": { - "housecode": "C\u00f3digo de casa (a - p)", + "housecode": "Housecode (a - p)", "platform": "Plataforma", "steps": "Pasos de atenuaci\u00f3n (s\u00f3lo para dispositivos de luz, por defecto 22)", "unitcode": "Unitcode (1 - 16)" }, - "description": "Cambie la contrase\u00f1a del Hub Insteon." + "description": "Cambia la contrase\u00f1a del Hub Insteon." }, "change_hub_config": { "data": { @@ -78,26 +78,26 @@ "port": "Puerto", "username": "Nombre de usuario" }, - "description": "Cambiar la informaci\u00f3n de la conexi\u00f3n del Hub Insteon. Debes reiniciar el Home Assistant despu\u00e9s de hacer este cambio. Esto no cambia la configuraci\u00f3n del Hub en s\u00ed. Para cambiar la configuraci\u00f3n del Hub usa la aplicaci\u00f3n Hub." + "description": "Cambia la informaci\u00f3n de conexi\u00f3n del Hub Insteon. Debes reiniciar Home Assistant despu\u00e9s de realizar este cambio. Esto no cambia la configuraci\u00f3n del Hub en s\u00ed. Para cambiar la configuraci\u00f3n en el Hub, usa la aplicaci\u00f3n Hub." }, "init": { "data": { - "add_override": "Agregue una anulaci\u00f3n del dispositivo.", + "add_override": "A\u00f1ade una anulaci\u00f3n de dispositivo.", "add_x10": "A\u00f1ade un dispositivo X10.", - "change_hub_config": "Cambie la configuraci\u00f3n del Hub.", - "remove_override": "Eliminar una anulaci\u00f3n del dispositivo.", - "remove_x10": "Eliminar un dispositivo X10" + "change_hub_config": "Cambia la configuraci\u00f3n del Hub.", + "remove_override": "Eliminar una anulaci\u00f3n de dispositivo.", + "remove_x10": "Eliminar un dispositivo X10." } }, "remove_override": { "data": { - "address": "Seleccione una direcci\u00f3n del dispositivo para eliminar" + "address": "Selecciona una direcci\u00f3n de dispositivo para eliminar" }, - "description": "Eliminar una anulaci\u00f3n del dispositivo" + "description": "Elimina una anulaci\u00f3n de dispositivo" }, "remove_x10": { "data": { - "address": "Seleccione la direcci\u00f3n del dispositivo para eliminar" + "address": "Selecciona una direcci\u00f3n de dispositivo para eliminar" }, "description": "Eliminar un dispositivo X10" } diff --git a/homeassistant/components/ipma/translations/es.json b/homeassistant/components/ipma/translations/es.json index d942608ad87..089a58903b4 100644 --- a/homeassistant/components/ipma/translations/es.json +++ b/homeassistant/components/ipma/translations/es.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "El nombre ya existe" + "name_exists": "Nombre ya existe" }, "step": { "user": { @@ -18,7 +18,7 @@ }, "system_health": { "info": { - "api_endpoint_reachable": "Se puede acceder al punto de conexi\u00f3n de la API IPMA" + "api_endpoint_reachable": "Se puede llegar al punto de conexi\u00f3n de la API IPMA" } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/es.json b/homeassistant/components/ipp/translations/es.json index a5a067b5ffd..845ba0b2b7b 100644 --- a/homeassistant/components/ipp/translations/es.json +++ b/homeassistant/components/ipp/translations/es.json @@ -4,9 +4,9 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", "connection_upgrade": "No se pudo conectar con la impresora debido a que se requiere una actualizaci\u00f3n de la conexi\u00f3n.", - "ipp_error": "Error IPP encontrado.", - "ipp_version_error": "Versi\u00f3n de IPP no compatible con la impresora.", - "parse_error": "Error al analizar la respuesta de la impresora.", + "ipp_error": "Se encontr\u00f3 un error de IPP.", + "ipp_version_error": "La versi\u00f3n de IPP no es compatible con la impresora.", + "parse_error": "No se pudo analizar la respuesta de la impresora.", "unique_id_required": "El dispositivo no tiene identificaci\u00f3n \u00fanica necesaria para el descubrimiento." }, "error": { @@ -28,7 +28,7 @@ }, "zeroconf_confirm": { "description": "\u00bfQuieres configurar {name}?", - "title": "Impresora encontrada" + "title": "Impresora descubierta" } } } diff --git a/homeassistant/components/iqvia/translations/es.json b/homeassistant/components/iqvia/translations/es.json index dc26ca6c065..b864075d56f 100644 --- a/homeassistant/components/iqvia/translations/es.json +++ b/homeassistant/components/iqvia/translations/es.json @@ -11,7 +11,7 @@ "data": { "zip_code": "C\u00f3digo postal" }, - "description": "Indica tu c\u00f3digo postal de Estados Unidos o Canad\u00e1." + "description": "Completa tu c\u00f3digo postal de EE UU. o Canad\u00e1." } } } diff --git a/homeassistant/components/isy994/translations/es.json b/homeassistant/components/isy994/translations/es.json index 4c3c4475743..0890cf8e251 100644 --- a/homeassistant/components/isy994/translations/es.json +++ b/homeassistant/components/isy994/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Error al conectar", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_host": "La entrada del host no estaba en formato URL completo, por ejemplo, http://192.168.10.100:80", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", @@ -28,7 +28,7 @@ "username": "Nombre de usuario" }, "description": "La entrada del host debe estar en formato URL completo, por ejemplo, http://192.168.10.100:80", - "title": "Conexi\u00f3n con ISY" + "title": "Conectar con tu ISY" } } }, @@ -37,7 +37,7 @@ "init": { "data": { "ignore_string": "Ignorar Cadena", - "restore_light_state": "Restaurar Intensidad de la Luz", + "restore_light_state": "Restaurar el brillo de la luz", "sensor_string": "Cadena Nodo Sensor", "variable_sensor_string": "Cadena de Sensor Variable" }, diff --git a/homeassistant/components/keenetic_ndms2/translations/es.json b/homeassistant/components/keenetic_ndms2/translations/es.json index 84e39aed3c5..15b266d4f39 100644 --- a/homeassistant/components/keenetic_ndms2/translations/es.json +++ b/homeassistant/components/keenetic_ndms2/translations/es.json @@ -30,7 +30,7 @@ "include_associated": "Usar datos de asociaciones de puntos de acceso WiFi (se ignoran si se usan datos de puntos de acceso)", "interfaces": "Elige las interfaces para escanear", "scan_interval": "Intervalo de escaneo", - "try_hotspot": "Usar los datos de 'ip hotspot' (m\u00e1s precisos)" + "try_hotspot": "Usar los datos de 'ip hotspot' (m\u00e1s preciso)" } } } diff --git a/homeassistant/components/kodi/translations/es.json b/homeassistant/components/kodi/translations/es.json index 0b7b37a6e11..5ea61e0e0c9 100644 --- a/homeassistant/components/kodi/translations/es.json +++ b/homeassistant/components/kodi/translations/es.json @@ -3,13 +3,13 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autentificacion invalida", - "no_uuid": "La instancia de Kodi no tiene un identificador \u00fanico. Esto probablemente es debido a una versi\u00f3n antigua Kodi (17.x o inferior). Puedes configurar la integraci\u00f3n manualmente o actualizar a una versi\u00f3n m\u00e1s reciente de Kodi.", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "no_uuid": "La instancia de Kodi no tiene un identificador \u00fanico. Lo m\u00e1s probable es que se deba a una versi\u00f3n antigua de Kodi (17.x o inferior). Puedes configurar la integraci\u00f3n manualmente o actualizar a una versi\u00f3n m\u00e1s reciente de Kodi.", "unknown": "Error inesperado" }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "flow_title": "{name}", @@ -19,7 +19,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Por favor, introduzca su nombre de usuario y contrase\u00f1a de Kodi. Estos se pueden encontrar en Sistema/Configuraci\u00f3n/Red/Servicios." + "description": "Por favor, introduce tu nombre de usuario y contrase\u00f1a de Kodi. Estos se pueden encontrar en Sistema/Configuraci\u00f3n/Red/Servicios." }, "discovery_confirm": { "description": "\u00bfQuieres a\u00f1adir Kodi (`{name}`) a Home Assistant?", @@ -31,7 +31,7 @@ "port": "Puerto", "ssl": "Utiliza un certificado SSL" }, - "description": "Informaci\u00f3n de la conexi\u00f3n de Kodi. Por favor, aseg\u00farese de habilitar \"Permitir el control de Kodi v\u00eda HTTP\" en Sistema/Configuraci\u00f3n/Red/Servicios." + "description": "Informaci\u00f3n de conexi\u00f3n de Kodi. Aseg\u00farate de habilitar \"Permitir el control de Kodi a trav\u00e9s de HTTP\" en Sistema/Configuraci\u00f3n/Red/Servicios." }, "ws_port": { "data": { diff --git a/homeassistant/components/konnected/translations/es.json b/homeassistant/components/konnected/translations/es.json index 7ba726e8e6a..47a4d1dc5b4 100644 --- a/homeassistant/components/konnected/translations/es.json +++ b/homeassistant/components/konnected/translations/es.json @@ -12,7 +12,7 @@ }, "step": { "confirm": { - "description": "Modelo: {model}\nID: {id}\nHost: {host}\nPuerto: {port}\n\nPuede configurar las E/S y el comportamiento del panel en los ajustes del Panel de Alarmas Konnected.", + "description": "Modelo: {model}\nID: {id}\nHost: {host}\nPuerto: {port}\n\nPuedes configurar la E/S y el comportamiento del panel en los ajustes del Panel de Alarmas Konnected.", "title": "Dispositivo Konnected Listo" }, "import_confirm": { @@ -24,7 +24,7 @@ "host": "Direcci\u00f3n IP", "port": "Puerto" }, - "description": "Introduzca la informaci\u00f3n del host de su panel Konnected." + "description": "Por favor, introduce la informaci\u00f3n del host para tu Konnected Panel." } } }, @@ -33,17 +33,17 @@ "not_konn_panel": "No es un dispositivo Konnected.io reconocido" }, "error": { - "bad_host": "La URL de sustituci\u00f3n del host de la API es inv\u00e1lida" + "bad_host": "Anulaci\u00f3n de la URL de la API del host no v\u00e1lida" }, "step": { "options_binary": { "data": { "inverse": "Invertir el estado de apertura/cierre", "name": "Nombre", - "type": "Tipo de sensor binario" + "type": "Tipo de Sensor Binario" }, "description": "Opciones de {zone}", - "title": "Configurar sensor binario" + "title": "Configurar Sensor Binario" }, "options_digital": { "data": { @@ -52,7 +52,7 @@ "type": "Tipo de sensor" }, "description": "Opciones de {zone}", - "title": "Configurar el sensor digital" + "title": "Configurar Sensor Digital" }, "options_io": { "data": { @@ -65,7 +65,7 @@ "7": "Zona 7", "out": "OUT" }, - "description": "Descubierto un {model} en {host} . Seleccione la configuraci\u00f3n base de cada I/O a continuaci\u00f3n: seg\u00fan la I/O, puede permitir sensores binarios (contactos de apertura / cierre), sensores digitales (dht y ds18b20) o salidas conmutables. Podr\u00e1 configurar opciones detalladas en los pr\u00f3ximos pasos.", + "description": "Descubierto un {model} en {host}. Selecciona la configuraci\u00f3n base de cada E/S a continuaci\u00f3n; seg\u00fan la E/S, puedes permitir sensores binarios (contactos abiertos/cerrados), sensores digitales (dht y ds18b20) o salidas conmutables. Podr\u00e1s configurar opciones detalladas en los siguientes pasos.", "title": "Configurar E/S" }, "options_io_ext": { @@ -75,34 +75,34 @@ "12": "Zona 12", "8": "Zona 8", "9": "Zona 9", - "alarm1": "ALARMA1", + "alarm1": "ALARM1", "alarm2_out2": "OUT2/ALARM2", "out1": "OUT1" }, - "description": "Seleccione la configuraci\u00f3n de las E/S restantes a continuaci\u00f3n. Podr\u00e1s configurar opciones detalladas en los pr\u00f3ximos pasos.", + "description": "Selecciona la configuraci\u00f3n de las E/S restantes a continuaci\u00f3n. Podr\u00e1s configurar opciones detalladas en los siguientes pasos.", "title": "Configurar E/S extendidas" }, "options_misc": { "data": { "api_host": "Anular la URL de la API del host", "blink": "Parpadear el LED del panel al enviar un cambio de estado", - "discovery": "Responde a las solicitudes de descubrimiento en tu red", - "override_api_host": "Reemplazar la URL predeterminada del panel host de la API de Home Assistant" + "discovery": "Responder a las solicitudes de descubrimiento en tu red", + "override_api_host": "Anular la URL predeterminada del panel de host de la API de Home Assistant" }, - "description": "Seleccione el comportamiento deseado para su panel", + "description": "Selecciona el comportamiento deseado para tu panel", "title": "Configurar miscel\u00e1neos" }, "options_switch": { "data": { "activation": "Salida cuando est\u00e1 activada", "momentary": "Duraci\u00f3n del pulso (ms)", - "more_states": "Configurar estados adicionales para esta zona", + "more_states": "Configura estados adicionales para esta zona", "name": "Nombre", "pause": "Pausa entre pulsos (ms)", "repeat": "Veces que se repite (-1=infinito)" }, "description": "Opciones de {zone}: estado {state}", - "title": "Configurar la salida conmutable" + "title": "Configurar salida conmutable" } } } diff --git a/homeassistant/components/life360/translations/es.json b/homeassistant/components/life360/translations/es.json index 4645b9c88b9..72e880463fe 100644 --- a/homeassistant/components/life360/translations/es.json +++ b/homeassistant/components/life360/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" }, @@ -12,7 +12,7 @@ "error": { "already_configured": "La cuenta ya est\u00e1 configurada", "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_username": "Nombre de usuario no v\u00e1lido", "unknown": "Error inesperado" }, @@ -29,7 +29,7 @@ "username": "Nombre de usuario" }, "description": "Para configurar las opciones avanzadas, consulta la [documentaci\u00f3n de Life360]({docs_url}).\nEs posible que quieras hacerlo antes de a\u00f1adir cuentas.", - "title": "Configurar la cuenta de Life360" + "title": "Configurar cuenta de Life360" } } }, diff --git a/homeassistant/components/light/translations/es.json b/homeassistant/components/light/translations/es.json index 94e28719702..7270ce6e990 100644 --- a/homeassistant/components/light/translations/es.json +++ b/homeassistant/components/light/translations/es.json @@ -3,7 +3,7 @@ "action_type": { "brightness_decrease": "Disminuir brillo de {entity_name}", "brightness_increase": "Aumentar brillo de {entity_name}", - "flash": "Destellos {entity_name}", + "flash": "Destellear {entity_name}", "toggle": "Alternar {entity_name}", "turn_off": "Apagar {entity_name}", "turn_on": "Encender {entity_name}" @@ -20,7 +20,7 @@ }, "state": { "_": { - "off": "Apagado", + "off": "Apagada", "on": "Encendida" } }, diff --git a/homeassistant/components/lock/translations/es.json b/homeassistant/components/lock/translations/es.json index 5cc0e80f97a..90347ba017d 100644 --- a/homeassistant/components/lock/translations/es.json +++ b/homeassistant/components/lock/translations/es.json @@ -6,12 +6,12 @@ "unlock": "Desbloquear {entity_name}" }, "condition_type": { - "is_locked": "{entity_name} est\u00e1 bloqueado", - "is_unlocked": "{entity_name} est\u00e1 desbloqueado" + "is_locked": "{entity_name} est\u00e1 bloqueada", + "is_unlocked": "{entity_name} est\u00e1 desbloqueada" }, "trigger_type": { - "locked": "{entity_name} bloqueado", - "unlocked": "{entity_name} desbloqueado" + "locked": "{entity_name} bloqueada", + "unlocked": "{entity_name} desbloqueada" } }, "state": { diff --git a/homeassistant/components/logi_circle/translations/es.json b/homeassistant/components/logi_circle/translations/es.json index ac1fdc8dc5a..0bc9f8f6521 100644 --- a/homeassistant/components/logi_circle/translations/es.json +++ b/homeassistant/components/logi_circle/translations/es.json @@ -2,19 +2,19 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "external_error": "Ocurri\u00f3 una excepci\u00f3n de otro flujo.", + "external_error": "Ocurri\u00f3 una excepci\u00f3n desde otro flujo.", "external_setup": "Logi Circle se configur\u00f3 correctamente desde otro flujo.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n." + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n." }, "error": { - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "follow_link": "Por favor, sigue el enlace y autent\u00edcate antes de presionar Enviar.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "follow_link": "Por favor, sigue el enlace y autent\u00edcate antes de pulsar Enviar.", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "auth": { - "description": "Por favor, sigue el enlace a continuaci\u00f3n y **Acepta** el acceso a tu cuenta Logi Circle, luego regresa y presiona **Enviar** a continuaci\u00f3n. \n\n[Enlace]({authorization_url})", - "title": "Autenticaci\u00f3n con Logi Circle" + "description": "Por favor, sigue el enlace a continuaci\u00f3n y **Acepta** el acceso a tu cuenta Logi Circle, luego regresa y pulsa **Enviar** a continuaci\u00f3n. \n\n[Enlace]({authorization_url})", + "title": "Autenticar con Logi Circle" }, "user": { "data": { diff --git a/homeassistant/components/lutron_caseta/translations/es.json b/homeassistant/components/lutron_caseta/translations/es.json index c6e90ec6364..aa33259c5a3 100644 --- a/homeassistant/components/lutron_caseta/translations/es.json +++ b/homeassistant/components/lutron_caseta/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "not_lutron_device": "El dispositivo descubierto no es un dispositivo de Lutron" + "not_lutron_device": "El dispositivo descubierto no es un dispositivo Lutron" }, "error": { "cannot_connect": "No se pudo conectar" @@ -11,8 +11,8 @@ "flow_title": "{name} ({host})", "step": { "import_failed": { - "description": "No se ha podido configurar el enlace (anfitri\u00f3n: {host}) importado de configuration.yaml.", - "title": "Error al importar la configuraci\u00f3n del bridge Cas\u00e9ta." + "description": "No se pudo configurar el puente (host: {host}) importado desde configuration.yaml.", + "title": "No se pudo importar la configuraci\u00f3n del puente Cas\u00e9ta." }, "link": { "description": "Para emparejar con {name} ({host}), despu\u00e9s de enviar este formulario, presiona el bot\u00f3n negro en la parte posterior del puente.", @@ -22,8 +22,8 @@ "data": { "host": "Host" }, - "description": "Introduzca la direcci\u00f3n ip del dispositivo.", - "title": "Conectar autom\u00e1ticamente con el dispositivo" + "description": "Introduce la direcci\u00f3n IP del dispositivo.", + "title": "Conectar autom\u00e1ticamente al puente" } } }, @@ -42,11 +42,11 @@ "group_1_button_2": "Segundo bot\u00f3n del primer grupo", "group_2_button_1": "Primer bot\u00f3n del segundo grupo", "group_2_button_2": "Segundo bot\u00f3n del segundo grupo", - "lower": "Inferior", - "lower_1": "Inferior 1", - "lower_2": "Inferior 2", - "lower_3": "Inferior 3", - "lower_4": "Inferior 4", + "lower": "Bajar", + "lower_1": "Bajar 1", + "lower_2": "Bajar 2", + "lower_3": "Bajar 3", + "lower_4": "Bajar 4", "lower_all": "Bajar todo", "off": "Apagado", "on": "Encendido", @@ -69,8 +69,8 @@ "stop_all": "Detener todo" }, "trigger_type": { - "press": "\"{subtype}\" presionado", - "release": "\"{subtype}\" liberado" + "press": "\"{subtype}\" pulsado", + "release": "\"{subtype}\" soltado" } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/es.json b/homeassistant/components/lyric/translations/es.json index 5405ca19ffa..aaddd58b433 100644 --- a/homeassistant/components/lyric/translations/es.json +++ b/homeassistant/components/lyric/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "create_entry": { diff --git a/homeassistant/components/mazda/translations/es.json b/homeassistant/components/mazda/translations/es.json index 01140eb3aad..009385a5bc1 100644 --- a/homeassistant/components/mazda/translations/es.json +++ b/homeassistant/components/mazda/translations/es.json @@ -5,7 +5,7 @@ "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "account_locked": "Cuenta bloqueada. Por favor, int\u00e9ntelo de nuevo m\u00e1s tarde.", + "account_locked": "Cuenta bloqueada. Por favor, int\u00e9ntalo de nuevo m\u00e1s tarde.", "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "email": "Correo electronico", + "email": "Correo electr\u00f3nico", "password": "Contrase\u00f1a", "region": "Regi\u00f3n" }, diff --git a/homeassistant/components/media_player/translations/es.json b/homeassistant/components/media_player/translations/es.json index 0924f212179..946062acdc7 100644 --- a/homeassistant/components/media_player/translations/es.json +++ b/homeassistant/components/media_player/translations/es.json @@ -4,7 +4,7 @@ "is_buffering": "{entity_name} est\u00e1 almacenando en b\u00fafer", "is_idle": "{entity_name} est\u00e1 inactivo", "is_off": "{entity_name} est\u00e1 apagado", - "is_on": "{entity_name} est\u00e1 activado", + "is_on": "{entity_name} est\u00e1 encendido", "is_paused": "{entity_name} est\u00e1 en pausa", "is_playing": "{entity_name} est\u00e1 reproduciendo" }, @@ -13,9 +13,9 @@ "changed_states": "{entity_name} cambi\u00f3 de estado", "idle": "{entity_name} est\u00e1 inactivo", "paused": "{entity_name} est\u00e1 en pausa", - "playing": "{entity_name} comienza a reproducirse", - "turned_off": "{entity_name} desactivado", - "turned_on": "{entity_name} activado" + "playing": "{entity_name} comienza a reproducir", + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido" } }, "state": { diff --git a/homeassistant/components/melcloud/translations/es.json b/homeassistant/components/melcloud/translations/es.json index 94583ed2589..904fe4af2bc 100644 --- a/homeassistant/components/melcloud/translations/es.json +++ b/homeassistant/components/melcloud/translations/es.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Integraci\u00f3n mELCloud ya configurada para este correo electr\u00f3nico. Se ha actualizado el token de acceso." + "already_configured": "Integraci\u00f3n MELCloud ya configurada para este correo electr\u00f3nico. El token de acceso se ha actualizado." }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -14,8 +14,8 @@ "password": "Contrase\u00f1a", "username": "Correo electr\u00f3nico" }, - "description": "Con\u00e9ctate usando tu cuenta de MELCloud.", - "title": "Con\u00e9ctese a MELCloud" + "description": "Con\u00e9ctate usando tu cuenta MELCloud.", + "title": "Conectar a MELCloud" } } } diff --git a/homeassistant/components/met/translations/es.json b/homeassistant/components/met/translations/es.json index b03e6636cf8..645f37aa9a5 100644 --- a/homeassistant/components/met/translations/es.json +++ b/homeassistant/components/met/translations/es.json @@ -9,12 +9,12 @@ "step": { "user": { "data": { - "elevation": "Altitud", + "elevation": "Elevaci\u00f3n", "latitude": "Latitud", "longitude": "Longitud", "name": "Nombre" }, - "description": "Instituto de meteorolog\u00eda", + "description": "Meteorologisk institutt", "title": "Ubicaci\u00f3n" } } diff --git a/homeassistant/components/meteo_france/translations/es.json b/homeassistant/components/meteo_france/translations/es.json index a16c47e4aa9..b8363d01868 100644 --- a/homeassistant/components/meteo_france/translations/es.json +++ b/homeassistant/components/meteo_france/translations/es.json @@ -5,7 +5,7 @@ "unknown": "Error inesperado" }, "error": { - "empty": "No hay resultado en la b\u00fasqueda de la ciudad: por favor, comprueba el campo de la ciudad" + "empty": "No hay resultado en la b\u00fasqueda de la ciudad: por favor, verifica el campo de la ciudad" }, "step": { "cities": { @@ -18,7 +18,7 @@ "data": { "city": "Ciudad" }, - "description": "Introduzca el c\u00f3digo postal (solo para Francia, recomendado) o el nombre de la ciudad" + "description": "Introduce el c\u00f3digo postal (solo para Francia, recomendado) o el nombre de la ciudad" } } }, diff --git a/homeassistant/components/metoffice/translations/es.json b/homeassistant/components/metoffice/translations/es.json index 5751db1f760..0a533ecd130 100644 --- a/homeassistant/components/metoffice/translations/es.json +++ b/homeassistant/components/metoffice/translations/es.json @@ -15,7 +15,7 @@ "longitude": "Longitud" }, "description": "La latitud y la longitud se utilizar\u00e1n para encontrar la estaci\u00f3n meteorol\u00f3gica m\u00e1s cercana.", - "title": "Con\u00e9ctar con la Oficina Meteorol\u00f3gica del Reino Unido" + "title": "Conectar con la UK Met Office" } } } diff --git a/homeassistant/components/mikrotik/translations/es.json b/homeassistant/components/mikrotik/translations/es.json index b1cd62a1763..13bea3f0be0 100644 --- a/homeassistant/components/mikrotik/translations/es.json +++ b/homeassistant/components/mikrotik/translations/es.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "name_exists": "El nombre ya existe" + "name_exists": "El nombre existe" }, "step": { "user": { @@ -18,7 +18,7 @@ "username": "Nombre de usuario", "verify_ssl": "Usar ssl" }, - "title": "Configurar el router Mikrotik" + "title": "Configurar router Mikrotik" } } }, @@ -27,8 +27,8 @@ "device_tracker": { "data": { "arp_ping": "Habilitar ping ARP", - "detection_time": "Considere el intervalo de inicio", - "force_dhcp": "Forzar el escaneo usando DHCP" + "detection_time": "Considerar el intervalo de inicio", + "force_dhcp": "Forzar escaneo usando DHCP" } } } diff --git a/homeassistant/components/minecraft_server/translations/es.json b/homeassistant/components/minecraft_server/translations/es.json index a5c5ae531d9..8fa95f0dd10 100644 --- a/homeassistant/components/minecraft_server/translations/es.json +++ b/homeassistant/components/minecraft_server/translations/es.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "El servicio ya est\u00e1 configurado." + "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { - "cannot_connect": "No se pudo conectar al servidor. Comprueba el host y el puerto e int\u00e9ntalo de nuevo. Tambi\u00e9n aseg\u00farate de que est\u00e1s ejecutando al menos Minecraft versi\u00f3n 1.7 en tu servidor.", - "invalid_ip": "La direcci\u00f3n IP no es valida (no se pudo determinar la direcci\u00f3n MAC). Por favor, corr\u00edgelo e int\u00e9ntalo de nuevo.", - "invalid_port": "El puerto debe estar en el rango de 1024 a 65535. Por favor, corr\u00edgelo e int\u00e9ntalo de nuevo." + "cannot_connect": "Error al conectar con el servidor. Verifica el host y el puerto y vuelve a intentarlo. Tambi\u00e9n aseg\u00farate de estar ejecutando al menos la versi\u00f3n 1.7 de Minecraft en tu servidor.", + "invalid_ip": "La direcci\u00f3n IP no es v\u00e1lida (no se pudo determinar la direcci\u00f3n MAC). Por favor, corr\u00edgelo y vuelve a intentarlo.", + "invalid_port": "El puerto debe estar en el rango de 1024 a 65535. Por favor, corr\u00edgelo y vuelve a intentarlo." }, "step": { "user": { diff --git a/homeassistant/components/motion_blinds/translations/es.json b/homeassistant/components/motion_blinds/translations/es.json index c7469a93820..585a7c0a8ed 100644 --- a/homeassistant/components/motion_blinds/translations/es.json +++ b/homeassistant/components/motion_blinds/translations/es.json @@ -6,7 +6,7 @@ "connection_error": "No se pudo conectar" }, "error": { - "discovery_error": "No se pudo descubrir un detector de movimiento" + "discovery_error": "No se pudo descubrir un Motion Gateway" }, "flow_title": "{short_mac} ({ip_address})", "step": { @@ -14,20 +14,20 @@ "data": { "api_key": "Clave API" }, - "description": "Necesitar\u00e1 la clave de API de 16 caracteres, consulte https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key para obtener instrucciones" + "description": "Necesitar\u00e1s la clave API de 16 caracteres, consulta https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key para obtener instrucciones" }, "select": { "data": { "select_ip": "Direcci\u00f3n IP" }, - "description": "Ejecute la configuraci\u00f3n de nuevo si desea conectar detectores de movimiento adicionales", - "title": "Selecciona el detector de Movimiento que deseas conectar" + "description": "Vuelve a ejecutar la configuraci\u00f3n si deseas conectar Motion Gateways adicionales", + "title": "Selecciona el Motion Gateway que deseas conectar" }, "user": { "data": { "host": "Direcci\u00f3n IP" }, - "description": "Con\u00e9ctate a tu Motion Gateway, si la direcci\u00f3n IP no est\u00e1 establecida, se utilitzar\u00e1 la detecci\u00f3n autom\u00e1tica" + "description": "Con\u00e9ctate a tu Motion Gateway, si la direcci\u00f3n IP no est\u00e1 configurada, se utiliza la detecci\u00f3n autom\u00e1tica" } } }, diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index 4c88a5a66f7..016cb320cfd 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -43,7 +43,7 @@ "button_long_press": "\"{subtype}\" pulsado continuamente", "button_long_release": "\"{subtype}\" soltado despu\u00e9s de pulsaci\u00f3n larga", "button_quadruple_press": "\"{subtype}\" cu\u00e1druple pulsaci\u00f3n", - "button_quintuple_press": "\"{subtype}\" quintuple pulsaci\u00f3n", + "button_quintuple_press": "\"{subtype}\" qu\u00edntuple pulsaci\u00f3n", "button_short_press": "\"{subtype}\" pulsado", "button_short_release": "\"{subtype}\" soltado", "button_triple_press": "\"{subtype}\" triple pulsaci\u00f3n" @@ -51,8 +51,8 @@ }, "options": { "error": { - "bad_birth": "Tema de nacimiento inv\u00e1lido.", - "bad_will": "Tema de voluntad inv\u00e1lido.", + "bad_birth": "Tema de nacimiento no v\u00e1lido.", + "bad_will": "Tema de voluntad no v\u00e1lido.", "cannot_connect": "No se pudo conectar" }, "step": { @@ -63,7 +63,7 @@ "port": "Puerto", "username": "Nombre de usuario" }, - "description": "Por favor, introduzca la informaci\u00f3n de conexi\u00f3n de su br\u00f3ker MQTT.", + "description": "Por favor, introduce la informaci\u00f3n de conexi\u00f3n de tu br\u00f3ker MQTT.", "title": "Opciones del br\u00f3ker" }, "options": { @@ -80,7 +80,7 @@ "will_retain": "Retenci\u00f3n del mensaje de voluntad", "will_topic": "Tema del mensaje de voluntad" }, - "description": "Descubrimiento - Si el descubrimiento est\u00e1 habilitado (recomendado), Home Assistant descubrir\u00e1 autom\u00e1ticamente los dispositivos y entidades que publiquen su configuraci\u00f3n en el br\u00f3ker MQTT. Si el descubrimiento est\u00e1 deshabilitado, toda la configuraci\u00f3n debe hacerse manualmente.\nMensaje de nacimiento - El mensaje de nacimiento se enviar\u00e1 cada vez que Home Assistant se (re)conecte al br\u00f3ker MQTT.\nMensaje de voluntad - El mensaje de voluntad se enviar\u00e1 cada vez que Home Assistant pierda su conexi\u00f3n con el br\u00f3ker, tanto en el caso de una desconexi\u00f3n limpia (por ejemplo, el cierre de Home Assistant) como en el caso de una desconexi\u00f3n no limpia (por ejemplo, el cierre de Home Assistant o la p\u00e9rdida de su conexi\u00f3n de red).", + "description": "Descubrimiento: si el descubrimiento est\u00e1 habilitado (recomendado), Home Assistant descubrir\u00e1 autom\u00e1ticamente los dispositivos y entidades que publican su configuraci\u00f3n en el br\u00f3ker MQTT. Si el descubrimiento est\u00e1 deshabilitado, toda la configuraci\u00f3n debe realizarse manualmente.\nMensaje de nacimiento: el mensaje de nacimiento se enviar\u00e1 cada vez que Home Assistant se (re)conecte con el br\u00f3ker MQTT.\nMensaje de voluntad: el mensaje de voluntad se enviar\u00e1 cada vez que Home Assistant pierda su conexi\u00f3n con el br\u00f3ker, tanto en caso de desconexi\u00f3n limpia (por ejemplo, que Home Assistant se apague) como en caso de desconexi\u00f3n no limpia (por ejemplo, Home Assistant se cuelgue o pierda su conexi\u00f3n de red).", "title": "Opciones de MQTT" } } diff --git a/homeassistant/components/myq/translations/es.json b/homeassistant/components/myq/translations/es.json index 9f20a31ff15..7cb7dcb9354 100644 --- a/homeassistant/components/myq/translations/es.json +++ b/homeassistant/components/myq/translations/es.json @@ -6,7 +6,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { @@ -22,7 +22,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "title": "Conectar con el Gateway " + "title": "Conectar a la puerta de enlace MyQ" } } } diff --git a/homeassistant/components/mysensors/translations/es.json b/homeassistant/components/mysensors/translations/es.json index 15234537136..62010958d67 100644 --- a/homeassistant/components/mysensors/translations/es.json +++ b/homeassistant/components/mysensors/translations/es.json @@ -13,10 +13,10 @@ "invalid_publish_topic": "Tema de publicaci\u00f3n no v\u00e1lido", "invalid_serial": "Puerto serie no v\u00e1lido", "invalid_subscribe_topic": "Tema de suscripci\u00f3n no v\u00e1lido", - "invalid_version": "Versi\u00f3n inv\u00e1lida de MySensors", + "invalid_version": "Versi\u00f3n no v\u00e1lida de MySensors", "mqtt_required": "La integraci\u00f3n MQTT no est\u00e1 configurada", - "not_a_number": "Por favor, introduzca un n\u00famero", - "port_out_of_range": "El n\u00famero de puerto debe ser como m\u00ednimo 1 y como m\u00e1ximo 65535", + "not_a_number": "Por favor, introduce un n\u00famero", + "port_out_of_range": "El n\u00famero de puerto debe ser al menos 1 y como m\u00e1ximo 65535", "same_topic": "Los temas de suscripci\u00f3n y publicaci\u00f3n son los mismos", "unknown": "Error inesperado" }, @@ -36,14 +36,14 @@ "invalid_version": "Versi\u00f3n no v\u00e1lida de MySensors", "mqtt_required": "La integraci\u00f3n MQTT no est\u00e1 configurada", "not_a_number": "Por favor, introduce un n\u00famero", - "port_out_of_range": "El n\u00famero de puerto debe ser como m\u00ednimo 1 y como m\u00e1ximo 65535", + "port_out_of_range": "El n\u00famero de puerto debe ser al menos 1 y como m\u00e1ximo 65535", "same_topic": "Los temas de suscripci\u00f3n y publicaci\u00f3n son los mismos", "unknown": "Error inesperado" }, "step": { "gw_mqtt": { "data": { - "persistence_file": "archivo de persistencia (d\u00e9jelo vac\u00edo para que se genere autom\u00e1ticamente)", + "persistence_file": "archivo de persistencia (d\u00e9jalo vac\u00edo para que se genere autom\u00e1ticamente)", "retain": "retenci\u00f3n mqtt", "topic_in_prefix": "prefijo para los temas de entrada (topic_in_prefix)", "topic_out_prefix": "prefijo para los temas de salida (topic_out_prefix)", @@ -55,19 +55,19 @@ "data": { "baud_rate": "tasa de baudios", "device": "Puerto serie", - "persistence_file": "archivo de persistencia (d\u00e9jelo vac\u00edo para que se genere autom\u00e1ticamente)", + "persistence_file": "archivo de persistencia (d\u00e9jalo vac\u00edo para que se genere autom\u00e1ticamente)", "version": "Versi\u00f3n de MySensors" }, - "description": "Configuraci\u00f3n de la pasarela en serie" + "description": "Configuraci\u00f3n de la puerta de enlace serie" }, "gw_tcp": { "data": { - "device": "Direcci\u00f3n IP de la pasarela", - "persistence_file": "archivo de persistencia (d\u00e9jelo vac\u00edo para que se genere autom\u00e1ticamente)", - "tcp_port": "Puerto", - "version": "Versi\u00f3n de MySensores" + "device": "Direcci\u00f3n IP de la puerta de enlace", + "persistence_file": "archivo de persistencia (d\u00e9jalo vac\u00edo para que se genere autom\u00e1ticamente)", + "tcp_port": "puerto", + "version": "Versi\u00f3n de MySensors" }, - "description": "Configuraci\u00f3n de la pasarela Ethernet" + "description": "Configuraci\u00f3n de la puerta de enlace Ethernet" }, "select_gateway_type": { "description": "Selecciona qu\u00e9 puerta de enlace configurar.", @@ -79,9 +79,9 @@ }, "user": { "data": { - "gateway_type": "Tipo de pasarela" + "gateway_type": "Tipo de puerta de enlace" }, - "description": "Elija el m\u00e9todo de conexi\u00f3n con la pasarela" + "description": "Elige el m\u00e9todo de conexi\u00f3n a la puerta de enlace" } } } diff --git a/homeassistant/components/nanoleaf/translations/es.json b/homeassistant/components/nanoleaf/translations/es.json index 12ae1b235ba..9de6598da5c 100644 --- a/homeassistant/components/nanoleaf/translations/es.json +++ b/homeassistant/components/nanoleaf/translations/es.json @@ -15,7 +15,7 @@ "flow_title": "{name}", "step": { "link": { - "description": "Mant\u00e9n presionado el bot\u00f3n de encendido de tu Nanoleaf durante 5 segundos hasta que los LED del bot\u00f3n comiencen a parpadear, luego haz clic en **ENVIAR** en los siguientes 30 segundos.", + "description": "Mant\u00e9n pulsado el bot\u00f3n de encendido de tu Nanoleaf durante 5 segundos hasta que los LED del bot\u00f3n comiencen a parpadear, luego haz clic en **ENVIAR** en los siguientes 30 segundos.", "title": "Vincular Nanoleaf" }, "user": { diff --git a/homeassistant/components/neato/translations/es.json b/homeassistant/components/neato/translations/es.json index 7eeecb4dde0..81f11ec51ff 100644 --- a/homeassistant/components/neato/translations/es.json +++ b/homeassistant/components/neato/translations/es.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index 93dd7eb12a2..7715f1f99bd 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -5,16 +5,16 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "invalid_access_token": "Token de acceso no v\u00e1lido", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, "create_entry": { - "default": "Autenticado con \u00e9xito" + "default": "Autenticado correctamente" }, "error": { "bad_project_id": "Por favor introduce un ID de proyecto en la nube v\u00e1lido (verifica la Cloud Console)", @@ -74,7 +74,7 @@ "title": "Vincular cuenta de Nest" }, "pick_implementation": { - "title": "Elija el m\u00e9todo de autenticaci\u00f3n" + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" }, "pubsub": { "data": { @@ -84,7 +84,7 @@ "title": "Configurar Google Cloud" }, "reauth_confirm": { - "description": "La integraci\u00f3n de Nest necesita volver a autenticar tu cuenta", + "description": "La integraci\u00f3n Nest necesita volver a autenticar tu cuenta", "title": "Volver a autenticar la integraci\u00f3n" } } @@ -103,7 +103,7 @@ "title": "Se va a eliminar la configuraci\u00f3n YAML de Nest" }, "removed_app_auth": { - "description": "Para mejorar la seguridad y reducir el riesgo de phishing, Google ha dejado de utilizar el m\u00e9todo de autenticaci\u00f3n utilizado por Home Assistant. \n\n **Esto requiere una acci\u00f3n de su parte para resolverlo** ([m\u00e1s informaci\u00f3n]({more_info_url})) \n\n 1. Visita la p\u00e1gina de integraciones\n 1. Haz clic en Reconfigurar en la integraci\u00f3n de Nest.\n 1. Home Assistant te guiar\u00e1 a trav\u00e9s de los pasos para actualizar a la autenticaci\u00f3n web. \n\nConsulta las [instrucciones de integraci\u00f3n]({documentation_url}) de Nest para obtener informaci\u00f3n sobre la soluci\u00f3n de problemas.", + "description": "Para mejorar la seguridad y reducir el riesgo de phishing, Google ha dejado de utilizar el m\u00e9todo de autenticaci\u00f3n utilizado por Home Assistant. \n\n **Esto requiere una acci\u00f3n por tu parte para resolverlo** ([m\u00e1s informaci\u00f3n]({more_info_url})) \n\n 1. Visita la p\u00e1gina de integraciones\n 1. Haz clic en Reconfigurar en la integraci\u00f3n de Nest.\n 1. Home Assistant te guiar\u00e1 a trav\u00e9s de los pasos para actualizar a la autenticaci\u00f3n web. \n\nConsulta las [instrucciones de integraci\u00f3n]({documentation_url}) de Nest para obtener informaci\u00f3n sobre la soluci\u00f3n de problemas.", "title": "Las credenciales de autenticaci\u00f3n de Nest deben actualizarse" } } diff --git a/homeassistant/components/netatmo/translations/es.json b/homeassistant/components/netatmo/translations/es.json index 0d8e4167ab8..3a8350932be 100644 --- a/homeassistant/components/netatmo/translations/es.json +++ b/homeassistant/components/netatmo/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "authorize_url_timeout": "Tiempo excedido generando la url de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Por favor, consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." @@ -12,7 +12,7 @@ }, "step": { "pick_implementation": { - "title": "Selecciona un m\u00e9todo de autenticaci\u00f3n" + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" }, "reauth_confirm": { "description": "La integraci\u00f3n Netatmo necesita volver a autenticar tu cuenta", @@ -47,23 +47,23 @@ "public_weather": { "data": { "area_name": "Nombre del \u00e1rea", - "lat_ne": "Latitud Esquina noreste", - "lat_sw": "Latitud Esquina suroeste", - "lon_ne": "Longitud Esquina noreste", - "lon_sw": "Longitud Esquina suroeste", + "lat_ne": "Latitud de la esquina noreste", + "lat_sw": "Latitud de la esquina suroeste", + "lon_ne": "Longitud de la esquina noreste", + "lon_sw": "Longitud de la esquina suroeste", "mode": "C\u00e1lculo", "show_on_map": "Mostrar en el mapa" }, "description": "Configura un sensor de clima p\u00fablico para un \u00e1rea.", - "title": "Sensor de clima p\u00fablico Netatmo" + "title": "Sensor meteorol\u00f3gico p\u00fablico Netatmo" }, "public_weather_areas": { "data": { "new_area": "Nombre del \u00e1rea", "weather_areas": "Zonas meteorol\u00f3gicas" }, - "description": "Configurar sensores de clima p\u00fablicos.", - "title": "Sensor de clima p\u00fablico Netatmo" + "description": "Configura sensores meteorol\u00f3gicos p\u00fablicos.", + "title": "Sensor meteorol\u00f3gico p\u00fablico Netatmo" } } } diff --git a/homeassistant/components/nexia/translations/es.json b/homeassistant/components/nexia/translations/es.json index 9e9e720dc15..206e27d1fd2 100644 --- a/homeassistant/components/nexia/translations/es.json +++ b/homeassistant/components/nexia/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/nightscout/translations/es.json b/homeassistant/components/nightscout/translations/es.json index ab9ce8baa02..2d41c4714ce 100644 --- a/homeassistant/components/nightscout/translations/es.json +++ b/homeassistant/components/nightscout/translations/es.json @@ -15,7 +15,7 @@ "url": "URL" }, "description": "- URL: la direcci\u00f3n de tu instancia nightscout. Por ejemplo: https://myhomeassistant.duckdns.org:5423\n- Clave de API (opcional): usar solo si tu instancia est\u00e1 protegida (auth_default_roles != legible).", - "title": "Introduce la informaci\u00f3n del servidor de Nightscout." + "title": "Introduce tu informaci\u00f3n del servidor Nightscout." } } } diff --git a/homeassistant/components/nuheat/translations/es.json b/homeassistant/components/nuheat/translations/es.json index a64a68e2e70..cca9cb83f05 100644 --- a/homeassistant/components/nuheat/translations/es.json +++ b/homeassistant/components/nuheat/translations/es.json @@ -16,8 +16,8 @@ "serial_number": "N\u00famero de serie del termostato.", "username": "Nombre de usuario" }, - "description": "Necesitas obtener el n\u00famero de serie o el ID de tu termostato iniciando sesi\u00f3n en https://MyNuHeat.com y seleccionando tu(s) termostato(s).", - "title": "ConectarNuHeat" + "description": "Deber\u00e1s obtener el n\u00famero de serie num\u00e9rico o ID de tu termostato iniciando sesi\u00f3n en https://MyNuHeat.com y seleccionando tu(s) termostato(s).", + "title": "Conectar al NuHeat" } } } diff --git a/homeassistant/components/nut/translations/es.json b/homeassistant/components/nut/translations/es.json index edb49ebcbcd..f02fa8017bc 100644 --- a/homeassistant/components/nut/translations/es.json +++ b/homeassistant/components/nut/translations/es.json @@ -12,7 +12,7 @@ "data": { "alias": "Alias" }, - "title": "Selecciona el UPS a monitorizar" + "title": "Elige el SAI a supervisar" }, "user": { "data": { diff --git a/homeassistant/components/nzbget/translations/es.json b/homeassistant/components/nzbget/translations/es.json index eeb2b46ba8b..4785e8437cd 100644 --- a/homeassistant/components/nzbget/translations/es.json +++ b/homeassistant/components/nzbget/translations/es.json @@ -19,7 +19,7 @@ "username": "Nombre de usuario", "verify_ssl": "Verificar el certificado SSL" }, - "title": "Conectarse a NZBGet" + "title": "Conectar a NZBGet" } } }, diff --git a/homeassistant/components/ondilo_ico/translations/es.json b/homeassistant/components/ondilo_ico/translations/es.json index 1de8399aced..3d55a3d6f31 100644 --- a/homeassistant/components/ondilo_ico/translations/es.json +++ b/homeassistant/components/ondilo_ico/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n." + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n." }, "create_entry": { "default": "Autenticado correctamente" diff --git a/homeassistant/components/onvif/translations/es.json b/homeassistant/components/onvif/translations/es.json index aa1d40e3f14..0b450da3d46 100644 --- a/homeassistant/components/onvif/translations/es.json +++ b/homeassistant/components/onvif/translations/es.json @@ -3,9 +3,9 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "no_h264": "No hab\u00eda transmisiones H264 disponibles. Verifique la configuraci\u00f3n del perfil en su dispositivo.", - "no_mac": "No se pudo configurar una identificaci\u00f3n \u00fanica para el dispositivo ONVIF.", - "onvif_error": "Error de configuraci\u00f3n del dispositivo ONVIF. Comprueba el registro para m\u00e1s informaci\u00f3n." + "no_h264": "No hab\u00eda transmisiones H264 disponibles. Verifica la configuraci\u00f3n del perfil en tu dispositivo.", + "no_mac": "No se pudo configurar un ID \u00fanico para el dispositivo ONVIF.", + "onvif_error": "Error al configurar el dispositivo ONVIF. Consulta los registros para obtener m\u00e1s informaci\u00f3n." }, "error": { "cannot_connect": "No se pudo conectar" @@ -23,22 +23,22 @@ }, "configure_profile": { "data": { - "include": "Crear la entidad de la c\u00e1mara" + "include": "Crear la entidad de c\u00e1mara" }, "description": "\u00bfCrear la entidad de c\u00e1mara para {profile} a {resolution} de resoluci\u00f3n?", - "title": "Configurar los perfiles" + "title": "Configurar perfiles" }, "device": { "data": { - "host": "Seleccione el dispositivo ONVIF descubierto" + "host": "Selecciona el dispositivo ONVIF descubierto" }, - "title": "Seleccione el dispositivo ONVIF" + "title": "Selecciona el dispositivo ONVIF" }, "user": { "data": { "auto": "Buscar autom\u00e1ticamente" }, - "description": "Al hacer clic en Enviar, buscaremos en su red dispositivos ONVIF compatibles con el perfil S.\n\nAlgunos fabricantes han comenzado a desactivar ONVIF de forma predeterminada. Aseg\u00farese de que ONVIF est\u00e9 activado en la configuraci\u00f3n de la c\u00e1mara.", + "description": "Al hacer clic en enviar, buscaremos en tu red dispositivos ONVIF compatibles con el perfil S. \n\nAlgunos fabricantes han comenzado a deshabilitar ONVIF por defecto. Aseg\u00farate de que ONVIF est\u00e9 habilitado en la configuraci\u00f3n de tu c\u00e1mara.", "title": "Configuraci\u00f3n del dispositivo ONVIF" } } diff --git a/homeassistant/components/openexchangerates/translations/it.json b/homeassistant/components/openexchangerates/translations/it.json index d42f7122593..38fca960f50 100644 --- a/homeassistant/components/openexchangerates/translations/it.json +++ b/homeassistant/components/openexchangerates/translations/it.json @@ -17,13 +17,17 @@ "data": { "api_key": "Chiave API", "base": "Valuta di base" + }, + "data_description": { + "base": "L'utilizzo di una valuta di base diversa da USD richiede un [piano a pagamento]({signup})." } } } }, "issues": { "deprecated_yaml": { - "description": "La configurazione di Open Exchange Rates tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Open Exchange Rates dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema." + "description": "La configurazione di Open Exchange Rates tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Open Exchange Rates dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Open Exchange Rates sar\u00e0 rimossa" } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/es.json b/homeassistant/components/opentherm_gw/translations/es.json index 0310cdd8236..1088ce834cf 100644 --- a/homeassistant/components/opentherm_gw/translations/es.json +++ b/homeassistant/components/opentherm_gw/translations/es.json @@ -3,7 +3,7 @@ "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", "cannot_connect": "No se pudo conectar", - "id_exists": "El ID del Gateway ya existe", + "id_exists": "El ID de la puerta de enlace ya existe", "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n" }, "step": { diff --git a/homeassistant/components/ovo_energy/translations/es.json b/homeassistant/components/ovo_energy/translations/es.json index a060f0f9552..0af57427980 100644 --- a/homeassistant/components/ovo_energy/translations/es.json +++ b/homeassistant/components/ovo_energy/translations/es.json @@ -11,7 +11,7 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Error de autenticaci\u00f3n para OVO Energy. Ingrese sus credenciales actuales.", + "description": "La autenticaci\u00f3n fall\u00f3 para OVO Energy. Introduce tus credenciales actuales.", "title": "Reautenticaci\u00f3n" }, "user": { @@ -19,7 +19,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Configurar una instancia de OVO Energy para acceder a su consumo de energ\u00eda.", + "description": "Configura una instancia de OVO Energy para acceder a tu consumo de energ\u00eda.", "title": "A\u00f1adir cuenta de OVO Energy" } } diff --git a/homeassistant/components/panasonic_viera/translations/es.json b/homeassistant/components/panasonic_viera/translations/es.json index d14cc6b878b..76037a96339 100644 --- a/homeassistant/components/panasonic_viera/translations/es.json +++ b/homeassistant/components/panasonic_viera/translations/es.json @@ -7,14 +7,14 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_pin_code": "El c\u00f3digo PIN que has introducido no es v\u00e1lido" + "invalid_pin_code": "El C\u00f3digo PIN que has introducido no es v\u00e1lido" }, "step": { "pairing": { "data": { "pin": "C\u00f3digo PIN" }, - "description": "Introduce el PIN que aparece en tu Televisor", + "description": "Introduce el C\u00f3digo PIN que se muestra en tu TV", "title": "Emparejamiento" }, "user": { @@ -22,8 +22,8 @@ "host": "Direcci\u00f3n IP", "name": "Nombre" }, - "description": "Introduce la direcci\u00f3n IP de tu Panasonic Viera TV", - "title": "Configura tu televisi\u00f3n" + "description": "Introduce la Direcci\u00f3n IP de tu Panasonic Viera TV", + "title": "Configura tu TV" } } } diff --git a/homeassistant/components/philips_js/translations/es.json b/homeassistant/components/philips_js/translations/es.json index a2842917b87..5814e5e4a30 100644 --- a/homeassistant/components/philips_js/translations/es.json +++ b/homeassistant/components/philips_js/translations/es.json @@ -19,7 +19,7 @@ }, "user": { "data": { - "api_version": "Versi\u00f3n del API", + "api_version": "Versi\u00f3n de la API", "host": "Host" } } @@ -27,7 +27,7 @@ }, "device_automation": { "trigger_type": { - "turn_on": "Se solicita al dispositivo que se encienda" + "turn_on": "Se solicita que el dispositivo se encienda" } }, "options": { diff --git a/homeassistant/components/plaato/translations/es.json b/homeassistant/components/plaato/translations/es.json index df1ffb99cb1..23acbb80c6c 100644 --- a/homeassistant/components/plaato/translations/es.json +++ b/homeassistant/components/plaato/translations/es.json @@ -7,12 +7,12 @@ "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, "create_entry": { - "default": "\u00a1El dispositivo Plaato {device_type} con nombre **{device_name}** se ha configurado correctamente!" + "default": "\u00a1Tu {device_type} Plaato con nombre **{device_name}** se configur\u00f3 correctamente!" }, "error": { "invalid_webhook_device": "Has seleccionado un dispositivo que no admite el env\u00edo de datos a un webhook. Solo est\u00e1 disponible para el Airlock", "no_api_method": "Necesitas a\u00f1adir un token de autenticaci\u00f3n o seleccionar un webhook", - "no_auth_token": "Es necesario a\u00f1adir un token de autenticaci\u00f3n" + "no_auth_token": "Necesitas a\u00f1adir un token de autenticaci\u00f3n" }, "step": { "api_method": { @@ -20,19 +20,19 @@ "token": "Pega el token de autenticaci\u00f3n aqu\u00ed", "use_webhook": "Usar webhook" }, - "description": "Para poder consultar la API se necesita un `auth_token` que puede obtenerse siguiendo [estas](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instrucciones\n\n Dispositivo seleccionado: **{device_type}** \n\nSi prefiere utilizar el m\u00e9todo de webhook incorporado (s\u00f3lo Airlock), marque la casilla siguiente y deje en blanco el Auth Token", - "title": "Selecciona el m\u00e9todo API" + "description": "Para poder consultar la API, se requiere un `auth_token` que se puede obtener siguiendo [estas](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instrucciones \n\nDispositivo seleccionado: ** {device_type} ** \n\nSi prefieres utilizar el m\u00e9todo de webhook incorporado (solo Airlock), marca la casilla a continuaci\u00f3n y deja el token de autenticaci\u00f3n en blanco", + "title": "Seleccionar el m\u00e9todo API" }, "user": { "data": { - "device_name": "Nombre de su dispositivo", + "device_name": "Nombra tu dispositivo", "device_type": "Tipo de dispositivo Plaato" }, - "description": "\u00bfQuieres empezar la configuraci\u00f3n?", - "title": "Configura dispositivos Plaato" + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?", + "title": "Configurar los dispositivos Plaato" }, "webhook": { - "description": "Para enviar eventos a Home Assistant, deber\u00e1 configurar la funci\u00f3n de webhook en Plaato Airlock. \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\n Consulte [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s detalles.", + "description": "Para enviar eventos a Home Assistant, deber\u00e1s configurar la funci\u00f3n de webhook en Plaato Airlock. \n\nCompleta la siguiente informaci\u00f3n: \n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST \n\nConsulta [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s detalles.", "title": "Webhook a utilizar" } } @@ -43,11 +43,11 @@ "data": { "update_interval": "Intervalo de actualizaci\u00f3n (minutos)" }, - "description": "Intervalo de actualizaci\u00f3n (minutos)", + "description": "Establecer el intervalo de actualizaci\u00f3n (minutos)", "title": "Opciones de Plaato" }, "webhook": { - "description": "Informaci\u00f3n de webhook: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\n", + "description": "Informaci\u00f3n del webhook: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST\n\n", "title": "Opciones para Plaato Airlock" } } diff --git a/homeassistant/components/plex/translations/es.json b/homeassistant/components/plex/translations/es.json index 0b030c2a958..14fe1b840a5 100644 --- a/homeassistant/components/plex/translations/es.json +++ b/homeassistant/components/plex/translations/es.json @@ -10,9 +10,9 @@ }, "error": { "faulty_credentials": "La autorizaci\u00f3n ha fallado, verifica el token", - "host_or_token": "Debes proporcionar al menos uno de Host o Token", + "host_or_token": "Debes proporcionar al menos uno entre Host y Token", "no_servers": "No hay servidores vinculados a la cuenta Plex", - "not_found": "No se ha encontrado el servidor Plex", + "not_found": "Servidor Plex no encontrado", "ssl_error": "Problema con el certificado SSL" }, "flow_title": "{name} ({host})", @@ -31,8 +31,8 @@ "data": { "server": "Servidor" }, - "description": "Varios servidores disponibles, seleccione uno:", - "title": "Seleccione el servidor Plex" + "description": "M\u00faltiples servidores disponibles, selecciona uno:", + "title": "Selecciona el servidor Plex" }, "user": { "description": "Contin\u00faa hacia [plex.tv](https://plex.tv) para vincular un servidor Plex." @@ -50,7 +50,7 @@ "data": { "ignore_new_shared_users": "Ignorar nuevos usuarios administrados/compartidos", "ignore_plex_web_clients": "Ignorar clientes web de Plex", - "monitored_users": "Usuarios monitorizados", + "monitored_users": "Usuarios supervisados", "use_episode_art": "Usar el arte de episodios" }, "description": "Opciones para reproductores multimedia Plex" diff --git a/homeassistant/components/plugwise/translations/es.json b/homeassistant/components/plugwise/translations/es.json index 570184aaabf..50e440f6d6a 100644 --- a/homeassistant/components/plugwise/translations/es.json +++ b/homeassistant/components/plugwise/translations/es.json @@ -5,7 +5,7 @@ "anna_with_adam": "Tanto Anna como Adam han sido detectados. A\u00f1ade tu Adam en lugar de tu Anna" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_setup": "A\u00f1ade tu Adam en lugar de tu Anna, consulta la documentaci\u00f3n de la integraci\u00f3n Home Assistant Plugwise para m\u00e1s informaci\u00f3n", "unknown": "Error inesperado" @@ -26,12 +26,12 @@ "user_gateway": { "data": { "host": "Direcci\u00f3n IP", - "password": "ID Smile", + "password": "ID de Smile", "port": "Puerto", "username": "Nombre de usuario Smile" }, "description": "Por favor, introduce:", - "title": "Conectarse a Smile" + "title": "Conectar a Smile" } } }, diff --git a/homeassistant/components/point/translations/es.json b/homeassistant/components/point/translations/es.json index 2e1de2f1f6f..c0a86f70d55 100644 --- a/homeassistant/components/point/translations/es.json +++ b/homeassistant/components/point/translations/es.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_setup": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "external_setup": "Point se ha configurado correctamente a partir de otro flujo.", - "no_flows": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "no_flows": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, "create_entry": { diff --git a/homeassistant/components/powerwall/translations/es.json b/homeassistant/components/powerwall/translations/es.json index baf6fbda838..0e34589a260 100644 --- a/homeassistant/components/powerwall/translations/es.json +++ b/homeassistant/components/powerwall/translations/es.json @@ -9,7 +9,7 @@ "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado", - "wrong_version": "Tu powerwall utiliza una versi\u00f3n de software que no es compatible. Por favor, considera actualizar o informar este problema para que pueda resolverse." + "wrong_version": "Tu powerwall utiliza una versi\u00f3n de software que no es compatible. Por favor, considera actualizar o informar de este problema para que pueda resolverse." }, "flow_title": "{name} ({ip_address})", "step": { diff --git a/homeassistant/components/progettihwsw/translations/es.json b/homeassistant/components/progettihwsw/translations/es.json index d4915dcb4cd..16a77ce813a 100644 --- a/homeassistant/components/progettihwsw/translations/es.json +++ b/homeassistant/components/progettihwsw/translations/es.json @@ -34,7 +34,7 @@ "host": "Host", "port": "Puerto" }, - "title": "Configurar tablero" + "title": "Configurar placa" } } } diff --git a/homeassistant/components/ps4/translations/es.json b/homeassistant/components/ps4/translations/es.json index fd1283ccfd8..01df37c8dfb 100644 --- a/homeassistant/components/ps4/translations/es.json +++ b/homeassistant/components/ps4/translations/es.json @@ -9,13 +9,13 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "credential_timeout": "Se agot\u00f3 el tiempo de espera del servicio de credenciales. Presiona enviar para reiniciar.", + "credential_timeout": "Se agot\u00f3 el tiempo de espera del servicio de credenciales. Pulsa enviar para reiniciar.", "login_failed": "No se pudo emparejar con PlayStation 4. Verifica que el C\u00f3digo PIN sea correcto.", "no_ipaddress": "Introduce la Direcci\u00f3n IP de la PlayStation 4 que deseas configurar." }, "step": { "creds": { - "description": "Credenciales necesarias. Presiona 'Enviar' y luego en la aplicaci\u00f3n PS4 Second Screen, actualiza los dispositivos y selecciona el dispositivo 'Home-Assistant' para continuar." + "description": "Credenciales necesarias. Pulsa 'Enviar' y luego en la aplicaci\u00f3n PS4 Second Screen, actualiza los dispositivos y selecciona el dispositivo 'Home-Assistant' para continuar." }, "link": { "data": { diff --git a/homeassistant/components/rachio/translations/es.json b/homeassistant/components/rachio/translations/es.json index 671a00a334c..d78f48c9333 100644 --- a/homeassistant/components/rachio/translations/es.json +++ b/homeassistant/components/rachio/translations/es.json @@ -4,8 +4,8 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Fall\u00f3 la conexi\u00f3n", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/recollect_waste/translations/es.json b/homeassistant/components/recollect_waste/translations/es.json index 69a39d435eb..924c162f7bf 100644 --- a/homeassistant/components/recollect_waste/translations/es.json +++ b/homeassistant/components/recollect_waste/translations/es.json @@ -21,7 +21,7 @@ "data": { "friendly_name": "Utilizar nombres descriptivos para los tipos de recogida (cuando sea posible)" }, - "title": "Configurar la recogida de residuos" + "title": "Configurar ReCollect Waste" } } } diff --git a/homeassistant/components/remote/translations/es.json b/homeassistant/components/remote/translations/es.json index b2c8aea25cc..26770bb1233 100644 --- a/homeassistant/components/remote/translations/es.json +++ b/homeassistant/components/remote/translations/es.json @@ -7,12 +7,12 @@ }, "condition_type": { "is_off": "{entity_name} est\u00e1 apagado", - "is_on": "{entity_name} est\u00e1 activado" + "is_on": "{entity_name} est\u00e1 encendido" }, "trigger_type": { "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", - "turned_off": "{entity_name} desactivado", - "turned_on": "{entity_name} activado" + "turned_off": "{entity_name} apagado", + "turned_on": "{entity_name} encendido" } }, "state": { diff --git a/homeassistant/components/rfxtrx/translations/es.json b/homeassistant/components/rfxtrx/translations/es.json index aa567d0093d..a3536c867d4 100644 --- a/homeassistant/components/rfxtrx/translations/es.json +++ b/homeassistant/components/rfxtrx/translations/es.json @@ -13,11 +13,11 @@ "host": "Host", "port": "Puerto" }, - "title": "Seleccionar la direcci\u00f3n de conexi\u00f3n" + "title": "Selecciona la direcci\u00f3n de conexi\u00f3n" }, "setup_serial": { "data": { - "device": "Seleccionar dispositivo" + "device": "Selecciona el dispositivo" }, "title": "Dispositivo" }, @@ -31,7 +31,7 @@ "data": { "type": "Tipo de conexi\u00f3n" }, - "title": "Seleccionar tipo de conexi\u00f3n" + "title": "Selecciona el tipo de conexi\u00f3n" } } }, @@ -49,18 +49,18 @@ "error": { "already_configured_device": "El dispositivo ya est\u00e1 configurado", "invalid_event_code": "C\u00f3digo de evento no v\u00e1lido", - "invalid_input_2262_off": "Entrada inv\u00e1lida para el comando de apagado", - "invalid_input_2262_on": "Entrada inv\u00e1lida para el comando de encendido", - "invalid_input_off_delay": "Entrada inv\u00e1lida para el retardo de apagado", + "invalid_input_2262_off": "Entrada no v\u00e1lida para el comando de apagado", + "invalid_input_2262_on": "Entrada no v\u00e1lida para el comando de encendido", + "invalid_input_off_delay": "Entrada no v\u00e1lida para retardo de desconexi\u00f3n", "unknown": "Error inesperado" }, "step": { "prompt_options": { "data": { - "automatic_add": "Activar la adici\u00f3n autom\u00e1tica", - "debug": "Activar la depuraci\u00f3n", - "device": "Seleccionar dispositivo para configurar", - "event_code": "Introducir el c\u00f3digo de evento para a\u00f1adir", + "automatic_add": "Habilitar a\u00f1adir autom\u00e1ticamente", + "debug": "Habilitar depuraci\u00f3n", + "device": "Selecciona el dispositivo a configurar", + "event_code": "Introduce el c\u00f3digo de evento para a\u00f1adir", "protocols": "Protocolos" }, "title": "Opciones de Rfxtrx" @@ -72,7 +72,7 @@ "data_bit": "N\u00famero de bits de datos", "off_delay": "Retraso de apagado", "off_delay_enabled": "Activar retardo de apagado", - "replace_device": "Seleccione el dispositivo que desea reemplazar", + "replace_device": "Selecciona el dispositivo para reemplazar", "venetian_blind_mode": "Modo de persiana veneciana" }, "title": "Configurar las opciones del dispositivo" diff --git a/homeassistant/components/ring/translations/es.json b/homeassistant/components/ring/translations/es.json index 4a3947c5efc..07d09b7b08d 100644 --- a/homeassistant/components/ring/translations/es.json +++ b/homeassistant/components/ring/translations/es.json @@ -19,7 +19,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "title": "Iniciar sesi\u00f3n con cuenta de Ring" + "title": "Iniciar sesi\u00f3n con cuenta Ring" } } } diff --git a/homeassistant/components/risco/translations/es.json b/homeassistant/components/risco/translations/es.json index 8d0c0460a3c..ff6176afc5a 100644 --- a/homeassistant/components/risco/translations/es.json +++ b/homeassistant/components/risco/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { @@ -28,12 +28,12 @@ "armed_night": "Armada Noche" }, "description": "Selecciona en qu\u00e9 estado quieres configurar la alarma Risco cuando armes la alarma de Home Assistant", - "title": "Mapear estados de Home Assistant a estados Risco" + "title": "Mapear estados de Home Assistant a estados de Risco" }, "init": { "data": { - "code_arm_required": "Requiere un C\u00f3digo PIN para armar", - "code_disarm_required": "Requiere un c\u00f3digo PIN para desactivar", + "code_arm_required": "Requerir C\u00f3digo PIN para armar", + "code_disarm_required": "Requerir C\u00f3digo PIN para desarmar", "scan_interval": "Con qu\u00e9 frecuencia sondear Risco (en segundos)" }, "title": "Configurar opciones" @@ -48,7 +48,7 @@ "partial_arm": "Parcialmente Armada (EN CASA)" }, "description": "Selecciona qu\u00e9 estado reportar\u00e1 la alarma de tu Home Assistant para cada estado reportado por Risco", - "title": "Asignar estados de Risco a estados de Home Assistant" + "title": "Mapear estados de Risco a estados de Home Assistant" } } } diff --git a/homeassistant/components/roku/translations/es.json b/homeassistant/components/roku/translations/es.json index 015360d76c9..cedfbc6d4b1 100644 --- a/homeassistant/components/roku/translations/es.json +++ b/homeassistant/components/roku/translations/es.json @@ -6,12 +6,12 @@ "unknown": "Error inesperado" }, "error": { - "cannot_connect": "Fallo al conectar" + "cannot_connect": "No se pudo conectar" }, "flow_title": "{name}", "step": { "discovery_confirm": { - "description": "\u00bfQuieres configurar {name} ?" + "description": "\u00bfQuieres configurar {name}?" }, "user": { "data": { diff --git a/homeassistant/components/roomba/translations/es.json b/homeassistant/components/roomba/translations/es.json index cc2954335b6..0744bf05620 100644 --- a/homeassistant/components/roomba/translations/es.json +++ b/homeassistant/components/roomba/translations/es.json @@ -12,15 +12,15 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "Mant\u00e9n presionado el bot\u00f3n Inicio en {name} hasta que el dispositivo genere un sonido (alrededor de dos segundos), luego haz clic en enviar en los siguientes 30 segundos.", + "description": "Mant\u00e9n pulsado el bot\u00f3n Inicio en {name} hasta que el dispositivo genere un sonido (alrededor de dos segundos), luego haz clic en enviar en los siguientes 30 segundos.", "title": "Recuperar Contrase\u00f1a" }, "link_manual": { "data": { "password": "Contrase\u00f1a" }, - "description": "La contrase\u00f1a no se pudo recuperar del dispositivo autom\u00e1ticamente. Sigue los pasos descritos en la documentaci\u00f3n en: {auth_help_url}", - "title": "Escribe la contrase\u00f1a" + "description": "La contrase\u00f1a no se pudo recuperar del dispositivo autom\u00e1ticamente. Por favor, sigue los pasos descritos en la documentaci\u00f3n en: {auth_help_url}", + "title": "Introduce la contrase\u00f1a" }, "manual": { "data": { @@ -33,8 +33,8 @@ "data": { "host": "Host" }, - "description": "Seleccione una Roomba o Braava.", - "title": "Conexi\u00f3n autom\u00e1tica con el dispositivo" + "description": "Selecciona una Roomba o Braava.", + "title": "Conectar autom\u00e1ticamente al dispositivo" } } }, diff --git a/homeassistant/components/rpi_power/translations/es.json b/homeassistant/components/rpi_power/translations/es.json index 209cb867d23..806a049bb1b 100644 --- a/homeassistant/components/rpi_power/translations/es.json +++ b/homeassistant/components/rpi_power/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "No se puede encontrar la clase de sistema necesaria para este componente, aseg\u00farate de que tu kernel es reciente y el hardware es compatible", + "no_devices_found": "No se puede encontrar la clase de sistema necesaria para este componente, aseg\u00farate de que tu kernel es reciente y que el hardware es compatible", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "step": { diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json index 9a5c8c5fa51..6a5487175e7 100644 --- a/homeassistant/components/samsungtv/translations/es.json +++ b/homeassistant/components/samsungtv/translations/es.json @@ -3,27 +3,27 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este TV Samsung. Verifica la configuraci\u00f3n del Administrador de dispositivos externos de tu TV para autorizar a Home Assistant.", + "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a esta TV Samsung. Verifica la configuraci\u00f3n del Administrador de dispositivos externos de tu TV para autorizar a Home Assistant.", "cannot_connect": "No se pudo conectar", "id_missing": "Este dispositivo Samsung no tiene un n\u00famero de serie.", - "not_supported": "Este dispositivo Samsung no es actualmente compatible.", + "not_supported": "Este dispositivo Samsung no es compatible actualmente.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" }, "error": { - "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a este TV Samsung. Verifica la configuraci\u00f3n del Administrador de dispositivos externos de tu TV para autorizar a Home Assistant.", + "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a esta TV Samsung. Verifica la configuraci\u00f3n del Administrador de dispositivos externos de tu TV para autorizar a Home Assistant.", "invalid_pin": "El PIN no es v\u00e1lido, por favor, int\u00e9ntalo de nuevo." }, "flow_title": "{device}", "step": { "confirm": { - "description": "\u00bfQuieres configurar {device}? Si nunca la has conectado a Home Assistant antes deber\u00edas ver una ventana en tu TV pidiendo autorizaci\u00f3n." + "description": "\u00bfQuieres configurar {device}? Si nunca la has conectado a Home Assistant antes, deber\u00edas ver una ventana en tu TV pidiendo autorizaci\u00f3n." }, "encrypted_pairing": { "description": "Por favor, introduce el PIN que aparece en {device}." }, "pairing": { - "description": "\u00bfQuieres configurar {device}? Si nunca la has conectado a Home Assistant antes deber\u00edas ver una ventana en tu TV pidiendo autorizaci\u00f3n." + "description": "\u00bfQuieres configurar {device}? Si nunca la has conectado a Home Assistant antes, deber\u00edas ver una ventana en tu TV pidiendo autorizaci\u00f3n." }, "reauth_confirm": { "description": "Despu\u00e9s de enviar, acepta la ventana emergente en {device} solicitando autorizaci\u00f3n dentro de los 30 segundos o introduce el PIN." @@ -36,7 +36,7 @@ "host": "Host", "name": "Nombre" }, - "description": "Introduce la informaci\u00f3n de tu televisi\u00f3n Samsung. Si nunca antes te conectaste con Home Assistant, deber\u00edas ver un mensaje en tu televisi\u00f3n pidiendo autorizaci\u00f3n." + "description": "Introduce la informaci\u00f3n de tu TV Samsung. Si nunca la has conectado a Home Assistant antes, deber\u00edas ver una ventana emergente en tu TV solicitando autorizaci\u00f3n." } } } diff --git a/homeassistant/components/schedule/translations/hu.json b/homeassistant/components/schedule/translations/hu.json index 44b70c0497b..c543b7b2e07 100644 --- a/homeassistant/components/schedule/translations/hu.json +++ b/homeassistant/components/schedule/translations/hu.json @@ -5,5 +5,5 @@ "on": "Be" } }, - "title": "Id\u0151z\u00edt\u00e9s" + "title": "Id\u0151z\u00edt\u0151" } \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/it.json b/homeassistant/components/schedule/translations/it.json new file mode 100644 index 00000000000..c50d8a66d1c --- /dev/null +++ b/homeassistant/components/schedule/translations/it.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Spento", + "on": "Acceso" + } + }, + "title": "Programma" +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/es.json b/homeassistant/components/sensor/translations/es.json index a99b59dceae..fd8a1581530 100644 --- a/homeassistant/components/sensor/translations/es.json +++ b/homeassistant/components/sensor/translations/es.json @@ -31,15 +31,15 @@ }, "trigger_type": { "apparent_power": "{entity_name} ha cambiado de potencia aparente", - "battery_level": "Cambios de nivel de bater\u00eda de {entity_name}", + "battery_level": "El nivel de bater\u00eda de {entity_name} cambia", "carbon_dioxide": "{entity_name} cambios en la concentraci\u00f3n de di\u00f3xido de carbono", "carbon_monoxide": "{entity_name} cambios en la concentraci\u00f3n de mon\u00f3xido de carbono", "current": "Cambio de corriente en {entity_name}", "energy": "Cambio de energ\u00eda en {entity_name}", "frequency": "{entity_name} ha cambiado de frecuencia", "gas": "{entity_name} ha hambiado gas", - "humidity": "Cambios de humedad de {entity_name}", - "illuminance": "Cambios de luminosidad de {entity_name}", + "humidity": "La humedad de {entity_name} cambia", + "illuminance": "La luminosidad de {entity_name} cambia", "nitrogen_dioxide": "{entity_name} ha cambiado en la concentraci\u00f3n de di\u00f3xido de nitr\u00f3geno", "nitrogen_monoxide": "{entity_name} ha cambiado en la concentraci\u00f3n de mon\u00f3xido de nitr\u00f3geno", "nitrous_oxide": "{entity_name} ha cambiado en la concentraci\u00f3n de \u00f3xido nitroso", @@ -47,14 +47,14 @@ "pm1": "{entity_name} ha cambiado en la concentraci\u00f3n de PM1", "pm10": "{entity_name} ha cambiado en la concentraci\u00f3n de PM10", "pm25": "{entity_name} ha cambiado en la concentraci\u00f3n de PM2.5", - "power": "Cambios de potencia de {entity_name}", + "power": "La potencia de {entity_name} cambia", "power_factor": "Cambio de factor de potencia en {entity_name}", - "pressure": "Cambios de presi\u00f3n de {entity_name}", + "pressure": "La presi\u00f3n de {entity_name} cambia", "reactive_power": "{entity_name} ha cambiado de potencia reactiva", - "signal_strength": "cambios de la intensidad de se\u00f1al de {entity_name}", + "signal_strength": "La intensidad de se\u00f1al de {entity_name} cambia", "sulphur_dioxide": "{entity_name} ha cambiado en la concentraci\u00f3n de di\u00f3xido de azufre", - "temperature": "{entity_name} cambios de temperatura", - "value": "Cambios de valor de la {entity_name}", + "temperature": "La temperatura de {entity_name} cambia", + "value": "El valor de {entity_name} cambia", "volatile_organic_compounds": "{entity_name} ha cambiado la concentraci\u00f3n de compuestos org\u00e1nicos vol\u00e1tiles", "voltage": "Cambio de voltaje en {entity_name}" } diff --git a/homeassistant/components/sentry/translations/es.json b/homeassistant/components/sentry/translations/es.json index 058cc27642f..b27f797e8b4 100644 --- a/homeassistant/components/sentry/translations/es.json +++ b/homeassistant/components/sentry/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { "bad_dsn": "DSN no v\u00e1lido", @@ -20,13 +20,13 @@ "init": { "data": { "environment": "Nombre opcional del entorno.", - "event_custom_components": "Env\u00eda eventos desde componentes personalizados", - "event_handled": "Enviar eventos controlados", - "event_third_party_packages": "Env\u00eda eventos desde paquetes de terceros", - "logging_event_level": "El nivel de registro Sentry registrar\u00e1 un evento para", - "logging_level": "El nivel de registro Sentry registrar\u00e1 registros como migas de pan para", - "tracing": "Habilitar el seguimiento del rendimiento", - "tracing_sample_rate": "Seguimiento de la frecuencia de muestreo; entre 0.0 y 1.0 (1.0 = 100%)" + "event_custom_components": "Enviar eventos desde componentes personalizados", + "event_handled": "Enviar eventos gestionados", + "event_third_party_packages": "Enviar eventos desde paquetes de terceros", + "logging_event_level": "El nivel de registro para el que Sentry registrar\u00e1 un evento", + "logging_level": "El nivel de registro para el que Sentry guardar\u00e1 registros como migas de pan", + "tracing": "Habilitar traza del rendimiento", + "tracing_sample_rate": "Frecuencia de muestreo de la traza; entre 0,0 y 1,0 (1,0 = 100%)" } } } diff --git a/homeassistant/components/senz/translations/es.json b/homeassistant/components/senz/translations/es.json index fc1c1ae2d13..e08ed208ac6 100644 --- a/homeassistant/components/senz/translations/es.json +++ b/homeassistant/components/senz/translations/es.json @@ -3,8 +3,8 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "oauth_error": "Se han recibido datos de token no v\u00e1lidos." }, diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index 9d344f83992..585ec778f7c 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/translations/es.json @@ -13,7 +13,7 @@ "flow_title": "{name}", "step": { "confirm_discovery": { - "description": "\u00bfQuieres configurar {model} en {host}? \n\nLos dispositivos alimentados por bater\u00eda que est\u00e1n protegidos con contrase\u00f1a deben despertarse antes de continuar con la configuraci\u00f3n.\nLos dispositivos que funcionan con bater\u00eda que no est\u00e1n protegidos con contrase\u00f1a se agregar\u00e1n cuando el dispositivo se despierte, puedes activar manualmente el dispositivo ahora con un bot\u00f3n o esperar la pr\u00f3xima actualizaci\u00f3n de datos del dispositivo." + "description": "\u00bfQuieres configurar {model} en {host}? \n\nLos dispositivos alimentados por bater\u00eda que est\u00e1n protegidos con contrase\u00f1a deben despertarse antes de continuar con la configuraci\u00f3n.\nLos dispositivos que funcionan con bater\u00eda que no est\u00e1n protegidos con contrase\u00f1a se agregar\u00e1n cuando el dispositivo se despierte. Puedes activar manualmente el dispositivo ahora con un bot\u00f3n del mismo o esperar a la pr\u00f3xima actualizaci\u00f3n de datos del dispositivo." }, "credentials": { "data": { @@ -25,7 +25,7 @@ "data": { "host": "Host" }, - "description": "Antes de configurar, los dispositivos alimentados por bater\u00eda deben despertarse, puede despertar el dispositivo ahora con un bot\u00f3n." + "description": "Antes de configurarlos, los dispositivos alimentados por bater\u00eda deben despertarse. Puedes despertar el dispositivo ahora usando uno de sus botones." } } }, @@ -42,7 +42,7 @@ "btn_up": "Bot\u00f3n {subtype} soltado", "double": "Pulsaci\u00f3n doble de {subtype}", "double_push": "Pulsaci\u00f3n doble de {subtype}", - "long": "{subtype} con pulsaci\u00f3n larga", + "long": "Pulsaci\u00f3n larga de {subtype}", "long_push": "Pulsaci\u00f3n larga de {subtype}", "long_single": "Pulsaci\u00f3n larga de {subtype} seguida de una pulsaci\u00f3n simple", "single": "Pulsaci\u00f3n simple de {subtype}", diff --git a/homeassistant/components/simplisafe/translations/es.json b/homeassistant/components/simplisafe/translations/es.json index 1558f96fa4f..842a2245fe3 100644 --- a/homeassistant/components/simplisafe/translations/es.json +++ b/homeassistant/components/simplisafe/translations/es.json @@ -8,7 +8,7 @@ }, "error": { "identifier_exists": "Cuenta ya registrada", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_auth_code_length": "Los c\u00f3digos de autorizaci\u00f3n de SimpliSafe tienen 45 caracteres de longitud", "unknown": "Error inesperado" }, @@ -20,8 +20,8 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Vuelve a introducir la contrase\u00f1a de {username}", - "title": "Reautenticaci\u00f3n de la integraci\u00f3n" + "description": "Por favor, vuelve a introducir la contrase\u00f1a de {username}", + "title": "Volver a autenticar la integraci\u00f3n" }, "sms_2fa": { "data": { @@ -43,7 +43,7 @@ "step": { "init": { "data": { - "code": "C\u00f3digo (utilizado en el interfaz de usuario de Home Assistant)" + "code": "C\u00f3digo (utilizado en la IU de Home Assistant)" }, "title": "Configurar SimpliSafe" } diff --git a/homeassistant/components/smappee/translations/es.json b/homeassistant/components/smappee/translations/es.json index 1a9e2ee28dd..ebffb459169 100644 --- a/homeassistant/components/smappee/translations/es.json +++ b/homeassistant/components/smappee/translations/es.json @@ -3,17 +3,17 @@ "abort": { "already_configured_device": "El dispositivo ya est\u00e1 configurado", "already_configured_local_device": "Los dispositivos locales ya est\u00e1n configurados. Elim\u00ednelos primero antes de configurar un dispositivo en la nube.", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "cannot_connect": "No se pudo conectar", - "invalid_mdns": "Dispositivo no compatible para la integraci\u00f3n de Smappee.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "invalid_mdns": "Dispositivo no compatible con la integraci\u00f3n de Smappee.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" }, "flow_title": "{name}", "step": { "environment": { "data": { - "environment": "Ambiente" + "environment": "Entorno" }, "description": "Configura tu Smappee para que se integre con Home Assistant." }, @@ -21,7 +21,7 @@ "data": { "host": "Host" }, - "description": "Ingrese el host para iniciar la integraci\u00f3n local de Smappee" + "description": "Introduce el host para iniciar la integraci\u00f3n local de Smappee" }, "pick_implementation": { "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" diff --git a/homeassistant/components/smartthings/translations/es.json b/homeassistant/components/smartthings/translations/es.json index 8aee6337cef..b77cb803205 100644 --- a/homeassistant/components/smartthings/translations/es.json +++ b/homeassistant/components/smartthings/translations/es.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "invalid_webhook_url": "Home Assistant no est\u00e1 configurado correctamente para recibir actualizaciones de SmartThings. La URL del webhook no es v\u00e1lida: \n > {webhook_url} \n\n Actualiza tu configuraci\u00f3n seg\u00fan las [instrucciones]({component_url}), reinicia Home Assistant e int\u00e9ntalo de nuevo.", + "invalid_webhook_url": "Home Assistant no est\u00e1 configurado correctamente para recibir actualizaciones de SmartThings. La URL del webhook no es v\u00e1lida:\n> {webhook_url} \n\nActualiza tu configuraci\u00f3n seg\u00fan las [instrucciones]({component_url}), reinicia Home Assistant y vuelve a intentarlo.", "no_available_locations": "No hay Ubicaciones SmartThings disponibles para configurar en Home Assistant." }, "error": { - "app_setup_error": "No se pudo configurar el SmartApp. Por favor, int\u00e9ntelo de nuevo.", + "app_setup_error": "No se puede configurar la SmartApp. Por favor, int\u00e9ntalo de nuevo.", "token_forbidden": "El token no tiene los \u00e1mbitos de OAuth necesarios.", "token_invalid_format": "El token debe estar en formato UID/GUID", "token_unauthorized": "El token no es v\u00e1lido o ya no est\u00e1 autorizado.", diff --git a/homeassistant/components/solaredge/translations/es.json b/homeassistant/components/solaredge/translations/es.json index d152481ae0d..71151cfbe1f 100644 --- a/homeassistant/components/solaredge/translations/es.json +++ b/homeassistant/components/solaredge/translations/es.json @@ -14,7 +14,7 @@ "data": { "api_key": "Clave API", "name": "El nombre de esta instalaci\u00f3n", - "site_id": "La identificaci\u00f3n del sitio de SolarEdge" + "site_id": "El ID del sitio de SolarEdge" }, "title": "Definir los par\u00e1metros de la API para esta instalaci\u00f3n" } diff --git a/homeassistant/components/solarlog/translations/es.json b/homeassistant/components/solarlog/translations/es.json index a5d8b41ba49..0324b8ee5e6 100644 --- a/homeassistant/components/solarlog/translations/es.json +++ b/homeassistant/components/solarlog/translations/es.json @@ -11,9 +11,9 @@ "user": { "data": { "host": "Host", - "name": "El prefijo que se utilizar\u00e1 para los sensores Solar-Log" + "name": "El prefijo que se utilizar\u00e1 para tus sensores Solar-Log" }, - "title": "Defina su conexi\u00f3n Solar-Log" + "title": "Define tu conexi\u00f3n Solar-Log" } } } diff --git a/homeassistant/components/soma/translations/es.json b/homeassistant/components/soma/translations/es.json index 0e2312bd335..f57970c4f77 100644 --- a/homeassistant/components/soma/translations/es.json +++ b/homeassistant/components/soma/translations/es.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_setup": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "connection_error": "No se pudo conectar", - "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, leer la documentaci\u00f3n.", - "result_error": "SOMA Connect respondi\u00f3 con un error." + "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", + "result_error": "SOMA Connect respondi\u00f3 con un estado de error." }, "create_entry": { - "default": "Autenticaci\u00f3n exitosa" + "default": "Autenticado correctamente" }, "step": { "user": { @@ -16,7 +16,7 @@ "host": "Host", "port": "Puerto" }, - "description": "Por favor, introduzca los ajustes de conexi\u00f3n de SOMA Connect.", + "description": "Por favor, introduce la configuraci\u00f3n de conexi\u00f3n de tu SOMA Connect.", "title": "SOMA Connect" } } diff --git a/homeassistant/components/sonarr/translations/es.json b/homeassistant/components/sonarr/translations/es.json index 3d8fc33de59..eee1e025e36 100644 --- a/homeassistant/components/sonarr/translations/es.json +++ b/homeassistant/components/sonarr/translations/es.json @@ -12,8 +12,8 @@ "flow_title": "{name}", "step": { "reauth_confirm": { - "description": "La integraci\u00f3n de Sonarr debe volver a autenticarse manualmente con la API de Sonarr alojada en: {url}", - "title": "Reautenticaci\u00f3n de la integraci\u00f3n" + "description": "La integraci\u00f3n Sonarr debe volver a autenticarse manualmente con la API de Sonarr alojada en: {url}", + "title": "Volver a autenticar la integraci\u00f3n" }, "user": { "data": { diff --git a/homeassistant/components/songpal/translations/es.json b/homeassistant/components/songpal/translations/es.json index 153d107eacd..eb3abdea6a0 100644 --- a/homeassistant/components/songpal/translations/es.json +++ b/homeassistant/components/songpal/translations/es.json @@ -14,7 +14,7 @@ }, "user": { "data": { - "endpoint": "Endpoint" + "endpoint": "Extremo" } } } diff --git a/homeassistant/components/soundtouch/translations/es.json b/homeassistant/components/soundtouch/translations/es.json index 0ee2968bf09..c8c13367ca4 100644 --- a/homeassistant/components/soundtouch/translations/es.json +++ b/homeassistant/components/soundtouch/translations/es.json @@ -20,7 +20,7 @@ }, "issues": { "deprecated_yaml": { - "description": "Se va a eliminar la configuraci\u00f3n de Bose SoundTouch mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimine la configuraci\u00f3n YAML de Bose SoundTouch de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se va a eliminar la configuraci\u00f3n de Bose SoundTouch mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Bose SoundTouch de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la configuraci\u00f3n YAML de Bose SoundTouch" } } diff --git a/homeassistant/components/speedtestdotnet/translations/es.json b/homeassistant/components/speedtestdotnet/translations/es.json index 90fe06ccf55..9ba5fcbd4bb 100644 --- a/homeassistant/components/speedtestdotnet/translations/es.json +++ b/homeassistant/components/speedtestdotnet/translations/es.json @@ -15,7 +15,7 @@ "data": { "manual": "Desactivar actualizaci\u00f3n autom\u00e1tica", "scan_interval": "Frecuencia de actualizaci\u00f3n (minutos)", - "server_name": "Seleccione el servidor de prueba" + "server_name": "Selecciona el servidor de prueba" } } } diff --git a/homeassistant/components/spotify/translations/es.json b/homeassistant/components/spotify/translations/es.json index 377c2a9df58..edb2ff4bf82 100644 --- a/homeassistant/components/spotify/translations/es.json +++ b/homeassistant/components/spotify/translations/es.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", - "missing_configuration": "La integraci\u00f3n de Spotify no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "La integraci\u00f3n de Spotify no est\u00e1 configurada. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "reauth_account_mismatch": "La cuenta de Spotify con la que est\u00e1s autenticado, no coincide con la cuenta necesaria para re-autenticaci\u00f3n." }, "create_entry": { - "default": "Autentificado con \u00e9xito con Spotify." + "default": "Autenticado con \u00e9xito con Spotify." }, "step": { "pick_implementation": { @@ -27,7 +27,7 @@ }, "system_health": { "info": { - "api_endpoint_reachable": "Se puede acceder al punto de conexi\u00f3n de la API de Spotify" + "api_endpoint_reachable": "Se puede llegar al punto de conexi\u00f3n de la API de Spotify" } } } \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/es.json b/homeassistant/components/squeezebox/translations/es.json index b6745920acd..b8e8965b621 100644 --- a/homeassistant/components/squeezebox/translations/es.json +++ b/homeassistant/components/squeezebox/translations/es.json @@ -19,7 +19,7 @@ "port": "Puerto", "username": "Nombre de usuario" }, - "title": "Editar la informaci\u00f3n de conexi\u00f3n" + "title": "Edita la informaci\u00f3n de conexi\u00f3n" }, "user": { "data": { diff --git a/homeassistant/components/srp_energy/translations/es.json b/homeassistant/components/srp_energy/translations/es.json index b82a4e6f6f6..eed1c040338 100644 --- a/homeassistant/components/srp_energy/translations/es.json +++ b/homeassistant/components/srp_energy/translations/es.json @@ -13,7 +13,7 @@ "user": { "data": { "id": "ID de la cuenta", - "is_tou": "Es el plan de tiempo de uso", + "is_tou": "Es un plan por Tiempo de uso", "password": "Contrase\u00f1a", "username": "Nombre de usuario" } diff --git a/homeassistant/components/switch/translations/es.json b/homeassistant/components/switch/translations/es.json index a605ceb238b..5c72f292911 100644 --- a/homeassistant/components/switch/translations/es.json +++ b/homeassistant/components/switch/translations/es.json @@ -6,8 +6,8 @@ "turn_on": "Encender {entity_name}" }, "condition_type": { - "is_off": "{entity_name} est\u00e1 apagada", - "is_on": "{entity_name} est\u00e1 encendida" + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 encendido" }, "trigger_type": { "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", diff --git a/homeassistant/components/switchbot/translations/it.json b/homeassistant/components/switchbot/translations/it.json index bcd6465acae..3592ce065e3 100644 --- a/homeassistant/components/switchbot/translations/it.json +++ b/homeassistant/components/switchbot/translations/it.json @@ -13,6 +13,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "Vuoi configurare {name}?" + }, + "password": { + "data": { + "password": "Password" + }, + "description": "Il dispositivo {name} richiede una password" + }, "user": { "data": { "address": "Indirizzo del dispositivo", diff --git a/homeassistant/components/syncthru/translations/es.json b/homeassistant/components/syncthru/translations/es.json index da3002496db..2aa03a63d20 100644 --- a/homeassistant/components/syncthru/translations/es.json +++ b/homeassistant/components/syncthru/translations/es.json @@ -6,7 +6,7 @@ "error": { "invalid_url": "URL no v\u00e1lida", "syncthru_not_supported": "El dispositivo no es compatible con SyncThru", - "unknown_state": "Estado de la impresora desconocido, verifica la URL y la conectividad de la red" + "unknown_state": "Estado de la impresora desconocido, verifica la URL y la conectividad de red" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/synology_dsm/translations/es.json b/homeassistant/components/synology_dsm/translations/es.json index af0d17d7d39..817001cdc20 100644 --- a/homeassistant/components/synology_dsm/translations/es.json +++ b/homeassistant/components/synology_dsm/translations/es.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado.", + "already_configured": "El dispositivo ya est\u00e1 configurado", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "reconfigure_successful": "La reconfiguraci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "missing_data": "Faltan datos: por favor, vuelva a intentarlo m\u00e1s tarde o pruebe con otra configuraci\u00f3n", - "otp_failed": "La autenticaci\u00f3n de dos pasos fall\u00f3, vuelva a intentar con un nuevo c\u00f3digo de acceso", + "missing_data": "Faltan datos: por favor, vuelve a intentarlo m\u00e1s tarde o intenta otra configuraci\u00f3n", + "otp_failed": "La autenticaci\u00f3n en dos pasos fall\u00f3, vuelve a intentarlo con un nuevo c\u00f3digo de acceso", "unknown": "Error inesperado" }, "flow_title": "{name} ({host})", diff --git a/homeassistant/components/system_health/translations/es.json b/homeassistant/components/system_health/translations/es.json index ada0964a358..9015f0899ac 100644 --- a/homeassistant/components/system_health/translations/es.json +++ b/homeassistant/components/system_health/translations/es.json @@ -1,3 +1,3 @@ { - "title": "Estado del sistema" + "title": "Salud del Sistema" } \ No newline at end of file diff --git a/homeassistant/components/tado/translations/es.json b/homeassistant/components/tado/translations/es.json index 42090cccb15..e7acf6d3d95 100644 --- a/homeassistant/components/tado/translations/es.json +++ b/homeassistant/components/tado/translations/es.json @@ -15,7 +15,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "title": "Conectar con tu cuenta de Tado" + "title": "Conectar con tu cuenta Tado" } } }, @@ -23,7 +23,7 @@ "step": { "init": { "data": { - "fallback": "Elige el modo alternativo." + "fallback": "Elige el modo de respaldo." }, "description": "El modo de respaldo te permite elegir cu\u00e1ndo retroceder a Smart Schedule desde tu superposici\u00f3n de zona manual. (NEXT_TIME_BLOCK:= Cambiar en el pr\u00f3ximo cambio de Smart Schedule; MANUAL:= No cambiar hasta que canceles; TADO_DEFAULT:= Cambiar seg\u00fan tu configuraci\u00f3n en la aplicaci\u00f3n Tado).", "title": "Ajustar las opciones de Tado" diff --git a/homeassistant/components/tellduslive/translations/es.json b/homeassistant/components/tellduslive/translations/es.json index fbda5550ac3..2fd67d46d01 100644 --- a/homeassistant/components/tellduslive/translations/es.json +++ b/homeassistant/components/tellduslive/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "unknown": "Error inesperado", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, diff --git a/homeassistant/components/tibber/translations/es.json b/homeassistant/components/tibber/translations/es.json index 53c0c2aaab7..349f35a05f8 100644 --- a/homeassistant/components/tibber/translations/es.json +++ b/homeassistant/components/tibber/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_access_token": "Token de acceso inv\u00e1lido", + "invalid_access_token": "Token de acceso no v\u00e1lido", "timeout": "Tiempo de espera para conectarse a Tibber" }, "step": { diff --git a/homeassistant/components/toon/translations/es.json b/homeassistant/components/toon/translations/es.json index 423d81dde0c..89f85a222a1 100644 --- a/homeassistant/components/toon/translations/es.json +++ b/homeassistant/components/toon/translations/es.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_configured": "El acuerdo seleccionado ya est\u00e1 configurado.", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_agreements": "Esta cuenta no tiene pantallas Toon.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." @@ -17,7 +17,7 @@ "title": "Selecciona tu acuerdo" }, "pick_implementation": { - "title": "Elige el arrendatario con el cual deseas autenticarte" + "title": "Elige tu inquilino con el que autenticarse" } } } diff --git a/homeassistant/components/traccar/translations/es.json b/homeassistant/components/traccar/translations/es.json index 21e90275d17..1177250f930 100644 --- a/homeassistant/components/traccar/translations/es.json +++ b/homeassistant/components/traccar/translations/es.json @@ -6,12 +6,12 @@ "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, "create_entry": { - "default": "Para enviar eventos a Home Assistant, tendr\u00e1s que configurar la opci\u00f3n webhook de Traccar.\n\nUtilice la siguiente url: `{webhook_url}`\n\nConsulte la [documentaci\u00f3n]({docs_url}) para m\u00e1s detalles." + "default": "Para enviar eventos a Home Assistant, deber\u00e1s configurar la funci\u00f3n de webhook en Traccar. \n\nUtiliza la siguiente URL: `{webhook_url}` \n\nConsulta [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s detalles." }, "step": { "user": { "description": "\u00bfEst\u00e1s seguro de que quieres configurar Traccar?", - "title": "Configura Traccar" + "title": "Configurar Traccar" } } } diff --git a/homeassistant/components/transmission/translations/es.json b/homeassistant/components/transmission/translations/es.json index 7ae9bfa87fa..d722d0a528a 100644 --- a/homeassistant/components/transmission/translations/es.json +++ b/homeassistant/components/transmission/translations/es.json @@ -25,7 +25,7 @@ "port": "Puerto", "username": "Nombre de usuario" }, - "title": "Configuraci\u00f3n del cliente de transmisi\u00f3n" + "title": "Configuraci\u00f3n del cliente Transmission" } } }, @@ -34,10 +34,10 @@ "init": { "data": { "limit": "L\u00edmite", - "order": "Pedido", + "order": "Ordenar", "scan_interval": "Frecuencia de actualizaci\u00f3n" }, - "title": "Configurar opciones para la transmisi\u00f3n" + "title": "Configurar opciones para Transmission" } } } diff --git a/homeassistant/components/tuya/translations/es.json b/homeassistant/components/tuya/translations/es.json index 55ab464c3c2..6191d5acf12 100644 --- a/homeassistant/components/tuya/translations/es.json +++ b/homeassistant/components/tuya/translations/es.json @@ -13,7 +13,7 @@ "password": "Contrase\u00f1a", "username": "Cuenta" }, - "description": "Introduce tus credencial de Tuya" + "description": "Introduce tus credenciales Tuya" } } } diff --git a/homeassistant/components/twentemilieu/translations/es.json b/homeassistant/components/twentemilieu/translations/es.json index 93effca8aad..6bd230b377b 100644 --- a/homeassistant/components/twentemilieu/translations/es.json +++ b/homeassistant/components/twentemilieu/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_address": "No se ha encontrado la direcci\u00f3n en el \u00e1rea de servicio de Twente Milieu." + "invalid_address": "Direcci\u00f3n no encontrada en el \u00e1rea de servicio de Twente Milieu." }, "step": { "user": { @@ -14,7 +14,7 @@ "house_number": "N\u00famero de casa", "post_code": "C\u00f3digo postal" }, - "description": "Configura Twente Milieu con informaci\u00f3n de la recogida de residuos en tu direcci\u00f3n." + "description": "Configura Twente Milieu proporcionando informaci\u00f3n de recogida de residuos en tu direcci\u00f3n." } } } diff --git a/homeassistant/components/unifi/translations/es.json b/homeassistant/components/unifi/translations/es.json index ff9b9575b78..1bc3e90ac91 100644 --- a/homeassistant/components/unifi/translations/es.json +++ b/homeassistant/components/unifi/translations/es.json @@ -36,16 +36,16 @@ "dpi_restrictions": "Permitir el control de los grupos de restricci\u00f3n de DPI", "poe_clients": "Permitir control PoE de clientes" }, - "description": "Configurar controles de cliente\n\nCrea conmutadores para los n\u00fameros de serie para los que deseas controlar el acceso a la red.", + "description": "Configurar controles de cliente \n\nCrea interruptores para los n\u00fameros de serie para los que quieras controlar el acceso a la red.", "title": "Opciones de UniFi Network 2/3" }, "device_tracker": { "data": { - "detection_time": "Tiempo en segundos desde la \u00faltima vez que se vio hasta considerarlo desconectado", - "ignore_wired_bug": "Desactiva la l\u00f3gica de errores de UniFi Network", - "ssid_filter": "Seleccione los SSIDs para realizar seguimiento de clientes inal\u00e1mbricos", + "detection_time": "Tiempo en segundos desde la \u00faltima vez que se vio hasta que se considera ausente", + "ignore_wired_bug": "Deshabilitar la l\u00f3gica de errores cableada de UniFi Network", + "ssid_filter": "Selecciona los SSIDs para realizar el seguimiento de clientes inal\u00e1mbricos", "track_clients": "Seguimiento de los clientes de red", - "track_devices": "Rastree dispositivos de red (dispositivos Ubiquiti)", + "track_devices": "Seguimiento de dispositivos de red (dispositivos Ubiquiti)", "track_wired_clients": "Incluir clientes de red cableada" }, "description": "Configurar dispositivo de seguimiento", @@ -53,15 +53,15 @@ }, "simple_options": { "data": { - "block_client": "Acceso controlado a la red de los clientes", - "track_clients": "Rastree clientes de red", - "track_devices": "Rastree dispositivos de red (dispositivos Ubiquiti)" + "block_client": "Clientes con acceso controlado a la red", + "track_clients": "Seguimiento de los clientes de red", + "track_devices": "Seguimiento de dispositivos de red (dispositivos Ubiquiti)" }, "description": "Configura la integraci\u00f3n UniFi Network" }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Sensores de uso de ancho de banda para los clientes de la red", + "allow_bandwidth_sensors": "Sensores de uso de ancho de banda para clientes de red", "allow_uptime_sensors": "Sensores de tiempo de actividad para clientes de la red" }, "description": "Configurar estad\u00edsticas de los sensores", diff --git a/homeassistant/components/upb/translations/es.json b/homeassistant/components/upb/translations/es.json index 7cbf3446c6a..4b4a44ce0d3 100644 --- a/homeassistant/components/upb/translations/es.json +++ b/homeassistant/components/upb/translations/es.json @@ -11,11 +11,11 @@ "step": { "user": { "data": { - "address": "Direcci\u00f3n (v\u00e9ase la descripci\u00f3n anterior)", + "address": "Direcci\u00f3n (consulta la descripci\u00f3n anterior)", "file_path": "Ruta y nombre del archivo de exportaci\u00f3n UPStart UPB.", "protocol": "Protocolo" }, - "description": "Conecte un M\u00f3dulo de Interfaz Universal Powerline Bus Powerline (UPB PIM). La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n [: puerto]' para 'tcp'. El puerto es opcional y el valor predeterminado es 2101. Ejemplo: '192.168.1.42'. Para el protocolo serie, la direcci\u00f3n debe estar en la forma 'tty [: baudios]'. El baud es opcional y el valor predeterminado es 4800. Ejemplo: '/ dev / ttyS1'.", + "description": "Conecta un Universal Powerline Bus Powerline Interface Module (UPB PIM). La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n[:puerto]' para 'tcp'. El puerto es opcional y el valor predeterminado es 2101. Ejemplo: '192.168.1.42'. Para el protocolo serial, la direcci\u00f3n debe tener el formato 'tty[:baudios]'. El par\u00e1metro baudios es opcional y el valor predeterminado es 4800. Ejemplo: '/dev/ttyS1'.", "title": "Conectar con UPB PIM" } } diff --git a/homeassistant/components/vacuum/translations/es.json b/homeassistant/components/vacuum/translations/es.json index 87a79a4e5da..c5aa4366e5e 100644 --- a/homeassistant/components/vacuum/translations/es.json +++ b/homeassistant/components/vacuum/translations/es.json @@ -1,8 +1,8 @@ { "device_automation": { "action_type": { - "clean": "Deje que {entity_name} limpie", - "dock": "Deje que {entity_name} regrese a la base" + "clean": "Dejar que {entity_name} limpie", + "dock": "Dejar que {entity_name} regrese a la base" }, "condition_type": { "is_cleaning": "{entity_name} est\u00e1 limpiando", diff --git a/homeassistant/components/velbus/translations/es.json b/homeassistant/components/velbus/translations/es.json index ed2585a03e9..cb600d577e7 100644 --- a/homeassistant/components/velbus/translations/es.json +++ b/homeassistant/components/velbus/translations/es.json @@ -10,10 +10,10 @@ "step": { "user": { "data": { - "name": "Nombre de la conexi\u00f3n Velbus", + "name": "El nombre de esta conexi\u00f3n velbus", "port": "Cadena de conexi\u00f3n" }, - "title": "Tipo de conexi\u00f3n Velbus" + "title": "Definir el tipo de conexi\u00f3n velbus" } } } diff --git a/homeassistant/components/vera/translations/es.json b/homeassistant/components/vera/translations/es.json index 8d9686b0a13..8aacb4f6a96 100644 --- a/homeassistant/components/vera/translations/es.json +++ b/homeassistant/components/vera/translations/es.json @@ -6,8 +6,8 @@ "step": { "user": { "data": { - "exclude": "Identificadores de dispositivos Vera a excluir de Home Assistant", - "lights": "Identificadores de interruptores Vera que deben ser tratados como luces en Home Assistant", + "exclude": "IDs de dispositivos Vera para excluir de Home Assistant.", + "lights": "IDs de dispositivos interruptores Vera para tratarlos como luces en Home Assistant.", "vera_controller_url": "URL del controlador" }, "data_description": { @@ -20,10 +20,10 @@ "step": { "init": { "data": { - "exclude": "Identificadores de dispositivos Vera a excluir de Home Assistant", + "exclude": "IDs de dispositivos Vera para excluir de Home Assistant.", "lights": "Identificadores de interruptores Vera que deben ser tratados como luces en Home Assistant" }, - "description": "Consulte la documentaci\u00f3n de Vera para obtener detalles sobre los par\u00e1metros opcionales: https://www.home-assistant.io/integrations/vera/. Nota: Cualquier cambio aqu\u00ed necesitar\u00e1 un reinicio del servidor de Home Assistant. Para borrar valores, introduce un espacio.", + "description": "Consulta la documentaci\u00f3n de vera para obtener detalles sobre los par\u00e1metros opcionales: https://www.home-assistant.io/integrations/vera/. Nota: Cualquier cambio aqu\u00ed necesitar\u00e1 un reinicio en el servidor Home Assistant. Para borrar valores, proporciona un espacio.", "title": "Opciones del controlador Vera" } } diff --git a/homeassistant/components/vesync/translations/es.json b/homeassistant/components/vesync/translations/es.json index dd5f95a2e13..4f03e42dec0 100644 --- a/homeassistant/components/vesync/translations/es.json +++ b/homeassistant/components/vesync/translations/es.json @@ -12,7 +12,7 @@ "password": "Contrase\u00f1a", "username": "Correo electr\u00f3nico" }, - "title": "Introduzca el nombre de usuario y la contrase\u00f1a" + "title": "Introduce el nombre de usuario y la contrase\u00f1a" } } } diff --git a/homeassistant/components/vizio/translations/es.json b/homeassistant/components/vizio/translations/es.json index 6ac88586cad..874f19ab119 100644 --- a/homeassistant/components/vizio/translations/es.json +++ b/homeassistant/components/vizio/translations/es.json @@ -7,23 +7,23 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "complete_pairing_failed": "No se pudo completar el emparejamiento. Aseg\u00farate de que el PIN que has proporcionado es correcto y que el televisor sigue encendido y conectado a la red antes de volver a enviarlo.", - "existing_config_entry_found": "Ya se ha configurado una entrada VIZIO SmartCast Device con el mismo n\u00famero de serie. Debes borrar la entrada existente para configurar \u00e9sta." + "complete_pairing_failed": "No se pudo completar el emparejamiento. Aseg\u00farate de que el PIN que has proporcionado es correcto y que la TV sigue encendida y conectada a la red antes de volver a enviarlo.", + "existing_config_entry_found": "Ya se ha configurado una entrada Dispositivo VIZIO SmartCast con el mismo n\u00famero de serie. Debes borrar la entrada existente para configurar \u00e9sta." }, "step": { "pair_tv": { "data": { "pin": "C\u00f3digo PIN" }, - "description": "Tu TV debe estar mostrando un c\u00f3digo. Escribe ese c\u00f3digo en el formulario y contin\u00faa con el paso siguiente para completar el emparejamiento.", + "description": "Tu TV deber\u00eda mostrar un c\u00f3digo. Introduce ese c\u00f3digo en el formulario y luego contin\u00faa con el siguiente paso para completar el emparejamiento.", "title": "Completar Proceso de Emparejamiento" }, "pairing_complete": { - "description": "Tu VIZIO SmartCast Device ahora est\u00e1 conectado a Home Assistant.", + "description": "Tu Dispositivo VIZIO SmartCast ahora est\u00e1 conectado a Home Assistant.", "title": "Emparejamiento Completado" }, "pairing_complete_import": { - "description": "Tu VIZIO SmartCast Device ahora est\u00e1 conectado a Home Assistant. \n\nTu Token de acceso es '**{access_token}**'.", + "description": "Tu Dispositivo VIZIO SmartCast ahora est\u00e1 conectado a Home Assistant. \n\nTu Token de acceso es '**{access_token}**'.", "title": "Emparejamiento Completado" }, "user": { @@ -34,7 +34,7 @@ "name": "Nombre" }, "description": "Solo se necesita un Token de acceso para TVs. Si est\u00e1s configurando una TV y a\u00fan no tienes un Token de acceso, d\u00e9jalo en blanco para pasar por un proceso de emparejamiento.", - "title": "VIZIO SmartCast Device" + "title": "Dispositivo VIZIO SmartCast" } } }, @@ -46,8 +46,8 @@ "include_or_exclude": "\u00bfIncluir o excluir aplicaciones?", "volume_step": "Tama\u00f1o del paso de volumen" }, - "description": "Si tienes un Smart TV, opcionalmente puedes filtrar su lista de fuentes eligiendo qu\u00e9 aplicaciones incluir o excluir en su lista de fuentes.", - "title": "Actualizar las opciones de VIZIO SmartCast Device" + "description": "Si tiene un Smart TV, puede filtrar opcionalmente tu lista de fuentes eligiendo qu\u00e9 aplicaciones incluir o excluir en tu lista de fuentes.", + "title": "Actualizar las opciones de Dispositivo VIZIO SmartCast" } } } diff --git a/homeassistant/components/weather/translations/es.json b/homeassistant/components/weather/translations/es.json index a6a7336e612..a616bc9ef01 100644 --- a/homeassistant/components/weather/translations/es.json +++ b/homeassistant/components/weather/translations/es.json @@ -9,7 +9,7 @@ "lightning": "Rel\u00e1mpagos", "lightning-rainy": "Rel\u00e1mpagos, lluvioso", "partlycloudy": "Parcialmente nublado", - "pouring": "Lluvia", + "pouring": "Lluvia torrencial", "rainy": "Lluvioso", "snowy": "Nevado", "snowy-rainy": "Nevado, lluvioso", diff --git a/homeassistant/components/wilight/translations/es.json b/homeassistant/components/wilight/translations/es.json index 5836867c0e7..150cb5c91b2 100644 --- a/homeassistant/components/wilight/translations/es.json +++ b/homeassistant/components/wilight/translations/es.json @@ -3,12 +3,12 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "not_supported_device": "Este WiLight no es compatible actualmente", - "not_wilight_device": "Este dispositivo no es un Wilight" + "not_wilight_device": "Este dispositivo no es un WiLight" }, "flow_title": "{name}", "step": { "confirm": { - "description": "Se admiten los siguientes componentes: {componentes}" + "description": "Se admiten los siguientes componentes: {components}" } } } diff --git a/homeassistant/components/withings/translations/es.json b/homeassistant/components/withings/translations/es.json index d90f1ccacc3..07734a7263f 100644 --- a/homeassistant/components/withings/translations/es.json +++ b/homeassistant/components/withings/translations/es.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "Configuraci\u00f3n actualizada para el perfil.", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" }, "create_entry": { - "default": "Autenticado correctamente con Withings." + "default": "Autenticado con \u00e9xito con Withings." }, "error": { "already_configured": "La cuenta ya est\u00e1 configurada" diff --git a/homeassistant/components/wled/translations/es.json b/homeassistant/components/wled/translations/es.json index 0547b1598e3..1b8d0eeeb00 100644 --- a/homeassistant/components/wled/translations/es.json +++ b/homeassistant/components/wled/translations/es.json @@ -18,7 +18,7 @@ }, "zeroconf_confirm": { "description": "\u00bfQuieres a\u00f1adir el WLED `{name}` a Home Assistant?", - "title": "Dispositivo WLED detectado" + "title": "Dispositivo WLED descubierto" } } }, diff --git a/homeassistant/components/wolflink/translations/es.json b/homeassistant/components/wolflink/translations/es.json index 601026f784a..a98fe86a983 100644 --- a/homeassistant/components/wolflink/translations/es.json +++ b/homeassistant/components/wolflink/translations/es.json @@ -13,7 +13,7 @@ "data": { "device_name": "Dispositivo" }, - "title": "Seleccionar dispositivo WOLF" + "title": "Selecciona dispositivo WOLF" }, "user": { "data": { diff --git a/homeassistant/components/wolflink/translations/sensor.es.json b/homeassistant/components/wolflink/translations/sensor.es.json index e321ecb72f4..43f83e8aed6 100644 --- a/homeassistant/components/wolflink/translations/sensor.es.json +++ b/homeassistant/components/wolflink/translations/sensor.es.json @@ -7,8 +7,8 @@ "absenkstop": "Parada de retroceso", "aktiviert": "Activado", "antilegionellenfunktion": "Funci\u00f3n anti legionela", - "at_abschaltung": "Apagado de OT", - "at_frostschutz": "OT protecci\u00f3n contra heladas", + "at_abschaltung": "Apagado OT", + "at_frostschutz": "Protecci\u00f3n contra heladas OT", "aus": "Deshabilitado", "auto": "Autom\u00e1tico", "auto_off_cool": "AutoOffCool", @@ -16,7 +16,7 @@ "automatik_aus": "Apagado autom\u00e1tico", "automatik_ein": "Encendido autom\u00e1tico", "bereit_keine_ladung": "Listo, no est\u00e1 cargando", - "betrieb_ohne_brenner": "Trabajando sin quemador", + "betrieb_ohne_brenner": "Trabajar sin quemador", "cooling": "Enfriamiento", "deaktiviert": "Inactivo", "dhw_prior": "DHWPrior", @@ -24,7 +24,7 @@ "ein": "Habilitado", "estrichtrocknung": "Secado en regla", "externe_deaktivierung": "Desactivaci\u00f3n externa", - "fernschalter_ein": "Mando a distancia activado", + "fernschalter_ein": "Control remoto habilitado", "frost_heizkreis": "Escarcha del circuito de calefacci\u00f3n", "frost_warmwasser": "Heladas de DHW", "frostschutz": "Protecci\u00f3n contra las heladas", @@ -37,15 +37,15 @@ "initialisierung": "Inicializaci\u00f3n", "kalibration": "Calibraci\u00f3n", "kalibration_heizbetrieb": "Calibraci\u00f3n del modo de calefacci\u00f3n", - "kalibration_kombibetrieb": "Calibraci\u00f3n en modo combinado", + "kalibration_kombibetrieb": "Calibraci\u00f3n en modo mixto", "kalibration_warmwasserbetrieb": "Calibraci\u00f3n DHW", "kaskadenbetrieb": "Operaci\u00f3n en cascada", - "kombibetrieb": "Modo combinado", - "kombigerat": "Caldera combinada", - "kombigerat_mit_solareinbindung": "Caldera Combi con integraci\u00f3n solar", - "mindest_kombizeit": "Tiempo m\u00ednimo de combinaci\u00f3n", + "kombibetrieb": "Modo mixto", + "kombigerat": "Caldera mixta", + "kombigerat_mit_solareinbindung": "Caldera mixta con integraci\u00f3n solar", + "mindest_kombizeit": "Tiempo combinado m\u00ednimo", "nachlauf_heizkreispumpe": "Bomba de circuito de calefacci\u00f3n en ejecuci\u00f3n", - "nachspulen": "Enviar enjuague", + "nachspulen": "Post-lavado", "nur_heizgerat": "S\u00f3lo la caldera", "parallelbetrieb": "Modo paralelo", "partymodus": "Modo fiesta", diff --git a/homeassistant/components/xbox/translations/es.json b/homeassistant/components/xbox/translations/es.json index 7b7fe2d32fe..f1b1945c832 100644 --- a/homeassistant/components/xbox/translations/es.json +++ b/homeassistant/components/xbox/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "create_entry": { diff --git a/homeassistant/components/xiaomi_aqara/translations/es.json b/homeassistant/components/xiaomi_aqara/translations/es.json index 41d3ffa8207..6c0667af5d5 100644 --- a/homeassistant/components/xiaomi_aqara/translations/es.json +++ b/homeassistant/components/xiaomi_aqara/translations/es.json @@ -3,13 +3,13 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "not_xiaomi_aqara": "No es un Xiaomi Aqara Gateway, el dispositivo descubierto no coincide con los gateways conocidos" + "not_xiaomi_aqara": "No es un Xiaomi Aqara Gateway, el dispositivo descubierto no coincide con las puertas de enlace conocidas" }, "error": { "discovery_error": "No se pudo descubrir un Xiaomi Aqara Gateway, intenta utilizar la IP del dispositivo que ejecuta HomeAssistant como interfaz", - "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos, consulte https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", - "invalid_interface": "Interfaz de red inv\u00e1lida", - "invalid_key": "Clave del gateway inv\u00e1lida", + "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos , consulta https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "invalid_interface": "Interfaz de red no v\u00e1lida", + "invalid_key": "Clave de puerta de enlace no v\u00e1lida", "invalid_mac": "Direcci\u00f3n Mac no v\u00e1lida" }, "flow_title": "{name}", @@ -18,14 +18,14 @@ "data": { "select_ip": "Direcci\u00f3n IP" }, - "description": "Selecciona la puerta de enlace Xiaomi Aqara que deseas conectar" + "description": "Selecciona el Xiaomi Aqara Gateway que deseas conectar" }, "settings": { "data": { - "key": "La clave de tu gateway", - "name": "Nombre del Gateway" + "key": "La clave de tu puerta de enlace", + "name": "Nombre de la puerta de enlace" }, - "description": "La clave (contrase\u00f1a) se puede obtener con este tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Si no se proporciona la clave solo se podr\u00e1 acceder a los sensores", + "description": "La clave (contrase\u00f1a) se puede recuperar con este tutorial: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Si no se proporciona la llave, solo se podr\u00e1 acceder a los sensores", "title": "Configuraciones opcionales" }, "user": { diff --git a/homeassistant/components/yalexs_ble/translations/de.json b/homeassistant/components/yalexs_ble/translations/de.json index cfd368aadef..416761bd91a 100644 --- a/homeassistant/components/yalexs_ble/translations/de.json +++ b/homeassistant/components/yalexs_ble/translations/de.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "no_unconfigured_devices": "Keine unkonfigurierten Ger\u00e4te gefunden." }, @@ -23,7 +24,7 @@ "key": "Offline-Schl\u00fcssel (32-Byte-Hex-String)", "slot": "Offline-Schl\u00fcssel-Slot (Ganzzahl zwischen 0 und 255)" }, - "description": "Lies in der Dokumentation unter {docs_url} nach, wie du den Offline-Schl\u00fcssel finden kannst." + "description": "Lies in der Dokumentation nach, wie du den Offline-Schl\u00fcssel finden kannst." } } } diff --git a/homeassistant/components/yalexs_ble/translations/es.json b/homeassistant/components/yalexs_ble/translations/es.json index 32863d750d2..adad8a18c40 100644 --- a/homeassistant/components/yalexs_ble/translations/es.json +++ b/homeassistant/components/yalexs_ble/translations/es.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "no_devices_found": "No se encontraron dispositivos en la red", "no_unconfigured_devices": "No se encontraron dispositivos no configurados." }, @@ -23,7 +24,7 @@ "key": "Clave sin conexi\u00f3n (cadena hexadecimal de 32 bytes)", "slot": "Ranura de clave sin conexi\u00f3n (entero entre 0 y 255)" }, - "description": "Consulta la documentaci\u00f3n en {docs_url} para saber c\u00f3mo encontrar la clave sin conexi\u00f3n." + "description": "Consulta la documentaci\u00f3n para saber c\u00f3mo encontrar la clave sin conexi\u00f3n." } } } diff --git a/homeassistant/components/yalexs_ble/translations/fr.json b/homeassistant/components/yalexs_ble/translations/fr.json index 7ec2edfc86e..e073f5afee3 100644 --- a/homeassistant/components/yalexs_ble/translations/fr.json +++ b/homeassistant/components/yalexs_ble/translations/fr.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", "no_unconfigured_devices": "Aucun appareil non configur\u00e9 n'a \u00e9t\u00e9 trouv\u00e9." }, @@ -15,7 +16,8 @@ "user": { "data": { "address": "Adresse Bluetooth" - } + }, + "description": "Consultez la documentation pour d\u00e9couvrir comment trouver la cl\u00e9 hors ligne." } } } diff --git a/homeassistant/components/yalexs_ble/translations/hu.json b/homeassistant/components/yalexs_ble/translations/hu.json index 44e14971cc7..4208d98a1e6 100644 --- a/homeassistant/components/yalexs_ble/translations/hu.json +++ b/homeassistant/components/yalexs_ble/translations/hu.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", "no_unconfigured_devices": "Nem tal\u00e1lhat\u00f3 konfigur\u00e1latlan eszk\u00f6z." }, diff --git a/homeassistant/components/yalexs_ble/translations/id.json b/homeassistant/components/yalexs_ble/translations/id.json index fda1e243e0c..3f2e275e458 100644 --- a/homeassistant/components/yalexs_ble/translations/id.json +++ b/homeassistant/components/yalexs_ble/translations/id.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", "no_unconfigured_devices": "Tidak ditemukan perangkat yang tidak dikonfigurasi." }, @@ -23,7 +24,7 @@ "key": "Kunci Offline (string heksadesimal 32 byte)", "slot": "Slot Kunci Offline (Bilangan Bulat antara 0 dan 255)" }, - "description": "Lihat dokumentasi di {docs_url} untuk mengetahui cara menemukan kunci offline." + "description": "Lihat dokumentasi untuk mengetahui cara menemukan kunci offline." } } } diff --git a/homeassistant/components/yalexs_ble/translations/it.json b/homeassistant/components/yalexs_ble/translations/it.json new file mode 100644 index 00000000000..0ca0e6c96ee --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/it.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "no_unconfigured_devices": "Nessun dispositivo non configurato trovato." + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "invalid_key_format": "La chiave offline deve essere una stringa esadecimale di 32 byte.", + "invalid_key_index": "Lo slot della chiave offline deve essere un numero intero compreso tra 0 e 255.", + "unknown": "Errore imprevisto" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "Vuoi configurare {name} tramite Bluetooth con l'indirizzo {address}?" + }, + "user": { + "data": { + "address": "Indirizzo Bluetooth", + "key": "Chiave offline (stringa esadecimale a 32 byte)", + "slot": "Slot chiave offline (numero intero compreso tra 0 e 255)" + }, + "description": "Controlla la documentazione su come trovare la chiave offline" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/pt-BR.json b/homeassistant/components/yalexs_ble/translations/pt-BR.json index 68a52803cbe..19467e26e36 100644 --- a/homeassistant/components/yalexs_ble/translations/pt-BR.json +++ b/homeassistant/components/yalexs_ble/translations/pt-BR.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede", "no_unconfigured_devices": "Nenhum dispositivo n\u00e3o configurado encontrado." }, @@ -23,7 +24,7 @@ "key": "Chave offline (sequ\u00eancia hexadecimal de 32 bytes)", "slot": "Slot de chave offline (inteiro entre 0 e 255)" }, - "description": "Verifique a documenta\u00e7\u00e3o em {docs_url} para saber como encontrar a chave off-line." + "description": "Verifique a documenta\u00e7\u00e3o para saber como encontrar a chave offline." } } } diff --git a/homeassistant/components/yolink/translations/es.json b/homeassistant/components/yolink/translations/es.json index c4bcfa6e75b..82f45972147 100644 --- a/homeassistant/components/yolink/translations/es.json +++ b/homeassistant/components/yolink/translations/es.json @@ -3,8 +3,8 @@ "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "authorize_url_timeout": "Tiempo de espera agotado durante la generaci\u00f3n de la URL de autorizaci\u00f3n.", - "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n.", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 0378ee6cdd0..6dcc3c84e42 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -87,12 +87,12 @@ }, "trigger_type": { "device_dropped": "Dispositivo ca\u00eddo", - "device_flipped": "Dispositivo volteado \" {subtype} \"", - "device_knocked": "Dispositivo eliminado \" {subtype} \"", - "device_offline": "Dispositivo desconectado", - "device_rotated": "Dispositivo girado \" {subtype} \"", + "device_flipped": "Dispositivo volteado \"{subtype}\"", + "device_knocked": "Dispositivo golpeado \"{subtype}\"", + "device_offline": "Dispositivo sin conexi\u00f3n", + "device_rotated": "Dispositivo girado \"{subtype}\"", "device_shaken": "Dispositivo agitado", - "device_slid": "Dispositivo deslizado \" {subtype} \"", + "device_slid": "Dispositivo deslizado \"{subtype}\"", "device_tilted": "Dispositivo inclinado", "remote_button_alt_double_press": "Bot\u00f3n \"{subtype}\" doble pulsaci\u00f3n (modo Alternativo)", "remote_button_alt_long_press": "Bot\u00f3n \"{subtype}\" pulsado continuamente (modo Alternativo)", @@ -102,14 +102,14 @@ "remote_button_alt_short_press": "Bot\u00f3n \"{subtype}\" pulsado (modo Alternativo)", "remote_button_alt_short_release": "Bot\u00f3n \"{subtype}\" soltado (modo Alternativo)", "remote_button_alt_triple_press": "Bot\u00f3n \"{subtype}\" triple pulsaci\u00f3n (modo Alternativo)", - "remote_button_double_press": "Bot\u00f3n \"{subtype}\" doble pulsaci\u00f3n", + "remote_button_double_press": "Bot\u00f3n \"{subtype}\" pulsado dos veces", "remote_button_long_press": "Bot\u00f3n \"{subtype}\" pulsado continuamente", "remote_button_long_release": "Bot\u00f3n \"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", - "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" cu\u00e1druple pulsaci\u00f3n", - "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" qu\u00edntuple pulsaci\u00f3n", + "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces", + "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces", "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", "remote_button_short_release": "Bot\u00f3n \"{subtype}\" soltado", - "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" triple pulsaci\u00f3n" + "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado tres veces" } } } \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json index 9e3a2f154bc..834c8a1116b 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "addon_get_discovery_info_failed": "Fallo en la obtenci\u00f3n de la informaci\u00f3n de descubrimiento del complemento Z-Wave JS.", + "addon_get_discovery_info_failed": "No se pudo obtener la informaci\u00f3n de descubrimiento del complemento Z-Wave JS.", "addon_info_failed": "No se pudo obtener la informaci\u00f3n del complemento Z-Wave JS.", - "addon_install_failed": "No se ha podido instalar el complemento Z-Wave JS.", - "addon_set_config_failed": "Fallo en la configuraci\u00f3n de Z-Wave JS.", + "addon_install_failed": "No se pudo instalar el complemento Z-Wave JS.", + "addon_set_config_failed": "No se pudo establecer la configuraci\u00f3n de Z-Wave JS.", "addon_start_failed": "No se pudo iniciar el complemento Z-Wave JS.", "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", @@ -13,14 +13,14 @@ "not_zwave_device": "El dispositivo descubierto no es un dispositivo Z-Wave." }, "error": { - "addon_start_failed": "No se pudo iniciar el complemento Z-Wave JS. Comprueba la configuraci\u00f3n.", + "addon_start_failed": "No se pudo iniciar el complemento Z-Wave JS. Verifica la configuraci\u00f3n.", "cannot_connect": "No se pudo conectar", "invalid_ws_url": "URL de websocket no v\u00e1lida", "unknown": "Error inesperado" }, "flow_title": "{name}", "progress": { - "install_addon": "Espera mientras termina la instalaci\u00f3n del complemento Z-Wave JS. Puede tardar varios minutos.", + "install_addon": "Por favor, espera mientras finaliza la instalaci\u00f3n del complemento Z-Wave JS. Esto puede tardar varios minutos.", "start_addon": "Por favor, espera mientras se completa el inicio del complemento Z-Wave JS. Esto puede tardar unos segundos." }, "step": { @@ -33,7 +33,7 @@ "usb_path": "Ruta del dispositivo USB" }, "description": "El complemento generar\u00e1 claves de seguridad si esos campos se dejan vac\u00edos.", - "title": "Introduzca la configuraci\u00f3n del complemento Z-Wave JS" + "title": "Introduce la configuraci\u00f3n del complemento Z-Wave JS" }, "hassio_confirm": { "title": "Configurar la integraci\u00f3n de Z-Wave JS con el complemento Z-Wave JS" @@ -50,7 +50,7 @@ "data": { "use_addon": "Usar el complemento Z-Wave JS Supervisor" }, - "description": "\u00bfQuieres utilizar el complemento Z-Wave JS Supervisor?", + "description": "\u00bfQuieres usar el complemento Z-Wave JS Supervisor?", "title": "Selecciona el m\u00e9todo de conexi\u00f3n" }, "start_addon": { From 0d45cef6f6adce870d0967232fde5a244af958be Mon Sep 17 00:00:00 2001 From: Marvin Wichmann Date: Sun, 14 Aug 2022 06:31:39 +0200 Subject: [PATCH 3328/3516] =?UTF-8?q?Update=20xknx=20to=201.0.0=20?= =?UTF-8?q?=F0=9F=8E=89=20(#76734)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 266eceaacee..0f9b6b4b95a 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.22.1"], + "requirements": ["xknx==1.0.0"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 058218dcefb..85915ca6b9c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2483,7 +2483,7 @@ xboxapi==2.0.1 xiaomi-ble==0.9.0 # homeassistant.components.knx -xknx==0.22.1 +xknx==1.0.0 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 875ad6e7faa..17dfb974aaf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1681,7 +1681,7 @@ xbox-webapi==2.0.11 xiaomi-ble==0.9.0 # homeassistant.components.knx -xknx==0.22.1 +xknx==1.0.0 # homeassistant.components.bluesound # homeassistant.components.fritz From f55c274d8390a2dedce1b16d6883dcb4c799faa7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Aug 2022 21:00:27 -1000 Subject: [PATCH 3329/3516] Add Qingping integration (BLE) (#76598) * Add Qingping integration (BLE) * commit the binary sensor * add binary_sensor file * Update homeassistant/components/qingping/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/qingping/config_flow.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/qingping/sensor.py Co-authored-by: Martin Hjelmare * fix const CONCENTRATION_MICROGRAMS_PER_CUBIC_METER * cover case where config flow is started, another path adds it, and then they resume * fix missed values Co-authored-by: Martin Hjelmare --- CODEOWNERS | 2 + homeassistant/components/qingping/__init__.py | 49 ++++ .../components/qingping/binary_sensor.py | 95 +++++++ .../components/qingping/config_flow.py | 121 +++++++++ homeassistant/components/qingping/const.py | 3 + homeassistant/components/qingping/device.py | 31 +++ .../components/qingping/manifest.json | 11 + homeassistant/components/qingping/sensor.py | 173 +++++++++++++ .../components/qingping/strings.json | 22 ++ .../components/qingping/translations/en.json | 22 ++ homeassistant/generated/bluetooth.py | 4 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/qingping/__init__.py | 40 +++ tests/components/qingping/conftest.py | 8 + .../components/qingping/test_binary_sensor.py | 46 ++++ tests/components/qingping/test_config_flow.py | 234 ++++++++++++++++++ tests/components/qingping/test_sensor.py | 50 ++++ 19 files changed, 918 insertions(+) create mode 100644 homeassistant/components/qingping/__init__.py create mode 100644 homeassistant/components/qingping/binary_sensor.py create mode 100644 homeassistant/components/qingping/config_flow.py create mode 100644 homeassistant/components/qingping/const.py create mode 100644 homeassistant/components/qingping/device.py create mode 100644 homeassistant/components/qingping/manifest.json create mode 100644 homeassistant/components/qingping/sensor.py create mode 100644 homeassistant/components/qingping/strings.json create mode 100644 homeassistant/components/qingping/translations/en.json create mode 100644 tests/components/qingping/__init__.py create mode 100644 tests/components/qingping/conftest.py create mode 100644 tests/components/qingping/test_binary_sensor.py create mode 100644 tests/components/qingping/test_config_flow.py create mode 100644 tests/components/qingping/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 5de44d30fb3..3ac50eeb1db 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -844,6 +844,8 @@ build.json @home-assistant/supervisor /homeassistant/components/pvpc_hourly_pricing/ @azogue /tests/components/pvpc_hourly_pricing/ @azogue /homeassistant/components/qbittorrent/ @geoffreylagaisse +/homeassistant/components/qingping/ @bdraco +/tests/components/qingping/ @bdraco /homeassistant/components/qld_bushfire/ @exxamalte /tests/components/qld_bushfire/ @exxamalte /homeassistant/components/qnap_qsw/ @Noltari diff --git a/homeassistant/components/qingping/__init__.py b/homeassistant/components/qingping/__init__.py new file mode 100644 index 00000000000..9155c02c07c --- /dev/null +++ b/homeassistant/components/qingping/__init__.py @@ -0,0 +1,49 @@ +"""The Qingping integration.""" +from __future__ import annotations + +import logging + +from qingping_ble import QingpingBluetoothDeviceData + +from homeassistant.components.bluetooth import BluetoothScanningMode +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Qingping BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + data = QingpingBluetoothDeviceData() + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/qingping/binary_sensor.py b/homeassistant/components/qingping/binary_sensor.py new file mode 100644 index 00000000000..273a9a93351 --- /dev/null +++ b/homeassistant/components/qingping/binary_sensor.py @@ -0,0 +1,95 @@ +"""Support for Qingping binary sensors.""" +from __future__ import annotations + +from typing import Optional + +from qingping_ble import ( + BinarySensorDeviceClass as QingpingBinarySensorDeviceClass, + SensorUpdate, +) + +from homeassistant import config_entries +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass + +BINARY_SENSOR_DESCRIPTIONS = { + QingpingBinarySensorDeviceClass.MOTION: BinarySensorEntityDescription( + key=QingpingBinarySensorDeviceClass.MOTION, + device_class=BinarySensorDeviceClass.MOTION, + ), + QingpingBinarySensorDeviceClass.LIGHT: BinarySensorEntityDescription( + key=QingpingBinarySensorDeviceClass.LIGHT, + device_class=BinarySensorDeviceClass.LIGHT, + ), +} + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + device_key_to_bluetooth_entity_key(device_key): BINARY_SENSOR_DESCRIPTIONS[ + description.device_class + ] + for device_key, description in sensor_update.binary_entity_descriptions.items() + if description.device_class + }, + entity_data={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.binary_entity_values.items() + }, + entity_names={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.binary_entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Qingping BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + entry.async_on_unload( + processor.async_add_entities_listener( + QingpingBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class QingpingBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[Optional[bool]]], + BinarySensorEntity, +): + """Representation of a Qingping binary sensor.""" + + @property + def is_on(self) -> bool | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/qingping/config_flow.py b/homeassistant/components/qingping/config_flow.py new file mode 100644 index 00000000000..c4ebc4c4273 --- /dev/null +++ b/homeassistant/components/qingping/config_flow.py @@ -0,0 +1,121 @@ +"""Config flow for Qingping integration.""" +from __future__ import annotations + +import asyncio +from typing import Any + +from qingping_ble import QingpingBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothScanningMode, + BluetoothServiceInfoBleak, + async_discovered_service_info, + async_process_advertisements, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + +# How long to wait for additional advertisement packets if we don't have the right ones +ADDITIONAL_DISCOVERY_TIMEOUT = 60 + + +class QingpingConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for qingping.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def _async_wait_for_full_advertisement( + self, discovery_info: BluetoothServiceInfoBleak, device: DeviceData + ) -> BluetoothServiceInfoBleak: + """Wait for the full advertisement. + + Sometimes the first advertisement we receive is blank or incomplete. + """ + if device.supported(discovery_info): + return discovery_info + return await async_process_advertisements( + self.hass, + device.supported, + {"address": discovery_info.address}, + BluetoothScanningMode.ACTIVE, + ADDITIONAL_DISCOVERY_TIMEOUT, + ) + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + try: + self._discovery_info = await self._async_wait_for_full_advertisement( + discovery_info, device + ) + except asyncio.TimeoutError: + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/qingping/const.py b/homeassistant/components/qingping/const.py new file mode 100644 index 00000000000..ccc87ac28f4 --- /dev/null +++ b/homeassistant/components/qingping/const.py @@ -0,0 +1,3 @@ +"""Constants for the Qingping integration.""" + +DOMAIN = "qingping" diff --git a/homeassistant/components/qingping/device.py b/homeassistant/components/qingping/device.py new file mode 100644 index 00000000000..4e4f29b8db8 --- /dev/null +++ b/homeassistant/components/qingping/device.py @@ -0,0 +1,31 @@ +"""Support for Qingping devices.""" +from __future__ import annotations + +from qingping_ble import DeviceKey, SensorDeviceInfo + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothEntityKey, +) +from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME +from homeassistant.helpers.entity import DeviceInfo + + +def device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) + + +def sensor_device_info_to_hass( + sensor_device_info: SensorDeviceInfo, +) -> DeviceInfo: + """Convert a qingping device info to a sensor device info.""" + hass_device_info = DeviceInfo({}) + if sensor_device_info.name is not None: + hass_device_info[ATTR_NAME] = sensor_device_info.name + if sensor_device_info.manufacturer is not None: + hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer + if sensor_device_info.model is not None: + hass_device_info[ATTR_MODEL] = sensor_device_info.model + return hass_device_info diff --git a/homeassistant/components/qingping/manifest.json b/homeassistant/components/qingping/manifest.json new file mode 100644 index 00000000000..221087de8c4 --- /dev/null +++ b/homeassistant/components/qingping/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "qingping", + "name": "Qingping", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/qingping", + "bluetooth": [{ "local_name": "Qingping*" }], + "requirements": ["qingping-ble==0.2.3"], + "dependencies": ["bluetooth"], + "codeowners": ["@bdraco"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/qingping/sensor.py b/homeassistant/components/qingping/sensor.py new file mode 100644 index 00000000000..1affd320af2 --- /dev/null +++ b/homeassistant/components/qingping/sensor.py @@ -0,0 +1,173 @@ +"""Support for Qingping sensors.""" +from __future__ import annotations + +from typing import Optional, Union + +from qingping_ble import ( + SensorDeviceClass as QingpingSensorDeviceClass, + SensorUpdate, + Units, +) + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + LIGHT_LUX, + PERCENTAGE, + PRESSURE_MBAR, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + TEMP_CELSIUS, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass + +SENSOR_DESCRIPTIONS = { + (QingpingSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{QingpingSensorDeviceClass.BATTERY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + ( + QingpingSensorDeviceClass.CO2, + Units.CONCENTRATION_PARTS_PER_MILLION, + ): SensorEntityDescription( + key=f"{QingpingSensorDeviceClass.CO2}_{Units.CONCENTRATION_PARTS_PER_MILLION}", + device_class=SensorDeviceClass.CO2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=SensorStateClass.MEASUREMENT, + ), + (QingpingSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription( + key=f"{QingpingSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}", + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + (QingpingSensorDeviceClass.ILLUMINANCE, Units.LIGHT_LUX): SensorEntityDescription( + key=f"{QingpingSensorDeviceClass.ILLUMINANCE}_{Units.LIGHT_LUX}", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=LIGHT_LUX, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + QingpingSensorDeviceClass.PM10, + Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ): SensorEntityDescription( + key=f"{QingpingSensorDeviceClass.PM10}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", + device_class=SensorDeviceClass.PM10, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + QingpingSensorDeviceClass.PM25, + Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + ): SensorEntityDescription( + key=f"{QingpingSensorDeviceClass.PM25}_{Units.CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}", + device_class=SensorDeviceClass.PM25, + native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=SensorStateClass.MEASUREMENT, + ), + (QingpingSensorDeviceClass.PRESSURE, Units.PRESSURE_MBAR): SensorEntityDescription( + key=f"{QingpingSensorDeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}", + device_class=SensorDeviceClass.PRESSURE, + native_unit_of_measurement=PRESSURE_MBAR, + state_class=SensorStateClass.MEASUREMENT, + ), + ( + QingpingSensorDeviceClass.SIGNAL_STRENGTH, + Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ): SensorEntityDescription( + key=f"{QingpingSensorDeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + ( + QingpingSensorDeviceClass.TEMPERATURE, + Units.TEMP_CELSIUS, + ): SensorEntityDescription( + key=f"{QingpingSensorDeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=TEMP_CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), +} + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: sensor_device_info_to_hass(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + (description.device_class, description.native_unit_of_measurement) + ] + for device_key, description in sensor_update.entity_descriptions.items() + if description.device_class and description.native_unit_of_measurement + }, + entity_data={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Qingping BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + entry.async_on_unload( + processor.async_add_entities_listener( + QingpingBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class QingpingBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[ + PassiveBluetoothDataProcessor[Optional[Union[float, int]]] + ], + SensorEntity, +): + """Representation of a Qingping sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/qingping/strings.json b/homeassistant/components/qingping/strings.json new file mode 100644 index 00000000000..a045d84771e --- /dev/null +++ b/homeassistant/components/qingping/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "not_supported": "Device not supported", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/qingping/translations/en.json b/homeassistant/components/qingping/translations/en.json new file mode 100644 index 00000000000..ebd9760c161 --- /dev/null +++ b/homeassistant/components/qingping/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network", + "not_supported": "Device not supported" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to setup {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to setup" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index 2575be9aa69..39eab0fb973 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -97,6 +97,10 @@ BLUETOOTH: list[dict[str, str | int | list[int]]] = [ "domain": "moat", "local_name": "Moat_S*" }, + { + "domain": "qingping", + "local_name": "Qingping*" + }, { "domain": "sensorpush", "local_name": "SensorPush*" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a20f1229a39..ce95cb66cc5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -289,6 +289,7 @@ FLOWS = { "pure_energie", "pvoutput", "pvpc_hourly_pricing", + "qingping", "qnap_qsw", "rachio", "radio_browser", diff --git a/requirements_all.txt b/requirements_all.txt index 85915ca6b9c..089903ea467 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2063,6 +2063,9 @@ pyzbar==0.1.7 # homeassistant.components.zerproc pyzerproc==0.4.8 +# homeassistant.components.qingping +qingping-ble==0.2.3 + # homeassistant.components.qnap qnapstats==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 17dfb974aaf..c69121c1a8a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1405,6 +1405,9 @@ pyws66i==1.1 # homeassistant.components.zerproc pyzerproc==0.4.8 +# homeassistant.components.qingping +qingping-ble==0.2.3 + # homeassistant.components.rachio rachiopy==1.0.3 diff --git a/tests/components/qingping/__init__.py b/tests/components/qingping/__init__.py new file mode 100644 index 00000000000..66d706463be --- /dev/null +++ b/tests/components/qingping/__init__.py @@ -0,0 +1,40 @@ +"""Tests for the Qingping integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_QINGPING_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="61DE521B-F0BF-9F44-64D4-75BBE1738105", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +LIGHT_AND_SIGNAL_SERVICE_INFO = BluetoothServiceInfo( + name="Qingping Motion & Light", + manufacturer_data={}, + service_uuids=[], + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + service_data={ + "0000fdcd-0000-1000-8000-00805f9b34fb": b"H\x12" + b"\xcd\xd5`4-X\x08\x04\x00\r\x00\x00\x0f\x01\xee" + }, + source="local", +) + + +NO_DATA_SERVICE_INFO = BluetoothServiceInfo( + name="Qingping Motion & Light", + manufacturer_data={}, + service_uuids=[], + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + service_data={ + "0000fdcd-0000-1000-8000-00805f9b34fb": b"0X\x83\n\x02\xcd\xd5`4-X\x08" + }, + source="local", +) diff --git a/tests/components/qingping/conftest.py b/tests/components/qingping/conftest.py new file mode 100644 index 00000000000..e74bf38b26d --- /dev/null +++ b/tests/components/qingping/conftest.py @@ -0,0 +1,8 @@ +"""Qingping session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/qingping/test_binary_sensor.py b/tests/components/qingping/test_binary_sensor.py new file mode 100644 index 00000000000..54e863cd158 --- /dev/null +++ b/tests/components/qingping/test_binary_sensor.py @@ -0,0 +1,46 @@ +"""Test the Qingping binary sensors.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.qingping.const import DOMAIN +from homeassistant.const import ATTR_FRIENDLY_NAME + +from . import LIGHT_AND_SIGNAL_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_binary_sensors(hass): + """Test setting up creates the binary sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("binary_sensor")) == 0 + saved_callback(LIGHT_AND_SIGNAL_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all("binary_sensor")) == 1 + + motion_sensor = hass.states.get("binary_sensor.motion_light_eeff_motion") + assert motion_sensor.state == "off" + assert motion_sensor.attributes[ATTR_FRIENDLY_NAME] == "Motion & Light EEFF Motion" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/qingping/test_config_flow.py b/tests/components/qingping/test_config_flow.py new file mode 100644 index 00000000000..a68b7c050fc --- /dev/null +++ b/tests/components/qingping/test_config_flow.py @@ -0,0 +1,234 @@ +"""Test the Qingping config flow.""" + +import asyncio +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.qingping.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import ( + LIGHT_AND_SIGNAL_SERVICE_INFO, + NO_DATA_SERVICE_INFO, + NOT_QINGPING_SERVICE_INFO, +) + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=LIGHT_AND_SIGNAL_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.qingping.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Motion & Light EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_bluetooth_not_enough_info_at_start(hass): + """Test discovery via bluetooth with only a partial adv at the start.""" + with patch( + "homeassistant.components.qingping.config_flow.async_process_advertisements", + return_value=LIGHT_AND_SIGNAL_SERVICE_INFO, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NO_DATA_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.qingping.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Qingping Motion & Light" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_bluetooth_not_qingping(hass): + """Test discovery via bluetooth not qingping.""" + with patch( + "homeassistant.components.qingping.config_flow.async_process_advertisements", + side_effect=asyncio.TimeoutError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_QINGPING_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.qingping.config_flow.async_discovered_service_info", + return_value=[LIGHT_AND_SIGNAL_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.qingping.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Motion & Light EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.qingping.config_flow.async_discovered_service_info", + return_value=[LIGHT_AND_SIGNAL_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.qingping.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.qingping.config_flow.async_discovered_service_info", + return_value=[LIGHT_AND_SIGNAL_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=LIGHT_AND_SIGNAL_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=LIGHT_AND_SIGNAL_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=LIGHT_AND_SIGNAL_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=LIGHT_AND_SIGNAL_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.qingping.config_flow.async_discovered_service_info", + return_value=[LIGHT_AND_SIGNAL_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.qingping.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Motion & Light EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/qingping/test_sensor.py b/tests/components/qingping/test_sensor.py new file mode 100644 index 00000000000..0f7e7c6a58e --- /dev/null +++ b/tests/components/qingping/test_sensor.py @@ -0,0 +1,50 @@ +"""Test the Qingping sensors.""" + +from unittest.mock import patch + +from homeassistant.components.bluetooth import BluetoothChange +from homeassistant.components.qingping.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT + +from . import LIGHT_AND_SIGNAL_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_sensors(hass): + """Test setting up creates the sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + saved_callback = None + + def _async_register_callback(_hass, _callback, _matcher, _mode): + nonlocal saved_callback + saved_callback = _callback + return lambda: None + + with patch( + "homeassistant.components.bluetooth.update_coordinator.async_register_callback", + _async_register_callback, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 0 + saved_callback(LIGHT_AND_SIGNAL_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 1 + + lux_sensor = hass.states.get("sensor.motion_light_eeff_illuminance") + lux_sensor_attrs = lux_sensor.attributes + assert lux_sensor.state == "13" + assert lux_sensor_attrs[ATTR_FRIENDLY_NAME] == "Motion & Light EEFF Illuminance" + assert lux_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "lx" + assert lux_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() From 5f827f4ca64067ca886e0e4dcb0d1498689e1ded Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Aug 2022 21:56:57 -1000 Subject: [PATCH 3330/3516] Bump aiohomekit to 1.2.10 (#76738) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 4bd9a0b70f9..49635dd797c 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.9"], + "requirements": ["aiohomekit==1.2.10"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 089903ea467..f11c78be101 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.9 +aiohomekit==1.2.10 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c69121c1a8a..fd28a98e9ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.9 +aiohomekit==1.2.10 # homeassistant.components.emulated_hue # homeassistant.components.http From 7fc2d9e087a906ef1121c121ffc2d69f86ddec72 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sun, 14 Aug 2022 16:57:25 -0400 Subject: [PATCH 3331/3516] Persist previous mic/record values for UniFi Protect privacy mode (#76472) --- .../components/unifiprotect/switch.py | 140 +++++++++++++----- tests/components/unifiprotect/test_switch.py | 41 ++++- 2 files changed, 133 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index 5bc4e1f17eb..e0ddddd4c53 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -2,22 +2,23 @@ from __future__ import annotations from dataclasses import dataclass -import logging from typing import Any from pyunifiprotect.data import ( Camera, ProtectAdoptableDeviceModel, + ProtectModelWithId, RecordingMode, VideoMode, ) from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity from .const import DISPATCH_ADOPT, DOMAIN from .data import ProtectData @@ -25,7 +26,8 @@ from .entity import ProtectDeviceEntity, async_all_device_entities from .models import PermRequired, ProtectSetableKeysMixin, T from .utils import async_dispatch_id as _ufpd -_LOGGER = logging.getLogger(__name__) +ATTR_PREV_MIC = "prev_mic_level" +ATTR_PREV_RECORD = "prev_record_mode" @dataclass @@ -35,9 +37,6 @@ class ProtectSwitchEntityDescription( """Describes UniFi Protect Switch entity.""" -_KEY_PRIVACY_MODE = "privacy_mode" - - async def _set_highfps(obj: Camera, value: bool) -> None: if value: await obj.set_video_mode(VideoMode.HIGH_FPS) @@ -86,15 +85,6 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ufp_set_method_fn=_set_highfps, ufp_perm=PermRequired.WRITE, ), - ProtectSwitchEntityDescription( - key=_KEY_PRIVACY_MODE, - name="Privacy Mode", - icon="mdi:eye-settings", - entity_category=EntityCategory.CONFIG, - ufp_required_field="feature_flags.has_privacy_mask", - ufp_value="is_privacy_on", - ufp_perm=PermRequired.WRITE, - ), ProtectSwitchEntityDescription( key="system_sounds", name="System Sounds", @@ -192,6 +182,16 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ), ) +PRIVACY_MODE_SWITCH = ProtectSwitchEntityDescription[Camera]( + key="privacy_mode", + name="Privacy Mode", + icon="mdi:eye-settings", + entity_category=EntityCategory.CONFIG, + ufp_required_field="feature_flags.has_privacy_mask", + ufp_value="is_privacy_on", + ufp_perm=PermRequired.WRITE, +) + SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( ProtectSwitchEntityDescription( key="status_light", @@ -316,6 +316,11 @@ async def async_setup_entry( viewer_descs=VIEWER_SWITCHES, ufp_device=device, ) + entities += async_all_device_entities( + data, + ProtectPrivacyModeSwitch, + camera_descs=[PRIVACY_MODE_SWITCH], + ) async_add_entities(entities) entry.async_on_unload( @@ -331,6 +336,11 @@ async def async_setup_entry( lock_descs=DOORLOCK_SWITCHES, viewer_descs=VIEWER_SWITCHES, ) + entities += async_all_device_entities( + data, + ProtectPrivacyModeSwitch, + camera_descs=[PRIVACY_MODE_SWITCH], + ) async_add_entities(entities) @@ -350,17 +360,6 @@ class ProtectSwitch(ProtectDeviceEntity, SwitchEntity): self._attr_name = f"{self.device.display_name} {self.entity_description.name}" self._switch_type = self.entity_description.key - if not isinstance(self.device, Camera): - return - - if self.entity_description.key == _KEY_PRIVACY_MODE: - if self.device.is_privacy_on: - self._previous_mic_level = 100 - self._previous_record_mode = RecordingMode.ALWAYS - else: - self._previous_mic_level = self.device.mic_volume - self._previous_record_mode = self.device.recording_settings.mode - @property def is_on(self) -> bool: """Return true if device is on.""" @@ -368,24 +367,83 @@ class ProtectSwitch(ProtectDeviceEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" - if self._switch_type == _KEY_PRIVACY_MODE: - assert isinstance(self.device, Camera) - self._previous_mic_level = self.device.mic_volume - self._previous_record_mode = self.device.recording_settings.mode - await self.device.set_privacy(True, 0, RecordingMode.NEVER) - else: - await self.entity_description.ufp_set(self.device, True) + + await self.entity_description.ufp_set(self.device, True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" - if self._switch_type == _KEY_PRIVACY_MODE: - assert isinstance(self.device, Camera) - _LOGGER.debug( - "Setting Privacy Mode to false for %s", self.device.display_name - ) - await self.device.set_privacy( - False, self._previous_mic_level, self._previous_record_mode + await self.entity_description.ufp_set(self.device, False) + + +class ProtectPrivacyModeSwitch(RestoreEntity, ProtectSwitch): + """A UniFi Protect Switch.""" + + device: Camera + + def __init__( + self, + data: ProtectData, + device: ProtectAdoptableDeviceModel, + description: ProtectSwitchEntityDescription, + ) -> None: + """Initialize an UniFi Protect Switch.""" + super().__init__(data, device, description) + + if self.device.is_privacy_on: + extra_state = self.extra_state_attributes or {} + self._previous_mic_level = extra_state.get(ATTR_PREV_MIC, 100) + self._previous_record_mode = extra_state.get( + ATTR_PREV_RECORD, RecordingMode.ALWAYS ) else: - await self.entity_description.ufp_set(self.device, False) + self._previous_mic_level = self.device.mic_volume + self._previous_record_mode = self.device.recording_settings.mode + + @callback + def _update_previous_attr(self) -> None: + if self.is_on: + self._attr_extra_state_attributes = { + ATTR_PREV_MIC: self._previous_mic_level, + ATTR_PREV_RECORD: self._previous_record_mode, + } + else: + self._attr_extra_state_attributes = {} + + @callback + def _async_update_device_from_protect(self, device: ProtectModelWithId) -> None: + super()._async_update_device_from_protect(device) + + # do not add extra state attribute on initialize + if self.entity_id: + self._update_previous_attr() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + + self._previous_mic_level = self.device.mic_volume + self._previous_record_mode = self.device.recording_settings.mode + await self.device.set_privacy(True, 0, RecordingMode.NEVER) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + + extra_state = self.extra_state_attributes or {} + prev_mic = extra_state.get(ATTR_PREV_MIC, self._previous_mic_level) + prev_record = extra_state.get(ATTR_PREV_RECORD, self._previous_record_mode) + await self.device.set_privacy(False, prev_mic, prev_record) + + async def async_added_to_hass(self) -> None: + """Restore extra state attributes on startp up.""" + await super().async_added_to_hass() + + if not (last_state := await self.async_get_last_state()): + return + + self._previous_mic_level = last_state.attributes.get( + ATTR_PREV_MIC, self._previous_mic_level + ) + self._previous_record_mode = last_state.attributes.get( + ATTR_PREV_RECORD, self._previous_record_mode + ) + self._update_previous_attr() diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 684e3b8e441..6fa718c4952 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -9,8 +9,11 @@ from pyunifiprotect.data import Camera, Light, Permission, RecordingMode, VideoM from homeassistant.components.unifiprotect.const import DEFAULT_ATTRIBUTION from homeassistant.components.unifiprotect.switch import ( + ATTR_PREV_MIC, + ATTR_PREV_RECORD, CAMERA_SWITCHES, LIGHT_SWITCHES, + PRIVACY_MODE_SWITCH, ProtectSwitchEntityDescription, ) from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, STATE_OFF, Platform @@ -347,31 +350,55 @@ async def test_switch_camera_highfps( async def test_switch_camera_privacy( hass: HomeAssistant, ufp: MockUFPFixture, doorbell: Camera ): - """Tests Privacy Mode switch for cameras.""" + """Tests Privacy Mode switch for cameras with privacy mode defaulted on.""" + + previous_mic = doorbell.mic_volume = 53 + previous_record = doorbell.recording_settings.mode = RecordingMode.DETECTIONS await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SWITCH, 13, 12) - description = CAMERA_SWITCHES[4] + description = PRIVACY_MODE_SWITCH doorbell.__fields__["set_privacy"] = Mock() doorbell.set_privacy = AsyncMock() _, entity_id = ids_from_device_description(Platform.SWITCH, doorbell, description) + state = hass.states.get(entity_id) + assert state and state.state == "off" + assert ATTR_PREV_MIC not in state.attributes + assert ATTR_PREV_RECORD not in state.attributes + await hass.services.async_call( "switch", "turn_on", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - doorbell.set_privacy.assert_called_once_with(True, 0, RecordingMode.NEVER) + doorbell.set_privacy.assert_called_with(True, 0, RecordingMode.NEVER) + + new_doorbell = doorbell.copy() + new_doorbell.add_privacy_zone() + new_doorbell.mic_volume = 0 + new_doorbell.recording_settings.mode = RecordingMode.NEVER + ufp.api.bootstrap.cameras = {new_doorbell.id: new_doorbell} + + mock_msg = Mock() + mock_msg.changed_data = {} + mock_msg.new_obj = new_doorbell + ufp.ws_msg(mock_msg) + + state = hass.states.get(entity_id) + assert state and state.state == "on" + assert state.attributes[ATTR_PREV_MIC] == previous_mic + assert state.attributes[ATTR_PREV_RECORD] == previous_record.value + + doorbell.set_privacy.reset_mock() await hass.services.async_call( "switch", "turn_off", {ATTR_ENTITY_ID: entity_id}, blocking=True ) - doorbell.set_privacy.assert_called_with( - False, doorbell.mic_volume, doorbell.recording_settings.mode - ) + doorbell.set_privacy.assert_called_with(False, previous_mic, previous_record) async def test_switch_camera_privacy_already_on( @@ -383,7 +410,7 @@ async def test_switch_camera_privacy_already_on( await init_entry(hass, ufp, [doorbell]) assert_entity_counts(hass, Platform.SWITCH, 13, 12) - description = CAMERA_SWITCHES[4] + description = PRIVACY_MODE_SWITCH doorbell.__fields__["set_privacy"] = Mock() doorbell.set_privacy = AsyncMock() From aadecdf6cb60642fe11e7b9d1c0d5c07e9aba01a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sun, 14 Aug 2022 23:54:25 +0200 Subject: [PATCH 3332/3516] Add type hints to MediaPlayerEntity (#76743) * Add media-player checks to pylint plugin * Fix invalid hints * Add tests * Adjust tests * Add extra test * Adjust regex * Cleanup comment * Revert * Revert * Update homeassistant/components/media_player/__init__.py Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> * Update homeassistant/components/denonavr/media_player.py Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- .../components/denonavr/media_player.py | 2 +- .../components/group/media_player.py | 2 +- .../components/media_player/__init__.py | 158 ++++++++++-------- .../components/webostv/media_player.py | 2 +- 4 files changed, 95 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 16814b72bc7..7c5d98ca1b3 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -448,7 +448,7 @@ class DenonDevice(MediaPlayerEntity): await self._receiver.async_volume_down() @async_log_errors - async def async_set_volume_level(self, volume: int): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" # Volume has to be sent in a format like -50.0. Minimum is -80.0, # maximum is 18.0 diff --git a/homeassistant/components/group/media_player.py b/homeassistant/components/group/media_player.py index e0cbf84a693..60cb37f46ba 100644 --- a/homeassistant/components/group/media_player.py +++ b/homeassistant/components/group/media_player.py @@ -277,7 +277,7 @@ class MediaPlayerGroup(MediaPlayerEntity): context=self._context, ) - async def async_media_seek(self, position: int) -> None: + async def async_media_seek(self, position: float) -> None: """Send seek command.""" data = { ATTR_ENTITY_ID: self._features[KEY_SEEK], diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 9ca9613278d..29c75a4fc22 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -677,213 +677,231 @@ class MediaPlayerEntity(Entity): """Flag media player features that are supported.""" return self._attr_supported_features - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" raise NotImplementedError() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn the media player on.""" await self.hass.async_add_executor_job(self.turn_on) - def turn_off(self): + def turn_off(self) -> None: """Turn the media player off.""" raise NotImplementedError() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn the media player off.""" await self.hass.async_add_executor_job(self.turn_off) - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute the volume.""" raise NotImplementedError() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" await self.hass.async_add_executor_job(self.mute_volume, mute) - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" raise NotImplementedError() - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" await self.hass.async_add_executor_job(self.set_volume_level, volume) - def media_play(self): + def media_play(self) -> None: """Send play command.""" raise NotImplementedError() - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" await self.hass.async_add_executor_job(self.media_play) - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" raise NotImplementedError() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" await self.hass.async_add_executor_job(self.media_pause) - def media_stop(self): + def media_stop(self) -> None: """Send stop command.""" raise NotImplementedError() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop command.""" await self.hass.async_add_executor_job(self.media_stop) - def media_previous_track(self): + def media_previous_track(self) -> None: """Send previous track command.""" raise NotImplementedError() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command.""" await self.hass.async_add_executor_job(self.media_previous_track) - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" raise NotImplementedError() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" await self.hass.async_add_executor_job(self.media_next_track) - def media_seek(self, position): + def media_seek(self, position: float) -> None: """Send seek command.""" raise NotImplementedError() - async def async_media_seek(self, position): + async def async_media_seek(self, position: float) -> None: """Send seek command.""" await self.hass.async_add_executor_job(self.media_seek, position) - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play a piece of media.""" raise NotImplementedError() - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Play a piece of media.""" await self.hass.async_add_executor_job( ft.partial(self.play_media, media_type, media_id, **kwargs) ) - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" raise NotImplementedError() - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select input source.""" await self.hass.async_add_executor_job(self.select_source, source) - def select_sound_mode(self, sound_mode): + def select_sound_mode(self, sound_mode: str) -> None: """Select sound mode.""" raise NotImplementedError() - async def async_select_sound_mode(self, sound_mode): + async def async_select_sound_mode(self, sound_mode: str) -> None: """Select sound mode.""" await self.hass.async_add_executor_job(self.select_sound_mode, sound_mode) - def clear_playlist(self): + def clear_playlist(self) -> None: """Clear players playlist.""" raise NotImplementedError() - async def async_clear_playlist(self): + async def async_clear_playlist(self) -> None: """Clear players playlist.""" await self.hass.async_add_executor_job(self.clear_playlist) - def set_shuffle(self, shuffle): + def set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" raise NotImplementedError() - async def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" await self.hass.async_add_executor_job(self.set_shuffle, shuffle) - def set_repeat(self, repeat): + def set_repeat(self, repeat: str) -> None: """Set repeat mode.""" raise NotImplementedError() - async def async_set_repeat(self, repeat): + async def async_set_repeat(self, repeat: str) -> None: """Set repeat mode.""" await self.hass.async_add_executor_job(self.set_repeat, repeat) # No need to overwrite these. + @final @property - def support_play(self): + def support_play(self) -> bool: """Boolean if play is supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.PLAY) + @final @property - def support_pause(self): + def support_pause(self) -> bool: """Boolean if pause is supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.PAUSE) + @final @property - def support_stop(self): + def support_stop(self) -> bool: """Boolean if stop is supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.STOP) + @final @property - def support_seek(self): + def support_seek(self) -> bool: """Boolean if seek is supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.SEEK) + @final @property - def support_volume_set(self): + def support_volume_set(self) -> bool: """Boolean if setting volume is supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.VOLUME_SET) + @final @property - def support_volume_mute(self): + def support_volume_mute(self) -> bool: """Boolean if muting volume is supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.VOLUME_MUTE) + @final @property - def support_previous_track(self): + def support_previous_track(self) -> bool: """Boolean if previous track command supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.PREVIOUS_TRACK) + @final @property - def support_next_track(self): + def support_next_track(self) -> bool: """Boolean if next track command supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.NEXT_TRACK) + @final @property - def support_play_media(self): + def support_play_media(self) -> bool: """Boolean if play media command supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.PLAY_MEDIA) + @final @property - def support_select_source(self): + def support_select_source(self) -> bool: """Boolean if select source command supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.SELECT_SOURCE) + @final @property - def support_select_sound_mode(self): + def support_select_sound_mode(self) -> bool: """Boolean if select sound mode command supported.""" return bool( self.supported_features & MediaPlayerEntityFeature.SELECT_SOUND_MODE ) + @final @property - def support_clear_playlist(self): + def support_clear_playlist(self) -> bool: """Boolean if clear playlist command supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.CLEAR_PLAYLIST) + @final @property - def support_shuffle_set(self): + def support_shuffle_set(self) -> bool: """Boolean if shuffle is supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.SHUFFLE_SET) + @final @property - def support_grouping(self): + def support_grouping(self) -> bool: """Boolean if player grouping is supported.""" return bool(self.supported_features & MediaPlayerEntityFeature.GROUPING) - async def async_toggle(self): + async def async_toggle(self) -> None: """Toggle the power on the media player.""" if hasattr(self, "toggle"): - await self.hass.async_add_executor_job(self.toggle) + await self.hass.async_add_executor_job( + self.toggle # type: ignore[attr-defined] + ) return if self.state in (STATE_OFF, STATE_IDLE, STATE_STANDBY): @@ -891,40 +909,48 @@ class MediaPlayerEntity(Entity): else: await self.async_turn_off() - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Turn volume up for media player. This method is a coroutine. """ if hasattr(self, "volume_up"): - await self.hass.async_add_executor_job(self.volume_up) + await self.hass.async_add_executor_job( + self.volume_up # type: ignore[attr-defined] + ) return if ( - self.volume_level < 1 + self.volume_level is not None + and self.volume_level < 1 and self.supported_features & MediaPlayerEntityFeature.VOLUME_SET ): await self.async_set_volume_level(min(1, self.volume_level + 0.1)) - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Turn volume down for media player. This method is a coroutine. """ if hasattr(self, "volume_down"): - await self.hass.async_add_executor_job(self.volume_down) + await self.hass.async_add_executor_job( + self.volume_down # type: ignore[attr-defined] + ) return if ( - self.volume_level > 0 + self.volume_level is not None + and self.volume_level > 0 and self.supported_features & MediaPlayerEntityFeature.VOLUME_SET ): await self.async_set_volume_level(max(0, self.volume_level - 0.1)) - async def async_media_play_pause(self): + async def async_media_play_pause(self) -> None: """Play or pause the media player.""" if hasattr(self, "media_play_pause"): - await self.hass.async_add_executor_job(self.media_play_pause) + await self.hass.async_add_executor_job( + self.media_play_pause # type: ignore[attr-defined] + ) return if self.state == STATE_PLAYING: @@ -933,7 +959,7 @@ class MediaPlayerEntity(Entity): await self.async_media_play() @property - def entity_picture(self): + def entity_picture(self) -> str | None: """Return image of the media playing.""" if self.state == STATE_OFF: return None @@ -944,7 +970,7 @@ class MediaPlayerEntity(Entity): return self.media_image_local @property - def media_image_local(self): + def media_image_local(self) -> str | None: """Return local url to media image.""" if (image_hash := self.media_image_hash) is None: return None @@ -955,10 +981,10 @@ class MediaPlayerEntity(Entity): ) @property - def capability_attributes(self): + def capability_attributes(self) -> dict[str, Any]: """Return capability attributes.""" supported_features = self.supported_features or 0 - data = {} + data: dict[str, Any] = {} if supported_features & MediaPlayerEntityFeature.SELECT_SOURCE and ( source_list := self.source_list @@ -974,9 +1000,9 @@ class MediaPlayerEntity(Entity): @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" - state_attr = {} + state_attr: dict[str, Any] = {} if self.support_grouping: state_attr[ATTR_GROUP_MEMBERS] = self.group_members @@ -1005,19 +1031,19 @@ class MediaPlayerEntity(Entity): """ raise NotImplementedError() - def join_players(self, group_members): + def join_players(self, group_members: list[str]) -> None: """Join `group_members` as a player group with the current player.""" raise NotImplementedError() - async def async_join_players(self, group_members): + async def async_join_players(self, group_members: list[str]) -> None: """Join `group_members` as a player group with the current player.""" await self.hass.async_add_executor_job(self.join_players, group_members) - def unjoin_player(self): + def unjoin_player(self) -> None: """Remove this player from any group.""" raise NotImplementedError() - async def async_unjoin_player(self): + async def async_unjoin_player(self) -> None: """Remove this player from any group.""" await self.hass.async_add_executor_job(self.unjoin_player) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 3806ee6c2bb..36941b15240 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -340,7 +340,7 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): await self._client.volume_down() @cmd - async def async_set_volume_level(self, volume: int) -> None: + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" tv_volume = int(round(volume * 100)) await self._client.set_volume(tv_volume) From 8a85881ca076082e9df0777a0aabe219638866d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Aug 2022 19:53:27 -1000 Subject: [PATCH 3333/3516] Bump pySwitchbot to 0.18.10 to handle empty data and disconnects (#76684) * Bump pySwitchbot to 0.18.7 to handle empty data Fixes #76621 * bump again * bump * bump for rssi on disconnect logging --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index e26100108c9..e70f467ae74 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.18.6"], + "requirements": ["PySwitchbot==0.18.10"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index f11c78be101..1e7db64277c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.6 +PySwitchbot==0.18.10 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fd28a98e9ea..10e88fcc1ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.6 +PySwitchbot==0.18.10 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 4f9e6d240771680ff7ab7248163cb6fbf5d7dba7 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 15 Aug 2022 08:23:05 +0200 Subject: [PATCH 3334/3516] Improve vacuum type hints (#76747) * Improve vacuum type hints * Black * Black * Adjust * One more --- homeassistant/components/vacuum/__init__.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 74636e82e69..24d4718540e 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -114,7 +114,7 @@ SUPPORT_START = 8192 @bind_hass -def is_on(hass, entity_id): +def is_on(hass: HomeAssistant, entity_id: str) -> bool: """Return if the vacuum is on based on the statemachine.""" return hass.states.is_state(entity_id, STATE_ON) @@ -286,13 +286,19 @@ class _BaseVacuum(Entity): ) def send_command( - self, command: str, params: dict | list | None = None, **kwargs: Any + self, + command: str, + params: dict[str, Any] | list[Any] | None = None, + **kwargs: Any, ) -> None: """Send a command to a vacuum cleaner.""" raise NotImplementedError() async def async_send_command( - self, command: str, params: dict | list | None = None, **kwargs: Any + self, + command: str, + params: dict[str, Any] | list[Any] | None = None, + **kwargs: Any, ) -> None: """Send a command to a vacuum cleaner. From f72cfef7be56d2e5561fa711273a3837f1ea5646 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 15 Aug 2022 08:26:17 +0200 Subject: [PATCH 3335/3516] Fix MQTT camera encoding (#76124) * Fix MQTT camera encoding * Reduce code * Add test for using image_encoding parameter * Move deprecation check to validation * Dependency * Set correct strings and log warning * Rename constant * Use better issue string identifier * Revert unwanted change to hassio test * Avoid term `deprecated` in issue description * Revert changes using the repairs API * Add a notice when work-a-round will be removed * Update homeassistant/components/mqtt/camera.py Co-authored-by: Erik Montnemery Co-authored-by: Erik Montnemery --- .../components/mqtt/abbreviations.py | 1 + homeassistant/components/mqtt/camera.py | 42 +++++++++-- tests/components/mqtt/test_camera.py | 74 +++++++++++++++++++ 3 files changed, 109 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index ddbced5286d..32b67874a45 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -80,6 +80,7 @@ ABBREVIATIONS = { "hs_stat_t": "hs_state_topic", "hs_val_tpl": "hs_value_template", "ic": "icon", + "img_e": "image_encoding", "init": "initial", "hum_cmd_t": "target_humidity_command_topic", "hum_cmd_tpl": "target_humidity_command_template", diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index f213bec9bb6..61c87e86888 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -3,6 +3,7 @@ from __future__ import annotations from base64 import b64decode import functools +import logging import voluptuous as vol @@ -17,7 +18,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription from .config import MQTT_BASE_SCHEMA -from .const import CONF_ENCODING, CONF_QOS, CONF_TOPIC +from .const import CONF_ENCODING, CONF_QOS, CONF_TOPIC, DEFAULT_ENCODING from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, @@ -29,6 +30,10 @@ from .mixins import ( ) from .util import valid_subscribe_topic +_LOGGER = logging.getLogger(__name__) + +CONF_IMAGE_ENCODING = "image_encoding" + DEFAULT_NAME = "MQTT Camera" MQTT_CAMERA_ATTRIBUTES_BLOCKED = frozenset( @@ -40,20 +45,41 @@ MQTT_CAMERA_ATTRIBUTES_BLOCKED = frozenset( } ) -PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( + +# Using CONF_ENCODING to set b64 encoding for images is deprecated as of Home Assistant 2022.9 +# use CONF_IMAGE_ENCODING instead, support for the work-a-round will be removed with Home Assistant 2022.11 +def repair_legacy_encoding(config: ConfigType) -> ConfigType: + """Check incorrect deprecated config of image encoding.""" + if config[CONF_ENCODING] == "b64": + config[CONF_IMAGE_ENCODING] = "b64" + config[CONF_ENCODING] = DEFAULT_ENCODING + _LOGGER.warning( + "Using the `encoding` parameter to set image encoding has been deprecated, use `image_encoding` instead" + ) + return config + + +PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_IMAGE_ENCODING): "b64", } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Camera under the camera platform key is deprecated in HA Core 2022.6 -PLATFORM_SCHEMA = vol.All( - cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_MODERN.schema), - warn_for_legacy_schema(camera.DOMAIN), +PLATFORM_SCHEMA_MODERN = vol.All( + PLATFORM_SCHEMA_BASE.schema, + repair_legacy_encoding, ) -DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) +# Configuring MQTT Camera under the camera platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA = vol.All( + cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_BASE.schema), + warn_for_legacy_schema(camera.DOMAIN), + repair_legacy_encoding, +) + +DISCOVERY_SCHEMA = PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA) async def async_setup_platform( @@ -124,7 +150,7 @@ class MqttCamera(MqttEntity, Camera): @log_messages(self.hass, self.entity_id) def message_received(msg): """Handle new MQTT messages.""" - if self._config[CONF_ENCODING] == "b64": + if CONF_IMAGE_ENCODING in self._config: self._last_image = b64decode(msg.payload) else: self._last_image = msg.payload diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index c6d116b6a74..a76025a608a 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -110,6 +110,80 @@ async def test_run_camera_b64_encoded( assert body == "grass" +# Using CONF_ENCODING to set b64 encoding for images is deprecated Home Assistant 2022.9, use CONF_IMAGE_ENCODING instead +async def test_legacy_camera_b64_encoded_with_availability( + hass, hass_client_no_auth, mqtt_mock_entry_with_yaml_config +): + """Test availability works if b64 encoding (legacy mode) is turned on.""" + topic = "test/camera" + topic_availability = "test/camera_availability" + await async_setup_component( + hass, + "camera", + { + "camera": { + "platform": "mqtt", + "topic": topic, + "name": "Test Camera", + "encoding": "b64", + "availability": {"topic": topic_availability}, + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + # Make sure we are available + async_fire_mqtt_message(hass, topic_availability, "online") + + url = hass.states.get("camera.test_camera").attributes["entity_picture"] + + async_fire_mqtt_message(hass, topic, b64encode(b"grass")) + + client = await hass_client_no_auth() + resp = await client.get(url) + assert resp.status == HTTPStatus.OK + body = await resp.text() + assert body == "grass" + + +async def test_camera_b64_encoded_with_availability( + hass, hass_client_no_auth, mqtt_mock_entry_with_yaml_config +): + """Test availability works if b64 encoding is turned on.""" + topic = "test/camera" + topic_availability = "test/camera_availability" + await async_setup_component( + hass, + "camera", + { + "camera": { + "platform": "mqtt", + "topic": topic, + "name": "Test Camera", + "encoding": "utf-8", + "image_encoding": "b64", + "availability": {"topic": topic_availability}, + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + # Make sure we are available + async_fire_mqtt_message(hass, topic_availability, "online") + + url = hass.states.get("camera.test_camera").attributes["entity_picture"] + + async_fire_mqtt_message(hass, topic, b64encode(b"grass")) + + client = await hass_client_no_auth() + resp = await client.get(url) + assert resp.status == HTTPStatus.OK + body = await resp.text() + assert body == "grass" + + async def test_availability_when_connection_lost( hass, mqtt_mock_entry_with_yaml_config ): From 161e533c5f0a5225327ff0710fa7eb2ac2a704ec Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 15 Aug 2022 08:27:37 +0200 Subject: [PATCH 3336/3516] Remove MQTT climate support for hold and away modes (#76299) Remove support for hold and away modes --- homeassistant/components/mqtt/climate.py | 191 ++---------- tests/components/mqtt/test_climate.py | 359 ----------------------- 2 files changed, 23 insertions(+), 527 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index bf53544b491..f44cf6fe8fc 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -19,7 +19,6 @@ from homeassistant.components.climate.const import ( FAN_HIGH, FAN_LOW, FAN_MEDIUM, - PRESET_AWAY, PRESET_NONE, SWING_OFF, SWING_ON, @@ -68,10 +67,11 @@ CONF_ACTION_TOPIC = "action_topic" CONF_AUX_COMMAND_TOPIC = "aux_command_topic" CONF_AUX_STATE_TEMPLATE = "aux_state_template" CONF_AUX_STATE_TOPIC = "aux_state_topic" -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 +# AWAY and HOLD mode topics and templates are no longer supported, support was removed with release 2022.9 CONF_AWAY_MODE_COMMAND_TOPIC = "away_mode_command_topic" CONF_AWAY_MODE_STATE_TEMPLATE = "away_mode_state_template" CONF_AWAY_MODE_STATE_TOPIC = "away_mode_state_topic" + CONF_CURRENT_TEMP_TEMPLATE = "current_temperature_template" CONF_CURRENT_TEMP_TOPIC = "current_temperature_topic" CONF_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_command_template" @@ -79,12 +79,13 @@ CONF_FAN_MODE_COMMAND_TOPIC = "fan_mode_command_topic" CONF_FAN_MODE_LIST = "fan_modes" CONF_FAN_MODE_STATE_TEMPLATE = "fan_mode_state_template" CONF_FAN_MODE_STATE_TOPIC = "fan_mode_state_topic" -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 +# AWAY and HOLD mode topics and templates are no longer supported, support was removed with release 2022.9 CONF_HOLD_COMMAND_TEMPLATE = "hold_command_template" CONF_HOLD_COMMAND_TOPIC = "hold_command_topic" CONF_HOLD_STATE_TEMPLATE = "hold_state_template" CONF_HOLD_STATE_TOPIC = "hold_state_topic" CONF_HOLD_LIST = "hold_modes" + CONF_MODE_COMMAND_TEMPLATE = "mode_command_template" CONF_MODE_COMMAND_TOPIC = "mode_command_topic" CONF_MODE_LIST = "modes" @@ -150,12 +151,8 @@ MQTT_CLIMATE_ATTRIBUTES_BLOCKED = frozenset( VALUE_TEMPLATE_KEYS = ( CONF_AUX_STATE_TEMPLATE, - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - CONF_AWAY_MODE_STATE_TEMPLATE, CONF_CURRENT_TEMP_TEMPLATE, CONF_FAN_MODE_STATE_TEMPLATE, - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - CONF_HOLD_STATE_TEMPLATE, CONF_MODE_STATE_TEMPLATE, CONF_POWER_STATE_TEMPLATE, CONF_ACTION_TEMPLATE, @@ -168,8 +165,6 @@ VALUE_TEMPLATE_KEYS = ( COMMAND_TEMPLATE_KEYS = { CONF_FAN_MODE_COMMAND_TEMPLATE, - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - CONF_HOLD_COMMAND_TEMPLATE, CONF_MODE_COMMAND_TEMPLATE, CONF_PRESET_MODE_COMMAND_TEMPLATE, CONF_SWING_MODE_COMMAND_TEMPLATE, @@ -178,32 +173,14 @@ COMMAND_TEMPLATE_KEYS = { CONF_TEMP_LOW_COMMAND_TEMPLATE, } -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -DEPRECATED_INVALID = [ - CONF_AWAY_MODE_COMMAND_TOPIC, - CONF_AWAY_MODE_STATE_TEMPLATE, - CONF_AWAY_MODE_STATE_TOPIC, - CONF_HOLD_COMMAND_TEMPLATE, - CONF_HOLD_COMMAND_TOPIC, - CONF_HOLD_STATE_TEMPLATE, - CONF_HOLD_STATE_TOPIC, - CONF_HOLD_LIST, -] - TOPIC_KEYS = ( CONF_ACTION_TOPIC, CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC, - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - CONF_AWAY_MODE_COMMAND_TOPIC, - CONF_AWAY_MODE_STATE_TOPIC, CONF_CURRENT_TEMP_TOPIC, CONF_FAN_MODE_COMMAND_TOPIC, CONF_FAN_MODE_STATE_TOPIC, - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - CONF_HOLD_COMMAND_TOPIC, - CONF_HOLD_STATE_TOPIC, CONF_MODE_COMMAND_TOPIC, CONF_MODE_STATE_TOPIC, CONF_POWER_COMMAND_TOPIC, @@ -225,12 +202,6 @@ def valid_preset_mode_configuration(config): """Validate that the preset mode reset payload is not one of the preset modes.""" if PRESET_NONE in config.get(CONF_PRESET_MODES_LIST): raise ValueError("preset_modes must not include preset mode 'none'") - if config.get(CONF_PRESET_MODE_COMMAND_TOPIC): - for config_parameter in DEPRECATED_INVALID: - if config.get(config_parameter): - raise vol.MultipleInvalid( - "preset_modes cannot be used with deprecated away or hold mode config options" - ) return config @@ -239,10 +210,6 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( vol.Optional(CONF_AUX_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template, vol.Optional(CONF_AUX_STATE_TOPIC): valid_subscribe_topic, - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - vol.Optional(CONF_AWAY_MODE_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_AWAY_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template, vol.Optional(CONF_CURRENT_TEMP_TOPIC): valid_subscribe_topic, vol.Optional(CONF_FAN_MODE_COMMAND_TEMPLATE): cv.template, @@ -253,12 +220,6 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( ): cv.ensure_list, vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_FAN_MODE_STATE_TOPIC): valid_subscribe_topic, - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - vol.Optional(CONF_HOLD_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_HOLD_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_HOLD_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_HOLD_LIST): cv.ensure_list, vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_MODE_COMMAND_TOPIC): valid_publish_topic, vol.Optional( @@ -334,15 +295,15 @@ PLATFORM_SCHEMA = vol.All( cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), # Support CONF_SEND_IF_OFF is removed with release 2022.9 cv.removed(CONF_SEND_IF_OFF), - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - cv.deprecated(CONF_AWAY_MODE_COMMAND_TOPIC), - cv.deprecated(CONF_AWAY_MODE_STATE_TEMPLATE), - cv.deprecated(CONF_AWAY_MODE_STATE_TOPIC), - cv.deprecated(CONF_HOLD_COMMAND_TEMPLATE), - cv.deprecated(CONF_HOLD_COMMAND_TOPIC), - cv.deprecated(CONF_HOLD_STATE_TEMPLATE), - cv.deprecated(CONF_HOLD_STATE_TOPIC), - cv.deprecated(CONF_HOLD_LIST), + # AWAY and HOLD mode topics and templates are no longer supported, support was removed with release 2022.9 + cv.removed(CONF_AWAY_MODE_COMMAND_TOPIC), + cv.removed(CONF_AWAY_MODE_STATE_TEMPLATE), + cv.removed(CONF_AWAY_MODE_STATE_TOPIC), + cv.removed(CONF_HOLD_COMMAND_TEMPLATE), + cv.removed(CONF_HOLD_COMMAND_TOPIC), + cv.removed(CONF_HOLD_STATE_TEMPLATE), + cv.removed(CONF_HOLD_STATE_TOPIC), + cv.removed(CONF_HOLD_LIST), valid_preset_mode_configuration, warn_for_legacy_schema(climate.DOMAIN), ) @@ -353,15 +314,15 @@ DISCOVERY_SCHEMA = vol.All( _DISCOVERY_SCHEMA_BASE, # Support CONF_SEND_IF_OFF is removed with release 2022.9 cv.removed(CONF_SEND_IF_OFF), - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - cv.deprecated(CONF_AWAY_MODE_COMMAND_TOPIC), - cv.deprecated(CONF_AWAY_MODE_STATE_TEMPLATE), - cv.deprecated(CONF_AWAY_MODE_STATE_TOPIC), - cv.deprecated(CONF_HOLD_COMMAND_TEMPLATE), - cv.deprecated(CONF_HOLD_COMMAND_TOPIC), - cv.deprecated(CONF_HOLD_STATE_TEMPLATE), - cv.deprecated(CONF_HOLD_STATE_TOPIC), - cv.deprecated(CONF_HOLD_LIST), + # AWAY and HOLD mode topics and templates are no longer supported, support was removed with release 2022.9 + cv.removed(CONF_AWAY_MODE_COMMAND_TOPIC), + cv.removed(CONF_AWAY_MODE_STATE_TEMPLATE), + cv.removed(CONF_AWAY_MODE_STATE_TOPIC), + cv.removed(CONF_HOLD_COMMAND_TEMPLATE), + cv.removed(CONF_HOLD_COMMAND_TOPIC), + cv.removed(CONF_HOLD_STATE_TEMPLATE), + cv.removed(CONF_HOLD_STATE_TOPIC), + cv.removed(CONF_HOLD_LIST), valid_preset_mode_configuration, ) @@ -419,12 +380,10 @@ class MqttClimate(MqttEntity, ClimateEntity): """Initialize the climate device.""" self._action = None self._aux = False - self._away = False self._current_fan_mode = None self._current_operation = None self._current_swing_mode = None self._current_temp = None - self._hold = None self._preset_mode = None self._target_temp = None self._target_temp_high = None @@ -435,10 +394,6 @@ class MqttClimate(MqttEntity, ClimateEntity): self._feature_preset_mode = False self._optimistic_preset_mode = None - # AWAY and HOLD mode topics and templates are deprecated, - # support will be removed with release 2022.9 - self._hold_list = [] - MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod @@ -477,9 +432,6 @@ class MqttClimate(MqttEntity, ClimateEntity): self._preset_modes = [] self._optimistic_preset_mode = CONF_PRESET_MODE_STATE_TOPIC not in config self._action = None - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - self._away = False - self._hold = None self._aux = False value_templates = {} @@ -507,11 +459,6 @@ class MqttClimate(MqttEntity, ClimateEntity): self._command_templates = command_templates - # AWAY and HOLD mode topics and templates are deprecated, - # support will be removed with release 2022.9 - if CONF_HOLD_LIST in config: - self._hold_list = config[CONF_HOLD_LIST] - def _prepare_subscribe_topics(self): # noqa: C901 """(Re)Subscribe to topics.""" topics = {} @@ -682,15 +629,6 @@ class MqttClimate(MqttEntity, ClimateEntity): self.async_write_ha_state() - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - @callback - @log_messages(self.hass, self.entity_id) - def handle_away_mode_received(msg): - """Handle receiving away mode via MQTT.""" - handle_onoff_mode_received(msg, CONF_AWAY_MODE_STATE_TEMPLATE, "_away") - - add_subscription(topics, CONF_AWAY_MODE_STATE_TOPIC, handle_away_mode_received) - @callback @log_messages(self.hass, self.entity_id) def handle_aux_mode_received(msg): @@ -699,22 +637,6 @@ class MqttClimate(MqttEntity, ClimateEntity): add_subscription(topics, CONF_AUX_STATE_TOPIC, handle_aux_mode_received) - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - @callback - @log_messages(self.hass, self.entity_id) - def handle_hold_mode_received(msg): - """Handle receiving hold mode via MQTT.""" - payload = render_template(msg, CONF_HOLD_STATE_TEMPLATE) - - if payload == "off": - payload = None - - self._hold = payload - self._preset_mode = None - self.async_write_ha_state() - - add_subscription(topics, CONF_HOLD_STATE_TOPIC, handle_hold_mode_received) - @callback @log_messages(self.hass, self.entity_id) def handle_preset_mode_received(msg): @@ -802,11 +724,6 @@ class MqttClimate(MqttEntity, ClimateEntity): """Return preset mode.""" if self._feature_preset_mode and self._preset_mode is not None: return self._preset_mode - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - if self._hold: - return self._hold - if self._away: - return PRESET_AWAY return PRESET_NONE @property @@ -814,17 +731,6 @@ class MqttClimate(MqttEntity, ClimateEntity): """Return preset modes.""" presets = [] presets.extend(self._preset_modes) - - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - if (self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None) or ( - self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None - ): - presets.append(PRESET_AWAY) - - # AWAY and HOLD mode topics and templates are deprecated, - # support will be removed with release 2022.9 - presets.extend(self._hold_list) - if presets: presets.insert(0, PRESET_NONE) @@ -895,7 +801,6 @@ class MqttClimate(MqttEntity, ClimateEntity): "_target_temp_high", ) - # Always optimistic? self.async_write_ha_state() async def async_set_swing_mode(self, swing_mode: str) -> None: @@ -962,49 +867,6 @@ class MqttClimate(MqttEntity, ClimateEntity): return - # Update hold or away mode: Track if we should optimistic update the state - optimistic_update = await self._set_away_mode(preset_mode == PRESET_AWAY) - hold_mode: str | None = preset_mode - if preset_mode in [PRESET_NONE, PRESET_AWAY]: - hold_mode = None - optimistic_update = await self._set_hold_mode(hold_mode) or optimistic_update - - if optimistic_update: - self.async_write_ha_state() - - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - async def _set_away_mode(self, state): - """Set away mode. - - Returns if we should optimistically write the state. - """ - await self._publish( - CONF_AWAY_MODE_COMMAND_TOPIC, - self._config[CONF_PAYLOAD_ON] if state else self._config[CONF_PAYLOAD_OFF], - ) - - if self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None: - return False - - self._away = state - return True - - async def _set_hold_mode(self, hold_mode): - """Set hold mode. - - Returns if we should optimistically write the state. - """ - payload = self._command_templates[CONF_HOLD_COMMAND_TEMPLATE]( - hold_mode or "off" - ) - await self._publish(CONF_HOLD_COMMAND_TOPIC, payload) - - if self._topic[CONF_HOLD_STATE_TOPIC] is not None: - return False - - self._hold = hold_mode - return True - async def _set_aux_heat(self, state): await self._publish( CONF_AUX_COMMAND_TOPIC, @@ -1053,14 +915,7 @@ class MqttClimate(MqttEntity, ClimateEntity): ): support |= ClimateEntityFeature.SWING_MODE - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - if ( - self._feature_preset_mode - or (self._topic[CONF_AWAY_MODE_STATE_TOPIC] is not None) - or (self._topic[CONF_AWAY_MODE_COMMAND_TOPIC] is not None) - or (self._topic[CONF_HOLD_STATE_TOPIC] is not None) - or (self._topic[CONF_HOLD_COMMAND_TOPIC] is not None) - ): + if self._feature_preset_mode: support |= ClimateEntityFeature.PRESET_MODE if (self._topic[CONF_AUX_STATE_TOPIC] is not None) or ( diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 679f853a3a8..d5164e85718 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -13,15 +13,12 @@ from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_HVAC_ACTION, - ATTR_PRESET_MODE, ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_ACTIONS, DOMAIN as CLIMATE_DOMAIN, - PRESET_AWAY, PRESET_ECO, - PRESET_NONE, ClimateEntityFeature, HVACMode, ) @@ -89,23 +86,6 @@ DEFAULT_CONFIG = { } } -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -DEFAULT_LEGACY_CONFIG = { - CLIMATE_DOMAIN: { - "platform": "mqtt", - "name": "test", - "mode_command_topic": "mode-topic", - "temperature_command_topic": "temperature-topic", - "temperature_low_command_topic": "temperature-low-topic", - "temperature_high_command_topic": "temperature-high-topic", - "fan_mode_command_topic": "fan-mode-topic", - "swing_mode_command_topic": "swing-mode-topic", - "aux_command_topic": "aux-topic", - "away_mode_command_topic": "away-mode-topic", - "hold_command_topic": "hold-topic", - } -} - @pytest.fixture(autouse=True) def climate_platform_only(): @@ -654,241 +634,6 @@ async def test_set_preset_mode_pessimistic( assert state.attributes.get("preset_mode") == "home" -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_away_mode_pessimistic(hass, mqtt_mock_entry_with_yaml_config): - """Test setting of the away mode.""" - config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) - config["climate"]["away_mode_state_topic"] = "away-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - - await common.async_set_preset_mode(hass, "away", ENTITY_CLIMATE) - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - - async_fire_mqtt_message(hass, "away-state", "ON") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "away" - - async_fire_mqtt_message(hass, "away-state", "OFF") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - - async_fire_mqtt_message(hass, "away-state", "nonsense") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - - -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_away_mode(hass, mqtt_mock_entry_with_yaml_config): - """Test setting of the away mode.""" - config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) - config["climate"]["payload_on"] = "AN" - config["climate"]["payload_off"] = "AUS" - - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - - mqtt_mock.async_publish.reset_mock() - await common.async_set_preset_mode(hass, "away", ENTITY_CLIMATE) - assert mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "AN", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "off", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "away" - - await common.async_set_preset_mode(hass, PRESET_NONE, ENTITY_CLIMATE) - assert mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "AUS", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "off", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - - await common.async_set_preset_mode(hass, "hold-on", ENTITY_CLIMATE) - mqtt_mock.async_publish.reset_mock() - - await common.async_set_preset_mode(hass, "away", ENTITY_CLIMATE) - assert mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "AN", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "off", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "away" - - -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_hold_pessimistic(hass, mqtt_mock_entry_with_yaml_config): - """Test setting the hold mode in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) - config["climate"]["hold_state_topic"] = "hold-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("hold_mode") is None - - await common.async_set_preset_mode(hass, "hold", ENTITY_CLIMATE) - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("hold_mode") is None - - async_fire_mqtt_message(hass, "hold-state", "on") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "on" - - async_fire_mqtt_message(hass, "hold-state", "off") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - - -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_hold(hass, mqtt_mock_entry_with_yaml_config): - """Test setting the hold mode.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - await common.async_set_preset_mode(hass, "hold-on", ENTITY_CLIMATE) - mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "hold-on", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "hold-on" - - await common.async_set_preset_mode(hass, PRESET_ECO, ENTITY_CLIMATE) - mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "eco", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == PRESET_ECO - - await common.async_set_preset_mode(hass, PRESET_NONE, ENTITY_CLIMATE) - mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "off", 0, False) - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - - -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_preset_away(hass, mqtt_mock_entry_with_yaml_config): - """Test setting the hold mode and away mode.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == PRESET_NONE - - await common.async_set_preset_mode(hass, "hold-on", ENTITY_CLIMATE) - mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "hold-on", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "hold-on" - - await common.async_set_preset_mode(hass, PRESET_AWAY, ENTITY_CLIMATE) - assert mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "ON", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "off", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == PRESET_AWAY - - await common.async_set_preset_mode(hass, "hold-on-again", ENTITY_CLIMATE) - assert mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("hold-topic", "hold-on-again", 0, False) - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "hold-on-again" - - -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_preset_away_pessimistic(hass, mqtt_mock_entry_with_yaml_config): - """Test setting the hold mode and away mode in pessimistic mode.""" - config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) - config["climate"]["hold_state_topic"] = "hold-state" - config["climate"]["away_mode_state_topic"] = "away-state" - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == PRESET_NONE - - await common.async_set_preset_mode(hass, "hold-on", ENTITY_CLIMATE) - mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "hold-on", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == PRESET_NONE - - async_fire_mqtt_message(hass, "hold-state", "hold-on") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "hold-on" - - await common.async_set_preset_mode(hass, PRESET_AWAY, ENTITY_CLIMATE) - assert mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "ON", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "off", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "hold-on" - - async_fire_mqtt_message(hass, "away-state", "ON") - async_fire_mqtt_message(hass, "hold-state", "off") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == PRESET_AWAY - - await common.async_set_preset_mode(hass, "hold-on-again", ENTITY_CLIMATE) - assert mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("hold-topic", "hold-on-again", 0, False) - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == PRESET_AWAY - - async_fire_mqtt_message(hass, "hold-state", "hold-on-again") - async_fire_mqtt_message(hass, "away-state", "OFF") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "hold-on-again" - - -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_preset_mode_twice(hass, mqtt_mock_entry_with_yaml_config): - """Test setting of the same mode twice only publishes once.""" - assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_LEGACY_CONFIG) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - await common.async_set_preset_mode(hass, "hold-on", ENTITY_CLIMATE) - mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "hold-on", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "hold-on" - - async def test_set_aux_pessimistic(hass, mqtt_mock_entry_with_yaml_config): """Test setting of the aux heating in pessimistic mode.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -1103,57 +848,6 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog ) -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_get_with_hold_and_away_mode_and_templates( - hass, mqtt_mock_entry_with_yaml_config, caplog -): - """Test getting various for hold and away mode attributes with templates.""" - config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) - config["climate"]["mode_state_topic"] = "mode-state" - # By default, just unquote the JSON-strings - config["climate"]["value_template"] = "{{ value_json }}" - # Something more complicated for hold mode - config["climate"]["hold_state_template"] = "{{ value_json.attribute }}" - config["climate"]["away_mode_state_topic"] = "away-state" - config["climate"]["hold_state_topic"] = "hold-state" - - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - - # Operation Mode - state = hass.states.get(ENTITY_CLIMATE) - async_fire_mqtt_message(hass, "mode-state", '"cool"') - state = hass.states.get(ENTITY_CLIMATE) - assert state.state == "cool" - - # Away Mode - assert state.attributes.get("preset_mode") == "none" - async_fire_mqtt_message(hass, "away-state", '"ON"') - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "away" - - # Away Mode with JSON values - async_fire_mqtt_message(hass, "away-state", "false") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "none" - - async_fire_mqtt_message(hass, "away-state", "true") - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "away" - - # Hold Mode - async_fire_mqtt_message( - hass, - "hold-state", - """ - { "attribute": "somemode" } - """, - ) - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == "somemode" - - async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test setting various attributes with templates.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -1232,29 +926,6 @@ async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog) assert state.attributes.get("target_temp_high") == 23 -# AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 -async def test_set_with_away_and_hold_modes_and_templates( - hass, mqtt_mock_entry_with_yaml_config, caplog -): - """Test setting various attributes on hold and away mode with templates.""" - config = copy.deepcopy(DEFAULT_LEGACY_CONFIG) - # Create simple templates - config["climate"]["hold_command_template"] = "hold: {{ value }}" - - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - - # Hold Mode - await common.async_set_preset_mode(hass, PRESET_ECO, ENTITY_CLIMATE) - mqtt_mock.async_publish.call_count == 2 - mqtt_mock.async_publish.assert_any_call("away-mode-topic", "OFF", 0, False) - mqtt_mock.async_publish.assert_any_call("hold-topic", "hold: eco", 0, False) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get(ENTITY_CLIMATE) - assert state.attributes.get("preset_mode") == PRESET_ECO - - async def test_min_temp_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom min temp.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -1404,12 +1075,8 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): ("action_topic", "heating", ATTR_HVAC_ACTION, "heating"), ("action_topic", "cooling", ATTR_HVAC_ACTION, "cooling"), ("aux_state_topic", "ON", ATTR_AUX_HEAT, "on"), - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - ("away_mode_state_topic", "ON", ATTR_PRESET_MODE, "away"), ("current_temperature_topic", "22.1", ATTR_CURRENT_TEMPERATURE, 22.1), ("fan_mode_state_topic", "low", ATTR_FAN_MODE, "low"), - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - ("hold_state_topic", "mode1", ATTR_PRESET_MODE, "mode1"), ("mode_state_topic", "cool", None, None), ("mode_state_topic", "fan_only", None, None), ("swing_mode_state_topic", "on", ATTR_SWING_MODE, "on"), @@ -1429,11 +1096,6 @@ async def test_encoding_subscribable_topics( ): """Test handling of incoming encoded payload.""" config = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) - # AWAY and HOLD mode topics and templates are deprecated, support will be removed with release 2022.9 - if topic in ["hold_state_topic", "away_mode_state_topic"]: - config["hold_modes"] = ["mode1", "mode2"] - del config["preset_modes"] - del config["preset_mode_command_topic"] await help_test_encoding_subscribable_topics( hass, mqtt_mock_entry_with_yaml_config, @@ -1638,27 +1300,6 @@ async def test_precision_whole(hass, mqtt_mock_entry_with_yaml_config): "sleep", "preset_mode_command_template", ), - ( - climate.SERVICE_SET_PRESET_MODE, - "away_mode_command_topic", - {"preset_mode": "away"}, - "ON", - None, - ), - ( - climate.SERVICE_SET_PRESET_MODE, - "hold_command_topic", - {"preset_mode": "eco"}, - "eco", - "hold_command_template", - ), - ( - climate.SERVICE_SET_PRESET_MODE, - "hold_command_topic", - {"preset_mode": "comfort"}, - "comfort", - "hold_command_template", - ), ( climate.SERVICE_SET_FAN_MODE, "fan_mode_command_topic", From 9dedba4843e1348edfaa9800b6894373065b6741 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Aug 2022 20:48:06 -1000 Subject: [PATCH 3337/3516] Fix bad data with inkbird bbq sensors (#76739) --- homeassistant/components/inkbird/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/inkbird/test_config_flow.py | 6 +++--- tests/components/inkbird/test_sensor.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index b0ef08143c2..f65177ab6e2 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -10,7 +10,7 @@ { "local_name": "xBBQ*" }, { "local_name": "tps" } ], - "requirements": ["inkbird-ble==0.5.2"], + "requirements": ["inkbird-ble==0.5.5"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 1e7db64277c..1ab91b4cd05 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -899,7 +899,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.2 +inkbird-ble==0.5.5 # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 10e88fcc1ab..a360de10112 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -652,7 +652,7 @@ influxdb-client==1.24.0 influxdb==5.3.1 # homeassistant.components.inkbird -inkbird-ble==0.5.2 +inkbird-ble==0.5.5 # homeassistant.components.insteon insteon-frontend-home-assistant==0.2.0 diff --git a/tests/components/inkbird/test_config_flow.py b/tests/components/inkbird/test_config_flow.py index fe210f75f4b..4d9fbc65df7 100644 --- a/tests/components/inkbird/test_config_flow.py +++ b/tests/components/inkbird/test_config_flow.py @@ -25,7 +25,7 @@ async def test_async_step_bluetooth_valid_device(hass): result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "iBBQ 6AADDD4CAC3D" + assert result2["title"] == "iBBQ AC3D" assert result2["data"] == {} assert result2["result"].unique_id == "4125DDBA-2774-4851-9889-6AADDD4CAC3D" @@ -69,7 +69,7 @@ async def test_async_step_user_with_found_devices(hass): user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "IBS-TH 75BBE1738105" + assert result2["title"] == "IBS-TH 8105" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" @@ -184,7 +184,7 @@ async def test_async_step_user_takes_precedence_over_discovery(hass): user_input={"address": "61DE521B-F0BF-9F44-64D4-75BBE1738105"}, ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "IBS-TH 75BBE1738105" + assert result2["title"] == "IBS-TH 8105" assert result2["data"] == {} assert result2["result"].unique_id == "61DE521B-F0BF-9F44-64D4-75BBE1738105" diff --git a/tests/components/inkbird/test_sensor.py b/tests/components/inkbird/test_sensor.py index cafc22911c3..c54c6e3c242 100644 --- a/tests/components/inkbird/test_sensor.py +++ b/tests/components/inkbird/test_sensor.py @@ -39,10 +39,10 @@ async def test_sensors(hass): await hass.async_block_till_done() assert len(hass.states.async_all()) == 3 - temp_sensor = hass.states.get("sensor.ibs_th_75bbe1738105_battery") + temp_sensor = hass.states.get("sensor.ibs_th_8105_battery") temp_sensor_attribtes = temp_sensor.attributes assert temp_sensor.state == "87" - assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "IBS-TH 75BBE1738105 Battery" + assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "IBS-TH 8105 Battery" assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "%" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" From cf19536c43490d5c00ce5ae778669f74a67179d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Aug 2022 20:48:28 -1000 Subject: [PATCH 3338/3516] Fix stale data with SensorPush sensors (#76771) --- homeassistant/components/sensorpush/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensorpush/manifest.json b/homeassistant/components/sensorpush/manifest.json index a5d900aaf3b..906b5c22f6b 100644 --- a/homeassistant/components/sensorpush/manifest.json +++ b/homeassistant/components/sensorpush/manifest.json @@ -8,7 +8,7 @@ "local_name": "SensorPush*" } ], - "requirements": ["sensorpush-ble==1.5.1"], + "requirements": ["sensorpush-ble==1.5.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 1ab91b4cd05..1d5151cdf08 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2173,7 +2173,7 @@ sendgrid==6.8.2 sense_energy==0.10.4 # homeassistant.components.sensorpush -sensorpush-ble==1.5.1 +sensorpush-ble==1.5.2 # homeassistant.components.sentry sentry-sdk==1.9.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a360de10112..13d91ebe5fc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1470,7 +1470,7 @@ securetar==2022.2.0 sense_energy==0.10.4 # homeassistant.components.sensorpush -sensorpush-ble==1.5.1 +sensorpush-ble==1.5.2 # homeassistant.components.sentry sentry-sdk==1.9.3 From 68979009e349cf165f4f0deb4dfeb499f2d57ea2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Aug 2022 20:54:49 -1000 Subject: [PATCH 3339/3516] Bump aiohomekit to 1.2.11 (#76784) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 49635dd797c..ece53d29406 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.10"], + "requirements": ["aiohomekit==1.2.11"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 1d5151cdf08..057a979f156 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.10 +aiohomekit==1.2.11 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13d91ebe5fc..ec83da82d2d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.10 +aiohomekit==1.2.11 # homeassistant.components.emulated_hue # homeassistant.components.http From c9feda1562d7c96146d7cb216126445a1ec57cfa Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 15 Aug 2022 01:13:11 -0600 Subject: [PATCH 3340/3516] Fix missing state classes on various Ambient PWS entities (#76683) --- .../components/ambient_station/sensor.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index 1a51e57bfa3..a04c279915f 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -203,78 +203,91 @@ SENSOR_DESCRIPTIONS = ( name="Humidity 10", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY1, name="Humidity 1", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY2, name="Humidity 2", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY3, name="Humidity 3", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY4, name="Humidity 4", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY5, name="Humidity 5", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY6, name="Humidity 6", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY7, name="Humidity 7", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY8, name="Humidity 8", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY9, name="Humidity 9", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITY, name="Humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_HUMIDITYIN, name="Humidity in", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_LASTRAIN, name="Last rain", icon="mdi:water", device_class=SensorDeviceClass.TIMESTAMP, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_LIGHTNING_PER_DAY, @@ -335,60 +348,70 @@ SENSOR_DESCRIPTIONS = ( name="Soil humidity 10", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM1, name="Soil humidity 1", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM2, name="Soil humidity 2", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM3, name="Soil humidity 3", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM4, name="Soil humidity 4", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM5, name="Soil humidity 5", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM6, name="Soil humidity 6", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM7, name="Soil humidity 7", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM8, name="Soil humidity 8", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILHUM9, name="Soil humidity 9", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_SOILTEMP10F, From 6243f24b05e2c766a107ffaa44072b6cef036255 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 15 Aug 2022 09:48:03 +0200 Subject: [PATCH 3341/3516] Add media-player checks to pylint plugin (#76675) * Add media-player checks to pylint plugin * Fix invalid hints * Add tests * Adjust tests * Add extra test * Adjust regex * Cleanup comment * Move media player tests up --- pylint/plugins/hass_enforce_type_hints.py | 318 +++++++++++++++++++++- tests/pylint/test_enforce_type_hints.py | 30 ++ 2 files changed, 347 insertions(+), 1 deletion(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 852c5b544c4..b6eecdadfee 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -59,7 +59,7 @@ _TYPE_HINT_MATCHERS: dict[str, re.Pattern[str]] = { # a_or_b matches items such as "DiscoveryInfoType | None" "a_or_b": re.compile(r"^(\w+) \| (\w+)$"), } -_INNER_MATCH = r"((?:\w+)|(?:\.{3})|(?:\w+\[.+\]))" +_INNER_MATCH = r"((?:[\w\| ]+)|(?:\.{3})|(?:\w+\[.+\]))" _INNER_MATCH_POSSIBILITIES = [i + 1 for i in range(5)] _TYPE_HINT_MATCHERS.update( { @@ -1465,6 +1465,322 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "media_player": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="MediaPlayerEntity", + matches=[ + TypeHintMatch( + function_name="device_class", + return_type=["MediaPlayerDeviceClass", "str", None], + ), + TypeHintMatch( + function_name="state", + return_type=["str", None], + ), + TypeHintMatch( + function_name="access_token", + return_type="str", + ), + TypeHintMatch( + function_name="volume_level", + return_type=["float", None], + ), + TypeHintMatch( + function_name="is_volume_muted", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="media_content_id", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_content_type", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_duration", + return_type=["int", None], + ), + TypeHintMatch( + function_name="media_position", + return_type=["int", None], + ), + TypeHintMatch( + function_name="media_position_updated_at", + return_type=["datetime", None], + ), + TypeHintMatch( + function_name="media_image_url", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_image_remotely_accessible", + return_type="bool", + ), + TypeHintMatch( + function_name="media_image_hash", + return_type=["str", None], + ), + TypeHintMatch( + function_name="async_get_media_image", + return_type="tuple[bytes | None, str | None]", + ), + TypeHintMatch( + function_name="async_get_browse_image", + arg_types={ + 1: "str", + 2: "str", + 3: "str | None", + }, + return_type="tuple[bytes | None, str | None]", + ), + TypeHintMatch( + function_name="media_title", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_artist", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_album_name", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_album_artist", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_track", + return_type=["int", None], + ), + TypeHintMatch( + function_name="media_series_title", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_season", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_episode", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_channel", + return_type=["str", None], + ), + TypeHintMatch( + function_name="media_playlist", + return_type=["str", None], + ), + TypeHintMatch( + function_name="app_id", + return_type=["str", None], + ), + TypeHintMatch( + function_name="app_name", + return_type=["str", None], + ), + TypeHintMatch( + function_name="source", + return_type=["str", None], + ), + TypeHintMatch( + function_name="source_list", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="sound_mode", + return_type=["str", None], + ), + TypeHintMatch( + function_name="sound_mode_list", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="shuffle", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="repeat", + return_type=["str", None], + ), + TypeHintMatch( + function_name="group_members", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="turn_on", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_off", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="mute_volume", + arg_types={ + 1: "bool", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_volume_level", + arg_types={ + 1: "float", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_play", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_pause", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_stop", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_previous_track", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_next_track", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_seek", + arg_types={ + 1: "float", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="play_media", + arg_types={ + 1: "str", + 2: "str", + }, + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="select_source", + arg_types={ + 1: "str", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="select_sound_mode", + arg_types={ + 1: "str", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="clear_playlist", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_shuffle", + arg_types={ + 1: "bool", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_repeat", + arg_types={ + 1: "str", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="toggle", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="volume_up", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="volume_down", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_play_pause", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="media_image_local", + return_type=["str", None], + ), + TypeHintMatch( + function_name="capability_attributes", + return_type="dict[str, Any]", + ), + TypeHintMatch( + function_name="async_browse_media", + arg_types={ + 1: "str | None", + 2: "str | None", + }, + return_type="BrowseMedia", + ), + TypeHintMatch( + function_name="join_players", + arg_types={ + 1: "list[str]", + }, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="unjoin_player", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="get_browse_image_url", + arg_types={ + 1: "str", + 2: "str", + 3: "str | None", + }, + return_type="str", + ), + ], + ), + ], "number": [ ClassTypeHintMatch( base_class="Entity", diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index b3c233d1c3b..1381ed34a7b 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -53,6 +53,7 @@ def test_regex_get_module_platform( ("Awaitable[None]", 1, ("Awaitable", "None")), ("list[dict[str, str]]", 1, ("list", "dict[str, str]")), ("list[dict[str, Any]]", 1, ("list", "dict[str, Any]")), + ("tuple[bytes | None, str | None]", 2, ("tuple", "bytes | None", "str | None")), ], ) def test_regex_x_of_y_i( @@ -902,6 +903,35 @@ def test_invalid_device_class( type_hint_checker.visit_classdef(class_node) +def test_media_player_entity( + linter: UnittestLinter, type_hint_checker: BaseChecker +) -> None: + """Ensure valid hints are accepted for media_player entity.""" + # Set bypass option + type_hint_checker.config.ignore_missing_annotations = False + + class_node = astroid.extract_node( + """ + class Entity(): + pass + + class MediaPlayerEntity(Entity): + pass + + class MyMediaPlayer( #@ + MediaPlayerEntity + ): + async def async_get_media_image(self) -> tuple[bytes | None, str | None]: + pass + """, + "homeassistant.components.pylint_test.media_player", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_no_messages(linter): + type_hint_checker.visit_classdef(class_node) + + def test_number_entity(linter: UnittestLinter, type_hint_checker: BaseChecker) -> None: """Ensure valid hints are accepted for number entity.""" # Set bypass option From f4c23ad68dd70bda8cc5e0eeee3a71f736f01694 Mon Sep 17 00:00:00 2001 From: Frank <46161394+BraveChicken1@users.noreply.github.com> Date: Mon, 15 Aug 2022 10:44:14 +0200 Subject: [PATCH 3342/3516] Bump homeconnect to 0.7.2 (#76773) --- homeassistant/components/home_connect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json index ca6e0f012ac..c9aa5d229b8 100644 --- a/homeassistant/components/home_connect/manifest.json +++ b/homeassistant/components/home_connect/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/home_connect", "dependencies": ["application_credentials"], "codeowners": ["@DavidMStraub"], - "requirements": ["homeconnect==0.7.1"], + "requirements": ["homeconnect==0.7.2"], "config_flow": true, "iot_class": "cloud_push", "loggers": ["homeconnect"] diff --git a/requirements_all.txt b/requirements_all.txt index 057a979f156..130c40541d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -839,7 +839,7 @@ holidays==0.14.2 home-assistant-frontend==20220802.0 # homeassistant.components.home_connect -homeconnect==0.7.1 +homeconnect==0.7.2 # homeassistant.components.homematicip_cloud homematicip==1.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec83da82d2d..0977ec957ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,7 +616,7 @@ holidays==0.14.2 home-assistant-frontend==20220802.0 # homeassistant.components.home_connect -homeconnect==0.7.1 +homeconnect==0.7.2 # homeassistant.components.homematicip_cloud homematicip==1.0.7 From 669e6bec39f1507320aefa2bd7c5a28e93338360 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 15 Aug 2022 11:23:38 +0200 Subject: [PATCH 3343/3516] Bump aiohue to 4.5.0 (#76757) --- homeassistant/components/hue/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index b3dbe4df50a..3854b861c98 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,7 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==4.4.2"], + "requirements": ["aiohue==4.5.0"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 130c40541d8..8533e2276e0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aiohomekit==1.2.11 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.4.2 +aiohue==4.5.0 # homeassistant.components.imap aioimaplib==1.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0977ec957ec..1a4f9c64229 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -159,7 +159,7 @@ aiohomekit==1.2.11 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.4.2 +aiohue==4.5.0 # homeassistant.components.apache_kafka aiokafka==0.7.2 From 8f0f734c288e92b4f2f74aee45465214d278a346 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 15 Aug 2022 11:35:53 +0200 Subject: [PATCH 3344/3516] Fix entity category for LIFX buttons (#76788) --- homeassistant/components/lifx/button.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lifx/button.py b/homeassistant/components/lifx/button.py index 3a4c73d2889..76afdc785e9 100644 --- a/homeassistant/components/lifx/button.py +++ b/homeassistant/components/lifx/button.py @@ -19,13 +19,13 @@ RESTART_BUTTON_DESCRIPTION = ButtonEntityDescription( key=RESTART, name="Restart", device_class=ButtonDeviceClass.RESTART, - entity_category=EntityCategory.DIAGNOSTIC, + entity_category=EntityCategory.CONFIG, ) IDENTIFY_BUTTON_DESCRIPTION = ButtonEntityDescription( key=IDENTIFY, name="Identify", - entity_category=EntityCategory.DIAGNOSTIC, + entity_category=EntityCategory.CONFIG, ) From f443edfef4808cf1df26d8ffde586e1d92592f7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 15 Aug 2022 11:49:22 +0200 Subject: [PATCH 3345/3516] Enable statistics for WLED WiFi RSSI/Signal sensors (#76789) --- homeassistant/components/wled/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 4a677910273..1b5e81ec469 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -96,6 +96,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( name="Wi-Fi signal", icon="mdi:wifi", native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda device: device.info.wifi.signal if device.info.wifi else None, @@ -105,6 +106,7 @@ SENSORS: tuple[WLEDSensorEntityDescription, ...] = ( name="Wi-Fi RSSI", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=lambda device: device.info.wifi.rssi if device.info.wifi else None, From cf867730cdad643bf9875cfe1e5bf855d143a3ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Mon, 15 Aug 2022 12:12:31 +0200 Subject: [PATCH 3346/3516] Update aioqsw to v0.2.2 (#76760) --- homeassistant/components/qnap_qsw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/qnap_qsw/test_coordinator.py | 6 ++++++ tests/components/qnap_qsw/util.py | 16 ++++++++++++++++ 5 files changed, 25 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qnap_qsw/manifest.json b/homeassistant/components/qnap_qsw/manifest.json index 690297d69bc..91d77a7362f 100644 --- a/homeassistant/components/qnap_qsw/manifest.json +++ b/homeassistant/components/qnap_qsw/manifest.json @@ -3,7 +3,7 @@ "name": "QNAP QSW", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/qnap_qsw", - "requirements": ["aioqsw==0.2.0"], + "requirements": ["aioqsw==0.2.2"], "codeowners": ["@Noltari"], "iot_class": "local_polling", "loggers": ["aioqsw"], diff --git a/requirements_all.txt b/requirements_all.txt index 8533e2276e0..a500a38fa1b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -232,7 +232,7 @@ aiopvpc==3.0.0 aiopyarr==22.7.0 # homeassistant.components.qnap_qsw -aioqsw==0.2.0 +aioqsw==0.2.2 # homeassistant.components.recollect_waste aiorecollect==1.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1a4f9c64229..a3289b4b028 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -207,7 +207,7 @@ aiopvpc==3.0.0 aiopyarr==22.7.0 # homeassistant.components.qnap_qsw -aioqsw==0.2.0 +aioqsw==0.2.2 # homeassistant.components.recollect_waste aiorecollect==1.0.8 diff --git a/tests/components/qnap_qsw/test_coordinator.py b/tests/components/qnap_qsw/test_coordinator.py index 125b333c8d6..61d1fa04200 100644 --- a/tests/components/qnap_qsw/test_coordinator.py +++ b/tests/components/qnap_qsw/test_coordinator.py @@ -18,6 +18,7 @@ from .util import ( FIRMWARE_CONDITION_MOCK, FIRMWARE_INFO_MOCK, FIRMWARE_UPDATE_CHECK_MOCK, + LACP_INFO_MOCK, PORTS_STATISTICS_MOCK, PORTS_STATUS_MOCK, SYSTEM_BOARD_MOCK, @@ -46,6 +47,9 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_update_check", return_value=FIRMWARE_UPDATE_CHECK_MOCK, ) as mock_firmware_update_check, patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_lacp_info", + return_value=LACP_INFO_MOCK, + ) as mock_lacp_info, patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_statistics", return_value=PORTS_STATISTICS_MOCK, ) as mock_ports_statistics, patch( @@ -73,6 +77,7 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: mock_firmware_condition.assert_called_once() mock_firmware_info.assert_called_once() mock_firmware_update_check.assert_called_once() + mock_lacp_info.assert_called_once() mock_ports_statistics.assert_called_once() mock_ports_status.assert_called_once() mock_system_board.assert_called_once() @@ -84,6 +89,7 @@ async def test_coordinator_client_connector_error(hass: HomeAssistant) -> None: mock_firmware_condition.reset_mock() mock_firmware_info.reset_mock() mock_firmware_update_check.reset_mock() + mock_lacp_info.reset_mock() mock_ports_statistics.reset_mock() mock_ports_status.reset_mock() mock_system_board.reset_mock() diff --git a/tests/components/qnap_qsw/util.py b/tests/components/qnap_qsw/util.py index d3a62d413fa..5ae801283bc 100644 --- a/tests/components/qnap_qsw/util.py +++ b/tests/components/qnap_qsw/util.py @@ -23,6 +23,8 @@ from aioqsw.const import ( API_KEY, API_LINK, API_MAC_ADDR, + API_MAX_PORT_CHANNELS, + API_MAX_PORTS_PER_PORT_CHANNEL, API_MAX_SWITCH_TEMP, API_MESSAGE, API_MODEL, @@ -36,6 +38,7 @@ from aioqsw.const import ( API_RX_OCTETS, API_SERIAL, API_SPEED, + API_START_INDEX, API_SWITCH_TEMP, API_TRUNK_NUM, API_TX_OCTETS, @@ -120,6 +123,16 @@ FIRMWARE_UPDATE_CHECK_MOCK = { }, } +LACP_INFO_MOCK = { + API_ERROR_CODE: 200, + API_ERROR_MESSAGE: "OK", + API_RESULT: { + API_START_INDEX: 28, + API_MAX_PORT_CHANNELS: 8, + API_MAX_PORTS_PER_PORT_CHANNEL: 8, + }, +} + PORTS_STATISTICS_MOCK = { API_ERROR_CODE: 200, API_ERROR_MESSAGE: "OK", @@ -499,6 +512,9 @@ async def async_init_integration( ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_firmware_update_check", return_value=FIRMWARE_UPDATE_CHECK_MOCK, + ), patch( + "homeassistant.components.qnap_qsw.QnapQswApi.get_lacp_info", + return_value=LACP_INFO_MOCK, ), patch( "homeassistant.components.qnap_qsw.QnapQswApi.get_ports_statistics", return_value=PORTS_STATISTICS_MOCK, From 4239757c2c3a22bc2518211c69e58a8117a3e022 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Mon, 15 Aug 2022 13:55:23 +0200 Subject: [PATCH 3347/3516] Bump bimmer_connected to 0.10.2 (#76751) Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index 0381035a63e..f540176a837 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,7 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": ["bimmer_connected==0.10.1"], + "requirements": ["bimmer_connected==0.10.2"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index a500a38fa1b..10e48ccf2ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -399,7 +399,7 @@ beautifulsoup4==4.11.1 bellows==0.32.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.10.1 +bimmer_connected==0.10.2 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a3289b4b028..796138e09ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -323,7 +323,7 @@ beautifulsoup4==4.11.1 bellows==0.32.0 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.10.1 +bimmer_connected==0.10.2 # homeassistant.components.bluetooth bleak==0.15.1 From c93d9d9a90227fbffd2e36c04710183c8cca999d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 14:37:11 +0200 Subject: [PATCH 3348/3516] Move `AutomationActionType` to helpers.trigger (#76790) --- .../components/automation/__init__.py | 42 ++++++------------- .../binary_sensor/device_trigger.py | 11 ++--- homeassistant/helpers/trigger.py | 39 +++++++++++++---- pylint/plugins/hass_enforce_type_hints.py | 4 +- 4 files changed, 50 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index c3a669511bc..43ff31dfab8 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable import logging -from typing import Any, Protocol, TypedDict, cast +from typing import Any, cast import voluptuous as vol from voluptuous.humanize import humanize_error @@ -72,8 +72,13 @@ from homeassistant.helpers.trace import ( trace_get, trace_path, ) -from homeassistant.helpers.trigger import async_initialize_triggers -from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.trigger import ( + TriggerActionType, + TriggerData, + TriggerInfo, + async_initialize_triggers, +) +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.dt import parse_datetime @@ -112,32 +117,11 @@ SERVICE_TRIGGER = "trigger" _LOGGER = logging.getLogger(__name__) -class AutomationActionType(Protocol): - """Protocol type for automation action callback.""" - - async def __call__( - self, - run_variables: dict[str, Any], - context: Context | None = None, - ) -> None: - """Define action callback type.""" - - -class AutomationTriggerData(TypedDict): - """Automation trigger data.""" - - id: str - idx: str - - -class AutomationTriggerInfo(TypedDict): - """Information about automation trigger.""" - - domain: str - name: str - home_assistant_start: bool - variables: TemplateVarsType - trigger_data: AutomationTriggerData +# AutomationActionType, AutomationTriggerData, +# and AutomationTriggerInfo are deprecated as of 2022.9. +AutomationActionType = TriggerActionType +AutomationTriggerData = TriggerData +AutomationTriggerInfo = TriggerInfo @bind_hass diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index 12b620b8c4f..a6dfc762d45 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -1,10 +1,6 @@ """Provides device triggers for binary sensors.""" import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.const import ( CONF_TURNED_OFF, @@ -15,6 +11,7 @@ from homeassistant.const import CONF_ENTITY_ID, CONF_FOR, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity import get_device_class +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN, BinarySensorDeviceClass @@ -263,8 +260,8 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_type = config[CONF_TYPE] @@ -283,7 +280,7 @@ async def async_attach_trigger( state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index e90c684365d..a48ccbbb7b9 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -5,7 +5,7 @@ import asyncio from collections.abc import Callable import functools import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Protocol, TypedDict import voluptuous as vol @@ -27,6 +27,34 @@ _PLATFORM_ALIASES = { } +class TriggerActionType(Protocol): + """Protocol type for trigger action callback.""" + + async def __call__( + self, + run_variables: dict[str, Any], + context: Context | None = None, + ) -> None: + """Define action callback type.""" + + +class TriggerData(TypedDict): + """Trigger data.""" + + id: str + idx: str + + +class TriggerInfo(TypedDict): + """Information about trigger.""" + + domain: str + name: str + home_assistant_start: bool + variables: TemplateVarsType + trigger_data: TriggerData + + async def _async_get_trigger_platform( hass: HomeAssistant, config: ConfigType ) -> DeviceAutomationTriggerProtocol: @@ -93,11 +121,6 @@ async def async_initialize_triggers( variables: TemplateVarsType = None, ) -> CALLBACK_TYPE | None: """Initialize triggers.""" - from homeassistant.components.automation import ( # pylint:disable=[import-outside-toplevel] - AutomationTriggerData, - AutomationTriggerInfo, - ) - triggers = [] for idx, conf in enumerate(trigger_config): # Skip triggers that are not enabled @@ -107,8 +130,8 @@ async def async_initialize_triggers( platform = await _async_get_trigger_platform(hass, conf) trigger_id = conf.get(CONF_ID, f"{idx}") trigger_idx = f"{idx}" - trigger_data = AutomationTriggerData(id=trigger_id, idx=trigger_idx) - info = AutomationTriggerInfo( + trigger_data = TriggerData(id=trigger_id, idx=trigger_idx) + info = TriggerInfo( domain=domain, name=name, home_assistant_start=home_assistant_start, diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index b6eecdadfee..158848ba4d4 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -337,8 +337,8 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { arg_types={ 0: "HomeAssistant", 1: "ConfigType", - 2: "AutomationActionType", - 3: "AutomationTriggerInfo", + # 2: "AutomationActionType", # AutomationActionType is deprecated -> TriggerActionType + # 3: "AutomationTriggerInfo", # AutomationTriggerInfo is deprecated -> TriggerInfo }, return_type="CALLBACK_TYPE", ), From 64898af58ee4dc265d71720c5242659a8f8d880a Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 15 Aug 2022 14:00:29 +0100 Subject: [PATCH 3349/3516] Update systembridgeconnector to 3.4.4 (#75362) Co-authored-by: Martin Hjelmare --- .../components/system_bridge/__init__.py | 18 +- .../components/system_bridge/config_flow.py | 32 ++- .../components/system_bridge/coordinator.py | 13 +- .../components/system_bridge/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../system_bridge/test_config_flow.py | 208 ++++++++++++------ 7 files changed, 183 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/system_bridge/__init__.py b/homeassistant/components/system_bridge/__init__.py index 19bcc224a66..74be1faed40 100644 --- a/homeassistant/components/system_bridge/__init__.py +++ b/homeassistant/components/system_bridge/__init__.py @@ -10,6 +10,10 @@ from systembridgeconnector.exceptions import ( ConnectionClosedException, ConnectionErrorException, ) +from systembridgeconnector.models.keyboard_key import KeyboardKey +from systembridgeconnector.models.keyboard_text import KeyboardText +from systembridgeconnector.models.open_path import OpenPath +from systembridgeconnector.models.open_url import OpenUrl from systembridgeconnector.version import SUPPORTED_VERSION, Version import voluptuous as vol @@ -149,7 +153,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ call.data[CONF_BRIDGE] ] - await coordinator.websocket_client.open_path(call.data[CONF_PATH]) + await coordinator.websocket_client.open_path( + OpenPath(path=call.data[CONF_PATH]) + ) async def handle_open_url(call: ServiceCall) -> None: """Handle the open url service call.""" @@ -157,21 +163,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ call.data[CONF_BRIDGE] ] - await coordinator.websocket_client.open_url(call.data[CONF_URL]) + await coordinator.websocket_client.open_url(OpenUrl(url=call.data[CONF_URL])) async def handle_send_keypress(call: ServiceCall) -> None: """Handle the send_keypress service call.""" coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ call.data[CONF_BRIDGE] ] - await coordinator.websocket_client.keyboard_keypress(call.data[CONF_KEY]) + await coordinator.websocket_client.keyboard_keypress( + KeyboardKey(key=call.data[CONF_KEY]) + ) async def handle_send_text(call: ServiceCall) -> None: """Handle the send_keypress service call.""" coordinator: SystemBridgeDataUpdateCoordinator = hass.data[DOMAIN][ call.data[CONF_BRIDGE] ] - await coordinator.websocket_client.keyboard_text(call.data[CONF_TEXT]) + await coordinator.websocket_client.keyboard_text( + KeyboardText(text=call.data[CONF_TEXT]) + ) hass.services.async_register( DOMAIN, diff --git a/homeassistant/components/system_bridge/config_flow.py b/homeassistant/components/system_bridge/config_flow.py index 9d89cf83288..995df6391cc 100644 --- a/homeassistant/components/system_bridge/config_flow.py +++ b/homeassistant/components/system_bridge/config_flow.py @@ -7,12 +7,13 @@ import logging from typing import Any import async_timeout -from systembridgeconnector.const import EVENT_MODULE, EVENT_TYPE, TYPE_DATA_UPDATE from systembridgeconnector.exceptions import ( AuthenticationException, ConnectionClosedException, ConnectionErrorException, ) +from systembridgeconnector.models.get_data import GetData +from systembridgeconnector.models.system import System from systembridgeconnector.websocket_client import WebSocketClient import voluptuous as vol @@ -38,7 +39,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -async def validate_input( +async def _validate_input( hass: HomeAssistant, data: dict[str, Any], ) -> dict[str, str]: @@ -56,15 +57,12 @@ async def validate_input( try: async with async_timeout.timeout(30): await websocket_client.connect(session=async_get_clientsession(hass)) - await websocket_client.get_data(["system"]) - while True: - message = await websocket_client.receive_message() - _LOGGER.debug("Message: %s", message) - if ( - message[EVENT_TYPE] == TYPE_DATA_UPDATE - and message[EVENT_MODULE] == "system" - ): - break + hass.async_create_task(websocket_client.listen()) + response = await websocket_client.get_data(GetData(modules=["system"])) + _LOGGER.debug("Got response: %s", response.json()) + if response.data is None or not isinstance(response.data, System): + raise CannotConnect("No data received") + system: System = response.data except AuthenticationException as exception: _LOGGER.warning( "Authentication error when connecting to %s: %s", data[CONF_HOST], exception @@ -81,14 +79,12 @@ async def validate_input( except asyncio.TimeoutError as exception: _LOGGER.warning("Timed out connecting to %s: %s", data[CONF_HOST], exception) raise CannotConnect from exception + except ValueError as exception: + raise CannotConnect from exception - _LOGGER.debug("%s Message: %s", TYPE_DATA_UPDATE, message) + _LOGGER.debug("Got System data: %s", system.json()) - if "uuid" not in message["data"]: - error = "No UUID in result!" - raise CannotConnect(error) - - return {"hostname": host, "uuid": message["data"]["uuid"]} + return {"hostname": host, "uuid": system.uuid} async def _async_get_info( @@ -98,7 +94,7 @@ async def _async_get_info( errors = {} try: - info = await validate_input(hass, user_input) + info = await _validate_input(hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py index 1719d951cf0..695dca44342 100644 --- a/homeassistant/components/system_bridge/coordinator.py +++ b/homeassistant/components/system_bridge/coordinator.py @@ -5,6 +5,7 @@ import asyncio from collections.abc import Callable from datetime import timedelta import logging +from typing import Any import async_timeout from pydantic import BaseModel # pylint: disable=no-name-in-module @@ -17,8 +18,10 @@ from systembridgeconnector.models.battery import Battery from systembridgeconnector.models.cpu import Cpu from systembridgeconnector.models.disk import Disk from systembridgeconnector.models.display import Display +from systembridgeconnector.models.get_data import GetData from systembridgeconnector.models.gpu import Gpu from systembridgeconnector.models.memory import Memory +from systembridgeconnector.models.register_data_listener import RegisterDataListener from systembridgeconnector.models.system import System from systembridgeconnector.websocket_client import WebSocketClient @@ -93,12 +96,14 @@ class SystemBridgeDataUpdateCoordinator( if not self.websocket_client.connected: await self._setup_websocket() - self.hass.async_create_task(self.websocket_client.get_data(modules)) + self.hass.async_create_task( + self.websocket_client.get_data(GetData(modules=modules)) + ) async def async_handle_module( self, module_name: str, - module, + module: Any, ) -> None: """Handle data from the WebSocket client.""" self.logger.debug("Set new data for: %s", module_name) @@ -174,7 +179,9 @@ class SystemBridgeDataUpdateCoordinator( self.hass.async_create_task(self._listen_for_data()) - await self.websocket_client.register_data_listener(MODULES) + await self.websocket_client.register_data_listener( + RegisterDataListener(modules=MODULES) + ) self.last_update_success = True self.async_update_listeners() diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 4fb2201e2c7..7968b588814 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -3,7 +3,7 @@ "name": "System Bridge", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/system_bridge", - "requirements": ["systembridgeconnector==3.3.2"], + "requirements": ["systembridgeconnector==3.4.4"], "codeowners": ["@timmo001"], "zeroconf": ["_system-bridge._tcp.local."], "after_dependencies": ["zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 10e48ccf2ef..4bb2e474f03 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2294,7 +2294,7 @@ swisshydrodata==0.1.0 synology-srm==0.2.0 # homeassistant.components.system_bridge -systembridgeconnector==3.3.2 +systembridgeconnector==3.4.4 # homeassistant.components.tailscale tailscale==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 796138e09ee..9909033875d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1558,7 +1558,7 @@ sunwatcher==0.2.1 surepy==0.7.2 # homeassistant.components.system_bridge -systembridgeconnector==3.3.2 +systembridgeconnector==3.4.4 # homeassistant.components.tailscale tailscale==0.2.0 diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py index 45131353550..d01ed9a3ff8 100644 --- a/tests/components/system_bridge/test_config_flow.py +++ b/tests/components/system_bridge/test_config_flow.py @@ -2,21 +2,14 @@ import asyncio from unittest.mock import patch -from systembridgeconnector.const import ( - EVENT_DATA, - EVENT_MESSAGE, - EVENT_MODULE, - EVENT_SUBTYPE, - EVENT_TYPE, - SUBTYPE_BAD_API_KEY, - TYPE_DATA_UPDATE, - TYPE_ERROR, -) +from systembridgeconnector.const import MODEL_SYSTEM, TYPE_DATA_UPDATE from systembridgeconnector.exceptions import ( AuthenticationException, ConnectionClosedException, ConnectionErrorException, ) +from systembridgeconnector.models.response import Response +from systembridgeconnector.models.system import LastUpdated, System from homeassistant import config_entries, data_entry_flow from homeassistant.components import zeroconf @@ -48,8 +41,8 @@ FIXTURE_ZEROCONF = zeroconf.ZeroconfServiceInfo( addresses=["1.1.1.1"], port=9170, hostname="test-bridge.local.", - type="_system-bridge._udp.local.", - name="System Bridge - test-bridge._system-bridge._udp.local.", + type="_system-bridge._tcp.local.", + name="System Bridge - test-bridge._system-bridge._tcp.local.", properties={ "address": "http://test-bridge:9170", "fqdn": "test-bridge", @@ -66,34 +59,70 @@ FIXTURE_ZEROCONF_BAD = zeroconf.ZeroconfServiceInfo( addresses=["1.1.1.1"], port=9170, hostname="test-bridge.local.", - type="_system-bridge._udp.local.", - name="System Bridge - test-bridge._system-bridge._udp.local.", + type="_system-bridge._tcp.local.", + name="System Bridge - test-bridge._system-bridge._tcp.local.", properties={ "something": "bad", }, ) -FIXTURE_DATA_SYSTEM = { - EVENT_TYPE: TYPE_DATA_UPDATE, - EVENT_MESSAGE: "Data changed", - EVENT_MODULE: "system", - EVENT_DATA: { - "uuid": FIXTURE_UUID, - }, -} -FIXTURE_DATA_SYSTEM_BAD = { - EVENT_TYPE: TYPE_DATA_UPDATE, - EVENT_MESSAGE: "Data changed", - EVENT_MODULE: "system", - EVENT_DATA: {}, -} +FIXTURE_SYSTEM = System( + id=FIXTURE_UUID, + boot_time=1, + fqdn="", + hostname="1.1.1.1", + ip_address_4="1.1.1.1", + mac_address=FIXTURE_MAC_ADDRESS, + platform="", + platform_version="", + uptime=1, + uuid=FIXTURE_UUID, + version="", + version_latest="", + version_newer_available=False, + last_updated=LastUpdated( + boot_time=1, + fqdn=1, + hostname=1, + ip_address_4=1, + mac_address=1, + platform=1, + platform_version=1, + uptime=1, + uuid=1, + version=1, + version_latest=1, + version_newer_available=1, + ), +) -FIXTURE_DATA_AUTH_ERROR = { - EVENT_TYPE: TYPE_ERROR, - EVENT_SUBTYPE: SUBTYPE_BAD_API_KEY, - EVENT_MESSAGE: "Invalid api-key", -} +FIXTURE_DATA_RESPONSE = Response( + id="1234", + type=TYPE_DATA_UPDATE, + subtype=None, + message="Data received", + module=MODEL_SYSTEM, + data=FIXTURE_SYSTEM, +) + +FIXTURE_DATA_RESPONSE_BAD = Response( + id="1234", + type=TYPE_DATA_UPDATE, + subtype=None, + message="Data received", + module=MODEL_SYSTEM, + data={}, +) + +FIXTURE_DATA_RESPONSE_BAD = Response( + id="1234", + type=TYPE_DATA_UPDATE, + subtype=None, + message="Data received", + module=MODEL_SYSTEM, + data={}, +) async def test_show_user_form(hass: HomeAssistant) -> None: @@ -117,9 +146,11 @@ async def test_user_flow(hass: HomeAssistant) -> None: with patch( "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" - ), patch("systembridgeconnector.websocket_client.WebSocketClient.get_data"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", - return_value=FIXTURE_DATA_SYSTEM, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + return_value=FIXTURE_DATA_RESPONSE, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen" ), patch( "homeassistant.components.system_bridge.async_setup_entry", return_value=True, @@ -167,11 +198,13 @@ async def test_form_connection_closed_cannot_connect(hass: HomeAssistant) -> Non assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + "systembridgeconnector.websocket_client.WebSocketClient.get_data", side_effect=ConnectionClosedException, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -192,11 +225,13 @@ async def test_form_timeout_cannot_connect(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + "systembridgeconnector.websocket_client.WebSocketClient.get_data", side_effect=asyncio.TimeoutError, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -217,11 +252,13 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + "systembridgeconnector.websocket_client.WebSocketClient.get_data", side_effect=AuthenticationException, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -242,11 +279,40 @@ async def test_form_uuid_error(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", - return_value=FIXTURE_DATA_SYSTEM_BAD, + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + side_effect=ValueError, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_USER_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_value_error(hass: HomeAssistant) -> None: + """Test we handle error from bad value.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + return_value=FIXTURE_DATA_RESPONSE_BAD, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -267,11 +333,13 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] is None - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + "systembridgeconnector.websocket_client.WebSocketClient.get_data", side_effect=Exception, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT @@ -292,11 +360,13 @@ async def test_reauth_authorization_error(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + "systembridgeconnector.websocket_client.WebSocketClient.get_data", side_effect=AuthenticationException, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_AUTH_INPUT @@ -340,11 +410,13 @@ async def test_reauth_connection_closed_error(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", + "systembridgeconnector.websocket_client.WebSocketClient.get_data", side_effect=ConnectionClosedException, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_AUTH_INPUT @@ -370,11 +442,13 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "authenticate" - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", - return_value=FIXTURE_DATA_SYSTEM, + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + return_value=FIXTURE_DATA_RESPONSE, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen" ), patch( "homeassistant.components.system_bridge.async_setup_entry", return_value=True, @@ -402,11 +476,13 @@ async def test_zeroconf_flow(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.FlowResultType.FORM assert not result["errors"] - with patch("systembridgeconnector.websocket_client.WebSocketClient.connect"), patch( - "systembridgeconnector.websocket_client.WebSocketClient.get_data" + with patch( + "homeassistant.components.system_bridge.config_flow.WebSocketClient.connect" ), patch( - "systembridgeconnector.websocket_client.WebSocketClient.receive_message", - return_value=FIXTURE_DATA_SYSTEM, + "systembridgeconnector.websocket_client.WebSocketClient.get_data", + return_value=FIXTURE_DATA_RESPONSE, + ), patch( + "systembridgeconnector.websocket_client.WebSocketClient.listen" ), patch( "homeassistant.components.system_bridge.async_setup_entry", return_value=True, From 1557a7c36d6b0f2e46599f3d6d18550eb5b31703 Mon Sep 17 00:00:00 2001 From: hansgoed Date: Mon, 15 Aug 2022 15:07:39 +0200 Subject: [PATCH 3350/3516] =?UTF-8?q?=F0=9F=90=9B=20Fix=20"The=20request?= =?UTF-8?q?=20content=20was=20malformed"=20error=20in=20home=5Fconnect=20(?= =?UTF-8?q?#76411)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/home_connect/__init__.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index 6e664ad07e4..0fa14682f44 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -138,11 +138,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Execute calls to services taking a program.""" program = call.data[ATTR_PROGRAM] device_id = call.data[ATTR_DEVICE_ID] - options = { - ATTR_KEY: call.data.get(ATTR_KEY), - ATTR_VALUE: call.data.get(ATTR_VALUE), - ATTR_UNIT: call.data.get(ATTR_UNIT), - } + + options = [] + + option_key = call.data.get(ATTR_KEY) + if option_key is not None: + option = {ATTR_KEY: option_key, ATTR_VALUE: call.data[ATTR_VALUE]} + + option_unit = call.data.get(ATTR_UNIT) + if option_unit is not None: + option[ATTR_UNIT] = option_unit + + options.append(option) appliance = _get_appliance_by_device_id(hass, device_id) await hass.async_add_executor_job(getattr(appliance, method), program, options) From e39672078d259e4366a409f6258f6a0ef61fb58a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:39:14 +0200 Subject: [PATCH 3351/3516] Use TriggerActionType [core, d-h] (#76804) --- .../components/device_automation/entity.py | 11 ++++------- .../components/device_automation/toggle_entity.py | 13 +++++-------- .../components/device_automation/trigger.py | 15 ++++++--------- homeassistant/components/fan/device_trigger.py | 13 ++++--------- homeassistant/components/geo_location/trigger.py | 13 +++++-------- .../components/humidifier/device_trigger.py | 15 +++++---------- 6 files changed, 29 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/device_automation/entity.py b/homeassistant/components/device_automation/entity.py index 4bc77370150..c7716f38712 100644 --- a/homeassistant/components/device_automation/entity.py +++ b/homeassistant/components/device_automation/entity.py @@ -3,14 +3,11 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import CONF_ENTITY_ID, CONF_FOR, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DEVICE_TRIGGER_BASE_SCHEMA @@ -38,8 +35,8 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" to_state = None @@ -53,7 +50,7 @@ async def async_attach_trigger( state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index af97de85f70..f14c4de5c2f 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import ( ATTR_ENTITY_ID, @@ -23,6 +19,7 @@ from homeassistant.helpers import ( config_validation as cv, entity_registry as er, ) +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DEVICE_TRIGGER_BASE_SCHEMA, entity @@ -155,12 +152,12 @@ def async_condition_from_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" if config[CONF_TYPE] not in [CONF_TURNED_ON, CONF_TURNED_OFF]: - return await entity.async_attach_trigger(hass, config, action, automation_info) + return await entity.async_attach_trigger(hass, config, action, trigger_info) if config[CONF_TYPE] == CONF_TURNED_ON: to_state = "on" @@ -176,7 +173,7 @@ async def async_attach_trigger( state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py index eb39ec383af..bd72b24d844 100644 --- a/homeassistant/components/device_automation/trigger.py +++ b/homeassistant/components/device_automation/trigger.py @@ -5,12 +5,9 @@ from typing import Any, Protocol, cast import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_DOMAIN from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import ( @@ -40,8 +37,8 @@ class DeviceAutomationTriggerProtocol(Protocol): self, hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" @@ -74,11 +71,11 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for trigger.""" platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], DeviceAutomationType.TRIGGER ) - return await platform.async_attach_trigger(hass, config, action, automation_info) + return await platform.async_attach_trigger(hass, config, action, trigger_info) diff --git a/homeassistant/components/fan/device_trigger.py b/homeassistant/components/fan/device_trigger.py index 35eb5a9f50c..cc10e9cbeca 100644 --- a/homeassistant/components/fan/device_trigger.py +++ b/homeassistant/components/fan/device_trigger.py @@ -3,13 +3,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -37,10 +34,8 @@ async def async_get_trigger_capabilities( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) + return await toggle_entity.async_attach_trigger(hass, config, action, trigger_info) diff --git a/homeassistant/components/geo_location/trigger.py b/homeassistant/components/geo_location/trigger.py index 9f2b56a31c6..bc04490e76c 100644 --- a/homeassistant/components/geo_location/trigger.py +++ b/homeassistant/components/geo_location/trigger.py @@ -3,15 +3,12 @@ import logging import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.config_validation import entity_domain from homeassistant.helpers.event import TrackStates, async_track_state_change_filtered +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -44,11 +41,11 @@ def source_match(state, source): async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] source: str = config[CONF_SOURCE].lower() zone_entity_id = config.get(CONF_ZONE) trigger_event = config.get(CONF_EVENT) @@ -66,7 +63,7 @@ async def async_attach_trigger( if (zone_state := hass.states.get(zone_entity_id)) is None: _LOGGER.warning( "Unable to execute automation %s: Zone %s not found", - automation_info["name"], + trigger_info["name"], zone_entity_id, ) return diff --git a/homeassistant/components/humidifier/device_trigger.py b/homeassistant/components/humidifier/device_trigger.py index e8f3a0ac446..b9abb231dfd 100644 --- a/homeassistant/components/humidifier/device_trigger.py +++ b/homeassistant/components/humidifier/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import ( DEVICE_TRIGGER_BASE_SCHEMA, toggle_entity, @@ -27,6 +23,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -82,8 +79,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if config[CONF_TYPE] == "target_humidity_changed": @@ -106,12 +103,10 @@ async def async_attach_trigger( ) ) return await numeric_state_trigger.async_attach_trigger( - hass, numeric_state_config, action, automation_info, platform_type="device" + hass, numeric_state_config, action, trigger_info, platform_type="device" ) - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) + return await toggle_entity.async_attach_trigger(hass, config, action, trigger_info) async def async_get_trigger_capabilities( From af002d9dc4536d641df41911f18e13cb3e273524 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:39:55 +0200 Subject: [PATCH 3352/3516] Use TriggerActionType [core, l-m] (#76806) --- .../components/light/device_trigger.py | 13 ++++-------- .../components/lock/device_trigger.py | 11 ++++------ .../components/media_player/device_trigger.py | 13 +++++------- .../components/mqtt/device_trigger.py | 21 ++++++++----------- homeassistant/components/mqtt/trigger.py | 15 ++++++------- 5 files changed, 28 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py index 6f5f5fd7f52..5ae5b12bf61 100644 --- a/homeassistant/components/light/device_trigger.py +++ b/homeassistant/components/light/device_trigger.py @@ -3,13 +3,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -23,13 +20,11 @@ TRIGGER_SCHEMA = vol.All( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) + return await toggle_entity.async_attach_trigger(hass, config, action, trigger_info) async def async_get_triggers( diff --git a/homeassistant/components/lock/device_trigger.py b/homeassistant/components/lock/device_trigger.py index b55a2ac254b..9fc35fb1352 100644 --- a/homeassistant/components/lock/device_trigger.py +++ b/homeassistant/components/lock/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import ( @@ -24,6 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -80,8 +77,8 @@ async def async_get_trigger_capabilities( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if config[CONF_TYPE] == "jammed": @@ -104,5 +101,5 @@ async def async_attach_trigger( state_config[CONF_FOR] = config[CONF_FOR] state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/media_player/device_trigger.py b/homeassistant/components/media_player/device_trigger.py index e0c88489841..9b61c89dafb 100644 --- a/homeassistant/components/media_player/device_trigger.py +++ b/homeassistant/components/media_player/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import ( DEVICE_TRIGGER_BASE_SCHEMA, entity, @@ -28,6 +24,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -94,12 +91,12 @@ async def async_get_trigger_capabilities( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if config[CONF_TYPE] not in TRIGGER_TYPES: - return await entity.async_attach_trigger(hass, config, action, automation_info) + return await entity.async_attach_trigger(hass, config, action, trigger_info) if config[CONF_TYPE] == "buffering": to_state = STATE_BUFFERING elif config[CONF_TYPE] == "idle": @@ -122,5 +119,5 @@ async def async_attach_trigger( state_config[CONF_FOR] = config[CONF_FOR] state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 0b4bcbfcbc2..30d6fdea05f 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -8,10 +8,6 @@ from typing import cast import attr import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -26,6 +22,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import debug_info, trigger as mqtt_trigger @@ -93,8 +90,8 @@ LOG_NAME = "Device trigger" class TriggerInstance: """Attached trigger settings.""" - action: AutomationActionType = attr.ib() - automation_info: AutomationTriggerInfo = attr.ib() + action: TriggerActionType = attr.ib() + trigger_info: TriggerInfo = attr.ib() trigger: Trigger = attr.ib() remove: CALLBACK_TYPE | None = attr.ib(default=None) @@ -118,7 +115,7 @@ class TriggerInstance: self.trigger.hass, mqtt_config, self.action, - self.automation_info, + self.trigger_info, ) @@ -138,10 +135,10 @@ class Trigger: trigger_instances: list[TriggerInstance] = attr.ib(factory=list) async def add_trigger( - self, action: AutomationActionType, automation_info: AutomationTriggerInfo + self, action: TriggerActionType, trigger_info: TriggerInfo ) -> Callable: """Add MQTT trigger.""" - instance = TriggerInstance(action, automation_info, self) + instance = TriggerInstance(action, trigger_info, self) self.trigger_instances.append(instance) if self.topic is not None: @@ -323,8 +320,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" hass.data.setdefault(DEVICE_TRIGGERS, {}) @@ -344,5 +341,5 @@ async def async_attach_trigger( value_template=None, ) return await hass.data[DEVICE_TRIGGERS][discovery_id].add_trigger( - action, automation_info + action, trigger_info ) diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index aca4cf0a480..3530538122d 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -4,14 +4,11 @@ import logging import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_PAYLOAD, CONF_PLATFORM, CONF_VALUE_TEMPLATE from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.json import json_loads +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .. import mqtt @@ -39,11 +36,11 @@ _LOGGER = logging.getLogger(__name__) async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] topic = config[CONF_TOPIC] wanted_payload = config.get(CONF_PAYLOAD) value_template = config.get(CONF_VALUE_TEMPLATE) @@ -51,8 +48,8 @@ async def async_attach_trigger( qos = config[CONF_QOS] job = HassJob(action) variables = None - if automation_info: - variables = automation_info.get("variables") + if trigger_info: + variables = trigger_info.get("variables") template.attach(hass, wanted_payload) if wanted_payload: From 1ebac6c7cacc41c9d706da993a0c504810e12522 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:40:16 +0200 Subject: [PATCH 3353/3516] Use TriggerActionType [core, r-t] (#76807) --- homeassistant/components/remote/device_trigger.py | 13 ++++--------- homeassistant/components/select/device_trigger.py | 11 ++++------- homeassistant/components/sensor/device_trigger.py | 11 ++++------- homeassistant/components/sun/trigger.py | 11 ++++------- homeassistant/components/switch/device_trigger.py | 13 ++++--------- homeassistant/components/tag/trigger.py | 11 ++++------- 6 files changed, 24 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/remote/device_trigger.py b/homeassistant/components/remote/device_trigger.py index 127f07827e2..f2d5c54e3c3 100644 --- a/homeassistant/components/remote/device_trigger.py +++ b/homeassistant/components/remote/device_trigger.py @@ -3,13 +3,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -23,13 +20,11 @@ TRIGGER_SCHEMA = vol.All( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) + return await toggle_entity.async_attach_trigger(hass, config, action, trigger_info) async def async_get_triggers( diff --git a/homeassistant/components/select/device_trigger.py b/homeassistant/components/select/device_trigger.py index 574acfc6893..897ed855a5e 100644 --- a/homeassistant/components/select/device_trigger.py +++ b/homeassistant/components/select/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers.state import ( CONF_FOR, @@ -26,6 +22,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.entity import get_capability +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import ATTR_OPTIONS, DOMAIN @@ -64,8 +61,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" state_config = { @@ -84,7 +81,7 @@ async def async_attach_trigger( state_config = await async_validate_state_trigger_config(hass, state_config) return await async_attach_state_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 741c0281e0d..5c815d3c546 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -1,10 +1,6 @@ """Provides device triggers for sensors.""" import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -27,6 +23,7 @@ from homeassistant.helpers.entity import ( get_device_class, get_unit_of_measurement, ) +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import ATTR_STATE_CLASS, DOMAIN, SensorDeviceClass @@ -143,8 +140,8 @@ TRIGGER_SCHEMA = vol.All( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" numeric_state_config = { @@ -162,7 +159,7 @@ async def async_attach_trigger( hass, numeric_state_config ) return await numeric_state_trigger.async_attach_trigger( - hass, numeric_state_config, action, automation_info, platform_type="device" + hass, numeric_state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/sun/trigger.py b/homeassistant/components/sun/trigger.py index 75f5b36f8f5..86af51f0283 100644 --- a/homeassistant/components/sun/trigger.py +++ b/homeassistant/components/sun/trigger.py @@ -3,10 +3,6 @@ from datetime import timedelta import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import ( CONF_EVENT, CONF_OFFSET, @@ -16,6 +12,7 @@ from homeassistant.const import ( from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_sunrise, async_track_sunset +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType # mypy: allow-untyped-defs, no-check-untyped-defs @@ -32,11 +29,11 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for events based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] event = config.get(CONF_EVENT) offset = config.get(CONF_OFFSET) description = event diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py index 9f56d7a09d2..499b04bbaf3 100644 --- a/homeassistant/components/switch/device_trigger.py +++ b/homeassistant/components/switch/device_trigger.py @@ -3,13 +3,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -23,13 +20,11 @@ TRIGGER_SCHEMA = vol.All( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) + return await toggle_entity.async_attach_trigger(hass, config, action, trigger_info) async def async_get_triggers( diff --git a/homeassistant/components/tag/trigger.py b/homeassistant/components/tag/trigger.py index b844ee260a2..146521dfba9 100644 --- a/homeassistant/components/tag/trigger.py +++ b/homeassistant/components/tag/trigger.py @@ -1,13 +1,10 @@ """Support for tag triggers.""" import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DEVICE_ID, DOMAIN, EVENT_TAG_SCANNED, TAG_ID @@ -24,11 +21,11 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for tag_scanned events based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] tag_ids = set(config[TAG_ID]) device_ids = set(config[DEVICE_ID]) if DEVICE_ID in config else None From 19cf6089d605ac1bdc015625e95387a23d355f25 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 17:44:12 +0200 Subject: [PATCH 3354/3516] Use TriggerActionType [core, a-d] (#76803) --- .../alarm_control_panel/device_trigger.py | 11 ++++------- homeassistant/components/button/device_trigger.py | 11 ++++------- homeassistant/components/calendar/trigger.py | 11 ++++------- homeassistant/components/climate/device_trigger.py | 13 +++++-------- homeassistant/components/cover/device_trigger.py | 13 +++++-------- .../components/device_tracker/device_trigger.py | 11 ++++------- 6 files changed, 26 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py index 840b5eba6f3..303243d66cb 100644 --- a/homeassistant/components/alarm_control_panel/device_trigger.py +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -5,10 +5,6 @@ from typing import Final import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import ( @@ -29,6 +25,7 @@ from homeassistant.const import ( from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.entity import get_supported_features +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -131,8 +128,8 @@ async def async_get_trigger_capabilities( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if config[CONF_TYPE] == "triggered": @@ -159,5 +156,5 @@ async def async_attach_trigger( state_config[CONF_FOR] = config[CONF_FOR] state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/button/device_trigger.py b/homeassistant/components/button/device_trigger.py index 1418039b2e8..673806be7d2 100644 --- a/homeassistant/components/button/device_trigger.py +++ b/homeassistant/components/button/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers.state import ( async_attach_trigger as async_attach_state_trigger, @@ -21,6 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -56,8 +53,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" state_config = { @@ -67,5 +64,5 @@ async def async_attach_trigger( state_config = await async_validate_state_trigger_config(hass, state_config) return await async_attach_state_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/calendar/trigger.py b/homeassistant/components/calendar/trigger.py index 7845037f896..74be0f7e71d 100644 --- a/homeassistant/components/calendar/trigger.py +++ b/homeassistant/components/calendar/trigger.py @@ -8,10 +8,6 @@ from typing import Any import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_ENTITY_ID, CONF_EVENT, CONF_OFFSET, CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError @@ -21,6 +17,7 @@ from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_time_interval, ) +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util @@ -167,8 +164,8 @@ class CalendarEventListener: async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach trigger for the specified calendar.""" entity_id = config[CONF_ENTITY_ID] @@ -184,7 +181,7 @@ async def async_attach_trigger( ) trigger_data = { - **automation_info["trigger_data"], + **trigger_info["trigger_data"], "platform": DOMAIN, "event": event_type, "offset": offset, diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index d8d46342603..a7b723eb41a 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import ( numeric_state as numeric_state_trigger, @@ -25,6 +21,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN, const @@ -112,8 +109,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if (trigger_type := config[CONF_TYPE]) == "hvac_mode_changed": @@ -133,7 +130,7 @@ async def async_attach_trigger( hass, state_config ) return await state_trigger.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) numeric_state_config = { @@ -161,7 +158,7 @@ async def async_attach_trigger( hass, numeric_state_config ) return await numeric_state_trigger.async_attach_trigger( - hass, numeric_state_config, action, automation_info, platform_type="device" + hass, numeric_state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/cover/device_trigger.py b/homeassistant/components/cover/device_trigger.py index a6ed7785486..b0be418f312 100644 --- a/homeassistant/components/cover/device_trigger.py +++ b/homeassistant/components/cover/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import ( numeric_state as numeric_state_trigger, @@ -30,6 +26,7 @@ from homeassistant.const import ( from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.entity import get_supported_features +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import ( @@ -147,8 +144,8 @@ async def async_get_trigger_capabilities( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if config[CONF_TYPE] in STATE_TRIGGER_TYPES: @@ -172,7 +169,7 @@ async def async_attach_trigger( hass, state_config ) return await state_trigger.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) if config[CONF_TYPE] == "position": @@ -194,5 +191,5 @@ async def async_attach_trigger( hass, numeric_state_config ) return await numeric_state_trigger.async_attach_trigger( - hass, numeric_state_config, action, automation_info, platform_type="device" + hass, numeric_state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/device_tracker/device_trigger.py b/homeassistant/components/device_tracker/device_trigger.py index 1d9dc4548b3..231fab65d35 100644 --- a/homeassistant/components/device_tracker/device_trigger.py +++ b/homeassistant/components/device_tracker/device_trigger.py @@ -5,10 +5,6 @@ from typing import Final import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.zone import DOMAIN as DOMAIN_ZONE, trigger as zone from homeassistant.const import ( @@ -22,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -74,8 +71,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if config[CONF_TYPE] == "enters": @@ -91,7 +88,7 @@ async def async_attach_trigger( } zone_config = await zone.async_validate_trigger_config(hass, zone_config) return await zone.async_attach_trigger( - hass, zone_config, action, automation_info, platform_type="device" + hass, zone_config, action, trigger_info, platform_type="device" ) From 7360cce1c2d1859a45ee350eee83f78dda71308d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 18:06:16 +0200 Subject: [PATCH 3355/3516] Use TriggerActionType [core, homeassistant] (#76805) --- .../components/homeassistant/trigger.py | 11 ++++------- .../homeassistant/triggers/event.py | 13 +++++-------- .../homeassistant/triggers/homeassistant.py | 13 +++++-------- .../homeassistant/triggers/numeric_state.py | 19 ++++++++----------- .../homeassistant/triggers/state.py | 19 ++++++++----------- .../components/homeassistant/triggers/time.py | 11 ++++------- .../homeassistant/triggers/time_pattern.py | 11 ++++------- 7 files changed, 38 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/homeassistant/trigger.py b/homeassistant/components/homeassistant/trigger.py index 42b0e30af1d..588b6713007 100644 --- a/homeassistant/components/homeassistant/trigger.py +++ b/homeassistant/components/homeassistant/trigger.py @@ -1,15 +1,12 @@ """Home Assistant trigger dispatcher.""" import importlib -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation.trigger import ( DeviceAutomationTriggerProtocol, ) from homeassistant.const import CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType @@ -31,9 +28,9 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach trigger of specified platform.""" platform = _get_trigger_platform(config) - return await platform.async_attach_trigger(hass, config, action, automation_info) + return await platform.async_attach_trigger(hass, config, action, trigger_info) diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index b0d817478dc..0796d49d770 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -5,13 +5,10 @@ from typing import Any import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_EVENT_DATA, CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType CONF_EVENT_TYPE = "event_type" @@ -37,14 +34,14 @@ def _schema_value(value: Any) -> Any: async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, *, platform_type: str = "event", ) -> CALLBACK_TYPE: """Listen for events based on configuration.""" - trigger_data = automation_info["trigger_data"] - variables = automation_info["variables"] + trigger_data = trigger_info["trigger_data"] + variables = trigger_info["variables"] template.attach(hass, config[CONF_EVENT_TYPE]) event_types = template.render_complex( diff --git a/homeassistant/components/homeassistant/triggers/homeassistant.py b/homeassistant/components/homeassistant/triggers/homeassistant.py index c9a5a780e88..8749c47861a 100644 --- a/homeassistant/components/homeassistant/triggers/homeassistant.py +++ b/homeassistant/components/homeassistant/triggers/homeassistant.py @@ -1,13 +1,10 @@ """Offer Home Assistant core automation rules.""" import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_EVENT, CONF_PLATFORM, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType # mypy: allow-untyped-defs @@ -26,11 +23,11 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for events based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] event = config.get(CONF_EVENT) job = HassJob(action) @@ -56,7 +53,7 @@ async def async_attach_trigger( # Automation are enabled while hass is starting up, fire right away # Check state because a config reload shouldn't trigger it. - if automation_info["home_assistant_start"]: + if trigger_info["home_assistant_start"]: hass.async_run_hass_job( job, { diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 934cc99993a..10971a03781 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -4,10 +4,6 @@ import logging import voluptuous as vol from homeassistant import exceptions -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import ( CONF_ABOVE, CONF_ATTRIBUTE, @@ -28,6 +24,7 @@ from homeassistant.helpers.event import ( async_track_same_state, async_track_state_change_event, ) +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs @@ -87,8 +84,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, *, platform_type: str = "numeric_state", ) -> CALLBACK_TYPE: @@ -105,8 +102,8 @@ async def async_attach_trigger( attribute = config.get(CONF_ATTRIBUTE) job = HassJob(action) - trigger_data = automation_info["trigger_data"] - _variables = automation_info["variables"] or {} + trigger_data = trigger_info["trigger_data"] + _variables = trigger_info["variables"] or {} if value_template is not None: value_template.hass = hass @@ -139,7 +136,7 @@ async def async_attach_trigger( except exceptions.ConditionError as ex: _LOGGER.warning( "Error initializing '%s' trigger: %s", - automation_info["name"], + trigger_info["name"], ex, ) @@ -185,7 +182,7 @@ async def async_attach_trigger( try: matching = check_numeric_state(entity_id, from_s, to_s) except exceptions.ConditionError as ex: - _LOGGER.warning("Error in '%s' trigger: %s", automation_info["name"], ex) + _LOGGER.warning("Error in '%s' trigger: %s", trigger_info["name"], ex) return if not matching: @@ -201,7 +198,7 @@ async def async_attach_trigger( except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( "Error rendering '%s' for template: %s", - automation_info["name"], + trigger_info["name"], ex, ) return diff --git a/homeassistant/components/homeassistant/triggers/state.py b/homeassistant/components/homeassistant/triggers/state.py index 4f1e823c90f..8514000de07 100644 --- a/homeassistant/components/homeassistant/triggers/state.py +++ b/homeassistant/components/homeassistant/triggers/state.py @@ -7,10 +7,6 @@ import logging import voluptuous as vol from homeassistant import exceptions -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_ATTRIBUTE, CONF_FOR, CONF_PLATFORM, MATCH_ALL from homeassistant.core import ( CALLBACK_TYPE, @@ -30,6 +26,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, process_state_match, ) +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs @@ -97,8 +94,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, *, platform_type: str = "state", ) -> CALLBACK_TYPE: @@ -131,8 +128,8 @@ async def async_attach_trigger( attribute = config.get(CONF_ATTRIBUTE) job = HassJob(action) - trigger_data = automation_info["trigger_data"] - _variables = automation_info["variables"] or {} + trigger_data = trigger_info["trigger_data"] + _variables = trigger_info["variables"] or {} @callback def state_automation_listener(event: Event): @@ -193,7 +190,7 @@ async def async_attach_trigger( call_action() return - trigger_info = { + data = { "trigger": { "platform": "state", "entity_id": entity, @@ -201,7 +198,7 @@ async def async_attach_trigger( "to_state": to_s, } } - variables = {**_variables, **trigger_info} + variables = {**_variables, **data} try: period[entity] = cv.positive_time_period( @@ -209,7 +206,7 @@ async def async_attach_trigger( ) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( - "Error rendering '%s' for template: %s", automation_info["name"], ex + "Error rendering '%s' for template: %s", trigger_info["name"], ex ) return diff --git a/homeassistant/components/homeassistant/triggers/time.py b/homeassistant/components/homeassistant/triggers/time.py index 619ef0e207c..a81afa1323a 100644 --- a/homeassistant/components/homeassistant/triggers/time.py +++ b/homeassistant/components/homeassistant/triggers/time.py @@ -5,10 +5,6 @@ from functools import partial import voluptuous as vol from homeassistant.components import sensor -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import ( ATTR_DEVICE_CLASS, CONF_AT, @@ -23,6 +19,7 @@ from homeassistant.helpers.event import ( async_track_state_change_event, async_track_time_change, ) +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util @@ -45,11 +42,11 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] entities: dict[str, CALLBACK_TYPE] = {} removes = [] job = HassJob(action) diff --git a/homeassistant/components/homeassistant/triggers/time_pattern.py b/homeassistant/components/homeassistant/triggers/time_pattern.py index 7ee1d218171..3c2cf58bca8 100644 --- a/homeassistant/components/homeassistant/triggers/time_pattern.py +++ b/homeassistant/components/homeassistant/triggers/time_pattern.py @@ -1,14 +1,11 @@ """Offer time listening automation rules.""" import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_time_change +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType # mypy: allow-untyped-defs, no-check-untyped-defs @@ -63,11 +60,11 @@ TRIGGER_SCHEMA = vol.All( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] hours = config.get(CONF_HOURS) minutes = config.get(CONF_MINUTES) seconds = config.get(CONF_SECONDS) From 453cbc3e14e7d546d937d0848d5a62308a226494 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 18:15:20 +0200 Subject: [PATCH 3356/3516] Use TriggerActionType [core, t-z] (#76808) --- homeassistant/components/template/trigger.py | 21 ++++++++----------- .../components/update/device_trigger.py | 13 ++++-------- .../components/vacuum/device_trigger.py | 11 ++++------ homeassistant/components/webhook/trigger.py | 19 +++++++---------- homeassistant/components/zone/trigger.py | 13 +++++------- 5 files changed, 30 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index 33ac90079b7..7c25e7090a6 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -4,10 +4,6 @@ import logging import voluptuous as vol from homeassistant import exceptions -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_FOR, CONF_PLATFORM, CONF_VALUE_TEMPLATE from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, template @@ -17,6 +13,7 @@ from homeassistant.helpers.event import ( async_track_template_result, ) from homeassistant.helpers.template import Template, result_as_boolean +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType # mypy: allow-untyped-defs, no-check-untyped-defs @@ -35,13 +32,13 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, *, platform_type: str = "template", ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] value_template: Template = config[CONF_VALUE_TEMPLATE] value_template.hass = hass time_delta = config.get(CONF_FOR) @@ -53,13 +50,13 @@ async def async_attach_trigger( # Arm at setup if the template is already false. try: if not result_as_boolean( - value_template.async_render(automation_info["variables"]) + value_template.async_render(trigger_info["variables"]) ): armed = True except exceptions.TemplateError as ex: _LOGGER.warning( "Error initializing 'template' trigger for '%s': %s", - automation_info["name"], + trigger_info["name"], ex, ) @@ -72,7 +69,7 @@ async def async_attach_trigger( if isinstance(result, exceptions.TemplateError): _LOGGER.warning( "Error evaluating 'template' trigger for '%s': %s", - automation_info["name"], + trigger_info["name"], result, ) return @@ -134,7 +131,7 @@ async def async_attach_trigger( ) except (exceptions.TemplateError, vol.Invalid) as ex: _LOGGER.error( - "Error rendering '%s' for template: %s", automation_info["name"], ex + "Error rendering '%s' for template: %s", trigger_info["name"], ex ) return @@ -144,7 +141,7 @@ async def async_attach_trigger( info = async_track_template_result( hass, - [TrackTemplate(value_template, automation_info["variables"])], + [TrackTemplate(value_template, trigger_info["variables"])], template_listener, ) unsub = info.async_remove diff --git a/homeassistant/components/update/device_trigger.py b/homeassistant/components/update/device_trigger.py index ac8113d5708..bd0e1a6e1b7 100644 --- a/homeassistant/components/update/device_trigger.py +++ b/homeassistant/components/update/device_trigger.py @@ -3,13 +3,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -23,13 +20,11 @@ TRIGGER_SCHEMA = vol.All( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - return await toggle_entity.async_attach_trigger( - hass, config, action, automation_info - ) + return await toggle_entity.async_attach_trigger(hass, config, action, trigger_info) async def async_get_triggers( diff --git a/homeassistant/components/vacuum/device_trigger.py b/homeassistant/components/vacuum/device_trigger.py index 4b8ec2fc08d..c90aa1756e4 100644 --- a/homeassistant/components/vacuum/device_trigger.py +++ b/homeassistant/components/vacuum/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import state as state_trigger from homeassistant.const import ( @@ -19,6 +15,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN, STATE_CLEANING, STATE_DOCKED @@ -74,8 +71,8 @@ async def async_get_trigger_capabilities( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if config[CONF_TYPE] == "cleaning": @@ -92,5 +89,5 @@ async def async_attach_trigger( state_config[CONF_FOR] = config[CONF_FOR] state_config = await state_trigger.async_validate_trigger_config(hass, state_config) return await state_trigger.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/webhook/trigger.py b/homeassistant/components/webhook/trigger.py index 498a7363a61..262d77e61f7 100644 --- a/homeassistant/components/webhook/trigger.py +++ b/homeassistant/components/webhook/trigger.py @@ -6,13 +6,10 @@ from dataclasses import dataclass from aiohttp import hdrs import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_PLATFORM, CONF_WEBHOOK_ID from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN, async_register, async_unregister @@ -35,7 +32,7 @@ WEBHOOK_TRIGGERS = f"{DOMAIN}_triggers" class TriggerInstance: """Attached trigger settings.""" - automation_info: AutomationTriggerInfo + trigger_info: TriggerInfo job: HassJob @@ -55,15 +52,15 @@ async def _handle_webhook(hass, webhook_id, request): WEBHOOK_TRIGGERS, {} ) for trigger in triggers[webhook_id]: - result = {**base_result, **trigger.automation_info["trigger_data"]} + result = {**base_result, **trigger.trigger_info["trigger_data"]} hass.async_run_hass_job(trigger.job, {"trigger": result}) async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Trigger based on incoming webhooks.""" webhook_id: str = config[CONF_WEBHOOK_ID] @@ -76,14 +73,14 @@ async def async_attach_trigger( if webhook_id not in triggers: async_register( hass, - automation_info["domain"], - automation_info["name"], + trigger_info["domain"], + trigger_info["name"], webhook_id, _handle_webhook, ) triggers[webhook_id] = [] - trigger_instance = TriggerInstance(automation_info, job) + trigger_instance = TriggerInstance(trigger_info, job) triggers[webhook_id].append(trigger_instance) @callback diff --git a/homeassistant/components/zone/trigger.py b/homeassistant/components/zone/trigger.py index 0865182df80..4958ec102d1 100644 --- a/homeassistant/components/zone/trigger.py +++ b/homeassistant/components/zone/trigger.py @@ -3,10 +3,6 @@ import logging import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import ( ATTR_FRIENDLY_NAME, CONF_ENTITY_ID, @@ -22,6 +18,7 @@ from homeassistant.helpers import ( location, ) from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType # mypy: allow-incomplete-defs, allow-untyped-defs @@ -62,13 +59,13 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, *, platform_type: str = "zone", ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] entity_id: list[str] = config[CONF_ENTITY_ID] zone_entity_id = config.get(CONF_ZONE) event = config.get(CONF_EVENT) @@ -91,7 +88,7 @@ async def async_attach_trigger( if not (zone_state := hass.states.get(zone_entity_id)): _LOGGER.warning( "Automation '%s' is referencing non-existing zone '%s' in a zone trigger", - automation_info["name"], + trigger_info["name"], zone_entity_id, ) return From 223ea034926d63560c51dc7ca403d6aa65f57abb Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 15 Aug 2022 19:32:44 +0200 Subject: [PATCH 3357/3516] Fix Hue events for relative_rotary devices (such as Hue Tap Dial) (#76758) Co-authored-by: Paulus Schoutsen --- homeassistant/components/hue/logbook.py | 3 + homeassistant/components/hue/strings.json | 21 +++-- .../components/hue/translations/en.json | 19 ++-- .../components/hue/v2/device_trigger.py | 88 +++++++++---------- homeassistant/components/hue/v2/hue_event.py | 33 ++++++- 5 files changed, 98 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/hue/logbook.py b/homeassistant/components/hue/logbook.py index e98a99f1861..412ca044b58 100644 --- a/homeassistant/components/hue/logbook.py +++ b/homeassistant/components/hue/logbook.py @@ -28,6 +28,8 @@ TRIGGER_SUBTYPE = { "2": "second button", "3": "third button", "4": "fourth button", + "clock_wise": "Rotation clockwise", + "counter_clock_wise": "Rotation counter-clockwise", } TRIGGER_TYPE = { "remote_button_long_release": "{subtype} released after long press", @@ -40,6 +42,7 @@ TRIGGER_TYPE = { "short_release": "{subtype} released after short press", "long_release": "{subtype} released after long press", "double_short_release": "both {subtype} released", + "start": '"{subtype}" pressed initially', } UNKNOWN_TYPE = "unknown type" diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index 0d7c67ec84b..a44eea0fe33 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -49,20 +49,23 @@ "1": "First button", "2": "Second button", "3": "Third button", - "4": "Fourth button" + "4": "Fourth button", + "clock_wise": "Rotation clockwise", + "counter_clock_wise": "Rotation counter-clockwise" }, "trigger_type": { - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_long_release": "\"{subtype}\" released after long press", + "remote_button_short_press": "\"{subtype}\" pressed", + "remote_button_short_release": "\"{subtype}\" released", "remote_double_button_long_press": "Both \"{subtype}\" released after long press", "remote_double_button_short_press": "Both \"{subtype}\" released", - "initial_press": "Button \"{subtype}\" pressed initially", - "repeat": "Button \"{subtype}\" held down", - "short_release": "Button \"{subtype}\" released after short press", - "long_release": "Button \"{subtype}\" released after long press", - "double_short_release": "Both \"{subtype}\" released" + "initial_press": "\"{subtype}\" pressed initially", + "repeat": "\"{subtype}\" held down", + "short_release": "\"{subtype}\" released after short press", + "long_release": "\"{subtype}\" released after long press", + "double_short_release": "Both \"{subtype}\" released", + "start": "\"{subtype}\" pressed initially" } }, "options": { diff --git a/homeassistant/components/hue/translations/en.json b/homeassistant/components/hue/translations/en.json index f617be431b9..7a54fc5ce03 100644 --- a/homeassistant/components/hue/translations/en.json +++ b/homeassistant/components/hue/translations/en.json @@ -49,19 +49,22 @@ "double_buttons_1_3": "First and Third buttons", "double_buttons_2_4": "Second and Fourth buttons", "turn_off": "Turn off", - "turn_on": "Turn on" + "turn_on": "Turn on", + "clock_wise": "Rotation clockwise", + "counter_clock_wise": "Rotation counter-clockwise" }, "trigger_type": { "double_short_release": "Both \"{subtype}\" released", - "initial_press": "Button \"{subtype}\" pressed initially", - "long_release": "Button \"{subtype}\" released after long press", - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", + "initial_press": "\"{subtype}\" pressed initially", + "long_release": "\"{subtype}\" released after long press", + "remote_button_long_release": "\"{subtype}\" released after long press", + "remote_button_short_press": "\"{subtype}\" pressed", + "remote_button_short_release": "\"{subtype}\" released", "remote_double_button_long_press": "Both \"{subtype}\" released after long press", "remote_double_button_short_press": "Both \"{subtype}\" released", - "repeat": "Button \"{subtype}\" held down", - "short_release": "Button \"{subtype}\" released after short press" + "repeat": "\"{subtype}\" held down", + "short_release": "\"{subtype}\" released after short press", + "start": "\"{subtype}\" pressed initially" } }, "options": { diff --git a/homeassistant/components/hue/v2/device_trigger.py b/homeassistant/components/hue/v2/device_trigger.py index 2b868a4685c..0fa9e37569d 100644 --- a/homeassistant/components/hue/v2/device_trigger.py +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -4,10 +4,13 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any from aiohue.v2.models.button import ButtonEvent +from aiohue.v2.models.relative_rotary import ( + RelativeRotaryAction, + RelativeRotaryDirection, +) from aiohue.v2.models.resource import ResourceTypes import voluptuous as vol -from homeassistant.components import persistent_notification from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import ( @@ -49,48 +52,25 @@ DEFAULT_BUTTON_EVENT_TYPES = ( ButtonEvent.LONG_RELEASE, ) +DEFAULT_ROTARY_EVENT_TYPES = (RelativeRotaryAction.START, RelativeRotaryAction.REPEAT) +DEFAULT_ROTARY_EVENT_SUBTYPES = ( + RelativeRotaryDirection.CLOCK_WISE, + RelativeRotaryDirection.COUNTER_CLOCK_WISE, +) + DEVICE_SPECIFIC_EVENT_TYPES = { # device specific overrides of specific supported button events "Hue tap switch": (ButtonEvent.INITIAL_PRESS,), } -def check_invalid_device_trigger( - bridge: HueBridge, - config: ConfigType, - device_entry: DeviceEntry, - automation_info: AutomationTriggerInfo | None = None, -): - """Check automation config for deprecated format.""" - # NOTE: Remove this check after 2022.6 - if isinstance(config["subtype"], int): - return - # found deprecated V1 style trigger, notify the user that it should be adjusted - msg = ( - f"Incompatible device trigger detected for " - f"[{device_entry.name}](/config/devices/device/{device_entry.id}) " - "Please manually fix the outdated automation(s) once to fix this issue." - ) - if automation_info: - automation_id = automation_info["variables"]["this"]["attributes"]["id"] # type: ignore[index] - msg += f"\n\n[Check it out](/config/automation/edit/{automation_id})." - persistent_notification.async_create( - bridge.hass, - msg, - title="Outdated device trigger found", - notification_id=f"hue_trigger_{device_entry.id}", - ) - - async def async_validate_trigger_config( bridge: "HueBridge", device_entry: DeviceEntry, config: ConfigType, ) -> ConfigType: """Validate config.""" - config = TRIGGER_SCHEMA(config) - check_invalid_device_trigger(bridge, config, device_entry) - return config + return TRIGGER_SCHEMA(config) async def async_attach_trigger( @@ -113,7 +93,6 @@ async def async_attach_trigger( }, } ) - check_invalid_device_trigger(bridge, config, device_entry, automation_info) return await event_trigger.async_attach_trigger( hass, event_config, action, automation_info, platform_type="device" ) @@ -131,22 +110,37 @@ def async_get_triggers( # extract triggers from all button resources of this Hue device triggers = [] model_id = api.devices[hue_dev_id].product_data.product_name + for resource in api.devices.get_sensors(hue_dev_id): - if resource.type != ResourceTypes.BUTTON: - continue - for event_type in DEVICE_SPECIFIC_EVENT_TYPES.get( - model_id, DEFAULT_BUTTON_EVENT_TYPES - ): - triggers.append( - { - CONF_DEVICE_ID: device_entry.id, - CONF_DOMAIN: DOMAIN, - CONF_PLATFORM: "device", - CONF_TYPE: event_type.value, - CONF_SUBTYPE: resource.metadata.control_id, - CONF_UNIQUE_ID: resource.id, - } - ) + # button triggers + if resource.type == ResourceTypes.BUTTON: + for event_type in DEVICE_SPECIFIC_EVENT_TYPES.get( + model_id, DEFAULT_BUTTON_EVENT_TYPES + ): + triggers.append( + { + CONF_DEVICE_ID: device_entry.id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_TYPE: event_type.value, + CONF_SUBTYPE: resource.metadata.control_id, + CONF_UNIQUE_ID: resource.id, + } + ) + # relative_rotary triggers + elif resource.type == ResourceTypes.RELATIVE_ROTARY: + for event_type in DEFAULT_ROTARY_EVENT_TYPES: + for sub_type in DEFAULT_ROTARY_EVENT_SUBTYPES: + triggers.append( + { + CONF_DEVICE_ID: device_entry.id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_TYPE: event_type.value, + CONF_SUBTYPE: sub_type.value, + CONF_UNIQUE_ID: resource.id, + } + ) return triggers diff --git a/homeassistant/components/hue/v2/hue_event.py b/homeassistant/components/hue/v2/hue_event.py index 4b9adf16226..8e85288eee0 100644 --- a/homeassistant/components/hue/v2/hue_event.py +++ b/homeassistant/components/hue/v2/hue_event.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType from aiohue.v2.models.button import Button +from aiohue.v2.models.relative_rotary import RelativeRotary from homeassistant.const import CONF_DEVICE_ID, CONF_ID, CONF_TYPE, CONF_UNIQUE_ID from homeassistant.core import callback @@ -14,6 +15,8 @@ from homeassistant.util import slugify from ..const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN as DOMAIN CONF_CONTROL_ID = "control_id" +CONF_DURATION = "duration" +CONF_STEPS = "steps" if TYPE_CHECKING: from ..bridge import HueBridge @@ -28,12 +31,12 @@ async def async_setup_hue_events(bridge: "HueBridge"): conf_entry = bridge.config_entry dev_reg = device_registry.async_get(hass) - # at this time the `button` resource is the only source of hue events btn_controller = api.sensors.button + rotary_controller = api.sensors.relative_rotary @callback def handle_button_event(evt_type: EventType, hue_resource: Button) -> None: - """Handle event from Hue devices controller.""" + """Handle event from Hue button resource controller.""" LOGGER.debug("Received button event: %s", hue_resource) # guard for missing button object on the resource @@ -60,3 +63,29 @@ async def async_setup_hue_events(bridge: "HueBridge"): handle_button_event, event_filter=EventType.RESOURCE_UPDATED ) ) + + @callback + def handle_rotary_event(evt_type: EventType, hue_resource: RelativeRotary) -> None: + """Handle event from Hue relative_rotary resource controller.""" + LOGGER.debug("Received relative_rotary event: %s", hue_resource) + + hue_device = btn_controller.get_device(hue_resource.id) + device = dev_reg.async_get_device({(DOMAIN, hue_device.id)}) + + # Fire event + data = { + CONF_DEVICE_ID: device.id, # type: ignore[union-attr] + CONF_UNIQUE_ID: hue_resource.id, + CONF_TYPE: hue_resource.relative_rotary.last_event.action.value, + CONF_SUBTYPE: hue_resource.relative_rotary.last_event.rotation.direction.value, + CONF_DURATION: hue_resource.relative_rotary.last_event.rotation.duration, + CONF_STEPS: hue_resource.relative_rotary.last_event.rotation.steps, + } + hass.bus.async_fire(ATTR_HUE_EVENT, data) + + # add listener for updates from `relative_rotary` resource + conf_entry.async_on_unload( + rotary_controller.subscribe( + handle_rotary_event, event_filter=EventType.RESOURCE_UPDATED + ) + ) From 702f8180a694cfeb4630e0c9432eb937f6821d6f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 20:00:42 +0200 Subject: [PATCH 3358/3516] Use TriggerActionType [l-t] (#76813) --- .../components/lcn/device_trigger.py | 11 ++++------ homeassistant/components/litejet/trigger.py | 11 ++++------ .../lutron_caseta/device_trigger.py | 11 ++++------ .../components/nanoleaf/device_trigger.py | 11 ++++------ .../components/nest/device_trigger.py | 11 ++++------ .../components/netatmo/device_trigger.py | 11 ++++------ .../components/philips_js/__init__.py | 4 ++-- .../components/philips_js/device_trigger.py | 11 ++++------ .../components/rfxtrx/device_trigger.py | 11 ++++------ .../components/shelly/device_trigger.py | 11 ++++------ .../components/tasmota/device_trigger.py | 21 ++++++++----------- 11 files changed, 47 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/lcn/device_trigger.py b/homeassistant/components/lcn/device_trigger.py index 8ae640cf6c2..46a94929d0b 100644 --- a/homeassistant/components/lcn/device_trigger.py +++ b/homeassistant/components/lcn/device_trigger.py @@ -3,15 +3,12 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import event from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, KEY_ACTIONS, SENDKEYS @@ -75,8 +72,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" event_data = { @@ -97,7 +94,7 @@ async def async_attach_trigger( ) return await event.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/litejet/trigger.py b/homeassistant/components/litejet/trigger.py index 5aff5dbc66c..a0cdeaf9a01 100644 --- a/homeassistant/components/litejet/trigger.py +++ b/homeassistant/components/litejet/trigger.py @@ -5,14 +5,11 @@ from collections.abc import Callable import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util @@ -39,11 +36,11 @@ TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for events based on configuration.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] number = config.get(CONF_NUMBER) held_more_than = config.get(CONF_HELD_MORE_THAN) held_less_than = config.get(CONF_HELD_LESS_THAN) diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index 27227619d45..77cad154c06 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -22,6 +18,7 @@ from homeassistant.const import ( from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import ( @@ -425,8 +422,8 @@ def _device_model_to_type(model: str) -> str: async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" device_registry = dr.async_get(hass) @@ -453,7 +450,7 @@ async def async_attach_trigger( } event_config = event_trigger.TRIGGER_SCHEMA(event_config) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/nanoleaf/device_trigger.py b/homeassistant/components/nanoleaf/device_trigger.py index 68dc6326719..5de093a4a17 100644 --- a/homeassistant/components/nanoleaf/device_trigger.py +++ b/homeassistant/components/nanoleaf/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import DeviceNotFound from homeassistant.components.homeassistant.triggers import event as event_trigger @@ -19,6 +15,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, NANOLEAF_EVENT, TOUCH_GESTURE_TRIGGER_MAP, TOUCH_MODELS @@ -58,8 +55,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" event_config = event_trigger.TRIGGER_SCHEMA( @@ -73,5 +70,5 @@ async def async_attach_trigger( } ) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index cb546c87ee4..f7369489e22 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -14,6 +10,7 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -57,8 +54,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" event_config = event_trigger.TRIGGER_SCHEMA( @@ -72,5 +69,5 @@ async def async_attach_trigger( } ) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/netatmo/device_trigger.py b/homeassistant/components/netatmo/device_trigger.py index 25f76307b5f..955671e3dc1 100644 --- a/homeassistant/components/netatmo/device_trigger.py +++ b/homeassistant/components/netatmo/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -26,6 +22,7 @@ from homeassistant.helpers import ( device_registry as dr, entity_registry, ) +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .climate import STATE_NETATMO_AWAY, STATE_NETATMO_HG, STATE_NETATMO_SCHEDULE @@ -140,8 +137,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" device_registry = dr.async_get(hass) @@ -169,5 +166,5 @@ async def async_attach_trigger( event_config = event_trigger.TRIGGER_SCHEMA(event_config) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/philips_js/__init__.py b/homeassistant/components/philips_js/__init__.py index 154df3ed214..29c8ab36ba2 100644 --- a/homeassistant/components/philips_js/__init__.py +++ b/homeassistant/components/philips_js/__init__.py @@ -10,7 +10,6 @@ from typing import Any from haphilipsjs import ConnectionFailure, PhilipsTV from haphilipsjs.typing import SystemType -from homeassistant.components.automation import AutomationActionType from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_VERSION, @@ -21,6 +20,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, HassJob, HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.trigger import TriggerActionType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, DOMAIN @@ -93,7 +93,7 @@ class PluggableAction: return bool(self._actions) @callback - def async_attach(self, action: AutomationActionType, variables: dict[str, Any]): + def async_attach(self, action: TriggerActionType, variables: dict[str, Any]): """Attach a device trigger for turn on.""" @callback diff --git a/homeassistant/components/philips_js/device_trigger.py b/homeassistant/components/philips_js/device_trigger.py index eca3158fb15..d7ce9807d64 100644 --- a/homeassistant/components/philips_js/device_trigger.py +++ b/homeassistant/components/philips_js/device_trigger.py @@ -3,15 +3,12 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import PhilipsTVDataUpdateCoordinator @@ -47,11 +44,11 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] registry: dr.DeviceRegistry = dr.async_get(hass) if (trigger_type := config[CONF_TYPE]) == TRIGGER_TYPE_TURN_ON: variables = { diff --git a/homeassistant/components/rfxtrx/device_trigger.py b/homeassistant/components/rfxtrx/device_trigger.py index 196377dd60f..a2f32395572 100644 --- a/homeassistant/components/rfxtrx/device_trigger.py +++ b/homeassistant/components/rfxtrx/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -20,6 +16,7 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -91,8 +88,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" config = TRIGGER_SCHEMA(config) @@ -113,5 +110,5 @@ async def async_attach_trigger( ) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py index 0f9fc55ed71..fe253ebacb6 100644 --- a/homeassistant/components/shelly/device_trigger.py +++ b/homeassistant/components/shelly/device_trigger.py @@ -5,10 +5,6 @@ from typing import Final import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -23,6 +19,7 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import get_block_device_wrapper, get_rpc_device_wrapper @@ -140,8 +137,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" event_config = { @@ -156,5 +153,5 @@ async def async_attach_trigger( event_config = event_trigger.TRIGGER_SCHEMA(event_config) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py index 69cb3a0bd72..79637e90424 100644 --- a/homeassistant/components/tasmota/device_trigger.py +++ b/homeassistant/components/tasmota/device_trigger.py @@ -9,10 +9,6 @@ from hatasmota.models import DiscoveryHashType from hatasmota.trigger import TasmotaTrigger, TasmotaTriggerConfig import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.config_entries import ConfigEntry @@ -22,6 +18,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, TASMOTA_EVENT @@ -51,8 +48,8 @@ DEVICE_TRIGGERS = "tasmota_device_triggers" class TriggerInstance: """Attached trigger settings.""" - action: AutomationActionType = attr.ib() - automation_info: AutomationTriggerInfo = attr.ib() + action: TriggerActionType = attr.ib() + trigger_info: TriggerInfo = attr.ib() trigger: Trigger = attr.ib() remove: CALLBACK_TYPE | None = attr.ib(default=None) @@ -77,7 +74,7 @@ class TriggerInstance: self.trigger.hass, event_config, self.action, - self.automation_info, + self.trigger_info, platform_type="device", ) @@ -96,10 +93,10 @@ class Trigger: trigger_instances: list[TriggerInstance] = attr.ib(factory=list) async def add_trigger( - self, action: AutomationActionType, automation_info: AutomationTriggerInfo + self, action: TriggerActionType, trigger_info: TriggerInfo ) -> Callable[[], None]: """Add Tasmota trigger.""" - instance = TriggerInstance(action, automation_info, self) + instance = TriggerInstance(action, trigger_info, self) self.trigger_instances.append(instance) if self.tasmota_trigger is not None: @@ -303,8 +300,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a device trigger.""" if DEVICE_TRIGGERS not in hass.data: @@ -325,4 +322,4 @@ async def async_attach_trigger( tasmota_trigger=None, ) trigger: Trigger = device_triggers[discovery_id] - return await trigger.add_trigger(action, automation_info) + return await trigger.add_trigger(action, trigger_info) From badbc414fb88f2129258525b803ce7785905d141 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 20:15:57 +0200 Subject: [PATCH 3359/3516] Use TriggerActionType [w-z] (#76814) --- homeassistant/components/webostv/__init__.py | 4 ++-- .../components/webostv/device_trigger.py | 11 ++++------- homeassistant/components/webostv/trigger.py | 11 ++++------- .../components/webostv/triggers/turn_on.py | 11 ++++------- homeassistant/components/wemo/device_trigger.py | 11 ++++------- homeassistant/components/zha/device_trigger.py | 11 ++++------- .../components/zwave_js/device_trigger.py | 15 ++++++--------- homeassistant/components/zwave_js/trigger.py | 11 ++++------- .../components/zwave_js/triggers/event.py | 11 ++++------- .../components/zwave_js/triggers/value_updated.py | 11 ++++------- 10 files changed, 40 insertions(+), 67 deletions(-) diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index 32161e6bad6..8b023990590 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -10,7 +10,6 @@ from aiowebostv import WebOsClient, WebOsTvPairError import voluptuous as vol from homeassistant.components import notify as hass_notify -from homeassistant.components.automation import AutomationActionType from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_COMMAND, @@ -30,6 +29,7 @@ from homeassistant.core import ( ) from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.trigger import TriggerActionType from homeassistant.helpers.typing import ConfigType from .const import ( @@ -181,7 +181,7 @@ class PluggableAction: @callback def async_attach( - self, action: AutomationActionType, variables: dict[str, Any] + self, action: TriggerActionType, variables: dict[str, Any] ) -> Callable[[], None]: """Attach a device trigger for turn on.""" diff --git a/homeassistant/components/webostv/device_trigger.py b/homeassistant/components/webostv/device_trigger.py index 9ce49bbe79e..859accc86e6 100644 --- a/homeassistant/components/webostv/device_trigger.py +++ b/homeassistant/components/webostv/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -14,6 +10,7 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import trigger @@ -75,8 +72,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if (trigger_type := config[CONF_TYPE]) == TURN_ON_PLATFORM_TYPE: @@ -88,7 +85,7 @@ async def async_attach_trigger( hass, trigger_config ) return await trigger.async_attach_trigger( - hass, trigger_config, action, automation_info + hass, trigger_config, action, trigger_info ) raise HomeAssistantError(f"Unhandled trigger type {trigger_type}") diff --git a/homeassistant/components/webostv/trigger.py b/homeassistant/components/webostv/trigger.py index 1ad7058e1de..5441917cc31 100644 --- a/homeassistant/components/webostv/trigger.py +++ b/homeassistant/components/webostv/trigger.py @@ -3,12 +3,9 @@ from __future__ import annotations from typing import cast -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .triggers import TriggersPlatformModule, turn_on @@ -39,8 +36,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach trigger of specified platform.""" platform = _get_trigger_platform(config) @@ -48,6 +45,6 @@ async def async_attach_trigger( return cast( CALLBACK_TYPE, await getattr(platform, "async_attach_trigger")( - hass, config, action, automation_info + hass, config, action, trigger_info ), ) diff --git a/homeassistant/components/webostv/triggers/turn_on.py b/homeassistant/components/webostv/triggers/turn_on.py index 71949ce58ce..806b0b4b964 100644 --- a/homeassistant/components/webostv/triggers/turn_on.py +++ b/homeassistant/components/webostv/triggers/turn_on.py @@ -3,13 +3,10 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from ..const import DOMAIN @@ -39,8 +36,8 @@ TRIGGER_SCHEMA = vol.All( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, *, platform_type: str = PLATFORM_TYPE, ) -> CALLBACK_TYPE | None: @@ -57,7 +54,7 @@ async def async_attach_trigger( } ) - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] unsubs = [] diff --git a/homeassistant/components/wemo/device_trigger.py b/homeassistant/components/wemo/device_trigger.py index 973a3358b1b..077c32fb1ad 100644 --- a/homeassistant/components/wemo/device_trigger.py +++ b/homeassistant/components/wemo/device_trigger.py @@ -4,14 +4,11 @@ from __future__ import annotations from pywemo.subscribe import EVENT_TYPE_LONG_PRESS import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN as WEMO_DOMAIN, WEMO_SUBSCRIPTION_EVENT @@ -57,8 +54,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" event_config = event_trigger.TRIGGER_SCHEMA( @@ -72,5 +69,5 @@ async def async_attach_trigger( } ) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 4ad8eccea1d..94b94b89e40 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -2,10 +2,6 @@ import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -14,6 +10,7 @@ from homeassistant.components.homeassistant.triggers import event as event_trigg from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.exceptions import HomeAssistantError, IntegrationError +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN as ZHA_DOMAIN @@ -54,8 +51,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" trigger_key: tuple[str, str] = (config[CONF_TYPE], config[CONF_SUBTYPE]) @@ -79,7 +76,7 @@ async def async_attach_trigger( event_config = event_trigger.TRIGGER_SCHEMA(event_config) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/zwave_js/device_trigger.py b/homeassistant/components/zwave_js/device_trigger.py index dfca2eb4b27..76a7f134d17 100644 --- a/homeassistant/components/zwave_js/device_trigger.py +++ b/homeassistant/components/zwave_js/device_trigger.py @@ -6,10 +6,6 @@ from typing import Any import voluptuous as vol from zwave_js_server.const import CommandClass -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -29,6 +25,7 @@ from homeassistant.helpers import ( device_registry, entity_registry, ) +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import trigger @@ -366,8 +363,8 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" trigger_type = config[CONF_TYPE] @@ -411,7 +408,7 @@ async def async_attach_trigger( event_config = event.TRIGGER_SCHEMA(event_config) return await event.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) if trigger_platform == "state": @@ -427,7 +424,7 @@ async def async_attach_trigger( state_config = await state.async_validate_trigger_config(hass, state_config) return await state.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, state_config, action, trigger_info, platform_type="device" ) if trigger_platform == VALUE_UPDATED_PLATFORM_TYPE: @@ -451,7 +448,7 @@ async def async_attach_trigger( hass, zwave_js_config ) return await trigger.async_attach_trigger( - hass, zwave_js_config, action, automation_info + hass, zwave_js_config, action, trigger_info ) raise HomeAssistantError(f"Unhandled trigger type {trigger_type}") diff --git a/homeassistant/components/zwave_js/trigger.py b/homeassistant/components/zwave_js/trigger.py index 07f89388e67..d1751dc4f4f 100644 --- a/homeassistant/components/zwave_js/trigger.py +++ b/homeassistant/components/zwave_js/trigger.py @@ -4,12 +4,9 @@ from __future__ import annotations from types import ModuleType from typing import cast -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.const import CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .triggers import event, value_updated @@ -45,8 +42,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach trigger of specified platform.""" platform = _get_trigger_platform(config) @@ -54,6 +51,6 @@ async def async_attach_trigger( return cast( CALLBACK_TYPE, await getattr(platform, "async_attach_trigger")( - hass, config, action, automation_info + hass, config, action, trigger_info ), ) diff --git a/homeassistant/components/zwave_js/triggers/event.py b/homeassistant/components/zwave_js/triggers/event.py index 784ae74777b..eecd685cc1b 100644 --- a/homeassistant/components/zwave_js/triggers/event.py +++ b/homeassistant/components/zwave_js/triggers/event.py @@ -10,10 +10,6 @@ from zwave_js_server.model.controller import CONTROLLER_EVENT_MODEL_MAP from zwave_js_server.model.driver import DRIVER_EVENT_MODEL_MAP from zwave_js_server.model.node import NODE_EVENT_MODEL_MAP -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.zwave_js.const import ( ATTR_CONFIG_ENTRY_ID, ATTR_EVENT, @@ -32,6 +28,7 @@ from homeassistant.components.zwave_js.helpers import ( from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .helpers import async_bypass_dynamic_config_validation @@ -136,8 +133,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, *, platform_type: str = PLATFORM_TYPE, ) -> CALLBACK_TYPE: @@ -156,7 +153,7 @@ async def async_attach_trigger( unsubs = [] job = HassJob(action) - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] @callback def async_on_event(event_data: dict, device: dr.DeviceEntry | None = None) -> None: diff --git a/homeassistant/components/zwave_js/triggers/value_updated.py b/homeassistant/components/zwave_js/triggers/value_updated.py index 29b4b4d06d6..6a94ab0577b 100644 --- a/homeassistant/components/zwave_js/triggers/value_updated.py +++ b/homeassistant/components/zwave_js/triggers/value_updated.py @@ -7,10 +7,6 @@ import voluptuous as vol from zwave_js_server.const import CommandClass from zwave_js_server.model.value import Value, get_value_id -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.zwave_js.config_validation import VALUE_SCHEMA from homeassistant.components.zwave_js.const import ( ATTR_COMMAND_CLASS, @@ -34,6 +30,7 @@ from homeassistant.components.zwave_js.helpers import ( from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM, MATCH_ALL from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .helpers import async_bypass_dynamic_config_validation @@ -87,8 +84,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, *, platform_type: str = PLATFORM_TYPE, ) -> CALLBACK_TYPE: @@ -108,7 +105,7 @@ async def async_attach_trigger( unsubs = [] job = HassJob(action) - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] @callback def async_on_value_updated( From d8916c3e47d02f9bae08111b033d230529c78d00 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 20:26:49 +0200 Subject: [PATCH 3360/3516] Use TriggerActionType [a-k] (#76812) --- .../components/arcam_fmj/device_trigger.py | 11 ++++------- .../components/deconz/device_trigger.py | 11 ++++------- .../homekit_controller/device_trigger.py | 17 +++++++---------- .../components/hue/device_trigger.py | 13 +++++-------- .../components/hue/v1/device_trigger.py | 11 ++++------- .../components/hue/v2/device_trigger.py | 11 ++++------- .../components/kodi/device_trigger.py | 19 ++++++++----------- 7 files changed, 36 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/arcam_fmj/device_trigger.py b/homeassistant/components/arcam_fmj/device_trigger.py index 593250e4983..13f1acc7244 100644 --- a/homeassistant/components/arcam_fmj/device_trigger.py +++ b/homeassistant/components/arcam_fmj/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.const import ( ATTR_ENTITY_ID, @@ -18,6 +14,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, EVENT_TURN_ON @@ -57,11 +54,11 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] job = HassJob(action) if config[CONF_TYPE] == "turn_on": diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index e15513bddbf..601fe95616b 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -22,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from . import DOMAIN @@ -668,8 +665,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" event_data: dict[str, int | str] = {} @@ -693,7 +690,7 @@ async def async_attach_trigger( event_config = event_trigger.TRIGGER_SCHEMA(raw_event_config) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/homekit_controller/device_trigger.py b/homeassistant/components/homekit_controller/device_trigger.py index 700ab60c47f..ecffb902928 100644 --- a/homeassistant/components/homekit_controller/device_trigger.py +++ b/homeassistant/components/homekit_controller/device_trigger.py @@ -10,14 +10,11 @@ from aiohomekit.model.services import ServicesTypes from aiohomekit.utils import clamp_enum_to_char import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, KNOWN_DEVICES, TRIGGERS @@ -84,11 +81,11 @@ class TriggerSource: async def async_attach_trigger( self, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] job = HassJob(action) @callback @@ -269,10 +266,10 @@ async def async_get_triggers( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" device_id = config[CONF_DEVICE_ID] device = hass.data[TRIGGERS][device_id] - return await device.async_attach_trigger(config, action, automation_info) + return await device.async_attach_trigger(config, action, trigger_info) diff --git a/homeassistant/components/hue/device_trigger.py b/homeassistant/components/hue/device_trigger.py index e8eb7695ed9..069f0d42d8d 100644 --- a/homeassistant/components/hue/device_trigger.py +++ b/homeassistant/components/hue/device_trigger.py @@ -24,11 +24,8 @@ from .v2.device_trigger import ( ) if TYPE_CHECKING: - from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, - ) from homeassistant.core import HomeAssistant + from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from .bridge import HueBridge @@ -59,8 +56,8 @@ async def async_validate_trigger_config( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" device_id = config[CONF_DEVICE_ID] @@ -75,10 +72,10 @@ async def async_attach_trigger( bridge: HueBridge = hass.data[DOMAIN][conf_entry_id] if bridge.api_version == 1: return await async_attach_trigger_v1( - bridge, device_entry, config, action, automation_info + bridge, device_entry, config, action, trigger_info ) return await async_attach_trigger_v2( - bridge, device_entry, config, action, automation_info + bridge, device_entry, config, action, trigger_info ) raise InvalidDeviceAutomationConfig( f"Device ID {device_id} is not found on any Hue bridge" diff --git a/homeassistant/components/hue/v1/device_trigger.py b/homeassistant/components/hue/v1/device_trigger.py index 579f4b71efb..4316ea65406 100644 --- a/homeassistant/components/hue/v1/device_trigger.py +++ b/homeassistant/components/hue/v1/device_trigger.py @@ -3,10 +3,6 @@ from typing import TYPE_CHECKING import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, @@ -22,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from ..const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN @@ -148,8 +145,8 @@ async def async_attach_trigger( bridge: "HueBridge", device_entry: DeviceEntry, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" hass = bridge.hass @@ -171,7 +168,7 @@ async def async_attach_trigger( event_config = event_trigger.TRIGGER_SCHEMA(event_config) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/hue/v2/device_trigger.py b/homeassistant/components/hue/v2/device_trigger.py index 0fa9e37569d..62ce0ec1edf 100644 --- a/homeassistant/components/hue/v2/device_trigger.py +++ b/homeassistant/components/hue/v2/device_trigger.py @@ -29,10 +29,7 @@ from ..const import ATTR_HUE_EVENT, CONF_SUBTYPE, DOMAIN if TYPE_CHECKING: from aiohue.v2 import HueBridgeV2 - from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, - ) + from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from ..bridge import HueBridge @@ -77,8 +74,8 @@ async def async_attach_trigger( bridge: "HueBridge", device_entry: DeviceEntry, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Listen for state changes based on configuration.""" hass = bridge.hass @@ -94,7 +91,7 @@ async def async_attach_trigger( } ) return await event_trigger.async_attach_trigger( - hass, event_config, action, automation_info, platform_type="device" + hass, event_config, action, trigger_info, platform_type="device" ) diff --git a/homeassistant/components/kodi/device_trigger.py b/homeassistant/components/kodi/device_trigger.py index 11d0b1567f9..07fcf11c077 100644 --- a/homeassistant/components/kodi/device_trigger.py +++ b/homeassistant/components/kodi/device_trigger.py @@ -3,10 +3,6 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.components.automation import ( - AutomationActionType, - AutomationTriggerInfo, -) from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.const import ( ATTR_ENTITY_ID, @@ -18,6 +14,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, EVENT_TURN_OFF, EVENT_TURN_ON @@ -68,11 +65,11 @@ async def async_get_triggers( def _attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, + action: TriggerActionType, event_type, - automation_info: AutomationTriggerInfo, + trigger_info: TriggerInfo, ): - trigger_data = automation_info["trigger_data"] + trigger_data = trigger_info["trigger_data"] job = HassJob(action) @callback @@ -90,14 +87,14 @@ def _attach_trigger( async def async_attach_trigger( hass: HomeAssistant, config: ConfigType, - action: AutomationActionType, - automation_info: AutomationTriggerInfo, + action: TriggerActionType, + trigger_info: TriggerInfo, ) -> CALLBACK_TYPE: """Attach a trigger.""" if config[CONF_TYPE] == "turn_on": - return _attach_trigger(hass, config, action, EVENT_TURN_ON, automation_info) + return _attach_trigger(hass, config, action, EVENT_TURN_ON, trigger_info) if config[CONF_TYPE] == "turn_off": - return _attach_trigger(hass, config, action, EVENT_TURN_OFF, automation_info) + return _attach_trigger(hass, config, action, EVENT_TURN_OFF, trigger_info) return lambda: None From 4890785299be7b7e7984e5162415a9bdbf9e1fac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Aug 2022 10:19:37 -1000 Subject: [PATCH 3361/3516] Fix bluetooth callback registration not surviving a reload (#76817) --- .../components/bluetooth/__init__.py | 21 ++-- tests/components/bluetooth/test_init.py | 97 +++++++++++++++++++ 2 files changed, 110 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index a5204d50b68..19df484c4e1 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -364,13 +364,13 @@ class BluetoothManager: async with async_timeout.timeout(START_TIMEOUT): await self.scanner.start() # type: ignore[no-untyped-call] except InvalidMessageError as ex: - self._cancel_device_detected() + self._async_cancel_scanner_callback() _LOGGER.debug("Invalid DBus message received: %s", ex, exc_info=True) raise ConfigEntryNotReady( f"Invalid DBus message received: {ex}; try restarting `dbus`" ) from ex except BrokenPipeError as ex: - self._cancel_device_detected() + self._async_cancel_scanner_callback() _LOGGER.debug("DBus connection broken: %s", ex, exc_info=True) if is_docker_env(): raise ConfigEntryNotReady( @@ -380,7 +380,7 @@ class BluetoothManager: f"DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`" ) from ex except FileNotFoundError as ex: - self._cancel_device_detected() + self._async_cancel_scanner_callback() _LOGGER.debug( "FileNotFoundError while starting bluetooth: %s", ex, exc_info=True ) @@ -392,12 +392,12 @@ class BluetoothManager: f"DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}" ) from ex except asyncio.TimeoutError as ex: - self._cancel_device_detected() + self._async_cancel_scanner_callback() raise ConfigEntryNotReady( f"Timed out starting Bluetooth after {START_TIMEOUT} seconds" ) from ex except BleakError as ex: - self._cancel_device_detected() + self._async_cancel_scanner_callback() _LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True) raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex self.async_setup_unavailable_tracking() @@ -579,15 +579,20 @@ class BluetoothManager: self._cancel_stop = None await self.async_stop() + @hass_callback + def _async_cancel_scanner_callback(self) -> None: + """Cancel the scanner callback.""" + if self._cancel_device_detected: + self._cancel_device_detected() + self._cancel_device_detected = None + async def async_stop(self) -> None: """Stop bluetooth discovery.""" _LOGGER.debug("Stopping bluetooth discovery") if self._cancel_watchdog: self._cancel_watchdog() self._cancel_watchdog = None - if self._cancel_device_detected: - self._cancel_device_detected() - self._cancel_device_detected = None + self._async_cancel_scanner_callback() if self._cancel_unavailable_tracking: self._cancel_unavailable_tracking() self._cancel_unavailable_tracking = None diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 45babd05748..796b3ffb469 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -165,6 +165,43 @@ async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): await hass.async_block_till_done() +async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): + """Test we can successfully reload when the entry is in a retry state.""" + mock_bt = [] + with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + side_effect=BleakError, + ), patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] + + assert "Failed to start Bluetooth" in caplog.text + assert len(bluetooth.async_discovered_service_info(hass)) == 0 + assert entry.state == ConfigEntryState.SETUP_RETRY + + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.start", + ): + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + + with patch( + "homeassistant.components.bluetooth.HaBleakScanner.stop", + ): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): """Test we fail gracefully when asking for discovered devices and there is no blueooth.""" mock_bt = [] @@ -868,6 +905,66 @@ async def test_register_callback_by_address( assert service_info.manufacturer_id == 89 +async def test_register_callback_survives_reload( + hass, mock_bleak_scanner_start, enable_bluetooth +): + """Test registering a callback by address survives bluetooth being reloaded.""" + mock_bt = [] + callbacks = [] + + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + """Fake subscriber for the BleakScanner.""" + callbacks.append((service_info, change)) + + with patch( + "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + bluetooth.async_register_callback( + hass, + _fake_subscriber, + {"address": "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, + ) + + assert len(mock_bleak_scanner_start.mock_calls) == 1 + + switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") + switchbot_adv = AdvertisementData( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, + ) + + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + assert len(callbacks) == 1 + service_info: BluetoothServiceInfo = callbacks[0][0] + assert service_info.name == "wohand" + assert service_info.manufacturer == "Nordic Semiconductor ASA" + assert service_info.manufacturer_id == 89 + + entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + assert len(callbacks) == 2 + service_info: BluetoothServiceInfo = callbacks[1][0] + assert service_info.name == "wohand" + assert service_info.manufacturer == "Nordic Semiconductor ASA" + assert service_info.manufacturer_id == 89 + + async def test_process_advertisements_bail_on_good_advertisement( hass: HomeAssistant, mock_bleak_scanner_start, enable_bluetooth ): From f400a404cdc3c647fc136af3bc6001c54a2cfb7f Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 15 Aug 2022 23:27:08 +0200 Subject: [PATCH 3362/3516] Update pylint to 2.14.5 (#76821) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 6685d06c3b0..63634bc4022 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -13,7 +13,7 @@ freezegun==1.2.1 mock-open==1.4.0 mypy==0.971 pre-commit==2.20.0 -pylint==2.14.4 +pylint==2.14.5 pipdeptree==2.2.1 pytest-aiohttp==0.3.0 pytest-cov==3.0.0 From 1c1b23ef69822850abc698b56576489403310087 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 15 Aug 2022 23:35:30 +0200 Subject: [PATCH 3363/3516] Correct referenced entities and devices for event triggers (#76818) --- .../components/automation/__init__.py | 8 +++-- tests/components/automation/test_init.py | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 43ff31dfab8..7421e95f293 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -36,6 +36,7 @@ from homeassistant.core import ( HomeAssistant, callback, split_entity_id, + valid_entity_id, ) from homeassistant.exceptions import ( ConditionError, @@ -355,7 +356,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): referenced |= condition.async_extract_devices(conf) for conf in self._trigger_config: - referenced |= set(_trigger_extract_device(conf)) + referenced |= set(_trigger_extract_devices(conf)) self._referenced_devices = referenced return referenced @@ -762,7 +763,7 @@ async def _async_process_if(hass, name, config, p_config): @callback -def _trigger_extract_device(trigger_conf: dict) -> list[str]: +def _trigger_extract_devices(trigger_conf: dict) -> list[str]: """Extract devices from a trigger config.""" if trigger_conf[CONF_PLATFORM] == "device": return [trigger_conf[CONF_DEVICE_ID]] @@ -771,6 +772,7 @@ def _trigger_extract_device(trigger_conf: dict) -> list[str]: trigger_conf[CONF_PLATFORM] == "event" and CONF_EVENT_DATA in trigger_conf and CONF_DEVICE_ID in trigger_conf[CONF_EVENT_DATA] + and isinstance(trigger_conf[CONF_EVENT_DATA][CONF_DEVICE_ID], str) ): return [trigger_conf[CONF_EVENT_DATA][CONF_DEVICE_ID]] @@ -802,6 +804,8 @@ def _trigger_extract_entities(trigger_conf: dict) -> list[str]: trigger_conf[CONF_PLATFORM] == "event" and CONF_EVENT_DATA in trigger_conf and CONF_ENTITY_ID in trigger_conf[CONF_EVENT_DATA] + and isinstance(trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID], str) + and valid_entity_id(trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID]) ): return [trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID]] diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index bcbcf382892..cef553653de 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1103,6 +1103,24 @@ async def test_extraction_functions(hass): "event_type": "state_changed", "event_data": {"entity_id": "sensor.trigger_event"}, }, + # entity_id is a list of strings (not supported) + { + "platform": "event", + "event_type": "state_changed", + "event_data": {"entity_id": ["sensor.trigger_event2"]}, + }, + # entity_id is not a valid entity ID + { + "platform": "event", + "event_type": "state_changed", + "event_data": {"entity_id": "abc"}, + }, + # entity_id is not a string + { + "platform": "event", + "event_type": "state_changed", + "event_data": {"entity_id": 123}, + }, ], "condition": { "condition": "state", @@ -1151,6 +1169,18 @@ async def test_extraction_functions(hass): "event_type": "esphome.button_pressed", "event_data": {"device_id": "device-trigger-event"}, }, + # device_id is a list of strings (not supported) + { + "platform": "event", + "event_type": "esphome.button_pressed", + "event_data": {"device_id": ["device-trigger-event"]}, + }, + # device_id is not a string + { + "platform": "event", + "event_type": "esphome.button_pressed", + "event_data": {"device_id": 123}, + }, ], "condition": { "condition": "device", From ff3fd4c29d09b1cc5d3158643ff1dad3de5c9cbb Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 16 Aug 2022 00:30:51 +0000 Subject: [PATCH 3364/3516] [ci skip] Translation update --- .../components/acmeda/translations/es.json | 2 +- .../components/airtouch4/translations/es.json | 2 +- .../components/airvisual/translations/es.json | 4 +- .../components/almond/translations/es.json | 2 +- .../android_ip_webcam/translations/fi.json | 12 +++ .../android_ip_webcam/translations/ja.json | 25 +++++ .../android_ip_webcam/translations/ru.json | 26 ++++++ .../components/androidtv/translations/es.json | 2 +- .../components/apple_tv/translations/es.json | 16 ++-- .../components/asuswrt/translations/es.json | 4 +- .../components/atag/translations/es.json | 2 +- .../aurora_abb_powerone/translations/es.json | 6 +- .../components/auth/translations/es.json | 6 +- .../components/awair/translations/ca.json | 9 +- .../components/awair/translations/el.json | 15 ++- .../components/awair/translations/fi.json | 16 ++++ .../components/awair/translations/ja.json | 31 ++++++- .../components/awair/translations/no.json | 31 ++++++- .../components/awair/translations/ru.json | 31 ++++++- .../components/bluetooth/translations/es.json | 2 +- .../components/bosch_shc/translations/es.json | 4 +- .../components/broadlink/translations/fr.json | 2 +- .../components/cast/translations/es.json | 4 +- .../components/climate/translations/es.json | 6 +- .../components/cover/translations/es.json | 4 +- .../components/demo/translations/ca.json | 2 +- .../components/demo/translations/ja.json | 6 +- .../deutsche_bahn/translations/ca.json | 1 + .../deutsche_bahn/translations/es.json | 2 +- .../device_tracker/translations/es.json | 2 +- .../components/doorbird/translations/es.json | 2 +- .../components/dsmr/translations/ja.json | 1 + .../components/eafm/translations/es.json | 2 +- .../components/elkm1/translations/es.json | 2 +- .../components/elmax/translations/es.json | 2 +- .../components/escea/translations/ja.json | 5 + .../components/esphome/translations/es.json | 4 +- .../components/fivem/translations/es.json | 2 +- .../flunearyou/translations/ca.json | 1 + .../flunearyou/translations/es.json | 2 +- .../flunearyou/translations/it.json | 1 + .../forecast_solar/translations/es.json | 2 +- .../forked_daapd/translations/es.json | 2 +- .../fritzbox_callmonitor/translations/es.json | 2 +- .../components/generic/translations/es.json | 4 +- .../geocaching/translations/es.json | 2 +- .../components/github/translations/es.json | 2 +- .../components/google/translations/es.json | 4 +- .../components/group/translations/es.json | 4 +- .../components/guardian/translations/ca.json | 13 +++ .../components/guardian/translations/es.json | 2 +- .../components/guardian/translations/ja.json | 12 +++ .../components/guardian/translations/ru.json | 13 +++ .../components/habitica/translations/es.json | 2 +- .../components/hassio/translations/es.json | 2 +- .../components/hive/translations/es.json | 2 +- .../home_connect/translations/es.json | 2 +- .../home_plus_control/translations/es.json | 2 +- .../components/homekit/translations/es.json | 2 +- .../homekit_controller/translations/es.json | 4 +- .../translations/sensor.it.json | 15 ++- .../homematicip_cloud/translations/es.json | 6 +- .../components/honeywell/translations/es.json | 2 +- .../huawei_lte/translations/es.json | 2 +- .../components/hue/translations/ca.json | 17 ++-- .../components/hue/translations/en.json | 6 +- .../components/hue/translations/es.json | 4 +- .../components/hue/translations/id.json | 17 ++-- .../components/hue/translations/pt-BR.json | 17 ++-- .../humidifier/translations/es.json | 2 +- .../components/hyperion/translations/es.json | 2 +- .../components/icloud/translations/es.json | 2 +- .../components/insteon/translations/es.json | 6 +- .../components/ipma/translations/es.json | 2 +- .../components/ipp/translations/es.json | 2 +- .../justnimbus/translations/ja.json | 19 ++++ .../components/knx/translations/es.json | 8 +- .../components/kodi/translations/es.json | 2 +- .../components/konnected/translations/es.json | 2 +- .../components/kraken/translations/ja.json | 3 + .../components/lifx/translations/es.json | 2 +- .../components/mailgun/translations/es.json | 2 +- .../components/mazda/translations/es.json | 4 +- .../components/meater/translations/es.json | 2 +- .../media_player/translations/es.json | 2 +- .../met_eireann/translations/es.json | 2 +- .../meteo_france/translations/es.json | 2 +- .../components/metoffice/translations/es.json | 2 +- .../components/miflora/translations/es.json | 2 +- .../minecraft_server/translations/es.json | 6 +- .../components/mitemp_bt/translations/ca.json | 2 +- .../components/mitemp_bt/translations/es.json | 2 +- .../motion_blinds/translations/es.json | 4 +- .../components/mysensors/translations/ja.json | 2 + .../components/nam/translations/es.json | 2 +- .../components/neato/translations/es.json | 2 +- .../components/nest/translations/ca.json | 2 + .../components/nest/translations/es.json | 14 +-- .../components/netatmo/translations/es.json | 4 +- .../components/netgear/translations/es.json | 2 +- .../components/nina/translations/es.json | 2 +- .../nmap_tracker/translations/es.json | 2 +- .../components/nws/translations/es.json | 2 +- .../components/onvif/translations/es.json | 6 +- .../openexchangerates/translations/ca.json | 1 + .../openexchangerates/translations/ja.json | 9 +- .../components/overkiz/translations/es.json | 4 +- .../ovo_energy/translations/es.json | 2 +- .../components/plaato/translations/es.json | 4 +- .../components/plugwise/translations/es.json | 2 +- .../components/point/translations/es.json | 8 +- .../components/qingping/translations/ca.json | 22 +++++ .../components/qingping/translations/de.json | 22 +++++ .../components/qingping/translations/el.json | 22 +++++ .../components/qingping/translations/es.json | 22 +++++ .../components/qingping/translations/et.json | 22 +++++ .../components/qingping/translations/fi.json | 7 ++ .../components/qingping/translations/fr.json | 22 +++++ .../components/qingping/translations/hu.json | 22 +++++ .../components/qingping/translations/id.json | 22 +++++ .../components/qingping/translations/it.json | 22 +++++ .../components/qingping/translations/ja.json | 22 +++++ .../components/qingping/translations/no.json | 22 +++++ .../qingping/translations/pt-BR.json | 22 +++++ .../components/qingping/translations/ru.json | 22 +++++ .../qingping/translations/zh-Hant.json | 22 +++++ .../components/roon/translations/ja.json | 3 + .../rtsp_to_webrtc/translations/es.json | 8 +- .../components/samsungtv/translations/es.json | 2 +- .../components/schedule/translations/el.json | 6 ++ .../components/schedule/translations/fi.json | 3 + .../components/schedule/translations/ja.json | 9 ++ .../components/schedule/translations/no.json | 9 ++ .../components/schedule/translations/ru.json | 9 ++ .../components/scrape/translations/es.json | 12 +-- .../components/select/translations/es.json | 2 +- .../components/sensor/translations/es.json | 92 +++++++++---------- .../components/senz/translations/es.json | 2 +- .../components/shelly/translations/es.json | 2 +- .../simplisafe/translations/ca.json | 2 +- .../simplisafe/translations/es.json | 2 +- .../simplisafe/translations/it.json | 2 +- .../simplisafe/translations/ja.json | 1 + .../components/smappee/translations/es.json | 4 +- .../smartthings/translations/es.json | 6 +- .../components/solarlog/translations/es.json | 2 +- .../components/spotify/translations/es.json | 2 +- .../components/sql/translations/es.json | 4 +- .../steam_online/translations/es.json | 2 +- .../components/subaru/translations/es.json | 6 +- .../switch_as_x/translations/es.json | 2 +- .../components/switchbot/translations/el.json | 3 + .../components/switchbot/translations/fi.json | 11 +++ .../components/switchbot/translations/ja.json | 12 +++ .../components/switchbot/translations/ru.json | 9 ++ .../tellduslive/translations/es.json | 2 +- .../tesla_wall_connector/translations/es.json | 2 +- .../tomorrowio/translations/es.json | 2 +- .../components/toon/translations/es.json | 2 +- .../totalconnect/translations/es.json | 2 +- .../components/traccar/translations/es.json | 2 +- .../components/tractive/translations/es.json | 2 +- .../transmission/translations/es.json | 2 +- .../components/twilio/translations/es.json | 2 +- .../components/unifi/translations/es.json | 12 +-- .../unifiprotect/translations/ca.json | 1 + .../unifiprotect/translations/es.json | 2 +- .../unifiprotect/translations/ja.json | 1 + .../components/upb/translations/es.json | 4 +- .../components/update/translations/es.json | 2 +- .../components/upnp/translations/ja.json | 3 + .../uptimerobot/translations/es.json | 2 +- .../components/uscis/translations/es.json | 2 +- .../components/version/translations/es.json | 4 +- .../components/vulcan/translations/es.json | 2 +- .../components/webostv/translations/es.json | 2 +- .../components/withings/translations/es.json | 2 +- .../components/wiz/translations/es.json | 2 +- .../xiaomi_aqara/translations/es.json | 6 +- .../xiaomi_ble/translations/ca.json | 6 ++ .../xiaomi_miio/translations/es.json | 10 +- .../yalexs_ble/translations/ca.json | 8 +- .../yalexs_ble/translations/el.json | 11 ++- .../yalexs_ble/translations/es.json | 2 +- .../yalexs_ble/translations/et.json | 3 +- .../yalexs_ble/translations/hu.json | 1 + .../yalexs_ble/translations/ja.json | 28 ++++++ .../yalexs_ble/translations/no.json | 3 +- .../yalexs_ble/translations/ru.json | 31 +++++++ .../yalexs_ble/translations/zh-Hant.json | 3 +- .../components/yeelight/translations/es.json | 4 +- .../components/yolink/translations/es.json | 2 +- .../components/zwave_js/translations/es.json | 10 +- 193 files changed, 1048 insertions(+), 302 deletions(-) create mode 100644 homeassistant/components/android_ip_webcam/translations/fi.json create mode 100644 homeassistant/components/android_ip_webcam/translations/ja.json create mode 100644 homeassistant/components/android_ip_webcam/translations/ru.json create mode 100644 homeassistant/components/awair/translations/fi.json create mode 100644 homeassistant/components/justnimbus/translations/ja.json create mode 100644 homeassistant/components/qingping/translations/ca.json create mode 100644 homeassistant/components/qingping/translations/de.json create mode 100644 homeassistant/components/qingping/translations/el.json create mode 100644 homeassistant/components/qingping/translations/es.json create mode 100644 homeassistant/components/qingping/translations/et.json create mode 100644 homeassistant/components/qingping/translations/fi.json create mode 100644 homeassistant/components/qingping/translations/fr.json create mode 100644 homeassistant/components/qingping/translations/hu.json create mode 100644 homeassistant/components/qingping/translations/id.json create mode 100644 homeassistant/components/qingping/translations/it.json create mode 100644 homeassistant/components/qingping/translations/ja.json create mode 100644 homeassistant/components/qingping/translations/no.json create mode 100644 homeassistant/components/qingping/translations/pt-BR.json create mode 100644 homeassistant/components/qingping/translations/ru.json create mode 100644 homeassistant/components/qingping/translations/zh-Hant.json create mode 100644 homeassistant/components/schedule/translations/fi.json create mode 100644 homeassistant/components/schedule/translations/ja.json create mode 100644 homeassistant/components/schedule/translations/no.json create mode 100644 homeassistant/components/schedule/translations/ru.json create mode 100644 homeassistant/components/switchbot/translations/fi.json create mode 100644 homeassistant/components/yalexs_ble/translations/ja.json create mode 100644 homeassistant/components/yalexs_ble/translations/ru.json diff --git a/homeassistant/components/acmeda/translations/es.json b/homeassistant/components/acmeda/translations/es.json index 6e336c0315b..0eb22f132bd 100644 --- a/homeassistant/components/acmeda/translations/es.json +++ b/homeassistant/components/acmeda/translations/es.json @@ -8,7 +8,7 @@ "data": { "id": "ID de host" }, - "title": "Elige un hub para a\u00f1adir" + "title": "Elige un concentrador para a\u00f1adir" } } } diff --git a/homeassistant/components/airtouch4/translations/es.json b/homeassistant/components/airtouch4/translations/es.json index 65616d2a2e9..dd7f1044b20 100644 --- a/homeassistant/components/airtouch4/translations/es.json +++ b/homeassistant/components/airtouch4/translations/es.json @@ -12,7 +12,7 @@ "data": { "host": "Host" }, - "title": "Configura los detalles de conexi\u00f3n de tu AirTouch 4." + "title": "Configurar los detalles de conexi\u00f3n de tu AirTouch 4." } } } diff --git a/homeassistant/components/airvisual/translations/es.json b/homeassistant/components/airvisual/translations/es.json index 739aaa818ed..acffe47f3ca 100644 --- a/homeassistant/components/airvisual/translations/es.json +++ b/homeassistant/components/airvisual/translations/es.json @@ -17,7 +17,7 @@ "latitude": "Latitud", "longitude": "Longitud" }, - "description": "Utiliza la API de nube de AirVisual para supervisar una latitud/longitud.", + "description": "Usar la API de la nube de AirVisual para supervisar una latitud/longitud.", "title": "Configurar una geograf\u00eda" }, "geography_by_name": { @@ -27,7 +27,7 @@ "country": "Pa\u00eds", "state": "estado" }, - "description": "Utiliza la API en la nube de AirVisual para supervisar una ciudad/estado/pa\u00eds.", + "description": "Usar la API de la nube de AirVisual para supervisar una ciudad/estado/pa\u00eds.", "title": "Configurar una geograf\u00eda" }, "node_pro": { diff --git a/homeassistant/components/almond/translations/es.json b/homeassistant/components/almond/translations/es.json index 1a5b3ddf074..7c0a80ef444 100644 --- a/homeassistant/components/almond/translations/es.json +++ b/homeassistant/components/almond/translations/es.json @@ -3,7 +3,7 @@ "abort": { "cannot_connect": "No se pudo conectar", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "step": { diff --git a/homeassistant/components/android_ip_webcam/translations/fi.json b/homeassistant/components/android_ip_webcam/translations/fi.json new file mode 100644 index 00000000000..61febe9dd9c --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/fi.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Salasana", + "username": "K\u00e4ytt\u00e4j\u00e4tunnus" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/ja.json b/homeassistant/components/android_ip_webcam/translations/ja.json new file mode 100644 index 00000000000..832d6b9c71c --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/ja.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "port": "\u30dd\u30fc\u30c8", + "username": "\u30e6\u30fc\u30b6\u30fc\u540d" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "Android IP Webcam YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/ru.json b/homeassistant/components/android_ip_webcam/translations/ru.json new file mode 100644 index 00000000000..deeac93856b --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Android IP Webcam \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Android IP Webcam \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/translations/es.json b/homeassistant/components/androidtv/translations/es.json index b41827566ab..bc6f9f6996f 100644 --- a/homeassistant/components/androidtv/translations/es.json +++ b/homeassistant/components/androidtv/translations/es.json @@ -14,7 +14,7 @@ "step": { "user": { "data": { - "adb_server_ip": "Direcci\u00f3n IP del servidor ADB (d\u00e9jalo vac\u00edo para no utilizarlo)", + "adb_server_ip": "Direcci\u00f3n IP del servidor ADB (d\u00e9jalo vac\u00edo para no usarlo)", "adb_server_port": "Puerto del servidor ADB", "adbkey": "Ruta a tu archivo de clave ADB (d\u00e9jalo en blanco para generarlo autom\u00e1ticamente)", "device_class": "Tipo de dispositivo", diff --git a/homeassistant/components/apple_tv/translations/es.json b/homeassistant/components/apple_tv/translations/es.json index d1654272818..3881692d0be 100644 --- a/homeassistant/components/apple_tv/translations/es.json +++ b/homeassistant/components/apple_tv/translations/es.json @@ -3,10 +3,10 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "backoff": "El dispositivo no acepta solicitudes de emparejamiento en este momento (es posible que hayas introducido un c\u00f3digo PIN no v\u00e1lido demasiadas veces), intenta de nuevo m\u00e1s tarde.", + "backoff": "El dispositivo no acepta solicitudes de emparejamiento en este momento (es posible que hayas introducido un c\u00f3digo PIN no v\u00e1lido demasiadas veces), vuelve a intentarlo m\u00e1s tarde.", "device_did_not_pair": "No se ha intentado finalizar el proceso de emparejamiento desde el dispositivo.", - "device_not_found": "No se encontr\u00f3 el dispositivo durante el descubrimiento, por favor intenta a\u00f1adirlo nuevamente.", - "inconsistent_device": "No se encontraron los protocolos esperados durante el descubrimiento. Esto normalmente indica un problema con multicast DNS (Zeroconf). Por favor, intenta a\u00f1adir el dispositivo nuevamente.", + "device_not_found": "No se encontr\u00f3 el dispositivo durante el descubrimiento, por favor, intenta a\u00f1adi\u00e9ndolo de nuevo.", + "inconsistent_device": "No se encontraron los protocolos esperados durante el descubrimiento. Esto normalmente indica un problema con multicast DNS (Zeroconf). Por favor, intenta a\u00f1adir el dispositivo de nuevo.", "ipv6_not_supported": "IPv6 no es compatible.", "no_devices_found": "No se encontraron dispositivos en la red", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", @@ -22,8 +22,8 @@ "flow_title": "{name} ({type})", "step": { "confirm": { - "description": "Est\u00e1s a punto de a\u00f1adir `{name}` con el tipo `{type}` en Home Assistant.\n\n**Para completar el proceso, puede que tengas que introducir varios c\u00f3digos PIN.**\n\nTen en cuenta que *no* podr\u00e1s apagar tu Apple TV con esta integraci\u00f3n. \u00a1S\u00f3lo se apagar\u00e1 el reproductor de medios en Home Assistant!", - "title": "Confirma para a\u00f1adir Apple TV" + "description": "Est\u00e1s a punto de a\u00f1adir `{name}` con el tipo `{type}` en Home Assistant.\n\n**Para completar el proceso, puede que tengas que introducir varios c\u00f3digos PIN.**\n\nPor favor, ten en cuenta que *no* podr\u00e1s apagar tu Apple TV con esta integraci\u00f3n. \u00a1S\u00f3lo se apagar\u00e1 el reproductor de medios en Home Assistant!", + "title": "Confirmar la adici\u00f3n de Apple TV" }, "pair_no_pin": { "description": "Se requiere emparejamiento para el servicio `{protocol}`. Por favor, introduce el PIN {pin} en tu dispositivo para continuar.", @@ -33,15 +33,15 @@ "data": { "pin": "C\u00f3digo PIN" }, - "description": "El emparejamiento es necesario para el protocolo `{protocol}`. Introduce el c\u00f3digo PIN que aparece en la pantalla. Los ceros iniciales deben ser omitidos, es decir, introduce 123 si el c\u00f3digo mostrado es 0123.", + "description": "El emparejamiento es necesario para el protocolo `{protocol}`. Por favor, introduce el c\u00f3digo PIN que aparece en la pantalla. Los ceros iniciales deben ser omitidos, es decir, introduce 123 si el c\u00f3digo mostrado es 0123.", "title": "Emparejamiento" }, "password": { - "description": "Se requiere una contrase\u00f1a por `{protocol}`. Esto a\u00fan no es compatible, por favor deshabilita la contrase\u00f1a para continuar.", + "description": "Se requiere una contrase\u00f1a por `{protocol}`. Esto a\u00fan no es compatible, por favor, deshabilita la contrase\u00f1a para continuar.", "title": "Se requiere contrase\u00f1a" }, "protocol_disabled": { - "description": "Se requiere emparejamiento para `{protocol}` pero est\u00e1 deshabilitado en el dispositivo. Revisa las posibles restricciones de acceso (p. ej., permitir que todos los dispositivos de la red local se conecten) en el dispositivo. \n\nPuedes continuar sin emparejar este protocolo, pero algunas funciones estar\u00e1n limitadas.", + "description": "Se requiere emparejamiento para `{protocol}` pero est\u00e1 deshabilitado en el dispositivo. Por favor, revisa las posibles restricciones de acceso (p. ej., permitir que todos los dispositivos de la red local se conecten) en el dispositivo. \n\nPuedes continuar sin emparejar este protocolo, pero algunas funciones estar\u00e1n limitadas.", "title": "No es posible el emparejamiento" }, "reconfigure": { diff --git a/homeassistant/components/asuswrt/translations/es.json b/homeassistant/components/asuswrt/translations/es.json index 57e7bb4cde6..3b6871093c4 100644 --- a/homeassistant/components/asuswrt/translations/es.json +++ b/homeassistant/components/asuswrt/translations/es.json @@ -20,7 +20,7 @@ "name": "Nombre", "password": "Contrase\u00f1a", "port": "Puerto (dejar vac\u00edo para el predeterminado del protocolo)", - "protocol": "Protocolo de comunicaci\u00f3n a utilizar", + "protocol": "Protocolo de comunicaci\u00f3n a usar", "ssh_key": "Ruta a tu archivo de clave SSH (en lugar de contrase\u00f1a)", "username": "Nombre de usuario" }, @@ -37,7 +37,7 @@ "dnsmasq": "La ubicaci\u00f3n en el router de los archivos dnsmasq.leases", "interface": "La interfaz de la que quieres estad\u00edsticas (por ejemplo, eth0, eth1, etc.)", "require_ip": "Los dispositivos deben tener IP (para el modo de punto de acceso)", - "track_unknown": "Seguimiento de dispositivos desconocidos/sin nombre" + "track_unknown": "Rastrear dispositivos desconocidos/sin nombre" }, "title": "Opciones de AsusWRT" } diff --git a/homeassistant/components/atag/translations/es.json b/homeassistant/components/atag/translations/es.json index c1bc879f64d..c65d1b536c1 100644 --- a/homeassistant/components/atag/translations/es.json +++ b/homeassistant/components/atag/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "unauthorized": "Emparejamiento denegado, verifica el dispositivo para la solicitud de autenticaci\u00f3n" + "unauthorized": "Emparejamiento denegado, comprueba el dispositivo para la solicitud de autenticaci\u00f3n" }, "step": { "user": { diff --git a/homeassistant/components/aurora_abb_powerone/translations/es.json b/homeassistant/components/aurora_abb_powerone/translations/es.json index 537ee81a30e..e14da3c18f7 100644 --- a/homeassistant/components/aurora_abb_powerone/translations/es.json +++ b/homeassistant/components/aurora_abb_powerone/translations/es.json @@ -5,8 +5,8 @@ "no_serial_ports": "No se encontraron puertos de comunicaciones. Necesitas un dispositivo RS485 v\u00e1lido para comunicarse." }, "error": { - "cannot_connect": "No se puede conectar, por favor, verifica el puerto serie, la direcci\u00f3n, la conexi\u00f3n el\u00e9ctrica y que el inversor est\u00e9 encendido (durante la luz del d\u00eda)", - "cannot_open_serial_port": "No se puede abrir el puerto serie, por favor, verif\u00edcalo e int\u00e9ntalo de nuevo", + "cannot_connect": "No se puede conectar, por favor, comprueba el puerto serie, la direcci\u00f3n, la conexi\u00f3n el\u00e9ctrica y que el inversor est\u00e9 encendido (durante la luz del d\u00eda)", + "cannot_open_serial_port": "No se puede abrir el puerto serie, por favor, compru\u00e9balo e int\u00e9ntalo de nuevo", "invalid_serial_port": "El puerto serie no es un dispositivo v\u00e1lido o no se pudo abrir" }, "step": { @@ -15,7 +15,7 @@ "address": "Direcci\u00f3n del inversor", "port": "Puerto adaptador RS485 o USB-RS485" }, - "description": "El inversor debe estar conectado a trav\u00e9s de un adaptador RS485, por favor, selecciona el puerto serie y la direcci\u00f3n del inversor seg\u00fan lo configurado en el panel LCD" + "description": "El inversor debe estar conectado a trav\u00e9s de un adaptador RS485. Por favor, selecciona el puerto serie y la direcci\u00f3n del inversor seg\u00fan lo configurado en el panel LCD" } } } diff --git a/homeassistant/components/auth/translations/es.json b/homeassistant/components/auth/translations/es.json index 85f412f0814..f0c70702847 100644 --- a/homeassistant/components/auth/translations/es.json +++ b/homeassistant/components/auth/translations/es.json @@ -5,11 +5,11 @@ "no_available_service": "No hay servicios de notificaci\u00f3n disponibles." }, "error": { - "invalid_code": "C\u00f3digo no v\u00e1lido, por favor, vuelve a intentarlo." + "invalid_code": "C\u00f3digo no v\u00e1lido, por favor, int\u00e9ntalo de nuevo." }, "step": { "init": { - "description": "Selecciona uno de los servicios de notificaci\u00f3n:", + "description": "Por favor, selecciona uno de los servicios de notificaci\u00f3n:", "title": "Configurar una contrase\u00f1a de un solo uso entregada por el componente de notificaci\u00f3n" }, "setup": { @@ -21,7 +21,7 @@ }, "totp": { "error": { - "invalid_code": "C\u00f3digo no v\u00e1lido, por favor, vuelve a intentarlo. Si recibes este error constantemente, aseg\u00farate de que el reloj de tu sistema Home Assistant sea exacto." + "invalid_code": "C\u00f3digo no v\u00e1lido, por favor, int\u00e9ntalo de nuevo. Si recibes este error constantemente, aseg\u00farate de que el reloj de tu sistema Home Assistant sea exacto." }, "step": { "init": { diff --git a/homeassistant/components/awair/translations/ca.json b/homeassistant/components/awair/translations/ca.json index eaf255a0532..b5c16826078 100644 --- a/homeassistant/components/awair/translations/ca.json +++ b/homeassistant/components/awair/translations/ca.json @@ -19,7 +19,8 @@ "data": { "access_token": "Token d'acc\u00e9s", "email": "Correu electr\u00f2nic" - } + }, + "description": "T'has de registrar a Awair per a obtenir un token d'acc\u00e9s de desenvolupador a: {url}" }, "discovery_confirm": { "description": "Vols configurar {model} ({device_id})?" @@ -27,7 +28,8 @@ "local": { "data": { "host": "Adre\u00e7a IP" - } + }, + "description": "L'API local d'Awair s'ha d'activar seguint aquests passos: {url}" }, "reauth": { "data": { @@ -48,8 +50,9 @@ "access_token": "Token d'acc\u00e9s", "email": "Correu electr\u00f2nic" }, - "description": "T'has de registrar a Awair per a obtenir un token d'acc\u00e9s de desenvolupador a trav\u00e9s de l'enlla\u00e7 seg\u00fcent: https://developer.getawair.com/onboard/login", + "description": "Tria local per a la millor experi\u00e8ncia. Utilitza 'al n\u00favol' si el teu dispositiu no est\u00e0 connectat a la mateixa xarxa que Home Assistant, o si tens un dispositiu antic.", "menu_options": { + "cloud": "Connecta't a trav\u00e9s del n\u00favol", "local": "Connecta't localment (preferit)" } } diff --git a/homeassistant/components/awair/translations/el.json b/homeassistant/components/awair/translations/el.json index 4cd1ac93d0a..75447f29c15 100644 --- a/homeassistant/components/awair/translations/el.json +++ b/homeassistant/components/awair/translations/el.json @@ -2,22 +2,33 @@ "config": { "abort": { "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_configured_account": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_configured_device": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", - "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "unreachable": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "error": { "invalid_access_token": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", - "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "unreachable": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, "flow_title": "{model} ({device_id})", "step": { "cloud": { + "data": { + "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "email": "Email" + }, "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03b5\u03af\u03c4\u03b5 \u03b3\u03b9\u03b1 \u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03c4\u03ae Awair \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7: {url}" }, "discovery_confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {model} ({device_id});" }, "local": { + "data": { + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" + }, "description": "\u03a4\u03bf Awair Local API \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ce\u03bd\u03c4\u03b1\u03c2 \u03b1\u03c5\u03c4\u03ac \u03c4\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1: {url}" }, "reauth": { diff --git a/homeassistant/components/awair/translations/fi.json b/homeassistant/components/awair/translations/fi.json new file mode 100644 index 00000000000..edbdcb1b086 --- /dev/null +++ b/homeassistant/components/awair/translations/fi.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "cloud": { + "data": { + "email": "S\u00e4hk\u00f6posti" + } + }, + "local": { + "data": { + "host": "IP Osoite" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/ja.json b/homeassistant/components/awair/translations/ja.json index 7c7b73b312f..6599af1bd14 100644 --- a/homeassistant/components/awair/translations/ja.json +++ b/homeassistant/components/awair/translations/ja.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_configured_account": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_configured_device": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", - "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", + "unreachable": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "error": { "invalid_access_token": "\u7121\u52b9\u306a\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", - "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc", + "unreachable": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", + "email": "E\u30e1\u30fc\u30eb" + }, + "description": "Awair\u958b\u767a\u8005\u30a2\u30af\u30bb\u30b9 \u30c8\u30fc\u30af\u30f3\u3092\u767b\u9332\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {url}" + }, + "discovery_confirm": { + "description": "{model} ({device_id}) \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "local": { + "data": { + "host": "IP\u30a2\u30c9\u30ec\u30b9" + }, + "description": "\u6b21\u306e\u624b\u9806\u306b\u5f93\u3063\u3066\u3001Awair Local API\u3092\u6709\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {url}" + }, "reauth": { "data": { "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", @@ -29,7 +50,11 @@ "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", "email": "E\u30e1\u30fc\u30eb" }, - "description": "Awair developer access token\u306e\u767b\u9332\u306f\u4ee5\u4e0b\u306e\u30b5\u30a4\u30c8\u3067\u884c\u3046\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: https://developer.getawair.com/onboard/login" + "description": "Awair developer access token\u306e\u767b\u9332\u306f\u4ee5\u4e0b\u306e\u30b5\u30a4\u30c8\u3067\u884c\u3046\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: https://developer.getawair.com/onboard/login", + "menu_options": { + "cloud": "\u30af\u30e9\u30a6\u30c9\u7d4c\u7531\u3067\u63a5\u7d9a", + "local": "\u30ed\u30fc\u30ab\u30eb\u306b\u63a5\u7d9a(\u63a8\u5968)" + } } } } diff --git a/homeassistant/components/awair/translations/no.json b/homeassistant/components/awair/translations/no.json index 13232ca37df..9ffbf544909 100644 --- a/homeassistant/components/awair/translations/no.json +++ b/homeassistant/components/awair/translations/no.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", + "already_configured_account": "Kontoen er allerede konfigurert", + "already_configured_device": "Enheten er allerede konfigurert", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", - "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", + "unreachable": "Tilkobling mislyktes" }, "error": { "invalid_access_token": "Ugyldig tilgangstoken", - "unknown": "Uventet feil" + "unknown": "Uventet feil", + "unreachable": "Tilkobling mislyktes" }, + "flow_title": "{model} ( {device_id} )", "step": { + "cloud": { + "data": { + "access_token": "Tilgangstoken", + "email": "E-post" + }, + "description": "Du m\u00e5 registrere deg for et Awair-utviklertilgangstoken p\u00e5: {url}" + }, + "discovery_confirm": { + "description": "Vil du konfigurere {model} ( {device_id} )?" + }, + "local": { + "data": { + "host": "IP adresse" + }, + "description": "Awair Local API m\u00e5 aktiveres ved \u00e5 f\u00f8lge disse trinnene: {url}" + }, "reauth": { "data": { "access_token": "Tilgangstoken", @@ -29,7 +50,11 @@ "access_token": "Tilgangstoken", "email": "E-post" }, - "description": "Du m\u00e5 registrere deg for et Awair-utviklertilgangstoken p\u00e5: https://developer.getawair.com/onboard/login" + "description": "Velg lokal for den beste opplevelsen. Bruk bare sky hvis enheten ikke er koblet til samme nettverk som Home Assistant, eller hvis du har en eldre enhet.", + "menu_options": { + "cloud": "Koble til via skyen", + "local": "Koble til lokalt (foretrukket)" + } } } } diff --git a/homeassistant/components/awair/translations/ru.json b/homeassistant/components/awair/translations/ru.json index 23424091565..c0be2df4b0a 100644 --- a/homeassistant/components/awair/translations/ru.json +++ b/homeassistant/components/awair/translations/ru.json @@ -2,14 +2,35 @@ "config": { "abort": { "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "already_configured_account": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "already_configured_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", + "unreachable": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "error": { "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", + "unreachable": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "description": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a Awair \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443: {url}" + }, + "discovery_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 {model} ({device_id})?" + }, + "local": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u0427\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 API Awair, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0437\u0434\u0435\u0441\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f: {url}" + }, "reauth": { "data": { "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", @@ -29,7 +50,11 @@ "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" }, - "description": "\u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a Awair \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443: https://developer.getawair.com/onboard/login" + "description": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043e\u0431\u043b\u0430\u043a\u043e, \u0435\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0435\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0438\u043b\u0438 \u0435\u0441\u043b\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a \u0442\u043e\u0439 \u0436\u0435 \u0441\u0435\u0442\u0438, \u0447\u0442\u043e \u0438 Home Assistant. \u0412 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0445 \u0441\u043b\u0443\u0447\u0430\u044f\u0445 \u043e\u0442\u0434\u0430\u0432\u0430\u0439\u0442\u0435 \u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0442\u0435\u043d\u0438\u0435 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u043c\u0443 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044e.", + "menu_options": { + "cloud": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u0447\u0435\u0440\u0435\u0437 \u043e\u0431\u043b\u0430\u043a\u043e", + "local": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e (\u043f\u0440\u0435\u0434\u043f\u043e\u0447\u0442\u0438\u0442\u0435\u043b\u044c\u043d\u043e)" + } } } } diff --git a/homeassistant/components/bluetooth/translations/es.json b/homeassistant/components/bluetooth/translations/es.json index ec970720228..1dc8669a067 100644 --- a/homeassistant/components/bluetooth/translations/es.json +++ b/homeassistant/components/bluetooth/translations/es.json @@ -24,7 +24,7 @@ "step": { "init": { "data": { - "adapter": "El adaptador Bluetooth que se utilizar\u00e1 para escanear" + "adapter": "El adaptador Bluetooth que se usar\u00e1 para escanear" } } } diff --git a/homeassistant/components/bosch_shc/translations/es.json b/homeassistant/components/bosch_shc/translations/es.json index 7a019e277a4..b2934bca747 100644 --- a/homeassistant/components/bosch_shc/translations/es.json +++ b/homeassistant/components/bosch_shc/translations/es.json @@ -7,14 +7,14 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "pairing_failed": "Emparejamiento fallido; Verifica que el Smart Home Controller de Bosch est\u00e9 en modo de emparejamiento (el LED parpadea) y que tu contrase\u00f1a sea correcta.", + "pairing_failed": "Emparejamiento fallido; por favor, comprueba que el Smart Home Controller de Bosch est\u00e9 en modo de emparejamiento (el LED parpadea) y que tu contrase\u00f1a sea correcta.", "session_error": "Error de sesi\u00f3n: la API devuelve un resultado No-OK.", "unknown": "Error inesperado" }, "flow_title": "Bosch SHC: {name}", "step": { "confirm_discovery": { - "description": "Pulsa el bot\u00f3n frontal del Smart Home Controller de Bosch hasta que el LED empiece a parpadear.\n\u00bfPreparado para seguir configurando {model} @ {host} con Home Assistant?" + "description": "Por favor, pulsa el bot\u00f3n frontal del Smart Home Controller de Bosch hasta que el LED empiece a parpadear.\n\u00bfPreparado para seguir configurando {model} @ {host} con Home Assistant?" }, "credentials": { "data": { diff --git a/homeassistant/components/broadlink/translations/fr.json b/homeassistant/components/broadlink/translations/fr.json index e39b722d8c9..7c9d22fd52f 100644 --- a/homeassistant/components/broadlink/translations/fr.json +++ b/homeassistant/components/broadlink/translations/fr.json @@ -5,7 +5,7 @@ "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", "cannot_connect": "\u00c9chec de connexion", "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", - "not_supported": "Dispositif non pris en charge", + "not_supported": "Appareil non pris en charge", "unknown": "Erreur inattendue" }, "error": { diff --git a/homeassistant/components/cast/translations/es.json b/homeassistant/components/cast/translations/es.json index 9c0847b52ce..8f6bc774098 100644 --- a/homeassistant/components/cast/translations/es.json +++ b/homeassistant/components/cast/translations/es.json @@ -11,7 +11,7 @@ "data": { "known_hosts": "Hosts conocidos" }, - "description": "Hosts conocidos: una lista separada por comas de nombres de host o direcciones IP de dispositivos de transmisi\u00f3n, se usa si el descubrimiento de mDNS no funciona.", + "description": "Hosts conocidos: una lista separada por comas de nombres de host o direcciones IP de dispositivos de transmisi\u00f3n, se usa si el descubrimiento mDNS no funciona.", "title": "Configuraci\u00f3n de Google Cast" }, "confirm": { @@ -29,7 +29,7 @@ "ignore_cec": "Ignorar CEC", "uuid": "UUIDs permitidos" }, - "description": "UUID permitidos: una lista separada por comas de UUID de dispositivos Cast para a\u00f1adir a Home Assistant. \u00dasalo solo si no deseas a\u00f1adir todos los dispositivos de transmisi\u00f3n disponibles.\nIgnorar CEC: una lista separada por comas de Chromecasts que deben ignorar los datos de CEC para determinar la entrada activa. Esto se pasar\u00e1 a pychromecast.IGNORE_CEC.", + "description": "UUID permitidos: una lista separada por comas de UUIDs de dispositivos Cast para a\u00f1adir a Home Assistant. \u00dasalo solo si no deseas a\u00f1adir todos los dispositivos de transmisi\u00f3n disponibles.\nIgnorar CEC: una lista separada por comas de Chromecasts que deben ignorar los datos de CEC para determinar la entrada activa. Esto se pasar\u00e1 a pychromecast.IGNORE_CEC.", "title": "Configuraci\u00f3n avanzada de Google Cast" }, "basic_options": { diff --git a/homeassistant/components/climate/translations/es.json b/homeassistant/components/climate/translations/es.json index bf7e37c71ff..188adec66c5 100644 --- a/homeassistant/components/climate/translations/es.json +++ b/homeassistant/components/climate/translations/es.json @@ -9,9 +9,9 @@ "is_preset_mode": "{entity_name} se establece en un modo preestablecido espec\u00edfico" }, "trigger_type": { - "current_humidity_changed": "{entity_name} cambi\u00f3 la humedad medida", - "current_temperature_changed": "{entity_name} cambi\u00f3 la temperatura medida", - "hvac_mode_changed": "{entity_name} cambi\u00f3 el modo HVAC" + "current_humidity_changed": "La humedad medida por {entity_name} cambi\u00f3", + "current_temperature_changed": "La temperatura medida por {entity_name} cambi\u00f3", + "hvac_mode_changed": "El modo HVAC de {entity_name} cambi\u00f3" } }, "state": { diff --git a/homeassistant/components/cover/translations/es.json b/homeassistant/components/cover/translations/es.json index 708c4a2dc7d..327a41bc24c 100644 --- a/homeassistant/components/cover/translations/es.json +++ b/homeassistant/components/cover/translations/es.json @@ -22,8 +22,8 @@ "closing": "{entity_name} cerr\u00e1ndose", "opened": "{entity_name} abierto", "opening": "{entity_name} abri\u00e9ndose", - "position": "{entity_name} cambi\u00f3 de posici\u00f3n", - "tilt_position": "{entity_name} cambi\u00f3 de posici\u00f3n de inclinaci\u00f3n" + "position": "La posici\u00f3n de {entity_name} cambia", + "tilt_position": "La posici\u00f3n de inclinaci\u00f3n de {entity_name} cambia" } }, "state": { diff --git a/homeassistant/components/demo/translations/ca.json b/homeassistant/components/demo/translations/ca.json index 19fc5a86e0b..cf6055bfda4 100644 --- a/homeassistant/components/demo/translations/ca.json +++ b/homeassistant/components/demo/translations/ca.json @@ -15,7 +15,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Prem D'acord quan s'hagi omplert el l\u00edquid d'intermitents", + "description": "Prem ENVIA quan s'hagi omplert el l\u00edquid d'intermitents", "title": "Cal omplir el l\u00edquid d'intermitents" } } diff --git a/homeassistant/components/demo/translations/ja.json b/homeassistant/components/demo/translations/ja.json index 97eb4866bfa..bd4d650de1c 100644 --- a/homeassistant/components/demo/translations/ja.json +++ b/homeassistant/components/demo/translations/ja.json @@ -4,10 +4,12 @@ "fix_flow": { "step": { "confirm": { - "description": "SUBMIT(\u9001\u4fe1)\u3092\u62bc\u3057\u3066\u3001\u96fb\u6e90\u304c\u4ea4\u63db\u3055\u308c\u305f\u3053\u3068\u3092\u78ba\u8a8d\u3057\u307e\u3059" + "description": "SUBMIT(\u9001\u4fe1)\u3092\u62bc\u3057\u3066\u3001\u96fb\u6e90\u304c\u4ea4\u63db\u3055\u308c\u305f\u3053\u3068\u3092\u78ba\u8a8d\u3057\u307e\u3059", + "title": "\u96fb\u6e90\u3092\u4ea4\u63db\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059" } } - } + }, + "title": "\u96fb\u6e90\u304c\u4e0d\u5b89\u5b9a" }, "out_of_blinker_fluid": { "fix_flow": { diff --git a/homeassistant/components/deutsche_bahn/translations/ca.json b/homeassistant/components/deutsche_bahn/translations/ca.json index a4dc0724423..406d134c92f 100644 --- a/homeassistant/components/deutsche_bahn/translations/ca.json +++ b/homeassistant/components/deutsche_bahn/translations/ca.json @@ -1,6 +1,7 @@ { "issues": { "pending_removal": { + "description": "La integraci\u00f3 de Deutsche Bahn est\u00e0 pendent d'eliminar-se de Home Assistant i ja no estar\u00e0 disponible a partir de Home Assistant 2022.11. \n\nLa integraci\u00f3 s'est\u00e0 eliminant, perqu\u00e8 es basa en el 'webscraping', que no est\u00e0 adm\u00e8s. \n\nElimina la configuraci\u00f3 YAML de Deutsche Bahn del fitxer configuration.yaml i reinicia Home Assistant per arreglar aquest error.", "title": "La integraci\u00f3 Deutsche Bahn est\u00e0 sent eliminada" } } diff --git a/homeassistant/components/deutsche_bahn/translations/es.json b/homeassistant/components/deutsche_bahn/translations/es.json index 32aaf5a3893..2572474f2bc 100644 --- a/homeassistant/components/deutsche_bahn/translations/es.json +++ b/homeassistant/components/deutsche_bahn/translations/es.json @@ -1,7 +1,7 @@ { "issues": { "pending_removal": { - "description": "La integraci\u00f3n Deutsche Bahn est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.11. \n\nLa integraci\u00f3n se elimina porque se basa en webscraping, que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de Deutsche Bahn de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "La integraci\u00f3n Deutsche Bahn est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.11. \n\nLa integraci\u00f3n se elimina porque se basa en webscraping, algo que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de Deutsche Bahn de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la integraci\u00f3n Deutsche Bahn" } } diff --git a/homeassistant/components/device_tracker/translations/es.json b/homeassistant/components/device_tracker/translations/es.json index 47948da66ec..e68dbcf4087 100644 --- a/homeassistant/components/device_tracker/translations/es.json +++ b/homeassistant/components/device_tracker/translations/es.json @@ -15,5 +15,5 @@ "not_home": "Fuera" } }, - "title": "Seguimiento de dispositivos" + "title": "Rastreador de dispositivos" } \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/es.json b/homeassistant/components/doorbird/translations/es.json index 509b6e7d8c7..538178301cc 100644 --- a/homeassistant/components/doorbird/translations/es.json +++ b/homeassistant/components/doorbird/translations/es.json @@ -29,7 +29,7 @@ "events": "Lista de eventos separados por comas." }, "data_description": { - "events": "A\u00f1ade un nombre de evento separado por comas para cada evento que desees rastrear. Despu\u00e9s de introducirlos aqu\u00ed, usa la aplicaci\u00f3n DoorBird para asignarlos a un evento espec\u00edfico. \n\nEjemplo: alguien_puls\u00f3_el_bot\u00f3n, movimiento" + "events": "A\u00f1ade un nombre de evento separado por comas para cada evento que desees rastrear. Despu\u00e9s de introducirlos aqu\u00ed, usa la aplicaci\u00f3n DoorBird para asignarlos a un evento espec\u00edfico. \n\nEjemplo: somebody_pressed_the_button, motion" } } } diff --git a/homeassistant/components/dsmr/translations/ja.json b/homeassistant/components/dsmr/translations/ja.json index 53c7d5c1050..edabe226727 100644 --- a/homeassistant/components/dsmr/translations/ja.json +++ b/homeassistant/components/dsmr/translations/ja.json @@ -11,6 +11,7 @@ "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { + "other": "\u7a7a", "setup_network": { "data": { "dsmr_version": "DSMR\u30d0\u30fc\u30b8\u30e7\u30f3\u3092\u9078\u629e", diff --git a/homeassistant/components/eafm/translations/es.json b/homeassistant/components/eafm/translations/es.json index 8e38ac23e0a..a846a0bc04c 100644 --- a/homeassistant/components/eafm/translations/es.json +++ b/homeassistant/components/eafm/translations/es.json @@ -10,7 +10,7 @@ "station": "Estaci\u00f3n" }, "description": "Selecciona la estaci\u00f3n que deseas supervisar", - "title": "Seguimiento de una estaci\u00f3n de supervisi\u00f3n de inundaciones" + "title": "Rastrear una estaci\u00f3n de supervisi\u00f3n de inundaciones" } } } diff --git a/homeassistant/components/elkm1/translations/es.json b/homeassistant/components/elkm1/translations/es.json index 1cbae31550f..25d07dcfef8 100644 --- a/homeassistant/components/elkm1/translations/es.json +++ b/homeassistant/components/elkm1/translations/es.json @@ -34,7 +34,7 @@ "temperature_unit": "La unidad de temperatura que utiliza ElkM1.", "username": "Nombre de usuario" }, - "description": "La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n[:puerto]' para 'seguro' y 'no seguro'. Ejemplo: '192.168.1.1'. El puerto es opcional y el valor predeterminado es 2101 para 'no seguro' y 2601 para 'seguro'. Para el protocolo serial, la direcci\u00f3n debe tener el formato 'tty[:baud]'. Ejemplo: '/dev/ttyS1'. El baudio es opcional y el valor predeterminado es 115200.", + "description": "La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n[:puerto]' para 'seguro' y 'no seguro'. Ejemplo: '192.168.1.1'. El puerto es opcional y el valor predeterminado es 2101 para 'no seguro' y 2601 para 'seguro'. Para el protocolo serial, la direcci\u00f3n debe tener el formato 'tty[:baudios]'. Ejemplo: '/dev/ttyS1'. Los baudios son opcionales y el valor predeterminado es 115200.", "title": "Conectar con Control Elk-M1" }, "user": { diff --git a/homeassistant/components/elmax/translations/es.json b/homeassistant/components/elmax/translations/es.json index 8d6bccd03ab..f2ae2d7ef83 100644 --- a/homeassistant/components/elmax/translations/es.json +++ b/homeassistant/components/elmax/translations/es.json @@ -17,7 +17,7 @@ "panel_name": "Nombre del panel", "panel_pin": "C\u00f3digo PIN" }, - "description": "Selecciona qu\u00e9 panel te gustar\u00eda controlar con esta integraci\u00f3n. Ten en cuenta que el panel debe estar ENCENDIDO para poder configurarlo." + "description": "Selecciona qu\u00e9 panel te gustar\u00eda controlar con esta integraci\u00f3n. Por favor, ten en cuenta que el panel debe estar ENCENDIDO para poder configurarlo." }, "user": { "data": { diff --git a/homeassistant/components/escea/translations/ja.json b/homeassistant/components/escea/translations/ja.json index a64c00ee212..73e0c4d503e 100644 --- a/homeassistant/components/escea/translations/ja.json +++ b/homeassistant/components/escea/translations/ja.json @@ -3,6 +3,11 @@ "abort": { "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" + }, + "step": { + "confirm": { + "description": "Escea fireplace\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + } } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/es.json b/homeassistant/components/esphome/translations/es.json index f8e933f7711..87c8dc6ddad 100644 --- a/homeassistant/components/esphome/translations/es.json +++ b/homeassistant/components/esphome/translations/es.json @@ -17,7 +17,7 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "Escribe la contrase\u00f1a que hayas puesto en la configuraci\u00f3n para {name}." + "description": "Por favor, introduce la contrase\u00f1a que hayas puesto en la configuraci\u00f3n para {name}." }, "discovery_confirm": { "description": "\u00bfQuieres a\u00f1adir el nodo de ESPHome `{name}` a Home Assistant?", @@ -33,7 +33,7 @@ "data": { "noise_psk": "Clave de cifrado" }, - "description": "El dispositivo ESPHome {name} habilit\u00f3 el cifrado de transporte o cambi\u00f3 la clave de cifrado. Introduce la clave actualizada." + "description": "El dispositivo ESPHome {name} habilit\u00f3 el cifrado de transporte o cambi\u00f3 la clave de cifrado. Por favor, introduce la clave actualizada." }, "user": { "data": { diff --git a/homeassistant/components/fivem/translations/es.json b/homeassistant/components/fivem/translations/es.json index 3264888e71e..4ec4b4bd295 100644 --- a/homeassistant/components/fivem/translations/es.json +++ b/homeassistant/components/fivem/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { - "cannot_connect": "No se pudo conectar. Por favor, verifica el host y el puerto y vuelve a intentarlo. Tambi\u00e9n aseg\u00farate de estar ejecutando el servidor FiveM m\u00e1s reciente.", + "cannot_connect": "No se pudo conectar. Por favor, comprueba el host y el puerto e int\u00e9ntalo de nuevo. Tambi\u00e9n aseg\u00farate de estar ejecutando el servidor FiveM m\u00e1s reciente.", "invalid_game_name": "La API del juego al que intentas conectarte no es un juego de FiveM.", "unknown_error": "Error inesperado" }, diff --git a/homeassistant/components/flunearyou/translations/ca.json b/homeassistant/components/flunearyou/translations/ca.json index 912f88d8b28..9c4e55f8b54 100644 --- a/homeassistant/components/flunearyou/translations/ca.json +++ b/homeassistant/components/flunearyou/translations/ca.json @@ -22,6 +22,7 @@ "fix_flow": { "step": { "confirm": { + "description": "La font de dades externa que alimenta la integraci\u00f3 Flu Near You ja no est\u00e0 disponible; per tant, la integraci\u00f3 ja no funciona. \n\nPrem ENVIAR per eliminar Flu Near You de Home Assistant.", "title": "Elimina Flu Near You" } } diff --git a/homeassistant/components/flunearyou/translations/es.json b/homeassistant/components/flunearyou/translations/es.json index 3a2c41cc735..a7d9cf89f6e 100644 --- a/homeassistant/components/flunearyou/translations/es.json +++ b/homeassistant/components/flunearyou/translations/es.json @@ -22,7 +22,7 @@ "fix_flow": { "step": { "confirm": { - "description": "La fuente de datos externa que alimenta la integraci\u00f3n Flu Near You ya no est\u00e1 disponible; por lo tanto, la integraci\u00f3n ya no funciona. \n\nPulsar ENVIAR para eliminar Flu Near You de tu instancia Home Assistant.", + "description": "La fuente de datos externa que alimenta la integraci\u00f3n Flu Near You ya no est\u00e1 disponible; por lo tanto, la integraci\u00f3n ya no funciona. \n\nPulsa ENVIAR para eliminar Flu Near You de tu instancia Home Assistant.", "title": "Eliminar Flu Near You" } } diff --git a/homeassistant/components/flunearyou/translations/it.json b/homeassistant/components/flunearyou/translations/it.json index 4b3bd589325..a8939f2d18c 100644 --- a/homeassistant/components/flunearyou/translations/it.json +++ b/homeassistant/components/flunearyou/translations/it.json @@ -22,6 +22,7 @@ "fix_flow": { "step": { "confirm": { + "description": "L'origine dati esterna che alimenta l'integrazione Flu Near You non \u00e8 pi\u00f9 disponibile; quindi, l'integrazione non funziona pi\u00f9. \n\nPremi INVIA per rimuovere Flu Near You dall'istanza di Home Assistant.", "title": "Rimuovi Flu Near You" } } diff --git a/homeassistant/components/forecast_solar/translations/es.json b/homeassistant/components/forecast_solar/translations/es.json index 5567fa63ee2..08e67ec95d1 100644 --- a/homeassistant/components/forecast_solar/translations/es.json +++ b/homeassistant/components/forecast_solar/translations/es.json @@ -10,7 +10,7 @@ "modules power": "Potencia pico total en vatios de tus m\u00f3dulos solares", "name": "Nombre" }, - "description": "Rellena los datos de tus placas solares. Consulta la documentaci\u00f3n si un campo no est\u00e1 claro." + "description": "Rellena los datos de tus placas solares. Por favor, consulta la documentaci\u00f3n si un campo no est\u00e1 claro." } } }, diff --git a/homeassistant/components/forked_daapd/translations/es.json b/homeassistant/components/forked_daapd/translations/es.json index 2e6c986a61e..999ec0846d5 100644 --- a/homeassistant/components/forked_daapd/translations/es.json +++ b/homeassistant/components/forked_daapd/translations/es.json @@ -5,7 +5,7 @@ "not_forked_daapd": "El dispositivo no es un servidor forked-daapd." }, "error": { - "forbidden": "No se puede conectar. Comprueba los permisos de tu forked-daapd.", + "forbidden": "No se puede conectar. Por favor, comprueba los permisos de red de tu forked-daapd.", "unknown_error": "Error inesperado", "websocket_not_enabled": "Websocket del servidor forked-daapd no habilitado.", "wrong_host_or_port": "No se ha podido conectar. Por favor, comprueba host y puerto.", diff --git a/homeassistant/components/fritzbox_callmonitor/translations/es.json b/homeassistant/components/fritzbox_callmonitor/translations/es.json index 8e7dbbc1b8e..d8203394eba 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/es.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/es.json @@ -27,7 +27,7 @@ }, "options": { "error": { - "malformed_prefixes": "Los prefijos tienen un formato incorrecto, por favor, verifica el formato." + "malformed_prefixes": "Los prefijos tienen un formato incorrecto, por favor, comprueba su formato." }, "step": { "init": { diff --git a/homeassistant/components/generic/translations/es.json b/homeassistant/components/generic/translations/es.json index ac5c16c3120..ff3ce5d4a91 100644 --- a/homeassistant/components/generic/translations/es.json +++ b/homeassistant/components/generic/translations/es.json @@ -36,7 +36,7 @@ "data": { "authentication": "Autenticaci\u00f3n", "framerate": "Velocidad de fotogramas (Hz)", - "limit_refetch_to_url_change": "Limitar recuperaci\u00f3n al cambio de URL", + "limit_refetch_to_url_change": "Limitar la reobtenci\u00f3n de cambios de URL", "password": "Contrase\u00f1a", "rtsp_transport": "Protocolo de transporte RTSP", "still_image_url": "URL de la imagen fija (p. ej., http://...)", @@ -78,7 +78,7 @@ "data": { "authentication": "Autenticaci\u00f3n", "framerate": "Velocidad de fotogramas (Hz)", - "limit_refetch_to_url_change": "Limitar recuperaci\u00f3n al cambio de URL", + "limit_refetch_to_url_change": "Limitar la reobtenci\u00f3n de cambios de URL", "password": "Contrase\u00f1a", "rtsp_transport": "Protocolo de transporte RTSP", "still_image_url": "URL de la imagen fija (p. ej., http://...)", diff --git a/homeassistant/components/geocaching/translations/es.json b/homeassistant/components/geocaching/translations/es.json index 14b534271f9..357eeba9000 100644 --- a/homeassistant/components/geocaching/translations/es.json +++ b/homeassistant/components/geocaching/translations/es.json @@ -5,7 +5,7 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, diff --git a/homeassistant/components/github/translations/es.json b/homeassistant/components/github/translations/es.json index bf1dcf64ae4..02006e7f7b3 100644 --- a/homeassistant/components/github/translations/es.json +++ b/homeassistant/components/github/translations/es.json @@ -10,7 +10,7 @@ "step": { "repositories": { "data": { - "repositories": "Selecciona los repositorios a seguir." + "repositories": "Selecciona repositorios para rastrear." }, "title": "Configura los repositorios" } diff --git a/homeassistant/components/google/translations/es.json b/homeassistant/components/google/translations/es.json index be94117d1bd..4b36c5006c0 100644 --- a/homeassistant/components/google/translations/es.json +++ b/homeassistant/components/google/translations/es.json @@ -39,8 +39,8 @@ "title": "Se va a eliminar la configuraci\u00f3n YAML de Google Calendar" }, "removed_track_new_yaml": { - "description": "Has inhabilitado el seguimiento de entidades para Google Calendar en configuration.yaml, que ya no es compatible. Debes cambiar manualmente las opciones del sistema de integraci\u00f3n en la IU para deshabilitar las entidades reci\u00e9n descubiertas en el futuro. Elimina la configuraci\u00f3n track_new de configuration.yaml y reinicia Home Assistant para solucionar este problema.", - "title": "El seguimiento de entidades de Google Calendar ha cambiado" + "description": "Has deshabilitado el rastreo de entidades para Google Calendar en configuration.yaml, que ya no es compatible. Debes cambiar manualmente las opciones de sistema de la integraci\u00f3n en la IU para deshabilitar las entidades reci\u00e9n descubiertas en el futuro. Elimina la configuraci\u00f3n track_new de configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "El rastreo de entidades de Google Calendar ha cambiado" } }, "options": { diff --git a/homeassistant/components/group/translations/es.json b/homeassistant/components/group/translations/es.json index c5fe0ede852..d80fea1008d 100644 --- a/homeassistant/components/group/translations/es.json +++ b/homeassistant/components/group/translations/es.json @@ -66,9 +66,9 @@ "cover": "Grupo de persianas/cortinas", "fan": "Grupo de ventiladores", "light": "Grupo de luces", - "lock": "Bloquear el grupo", + "lock": "Grupo de cerraduras", "media_player": "Grupo de reproductores multimedia", - "switch": "Grupo de conmutadores" + "switch": "Grupo de interruptores" }, "title": "A\u00f1adir grupo" } diff --git a/homeassistant/components/guardian/translations/ca.json b/homeassistant/components/guardian/translations/ca.json index 59d9f6d03b8..a338db67446 100644 --- a/homeassistant/components/guardian/translations/ca.json +++ b/homeassistant/components/guardian/translations/ca.json @@ -17,5 +17,18 @@ "description": "Configura un dispositiu Elexa Guardian local." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei perqu\u00e8 passin a utilitzar el servei `{alternate_service}` amb un ID d'entitat objectiu o 'target' `{alternate_target}`. Despr\u00e9s, fes clic a ENVIAR per marcar aquest problema com a resolt.", + "title": "El servei {deprecated_service} est\u00e0 sent eliminat" + } + } + }, + "title": "El servei {deprecated_service} est\u00e0 sent eliminat" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/es.json b/homeassistant/components/guardian/translations/es.json index a7f54c0a726..8df17ed3ff3 100644 --- a/homeassistant/components/guardian/translations/es.json +++ b/homeassistant/components/guardian/translations/es.json @@ -23,7 +23,7 @@ "fix_flow": { "step": { "confirm": { - "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con un ID de entidad de destino de `{alternate_target}`. A continuaci\u00f3n, haz clic en ENVIAR m\u00e1s abajo para marcar este problema como resuelto.", + "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con un ID de entidad de destino de `{alternate_target}`. Luego, haz clic en ENVIAR a continuaci\u00f3n para marcar este problema como resuelto.", "title": "El servicio {deprecated_service} ser\u00e1 eliminado" } } diff --git a/homeassistant/components/guardian/translations/ja.json b/homeassistant/components/guardian/translations/ja.json index 337223c5736..7e95869e071 100644 --- a/homeassistant/components/guardian/translations/ja.json +++ b/homeassistant/components/guardian/translations/ja.json @@ -17,5 +17,17 @@ "description": "\u30ed\u30fc\u30ab\u30eb\u306eElexa Guardian\u30c7\u30d0\u30a4\u30b9\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "title": "{deprecated_service} \u30b5\u30fc\u30d3\u30b9\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + } + }, + "title": "{deprecated_service} \u30b5\u30fc\u30d3\u30b9\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/ru.json b/homeassistant/components/guardian/translations/ru.json index fe93ccb6369..068787b5e86 100644 --- a/homeassistant/components/guardian/translations/ru.json +++ b/homeassistant/components/guardian/translations/ru.json @@ -17,5 +17,18 @@ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Elexa Guardian." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0412\u043c\u0435\u0441\u0442\u043e \u044d\u0442\u043e\u0439 \u0441\u043b\u0443\u0436\u0431\u044b \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0443\u0436\u0431\u0443 `{alternate_service}` \u0441 \u0446\u0435\u043b\u0435\u0432\u044b\u043c \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u043c `{alternate_target}`. \u041e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u044b \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c, \u0447\u0442\u043e\u0431\u044b \u043e\u0442\u043c\u0435\u0442\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443 \u043a\u0430\u043a \u0443\u0441\u0442\u0440\u0430\u043d\u0451\u043d\u043d\u0443\u044e.", + "title": "\u0421\u043b\u0443\u0436\u0431\u0430 {deprecated_service} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } + }, + "title": "\u0421\u043b\u0443\u0436\u0431\u0430 {deprecated_service} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/es.json b/homeassistant/components/habitica/translations/es.json index 681336e553f..55cf8eb7642 100644 --- a/homeassistant/components/habitica/translations/es.json +++ b/homeassistant/components/habitica/translations/es.json @@ -12,7 +12,7 @@ "name": "Anular el nombre de usuario de Habitica. Se utilizar\u00e1 para llamadas de servicio.", "url": "URL" }, - "description": "Conecta tu perfil de Habitica para permitir el seguimiento del perfil y las tareas de tu usuario. Ten en cuenta que api_id y api_key deben obtenerse de https://habitica.com/user/settings/api" + "description": "Conecta tu perfil de Habitica para permitir la supervisi\u00f3n del perfil y las tareas de tu usuario. Ten en cuenta que api_id y api_key deben obtenerse de https://habitica.com/user/settings/api" } } } diff --git a/homeassistant/components/hassio/translations/es.json b/homeassistant/components/hassio/translations/es.json index 2d88b0d2252..102256ef117 100644 --- a/homeassistant/components/hassio/translations/es.json +++ b/homeassistant/components/hassio/translations/es.json @@ -13,7 +13,7 @@ "supervisor_version": "Versi\u00f3n del Supervisor", "supported": "Soportado", "update_channel": "Canal de actualizaci\u00f3n", - "version_api": "Versi\u00f3n del API" + "version_api": "Versi\u00f3n de la API" } } } \ No newline at end of file diff --git a/homeassistant/components/hive/translations/es.json b/homeassistant/components/hive/translations/es.json index ffba8558cac..0bece2f0e54 100644 --- a/homeassistant/components/hive/translations/es.json +++ b/homeassistant/components/hive/translations/es.json @@ -7,7 +7,7 @@ }, "error": { "invalid_code": "Error al iniciar sesi\u00f3n en Hive. Tu c\u00f3digo de autenticaci\u00f3n de dos factores era incorrecto.", - "invalid_password": "Error al iniciar sesi\u00f3n en Hive. Contrase\u00f1a incorrecta, por favor, prueba de nuevo.", + "invalid_password": "Error al iniciar sesi\u00f3n en Hive. Contrase\u00f1a incorrecta, por favor, int\u00e9ntalo de nuevo.", "invalid_username": "Error al iniciar sesi\u00f3n en Hive. No se reconoce tu direcci\u00f3n de correo electr\u00f3nico.", "no_internet_available": "Se requiere una conexi\u00f3n a Internet para conectarse a Hive.", "unknown": "Error inesperado" diff --git a/homeassistant/components/home_connect/translations/es.json b/homeassistant/components/home_connect/translations/es.json index a476741a8b1..70d052ee0e3 100644 --- a/homeassistant/components/home_connect/translations/es.json +++ b/homeassistant/components/home_connect/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})" }, "create_entry": { "default": "Autenticado correctamente" diff --git a/homeassistant/components/home_plus_control/translations/es.json b/homeassistant/components/home_plus_control/translations/es.json index 4796a9e1236..609a989aa84 100644 --- a/homeassistant/components/home_plus_control/translations/es.json +++ b/homeassistant/components/home_plus_control/translations/es.json @@ -5,7 +5,7 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "create_entry": { diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index 3bca7c0f253..f0b6a1ecb6d 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -37,7 +37,7 @@ "camera_audio": "C\u00e1maras que admiten audio", "camera_copy": "C\u00e1maras compatibles con transmisiones H.264 nativas" }, - "description": "Verifica todas las c\u00e1maras que admitan transmisiones H.264 nativas. Si la c\u00e1mara no emite una transmisi\u00f3n H.264, el sistema transcodificar\u00e1 el video a H.264 para HomeKit. La transcodificaci\u00f3n requiere una CPU de alto rendimiento y es poco probable que funcione en ordenadores de placa \u00fanica.", + "description": "Marca todas las c\u00e1maras que admitan transmisiones H.264 nativas. Si la c\u00e1mara no emite una transmisi\u00f3n H.264, el sistema transcodificar\u00e1 el video a H.264 para HomeKit. La transcodificaci\u00f3n requiere una CPU de alto rendimiento y es poco probable que funcione en ordenadores de placa \u00fanica.", "title": "Configuraci\u00f3n de C\u00e1mara" }, "exclude": { diff --git a/homeassistant/components/homekit_controller/translations/es.json b/homeassistant/components/homekit_controller/translations/es.json index 6185c432007..a43373cd399 100644 --- a/homeassistant/components/homekit_controller/translations/es.json +++ b/homeassistant/components/homekit_controller/translations/es.json @@ -11,10 +11,10 @@ "no_devices": "No se encontraron dispositivos no emparejados" }, "error": { - "authentication_error": "C\u00f3digo de HomeKit incorrecto. Por favor, rev\u00edsalo e int\u00e9ntalo de nuevo.", + "authentication_error": "C\u00f3digo de HomeKit incorrecto. Por favor, compru\u00e9balo e int\u00e9ntalo de nuevo.", "insecure_setup_code": "El c\u00f3digo de configuraci\u00f3n solicitado no es seguro debido a su naturaleza trivial. Este accesorio no cumple con los requisitos b\u00e1sicos de seguridad.", "max_peers_error": "El dispositivo se neg\u00f3 a a\u00f1adir el emparejamiento porque no tiene almacenamiento disponible para emparejamientos.", - "pairing_failed": "Se produjo un error no controlado al intentar emparejar con este dispositivo. Esto puede ser un fallo temporal o que tu dispositivo no sea compatible actualmente.", + "pairing_failed": "Se produjo un error no controlado al intentar emparejar con este dispositivo. Esto puede ser un fallo temporal o que tu dispositivo no sea compatible por el momento.", "unable_to_pair": "No se puede emparejar, por favor, int\u00e9ntalo de nuevo.", "unknown_error": "El dispositivo report\u00f3 un error desconocido. El emparejamiento ha fallado." }, diff --git a/homeassistant/components/homekit_controller/translations/sensor.it.json b/homeassistant/components/homekit_controller/translations/sensor.it.json index acfff28f0b2..338c9e58009 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.it.json +++ b/homeassistant/components/homekit_controller/translations/sensor.it.json @@ -1,10 +1,21 @@ { "state": { "homekit_controller__thread_node_capabilities": { - "none": "Nessuna" + "border_router_capable": "Capacit\u00e0 di router di confine", + "full": "Dispositivo terminale completo", + "minimal": "Dispositivo terminale minimale", + "none": "Nessuna", + "router_eligible": "Dispositivo terminale idoneo al router", + "sleepy": "Dispositivo terminale dormiente" }, "homekit_controller__thread_status": { - "disabled": "Disabilitato" + "border_router": "Router di confine", + "child": "Figlio", + "detached": "Separato", + "disabled": "Disabilitato", + "joining": "Unendosi", + "leader": "Capo", + "router": "Router" } } } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/es.json b/homeassistant/components/homematicip_cloud/translations/es.json index afc637bbaa7..0a6096c644d 100644 --- a/homeassistant/components/homematicip_cloud/translations/es.json +++ b/homeassistant/components/homematicip_cloud/translations/es.json @@ -6,10 +6,10 @@ "unknown": "Error inesperado" }, "error": { - "invalid_sgtin_or_pin": "SGTIN o C\u00f3digo PIN no v\u00e1lido, int\u00e9ntalo de nuevo.", + "invalid_sgtin_or_pin": "SGTIN o C\u00f3digo PIN no v\u00e1lido, por favor, int\u00e9ntalo de nuevo.", "press_the_button": "Por favor, pulsa el bot\u00f3n azul", - "register_failed": "No se pudo registrar, por favor int\u00e9ntalo de nuevo.", - "timeout_button": "Se agot\u00f3 el tiempo de espera para presionar el bot\u00f3n azul. Vuelve a intentarlo." + "register_failed": "No se pudo registrar, por favor, int\u00e9ntalo de nuevo.", + "timeout_button": "Se agot\u00f3 el tiempo de espera para presionar el bot\u00f3n azul, por favor, int\u00e9ntalo de nuevo." }, "step": { "init": { diff --git a/homeassistant/components/honeywell/translations/es.json b/homeassistant/components/honeywell/translations/es.json index c97e5aa3ddf..ef261013844 100644 --- a/homeassistant/components/honeywell/translations/es.json +++ b/homeassistant/components/honeywell/translations/es.json @@ -9,7 +9,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Por favor, introduce las credenciales utilizadas para iniciar sesi\u00f3n en mytotalconnectcomfort.com." + "description": "Por favor, introduce las credenciales usadas para iniciar sesi\u00f3n en mytotalconnectcomfort.com." } } }, diff --git a/homeassistant/components/huawei_lte/translations/es.json b/homeassistant/components/huawei_lte/translations/es.json index 03b159989f2..a88f35ba3d5 100644 --- a/homeassistant/components/huawei_lte/translations/es.json +++ b/homeassistant/components/huawei_lte/translations/es.json @@ -9,7 +9,7 @@ "incorrect_username": "Nombre de usuario incorrecto", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_url": "URL no v\u00e1lida", - "login_attempts_exceeded": "Se han superado los intentos de inicio de sesi\u00f3n m\u00e1ximos, por favor, int\u00e9ntalo de nuevo m\u00e1s tarde.", + "login_attempts_exceeded": "Se han superado los intentos de inicio de sesi\u00f3n m\u00e1ximos, por favor, vuelve a intentarlo m\u00e1s tarde.", "response_error": "Error desconocido del dispositivo", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/hue/translations/ca.json b/homeassistant/components/hue/translations/ca.json index 3b192e80b00..11f66de23b9 100644 --- a/homeassistant/components/hue/translations/ca.json +++ b/homeassistant/components/hue/translations/ca.json @@ -44,6 +44,8 @@ "button_2": "Segon bot\u00f3", "button_3": "Tercer bot\u00f3", "button_4": "Quart bot\u00f3", + "clock_wise": "Rotaci\u00f3 en sentit horari", + "counter_clock_wise": "Rotaci\u00f3 en sentit antihorari", "dim_down": "Atenua la brillantor", "dim_up": "Augmenta la brillantor", "double_buttons_1_3": "primer i tercer botons", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "Ambd\u00f3s \"{subtype}\" alliberats", - "initial_press": "Bot\u00f3 \"{subtype}\" premut inicialment", - "long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", - "remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", - "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", - "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", + "initial_press": "\"{subtype}\" premut inicialment", + "long_release": "\"{subtype}\" alliberat despr\u00e9s d'una estona premut", + "remote_button_long_release": "\"{subtype}\" alliberat despr\u00e9s d'una estona premut", + "remote_button_short_press": "\"{subtype}\" premut", + "remote_button_short_release": "\"{subtype}\" alliberat", "remote_double_button_long_press": "Ambd\u00f3s \"{subtype}\" alliberats despr\u00e9s d'una estona premuts", "remote_double_button_short_press": "Ambd\u00f3s \"{subtype}\" alliberats", - "repeat": "Bot\u00f3 \"{subtype}\" mantingut premut", - "short_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s de pr\u00e9mer breument" + "repeat": "\"{subtype}\" mantingut premut", + "short_release": "\"{subtype}\" alliberat despr\u00e9s de pr\u00e9mer breument", + "start": "\"{subtype}\" premut inicialment" } }, "options": { diff --git a/homeassistant/components/hue/translations/en.json b/homeassistant/components/hue/translations/en.json index 7a54fc5ce03..ca50dec4715 100644 --- a/homeassistant/components/hue/translations/en.json +++ b/homeassistant/components/hue/translations/en.json @@ -44,14 +44,14 @@ "button_2": "Second button", "button_3": "Third button", "button_4": "Fourth button", + "clock_wise": "Rotation clockwise", + "counter_clock_wise": "Rotation counter-clockwise", "dim_down": "Dim down", "dim_up": "Dim up", "double_buttons_1_3": "First and Third buttons", "double_buttons_2_4": "Second and Fourth buttons", "turn_off": "Turn off", - "turn_on": "Turn on", - "clock_wise": "Rotation clockwise", - "counter_clock_wise": "Rotation counter-clockwise" + "turn_on": "Turn on" }, "trigger_type": { "double_short_release": "Both \"{subtype}\" released", diff --git a/homeassistant/components/hue/translations/es.json b/homeassistant/components/hue/translations/es.json index e49438144b7..96f3388f2d3 100644 --- a/homeassistant/components/hue/translations/es.json +++ b/homeassistant/components/hue/translations/es.json @@ -13,7 +13,7 @@ }, "error": { "linking": "Error inesperado", - "register_failed": "No se pudo registrar, int\u00e9ntalo de nuevo" + "register_failed": "No se pudo registrar, por favor, int\u00e9ntalo de nuevo" }, "step": { "init": { @@ -24,7 +24,7 @@ }, "link": { "description": "Presiona el bot\u00f3n en la pasarela para registrar Philips Hue con Home Assistant. \n\n![Ubicaci\u00f3n del bot\u00f3n en la pasarela](/static/images/config_philips_hue.jpg)", - "title": "Vincular pasarela" + "title": "Vincular concentrador" }, "manual": { "data": { diff --git a/homeassistant/components/hue/translations/id.json b/homeassistant/components/hue/translations/id.json index d450adfb3c4..0b81e1093df 100644 --- a/homeassistant/components/hue/translations/id.json +++ b/homeassistant/components/hue/translations/id.json @@ -44,6 +44,8 @@ "button_2": "Tombol kedua", "button_3": "Tombol ketiga", "button_4": "Tombol keempat", + "clock_wise": "Rotasi searah jarum jam", + "counter_clock_wise": "Rotasi berlawanan arah jarum jam", "dim_down": "Redupkan", "dim_up": "Terangkan", "double_buttons_1_3": "Tombol Pertama dan Ketiga", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "Kedua \"{subtype}\" dilepaskan", - "initial_press": "Tombol \"{subtype}\" awalnya ditekan", - "long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama", - "remote_button_long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama", - "remote_button_short_press": "Tombol \"{subtype}\" ditekan", - "remote_button_short_release": "Tombol \"{subtype}\" dilepaskan", + "initial_press": "\"{subtype}\" awalnya ditekan", + "long_release": "\"{subtype}\" dilepaskan setelah ditekan lama", + "remote_button_long_release": "\"{subtype}\" dilepaskan setelah ditekan lama", + "remote_button_short_press": "\"{subtype}\" ditekan", + "remote_button_short_release": "\"{subtype}\" dilepaskan", "remote_double_button_long_press": "Kedua \"{subtype}\" dilepaskan setelah ditekan lama", "remote_double_button_short_press": "Kedua \"{subtype}\" dilepas", - "repeat": "Tombol \"{subtype}\" ditekan terus", - "short_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan sebentar" + "repeat": "\"{subtype}\" ditekan terus", + "short_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan sebentar", + "start": "\"{subtype}\" awalnya ditekan" } }, "options": { diff --git a/homeassistant/components/hue/translations/pt-BR.json b/homeassistant/components/hue/translations/pt-BR.json index cc97e5e055e..12a609034dc 100644 --- a/homeassistant/components/hue/translations/pt-BR.json +++ b/homeassistant/components/hue/translations/pt-BR.json @@ -44,6 +44,8 @@ "button_2": "Segundo bot\u00e3o", "button_3": "Terceiro bot\u00e3o", "button_4": "Quarto bot\u00e3o", + "clock_wise": "Rota\u00e7\u00e3o no sentido hor\u00e1rio", + "counter_clock_wise": "Rota\u00e7\u00e3o no sentido anti-hor\u00e1rio", "dim_down": "Diminuir a luminosidade", "dim_up": "Aumentar a luminosidade", "double_buttons_1_3": "Primeiro e terceiro bot\u00f5es", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "Ambos \"{subtype}\" liberados", - "initial_press": "Bot\u00e3o \" {subtype} \" pressionado inicialmente", - "long_release": "Bot\u00e3o \" {subtype} \" liberado ap\u00f3s press\u00e3o longa", - "remote_button_long_release": "Bot\u00e3o \"{subtype}\" liberado ap\u00f3s longa press\u00e3o", - "remote_button_short_press": "Bot\u00e3o \"{subtype}\" pressionado", - "remote_button_short_release": "Bot\u00e3o \"{subtype}\" liberado", + "initial_press": "\"{subtype}\" pressionado inicialmente", + "long_release": "\"{subtype}\" liberado ap\u00f3s pressionar longamente", + "remote_button_long_release": "\"{subtype}\" liberado ap\u00f3s pressionar longamente", + "remote_button_short_press": "\"{subtype}\" pressionado", + "remote_button_short_release": "\"{subtype}\" lan\u00e7ado", "remote_double_button_long_press": "Ambos \"{subtype}\" lan\u00e7ados ap\u00f3s longa imprensa", "remote_double_button_short_press": "Ambos \"{subtype}\" lan\u00e7ados", - "repeat": "Bot\u00e3o \" {subtype} \" pressionado", - "short_release": "Bot\u00e3o \" {subtype} \" liberado ap\u00f3s pressionamento curto" + "repeat": "\"{subtype}\" pressionado", + "short_release": "\"{subtype}\" liberado ap\u00f3s pressionamento curto", + "start": "\"{subtype}\" pressionado inicialmente" } }, "options": { diff --git a/homeassistant/components/humidifier/translations/es.json b/homeassistant/components/humidifier/translations/es.json index 7a03cf901fc..c7331938aef 100644 --- a/homeassistant/components/humidifier/translations/es.json +++ b/homeassistant/components/humidifier/translations/es.json @@ -14,7 +14,7 @@ }, "trigger_type": { "changed_states": "{entity_name} se encendi\u00f3 o apag\u00f3", - "target_humidity_changed": "{entity_name} cambi\u00f3 su humedad objetivo", + "target_humidity_changed": "La humedad objetivo de {entity_name} cambi\u00f3", "turned_off": "{entity_name} apagado", "turned_on": "{entity_name} encendido" } diff --git a/homeassistant/components/hyperion/translations/es.json b/homeassistant/components/hyperion/translations/es.json index 91143ec96a8..3220866ab16 100644 --- a/homeassistant/components/hyperion/translations/es.json +++ b/homeassistant/components/hyperion/translations/es.json @@ -27,7 +27,7 @@ "title": "Confirmar para a\u00f1adir el servicio Hyperion Ambilight" }, "create_token": { - "description": "Elige **Enviar** a continuaci\u00f3n para solicitar un nuevo token de autenticaci\u00f3n. Ser\u00e1s redirigido a la IU de Hyperion para aprobar la solicitud. Verifica que la identificaci\u00f3n que se muestra sea \"{auth_id}\"", + "description": "Elige **Enviar** a continuaci\u00f3n para solicitar un nuevo token de autenticaci\u00f3n. Ser\u00e1s redirigido a la IU de Hyperion para aprobar la solicitud. Por favor, verifica que la identificaci\u00f3n que se muestra sea \"{auth_id}\"", "title": "Crear autom\u00e1ticamente un nuevo token de autenticaci\u00f3n" }, "create_token_external": { diff --git a/homeassistant/components/icloud/translations/es.json b/homeassistant/components/icloud/translations/es.json index e5e6a8927a9..5f0f4ac6495 100644 --- a/homeassistant/components/icloud/translations/es.json +++ b/homeassistant/components/icloud/translations/es.json @@ -8,7 +8,7 @@ "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "send_verification_code": "No se pudo enviar el c\u00f3digo de verificaci\u00f3n", - "validate_verification_code": "No se ha podido verificar el c\u00f3digo de verificaci\u00f3n, vuelve a intentarlo" + "validate_verification_code": "No se ha podido verificar el c\u00f3digo de verificaci\u00f3n, int\u00e9ntalo de nuevo" }, "step": { "reauth": { diff --git a/homeassistant/components/insteon/translations/es.json b/homeassistant/components/insteon/translations/es.json index 8dbe09d635c..855349ab9cf 100644 --- a/homeassistant/components/insteon/translations/es.json +++ b/homeassistant/components/insteon/translations/es.json @@ -50,7 +50,7 @@ "options": { "error": { "cannot_connect": "No se pudo conectar", - "input_error": "Entradas no v\u00e1lidas, por favor, verifica tus valores.", + "input_error": "Entradas no v\u00e1lidas, por favor, comprueba tus valores.", "select_single": "Selecciona una opci\u00f3n." }, "step": { @@ -69,7 +69,7 @@ "steps": "Pasos de atenuaci\u00f3n (s\u00f3lo para dispositivos de luz, por defecto 22)", "unitcode": "Unitcode (1 - 16)" }, - "description": "Cambia la contrase\u00f1a del Hub Insteon." + "description": "Cambia la contrase\u00f1a del Insteon Hub." }, "change_hub_config": { "data": { @@ -78,7 +78,7 @@ "port": "Puerto", "username": "Nombre de usuario" }, - "description": "Cambia la informaci\u00f3n de conexi\u00f3n del Hub Insteon. Debes reiniciar Home Assistant despu\u00e9s de realizar este cambio. Esto no cambia la configuraci\u00f3n del Hub en s\u00ed. Para cambiar la configuraci\u00f3n en el Hub, usa la aplicaci\u00f3n Hub." + "description": "Cambia la informaci\u00f3n de conexi\u00f3n del Insteon Hub. Debes reiniciar Home Assistant despu\u00e9s de realizar este cambio. Esto no cambia la configuraci\u00f3n del Hub en s\u00ed. Para cambiar la configuraci\u00f3n en el Hub, usa la aplicaci\u00f3n Hub." }, "init": { "data": { diff --git a/homeassistant/components/ipma/translations/es.json b/homeassistant/components/ipma/translations/es.json index 089a58903b4..078dd7546d9 100644 --- a/homeassistant/components/ipma/translations/es.json +++ b/homeassistant/components/ipma/translations/es.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Nombre ya existe" + "name_exists": "El nombre ya existe" }, "step": { "user": { diff --git a/homeassistant/components/ipp/translations/es.json b/homeassistant/components/ipp/translations/es.json index 845ba0b2b7b..740462de0f4 100644 --- a/homeassistant/components/ipp/translations/es.json +++ b/homeassistant/components/ipp/translations/es.json @@ -11,7 +11,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "connection_upgrade": "No se pudo conectar con la impresora. Int\u00e9ntalo de nuevo con la opci\u00f3n SSL/TLS marcada." + "connection_upgrade": "No se pudo conectar con la impresora. Por favor, int\u00e9ntalo de nuevo con la opci\u00f3n SSL/TLS marcada." }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/justnimbus/translations/ja.json b/homeassistant/components/justnimbus/translations/ja.json new file mode 100644 index 00000000000..93f2481120e --- /dev/null +++ b/homeassistant/components/justnimbus/translations/ja.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "client_id": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/es.json b/homeassistant/components/knx/translations/es.json index cad83301d83..19de37aaf56 100644 --- a/homeassistant/components/knx/translations/es.json +++ b/homeassistant/components/knx/translations/es.json @@ -21,7 +21,7 @@ }, "data_description": { "host": "Direcci\u00f3n IP del dispositivo de tunelizaci\u00f3n KNX/IP.", - "local_ip": "D\u00e9jalo en blanco para utilizar el descubrimiento autom\u00e1tico.", + "local_ip": "D\u00e9jalo en blanco para usar el descubrimiento autom\u00e1tico.", "port": "Puerto del dispositivo de tunelizaci\u00f3n KNX/IP." }, "description": "Por favor, introduce la informaci\u00f3n de conexi\u00f3n de tu dispositivo de t\u00fanel." @@ -61,7 +61,7 @@ "user_id": "Este suele ser el n\u00famero de t\u00fanel +1. Por tanto, 'T\u00fanel 2' tendr\u00eda ID de usuario '3'.", "user_password": "Contrase\u00f1a para la conexi\u00f3n de t\u00fanel espec\u00edfica establecida en el panel 'Propiedades' del t\u00fanel en ETS." }, - "description": "Introduce tu informaci\u00f3n de IP segura." + "description": "Por favor, introduce tu informaci\u00f3n de IP segura." }, "secure_tunneling": { "description": "Selecciona c\u00f3mo quieres configurar KNX/IP Secure.", @@ -74,7 +74,7 @@ "data": { "gateway": "Conexi\u00f3n de t\u00fanel KNX" }, - "description": "Selecciona una puerta de enlace de la lista." + "description": "Por favor, selecciona una puerta de enlace de la lista." }, "type": { "data": { @@ -98,7 +98,7 @@ }, "data_description": { "individual_address": "Direcci\u00f3n KNX que usar\u00e1 Home Assistant, por ejemplo, `0.0.4`", - "local_ip": "Usa `0.0.0.0` para el descubrimiento autom\u00e1tico.", + "local_ip": "Usar `0.0.0.0` para el descubrimiento autom\u00e1tico.", "multicast_group": "Se utiliza para el enrutamiento y el descubrimiento. Predeterminado: `224.0.23.12`", "multicast_port": "Se utiliza para el enrutamiento y el descubrimiento. Predeterminado: `3671`", "rate_limit": "N\u00famero m\u00e1ximo de telegramas salientes por segundo.\nRecomendado: 20 a 40", diff --git a/homeassistant/components/kodi/translations/es.json b/homeassistant/components/kodi/translations/es.json index 5ea61e0e0c9..b0599290789 100644 --- a/homeassistant/components/kodi/translations/es.json +++ b/homeassistant/components/kodi/translations/es.json @@ -31,7 +31,7 @@ "port": "Puerto", "ssl": "Utiliza un certificado SSL" }, - "description": "Informaci\u00f3n de conexi\u00f3n de Kodi. Aseg\u00farate de habilitar \"Permitir el control de Kodi a trav\u00e9s de HTTP\" en Sistema/Configuraci\u00f3n/Red/Servicios." + "description": "Informaci\u00f3n de conexi\u00f3n de Kodi. Por favor, aseg\u00farate de habilitar \"Permitir el control de Kodi a trav\u00e9s de HTTP\" en Sistema/Configuraci\u00f3n/Red/Servicios." }, "ws_port": { "data": { diff --git a/homeassistant/components/konnected/translations/es.json b/homeassistant/components/konnected/translations/es.json index 47a4d1dc5b4..b99cbcde3f5 100644 --- a/homeassistant/components/konnected/translations/es.json +++ b/homeassistant/components/konnected/translations/es.json @@ -89,7 +89,7 @@ "discovery": "Responder a las solicitudes de descubrimiento en tu red", "override_api_host": "Anular la URL predeterminada del panel de host de la API de Home Assistant" }, - "description": "Selecciona el comportamiento deseado para tu panel", + "description": "Por favor, selecciona el comportamiento deseado para tu panel", "title": "Configurar miscel\u00e1neos" }, "options_switch": { diff --git a/homeassistant/components/kraken/translations/ja.json b/homeassistant/components/kraken/translations/ja.json index 40a1ac53232..97ae0c9737e 100644 --- a/homeassistant/components/kraken/translations/ja.json +++ b/homeassistant/components/kraken/translations/ja.json @@ -5,6 +5,9 @@ }, "step": { "user": { + "data": { + "other": "\u7a7a" + }, "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" } } diff --git a/homeassistant/components/lifx/translations/es.json b/homeassistant/components/lifx/translations/es.json index a308d5fbb9e..6bc2249182e 100644 --- a/homeassistant/components/lifx/translations/es.json +++ b/homeassistant/components/lifx/translations/es.json @@ -26,7 +26,7 @@ "data": { "host": "Host" }, - "description": "Si deja el host vac\u00edo, se usar\u00e1 el descubrimiento para encontrar dispositivos." + "description": "Si dejas el host vac\u00edo, se usar\u00e1 el descubrimiento para encontrar dispositivos." } } } diff --git a/homeassistant/components/mailgun/translations/es.json b/homeassistant/components/mailgun/translations/es.json index 85007655134..bf632ad9700 100644 --- a/homeassistant/components/mailgun/translations/es.json +++ b/homeassistant/components/mailgun/translations/es.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, "create_entry": { - "default": "Para enviar eventos a Home Assistant debes configurar los [Webhooks en Mailgun]({mailgun_url}). \n\n Completa la siguiente informaci\u00f3n: \n\n- URL: `{webhook_url}` \n- M\u00e9todo: POST \n- Tipo de contenido: application/json \n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." + "default": "Para enviar eventos a Home Assistant debes configurar los [Webhooks con Mailgun]({mailgun_url}). \n\n Completa la siguiente informaci\u00f3n: \n\n- URL: `{webhook_url}` \n- M\u00e9todo: POST \n- Tipo de contenido: application/json \n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." }, "step": { "user": { diff --git a/homeassistant/components/mazda/translations/es.json b/homeassistant/components/mazda/translations/es.json index 009385a5bc1..53dec475cad 100644 --- a/homeassistant/components/mazda/translations/es.json +++ b/homeassistant/components/mazda/translations/es.json @@ -5,7 +5,7 @@ "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { - "account_locked": "Cuenta bloqueada. Por favor, int\u00e9ntalo de nuevo m\u00e1s tarde.", + "account_locked": "Cuenta bloqueada. Por favor, vuelve a intentarlo m\u00e1s tarde.", "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" @@ -17,7 +17,7 @@ "password": "Contrase\u00f1a", "region": "Regi\u00f3n" }, - "description": "Introduce la direcci\u00f3n de correo electr\u00f3nico y la contrase\u00f1a que utilizas para iniciar sesi\u00f3n en la aplicaci\u00f3n m\u00f3vil MyMazda." + "description": "Por favor, introduce la direcci\u00f3n de correo electr\u00f3nico y la contrase\u00f1a que usas para iniciar sesi\u00f3n en la aplicaci\u00f3n m\u00f3vil MyMazda." } } } diff --git a/homeassistant/components/meater/translations/es.json b/homeassistant/components/meater/translations/es.json index 8c4be00c610..1fc556b8ee2 100644 --- a/homeassistant/components/meater/translations/es.json +++ b/homeassistant/components/meater/translations/es.json @@ -2,7 +2,7 @@ "config": { "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "service_unavailable_error": "La API no est\u00e1 disponible en este momento, por favor int\u00e9ntalo m\u00e1s tarde.", + "service_unavailable_error": "La API no est\u00e1 disponible en este momento, por favor, vuelve a intentarlo m\u00e1s tarde.", "unknown_auth_error": "Error inesperado" }, "step": { diff --git a/homeassistant/components/media_player/translations/es.json b/homeassistant/components/media_player/translations/es.json index 946062acdc7..fe056b81eb1 100644 --- a/homeassistant/components/media_player/translations/es.json +++ b/homeassistant/components/media_player/translations/es.json @@ -20,7 +20,7 @@ }, "state": { "_": { - "buffering": "almacenamiento en b\u00fafer", + "buffering": "almacenando en b\u00fafer", "idle": "Inactivo", "off": "Apagado", "on": "Encendido", diff --git a/homeassistant/components/met_eireann/translations/es.json b/homeassistant/components/met_eireann/translations/es.json index 5448f05a9bb..d476d15366e 100644 --- a/homeassistant/components/met_eireann/translations/es.json +++ b/homeassistant/components/met_eireann/translations/es.json @@ -11,7 +11,7 @@ "longitude": "Longitud", "name": "Nombre" }, - "description": "Introduce tu ubicaci\u00f3n para utilizar los datos meteorol\u00f3gicos de la API del pron\u00f3stico meteorol\u00f3gico p\u00fablico de Met \u00c9ireann", + "description": "Introduce tu ubicaci\u00f3n para usar los datos meteorol\u00f3gicos de la API del pron\u00f3stico meteorol\u00f3gico p\u00fablico de Met \u00c9ireann", "title": "Ubicaci\u00f3n" } } diff --git a/homeassistant/components/meteo_france/translations/es.json b/homeassistant/components/meteo_france/translations/es.json index b8363d01868..d0a713119af 100644 --- a/homeassistant/components/meteo_france/translations/es.json +++ b/homeassistant/components/meteo_france/translations/es.json @@ -5,7 +5,7 @@ "unknown": "Error inesperado" }, "error": { - "empty": "No hay resultado en la b\u00fasqueda de la ciudad: por favor, verifica el campo de la ciudad" + "empty": "No hay resultado en la b\u00fasqueda de la ciudad: por favor, comprueba el campo de la ciudad" }, "step": { "cities": { diff --git a/homeassistant/components/metoffice/translations/es.json b/homeassistant/components/metoffice/translations/es.json index 0a533ecd130..9d984f9841a 100644 --- a/homeassistant/components/metoffice/translations/es.json +++ b/homeassistant/components/metoffice/translations/es.json @@ -14,7 +14,7 @@ "latitude": "Latitud", "longitude": "Longitud" }, - "description": "La latitud y la longitud se utilizar\u00e1n para encontrar la estaci\u00f3n meteorol\u00f3gica m\u00e1s cercana.", + "description": "La latitud y la longitud se usar\u00e1n para encontrar la estaci\u00f3n meteorol\u00f3gica m\u00e1s cercana.", "title": "Conectar con la UK Met Office" } } diff --git a/homeassistant/components/miflora/translations/es.json b/homeassistant/components/miflora/translations/es.json index 93a1e82aed6..c16bf2bae56 100644 --- a/homeassistant/components/miflora/translations/es.json +++ b/homeassistant/components/miflora/translations/es.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "La integraci\u00f3n Mi Flora dej\u00f3 de funcionar en Home Assistant 2022.7 y se reemplaz\u00f3 por la integraci\u00f3n de Xiaomi BLE en la versi\u00f3n 2022.8. \n\nNo hay una ruta de migraci\u00f3n posible, por lo tanto, debes agregar tu dispositivo Mi Flora usando la nueva integraci\u00f3n manualmente. \n\nHome Assistant ya no usa la configuraci\u00f3n YAML existente de Mi Flora. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "La integraci\u00f3n Mi Flora dej\u00f3 de funcionar en Home Assistant 2022.7 y se reemplaz\u00f3 por la integraci\u00f3n de Xiaomi BLE en la versi\u00f3n 2022.8. \n\nNo hay una ruta de migraci\u00f3n posible, por lo tanto, debes a\u00f1adir manualmente tu dispositivo Mi Flora usando la nueva integraci\u00f3n. \n\nHome Assistant ya no usa la configuraci\u00f3n YAML existente de Mi Flora. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "La integraci\u00f3n Mi Flora ha sido reemplazada" } } diff --git a/homeassistant/components/minecraft_server/translations/es.json b/homeassistant/components/minecraft_server/translations/es.json index 8fa95f0dd10..34fa86dc45d 100644 --- a/homeassistant/components/minecraft_server/translations/es.json +++ b/homeassistant/components/minecraft_server/translations/es.json @@ -4,9 +4,9 @@ "already_configured": "El servicio ya est\u00e1 configurado" }, "error": { - "cannot_connect": "Error al conectar con el servidor. Verifica el host y el puerto y vuelve a intentarlo. Tambi\u00e9n aseg\u00farate de estar ejecutando al menos la versi\u00f3n 1.7 de Minecraft en tu servidor.", - "invalid_ip": "La direcci\u00f3n IP no es v\u00e1lida (no se pudo determinar la direcci\u00f3n MAC). Por favor, corr\u00edgelo y vuelve a intentarlo.", - "invalid_port": "El puerto debe estar en el rango de 1024 a 65535. Por favor, corr\u00edgelo y vuelve a intentarlo." + "cannot_connect": "Error al conectar con el servidor. Por favor, comprueba el host y el puerto e int\u00e9ntalo de nuevo. Tambi\u00e9n aseg\u00farate de estar ejecutando al menos la versi\u00f3n 1.7 de Minecraft en tu servidor.", + "invalid_ip": "La direcci\u00f3n IP no es v\u00e1lida (no se pudo determinar la direcci\u00f3n MAC). Por favor, corr\u00edgelo e int\u00e9ntalo de nuevo.", + "invalid_port": "El puerto debe estar en el rango de 1024 a 65535. Por favor, corr\u00edgelo e int\u00e9ntalo de nuevo." }, "step": { "user": { diff --git a/homeassistant/components/mitemp_bt/translations/ca.json b/homeassistant/components/mitemp_bt/translations/ca.json index a567bceb950..c46a8d3ff3d 100644 --- a/homeassistant/components/mitemp_bt/translations/ca.json +++ b/homeassistant/components/mitemp_bt/translations/ca.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "La integraci\u00f3 Xiaomi Mijia sensor de temperatura i humitat BLE va deixar de funcionar a Home Assistant 2022.7 i ha estat substitu\u00efda per la integraci\u00f3 Xiaomi BLE a la versi\u00f3 Home Assistant 2022.8.\n\nNo hi ha cap migraci\u00f3 possible, per tant, has d'afegir els teus dispositius Xiaomi Mijia BLE manualment utilitzant la nova integraci\u00f3.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML de l'antiga integraci\u00f3. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "description": "La integraci\u00f3 Xiaomi Mijia sensor de temperatura i humitat BLE va deixar de funcionar a Home Assistant 2022.7 i ha estat substitu\u00efda per la integraci\u00f3 Xiaomi BLE a la versi\u00f3 de Home Assistant 2022.8.\n\nNo hi ha cap migraci\u00f3 possible, per tant, has d'afegir els teus dispositius Xiaomi Mijia BLE manualment utilitzant la nova integraci\u00f3.\n\nHome Assistant ja no utilitza la configuraci\u00f3 YAML de l'antiga integraci\u00f3. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", "title": "La integraci\u00f3 Xiaomi Mijia sensor de temperatura i humitat BLE ha estat substitu\u00efda" } } diff --git a/homeassistant/components/mitemp_bt/translations/es.json b/homeassistant/components/mitemp_bt/translations/es.json index 6ae1e300961..544bc32943a 100644 --- a/homeassistant/components/mitemp_bt/translations/es.json +++ b/homeassistant/components/mitemp_bt/translations/es.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "La integraci\u00f3n del sensor de temperatura y humedad Xiaomi Mijia BLE dej\u00f3 de funcionar en Home Assistant 2022.7 y fue reemplazada por la integraci\u00f3n Xiaomi BLE en la versi\u00f3n 2022.8. \n\nNo hay una ruta de migraci\u00f3n posible, por lo tanto, debes agregar tu dispositivo Xiaomi Mijia BLE utilizando la nueva integraci\u00f3n manualmente. \n\nHome Assistant ya no utiliza la configuraci\u00f3n YAML del sensor de temperatura y humedad BLE de Xiaomi Mijia. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "La integraci\u00f3n del sensor de temperatura y humedad Xiaomi Mijia BLE dej\u00f3 de funcionar en Home Assistant 2022.7 y fue reemplazada por la integraci\u00f3n Xiaomi BLE en la versi\u00f3n 2022.8. \n\nNo hay una ruta de migraci\u00f3n posible, por lo tanto, debes a\u00f1adir manualmente tu dispositivo Xiaomi Mijia BLE usando la nueva integraci\u00f3n. \n\nHome Assistant ya no utiliza la configuraci\u00f3n YAML del sensor de temperatura y humedad BLE de Xiaomi Mijia. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "La integraci\u00f3n del sensor de temperatura y humedad Xiaomi Mijia BLE ha sido reemplazada" } } diff --git a/homeassistant/components/motion_blinds/translations/es.json b/homeassistant/components/motion_blinds/translations/es.json index 585a7c0a8ed..d2f6235e9d5 100644 --- a/homeassistant/components/motion_blinds/translations/es.json +++ b/homeassistant/components/motion_blinds/translations/es.json @@ -20,14 +20,14 @@ "data": { "select_ip": "Direcci\u00f3n IP" }, - "description": "Vuelve a ejecutar la configuraci\u00f3n si deseas conectar Motion Gateways adicionales", + "description": "Ejecuta la configuraci\u00f3n de nuevo si quieres conectar Motion Gateways adicionales", "title": "Selecciona el Motion Gateway que deseas conectar" }, "user": { "data": { "host": "Direcci\u00f3n IP" }, - "description": "Con\u00e9ctate a tu Motion Gateway, si la direcci\u00f3n IP no est\u00e1 configurada, se utiliza la detecci\u00f3n autom\u00e1tica" + "description": "Con\u00e9ctate a tu Motion Gateway, si la direcci\u00f3n IP no est\u00e1 configurada, se usar\u00e1 la detecci\u00f3n autom\u00e1tica" } } }, diff --git a/homeassistant/components/mysensors/translations/ja.json b/homeassistant/components/mysensors/translations/ja.json index 2732a622511..fd2294ab820 100644 --- a/homeassistant/components/mysensors/translations/ja.json +++ b/homeassistant/components/mysensors/translations/ja.json @@ -14,6 +14,7 @@ "invalid_serial": "\u7121\u52b9\u306a\u30b7\u30ea\u30a2\u30eb\u30dd\u30fc\u30c8", "invalid_subscribe_topic": "\u7121\u52b9\u306a\u30b5\u30d6\u30b9\u30af\u30e9\u30a4\u30d6 \u30c8\u30d4\u30c3\u30af", "invalid_version": "MySensors\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u7121\u52b9\u3067\u3059", + "mqtt_required": "MQTT\u7d71\u5408\u304c\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "not_a_number": "\u6570\u5b57\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", "port_out_of_range": "\u30dd\u30fc\u30c8\u756a\u53f7\u306f1\u4ee5\u4e0a65535\u4ee5\u4e0b\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059", "same_topic": "\u30b5\u30d6\u30b9\u30af\u30e9\u30a4\u30d6\u3068\u30d1\u30d6\u30ea\u30c3\u30b7\u30e5\u306e\u30c8\u30d4\u30c3\u30af\u304c\u540c\u3058\u3067\u3059", @@ -72,6 +73,7 @@ "description": "\u8a2d\u5b9a\u3059\u308b\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u3092\u9078\u629e\u3057\u307e\u3059\u3002", "menu_options": { "gw_mqtt": "MQTT\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u8a2d\u5b9a", + "gw_serial": "\u30b7\u30ea\u30a2\u30eb\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u8a2d\u5b9a", "gw_tcp": "TCP\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4\u306e\u8a2d\u5b9a" } }, diff --git a/homeassistant/components/nam/translations/es.json b/homeassistant/components/nam/translations/es.json index 0ba92e457ca..c0ac0958d24 100644 --- a/homeassistant/components/nam/translations/es.json +++ b/homeassistant/components/nam/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "device_unsupported": "El dispositivo no es compatible.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", - "reauth_unsuccessful": "No se pudo volver a autenticar, elimina la integraci\u00f3n y vuelve a configurarla." + "reauth_unsuccessful": "No se pudo volver a autenticar, por favor, elimina la integraci\u00f3n y vuelve a configurarla." }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/neato/translations/es.json b/homeassistant/components/neato/translations/es.json index 81f11ec51ff..4dcab62da43 100644 --- a/homeassistant/components/neato/translations/es.json +++ b/homeassistant/components/neato/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "create_entry": { diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index 33d0179208b..20fc09c1af3 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -99,9 +99,11 @@ }, "issues": { "deprecated_yaml": { + "description": "La configuraci\u00f3 de Nest mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant a la versi\u00f3 2022.10. \n\nLa configuraci\u00f3 existent de credencials d'aplicaci\u00f3 OAuth i d'acc\u00e9s s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari. Elimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", "title": "La configuraci\u00f3 YAML de Nest est\u00e0 sent eliminada" }, "removed_app_auth": { + "description": "Per millorar la seguretat i reduir riscos, Google ha fet obsolet el m\u00e8tode d'autenticaci\u00f3 que utilitza Home Assistant. \n\n **Per resoldre-ho, has de dur a terme una acci\u00f3** ([m\u00e9s informaci\u00f3]({more_info_url})) \n\n 1. Visita la p\u00e0gina d'integracions.\n 2. Fes clic a Reconfigura a la integraci\u00f3 Nest.\n 3. Home Assistant et guiar\u00e0 a trav\u00e9s dels passos per actualitzar a l'autenticaci\u00f3 web. \n\nConsulta les [instruccions de la integraci\u00f3] Nest ({documentation_url}) per m\u00e9s informaci\u00f3 sobre resoluci\u00f3 de problemes.", "title": "Les credencials d'autenticaci\u00f3 de Nest s'han d'actualitzar" } } diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index 7715f1f99bd..535482b74da 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -8,7 +8,7 @@ "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "invalid_access_token": "Token de acceso no v\u00e1lido", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." @@ -17,13 +17,13 @@ "default": "Autenticado correctamente" }, "error": { - "bad_project_id": "Por favor introduce un ID de proyecto en la nube v\u00e1lido (verifica la Cloud Console)", + "bad_project_id": "Por favor, introduce un ID de proyecto en la nube v\u00e1lido (comprueba la Consola Cloud)", "internal_error": "Error interno validando el c\u00f3digo", "invalid_pin": "C\u00f3digo PIN no v\u00e1lido", "subscriber_error": "Error de suscriptor desconocido, mira los registros", "timeout": "Tiempo de espera agotado validando el c\u00f3digo", "unknown": "Error inesperado", - "wrong_project_id": "Por favor introduce un ID de proyecto en la nube v\u00e1lido (era el mismo que el ID del proyecto de acceso al dispositivo)" + "wrong_project_id": "Por favor, introduce un ID de proyecto en la nube v\u00e1lido (era el mismo que el ID del proyecto de acceso al dispositivo)" }, "step": { "auth": { @@ -45,14 +45,14 @@ "title": "Nest: Introduce el ID del Cloud Project" }, "create_cloud_project": { - "description": "La integraci\u00f3n Nest te permite integrar tus termostatos, c\u00e1maras y timbres Nest mediante la API de administraci\u00f3n de dispositivos inteligentes (SDM). La API de SDM **requiere una tarifa de configuraci\u00f3n \u00fanica de 5$**. Consulta la documentaci\u00f3n para obtener [m\u00e1s informaci\u00f3n]({more_info_url}).\n\n1. Ve a [Google Cloud Console]({cloud_console_url}).\n1. Si este es tu primer proyecto, haz clic en **Crear proyecto** y luego en **Nuevo proyecto**.\n1. Asigna un nombre a tu Cloud Project y, a continuaci\u00f3n, haz clic en **Crear**.\n1. Guarda el ID del Cloud Project, por ejemplo, *example-project-12345* ya que lo necesitar\u00e1s m\u00e1s adelante\n1. Ve a la Biblioteca de API de [API de administraci\u00f3n de dispositivos inteligentes]({sdm_api_url}) y haz clic en **Habilitar**.\n1. Ve a la biblioteca de API de [Cloud Pub/Sub API]({pubsub_api_url}) y haz clic en **Habilitar**.\n\nContin\u00faa cuando tu proyecto en la nube est\u00e9 configurado.", + "description": "La integraci\u00f3n Nest te permite integrar tus termostatos, c\u00e1maras y timbres Nest mediante la API de administraci\u00f3n de dispositivos inteligentes (SDM). La API de SDM **requiere una cuota \u00fanica de configuraci\u00f3n de 5$**. Consulta la documentaci\u00f3n para obtener [m\u00e1s informaci\u00f3n]({more_info_url}).\n\n1. Ve a [Google Cloud Console]({cloud_console_url}).\n1. Si este es tu primer proyecto, haz clic en **Crear proyecto** y luego en **Nuevo proyecto**.\n1. Asigna un nombre a tu Cloud Project y, a continuaci\u00f3n, haz clic en **Crear**.\n1. Guarda el ID del Cloud Project, por ejemplo, *example-project-12345* ya que lo necesitar\u00e1s m\u00e1s adelante\n1. Ve a la Biblioteca de API de [API de administraci\u00f3n de dispositivos inteligentes]({sdm_api_url}) y haz clic en **Habilitar**.\n1. Ve a la biblioteca de API de [Cloud Pub/Sub API]({pubsub_api_url}) y haz clic en **Habilitar**.\n\nContin\u00faa cuando tu proyecto en la nube est\u00e9 configurado.", "title": "Nest: Crear y configurar un Cloud Project" }, "device_project": { "data": { "project_id": "ID de proyecto de acceso a dispositivos" }, - "description": "Crea un proyecto de acceso a dispositivos Nest que **requiere una tarifa de 5$** para configurarlo.\n 1. Ve a la [Consola de acceso al dispositivo] ({device_access_console_url}) y sigue el flujo de pago.\n 1. Haz clic en **Crear proyecto**\n 1. Asigna un nombre a tu proyecto de acceso a dispositivos y haz clic en **Siguiente**.\n 1. Introduce tu ID de cliente de OAuth\n 1. Habilita los eventos haciendo clic en **Habilitar** y **Crear proyecto**. \n\n Introduce tu ID de proyecto de acceso a dispositivos a continuaci\u00f3n ([m\u00e1s informaci\u00f3n]({more_info_url})).", + "description": "Crea un proyecto de acceso a dispositivos Nest que **requiere una cuota de 5$** para configurarlo.\n 1. Ve a la [Consola de acceso al dispositivo]({device_access_console_url}) y sigue el flujo de pago.\n 1. Haz clic en **Crear proyecto**\n 1. Asigna un nombre a tu proyecto de acceso a dispositivos y haz clic en **Siguiente**.\n 1. Introduce tu ID de cliente de OAuth\n 1. Habilita los eventos haciendo clic en **Habilitar** y **Crear proyecto**. \n\n Introduce tu ID de proyecto de acceso a dispositivos a continuaci\u00f3n ([m\u00e1s informaci\u00f3n]({more_info_url})).", "title": "Nest: Crear un proyecto de acceso a dispositivos" }, "device_project_upgrade": { @@ -71,7 +71,7 @@ "code": "C\u00f3digo PIN" }, "description": "Para vincular tu cuenta Nest, [autoriza tu cuenta]({url}). \n\nDespu\u00e9s de la autorizaci\u00f3n, copia y pega el c\u00f3digo PIN proporcionado a continuaci\u00f3n.", - "title": "Vincular cuenta de Nest" + "title": "Vincular cuenta Nest" }, "pick_implementation": { "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" @@ -103,7 +103,7 @@ "title": "Se va a eliminar la configuraci\u00f3n YAML de Nest" }, "removed_app_auth": { - "description": "Para mejorar la seguridad y reducir el riesgo de phishing, Google ha dejado de utilizar el m\u00e9todo de autenticaci\u00f3n utilizado por Home Assistant. \n\n **Esto requiere una acci\u00f3n por tu parte para resolverlo** ([m\u00e1s informaci\u00f3n]({more_info_url})) \n\n 1. Visita la p\u00e1gina de integraciones\n 1. Haz clic en Reconfigurar en la integraci\u00f3n de Nest.\n 1. Home Assistant te guiar\u00e1 a trav\u00e9s de los pasos para actualizar a la autenticaci\u00f3n web. \n\nConsulta las [instrucciones de integraci\u00f3n]({documentation_url}) de Nest para obtener informaci\u00f3n sobre la soluci\u00f3n de problemas.", + "description": "Para mejorar la seguridad y reducir el riesgo de phishing, Google ha dejado de usar el m\u00e9todo de autenticaci\u00f3n utilizado por Home Assistant. \n\n **Esto requiere una acci\u00f3n por tu parte para resolverlo** ([m\u00e1s informaci\u00f3n]({more_info_url})) \n\n 1. Visita la p\u00e1gina de integraciones\n 1. Haz clic en Reconfigurar en la integraci\u00f3n de Nest.\n 1. Home Assistant te guiar\u00e1 a trav\u00e9s de los pasos para actualizar a la autenticaci\u00f3n web. \n\nConsulta las [instrucciones de la integraci\u00f3n]({documentation_url}) Nest para obtener informaci\u00f3n sobre la soluci\u00f3n de problemas.", "title": "Las credenciales de autenticaci\u00f3n de Nest deben actualizarse" } } diff --git a/homeassistant/components/netatmo/translations/es.json b/homeassistant/components/netatmo/translations/es.json index 3a8350932be..68f4160beb9 100644 --- a/homeassistant/components/netatmo/translations/es.json +++ b/homeassistant/components/netatmo/translations/es.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, @@ -36,7 +36,7 @@ "person": "{entity_name} ha detectado una persona", "person_away": "{entity_name} ha detectado que una persona se ha ido", "set_point": "Temperatura objetivo de {entity_name} configurada manualmente", - "therm_mode": "{entity_name} cambi\u00f3 a \" {subtype} \"", + "therm_mode": "{entity_name} cambi\u00f3 a \"{subtype}\"", "turned_off": "{entity_name} apagado", "turned_on": "{entity_name} encendido", "vehicle": "{entity_name} ha detectado un veh\u00edculo" diff --git a/homeassistant/components/netgear/translations/es.json b/homeassistant/components/netgear/translations/es.json index 2ad6d0e6d16..740e45a302d 100644 --- a/homeassistant/components/netgear/translations/es.json +++ b/homeassistant/components/netgear/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "config": "Error de conexi\u00f3n o de inicio de sesi\u00f3n: verifica tu configuraci\u00f3n" + "config": "Error de conexi\u00f3n o de inicio de sesi\u00f3n: por favor, comprueba tu configuraci\u00f3n" }, "step": { "user": { diff --git a/homeassistant/components/nina/translations/es.json b/homeassistant/components/nina/translations/es.json index a4cabb669d8..8cc397794ba 100644 --- a/homeassistant/components/nina/translations/es.json +++ b/homeassistant/components/nina/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "no_selection": "Por favor selecciona al menos una ciudad/condado", + "no_selection": "Por favor, selecciona al menos una ciudad/condado", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/nmap_tracker/translations/es.json b/homeassistant/components/nmap_tracker/translations/es.json index f978f0d18ad..6491f3dc68a 100644 --- a/homeassistant/components/nmap_tracker/translations/es.json +++ b/homeassistant/components/nmap_tracker/translations/es.json @@ -36,5 +36,5 @@ } } }, - "title": "Rastreador de Nmap" + "title": "Rastreador Nmap" } \ No newline at end of file diff --git a/homeassistant/components/nws/translations/es.json b/homeassistant/components/nws/translations/es.json index 522e601d050..56fe3d4db01 100644 --- a/homeassistant/components/nws/translations/es.json +++ b/homeassistant/components/nws/translations/es.json @@ -15,7 +15,7 @@ "longitude": "Longitud", "station": "C\u00f3digo de estaci\u00f3n METAR" }, - "description": "Si no se especifica un c\u00f3digo de estaci\u00f3n METAR, la latitud y la longitud se utilizar\u00e1n para encontrar la estaci\u00f3n m\u00e1s cercana. Por ahora, una clave API puede ser cualquier cosa. Se recomienda utilizar una direcci\u00f3n de correo electr\u00f3nico v\u00e1lida.", + "description": "Si no se especifica un c\u00f3digo de estaci\u00f3n METAR, la latitud y la longitud se usar\u00e1n para encontrar la estaci\u00f3n m\u00e1s cercana. Por ahora, una clave API puede ser cualquier cosa. Se recomienda utilizar una direcci\u00f3n de correo electr\u00f3nico v\u00e1lida.", "title": "Conectar con el National Weather Service" } } diff --git a/homeassistant/components/onvif/translations/es.json b/homeassistant/components/onvif/translations/es.json index 0b450da3d46..ce828b53cd5 100644 --- a/homeassistant/components/onvif/translations/es.json +++ b/homeassistant/components/onvif/translations/es.json @@ -3,9 +3,9 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "no_h264": "No hab\u00eda transmisiones H264 disponibles. Verifica la configuraci\u00f3n del perfil en tu dispositivo.", + "no_h264": "No hab\u00eda transmisiones H264 disponibles. Comprueba la configuraci\u00f3n del perfil en tu dispositivo.", "no_mac": "No se pudo configurar un ID \u00fanico para el dispositivo ONVIF.", - "onvif_error": "Error al configurar el dispositivo ONVIF. Consulta los registros para obtener m\u00e1s informaci\u00f3n." + "onvif_error": "Error al configurar el dispositivo ONVIF. Revisa los registros para obtener m\u00e1s informaci\u00f3n." }, "error": { "cannot_connect": "No se pudo conectar" @@ -38,7 +38,7 @@ "data": { "auto": "Buscar autom\u00e1ticamente" }, - "description": "Al hacer clic en enviar, buscaremos en tu red dispositivos ONVIF compatibles con el perfil S. \n\nAlgunos fabricantes han comenzado a deshabilitar ONVIF por defecto. Aseg\u00farate de que ONVIF est\u00e9 habilitado en la configuraci\u00f3n de tu c\u00e1mara.", + "description": "Al hacer clic en enviar, buscaremos en tu red dispositivos ONVIF compatibles con el perfil S. \n\nAlgunos fabricantes han comenzado a deshabilitar ONVIF por defecto. Por favor, aseg\u00farate de que ONVIF est\u00e9 habilitado en la configuraci\u00f3n de tu c\u00e1mara.", "title": "Configuraci\u00f3n del dispositivo ONVIF" } } diff --git a/homeassistant/components/openexchangerates/translations/ca.json b/homeassistant/components/openexchangerates/translations/ca.json index 98c74ae55b3..634ea7578e7 100644 --- a/homeassistant/components/openexchangerates/translations/ca.json +++ b/homeassistant/components/openexchangerates/translations/ca.json @@ -26,6 +26,7 @@ }, "issues": { "deprecated_yaml": { + "description": "La configuraci\u00f3 d'Open Exchange Rates mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML d'Open Exchange Rates del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", "title": "La configuraci\u00f3 YAML d'Open Exchange Rates est\u00e0 sent eliminada" } } diff --git a/homeassistant/components/openexchangerates/translations/ja.json b/homeassistant/components/openexchangerates/translations/ja.json index 35a212e6d82..b4299af9207 100644 --- a/homeassistant/components/openexchangerates/translations/ja.json +++ b/homeassistant/components/openexchangerates/translations/ja.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f", "timeout_connect": "\u63a5\u7d9a\u78ba\u7acb\u6642\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8" }, @@ -14,12 +15,18 @@ "step": { "user": { "data": { - "api_key": "API\u30ad\u30fc" + "api_key": "API\u30ad\u30fc", + "base": "\u57fa\u672c\u901a\u8ca8" }, "data_description": { "base": "\u7c73\u30c9\u30eb\u4ee5\u5916\u306e\u57fa\u672c\u901a\u8ca8\u3092\u4f7f\u7528\u3059\u308b\u306b\u306f\u3001[\u6709\u6599\u30d7\u30e9\u30f3] ({signup}) \u304c\u5fc5\u8981\u3067\u3059\u3002" } } } + }, + "issues": { + "deprecated_yaml": { + "title": "Open Exchange Rates YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/es.json b/homeassistant/components/overkiz/translations/es.json index 5503525ac58..a2b04f05cf9 100644 --- a/homeassistant/components/overkiz/translations/es.json +++ b/homeassistant/components/overkiz/translations/es.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "server_in_maintenance": "El servidor est\u00e1 fuera de servicio por mantenimiento", + "server_in_maintenance": "El servidor est\u00e1 ca\u00eddo por mantenimiento", "too_many_attempts": "Demasiados intentos con un token no v\u00e1lido, prohibido temporalmente", "too_many_requests": "Demasiadas solicitudes, vuelve a intentarlo m\u00e1s tarde", "unknown": "Error inesperado" @@ -22,7 +22,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "La plataforma Overkiz es utilizada por varios proveedores como Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) y Atlantic (Cozytouch). Introduce las credenciales de tu aplicaci\u00f3n y selecciona tu concentrador." + "description": "La plataforma Overkiz es usada por varios proveedores como Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) y Atlantic (Cozytouch). Introduce las credenciales de tu aplicaci\u00f3n y selecciona tu concentrador." } } } diff --git a/homeassistant/components/ovo_energy/translations/es.json b/homeassistant/components/ovo_energy/translations/es.json index 0af57427980..a28e441d9d8 100644 --- a/homeassistant/components/ovo_energy/translations/es.json +++ b/homeassistant/components/ovo_energy/translations/es.json @@ -11,7 +11,7 @@ "data": { "password": "Contrase\u00f1a" }, - "description": "La autenticaci\u00f3n fall\u00f3 para OVO Energy. Introduce tus credenciales actuales.", + "description": "La autenticaci\u00f3n fall\u00f3 para OVO Energy. Por favor, introduce tus credenciales actuales.", "title": "Reautenticaci\u00f3n" }, "user": { diff --git a/homeassistant/components/plaato/translations/es.json b/homeassistant/components/plaato/translations/es.json index 23acbb80c6c..e65cf7e82d5 100644 --- a/homeassistant/components/plaato/translations/es.json +++ b/homeassistant/components/plaato/translations/es.json @@ -20,7 +20,7 @@ "token": "Pega el token de autenticaci\u00f3n aqu\u00ed", "use_webhook": "Usar webhook" }, - "description": "Para poder consultar la API, se requiere un `auth_token` que se puede obtener siguiendo [estas](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instrucciones \n\nDispositivo seleccionado: ** {device_type} ** \n\nSi prefieres utilizar el m\u00e9todo de webhook incorporado (solo Airlock), marca la casilla a continuaci\u00f3n y deja el token de autenticaci\u00f3n en blanco", + "description": "Para poder consultar la API, se requiere un `auth_token` que se puede obtener siguiendo [estas](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instrucciones \n\nDispositivo seleccionado: **{device_type}** \n\nSi prefieres usar el m\u00e9todo de webhook incorporado (solo Airlock), por favor, marca la casilla a continuaci\u00f3n y deja el token de autenticaci\u00f3n en blanco", "title": "Seleccionar el m\u00e9todo API" }, "user": { @@ -33,7 +33,7 @@ }, "webhook": { "description": "Para enviar eventos a Home Assistant, deber\u00e1s configurar la funci\u00f3n de webhook en Plaato Airlock. \n\nCompleta la siguiente informaci\u00f3n: \n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST \n\nConsulta [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s detalles.", - "title": "Webhook a utilizar" + "title": "Webhook a usar" } } }, diff --git a/homeassistant/components/plugwise/translations/es.json b/homeassistant/components/plugwise/translations/es.json index 50e440f6d6a..fed26040384 100644 --- a/homeassistant/components/plugwise/translations/es.json +++ b/homeassistant/components/plugwise/translations/es.json @@ -30,7 +30,7 @@ "port": "Puerto", "username": "Nombre de usuario Smile" }, - "description": "Por favor, introduce:", + "description": "Por favor, introduce", "title": "Conectar a Smile" } } diff --git a/homeassistant/components/point/translations/es.json b/homeassistant/components/point/translations/es.json index c0a86f70d55..dcde44ad273 100644 --- a/homeassistant/components/point/translations/es.json +++ b/homeassistant/components/point/translations/es.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", - "external_setup": "Point se ha configurado correctamente a partir de otro flujo.", + "external_setup": "Point se ha configurado correctamente desde otro flujo.", "no_flows": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, @@ -11,13 +11,13 @@ "default": "Autenticado correctamente" }, "error": { - "follow_link": "Por favor, sigue el enlace y autent\u00edcate antes de presionar Enviar", + "follow_link": "Por favor, sigue el enlace y autent\u00edcate antes de pulsar Enviar", "no_token": "Token de acceso no v\u00e1lido" }, "step": { "auth": { - "description": "Por favor, sigue el enlace a continuaci\u00f3n y **Acepta** el acceso a tu cuenta Minut, luego regresa y presiona **Enviar** a continuaci\u00f3n. \n\n[Enlace]({authorization_url})", - "title": "Autenticaci\u00f3n con Point" + "description": "Por favor, sigue el enlace a continuaci\u00f3n y **Acepta** el acceso a tu cuenta Minut, luego regresa y pulsa **Enviar** a continuaci\u00f3n. \n\n[Enlace]({authorization_url})", + "title": "Autenticar Point" }, "user": { "data": { diff --git a/homeassistant/components/qingping/translations/ca.json b/homeassistant/components/qingping/translations/ca.json new file mode 100644 index 00000000000..c121ff7408c --- /dev/null +++ b/homeassistant/components/qingping/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "not_supported": "Dispositiu no compatible" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/de.json b/homeassistant/components/qingping/translations/de.json new file mode 100644 index 00000000000..4c5720ec6fb --- /dev/null +++ b/homeassistant/components/qingping/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "not_supported": "Ger\u00e4t nicht unterst\u00fctzt" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/el.json b/homeassistant/components/qingping/translations/el.json new file mode 100644 index 00000000000..cdb57c8ac1b --- /dev/null +++ b/homeassistant/components/qingping/translations/el.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "not_supported": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/es.json b/homeassistant/components/qingping/translations/es.json new file mode 100644 index 00000000000..ae0ab01acdf --- /dev/null +++ b/homeassistant/components/qingping/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red", + "not_supported": "Dispositivo no compatible" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/et.json b/homeassistant/components/qingping/translations/et.json new file mode 100644 index 00000000000..9d3874dde88 --- /dev/null +++ b/homeassistant/components/qingping/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud", + "not_supported": "Seda seadet ei toetata" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadaistada {name}?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/fi.json b/homeassistant/components/qingping/translations/fi.json new file mode 100644 index 00000000000..5c7f611a2cd --- /dev/null +++ b/homeassistant/components/qingping/translations/fi.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "Laitetta ei tueta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/fr.json b/homeassistant/components/qingping/translations/fr.json new file mode 100644 index 00000000000..8ddb4af4dbc --- /dev/null +++ b/homeassistant/components/qingping/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "not_supported": "Appareil non pris en charge" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, + "user": { + "data": { + "address": "Appareil" + }, + "description": "S\u00e9lectionnez l'appareil \u00e0 configurer" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/hu.json b/homeassistant/components/qingping/translations/hu.json new file mode 100644 index 00000000000..8a1bc9a1c42 --- /dev/null +++ b/homeassistant/components/qingping/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r be van \u00e1ll\u00edtva", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nincs felder\u00edtett eszk\u00f6z a h\u00e1l\u00f3zaton", + "not_supported": "Eszk\u00f6z nem t\u00e1mogatott" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Be\u00e1ll\u00edtja a k\u00f6vetkez\u0151t: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lasszon ki egy eszk\u00f6zt a be\u00e1ll\u00edt\u00e1shoz" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/id.json b/homeassistant/components/qingping/translations/id.json new file mode 100644 index 00000000000..573eb39ed15 --- /dev/null +++ b/homeassistant/components/qingping/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "not_supported": "Perangkat tidak didukung" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/it.json b/homeassistant/components/qingping/translations/it.json new file mode 100644 index 00000000000..7784ed3a240 --- /dev/null +++ b/homeassistant/components/qingping/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "not_supported": "Dispositivo non supportato" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Seleziona un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/ja.json b/homeassistant/components/qingping/translations/ja.json new file mode 100644 index 00000000000..fe1c5746cda --- /dev/null +++ b/homeassistant/components/qingping/translations/ja.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "not_supported": "\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/no.json b/homeassistant/components/qingping/translations/no.json new file mode 100644 index 00000000000..0bf8b1695ec --- /dev/null +++ b/homeassistant/components/qingping/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "not_supported": "Enheten st\u00f8ttes ikke" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du konfigurere {name}?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/pt-BR.json b/homeassistant/components/qingping/translations/pt-BR.json new file mode 100644 index 00000000000..0da7639fa2a --- /dev/null +++ b/homeassistant/components/qingping/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "not_supported": "Dispositivo n\u00e3o suportado" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/ru.json b/homeassistant/components/qingping/translations/ru.json new file mode 100644 index 00000000000..887499e5f2e --- /dev/null +++ b/homeassistant/components/qingping/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "not_supported": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/zh-Hant.json b/homeassistant/components/qingping/translations/zh-Hant.json new file mode 100644 index 00000000000..64ae1f19094 --- /dev/null +++ b/homeassistant/components/qingping/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "not_supported": "\u88dd\u7f6e\u4e0d\u652f\u63f4" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/ja.json b/homeassistant/components/roon/translations/ja.json index 12a1b074916..7f6adbd1f04 100644 --- a/homeassistant/components/roon/translations/ja.json +++ b/homeassistant/components/roon/translations/ja.json @@ -18,6 +18,9 @@ "link": { "description": "Roon\u3067Home Assistant\u3092\u8a8d\u8a3c\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\u9001\u4fe1(submit) \u3092\u30af\u30ea\u30c3\u30af\u3057\u305f\u5f8c\u3001Roon Core\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3067\u3001\u8a2d\u5b9a(Settings )\u3092\u958b\u304d\u3001\u6a5f\u80fd\u62e1\u5f35\u30bf\u30d6(extensions tab)\u3067Home Assistant\u3092\u6709\u52b9(enable )\u306b\u3057\u307e\u3059\u3002", "title": "Roon\u3067HomeAssistant\u3092\u8a8d\u8a3c\u3059\u308b" + }, + "user": { + "other": "\u7a7a" } } } diff --git a/homeassistant/components/rtsp_to_webrtc/translations/es.json b/homeassistant/components/rtsp_to_webrtc/translations/es.json index 5a066d6645e..ea3e8c4afc1 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/es.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/es.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "server_failure": "El servidor RTSPtoWebRTC devolvi\u00f3 un error. Consulta los registros para obtener m\u00e1s informaci\u00f3n.", - "server_unreachable": "No se puede comunicar con el servidor RTSPtoWebRTC. Consulta los registros para obtener m\u00e1s informaci\u00f3n.", + "server_failure": "El servidor RTSPtoWebRTC devolvi\u00f3 un error. Revisa los registros para obtener m\u00e1s informaci\u00f3n.", + "server_unreachable": "No se puede comunicar con el servidor RTSPtoWebRTC. Revisa los registros para obtener m\u00e1s informaci\u00f3n.", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { "invalid_url": "Debe ser una URL de servidor RTSPtoWebRTC v\u00e1lida, por ejemplo, https://example.com", - "server_failure": "El servidor RTSPtoWebRTC devolvi\u00f3 un error. Consulta los registros para obtener m\u00e1s informaci\u00f3n.", - "server_unreachable": "No se puede comunicar con el servidor RTSPtoWebRTC. Consulta los registros para obtener m\u00e1s informaci\u00f3n." + "server_failure": "El servidor RTSPtoWebRTC devolvi\u00f3 un error. Revisa los registros para obtener m\u00e1s informaci\u00f3n.", + "server_unreachable": "No se puede comunicar con el servidor RTSPtoWebRTC. Revisa los registros para obtener m\u00e1s informaci\u00f3n." }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/samsungtv/translations/es.json b/homeassistant/components/samsungtv/translations/es.json index 6a5487175e7..b102c85b2f3 100644 --- a/homeassistant/components/samsungtv/translations/es.json +++ b/homeassistant/components/samsungtv/translations/es.json @@ -6,7 +6,7 @@ "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a esta TV Samsung. Verifica la configuraci\u00f3n del Administrador de dispositivos externos de tu TV para autorizar a Home Assistant.", "cannot_connect": "No se pudo conectar", "id_missing": "Este dispositivo Samsung no tiene un n\u00famero de serie.", - "not_supported": "Este dispositivo Samsung no es compatible actualmente.", + "not_supported": "Este dispositivo Samsung no es compatible por el momento.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/schedule/translations/el.json b/homeassistant/components/schedule/translations/el.json index 2c6a17ab298..b0a7c066a53 100644 --- a/homeassistant/components/schedule/translations/el.json +++ b/homeassistant/components/schedule/translations/el.json @@ -1,3 +1,9 @@ { + "state": { + "_": { + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc" + } + }, "title": "\u03a0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1" } \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/fi.json b/homeassistant/components/schedule/translations/fi.json new file mode 100644 index 00000000000..927dd806b5c --- /dev/null +++ b/homeassistant/components/schedule/translations/fi.json @@ -0,0 +1,3 @@ +{ + "title": "Aikataulu" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/ja.json b/homeassistant/components/schedule/translations/ja.json new file mode 100644 index 00000000000..690284b8afa --- /dev/null +++ b/homeassistant/components/schedule/translations/ja.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u30aa\u30d5", + "on": "\u30aa\u30f3" + } + }, + "title": "\u30b9\u30b1\u30b8\u30e5\u30fc\u30eb" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/no.json b/homeassistant/components/schedule/translations/no.json new file mode 100644 index 00000000000..7dfb9cae4ea --- /dev/null +++ b/homeassistant/components/schedule/translations/no.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Av", + "on": "P\u00e5" + } + }, + "title": "Timeplan" +} \ No newline at end of file diff --git a/homeassistant/components/schedule/translations/ru.json b/homeassistant/components/schedule/translations/ru.json new file mode 100644 index 00000000000..26a5aeac364 --- /dev/null +++ b/homeassistant/components/schedule/translations/ru.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + } + }, + "title": "\u0420\u0430\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0435" +} \ No newline at end of file diff --git a/homeassistant/components/scrape/translations/es.json b/homeassistant/components/scrape/translations/es.json index f5c07aa30b4..d88197473d3 100644 --- a/homeassistant/components/scrape/translations/es.json +++ b/homeassistant/components/scrape/translations/es.json @@ -25,10 +25,10 @@ "attribute": "Obtener el valor de un atributo en la etiqueta seleccionada", "authentication": "Tipo de autenticaci\u00f3n HTTP. Puede ser basic o digest", "device_class": "El tipo/clase del sensor para establecer el icono en el frontend", - "headers": "Cabeceras a utilizar para la petici\u00f3n web", - "index": "Define cu\u00e1l de los elementos devueltos por el selector CSS usar", + "headers": "Cabeceras a usar para la petici\u00f3n web", + "index": "Define cu\u00e1l de los elementos devueltos por el selector CSS se va a usar", "resource": "La URL del sitio web que contiene el valor.", - "select": "Define qu\u00e9 etiqueta buscar. Consulta los selectores CSS de Beautifulsoup para obtener m\u00e1s informaci\u00f3n.", + "select": "Define qu\u00e9 etiqueta buscar. Revisa los selectores CSS de Beautifulsoup para obtener m\u00e1s informaci\u00f3n.", "state_class": "El state_class del sensor", "value_template": "Define una plantilla para obtener el estado del sensor", "verify_ssl": "Habilita/deshabilita la verificaci\u00f3n del certificado SSL/TLS, por ejemplo, si est\u00e1 autofirmado" @@ -59,10 +59,10 @@ "attribute": "Obtener el valor de un atributo en la etiqueta seleccionada", "authentication": "Tipo de autenticaci\u00f3n HTTP. Puede ser basic o digest", "device_class": "El tipo/clase del sensor para establecer el icono en el frontend", - "headers": "Cabeceras a utilizar para la petici\u00f3n web", - "index": "Define cu\u00e1l de los elementos devueltos por el selector CSS usar", + "headers": "Cabeceras a usar para la petici\u00f3n web", + "index": "Define cu\u00e1l de los elementos devueltos por el selector CSS se va a usar", "resource": "La URL del sitio web que contiene el valor.", - "select": "Define qu\u00e9 etiqueta buscar. Consulta los selectores CSS de Beautifulsoup para obtener m\u00e1s informaci\u00f3n.", + "select": "Define qu\u00e9 etiqueta buscar. Revisa los selectores CSS de Beautifulsoup para obtener m\u00e1s informaci\u00f3n.", "state_class": "El state_class del sensor", "value_template": "Define una plantilla para obtener el estado del sensor", "verify_ssl": "Habilita/deshabilita la verificaci\u00f3n del certificado SSL/TLS, por ejemplo, si est\u00e1 autofirmado" diff --git a/homeassistant/components/select/translations/es.json b/homeassistant/components/select/translations/es.json index b2066b4606d..a2927327151 100644 --- a/homeassistant/components/select/translations/es.json +++ b/homeassistant/components/select/translations/es.json @@ -7,7 +7,7 @@ "selected_option": "Opci\u00f3n de {entity_name} seleccionada actualmente" }, "trigger_type": { - "current_option_changed": "Opci\u00f3n de {entity_name} cambiada" + "current_option_changed": "Una opci\u00f3n de {entity_name} cambi\u00f3" } }, "title": "Seleccionar" diff --git a/homeassistant/components/sensor/translations/es.json b/homeassistant/components/sensor/translations/es.json index fd8a1581530..c61b0b7927a 100644 --- a/homeassistant/components/sensor/translations/es.json +++ b/homeassistant/components/sensor/translations/es.json @@ -1,62 +1,62 @@ { "device_automation": { "condition_type": { - "is_apparent_power": "Potencia aparente actual de {entity_name}", - "is_battery_level": "Nivel de bater\u00eda actual de {entity_name}", - "is_carbon_dioxide": "Nivel actual en {entity_name} de concentraci\u00f3n de di\u00f3xido de carbono", - "is_carbon_monoxide": "Nivel actual en {entity_name} de concentraci\u00f3n de mon\u00f3xido de carbono", - "is_current": "Corriente actual de {entity_name}", - "is_energy": "Energ\u00eda actual de {entity_name}", - "is_frequency": "Frecuencia de {entity_name} actual", - "is_gas": "Gas actual de {entity_name}", - "is_humidity": "Humedad actual de {entity_name}", - "is_illuminance": "Luminosidad actual de {entity_name}", - "is_nitrogen_dioxide": "Nivel actual de concentraci\u00f3n de di\u00f3xido de nitr\u00f3geno de {entity_name}", - "is_nitrogen_monoxide": "Nivel actual de concentraci\u00f3n de mon\u00f3xido de nitr\u00f3geno de {entity_name}", - "is_nitrous_oxide": "Nivel actual de concentraci\u00f3n de \u00f3xido nitroso de {entity_name}", - "is_ozone": "Nivel actual de concentraci\u00f3n de ozono de {entity_name}", - "is_pm1": "Nivel actual de concentraci\u00f3n de PM1 de {entity_name}", - "is_pm10": "Nivel actual de concentraci\u00f3n de PM10 de {entity_name}", - "is_pm25": "Nivel actual de concentraci\u00f3n de PM2.5 de {entity_name}", - "is_power": "Potencia actual de {entity_name}", - "is_power_factor": "Factor de potencia actual de {entity_name}", - "is_pressure": "Presi\u00f3n actual de {entity_name}", - "is_reactive_power": "Potencia reactiva actual de {entity_name}", - "is_signal_strength": "Intensidad de la se\u00f1al actual de {entity_name}", - "is_sulphur_dioxide": "Nivel actual de concentraci\u00f3n de di\u00f3xido de azufre de {entity_name}", - "is_temperature": "Temperatura actual de {entity_name}", - "is_value": "Valor actual de {entity_name}", - "is_volatile_organic_compounds": "Nivel actual de concentraci\u00f3n de compuestos org\u00e1nicos vol\u00e1tiles de {entity_name}", - "is_voltage": "Voltaje actual de {entity_name}" + "is_apparent_power": "La potencia aparente actual de {entity_name}", + "is_battery_level": "El nivel de bater\u00eda actual de {entity_name}", + "is_carbon_dioxide": "El nivel de la concentraci\u00f3n de di\u00f3xido de carbono actual de {entity_name}", + "is_carbon_monoxide": "El nivel de la concentraci\u00f3n de mon\u00f3xido de carbono actual de {entity_name}", + "is_current": "La intensidad de corriente actual de {entity_name}", + "is_energy": "La energ\u00eda actual de {entity_name}", + "is_frequency": "La frecuencia actual de {entity_name}", + "is_gas": "El gas actual de {entity_name}", + "is_humidity": "La humedad actual de {entity_name}", + "is_illuminance": "La luminosidad actual de {entity_name}", + "is_nitrogen_dioxide": "El nivel de la concentraci\u00f3n de di\u00f3xido de nitr\u00f3geno actual de {entity_name}", + "is_nitrogen_monoxide": "El nivel de la concentraci\u00f3n de mon\u00f3xido de nitr\u00f3geno actual de {entity_name}", + "is_nitrous_oxide": "El nivel de la concentraci\u00f3n de \u00f3xido nitroso actual de {entity_name}", + "is_ozone": "El nivel de la concentraci\u00f3n de ozono actual de {entity_name}", + "is_pm1": "El nivel de la concentraci\u00f3n de PM1 actual de {entity_name}", + "is_pm10": "El nivel de la concentraci\u00f3n de PM10 actual de {entity_name}", + "is_pm25": "El nivel de la concentraci\u00f3n de PM2.5 actual de {entity_name}", + "is_power": "La potencia actual de {entity_name}", + "is_power_factor": "El factor de potencia actual de {entity_name}", + "is_pressure": "La presi\u00f3n actual de {entity_name}", + "is_reactive_power": "La potencia reactiva actual de {entity_name}", + "is_signal_strength": "La intensidad de la se\u00f1al actual de {entity_name}", + "is_sulphur_dioxide": "El nivel de la concentraci\u00f3n de di\u00f3xido de azufre actual de {entity_name}", + "is_temperature": "La temperatura actual de {entity_name}", + "is_value": "El valor actual de {entity_name}", + "is_volatile_organic_compounds": "El nivel de la concentraci\u00f3n de compuestos org\u00e1nicos vol\u00e1tiles actual de {entity_name}", + "is_voltage": "El voltaje actual de {entity_name}" }, "trigger_type": { - "apparent_power": "{entity_name} ha cambiado de potencia aparente", + "apparent_power": "La potencia aparente de {entity_name} cambia", "battery_level": "El nivel de bater\u00eda de {entity_name} cambia", - "carbon_dioxide": "{entity_name} cambios en la concentraci\u00f3n de di\u00f3xido de carbono", - "carbon_monoxide": "{entity_name} cambios en la concentraci\u00f3n de mon\u00f3xido de carbono", - "current": "Cambio de corriente en {entity_name}", - "energy": "Cambio de energ\u00eda en {entity_name}", - "frequency": "{entity_name} ha cambiado de frecuencia", - "gas": "{entity_name} ha hambiado gas", + "carbon_dioxide": "La concentraci\u00f3n de di\u00f3xido de carbono de {entity_name} cambia", + "carbon_monoxide": "La concentraci\u00f3n de mon\u00f3xido de carbono de {entity_name} cambia", + "current": "La intensidad de corriente de {entity_name} cambia", + "energy": "La energ\u00eda de {entity_name} cambia", + "frequency": "La frecuencia de {entity_name} cambia", + "gas": "El gas de {entity_name} cambia", "humidity": "La humedad de {entity_name} cambia", "illuminance": "La luminosidad de {entity_name} cambia", - "nitrogen_dioxide": "{entity_name} ha cambiado en la concentraci\u00f3n de di\u00f3xido de nitr\u00f3geno", - "nitrogen_monoxide": "{entity_name} ha cambiado en la concentraci\u00f3n de mon\u00f3xido de nitr\u00f3geno", - "nitrous_oxide": "{entity_name} ha cambiado en la concentraci\u00f3n de \u00f3xido nitroso", - "ozone": "{entity_name} ha cambiado en la concentraci\u00f3n de ozono", - "pm1": "{entity_name} ha cambiado en la concentraci\u00f3n de PM1", - "pm10": "{entity_name} ha cambiado en la concentraci\u00f3n de PM10", - "pm25": "{entity_name} ha cambiado en la concentraci\u00f3n de PM2.5", + "nitrogen_dioxide": "La concentraci\u00f3n de di\u00f3xido de nitr\u00f3geno de {entity_name} cambia", + "nitrogen_monoxide": "La concentraci\u00f3n de mon\u00f3xido de nitr\u00f3geno de {entity_name} cambia", + "nitrous_oxide": "La concentraci\u00f3n de \u00f3xido nitroso de {entity_name} cambia", + "ozone": "La concentraci\u00f3n de ozono de {entity_name} cambia", + "pm1": "La concentraci\u00f3n de PM1 de {entity_name} cambia", + "pm10": "La concentraci\u00f3n de PM10 de {entity_name} cambia", + "pm25": "La concentraci\u00f3n de PM2.5 de {entity_name} cambia", "power": "La potencia de {entity_name} cambia", - "power_factor": "Cambio de factor de potencia en {entity_name}", + "power_factor": "El factor de potencia de {entity_name} cambia", "pressure": "La presi\u00f3n de {entity_name} cambia", - "reactive_power": "{entity_name} ha cambiado de potencia reactiva", + "reactive_power": "La potencia reactiva de {entity_name} cambia", "signal_strength": "La intensidad de se\u00f1al de {entity_name} cambia", - "sulphur_dioxide": "{entity_name} ha cambiado en la concentraci\u00f3n de di\u00f3xido de azufre", + "sulphur_dioxide": "La concentraci\u00f3n de di\u00f3xido de azufre de {entity_name} cambia", "temperature": "La temperatura de {entity_name} cambia", "value": "El valor de {entity_name} cambia", - "volatile_organic_compounds": "{entity_name} ha cambiado la concentraci\u00f3n de compuestos org\u00e1nicos vol\u00e1tiles", - "voltage": "Cambio de voltaje en {entity_name}" + "volatile_organic_compounds": "La concentraci\u00f3n de compuestos org\u00e1nicos vol\u00e1tiles de {entity_name} cambia", + "voltage": "El voltaje de {entity_name} cambia" } }, "state": { diff --git a/homeassistant/components/senz/translations/es.json b/homeassistant/components/senz/translations/es.json index e08ed208ac6..406d6b64017 100644 --- a/homeassistant/components/senz/translations/es.json +++ b/homeassistant/components/senz/translations/es.json @@ -5,7 +5,7 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "oauth_error": "Se han recibido datos de token no v\u00e1lidos." }, "create_entry": { diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index 585ec778f7c..f1ad9c7ffda 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/translations/es.json @@ -13,7 +13,7 @@ "flow_title": "{name}", "step": { "confirm_discovery": { - "description": "\u00bfQuieres configurar {model} en {host}? \n\nLos dispositivos alimentados por bater\u00eda que est\u00e1n protegidos con contrase\u00f1a deben despertarse antes de continuar con la configuraci\u00f3n.\nLos dispositivos que funcionan con bater\u00eda que no est\u00e1n protegidos con contrase\u00f1a se agregar\u00e1n cuando el dispositivo se despierte. Puedes activar manualmente el dispositivo ahora con un bot\u00f3n del mismo o esperar a la pr\u00f3xima actualizaci\u00f3n de datos del dispositivo." + "description": "\u00bfQuieres configurar {model} en {host}? \n\nLos dispositivos alimentados por bater\u00eda que est\u00e1n protegidos por contrase\u00f1a deben despertarse antes de continuar con la configuraci\u00f3n.\nLos dispositivos que funcionan con bater\u00eda que no est\u00e1n protegidos por contrase\u00f1a se a\u00f1adir\u00e1n cuando el dispositivo se despierte. Puedes activar manualmente el dispositivo ahora con un bot\u00f3n del mismo o esperar a la pr\u00f3xima actualizaci\u00f3n de datos del dispositivo." }, "credentials": { "data": { diff --git a/homeassistant/components/simplisafe/translations/ca.json b/homeassistant/components/simplisafe/translations/ca.json index 9f44ba9ade9..13ec9c79d38 100644 --- a/homeassistant/components/simplisafe/translations/ca.json +++ b/homeassistant/components/simplisafe/translations/ca.json @@ -35,7 +35,7 @@ "password": "Contrasenya", "username": "Nom d'usuari" }, - "description": "SimpliSafe autentica els seus usuaris a trav\u00e9s de la seva aplicaci\u00f3 web. A causa de les limitacions t\u00e8cniques, hi ha un pas manual al final d'aquest proc\u00e9s; assegura't de llegir la [documentaci\u00f3](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) abans de comen\u00e7ar.\n\nQuan ja estiguis, fes clic [aqu\u00ed]({url}) per obrir l'aplicaci\u00f3 web de SimpliSafe i introdueix les teves credencials. Quan el proc\u00e9s s'hagi completat, torna aqu\u00ed i introdueix, a sota, el codi d'autoritzaci\u00f3 de l'URL de l'aplicaci\u00f3 web de SimpliSafe." + "description": "SimpliSafe autentica els seus usuaris a trav\u00e9s de la seva aplicaci\u00f3 web. A causa de les limitacions t\u00e8cniques, hi ha un pas manual al final d'aquest proc\u00e9s; assegura't de llegir la [documentaci\u00f3](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) abans de comen\u00e7ar.\n\nQuan ja estiguis, fes clic [aqu\u00ed]({url}) per obrir l'aplicaci\u00f3 web de SimpliSafe i introdueix les teves credencials. Si ja has iniciat sessi\u00f3 a SimpliSafe a trav\u00e9s del navegador, potser hauries d'obrir una nova pestanya i copiar-hi l'URL de dalt.\n\nQuan el proc\u00e9s s'hagi completat, torna aqu\u00ed i introdueix, a sota, el codi d'autoritzaci\u00f3 de l'URL `com.simplisafe.mobile`." } } }, diff --git a/homeassistant/components/simplisafe/translations/es.json b/homeassistant/components/simplisafe/translations/es.json index 842a2245fe3..408b595d329 100644 --- a/homeassistant/components/simplisafe/translations/es.json +++ b/homeassistant/components/simplisafe/translations/es.json @@ -35,7 +35,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "SimpliSafe autentica a los usuarios a trav\u00e9s de su aplicaci\u00f3n web. Debido a limitaciones t\u00e9cnicas, existe un paso manual al final de este proceso; aseg\u00farate de leer la [documentaci\u00f3n](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de comenzar. \n\nCuando est\u00e9s listo, haz clic [aqu\u00ed]({url}) para abrir la aplicaci\u00f3n web SimpliSafe e introduce tus credenciales. Si ya iniciaste sesi\u00f3n en SimpliSafe en tu navegador, es posible que desees abrir una nueva pesta\u00f1a y luego copiar/pegar la URL anterior en esa pesta\u00f1a. \n\nCuando se complete el proceso, regresa aqu\u00ed e introduce el c\u00f3digo de autorizaci\u00f3n de la URL `com.simplisafe.mobile`." + "description": "SimpliSafe autentica a los usuarios a trav\u00e9s de su aplicaci\u00f3n web. Debido a limitaciones t\u00e9cnicas, existe un paso manual al final de este proceso; por favor, aseg\u00farate de leer la [documentaci\u00f3n](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) antes de comenzar. \n\nCuando est\u00e9s listo, haz clic [aqu\u00ed]({url}) para abrir la aplicaci\u00f3n web SimpliSafe e introduce tus credenciales. Si ya iniciaste sesi\u00f3n en SimpliSafe en tu navegador, es posible que desees abrir una nueva pesta\u00f1a y luego copiar/pegar la URL anterior en esa pesta\u00f1a. \n\nCuando se complete el proceso, regresa aqu\u00ed e introduce el c\u00f3digo de autorizaci\u00f3n de la URL `com.simplisafe.mobile`." } } }, diff --git a/homeassistant/components/simplisafe/translations/it.json b/homeassistant/components/simplisafe/translations/it.json index 6f2a61e2df4..997f376916d 100644 --- a/homeassistant/components/simplisafe/translations/it.json +++ b/homeassistant/components/simplisafe/translations/it.json @@ -35,7 +35,7 @@ "password": "Password", "username": "Nome utente" }, - "description": "SimpliSafe autentica gli utenti tramite la sua app web. A causa di limitazioni tecniche, alla fine di questo processo \u00e8 previsto un passaggio manuale; assicurati, prima di iniziare, di leggere la [documentazione](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code). \n\nQuando sei pronto, fai clic [qui]({url}) per aprire l'app Web SimpliSafe e inserire le tue credenziali. Al termine del processo, ritorna qui e inserisci il codice di autorizzazione dall'URL dell'app Web SimpliSafe." + "description": "SimpliSafe autentica gli utenti tramite la sua app web. A causa di limitazioni tecniche, alla fine di questo processo \u00e8 previsto un passaggio manuale; assicurati, prima di iniziare, di leggere la [documentazione](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code). \n\nQuando sei pronto, fai clic [qui]({url}) per aprire l'app web di SimpliSafe e inserisci le tue credenziali. Se hai gi\u00e0 effettuato l'accesso a SimpliSafe nel tuo browser, puoi aprire una nuova scheda e copiare/incollare l'URL sopra indicato in quella nuova scheda.\n\nAl termine del processo, ritorna qui e inserisci il codice di autorizzazione dall'URL `com.simplisafe.mobile`." } } }, diff --git a/homeassistant/components/simplisafe/translations/ja.json b/homeassistant/components/simplisafe/translations/ja.json index 4e65fe8a057..38e383a75af 100644 --- a/homeassistant/components/simplisafe/translations/ja.json +++ b/homeassistant/components/simplisafe/translations/ja.json @@ -9,6 +9,7 @@ "error": { "identifier_exists": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u767b\u9332\u3055\u308c\u3066\u3044\u307e\u3059", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_auth_code_length": "SimpliSafe\u306e\u8a8d\u8a3c\u30b3\u30fc\u30c9\u306f45\u6587\u5b57\u3067\u3059", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, "progress": { diff --git a/homeassistant/components/smappee/translations/es.json b/homeassistant/components/smappee/translations/es.json index ebffb459169..15e8ed37363 100644 --- a/homeassistant/components/smappee/translations/es.json +++ b/homeassistant/components/smappee/translations/es.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured_device": "El dispositivo ya est\u00e1 configurado", - "already_configured_local_device": "Los dispositivos locales ya est\u00e1n configurados. Elim\u00ednelos primero antes de configurar un dispositivo en la nube.", + "already_configured_local_device": "Los dispositivos locales ya est\u00e1n configurados. Por favor, elim\u00ednalos primero antes de configurar un dispositivo en la nube.", "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "cannot_connect": "No se pudo conectar", "invalid_mdns": "Dispositivo no compatible con la integraci\u00f3n de Smappee.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/smartthings/translations/es.json b/homeassistant/components/smartthings/translations/es.json index b77cb803205..5e05cbf711e 100644 --- a/homeassistant/components/smartthings/translations/es.json +++ b/homeassistant/components/smartthings/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "invalid_webhook_url": "Home Assistant no est\u00e1 configurado correctamente para recibir actualizaciones de SmartThings. La URL del webhook no es v\u00e1lida:\n> {webhook_url} \n\nActualiza tu configuraci\u00f3n seg\u00fan las [instrucciones]({component_url}), reinicia Home Assistant y vuelve a intentarlo.", + "invalid_webhook_url": "Home Assistant no est\u00e1 configurado correctamente para recibir actualizaciones de SmartThings. La URL del webhook no es v\u00e1lida:\n> {webhook_url} \n\nPor favor, actualiza tu configuraci\u00f3n seg\u00fan las [instrucciones]({component_url}), reinicia Home Assistant e int\u00e9ntalo de nuevo.", "no_available_locations": "No hay Ubicaciones SmartThings disponibles para configurar en Home Assistant." }, "error": { @@ -9,7 +9,7 @@ "token_forbidden": "El token no tiene los \u00e1mbitos de OAuth necesarios.", "token_invalid_format": "El token debe estar en formato UID/GUID", "token_unauthorized": "El token no es v\u00e1lido o ya no est\u00e1 autorizado.", - "webhook_error": "SmartThings no pudo validar la URL del webhook. Aseg\u00farate de que la URL del webhook sea accesible desde Internet y vuelve a intentarlo." + "webhook_error": "SmartThings no pudo validar la URL del webhook. Por favor, aseg\u00farate de que la URL del webhook sea accesible desde Internet e int\u00e9ntalo de nuevo." }, "step": { "authorize": { @@ -26,7 +26,7 @@ "data": { "location_id": "Ubicaci\u00f3n" }, - "description": "Selecciona la Ubicaci\u00f3n SmartThings que quieres a\u00f1adir a Home Assistant. Se abrir\u00e1 una nueva ventana que te pedir\u00e1 que inicies sesi\u00f3n y autorices la instalaci\u00f3n de la integraci\u00f3n de Home Assistant en la ubicaci\u00f3n seleccionada.", + "description": "Por favor, selecciona la Ubicaci\u00f3n SmartThings que quieres a\u00f1adir a Home Assistant. Se abrir\u00e1 una nueva ventana que te pedir\u00e1 que inicies sesi\u00f3n y autorices la instalaci\u00f3n de la integraci\u00f3n de Home Assistant en la ubicaci\u00f3n seleccionada.", "title": "Seleccionar Ubicaci\u00f3n" }, "user": { diff --git a/homeassistant/components/solarlog/translations/es.json b/homeassistant/components/solarlog/translations/es.json index 0324b8ee5e6..195b623a1d2 100644 --- a/homeassistant/components/solarlog/translations/es.json +++ b/homeassistant/components/solarlog/translations/es.json @@ -11,7 +11,7 @@ "user": { "data": { "host": "Host", - "name": "El prefijo que se utilizar\u00e1 para tus sensores Solar-Log" + "name": "El prefijo que se usar\u00e1 para tus sensores Solar-Log" }, "title": "Define tu conexi\u00f3n Solar-Log" } diff --git a/homeassistant/components/spotify/translations/es.json b/homeassistant/components/spotify/translations/es.json index edb2ff4bf82..e2e3cf927a7 100644 --- a/homeassistant/components/spotify/translations/es.json +++ b/homeassistant/components/spotify/translations/es.json @@ -3,7 +3,7 @@ "abort": { "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "La integraci\u00f3n de Spotify no est\u00e1 configurada. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "reauth_account_mismatch": "La cuenta de Spotify con la que est\u00e1s autenticado, no coincide con la cuenta necesaria para re-autenticaci\u00f3n." }, "create_entry": { diff --git a/homeassistant/components/sql/translations/es.json b/homeassistant/components/sql/translations/es.json index 427510a3f1d..aefea983a01 100644 --- a/homeassistant/components/sql/translations/es.json +++ b/homeassistant/components/sql/translations/es.json @@ -19,7 +19,7 @@ }, "data_description": { "column": "Columna de respuesta de la consulta para presentar como estado", - "db_url": "URL de la base de datos, d\u00e9jalo en blanco para utilizar la predeterminada de HA", + "db_url": "URL de la base de datos, d\u00e9jalo en blanco para usar la predeterminada de HA", "name": "Nombre que se usar\u00e1 para la entrada de configuraci\u00f3n y tambi\u00e9n para el sensor", "query": "Consulta a ejecutar, debe empezar por 'SELECT'", "unit_of_measurement": "Unidad de medida (opcional)", @@ -45,7 +45,7 @@ }, "data_description": { "column": "Columna de respuesta de la consulta para presentar como estado", - "db_url": "URL de la base de datos, d\u00e9jalo en blanco para utilizar la predeterminada de HA", + "db_url": "URL de la base de datos, d\u00e9jalo en blanco para usar la predeterminada de HA", "name": "Nombre que se usar\u00e1 para la entrada de configuraci\u00f3n y tambi\u00e9n para el sensor", "query": "Consulta a ejecutar, debe empezar por 'SELECT'", "unit_of_measurement": "Unidad de medida (opcional)", diff --git a/homeassistant/components/steam_online/translations/es.json b/homeassistant/components/steam_online/translations/es.json index 7aeca102cc4..e558ead9ff5 100644 --- a/homeassistant/components/steam_online/translations/es.json +++ b/homeassistant/components/steam_online/translations/es.json @@ -20,7 +20,7 @@ "account": "ID de la cuenta de Steam", "api_key": "Clave API" }, - "description": "Utiliza {account_id_url} para encontrar el ID de tu cuenta de Steam" + "description": "Usa {account_id_url} para encontrar el ID de tu cuenta de Steam" } } }, diff --git a/homeassistant/components/subaru/translations/es.json b/homeassistant/components/subaru/translations/es.json index be4121cf747..262e5b3186c 100644 --- a/homeassistant/components/subaru/translations/es.json +++ b/homeassistant/components/subaru/translations/es.json @@ -11,7 +11,7 @@ "incorrect_pin": "PIN incorrecto", "incorrect_validation_code": "C\u00f3digo de validaci\u00f3n incorrecto", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "two_factor_request_failed": "La solicitud del c\u00f3digo 2FA fall\u00f3, int\u00e9ntalo de nuevo" + "two_factor_request_failed": "La solicitud del c\u00f3digo 2FA fall\u00f3, por favor, int\u00e9ntalo de nuevo" }, "step": { "pin": { @@ -23,7 +23,7 @@ }, "two_factor": { "data": { - "contact_method": "Selecciona un m\u00e9todo de contacto:" + "contact_method": "Por favor, selecciona un m\u00e9todo de contacto:" }, "description": "Se requiere autenticaci\u00f3n de dos factores", "title": "Configuraci\u00f3n de Subaru Starlink" @@ -52,7 +52,7 @@ "data": { "update_enabled": "Habilitar el sondeo de veh\u00edculos" }, - "description": "Cuando est\u00e1 habilitado, el sondeo de veh\u00edculos enviar\u00e1 un comando remoto a tu veh\u00edculo cada 2 horas para obtener nuevos datos del sensor. Sin sondeo del veh\u00edculo, los nuevos datos del sensor solo se reciben cuando el veh\u00edculo env\u00eda datos autom\u00e1ticamente (normalmente despu\u00e9s de apagar el motor).", + "description": "Cuando est\u00e1 habilitado, el sondeo del veh\u00edculo enviar\u00e1 un comando remoto a tu veh\u00edculo cada 2 horas para obtener nuevos datos del sensor. Sin sondeo del veh\u00edculo, los nuevos datos del sensor solo se reciben cuando el veh\u00edculo env\u00eda datos autom\u00e1ticamente (normalmente despu\u00e9s de apagar el motor).", "title": "Opciones de Subaru Starlink" } } diff --git a/homeassistant/components/switch_as_x/translations/es.json b/homeassistant/components/switch_as_x/translations/es.json index eeeae5ede55..014106acb4e 100644 --- a/homeassistant/components/switch_as_x/translations/es.json +++ b/homeassistant/components/switch_as_x/translations/es.json @@ -3,7 +3,7 @@ "step": { "user": { "data": { - "entity_id": "Conmutador", + "entity_id": "Interruptor", "target_domain": "Nuevo tipo" }, "description": "Elige un interruptor que desees que aparezca en Home Assistant como luz, cubierta o cualquier otra cosa. El interruptor original se ocultar\u00e1." diff --git a/homeassistant/components/switchbot/translations/el.json b/homeassistant/components/switchbot/translations/el.json index d38179c0702..14a5af2818a 100644 --- a/homeassistant/components/switchbot/translations/el.json +++ b/homeassistant/components/switchbot/translations/el.json @@ -13,6 +13,9 @@ "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" }, "password": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + }, "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae {name} \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" }, "user": { diff --git a/homeassistant/components/switchbot/translations/fi.json b/homeassistant/components/switchbot/translations/fi.json new file mode 100644 index 00000000000..33c3ca73dcc --- /dev/null +++ b/homeassistant/components/switchbot/translations/fi.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "password": { + "data": { + "password": "Salasana" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/ja.json b/homeassistant/components/switchbot/translations/ja.json index 3f9425d179a..8954b2397ab 100644 --- a/homeassistant/components/switchbot/translations/ja.json +++ b/homeassistant/components/switchbot/translations/ja.json @@ -7,8 +7,20 @@ "switchbot_unsupported_type": "\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u306a\u3044\u7a2e\u985e\u306eSwitchbot", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" }, + "error": { + "other": "\u7a7a" + }, "flow_title": "{name}", "step": { + "confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "password": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + }, + "description": "{name} \u30c7\u30d0\u30a4\u30b9\u306b\u306f\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u5fc5\u8981\u3067\u3059" + }, "user": { "data": { "address": "\u30c7\u30d0\u30a4\u30b9\u30a2\u30c9\u30ec\u30b9", diff --git a/homeassistant/components/switchbot/translations/ru.json b/homeassistant/components/switchbot/translations/ru.json index 7b2f4f73cc2..ddf26f9a40e 100644 --- a/homeassistant/components/switchbot/translations/ru.json +++ b/homeassistant/components/switchbot/translations/ru.json @@ -9,6 +9,15 @@ }, "flow_title": "{name} ({address})", "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "password": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e {name} \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043f\u0430\u0440\u043e\u043b\u044c" + }, "user": { "data": { "address": "\u0410\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", diff --git a/homeassistant/components/tellduslive/translations/es.json b/homeassistant/components/tellduslive/translations/es.json index 2fd67d46d01..cad0b303116 100644 --- a/homeassistant/components/tellduslive/translations/es.json +++ b/homeassistant/components/tellduslive/translations/es.json @@ -12,7 +12,7 @@ "step": { "auth": { "description": "Para vincular tu cuenta de Telldus Live:\n1. Pulsa el siguiente enlace\n2. Inicia sesi\u00f3n en Telldus Live\n3. Autoriza **{app_name}** (pulsa en **Yes**).\n4. Vuelve atr\u00e1s y pulsa **ENVIAR**.\n\n[Enlace a la cuenta TelldusLive]({auth_url})", - "title": "Autenticaci\u00f3n contra TelldusLive" + "title": "Autenticar contra TelldusLive" }, "user": { "data": { diff --git a/homeassistant/components/tesla_wall_connector/translations/es.json b/homeassistant/components/tesla_wall_connector/translations/es.json index c324ad42733..46f62664445 100644 --- a/homeassistant/components/tesla_wall_connector/translations/es.json +++ b/homeassistant/components/tesla_wall_connector/translations/es.json @@ -13,7 +13,7 @@ "data": { "host": "Host" }, - "title": "Configurar el Tesla Wall Connector" + "title": "Configurar Tesla Wall Connector" } } } diff --git a/homeassistant/components/tomorrowio/translations/es.json b/homeassistant/components/tomorrowio/translations/es.json index 48bac23d2ba..27eeea44515 100644 --- a/homeassistant/components/tomorrowio/translations/es.json +++ b/homeassistant/components/tomorrowio/translations/es.json @@ -3,7 +3,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_api_key": "Clave API no v\u00e1lida", - "rate_limited": "Actualmente el ritmo de consultas est\u00e1 limitado, por favor int\u00e9ntalo m\u00e1s tarde.", + "rate_limited": "La frecuencia de consulta est\u00e1 limitada en este momento, por favor, vuelve a intentarlo m\u00e1s tarde.", "unknown": "Error inesperado" }, "step": { diff --git a/homeassistant/components/toon/translations/es.json b/homeassistant/components/toon/translations/es.json index 89f85a222a1..2cab9e84513 100644 --- a/homeassistant/components/toon/translations/es.json +++ b/homeassistant/components/toon/translations/es.json @@ -5,7 +5,7 @@ "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", "no_agreements": "Esta cuenta no tiene pantallas Toon.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, "step": { diff --git a/homeassistant/components/totalconnect/translations/es.json b/homeassistant/components/totalconnect/translations/es.json index 61930b53fb0..079e0be5af0 100644 --- a/homeassistant/components/totalconnect/translations/es.json +++ b/homeassistant/components/totalconnect/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "no_locations": "No hay ubicaciones disponibles para este usuario, verifica la configuraci\u00f3n de TotalConnect", + "no_locations": "No hay ubicaciones disponibles para este usuario, comprueba la configuraci\u00f3n de TotalConnect", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/traccar/translations/es.json b/homeassistant/components/traccar/translations/es.json index 1177250f930..a3710a80a50 100644 --- a/homeassistant/components/traccar/translations/es.json +++ b/homeassistant/components/traccar/translations/es.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, "create_entry": { - "default": "Para enviar eventos a Home Assistant, deber\u00e1s configurar la funci\u00f3n de webhook en Traccar. \n\nUtiliza la siguiente URL: `{webhook_url}` \n\nConsulta [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s detalles." + "default": "Para enviar eventos a Home Assistant, deber\u00e1s configurar la funci\u00f3n de webhook en Traccar. \n\nUsa la siguiente URL: `{webhook_url}` \n\nConsulta [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s detalles." }, "step": { "user": { diff --git a/homeassistant/components/tractive/translations/es.json b/homeassistant/components/tractive/translations/es.json index 0e242f535ee..41f5827ac1a 100644 --- a/homeassistant/components/tractive/translations/es.json +++ b/homeassistant/components/tractive/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, elimina la integraci\u00f3n y vuelve a configurarla.", + "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, por favor, elimina la integraci\u00f3n y vuelve a configurarla.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { diff --git a/homeassistant/components/transmission/translations/es.json b/homeassistant/components/transmission/translations/es.json index d722d0a528a..e9d084d9758 100644 --- a/homeassistant/components/transmission/translations/es.json +++ b/homeassistant/components/transmission/translations/es.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "name_exists": "Nombre ya existente" + "name_exists": "El nombre ya existe" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/twilio/translations/es.json b/homeassistant/components/twilio/translations/es.json index 3743b0ee163..8640618e8cd 100644 --- a/homeassistant/components/twilio/translations/es.json +++ b/homeassistant/components/twilio/translations/es.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Tu instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes webhook." }, "create_entry": { - "default": "Para enviar eventos a Home Assistant debes configurar los [Webhooks en Twilio]({twilio_url}). \n\n Completa la siguiente informaci\u00f3n: \n\n- URL: `{webhook_url}` \n- M\u00e9todo: POST \n- Tipo de contenido: application/x-www-form-urlencoded \n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." + "default": "Para enviar eventos a Home Assistant debes configurar los [Webhooks en Twilio]({twilio_url}). \n\nCompleta la siguiente informaci\u00f3n: \n\n- URL: `{webhook_url}` \n- M\u00e9todo: POST \n- Tipo de contenido: application/x-www-form-urlencoded \n\nConsulta [la documentaci\u00f3n]({docs_url}) sobre c\u00f3mo configurar las automatizaciones para manejar los datos entrantes." }, "step": { "user": { diff --git a/homeassistant/components/unifi/translations/es.json b/homeassistant/components/unifi/translations/es.json index 1bc3e90ac91..1c03e7b3617 100644 --- a/homeassistant/components/unifi/translations/es.json +++ b/homeassistant/components/unifi/translations/es.json @@ -43,19 +43,19 @@ "data": { "detection_time": "Tiempo en segundos desde la \u00faltima vez que se vio hasta que se considera ausente", "ignore_wired_bug": "Deshabilitar la l\u00f3gica de errores cableada de UniFi Network", - "ssid_filter": "Selecciona los SSIDs para realizar el seguimiento de clientes inal\u00e1mbricos", - "track_clients": "Seguimiento de los clientes de red", - "track_devices": "Seguimiento de dispositivos de red (dispositivos Ubiquiti)", + "ssid_filter": "Selecciona los SSIDs en los que rastrear clientes inal\u00e1mbricos", + "track_clients": "Rastrear clientes de red", + "track_devices": "Rastrear dispositivos de red (dispositivos Ubiquiti)", "track_wired_clients": "Incluir clientes de red cableada" }, - "description": "Configurar dispositivo de seguimiento", + "description": "Configurar rastreo de dispositivo", "title": "Opciones de UniFi Network 1/3" }, "simple_options": { "data": { "block_client": "Clientes con acceso controlado a la red", - "track_clients": "Seguimiento de los clientes de red", - "track_devices": "Seguimiento de dispositivos de red (dispositivos Ubiquiti)" + "track_clients": "Rastrear clientes de red", + "track_devices": "Rastrear dispositivos de red (dispositivos Ubiquiti)" }, "description": "Configura la integraci\u00f3n UniFi Network" }, diff --git a/homeassistant/components/unifiprotect/translations/ca.json b/homeassistant/components/unifiprotect/translations/ca.json index a3d873ead5b..830b73d1eee 100644 --- a/homeassistant/components/unifiprotect/translations/ca.json +++ b/homeassistant/components/unifiprotect/translations/ca.json @@ -47,6 +47,7 @@ "data": { "all_updates": "M\u00e8triques en temps real (ALERTA: augmenta considerablement l'\u00fas de CPU)", "disable_rtsp": "Desactiva el flux RTSP", + "max_media": "Nombre m\u00e0xim d'esdeveniments a carregar al navegador multim\u00e8dia (augmenta l'\u00fas de RAM)", "override_connection_host": "Substitueix l'amfitri\u00f3 de connexi\u00f3" }, "description": "Les m\u00e8triques en temps real nom\u00e9s s'haurien d'activar si has habilitat sensors de diagn\u00f2stic i vols que s'actualitzin en temps real. Si no s'activa aquesta opci\u00f3, els sensors s'actualitzaran cada 15 minuts.", diff --git a/homeassistant/components/unifiprotect/translations/es.json b/homeassistant/components/unifiprotect/translations/es.json index 684ee3f817f..d12744bf841 100644 --- a/homeassistant/components/unifiprotect/translations/es.json +++ b/homeassistant/components/unifiprotect/translations/es.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "protect_version": "La versi\u00f3n m\u00ednima requerida es v1.20.0. Actualiza UniFi Protect y vuelve a intentarlo." + "protect_version": "La versi\u00f3n m\u00ednima requerida es v1.20.0. Por favor, actualiza UniFi Protect y vuelve a intentarlo." }, "flow_title": "{name} ({ip_address})", "step": { diff --git a/homeassistant/components/unifiprotect/translations/ja.json b/homeassistant/components/unifiprotect/translations/ja.json index 12275699260..608601e2175 100644 --- a/homeassistant/components/unifiprotect/translations/ja.json +++ b/homeassistant/components/unifiprotect/translations/ja.json @@ -47,6 +47,7 @@ "data": { "all_updates": "\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u30e1\u30c8\u30ea\u30c3\u30af(Realtime metrics)(\u8b66\u544a: CPU\u4f7f\u7528\u7387\u304c\u5927\u5e45\u306b\u5897\u52a0\u3057\u307e\u3059)", "disable_rtsp": "RTSP\u30b9\u30c8\u30ea\u30fc\u30e0\u3092\u7121\u52b9\u306b\u3059\u308b", + "max_media": "\u30e1\u30c7\u30a3\u30a2\u30d6\u30e9\u30a6\u30b6\u306b\u30ed\u30fc\u30c9\u3059\u308b\u30a4\u30d9\u30f3\u30c8\u306e\u6700\u5927\u6570(RAM\u4f7f\u7528\u91cf\u304c\u5897\u52a0)", "override_connection_host": "\u63a5\u7d9a\u30db\u30b9\u30c8\u3092\u4e0a\u66f8\u304d" }, "description": "\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u30e1\u30c8\u30ea\u30c3\u30af \u30aa\u30d7\u30b7\u30e7\u30f3(Realtime metrics option)\u306f\u3001\u8a3a\u65ad\u30bb\u30f3\u30b5\u30fc(diagnostics sensors)\u3092\u6709\u52b9\u306b\u3057\u3066\u3044\u3066\u3001\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u3067\u66f4\u65b0\u3057\u305f\u3044\u5834\u5408\u306b\u306e\u307f\u6709\u52b9\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u6709\u52b9\u306b\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306b\u306f\u300115\u5206\u3054\u3068\u306b1\u56de\u3060\u3051\u66f4\u65b0\u3055\u308c\u307e\u3059\u3002", diff --git a/homeassistant/components/upb/translations/es.json b/homeassistant/components/upb/translations/es.json index 4b4a44ce0d3..6cef1f516a2 100644 --- a/homeassistant/components/upb/translations/es.json +++ b/homeassistant/components/upb/translations/es.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "invalid_upb_file": "Archivo de exportaci\u00f3n UPB UPStart faltante o no v\u00e1lido, verifique el nombre y la ruta del archivo.", + "invalid_upb_file": "Falta el archivo de exportaci\u00f3n UPB UPStart o no es v\u00e1lido, comprueba el nombre y la ruta del archivo.", "unknown": "Error inesperado" }, "step": { @@ -15,7 +15,7 @@ "file_path": "Ruta y nombre del archivo de exportaci\u00f3n UPStart UPB.", "protocol": "Protocolo" }, - "description": "Conecta un Universal Powerline Bus Powerline Interface Module (UPB PIM). La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n[:puerto]' para 'tcp'. El puerto es opcional y el valor predeterminado es 2101. Ejemplo: '192.168.1.42'. Para el protocolo serial, la direcci\u00f3n debe tener el formato 'tty[:baudios]'. El par\u00e1metro baudios es opcional y el valor predeterminado es 4800. Ejemplo: '/dev/ttyS1'.", + "description": "Conecta un Universal Powerline Bus Powerline Interface Module (UPB PIM). La cadena de direcci\u00f3n debe tener el formato 'direcci\u00f3n[:puerto]' para 'tcp'. El puerto es opcional y el valor predeterminado es 2101. Ejemplo: '192.168.1.42'. Para el protocolo serial, la direcci\u00f3n debe tener el formato 'tty[:baudios]'. Los baudios son opcionales y el valor predeterminado es 4800. Ejemplo: '/dev/ttyS1'.", "title": "Conectar con UPB PIM" } } diff --git a/homeassistant/components/update/translations/es.json b/homeassistant/components/update/translations/es.json index 49012350f47..f53c1247596 100644 --- a/homeassistant/components/update/translations/es.json +++ b/homeassistant/components/update/translations/es.json @@ -1,7 +1,7 @@ { "device_automation": { "trigger_type": { - "changed_states": "La disponibilidad de la actualizaci\u00f3n de {entity_name} cambie", + "changed_states": "La disponibilidad de la actualizaci\u00f3n de {entity_name} cambi\u00f3", "turned_off": "{entity_name} se actualiz\u00f3", "turned_on": "{entity_name} tiene una actualizaci\u00f3n disponible" } diff --git a/homeassistant/components/upnp/translations/ja.json b/homeassistant/components/upnp/translations/ja.json index 8c4d8d4695e..2beff286e3c 100644 --- a/homeassistant/components/upnp/translations/ja.json +++ b/homeassistant/components/upnp/translations/ja.json @@ -5,6 +5,9 @@ "incomplete_discovery": "\u4e0d\u5b8c\u5168\u306a\u691c\u51fa", "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" }, + "error": { + "other": "\u7a7a" + }, "flow_title": "{name}", "step": { "ssdp_confirm": { diff --git a/homeassistant/components/uptimerobot/translations/es.json b/homeassistant/components/uptimerobot/translations/es.json index e1ede36a55a..67e99d60ef4 100644 --- a/homeassistant/components/uptimerobot/translations/es.json +++ b/homeassistant/components/uptimerobot/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "La cuenta ya est\u00e1 configurada", - "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, eliminq la integraci\u00f3n y vuelve a configurarla.", + "reauth_failed_existing": "No se pudo actualizar la entrada de configuraci\u00f3n, por favor, elimina la integraci\u00f3n y vuelve a configurarla.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "unknown": "Error inesperado" }, diff --git a/homeassistant/components/uscis/translations/es.json b/homeassistant/components/uscis/translations/es.json index 8dd3ad76874..f18acfff461 100644 --- a/homeassistant/components/uscis/translations/es.json +++ b/homeassistant/components/uscis/translations/es.json @@ -1,7 +1,7 @@ { "issues": { "pending_removal": { - "description": "La integraci\u00f3n de los Servicios de Inmigraci\u00f3n y Ciudadan\u00eda de los EE.UU. (USCIS) est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nLa integraci\u00f3n se elimina porque se basa en webscraping, que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "La integraci\u00f3n de los Servicios de Inmigraci\u00f3n y Ciudadan\u00eda de los EE.UU. (USCIS) est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nLa integraci\u00f3n se elimina porque se basa en webscraping, algo que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la integraci\u00f3n USCIS" } } diff --git a/homeassistant/components/version/translations/es.json b/homeassistant/components/version/translations/es.json index cbcd4bcd511..6433f5657b7 100644 --- a/homeassistant/components/version/translations/es.json +++ b/homeassistant/components/version/translations/es.json @@ -8,7 +8,7 @@ "data": { "version_source": "Fuente de la versi\u00f3n" }, - "description": "Selecciona la fuente de la que deseas realizar un seguimiento de las versiones", + "description": "Selecciona la fuente de la que deseas realizar un rastreo de las versiones", "title": "Selecciona el tipo de instalaci\u00f3n" }, "version_source": { @@ -18,7 +18,7 @@ "channel": "Qu\u00e9 canal debe ser rastreado", "image": "Qu\u00e9 imagen debe ser rastreada" }, - "description": "Configurar el seguimiento de versiones de {version_source}", + "description": "Configura el rastreo de versiones de {version_source}", "title": "Configurar" } } diff --git a/homeassistant/components/vulcan/translations/es.json b/homeassistant/components/vulcan/translations/es.json index 7e8b3b8b067..f98fa85f818 100644 --- a/homeassistant/components/vulcan/translations/es.json +++ b/homeassistant/components/vulcan/translations/es.json @@ -3,7 +3,7 @@ "abort": { "all_student_already_configured": "Todos los estudiantes ya han sido a\u00f1adidos.", "already_configured": "Ese estudiante ya ha sido a\u00f1adido.", - "no_matching_entries": "No se encontraron entradas que coincidan, usa una cuenta diferente o elimina la integraci\u00f3n con el estudiante obsoleto.", + "no_matching_entries": "No se encontraron entradas que coincidan, por favor, usa una cuenta diferente o elimina la integraci\u00f3n con el estudiante obsoleto.", "reauth_successful": "Re-autenticaci\u00f3n exitosa" }, "error": { diff --git a/homeassistant/components/webostv/translations/es.json b/homeassistant/components/webostv/translations/es.json index d4b3d2eecdc..c09d156bfb5 100644 --- a/homeassistant/components/webostv/translations/es.json +++ b/homeassistant/components/webostv/translations/es.json @@ -6,7 +6,7 @@ "error_pairing": "Conectado a LG webOS TV pero no emparejado" }, "error": { - "cannot_connect": "No se pudo conectar, por favor, enciende tu televisor o verifica la direcci\u00f3n IP" + "cannot_connect": "No se pudo conectar, por favor, enciende tu TV o comprueba la direcci\u00f3n IP" }, "flow_title": "LG webOS Smart TV", "step": { diff --git a/homeassistant/components/withings/translations/es.json b/homeassistant/components/withings/translations/es.json index 07734a7263f..e3a101b1892 100644 --- a/homeassistant/components/withings/translations/es.json +++ b/homeassistant/components/withings/translations/es.json @@ -4,7 +4,7 @@ "already_configured": "Configuraci\u00f3n actualizada para el perfil.", "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})" + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})" }, "create_entry": { "default": "Autenticado con \u00e9xito con Withings." diff --git a/homeassistant/components/wiz/translations/es.json b/homeassistant/components/wiz/translations/es.json index d3d0469cd0b..ef1292d1920 100644 --- a/homeassistant/components/wiz/translations/es.json +++ b/homeassistant/components/wiz/translations/es.json @@ -6,7 +6,7 @@ "no_devices_found": "No se encontraron dispositivos en la red" }, "error": { - "bulb_time_out": "No se puede conectar a la bombilla. Tal vez la bombilla est\u00e1 desconectada o se ha introducido una IP incorrecta. \u00a1Por favor, enciende la luz y vuelve a intentarlo!", + "bulb_time_out": "No se puede conectar a la bombilla. Tal vez la bombilla est\u00e9 sin conexi\u00f3n o se ha introducido una IP incorrecta. \u00a1Por favor, enciende la luz e int\u00e9ntalo de nuevo!", "cannot_connect": "No se pudo conectar", "no_ip": "No es una direcci\u00f3n IP v\u00e1lida.", "no_wiz_light": "La bombilla no se puede conectar a trav\u00e9s de la integraci\u00f3n WiZ Platform.", diff --git a/homeassistant/components/xiaomi_aqara/translations/es.json b/homeassistant/components/xiaomi_aqara/translations/es.json index 6c0667af5d5..d58fbec1c58 100644 --- a/homeassistant/components/xiaomi_aqara/translations/es.json +++ b/homeassistant/components/xiaomi_aqara/translations/es.json @@ -10,7 +10,7 @@ "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos , consulta https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "Interfaz de red no v\u00e1lida", "invalid_key": "Clave de puerta de enlace no v\u00e1lida", - "invalid_mac": "Direcci\u00f3n Mac no v\u00e1lida" + "invalid_mac": "Direcci\u00f3n MAC no v\u00e1lida" }, "flow_title": "{name}", "step": { @@ -32,9 +32,9 @@ "data": { "host": "Direcci\u00f3n IP (opcional)", "interface": "La interfaz de la red a usar", - "mac": "Direcci\u00f3n Mac (opcional)" + "mac": "Direcci\u00f3n MAC (opcional)" }, - "description": "Si las direcciones IP y MAC se dejan vac\u00edas, se utiliza la detecci\u00f3n autom\u00e1tica" + "description": "Si las direcciones IP y MAC se dejan vac\u00edas, se usa la detecci\u00f3n autom\u00e1tica" } } } diff --git a/homeassistant/components/xiaomi_ble/translations/ca.json b/homeassistant/components/xiaomi_ble/translations/ca.json index 6dde2ede685..1411fc6b35e 100644 --- a/homeassistant/components/xiaomi_ble/translations/ca.json +++ b/homeassistant/components/xiaomi_ble/translations/ca.json @@ -19,6 +19,9 @@ "bluetooth_confirm": { "description": "Vols configurar {name}?" }, + "confirm_slow": { + "description": "No s'ha em\u00e8s cap 'broadcast' des d'aquest dispositiu durant l'\u00faltim minut, per tant no estem segurs de si aquest dispositiu utilitza encriptaci\u00f3 o no. Aix\u00f2 pot ser perqu\u00e8 el dispositiu utilitza un interval de 'broadcast' lent. Confirma per afegir aquest dispositiu de totes maneres, i la pr\u00f2xima vegada que rebi un 'broadcast' se't demanar\u00e0 que introdueixis la seva clau d'enlla\u00e7 si \u00e9s necessari." + }, "get_encryption_key_4_5": { "data": { "bindkey": "Bindkey" @@ -31,6 +34,9 @@ }, "description": "Les dades del sensor emeses estan xifrades. Per desxifrar-les necessites una clau d'enlla\u00e7 de 24 car\u00e0cters hexadecimals." }, + "slow_confirm": { + "description": "No s'ha em\u00e8s cap 'broadcast' des d'aquest dispositiu durant l'\u00faltim minut, per tant no estem segurs de si aquest dispositiu utilitza encriptaci\u00f3 o no. Aix\u00f2 pot ser perqu\u00e8 el dispositiu utilitza un interval de 'broadcast' lent. Confirma per afegir aquest dispositiu de totes maneres, i la pr\u00f2xima vegada que rebi un 'broadcast' se't demanar\u00e0 que introdueixis la seva clau d'enlla\u00e7 si \u00e9s necessari." + }, "user": { "data": { "address": "Dispositiu" diff --git a/homeassistant/components/xiaomi_miio/translations/es.json b/homeassistant/components/xiaomi_miio/translations/es.json index 8f692a87b2c..a93ca090955 100644 --- a/homeassistant/components/xiaomi_miio/translations/es.json +++ b/homeassistant/components/xiaomi_miio/translations/es.json @@ -10,7 +10,7 @@ "error": { "cannot_connect": "No se pudo conectar", "cloud_credentials_incomplete": "Credenciales de la nube incompletas, por favor, completa el nombre de usuario, la contrase\u00f1a y el pa\u00eds", - "cloud_login_error": "No se pudo iniciar sesi\u00f3n en Xiaomi Miio Cloud, verifica las credenciales.", + "cloud_login_error": "No se pudo iniciar sesi\u00f3n en Xiaomi Miio Cloud, comprueba las credenciales.", "cloud_no_devices": "No se encontraron dispositivos en esta cuenta en la nube de Xiaomi Miio.", "unknown_device": "Se desconoce el modelo del dispositivo, no se puede configurar el dispositivo mediante el flujo de configuraci\u00f3n.", "wrong_token": "Error de suma de comprobaci\u00f3n, token err\u00f3neo" @@ -24,7 +24,7 @@ "cloud_username": "Nombre de usuario de la nube", "manual": "Configurar manualmente (no recomendado)" }, - "description": "Inicia sesi\u00f3n en la nube de Xiaomi Miio, consulta https://www.openhab.org/addons/bindings/miio/#country-servers para conocer el servidor de la nube que debes utilizar." + "description": "Inicia sesi\u00f3n en la nube de Xiaomi Miio, consulta https://www.openhab.org/addons/bindings/miio/#country-servers para usar el servidor en la nube." }, "connect": { "data": { @@ -36,7 +36,7 @@ "host": "Direcci\u00f3n IP", "token": "Token API" }, - "description": "Necesitar\u00e1s la clave de 32 caracteres Token API, consulta https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token para obtener instrucciones. Ten en cuenta que esta Token API es diferente de la clave utilizada por la integraci\u00f3n de Xiaomi Aqara." + "description": "Necesitar\u00e1s la clave de 32 caracteres Token API, consulta https://www.home-assistant.io/integrations/xiaomi_miio#retrieving-the-access-token para obtener instrucciones. Por favor, ten en cuenta que esta Token API es diferente de la clave utilizada por la integraci\u00f3n de Xiaomi Aqara." }, "reauth_confirm": { "description": "La integraci\u00f3n de Xiaomi Miio necesita volver a autenticar tu cuenta para actualizar los tokens o a\u00f1adir las credenciales de la nube que faltan.", @@ -52,12 +52,12 @@ }, "options": { "error": { - "cloud_credentials_incomplete": "Las credenciales de la nube est\u00e1n incompletas, por favor, rellena el nombre de usuario, la contrase\u00f1a y el pa\u00eds" + "cloud_credentials_incomplete": "Las credenciales de la nube est\u00e1n incompletas, por favor, completa el nombre de usuario, la contrase\u00f1a y el pa\u00eds" }, "step": { "init": { "data": { - "cloud_subdevices": "Utiliza la nube para conectar subdispositivos" + "cloud_subdevices": "Usar la nube para obtener subdispositivos conectados" } } } diff --git a/homeassistant/components/yalexs_ble/translations/ca.json b/homeassistant/components/yalexs_ble/translations/ca.json index 5b4b014c4ac..881c5ef5d08 100644 --- a/homeassistant/components/yalexs_ble/translations/ca.json +++ b/homeassistant/components/yalexs_ble/translations/ca.json @@ -2,12 +2,15 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "no_devices_found": "No s'han trobat dispositius a la xarxa", "no_unconfigured_devices": "No s'han trobat dispositius no configurats." }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_key_format": "La clau fora de l\u00ednia ha de ser una cadena hexadecimal de 32 bytes.", + "invalid_key_index": "L'slot de clau fora de l\u00ednia ha de ser un nombre enter entre 0 i 255.", "unknown": "Error inesperat" }, "flow_title": "{name}", @@ -18,9 +21,10 @@ "user": { "data": { "address": "Adre\u00e7a Bluetooth", - "key": "Clau fora de l\u00ednia (cadena hexadecimal de 32 bytes)" + "key": "Clau fora de l\u00ednia (cadena hexadecimal de 32 bytes)", + "slot": "'Slot' de clau fora de l\u00ednia (nombre enter entre 0 i 255)" }, - "description": "Consulta la documentaci\u00f3 a {docs_url} per saber com trobar la clau de fora de l\u00ednia." + "description": "Consulta la documentaci\u00f3 per saber com trobar la clau de fora de l\u00ednia." } } } diff --git a/homeassistant/components/yalexs_ble/translations/el.json b/homeassistant/components/yalexs_ble/translations/el.json index 2a0243266be..3f6ae763e5e 100644 --- a/homeassistant/components/yalexs_ble/translations/el.json +++ b/homeassistant/components/yalexs_ble/translations/el.json @@ -1,11 +1,17 @@ { "config": { "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", "no_unconfigured_devices": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2." }, "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", "invalid_key_format": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03ae \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac 32 byte.", - "invalid_key_index": "\u0397 \u03c5\u03c0\u03bf\u03b4\u03bf\u03c7\u03ae \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03b1\u03ba\u03ad\u03c1\u03b1\u03b9\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd 0 \u03ba\u03b1\u03b9 255." + "invalid_key_index": "\u0397 \u03c5\u03c0\u03bf\u03b4\u03bf\u03c7\u03ae \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03b1\u03ba\u03ad\u03c1\u03b1\u03b9\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd 0 \u03ba\u03b1\u03b9 255.", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "flow_title": "{name}", "step": { @@ -15,7 +21,8 @@ "user": { "data": { "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 Bluetooth", - "key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03ae \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac 32 byte)" + "key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03ae \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac 32 byte)", + "slot": "\u03a5\u03c0\u03bf\u03b4\u03bf\u03c7\u03ae \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 (\u0391\u03ba\u03ad\u03c1\u03b1\u03b9\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd 0 \u03ba\u03b1\u03b9 255)" }, "description": "\u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {docs_url} \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2." } diff --git a/homeassistant/components/yalexs_ble/translations/es.json b/homeassistant/components/yalexs_ble/translations/es.json index adad8a18c40..56103ea2d3e 100644 --- a/homeassistant/components/yalexs_ble/translations/es.json +++ b/homeassistant/components/yalexs_ble/translations/es.json @@ -24,7 +24,7 @@ "key": "Clave sin conexi\u00f3n (cadena hexadecimal de 32 bytes)", "slot": "Ranura de clave sin conexi\u00f3n (entero entre 0 y 255)" }, - "description": "Consulta la documentaci\u00f3n para saber c\u00f3mo encontrar la clave sin conexi\u00f3n." + "description": "Revisa la documentaci\u00f3n para saber c\u00f3mo encontrar la clave sin conexi\u00f3n." } } } diff --git a/homeassistant/components/yalexs_ble/translations/et.json b/homeassistant/components/yalexs_ble/translations/et.json index 564b6c32dd5..dae8737aadc 100644 --- a/homeassistant/components/yalexs_ble/translations/et.json +++ b/homeassistant/components/yalexs_ble/translations/et.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", "no_devices_found": "V\u00f5rgust seadmeid ei leitud", "no_unconfigured_devices": "H\u00e4\u00e4lestamata seadmeid ei leitud." }, @@ -23,7 +24,7 @@ "key": "V\u00f5rgu\u00fchenduseta v\u00f5ti (32-baidine kuueteistk\u00fcmnebaidine string)", "slot": "V\u00f5rgu\u00fchenduseta v\u00f5tmepesa (t\u00e4isarv vahemikus 0 kuni 255)" }, - "description": "V\u00f5rgu\u00fchenduseta v\u00f5tme leidmise kohta vaata dokumentatsiooni aadressil {docs_url} ." + "description": "V\u00f5rgu\u00fchenduseta v\u00f5tme leidmise kohta vaata dokumentatsiooni." } } } diff --git a/homeassistant/components/yalexs_ble/translations/hu.json b/homeassistant/components/yalexs_ble/translations/hu.json index 4208d98a1e6..fc957171d4f 100644 --- a/homeassistant/components/yalexs_ble/translations/hu.json +++ b/homeassistant/components/yalexs_ble/translations/hu.json @@ -13,6 +13,7 @@ "invalid_key_index": "Az offline kulcshelynek 0 \u00e9s 255 k\u00f6z\u00f6tti eg\u00e9sz sz\u00e1mnak kell lennie.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, + "flow_title": "{name}", "step": { "integration_discovery_confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {nane}, Bluetooth-on kereszt\u00fcl, {address} c\u00edmmel?" diff --git a/homeassistant/components/yalexs_ble/translations/ja.json b/homeassistant/components/yalexs_ble/translations/ja.json new file mode 100644 index 00000000000..b847383ba9b --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/ja.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "already_in_progress": "\u69cb\u6210\u30d5\u30ed\u30fc\u306f\u3059\u3067\u306b\u9032\u884c\u4e2d\u3067\u3059", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "no_unconfigured_devices": "\u672a\u69cb\u6210\u306e\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "invalid_key_format": "\u30aa\u30d5\u30e9\u30a4\u30f3\u30ad\u30fc\u306f\u300132\u30d0\u30a4\u30c8\u306e16\u9032\u6587\u5b57\u5217\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "invalid_key_index": "\u30aa\u30d5\u30e9\u30a4\u30f3\u30ad\u30fc\u30b9\u30ed\u30c3\u30c8\u306f\u30010\uff5e255\u306e\u6574\u6570\u3067\u3042\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth\u30a2\u30c9\u30ec\u30b9", + "key": "\u30aa\u30d5\u30e9\u30a4\u30f3\u30ad\u30fc(32\u30d0\u30a4\u30c8\u306e16\u9032\u6587\u5b57\u5217)", + "slot": "\u30aa\u30d5\u30e9\u30a4\u30f3\u30ad\u30fc\u30b9\u30ed\u30c3\u30c8(0\uff5e255\u306e\u6574\u6570)" + }, + "description": "\u30aa\u30d5\u30e9\u30a4\u30f3\u30ad\u30fc\u3092\u898b\u3064\u3051\u308b\u65b9\u6cd5\u306b\u3064\u3044\u3066\u306f\u3001\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/no.json b/homeassistant/components/yalexs_ble/translations/no.json index 6d2be535c73..99a1b78849d 100644 --- a/homeassistant/components/yalexs_ble/translations/no.json +++ b/homeassistant/components/yalexs_ble/translations/no.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", "no_unconfigured_devices": "Fant ingen ukonfigurerte enheter." }, @@ -23,7 +24,7 @@ "key": "Frakoblet n\u00f8kkel (32-byte sekskantstreng)", "slot": "Frakoblet n\u00f8kkelspor (heltall mellom 0 og 255)" }, - "description": "Se dokumentasjonen p\u00e5 {docs_url} for hvordan du finner frakoblet n\u00f8kkel." + "description": "Sjekk dokumentasjonen for hvordan du finner frakoblet n\u00f8kkel." } } } diff --git a/homeassistant/components/yalexs_ble/translations/ru.json b/homeassistant/components/yalexs_ble/translations/ru.json new file mode 100644 index 00000000000..15fed5d664b --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/ru.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "no_unconfigured_devices": "\u041d\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_key_format": "\u0410\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u0434\u043e\u043b\u0436\u0435\u043d \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c \u0441\u043e\u0431\u043e\u0439 32-\u0431\u0430\u0439\u0442\u043e\u0432\u0443\u044e \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u0443\u044e \u0441\u0442\u0440\u043e\u043a\u0443.", + "invalid_key_index": "\u0421\u043b\u043e\u0442 \u0430\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u043e\u0433\u043e \u043a\u043b\u044e\u0447\u0430 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0446\u0435\u043b\u044b\u043c \u0447\u0438\u0441\u043b\u043e\u043c \u043e\u0442 0 \u0434\u043e 255.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} \u0441 \u0430\u0434\u0440\u0435\u0441\u043e\u043c {address}, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f Bluetooth?" + }, + "user": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441 Bluetooth", + "key": "\u0410\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 (32-\u0431\u0430\u0439\u0442\u043e\u0432\u0430\u044f \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u0430\u044f \u0441\u0442\u0440\u043e\u043a\u0430)", + "slot": "\u0421\u043b\u043e\u0442 \u0430\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u043e\u0433\u043e \u043a\u043b\u044e\u0447\u0430 (\u0446\u0435\u043b\u043e\u0435 \u0447\u0438\u0441\u043b\u043e \u043e\u0442 0 \u0434\u043e 255)" + }, + "description": "\u041a\u0430\u043a \u043d\u0430\u0439\u0442\u0438 \u0430\u0432\u0442\u043e\u043d\u043e\u043c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447, \u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/zh-Hant.json b/homeassistant/components/yalexs_ble/translations/zh-Hant.json index f16fdcb0d07..94093fd8ef8 100644 --- a/homeassistant/components/yalexs_ble/translations/zh-Hant.json +++ b/homeassistant/components/yalexs_ble/translations/zh-Hant.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "no_unconfigured_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u8a2d\u5b9a\u88dd\u7f6e\u3002" }, @@ -23,7 +24,7 @@ "key": "\u96e2\u7dda\u91d1\u9470\uff0832 \u4f4d\u5143 16 \u9032\u4f4d\u5b57\u4e32\uff09", "slot": "\u96e2\u7dda\u91d1\u9470\uff080 \u81f3 255 \u9593\u7684\u6574\u6578\uff09" }, - "description": "\u8acb\u53c3\u8003\u6587\u4ef6 {docs_url} \u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3002" + "description": "\u8acb\u53c3\u8003\u6587\u4ef6\u4ee5\u7372\u5f97\u53d6\u7684\u96e2\u7dda\u91d1\u9470\u8a73\u7d30\u8cc7\u8a0a\u3002" } } } diff --git a/homeassistant/components/yeelight/translations/es.json b/homeassistant/components/yeelight/translations/es.json index 6ec5b64a3fd..d8ff3f8adf4 100644 --- a/homeassistant/components/yeelight/translations/es.json +++ b/homeassistant/components/yeelight/translations/es.json @@ -21,7 +21,7 @@ "data": { "host": "Host" }, - "description": "Si dejas el host vac\u00edo, se usar\u00e1 descubrimiento para encontrar dispositivos." + "description": "Si dejas el host vac\u00edo, se usar\u00e1 el descubrimiento para encontrar dispositivos." } } }, @@ -30,7 +30,7 @@ "init": { "data": { "model": "Modelo", - "nightlight_switch": "Usar interruptor de luz nocturna", + "nightlight_switch": "Usar interruptor de Nightlight", "save_on_change": "Guardar estado al cambiar", "transition": "Tiempo de transici\u00f3n (ms)", "use_music_mode": "Activar el Modo M\u00fasica" diff --git a/homeassistant/components/yolink/translations/es.json b/homeassistant/components/yolink/translations/es.json index 82f45972147..391bf1b1164 100644 --- a/homeassistant/components/yolink/translations/es.json +++ b/homeassistant/components/yolink/translations/es.json @@ -5,7 +5,7 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json index 834c8a1116b..62b43a0bab6 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -13,7 +13,7 @@ "not_zwave_device": "El dispositivo descubierto no es un dispositivo Z-Wave." }, "error": { - "addon_start_failed": "No se pudo iniciar el complemento Z-Wave JS. Verifica la configuraci\u00f3n.", + "addon_start_failed": "No se pudo iniciar el complemento Z-Wave JS. Comprueba la configuraci\u00f3n.", "cannot_connect": "No se pudo conectar", "invalid_ws_url": "URL de websocket no v\u00e1lida", "unknown": "Error inesperado" @@ -48,9 +48,9 @@ }, "on_supervisor": { "data": { - "use_addon": "Usar el complemento Z-Wave JS Supervisor" + "use_addon": "Utilizar el complemento del Supervisor Z-Wave JS" }, - "description": "\u00bfQuieres usar el complemento Z-Wave JS Supervisor?", + "description": "\u00bfQuieres usar el complemento del Supervisor Z-Wave JS?", "title": "Selecciona el m\u00e9todo de conexi\u00f3n" }, "start_addon": { @@ -135,9 +135,9 @@ }, "on_supervisor": { "data": { - "use_addon": "Usar el complemento Supervisor Z-Wave JS" + "use_addon": "Utilizar el complemento del Supervisor Z-Wave JS" }, - "description": "\u00bfQuieres utilizar el complemento Supervisor Z-Wave JS ?", + "description": "\u00bfQuieres usar el complemento del Supervisor Z-Wave JS?", "title": "Selecciona el m\u00e9todo de conexi\u00f3n" }, "start_addon": { From b43242ef0d7a89ecdbb2e36e7652fa6a83d17342 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 15 Aug 2022 15:09:13 -1000 Subject: [PATCH 3365/3516] Fix lifx homekit discoveries not being ignorable or updating the IP (#76825) --- homeassistant/components/lifx/config_flow.py | 10 +++--- tests/components/lifx/test_config_flow.py | 33 +++++++++++++++----- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/lifx/config_flow.py b/homeassistant/components/lifx/config_flow.py index daa917dc847..4b2a5b0895e 100644 --- a/homeassistant/components/lifx/config_flow.py +++ b/homeassistant/components/lifx/config_flow.py @@ -119,18 +119,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Confirm discovery.""" assert self._discovered_device is not None + discovered = self._discovered_device _LOGGER.debug( "Confirming discovery: %s with serial %s", - self._discovered_device.label, + discovered.label, self.unique_id, ) if user_input is not None or self._async_discovered_pending_migration(): - return self._async_create_entry_from_device(self._discovered_device) + return self._async_create_entry_from_device(discovered) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovered.ip_addr}) self._set_confirm_only() placeholders = { - "label": self._discovered_device.label, - "host": self._discovered_device.ip_addr, + "label": discovered.label, + "host": discovered.ip_addr, "serial": self.unique_id, } self.context["title_placeholders"] = placeholders diff --git a/tests/components/lifx/test_config_flow.py b/tests/components/lifx/test_config_flow.py index b346233874a..c9f069d8660 100644 --- a/tests/components/lifx/test_config_flow.py +++ b/tests/components/lifx/test_config_flow.py @@ -466,21 +466,38 @@ async def test_discovered_by_dhcp_or_discovery_failed_to_get_device(hass, source assert result["reason"] == "cannot_connect" -async def test_discovered_by_dhcp_updates_ip(hass): +@pytest.mark.parametrize( + "source, data", + [ + ( + config_entries.SOURCE_DHCP, + dhcp.DhcpServiceInfo(ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=LABEL), + ), + ( + config_entries.SOURCE_HOMEKIT, + zeroconf.ZeroconfServiceInfo( + host=IP_ADDRESS, + addresses=[IP_ADDRESS], + hostname=LABEL, + name=LABEL, + port=None, + properties={zeroconf.ATTR_PROPERTIES_ID: "any"}, + type="mock_type", + ), + ), + ], +) +async def test_discovered_by_dhcp_or_homekit_updates_ip(hass, source, data): """Update host from dhcp.""" config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "127.0.0.2"}, unique_id=SERIAL ) config_entry.add_to_hass(hass) - with _patch_discovery(no_device=True), _patch_config_flow_try_connect( - no_device=True - ): + with _patch_discovery(), _patch_config_flow_try_connect(): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, - data=dhcp.DhcpServiceInfo( - ip=IP_ADDRESS, macaddress=MAC_ADDRESS, hostname=LABEL - ), + context={"source": source}, + data=data, ) await hass.async_block_till_done() assert result["type"] == FlowResultType.ABORT From b8f83f6c70eaf5f734eb2527162084701cfef600 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 16 Aug 2022 07:40:33 +0200 Subject: [PATCH 3366/3516] Use BinarySensorDeviceClass instead of deprecated constants (#76830) --- homeassistant/components/devolo_home_network/binary_sensor.py | 4 ++-- homeassistant/components/zwave_me/binary_sensor.py | 4 ++-- pylint/plugins/hass_imports.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/devolo_home_network/binary_sensor.py b/homeassistant/components/devolo_home_network/binary_sensor.py index 6bc02d802f5..2e87bd180b1 100644 --- a/homeassistant/components/devolo_home_network/binary_sensor.py +++ b/homeassistant/components/devolo_home_network/binary_sensor.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from devolo_plc_api.device import Device from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_PLUG, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -47,7 +47,7 @@ class DevoloBinarySensorEntityDescription( SENSOR_TYPES: dict[str, DevoloBinarySensorEntityDescription] = { CONNECTED_TO_ROUTER: DevoloBinarySensorEntityDescription( key=CONNECTED_TO_ROUTER, - device_class=DEVICE_CLASS_PLUG, + device_class=BinarySensorDeviceClass.PLUG, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:router-network", diff --git a/homeassistant/components/zwave_me/binary_sensor.py b/homeassistant/components/zwave_me/binary_sensor.py index 40d850b8483..f1ee6896b25 100644 --- a/homeassistant/components/zwave_me/binary_sensor.py +++ b/homeassistant/components/zwave_me/binary_sensor.py @@ -4,7 +4,7 @@ from __future__ import annotations from zwave_me_ws import ZWaveMeData from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_MOTION, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -22,7 +22,7 @@ BINARY_SENSORS_MAP: dict[str, BinarySensorEntityDescription] = { ), "motion": BinarySensorEntityDescription( key="motion", - device_class=DEVICE_CLASS_MOTION, + device_class=BinarySensorDeviceClass.MOTION, ), } DEVICE_NAME = ZWaveMePlatform.BINARY_SENSOR diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index 31fbe8f498e..de981ab6a6d 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -38,7 +38,7 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { constant=re.compile(r"^FORMAT_(\w*)$"), ), ], - "homeassistant.components.binarysensor": [ + "homeassistant.components.binary_sensor": [ ObsoleteImportMatch( reason="replaced by BinarySensorDeviceClass enum", constant=re.compile(r"^DEVICE_CLASS_(\w*)$"), From 7ec54edc69c8f2563045b92a68370119d5b4aef2 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Tue, 16 Aug 2022 07:54:26 +0200 Subject: [PATCH 3367/3516] Fix Overkiz startup order to prevent unnamed device showing up (#76695) Gateways should be added first, before platform setup --- homeassistant/components/overkiz/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/overkiz/__init__.py b/homeassistant/components/overkiz/__init__.py index 9acdbfb9ec9..a4240bc0550 100644 --- a/homeassistant/components/overkiz/__init__.py +++ b/homeassistant/components/overkiz/__init__.py @@ -111,8 +111,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) or OVERKIZ_DEVICE_TO_PLATFORM.get(device.ui_class): platforms[platform].append(device) - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - device_registry = dr.async_get(hass) for gateway in setup.gateways: @@ -128,6 +126,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: configuration_url=server.configuration_url, ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True From 91005f4694aa706d9401e5858ff32f3e4eef1cb7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 16 Aug 2022 11:16:33 +0200 Subject: [PATCH 3368/3516] Update pylint plugin to use TriggerActionType (#76819) --- pylint/plugins/hass_enforce_type_hints.py | 4 ++-- pylint/plugins/hass_imports.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 158848ba4d4..35862742b18 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -337,8 +337,8 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { arg_types={ 0: "HomeAssistant", 1: "ConfigType", - # 2: "AutomationActionType", # AutomationActionType is deprecated -> TriggerActionType - # 3: "AutomationTriggerInfo", # AutomationTriggerInfo is deprecated -> TriggerInfo + 2: "TriggerActionType", + 3: "TriggerInfo", }, return_type="CALLBACK_TYPE", ), diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index de981ab6a6d..caea0451f4d 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -38,6 +38,20 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { constant=re.compile(r"^FORMAT_(\w*)$"), ), ], + "homeassistant.components.automation": [ + ObsoleteImportMatch( + reason="replaced by TriggerActionType from helpers.trigger", + constant=re.compile(r"^AutomationActionType$") + ), + ObsoleteImportMatch( + reason="replaced by TriggerData from helpers.trigger", + constant=re.compile(r"^AutomationTriggerData$") + ), + ObsoleteImportMatch( + reason="replaced by TriggerInfo from helpers.trigger", + constant=re.compile(r"^AutomationTriggerInfo$") + ), + ], "homeassistant.components.binary_sensor": [ ObsoleteImportMatch( reason="replaced by BinarySensorDeviceClass enum", From 93630cf1f84ea37a58e98c44a9ce377bd194f669 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 16 Aug 2022 11:17:10 +0200 Subject: [PATCH 3369/3516] Add missing entry for `SOURCE_TYPE_*` to hass-imports plugin (#76829) --- pylint/plugins/hass_imports.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index caea0451f4d..c7abe0ad6ac 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -108,6 +108,18 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = { constant=re.compile(r"^SUPPORT_(\w*)$"), ), ], + "homeassistant.components.device_tracker": [ + ObsoleteImportMatch( + reason="replaced by SourceType enum", + constant=re.compile(r"^SOURCE_TYPE_\w+$") + ), + ], + "homeassistant.components.device_tracker.const": [ + ObsoleteImportMatch( + reason="replaced by SourceType enum", + constant=re.compile(r"^SOURCE_TYPE_\w+$") + ), + ], "homeassistant.components.fan": [ ObsoleteImportMatch( reason="replaced by FanEntityFeature enum", From a663445f25e42c01add7d8033952b11d3d582318 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 16 Aug 2022 10:34:17 +0100 Subject: [PATCH 3370/3516] Bump aiohomekit to 1.3.0 (#76841) --- homeassistant/components/homekit_controller/config_flow.py | 5 ----- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit_controller/test_config_flow.py | 1 - 5 files changed, 3 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index eba531b917c..2ccdd557a5b 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -281,11 +281,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): existing_entry, data={**existing_entry.data, **updated_ip_port} ) conn: HKDevice = self.hass.data[KNOWN_DEVICES][hkid] - # When we rediscover the device, let aiohomekit know - # that the device is available and we should not wait - # to retry connecting any longer. reconnect_soon - # will do nothing if the device is already connected - await conn.pairing.reconnect_soon() if config_num and conn.config_num != config_num: _LOGGER.debug( "HomeKit info %s: c# incremented, refreshing entities", hkid diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index ece53d29406..3f8d7828236 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.2.11"], + "requirements": ["aiohomekit==1.3.0"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/requirements_all.txt b/requirements_all.txt index 4bb2e474f03..5388db5998b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.11 +aiohomekit==1.3.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9909033875d..9baff4f1dbd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.2.11 +aiohomekit==1.3.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index ff9b89473d6..3f545be8931 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -524,7 +524,6 @@ async def test_discovery_already_configured_update_csharp(hass, controller): entry.add_to_hass(hass) connection_mock = AsyncMock() - connection_mock.pairing.connect.reconnect_soon = AsyncMock() connection_mock.async_notify_config_changed = MagicMock() hass.data[KNOWN_DEVICES] = {"AA:BB:CC:DD:EE:FF": connection_mock} From 3e1c9f1ac7f10133abd7967dee45555da915ec6e Mon Sep 17 00:00:00 2001 From: jonasrickert <46763066+jonasrickert@users.noreply.github.com> Date: Tue, 16 Aug 2022 11:49:31 +0200 Subject: [PATCH 3371/3516] Add Rollotron DECT 1213 to fritzbox (#76386) --- homeassistant/components/fritzbox/const.py | 3 +- homeassistant/components/fritzbox/cover.py | 78 +++++++++++++++++ .../components/fritzbox/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/fritzbox/__init__.py | 19 ++++ tests/components/fritzbox/test_cover.py | 86 +++++++++++++++++++ 7 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/fritzbox/cover.py create mode 100644 tests/components/fritzbox/test_cover.py diff --git a/homeassistant/components/fritzbox/const.py b/homeassistant/components/fritzbox/const.py index 7b67bcd6cf8..4831b0f6ab2 100644 --- a/homeassistant/components/fritzbox/const.py +++ b/homeassistant/components/fritzbox/const.py @@ -27,7 +27,8 @@ LOGGER: Final[logging.Logger] = logging.getLogger(__package__) PLATFORMS: Final[list[Platform]] = [ Platform.BINARY_SENSOR, Platform.CLIMATE, + Platform.COVER, Platform.LIGHT, - Platform.SWITCH, Platform.SENSOR, + Platform.SWITCH, ] diff --git a/homeassistant/components/fritzbox/cover.py b/homeassistant/components/fritzbox/cover.py new file mode 100644 index 00000000000..d8fc8d4f3c3 --- /dev/null +++ b/homeassistant/components/fritzbox/cover.py @@ -0,0 +1,78 @@ +"""Support for AVM FRITZ!SmartHome cover devices.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.cover import ( + ATTR_POSITION, + CoverDeviceClass, + CoverEntity, + CoverEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import FritzBoxEntity +from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the FRITZ!SmartHome cover from ConfigEntry.""" + coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR] + + async_add_entities( + FritzboxCover(coordinator, ain) + for ain, device in coordinator.data.items() + if device.has_blind + ) + + +class FritzboxCover(FritzBoxEntity, CoverEntity): + """The cover class for FRITZ!SmartHome covers.""" + + _attr_device_class = CoverDeviceClass.BLIND + _attr_supported_features = ( + CoverEntityFeature.OPEN + | CoverEntityFeature.SET_POSITION + | CoverEntityFeature.CLOSE + | CoverEntityFeature.STOP + ) + + @property + def current_cover_position(self) -> int | None: + """Return the current position.""" + position = None + if self.device.levelpercentage is not None: + position = 100 - self.device.levelpercentage + return position + + @property + def is_closed(self) -> bool | None: + """Return if the cover is closed.""" + if self.device.levelpercentage is None: + return None + return self.device.levelpercentage == 100 # type: ignore [no-any-return] + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + await self.hass.async_add_executor_job(self.device.set_blind_open) + await self.coordinator.async_refresh() + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close the cover.""" + await self.hass.async_add_executor_job(self.device.set_blind_close) + await self.coordinator.async_refresh() + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Move the cover to a specific position.""" + await self.hass.async_add_executor_job( + self.device.set_level_percentage, 100 - kwargs[ATTR_POSITION] + ) + + async def async_stop_cover(self, **kwargs: Any) -> None: + """Stop the cover.""" + await self.hass.async_add_executor_job(self.device.set_blind_stop) + await self.coordinator.async_refresh() diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index 1dac4ddd78a..710f7e8f0c4 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -2,7 +2,7 @@ "domain": "fritzbox", "name": "AVM FRITZ!SmartHome", "documentation": "https://www.home-assistant.io/integrations/fritzbox", - "requirements": ["pyfritzhome==0.6.4"], + "requirements": ["pyfritzhome==0.6.5"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:fritzbox:1" diff --git a/requirements_all.txt b/requirements_all.txt index 5388db5998b..184c6e4d77e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1536,7 +1536,7 @@ pyforked-daapd==0.1.11 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.4 +pyfritzhome==0.6.5 # homeassistant.components.fronius pyfronius==0.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9baff4f1dbd..e8e7774d313 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1061,7 +1061,7 @@ pyforked-daapd==0.1.11 pyfreedompro==1.1.0 # homeassistant.components.fritzbox -pyfritzhome==0.6.4 +pyfritzhome==0.6.5 # homeassistant.components.fronius pyfronius==0.7.1 diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 05003ccdf51..05cd60059fa 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -61,6 +61,7 @@ class FritzDeviceBinarySensorMock(FritzDeviceBaseMock): has_lightbulb = False has_temperature_sensor = False has_thermostat = False + has_blind = False present = True @@ -82,6 +83,7 @@ class FritzDeviceClimateMock(FritzDeviceBaseMock): has_switch = False has_temperature_sensor = True has_thermostat = True + has_blind = False holiday_active = "fake_holiday" lock = "fake_locked" present = True @@ -106,6 +108,7 @@ class FritzDeviceSensorMock(FritzDeviceBaseMock): has_switch = False has_temperature_sensor = True has_thermostat = False + has_blind = False lock = "fake_locked" present = True temperature = 1.23 @@ -126,6 +129,7 @@ class FritzDeviceSwitchMock(FritzDeviceBaseMock): has_switch = True has_temperature_sensor = True has_thermostat = False + has_blind = False switch_state = "fake_state" lock = "fake_locked" power = 5678 @@ -143,6 +147,21 @@ class FritzDeviceLightMock(FritzDeviceBaseMock): has_switch = False has_temperature_sensor = False has_thermostat = False + has_blind = False level = 100 present = True state = True + + +class FritzDeviceCoverMock(FritzDeviceBaseMock): + """Mock of a AVM Fritz!Box cover device.""" + + fw_version = "1.2.3" + has_alarm = False + has_powermeter = False + has_lightbulb = False + has_switch = False + has_temperature_sensor = False + has_thermostat = False + has_blind = True + levelpercentage = 0 diff --git a/tests/components/fritzbox/test_cover.py b/tests/components/fritzbox/test_cover.py new file mode 100644 index 00000000000..07b6ab5990e --- /dev/null +++ b/tests/components/fritzbox/test_cover.py @@ -0,0 +1,86 @@ +"""Tests for AVM Fritz!Box switch component.""" +from unittest.mock import Mock, call + +from homeassistant.components.cover import ATTR_CURRENT_POSITION, ATTR_POSITION, DOMAIN +from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DEVICES, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, +) +from homeassistant.core import HomeAssistant + +from . import FritzDeviceCoverMock, setup_config_entry +from .const import CONF_FAKE_NAME, MOCK_CONFIG + +ENTITY_ID = f"{DOMAIN}.{CONF_FAKE_NAME}" + + +async def test_setup(hass: HomeAssistant, fritz: Mock): + """Test setup of platform.""" + device = FritzDeviceCoverMock() + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + state = hass.states.get(ENTITY_ID) + assert state + assert state.attributes[ATTR_CURRENT_POSITION] == 100 + + +async def test_open_cover(hass: HomeAssistant, fritz: Mock): + """Test opening the cover.""" + device = FritzDeviceCoverMock() + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + assert await hass.services.async_call( + DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert device.set_blind_open.call_count == 1 + + +async def test_close_cover(hass: HomeAssistant, fritz: Mock): + """Test closing the device.""" + device = FritzDeviceCoverMock() + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + assert await hass.services.async_call( + DOMAIN, SERVICE_CLOSE_COVER, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert device.set_blind_close.call_count == 1 + + +async def test_set_position_cover(hass: HomeAssistant, fritz: Mock): + """Test stopping the device.""" + device = FritzDeviceCoverMock() + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + assert await hass.services.async_call( + DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_POSITION: 50}, + True, + ) + assert device.set_level_percentage.call_args_list == [call(50)] + + +async def test_stop_cover(hass: HomeAssistant, fritz: Mock): + """Test stopping the device.""" + device = FritzDeviceCoverMock() + assert await setup_config_entry( + hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz + ) + + assert await hass.services.async_call( + DOMAIN, SERVICE_STOP_COVER, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + assert device.set_blind_stop.call_count == 1 From c7d46bc71971f0072e7aaa93e2b2ef219cebd92c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 16 Aug 2022 08:30:07 -0400 Subject: [PATCH 3372/3516] Improve Awair config flow (#76838) --- homeassistant/components/awair/config_flow.py | 79 +++++++++++++++---- homeassistant/components/awair/manifest.json | 5 ++ homeassistant/components/awair/strings.json | 7 +- homeassistant/generated/dhcp.py | 1 + homeassistant/strings.json | 1 + tests/components/awair/test_config_flow.py | 56 ++++++++++++- 6 files changed, 130 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index 3fb822ab4fe..418413b690f 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -4,14 +4,15 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any -from aiohttp.client_exceptions import ClientConnectorError +from aiohttp.client_exceptions import ClientError from python_awair import Awair, AwairLocal, AwairLocalDevice from python_awair.exceptions import AuthError, AwairError import voluptuous as vol from homeassistant.components import zeroconf -from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST +from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DEVICE, CONF_HOST +from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -40,10 +41,11 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured(error="already_configured_device") self.context.update( { + "host": host, "title_placeholders": { "model": self._device.model, "device_id": self._device.device_id, - } + }, } ) else: @@ -109,31 +111,76 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_local(self, user_input: Mapping[str, Any]) -> FlowResult: + @callback + def _get_discovered_entries(self) -> dict[str, str]: + """Get discovered entries.""" + entries: dict[str, str] = {} + for flow in self._async_in_progress(): + if flow["context"]["source"] == SOURCE_ZEROCONF: + info = flow["context"]["title_placeholders"] + entries[ + flow["context"]["host"] + ] = f"{info['model']} ({info['device_id']})" + return entries + + async def async_step_local( + self, user_input: Mapping[str, Any] | None = None + ) -> FlowResult: + """Show how to enable local API.""" + if user_input is not None: + return await self.async_step_local_pick() + + return self.async_show_form( + step_id="local", + description_placeholders={ + "url": "https://support.getawair.com/hc/en-us/articles/360049221014-Awair-Element-Local-API-Feature#h_01F40FBBW5323GBPV7D6XMG4J8" + }, + ) + + async def async_step_local_pick( + self, user_input: Mapping[str, Any] | None = None + ) -> FlowResult: """Handle collecting and verifying Awair Local API hosts.""" errors = {} - if user_input is not None: + # User input is either: + # 1. None if first time on this step + # 2. {device: manual} if picked manual entry option + # 3. {device: } if picked a device + # 4. {host: } if manually entered a host + # + # Option 1 and 2 will show the form again. + if user_input and user_input.get(CONF_DEVICE) != "manual": + if CONF_DEVICE in user_input: + user_input = {CONF_HOST: user_input[CONF_DEVICE]} + self._device, error = await self._check_local_connection( - user_input[CONF_HOST] + user_input.get(CONF_DEVICE) or user_input[CONF_HOST] ) if self._device is not None: - await self.async_set_unique_id(self._device.mac_address) - self._abort_if_unique_id_configured(error="already_configured_device") + await self.async_set_unique_id( + self._device.mac_address, raise_on_progress=False + ) title = f"{self._device.model} ({self._device.device_id})" return self.async_create_entry(title=title, data=user_input) if error is not None: - errors = {CONF_HOST: error} + errors = {"base": error} + + discovered = self._get_discovered_entries() + + if not discovered or (user_input and user_input.get(CONF_DEVICE) == "manual"): + data_schema = vol.Schema({vol.Required(CONF_HOST): str}) + + elif discovered: + discovered["manual"] = "Manual" + data_schema = vol.Schema({vol.Required(CONF_DEVICE): vol.In(discovered)}) return self.async_show_form( - step_id="local", - data_schema=vol.Schema({vol.Required(CONF_HOST): str}), - description_placeholders={ - "url": "https://support.getawair.com/hc/en-us/articles/360049221014-Awair-Element-Local-API-Feature" - }, + step_id="local_pick", + data_schema=data_schema, errors=errors, ) @@ -177,7 +224,7 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): devices = await awair.devices() return (devices[0], None) - except ClientConnectorError as err: + except ClientError as err: LOGGER.error("Unable to connect error: %s", err) return (None, "unreachable") diff --git a/homeassistant/components/awair/manifest.json b/homeassistant/components/awair/manifest.json index cea5d01bfab..131a955a6eb 100644 --- a/homeassistant/components/awair/manifest.json +++ b/homeassistant/components/awair/manifest.json @@ -12,5 +12,10 @@ "type": "_http._tcp.local.", "name": "awair*" } + ], + "dhcp": [ + { + "macaddress": "70886B1*" + } ] } diff --git a/homeassistant/components/awair/strings.json b/homeassistant/components/awair/strings.json index fc95fc861f1..5b16f359403 100644 --- a/homeassistant/components/awair/strings.json +++ b/homeassistant/components/awair/strings.json @@ -9,10 +9,13 @@ } }, "local": { + "description": "Follow [these instructions]({url}) on how to enable the Awair Local API.\n\nClick submit when done." + }, + "local_pick": { "data": { + "device": "[%key:common::config_flow::data::device%]", "host": "[%key:common::config_flow::data::ip%]" - }, - "description": "Awair Local API must be enabled following these steps: {url}" + } }, "reauth_confirm": { "description": "Please re-enter your Awair developer access token.", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index fb8000f8393..9179a314215 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -11,6 +11,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'august', 'hostname': 'connect', 'macaddress': 'B8B7F1*'}, {'domain': 'august', 'hostname': 'connect', 'macaddress': '2C9FFB*'}, {'domain': 'august', 'hostname': 'august*', 'macaddress': 'E076D0*'}, + {'domain': 'awair', 'macaddress': '70886B1*'}, {'domain': 'axis', 'registered_devices': True}, {'domain': 'axis', 'hostname': 'axis-00408c*', 'macaddress': '00408C*'}, {'domain': 'axis', 'hostname': 'axis-accc8e*', 'macaddress': 'ACCC8E*'}, diff --git a/homeassistant/strings.json b/homeassistant/strings.json index 9ae30becaee..86155be7b4d 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -26,6 +26,7 @@ "confirm_setup": "Do you want to start set up?" }, "data": { + "device": "Device", "name": "Name", "email": "Email", "username": "Username", diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index f6513321dfb..3fdf84f8260 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -240,6 +240,12 @@ async def test_create_local_entry(hass: HomeAssistant, local_devices): {"next_step_id": "local"}, ) + # We're being shown the local instructions + form_step = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + {}, + ) + result = await hass.config_entries.flow.async_configure( form_step["flow_id"], LOCAL_CONFIG, @@ -251,6 +257,48 @@ async def test_create_local_entry(hass: HomeAssistant, local_devices): assert result["result"].unique_id == LOCAL_UNIQUE_ID +async def test_create_local_entry_from_discovery(hass: HomeAssistant, local_devices): + """Test local API when device discovered after instructions shown.""" + + menu_step = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=LOCAL_CONFIG + ) + + form_step = await hass.config_entries.flow.async_configure( + menu_step["flow_id"], + {"next_step_id": "local"}, + ) + + # Create discovered entry in progress + with patch("python_awair.AwairClient.query", side_effect=[local_devices]): + await hass.config_entries.flow.async_init( + DOMAIN, + data=Mock(host=LOCAL_CONFIG[CONF_HOST]), + context={"source": SOURCE_ZEROCONF}, + ) + + # We're being shown the local instructions + form_step = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + {}, + ) + + with patch("python_awair.AwairClient.query", side_effect=[local_devices]), patch( + "homeassistant.components.awair.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + {"device": LOCAL_CONFIG[CONF_HOST]}, + ) + + print(result) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "Awair Element (24947)" + assert result["data"][CONF_HOST] == LOCAL_CONFIG[CONF_HOST] + assert result["result"].unique_id == LOCAL_UNIQUE_ID + + async def test_create_local_entry_awair_error(hass: HomeAssistant): """Test overall flow when using local API and device is returns error.""" @@ -267,6 +315,12 @@ async def test_create_local_entry_awair_error(hass: HomeAssistant): {"next_step_id": "local"}, ) + # We're being shown the local instructions + form_step = await hass.config_entries.flow.async_configure( + form_step["flow_id"], + {}, + ) + result = await hass.config_entries.flow.async_configure( form_step["flow_id"], LOCAL_CONFIG, @@ -274,7 +328,7 @@ async def test_create_local_entry_awair_error(hass: HomeAssistant): # User is returned to form to try again assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "local" + assert result["step_id"] == "local_pick" async def test_create_zeroconf_entry(hass: HomeAssistant, local_devices): From 45b253f65f701c685ecff0fe5bc9d924cd9ee370 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 16 Aug 2022 14:58:55 +0200 Subject: [PATCH 3373/3516] Clean awair debug print (#76864) --- tests/components/awair/test_config_flow.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index 3fdf84f8260..16fca099d8c 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -292,7 +292,6 @@ async def test_create_local_entry_from_discovery(hass: HomeAssistant, local_devi {"device": LOCAL_CONFIG[CONF_HOST]}, ) - print(result) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "Awair Element (24947)" assert result["data"][CONF_HOST] == LOCAL_CONFIG[CONF_HOST] From 00c0ea8869dbe4241a4ad6dea562a62bedea7b68 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 16 Aug 2022 15:33:33 +0200 Subject: [PATCH 3374/3516] Remove stale debug prints (#76865) --- homeassistant/components/unifiprotect/media_source.py | 1 - tests/components/flo/test_sensor.py | 2 -- tests/components/group/test_fan.py | 1 - 3 files changed, 4 deletions(-) diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 17db4db5c3c..f9ea3620be3 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -839,7 +839,6 @@ class ProtectMediaSource(MediaSource): """Return all media source for all UniFi Protect NVRs.""" consoles: list[BrowseMediaSource] = [] - print(len(self.data_sources.values())) for data_source in self.data_sources.values(): if not data_source.api.bootstrap.has_media: continue diff --git a/tests/components/flo/test_sensor.py b/tests/components/flo/test_sensor.py index b5439241d33..c3266a84bd8 100644 --- a/tests/components/flo/test_sensor.py +++ b/tests/components/flo/test_sensor.py @@ -18,7 +18,6 @@ async def test_sensors(hass, config_entry, aioclient_mock_fixture): assert len(hass.data[FLO_DOMAIN][config_entry.entry_id]["devices"]) == 2 # we should have 5 entities for the valve - print(hass.states) assert ( hass.states.get("sensor.smart_water_shutoff_current_system_mode").state == "home" @@ -59,7 +58,6 @@ async def test_sensors(hass, config_entry, aioclient_mock_fixture): ) # and 3 entities for the detector - print(hass.states) assert hass.states.get("sensor.kitchen_sink_temperature").state == "16" assert ( hass.states.get("sensor.kitchen_sink_temperature").attributes[ATTR_STATE_CLASS] diff --git a/tests/components/group/test_fan.py b/tests/components/group/test_fan.py index bb2cf311191..b8e47bfb289 100644 --- a/tests/components/group/test_fan.py +++ b/tests/components/group/test_fan.py @@ -148,7 +148,6 @@ async def test_state(hass, setup_comp): for state_1 in (STATE_UNAVAILABLE, STATE_UNKNOWN): for state_2 in (STATE_UNAVAILABLE, STATE_UNKNOWN): for state_3 in (STATE_UNAVAILABLE, STATE_UNKNOWN): - print("meh") hass.states.async_set(CEILING_FAN_ENTITY_ID, state_1, {}) hass.states.async_set(LIVING_ROOM_FAN_ENTITY_ID, state_2, {}) hass.states.async_set(PERCENTAGE_FULL_FAN_ENTITY_ID, state_3, {}) From 3cb062dc132b3c96c010d0b4a9caf40ccc4daf6f Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Tue, 16 Aug 2022 14:48:01 +0100 Subject: [PATCH 3375/3516] Add System Bridge Media Source (#72865) --- .coveragerc | 1 + .../components/system_bridge/coordinator.py | 34 +++ .../components/system_bridge/manifest.json | 1 + .../components/system_bridge/media_source.py | 209 ++++++++++++++++++ 4 files changed, 245 insertions(+) create mode 100644 homeassistant/components/system_bridge/media_source.py diff --git a/.coveragerc b/.coveragerc index 283adf0d3fc..8b632a524ff 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1223,6 +1223,7 @@ omit = homeassistant/components/system_bridge/binary_sensor.py homeassistant/components/system_bridge/const.py homeassistant/components/system_bridge/coordinator.py + homeassistant/components/system_bridge/media_source.py homeassistant/components/system_bridge/sensor.py homeassistant/components/systemmonitor/sensor.py homeassistant/components/tado/__init__.py diff --git a/homeassistant/components/system_bridge/coordinator.py b/homeassistant/components/system_bridge/coordinator.py index 695dca44342..320c09a6f07 100644 --- a/homeassistant/components/system_bridge/coordinator.py +++ b/homeassistant/components/system_bridge/coordinator.py @@ -20,6 +20,10 @@ from systembridgeconnector.models.disk import Disk from systembridgeconnector.models.display import Display from systembridgeconnector.models.get_data import GetData from systembridgeconnector.models.gpu import Gpu +from systembridgeconnector.models.media_directories import MediaDirectories +from systembridgeconnector.models.media_files import File as MediaFile, MediaFiles +from systembridgeconnector.models.media_get_file import MediaGetFile +from systembridgeconnector.models.media_get_files import MediaGetFiles from systembridgeconnector.models.memory import Memory from systembridgeconnector.models.register_data_listener import RegisterDataListener from systembridgeconnector.models.system import System @@ -100,6 +104,36 @@ class SystemBridgeDataUpdateCoordinator( self.websocket_client.get_data(GetData(modules=modules)) ) + async def async_get_media_directories(self) -> MediaDirectories: + """Get media directories.""" + return await self.websocket_client.get_directories() + + async def async_get_media_files( + self, + base: str, + path: str | None = None, + ) -> MediaFiles: + """Get media files.""" + return await self.websocket_client.get_files( + MediaGetFiles( + base=base, + path=path, + ) + ) + + async def async_get_media_file( + self, + base: str, + path: str, + ) -> MediaFile: + """Get media file.""" + return await self.websocket_client.get_file( + MediaGetFile( + base=base, + path=path, + ) + ) + async def async_handle_module( self, module_name: str, diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index 7968b588814..9370de70787 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -6,6 +6,7 @@ "requirements": ["systembridgeconnector==3.4.4"], "codeowners": ["@timmo001"], "zeroconf": ["_system-bridge._tcp.local."], + "dependencies": ["media_source"], "after_dependencies": ["zeroconf"], "quality_scale": "silver", "iot_class": "local_push", diff --git a/homeassistant/components/system_bridge/media_source.py b/homeassistant/components/system_bridge/media_source.py new file mode 100644 index 00000000000..6190cc1c5fe --- /dev/null +++ b/homeassistant/components/system_bridge/media_source.py @@ -0,0 +1,209 @@ +"""System Bridge Media Source Implementation.""" +from __future__ import annotations + +from systembridgeconnector.models.media_directories import MediaDirectories +from systembridgeconnector.models.media_files import File as MediaFile, MediaFiles + +from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY +from homeassistant.components.media_source.const import ( + MEDIA_CLASS_MAP, + MEDIA_MIME_TYPES, +) +from homeassistant.components.media_source.models import ( + BrowseMediaSource, + MediaSource, + MediaSourceItem, + PlayMedia, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import SystemBridgeDataUpdateCoordinator + + +async def async_get_media_source(hass: HomeAssistant) -> MediaSource: + """Set up SystemBridge media source.""" + return SystemBridgeSource(hass) + + +class SystemBridgeSource(MediaSource): + """Provide System Bridge media files as a media source.""" + + def __init__( + self, + hass: HomeAssistant, + ) -> None: + """Initialize source.""" + super().__init__(DOMAIN) + self.name = "System Bridge" + self.hass: HomeAssistant = hass + + async def async_resolve_media( + self, + item: MediaSourceItem, + ) -> PlayMedia: + """Resolve media to a url.""" + entry_id, path, mime_type = item.identifier.split("~~", 2) + entry = self.hass.config_entries.async_get_entry(entry_id) + if entry is None: + raise ValueError("Invalid entry") + path_split = path.split("/", 1) + return PlayMedia( + f"{_build_base_url(entry)}&base={path_split[0]}&path={path_split[1]}", + mime_type, + ) + + async def async_browse_media( + self, + item: MediaSourceItem, + ) -> BrowseMediaSource: + """Return media.""" + if not item.identifier: + return self._build_bridges() + + if "~~" not in item.identifier: + entry = self.hass.config_entries.async_get_entry(item.identifier) + if entry is None: + raise ValueError("Invalid entry") + coordinator: SystemBridgeDataUpdateCoordinator = self.hass.data[DOMAIN].get( + entry.entry_id + ) + directories = await coordinator.async_get_media_directories() + return _build_root_paths(entry, directories) + + entry_id, path = item.identifier.split("~~", 1) + entry = self.hass.config_entries.async_get_entry(entry_id) + if entry is None: + raise ValueError("Invalid entry") + + coordinator = self.hass.data[DOMAIN].get(entry.entry_id) + + path_split = path.split("/", 1) + + files = await coordinator.async_get_media_files( + path_split[0], path_split[1] if len(path_split) > 1 else None + ) + + return _build_media_items(entry, files, path, item.identifier) + + def _build_bridges(self) -> BrowseMediaSource: + """Build bridges for System Bridge media.""" + children = [] + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.entry_id is not None: + children.append( + BrowseMediaSource( + domain=DOMAIN, + identifier=entry.entry_id, + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type="", + title=entry.title, + can_play=False, + can_expand=True, + children=[], + children_media_class=MEDIA_CLASS_DIRECTORY, + ) + ) + + return BrowseMediaSource( + domain=DOMAIN, + identifier="", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type="", + title=self.name, + can_play=False, + can_expand=True, + children=children, + children_media_class=MEDIA_CLASS_DIRECTORY, + ) + + +def _build_base_url( + entry: ConfigEntry, +) -> str: + """Build base url for System Bridge media.""" + return f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}/api/media/file/data?apiKey={entry.data[CONF_API_KEY]}" + + +def _build_root_paths( + entry: ConfigEntry, + media_directories: MediaDirectories, +) -> BrowseMediaSource: + """Build base categories for System Bridge media.""" + return BrowseMediaSource( + domain=DOMAIN, + identifier="", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type="", + title=entry.title, + can_play=False, + can_expand=True, + children=[ + BrowseMediaSource( + domain=DOMAIN, + identifier=f"{entry.entry_id}~~{directory.key}", + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type="", + title=f"{directory.key[:1].capitalize()}{directory.key[1:]}", + can_play=False, + can_expand=True, + children=[], + children_media_class=MEDIA_CLASS_DIRECTORY, + ) + for directory in media_directories.directories + ], + children_media_class=MEDIA_CLASS_DIRECTORY, + ) + + +def _build_media_items( + entry: ConfigEntry, + media_files: MediaFiles, + path: str, + identifier: str, +) -> BrowseMediaSource: + """Fetch requested files.""" + return BrowseMediaSource( + domain=DOMAIN, + identifier=identifier, + media_class=MEDIA_CLASS_DIRECTORY, + media_content_type="", + title=f"{entry.title} - {path}", + can_play=False, + can_expand=True, + children=[ + _build_media_item(identifier, file) + for file in media_files.files + if file.is_directory + or ( + file.is_file + and file.mime_type is not None + and file.mime_type.startswith(MEDIA_MIME_TYPES) + ) + ], + ) + + +def _build_media_item( + path: str, + media_file: MediaFile, +) -> BrowseMediaSource: + """Build individual media item.""" + ext = ( + f"~~{media_file.mime_type}" + if media_file.is_file and media_file.mime_type is not None + else "" + ) + return BrowseMediaSource( + domain=DOMAIN, + identifier=f"{path}/{media_file.name}{ext}", + media_class=MEDIA_CLASS_DIRECTORY + if media_file.is_directory or media_file.mime_type is None + else MEDIA_CLASS_MAP[media_file.mime_type.split("/", 1)[0]], + media_content_type=media_file.mime_type, + title=media_file.name, + can_play=media_file.is_file, + can_expand=media_file.is_directory, + ) From 735dec8dde2be75363e1bf12bd5f5e1d0b3f0d5f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 16 Aug 2022 15:53:16 +0200 Subject: [PATCH 3376/3516] Process UniFi Protect review comments (#76870) --- .../components/unifiprotect/media_source.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index f9ea3620be3..7e66f783ee0 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -344,11 +344,11 @@ class ProtectMediaSource(MediaSource): return await self._build_event(data, event, thumbnail_only) - async def get_registry(self) -> er.EntityRegistry: + @callback + def async_get_registry(self) -> er.EntityRegistry: """Get or return Entity Registry.""" - if self._registry is None: - self._registry = await er.async_get_registry(self.hass) + self._registry = er.async_get(self.hass) return self._registry def _breadcrumb( @@ -473,9 +473,8 @@ class ProtectMediaSource(MediaSource): continue # smart detect events have a paired motion event - if ( - event.get("type") == EventType.MOTION.value - and len(event.get("smartDetectEvents", [])) > 0 + if event.get("type") == EventType.MOTION.value and event.get( + "smartDetectEvents" ): continue @@ -717,7 +716,7 @@ class ProtectMediaSource(MediaSource): return None entity_id: str | None = None - entity_registry = await self.get_registry() + entity_registry = self.async_get_registry() for channel in camera.channels: # do not use the package camera if channel.id == 3: From 563ec67d39fcb8fc34df3344276147d5fc5acf0a Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 16 Aug 2022 16:10:37 +0200 Subject: [PATCH 3377/3516] Add strict typing for auth (#75586) --- .strict-typing | 1 + homeassistant/components/auth/__init__.py | 86 ++++++++++++------- homeassistant/components/auth/indieauth.py | 30 ++++--- homeassistant/components/auth/login_flow.py | 64 +++++++++----- .../components/auth/mfa_setup_flow.py | 58 ++++++++----- homeassistant/data_entry_flow.py | 2 +- mypy.ini | 10 +++ tests/components/auth/test_mfa_setup_flow.py | 8 +- 8 files changed, 174 insertions(+), 85 deletions(-) diff --git a/.strict-typing b/.strict-typing index 1574ea09f2e..db51acc07df 100644 --- a/.strict-typing +++ b/.strict-typing @@ -59,6 +59,7 @@ homeassistant.components.ampio.* homeassistant.components.anthemav.* homeassistant.components.aseko_pool_live.* homeassistant.components.asuswrt.* +homeassistant.components.auth.* homeassistant.components.automation.* homeassistant.components.backup.* homeassistant.components.baf.* diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index 897ca037c98..a8c7019030f 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -124,15 +124,22 @@ as part of a config flow. """ from __future__ import annotations -from datetime import timedelta +from collections.abc import Callable +from datetime import datetime, timedelta from http import HTTPStatus +from typing import Any, Optional, cast import uuid from aiohttp import web +from multidict import MultiDictProxy import voluptuous as vol from homeassistant.auth import InvalidAuthError -from homeassistant.auth.models import TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, Credentials +from homeassistant.auth.models import ( + TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, + Credentials, + User, +) from homeassistant.components import websocket_api from homeassistant.components.http.auth import ( async_sign_path, @@ -151,11 +158,16 @@ from . import indieauth, login_flow, mfa_setup_flow DOMAIN = "auth" +StoreResultType = Callable[[str, Credentials], str] +RetrieveResultType = Callable[[str, str], Optional[Credentials]] + @bind_hass -def create_auth_code(hass, client_id: str, credential: Credentials) -> str: +def create_auth_code( + hass: HomeAssistant, client_id: str, credential: Credentials +) -> str: """Create an authorization code to fetch tokens.""" - return hass.data[DOMAIN](client_id, credential) + return cast(StoreResultType, hass.data[DOMAIN])(client_id, credential) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -188,15 +200,15 @@ class TokenView(HomeAssistantView): requires_auth = False cors_allowed = True - def __init__(self, retrieve_auth): + def __init__(self, retrieve_auth: RetrieveResultType) -> None: """Initialize the token view.""" self._retrieve_auth = retrieve_auth @log_invalid_auth - async def post(self, request): + async def post(self, request: web.Request) -> web.Response: """Grant a token.""" - hass = request.app["hass"] - data = await request.post() + hass: HomeAssistant = request.app["hass"] + data = cast(MultiDictProxy[str], await request.post()) grant_type = data.get("grant_type") @@ -217,7 +229,11 @@ class TokenView(HomeAssistantView): {"error": "unsupported_grant_type"}, status_code=HTTPStatus.BAD_REQUEST ) - async def _async_handle_revoke_token(self, hass, data): + async def _async_handle_revoke_token( + self, + hass: HomeAssistant, + data: MultiDictProxy[str], + ) -> web.Response: """Handle revoke token request.""" # OAuth 2.0 Token Revocation [RFC7009] @@ -235,7 +251,12 @@ class TokenView(HomeAssistantView): await hass.auth.async_remove_refresh_token(refresh_token) return web.Response(status=HTTPStatus.OK) - async def _async_handle_auth_code(self, hass, data, remote_addr): + async def _async_handle_auth_code( + self, + hass: HomeAssistant, + data: MultiDictProxy[str], + remote_addr: str | None, + ) -> web.Response: """Handle authorization code request.""" client_id = data.get("client_id") if client_id is None or not indieauth.verify_client_id(client_id): @@ -298,7 +319,12 @@ class TokenView(HomeAssistantView): }, ) - async def _async_handle_refresh_token(self, hass, data, remote_addr): + async def _async_handle_refresh_token( + self, + hass: HomeAssistant, + data: MultiDictProxy[str], + remote_addr: str | None, + ) -> web.Response: """Handle authorization code request.""" client_id = data.get("client_id") if client_id is not None and not indieauth.verify_client_id(client_id): @@ -366,15 +392,15 @@ class LinkUserView(HomeAssistantView): url = "/auth/link_user" name = "api:auth:link_user" - def __init__(self, retrieve_credentials): + def __init__(self, retrieve_credentials: RetrieveResultType) -> None: """Initialize the link user view.""" self._retrieve_credentials = retrieve_credentials @RequestDataValidator(vol.Schema({"code": str, "client_id": str})) - async def post(self, request, data): + async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Link a user.""" - hass = request.app["hass"] - user = request["hass_user"] + hass: HomeAssistant = request.app["hass"] + user: User = request["hass_user"] credentials = self._retrieve_credentials(data["client_id"], data["code"]) @@ -394,12 +420,12 @@ class LinkUserView(HomeAssistantView): @callback -def _create_auth_code_store(): +def _create_auth_code_store() -> tuple[StoreResultType, RetrieveResultType]: """Create an in memory store.""" - temp_results = {} + temp_results: dict[tuple[str, str], tuple[datetime, Credentials]] = {} @callback - def store_result(client_id, result): + def store_result(client_id: str, result: Credentials) -> str: """Store flow result and return a code to retrieve it.""" if not isinstance(result, Credentials): raise ValueError("result has to be a Credentials instance") @@ -412,7 +438,7 @@ def _create_auth_code_store(): return code @callback - def retrieve_result(client_id, code): + def retrieve_result(client_id: str, code: str) -> Credentials | None: """Retrieve flow result.""" key = (client_id, code) @@ -437,8 +463,8 @@ def _create_auth_code_store(): @websocket_api.ws_require_user() @websocket_api.async_response async def websocket_current_user( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg -): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: """Return the current user.""" user = connection.user enabled_modules = await hass.auth.async_get_enabled_mfa(user) @@ -482,8 +508,8 @@ async def websocket_current_user( @websocket_api.ws_require_user() @websocket_api.async_response async def websocket_create_long_lived_access_token( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg -): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: """Create or a long-lived access token.""" refresh_token = await hass.auth.async_create_refresh_token( connection.user, @@ -506,12 +532,12 @@ async def websocket_create_long_lived_access_token( @websocket_api.ws_require_user() @callback def websocket_refresh_tokens( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg -): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: """Return metadata of users refresh tokens.""" current_id = connection.refresh_token_id - tokens = [] + tokens: list[dict[str, Any]] = [] for refresh in connection.user.refresh_tokens.values(): if refresh.credential: auth_provider_type = refresh.credential.auth_provider_type @@ -545,8 +571,8 @@ def websocket_refresh_tokens( @websocket_api.ws_require_user() @websocket_api.async_response async def websocket_delete_refresh_token( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg -): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: """Handle a delete refresh token request.""" refresh_token = connection.user.refresh_tokens.get(msg["refresh_token_id"]) @@ -569,8 +595,8 @@ async def websocket_delete_refresh_token( @websocket_api.ws_require_user() @callback def websocket_sign_path( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg -): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: """Handle a sign path request.""" connection.send_message( websocket_api.result_message( diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index e823659f62b..fc4c298ca6c 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -1,18 +1,24 @@ """Helpers to resolve client ID/secret.""" +from __future__ import annotations + import asyncio from html.parser import HTMLParser from ipaddress import ip_address import logging -from urllib.parse import urljoin, urlparse +from urllib.parse import ParseResult, urljoin, urlparse import aiohttp +import aiohttp.client_exceptions +from homeassistant.core import HomeAssistant from homeassistant.util.network import is_local _LOGGER = logging.getLogger(__name__) -async def verify_redirect_uri(hass, client_id, redirect_uri): +async def verify_redirect_uri( + hass: HomeAssistant, client_id: str, redirect_uri: str +) -> bool: """Verify that the client and redirect uri match.""" try: client_id_parts = _parse_client_id(client_id) @@ -47,24 +53,24 @@ async def verify_redirect_uri(hass, client_id, redirect_uri): class LinkTagParser(HTMLParser): """Parser to find link tags.""" - def __init__(self, rel): + def __init__(self, rel: str) -> None: """Initialize a link tag parser.""" super().__init__() self.rel = rel - self.found = [] + self.found: list[str | None] = [] - def handle_starttag(self, tag, attrs): + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: """Handle finding a start tag.""" if tag != "link": return - attrs = dict(attrs) + attributes: dict[str, str | None] = dict(attrs) - if attrs.get("rel") == self.rel: - self.found.append(attrs.get("href")) + if attributes.get("rel") == self.rel: + self.found.append(attributes.get("href")) -async def fetch_redirect_uris(hass, url): +async def fetch_redirect_uris(hass: HomeAssistant, url: str) -> list[str]: """Find link tag with redirect_uri values. IndieAuth 4.2.2 @@ -108,7 +114,7 @@ async def fetch_redirect_uris(hass, url): return [urljoin(url, found) for found in parser.found] -def verify_client_id(client_id): +def verify_client_id(client_id: str) -> bool: """Verify that the client id is valid.""" try: _parse_client_id(client_id) @@ -117,7 +123,7 @@ def verify_client_id(client_id): return False -def _parse_url(url): +def _parse_url(url: str) -> ParseResult: """Parse a url in parts and canonicalize according to IndieAuth.""" parts = urlparse(url) @@ -134,7 +140,7 @@ def _parse_url(url): return parts -def _parse_client_id(client_id): +def _parse_client_id(client_id: str) -> ParseResult: """Test if client id is a valid URL according to IndieAuth section 3.2. https://indieauth.spec.indieweb.org/#client-identifier diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index 6cc9d94c7a6..dc094cd581f 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -66,14 +66,19 @@ associate with an credential if "type" set to "link_user" in "version": 1 } """ +from __future__ import annotations + +from collections.abc import Callable from http import HTTPStatus from ipaddress import ip_address +from typing import TYPE_CHECKING, Any from aiohttp import web import voluptuous as vol import voluptuous_serialize from homeassistant import data_entry_flow +from homeassistant.auth import AuthManagerFlowManager from homeassistant.auth.models import Credentials from homeassistant.components import onboarding from homeassistant.components.http.auth import async_user_not_allowed_do_auth @@ -88,8 +93,13 @@ from homeassistant.core import HomeAssistant from . import indieauth +if TYPE_CHECKING: + from . import StoreResultType -async def async_setup(hass, store_result): + +async def async_setup( + hass: HomeAssistant, store_result: Callable[[str, Credentials], str] +) -> None: """Component to allow users to login.""" hass.http.register_view(AuthProvidersView) hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow, store_result)) @@ -103,9 +113,9 @@ class AuthProvidersView(HomeAssistantView): name = "api:auth:providers" requires_auth = False - async def get(self, request): + async def get(self, request: web.Request) -> web.Response: """Get available auth providers.""" - hass = request.app["hass"] + hass: HomeAssistant = request.app["hass"] if not onboarding.async_is_user_onboarded(hass): return self.json_message( message="Onboarding not finished", @@ -121,7 +131,9 @@ class AuthProvidersView(HomeAssistantView): ) -def _prepare_result_json(result): +def _prepare_result_json( + result: data_entry_flow.FlowResult, +) -> data_entry_flow.FlowResult: """Convert result to JSON.""" if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: data = result.copy() @@ -147,12 +159,21 @@ class LoginFlowBaseView(HomeAssistantView): requires_auth = False - def __init__(self, flow_mgr, store_result): + def __init__( + self, + flow_mgr: AuthManagerFlowManager, + store_result: StoreResultType, + ) -> None: """Initialize the flow manager index view.""" self._flow_mgr = flow_mgr self._store_result = store_result - async def _async_flow_result_to_response(self, request, client_id, result): + async def _async_flow_result_to_response( + self, + request: web.Request, + client_id: str, + result: data_entry_flow.FlowResult, + ) -> web.Response: """Convert the flow result to a response.""" if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY: # @log_invalid_auth does not work here since it returns HTTP 200. @@ -196,7 +217,7 @@ class LoginFlowIndexView(LoginFlowBaseView): url = "/auth/login_flow" name = "api:auth:login_flow" - async def get(self, request): + async def get(self, request: web.Request) -> web.Response: """Do not allow index of flows in progress.""" return web.Response(status=HTTPStatus.METHOD_NOT_ALLOWED) @@ -211,15 +232,18 @@ class LoginFlowIndexView(LoginFlowBaseView): ) ) @log_invalid_auth - async def post(self, request, data): + async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Create a new login flow.""" - if not await indieauth.verify_redirect_uri( - request.app["hass"], data["client_id"], data["redirect_uri"] - ): + hass: HomeAssistant = request.app["hass"] + client_id: str = data["client_id"] + redirect_uri: str = data["redirect_uri"] + + if not await indieauth.verify_redirect_uri(hass, client_id, redirect_uri): return self.json_message( "invalid client id or redirect uri", HTTPStatus.BAD_REQUEST ) + handler: tuple[str, ...] | str if isinstance(data["handler"], list): handler = tuple(data["handler"]) else: @@ -227,9 +251,9 @@ class LoginFlowIndexView(LoginFlowBaseView): try: result = await self._flow_mgr.async_init( - handler, + handler, # type: ignore[arg-type] context={ - "ip_address": ip_address(request.remote), + "ip_address": ip_address(request.remote), # type: ignore[arg-type] "credential_only": data.get("type") == "link_user", }, ) @@ -240,9 +264,7 @@ class LoginFlowIndexView(LoginFlowBaseView): "Handler does not support init", HTTPStatus.BAD_REQUEST ) - return await self._async_flow_result_to_response( - request, data["client_id"], result - ) + return await self._async_flow_result_to_response(request, client_id, result) class LoginFlowResourceView(LoginFlowBaseView): @@ -251,13 +273,15 @@ class LoginFlowResourceView(LoginFlowBaseView): url = "/auth/login_flow/{flow_id}" name = "api:auth:login_flow:resource" - async def get(self, request): + async def get(self, request: web.Request) -> web.Response: """Do not allow getting status of a flow in progress.""" return self.json_message("Invalid flow specified", HTTPStatus.NOT_FOUND) @RequestDataValidator(vol.Schema({"client_id": str}, extra=vol.ALLOW_EXTRA)) @log_invalid_auth - async def post(self, request, data, flow_id): + async def post( + self, request: web.Request, data: dict[str, Any], flow_id: str + ) -> web.Response: """Handle progressing a login flow request.""" client_id = data.pop("client_id") @@ -267,7 +291,7 @@ class LoginFlowResourceView(LoginFlowBaseView): try: # do not allow change ip during login flow flow = self._flow_mgr.async_get(flow_id) - if flow["context"]["ip_address"] != ip_address(request.remote): + if flow["context"]["ip_address"] != ip_address(request.remote): # type: ignore[arg-type] return self.json_message("IP address changed", HTTPStatus.BAD_REQUEST) result = await self._flow_mgr.async_configure(flow_id, data) except data_entry_flow.UnknownFlow: @@ -277,7 +301,7 @@ class LoginFlowResourceView(LoginFlowBaseView): return await self._async_flow_result_to_response(request, client_id, result) - async def delete(self, request, flow_id): + async def delete(self, request: web.Request, flow_id: str) -> web.Response: """Cancel a flow in progress.""" try: self._flow_mgr.async_abort(flow_id) diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index e288fe33df7..d6a9282e089 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -1,5 +1,8 @@ """Helpers to setup multi-factor auth module.""" +from __future__ import annotations + import logging +from typing import Any import voluptuous as vol import voluptuous_serialize @@ -7,15 +10,19 @@ import voluptuous_serialize from homeassistant import data_entry_flow from homeassistant.components import websocket_api from homeassistant.core import HomeAssistant, callback +import homeassistant.helpers.config_validation as cv WS_TYPE_SETUP_MFA = "auth/setup_mfa" -SCHEMA_WS_SETUP_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - { - vol.Required("type"): WS_TYPE_SETUP_MFA, - vol.Exclusive("mfa_module_id", "module_or_flow_id"): str, - vol.Exclusive("flow_id", "module_or_flow_id"): str, - vol.Optional("user_input"): object, - } +SCHEMA_WS_SETUP_MFA = vol.All( + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + vol.Required("type"): WS_TYPE_SETUP_MFA, + vol.Exclusive("mfa_module_id", "module_or_flow_id"): str, + vol.Exclusive("flow_id", "module_or_flow_id"): str, + vol.Optional("user_input"): object, + } + ), + cv.has_at_least_one_key("mfa_module_id", "flow_id"), ) WS_TYPE_DEPOSE_MFA = "auth/depose_mfa" @@ -31,7 +38,13 @@ _LOGGER = logging.getLogger(__name__) class MfaFlowManager(data_entry_flow.FlowManager): """Manage multi factor authentication flows.""" - async def async_create_flow(self, handler_key, *, context, data): + async def async_create_flow( # type: ignore[override] + self, + handler_key: Any, + *, + context: dict[str, Any], + data: dict[str, Any], + ) -> data_entry_flow.FlowHandler: """Create a setup flow. handler is a mfa module.""" mfa_module = self.hass.auth.get_auth_mfa_module(handler_key) if mfa_module is None: @@ -40,13 +53,15 @@ class MfaFlowManager(data_entry_flow.FlowManager): user_id = data.pop("user_id") return await mfa_module.async_setup_flow(user_id) - async def async_finish_flow(self, flow, result): + async def async_finish_flow( + self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult + ) -> data_entry_flow.FlowResult: """Complete an mfs setup flow.""" _LOGGER.debug("flow_result: %s", result) return result -async def async_setup(hass): +async def async_setup(hass: HomeAssistant) -> None: """Init mfa setup flow manager.""" hass.data[DATA_SETUP_FLOW_MGR] = MfaFlowManager(hass) @@ -62,13 +77,13 @@ async def async_setup(hass): @callback @websocket_api.ws_require_user(allow_system_user=False) def websocket_setup_mfa( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg -): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: """Return a setup flow for mfa auth module.""" - async def async_setup_flow(msg): + async def async_setup_flow(msg: dict[str, Any]) -> None: """Return a setup flow for mfa auth module.""" - flow_manager = hass.data[DATA_SETUP_FLOW_MGR] + flow_manager: MfaFlowManager = hass.data[DATA_SETUP_FLOW_MGR] if (flow_id := msg.get("flow_id")) is not None: result = await flow_manager.async_configure(flow_id, msg.get("user_input")) @@ -77,9 +92,8 @@ def websocket_setup_mfa( ) return - mfa_module_id = msg.get("mfa_module_id") - mfa_module = hass.auth.get_auth_mfa_module(mfa_module_id) - if mfa_module is None: + mfa_module_id = msg["mfa_module_id"] + if hass.auth.get_auth_mfa_module(mfa_module_id) is None: connection.send_message( websocket_api.error_message( msg["id"], "no_module", f"MFA module {mfa_module_id} is not found" @@ -101,11 +115,11 @@ def websocket_setup_mfa( @callback @websocket_api.ws_require_user(allow_system_user=False) def websocket_depose_mfa( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg -): + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: """Remove user from mfa module.""" - async def async_depose(msg): + async def async_depose(msg: dict[str, Any]) -> None: """Remove user from mfa auth module.""" mfa_module_id = msg["mfa_module_id"] try: @@ -127,7 +141,9 @@ def websocket_depose_mfa( hass.async_create_task(async_depose(msg)) -def _prepare_result_json(result): +def _prepare_result_json( + result: data_entry_flow.FlowResult, +) -> data_entry_flow.FlowResult: """Convert result to JSON.""" if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: data = result.copy() diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 23b35138df7..64750b2ff50 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -175,7 +175,7 @@ class FlowManager(abc.ABC): ) @callback - def async_get(self, flow_id: str) -> FlowResult | None: + def async_get(self, flow_id: str) -> FlowResult: """Return a flow in progress as a partial FlowResult.""" if (flow := self._progress.get(flow_id)) is None: raise UnknownFlow diff --git a/mypy.ini b/mypy.ini index 7338d9a67f0..5d3b184880d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -349,6 +349,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.auth.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.automation.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/auth/test_mfa_setup_flow.py b/tests/components/auth/test_mfa_setup_flow.py index b0851a3cfe6..c2ff5a31d75 100644 --- a/tests/components/auth/test_mfa_setup_flow.py +++ b/tests/components/auth/test_mfa_setup_flow.py @@ -44,7 +44,13 @@ async def test_ws_setup_depose_mfa(hass, hass_ws_client): client = await hass_ws_client(hass, access_token) - await client.send_json({"id": 10, "type": mfa_setup_flow.WS_TYPE_SETUP_MFA}) + await client.send_json( + { + "id": 10, + "type": mfa_setup_flow.WS_TYPE_SETUP_MFA, + "mfa_module_id": "invalid_module", + } + ) result = await client.receive_json() assert result["id"] == 10 From 3d567d2c1b79caf321b291c26e28f88062886d0a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 16 Aug 2022 16:18:40 +0200 Subject: [PATCH 3378/3516] Update numpy to 1.23.2 (#76855) --- homeassistant/components/compensation/manifest.json | 2 +- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/gen_requirements_all.py | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/compensation/manifest.json b/homeassistant/components/compensation/manifest.json index 5b1ff9715d1..a774541bd36 100644 --- a/homeassistant/components/compensation/manifest.json +++ b/homeassistant/components/compensation/manifest.json @@ -2,7 +2,7 @@ "domain": "compensation", "name": "Compensation", "documentation": "https://www.home-assistant.io/integrations/compensation", - "requirements": ["numpy==1.23.1"], + "requirements": ["numpy==1.23.2"], "codeowners": ["@Petro31"], "iot_class": "calculated" } diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 561ebdb6e89..9da0af32da3 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,7 +3,7 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.23.1", "pyiqvia==2022.04.0"], + "requirements": ["numpy==1.23.2", "pyiqvia==2022.04.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["pyiqvia"] diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index cada1199084..8a1658a5302 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,7 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.23.1", "opencv-python-headless==4.6.0.66"], + "requirements": ["numpy==1.23.2", "opencv-python-headless==4.6.0.66"], "codeowners": [], "iot_class": "local_push" } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index c8a62791429..9f719b7e1b3 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -6,7 +6,7 @@ "tensorflow==2.5.0", "tf-models-official==2.5.0", "pycocotools==2.0.1", - "numpy==1.23.1", + "numpy==1.23.2", "pillow==9.2.0" ], "codeowners": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index e70056f207a..14bce1a0757 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.23.1"], + "requirements": ["numpy==1.23.2"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b7c2fd8cbb5..82c37466db7 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -91,7 +91,7 @@ httpcore==0.15.0 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy==1.23.1 +numpy==1.23.2 # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 184c6e4d77e..5b34c0b0995 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1140,7 +1140,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.23.1 +numpy==1.23.2 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8e7774d313..3d79f5ff7d8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -809,7 +809,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.23.1 +numpy==1.23.2 # homeassistant.components.google oauth2client==4.1.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 3cb35eec147..3709d4cff08 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -105,7 +105,7 @@ httpcore==0.15.0 hyperframe>=5.2.0 # Ensure we run compatible with musllinux build env -numpy==1.23.1 +numpy==1.23.2 # pytest_asyncio breaks our test suite. We rely on pytest-aiohttp instead pytest_asyncio==1000000000.0.0 From 2e191d6a60a8904644c536c4d3103f5e9bb6158f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 16 Aug 2022 16:30:04 +0200 Subject: [PATCH 3379/3516] Update sentry-sdk to 1.9.5 (#76857) --- homeassistant/components/sentry/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 2360180d1cf..849b40170f8 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,7 +3,7 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==1.9.3"], + "requirements": ["sentry-sdk==1.9.5"], "codeowners": ["@dcramer", "@frenck"], "iot_class": "cloud_polling" } diff --git a/requirements_all.txt b/requirements_all.txt index 5b34c0b0995..76ee358edb6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2176,7 +2176,7 @@ sense_energy==0.10.4 sensorpush-ble==1.5.2 # homeassistant.components.sentry -sentry-sdk==1.9.3 +sentry-sdk==1.9.5 # homeassistant.components.sharkiq sharkiq==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3d79f5ff7d8..3c6af75e928 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1473,7 +1473,7 @@ sense_energy==0.10.4 sensorpush-ble==1.5.2 # homeassistant.components.sentry -sentry-sdk==1.9.3 +sentry-sdk==1.9.5 # homeassistant.components.sharkiq sharkiq==0.0.1 From 63d71457aacb56c90ff89852fb201f74562ebec2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 16 Aug 2022 16:31:09 +0200 Subject: [PATCH 3380/3516] Type BrowseMedia children as a covariant (#76869) --- homeassistant/components/apple_tv/media_player.py | 2 +- homeassistant/components/jellyfin/media_source.py | 10 +++++----- .../components/media_player/browse_media.py | 3 ++- homeassistant/components/media_source/models.py | 2 -- .../components/unifiprotect/media_source.py | 4 ++-- homeassistant/components/xbox/browse_media.py | 13 ++++++------- 6 files changed, 16 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 362a09fb5fc..3a495e053eb 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -422,7 +422,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): return cur_item # Add app item if we have one - if self._app_list and cur_item.children: + if self._app_list and cur_item.children and isinstance(cur_item.children, list): cur_item.children.insert(0, build_app_list(self._app_list)) return cur_item diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index 8a09fd8d552..662c0f0040a 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -173,10 +173,10 @@ class JellyfinSource(MediaSource): if include_children: result.children_media_class = MEDIA_CLASS_ARTIST - result.children = await self._build_artists(library_id) # type: ignore[assignment] + result.children = await self._build_artists(library_id) if not result.children: result.children_media_class = MEDIA_CLASS_ALBUM - result.children = await self._build_albums(library_id) # type: ignore[assignment] + result.children = await self._build_albums(library_id) return result @@ -207,7 +207,7 @@ class JellyfinSource(MediaSource): if include_children: result.children_media_class = MEDIA_CLASS_ALBUM - result.children = await self._build_albums(artist_id) # type: ignore[assignment] + result.children = await self._build_albums(artist_id) return result @@ -238,7 +238,7 @@ class JellyfinSource(MediaSource): if include_children: result.children_media_class = MEDIA_CLASS_TRACK - result.children = await self._build_tracks(album_id) # type: ignore[assignment] + result.children = await self._build_tracks(album_id) return result @@ -293,7 +293,7 @@ class JellyfinSource(MediaSource): if include_children: result.children_media_class = MEDIA_CLASS_MOVIE - result.children = await self._build_movies(library_id) # type: ignore[assignment] + result.children = await self._build_movies(library_id) return result diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index 9327bf68f9f..e3474eeb58e 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -1,6 +1,7 @@ """Browse media features for media player.""" from __future__ import annotations +from collections.abc import Sequence from datetime import timedelta import logging from typing import Any @@ -97,7 +98,7 @@ class BrowseMedia: title: str, can_play: bool, can_expand: bool, - children: list[BrowseMedia] | None = None, + children: Sequence[BrowseMedia] | None = None, children_media_class: str | None = None, thumbnail: str | None = None, not_shown: int = 0, diff --git a/homeassistant/components/media_source/models.py b/homeassistant/components/media_source/models.py index 0aee6ad1330..f6772bc6ad9 100644 --- a/homeassistant/components/media_source/models.py +++ b/homeassistant/components/media_source/models.py @@ -27,8 +27,6 @@ class PlayMedia: class BrowseMediaSource(BrowseMedia): """Represent a browsable media file.""" - children: list[BrowseMediaSource | BrowseMedia] | None - def __init__( self, *, domain: str | None, identifier: str | None, **kwargs: Any ) -> None: diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index 7e66f783ee0..104323eeaa2 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -528,7 +528,7 @@ class ProtectMediaSource(MediaSource): args["camera_id"] = camera_id events = await self._build_events(**args) # type: ignore[arg-type] - source.children = events # type: ignore[assignment] + source.children = events source.title = self._breadcrumb( data, title, @@ -653,7 +653,7 @@ class ProtectMediaSource(MediaSource): title = f"{start.strftime('%B %Y')} > {title}" events = await self._build_events(**args) # type: ignore[arg-type] - source.children = events # type: ignore[assignment] + source.children = events source.title = self._breadcrumb( data, title, diff --git a/homeassistant/components/xbox/browse_media.py b/homeassistant/components/xbox/browse_media.py index ee1eabf1e00..e4c268bd6b8 100644 --- a/homeassistant/components/xbox/browse_media.py +++ b/homeassistant/components/xbox/browse_media.py @@ -1,7 +1,7 @@ """Support for media browsing.""" from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from typing import NamedTuple from xbox.webapi.api.client import XboxLiveClient from xbox.webapi.api.provider.catalog.const import HOME_APP_IDS, SYSTEM_PFN_ID_MAP @@ -56,6 +56,7 @@ async def build_item_response( apps: InstalledPackagesList = await client.smartglass.get_installed_apps(device_id) if media_content_type in (None, "library"): + children: list[BrowseMedia] = [] library_info = BrowseMedia( media_class=MEDIA_CLASS_DIRECTORY, media_content_id="library", @@ -63,10 +64,8 @@ async def build_item_response( title="Installed Applications", can_play=False, can_expand=True, - children=[], + children=children, ) - if TYPE_CHECKING: - assert library_info.children is not None # Add Home id_type = AlternateIdType.LEGACY_XBOX_PRODUCT_ID @@ -78,7 +77,7 @@ async def build_item_response( home_thumb = _find_media_image( home_catalog.products[0].localized_properties[0].images ) - library_info.children.append( + children.append( BrowseMedia( media_class=MEDIA_CLASS_APP, media_content_id="Home", @@ -101,7 +100,7 @@ async def build_item_response( tv_thumb = _find_media_image( tv_catalog.products[0].localized_properties[0].images ) - library_info.children.append( + children.append( BrowseMedia( media_class=MEDIA_CLASS_APP, media_content_id="TV", @@ -117,7 +116,7 @@ async def build_item_response( {app.content_type for app in apps.result if app.content_type in TYPE_MAP} ) for c_type in content_types: - library_info.children.append( + children.append( BrowseMedia( media_class=MEDIA_CLASS_DIRECTORY, media_content_id=c_type, From 73001e29ff22f7a5008a0b0a0e058aae22771d16 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 16 Aug 2022 16:47:21 +0200 Subject: [PATCH 3381/3516] Remove deprecated white_value support from MQTT light (#76848) * Remove deprecated white_value support from MQTT light * Remove deprecated white_value support from MQTT JSON light * Remove deprecated white_value support from MQTT template light --- .../components/mqtt/light/schema_basic.py | 207 +---- .../components/mqtt/light/schema_json.py | 37 +- .../components/mqtt/light/schema_template.py | 36 +- tests/components/mqtt/test_light.py | 735 +----------------- tests/components/mqtt/test_light_json.py | 136 +--- tests/components/mqtt/test_light_template.py | 145 ++-- 6 files changed, 133 insertions(+), 1163 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 1c94fa82f73..05778aa7711 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -17,13 +17,8 @@ from homeassistant.components.light import ( ATTR_RGBWW_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_WHITE, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, ENTITY_ID_FORMAT, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, - SUPPORT_WHITE_VALUE, ColorMode, LightEntity, LightEntityFeature, @@ -119,7 +114,6 @@ MQTT_LIGHT_ATTRIBUTES_BLOCKED = frozenset( ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, ATTR_SUPPORTED_COLOR_MODES, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, } ) @@ -129,7 +123,6 @@ DEFAULT_NAME = "MQTT LightEntity" DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_ON = "ON" -DEFAULT_WHITE_VALUE_SCALE = 255 DEFAULT_WHITE_SCALE = 255 DEFAULT_ON_COMMAND_TYPE = "last" @@ -153,7 +146,6 @@ VALUE_TEMPLATE_KEYS = [ CONF_RGBW_VALUE_TEMPLATE, CONF_RGBWW_VALUE_TEMPLATE, CONF_STATE_VALUE_TEMPLATE, - CONF_WHITE_VALUE_TEMPLATE, CONF_XY_VALUE_TEMPLATE, ] @@ -207,12 +199,6 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_WHITE_SCALE, default=DEFAULT_WHITE_SCALE): vol.All( vol.Coerce(int), vol.Range(min=1) ), - vol.Optional(CONF_WHITE_VALUE_COMMAND_TOPIC): valid_publish_topic, - vol.Optional( - CONF_WHITE_VALUE_SCALE, default=DEFAULT_WHITE_VALUE_SCALE - ): vol.All(vol.Coerce(int), vol.Range(min=1)), - vol.Optional(CONF_WHITE_VALUE_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_XY_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_XY_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_XY_VALUE_TEMPLATE): cv.template, @@ -224,22 +210,17 @@ _PLATFORM_SCHEMA_BASE = ( # The use of PLATFORM_SCHEMA is deprecated in HA Core 2022.6 PLATFORM_SCHEMA_BASIC = vol.All( - # CONF_WHITE_VALUE_* is deprecated, support will be removed in release 2022.9 - cv.deprecated(CONF_WHITE_VALUE_COMMAND_TOPIC), - cv.deprecated(CONF_WHITE_VALUE_SCALE), - cv.deprecated(CONF_WHITE_VALUE_STATE_TOPIC), - cv.deprecated(CONF_WHITE_VALUE_TEMPLATE), cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), ) DISCOVERY_SCHEMA_BASIC = vol.All( # CONF_VALUE_TEMPLATE is no longer supported, support was removed in 2022.2 cv.removed(CONF_VALUE_TEMPLATE), - # CONF_WHITE_VALUE_* is deprecated, support will be removed in release 2022.9 - cv.deprecated(CONF_WHITE_VALUE_COMMAND_TOPIC), - cv.deprecated(CONF_WHITE_VALUE_SCALE), - cv.deprecated(CONF_WHITE_VALUE_STATE_TOPIC), - cv.deprecated(CONF_WHITE_VALUE_TEMPLATE), + # CONF_WHITE_VALUE_* is no longer supported, support was removed in 2022.9 + cv.removed(CONF_WHITE_VALUE_COMMAND_TOPIC), + cv.removed(CONF_WHITE_VALUE_SCALE), + cv.removed(CONF_WHITE_VALUE_STATE_TOPIC), + cv.removed(CONF_WHITE_VALUE_TEMPLATE), _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), ) @@ -266,13 +247,11 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._color_temp = None self._effect = None self._hs_color = None - self._legacy_mode = False self._rgb_color = None self._rgbw_color = None self._rgbww_color = None self._state = None self._supported_color_modes = None - self._white_value = None self._xy_color = None self._topic = None @@ -288,7 +267,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._optimistic_rgb_color = False self._optimistic_rgbw_color = False self._optimistic_rgbww_color = False - self._optimistic_white_value = False self._optimistic_xy_color = False MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @@ -324,8 +302,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): CONF_RGBWW_STATE_TOPIC, CONF_STATE_TOPIC, CONF_WHITE_COMMAND_TOPIC, - CONF_WHITE_VALUE_COMMAND_TOPIC, - CONF_WHITE_VALUE_STATE_TOPIC, CONF_XY_COMMAND_TOPIC, CONF_XY_STATE_TOPIC, ) @@ -384,9 +360,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ) self._optimistic_effect = optimistic or topic[CONF_EFFECT_STATE_TOPIC] is None self._optimistic_hs_color = optimistic or topic[CONF_HS_STATE_TOPIC] is None - self._optimistic_white_value = ( - optimistic or topic[CONF_WHITE_VALUE_STATE_TOPIC] is None - ) self._optimistic_xy_color = optimistic or topic[CONF_XY_STATE_TOPIC] is None supported_color_modes = set() if topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None: @@ -423,9 +396,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): # Validate the color_modes configuration self._supported_color_modes = valid_supported_color_modes(supported_color_modes) - if topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None: - self._legacy_mode = True - def _is_optimistic(self, attribute): """Return True if the attribute is optimistically updated.""" return getattr(self, f"_optimistic_{attribute}") @@ -513,10 +483,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ) if not rgb: return - if self._legacy_mode: - self._hs_color = color_util.color_RGB_to_hs(*rgb) - else: - self._rgb_color = rgb + self._rgb_color = rgb self.async_write_ha_state() add_topic(CONF_RGB_STATE_TOPIC, rgb_received) @@ -624,24 +591,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): add_topic(CONF_HS_STATE_TOPIC, hs_received) - @callback - @log_messages(self.hass, self.entity_id) - def white_value_received(msg): - """Handle new MQTT messages for white value.""" - payload = self._value_templates[CONF_WHITE_VALUE_TEMPLATE]( - msg.payload, None - ) - if not payload: - _LOGGER.debug("Ignoring empty white value message from '%s'", msg.topic) - return - - device_value = float(payload) - percent_white = device_value / self._config[CONF_WHITE_VALUE_SCALE] - self._white_value = percent_white * 255 - self.async_write_ha_state() - - add_topic(CONF_WHITE_VALUE_STATE_TOPIC, white_value_received) - @callback @log_messages(self.hass, self.entity_id) def xy_received(msg): @@ -654,10 +603,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): xy_color = tuple(float(val) for val in payload.split(",")) if self._optimistic_color_mode: self._color_mode = ColorMode.XY - if self._legacy_mode: - self._hs_color = color_util.color_xy_to_hs(*xy_color) - else: - self._xy_color = xy_color + self._xy_color = xy_color self.async_write_ha_state() add_topic(CONF_XY_STATE_TOPIC, xy_received) @@ -690,7 +636,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): restore_state(ATTR_COLOR_TEMP) restore_state(ATTR_EFFECT) restore_state(ATTR_HS_COLOR) - restore_state(ATTR_WHITE_VALUE) restore_state(ATTR_XY_COLOR) restore_state(ATTR_HS_COLOR, ATTR_XY_COLOR) @@ -704,19 +649,11 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): @property def color_mode(self): """Return current color mode.""" - if self._legacy_mode: - return None return self._color_mode @property def hs_color(self): """Return the hs color value.""" - if not self._legacy_mode: - return self._hs_color - - # Legacy mode, gate color_temp with white_value == 0 - if self._white_value: - return None return self._hs_color @property @@ -742,18 +679,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): @property def color_temp(self): """Return the color temperature in mired.""" - if not self._legacy_mode: - return self._color_temp - - # Legacy mode, gate color_temp with white_value > 0 - supports_color = ( - self._topic[CONF_RGB_COMMAND_TOPIC] - or self._topic[CONF_HS_COMMAND_TOPIC] - or self._topic[CONF_XY_COMMAND_TOPIC] - ) - if self._white_value or not supports_color: - return self._color_temp - return None + return self._color_temp @property def min_mireds(self): @@ -765,13 +691,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): """Return the warmest color_temp that this light supports.""" return self._config.get(CONF_MAX_MIREDS, super().max_mireds) - @property - def white_value(self): - """Return the white property.""" - if white_value := self._white_value: - return min(round(white_value), 255) - return None - @property def is_on(self): """Return true if device is on.""" @@ -795,8 +714,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): @property def supported_color_modes(self): """Flag supported color modes.""" - if self._legacy_mode: - return None return self._supported_color_modes @property @@ -807,32 +724,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._topic[CONF_EFFECT_COMMAND_TOPIC] is not None and LightEntityFeature.EFFECT ) - if not self._legacy_mode: - return supported_features - - # Legacy mode - supported_features |= self._topic[CONF_RGB_COMMAND_TOPIC] is not None and ( - SUPPORT_COLOR | SUPPORT_BRIGHTNESS - ) - supported_features |= ( - self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None - and SUPPORT_BRIGHTNESS - ) - supported_features |= ( - self._topic[CONF_COLOR_TEMP_COMMAND_TOPIC] is not None - and SUPPORT_COLOR_TEMP - ) - supported_features |= ( - self._topic[CONF_HS_COMMAND_TOPIC] is not None and SUPPORT_COLOR - ) - supported_features |= ( - self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None - and SUPPORT_WHITE_VALUE - ) - supported_features |= ( - self._topic[CONF_XY_COMMAND_TOPIC] is not None and SUPPORT_COLOR - ) - return supported_features async def async_turn_on(self, **kwargs): # noqa: C901 @@ -905,70 +796,38 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): kwargs[ATTR_BRIGHTNESS] = self._brightness if self._brightness else 255 hs_color = kwargs.get(ATTR_HS_COLOR) - if ( - hs_color - and self._topic[CONF_RGB_COMMAND_TOPIC] is not None - and self._legacy_mode - ): - # Legacy mode: Convert HS to RGB - rgb = scale_rgbx(color_util.color_hsv_to_RGB(*hs_color, 100)) - rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE, ColorMode.RGB) - await publish(CONF_RGB_COMMAND_TOPIC, rgb_s) - should_update |= set_optimistic( - ATTR_HS_COLOR, hs_color, condition_attribute=ATTR_RGB_COLOR - ) if hs_color and self._topic[CONF_HS_COMMAND_TOPIC] is not None: await publish(CONF_HS_COMMAND_TOPIC, f"{hs_color[0]},{hs_color[1]}") should_update |= set_optimistic(ATTR_HS_COLOR, hs_color, ColorMode.HS) - if ( - hs_color - and self._topic[CONF_XY_COMMAND_TOPIC] is not None - and self._legacy_mode - ): - # Legacy mode: Convert HS to XY - xy_color = color_util.color_hs_to_xy(*hs_color) - await publish(CONF_XY_COMMAND_TOPIC, f"{xy_color[0]},{xy_color[1]}") - should_update |= set_optimistic( - ATTR_HS_COLOR, hs_color, condition_attribute=ATTR_XY_COLOR - ) - - if ( - (rgb := kwargs.get(ATTR_RGB_COLOR)) - and self._topic[CONF_RGB_COMMAND_TOPIC] is not None - and not self._legacy_mode - ): + if (rgb := kwargs.get(ATTR_RGB_COLOR)) and self._topic[ + CONF_RGB_COMMAND_TOPIC + ] is not None: scaled = scale_rgbx(rgb) rgb_s = render_rgbx(scaled, CONF_RGB_COMMAND_TEMPLATE, ColorMode.RGB) await publish(CONF_RGB_COMMAND_TOPIC, rgb_s) should_update |= set_optimistic(ATTR_RGB_COLOR, rgb, ColorMode.RGB) - if ( - (rgbw := kwargs.get(ATTR_RGBW_COLOR)) - and self._topic[CONF_RGBW_COMMAND_TOPIC] is not None - and not self._legacy_mode - ): + if (rgbw := kwargs.get(ATTR_RGBW_COLOR)) and self._topic[ + CONF_RGBW_COMMAND_TOPIC + ] is not None: scaled = scale_rgbx(rgbw) rgbw_s = render_rgbx(scaled, CONF_RGBW_COMMAND_TEMPLATE, ColorMode.RGBW) await publish(CONF_RGBW_COMMAND_TOPIC, rgbw_s) should_update |= set_optimistic(ATTR_RGBW_COLOR, rgbw, ColorMode.RGBW) - if ( - (rgbww := kwargs.get(ATTR_RGBWW_COLOR)) - and self._topic[CONF_RGBWW_COMMAND_TOPIC] is not None - and not self._legacy_mode - ): + if (rgbww := kwargs.get(ATTR_RGBWW_COLOR)) and self._topic[ + CONF_RGBWW_COMMAND_TOPIC + ] is not None: scaled = scale_rgbx(rgbww) rgbww_s = render_rgbx(scaled, CONF_RGBWW_COMMAND_TEMPLATE, ColorMode.RGBWW) await publish(CONF_RGBWW_COMMAND_TOPIC, rgbww_s) should_update |= set_optimistic(ATTR_RGBWW_COLOR, rgbww, ColorMode.RGBWW) - if ( - (xy_color := kwargs.get(ATTR_XY_COLOR)) - and self._topic[CONF_XY_COMMAND_TOPIC] is not None - and not self._legacy_mode - ): + if (xy_color := kwargs.get(ATTR_XY_COLOR)) and self._topic[ + CONF_XY_COMMAND_TOPIC + ] is not None: await publish(CONF_XY_COMMAND_TOPIC, f"{xy_color[0]},{xy_color[1]}") should_update |= set_optimistic(ATTR_XY_COLOR, xy_color, ColorMode.XY) @@ -987,24 +846,10 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): device_brightness = tpl(variables={"value": device_brightness}) await publish(CONF_BRIGHTNESS_COMMAND_TOPIC, device_brightness) should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) - elif ( - ATTR_BRIGHTNESS in kwargs - and ATTR_HS_COLOR not in kwargs - and self._topic[CONF_RGB_COMMAND_TOPIC] is not None - and self._legacy_mode - ): - # Legacy mode - hs_color = self._hs_color if self._hs_color is not None else (0, 0) - brightness = kwargs[ATTR_BRIGHTNESS] - rgb = scale_rgbx(color_util.color_hsv_to_RGB(*hs_color, 100), brightness) - rgb_s = render_rgbx(rgb, CONF_RGB_COMMAND_TEMPLATE, ColorMode.RGB) - await publish(CONF_RGB_COMMAND_TOPIC, rgb_s) - should_update |= set_optimistic(ATTR_BRIGHTNESS, kwargs[ATTR_BRIGHTNESS]) elif ( ATTR_BRIGHTNESS in kwargs and ATTR_RGB_COLOR not in kwargs and self._topic[CONF_RGB_COMMAND_TOPIC] is not None - and not self._legacy_mode ): rgb_color = self._rgb_color if self._rgb_color is not None else (255,) * 3 rgb = scale_rgbx(rgb_color, kwargs[ATTR_BRIGHTNESS]) @@ -1015,7 +860,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ATTR_BRIGHTNESS in kwargs and ATTR_RGBW_COLOR not in kwargs and self._topic[CONF_RGBW_COMMAND_TOPIC] is not None - and not self._legacy_mode ): rgbw_color = ( self._rgbw_color if self._rgbw_color is not None else (255,) * 4 @@ -1028,7 +872,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ATTR_BRIGHTNESS in kwargs and ATTR_RGBWW_COLOR not in kwargs and self._topic[CONF_RGBWW_COMMAND_TOPIC] is not None - and not self._legacy_mode ): rgbww_color = ( self._rgbww_color if self._rgbww_color is not None else (255,) * 5 @@ -1069,16 +912,6 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): ColorMode.WHITE, ) - if ( - ATTR_WHITE_VALUE in kwargs - and self._topic[CONF_WHITE_VALUE_COMMAND_TOPIC] is not None - ): - percent_white = float(kwargs[ATTR_WHITE_VALUE]) / 255 - white_scale = self._config[CONF_WHITE_VALUE_SCALE] - device_white_value = min(round(percent_white * white_scale), white_scale) - await publish(CONF_WHITE_VALUE_COMMAND_TOPIC, device_white_value) - should_update |= set_optimistic(ATTR_WHITE_VALUE, kwargs[ATTR_WHITE_VALUE]) - if on_command_type == "last": await publish(CONF_COMMAND_TOPIC, self._payload["on"]) should_update = True diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 716366cbe22..659dd212b51 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -15,7 +15,6 @@ from homeassistant.components.light import ( ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, ENTITY_ID_FORMAT, FLASH_LONG, @@ -23,7 +22,6 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - SUPPORT_WHITE_VALUE, VALID_COLOR_MODES, ColorMode, LightEntity, @@ -78,7 +76,6 @@ DEFAULT_FLASH_TIME_SHORT = 2 DEFAULT_NAME = "MQTT JSON Light" DEFAULT_OPTIMISTIC = False DEFAULT_RGB = False -DEFAULT_WHITE_VALUE = False DEFAULT_XY = False DEFAULT_HS = False DEFAULT_BRIGHTNESS_SCALE = 255 @@ -97,7 +94,7 @@ CONF_MIN_MIREDS = "min_mireds" def valid_color_configuration(config): """Test color_mode is not combined with deprecated config.""" - deprecated = {CONF_COLOR_TEMP, CONF_HS, CONF_RGB, CONF_WHITE_VALUE, CONF_XY} + deprecated = {CONF_COLOR_TEMP, CONF_HS, CONF_RGB, CONF_XY} if config[CONF_COLOR_MODE] and any(config.get(key) for key in deprecated): raise vol.Invalid(f"color_mode must not be combined with any of {deprecated}") return config @@ -139,7 +136,6 @@ _PLATFORM_SCHEMA_BASE = ( vol.Unique(), valid_supported_color_modes, ), - vol.Optional(CONF_WHITE_VALUE, default=DEFAULT_WHITE_VALUE): cv.boolean, vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean, }, ) @@ -149,15 +145,13 @@ _PLATFORM_SCHEMA_BASE = ( # Configuring MQTT Lights under the light platform key is deprecated in HA Core 2022.6 PLATFORM_SCHEMA_JSON = vol.All( - # CONF_WHITE_VALUE is deprecated, support will be removed in release 2022.9 - cv.deprecated(CONF_WHITE_VALUE), cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), valid_color_configuration, ) DISCOVERY_SCHEMA_JSON = vol.All( - # CONF_WHITE_VALUE is deprecated, support will be removed in release 2022.9 - cv.deprecated(CONF_WHITE_VALUE), + # CONF_WHITE_VALUE is no longer supported, support was removed in 2022.9 + cv.removed(CONF_WHITE_VALUE), _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), valid_color_configuration, ) @@ -197,7 +191,6 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._rgb = None self._rgbw = None self._rgbww = None - self._white_value = None self._xy = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @@ -231,7 +224,6 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._supported_features |= config[CONF_RGB] and ( SUPPORT_COLOR | SUPPORT_BRIGHTNESS ) - self._supported_features |= config[CONF_WHITE_VALUE] and SUPPORT_WHITE_VALUE self._supported_features |= config[CONF_XY] and SUPPORT_COLOR def _update_color(self, values): @@ -366,14 +358,6 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): with suppress(KeyError): self._effect = values["effect"] - if self._supported_features and SUPPORT_WHITE_VALUE: - try: - self._white_value = int(values["white_value"]) - except KeyError: - pass - except ValueError: - _LOGGER.warning("Invalid white value received") - self.async_write_ha_state() if self._topic[CONF_STATE_TOPIC] is not None: @@ -406,7 +390,6 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._rgb = last_attributes.get(ATTR_RGB_COLOR, self._rgb) self._rgbw = last_attributes.get(ATTR_RGBW_COLOR, self._rgbw) self._rgbww = last_attributes.get(ATTR_RGBWW_COLOR, self._rgbww) - self._white_value = last_attributes.get(ATTR_WHITE_VALUE, self._white_value) self._xy = last_attributes.get(ATTR_XY_COLOR, self._xy) @property @@ -464,11 +447,6 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): """Return the hs color value.""" return self._xy - @property - def white_value(self): - """Return the white property.""" - return self._white_value - @property def is_on(self): """Return true if device is on.""" @@ -520,7 +498,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): def _supports_color_mode(self, color_mode): return self.supported_color_modes and color_mode in self.supported_color_modes - async def async_turn_on(self, **kwargs): # noqa: C901 + async def async_turn_on(self, **kwargs): """Turn the device on. This method is a coroutine. @@ -635,13 +613,6 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._effect = kwargs[ATTR_EFFECT] should_update = True - if ATTR_WHITE_VALUE in kwargs: - message["white_value"] = int(kwargs[ATTR_WHITE_VALUE]) - - if self._optimistic: - self._white_value = kwargs[ATTR_WHITE_VALUE] - should_update = True - await self.async_publish( self._topic[CONF_COMMAND_TOPIC], json_dumps(message), diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 779f2f17e24..6f211e598b4 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -10,12 +10,10 @@ from homeassistant.components.light import ( ATTR_FLASH, ATTR_HS_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, ENTITY_ID_FORMAT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - SUPPORT_WHITE_VALUE, LightEntity, LightEntityFeature, ) @@ -84,7 +82,6 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_RED_TEMPLATE): cv.template, vol.Optional(CONF_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template, } ) .extend(MQTT_ENTITY_COMMON_SCHEMA.schema) @@ -93,14 +90,12 @@ _PLATFORM_SCHEMA_BASE = ( # Configuring MQTT Lights under the light platform key is deprecated in HA Core 2022.6 PLATFORM_SCHEMA_TEMPLATE = vol.All( - # CONF_WHITE_VALUE_TEMPLATE is deprecated, support will be removed in release 2022.9 - cv.deprecated(CONF_WHITE_VALUE_TEMPLATE), cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), ) DISCOVERY_SCHEMA_TEMPLATE = vol.All( - # CONF_WHITE_VALUE_TEMPLATE is deprecated, support will be removed in release 2022.9 - cv.deprecated(CONF_WHITE_VALUE_TEMPLATE), + # CONF_WHITE_VALUE_TEMPLATE is no longer supported, support was removed in 2022.9 + cv.removed(CONF_WHITE_VALUE_TEMPLATE), _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), ) @@ -131,7 +126,6 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): # features self._brightness = None self._color_temp = None - self._white_value = None self._hs = None self._effect = None @@ -159,7 +153,6 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): CONF_GREEN_TEMPLATE, CONF_RED_TEMPLATE, CONF_STATE_TEMPLATE, - CONF_WHITE_VALUE_TEMPLATE, ) } optimistic = config[CONF_OPTIMISTIC] @@ -236,16 +229,6 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): except ValueError: _LOGGER.warning("Invalid color value received") - if self._templates[CONF_WHITE_VALUE_TEMPLATE] is not None: - try: - self._white_value = int( - self._templates[ - CONF_WHITE_VALUE_TEMPLATE - ].async_render_with_possible_json_value(msg.payload) - ) - except ValueError: - _LOGGER.warning("Invalid white value received") - if self._templates[CONF_EFFECT_TEMPLATE] is not None: effect = self._templates[ CONF_EFFECT_TEMPLATE @@ -287,8 +270,6 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): self._color_temp = last_state.attributes.get(ATTR_COLOR_TEMP) if last_state.attributes.get(ATTR_EFFECT): self._effect = last_state.attributes.get(ATTR_EFFECT) - if last_state.attributes.get(ATTR_WHITE_VALUE): - self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE) @property def brightness(self): @@ -315,11 +296,6 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): """Return the hs color value [int, int].""" return self._hs - @property - def white_value(self): - """Return the white property.""" - return self._white_value - @property def is_on(self): """Return True if entity is on.""" @@ -385,12 +361,6 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): if self._optimistic: self._hs = kwargs[ATTR_HS_COLOR] - if ATTR_WHITE_VALUE in kwargs: - values["white_value"] = int(kwargs[ATTR_WHITE_VALUE]) - - if self._optimistic: - self._white_value = kwargs[ATTR_WHITE_VALUE] - if ATTR_EFFECT in kwargs: values["effect"] = kwargs.get(ATTR_EFFECT) @@ -457,7 +427,5 @@ class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): features = features | LightEntityFeature.EFFECT if self._templates[CONF_COLOR_TEMP_TEMPLATE] is not None: features = features | SUPPORT_COLOR_TEMP - if self._templates[CONF_WHITE_VALUE_TEMPLATE] is not None: - features = features | SUPPORT_WHITE_VALUE return features diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index bfafc99a9e2..ff529b3dda4 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -106,23 +106,6 @@ light: payload_on: "on" payload_off: "off" -config for RGB Version with white value and scale: - -light: - platform: mqtt - name: "Office Light RGB" - state_topic: "office/rgb1/light/status" - command_topic: "office/rgb1/light/switch" - white_value_state_topic: "office/rgb1/white_value/status" - white_value_command_topic: "office/rgb1/white_value/set" - white_value_scale: 99 - rgb_state_topic: "office/rgb1/rgb/status" - rgb_command_topic: "office/rgb1/rgb/set" - rgb_scale: 99 - qos: 0 - payload_on: "on" - payload_off: "off" - config for RGB Version with RGB command template: light: @@ -199,13 +182,11 @@ from homeassistant.components.mqtt.light.schema_basic import ( CONF_RGB_COMMAND_TOPIC, CONF_RGBW_COMMAND_TOPIC, CONF_RGBWW_COMMAND_TOPIC, - CONF_WHITE_VALUE_COMMAND_TOPIC, CONF_XY_COMMAND_TOPIC, MQTT_LIGHT_ATTRIBUTES_BLOCKED, ) from homeassistant.const import ( ATTR_ASSUMED_STATE, - ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, STATE_UNKNOWN, @@ -270,33 +251,6 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_conf assert hass.states.get("light.test") is None -async def test_legacy_rgb_white_light(hass, mqtt_mock_entry_with_yaml_config): - """Test legacy RGB + white light flags brightness support.""" - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "test_light_rgb/set", - "rgb_command_topic": "test_light_rgb/rgb/set", - "white_value_command_topic": "test_light_rgb/white/set", - } - }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get("light.test") - expected_features = ( - light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS | light.SUPPORT_WHITE_VALUE - ) - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features - assert state.attributes.get(light.ATTR_COLOR_MODE) is None - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == ["hs", "rgbw"] - - async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics( hass, mqtt_mock_entry_with_yaml_config ): @@ -325,7 +279,6 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics( assert state.attributes.get("rgb_color") is None assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get(light.ATTR_COLOR_MODE) is None assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == ["onoff"] @@ -341,7 +294,6 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics( assert state.attributes.get("rgb_color") is None assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get(light.ATTR_COLOR_MODE) == "onoff" assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == ["onoff"] @@ -357,138 +309,6 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics( assert state.state == STATE_UNKNOWN -async def test_legacy_controlling_state_via_topic( - hass, mqtt_mock_entry_with_yaml_config -): - """Test the controlling of the state via topic for legacy light (white_value).""" - config = { - light.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test_light_rgb/status", - "command_topic": "test_light_rgb/set", - "brightness_state_topic": "test_light_rgb/brightness/status", - "brightness_command_topic": "test_light_rgb/brightness/set", - "rgb_state_topic": "test_light_rgb/rgb/status", - "rgb_command_topic": "test_light_rgb/rgb/set", - "color_temp_state_topic": "test_light_rgb/color_temp/status", - "color_temp_command_topic": "test_light_rgb/color_temp/set", - "effect_state_topic": "test_light_rgb/effect/status", - "effect_command_topic": "test_light_rgb/effect/set", - "hs_state_topic": "test_light_rgb/hs/status", - "hs_command_topic": "test_light_rgb/hs/set", - "white_value_state_topic": "test_light_rgb/white_value/status", - "white_value_command_topic": "test_light_rgb/white_value/set", - "xy_state_topic": "test_light_rgb/xy/status", - "xy_command_topic": "test_light_rgb/xy/set", - "qos": "0", - "payload_on": 1, - "payload_off": 0, - } - } - color_modes = ["color_temp", "hs", "rgbw"] - - assert await async_setup_component(hass, light.DOMAIN, config) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get("light.test") - assert state.state == STATE_UNKNOWN - assert state.attributes.get("rgb_color") is None - assert state.attributes.get("brightness") is None - assert state.attributes.get("color_temp") is None - assert state.attributes.get("effect") is None - assert state.attributes.get("hs_color") is None - assert state.attributes.get("rgb_color") is None - assert state.attributes.get("rgbw_color") is None - assert state.attributes.get("rgbww_color") is None - assert state.attributes.get("white_value") is None - assert state.attributes.get("xy_color") is None - assert state.attributes.get(light.ATTR_COLOR_MODE) is None - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - async_fire_mqtt_message(hass, "test_light_rgb/status", "1") - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("rgb_color") is None - assert state.attributes.get("brightness") is None - assert state.attributes.get("color_temp") is None - assert state.attributes.get("effect") is None - assert state.attributes.get("hs_color") is None - assert state.attributes.get("rgb_color") is None - assert state.attributes.get("rgbw_color") is None - assert state.attributes.get("rgbww_color") is None - assert state.attributes.get("white_value") is None - assert state.attributes.get("xy_color") is None - assert state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - async_fire_mqtt_message(hass, "test_light_rgb/status", "0") - - state = hass.states.get("light.test") - assert state.state == STATE_OFF - - async_fire_mqtt_message(hass, "test_light_rgb/status", "1") - - async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "100") - - light_state = hass.states.get("light.test") - assert light_state.attributes["brightness"] == 100 - assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" - assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "300") - light_state = hass.states.get("light.test") - assert light_state.attributes.get("color_temp") is None - assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" - assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", "100") - - light_state = hass.states.get("light.test") - assert light_state.attributes["white_value"] == 100 - assert light_state.attributes["color_temp"] == 300 - assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" - assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "rainbow") - light_state = hass.states.get("light.test") - assert light_state.attributes["effect"] == "rainbow" - assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" - assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - async_fire_mqtt_message(hass, "test_light_rgb/status", "1") - - async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "125,125,125") - - light_state = hass.states.get("light.test") - assert light_state.attributes.get("rgb_color") == (255, 187, 131) - assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" - assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", "0") - light_state = hass.states.get("light.test") - assert light_state.attributes.get("rgb_color") == (255, 255, 255) - assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" - assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "200,50") - - light_state = hass.states.get("light.test") - assert light_state.attributes.get("hs_color") == (200, 50) - assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" - assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - async_fire_mqtt_message(hass, "test_light_rgb/xy/status", "0.675,0.322") - - light_state = hass.states.get("light.test") - assert light_state.attributes.get("xy_color") == (0.672, 0.324) - assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" - assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): """Test the controlling of the state via topic.""" config = { @@ -534,7 +354,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi assert state.attributes.get("rgb_color") is None assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get(light.ATTR_COLOR_MODE) is None assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes @@ -551,7 +370,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi assert state.attributes.get("rgb_color") is None assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes @@ -611,125 +429,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_legacy_invalid_state_via_topic( - hass, mqtt_mock_entry_with_yaml_config, caplog -): - """Test handling of empty data via topic.""" - config = { - light.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test_light_rgb/status", - "command_topic": "test_light_rgb/set", - "brightness_state_topic": "test_light_rgb/brightness/status", - "brightness_command_topic": "test_light_rgb/brightness/set", - "rgb_state_topic": "test_light_rgb/rgb/status", - "rgb_command_topic": "test_light_rgb/rgb/set", - "color_temp_state_topic": "test_light_rgb/color_temp/status", - "color_temp_command_topic": "test_light_rgb/color_temp/set", - "effect_state_topic": "test_light_rgb/effect/status", - "effect_command_topic": "test_light_rgb/effect/set", - "hs_state_topic": "test_light_rgb/hs/status", - "hs_command_topic": "test_light_rgb/hs/set", - "white_value_state_topic": "test_light_rgb/white_value/status", - "white_value_command_topic": "test_light_rgb/white_value/set", - "xy_state_topic": "test_light_rgb/xy/status", - "xy_command_topic": "test_light_rgb/xy/set", - "qos": "0", - "payload_on": 1, - "payload_off": 0, - } - } - - assert await async_setup_component(hass, light.DOMAIN, config) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get("light.test") - assert state.state == STATE_UNKNOWN - assert state.attributes.get("rgb_color") is None - assert state.attributes.get("brightness") is None - assert state.attributes.get("color_temp") is None - assert state.attributes.get("effect") is None - assert state.attributes.get("hs_color") is None - assert state.attributes.get("white_value") is None - assert state.attributes.get("xy_color") is None - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - async_fire_mqtt_message(hass, "test_light_rgb/status", "1") - async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "255,255,255") - async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "255") - async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "none") - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("rgb_color") == (255, 255, 255) - assert state.attributes.get("brightness") == 255 - assert state.attributes.get("color_temp") is None - assert state.attributes.get("effect") == "none" - assert state.attributes.get("hs_color") == (0, 0) - assert state.attributes.get("white_value") is None - assert state.attributes.get("xy_color") == (0.323, 0.329) - - async_fire_mqtt_message(hass, "test_light_rgb/status", "") - assert "Ignoring empty state message" in caplog.text - light_state = hass.states.get("light.test") - assert state.state == STATE_ON - - async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "") - assert "Ignoring empty brightness message" in caplog.text - light_state = hass.states.get("light.test") - assert light_state.attributes["brightness"] == 255 - - async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "") - assert "Ignoring empty effect message" in caplog.text - light_state = hass.states.get("light.test") - assert light_state.attributes["effect"] == "none" - - async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "") - assert "Ignoring empty rgb message" in caplog.text - light_state = hass.states.get("light.test") - assert light_state.attributes.get("rgb_color") == (255, 255, 255) - - async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "") - assert "Ignoring empty hs message" in caplog.text - light_state = hass.states.get("light.test") - assert light_state.attributes.get("hs_color") == (0, 0) - - async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "bad,bad") - assert "Failed to parse hs state update" in caplog.text - light_state = hass.states.get("light.test") - assert light_state.attributes.get("hs_color") == (0, 0) - - async_fire_mqtt_message(hass, "test_light_rgb/xy/status", "") - assert "Ignoring empty xy-color message" in caplog.text - light_state = hass.states.get("light.test") - assert light_state.attributes.get("xy_color") == (0.323, 0.329) - - async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "153") - async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", "255") - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("rgb_color") == (255, 254, 250) - assert state.attributes.get("brightness") == 255 - assert state.attributes.get("color_temp") == 153 - assert state.attributes.get("effect") == "none" - assert state.attributes.get("hs_color") == (54.768, 1.6) - assert state.attributes.get("white_value") == 255 - assert state.attributes.get("xy_color") == (0.326, 0.333) - - async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "") - assert "Ignoring empty color temp message" in caplog.text - light_state = hass.states.get("light.test") - assert light_state.attributes["color_temp"] == 153 - - async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", "") - assert "Ignoring empty white value message" in caplog.text - light_state = hass.states.get("light.test") - assert light_state.attributes["white_value"] == 255 - - async def test_invalid_state_via_topic(hass, mqtt_mock_entry_with_yaml_config, caplog): """Test handling of empty data via topic.""" config = { @@ -955,148 +654,6 @@ async def test_brightness_from_rgb_controlling_scale( assert state.attributes.get("brightness") == 127 -async def test_legacy_white_value_controlling_scale( - hass, mqtt_mock_entry_with_yaml_config -): - """Test the white_value controlling scale.""" - with assert_setup_component(1, light.DOMAIN): - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test_scale/status", - "command_topic": "test_scale/set", - "white_value_state_topic": "test_scale/white_value/status", - "white_value_command_topic": "test_scale/white_value/set", - "white_value_scale": "99", - "qos": 0, - "payload_on": "on", - "payload_off": "off", - } - }, - ) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get("light.test") - assert state.state == STATE_UNKNOWN - assert state.attributes.get("white_value") is None - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - async_fire_mqtt_message(hass, "test_scale/status", "on") - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("white_value") is None - - async_fire_mqtt_message(hass, "test_scale/status", "off") - - state = hass.states.get("light.test") - assert state.state == STATE_OFF - - async_fire_mqtt_message(hass, "test_scale/status", "on") - - async_fire_mqtt_message(hass, "test_scale/white_value/status", "99") - - light_state = hass.states.get("light.test") - assert light_state.attributes["white_value"] == 255 - - -async def test_legacy_controlling_state_via_topic_with_templates( - hass, mqtt_mock_entry_with_yaml_config -): - """Test the setting of the state with a template.""" - config = { - light.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test_light_rgb/status", - "command_topic": "test_light_rgb/set", - "brightness_command_topic": "test_light_rgb/brightness/set", - "rgb_command_topic": "test_light_rgb/rgb/set", - "color_temp_command_topic": "test_light_rgb/color_temp/set", - "effect_command_topic": "test_light_rgb/effect/set", - "hs_command_topic": "test_light_rgb/hs/set", - "white_value_command_topic": "test_light_rgb/white_value/set", - "xy_command_topic": "test_light_rgb/xy/set", - "brightness_state_topic": "test_light_rgb/brightness/status", - "color_temp_state_topic": "test_light_rgb/color_temp/status", - "effect_state_topic": "test_light_rgb/effect/status", - "hs_state_topic": "test_light_rgb/hs/status", - "rgb_state_topic": "test_light_rgb/rgb/status", - "white_value_state_topic": "test_light_rgb/white_value/status", - "xy_state_topic": "test_light_rgb/xy/status", - "state_value_template": "{{ value_json.hello }}", - "brightness_value_template": "{{ value_json.hello }}", - "color_temp_value_template": "{{ value_json.hello }}", - "effect_value_template": "{{ value_json.hello }}", - "hs_value_template": '{{ value_json.hello | join(",") }}', - "rgb_value_template": '{{ value_json.hello | join(",") }}', - "white_value_template": "{{ value_json.hello }}", - "xy_value_template": '{{ value_json.hello | join(",") }}', - } - } - - assert await async_setup_component(hass, light.DOMAIN, config) - await hass.async_block_till_done() - await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get("light.test") - assert state.state == STATE_UNKNOWN - assert state.attributes.get("brightness") is None - assert state.attributes.get("rgb_color") is None - - async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", '{"hello": [1, 2, 3]}') - async_fire_mqtt_message(hass, "test_light_rgb/status", '{"hello": "ON"}') - async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", '{"hello": "50"}') - async_fire_mqtt_message( - hass, "test_light_rgb/color_temp/status", '{"hello": "300"}' - ) - async_fire_mqtt_message( - hass, "test_light_rgb/effect/status", '{"hello": "rainbow"}' - ) - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("brightness") == 50 - assert state.attributes.get("rgb_color") == (84, 169, 255) - assert state.attributes.get("color_temp") is None - assert state.attributes.get("effect") == "rainbow" - assert state.attributes.get("white_value") is None - - async_fire_mqtt_message( - hass, "test_light_rgb/white_value/status", '{"hello": "75"}' - ) - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("brightness") == 50 - assert state.attributes.get("rgb_color") == (255, 187, 131) - assert state.attributes.get("color_temp") == 300 - assert state.attributes.get("effect") == "rainbow" - assert state.attributes.get("white_value") == 75 - - async_fire_mqtt_message(hass, "test_light_rgb/hs/status", '{"hello": [100,50]}') - async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", '{"hello": "0"}') - - state = hass.states.get("light.test") - assert state.attributes.get("hs_color") == (100, 50) - - async_fire_mqtt_message( - hass, "test_light_rgb/xy/status", '{"hello": [0.123,0.123]}' - ) - - state = hass.states.get("light.test") - assert state.attributes.get("xy_color") == (0.14, 0.131) - - async_fire_mqtt_message(hass, "test_light_rgb/status", '{"hello": null}') - state = hass.states.get("light.test") - assert state.state == STATE_UNKNOWN - - async def test_controlling_state_via_topic_with_templates( hass, mqtt_mock_entry_with_yaml_config ): @@ -1200,139 +757,6 @@ async def test_controlling_state_via_topic_with_templates( assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_legacy_sending_mqtt_commands_and_optimistic( - hass, mqtt_mock_entry_with_yaml_config -): - """Test the sending of command in optimistic mode.""" - config = { - light.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "test_light_rgb/set", - "brightness_command_topic": "test_light_rgb/brightness/set", - "rgb_command_topic": "test_light_rgb/rgb/set", - "color_temp_command_topic": "test_light_rgb/color_temp/set", - "effect_command_topic": "test_light_rgb/effect/set", - "hs_command_topic": "test_light_rgb/hs/set", - "white_value_command_topic": "test_light_rgb/white_value/set", - "xy_command_topic": "test_light_rgb/xy/set", - "effect_list": ["colorloop", "random"], - "qos": 2, - "payload_on": "on", - "payload_off": "off", - } - } - color_modes = ["color_temp", "hs", "rgbw"] - fake_state = ha.State( - "light.test", - "on", - { - "brightness": 95, - "hs_color": [100, 100], - "effect": "random", - "color_temp": 100, - # TODO: Test restoring state with white_value - "white_value": 0, - }, - ) - with patch( - "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", - return_value=fake_state, - ), assert_setup_component(1, light.DOMAIN): - assert await async_setup_component(hass, light.DOMAIN, config) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("brightness") == 95 - assert state.attributes.get("hs_color") == (100, 100) - assert state.attributes.get("effect") == "random" - assert state.attributes.get("color_temp") is None - assert state.attributes.get("white_value") is None - assert state.attributes.get(ATTR_ASSUMED_STATE) - assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - await common.async_turn_on(hass, "light.test") - mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on", 2, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - await common.async_turn_off(hass, "light.test") - mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "off", 2, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("light.test") - assert state.state == STATE_OFF - - mqtt_mock.reset_mock() - await common.async_turn_on( - hass, "light.test", brightness=50, xy_color=[0.123, 0.123] - ) - state = hass.states.get("light.test") - assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78]) - state = hass.states.get("light.test") - assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) - state = hass.states.get("light.test") - assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - mqtt_mock.async_publish.assert_has_calls( - [ - call("test_light_rgb/set", "on", 2, False), - call("test_light_rgb/rgb/set", "255,128,0", 2, False), - call("test_light_rgb/brightness/set", "50", 2, False), - call("test_light_rgb/hs/set", "359.0,78.0", 2, False), - call("test_light_rgb/xy/set", "0.14,0.131", 2, False), - ], - any_order=True, - ) - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes["rgb_color"] == (255, 128, 0) - assert state.attributes["brightness"] == 50 - assert state.attributes["hs_color"] == (30.118, 100) - assert state.attributes.get("white_value") is None - assert state.attributes["xy_color"] == (0.611, 0.375) - assert state.attributes.get("color_temp") is None - - await common.async_turn_on(hass, "light.test", white_value=80, color_temp=125) - state = hass.states.get("light.test") - assert state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" - assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes - - mqtt_mock.async_publish.assert_has_calls( - [ - call("test_light_rgb/white_value/set", "80", 2, False), - call("test_light_rgb/color_temp/set", "125", 2, False), - ], - any_order=True, - ) - - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("rgb_color") == (221, 229, 255) - assert state.attributes["brightness"] == 50 - assert state.attributes.get("hs_color") == (224.772, 13.249) - assert state.attributes["white_value"] == 80 - assert state.attributes.get("xy_color") == (0.296, 0.301) - assert state.attributes["color_temp"] == 125 - - async def test_sending_mqtt_commands_and_optimistic( hass, mqtt_mock_entry_with_yaml_config ): @@ -1884,98 +1308,6 @@ async def test_on_command_brightness_scaled(hass, mqtt_mock_entry_with_yaml_conf ) -async def test_legacy_on_command_rgb(hass, mqtt_mock_entry_with_yaml_config): - """Test on command in RGB brightness mode.""" - config = { - light.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "test_light/set", - "rgb_command_topic": "test_light/rgb", - "white_value_command_topic": "test_light/white_value", - } - } - - assert await async_setup_component(hass, light.DOMAIN, config) - await hass.async_block_till_done() - mqtt_mock = await mqtt_mock_entry_with_yaml_config() - - state = hass.states.get("light.test") - assert state.state == STATE_UNKNOWN - - await common.async_turn_on(hass, "light.test", brightness=127) - - # Should get the following MQTT messages. - # test_light/rgb: '127,127,127' - # test_light/set: 'ON' - mqtt_mock.async_publish.assert_has_calls( - [ - call("test_light/rgb", "127,127,127", 0, False), - call("test_light/set", "ON", 0, False), - ], - any_order=True, - ) - mqtt_mock.async_publish.reset_mock() - - await common.async_turn_on(hass, "light.test", brightness=255) - - # Should get the following MQTT messages. - # test_light/rgb: '255,255,255' - # test_light/set: 'ON' - mqtt_mock.async_publish.assert_has_calls( - [ - call("test_light/rgb", "255,255,255", 0, False), - call("test_light/set", "ON", 0, False), - ], - any_order=True, - ) - mqtt_mock.async_publish.reset_mock() - - await common.async_turn_on(hass, "light.test", brightness=1) - - # Should get the following MQTT messages. - # test_light/rgb: '1,1,1' - # test_light/set: 'ON' - mqtt_mock.async_publish.assert_has_calls( - [ - call("test_light/rgb", "1,1,1", 0, False), - call("test_light/set", "ON", 0, False), - ], - any_order=True, - ) - mqtt_mock.async_publish.reset_mock() - - await common.async_turn_off(hass, "light.test") - - mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) - - # Ensure color gets scaled with brightness. - await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) - - mqtt_mock.async_publish.assert_has_calls( - [ - call("test_light/rgb", "1,0,0", 0, False), - call("test_light/set", "ON", 0, False), - ], - any_order=True, - ) - mqtt_mock.async_publish.reset_mock() - - await common.async_turn_on(hass, "light.test", brightness=255) - - # Should get the following MQTT messages. - # test_light/rgb: '255,128,0' - # test_light/set: 'ON' - mqtt_mock.async_publish.assert_has_calls( - [ - call("test_light/rgb", "255,128,0", 0, False), - call("test_light/set", "ON", 0, False), - ], - any_order=True, - ) - mqtt_mock.async_publish.reset_mock() - - async def test_on_command_rgb(hass, mqtt_mock_entry_with_yaml_config): """Test on command in RGB brightness mode.""" config = { @@ -2486,7 +1818,6 @@ async def test_explicit_color_mode(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("rgb_color") is None assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get(light.ATTR_COLOR_MODE) is None assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes @@ -2503,7 +1834,6 @@ async def test_explicit_color_mode(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("rgb_color") is None assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes @@ -2923,14 +2253,12 @@ async def test_discovery_update_light_topic_and_template( "color_temp_command_topic": "test_light_rgb/state1", "effect_command_topic": "test_light_rgb/effect/set", "hs_command_topic": "test_light_rgb/hs/set", - "white_value_command_topic": "test_light_rgb/white_value/set", "xy_command_topic": "test_light_rgb/xy/set", "brightness_state_topic": "test_light_rgb/state1", "color_temp_state_topic": "test_light_rgb/state1", "effect_state_topic": "test_light_rgb/state1", "hs_state_topic": "test_light_rgb/state1", "rgb_state_topic": "test_light_rgb/state1", - "white_value_state_topic": "test_light_rgb/state1", "xy_state_topic": "test_light_rgb/state1", "state_value_template": "{{ value_json.state1.state }}", "brightness_value_template": "{{ value_json.state1.brightness }}", @@ -2938,7 +2266,6 @@ async def test_discovery_update_light_topic_and_template( "effect_value_template": "{{ value_json.state1.fx }}", "hs_value_template": "{{ value_json.state1.hs }}", "rgb_value_template": "{{ value_json.state1.rgb }}", - "white_value_template": "{{ value_json.state1.white }}", "xy_value_template": "{{ value_json.state1.xy }}", } @@ -2951,14 +2278,12 @@ async def test_discovery_update_light_topic_and_template( "color_temp_command_topic": "test_light_rgb/state2", "effect_command_topic": "test_light_rgb/effect/set", "hs_command_topic": "test_light_rgb/hs/set", - "white_value_command_topic": "test_light_rgb/white_value/set", "xy_command_topic": "test_light_rgb/xy/set", "brightness_state_topic": "test_light_rgb/state2", "color_temp_state_topic": "test_light_rgb/state2", "effect_state_topic": "test_light_rgb/state2", "hs_state_topic": "test_light_rgb/state2", "rgb_state_topic": "test_light_rgb/state2", - "white_value_state_topic": "test_light_rgb/state2", "xy_state_topic": "test_light_rgb/state2", "state_value_template": "{{ value_json.state2.state }}", "brightness_value_template": "{{ value_json.state2.brightness }}", @@ -2966,7 +2291,6 @@ async def test_discovery_update_light_topic_and_template( "effect_value_template": "{{ value_json.state2.fx }}", "hs_value_template": "{{ value_json.state2.hs }}", "rgb_value_template": "{{ value_json.state2.rgb }}", - "white_value_template": "{{ value_json.state2.white }}", "xy_value_template": "{{ value_json.state2.xy }}", } state_data1 = [ @@ -2981,7 +2305,6 @@ async def test_discovery_update_light_topic_and_template( [ ("brightness", 100), ("color_temp", 123), - ("white_value", 100), ("effect", "cycle"), ], ), @@ -2998,7 +2321,7 @@ async def test_discovery_update_light_topic_and_template( ) ], "on", - [("hs_color", (1, 2)), ("white_value", None)], + [("hs_color", (1, 2))], ), ( [ @@ -3018,7 +2341,7 @@ async def test_discovery_update_light_topic_and_template( ) ], "on", - [("xy_color", (0.3, 0.401))], + [("xy_color", (0.3, 0.4))], ), ] state_data2 = [ @@ -3033,7 +2356,6 @@ async def test_discovery_update_light_topic_and_template( [ ("brightness", 50), ("color_temp", 200), - ("white_value", 50), ("effect", "loop"), ], ), @@ -3083,7 +2405,7 @@ async def test_discovery_update_light_topic_and_template( ) ], "on", - [("hs_color", (1.2, 2.2)), ("white_value", None)], + [("hs_color", (1.2, 2.2))], ), ( [ @@ -3186,14 +2508,12 @@ async def test_discovery_update_light_template( "color_temp_command_topic": "test_light_rgb/state1", "effect_command_topic": "test_light_rgb/effect/set", "hs_command_topic": "test_light_rgb/hs/set", - "white_value_command_topic": "test_light_rgb/white_value/set", "xy_command_topic": "test_light_rgb/xy/set", "brightness_state_topic": "test_light_rgb/state1", "color_temp_state_topic": "test_light_rgb/state1", "effect_state_topic": "test_light_rgb/state1", "hs_state_topic": "test_light_rgb/state1", "rgb_state_topic": "test_light_rgb/state1", - "white_value_state_topic": "test_light_rgb/state1", "xy_state_topic": "test_light_rgb/state1", "state_value_template": "{{ value_json.state1.state }}", "brightness_value_template": "{{ value_json.state1.brightness }}", @@ -3201,7 +2521,6 @@ async def test_discovery_update_light_template( "effect_value_template": "{{ value_json.state1.fx }}", "hs_value_template": "{{ value_json.state1.hs }}", "rgb_value_template": "{{ value_json.state1.rgb }}", - "white_value_template": "{{ value_json.state1.white }}", "xy_value_template": "{{ value_json.state1.xy }}", } @@ -3214,14 +2533,12 @@ async def test_discovery_update_light_template( "color_temp_command_topic": "test_light_rgb/state1", "effect_command_topic": "test_light_rgb/effect/set", "hs_command_topic": "test_light_rgb/hs/set", - "white_value_command_topic": "test_light_rgb/white_value/set", "xy_command_topic": "test_light_rgb/xy/set", "brightness_state_topic": "test_light_rgb/state1", "color_temp_state_topic": "test_light_rgb/state1", "effect_state_topic": "test_light_rgb/state1", "hs_state_topic": "test_light_rgb/state1", "rgb_state_topic": "test_light_rgb/state1", - "white_value_state_topic": "test_light_rgb/state1", "xy_state_topic": "test_light_rgb/state1", "state_value_template": "{{ value_json.state2.state }}", "brightness_value_template": "{{ value_json.state2.brightness }}", @@ -3229,7 +2546,6 @@ async def test_discovery_update_light_template( "effect_value_template": "{{ value_json.state2.fx }}", "hs_value_template": "{{ value_json.state2.hs }}", "rgb_value_template": "{{ value_json.state2.rgb }}", - "white_value_template": "{{ value_json.state2.white }}", "xy_value_template": "{{ value_json.state2.xy }}", } state_data1 = [ @@ -3244,7 +2560,6 @@ async def test_discovery_update_light_template( [ ("brightness", 100), ("color_temp", 123), - ("white_value", 100), ("effect", "cycle"), ], ), @@ -3281,7 +2596,7 @@ async def test_discovery_update_light_template( ) ], "on", - [("white_value", None), ("xy_color", (0.3, 0.401))], + [("xy_color", (0.3, 0.4))], ), ] state_data2 = [ @@ -3296,7 +2611,6 @@ async def test_discovery_update_light_template( [ ("brightness", 50), ("color_temp", 200), - ("white_value", 50), ("effect", "loop"), ], ), @@ -3368,7 +2682,7 @@ async def test_discovery_update_light_template( ) ], "on", - [("white_value", None), ("xy_color", (0.4, 0.3))], + [("xy_color", (0.4, 0.3))], ), ( [ @@ -3378,7 +2692,7 @@ async def test_discovery_update_light_template( ) ], "on", - [("white_value", None), ("xy_color", (0.4, 0.3))], + [("xy_color", (0.4, 0.3))], ), ] @@ -3646,7 +2960,6 @@ async def test_reloadable_late(hass, mqtt_client_mock, caplog, tmp_path): "topic,value,attribute,attribute_value,init_payload", [ ("state_topic", "ON", None, "on", None), - ("brightness_state_topic", "60", "brightness", 60, ("state_topic", "ON")), ( "color_mode_state_topic", "200", @@ -3695,8 +3008,40 @@ async def test_encoding_subscribable_topics( config[CONF_RGBWW_COMMAND_TOPIC] = "light/CONF_RGBWW_COMMAND_TOPIC" config[CONF_XY_COMMAND_TOPIC] = "light/CONF_XY_COMMAND_TOPIC" config[CONF_EFFECT_LIST] = ["colorloop", "random"] - if attribute and attribute == "brightness": - config[CONF_WHITE_VALUE_COMMAND_TOPIC] = "light/CONF_WHITE_VALUE_COMMAND_TOPIC" + + await help_test_encoding_subscribable_topics( + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + light.DOMAIN, + config, + topic, + value, + attribute, + attribute_value, + init_payload, + ) + + +@pytest.mark.parametrize( + "topic,value,attribute,attribute_value,init_payload", + [ + ("brightness_state_topic", "60", "brightness", 60, ("state_topic", "ON")), + ], +) +async def test_encoding_subscribable_topics_brightness( + hass, + mqtt_mock_entry_with_yaml_config, + caplog, + topic, + value, + attribute, + attribute_value, + init_payload, +): + """Test handling of incoming encoded payload for a brightness only light.""" + config = copy.deepcopy(DEFAULT_CONFIG[light.DOMAIN]) + config[CONF_BRIGHTNESS_COMMAND_TOPIC] = "light/CONF_BRIGHTNESS_COMMAND_TOPIC" await help_test_encoding_subscribable_topics( hass, diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index b930de9b6c3..d57fc1cceee 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -1,6 +1,6 @@ """The tests for the MQTT JSON light platform. -Configuration with RGB, brightness, color temp, effect, white value and XY: +Configuration with RGB, brightness, color temp, effect, and XY: light: platform: mqtt_json @@ -11,22 +11,8 @@ light: color_temp: true effect: true rgb: true - white_value: true xy: true -Configuration with RGB, brightness, color temp, effect, white value: - -light: - platform: mqtt_json - name: mqtt_json_light_1 - state_topic: "home/rgb1" - command_topic: "home/rgb1/set" - brightness: true - color_temp: true - effect: true - rgb: true - white_value: true - Configuration with RGB, brightness, color temp and effect: light: @@ -182,7 +168,7 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock_entry_no_yaml_conf assert hass.states.get("light.test") is None -@pytest.mark.parametrize("deprecated", ("color_temp", "hs", "rgb", "white_value", "xy")) +@pytest.mark.parametrize("deprecated", ("color_temp", "hs", "rgb", "xy")) async def test_fail_setup_if_color_mode_deprecated( hass, mqtt_mock_entry_no_yaml_config, deprecated ): @@ -267,10 +253,10 @@ async def test_rgb_light(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features -async def test_no_color_brightness_color_temp_white_val_if_no_topics( +async def test_no_color_brightness_color_temp_if_no_topics( hass, mqtt_mock_entry_with_yaml_config ): - """Test for no RGB, brightness, color temp, effect, white val or XY.""" + """Test for no RGB, brightness, color temp, effector XY.""" assert await async_setup_component( hass, light.DOMAIN, @@ -295,7 +281,6 @@ async def test_no_color_brightness_color_temp_white_val_if_no_topics( assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None assert state.attributes.get("effect") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get("hs_color") is None @@ -307,7 +292,6 @@ async def test_no_color_brightness_color_temp_white_val_if_no_topics( assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None assert state.attributes.get("effect") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get("hs_color") is None @@ -338,7 +322,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi "color_temp": True, "effect": True, "rgb": True, - "white_value": True, "xy": True, "hs": True, "qos": "0", @@ -357,19 +340,17 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi | light.SUPPORT_EFFECT | light.SUPPORT_FLASH | light.SUPPORT_TRANSITION - | light.SUPPORT_WHITE_VALUE ) assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None assert state.attributes.get("effect") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get("hs_color") is None assert not state.attributes.get(ATTR_ASSUMED_STATE) - # Turn on the light, full white + # Turn on the light async_fire_mqtt_message( hass, "test_light_rgb", @@ -377,8 +358,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi '"color":{"r":255,"g":255,"b":255},' '"brightness":255,' '"color_temp":155,' - '"effect":"colorloop",' - '"white_value":150}', + '"effect":"colorloop"}', ) state = hass.states.get("light.test") @@ -387,7 +367,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi assert state.attributes.get("brightness") == 255 assert state.attributes.get("color_temp") == 155 assert state.attributes.get("effect") == "colorloop" - assert state.attributes.get("white_value") == 150 assert state.attributes.get("xy_color") == (0.323, 0.329) assert state.attributes.get("hs_color") == (0.0, 0.0) @@ -446,11 +425,6 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi light_state = hass.states.get("light.test") assert light_state.attributes.get("effect") == "colorloop" - async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "white_value":155}') - - light_state = hass.states.get("light.test") - assert light_state.attributes.get("white_value") == 155 - async def test_controlling_state_via_topic2( hass, mqtt_mock_entry_with_yaml_config, caplog @@ -499,7 +473,6 @@ async def test_controlling_state_via_topic2( assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") is None assert state.attributes.get("supported_color_modes") == supported_color_modes - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -512,8 +485,7 @@ async def test_controlling_state_via_topic2( '"color":{"r":255,"g":128,"b":64, "c": 32, "w": 16, "x": 1, "y": 1},' '"brightness":255,' '"color_temp":155,' - '"effect":"colorloop",' - '"white_value":150}', + '"effect":"colorloop"}', ) state = hass.states.get("light.test") @@ -526,7 +498,6 @@ async def test_controlling_state_via_topic2( assert state.attributes.get("rgb_color") == (255, 136, 74) assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") == (255, 128, 64, 32, 16) - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") == (0.571, 0.361) # Light turned off @@ -595,11 +566,6 @@ async def test_controlling_state_via_topic2( state = hass.states.get("light.test") assert state.attributes.get("effect") == "other_effect" - # White value should be ignored - async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "white_value":155}') - state = hass.states.get("light.test") - assert state.attributes.get("white_value") is None - # Invalid color mode async_fire_mqtt_message( hass, "test_light_rgb", '{"state":"ON", "color_mode":"col_temp"}' @@ -635,7 +601,6 @@ async def test_sending_mqtt_commands_and_optimistic( "hs_color": [100, 100], "effect": "random", "color_temp": 100, - "white_value": 50, }, ) @@ -658,7 +623,6 @@ async def test_sending_mqtt_commands_and_optimistic( "hs": True, "rgb": True, "xy": True, - "white_value": True, "qos": 2, } }, @@ -672,7 +636,6 @@ async def test_sending_mqtt_commands_and_optimistic( assert state.attributes.get("hs_color") == (100, 100) assert state.attributes.get("effect") == "random" assert state.attributes.get("color_temp") == 100 - assert state.attributes.get("white_value") == 50 expected_features = ( light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR @@ -680,7 +643,6 @@ async def test_sending_mqtt_commands_and_optimistic( | light.SUPPORT_EFFECT | light.SUPPORT_FLASH | light.SUPPORT_TRANSITION - | light.SUPPORT_WHITE_VALUE ) assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get(ATTR_ASSUMED_STATE) @@ -720,9 +682,7 @@ async def test_sending_mqtt_commands_and_optimistic( hass, "light.test", brightness=50, xy_color=[0.123, 0.123] ) await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78]) - await common.async_turn_on( - hass, "light.test", rgb_color=[255, 128, 0], white_value=80 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) mqtt_mock.async_publish.assert_has_calls( [ @@ -750,8 +710,7 @@ async def test_sending_mqtt_commands_and_optimistic( "test_light_rgb/set", JsonValidator( '{"state": "ON", "color": {"r": 255, "g": 128, "b": 0,' - ' "x": 0.611, "y": 0.375, "h": 30.118, "s": 100.0},' - ' "white_value": 80}' + ' "x": 0.611, "y": 0.375, "h": 30.118, "s": 100.0}}' ), 2, False, @@ -765,7 +724,6 @@ async def test_sending_mqtt_commands_and_optimistic( assert state.attributes["rgb_color"] == (255, 128, 0) assert state.attributes["brightness"] == 50 assert state.attributes["hs_color"] == (30.118, 100) - assert state.attributes["white_value"] == 80 assert state.attributes["xy_color"] == (0.611, 0.375) @@ -783,7 +741,6 @@ async def test_sending_mqtt_commands_and_optimistic2( "color_mode": "rgb", "effect": "random", "hs_color": [100, 100], - "white_value": 50, }, ) @@ -831,7 +788,6 @@ async def test_sending_mqtt_commands_and_optimistic2( assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") is None assert state.attributes.get("supported_color_modes") == supported_color_modes - assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None assert state.attributes.get(ATTR_ASSUMED_STATE) @@ -876,7 +832,6 @@ async def test_sending_mqtt_commands_and_optimistic2( assert state.attributes["xy_color"] == (0.654, 0.301) assert "rgbw_color" not in state.attributes assert "rgbww_color" not in state.attributes - assert "white_value" not in state.attributes mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", JsonValidator( @@ -887,10 +842,8 @@ async def test_sending_mqtt_commands_and_optimistic2( ) mqtt_mock.async_publish.reset_mock() - # Set rgb color, white value should be discarded - await common.async_turn_on( - hass, "light.test", rgb_color=[255, 128, 0], white_value=80 - ) + # Set rgb color + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes["brightness"] == 75 @@ -900,7 +853,6 @@ async def test_sending_mqtt_commands_and_optimistic2( assert state.attributes["xy_color"] == (0.611, 0.375) assert "rgbw_color" not in state.attributes assert "rgbww_color" not in state.attributes - assert "white_value" not in state.attributes mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", JsonValidator('{"state": "ON", "color": {"r": 255, "g": 128, "b": 0} }'), @@ -910,9 +862,7 @@ async def test_sending_mqtt_commands_and_optimistic2( mqtt_mock.async_publish.reset_mock() # Set rgbw color - await common.async_turn_on( - hass, "light.test", rgbw_color=[255, 128, 0, 123], white_value=80 - ) + await common.async_turn_on(hass, "light.test", rgbw_color=[255, 128, 0, 123]) state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes["brightness"] == 75 @@ -921,7 +871,6 @@ async def test_sending_mqtt_commands_and_optimistic2( assert state.attributes["hs_color"] == (30.0, 67.451) assert state.attributes["rgb_color"] == (255, 169, 83) assert "rgbww_color" not in state.attributes - assert "white_value" not in state.attributes assert state.attributes["xy_color"] == (0.526, 0.393) mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", @@ -943,7 +892,6 @@ async def test_sending_mqtt_commands_and_optimistic2( assert state.attributes["hs_color"] == (29.872, 92.157) assert state.attributes["rgb_color"] == (255, 137, 20) assert "rgbw_color" not in state.attributes - assert "white_value" not in state.attributes assert state.attributes["xy_color"] == (0.596, 0.382) mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", @@ -968,7 +916,6 @@ async def test_sending_mqtt_commands_and_optimistic2( assert state.attributes["xy_color"] == (0.123, 0.223) assert "rgbw_color" not in state.attributes assert "rgbww_color" not in state.attributes - assert "white_value" not in state.attributes mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", JsonValidator( @@ -993,7 +940,6 @@ async def test_sending_hs_color(hass, mqtt_mock_entry_with_yaml_config): "command_topic": "test_light_rgb/set", "brightness": True, "hs": True, - "white_value": True, } }, ) @@ -1008,9 +954,7 @@ async def test_sending_hs_color(hass, mqtt_mock_entry_with_yaml_config): hass, "light.test", brightness=50, xy_color=[0.123, 0.123] ) await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78]) - await common.async_turn_on( - hass, "light.test", rgb_color=[255, 128, 0], white_value=80 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) mqtt_mock.async_publish.assert_has_calls( [ @@ -1034,10 +978,7 @@ async def test_sending_hs_color(hass, mqtt_mock_entry_with_yaml_config): ), call( "test_light_rgb/set", - JsonValidator( - '{"state": "ON", "color": {"h": 30.118, "s": 100.0},' - ' "white_value": 80}' - ), + JsonValidator('{"state": "ON", "color": {"h": 30.118, "s": 100.0}}'), 0, False, ), @@ -1193,7 +1134,6 @@ async def test_sending_rgb_color_with_brightness( "command_topic": "test_light_rgb/set", "brightness": True, "rgb": True, - "white_value": True, } }, ) @@ -1208,9 +1148,7 @@ async def test_sending_rgb_color_with_brightness( ) await common.async_turn_on(hass, "light.test", brightness=255, hs_color=[359, 78]) await common.async_turn_on(hass, "light.test", brightness=1) - await common.async_turn_on( - hass, "light.test", rgb_color=[255, 128, 0], white_value=80 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) mqtt_mock.async_publish.assert_has_calls( [ @@ -1240,10 +1178,7 @@ async def test_sending_rgb_color_with_brightness( ), call( "test_light_rgb/set", - JsonValidator( - '{"state": "ON", "color": {"r": 255, "g": 128, "b": 0},' - ' "white_value": 80}' - ), + JsonValidator('{"state": "ON", "color": {"r": 255, "g": 128, "b": 0}}'), 0, False, ), @@ -1267,7 +1202,6 @@ async def test_sending_rgb_color_with_scaled_brightness( "brightness": True, "brightness_scale": 100, "rgb": True, - "white_value": True, } }, ) @@ -1282,9 +1216,7 @@ async def test_sending_rgb_color_with_scaled_brightness( ) await common.async_turn_on(hass, "light.test", brightness=255, hs_color=[359, 78]) await common.async_turn_on(hass, "light.test", brightness=1) - await common.async_turn_on( - hass, "light.test", rgb_color=[255, 128, 0], white_value=80 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) mqtt_mock.async_publish.assert_has_calls( [ @@ -1314,10 +1246,7 @@ async def test_sending_rgb_color_with_scaled_brightness( ), call( "test_light_rgb/set", - JsonValidator( - '{"state": "ON", "color": {"r": 255, "g": 128, "b": 0},' - ' "white_value": 80}' - ), + JsonValidator('{"state": "ON", "color": {"r": 255, "g": 128, "b": 0}}'), 0, False, ), @@ -1338,7 +1267,6 @@ async def test_sending_xy_color(hass, mqtt_mock_entry_with_yaml_config): "command_topic": "test_light_rgb/set", "brightness": True, "xy": True, - "white_value": True, } }, ) @@ -1352,9 +1280,7 @@ async def test_sending_xy_color(hass, mqtt_mock_entry_with_yaml_config): hass, "light.test", brightness=50, xy_color=[0.123, 0.123] ) await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78]) - await common.async_turn_on( - hass, "light.test", rgb_color=[255, 128, 0], white_value=80 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) mqtt_mock.async_publish.assert_has_calls( [ @@ -1378,10 +1304,7 @@ async def test_sending_xy_color(hass, mqtt_mock_entry_with_yaml_config): ), call( "test_light_rgb/set", - JsonValidator( - '{"state": "ON", "color": {"x": 0.611, "y": 0.375},' - ' "white_value": 80}' - ), + JsonValidator('{"state": "ON", "color": {"x": 0.611, "y": 0.375}}'), 0, False, ), @@ -1605,7 +1528,7 @@ async def test_brightness_scale(hass, mqtt_mock_entry_with_yaml_config): async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): - """Test that invalid color/brightness/white/etc. values are ignored.""" + """Test that invalid color/brightness/etc. values are ignored.""" assert await async_setup_component( hass, light.DOMAIN, @@ -1619,7 +1542,6 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): "brightness": True, "color_temp": True, "rgb": True, - "white_value": True, "qos": "0", } }, @@ -1635,12 +1557,10 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): | light.SUPPORT_COLOR_TEMP | light.SUPPORT_FLASH | light.SUPPORT_TRANSITION - | light.SUPPORT_WHITE_VALUE ) assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("color_temp") is None assert not state.attributes.get(ATTR_ASSUMED_STATE) @@ -1651,7 +1571,6 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): '{"state":"ON",' '"color":{"r":255,"g":255,"b":255},' '"brightness": 255,' - '"white_value": 255,' '"color_temp": 100,' '"effect": "rainbow"}', ) @@ -1660,7 +1579,6 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): assert state.state == STATE_ON assert state.attributes.get("rgb_color") == (255, 255, 255) assert state.attributes.get("brightness") == 255 - assert state.attributes.get("white_value") == 255 assert state.attributes.get("color_temp") == 100 # Empty color value @@ -1721,16 +1639,6 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): assert state.state == STATE_ON assert state.attributes.get("brightness") == 255 - # Bad white value - async_fire_mqtt_message( - hass, "test_light_rgb", '{"state":"ON",' '"white_value": "badValue"}' - ) - - # White value should not have changed - state = hass.states.get("light.test") - assert state.state == STATE_ON - assert state.attributes.get("white_value") == 255 - # Bad color temperature async_fire_mqtt_message( hass, "test_light_rgb", '{"state":"ON",' '"color_temp": "badValue"}' @@ -2071,8 +1979,6 @@ async def test_publishing_with_custom_encoding( config = copy.deepcopy(DEFAULT_CONFIG[domain]) if topic == "effect_command_topic": config["effect_list"] = ["random", "color_loop"] - elif topic == "white_command_topic": - config["rgb_command_topic"] = "some-cmd-topic" await help_test_publishing_with_custom_encoding( hass, diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 2c96468057f..e4f755eab4e 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -13,7 +13,6 @@ light: state_template: '{{ value.split(",")[0] }}' brightness_template: '{{ value.split(",")[1] }}' color_temp_template: '{{ value.split(",")[2] }}' - white_value_template: '{{ value.split(",")[3] }}' red_template: '{{ value.split(",")[4].split("-")[0] }}' green_template: '{{ value.split(",")[4].split("-")[1] }}' blue_template: '{{ value.split(",")[4].split("-")[2] }}' @@ -22,8 +21,6 @@ If your light doesn't support brightness feature, omit `brightness_template`. If your light doesn't support color temp feature, omit `color_temp_template`. -If your light doesn't support white value feature, omit `white_value_template`. - If your light doesn't support RGB feature, omit `(red|green|blue)_template`. """ import copy @@ -194,7 +191,6 @@ async def test_state_change_via_topic(hass, mqtt_mock_entry_with_yaml_config): "command_on_template": "on," "{{ brightness|d }}," "{{ color_temp|d }}," - "{{ white_value|d }}," "{{ red|d }}-" "{{ green|d }}-" "{{ blue|d }}", @@ -211,7 +207,6 @@ async def test_state_change_via_topic(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None - assert state.attributes.get("white_value") is None assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "test_light_rgb", "on") @@ -221,7 +216,6 @@ async def test_state_change_via_topic(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None - assert state.attributes.get("white_value") is None async_fire_mqtt_message(hass, "test_light_rgb", "off") @@ -234,10 +228,10 @@ async def test_state_change_via_topic(hass, mqtt_mock_entry_with_yaml_config): assert state.state == STATE_UNKNOWN -async def test_state_brightness_color_effect_temp_white_change_via_topic( +async def test_state_brightness_color_effect_temp_change_via_topic( hass, mqtt_mock_entry_with_yaml_config ): - """Test state, bri, color, effect, color temp, white val change.""" + """Test state, bri, color, effect, color temp change.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( hass, @@ -253,7 +247,6 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( "command_on_template": "on," "{{ brightness|d }}," "{{ color_temp|d }}," - "{{ white_value|d }}," "{{ red|d }}-" "{{ green|d }}-" "{{ blue|d }}," @@ -262,11 +255,10 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( "state_template": '{{ value.split(",")[0] }}', "brightness_template": '{{ value.split(",")[1] }}', "color_temp_template": '{{ value.split(",")[2] }}', - "white_value_template": '{{ value.split(",")[3] }}', - "red_template": '{{ value.split(",")[4].' 'split("-")[0] }}', - "green_template": '{{ value.split(",")[4].' 'split("-")[1] }}', - "blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}', - "effect_template": '{{ value.split(",")[5] }}', + "red_template": '{{ value.split(",")[3].' 'split("-")[0] }}', + "green_template": '{{ value.split(",")[3].' 'split("-")[1] }}', + "blue_template": '{{ value.split(",")[3].' 'split("-")[2] }}', + "effect_template": '{{ value.split(",")[4] }}', } }, ) @@ -279,18 +271,16 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( assert state.attributes.get("brightness") is None assert state.attributes.get("effect") is None assert state.attributes.get("color_temp") is None - assert state.attributes.get("white_value") is None assert not state.attributes.get(ATTR_ASSUMED_STATE) - # turn on the light, full white - async_fire_mqtt_message(hass, "test_light_rgb", "on,255,145,123,255-128-64,") + # turn on the light + async_fire_mqtt_message(hass, "test_light_rgb", "on,255,145,255-128-64,") state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("rgb_color") == (255, 128, 63) assert state.attributes.get("brightness") == 255 assert state.attributes.get("color_temp") == 145 - assert state.attributes.get("white_value") == 123 assert state.attributes.get("effect") is None # make the light state unknown @@ -318,19 +308,13 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( assert light_state.attributes["color_temp"] == 195 # change the color - async_fire_mqtt_message(hass, "test_light_rgb", "on,,,,41-42-43") + async_fire_mqtt_message(hass, "test_light_rgb", "on,,,41-42-43") light_state = hass.states.get("light.test") assert light_state.attributes.get("rgb_color") == (243, 249, 255) - # change the white value - async_fire_mqtt_message(hass, "test_light_rgb", "on,,,134") - - light_state = hass.states.get("light.test") - assert light_state.attributes["white_value"] == 134 - # change the effect - async_fire_mqtt_message(hass, "test_light_rgb", "on,,,,41-42-43,rainbow") + async_fire_mqtt_message(hass, "test_light_rgb", "on,,,41-42-43,rainbow") light_state = hass.states.get("light.test") assert light_state.attributes.get("effect") == "rainbow" @@ -348,7 +332,6 @@ async def test_sending_mqtt_commands_and_optimistic( "hs_color": [100, 100], "effect": "random", "color_temp": 100, - "white_value": 50, }, ) @@ -368,7 +351,6 @@ async def test_sending_mqtt_commands_and_optimistic( "command_on_template": "on," "{{ brightness|d }}," "{{ color_temp|d }}," - "{{ white_value|d }}," "{{ red|d }}-" "{{ green|d }}-" "{{ blue|d }}," @@ -379,11 +361,10 @@ async def test_sending_mqtt_commands_and_optimistic( "optimistic": True, "state_template": '{{ value.split(",")[0] }}', "color_temp_template": '{{ value.split(",")[2] }}', - "white_value_template": '{{ value.split(",")[3] }}', - "red_template": '{{ value.split(",")[4].' 'split("-")[0] }}', - "green_template": '{{ value.split(",")[4].' 'split("-")[1] }}', - "blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}', - "effect_template": '{{ value.split(",")[5] }}', + "red_template": '{{ value.split(",")[3].' 'split("-")[0] }}', + "green_template": '{{ value.split(",")[3].' 'split("-")[1] }}', + "blue_template": '{{ value.split(",")[3].' 'split("-")[2] }}', + "effect_template": '{{ value.split(",")[4] }}', "qos": 2, } }, @@ -396,7 +377,6 @@ async def test_sending_mqtt_commands_and_optimistic( assert state.attributes.get("hs_color") == (100, 100) assert state.attributes.get("effect") == "random" assert state.attributes.get("color_temp") == 100 - assert state.attributes.get("white_value") == 50 assert state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_off(hass, "light.test") @@ -409,7 +389,7 @@ async def test_sending_mqtt_commands_and_optimistic( await common.async_turn_on(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,,--,-", 2, False + "test_light_rgb/set", "on,,,--,-", 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") @@ -418,7 +398,7 @@ async def test_sending_mqtt_commands_and_optimistic( # Set color_temp await common.async_turn_on(hass, "light.test", color_temp=70) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,70,,--,-", 2, False + "test_light_rgb/set", "on,,70,--,-", 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") @@ -428,29 +408,26 @@ async def test_sending_mqtt_commands_and_optimistic( # Set full brightness await common.async_turn_on(hass, "light.test", brightness=255) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,255,,,--,-", 2, False + "test_light_rgb/set", "on,255,,--,-", 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") assert state.state == STATE_ON # Full brightness - no scaling of RGB values sent over MQTT - await common.async_turn_on( - hass, "light.test", rgb_color=[255, 128, 0], white_value=80 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,80,255-128-0,30.118-100.0", 2, False + "test_light_rgb/set", "on,,,255-128-0,30.118-100.0", 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("white_value") == 80 assert state.attributes.get("rgb_color") == (255, 128, 0) # Full brightness - normalization of RGB values sent over MQTT await common.async_turn_on(hass, "light.test", rgb_color=[128, 64, 0]) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,,255-127-0,30.0-100.0", 2, False + "test_light_rgb/set", "on,,,255-127-0,30.0-100.0", 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") @@ -460,36 +437,30 @@ async def test_sending_mqtt_commands_and_optimistic( # Set half brightness await common.async_turn_on(hass, "light.test", brightness=128) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,128,,,--,-", 2, False + "test_light_rgb/set", "on,128,,--,-", 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") assert state.state == STATE_ON # Half brightness - scaling of RGB values sent over MQTT - await common.async_turn_on( - hass, "light.test", rgb_color=[0, 255, 128], white_value=40 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[0, 255, 128]) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,40,0-128-64,150.118-100.0", 2, False + "test_light_rgb/set", "on,,,0-128-64,150.118-100.0", 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("white_value") == 40 assert state.attributes.get("rgb_color") == (0, 255, 128) # Half brightness - normalization+scaling of RGB values sent over MQTT - await common.async_turn_on( - hass, "light.test", rgb_color=[0, 32, 16], white_value=40 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[0, 32, 16]) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,40,0-128-64,150.0-100.0", 2, False + "test_light_rgb/set", "on,,,0-128-64,150.0-100.0", 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes.get("white_value") == 40 assert state.attributes.get("rgb_color") == (0, 255, 127) @@ -512,7 +483,6 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( "command_on_template": "on," "{{ brightness|d }}," "{{ color_temp|d }}," - "{{ white_value|d }}," "{{ red|d }}-" "{{ green|d }}-" "{{ blue|d }}," @@ -522,11 +492,10 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( "state_template": '{{ value.split(",")[0] }}', "brightness_template": '{{ value.split(",")[1] }}', "color_temp_template": '{{ value.split(",")[2] }}', - "white_value_template": '{{ value.split(",")[3] }}', - "red_template": '{{ value.split(",")[4].' 'split("-")[0] }}', - "green_template": '{{ value.split(",")[4].' 'split("-")[1] }}', - "blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}', - "effect_template": '{{ value.split(",")[5] }}', + "red_template": '{{ value.split(",")[3].' 'split("-")[0] }}', + "green_template": '{{ value.split(",")[3].' 'split("-")[1] }}', + "blue_template": '{{ value.split(",")[3].' 'split("-")[2] }}', + "effect_template": '{{ value.split(",")[4] }}', } }, ) @@ -539,7 +508,6 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( assert not state.attributes.get("hs_color") assert not state.attributes.get("effect") assert not state.attributes.get("color_temp") - assert not state.attributes.get("white_value") assert not state.attributes.get(ATTR_ASSUMED_STATE) await common.async_turn_off(hass, "light.test") @@ -552,7 +520,7 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( await common.async_turn_on(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,,--,-", 0, False + "test_light_rgb/set", "on,,,--,-", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") @@ -561,7 +529,7 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( # Set color_temp await common.async_turn_on(hass, "light.test", color_temp=70) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,70,,--,-", 0, False + "test_light_rgb/set", "on,,70,--,-", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") @@ -571,7 +539,7 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( # Set full brightness await common.async_turn_on(hass, "light.test", brightness=255) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,255,,,--,-", 0, False + "test_light_rgb/set", "on,255,,--,-", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") @@ -579,48 +547,41 @@ async def test_sending_mqtt_commands_non_optimistic_brightness_template( assert not state.attributes.get("brightness") # Full brightness - no scaling of RGB values sent over MQTT - await common.async_turn_on( - hass, "light.test", rgb_color=[255, 128, 0], white_value=80 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,80,255-128-0,30.118-100.0", 0, False + "test_light_rgb/set", "on,,,255-128-0,30.118-100.0", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") assert state.state == STATE_UNKNOWN - assert not state.attributes.get("white_value") assert not state.attributes.get("rgb_color") # Full brightness - normalization of RGB values sent over MQTT await common.async_turn_on(hass, "light.test", rgb_color=[128, 64, 0]) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,,255-127-0,30.0-100.0", 0, False + "test_light_rgb/set", "on,,,255-127-0,30.0-100.0", 0, False ) mqtt_mock.async_publish.reset_mock() # Set half brightness await common.async_turn_on(hass, "light.test", brightness=128) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,128,,,--,-", 0, False + "test_light_rgb/set", "on,128,,--,-", 0, False ) mqtt_mock.async_publish.reset_mock() # Half brightness - no scaling of RGB values sent over MQTT - await common.async_turn_on( - hass, "light.test", rgb_color=[0, 255, 128], white_value=40 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[0, 255, 128]) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,40,0-255-128,150.118-100.0", 0, False + "test_light_rgb/set", "on,,,0-255-128,150.118-100.0", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") # Half brightness - normalization but no scaling of RGB values sent over MQTT - await common.async_turn_on( - hass, "light.test", rgb_color=[0, 32, 16], white_value=40 - ) + await common.async_turn_on(hass, "light.test", rgb_color=[0, 32, 16]) mqtt_mock.async_publish.assert_called_once_with( - "test_light_rgb/set", "on,,,40,0-255-127,150.0-100.0", 0, False + "test_light_rgb/set", "on,,,0-255-127,150.0-100.0", 0, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") @@ -795,11 +756,10 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): "state_template": '{{ value.split(",")[0] }}', "brightness_template": '{{ value.split(",")[1] }}', "color_temp_template": '{{ value.split(",")[2] }}', - "white_value_template": '{{ value.split(",")[3] }}', - "red_template": '{{ value.split(",")[4].' 'split("-")[0] }}', - "green_template": '{{ value.split(",")[4].' 'split("-")[1] }}', - "blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}', - "effect_template": '{{ value.split(",")[5] }}', + "red_template": '{{ value.split(",")[3].' 'split("-")[0] }}', + "green_template": '{{ value.split(",")[3].' 'split("-")[1] }}', + "blue_template": '{{ value.split(",")[3].' 'split("-")[2] }}', + "effect_template": '{{ value.split(",")[4] }}', } }, ) @@ -812,20 +772,16 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None assert state.attributes.get("effect") is None - assert state.attributes.get("white_value") is None assert not state.attributes.get(ATTR_ASSUMED_STATE) - # turn on the light, full white - async_fire_mqtt_message( - hass, "test_light_rgb", "on,255,215,222,255-255-255,rainbow" - ) + # turn on the light + async_fire_mqtt_message(hass, "test_light_rgb", "on,255,215,255-255-255,rainbow") state = hass.states.get("light.test") assert state.state == STATE_ON assert state.attributes.get("brightness") == 255 assert state.attributes.get("color_temp") == 215 assert state.attributes.get("rgb_color") == (255, 255, 255) - assert state.attributes.get("white_value") == 222 assert state.attributes.get("effect") == "rainbow" # bad state value @@ -856,13 +812,6 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): state = hass.states.get("light.test") assert state.attributes.get("rgb_color") == (255, 255, 255) - # bad white value values - async_fire_mqtt_message(hass, "test_light_rgb", "on,,,off,255-255-255") - - # white value should not have changed - state = hass.states.get("light.test") - assert state.attributes.get("white_value") == 222 - # bad effect value async_fire_mqtt_message(hass, "test_light_rgb", "on,255,a-b-c,white") @@ -1191,8 +1140,6 @@ async def test_publishing_with_custom_encoding( config = copy.deepcopy(DEFAULT_CONFIG[domain]) if topic == "effect_command_topic": config["effect_list"] = ["random", "color_loop"] - elif topic == "white_command_topic": - config["rgb_command_topic"] = "some-cmd-topic" await help_test_publishing_with_custom_encoding( hass, From 2ab4817d8173825f56de536f7e34ca61e02758ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 16 Aug 2022 17:05:09 +0200 Subject: [PATCH 3382/3516] Use secure in Speedtest (#76852) --- homeassistant/components/speedtestdotnet/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 00221c39a42..2684b24c81f 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -85,7 +85,7 @@ class SpeedTestDataCoordinator(DataUpdateCoordinator): def initialize(self) -> None: """Initialize speedtest api.""" - self.api = speedtest.Speedtest() + self.api = speedtest.Speedtest(secure=True) self.update_servers() def update_servers(self): From 1a2a20cfc5f51a3a7fb3aac69fbf5f9654c270c2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 16 Aug 2022 17:09:00 +0200 Subject: [PATCH 3383/3516] Update google-cloud-texttospeech to 2.12.1 (#76854) --- homeassistant/components/google_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_cloud/manifest.json b/homeassistant/components/google_cloud/manifest.json index d48571c55bd..9ce8085d5e8 100644 --- a/homeassistant/components/google_cloud/manifest.json +++ b/homeassistant/components/google_cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "google_cloud", "name": "Google Cloud Platform", "documentation": "https://www.home-assistant.io/integrations/google_cloud", - "requirements": ["google-cloud-texttospeech==2.12.0"], + "requirements": ["google-cloud-texttospeech==2.12.1"], "codeowners": ["@lufton"], "iot_class": "cloud_push" } diff --git a/requirements_all.txt b/requirements_all.txt index 76ee358edb6..790a9572334 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -745,7 +745,7 @@ goodwe==0.2.18 google-cloud-pubsub==2.11.0 # homeassistant.components.google_cloud -google-cloud-texttospeech==2.12.0 +google-cloud-texttospeech==2.12.1 # homeassistant.components.nest google-nest-sdm==2.0.0 From 93a72982ce73278c9588b88b97ae746dd394897f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 16 Aug 2022 17:09:50 +0200 Subject: [PATCH 3384/3516] Update debugpy to 1.6.3 (#76849) --- homeassistant/components/debugpy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index 3cc6c16e831..dc0818f54b6 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -2,7 +2,7 @@ "domain": "debugpy", "name": "Remote Python Debugger", "documentation": "https://www.home-assistant.io/integrations/debugpy", - "requirements": ["debugpy==1.6.2"], + "requirements": ["debugpy==1.6.3"], "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 790a9572334..8a19ae220d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -523,7 +523,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.6.2 +debugpy==1.6.3 # homeassistant.components.decora # decora==0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3c6af75e928..9e04359182a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -400,7 +400,7 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.debugpy -debugpy==1.6.2 +debugpy==1.6.3 # homeassistant.components.ihc # homeassistant.components.namecheapdns From d50b5cebeec776aecb5f3236d9c4090c69003100 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 16 Aug 2022 17:10:11 +0200 Subject: [PATCH 3385/3516] Various improvement for JustNimbus (#76858) Co-authored-by: Martin Hjelmare --- homeassistant/components/justnimbus/entity.py | 10 ++---- homeassistant/components/justnimbus/sensor.py | 8 ++--- .../components/justnimbus/test_config_flow.py | 36 ++++++++++++++++++- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/justnimbus/entity.py b/homeassistant/components/justnimbus/entity.py index f9ea5ba1151..cfd261f1bc0 100644 --- a/homeassistant/components/justnimbus/entity.py +++ b/homeassistant/components/justnimbus/entity.py @@ -1,19 +1,15 @@ """Base Entity for JustNimbus sensors.""" from __future__ import annotations -import justnimbus - -from homeassistant.components.sensor import SensorEntity -from homeassistant.helpers import update_coordinator from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import JustNimbusCoordinator from .const import DOMAIN +from .coordinator import JustNimbusCoordinator class JustNimbusEntity( - update_coordinator.CoordinatorEntity[justnimbus.JustNimbusModel], - SensorEntity, + CoordinatorEntity[JustNimbusCoordinator], ): """Defines a base JustNimbus entity.""" diff --git a/homeassistant/components/justnimbus/sensor.py b/homeassistant/components/justnimbus/sensor.py index 6041f84e25a..73a68ac9139 100644 --- a/homeassistant/components/justnimbus/sensor.py +++ b/homeassistant/components/justnimbus/sensor.py @@ -7,6 +7,7 @@ from typing import Any from homeassistant.components.sensor import ( SensorDeviceClass, + SensorEntity, SensorEntityDescription, SensorStateClass, ) @@ -15,6 +16,7 @@ from homeassistant.const import ( CONF_CLIENT_ID, PRESSURE_BAR, TEMP_CELSIUS, + TIME_HOURS, VOLUME_LITERS, ) from homeassistant.core import HomeAssistant @@ -82,6 +84,7 @@ SENSOR_TYPES = ( name="Pump hours", icon="mdi:clock", device_class=SensorDeviceClass.DURATION, + native_unit_of_measurement=TIME_HOURS, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.pump_hours, @@ -127,7 +130,6 @@ SENSOR_TYPES = ( name="Error code", icon="mdi:bug", entity_registry_enabled_default=False, - native_unit_of_measurement="", entity_category=EntityCategory.DIAGNOSTIC, value_fn=lambda coordinator: coordinator.data.error_code, ), @@ -167,9 +169,7 @@ async def async_setup_entry( ) -class JustNimbusSensor( - JustNimbusEntity, -): +class JustNimbusSensor(JustNimbusEntity, SensorEntity): """Implementation of the JustNimbus sensor.""" def __init__( diff --git a/tests/components/justnimbus/test_config_flow.py b/tests/components/justnimbus/test_config_flow.py index d2cfb64d4c7..1d3565c21bd 100644 --- a/tests/components/justnimbus/test_config_flow.py +++ b/tests/components/justnimbus/test_config_flow.py @@ -10,6 +10,8 @@ from homeassistant.const import CONF_CLIENT_ID from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from tests.common import MockConfigEntry + async def test_form(hass: HomeAssistant) -> None: """Test we get the form.""" @@ -30,9 +32,13 @@ async def test_form(hass: HomeAssistant) -> None: {"base": "invalid_auth"}, ), ( - JustNimbusError(), + JustNimbusError, {"base": "cannot_connect"}, ), + ( + RuntimeError, + {"base": "unknown"}, + ), ), ) async def test_form_errors( @@ -62,6 +68,34 @@ async def test_form_errors( await _set_up_justnimbus(hass=hass, flow_id=result["flow_id"]) +async def test_abort_already_configured(hass: HomeAssistant) -> None: + """Test we abort when the device is already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="JustNimbus", + data={CONF_CLIENT_ID: "test_id"}, + unique_id="test_id", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") is None + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], + user_input={ + CONF_CLIENT_ID: "test_id", + }, + ) + + assert result2.get("type") == FlowResultType.ABORT + assert result2.get("reason") == "already_configured" + + async def _set_up_justnimbus(hass: HomeAssistant, flow_id: str) -> None: """Reusable successful setup of JustNimbus sensor.""" with patch("justnimbus.JustNimbusClient.get_data"), patch( From 65f86ce44fa663c64cbd6f16331fc6e1e9806506 Mon Sep 17 00:00:00 2001 From: Igor Pakhomov Date: Tue, 16 Aug 2022 18:30:56 +0300 Subject: [PATCH 3386/3516] Add additional select for dmaker.airfresh.t2017 to xiaomi_miio (#67058) --- .coveragerc | 1 - .../components/xiaomi_miio/select.py | 320 +++++++++--------- .../xiaomi_miio/strings.select.json | 10 + tests/components/xiaomi_miio/test_select.py | 158 +++++++++ 4 files changed, 329 insertions(+), 160 deletions(-) create mode 100644 tests/components/xiaomi_miio/test_select.py diff --git a/.coveragerc b/.coveragerc index 8b632a524ff..d3e1b75b928 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1478,7 +1478,6 @@ omit = homeassistant/components/xiaomi_miio/light.py homeassistant/components/xiaomi_miio/number.py homeassistant/components/xiaomi_miio/remote.py - homeassistant/components/xiaomi_miio/select.py homeassistant/components/xiaomi_miio/sensor.py homeassistant/components/xiaomi_miio/switch.py homeassistant/components/xiaomi_tv/media_player.py diff --git a/homeassistant/components/xiaomi_miio/select.py b/homeassistant/components/xiaomi_miio/select.py index 7f6f8ce1cb1..14ceac7f2f4 100644 --- a/homeassistant/components/xiaomi_miio/select.py +++ b/homeassistant/components/xiaomi_miio/select.py @@ -1,9 +1,14 @@ """Support led_brightness for Mi Air Humidifier.""" from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field +from typing import NamedTuple from miio.fan_common import LedBrightness as FanLedBrightness +from miio.integrations.airpurifier.dmaker.airfresh_t2017 import ( + DisplayOrientation as AirfreshT2017DisplayOrientation, + PtcLevel as AirfreshT2017PtcLevel, +) from miio.integrations.airpurifier.zhimi.airfresh import ( LedBrightness as AirfreshLedBrightness, ) @@ -31,53 +36,134 @@ from .const import ( CONF_DEVICE, CONF_FLOW_TYPE, DOMAIN, - FEATURE_SET_LED_BRIGHTNESS, KEY_COORDINATOR, KEY_DEVICE, + MODEL_AIRFRESH_T2017, MODEL_AIRFRESH_VA2, - MODEL_AIRPURIFIER_3C, + MODEL_AIRHUMIDIFIER_CA1, + MODEL_AIRHUMIDIFIER_CA4, + MODEL_AIRHUMIDIFIER_CB1, + MODEL_AIRHUMIDIFIER_V1, + MODEL_AIRPURIFIER_3, + MODEL_AIRPURIFIER_3H, MODEL_AIRPURIFIER_M1, MODEL_AIRPURIFIER_M2, + MODEL_AIRPURIFIER_PROH, MODEL_FAN_SA1, MODEL_FAN_V2, MODEL_FAN_V3, MODEL_FAN_ZA1, MODEL_FAN_ZA3, MODEL_FAN_ZA4, - MODELS_HUMIDIFIER_MIIO, - MODELS_HUMIDIFIER_MIOT, - MODELS_PURIFIER_MIOT, ) from .device import XiaomiCoordinatedMiioEntity +ATTR_DISPLAY_ORIENTATION = "display_orientation" ATTR_LED_BRIGHTNESS = "led_brightness" - - -LED_BRIGHTNESS_MAP = {"Bright": 0, "Dim": 1, "Off": 2} -LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT = {"Bright": 2, "Dim": 1, "Off": 0} -LED_BRIGHTNESS_REVERSE_MAP = {val: key for key, val in LED_BRIGHTNESS_MAP.items()} -LED_BRIGHTNESS_REVERSE_MAP_HUMIDIFIER_MIOT = { - val: key for key, val in LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT.items() -} +ATTR_PTC_LEVEL = "ptc_level" @dataclass class XiaomiMiioSelectDescription(SelectEntityDescription): """A class that describes select entities.""" + attr_name: str = "" + options_map: dict = field(default_factory=dict) + set_method: str = "" + set_method_error_message: str = "" options: tuple = () -SELECTOR_TYPES = { - FEATURE_SET_LED_BRIGHTNESS: XiaomiMiioSelectDescription( +class AttributeEnumMapping(NamedTuple): + """Class to mapping Attribute to Enum Class.""" + + attr_name: str + enum_class: type + + +MODEL_TO_ATTR_MAP: dict[str, list] = { + MODEL_AIRFRESH_T2017: [ + AttributeEnumMapping(ATTR_DISPLAY_ORIENTATION, AirfreshT2017DisplayOrientation), + AttributeEnumMapping(ATTR_PTC_LEVEL, AirfreshT2017PtcLevel), + ], + MODEL_AIRFRESH_VA2: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirfreshLedBrightness) + ], + MODEL_AIRHUMIDIFIER_CA1: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirhumidifierLedBrightness) + ], + MODEL_AIRHUMIDIFIER_CA4: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirhumidifierMiotLedBrightness) + ], + MODEL_AIRHUMIDIFIER_CB1: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirhumidifierLedBrightness) + ], + MODEL_AIRHUMIDIFIER_V1: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirhumidifierLedBrightness) + ], + MODEL_AIRPURIFIER_3: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierMiotLedBrightness) + ], + MODEL_AIRPURIFIER_3H: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierMiotLedBrightness) + ], + MODEL_AIRPURIFIER_M1: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierLedBrightness) + ], + MODEL_AIRPURIFIER_M2: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierLedBrightness) + ], + MODEL_AIRPURIFIER_PROH: [ + AttributeEnumMapping(ATTR_LED_BRIGHTNESS, AirpurifierMiotLedBrightness) + ], + MODEL_FAN_SA1: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)], + MODEL_FAN_V2: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)], + MODEL_FAN_V3: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)], + MODEL_FAN_ZA1: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)], + MODEL_FAN_ZA3: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)], + MODEL_FAN_ZA4: [AttributeEnumMapping(ATTR_LED_BRIGHTNESS, FanLedBrightness)], +} + +SELECTOR_TYPES = ( + XiaomiMiioSelectDescription( + key=ATTR_DISPLAY_ORIENTATION, + attr_name=ATTR_DISPLAY_ORIENTATION, + name="Display Orientation", + options_map={ + "Portrait": "Forward", + "LandscapeLeft": "Left", + "LandscapeRight": "Right", + }, + set_method="set_display_orientation", + set_method_error_message="Setting the display orientation failed.", + icon="mdi:tablet", + device_class="xiaomi_miio__display_orientation", + options=("forward", "left", "right"), + entity_category=EntityCategory.CONFIG, + ), + XiaomiMiioSelectDescription( key=ATTR_LED_BRIGHTNESS, + attr_name=ATTR_LED_BRIGHTNESS, name="Led Brightness", + set_method="set_led_brightness", + set_method_error_message="Setting the led brightness failed.", icon="mdi:brightness-6", device_class="xiaomi_miio__led_brightness", options=("bright", "dim", "off"), entity_category=EntityCategory.CONFIG, ), -} + XiaomiMiioSelectDescription( + key=ATTR_PTC_LEVEL, + attr_name=ATTR_PTC_LEVEL, + name="Auxiliary Heat Level", + set_method="set_ptc_level", + set_method_error_message="Setting the ptc level failed.", + icon="mdi:fire-circle", + device_class="xiaomi_miio__ptc_level", + options=("low", "medium", "high"), + entity_category=EntityCategory.CONFIG, + ), +) async def async_setup_entry( @@ -89,44 +175,29 @@ async def async_setup_entry( if not config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: return - entities = [] model = config_entry.data[CONF_MODEL] + if model not in MODEL_TO_ATTR_MAP: + return + + entities = [] + unique_id = config_entry.unique_id device = hass.data[DOMAIN][config_entry.entry_id][KEY_DEVICE] + coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR] + attributes = MODEL_TO_ATTR_MAP[model] - if model == MODEL_AIRPURIFIER_3C: - return - if model in MODELS_HUMIDIFIER_MIIO: - entity_class = XiaomiAirHumidifierSelector - elif model in MODELS_HUMIDIFIER_MIOT: - entity_class = XiaomiAirHumidifierMiotSelector - elif model in [MODEL_AIRPURIFIER_M1, MODEL_AIRPURIFIER_M2]: - entity_class = XiaomiAirPurifierSelector - elif model in MODELS_PURIFIER_MIOT: - entity_class = XiaomiAirPurifierMiotSelector - elif model == MODEL_AIRFRESH_VA2: - entity_class = XiaomiAirFreshSelector - elif model in ( - MODEL_FAN_ZA1, - MODEL_FAN_ZA3, - MODEL_FAN_ZA4, - MODEL_FAN_SA1, - MODEL_FAN_V2, - MODEL_FAN_V3, - ): - entity_class = XiaomiFanSelector - else: - return - - description = SELECTOR_TYPES[FEATURE_SET_LED_BRIGHTNESS] - entities.append( - entity_class( - device, - config_entry, - f"{description.key}_{config_entry.unique_id}", - hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR], - description, - ) - ) + for description in SELECTOR_TYPES: + for attribute in attributes: + if description.key == attribute.attr_name: + entities.append( + XiaomiGenericSelector( + device, + config_entry, + f"{description.key}_{unique_id}", + coordinator, + description, + attribute.enum_class, + ) + ) async_add_entities(entities) @@ -141,129 +212,60 @@ class XiaomiSelector(XiaomiCoordinatedMiioEntity, SelectEntity): self.entity_description = description -class XiaomiAirHumidifierSelector(XiaomiSelector): - """Representation of a Xiaomi Air Humidifier selector.""" +class XiaomiGenericSelector(XiaomiSelector): + """Representation of a Xiaomi generic selector.""" - def __init__(self, device, entry, unique_id, coordinator, description): - """Initialize the plug switch.""" + entity_description: XiaomiMiioSelectDescription + + def __init__(self, device, entry, unique_id, coordinator, description, enum_class): + """Initialize the generic Xiaomi attribute selector.""" super().__init__(device, entry, unique_id, coordinator, description) - self._current_led_brightness = self._extract_value_from_attribute( - self.coordinator.data, self.entity_description.key + self._current_attr = enum_class( + self._extract_value_from_attribute( + self.coordinator.data, self.entity_description.attr_name + ) ) + if description.options_map: + self._options_map = {} + for key, val in enum_class._member_map_.items(): + self._options_map[description.options_map[key]] = val + else: + self._options_map = enum_class._member_map_ + self._reverse_map = {val: key for key, val in self._options_map.items()} + self._enum_class = enum_class + @callback def _handle_coordinator_update(self): """Fetch state from the device.""" - led_brightness = self._extract_value_from_attribute( - self.coordinator.data, self.entity_description.key + attr = self._enum_class( + self._extract_value_from_attribute( + self.coordinator.data, self.entity_description.attr_name + ) ) - # Sometimes (quite rarely) the device returns None as the LED brightness so we - # check that the value is not None before updating the state. - if led_brightness is not None: - self._current_led_brightness = led_brightness + if attr is not None: + self._current_attr = attr self.async_write_ha_state() @property - def current_option(self) -> str: + def current_option(self) -> str | None: """Return the current option.""" - return self.led_brightness.lower() + option = self._reverse_map.get(self._current_attr) + if option is not None: + return option.lower() + return None async def async_select_option(self, option: str) -> None: """Set an option of the miio device.""" - await self.async_set_led_brightness(option.title()) + await self.async_set_attr(option.title()) - @property - def led_brightness(self): - """Return the current led brightness.""" - return LED_BRIGHTNESS_REVERSE_MAP.get(self._current_led_brightness) - - async def async_set_led_brightness(self, brightness: str): - """Set the led brightness.""" + async def async_set_attr(self, attr_value: str): + """Set attr.""" + method = getattr(self._device, self.entity_description.set_method) if await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - AirhumidifierLedBrightness(LED_BRIGHTNESS_MAP[brightness]), + self.entity_description.set_method_error_message, + method, + self._enum_class(self._options_map[attr_value]), ): - self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness] - self.async_write_ha_state() - - -class XiaomiAirHumidifierMiotSelector(XiaomiAirHumidifierSelector): - """Representation of a Xiaomi Air Humidifier (MiOT protocol) selector.""" - - @property - def led_brightness(self): - """Return the current led brightness.""" - return LED_BRIGHTNESS_REVERSE_MAP_HUMIDIFIER_MIOT.get( - self._current_led_brightness - ) - - async def async_set_led_brightness(self, brightness: str) -> None: - """Set the led brightness.""" - if await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - AirhumidifierMiotLedBrightness( - LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT[brightness] - ), - ): - self._current_led_brightness = LED_BRIGHTNESS_MAP_HUMIDIFIER_MIOT[ - brightness - ] - self.async_write_ha_state() - - -class XiaomiAirPurifierSelector(XiaomiAirHumidifierSelector): - """Representation of a Xiaomi Air Purifier (MIIO protocol) selector.""" - - async def async_set_led_brightness(self, brightness: str) -> None: - """Set the led brightness.""" - if await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - AirpurifierLedBrightness(LED_BRIGHTNESS_MAP[brightness]), - ): - self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness] - self.async_write_ha_state() - - -class XiaomiAirPurifierMiotSelector(XiaomiAirHumidifierSelector): - """Representation of a Xiaomi Air Purifier (MiOT protocol) selector.""" - - async def async_set_led_brightness(self, brightness: str) -> None: - """Set the led brightness.""" - if await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - AirpurifierMiotLedBrightness(LED_BRIGHTNESS_MAP[brightness]), - ): - self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness] - self.async_write_ha_state() - - -class XiaomiFanSelector(XiaomiAirHumidifierSelector): - """Representation of a Xiaomi Fan (MIIO protocol) selector.""" - - async def async_set_led_brightness(self, brightness: str) -> None: - """Set the led brightness.""" - if await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - FanLedBrightness(LED_BRIGHTNESS_MAP[brightness]), - ): - self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness] - self.async_write_ha_state() - - -class XiaomiAirFreshSelector(XiaomiAirHumidifierSelector): - """Representation of a Xiaomi Air Fresh selector.""" - - async def async_set_led_brightness(self, brightness: str) -> None: - """Set the led brightness.""" - if await self._try_command( - "Setting the led brightness of the miio device failed.", - self._device.set_led_brightness, - AirfreshLedBrightness(LED_BRIGHTNESS_MAP[brightness]), - ): - self._current_led_brightness = LED_BRIGHTNESS_MAP[brightness] + self._current_attr = self._options_map[attr_value] self.async_write_ha_state() diff --git a/homeassistant/components/xiaomi_miio/strings.select.json b/homeassistant/components/xiaomi_miio/strings.select.json index 265aec66531..38ee8b1aa07 100644 --- a/homeassistant/components/xiaomi_miio/strings.select.json +++ b/homeassistant/components/xiaomi_miio/strings.select.json @@ -4,6 +4,16 @@ "bright": "Bright", "dim": "Dim", "off": "Off" + }, + "xiaomi_miio__display_orientation": { + "forward": "Forward", + "left": "Left", + "right": "Right" + }, + "xiaomi_miio__ptc_level": { + "low": "Low", + "medium": "Medium", + "high": "High" } } } diff --git a/tests/components/xiaomi_miio/test_select.py b/tests/components/xiaomi_miio/test_select.py new file mode 100644 index 00000000000..3fa8a3de291 --- /dev/null +++ b/tests/components/xiaomi_miio/test_select.py @@ -0,0 +1,158 @@ +"""The tests for the xiaomi_miio select component.""" + +from unittest.mock import MagicMock, patch + +from arrow import utcnow +from miio.integrations.airpurifier.dmaker.airfresh_t2017 import ( + DisplayOrientation, + PtcLevel, +) +import pytest + +from homeassistant.components.select import DOMAIN +from homeassistant.components.select.const import ( + ATTR_OPTION, + ATTR_OPTIONS, + SERVICE_SELECT_OPTION, +) +from homeassistant.components.xiaomi_miio import UPDATE_INTERVAL +from homeassistant.components.xiaomi_miio.const import ( + CONF_DEVICE, + CONF_FLOW_TYPE, + CONF_MAC, + DOMAIN as XIAOMI_DOMAIN, + MODEL_AIRFRESH_T2017, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + CONF_MODEL, + CONF_TOKEN, + Platform, +) +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, async_fire_time_changed +from tests.components.xiaomi_miio import TEST_MAC + + +@pytest.fixture(autouse=True) +async def setup_test(hass: HomeAssistant): + """Initialize test xiaomi_miio for select entity.""" + + mock_airfresh = MagicMock() + mock_airfresh.status().display_orientation = DisplayOrientation.Portrait + mock_airfresh.status().ptc_level = PtcLevel.Low + + with patch( + "homeassistant.components.xiaomi_miio.get_platforms", + return_value=[ + Platform.SELECT, + ], + ), patch("homeassistant.components.xiaomi_miio.AirFreshT2017") as mock_airfresh_cls: + mock_airfresh_cls.return_value = mock_airfresh + yield mock_airfresh + + +async def test_select_params(hass: HomeAssistant) -> None: + """Test the initial parameters.""" + + entity_name = "test_airfresh_select" + entity_id = await setup_component(hass, entity_name) + + select_entity = hass.states.get(entity_id + "_display_orientation") + assert select_entity + assert select_entity.state == "forward" + assert select_entity.attributes.get(ATTR_OPTIONS) == ["forward", "left", "right"] + + +async def test_select_bad_attr(hass: HomeAssistant) -> None: + """Test selecting a different option with invalid option value.""" + + entity_name = "test_airfresh_select" + entity_id = await setup_component(hass, entity_name) + + state = hass.states.get(entity_id + "_display_orientation") + assert state + assert state.state == "forward" + + with pytest.raises(ValueError): + await hass.services.async_call( + "select", + SERVICE_SELECT_OPTION, + {ATTR_OPTION: "up", ATTR_ENTITY_ID: entity_id + "_display_orientation"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id + "_display_orientation") + assert state + assert state.state == "forward" + + +async def test_select_option(hass: HomeAssistant) -> None: + """Test selecting of a option.""" + + entity_name = "test_airfresh_select" + entity_id = await setup_component(hass, entity_name) + + state = hass.states.get(entity_id + "_display_orientation") + assert state + assert state.state == "forward" + + await hass.services.async_call( + "select", + SERVICE_SELECT_OPTION, + {ATTR_OPTION: "left", ATTR_ENTITY_ID: entity_id + "_display_orientation"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(entity_id + "_display_orientation") + assert state + assert state.state == "left" + + +async def test_select_coordinator_update(hass: HomeAssistant, setup_test) -> None: + """Test coordinator update of a option.""" + + entity_name = "test_airfresh_select" + entity_id = await setup_component(hass, entity_name) + + state = hass.states.get(entity_id + "_display_orientation") + assert state + assert state.state == "forward" + + # emulate someone change state from device maybe used app + setup_test.status().display_orientation = DisplayOrientation.LandscapeLeft + + async_fire_time_changed(hass, utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(entity_id + "_display_orientation") + assert state + assert state.state == "left" + + +async def setup_component(hass, entity_name): + """Set up component.""" + entity_id = f"{DOMAIN}.{entity_name}" + + config_entry = MockConfigEntry( + domain=XIAOMI_DOMAIN, + unique_id="123456", + title=entity_name, + data={ + CONF_FLOW_TYPE: CONF_DEVICE, + CONF_HOST: "0.0.0.0", + CONF_TOKEN: "12345678901234567890123456789012", + CONF_MODEL: MODEL_AIRFRESH_T2017, + CONF_MAC: TEST_MAC, + }, + ) + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return entity_id From 5331af214362378e600bf4d640a4983be4195768 Mon Sep 17 00:00:00 2001 From: Zach Berger Date: Tue, 16 Aug 2022 10:17:53 -0700 Subject: [PATCH 3387/3516] Capture local Awair firmware version to DeviceInfo (#76700) --- .strict-typing | 1 + homeassistant/components/awair/__init__.py | 19 +++++++++++++++---- homeassistant/components/awair/config_flow.py | 11 ++++++++--- homeassistant/components/awair/manifest.json | 2 +- homeassistant/components/awair/sensor.py | 12 ++++++++++-- mypy.ini | 10 ++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 47 insertions(+), 12 deletions(-) diff --git a/.strict-typing b/.strict-typing index db51acc07df..02d753b3994 100644 --- a/.strict-typing +++ b/.strict-typing @@ -61,6 +61,7 @@ homeassistant.components.aseko_pool_live.* homeassistant.components.asuswrt.* homeassistant.components.auth.* homeassistant.components.automation.* +homeassistant.components.awair.* homeassistant.components.backup.* homeassistant.components.baf.* homeassistant.components.binary_sensor.* diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index b0a5d39814c..359d0d6d853 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -2,7 +2,9 @@ from __future__ import annotations from asyncio import gather +from datetime import timedelta +from aiohttp import ClientSession from async_timeout import timeout from python_awair import Awair, AwairLocal from python_awair.devices import AwairBaseDevice, AwairLocalDevice @@ -63,13 +65,18 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> class AwairDataUpdateCoordinator(DataUpdateCoordinator): """Define a wrapper class to update Awair data.""" - def __init__(self, hass, config_entry, update_interval) -> None: + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + update_interval: timedelta | None, + ) -> None: """Set up the AwairDataUpdateCoordinator class.""" self._config_entry = config_entry super().__init__(hass, LOGGER, name=DOMAIN, update_interval=update_interval) - async def _fetch_air_data(self, device: AwairBaseDevice): + async def _fetch_air_data(self, device: AwairBaseDevice) -> AwairResult: """Fetch latest air quality data.""" LOGGER.debug("Fetching data for %s", device.uuid) air_data = await device.air_data_latest() @@ -80,7 +87,9 @@ class AwairDataUpdateCoordinator(DataUpdateCoordinator): class AwairCloudDataUpdateCoordinator(AwairDataUpdateCoordinator): """Define a wrapper class to update Awair data from Cloud API.""" - def __init__(self, hass, config_entry, session) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, session: ClientSession + ) -> None: """Set up the AwairCloudDataUpdateCoordinator class.""" access_token = config_entry.data[CONF_ACCESS_TOKEN] self._awair = Awair(access_token=access_token, session=session) @@ -109,7 +118,9 @@ class AwairLocalDataUpdateCoordinator(AwairDataUpdateCoordinator): _device: AwairLocalDevice | None = None - def __init__(self, hass, config_entry, session) -> None: + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, session: ClientSession + ) -> None: """Set up the AwairLocalDataUpdateCoordinator class.""" self._awair = AwairLocal( session=session, device_addrs=[config_entry.data[CONF_HOST]] diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index 418413b690f..becf6ce46ff 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -7,6 +7,7 @@ from typing import Any from aiohttp.client_exceptions import ClientError from python_awair import Awair, AwairLocal, AwairLocalDevice from python_awair.exceptions import AuthError, AwairError +from python_awair.user import AwairUser import voluptuous as vol from homeassistant.components import zeroconf @@ -97,7 +98,7 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): title = user.email return self.async_create_entry(title=title, data=user_input) - if error != "invalid_access_token": + if error and error != "invalid_access_token": return self.async_abort(reason=error) errors = {CONF_ACCESS_TOKEN: "invalid_access_token"} @@ -215,7 +216,9 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def _check_local_connection(self, device_address: str): + async def _check_local_connection( + self, device_address: str + ) -> tuple[AwairLocalDevice | None, str | None]: """Check the access token is valid.""" session = async_get_clientsession(self.hass) awair = AwairLocal(session=session, device_addrs=[device_address]) @@ -232,7 +235,9 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): LOGGER.error("Unexpected API error: %s", err) return (None, "unknown") - async def _check_cloud_connection(self, access_token: str): + async def _check_cloud_connection( + self, access_token: str + ) -> tuple[AwairUser | None, str | None]: """Check the access token is valid.""" session = async_get_clientsession(self.hass) awair = Awair(access_token=access_token, session=session) diff --git a/homeassistant/components/awair/manifest.json b/homeassistant/components/awair/manifest.json index 131a955a6eb..f09d9c2ee33 100644 --- a/homeassistant/components/awair/manifest.json +++ b/homeassistant/components/awair/manifest.json @@ -2,7 +2,7 @@ "domain": "awair", "name": "Awair", "documentation": "https://www.home-assistant.io/integrations/awair", - "requirements": ["python_awair==0.2.3"], + "requirements": ["python_awair==0.2.4"], "codeowners": ["@ahayworth", "@danielsjf"], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index cda7f31095e..00d5c929409 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -2,11 +2,16 @@ from __future__ import annotations from python_awair.air_data import AirData -from python_awair.devices import AwairBaseDevice +from python_awair.devices import AwairBaseDevice, AwairLocalDevice from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_CONNECTIONS, ATTR_NAME +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_CONNECTIONS, + ATTR_NAME, + ATTR_SW_VERSION, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo @@ -209,6 +214,9 @@ class AwairSensor(CoordinatorEntity[AwairDataUpdateCoordinator], SensorEntity): (dr.CONNECTION_NETWORK_MAC, self._device.mac_address) } + if isinstance(self._device, AwairLocalDevice): + info[ATTR_SW_VERSION] = self._device.fw_version + return info @property diff --git a/mypy.ini b/mypy.ini index 5d3b184880d..2645cb9d107 100644 --- a/mypy.ini +++ b/mypy.ini @@ -369,6 +369,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.awair.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.backup.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 8a19ae220d3..7a372461d3e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1981,7 +1981,7 @@ python-telegram-bot==13.1 python-vlc==1.1.2 # homeassistant.components.awair -python_awair==0.2.3 +python_awair==0.2.4 # homeassistant.components.swiss_public_transport python_opendata_transport==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9e04359182a..27f54b09389 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1347,7 +1347,7 @@ python-tado==0.12.0 python-telegram-bot==13.1 # homeassistant.components.awair -python_awair==0.2.3 +python_awair==0.2.4 # homeassistant.components.tile pytile==2022.02.0 From 2630ff8fce6bf4b36082b29af907c3bca4d6b642 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 16 Aug 2022 19:22:15 +0200 Subject: [PATCH 3388/3516] Add sensor checks to pylint plugin (#76876) --- pylint/plugins/hass_enforce_type_hints.py | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 35862742b18..e9147fcca70 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1863,6 +1863,46 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "sensor": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="SensorEntity", + matches=[ + TypeHintMatch( + function_name="device_class", + return_type=["SensorDeviceClass", "str", None], + ), + TypeHintMatch( + function_name="state_class", + return_type=["SensorStateClass", "str", None], + ), + TypeHintMatch( + function_name="last_reset", + return_type=["datetime", None], + ), + TypeHintMatch( + function_name="native_value", + return_type=[ + "StateType", + "str", + "int", + "float", + None, + "date", + "datetime", + "Decimal", + ], + ), + TypeHintMatch( + function_name="native_unit_of_measurement", + return_type=["str", None], + ), + ], + ), + ], "siren": [ ClassTypeHintMatch( base_class="Entity", From 5736ba62309debe36781209760015dab1fbbca8b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 16 Aug 2022 19:24:00 +0200 Subject: [PATCH 3389/3516] Add remote checks to pylint plugin (#76875) --- pylint/plugins/hass_enforce_type_hints.py | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index e9147fcca70..b34b88c27c9 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1830,6 +1830,52 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "remote": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="ToggleEntity", + matches=_TOGGLE_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="RemoteEntity", + matches=[ + TypeHintMatch( + function_name="supported_features", + return_type="int", + ), + TypeHintMatch( + function_name="current_activity", + return_type=["str", None], + ), + TypeHintMatch( + function_name="activity_list", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="send_command", + arg_types={1: "Iterable[str]"}, + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="learn_command", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="delete_command", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ], "select": [ ClassTypeHintMatch( base_class="Entity", From 73ad34244e6e1ff52e735989a7cba412494facd8 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Tue, 16 Aug 2022 19:49:49 +0200 Subject: [PATCH 3390/3516] Bump pynetgear to 0.10.7 (#76754) --- homeassistant/components/netgear/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index 5fd59faac83..69a21e5aace 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,7 +2,7 @@ "domain": "netgear", "name": "NETGEAR", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": ["pynetgear==0.10.6"], + "requirements": ["pynetgear==0.10.7"], "codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"], "iot_class": "local_polling", "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index 7a372461d3e..8b2e2349715 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1692,7 +1692,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.10.6 +pynetgear==0.10.7 # homeassistant.components.netio pynetio==0.1.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27f54b09389..b5cf5bccb54 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1175,7 +1175,7 @@ pymyq==3.1.4 pymysensors==0.22.1 # homeassistant.components.netgear -pynetgear==0.10.6 +pynetgear==0.10.7 # homeassistant.components.nina pynina==0.1.8 From 1e9ede25ad253bc42bfd764435a9d37bd4fd3a80 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 16 Aug 2022 14:08:35 -0400 Subject: [PATCH 3391/3516] Add Fully Kiosk Browser integration with initial binary sensor platform (#76737) Co-authored-by: Franck Nijhof --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/fully_kiosk/__init__.py | 31 ++++ .../components/fully_kiosk/binary_sensor.py | 73 ++++++++++ .../components/fully_kiosk/config_flow.py | 63 ++++++++ homeassistant/components/fully_kiosk/const.py | 13 ++ .../components/fully_kiosk/coordinator.py | 48 +++++++ .../components/fully_kiosk/entity.py | 26 ++++ .../components/fully_kiosk/manifest.json | 10 ++ .../components/fully_kiosk/strings.json | 20 +++ .../fully_kiosk/translations/en.json | 20 +++ homeassistant/generated/config_flows.py | 1 + mypy.ini | 10 ++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/fully_kiosk/__init__.py | 1 + tests/components/fully_kiosk/conftest.py | 76 ++++++++++ .../fully_kiosk/fixtures/deviceinfo.json | 79 ++++++++++ .../fully_kiosk/test_binary_sensor.py | 93 ++++++++++++ .../fully_kiosk/test_config_flow.py | 136 ++++++++++++++++++ tests/components/fully_kiosk/test_init.py | 53 +++++++ 21 files changed, 762 insertions(+) create mode 100644 homeassistant/components/fully_kiosk/__init__.py create mode 100644 homeassistant/components/fully_kiosk/binary_sensor.py create mode 100644 homeassistant/components/fully_kiosk/config_flow.py create mode 100644 homeassistant/components/fully_kiosk/const.py create mode 100644 homeassistant/components/fully_kiosk/coordinator.py create mode 100644 homeassistant/components/fully_kiosk/entity.py create mode 100644 homeassistant/components/fully_kiosk/manifest.json create mode 100644 homeassistant/components/fully_kiosk/strings.json create mode 100644 homeassistant/components/fully_kiosk/translations/en.json create mode 100644 tests/components/fully_kiosk/__init__.py create mode 100644 tests/components/fully_kiosk/conftest.py create mode 100644 tests/components/fully_kiosk/fixtures/deviceinfo.json create mode 100644 tests/components/fully_kiosk/test_binary_sensor.py create mode 100644 tests/components/fully_kiosk/test_config_flow.py create mode 100644 tests/components/fully_kiosk/test_init.py diff --git a/.strict-typing b/.strict-typing index 02d753b3994..d9cc4ffb55a 100644 --- a/.strict-typing +++ b/.strict-typing @@ -108,6 +108,7 @@ homeassistant.components.fritzbox_callmonitor.* homeassistant.components.fronius.* homeassistant.components.frontend.* homeassistant.components.fritz.* +homeassistant.components.fully_kiosk.* homeassistant.components.geo_location.* homeassistant.components.geocaching.* homeassistant.components.gios.* diff --git a/CODEOWNERS b/CODEOWNERS index 3ac50eeb1db..0c8aa94dfb8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -375,6 +375,8 @@ build.json @home-assistant/supervisor /homeassistant/components/frontend/ @home-assistant/frontend /tests/components/frontend/ @home-assistant/frontend /homeassistant/components/frontier_silicon/ @wlcrs +/homeassistant/components/fully_kiosk/ @cgarwood +/tests/components/fully_kiosk/ @cgarwood /homeassistant/components/garages_amsterdam/ @klaasnicolaas /tests/components/garages_amsterdam/ @klaasnicolaas /homeassistant/components/gdacs/ @exxamalte diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py new file mode 100644 index 00000000000..943f5c69cbe --- /dev/null +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -0,0 +1,31 @@ +"""The Fully Kiosk Browser integration.""" +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import FullyKioskDataUpdateCoordinator + +PLATFORMS = [Platform.BINARY_SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Fully Kiosk Browser from a config entry.""" + + coordinator = FullyKioskDataUpdateCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/fully_kiosk/binary_sensor.py b/homeassistant/components/fully_kiosk/binary_sensor.py new file mode 100644 index 00000000000..6f1fccfb9d3 --- /dev/null +++ b/homeassistant/components/fully_kiosk/binary_sensor.py @@ -0,0 +1,73 @@ +"""Fully Kiosk Browser sensor.""" +from __future__ import annotations + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import FullyKioskDataUpdateCoordinator +from .entity import FullyKioskEntity + +SENSORS: tuple[BinarySensorEntityDescription, ...] = ( + BinarySensorEntityDescription( + key="kioskMode", + name="Kiosk mode", + entity_category=EntityCategory.DIAGNOSTIC, + ), + BinarySensorEntityDescription( + key="plugged", + name="Plugged in", + device_class=BinarySensorDeviceClass.PLUG, + entity_category=EntityCategory.DIAGNOSTIC, + ), + BinarySensorEntityDescription( + key="isDeviceAdmin", + name="Device admin", + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Fully Kiosk Browser sensor.""" + coordinator: FullyKioskDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + async_add_entities( + FullyBinarySensor(coordinator, description) + for description in SENSORS + if description.key in coordinator.data + ) + + +class FullyBinarySensor(FullyKioskEntity, BinarySensorEntity): + """Representation of a Fully Kiosk Browser binary sensor.""" + + def __init__( + self, + coordinator: FullyKioskDataUpdateCoordinator, + description: BinarySensorEntityDescription, + ) -> None: + """Initialize the binary sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.data['deviceID']}-{description.key}" + + @property + def is_on(self) -> bool | None: + """Return if the binary sensor is on.""" + if (value := self.coordinator.data.get(self.entity_description.key)) is None: + return None + return bool(value) diff --git a/homeassistant/components/fully_kiosk/config_flow.py b/homeassistant/components/fully_kiosk/config_flow.py new file mode 100644 index 00000000000..09eb94d6b07 --- /dev/null +++ b/homeassistant/components/fully_kiosk/config_flow.py @@ -0,0 +1,63 @@ +"""Config flow for Fully Kiosk Browser integration.""" +from __future__ import annotations + +import asyncio +from typing import Any + +from aiohttp.client_exceptions import ClientConnectorError +from async_timeout import timeout +from fullykiosk import FullyKiosk +from fullykiosk.exceptions import FullyKioskError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DEFAULT_PORT, DOMAIN, LOGGER + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Fully Kiosk Browser.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + fully = FullyKiosk( + async_get_clientsession(self.hass), + user_input[CONF_HOST], + DEFAULT_PORT, + user_input[CONF_PASSWORD], + ) + + try: + async with timeout(15): + device_info = await fully.getDeviceInfo() + except (ClientConnectorError, FullyKioskError, asyncio.TimeoutError): + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(device_info["deviceID"]) + self._abort_if_unique_id_configured(updates=user_input) + return self.async_create_entry( + title=device_info["deviceName"], data=user_input + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/fully_kiosk/const.py b/homeassistant/components/fully_kiosk/const.py new file mode 100644 index 00000000000..f21906bae73 --- /dev/null +++ b/homeassistant/components/fully_kiosk/const.py @@ -0,0 +1,13 @@ +"""Constants for the Fully Kiosk Browser integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Final + +DOMAIN: Final = "fully_kiosk" + +LOGGER = logging.getLogger(__package__) +UPDATE_INTERVAL = timedelta(seconds=30) + +DEFAULT_PORT = 2323 diff --git a/homeassistant/components/fully_kiosk/coordinator.py b/homeassistant/components/fully_kiosk/coordinator.py new file mode 100644 index 00000000000..fbd08f8d2c5 --- /dev/null +++ b/homeassistant/components/fully_kiosk/coordinator.py @@ -0,0 +1,48 @@ +"""Provides the The Fully Kiosk Browser DataUpdateCoordinator.""" +import asyncio +from typing import Any, cast + +from async_timeout import timeout +from fullykiosk import FullyKiosk +from fullykiosk.exceptions import FullyKioskError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DEFAULT_PORT, LOGGER, UPDATE_INTERVAL + + +class FullyKioskDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to hold Fully Kiosk Browser data.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize.""" + self.fully = FullyKiosk( + async_get_clientsession(hass), + entry.data[CONF_HOST], + DEFAULT_PORT, + entry.data[CONF_PASSWORD], + ) + super().__init__( + hass, + LOGGER, + name=entry.data[CONF_HOST], + update_interval=UPDATE_INTERVAL, + ) + + async def _async_update_data(self) -> dict[str, Any]: + """Update data via library.""" + try: + async with timeout(15): + # Get device info and settings in parallel + result = await asyncio.gather( + self.fully.getDeviceInfo(), self.fully.getSettings() + ) + # Store settings under settings key in data + result[0]["settings"] = result[1] + return cast(dict[str, Any], result[0]) + except FullyKioskError as error: + raise UpdateFailed(error) from error diff --git a/homeassistant/components/fully_kiosk/entity.py b/homeassistant/components/fully_kiosk/entity.py new file mode 100644 index 00000000000..4e50bb6efe6 --- /dev/null +++ b/homeassistant/components/fully_kiosk/entity.py @@ -0,0 +1,26 @@ +"""Base entity for the Fully Kiosk Browser integration.""" +from __future__ import annotations + +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import FullyKioskDataUpdateCoordinator + + +class FullyKioskEntity(CoordinatorEntity[FullyKioskDataUpdateCoordinator], Entity): + """Defines a Fully Kiosk Browser entity.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: FullyKioskDataUpdateCoordinator) -> None: + """Initialize the Fully Kiosk Browser entity.""" + super().__init__(coordinator=coordinator) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.data["deviceID"])}, + name=coordinator.data["deviceName"], + manufacturer=coordinator.data["deviceManufacturer"], + model=coordinator.data["deviceModel"], + sw_version=coordinator.data["appVersionName"], + configuration_url=f"http://{coordinator.data['ip4']}:2323", + ) diff --git a/homeassistant/components/fully_kiosk/manifest.json b/homeassistant/components/fully_kiosk/manifest.json new file mode 100644 index 00000000000..40c7e5293e7 --- /dev/null +++ b/homeassistant/components/fully_kiosk/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "fully_kiosk", + "name": "Fully Kiosk Browser", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/fullykiosk", + "requirements": ["python-fullykiosk==0.0.11"], + "dependencies": [], + "codeowners": ["@cgarwood"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/fully_kiosk/strings.json b/homeassistant/components/fully_kiosk/strings.json new file mode 100644 index 00000000000..05b9e067962 --- /dev/null +++ b/homeassistant/components/fully_kiosk/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + } + } +} diff --git a/homeassistant/components/fully_kiosk/translations/en.json b/homeassistant/components/fully_kiosk/translations/en.json new file mode 100644 index 00000000000..338c50514fb --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ce95cb66cc5..8b5db1b45ba 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -125,6 +125,7 @@ FLOWS = { "fritzbox", "fritzbox_callmonitor", "fronius", + "fully_kiosk", "garages_amsterdam", "gdacs", "generic", diff --git a/mypy.ini b/mypy.ini index 2645cb9d107..570004a14dd 100644 --- a/mypy.ini +++ b/mypy.ini @@ -839,6 +839,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.fully_kiosk.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.geo_location.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 8b2e2349715..dfae2e4afd9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1914,6 +1914,9 @@ python-family-hub-local==0.0.2 # homeassistant.components.darksky python-forecastio==1.4.0 +# homeassistant.components.fully_kiosk +python-fullykiosk==0.0.11 + # homeassistant.components.sms # python-gammu==3.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b5cf5bccb54..0623b6c7c79 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1313,6 +1313,9 @@ python-ecobee-api==0.2.14 # homeassistant.components.darksky python-forecastio==1.4.0 +# homeassistant.components.fully_kiosk +python-fullykiosk==0.0.11 + # homeassistant.components.homewizard python-homewizard-energy==1.1.0 diff --git a/tests/components/fully_kiosk/__init__.py b/tests/components/fully_kiosk/__init__.py new file mode 100644 index 00000000000..7cdc13ace56 --- /dev/null +++ b/tests/components/fully_kiosk/__init__.py @@ -0,0 +1 @@ +"""Tests for the Fully Kiosk Browser integration.""" diff --git a/tests/components/fully_kiosk/conftest.py b/tests/components/fully_kiosk/conftest.py new file mode 100644 index 00000000000..c5476ea6a9d --- /dev/null +++ b/tests/components/fully_kiosk/conftest.py @@ -0,0 +1,76 @@ +"""Fixtures for the Fully Kiosk Browser integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +import json +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant.components.fully_kiosk.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="Test device", + domain=DOMAIN, + data={CONF_HOST: "127.0.0.1", CONF_PASSWORD: "mocked-password"}, + unique_id="12345", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.fully_kiosk.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup + + +@pytest.fixture +def mock_fully_kiosk_config_flow() -> Generator[MagicMock, None, None]: + """Return a mocked Fully Kiosk client for the config flow.""" + with patch( + "homeassistant.components.fully_kiosk.config_flow.FullyKiosk", + autospec=True, + ) as client_mock: + client = client_mock.return_value + client.getDeviceInfo.return_value = { + "deviceName": "Test device", + "deviceID": "12345", + } + yield client + + +@pytest.fixture +def mock_fully_kiosk() -> Generator[MagicMock, None, None]: + """Return a mocked Fully Kiosk client.""" + with patch( + "homeassistant.components.fully_kiosk.coordinator.FullyKiosk", + autospec=True, + ) as client_mock: + client = client_mock.return_value + client.getDeviceInfo.return_value = json.loads( + load_fixture("deviceinfo.json", DOMAIN) + ) + yield client + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_fully_kiosk: MagicMock +) -> MockConfigEntry: + """Set up the Fully Kiosk Browser integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/fully_kiosk/fixtures/deviceinfo.json b/tests/components/fully_kiosk/fixtures/deviceinfo.json new file mode 100644 index 00000000000..0a530dc35c8 --- /dev/null +++ b/tests/components/fully_kiosk/fixtures/deviceinfo.json @@ -0,0 +1,79 @@ +{ + "deviceName": "Amazon Fire", + "batteryLevel": 100, + "isPlugged": true, + "SSID": "\"freewifi\"", + "Mac": "aa:bb:cc:dd:ee:ff", + "ip4": "192.168.1.234", + "ip6": "FE80::1874:2EFF:FEA2:7848", + "hostname4": "192.168.1.234", + "hostname6": "fe80::1874:2eff:fea2:7848%p2p0", + "wifiSignalLevel": 7, + "isMobileDataEnabled": true, + "screenOrientation": 90, + "screenBrightness": 9, + "screenLocked": false, + "screenOn": true, + "batteryTemperature": 27, + "plugged": true, + "keyguardLocked": false, + "locale": "en_US", + "serial": "ABCDEF1234567890", + "build": "cm_douglas-userdebug 5.1.1 LMY49M 731a881f9d test-keys", + "androidVersion": "5.1.1", + "webviewUA": "Mozilla/5.0 (Linux; Android 5.1.1; KFDOWI Build/LMY49M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/81.0.4044.117 Safari/537.36", + "motionDetectorStatus": 0, + "isDeviceAdmin": true, + "isDeviceOwner": false, + "internalStorageFreeSpace": 11675512832, + "internalStorageTotalSpace": 12938534912, + "ramUsedMemory": 1077755904, + "ramFreeMemory": 362373120, + "ramTotalMemory": 1440129024, + "appUsedMemory": 24720592, + "appFreeMemory": 59165440, + "appTotalMemory": 83886080, + "displayHeightPixels": 800, + "displayWidthPixels": 1280, + "isMenuOpen": false, + "topFragmentTag": "", + "isInDaydream": false, + "isRooted": true, + "isLicensed": true, + "isInScreensaver": false, + "kioskLocked": true, + "isInForcedSleep": false, + "maintenanceMode": false, + "kioskMode": true, + "startUrl": "https://homeassistant.local", + "currentTabIndex": 0, + "mqttConnected": true, + "deviceID": "abcdef-123456", + "appVersionCode": 875, + "appVersionName": "1.42.5", + "androidSdk": 22, + "deviceModel": "KFDOWI", + "deviceManufacturer": "amzn", + "foregroundApp": "de.ozerov.fully", + "currentPage": "https://homeassistant.local", + "lastAppStart": "8/13/2022 1:00:47 AM", + "sensorInfo": [ + { + "type": 8, + "name": "PROXIMITY", + "vendor": "MTK", + "version": 1, + "accuracy": -1 + }, + { + "type": 5, + "name": "LIGHT", + "vendor": "MTK", + "version": 1, + "accuracy": 3, + "values": [0, 0, 0], + "lastValuesTime": 1660435566561, + "lastAccuracyTime": 1660366847543 + } + ] +} diff --git a/tests/components/fully_kiosk/test_binary_sensor.py b/tests/components/fully_kiosk/test_binary_sensor.py new file mode 100644 index 00000000000..3583a66b8e7 --- /dev/null +++ b/tests/components/fully_kiosk/test_binary_sensor.py @@ -0,0 +1,93 @@ +"""Test the Fully Kiosk Browser binary sensors.""" +from unittest.mock import MagicMock + +from fullykiosk import FullyKioskError + +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.components.fully_kiosk.const import DOMAIN, UPDATE_INTERVAL +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory +from homeassistant.util import dt + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_binary_sensors( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test standard Fully Kiosk binary sensors.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("binary_sensor.amazon_fire_plugged_in") + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.PLUG + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Plugged in" + + entry = entity_registry.async_get("binary_sensor.amazon_fire_plugged_in") + assert entry + assert entry.unique_id == "abcdef-123456-plugged" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + state = hass.states.get("binary_sensor.amazon_fire_kiosk_mode") + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Kiosk mode" + + entry = entity_registry.async_get("binary_sensor.amazon_fire_kiosk_mode") + assert entry + assert entry.unique_id == "abcdef-123456-kioskMode" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + state = hass.states.get("binary_sensor.amazon_fire_device_admin") + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Device admin" + + entry = entity_registry.async_get("binary_sensor.amazon_fire_device_admin") + assert entry + assert entry.unique_id == "abcdef-123456-isDeviceAdmin" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.configuration_url == "http://192.168.1.234:2323" + assert device_entry.entry_type is None + assert device_entry.hw_version is None + assert device_entry.identifiers == {(DOMAIN, "abcdef-123456")} + assert device_entry.manufacturer == "amzn" + assert device_entry.model == "KFDOWI" + assert device_entry.name == "Amazon Fire" + assert device_entry.sw_version == "1.42.5" + + # Test unknown/missing data + mock_fully_kiosk.getDeviceInfo.return_value = {} + async_fire_time_changed(hass, dt.utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.amazon_fire_plugged_in") + assert state + assert state.state == STATE_UNKNOWN + + # Test failed update + mock_fully_kiosk.getDeviceInfo.side_effect = FullyKioskError("error", "status") + async_fire_time_changed(hass, dt.utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.amazon_fire_plugged_in") + assert state + assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/fully_kiosk/test_config_flow.py b/tests/components/fully_kiosk/test_config_flow.py new file mode 100644 index 00000000000..2617a3f7adb --- /dev/null +++ b/tests/components/fully_kiosk/test_config_flow.py @@ -0,0 +1,136 @@ +"""Test the Fully Kiosk Browser config flow.""" + +import asyncio +from unittest.mock import AsyncMock, MagicMock, Mock + +from aiohttp.client_exceptions import ClientConnectorError +from fullykiosk import FullyKioskError +import pytest + +from homeassistant.components.fully_kiosk.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_full_flow( + hass: HomeAssistant, + mock_fully_kiosk_config_flow: MagicMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test the full user initiated config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("title") == "Test device" + assert result2.get("data") == { + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test-password", + } + assert "result" in result2 + assert result2["result"].unique_id == "12345" + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_fully_kiosk_config_flow.getDeviceInfo.mock_calls) == 1 + + +@pytest.mark.parametrize( + "side_effect,reason", + [ + (FullyKioskError("error", "status"), "cannot_connect"), + (ClientConnectorError(None, Mock()), "cannot_connect"), + (asyncio.TimeoutError, "cannot_connect"), + (RuntimeError, "unknown"), + ], +) +async def test_errors( + hass: HomeAssistant, + mock_fully_kiosk_config_flow: MagicMock, + mock_setup_entry: AsyncMock, + side_effect: Exception, + reason: str, +) -> None: + """Test errors raised during flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert "flow_id" in result + flow_id = result["flow_id"] + + mock_fully_kiosk_config_flow.getDeviceInfo.side_effect = side_effect + result2 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password"} + ) + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "user" + assert result2.get("errors") == {"base": reason} + + assert len(mock_fully_kiosk_config_flow.getDeviceInfo.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 0 + + mock_fully_kiosk_config_flow.getDeviceInfo.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password"} + ) + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "Test device" + assert result3.get("data") == { + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test-password", + } + assert "result" in result3 + assert result3["result"].unique_id == "12345" + + assert len(mock_fully_kiosk_config_flow.getDeviceInfo.mock_calls) == 2 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_duplicate_updates_existing_entry( + hass: HomeAssistant, + mock_fully_kiosk_config_flow: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test adding existing device updates existing entry.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2.get("type") == FlowResultType.ABORT + assert result2.get("reason") == "already_configured" + assert mock_config_entry.data == { + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "test-password", + } + + assert len(mock_fully_kiosk_config_flow.getDeviceInfo.mock_calls) == 1 diff --git a/tests/components/fully_kiosk/test_init.py b/tests/components/fully_kiosk/test_init.py new file mode 100644 index 00000000000..5960873e124 --- /dev/null +++ b/tests/components/fully_kiosk/test_init.py @@ -0,0 +1,53 @@ +"""Tests for the Fully Kiosk Browser integration.""" +import asyncio +from unittest.mock import MagicMock + +from fullykiosk import FullyKioskError +import pytest + +from homeassistant.components.fully_kiosk.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_fully_kiosk: MagicMock, +) -> None: + """Test the Fully Kiosk Browser configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + assert len(mock_fully_kiosk.getDeviceInfo.mock_calls) == 1 + assert len(mock_fully_kiosk.getSettings.mock_calls) == 1 + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + "side_effect", + [FullyKioskError("error", "status"), asyncio.TimeoutError], +) +async def test_config_entry_not_ready( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_fully_kiosk: MagicMock, + side_effect: Exception, +) -> None: + """Test the Fully Kiosk Browser configuration entry not ready.""" + mock_fully_kiosk.getDeviceInfo.side_effect = side_effect + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY From cb2799bc37872d179f7fa8b5bc6dbf81bd75828f Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Tue, 16 Aug 2022 21:36:33 +0200 Subject: [PATCH 3392/3516] Fix displayed units for BMW Connected Drive (#76613) * Fix displayed units * Add tests for unit conversion * Streamline test config entry init * Refactor test to pytest fixture * Fix renamed mock Co-authored-by: rikroe --- .../components/bmw_connected_drive/sensor.py | 40 ++-- .../bmw_connected_drive/__init__.py | 86 ++++++++ .../bmw_connected_drive/conftest.py | 12 + .../I01/state_WBY00000000REXI01_0.json | 206 ++++++++++++++++++ .../vehicles/I01/vehicles_v2_bmw_0.json | 47 ++++ .../bmw_connected_drive/test_config_flow.py | 8 +- .../bmw_connected_drive/test_sensor.py | 52 +++++ 7 files changed, 420 insertions(+), 31 deletions(-) create mode 100644 tests/components/bmw_connected_drive/conftest.py create mode 100644 tests/components/bmw_connected_drive/fixtures/vehicles/I01/state_WBY00000000REXI01_0.json create mode 100644 tests/components/bmw_connected_drive/fixtures/vehicles/I01/vehicles_v2_bmw_0.json create mode 100644 tests/components/bmw_connected_drive/test_sensor.py diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 26fbe19b5b1..ae3dc0bb8b9 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -15,13 +15,7 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - LENGTH_KILOMETERS, - LENGTH_MILES, - PERCENTAGE, - VOLUME_GALLONS, - VOLUME_LITERS, -) +from homeassistant.const import LENGTH, PERCENTAGE, VOLUME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -39,8 +33,7 @@ class BMWSensorEntityDescription(SensorEntityDescription): """Describes BMW sensor entity.""" key_class: str | None = None - unit_metric: str | None = None - unit_imperial: str | None = None + unit_type: str | None = None value: Callable = lambda x, y: x @@ -81,56 +74,49 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = { "remaining_battery_percent": BMWSensorEntityDescription( key="remaining_battery_percent", key_class="fuel_and_battery", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, + unit_type=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, ), # --- Specific --- "mileage": BMWSensorEntityDescription( key="mileage", icon="mdi:speedometer", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, + unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), ), "remaining_range_total": BMWSensorEntityDescription( key="remaining_range_total", key_class="fuel_and_battery", icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, + unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), ), "remaining_range_electric": BMWSensorEntityDescription( key="remaining_range_electric", key_class="fuel_and_battery", icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, + unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), ), "remaining_range_fuel": BMWSensorEntityDescription( key="remaining_range_fuel", key_class="fuel_and_battery", icon="mdi:map-marker-distance", - unit_metric=LENGTH_KILOMETERS, - unit_imperial=LENGTH_MILES, + unit_type=LENGTH, value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2), ), "remaining_fuel": BMWSensorEntityDescription( key="remaining_fuel", key_class="fuel_and_battery", icon="mdi:gas-station", - unit_metric=VOLUME_LITERS, - unit_imperial=VOLUME_GALLONS, + unit_type=VOLUME, value=lambda x, hass: convert_and_round(x, hass.config.units.volume, 2), ), "remaining_fuel_percent": BMWSensorEntityDescription( key="remaining_fuel_percent", key_class="fuel_and_battery", icon="mdi:gas-station", - unit_metric=PERCENTAGE, - unit_imperial=PERCENTAGE, + unit_type=PERCENTAGE, ), } @@ -177,8 +163,12 @@ class BMWSensor(BMWBaseEntity, SensorEntity): self._attr_name = f"{vehicle.name} {description.key}" self._attr_unique_id = f"{vehicle.vin}-{description.key}" - # Force metric system as BMW API apparently only returns metric values now - self._attr_native_unit_of_measurement = description.unit_metric + # Set the correct unit of measurement based on the unit_type + if description.unit_type: + self._attr_native_unit_of_measurement = ( + coordinator.hass.config.units.as_dict().get(description.unit_type) + or description.unit_type + ) @callback def _handle_coordinator_update(self) -> None: diff --git a/tests/components/bmw_connected_drive/__init__.py b/tests/components/bmw_connected_drive/__init__.py index 4774032b409..c2bb65b3fa7 100644 --- a/tests/components/bmw_connected_drive/__init__.py +++ b/tests/components/bmw_connected_drive/__init__.py @@ -1,17 +1,29 @@ """Tests for the for the BMW Connected Drive integration.""" +import json +from pathlib import Path + +from bimmer_connected.account import MyBMWAccount +from bimmer_connected.api.utils import log_to_to_file + from homeassistant import config_entries from homeassistant.components.bmw_connected_drive.const import ( CONF_READ_ONLY, + CONF_REFRESH_TOKEN, DOMAIN as BMW_DOMAIN, ) from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow + +from tests.common import MockConfigEntry, get_fixture_path, load_fixture FIXTURE_USER_INPUT = { CONF_USERNAME: "user@domain.com", CONF_PASSWORD: "p4ssw0rd", CONF_REGION: "rest_of_world", } +FIXTURE_REFRESH_TOKEN = "SOME_REFRESH_TOKEN" FIXTURE_CONFIG_ENTRY = { "entry_id": "1", @@ -21,8 +33,82 @@ FIXTURE_CONFIG_ENTRY = { CONF_USERNAME: FIXTURE_USER_INPUT[CONF_USERNAME], CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD], CONF_REGION: FIXTURE_USER_INPUT[CONF_REGION], + CONF_REFRESH_TOKEN: FIXTURE_REFRESH_TOKEN, }, "options": {CONF_READ_ONLY: False}, "source": config_entries.SOURCE_USER, "unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}", } + + +async def mock_vehicles_from_fixture(account: MyBMWAccount) -> None: + """Load MyBMWVehicle from fixtures and add them to the account.""" + + fixture_path = Path(get_fixture_path("", integration=BMW_DOMAIN)) + + fixture_vehicles_bmw = list(fixture_path.rglob("vehicles_v2_bmw_*.json")) + fixture_vehicles_mini = list(fixture_path.rglob("vehicles_v2_mini_*.json")) + + # Load vehicle base lists as provided by vehicles/v2 API + vehicles = { + "bmw": [ + vehicle + for bmw_file in fixture_vehicles_bmw + for vehicle in json.loads(load_fixture(bmw_file, integration=BMW_DOMAIN)) + ], + "mini": [ + vehicle + for mini_file in fixture_vehicles_mini + for vehicle in json.loads(load_fixture(mini_file, integration=BMW_DOMAIN)) + ], + } + fetched_at = utcnow() + + # simulate storing fingerprints + if account.config.log_response_path: + for brand in ["bmw", "mini"]: + log_to_to_file( + json.dumps(vehicles[brand]), + account.config.log_response_path, + f"vehicles_v2_{brand}", + ) + + # Create a vehicle with base + specific state as provided by state/VIN API + for vehicle_base in [vehicle for brand in vehicles.values() for vehicle in brand]: + vehicle_state_path = ( + Path("vehicles") + / vehicle_base["attributes"]["bodyType"] + / f"state_{vehicle_base['vin']}_0.json" + ) + vehicle_state = json.loads( + load_fixture( + vehicle_state_path, + integration=BMW_DOMAIN, + ) + ) + + account.add_vehicle( + vehicle_base, + vehicle_state, + fetched_at, + ) + + # simulate storing fingerprints + if account.config.log_response_path: + log_to_to_file( + json.dumps(vehicle_state), + account.config.log_response_path, + f"state_{vehicle_base['vin']}", + ) + + +async def setup_mocked_integration(hass: HomeAssistant) -> MockConfigEntry: + """Mock a fully setup config entry and all components based on fixtures.""" + + mock_config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/bmw_connected_drive/conftest.py b/tests/components/bmw_connected_drive/conftest.py new file mode 100644 index 00000000000..bf9d32ed9fa --- /dev/null +++ b/tests/components/bmw_connected_drive/conftest.py @@ -0,0 +1,12 @@ +"""Fixtures for BMW tests.""" + +from bimmer_connected.account import MyBMWAccount +import pytest + +from . import mock_vehicles_from_fixture + + +@pytest.fixture +async def bmw_fixture(monkeypatch): + """Patch the vehicle fixtures into a MyBMWAccount.""" + monkeypatch.setattr(MyBMWAccount, "get_vehicles", mock_vehicles_from_fixture) diff --git a/tests/components/bmw_connected_drive/fixtures/vehicles/I01/state_WBY00000000REXI01_0.json b/tests/components/bmw_connected_drive/fixtures/vehicles/I01/state_WBY00000000REXI01_0.json new file mode 100644 index 00000000000..adc2bde3650 --- /dev/null +++ b/tests/components/bmw_connected_drive/fixtures/vehicles/I01/state_WBY00000000REXI01_0.json @@ -0,0 +1,206 @@ +{ + "capabilities": { + "climateFunction": "AIR_CONDITIONING", + "climateNow": true, + "climateTimerTrigger": "DEPARTURE_TIMER", + "horn": true, + "isBmwChargingSupported": true, + "isCarSharingSupported": false, + "isChargeNowForBusinessSupported": false, + "isChargingHistorySupported": true, + "isChargingHospitalityEnabled": false, + "isChargingLoudnessEnabled": false, + "isChargingPlanSupported": true, + "isChargingPowerLimitEnabled": false, + "isChargingSettingsEnabled": false, + "isChargingTargetSocEnabled": false, + "isClimateTimerSupported": true, + "isCustomerEsimSupported": false, + "isDCSContractManagementSupported": true, + "isDataPrivacyEnabled": false, + "isEasyChargeEnabled": false, + "isEvGoChargingSupported": false, + "isMiniChargingSupported": false, + "isNonLscFeatureEnabled": false, + "isRemoteEngineStartSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteHistorySupported": true, + "isRemoteParkingSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": false, + "isSustainabilitySupported": false, + "isWifiHotspotServiceSupported": false, + "lastStateCallState": "ACTIVATED", + "lights": true, + "lock": true, + "remoteChargingCommands": {}, + "sendPoi": true, + "specialThemeSupport": [], + "unlock": true, + "vehicleFinder": false, + "vehicleStateSource": "LAST_STATE_CALL" + }, + "state": { + "chargingProfile": { + "chargingControlType": "WEEKLY_PLANNER", + "chargingMode": "DELAYED_CHARGING", + "chargingPreference": "CHARGING_WINDOW", + "chargingSettings": { + "hospitality": "NO_ACTION", + "idcc": "NO_ACTION", + "targetSoc": 100 + }, + "climatisationOn": false, + "departureTimes": [ + { + "action": "DEACTIVATE", + "id": 1, + "timeStamp": { + "hour": 7, + "minute": 35 + }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY" + ] + }, + { + "action": "DEACTIVATE", + "id": 2, + "timeStamp": { + "hour": 18, + "minute": 0 + }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY" + ] + }, + { + "action": "DEACTIVATE", + "id": 3, + "timeStamp": { + "hour": 7, + "minute": 0 + }, + "timerWeekDays": [] + }, + { + "action": "DEACTIVATE", + "id": 4, + "timerWeekDays": [] + } + ], + "reductionOfChargeCurrent": { + "end": { + "hour": 1, + "minute": 30 + }, + "start": { + "hour": 18, + "minute": 1 + } + } + }, + "checkControlMessages": [], + "climateTimers": [ + { + "departureTime": { + "hour": 6, + "minute": 40 + }, + "isWeeklyTimer": true, + "timerAction": "ACTIVATE", + "timerWeekDays": ["THURSDAY", "SUNDAY"] + }, + { + "departureTime": { + "hour": 12, + "minute": 50 + }, + "isWeeklyTimer": false, + "timerAction": "ACTIVATE", + "timerWeekDays": ["MONDAY"] + }, + { + "departureTime": { + "hour": 18, + "minute": 59 + }, + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": ["WEDNESDAY"] + } + ], + "combustionFuelLevel": { + "range": 105, + "remainingFuelLiters": 6, + "remainingFuelPercent": 65 + }, + "currentMileage": 137009, + "doorsState": { + "combinedSecurityState": "UNLOCKED", + "combinedState": "CLOSED", + "hood": "CLOSED", + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "trunk": "CLOSED" + }, + "driverPreferences": { + "lscPrivacyMode": "OFF" + }, + "electricChargingState": { + "chargingConnectionType": "CONDUCTIVE", + "chargingLevelPercent": 82, + "chargingStatus": "WAITING_FOR_CHARGING", + "chargingTarget": 100, + "isChargerConnected": true, + "range": 174 + }, + "isLeftSteering": true, + "isLscSupported": true, + "lastFetched": "2022-06-22T14:24:23.982Z", + "lastUpdatedAt": "2022-06-22T13:58:52Z", + "range": 174, + "requiredServices": [ + { + "dateTime": "2022-10-01T00:00:00.000Z", + "description": "Next service due by the specified date.", + "status": "OK", + "type": "BRAKE_FLUID" + }, + { + "dateTime": "2023-05-01T00:00:00.000Z", + "description": "Next vehicle check due after the specified distance or date.", + "status": "OK", + "type": "VEHICLE_CHECK" + }, + { + "dateTime": "2023-05-01T00:00:00.000Z", + "description": "Next state inspection due by the specified date.", + "status": "OK", + "type": "VEHICLE_TUV" + } + ], + "roofState": { + "roofState": "CLOSED", + "roofStateType": "SUN_ROOF" + }, + "windowsState": { + "combinedState": "CLOSED", + "leftFront": "CLOSED", + "rightFront": "CLOSED" + } + } +} diff --git a/tests/components/bmw_connected_drive/fixtures/vehicles/I01/vehicles_v2_bmw_0.json b/tests/components/bmw_connected_drive/fixtures/vehicles/I01/vehicles_v2_bmw_0.json new file mode 100644 index 00000000000..145bc13378e --- /dev/null +++ b/tests/components/bmw_connected_drive/fixtures/vehicles/I01/vehicles_v2_bmw_0.json @@ -0,0 +1,47 @@ +[ + { + "appVehicleType": "CONNECTED", + "attributes": { + "a4aType": "USB_ONLY", + "bodyType": "I01", + "brand": "BMW_I", + "color": 4284110934, + "countryOfOrigin": "CZ", + "driveTrain": "ELECTRIC_WITH_RANGE_EXTENDER", + "driverGuideInfo": { + "androidAppScheme": "com.bmwgroup.driversguide.row", + "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", + "iosAppScheme": "bmwdriversguide:///open", + "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" + }, + "headUnitType": "NBT", + "hmiVersion": "ID4", + "lastFetched": "2022-07-10T09:25:53.104Z", + "model": "i3 (+ REX)", + "softwareVersionCurrent": { + "iStep": 510, + "puStep": { + "month": 11, + "year": 21 + }, + "seriesCluster": "I001" + }, + "softwareVersionExFactory": { + "iStep": 502, + "puStep": { + "month": 3, + "year": 15 + }, + "seriesCluster": "I001" + }, + "year": 2015 + }, + "mappingInfo": { + "isAssociated": false, + "isLmmEnabled": false, + "isPrimaryUser": true, + "mappingStatus": "CONFIRMED" + }, + "vin": "WBY00000000REXI01" + } +] diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index 3f22f984a54..daac0c04f7b 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -12,15 +12,11 @@ from homeassistant.components.bmw_connected_drive.const import ( ) from homeassistant.const import CONF_USERNAME -from . import FIXTURE_CONFIG_ENTRY, FIXTURE_USER_INPUT +from . import FIXTURE_CONFIG_ENTRY, FIXTURE_REFRESH_TOKEN, FIXTURE_USER_INPUT from tests.common import MockConfigEntry -FIXTURE_REFRESH_TOKEN = "SOME_REFRESH_TOKEN" -FIXTURE_COMPLETE_ENTRY = { - **FIXTURE_USER_INPUT, - CONF_REFRESH_TOKEN: FIXTURE_REFRESH_TOKEN, -} +FIXTURE_COMPLETE_ENTRY = FIXTURE_CONFIG_ENTRY["data"] FIXTURE_IMPORT_ENTRY = {**FIXTURE_USER_INPUT, CONF_REFRESH_TOKEN: None} diff --git a/tests/components/bmw_connected_drive/test_sensor.py b/tests/components/bmw_connected_drive/test_sensor.py new file mode 100644 index 00000000000..cb1299a274b --- /dev/null +++ b/tests/components/bmw_connected_drive/test_sensor.py @@ -0,0 +1,52 @@ +"""Test BMW sensors.""" +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.util.unit_system import ( + IMPERIAL_SYSTEM as IMPERIAL, + METRIC_SYSTEM as METRIC, + UnitSystem, +) + +from . import setup_mocked_integration + + +@pytest.mark.parametrize( + "entity_id,unit_system,value,unit_of_measurement", + [ + ("sensor.i3_rex_remaining_range_total", METRIC, "279", "km"), + ("sensor.i3_rex_remaining_range_total", IMPERIAL, "173.36", "mi"), + ("sensor.i3_rex_mileage", METRIC, "137009", "km"), + ("sensor.i3_rex_mileage", IMPERIAL, "85133.42", "mi"), + ("sensor.i3_rex_remaining_battery_percent", METRIC, "82", "%"), + ("sensor.i3_rex_remaining_battery_percent", IMPERIAL, "82", "%"), + ("sensor.i3_rex_remaining_range_electric", METRIC, "174", "km"), + ("sensor.i3_rex_remaining_range_electric", IMPERIAL, "108.12", "mi"), + ("sensor.i3_rex_remaining_fuel", METRIC, "6", "L"), + ("sensor.i3_rex_remaining_fuel", IMPERIAL, "1.59", "gal"), + ("sensor.i3_rex_remaining_range_fuel", METRIC, "105", "km"), + ("sensor.i3_rex_remaining_range_fuel", IMPERIAL, "65.24", "mi"), + ("sensor.i3_rex_remaining_fuel_percent", METRIC, "65", "%"), + ("sensor.i3_rex_remaining_fuel_percent", IMPERIAL, "65", "%"), + ], +) +async def test_unit_conversion( + hass: HomeAssistant, + entity_id: str, + unit_system: UnitSystem, + value: str, + unit_of_measurement: str, + bmw_fixture, +) -> None: + """Test conversion between metric and imperial units for sensors.""" + + # Set unit system + hass.config.units = unit_system + + # Setup component + assert await setup_mocked_integration(hass) + + # Test + entity = hass.states.get(entity_id) + assert entity.state == value + assert entity.attributes.get("unit_of_measurement") == unit_of_measurement From 59878ea1ef94c870368a73285b6c86a2b2c0fdc1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 16 Aug 2022 17:17:10 -0400 Subject: [PATCH 3393/3516] Indieauth updates (#76880) --- homeassistant/components/auth/__init__.py | 58 +++++++++------- homeassistant/components/auth/login_flow.py | 53 ++++++++++++--- tests/components/auth/test_init.py | 23 +++++-- tests/components/auth/test_init_link_user.py | 7 +- tests/components/auth/test_login_flow.py | 70 +++++++++++++++++++- 5 files changed, 169 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index a8c7019030f..ac5035de645 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -177,6 +177,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.data[DOMAIN] = store_result hass.http.register_view(TokenView(retrieve_result)) + hass.http.register_view(RevokeTokenView()) hass.http.register_view(LinkUserView(retrieve_result)) hass.http.register_view(OAuth2AuthorizeCallbackView()) @@ -192,8 +193,37 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True +class RevokeTokenView(HomeAssistantView): + """View to revoke tokens.""" + + url = "/auth/revoke" + name = "api:auth:revocation" + requires_auth = False + cors_allowed = True + + async def post(self, request: web.Request) -> web.Response: + """Revoke a token.""" + hass: HomeAssistant = request.app["hass"] + data = cast(MultiDictProxy[str], await request.post()) + + # OAuth 2.0 Token Revocation [RFC7009] + # 2.2 The authorization server responds with HTTP status code 200 + # if the token has been revoked successfully or if the client + # submitted an invalid token. + if (token := data.get("token")) is None: + return web.Response(status=HTTPStatus.OK) + + refresh_token = await hass.auth.async_get_refresh_token_by_token(token) + + if refresh_token is None: + return web.Response(status=HTTPStatus.OK) + + await hass.auth.async_remove_refresh_token(refresh_token) + return web.Response(status=HTTPStatus.OK) + + class TokenView(HomeAssistantView): - """View to issue or revoke tokens.""" + """View to issue tokens.""" url = "/auth/token" name = "api:auth:token" @@ -217,7 +247,9 @@ class TokenView(HomeAssistantView): # The revocation request includes an additional parameter, # action=revoke. if data.get("action") == "revoke": - return await self._async_handle_revoke_token(hass, data) + # action=revoke is deprecated. Use /auth/revoke instead. + # Keep here for backwards compat + return await RevokeTokenView.post(self, request) # type: ignore[arg-type] if grant_type == "authorization_code": return await self._async_handle_auth_code(hass, data, request.remote) @@ -229,28 +261,6 @@ class TokenView(HomeAssistantView): {"error": "unsupported_grant_type"}, status_code=HTTPStatus.BAD_REQUEST ) - async def _async_handle_revoke_token( - self, - hass: HomeAssistant, - data: MultiDictProxy[str], - ) -> web.Response: - """Handle revoke token request.""" - - # OAuth 2.0 Token Revocation [RFC7009] - # 2.2 The authorization server responds with HTTP status code 200 - # if the token has been revoked successfully or if the client - # submitted an invalid token. - if (token := data.get("token")) is None: - return web.Response(status=HTTPStatus.OK) - - refresh_token = await hass.auth.async_get_refresh_token_by_token(token) - - if refresh_token is None: - return web.Response(status=HTTPStatus.OK) - - await hass.auth.async_remove_refresh_token(refresh_token) - return web.Response(status=HTTPStatus.OK) - async def _async_handle_auth_code( self, hass: HomeAssistant, diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index dc094cd581f..bb13431bfa7 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -101,11 +101,32 @@ async def async_setup( hass: HomeAssistant, store_result: Callable[[str, Credentials], str] ) -> None: """Component to allow users to login.""" + hass.http.register_view(WellKnownOAuthInfoView) hass.http.register_view(AuthProvidersView) hass.http.register_view(LoginFlowIndexView(hass.auth.login_flow, store_result)) hass.http.register_view(LoginFlowResourceView(hass.auth.login_flow, store_result)) +class WellKnownOAuthInfoView(HomeAssistantView): + """View to host the OAuth2 information.""" + + requires_auth = False + url = "/.well-known/oauth-authorization-server" + name = "well-known/oauth-authorization-server" + + async def get(self, request: web.Request) -> web.Response: + """Return the well known OAuth2 authorization info.""" + return self.json( + { + "authorization_endpoint": "/auth/authorize", + "token_endpoint": "/auth/token", + "revocation_endpoint": "/auth/revoke", + "response_types_supported": ["code"], + "service_documentation": "https://developers.home-assistant.io/docs/auth_api", + } + ) + + class AuthProvidersView(HomeAssistantView): """View to get available auth providers.""" @@ -172,6 +193,7 @@ class LoginFlowBaseView(HomeAssistantView): self, request: web.Request, client_id: str, + redirect_uri: str, result: data_entry_flow.FlowResult, ) -> web.Response: """Convert the flow result to a response.""" @@ -190,9 +212,13 @@ class LoginFlowBaseView(HomeAssistantView): await process_wrong_login(request) return self.json(_prepare_result_json(result)) + hass: HomeAssistant = request.app["hass"] + + if not await indieauth.verify_redirect_uri(hass, client_id, redirect_uri): + return self.json_message("Invalid redirect URI", HTTPStatus.FORBIDDEN) + result.pop("data") - hass: HomeAssistant = request.app["hass"] result_obj: Credentials = result.pop("result") # Result can be None if credential was never linked to a user before. @@ -234,14 +260,11 @@ class LoginFlowIndexView(LoginFlowBaseView): @log_invalid_auth async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response: """Create a new login flow.""" - hass: HomeAssistant = request.app["hass"] client_id: str = data["client_id"] redirect_uri: str = data["redirect_uri"] - if not await indieauth.verify_redirect_uri(hass, client_id, redirect_uri): - return self.json_message( - "invalid client id or redirect uri", HTTPStatus.BAD_REQUEST - ) + if not indieauth.verify_client_id(client_id): + return self.json_message("Invalid client id", HTTPStatus.BAD_REQUEST) handler: tuple[str, ...] | str if isinstance(data["handler"], list): @@ -264,7 +287,9 @@ class LoginFlowIndexView(LoginFlowBaseView): "Handler does not support init", HTTPStatus.BAD_REQUEST ) - return await self._async_flow_result_to_response(request, client_id, result) + return await self._async_flow_result_to_response( + request, client_id, redirect_uri, result + ) class LoginFlowResourceView(LoginFlowBaseView): @@ -277,13 +302,19 @@ class LoginFlowResourceView(LoginFlowBaseView): """Do not allow getting status of a flow in progress.""" return self.json_message("Invalid flow specified", HTTPStatus.NOT_FOUND) - @RequestDataValidator(vol.Schema({"client_id": str}, extra=vol.ALLOW_EXTRA)) + @RequestDataValidator( + vol.Schema( + {vol.Required("client_id"): str, vol.Required("redirect_uri"): str}, + extra=vol.ALLOW_EXTRA, + ) + ) @log_invalid_auth async def post( self, request: web.Request, data: dict[str, Any], flow_id: str ) -> web.Response: """Handle progressing a login flow request.""" - client_id = data.pop("client_id") + client_id: str = data.pop("client_id") + redirect_uri: str = data.pop("redirect_uri") if not indieauth.verify_client_id(client_id): return self.json_message("Invalid client id", HTTPStatus.BAD_REQUEST) @@ -299,7 +330,9 @@ class LoginFlowResourceView(LoginFlowBaseView): except vol.Invalid: return self.json_message("User input malformed", HTTPStatus.BAD_REQUEST) - return await self._async_flow_result_to_response(request, client_id, result) + return await self._async_flow_result_to_response( + request, client_id, redirect_uri, result + ) async def delete(self, request: web.Request, flow_id: str) -> web.Response: """Cancel a flow in progress.""" diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 706221d9371..09a74cf9bc9 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -62,7 +62,12 @@ async def test_login_new_user_and_trying_refresh_token(hass, aiohttp_client): resp = await client.post( f"/auth/login_flow/{step['flow_id']}", - json={"client_id": CLIENT_ID, "username": "test-user", "password": "test-pass"}, + json={ + "client_id": CLIENT_ID, + "redirect_uri": CLIENT_REDIRECT_URI, + "username": "test-user", + "password": "test-pass", + }, ) assert resp.status == HTTPStatus.OK @@ -126,7 +131,12 @@ async def test_auth_code_checks_local_only_user(hass, aiohttp_client): resp = await client.post( f"/auth/login_flow/{step['flow_id']}", - json={"client_id": CLIENT_ID, "username": "test-user", "password": "test-pass"}, + json={ + "client_id": CLIENT_ID, + "redirect_uri": CLIENT_REDIRECT_URI, + "username": "test-user", + "password": "test-pass", + }, ) assert resp.status == HTTPStatus.OK @@ -358,7 +368,10 @@ async def test_refresh_token_provider_rejected( assert result["error_description"] == "Invalid access" -async def test_revoking_refresh_token(hass, aiohttp_client): +@pytest.mark.parametrize( + "url,base_data", [("/auth/token", {"action": "revoke"}), ("/auth/revoke", {})] +) +async def test_revoking_refresh_token(url, base_data, hass, aiohttp_client): """Test that we can revoke refresh tokens.""" client = await async_setup_auth(hass, aiohttp_client) refresh_token = await async_setup_user_refresh_token(hass) @@ -380,9 +393,7 @@ async def test_revoking_refresh_token(hass, aiohttp_client): ) # Revoke refresh token - resp = await client.post( - "/auth/token", data={"token": refresh_token.token, "action": "revoke"} - ) + resp = await client.post(url, data={**base_data, "token": refresh_token.token}) assert resp.status == HTTPStatus.OK # Old access token should be no longer valid diff --git a/tests/components/auth/test_init_link_user.py b/tests/components/auth/test_init_link_user.py index 036dad4265f..bad6e3bfefe 100644 --- a/tests/components/auth/test_init_link_user.py +++ b/tests/components/auth/test_init_link_user.py @@ -46,7 +46,12 @@ async def async_get_code(hass, aiohttp_client): resp = await client.post( f"/auth/login_flow/{step['flow_id']}", - json={"client_id": CLIENT_ID, "username": "2nd-user", "password": "2nd-pass"}, + json={ + "client_id": CLIENT_ID, + "redirect_uri": CLIENT_REDIRECT_URI, + "username": "2nd-user", + "password": "2nd-pass", + }, ) assert resp.status == HTTPStatus.OK diff --git a/tests/components/auth/test_login_flow.py b/tests/components/auth/test_login_flow.py index 1fa06045de6..ce547149786 100644 --- a/tests/components/auth/test_login_flow.py +++ b/tests/components/auth/test_login_flow.py @@ -61,6 +61,7 @@ async def test_invalid_username_password(hass, aiohttp_client): f"/auth/login_flow/{step['flow_id']}", json={ "client_id": CLIENT_ID, + "redirect_uri": CLIENT_REDIRECT_URI, "username": "wrong-user", "password": "test-pass", }, @@ -81,6 +82,7 @@ async def test_invalid_username_password(hass, aiohttp_client): f"/auth/login_flow/{step['flow_id']}", json={ "client_id": CLIENT_ID, + "redirect_uri": CLIENT_REDIRECT_URI, "username": "test-user", "password": "wrong-pass", }, @@ -93,6 +95,49 @@ async def test_invalid_username_password(hass, aiohttp_client): assert step["step_id"] == "init" assert step["errors"]["base"] == "invalid_auth" + # Incorrect username and invalid redirect URI fails on wrong login + with patch( + "homeassistant.components.auth.login_flow.process_wrong_login" + ) as mock_process_wrong_login: + resp = await client.post( + f"/auth/login_flow/{step['flow_id']}", + json={ + "client_id": CLIENT_ID, + "redirect_uri": "http://some-other-domain.com", + "username": "wrong-user", + "password": "test-pass", + }, + ) + + assert resp.status == HTTPStatus.OK + step = await resp.json() + assert len(mock_process_wrong_login.mock_calls) == 1 + + assert step["step_id"] == "init" + assert step["errors"]["base"] == "invalid_auth" + + # Incorrect redirect URI + with patch( + "homeassistant.components.auth.indieauth.fetch_redirect_uris", return_value=[] + ), patch( + "homeassistant.components.http.ban.process_wrong_login" + ) as mock_process_wrong_login: + resp = await client.post( + f"/auth/login_flow/{step['flow_id']}", + json={ + "client_id": CLIENT_ID, + "redirect_uri": "http://some-other-domain.com", + "username": "test-user", + "password": "test-pass", + }, + ) + + assert resp.status == HTTPStatus.FORBIDDEN + data = await resp.json() + assert len(mock_process_wrong_login.mock_calls) == 1 + + assert data["message"] == "Invalid redirect URI" + async def test_login_exist_user(hass, aiohttp_client): """Test logging in with exist user.""" @@ -120,6 +165,7 @@ async def test_login_exist_user(hass, aiohttp_client): f"/auth/login_flow/{step['flow_id']}", json={ "client_id": CLIENT_ID, + "redirect_uri": CLIENT_REDIRECT_URI, "username": "test-user", "password": "test-pass", }, @@ -160,6 +206,7 @@ async def test_login_local_only_user(hass, aiohttp_client): f"/auth/login_flow/{step['flow_id']}", json={ "client_id": CLIENT_ID, + "redirect_uri": CLIENT_REDIRECT_URI, "username": "test-user", "password": "test-pass", }, @@ -202,9 +249,30 @@ async def test_login_exist_user_ip_changes(hass, aiohttp_client): resp = await client.post( f"/auth/login_flow/{step['flow_id']}", - json={"client_id": CLIENT_ID, "username": "test-user", "password": "test-pass"}, + json={ + "client_id": CLIENT_ID, + "redirect_uri": CLIENT_REDIRECT_URI, + "username": "test-user", + "password": "test-pass", + }, ) assert resp.status == 400 response = await resp.json() assert response == {"message": "IP address changed"} + + +async def test_well_known_auth_info(hass, aiohttp_client): + """Test logging in and the ip address changes results in an rejection.""" + client = await async_setup_auth(hass, aiohttp_client, setup_api=True) + resp = await client.get( + "/.well-known/oauth-authorization-server", + ) + assert resp.status == 200 + assert await resp.json() == { + "authorization_endpoint": "/auth/authorize", + "token_endpoint": "/auth/token", + "revocation_endpoint": "/auth/revoke", + "response_types_supported": ["code"], + "service_documentation": "https://developers.home-assistant.io/docs/auth_api", + } From 8070875ff486e05c38c1792ac0dd2b33ce1f1c76 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Tue, 16 Aug 2022 18:20:30 -0400 Subject: [PATCH 3394/3516] Add Fully Kiosk Browser sensor platform (#76887) --- .../components/fully_kiosk/__init__.py | 2 +- .../components/fully_kiosk/sensor.py | 138 ++++++++++++++++ tests/components/fully_kiosk/test_sensor.py | 156 ++++++++++++++++++ 3 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/fully_kiosk/sensor.py create mode 100644 tests/components/fully_kiosk/test_sensor.py diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py index 943f5c69cbe..a4c71168f4e 100644 --- a/homeassistant/components/fully_kiosk/__init__.py +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import FullyKioskDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/fully_kiosk/sensor.py b/homeassistant/components/fully_kiosk/sensor.py new file mode 100644 index 00000000000..6b3ca3fbcb0 --- /dev/null +++ b/homeassistant/components/fully_kiosk/sensor.py @@ -0,0 +1,138 @@ +"""Fully Kiosk Browser sensor.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DATA_MEGABYTES, PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import FullyKioskDataUpdateCoordinator +from .entity import FullyKioskEntity + + +def round_storage(value: int) -> float: + """Convert storage values from bytes to megabytes.""" + return round(value * 0.000001, 1) + + +@dataclass +class FullySensorEntityDescription(SensorEntityDescription): + """Fully Kiosk Browser sensor description.""" + + state_fn: Callable | None = None + + +SENSORS: tuple[FullySensorEntityDescription, ...] = ( + FullySensorEntityDescription( + key="batteryLevel", + name="Battery", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + FullySensorEntityDescription( + key="screenOrientation", + name="Screen orientation", + entity_category=EntityCategory.DIAGNOSTIC, + ), + FullySensorEntityDescription( + key="foregroundApp", + name="Foreground app", + entity_category=EntityCategory.DIAGNOSTIC, + ), + FullySensorEntityDescription( + key="currentPage", + name="Current page", + entity_category=EntityCategory.DIAGNOSTIC, + ), + FullySensorEntityDescription( + key="internalStorageFreeSpace", + name="Internal storage free space", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + state_class=SensorStateClass.MEASUREMENT, + state_fn=round_storage, + ), + FullySensorEntityDescription( + key="internalStorageTotalSpace", + name="Internal storage total space", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + state_class=SensorStateClass.MEASUREMENT, + state_fn=round_storage, + ), + FullySensorEntityDescription( + key="ramFreeMemory", + name="Free memory", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + state_class=SensorStateClass.MEASUREMENT, + state_fn=round_storage, + ), + FullySensorEntityDescription( + key="ramTotalMemory", + name="Total memory", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DATA_MEGABYTES, + state_class=SensorStateClass.MEASUREMENT, + state_fn=round_storage, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Fully Kiosk Browser sensor.""" + coordinator: FullyKioskDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + async_add_entities( + FullySensor(coordinator, description) + for description in SENSORS + if description.key in coordinator.data + ) + + +class FullySensor(FullyKioskEntity, SensorEntity): + """Representation of a Fully Kiosk Browser sensor.""" + + entity_description: FullySensorEntityDescription + + def __init__( + self, + coordinator: FullyKioskDataUpdateCoordinator, + sensor: FullySensorEntityDescription, + ) -> None: + """Initialize the sensor entity.""" + self.entity_description = sensor + + self._attr_unique_id = f"{coordinator.data['deviceID']}-{sensor.key}" + + super().__init__(coordinator) + + @property + def native_value(self) -> Any: + """Return the state of the sensor.""" + if (value := self.coordinator.data.get(self.entity_description.key)) is None: + return None + + if self.entity_description.state_fn is not None: + return self.entity_description.state_fn(value) + + return value diff --git a/tests/components/fully_kiosk/test_sensor.py b/tests/components/fully_kiosk/test_sensor.py new file mode 100644 index 00000000000..b54627f85b2 --- /dev/null +++ b/tests/components/fully_kiosk/test_sensor.py @@ -0,0 +1,156 @@ +"""Test the Fully Kiosk Browser sensors.""" +from unittest.mock import MagicMock + +from fullykiosk import FullyKioskError + +from homeassistant.components.fully_kiosk.const import DOMAIN, UPDATE_INTERVAL +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.helpers.entity import EntityCategory +from homeassistant.util import dt + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_sensors_sensors( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test standard Fully Kiosk sensors.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("sensor.amazon_fire_battery") + assert state + assert state.state == "100" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.BATTERY + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Battery" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = entity_registry.async_get("sensor.amazon_fire_battery") + assert entry + assert entry.unique_id == "abcdef-123456-batteryLevel" + + state = hass.states.get("sensor.amazon_fire_screen_orientation") + assert state + assert state.state == "90" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Screen orientation" + + entry = entity_registry.async_get("sensor.amazon_fire_screen_orientation") + assert entry + assert entry.unique_id == "abcdef-123456-screenOrientation" + + state = hass.states.get("sensor.amazon_fire_foreground_app") + assert state + assert state.state == "de.ozerov.fully" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Foreground app" + + entry = entity_registry.async_get("sensor.amazon_fire_foreground_app") + assert entry + assert entry.unique_id == "abcdef-123456-foregroundApp" + + state = hass.states.get("sensor.amazon_fire_current_page") + assert state + assert state.state == "https://homeassistant.local" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Current page" + + entry = entity_registry.async_get("sensor.amazon_fire_current_page") + assert entry + assert entry.unique_id == "abcdef-123456-currentPage" + + state = hass.states.get("sensor.amazon_fire_internal_storage_free_space") + assert state + assert state.state == "11675.5" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Amazon Fire Internal storage free space" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = entity_registry.async_get("sensor.amazon_fire_internal_storage_free_space") + assert entry + assert entry.unique_id == "abcdef-123456-internalStorageFreeSpace" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + state = hass.states.get("sensor.amazon_fire_internal_storage_total_space") + assert state + assert state.state == "12938.5" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Amazon Fire Internal storage total space" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = entity_registry.async_get("sensor.amazon_fire_internal_storage_total_space") + assert entry + assert entry.unique_id == "abcdef-123456-internalStorageTotalSpace" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + state = hass.states.get("sensor.amazon_fire_free_memory") + assert state + assert state.state == "362.4" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Free memory" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = entity_registry.async_get("sensor.amazon_fire_free_memory") + assert entry + assert entry.unique_id == "abcdef-123456-ramFreeMemory" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + state = hass.states.get("sensor.amazon_fire_total_memory") + assert state + assert state.state == "1440.1" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Amazon Fire Total memory" + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + + entry = entity_registry.async_get("sensor.amazon_fire_total_memory") + assert entry + assert entry.unique_id == "abcdef-123456-ramTotalMemory" + assert entry.entity_category == EntityCategory.DIAGNOSTIC + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.configuration_url == "http://192.168.1.234:2323" + assert device_entry.entry_type is None + assert device_entry.hw_version is None + assert device_entry.identifiers == {(DOMAIN, "abcdef-123456")} + assert device_entry.manufacturer == "amzn" + assert device_entry.model == "KFDOWI" + assert device_entry.name == "Amazon Fire" + assert device_entry.sw_version == "1.42.5" + + # Test unknown/missing data + mock_fully_kiosk.getDeviceInfo.return_value = {} + async_fire_time_changed(hass, dt.utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("sensor.amazon_fire_internal_storage_free_space") + assert state + assert state.state == STATE_UNKNOWN + + # Test failed update + mock_fully_kiosk.getDeviceInfo.side_effect = FullyKioskError("error", "status") + async_fire_time_changed(hass, dt.utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("sensor.amazon_fire_internal_storage_free_space") + assert state + assert state.state == STATE_UNAVAILABLE From 683132ae19cd6a01f9fe74740fd97833518f6eef Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 17 Aug 2022 00:26:42 +0000 Subject: [PATCH 3395/3516] [ci skip] Translation update --- .../android_ip_webcam/translations/ja.json | 1 + .../components/awair/translations/ca.json | 8 +++++++- .../components/awair/translations/de.json | 8 +++++++- .../components/awair/translations/en.json | 8 +++++++- .../components/awair/translations/es.json | 8 +++++++- .../components/awair/translations/et.json | 8 +++++++- .../components/awair/translations/id.json | 6 ++++++ .../components/awair/translations/it.json | 8 +++++++- .../components/awair/translations/no.json | 6 ++++++ .../components/awair/translations/pt-BR.json | 8 +++++++- .../components/awair/translations/ru.json | 6 ++++++ .../fully_kiosk/translations/ca.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/es.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/et.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/it.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/pt-BR.json | 20 +++++++++++++++++++ .../components/hue/translations/de.json | 17 +++++++++------- .../components/hue/translations/es.json | 17 +++++++++------- .../components/hue/translations/et.json | 17 +++++++++------- .../components/hue/translations/it.json | 17 +++++++++------- .../components/hue/translations/ja.json | 2 ++ .../components/hue/translations/no.json | 17 +++++++++------- .../components/hue/translations/ru.json | 5 ++++- .../components/hue/translations/zh-Hant.json | 17 +++++++++------- .../lutron_caseta/translations/es.json | 10 +++++----- .../openexchangerates/translations/ja.json | 1 + .../simplepush/translations/ja.json | 1 + .../tuya/translations/select.ja.json | 2 +- .../xiaomi_miio/translations/select.ca.json | 10 ++++++++++ .../xiaomi_miio/translations/select.en.json | 10 ++++++++++ .../xiaomi_miio/translations/select.es.json | 10 ++++++++++ .../xiaomi_miio/translations/select.et.json | 10 ++++++++++ .../xiaomi_miio/translations/select.it.json | 10 ++++++++++ .../translations/select.pt-BR.json | 10 ++++++++++ .../components/zha/translations/de.json | 2 +- 35 files changed, 303 insertions(+), 57 deletions(-) create mode 100644 homeassistant/components/fully_kiosk/translations/ca.json create mode 100644 homeassistant/components/fully_kiosk/translations/es.json create mode 100644 homeassistant/components/fully_kiosk/translations/et.json create mode 100644 homeassistant/components/fully_kiosk/translations/it.json create mode 100644 homeassistant/components/fully_kiosk/translations/pt-BR.json diff --git a/homeassistant/components/android_ip_webcam/translations/ja.json b/homeassistant/components/android_ip_webcam/translations/ja.json index 832d6b9c71c..519edb51609 100644 --- a/homeassistant/components/android_ip_webcam/translations/ja.json +++ b/homeassistant/components/android_ip_webcam/translations/ja.json @@ -19,6 +19,7 @@ }, "issues": { "deprecated_yaml": { + "description": "Android IP Webcam\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Android IP Webcam\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Android IP Webcam YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/awair/translations/ca.json b/homeassistant/components/awair/translations/ca.json index b5c16826078..89e461c709d 100644 --- a/homeassistant/components/awair/translations/ca.json +++ b/homeassistant/components/awair/translations/ca.json @@ -29,7 +29,13 @@ "data": { "host": "Adre\u00e7a IP" }, - "description": "L'API local d'Awair s'ha d'activar seguint aquests passos: {url}" + "description": "Segueix [aquestes instruccions]({url}) per com activar l'API local d'Awair.\n\nPrem 'envia' quan hagis acabat." + }, + "local_pick": { + "data": { + "device": "Dispositiu", + "host": "Adre\u00e7a IP" + } }, "reauth": { "data": { diff --git a/homeassistant/components/awair/translations/de.json b/homeassistant/components/awair/translations/de.json index 43b387e8286..cba100b899c 100644 --- a/homeassistant/components/awair/translations/de.json +++ b/homeassistant/components/awair/translations/de.json @@ -29,7 +29,13 @@ "data": { "host": "IP-Adresse" }, - "description": "Awair Local API muss wie folgt aktiviert werden: {url}" + "description": "Befolge [diese Anweisungen]({url}), um die Awair Local API zu aktivieren. \n\nKlicke abschlie\u00dfend auf Senden." + }, + "local_pick": { + "data": { + "device": "Ger\u00e4t", + "host": "IP-Adresse" + } }, "reauth": { "data": { diff --git a/homeassistant/components/awair/translations/en.json b/homeassistant/components/awair/translations/en.json index ca572958c46..dfc06d2346a 100644 --- a/homeassistant/components/awair/translations/en.json +++ b/homeassistant/components/awair/translations/en.json @@ -29,7 +29,13 @@ "data": { "host": "IP Address" }, - "description": "Awair Local API must be enabled following these steps: {url}" + "description": "Follow [these instructions]({url}) on how to enable the Awair Local API.\n\nClick submit when done." + }, + "local_pick": { + "data": { + "device": "Device", + "host": "IP Address" + } }, "reauth": { "data": { diff --git a/homeassistant/components/awair/translations/es.json b/homeassistant/components/awair/translations/es.json index df5667ff1f9..82568ce9983 100644 --- a/homeassistant/components/awair/translations/es.json +++ b/homeassistant/components/awair/translations/es.json @@ -29,7 +29,13 @@ "data": { "host": "Direcci\u00f3n IP" }, - "description": "La API local de Awair debe estar habilitada siguiendo estos pasos: {url}" + "description": "Sigue [estas instrucciones]({url}) sobre c\u00f3mo habilitar la API local de Awair. \n\nHaz clic en enviar cuando hayas terminado." + }, + "local_pick": { + "data": { + "device": "Dispositivo", + "host": "Direcci\u00f3n IP" + } }, "reauth": { "data": { diff --git a/homeassistant/components/awair/translations/et.json b/homeassistant/components/awair/translations/et.json index c40f4be1f1c..327379ace6e 100644 --- a/homeassistant/components/awair/translations/et.json +++ b/homeassistant/components/awair/translations/et.json @@ -29,7 +29,13 @@ "data": { "host": "IP aadress" }, - "description": "Awairi kohalik API tuleb lubada j\u00e4rgmiste sammude abil: {url}" + "description": "J\u00e4rgi [neid juhiseid]({url}) Awair Local API lubamise kohta.\n\nKui oled l\u00f5petanud, kl\u00f5psa nuppu Esita." + }, + "local_pick": { + "data": { + "device": "Seade", + "host": "IP aadress" + } }, "reauth": { "data": { diff --git a/homeassistant/components/awair/translations/id.json b/homeassistant/components/awair/translations/id.json index 835f0d7716a..39b63bef7e5 100644 --- a/homeassistant/components/awair/translations/id.json +++ b/homeassistant/components/awair/translations/id.json @@ -31,6 +31,12 @@ }, "description": "API Awair Local harus diaktifkan dengan mengikuti langkah-langkah berikut: {url}" }, + "local_pick": { + "data": { + "device": "Perangkat", + "host": "Alamat IP" + } + }, "reauth": { "data": { "access_token": "Token Akses", diff --git a/homeassistant/components/awair/translations/it.json b/homeassistant/components/awair/translations/it.json index d82934fceb9..c8c1f1f6ec6 100644 --- a/homeassistant/components/awair/translations/it.json +++ b/homeassistant/components/awair/translations/it.json @@ -29,7 +29,13 @@ "data": { "host": "Indirizzo IP" }, - "description": "Awair Local API deve essere abilitato seguendo questi passaggi: {url}" + "description": "Segui [queste istruzioni]({url}) su come abilitare l'API Awair Local. \n\n Fai clic su Invia quando hai finito." + }, + "local_pick": { + "data": { + "device": "Dispositivo", + "host": "Indirizzo IP" + } }, "reauth": { "data": { diff --git a/homeassistant/components/awair/translations/no.json b/homeassistant/components/awair/translations/no.json index 9ffbf544909..d1c7e89f204 100644 --- a/homeassistant/components/awair/translations/no.json +++ b/homeassistant/components/awair/translations/no.json @@ -31,6 +31,12 @@ }, "description": "Awair Local API m\u00e5 aktiveres ved \u00e5 f\u00f8lge disse trinnene: {url}" }, + "local_pick": { + "data": { + "device": "Enhet", + "host": "IP adresse" + } + }, "reauth": { "data": { "access_token": "Tilgangstoken", diff --git a/homeassistant/components/awair/translations/pt-BR.json b/homeassistant/components/awair/translations/pt-BR.json index e3c3e24f738..20d2c104b60 100644 --- a/homeassistant/components/awair/translations/pt-BR.json +++ b/homeassistant/components/awair/translations/pt-BR.json @@ -29,7 +29,13 @@ "data": { "host": "Endere\u00e7o IP" }, - "description": "Awair Local API deve ser ativada seguindo estas etapas: {url}" + "description": "Siga [estas instru\u00e7\u00f5es]({url}) sobre como ativar a API local Awair. \n\n Clique em enviar quando terminar." + }, + "local_pick": { + "data": { + "device": "Dispositivo", + "host": "Endere\u00e7o IP" + } }, "reauth": { "data": { diff --git a/homeassistant/components/awair/translations/ru.json b/homeassistant/components/awair/translations/ru.json index c0be2df4b0a..a81ef9585bd 100644 --- a/homeassistant/components/awair/translations/ru.json +++ b/homeassistant/components/awair/translations/ru.json @@ -31,6 +31,12 @@ }, "description": "\u0427\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 API Awair, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0437\u0434\u0435\u0441\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f: {url}" }, + "local_pick": { + "data": { + "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "host": "IP-\u0430\u0434\u0440\u0435\u0441" + } + }, "reauth": { "data": { "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", diff --git a/homeassistant/components/fully_kiosk/translations/ca.json b/homeassistant/components/fully_kiosk/translations/ca.json new file mode 100644 index 00000000000..2cb3945ed01 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/es.json b/homeassistant/components/fully_kiosk/translations/es.json new file mode 100644 index 00000000000..1b617892060 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/et.json b/homeassistant/components/fully_kiosk/translations/et.json new file mode 100644 index 00000000000..b046deb0527 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba seadistatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/it.json b/homeassistant/components/fully_kiosk/translations/it.json new file mode 100644 index 00000000000..f8b414166c9 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/pt-BR.json b/homeassistant/components/fully_kiosk/translations/pt-BR.json new file mode 100644 index 00000000000..172933953e8 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 est\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Senha" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/de.json b/homeassistant/components/hue/translations/de.json index c28014031cc..06f610099c6 100644 --- a/homeassistant/components/hue/translations/de.json +++ b/homeassistant/components/hue/translations/de.json @@ -44,6 +44,8 @@ "button_2": "Zweite Taste", "button_3": "Dritte Taste", "button_4": "Vierte Taste", + "clock_wise": "Drehung im Uhrzeigersinn", + "counter_clock_wise": "Drehung gegen den Uhrzeigersinn", "dim_down": "Dimmer runter", "dim_up": "Dimmer hoch", "double_buttons_1_3": "Erste und dritte Taste", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "Beide \"{subtype}\" losgelassen", - "initial_press": "Taste \"{subtype}\" anfangs gedr\u00fcckt", - "long_release": "Taste \"{subtype}\" nach langem Dr\u00fccken losgelassen", - "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen", - "remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt", - "remote_button_short_release": "\"{subtype}\" Taste losgelassen", + "initial_press": "\"{subtype}\" anfangs gedr\u00fcckt", + "long_release": "\"{subtype}\" nach langem Dr\u00fccken losgelassen", + "remote_button_long_release": "\"{subtype}\" nach langem Dr\u00fccken losgelassen", + "remote_button_short_press": "\"{subtype}\" gedr\u00fcckt", + "remote_button_short_release": "\"{subtype}\" losgelassen", "remote_double_button_long_press": "Beide \"{subtype}\" nach langem Dr\u00fccken losgelassen", "remote_double_button_short_press": "Beide \"{subtype}\" losgelassen", - "repeat": "Taste \"{subtype}\" gedr\u00fcckt gehalten", - "short_release": "Taste \"{subtype}\" nach kurzem Dr\u00fccken losgelassen" + "repeat": "\"{subtype}\" gedr\u00fcckt gehalten", + "short_release": "\"{subtype}\" nach kurzem Dr\u00fccken losgelassen", + "start": "\"{subtype}\" wurde anf\u00e4nglich gedr\u00fcckt" } }, "options": { diff --git a/homeassistant/components/hue/translations/es.json b/homeassistant/components/hue/translations/es.json index 96f3388f2d3..eede6e94959 100644 --- a/homeassistant/components/hue/translations/es.json +++ b/homeassistant/components/hue/translations/es.json @@ -44,6 +44,8 @@ "button_2": "Segundo bot\u00f3n", "button_3": "Tercer bot\u00f3n", "button_4": "Cuarto bot\u00f3n", + "clock_wise": "Rotaci\u00f3n en el sentido de las agujas del reloj", + "counter_clock_wise": "Rotaci\u00f3n en sentido contrario a las agujas del reloj", "dim_down": "Bajar la intensidad", "dim_up": "Subir la intensidad", "double_buttons_1_3": "Botones Primero y Tercero", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "Ambos \"{subtype}\" soltados", - "initial_press": "Bot\u00f3n \"{subtype}\" pulsado inicialmente", - "long_release": "Bot\u00f3n \"{subtype}\" soltado tras una pulsaci\u00f3n larga", - "remote_button_long_release": "Bot\u00f3n \"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", - "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", - "remote_button_short_release": "Bot\u00f3n \"{subtype}\" soltado", + "initial_press": "\"{subtype}\" pulsado inicialmente", + "long_release": "\"{subtype}\" soltado tras una pulsaci\u00f3n larga", + "remote_button_long_release": "\"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", + "remote_button_short_press": "\"{subtype}\" pulsado", + "remote_button_short_release": "\"{subtype}\" soltado", "remote_double_button_long_press": "Ambos \"{subtype}\" soltados despu\u00e9s de pulsaci\u00f3n larga", "remote_double_button_short_press": "Ambos \"{subtype}\" soltados", - "repeat": "Bot\u00f3n \"{subtype}\" presionado", - "short_release": "Bot\u00f3n \"{subtype}\" soltado tras una breve pulsaci\u00f3n" + "repeat": "\"{subtype}\" mantenido pulsado", + "short_release": "\"{subtype}\" soltado tras una breve pulsaci\u00f3n", + "start": "\"{subtype}\" pulsado inicialmente" } }, "options": { diff --git a/homeassistant/components/hue/translations/et.json b/homeassistant/components/hue/translations/et.json index df1288d415d..274502b1587 100644 --- a/homeassistant/components/hue/translations/et.json +++ b/homeassistant/components/hue/translations/et.json @@ -44,6 +44,8 @@ "button_2": "Teine nupp", "button_3": "Kolmas nupp", "button_4": "Neljas nupp", + "clock_wise": "P\u00f6\u00f6rlemine p\u00e4rip\u00e4eva", + "counter_clock_wise": "P\u00f6\u00f6ramine vastup\u00e4eva", "dim_down": "H\u00e4marda", "dim_up": "Tee heledamaks", "double_buttons_1_3": "Esimene ja kolmas nupp", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "\"{subtype}\" nupp vabastatati", - "initial_press": "Nuppu \"{subtype}\" on vajutatud", - "long_release": "Nupp \"{subtype}\" vabastati p\u00e4rast pikka vajutust", - "remote_button_long_release": "\"{subtype}\" nupp vabastatati p\u00e4rast pikka vajutust", - "remote_button_short_press": "\"{subtype}\" nupp on vajutatud", - "remote_button_short_release": "\"{subtype}\" nupp vabastati", + "initial_press": "\"{subtype}\" on vajutatud", + "long_release": "\"{subtype}\" vabastati p\u00e4rast pikka vajutust", + "remote_button_long_release": "\"{subtype}\" vabastatati p\u00e4rast pikka vajutust", + "remote_button_short_press": "\"{subtype}\" vajutati", + "remote_button_short_release": "\"{subtype}\" vabastati", "remote_double_button_long_press": "M\u00f5lemad \"{subtype}\" nupud vabastatati p\u00e4rast pikka vajutust", "remote_double_button_short_press": "M\u00f5lemad \"{subtype}\" nupud vabastatati", - "repeat": "Nuppu \" {subtype} \" hoitakse all", - "short_release": "Nupp \" {subtype} \" vabastati p\u00e4rast l\u00fchikest vajutust" + "repeat": "\" {subtype} \" hoitakse all", + "short_release": "\" {subtype} \" vabastati p\u00e4rast l\u00fchikest vajutust", + "start": "vajutati \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/hue/translations/it.json b/homeassistant/components/hue/translations/it.json index 0c1dfce4c1a..be64a3b0624 100644 --- a/homeassistant/components/hue/translations/it.json +++ b/homeassistant/components/hue/translations/it.json @@ -44,6 +44,8 @@ "button_2": "Secondo pulsante", "button_3": "Terzo pulsante", "button_4": "Quarto pulsante", + "clock_wise": "Rotazione in senso orario", + "counter_clock_wise": "Rotazione in senso antiorario", "dim_down": "Diminuisce luminosit\u00e0", "dim_up": "Aumenta luminosit\u00e0", "double_buttons_1_3": "Pulsanti Primo e Terzo", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "Entrambi \"{subtype}\" rilasciati", - "initial_press": "Pulsante \"{subtype}\" premuto inizialmente", - "long_release": "Pulsante \"{subtype}\" rilasciato dopo una pressione prolungata", - "remote_button_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione", - "remote_button_short_press": "Pulsante \"{subtype}\" premuto", - "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", + "initial_press": "\"{subtype}\" premuto inizialmente", + "long_release": "\"{subtype}\" rilasciato dopo una pressione prolungata", + "remote_button_long_release": "\"{subtype}\" rilasciato dopo una lunga pressione", + "remote_button_short_press": "\"{subtype}\" premuto", + "remote_button_short_release": "\"{subtype}\" rilasciato", "remote_double_button_long_press": "Entrambi i \"{subtype}\" rilasciati dopo una lunga pressione", "remote_double_button_short_press": "Entrambi i \"{subtype}\" rilasciati", - "repeat": "Pulsante \"{subtype}\" tenuto premuto", - "short_release": "Pulsante \"{subtype}\" rilasciato dopo una breve pressione" + "repeat": "\"{subtype}\" tenuto premuto", + "short_release": "\"{subtype}\" rilasciato dopo una breve pressione", + "start": "\"{subtype}\" premuto inizialmente" } }, "options": { diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index ec88173adca..f5eddbc0242 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -44,6 +44,8 @@ "button_2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_4": "4\u756a\u76ee\u306e\u30dc\u30bf\u30f3", + "clock_wise": "\u6642\u8a08\u56de\u308a\u306b\u56de\u8ee2", + "counter_clock_wise": "\u53cd\u6642\u8a08\u56de\u308a\u306b\u56de\u8ee2", "dim_down": "\u8584\u6697\u304f\u3059\u308b", "dim_up": "\u5fae\u304b\u306b\u660e\u308b\u304f\u3059\u308b", "double_buttons_1_3": "1\u756a\u76ee\u30683\u756a\u76ee\u306e\u30dc\u30bf\u30f3", diff --git a/homeassistant/components/hue/translations/no.json b/homeassistant/components/hue/translations/no.json index d34899db978..598aa77629f 100644 --- a/homeassistant/components/hue/translations/no.json +++ b/homeassistant/components/hue/translations/no.json @@ -44,6 +44,8 @@ "button_2": "Andre knapp", "button_3": "Tredje knapp", "button_4": "Fjerde knapp", + "clock_wise": "Rotasjon med klokken", + "counter_clock_wise": "Rotasjon mot klokken", "dim_down": "Dimm ned", "dim_up": "Dimm opp", "double_buttons_1_3": "F\u00f8rste og tredje knapper", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "Begge \"{subtype}\" er utgitt", - "initial_press": "Knappen \"{subtype}\" ble f\u00f8rst trykket", - "long_release": "Knapp \"{subtype}\" slippes etter lang trykk", - "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", - "remote_button_short_press": "\"{subtype}\" -knappen ble trykket", - "remote_button_short_release": "\"{subtype}\"-knappen sluppet", + "initial_press": "\" {subtype} \" trykket f\u00f8rst", + "long_release": "\" {subtype} \" utgitt etter langt trykk", + "remote_button_long_release": "\" {subtype} \" utgitt etter langt trykk", + "remote_button_short_press": "\" {subtype} \" trykket", + "remote_button_short_release": "\" {subtype} \" utgitt", "remote_double_button_long_press": "Begge \"{subtype}\" utgitt etter lang trykk", "remote_double_button_short_press": "Begge \"{subtype}\" utgitt", - "repeat": "Knappen \" {subtype} \" holdt nede", - "short_release": "Knapp \"{subtype}\" slippes etter kort trykk" + "repeat": "\" {subtype} \" holdt nede", + "short_release": "\" {subtype} \" utgitt etter kort trykk", + "start": "\" {subtype} \" trykket f\u00f8rst" } }, "options": { diff --git a/homeassistant/components/hue/translations/ru.json b/homeassistant/components/hue/translations/ru.json index 6e470b74f1f..3f34b6b99e4 100644 --- a/homeassistant/components/hue/translations/ru.json +++ b/homeassistant/components/hue/translations/ru.json @@ -44,6 +44,8 @@ "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "clock_wise": "\u0412\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043f\u043e \u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0441\u0442\u0440\u0435\u043b\u043a\u0435", + "counter_clock_wise": "\u0412\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0442\u0438\u0432 \u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0441\u0442\u0440\u0435\u043b\u043a\u0438", "dim_down": "\u0423\u043c\u0435\u043d\u044c\u0448\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", "dim_up": "\u0423\u0432\u0435\u043b\u0438\u0447\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", "double_buttons_1_3": "\u041f\u0435\u0440\u0432\u0430\u044f \u0438 \u0442\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0438", @@ -61,7 +63,8 @@ "remote_double_button_long_press": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u044b \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", "remote_double_button_short_press": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u044b \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", "repeat": "{subtype} \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0436\u0430\u0442\u043e\u0439", - "short_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f" + "short_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "start": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u0435\u0440\u0432\u043e\u043d\u0430\u0447\u0430\u043b\u044c\u043d\u043e" } }, "options": { diff --git a/homeassistant/components/hue/translations/zh-Hant.json b/homeassistant/components/hue/translations/zh-Hant.json index 1816b459a85..db9ae4c5355 100644 --- a/homeassistant/components/hue/translations/zh-Hant.json +++ b/homeassistant/components/hue/translations/zh-Hant.json @@ -44,6 +44,8 @@ "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215", "button_3": "\u7b2c\u4e09\u500b\u6309\u9215", "button_4": "\u7b2c\u56db\u500b\u6309\u9215", + "clock_wise": "\u9806\u6642\u91dd\u65cb\u8f49", + "counter_clock_wise": "\u9006\u6642\u91dd\u65cb\u8f49", "dim_down": "\u8abf\u6697", "dim_up": "\u8abf\u4eae", "double_buttons_1_3": "\u7b2c\u4e00\u8207\u7b2c\u4e09\u500b\u6309\u9215", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "\"{subtype}\" \u4e00\u8d77\u91cb\u653e", - "initial_press": "\u6309\u9215 \"{subtype}\" \u6700\u521d\u6309\u4e0b", - "long_release": "\u6309\u9215 \"{subtype}\" \u9577\u6309\u5f8c\u91cb\u653e", - "remote_button_long_release": "\"{subtype}\" \u6309\u9215\u9577\u6309\u5f8c\u91cb\u653e", - "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", - "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", + "initial_press": "\"{subtype}\" \u6700\u521d\u6309\u4e0b", + "long_release": "\"{subtype}\" \u9577\u6309\u5f8c\u91cb\u653e", + "remote_button_long_release": "\"{subtype}\" \u9577\u6309\u5f8c\u91cb\u653e", + "remote_button_short_press": "\"{subtype}\" \u5df2\u6309\u4e0b", + "remote_button_short_release": "\"{subtype}\" \u5df2\u91cb\u653e", "remote_double_button_long_press": "\"{subtype}\" \u4e00\u8d77\u9577\u6309\u5f8c\u91cb\u653e", "remote_double_button_short_press": "\"{subtype}\" \u4e00\u8d77\u91cb\u653e", - "repeat": "\u6309\u9215 \"{subtype}\" \u6309\u4e0b", - "short_release": "\u6309\u9215 \"{subtype}\" \u77ed\u6309\u5f8c\u91cb\u653e" + "repeat": "\"{subtype}\" \u6309\u4e0b", + "short_release": "\"{subtype}\" \u77ed\u6309\u5f8c\u91cb\u653e", + "start": "\"{subtype}\" \u6700\u521d\u6309\u4e0b" } }, "options": { diff --git a/homeassistant/components/lutron_caseta/translations/es.json b/homeassistant/components/lutron_caseta/translations/es.json index aa33259c5a3..95559e14baf 100644 --- a/homeassistant/components/lutron_caseta/translations/es.json +++ b/homeassistant/components/lutron_caseta/translations/es.json @@ -43,11 +43,11 @@ "group_2_button_1": "Primer bot\u00f3n del segundo grupo", "group_2_button_2": "Segundo bot\u00f3n del segundo grupo", "lower": "Bajar", - "lower_1": "Bajar 1", - "lower_2": "Bajar 2", - "lower_3": "Bajar 3", - "lower_4": "Bajar 4", - "lower_all": "Bajar todo", + "lower_1": "Inferior 1", + "lower_2": "Inferior 2", + "lower_3": "Inferior 3", + "lower_4": "Inferior 4", + "lower_all": "Todos los inferiores", "off": "Apagado", "on": "Encendido", "open_1": "Abrir 1", diff --git a/homeassistant/components/openexchangerates/translations/ja.json b/homeassistant/components/openexchangerates/translations/ja.json index b4299af9207..0946ca7a4d9 100644 --- a/homeassistant/components/openexchangerates/translations/ja.json +++ b/homeassistant/components/openexchangerates/translations/ja.json @@ -26,6 +26,7 @@ }, "issues": { "deprecated_yaml": { + "description": "Open Exchange Rates\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Open Exchange Rates\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Open Exchange Rates YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/simplepush/translations/ja.json b/homeassistant/components/simplepush/translations/ja.json index c407990aa4d..11e28073318 100644 --- a/homeassistant/components/simplepush/translations/ja.json +++ b/homeassistant/components/simplepush/translations/ja.json @@ -24,6 +24,7 @@ "title": "Simplepush YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" }, "removed_yaml": { + "description": "Simplepush\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Simplepush\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Simplepush YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f" } } diff --git a/homeassistant/components/tuya/translations/select.ja.json b/homeassistant/components/tuya/translations/select.ja.json index 5712544feb3..5d8c83175b2 100644 --- a/homeassistant/components/tuya/translations/select.ja.json +++ b/homeassistant/components/tuya/translations/select.ja.json @@ -37,7 +37,7 @@ "90": "90\u00b0" }, "tuya__fingerbot_mode": { - "click": "\u62bc\u3059", + "click": "\u30d7\u30c3\u30b7\u30e5", "switch": "\u30b9\u30a4\u30c3\u30c1" }, "tuya__humidifier_level": { diff --git a/homeassistant/components/xiaomi_miio/translations/select.ca.json b/homeassistant/components/xiaomi_miio/translations/select.ca.json index bc96de04645..41d5169fda6 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.ca.json +++ b/homeassistant/components/xiaomi_miio/translations/select.ca.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Endavant", + "left": "Esquerra", + "right": "Dreta" + }, "xiaomi_miio__led_brightness": { "bright": "Brillant", "dim": "Atenua", "off": "OFF" + }, + "xiaomi_miio__ptc_level": { + "high": "Alt", + "low": "Baix", + "medium": "Mitj\u00e0" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.en.json b/homeassistant/components/xiaomi_miio/translations/select.en.json index 60a1d738b81..217b803b72f 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.en.json +++ b/homeassistant/components/xiaomi_miio/translations/select.en.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Forward", + "left": "Left", + "right": "Right" + }, "xiaomi_miio__led_brightness": { "bright": "Bright", "dim": "Dim", "off": "Off" + }, + "xiaomi_miio__ptc_level": { + "high": "High", + "low": "Low", + "medium": "Medium" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.es.json b/homeassistant/components/xiaomi_miio/translations/select.es.json index 3906ef91342..0dfd04e5ca2 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.es.json +++ b/homeassistant/components/xiaomi_miio/translations/select.es.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Adelante", + "left": "Izquierda", + "right": "Derecha" + }, "xiaomi_miio__led_brightness": { "bright": "Brillo", "dim": "Atenuar", "off": "Apagado" + }, + "xiaomi_miio__ptc_level": { + "high": "Alto", + "low": "Bajo", + "medium": "Medio" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.et.json b/homeassistant/components/xiaomi_miio/translations/select.et.json index 7195f5703b4..a13acab984f 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.et.json +++ b/homeassistant/components/xiaomi_miio/translations/select.et.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Edasi", + "left": "Vasakule", + "right": "Paremale" + }, "xiaomi_miio__led_brightness": { "bright": "Hele", "dim": "Tuhm", "off": "Kustu" + }, + "xiaomi_miio__ptc_level": { + "high": "K\u00f5rge", + "low": "Madal", + "medium": "Keskmine" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.it.json b/homeassistant/components/xiaomi_miio/translations/select.it.json index 21e79e41e99..dd755b4cf27 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.it.json +++ b/homeassistant/components/xiaomi_miio/translations/select.it.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Avanti", + "left": "Sinistra", + "right": "Destra" + }, "xiaomi_miio__led_brightness": { "bright": "Brillante", "dim": "Fioca", "off": "Spento" + }, + "xiaomi_miio__ptc_level": { + "high": "Alto", + "low": "Basso", + "medium": "Medio" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.pt-BR.json b/homeassistant/components/xiaomi_miio/translations/select.pt-BR.json index c4c1735bff3..1fba0d03d66 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.pt-BR.json +++ b/homeassistant/components/xiaomi_miio/translations/select.pt-BR.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Avan\u00e7ar", + "left": "Esquerda", + "right": "Direita" + }, "xiaomi_miio__led_brightness": { "bright": "Brilhante", "dim": "Escurecido", "off": "Desligado" + }, + "xiaomi_miio__ptc_level": { + "high": "Alto", + "low": "Baixo", + "medium": "M\u00e9dio" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index b4ad58636a7..7786a3b7bf3 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -52,7 +52,7 @@ "default_light_transition": "Standardlicht\u00fcbergangszeit (Sekunden)", "enable_identify_on_join": "Aktiviere den Identifikationseffekt, wenn Ger\u00e4te dem Netzwerk beitreten", "enhanced_light_transition": "Aktiviere einen verbesserten Lichtfarben-/Temperatur\u00fcbergang aus einem ausgeschalteten Zustand", - "light_transitioning_flag": "Erweiterten Helligkeitsregler w\u00e4hrend des Licht\u00fcbergangs aktivieren", + "light_transitioning_flag": "Verbesserten Helligkeitsregler w\u00e4hrend des Licht\u00fcbergangs aktivieren", "title": "Globale Optionen" } }, From 8c62713af3efc736f5678b089ae29c9e468765e4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 16 Aug 2022 20:49:27 -0400 Subject: [PATCH 3396/3516] Bump frontend to 20220816.0 (#76895) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index ed9b381ee9d..207b57babb2 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20220802.0"], + "requirements": ["home-assistant-frontend==20220816.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 82c37466db7..eaf463df32d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -18,7 +18,7 @@ cryptography==36.0.2 fnvhash==0.1.0 hass-nabucasa==0.54.1 home-assistant-bluetooth==1.3.0 -home-assistant-frontend==20220802.0 +home-assistant-frontend==20220816.0 httpx==0.23.0 ifaddr==0.1.7 jinja2==3.1.2 diff --git a/requirements_all.txt b/requirements_all.txt index dfae2e4afd9..c4fdeec309b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -836,7 +836,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220802.0 +home-assistant-frontend==20220816.0 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0623b6c7c79..fca44241201 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -613,7 +613,7 @@ hole==0.7.0 holidays==0.14.2 # homeassistant.components.frontend -home-assistant-frontend==20220802.0 +home-assistant-frontend==20220816.0 # homeassistant.components.home_connect homeconnect==0.7.2 From 6f3cdb6db17f6736d9276bb15e0276ed4f417e66 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 16 Aug 2022 14:52:53 -1000 Subject: [PATCH 3397/3516] Reorganize bluetooth integration to prepare for remote and multi-adapter support (#76883) --- .../components/bluetooth/__init__.py | 443 ++---------------- homeassistant/components/bluetooth/const.py | 12 + homeassistant/components/bluetooth/manager.py | 393 ++++++++++++++++ homeassistant/components/bluetooth/models.py | 47 ++ tests/components/bluetooth/test_init.py | 100 ++-- .../test_passive_update_coordinator.py | 2 +- .../test_passive_update_processor.py | 2 +- tests/conftest.py | 4 +- 8 files changed, 542 insertions(+), 461 deletions(-) create mode 100644 homeassistant/components/bluetooth/manager.py diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 19df484c4e1..a90f367fc38 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -1,114 +1,51 @@ """The bluetooth integration.""" from __future__ import annotations -import asyncio from asyncio import Future from collections.abc import Callable -from dataclasses import dataclass -from datetime import datetime, timedelta -from enum import Enum -import logging -import time -from typing import TYPE_CHECKING, Final +from typing import TYPE_CHECKING import async_timeout -from bleak import BleakError -from dbus_next import InvalidMessageError from homeassistant import config_entries -from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import ( - CALLBACK_TYPE, - Event, - HomeAssistant, - callback as hass_callback, -) -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.core import HomeAssistant, callback as hass_callback from homeassistant.helpers import discovery_flow -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.loader import async_get_bluetooth -from homeassistant.util.package import is_docker_env -from . import models -from .const import CONF_ADAPTER, DEFAULT_ADAPTERS, DOMAIN -from .match import ( - ADDRESS, - BluetoothCallbackMatcher, - IntegrationMatcher, - ble_device_matches, +from .const import CONF_ADAPTER, DOMAIN, SOURCE_LOCAL +from .manager import BluetoothManager +from .match import BluetoothCallbackMatcher, IntegrationMatcher +from .models import ( + BluetoothCallback, + BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfo, + BluetoothServiceInfoBleak, + HaBleakScannerWrapper, + ProcessAdvertisementCallback, ) -from .models import HaBleakScanner, HaBleakScannerWrapper -from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher from .util import async_get_bluetooth_adapters if TYPE_CHECKING: from bleak.backends.device import BLEDevice - from bleak.backends.scanner import AdvertisementData from homeassistant.helpers.typing import ConfigType -_LOGGER = logging.getLogger(__name__) - - -UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 -START_TIMEOUT = 9 - -SOURCE_LOCAL: Final = "local" - -SCANNER_WATCHDOG_TIMEOUT: Final = 60 * 5 -SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=SCANNER_WATCHDOG_TIMEOUT) -MONOTONIC_TIME = time.monotonic - - -@dataclass -class BluetoothServiceInfoBleak(BluetoothServiceInfo): - """BluetoothServiceInfo with bleak data. - - Integrations may need BLEDevice and AdvertisementData - to connect to the device without having bleak trigger - another scan to translate the address to the system's - internal details. - """ - - device: BLEDevice - advertisement: AdvertisementData - - @classmethod - def from_advertisement( - cls, device: BLEDevice, advertisement_data: AdvertisementData, source: str - ) -> BluetoothServiceInfoBleak: - """Create a BluetoothServiceInfoBleak from an advertisement.""" - return cls( - name=advertisement_data.local_name or device.name or device.address, - address=device.address, - rssi=device.rssi, - manufacturer_data=advertisement_data.manufacturer_data, - service_data=advertisement_data.service_data, - service_uuids=advertisement_data.service_uuids, - source=source, - device=device, - advertisement=advertisement_data, - ) - - -class BluetoothScanningMode(Enum): - """The mode of scanning for bluetooth devices.""" - - PASSIVE = "passive" - ACTIVE = "active" - - -SCANNING_MODE_TO_BLEAK = { - BluetoothScanningMode.ACTIVE: "active", - BluetoothScanningMode.PASSIVE: "passive", -} - - -BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") -BluetoothCallback = Callable[[BluetoothServiceInfoBleak, BluetoothChange], None] -ProcessAdvertisementCallback = Callable[[BluetoothServiceInfoBleak], bool] +__all__ = [ + "async_ble_device_from_address", + "async_discovered_service_info", + "async_get_scanner", + "async_process_advertisements", + "async_rediscover_address", + "async_register_callback", + "async_track_unavailable", + "BluetoothServiceInfo", + "BluetoothServiceInfoBleak", + "BluetoothScanningMode", + "BluetoothCallback", + "SOURCE_LOCAL", +] @hass_callback @@ -287,329 +224,3 @@ async def async_unload_entry( manager.async_start_reload() await manager.async_stop() return True - - -class BluetoothManager: - """Manage Bluetooth.""" - - def __init__( - self, - hass: HomeAssistant, - integration_matcher: IntegrationMatcher, - ) -> None: - """Init bluetooth discovery.""" - self.hass = hass - self._integration_matcher = integration_matcher - self.scanner: HaBleakScanner | None = None - self.start_stop_lock = asyncio.Lock() - self._cancel_device_detected: CALLBACK_TYPE | None = None - self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None - self._cancel_stop: CALLBACK_TYPE | None = None - self._cancel_watchdog: CALLBACK_TYPE | None = None - self._unavailable_callbacks: dict[str, list[Callable[[str], None]]] = {} - self._callbacks: list[ - tuple[BluetoothCallback, BluetoothCallbackMatcher | None] - ] = [] - self._last_detection = 0.0 - self._reloading = False - self._adapter: str | None = None - self._scanning_mode = BluetoothScanningMode.ACTIVE - - @hass_callback - def async_setup(self) -> None: - """Set up the bluetooth manager.""" - models.HA_BLEAK_SCANNER = self.scanner = HaBleakScanner() - - @hass_callback - def async_get_scanner(self) -> HaBleakScannerWrapper: - """Get the scanner.""" - return HaBleakScannerWrapper() - - @hass_callback - def async_start_reload(self) -> None: - """Start reloading.""" - self._reloading = True - - async def async_start( - self, scanning_mode: BluetoothScanningMode, adapter: str | None - ) -> None: - """Set up BT Discovery.""" - assert self.scanner is not None - self._adapter = adapter - self._scanning_mode = scanning_mode - if self._reloading: - # On reload, we need to reset the scanner instance - # since the devices in its history may not be reachable - # anymore. - self.scanner.async_reset() - self._integration_matcher.async_clear_history() - self._reloading = False - scanner_kwargs = {"scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode]} - if adapter and adapter not in DEFAULT_ADAPTERS: - scanner_kwargs["adapter"] = adapter - _LOGGER.debug("Initializing bluetooth scanner with %s", scanner_kwargs) - try: - self.scanner.async_setup(**scanner_kwargs) - except (FileNotFoundError, BleakError) as ex: - raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex - install_multiple_bleak_catcher() - # We have to start it right away as some integrations might - # need it straight away. - _LOGGER.debug("Starting bluetooth scanner") - self.scanner.register_detection_callback(self.scanner.async_callback_dispatcher) - self._cancel_device_detected = self.scanner.async_register_callback( - self._device_detected, {} - ) - try: - async with async_timeout.timeout(START_TIMEOUT): - await self.scanner.start() # type: ignore[no-untyped-call] - except InvalidMessageError as ex: - self._async_cancel_scanner_callback() - _LOGGER.debug("Invalid DBus message received: %s", ex, exc_info=True) - raise ConfigEntryNotReady( - f"Invalid DBus message received: {ex}; try restarting `dbus`" - ) from ex - except BrokenPipeError as ex: - self._async_cancel_scanner_callback() - _LOGGER.debug("DBus connection broken: %s", ex, exc_info=True) - if is_docker_env(): - raise ConfigEntryNotReady( - f"DBus connection broken: {ex}; try restarting `bluetooth`, `dbus`, and finally the docker container" - ) from ex - raise ConfigEntryNotReady( - f"DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`" - ) from ex - except FileNotFoundError as ex: - self._async_cancel_scanner_callback() - _LOGGER.debug( - "FileNotFoundError while starting bluetooth: %s", ex, exc_info=True - ) - if is_docker_env(): - raise ConfigEntryNotReady( - f"DBus service not found; docker config may be missing `-v /run/dbus:/run/dbus:ro`: {ex}" - ) from ex - raise ConfigEntryNotReady( - f"DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}" - ) from ex - except asyncio.TimeoutError as ex: - self._async_cancel_scanner_callback() - raise ConfigEntryNotReady( - f"Timed out starting Bluetooth after {START_TIMEOUT} seconds" - ) from ex - except BleakError as ex: - self._async_cancel_scanner_callback() - _LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True) - raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex - self.async_setup_unavailable_tracking() - self._async_setup_scanner_watchdog() - self._cancel_stop = self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping - ) - - @hass_callback - def _async_setup_scanner_watchdog(self) -> None: - """If Dbus gets restarted or updated, we need to restart the scanner.""" - self._last_detection = MONOTONIC_TIME() - self._cancel_watchdog = async_track_time_interval( - self.hass, self._async_scanner_watchdog, SCANNER_WATCHDOG_INTERVAL - ) - - async def _async_scanner_watchdog(self, now: datetime) -> None: - """Check if the scanner is running.""" - time_since_last_detection = MONOTONIC_TIME() - self._last_detection - if time_since_last_detection < SCANNER_WATCHDOG_TIMEOUT: - return - _LOGGER.info( - "Bluetooth scanner has gone quiet for %s, restarting", - SCANNER_WATCHDOG_INTERVAL, - ) - async with self.start_stop_lock: - self.async_start_reload() - await self.async_stop() - await self.async_start(self._scanning_mode, self._adapter) - - @hass_callback - def async_setup_unavailable_tracking(self) -> None: - """Set up the unavailable tracking.""" - - @hass_callback - def _async_check_unavailable(now: datetime) -> None: - """Watch for unavailable devices.""" - scanner = self.scanner - assert scanner is not None - history = set(scanner.history) - active = {device.address for device in scanner.discovered_devices} - disappeared = history.difference(active) - for address in disappeared: - del scanner.history[address] - if not (callbacks := self._unavailable_callbacks.get(address)): - continue - for callback in callbacks: - try: - callback(address) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error in unavailable callback") - - self._cancel_unavailable_tracking = async_track_time_interval( - self.hass, - _async_check_unavailable, - timedelta(seconds=UNAVAILABLE_TRACK_SECONDS), - ) - - @hass_callback - def _device_detected( - self, device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: - """Handle a detected device.""" - self._last_detection = MONOTONIC_TIME() - matched_domains = self._integration_matcher.match_domains( - device, advertisement_data - ) - _LOGGER.debug( - "Device detected: %s with advertisement_data: %s matched domains: %s", - device.address, - advertisement_data, - matched_domains, - ) - - if not matched_domains and not self._callbacks: - return - - service_info: BluetoothServiceInfoBleak | None = None - for callback, matcher in self._callbacks: - if matcher is None or ble_device_matches( - matcher, device, advertisement_data - ): - if service_info is None: - service_info = BluetoothServiceInfoBleak.from_advertisement( - device, advertisement_data, SOURCE_LOCAL - ) - try: - callback(service_info, BluetoothChange.ADVERTISEMENT) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error in bluetooth callback") - - if not matched_domains: - return - if service_info is None: - service_info = BluetoothServiceInfoBleak.from_advertisement( - device, advertisement_data, SOURCE_LOCAL - ) - for domain in matched_domains: - discovery_flow.async_create_flow( - self.hass, - domain, - {"source": config_entries.SOURCE_BLUETOOTH}, - service_info, - ) - - @hass_callback - def async_track_unavailable( - self, callback: Callable[[str], None], address: str - ) -> Callable[[], None]: - """Register a callback.""" - self._unavailable_callbacks.setdefault(address, []).append(callback) - - @hass_callback - def _async_remove_callback() -> None: - self._unavailable_callbacks[address].remove(callback) - if not self._unavailable_callbacks[address]: - del self._unavailable_callbacks[address] - - return _async_remove_callback - - @hass_callback - def async_register_callback( - self, - callback: BluetoothCallback, - matcher: BluetoothCallbackMatcher | None = None, - ) -> Callable[[], None]: - """Register a callback.""" - callback_entry = (callback, matcher) - self._callbacks.append(callback_entry) - - @hass_callback - def _async_remove_callback() -> None: - self._callbacks.remove(callback_entry) - - # If we have history for the subscriber, we can trigger the callback - # immediately with the last packet so the subscriber can see the - # device. - if ( - matcher - and (address := matcher.get(ADDRESS)) - and self.scanner - and (device_adv_data := self.scanner.history.get(address)) - ): - try: - callback( - BluetoothServiceInfoBleak.from_advertisement( - *device_adv_data, SOURCE_LOCAL - ), - BluetoothChange.ADVERTISEMENT, - ) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error in bluetooth callback") - - return _async_remove_callback - - @hass_callback - def async_ble_device_from_address(self, address: str) -> BLEDevice | None: - """Return the BLEDevice if present.""" - if self.scanner and (ble_adv := self.scanner.history.get(address)): - return ble_adv[0] - return None - - @hass_callback - def async_address_present(self, address: str) -> bool: - """Return if the address is present.""" - return bool(self.scanner and address in self.scanner.history) - - @hass_callback - def async_discovered_service_info(self) -> list[BluetoothServiceInfoBleak]: - """Return if the address is present.""" - assert self.scanner is not None - return [ - BluetoothServiceInfoBleak.from_advertisement(*device_adv, SOURCE_LOCAL) - for device_adv in self.scanner.history.values() - ] - - async def _async_hass_stopping(self, event: Event) -> None: - """Stop the Bluetooth integration at shutdown.""" - self._cancel_stop = None - await self.async_stop() - - @hass_callback - def _async_cancel_scanner_callback(self) -> None: - """Cancel the scanner callback.""" - if self._cancel_device_detected: - self._cancel_device_detected() - self._cancel_device_detected = None - - async def async_stop(self) -> None: - """Stop bluetooth discovery.""" - _LOGGER.debug("Stopping bluetooth discovery") - if self._cancel_watchdog: - self._cancel_watchdog() - self._cancel_watchdog = None - self._async_cancel_scanner_callback() - if self._cancel_unavailable_tracking: - self._cancel_unavailable_tracking() - self._cancel_unavailable_tracking = None - if self._cancel_stop: - self._cancel_stop() - self._cancel_stop = None - if self.scanner: - try: - await self.scanner.stop() # type: ignore[no-untyped-call] - except BleakError as ex: - # This is not fatal, and they may want to reload - # the config entry to restart the scanner if they - # change the bluetooth dongle. - _LOGGER.error("Error stopping scanner: %s", ex) - uninstall_multiple_bleak_catcher() - - @hass_callback - def async_rediscover_address(self, address: str) -> None: - """Trigger discovery of devices which have already been seen.""" - self._integration_matcher.async_clear_address(address) diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index f3f00f581ee..fac191202b0 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -1,4 +1,8 @@ """Constants for the Bluetooth integration.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Final DOMAIN = "bluetooth" DEFAULT_NAME = "Bluetooth" @@ -9,3 +13,11 @@ MACOS_DEFAULT_BLUETOOTH_ADAPTER = "CoreBluetooth" UNIX_DEFAULT_BLUETOOTH_ADAPTER = "hci0" DEFAULT_ADAPTERS = {MACOS_DEFAULT_BLUETOOTH_ADAPTER, UNIX_DEFAULT_BLUETOOTH_ADAPTER} + +SOURCE_LOCAL: Final = "local" + + +UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 +START_TIMEOUT = 12 +SCANNER_WATCHDOG_TIMEOUT: Final = 60 * 5 +SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=SCANNER_WATCHDOG_TIMEOUT) diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py new file mode 100644 index 00000000000..e4f75350575 --- /dev/null +++ b/homeassistant/components/bluetooth/manager.py @@ -0,0 +1,393 @@ +"""The bluetooth integration.""" +from __future__ import annotations + +import asyncio +from collections.abc import Callable +from datetime import datetime, timedelta +import logging +import time +from typing import TYPE_CHECKING + +import async_timeout +from bleak import BleakError +from dbus_next import InvalidMessageError + +from homeassistant import config_entries +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HomeAssistant, + callback as hass_callback, +) +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import discovery_flow +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.package import is_docker_env + +from . import models +from .const import ( + DEFAULT_ADAPTERS, + SCANNER_WATCHDOG_INTERVAL, + SCANNER_WATCHDOG_TIMEOUT, + SOURCE_LOCAL, + START_TIMEOUT, + UNAVAILABLE_TRACK_SECONDS, +) +from .match import ( + ADDRESS, + BluetoothCallbackMatcher, + IntegrationMatcher, + ble_device_matches, +) +from .models import ( + BluetoothCallback, + BluetoothChange, + BluetoothScanningMode, + BluetoothServiceInfoBleak, + HaBleakScanner, + HaBleakScannerWrapper, +) +from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher + +if TYPE_CHECKING: + from bleak.backends.device import BLEDevice + from bleak.backends.scanner import AdvertisementData + + +_LOGGER = logging.getLogger(__name__) + + +MONOTONIC_TIME = time.monotonic + + +SCANNING_MODE_TO_BLEAK = { + BluetoothScanningMode.ACTIVE: "active", + BluetoothScanningMode.PASSIVE: "passive", +} + + +class BluetoothManager: + """Manage Bluetooth.""" + + def __init__( + self, + hass: HomeAssistant, + integration_matcher: IntegrationMatcher, + ) -> None: + """Init bluetooth discovery.""" + self.hass = hass + self._integration_matcher = integration_matcher + self.scanner: HaBleakScanner | None = None + self.start_stop_lock = asyncio.Lock() + self._cancel_device_detected: CALLBACK_TYPE | None = None + self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None + self._cancel_stop: CALLBACK_TYPE | None = None + self._cancel_watchdog: CALLBACK_TYPE | None = None + self._unavailable_callbacks: dict[str, list[Callable[[str], None]]] = {} + self._callbacks: list[ + tuple[BluetoothCallback, BluetoothCallbackMatcher | None] + ] = [] + self._last_detection = 0.0 + self._reloading = False + self._adapter: str | None = None + self._scanning_mode = BluetoothScanningMode.ACTIVE + + @hass_callback + def async_setup(self) -> None: + """Set up the bluetooth manager.""" + models.HA_BLEAK_SCANNER = self.scanner = HaBleakScanner() + + @hass_callback + def async_get_scanner(self) -> HaBleakScannerWrapper: + """Get the scanner.""" + return HaBleakScannerWrapper() + + @hass_callback + def async_start_reload(self) -> None: + """Start reloading.""" + self._reloading = True + + async def async_start( + self, scanning_mode: BluetoothScanningMode, adapter: str | None + ) -> None: + """Set up BT Discovery.""" + assert self.scanner is not None + self._adapter = adapter + self._scanning_mode = scanning_mode + if self._reloading: + # On reload, we need to reset the scanner instance + # since the devices in its history may not be reachable + # anymore. + self.scanner.async_reset() + self._integration_matcher.async_clear_history() + self._reloading = False + scanner_kwargs = {"scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode]} + if adapter and adapter not in DEFAULT_ADAPTERS: + scanner_kwargs["adapter"] = adapter + _LOGGER.debug("Initializing bluetooth scanner with %s", scanner_kwargs) + try: + self.scanner.async_setup(**scanner_kwargs) + except (FileNotFoundError, BleakError) as ex: + raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex + install_multiple_bleak_catcher() + # We have to start it right away as some integrations might + # need it straight away. + _LOGGER.debug("Starting bluetooth scanner") + self.scanner.register_detection_callback(self.scanner.async_callback_dispatcher) + self._cancel_device_detected = self.scanner.async_register_callback( + self._device_detected, {} + ) + try: + async with async_timeout.timeout(START_TIMEOUT): + await self.scanner.start() # type: ignore[no-untyped-call] + except InvalidMessageError as ex: + self._async_cancel_scanner_callback() + _LOGGER.debug("Invalid DBus message received: %s", ex, exc_info=True) + raise ConfigEntryNotReady( + f"Invalid DBus message received: {ex}; try restarting `dbus`" + ) from ex + except BrokenPipeError as ex: + self._async_cancel_scanner_callback() + _LOGGER.debug("DBus connection broken: %s", ex, exc_info=True) + if is_docker_env(): + raise ConfigEntryNotReady( + f"DBus connection broken: {ex}; try restarting `bluetooth`, `dbus`, and finally the docker container" + ) from ex + raise ConfigEntryNotReady( + f"DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`" + ) from ex + except FileNotFoundError as ex: + self._async_cancel_scanner_callback() + _LOGGER.debug( + "FileNotFoundError while starting bluetooth: %s", ex, exc_info=True + ) + if is_docker_env(): + raise ConfigEntryNotReady( + f"DBus service not found; docker config may be missing `-v /run/dbus:/run/dbus:ro`: {ex}" + ) from ex + raise ConfigEntryNotReady( + f"DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}" + ) from ex + except asyncio.TimeoutError as ex: + self._async_cancel_scanner_callback() + raise ConfigEntryNotReady( + f"Timed out starting Bluetooth after {START_TIMEOUT} seconds" + ) from ex + except BleakError as ex: + self._async_cancel_scanner_callback() + _LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True) + raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex + self.async_setup_unavailable_tracking() + self._async_setup_scanner_watchdog() + self._cancel_stop = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping + ) + + @hass_callback + def _async_setup_scanner_watchdog(self) -> None: + """If Dbus gets restarted or updated, we need to restart the scanner.""" + self._last_detection = MONOTONIC_TIME() + self._cancel_watchdog = async_track_time_interval( + self.hass, self._async_scanner_watchdog, SCANNER_WATCHDOG_INTERVAL + ) + + async def _async_scanner_watchdog(self, now: datetime) -> None: + """Check if the scanner is running.""" + time_since_last_detection = MONOTONIC_TIME() - self._last_detection + if time_since_last_detection < SCANNER_WATCHDOG_TIMEOUT: + return + _LOGGER.info( + "Bluetooth scanner has gone quiet for %s, restarting", + SCANNER_WATCHDOG_INTERVAL, + ) + async with self.start_stop_lock: + self.async_start_reload() + await self.async_stop() + await self.async_start(self._scanning_mode, self._adapter) + + @hass_callback + def async_setup_unavailable_tracking(self) -> None: + """Set up the unavailable tracking.""" + + @hass_callback + def _async_check_unavailable(now: datetime) -> None: + """Watch for unavailable devices.""" + scanner = self.scanner + assert scanner is not None + history = set(scanner.history) + active = {device.address for device in scanner.discovered_devices} + disappeared = history.difference(active) + for address in disappeared: + del scanner.history[address] + if not (callbacks := self._unavailable_callbacks.get(address)): + continue + for callback in callbacks: + try: + callback(address) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in unavailable callback") + + self._cancel_unavailable_tracking = async_track_time_interval( + self.hass, + _async_check_unavailable, + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS), + ) + + @hass_callback + def _device_detected( + self, device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Handle a detected device.""" + self._last_detection = MONOTONIC_TIME() + matched_domains = self._integration_matcher.match_domains( + device, advertisement_data + ) + _LOGGER.debug( + "Device detected: %s with advertisement_data: %s matched domains: %s", + device.address, + advertisement_data, + matched_domains, + ) + + if not matched_domains and not self._callbacks: + return + + service_info: BluetoothServiceInfoBleak | None = None + for callback, matcher in self._callbacks: + if matcher is None or ble_device_matches( + matcher, device, advertisement_data + ): + if service_info is None: + service_info = BluetoothServiceInfoBleak.from_advertisement( + device, advertisement_data, SOURCE_LOCAL + ) + try: + callback(service_info, BluetoothChange.ADVERTISEMENT) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in bluetooth callback") + + if not matched_domains: + return + if service_info is None: + service_info = BluetoothServiceInfoBleak.from_advertisement( + device, advertisement_data, SOURCE_LOCAL + ) + for domain in matched_domains: + discovery_flow.async_create_flow( + self.hass, + domain, + {"source": config_entries.SOURCE_BLUETOOTH}, + service_info, + ) + + @hass_callback + def async_track_unavailable( + self, callback: Callable[[str], None], address: str + ) -> Callable[[], None]: + """Register a callback.""" + self._unavailable_callbacks.setdefault(address, []).append(callback) + + @hass_callback + def _async_remove_callback() -> None: + self._unavailable_callbacks[address].remove(callback) + if not self._unavailable_callbacks[address]: + del self._unavailable_callbacks[address] + + return _async_remove_callback + + @hass_callback + def async_register_callback( + self, + callback: BluetoothCallback, + matcher: BluetoothCallbackMatcher | None = None, + ) -> Callable[[], None]: + """Register a callback.""" + callback_entry = (callback, matcher) + self._callbacks.append(callback_entry) + + @hass_callback + def _async_remove_callback() -> None: + self._callbacks.remove(callback_entry) + + # If we have history for the subscriber, we can trigger the callback + # immediately with the last packet so the subscriber can see the + # device. + if ( + matcher + and (address := matcher.get(ADDRESS)) + and self.scanner + and (device_adv_data := self.scanner.history.get(address)) + ): + try: + callback( + BluetoothServiceInfoBleak.from_advertisement( + *device_adv_data, SOURCE_LOCAL + ), + BluetoothChange.ADVERTISEMENT, + ) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in bluetooth callback") + + return _async_remove_callback + + @hass_callback + def async_ble_device_from_address(self, address: str) -> BLEDevice | None: + """Return the BLEDevice if present.""" + if self.scanner and (ble_adv := self.scanner.history.get(address)): + return ble_adv[0] + return None + + @hass_callback + def async_address_present(self, address: str) -> bool: + """Return if the address is present.""" + return bool(self.scanner and address in self.scanner.history) + + @hass_callback + def async_discovered_service_info(self) -> list[BluetoothServiceInfoBleak]: + """Return if the address is present.""" + assert self.scanner is not None + return [ + BluetoothServiceInfoBleak.from_advertisement(*device_adv, SOURCE_LOCAL) + for device_adv in self.scanner.history.values() + ] + + async def _async_hass_stopping(self, event: Event) -> None: + """Stop the Bluetooth integration at shutdown.""" + self._cancel_stop = None + await self.async_stop() + + @hass_callback + def _async_cancel_scanner_callback(self) -> None: + """Cancel the scanner callback.""" + if self._cancel_device_detected: + self._cancel_device_detected() + self._cancel_device_detected = None + + async def async_stop(self) -> None: + """Stop bluetooth discovery.""" + _LOGGER.debug("Stopping bluetooth discovery") + if self._cancel_watchdog: + self._cancel_watchdog() + self._cancel_watchdog = None + self._async_cancel_scanner_callback() + if self._cancel_unavailable_tracking: + self._cancel_unavailable_tracking() + self._cancel_unavailable_tracking = None + if self._cancel_stop: + self._cancel_stop() + self._cancel_stop = None + if self.scanner: + try: + await self.scanner.stop() # type: ignore[no-untyped-call] + except BleakError as ex: + # This is not fatal, and they may want to reload + # the config entry to restart the scanner if they + # change the bluetooth dongle. + _LOGGER.error("Error stopping scanner: %s", ex) + uninstall_multiple_bleak_catcher() + + @hass_callback + def async_rediscover_address(self, address: str) -> None: + """Trigger discovery of devices which have already been seen.""" + self._integration_matcher.async_clear_address(address) diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 51704a2f530..d5cb1429a2a 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -2,7 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import Callable import contextlib +from dataclasses import dataclass +from enum import Enum import logging from typing import TYPE_CHECKING, Any, Final @@ -14,6 +17,7 @@ from bleak.backends.scanner import ( ) from homeassistant.core import CALLBACK_TYPE, callback as hass_callback +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo if TYPE_CHECKING: from bleak.backends.device import BLEDevice @@ -26,6 +30,49 @@ FILTER_UUIDS: Final = "UUIDs" HA_BLEAK_SCANNER: HaBleakScanner | None = None +@dataclass +class BluetoothServiceInfoBleak(BluetoothServiceInfo): + """BluetoothServiceInfo with bleak data. + + Integrations may need BLEDevice and AdvertisementData + to connect to the device without having bleak trigger + another scan to translate the address to the system's + internal details. + """ + + device: BLEDevice + advertisement: AdvertisementData + + @classmethod + def from_advertisement( + cls, device: BLEDevice, advertisement_data: AdvertisementData, source: str + ) -> BluetoothServiceInfoBleak: + """Create a BluetoothServiceInfoBleak from an advertisement.""" + return cls( + name=advertisement_data.local_name or device.name or device.address, + address=device.address, + rssi=device.rssi, + manufacturer_data=advertisement_data.manufacturer_data, + service_data=advertisement_data.service_data, + service_uuids=advertisement_data.service_uuids, + source=source, + device=device, + advertisement=advertisement_data, + ) + + +class BluetoothScanningMode(Enum): + """The mode of scanning for bluetooth devices.""" + + PASSIVE = "passive" + ACTIVE = "active" + + +BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT") +BluetoothCallback = Callable[[BluetoothServiceInfoBleak, BluetoothChange], None] +ProcessAdvertisementCallback = Callable[[BluetoothServiceInfoBleak], bool] + + def _dispatch_callback( callback: AdvertisementDataCallback, filters: dict[str, set[str]], diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 796b3ffb469..2387d35fc23 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -10,20 +10,21 @@ import pytest from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( - SCANNER_WATCHDOG_INTERVAL, - SCANNER_WATCHDOG_TIMEOUT, - SOURCE_LOCAL, - UNAVAILABLE_TRACK_SECONDS, BluetoothChange, BluetoothScanningMode, BluetoothServiceInfo, async_process_advertisements, async_rediscover_address, async_track_unavailable, + manager, models, ) from homeassistant.components.bluetooth.const import ( CONF_ADAPTER, + SCANNER_WATCHDOG_INTERVAL, + SCANNER_WATCHDOG_TIMEOUT, + SOURCE_LOCAL, + UNAVAILABLE_TRACK_SECONDS, UNIX_DEFAULT_BLUETOOTH_ADAPTER, ) from homeassistant.config_entries import ConfigEntryState @@ -62,7 +63,7 @@ async def test_setup_and_stop_no_bluetooth(hass, caplog): {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} ] with patch( - "homeassistant.components.bluetooth.HaBleakScanner.async_setup", + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup", side_effect=BleakError, ) as mock_ha_bleak_scanner, patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -83,8 +84,10 @@ async def test_setup_and_stop_no_bluetooth(hass, caplog): async def test_setup_and_stop_broken_bluetooth(hass, caplog): """Test we fail gracefully when bluetooth/dbus is broken.""" mock_bt = [] - with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + with patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.start", side_effect=BleakError, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -109,10 +112,10 @@ async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): async def _mock_hang(): await asyncio.sleep(1) - with patch.object(bluetooth, "START_TIMEOUT", 0), patch( - "homeassistant.components.bluetooth.HaBleakScanner.async_setup" + with patch.object(manager, "START_TIMEOUT", 0), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" ), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + "homeassistant.components.bluetooth.models.HaBleakScanner.start", side_effect=_mock_hang, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -132,8 +135,10 @@ async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): """Test we retry if the adapter is not yet available.""" mock_bt = [] - with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + with patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.start", side_effect=BleakError, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -152,14 +157,14 @@ async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): assert entry.state == ConfigEntryState.SETUP_RETRY with patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + "homeassistant.components.bluetooth.models.HaBleakScanner.start", ): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) await hass.async_block_till_done() assert entry.state == ConfigEntryState.LOADED with patch( - "homeassistant.components.bluetooth.HaBleakScanner.stop", + "homeassistant.components.bluetooth.models.HaBleakScanner.stop", ): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() @@ -168,8 +173,10 @@ async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): """Test we can successfully reload when the entry is in a retry state.""" mock_bt = [] - with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + with patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.start", side_effect=BleakError, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -188,7 +195,7 @@ async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): assert entry.state == ConfigEntryState.SETUP_RETRY with patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + "homeassistant.components.bluetooth.models.HaBleakScanner.start", ): await hass.config_entries.async_reload(entry.entry_id) await hass.async_block_till_done() @@ -196,7 +203,7 @@ async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): assert entry.state == ConfigEntryState.LOADED with patch( - "homeassistant.components.bluetooth.HaBleakScanner.stop", + "homeassistant.components.bluetooth.models.HaBleakScanner.stop", ): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() @@ -206,7 +213,7 @@ async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): """Test we fail gracefully when asking for discovered devices and there is no blueooth.""" mock_bt = [] with patch( - "homeassistant.components.bluetooth.HaBleakScanner.async_setup", + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup", side_effect=FileNotFoundError, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -1514,7 +1521,8 @@ async def test_config_entry_can_be_reloaded_when_stop_raises( assert entry.state == ConfigEntryState.LOADED with patch( - "homeassistant.components.bluetooth.HaBleakScanner.stop", side_effect=BleakError + "homeassistant.components.bluetooth.models.HaBleakScanner.stop", + side_effect=BleakError, ): await hass.config_entries.async_reload(entry.entry_id) await hass.async_block_till_done() @@ -1533,11 +1541,11 @@ async def test_changing_the_adapter_at_runtime(hass): entry.add_to_hass(hass) with patch( - "homeassistant.components.bluetooth.HaBleakScanner.async_setup" + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" ) as mock_setup, patch( - "homeassistant.components.bluetooth.HaBleakScanner.start" + "homeassistant.components.bluetooth.models.HaBleakScanner.start" ), patch( - "homeassistant.components.bluetooth.HaBleakScanner.stop" + "homeassistant.components.bluetooth.models.HaBleakScanner.stop" ): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -1558,9 +1566,11 @@ async def test_dbus_socket_missing_in_container(hass, caplog): """Test we handle dbus being missing in the container.""" with patch( - "homeassistant.components.bluetooth.is_docker_env", return_value=True - ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + "homeassistant.components.bluetooth.manager.is_docker_env", return_value=True + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.start", side_effect=FileNotFoundError, ): assert await async_setup_component( @@ -1580,9 +1590,11 @@ async def test_dbus_socket_missing(hass, caplog): """Test we handle dbus being missing.""" with patch( - "homeassistant.components.bluetooth.is_docker_env", return_value=False - ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + "homeassistant.components.bluetooth.manager.is_docker_env", return_value=False + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.start", side_effect=FileNotFoundError, ): assert await async_setup_component( @@ -1602,9 +1614,11 @@ async def test_dbus_broken_pipe_in_container(hass, caplog): """Test we handle dbus broken pipe in the container.""" with patch( - "homeassistant.components.bluetooth.is_docker_env", return_value=True - ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + "homeassistant.components.bluetooth.manager.is_docker_env", return_value=True + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.start", side_effect=BrokenPipeError, ): assert await async_setup_component( @@ -1625,9 +1639,11 @@ async def test_dbus_broken_pipe(hass, caplog): """Test we handle dbus broken pipe.""" with patch( - "homeassistant.components.bluetooth.is_docker_env", return_value=False - ), patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + "homeassistant.components.bluetooth.manager.is_docker_env", return_value=False + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.start", side_effect=BrokenPipeError, ): assert await async_setup_component( @@ -1647,8 +1663,10 @@ async def test_dbus_broken_pipe(hass, caplog): async def test_invalid_dbus_message(hass, caplog): """Test we handle invalid dbus message.""" - with patch("homeassistant.components.bluetooth.HaBleakScanner.async_setup"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + with patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" + ), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.start", side_effect=InvalidMessageError, ): assert await async_setup_component( @@ -1678,7 +1696,7 @@ async def test_recovery_from_dbus_restart( # Ensure we don't restart the scanner if we don't need to with patch( - "homeassistant.components.bluetooth.MONOTONIC_TIME", + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", return_value=start_time_monotonic + 10, ): async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) @@ -1688,7 +1706,7 @@ async def test_recovery_from_dbus_restart( # Fire a callback to reset the timer with patch( - "homeassistant.components.bluetooth.MONOTONIC_TIME", + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", return_value=start_time_monotonic, ): scanner._callback( @@ -1698,7 +1716,7 @@ async def test_recovery_from_dbus_restart( # Ensure we don't restart the scanner if we don't need to with patch( - "homeassistant.components.bluetooth.MONOTONIC_TIME", + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", return_value=start_time_monotonic + 20, ): async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) @@ -1708,7 +1726,7 @@ async def test_recovery_from_dbus_restart( # We hit the timer, so we restart the scanner with patch( - "homeassistant.components.bluetooth.MONOTONIC_TIME", + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", return_value=start_time_monotonic + SCANNER_WATCHDOG_TIMEOUT, ): async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 31530cd6995..12531c52e40 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -8,10 +8,10 @@ from unittest.mock import MagicMock, patch from homeassistant.components.bluetooth import ( DOMAIN, - UNAVAILABLE_TRACK_SECONDS, BluetoothChange, BluetoothScanningMode, ) +from homeassistant.components.bluetooth.const import UNAVAILABLE_TRACK_SECONDS from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, PassiveBluetoothDataUpdateCoordinator, diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 5653b938ada..6b21d1aa32c 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -14,10 +14,10 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.components.bluetooth import ( DOMAIN, - UNAVAILABLE_TRACK_SECONDS, BluetoothChange, BluetoothScanningMode, ) +from homeassistant.components.bluetooth.const import UNAVAILABLE_TRACK_SECONDS from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothDataProcessor, PassiveBluetoothDataUpdate, diff --git a/tests/conftest.py b/tests/conftest.py index 50c24df8d44..3b43fcd14ba 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -904,8 +904,8 @@ def mock_bleak_scanner_start(): scanner = bleak.BleakScanner bluetooth_models.HA_BLEAK_SCANNER = None - with patch("homeassistant.components.bluetooth.HaBleakScanner.stop"), patch( - "homeassistant.components.bluetooth.HaBleakScanner.start", + with patch("homeassistant.components.bluetooth.models.HaBleakScanner.stop"), patch( + "homeassistant.components.bluetooth.models.HaBleakScanner.start", ) as mock_bleak_scanner_start: yield mock_bleak_scanner_start From ec1b133201a4301f0179d1c10bdac9e1eafab16c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 17 Aug 2022 03:19:23 +0200 Subject: [PATCH 3398/3516] Add DHCP updates to Fully Kiosk (#76896) --- .../components/fully_kiosk/config_flow.py | 24 +++++++- .../components/fully_kiosk/entity.py | 2 + .../components/fully_kiosk/manifest.json | 3 +- homeassistant/generated/dhcp.py | 1 + tests/components/fully_kiosk/conftest.py | 9 ++- .../fully_kiosk/test_config_flow.py | 55 ++++++++++++++++++- 6 files changed, 87 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fully_kiosk/config_flow.py b/homeassistant/components/fully_kiosk/config_flow.py index 09eb94d6b07..5257030ecf0 100644 --- a/homeassistant/components/fully_kiosk/config_flow.py +++ b/homeassistant/components/fully_kiosk/config_flow.py @@ -11,9 +11,11 @@ from fullykiosk.exceptions import FullyKioskError import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.components.dhcp import DhcpServiceInfo +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import format_mac from .const import DEFAULT_PORT, DOMAIN, LOGGER @@ -48,7 +50,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(device_info["deviceID"]) self._abort_if_unique_id_configured(updates=user_input) return self.async_create_entry( - title=device_info["deviceName"], data=user_input + title=device_info["deviceName"], + data=user_input | {CONF_MAC: format_mac(device_info["Mac"])}, ) return self.async_show_form( @@ -61,3 +64,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ), errors=errors, ) + + async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult: + """Handle dhcp discovery.""" + mac = format_mac(discovery_info.macaddress) + + for entry in self._async_current_entries(): + if entry.data[CONF_MAC] == mac: + self.hass.config_entries.async_update_entry( + entry, + data=entry.data | {CONF_HOST: discovery_info.ip}, + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(entry.entry_id) + ) + return self.async_abort(reason="already_configured") + + return self.async_abort(reason="unknown") diff --git a/homeassistant/components/fully_kiosk/entity.py b/homeassistant/components/fully_kiosk/entity.py index 4e50bb6efe6..7be06c79573 100644 --- a/homeassistant/components/fully_kiosk/entity.py +++ b/homeassistant/components/fully_kiosk/entity.py @@ -1,6 +1,7 @@ """Base entity for the Fully Kiosk Browser integration.""" from __future__ import annotations +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -23,4 +24,5 @@ class FullyKioskEntity(CoordinatorEntity[FullyKioskDataUpdateCoordinator], Entit model=coordinator.data["deviceModel"], sw_version=coordinator.data["appVersionName"], configuration_url=f"http://{coordinator.data['ip4']}:2323", + connections={(CONNECTION_NETWORK_MAC, coordinator.data["Mac"])}, ) diff --git a/homeassistant/components/fully_kiosk/manifest.json b/homeassistant/components/fully_kiosk/manifest.json index 40c7e5293e7..8918ce28062 100644 --- a/homeassistant/components/fully_kiosk/manifest.json +++ b/homeassistant/components/fully_kiosk/manifest.json @@ -6,5 +6,6 @@ "requirements": ["python-fullykiosk==0.0.11"], "dependencies": [], "codeowners": ["@cgarwood"], - "iot_class": "local_polling" + "iot_class": "local_polling", + "dhcp": [{ "registered_devices": true }] } diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 9179a314215..841db03c3a6 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -43,6 +43,7 @@ DHCP: list[dict[str, str | bool]] = [ {'domain': 'flux_led', 'hostname': 'zengge_[0-9a-f][0-9a-f]_*'}, {'domain': 'flux_led', 'hostname': 'sta*', 'macaddress': 'C82E47*'}, {'domain': 'fronius', 'macaddress': '0003AC*'}, + {'domain': 'fully_kiosk', 'registered_devices': True}, {'domain': 'goalzero', 'registered_devices': True}, {'domain': 'goalzero', 'hostname': 'yeti*'}, {'domain': 'gogogate2', 'hostname': 'ismartgate*'}, diff --git a/tests/components/fully_kiosk/conftest.py b/tests/components/fully_kiosk/conftest.py index c5476ea6a9d..35d5c12e694 100644 --- a/tests/components/fully_kiosk/conftest.py +++ b/tests/components/fully_kiosk/conftest.py @@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest from homeassistant.components.fully_kiosk.const import DOMAIN -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -20,7 +20,11 @@ def mock_config_entry() -> MockConfigEntry: return MockConfigEntry( title="Test device", domain=DOMAIN, - data={CONF_HOST: "127.0.0.1", CONF_PASSWORD: "mocked-password"}, + data={ + CONF_HOST: "127.0.0.1", + CONF_PASSWORD: "mocked-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", + }, unique_id="12345", ) @@ -45,6 +49,7 @@ def mock_fully_kiosk_config_flow() -> Generator[MagicMock, None, None]: client.getDeviceInfo.return_value = { "deviceName": "Test device", "deviceID": "12345", + "Mac": "AA:BB:CC:DD:EE:FF", } yield client diff --git a/tests/components/fully_kiosk/test_config_flow.py b/tests/components/fully_kiosk/test_config_flow.py index 2617a3f7adb..19a8715b4cd 100644 --- a/tests/components/fully_kiosk/test_config_flow.py +++ b/tests/components/fully_kiosk/test_config_flow.py @@ -7,9 +7,10 @@ from aiohttp.client_exceptions import ClientConnectorError from fullykiosk import FullyKioskError import pytest +from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.components.fully_kiosk.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -42,6 +43,7 @@ async def test_full_flow( assert result2.get("data") == { CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", } assert "result" in result2 assert result2["result"].unique_id == "12345" @@ -95,6 +97,7 @@ async def test_errors( assert result3.get("data") == { CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", } assert "result" in result3 assert result3["result"].unique_id == "12345" @@ -131,6 +134,54 @@ async def test_duplicate_updates_existing_entry( assert mock_config_entry.data == { CONF_HOST: "1.1.1.1", CONF_PASSWORD: "test-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", } assert len(mock_fully_kiosk_config_flow.getDeviceInfo.mock_calls) == 1 + + +async def test_dhcp_discovery_updates_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test DHCP discovery updates config entries.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="tablet", + ip="127.0.0.2", + macaddress="aa:bb:cc:dd:ee:ff", + ), + ) + + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "already_configured" + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.2", + CONF_PASSWORD: "mocked-password", + CONF_MAC: "aa:bb:cc:dd:ee:ff", + } + + +async def test_dhcp_unknown_device( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test unknown DHCP discovery aborts flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + hostname="tablet", + ip="127.0.0.2", + macaddress="aa:bb:cc:dd:ee:00", + ), + ) + + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "unknown" From d73754d292136d7daf952fec731583a6e83833eb Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 17 Aug 2022 03:19:55 +0200 Subject: [PATCH 3399/3516] Fix TypeAlias + TypeVar names (#76897) --- homeassistant/components/samsungtv/bridge.py | 14 +++++++------- homeassistant/components/zamg/sensor.py | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index fe0b102647a..f1618bfba14 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -74,8 +74,8 @@ ENCRYPTED_MODEL_USES_POWER = {"JU6400", "JU641D"} REST_EXCEPTIONS = (HttpApiError, AsyncioTimeoutError, ResponseError) -_TRemote = TypeVar("_TRemote", SamsungTVWSAsyncRemote, SamsungTVEncryptedWSAsyncRemote) -_TCommand = TypeVar("_TCommand", SamsungTVCommand, SamsungTVEncryptedCommand) +_RemoteT = TypeVar("_RemoteT", SamsungTVWSAsyncRemote, SamsungTVEncryptedWSAsyncRemote) +_CommandT = TypeVar("_CommandT", SamsungTVCommand, SamsungTVEncryptedCommand) def mac_from_device_info(info: dict[str, Any]) -> str | None: @@ -367,7 +367,7 @@ class SamsungTVLegacyBridge(SamsungTVBridge): LOGGER.debug("Could not establish connection") -class SamsungTVWSBaseBridge(SamsungTVBridge, Generic[_TRemote, _TCommand]): +class SamsungTVWSBaseBridge(SamsungTVBridge, Generic[_RemoteT, _CommandT]): """The Bridge for WebSocket TVs (v1/v2).""" def __init__( @@ -379,7 +379,7 @@ class SamsungTVWSBaseBridge(SamsungTVBridge, Generic[_TRemote, _TCommand]): ) -> None: """Initialize Bridge.""" super().__init__(hass, method, host, port) - self._remote: _TRemote | None = None + self._remote: _RemoteT | None = None self._remote_lock = asyncio.Lock() async def async_is_on(self) -> bool: @@ -389,7 +389,7 @@ class SamsungTVWSBaseBridge(SamsungTVBridge, Generic[_TRemote, _TCommand]): return remote.is_alive() # type: ignore[no-any-return] return False - async def _async_send_commands(self, commands: list[_TCommand]) -> None: + async def _async_send_commands(self, commands: list[_CommandT]) -> None: """Send the commands using websocket protocol.""" try: # recreate connection if connection was dead @@ -410,7 +410,7 @@ class SamsungTVWSBaseBridge(SamsungTVBridge, Generic[_TRemote, _TCommand]): # Different reasons, e.g. hostname not resolveable pass - async def _async_get_remote(self) -> _TRemote | None: + async def _async_get_remote(self) -> _RemoteT | None: """Create or return a remote control instance.""" if (remote := self._remote) and remote.is_alive(): # If we have one then try to use it @@ -422,7 +422,7 @@ class SamsungTVWSBaseBridge(SamsungTVBridge, Generic[_TRemote, _TCommand]): return await self._async_get_remote_under_lock() @abstractmethod - async def _async_get_remote_under_lock(self) -> _TRemote | None: + async def _async_get_remote_under_lock(self) -> _RemoteT | None: """Create or return a remote control instance.""" async def async_close_remote(self) -> None: diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 8452841520b..e8d5f745cce 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -51,7 +51,7 @@ DEFAULT_NAME = "zamg" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) VIENNA_TIME_ZONE = dt_util.get_time_zone("Europe/Vienna") -DTypeT = Union[type[int], type[float], type[str]] +_DType = Union[type[int], type[float], type[str]] @dataclass @@ -59,7 +59,7 @@ class ZamgRequiredKeysMixin: """Mixin for required keys.""" col_heading: str - dtype: DTypeT + dtype: _DType @dataclass @@ -178,7 +178,7 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] -API_FIELDS: dict[str, tuple[str, DTypeT]] = { +API_FIELDS: dict[str, tuple[str, _DType]] = { desc.col_heading: (desc.key, desc.dtype) for desc in SENSOR_TYPES } From 7a82279af80fea5216f7678726a53a24b873f756 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 17 Aug 2022 03:20:47 +0200 Subject: [PATCH 3400/3516] Update hass-nabucasa to 0.55.0 (#76892) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 4987169d280..02ffa0a4775 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.54.1"], + "requirements": ["hass-nabucasa==0.55.0"], "dependencies": ["http", "webhook"], "after_dependencies": ["google_assistant", "alexa"], "codeowners": ["@home-assistant/cloud"], diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index eaf463df32d..d29a186f445 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ certifi>=2021.5.30 ciso8601==2.2.0 cryptography==36.0.2 fnvhash==0.1.0 -hass-nabucasa==0.54.1 +hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 home-assistant-frontend==20220816.0 httpx==0.23.0 diff --git a/requirements_all.txt b/requirements_all.txt index c4fdeec309b..c28a923b82d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -803,7 +803,7 @@ habitipy==0.2.0 hangups==0.4.18 # homeassistant.components.cloud -hass-nabucasa==0.54.1 +hass-nabucasa==0.55.0 # homeassistant.components.splunk hass_splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fca44241201..bdd90857d01 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -592,7 +592,7 @@ habitipy==0.2.0 hangups==0.4.18 # homeassistant.components.cloud -hass-nabucasa==0.54.1 +hass-nabucasa==0.55.0 # homeassistant.components.tasmota hatasmota==0.5.1 From ee1b08bbd6af8a4267c97376bf2119b992fd1e82 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 16 Aug 2022 15:21:47 -1000 Subject: [PATCH 3401/3516] Bump govee-ble to 0.16.0 (#76882) --- homeassistant/components/govee_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/govee_ble/test_sensor.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index 7c65fe35b5d..d31abe48cae 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -36,7 +36,7 @@ "service_uuid": "00008251-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["govee-ble==0.14.1"], + "requirements": ["govee-ble==0.16.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index c28a923b82d..5aeb4d57d1b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -757,7 +757,7 @@ googlemaps==2.5.1 goslide-api==0.5.1 # homeassistant.components.govee_ble -govee-ble==0.14.1 +govee-ble==0.16.0 # homeassistant.components.remote_rpi_gpio gpiozero==1.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bdd90857d01..c6659b8c5b5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -558,7 +558,7 @@ google-nest-sdm==2.0.0 googlemaps==2.5.1 # homeassistant.components.govee_ble -govee-ble==0.14.1 +govee-ble==0.16.0 # homeassistant.components.gree greeclimate==1.3.0 diff --git a/tests/components/govee_ble/test_sensor.py b/tests/components/govee_ble/test_sensor.py index 75d269ea0ba..da67d32e681 100644 --- a/tests/components/govee_ble/test_sensor.py +++ b/tests/components/govee_ble/test_sensor.py @@ -41,7 +41,7 @@ async def test_sensors(hass): temp_sensor = hass.states.get("sensor.h5075_2762_temperature") temp_sensor_attribtes = temp_sensor.attributes - assert temp_sensor.state == "21.3442" + assert temp_sensor.state == "21.34" assert temp_sensor_attribtes[ATTR_FRIENDLY_NAME] == "H5075_2762 Temperature" assert temp_sensor_attribtes[ATTR_UNIT_OF_MEASUREMENT] == "°C" assert temp_sensor_attribtes[ATTR_STATE_CLASS] == "measurement" From 7e366a78e62725e415d371209d0e790503837289 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 17 Aug 2022 02:36:56 -0400 Subject: [PATCH 3402/3516] Add Fully Kiosk Browser button platform (#76894) Co-authored-by: Franck Nijhof --- .../components/fully_kiosk/__init__.py | 2 +- .../components/fully_kiosk/button.py | 105 ++++++++++++++++++ tests/components/fully_kiosk/test_button.py | 69 ++++++++++++ 3 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/fully_kiosk/button.py create mode 100644 tests/components/fully_kiosk/test_button.py diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py index a4c71168f4e..311dae20082 100644 --- a/homeassistant/components/fully_kiosk/__init__.py +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import FullyKioskDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/fully_kiosk/button.py b/homeassistant/components/fully_kiosk/button.py new file mode 100644 index 00000000000..387ee638547 --- /dev/null +++ b/homeassistant/components/fully_kiosk/button.py @@ -0,0 +1,105 @@ +"""Fully Kiosk Browser button.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from fullykiosk import FullyKiosk + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import FullyKioskDataUpdateCoordinator +from .entity import FullyKioskEntity + + +@dataclass +class FullyButtonEntityDescriptionMixin: + """Mixin to describe a Fully Kiosk Browser button entity.""" + + press_action: Callable[[FullyKiosk], Any] + + +@dataclass +class FullyButtonEntityDescription( + ButtonEntityDescription, FullyButtonEntityDescriptionMixin +): + """Fully Kiosk Browser button description.""" + + +BUTTONS: tuple[FullyButtonEntityDescription, ...] = ( + FullyButtonEntityDescription( + key="restartApp", + name="Restart browser", + device_class=ButtonDeviceClass.RESTART, + entity_category=EntityCategory.CONFIG, + press_action=lambda fully: fully.restartApp(), + ), + FullyButtonEntityDescription( + key="rebootDevice", + name="Reboot device", + device_class=ButtonDeviceClass.RESTART, + entity_category=EntityCategory.CONFIG, + press_action=lambda fully: fully.rebootDevice(), + ), + FullyButtonEntityDescription( + key="toForeground", + name="Bring to foreground", + press_action=lambda fully: fully.toForeground(), + ), + FullyButtonEntityDescription( + key="toBackground", + name="Send to background", + press_action=lambda fully: fully.toBackground(), + ), + FullyButtonEntityDescription( + key="loadStartUrl", + name="Load start URL", + press_action=lambda fully: fully.loadStartUrl(), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Fully Kiosk Browser button entities.""" + coordinator: FullyKioskDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + async_add_entities( + FullyButtonEntity(coordinator, description) for description in BUTTONS + ) + + +class FullyButtonEntity(FullyKioskEntity, ButtonEntity): + """Representation of a Fully Kiosk Browser button.""" + + entity_description: FullyButtonEntityDescription + + def __init__( + self, + coordinator: FullyKioskDataUpdateCoordinator, + description: FullyButtonEntityDescription, + ) -> None: + """Initialize the button.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.data['deviceID']}-{description.key}" + + async def async_press(self) -> None: + """Set the value of the entity.""" + await self.entity_description.press_action(self.coordinator.fully) + await self.coordinator.async_refresh() diff --git a/tests/components/fully_kiosk/test_button.py b/tests/components/fully_kiosk/test_button.py new file mode 100644 index 00000000000..7183fc3db92 --- /dev/null +++ b/tests/components/fully_kiosk/test_button.py @@ -0,0 +1,69 @@ +"""Test the Fully Kiosk Browser buttons.""" +from unittest.mock import MagicMock + +import homeassistant.components.button as button +from homeassistant.components.fully_kiosk.const import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_binary_sensors( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test standard Fully Kiosk binary sensors.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + entry = entity_registry.async_get("button.amazon_fire_restart_browser") + assert entry + assert entry.unique_id == "abcdef-123456-restartApp" + await call_service(hass, "press", "button.amazon_fire_restart_browser") + assert len(mock_fully_kiosk.restartApp.mock_calls) == 1 + + entry = entity_registry.async_get("button.amazon_fire_reboot_device") + assert entry + assert entry.unique_id == "abcdef-123456-rebootDevice" + await call_service(hass, "press", "button.amazon_fire_reboot_device") + assert len(mock_fully_kiosk.rebootDevice.mock_calls) == 1 + + entry = entity_registry.async_get("button.amazon_fire_bring_to_foreground") + assert entry + assert entry.unique_id == "abcdef-123456-toForeground" + await call_service(hass, "press", "button.amazon_fire_bring_to_foreground") + assert len(mock_fully_kiosk.toForeground.mock_calls) == 1 + + entry = entity_registry.async_get("button.amazon_fire_send_to_background") + assert entry + assert entry.unique_id == "abcdef-123456-toBackground" + await call_service(hass, "press", "button.amazon_fire_send_to_background") + assert len(mock_fully_kiosk.toBackground.mock_calls) == 1 + + entry = entity_registry.async_get("button.amazon_fire_load_start_url") + assert entry + assert entry.unique_id == "abcdef-123456-loadStartUrl" + await call_service(hass, "press", "button.amazon_fire_load_start_url") + assert len(mock_fully_kiosk.loadStartUrl.mock_calls) == 1 + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.configuration_url == "http://192.168.1.234:2323" + assert device_entry.entry_type is None + assert device_entry.hw_version is None + assert device_entry.identifiers == {(DOMAIN, "abcdef-123456")} + assert device_entry.manufacturer == "amzn" + assert device_entry.model == "KFDOWI" + assert device_entry.name == "Amazon Fire" + assert device_entry.sw_version == "1.42.5" + + +def call_service(hass, service, entity_id): + """Call any service on entity.""" + return hass.services.async_call( + button.DOMAIN, service, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) From 5ef6b5a3003b8fd5d4821f2e2ca1192fe5b21f9f Mon Sep 17 00:00:00 2001 From: mkmer Date: Wed, 17 Aug 2022 03:09:19 -0400 Subject: [PATCH 3403/3516] Add BLE sensor to Aladdin_connect (#76221) * Add BLE sensor Default Enable BLE & Battery for Model 02 * recommended changes * Recommended changes Model 02 -> 01 (oops) 2x async_block_till_done() not needed. --- .../components/aladdin_connect/model.py | 1 + .../components/aladdin_connect/sensor.py | 15 +++- tests/components/aladdin_connect/conftest.py | 3 + .../components/aladdin_connect/test_sensor.py | 81 ++++++++++++++++++- 4 files changed, 98 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aladdin_connect/model.py b/homeassistant/components/aladdin_connect/model.py index 63624b223a9..9b250459d3b 100644 --- a/homeassistant/components/aladdin_connect/model.py +++ b/homeassistant/components/aladdin_connect/model.py @@ -12,3 +12,4 @@ class DoorDevice(TypedDict): name: str status: str serial: str + model: str diff --git a/homeassistant/components/aladdin_connect/sensor.py b/homeassistant/components/aladdin_connect/sensor.py index 68631c57fc8..3d319a724c1 100644 --- a/homeassistant/components/aladdin_connect/sensor.py +++ b/homeassistant/components/aladdin_connect/sensor.py @@ -56,6 +56,15 @@ SENSORS: tuple[AccSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, value_fn=AladdinConnectClient.get_rssi_status, ), + AccSensorEntityDescription( + key="ble_strength", + name="BLE Strength", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_registry_enabled_default=False, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + value_fn=AladdinConnectClient.get_ble_strength, + ), ) @@ -89,14 +98,17 @@ class AladdinConnectSensor(SensorEntity): device: DoorDevice, description: AccSensorEntityDescription, ) -> None: - """Initialize a sensor for an Abode device.""" + """Initialize a sensor for an Aladdin Connect device.""" self._device_id = device["device_id"] self._number = device["door_number"] self._name = device["name"] + self._model = device["model"] self._acc = acc self.entity_description = description self._attr_unique_id = f"{self._device_id}-{self._number}-{description.key}" self._attr_has_entity_name = True + if self._model == "01" and description.key in ("battery_level", "ble_strength"): + self._attr_entity_registry_enabled_default = True @property def device_info(self) -> DeviceInfo | None: @@ -105,6 +117,7 @@ class AladdinConnectSensor(SensorEntity): identifiers={(DOMAIN, self._device_id)}, name=self._name, manufacturer="Overhead Door", + model=self._model, ) @property diff --git a/tests/components/aladdin_connect/conftest.py b/tests/components/aladdin_connect/conftest.py index c8f7d240ba5..ee9afed9823 100644 --- a/tests/components/aladdin_connect/conftest.py +++ b/tests/components/aladdin_connect/conftest.py @@ -11,6 +11,7 @@ DEVICE_CONFIG_OPEN = { "status": "open", "link_status": "Connected", "serial": "12345", + "model": "02", } @@ -31,6 +32,8 @@ def fixture_mock_aladdinconnect_api(): mock_opener.get_battery_status.return_value = "99" mock_opener.async_get_rssi_status = AsyncMock(return_value="-55") mock_opener.get_rssi_status.return_value = "-55" + mock_opener.async_get_ble_strength = AsyncMock(return_value="-45") + mock_opener.get_ble_strength.return_value = "-45" mock_opener.get_doors = AsyncMock(return_value=[DEVICE_CONFIG_OPEN]) mock_opener.register_callback = mock.Mock(return_value=True) diff --git a/tests/components/aladdin_connect/test_sensor.py b/tests/components/aladdin_connect/test_sensor.py index 3702bcd9efa..282f6d3e04c 100644 --- a/tests/components/aladdin_connect/test_sensor.py +++ b/tests/components/aladdin_connect/test_sensor.py @@ -1,6 +1,6 @@ """Test the Aladdin Connect Sensors.""" from datetime import timedelta -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from homeassistant.components.aladdin_connect.const import DOMAIN from homeassistant.components.aladdin_connect.cover import SCAN_INTERVAL @@ -10,6 +10,17 @@ from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry, async_fire_time_changed +DEVICE_CONFIG_MODEL_01 = { + "device_id": 533255, + "door_number": 1, + "name": "home", + "status": "closed", + "link_status": "Connected", + "serial": "12345", + "model": "01", +} + + CONFIG = {"username": "test-user", "password": "test-password"} RELOAD_AFTER_UPDATE_DELAY = timedelta(seconds=31) @@ -83,3 +94,71 @@ async def test_sensors( state = hass.states.get("sensor.home_wi_fi_rssi") assert state + + +async def test_sensors_model_01( + hass: HomeAssistant, + mock_aladdinconnect_api: MagicMock, +) -> None: + """Test Sensors for AladdinConnect.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=CONFIG, + unique_id="test-id", + ) + config_entry.add_to_hass(hass) + + await hass.async_block_till_done() + + with patch( + "homeassistant.components.aladdin_connect.AladdinConnectClient", + return_value=mock_aladdinconnect_api, + ): + mock_aladdinconnect_api.get_doors = AsyncMock( + return_value=[DEVICE_CONFIG_MODEL_01] + ) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + registry = entity_registry.async_get(hass) + entry = registry.async_get("sensor.home_battery_level") + assert entry + assert entry.disabled is False + assert entry.disabled_by is None + state = hass.states.get("sensor.home_battery_level") + assert state + + entry = registry.async_get("sensor.home_wi_fi_rssi") + await hass.async_block_till_done() + assert entry + assert entry.disabled + assert entry.disabled_by is entity_registry.RegistryEntryDisabler.INTEGRATION + update_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert update_entry != entry + assert update_entry.disabled is False + state = hass.states.get("sensor.home_wi_fi_rssi") + assert state is None + + update_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + async_fire_time_changed( + hass, + utcnow() + SCAN_INTERVAL, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.home_wi_fi_rssi") + assert state + + entry = registry.async_get("sensor.home_ble_strength") + await hass.async_block_till_done() + assert entry + assert entry.disabled is False + assert entry.disabled_by is None + state = hass.states.get("sensor.home_ble_strength") + assert state From b4323108b139680e44e7b5c5ff3d02d92443fee4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 17 Aug 2022 09:41:50 +0200 Subject: [PATCH 3404/3516] Update cryptography to 37.0.4 (#76853) --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d29a186f445..8381e204a77 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ bleak==0.15.1 bluetooth-adapters==0.1.3 certifi>=2021.5.30 ciso8601==2.2.0 -cryptography==36.0.2 +cryptography==37.0.4 fnvhash==0.1.0 hass-nabucasa==0.55.0 home-assistant-bluetooth==1.3.0 diff --git a/pyproject.toml b/pyproject.toml index c0d96aa5ee0..3297cb39db2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ dependencies = [ "lru-dict==1.1.8", "PyJWT==2.4.0", # PyJWT has loose dependency. We want the latest one. - "cryptography==36.0.2", + "cryptography==37.0.4", "orjson==3.7.11", "pip>=21.0,<22.3", "python-slugify==4.0.1", diff --git a/requirements.txt b/requirements.txt index fce57620224..f190aa50233 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 PyJWT==2.4.0 -cryptography==36.0.2 +cryptography==37.0.4 orjson==3.7.11 pip>=21.0,<22.3 python-slugify==4.0.1 From 0ed265e2be5436b56dffda5a43b070ca4f0f4207 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Aug 2022 10:53:05 +0200 Subject: [PATCH 3405/3516] Correct restoring of mobile_app sensors (#76886) --- .../components/mobile_app/binary_sensor.py | 5 +- homeassistant/components/mobile_app/entity.py | 5 +- homeassistant/components/mobile_app/sensor.py | 28 ++- tests/components/mobile_app/test_sensor.py | 160 ++++++++++++++++-- 4 files changed, 172 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index fd8545b1f98..69ecb913c98 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -75,9 +75,8 @@ class MobileAppBinarySensor(MobileAppEntity, BinarySensorEntity): """Return the state of the binary sensor.""" return self._config[ATTR_SENSOR_STATE] - @callback - def async_restore_last_state(self, last_state): + async def async_restore_last_state(self, last_state): """Restore previous state.""" - super().async_restore_last_state(last_state) + await super().async_restore_last_state(last_state) self._config[ATTR_SENSOR_STATE] = last_state.state == STATE_ON diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index d4c4374b8d9..3a2f038a0af 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -43,10 +43,9 @@ class MobileAppEntity(RestoreEntity): if (state := await self.async_get_last_state()) is None: return - self.async_restore_last_state(state) + await self.async_restore_last_state(state) - @callback - def async_restore_last_state(self, last_state): + async def async_restore_last_state(self, last_state): """Restore previous state.""" self._config[ATTR_SENSOR_STATE] = last_state.state self._config[ATTR_SENSOR_ATTRIBUTES] = { diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index d7cfc9545f6..ef7dd122496 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -3,9 +3,9 @@ from __future__ import annotations from typing import Any -from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.components.sensor import RestoreSensor, SensorDeviceClass from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_WEBHOOK_ID, STATE_UNKNOWN +from homeassistant.const import CONF_WEBHOOK_ID, STATE_UNKNOWN, TEMP_CELSIUS from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -27,6 +27,7 @@ from .const import ( DOMAIN, ) from .entity import MobileAppEntity +from .webhook import _extract_sensor_unique_id async def async_setup_entry( @@ -73,9 +74,30 @@ async def async_setup_entry( ) -class MobileAppSensor(MobileAppEntity, SensorEntity): +class MobileAppSensor(MobileAppEntity, RestoreSensor): """Representation of an mobile app sensor.""" + async def async_restore_last_state(self, last_state): + """Restore previous state.""" + + await super().async_restore_last_state(last_state) + + if not (last_sensor_data := await self.async_get_last_sensor_data()): + # Workaround to handle migration to RestoreSensor, can be removed + # in HA Core 2023.4 + self._config[ATTR_SENSOR_STATE] = None + webhook_id = self._entry.data[CONF_WEBHOOK_ID] + sensor_unique_id = _extract_sensor_unique_id(webhook_id, self.unique_id) + if ( + self.device_class == SensorDeviceClass.TEMPERATURE + and sensor_unique_id == "battery_temperature" + ): + self._config[ATTR_SENSOR_UOM] = TEMP_CELSIUS + return + + self._config[ATTR_SENSOR_STATE] = last_sensor_data.native_value + self._config[ATTR_SENSOR_UOM] = last_sensor_data.native_unit_of_measurement + @property def native_value(self): """Return the state of the sensor.""" diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index c0f7f126a49..930fb522c4c 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -1,15 +1,34 @@ """Entity tests for mobile_app.""" from http import HTTPStatus +from unittest.mock import patch import pytest from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.const import PERCENTAGE, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import ( + PERCENTAGE, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -async def test_sensor(hass, create_registrations, webhook_client): +@pytest.mark.parametrize( + "unit_system, state_unit, state1, state2", + ( + (METRIC_SYSTEM, TEMP_CELSIUS, "100", "123"), + (IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, "212", "253"), + ), +) +async def test_sensor( + hass, create_registrations, webhook_client, unit_system, state_unit, state1, state2 +): """Test that sensors can be registered and updated.""" + hass.config.units = unit_system + webhook_id = create_registrations[1]["webhook_id"] webhook_url = f"/api/webhook/{webhook_id}" @@ -19,15 +38,15 @@ async def test_sensor(hass, create_registrations, webhook_client): "type": "register_sensor", "data": { "attributes": {"foo": "bar"}, - "device_class": "battery", + "device_class": "temperature", "icon": "mdi:battery", - "name": "Battery State", + "name": "Battery Temperature", "state": 100, "type": "sensor", "entity_category": "diagnostic", - "unique_id": "battery_state", + "unique_id": "battery_temp", "state_class": "total", - "unit_of_measurement": PERCENTAGE, + "unit_of_measurement": TEMP_CELSIUS, }, }, ) @@ -38,20 +57,23 @@ async def test_sensor(hass, create_registrations, webhook_client): assert json == {"success": True} await hass.async_block_till_done() - entity = hass.states.get("sensor.test_1_battery_state") + entity = hass.states.get("sensor.test_1_battery_temperature") assert entity is not None - assert entity.attributes["device_class"] == "battery" + assert entity.attributes["device_class"] == "temperature" assert entity.attributes["icon"] == "mdi:battery" - assert entity.attributes["unit_of_measurement"] == PERCENTAGE + # unit of temperature sensor is automatically converted to the system UoM + assert entity.attributes["unit_of_measurement"] == state_unit assert entity.attributes["foo"] == "bar" assert entity.attributes["state_class"] == "total" assert entity.domain == "sensor" - assert entity.name == "Test 1 Battery State" - assert entity.state == "100" + assert entity.name == "Test 1 Battery Temperature" + assert entity.state == state1 assert ( - er.async_get(hass).async_get("sensor.test_1_battery_state").entity_category + er.async_get(hass) + .async_get("sensor.test_1_battery_temperature") + .entity_category == "diagnostic" ) @@ -64,7 +86,7 @@ async def test_sensor(hass, create_registrations, webhook_client): "icon": "mdi:battery-unknown", "state": 123, "type": "sensor", - "unique_id": "battery_state", + "unique_id": "battery_temp", }, # This invalid data should not invalidate whole request {"type": "sensor", "unique_id": "invalid_state", "invalid": "data"}, @@ -77,8 +99,8 @@ async def test_sensor(hass, create_registrations, webhook_client): json = await update_resp.json() assert json["invalid_state"]["success"] is False - updated_entity = hass.states.get("sensor.test_1_battery_state") - assert updated_entity.state == "123" + updated_entity = hass.states.get("sensor.test_1_battery_temperature") + assert updated_entity.state == state2 assert "foo" not in updated_entity.attributes dev_reg = dr.async_get(hass) @@ -88,16 +110,120 @@ async def test_sensor(hass, create_registrations, webhook_client): config_entry = hass.config_entries.async_entries("mobile_app")[1] await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - unloaded_entity = hass.states.get("sensor.test_1_battery_state") + unloaded_entity = hass.states.get("sensor.test_1_battery_temperature") assert unloaded_entity.state == STATE_UNAVAILABLE await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - restored_entity = hass.states.get("sensor.test_1_battery_state") + restored_entity = hass.states.get("sensor.test_1_battery_temperature") assert restored_entity.state == updated_entity.state assert restored_entity.attributes == updated_entity.attributes +@pytest.mark.parametrize( + "unique_id, unit_system, state_unit, state1, state2", + ( + ("battery_temperature", METRIC_SYSTEM, TEMP_CELSIUS, "100", "123"), + ("battery_temperature", IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, "212", "253"), + # The unique_id doesn't match that of the mobile app's battery temperature sensor + ("battery_temp", IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, "212", "123"), + ), +) +async def test_sensor_migration( + hass, + create_registrations, + webhook_client, + unique_id, + unit_system, + state_unit, + state1, + state2, +): + """Test migration to RestoreSensor.""" + hass.config.units = unit_system + + webhook_id = create_registrations[1]["webhook_id"] + webhook_url = f"/api/webhook/{webhook_id}" + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "attributes": {"foo": "bar"}, + "device_class": "temperature", + "icon": "mdi:battery", + "name": "Battery Temperature", + "state": 100, + "type": "sensor", + "entity_category": "diagnostic", + "unique_id": unique_id, + "state_class": "total", + "unit_of_measurement": TEMP_CELSIUS, + }, + }, + ) + + assert reg_resp.status == HTTPStatus.CREATED + + json = await reg_resp.json() + assert json == {"success": True} + await hass.async_block_till_done() + + entity = hass.states.get("sensor.test_1_battery_temperature") + assert entity is not None + + assert entity.attributes["device_class"] == "temperature" + assert entity.attributes["icon"] == "mdi:battery" + # unit of temperature sensor is automatically converted to the system UoM + assert entity.attributes["unit_of_measurement"] == state_unit + assert entity.attributes["foo"] == "bar" + assert entity.attributes["state_class"] == "total" + assert entity.domain == "sensor" + assert entity.name == "Test 1 Battery Temperature" + assert entity.state == state1 + + # Reload to verify state is restored + config_entry = hass.config_entries.async_entries("mobile_app")[1] + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + unloaded_entity = hass.states.get("sensor.test_1_battery_temperature") + assert unloaded_entity.state == STATE_UNAVAILABLE + + # Simulate migration to RestoreSensor + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_extra_data", + return_value=None, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + restored_entity = hass.states.get("sensor.test_1_battery_temperature") + assert restored_entity.state == "unknown" + assert restored_entity.attributes == entity.attributes + + # Test unit conversion is working + update_resp = await webhook_client.post( + webhook_url, + json={ + "type": "update_sensor_states", + "data": [ + { + "icon": "mdi:battery-unknown", + "state": 123, + "type": "sensor", + "unique_id": unique_id, + }, + ], + }, + ) + + assert update_resp.status == HTTPStatus.OK + + updated_entity = hass.states.get("sensor.test_1_battery_temperature") + assert updated_entity.state == state2 + assert "foo" not in updated_entity.attributes + + async def test_sensor_must_register(hass, create_registrations, webhook_client): """Test that sensors must be registered before updating.""" webhook_id = create_registrations[1]["webhook_id"] From 4cc1428eea98efc961a124be88e4ab4604ca0ee2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Aug 2022 13:07:50 +0200 Subject: [PATCH 3406/3516] Add support for color_mode white to MQTT JSON light (#76918) --- .../components/mqtt/light/schema_json.py | 28 ++- tests/components/mqtt/test_light_json.py | 174 ++++++++++++++++-- 2 files changed, 188 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 659dd212b51..910d48f750d 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -15,6 +15,7 @@ from homeassistant.components.light import ( ATTR_RGBW_COLOR, ATTR_RGBWW_COLOR, ATTR_TRANSITION, + ATTR_WHITE, ATTR_XY_COLOR, ENTITY_ID_FORMAT, FLASH_LONG, @@ -61,7 +62,11 @@ from ..debug_info import log_messages from ..mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity from ..util import valid_subscribe_topic from .schema import MQTT_LIGHT_SCHEMA_SCHEMA -from .schema_basic import CONF_BRIGHTNESS_SCALE, MQTT_LIGHT_ATTRIBUTES_BLOCKED +from .schema_basic import ( + CONF_BRIGHTNESS_SCALE, + CONF_WHITE_SCALE, + MQTT_LIGHT_ATTRIBUTES_BLOCKED, +) _LOGGER = logging.getLogger(__name__) @@ -79,6 +84,7 @@ DEFAULT_RGB = False DEFAULT_XY = False DEFAULT_HS = False DEFAULT_BRIGHTNESS_SCALE = 255 +DEFAULT_WHITE_SCALE = 255 CONF_COLOR_MODE = "color_mode" CONF_SUPPORTED_COLOR_MODES = "supported_color_modes" @@ -136,6 +142,9 @@ _PLATFORM_SCHEMA_BASE = ( vol.Unique(), valid_supported_color_modes, ), + vol.Optional(CONF_WHITE_SCALE, default=DEFAULT_WHITE_SCALE): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean, }, ) @@ -294,6 +303,8 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): w = int(values["color"]["w"]) # pylint: disable=invalid-name self._color_mode = ColorMode.RGBWW self._rgbww = (r, g, b, c, w) + elif color_mode == ColorMode.WHITE: + self._color_mode = ColorMode.WHITE elif color_mode == ColorMode.XY: x = float(values["color"]["x"]) # pylint: disable=invalid-name y = float(values["color"]["y"]) # pylint: disable=invalid-name @@ -498,7 +509,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): def _supports_color_mode(self, color_mode): return self.supported_color_modes and color_mode in self.supported_color_modes - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): # noqa: C901 """Turn the device on. This method is a coroutine. @@ -613,6 +624,19 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): self._effect = kwargs[ATTR_EFFECT] should_update = True + if ATTR_WHITE in kwargs and self._supports_color_mode(ColorMode.WHITE): + white_normalized = kwargs[ATTR_WHITE] / DEFAULT_WHITE_SCALE + white_scale = self._config[CONF_WHITE_SCALE] + device_white_level = min(round(white_normalized * white_scale), white_scale) + # Make sure the brightness is not rounded down to 0 + device_white_level = max(device_white_level, 1) + message["white"] = device_white_level + + if self._optimistic: + self._color_mode = ColorMode.WHITE + self._brightness = kwargs[ATTR_WHITE] + should_update = True + await self.async_publish( self._topic[CONF_COMMAND_TOPIC], json_dumps(message), diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index d57fc1cceee..89e966112c1 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -383,7 +383,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi assert light_state.attributes["brightness"] == 100 async_fire_mqtt_message( - hass, "test_light_rgb", '{"state":"ON", ' '"color":{"r":125,"g":125,"b":125}}' + hass, "test_light_rgb", '{"state":"ON", "color":{"r":125,"g":125,"b":125}}' ) light_state = hass.states.get("light.test") @@ -430,7 +430,7 @@ async def test_controlling_state_via_topic2( hass, mqtt_mock_entry_with_yaml_config, caplog ): """Test the controlling of the state via topic for a light supporting color mode.""" - supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] + supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "white", "xy"] assert await async_setup_component( hass, @@ -560,6 +560,17 @@ async def test_controlling_state_via_topic2( assert state.attributes.get("color_mode") == "color_temp" assert state.attributes.get("color_temp") == 155 + # White + async_fire_mqtt_message( + hass, + "test_light_rgb", + '{"state":"ON", "color_mode":"white", "brightness":123}', + ) + state = hass.states.get("light.test") + assert state.attributes.get("color_mode") == "white" + assert state.attributes.get("brightness") == 123 + + # Effect async_fire_mqtt_message( hass, "test_light_rgb", '{"state":"ON", "effect":"other_effect"}' ) @@ -731,7 +742,7 @@ async def test_sending_mqtt_commands_and_optimistic2( hass, mqtt_mock_entry_with_yaml_config ): """Test the sending of command in optimistic mode for a light supporting color mode.""" - supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] + supported_color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "white", "xy"] fake_state = ha.State( "light.test", "on", @@ -788,6 +799,7 @@ async def test_sending_mqtt_commands_and_optimistic2( assert state.attributes.get("rgbw_color") is None assert state.attributes.get("rgbww_color") is None assert state.attributes.get("supported_color_modes") == supported_color_modes + assert state.attributes.get("white") is None assert state.attributes.get("xy_color") is None assert state.attributes.get(ATTR_ASSUMED_STATE) @@ -835,7 +847,7 @@ async def test_sending_mqtt_commands_and_optimistic2( mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", JsonValidator( - '{"state": "ON", "color": {"h": 359.0, "s": 78.0},' ' "brightness": 75}' + '{"state": "ON", "color": {"h": 359.0, "s": 78.0}, "brightness": 75}' ), 2, False, @@ -919,13 +931,51 @@ async def test_sending_mqtt_commands_and_optimistic2( mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", JsonValidator( - '{"state": "ON", "color": {"x": 0.123, "y": 0.223},' ' "brightness": 50}' + '{"state": "ON", "color": {"x": 0.123, "y": 0.223}, "brightness": 50}' ), 2, False, ) mqtt_mock.async_publish.reset_mock() + # Set to white + await common.async_turn_on(hass, "light.test", white=75) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes["brightness"] == 75 + assert state.attributes["color_mode"] == "white" + assert "hs_color" not in state.attributes + assert "rgb_color" not in state.attributes + assert "xy_color" not in state.attributes + assert "rgbw_color" not in state.attributes + assert "rgbww_color" not in state.attributes + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", + JsonValidator('{"state": "ON", "white": 75}'), + 2, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Set to white, brightness also present in turn_on + await common.async_turn_on(hass, "light.test", brightness=60, white=80) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes["brightness"] == 60 + assert state.attributes["color_mode"] == "white" + assert "hs_color" not in state.attributes + assert "rgb_color" not in state.attributes + assert "xy_color" not in state.attributes + assert "rgbw_color" not in state.attributes + assert "rgbww_color" not in state.attributes + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", + JsonValidator('{"state": "ON", "white": 60}'), + 2, + False, + ) + mqtt_mock.async_publish.reset_mock() + async def test_sending_hs_color(hass, mqtt_mock_entry_with_yaml_config): """Test light.turn_on with hs color sends hs color parameters.""" @@ -1254,6 +1304,50 @@ async def test_sending_rgb_color_with_scaled_brightness( ) +async def test_sending_scaled_white(hass, mqtt_mock_entry_with_yaml_config): + """Test light.turn_on with scaled white.""" + assert await async_setup_component( + hass, + light.DOMAIN, + { + light.DOMAIN: { + "platform": "mqtt", + "schema": "json", + "name": "test", + "command_topic": "test_light_rgb/set", + "brightness": True, + "brightness_scale": 100, + "color_mode": True, + "supported_color_modes": ["hs", "white"], + "white_scale": 50, + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get("light.test") + assert state.state == STATE_UNKNOWN + + await common.async_turn_on(hass, "light.test", brightness=128) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", JsonValidator('{"state":"ON", "brightness":50}'), 0, False + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255, white=25) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", JsonValidator('{"state":"ON", "white":50}'), 0, False + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", white=25) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", JsonValidator('{"state":"ON", "white":5}'), 0, False + ) + mqtt_mock.async_publish.reset_mock() + + async def test_sending_xy_color(hass, mqtt_mock_entry_with_yaml_config): """Test light.turn_on with hs color sends xy color parameters.""" assert await async_setup_component( @@ -1527,6 +1621,62 @@ async def test_brightness_scale(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("brightness") == 255 +async def test_white_scale(hass, mqtt_mock_entry_with_yaml_config): + """Test for white scaling.""" + assert await async_setup_component( + hass, + light.DOMAIN, + { + light.DOMAIN: { + "platform": "mqtt", + "schema": "json", + "name": "test", + "state_topic": "test_light_bright_scale", + "command_topic": "test_light_bright_scale/set", + "brightness": True, + "brightness_scale": 99, + "color_mode": True, + "supported_color_modes": ["hs", "white"], + "white_scale": 50, + } + }, + ) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get("light.test") + assert state.state == STATE_UNKNOWN + assert state.attributes.get("brightness") is None + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + # Turn on the light + async_fire_mqtt_message(hass, "test_light_bright_scale", '{"state":"ON"}') + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") is None + + # Turn on the light with brightness + async_fire_mqtt_message( + hass, "test_light_bright_scale", '{"state":"ON", "brightness": 99}' + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 255 + + # Turn on the light with white - white_scale is NOT used + async_fire_mqtt_message( + hass, + "test_light_bright_scale", + '{"state":"ON", "color_mode":"white", "brightness": 50}', + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 128 + + async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): """Test that invalid color/brightness/etc. values are ignored.""" assert await async_setup_component( @@ -1585,7 +1735,7 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): async_fire_mqtt_message( hass, "test_light_rgb", - '{"state":"ON",' '"color":{}}', + '{"state":"ON", "color":{}}', ) # Color should not have changed @@ -1597,7 +1747,7 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): async_fire_mqtt_message( hass, "test_light_rgb", - '{"state":"ON",' '"color":{"h":"bad","s":"val"}}', + '{"state":"ON", "color":{"h":"bad","s":"val"}}', ) # Color should not have changed @@ -1609,7 +1759,7 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): async_fire_mqtt_message( hass, "test_light_rgb", - '{"state":"ON",' '"color":{"r":"bad","g":"val","b":"test"}}', + '{"state":"ON", "color":{"r":"bad","g":"val","b":"test"}}', ) # Color should not have changed @@ -1621,7 +1771,7 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): async_fire_mqtt_message( hass, "test_light_rgb", - '{"state":"ON",' '"color":{"x":"bad","y":"val"}}', + '{"state":"ON", "color":{"x":"bad","y":"val"}}', ) # Color should not have changed @@ -1631,7 +1781,7 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): # Bad brightness values async_fire_mqtt_message( - hass, "test_light_rgb", '{"state":"ON",' '"brightness": "badValue"}' + hass, "test_light_rgb", '{"state":"ON", "brightness": "badValue"}' ) # Brightness should not have changed @@ -1641,7 +1791,7 @@ async def test_invalid_values(hass, mqtt_mock_entry_with_yaml_config): # Bad color temperature async_fire_mqtt_message( - hass, "test_light_rgb", '{"state":"ON",' '"color_temp": "badValue"}' + hass, "test_light_rgb", '{"state":"ON", "color_temp": "badValue"}' ) # Color temperature should not have changed @@ -1767,7 +1917,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): async def test_discovery_removal(hass, mqtt_mock_entry_no_yaml_config, caplog): """Test removal of discovered mqtt_json lights.""" - data = '{ "name": "test",' ' "schema": "json",' ' "command_topic": "test_topic" }' + data = '{ "name": "test", "schema": "json", "command_topic": "test_topic" }' await help_test_discovery_removal( hass, mqtt_mock_entry_no_yaml_config, From 426a620084c75e98804ea03266b86041819f62ed Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Aug 2022 13:53:56 +0200 Subject: [PATCH 3407/3516] Remove deprecated white_value support from template light (#76923) --- homeassistant/components/template/light.py | 68 +--------------- tests/components/template/test_light.py | 93 ---------------------- 2 files changed, 2 insertions(+), 159 deletions(-) diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index f4ad971c1d6..f10e6c2ea09 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -12,12 +12,10 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, ENTITY_ID_FORMAT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - SUPPORT_WHITE_VALUE, LightEntity, LightEntityFeature, ) @@ -88,16 +86,14 @@ LIGHT_SCHEMA = vol.All( vol.Optional(CONF_TEMPERATURE_TEMPLATE): cv.template, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_WHITE_VALUE_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template, } ).extend(TEMPLATE_ENTITY_COMMON_SCHEMA_LEGACY.schema), ) PLATFORM_SCHEMA = vol.All( # CONF_WHITE_VALUE_* is deprecated, support will be removed in release 2022.9 - cv.deprecated(CONF_WHITE_VALUE_ACTION), - cv.deprecated(CONF_WHITE_VALUE_TEMPLATE), + cv.removed(CONF_WHITE_VALUE_ACTION), + cv.removed(CONF_WHITE_VALUE_TEMPLATE), PLATFORM_SCHEMA.extend( {vol.Required(CONF_LIGHTS): cv.schema_with_slug_keys(LIGHT_SCHEMA)} ), @@ -171,12 +167,6 @@ class LightTemplate(TemplateEntity, LightEntity): if (color_action := config.get(CONF_COLOR_ACTION)) is not None: self._color_script = Script(hass, color_action, friendly_name, DOMAIN) self._color_template = config.get(CONF_COLOR_TEMPLATE) - self._white_value_script = None - if (white_value_action := config.get(CONF_WHITE_VALUE_ACTION)) is not None: - self._white_value_script = Script( - hass, white_value_action, friendly_name, DOMAIN - ) - self._white_value_template = config.get(CONF_WHITE_VALUE_TEMPLATE) self._effect_script = None if (effect_action := config.get(CONF_EFFECT_ACTION)) is not None: self._effect_script = Script(hass, effect_action, friendly_name, DOMAIN) @@ -190,7 +180,6 @@ class LightTemplate(TemplateEntity, LightEntity): self._brightness = None self._temperature = None self._color = None - self._white_value = None self._effect = None self._effect_list = None self._max_mireds = None @@ -223,11 +212,6 @@ class LightTemplate(TemplateEntity, LightEntity): return super().min_mireds - @property - def white_value(self) -> int | None: - """Return the white value.""" - return self._white_value - @property def hs_color(self) -> tuple[float, float] | None: """Return the hue and saturation color value [float, float].""" @@ -253,8 +237,6 @@ class LightTemplate(TemplateEntity, LightEntity): supported_features |= SUPPORT_COLOR_TEMP if self._color_script is not None: supported_features |= SUPPORT_COLOR - if self._white_value_script is not None: - supported_features |= SUPPORT_WHITE_VALUE if self._effect_script is not None: supported_features |= LightEntityFeature.EFFECT if self._supports_transition is True: @@ -312,14 +294,6 @@ class LightTemplate(TemplateEntity, LightEntity): self._update_color, none_on_template_error=True, ) - if self._white_value_template: - self.add_template_attribute( - "_white_value", - self._white_value_template, - None, - self._update_white_value, - none_on_template_error=True, - ) if self._effect_list_template: self.add_template_attribute( "_effect_list", @@ -361,13 +335,6 @@ class LightTemplate(TemplateEntity, LightEntity): self._brightness = kwargs[ATTR_BRIGHTNESS] optimistic_set = True - if self._white_value_template is None and ATTR_WHITE_VALUE in kwargs: - _LOGGER.debug( - "Optimistically setting white value to %s", kwargs[ATTR_WHITE_VALUE] - ) - self._white_value = kwargs[ATTR_WHITE_VALUE] - optimistic_set = True - if self._temperature_template is None and ATTR_COLOR_TEMP in kwargs: _LOGGER.debug( "Optimistically setting color temperature to %s", @@ -404,14 +371,6 @@ class LightTemplate(TemplateEntity, LightEntity): run_variables=common_params, context=self._context, ) - elif ATTR_WHITE_VALUE in kwargs and self._white_value_script: - common_params["white_value"] = kwargs[ATTR_WHITE_VALUE] - - await self.async_run_script( - self._white_value_script, - run_variables=common_params, - context=self._context, - ) elif ATTR_EFFECT in kwargs and self._effect_script: effect = kwargs[ATTR_EFFECT] if effect not in self._effect_list: @@ -486,29 +445,6 @@ class LightTemplate(TemplateEntity, LightEntity): ) self._brightness = None - @callback - def _update_white_value(self, white_value): - """Update the white value from the template.""" - try: - if white_value in (None, "None", ""): - self._white_value = None - return - if 0 <= int(white_value) <= 255: - self._white_value = int(white_value) - else: - _LOGGER.error( - "Received invalid white value: %s for entity %s. Expected: 0-255", - white_value, - self.entity_id, - ) - self._white_value = None - except ValueError: - _LOGGER.error( - "Template must supply an integer white_value from 0-255, or 'None'", - exc_info=True, - ) - self._white_value = None - @callback def _update_effect_list(self, effect_list): """Update the effect list from the template.""" diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index ca03c6f5d82..9b0ad613120 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -9,12 +9,10 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, - SUPPORT_WHITE_VALUE, ColorMode, LightEntityFeature, ) @@ -616,97 +614,6 @@ async def test_off_action_optimistic( assert state.attributes["supported_features"] == supported_features -@pytest.mark.parametrize("count", [1]) -@pytest.mark.parametrize( - "supported_features,supported_color_modes,expected_color_mode", - [(SUPPORT_WHITE_VALUE, [ColorMode.RGBW], ColorMode.UNKNOWN)], -) -@pytest.mark.parametrize( - "light_config", - [ - { - "test_template_light": { - **OPTIMISTIC_WHITE_VALUE_LIGHT_CONFIG, - "value_template": "{{1 == 1}}", - } - }, - ], -) -async def test_white_value_action_no_template( - hass, - setup_light, - calls, - supported_color_modes, - supported_features, - expected_color_mode, -): - """Test setting white value with optimistic template.""" - state = hass.states.get("light.test_template_light") - assert state.attributes.get("white_value") is None - - await hass.services.async_call( - light.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.test_template_light", ATTR_WHITE_VALUE: 124}, - blocking=True, - ) - - assert len(calls) == 1 - assert calls[-1].data["action"] == "set_white_value" - assert calls[-1].data["caller"] == "light.test_template_light" - assert calls[-1].data["white_value"] == 124 - - state = hass.states.get("light.test_template_light") - assert state.attributes.get("white_value") == 124 - assert state.state == STATE_ON - assert state.attributes["color_mode"] == expected_color_mode # hs_color is None - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features - - -@pytest.mark.parametrize("count", [1]) -@pytest.mark.parametrize( - "supported_features,supported_color_modes,expected_color_mode", - [(SUPPORT_WHITE_VALUE, [ColorMode.RGBW], ColorMode.UNKNOWN)], -) -@pytest.mark.parametrize( - "expected_white_value,white_value_template", - [ - (255, "{{255}}"), - (None, "{{256}}"), - (None, "{{x-12}}"), - (None, "{{ none }}"), - (None, ""), - ], -) -async def test_white_value_template( - hass, - expected_white_value, - supported_features, - supported_color_modes, - expected_color_mode, - count, - white_value_template, -): - """Test the template for the white value.""" - light_config = { - "test_template_light": { - **OPTIMISTIC_WHITE_VALUE_LIGHT_CONFIG, - "value_template": "{{ 1 == 1 }}", - "white_value_template": white_value_template, - } - } - await async_setup_light(hass, count, light_config) - - state = hass.states.get("light.test_template_light") - assert state is not None - assert state.attributes.get("white_value") == expected_white_value - assert state.state == STATE_ON - assert state.attributes["color_mode"] == expected_color_mode # hs_color is None - assert state.attributes["supported_color_modes"] == supported_color_modes - assert state.attributes["supported_features"] == supported_features - - @pytest.mark.parametrize("count", [1]) @pytest.mark.parametrize( "supported_features,supported_color_modes,expected_color_mode", From d034ed1fc209868d3e96dfb4c623ad5c68e79093 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Aug 2022 13:54:54 +0200 Subject: [PATCH 3408/3516] Remove some error prone code from Alexa tests (#76917) --- tests/components/alexa/test_smart_home.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index df45d90358b..d04b3719bf9 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -26,10 +26,11 @@ from homeassistant.components.media_player.const import ( ) import homeassistant.components.vacuum as vacuum from homeassistant.config import async_process_ha_core_config -from homeassistant.const import STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import STATE_UNKNOWN, TEMP_FAHRENHEIT from homeassistant.core import Context from homeassistant.helpers import entityfilter from homeassistant.setup import async_setup_component +from homeassistant.util.unit_system import IMPERIAL_SYSTEM from .test_common import ( MockConfig, @@ -2019,7 +2020,7 @@ async def test_unknown_sensor(hass): async def test_thermostat(hass): """Test thermostat discovery.""" - hass.config.units.temperature_unit = TEMP_FAHRENHEIT + hass.config.units = IMPERIAL_SYSTEM device = ( "climate.test_thermostat", "cool", @@ -2287,9 +2288,6 @@ async def test_thermostat(hass): ) assert call.data["preset_mode"] == "eco" - # Reset config temperature_unit back to CELSIUS, required for additional tests outside this component. - hass.config.units.temperature_unit = TEMP_CELSIUS - async def test_exclude_filters(hass): """Test exclusion filters.""" From 05e33821fdc15fa6df589452dc8a2baf036f9275 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Aug 2022 14:01:50 +0200 Subject: [PATCH 3409/3516] Remove white_value support from group light (#76924) --- homeassistant/components/group/light.py | 16 +-------- tests/components/group/test_light.py | 43 ------------------------- 2 files changed, 1 insertion(+), 58 deletions(-) diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index e0645da6141..1563e811fe9 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -25,10 +25,8 @@ from homeassistant.components.light import ( ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_WHITE, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, PLATFORM_SCHEMA, - SUPPORT_WHITE_VALUE, ColorMode, LightEntity, LightEntityFeature, @@ -71,10 +69,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) SUPPORT_GROUP_LIGHT = ( - LightEntityFeature.EFFECT - | LightEntityFeature.FLASH - | LightEntityFeature.TRANSITION - | SUPPORT_WHITE_VALUE + LightEntityFeature.EFFECT | LightEntityFeature.FLASH | LightEntityFeature.TRANSITION ) _LOGGER = logging.getLogger(__name__) @@ -128,7 +123,6 @@ FORWARDED_ATTRIBUTES = frozenset( ATTR_RGBWW_COLOR, ATTR_TRANSITION, ATTR_WHITE, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, } ) @@ -148,7 +142,6 @@ class LightGroup(GroupEntity, LightEntity): ) -> None: """Initialize a light group.""" self._entity_ids = entity_ids - self._white_value: int | None = None self._attr_name = name self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} @@ -174,11 +167,6 @@ class LightGroup(GroupEntity, LightEntity): await super().async_added_to_hass() - @property - def white_value(self) -> int | None: - """Return the white value of this light group between 0..255.""" - return self._white_value - async def async_turn_on(self, **kwargs: Any) -> None: """Forward the turn_on command to all lights in the light group.""" data = { @@ -251,8 +239,6 @@ class LightGroup(GroupEntity, LightEntity): on_states, ATTR_XY_COLOR, reduce=mean_tuple ) - self._white_value = reduce_attribute(on_states, ATTR_WHITE_VALUE) - self._attr_color_temp = reduce_attribute(on_states, ATTR_COLOR_TEMP) self._attr_min_mireds = reduce_attribute( states, ATTR_MIN_MIREDS, default=154, reduce=min diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index f3083812553..5329d3074b4 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -618,48 +618,6 @@ async def test_color_rgbww(hass, enable_custom_integrations): assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 -async def test_white_value(hass): - """Test white value reporting.""" - await async_setup_component( - hass, - LIGHT_DOMAIN, - { - LIGHT_DOMAIN: { - "platform": DOMAIN, - "entities": ["light.test1", "light.test2"], - "all": "false", - } - }, - ) - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() - - hass.states.async_set( - "light.test1", STATE_ON, {ATTR_WHITE_VALUE: 255, ATTR_SUPPORTED_FEATURES: 128} - ) - await hass.async_block_till_done() - state = hass.states.get("light.light_group") - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 128 - assert state.attributes[ATTR_WHITE_VALUE] == 255 - - hass.states.async_set( - "light.test2", STATE_ON, {ATTR_WHITE_VALUE: 100, ATTR_SUPPORTED_FEATURES: 128} - ) - await hass.async_block_till_done() - state = hass.states.get("light.light_group") - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 128 - assert state.attributes[ATTR_WHITE_VALUE] == 177 - - hass.states.async_set( - "light.test1", STATE_OFF, {ATTR_WHITE_VALUE: 255, ATTR_SUPPORTED_FEATURES: 128} - ) - await hass.async_block_till_done() - state = hass.states.get("light.light_group") - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 128 - assert state.attributes[ATTR_WHITE_VALUE] == 100 - - async def test_white(hass, enable_custom_integrations): """Test white reporting.""" platform = getattr(hass.components, "test.light") @@ -1493,7 +1451,6 @@ async def test_invalid_service_calls(hass): ATTR_XY_COLOR: (0.5, 0.42), ATTR_RGB_COLOR: (80, 120, 50), ATTR_COLOR_TEMP: 1234, - ATTR_WHITE_VALUE: 1, ATTR_EFFECT: "Sunshine", ATTR_TRANSITION: 4, ATTR_FLASH: "long", From 924704e0d128bb15d84ace156adaf90d9ba9ed9e Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 17 Aug 2022 10:22:12 -0400 Subject: [PATCH 3410/3516] Fix fully_kiosk button test docstring and function name (#76935) Fix button test docstring and function name --- tests/components/fully_kiosk/test_button.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/fully_kiosk/test_button.py b/tests/components/fully_kiosk/test_button.py index 7183fc3db92..8616d7107f7 100644 --- a/tests/components/fully_kiosk/test_button.py +++ b/tests/components/fully_kiosk/test_button.py @@ -10,12 +10,12 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import MockConfigEntry -async def test_binary_sensors( +async def test_buttons( hass: HomeAssistant, mock_fully_kiosk: MagicMock, init_integration: MockConfigEntry, ) -> None: - """Test standard Fully Kiosk binary sensors.""" + """Test standard Fully Kiosk buttons.""" entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) From ef6b6e78504d9f4d071217101a817d2e1f7567e2 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Wed, 17 Aug 2022 15:25:34 +0100 Subject: [PATCH 3411/3516] Remove deprecated utility_meter entity (#76480) * remove deprecated utility_meter domain * remove select_tariff --- .../components/utility_meter/__init__.py | 4 - .../components/utility_meter/const.py | 3 - .../components/utility_meter/select.py | 131 +----------------- .../components/utility_meter/services.yaml | 22 --- tests/components/utility_meter/test_init.py | 107 +------------- 5 files changed, 10 insertions(+), 257 deletions(-) diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index b17592cdf0b..ea5fb4933be 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -13,7 +13,6 @@ from homeassistant.core import HomeAssistant, split_entity_id from homeassistant.helpers import discovery, entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType from .const import ( @@ -27,7 +26,6 @@ from .const import ( CONF_TARIFF, CONF_TARIFF_ENTITY, CONF_TARIFFS, - DATA_LEGACY_COMPONENT, DATA_TARIFF_SENSORS, DATA_UTILITY, DOMAIN, @@ -101,8 +99,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up an Utility Meter.""" - hass.data[DATA_LEGACY_COMPONENT] = EntityComponent(_LOGGER, DOMAIN, hass) - hass.data[DATA_UTILITY] = {} async def async_reset_meters(service_call): diff --git a/homeassistant/components/utility_meter/const.py b/homeassistant/components/utility_meter/const.py index 2bac649aace..9b85e9e3ae9 100644 --- a/homeassistant/components/utility_meter/const.py +++ b/homeassistant/components/utility_meter/const.py @@ -25,7 +25,6 @@ METER_TYPES = [ DATA_UTILITY = "utility_meter_data" DATA_TARIFF_SENSORS = "utility_meter_sensors" -DATA_LEGACY_COMPONENT = "utility_meter_legacy_component" CONF_METER = "meter" CONF_SOURCE_SENSOR = "source" @@ -48,6 +47,4 @@ SIGNAL_START_PAUSE_METER = "utility_meter_start_pause" SIGNAL_RESET_METER = "utility_meter_reset" SERVICE_RESET = "reset" -SERVICE_SELECT_TARIFF = "select_tariff" -SERVICE_SELECT_NEXT_TARIFF = "next_tariff" SERVICE_CALIBRATE_METER = "calibrate" diff --git a/homeassistant/components/utility_meter/select.py b/homeassistant/components/utility_meter/select.py index 008a0ff6120..efddfba94e5 100644 --- a/homeassistant/components/utility_meter/select.py +++ b/homeassistant/components/utility_meter/select.py @@ -3,41 +3,15 @@ from __future__ import annotations import logging -import voluptuous as vol - from homeassistant.components.select import SelectEntity -from homeassistant.components.select.const import ( - ATTR_OPTION, - ATTR_OPTIONS, - DOMAIN as SELECT_DOMAIN, - SERVICE_SELECT_OPTION, -) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_FRIENDLY_NAME, - CONF_UNIQUE_ID, - STATE_UNAVAILABLE, -) -from homeassistant.core import Event, HomeAssistant, callback, split_entity_id -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_UNIQUE_ID +from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import ( - ATTR_TARIFF, - ATTR_TARIFFS, - CONF_METER, - CONF_TARIFFS, - DATA_LEGACY_COMPONENT, - DATA_UTILITY, - SERVICE_SELECT_NEXT_TARIFF, - SERVICE_SELECT_TARIFF, - TARIFF_ICON, -) +from .const import CONF_METER, CONF_TARIFFS, DATA_UTILITY, TARIFF_ICON _LOGGER = logging.getLogger(__name__) @@ -51,9 +25,8 @@ async def async_setup_entry( name = config_entry.title tariffs = config_entry.options[CONF_TARIFFS] - legacy_add_entities = None unique_id = config_entry.entry_id - tariff_select = TariffSelect(name, tariffs, legacy_add_entities, unique_id) + tariff_select = TariffSelect(name, tariffs, unique_id) async_add_entities([tariff_select]) @@ -71,7 +44,6 @@ async def async_setup_platform( ) return - legacy_component = hass.data[DATA_LEGACY_COMPONENT] meter: str = discovery_info[CONF_METER] conf_meter_unique_id: str | None = hass.data[DATA_UTILITY][meter].get( CONF_UNIQUE_ID @@ -82,27 +54,16 @@ async def async_setup_platform( TariffSelect( discovery_info[CONF_METER], discovery_info[CONF_TARIFFS], - legacy_component.async_add_entities, conf_meter_unique_id, ) ] ) - legacy_component.async_register_entity_service( - SERVICE_SELECT_TARIFF, - {vol.Required(ATTR_TARIFF): cv.string}, - "async_select_tariff", - ) - - legacy_component.async_register_entity_service( - SERVICE_SELECT_NEXT_TARIFF, {}, "async_next_tariff" - ) - class TariffSelect(SelectEntity, RestoreEntity): """Representation of a Tariff selector.""" - def __init__(self, name, tariffs, add_legacy_entities, unique_id): + def __init__(self, name, tariffs, unique_id): """Initialize a tariff selector.""" self._attr_name = name self._attr_unique_id = unique_id @@ -110,7 +71,6 @@ class TariffSelect(SelectEntity, RestoreEntity): self._tariffs = tariffs self._attr_icon = TARIFF_ICON self._attr_should_poll = False - self._add_legacy_entities = add_legacy_entities @property def options(self): @@ -126,9 +86,6 @@ class TariffSelect(SelectEntity, RestoreEntity): """Run when entity about to be added.""" await super().async_added_to_hass() - if self._add_legacy_entities: - await self._add_legacy_entities([LegacyTariffSelect(self.entity_id)]) - state = await self.async_get_last_state() if not state or state.state not in self._tariffs: self._current_tariff = self._tariffs[0] @@ -139,81 +96,3 @@ class TariffSelect(SelectEntity, RestoreEntity): """Select new tariff (option).""" self._current_tariff = option self.async_write_ha_state() - - -class LegacyTariffSelect(Entity): - """Backwards compatibility for deprecated utility_meter select entity.""" - - def __init__(self, tracked_entity_id): - """Initialize the entity.""" - self._attr_icon = TARIFF_ICON - # Set name to influence entity_id - self._attr_name = split_entity_id(tracked_entity_id)[1] - self.tracked_entity_id = tracked_entity_id - - @callback - def async_state_changed_listener(self, event: Event | None = None) -> None: - """Handle child updates.""" - if ( - state := self.hass.states.get(self.tracked_entity_id) - ) is None or state.state == STATE_UNAVAILABLE: - self._attr_available = False - return - - self._attr_available = True - - self._attr_name = state.attributes.get(ATTR_FRIENDLY_NAME) - self._attr_state = state.state - self._attr_extra_state_attributes = { - ATTR_TARIFFS: state.attributes.get(ATTR_OPTIONS) - } - - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - - @callback - def _async_state_changed_listener(event: Event | None = None) -> None: - """Handle child updates.""" - self.async_state_changed_listener(event) - self.async_write_ha_state() - - self.async_on_remove( - async_track_state_change_event( - self.hass, [self.tracked_entity_id], _async_state_changed_listener - ) - ) - - # Call once on adding - _async_state_changed_listener() - - async def async_select_tariff(self, tariff): - """Select new option.""" - _LOGGER.warning( - "The 'utility_meter.select_tariff' service has been deprecated and will " - "be removed in HA Core 2022.7. Please use 'select.select_option' instead", - ) - await self.hass.services.async_call( - SELECT_DOMAIN, - SERVICE_SELECT_OPTION, - {ATTR_ENTITY_ID: self.tracked_entity_id, ATTR_OPTION: tariff}, - blocking=True, - context=self._context, - ) - - async def async_next_tariff(self): - """Offset current index.""" - _LOGGER.warning( - "The 'utility_meter.next_tariff' service has been deprecated and will " - "be removed in HA Core 2022.7. Please use 'select.select_option' instead", - ) - if ( - not self.available - or (state := self.hass.states.get(self.tracked_entity_id)) is None - ): - return - tariffs = state.attributes.get(ATTR_OPTIONS) - current_tariff = state.state - current_index = tariffs.index(current_tariff) - new_index = (current_index + 1) % len(tariffs) - - await self.async_select_tariff(tariffs[new_index]) diff --git a/homeassistant/components/utility_meter/services.yaml b/homeassistant/components/utility_meter/services.yaml index fc1e08b153a..194eff5d7d0 100644 --- a/homeassistant/components/utility_meter/services.yaml +++ b/homeassistant/components/utility_meter/services.yaml @@ -7,28 +7,6 @@ reset: entity: domain: select -next_tariff: - name: Next Tariff - description: Changes the tariff to the next one. - target: - entity: - domain: utility_meter - -select_tariff: - name: Select Tariff - description: Selects the current tariff of a utility meter. - target: - entity: - domain: utility_meter - fields: - tariff: - name: Tariff - description: Name of the tariff to switch to - example: "offpeak" - required: true - selector: - text: - calibrate: name: Calibrate description: Calibrates a utility meter sensor. diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index d358d7e4b9e..faafd559852 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -7,16 +7,11 @@ from unittest.mock import patch import pytest from homeassistant.components.select.const import ( + ATTR_OPTION, DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) -from homeassistant.components.utility_meter.const import ( - DOMAIN, - SERVICE_RESET, - SERVICE_SELECT_NEXT_TARIFF, - SERVICE_SELECT_TARIFF, - SIGNAL_RESET_METER, -) +from homeassistant.components.utility_meter.const import DOMAIN, SERVICE_RESET import homeassistant.components.utility_meter.select as um_select import homeassistant.components.utility_meter.sensor as um_sensor from homeassistant.const import ( @@ -28,9 +23,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant, State -from homeassistant.exceptions import ServiceNotFound from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -71,8 +64,6 @@ async def test_restore_state(hass): ( ["select.energy_bill"], "select.energy_bill", - ["utility_meter.energy_bill"], - "utility_meter.energy_bill", ), ) async def test_services(hass, meter): @@ -119,9 +110,9 @@ async def test_services(hass, meter): state = hass.states.get("sensor.energy_bill_offpeak") assert state.state == "0" - # Next tariff - only supported on legacy entity - data = {ATTR_ENTITY_ID: "utility_meter.energy_bill"} - await hass.services.async_call(DOMAIN, SERVICE_SELECT_NEXT_TARIFF, data) + # Change tariff + data = {ATTR_ENTITY_ID: "select.energy_bill", ATTR_OPTION: "offpeak"} + await hass.services.async_call(SELECT_DOMAIN, SERVICE_SELECT_OPTION, data) await hass.async_block_till_done() now += timedelta(seconds=10) @@ -243,12 +234,6 @@ async def test_services_config_entry(hass): state = hass.states.get("sensor.energy_bill_offpeak") assert state.state == "0" - # Next tariff - only supported on legacy entity - with pytest.raises(ServiceNotFound): - data = {ATTR_ENTITY_ID: "utility_meter.energy_bill"} - await hass.services.async_call(DOMAIN, SERVICE_SELECT_NEXT_TARIFF, data) - await hass.async_block_till_done() - # Change tariff data = {ATTR_ENTITY_ID: "select.energy_bill", "option": "offpeak"} await hass.services.async_call(SELECT_DOMAIN, SERVICE_SELECT_OPTION, data) @@ -394,88 +379,6 @@ async def test_setup_missing_discovery(hass): assert not await um_sensor.async_setup_platform(hass, {CONF_PLATFORM: DOMAIN}, None) -async def test_legacy_support(hass): - """Test legacy entity support.""" - config = { - "utility_meter": { - "energy_bill": { - "source": "sensor.energy", - "cycle": "hourly", - "tariffs": ["peak", "offpeak"], - }, - } - } - - assert await async_setup_component(hass, DOMAIN, config) - assert await async_setup_component(hass, Platform.SENSOR, config) - await hass.async_block_till_done() - - select_state = hass.states.get("select.energy_bill") - legacy_state = hass.states.get("utility_meter.energy_bill") - - assert select_state.state == legacy_state.state == "peak" - select_attributes = select_state.attributes - legacy_attributes = legacy_state.attributes - assert select_attributes.keys() == { - "friendly_name", - "icon", - "options", - } - assert legacy_attributes.keys() == {"friendly_name", "icon", "tariffs"} - assert select_attributes["friendly_name"] == legacy_attributes["friendly_name"] - assert select_attributes["icon"] == legacy_attributes["icon"] - assert select_attributes["options"] == legacy_attributes["tariffs"] - - # Change tariff on the select - data = {ATTR_ENTITY_ID: "select.energy_bill", "option": "offpeak"} - await hass.services.async_call(SELECT_DOMAIN, SERVICE_SELECT_OPTION, data) - await hass.async_block_till_done() - - select_state = hass.states.get("select.energy_bill") - legacy_state = hass.states.get("utility_meter.energy_bill") - assert select_state.state == legacy_state.state == "offpeak" - - # Change tariff on the legacy entity - data = {ATTR_ENTITY_ID: "utility_meter.energy_bill", "tariff": "offpeak"} - await hass.services.async_call(DOMAIN, SERVICE_SELECT_TARIFF, data) - await hass.async_block_till_done() - - select_state = hass.states.get("select.energy_bill") - legacy_state = hass.states.get("utility_meter.energy_bill") - assert select_state.state == legacy_state.state == "offpeak" - - # Cycle tariffs on the select - not supported - data = {ATTR_ENTITY_ID: "select.energy_bill"} - await hass.services.async_call(DOMAIN, SERVICE_SELECT_NEXT_TARIFF, data) - await hass.async_block_till_done() - - select_state = hass.states.get("select.energy_bill") - legacy_state = hass.states.get("utility_meter.energy_bill") - assert select_state.state == legacy_state.state == "offpeak" - - # Cycle tariffs on the legacy entity - data = {ATTR_ENTITY_ID: "utility_meter.energy_bill"} - await hass.services.async_call(DOMAIN, SERVICE_SELECT_NEXT_TARIFF, data) - await hass.async_block_till_done() - - select_state = hass.states.get("select.energy_bill") - legacy_state = hass.states.get("utility_meter.energy_bill") - assert select_state.state == legacy_state.state == "peak" - - # Reset the legacy entity - reset_calls = [] - - def async_reset_meter(entity_id): - reset_calls.append(entity_id) - - async_dispatcher_connect(hass, SIGNAL_RESET_METER, async_reset_meter) - - data = {ATTR_ENTITY_ID: "utility_meter.energy_bill"} - await hass.services.async_call(DOMAIN, SERVICE_RESET, data) - await hass.async_block_till_done() - assert reset_calls == ["select.energy_bill"] - - @pytest.mark.parametrize( "tariffs,expected_entities", ( From eec0f3351a5be10a0e0b1a1822339c7582b74a76 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Aug 2022 16:27:47 +0200 Subject: [PATCH 3412/3516] Add weather checks to pylint plugin (#76915) --- pylint/plugins/hass_enforce_type_hints.py | 71 +++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index b34b88c27c9..3d86767df43 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1968,6 +1968,77 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "weather": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="WeatherEntity", + matches=[ + TypeHintMatch( + function_name="native_temperature", + return_type=["float", None], + ), + TypeHintMatch( + function_name="native_temperature_unit", + return_type=["str", None], + ), + TypeHintMatch( + function_name="native_pressure", + return_type=["float", None], + ), + TypeHintMatch( + function_name="native_pressure_unit", + return_type=["str", None], + ), + TypeHintMatch( + function_name="humidity", + return_type=["float", None], + ), + TypeHintMatch( + function_name="native_wind_speed", + return_type=["float", None], + ), + TypeHintMatch( + function_name="native_wind_speed_unit", + return_type=["str", None], + ), + TypeHintMatch( + function_name="wind_bearing", + return_type=["float", "str", None], + ), + TypeHintMatch( + function_name="ozone", + return_type=["float", None], + ), + TypeHintMatch( + function_name="native_visibility", + return_type=["float", None], + ), + TypeHintMatch( + function_name="native_visibility_unit", + return_type=["str", None], + ), + TypeHintMatch( + function_name="forecast", + return_type=["list[Forecast]", None], + ), + TypeHintMatch( + function_name="native_precipitation_unit", + return_type=["str", None], + ), + TypeHintMatch( + function_name="precision", + return_type="float", + ), + TypeHintMatch( + function_name="condition", + return_type=["str", None], + ), + ], + ), + ], } From 79ab13d118fbcca74720f4817579e5ac5ec26004 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Wed, 17 Aug 2022 10:30:20 -0400 Subject: [PATCH 3413/3516] Add Fully Kiosk Browser switch platform (#76931) --- .../components/fully_kiosk/__init__.py | 2 +- .../components/fully_kiosk/switch.py | 117 ++++++ tests/components/fully_kiosk/conftest.py | 3 + .../fully_kiosk/fixtures/listsettings.json | 332 ++++++++++++++++++ tests/components/fully_kiosk/test_switch.py | 81 +++++ 5 files changed, 534 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/fully_kiosk/switch.py create mode 100644 tests/components/fully_kiosk/fixtures/listsettings.json create mode 100644 tests/components/fully_kiosk/test_switch.py diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py index 311dae20082..5a3d6078004 100644 --- a/homeassistant/components/fully_kiosk/__init__.py +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import FullyKioskDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/fully_kiosk/switch.py b/homeassistant/components/fully_kiosk/switch.py new file mode 100644 index 00000000000..7dfcc1e71ac --- /dev/null +++ b/homeassistant/components/fully_kiosk/switch.py @@ -0,0 +1,117 @@ +"""Fully Kiosk Browser switch.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from fullykiosk import FullyKiosk + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import FullyKioskDataUpdateCoordinator +from .entity import FullyKioskEntity + + +@dataclass +class FullySwitchEntityDescriptionMixin: + """Fully Kiosk Browser switch entity description mixin.""" + + on_action: Callable[[FullyKiosk], Any] + off_action: Callable[[FullyKiosk], Any] + is_on_fn: Callable[[dict[str, Any]], Any] + + +@dataclass +class FullySwitchEntityDescription( + SwitchEntityDescription, FullySwitchEntityDescriptionMixin +): + """Fully Kiosk Browser switch entity description.""" + + +SWITCHES: tuple[FullySwitchEntityDescription, ...] = ( + FullySwitchEntityDescription( + key="screensaver", + name="Screensaver", + on_action=lambda fully: fully.startScreensaver(), + off_action=lambda fully: fully.stopScreensaver(), + is_on_fn=lambda data: data.get("isInScreensaver"), + ), + FullySwitchEntityDescription( + key="maintenance", + name="Maintenance mode", + entity_category=EntityCategory.CONFIG, + on_action=lambda fully: fully.enableLockedMode(), + off_action=lambda fully: fully.disableLockedMode(), + is_on_fn=lambda data: data.get("maintenanceMode"), + ), + FullySwitchEntityDescription( + key="kiosk", + name="Kiosk lock", + entity_category=EntityCategory.CONFIG, + on_action=lambda fully: fully.lockKiosk(), + off_action=lambda fully: fully.unlockKiosk(), + is_on_fn=lambda data: data.get("kioskLocked"), + ), + FullySwitchEntityDescription( + key="motion-detection", + name="Motion detection", + entity_category=EntityCategory.CONFIG, + on_action=lambda fully: fully.enableMotionDetection(), + off_action=lambda fully: fully.disableMotionDetection(), + is_on_fn=lambda data: data["settings"].get("motionDetection"), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Fully Kiosk Browser switch.""" + coordinator: FullyKioskDataUpdateCoordinator = hass.data[DOMAIN][ + config_entry.entry_id + ] + + async_add_entities( + FullySwitchEntity(coordinator, description) for description in SWITCHES + ) + + +class FullySwitchEntity(FullyKioskEntity, SwitchEntity): + """Fully Kiosk Browser switch entity.""" + + entity_description: FullySwitchEntityDescription + + def __init__( + self, + coordinator: FullyKioskDataUpdateCoordinator, + description: FullySwitchEntityDescription, + ) -> None: + """Initialize the Fully Kiosk Browser switch entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.data['deviceID']}-{description.key}" + + @property + def is_on(self) -> bool | None: + """Return true if the entity is on.""" + if self.entity_description.is_on_fn(self.coordinator.data) is not None: + return bool(self.entity_description.is_on_fn(self.coordinator.data)) + return None + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.entity_description.on_action(self.coordinator.fully) + await self.coordinator.async_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self.entity_description.off_action(self.coordinator.fully) + await self.coordinator.async_refresh() diff --git a/tests/components/fully_kiosk/conftest.py b/tests/components/fully_kiosk/conftest.py index 35d5c12e694..bed08b532fd 100644 --- a/tests/components/fully_kiosk/conftest.py +++ b/tests/components/fully_kiosk/conftest.py @@ -65,6 +65,9 @@ def mock_fully_kiosk() -> Generator[MagicMock, None, None]: client.getDeviceInfo.return_value = json.loads( load_fixture("deviceinfo.json", DOMAIN) ) + client.getSettings.return_value = json.loads( + load_fixture("listsettings.json", DOMAIN) + ) yield client diff --git a/tests/components/fully_kiosk/fixtures/listsettings.json b/tests/components/fully_kiosk/fixtures/listsettings.json new file mode 100644 index 00000000000..d11291032a9 --- /dev/null +++ b/tests/components/fully_kiosk/fixtures/listsettings.json @@ -0,0 +1,332 @@ +{ + "tapsToPinDialogInSingleAppMode": "7", + "mdmSystemUpdatePolicy": "0", + "showMenuHint": true, + "movementBeaconList": "", + "authUsername": "", + "motionSensitivity": "90", + "remoteAdminPassword": "admin_password", + "launchOnBoot": true, + "kioskAppBlacklist": "", + "folderCleanupTime": "", + "preventSleepWhileScreenOff": false, + "touchInteraction": true, + "showBackButton": true, + "volumeLimits": "", + "addressBarBgColor": -2236963, + "mdmSystemAppsToEnable": "", + "rebootTime": "", + "kioskWifiPin": "", + "appLauncherTextColor": -16777216, + "wifiSSID": "", + "keepSleepingIfUnplugged": false, + "microphoneAccess": false, + "barcodeScanBroadcastExtra": "", + "cacheMode": "-1", + "disableOutgoingCalls": false, + "knoxDisableUsbDebugging": false, + "screensaverBrightness": "0", + "thirdPartyCookies": true, + "desktopMode": false, + "protectedContent": false, + "knoxDisableBluetoothTethering": false, + "safeBrowsing": false, + "mqttBrokerPassword": "mqtt_password", + "screensaverOtherAppIntent": "", + "disablePowerButton": true, + "inUseWhileKeyboardVisible": false, + "screensaverFullscreen": true, + "actionBarTitle": "Fully Kiosk Browser", + "pageTransitions": false, + "urlBlacklist": "", + "appBlockReturnIntent": "", + "playAlarmSoundUntilPin": false, + "enableQrScan": false, + "knoxDisableBackup": false, + "appLauncherScaling": "100", + "showAppLauncherOnStart": false, + "appToRunInForegroundOnStart": "", + "usageStatistics": true, + "restartOnCrash": true, + "tabsFgColor": -16777216, + "knoxDisableGoogleAccountsAutoSync": false, + "appToRunOnStart": "", + "knoxDisableDeveloperMode": false, + "sleepOnPowerDisconnect": false, + "remotePdfFileMode": "0", + "keepOnWhileFullscreen": true, + "mainWebAutomation": "", + "mdmMinimumPasswordLength": "5", + "deleteHistoryOnReload": false, + "showQrScanButton": false, + "mdmApkToInstall": "", + "autoplayVideos": false, + "loadOverview": false, + "startURL": "https://homeassistant.local/", + "screensaverOtherApp": false, + "showNewTabButton": false, + "advancedKioskProtection": false, + "mdmDisableKeyguard": false, + "searchProviderUrl": "https://www.google.com/search?q=", + "defaultWebviewBackgroundColor": -1, + "motionDetectionAcoustic": false, + "reloadOnScreenOn": false, + "knoxDisableMultiUser": false, + "enableBackButton": true, + "mdmDisableUsbStorage": false, + "actionBarBgUrl": "", + "lockSafeMode": false, + "setWifiWakelock": false, + "knoxHideStatusBar": false, + "ignoreMotionWhenScreensaverOnOff": false, + "kioskWifiPinCustomIntent": "", + "showTabCloseButtons": true, + "knoxDisablePowerSavingMode": false, + "killOtherApps": false, + "knoxDisableMtp": false, + "deleteWebstorageOnReload": false, + "knoxDisablePowerOff": false, + "knoxDisableGoogleCrashReport": false, + "barcodeScanIntent": "", + "mdmAppsToDisable": "", + "barcodeScanTargetUrl": "", + "knoxDisablePowerButton": false, + "actionBarFgColor": -1, + "fadeInOutDuration": "200", + "audioRecordUploads": false, + "autoplayAudio": false, + "remoteAdminCamshot": true, + "showPrintButton": false, + "knoxDisableTaskManager": false, + "kioskExitGesture": "2", + "remoteAdminScreenshot": true, + "kioskAppWhitelist": "de.badaix.snapcast", + "enableVersionInfo": true, + "removeStatusBar": false, + "knoxDisableCellularData": false, + "showShareButton": false, + "recreateTabsOnReload": false, + "injectJsCode": "", + "screensaverWallpaperURL": "fully://color#000000", + "removeNavigationBar": false, + "forceScreenOrientation": "0", + "showStatusBar": false, + "knoxDisableWiFi": false, + "inUseWhileAnotherAppInForeground": false, + "appLauncherBackgroundColor": -1, + "mdmAppLockTaskWhitelist": "", + "webcamAccess": false, + "knoxDisableClipboard": false, + "playAlarmSoundOnMovement": false, + "loadContentZipFileUrl": "", + "forceImmersive": false, + "forceShowKeyboard": false, + "keepScreenOn": true, + "pauseMotionInBackground": false, + "showCamPreview": false, + "timeToScreenOffV2": "0", + "softKeyboard": true, + "statusBarColor": 0, + "mqttBrokerUsername": "mqtt", + "fileUploads": false, + "rootEnable": true, + "knoxDisableUsbTethering": false, + "screensaverPlaylist": "", + "showNavigationBar": false, + "launcherApps": "[\n {\n \"label\": \"Plex\",\n \"component\": \"com.plexapp.android\\/com.plexapp.plex.activities.SplashActivity\"\n },\n {\n \"label\": \"Spotify\",\n \"component\": \"com.spotify.music\\/com.spotify.music.MainActivity\"\n }\n]", + "autoImportSettings": true, + "motionDetection": false, + "cloudService": false, + "webviewDarkMode": "1", + "killAppsBeforeStartingList": "", + "websiteIntegration": true, + "clientCaPassword": "", + "deleteCookiesOnReload": false, + "errorURL": "", + "knoxDisableFactoryReset": false, + "knoxDisableCamera": false, + "knoxEnabled": false, + "playMedia": true, + "stopScreensaverOnMotion": true, + "redirectBlocked": false, + "mdmDisableADB": true, + "showTabs": false, + "restartAfterUpdate": true, + "mqttEventTopic": "$appId/event/$event/$deviceId", + "fontSize": "100", + "clearCacheEach": false, + "mdmDisableVolumeButtons": false, + "knoxDisableVpn": false, + "confirmExit": true, + "ignoreSSLerrors": true, + "mqttClientId": "", + "setRemoveSystemUI": false, + "wifiKey": "", + "screenOffInDarkness": false, + "knoxDisableWifiDirect": false, + "inactiveTabsBgColor": -4144960, + "useWideViewport": true, + "forceSwipeUnlock": false, + "geoLocationAccess": false, + "graphicsAccelerationMode": "2", + "barcodeScanBroadcastAction": "", + "authPassword": "", + "tabsBgColor": -2236963, + "loadCurrentPageOnReload": false, + "skipReloadIfStartUrlShowing": false, + "enablePullToRefresh": false, + "motionSensitivityAcoustic": "90", + "wifiMode": "0", + "ignoreMotionWhenMoving": false, + "knoxDisableAirViewMode": false, + "actionBarCustomButtonUrl": "", + "kioskHomeStartURL": true, + "environmentSensorsEnabled": true, + "mdmRuntimePermissionPolicy": "0", + "disableCamera": false, + "knoxDisableRecentTaskButton": false, + "knoxDisableAirCommandMode": false, + "remoteAdminSingleAppExit": false, + "mqttDeviceInfoTopic": "$appId/deviceInfo/$deviceId", + "darknessLevel": "10", + "actionBarInSettings": false, + "remoteAdminLan": true, + "knoxDisableSDCardWrite": false, + "movementBeaconDistance": "5", + "forceSleepIfUnplugged": false, + "swipeTabs": false, + "alarmSoundFileUrl": "", + "knoxDisableStatusBar": false, + "reloadOnInternet": false, + "knoxDisableNonMarketApps": false, + "knoxDisableSettingsChanges": false, + "barcodeScanListenKeys": false, + "mdmLockTask": false, + "timeToScreensaverV2": "900", + "movementDetection": true, + "clientCaUrl": "", + "sleepOnPowerConnect": false, + "resumeVideoAudio": true, + "addXffHeader": false, + "readNfcTag": false, + "swipeNavigation": false, + "knoxDisableVolumeButtons": false, + "screenOnOnMovement": true, + "knoxHideNavigationBar": false, + "timeToClearSingleAppData": "0", + "timeToGoBackground": "0", + "timeToShutdownOnPowerDisconnect": "0", + "webviewDragging": true, + "timeToRegainFocus": "600", + "pauseWebviewOnPause": false, + "knoxDisableUsbHostStorage": false, + "folderCleanupList": "", + "movementWhenUnplugged": false, + "actionBarBgColor": -15906911, + "enableZoom": false, + "resetZoomEach": false, + "knoxDisableAndroidBeam": false, + "reloadOnWifiOn": true, + "compassSensitivity": "50", + "touchesOtherAppsBreakIdle": true, + "reloadPageFailure": "300", + "knoxDisableBackButton": false, + "actionBarIconUrl": "", + "initialScale": "0", + "knoxDisableMicrophoneState": false, + "remoteAdminFileManagement": true, + "knoxDisableFirmwareRecovery": false, + "errorUrlOnDisconnection": "0", + "addRefererHeader": true, + "showHomeButton": true, + "reloadEachSeconds": "0", + "accelerometerSensitivity": "80", + "knoxDisableScreenCapture": false, + "knoxDisableWifiTethering": false, + "knoxDisableEdgeScreen": false, + "forceScreenOrientationGlobal": false, + "mqttBrokerUrl": "tcp://192.168.1.2:1883", + "wakeupOnPowerConnect": false, + "knoxDisableClipboardShare": false, + "forceScreenUnlock": true, + "knoxDisableAirplaneMode": false, + "barcodeScanInsertInputField": false, + "waitInternetOnReload": true, + "forceOpenByAppUrl": "", + "disableVolumeButtons": true, + "disableStatusBar": true, + "kioskPin": "1234", + "screensaverDaydream": false, + "detectIBeacons": false, + "kioskWifiPinAction": "0", + "stopScreensaverOnMovement": true, + "videoCaptureUploads": false, + "disableIncomingCalls": false, + "motionCameraId": "", + "knoxDisableSafeMode": false, + "webHostFilter": false, + "urlWhitelist": "", + "disableOtherApps": true, + "progressBarColor": -13611010, + "enableTapSound": true, + "volumeLicenseKey": "", + "showActionBar": false, + "ignoreJustOnceLauncher": false, + "remoteAdmin": true, + "launcherBgUrl": "", + "mdmDisableSafeModeBoot": true, + "launcherInjectCode": "", + "cameraCaptureUploads": false, + "disableNotifications": false, + "sleepSchedule": "", + "knoxDisableAudioRecord": false, + "setCpuWakelock": false, + "enablePopups": false, + "mqttEnabled": true, + "knoxDisableOtaUpgrades": false, + "mdmDisableStatusBar": false, + "lastVersionInfo": "1.39", + "textSelection": false, + "jsAlerts": true, + "knoxActiveByKiosk": false, + "disableHomeButton": true, + "webviewDebugging": true, + "mdmDisableAppsFromUnknownSources": true, + "knoxDisableBluetooth": false, + "showAddressBar": false, + "batteryWarning": "20", + "playerCacheImages": true, + "localPdfFileMode": "0", + "resendFormData": false, + "mdmDisableScreenCapture": false, + "kioskMode": true, + "singleAppMode": false, + "mdmPasswordQuality": "0", + "enableUrlOtherApps": true, + "navigationBarColor": 0, + "isRunning": true, + "knoxDisableHomeButton": false, + "webviewScrolling": true, + "motionFps": "5", + "enableLocalhost": false, + "reloadOnScreensaverStop": false, + "customUserAgent": "", + "screenBrightness": "9", + "knoxDisableVideoRecord": false, + "showProgressBar": true, + "formAutoComplete": true, + "showRefreshButton": false, + "knoxDisableHeadphoneState": false, + "showForwardButton": true, + "timeToClearLauncherAppData": "0", + "bluetoothMode": "0", + "enableFullscreenVideos": true, + "remoteFileMode": "0", + "barcodeScanSubmitInputField": false, + "knoxDisableMultiWindowMode": false, + "singleAppIntent": "", + "userAgent": "0", + "runInForeground": true, + "deleteCacheOnReload": false, + "screenOnOnMotion": true +} diff --git a/tests/components/fully_kiosk/test_switch.py b/tests/components/fully_kiosk/test_switch.py new file mode 100644 index 00000000000..6b6d2829790 --- /dev/null +++ b/tests/components/fully_kiosk/test_switch.py @@ -0,0 +1,81 @@ +"""Test the Fully Kiosk Browser switches.""" +from unittest.mock import MagicMock + +from homeassistant.components.fully_kiosk.const import DOMAIN +import homeassistant.components.switch as switch +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_switches( + hass: HomeAssistant, mock_fully_kiosk: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test Fully Kiosk switches.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + entity = hass.states.get("switch.amazon_fire_screensaver") + assert entity + assert entity.state == "off" + entry = entity_registry.async_get("switch.amazon_fire_screensaver") + assert entry + assert entry.unique_id == "abcdef-123456-screensaver" + await call_service(hass, "turn_on", "switch.amazon_fire_screensaver") + assert len(mock_fully_kiosk.startScreensaver.mock_calls) == 1 + await call_service(hass, "turn_off", "switch.amazon_fire_screensaver") + assert len(mock_fully_kiosk.stopScreensaver.mock_calls) == 1 + + entity = hass.states.get("switch.amazon_fire_maintenance_mode") + assert entity + assert entity.state == "off" + entry = entity_registry.async_get("switch.amazon_fire_maintenance_mode") + assert entry + assert entry.unique_id == "abcdef-123456-maintenance" + await call_service(hass, "turn_on", "switch.amazon_fire_maintenance_mode") + assert len(mock_fully_kiosk.enableLockedMode.mock_calls) == 1 + await call_service(hass, "turn_off", "switch.amazon_fire_maintenance_mode") + assert len(mock_fully_kiosk.disableLockedMode.mock_calls) == 1 + + entity = hass.states.get("switch.amazon_fire_kiosk_lock") + assert entity + assert entity.state == "on" + entry = entity_registry.async_get("switch.amazon_fire_kiosk_lock") + assert entry + assert entry.unique_id == "abcdef-123456-kiosk" + await call_service(hass, "turn_off", "switch.amazon_fire_kiosk_lock") + assert len(mock_fully_kiosk.unlockKiosk.mock_calls) == 1 + await call_service(hass, "turn_on", "switch.amazon_fire_kiosk_lock") + assert len(mock_fully_kiosk.lockKiosk.mock_calls) == 1 + + entity = hass.states.get("switch.amazon_fire_motion_detection") + assert entity + assert entity.state == "off" + entry = entity_registry.async_get("switch.amazon_fire_motion_detection") + assert entry + assert entry.unique_id == "abcdef-123456-motion-detection" + await call_service(hass, "turn_on", "switch.amazon_fire_motion_detection") + assert len(mock_fully_kiosk.enableMotionDetection.mock_calls) == 1 + await call_service(hass, "turn_off", "switch.amazon_fire_motion_detection") + assert len(mock_fully_kiosk.disableMotionDetection.mock_calls) == 1 + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.configuration_url == "http://192.168.1.234:2323" + assert device_entry.entry_type is None + assert device_entry.hw_version is None + assert device_entry.identifiers == {(DOMAIN, "abcdef-123456")} + assert device_entry.manufacturer == "amzn" + assert device_entry.model == "KFDOWI" + assert device_entry.name == "Amazon Fire" + assert device_entry.sw_version == "1.42.5" + + +def call_service(hass, service, entity_id): + """Call any service on entity.""" + return hass.services.async_call( + switch.DOMAIN, service, {ATTR_ENTITY_ID: entity_id}, blocking=True + ) From 8619df52940dd6fd2402f32bf8971fce2bf19aeb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Aug 2022 16:38:41 +0200 Subject: [PATCH 3414/3516] Improve type hints in utility_meter select entity (#76447) --- homeassistant/components/utility_meter/select.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/utility_meter/select.py b/homeassistant/components/utility_meter/select.py index efddfba94e5..55845569af0 100644 --- a/homeassistant/components/utility_meter/select.py +++ b/homeassistant/components/utility_meter/select.py @@ -23,7 +23,7 @@ async def async_setup_entry( ) -> None: """Initialize Utility Meter config entry.""" name = config_entry.title - tariffs = config_entry.options[CONF_TARIFFS] + tariffs: list[str] = config_entry.options[CONF_TARIFFS] unique_id = config_entry.entry_id tariff_select = TariffSelect(name, tariffs, unique_id) @@ -52,7 +52,7 @@ async def async_setup_platform( async_add_entities( [ TariffSelect( - discovery_info[CONF_METER], + meter, discovery_info[CONF_TARIFFS], conf_meter_unique_id, ) @@ -67,22 +67,22 @@ class TariffSelect(SelectEntity, RestoreEntity): """Initialize a tariff selector.""" self._attr_name = name self._attr_unique_id = unique_id - self._current_tariff = None + self._current_tariff: str | None = None self._tariffs = tariffs self._attr_icon = TARIFF_ICON self._attr_should_poll = False @property - def options(self): + def options(self) -> list[str]: """Return the available tariffs.""" return self._tariffs @property - def current_option(self): + def current_option(self) -> str | None: """Return current tariff.""" return self._current_tariff - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added.""" await super().async_added_to_hass() From 673a72503d14497b85e06c630bb6e8c5f36361cc Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Aug 2022 16:58:17 +0200 Subject: [PATCH 3415/3516] Improve type hints in water_heater (#76910) * Improve type hints in water_heater * Adjust melcloud --- .../components/melcloud/water_heater.py | 10 +++-- .../components/water_heater/__init__.py | 43 +++++++++++-------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/melcloud/water_heater.py b/homeassistant/components/melcloud/water_heater.py index c9075abb008..58da39b1a61 100644 --- a/homeassistant/components/melcloud/water_heater.py +++ b/homeassistant/components/melcloud/water_heater.py @@ -9,6 +9,8 @@ from pymelcloud.atw_device import ( from pymelcloud.device import PROPERTY_POWER from homeassistant.components.water_heater import ( + DEFAULT_MAX_TEMP, + DEFAULT_MIN_TEMP, WaterHeaterEntity, WaterHeaterEntityFeature, ) @@ -122,11 +124,11 @@ class AtwWaterHeater(WaterHeaterEntity): await self._device.set({PROPERTY_OPERATION_MODE: operation_mode}) @property - def min_temp(self) -> float | None: + def min_temp(self) -> float: """Return the minimum temperature.""" - return self._device.target_tank_temperature_min + return self._device.target_tank_temperature_min or DEFAULT_MIN_TEMP @property - def max_temp(self) -> float | None: + def max_temp(self) -> float: """Return the maximum temperature.""" - return self._device.target_tank_temperature_max + return self._device.target_tank_temperature_max or DEFAULT_MAX_TEMP diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index e24d117f678..50ba1d15f2e 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -1,12 +1,13 @@ """Support for water heater devices.""" from __future__ import annotations +from collections.abc import Mapping from dataclasses import dataclass from datetime import timedelta from enum import IntEnum import functools as ft import logging -from typing import final +from typing import Any, final import voluptuous as vol @@ -23,7 +24,7 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -188,11 +189,11 @@ class WaterHeaterEntity(Entity): return PRECISION_WHOLE @property - def capability_attributes(self): + def capability_attributes(self) -> Mapping[str, Any]: """Return capability attributes.""" supported_features = self.supported_features or 0 - data = { + data: dict[str, Any] = { ATTR_MIN_TEMP: show_temp( self.hass, self.min_temp, self.temperature_unit, self.precision ), @@ -208,9 +209,9 @@ class WaterHeaterEntity(Entity): @final @property - def state_attributes(self): + def state_attributes(self) -> dict[str, Any]: """Return the optional state attributes.""" - data = { + data: dict[str, Any] = { ATTR_CURRENT_TEMPERATURE: show_temp( self.hass, self.current_temperature, @@ -237,7 +238,7 @@ class WaterHeaterEntity(Entity): ), } - supported_features = self.supported_features + supported_features = self.supported_features or 0 if supported_features & WaterHeaterEntityFeature.OPERATION_MODE: data[ATTR_OPERATION_MODE] = self.current_operation @@ -288,42 +289,42 @@ class WaterHeaterEntity(Entity): """Return true if away mode is on.""" return self._attr_is_away_mode_on - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" raise NotImplementedError() - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" await self.hass.async_add_executor_job( ft.partial(self.set_temperature, **kwargs) ) - def set_operation_mode(self, operation_mode): + def set_operation_mode(self, operation_mode: str) -> None: """Set new target operation mode.""" raise NotImplementedError() - async def async_set_operation_mode(self, operation_mode): + async def async_set_operation_mode(self, operation_mode: str) -> None: """Set new target operation mode.""" await self.hass.async_add_executor_job(self.set_operation_mode, operation_mode) - def turn_away_mode_on(self): + def turn_away_mode_on(self) -> None: """Turn away mode on.""" raise NotImplementedError() - async def async_turn_away_mode_on(self): + async def async_turn_away_mode_on(self) -> None: """Turn away mode on.""" await self.hass.async_add_executor_job(self.turn_away_mode_on) - def turn_away_mode_off(self): + def turn_away_mode_off(self) -> None: """Turn away mode off.""" raise NotImplementedError() - async def async_turn_away_mode_off(self): + async def async_turn_away_mode_off(self) -> None: """Turn away mode off.""" await self.hass.async_add_executor_job(self.turn_away_mode_off) @property - def min_temp(self): + def min_temp(self) -> float: """Return the minimum temperature.""" if hasattr(self, "_attr_min_temp"): return self._attr_min_temp @@ -332,7 +333,7 @@ class WaterHeaterEntity(Entity): ) @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum temperature.""" if hasattr(self, "_attr_max_temp"): return self._attr_max_temp @@ -341,7 +342,9 @@ class WaterHeaterEntity(Entity): ) -async def async_service_away_mode(entity, service): +async def async_service_away_mode( + entity: WaterHeaterEntity, service: ServiceCall +) -> None: """Handle away mode service.""" if service.data[ATTR_AWAY_MODE]: await entity.async_turn_away_mode_on() @@ -349,7 +352,9 @@ async def async_service_away_mode(entity, service): await entity.async_turn_away_mode_off() -async def async_service_temperature_set(entity, service): +async def async_service_temperature_set( + entity: WaterHeaterEntity, service: ServiceCall +) -> None: """Handle set temperature service.""" hass = entity.hass kwargs = {} From 35d01d79392eb4121019f1df0631d68ac46ecf54 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Aug 2022 16:58:42 +0200 Subject: [PATCH 3416/3516] Add RestoreNumber to number checks in pylint (#76933) --- pylint/plugins/hass_enforce_type_hints.py | 31 +++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 3d86767df43..a6c4a968aa7 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -577,6 +577,20 @@ _ENTITY_MATCH: list[TypeHintMatch] = [ has_async_counterpart=True, ), ] +_RESTORE_ENTITY_MATCH: list[TypeHintMatch] = [ + TypeHintMatch( + function_name="async_get_last_state", + return_type=["State", None], + ), + TypeHintMatch( + function_name="async_get_last_extra_data", + return_type=["ExtraStoredData", None], + ), + TypeHintMatch( + function_name="extra_restore_state_data", + return_type=["ExtraStoredData", None], + ), +] _TOGGLE_ENTITY_MATCH: list[TypeHintMatch] = [ TypeHintMatch( function_name="is_on", @@ -1786,6 +1800,10 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { base_class="Entity", matches=_ENTITY_MATCH, ), + ClassTypeHintMatch( + base_class="RestoreEntity", + matches=_RESTORE_ENTITY_MATCH, + ), ClassTypeHintMatch( base_class="NumberEntity", matches=[ @@ -1829,6 +1847,19 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), ], ), + ClassTypeHintMatch( + base_class="RestoreNumber", + matches=[ + TypeHintMatch( + function_name="extra_restore_state_data", + return_type="NumberExtraStoredData", + ), + TypeHintMatch( + function_name="async_get_last_number_data", + return_type=["NumberExtraStoredData", None], + ), + ], + ), ], "remote": [ ClassTypeHintMatch( From 171893b4847bd73f85fee7f0a8f613b6301da8aa Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Aug 2022 17:00:30 +0200 Subject: [PATCH 3417/3516] Fix acmeda set cover tilt position (#76927) --- homeassistant/components/acmeda/cover.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/acmeda/cover.py b/homeassistant/components/acmeda/cover.py index 887e26cd7fc..3c3a5ba825a 100644 --- a/homeassistant/components/acmeda/cover.py +++ b/homeassistant/components/acmeda/cover.py @@ -16,6 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .base import AcmedaBase from .const import ACMEDA_HUB_UPDATE, DOMAIN from .helpers import async_add_acmeda_entities +from .hub import PulseHub async def async_setup_entry( @@ -24,7 +25,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Acmeda Rollers from a config entry.""" - hub = hass.data[DOMAIN][config_entry.entry_id] + hub: PulseHub = hass.data[DOMAIN][config_entry.entry_id] current: set[int] = set() @@ -122,6 +123,6 @@ class AcmedaCover(AcmedaBase, CoverEntity): """Stop the roller.""" await self.roller.move_stop() - async def async_set_cover_tilt(self, **kwargs): + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Tilt the roller shutter to a specific position.""" await self.roller.move_to(100 - kwargs[ATTR_POSITION]) From ce077489792ea0ec2545514987b35a0912618fa2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Aug 2022 17:50:00 +0200 Subject: [PATCH 3418/3516] Add water_heater checks to pylint plugin (#76911) --- pylint/plugins/hass_enforce_type_hints.py | 77 +++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index a6c4a968aa7..004f01aab42 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1999,6 +1999,83 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "water_heater": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="WaterHeaterEntity", + matches=[ + TypeHintMatch( + function_name="precision", + return_type="float", + ), + TypeHintMatch( + function_name="temperature_unit", + return_type="str", + ), + TypeHintMatch( + function_name="current_operation", + return_type=["str", None], + ), + TypeHintMatch( + function_name="operation_list", + return_type=["list[str]", None], + ), + TypeHintMatch( + function_name="current_temperature", + return_type=["float", None], + ), + TypeHintMatch( + function_name="target_temperature", + return_type=["float", None], + ), + TypeHintMatch( + function_name="target_temperature_high", + return_type=["float", None], + ), + TypeHintMatch( + function_name="target_temperature_low", + return_type=["float", None], + ), + TypeHintMatch( + function_name="is_away_mode_on", + return_type=["bool", None], + ), + TypeHintMatch( + function_name="set_temperature", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_operation_mode", + arg_types={1: "str"}, + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_away_mode_on", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="turn_away_mode_off", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="min_temp", + return_type="float", + ), + TypeHintMatch( + function_name="max_temp", + return_type="float", + ), + ], + ), + ], "weather": [ ClassTypeHintMatch( base_class="Entity", From a63a3b96b73fc802b3048288b20ae5ff1903d354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Klomp?= Date: Wed, 17 Aug 2022 17:53:21 +0200 Subject: [PATCH 3419/3516] Bump pysma to 0.6.12 (#76937) --- homeassistant/components/sma/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 6438c3a5777..8183e7a97c0 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "SMA Solar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": ["pysma==0.6.11"], + "requirements": ["pysma==0.6.12"], "codeowners": ["@kellerza", "@rklomp"], "iot_class": "local_polling", "loggers": ["pysma"] diff --git a/requirements_all.txt b/requirements_all.txt index 5aeb4d57d1b..000e34e6218 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1840,7 +1840,7 @@ pysignalclirestapi==0.3.18 pyskyqhub==0.1.4 # homeassistant.components.sma -pysma==0.6.11 +pysma==0.6.12 # homeassistant.components.smappee pysmappee==0.2.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c6659b8c5b5..07a11f0aae6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1275,7 +1275,7 @@ pysiaalarm==3.0.2 pysignalclirestapi==0.3.18 # homeassistant.components.sma -pysma==0.6.11 +pysma==0.6.12 # homeassistant.components.smappee pysmappee==0.2.29 From fc6c66fe6c5b48c669b50f45869a2670b5c5087e Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Aug 2022 18:42:35 +0200 Subject: [PATCH 3420/3516] Add RestoreEntity to button checks in pylint (#76932) --- pylint/plugins/hass_enforce_type_hints.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 004f01aab42..4e81b7b77af 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -724,6 +724,10 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { base_class="Entity", matches=_ENTITY_MATCH, ), + ClassTypeHintMatch( + base_class="RestoreEntity", + matches=_RESTORE_ENTITY_MATCH, + ), ClassTypeHintMatch( base_class="ButtonEntity", matches=[ From 27b5ebb9d3a1912f55ec365b805f6e15f8bfe1a8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Aug 2022 18:43:28 +0200 Subject: [PATCH 3421/3516] Add RestoreSensor to sensor checks in pylint (#76916) --- pylint/plugins/hass_enforce_type_hints.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 4e81b7b77af..7eb31eba523 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1949,6 +1949,10 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { base_class="Entity", matches=_ENTITY_MATCH, ), + ClassTypeHintMatch( + base_class="RestoreEntity", + matches=_RESTORE_ENTITY_MATCH, + ), ClassTypeHintMatch( base_class="SensorEntity", matches=[ @@ -1983,6 +1987,19 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), ], ), + ClassTypeHintMatch( + base_class="RestoreSensor", + matches=[ + TypeHintMatch( + function_name="extra_restore_state_data", + return_type="SensorExtraStoredData", + ), + TypeHintMatch( + function_name="async_get_last_sensor_data", + return_type=["SensorExtraStoredData", None], + ), + ], + ), ], "siren": [ ClassTypeHintMatch( From 49c793b1a2a5e110b210d81729c18c1f5a419352 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Aug 2022 18:44:08 +0200 Subject: [PATCH 3422/3516] Add scene checks to pylint plugin (#76908) --- pylint/plugins/hass_enforce_type_hints.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 7eb31eba523..99d06c380fd 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1911,6 +1911,27 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "scene": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="RestoreEntity", + matches=_RESTORE_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="Scene", + matches=[ + TypeHintMatch( + function_name="activate", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ], "select": [ ClassTypeHintMatch( base_class="Entity", From ff7ef7e5265c565921ee33a4b9f4ab35290d1a1c Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Wed, 17 Aug 2022 12:45:34 -0400 Subject: [PATCH 3423/3516] Bump version of pyunifiprotect to 4.1.2 (#76936) --- homeassistant/components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 246a4643412..218516dc81b 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Protect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.0.13", "unifi-discovery==1.1.5"], + "requirements": ["pyunifiprotect==4.1.2", "unifi-discovery==1.1.5"], "dependencies": ["http"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/requirements_all.txt b/requirements_all.txt index 000e34e6218..ecebd456696 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2016,7 +2016,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.13 +pyunifiprotect==4.1.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 07a11f0aae6..321398720cf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1373,7 +1373,7 @@ pytrafikverket==0.2.0.1 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.0.13 +pyunifiprotect==4.1.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 3bcc274dfa90d7d3c01ace83137c46a0898c107f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Aug 2022 10:51:56 -1000 Subject: [PATCH 3424/3516] Rework bluetooth to support scans from multiple sources (#76900) --- .../components/bluetooth/__init__.py | 56 +-- homeassistant/components/bluetooth/const.py | 1 + homeassistant/components/bluetooth/manager.py | 329 ++++++-------- homeassistant/components/bluetooth/match.py | 4 - homeassistant/components/bluetooth/models.py | 98 +---- homeassistant/components/bluetooth/scanner.py | 241 ++++++++++ tests/components/bluetooth/__init__.py | 36 +- tests/components/bluetooth/test_init.py | 412 ++++-------------- .../test_passive_update_coordinator.py | 14 +- .../test_passive_update_processor.py | 15 +- tests/components/bluetooth/test_scanner.py | 264 +++++++++++ tests/conftest.py | 20 +- 12 files changed, 805 insertions(+), 685 deletions(-) create mode 100644 homeassistant/components/bluetooth/scanner.py create mode 100644 tests/components/bluetooth/test_scanner.py diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index a90f367fc38..8bb275c94fd 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -8,11 +8,14 @@ from typing import TYPE_CHECKING import async_timeout from homeassistant import config_entries +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback as hass_callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery_flow from homeassistant.loader import async_get_bluetooth -from .const import CONF_ADAPTER, DOMAIN, SOURCE_LOCAL +from . import models +from .const import CONF_ADAPTER, DATA_MANAGER, DOMAIN, SOURCE_LOCAL from .manager import BluetoothManager from .match import BluetoothCallbackMatcher, IntegrationMatcher from .models import ( @@ -24,6 +27,7 @@ from .models import ( HaBleakScannerWrapper, ProcessAdvertisementCallback, ) +from .scanner import HaScanner, create_bleak_scanner from .util import async_get_bluetooth_adapters if TYPE_CHECKING: @@ -55,10 +59,7 @@ def async_get_scanner(hass: HomeAssistant) -> HaBleakScannerWrapper: This is a wrapper around our BleakScanner singleton that allows multiple integrations to share the same BleakScanner. """ - if DOMAIN not in hass.data: - raise RuntimeError("Bluetooth integration not loaded") - manager: BluetoothManager = hass.data[DOMAIN] - return manager.async_get_scanner() + return HaBleakScannerWrapper() @hass_callback @@ -66,9 +67,9 @@ def async_discovered_service_info( hass: HomeAssistant, ) -> list[BluetoothServiceInfoBleak]: """Return the discovered devices list.""" - if DOMAIN not in hass.data: + if DATA_MANAGER not in hass.data: return [] - manager: BluetoothManager = hass.data[DOMAIN] + manager: BluetoothManager = hass.data[DATA_MANAGER] return manager.async_discovered_service_info() @@ -78,9 +79,9 @@ def async_ble_device_from_address( address: str, ) -> BLEDevice | None: """Return BLEDevice for an address if its present.""" - if DOMAIN not in hass.data: + if DATA_MANAGER not in hass.data: return None - manager: BluetoothManager = hass.data[DOMAIN] + manager: BluetoothManager = hass.data[DATA_MANAGER] return manager.async_ble_device_from_address(address) @@ -90,9 +91,9 @@ def async_address_present( address: str, ) -> bool: """Check if an address is present in the bluetooth device list.""" - if DOMAIN not in hass.data: + if DATA_MANAGER not in hass.data: return False - manager: BluetoothManager = hass.data[DOMAIN] + manager: BluetoothManager = hass.data[DATA_MANAGER] return manager.async_address_present(address) @@ -112,7 +113,7 @@ def async_register_callback( Returns a callback that can be used to cancel the registration. """ - manager: BluetoothManager = hass.data[DOMAIN] + manager: BluetoothManager = hass.data[DATA_MANAGER] return manager.async_register_callback(callback, match_dict) @@ -152,14 +153,14 @@ def async_track_unavailable( Returns a callback that can be used to cancel the registration. """ - manager: BluetoothManager = hass.data[DOMAIN] + manager: BluetoothManager = hass.data[DATA_MANAGER] return manager.async_track_unavailable(callback, address) @hass_callback def async_rediscover_address(hass: HomeAssistant, address: str) -> None: """Trigger discovery of devices which have already been seen.""" - manager: BluetoothManager = hass.data[DOMAIN] + manager: BluetoothManager = hass.data[DATA_MANAGER] manager.async_rediscover_address(address) @@ -173,7 +174,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass)) manager = BluetoothManager(hass, integration_matcher) manager.async_setup() - hass.data[DOMAIN] = manager + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop) + hass.data[DATA_MANAGER] = models.MANAGER = manager # The config entry is responsible for starting the manager # if its enabled @@ -198,13 +200,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: - """Set up the bluetooth integration from a config entry.""" - manager: BluetoothManager = hass.data[DOMAIN] - async with manager.start_stop_lock: - await manager.async_start( - BluetoothScanningMode.ACTIVE, entry.options.get(CONF_ADAPTER) - ) + """Set up a config entry for a bluetooth scanner.""" + manager: BluetoothManager = hass.data[DATA_MANAGER] + adapter: str | None = entry.options.get(CONF_ADAPTER) + try: + bleak_scanner = create_bleak_scanner(BluetoothScanningMode.ACTIVE, adapter) + except RuntimeError as err: + raise ConfigEntryNotReady from err + scanner = HaScanner(hass, bleak_scanner, adapter) + entry.async_on_unload(scanner.async_register_callback(manager.scanner_adv_received)) + await scanner.async_start() + entry.async_on_unload(manager.async_register_scanner(scanner)) entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = scanner return True @@ -219,8 +227,6 @@ async def async_unload_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: """Unload a config entry.""" - manager: BluetoothManager = hass.data[DOMAIN] - async with manager.start_stop_lock: - manager.async_start_reload() - await manager.async_stop() + scanner: HaScanner = hass.data[DOMAIN].pop(entry.entry_id) + await scanner.async_stop() return True diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index fac191202b0..04581b841b9 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -16,6 +16,7 @@ DEFAULT_ADAPTERS = {MACOS_DEFAULT_BLUETOOTH_ADAPTER, UNIX_DEFAULT_BLUETOOTH_ADAP SOURCE_LOCAL: Final = "local" +DATA_MANAGER: Final = "bluetooth_manager" UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 START_TIMEOUT = 12 diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index e4f75350575..15b05271bd4 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -1,70 +1,66 @@ """The bluetooth integration.""" from __future__ import annotations -import asyncio -from collections.abc import Callable +from collections.abc import Callable, Iterable from datetime import datetime, timedelta +import itertools import logging -import time -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Final -import async_timeout -from bleak import BleakError -from dbus_next import InvalidMessageError +from bleak.backends.scanner import AdvertisementDataCallback from homeassistant import config_entries -from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import ( CALLBACK_TYPE, Event, HomeAssistant, callback as hass_callback, ) -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval -from homeassistant.util.package import is_docker_env -from . import models -from .const import ( - DEFAULT_ADAPTERS, - SCANNER_WATCHDOG_INTERVAL, - SCANNER_WATCHDOG_TIMEOUT, - SOURCE_LOCAL, - START_TIMEOUT, - UNAVAILABLE_TRACK_SECONDS, -) +from .const import SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS from .match import ( ADDRESS, BluetoothCallbackMatcher, IntegrationMatcher, ble_device_matches, ) -from .models import ( - BluetoothCallback, - BluetoothChange, - BluetoothScanningMode, - BluetoothServiceInfoBleak, - HaBleakScanner, - HaBleakScannerWrapper, -) +from .models import BluetoothCallback, BluetoothChange, BluetoothServiceInfoBleak from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher if TYPE_CHECKING: from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementData + from .scanner import HaScanner + +FILTER_UUIDS: Final = "UUIDs" + _LOGGER = logging.getLogger(__name__) -MONOTONIC_TIME = time.monotonic +def _dispatch_bleak_callback( + callback: AdvertisementDataCallback, + filters: dict[str, set[str]], + device: BLEDevice, + advertisement_data: AdvertisementData, +) -> None: + """Dispatch the callback.""" + if not callback: + # Callback destroyed right before being called, ignore + return # type: ignore[unreachable] # pragma: no cover + if (uuids := filters.get(FILTER_UUIDS)) and not uuids.intersection( + advertisement_data.service_uuids + ): + return -SCANNING_MODE_TO_BLEAK = { - BluetoothScanningMode.ACTIVE: "active", - BluetoothScanningMode.PASSIVE: "passive", -} + try: + callback(device, advertisement_data) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Error in callback: %s", callback) class BluetoothManager: @@ -75,136 +71,46 @@ class BluetoothManager: hass: HomeAssistant, integration_matcher: IntegrationMatcher, ) -> None: - """Init bluetooth discovery.""" + """Init bluetooth manager.""" self.hass = hass self._integration_matcher = integration_matcher - self.scanner: HaBleakScanner | None = None - self.start_stop_lock = asyncio.Lock() - self._cancel_device_detected: CALLBACK_TYPE | None = None self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None - self._cancel_stop: CALLBACK_TYPE | None = None - self._cancel_watchdog: CALLBACK_TYPE | None = None self._unavailable_callbacks: dict[str, list[Callable[[str], None]]] = {} self._callbacks: list[ tuple[BluetoothCallback, BluetoothCallbackMatcher | None] ] = [] - self._last_detection = 0.0 - self._reloading = False - self._adapter: str | None = None - self._scanning_mode = BluetoothScanningMode.ACTIVE + self._bleak_callbacks: list[ + tuple[AdvertisementDataCallback, dict[str, set[str]]] + ] = [] + self.history: dict[str, tuple[BLEDevice, AdvertisementData, float, str]] = {} + self._scanners: list[HaScanner] = [] @hass_callback def async_setup(self) -> None: """Set up the bluetooth manager.""" - models.HA_BLEAK_SCANNER = self.scanner = HaBleakScanner() - - @hass_callback - def async_get_scanner(self) -> HaBleakScannerWrapper: - """Get the scanner.""" - return HaBleakScannerWrapper() - - @hass_callback - def async_start_reload(self) -> None: - """Start reloading.""" - self._reloading = True - - async def async_start( - self, scanning_mode: BluetoothScanningMode, adapter: str | None - ) -> None: - """Set up BT Discovery.""" - assert self.scanner is not None - self._adapter = adapter - self._scanning_mode = scanning_mode - if self._reloading: - # On reload, we need to reset the scanner instance - # since the devices in its history may not be reachable - # anymore. - self.scanner.async_reset() - self._integration_matcher.async_clear_history() - self._reloading = False - scanner_kwargs = {"scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode]} - if adapter and adapter not in DEFAULT_ADAPTERS: - scanner_kwargs["adapter"] = adapter - _LOGGER.debug("Initializing bluetooth scanner with %s", scanner_kwargs) - try: - self.scanner.async_setup(**scanner_kwargs) - except (FileNotFoundError, BleakError) as ex: - raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex install_multiple_bleak_catcher() - # We have to start it right away as some integrations might - # need it straight away. - _LOGGER.debug("Starting bluetooth scanner") - self.scanner.register_detection_callback(self.scanner.async_callback_dispatcher) - self._cancel_device_detected = self.scanner.async_register_callback( - self._device_detected, {} - ) - try: - async with async_timeout.timeout(START_TIMEOUT): - await self.scanner.start() # type: ignore[no-untyped-call] - except InvalidMessageError as ex: - self._async_cancel_scanner_callback() - _LOGGER.debug("Invalid DBus message received: %s", ex, exc_info=True) - raise ConfigEntryNotReady( - f"Invalid DBus message received: {ex}; try restarting `dbus`" - ) from ex - except BrokenPipeError as ex: - self._async_cancel_scanner_callback() - _LOGGER.debug("DBus connection broken: %s", ex, exc_info=True) - if is_docker_env(): - raise ConfigEntryNotReady( - f"DBus connection broken: {ex}; try restarting `bluetooth`, `dbus`, and finally the docker container" - ) from ex - raise ConfigEntryNotReady( - f"DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`" - ) from ex - except FileNotFoundError as ex: - self._async_cancel_scanner_callback() - _LOGGER.debug( - "FileNotFoundError while starting bluetooth: %s", ex, exc_info=True - ) - if is_docker_env(): - raise ConfigEntryNotReady( - f"DBus service not found; docker config may be missing `-v /run/dbus:/run/dbus:ro`: {ex}" - ) from ex - raise ConfigEntryNotReady( - f"DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}" - ) from ex - except asyncio.TimeoutError as ex: - self._async_cancel_scanner_callback() - raise ConfigEntryNotReady( - f"Timed out starting Bluetooth after {START_TIMEOUT} seconds" - ) from ex - except BleakError as ex: - self._async_cancel_scanner_callback() - _LOGGER.debug("BleakError while starting bluetooth: %s", ex, exc_info=True) - raise ConfigEntryNotReady(f"Failed to start Bluetooth: {ex}") from ex self.async_setup_unavailable_tracking() - self._async_setup_scanner_watchdog() - self._cancel_stop = self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping + + @hass_callback + def async_stop(self, event: Event) -> None: + """Stop the Bluetooth integration at shutdown.""" + _LOGGER.debug("Stopping bluetooth manager") + if self._cancel_unavailable_tracking: + self._cancel_unavailable_tracking() + self._cancel_unavailable_tracking = None + uninstall_multiple_bleak_catcher() + + @hass_callback + def async_all_discovered_devices(self) -> Iterable[BLEDevice]: + """Return all of discovered devices from all the scanners including duplicates.""" + return itertools.chain.from_iterable( + scanner.discovered_devices for scanner in self._scanners ) @hass_callback - def _async_setup_scanner_watchdog(self) -> None: - """If Dbus gets restarted or updated, we need to restart the scanner.""" - self._last_detection = MONOTONIC_TIME() - self._cancel_watchdog = async_track_time_interval( - self.hass, self._async_scanner_watchdog, SCANNER_WATCHDOG_INTERVAL - ) - - async def _async_scanner_watchdog(self, now: datetime) -> None: - """Check if the scanner is running.""" - time_since_last_detection = MONOTONIC_TIME() - self._last_detection - if time_since_last_detection < SCANNER_WATCHDOG_TIMEOUT: - return - _LOGGER.info( - "Bluetooth scanner has gone quiet for %s, restarting", - SCANNER_WATCHDOG_INTERVAL, - ) - async with self.start_stop_lock: - self.async_start_reload() - await self.async_stop() - await self.async_start(self._scanning_mode, self._adapter) + def async_discovered_devices(self) -> list[BLEDevice]: + """Return all of combined best path to discovered from all the scanners.""" + return [history[0] for history in self.history.values()] @hass_callback def async_setup_unavailable_tracking(self) -> None: @@ -213,13 +119,13 @@ class BluetoothManager: @hass_callback def _async_check_unavailable(now: datetime) -> None: """Watch for unavailable devices.""" - scanner = self.scanner - assert scanner is not None - history = set(scanner.history) - active = {device.address for device in scanner.discovered_devices} - disappeared = history.difference(active) + history_set = set(self.history) + active_addresses = { + device.address for device in self.async_all_discovered_devices() + } + disappeared = history_set.difference(active_addresses) for address in disappeared: - del scanner.history[address] + del self.history[address] if not (callbacks := self._unavailable_callbacks.get(address)): continue for callback in callbacks: @@ -235,16 +141,40 @@ class BluetoothManager: ) @hass_callback - def _device_detected( - self, device: BLEDevice, advertisement_data: AdvertisementData + def scanner_adv_received( + self, + device: BLEDevice, + advertisement_data: AdvertisementData, + monotonic_time: float, + source: str, ) -> None: - """Handle a detected device.""" - self._last_detection = MONOTONIC_TIME() + """Handle a new advertisement from any scanner. + + Callbacks from all the scanners arrive here. + + In the future we will only process callbacks if + + - The device is not in the history + - The RSSI is above a certain threshold better than + than the source from the history or the timestamp + in the history is older than 180s + """ + self.history[device.address] = ( + device, + advertisement_data, + monotonic_time, + source, + ) + + for callback_filters in self._bleak_callbacks: + _dispatch_bleak_callback(*callback_filters, device, advertisement_data) + matched_domains = self._integration_matcher.match_domains( device, advertisement_data ) _LOGGER.debug( - "Device detected: %s with advertisement_data: %s matched domains: %s", + "%s: %s %s match: %s", + source, device.address, advertisement_data, matched_domains, @@ -260,7 +190,7 @@ class BluetoothManager: ): if service_info is None: service_info = BluetoothServiceInfoBleak.from_advertisement( - device, advertisement_data, SOURCE_LOCAL + device, advertisement_data, source ) try: callback(service_info, BluetoothChange.ADVERTISEMENT) @@ -271,7 +201,7 @@ class BluetoothManager: return if service_info is None: service_info = BluetoothServiceInfoBleak.from_advertisement( - device, advertisement_data, SOURCE_LOCAL + device, advertisement_data, source ) for domain in matched_domains: discovery_flow.async_create_flow( @@ -316,13 +246,13 @@ class BluetoothManager: if ( matcher and (address := matcher.get(ADDRESS)) - and self.scanner - and (device_adv_data := self.scanner.history.get(address)) + and (device_adv_data := self.history.get(address)) ): + ble_device, adv_data, _, _ = device_adv_data try: callback( BluetoothServiceInfoBleak.from_advertisement( - *device_adv_data, SOURCE_LOCAL + ble_device, adv_data, SOURCE_LOCAL ), BluetoothChange.ADVERTISEMENT, ) @@ -334,60 +264,55 @@ class BluetoothManager: @hass_callback def async_ble_device_from_address(self, address: str) -> BLEDevice | None: """Return the BLEDevice if present.""" - if self.scanner and (ble_adv := self.scanner.history.get(address)): + if ble_adv := self.history.get(address): return ble_adv[0] return None @hass_callback def async_address_present(self, address: str) -> bool: """Return if the address is present.""" - return bool(self.scanner and address in self.scanner.history) + return address in self.history @hass_callback def async_discovered_service_info(self) -> list[BluetoothServiceInfoBleak]: """Return if the address is present.""" - assert self.scanner is not None return [ - BluetoothServiceInfoBleak.from_advertisement(*device_adv, SOURCE_LOCAL) - for device_adv in self.scanner.history.values() + BluetoothServiceInfoBleak.from_advertisement( + device_adv[0], device_adv[1], SOURCE_LOCAL + ) + for device_adv in self.history.values() ] - async def _async_hass_stopping(self, event: Event) -> None: - """Stop the Bluetooth integration at shutdown.""" - self._cancel_stop = None - await self.async_stop() - - @hass_callback - def _async_cancel_scanner_callback(self) -> None: - """Cancel the scanner callback.""" - if self._cancel_device_detected: - self._cancel_device_detected() - self._cancel_device_detected = None - - async def async_stop(self) -> None: - """Stop bluetooth discovery.""" - _LOGGER.debug("Stopping bluetooth discovery") - if self._cancel_watchdog: - self._cancel_watchdog() - self._cancel_watchdog = None - self._async_cancel_scanner_callback() - if self._cancel_unavailable_tracking: - self._cancel_unavailable_tracking() - self._cancel_unavailable_tracking = None - if self._cancel_stop: - self._cancel_stop() - self._cancel_stop = None - if self.scanner: - try: - await self.scanner.stop() # type: ignore[no-untyped-call] - except BleakError as ex: - # This is not fatal, and they may want to reload - # the config entry to restart the scanner if they - # change the bluetooth dongle. - _LOGGER.error("Error stopping scanner: %s", ex) - uninstall_multiple_bleak_catcher() - @hass_callback def async_rediscover_address(self, address: str) -> None: """Trigger discovery of devices which have already been seen.""" self._integration_matcher.async_clear_address(address) + + def async_register_scanner(self, scanner: HaScanner) -> CALLBACK_TYPE: + """Register a new scanner.""" + + def _unregister_scanner() -> None: + self._scanners.remove(scanner) + + self._scanners.append(scanner) + return _unregister_scanner + + @hass_callback + def async_register_bleak_callback( + self, callback: AdvertisementDataCallback, filters: dict[str, set[str]] + ) -> CALLBACK_TYPE: + """Register a callback.""" + callback_entry = (callback, filters) + self._bleak_callbacks.append(callback_entry) + + @hass_callback + def _remove_callback() -> None: + self._bleak_callbacks.remove(callback_entry) + + # Replay the history since otherwise we miss devices + # that were already discovered before the callback was registered + # or we are in passive mode + for device, advertisement_data, _, _ in self.history.values(): + _dispatch_bleak_callback(callback, filters, device, advertisement_data) + + return _remove_callback diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 9c942d9f411..49f9e49db54 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -74,10 +74,6 @@ class IntegrationMatcher: MAX_REMEMBER_ADDRESSES ) - def async_clear_history(self) -> None: - """Clear the history.""" - self._matched = {} - def async_clear_address(self, address: str) -> None: """Clear the history matches for a set of domains.""" self._matched.pop(address, None) diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index d5cb1429a2a..1006ed912dd 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -9,25 +9,26 @@ from enum import Enum import logging from typing import TYPE_CHECKING, Any, Final -from bleak import BleakScanner from bleak.backends.scanner import ( AdvertisementData, AdvertisementDataCallback, BaseBleakScanner, ) -from homeassistant.core import CALLBACK_TYPE, callback as hass_callback +from homeassistant.core import CALLBACK_TYPE from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo if TYPE_CHECKING: from bleak.backends.device import BLEDevice + from .manager import BluetoothManager + _LOGGER = logging.getLogger(__name__) FILTER_UUIDS: Final = "UUIDs" -HA_BLEAK_SCANNER: HaBleakScanner | None = None +MANAGER: BluetoothManager | None = None @dataclass @@ -73,89 +74,6 @@ BluetoothCallback = Callable[[BluetoothServiceInfoBleak, BluetoothChange], None] ProcessAdvertisementCallback = Callable[[BluetoothServiceInfoBleak], bool] -def _dispatch_callback( - callback: AdvertisementDataCallback, - filters: dict[str, set[str]], - device: BLEDevice, - advertisement_data: AdvertisementData, -) -> None: - """Dispatch the callback.""" - if not callback: - # Callback destroyed right before being called, ignore - return # type: ignore[unreachable] - - if (uuids := filters.get(FILTER_UUIDS)) and not uuids.intersection( - advertisement_data.service_uuids - ): - return - - try: - callback(device, advertisement_data) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error in callback: %s", callback) - - -class HaBleakScanner(BleakScanner): - """BleakScanner that cannot be stopped.""" - - def __init__( # pylint: disable=super-init-not-called - self, *args: Any, **kwargs: Any - ) -> None: - """Initialize the BleakScanner.""" - self._callbacks: list[ - tuple[AdvertisementDataCallback, dict[str, set[str]]] - ] = [] - self.history: dict[str, tuple[BLEDevice, AdvertisementData]] = {} - # Init called later in async_setup if we are enabling the scanner - # since init has side effects that can throw exceptions - self._setup = False - - @hass_callback - def async_setup(self, *args: Any, **kwargs: Any) -> None: - """Deferred setup of the BleakScanner since __init__ has side effects.""" - if not self._setup: - super().__init__(*args, **kwargs) - self._setup = True - - @hass_callback - def async_reset(self) -> None: - """Reset the scanner so it can be setup again.""" - self.history = {} - self._setup = False - - @hass_callback - def async_register_callback( - self, callback: AdvertisementDataCallback, filters: dict[str, set[str]] - ) -> CALLBACK_TYPE: - """Register a callback.""" - callback_entry = (callback, filters) - self._callbacks.append(callback_entry) - - @hass_callback - def _remove_callback() -> None: - self._callbacks.remove(callback_entry) - - # Replay the history since otherwise we miss devices - # that were already discovered before the callback was registered - # or we are in passive mode - for device, advertisement_data in self.history.values(): - _dispatch_callback(callback, filters, device, advertisement_data) - - return _remove_callback - - def async_callback_dispatcher( - self, device: BLEDevice, advertisement_data: AdvertisementData - ) -> None: - """Dispatch the callback. - - Here we get the actual callback from bleak and dispatch - it to all the wrapped HaBleakScannerWrapper classes - """ - self.history[device.address] = (device, advertisement_data) - for callback_filters in self._callbacks: - _dispatch_callback(*callback_filters, device, advertisement_data) - - class HaBleakScannerWrapper(BaseBleakScanner): """A wrapper that uses the single instance.""" @@ -215,8 +133,8 @@ class HaBleakScannerWrapper(BaseBleakScanner): @property def discovered_devices(self) -> list[BLEDevice]: """Return a list of discovered devices.""" - assert HA_BLEAK_SCANNER is not None - return HA_BLEAK_SCANNER.discovered_devices + assert MANAGER is not None + return list(MANAGER.async_discovered_devices()) def register_detection_callback( self, callback: AdvertisementDataCallback | None @@ -235,9 +153,9 @@ class HaBleakScannerWrapper(BaseBleakScanner): return self._cancel_callback() super().register_detection_callback(self._adv_data_callback) - assert HA_BLEAK_SCANNER is not None + assert MANAGER is not None assert self._callback is not None - self._detection_cancel = HA_BLEAK_SCANNER.async_register_callback( + self._detection_cancel = MANAGER.async_register_bleak_callback( self._callback, self._mapped_filters ) diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py new file mode 100644 index 00000000000..c3d45dbde95 --- /dev/null +++ b/homeassistant/components/bluetooth/scanner.py @@ -0,0 +1,241 @@ +"""The bluetooth integration.""" +from __future__ import annotations + +import asyncio +from collections.abc import Callable +from datetime import datetime +import logging +import time + +import async_timeout +import bleak +from bleak import BleakError +from bleak.backends.device import BLEDevice +from bleak.backends.scanner import AdvertisementData +from dbus_next import InvalidMessageError + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HomeAssistant, + callback as hass_callback, +) +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.package import is_docker_env + +from .const import ( + DEFAULT_ADAPTERS, + SCANNER_WATCHDOG_INTERVAL, + SCANNER_WATCHDOG_TIMEOUT, + SOURCE_LOCAL, + START_TIMEOUT, +) +from .models import BluetoothScanningMode + +OriginalBleakScanner = bleak.BleakScanner +MONOTONIC_TIME = time.monotonic + + +_LOGGER = logging.getLogger(__name__) + + +MONOTONIC_TIME = time.monotonic + + +SCANNING_MODE_TO_BLEAK = { + BluetoothScanningMode.ACTIVE: "active", + BluetoothScanningMode.PASSIVE: "passive", +} + + +def create_bleak_scanner( + scanning_mode: BluetoothScanningMode, adapter: str | None +) -> bleak.BleakScanner: + """Create a Bleak scanner.""" + scanner_kwargs = {"scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode]} + if adapter and adapter not in DEFAULT_ADAPTERS: + scanner_kwargs["adapter"] = adapter + _LOGGER.debug("Initializing bluetooth scanner with %s", scanner_kwargs) + try: + return OriginalBleakScanner(**scanner_kwargs) # type: ignore[arg-type] + except (FileNotFoundError, BleakError) as ex: + raise RuntimeError(f"Failed to initialize Bluetooth: {ex}") from ex + + +class HaScanner: + """Operate a BleakScanner. + + Multiple BleakScanner can be used at the same time + if there are multiple adapters. This is only useful + if the adapters are not located physically next to each other. + + Example use cases are usbip, a long extension cable, usb to bluetooth + over ethernet, usb over ethernet, etc. + """ + + def __init__( + self, hass: HomeAssistant, scanner: bleak.BleakScanner, adapter: str | None + ) -> None: + """Init bluetooth discovery.""" + self.hass = hass + self.scanner = scanner + self.adapter = adapter + self._start_stop_lock = asyncio.Lock() + self._cancel_stop: CALLBACK_TYPE | None = None + self._cancel_watchdog: CALLBACK_TYPE | None = None + self._last_detection = 0.0 + self._callbacks: list[ + Callable[[BLEDevice, AdvertisementData, float, str], None] + ] = [] + self.name = self.adapter or "default" + self.source = self.adapter or SOURCE_LOCAL + + @property + def discovered_devices(self) -> list[BLEDevice]: + """Return a list of discovered devices.""" + return self.scanner.discovered_devices + + @hass_callback + def async_register_callback( + self, callback: Callable[[BLEDevice, AdvertisementData, float, str], None] + ) -> CALLBACK_TYPE: + """Register a callback. + + Currently this is used to feed the callbacks into the + central manager. + """ + + def _remove() -> None: + self._callbacks.remove(callback) + + self._callbacks.append(callback) + return _remove + + @hass_callback + def _async_detection_callback( + self, + ble_device: BLEDevice, + advertisement_data: AdvertisementData, + ) -> None: + """Call the callback when an advertisement is received. + + Currently this is used to feed the callbacks into the + central manager. + """ + self._last_detection = MONOTONIC_TIME() + for callback in self._callbacks: + callback(ble_device, advertisement_data, self._last_detection, self.source) + + async def async_start(self) -> None: + """Start bluetooth scanner.""" + self.scanner.register_detection_callback(self._async_detection_callback) + + async with self._start_stop_lock: + await self._async_start() + + async def _async_start(self) -> None: + """Start bluetooth scanner under the lock.""" + try: + async with async_timeout.timeout(START_TIMEOUT): + await self.scanner.start() # type: ignore[no-untyped-call] + except InvalidMessageError as ex: + _LOGGER.debug( + "%s: Invalid DBus message received: %s", self.name, ex, exc_info=True + ) + raise ConfigEntryNotReady( + f"{self.name}: Invalid DBus message received: {ex}; try restarting `dbus`" + ) from ex + except BrokenPipeError as ex: + _LOGGER.debug( + "%s: DBus connection broken: %s", self.name, ex, exc_info=True + ) + if is_docker_env(): + raise ConfigEntryNotReady( + f"{self.name}: DBus connection broken: {ex}; try restarting `bluetooth`, `dbus`, and finally the docker container" + ) from ex + raise ConfigEntryNotReady( + f"{self.name}: DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`" + ) from ex + except FileNotFoundError as ex: + _LOGGER.debug( + "%s: FileNotFoundError while starting bluetooth: %s", + self.name, + ex, + exc_info=True, + ) + if is_docker_env(): + raise ConfigEntryNotReady( + f"{self.name}: DBus service not found; docker config may be missing `-v /run/dbus:/run/dbus:ro`: {ex}" + ) from ex + raise ConfigEntryNotReady( + f"{self.name}: DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}" + ) from ex + except asyncio.TimeoutError as ex: + raise ConfigEntryNotReady( + f"{self.name}: Timed out starting Bluetooth after {START_TIMEOUT} seconds" + ) from ex + except BleakError as ex: + _LOGGER.debug( + "%s: BleakError while starting bluetooth: %s", + self.name, + ex, + exc_info=True, + ) + raise ConfigEntryNotReady( + f"{self.name}: Failed to start Bluetooth: {ex}" + ) from ex + self._async_setup_scanner_watchdog() + self._cancel_stop = self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping + ) + + @hass_callback + def _async_setup_scanner_watchdog(self) -> None: + """If Dbus gets restarted or updated, we need to restart the scanner.""" + self._last_detection = MONOTONIC_TIME() + self._cancel_watchdog = async_track_time_interval( + self.hass, self._async_scanner_watchdog, SCANNER_WATCHDOG_INTERVAL + ) + + async def _async_scanner_watchdog(self, now: datetime) -> None: + """Check if the scanner is running.""" + time_since_last_detection = MONOTONIC_TIME() - self._last_detection + if time_since_last_detection < SCANNER_WATCHDOG_TIMEOUT: + return + _LOGGER.info( + "%s: Bluetooth scanner has gone quiet for %s, restarting", + self.name, + SCANNER_WATCHDOG_INTERVAL, + ) + async with self._start_stop_lock: + await self._async_stop() + await self._async_start() + + async def _async_hass_stopping(self, event: Event) -> None: + """Stop the Bluetooth integration at shutdown.""" + self._cancel_stop = None + await self.async_stop() + + async def async_stop(self) -> None: + """Stop bluetooth scanner.""" + async with self._start_stop_lock: + await self._async_stop() + + async def _async_stop(self) -> None: + """Stop bluetooth discovery under the lock.""" + _LOGGER.debug("Stopping bluetooth discovery") + if self._cancel_watchdog: + self._cancel_watchdog() + self._cancel_watchdog = None + if self._cancel_stop: + self._cancel_stop() + self._cancel_stop = None + try: + await self.scanner.stop() # type: ignore[no-untyped-call] + except BleakError as ex: + # This is not fatal, and they may want to reload + # the config entry to restart the scanner if they + # change the bluetooth dongle. + _LOGGER.error("Error stopping scanner: %s", ex) diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index 3dc80d55590..2b6ad75d2b9 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -1,8 +1,38 @@ """Tests for the Bluetooth integration.""" -from homeassistant.components.bluetooth import models + +import time +from unittest.mock import patch + +from bleak.backends.scanner import AdvertisementData, BLEDevice + +from homeassistant.components.bluetooth import SOURCE_LOCAL, models +from homeassistant.components.bluetooth.manager import BluetoothManager -def _get_underlying_scanner(): +def _get_manager() -> BluetoothManager: + """Return the bluetooth manager.""" + return models.MANAGER + + +def inject_advertisement(device: BLEDevice, adv: AdvertisementData) -> None: """Return the underlying scanner that has been wrapped.""" - return models.HA_BLEAK_SCANNER + return _get_manager().scanner_adv_received( + device, adv, time.monotonic(), SOURCE_LOCAL + ) + + +def patch_all_discovered_devices(mock_discovered: list[BLEDevice]) -> None: + """Mock all the discovered devices from all the scanners.""" + manager = _get_manager() + return patch.object( + manager, "async_all_discovered_devices", return_value=mock_discovered + ) + + +def patch_discovered_devices(mock_discovered: list[BLEDevice]) -> None: + """Mock the combined best path to discovered devices from all the scanners.""" + manager = _get_manager() + return patch.object( + manager, "async_discovered_devices", return_value=mock_discovered + ) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 2387d35fc23..84c37300dc4 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -5,7 +5,6 @@ from unittest.mock import MagicMock, patch from bleak import BleakError from bleak.backends.scanner import AdvertisementData, BLEDevice -from dbus_next import InvalidMessageError import pytest from homeassistant.components import bluetooth @@ -16,16 +15,12 @@ from homeassistant.components.bluetooth import ( async_process_advertisements, async_rediscover_address, async_track_unavailable, - manager, models, + scanner, ) from homeassistant.components.bluetooth.const import ( - CONF_ADAPTER, - SCANNER_WATCHDOG_INTERVAL, - SCANNER_WATCHDOG_TIMEOUT, SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, - UNIX_DEFAULT_BLUETOOTH_ADAPTER, ) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP @@ -33,7 +28,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import _get_underlying_scanner +from . import _get_manager, inject_advertisement, patch_discovered_devices from tests.common import MockConfigEntry, async_fire_time_changed @@ -63,7 +58,7 @@ async def test_setup_and_stop_no_bluetooth(hass, caplog): {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} ] with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup", + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner", side_effect=BleakError, ) as mock_ha_bleak_scanner, patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -85,9 +80,7 @@ async def test_setup_and_stop_broken_bluetooth(hass, caplog): """Test we fail gracefully when bluetooth/dbus is broken.""" mock_bt = [] with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=BleakError, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -112,10 +105,8 @@ async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): async def _mock_hang(): await asyncio.sleep(1) - with patch.object(manager, "START_TIMEOUT", 0), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", + with patch.object(scanner, "START_TIMEOUT", 0), patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=_mock_hang, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -136,9 +127,7 @@ async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): """Test we retry if the adapter is not yet available.""" mock_bt = [] with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=BleakError, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -157,14 +146,14 @@ async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): assert entry.state == ConfigEntryState.SETUP_RETRY with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", ): async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) await hass.async_block_till_done() assert entry.state == ConfigEntryState.LOADED with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.stop", + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.stop", ): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() @@ -174,9 +163,7 @@ async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): """Test we can successfully reload when the entry is in a retry state.""" mock_bt = [] with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=BleakError, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -195,7 +182,7 @@ async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): assert entry.state == ConfigEntryState.SETUP_RETRY with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", ): await hass.config_entries.async_reload(entry.entry_id) await hass.async_block_till_done() @@ -203,7 +190,7 @@ async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): assert entry.state == ConfigEntryState.LOADED with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.stop", + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.stop", ): hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() @@ -213,7 +200,7 @@ async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): """Test we fail gracefully when asking for discovered devices and there is no blueooth.""" mock_bt = [] with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup", + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner", side_effect=FileNotFoundError, ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt @@ -252,7 +239,7 @@ async def test_discovery_match_by_service_uuid( wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - _get_underlying_scanner()._callback(wrong_device, wrong_adv) + inject_advertisement(wrong_device, wrong_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -262,7 +249,7 @@ async def test_discovery_match_by_service_uuid( local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -289,7 +276,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): wrong_device = BLEDevice("44:44:33:11:23:45", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - _get_underlying_scanner()._callback(wrong_device, wrong_adv) + inject_advertisement(wrong_device, wrong_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -297,7 +284,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -343,21 +330,21 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( # 1st discovery with no manufacturer data # should not trigger config flow - _get_underlying_scanner()._callback(hkc_device, hkc_adv_no_mfr_data) + inject_advertisement(hkc_device, hkc_adv_no_mfr_data) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 mock_config_flow.reset_mock() # 2nd discovery with manufacturer data # should trigger a config flow - _get_underlying_scanner()._callback(hkc_device, hkc_adv) + inject_advertisement(hkc_device, hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "homekit_controller" mock_config_flow.reset_mock() # 3rd discovery should not generate another flow - _get_underlying_scanner()._callback(hkc_device, hkc_adv) + inject_advertisement(hkc_device, hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -368,7 +355,7 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( local_name="lock", service_uuids=[], manufacturer_data={76: b"\x02"} ) - _get_underlying_scanner()._callback(not_hkc_device, not_hkc_adv) + inject_advertisement(not_hkc_device, not_hkc_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -377,7 +364,7 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( local_name="lock", service_uuids=[], manufacturer_data={21: b"\x02"} ) - _get_underlying_scanner()._callback(not_apple_device, not_apple_adv) + inject_advertisement(not_apple_device, not_apple_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -453,21 +440,21 @@ async def test_discovery_match_by_service_data_uuid_then_others( ) # 1st discovery should not generate a flow because the # service_data_uuid is not in the advertisement - _get_underlying_scanner()._callback(device, adv_without_service_data_uuid) + inject_advertisement(device, adv_without_service_data_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 mock_config_flow.reset_mock() # 2nd discovery should not generate a flow because the # service_data_uuid is not in the advertisement - _get_underlying_scanner()._callback(device, adv_without_service_data_uuid) + inject_advertisement(device, adv_without_service_data_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 mock_config_flow.reset_mock() # 3rd discovery should generate a flow because the # manufacturer_data is in the advertisement - _get_underlying_scanner()._callback(device, adv_with_mfr_data) + inject_advertisement(device, adv_with_mfr_data) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "other_domain" @@ -476,7 +463,7 @@ async def test_discovery_match_by_service_data_uuid_then_others( # 4th discovery should generate a flow because the # service_data_uuid is in the advertisement and # we never saw a service_data_uuid before - _get_underlying_scanner()._callback(device, adv_with_service_data_uuid) + inject_advertisement(device, adv_with_service_data_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "my_domain" @@ -484,16 +471,14 @@ async def test_discovery_match_by_service_data_uuid_then_others( # 5th discovery should not generate a flow because the # we already saw an advertisement with the service_data_uuid - _get_underlying_scanner()._callback(device, adv_with_service_data_uuid) + inject_advertisement(device, adv_with_service_data_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 # 6th discovery should not generate a flow because the # manufacturer_data is in the advertisement # and we saw manufacturer_data before - _get_underlying_scanner()._callback( - device, adv_with_service_data_uuid_and_mfr_data - ) + inject_advertisement(device, adv_with_service_data_uuid_and_mfr_data) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 mock_config_flow.reset_mock() @@ -501,7 +486,7 @@ async def test_discovery_match_by_service_data_uuid_then_others( # 7th discovery should generate a flow because the # service_uuids is in the advertisement # and we never saw service_uuids before - _get_underlying_scanner()._callback( + inject_advertisement( device, adv_with_service_data_uuid_and_mfr_data_and_service_uuid ) await hass.async_block_till_done() @@ -514,7 +499,7 @@ async def test_discovery_match_by_service_data_uuid_then_others( # 8th discovery should not generate a flow # since all fields have been seen at this point - _get_underlying_scanner()._callback( + inject_advertisement( device, adv_with_service_data_uuid_and_mfr_data_and_service_uuid ) await hass.async_block_till_done() @@ -523,19 +508,19 @@ async def test_discovery_match_by_service_data_uuid_then_others( # 9th discovery should not generate a flow # since all fields have been seen at this point - _get_underlying_scanner()._callback(device, adv_with_service_uuid) + inject_advertisement(device, adv_with_service_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 # 10th discovery should not generate a flow # since all fields have been seen at this point - _get_underlying_scanner()._callback(device, adv_with_service_data_uuid) + inject_advertisement(device, adv_with_service_data_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 # 11th discovery should not generate a flow # since all fields have been seen at this point - _get_underlying_scanner()._callback(device, adv_without_service_data_uuid) + inject_advertisement(device, adv_without_service_data_uuid) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -582,7 +567,7 @@ async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( # 1st discovery with matches service_uuid # should trigger config flow - _get_underlying_scanner()._callback(device, adv_service_uuids) + inject_advertisement(device, adv_service_uuids) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "my_domain" @@ -590,19 +575,19 @@ async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( # 2nd discovery with manufacturer data # should trigger a config flow - _get_underlying_scanner()._callback(device, adv_manufacturer_data) + inject_advertisement(device, adv_manufacturer_data) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 assert mock_config_flow.mock_calls[0][1][0] == "my_domain" mock_config_flow.reset_mock() # 3rd discovery should not generate another flow - _get_underlying_scanner()._callback(device, adv_service_uuids) + inject_advertisement(device, adv_service_uuids) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 # 4th discovery should not generate another flow - _get_underlying_scanner()._callback(device, adv_manufacturer_data) + inject_advertisement(device, adv_manufacturer_data) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 0 @@ -628,10 +613,10 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth): local_name="wohand", service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 1 @@ -639,7 +624,7 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth): async_rediscover_address(hass, "44:44:33:11:23:45") - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(mock_config_flow.mock_calls) == 2 @@ -672,10 +657,10 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): wrong_device = BLEDevice("44:44:33:11:23:42", "wrong_name") wrong_adv = AdvertisementData(local_name="wrong_name", service_uuids=[]) - _get_underlying_scanner()._callback(wrong_device, wrong_adv) + inject_advertisement(wrong_device, wrong_adv) switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) wrong_device_went_unavailable = False switchbot_device_went_unavailable = False @@ -709,8 +694,8 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): assert wrong_device_went_unavailable is True # See the devices again - _get_underlying_scanner()._callback(wrong_device, wrong_adv) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(wrong_device, wrong_adv) + inject_advertisement(switchbot_device, switchbot_adv) # Cancel the callbacks wrong_device_unavailable_cancel() switchbot_device_unavailable_cancel() @@ -776,25 +761,25 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) await hass.async_block_till_done() empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") # 3rd callback raises ValueError but is still tracked - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) await hass.async_block_till_done() cancel() # 4th callback should not be tracked since we canceled - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) await hass.async_block_till_done() assert len(callbacks) == 3 @@ -862,25 +847,25 @@ async def test_register_callback_by_address( service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) await hass.async_block_till_done() empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") # 3rd callback raises ValueError but is still tracked - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) await hass.async_block_till_done() cancel() # 4th callback should not be tracked since we canceled - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) await hass.async_block_till_done() # Now register again with a callback that fails to @@ -953,7 +938,7 @@ async def test_register_callback_survives_reload( service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) assert len(callbacks) == 1 service_info: BluetoothServiceInfo = callbacks[0][0] assert service_info.name == "wohand" @@ -964,7 +949,7 @@ async def test_register_callback_survives_reload( await hass.config_entries.async_reload(entry.entry_id) await hass.async_block_till_done() - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) assert len(callbacks) == 2 service_info: BluetoothServiceInfo = callbacks[1][0] assert service_info.name == "wohand" @@ -1001,9 +986,9 @@ async def test_process_advertisements_bail_on_good_advertisement( service_data={"00000d00-0000-1000-8000-00805f9b34fa": b"H\x10c"}, ) - _get_underlying_scanner()._callback(device, adv) - _get_underlying_scanner()._callback(device, adv) - _get_underlying_scanner()._callback(device, adv) + inject_advertisement(device, adv) + inject_advertisement(device, adv) + inject_advertisement(device, adv) await asyncio.sleep(0) @@ -1043,14 +1028,14 @@ async def test_process_advertisements_ignore_bad_advertisement( # The goal of this loop is to make sure that async_process_advertisements sees at least one # callback that returns False while not done.is_set(): - _get_underlying_scanner()._callback(device, adv) + inject_advertisement(device, adv) await asyncio.sleep(0) # Set the return value and mutate the advertisement # Check that scan ends and correct advertisement data is returned return_value.set() adv.service_data["00000d00-0000-1000-8000-00805f9b34fa"] = b"H\x10c" - _get_underlying_scanner()._callback(device, adv) + inject_advertisement(device, adv) await asyncio.sleep(0) result = await handle @@ -1105,20 +1090,18 @@ async def test_wrapped_instance_with_filter( empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - assert _get_underlying_scanner() is not None + assert _get_manager() is not None scanner = models.HaBleakScannerWrapper( filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} ) scanner.register_detection_callback(_device_detected) - mock_discovered = [MagicMock()] - type(_get_underlying_scanner()).discovered_devices = mock_discovered - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() discovered = await scanner.discover(timeout=0) assert len(discovered) == 1 - assert discovered == mock_discovered + assert discovered == [switchbot_device] assert len(detected) == 1 scanner.register_detection_callback(_device_detected) @@ -1128,17 +1111,17 @@ async def test_wrapped_instance_with_filter( # We should get a reply from the history when we register again assert len(detected) == 3 - type(_get_underlying_scanner()).discovered_devices = [] - discovered = await scanner.discover(timeout=0) - assert len(discovered) == 0 - assert discovered == [] + with patch_discovered_devices([]): + discovered = await scanner.discover(timeout=0) + assert len(discovered) == 0 + assert discovered == [] - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) assert len(detected) == 4 # The filter we created in the wrapped scanner with should be respected # and we should not get another callback - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) assert len(detected) == 4 @@ -1176,22 +1159,21 @@ async def test_wrapped_instance_with_service_uuids( empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - assert _get_underlying_scanner() is not None + assert _get_manager() is not None scanner = models.HaBleakScannerWrapper( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) scanner.register_detection_callback(_device_detected) - type(_get_underlying_scanner()).discovered_devices = [MagicMock()] for _ in range(2): - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 2 # The UUIDs list we created in the wrapped scanner with should be respected # and we should not get another callback - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) assert len(detected) == 2 @@ -1229,15 +1211,15 @@ async def test_wrapped_instance_with_broken_callbacks( service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) - assert _get_underlying_scanner() is not None + assert _get_manager() is not None scanner = models.HaBleakScannerWrapper( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) scanner.register_detection_callback(_device_detected) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 1 @@ -1275,23 +1257,22 @@ async def test_wrapped_instance_changes_uuids( empty_device = BLEDevice("11:22:33:44:55:66", "empty") empty_adv = AdvertisementData(local_name="empty") - assert _get_underlying_scanner() is not None + assert _get_manager() is not None scanner = models.HaBleakScannerWrapper() scanner.set_scanning_filter( service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] ) scanner.register_detection_callback(_device_detected) - type(_get_underlying_scanner()).discovered_devices = [MagicMock()] for _ in range(2): - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 2 # The UUIDs list we created in the wrapped scanner with should be respected # and we should not get another callback - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) assert len(detected) == 2 @@ -1328,23 +1309,22 @@ async def test_wrapped_instance_changes_filters( empty_device = BLEDevice("11:22:33:44:55:62", "empty") empty_adv = AdvertisementData(local_name="empty") - assert _get_underlying_scanner() is not None + assert _get_manager() is not None scanner = models.HaBleakScannerWrapper() scanner.set_scanning_filter( filters={"UUIDs": ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"]} ) scanner.register_detection_callback(_device_detected) - type(_get_underlying_scanner()).discovered_devices = [MagicMock()] for _ in range(2): - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert len(detected) == 2 # The UUIDs list we created in the wrapped scanner with should be respected # and we should not get another callback - _get_underlying_scanner()._callback(empty_device, empty_adv) + inject_advertisement(empty_device, empty_adv) assert len(detected) == 2 @@ -1363,7 +1343,7 @@ async def test_wrapped_instance_unsupported_filter( with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert _get_underlying_scanner() is not None + assert _get_manager() is not None scanner = models.HaBleakScannerWrapper() scanner.set_scanning_filter( filters={ @@ -1401,7 +1381,7 @@ async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData(local_name="wohand", service_uuids=[]) - _get_underlying_scanner()._callback(switchbot_device, switchbot_adv) + inject_advertisement(switchbot_device, switchbot_adv) await hass.async_block_till_done() assert ( @@ -1501,235 +1481,7 @@ async def test_no_auto_detect_bluetooth_adapters_windows(hass): assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 0 -async def test_raising_runtime_error_when_no_bluetooth(hass): - """Test we raise an exception if we try to get the scanner when its not there.""" - with pytest.raises(RuntimeError): - bluetooth.async_get_scanner(hass) - - async def test_getting_the_scanner_returns_the_wrapped_instance(hass, enable_bluetooth): """Test getting the scanner returns the wrapped instance.""" scanner = bluetooth.async_get_scanner(hass) assert isinstance(scanner, models.HaBleakScannerWrapper) - - -async def test_config_entry_can_be_reloaded_when_stop_raises( - hass, caplog, enable_bluetooth -): - """Test we can reload if stopping the scanner raises.""" - entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] - assert entry.state == ConfigEntryState.LOADED - - with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.stop", - side_effect=BleakError, - ): - await hass.config_entries.async_reload(entry.entry_id) - await hass.async_block_till_done() - - assert entry.state == ConfigEntryState.LOADED - assert "Error stopping scanner" in caplog.text - - -async def test_changing_the_adapter_at_runtime(hass): - """Test we can change the adapter at runtime.""" - entry = MockConfigEntry( - domain=bluetooth.DOMAIN, - data={}, - options={CONF_ADAPTER: UNIX_DEFAULT_BLUETOOTH_ADAPTER}, - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ) as mock_setup, patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.stop" - ): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert "adapter" not in mock_setup.mock_calls[0][2] - - entry.options = {CONF_ADAPTER: "hci1"} - - await hass.config_entries.async_reload(entry.entry_id) - await hass.async_block_till_done() - assert mock_setup.mock_calls[1][2]["adapter"] == "hci1" - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - - -async def test_dbus_socket_missing_in_container(hass, caplog): - """Test we handle dbus being missing in the container.""" - - with patch( - "homeassistant.components.bluetooth.manager.is_docker_env", return_value=True - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", - side_effect=FileNotFoundError, - ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - assert "/run/dbus" in caplog.text - assert "docker" in caplog.text - - -async def test_dbus_socket_missing(hass, caplog): - """Test we handle dbus being missing.""" - - with patch( - "homeassistant.components.bluetooth.manager.is_docker_env", return_value=False - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", - side_effect=FileNotFoundError, - ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - assert "DBus" in caplog.text - assert "docker" not in caplog.text - - -async def test_dbus_broken_pipe_in_container(hass, caplog): - """Test we handle dbus broken pipe in the container.""" - - with patch( - "homeassistant.components.bluetooth.manager.is_docker_env", return_value=True - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", - side_effect=BrokenPipeError, - ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - assert "dbus" in caplog.text - assert "restarting" in caplog.text - assert "container" in caplog.text - - -async def test_dbus_broken_pipe(hass, caplog): - """Test we handle dbus broken pipe.""" - - with patch( - "homeassistant.components.bluetooth.manager.is_docker_env", return_value=False - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", - side_effect=BrokenPipeError, - ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - assert "DBus" in caplog.text - assert "restarting" in caplog.text - assert "container" not in caplog.text - - -async def test_invalid_dbus_message(hass, caplog): - """Test we handle invalid dbus message.""" - - with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.async_setup" - ), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", - side_effect=InvalidMessageError, - ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - assert "dbus" in caplog.text - - -async def test_recovery_from_dbus_restart( - hass, mock_bleak_scanner_start, enable_bluetooth -): - """Test we can recover when DBus gets restarted out from under us.""" - assert await async_setup_component(hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}) - await hass.async_block_till_done() - assert len(mock_bleak_scanner_start.mock_calls) == 1 - - start_time_monotonic = 1000 - scanner = _get_underlying_scanner() - mock_discovered = [MagicMock()] - type(scanner).discovered_devices = mock_discovered - - # Ensure we don't restart the scanner if we don't need to - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=start_time_monotonic + 10, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() - - assert len(mock_bleak_scanner_start.mock_calls) == 1 - - # Fire a callback to reset the timer - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=start_time_monotonic, - ): - scanner._callback( - BLEDevice("44:44:33:11:23:42", "any_name"), - AdvertisementData(local_name="any_name"), - ) - - # Ensure we don't restart the scanner if we don't need to - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=start_time_monotonic + 20, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() - - assert len(mock_bleak_scanner_start.mock_calls) == 1 - - # We hit the timer, so we restart the scanner - with patch( - "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", - return_value=start_time_monotonic + SCANNER_WATCHDOG_TIMEOUT, - ): - async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) - await hass.async_block_till_done() - - assert len(mock_bleak_scanner_start.mock_calls) == 2 diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index 12531c52e40..9a90f99d11b 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -20,7 +20,7 @@ from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import _get_underlying_scanner +from . import _get_manager, patch_all_discovered_devices from tests.common import async_fire_time_changed @@ -178,11 +178,10 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert coordinator.available is True - scanner = _get_underlying_scanner() + scanner = _get_manager() - with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", - [MagicMock(address="44:44:33:11:23:45")], + with patch_all_discovered_devices( + [MagicMock(address="44:44:33:11:23:45")] ), patch.object( scanner, "history", @@ -197,9 +196,8 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( saved_callback(GENERIC_BLUETOOTH_SERVICE_INFO, BluetoothChange.ADVERTISEMENT) assert coordinator.available is True - with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", - [MagicMock(address="44:44:33:11:23:45")], + with patch_all_discovered_devices( + [MagicMock(address="44:44:33:11:23:45")] ), patch.object( scanner, "history", diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index 6b21d1aa32c..ac35e9f2bee 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -32,7 +32,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import _get_underlying_scanner +from . import _get_manager, patch_all_discovered_devices from tests.common import MockEntityPlatform, async_fire_time_changed @@ -246,11 +246,9 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True assert processor.available is True - scanner = _get_underlying_scanner() - - with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", - [MagicMock(address="44:44:33:11:23:45")], + scanner = _get_manager() + with patch_all_discovered_devices( + [MagicMock(address="44:44:33:11:23:45")] ), patch.object( scanner, "history", @@ -268,9 +266,8 @@ async def test_unavailable_after_no_data(hass, mock_bleak_scanner_start): assert coordinator.available is True assert processor.available is True - with patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.discovered_devices", - [MagicMock(address="44:44:33:11:23:45")], + with patch_all_discovered_devices( + [MagicMock(address="44:44:33:11:23:45")] ), patch.object( scanner, "history", diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py new file mode 100644 index 00000000000..032b67662df --- /dev/null +++ b/tests/components/bluetooth/test_scanner.py @@ -0,0 +1,264 @@ +"""Tests for the Bluetooth integration scanners.""" +from unittest.mock import MagicMock, patch + +from bleak import BleakError +from bleak.backends.scanner import ( + AdvertisementData, + AdvertisementDataCallback, + BLEDevice, +) +from dbus_next import InvalidMessageError + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth.const import ( + CONF_ADAPTER, + SCANNER_WATCHDOG_INTERVAL, + SCANNER_WATCHDOG_TIMEOUT, + UNIX_DEFAULT_BLUETOOTH_ADAPTER, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from . import _get_manager + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_config_entry_can_be_reloaded_when_stop_raises( + hass, caplog, enable_bluetooth +): + """Test we can reload if stopping the scanner raises.""" + entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] + assert entry.state == ConfigEntryState.LOADED + + with patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.stop", + side_effect=BleakError, + ): + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + assert "Error stopping scanner" in caplog.text + + +async def test_changing_the_adapter_at_runtime(hass): + """Test we can change the adapter at runtime.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, + data={}, + options={CONF_ADAPTER: UNIX_DEFAULT_BLUETOOTH_ADAPTER}, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start" + ), patch("homeassistant.components.bluetooth.scanner.OriginalBleakScanner.stop"): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entry.options = {CONF_ADAPTER: "hci1"} + + await hass.config_entries.async_reload(entry.entry_id) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + +async def test_dbus_socket_missing_in_container(hass, caplog): + """Test we handle dbus being missing in the container.""" + + with patch( + "homeassistant.components.bluetooth.scanner.is_docker_env", return_value=True + ), patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", + side_effect=FileNotFoundError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "/run/dbus" in caplog.text + assert "docker" in caplog.text + + +async def test_dbus_socket_missing(hass, caplog): + """Test we handle dbus being missing.""" + + with patch( + "homeassistant.components.bluetooth.scanner.is_docker_env", return_value=False + ), patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", + side_effect=FileNotFoundError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "DBus" in caplog.text + assert "docker" not in caplog.text + + +async def test_dbus_broken_pipe_in_container(hass, caplog): + """Test we handle dbus broken pipe in the container.""" + + with patch( + "homeassistant.components.bluetooth.scanner.is_docker_env", return_value=True + ), patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", + side_effect=BrokenPipeError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "dbus" in caplog.text + assert "restarting" in caplog.text + assert "container" in caplog.text + + +async def test_dbus_broken_pipe(hass, caplog): + """Test we handle dbus broken pipe.""" + + with patch( + "homeassistant.components.bluetooth.scanner.is_docker_env", return_value=False + ), patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", + side_effect=BrokenPipeError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "DBus" in caplog.text + assert "restarting" in caplog.text + assert "container" not in caplog.text + + +async def test_invalid_dbus_message(hass, caplog): + """Test we handle invalid dbus message.""" + + with patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", + side_effect=InvalidMessageError, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + assert "dbus" in caplog.text + + +async def test_recovery_from_dbus_restart(hass): + """Test we can recover when DBus gets restarted out from under us.""" + + called_start = 0 + called_stop = 0 + _callback = None + mock_discovered = [] + + class MockBleakScanner: + async def start(self, *args, **kwargs): + """Mock Start.""" + nonlocal called_start + called_start += 1 + + async def stop(self, *args, **kwargs): + """Mock Start.""" + nonlocal called_stop + called_stop += 1 + + @property + def discovered_devices(self): + """Mock discovered_devices.""" + nonlocal mock_discovered + return mock_discovered + + def register_detection_callback(self, callback: AdvertisementDataCallback): + """Mock Register Detection Callback.""" + nonlocal _callback + _callback = callback + + scanner = MockBleakScanner() + + with patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner", + return_value=scanner, + ): + assert await async_setup_component( + hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} + ) + await hass.async_block_till_done() + assert called_start == 1 + + start_time_monotonic = 1000 + scanner = _get_manager() + mock_discovered = [MagicMock()] + + # Ensure we don't restart the scanner if we don't need to + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + 10, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert called_start == 1 + + # Fire a callback to reset the timer + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic, + ): + _callback( + BLEDevice("44:44:33:11:23:42", "any_name"), + AdvertisementData(local_name="any_name"), + ) + + # Ensure we don't restart the scanner if we don't need to + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + 20, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert called_start == 1 + + # We hit the timer, so we restart the scanner + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + SCANNER_WATCHDOG_TIMEOUT, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert called_start == 2 diff --git a/tests/conftest.py b/tests/conftest.py index 3b43fcd14ba..4c268206805 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -895,26 +895,18 @@ def mock_bleak_scanner_start(): # Late imports to avoid loading bleak unless we need it - import bleak # pylint: disable=import-outside-toplevel - from homeassistant.components.bluetooth import ( # pylint: disable=import-outside-toplevel - models as bluetooth_models, + scanner as bluetooth_scanner, ) - scanner = bleak.BleakScanner - bluetooth_models.HA_BLEAK_SCANNER = None - - with patch("homeassistant.components.bluetooth.models.HaBleakScanner.stop"), patch( - "homeassistant.components.bluetooth.models.HaBleakScanner.start", - ) as mock_bleak_scanner_start: - yield mock_bleak_scanner_start - # We need to drop the stop method from the object since we patched # out start and this fixture will expire before the stop method is called # when EVENT_HOMEASSISTANT_STOP is fired. - if bluetooth_models.HA_BLEAK_SCANNER: - bluetooth_models.HA_BLEAK_SCANNER.stop = AsyncMock() - bleak.BleakScanner = scanner + bluetooth_scanner.OriginalBleakScanner.stop = AsyncMock() + with patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", + ) as mock_bleak_scanner_start: + yield mock_bleak_scanner_start @pytest.fixture(name="mock_bluetooth") From 7bf13167d8170898c693b592e98e708c6fa28e6d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Aug 2022 11:42:12 -1000 Subject: [PATCH 3425/3516] Prevent bluetooth scanner from being shutdown by BleakClient not using BLEDevice (#76945) --- homeassistant/components/bluetooth/models.py | 34 ++++++++++- homeassistant/components/bluetooth/usage.py | 5 +- tests/components/bluetooth/test_usage.py | 60 +++++++++++++++++++- 3 files changed, 95 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/models.py b/homeassistant/components/bluetooth/models.py index 1006ed912dd..7857b02f121 100644 --- a/homeassistant/components/bluetooth/models.py +++ b/homeassistant/components/bluetooth/models.py @@ -9,6 +9,8 @@ from enum import Enum import logging from typing import TYPE_CHECKING, Any, Final +from bleak import BleakClient, BleakError +from bleak.backends.device import BLEDevice from bleak.backends.scanner import ( AdvertisementData, AdvertisementDataCallback, @@ -16,10 +18,10 @@ from bleak.backends.scanner import ( ) from homeassistant.core import CALLBACK_TYPE +from homeassistant.helpers.frame import report from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo if TYPE_CHECKING: - from bleak.backends.device import BLEDevice from .manager import BluetoothManager @@ -165,3 +167,33 @@ class HaBleakScannerWrapper(BaseBleakScanner): # Nothing to do if event loop is already closed with contextlib.suppress(RuntimeError): asyncio.get_running_loop().call_soon_threadsafe(self._detection_cancel) + + +class HaBleakClientWrapper(BleakClient): + """Wrap the BleakClient to ensure it does not shutdown our scanner. + + If an address is passed into BleakClient instead of a BLEDevice, + bleak will quietly start a new scanner under the hood to resolve + the address. This can cause a conflict with our scanner. We need + to handle translating the address to the BLEDevice in this case + to avoid the whole stack from getting stuck in an in progress state + when an integration does this. + """ + + def __init__( + self, address_or_ble_device: str | BLEDevice, *args: Any, **kwargs: Any + ) -> None: + """Initialize the BleakClient.""" + if isinstance(address_or_ble_device, BLEDevice): + super().__init__(address_or_ble_device, *args, **kwargs) + return + report( + "attempted to call BleakClient with an address instead of a BLEDevice", + exclude_integrations={"bluetooth"}, + error_if_core=False, + ) + assert MANAGER is not None + ble_device = MANAGER.async_ble_device_from_address(address_or_ble_device) + if ble_device is None: + raise BleakError(f"No device found for address {address_or_ble_device}") + super().__init__(ble_device, *args, **kwargs) diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py index b3a6783cf30..d282ca7415b 100644 --- a/homeassistant/components/bluetooth/usage.py +++ b/homeassistant/components/bluetooth/usage.py @@ -4,16 +4,19 @@ from __future__ import annotations import bleak -from .models import HaBleakScannerWrapper +from .models import HaBleakClientWrapper, HaBleakScannerWrapper ORIGINAL_BLEAK_SCANNER = bleak.BleakScanner +ORIGINAL_BLEAK_CLIENT = bleak.BleakClient def install_multiple_bleak_catcher() -> None: """Wrap the bleak classes to return the shared instance if multiple instances are detected.""" bleak.BleakScanner = HaBleakScannerWrapper # type: ignore[misc, assignment] + bleak.BleakClient = HaBleakClientWrapper # type: ignore[misc] def uninstall_multiple_bleak_catcher() -> None: """Unwrap the bleak classes.""" bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER # type: ignore[misc] + bleak.BleakClient = ORIGINAL_BLEAK_CLIENT # type: ignore[misc] diff --git a/tests/components/bluetooth/test_usage.py b/tests/components/bluetooth/test_usage.py index 8e566a7ce5a..3e35547d2f2 100644 --- a/tests/components/bluetooth/test_usage.py +++ b/tests/components/bluetooth/test_usage.py @@ -1,14 +1,27 @@ """Tests for the Bluetooth integration.""" -import bleak +from unittest.mock import patch -from homeassistant.components.bluetooth.models import HaBleakScannerWrapper +import bleak +from bleak.backends.device import BLEDevice +import pytest + +from homeassistant.components.bluetooth.models import ( + HaBleakClientWrapper, + HaBleakScannerWrapper, +) from homeassistant.components.bluetooth.usage import ( install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher, ) +from . import _get_manager + +MOCK_BLE_DEVICE = BLEDevice( + "00:00:00:00:00:00", "any", delegate="", details={"path": "/dev/hci0/device"} +) + async def test_multiple_bleak_scanner_instances(hass): """Test creating multiple BleakScanners without an integration.""" @@ -23,3 +36,46 @@ async def test_multiple_bleak_scanner_instances(hass): instance = bleak.BleakScanner() assert not isinstance(instance, HaBleakScannerWrapper) + + +async def test_wrapping_bleak_client(hass, enable_bluetooth): + """Test we wrap BleakClient.""" + install_multiple_bleak_catcher() + + instance = bleak.BleakClient(MOCK_BLE_DEVICE) + + assert isinstance(instance, HaBleakClientWrapper) + + uninstall_multiple_bleak_catcher() + + instance = bleak.BleakClient(MOCK_BLE_DEVICE) + + assert not isinstance(instance, HaBleakClientWrapper) + + +async def test_bleak_client_reports_with_address(hass, enable_bluetooth, caplog): + """Test we report when we pass an address to BleakClient.""" + install_multiple_bleak_catcher() + + with pytest.raises(bleak.BleakError): + instance = bleak.BleakClient("00:00:00:00:00:00") + + with patch.object( + _get_manager(), + "async_ble_device_from_address", + return_value=MOCK_BLE_DEVICE, + ): + instance = bleak.BleakClient("00:00:00:00:00:00") + + assert "BleakClient with an address instead of a BLEDevice" in caplog.text + + assert isinstance(instance, HaBleakClientWrapper) + + uninstall_multiple_bleak_catcher() + + caplog.clear() + + instance = bleak.BleakClient("00:00:00:00:00:00") + + assert not isinstance(instance, HaBleakClientWrapper) + assert "BleakClient with an address instead of a BLEDevice" not in caplog.text From 071cae2c0b01032e2eac0cffc61b2cd9d904272b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Aug 2022 12:38:04 -1000 Subject: [PATCH 3426/3516] Implement auto switching when there are multiple bluetooth scanners (#76947) --- homeassistant/components/bluetooth/manager.py | 84 +++++++-- tests/components/bluetooth/__init__.py | 20 +- tests/components/bluetooth/test_manager.py | 175 ++++++++++++++++++ 3 files changed, 259 insertions(+), 20 deletions(-) create mode 100644 tests/components/bluetooth/test_manager.py diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 15b05271bd4..0fba2d2aae1 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Callable, Iterable +from dataclasses import dataclass from datetime import datetime, timedelta import itertools import logging @@ -38,9 +39,56 @@ if TYPE_CHECKING: FILTER_UUIDS: Final = "UUIDs" +RSSI_SWITCH_THRESHOLD = 10 +STALE_ADVERTISEMENT_SECONDS = 180 + _LOGGER = logging.getLogger(__name__) +@dataclass +class AdvertisementHistory: + """Bluetooth advertisement history.""" + + ble_device: BLEDevice + advertisement_data: AdvertisementData + time: float + source: str + + +def _prefer_previous_adv(old: AdvertisementHistory, new: AdvertisementHistory) -> bool: + """Prefer previous advertisement if it is better.""" + if new.time - old.time > STALE_ADVERTISEMENT_SECONDS: + # If the old advertisement is stale, any new advertisement is preferred + if new.source != old.source: + _LOGGER.debug( + "%s (%s): Switching from %s to %s (time_elapsed:%s > stale_seconds:%s)", + new.advertisement_data.local_name, + new.ble_device.address, + old.source, + new.source, + new.time - old.time, + STALE_ADVERTISEMENT_SECONDS, + ) + return False + if new.ble_device.rssi - RSSI_SWITCH_THRESHOLD > old.ble_device.rssi: + # If new advertisement is RSSI_SWITCH_THRESHOLD more, the new one is preferred + if new.source != old.source: + _LOGGER.debug( + "%s (%s): Switching from %s to %s (new_rssi:%s - threadshold:%s > old_rssi:%s)", + new.advertisement_data.local_name, + new.ble_device.address, + old.source, + new.source, + new.ble_device.rssi, + RSSI_SWITCH_THRESHOLD, + old.ble_device.rssi, + ) + return False + # If the source is the different, the old one is preferred because its + # not stale and its RSSI_SWITCH_THRESHOLD less than the new one + return old.source != new.source + + def _dispatch_bleak_callback( callback: AdvertisementDataCallback, filters: dict[str, set[str]], @@ -82,7 +130,7 @@ class BluetoothManager: self._bleak_callbacks: list[ tuple[AdvertisementDataCallback, dict[str, set[str]]] ] = [] - self.history: dict[str, tuple[BLEDevice, AdvertisementData, float, str]] = {} + self.history: dict[str, AdvertisementHistory] = {} self._scanners: list[HaScanner] = [] @hass_callback @@ -110,7 +158,7 @@ class BluetoothManager: @hass_callback def async_discovered_devices(self) -> list[BLEDevice]: """Return all of combined best path to discovered from all the scanners.""" - return [history[0] for history in self.history.values()] + return [history.ble_device for history in self.history.values()] @hass_callback def async_setup_unavailable_tracking(self) -> None: @@ -159,12 +207,15 @@ class BluetoothManager: than the source from the history or the timestamp in the history is older than 180s """ - self.history[device.address] = ( - device, - advertisement_data, - monotonic_time, - source, + new_history = AdvertisementHistory( + device, advertisement_data, monotonic_time, source ) + if (old_history := self.history.get(device.address)) and _prefer_previous_adv( + old_history, new_history + ): + return + + self.history[device.address] = new_history for callback_filters in self._bleak_callbacks: _dispatch_bleak_callback(*callback_filters, device, advertisement_data) @@ -246,13 +297,12 @@ class BluetoothManager: if ( matcher and (address := matcher.get(ADDRESS)) - and (device_adv_data := self.history.get(address)) + and (history := self.history.get(address)) ): - ble_device, adv_data, _, _ = device_adv_data try: callback( BluetoothServiceInfoBleak.from_advertisement( - ble_device, adv_data, SOURCE_LOCAL + history.ble_device, history.advertisement_data, SOURCE_LOCAL ), BluetoothChange.ADVERTISEMENT, ) @@ -264,8 +314,8 @@ class BluetoothManager: @hass_callback def async_ble_device_from_address(self, address: str) -> BLEDevice | None: """Return the BLEDevice if present.""" - if ble_adv := self.history.get(address): - return ble_adv[0] + if history := self.history.get(address): + return history.ble_device return None @hass_callback @@ -278,9 +328,9 @@ class BluetoothManager: """Return if the address is present.""" return [ BluetoothServiceInfoBleak.from_advertisement( - device_adv[0], device_adv[1], SOURCE_LOCAL + history.ble_device, history.advertisement_data, SOURCE_LOCAL ) - for device_adv in self.history.values() + for history in self.history.values() ] @hass_callback @@ -312,7 +362,9 @@ class BluetoothManager: # Replay the history since otherwise we miss devices # that were already discovered before the callback was registered # or we are in passive mode - for device, advertisement_data, _, _ in self.history.values(): - _dispatch_bleak_callback(callback, filters, device, advertisement_data) + for history in self.history.values(): + _dispatch_bleak_callback( + callback, filters, history.ble_device, history.advertisement_data + ) return _remove_callback diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index 2b6ad75d2b9..44da1a60f03 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -16,10 +16,22 @@ def _get_manager() -> BluetoothManager: def inject_advertisement(device: BLEDevice, adv: AdvertisementData) -> None: - """Return the underlying scanner that has been wrapped.""" - return _get_manager().scanner_adv_received( - device, adv, time.monotonic(), SOURCE_LOCAL - ) + """Inject an advertisement into the manager.""" + return inject_advertisement_with_source(device, adv, SOURCE_LOCAL) + + +def inject_advertisement_with_source( + device: BLEDevice, adv: AdvertisementData, source: str +) -> None: + """Inject an advertisement into the manager from a specific source.""" + inject_advertisement_with_time_and_source(device, adv, time.monotonic(), source) + + +def inject_advertisement_with_time_and_source( + device: BLEDevice, adv: AdvertisementData, time: float, source: str +) -> None: + """Inject an advertisement into the manager from a specific source at a time.""" + return _get_manager().scanner_adv_received(device, adv, time, source) def patch_all_discovered_devices(mock_discovered: list[BLEDevice]) -> None: diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py new file mode 100644 index 00000000000..eb6363521f8 --- /dev/null +++ b/tests/components/bluetooth/test_manager.py @@ -0,0 +1,175 @@ +"""Tests for the Bluetooth integration manager.""" + + +from bleak.backends.scanner import AdvertisementData, BLEDevice + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth.manager import STALE_ADVERTISEMENT_SECONDS + +from . import ( + inject_advertisement_with_source, + inject_advertisement_with_time_and_source, +) + + +async def test_advertisements_do_not_switch_adapters_for_no_reason( + hass, enable_bluetooth +): + """Test we only switch adapters when needed.""" + + address = "44:44:33:11:23:12" + + switchbot_device_signal_100 = BLEDevice(address, "wohand_signal_100", rssi=-100) + switchbot_adv_signal_100 = AdvertisementData( + local_name="wohand_signal_100", service_uuids=[] + ) + inject_advertisement_with_source( + switchbot_device_signal_100, switchbot_adv_signal_100, "hci0" + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_signal_100 + ) + + switchbot_device_signal_99 = BLEDevice(address, "wohand_signal_99", rssi=-99) + switchbot_adv_signal_99 = AdvertisementData( + local_name="wohand_signal_99", service_uuids=[] + ) + inject_advertisement_with_source( + switchbot_device_signal_99, switchbot_adv_signal_99, "hci0" + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_signal_99 + ) + + switchbot_device_signal_98 = BLEDevice(address, "wohand_good_signal", rssi=-98) + switchbot_adv_signal_98 = AdvertisementData( + local_name="wohand_good_signal", service_uuids=[] + ) + inject_advertisement_with_source( + switchbot_device_signal_98, switchbot_adv_signal_98, "hci1" + ) + + # should not switch to hci1 + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_signal_99 + ) + + +async def test_switching_adapters_based_on_rssi(hass, enable_bluetooth): + """Test switching adapters based on rssi.""" + + address = "44:44:33:11:23:45" + + switchbot_device_poor_signal = BLEDevice(address, "wohand_poor_signal", rssi=-100) + switchbot_adv_poor_signal = AdvertisementData( + local_name="wohand_poor_signal", service_uuids=[] + ) + inject_advertisement_with_source( + switchbot_device_poor_signal, switchbot_adv_poor_signal, "hci0" + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_poor_signal + ) + + switchbot_device_good_signal = BLEDevice(address, "wohand_good_signal", rssi=-60) + switchbot_adv_good_signal = AdvertisementData( + local_name="wohand_good_signal", service_uuids=[] + ) + inject_advertisement_with_source( + switchbot_device_good_signal, switchbot_adv_good_signal, "hci1" + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + inject_advertisement_with_source( + switchbot_device_good_signal, switchbot_adv_poor_signal, "hci0" + ) + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + # We should not switch adapters unless the signal hits the threshold + switchbot_device_similar_signal = BLEDevice( + address, "wohand_similar_signal", rssi=-62 + ) + switchbot_adv_similar_signal = AdvertisementData( + local_name="wohand_similar_signal", service_uuids=[] + ) + + inject_advertisement_with_source( + switchbot_device_similar_signal, switchbot_adv_similar_signal, "hci0" + ) + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_good_signal + ) + + +async def test_switching_adapters_based_on_stale(hass, enable_bluetooth): + """Test switching adapters based on the previous advertisement being stale.""" + + address = "44:44:33:11:23:41" + start_time_monotonic = 50.0 + + switchbot_device_poor_signal_hci0 = BLEDevice( + address, "wohand_poor_signal_hci0", rssi=-100 + ) + switchbot_adv_poor_signal_hci0 = AdvertisementData( + local_name="wohand_poor_signal_hci0", service_uuids=[] + ) + inject_advertisement_with_time_and_source( + switchbot_device_poor_signal_hci0, + switchbot_adv_poor_signal_hci0, + start_time_monotonic, + "hci0", + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_poor_signal_hci0 + ) + + switchbot_device_poor_signal_hci1 = BLEDevice( + address, "wohand_poor_signal_hci1", rssi=-99 + ) + switchbot_adv_poor_signal_hci1 = AdvertisementData( + local_name="wohand_poor_signal_hci1", service_uuids=[] + ) + inject_advertisement_with_time_and_source( + switchbot_device_poor_signal_hci1, + switchbot_adv_poor_signal_hci1, + start_time_monotonic, + "hci1", + ) + + # Should not switch adapters until the advertisement is stale + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_poor_signal_hci0 + ) + + # Should switch to hci1 since the previous advertisement is stale + # even though the signal is poor because the device is now + # likely unreachable via hci0 + inject_advertisement_with_time_and_source( + switchbot_device_poor_signal_hci1, + switchbot_adv_poor_signal_hci1, + start_time_monotonic + STALE_ADVERTISEMENT_SECONDS + 1, + "hci1", + ) + + assert ( + bluetooth.async_ble_device_from_address(hass, address) + is switchbot_device_poor_signal_hci1 + ) From 71cdc1645b343fd1a053bdcd5a5a4821afdde7cf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Aug 2022 00:49:11 +0200 Subject: [PATCH 3427/3516] Refactor LaMetric integration (#76759) * Refactor LaMetric integration * Use async_setup Co-authored-by: Martin Hjelmare * use async_get_service Co-authored-by: Martin Hjelmare * Update tests/components/lametric/conftest.py Co-authored-by: Martin Hjelmare * Update tests/components/lametric/conftest.py Co-authored-by: Martin Hjelmare * Pass hassconfig * Remove try/catch * Fix passing hassconfig * Use menu Co-authored-by: Martin Hjelmare --- .coveragerc | 3 +- CODEOWNERS | 1 + homeassistant/components/lametric/__init__.py | 97 ++- .../lametric/application_credentials.py | 11 + .../components/lametric/config_flow.py | 251 +++++++ homeassistant/components/lametric/const.py | 6 +- .../components/lametric/manifest.json | 13 +- homeassistant/components/lametric/notify.py | 171 ++--- .../components/lametric/strings.json | 50 ++ .../components/lametric/translations/en.json | 50 ++ .../generated/application_credentials.py | 1 + homeassistant/generated/config_flows.py | 1 + homeassistant/generated/ssdp.py | 5 + requirements_all.txt | 6 +- requirements_test_all.txt | 3 + tests/components/lametric/__init__.py | 1 + tests/components/lametric/conftest.py | 81 ++ .../lametric/fixtures/cloud_devices.json | 38 + .../components/lametric/fixtures/device.json | 72 ++ tests/components/lametric/test_config_flow.py | 697 ++++++++++++++++++ 20 files changed, 1385 insertions(+), 173 deletions(-) create mode 100644 homeassistant/components/lametric/application_credentials.py create mode 100644 homeassistant/components/lametric/config_flow.py create mode 100644 homeassistant/components/lametric/strings.json create mode 100644 homeassistant/components/lametric/translations/en.json create mode 100644 tests/components/lametric/__init__.py create mode 100644 tests/components/lametric/conftest.py create mode 100644 tests/components/lametric/fixtures/cloud_devices.json create mode 100644 tests/components/lametric/fixtures/device.json create mode 100644 tests/components/lametric/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index d3e1b75b928..49cfbb3acc7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -639,7 +639,8 @@ omit = homeassistant/components/kostal_plenticore/switch.py homeassistant/components/kwb/sensor.py homeassistant/components/lacrosse/sensor.py - homeassistant/components/lametric/* + homeassistant/components/lametric/__init__.py + homeassistant/components/lametric/notify.py homeassistant/components/lannouncer/notify.py homeassistant/components/lastfm/sensor.py homeassistant/components/launch_library/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 0c8aa94dfb8..1f3ce12e63a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -586,6 +586,7 @@ build.json @home-assistant/supervisor /homeassistant/components/lacrosse_view/ @IceBotYT /tests/components/lacrosse_view/ @IceBotYT /homeassistant/components/lametric/ @robbiet480 @frenck +/tests/components/lametric/ @robbiet480 @frenck /homeassistant/components/launch_library/ @ludeeus @DurgNomis-drol /tests/components/launch_library/ @ludeeus @DurgNomis-drol /homeassistant/components/laundrify/ @xLarry diff --git a/homeassistant/components/lametric/__init__.py b/homeassistant/components/lametric/__init__.py index 970bdd4b3b6..2f89d88d79d 100644 --- a/homeassistant/components/lametric/__init__.py +++ b/homeassistant/components/lametric/__init__.py @@ -1,52 +1,83 @@ """Support for LaMetric time.""" -from lmnotify import LaMetricManager +from demetriek import LaMetricConnectionError, LaMetricDevice import voluptuous as vol -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.components.repairs import IssueSeverity, async_create_issue +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_API_KEY, + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_HOST, + CONF_NAME, + Platform, +) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import discovery +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN, LOGGER +from .const import DOMAIN CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the LaMetricManager.""" - LOGGER.debug("Setting up LaMetric platform") - conf = config[DOMAIN] - hlmn = HassLaMetricManager( - client_id=conf[CONF_CLIENT_ID], client_secret=conf[CONF_CLIENT_SECRET] - ) - if not (devices := hlmn.manager.get_devices()): - LOGGER.error("No LaMetric devices found") - return False - - hass.data[DOMAIN] = hlmn - for dev in devices: - LOGGER.debug("Discovered LaMetric device: %s", dev) +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the LaMetric integration.""" + hass.data[DOMAIN] = {"hass_config": config} + if DOMAIN in config: + async_create_issue( + hass, + DOMAIN, + "manual_migration", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="manual_migration", + ) return True -class HassLaMetricManager: - """A class that encapsulated requests to the LaMetric manager.""" +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up LaMetric from a config entry.""" + lametric = LaMetricDevice( + host=entry.data[CONF_HOST], + api_key=entry.data[CONF_API_KEY], + session=async_get_clientsession(hass), + ) - def __init__(self, client_id: str, client_secret: str) -> None: - """Initialize HassLaMetricManager and connect to LaMetric.""" + try: + device = await lametric.device() + except LaMetricConnectionError as ex: + raise ConfigEntryNotReady("Cannot connect to LaMetric device") from ex - LOGGER.debug("Connecting to LaMetric") - self.manager = LaMetricManager(client_id, client_secret) - self._client_id = client_id - self._client_secret = client_secret + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = lametric + + # Set up notify platform, no entry support for notify component yet, + # have to use discovery to load platform. + hass.async_create_task( + discovery.async_load_platform( + hass, + Platform.NOTIFY, + DOMAIN, + {CONF_NAME: device.name, "entry_id": entry.entry_id}, + hass.data[DOMAIN]["hass_config"], + ) + ) + return True diff --git a/homeassistant/components/lametric/application_credentials.py b/homeassistant/components/lametric/application_credentials.py new file mode 100644 index 00000000000..ab763c8f6fb --- /dev/null +++ b/homeassistant/components/lametric/application_credentials.py @@ -0,0 +1,11 @@ +"""Application credentials platform for LaMetric.""" +from homeassistant.components.application_credentials import AuthorizationServer +from homeassistant.core import HomeAssistant + + +async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: + """Return authorization server.""" + return AuthorizationServer( + authorize_url="https://developer.lametric.com/api/v2/oauth2/authorize", + token_url="https://developer.lametric.com/api/v2/oauth2/token", + ) diff --git a/homeassistant/components/lametric/config_flow.py b/homeassistant/components/lametric/config_flow.py new file mode 100644 index 00000000000..4bb293b0a4d --- /dev/null +++ b/homeassistant/components/lametric/config_flow.py @@ -0,0 +1,251 @@ +"""Config flow to configure the LaMetric integration.""" +from __future__ import annotations + +from ipaddress import ip_address +import logging +from typing import Any + +from demetriek import ( + CloudDevice, + LaMetricCloud, + LaMetricConnectionError, + LaMetricDevice, + Model, + Notification, + NotificationIconType, + NotificationSound, + Simple, + Sound, +) +import voluptuous as vol +from yarl import URL + +from homeassistant.components.ssdp import ( + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_SERIAL, + SsdpServiceInfo, +) +from homeassistant.const import CONF_API_KEY, CONF_DEVICE, CONF_HOST, CONF_MAC +from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler +from homeassistant.helpers.selector import ( + SelectOptionDict, + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, + TextSelector, + TextSelectorConfig, + TextSelectorType, +) +from homeassistant.util.network import is_link_local + +from .const import DOMAIN, LOGGER + + +class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): + """Handle a LaMetric config flow.""" + + DOMAIN = DOMAIN + VERSION = 1 + + devices: dict[str, CloudDevice] + discovered_host: str + discovered_serial: str + discovered: bool = False + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return LOGGER + + @property + def extra_authorize_data(self) -> dict[str, Any]: + """Extra data that needs to be appended to the authorize url.""" + return {"scope": "basic devices_read"} + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by the user.""" + return await self.async_step_choice_enter_manual_or_fetch_cloud() + + async def async_step_ssdp(self, discovery_info: SsdpServiceInfo) -> FlowResult: + """Handle a flow initiated by SSDP discovery.""" + url = URL(discovery_info.ssdp_location or "") + if url.host is None or not ( + serial := discovery_info.upnp.get(ATTR_UPNP_SERIAL) + ): + return self.async_abort(reason="invalid_discovery_info") + + if is_link_local(ip_address(url.host)): + return self.async_abort(reason="link_local_address") + + await self.async_set_unique_id(serial) + self._abort_if_unique_id_configured(updates={CONF_HOST: url.host}) + + self.context.update( + { + "title_placeholders": { + "name": discovery_info.upnp.get( + ATTR_UPNP_FRIENDLY_NAME, "LaMetric TIME" + ), + }, + "configuration_url": "https://developer.lametric.com", + } + ) + + self.discovered = True + self.discovered_host = str(url.host) + self.discovered_serial = serial + return await self.async_step_choice_enter_manual_or_fetch_cloud() + + async def async_step_choice_enter_manual_or_fetch_cloud( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user's choice of entering the manual credentials or fetching the cloud credentials.""" + return self.async_show_menu( + step_id="choice_enter_manual_or_fetch_cloud", + menu_options=["pick_implementation", "manual_entry"], + ) + + async def async_step_manual_entry( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user's choice of entering the device manually.""" + errors: dict[str, str] = {} + if user_input is not None: + if self.discovered: + host = self.discovered_host + else: + host = user_input[CONF_HOST] + + try: + return await self._async_step_create_entry( + host, user_input[CONF_API_KEY] + ) + except AbortFlow as ex: + raise ex + except LaMetricConnectionError as ex: + LOGGER.error("Error connecting to LaMetric: %s", ex) + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unexpected error occurred") + errors["base"] = "unknown" + + # Don't ask for a host if it was discovered + schema = { + vol.Required(CONF_API_KEY): TextSelector( + TextSelectorConfig(type=TextSelectorType.PASSWORD) + ) + } + if not self.discovered: + schema = {vol.Required(CONF_HOST): TextSelector()} | schema + + return self.async_show_form( + step_id="manual_entry", + data_schema=vol.Schema(schema), + errors=errors, + ) + + async def async_step_cloud_fetch_devices(self, data: dict[str, Any]) -> FlowResult: + """Fetch information about devices from the cloud.""" + lametric = LaMetricCloud( + token=data["token"]["access_token"], + session=async_get_clientsession(self.hass), + ) + self.devices = { + device.serial_number: device + for device in sorted(await lametric.devices(), key=lambda d: d.name) + } + + if not self.devices: + return self.async_abort(reason="no_devices") + + return await self.async_step_cloud_select_device() + + async def async_step_cloud_select_device( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle device selection from devices offered by the cloud.""" + if self.discovered: + user_input = {CONF_DEVICE: self.discovered_serial} + elif len(self.devices) == 1: + user_input = {CONF_DEVICE: list(self.devices.values())[0].serial_number} + + errors: dict[str, str] = {} + if user_input is not None: + device = self.devices[user_input[CONF_DEVICE]] + try: + return await self._async_step_create_entry( + str(device.ip), device.api_key + ) + except AbortFlow as ex: + raise ex + except LaMetricConnectionError as ex: + LOGGER.error("Error connecting to LaMetric: %s", ex) + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unexpected error occurred") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="cloud_select_device", + data_schema=vol.Schema( + { + vol.Required(CONF_DEVICE): SelectSelector( + SelectSelectorConfig( + mode=SelectSelectorMode.DROPDOWN, + options=[ + SelectOptionDict( + value=device.serial_number, + label=device.name, + ) + for device in self.devices.values() + ], + ) + ), + } + ), + errors=errors, + ) + + async def _async_step_create_entry(self, host: str, api_key: str) -> FlowResult: + """Create entry.""" + lametric = LaMetricDevice( + host=host, + api_key=api_key, + session=async_get_clientsession(self.hass), + ) + + device = await lametric.device() + + await self.async_set_unique_id(device.serial_number) + self._abort_if_unique_id_configured( + updates={CONF_HOST: lametric.host, CONF_API_KEY: lametric.api_key} + ) + + await lametric.notify( + notification=Notification( + icon_type=NotificationIconType.INFO, + model=Model( + cycles=2, + frames=[Simple(text="Connected to Home Assistant!", icon=7956)], + sound=Sound(id=NotificationSound.WIN), + ), + ) + ) + + return self.async_create_entry( + title=device.name, + data={ + CONF_API_KEY: lametric.api_key, + CONF_HOST: lametric.host, + CONF_MAC: device.wifi.mac, + }, + ) + + # Replace OAuth create entry with a fetch devices step + # LaMetric only use OAuth to get device information, but doesn't + # use it later on. + async_oauth_create_entry = async_step_cloud_fetch_devices diff --git a/homeassistant/components/lametric/const.py b/homeassistant/components/lametric/const.py index 85e61cd8d9a..d357f678d9d 100644 --- a/homeassistant/components/lametric/const.py +++ b/homeassistant/components/lametric/const.py @@ -7,10 +7,8 @@ DOMAIN: Final = "lametric" LOGGER = logging.getLogger(__package__) -AVAILABLE_PRIORITIES: Final = ["info", "warning", "critical"] -AVAILABLE_ICON_TYPES: Final = ["none", "info", "alert"] - CONF_CYCLES: Final = "cycles" +CONF_ICON_TYPE: Final = "icon_type" CONF_LIFETIME: Final = "lifetime" CONF_PRIORITY: Final = "priority" -CONF_ICON_TYPE: Final = "icon_type" +CONF_SOUND: Final = "sound" diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json index a2c0aecb58d..1a40f962156 100644 --- a/homeassistant/components/lametric/manifest.json +++ b/homeassistant/components/lametric/manifest.json @@ -2,8 +2,15 @@ "domain": "lametric", "name": "LaMetric", "documentation": "https://www.home-assistant.io/integrations/lametric", - "requirements": ["lmnotify==0.0.4"], + "requirements": ["demetriek==0.2.2"], "codeowners": ["@robbiet480", "@frenck"], - "iot_class": "cloud_push", - "loggers": ["lmnotify"] + "iot_class": "local_push", + "dependencies": ["application_credentials", "repairs"], + "loggers": ["demetriek"], + "config_flow": true, + "ssdp": [ + { + "deviceType": "urn:schemas-upnp-org:device:LaMetric:1" + } + ] } diff --git a/homeassistant/components/lametric/notify.py b/homeassistant/components/lametric/notify.py index f3c098a841e..4b404840388 100644 --- a/homeassistant/components/lametric/notify.py +++ b/homeassistant/components/lametric/notify.py @@ -3,157 +3,70 @@ from __future__ import annotations from typing import Any -from lmnotify import Model, SimpleFrame, Sound -from oauthlib.oauth2 import TokenExpiredError -from requests.exceptions import ConnectionError as RequestsConnectionError -import voluptuous as vol - -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_TARGET, - PLATFORM_SCHEMA, - BaseNotificationService, +from demetriek import ( + LaMetricDevice, + LaMetricError, + Model, + Notification, + NotificationIconType, + NotificationPriority, + Simple, + Sound, ) + +from homeassistant.components.notify import ATTR_DATA, BaseNotificationService from homeassistant.const import CONF_ICON from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import HassLaMetricManager -from .const import ( - AVAILABLE_ICON_TYPES, - AVAILABLE_PRIORITIES, - CONF_CYCLES, - CONF_ICON_TYPE, - CONF_LIFETIME, - CONF_PRIORITY, - DOMAIN, - LOGGER, -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_ICON, default="a7956"): cv.string, - vol.Optional(CONF_LIFETIME, default=10): cv.positive_int, - vol.Optional(CONF_CYCLES, default=1): cv.positive_int, - vol.Optional(CONF_PRIORITY, default="warning"): vol.In(AVAILABLE_PRIORITIES), - vol.Optional(CONF_ICON_TYPE, default="info"): vol.In(AVAILABLE_ICON_TYPES), - } -) +from .const import CONF_CYCLES, CONF_ICON_TYPE, CONF_PRIORITY, CONF_SOUND, DOMAIN -def get_service( +async def async_get_service( hass: HomeAssistant, config: ConfigType, discovery_info: DiscoveryInfoType | None = None, -) -> LaMetricNotificationService: +) -> LaMetricNotificationService | None: """Get the LaMetric notification service.""" - return LaMetricNotificationService( - hass.data[DOMAIN], - config[CONF_ICON], - config[CONF_LIFETIME] * 1000, - config[CONF_CYCLES], - config[CONF_PRIORITY], - config[CONF_ICON_TYPE], - ) + if discovery_info is None: + return None + lametric: LaMetricDevice = hass.data[DOMAIN][discovery_info["entry_id"]] + return LaMetricNotificationService(lametric) class LaMetricNotificationService(BaseNotificationService): """Implement the notification service for LaMetric.""" - def __init__( - self, - hasslametricmanager: HassLaMetricManager, - icon: str, - lifetime: int, - cycles: int, - priority: str, - icon_type: str, - ) -> None: + def __init__(self, lametric: LaMetricDevice) -> None: """Initialize the service.""" - self.hasslametricmanager = hasslametricmanager - self._icon = icon - self._lifetime = lifetime - self._cycles = cycles - self._priority = priority - self._icon_type = icon_type - self._devices: list[dict[str, Any]] = [] + self.lametric = lametric - def send_message(self, message: str = "", **kwargs: Any) -> None: - """Send a message to some LaMetric device.""" + async def async_send_message(self, message: str = "", **kwargs: Any) -> None: + """Send a message to a LaMetric device.""" + if not (data := kwargs.get(ATTR_DATA)): + data = {} - targets = kwargs.get(ATTR_TARGET) - data = kwargs.get(ATTR_DATA) - LOGGER.debug("Targets/Data: %s/%s", targets, data) - icon = self._icon - cycles = self._cycles sound = None - priority = self._priority - icon_type = self._icon_type + if CONF_SOUND in data: + sound = Sound(id=data[CONF_SOUND], category=None) - # Additional data? - if data is not None: - if "icon" in data: - icon = data["icon"] - if "sound" in data: - try: - sound = Sound(category="notifications", sound_id=data["sound"]) - LOGGER.debug("Adding notification sound %s", data["sound"]) - except AssertionError: - LOGGER.error("Sound ID %s unknown, ignoring", data["sound"]) - if "cycles" in data: - cycles = int(data["cycles"]) - if "icon_type" in data: - if data["icon_type"] in AVAILABLE_ICON_TYPES: - icon_type = data["icon_type"] - else: - LOGGER.warning( - "Priority %s invalid, using default %s", - data["priority"], - priority, + notification = Notification( + icon_type=NotificationIconType(data.get(CONF_ICON_TYPE, "none")), + priority=NotificationPriority(data.get(CONF_PRIORITY, "info")), + model=Model( + frames=[ + Simple( + icon=data.get(CONF_ICON, "a7956"), + text=message, ) - if "priority" in data: - if data["priority"] in AVAILABLE_PRIORITIES: - priority = data["priority"] - else: - LOGGER.warning( - "Priority %s invalid, using default %s", - data["priority"], - priority, - ) - text_frame = SimpleFrame(icon, message) - LOGGER.debug( - "Icon/Message/Cycles/Lifetime: %s, %s, %d, %d", - icon, - message, - self._cycles, - self._lifetime, + ], + cycles=int(data.get(CONF_CYCLES, 1)), + sound=sound, + ), ) - frames = [text_frame] - - model = Model(frames=frames, cycles=cycles, sound=sound) - lmn = self.hasslametricmanager.manager try: - self._devices = lmn.get_devices() - except TokenExpiredError: - LOGGER.debug("Token expired, fetching new token") - lmn.get_token() - self._devices = lmn.get_devices() - except RequestsConnectionError: - LOGGER.warning( - "Problem connecting to LaMetric, using cached devices instead" - ) - for dev in self._devices: - if targets is None or dev["name"] in targets: - try: - lmn.set_device(dev) - lmn.send_notification( - model, - lifetime=self._lifetime, - priority=priority, - icon_type=icon_type, - ) - LOGGER.debug("Sent notification to LaMetric %s", dev["name"]) - except OSError: - LOGGER.warning("Cannot connect to LaMetric %s", dev["name"]) + await self.lametric.notify(notification=notification) + except LaMetricError as ex: + raise HomeAssistantError("Could not send LaMetric notification") from ex diff --git a/homeassistant/components/lametric/strings.json b/homeassistant/components/lametric/strings.json new file mode 100644 index 00000000000..53271a8d0d8 --- /dev/null +++ b/homeassistant/components/lametric/strings.json @@ -0,0 +1,50 @@ +{ + "config": { + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "A LaMetric device can be set up in Home Assistant in two different ways.\n\nYou can enter all device information and API tokens yourself, or Home Asssistant can import them from your LaMetric.com account.", + "menu_options": { + "pick_implementation": "Import from LaMetric.com (recommended)", + "manual_entry": "Enter manually" + } + }, + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, + "manual_entry": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "api_key": "[%key:common::config_flow::data::api_key%]" + }, + "data_description": { + "host": "The IP address or hostname of your LaMetric TIME on your network.", + "api_key": "You can find this API key in [devices page in your LaMetric developer account](https://developer.lametric.com/user/devices)." + } + }, + "user_cloud_select_device": { + "data": { + "device": "Select the LaMetric device to add" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "invalid_discovery_info": "Invalid discovery information received", + "link_local_address": "Link local addresses are not supported", + "missing_configuration": "The LaMetric integration is not configured. Please follow the documentation.", + "no_devices": "The authorized user has no LaMetric devices", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" + } + }, + "issues": { + "manual_migration": { + "title": "Manual migration required for LaMetric", + "description": "The LaMetric integration has been modernized: It is now configured and set up via the user interface and the communcations are now local.\n\nUnfortunately, there is no automatic migration path possible and thus requires you to re-setup your LaMetric with Home Assistant. Please consult the Home Assistant LaMetric integration documentation on how to set it up.\n\nRemove the old LaMetric YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/lametric/translations/en.json b/homeassistant/components/lametric/translations/en.json new file mode 100644 index 00000000000..c02b7d6d05f --- /dev/null +++ b/homeassistant/components/lametric/translations/en.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "authorize_url_timeout": "Timeout generating authorize URL.", + "invalid_discovery_info": "Invalid discovery information received", + "link_local_address": "Link local addresses are not supported", + "missing_configuration": "The LaMetric integration is not configured. Please follow the documentation.", + "no_devices": "The authorized user has no LaMetric devices", + "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "A LaMetric device can be set up in Home Assistant in two different ways.\n\nYou can enter all device information and API tokens yourself, or Home Asssistant can import them from your LaMetric.com account.", + "menu_options": { + "manual_entry": "Enter manually", + "pick_implementation": "Import from LaMetric.com (recommended)" + } + }, + "manual_entry": { + "data": { + "api_key": "API Key", + "host": "Host" + }, + "data_description": { + "api_key": "You can find this API key in [devices page in your LaMetric developer account](https://developer.lametric.com/user/devices).", + "host": "The IP address or hostname of your LaMetric TIME on your network." + } + }, + "pick_implementation": { + "title": "Pick Authentication Method" + }, + "user_cloud_select_device": { + "data": { + "device": "Select the LaMetric device to add" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "The LaMetric integration has been modernized: It is now configured and set up via the user interface and the communcations are now local.\n\nUnfortunately, there is no automatic migration path possible and thus requires you to re-setup your LaMetric with Home Assistant. Please consult the Home Assistant LaMetric integration documentation on how to set it up.\n\nRemove the old LaMetric YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "Manual migration required for LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index ba9762f58c0..4673cf2378d 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -9,6 +9,7 @@ APPLICATION_CREDENTIALS = [ "geocaching", "google", "home_connect", + "lametric", "lyric", "neato", "nest", diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 8b5db1b45ba..42b426b8864 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -196,6 +196,7 @@ FLOWS = { "kraken", "kulersky", "lacrosse_view", + "lametric", "launch_library", "laundrify", "lg_soundbar", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 851d9b0fd10..b55240d1dc6 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -190,6 +190,11 @@ SSDP = { "manufacturer": "konnected.io" } ], + "lametric": [ + { + "deviceType": "urn:schemas-upnp-org:device:LaMetric:1" + } + ], "nanoleaf": [ { "st": "Nanoleaf_aurora:light" diff --git a/requirements_all.txt b/requirements_all.txt index ecebd456696..81e62987592 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -539,6 +539,9 @@ defusedxml==0.7.1 # homeassistant.components.deluge deluge-client==1.7.1 +# homeassistant.components.lametric +demetriek==0.2.2 + # homeassistant.components.denonavr denonavr==0.10.11 @@ -979,9 +982,6 @@ limitlessled==1.1.3 # homeassistant.components.linode linode-api==4.1.9b1 -# homeassistant.components.lametric -lmnotify==0.0.4 - # homeassistant.components.google_maps locationsharinglib==4.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 321398720cf..69d56e4ed40 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -410,6 +410,9 @@ defusedxml==0.7.1 # homeassistant.components.deluge deluge-client==1.7.1 +# homeassistant.components.lametric +demetriek==0.2.2 + # homeassistant.components.denonavr denonavr==0.10.11 diff --git a/tests/components/lametric/__init__.py b/tests/components/lametric/__init__.py new file mode 100644 index 00000000000..330b8c40cc9 --- /dev/null +++ b/tests/components/lametric/__init__.py @@ -0,0 +1 @@ +"""Tests for the LaMetric integration.""" diff --git a/tests/components/lametric/conftest.py b/tests/components/lametric/conftest.py new file mode 100644 index 00000000000..3640742c8ff --- /dev/null +++ b/tests/components/lametric/conftest.py @@ -0,0 +1,81 @@ +"""Fixtures for LaMetric integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +from demetriek import CloudDevice, Device +from pydantic import parse_raw_as +import pytest + +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.lametric.const import DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_MAC +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture(autouse=True) +async def setup_credentials(hass: HomeAssistant) -> None: + """Fixture to setup credentials.""" + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, DOMAIN, ClientCredential("client", "secret"), "credentials" + ) + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="My LaMetric", + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.2", + CONF_API_KEY: "mock-from-fixture", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + }, + unique_id="SA110405124500W00BS9", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.lametric.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup + + +@pytest.fixture +def mock_lametric_config_flow() -> Generator[MagicMock, None, None]: + """Return a mocked LaMetric client.""" + with patch( + "homeassistant.components.lametric.config_flow.LaMetricDevice", autospec=True + ) as lametric_mock: + lametric = lametric_mock.return_value + lametric.api_key = "mock-api-key" + lametric.host = "127.0.0.1" + lametric.device.return_value = Device.parse_raw( + load_fixture("device.json", DOMAIN) + ) + yield lametric + + +@pytest.fixture +def mock_lametric_cloud_config_flow() -> Generator[MagicMock, None, None]: + """Return a mocked LaMetric Cloud client.""" + with patch( + "homeassistant.components.lametric.config_flow.LaMetricCloud", autospec=True + ) as lametric_mock: + lametric = lametric_mock.return_value + lametric.devices.return_value = parse_raw_as( + list[CloudDevice], load_fixture("cloud_devices.json", DOMAIN) + ) + yield lametric diff --git a/tests/components/lametric/fixtures/cloud_devices.json b/tests/components/lametric/fixtures/cloud_devices.json new file mode 100644 index 00000000000..a375f968baa --- /dev/null +++ b/tests/components/lametric/fixtures/cloud_devices.json @@ -0,0 +1,38 @@ +[ + { + "id": 1, + "name": "Frenck's LaMetric", + "state": "configured", + "serial_number": "SA110405124500W00BS9", + "api_key": "mock-api-key", + "ipv4_internal": "127.0.0.1", + "mac": "AA:BB:CC:DD:EE:FF", + "wifi_ssid": "IoT", + "created_at": "2015-08-12T15:15:55+00:00", + "updated_at": "2016-08-13T18:16:17+00:00" + }, + { + "id": 21, + "name": "Blackjack", + "state": "configured", + "serial_number": "SA140100002200W00B21", + "api_key": "8adaa0c98278dbb1ecb218d1c3e11f9312317ba474ab3361f80c0bd4f13a6721", + "ipv4_internal": "192.168.1.21", + "mac": "AA:BB:CC:DD:EE:21", + "wifi_ssid": "AllYourBaseAreBelongToUs", + "created_at": "2015-03-06T15:15:55+00:00", + "updated_at": "2016-06-14T18:27:13+00:00" + }, + { + "id": 42, + "name": "The Answer", + "state": "configured", + "serial_number": "SA140100002200W00B42", + "api_key": "8adaa0c98278dbb1ecb218d1c3e11f9312317ba474ab3361f80c0bd4f13a6742", + "ipv4_internal": "192.168.1.42", + "mac": "AA:BB:CC:DD:EE:42", + "wifi_ssid": "AllYourBaseAreBelongToUs", + "created_at": "2015-03-06T15:15:55+00:00", + "updated_at": "2016-06-14T18:27:13+00:00" + } +] diff --git a/tests/components/lametric/fixtures/device.json b/tests/components/lametric/fixtures/device.json new file mode 100644 index 00000000000..a184d9f0aa1 --- /dev/null +++ b/tests/components/lametric/fixtures/device.json @@ -0,0 +1,72 @@ +{ + "audio": { + "volume": 100, + "volume_limit": { + "max": 100, + "min": 0 + }, + "volume_range": { + "max": 100, + "min": 0 + } + }, + "bluetooth": { + "active": false, + "address": "AA:BB:CC:DD:EE:FF", + "available": true, + "discoverable": true, + "low_energy": { + "active": true, + "advertising": true, + "connectable": true + }, + "name": "LM1234", + "pairable": true + }, + "display": { + "brightness": 100, + "brightness_limit": { + "max": 100, + "min": 2 + }, + "brightness_mode": "auto", + "brightness_range": { + "max": 100, + "min": 0 + }, + "height": 8, + "screensaver": { + "enabled": false, + "modes": { + "time_based": { + "enabled": true, + "local_start_time": "01:00:39", + "start_time": "00:00:39" + }, + "when_dark": { + "enabled": false + } + }, + "widget": "08b8eac21074f8f7e5a29f2855ba8060" + }, + "type": "mixed", + "width": 37 + }, + "id": "12345", + "mode": "auto", + "model": "LM 37X8", + "name": "Frenck's LaMetric", + "os_version": "2.2.2", + "serial_number": "SA110405124500W00BS9", + "wifi": { + "active": true, + "mac": "AA:BB:CC:DD:EE:FF", + "available": true, + "encryption": "WPA", + "ssid": "IoT", + "ip": "127.0.0.1", + "mode": "dhcp", + "netmask": "255.255.255.0", + "rssi": 21 + } +} diff --git a/tests/components/lametric/test_config_flow.py b/tests/components/lametric/test_config_flow.py new file mode 100644 index 00000000000..2134ee135f6 --- /dev/null +++ b/tests/components/lametric/test_config_flow.py @@ -0,0 +1,697 @@ +"""Tests for the LaMetric config flow.""" +from collections.abc import Awaitable, Callable +from http import HTTPStatus +from unittest.mock import MagicMock + +from aiohttp.test_utils import TestClient +from demetriek import ( + LaMetricConnectionError, + LaMetricConnectionTimeoutError, + LaMetricError, +) +import pytest + +from homeassistant.components.lametric.const import DOMAIN +from homeassistant.components.ssdp import ( + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_SERIAL, + SsdpServiceInfo, +) +from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER +from homeassistant.const import CONF_API_KEY, CONF_DEVICE, CONF_HOST, CONF_MAC +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import config_entry_oauth2_flow + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + +SSDP_DISCOVERY_INFO = SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://127.0.0.1:44057/465d585b-1c05-444a-b14e-6ffb875b46a6/device_description.xml", + upnp={ + ATTR_UPNP_FRIENDLY_NAME: "LaMetric Time (LM1245)", + ATTR_UPNP_SERIAL: "SA110405124500W00BS9", + }, +) + + +async def test_full_cloud_import_flow_multiple_devices( + hass: HomeAssistant, + hass_client_no_auth: Callable[[], Awaitable[TestClient]], + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + mock_setup_entry: MagicMock, + mock_lametric_cloud_config_flow: MagicMock, + mock_lametric_config_flow: MagicMock, +) -> None: + """Check a full flow importing from cloud, with multiple devices.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == FlowResultType.MENU + assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" + assert result.get("menu_options") == ["pick_implementation", "manual_entry"] + assert "flow_id" in result + flow_id = result["flow_id"] + + result2 = await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "pick_implementation"} + ) + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": flow_id, + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result2.get("type") == FlowResultType.EXTERNAL_STEP + assert result2.get("url") == ( + "https://developer.lametric.com/api/v2/oauth2/authorize" + "?response_type=code&client_id=client" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + "&scope=basic+devices_read" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + "https://developer.lametric.com/api/v2/oauth2/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + result3 = await hass.config_entries.flow.async_configure(flow_id) + + assert result3.get("type") == FlowResultType.FORM + assert result3.get("step_id") == "cloud_select_device" + + result4 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"} + ) + + assert result4.get("type") == FlowResultType.CREATE_ENTRY + assert result4.get("title") == "Frenck's LaMetric" + assert result4.get("data") == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + assert "result" in result4 + assert result4["result"].unique_id == "SA110405124500W00BS9" + + assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1 + assert len(mock_lametric_config_flow.device.mock_calls) == 1 + assert len(mock_lametric_config_flow.notify.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_full_cloud_import_flow_single_device( + hass: HomeAssistant, + hass_client_no_auth: Callable[[], Awaitable[TestClient]], + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + mock_setup_entry: MagicMock, + mock_lametric_cloud_config_flow: MagicMock, + mock_lametric_config_flow: MagicMock, +) -> None: + """Check a full flow importing from cloud, with a single device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == FlowResultType.MENU + assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" + assert result.get("menu_options") == ["pick_implementation", "manual_entry"] + assert "flow_id" in result + flow_id = result["flow_id"] + + result2 = await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "pick_implementation"} + ) + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": flow_id, + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result2.get("type") == FlowResultType.EXTERNAL_STEP + assert result2.get("url") == ( + "https://developer.lametric.com/api/v2/oauth2/authorize" + "?response_type=code&client_id=client" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + "&scope=basic+devices_read" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + "https://developer.lametric.com/api/v2/oauth2/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + # Stage a single device + # Should skip step that ask for device selection + mock_lametric_cloud_config_flow.devices.return_value = [ + mock_lametric_cloud_config_flow.devices.return_value[0] + ] + result3 = await hass.config_entries.flow.async_configure(flow_id) + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "Frenck's LaMetric" + assert result3.get("data") == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + assert "result" in result3 + assert result3["result"].unique_id == "SA110405124500W00BS9" + + assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1 + assert len(mock_lametric_config_flow.device.mock_calls) == 1 + assert len(mock_lametric_config_flow.notify.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_full_manual( + hass: HomeAssistant, + mock_setup_entry: MagicMock, + mock_lametric_config_flow: MagicMock, +) -> None: + """Check a full flow manual entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == FlowResultType.MENU + assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" + assert result.get("menu_options") == ["pick_implementation", "manual_entry"] + assert "flow_id" in result + flow_id = result["flow_id"] + + result2 = await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "manual_entry"} + ) + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "manual_entry" + + result3 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"} + ) + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "Frenck's LaMetric" + assert result3.get("data") == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + assert "result" in result3 + assert result3["result"].unique_id == "SA110405124500W00BS9" + + assert len(mock_lametric_config_flow.device.mock_calls) == 1 + assert len(mock_lametric_config_flow.notify.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_full_ssdp_with_cloud_import( + hass: HomeAssistant, + hass_client_no_auth: Callable[[], Awaitable[TestClient]], + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + mock_setup_entry: MagicMock, + mock_lametric_cloud_config_flow: MagicMock, + mock_lametric_config_flow: MagicMock, +) -> None: + """Check a full flow triggered by SSDP, importing from cloud.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DISCOVERY_INFO + ) + + assert result.get("type") == FlowResultType.MENU + assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" + assert result.get("menu_options") == ["pick_implementation", "manual_entry"] + assert "flow_id" in result + flow_id = result["flow_id"] + + result2 = await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "pick_implementation"} + ) + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": flow_id, + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result2.get("type") == FlowResultType.EXTERNAL_STEP + assert result2.get("url") == ( + "https://developer.lametric.com/api/v2/oauth2/authorize" + "?response_type=code&client_id=client" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + "&scope=basic+devices_read" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == HTTPStatus.OK + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + "https://developer.lametric.com/api/v2/oauth2/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + result3 = await hass.config_entries.flow.async_configure(flow_id) + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "Frenck's LaMetric" + assert result3.get("data") == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + assert "result" in result3 + assert result3["result"].unique_id == "SA110405124500W00BS9" + + assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1 + assert len(mock_lametric_config_flow.device.mock_calls) == 1 + assert len(mock_lametric_config_flow.notify.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_full_ssdp_manual_entry( + hass: HomeAssistant, + mock_setup_entry: MagicMock, + mock_lametric_config_flow: MagicMock, +) -> None: + """Check a full flow triggered by SSDP, with manual API key entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DISCOVERY_INFO + ) + + assert result.get("type") == FlowResultType.MENU + assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" + assert result.get("menu_options") == ["pick_implementation", "manual_entry"] + assert "flow_id" in result + flow_id = result["flow_id"] + + result2 = await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "manual_entry"} + ) + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "manual_entry" + + result3 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_API_KEY: "mock-api-key"} + ) + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "Frenck's LaMetric" + assert result3.get("data") == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + assert "result" in result3 + assert result3["result"].unique_id == "SA110405124500W00BS9" + + assert len(mock_lametric_config_flow.device.mock_calls) == 1 + assert len(mock_lametric_config_flow.notify.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "data,reason", + [ + ( + SsdpServiceInfo(ssdp_usn="mock_usn", ssdp_st="mock_st", upnp={}), + "invalid_discovery_info", + ), + ( + SsdpServiceInfo( + ssdp_usn="mock_usn", + ssdp_st="mock_st", + ssdp_location="http://169.254.0.1:44057/465d585b-1c05-444a-b14e-6ffb875b46a6/device_description.xml", + upnp={ + ATTR_UPNP_SERIAL: "SA110405124500W00BS9", + }, + ), + "link_local_address", + ), + ], +) +async def test_ssdp_abort_invalid_discovery( + hass: HomeAssistant, data: SsdpServiceInfo, reason: str +) -> None: + """Check a full flow triggered by SSDP, with manual API key entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=data + ) + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == reason + + +async def test_cloud_import_updates_existing_entry( + hass: HomeAssistant, + hass_client_no_auth: Callable[[], Awaitable[TestClient]], + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + mock_lametric_cloud_config_flow: MagicMock, + mock_lametric_config_flow: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test cloud importing existing device updates existing entry.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert "flow_id" in result + flow_id = result["flow_id"] + + await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "pick_implementation"} + ) + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": flow_id, + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + client = await hass_client_no_auth() + await client.get(f"/auth/external/callback?code=abcd&state={state}") + aioclient_mock.post( + "https://developer.lametric.com/api/v2/oauth2/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + await hass.config_entries.flow.async_configure(flow_id) + + result2 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"} + ) + + assert result2.get("type") == FlowResultType.ABORT + assert result2.get("reason") == "already_configured" + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + + assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1 + assert len(mock_lametric_config_flow.device.mock_calls) == 1 + assert len(mock_lametric_config_flow.notify.mock_calls) == 0 + + +async def test_manual_updates_existing_entry( + hass: HomeAssistant, + mock_lametric_config_flow: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test adding existing device updates existing entry.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert "flow_id" in result + flow_id = result["flow_id"] + + await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "manual_entry"} + ) + + result3 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"} + ) + + assert result3.get("type") == FlowResultType.ABORT + assert result3.get("reason") == "already_configured" + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + + assert len(mock_lametric_config_flow.device.mock_calls) == 1 + assert len(mock_lametric_config_flow.notify.mock_calls) == 0 + + +async def test_discovery_updates_existing_entry( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test discovery of existing device updates entry.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_SSDP}, data=SSDP_DISCOVERY_INFO + ) + + assert result.get("type") == FlowResultType.ABORT + assert result.get("reason") == "already_configured" + assert mock_config_entry.data == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-from-fixture", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + + +async def test_cloud_abort_no_devices( + hass: HomeAssistant, + hass_client_no_auth: Callable[[], Awaitable[TestClient]], + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + mock_lametric_cloud_config_flow: MagicMock, +) -> None: + """Test cloud importing aborts when account has no devices.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert "flow_id" in result + flow_id = result["flow_id"] + + await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "pick_implementation"} + ) + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": flow_id, + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + client = await hass_client_no_auth() + await client.get(f"/auth/external/callback?code=abcd&state={state}") + aioclient_mock.post( + "https://developer.lametric.com/api/v2/oauth2/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + # Stage there are no devices + mock_lametric_cloud_config_flow.devices.return_value = [] + result2 = await hass.config_entries.flow.async_configure(flow_id) + + assert result2.get("type") == FlowResultType.ABORT + assert result2.get("reason") == "no_devices" + + assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1 + + +@pytest.mark.parametrize( + "side_effect,reason", + [ + (LaMetricConnectionTimeoutError, "cannot_connect"), + (LaMetricConnectionError, "cannot_connect"), + (LaMetricError, "unknown"), + (RuntimeError, "unknown"), + ], +) +async def test_manual_errors( + hass: HomeAssistant, + mock_lametric_config_flow: MagicMock, + mock_setup_entry: MagicMock, + side_effect: Exception, + reason: str, +) -> None: + """Test adding existing device updates existing entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert "flow_id" in result + flow_id = result["flow_id"] + + await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "manual_entry"} + ) + + mock_lametric_config_flow.device.side_effect = side_effect + result2 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"} + ) + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "manual_entry" + assert result2.get("errors") == {"base": reason} + + assert len(mock_lametric_config_flow.device.mock_calls) == 1 + assert len(mock_lametric_config_flow.notify.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + mock_lametric_config_flow.device.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_HOST: "127.0.0.1", CONF_API_KEY: "mock-api-key"} + ) + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "Frenck's LaMetric" + assert result3.get("data") == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + assert "result" in result3 + assert result3["result"].unique_id == "SA110405124500W00BS9" + + assert len(mock_lametric_config_flow.device.mock_calls) == 2 + assert len(mock_lametric_config_flow.notify.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "side_effect,reason", + [ + (LaMetricConnectionTimeoutError, "cannot_connect"), + (LaMetricConnectionError, "cannot_connect"), + (LaMetricError, "unknown"), + (RuntimeError, "unknown"), + ], +) +async def test_cloud_errors( + hass: HomeAssistant, + hass_client_no_auth: Callable[[], Awaitable[TestClient]], + aioclient_mock: AiohttpClientMocker, + current_request_with_host: None, + mock_setup_entry: MagicMock, + mock_lametric_cloud_config_flow: MagicMock, + mock_lametric_config_flow: MagicMock, + side_effect: Exception, + reason: str, +) -> None: + """Test adding existing device updates existing entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert "flow_id" in result + flow_id = result["flow_id"] + + await hass.config_entries.flow.async_configure( + flow_id, user_input={"next_step_id": "pick_implementation"} + ) + + # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": flow_id, + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + client = await hass_client_no_auth() + await client.get(f"/auth/external/callback?code=abcd&state={state}") + aioclient_mock.post( + "https://developer.lametric.com/api/v2/oauth2/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + await hass.config_entries.flow.async_configure(flow_id) + + mock_lametric_config_flow.device.side_effect = side_effect + result2 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"} + ) + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "cloud_select_device" + assert result2.get("errors") == {"base": reason} + + assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1 + assert len(mock_lametric_config_flow.device.mock_calls) == 1 + assert len(mock_lametric_config_flow.notify.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + mock_lametric_config_flow.device.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + flow_id, user_input={CONF_DEVICE: "SA110405124500W00BS9"} + ) + + assert result3.get("type") == FlowResultType.CREATE_ENTRY + assert result3.get("title") == "Frenck's LaMetric" + assert result3.get("data") == { + CONF_HOST: "127.0.0.1", + CONF_API_KEY: "mock-api-key", + CONF_MAC: "AA:BB:CC:DD:EE:FF", + } + assert "result" in result3 + assert result3["result"].unique_id == "SA110405124500W00BS9" + + assert len(mock_lametric_cloud_config_flow.devices.mock_calls) == 1 + assert len(mock_lametric_config_flow.device.mock_calls) == 2 + assert len(mock_lametric_config_flow.notify.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 From d2e5d91eba103989f9c3ceee40babbb67da26f1f Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 18 Aug 2022 00:25:40 +0000 Subject: [PATCH 3428/3516] [ci skip] Translation update --- .../components/ambee/translations/ja.json | 1 + .../components/awair/translations/el.json | 6 ++++++ .../components/awair/translations/fr.json | 8 +++++++- .../components/awair/translations/ja.json | 6 ++++++ .../components/awair/translations/no.json | 2 +- .../components/awair/translations/ru.json | 2 +- .../awair/translations/zh-Hant.json | 8 +++++++- .../deutsche_bahn/translations/ja.json | 1 + .../components/foscam/translations/bg.json | 1 + .../components/fritz/translations/bg.json | 3 ++- .../fritzbox_callmonitor/translations/bg.json | 1 + .../fully_kiosk/translations/de.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/el.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/fr.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/ja.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/no.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/ru.json | 20 +++++++++++++++++++ .../fully_kiosk/translations/zh-Hant.json | 20 +++++++++++++++++++ .../components/google/translations/ja.json | 3 ++- .../google_travel_time/translations/bg.json | 3 +++ .../components/hue/translations/el.json | 5 ++++- .../components/hue/translations/fr.json | 17 +++++++++------- .../components/hue/translations/ja.json | 3 ++- .../components/hue/translations/ru.json | 16 +++++++-------- .../components/lametric/translations/de.json | 12 +++++++++++ .../components/lyric/translations/ja.json | 2 +- .../components/miflora/translations/ja.json | 1 + .../components/mitemp_bt/translations/ja.json | 1 + .../components/nest/translations/ja.json | 2 +- .../components/nuki/translations/bg.json | 3 +++ .../radiotherm/translations/ja.json | 2 +- .../components/senz/translations/bg.json | 3 +++ .../components/spotify/translations/ja.json | 2 +- .../steam_online/translations/ja.json | 2 +- .../components/subaru/translations/bg.json | 1 + .../components/uscis/translations/ja.json | 1 + .../waze_travel_time/translations/bg.json | 3 +++ .../components/xbox/translations/ja.json | 2 +- .../xiaomi_miio/translations/select.de.json | 10 ++++++++++ .../xiaomi_miio/translations/select.el.json | 10 ++++++++++ .../xiaomi_miio/translations/select.fr.json | 10 ++++++++++ .../xiaomi_miio/translations/select.ja.json | 10 ++++++++++ .../xiaomi_miio/translations/select.no.json | 10 ++++++++++ .../xiaomi_miio/translations/select.ru.json | 10 ++++++++++ .../translations/select.zh-Hant.json | 10 ++++++++++ 45 files changed, 304 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/fully_kiosk/translations/de.json create mode 100644 homeassistant/components/fully_kiosk/translations/el.json create mode 100644 homeassistant/components/fully_kiosk/translations/fr.json create mode 100644 homeassistant/components/fully_kiosk/translations/ja.json create mode 100644 homeassistant/components/fully_kiosk/translations/no.json create mode 100644 homeassistant/components/fully_kiosk/translations/ru.json create mode 100644 homeassistant/components/fully_kiosk/translations/zh-Hant.json create mode 100644 homeassistant/components/lametric/translations/de.json diff --git a/homeassistant/components/ambee/translations/ja.json b/homeassistant/components/ambee/translations/ja.json index e4502068d69..2d6bf3b2466 100644 --- a/homeassistant/components/ambee/translations/ja.json +++ b/homeassistant/components/ambee/translations/ja.json @@ -27,6 +27,7 @@ }, "issues": { "pending_removal": { + "description": "Ambee\u306e\u7d71\u5408\u306fHome Assistant\u304b\u3089\u306e\u524a\u9664\u306f\u4fdd\u7559\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001Home Assistant 2022.10\u4ee5\u964d\u306f\u5229\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002 \n\nAmbee\u304c\u7121\u6599(\u9650\u5b9a)\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u524a\u9664\u3057\u3001\u4e00\u822c\u30e6\u30fc\u30b6\u30fc\u304c\u6709\u6599\u30d7\u30e9\u30f3\u306b\u30b5\u30a4\u30f3\u30a2\u30c3\u30d7\u3059\u308b\u65b9\u6cd5\u3092\u63d0\u4f9b\u3057\u306a\u304f\u306a\u3063\u305f\u305f\u3081\u3001\u7d71\u5408\u304c\u524a\u9664\u3055\u308c\u308b\u3053\u3068\u306b\u306a\u308a\u307e\u3057\u305f\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u304b\u3089Ambee\u306e\u7d71\u5408\u306e\u30a8\u30f3\u30c8\u30ea\u3092\u524a\u9664\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "title": "Ambee\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/awair/translations/el.json b/homeassistant/components/awair/translations/el.json index 75447f29c15..187551f40a3 100644 --- a/homeassistant/components/awair/translations/el.json +++ b/homeassistant/components/awair/translations/el.json @@ -31,6 +31,12 @@ }, "description": "\u03a4\u03bf Awair Local API \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ce\u03bd\u03c4\u03b1\u03c2 \u03b1\u03c5\u03c4\u03ac \u03c4\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1: {url}" }, + "local_pick": { + "data": { + "device": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae", + "host": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" + } + }, "reauth": { "data": { "access_token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/awair/translations/fr.json b/homeassistant/components/awair/translations/fr.json index 2b4d572609d..79101fd9128 100644 --- a/homeassistant/components/awair/translations/fr.json +++ b/homeassistant/components/awair/translations/fr.json @@ -29,7 +29,13 @@ "data": { "host": "Adresse IP" }, - "description": "L'API locale Awair doit \u00eatre activ\u00e9e en suivant ces \u00e9tapes\u00a0: {url}" + "description": "Suivez [ces instructions]({url}) pour activer l\u2019API locale Awair.\n\nCliquez sur Envoyer apr\u00e8s avoir termin\u00e9." + }, + "local_pick": { + "data": { + "device": "Appareil", + "host": "Adresse IP" + } }, "reauth": { "data": { diff --git a/homeassistant/components/awair/translations/ja.json b/homeassistant/components/awair/translations/ja.json index 6599af1bd14..a6bbe56c838 100644 --- a/homeassistant/components/awair/translations/ja.json +++ b/homeassistant/components/awair/translations/ja.json @@ -31,6 +31,12 @@ }, "description": "\u6b21\u306e\u624b\u9806\u306b\u5f93\u3063\u3066\u3001Awair Local API\u3092\u6709\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059: {url}" }, + "local_pick": { + "data": { + "device": "\u30c7\u30d0\u30a4\u30b9", + "host": "IP\u30a2\u30c9\u30ec\u30b9" + } + }, "reauth": { "data": { "access_token": "\u30a2\u30af\u30bb\u30b9\u30c8\u30fc\u30af\u30f3", diff --git a/homeassistant/components/awair/translations/no.json b/homeassistant/components/awair/translations/no.json index d1c7e89f204..b71736d7a6d 100644 --- a/homeassistant/components/awair/translations/no.json +++ b/homeassistant/components/awair/translations/no.json @@ -29,7 +29,7 @@ "data": { "host": "IP adresse" }, - "description": "Awair Local API m\u00e5 aktiveres ved \u00e5 f\u00f8lge disse trinnene: {url}" + "description": "F\u00f8lg [disse instruksjonene]( {url} ) om hvordan du aktiverer Awair Local API. \n\n Klikk p\u00e5 send n\u00e5r du er ferdig." }, "local_pick": { "data": { diff --git a/homeassistant/components/awair/translations/ru.json b/homeassistant/components/awair/translations/ru.json index a81ef9585bd..d59c4ed50f1 100644 --- a/homeassistant/components/awair/translations/ru.json +++ b/homeassistant/components/awair/translations/ru.json @@ -29,7 +29,7 @@ "data": { "host": "IP-\u0430\u0434\u0440\u0435\u0441" }, - "description": "\u0427\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 API Awair, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0437\u0434\u0435\u0441\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f: {url}" + "description": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 [\u044d\u0442\u0438\u043c \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c]({url}), \u0447\u0442\u043e\u0431\u044b \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 API Awair.\n\n\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f." }, "local_pick": { "data": { diff --git a/homeassistant/components/awair/translations/zh-Hant.json b/homeassistant/components/awair/translations/zh-Hant.json index e7953517823..fb6e6acf9cb 100644 --- a/homeassistant/components/awair/translations/zh-Hant.json +++ b/homeassistant/components/awair/translations/zh-Hant.json @@ -29,7 +29,13 @@ "data": { "host": "IP \u4f4d\u5740" }, - "description": "\u5fc5\u9808\u900f\u904e\u4ee5\u4e0b\u6b65\u9a5f\u4ee5\u555f\u7528 Awair \u672c\u5730\u7aef API\uff1a{url}" + "description": "\u8ddf\u96a8 [\u4ee5\u4e0b\u6b65\u9a5f]({url}) \u4ee5\u555f\u7528 Awair \u672c\u5730\u7aef API\u3002\n\n\u5b8c\u6210\u5f8c\u9ede\u9078\u50b3\u9001\u3002" + }, + "local_pick": { + "data": { + "device": "\u88dd\u7f6e", + "host": "IP \u4f4d\u5740" + } }, "reauth": { "data": { diff --git a/homeassistant/components/deutsche_bahn/translations/ja.json b/homeassistant/components/deutsche_bahn/translations/ja.json index 191ec874abf..b76158e0b22 100644 --- a/homeassistant/components/deutsche_bahn/translations/ja.json +++ b/homeassistant/components/deutsche_bahn/translations/ja.json @@ -1,6 +1,7 @@ { "issues": { "pending_removal": { + "description": "Deutsche Bahn(\u30c9\u30a4\u30c4\u9244\u9053)\u306e\u7d71\u5408\u306f\u3001Home Assistant\u304b\u3089\u306e\u524a\u9664\u306f\u4fdd\u7559\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001Home Assistant 2022.11\u4ee5\u964d\u306f\u5229\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002 \n\n\u3053\u306e\u7d71\u5408\u306f\u3001\u8a31\u53ef\u3055\u308c\u3066\u3044\u306a\u3044Web\u30b9\u30af\u30ec\u30a4\u30d4\u30f3\u30b0\u306b\u4f9d\u5b58\u3057\u3066\u3044\u308b\u305f\u3081\u3001\u524a\u9664\u3055\u308c\u308b\u4e88\u5b9a\u3067\u3059\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089Deutsche Bahn YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "\u30c9\u30a4\u30c4\u9244\u9053(Deutsche Bahn)\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/foscam/translations/bg.json b/homeassistant/components/foscam/translations/bg.json index 5c41e03c838..8e0b1bac052 100644 --- a/homeassistant/components/foscam/translations/bg.json +++ b/homeassistant/components/foscam/translations/bg.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "host": "\u0425\u043e\u0441\u0442", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "port": "\u041f\u043e\u0440\u0442", "rtsp_port": "RTSP \u043f\u043e\u0440\u0442", diff --git a/homeassistant/components/fritz/translations/bg.json b/homeassistant/components/fritz/translations/bg.json index fa080a7662e..e9162b8b35b 100644 --- a/homeassistant/components/fritz/translations/bg.json +++ b/homeassistant/components/fritz/translations/bg.json @@ -4,7 +4,8 @@ "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0442\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435 \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e" }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/fritzbox_callmonitor/translations/bg.json b/homeassistant/components/fritzbox_callmonitor/translations/bg.json index ed2dd868df7..ee2dbc7bb3a 100644 --- a/homeassistant/components/fritzbox_callmonitor/translations/bg.json +++ b/homeassistant/components/fritzbox_callmonitor/translations/bg.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{name}", "step": { "phonebook": { "data": { diff --git a/homeassistant/components/fully_kiosk/translations/de.json b/homeassistant/components/fully_kiosk/translations/de.json new file mode 100644 index 00000000000..cd099ee8ef9 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/el.json b/homeassistant/components/fully_kiosk/translations/el.json new file mode 100644 index 00000000000..9af93e0c36a --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/el.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u039f \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "user": { + "data": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/fr.json b/homeassistant/components/fully_kiosk/translations/fr.json new file mode 100644 index 00000000000..a4a0a822a88 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/ja.json b/homeassistant/components/fully_kiosk/translations/ja.json new file mode 100644 index 00000000000..b5ef5895312 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/ja.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u30a2\u30ab\u30a6\u30f3\u30c8\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8", + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/no.json b/homeassistant/components/fully_kiosk/translations/no.json new file mode 100644 index 00000000000..3234879412b --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/ru.json b/homeassistant/components/fully_kiosk/translations/ru.json new file mode 100644 index 00000000000..00a9a3616ff --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/zh-Hant.json b/homeassistant/components/fully_kiosk/translations/zh-Hant.json new file mode 100644 index 00000000000..5b13923ac70 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/ja.json b/homeassistant/components/google/translations/ja.json index 7ab3209ac1c..eaa61a0fd91 100644 --- a/homeassistant/components/google/translations/ja.json +++ b/homeassistant/components/google/translations/ja.json @@ -35,10 +35,11 @@ }, "issues": { "deprecated_yaml": { - "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Google\u30ab\u30ec\u30f3\u30c0\u30fc\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Google\u30ab\u30ec\u30f3\u30c0\u30fc\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Google\u30ab\u30ec\u30f3\u30c0\u30fcyaml\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" }, "removed_track_new_yaml": { + "description": "configuration.yaml\u3067\u3001Google \u30ab\u30ec\u30f3\u30c0\u30fc\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u8ffd\u8de1\u3092\u7121\u52b9\u306b\u3067\u304d\u307e\u3057\u305f\u304c\u3001\u3053\u308c\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002UI\u306e\u7d71\u5408\u30b7\u30b9\u30c6\u30e0\u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u624b\u52d5\u3067\u5909\u66f4\u3057\u3066\u3001\u65b0\u3057\u304f\u691c\u51fa\u3055\u308c\u305f\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u3092\u4eca\u5f8c\u7121\u52b9\u306b\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059(disable newly discovered entities going forward)\u3002configuration.yaml\u304b\u3089track_new\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u3066\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3057\u307e\u3059\u3002", "title": "Google\u30ab\u30ec\u30f3\u30c0\u30fc\u306e\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30c8\u30e9\u30c3\u30ad\u30f3\u30b0\u304c\u5909\u66f4\u3055\u308c\u307e\u3057\u305f" } }, diff --git a/homeassistant/components/google_travel_time/translations/bg.json b/homeassistant/components/google_travel_time/translations/bg.json index d49807e49af..215f8c00629 100644 --- a/homeassistant/components/google_travel_time/translations/bg.json +++ b/homeassistant/components/google_travel_time/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/hue/translations/el.json b/homeassistant/components/hue/translations/el.json index 7d1c2cabd44..5641ef04add 100644 --- a/homeassistant/components/hue/translations/el.json +++ b/homeassistant/components/hue/translations/el.json @@ -44,6 +44,8 @@ "button_2": "\u0394\u03b5\u03cd\u03c4\u03b5\u03c1\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af", "button_3": "\u03a4\u03c1\u03af\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af", "button_4": "\u03a4\u03ad\u03c4\u03b1\u03c1\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af", + "clock_wise": "\u03a0\u03b5\u03c1\u03b9\u03c3\u03c4\u03c1\u03bf\u03c6\u03ae \u03b4\u03b5\u03be\u03b9\u03cc\u03c3\u03c4\u03c1\u03bf\u03c6\u03b1", + "counter_clock_wise": "\u03a0\u03b5\u03c1\u03b9\u03c3\u03c4\u03c1\u03bf\u03c6\u03ae \u03b1\u03c1\u03b9\u03c3\u03c4\u03b5\u03c1\u03cc\u03c3\u03c4\u03c1\u03bf\u03c6\u03b1", "dim_down": "\u039c\u03b5\u03af\u03c9\u03c3\u03b7 \u03ad\u03bd\u03c4\u03b1\u03c3\u03b7\u03c2", "dim_up": "\u0391\u03cd\u03be\u03b7\u03c3\u03b7 \u03ad\u03bd\u03c4\u03b1\u03c3\u03b7\u03c2", "double_buttons_1_3": "\u03a0\u03c1\u03ce\u03c4\u03bf \u03ba\u03b1\u03b9 \u03c4\u03c1\u03af\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af", @@ -61,7 +63,8 @@ "remote_double_button_long_press": "\u039a\u03b1\u03b9 \u03c4\u03b1 \u03b4\u03cd\u03bf \"{subtype}\" \u03b1\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc \u03c0\u03b1\u03c1\u03b1\u03c4\u03b5\u03c4\u03b1\u03bc\u03ad\u03bd\u03bf \u03c0\u03ac\u03c4\u03b7\u03bc\u03b1", "remote_double_button_short_press": "\u039a\u03b1\u03b9 \u03c4\u03b1 \u03b4\u03cd\u03bf \"{subtype}\" \u03b1\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b1\u03bd", "repeat": "\u03a4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \"{subtype}\" \u03ba\u03c1\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c0\u03b1\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf", - "short_release": "\u03a4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \" {subtype} \" \u03b1\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc \u03c3\u03cd\u03bd\u03c4\u03bf\u03bc\u03bf \u03c0\u03ac\u03c4\u03b7\u03bc\u03b1" + "short_release": "\u03a4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \" {subtype} \" \u03b1\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc \u03c3\u03cd\u03bd\u03c4\u03bf\u03bc\u03bf \u03c0\u03ac\u03c4\u03b7\u03bc\u03b1", + "start": "\"{subtype}\" \u03c0\u03b9\u03ad\u03c3\u03c4\u03b7\u03ba\u03b5 \u03b1\u03c1\u03c7\u03b9\u03ba\u03ac" } }, "options": { diff --git a/homeassistant/components/hue/translations/fr.json b/homeassistant/components/hue/translations/fr.json index cd07cff9fea..276e12c6c30 100644 --- a/homeassistant/components/hue/translations/fr.json +++ b/homeassistant/components/hue/translations/fr.json @@ -44,6 +44,8 @@ "button_2": "Deuxi\u00e8me bouton", "button_3": "Troisi\u00e8me bouton", "button_4": "Quatri\u00e8me bouton", + "clock_wise": "Rotation horaire", + "counter_clock_wise": "Rotation anti-horaire", "dim_down": "Assombrir", "dim_up": "\u00c9claircir", "double_buttons_1_3": "Premier et troisi\u00e8me boutons", @@ -53,15 +55,16 @@ }, "trigger_type": { "double_short_release": "Les deux \u00ab\u00a0{subtype}\u00a0\u00bb sont rel\u00e2ch\u00e9s", - "initial_press": "D\u00e9but de l'appui du bouton \u00ab\u00a0{subtype}\u00a0\u00bb", - "long_release": "Bouton \u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9 apr\u00e8s un appui long", - "remote_button_long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long", - "remote_button_short_press": "bouton \"{subtype}\" est press\u00e9", - "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", + "initial_press": "D\u00e9but de l'appui sur \u00ab\u00a0{subtype}\u00a0\u00bb", + "long_release": "\u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9 apr\u00e8s un appui long", + "remote_button_long_release": "\u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9 apr\u00e8s un appui long", + "remote_button_short_press": "\u00ab\u00a0{subtype}\u00a0\u00bb appuy\u00e9", + "remote_button_short_release": "\u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9", "remote_double_button_long_press": "Les deux \"{sous-type}\" ont \u00e9t\u00e9 rel\u00e2ch\u00e9s apr\u00e8s un appui long", "remote_double_button_short_press": "Les deux \" {subtype} \" ont \u00e9t\u00e9 rel\u00e2ch\u00e9s", - "repeat": "Bouton \u00ab\u00a0{subtype}\u00a0\u00bb maintenu enfonc\u00e9", - "short_release": "Bouton \u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9 apr\u00e8s un appui court" + "repeat": "\u00ab\u00a0{subtype}\u00a0\u00bb maintenu appuy\u00e9", + "short_release": "\u00ab\u00a0{subtype}\u00a0\u00bb rel\u00e2ch\u00e9 apr\u00e8s un appui court", + "start": "D\u00e9but de l'appui sur \u00ab\u00a0{subtype}\u00a0\u00bb" } }, "options": { diff --git a/homeassistant/components/hue/translations/ja.json b/homeassistant/components/hue/translations/ja.json index f5eddbc0242..dcf38b0e48c 100644 --- a/homeassistant/components/hue/translations/ja.json +++ b/homeassistant/components/hue/translations/ja.json @@ -63,7 +63,8 @@ "remote_double_button_long_press": "\u4e21\u65b9\u306e \"{subtype}\" \u306f\u9577\u62bc\u3057\u5f8c\u306b\u30ea\u30ea\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f", "remote_double_button_short_press": "\u4e21\u65b9\u306e \"{subtype}\" \u3092\u96e2\u3059", "repeat": "\u30dc\u30bf\u30f3 \"{subtype}\" \u3092\u62bc\u3057\u305f\u307e\u307e", - "short_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u77ed\u62bc\u3057\u306e\u5f8c\u306b\u96e2\u3059" + "short_release": "\u30dc\u30bf\u30f3 \"{subtype}\" \u77ed\u62bc\u3057\u306e\u5f8c\u306b\u96e2\u3059", + "start": "\"{subtype}\" \u304c\u6700\u521d\u306b\u62bc\u3055\u308c\u307e\u3057\u305f" } }, "options": { diff --git a/homeassistant/components/hue/translations/ru.json b/homeassistant/components/hue/translations/ru.json index 3f34b6b99e4..0614d75e176 100644 --- a/homeassistant/components/hue/translations/ru.json +++ b/homeassistant/components/hue/translations/ru.json @@ -55,16 +55,16 @@ }, "trigger_type": { "double_short_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u044b \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", - "initial_press": "{subtype} \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", - "long_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", - "remote_button_long_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", - "remote_button_short_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430", - "remote_button_short_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "initial_press": "\"{subtype}\" \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "long_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_long_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_short_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_short_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", "remote_double_button_long_press": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u044b \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", "remote_double_button_short_press": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u044b \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", - "repeat": "{subtype} \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0436\u0430\u0442\u043e\u0439", - "short_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", - "start": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u0435\u0440\u0432\u043e\u043d\u0430\u0447\u0430\u043b\u044c\u043d\u043e" + "repeat": "\"{subtype}\" \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0436\u0430\u0442\u043e\u0439", + "short_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "start": "\"{subtype}\" \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430" } }, "options": { diff --git a/homeassistant/components/lametric/translations/de.json b/homeassistant/components/lametric/translations/de.json new file mode 100644 index 00000000000..1ea4d15dcd8 --- /dev/null +++ b/homeassistant/components/lametric/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "step": { + "pick_implementation": { + "title": "W\u00e4hle eine Authentifizierungsmethode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/ja.json b/homeassistant/components/lyric/translations/ja.json index bd8b4b93442..c9bf880233d 100644 --- a/homeassistant/components/lyric/translations/ja.json +++ b/homeassistant/components/lyric/translations/ja.json @@ -20,7 +20,7 @@ }, "issues": { "removed_yaml": { - "description": "Honeywell Lyric\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "Honeywell Lyric\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Honeywell Lyric YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/miflora/translations/ja.json b/homeassistant/components/miflora/translations/ja.json index 30b2730980b..a9c2df5bf2e 100644 --- a/homeassistant/components/miflora/translations/ja.json +++ b/homeassistant/components/miflora/translations/ja.json @@ -1,6 +1,7 @@ { "issues": { "replaced": { + "description": "Mi Flora\u306e\u7d71\u5408\u306fHome Assistant 2022.7\u3067\u52d5\u4f5c\u3057\u306a\u304f\u306a\u308a\u30012022.8\u306e\u30ea\u30ea\u30fc\u30b9\u3067Xiaomi BLE\u7d71\u5408\u306b\u7f6e\u304d\u63db\u308f\u308a\u307e\u3057\u305f\u3002\n\n\u79fb\u884c\u30d1\u30b9\u306f\u3042\u308a\u307e\u305b\u3093\u306e\u3067\u3001\u65b0\u3057\u3044\u7d71\u5408\u3092\u4f7f\u7528\u3057\u3066Mi Flora\u30c7\u30d0\u30a4\u30b9\u3092\u624b\u52d5\u3067\u8ffd\u52a0\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u65e2\u5b58\u306eMi Flora\u306eYAML\u69cb\u6210\u306fHome Assistant\u3067\u4f7f\u7528\u3055\u308c\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001config.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "MiFlora\u306e\u7d71\u5408\u306f\u7f6e\u304d\u63db\u3048\u3089\u308c\u307e\u3057\u305f" } } diff --git a/homeassistant/components/mitemp_bt/translations/ja.json b/homeassistant/components/mitemp_bt/translations/ja.json index 11212382f1f..da129842bc8 100644 --- a/homeassistant/components/mitemp_bt/translations/ja.json +++ b/homeassistant/components/mitemp_bt/translations/ja.json @@ -1,6 +1,7 @@ { "issues": { "replaced": { + "description": "Xiaomi Mijia BLE\u6e29\u5ea6\u30fb\u6e7f\u5ea6\u30bb\u30f3\u30b5\u30fc\u306e\u7d71\u5408\u306f\u3001Home Assistant 2022.7\u3067\u52d5\u4f5c\u3057\u306a\u304f\u306a\u308a\u30012022.8\u306e\u30ea\u30ea\u30fc\u30b9\u3067Xiaomi BLE\u7d71\u5408\u306b\u7f6e\u304d\u63db\u308f\u308a\u307e\u3057\u305f\u3002\n\n\u79fb\u884c\u30d1\u30b9\u306f\u3042\u308a\u307e\u305b\u3093\u306e\u3067\u3001\u65b0\u3057\u3044\u7d71\u5408\u3092\u4f7f\u7528\u3057\u3066Xiaomi Mijia BLE\u30c7\u30d0\u30a4\u30b9\u3092\u624b\u52d5\u3067\u8ffd\u52a0\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u3002\n\n\u65e2\u5b58\u306eXiaomi Mijia BLE\u6e29\u5ea6\u30fb\u6e7f\u5ea6\u30bb\u30f3\u30b5\u30fc\u306eYAML\u69cb\u6210\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3055\u308c\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Xiaomi Mijia BLE\u6e29\u5ea6\u304a\u3088\u3073\u6e7f\u5ea6\u30bb\u30f3\u30b5\u30fc\u306e\u7d71\u5408\u306f\u3001\u30ea\u30d7\u30ec\u30fc\u30b9\u3055\u308c\u307e\u3057\u305f\u3002" } } diff --git a/homeassistant/components/nest/translations/ja.json b/homeassistant/components/nest/translations/ja.json index 5509cd497eb..06c5e54ef88 100644 --- a/homeassistant/components/nest/translations/ja.json +++ b/homeassistant/components/nest/translations/ja.json @@ -92,7 +92,7 @@ }, "issues": { "deprecated_yaml": { - "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Nest\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.10\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Nest\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.10\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Nest YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" }, "removed_app_auth": { diff --git a/homeassistant/components/nuki/translations/bg.json b/homeassistant/components/nuki/translations/bg.json index 4983c9a14b2..37e8e854866 100644 --- a/homeassistant/components/nuki/translations/bg.json +++ b/homeassistant/components/nuki/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/radiotherm/translations/ja.json b/homeassistant/components/radiotherm/translations/ja.json index e3cf6571357..64d3cca9112 100644 --- a/homeassistant/components/radiotherm/translations/ja.json +++ b/homeassistant/components/radiotherm/translations/ja.json @@ -21,7 +21,7 @@ }, "issues": { "deprecated_yaml": { - "description": "YAML\u3092\u4f7f\u7528\u3057\u305f\u3001Radio Thermostat climate(\u6c17\u5019)\u30d7\u30e9\u30c3\u30c8\u30d5\u30a9\u30fc\u30e0\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "YAML\u3092\u4f7f\u7528\u3057\u305f\u3001Radio Thermostat climate(\u6c17\u5019)\u30d7\u30e9\u30c3\u30c8\u30d5\u30a9\u30fc\u30e0\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Radio Thermostat YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } }, diff --git a/homeassistant/components/senz/translations/bg.json b/homeassistant/components/senz/translations/bg.json index a99746433a0..9493398f365 100644 --- a/homeassistant/components/senz/translations/bg.json +++ b/homeassistant/components/senz/translations/bg.json @@ -4,6 +4,9 @@ "already_configured": "\u0410\u043a\u0430\u0443\u043d\u0442\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430." }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" + }, "step": { "pick_implementation": { "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" diff --git a/homeassistant/components/spotify/translations/ja.json b/homeassistant/components/spotify/translations/ja.json index 6f1c48c8572..efcfa959e5a 100644 --- a/homeassistant/components/spotify/translations/ja.json +++ b/homeassistant/components/spotify/translations/ja.json @@ -21,7 +21,7 @@ }, "issues": { "removed_yaml": { - "description": "Spotify\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "Spotify\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Spotify YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } }, diff --git a/homeassistant/components/steam_online/translations/ja.json b/homeassistant/components/steam_online/translations/ja.json index 1524e2afc5a..75fe7d9cd99 100644 --- a/homeassistant/components/steam_online/translations/ja.json +++ b/homeassistant/components/steam_online/translations/ja.json @@ -26,7 +26,7 @@ }, "issues": { "removed_yaml": { - "description": "Steam\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "Steam\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Steam YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } }, diff --git a/homeassistant/components/subaru/translations/bg.json b/homeassistant/components/subaru/translations/bg.json index 212303991b0..c43cb84d5f6 100644 --- a/homeassistant/components/subaru/translations/bg.json +++ b/homeassistant/components/subaru/translations/bg.json @@ -6,6 +6,7 @@ }, "error": { "bad_pin_format": "\u041f\u0418\u041d \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0435 4 \u0446\u0438\u0444\u0440\u0438", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "incorrect_validation_code": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u0435\u043d \u043a\u043e\u0434 \u0437\u0430 \u0432\u0430\u043b\u0438\u0434\u0438\u0440\u0430\u043d\u0435" }, "step": { diff --git a/homeassistant/components/uscis/translations/ja.json b/homeassistant/components/uscis/translations/ja.json index b5abb7e0825..46f021952d8 100644 --- a/homeassistant/components/uscis/translations/ja.json +++ b/homeassistant/components/uscis/translations/ja.json @@ -1,6 +1,7 @@ { "issues": { "pending_removal": { + "description": "\u7c73\u56fd\u5e02\u6c11\u6a29\u79fb\u6c11\u5c40(US Citizenship and Immigration Services (USCIS))\u306e\u7d71\u5408\u306f\u3001Home Assistant\u304b\u3089\u306e\u524a\u9664\u306f\u4fdd\u7559\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001Home Assistant 2022.10\u4ee5\u964d\u306f\u5229\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002 \n\n\u3053\u306e\u7d71\u5408\u306f\u3001\u8a31\u53ef\u3055\u308c\u3066\u3044\u306a\u3044Web\u30b9\u30af\u30ec\u30a4\u30d4\u30f3\u30b0\u306b\u4f9d\u5b58\u3057\u3066\u3044\u308b\u305f\u3081\u3001\u524a\u9664\u3055\u308c\u308b\u4e88\u5b9a\u3067\u3059\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "USCIS\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/waze_travel_time/translations/bg.json b/homeassistant/components/waze_travel_time/translations/bg.json index f7d35259c93..fb5df032671 100644 --- a/homeassistant/components/waze_travel_time/translations/bg.json +++ b/homeassistant/components/waze_travel_time/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/xbox/translations/ja.json b/homeassistant/components/xbox/translations/ja.json index 530299b0b24..2534412e55b 100644 --- a/homeassistant/components/xbox/translations/ja.json +++ b/homeassistant/components/xbox/translations/ja.json @@ -16,7 +16,7 @@ }, "issues": { "deprecated_yaml": { - "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Xbox\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u306e\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "description": "configuration.yaml\u3092\u4f7f\u7528\u3057\u305f\u3001Xbox\u306e\u8a2d\u5b9a\u306f\u3001Home Assistant 2022.9\u3067\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002 \n\n\u65e2\u5b58\u306e\u3001OAuth \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u8cc7\u683c\u60c5\u5831\u3068\u30a2\u30af\u30bb\u30b9\u8a2d\u5b9a\u304c\u3001UI\u306b\u81ea\u52d5\u7684\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3055\u308c\u307e\u3057\u305f\u3002\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml \u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", "title": "Xbox YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" } } diff --git a/homeassistant/components/xiaomi_miio/translations/select.de.json b/homeassistant/components/xiaomi_miio/translations/select.de.json index 804eb7a7629..a4ac15de93b 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.de.json +++ b/homeassistant/components/xiaomi_miio/translations/select.de.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Vorw\u00e4rts", + "left": "Links", + "right": "Rechts" + }, "xiaomi_miio__led_brightness": { "bright": "Helligkeit", "dim": "Dimmer", "off": "Aus" + }, + "xiaomi_miio__ptc_level": { + "high": "Hoch", + "low": "Niedrig", + "medium": "Mittel" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.el.json b/homeassistant/components/xiaomi_miio/translations/select.el.json index 24c8a037676..911360a6ee5 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.el.json +++ b/homeassistant/components/xiaomi_miio/translations/select.el.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "\u0395\u03bc\u03c0\u03c1\u03cc\u03c2", + "left": "\u0391\u03c1\u03b9\u03c3\u03c4\u03b5\u03c1\u03ac", + "right": "\u0394\u03b5\u03be\u03b9\u03ac" + }, "xiaomi_miio__led_brightness": { "bright": "\u03a6\u03c9\u03c4\u03b5\u03b9\u03bd\u03cc", "dim": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03cc", "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc" + }, + "xiaomi_miio__ptc_level": { + "high": "\u03a5\u03c8\u03b7\u03bb\u03ae", + "low": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03ae", + "medium": "\u039c\u03b5\u03c3\u03b1\u03af\u03b1" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.fr.json b/homeassistant/components/xiaomi_miio/translations/select.fr.json index 29c9afe1e95..8ffc46043bc 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.fr.json +++ b/homeassistant/components/xiaomi_miio/translations/select.fr.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Vers l'avant", + "left": "Gauche", + "right": "Droite" + }, "xiaomi_miio__led_brightness": { "bright": "Brillant", "dim": "Faible", "off": "\u00c9teint" + }, + "xiaomi_miio__ptc_level": { + "high": "\u00c9lev\u00e9", + "low": "Faible", + "medium": "Moyen" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.ja.json b/homeassistant/components/xiaomi_miio/translations/select.ja.json index 22a7a4ea058..245225a5cae 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.ja.json +++ b/homeassistant/components/xiaomi_miio/translations/select.ja.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "\u9032\u3080", + "left": "\u5de6", + "right": "\u53f3" + }, "xiaomi_miio__led_brightness": { "bright": "\u660e\u308b\u3044", "dim": "\u8584\u6697\u3044", "off": "\u30aa\u30d5" + }, + "xiaomi_miio__ptc_level": { + "high": "\u9ad8", + "low": "\u4f4e", + "medium": "\u4e2d" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.no.json b/homeassistant/components/xiaomi_miio/translations/select.no.json index 8205447ac2c..611095b4712 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.no.json +++ b/homeassistant/components/xiaomi_miio/translations/select.no.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Framover", + "left": "Venstre", + "right": "H\u00f8yre" + }, "xiaomi_miio__led_brightness": { "bright": "Lys", "dim": "Dim", "off": "Av" + }, + "xiaomi_miio__ptc_level": { + "high": "H\u00f8y", + "low": "Lav", + "medium": "Medium" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.ru.json b/homeassistant/components/xiaomi_miio/translations/select.ru.json index 138d2b4fdce..322dc84e01c 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.ru.json +++ b/homeassistant/components/xiaomi_miio/translations/select.ru.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "\u0412\u043f\u0435\u0440\u0435\u0434", + "left": "\u041d\u0430\u043b\u0435\u0432\u043e", + "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e" + }, "xiaomi_miio__led_brightness": { "bright": "\u042f\u0440\u043a\u043e", "dim": "\u0422\u0443\u0441\u043a\u043b\u043e", "off": "\u041e\u0442\u043a\u043b." + }, + "xiaomi_miio__ptc_level": { + "high": "\u0412\u044b\u0441\u043e\u043a\u0438\u0439", + "low": "\u041d\u0438\u0437\u043a\u0438\u0439", + "medium": "\u0421\u0440\u0435\u0434\u043d\u0438\u0439" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/select.zh-Hant.json index 3c3152db0da..527bad0c120 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.zh-Hant.json +++ b/homeassistant/components/xiaomi_miio/translations/select.zh-Hant.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "\u524d", + "left": "\u5de6", + "right": "\u53f3" + }, "xiaomi_miio__led_brightness": { "bright": "\u4eae\u5149", "dim": "\u5fae\u5149", "off": "\u95dc\u9589" + }, + "xiaomi_miio__ptc_level": { + "high": "\u9ad8", + "low": "\u4f4e", + "medium": "\u4e2d" } } } \ No newline at end of file From 3eaa1c30af36320ec54e4cb96b23b5a654919575 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 18 Aug 2022 04:15:48 +0200 Subject: [PATCH 3429/3516] Restore fixed step fan speeds for google assistant (#76871) --- .../components/google_assistant/const.py | 18 +++ .../components/google_assistant/trait.py | 58 ++++++++- .../components/google_assistant/test_trait.py | 117 +++++++++++++++++- 3 files changed, 191 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index dbcf60ac098..20c4ab60e88 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -186,3 +186,21 @@ SOURCE_CLOUD = "cloud" SOURCE_LOCAL = "local" NOT_EXPOSE_LOCAL = {TYPE_ALARM, TYPE_LOCK} + +FAN_SPEEDS = { + "5/5": ["High", "Max", "Fast", "5"], + "4/5": ["Medium High", "4"], + "3/5": ["Medium", "3"], + "2/5": ["Medium Low", "2"], + "1/5": ["Low", "Min", "Slow", "1"], + "4/4": ["High", "Max", "Fast", "4"], + "3/4": ["Medium High", "3"], + "2/4": ["Medium Low", "2"], + "1/4": ["Low", "Min", "Slow", "1"], + "3/3": ["High", "Max", "Fast", "3"], + "2/3": ["Medium", "2"], + "1/3": ["Low", "Min", "Slow", "1"], + "2/2": ["High", "Max", "Fast", "2"], + "1/2": ["Low", "Min", "Slow", "1"], + "1/1": ["Normal", "1"], +} diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index edc8ed124b3..defc5b0cc89 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components import ( alarm_control_panel, @@ -68,6 +69,10 @@ from homeassistant.const import ( from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.helpers.network import get_url from homeassistant.util import color as color_util, dt, temperature as temp_util +from homeassistant.util.percentage import ( + ordered_list_item_to_percentage, + percentage_to_ordered_list_item, +) from .const import ( CHALLENGE_ACK_NEEDED, @@ -82,6 +87,7 @@ from .const import ( ERR_NOT_SUPPORTED, ERR_UNSUPPORTED_INPUT, ERR_VALUE_OUT_OF_RANGE, + FAN_SPEEDS, ) from .error import ChallengeNeeded, SmartHomeError @@ -157,6 +163,8 @@ COMMAND_CHARGE = f"{PREFIX_COMMANDS}Charge" TRAITS = [] +FAN_SPEED_MAX_SPEED_COUNT = 5 + def register_trait(trait): """Decorate a function to register a trait.""" @@ -1359,6 +1367,20 @@ class ArmDisArmTrait(_Trait): ) +def _get_fan_speed(speed_name: str) -> dict[str, Any]: + """Return a fan speed synonyms for a speed name.""" + speed_synonyms = FAN_SPEEDS.get(speed_name, [f"{speed_name}"]) + return { + "speed_name": speed_name, + "speed_values": [ + { + "speed_synonym": speed_synonyms, + "lang": "en", + } + ], + } + + @register_trait class FanSpeedTrait(_Trait): """Trait to control speed of Fan. @@ -1369,6 +1391,18 @@ class FanSpeedTrait(_Trait): name = TRAIT_FANSPEED commands = [COMMAND_FANSPEED, COMMAND_REVERSE] + def __init__(self, hass, state, config): + """Initialize a trait for a state.""" + super().__init__(hass, state, config) + if state.domain == fan.DOMAIN: + speed_count = min( + FAN_SPEED_MAX_SPEED_COUNT, + round(100 / self.state.attributes.get(fan.ATTR_PERCENTAGE_STEP) or 1.0), + ) + self._ordered_speed = [ + f"{speed}/{speed_count}" for speed in range(1, speed_count + 1) + ] + @staticmethod def supported(domain, features, device_class, _): """Test if state is supported.""" @@ -1397,6 +1431,18 @@ class FanSpeedTrait(_Trait): } ) + if self._ordered_speed: + result.update( + { + "availableFanSpeeds": { + "speeds": [ + _get_fan_speed(speed) for speed in self._ordered_speed + ], + "ordered": True, + }, + } + ) + elif domain == climate.DOMAIN: modes = self.state.attributes.get(climate.ATTR_FAN_MODES) or [] for mode in modes: @@ -1428,6 +1474,9 @@ class FanSpeedTrait(_Trait): if domain == fan.DOMAIN: percent = attrs.get(fan.ATTR_PERCENTAGE) or 0 response["currentFanSpeedPercent"] = percent + response["currentFanSpeedSetting"] = percentage_to_ordered_list_item( + self._ordered_speed, percent + ) return response @@ -1447,12 +1496,19 @@ class FanSpeedTrait(_Trait): ) if domain == fan.DOMAIN: + if fan_speed := params.get("fanSpeed"): + fan_speed_percent = ordered_list_item_to_percentage( + self._ordered_speed, fan_speed + ) + else: + fan_speed_percent = params.get("fanSpeedPercent") + await self.hass.services.async_call( fan.DOMAIN, fan.SERVICE_SET_PERCENTAGE, { ATTR_ENTITY_ID: self.state.entity_id, - fan.ATTR_PERCENTAGE: params["fanSpeedPercent"], + fan.ATTR_PERCENTAGE: fan_speed_percent, }, blocking=not self.config.should_report_state, context=data.context, diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 0012826074b..a3024c184d6 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1,6 +1,6 @@ """Tests for the Google Assistant traits.""" from datetime import datetime, timedelta -from unittest.mock import patch +from unittest.mock import ANY, patch import pytest @@ -1601,10 +1601,12 @@ async def test_fan_speed(hass): assert trt.sync_attributes() == { "reversible": False, "supportsFanSpeedPercent": True, + "availableFanSpeeds": ANY, } assert trt.query_attributes() == { "currentFanSpeedPercent": 33, + "currentFanSpeedSetting": ANY, } assert trt.can_execute(trait.COMMAND_FANSPEED, params={"fanSpeedPercent": 10}) @@ -1616,6 +1618,117 @@ async def test_fan_speed(hass): assert calls[0].data == {"entity_id": "fan.living_room_fan", "percentage": 10} +@pytest.mark.parametrize( + "percentage,percentage_step, speed, speeds, percentage_result", + [ + ( + 33, + 1.0, + "2/5", + [ + ["Low", "Min", "Slow", "1"], + ["Medium Low", "2"], + ["Medium", "3"], + ["Medium High", "4"], + ["High", "Max", "Fast", "5"], + ], + 40, + ), + ( + 40, + 1.0, + "2/5", + [ + ["Low", "Min", "Slow", "1"], + ["Medium Low", "2"], + ["Medium", "3"], + ["Medium High", "4"], + ["High", "Max", "Fast", "5"], + ], + 40, + ), + ( + 33, + 100 / 3, + "1/3", + [ + ["Low", "Min", "Slow", "1"], + ["Medium", "2"], + ["High", "Max", "Fast", "3"], + ], + 33, + ), + ( + 20, + 100 / 4, + "1/4", + [ + ["Low", "Min", "Slow", "1"], + ["Medium Low", "2"], + ["Medium High", "3"], + ["High", "Max", "Fast", "4"], + ], + 25, + ), + ], +) +async def test_fan_speed_ordered( + hass, + percentage: int, + percentage_step: float, + speed: str, + speeds: list[list[str]], + percentage_result: int, +): + """Test FanSpeed trait speed control support for fan domain.""" + assert helpers.get_google_type(fan.DOMAIN, None) is not None + assert trait.FanSpeedTrait.supported(fan.DOMAIN, fan.SUPPORT_SET_SPEED, None, None) + + trt = trait.FanSpeedTrait( + hass, + State( + "fan.living_room_fan", + STATE_ON, + attributes={ + "percentage": percentage, + "percentage_step": percentage_step, + }, + ), + BASIC_CONFIG, + ) + + assert trt.sync_attributes() == { + "reversible": False, + "supportsFanSpeedPercent": True, + "availableFanSpeeds": { + "ordered": True, + "speeds": [ + { + "speed_name": f"{idx+1}/{len(speeds)}", + "speed_values": [{"lang": "en", "speed_synonym": x}], + } + for idx, x in enumerate(speeds) + ], + }, + } + + assert trt.query_attributes() == { + "currentFanSpeedPercent": percentage, + "currentFanSpeedSetting": speed, + } + + assert trt.can_execute(trait.COMMAND_FANSPEED, params={"fanSpeed": speed}) + + calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_PERCENTAGE) + await trt.execute(trait.COMMAND_FANSPEED, BASIC_DATA, {"fanSpeed": speed}, {}) + + assert len(calls) == 1 + assert calls[0].data == { + "entity_id": "fan.living_room_fan", + "percentage": percentage_result, + } + + @pytest.mark.parametrize( "direction_state,direction_call", [ @@ -1647,10 +1760,12 @@ async def test_fan_reverse(hass, direction_state, direction_call): assert trt.sync_attributes() == { "reversible": True, "supportsFanSpeedPercent": True, + "availableFanSpeeds": ANY, } assert trt.query_attributes() == { "currentFanSpeedPercent": 33, + "currentFanSpeedSetting": ANY, } assert trt.can_execute(trait.COMMAND_REVERSE, params={}) From 03fac0c529fea8ffaa5290a194c863423f923d5d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Aug 2022 16:37:47 -1000 Subject: [PATCH 3430/3516] Fix race in notify setup (#76954) --- homeassistant/components/notify/__init__.py | 12 ++- homeassistant/components/notify/legacy.py | 21 ++--- tests/components/notify/test_init.py | 99 ++++++++++++++++++++- 3 files changed, 119 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 788c698c0ca..60d24578593 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -1,6 +1,8 @@ """Provides functionality to notify people.""" from __future__ import annotations +import asyncio + import voluptuous as vol import homeassistant.components.persistent_notification as pn @@ -40,13 +42,19 @@ PLATFORM_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the notify services.""" + platform_setups = async_setup_legacy(hass, config) + # We need to add the component here break the deadlock # when setting up integrations from config entries as # they would otherwise wait for notify to be # setup and thus the config entries would not be able to - # setup their platforms. + # setup their platforms, but we need to do it after + # the dispatcher is connected so we don't miss integrations + # that are registered before the dispatcher is connected hass.config.components.add(DOMAIN) - await async_setup_legacy(hass, config) + + if platform_setups: + await asyncio.wait([asyncio.create_task(setup) for setup in platform_setups]) async def persistent_notification(service: ServiceCall) -> None: """Send notification via the built-in persistsent_notify integration.""" diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py index 50b02324827..f9066b7dff9 100644 --- a/homeassistant/components/notify/legacy.py +++ b/homeassistant/components/notify/legacy.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Coroutine from functools import partial from typing import Any, cast @@ -32,7 +33,10 @@ NOTIFY_SERVICES = "notify_services" NOTIFY_DISCOVERY_DISPATCHER = "notify_discovery_dispatcher" -async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None: +@callback +def async_setup_legacy( + hass: HomeAssistant, config: ConfigType +) -> list[Coroutine[Any, Any, None]]: """Set up legacy notify services.""" hass.data.setdefault(NOTIFY_SERVICES, {}) hass.data.setdefault(NOTIFY_DISCOVERY_DISPATCHER, None) @@ -101,15 +105,6 @@ async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None: ) hass.config.components.add(f"{DOMAIN}.{integration_name}") - setup_tasks = [ - asyncio.create_task(async_setup_platform(integration_name, p_config)) - for integration_name, p_config in config_per_platform(config, DOMAIN) - if integration_name is not None - ] - - if setup_tasks: - await asyncio.wait(setup_tasks) - async def async_platform_discovered( platform: str, info: DiscoveryInfoType | None ) -> None: @@ -120,6 +115,12 @@ async def async_setup_legacy(hass: HomeAssistant, config: ConfigType) -> None: hass, DOMAIN, async_platform_discovered ) + return [ + async_setup_platform(integration_name, p_config) + for integration_name, p_config in config_per_platform(config, DOMAIN) + if integration_name is not None + ] + @callback def check_templates_warn(hass: HomeAssistant, tpl: template.Template) -> None: diff --git a/tests/components/notify/test_init.py b/tests/components/notify/test_init.py index ae32884add7..b691ed7a051 100644 --- a/tests/components/notify/test_init.py +++ b/tests/components/notify/test_init.py @@ -1,12 +1,13 @@ """The tests for notify services that change targets.""" +import asyncio from unittest.mock import Mock, patch import yaml from homeassistant import config as hass_config from homeassistant.components import notify -from homeassistant.const import SERVICE_RELOAD +from homeassistant.const import SERVICE_RELOAD, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.reload import async_setup_reload_service @@ -330,3 +331,99 @@ async def test_setup_platform_and_reload(hass, caplog, tmp_path): # Check if the dynamically notify services from setup were removed assert not hass.services.has_service(notify.DOMAIN, "testnotify2_c") assert not hass.services.has_service(notify.DOMAIN, "testnotify2_d") + + +async def test_setup_platform_before_notify_setup(hass, caplog, tmp_path): + """Test trying to setup a platform before notify is setup.""" + get_service_called = Mock() + + async def async_get_service(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"a": 1, "b": 2} + return NotificationService(hass, targetlist, "testnotify") + + async def async_get_service2(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"c": 3, "d": 4} + return NotificationService(hass, targetlist, "testnotify2") + + # Mock first platform + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + + # Initialize a second platform testnotify2 + mock_notify_platform( + hass, tmp_path, "testnotify2", async_get_service=async_get_service2 + ) + + hass_config = {"notify": [{"platform": "testnotify"}]} + + # Setup the second testnotify2 platform from discovery + load_coro = async_load_platform( + hass, Platform.NOTIFY, "testnotify2", {}, hass_config=hass_config + ) + + # Setup the testnotify platform + setup_coro = async_setup_component(hass, "notify", hass_config) + + load_task = asyncio.create_task(load_coro) + setup_task = asyncio.create_task(setup_coro) + + await asyncio.gather(load_task, setup_task) + + await hass.async_block_till_done() + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") + + +async def test_setup_platform_after_notify_setup(hass, caplog, tmp_path): + """Test trying to setup a platform after notify is setup.""" + get_service_called = Mock() + + async def async_get_service(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"a": 1, "b": 2} + return NotificationService(hass, targetlist, "testnotify") + + async def async_get_service2(hass, config, discovery_info=None): + """Get notify service for mocked platform.""" + get_service_called(config, discovery_info) + targetlist = {"c": 3, "d": 4} + return NotificationService(hass, targetlist, "testnotify2") + + # Mock first platform + mock_notify_platform( + hass, tmp_path, "testnotify", async_get_service=async_get_service + ) + + # Initialize a second platform testnotify2 + mock_notify_platform( + hass, tmp_path, "testnotify2", async_get_service=async_get_service2 + ) + + hass_config = {"notify": [{"platform": "testnotify"}]} + + # Setup the second testnotify2 platform from discovery + load_coro = async_load_platform( + hass, Platform.NOTIFY, "testnotify2", {}, hass_config=hass_config + ) + + # Setup the testnotify platform + setup_coro = async_setup_component(hass, "notify", hass_config) + + setup_task = asyncio.create_task(setup_coro) + load_task = asyncio.create_task(load_coro) + + await asyncio.gather(load_task, setup_task) + + await hass.async_block_till_done() + assert hass.services.has_service(notify.DOMAIN, "testnotify_a") + assert hass.services.has_service(notify.DOMAIN, "testnotify_b") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_c") + assert hass.services.has_service(notify.DOMAIN, "testnotify2_d") From 280ae91ba17aa7ed98eec2f41607969a03685b2f Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 17 Aug 2022 22:41:28 -0400 Subject: [PATCH 3431/3516] Pass the real config for Slack (#76960) --- homeassistant/components/slack/__init__.py | 6 ++++-- homeassistant/components/slack/const.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/slack/__init__.py b/homeassistant/components/slack/__init__.py index ae52013621f..a89f645e9b6 100644 --- a/homeassistant/components/slack/__init__.py +++ b/homeassistant/components/slack/__init__.py @@ -12,7 +12,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, discovery from homeassistant.helpers.typing import ConfigType -from .const import DATA_CLIENT, DOMAIN +from .const import DATA_CLIENT, DATA_HASS_CONFIG, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -21,6 +21,8 @@ PLATFORMS = [Platform.NOTIFY] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Slack component.""" + hass.data[DATA_HASS_CONFIG] = config + # Iterate all entries for notify to only get Slack if Platform.NOTIFY in config: for entry in config[Platform.NOTIFY]: @@ -55,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: Platform.NOTIFY, DOMAIN, hass.data[DOMAIN][entry.entry_id], - hass.data[DOMAIN], + hass.data[DATA_HASS_CONFIG], ) ) diff --git a/homeassistant/components/slack/const.py b/homeassistant/components/slack/const.py index b7b5707aeeb..83937f4a43e 100644 --- a/homeassistant/components/slack/const.py +++ b/homeassistant/components/slack/const.py @@ -14,3 +14,5 @@ CONF_DEFAULT_CHANNEL = "default_channel" DATA_CLIENT = "client" DEFAULT_TIMEOUT_SECONDS = 15 DOMAIN: Final = "slack" + +DATA_HASS_CONFIG = "slack_hass_config" From 6ab9652b600de93edec05e4ea40d768c6e04c9f6 Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Wed, 17 Aug 2022 22:41:59 -0400 Subject: [PATCH 3432/3516] Pass the real config for Discord (#76959) --- homeassistant/components/discord/__init__.py | 16 ++++++++++------ homeassistant/components/discord/const.py | 2 ++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/discord/__init__.py b/homeassistant/components/discord/__init__.py index ae06447f741..a52c079ac8e 100644 --- a/homeassistant/components/discord/__init__.py +++ b/homeassistant/components/discord/__init__.py @@ -7,12 +7,20 @@ from homeassistant.const import CONF_API_TOKEN, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import discovery +from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import DATA_HASS_CONFIG, DOMAIN PLATFORMS = [Platform.NOTIFY] +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Discord component.""" + + hass.data[DATA_HASS_CONFIG] = config + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Discord from a config entry.""" nextcord.VoiceClient.warn_nacl = False @@ -30,11 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.async_create_task( discovery.async_load_platform( - hass, - Platform.NOTIFY, - DOMAIN, - hass.data[DOMAIN][entry.entry_id], - hass.data[DOMAIN], + hass, Platform.NOTIFY, DOMAIN, dict(entry.data), hass.data[DATA_HASS_CONFIG] ) ) diff --git a/homeassistant/components/discord/const.py b/homeassistant/components/discord/const.py index 9f11c3e2d7a..82ddb890685 100644 --- a/homeassistant/components/discord/const.py +++ b/homeassistant/components/discord/const.py @@ -8,3 +8,5 @@ DEFAULT_NAME = "Discord" DOMAIN: Final = "discord" URL_PLACEHOLDER = {CONF_URL: "https://www.home-assistant.io/integrations/discord"} + +DATA_HASS_CONFIG = "discord_hass_config" From 82b6deeb799221e3a1b1043fd12edb5b07b26e93 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Aug 2022 16:43:22 -1000 Subject: [PATCH 3433/3516] Bump qingping-ble to 0.2.4 (#76958) --- homeassistant/components/qingping/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/qingping/manifest.json b/homeassistant/components/qingping/manifest.json index 221087de8c4..20adbaf15f1 100644 --- a/homeassistant/components/qingping/manifest.json +++ b/homeassistant/components/qingping/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/qingping", "bluetooth": [{ "local_name": "Qingping*" }], - "requirements": ["qingping-ble==0.2.3"], + "requirements": ["qingping-ble==0.2.4"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/requirements_all.txt b/requirements_all.txt index 81e62987592..785428e4c9e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2067,7 +2067,7 @@ pyzbar==0.1.7 pyzerproc==0.4.8 # homeassistant.components.qingping -qingping-ble==0.2.3 +qingping-ble==0.2.4 # homeassistant.components.qnap qnapstats==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 69d56e4ed40..b499659dc44 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1412,7 +1412,7 @@ pyws66i==1.1 pyzerproc==0.4.8 # homeassistant.components.qingping -qingping-ble==0.2.3 +qingping-ble==0.2.4 # homeassistant.components.rachio rachiopy==1.0.3 From 4a84a8caa95b6b13e8744790d31ed81e311f3f63 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 10:22:49 +0200 Subject: [PATCH 3434/3516] Use Platform enum (#76967) --- homeassistant/components/bayesian/__init__.py | 4 +++- homeassistant/components/google/__init__.py | 3 ++- homeassistant/components/ping/const.py | 4 +++- homeassistant/components/sabnzbd/__init__.py | 3 ++- .../components/slimproto/__init__.py | 4 ++-- homeassistant/components/sonos/const.py | 20 ++++++++----------- homeassistant/components/vulcan/__init__.py | 3 ++- homeassistant/components/webostv/const.py | 4 +++- homeassistant/components/ws66i/__init__.py | 4 ++-- 9 files changed, 27 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/bayesian/__init__.py b/homeassistant/components/bayesian/__init__.py index 485592dc5e4..e6f865b5656 100644 --- a/homeassistant/components/bayesian/__init__.py +++ b/homeassistant/components/bayesian/__init__.py @@ -1,4 +1,6 @@ """The bayesian component.""" +from homeassistant.const import Platform + DOMAIN = "bayesian" -PLATFORMS = ["binary_sensor"] +PLATFORMS = [Platform.BINARY_SENSOR] diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 4b72aaa77ad..c983868b167 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -29,6 +29,7 @@ from homeassistant.const import ( CONF_ENTITIES, CONF_NAME, CONF_OFFSET, + Platform, ) from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ( @@ -88,7 +89,7 @@ YAML_DEVICES = f"{DOMAIN}_calendars.yaml" TOKEN_FILE = f".{DOMAIN}.token" -PLATFORMS = ["calendar"] +PLATFORMS = [Platform.CALENDAR] CONFIG_SCHEMA = vol.Schema( diff --git a/homeassistant/components/ping/const.py b/homeassistant/components/ping/const.py index 9ca99db2419..1a77c62fa5c 100644 --- a/homeassistant/components/ping/const.py +++ b/homeassistant/components/ping/const.py @@ -1,5 +1,7 @@ """Tracks devices by sending a ICMP echo request (ping).""" +from homeassistant.const import Platform + # The ping binary and icmplib timeouts are not the same # timeout. ping is an overall timeout, icmplib is the # time since the data was sent. @@ -13,6 +15,6 @@ ICMP_TIMEOUT = 1 PING_ATTEMPTS_COUNT = 3 DOMAIN = "ping" -PLATFORMS = ["binary_sensor"] +PLATFORMS = [Platform.BINARY_SENSOR] PING_PRIVS = "ping_privs" diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index aca8d1cd9f4..fe9a64f3d6b 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_PORT, CONF_SENSORS, CONF_SSL, + Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError @@ -47,7 +48,7 @@ from .const import ( from .sab import get_client from .sensor import OLD_SENSOR_KEYS -PLATFORMS = ["sensor"] +PLATFORMS = [Platform.SENSOR] _LOGGER = logging.getLogger(__name__) SERVICES = ( diff --git a/homeassistant/components/slimproto/__init__.py b/homeassistant/components/slimproto/__init__.py index a96ff7ae925..c22349bb2f2 100644 --- a/homeassistant/components/slimproto/__init__.py +++ b/homeassistant/components/slimproto/__init__.py @@ -4,13 +4,13 @@ from __future__ import annotations from aioslimproto import SlimServer from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import Event, HomeAssistant from homeassistant.helpers import device_registry as dr from .const import DOMAIN -PLATFORMS = ["media_player"] +PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/sonos/const.py b/homeassistant/components/sonos/const.py index e6a5225e30e..4c10bb113c7 100644 --- a/homeassistant/components/sonos/const.py +++ b/homeassistant/components/sonos/const.py @@ -1,8 +1,6 @@ """Const for Sonos.""" import datetime -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.media_player.const import ( MEDIA_CLASS_ALBUM, MEDIA_CLASS_ARTIST, @@ -19,22 +17,20 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_PLAYLIST, MEDIA_TYPE_TRACK, ) -from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import Platform UPNP_ST = "urn:schemas-upnp-org:device:ZonePlayer:1" DOMAIN = "sonos" DATA_SONOS = "sonos_media_player" DATA_SONOS_DISCOVERY_MANAGER = "sonos_discovery_manager" -PLATFORMS = { - BINARY_SENSOR_DOMAIN, - MP_DOMAIN, - NUMBER_DOMAIN, - SENSOR_DOMAIN, - SWITCH_DOMAIN, -} +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.MEDIA_PLAYER, + Platform.NUMBER, + Platform.SENSOR, + Platform.SWITCH, +] SONOS_ARTIST = "artists" SONOS_ALBUM = "albums" diff --git a/homeassistant/components/vulcan/__init__.py b/homeassistant/components/vulcan/__init__.py index 8e66075935b..0bfd09d590d 100644 --- a/homeassistant/components/vulcan/__init__.py +++ b/homeassistant/components/vulcan/__init__.py @@ -4,13 +4,14 @@ from aiohttp import ClientConnectorError from vulcan import Account, Keystore, UnauthorizedCertificateException, Vulcan from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN -PLATFORMS = ["calendar"] +PLATFORMS = [Platform.CALENDAR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/webostv/const.py b/homeassistant/components/webostv/const.py index f471ca7340d..830c0a4134a 100644 --- a/homeassistant/components/webostv/const.py +++ b/homeassistant/components/webostv/const.py @@ -4,8 +4,10 @@ import asyncio from aiowebostv import WebOsTvCommandError from websockets.exceptions import ConnectionClosed, ConnectionClosedOK +from homeassistant.const import Platform + DOMAIN = "webostv" -PLATFORMS = ["media_player"] +PLATFORMS = [Platform.MEDIA_PLAYER] DATA_CONFIG_ENTRY = "config_entry" DATA_HASS_CONFIG = "hass_config" DEFAULT_NAME = "LG webOS Smart TV" diff --git a/homeassistant/components/ws66i/__init__.py b/homeassistant/components/ws66i/__init__.py index 0b40ce84816..cffedc2f684 100644 --- a/homeassistant/components/ws66i/__init__.py +++ b/homeassistant/components/ws66i/__init__.py @@ -6,7 +6,7 @@ import logging from pyws66i import WS66i, get_ws66i from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_IP_ADDRESS, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_IP_ADDRESS, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -16,7 +16,7 @@ from .models import SourceRep, Ws66iData _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["media_player"] +PLATFORMS = [Platform.MEDIA_PLAYER] @callback From 6fbdc8e33939b12053e58047db8862b3f99fa0d1 Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Thu, 18 Aug 2022 04:51:28 -0400 Subject: [PATCH 3435/3516] Add Fully Kiosk Browser number platform (#76952) Co-authored-by: Franck Nijhof --- .../components/fully_kiosk/__init__.py | 8 +- .../components/fully_kiosk/number.py | 99 +++++++++++++++++++ tests/components/fully_kiosk/test_number.py | 91 +++++++++++++++++ 3 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/fully_kiosk/number.py create mode 100644 tests/components/fully_kiosk/test_number.py diff --git a/homeassistant/components/fully_kiosk/__init__.py b/homeassistant/components/fully_kiosk/__init__.py index 5a3d6078004..ebd9af1134a 100644 --- a/homeassistant/components/fully_kiosk/__init__.py +++ b/homeassistant/components/fully_kiosk/__init__.py @@ -6,7 +6,13 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import FullyKioskDataUpdateCoordinator -PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.NUMBER, + Platform.SENSOR, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/fully_kiosk/number.py b/homeassistant/components/fully_kiosk/number.py new file mode 100644 index 00000000000..d39f3f6391d --- /dev/null +++ b/homeassistant/components/fully_kiosk/number.py @@ -0,0 +1,99 @@ +"""Fully Kiosk Browser number entity.""" +from __future__ import annotations + +from contextlib import suppress + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import TIME_SECONDS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import FullyKioskDataUpdateCoordinator +from .entity import FullyKioskEntity + +ENTITY_TYPES: tuple[NumberEntityDescription, ...] = ( + NumberEntityDescription( + key="timeToScreensaverV2", + name="Screensaver timer", + native_max_value=9999, + native_step=1, + native_min_value=0, + native_unit_of_measurement=TIME_SECONDS, + entity_category=EntityCategory.CONFIG, + ), + NumberEntityDescription( + key="screensaverBrightness", + name="Screensaver brightness", + native_max_value=255, + native_step=1, + native_min_value=0, + entity_category=EntityCategory.CONFIG, + ), + NumberEntityDescription( + key="timeToScreenOffV2", + name="Screen off timer", + native_max_value=9999, + native_step=1, + native_min_value=0, + native_unit_of_measurement=TIME_SECONDS, + entity_category=EntityCategory.CONFIG, + ), + NumberEntityDescription( + key="screenBrightness", + name="Screen brightness", + native_max_value=255, + native_step=1, + native_min_value=0, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Fully Kiosk Browser number entities.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + FullyNumberEntity(coordinator, entity) + for entity in ENTITY_TYPES + if entity.key in coordinator.data["settings"] + ) + + +class FullyNumberEntity(FullyKioskEntity, NumberEntity): + """Representation of a Fully Kiosk Browser entity.""" + + def __init__( + self, + coordinator: FullyKioskDataUpdateCoordinator, + description: NumberEntityDescription, + ) -> None: + """Initialize the number entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.data['deviceID']}-{description.key}" + + @property + def native_value(self) -> int | None: + """Return the state of the number entity.""" + if ( + value := self.coordinator.data["settings"].get(self.entity_description.key) + ) is None: + return None + + with suppress(ValueError): + return int(value) + + return None + + async def async_set_native_value(self, value: float) -> None: + """Set the value of the entity.""" + await self.coordinator.fully.setConfigurationString( + self.entity_description.key, int(value) + ) diff --git a/tests/components/fully_kiosk/test_number.py b/tests/components/fully_kiosk/test_number.py new file mode 100644 index 00000000000..968faa3f0b4 --- /dev/null +++ b/tests/components/fully_kiosk/test_number.py @@ -0,0 +1,91 @@ +"""Test the Fully Kiosk Browser number entities.""" +from unittest.mock import MagicMock + +from homeassistant.components.fully_kiosk.const import DOMAIN, UPDATE_INTERVAL +import homeassistant.components.number as number +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util import dt + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_numbers( + hass: HomeAssistant, + mock_fully_kiosk: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test standard Fully Kiosk numbers.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("number.amazon_fire_screensaver_timer") + assert state + assert state.state == "900" + entry = entity_registry.async_get("number.amazon_fire_screensaver_timer") + assert entry + assert entry.unique_id == "abcdef-123456-timeToScreensaverV2" + await set_value(hass, "number.amazon_fire_screensaver_timer", 600) + assert len(mock_fully_kiosk.setConfigurationString.mock_calls) == 1 + + state = hass.states.get("number.amazon_fire_screensaver_brightness") + assert state + assert state.state == "0" + entry = entity_registry.async_get("number.amazon_fire_screensaver_brightness") + assert entry + assert entry.unique_id == "abcdef-123456-screensaverBrightness" + + state = hass.states.get("number.amazon_fire_screen_off_timer") + assert state + assert state.state == "0" + entry = entity_registry.async_get("number.amazon_fire_screen_off_timer") + assert entry + assert entry.unique_id == "abcdef-123456-timeToScreenOffV2" + + state = hass.states.get("number.amazon_fire_screen_brightness") + assert state + assert state.state == "9" + entry = entity_registry.async_get("number.amazon_fire_screen_brightness") + assert entry + assert entry.unique_id == "abcdef-123456-screenBrightness" + + # Test invalid numeric data + mock_fully_kiosk.getSettings.return_value = {"screenBrightness": "invalid"} + async_fire_time_changed(hass, dt.utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("number.amazon_fire_screen_brightness") + assert state + assert state.state == STATE_UNKNOWN + + # Test unknown/missing data + mock_fully_kiosk.getSettings.return_value = {} + async_fire_time_changed(hass, dt.utcnow() + UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("number.amazon_fire_screensaver_timer") + assert state + assert state.state == STATE_UNKNOWN + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.configuration_url == "http://192.168.1.234:2323" + assert device_entry.entry_type is None + assert device_entry.hw_version is None + assert device_entry.identifiers == {(DOMAIN, "abcdef-123456")} + assert device_entry.manufacturer == "amzn" + assert device_entry.model == "KFDOWI" + assert device_entry.name == "Amazon Fire" + assert device_entry.sw_version == "1.42.5" + + +def set_value(hass, entity_id, value): + """Set the value of a number entity.""" + return hass.services.async_call( + number.DOMAIN, + "set_value", + {ATTR_ENTITY_ID: entity_id, number.ATTR_VALUE: value}, + blocking=True, + ) From 6e9c67c203aec123cd111e26c1588776cecb4e39 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Aug 2022 10:52:55 +0200 Subject: [PATCH 3436/3516] Update coverage to 6.4.4 (#76907) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 63634bc4022..94a1e0c3120 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,7 +8,7 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt codecov==2.1.12 -coverage==6.4.3 +coverage==6.4.4 freezegun==1.2.1 mock-open==1.4.0 mypy==0.971 From 681b726128efb223179a8aec44f09226d0e60bc2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 18 Aug 2022 11:40:24 +0200 Subject: [PATCH 3437/3516] Add parental control switches to NextDNS integration (#76559) * Add new switches * Make new switches disabled by default * Update tests --- homeassistant/components/nextdns/switch.py | 336 ++++++++++ tests/components/nextdns/test_switch.py | 673 +++++++++++++++++++++ 2 files changed, 1009 insertions(+) diff --git a/homeassistant/components/nextdns/switch.py b/homeassistant/components/nextdns/switch.py index 4bd3c14c20f..a353106723b 100644 --- a/homeassistant/components/nextdns/switch.py +++ b/homeassistant/components/nextdns/switch.py @@ -184,6 +184,342 @@ SWITCHES = ( icon="mdi:youtube", state=lambda data: data.youtube_restricted_mode, ), + NextDnsSwitchEntityDescription[Settings]( + key="block_9gag", + name="Block 9GAG", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:file-gif-box", + state=lambda data: data.block_9gag, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_amazon", + name="Block Amazon", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:cart-outline", + state=lambda data: data.block_amazon, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_blizzard", + name="Block Blizzard", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:sword-cross", + state=lambda data: data.block_blizzard, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_dailymotion", + name="Block Dailymotion", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:movie-search-outline", + state=lambda data: data.block_dailymotion, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_discord", + name="Block Discord", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:message-text", + state=lambda data: data.block_discord, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_disneyplus", + name="Block Disney Plus", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:movie-search-outline", + state=lambda data: data.block_disneyplus, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_ebay", + name="Block eBay", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:basket-outline", + state=lambda data: data.block_ebay, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_facebook", + name="Block Facebook", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:facebook", + state=lambda data: data.block_facebook, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_fortnite", + name="Block Fortnite", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:tank", + state=lambda data: data.block_fortnite, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_hulu", + name="Block Hulu", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:hulu", + state=lambda data: data.block_hulu, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_imgur", + name="Block Imgur", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:camera-image", + state=lambda data: data.block_imgur, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_instagram", + name="Block Instagram", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:instagram", + state=lambda data: data.block_instagram, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_leagueoflegends", + name="Block League of Legends", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:sword", + state=lambda data: data.block_leagueoflegends, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_messenger", + name="Block Messenger", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:message-text", + state=lambda data: data.block_messenger, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_minecraft", + name="Block Minecraft", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:minecraft", + state=lambda data: data.block_minecraft, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_netflix", + name="Block Netflix", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:netflix", + state=lambda data: data.block_netflix, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_pinterest", + name="Block Pinterest", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:pinterest", + state=lambda data: data.block_pinterest, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_primevideo", + name="Block Prime Video", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:filmstrip", + state=lambda data: data.block_primevideo, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_reddit", + name="Block Reddit", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:reddit", + state=lambda data: data.block_reddit, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_roblox", + name="Block Roblox", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:robot", + state=lambda data: data.block_roblox, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_signal", + name="Block Signal", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:chat-outline", + state=lambda data: data.block_signal, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_skype", + name="Block Skype", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:skype", + state=lambda data: data.block_skype, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_snapchat", + name="Block Snapchat", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:snapchat", + state=lambda data: data.block_snapchat, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_spotify", + name="Block Spotify", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:spotify", + state=lambda data: data.block_spotify, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_steam", + name="Block Steam", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:steam", + state=lambda data: data.block_steam, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_telegram", + name="Block Telegram", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:send-outline", + state=lambda data: data.block_telegram, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_tiktok", + name="Block TikTok", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:music-note", + state=lambda data: data.block_tiktok, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_tinder", + name="Block Tinder", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:fire", + state=lambda data: data.block_tinder, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_tumblr", + name="Block Tumblr", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:image-outline", + state=lambda data: data.block_tumblr, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_twitch", + name="Block Twitch", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:twitch", + state=lambda data: data.block_twitch, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_twitter", + name="Block Twitter", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:twitter", + state=lambda data: data.block_twitter, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_vimeo", + name="Block Vimeo", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:vimeo", + state=lambda data: data.block_vimeo, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_vk", + name="Block VK", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:power-socket-eu", + state=lambda data: data.block_vk, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_whatsapp", + name="Block WhatsApp", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:whatsapp", + state=lambda data: data.block_whatsapp, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_xboxlive", + name="Block Xbox Live", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:microsoft-xbox", + state=lambda data: data.block_xboxlive, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_youtube", + name="Block YouTube", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:youtube", + state=lambda data: data.block_youtube, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_zoom", + name="Block Zoom", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:video", + state=lambda data: data.block_zoom, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_dating", + name="Block dating", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:candelabra", + state=lambda data: data.block_dating, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_gambling", + name="Block gambling", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:slot-machine", + state=lambda data: data.block_gambling, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_piracy", + name="Block piracy", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:pirate", + state=lambda data: data.block_piracy, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_porn", + name="Block porn", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:movie-off", + state=lambda data: data.block_porn, + ), + NextDnsSwitchEntityDescription[Settings]( + key="block_social_networks", + name="Block social networks", + entity_category=EntityCategory.CONFIG, + entity_registry_enabled_default=False, + icon="mdi:facebook", + state=lambda data: data.block_social_networks, + ), ) diff --git a/tests/components/nextdns/test_switch.py b/tests/components/nextdns/test_switch.py index 3e07a2633d1..cc873fcc4b7 100644 --- a/tests/components/nextdns/test_switch.py +++ b/tests/components/nextdns/test_switch.py @@ -4,6 +4,7 @@ from unittest.mock import patch from nextdns import ApiError +from homeassistant.components.nextdns.const import DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, @@ -26,6 +27,342 @@ async def test_switch(hass: HomeAssistant) -> None: """Test states of the switches.""" registry = er.async_get(hass) + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_9gag", + suggested_object_id="fake_profile_block_9gag", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_amazon", + suggested_object_id="fake_profile_block_amazon", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_blizzard", + suggested_object_id="fake_profile_block_blizzard", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_dailymotion", + suggested_object_id="fake_profile_block_dailymotion", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_discord", + suggested_object_id="fake_profile_block_discord", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_disneyplus", + suggested_object_id="fake_profile_block_disneyplus", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_ebay", + suggested_object_id="fake_profile_block_ebay", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_facebook", + suggested_object_id="fake_profile_block_facebook", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_fortnite", + suggested_object_id="fake_profile_block_fortnite", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_hulu", + suggested_object_id="fake_profile_block_hulu", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_imgur", + suggested_object_id="fake_profile_block_imgur", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_instagram", + suggested_object_id="fake_profile_block_instagram", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_leagueoflegends", + suggested_object_id="fake_profile_block_league_of_legends", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_messenger", + suggested_object_id="fake_profile_block_messenger", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_minecraft", + suggested_object_id="fake_profile_block_minecraft", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_netflix", + suggested_object_id="fake_profile_block_netflix", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_pinterest", + suggested_object_id="fake_profile_block_pinterest", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_primevideo", + suggested_object_id="fake_profile_block_primevideo", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_reddit", + suggested_object_id="fake_profile_block_reddit", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_roblox", + suggested_object_id="fake_profile_block_roblox", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_signal", + suggested_object_id="fake_profile_block_signal", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_skype", + suggested_object_id="fake_profile_block_skype", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_snapchat", + suggested_object_id="fake_profile_block_snapchat", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_spotify", + suggested_object_id="fake_profile_block_spotify", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_steam", + suggested_object_id="fake_profile_block_steam", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_telegram", + suggested_object_id="fake_profile_block_telegram", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_tiktok", + suggested_object_id="fake_profile_block_tiktok", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_tinder", + suggested_object_id="fake_profile_block_tinder", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_tumblr", + suggested_object_id="fake_profile_block_tumblr", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_twitch", + suggested_object_id="fake_profile_block_twitch", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_twitter", + suggested_object_id="fake_profile_block_twitter", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_vimeo", + suggested_object_id="fake_profile_block_vimeo", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_vk", + suggested_object_id="fake_profile_block_vk", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_whatsapp", + suggested_object_id="fake_profile_block_whatsapp", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_xboxlive", + suggested_object_id="fake_profile_block_xboxlive", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_youtube", + suggested_object_id="fake_profile_block_youtube", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_zoom", + suggested_object_id="fake_profile_block_zoom", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_dating", + suggested_object_id="fake_profile_block_dating", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_gambling", + suggested_object_id="fake_profile_block_gambling", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_piracy", + suggested_object_id="fake_profile_block_piracy", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_porn", + suggested_object_id="fake_profile_block_porn", + disabled_by=None, + ) + + registry.async_get_or_create( + SWITCH_DOMAIN, + DOMAIN, + "xyz12_block_social_networks", + suggested_object_id="fake_profile_block_social_networks", + disabled_by=None, + ) + await init_integration(hass) state = hass.states.get("switch.fake_profile_ai_driven_threat_detection") @@ -218,6 +555,342 @@ async def test_switch(hass: HomeAssistant) -> None: assert entry assert entry.unique_id == "xyz12_web3" + state = hass.states.get("switch.fake_profile_block_9gag") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_9gag") + assert entry + assert entry.unique_id == "xyz12_block_9gag" + + state = hass.states.get("switch.fake_profile_block_amazon") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_amazon") + assert entry + assert entry.unique_id == "xyz12_block_amazon" + + state = hass.states.get("switch.fake_profile_block_blizzard") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_blizzard") + assert entry + assert entry.unique_id == "xyz12_block_blizzard" + + state = hass.states.get("switch.fake_profile_block_dailymotion") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_dailymotion") + assert entry + assert entry.unique_id == "xyz12_block_dailymotion" + + state = hass.states.get("switch.fake_profile_block_discord") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_discord") + assert entry + assert entry.unique_id == "xyz12_block_discord" + + state = hass.states.get("switch.fake_profile_block_disneyplus") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_disneyplus") + assert entry + assert entry.unique_id == "xyz12_block_disneyplus" + + state = hass.states.get("switch.fake_profile_block_ebay") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_ebay") + assert entry + assert entry.unique_id == "xyz12_block_ebay" + + state = hass.states.get("switch.fake_profile_block_facebook") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_facebook") + assert entry + assert entry.unique_id == "xyz12_block_facebook" + + state = hass.states.get("switch.fake_profile_block_fortnite") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_fortnite") + assert entry + assert entry.unique_id == "xyz12_block_fortnite" + + state = hass.states.get("switch.fake_profile_block_hulu") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_hulu") + assert entry + assert entry.unique_id == "xyz12_block_hulu" + + state = hass.states.get("switch.fake_profile_block_imgur") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_imgur") + assert entry + assert entry.unique_id == "xyz12_block_imgur" + + state = hass.states.get("switch.fake_profile_block_instagram") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_instagram") + assert entry + assert entry.unique_id == "xyz12_block_instagram" + + state = hass.states.get("switch.fake_profile_block_league_of_legends") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_league_of_legends") + assert entry + assert entry.unique_id == "xyz12_block_leagueoflegends" + + state = hass.states.get("switch.fake_profile_block_messenger") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_messenger") + assert entry + assert entry.unique_id == "xyz12_block_messenger" + + state = hass.states.get("switch.fake_profile_block_minecraft") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_minecraft") + assert entry + assert entry.unique_id == "xyz12_block_minecraft" + + state = hass.states.get("switch.fake_profile_block_netflix") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_netflix") + assert entry + assert entry.unique_id == "xyz12_block_netflix" + + state = hass.states.get("switch.fake_profile_block_pinterest") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_pinterest") + assert entry + assert entry.unique_id == "xyz12_block_pinterest" + + state = hass.states.get("switch.fake_profile_block_primevideo") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_primevideo") + assert entry + assert entry.unique_id == "xyz12_block_primevideo" + + state = hass.states.get("switch.fake_profile_block_reddit") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_reddit") + assert entry + assert entry.unique_id == "xyz12_block_reddit" + + state = hass.states.get("switch.fake_profile_block_roblox") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_roblox") + assert entry + assert entry.unique_id == "xyz12_block_roblox" + + state = hass.states.get("switch.fake_profile_block_signal") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_signal") + assert entry + assert entry.unique_id == "xyz12_block_signal" + + state = hass.states.get("switch.fake_profile_block_skype") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_skype") + assert entry + assert entry.unique_id == "xyz12_block_skype" + + state = hass.states.get("switch.fake_profile_block_snapchat") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_snapchat") + assert entry + assert entry.unique_id == "xyz12_block_snapchat" + + state = hass.states.get("switch.fake_profile_block_spotify") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_spotify") + assert entry + assert entry.unique_id == "xyz12_block_spotify" + + state = hass.states.get("switch.fake_profile_block_steam") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_steam") + assert entry + assert entry.unique_id == "xyz12_block_steam" + + state = hass.states.get("switch.fake_profile_block_telegram") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_telegram") + assert entry + assert entry.unique_id == "xyz12_block_telegram" + + state = hass.states.get("switch.fake_profile_block_tiktok") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_tiktok") + assert entry + assert entry.unique_id == "xyz12_block_tiktok" + + state = hass.states.get("switch.fake_profile_block_tinder") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_tinder") + assert entry + assert entry.unique_id == "xyz12_block_tinder" + + state = hass.states.get("switch.fake_profile_block_tumblr") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_tumblr") + assert entry + assert entry.unique_id == "xyz12_block_tumblr" + + state = hass.states.get("switch.fake_profile_block_twitch") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_twitch") + assert entry + assert entry.unique_id == "xyz12_block_twitch" + + state = hass.states.get("switch.fake_profile_block_twitter") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_twitter") + assert entry + assert entry.unique_id == "xyz12_block_twitter" + + state = hass.states.get("switch.fake_profile_block_vimeo") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_vimeo") + assert entry + assert entry.unique_id == "xyz12_block_vimeo" + + state = hass.states.get("switch.fake_profile_block_vk") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_vk") + assert entry + assert entry.unique_id == "xyz12_block_vk" + + state = hass.states.get("switch.fake_profile_block_whatsapp") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_whatsapp") + assert entry + assert entry.unique_id == "xyz12_block_whatsapp" + + state = hass.states.get("switch.fake_profile_block_xboxlive") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_xboxlive") + assert entry + assert entry.unique_id == "xyz12_block_xboxlive" + + state = hass.states.get("switch.fake_profile_block_youtube") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_youtube") + assert entry + assert entry.unique_id == "xyz12_block_youtube" + + state = hass.states.get("switch.fake_profile_block_zoom") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_zoom") + assert entry + assert entry.unique_id == "xyz12_block_zoom" + + state = hass.states.get("switch.fake_profile_block_dating") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_dating") + assert entry + assert entry.unique_id == "xyz12_block_dating" + + state = hass.states.get("switch.fake_profile_block_gambling") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_gambling") + assert entry + assert entry.unique_id == "xyz12_block_gambling" + + state = hass.states.get("switch.fake_profile_block_piracy") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_piracy") + assert entry + assert entry.unique_id == "xyz12_block_piracy" + + state = hass.states.get("switch.fake_profile_block_porn") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_porn") + assert entry + assert entry.unique_id == "xyz12_block_porn" + + state = hass.states.get("switch.fake_profile_block_social_networks") + assert state + assert state.state == STATE_ON + + entry = registry.async_get("switch.fake_profile_block_social_networks") + assert entry + assert entry.unique_id == "xyz12_block_social_networks" + async def test_switch_on(hass: HomeAssistant) -> None: """Test the switch can be turned on.""" From a79f578b1da5f499c3dca81d352f836176665ab5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 18 Aug 2022 12:45:24 +0200 Subject: [PATCH 3438/3516] Add issue_domain parameter to repairs.create_issue (#76972) --- homeassistant/components/repairs/issue_handler.py | 6 ++++-- homeassistant/components/repairs/issue_registry.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index b37d4c10e06..eecbfe59bde 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -119,11 +119,11 @@ def async_create_issue( domain: str, issue_id: str, *, - issue_domain: str | None = None, breaks_in_ha_version: str | None = None, data: dict[str, str | int | float | None] | None = None, is_fixable: bool, is_persistent: bool = False, + issue_domain: str | None = None, learn_more_url: str | None = None, severity: IssueSeverity, translation_key: str, @@ -142,11 +142,11 @@ def async_create_issue( issue_registry.async_get_or_create( domain, issue_id, - issue_domain=issue_domain, breaks_in_ha_version=breaks_in_ha_version, data=data, is_fixable=is_fixable, is_persistent=is_persistent, + issue_domain=issue_domain, learn_more_url=learn_more_url, severity=severity, translation_key=translation_key, @@ -163,6 +163,7 @@ def create_issue( data: dict[str, str | int | float | None] | None = None, is_fixable: bool, is_persistent: bool = False, + issue_domain: str | None = None, learn_more_url: str | None = None, severity: IssueSeverity, translation_key: str, @@ -180,6 +181,7 @@ def create_issue( data=data, is_fixable=is_fixable, is_persistent=is_persistent, + issue_domain=issue_domain, learn_more_url=learn_more_url, severity=severity, translation_key=translation_key, diff --git a/homeassistant/components/repairs/issue_registry.py b/homeassistant/components/repairs/issue_registry.py index f9a15e0f165..a8843011023 100644 --- a/homeassistant/components/repairs/issue_registry.py +++ b/homeassistant/components/repairs/issue_registry.py @@ -106,11 +106,11 @@ class IssueRegistry: domain: str, issue_id: str, *, - issue_domain: str | None = None, breaks_in_ha_version: str | None = None, data: dict[str, str | int | float | None] | None = None, is_fixable: bool, is_persistent: bool, + issue_domain: str | None = None, learn_more_url: str | None = None, severity: IssueSeverity, translation_key: str, From 07ba3c1383df5c6a57ef7e4486f36875232e7256 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 13:27:05 +0200 Subject: [PATCH 3439/3516] Add update checks to pylint plugin (#76912) --- pylint/plugins/hass_enforce_type_hints.py | 63 +++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 99d06c380fd..1849d38fbc3 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -2041,6 +2041,69 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "update": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="RestoreEntity", + matches=_RESTORE_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="UpdateEntity", + matches=[ + TypeHintMatch( + function_name="auto_update", + return_type="bool", + ), + TypeHintMatch( + function_name="installed_version", + return_type=["str", None], + ), + TypeHintMatch( + function_name="device_class", + return_type=["UpdateDeviceClass", "str", None], + ), + TypeHintMatch( + function_name="in_progress", + return_type=["bool", "int", None], + ), + TypeHintMatch( + function_name="latest_version", + return_type=["str", None], + ), + TypeHintMatch( + function_name="release_summary", + return_type=["str", None], + ), + TypeHintMatch( + function_name="release_url", + return_type=["str", None], + ), + TypeHintMatch( + function_name="supported_features", + return_type="int", + ), + TypeHintMatch( + function_name="title", + return_type=["str", None], + ), + TypeHintMatch( + function_name="install", + arg_types={1: "str | None", 2: "bool"}, + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="release_notes", + return_type=["str", None], + has_async_counterpart=True, + ), + ], + ), + ], "water_heater": [ ClassTypeHintMatch( base_class="Entity", From 1aef60c81cfdc73231f9c6ddfed33e62377da6eb Mon Sep 17 00:00:00 2001 From: Charles Garwood Date: Thu, 18 Aug 2022 07:33:38 -0400 Subject: [PATCH 3440/3516] Add screen on/off switch to Fully Kiosk Browser integration (#76957) --- homeassistant/components/fully_kiosk/switch.py | 7 +++++++ tests/components/fully_kiosk/test_switch.py | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/homeassistant/components/fully_kiosk/switch.py b/homeassistant/components/fully_kiosk/switch.py index 7dfcc1e71ac..581700c87d6 100644 --- a/homeassistant/components/fully_kiosk/switch.py +++ b/homeassistant/components/fully_kiosk/switch.py @@ -66,6 +66,13 @@ SWITCHES: tuple[FullySwitchEntityDescription, ...] = ( off_action=lambda fully: fully.disableMotionDetection(), is_on_fn=lambda data: data["settings"].get("motionDetection"), ), + FullySwitchEntityDescription( + key="screenOn", + name="Screen", + on_action=lambda fully: fully.screenOn(), + off_action=lambda fully: fully.screenOff(), + is_on_fn=lambda data: data.get("screenOn"), + ), ) diff --git a/tests/components/fully_kiosk/test_switch.py b/tests/components/fully_kiosk/test_switch.py index 6b6d2829790..8da01ff2fe9 100644 --- a/tests/components/fully_kiosk/test_switch.py +++ b/tests/components/fully_kiosk/test_switch.py @@ -61,6 +61,17 @@ async def test_switches( await call_service(hass, "turn_off", "switch.amazon_fire_motion_detection") assert len(mock_fully_kiosk.disableMotionDetection.mock_calls) == 1 + entity = hass.states.get("switch.amazon_fire_screen") + assert entity + assert entity.state == "on" + entry = entity_registry.async_get("switch.amazon_fire_screen") + assert entry + assert entry.unique_id == "abcdef-123456-screenOn" + await call_service(hass, "turn_off", "switch.amazon_fire_screen") + assert len(mock_fully_kiosk.screenOff.mock_calls) == 1 + await call_service(hass, "turn_on", "switch.amazon_fire_screen") + assert len(mock_fully_kiosk.screenOn.mock_calls) == 1 + assert entry.device_id device_entry = device_registry.async_get(entry.device_id) assert device_entry From c212fe7ca51bd87ea66734ff2433a40f1c470c7d Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 18 Aug 2022 14:06:50 +0200 Subject: [PATCH 3441/3516] Adjust version comparison in HA Cloud account linking (#76978) --- homeassistant/components/cloud/account_link.py | 5 ++++- tests/components/cloud/test_account_link.py | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index 19819307cf0..3cf021c24fe 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -18,6 +18,9 @@ CACHE_TIMEOUT = 3600 _LOGGER = logging.getLogger(__name__) CURRENT_VERSION = AwesomeVersion(HA_VERSION) +CURRENT_PLAIN_VERSION = AwesomeVersion( + CURRENT_VERSION.string.removesuffix(f"{CURRENT_VERSION.modifier}") +) @callback @@ -35,7 +38,7 @@ async def async_provide_implementation(hass: HomeAssistant, domain: str): for service in services: if ( service["service"] == domain - and CURRENT_VERSION >= service["min_version"] + and CURRENT_PLAIN_VERSION >= service["min_version"] and ( service.get("accepts_new_authorizations", True) or ( diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index ad914436c07..6928e2b7a11 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -57,6 +57,7 @@ async def test_setup_provide_implementation(hass): return_value=[ {"service": "test", "min_version": "0.1.0"}, {"service": "too_new", "min_version": "1000000.0.0"}, + {"service": "dev", "min_version": "2022.9.0"}, { "service": "deprecated", "min_version": "0.1.0", @@ -73,6 +74,8 @@ async def test_setup_provide_implementation(hass): "accepts_new_authorizations": False, }, ], + ), patch( + "homeassistant.components.cloud.account_link.HA_VERSION", "2022.9.0.dev20220817" ): assert ( await config_entry_oauth2_flow.async_get_implementations( @@ -101,6 +104,10 @@ async def test_setup_provide_implementation(hass): await config_entry_oauth2_flow.async_get_implementations(hass, "legacy") ) + dev_implementations = await config_entry_oauth2_flow.async_get_implementations( + hass, "dev" + ) + assert "cloud" in implementations assert implementations["cloud"].domain == "cloud" assert implementations["cloud"].service == "test" @@ -111,6 +118,11 @@ async def test_setup_provide_implementation(hass): assert legacy_implementations["cloud"].service == "legacy" assert legacy_implementations["cloud"].hass is hass + assert "cloud" in dev_implementations + assert dev_implementations["cloud"].domain == "cloud" + assert dev_implementations["cloud"].service == "dev" + assert dev_implementations["cloud"].hass is hass + async def test_get_services_cached(hass): """Test that we cache services.""" From f0deaa33a0eaff647262924afa4fb292b9d5ba98 Mon Sep 17 00:00:00 2001 From: Yasser Saleemi Date: Thu, 18 Aug 2022 13:07:58 +0100 Subject: [PATCH 3442/3516] Include moonsighting calc for islamic_prayer_times (#75595) --- homeassistant/components/islamic_prayer_times/const.py | 2 +- homeassistant/components/islamic_prayer_times/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/islamic_prayer_times/const.py b/homeassistant/components/islamic_prayer_times/const.py index ee7512c2d7a..86f953cc856 100644 --- a/homeassistant/components/islamic_prayer_times/const.py +++ b/homeassistant/components/islamic_prayer_times/const.py @@ -15,7 +15,7 @@ SENSOR_TYPES = { CONF_CALC_METHOD = "calculation_method" -CALC_METHODS = ["isna", "karachi", "mwl", "makkah"] +CALC_METHODS = ["isna", "karachi", "mwl", "makkah", "moonsighting"] DEFAULT_CALC_METHOD = "isna" DATA_UPDATED = "Islamic_prayer_data_updated" diff --git a/homeassistant/components/islamic_prayer_times/manifest.json b/homeassistant/components/islamic_prayer_times/manifest.json index 455f3bab675..a065ca17ab4 100644 --- a/homeassistant/components/islamic_prayer_times/manifest.json +++ b/homeassistant/components/islamic_prayer_times/manifest.json @@ -2,7 +2,7 @@ "domain": "islamic_prayer_times", "name": "Islamic Prayer Times", "documentation": "https://www.home-assistant.io/integrations/islamic_prayer_times", - "requirements": ["prayer_times_calculator==0.0.5"], + "requirements": ["prayer_times_calculator==0.0.6"], "codeowners": ["@engrbm87"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/requirements_all.txt b/requirements_all.txt index 785428e4c9e..96dfac44800 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1283,7 +1283,7 @@ poolsense==0.0.8 praw==7.5.0 # homeassistant.components.islamic_prayer_times -prayer_times_calculator==0.0.5 +prayer_times_calculator==0.0.6 # homeassistant.components.progettihwsw progettihwsw==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b499659dc44..2db72263205 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -904,7 +904,7 @@ poolsense==0.0.8 praw==7.5.0 # homeassistant.components.islamic_prayer_times -prayer_times_calculator==0.0.5 +prayer_times_calculator==0.0.6 # homeassistant.components.progettihwsw progettihwsw==0.1.1 From 60c8d95a77959168c2c217a633479ccf5066cbcf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 18 Aug 2022 14:21:05 +0200 Subject: [PATCH 3443/3516] Remove white_value support from light (#76926) --- homeassistant/components/fibaro/__init__.py | 2 +- homeassistant/components/flux/switch.py | 2 - homeassistant/components/light/__init__.py | 51 ------- .../components/light/reproduce_state.py | 4 - homeassistant/components/light/services.yaml | 8 -- .../components/light/significant_change.py | 16 +-- .../components/mqtt/light/schema_json.py | 3 +- homeassistant/const.py | 1 - pylint/plugins/hass_enforce_type_hints.py | 5 - tests/components/group/test_light.py | 2 - tests/components/light/common.py | 9 -- tests/components/light/test_init.py | 126 ++---------------- .../components/light/test_reproduce_state.py | 16 +-- .../light/test_significant_change.py | 9 -- tests/components/mqtt/test_discovery.py | 2 + tests/components/switch/test_light.py | 1 - tests/components/switch_as_x/test_light.py | 2 - tests/components/tasmota/test_light.py | 36 +---- tests/components/template/test_light.py | 13 -- .../custom_components/test/light.py | 2 - 20 files changed, 19 insertions(+), 291 deletions(-) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 9431cd162bc..ece4d38d726 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -24,7 +24,6 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_URL, CONF_USERNAME, - CONF_WHITE_VALUE, Platform, ) from homeassistant.core import HomeAssistant @@ -45,6 +44,7 @@ CONF_DIMMING = "dimming" CONF_GATEWAYS = "gateways" CONF_PLUGINS = "plugins" CONF_RESET_COLOR = "reset_color" +CONF_WHITE_VALUE = "white_value" FIBARO_CONTROLLER = "fibaro_controller" FIBARO_DEVICES = "fibaro_devices" PLATFORMS = [ diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index 4b9f5ad5285..2d175702047 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -15,7 +15,6 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_RGB_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, VALID_TRANSITION, @@ -101,7 +100,6 @@ async def async_set_lights_xy(hass, lights, x_val, y_val, brightness, transition service_data[ATTR_XY_COLOR] = [x_val, y_val] if brightness is not None: service_data[ATTR_BRIGHTNESS] = brightness - service_data[ATTR_WHITE_VALUE] = brightness if transition is not None: service_data[ATTR_TRANSITION] = transition await hass.services.async_call(LIGHT_DOMAIN, SERVICE_TURN_ON, service_data) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 099f917bc46..33ec3119b95 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -59,7 +59,6 @@ SUPPORT_EFFECT = 4 SUPPORT_FLASH = 8 SUPPORT_COLOR = 16 # Deprecated, replaced by color modes SUPPORT_TRANSITION = 32 -SUPPORT_WHITE_VALUE = 128 # Deprecated, replaced by color modes # Color mode of the light ATTR_COLOR_MODE = "color_mode" @@ -202,7 +201,6 @@ ATTR_KELVIN = "kelvin" ATTR_MIN_MIREDS = "min_mireds" ATTR_MAX_MIREDS = "max_mireds" ATTR_COLOR_NAME = "color_name" -ATTR_WHITE_VALUE = "white_value" ATTR_WHITE = "white" # Brightness of the light, 0..255 or percentage @@ -274,7 +272,6 @@ LIGHT_TURN_ON_SCHEMA = { vol.Coerce(tuple), vol.ExactSequence((cv.small_float, cv.small_float)) ), vol.Exclusive(ATTR_WHITE, COLOR_GROUP): VALID_BRIGHTNESS, - ATTR_WHITE_VALUE: vol.All(vol.Coerce(int), vol.Range(min=0, max=255)), ATTR_FLASH: VALID_FLASH, ATTR_EFFECT: cv.string, } @@ -341,8 +338,6 @@ def filter_turn_on_params(light, params): params.pop(ATTR_FLASH, None) if not supported_features & LightEntityFeature.TRANSITION: params.pop(ATTR_TRANSITION, None) - if not supported_features & SUPPORT_WHITE_VALUE: - params.pop(ATTR_WHITE_VALUE, None) supported_color_modes = ( light._light_internal_supported_color_modes # pylint:disable=protected-access @@ -421,16 +416,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: light._light_internal_supported_color_modes # pylint: disable=protected-access ) supported_color_modes = light.supported_color_modes - # Backwards compatibility: if an RGBWW color is specified, convert to RGB + W - # for legacy lights - if ATTR_RGBW_COLOR in params: - if ( - ColorMode.RGBW in legacy_supported_color_modes - and not supported_color_modes - ): - rgbw_color = params.pop(ATTR_RGBW_COLOR) - params[ATTR_RGB_COLOR] = rgbw_color[0:3] - params[ATTR_WHITE_VALUE] = rgbw_color[3] # If a color temperature is specified, emulate it if not supported by the light if ATTR_COLOR_TEMP in params: @@ -544,9 +529,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: params[ATTR_WHITE] = params.pop(ATTR_BRIGHTNESS, params[ATTR_WHITE]) # Remove deprecated white value if the light supports color mode - if supported_color_modes: - params.pop(ATTR_WHITE_VALUE, None) - if params.get(ATTR_BRIGHTNESS) == 0 or params.get(ATTR_WHITE) == 0: await async_handle_light_off_service(light, call) else: @@ -786,12 +768,6 @@ class LightEntity(ToggleEntity): # Add warning in 2021.6, remove in 2021.10 supported = self._light_internal_supported_color_modes - if ( - ColorMode.RGBW in supported - and self.white_value is not None - and self.hs_color is not None - ): - return ColorMode.RGBW if ColorMode.HS in supported and self.hs_color is not None: return ColorMode.HS if ColorMode.COLOR_TEMP in supported and self.color_temp is not None: @@ -828,19 +804,6 @@ class LightEntity(ToggleEntity): def _light_internal_rgbw_color(self) -> tuple[int, int, int, int] | None: """Return the rgbw color value [int, int, int, int].""" rgbw_color = self.rgbw_color - if ( - rgbw_color is None - and self.hs_color is not None - and self.white_value is not None - ): - # Backwards compatibility for rgbw_color added in 2021.4 - # Add warning in 2021.6, remove in 2021.10 - r, g, b = color_util.color_hs_to_RGB( # pylint: disable=invalid-name - *self.hs_color - ) - w = self.white_value # pylint: disable=invalid-name - rgbw_color = (r, g, b, w) - return rgbw_color @property @@ -867,11 +830,6 @@ class LightEntity(ToggleEntity): # https://developers.meethue.com/documentation/core-concepts return self._attr_max_mireds - @property - def white_value(self) -> int | None: - """Return the white value of this light between 0..255.""" - return None - @property def effect_list(self) -> list[str] | None: """Return the list of supported effects.""" @@ -982,13 +940,6 @@ class LightEntity(ToggleEntity): # Add warning in 2021.6, remove in 2021.10 data[ATTR_COLOR_TEMP] = self.color_temp - if supported_features & SUPPORT_WHITE_VALUE and not self.supported_color_modes: - # Backwards compatibility - # Add warning in 2021.6, remove in 2021.10 - data[ATTR_WHITE_VALUE] = self.white_value - if self.hs_color is not None: - data.update(self._light_internal_convert_color(ColorMode.HS)) - if supported_features & LightEntityFeature.EFFECT: data[ATTR_EFFECT] = self.effect @@ -1009,8 +960,6 @@ class LightEntity(ToggleEntity): supported_color_modes.add(ColorMode.COLOR_TEMP) if supported_features & SUPPORT_COLOR: supported_color_modes.add(ColorMode.HS) - if supported_features & SUPPORT_WHITE_VALUE: - supported_color_modes.add(ColorMode.RGBW) if supported_features & SUPPORT_BRIGHTNESS and not supported_color_modes: supported_color_modes = {ColorMode.BRIGHTNESS} diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index 5e60c616500..46adcc1fa2e 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -31,7 +31,6 @@ from . import ( ATTR_RGBWW_COLOR, ATTR_TRANSITION, ATTR_WHITE, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN, ColorMode, @@ -46,7 +45,6 @@ ATTR_GROUP = [ ATTR_BRIGHTNESS_PCT, ATTR_EFFECT, ATTR_FLASH, - ATTR_WHITE_VALUE, ATTR_TRANSITION, ] @@ -157,8 +155,6 @@ async def _async_reproduce_state( state.attributes.get(ATTR_COLOR_MODE, ColorMode.UNKNOWN) != ColorMode.UNKNOWN ): - # Remove deprecated white value if we got a valid color mode - service_data.pop(ATTR_WHITE_VALUE, None) color_mode = state.attributes[ATTR_COLOR_MODE] if color_mode_attr := COLOR_MODE_TO_ATTRIBUTE.get(color_mode): if color_mode_attr.state_attr not in state.attributes: diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 06349c72035..b7843a2f0ec 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -531,14 +531,6 @@ toggle: max: 6500 step: 100 unit_of_measurement: K - white_value: - name: White level - description: Number indicating level of white. - advanced: true - selector: - number: - min: 0 - max: 255 brightness: name: Brightness value description: Number indicating brightness, where 0 turns the light diff --git a/homeassistant/components/light/significant_change.py b/homeassistant/components/light/significant_change.py index 79f447f5794..dc8f711b579 100644 --- a/homeassistant/components/light/significant_change.py +++ b/homeassistant/components/light/significant_change.py @@ -6,13 +6,7 @@ from typing import Any from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.significant_change import check_absolute_change -from . import ( - ATTR_BRIGHTNESS, - ATTR_COLOR_TEMP, - ATTR_EFFECT, - ATTR_HS_COLOR, - ATTR_WHITE_VALUE, -) +from . import ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR @callback @@ -56,12 +50,4 @@ def async_check_significant_change( ): return True - if check_absolute_change( - # Range 0..255 - old_attrs.get(ATTR_WHITE_VALUE), - new_attrs.get(ATTR_WHITE_VALUE), - 5, - ): - return True - return False diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 910d48f750d..85a0bc335cd 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -38,7 +38,6 @@ from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_RGB, - CONF_WHITE_VALUE, CONF_XY, STATE_ON, ) @@ -97,6 +96,8 @@ CONF_FLASH_TIME_SHORT = "flash_time_short" CONF_MAX_MIREDS = "max_mireds" CONF_MIN_MIREDS = "min_mireds" +CONF_WHITE_VALUE = "white_value" + def valid_color_configuration(config): """Test color_mode is not combined with deprecated config.""" diff --git a/homeassistant/const.py b/homeassistant/const.py index 7542ce0d77e..750b014e0da 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -262,7 +262,6 @@ CONF_WHILE: Final = "while" CONF_WHITELIST: Final = "whitelist" CONF_ALLOWLIST_EXTERNAL_DIRS: Final = "allowlist_external_dirs" LEGACY_CONF_WHITELIST_EXTERNAL_DIRS: Final = "whitelist_external_dirs" -CONF_WHITE_VALUE: Final = "white_value" CONF_XY: Final = "xy" CONF_ZONE: Final = "zone" diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 1849d38fbc3..0791ebcd9b2 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -1378,10 +1378,6 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { function_name="max_mireds", return_type="int", ), - TypeHintMatch( - function_name="white_value", - return_type=["int", None], - ), TypeHintMatch( function_name="effect_list", return_type=["list[str]", None], @@ -1421,7 +1417,6 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { "transition": "float | None", "xy_color": "tuple[float, float] | None", "white": "int | None", - "white_value": "int | None", }, kwargs_type="Any", return_type=None, diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 5329d3074b4..be50f29fe5c 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -25,7 +25,6 @@ from homeassistant.components.light import ( ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_WHITE, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, SERVICE_TOGGLE, @@ -78,7 +77,6 @@ async def test_default_state(hass): assert state.attributes.get(ATTR_BRIGHTNESS) is None assert state.attributes.get(ATTR_HS_COLOR) is None assert state.attributes.get(ATTR_COLOR_TEMP) is None - assert state.attributes.get(ATTR_WHITE_VALUE) is None assert state.attributes.get(ATTR_EFFECT_LIST) is None assert state.attributes.get(ATTR_EFFECT) is None diff --git a/tests/components/light/common.py b/tests/components/light/common.py index 0c16e0f2703..4f83ffacbdc 100644 --- a/tests/components/light/common.py +++ b/tests/components/light/common.py @@ -18,7 +18,6 @@ from homeassistant.components.light import ( ATTR_RGBWW_COLOR, ATTR_TRANSITION, ATTR_WHITE, - ATTR_WHITE_VALUE, ATTR_XY_COLOR, DOMAIN, ) @@ -46,7 +45,6 @@ def turn_on( hs_color=None, color_temp=None, kelvin=None, - white_value=None, profile=None, flash=None, effect=None, @@ -68,7 +66,6 @@ def turn_on( hs_color, color_temp, kelvin, - white_value, profile, flash, effect, @@ -90,7 +87,6 @@ async def async_turn_on( hs_color=None, color_temp=None, kelvin=None, - white_value=None, profile=None, flash=None, effect=None, @@ -113,7 +109,6 @@ async def async_turn_on( (ATTR_HS_COLOR, hs_color), (ATTR_COLOR_TEMP, color_temp), (ATTR_KELVIN, kelvin), - (ATTR_WHITE_VALUE, white_value), (ATTR_FLASH, flash), (ATTR_EFFECT, effect), (ATTR_COLOR_NAME, color_name), @@ -158,7 +153,6 @@ def toggle( hs_color=None, color_temp=None, kelvin=None, - white_value=None, profile=None, flash=None, effect=None, @@ -177,7 +171,6 @@ def toggle( hs_color, color_temp, kelvin, - white_value, profile, flash, effect, @@ -196,7 +189,6 @@ async def async_toggle( hs_color=None, color_temp=None, kelvin=None, - white_value=None, profile=None, flash=None, effect=None, @@ -216,7 +208,6 @@ async def async_toggle( (ATTR_HS_COLOR, hs_color), (ATTR_COLOR_TEMP, color_temp), (ATTR_KELVIN, kelvin), - (ATTR_WHITE_VALUE, white_value), (ATTR_FLASH, flash), (ATTR_EFFECT, effect), (ATTR_COLOR_NAME, color_name), diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index ae9b00baeaa..1f21981340f 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -48,7 +48,6 @@ async def test_methods(hass): light.ATTR_XY_COLOR: "xy_color_val", light.ATTR_PROFILE: "profile_val", light.ATTR_COLOR_NAME: "color_name_val", - light.ATTR_WHITE_VALUE: "white_val", }, blocking=True, ) @@ -65,7 +64,6 @@ async def test_methods(hass): assert call.data.get(light.ATTR_XY_COLOR) == "xy_color_val" assert call.data.get(light.ATTR_PROFILE) == "profile_val" assert call.data.get(light.ATTR_COLOR_NAME) == "color_name_val" - assert call.data.get(light.ATTR_WHITE_VALUE) == "white_val" # Test turn_off turn_off_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_OFF) @@ -125,7 +123,6 @@ async def test_services(hass, mock_light_profiles, enable_custom_integrations): light.SUPPORT_COLOR | light.LightEntityFeature.EFFECT | light.LightEntityFeature.TRANSITION - | light.SUPPORT_WHITE_VALUE ) ent3.supported_features = ( light.LightEntityFeature.FLASH | light.LightEntityFeature.TRANSITION @@ -220,7 +217,6 @@ async def test_services(hass, mock_light_profiles, enable_custom_integrations): ATTR_ENTITY_ID: ent2.entity_id, light.ATTR_EFFECT: "fun_effect", light.ATTR_RGB_COLOR: (255, 255, 255), - light.ATTR_WHITE_VALUE: 255, }, blocking=True, ) @@ -246,7 +242,6 @@ async def test_services(hass, mock_light_profiles, enable_custom_integrations): assert data == { light.ATTR_EFFECT: "fun_effect", light.ATTR_HS_COLOR: (0, 0), - light.ATTR_WHITE_VALUE: 255, } _, data = ent3.last_call("turn_on") @@ -271,7 +266,6 @@ async def test_services(hass, mock_light_profiles, enable_custom_integrations): ATTR_ENTITY_ID: ent2.entity_id, light.ATTR_BRIGHTNESS: 0, light.ATTR_RGB_COLOR: (255, 255, 255), - light.ATTR_WHITE_VALUE: 0, }, blocking=True, ) @@ -427,13 +421,6 @@ async def test_services(hass, mock_light_profiles, enable_custom_integrations): }, blocking=True, ) - with pytest.raises(vol.MultipleInvalid): - await hass.services.async_call( - light.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ent2.entity_id, light.ATTR_WHITE_VALUE: "high"}, - blocking=True, - ) _, data = ent1.last_call("turn_on") assert data == {} @@ -1104,8 +1091,6 @@ async def test_light_backwards_compatibility_supported_color_modes( platform.ENTITIES.append(platform.MockLight("Test_2", light_state)) platform.ENTITIES.append(platform.MockLight("Test_3", light_state)) platform.ENTITIES.append(platform.MockLight("Test_4", light_state)) - platform.ENTITIES.append(platform.MockLight("Test_5", light_state)) - platform.ENTITIES.append(platform.MockLight("Test_6", light_state)) entity0 = platform.ENTITIES[0] @@ -1120,22 +1105,9 @@ async def test_light_backwards_compatibility_supported_color_modes( entity4 = platform.ENTITIES[4] entity4.supported_features = ( - light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_WHITE_VALUE - ) - - entity5 = platform.ENTITIES[5] - entity5.supported_features = ( light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP ) - entity6 = platform.ENTITIES[6] - entity6.supported_features = ( - light.SUPPORT_BRIGHTNESS - | light.SUPPORT_COLOR - | light.SUPPORT_COLOR_TEMP - | light.SUPPORT_WHITE_VALUE - ) - assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1168,16 +1140,6 @@ async def test_light_backwards_compatibility_supported_color_modes( assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN state = hass.states.get(entity4.entity_id) - assert state.attributes["supported_color_modes"] == [ - light.ColorMode.HS, - light.ColorMode.RGBW, - ] - if light_state == STATE_OFF: - assert "color_mode" not in state.attributes - else: - assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN - - state = hass.states.get(entity5.entity_id) assert state.attributes["supported_color_modes"] == [ light.ColorMode.COLOR_TEMP, light.ColorMode.HS, @@ -1187,17 +1149,6 @@ async def test_light_backwards_compatibility_supported_color_modes( else: assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN - state = hass.states.get(entity6.entity_id) - assert state.attributes["supported_color_modes"] == [ - light.ColorMode.COLOR_TEMP, - light.ColorMode.HS, - light.ColorMode.RGBW, - ] - if light_state == STATE_OFF: - assert "color_mode" not in state.attributes - else: - assert state.attributes["color_mode"] == light.ColorMode.UNKNOWN - async def test_light_backwards_compatibility_color_mode( hass, enable_custom_integrations @@ -1211,8 +1162,6 @@ async def test_light_backwards_compatibility_color_mode( platform.ENTITIES.append(platform.MockLight("Test_2", STATE_ON)) platform.ENTITIES.append(platform.MockLight("Test_3", STATE_ON)) platform.ENTITIES.append(platform.MockLight("Test_4", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_5", STATE_ON)) - platform.ENTITIES.append(platform.MockLight("Test_6", STATE_ON)) entity0 = platform.ENTITIES[0] @@ -1230,17 +1179,10 @@ async def test_light_backwards_compatibility_color_mode( entity4 = platform.ENTITIES[4] entity4.supported_features = ( - light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_WHITE_VALUE - ) - entity4.hs_color = (240, 100) - entity4.white_value = 100 - - entity5 = platform.ENTITIES[5] - entity5.supported_features = ( light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP ) - entity5.hs_color = (240, 100) - entity5.color_temp = 100 + entity4.hs_color = (240, 100) + entity4.color_temp = 100 assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() @@ -1265,13 +1207,6 @@ async def test_light_backwards_compatibility_color_mode( assert state.attributes["color_mode"] == light.ColorMode.HS state = hass.states.get(entity4.entity_id) - assert state.attributes["supported_color_modes"] == [ - light.ColorMode.HS, - light.ColorMode.RGBW, - ] - assert state.attributes["color_mode"] == light.ColorMode.RGBW - - state = hass.states.get(entity5.entity_id) assert state.attributes["supported_color_modes"] == [ light.ColorMode.COLOR_TEMP, light.ColorMode.HS, @@ -1281,38 +1216,26 @@ async def test_light_backwards_compatibility_color_mode( async def test_light_service_call_rgbw(hass, enable_custom_integrations): - """Test backwards compatibility for rgbw functionality in service calls.""" + """Test rgbw functionality in service calls.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) - platform.ENTITIES.append(platform.MockLight("Test_legacy_white_value", STATE_ON)) platform.ENTITIES.append(platform.MockLight("Test_rgbw", STATE_ON)) entity0 = platform.ENTITIES[0] - entity0.supported_features = ( - light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_WHITE_VALUE - ) - - entity1 = platform.ENTITIES[1] - entity1.supported_color_modes = {light.ColorMode.RGBW} + entity0.supported_color_modes = {light.ColorMode.RGBW} assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert state.attributes["supported_color_modes"] == [ - light.ColorMode.HS, - light.ColorMode.RGBW, - ] - - state = hass.states.get(entity1.entity_id) assert state.attributes["supported_color_modes"] == [light.ColorMode.RGBW] await hass.services.async_call( "light", "turn_on", { - "entity_id": [entity0.entity_id, entity1.entity_id], + "entity_id": [entity0.entity_id, entity0.entity_id], "brightness_pct": 100, "rgbw_color": (10, 20, 30, 40), }, @@ -1320,8 +1243,6 @@ async def test_light_service_call_rgbw(hass, enable_custom_integrations): ) _, data = entity0.last_call("turn_on") - assert data == {"brightness": 255, "hs_color": (210.0, 66.667), "white_value": 40} - _, data = entity1.last_call("turn_on") assert data == {"brightness": 255, "rgbw_color": (10, 20, 30, 40)} @@ -1330,47 +1251,21 @@ async def test_light_state_rgbw(hass, enable_custom_integrations): platform = getattr(hass.components, "test.light") platform.init(empty=True) - platform.ENTITIES.append(platform.MockLight("Test_legacy_white_value", STATE_ON)) platform.ENTITIES.append(platform.MockLight("Test_rgbw", STATE_ON)) entity0 = platform.ENTITIES[0] - legacy_supported_features = ( - light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_WHITE_VALUE - ) - entity0.supported_features = legacy_supported_features - entity0.hs_color = (210.0, 66.667) + entity0.supported_color_modes = {light.ColorMode.RGBW} + entity0.color_mode = light.ColorMode.RGBW + entity0.hs_color = "Invalid" # Should be ignored entity0.rgb_color = "Invalid" # Should be ignored + entity0.rgbw_color = (1, 2, 3, 4) entity0.rgbww_color = "Invalid" # Should be ignored - entity0.white_value = 40 entity0.xy_color = "Invalid" # Should be ignored - entity1 = platform.ENTITIES[1] - entity1.supported_color_modes = {light.ColorMode.RGBW} - entity1.color_mode = light.ColorMode.RGBW - entity1.hs_color = "Invalid" # Should be ignored - entity1.rgb_color = "Invalid" # Should be ignored - entity1.rgbw_color = (1, 2, 3, 4) - entity1.rgbww_color = "Invalid" # Should be ignored - entity1.white_value = "Invalid" # Should be ignored - entity1.xy_color = "Invalid" # Should be ignored - assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert state.attributes == { - "color_mode": light.ColorMode.RGBW, - "friendly_name": "Test_legacy_white_value", - "supported_color_modes": [light.ColorMode.HS, light.ColorMode.RGBW], - "supported_features": legacy_supported_features, - "hs_color": (210.0, 66.667), - "rgb_color": (84, 169, 255), - "rgbw_color": (84, 169, 255, 40), - "white_value": 40, - "xy_color": (0.173, 0.207), - } - - state = hass.states.get(entity1.entity_id) assert state.attributes == { "color_mode": light.ColorMode.RGBW, "friendly_name": "Test_rgbw", @@ -1397,7 +1292,6 @@ async def test_light_state_rgbww(hass, enable_custom_integrations): entity0.rgb_color = "Invalid" # Should be ignored entity0.rgbw_color = "Invalid" # Should be ignored entity0.rgbww_color = (1, 2, 3, 4, 5) - entity0.white_value = "Invalid" # Should be ignored entity0.xy_color = "Invalid" # Should be ignored assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) @@ -2264,7 +2158,6 @@ async def test_services_filter_parameters( light.ATTR_EFFECT: "fun_effect", light.ATTR_FLASH: "short", light.ATTR_TRANSITION: 10, - light.ATTR_WHITE_VALUE: 0, }, blocking=True, ) @@ -2353,7 +2246,6 @@ async def test_services_filter_parameters( light.ATTR_EFFECT: "fun_effect", light.ATTR_FLASH: "short", light.ATTR_TRANSITION: 10, - light.ATTR_WHITE_VALUE: 0, }, blocking=True, ) diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py index 6f35d083d56..1f69e94658d 100644 --- a/tests/components/light/test_reproduce_state.py +++ b/tests/components/light/test_reproduce_state.py @@ -9,7 +9,6 @@ from homeassistant.helpers.state import async_reproduce_state from tests.common import async_mock_service VALID_BRIGHTNESS = {"brightness": 180} -VALID_WHITE_VALUE = {"white_value": 200} VALID_FLASH = {"flash": "short"} VALID_EFFECT = {"effect": "random"} VALID_TRANSITION = {"transition": 15} @@ -28,7 +27,6 @@ async def test_reproducing_states(hass, caplog): """Test reproducing Light states.""" hass.states.async_set("light.entity_off", "off", {}) hass.states.async_set("light.entity_bright", "on", VALID_BRIGHTNESS) - hass.states.async_set("light.entity_white", "on", VALID_WHITE_VALUE) hass.states.async_set("light.entity_flash", "on", VALID_FLASH) hass.states.async_set("light.entity_effect", "on", VALID_EFFECT) hass.states.async_set("light.entity_trans", "on", VALID_TRANSITION) @@ -49,7 +47,6 @@ async def test_reproducing_states(hass, caplog): [ State("light.entity_off", "off"), State("light.entity_bright", "on", VALID_BRIGHTNESS), - State("light.entity_white", "on", VALID_WHITE_VALUE), State("light.entity_flash", "on", VALID_FLASH), State("light.entity_effect", "on", VALID_EFFECT), State("light.entity_trans", "on", VALID_TRANSITION), @@ -79,8 +76,7 @@ async def test_reproducing_states(hass, caplog): [ State("light.entity_xy", "off"), State("light.entity_off", "on", VALID_BRIGHTNESS), - State("light.entity_bright", "on", VALID_WHITE_VALUE), - State("light.entity_white", "on", VALID_FLASH), + State("light.entity_bright", "on", VALID_FLASH), State("light.entity_flash", "on", VALID_EFFECT), State("light.entity_effect", "on", VALID_TRANSITION), State("light.entity_trans", "on", VALID_COLOR_NAME), @@ -93,7 +89,7 @@ async def test_reproducing_states(hass, caplog): ], ) - assert len(turn_on_calls) == 12 + assert len(turn_on_calls) == 11 expected_calls = [] @@ -101,14 +97,10 @@ async def test_reproducing_states(hass, caplog): expected_off["entity_id"] = "light.entity_off" expected_calls.append(expected_off) - expected_bright = dict(VALID_WHITE_VALUE) + expected_bright = dict(VALID_FLASH) expected_bright["entity_id"] = "light.entity_bright" expected_calls.append(expected_bright) - expected_white = dict(VALID_FLASH) - expected_white["entity_id"] = "light.entity_white" - expected_calls.append(expected_white) - expected_flash = dict(VALID_EFFECT) expected_flash["entity_id"] = "light.entity_flash" expected_calls.append(expected_flash) @@ -181,7 +173,6 @@ async def test_filter_color_modes(hass, caplog, color_mode): """Test filtering of parameters according to color mode.""" hass.states.async_set("light.entity", "off", {}) all_colors = { - **VALID_WHITE_VALUE, **VALID_COLOR_NAME, **VALID_COLOR_TEMP, **VALID_HS_COLOR, @@ -210,7 +201,6 @@ async def test_filter_color_modes(hass, caplog, color_mode): light.ColorMode.UNKNOWN: { **VALID_BRIGHTNESS, **VALID_HS_COLOR, - **VALID_WHITE_VALUE, }, light.ColorMode.WHITE: { **VALID_BRIGHTNESS, diff --git a/tests/components/light/test_significant_change.py b/tests/components/light/test_significant_change.py index f935ec8df1a..1ce477de4c6 100644 --- a/tests/components/light/test_significant_change.py +++ b/tests/components/light/test_significant_change.py @@ -4,7 +4,6 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, - ATTR_WHITE_VALUE, ) from homeassistant.components.light.significant_change import ( async_check_significant_change, @@ -32,14 +31,6 @@ async def test_significant_change(): None, "on", {ATTR_COLOR_TEMP: 60}, "on", {ATTR_COLOR_TEMP: 65} ) - # White value - assert not async_check_significant_change( - None, "on", {ATTR_WHITE_VALUE: 60}, "on", {ATTR_WHITE_VALUE: 64} - ) - assert async_check_significant_change( - None, "on", {ATTR_WHITE_VALUE: 60}, "on", {ATTR_WHITE_VALUE: 65} - ) - # Effect for eff1, eff2, expected in ( (None, None, False), diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index e4c6f44883a..2bc330f8495 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1181,6 +1181,8 @@ ABBREVIATIONS_WHITE_LIST = [ "CONF_SCHEMA", "CONF_SWING_MODE_LIST", "CONF_TEMP_STEP", + # Removed + "CONF_WHITE_VALUE", ] diff --git a/tests/components/switch/test_light.py b/tests/components/switch/test_light.py index b37b6cf4471..f3d5cac9238 100644 --- a/tests/components/switch/test_light.py +++ b/tests/components/switch/test_light.py @@ -32,7 +32,6 @@ async def test_default_state(hass): assert state.attributes.get("brightness") is None assert state.attributes.get("hs_color") is None assert state.attributes.get("color_temp") is None - assert state.attributes.get("white_value") is None assert state.attributes.get("effect_list") is None assert state.attributes.get("effect") is None assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [ColorMode.ONOFF] diff --git a/tests/components/switch_as_x/test_light.py b/tests/components/switch_as_x/test_light.py index 7026a6dea76..b5976f17841 100644 --- a/tests/components/switch_as_x/test_light.py +++ b/tests/components/switch_as_x/test_light.py @@ -7,7 +7,6 @@ from homeassistant.components.light import ( ATTR_EFFECT_LIST, ATTR_HS_COLOR, ATTR_SUPPORTED_COLOR_MODES, - ATTR_WHITE_VALUE, DOMAIN as LIGHT_DOMAIN, ColorMode, ) @@ -50,7 +49,6 @@ async def test_default_state(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_BRIGHTNESS) is None assert state.attributes.get(ATTR_HS_COLOR) is None assert state.attributes.get(ATTR_COLOR_TEMP) is None - assert state.attributes.get(ATTR_WHITE_VALUE) is None assert state.attributes.get(ATTR_EFFECT_LIST) is None assert state.attributes.get(ATTR_EFFECT) is None assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [ColorMode.ONOFF] diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 1c51975b1f8..0b89d91831a 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -574,7 +574,6 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert "white_value" not in state.attributes # Setting white > 0 should clear the color assert "rgb_color" not in state.attributes assert state.attributes.get("color_mode") == "color_temp" @@ -593,7 +592,6 @@ async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): state = hass.states.get("light.test") assert state.state == STATE_ON # Setting white to 0 should clear the color_temp - assert "white_value" not in state.attributes assert "color_temp" not in state.attributes assert state.attributes.get("hs_color") == (30, 100) assert state.attributes.get("color_mode") == "hs" @@ -686,7 +684,6 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert "white_value" not in state.attributes # Setting white > 0 should clear the color assert "rgb_color" not in state.attributes assert state.attributes.get("color_mode") == "color_temp" @@ -704,8 +701,7 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm ) state = hass.states.get("light.test") assert state.state == STATE_ON - # Setting white to 0 should clear the white_value and color_temp - assert not state.attributes.get("white_value") + # Setting white to 0 should clear the color_temp assert not state.attributes.get("color_temp") assert state.attributes.get("color_mode") == "hs" @@ -904,16 +900,6 @@ async def test_sending_mqtt_commands_rgbw_legacy(hass, mqtt_mock, setup_tasmota) ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", white_value=128) - # white_value should be ignored - mqtt_mock.async_publish.assert_called_once_with( - "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON", - 0, - False, - ) - mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", effect="Random") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", @@ -1011,16 +997,6 @@ async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", white_value=128) - # white_value should be ignored - mqtt_mock.async_publish.assert_called_once_with( - "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON", - 0, - False, - ) - mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", effect="Random") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", @@ -1096,16 +1072,6 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", white_value=128) - # white_value should be ignored - mqtt_mock.async_publish.assert_called_once_with( - "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Power1 ON", - 0, - False, - ) - mqtt_mock.async_publish.reset_mock() - await common.async_turn_on(hass, "light.test", effect="Random") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index 9b0ad613120..1199a318626 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -90,19 +90,6 @@ OPTIMISTIC_HS_COLOR_LIGHT_CONFIG = { } -OPTIMISTIC_WHITE_VALUE_LIGHT_CONFIG = { - **OPTIMISTIC_ON_OFF_LIGHT_CONFIG, - "set_white_value": { - "service": "test.automation", - "data_template": { - "action": "set_white_value", - "caller": "{{ this.entity_id }}", - "white_value": "{{white_value}}", - }, - }, -} - - async def async_setup_light(hass, count, light_config): """Do setup of light integration.""" config = {"light": {"platform": "template", "lights": light_config}} diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index 1b33b1cafbf..a4b5a182edc 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -49,7 +49,6 @@ class MockLight(MockToggleEntity, LightEntity): rgbw_color = None rgbww_color = None xy_color = None - white_value = None def turn_on(self, **kwargs): """Turn the entity on.""" @@ -63,7 +62,6 @@ class MockLight(MockToggleEntity, LightEntity): "rgbw_color", "rgbww_color", "color_temp", - "white_value", ]: setattr(self, key, value) if key == "white": From c7301a449bc1b121dac2e1623768964ca14fa685 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 14:48:23 +0200 Subject: [PATCH 3444/3516] Add switch checks to pylint plugin (#76909) --- pylint/plugins/hass_enforce_type_hints.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 0791ebcd9b2..19a69f8808f 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -2036,6 +2036,25 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "switch": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="ToggleEntity", + matches=_TOGGLE_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="SwitchEntity", + matches=[ + TypeHintMatch( + function_name="device_class", + return_type=["SwitchDeviceClass", "str", None], + ), + ], + ), + ], "update": [ ClassTypeHintMatch( base_class="Entity", From 24f1287bf93abd15a8c16fcf06d2791dbc02bec8 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 15:39:56 +0200 Subject: [PATCH 3445/3516] Improve type hints in homeassistant scene (#76930) --- .../components/homeassistant/scene.py | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 21c364ba65b..6cf480b19a3 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -1,8 +1,9 @@ """Allow users to set and activate scenes.""" from __future__ import annotations +from collections.abc import Mapping, ValuesView import logging -from typing import Any, NamedTuple +from typing import Any, NamedTuple, cast import voluptuous as vol @@ -34,16 +35,16 @@ from homeassistant.helpers import ( config_validation as cv, entity_platform, ) -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddEntitiesCallback, EntityPlatform from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.state import async_reproduce_state from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.loader import async_get_integration -def _convert_states(states): +def _convert_states(states: dict[str, Any]) -> dict[str, State]: """Convert state definitions to State objects.""" - result = {} + result: dict[str, State] = {} for entity_id, info in states.items(): entity_id = cv.entity_id(entity_id) @@ -68,7 +69,7 @@ def _convert_states(states): return result -def _ensure_no_intersection(value): +def _ensure_no_intersection(value: dict[str, Any]) -> dict[str, Any]: """Validate that entities and snapshot_entities do not overlap.""" if ( CONF_SNAPSHOT not in value @@ -143,11 +144,12 @@ def scenes_with_entity(hass: HomeAssistant, entity_id: str) -> list[str]: if DATA_PLATFORM not in hass.data: return [] - platform = hass.data[DATA_PLATFORM] + platform: EntityPlatform = hass.data[DATA_PLATFORM] + scene_entities = cast(ValuesView[HomeAssistantScene], platform.entities.values()) return [ scene_entity.entity_id - for scene_entity in platform.entities.values() + for scene_entity in scene_entities if entity_id in scene_entity.scene_config.states ] @@ -158,12 +160,12 @@ def entities_in_scene(hass: HomeAssistant, entity_id: str) -> list[str]: if DATA_PLATFORM not in hass.data: return [] - platform = hass.data[DATA_PLATFORM] + platform: EntityPlatform = hass.data[DATA_PLATFORM] if (entity := platform.entities.get(entity_id)) is None: return [] - return list(entity.scene_config.states) + return list(cast(HomeAssistantScene, entity).scene_config.states) async def async_setup_platform( @@ -270,9 +272,12 @@ async def async_setup_platform( ) -def _process_scenes_config(hass, async_add_entities, config): +def _process_scenes_config( + hass, async_add_entities: AddEntitiesCallback, config: dict[str, Any] +) -> None: """Process multiple scenes and add them.""" # Check empty list + scene_config: list[dict[str, Any]] if not (scene_config := config[STATES]): return @@ -293,31 +298,33 @@ def _process_scenes_config(hass, async_add_entities, config): class HomeAssistantScene(Scene): """A scene is a group of entities and the states we want them to be.""" - def __init__(self, hass, scene_config, from_service=False): + def __init__( + self, hass: HomeAssistant, scene_config: SceneConfig, from_service: bool = False + ) -> None: """Initialize the scene.""" self.hass = hass self.scene_config = scene_config self.from_service = from_service @property - def name(self): + def name(self) -> str: """Return the name of the scene.""" return self.scene_config.name @property - def icon(self): + def icon(self) -> str | None: """Return the icon of the scene.""" return self.scene_config.icon @property - def unique_id(self): + def unique_id(self) -> str | None: """Return unique ID.""" return self.scene_config.id @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> Mapping[str, Any]: """Return the scene state attributes.""" - attributes = {ATTR_ENTITY_ID: list(self.scene_config.states)} + attributes: dict[str, Any] = {ATTR_ENTITY_ID: list(self.scene_config.states)} if (unique_id := self.unique_id) is not None: attributes[CONF_ID] = unique_id return attributes From 65eb1584f765dcc2ec502bd8a9fa8d2f23d47cfd Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 15:56:52 +0200 Subject: [PATCH 3446/3516] Improve entity type hints [a] (#76986) --- homeassistant/components/adax/climate.py | 4 +- homeassistant/components/ads/binary_sensor.py | 2 +- homeassistant/components/ads/sensor.py | 2 +- homeassistant/components/ads/switch.py | 8 ++-- .../components/advantage_air/climate.py | 11 ++--- .../components/advantage_air/switch.py | 6 ++- homeassistant/components/agent_dvr/camera.py | 12 +++--- homeassistant/components/airtouch4/climate.py | 16 ++++---- .../components/alarmdecoder/binary_sensor.py | 2 +- .../components/alarmdecoder/sensor.py | 2 +- .../components/ambiclimate/climate.py | 2 +- .../components/androidtv/media_player.py | 36 ++++++++-------- .../components/anel_pwrctrl/switch.py | 7 ++-- .../components/apcupsd/binary_sensor.py | 2 +- homeassistant/components/apcupsd/sensor.py | 2 +- .../components/apple_tv/media_player.py | 41 +++++++++++-------- homeassistant/components/apple_tv/remote.py | 8 ++-- homeassistant/components/aqualogic/sensor.py | 2 +- homeassistant/components/aqualogic/switch.py | 8 ++-- .../components/aquostv/media_player.py | 22 +++++----- .../components/arcam_fmj/media_player.py | 31 ++++++++------ .../components/arest/binary_sensor.py | 4 +- homeassistant/components/arest/sensor.py | 2 +- homeassistant/components/arest/switch.py | 13 +++--- homeassistant/components/atag/climate.py | 6 ++- homeassistant/components/atag/water_heater.py | 8 ++-- homeassistant/components/aten_pe/switch.py | 7 ++-- homeassistant/components/atome/sensor.py | 2 +- .../components/august/binary_sensor.py | 2 +- homeassistant/components/august/camera.py | 4 +- .../components/aurora_abb_powerone/sensor.py | 2 +- 31 files changed, 152 insertions(+), 124 deletions(-) diff --git a/homeassistant/components/adax/climate.py b/homeassistant/components/adax/climate.py index 258e50f1047..85532d9aadb 100644 --- a/homeassistant/components/adax/climate.py +++ b/homeassistant/components/adax/climate.py @@ -80,7 +80,7 @@ class AdaxDevice(ClimateEntity): manufacturer="Adax", ) - async def async_set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set hvac mode.""" if hvac_mode == HVACMode.HEAT: temperature = max(self.min_temp, self.target_temperature or self.min_temp) @@ -140,7 +140,7 @@ class LocalAdaxDevice(ClimateEntity): manufacturer="Adax", ) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return diff --git a/homeassistant/components/ads/binary_sensor.py b/homeassistant/components/ads/binary_sensor.py index 3ec1e8b1da4..b20ef010f1f 100644 --- a/homeassistant/components/ads/binary_sensor.py +++ b/homeassistant/components/ads/binary_sensor.py @@ -53,7 +53,7 @@ class AdsBinarySensor(AdsEntity, BinarySensorEntity): super().__init__(ads_hub, name, ads_var) self._attr_device_class = device_class or BinarySensorDeviceClass.MOVING - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register device notification.""" await self.async_initialize_device(self._ads_var, pyads.PLCTYPE_BOOL) diff --git a/homeassistant/components/ads/sensor.py b/homeassistant/components/ads/sensor.py index 7b6b1304987..172f8ee70df 100644 --- a/homeassistant/components/ads/sensor.py +++ b/homeassistant/components/ads/sensor.py @@ -70,7 +70,7 @@ class AdsSensor(AdsEntity, SensorEntity): self._ads_type = ads_type self._factor = factor - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register device notification.""" await self.async_initialize_device( self._ads_var, diff --git a/homeassistant/components/ads/switch.py b/homeassistant/components/ads/switch.py index d5ed25bad20..3f597fb9f5c 100644 --- a/homeassistant/components/ads/switch.py +++ b/homeassistant/components/ads/switch.py @@ -1,6 +1,8 @@ """Support for ADS switch platform.""" from __future__ import annotations +from typing import Any + import pyads import voluptuous as vol @@ -41,7 +43,7 @@ def setup_platform( class AdsSwitch(AdsEntity, SwitchEntity): """Representation of an ADS switch device.""" - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register device notification.""" await self.async_initialize_device(self._ads_var, pyads.PLCTYPE_BOOL) @@ -50,10 +52,10 @@ class AdsSwitch(AdsEntity, SwitchEntity): """Return True if the entity is on.""" return self._state_dict[STATE_KEY_STATE] - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self._ads_hub.write_by_name(self._ads_var, True, pyads.PLCTYPE_BOOL) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self._ads_hub.write_by_name(self._ads_var, False, pyads.PLCTYPE_BOOL) diff --git a/homeassistant/components/advantage_air/climate.py b/homeassistant/components/advantage_air/climate.py index e69dc06dd7d..d889bf35642 100644 --- a/homeassistant/components/advantage_air/climate.py +++ b/homeassistant/components/advantage_air/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -114,7 +115,7 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity): """Return the current fan modes.""" return ADVANTAGE_AIR_FAN_MODES.get(self._ac["fan"]) - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the HVAC Mode and State.""" if hvac_mode == HVACMode.OFF: await self.async_change( @@ -132,13 +133,13 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity): } ) - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set the Fan Mode.""" await self.async_change( {self.ac_key: {"info": {"fan": HASS_FAN_MODES.get(fan_mode)}}} ) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set the Temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) await self.async_change({self.ac_key: {"info": {"setTemp": temp}}}) @@ -179,7 +180,7 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): """Return the target temperature.""" return self._zone["setTemp"] - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the HVAC Mode and State.""" if hvac_mode == HVACMode.OFF: await self.async_change( @@ -198,7 +199,7 @@ class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): } ) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set the Temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) await self.async_change( diff --git a/homeassistant/components/advantage_air/switch.py b/homeassistant/components/advantage_air/switch.py index d9d46427599..52992ae9531 100644 --- a/homeassistant/components/advantage_air/switch.py +++ b/homeassistant/components/advantage_air/switch.py @@ -1,4 +1,6 @@ """Switch platform for Advantage Air integration.""" +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -44,13 +46,13 @@ class AdvantageAirFreshAir(AdvantageAirAcEntity, SwitchEntity): """Return the fresh air status.""" return self._ac["freshAirStatus"] == ADVANTAGE_AIR_STATE_ON - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn fresh air on.""" await self.async_change( {self.ac_key: {"info": {"freshAirStatus": ADVANTAGE_AIR_STATE_ON}}} ) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn fresh air off.""" await self.async_change( {self.ac_key: {"info": {"freshAirStatus": ADVANTAGE_AIR_STATE_OFF}}} diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py index e99f1ecf223..2e8568b22b7 100644 --- a/homeassistant/components/agent_dvr/camera.py +++ b/homeassistant/components/agent_dvr/camera.py @@ -91,7 +91,7 @@ class AgentCamera(MjpegCamera): sw_version=device.client.version, ) - async def async_update(self): + async def async_update(self) -> None: """Update our state from the Agent API.""" try: await self.device.update() @@ -148,7 +148,7 @@ class AgentCamera(MjpegCamera): return self.device.online @property - def motion_detection_enabled(self): + def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" return self.device.detector_active @@ -160,11 +160,11 @@ class AgentCamera(MjpegCamera): """Disable alerts.""" await self.device.alerts_off() - async def async_enable_motion_detection(self): + async def async_enable_motion_detection(self) -> None: """Enable motion detection.""" await self.device.detector_on() - async def async_disable_motion_detection(self): + async def async_disable_motion_detection(self) -> None: """Disable motion detection.""" await self.device.detector_off() @@ -176,7 +176,7 @@ class AgentCamera(MjpegCamera): """Stop recording.""" await self.device.record_stop() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Enable the camera.""" await self.device.enable() @@ -184,6 +184,6 @@ class AgentCamera(MjpegCamera): """Take a snapshot.""" await self.device.snapshot() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Disable the camera.""" await self.device.disable() diff --git a/homeassistant/components/airtouch4/climate.py b/homeassistant/components/airtouch4/climate.py index f660c06082c..370a061e901 100644 --- a/homeassistant/components/airtouch4/climate.py +++ b/homeassistant/components/airtouch4/climate.py @@ -154,7 +154,7 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity): modes.append(HVACMode.OFF) return modes - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" if hvac_mode not in HA_STATE_TO_AT: raise ValueError(f"Unsupported HVAC mode: {hvac_mode}") @@ -170,7 +170,7 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity): _LOGGER.debug("Setting operation mode of %s to %s", self._ac_number, hvac_mode) self.async_write_ha_state() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" if fan_mode not in self.fan_modes: raise ValueError(f"Unsupported fan mode: {fan_mode}") @@ -182,14 +182,14 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity): self._unit = self._airtouch.GetAcs()[self._ac_number] self.async_write_ha_state() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on.""" _LOGGER.debug("Turning %s on", self.unique_id) # in case ac is not on. Airtouch turns itself off if no groups are turned on # (even if groups turned back on) await self._airtouch.TurnAcOn(self._ac_number) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off.""" _LOGGER.debug("Turning %s off", self.unique_id) await self._airtouch.TurnAcOff(self._ac_number) @@ -266,7 +266,7 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity): return HVACMode.FAN_ONLY - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" if hvac_mode not in HA_STATE_TO_AT: raise ValueError(f"Unsupported HVAC mode: {hvac_mode}") @@ -304,7 +304,7 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity): ) self.async_write_ha_state() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" if fan_mode not in self.fan_modes: raise ValueError(f"Unsupported fan mode: {fan_mode}") @@ -315,7 +315,7 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity): ) self.async_write_ha_state() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on.""" _LOGGER.debug("Turning %s on", self.unique_id) await self._airtouch.TurnGroupOn(self._group_number) @@ -330,7 +330,7 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity): await self.coordinator.async_request_refresh() self.async_write_ha_state() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off.""" _LOGGER.debug("Turning %s off", self.unique_id) await self._airtouch.TurnGroupOff(self._group_number) diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index 24eeffde691..47e6066400c 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -88,7 +88,7 @@ class AlarmDecoderBinarySensor(BinarySensorEntity): CONF_ZONE_NUMBER: self._zone_number, } - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect(self.hass, SIGNAL_ZONE_FAULT, self._fault_callback) diff --git a/homeassistant/components/alarmdecoder/sensor.py b/homeassistant/components/alarmdecoder/sensor.py index 90d0606d681..f0ffc7e7158 100644 --- a/homeassistant/components/alarmdecoder/sensor.py +++ b/homeassistant/components/alarmdecoder/sensor.py @@ -24,7 +24,7 @@ class AlarmDecoderSensor(SensorEntity): _attr_name = "Alarm Panel Display" _attr_should_poll = False - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index 50135693ff4..99fefbb180e 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -170,7 +170,7 @@ class AmbiclimateEntity(ClimateEntity): return await self._heater.set_target_temperature(temperature) - async def async_set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" if hvac_mode == HVACMode.HEAT: await self._heater.turn_on() diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 0d43ada7bc0..696fab5788f 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -298,7 +298,7 @@ class ADBDevice(MediaPlayerEntity): self.turn_off_command = options.get(CONF_TURN_OFF_COMMAND) self.turn_on_command = options.get(CONF_TURN_ON_COMMAND) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Set config parameter when add to hass.""" await super().async_added_to_hass() self._process_config() @@ -321,7 +321,7 @@ class ADBDevice(MediaPlayerEntity): """Take a screen capture from the device.""" return await self.aftv.adb_screencap() - async def async_get_media_image(self): + async def async_get_media_image(self) -> tuple[bytes | None, str | None]: """Fetch current playing image.""" if not self._screencap or self.state in (STATE_OFF, None) or not self.available: return None, None @@ -337,22 +337,22 @@ class ADBDevice(MediaPlayerEntity): return None, None @adb_decorator() - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" await self.aftv.media_play() @adb_decorator() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" await self.aftv.media_pause() @adb_decorator() - async def async_media_play_pause(self): + async def async_media_play_pause(self) -> None: """Send play/pause command.""" await self.aftv.media_play_pause() @adb_decorator() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on the device.""" if self.turn_on_command: await self.aftv.adb_shell(self.turn_on_command) @@ -360,7 +360,7 @@ class ADBDevice(MediaPlayerEntity): await self.aftv.turn_on() @adb_decorator() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off the device.""" if self.turn_off_command: await self.aftv.adb_shell(self.turn_off_command) @@ -368,17 +368,17 @@ class ADBDevice(MediaPlayerEntity): await self.aftv.turn_off() @adb_decorator() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command (results in rewind).""" await self.aftv.media_previous_track() @adb_decorator() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command (results in fast-forward).""" await self.aftv.media_next_track() @adb_decorator() - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select input source. If the source starts with a '!', then it will close the app instead of @@ -469,7 +469,7 @@ class AndroidTVDevice(ADBDevice): ) @adb_decorator(override_available=True) - async def async_update(self): + async def async_update(self) -> None: """Update the device state and, if necessary, re-connect.""" # Check if device is disconnected. if not self._attr_available: @@ -514,12 +514,12 @@ class AndroidTVDevice(ADBDevice): self._attr_source_list = None @adb_decorator() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop command.""" await self.aftv.media_stop() @adb_decorator() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" is_muted = await self.aftv.is_volume_muted() @@ -528,17 +528,17 @@ class AndroidTVDevice(ADBDevice): await self.aftv.mute_volume() @adb_decorator() - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set the volume level.""" await self.aftv.set_volume_level(volume) @adb_decorator() - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Send volume down command.""" self._attr_volume_level = await self.aftv.volume_down(self._attr_volume_level) @adb_decorator() - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Send volume up command.""" self._attr_volume_level = await self.aftv.volume_up(self._attr_volume_level) @@ -558,7 +558,7 @@ class FireTVDevice(ADBDevice): ) @adb_decorator(override_available=True) - async def async_update(self): + async def async_update(self) -> None: """Update the device state and, if necessary, re-connect.""" # Check if device is disconnected. if not self._attr_available: @@ -600,6 +600,6 @@ class FireTVDevice(ADBDevice): self._attr_source_list = None @adb_decorator() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop (back) command.""" await self.aftv.back() diff --git a/homeassistant/components/anel_pwrctrl/switch.py b/homeassistant/components/anel_pwrctrl/switch.py index f4a5ca32c3c..19d1a2deaff 100644 --- a/homeassistant/components/anel_pwrctrl/switch.py +++ b/homeassistant/components/anel_pwrctrl/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any from anel_pwrctrl import DeviceMaster import voluptuous as vol @@ -78,16 +79,16 @@ class PwrCtrlSwitch(SwitchEntity): self._attr_unique_id = f"{port.device.host}-{port.get_index()}" self._attr_name = port.label - def update(self): + def update(self) -> None: """Trigger update for all switches on the parent device.""" self._parent_device.update() self._attr_is_on = self._port.get_state() - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self._port.on() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self._port.off() diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index 7ef491a5367..25d50c06c97 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -38,6 +38,6 @@ class OnlineStatus(BinarySensorEntity): self._data = data self._attr_name = config[CONF_NAME] - def update(self): + def update(self) -> None: """Get the status report from APCUPSd and set this entity's state.""" self._attr_is_on = int(self._data.status[KEY_STATUS], 16) & VALUE_ONLINE > 0 diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index b7e7366796b..0f85c4c6854 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -460,7 +460,7 @@ class APCUPSdSensor(SensorEntity): self._data = data self._attr_name = f"{SENSOR_PREFIX}{description.name}" - def update(self): + def update(self) -> None: """Get the latest status and use it to update our sensor state.""" key = self.entity_description.key.upper() if key not in self._data.status: diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 3a495e053eb..771b27a6dc3 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -1,5 +1,8 @@ """Support for Apple TV media player.""" +from __future__ import annotations + import logging +from typing import Any from pyatv import exceptions from pyatv.const import ( @@ -276,7 +279,9 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): return dt_util.utcnow() return None - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Send the play_media command to the media player.""" # If input (file) has a file format supported by pyatv, then stream it with # RAOP. Otherwise try to play it with regular AirPlay. @@ -314,7 +319,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): return self.atv.metadata.artwork_id return None - async def async_get_media_image(self): + async def async_get_media_image(self) -> tuple[bytes | None, str | None]: """Fetch media image of current playing image.""" state = self.state if self._playing and state not in [STATE_OFF, STATE_IDLE]: @@ -391,8 +396,8 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): async def async_browse_media( self, - media_content_type=None, - media_content_id=None, + media_content_type: str | None = None, + media_content_id: str | None = None, ) -> BrowseMedia: """Implement the websocket media browsing helper.""" if media_content_id == "apps" or ( @@ -427,12 +432,12 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): return cur_item - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn the media player on.""" if self._is_feature_available(FeatureName.TurnOn): await self.atv.power.turn_on() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn the media player off.""" if (self._is_feature_available(FeatureName.TurnOff)) and ( not self._is_feature_available(FeatureName.PowerState) @@ -440,58 +445,58 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): ): await self.atv.power.turn_off() - async def async_media_play_pause(self): + async def async_media_play_pause(self) -> None: """Pause media on media player.""" if self._playing: await self.atv.remote_control.play_pause() - async def async_media_play(self): + async def async_media_play(self) -> None: """Play media.""" if self.atv: await self.atv.remote_control.play() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Stop the media player.""" if self.atv: await self.atv.remote_control.stop() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Pause the media player.""" if self.atv: await self.atv.remote_control.pause() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" if self.atv: await self.atv.remote_control.next() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command.""" if self.atv: await self.atv.remote_control.previous() - async def async_media_seek(self, position): + async def async_media_seek(self, position: float) -> None: """Send seek command.""" if self.atv: await self.atv.remote_control.set_position(position) - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Turn volume up for media player.""" if self.atv: await self.atv.audio.volume_up() - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Turn volume down for media player.""" if self.atv: await self.atv.audio.volume_down() - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" if self.atv: # pyatv expects volume in percent await self.atv.audio.set_volume(volume * 100.0) - async def async_set_repeat(self, repeat): + async def async_set_repeat(self, repeat: str) -> None: """Set repeat mode.""" if self.atv: mode = { @@ -500,7 +505,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): }.get(repeat, RepeatState.Off) await self.atv.remote_control.set_repeat(mode) - async def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" if self.atv: await self.atv.remote_control.set_shuffle( diff --git a/homeassistant/components/apple_tv/remote.py b/homeassistant/components/apple_tv/remote.py index 2e8cfc4f6b5..f3be6977891 100644 --- a/homeassistant/components/apple_tv/remote.py +++ b/homeassistant/components/apple_tv/remote.py @@ -1,6 +1,8 @@ """Remote control support for Apple TV.""" import asyncio +from collections.abc import Iterable import logging +from typing import Any from homeassistant.components.remote import ( ATTR_DELAY_SECS, @@ -40,15 +42,15 @@ class AppleTVRemote(AppleTVEntity, RemoteEntity): """Return true if device is on.""" return self.atv is not None - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" await self.manager.connect() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" await self.manager.disconnect() - async def async_send_command(self, command, **kwargs): + async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a command to one device.""" num_repeats = kwargs[ATTR_NUM_REPEATS] delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) diff --git a/homeassistant/components/aqualogic/sensor.py b/homeassistant/components/aqualogic/sensor.py index 5e6e35cce76..d575beb0367 100644 --- a/homeassistant/components/aqualogic/sensor.py +++ b/homeassistant/components/aqualogic/sensor.py @@ -144,7 +144,7 @@ class AquaLogicSensor(SensorEntity): self._processor = processor self._attr_name = f"AquaLogic {description.name}" - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py index 953b0c9b527..e04bc8595fa 100644 --- a/homeassistant/components/aqualogic/switch.py +++ b/homeassistant/components/aqualogic/switch.py @@ -1,6 +1,8 @@ """Support for AquaLogic switches.""" from __future__ import annotations +from typing import Any + from aqualogic.core import States import voluptuous as vol @@ -82,19 +84,19 @@ class AquaLogicSwitch(SwitchEntity): state = panel.get_state(self._state_name) return state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if (panel := self._processor.panel) is None: return panel.set_state(self._state_name, True) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" if (panel := self._processor.panel) is None: return panel.set_state(self._state_name, False) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect(self.hass, UPDATE_TOPIC, self.async_write_ha_state) diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index ccbae7151b2..c9465424381 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -131,7 +131,7 @@ class SharpAquosTVDevice(MediaPlayerEntity): self._attr_state = state @_retry - def update(self): + def update(self) -> None: """Retrieve the latest data.""" if self._remote.power() == 1: self._attr_state = STATE_ON @@ -153,7 +153,7 @@ class SharpAquosTVDevice(MediaPlayerEntity): self._attr_volume_level = self._remote.volume() / 60 @_retry - def turn_off(self): + def turn_off(self) -> None: """Turn off tvplayer.""" self._remote.power(0) @@ -168,46 +168,46 @@ class SharpAquosTVDevice(MediaPlayerEntity): self._remote.volume(int(self.volume_level * 60) - 2) @_retry - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set Volume media player.""" self._remote.volume(int(volume * 60)) @_retry - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Send mute command.""" self._remote.mute(0) @_retry - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self._remote.power(1) @_retry - def media_play_pause(self): + def media_play_pause(self) -> None: """Simulate play pause media player.""" self._remote.remote_button(40) @_retry - def media_play(self): + def media_play(self) -> None: """Send play command.""" self._remote.remote_button(16) @_retry - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" self._remote.remote_button(16) @_retry - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" self._remote.remote_button(21) @_retry - def media_previous_track(self): + def media_previous_track(self) -> None: """Send the previous track command.""" self._remote.remote_button(19) - def select_source(self, source): + def select_source(self, source: str) -> None: """Set the input source.""" for key, value in SOURCES.items(): if source == value: diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 731eb0b0352..f995b79df04 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -1,5 +1,8 @@ """Arcam media player.""" +from __future__ import annotations + import logging +from typing import Any from arcam.fmj import SourceCodes from arcam.fmj.state import State @@ -107,7 +110,7 @@ class ArcamFmj(MediaPlayerEntity): name=self._device_name, ) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Once registered, add listener for events.""" await self._state.start() await self._state.update() @@ -139,17 +142,17 @@ class ArcamFmj(MediaPlayerEntity): async_dispatcher_connect(self.hass, SIGNAL_CLIENT_STOPPED, _stopped) ) - async def async_update(self): + async def async_update(self) -> None: """Force update of state.""" _LOGGER.debug("Update state %s", self.name) await self._state.update() - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" await self._state.set_mute(mute) self.async_write_ha_state() - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select a specific source.""" try: value = SourceCodes[source] @@ -160,7 +163,7 @@ class ArcamFmj(MediaPlayerEntity): await self._state.set_source(value) self.async_write_ha_state() - async def async_select_sound_mode(self, sound_mode): + async def async_select_sound_mode(self, sound_mode: str) -> None: """Select a specific source.""" try: await self._state.set_decode_mode(sound_mode) @@ -170,22 +173,22 @@ class ArcamFmj(MediaPlayerEntity): self.async_write_ha_state() - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" await self._state.set_volume(round(volume * 99.0)) self.async_write_ha_state() - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Turn volume up for media player.""" await self._state.inc_volume() self.async_write_ha_state() - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Turn volume up for media player.""" await self._state.dec_volume() self.async_write_ha_state() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn the media player on.""" if self._state.get_power() is not None: _LOGGER.debug("Turning on device using connection") @@ -194,11 +197,13 @@ class ArcamFmj(MediaPlayerEntity): _LOGGER.debug("Firing event to turn on device") self.hass.bus.async_fire(EVENT_TURN_ON, {ATTR_ENTITY_ID: self.entity_id}) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn the media player off.""" await self._state.set_power(False) - async def async_browse_media(self, media_content_type=None, media_content_id=None): + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: """Implement the websocket media browsing helper.""" if media_content_id not in (None, "root"): raise BrowseError( @@ -231,7 +236,9 @@ class ArcamFmj(MediaPlayerEntity): return root - async def async_play_media(self, media_type: str, media_id: str, **kwargs) -> None: + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Play media.""" if media_id.startswith("preset:"): diff --git a/homeassistant/components/arest/binary_sensor.py b/homeassistant/components/arest/binary_sensor.py index 92309f5620f..5d65a23335a 100644 --- a/homeassistant/components/arest/binary_sensor.py +++ b/homeassistant/components/arest/binary_sensor.py @@ -86,7 +86,7 @@ class ArestBinarySensor(BinarySensorEntity): if request.status_code != HTTPStatus.OK: _LOGGER.error("Can't set mode of %s", resource) - def update(self): + def update(self) -> None: """Get the latest data from aREST API.""" self.arest.update() self._attr_is_on = bool(self.arest.data.get("state")) @@ -102,7 +102,7 @@ class ArestData: self.data = {} @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): + def update(self) -> None: """Get the latest data from aREST device.""" try: response = requests.get(f"{self._resource}/digital/{self._pin}", timeout=10) diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index e4d3fff8c74..5c95fd63c3b 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -159,7 +159,7 @@ class ArestSensor(SensorEntity): if request.status_code != HTTPStatus.OK: _LOGGER.error("Can't set mode of %s", resource) - def update(self): + def update(self) -> None: """Get the latest data from aREST API.""" self.arest.update() self._attr_available = self.arest.available diff --git a/homeassistant/components/arest/switch.py b/homeassistant/components/arest/switch.py index f8c45313d52..6efa24c5a0f 100644 --- a/homeassistant/components/arest/switch.py +++ b/homeassistant/components/arest/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations from http import HTTPStatus import logging +from typing import Any import requests import voluptuous as vol @@ -122,7 +123,7 @@ class ArestSwitchFunction(ArestSwitchBase): except ValueError: _LOGGER.error("Response invalid") - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" request = requests.get( f"{self._resource}/{self._func}", timeout=10, params={"params": "1"} @@ -133,7 +134,7 @@ class ArestSwitchFunction(ArestSwitchBase): else: _LOGGER.error("Can't turn on function %s at %s", self._func, self._resource) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" request = requests.get( f"{self._resource}/{self._func}", timeout=10, params={"params": "0"} @@ -146,7 +147,7 @@ class ArestSwitchFunction(ArestSwitchBase): "Can't turn off function %s at %s", self._func, self._resource ) - def update(self): + def update(self) -> None: """Get the latest data from aREST API and update the state.""" try: request = requests.get(f"{self._resource}/{self._func}", timeout=10) @@ -171,7 +172,7 @@ class ArestSwitchPin(ArestSwitchBase): _LOGGER.error("Can't set mode") self._attr_available = False - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" turn_on_payload = int(not self.invert) request = requests.get( @@ -182,7 +183,7 @@ class ArestSwitchPin(ArestSwitchBase): else: _LOGGER.error("Can't turn on pin %s at %s", self._pin, self._resource) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" turn_off_payload = int(self.invert) request = requests.get( @@ -193,7 +194,7 @@ class ArestSwitchPin(ArestSwitchBase): else: _LOGGER.error("Can't turn off pin %s at %s", self._pin, self._resource) - def update(self): + def update(self) -> None: """Get the latest data from aREST API and update the state.""" try: request = requests.get(f"{self._resource}/digital/{self._pin}", timeout=10) diff --git a/homeassistant/components/atag/climate.py b/homeassistant/components/atag/climate.py index cf5624005ed..e6e9d8503e8 100644 --- a/homeassistant/components/atag/climate.py +++ b/homeassistant/components/atag/climate.py @@ -1,6 +1,8 @@ """Initialization of ATAG One climate platform.""" from __future__ import annotations +from typing import Any + from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( PRESET_AWAY, @@ -78,12 +80,12 @@ class AtagThermostat(AtagEntity, ClimateEntity): preset = self.coordinator.data.climate.preset_mode return PRESET_INVERTED.get(preset) - async def async_set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" await self.coordinator.data.climate.set_temp(kwargs.get(ATTR_TEMPERATURE)) self.async_write_ha_state() - async def async_set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" await self.coordinator.data.climate.set_hvac_mode(hvac_mode) self.async_write_ha_state() diff --git a/homeassistant/components/atag/water_heater.py b/homeassistant/components/atag/water_heater.py index 1dc38401574..009f84a72ef 100644 --- a/homeassistant/components/atag/water_heater.py +++ b/homeassistant/components/atag/water_heater.py @@ -1,4 +1,6 @@ """ATAG water heater component.""" +from typing import Any + from homeassistant.components.water_heater import ( STATE_ECO, STATE_PERFORMANCE, @@ -42,7 +44,7 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity): operation = self.coordinator.data.dhw.current_operation return operation if operation in self.operation_list else STATE_OFF - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if await self.coordinator.data.dhw.set_temp(kwargs.get(ATTR_TEMPERATURE)): self.async_write_ha_state() @@ -53,11 +55,11 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity): return self.coordinator.data.dhw.target_temperature @property - def max_temp(self): + def max_temp(self) -> float: """Return the maximum temperature.""" return self.coordinator.data.dhw.max_temp @property - def min_temp(self): + def min_temp(self) -> float: """Return the minimum temperature.""" return self.coordinator.data.dhw.min_temp diff --git a/homeassistant/components/aten_pe/switch.py b/homeassistant/components/aten_pe/switch.py index 92a58f37c8c..d49201f6d7b 100644 --- a/homeassistant/components/aten_pe/switch.py +++ b/homeassistant/components/aten_pe/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from atenpdu import AtenPE, AtenPEError import voluptuous as vol @@ -101,17 +102,17 @@ class AtenSwitch(SwitchEntity): self._attr_unique_id = f"{mac}-{outlet}" self._attr_name = name or f"Outlet {outlet}" - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self._device.setOutletStatus(self._outlet, "on") self._attr_is_on = True - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self._device.setOutletStatus(self._outlet, "off") self._attr_is_on = False - async def async_update(self): + async def async_update(self) -> None: """Process update from entity.""" status = await self._device.displayOutletStatus(self._outlet) if status == "on": diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index be769eae49b..d3df8b4e684 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -269,7 +269,7 @@ class AtomeSensor(SensorEntity): self._attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR self._attr_state_class = SensorStateClass.TOTAL_INCREASING - def update(self): + def update(self) -> None: """Update device state.""" update_function = getattr(self._data, f"update_{self._sensor_type}_usage") update_function() diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index a33d1cb96dc..6f45f626180 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -288,7 +288,7 @@ class AugustDoorbellBinarySensor(AugustEntityMixin, BinarySensorEntity): self._check_for_off_update_listener() self._check_for_off_update_listener = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call the mixin to subscribe and setup an async_track_point_in_utc_time to turn off the sensor if needed.""" self._schedule_update_to_recheck_turn_off_sensor() await super().async_added_to_hass() diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index c5ab5fc3cfa..32b23e1329c 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -43,12 +43,12 @@ class AugustCamera(AugustEntityMixin, Camera): self._attr_unique_id = f"{self._device_id:s}_camera" @property - def is_recording(self): + def is_recording(self) -> bool: """Return true if the device is recording.""" return self._device.has_subscription @property - def motion_detection_enabled(self): + def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" return True diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index 188f1c789a2..eeb72e4d485 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -82,7 +82,7 @@ class AuroraSensor(AuroraEntity, SensorEntity): self.entity_description = entity_description self.available_prev = True - def update(self): + def update(self) -> None: """Fetch new state data for the sensor. This is the only method that should fetch new data for Home Assistant. From 7a497c1e6e5a0d44b9418a754470ca9dd35e9719 Mon Sep 17 00:00:00 2001 From: Vincent Knoop Pathuis <48653141+vpathuis@users.noreply.github.com> Date: Thu, 18 Aug 2022 16:40:04 +0200 Subject: [PATCH 3447/3516] Add Landis+Gyr Heat Meter integration (#73363) * Add Landis+Gyr Heat Meter integration * Add contant for better sensor config * Add test for init * Refactor some of the PR suggestions in config_flow * Apply small fix * Correct total_increasing to total * Add test for restore state * Add MWh entity that can be added as gas on the energy dashoard * Remove GJ as unit * Round MWh to 5 iso 3 digits * Update homeassistant/components/landisgyr_heat_meter/const.py * Update CODEOWNERS Co-authored-by: Erik Montnemery --- CODEOWNERS | 2 + .../landisgyr_heat_meter/__init__.py | 56 +++++ .../landisgyr_heat_meter/config_flow.py | 136 +++++++++++ .../components/landisgyr_heat_meter/const.py | 192 +++++++++++++++ .../landisgyr_heat_meter/manifest.json | 13 + .../components/landisgyr_heat_meter/sensor.py | 108 +++++++++ .../landisgyr_heat_meter/strings.json | 23 ++ .../landisgyr_heat_meter/translations/en.json | 24 ++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + .../landisgyr_heat_meter/__init__.py | 1 + .../landisgyr_heat_meter/test_config_flow.py | 227 ++++++++++++++++++ .../landisgyr_heat_meter/test_init.py | 22 ++ .../landisgyr_heat_meter/test_sensor.py | 200 +++++++++++++++ 15 files changed, 1011 insertions(+) create mode 100644 homeassistant/components/landisgyr_heat_meter/__init__.py create mode 100644 homeassistant/components/landisgyr_heat_meter/config_flow.py create mode 100644 homeassistant/components/landisgyr_heat_meter/const.py create mode 100644 homeassistant/components/landisgyr_heat_meter/manifest.json create mode 100644 homeassistant/components/landisgyr_heat_meter/sensor.py create mode 100644 homeassistant/components/landisgyr_heat_meter/strings.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/en.json create mode 100644 tests/components/landisgyr_heat_meter/__init__.py create mode 100644 tests/components/landisgyr_heat_meter/test_config_flow.py create mode 100644 tests/components/landisgyr_heat_meter/test_init.py create mode 100644 tests/components/landisgyr_heat_meter/test_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 1f3ce12e63a..26d1a8da2f8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -587,6 +587,8 @@ build.json @home-assistant/supervisor /tests/components/lacrosse_view/ @IceBotYT /homeassistant/components/lametric/ @robbiet480 @frenck /tests/components/lametric/ @robbiet480 @frenck +/homeassistant/components/landisgyr_heat_meter/ @vpathuis +/tests/components/landisgyr_heat_meter/ @vpathuis /homeassistant/components/launch_library/ @ludeeus @DurgNomis-drol /tests/components/launch_library/ @ludeeus @DurgNomis-drol /homeassistant/components/laundrify/ @xLarry diff --git a/homeassistant/components/landisgyr_heat_meter/__init__.py b/homeassistant/components/landisgyr_heat_meter/__init__.py new file mode 100644 index 00000000000..b5a9a3fba79 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/__init__.py @@ -0,0 +1,56 @@ +"""The Landis+Gyr Heat Meter integration.""" +from __future__ import annotations + +import logging + +from ultraheat_api import HeatMeterService, UltraheatReader + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DEVICE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up heat meter from a config entry.""" + + _LOGGER.debug("Initializing %s integration on %s", DOMAIN, entry.data[CONF_DEVICE]) + reader = UltraheatReader(entry.data[CONF_DEVICE]) + + api = HeatMeterService(reader) + + async def async_update_data(): + """Fetch data from the API.""" + _LOGGER.info("Polling on %s", entry.data[CONF_DEVICE]) + return await hass.async_add_executor_job(api.read) + + # No automatic polling and no initial refresh of data is being done at this point, + # to prevent battery drain. The user will have to do it manually. + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="ultraheat_gateway", + update_method=async_update_data, + update_interval=None, + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/landisgyr_heat_meter/config_flow.py b/homeassistant/components/landisgyr_heat_meter/config_flow.py new file mode 100644 index 00000000000..e3dbbb7433b --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/config_flow.py @@ -0,0 +1,136 @@ +"""Config flow for Landis+Gyr Heat Meter integration.""" +from __future__ import annotations + +import logging +import os + +import async_timeout +import serial +import serial.tools.list_ports +from ultraheat_api import HeatMeterService, UltraheatReader +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_DEVICE +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +CONF_MANUAL_PATH = "Enter Manually" + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_DEVICE): str, + } +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Ultraheat Heat Meter.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Step when setting up serial configuration.""" + errors = {} + + if user_input is not None: + if user_input[CONF_DEVICE] == CONF_MANUAL_PATH: + return await self.async_step_setup_serial_manual_path() + + dev_path = await self.hass.async_add_executor_job( + get_serial_by_id, user_input[CONF_DEVICE] + ) + + try: + return await self.validate_and_create_entry(dev_path) + except CannotConnect: + errors["base"] = "cannot_connect" + + ports = await self.get_ports() + + schema = vol.Schema({vol.Required(CONF_DEVICE): vol.In(ports)}) + return self.async_show_form(step_id="user", data_schema=schema, errors=errors) + + async def async_step_setup_serial_manual_path(self, user_input=None): + """Set path manually.""" + errors = {} + + if user_input is not None: + dev_path = user_input[CONF_DEVICE] + try: + return await self.validate_and_create_entry(dev_path) + except CannotConnect: + errors["base"] = "cannot_connect" + + schema = vol.Schema({vol.Required(CONF_DEVICE): str}) + return self.async_show_form( + step_id="setup_serial_manual_path", + data_schema=schema, + errors=errors, + ) + + async def validate_and_create_entry(self, dev_path): + """Try to connect to the device path and return an entry.""" + model, device_number = await self.validate_ultraheat(dev_path) + + await self.async_set_unique_id(device_number) + self._abort_if_unique_id_configured() + data = { + CONF_DEVICE: dev_path, + "model": model, + "device_number": device_number, + } + return self.async_create_entry( + title=model, + data=data, + ) + + async def validate_ultraheat(self, port: str): + """Validate the user input allows us to connect.""" + + reader = UltraheatReader(port) + heat_meter = HeatMeterService(reader) + try: + async with async_timeout.timeout(10): + # validate and retrieve the model and device number for a unique id + data = await self.hass.async_add_executor_job(heat_meter.read) + _LOGGER.debug("Got data from Ultraheat API: %s", data) + + except Exception as err: + _LOGGER.warning("Failed read data from: %s. %s", port, err) + raise CannotConnect(f"Error communicating with device: {err}") from err + + _LOGGER.debug("Successfully connected to %s", port) + return data.model, data.device_number + + async def get_ports(self) -> dict: + """Get the available ports.""" + ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) + formatted_ports = {} + for port in ports: + formatted_ports[ + port.device + ] = f"{port}, s/n: {port.serial_number or 'n/a'}" + ( + f" - {port.manufacturer}" if port.manufacturer else "" + ) + formatted_ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH + return formatted_ports + + +def get_serial_by_id(dev_path: str) -> str: + """Return a /dev/serial/by-id match for given device if available.""" + by_id = "/dev/serial/by-id" + if not os.path.isdir(by_id): + return dev_path + + for path in (entry.path for entry in os.scandir(by_id) if entry.is_symlink()): + if os.path.realpath(path) == dev_path: + return path + return dev_path + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/landisgyr_heat_meter/const.py b/homeassistant/components/landisgyr_heat_meter/const.py new file mode 100644 index 00000000000..70008890d1f --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/const.py @@ -0,0 +1,192 @@ +"""Constants for the Landis+Gyr Heat Meter integration.""" + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ENERGY_MEGA_WATT_HOUR, TEMP_CELSIUS, VOLUME_CUBIC_METERS +from homeassistant.helpers.entity import EntityCategory + +DOMAIN = "landisgyr_heat_meter" + +GJ_TO_MWH = 0.277778 # conversion factor + +HEAT_METER_SENSOR_TYPES = ( + SensorEntityDescription( + key="heat_usage", + icon="mdi:fire", + name="Heat usage", + native_unit_of_measurement=ENERGY_MEGA_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL, + ), + SensorEntityDescription( + key="volume_usage_m3", + icon="mdi:fire", + name="Volume usage", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + state_class=SensorStateClass.TOTAL, + ), + # Diagnostic entity for debugging, this will match the value in GJ indicated on the meter's display + SensorEntityDescription( + key="heat_usage_gj", + icon="mdi:fire", + name="Heat usage GJ", + native_unit_of_measurement="GJ", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="heat_previous_year", + icon="mdi:fire", + name="Heat usage previous year", + native_unit_of_measurement=ENERGY_MEGA_WATT_HOUR, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="volume_previous_year_m3", + icon="mdi:fire", + name="Volume usage previous year", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="ownership_number", + name="Ownership number", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="error_number", + name="Error number", + icon="mdi:home-alert", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="device_number", + name="Device number", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="measurement_period_minutes", + name="Measurement period minutes", + icon="mdi:clock-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="power_max_kw", + name="Power max", + native_unit_of_measurement="kW", + icon="mdi:power-plug-outline", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="power_max_previous_year_kw", + name="Power max previous year", + native_unit_of_measurement="kW", + icon="mdi:power-plug-outline", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="flowrate_max_m3ph", + name="Flowrate max", + native_unit_of_measurement="m3ph", + icon="mdi:water-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="flowrate_max_previous_year_m3ph", + name="Flowrate max previous year", + native_unit_of_measurement="m3ph", + icon="mdi:water-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="return_temperature_max_c", + name="Return temperature max", + native_unit_of_measurement=TEMP_CELSIUS, + icon="mdi:thermometer", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="return_temperature_max_previous_year_c", + name="Return temperature max previous year", + native_unit_of_measurement=TEMP_CELSIUS, + icon="mdi:thermometer", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="flow_temperature_max_c", + name="Flow temperature max", + native_unit_of_measurement=TEMP_CELSIUS, + icon="mdi:thermometer", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="flow_temperature_max_previous_year_c", + name="Flow temperature max previous year", + native_unit_of_measurement=TEMP_CELSIUS, + icon="mdi:thermometer", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="operating_hours", + name="Operating hours", + icon="mdi:clock-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="flow_hours", + name="Flow hours", + icon="mdi:clock-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="fault_hours", + name="Fault hours", + icon="mdi:clock-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="fault_hours_previous_year", + name="Fault hours previous year", + icon="mdi:clock-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="yearly_set_day", + name="Yearly set day", + icon="mdi:clock-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="monthly_set_day", + name="Monthly set day", + icon="mdi:clock-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="meter_date_time", + name="Meter date time", + icon="mdi:clock-outline", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="measuring_range_m3ph", + name="Measuring range", + native_unit_of_measurement="m3ph", + icon="mdi:water-outline", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="settings_and_firmware", + name="Settings and firmware", + entity_category=EntityCategory.DIAGNOSTIC, + ), +) diff --git a/homeassistant/components/landisgyr_heat_meter/manifest.json b/homeassistant/components/landisgyr_heat_meter/manifest.json new file mode 100644 index 00000000000..359ca1acea6 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "landisgyr_heat_meter", + "name": "Landis+Gyr Heat Meter", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/landisgyr_heat_meter", + "requirements": ["ultraheat-api==0.4.1"], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [], + "codeowners": ["@vpathuis"], + "iot_class": "local_polling" +} diff --git a/homeassistant/components/landisgyr_heat_meter/sensor.py b/homeassistant/components/landisgyr_heat_meter/sensor.py new file mode 100644 index 00000000000..1d38b1f5816 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/sensor.py @@ -0,0 +1,108 @@ +"""Platform for sensor integration.""" +from __future__ import annotations + +from dataclasses import asdict +import logging + +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + RestoreSensor, + SensorDeviceClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import dt as dt_util + +from . import DOMAIN +from .const import GJ_TO_MWH, HEAT_METER_SENSOR_TYPES + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the sensor platform.""" + _LOGGER.info("The Landis+Gyr Heat Meter sensor platform is being set up!") + + unique_id = entry.entry_id + coordinator = hass.data[DOMAIN][entry.entry_id] + + model = entry.data["model"] + + device = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer="Landis & Gyr", + model=model, + name="Landis+Gyr Heat Meter", + ) + + sensors = [] + + for description in HEAT_METER_SENSOR_TYPES: + sensors.append(HeatMeterSensor(coordinator, unique_id, description, device)) + + async_add_entities(sensors) + + +class HeatMeterSensor(CoordinatorEntity, RestoreSensor): + """Representation of a Sensor.""" + + def __init__(self, coordinator, unique_id, description, device): + """Set up the sensor with the initial values.""" + super().__init__(coordinator) + self.key = description.key + self._attr_unique_id = f"{DOMAIN}_{unique_id}_{description.key}" + self._attr_name = "Heat Meter " + description.name + if hasattr(description, "icon"): + self._attr_icon = description.icon + if hasattr(description, "entity_category"): + self._attr_entity_category = description.entity_category + if hasattr(description, ATTR_STATE_CLASS): + self._attr_state_class = description.state_class + if hasattr(description, ATTR_DEVICE_CLASS): + self._attr_device_class = description.device_class + if hasattr(description, ATTR_UNIT_OF_MEASUREMENT): + self._attr_native_unit_of_measurement = ( + description.native_unit_of_measurement + ) + self._attr_device_info = device + self._attr_should_poll = bool(self.key in ("heat_usage", "heat_previous_year")) + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + await super().async_added_to_hass() + state = await self.async_get_last_sensor_data() + if state: + self._attr_native_value = state.native_value + + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + if self.key in asdict(self.coordinator.data): + if self.device_class == SensorDeviceClass.TIMESTAMP: + self._attr_native_value = dt_util.as_utc( + asdict(self.coordinator.data)[self.key] + ) + else: + self._attr_native_value = asdict(self.coordinator.data)[self.key] + + if self.key == "heat_usage": + self._attr_native_value = convert_gj_to_mwh( + self.coordinator.data.heat_usage_gj + ) + + if self.key == "heat_previous_year": + self._attr_native_value = convert_gj_to_mwh( + self.coordinator.data.heat_previous_year_gj + ) + + self.async_write_ha_state() + + +def convert_gj_to_mwh(gigajoule) -> float: + """Convert GJ to MWh using the conversion value.""" + return round(gigajoule * GJ_TO_MWH, 5) diff --git a/homeassistant/components/landisgyr_heat_meter/strings.json b/homeassistant/components/landisgyr_heat_meter/strings.json new file mode 100644 index 00000000000..61e170af2b3 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "device": "Select device" + } + }, + "setup_serial_manual_path": { + "data": { + "device": "[%key:common::config_flow::data::usb_path%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/landisgyr_heat_meter/translations/en.json b/homeassistant/components/landisgyr_heat_meter/translations/en.json new file mode 100644 index 00000000000..6915e8cb36a --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "device": "Select device" + } + }, + "setup_serial_manual_path": { + "data": { + "device": "USB-device path" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 42b426b8864..2ce09bfcafa 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -197,6 +197,7 @@ FLOWS = { "kulersky", "lacrosse_view", "lametric", + "landisgyr_heat_meter", "launch_library", "laundrify", "lg_soundbar", diff --git a/requirements_all.txt b/requirements_all.txt index 96dfac44800..ecaaf30280c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2383,6 +2383,9 @@ twitchAPI==2.5.2 # homeassistant.components.ukraine_alarm uasiren==0.0.1 +# homeassistant.components.landisgyr_heat_meter +ultraheat-api==0.4.1 + # homeassistant.components.unifiprotect unifi-discovery==1.1.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2db72263205..2b72beee027 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1614,6 +1614,9 @@ twitchAPI==2.5.2 # homeassistant.components.ukraine_alarm uasiren==0.0.1 +# homeassistant.components.landisgyr_heat_meter +ultraheat-api==0.4.1 + # homeassistant.components.unifiprotect unifi-discovery==1.1.5 diff --git a/tests/components/landisgyr_heat_meter/__init__.py b/tests/components/landisgyr_heat_meter/__init__.py new file mode 100644 index 00000000000..0ee6eb22510 --- /dev/null +++ b/tests/components/landisgyr_heat_meter/__init__.py @@ -0,0 +1 @@ +"""Tests for the Landis+Gyr Heat Meter component.""" diff --git a/tests/components/landisgyr_heat_meter/test_config_flow.py b/tests/components/landisgyr_heat_meter/test_config_flow.py new file mode 100644 index 00000000000..b51d4493879 --- /dev/null +++ b/tests/components/landisgyr_heat_meter/test_config_flow.py @@ -0,0 +1,227 @@ +"""Test the Landis + Gyr Heat Meter config flow.""" +from dataclasses import dataclass +from unittest.mock import MagicMock, patch + +import serial.tools.list_ports + +from homeassistant import config_entries +from homeassistant.components.landisgyr_heat_meter import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + + +def mock_serial_port(): + """Mock of a serial port.""" + port = serial.tools.list_ports_common.ListPortInfo("/dev/ttyUSB1234") + port.serial_number = "1234" + port.manufacturer = "Virtual serial port" + port.device = "/dev/ttyUSB1234" + port.description = "Some serial port" + + return port + + +@dataclass +class MockUltraheatRead: + """Mock of the response from the read method of the Ultraheat API.""" + + model: str + device_number: str + + +@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +async def test_manual_entry(mock_heat_meter, hass: HomeAssistant) -> None: + """Test manual entry.""" + + mock_heat_meter().read.return_value = MockUltraheatRead("LUGCUH50", "123456789") + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": "Enter Manually"} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "setup_serial_manual_path" + assert result["errors"] == {} + + with patch( + "homeassistant.components.landisgyr_heat_meter.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": "/dev/ttyUSB0"} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "LUGCUH50" + assert result["data"] == { + "device": "/dev/ttyUSB0", + "model": "LUGCUH50", + "device_number": "123456789", + } + + +@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +@patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()]) +async def test_list_entry(mock_port, mock_heat_meter, hass: HomeAssistant) -> None: + """Test select from list entry.""" + + mock_heat_meter().read.return_value = MockUltraheatRead("LUGCUH50", "123456789") + port = mock_serial_port() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": port.device} + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "LUGCUH50" + assert result["data"] == { + "device": port.device, + "model": "LUGCUH50", + "device_number": "123456789", + } + + +@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +async def test_manual_entry_fail(mock_heat_meter, hass: HomeAssistant) -> None: + """Test manual entry fails.""" + + mock_heat_meter().read.side_effect = Exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": "Enter Manually"} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "setup_serial_manual_path" + assert result["errors"] == {} + + with patch( + "homeassistant.components.landisgyr_heat_meter.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": "/dev/ttyUSB0"} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "setup_serial_manual_path" + assert result["errors"] == {"base": "cannot_connect"} + + +@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +@patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()]) +async def test_list_entry_fail(mock_port, mock_heat_meter, hass: HomeAssistant) -> None: + """Test select from list entry fails.""" + + mock_heat_meter().read.side_effect = Exception + port = mock_serial_port() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": port.device} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +@patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()]) +async def test_get_serial_by_id_realpath( + mock_port, mock_heat_meter, hass: HomeAssistant +) -> None: + """Test getting the serial path name.""" + + mock_heat_meter().read.return_value = MockUltraheatRead("LUGCUH50", "123456789") + port = mock_serial_port() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + scandir = [MagicMock(), MagicMock()] + scandir[0].path = "/dev/ttyUSB1234" + scandir[0].is_symlink.return_value = True + scandir[1].path = "/dev/ttyUSB5678" + scandir[1].is_symlink.return_value = True + + with patch("os.path") as path: + with patch("os.scandir", return_value=scandir): + path.isdir.return_value = True + path.realpath.side_effect = ["/dev/ttyUSB1234", "/dev/ttyUSB5678"] + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": port.device} + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "LUGCUH50" + assert result["data"] == { + "device": port.device, + "model": "LUGCUH50", + "device_number": "123456789", + } + + +@patch("homeassistant.components.landisgyr_heat_meter.config_flow.HeatMeterService") +@patch("serial.tools.list_ports.comports", return_value=[mock_serial_port()]) +async def test_get_serial_by_id_dev_path( + mock_port, mock_heat_meter, hass: HomeAssistant +) -> None: + """Test getting the serial path name with no realpath result.""" + + mock_heat_meter().read.return_value = MockUltraheatRead("LUGCUH50", "123456789") + port = mock_serial_port() + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + scandir = [MagicMock()] + scandir[0].path.return_value = "/dev/serial/by-id/USB5678" + scandir[0].is_symlink.return_value = True + + with patch("os.path") as path: + with patch("os.scandir", return_value=scandir): + path.isdir.return_value = True + path.realpath.side_effect = ["/dev/ttyUSB5678"] + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"device": port.device} + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "LUGCUH50" + assert result["data"] == { + "device": port.device, + "model": "LUGCUH50", + "device_number": "123456789", + } diff --git a/tests/components/landisgyr_heat_meter/test_init.py b/tests/components/landisgyr_heat_meter/test_init.py new file mode 100644 index 00000000000..b3630fc4872 --- /dev/null +++ b/tests/components/landisgyr_heat_meter/test_init.py @@ -0,0 +1,22 @@ +"""Test the Landis + Gyr Heat Meter init.""" + +from homeassistant.const import CONF_DEVICE + +from tests.common import MockConfigEntry + + +async def test_unload_entry(hass): + """Test removing config entry.""" + entry = MockConfigEntry( + domain="landisgyr_heat_meter", + title="LUGCUH50", + data={CONF_DEVICE: "/dev/1234"}, + ) + + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert "landisgyr_heat_meter" in hass.config.components + + assert await hass.config_entries.async_remove(entry.entry_id) diff --git a/tests/components/landisgyr_heat_meter/test_sensor.py b/tests/components/landisgyr_heat_meter/test_sensor.py new file mode 100644 index 00000000000..505efb446b8 --- /dev/null +++ b/tests/components/landisgyr_heat_meter/test_sensor.py @@ -0,0 +1,200 @@ +"""The tests for the Landis+Gyr Heat Meter sensor platform.""" +from dataclasses import dataclass +import datetime +from unittest.mock import patch + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.components.landisgyr_heat_meter.const import DOMAIN +from homeassistant.components.sensor import ( + ATTR_LAST_RESET, + ATTR_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + ENERGY_MEGA_WATT_HOUR, + VOLUME_CUBIC_METERS, +) +from homeassistant.core import CoreState, State +from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity import EntityCategory +from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util + +from tests.common import MockConfigEntry, mock_restore_cache_with_extra_data + + +@dataclass +class MockHeatMeterResponse: + """Mock for HeatMeterResponse.""" + + heat_usage_gj: int + volume_usage_m3: int + heat_previous_year_gj: int + device_number: str + meter_date_time: datetime.datetime + + +@patch("homeassistant.components.landisgyr_heat_meter.HeatMeterService") +async def test_create_sensors(mock_heat_meter, hass): + """Test sensor.""" + entry_data = { + "device": "/dev/USB0", + "model": "LUGCUH50", + "device_number": "123456789", + } + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + mock_heat_meter_response = MockHeatMeterResponse( + heat_usage_gj=123, + volume_usage_m3=456, + heat_previous_year_gj=111, + device_number="devicenr_789", + meter_date_time=dt_util.as_utc(datetime.datetime(2022, 5, 19, 19, 41, 17)), + ) + + mock_heat_meter().read.return_value = mock_heat_meter_response + + await hass.config_entries.async_setup(mock_entry.entry_id) + await async_setup_component(hass, HA_DOMAIN, {}) + await hass.async_block_till_done() + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: "sensor.heat_meter_heat_usage"}, + blocking=True, + ) + await hass.async_block_till_done() + + # check if 26 attributes have been created + assert len(hass.states.async_all()) == 26 + entity_reg = entity_registry.async_get(hass) + + state = hass.states.get("sensor.heat_meter_heat_usage") + assert state + assert state.state == "34.16669" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_MEGA_WATT_HOUR + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY + + state = hass.states.get("sensor.heat_meter_volume_usage") + assert state + assert state.state == "456" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL + + state = hass.states.get("sensor.heat_meter_device_number") + assert state + assert state.state == "devicenr_789" + assert state.attributes.get(ATTR_STATE_CLASS) is None + entity_registry_entry = entity_reg.async_get("sensor.heat_meter_device_number") + assert entity_registry_entry.entity_category == EntityCategory.DIAGNOSTIC + + state = hass.states.get("sensor.heat_meter_meter_date_time") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:clock-outline" + assert state.attributes.get(ATTR_STATE_CLASS) is None + entity_registry_entry = entity_reg.async_get("sensor.heat_meter_meter_date_time") + assert entity_registry_entry.entity_category == EntityCategory.DIAGNOSTIC + + +@patch("homeassistant.components.landisgyr_heat_meter.HeatMeterService") +async def test_restore_state(mock_heat_meter, hass): + """Test sensor restore state.""" + # Home assistant is not running yet + hass.state = CoreState.not_running + last_reset = "2022-07-01T00:00:00.000000+00:00" + mock_restore_cache_with_extra_data( + hass, + [ + ( + State( + "sensor.heat_meter_heat_usage", + "34167", + attributes={ + ATTR_LAST_RESET: last_reset, + ATTR_UNIT_OF_MEASUREMENT: ENERGY_MEGA_WATT_HOUR, + ATTR_STATE_CLASS: SensorStateClass.TOTAL, + }, + ), + { + "native_value": 34167, + "native_unit_of_measurement": ENERGY_MEGA_WATT_HOUR, + "icon": "mdi:fire", + "last_reset": last_reset, + }, + ), + ( + State( + "sensor.heat_meter_volume_usage", + "456", + attributes={ + ATTR_LAST_RESET: last_reset, + ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, + ATTR_STATE_CLASS: SensorStateClass.TOTAL, + }, + ), + { + "native_value": 456, + "native_unit_of_measurement": VOLUME_CUBIC_METERS, + "icon": "mdi:fire", + "last_reset": last_reset, + }, + ), + ( + State( + "sensor.heat_meter_device_number", + "devicenr_789", + attributes={ + ATTR_LAST_RESET: last_reset, + }, + ), + { + "native_value": "devicenr_789", + "native_unit_of_measurement": None, + "last_reset": last_reset, + }, + ), + ], + ) + entry_data = { + "device": "/dev/USB0", + "model": "LUGCUH50", + "device_number": "123456789", + } + + # create and add entry + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data) + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await async_setup_component(hass, HA_DOMAIN, {}) + await hass.async_block_till_done() + + # restore from cache + state = hass.states.get("sensor.heat_meter_heat_usage") + assert state + assert state.state == "34167" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_MEGA_WATT_HOUR + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL + + state = hass.states.get("sensor.heat_meter_volume_usage") + assert state + assert state.state == "456" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL + + state = hass.states.get("sensor.heat_meter_device_number") + assert state + print("STATE IS: ", state) + assert state.state == "devicenr_789" + assert state.attributes.get(ATTR_STATE_CLASS) is None From 88a5b90489af4ab98db382adaaefba41f4249b6f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 18 Aug 2022 16:52:41 +0200 Subject: [PATCH 3448/3516] Minor improvement of zha test (#76993) --- tests/components/zha/test_config_flow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 84290595f12..9a98b5e0caa 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -775,6 +775,7 @@ async def test_migration_ti_cc_to_znp(old_type, new_type, hass, config_entry): async def test_hardware_not_onboarded(hass): """Test hardware flow.""" data = { + "name": "Yellow", "radio_type": "efr32", "port": { "path": "/dev/ttyAMA1", @@ -790,7 +791,7 @@ async def test_hardware_not_onboarded(hass): ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "/dev/ttyAMA1" + assert result["title"] == "Yellow" assert result["data"] == { CONF_DEVICE: { CONF_BAUDRATE: 115200, From 21ebd1f612a42c72b506c32d5dc416a54611c382 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 18 Aug 2022 16:58:44 +0200 Subject: [PATCH 3449/3516] Simplify ZHA config entry title (#76991) --- homeassistant/components/zha/config_flow.py | 2 +- tests/components/zha/test_config_flow.py | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 69da95e8528..94723e38d58 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -128,7 +128,7 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_zha_device") self._device_path = dev_path - self._title = usb.human_readable_device_name( + self._title = description or usb.human_readable_device_name( dev_path, serial_number, manufacturer, diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 9a98b5e0caa..a769303a4c4 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -237,7 +237,7 @@ async def test_discovery_via_usb(detect_mock, hass): await hass.async_block_till_done() assert result2["type"] == FlowResultType.CREATE_ENTRY - assert "zigbee radio" in result2["title"] + assert result2["title"] == "zigbee radio" assert result2["data"] == { "device": { "baudrate": 115200, @@ -273,10 +273,7 @@ async def test_zigate_discovery_via_usb(detect_mock, hass): await hass.async_block_till_done() assert result2["type"] == FlowResultType.CREATE_ENTRY - assert ( - "zigate radio - /dev/ttyZIGBEE, s/n: 1234 - test - 6015:0403" - in result2["title"] - ) + assert result2["title"] == "zigate radio" assert result2["data"] == { "device": { "path": "/dev/ttyZIGBEE", From 92a9011953866ace16f07e06a0b17be2841f91f6 Mon Sep 17 00:00:00 2001 From: IceBotYT <34712694+IceBotYT@users.noreply.github.com> Date: Thu, 18 Aug 2022 11:17:58 -0400 Subject: [PATCH 3450/3516] Code quality changes for LaCrosse View (#76265) --- .../components/lacrosse_view/__init__.py | 54 ++------ .../components/lacrosse_view/config_flow.py | 44 +++++-- .../components/lacrosse_view/coordinator.py | 79 ++++++++++++ .../components/lacrosse_view/sensor.py | 122 +++++++++++------- .../components/lacrosse_view/strings.json | 3 +- .../lacrosse_view/translations/en.json | 3 +- tests/components/lacrosse_view/__init__.py | 11 ++ .../lacrosse_view/test_config_flow.py | 121 ++++++++++++++++- tests/components/lacrosse_view/test_init.py | 45 ++++++- tests/components/lacrosse_view/test_sensor.py | 32 ++++- 10 files changed, 401 insertions(+), 113 deletions(-) create mode 100644 homeassistant/components/lacrosse_view/coordinator.py diff --git a/homeassistant/components/lacrosse_view/__init__.py b/homeassistant/components/lacrosse_view/__init__.py index 0d3147f43a5..46239485eb3 100644 --- a/homeassistant/components/lacrosse_view/__init__.py +++ b/homeassistant/components/lacrosse_view/__init__.py @@ -1,18 +1,16 @@ """The LaCrosse View integration.""" from __future__ import annotations -from datetime import datetime, timedelta - -from lacrosse_view import LaCrosse, Location, LoginError, Sensor +from lacrosse_view import LaCrosse, LoginError from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, LOGGER, SCAN_INTERVAL +from .const import DOMAIN +from .coordinator import LaCrosseUpdateCoordinator PLATFORMS: list[Platform] = [Platform.SENSOR] @@ -20,52 +18,22 @@ PLATFORMS: list[Platform] = [Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up LaCrosse View from a config entry.""" - async def get_data() -> list[Sensor]: - """Get the data from the LaCrosse View.""" - now = datetime.utcnow() - - if hass.data[DOMAIN][entry.entry_id]["last_update"] < now - timedelta( - minutes=59 - ): # Get new token - hass.data[DOMAIN][entry.entry_id]["last_update"] = now - await api.login(entry.data["username"], entry.data["password"]) - - # Get the timestamp for yesterday at 6 PM (this is what is used in the app, i noticed it when proxying the request) - yesterday = now - timedelta(days=1) - yesterday = yesterday.replace(hour=18, minute=0, second=0, microsecond=0) - yesterday_timestamp = datetime.timestamp(yesterday) - - return await api.get_sensors( - location=Location(id=entry.data["id"], name=entry.data["name"]), - tz=hass.config.time_zone, - start=str(int(yesterday_timestamp)), - end=str(int(datetime.timestamp(now))), - ) - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { - "api": LaCrosse(async_get_clientsession(hass)), - "last_update": datetime.utcnow(), - } - api: LaCrosse = hass.data[DOMAIN][entry.entry_id]["api"] + api = LaCrosse(async_get_clientsession(hass)) try: await api.login(entry.data["username"], entry.data["password"]) except LoginError as error: - raise ConfigEntryNotReady from error + raise ConfigEntryAuthFailed from error - coordinator = DataUpdateCoordinator( - hass, - LOGGER, - name="LaCrosse View", - update_method=get_data, - update_interval=timedelta(seconds=SCAN_INTERVAL), - ) + coordinator = LaCrosseUpdateCoordinator(hass, api, entry) await coordinator.async_config_entry_first_refresh() - hass.data[DOMAIN][entry.entry_id]["coordinator"] = coordinator + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + "coordinator": coordinator, + } - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/lacrosse_view/config_flow.py b/homeassistant/components/lacrosse_view/config_flow.py index b5b89828e9b..2b694860bc8 100644 --- a/homeassistant/components/lacrosse_view/config_flow.py +++ b/homeassistant/components/lacrosse_view/config_flow.py @@ -1,7 +1,7 @@ """Config flow for LaCrosse View integration.""" from __future__ import annotations -import logging +from collections.abc import Mapping from typing import Any from lacrosse_view import LaCrosse, Location, LoginError @@ -13,9 +13,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN, LOGGER STEP_USER_DATA_SCHEMA = vol.Schema( { @@ -47,8 +45,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for LaCrosse View.""" VERSION = 1 - data: dict[str, str] = {} - locations: list[Location] = [] + + def __init__(self) -> None: + """Initialize the config flow.""" + self.data: dict[str, str] = {} + self.locations: list[Location] = [] + self._reauth_entry: config_entries.ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -68,11 +70,19 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except NoLocations: errors["base"] = "no_locations" except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") + LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: self.data = user_input self.locations = info + + # Check if we are reauthenticating + if self._reauth_entry is not None: + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=self._reauth_entry.data | self.data + ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") return await self.async_step_location() return self.async_show_form( @@ -98,11 +108,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): location_id = user_input["location"] - for location in self.locations: - if location.id == location_id: - location_name = location.name + location_name = next( + location.name for location in self.locations if location.id == location_id + ) await self.async_set_unique_id(location_id) + self._abort_if_unique_id_configured() return self.async_create_entry( @@ -115,9 +126,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): }, ) - -class CannotConnect(HomeAssistantError): - """Error to indicate we cannot connect.""" + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Reauth in case of a password change or other error.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_user() class InvalidAuth(HomeAssistantError): @@ -126,3 +140,7 @@ class InvalidAuth(HomeAssistantError): class NoLocations(HomeAssistantError): """Error to indicate there are no locations.""" + + +class NonExistentEntry(HomeAssistantError): + """Error to indicate that the entry does not exist when it should.""" diff --git a/homeassistant/components/lacrosse_view/coordinator.py b/homeassistant/components/lacrosse_view/coordinator.py new file mode 100644 index 00000000000..5361f94d04f --- /dev/null +++ b/homeassistant/components/lacrosse_view/coordinator.py @@ -0,0 +1,79 @@ +"""DataUpdateCoordinator for LaCrosse View.""" +from __future__ import annotations + +from datetime import datetime, timedelta + +from lacrosse_view import HTTPError, LaCrosse, Location, LoginError, Sensor + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import LOGGER, SCAN_INTERVAL + + +class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]): + """DataUpdateCoordinator for LaCrosse View.""" + + username: str + password: str + name: str + id: str + hass: HomeAssistant + + def __init__( + self, + hass: HomeAssistant, + api: LaCrosse, + entry: ConfigEntry, + ) -> None: + """Initialize DataUpdateCoordinator for LaCrosse View.""" + self.api = api + self.last_update = datetime.utcnow() + self.username = entry.data["username"] + self.password = entry.data["password"] + self.hass = hass + self.name = entry.data["name"] + self.id = entry.data["id"] + super().__init__( + hass, + LOGGER, + name="LaCrosse View", + update_interval=timedelta(seconds=SCAN_INTERVAL), + ) + + async def _async_update_data(self) -> list[Sensor]: + """Get the data for LaCrosse View.""" + now = datetime.utcnow() + + if self.last_update < now - timedelta(minutes=59): # Get new token + self.last_update = now + try: + await self.api.login(self.username, self.password) + except LoginError as error: + raise ConfigEntryAuthFailed from error + + # Get the timestamp for yesterday at 6 PM (this is what is used in the app, i noticed it when proxying the request) + yesterday = now - timedelta(days=1) + yesterday = yesterday.replace(hour=18, minute=0, second=0, microsecond=0) + yesterday_timestamp = datetime.timestamp(yesterday) + + try: + sensors = await self.api.get_sensors( + location=Location(id=self.id, name=self.name), + tz=self.hass.config.time_zone, + start=str(int(yesterday_timestamp)), + end=str(int(datetime.timestamp(now))), + ) + except HTTPError as error: + raise ConfigEntryNotReady from error + + # Verify that we have permission to read the sensors + for sensor in sensors: + if not sensor.permissions.get("read", False): + raise ConfigEntryAuthFailed( + f"This account does not have permission to read {sensor.name}" + ) + + return sensors diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py index 8ccbe0514b4..46c4671a109 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -3,16 +3,23 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from re import sub from lacrosse_view import Sensor from homeassistant.components.sensor import ( + SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + PERCENTAGE, + PRECIPITATION_INCHES, + SPEED_KILOMETERS_PER_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( @@ -27,7 +34,7 @@ from .const import DOMAIN, LOGGER class LaCrosseSensorEntityDescriptionMixin: """Mixin for required keys.""" - value_fn: Callable[..., float] + value_fn: Callable[[Sensor, str], float] @dataclass @@ -37,20 +44,51 @@ class LaCrosseSensorEntityDescription( """Description for LaCrosse View sensor.""" +def get_value(sensor: Sensor, field: str) -> float: + """Get the value of a sensor field.""" + return float(sensor.data[field]["values"][-1]["s"]) + + PARALLEL_UPDATES = 0 -ICON_LIST = { - "Temperature": "mdi:thermometer", - "Humidity": "mdi:water-percent", - "HeatIndex": "mdi:thermometer", - "WindSpeed": "mdi:weather-windy", - "Rain": "mdi:water", -} -UNIT_LIST = { - "degrees_celsius": "°C", - "degrees_fahrenheit": "°F", - "relative_humidity": "%", - "kilometers_per_hour": "km/h", - "inches": "in", +SENSOR_DESCRIPTIONS = { + "Temperature": LaCrosseSensorEntityDescription( + key="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + name="Temperature", + state_class=SensorStateClass.MEASUREMENT, + value_fn=get_value, + native_unit_of_measurement=TEMP_CELSIUS, + ), + "Humidity": LaCrosseSensorEntityDescription( + key="Humidity", + device_class=SensorDeviceClass.HUMIDITY, + name="Humidity", + state_class=SensorStateClass.MEASUREMENT, + value_fn=get_value, + native_unit_of_measurement=PERCENTAGE, + ), + "HeatIndex": LaCrosseSensorEntityDescription( + key="HeatIndex", + device_class=SensorDeviceClass.TEMPERATURE, + name="Heat Index", + state_class=SensorStateClass.MEASUREMENT, + value_fn=get_value, + native_unit_of_measurement=TEMP_FAHRENHEIT, + ), + "WindSpeed": LaCrosseSensorEntityDescription( + key="WindSpeed", + name="Wind Speed", + state_class=SensorStateClass.MEASUREMENT, + value_fn=get_value, + native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + ), + "Rain": LaCrosseSensorEntityDescription( + key="Rain", + name="Rain", + state_class=SensorStateClass.MEASUREMENT, + value_fn=get_value, + native_unit_of_measurement=PRECIPITATION_INCHES, + ), } @@ -66,35 +104,26 @@ async def async_setup_entry( sensors: list[Sensor] = coordinator.data sensor_list = [] - for i, sensor in enumerate(sensors): - if not sensor.permissions.get("read"): - LOGGER.warning( - "No permission to read sensor %s, are you sure you're signed into the right account?", - sensor.name, - ) - continue + for sensor in sensors: for field in sensor.sensor_field_names: + description = SENSOR_DESCRIPTIONS.get(field) + if description is None: + message = ( + f"Unsupported sensor field: {field}\nPlease create an issue on " + "GitHub. https://github.com/home-assistant/core/issues/new?assignees=&la" + "bels=&template=bug_report.yml&integration_name=LaCrosse%20View&integrat" + "ion_link=https://www.home-assistant.io/integrations/lacrosse_view/&addi" + f"tional_information=Field:%20{field}%0ASensor%20Model:%20{sensor.model}&" + f"title=LaCrosse%20View%20Unsupported%20sensor%20field:%20{field}" + ) + + LOGGER.warning(message) + continue sensor_list.append( LaCrosseViewSensor( coordinator=coordinator, - description=LaCrosseSensorEntityDescription( - key=str(i), - device_class="temperature" if field == "Temperature" else None, - # The regex is to convert CamelCase to Human Case - # e.g. "RelativeHumidity" -> "Relative Humidity" - name=f"{sensor.name} {sub(r'(? None: """Initialize.""" super().__init__(coordinator) - sensor = self.coordinator.data[int(description.key)] self.entity_description = description - self._attr_unique_id = f"{sensor.location.id}-{description.key}-{field}" + self._attr_unique_id = f"{sensor.sensor_id}-{description.key}" self._attr_name = f"{sensor.location.name} {description.name}" - self._attr_icon = ICON_LIST.get(field, "mdi:thermometer") self._attr_device_info = { "identifiers": {(DOMAIN, sensor.sensor_id)}, "name": sensor.name.split(" ")[0], @@ -129,8 +156,11 @@ class LaCrosseViewSensor( "model": sensor.model, "via_device": (DOMAIN, sensor.location.id), } + self._sensor = sensor @property - def native_value(self) -> float: + def native_value(self) -> float | str: """Return the sensor value.""" - return self.entity_description.value_fn() + return self.entity_description.value_fn( + self._sensor, self.entity_description.key + ) diff --git a/homeassistant/components/lacrosse_view/strings.json b/homeassistant/components/lacrosse_view/strings.json index 76f1971518a..160517793d8 100644 --- a/homeassistant/components/lacrosse_view/strings.json +++ b/homeassistant/components/lacrosse_view/strings.json @@ -14,7 +14,8 @@ "no_locations": "No locations found" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } diff --git a/homeassistant/components/lacrosse_view/translations/en.json b/homeassistant/components/lacrosse_view/translations/en.json index a2a7fd23272..9fc180b0754 100644 --- a/homeassistant/components/lacrosse_view/translations/en.json +++ b/homeassistant/components/lacrosse_view/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Device is already configured" + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "invalid_auth": "Invalid authentication", diff --git a/tests/components/lacrosse_view/__init__.py b/tests/components/lacrosse_view/__init__.py index ea01e7a72e3..bd4ccb17b17 100644 --- a/tests/components/lacrosse_view/__init__.py +++ b/tests/components/lacrosse_view/__init__.py @@ -30,3 +30,14 @@ TEST_NO_PERMISSION_SENSOR = Sensor( permissions={"read": False}, model="Test", ) +TEST_UNSUPPORTED_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["SomeUnsupportedField"], + location=Location(id="1", name="Test"), + data={"SomeUnsupportedField": {"values": [{"s": "2"}], "unit": "degrees_celsius"}}, + permissions={"read": True}, + model="Test", +) diff --git a/tests/components/lacrosse_view/test_config_flow.py b/tests/components/lacrosse_view/test_config_flow.py index dc55f02bff8..8325cec9209 100644 --- a/tests/components/lacrosse_view/test_config_flow.py +++ b/tests/components/lacrosse_view/test_config_flow.py @@ -6,7 +6,9 @@ from lacrosse_view import Location, LoginError from homeassistant import config_entries from homeassistant.components.lacrosse_view.const import DOMAIN from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, FlowResultType +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry async def test_form(hass: HomeAssistant) -> None: @@ -20,6 +22,8 @@ async def test_form(hass: HomeAssistant) -> None: with patch("lacrosse_view.LaCrosse.login", return_value=True,), patch( "lacrosse_view.LaCrosse.get_locations", return_value=[Location(id=1, name="Test")], + ), patch( + "homeassistant.components.lacrosse_view.async_setup_entry", return_value=True ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -46,7 +50,7 @@ async def test_form(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == FlowResultType.CREATE_ENTRY assert result3["title"] == "Test" assert result3["data"] == { "username": "test-username", @@ -161,3 +165,116 @@ async def test_form_unexpected_error(hass: HomeAssistant) -> None: assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "unknown"} + + +async def test_already_configured_device(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "username": "test-username", + "password": "test-password", + "id": "1", + "name": "Test", + }, + unique_id="1", + ) + mock_config_entry.add_to_hass(hass) + + # Now that we did the config once, let's try to do it again, this should raise the abort for already configured device + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch("lacrosse_view.LaCrosse.login", return_value=True,), patch( + "lacrosse_view.LaCrosse.get_locations", + return_value=[Location(id=1, name="Test")], + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "location" + assert result2["errors"] is None + + with patch( + "homeassistant.components.lacrosse_view.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + "location": "1", + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.ABORT + assert result3["reason"] == "already_configured" + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_reauth(hass: HomeAssistant) -> None: + """Test reauthentication.""" + data = { + "username": "test-username", + "password": "test-password", + "id": "1", + "name": "Test", + } + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data=data, + unique_id="1", + title="Test", + ) + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + "title_placeholders": {"name": mock_config_entry.title}, + "unique_id": mock_config_entry.unique_id, + }, + data=data, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + new_username = "new-username" + new_password = "new-password" + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_locations", + return_value=[Location(id=1, name="Test")], + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": new_username, + "password": new_password, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + + assert len(hass.config_entries.async_entries()) == 1 + assert hass.config_entries.async_entries()[0].data == { + "username": new_username, + "password": new_password, + "id": "1", + "name": "Test", + } diff --git a/tests/components/lacrosse_view/test_init.py b/tests/components/lacrosse_view/test_init.py index a719536f737..600fe1c9d24 100644 --- a/tests/components/lacrosse_view/test_init.py +++ b/tests/components/lacrosse_view/test_init.py @@ -47,11 +47,14 @@ async def test_login_error(hass: HomeAssistant) -> None: assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN] entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 - assert entries[0].state == ConfigEntryState.SETUP_RETRY + assert entries[0].state == ConfigEntryState.SETUP_ERROR + flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) + assert flows + assert len(flows) == 1 + assert flows[0]["context"]["source"] == "reauth" async def test_http_error(hass: HomeAssistant) -> None: @@ -65,7 +68,6 @@ async def test_http_error(hass: HomeAssistant) -> None: assert not await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN] entries = hass.config_entries.async_entries(DOMAIN) assert entries assert len(entries) == 1 @@ -100,3 +102,40 @@ async def test_new_token(hass: HomeAssistant) -> None: await hass.async_block_till_done() login.assert_called_once() + + +async def test_failed_token(hass: HomeAssistant) -> None: + """Test if a reauth flow occurs when token refresh fails.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True) as login, patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_SENSOR], + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + login.assert_called_once() + + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + + one_hour_after = datetime.utcnow() + timedelta(hours=1) + + with patch( + "lacrosse_view.LaCrosse.login", side_effect=LoginError("Test") + ), freeze_time(one_hour_after): + async_fire_time_changed(hass, one_hour_after) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress_by_handler(DOMAIN) + assert flows + assert len(flows) == 1 + assert flows[0]["context"]["source"] == "reauth" diff --git a/tests/components/lacrosse_view/test_sensor.py b/tests/components/lacrosse_view/test_sensor.py index 57197662cc9..0e102c2f3ef 100644 --- a/tests/components/lacrosse_view/test_sensor.py +++ b/tests/components/lacrosse_view/test_sensor.py @@ -5,7 +5,12 @@ from homeassistant.components.lacrosse_view import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant -from . import MOCK_ENTRY_DATA, TEST_NO_PERMISSION_SENSOR, TEST_SENSOR +from . import ( + MOCK_ENTRY_DATA, + TEST_NO_PERMISSION_SENSOR, + TEST_SENSOR, + TEST_UNSUPPORTED_SENSOR, +) from tests.common import MockConfigEntry @@ -26,7 +31,7 @@ async def test_entities_added(hass: HomeAssistant) -> None: assert entries assert len(entries) == 1 assert entries[0].state == ConfigEntryState.LOADED - assert hass.states.get("sensor.test_test_temperature") + assert hass.states.get("sensor.test_temperature") async def test_sensor_permission(hass: HomeAssistant, caplog) -> None: @@ -36,6 +41,25 @@ async def test_sensor_permission(hass: HomeAssistant, caplog) -> None: with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( "lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_NO_PERMISSION_SENSOR] + ): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.SETUP_ERROR + assert not hass.states.get("sensor.test_temperature") + assert "This account does not have permission to read Test" in caplog.text + + +async def test_field_not_supported(hass: HomeAssistant, caplog) -> None: + """Test if it raises a warning when the field is not supported.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_sensors", return_value=[TEST_UNSUPPORTED_SENSOR] ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -45,5 +69,5 @@ async def test_sensor_permission(hass: HomeAssistant, caplog) -> None: assert entries assert len(entries) == 1 assert entries[0].state == ConfigEntryState.LOADED - assert hass.states.get("sensor.test_test_temperature") is None - assert "No permission to read sensor" in caplog.text + assert hass.states.get("sensor.test_some_unsupported_field") is None + assert "Unsupported sensor field" in caplog.text From 6e92931087b27b04854d4a806136abc52a15c9d3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 18 Aug 2022 12:02:12 -0400 Subject: [PATCH 3451/3516] Add file selector and file upload integration (#76672) --- .strict-typing | 1 + CODEOWNERS | 2 + .../components/file_upload/__init__.py | 182 ++++++++++++++++++ .../components/file_upload/manifest.json | 8 + .../components/frontend/manifest.json | 1 + homeassistant/helpers/selector.py | 40 +++- mypy.ini | 10 + script/hassfest/manifest.py | 1 + tests/components/file_upload/__init__.py | 1 + tests/components/file_upload/test_init.py | 66 +++++++ tests/components/image/__init__.py | 3 + tests/components/image/test_init.py | 7 +- tests/helpers/test_selector.py | 16 ++ 13 files changed, 332 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/file_upload/__init__.py create mode 100644 homeassistant/components/file_upload/manifest.json create mode 100644 tests/components/file_upload/__init__.py create mode 100644 tests/components/file_upload/test_init.py diff --git a/.strict-typing b/.strict-typing index d9cc4ffb55a..f8a0579433b 100644 --- a/.strict-typing +++ b/.strict-typing @@ -98,6 +98,7 @@ homeassistant.components.energy.* homeassistant.components.evil_genius_labs.* homeassistant.components.fan.* homeassistant.components.fastdotcom.* +homeassistant.components.file_upload.* homeassistant.components.filesize.* homeassistant.components.fitbit.* homeassistant.components.flunearyou.* diff --git a/CODEOWNERS b/CODEOWNERS index 26d1a8da2f8..f952ae22d9b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -329,6 +329,8 @@ build.json @home-assistant/supervisor /tests/components/fibaro/ @rappenze /homeassistant/components/file/ @fabaff /tests/components/file/ @fabaff +/homeassistant/components/file_upload/ @home-assistant/core +/tests/components/file_upload/ @home-assistant/core /homeassistant/components/filesize/ @gjohansson-ST /tests/components/filesize/ @gjohansson-ST /homeassistant/components/filter/ @dgomes diff --git a/homeassistant/components/file_upload/__init__.py b/homeassistant/components/file_upload/__init__.py new file mode 100644 index 00000000000..9f548e14459 --- /dev/null +++ b/homeassistant/components/file_upload/__init__.py @@ -0,0 +1,182 @@ +"""The File Upload integration.""" +from __future__ import annotations + +import asyncio +from collections.abc import Iterator +from contextlib import contextmanager +from dataclasses import dataclass +from pathlib import Path +import shutil +import tempfile + +from aiohttp import web +import voluptuous as vol + +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers.typing import ConfigType +from homeassistant.util import raise_if_invalid_filename +from homeassistant.util.ulid import ulid_hex + +DOMAIN = "file_upload" + +# If increased, change upload view to streaming +# https://docs.aiohttp.org/en/stable/web_quickstart.html#file-uploads +MAX_SIZE = 1024 * 1024 * 10 +TEMP_DIR_NAME = f"home-assistant-{DOMAIN}" + + +@contextmanager +def process_uploaded_file(hass: HomeAssistant, file_id: str) -> Iterator[Path]: + """Get an uploaded file. + + File is removed at the end of the context. + """ + if DOMAIN not in hass.data: + raise ValueError("File does not exist") + + file_upload_data: FileUploadData = hass.data[DOMAIN] + + if not file_upload_data.has_file(file_id): + raise ValueError("File does not exist") + + try: + yield file_upload_data.file_path(file_id) + finally: + file_upload_data.files.pop(file_id) + shutil.rmtree(file_upload_data.file_dir(file_id)) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up File Upload.""" + hass.http.register_view(FileUploadView) + return True + + +@dataclass(frozen=True) +class FileUploadData: + """File upload data.""" + + temp_dir: Path + files: dict[str, str] + + @classmethod + async def create(cls, hass: HomeAssistant) -> FileUploadData: + """Initialize the file upload data.""" + + def _create_temp_dir() -> Path: + """Create temporary directory.""" + temp_dir = Path(tempfile.gettempdir()) / TEMP_DIR_NAME + + # If it exists, it's an old one and Home Assistant didn't shut down correctly. + if temp_dir.exists(): + shutil.rmtree(temp_dir) + + temp_dir.mkdir(0o700) + return temp_dir + + temp_dir = await hass.async_add_executor_job(_create_temp_dir) + + def cleanup_unused_files(ev: Event) -> None: + """Clean up unused files.""" + shutil.rmtree(temp_dir) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_unused_files) + + return cls(temp_dir, {}) + + def has_file(self, file_id: str) -> bool: + """Return if file exists.""" + return file_id in self.files + + def file_dir(self, file_id: str) -> Path: + """Return the file directory.""" + return self.temp_dir / file_id + + def file_path(self, file_id: str) -> Path: + """Return the file path.""" + return self.file_dir(file_id) / self.files[file_id] + + +class FileUploadView(HomeAssistantView): + """HTTP View to upload files.""" + + url = "/api/file_upload" + name = "api:file_upload" + + _upload_lock: asyncio.Lock | None = None + + @callback + def _get_upload_lock(self) -> asyncio.Lock: + """Get upload lock.""" + if self._upload_lock is None: + self._upload_lock = asyncio.Lock() + + return self._upload_lock + + async def post(self, request: web.Request) -> web.Response: + """Upload a file.""" + async with self._get_upload_lock(): + return await self._upload_file(request) + + async def _upload_file(self, request: web.Request) -> web.Response: + """Handle uploaded file.""" + # Increase max payload + request._client_max_size = MAX_SIZE # pylint: disable=protected-access + + data = await request.post() + file_field = data.get("file") + + if not isinstance(file_field, web.FileField): + raise vol.Invalid("Expected a file") + + try: + raise_if_invalid_filename(file_field.filename) + except ValueError as err: + raise web.HTTPBadRequest from err + + hass: HomeAssistant = request.app["hass"] + file_id = ulid_hex() + + if DOMAIN not in hass.data: + hass.data[DOMAIN] = await FileUploadData.create(hass) + + file_upload_data: FileUploadData = hass.data[DOMAIN] + file_dir = file_upload_data.file_dir(file_id) + + def _sync_work() -> None: + file_dir.mkdir() + + # MyPy forgets about the isinstance check because we're in a function scope + assert isinstance(file_field, web.FileField) + + with (file_dir / file_field.filename).open("wb") as target_fileobj: + shutil.copyfileobj(file_field.file, target_fileobj) + + await hass.async_add_executor_job(_sync_work) + + file_upload_data.files[file_id] = file_field.filename + + return self.json({"file_id": file_id}) + + @RequestDataValidator({vol.Required("file_id"): str}) + async def delete(self, request: web.Request, data: dict[str, str]) -> web.Response: + """Delete a file.""" + hass: HomeAssistant = request.app["hass"] + + if DOMAIN not in hass.data: + raise web.HTTPNotFound() + + file_id = data["file_id"] + file_upload_data: FileUploadData = hass.data[DOMAIN] + + if file_upload_data.files.pop(file_id, None) is None: + raise web.HTTPNotFound() + + await hass.async_add_executor_job( + lambda: shutil.rmtree(file_upload_data.file_dir(file_id)) + ) + + return self.json_message("File deleted") diff --git a/homeassistant/components/file_upload/manifest.json b/homeassistant/components/file_upload/manifest.json new file mode 100644 index 00000000000..6e190ba3712 --- /dev/null +++ b/homeassistant/components/file_upload/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "file_upload", + "name": "File Upload", + "documentation": "https://www.home-assistant.io/integrations/file_upload", + "dependencies": ["http"], + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" +} diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 207b57babb2..f1e6f31fd4b 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -9,6 +9,7 @@ "config", "device_automation", "diagnostics", + "file_upload", "http", "lovelace", "onboarding", diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index ccb7ac67dfb..deacc821672 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Sequence from typing import Any, TypedDict, cast +from uuid import UUID import voluptuous as vol @@ -10,7 +11,7 @@ from homeassistant.backports.enum import StrEnum from homeassistant.const import CONF_MODE, CONF_UNIT_OF_MEASUREMENT from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.util import decorator -from homeassistant.util.yaml.dumper import add_representer, represent_odict +from homeassistant.util.yaml import dumper from . import config_validation as cv @@ -888,9 +889,42 @@ class TimeSelector(Selector): return cast(str, data) -add_representer( +class FileSelectorConfig(TypedDict): + """Class to represent a file selector config.""" + + accept: str # required + + +@SELECTORS.register("file") +class FileSelector(Selector): + """Selector of a file.""" + + selector_type = "file" + + CONFIG_SCHEMA = vol.Schema( + { + # https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept + vol.Required("accept"): str, + } + ) + + def __init__(self, config: FileSelectorConfig | None = None) -> None: + """Instantiate a selector.""" + super().__init__(config) + + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + if not isinstance(data, str): + raise vol.Invalid("Value should be a string") + + UUID(data) + + return data + + +dumper.add_representer( Selector, - lambda dumper, value: represent_odict( + lambda dumper, value: dumper.represent_odict( dumper, "tag:yaml.org,2002:map", value.serialize() ), ) diff --git a/mypy.ini b/mypy.ini index 570004a14dd..051c1065423 100644 --- a/mypy.ini +++ b/mypy.ini @@ -739,6 +739,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.file_upload.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.filesize.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 1e6bd03f457..338682bbe76 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -51,6 +51,7 @@ NO_IOT_CLASS = [ "discovery", "downloader", "ffmpeg", + "file_upload", "frontend", "hardkernel", "hardware", diff --git a/tests/components/file_upload/__init__.py b/tests/components/file_upload/__init__.py new file mode 100644 index 00000000000..2630811ffc5 --- /dev/null +++ b/tests/components/file_upload/__init__.py @@ -0,0 +1 @@ +"""Tests for the File Upload integration.""" diff --git a/tests/components/file_upload/test_init.py b/tests/components/file_upload/test_init.py new file mode 100644 index 00000000000..ba3485c96e1 --- /dev/null +++ b/tests/components/file_upload/test_init.py @@ -0,0 +1,66 @@ +"""Test the File Upload integration.""" +from pathlib import Path +from random import getrandbits +from unittest.mock import patch + +import pytest + +from homeassistant.components import file_upload +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.components.image import TEST_IMAGE + + +@pytest.fixture +async def uploaded_file_dir(hass: HomeAssistant, hass_client) -> Path: + """Test uploading and using a file.""" + assert await async_setup_component(hass, "file_upload", {}) + client = await hass_client() + + with patch( + # Patch temp dir name to avoid tests fail running in parallel + "homeassistant.components.file_upload.TEMP_DIR_NAME", + file_upload.TEMP_DIR_NAME + f"-{getrandbits(10):03x}", + ), TEST_IMAGE.open("rb") as fp: + res = await client.post("/api/file_upload", data={"file": fp}) + + assert res.status == 200 + response = await res.json() + + file_dir = hass.data[file_upload.DOMAIN].file_dir(response["file_id"]) + assert file_dir.is_dir() + return file_dir + + +async def test_using_file(hass: HomeAssistant, uploaded_file_dir): + """Test uploading and using a file.""" + # Test we can use it + with file_upload.process_uploaded_file(hass, uploaded_file_dir.name) as file_path: + assert file_path.is_file() + assert file_path.parent == uploaded_file_dir + assert file_path.read_bytes() == TEST_IMAGE.read_bytes() + + # Test it's removed + assert not uploaded_file_dir.exists() + + +async def test_removing_file(hass: HomeAssistant, hass_client, uploaded_file_dir): + """Test uploading and using a file.""" + client = await hass_client() + + response = await client.delete( + "/api/file_upload", json={"file_id": uploaded_file_dir.name} + ) + assert response.status == 200 + + # Test it's removed + assert not uploaded_file_dir.exists() + + +async def test_removed_on_stop(hass: HomeAssistant, hass_client, uploaded_file_dir): + """Test uploading and using a file.""" + await hass.async_stop() + + # Test it's removed + assert not uploaded_file_dir.exists() diff --git a/tests/components/image/__init__.py b/tests/components/image/__init__.py index 8bf90c4f516..b04214669aa 100644 --- a/tests/components/image/__init__.py +++ b/tests/components/image/__init__.py @@ -1 +1,4 @@ """Tests for the Image integration.""" +import pathlib + +TEST_IMAGE = pathlib.Path(__file__).parent / "logo.png" diff --git a/tests/components/image/test_init.py b/tests/components/image/test_init.py index ab73bb71286..d62717cb894 100644 --- a/tests/components/image/test_init.py +++ b/tests/components/image/test_init.py @@ -9,11 +9,12 @@ from homeassistant.components.websocket_api import const as ws_const from homeassistant.setup import async_setup_component from homeassistant.util import dt as util_dt +from . import TEST_IMAGE + async def test_upload_image(hass, hass_client, hass_ws_client): """Test we can upload an image.""" now = util_dt.utcnow() - test_image = pathlib.Path(__file__).parent / "logo.png" with tempfile.TemporaryDirectory() as tempdir, patch.object( hass.config, "path", return_value=tempdir @@ -22,7 +23,7 @@ async def test_upload_image(hass, hass_client, hass_ws_client): ws_client: ClientWebSocketResponse = await hass_ws_client() client: ClientSession = await hass_client() - with test_image.open("rb") as fp: + with TEST_IMAGE.open("rb") as fp: res = await client.post("/api/image/upload", data={"file": fp}) assert res.status == 200 @@ -36,7 +37,7 @@ async def test_upload_image(hass, hass_client, hass_ws_client): tempdir = pathlib.Path(tempdir) item_folder: pathlib.Path = tempdir / item["id"] - assert (item_folder / "original").read_bytes() == test_image.read_bytes() + assert (item_folder / "original").read_bytes() == TEST_IMAGE.read_bytes() # fetch non-existing image res = await client.get("/api/image/serve/non-existing/256x256") diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 4cb924a520e..d1018299d96 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -646,3 +646,19 @@ def test_datetime_selector_schema(schema, valid_selections, invalid_selections): def test_template_selector_schema(schema, valid_selections, invalid_selections): """Test template selector.""" _test_selector("template", schema, valid_selections, invalid_selections) + + +@pytest.mark.parametrize( + "schema,valid_selections,invalid_selections", + ( + ( + {"accept": "image/*"}, + ("0182a1b99dbc5ae24aecd90c346605fa",), + (None, "not-a-uuid", "abcd", 1), + ), + ), +) +def test_file_selector_schema(schema, valid_selections, invalid_selections): + """Test file selector.""" + + _test_selector("file", schema, valid_selections, invalid_selections) From f5487b3a7ea5bc58f532eb790b614f687776010f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Thu, 18 Aug 2022 18:03:10 +0200 Subject: [PATCH 3452/3516] Bump pyhaversion from 22.4.1 to 22.8.0 (#76994) --- homeassistant/components/version/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index cd513b16e33..52ca1ca74f8 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -2,7 +2,7 @@ "domain": "version", "name": "Version", "documentation": "https://www.home-assistant.io/integrations/version", - "requirements": ["pyhaversion==22.4.1"], + "requirements": ["pyhaversion==22.8.0"], "codeowners": ["@ludeeus"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index ecaaf30280c..2a25c6da9df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1554,7 +1554,7 @@ pygtfs==0.1.6 pygti==0.9.3 # homeassistant.components.version -pyhaversion==22.4.1 +pyhaversion==22.8.0 # homeassistant.components.heos pyheos==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b72beee027..d9cfea5b10b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1076,7 +1076,7 @@ pyfttt==0.3 pygti==0.9.3 # homeassistant.components.version -pyhaversion==22.4.1 +pyhaversion==22.8.0 # homeassistant.components.heos pyheos==0.7.2 From fb5a67fb1fb1bd701d4b4f0c583afcbb0aff1ec2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 19:22:08 +0200 Subject: [PATCH 3453/3516] Add vacuum checks to pylint plugin (#76560) --- pylint/plugins/hass_enforce_type_hints.py | 136 +++++++++++++++++++++- tests/pylint/test_enforce_type_hints.py | 46 +++++++- 2 files changed, 179 insertions(+), 3 deletions(-) diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 19a69f8808f..4eedca487ab 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -55,11 +55,12 @@ class ClassTypeHintMatch: matches: list[TypeHintMatch] +_INNER_MATCH = r"((?:[\w\| ]+)|(?:\.{3})|(?:\w+\[.+\]))" _TYPE_HINT_MATCHERS: dict[str, re.Pattern[str]] = { # a_or_b matches items such as "DiscoveryInfoType | None" - "a_or_b": re.compile(r"^(\w+) \| (\w+)$"), + # or "dict | list | None" + "a_or_b": re.compile(rf"^(.+) \| {_INNER_MATCH}$"), } -_INNER_MATCH = r"((?:[\w\| ]+)|(?:\.{3})|(?:\w+\[.+\]))" _INNER_MATCH_POSSIBILITIES = [i + 1 for i in range(5)] _TYPE_HINT_MATCHERS.update( { @@ -2118,6 +2119,137 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ], ), ], + "vacuum": [ + ClassTypeHintMatch( + base_class="Entity", + matches=_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="ToggleEntity", + matches=_TOGGLE_ENTITY_MATCH, + ), + ClassTypeHintMatch( + base_class="_BaseVacuum", + matches=[ + TypeHintMatch( + function_name="battery_level", + return_type=["int", None], + ), + TypeHintMatch( + function_name="battery_icon", + return_type="str", + ), + TypeHintMatch( + function_name="fan_speed", + return_type=["str", None], + ), + TypeHintMatch( + function_name="fan_speed_list", + return_type="list[str]", + ), + TypeHintMatch( + function_name="stop", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="return_to_base", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="clean_spot", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="locate", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="set_fan_speed", + named_arg_types={ + "fan_speed": "str", + }, + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="send_command", + named_arg_types={ + "command": "str", + "params": "dict[str, Any] | list[Any] | None", + }, + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + ], + ), + ClassTypeHintMatch( + base_class="VacuumEntity", + matches=[ + TypeHintMatch( + function_name="status", + return_type=["str", None], + ), + TypeHintMatch( + function_name="start_pause", + kwargs_type="Any", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="async_pause", + return_type=None, + ), + TypeHintMatch( + function_name="async_start", + return_type=None, + ), + ], + ), + ClassTypeHintMatch( + base_class="StateVacuumEntity", + matches=[ + TypeHintMatch( + function_name="state", + return_type=["str", None], + ), + TypeHintMatch( + function_name="start", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="pause", + return_type=None, + has_async_counterpart=True, + ), + TypeHintMatch( + function_name="async_turn_on", + kwargs_type="Any", + return_type=None, + ), + TypeHintMatch( + function_name="async_turn_off", + kwargs_type="Any", + return_type=None, + ), + TypeHintMatch( + function_name="async_toggle", + kwargs_type="Any", + return_type=None, + ), + ], + ), + ], "water_heater": [ ClassTypeHintMatch( base_class="Entity", diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 1381ed34a7b..ebea738edc4 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -73,7 +73,12 @@ def test_regex_x_of_y_i( @pytest.mark.parametrize( ("string", "expected_a", "expected_b"), - [("DiscoveryInfoType | None", "DiscoveryInfoType", "None")], + [ + ("DiscoveryInfoType | None", "DiscoveryInfoType", "None"), + ("dict | list | None", "dict | list", "None"), + ("dict[str, Any] | list[Any] | None", "dict[str, Any] | list[Any]", "None"), + ("dict[str, Any] | list[Any]", "dict[str, Any]", "list[Any]"), + ], ) def test_regex_a_or_b( hass_enforce_type_hints: ModuleType, string: str, expected_a: str, expected_b: str @@ -967,3 +972,42 @@ def test_number_entity(linter: UnittestLinter, type_hint_checker: BaseChecker) - with assert_no_messages(linter): type_hint_checker.visit_classdef(class_node) + + +def test_vacuum_entity(linter: UnittestLinter, type_hint_checker: BaseChecker) -> None: + """Ensure valid hints are accepted for vacuum entity.""" + # Set bypass option + type_hint_checker.config.ignore_missing_annotations = False + + # Ensure that `dict | list | None` is valid for params + class_node = astroid.extract_node( + """ + class Entity(): + pass + + class ToggleEntity(Entity): + pass + + class _BaseVacuum(Entity): + pass + + class VacuumEntity(_BaseVacuum, ToggleEntity): + pass + + class MyVacuum( #@ + VacuumEntity + ): + def send_command( + self, + command: str, + params: dict[str, Any] | list[Any] | None = None, + **kwargs: Any, + ) -> None: + pass + """, + "homeassistant.components.pylint_test.vacuum", + ) + type_hint_checker.visit_module(class_node.parent) + + with assert_no_messages(linter): + type_hint_checker.visit_classdef(class_node) From bb74730e96dff382c2f79c65e48eda767ad899fc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 18 Aug 2022 21:52:12 +0200 Subject: [PATCH 3454/3516] Add support for USB dongles to the hardware integration (#76795) * Add support for USB dongles to the hardware integration * Update hardware integrations * Adjust tests * Add USB discovery for SkyConnect 1.0 * Improve test coverage * Apply suggestions from code review Co-authored-by: Paulus Schoutsen * Fix frozen dataclass shizzle * Adjust test Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 2 + .../components/hardkernel/hardware.py | 1 + homeassistant/components/hardware/models.py | 14 +- .../homeassistant_sky_connect/__init__.py | 35 +++++ .../homeassistant_sky_connect/config_flow.py | 37 +++++ .../homeassistant_sky_connect/const.py | 3 + .../homeassistant_sky_connect/hardware.py | 33 ++++ .../homeassistant_sky_connect/manifest.json | 17 ++ .../homeassistant_yellow/hardware.py | 1 + .../components/raspberry_pi/hardware.py | 1 + homeassistant/components/usb/__init__.py | 53 ++++--- homeassistant/components/zha/config_flow.py | 10 +- script/hassfest/manifest.py | 1 + tests/components/hardkernel/test_hardware.py | 1 + .../homeassistant_sky_connect/__init__.py | 1 + .../homeassistant_sky_connect/conftest.py | 14 ++ .../test_config_flow.py | 147 ++++++++++++++++++ .../test_hardware.py | 85 ++++++++++ .../homeassistant_sky_connect/test_init.py | 101 ++++++++++++ .../homeassistant_yellow/test_hardware.py | 1 + .../components/raspberry_pi/test_hardware.py | 1 + tests/components/usb/test_init.py | 42 +++++ tests/components/zha/test_config_flow.py | 8 +- 23 files changed, 581 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/homeassistant_sky_connect/__init__.py create mode 100644 homeassistant/components/homeassistant_sky_connect/config_flow.py create mode 100644 homeassistant/components/homeassistant_sky_connect/const.py create mode 100644 homeassistant/components/homeassistant_sky_connect/hardware.py create mode 100644 homeassistant/components/homeassistant_sky_connect/manifest.json create mode 100644 tests/components/homeassistant_sky_connect/__init__.py create mode 100644 tests/components/homeassistant_sky_connect/conftest.py create mode 100644 tests/components/homeassistant_sky_connect/test_config_flow.py create mode 100644 tests/components/homeassistant_sky_connect/test_hardware.py create mode 100644 tests/components/homeassistant_sky_connect/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index f952ae22d9b..ea9e47a9423 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -467,6 +467,8 @@ build.json @home-assistant/supervisor /tests/components/homeassistant/ @home-assistant/core /homeassistant/components/homeassistant_alerts/ @home-assistant/core /tests/components/homeassistant_alerts/ @home-assistant/core +/homeassistant/components/homeassistant_sky_connect/ @home-assistant/core +/tests/components/homeassistant_sky_connect/ @home-assistant/core /homeassistant/components/homeassistant_yellow/ @home-assistant/core /tests/components/homeassistant_yellow/ @home-assistant/core /homeassistant/components/homekit/ @bdraco diff --git a/homeassistant/components/hardkernel/hardware.py b/homeassistant/components/hardkernel/hardware.py index 804f105f2ed..ad45e3ac946 100644 --- a/homeassistant/components/hardkernel/hardware.py +++ b/homeassistant/components/hardkernel/hardware.py @@ -34,6 +34,7 @@ def async_info(hass: HomeAssistant) -> HardwareInfo: model=board, revision=None, ), + dongles=None, name=BOARD_NAMES.get(board, f"Unknown hardkernel Odroid model '{board}'"), url=None, ) diff --git a/homeassistant/components/hardware/models.py b/homeassistant/components/hardware/models.py index 067c2d955df..8f9819a853d 100644 --- a/homeassistant/components/hardware/models.py +++ b/homeassistant/components/hardware/models.py @@ -17,12 +17,24 @@ class BoardInfo: revision: str | None -@dataclass +@dataclass(frozen=True) +class USBInfo: + """USB info type.""" + + vid: str + pid: str + serial_number: str | None + manufacturer: str | None + description: str | None + + +@dataclass(frozen=True) class HardwareInfo: """Hardware info type.""" name: str | None board: BoardInfo | None + dongles: list[USBInfo] | None url: str | None diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py new file mode 100644 index 00000000000..981e96ccdee --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -0,0 +1,35 @@ +"""The Home Assistant Sky Connect integration.""" +from __future__ import annotations + +from homeassistant.components import usb +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a Home Assistant Sky Connect config entry.""" + usb_info = usb.UsbServiceInfo( + device=entry.data["device"], + vid=entry.data["vid"], + pid=entry.data["pid"], + serial_number=entry.data["serial_number"], + manufacturer=entry.data["manufacturer"], + description=entry.data["description"], + ) + if not usb.async_is_plugged_in(hass, entry.data): + # The USB dongle is not plugged in + raise ConfigEntryNotReady + + await hass.config_entries.flow.async_init( + "zha", + context={"source": "usb"}, + data=usb_info, + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return True diff --git a/homeassistant/components/homeassistant_sky_connect/config_flow.py b/homeassistant/components/homeassistant_sky_connect/config_flow.py new file mode 100644 index 00000000000..21cc5e3ace4 --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/config_flow.py @@ -0,0 +1,37 @@ +"""Config flow for the Home Assistant Sky Connect integration.""" +from __future__ import annotations + +from homeassistant.components import usb +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Home Assistant Sky Connect.""" + + VERSION = 1 + + async def async_step_usb(self, discovery_info: usb.UsbServiceInfo) -> FlowResult: + """Handle usb discovery.""" + device = discovery_info.device + vid = discovery_info.vid + pid = discovery_info.pid + serial_number = discovery_info.serial_number + manufacturer = discovery_info.manufacturer + description = discovery_info.description + unique_id = f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}" + if await self.async_set_unique_id(unique_id): + self._abort_if_unique_id_configured(updates={"device": device}) + return self.async_create_entry( + title="Home Assistant Sky Connect", + data={ + "device": device, + "vid": vid, + "pid": pid, + "serial_number": serial_number, + "manufacturer": manufacturer, + "description": description, + }, + ) diff --git a/homeassistant/components/homeassistant_sky_connect/const.py b/homeassistant/components/homeassistant_sky_connect/const.py new file mode 100644 index 00000000000..1deb8fd4603 --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/const.py @@ -0,0 +1,3 @@ +"""Constants for the Home Assistant Sky Connect integration.""" + +DOMAIN = "homeassistant_sky_connect" diff --git a/homeassistant/components/homeassistant_sky_connect/hardware.py b/homeassistant/components/homeassistant_sky_connect/hardware.py new file mode 100644 index 00000000000..3c1993bfd8b --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/hardware.py @@ -0,0 +1,33 @@ +"""The Home Assistant Sky Connect hardware platform.""" +from __future__ import annotations + +from homeassistant.components.hardware.models import HardwareInfo, USBInfo +from homeassistant.core import HomeAssistant, callback + +from .const import DOMAIN + +DONGLE_NAME = "Home Assistant Sky Connect" + + +@callback +def async_info(hass: HomeAssistant) -> HardwareInfo: + """Return board info.""" + entries = hass.config_entries.async_entries(DOMAIN) + + dongles = [ + USBInfo( + vid=entry.data["vid"], + pid=entry.data["pid"], + serial_number=entry.data["serial_number"], + manufacturer=entry.data["manufacturer"], + description=entry.data["description"], + ) + for entry in entries + ] + + return HardwareInfo( + board=None, + dongles=dongles, + name=DONGLE_NAME, + url=None, + ) diff --git a/homeassistant/components/homeassistant_sky_connect/manifest.json b/homeassistant/components/homeassistant_sky_connect/manifest.json new file mode 100644 index 00000000000..5ccb8bd5331 --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/manifest.json @@ -0,0 +1,17 @@ +{ + "domain": "homeassistant_sky_connect", + "name": "Home Assistant Sky Connect", + "config_flow": false, + "documentation": "https://www.home-assistant.io/integrations/homeassistant_sky_connect", + "dependencies": ["hardware", "usb"], + "codeowners": ["@home-assistant/core"], + "integration_type": "hardware", + "usb": [ + { + "vid": "10C4", + "pid": "EA60", + "description": "*skyconnect v1.0*", + "known_devices": ["SkyConnect v1.0"] + } + ] +} diff --git a/homeassistant/components/homeassistant_yellow/hardware.py b/homeassistant/components/homeassistant_yellow/hardware.py index aa1fe4b745b..01aee032a22 100644 --- a/homeassistant/components/homeassistant_yellow/hardware.py +++ b/homeassistant/components/homeassistant_yellow/hardware.py @@ -29,6 +29,7 @@ def async_info(hass: HomeAssistant) -> HardwareInfo: model=MODEL, revision=None, ), + dongles=None, name=BOARD_NAME, url=None, ) diff --git a/homeassistant/components/raspberry_pi/hardware.py b/homeassistant/components/raspberry_pi/hardware.py index 343ba69d76b..cd1b56ba789 100644 --- a/homeassistant/components/raspberry_pi/hardware.py +++ b/homeassistant/components/raspberry_pi/hardware.py @@ -49,6 +49,7 @@ def async_info(hass: HomeAssistant) -> HardwareInfo: model=MODELS.get(board), revision=None, ), + dongles=None, name=BOARD_NAMES.get(board, f"Unknown Raspberry Pi model '{board}'"), url=None, ) diff --git a/homeassistant/components/usb/__init__.py b/homeassistant/components/usb/__init__.py index 5783401df13..83c7a6a8a45 100644 --- a/homeassistant/components/usb/__init__.py +++ b/homeassistant/components/usb/__init__.py @@ -1,7 +1,7 @@ """The USB Discovery integration.""" from __future__ import annotations -from collections.abc import Coroutine +from collections.abc import Coroutine, Mapping import dataclasses import fnmatch import logging @@ -97,6 +97,27 @@ def _fnmatch_lower(name: str | None, pattern: str) -> bool: return fnmatch.fnmatch(name.lower(), pattern) +def _is_matching(device: USBDevice, matcher: Mapping[str, str]) -> bool: + """Return True if a device matches.""" + if "vid" in matcher and device.vid != matcher["vid"]: + return False + if "pid" in matcher and device.pid != matcher["pid"]: + return False + if "serial_number" in matcher and not _fnmatch_lower( + device.serial_number, matcher["serial_number"] + ): + return False + if "manufacturer" in matcher and not _fnmatch_lower( + device.manufacturer, matcher["manufacturer"] + ): + return False + if "description" in matcher and not _fnmatch_lower( + device.description, matcher["description"] + ): + return False + return True + + class USBDiscovery: """Manage USB Discovery.""" @@ -179,23 +200,8 @@ class USBDiscovery: self.seen.add(device_tuple) matched = [] for matcher in self.usb: - if "vid" in matcher and device.vid != matcher["vid"]: - continue - if "pid" in matcher and device.pid != matcher["pid"]: - continue - if "serial_number" in matcher and not _fnmatch_lower( - device.serial_number, matcher["serial_number"] - ): - continue - if "manufacturer" in matcher and not _fnmatch_lower( - device.manufacturer, matcher["manufacturer"] - ): - continue - if "description" in matcher and not _fnmatch_lower( - device.description, matcher["description"] - ): - continue - matched.append(matcher) + if _is_matching(device, matcher): + matched.append(matcher) if not matched: return @@ -265,3 +271,14 @@ async def websocket_usb_scan( if not usb_discovery.observer_active: await usb_discovery.async_request_scan_serial() connection.send_result(msg["id"]) + + +@callback +def async_is_plugged_in(hass: HomeAssistant, matcher: Mapping) -> bool: + """Return True is a USB device is present.""" + usb_discovery: USBDiscovery = hass.data[DOMAIN] + for device_tuple in usb_discovery.seen: + device = USBDevice(*device_tuple) + if _is_matching(device, matcher): + return True + return False diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 94723e38d58..4b90fdb3ad0 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -138,11 +138,11 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) self._set_confirm_only() self.context["title_placeholders"] = {CONF_NAME: self._title} - return await self.async_step_confirm() + return await self.async_step_confirm_usb() - async def async_step_confirm(self, user_input=None): - """Confirm a discovery.""" - if user_input is not None: + async def async_step_confirm_usb(self, user_input=None): + """Confirm a USB discovery.""" + if user_input is not None or not onboarding.async_is_onboarded(self.hass): auto_detected_data = await detect_radios(self._device_path) if auto_detected_data is None: # This path probably will not happen now that we have @@ -155,7 +155,7 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_show_form( - step_id="confirm", + step_id="confirm_usb", description_placeholders={CONF_NAME: self._title}, ) diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 338682bbe76..b0b4f5b0582 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -58,6 +58,7 @@ NO_IOT_CLASS = [ "history", "homeassistant", "homeassistant_alerts", + "homeassistant_sky_connect", "homeassistant_yellow", "image", "input_boolean", diff --git a/tests/components/hardkernel/test_hardware.py b/tests/components/hardkernel/test_hardware.py index 1c71959719c..5f33cb417f2 100644 --- a/tests/components/hardkernel/test_hardware.py +++ b/tests/components/hardkernel/test_hardware.py @@ -48,6 +48,7 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: "model": "odroid-n2", "revision": None, }, + "dongles": None, "name": "Home Assistant Blue / Hardkernel Odroid-N2", "url": None, } diff --git a/tests/components/homeassistant_sky_connect/__init__.py b/tests/components/homeassistant_sky_connect/__init__.py new file mode 100644 index 00000000000..90cd1594710 --- /dev/null +++ b/tests/components/homeassistant_sky_connect/__init__.py @@ -0,0 +1 @@ +"""Tests for the Home Assistant Sky Connect integration.""" diff --git a/tests/components/homeassistant_sky_connect/conftest.py b/tests/components/homeassistant_sky_connect/conftest.py new file mode 100644 index 00000000000..cc606c9b988 --- /dev/null +++ b/tests/components/homeassistant_sky_connect/conftest.py @@ -0,0 +1,14 @@ +"""Test fixtures for the Home Assistant Sky Connect integration.""" +from unittest.mock import patch + +import pytest + + +@pytest.fixture(autouse=True) +def mock_zha(): + """Mock the zha integration.""" + with patch( + "homeassistant.components.zha.async_setup_entry", + return_value=True, + ): + yield diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py new file mode 100644 index 00000000000..1db305f3ad0 --- /dev/null +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -0,0 +1,147 @@ +"""Test the Home Assistant Sky Connect config flow.""" +import copy +from unittest.mock import patch + +from homeassistant.components import homeassistant_sky_connect, usb +from homeassistant.components.homeassistant_sky_connect.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +USB_DATA = usb.UsbServiceInfo( + device="bla_device", + vid="bla_vid", + pid="bla_pid", + serial_number="bla_serial_number", + manufacturer="bla_manufacturer", + description="bla_description", +) + + +async def test_config_flow(hass: HomeAssistant) -> None: + """Test the config flow.""" + # mock_integration(hass, MockModule("hassio")) + + with patch( + "homeassistant.components.homeassistant_sky_connect.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=USB_DATA + ) + + expected_data = { + "device": USB_DATA.device, + "vid": USB_DATA.vid, + "pid": USB_DATA.pid, + "serial_number": USB_DATA.serial_number, + "manufacturer": USB_DATA.manufacturer, + "description": USB_DATA.description, + } + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Home Assistant Sky Connect" + assert result["data"] == expected_data + assert result["options"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == expected_data + assert config_entry.options == {} + assert config_entry.title == "Home Assistant Sky Connect" + assert ( + config_entry.unique_id + == f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}" + ) + + +async def test_config_flow_unique_id(hass: HomeAssistant) -> None: + """Test only a single entry is allowed for a dongle.""" + # Setup an existing config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_sky_connect.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=USB_DATA + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + mock_setup_entry.assert_not_called() + + +async def test_config_flow_multiple_entries(hass: HomeAssistant) -> None: + """Test multiple entries are allowed.""" + # Setup an existing config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", + ) + config_entry.add_to_hass(hass) + + usb_data = copy.copy(USB_DATA) + usb_data.serial_number = "bla_serial_number_2" + + with patch( + "homeassistant.components.homeassistant_sky_connect.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + + +async def test_config_flow_update_device(hass: HomeAssistant) -> None: + """Test updating device path.""" + # Setup an existing config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", + ) + config_entry.add_to_hass(hass) + + usb_data = copy.copy(USB_DATA) + usb_data.device = "bla_device_2" + + with patch( + "homeassistant.components.homeassistant_sky_connect.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + assert await hass.config_entries.async_setup(config_entry.entry_id) + assert len(mock_setup_entry.mock_calls) == 1 + + with patch( + "homeassistant.components.homeassistant_sky_connect.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.homeassistant_sky_connect.async_unload_entry", + wraps=homeassistant_sky_connect.async_unload_entry, + ) as mock_unload_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "usb"}, data=usb_data + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_unload_entry.mock_calls) == 1 diff --git a/tests/components/homeassistant_sky_connect/test_hardware.py b/tests/components/homeassistant_sky_connect/test_hardware.py new file mode 100644 index 00000000000..f4e48d56a67 --- /dev/null +++ b/tests/components/homeassistant_sky_connect/test_hardware.py @@ -0,0 +1,85 @@ +"""Test the Home Assistant Sky Connect hardware platform.""" +from unittest.mock import patch + +from homeassistant.components.homeassistant_sky_connect.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +CONFIG_ENTRY_DATA = { + "device": "bla_device", + "vid": "bla_vid", + "pid": "bla_pid", + "serial_number": "bla_serial_number", + "manufacturer": "bla_manufacturer", + "description": "bla_description", +} + +CONFIG_ENTRY_DATA_2 = { + "device": "bla_device_2", + "vid": "bla_vid_2", + "pid": "bla_pid_2", + "serial_number": "bla_serial_number_2", + "manufacturer": "bla_manufacturer_2", + "description": "bla_description_2", +} + + +async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can get the board info.""" + # Setup the config entry + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + unique_id="unique_1", + ) + config_entry.add_to_hass(hass) + config_entry_2 = MockConfigEntry( + data=CONFIG_ENTRY_DATA_2, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + unique_id="unique_2", + ) + config_entry_2.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "hardware/info"}) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["success"] + assert msg["result"] == { + "hardware": [ + { + "board": None, + "dongles": [ + { + "vid": "bla_vid", + "pid": "bla_pid", + "serial_number": "bla_serial_number", + "manufacturer": "bla_manufacturer", + "description": "bla_description", + }, + { + "vid": "bla_vid_2", + "pid": "bla_pid_2", + "serial_number": "bla_serial_number_2", + "manufacturer": "bla_manufacturer_2", + "description": "bla_description_2", + }, + ], + "name": "Home Assistant Sky Connect", + "url": None, + } + ] + } diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py new file mode 100644 index 00000000000..74c1b9cb14f --- /dev/null +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -0,0 +1,101 @@ +"""Test the Home Assistant Sky Connect integration.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.homeassistant_sky_connect.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +CONFIG_ENTRY_DATA = { + "device": "bla_device", + "vid": "bla_vid", + "pid": "bla_pid", + "serial_number": "bla_serial_number", + "manufacturer": "bla_manufacturer", + "description": "bla_description", +} + + +@pytest.mark.parametrize( + "onboarded, num_entries, num_flows", ((False, 1, 0), (True, 0, 1)) +) +async def test_setup_entry( + hass: HomeAssistant, onboarded, num_entries, num_flows +) -> None: + """Test setup of a config entry, including setup of zha.""" + # Setup the config entry + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ) as mock_is_plugged_in, patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=onboarded + ), patch( + "zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_is_plugged_in.mock_calls) == 1 + + assert len(hass.config_entries.async_entries("zha")) == num_entries + assert len(hass.config_entries.flow.async_progress_by_handler("zha")) == num_flows + + +async def test_setup_zha(hass: HomeAssistant) -> None: + """Test zha gets the right config.""" + # Setup the config entry + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=True, + ) as mock_is_plugged_in, patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ), patch( + "zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_is_plugged_in.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries("zha")[0] + assert config_entry.data == { + "device": {"baudrate": 115200, "flow_control": None, "path": "bla_device"}, + "radio_type": "znp", + } + assert config_entry.options == {} + assert config_entry.title == "bla_description" + + +async def test_setup_entry_wait_usb(hass: HomeAssistant) -> None: + """Test setup of a config entry when the dongle is not plugged in.""" + # Setup the config entry + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=DOMAIN, + options={}, + title="Home Assistant Sky Connect", + ) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.homeassistant_sky_connect.usb.async_is_plugged_in", + return_value=False, + ) as mock_is_plugged_in: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(mock_is_plugged_in.mock_calls) == 1 + assert config_entry.state == ConfigEntryState.SETUP_RETRY diff --git a/tests/components/homeassistant_yellow/test_hardware.py b/tests/components/homeassistant_yellow/test_hardware.py index 28403334ec1..295e44c6ce7 100644 --- a/tests/components/homeassistant_yellow/test_hardware.py +++ b/tests/components/homeassistant_yellow/test_hardware.py @@ -48,6 +48,7 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: "model": "yellow", "revision": None, }, + "dongles": None, "name": "Home Assistant Yellow", "url": None, } diff --git a/tests/components/raspberry_pi/test_hardware.py b/tests/components/raspberry_pi/test_hardware.py index a4e938079d3..ad9533e8af5 100644 --- a/tests/components/raspberry_pi/test_hardware.py +++ b/tests/components/raspberry_pi/test_hardware.py @@ -48,6 +48,7 @@ async def test_hardware_info(hass: HomeAssistant, hass_ws_client) -> None: "model": "1", "revision": None, }, + "dongles": None, "name": "Raspberry Pi", "url": None, } diff --git a/tests/components/usb/test_init.py b/tests/components/usb/test_init.py index f4245a0e0d6..0d1ad36a9f4 100644 --- a/tests/components/usb/test_init.py +++ b/tests/components/usb/test_init.py @@ -833,3 +833,45 @@ def test_human_readable_device_name(): assert "Silicon Labs" in name assert "10C4" in name assert "8A2A" in name + + +async def test_async_is_plugged_in(hass, hass_ws_client): + """Test async_is_plugged_in.""" + new_usb = [{"domain": "test1", "vid": "3039", "pid": "3039"}] + + mock_comports = [ + MagicMock( + device=slae_sh_device.device, + vid=12345, + pid=12345, + serial_number=slae_sh_device.serial_number, + manufacturer=slae_sh_device.manufacturer, + description=slae_sh_device.description, + ) + ] + + matcher = { + "vid": "3039", + "pid": "3039", + } + + with patch("pyudev.Context", side_effect=ImportError), patch( + "homeassistant.components.usb.async_get_usb", return_value=new_usb + ), patch("homeassistant.components.usb.comports", return_value=[]), patch.object( + hass.config_entries.flow, "async_init" + ): + assert await async_setup_component(hass, "usb", {"usb": {}}) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + assert not usb.async_is_plugged_in(hass, matcher) + + with patch( + "homeassistant.components.usb.comports", return_value=mock_comports + ), patch.object(hass.config_entries.flow, "async_init"): + ws_client = await hass_ws_client(hass) + await ws_client.send_json({"id": 1, "type": "usb/scan"}) + response = await ws_client.receive_json() + assert response["success"] + await hass.async_block_till_done() + assert usb.async_is_plugged_in(hass, matcher) diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index a769303a4c4..82c2fde7c1e 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -228,7 +228,7 @@ async def test_discovery_via_usb(detect_mock, hass): ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "confirm" + assert result["step_id"] == "confirm_usb" with patch("homeassistant.components.zha.async_setup_entry"): result2 = await hass.config_entries.flow.async_configure( @@ -264,7 +264,7 @@ async def test_zigate_discovery_via_usb(detect_mock, hass): ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "confirm" + assert result["step_id"] == "confirm_usb" with patch("homeassistant.components.zha.async_setup_entry"): result2 = await hass.config_entries.flow.async_configure( @@ -298,7 +298,7 @@ async def test_discovery_via_usb_no_radio(detect_mock, hass): ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "confirm" + assert result["step_id"] == "confirm_usb" with patch("homeassistant.components.zha.async_setup_entry"): result2 = await hass.config_entries.flow.async_configure( @@ -451,7 +451,7 @@ async def test_discovery_via_usb_deconz_ignored(detect_mock, hass): await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "confirm" + assert result["step_id"] == "confirm_usb" @patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) From eec45c1208f6bba1909487865cfecba947741369 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 22:21:19 +0200 Subject: [PATCH 3455/3516] Adjust type hints in august sensor entity (#76992) --- homeassistant/components/august/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index 8a6d169fcb0..af79cad459e 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -226,7 +226,7 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): return attributes - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Restore ATTR_CHANGED_BY on startup since it is likely no longer in the activity log.""" await super().async_added_to_hass() @@ -234,7 +234,7 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity): if not last_state or last_state.state == STATE_UNAVAILABLE: return - self._attr_state = last_state.state + self._attr_native_value = last_state.state if ATTR_ENTITY_PICTURE in last_state.attributes: self._entity_picture = last_state.attributes[ATTR_ENTITY_PICTURE] if ATTR_OPERATION_REMOTE in last_state.attributes: From b8d8d5540e812bd5b0148e72952a0b9d7ad18872 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Thu, 18 Aug 2022 22:35:28 +0200 Subject: [PATCH 3456/3516] P1 Monitor add water meter support (#74004) --- .../components/p1_monitor/__init__.py | 22 +- homeassistant/components/p1_monitor/const.py | 7 +- .../components/p1_monitor/diagnostics.py | 15 +- .../components/p1_monitor/manifest.json | 2 +- homeassistant/components/p1_monitor/sensor.py | 427 ++++++++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/p1_monitor/conftest.py | 9 +- .../p1_monitor/fixtures/watermeter.json | 10 + .../components/p1_monitor/test_config_flow.py | 14 +- .../components/p1_monitor/test_diagnostics.py | 5 + tests/components/p1_monitor/test_init.py | 2 +- tests/components/p1_monitor/test_sensor.py | 81 +++- 13 files changed, 367 insertions(+), 231 deletions(-) create mode 100644 tests/components/p1_monitor/fixtures/watermeter.json diff --git a/homeassistant/components/p1_monitor/__init__.py b/homeassistant/components/p1_monitor/__init__.py index 03055013345..b157f3e8116 100644 --- a/homeassistant/components/p1_monitor/__init__.py +++ b/homeassistant/components/p1_monitor/__init__.py @@ -3,7 +3,14 @@ from __future__ import annotations from typing import TypedDict -from p1monitor import P1Monitor, Phases, Settings, SmartMeter +from p1monitor import ( + P1Monitor, + P1MonitorNoDataError, + Phases, + Settings, + SmartMeter, + WaterMeter, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, Platform @@ -19,6 +26,7 @@ from .const import ( SERVICE_PHASES, SERVICE_SETTINGS, SERVICE_SMARTMETER, + SERVICE_WATERMETER, ) PLATFORMS = [Platform.SENSOR] @@ -55,12 +63,14 @@ class P1MonitorData(TypedDict): smartmeter: SmartMeter phases: Phases settings: Settings + watermeter: WaterMeter | None class P1MonitorDataUpdateCoordinator(DataUpdateCoordinator[P1MonitorData]): """Class to manage fetching P1 Monitor data from single endpoint.""" config_entry: ConfigEntry + has_water_meter: bool | None = None def __init__( self, @@ -84,6 +94,16 @@ class P1MonitorDataUpdateCoordinator(DataUpdateCoordinator[P1MonitorData]): SERVICE_SMARTMETER: await self.p1monitor.smartmeter(), SERVICE_PHASES: await self.p1monitor.phases(), SERVICE_SETTINGS: await self.p1monitor.settings(), + SERVICE_WATERMETER: None, } + if self.has_water_meter or self.has_water_meter is None: + try: + data[SERVICE_WATERMETER] = await self.p1monitor.watermeter() + self.has_water_meter = True + except P1MonitorNoDataError: + LOGGER.debug("No watermeter data received from P1 Monitor") + if self.has_water_meter is None: + self.has_water_meter = False + return data diff --git a/homeassistant/components/p1_monitor/const.py b/homeassistant/components/p1_monitor/const.py index d72927a80f6..045301d38c4 100644 --- a/homeassistant/components/p1_monitor/const.py +++ b/homeassistant/components/p1_monitor/const.py @@ -10,11 +10,6 @@ LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(seconds=5) SERVICE_SMARTMETER: Final = "smartmeter" +SERVICE_WATERMETER: Final = "watermeter" SERVICE_PHASES: Final = "phases" SERVICE_SETTINGS: Final = "settings" - -SERVICES: dict[str, str] = { - SERVICE_SMARTMETER: "SmartMeter", - SERVICE_PHASES: "Phases", - SERVICE_SETTINGS: "Settings", -} diff --git a/homeassistant/components/p1_monitor/diagnostics.py b/homeassistant/components/p1_monitor/diagnostics.py index b99cc7b86e1..29f48d47cd9 100644 --- a/homeassistant/components/p1_monitor/diagnostics.py +++ b/homeassistant/components/p1_monitor/diagnostics.py @@ -10,7 +10,13 @@ from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from . import P1MonitorDataUpdateCoordinator -from .const import DOMAIN, SERVICE_PHASES, SERVICE_SETTINGS, SERVICE_SMARTMETER +from .const import ( + DOMAIN, + SERVICE_PHASES, + SERVICE_SETTINGS, + SERVICE_SMARTMETER, + SERVICE_WATERMETER, +) TO_REDACT = { CONF_HOST, @@ -23,7 +29,7 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" coordinator: P1MonitorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - return { + data = { "entry": { "title": entry.title, "data": async_redact_data(entry.data, TO_REDACT), @@ -34,3 +40,8 @@ async def async_get_config_entry_diagnostics( "settings": asdict(coordinator.data[SERVICE_SETTINGS]), }, } + + if coordinator.has_water_meter: + data["data"]["watermeter"] = asdict(coordinator.data[SERVICE_WATERMETER]) + + return data diff --git a/homeassistant/components/p1_monitor/manifest.json b/homeassistant/components/p1_monitor/manifest.json index c94893f61fd..626cff15dfa 100644 --- a/homeassistant/components/p1_monitor/manifest.json +++ b/homeassistant/components/p1_monitor/manifest.json @@ -3,7 +3,7 @@ "name": "P1 Monitor", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/p1_monitor", - "requirements": ["p1monitor==1.0.1"], + "requirements": ["p1monitor==2.1.0"], "codeowners": ["@klaasnicolaas"], "quality_scale": "platinum", "iot_class": "local_polling", diff --git a/homeassistant/components/p1_monitor/sensor.py b/homeassistant/components/p1_monitor/sensor.py index 57f6b0ad99c..757bf249ca1 100644 --- a/homeassistant/components/p1_monitor/sensor.py +++ b/homeassistant/components/p1_monitor/sensor.py @@ -19,6 +19,7 @@ from homeassistant.const import ( ENERGY_KILO_WATT_HOUR, POWER_WATT, VOLUME_CUBIC_METERS, + VOLUME_LITERS, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType @@ -33,206 +34,256 @@ from .const import ( SERVICE_PHASES, SERVICE_SETTINGS, SERVICE_SMARTMETER, - SERVICES, + SERVICE_WATERMETER, ) -SENSORS: dict[ - Literal["smartmeter", "phases", "settings"], tuple[SensorEntityDescription, ...] -] = { - SERVICE_SMARTMETER: ( - SensorEntityDescription( - key="gas_consumption", - name="Gas Consumption", - entity_registry_enabled_default=False, - native_unit_of_measurement=VOLUME_CUBIC_METERS, - device_class=SensorDeviceClass.GAS, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - SensorEntityDescription( - key="power_consumption", - name="Power Consumption", - native_unit_of_measurement=POWER_WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="energy_consumption_high", - name="Energy Consumption - High Tariff", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - SensorEntityDescription( - key="energy_consumption_low", - name="Energy Consumption - Low Tariff", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - SensorEntityDescription( - key="power_production", - name="Power Production", - native_unit_of_measurement=POWER_WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="energy_production_high", - name="Energy Production - High Tariff", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - SensorEntityDescription( - key="energy_production_low", - name="Energy Production - Low Tariff", - native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - ), - SensorEntityDescription( - key="energy_tariff_period", - name="Energy Tariff Period", - icon="mdi:calendar-clock", - ), +SENSORS_SMARTMETER: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="gas_consumption", + name="Gas Consumption", + entity_registry_enabled_default=False, + native_unit_of_measurement=VOLUME_CUBIC_METERS, + device_class=SensorDeviceClass.GAS, + state_class=SensorStateClass.TOTAL_INCREASING, ), - SERVICE_PHASES: ( - SensorEntityDescription( - key="voltage_phase_l1", - name="Voltage Phase L1", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=SensorDeviceClass.VOLTAGE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="voltage_phase_l2", - name="Voltage Phase L2", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=SensorDeviceClass.VOLTAGE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="voltage_phase_l3", - name="Voltage Phase L3", - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, - device_class=SensorDeviceClass.VOLTAGE, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="current_phase_l1", - name="Current Phase L1", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=SensorDeviceClass.CURRENT, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="current_phase_l2", - name="Current Phase L2", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=SensorDeviceClass.CURRENT, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="current_phase_l3", - name="Current Phase L3", - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, - device_class=SensorDeviceClass.CURRENT, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="power_consumed_phase_l1", - name="Power Consumed Phase L1", - native_unit_of_measurement=POWER_WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="power_consumed_phase_l2", - name="Power Consumed Phase L2", - native_unit_of_measurement=POWER_WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="power_consumed_phase_l3", - name="Power Consumed Phase L3", - native_unit_of_measurement=POWER_WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="power_produced_phase_l1", - name="Power Produced Phase L1", - native_unit_of_measurement=POWER_WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="power_produced_phase_l2", - name="Power Produced Phase L2", - native_unit_of_measurement=POWER_WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key="power_produced_phase_l3", - name="Power Produced Phase L3", - native_unit_of_measurement=POWER_WATT, - device_class=SensorDeviceClass.POWER, - state_class=SensorStateClass.MEASUREMENT, - ), + SensorEntityDescription( + key="power_consumption", + name="Power Consumption", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, ), - SERVICE_SETTINGS: ( - SensorEntityDescription( - key="gas_consumption_price", - name="Gas Consumption Price", - entity_registry_enabled_default=False, - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=f"{CURRENCY_EURO}/{VOLUME_CUBIC_METERS}", - ), - SensorEntityDescription( - key="energy_consumption_price_low", - name="Energy Consumption Price - Low", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", - ), - SensorEntityDescription( - key="energy_consumption_price_high", - name="Energy Consumption Price - High", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", - ), - SensorEntityDescription( - key="energy_production_price_low", - name="Energy Production Price - Low", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", - ), - SensorEntityDescription( - key="energy_production_price_high", - name="Energy Production Price - High", - state_class=SensorStateClass.MEASUREMENT, - native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", - ), + SensorEntityDescription( + key="energy_consumption_high", + name="Energy Consumption - High Tariff", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, ), -} + SensorEntityDescription( + key="energy_consumption_low", + name="Energy Consumption - Low Tariff", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="power_production", + name="Power Production", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="energy_production_high", + name="Energy Production - High Tariff", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="energy_production_low", + name="Energy Production - Low Tariff", + native_unit_of_measurement=ENERGY_KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="energy_tariff_period", + name="Energy Tariff Period", + icon="mdi:calendar-clock", + ), +) + +SENSORS_PHASES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="voltage_phase_l1", + name="Voltage Phase L1", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="voltage_phase_l2", + name="Voltage Phase L2", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="voltage_phase_l3", + name="Voltage Phase L3", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="current_phase_l1", + name="Current Phase L1", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="current_phase_l2", + name="Current Phase L2", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="current_phase_l3", + name="Current Phase L3", + native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="power_consumed_phase_l1", + name="Power Consumed Phase L1", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="power_consumed_phase_l2", + name="Power Consumed Phase L2", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="power_consumed_phase_l3", + name="Power Consumed Phase L3", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="power_produced_phase_l1", + name="Power Produced Phase L1", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="power_produced_phase_l2", + name="Power Produced Phase L2", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="power_produced_phase_l3", + name="Power Produced Phase L3", + native_unit_of_measurement=POWER_WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), +) + +SENSORS_SETTINGS: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="gas_consumption_price", + name="Gas Consumption Price", + entity_registry_enabled_default=False, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=f"{CURRENCY_EURO}/{VOLUME_CUBIC_METERS}", + ), + SensorEntityDescription( + key="energy_consumption_price_low", + name="Energy Consumption Price - Low", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", + ), + SensorEntityDescription( + key="energy_consumption_price_high", + name="Energy Consumption Price - High", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", + ), + SensorEntityDescription( + key="energy_production_price_low", + name="Energy Production Price - Low", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", + ), + SensorEntityDescription( + key="energy_production_price_high", + name="Energy Production Price - High", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}", + ), +) + +SENSORS_WATERMETER: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="consumption_day", + name="Consumption Day", + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=VOLUME_LITERS, + ), + SensorEntityDescription( + key="consumption_total", + name="Consumption Total", + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=VOLUME_CUBIC_METERS, + ), + SensorEntityDescription( + key="pulse_count", + name="Pulse Count", + ), +) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up P1 Monitor Sensors based on a config entry.""" - async_add_entities( + coordinator: P1MonitorDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + entities: list[P1MonitorSensorEntity] = [] + entities.extend( P1MonitorSensorEntity( - coordinator=hass.data[DOMAIN][entry.entry_id], + coordinator=coordinator, description=description, - service_key=service_key, - name=entry.title, - service=SERVICES[service_key], + name="SmartMeter", + service_key="smartmeter", + service=SERVICE_SMARTMETER, ) - for service_key, service_sensors in SENSORS.items() - for description in service_sensors + for description in SENSORS_SMARTMETER ) + entities.extend( + P1MonitorSensorEntity( + coordinator=coordinator, + description=description, + name="Phases", + service_key="phases", + service=SERVICE_PHASES, + ) + for description in SENSORS_PHASES + ) + entities.extend( + P1MonitorSensorEntity( + coordinator=coordinator, + description=description, + name="Settings", + service_key="settings", + service=SERVICE_SETTINGS, + ) + for description in SENSORS_SETTINGS + ) + if coordinator.has_water_meter: + entities.extend( + P1MonitorSensorEntity( + coordinator=coordinator, + description=description, + name="WaterMeter", + service_key="watermeter", + service=SERVICE_WATERMETER, + ) + for description in SENSORS_WATERMETER + ) + async_add_entities(entities) class P1MonitorSensorEntity( @@ -245,7 +296,7 @@ class P1MonitorSensorEntity( *, coordinator: P1MonitorDataUpdateCoordinator, description: SensorEntityDescription, - service_key: Literal["smartmeter", "phases", "settings"], + service_key: Literal["smartmeter", "watermeter", "phases", "settings"], name: str, service: str, ) -> None: @@ -253,7 +304,7 @@ class P1MonitorSensorEntity( super().__init__(coordinator=coordinator) self._service_key = service_key - self.entity_id = f"{SENSOR_DOMAIN}.{name}_{description.key}" + self.entity_id = f"{SENSOR_DOMAIN}.{service}_{description.key}" self.entity_description = description self._attr_unique_id = ( f"{coordinator.config_entry.entry_id}_{service_key}_{description.key}" @@ -266,7 +317,7 @@ class P1MonitorSensorEntity( }, configuration_url=f"http://{coordinator.config_entry.data[CONF_HOST]}", manufacturer="P1 Monitor", - name=service, + name=name, ) @property diff --git a/requirements_all.txt b/requirements_all.txt index 2a25c6da9df..322d67a94df 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1206,7 +1206,7 @@ orvibo==1.1.1 ovoenergy==1.2.0 # homeassistant.components.p1_monitor -p1monitor==1.0.1 +p1monitor==2.1.0 # homeassistant.components.mqtt # homeassistant.components.shiftr diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d9cfea5b10b..8c67a756157 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -842,7 +842,7 @@ openerz-api==0.1.0 ovoenergy==1.2.0 # homeassistant.components.p1_monitor -p1monitor==1.0.1 +p1monitor==2.1.0 # homeassistant.components.mqtt # homeassistant.components.shiftr diff --git a/tests/components/p1_monitor/conftest.py b/tests/components/p1_monitor/conftest.py index dbdf572c6de..5f6713f5228 100644 --- a/tests/components/p1_monitor/conftest.py +++ b/tests/components/p1_monitor/conftest.py @@ -2,7 +2,7 @@ import json from unittest.mock import AsyncMock, MagicMock, patch -from p1monitor import Phases, Settings, SmartMeter +from p1monitor import Phases, Settings, SmartMeter, WaterMeter import pytest from homeassistant.components.p1_monitor.const import DOMAIN @@ -43,7 +43,12 @@ def mock_p1monitor(): json.loads(load_fixture("p1_monitor/settings.json")) ) ) - yield p1monitor_mock + client.watermeter = AsyncMock( + return_value=WaterMeter.from_dict( + json.loads(load_fixture("p1_monitor/watermeter.json")) + ) + ) + yield client @pytest.fixture diff --git a/tests/components/p1_monitor/fixtures/watermeter.json b/tests/components/p1_monitor/fixtures/watermeter.json new file mode 100644 index 00000000000..f69a4edcf88 --- /dev/null +++ b/tests/components/p1_monitor/fixtures/watermeter.json @@ -0,0 +1,10 @@ +[ + { + "TIMEPERIOD_ID": 13, + "TIMESTAMP_UTC": 1656194400, + "TIMESTAMP_lOCAL": "2022-06-26 00:00:00", + "WATERMETER_CONSUMPTION_LITER": 112.0, + "WATERMETER_CONSUMPTION_TOTAL_M3": 1696.14, + "WATERMETER_PULS_COUNT": 112.0 + } +] diff --git a/tests/components/p1_monitor/test_config_flow.py b/tests/components/p1_monitor/test_config_flow.py index 42a41789a92..13541e782b4 100644 --- a/tests/components/p1_monitor/test_config_flow.py +++ b/tests/components/p1_monitor/test_config_flow.py @@ -27,17 +27,12 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={ - CONF_NAME: "Name", - CONF_HOST: "example.com", - }, + user_input={CONF_NAME: "Name", CONF_HOST: "example.com"}, ) assert result2.get("type") == FlowResultType.CREATE_ENTRY assert result2.get("title") == "Name" - assert result2.get("data") == { - CONF_HOST: "example.com", - } + assert result2.get("data") == {CONF_HOST: "example.com"} assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_p1monitor.mock_calls) == 1 @@ -52,10 +47,7 @@ async def test_api_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, - data={ - CONF_NAME: "Name", - CONF_HOST: "example.com", - }, + data={CONF_NAME: "Name", CONF_HOST: "example.com"}, ) assert result.get("type") == FlowResultType.FORM diff --git a/tests/components/p1_monitor/test_diagnostics.py b/tests/components/p1_monitor/test_diagnostics.py index 6b97107c353..1ab9da7adc8 100644 --- a/tests/components/p1_monitor/test_diagnostics.py +++ b/tests/components/p1_monitor/test_diagnostics.py @@ -55,5 +55,10 @@ async def test_diagnostics( "energy_production_price_high": "0.20522", "energy_production_price_low": "0.20522", }, + "watermeter": { + "consumption_day": 112.0, + "consumption_total": 1696.14, + "pulse_count": 112.0, + }, }, } diff --git a/tests/components/p1_monitor/test_init.py b/tests/components/p1_monitor/test_init.py index 1cf9cf21966..d7817faecdf 100644 --- a/tests/components/p1_monitor/test_init.py +++ b/tests/components/p1_monitor/test_init.py @@ -28,7 +28,7 @@ async def test_load_unload_config_entry( @patch( - "homeassistant.components.p1_monitor.P1Monitor.request", + "homeassistant.components.p1_monitor.P1Monitor._request", side_effect=P1MonitorConnectionError, ) async def test_config_entry_not_ready( diff --git a/tests/components/p1_monitor/test_sensor.py b/tests/components/p1_monitor/test_sensor.py index faaafca5ab8..fe9560c9cb6 100644 --- a/tests/components/p1_monitor/test_sensor.py +++ b/tests/components/p1_monitor/test_sensor.py @@ -1,4 +1,7 @@ """Tests for the sensors provided by the P1 Monitor integration.""" +from unittest.mock import MagicMock + +from p1monitor import P1MonitorNoDataError import pytest from homeassistant.components.p1_monitor.const import DOMAIN @@ -17,6 +20,7 @@ from homeassistant.const import ( ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, POWER_WATT, + VOLUME_LITERS, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -33,8 +37,8 @@ async def test_smartmeter( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - state = hass.states.get("sensor.monitor_power_consumption") - entry = entity_registry.async_get("sensor.monitor_power_consumption") + state = hass.states.get("sensor.smartmeter_power_consumption") + entry = entity_registry.async_get("sensor.smartmeter_power_consumption") assert entry assert state assert entry.unique_id == f"{entry_id}_smartmeter_power_consumption" @@ -45,8 +49,8 @@ async def test_smartmeter( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.monitor_energy_consumption_high") - entry = entity_registry.async_get("sensor.monitor_energy_consumption_high") + state = hass.states.get("sensor.smartmeter_energy_consumption_high") + entry = entity_registry.async_get("sensor.smartmeter_energy_consumption_high") assert entry assert state assert entry.unique_id == f"{entry_id}_smartmeter_energy_consumption_high" @@ -59,8 +63,8 @@ async def test_smartmeter( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.monitor_energy_tariff_period") - entry = entity_registry.async_get("sensor.monitor_energy_tariff_period") + state = hass.states.get("sensor.smartmeter_energy_tariff_period") + entry = entity_registry.async_get("sensor.smartmeter_energy_tariff_period") assert entry assert state assert entry.unique_id == f"{entry_id}_smartmeter_energy_tariff_period" @@ -90,8 +94,8 @@ async def test_phases( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - state = hass.states.get("sensor.monitor_voltage_phase_l1") - entry = entity_registry.async_get("sensor.monitor_voltage_phase_l1") + state = hass.states.get("sensor.phases_voltage_phase_l1") + entry = entity_registry.async_get("sensor.phases_voltage_phase_l1") assert entry assert state assert entry.unique_id == f"{entry_id}_phases_voltage_phase_l1" @@ -102,8 +106,8 @@ async def test_phases( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.monitor_current_phase_l1") - entry = entity_registry.async_get("sensor.monitor_current_phase_l1") + state = hass.states.get("sensor.phases_current_phase_l1") + entry = entity_registry.async_get("sensor.phases_current_phase_l1") assert entry assert state assert entry.unique_id == f"{entry_id}_phases_current_phase_l1" @@ -114,8 +118,8 @@ async def test_phases( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT assert ATTR_ICON not in state.attributes - state = hass.states.get("sensor.monitor_power_consumed_phase_l1") - entry = entity_registry.async_get("sensor.monitor_power_consumed_phase_l1") + state = hass.states.get("sensor.phases_power_consumed_phase_l1") + entry = entity_registry.async_get("sensor.phases_power_consumed_phase_l1") assert entry assert state assert entry.unique_id == f"{entry_id}_phases_power_consumed_phase_l1" @@ -146,8 +150,8 @@ async def test_settings( entity_registry = er.async_get(hass) device_registry = dr.async_get(hass) - state = hass.states.get("sensor.monitor_energy_consumption_price_low") - entry = entity_registry.async_get("sensor.monitor_energy_consumption_price_low") + state = hass.states.get("sensor.settings_energy_consumption_price_low") + entry = entity_registry.async_get("sensor.settings_energy_consumption_price_low") assert entry assert state assert entry.unique_id == f"{entry_id}_settings_energy_consumption_price_low" @@ -159,8 +163,8 @@ async def test_settings( == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" ) - state = hass.states.get("sensor.monitor_energy_production_price_low") - entry = entity_registry.async_get("sensor.monitor_energy_production_price_low") + state = hass.states.get("sensor.settings_energy_production_price_low") + entry = entity_registry.async_get("sensor.settings_energy_production_price_low") assert entry assert state assert entry.unique_id == f"{entry_id}_settings_energy_production_price_low" @@ -183,9 +187,52 @@ async def test_settings( assert not device_entry.sw_version +async def test_watermeter( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test the P1 Monitor - WaterMeter sensors.""" + entry_id = init_integration.entry_id + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + state = hass.states.get("sensor.watermeter_consumption_day") + entry = entity_registry.async_get("sensor.watermeter_consumption_day") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_watermeter_consumption_day" + assert state.state == "112.0" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Consumption Day" + assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_LITERS + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, f"{entry_id}_watermeter")} + assert device_entry.manufacturer == "P1 Monitor" + assert device_entry.name == "WaterMeter" + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE + assert not device_entry.model + assert not device_entry.sw_version + + +async def test_no_watermeter( + hass: HomeAssistant, mock_p1monitor: MagicMock, mock_config_entry: MockConfigEntry +) -> None: + """Test the P1 Monitor - Without WaterMeter sensors.""" + mock_p1monitor.watermeter.side_effect = P1MonitorNoDataError + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.states.get("sensor.watermeter_consumption_day") + assert not hass.states.get("sensor.consumption_total") + assert not hass.states.get("sensor.pulse_count") + + @pytest.mark.parametrize( "entity_id", - ("sensor.monitor_gas_consumption",), + ("sensor.smartmeter_gas_consumption",), ) async def test_smartmeter_disabled_by_default( hass: HomeAssistant, init_integration: MockConfigEntry, entity_id: str From 7a457391047f00763ee9e86d2415bc2ee6a65235 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 22:47:56 +0200 Subject: [PATCH 3457/3516] Adjust type hints in aquostv media player entity (#76990) --- homeassistant/components/aquostv/media_player.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index c9465424381..b0ff674e2de 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -158,13 +158,19 @@ class SharpAquosTVDevice(MediaPlayerEntity): self._remote.power(0) @_retry - def volume_up(self): + def volume_up(self) -> None: """Volume up the media player.""" + if self.volume_level is None: + _LOGGER.debug("Unknown volume in volume_up") + return self._remote.volume(int(self.volume_level * 60) + 2) @_retry - def volume_down(self): + def volume_down(self) -> None: """Volume down media player.""" + if self.volume_level is None: + _LOGGER.debug("Unknown volume in volume_down") + return self._remote.volume(int(self.volume_level * 60) - 2) @_retry From 009a573324d083abc4dc4076cd7271fc18f11080 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 22:48:49 +0200 Subject: [PATCH 3458/3516] Adjust type hints in alpha-vantage sensor entity (#76988) --- homeassistant/components/alpha_vantage/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index 534383f0bbf..02c6958e0da 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -128,7 +128,7 @@ class AlphaVantageSensor(SensorEntity): self._attr_native_unit_of_measurement = symbol.get(CONF_CURRENCY, self._symbol) self._attr_icon = ICONS.get(symbol.get(CONF_CURRENCY, "USD")) - def update(self): + def update(self) -> None: """Get the latest data and updates the states.""" _LOGGER.debug("Requesting new data for symbol %s", self._symbol) all_values, _ = self._timeseries.get_intraday(self._symbol) @@ -144,7 +144,7 @@ class AlphaVantageSensor(SensorEntity): ATTR_LOW: values["3. low"], } if isinstance(values, dict) - else None + else {} ) _LOGGER.debug("Received new values for symbol %s", self._symbol) @@ -167,7 +167,7 @@ class AlphaVantageForeignExchange(SensorEntity): self._attr_icon = ICONS.get(self._from_currency, "USD") self._attr_native_unit_of_measurement = self._to_currency - def update(self): + def update(self) -> None: """Get the latest data and updates the states.""" _LOGGER.debug( "Requesting new data for forex %s - %s", @@ -187,7 +187,7 @@ class AlphaVantageForeignExchange(SensorEntity): CONF_TO: self._to_currency, } if values is not None - else None + else {} ) _LOGGER.debug( From f323c5e8806108d05a00551662c0cfed952d3014 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 18 Aug 2022 22:49:59 +0200 Subject: [PATCH 3459/3516] Adjust type hints in android_ip_webcam switch entity (#76989) --- homeassistant/components/android_ip_webcam/switch.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/android_ip_webcam/switch.py b/homeassistant/components/android_ip_webcam/switch.py index 57f78b20e3d..b09b0de4be8 100644 --- a/homeassistant/components/android_ip_webcam/switch.py +++ b/homeassistant/components/android_ip_webcam/switch.py @@ -1,8 +1,9 @@ """Support for Android IP Webcam settings.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Coroutine from dataclasses import dataclass +from typing import Any from pydroid_ipcam import PyDroidIPCam @@ -21,8 +22,8 @@ from .entity import AndroidIPCamBaseEntity class AndroidIPWebcamSwitchEntityDescriptionMixin: """Mixin for required keys.""" - on_func: Callable[[PyDroidIPCam], None] - off_func: Callable[[PyDroidIPCam], None] + on_func: Callable[[PyDroidIPCam], Coroutine[Any, Any, bool]] + off_func: Callable[[PyDroidIPCam], Coroutine[Any, Any, bool]] @dataclass @@ -159,12 +160,12 @@ class IPWebcamSettingSwitch(AndroidIPCamBaseEntity, SwitchEntity): """Return if settings is on or off.""" return bool(self.cam.current_settings.get(self.entity_description.key)) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn device on.""" await self.entity_description.on_func(self.cam) await self.coordinator.async_request_refresh() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn device off.""" await self.entity_description.off_func(self.cam) await self.coordinator.async_request_refresh() From a434d755b35701a710886638c200d9e2f4ba86c9 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 19 Aug 2022 00:27:31 +0000 Subject: [PATCH 3460/3516] [ci skip] Translation update --- .../components/almond/translations/es.json | 2 +- .../lacrosse_view/translations/de.json | 3 +- .../lacrosse_view/translations/el.json | 3 +- .../lacrosse_view/translations/es.json | 3 +- .../lacrosse_view/translations/fr.json | 3 +- .../lacrosse_view/translations/pt-BR.json | 3 +- .../components/lametric/translations/de.json | 40 ++++++++++++++- .../components/lametric/translations/el.json | 50 +++++++++++++++++++ .../components/lametric/translations/es.json | 50 +++++++++++++++++++ .../components/lametric/translations/fr.json | 44 ++++++++++++++++ .../components/lametric/translations/ja.json | 48 ++++++++++++++++++ .../components/lametric/translations/no.json | 50 +++++++++++++++++++ .../lametric/translations/pt-BR.json | 50 +++++++++++++++++++ .../components/lametric/translations/ru.json | 50 +++++++++++++++++++ .../lametric/translations/zh-Hant.json | 50 +++++++++++++++++++ .../landisgyr_heat_meter/translations/de.json | 23 +++++++++ .../landisgyr_heat_meter/translations/el.json | 23 +++++++++ .../landisgyr_heat_meter/translations/en.json | 11 ++-- .../landisgyr_heat_meter/translations/es.json | 23 +++++++++ .../landisgyr_heat_meter/translations/fr.json | 23 +++++++++ .../translations/pt-BR.json | 23 +++++++++ .../tankerkoenig/translations/es.json | 2 +- .../xiaomi_ble/translations/es.json | 2 +- .../yalexs_ble/translations/ja.json | 3 ++ 24 files changed, 567 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/lametric/translations/el.json create mode 100644 homeassistant/components/lametric/translations/es.json create mode 100644 homeassistant/components/lametric/translations/fr.json create mode 100644 homeassistant/components/lametric/translations/ja.json create mode 100644 homeassistant/components/lametric/translations/no.json create mode 100644 homeassistant/components/lametric/translations/pt-BR.json create mode 100644 homeassistant/components/lametric/translations/ru.json create mode 100644 homeassistant/components/lametric/translations/zh-Hant.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/de.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/el.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/es.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/fr.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json diff --git a/homeassistant/components/almond/translations/es.json b/homeassistant/components/almond/translations/es.json index 7c0a80ef444..7c768deecdd 100644 --- a/homeassistant/components/almond/translations/es.json +++ b/homeassistant/components/almond/translations/es.json @@ -8,7 +8,7 @@ }, "step": { "hassio_confirm": { - "description": "\u00bfQuieres configurar Home Assistant para conectarse a Almond proporcionado por el complemento: {addon} ?", + "description": "\u00bfQuieres configurar Home Assistant para conectarse a Almond proporcionado por el complemento: {addon}?", "title": "Almond a trav\u00e9s del complemento Home Assistant" }, "pick_implementation": { diff --git a/homeassistant/components/lacrosse_view/translations/de.json b/homeassistant/components/lacrosse_view/translations/de.json index d9aa1210fa0..362aa1619f8 100644 --- a/homeassistant/components/lacrosse_view/translations/de.json +++ b/homeassistant/components/lacrosse_view/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung", diff --git a/homeassistant/components/lacrosse_view/translations/el.json b/homeassistant/components/lacrosse_view/translations/el.json index 1975028e9c9..5be96ab9caa 100644 --- a/homeassistant/components/lacrosse_view/translations/el.json +++ b/homeassistant/components/lacrosse_view/translations/el.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "reauth_successful": "\u039f \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" }, "error": { "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2", diff --git a/homeassistant/components/lacrosse_view/translations/es.json b/homeassistant/components/lacrosse_view/translations/es.json index 1f341b0f44e..9b02b2bbd4f 100644 --- a/homeassistant/components/lacrosse_view/translations/es.json +++ b/homeassistant/components/lacrosse_view/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", diff --git a/homeassistant/components/lacrosse_view/translations/fr.json b/homeassistant/components/lacrosse_view/translations/fr.json index 689d3cf7cf6..c3ea4492cd3 100644 --- a/homeassistant/components/lacrosse_view/translations/fr.json +++ b/homeassistant/components/lacrosse_view/translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "invalid_auth": "Authentification non valide", diff --git a/homeassistant/components/lacrosse_view/translations/pt-BR.json b/homeassistant/components/lacrosse_view/translations/pt-BR.json index 29b458e5599..89f951e857b 100644 --- a/homeassistant/components/lacrosse_view/translations/pt-BR.json +++ b/homeassistant/components/lacrosse_view/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", diff --git a/homeassistant/components/lametric/translations/de.json b/homeassistant/components/lametric/translations/de.json index 1ea4d15dcd8..d44ad04ec0a 100644 --- a/homeassistant/components/lametric/translations/de.json +++ b/homeassistant/components/lametric/translations/de.json @@ -1,12 +1,50 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "invalid_discovery_info": "Ung\u00fcltige Suchinformationen erhalten", + "link_local_address": "Lokale Linkadressen werden nicht unterst\u00fctzt", + "missing_configuration": "Die LaMetric-Integration ist nicht konfiguriert. Bitte folge der Dokumentation.", + "no_devices": "Der autorisierte Benutzer hat keine LaMetric Ger\u00e4te", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" }, "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "Ein LaMetric-Ger\u00e4t kann im Home Assistant auf zwei verschiedene Arten eingerichtet werden.\n\nDu kannst alle Ger\u00e4teinformationen und API-Tokens selbst eingeben, oder Home Assistant kann sie von deinem LaMetric.com-Konto importieren.", + "menu_options": { + "manual_entry": "Manuell eintragen", + "pick_implementation": "Import von LaMetric.com (empfohlen)" + } + }, + "manual_entry": { + "data": { + "api_key": "API-Schl\u00fcssel", + "host": "Host" + }, + "data_description": { + "api_key": "Du findest diesen API-Schl\u00fcssel auf der [Ger\u00e4teseite in deinem LaMetric-Entwicklerkonto](https://developer.lametric.com/user/devices).", + "host": "Die IP-Adresse oder der Hostname deines LaMetric TIME in deinem Netzwerk." + } + }, "pick_implementation": { "title": "W\u00e4hle eine Authentifizierungsmethode" + }, + "user_cloud_select_device": { + "data": { + "device": "W\u00e4hle das hinzuzuf\u00fcgende LaMetric-Ger\u00e4t aus" + } } } + }, + "issues": { + "manual_migration": { + "description": "Die LaMetric-Integration wurde modernisiert: Sie wird nun \u00fcber die Benutzeroberfl\u00e4che konfiguriert und eingerichtet und die Kommunikation erfolgt nun lokal.\n\nLeider gibt es keinen automatischen Migrationspfad, so dass du dein LaMetric mit Home Assistant neu einrichten musst. Bitte konsultiere die Dokumentation zur LaMetric-Integration von Home Assistant, um zu erfahren, wie du diese einrichten kannst.\n\nEntferne die alte LaMetric YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um das Problem zu beheben.", + "title": "Manuelle Migration f\u00fcr LaMetric erforderlich" + } } } \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/el.json b/homeassistant/components/lametric/translations/el.json new file mode 100644 index 00000000000..964bfc4b212 --- /dev/null +++ b/homeassistant/components/lametric/translations/el.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "authorize_url_timeout": "\u039b\u03ae\u03be\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03bf\u03cd \u03bf\u03c1\u03af\u03bf\u03c5 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2.", + "invalid_discovery_info": "\u039b\u03ae\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7\u03c2", + "link_local_address": "\u039f\u03b9 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ad\u03c2 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03bc\u03bf\u03c5 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9", + "missing_configuration": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 LaMetric \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7.", + "no_devices": "\u039f \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03bf\u03c4\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 LaMetric", + "no_url_available": "\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL. \u0393\u03b9\u03b1 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1, [\u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b2\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1\u03c2] ( {docs_url} )" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "\u039c\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae LaMetric \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03bf Home Assistant \u03bc\u03b5 \u03b4\u03cd\u03bf \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03bf\u03cd\u03c2 \u03c4\u03c1\u03cc\u03c0\u03bf\u03c5\u03c2. \n\n \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03bc\u03cc\u03bd\u03bf\u03b9 \u03c3\u03b1\u03c2 \u03cc\u03bb\u03b5\u03c2 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03ba\u03b1\u03b9 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03ac API \u03ae \u03bf \u0392\u03bf\u03b7\u03b8\u03cc\u03c2 Home \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c4\u03b1 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf LaMetric.com.", + "menu_options": { + "manual_entry": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b1", + "pick_implementation": "\u0395\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae \u03b1\u03c0\u03cc \u03c4\u03bf LaMetric.com (\u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9)" + } + }, + "manual_entry": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + }, + "data_description": { + "api_key": "\u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03c3\u03c4\u03b7 [\u03c3\u03b5\u03bb\u03af\u03b4\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd \u03c3\u03c4\u03bf\u03bd \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03b9\u03c3\u03c4\u03ae LaMetric](https://developer.lametric.com/user/devices).", + "host": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ae \u03c4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03bf\u03cd \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae \u03c4\u03bf\u03c5 LaMetric TIME \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2." + } + }, + "pick_implementation": { + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03bc\u03b5\u03b8\u03cc\u03b4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "user_cloud_select_device": { + "data": { + "device": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae LaMetric \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 LaMetric \u03ad\u03c7\u03b5\u03b9 \u03b5\u03ba\u03c3\u03c5\u03b3\u03c7\u03c1\u03bf\u03bd\u03b9\u03c3\u03c4\u03b5\u03af: \u03a4\u03ce\u03c1\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af \u03ba\u03b1\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03bc\u03ad\u03c3\u03c9 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03bf\u03b9 \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b5\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03c4\u03bf\u03c0\u03b9\u03ba\u03ad\u03c2. \n\n \u0394\u03c5\u03c3\u03c4\u03c5\u03c7\u03ce\u03c2, \u03b4\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7\u03c2 \u03bc\u03b5\u03c4\u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03ba\u03b1\u03b9, \u03c9\u03c2 \u03b5\u03ba \u03c4\u03bf\u03cd\u03c4\u03bf\u03c5, \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03b1\u03c0\u03cc \u03b5\u03c3\u03ac\u03c2 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf LaMetric \u03c3\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf Home Assistant. \u03a3\u03c5\u03bc\u03b2\u03bf\u03c5\u03bb\u03b5\u03c5\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Home Assistant LaMetric \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03b1\u03bb\u03b9\u03ac \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML LaMetric \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0391\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/es.json b/homeassistant/components/lametric/translations/es.json new file mode 100644 index 00000000000..e7cbe914a2e --- /dev/null +++ b/homeassistant/components/lametric/translations/es.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "authorize_url_timeout": "Se agot\u00f3 el tiempo de espera para generar la URL de autorizaci\u00f3n.", + "invalid_discovery_info": "Se recibi\u00f3 informaci\u00f3n de descubrimiento no v\u00e1lida", + "link_local_address": "Las direcciones de enlace local no son compatibles", + "missing_configuration": "La integraci\u00f3n de LaMetric no est\u00e1 configurada. Por favor, sigue la documentaci\u00f3n.", + "no_devices": "El usuario autorizado no tiene dispositivos LaMetric", + "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "Un dispositivo LaMetric se puede configurar en Home Assistant de dos maneras diferentes. \n\nPuedes introducir t\u00fa mismo toda la informaci\u00f3n del dispositivo y los tokens API, o Home Assistant puede importarlos desde tu cuenta de LaMetric.com.", + "menu_options": { + "manual_entry": "Introducir manualmente", + "pick_implementation": "Importar desde LaMetric.com (recomendado)" + } + }, + "manual_entry": { + "data": { + "api_key": "Clave API", + "host": "Host" + }, + "data_description": { + "api_key": "Puedes encontrar esta clave API en la [p\u00e1gina de dispositivos en tu cuenta de desarrollador de LaMetric](https://developer.lametric.com/user/devices).", + "host": "La direcci\u00f3n IP o el nombre de host de tu LaMetric TIME en tu red." + } + }, + "pick_implementation": { + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" + }, + "user_cloud_select_device": { + "data": { + "device": "Selecciona el dispositivo LaMetric para a\u00f1adir" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "La integraci\u00f3n de LaMetric se ha modernizado: ahora se configura a trav\u00e9s de la interfaz de usuario y las comunicaciones son locales.\n\nDesafortunadamente, no existe una ruta de migraci\u00f3n autom\u00e1tica posible y, por lo tanto, requiere que vuelvas a configurar tu LaMetric con Home Assistant. Consulta la documentaci\u00f3n de la integraci\u00f3n LaMetric en Home Assistant sobre c\u00f3mo configurarlo. \n\nElimina la configuraci\u00f3n antigua de LaMetric YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se requiere migraci\u00f3n manual para LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/fr.json b/homeassistant/components/lametric/translations/fr.json new file mode 100644 index 00000000000..5bd4a5899f6 --- /dev/null +++ b/homeassistant/components/lametric/translations/fr.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification expir\u00e9.", + "invalid_discovery_info": "Informations de d\u00e9couverte re\u00e7ues non valides", + "link_local_address": "Les adresses de liaison locale ne sont pas prises en charge", + "missing_configuration": "L'int\u00e9gration LaMetric n'est pas configur\u00e9e\u00a0; veuillez suivre la documentation.", + "no_devices": "L'utilisateur autoris\u00e9 ne poss\u00e8de aucun appareil LaMetric", + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "menu_options": { + "manual_entry": "Saisir manuellement", + "pick_implementation": "Importer depuis LaMetric.com (recommand\u00e9)" + } + }, + "manual_entry": { + "data": { + "api_key": "Cl\u00e9 d'API", + "host": "H\u00f4te" + } + }, + "pick_implementation": { + "title": "S\u00e9lectionner une m\u00e9thode d'authentification" + }, + "user_cloud_select_device": { + "data": { + "device": "S\u00e9lectionnez l'appareil LaMetric \u00e0 ajouter" + } + } + } + }, + "issues": { + "manual_migration": { + "title": "Migration manuelle requise pour LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/ja.json b/homeassistant/components/lametric/translations/ja.json new file mode 100644 index 00000000000..242a43bddb7 --- /dev/null +++ b/homeassistant/components/lametric/translations/ja.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "authorize_url_timeout": "\u8a8d\u8a3cURL\u306e\u751f\u6210\u304c\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u3057\u307e\u3057\u305f\u3002", + "invalid_discovery_info": "\u7121\u52b9\u306a\u30c7\u30a3\u30b9\u30ab\u30d0\u30ea\u30fc\u60c5\u5831\u3092\u53d7\u4fe1\u3057\u305f", + "link_local_address": "\u30ed\u30fc\u30ab\u30eb\u30a2\u30c9\u30ec\u30b9\u306e\u30ea\u30f3\u30af\u306b\u306f\u5bfe\u5fdc\u3057\u3066\u3044\u307e\u305b\u3093", + "missing_configuration": "LaMetric\u306e\u7d71\u5408\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", + "no_devices": "\u8a31\u53ef\u3055\u308c\u305f\u30e6\u30fc\u30b6\u30fc\u306f\u3001LaMetric\u30c7\u30d0\u30a4\u30b9\u3092\u6301\u3063\u3066\u3044\u307e\u305b\u3093", + "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "menu_options": { + "manual_entry": "\u624b\u52d5\u3067\u5165\u529b", + "pick_implementation": "LaMetric.com\u304b\u3089\u306e\u30a4\u30f3\u30dd\u30fc\u30c8((\u63a8\u5968)" + } + }, + "manual_entry": { + "data": { + "api_key": "API\u30ad\u30fc", + "host": "\u30db\u30b9\u30c8" + }, + "data_description": { + "api_key": "\u3053\u306eAPI\u30ad\u30fc\u306f\u3001[LaMetric\u958b\u767a\u8005\u30a2\u30ab\u30a6\u30f3\u30c8\u306e\u30c7\u30d0\u30a4\u30b9\u30da\u30fc\u30b8](https://developer.lametric.com/user/devices)\u306b\u3042\u308a\u307e\u3059\u3002", + "host": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306e\u3001LaMetric TIME\u306eIP\u30a2\u30c9\u30ec\u30b9\u307e\u305f\u306f\u30db\u30b9\u30c8\u540d\u3002" + } + }, + "pick_implementation": { + "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" + }, + "user_cloud_select_device": { + "data": { + "device": "\u8ffd\u52a0\u3059\u308bLaMetric\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e" + } + } + } + }, + "issues": { + "manual_migration": { + "title": "LaMetric\u306b\u5fc5\u8981\u306a\u624b\u52d5\u3067\u306e\u79fb\u884c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/no.json b/homeassistant/components/lametric/translations/no.json new file mode 100644 index 00000000000..79d3591aec1 --- /dev/null +++ b/homeassistant/components/lametric/translations/no.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "invalid_discovery_info": "Ugyldig oppdagelsesinformasjon mottatt", + "link_local_address": "Lokale koblingsadresser st\u00f8ttes ikke", + "missing_configuration": "LaMetric-integrasjonen er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "no_devices": "Den autoriserte brukeren har ingen LaMetric-enheter", + "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "En LaMetric-enhet kan settes opp i Home Assistant p\u00e5 to forskjellige m\u00e5ter. \n\n Du kan skrive inn all enhetsinformasjon og API-tokens selv, eller Home Assistant kan importere dem fra LaMetric.com-kontoen din.", + "menu_options": { + "manual_entry": "G\u00e5 inn manuelt", + "pick_implementation": "Importer fra LaMetric.com (anbefalt)" + } + }, + "manual_entry": { + "data": { + "api_key": "API-n\u00f8kkel", + "host": "Vert" + }, + "data_description": { + "api_key": "Du finner denne API-n\u00f8kkelen p\u00e5 [enhetssiden i LaMetric-utviklerkontoen din](https://developer.lametric.com/user/devices).", + "host": "IP-adressen eller vertsnavnet til LaMetric TIME p\u00e5 nettverket ditt." + } + }, + "pick_implementation": { + "title": "Velg godkjenningsmetode" + }, + "user_cloud_select_device": { + "data": { + "device": "Velg LaMetric-enheten du vil legge til" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "LaMetric-integrasjonen er modernisert: Den er n\u00e5 konfigurert og satt opp via brukergrensesnittet og kommunikasjonen er n\u00e5 lokal. \n\n Dessverre er det ingen automatisk migreringsbane mulig og krever derfor at du konfigurerer LaMetric p\u00e5 nytt med Home Assistant. Se integreringsdokumentasjonen for Home Assistant LaMetric for hvordan du setter den opp. \n\n Fjern den gamle LaMetric YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Manuell migrering kreves for LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/pt-BR.json b/homeassistant/components/lametric/translations/pt-BR.json new file mode 100644 index 00000000000..4153b06e94d --- /dev/null +++ b/homeassistant/components/lametric/translations/pt-BR.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", + "invalid_discovery_info": "Informa\u00e7\u00f5es de descoberta inv\u00e1lidas recebidas", + "link_local_address": "Endere\u00e7os locais de links n\u00e3o s\u00e3o suportados", + "missing_configuration": "A integra\u00e7\u00e3o LaMetric n\u00e3o est\u00e1 configurada. Por favor, siga a documenta\u00e7\u00e3o.", + "no_devices": "O usu\u00e1rio autorizado n\u00e3o possui dispositivos LaMetric", + "no_url_available": "Nenhuma URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre este erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "Um dispositivo LaMetric pode ser configurado no Home Assistant de duas maneiras diferentes. \n\n Voc\u00ea mesmo pode inserir todas as informa\u00e7\u00f5es do dispositivo e tokens de API, ou o Home Assistant pode import\u00e1-los de sua conta LaMetric.com.", + "menu_options": { + "manual_entry": "Entre manualmente", + "pick_implementation": "Importar do LaMetric.com (recomendado)" + } + }, + "manual_entry": { + "data": { + "api_key": "Chave API", + "host": "Host" + }, + "data_description": { + "api_key": "Voc\u00ea pode encontrar essa chave de API em [p\u00e1gina de dispositivos em sua conta de desenvolvedor LaMetric](https://developer.lametric.com/user/devices).", + "host": "O endere\u00e7o IP ou nome de host do seu LaMetric TIME em sua rede." + } + }, + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "user_cloud_select_device": { + "data": { + "device": "Selecione o dispositivo LaMetric para adicionar" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "A integra\u00e7\u00e3o LaMetric foi modernizada: agora est\u00e1 configurada e configurada atrav\u00e9s da interface do usu\u00e1rio e as comunica\u00e7\u00f5es agora s\u00e3o locais. \n\n Infelizmente, n\u00e3o h\u00e1 caminho de migra\u00e7\u00e3o autom\u00e1tica poss\u00edvel e, portanto, exige que voc\u00ea reconfigure seu LaMetric com o Home Assistant. Consulte a documenta\u00e7\u00e3o de integra\u00e7\u00e3o do Home Assistant LaMetric sobre como configur\u00e1-lo. \n\n Remova a configura\u00e7\u00e3o antiga do LaMetric YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "Migra\u00e7\u00e3o manual necess\u00e1ria para LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/ru.json b/homeassistant/components/lametric/translations/ru.json new file mode 100644 index 00000000000..34a1bb58a62 --- /dev/null +++ b/homeassistant/components/lametric/translations/ru.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "invalid_discovery_info": "\u041f\u0440\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0431\u044b\u043b\u0430 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0430 \u043d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f.", + "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", + "missing_configuration": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f LaMetric \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.", + "no_devices": "\u0423 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 LaMetric.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e LaMetric \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant \u0434\u0432\u0443\u043c\u044f \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u043c\u0438 \u0441\u043f\u043e\u0441\u043e\u0431\u0430\u043c\u0438.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u0432\u0435\u0441\u0442\u0438 \u0432\u0441\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e\u0431 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 \u0438 API-\u0442\u043e\u043a\u0435\u043d\u044b \u0441\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e, \u0438\u043b\u0438 Home Asssistant \u043c\u043e\u0436\u0435\u0442 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0445 \u0438\u0437 \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 LaMetric.com.", + "menu_options": { + "manual_entry": "\u0412\u0432\u0435\u0441\u0442\u0438 \u0432\u0440\u0443\u0447\u043d\u0443\u044e", + "pick_implementation": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441 LaMetric.com (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f)" + } + }, + "manual_entry": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "host": "\u0425\u043e\u0441\u0442" + }, + "data_description": { + "api_key": "\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0439\u0442\u0438 \u043a\u043b\u044e\u0447 API \u043d\u0430 [\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0432 \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0430 LaMetric](https://developer.lametric.com/user/devices).", + "host": "IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 LaMetric TIME \u0432 \u0412\u0430\u0448\u0435\u0439 \u0441\u0435\u0442\u0438." + } + }, + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "user_cloud_select_device": { + "data": { + "device": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e LaMetric" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f LaMetric \u043c\u043e\u0434\u0435\u0440\u043d\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0422\u0435\u043f\u0435\u0440\u044c \u043e\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0438 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0447\u0435\u0440\u0435\u0437 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441, \u0430 \u043e\u0431\u043c\u0435\u043d \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438.\n\n\u041a \u0441\u043e\u0436\u0430\u043b\u0435\u043d\u0438\u044e, \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0438\u043c\u043f\u043e\u0440\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u0435\u043d, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0437\u0430\u043d\u043e\u0432\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c LaMetric \u0432 Home Assistant. \u0427\u0442\u043e\u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0432\u0441\u044e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439 \u043f\u043e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 LaMetric.\n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e LaMetric \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u043d\u0430 \u043d\u043e\u0432\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/zh-Hant.json b/homeassistant/components/lametric/translations/zh-Hant.json new file mode 100644 index 00000000000..e9c2835756c --- /dev/null +++ b/homeassistant/components/lametric/translations/zh-Hant.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", + "invalid_discovery_info": "\u63a5\u6536\u5230\u7121\u6548\u7684\u63a2\u7d22\u8cc7\u8a0a", + "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", + "missing_configuration": "LaMetric \u6574\u5408\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "no_devices": "\u8a8d\u8b49\u4f7f\u7528\u8005\u6c92\u6709\u4efb\u4f55 LaMetric \u88dd\u7f6e", + "no_url_available": "\u6c92\u6709\u53ef\u7528\u7684\u7db2\u5740\u3002\u95dc\u65bc\u6b64\u932f\u8aa4\u66f4\u8a73\u7d30\u8a0a\u606f\uff0c[\u9ede\u9078\u5354\u52a9\u7ae0\u7bc0]({docs_url})" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "\u6709\u5169\u7a2e\u4e0d\u540c\u65b9\u6cd5\u53ef\u4ee5\u5c07 LaMetric \u88dd\u7f6e\u6574\u5408\u9032 Home Assistant\u3002\n\n\u53ef\u4ee5\u81ea\u884c\u8f38\u5165 API \u6b0a\u6756\u8207\u5168\u90e8\u88dd\u7f6e\u8cc7\u8a0a\uff0c\u6216\u8005 Home Asssistant \u53ef\u4ee5\u7531 LaMetric.com \u5e33\u865f\u9032\u884c\u532f\u5165\u3002", + "menu_options": { + "manual_entry": "\u624b\u52d5\u8f38\u5165", + "pick_implementation": "\u7531 LaMetric.com \u532f\u5165\uff08\u5efa\u8b70\uff09" + } + }, + "manual_entry": { + "data": { + "api_key": "API \u91d1\u9470", + "host": "\u4e3b\u6a5f\u7aef" + }, + "data_description": { + "api_key": "\u53ef\u4ee5\u65bc [LaMetric \u958b\u767c\u8005\u5e33\u865f\u7684\u88dd\u7f6e\u9801\u9762 account](https://developer.lametric.com/user/devices) \u4e2d\u627e\u5230 API \u91d1\u9470\u3002", + "host": "\u7db2\u8def\u4e2d LaMetric TIME \u7684 IP \u4f4d\u5740\u6216\u4e3b\u6a5f\u540d\u7a31\u3002" + } + }, + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, + "user_cloud_select_device": { + "data": { + "device": "\u9078\u64c7\u6240\u8981\u65b0\u589e\u7684 LaMetric \u88dd\u7f6e" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "LaMetric \u6574\u5408\u5df2\u7d93\u9032\u884c\u66f4\u65b0\uff1a\u73fe\u5728\u53ef\u900f\u904e\u4f7f\u7528\u8005\u4ecb\u9762\u9032\u884c\u8a2d\u5b9a\u3001\u4e26\u4e14\u70ba\u672c\u5730\u7aef\u901a\u8a0a\u65b9\u5f0f\u3002\n\n\u4e0d\u5e78\u7684\u3001\u76ee\u524d\u6c92\u6709\u81ea\u52d5\u8f49\u79fb\u7684\u529f\u80fd\uff0c\u9700\u8981\u65bc Home Assistant \u91cd\u65b0\u8f38\u8a2d\u5b9a LaMetric\u3002\u8acb\u53c3\u95b1 Home Assistant LaMetric \u6574\u5408\u6587\u4ef6\u4ee5\u4e86\u89e3\u5982\u4f55\u9032\u884c\u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 LaMetric YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "\u9700\u8981\u624b\u52d5\u9032\u884c LaMetric \u6574\u5408\u8f49\u79fb" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/de.json b/homeassistant/components/landisgyr_heat_meter/translations/de.json new file mode 100644 index 00000000000..e8a48a02c51 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "USB-Ger\u00e4te-Pfad" + } + }, + "user": { + "data": { + "device": "Ger\u00e4t w\u00e4hlen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/el.json b/homeassistant/components/landisgyr_heat_meter/translations/el.json new file mode 100644 index 00000000000..1f7d7b27df9 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/el.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + }, + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 USB" + } + }, + "user": { + "data": { + "device": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/en.json b/homeassistant/components/landisgyr_heat_meter/translations/en.json index 6915e8cb36a..84caa2819a4 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/en.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/en.json @@ -5,19 +5,18 @@ }, "error": { "cannot_connect": "Failed to connect", - "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, "step": { + "setup_serial_manual_path": { + "data": { + "device": "USB Device Path" + } + }, "user": { "data": { "device": "Select device" } - }, - "setup_serial_manual_path": { - "data": { - "device": "USB-device path" - } } } } diff --git a/homeassistant/components/landisgyr_heat_meter/translations/es.json b/homeassistant/components/landisgyr_heat_meter/translations/es.json new file mode 100644 index 00000000000..956cebf852d --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "Ruta del dispositivo USB" + } + }, + "user": { + "data": { + "device": "Selecciona el dispositivo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/fr.json b/homeassistant/components/landisgyr_heat_meter/translations/fr.json new file mode 100644 index 00000000000..42d2fe61555 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "Chemin du p\u00e9riph\u00e9rique USB" + } + }, + "user": { + "data": { + "device": "S\u00e9lectionner un appareil" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json b/homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json new file mode 100644 index 00000000000..1ac8f7b38bf --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "unknown": "Erro inesperado" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "Caminho do dispositivo USB" + } + }, + "user": { + "data": { + "device": "Selecione o dispositivo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tankerkoenig/translations/es.json b/homeassistant/components/tankerkoenig/translations/es.json index ec0f079cbea..27ee462e17b 100644 --- a/homeassistant/components/tankerkoenig/translations/es.json +++ b/homeassistant/components/tankerkoenig/translations/es.json @@ -19,7 +19,7 @@ "stations": "Estaciones" }, "description": "se encontraron {stations_count} estaciones en el radio", - "title": "Selecciona las estaciones a a\u00f1adir" + "title": "Selecciona las estaciones para a\u00f1adir" }, "user": { "data": { diff --git a/homeassistant/components/xiaomi_ble/translations/es.json b/homeassistant/components/xiaomi_ble/translations/es.json index 504c7bc845c..357bb14cdf4 100644 --- a/homeassistant/components/xiaomi_ble/translations/es.json +++ b/homeassistant/components/xiaomi_ble/translations/es.json @@ -20,7 +20,7 @@ "description": "\u00bfQuieres configurar {name}?" }, "confirm_slow": { - "description": "No ha habido una transmisi\u00f3n desde este dispositivo en el \u00faltimo minuto, por lo que no estamos seguros de si este dispositivo usa cifrado o no. Esto puede deberse a que el dispositivo utiliza un intervalo de transmisi\u00f3n lento. Confirma para agregar este dispositivo de todos modos, luego, la pr\u00f3xima vez que se reciba una transmisi\u00f3n, se te pedir\u00e1 que ingreses su clave de enlace si es necesario." + "description": "No ha habido una transmisi\u00f3n desde este dispositivo en el \u00faltimo minuto, por lo que no estamos seguros de si este dispositivo usa cifrado o no. Esto puede deberse a que el dispositivo utiliza un intervalo de transmisi\u00f3n lento. Confirma para a\u00f1adir este dispositivo de todos modos, luego, la pr\u00f3xima vez que se reciba una transmisi\u00f3n, se te pedir\u00e1 que ingreses su clave de enlace si es necesario." }, "get_encryption_key_4_5": { "data": { diff --git a/homeassistant/components/yalexs_ble/translations/ja.json b/homeassistant/components/yalexs_ble/translations/ja.json index b847383ba9b..5e752cb1cfa 100644 --- a/homeassistant/components/yalexs_ble/translations/ja.json +++ b/homeassistant/components/yalexs_ble/translations/ja.json @@ -15,6 +15,9 @@ }, "flow_title": "{name}", "step": { + "integration_discovery_confirm": { + "description": "\u30a2\u30c9\u30ec\u30b9 {address} \u3092\u3001Bluetooth\u7d4c\u7531\u3067 {name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, "user": { "data": { "address": "Bluetooth\u30a2\u30c9\u30ec\u30b9", From cd59d3ab81b189cf6d14f91d88d92051e3c43dee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Aug 2022 15:41:07 -1000 Subject: [PATCH 3461/3516] Add support for multiple Bluetooth adapters (#76963) --- .../components/bluetooth/__init__.py | 119 +++++-- .../components/bluetooth/config_flow.py | 150 +++++---- homeassistant/components/bluetooth/const.py | 28 +- homeassistant/components/bluetooth/manager.py | 30 +- .../components/bluetooth/manifest.json | 2 +- .../components/bluetooth/strings.json | 23 +- .../components/bluetooth/translations/en.json | 21 +- homeassistant/components/bluetooth/util.py | 70 +++- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/bluetooth/__init__.py | 28 +- tests/components/bluetooth/conftest.py | 70 ++++ .../components/bluetooth/test_config_flow.py | 305 +++++++++--------- tests/components/bluetooth/test_init.py | 277 ++++++++-------- tests/components/bluetooth/test_scanner.py | 81 ++--- tests/conftest.py | 17 +- 17 files changed, 738 insertions(+), 489 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 8bb275c94fd..e659cec60a0 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from asyncio import Future from collections.abc import Callable +import platform from typing import TYPE_CHECKING import async_timeout @@ -11,11 +12,22 @@ from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback as hass_callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import discovery_flow +from homeassistant.helpers import device_registry as dr, discovery_flow from homeassistant.loader import async_get_bluetooth from . import models -from .const import CONF_ADAPTER, DATA_MANAGER, DOMAIN, SOURCE_LOCAL +from .const import ( + ADAPTER_ADDRESS, + ADAPTER_HW_VERSION, + ADAPTER_SW_VERSION, + CONF_ADAPTER, + CONF_DETAILS, + DATA_MANAGER, + DEFAULT_ADDRESS, + DOMAIN, + SOURCE_LOCAL, + AdapterDetails, +) from .manager import BluetoothManager from .match import BluetoothCallbackMatcher, IntegrationMatcher from .models import ( @@ -28,7 +40,7 @@ from .models import ( ProcessAdvertisementCallback, ) from .scanner import HaScanner, create_bleak_scanner -from .util import async_get_bluetooth_adapters +from .util import adapter_human_name, adapter_unique_name, async_default_adapter if TYPE_CHECKING: from bleak.backends.device import BLEDevice @@ -164,37 +176,88 @@ def async_rediscover_address(hass: HomeAssistant, address: str) -> None: manager.async_rediscover_address(address) -async def _async_has_bluetooth_adapter() -> bool: - """Return if the device has a bluetooth adapter.""" - return bool(await async_get_bluetooth_adapters()) - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the bluetooth integration.""" integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass)) + manager = BluetoothManager(hass, integration_matcher) manager.async_setup() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, manager.async_stop) hass.data[DATA_MANAGER] = models.MANAGER = manager - # The config entry is responsible for starting the manager - # if its enabled - if hass.config_entries.async_entries(DOMAIN): - return True - if DOMAIN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data={} - ) + adapters = await manager.async_get_bluetooth_adapters() + + async_migrate_entries(hass, adapters) + await async_discover_adapters(hass, adapters) + + return True + + +@hass_callback +def async_migrate_entries( + hass: HomeAssistant, + adapters: dict[str, AdapterDetails], +) -> None: + """Migrate config entries to support multiple.""" + current_entries = hass.config_entries.async_entries(DOMAIN) + default_adapter = async_default_adapter() + + for entry in current_entries: + if entry.unique_id: + continue + + address = DEFAULT_ADDRESS + adapter = entry.options.get(CONF_ADAPTER, default_adapter) + if adapter in adapters: + address = adapters[adapter][ADAPTER_ADDRESS] + hass.config_entries.async_update_entry( + entry, title=adapter_unique_name(adapter, address), unique_id=address ) - elif await _async_has_bluetooth_adapter(): + + +async def async_discover_adapters( + hass: HomeAssistant, + adapters: dict[str, AdapterDetails], +) -> None: + """Discover adapters and start flows.""" + if platform.system() == "Windows": + # We currently do not have a good way to detect if a bluetooth device is + # available on Windows. We will just assume that it is not unless they + # actively add it. + return + + for adapter, details in adapters.items(): discovery_flow.async_create_flow( hass, DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={}, + data={CONF_ADAPTER: adapter, CONF_DETAILS: details}, ) - return True + + +async def async_update_device( + entry: config_entries.ConfigEntry, + manager: BluetoothManager, + adapter: str, + address: str, +) -> None: + """Update device registry entry. + + The physical adapter can change from hci0/hci1 on reboot + or if the user moves around the usb sticks so we need to + update the device with the new location so they can + figure out where the adapter is. + """ + adapters = await manager.async_get_bluetooth_adapters() + details = adapters[adapter] + registry = dr.async_get(manager.hass) + registry.async_get_or_create( + config_entry_id=entry.entry_id, + name=adapter_human_name(adapter, details[ADAPTER_ADDRESS]), + connections={(dr.CONNECTION_BLUETOOTH, details[ADAPTER_ADDRESS])}, + sw_version=details.get(ADAPTER_SW_VERSION), + hw_version=details.get(ADAPTER_HW_VERSION), + ) async def async_setup_entry( @@ -202,7 +265,12 @@ async def async_setup_entry( ) -> bool: """Set up a config entry for a bluetooth scanner.""" manager: BluetoothManager = hass.data[DATA_MANAGER] - adapter: str | None = entry.options.get(CONF_ADAPTER) + address = entry.unique_id + assert address is not None + adapter = await manager.async_get_adapter_from_address(address) + if adapter is None: + raise ConfigEntryNotReady(f"Bluetooth adapter with address {address} not found") + try: bleak_scanner = create_bleak_scanner(BluetoothScanningMode.ACTIVE, adapter) except RuntimeError as err: @@ -211,18 +279,11 @@ async def async_setup_entry( entry.async_on_unload(scanner.async_register_callback(manager.scanner_adv_received)) await scanner.async_start() entry.async_on_unload(manager.async_register_scanner(scanner)) - entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + await async_update_device(entry, manager, adapter, address) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = scanner return True -async def _async_update_listener( - hass: HomeAssistant, entry: config_entries.ConfigEntry -) -> None: - """Handle options update.""" - await hass.config_entries.async_reload(entry.entry_id) - - async def async_unload_entry( hass: HomeAssistant, entry: config_entries.ConfigEntry ) -> bool: diff --git a/homeassistant/components/bluetooth/config_flow.py b/homeassistant/components/bluetooth/config_flow.py index 1a0be8706bf..2435a1e39ed 100644 --- a/homeassistant/components/bluetooth/config_flow.py +++ b/homeassistant/components/bluetooth/config_flow.py @@ -1,16 +1,16 @@ """Config flow to configure the Bluetooth integration.""" from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast import voluptuous as vol from homeassistant.components import onboarding -from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow -from homeassistant.core import callback +from homeassistant.config_entries import ConfigFlow +from homeassistant.helpers.typing import DiscoveryInfoType -from .const import CONF_ADAPTER, DEFAULT_NAME, DOMAIN -from .util import async_get_bluetooth_adapters +from .const import ADAPTER_ADDRESS, CONF_ADAPTER, CONF_DETAILS, DOMAIN, AdapterDetails +from .util import adapter_human_name, adapter_unique_name, async_get_bluetooth_adapters if TYPE_CHECKING: from homeassistant.data_entry_flow import FlowResult @@ -21,60 +21,94 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Initialize the config flow.""" + self._adapter: str | None = None + self._details: AdapterDetails | None = None + self._adapters: dict[str, AdapterDetails] = {} + + async def async_step_integration_discovery( + self, discovery_info: DiscoveryInfoType + ) -> FlowResult: + """Handle a flow initialized by discovery.""" + self._adapter = cast(str, discovery_info[CONF_ADAPTER]) + self._details = cast(AdapterDetails, discovery_info[CONF_DETAILS]) + await self.async_set_unique_id(self._details[ADAPTER_ADDRESS]) + self._abort_if_unique_id_configured() + self.context["title_placeholders"] = { + "name": adapter_human_name(self._adapter, self._details[ADAPTER_ADDRESS]) + } + return await self.async_step_single_adapter() + + async def async_step_single_adapter( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Select an adapter.""" + adapter = self._adapter + details = self._details + assert adapter is not None + assert details is not None + + address = details[ADAPTER_ADDRESS] + + if user_input is not None or not onboarding.async_is_onboarded(self.hass): + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=adapter_unique_name(adapter, address), data={} + ) + + return self.async_show_form( + step_id="single_adapter", + description_placeholders={"name": adapter_human_name(adapter, address)}, + ) + + async def async_step_multiple_adapters( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + if user_input is not None: + assert self._adapters is not None + adapter = user_input[CONF_ADAPTER] + address = self._adapters[adapter][ADAPTER_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=adapter_unique_name(adapter, address), data={} + ) + + configured_addresses = self._async_current_ids() + self._adapters = await async_get_bluetooth_adapters() + unconfigured_adapters = [ + adapter + for adapter, details in self._adapters.items() + if details[ADAPTER_ADDRESS] not in configured_addresses + ] + if not unconfigured_adapters: + return self.async_abort(reason="no_adapters") + if len(unconfigured_adapters) == 1: + self._adapter = list(self._adapters)[0] + self._details = self._adapters[self._adapter] + return await self.async_step_single_adapter() + + return self.async_show_form( + step_id="multiple_adapters", + data_schema=vol.Schema( + { + vol.Required(CONF_ADAPTER): vol.In( + { + adapter: adapter_human_name( + adapter, self._adapters[adapter][ADAPTER_ADDRESS] + ) + for adapter in sorted(unconfigured_adapters) + } + ), + } + ), + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initialized by the user.""" - return await self.async_step_enable_bluetooth() - - async def async_step_enable_bluetooth( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle a flow initialized by the user or import.""" - if self._async_current_entries(): - return self.async_abort(reason="already_configured") - - if user_input is not None or not onboarding.async_is_onboarded(self.hass): - return self.async_create_entry(title=DEFAULT_NAME, data={}) - - return self.async_show_form(step_id="enable_bluetooth") - - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Handle import from configuration.yaml.""" - return await self.async_step_enable_bluetooth(user_input) - - @staticmethod - @callback - def async_get_options_flow( - config_entry: ConfigEntry, - ) -> OptionsFlowHandler: - """Get the options flow for this handler.""" - return OptionsFlowHandler(config_entry) - - -class OptionsFlowHandler(OptionsFlow): - """Handle the option flow for bluetooth.""" - - def __init__(self, config_entry: ConfigEntry) -> None: - """Initialize options flow.""" - self.config_entry = config_entry - - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle options flow.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - if not (adapters := await async_get_bluetooth_adapters()): - return self.async_abort(reason="no_adapters") - - data_schema = vol.Schema( - { - vol.Required( - CONF_ADAPTER, - default=self.config_entry.options.get(CONF_ADAPTER, adapters[0]), - ): vol.In(adapters), - } - ) - return self.async_show_form(step_id="init", data_schema=data_schema) + return await self.async_step_multiple_adapters() diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index 04581b841b9..0cd02bcbb8d 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -2,18 +2,27 @@ from __future__ import annotations from datetime import timedelta -from typing import Final +from typing import Final, TypedDict DOMAIN = "bluetooth" -DEFAULT_NAME = "Bluetooth" CONF_ADAPTER = "adapter" +CONF_DETAILS = "details" -MACOS_DEFAULT_BLUETOOTH_ADAPTER = "CoreBluetooth" +WINDOWS_DEFAULT_BLUETOOTH_ADAPTER = "bluetooth" +MACOS_DEFAULT_BLUETOOTH_ADAPTER = "Core Bluetooth" UNIX_DEFAULT_BLUETOOTH_ADAPTER = "hci0" DEFAULT_ADAPTERS = {MACOS_DEFAULT_BLUETOOTH_ADAPTER, UNIX_DEFAULT_BLUETOOTH_ADAPTER} +DEFAULT_ADAPTER_BY_PLATFORM = { + "Windows": WINDOWS_DEFAULT_BLUETOOTH_ADAPTER, + "Darwin": MACOS_DEFAULT_BLUETOOTH_ADAPTER, +} + +# Some operating systems hide the adapter address for privacy reasons (ex MacOS) +DEFAULT_ADDRESS: Final = "00:00:00:00:00:00" + SOURCE_LOCAL: Final = "local" DATA_MANAGER: Final = "bluetooth_manager" @@ -22,3 +31,16 @@ UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 START_TIMEOUT = 12 SCANNER_WATCHDOG_TIMEOUT: Final = 60 * 5 SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=SCANNER_WATCHDOG_TIMEOUT) + + +class AdapterDetails(TypedDict, total=False): + """Adapter details.""" + + address: str + sw_version: str + hw_version: str + + +ADAPTER_ADDRESS: Final = "address" +ADAPTER_SW_VERSION: Final = "sw_version" +ADAPTER_HW_VERSION: Final = "hw_version" diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 0fba2d2aae1..0b588e71681 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -20,7 +20,12 @@ from homeassistant.core import ( from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval -from .const import SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS +from .const import ( + ADAPTER_ADDRESS, + SOURCE_LOCAL, + UNAVAILABLE_TRACK_SECONDS, + AdapterDetails, +) from .match import ( ADDRESS, BluetoothCallbackMatcher, @@ -29,6 +34,7 @@ from .match import ( ) from .models import BluetoothCallback, BluetoothChange, BluetoothServiceInfoBleak from .usage import install_multiple_bleak_catcher, uninstall_multiple_bleak_catcher +from .util import async_get_bluetooth_adapters if TYPE_CHECKING: from bleak.backends.device import BLEDevice @@ -39,7 +45,7 @@ if TYPE_CHECKING: FILTER_UUIDS: Final = "UUIDs" -RSSI_SWITCH_THRESHOLD = 10 +RSSI_SWITCH_THRESHOLD = 6 STALE_ADVERTISEMENT_SECONDS = 180 _LOGGER = logging.getLogger(__name__) @@ -132,6 +138,26 @@ class BluetoothManager: ] = [] self.history: dict[str, AdvertisementHistory] = {} self._scanners: list[HaScanner] = [] + self._adapters: dict[str, AdapterDetails] = {} + + def _find_adapter_by_address(self, address: str) -> str | None: + for adapter, details in self._adapters.items(): + if details[ADAPTER_ADDRESS] == address: + return adapter + return None + + async def async_get_bluetooth_adapters(self) -> dict[str, AdapterDetails]: + """Get bluetooth adapters.""" + if not self._adapters: + self._adapters = await async_get_bluetooth_adapters() + return self._adapters + + async def async_get_adapter_from_address(self, address: str) -> str | None: + """Get adapter from address.""" + if adapter := self._find_adapter_by_address(address): + return adapter + self._adapters = await async_get_bluetooth_adapters() + return self._find_adapter_by_address(address) @hass_callback def async_setup(self) -> None: diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index f3828db5d10..ff99bd3d97d 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.15.1", "bluetooth-adapters==0.1.3"], + "requirements": ["bleak==0.15.1", "bluetooth-adapters==0.2.0"], "codeowners": ["@bdraco"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/components/bluetooth/strings.json b/homeassistant/components/bluetooth/strings.json index beff2fd8312..269995192a8 100644 --- a/homeassistant/components/bluetooth/strings.json +++ b/homeassistant/components/bluetooth/strings.json @@ -2,9 +2,6 @@ "config": { "flow_title": "{name}", "step": { - "enable_bluetooth": { - "description": "Do you want to setup Bluetooth?" - }, "user": { "description": "Choose a device to setup", "data": { @@ -13,20 +10,20 @@ }, "bluetooth_confirm": { "description": "Do you want to setup {name}?" + }, + "multiple_adapters": { + "description": "Select a Bluetooth adapter to setup", + "data": { + "adapter": "Adapter" + } + }, + "single_adapter": { + "description": "Do you want to setup the Bluetooth adapter {name}?" } }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", - "no_adapters": "No Bluetooth adapters found" - } - }, - "options": { - "step": { - "init": { - "data": { - "adapter": "The Bluetooth Adapter to use for scanning" - } - } + "no_adapters": "No unconfigured Bluetooth adapters found" } } } diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json index 4b53822b771..ac80cfb620e 100644 --- a/homeassistant/components/bluetooth/translations/en.json +++ b/homeassistant/components/bluetooth/translations/en.json @@ -2,15 +2,21 @@ "config": { "abort": { "already_configured": "Service is already configured", - "no_adapters": "No Bluetooth adapters found" + "no_adapters": "No unconfigured Bluetooth adapters found" }, "flow_title": "{name}", "step": { "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, - "enable_bluetooth": { - "description": "Do you want to setup Bluetooth?" + "multiple_adapters": { + "data": { + "adapter": "Adapter" + }, + "description": "Select a Bluetooth adapter to setup" + }, + "single_adapter": { + "description": "Do you want to setup the Bluetooth adapter {name}?" }, "user": { "data": { @@ -19,14 +25,5 @@ "description": "Choose a device to setup" } } - }, - "options": { - "step": { - "init": { - "data": { - "adapter": "The Bluetooth Adapter to use for scanning" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 68920050748..3133b2f210d 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -3,25 +3,65 @@ from __future__ import annotations import platform -from .const import MACOS_DEFAULT_BLUETOOTH_ADAPTER, UNIX_DEFAULT_BLUETOOTH_ADAPTER +from homeassistant.core import callback + +from .const import ( + DEFAULT_ADAPTER_BY_PLATFORM, + DEFAULT_ADDRESS, + MACOS_DEFAULT_BLUETOOTH_ADAPTER, + UNIX_DEFAULT_BLUETOOTH_ADAPTER, + WINDOWS_DEFAULT_BLUETOOTH_ADAPTER, + AdapterDetails, +) -async def async_get_bluetooth_adapters() -> list[str]: +async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]: """Return a list of bluetooth adapters.""" - if platform.system() == "Windows": # We don't have a good way to detect on windows - return [] - if platform.system() == "Darwin": # CoreBluetooth is built in on MacOS hardware - return [MACOS_DEFAULT_BLUETOOTH_ADAPTER] + if platform.system() == "Windows": + return { + WINDOWS_DEFAULT_BLUETOOTH_ADAPTER: AdapterDetails( + address=DEFAULT_ADDRESS, + sw_version=platform.release(), + ) + } + if platform.system() == "Darwin": + return { + MACOS_DEFAULT_BLUETOOTH_ADAPTER: AdapterDetails( + address=DEFAULT_ADDRESS, + sw_version=platform.release(), + ) + } from bluetooth_adapters import ( # pylint: disable=import-outside-toplevel - get_bluetooth_adapters, + get_bluetooth_adapter_details, ) - adapters = await get_bluetooth_adapters() - if ( - UNIX_DEFAULT_BLUETOOTH_ADAPTER in adapters - and adapters[0] != UNIX_DEFAULT_BLUETOOTH_ADAPTER - ): - # The default adapter always needs to be the first in the list - # because that is how bleak works. - adapters.insert(0, adapters.pop(adapters.index(UNIX_DEFAULT_BLUETOOTH_ADAPTER))) + adapters: dict[str, AdapterDetails] = {} + adapter_details = await get_bluetooth_adapter_details() + for adapter, details in adapter_details.items(): + adapter1 = details["org.bluez.Adapter1"] + adapters[adapter] = AdapterDetails( + address=adapter1["Address"], + sw_version=adapter1["Name"], # This is actually the BlueZ version + hw_version=adapter1["Modalias"], + ) return adapters + + +@callback +def async_default_adapter() -> str: + """Return the default adapter for the platform.""" + return DEFAULT_ADAPTER_BY_PLATFORM.get( + platform.system(), UNIX_DEFAULT_BLUETOOTH_ADAPTER + ) + + +@callback +def adapter_human_name(adapter: str, address: str) -> str: + """Return a human readable name for the adapter.""" + return adapter if address == DEFAULT_ADDRESS else f"{adapter} ({address})" + + +@callback +def adapter_unique_name(adapter: str, address: str) -> str: + """Return a unique name for the adapter.""" + return adapter if address == DEFAULT_ADDRESS else address diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8381e204a77..b6e5dbc4119 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ attrs==21.2.0 awesomeversion==22.6.0 bcrypt==3.1.7 bleak==0.15.1 -bluetooth-adapters==0.1.3 +bluetooth-adapters==0.2.0 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 diff --git a/requirements_all.txt b/requirements_all.txt index 322d67a94df..733d82c9668 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -424,7 +424,7 @@ blockchain==1.4.4 # bluepy==1.3.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.1.3 +bluetooth-adapters==0.2.0 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8c67a756157..2a5a6c3f6cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,7 +335,7 @@ blebox_uniapi==2.0.2 blinkpy==0.19.0 # homeassistant.components.bluetooth -bluetooth-adapters==0.1.3 +bluetooth-adapters==0.2.0 # homeassistant.components.bond bond-async==0.1.22 diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index 44da1a60f03..220432c46c2 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -6,8 +6,13 @@ from unittest.mock import patch from bleak.backends.scanner import AdvertisementData, BLEDevice -from homeassistant.components.bluetooth import SOURCE_LOCAL, models +from homeassistant.components.bluetooth import DOMAIN, SOURCE_LOCAL, models +from homeassistant.components.bluetooth.const import DEFAULT_ADDRESS from homeassistant.components.bluetooth.manager import BluetoothManager +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry def _get_manager() -> BluetoothManager: @@ -48,3 +53,24 @@ def patch_discovered_devices(mock_discovered: list[BLEDevice]) -> None: return patch.object( manager, "async_discovered_devices", return_value=mock_discovered ) + + +async def async_setup_with_default_adapter(hass: HomeAssistant) -> MockConfigEntry: + """Set up the Bluetooth integration with a default adapter.""" + return await _async_setup_with_adapter(hass, DEFAULT_ADDRESS) + + +async def async_setup_with_one_adapter(hass: HomeAssistant) -> MockConfigEntry: + """Set up the Bluetooth integration with one adapter.""" + return await _async_setup_with_adapter(hass, "00:00:00:00:00:01") + + +async def _async_setup_with_adapter( + hass: HomeAssistant, address: str +) -> MockConfigEntry: + """Set up the Bluetooth integration with any adapter.""" + entry = MockConfigEntry(domain="bluetooth", unique_id=address) + entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + return entry diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 760500fe7a1..5ddd0fbc15f 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -1 +1,71 @@ """Tests for the bluetooth component.""" + +from unittest.mock import patch + +import pytest + + +@pytest.fixture(name="macos_adapter") +def macos_adapter(): + """Fixture that mocks the macos adapter.""" + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Darwin" + ): + yield + + +@pytest.fixture(name="windows_adapter") +def windows_adapter(): + """Fixture that mocks the windows adapter.""" + with patch( + "homeassistant.components.bluetooth.util.platform.system", + return_value="Windows", + ): + yield + + +@pytest.fixture(name="one_adapter") +def one_adapter_fixture(): + """Fixture that mocks one adapter on Linux.""" + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.get_bluetooth_adapter_details", + return_value={ + "hci0": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:01", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + }, + ): + yield + + +@pytest.fixture(name="two_adapters") +def two_adapters_fixture(): + """Fixture that mocks two adapters on Linux.""" + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.get_bluetooth_adapter_details", + return_value={ + "hci0": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:01", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + "hci1": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:02", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + }, + ): + yield diff --git a/tests/components/bluetooth/test_config_flow.py b/tests/components/bluetooth/test_config_flow.py index 1053133cac9..e16208b3d70 100644 --- a/tests/components/bluetooth/test_config_flow.py +++ b/tests/components/bluetooth/test_config_flow.py @@ -5,38 +5,88 @@ from unittest.mock import patch from homeassistant import config_entries from homeassistant.components.bluetooth.const import ( CONF_ADAPTER, + CONF_DETAILS, + DEFAULT_ADDRESS, DOMAIN, - MACOS_DEFAULT_BLUETOOTH_ADAPTER, + AdapterDetails, ) from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry -async def test_async_step_user(hass): - """Test setting up manually.""" +async def test_async_step_user_macos(hass, macos_adapter): + """Test setting up manually with one adapter on MacOS.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data={}, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "enable_bluetooth" + assert result["step_id"] == "single_adapter" with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( "homeassistant.components.bluetooth.async_setup_entry", return_value=True ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Bluetooth" + assert result2["title"] == "Core Bluetooth" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 -async def test_async_step_user_only_allows_one(hass): +async def test_async_step_user_linux_one_adapter(hass, one_adapter): + """Test setting up manually with one adapter on Linux.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "single_adapter" + with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "00:00:00:00:00:01" + assert result2["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_async_step_user_linux_two_adapters(hass, two_adapters): + """Test setting up manually with two adapters on Linux.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "multiple_adapters" + with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_ADAPTER: "hci1"} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "00:00:00:00:00:02" + assert result2["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_async_step_user_only_allows_one(hass, macos_adapter): """Test setting up manually with an existing entry.""" - entry = MockConfigEntry(domain=DOMAIN) + entry = MockConfigEntry(domain=DOMAIN, unique_id=DEFAULT_ADDRESS) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, @@ -44,34 +94,48 @@ async def test_async_step_user_only_allows_one(hass): data={}, ) assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" + assert result["reason"] == "no_adapters" async def test_async_step_integration_discovery(hass): """Test setting up from integration discovery.""" + + details = AdapterDetails( + address="00:00:00:00:00:01", sw_version="1.23.5", hw_version="1.2.3" + ) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={}, + data={CONF_ADAPTER: "hci0", CONF_DETAILS: details}, ) assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "enable_bluetooth" + assert result["step_id"] == "single_adapter" with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( "homeassistant.components.bluetooth.async_setup_entry", return_value=True ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Bluetooth" + assert result2["title"] == "00:00:00:00:00:01" assert result2["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 -async def test_async_step_integration_discovery_during_onboarding(hass): +async def test_async_step_integration_discovery_during_onboarding_one_adapter( + hass, one_adapter +): """Test setting up from integration discovery during onboarding.""" + details = AdapterDetails( + address="00:00:00:00:00:01", sw_version="1.23.5", hw_version="1.2.3" + ) with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( "homeassistant.components.bluetooth.async_setup_entry", return_value=True ) as mock_setup_entry, patch( "homeassistant.components.onboarding.async_is_onboarded", @@ -80,10 +144,77 @@ async def test_async_step_integration_discovery_during_onboarding(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={}, + data={CONF_ADAPTER: "hci0", CONF_DETAILS: details}, ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Bluetooth" + assert result["title"] == "00:00:00:00:00:01" + assert result["data"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_onboarding.mock_calls) == 1 + + +async def test_async_step_integration_discovery_during_onboarding_two_adapters( + hass, two_adapters +): + """Test setting up from integration discovery during onboarding.""" + details1 = AdapterDetails( + address="00:00:00:00:00:01", sw_version="1.23.5", hw_version="1.2.3" + ) + details2 = AdapterDetails( + address="00:00:00:00:00:02", sw_version="1.23.5", hw_version="1.2.3" + ) + + with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={CONF_ADAPTER: "hci0", CONF_DETAILS: details1}, + ) + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={CONF_ADAPTER: "hci1", CONF_DETAILS: details2}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "00:00:00:00:00:01" + assert result["data"] == {} + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "00:00:00:00:00:02" + assert result2["data"] == {} + + assert len(mock_setup_entry.mock_calls) == 2 + assert len(mock_onboarding.mock_calls) == 2 + + +async def test_async_step_integration_discovery_during_onboarding(hass, macos_adapter): + """Test setting up from integration discovery during onboarding.""" + details = AdapterDetails( + address=DEFAULT_ADDRESS, sw_version="1.23.5", hw_version="1.2.3" + ) + + with patch( + "homeassistant.components.bluetooth.async_setup", return_value=True + ), patch( + "homeassistant.components.bluetooth.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + data={CONF_ADAPTER: "Core Bluetooth", CONF_DETAILS: details}, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Core Bluetooth" assert result["data"] == {} assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_onboarding.mock_calls) == 1 @@ -91,150 +222,16 @@ async def test_async_step_integration_discovery_during_onboarding(hass): async def test_async_step_integration_discovery_already_exists(hass): """Test setting up from integration discovery when an entry already exists.""" - entry = MockConfigEntry(domain=DOMAIN) + details = AdapterDetails( + address="00:00:00:00:00:01", sw_version="1.23.5", hw_version="1.2.3" + ) + + entry = MockConfigEntry(domain=DOMAIN, unique_id="00:00:00:00:00:01") entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, - data={}, + data={CONF_ADAPTER: "hci0", CONF_DETAILS: details}, ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" - - -async def test_async_step_import(hass): - """Test setting up from integration discovery.""" - with patch( - "homeassistant.components.bluetooth.async_setup_entry", return_value=True - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={}, - ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Bluetooth" - assert result["data"] == {} - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_async_step_import_already_exists(hass): - """Test setting up from yaml when an entry already exists.""" - entry = MockConfigEntry(domain=DOMAIN) - entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={}, - ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" - - -@patch("homeassistant.components.bluetooth.util.platform.system", return_value="Linux") -async def test_options_flow_linux(mock_system, hass, mock_bleak_scanner_start): - """Test options on Linux.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={}, - unique_id="DOMAIN", - ) - entry.add_to_hass(hass) - - # Verify we can keep it as hci0 - with patch( - "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci0", "hci1"] - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "init" - assert result["errors"] is None - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_ADAPTER: "hci0", - }, - ) - await hass.async_block_till_done() - - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"][CONF_ADAPTER] == "hci0" - - # Verify we can change it to hci1 - with patch( - "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci0", "hci1"] - ): - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "init" - assert result["errors"] is None - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_ADAPTER: "hci1", - }, - ) - await hass.async_block_till_done() - - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"][CONF_ADAPTER] == "hci1" - - -@patch("homeassistant.components.bluetooth.util.platform.system", return_value="Darwin") -async def test_options_flow_macos(mock_system, hass, mock_bleak_scanner_start): - """Test options on MacOS.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={}, - unique_id="DOMAIN", - ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "init" - assert result["errors"] is None - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - CONF_ADAPTER: MACOS_DEFAULT_BLUETOOTH_ADAPTER, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["data"][CONF_ADAPTER] == MACOS_DEFAULT_BLUETOOTH_ADAPTER - - -@patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Windows" -) -async def test_options_flow_windows(mock_system, hass, mock_bleak_scanner_start): - """Test options on Windows.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={}, - options={}, - unique_id="DOMAIN", - ) - entry.add_to_hass(hass) - - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "no_adapters" diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 84c37300dc4..57fcb8402a0 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -19,6 +19,7 @@ from homeassistant.components.bluetooth import ( scanner, ) from homeassistant.components.bluetooth.const import ( + DEFAULT_ADDRESS, SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, ) @@ -28,7 +29,12 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import _get_manager, inject_advertisement, patch_discovered_devices +from . import ( + _get_manager, + async_setup_with_default_adapter, + inject_advertisement, + patch_discovered_devices, +) from tests.common import MockConfigEntry, async_fire_time_changed @@ -52,7 +58,7 @@ async def test_setup_and_stop(hass, mock_bleak_scanner_start, enable_bluetooth): assert len(mock_bleak_scanner_start.mock_calls) == 1 -async def test_setup_and_stop_no_bluetooth(hass, caplog): +async def test_setup_and_stop_no_bluetooth(hass, caplog, macos_adapter): """Test we fail gracefully when bluetooth is not available.""" mock_bt = [ {"domain": "switchbot", "service_uuid": "cba20d00-224d-11e6-9fb8-0002a5d5c51b"} @@ -63,10 +69,7 @@ async def test_setup_and_stop_no_bluetooth(hass, caplog): ) as mock_ha_bleak_scanner, patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -76,7 +79,7 @@ async def test_setup_and_stop_no_bluetooth(hass, caplog): assert "Failed to initialize Bluetooth" in caplog.text -async def test_setup_and_stop_broken_bluetooth(hass, caplog): +async def test_setup_and_stop_broken_bluetooth(hass, caplog, macos_adapter): """Test we fail gracefully when bluetooth/dbus is broken.""" mock_bt = [] with patch( @@ -85,10 +88,7 @@ async def test_setup_and_stop_broken_bluetooth(hass, caplog): ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -98,7 +98,7 @@ async def test_setup_and_stop_broken_bluetooth(hass, caplog): assert len(bluetooth.async_discovered_service_info(hass)) == 0 -async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): +async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog, macos_adapter): """Test we fail gracefully when bluetooth/dbus is hanging.""" mock_bt = [] @@ -111,10 +111,7 @@ async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -123,7 +120,7 @@ async def test_setup_and_stop_broken_bluetooth_hanging(hass, caplog): assert "Timed out starting Bluetooth" in caplog.text -async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): +async def test_setup_and_retry_adapter_not_yet_available(hass, caplog, macos_adapter): """Test we retry if the adapter is not yet available.""" mock_bt = [] with patch( @@ -132,10 +129,7 @@ async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -159,7 +153,7 @@ async def test_setup_and_retry_adapter_not_yet_available(hass, caplog): await hass.async_block_till_done() -async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): +async def test_no_race_during_manual_reload_in_retry_state(hass, caplog, macos_adapter): """Test we can successfully reload when the entry is in a retry state.""" mock_bt = [] with patch( @@ -168,10 +162,7 @@ async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -196,7 +187,9 @@ async def test_no_race_during_manual_reload_in_retry_state(hass, caplog): await hass.async_block_till_done() -async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): +async def test_calling_async_discovered_devices_no_bluetooth( + hass, caplog, macos_adapter +): """Test we fail gracefully when asking for discovered devices and there is no blueooth.""" mock_bt = [] with patch( @@ -205,9 +198,7 @@ async def test_calling_async_discovered_devices_no_bluetooth(hass, caplog): ), patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -228,9 +219,7 @@ async def test_discovery_match_by_service_uuid( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -256,16 +245,15 @@ async def test_discovery_match_by_service_uuid( assert mock_config_flow.mock_calls[0][1][0] == "switchbot" -async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): +async def test_discovery_match_by_local_name( + hass, mock_bleak_scanner_start, macos_adapter +): """Test bluetooth discovery match by local_name.""" mock_bt = [{"domain": "switchbot", "local_name": "wohand"}] with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -292,7 +280,7 @@ async def test_discovery_match_by_local_name(hass, mock_bleak_scanner_start): async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( - hass, mock_bleak_scanner_start + hass, mock_bleak_scanner_start, macos_adapter ): """Test bluetooth discovery match by manufacturer_id and manufacturer_data_start.""" mock_bt = [ @@ -305,10 +293,7 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -371,7 +356,7 @@ async def test_discovery_match_by_manufacturer_id_and_manufacturer_data_start( async def test_discovery_match_by_service_data_uuid_then_others( - hass, mock_bleak_scanner_start + hass, mock_bleak_scanner_start, macos_adapter ): """Test bluetooth discovery match by service_data_uuid and then other fields.""" mock_bt = [ @@ -391,10 +376,7 @@ async def test_discovery_match_by_service_data_uuid_then_others( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -526,7 +508,7 @@ async def test_discovery_match_by_service_data_uuid_then_others( async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( - hass, mock_bleak_scanner_start + hass, mock_bleak_scanner_start, macos_adapter ): """Test bluetooth discovery matches twice for service_uuid and then manufacturer_id.""" mock_bt = [ @@ -542,10 +524,7 @@ async def test_discovery_match_first_by_service_uuid_and_then_manufacturer_id( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -600,9 +579,7 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth): with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ), patch.object(hass.config_entries.flow, "async_init") as mock_config_flow: - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -631,7 +608,9 @@ async def test_rediscovery(hass, mock_bleak_scanner_start, enable_bluetooth): assert mock_config_flow.mock_calls[1][1][0] == "switchbot" -async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): +async def test_async_discovered_device_api( + hass, mock_bleak_scanner_start, macos_adapter +): """Test the async_discovered_device API.""" mock_bt = [] with patch( @@ -642,10 +621,7 @@ async def test_async_discovered_device_api(hass, mock_bleak_scanner_start): ): assert not bluetooth.async_discovered_service_info(hass) assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22") - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -738,9 +714,8 @@ async def test_register_callbacks(hass, mock_bleak_scanner_start, enable_bluetoo with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ), patch.object(hass.config_entries.flow, "async_init"): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) + await async_setup_with_default_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -821,10 +796,7 @@ async def test_register_callback_by_address( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -913,10 +885,7 @@ async def test_register_callback_survives_reload( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=mock_bt ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -933,7 +902,7 @@ async def test_register_callback_survives_reload( switchbot_device = BLEDevice("44:44:33:11:23:45", "wohand") switchbot_adv = AdvertisementData( local_name="wohand", - service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + service_uuids=["zba20d00-224d-11e6-9fb8-0002a5d5c51b"], manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, service_data={"00000d00-0000-1000-8000-00805f9b34fb": b"H\x10c"}, ) @@ -1063,10 +1032,7 @@ async def test_wrapped_instance_with_filter( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1132,10 +1098,7 @@ async def test_wrapped_instance_with_service_uuids( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1184,10 +1147,7 @@ async def test_wrapped_instance_with_broken_callbacks( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] ), patch.object(hass.config_entries.flow, "async_init"): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1231,10 +1191,7 @@ async def test_wrapped_instance_changes_uuids( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1283,10 +1240,7 @@ async def test_wrapped_instance_changes_filters( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1335,10 +1289,7 @@ async def test_wrapped_instance_unsupported_filter( with patch( "homeassistant.components.bluetooth.async_get_bluetooth", return_value=[] ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_default_adapter(hass) with patch.object(hass.config_entries.flow, "async_init"): hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) @@ -1354,7 +1305,9 @@ async def test_wrapped_instance_unsupported_filter( assert "Only UUIDs filters are supported" in caplog.text -async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): +async def test_async_ble_device_from_address( + hass, mock_bleak_scanner_start, macos_adapter +): """Test the async_ble_device_from_address api.""" mock_bt = [] with patch( @@ -1369,9 +1322,8 @@ async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): bluetooth.async_ble_device_from_address(hass, "44:44:33:11:23:45") is None ) - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) + await async_setup_with_default_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -1394,26 +1346,14 @@ async def test_async_ble_device_from_address(hass, mock_bleak_scanner_start): ) -async def test_setup_without_bluetooth_in_configuration_yaml(hass, mock_bluetooth): - """Test setting up without bluetooth in configuration.yaml does not create the config entry.""" - assert await async_setup_component(hass, bluetooth.DOMAIN, {}) - await hass.async_block_till_done() - assert not hass.config_entries.async_entries(bluetooth.DOMAIN) - - -async def test_setup_with_bluetooth_in_configuration_yaml(hass, mock_bluetooth): - """Test setting up with bluetooth in configuration.yaml creates the config entry.""" - assert await async_setup_component(hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}}) - await hass.async_block_till_done() - assert hass.config_entries.async_entries(bluetooth.DOMAIN) - - -async def test_can_unsetup_bluetooth(hass, mock_bleak_scanner_start, enable_bluetooth): +async def test_can_unsetup_bluetooth_single_adapter_macos( + hass, mock_bleak_scanner_start, enable_bluetooth, macos_adapter +): """Test we can setup and unsetup bluetooth.""" - entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}) + entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}, unique_id=DEFAULT_ADDRESS) entry.add_to_hass(hass) - for _ in range(2): + for _ in range(2): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -1421,35 +1361,80 @@ async def test_can_unsetup_bluetooth(hass, mock_bleak_scanner_start, enable_blue await hass.async_block_till_done() -async def test_auto_detect_bluetooth_adapters_linux(hass): +async def test_can_unsetup_bluetooth_single_adapter_linux( + hass, mock_bleak_scanner_start, enable_bluetooth, one_adapter +): + """Test we can setup and unsetup bluetooth.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01" + ) + entry.add_to_hass(hass) + + for _ in range(2): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_can_unsetup_bluetooth_multiple_adapters( + hass, mock_bleak_scanner_start, enable_bluetooth, two_adapters +): + """Test we can setup and unsetup bluetooth with multiple adapters.""" + entry1 = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:01" + ) + entry1.add_to_hass(hass) + + entry2 = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:02" + ) + entry2.add_to_hass(hass) + + for _ in range(2): + for entry in (entry1, entry2): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_three_adapters_one_missing( + hass, mock_bleak_scanner_start, enable_bluetooth, two_adapters +): + """Test three adapters but one is missing results in a retry on setup.""" + entry = MockConfigEntry( + domain=bluetooth.DOMAIN, data={}, unique_id="00:00:00:00:00:03" + ) + entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_RETRY + + +async def test_auto_detect_bluetooth_adapters_linux(hass, one_adapter): """Test we auto detect bluetooth adapters on linux.""" - with patch( - "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci0"] - ), patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" - ): - assert await async_setup_component(hass, bluetooth.DOMAIN, {}) - await hass.async_block_till_done() + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() assert not hass.config_entries.async_entries(bluetooth.DOMAIN) assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1 -async def test_auto_detect_bluetooth_adapters_linux_multiple(hass): +async def test_auto_detect_bluetooth_adapters_linux_multiple(hass, two_adapters): """Test we auto detect bluetooth adapters on linux with multiple adapters.""" - with patch( - "bluetooth_adapters.get_bluetooth_adapters", return_value=["hci1", "hci0"] - ), patch( - "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" - ): - assert await async_setup_component(hass, bluetooth.DOMAIN, {}) - await hass.async_block_till_done() + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() assert not hass.config_entries.async_entries(bluetooth.DOMAIN) - assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 1 + assert len(hass.config_entries.flow.async_progress(bluetooth.DOMAIN)) == 2 async def test_auto_detect_bluetooth_adapters_linux_none_found(hass): """Test we auto detect bluetooth adapters on linux with no adapters found.""" - with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=set()), patch( + with patch( + "bluetooth_adapters.get_bluetooth_adapter_details", return_value={} + ), patch( "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" ): assert await async_setup_component(hass, bluetooth.DOMAIN, {}) @@ -1485,3 +1470,23 @@ async def test_getting_the_scanner_returns_the_wrapped_instance(hass, enable_blu """Test getting the scanner returns the wrapped instance.""" scanner = bluetooth.async_get_scanner(hass) assert isinstance(scanner, models.HaBleakScannerWrapper) + + +async def test_migrate_single_entry_macos( + hass, mock_bleak_scanner_start, macos_adapter +): + """Test we can migrate a single entry on MacOS.""" + entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}) + entry.add_to_hass(hass) + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert entry.unique_id == DEFAULT_ADDRESS + + +async def test_migrate_single_entry_linux(hass, mock_bleak_scanner_start, one_adapter): + """Test we can migrate a single entry on Linux.""" + entry = MockConfigEntry(domain=bluetooth.DOMAIN, data={}) + entry.add_to_hass(hass) + assert await async_setup_component(hass, bluetooth.DOMAIN, {}) + await hass.async_block_till_done() + assert entry.unique_id == "00:00:00:00:00:01" diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index 032b67662df..bde1dbd1696 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -11,23 +11,20 @@ from dbus_next import InvalidMessageError from homeassistant.components import bluetooth from homeassistant.components.bluetooth.const import ( - CONF_ADAPTER, SCANNER_WATCHDOG_INTERVAL, SCANNER_WATCHDOG_TIMEOUT, - UNIX_DEFAULT_BLUETOOTH_ADAPTER, ) from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP -from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from . import _get_manager +from . import _get_manager, async_setup_with_one_adapter -from tests.common import MockConfigEntry, async_fire_time_changed +from tests.common import async_fire_time_changed async def test_config_entry_can_be_reloaded_when_stop_raises( - hass, caplog, enable_bluetooth + hass, caplog, enable_bluetooth, macos_adapter ): """Test we can reload if stopping the scanner raises.""" entry = hass.config_entries.async_entries(bluetooth.DOMAIN)[0] @@ -44,31 +41,7 @@ async def test_config_entry_can_be_reloaded_when_stop_raises( assert "Error stopping scanner" in caplog.text -async def test_changing_the_adapter_at_runtime(hass): - """Test we can change the adapter at runtime.""" - entry = MockConfigEntry( - domain=bluetooth.DOMAIN, - data={}, - options={CONF_ADAPTER: UNIX_DEFAULT_BLUETOOTH_ADAPTER}, - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start" - ), patch("homeassistant.components.bluetooth.scanner.OriginalBleakScanner.stop"): - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - entry.options = {CONF_ADAPTER: "hci1"} - - await hass.config_entries.async_reload(entry.entry_id) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - - -async def test_dbus_socket_missing_in_container(hass, caplog): +async def test_dbus_socket_missing_in_container(hass, caplog, one_adapter): """Test we handle dbus being missing in the container.""" with patch( @@ -77,10 +50,8 @@ async def test_dbus_socket_missing_in_container(hass, caplog): "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=FileNotFoundError, ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_one_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -90,7 +61,7 @@ async def test_dbus_socket_missing_in_container(hass, caplog): assert "docker" in caplog.text -async def test_dbus_socket_missing(hass, caplog): +async def test_dbus_socket_missing(hass, caplog, one_adapter): """Test we handle dbus being missing.""" with patch( @@ -99,10 +70,8 @@ async def test_dbus_socket_missing(hass, caplog): "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=FileNotFoundError, ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_one_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -112,7 +81,7 @@ async def test_dbus_socket_missing(hass, caplog): assert "docker" not in caplog.text -async def test_dbus_broken_pipe_in_container(hass, caplog): +async def test_dbus_broken_pipe_in_container(hass, caplog, one_adapter): """Test we handle dbus broken pipe in the container.""" with patch( @@ -121,10 +90,8 @@ async def test_dbus_broken_pipe_in_container(hass, caplog): "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=BrokenPipeError, ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_one_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -135,7 +102,7 @@ async def test_dbus_broken_pipe_in_container(hass, caplog): assert "container" in caplog.text -async def test_dbus_broken_pipe(hass, caplog): +async def test_dbus_broken_pipe(hass, caplog, one_adapter): """Test we handle dbus broken pipe.""" with patch( @@ -144,10 +111,8 @@ async def test_dbus_broken_pipe(hass, caplog): "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=BrokenPipeError, ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_one_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -158,17 +123,15 @@ async def test_dbus_broken_pipe(hass, caplog): assert "container" not in caplog.text -async def test_invalid_dbus_message(hass, caplog): +async def test_invalid_dbus_message(hass, caplog, one_adapter): """Test we handle invalid dbus message.""" with patch( "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", side_effect=InvalidMessageError, ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_one_adapter(hass) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -177,7 +140,7 @@ async def test_invalid_dbus_message(hass, caplog): assert "dbus" in caplog.text -async def test_recovery_from_dbus_restart(hass): +async def test_recovery_from_dbus_restart(hass, one_adapter): """Test we can recover when DBus gets restarted out from under us.""" called_start = 0 @@ -213,10 +176,8 @@ async def test_recovery_from_dbus_restart(hass): "homeassistant.components.bluetooth.scanner.OriginalBleakScanner", return_value=scanner, ): - assert await async_setup_component( - hass, bluetooth.DOMAIN, {bluetooth.DOMAIN: {}} - ) - await hass.async_block_till_done() + await async_setup_with_one_adapter(hass) + assert called_start == 1 start_time_monotonic = 1000 diff --git a/tests/conftest.py b/tests/conftest.py index 4c268206805..0c0a654059b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -876,7 +876,7 @@ async def mock_enable_bluetooth( hass, mock_bleak_scanner_start, mock_bluetooth_adapters ): """Fixture to mock starting the bleak scanner.""" - entry = MockConfigEntry(domain="bluetooth") + entry = MockConfigEntry(domain="bluetooth", unique_id="00:00:00:00:00:01") entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -885,7 +885,20 @@ async def mock_enable_bluetooth( @pytest.fixture(name="mock_bluetooth_adapters") def mock_bluetooth_adapters(): """Fixture to mock bluetooth adapters.""" - with patch("bluetooth_adapters.get_bluetooth_adapters", return_value=[]): + with patch( + "homeassistant.components.bluetooth.util.platform.system", return_value="Linux" + ), patch( + "bluetooth_adapters.get_bluetooth_adapter_details", + return_value={ + "hci0": { + "org.bluez.Adapter1": { + "Address": "00:00:00:00:00:01", + "Name": "BlueZ 4.63", + "Modalias": "usbid:1234", + } + }, + }, + ): yield From 09aaf45f0a879c8ecdad8c0fa98398e59900c71b Mon Sep 17 00:00:00 2001 From: Kevin Addeman Date: Thu, 18 Aug 2022 22:23:20 -0400 Subject: [PATCH 3462/3516] Fix lutron caseta Sunnata Keypad support (#75324) Co-authored-by: J. Nick Koston --- .../components/lutron_caseta/__init__.py | 3 +- .../lutron_caseta/device_trigger.py | 31 +++++++++++++++---- .../lutron_caseta/test_device_trigger.py | 4 +-- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index d235f294b3a..5653504c98a 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -46,6 +46,7 @@ from .const import ( from .device_trigger import ( DEVICE_TYPE_SUBTYPE_MAP_TO_LIP, LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP, + _lutron_model_to_device_type, ) from .models import LutronCasetaData from .util import serial_to_unique_id @@ -281,7 +282,7 @@ def _async_subscribe_pico_remote_events( else: action = ACTION_RELEASE - type_ = device["type"] + type_ = _lutron_model_to_device_type(device["model"], device["type"]) area, name = _area_and_name_from_name(device["name"]) leap_button_number = device["button_number"] lip_button_number = async_get_lip_button(type_, leap_button_number) diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index 77cad154c06..b355c3dcc3f 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -39,6 +39,13 @@ def _reverse_dict(forward_dict: dict) -> dict: return {v: k for k, v in forward_dict.items()} +LUTRON_MODEL_TO_TYPE = { + "RRST-W2B-XX": "SunnataKeypad_2Button", + "RRST-W3RL-XX": "SunnataKeypad_3ButtonRaiseLower", + "RRST-W4B-XX": "SunnataKeypad_4Button", +} + + SUPPORTED_INPUTS_EVENTS_TYPES = [ACTION_PRESS, ACTION_RELEASE] LUTRON_BUTTON_TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( @@ -379,9 +386,13 @@ async def async_validate_trigger_config( if not device: return config - if not (schema := DEVICE_TYPE_SCHEMA_MAP.get(device["type"])): + if not ( + schema := DEVICE_TYPE_SCHEMA_MAP.get( + _lutron_model_to_device_type(device["model"], device["type"]) + ) + ): raise InvalidDeviceAutomationConfig( - f"Device type {device['type']} not supported: {config[CONF_DEVICE_ID]}" + f"Device model {device['model']} with type {device['type']} not supported: {config[CONF_DEVICE_ID]}" ) return schema(config) @@ -396,7 +407,9 @@ async def async_get_triggers( if not (device := get_button_device_by_dr_id(hass, device_id)): raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}") - valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.get(device["type"], {}) + valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.get( + _lutron_model_to_device_type(device["model"], device["type"]), {} + ) for trigger in SUPPORTED_INPUTS_EVENTS_TYPES: for subtype in valid_buttons: @@ -413,10 +426,16 @@ async def async_get_triggers( return triggers -def _device_model_to_type(model: str) -> str: +def _device_model_to_type(device_registry_model: str) -> str: """Convert a lutron_caseta device registry entry model to type.""" - _, device_type = model.split(" ") - return device_type.replace("(", "").replace(")", "") + model, p_device_type = device_registry_model.split(" ") + device_type = p_device_type.replace("(", "").replace(")", "") + return _lutron_model_to_device_type(model, device_type) + + +def _lutron_model_to_device_type(model: str, device_type: str) -> str: + """Get the mapped type based on the lutron model or type.""" + return LUTRON_MODEL_TO_TYPE.get(model, device_type) async def async_attach_trigger( diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index 54cd842f0ee..b8c655a23bd 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -64,8 +64,8 @@ MOCK_BUTTON_DEVICES = [ {"Number": 11}, ], "leap_name": "Front Steps_Front Steps Sunnata Keypad", - "type": "SunnataKeypad_3ButtonRaiseLower", - "model": "PJ2-3BRL-GXX-X01", + "type": "SunnataKeypad", + "model": "RRST-W4B-XX", "serial": 43845547, }, ] From 72a4f8af3d9f614653f141e1cccf61a26694e2cd Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Fri, 19 Aug 2022 09:07:32 +0300 Subject: [PATCH 3463/3516] Add config flow to `pushover` (#74500) * Add config flow to `pushover` * Add tests for reauth * add deprecated yaml issue * address comments * fix test error, other fixes * update translations --- CODEOWNERS | 2 + homeassistant/components/pushover/__init__.py | 54 +++++ .../components/pushover/config_flow.py | 106 +++++++++ homeassistant/components/pushover/const.py | 20 ++ .../components/pushover/manifest.json | 4 +- homeassistant/components/pushover/notify.py | 122 ++++++---- .../components/pushover/strings.json | 34 +++ .../components/pushover/translations/en.json | 34 +++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/pushover/__init__.py | 10 + tests/components/pushover/test_config_flow.py | 218 ++++++++++++++++++ tests/components/pushover/test_init.py | 98 ++++++++ 13 files changed, 660 insertions(+), 46 deletions(-) create mode 100644 homeassistant/components/pushover/config_flow.py create mode 100644 homeassistant/components/pushover/const.py create mode 100644 homeassistant/components/pushover/strings.json create mode 100644 homeassistant/components/pushover/translations/en.json create mode 100644 tests/components/pushover/__init__.py create mode 100644 tests/components/pushover/test_config_flow.py create mode 100644 tests/components/pushover/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index ea9e47a9423..9a0c092eceb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -848,6 +848,8 @@ build.json @home-assistant/supervisor /tests/components/pure_energie/ @klaasnicolaas /homeassistant/components/push/ @dgomes /tests/components/push/ @dgomes +/homeassistant/components/pushover/ @engrbm87 +/tests/components/pushover/ @engrbm87 /homeassistant/components/pvoutput/ @frenck /tests/components/pvoutput/ @frenck /homeassistant/components/pvpc_hourly_pricing/ @azogue diff --git a/homeassistant/components/pushover/__init__.py b/homeassistant/components/pushover/__init__.py index 921d37ed332..fa9a9c5ebd9 100644 --- a/homeassistant/components/pushover/__init__.py +++ b/homeassistant/components/pushover/__init__.py @@ -1 +1,55 @@ """The pushover component.""" +from __future__ import annotations + +from pushover_complete import BadAPIRequestError, PushoverAPI + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import discovery +from homeassistant.helpers.typing import ConfigType + +from .const import CONF_USER_KEY, DATA_HASS_CONFIG, DOMAIN + +PLATFORMS = [Platform.NOTIFY] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the pushover component.""" + + hass.data[DATA_HASS_CONFIG] = config + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up pushover from a config entry.""" + + pushover_api = PushoverAPI(entry.data[CONF_API_KEY]) + try: + await hass.async_add_executor_job( + pushover_api.validate, entry.data[CONF_USER_KEY] + ) + + except BadAPIRequestError as err: + if "application token is invalid" in str(err): + raise ConfigEntryAuthFailed(err) from err + raise ConfigEntryNotReady(err) from err + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = pushover_api + + hass.async_create_task( + discovery.async_load_platform( + hass, + Platform.NOTIFY, + DOMAIN, + { + CONF_NAME: entry.data[CONF_NAME], + CONF_USER_KEY: entry.data[CONF_USER_KEY], + "entry_id": entry.entry_id, + }, + hass.data[DATA_HASS_CONFIG], + ) + ) + + return True diff --git a/homeassistant/components/pushover/config_flow.py b/homeassistant/components/pushover/config_flow.py new file mode 100644 index 00000000000..3f12446733e --- /dev/null +++ b/homeassistant/components/pushover/config_flow.py @@ -0,0 +1,106 @@ +"""Config flow for pushover integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +from pushover_complete import BadAPIRequestError, PushoverAPI +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult + +from .const import CONF_USER_KEY, DEFAULT_NAME, DOMAIN + +USER_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, + vol.Required(CONF_API_KEY): str, + vol.Required(CONF_USER_KEY): str, + } +) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: + """Validate user input.""" + errors = {} + pushover_api = PushoverAPI(data[CONF_API_KEY]) + try: + await hass.async_add_executor_job(pushover_api.validate, data[CONF_USER_KEY]) + except BadAPIRequestError as err: + if "application token is invalid" in str(err): + errors[CONF_API_KEY] = "invalid_api_key" + elif "user key is invalid" in str(err): + errors[CONF_USER_KEY] = "invalid_user_key" + else: + errors["base"] = "cannot_connect" + return errors + + +class PushBulletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for pushover integration.""" + + _reauth_entry: config_entries.ConfigEntry | None + + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + """Handle import from config.""" + return await self.async_step_user(import_config) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Confirm reauth dialog.""" + errors = {} + if user_input is not None and self._reauth_entry: + user_input = {**self._reauth_entry.data, **user_input} + errors = await validate_input(self.hass, user_input) + if not errors: + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=user_input + ) + await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY): str, + } + ), + errors=errors, + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors = {} + + if user_input is not None: + await self.async_set_unique_id(user_input[CONF_USER_KEY]) + self._abort_if_unique_id_configured() + + self._async_abort_entries_match({CONF_NAME: user_input[CONF_NAME]}) + + errors = await validate_input(self.hass, user_input) + if not errors: + return self.async_create_entry( + title=user_input[CONF_NAME], + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=USER_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/pushover/const.py b/homeassistant/components/pushover/const.py new file mode 100644 index 00000000000..af541132297 --- /dev/null +++ b/homeassistant/components/pushover/const.py @@ -0,0 +1,20 @@ +"""Constants for pushover.""" + +from typing import Final + +DOMAIN: Final = "pushover" +DATA_HASS_CONFIG: Final = "pushover_hass_config" +DEFAULT_NAME: Final = "Pushover" + +ATTR_ATTACHMENT: Final = "attachment" +ATTR_URL: Final = "url" +ATTR_URL_TITLE: Final = "url_title" +ATTR_PRIORITY: Final = "priority" +ATTR_RETRY: Final = "retry" +ATTR_SOUND: Final = "sound" +ATTR_HTML: Final = "html" +ATTR_CALLBACK_URL: Final = "callback_url" +ATTR_EXPIRE: Final = "expire" +ATTR_TIMESTAMP: Final = "timestamp" + +CONF_USER_KEY: Final = "user_key" diff --git a/homeassistant/components/pushover/manifest.json b/homeassistant/components/pushover/manifest.json index 0752fbc7b78..a13de899480 100644 --- a/homeassistant/components/pushover/manifest.json +++ b/homeassistant/components/pushover/manifest.json @@ -1,9 +1,11 @@ { "domain": "pushover", "name": "Pushover", + "dependencies": ["repairs"], "documentation": "https://www.home-assistant.io/integrations/pushover", "requirements": ["pushover_complete==1.1.1"], - "codeowners": [], + "codeowners": ["@engrbm87"], + "config_flow": true, "iot_class": "cloud_push", "loggers": ["pushover_complete"] } diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index 8a18597c2ca..d9073b18a0a 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -1,7 +1,10 @@ """Pushover platform for notify component.""" -import logging +from __future__ import annotations -from pushover_complete import PushoverAPI +import logging +from typing import Any + +from pushover_complete import BadAPIRequestError, PushoverAPI import voluptuous as vol from homeassistant.components.notify import ( @@ -12,47 +15,82 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.components.repairs.issue_handler import async_create_issue +from homeassistant.components.repairs.models import IssueSeverity +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from ...exceptions import HomeAssistantError +from .const import ( + ATTR_ATTACHMENT, + ATTR_CALLBACK_URL, + ATTR_EXPIRE, + ATTR_HTML, + ATTR_PRIORITY, + ATTR_RETRY, + ATTR_SOUND, + ATTR_TIMESTAMP, + ATTR_URL, + ATTR_URL_TITLE, + CONF_USER_KEY, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) -ATTR_ATTACHMENT = "attachment" -ATTR_URL = "url" -ATTR_URL_TITLE = "url_title" -ATTR_PRIORITY = "priority" -ATTR_RETRY = "retry" -ATTR_SOUND = "sound" -ATTR_HTML = "html" -ATTR_CALLBACK_URL = "callback_url" -ATTR_EXPIRE = "expire" -ATTR_TIMESTAMP = "timestamp" - -CONF_USER_KEY = "user_key" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Required(CONF_USER_KEY): cv.string, vol.Required(CONF_API_KEY): cv.string} ) -def get_service(hass, config, discovery_info=None): +async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> PushoverNotificationService | None: """Get the Pushover notification service.""" + if discovery_info is None: + + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2022.11.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) + return None + + pushover_api: PushoverAPI = hass.data[DOMAIN][discovery_info["entry_id"]] return PushoverNotificationService( - hass, config[CONF_USER_KEY], config[CONF_API_KEY] + hass, pushover_api, discovery_info[CONF_USER_KEY] ) class PushoverNotificationService(BaseNotificationService): """Implement the notification service for Pushover.""" - def __init__(self, hass, user_key, api_token): + def __init__( + self, hass: HomeAssistant, pushover: PushoverAPI, user_key: str + ) -> None: """Initialize the service.""" self._hass = hass self._user_key = user_key - self._api_token = api_token - self.pushover = PushoverAPI(self._api_token) + self.pushover = pushover - def send_message(self, message="", **kwargs): + def send_message(self, message: str = "", **kwargs: dict[str, Any]) -> None: """Send a message to a user.""" # Extract params from data dict @@ -87,28 +125,22 @@ class PushoverNotificationService(BaseNotificationService): # Remove attachment key to send without attachment. image = None - targets = kwargs.get(ATTR_TARGET) - - if not isinstance(targets, list): - targets = [targets] - - for target in targets: - try: - self.pushover.send_message( - self._user_key, - message, - target, - title, - url, - url_title, - image, - priority, - retry, - expire, - callback_url, - timestamp, - sound, - html, - ) - except ValueError as val_err: - _LOGGER.error(val_err) + try: + self.pushover.send_message( + self._user_key, + message, + kwargs.get(ATTR_TARGET), + title, + url, + url_title, + image, + priority, + retry, + expire, + callback_url, + timestamp, + sound, + html, + ) + except BadAPIRequestError as err: + raise HomeAssistantError(str(err)) from err diff --git a/homeassistant/components/pushover/strings.json b/homeassistant/components/pushover/strings.json new file mode 100644 index 00000000000..c309a1ec01f --- /dev/null +++ b/homeassistant/components/pushover/strings.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", + "invalid_user_key": "Invalid user key" + }, + "step": { + "user": { + "data": { + "name": "[%key:common::config_flow::data::name%]", + "api_key": "[%key:common::config_flow::data::api_key%]", + "user_key": "User key" + } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "The Pushover YAML configuration is being removed", + "description": "Configuring Pushover using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Pushover YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/pushover/translations/en.json b/homeassistant/components/pushover/translations/en.json new file mode 100644 index 00000000000..33826000dc3 --- /dev/null +++ b/homeassistant/components/pushover/translations/en.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_api_key": "Invalid API key", + "invalid_user_key": "Invalid user key" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API Key" + }, + "title": "Reauthenticate Integration" + }, + "user": { + "data": { + "api_key": "API Key", + "name": "Name", + "user_key": "User key" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring Pushover using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the Pushover YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Pushover YAML configuration is being removed" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2ce09bfcafa..c604c5e95cd 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -290,6 +290,7 @@ FLOWS = { "prosegur", "ps4", "pure_energie", + "pushover", "pvoutput", "pvpc_hourly_pricing", "qingping", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a5a6c3f6cc..ef6a03ddcd9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -918,6 +918,9 @@ pure-python-adb[async]==0.3.0.dev0 # homeassistant.components.pushbullet pushbullet.py==0.11.0 +# homeassistant.components.pushover +pushover_complete==1.1.1 + # homeassistant.components.pvoutput pvo==0.2.2 diff --git a/tests/components/pushover/__init__.py b/tests/components/pushover/__init__.py new file mode 100644 index 00000000000..cb0f5788099 --- /dev/null +++ b/tests/components/pushover/__init__.py @@ -0,0 +1,10 @@ +"""Tests for the pushover component.""" + +from homeassistant.components.pushover.const import CONF_USER_KEY +from homeassistant.const import CONF_API_KEY, CONF_NAME + +MOCK_CONFIG = { + CONF_NAME: "Pushover", + CONF_API_KEY: "MYAPIKEY", + CONF_USER_KEY: "MYUSERKEY", +} diff --git a/tests/components/pushover/test_config_flow.py b/tests/components/pushover/test_config_flow.py new file mode 100644 index 00000000000..68672961a56 --- /dev/null +++ b/tests/components/pushover/test_config_flow.py @@ -0,0 +1,218 @@ +"""Test pushbullet config flow.""" +from unittest.mock import MagicMock, patch + +from pushover_complete import BadAPIRequestError +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.pushover.const import CONF_USER_KEY, DOMAIN +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant + +from . import MOCK_CONFIG + +from tests.common import MockConfigEntry + + +@pytest.fixture(autouse=True) +def mock_pushover(): + """Mock pushover.""" + with patch( + "pushover_complete.PushoverAPI._generic_post", return_value={} + ) as mock_generic_post: + yield mock_generic_post + + +@pytest.fixture(autouse=True) +def pushover_setup_fixture(): + """Patch pushover setup entry.""" + with patch( + "homeassistant.components.pushover.async_setup_entry", return_value=True + ): + yield + + +async def test_flow_user(hass: HomeAssistant) -> None: + """Test user initialized flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Pushover" + assert result["data"] == MOCK_CONFIG + + +async def test_flow_user_key_already_configured(hass: HomeAssistant) -> None: + """Test user initialized flow with duplicate user key.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + unique_id="MYUSERKEY", + ) + + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_name_already_configured(hass: HomeAssistant) -> None: + """Test user initialized flow with duplicate server.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + unique_id="MYUSERKEY", + ) + + entry.add_to_hass(hass) + + new_config = MOCK_CONFIG.copy() + new_config[CONF_USER_KEY] = "NEUSERWKEY" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=new_config, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_invalid_user_key( + hass: HomeAssistant, mock_pushover: MagicMock +) -> None: + """Test user initialized flow with wrong user key.""" + + mock_pushover.side_effect = BadAPIRequestError("400: user key is invalid") + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {CONF_USER_KEY: "invalid_user_key"} + + +async def test_flow_invalid_api_key( + hass: HomeAssistant, mock_pushover: MagicMock +) -> None: + """Test user initialized flow with wrong api key.""" + + mock_pushover.side_effect = BadAPIRequestError("400: application token is invalid") + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} + + +async def test_flow_conn_err(hass: HomeAssistant, mock_pushover: MagicMock) -> None: + """Test user initialized flow with conn error.""" + + mock_pushover.side_effect = BadAPIRequestError + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=MOCK_CONFIG, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_import(hass: HomeAssistant) -> None: + """Test user initialized flow with unreachable server.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_CONFIG, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Pushover" + assert result["data"] == MOCK_CONFIG + + +async def test_reauth_success(hass: HomeAssistant) -> None: + """Test we can reauth.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_CONFIG, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "NEWAPIKEY", + }, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + + +async def test_reauth_failed(hass: HomeAssistant, mock_pushover: MagicMock) -> None: + """Test we can reauth.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_CONFIG, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + mock_pushover.side_effect = BadAPIRequestError("400: application token is invalid") + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "WRONGAPIKEY", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == { + CONF_API_KEY: "invalid_api_key", + } diff --git a/tests/components/pushover/test_init.py b/tests/components/pushover/test_init.py new file mode 100644 index 00000000000..22b38be0b48 --- /dev/null +++ b/tests/components/pushover/test_init.py @@ -0,0 +1,98 @@ +"""Test pushbullet integration.""" +from collections.abc import Awaitable +from typing import Callable +from unittest.mock import MagicMock, patch + +import aiohttp +from pushover_complete import BadAPIRequestError +import pytest + +from homeassistant.components.notify.const import DOMAIN as NOTIFY_DOMAIN +from homeassistant.components.pushover.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from . import MOCK_CONFIG + +from tests.common import MockConfigEntry +from tests.components.repairs import get_repairs + + +@pytest.fixture(autouse=True) +def mock_pushover(): + """Mock pushover.""" + with patch( + "pushover_complete.PushoverAPI._generic_post", return_value={} + ) as mock_generic_post: + yield mock_generic_post + + +async def test_setup( + hass: HomeAssistant, + hass_ws_client: Callable[ + [HomeAssistant], Awaitable[aiohttp.ClientWebSocketResponse] + ], +) -> None: + """Test integration failed due to an error.""" + assert await async_setup_component( + hass, + NOTIFY_DOMAIN, + { + NOTIFY_DOMAIN: [ + { + "name": "Pushover", + "platform": "pushover", + "api_key": "MYAPIKEY", + "user_key": "MYUSERKEY", + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.config_entries.async_entries(DOMAIN) + issues = await get_repairs(hass, hass_ws_client) + assert len(issues) == 1 + assert issues[0]["issue_id"] == "deprecated_yaml" + + +async def test_async_setup_entry_success(hass: HomeAssistant) -> None: + """Test pushover successful setup.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED + + +async def test_async_setup_entry_failed_invalid_api_key( + hass: HomeAssistant, mock_pushover: MagicMock +) -> None: + """Test pushover failed setup due to invalid api key.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + mock_pushover.side_effect = BadAPIRequestError("400: application token is invalid") + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_ERROR + + +async def test_async_setup_entry_failed_conn_error( + hass: HomeAssistant, mock_pushover: MagicMock +) -> None: + """Test pushover failed setup due to conn error.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + mock_pushover.side_effect = BadAPIRequestError + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.SETUP_RETRY From 4eb4146e29f711aaf1bbde81c767d4e03ea01f93 Mon Sep 17 00:00:00 2001 From: rikroe <42204099+rikroe@users.noreply.github.com> Date: Fri, 19 Aug 2022 08:36:46 +0200 Subject: [PATCH 3464/3516] Remove unneeded charging_status attribute in bmw_connected_drive binary sensor (#74921) * Use `charging_status.value` in attribute for BMW binary sensor * Remove `charging_status` attribute Co-authored-by: rikroe --- homeassistant/components/bmw_connected_drive/binary_sensor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index d7e28eef41c..87506cc2230 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -180,9 +180,6 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( icon="mdi:ev-station", # device class power: On means power detected, Off means no power value_fn=lambda v: v.fuel_and_battery.charging_status == ChargingState.CHARGING, - attr_fn=lambda v, u: { - "charging_status": str(v.fuel_and_battery.charging_status), - }, ), BMWBinarySensorEntityDescription( key="connection_status", From 1faabb8f401806bec0c93ccb5dca8dc856a99f50 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 19 Aug 2022 08:58:18 +0200 Subject: [PATCH 3465/3516] Add timeouts to requests calls (#76851) --- homeassistant/components/abode/camera.py | 4 +++- homeassistant/components/facebox/image_processing.py | 7 +++++-- homeassistant/components/llamalab_automate/notify.py | 2 +- homeassistant/components/nest/legacy/camera.py | 2 +- homeassistant/components/opencv/image_processing.py | 2 +- homeassistant/components/uk_transport/sensor.py | 2 +- homeassistant/components/withings/common.py | 1 + 7 files changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index d0f428a45fa..c4c2d0dc78d 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -74,7 +74,9 @@ class AbodeCamera(AbodeDevice, Camera): """Attempt to download the most recent capture.""" if self._device.image_url: try: - self._response = requests.get(self._device.image_url, stream=True) + self._response = requests.get( + self._device.image_url, stream=True, timeout=10 + ) self._response.raise_for_status() except requests.HTTPError as err: diff --git a/homeassistant/components/facebox/image_processing.py b/homeassistant/components/facebox/image_processing.py index 21eb91a04a3..5584efb883a 100644 --- a/homeassistant/components/facebox/image_processing.py +++ b/homeassistant/components/facebox/image_processing.py @@ -68,7 +68,7 @@ def check_box_health(url, username, password): if username: kwargs["auth"] = requests.auth.HTTPBasicAuth(username, password) try: - response = requests.get(url, **kwargs) + response = requests.get(url, **kwargs, timeout=10) if response.status_code == HTTPStatus.UNAUTHORIZED: _LOGGER.error("AuthenticationError on %s", CLASSIFIER) return None @@ -116,7 +116,9 @@ def post_image(url, image, username, password): if username: kwargs["auth"] = requests.auth.HTTPBasicAuth(username, password) try: - response = requests.post(url, json={"base64": encode_image(image)}, **kwargs) + response = requests.post( + url, json={"base64": encode_image(image)}, timeout=10, **kwargs + ) if response.status_code == HTTPStatus.UNAUTHORIZED: _LOGGER.error("AuthenticationError on %s", CLASSIFIER) return None @@ -137,6 +139,7 @@ def teach_file(url, name, file_path, username, password): url, data={FACEBOX_NAME: name, ATTR_ID: file_path}, files={"file": open_file}, + timeout=10, **kwargs, ) if response.status_code == HTTPStatus.UNAUTHORIZED: diff --git a/homeassistant/components/llamalab_automate/notify.py b/homeassistant/components/llamalab_automate/notify.py index e2850792906..af0271e107d 100644 --- a/homeassistant/components/llamalab_automate/notify.py +++ b/homeassistant/components/llamalab_automate/notify.py @@ -66,6 +66,6 @@ class AutomateNotificationService(BaseNotificationService): "payload": message, } - response = requests.post(_RESOURCE, json=data) + response = requests.post(_RESOURCE, json=data, timeout=10) if response.status_code != HTTPStatus.OK: _LOGGER.error("Error sending message: %s", response) diff --git a/homeassistant/components/nest/legacy/camera.py b/homeassistant/components/nest/legacy/camera.py index 9974ae2daaa..affe912b6fb 100644 --- a/homeassistant/components/nest/legacy/camera.py +++ b/homeassistant/components/nest/legacy/camera.py @@ -142,7 +142,7 @@ class NestCamera(Camera): url = self.device.snapshot_url try: - response = requests.get(url) + response = requests.get(url, timeout=10) except requests.exceptions.RequestException as error: _LOGGER.error("Error getting camera image: %s", error) return None diff --git a/homeassistant/components/opencv/image_processing.py b/homeassistant/components/opencv/image_processing.py index cde5a8264b8..463b5e0eed1 100644 --- a/homeassistant/components/opencv/image_processing.py +++ b/homeassistant/components/opencv/image_processing.py @@ -87,7 +87,7 @@ def _create_processor_from_config(hass, camera_entity, config): def _get_default_classifier(dest_path): """Download the default OpenCV classifier.""" _LOGGER.info("Downloading default classifier") - req = requests.get(CASCADE_URL, stream=True) + req = requests.get(CASCADE_URL, stream=True, timeout=10) with open(dest_path, "wb") as fil: for chunk in req.iter_content(chunk_size=1024): if chunk: # filter out keep-alive new chunks diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index f3c7c5d84a0..28d0fd71142 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -134,7 +134,7 @@ class UkTransportSensor(SensorEntity): {"app_id": self._api_app_id, "app_key": self._api_app_key}, **params ) - response = requests.get(self._url, params=request_params) + response = requests.get(self._url, params=request_params, timeout=10) if response.status_code != HTTPStatus.OK: _LOGGER.warning("Invalid response from API") elif "error" in response.json(): diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 93c3800e42f..11badca8d8c 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -503,6 +503,7 @@ class ConfigEntryWithingsApi(AbstractWithingsApi): f"{self.URL}/{path}", params=params, headers={"Authorization": f"Bearer {access_token}"}, + timeout=10, ) return response.json() From 6d49362573e00cba77431438b20b81b39b9bd58f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 19 Aug 2022 09:33:57 +0200 Subject: [PATCH 3466/3516] Revert rename of confirm step in zha config flow (#77010) * Revert rename of confirm step in zha config flow * Update tests --- homeassistant/components/zha/config_flow.py | 6 +++--- homeassistant/components/zha/strings.json | 3 +++ .../homeassistant_sky_connect/test_config_flow.py | 2 -- tests/components/zha/test_config_flow.py | 8 ++++---- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 4b90fdb3ad0..9c7ec46a386 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -138,9 +138,9 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) self._set_confirm_only() self.context["title_placeholders"] = {CONF_NAME: self._title} - return await self.async_step_confirm_usb() + return await self.async_step_confirm() - async def async_step_confirm_usb(self, user_input=None): + async def async_step_confirm(self, user_input=None): """Confirm a USB discovery.""" if user_input is not None or not onboarding.async_is_onboarded(self.hass): auto_detected_data = await detect_radios(self._device_path) @@ -155,7 +155,7 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_show_form( - step_id="confirm_usb", + step_id="confirm", description_placeholders={CONF_NAME: self._title}, ) diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 4eb872f4fae..37be80e9b56 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -10,6 +10,9 @@ "confirm": { "description": "Do you want to setup {name}?" }, + "confirm_hardware": { + "description": "Do you want to setup {name}?" + }, "pick_radio": { "data": { "radio_type": "Radio Type" }, "title": "Radio Type", diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index 1db305f3ad0..bbde732d201 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -21,8 +21,6 @@ USB_DATA = usb.UsbServiceInfo( async def test_config_flow(hass: HomeAssistant) -> None: """Test the config flow.""" - # mock_integration(hass, MockModule("hassio")) - with patch( "homeassistant.components.homeassistant_sky_connect.async_setup_entry", return_value=True, diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 82c2fde7c1e..a769303a4c4 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -228,7 +228,7 @@ async def test_discovery_via_usb(detect_mock, hass): ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "confirm_usb" + assert result["step_id"] == "confirm" with patch("homeassistant.components.zha.async_setup_entry"): result2 = await hass.config_entries.flow.async_configure( @@ -264,7 +264,7 @@ async def test_zigate_discovery_via_usb(detect_mock, hass): ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "confirm_usb" + assert result["step_id"] == "confirm" with patch("homeassistant.components.zha.async_setup_entry"): result2 = await hass.config_entries.flow.async_configure( @@ -298,7 +298,7 @@ async def test_discovery_via_usb_no_radio(detect_mock, hass): ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "confirm_usb" + assert result["step_id"] == "confirm" with patch("homeassistant.components.zha.async_setup_entry"): result2 = await hass.config_entries.flow.async_configure( @@ -451,7 +451,7 @@ async def test_discovery_via_usb_deconz_ignored(detect_mock, hass): await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "confirm_usb" + assert result["step_id"] == "confirm" @patch("zigpy_znp.zigbee.application.ControllerApplication.probe", return_value=True) From dd109839b9ddc8b33d2ef1c0039c51b7e1b67c5e Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 19 Aug 2022 01:39:48 -0600 Subject: [PATCH 3467/3516] Provide slight speedup to Guardian device lookup during service call (#77004) * Provide slight speedup to Guardian device lookup during service call * Messages --- homeassistant/components/guardian/__init__.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 909752b5b33..a30536f66c3 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -103,12 +103,16 @@ def async_get_entry_id_for_service_call(hass: HomeAssistant, call: ServiceCall) device_id = call.data[CONF_DEVICE_ID] device_registry = dr.async_get(hass) - if device_entry := device_registry.async_get(device_id): - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.entry_id in device_entry.config_entries: - return entry.entry_id + if (device_entry := device_registry.async_get(device_id)) is None: + raise ValueError(f"Invalid Guardian device ID: {device_id}") - raise ValueError(f"No client for device ID: {device_id}") + for entry_id in device_entry.config_entries: + if (entry := hass.config_entries.async_get_entry(entry_id)) is None: + continue + if entry.domain == DOMAIN: + return entry_id + + raise ValueError(f"No config entry for device ID: {device_id}") @callback From dedf063e43d94d9a0842ef9976d611c2a9229c0b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Aug 2022 09:54:13 +0200 Subject: [PATCH 3468/3516] Improve entity type hints [b] (#77012) --- homeassistant/components/balboa/climate.py | 9 +++-- .../components/bayesian/binary_sensor.py | 4 +- homeassistant/components/bbox/sensor.py | 4 +- .../components/beewi_smartclim/sensor.py | 2 +- homeassistant/components/bitcoin/sensor.py | 2 +- homeassistant/components/bizkaibus/sensor.py | 2 +- .../components/blackbird/media_player.py | 8 ++-- homeassistant/components/blebox/climate.py | 5 ++- homeassistant/components/blebox/light.py | 3 +- homeassistant/components/blebox/switch.py | 5 ++- .../components/blink/binary_sensor.py | 2 +- homeassistant/components/blink/camera.py | 6 +-- homeassistant/components/blink/sensor.py | 2 +- homeassistant/components/blockchain/sensor.py | 2 +- .../components/bloomsky/binary_sensor.py | 2 +- homeassistant/components/bloomsky/sensor.py | 2 +- .../components/bluesound/media_player.py | 38 +++++++++++-------- homeassistant/components/bosch_shc/switch.py | 9 +++-- homeassistant/components/broadlink/remote.py | 14 ++++--- homeassistant/components/broadlink/switch.py | 7 ++-- homeassistant/components/bsblan/climate.py | 4 +- 21 files changed, 73 insertions(+), 59 deletions(-) diff --git a/homeassistant/components/balboa/climate.py b/homeassistant/components/balboa/climate.py index 98acf88649e..cd10ccf2bc9 100644 --- a/homeassistant/components/balboa/climate.py +++ b/homeassistant/components/balboa/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from typing import Any from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -121,7 +122,7 @@ class BalboaSpaClimate(BalboaEntity, ClimateEntity): """Return current preset mode.""" return self._client.get_heatmode(True) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set a new target temperature.""" scale = self._client.get_tempscale() newtemp = kwargs[ATTR_TEMPERATURE] @@ -133,7 +134,7 @@ class BalboaSpaClimate(BalboaEntity, ClimateEntity): await asyncio.sleep(SET_TEMPERATURE_WAIT) await self._client.send_temp_change(newtemp) - async def async_set_preset_mode(self, preset_mode) -> None: + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" modelist = self._client.get_heatmode_stringlist() self._async_validate_mode_or_raise(preset_mode) @@ -141,7 +142,7 @@ class BalboaSpaClimate(BalboaEntity, ClimateEntity): raise ValueError(f"{preset_mode} is not a valid preset mode") await self._client.change_heatmode(modelist.index(preset_mode)) - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" await self._client.change_blower(self._ha_to_balboa_blower_map[fan_mode]) @@ -150,7 +151,7 @@ class BalboaSpaClimate(BalboaEntity, ClimateEntity): if mode == self._client.HEATMODE_RNR: raise ValueError(f"{mode} can only be reported but not set") - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode. OFF = Rest diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 216a8360a84..5641480ba98 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -162,7 +162,7 @@ class BayesianBinarySensor(BinarySensorEntity): "state": self._process_state, } - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """ Call when entity about to be added. @@ -397,7 +397,7 @@ class BayesianBinarySensor(BinarySensorEntity): ATTR_PROBABILITY_THRESHOLD: self._probability_threshold, } - async def async_update(self): + async def async_update(self) -> None: """Get the latest data and update the states.""" if not self._callbacks: self._recalculate_and_write_state() diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index b85c75569d4..43ba6956507 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -136,7 +136,7 @@ class BboxUptimeSensor(SensorEntity): self._attr_name = f"{name} {description.name}" self.bbox_data = bbox_data - def update(self): + def update(self) -> None: """Get the latest data from Bbox and update the state.""" self.bbox_data.update() self._attr_native_value = utcnow() - timedelta( @@ -155,7 +155,7 @@ class BboxSensor(SensorEntity): self._attr_name = f"{name} {description.name}" self.bbox_data = bbox_data - def update(self): + def update(self) -> None: """Get the latest data from Bbox and update the state.""" self.bbox_data.update() sensor_type = self.entity_description.key diff --git a/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant/components/beewi_smartclim/sensor.py index 476ffbdbf1a..4d8936859f5 100644 --- a/homeassistant/components/beewi_smartclim/sensor.py +++ b/homeassistant/components/beewi_smartclim/sensor.py @@ -73,7 +73,7 @@ class BeewiSmartclimSensor(SensorEntity): self._attr_device_class = self._device self._attr_unique_id = f"{mac}_{device}" - def update(self): + def update(self) -> None: """Fetch new state data from the poller.""" self._poller.update_sensor() self._attr_native_value = None diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index d5e9cc9adad..49691089f35 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -181,7 +181,7 @@ class BitcoinSensor(SensorEntity): self.data = data self._currency = currency - def update(self): + def update(self) -> None: """Get the latest data and updates the states.""" self.data.update() stats = self.data.stats diff --git a/homeassistant/components/bizkaibus/sensor.py b/homeassistant/components/bizkaibus/sensor.py index d79192af72d..2c2d1b9db29 100644 --- a/homeassistant/components/bizkaibus/sensor.py +++ b/homeassistant/components/bizkaibus/sensor.py @@ -54,7 +54,7 @@ class BizkaibusSensor(SensorEntity): self.data = data self._attr_name = name - def update(self): + def update(self) -> None: """Get the latest data from the webservice.""" self.data.update() with suppress(TypeError): diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index a20c4294438..b0fda2de0f4 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -158,7 +158,7 @@ class BlackbirdZone(MediaPlayerEntity): self._zone_id = zone_id self._attr_name = zone_name - def update(self): + def update(self) -> None: """Retrieve latest state.""" state = self._blackbird.zone_status(self._zone_id) if not state: @@ -183,7 +183,7 @@ class BlackbirdZone(MediaPlayerEntity): _LOGGER.debug("Setting all zones source to %s", idx) self._blackbird.set_all_zone_source(idx) - def select_source(self, source): + def select_source(self, source: str) -> None: """Set input source.""" if source not in self._source_name_id: return @@ -191,12 +191,12 @@ class BlackbirdZone(MediaPlayerEntity): _LOGGER.debug("Setting zone %d source to %s", self._zone_id, idx) self._blackbird.set_zone_source(self._zone_id, idx) - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" _LOGGER.debug("Turning zone %d on", self._zone_id) self._blackbird.set_zone_power(self._zone_id, True) - def turn_off(self): + def turn_off(self) -> None: """Turn the media player off.""" _LOGGER.debug("Turning zone %d off", self._zone_id) self._blackbird.set_zone_power(self._zone_id, False) diff --git a/homeassistant/components/blebox/climate.py b/homeassistant/components/blebox/climate.py index e279991df20..78f47b1ba46 100644 --- a/homeassistant/components/blebox/climate.py +++ b/homeassistant/components/blebox/climate.py @@ -1,5 +1,6 @@ """BleBox climate entity.""" from datetime import timedelta +from typing import Any from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -73,7 +74,7 @@ class BleBoxClimateEntity(BleBoxEntity, ClimateEntity): """Return the desired thermostat temperature.""" return self._feature.desired - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the climate entity mode.""" if hvac_mode == HVACMode.HEAT: await self._feature.async_on() @@ -81,7 +82,7 @@ class BleBoxClimateEntity(BleBoxEntity, ClimateEntity): await self._feature.async_off() - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set the thermostat temperature.""" value = kwargs[ATTR_TEMPERATURE] await self._feature.async_set_temperature(value) diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index a2ad51cfc9c..8202186d86d 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any import blebox_uniapi.light from blebox_uniapi.light import BleboxColorMode @@ -175,6 +176,6 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity): f"Turning on with effect '{self.name}' failed: {effect} not in effect list." ) from exc - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._feature.async_off() diff --git a/homeassistant/components/blebox/switch.py b/homeassistant/components/blebox/switch.py index f9c866244c7..5ae37d6b34d 100644 --- a/homeassistant/components/blebox/switch.py +++ b/homeassistant/components/blebox/switch.py @@ -1,5 +1,6 @@ """BleBox switch implementation.""" from datetime import timedelta +from typing import Any from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry @@ -35,10 +36,10 @@ class BleBoxSwitchEntity(BleBoxEntity, SwitchEntity): """Return whether switch is on.""" return self._feature.is_on - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" await self._feature.async_turn_on() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" await self._feature.async_turn_off() diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py index 5b2e490cda9..ddcf58deb6f 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -69,7 +69,7 @@ class BlinkBinarySensor(BinarySensorEntity): model=self._camera.camera_type, ) - def update(self): + def update(self) -> None: """Update sensor state.""" self.data.refresh() state = self._camera.attributes[self.entity_description.key] diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index 429452cf4bd..e500eb79e42 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -58,18 +58,18 @@ class BlinkCamera(Camera): """Return the camera attributes.""" return self._camera.attributes - def enable_motion_detection(self): + def enable_motion_detection(self) -> None: """Enable motion detection for the camera.""" self._camera.arm = True self.data.refresh() - def disable_motion_detection(self): + def disable_motion_detection(self) -> None: """Disable motion detection for the camera.""" self._camera.arm = False self.data.refresh() @property - def motion_detection_enabled(self): + def motion_detection_enabled(self) -> bool: """Return the state of the camera.""" return self._camera.arm diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py index 094fa8aadbb..3940522074b 100644 --- a/homeassistant/components/blink/sensor.py +++ b/homeassistant/components/blink/sensor.py @@ -72,7 +72,7 @@ class BlinkSensor(SensorEntity): model=self._camera.camera_type, ) - def update(self): + def update(self) -> None: """Retrieve sensor data from the camera.""" self.data.refresh() try: diff --git a/homeassistant/components/blockchain/sensor.py b/homeassistant/components/blockchain/sensor.py index 92cd7a56e92..4feb7a9fa6a 100644 --- a/homeassistant/components/blockchain/sensor.py +++ b/homeassistant/components/blockchain/sensor.py @@ -65,6 +65,6 @@ class BlockchainSensor(SensorEntity): self._attr_name = name self.addresses = addresses - def update(self): + def update(self) -> None: """Get the latest state of the sensor.""" self._attr_native_value = get_balance(self.addresses) diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py index e1d9d830efa..7b59039a89e 100644 --- a/homeassistant/components/bloomsky/binary_sensor.py +++ b/homeassistant/components/bloomsky/binary_sensor.py @@ -58,7 +58,7 @@ class BloomSkySensor(BinarySensorEntity): self._attr_unique_id = f"{self._device_id}-{sensor_name}" self._attr_device_class = SENSOR_TYPES.get(sensor_name) - def update(self): + def update(self) -> None: """Request an update from the BloomSky API.""" self._bloomsky.refresh_devices() diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index f064ce992c6..77978e6324d 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -112,7 +112,7 @@ class BloomSkySensor(SensorEntity): """Return the class of this device, from component DEVICE_CLASSES.""" return SENSOR_DEVICE_CLASS.get(self._sensor_name) - def update(self): + def update(self) -> None: """Request an update from the BloomSky API.""" self._bloomsky.refresh_devices() state = self._bloomsky.devices[self._device_id]["Data"][self._sensor_name] diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 7f1c6b6553f..53f6de14b3c 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -6,6 +6,7 @@ from asyncio import CancelledError from datetime import timedelta from http import HTTPStatus import logging +from typing import Any from urllib import parse import aiohttp @@ -22,6 +23,7 @@ from homeassistant.components.media_player import ( MediaPlayerEntityFeature, ) from homeassistant.components.media_player.browse_media import ( + BrowseMedia, async_process_play_media_url, ) from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC @@ -333,7 +335,7 @@ class BluesoundPlayer(MediaPlayerEntity): ) raise - async def async_update(self): + async def async_update(self) -> None: """Update internal status of the entity.""" if not self._is_online: return @@ -930,12 +932,12 @@ class BluesoundPlayer(MediaPlayerEntity): while sleep > 0: sleep = await self.async_increase_timer() - async def async_set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle: bool) -> None: """Enable or disable shuffle mode.""" value = "1" if shuffle else "0" return await self.send_bluesound_command(f"/Shuffle?state={value}") - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select input source.""" if self.is_grouped and not self.is_master: return @@ -958,14 +960,14 @@ class BluesoundPlayer(MediaPlayerEntity): return await self.send_bluesound_command(url) - async def async_clear_playlist(self): + async def async_clear_playlist(self) -> None: """Clear players playlist.""" if self.is_grouped and not self.is_master: return return await self.send_bluesound_command("Clear") - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send media_next command to media player.""" if self.is_grouped and not self.is_master: return @@ -978,7 +980,7 @@ class BluesoundPlayer(MediaPlayerEntity): return await self.send_bluesound_command(cmd) - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send media_previous command to media player.""" if self.is_grouped and not self.is_master: return @@ -991,35 +993,37 @@ class BluesoundPlayer(MediaPlayerEntity): return await self.send_bluesound_command(cmd) - async def async_media_play(self): + async def async_media_play(self) -> None: """Send media_play command to media player.""" if self.is_grouped and not self.is_master: return return await self.send_bluesound_command("Play") - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send media_pause command to media player.""" if self.is_grouped and not self.is_master: return return await self.send_bluesound_command("Pause") - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop command.""" if self.is_grouped and not self.is_master: return return await self.send_bluesound_command("Pause") - async def async_media_seek(self, position): + async def async_media_seek(self, position: float) -> None: """Send media_seek command to media player.""" if self.is_grouped and not self.is_master: return return await self.send_bluesound_command(f"Play?seek={float(position)}") - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Send the play_media command to the media player.""" if self.is_grouped and not self.is_master: return @@ -1036,21 +1040,21 @@ class BluesoundPlayer(MediaPlayerEntity): return await self.send_bluesound_command(url) - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Volume up the media player.""" current_vol = self.volume_level if not current_vol or current_vol >= 1: return return await self.async_set_volume_level(current_vol + 0.01) - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Volume down the media player.""" current_vol = self.volume_level if not current_vol or current_vol <= 0: return return await self.async_set_volume_level(current_vol - 0.01) - async def async_set_volume_level(self, volume): + async def async_set_volume_level(self, volume: float) -> None: """Send volume_up command to media player.""" if volume < 0: volume = 0 @@ -1058,13 +1062,15 @@ class BluesoundPlayer(MediaPlayerEntity): volume = 1 return await self.send_bluesound_command(f"Volume?level={float(volume) * 100}") - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Send mute command to media player.""" if mute: return await self.send_bluesound_command("Volume?mute=1") return await self.send_bluesound_command("Volume?mute=0") - async def async_browse_media(self, media_content_type=None, media_content_id=None): + async def async_browse_media( + self, media_content_type: str | None = None, media_content_id: str | None = None + ) -> BrowseMedia: """Implement the websocket media browsing helper.""" return await media_source.async_browse_media( self.hass, diff --git a/homeassistant/components/bosch_shc/switch.py b/homeassistant/components/bosch_shc/switch.py index aa49d873f6f..28b68ac091b 100644 --- a/homeassistant/components/bosch_shc/switch.py +++ b/homeassistant/components/bosch_shc/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Any from boschshcpy import ( SHCCamera360, @@ -183,11 +184,11 @@ class SHCSwitch(SHCEntity, SwitchEntity): == self.entity_description.on_value ) - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" setattr(self._device, self.entity_description.on_key, True) - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" setattr(self._device, self.entity_description.on_key, False) @@ -218,10 +219,10 @@ class SHCRoutingSwitch(SHCEntity, SwitchEntity): """Return the state of the switch.""" return self._device.routing.name == "ENABLED" - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self._device.routing = True - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self._device.routing = False diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index dd0f40d45bc..da72f4fcb06 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -2,9 +2,11 @@ import asyncio from base64 import b64encode from collections import defaultdict +from collections.abc import Iterable from datetime import timedelta from itertools import product import logging +from typing import Any from broadlink.exceptions import ( AuthorizationError, @@ -174,18 +176,18 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): """ return self._flags - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when the remote is added to hass.""" state = await self.async_get_last_state() self._attr_is_on = state is None or state.state != STATE_OFF await super().async_added_to_hass() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the remote.""" self._attr_is_on = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the remote.""" self._attr_is_on = False self.async_write_ha_state() @@ -198,7 +200,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): self._flags.update(await self._flag_storage.async_load() or {}) self._storage_loaded = True - async def async_send_command(self, command, **kwargs): + async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: """Send a list of commands to a device.""" kwargs[ATTR_COMMAND] = command kwargs = SERVICE_SEND_SCHEMA(kwargs) @@ -255,7 +257,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): if at_least_one_sent: self._flag_storage.async_delay_save(self._get_flags, FLAG_SAVE_DELAY) - async def async_learn_command(self, **kwargs): + async def async_learn_command(self, **kwargs: Any) -> None: """Learn a list of commands from a remote.""" kwargs = SERVICE_LEARN_SCHEMA(kwargs) commands = kwargs[ATTR_COMMAND] @@ -419,7 +421,7 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity): self.hass, notification_id="learn_command" ) - async def async_delete_command(self, **kwargs): + async def async_delete_command(self, **kwargs: Any) -> None: """Delete a list of commands from a remote.""" kwargs = SERVICE_DELETE_SCHEMA(kwargs) commands = kwargs[ATTR_COMMAND] diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index d38898a513f..229949b7ee2 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod import logging +from typing import Any from broadlink.exceptions import BroadlinkException import voluptuous as vol @@ -146,19 +147,19 @@ class BroadlinkSwitch(BroadlinkEntity, SwitchEntity, RestoreEntity, ABC): self._command_off = command_off self._attr_name = f"{device.name} Switch" - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when the switch is added to hass.""" state = await self.async_get_last_state() self._attr_is_on = state is not None and state.state == STATE_ON await super().async_added_to_hass() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" if await self._async_send_packet(self._command_on): self._attr_is_on = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" if await self._async_send_packet(self._command_off): self._attr_is_on = False diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 16d8452f10b..7ebcb48f307 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -106,14 +106,14 @@ class BSBLanClimate(ClimateEntity): self._store_hvac_mode = self._attr_hvac_mode await self.async_set_data(preset_mode=preset_mode) - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set HVAC mode.""" _LOGGER.debug("Setting HVAC mode to: %s", hvac_mode) # preset should be none when hvac mode is set self._attr_preset_mode = PRESET_NONE await self.async_set_data(hvac_mode=hvac_mode) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" await self.async_set_data(**kwargs) From 4de50fc4718257a8de519ffcd7d8eee2ce91c37f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Aug 2022 10:09:20 +0200 Subject: [PATCH 3469/3516] Improve type hint in bsblan climate entity (#77014) --- homeassistant/components/bsblan/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index 7ebcb48f307..e83415ebf52 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -85,7 +85,7 @@ class BSBLanClimate(ClimateEntity): ) -> None: """Initialize BSBLan climate device.""" self._attr_available = True - self._store_hvac_mode = None + self._store_hvac_mode: HVACMode | str | None = None self.bsblan = bsblan self._attr_name = self._attr_unique_id = info.device_identification self._attr_device_info = DeviceInfo( @@ -95,7 +95,7 @@ class BSBLanClimate(ClimateEntity): name="BSBLan Device", ) - async def async_set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset mode.""" _LOGGER.debug("Setting preset mode to: %s", preset_mode) if preset_mode == PRESET_NONE: From d70bc68b9392392633b91f7bd815531ab3e90225 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Aug 2022 10:30:34 +0200 Subject: [PATCH 3470/3516] Improve type hint in brottsplatskartan sensor entity (#77015) --- homeassistant/components/brottsplatskartan/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index 171986ffd1c..535fa9f56ad 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -92,10 +92,10 @@ class BrottsplatskartanSensor(SensorEntity): self._brottsplatskartan = bpk self._attr_name = name - def update(self): + def update(self) -> None: """Update device state.""" - incident_counts = defaultdict(int) + incident_counts: defaultdict[str, int] = defaultdict(int) incidents = self._brottsplatskartan.get_incidents() if incidents is False: From 801f7d1d5f95d4ae8b60dbef85ced0b257faf049 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Aug 2022 10:33:34 +0200 Subject: [PATCH 3471/3516] Adjust type hints in airtouch4 climate entity (#76987) --- homeassistant/components/airtouch4/climate.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airtouch4/climate.py b/homeassistant/components/airtouch4/climate.py index 370a061e901..dcc107453d5 100644 --- a/homeassistant/components/airtouch4/climate.py +++ b/homeassistant/components/airtouch4/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -294,9 +295,11 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity): ) return [AT_TO_HA_FAN_SPEED[speed] for speed in airtouch_fan_speeds] - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" - temp = kwargs.get(ATTR_TEMPERATURE) + if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: + _LOGGER.debug("Argument `temperature` is missing in set_temperature") + return _LOGGER.debug("Setting temp of %s to %s", self._group_number, str(temp)) self._unit = await self._airtouch.SetGroupToTemperature( From 655e2f92ba8f7cfab3dd13b8676d9fcafd82ba1e Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Fri, 19 Aug 2022 11:39:14 +0300 Subject: [PATCH 3472/3516] Add strict typing to mikrotik (#76974) add strict typing to mikrotik --- .strict-typing | 1 + homeassistant/components/mikrotik/__init__.py | 7 +- homeassistant/components/mikrotik/const.py | 3 - homeassistant/components/mikrotik/device.py | 66 ++++++++++++++++++ .../components/mikrotik/device_tracker.py | 2 +- homeassistant/components/mikrotik/hub.py | 67 ++----------------- mypy.ini | 10 +++ 7 files changed, 88 insertions(+), 68 deletions(-) create mode 100644 homeassistant/components/mikrotik/device.py diff --git a/.strict-typing b/.strict-typing index f8a0579433b..a215c2187ab 100644 --- a/.strict-typing +++ b/.strict-typing @@ -170,6 +170,7 @@ homeassistant.components.mailbox.* homeassistant.components.media_player.* homeassistant.components.media_source.* homeassistant.components.metoffice.* +homeassistant.components.mikrotik.* homeassistant.components.mjpeg.* homeassistant.components.modbus.* homeassistant.components.modem_callerid.* diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index f72c79c1559..6a158c60fcf 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -1,15 +1,18 @@ """The Mikrotik component.""" from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr -from .const import ATTR_MANUFACTURER, DOMAIN, PLATFORMS +from .const import ATTR_MANUFACTURER, DOMAIN from .errors import CannotConnect, LoginError from .hub import MikrotikDataUpdateCoordinator, get_api CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) +PLATFORMS = [Platform.DEVICE_TRACKER] + async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up the Mikrotik component.""" @@ -26,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) device_registry = dr.async_get(hass) device_registry.async_get_or_create( diff --git a/homeassistant/components/mikrotik/const.py b/homeassistant/components/mikrotik/const.py index bbe129c4a00..911d348365e 100644 --- a/homeassistant/components/mikrotik/const.py +++ b/homeassistant/components/mikrotik/const.py @@ -1,8 +1,6 @@ """Constants used in the Mikrotik components.""" from typing import Final -from homeassistant.const import Platform - DOMAIN: Final = "mikrotik" DEFAULT_NAME: Final = "Mikrotik" DEFAULT_API_PORT: Final = 8728 @@ -40,7 +38,6 @@ MIKROTIK_SERVICES: Final = { IS_CAPSMAN: "/caps-man/interface/print", } -PLATFORMS: Final = [Platform.DEVICE_TRACKER] ATTR_DEVICE_TRACKER: Final = [ "comment", diff --git a/homeassistant/components/mikrotik/device.py b/homeassistant/components/mikrotik/device.py new file mode 100644 index 00000000000..f37ef6fee80 --- /dev/null +++ b/homeassistant/components/mikrotik/device.py @@ -0,0 +1,66 @@ +"""Network client device class.""" + +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from homeassistant.util import slugify +import homeassistant.util.dt as dt_util + +from .const import ATTR_DEVICE_TRACKER + + +class Device: + """Represents a network device.""" + + def __init__(self, mac: str, params: dict[str, Any]) -> None: + """Initialize the network device.""" + self._mac = mac + self._params = params + self._last_seen: datetime | None = None + self._attrs: dict[str, Any] = {} + self._wireless_params: dict[str, Any] = {} + + @property + def name(self) -> str: + """Return device name.""" + return self._params.get("host-name", self.mac) + + @property + def ip_address(self) -> str | None: + """Return device primary ip address.""" + return self._params.get("address") + + @property + def mac(self) -> str: + """Return device mac.""" + return self._mac + + @property + def last_seen(self) -> datetime | None: + """Return device last seen.""" + return self._last_seen + + @property + def attrs(self) -> dict[str, Any]: + """Return device attributes.""" + attr_data = self._wireless_params | self._params + for attr in ATTR_DEVICE_TRACKER: + if attr in attr_data: + self._attrs[slugify(attr)] = attr_data[attr] + return self._attrs + + def update( + self, + wireless_params: dict[str, Any] | None = None, + params: dict[str, Any] | None = None, + active: bool = False, + ) -> None: + """Update Device params.""" + if wireless_params: + self._wireless_params = wireless_params + if params: + self._params = params + if active: + self._last_seen = dt_util.utcnow() diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 856521f019d..f50c49d5ab6 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -63,7 +63,7 @@ def update_items( coordinator: MikrotikDataUpdateCoordinator, async_add_entities: AddEntitiesCallback, tracked: dict[str, MikrotikDataUpdateCoordinatorTracker], -): +) -> None: """Update tracked device state from the hub.""" new_tracked: list[MikrotikDataUpdateCoordinatorTracker] = [] for mac, device in coordinator.api.devices.items(): diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 914911ee5cc..08320c603f9 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -1,7 +1,7 @@ """The Mikrotik router class.""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import timedelta import logging import socket import ssl @@ -14,12 +14,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.util import slugify -import homeassistant.util.dt as dt_util from .const import ( ARP, - ATTR_DEVICE_TRACKER, ATTR_FIRMWARE, ATTR_MODEL, ATTR_SERIAL_NUMBER, @@ -38,66 +35,12 @@ from .const import ( NAME, WIRELESS, ) +from .device import Device from .errors import CannotConnect, LoginError _LOGGER = logging.getLogger(__name__) -class Device: - """Represents a network device.""" - - def __init__(self, mac: str, params: dict[str, Any]) -> None: - """Initialize the network device.""" - self._mac = mac - self._params = params - self._last_seen: datetime | None = None - self._attrs: dict[str, Any] = {} - self._wireless_params: dict[str, Any] = {} - - @property - def name(self) -> str: - """Return device name.""" - return self._params.get("host-name", self.mac) - - @property - def ip_address(self) -> str | None: - """Return device primary ip address.""" - return self._params.get("address") - - @property - def mac(self) -> str: - """Return device mac.""" - return self._mac - - @property - def last_seen(self) -> datetime | None: - """Return device last seen.""" - return self._last_seen - - @property - def attrs(self) -> dict[str, Any]: - """Return device attributes.""" - attr_data = self._wireless_params | self._params - for attr in ATTR_DEVICE_TRACKER: - if attr in attr_data: - self._attrs[slugify(attr)] = attr_data[attr] - return self._attrs - - def update( - self, - wireless_params: dict[str, Any] | None = None, - params: dict[str, Any] | None = None, - active: bool = False, - ) -> None: - """Update Device params.""" - if wireless_params: - self._wireless_params = wireless_params - if params: - self._params = params - if active: - self._last_seen = dt_util.utcnow() - - class MikrotikData: """Handle all communication with the Mikrotik API.""" @@ -248,8 +191,8 @@ class MikrotikData: self, cmd: str, params: dict[str, Any] | None = None ) -> list[dict[str, Any]]: """Retrieve data from Mikrotik API.""" + _LOGGER.debug("Running command %s", cmd) try: - _LOGGER.debug("Running command %s", cmd) if params: return list(self.api(cmd=cmd, **params)) return list(self.api(cmd=cmd)) @@ -273,7 +216,7 @@ class MikrotikData: return [] -class MikrotikDataUpdateCoordinator(DataUpdateCoordinator): +class MikrotikDataUpdateCoordinator(DataUpdateCoordinator[None]): """Mikrotik Hub Object.""" def __init__( @@ -293,7 +236,7 @@ class MikrotikDataUpdateCoordinator(DataUpdateCoordinator): @property def host(self) -> str: """Return the host of this hub.""" - return self.config_entry.data[CONF_HOST] + return str(self.config_entry.data[CONF_HOST]) @property def hostname(self) -> str: diff --git a/mypy.ini b/mypy.ini index 051c1065423..fd609d8099b 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1459,6 +1459,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.mikrotik.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.mjpeg.*] check_untyped_defs = true disallow_incomplete_defs = true From c3305caabec31ea1a1977f7fac910fd07a68cdbb Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 19 Aug 2022 02:41:33 -0600 Subject: [PATCH 3473/3516] Provide slight speedup to RainMachine device lookup during service call (#76944) Fix --- homeassistant/components/rainmachine/__init__.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index a5426552ae2..52de2e1c61a 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -159,11 +159,15 @@ def async_get_controller_for_service_call( device_id = call.data[CONF_DEVICE_ID] device_registry = dr.async_get(hass) - if device_entry := device_registry.async_get(device_id): - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.entry_id in device_entry.config_entries: - data: RainMachineData = hass.data[DOMAIN][entry.entry_id] - return data.controller + if (device_entry := device_registry.async_get(device_id)) is None: + raise ValueError(f"Invalid RainMachine device ID: {device_id}") + + for entry_id in device_entry.config_entries: + if (entry := hass.config_entries.async_get_entry(entry_id)) is None: + continue + if entry.domain == DOMAIN: + data: RainMachineData = hass.data[DOMAIN][entry_id] + return data.controller raise ValueError(f"No controller for device ID: {device_id}") From cbeaea98d16c354032fad2fca257865faaf4d2fb Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Fri, 19 Aug 2022 04:56:01 -0400 Subject: [PATCH 3474/3516] Remove deprecated YAML configuration from Skybell (#76940) --- homeassistant/components/skybell/__init__.py | 59 +++++-------------- .../components/skybell/binary_sensor.py | 16 +---- homeassistant/components/skybell/camera.py | 30 +--------- .../components/skybell/config_flow.py | 7 --- homeassistant/components/skybell/const.py | 3 - .../components/skybell/manifest.json | 2 +- homeassistant/components/skybell/sensor.py | 16 ----- homeassistant/components/skybell/strings.json | 6 ++ homeassistant/components/skybell/switch.py | 20 +------ .../components/skybell/translations/en.json | 6 ++ tests/components/skybell/test_config_flow.py | 34 +---------- 11 files changed, 33 insertions(+), 166 deletions(-) diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py index 9f032327d62..1e272dba27f 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -2,39 +2,22 @@ from __future__ import annotations import asyncio -import os from aioskybell import Skybell from aioskybell.exceptions import SkybellAuthenticationException, SkybellException -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.components.repairs.issue_handler import async_create_issue +from homeassistant.components.repairs.models import IssueSeverity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType -from .const import DEFAULT_CACHEDB, DOMAIN +from .const import DOMAIN from .coordinator import SkybellDataUpdateCoordinator -CONFIG_SCHEMA = vol.Schema( - vol.All( - # Deprecated in Home Assistant 2022.6 - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - PLATFORMS = [ Platform.BINARY_SENSOR, Platform.CAMERA, @@ -48,31 +31,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the SkyBell component.""" hass.data.setdefault(DOMAIN, {}) - entry_config = {} - if DOMAIN not in config: - return True - for parameter, value in config[DOMAIN].items(): - if parameter == CONF_USERNAME: - entry_config[CONF_EMAIL] = value - else: - entry_config[parameter] = value - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=entry_config, - ) + if DOMAIN in config: + async_create_issue( + hass, + DOMAIN, + "removed_yaml", + breaks_in_ha_version="2022.9.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="removed_yaml", ) - # Clean up unused cache file since we are using an account specific name - # Remove with import - def clean_cache(): - """Clean old cache filename.""" - if os.path.exists(hass.config.path(DEFAULT_CACHEDB)): - os.remove(hass.config.path(DEFAULT_CACHEDB)) - - await hass.async_add_executor_job(clean_cache) - return True diff --git a/homeassistant/components/skybell/binary_sensor.py b/homeassistant/components/skybell/binary_sensor.py index 05f007e9455..6b49307d439 100644 --- a/homeassistant/components/skybell/binary_sensor.py +++ b/homeassistant/components/skybell/binary_sensor.py @@ -2,18 +2,14 @@ from __future__ import annotations from aioskybell.helpers import const as CONST -import voluptuous as vol from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import DOMAIN @@ -33,21 +29,11 @@ BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( ), ) -# Deprecated in Home Assistant 2022.6 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_ENTITY_NAMESPACE, default=DOMAIN): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( - cv.ensure_list, [vol.In(BINARY_SENSOR_TYPES)] - ), - } -) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up Skybell switch.""" + """Set up Skybell binary sensor.""" async_add_entities( SkybellBinarySensor(coordinator, sensor) for sensor in BINARY_SENSOR_TYPES diff --git a/homeassistant/components/skybell/camera.py b/homeassistant/components/skybell/camera.py index 5bbcea833c2..b9aba0e82ac 100644 --- a/homeassistant/components/skybell/camera.py +++ b/homeassistant/components/skybell/camera.py @@ -3,43 +3,19 @@ from __future__ import annotations from aiohttp import web from haffmpeg.camera import CameraMjpeg -import voluptuous as vol -from homeassistant.components.camera import ( - PLATFORM_SCHEMA, - Camera, - CameraEntityDescription, -) +from homeassistant.components.camera import Camera, CameraEntityDescription from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - CONF_ACTIVITY_NAME, - CONF_AVATAR_NAME, - DOMAIN, - IMAGE_ACTIVITY, - IMAGE_AVATAR, -) +from .const import DOMAIN from .coordinator import SkybellDataUpdateCoordinator from .entity import SkybellEntity -# Deprecated in Home Assistant 2022.6 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_MONITORED_CONDITIONS, default=[IMAGE_AVATAR]): vol.All( - cv.ensure_list, [vol.In([IMAGE_AVATAR, IMAGE_ACTIVITY])] - ), - vol.Optional(CONF_ACTIVITY_NAME): cv.string, - vol.Optional(CONF_AVATAR_NAME): cv.string, - } -) - CAMERA_TYPES: tuple[CameraEntityDescription, ...] = ( CameraEntityDescription(key="activity", name="Last activity"), CameraEntityDescription(key="avatar", name="Camera"), @@ -49,7 +25,7 @@ CAMERA_TYPES: tuple[CameraEntityDescription, ...] = ( async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up Skybell switch.""" + """Set up Skybell camera.""" entities = [] for description in CAMERA_TYPES: for coordinator in hass.data[DOMAIN][entry.entry_id]: diff --git a/homeassistant/components/skybell/config_flow.py b/homeassistant/components/skybell/config_flow.py index 7b7b43788b3..908eab4c46d 100644 --- a/homeassistant/components/skybell/config_flow.py +++ b/homeassistant/components/skybell/config_flow.py @@ -10,7 +10,6 @@ from homeassistant import config_entries from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType from .const import DOMAIN @@ -18,12 +17,6 @@ from .const import DOMAIN class SkybellFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Skybell.""" - async def async_step_import(self, user_input: ConfigType) -> FlowResult: - """Import a config entry from configuration.yaml.""" - if self._async_current_entries(): - return self.async_abort(reason="already_configured") - return await self.async_step_user(user_input) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/skybell/const.py b/homeassistant/components/skybell/const.py index d8f7e4992d5..1d46e45dad1 100644 --- a/homeassistant/components/skybell/const.py +++ b/homeassistant/components/skybell/const.py @@ -2,9 +2,6 @@ import logging from typing import Final -CONF_ACTIVITY_NAME = "activity_name" -CONF_AVATAR_NAME = "avatar_name" -DEFAULT_CACHEDB = "./skybell_cache.pickle" DEFAULT_NAME = "SkyBell" DOMAIN: Final = "skybell" diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index bfef4bc3422..4365a9cf713 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/skybell", "requirements": ["aioskybell==22.7.0"], - "dependencies": ["ffmpeg"], + "dependencies": ["ffmpeg", "repairs"], "codeowners": ["@tkdrob"], "iot_class": "cloud_polling", "loggers": ["aioskybell"] diff --git a/homeassistant/components/skybell/sensor.py b/homeassistant/components/skybell/sensor.py index 352d29bd793..7acc30d0bd0 100644 --- a/homeassistant/components/skybell/sensor.py +++ b/homeassistant/components/skybell/sensor.py @@ -7,18 +7,14 @@ from typing import Any from aioskybell import SkybellDevice from aioskybell.helpers import const as CONST -import voluptuous as vol from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -95,18 +91,6 @@ SENSOR_TYPES: tuple[SkybellSensorEntityDescription, ...] = ( ), ) -MONITORED_CONDITIONS = SENSOR_TYPES - -# Deprecated in Home Assistant 2022.6 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_ENTITY_NAMESPACE, default=DOMAIN): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( - cv.ensure_list, [vol.In(MONITORED_CONDITIONS)] - ), - } -) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/homeassistant/components/skybell/strings.json b/homeassistant/components/skybell/strings.json index e48a75c12bd..949223250df 100644 --- a/homeassistant/components/skybell/strings.json +++ b/homeassistant/components/skybell/strings.json @@ -17,5 +17,11 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } + }, + "issues": { + "removed_yaml": { + "title": "The Skybell YAML configuration has been removed", + "description": "Configuring Skybell using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } diff --git a/homeassistant/components/skybell/switch.py b/homeassistant/components/skybell/switch.py index 529be94f1ac..b3cb8c53032 100644 --- a/homeassistant/components/skybell/switch.py +++ b/homeassistant/components/skybell/switch.py @@ -3,17 +3,9 @@ from __future__ import annotations from typing import Any, cast -import voluptuous as vol - -from homeassistant.components.switch import ( - PLATFORM_SCHEMA, - SwitchEntity, - SwitchEntityDescription, -) +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_MONITORED_CONDITIONS from homeassistant.core import HomeAssistant -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -34,16 +26,6 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = ( ), ) -# Deprecated in Home Assistant 2022.6 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_ENTITY_NAMESPACE, default=DOMAIN): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( - cv.ensure_list, [vol.In(SWITCH_TYPES)] - ), - } -) - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback diff --git a/homeassistant/components/skybell/translations/en.json b/homeassistant/components/skybell/translations/en.json index d996004e5c4..f9fa0048854 100644 --- a/homeassistant/components/skybell/translations/en.json +++ b/homeassistant/components/skybell/translations/en.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "title": "The Skybell YAML configuration has been removed", + "description": "Configuring Skybell using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } } } \ No newline at end of file diff --git a/tests/components/skybell/test_config_flow.py b/tests/components/skybell/test_config_flow.py index cd2b5053ac7..21ead201b54 100644 --- a/tests/components/skybell/test_config_flow.py +++ b/tests/components/skybell/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from aioskybell import exceptions from homeassistant.components.skybell.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -99,35 +99,3 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} - - -async def test_flow_import(hass: HomeAssistant) -> None: - """Test import step.""" - with _patch_skybell(), _patch_skybell_devices(), _patch_setup_entry(), _patch_setup(): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT} - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input=CONF_CONFIG_FLOW, - ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "user" - assert result["data"] == CONF_CONFIG_FLOW - - -async def test_flow_import_already_configured(hass: HomeAssistant) -> None: - """Test import step already configured.""" - entry = MockConfigEntry( - domain=DOMAIN, unique_id="123456789012345678901234", data=CONF_CONFIG_FLOW - ) - - entry.add_to_hass(hass) - - with _patch_skybell(): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" From 61af82223f04548225d991b1ec04e3ce9ab9a526 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Aug 2022 10:58:51 +0200 Subject: [PATCH 3475/3516] Improve type hint in blebox light entity (#77013) * Improve type hint in blebox light entity * Adjust * Adjust supported_features * Adjust effect_list property * Improve base class --- homeassistant/components/blebox/__init__.py | 5 +++-- homeassistant/components/blebox/light.py | 11 +++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py index ff907d728b7..0f4bd1c1490 100644 --- a/homeassistant/components/blebox/__init__.py +++ b/homeassistant/components/blebox/__init__.py @@ -3,6 +3,7 @@ import logging from blebox_uniapi.box import Box from blebox_uniapi.error import Error +from blebox_uniapi.feature import Feature from blebox_uniapi.session import ApiHost from homeassistant.config_entries import ConfigEntry @@ -83,7 +84,7 @@ def create_blebox_entities( class BleBoxEntity(Entity): """Implements a common class for entities representing a BleBox feature.""" - def __init__(self, feature): + def __init__(self, feature: Feature) -> None: """Initialize a BleBox entity.""" self._feature = feature self._attr_name = feature.full_name @@ -97,7 +98,7 @@ class BleBoxEntity(Entity): sw_version=product.firmware_version, ) - async def async_update(self): + async def async_update(self) -> None: """Update the entity state.""" try: await self._feature.async_update() diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index 8202186d86d..c1245d52fec 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -56,11 +56,14 @@ COLOR_MODE_MAP = { class BleBoxLightEntity(BleBoxEntity, LightEntity): """Representation of BleBox lights.""" - def __init__(self, feature): + _feature: blebox_uniapi.light.Light + + def __init__(self, feature: blebox_uniapi.light.Light) -> None: """Initialize a BleBox light.""" super().__init__(feature) self._attr_supported_color_modes = {self.color_mode} - self._attr_supported_features = LightEntityFeature.EFFECT + if feature.effect_list: + self._attr_supported_features = LightEntityFeature.EFFECT @property def is_on(self) -> bool: @@ -91,7 +94,7 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity): return color_mode_tmp @property - def effect_list(self) -> list[str] | None: + def effect_list(self) -> list[str]: """Return the list of supported effects.""" return self._feature.effect_list @@ -125,7 +128,7 @@ class BleBoxLightEntity(BleBoxEntity, LightEntity): return None return tuple(blebox_uniapi.light.Light.rgb_hex_to_rgb_list(rgbww_hex)) - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" rgbw = kwargs.get(ATTR_RGBW_COLOR) From f966b48d8477df3af2c76bdace1d0e1571142c2d Mon Sep 17 00:00:00 2001 From: Johannes Jonker Date: Fri, 19 Aug 2022 11:01:42 +0200 Subject: [PATCH 3476/3516] Add newly-released Amazon Polly voices (#76934) * Add newly-released Amazon Polly voices Cf. announcement at https://aws.amazon.com/about-aws/whats-new/2022/06/amazon-polly-adds-male-neural-tts-voices-languages/ and updated voice list at https://docs.aws.amazon.com/polly/latest/dg/voicelist.html * Fix inline comment spacing --- homeassistant/components/amazon_polly/const.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/amazon_polly/const.py b/homeassistant/components/amazon_polly/const.py index 0d5e65b2a3a..3892204c47a 100644 --- a/homeassistant/components/amazon_polly/const.py +++ b/homeassistant/components/amazon_polly/const.py @@ -36,6 +36,7 @@ SUPPORTED_VOICES: Final[list[str]] = [ "Aditi", # Hindi "Amy", "Aria", + "Arthur", # English, Neural "Astrid", # Swedish "Ayanda", "Bianca", # Italian @@ -47,6 +48,7 @@ SUPPORTED_VOICES: Final[list[str]] = [ "Chantal", # French Canadian "Conchita", "Cristiano", + "Daniel", # German, Neural "Dora", # Icelandic "Emma", # English "Enrique", @@ -69,6 +71,7 @@ SUPPORTED_VOICES: Final[list[str]] = [ "Kevin", "Kimberly", "Lea", # French + "Liam", # Canadian French, Neural "Liv", # Norwegian "Lotte", # Dutch "Lucia", # Spanish European @@ -86,6 +89,7 @@ SUPPORTED_VOICES: Final[list[str]] = [ "Nicole", # English Australian "Olivia", # Female, Australian, Neural "Penelope", # Spanish US + "Pedro", # Spanish US, Neural "Raveena", # English, Indian "Ricardo", "Ruben", From a5e151691ce00cc72439997295f8460d48e61fe0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Aug 2022 11:02:48 +0200 Subject: [PATCH 3477/3516] Fix acmeda battery sensor definition (#76928) * Fix acmeda battery sensor definition * Use float | int | None --- homeassistant/components/acmeda/sensor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/acmeda/sensor.py b/homeassistant/components/acmeda/sensor.py index 88a0886a84e..f92d9fcf57b 100644 --- a/homeassistant/components/acmeda/sensor.py +++ b/homeassistant/components/acmeda/sensor.py @@ -11,6 +11,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .base import AcmedaBase from .const import ACMEDA_HUB_UPDATE, DOMAIN from .helpers import async_add_acmeda_entities +from .hub import PulseHub async def async_setup_entry( @@ -19,7 +20,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Acmeda Rollers from a config entry.""" - hub = hass.data[DOMAIN][config_entry.entry_id] + hub: PulseHub = hass.data[DOMAIN][config_entry.entry_id] current: set[int] = set() @@ -41,15 +42,15 @@ async def async_setup_entry( class AcmedaBattery(AcmedaBase, SensorEntity): """Representation of a Acmeda cover device.""" - device_class = SensorDeviceClass.BATTERY + _attr_device_class = SensorDeviceClass.BATTERY _attr_native_unit_of_measurement = PERCENTAGE @property - def name(self): + def name(self) -> str: """Return the name of roller.""" return f"{super().name} Battery" @property - def native_value(self): + def native_value(self) -> float | int | None: """Return the state of the device.""" return self.roller.battery From 90aba6c52376157b760148a136bc109eefd823c0 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 19 Aug 2022 11:12:47 +0200 Subject: [PATCH 3478/3516] Add cv.deprecated to MQTT modern schema's too (#76884) Add cv.deprcated to modern schema too --- homeassistant/components/mqtt/climate.py | 14 +++++++------- homeassistant/components/mqtt/cover.py | 1 + homeassistant/components/mqtt/fan.py | 10 ++++++++++ .../components/mqtt/light/schema_basic.py | 11 ++++++++++- homeassistant/components/mqtt/light/schema_json.py | 2 ++ .../components/mqtt/light/schema_template.py | 6 +++++- 6 files changed, 35 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index f44cf6fe8fc..f39d3857ec2 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -286,13 +286,6 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) PLATFORM_SCHEMA_MODERN = vol.All( - _PLATFORM_SCHEMA_BASE, - valid_preset_mode_configuration, -) - -# Configuring MQTT Climate under the climate platform key is deprecated in HA Core 2022.6 -PLATFORM_SCHEMA = vol.All( - cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), # Support CONF_SEND_IF_OFF is removed with release 2022.9 cv.removed(CONF_SEND_IF_OFF), # AWAY and HOLD mode topics and templates are no longer supported, support was removed with release 2022.9 @@ -304,6 +297,13 @@ PLATFORM_SCHEMA = vol.All( cv.removed(CONF_HOLD_STATE_TEMPLATE), cv.removed(CONF_HOLD_STATE_TOPIC), cv.removed(CONF_HOLD_LIST), + _PLATFORM_SCHEMA_BASE, + valid_preset_mode_configuration, +) + +# Configuring MQTT Climate under the climate platform key is deprecated in HA Core 2022.6 +PLATFORM_SCHEMA = vol.All( + cv.PLATFORM_SCHEMA.extend(_PLATFORM_SCHEMA_BASE.schema), valid_preset_mode_configuration, warn_for_legacy_schema(climate.DOMAIN), ) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index b0fbacd10fc..fd96fe524d9 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -200,6 +200,7 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) PLATFORM_SCHEMA_MODERN = vol.All( + cv.removed("tilt_invert_state"), _PLATFORM_SCHEMA_BASE, validate_options, ) diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 20c4936ab38..fab748d2bfc 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -186,6 +186,16 @@ PLATFORM_SCHEMA = vol.All( ) PLATFORM_SCHEMA_MODERN = vol.All( + # CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_LIST, CONF_SPEED_STATE_TOPIC, CONF_SPEED_VALUE_TEMPLATE and + # Speeds SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH SPEED_OFF, + # are no longer supported, support was removed in release 2021.12 + cv.removed(CONF_PAYLOAD_HIGH_SPEED), + cv.removed(CONF_PAYLOAD_LOW_SPEED), + cv.removed(CONF_PAYLOAD_MEDIUM_SPEED), + cv.removed(CONF_SPEED_COMMAND_TOPIC), + cv.removed(CONF_SPEED_LIST), + cv.removed(CONF_SPEED_STATE_TOPIC), + cv.removed(CONF_SPEED_VALUE_TEMPLATE), _PLATFORM_SCHEMA_BASE, valid_speed_range_configuration, valid_preset_mode_configuration, diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 05778aa7711..e2805781f45 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -224,7 +224,16 @@ DISCOVERY_SCHEMA_BASIC = vol.All( _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), ) -PLATFORM_SCHEMA_MODERN_BASIC = _PLATFORM_SCHEMA_BASE +PLATFORM_SCHEMA_MODERN_BASIC = vol.All( + # CONF_VALUE_TEMPLATE is no longer supported, support was removed in 2022.2 + cv.removed(CONF_VALUE_TEMPLATE), + # CONF_WHITE_VALUE_* is no longer supported, support was removed in 2022.9 + cv.removed(CONF_WHITE_VALUE_COMMAND_TOPIC), + cv.removed(CONF_WHITE_VALUE_SCALE), + cv.removed(CONF_WHITE_VALUE_STATE_TOPIC), + cv.removed(CONF_WHITE_VALUE_TEMPLATE), + _PLATFORM_SCHEMA_BASE, +) async def async_setup_entity_basic( diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 85a0bc335cd..295b43120d4 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -167,6 +167,8 @@ DISCOVERY_SCHEMA_JSON = vol.All( ) PLATFORM_SCHEMA_MODERN_JSON = vol.All( + # CONF_WHITE_VALUE is no longer supported, support was removed in 2022.9 + cv.removed(CONF_WHITE_VALUE), _PLATFORM_SCHEMA_BASE, valid_color_configuration, ) diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 6f211e598b4..73f2786ad12 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -99,7 +99,11 @@ DISCOVERY_SCHEMA_TEMPLATE = vol.All( _PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA), ) -PLATFORM_SCHEMA_MODERN_TEMPLATE = _PLATFORM_SCHEMA_BASE +PLATFORM_SCHEMA_MODERN_TEMPLATE = vol.All( + # CONF_WHITE_VALUE_TEMPLATE is no longer supported, support was removed in 2022.9 + cv.removed(CONF_WHITE_VALUE_TEMPLATE), + _PLATFORM_SCHEMA_BASE, +) async def async_setup_entity_template( From 324f5555ed6caa668111173ad81c50fb6f4f2e3a Mon Sep 17 00:00:00 2001 From: Dave Atherton Date: Fri, 19 Aug 2022 10:51:27 +0100 Subject: [PATCH 3479/3516] Change growatt server URL (#76824) Co-authored-by: Chris Straffon --- homeassistant/components/growatt_server/const.py | 6 +++++- homeassistant/components/growatt_server/sensor.py | 14 +++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/growatt_server/const.py b/homeassistant/components/growatt_server/const.py index 4fcc4887843..4e548ef2c2a 100644 --- a/homeassistant/components/growatt_server/const.py +++ b/homeassistant/components/growatt_server/const.py @@ -8,11 +8,15 @@ DEFAULT_PLANT_ID = "0" DEFAULT_NAME = "Growatt" SERVER_URLS = [ - "https://server.growatt.com/", + "https://server-api.growatt.com/", "https://server-us.growatt.com/", "http://server.smten.com/", ] +DEPRECATED_URLS = [ + "https://server.growatt.com/", +] + DEFAULT_URL = SERVER_URLS[0] DOMAIN = "growatt_server" diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index db045242987..c90bfa6f3fb 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -19,6 +19,7 @@ from .const import ( CONF_PLANT_ID, DEFAULT_PLANT_ID, DEFAULT_URL, + DEPRECATED_URLS, DOMAIN, LOGIN_INVALID_AUTH_CODE, ) @@ -62,12 +63,23 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Growatt sensor.""" - config = config_entry.data + config = {**config_entry.data} username = config[CONF_USERNAME] password = config[CONF_PASSWORD] url = config.get(CONF_URL, DEFAULT_URL) name = config[CONF_NAME] + # If the URL has been deprecated then change to the default instead + if url in DEPRECATED_URLS: + _LOGGER.info( + "URL: %s has been deprecated, migrating to the latest default: %s", + url, + DEFAULT_URL, + ) + url = DEFAULT_URL + config[CONF_URL] = url + hass.config_entries.async_update_entry(config_entry, data=config) + api = growattServer.GrowattApi() api.server_url = url From 63dcd8ec089e9490ba6caa4961d6a5af5adf6b3f Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Fri, 19 Aug 2022 12:57:30 +0300 Subject: [PATCH 3480/3516] Bump pydroid-ipcam to 2.0.0 (#76906) Co-authored-by: Martin Hjelmare --- .../android_ip_webcam/binary_sensor.py | 2 +- .../android_ip_webcam/config_flow.py | 19 ++++++++--- .../android_ip_webcam/coordinator.py | 8 +++-- .../android_ip_webcam/manifest.json | 2 +- .../components/android_ip_webcam/sensor.py | 32 +++++++++---------- .../components/android_ip_webcam/strings.json | 3 +- .../components/android_ip_webcam/switch.py | 8 ++--- .../android_ip_webcam/translations/en.json | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../android_ip_webcam/test_config_flow.py | 23 ++++++++++++- .../components/android_ip_webcam/test_init.py | 24 ++++++++++++-- 12 files changed, 91 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/android_ip_webcam/binary_sensor.py b/homeassistant/components/android_ip_webcam/binary_sensor.py index a2dc25d825b..6f17616a216 100644 --- a/homeassistant/components/android_ip_webcam/binary_sensor.py +++ b/homeassistant/components/android_ip_webcam/binary_sensor.py @@ -57,4 +57,4 @@ class IPWebcamBinarySensor(AndroidIPCamBaseEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return if motion is detected.""" - return self.cam.export_sensor(MOTION_ACTIVE)[0] == 1.0 + return self.cam.get_sensor_value(MOTION_ACTIVE) == 1.0 diff --git a/homeassistant/components/android_ip_webcam/config_flow.py b/homeassistant/components/android_ip_webcam/config_flow.py index 09f0fdaa3a2..c41a998ff54 100644 --- a/homeassistant/components/android_ip_webcam/config_flow.py +++ b/homeassistant/components/android_ip_webcam/config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Any from pydroid_ipcam import PyDroidIPCam +from pydroid_ipcam.exceptions import PyDroidIPCamException, Unauthorized import voluptuous as vol from homeassistant import config_entries @@ -33,7 +34,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( ) -async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool: +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]: """Validate the user input allows us to connect.""" websession = async_get_clientsession(hass) @@ -45,8 +46,16 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool: password=data.get(CONF_PASSWORD), ssl=False, ) - await cam.update() - return cam.available + errors = {} + try: + await cam.update() + except Unauthorized: + errors[CONF_USERNAME] = "invalid_auth" + errors[CONF_PASSWORD] = "invalid_auth" + except PyDroidIPCamException: + errors["base"] = "cannot_connect" + + return errors class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -68,13 +77,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) # to be removed when YAML import is removed title = user_input.get(CONF_NAME) or user_input[CONF_HOST] - if await validate_input(self.hass, user_input): + if not (errors := await validate_input(self.hass, user_input)): return self.async_create_entry(title=title, data=user_input) return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, - errors={"base": "cannot_connect"}, + errors=errors, ) async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: diff --git a/homeassistant/components/android_ip_webcam/coordinator.py b/homeassistant/components/android_ip_webcam/coordinator.py index 3940c6df7e4..1647b6890c1 100644 --- a/homeassistant/components/android_ip_webcam/coordinator.py +++ b/homeassistant/components/android_ip_webcam/coordinator.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging from pydroid_ipcam import PyDroidIPCam +from pydroid_ipcam.exceptions import PyDroidIPCamException from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST @@ -37,6 +38,7 @@ class AndroidIPCamDataUpdateCoordinator(DataUpdateCoordinator[None]): async def _async_update_data(self) -> None: """Update Android IP Webcam entities.""" - await self.cam.update() - if not self.cam.available: - raise UpdateFailed + try: + await self.cam.update() + except PyDroidIPCamException as err: + raise UpdateFailed(err) from err diff --git a/homeassistant/components/android_ip_webcam/manifest.json b/homeassistant/components/android_ip_webcam/manifest.json index 0023454728a..29a077443c0 100644 --- a/homeassistant/components/android_ip_webcam/manifest.json +++ b/homeassistant/components/android_ip_webcam/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["repairs"], "documentation": "https://www.home-assistant.io/integrations/android_ip_webcam", - "requirements": ["pydroid-ipcam==1.3.1"], + "requirements": ["pydroid-ipcam==2.0.0"], "codeowners": ["@engrbm87"], "iot_class": "local_polling" } diff --git a/homeassistant/components/android_ip_webcam/sensor.py b/homeassistant/components/android_ip_webcam/sensor.py index d699121d6c9..43a4a0c828c 100644 --- a/homeassistant/components/android_ip_webcam/sensor.py +++ b/homeassistant/components/android_ip_webcam/sensor.py @@ -54,8 +54,8 @@ SENSOR_TYPES: tuple[AndroidIPWebcamSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda ipcam: ipcam.export_sensor("battery_level")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("battery_level")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("battery_level"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("battery_level"), ), AndroidIPWebcamSensorEntityDescription( key="battery_temp", @@ -63,56 +63,56 @@ SENSOR_TYPES: tuple[AndroidIPWebcamSensorEntityDescription, ...] = ( icon="mdi:thermometer", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda ipcam: ipcam.export_sensor("battery_temp")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("battery_temp")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("battery_temp"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("battery_temp"), ), AndroidIPWebcamSensorEntityDescription( key="battery_voltage", name="Battery voltage", state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda ipcam: ipcam.export_sensor("battery_voltage")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("battery_voltage")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("battery_voltage"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("battery_voltage"), ), AndroidIPWebcamSensorEntityDescription( key="light", name="Light level", icon="mdi:flashlight", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda ipcam: ipcam.export_sensor("light")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("light")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("light"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("light"), ), AndroidIPWebcamSensorEntityDescription( key="motion", name="Motion", icon="mdi:run", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda ipcam: ipcam.export_sensor("motion")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("motion")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("motion"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("motion"), ), AndroidIPWebcamSensorEntityDescription( key="pressure", name="Pressure", icon="mdi:gauge", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda ipcam: ipcam.export_sensor("pressure")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("pressure")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("pressure"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("pressure"), ), AndroidIPWebcamSensorEntityDescription( key="proximity", name="Proximity", icon="mdi:map-marker-radius", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda ipcam: ipcam.export_sensor("proximity")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("proximity")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("proximity"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("proximity"), ), AndroidIPWebcamSensorEntityDescription( key="sound", name="Sound", icon="mdi:speaker", state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda ipcam: ipcam.export_sensor("sound")[0], - unit_fn=lambda ipcam: ipcam.export_sensor("sound")[1], + value_fn=lambda ipcam: ipcam.get_sensor_value("sound"), + unit_fn=lambda ipcam: ipcam.get_sensor_unit("sound"), ), AndroidIPWebcamSensorEntityDescription( key="video_connections", diff --git a/homeassistant/components/android_ip_webcam/strings.json b/homeassistant/components/android_ip_webcam/strings.json index a9ade78a413..6f6639cecb4 100644 --- a/homeassistant/components/android_ip_webcam/strings.json +++ b/homeassistant/components/android_ip_webcam/strings.json @@ -11,7 +11,8 @@ } }, "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" diff --git a/homeassistant/components/android_ip_webcam/switch.py b/homeassistant/components/android_ip_webcam/switch.py index b09b0de4be8..9d2175fe3d3 100644 --- a/homeassistant/components/android_ip_webcam/switch.py +++ b/homeassistant/components/android_ip_webcam/switch.py @@ -55,8 +55,8 @@ SWITCH_TYPES: tuple[AndroidIPWebcamSwitchEntityDescription, ...] = ( name="Focus", icon="mdi:image-filter-center-focus", entity_category=EntityCategory.CONFIG, - on_func=lambda ipcam: ipcam.torch(activate=True), - off_func=lambda ipcam: ipcam.torch(activate=False), + on_func=lambda ipcam: ipcam.focus(activate=True), + off_func=lambda ipcam: ipcam.focus(activate=False), ), AndroidIPWebcamSwitchEntityDescription( key="gps_active", @@ -111,8 +111,8 @@ SWITCH_TYPES: tuple[AndroidIPWebcamSwitchEntityDescription, ...] = ( name="Video recording", icon="mdi:record-rec", entity_category=EntityCategory.CONFIG, - on_func=lambda ipcam: ipcam.record(activate=True), - off_func=lambda ipcam: ipcam.record(activate=False), + on_func=lambda ipcam: ipcam.record(record=True), + off_func=lambda ipcam: ipcam.record(record=False), ), ) diff --git a/homeassistant/components/android_ip_webcam/translations/en.json b/homeassistant/components/android_ip_webcam/translations/en.json index 775263225ea..be6416341c2 100644 --- a/homeassistant/components/android_ip_webcam/translations/en.json +++ b/homeassistant/components/android_ip_webcam/translations/en.json @@ -4,7 +4,8 @@ "already_configured": "Device is already configured" }, "error": { - "cannot_connect": "Failed to connect" + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" }, "step": { "user": { diff --git a/requirements_all.txt b/requirements_all.txt index 733d82c9668..88174881477 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1476,7 +1476,7 @@ pydexcom==0.2.3 pydoods==1.0.2 # homeassistant.components.android_ip_webcam -pydroid-ipcam==1.3.1 +pydroid-ipcam==2.0.0 # homeassistant.components.ebox pyebox==1.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef6a03ddcd9..1291ff55669 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1025,7 +1025,7 @@ pydeconz==103 pydexcom==0.2.3 # homeassistant.components.android_ip_webcam -pydroid-ipcam==1.3.1 +pydroid-ipcam==2.0.0 # homeassistant.components.econet pyeconet==0.1.15 diff --git a/tests/components/android_ip_webcam/test_config_flow.py b/tests/components/android_ip_webcam/test_config_flow.py index 1ede523ecd2..d203ef15e63 100644 --- a/tests/components/android_ip_webcam/test_config_flow.py +++ b/tests/components/android_ip_webcam/test_config_flow.py @@ -1,6 +1,6 @@ """Test the Android IP Webcam config flow.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import Mock, patch import aiohttp @@ -99,6 +99,27 @@ async def test_device_already_configured( assert result2["reason"] == "already_configured" +async def test_form_invalid_auth( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we handle invalid auth error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + aioclient_mock.get( + "http://1.1.1.1:8080/status.json?show_avail=1", + exc=aiohttp.ClientResponseError(Mock(), (), status=401), + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1", "port": 8080, "username": "user", "password": "wrong-pass"}, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"username": "invalid_auth", "password": "invalid_auth"} + + async def test_form_cannot_connect( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: diff --git a/tests/components/android_ip_webcam/test_init.py b/tests/components/android_ip_webcam/test_init.py index e0c21445d71..1fee1a5c388 100644 --- a/tests/components/android_ip_webcam/test_init.py +++ b/tests/components/android_ip_webcam/test_init.py @@ -3,6 +3,7 @@ from collections.abc import Awaitable from typing import Callable +from unittest.mock import Mock import aiohttp @@ -19,6 +20,8 @@ MOCK_CONFIG_DATA = { "name": "IP Webcam", "host": "1.1.1.1", "port": 8080, + "username": "user", + "password": "pass", } @@ -50,10 +53,10 @@ async def test_successful_config_entry( assert entry.state == ConfigEntryState.LOADED -async def test_setup_failed( +async def test_setup_failed_connection_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: - """Test integration failed due to an error.""" + """Test integration failed due to connection error.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) entry.add_to_hass(hass) @@ -67,6 +70,23 @@ async def test_setup_failed( assert entry.state == ConfigEntryState.SETUP_RETRY +async def test_setup_failed_invalid_auth( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test integration failed due to invalid auth.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) + entry.add_to_hass(hass) + aioclient_mock.get( + "http://1.1.1.1:8080/status.json?show_avail=1", + exc=aiohttp.ClientResponseError(Mock(), (), status=401), + ) + + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state == ConfigEntryState.SETUP_RETRY + + async def test_unload_entry(hass: HomeAssistant, aioclient_mock_fixture) -> None: """Test removing integration.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) From 039c071a80b5da5f767114740fa3356e6bfdcf9c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Aug 2022 12:09:58 +0200 Subject: [PATCH 3481/3516] Improve type hint in brottsplatskartan sensor entity (#77019) --- homeassistant/components/brottsplatskartan/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index 535fa9f56ad..d76cb7c8a5f 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -87,7 +87,7 @@ class BrottsplatskartanSensor(SensorEntity): _attr_attribution = brottsplatskartan.ATTRIBUTION - def __init__(self, bpk, name): + def __init__(self, bpk: brottsplatskartan.BrottsplatsKartan, name: str) -> None: """Initialize the Brottsplatskartan sensor.""" self._brottsplatskartan = bpk self._attr_name = name @@ -103,8 +103,8 @@ class BrottsplatskartanSensor(SensorEntity): return for incident in incidents: - incident_type = incident.get("title_type") - incident_counts[incident_type] += 1 + if (incident_type := incident.get("title_type")) is not None: + incident_counts[incident_type] += 1 self._attr_extra_state_attributes = incident_counts self._attr_native_value = len(incidents) From 80c1c11b1a38c750093a321fd13138a21b8b13dd Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Fri, 19 Aug 2022 13:10:34 +0300 Subject: [PATCH 3482/3516] Re-write tests for `transmission` (#76607) Co-authored-by: Martin Hjelmare --- tests/components/transmission/__init__.py | 8 + .../transmission/test_config_flow.py | 459 +++++++----------- tests/components/transmission/test_init.py | 146 ++---- 3 files changed, 238 insertions(+), 375 deletions(-) diff --git a/tests/components/transmission/__init__.py b/tests/components/transmission/__init__.py index b8f8d8c847f..9da6c8304e0 100644 --- a/tests/components/transmission/__init__.py +++ b/tests/components/transmission/__init__.py @@ -1 +1,9 @@ """Tests for Transmission.""" + +MOCK_CONFIG_DATA = { + "name": "Transmission", + "host": "0.0.0.0", + "username": "user", + "password": "pass", + "port": 9091, +} diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 24df92f536e..44edc4b28a9 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -1,334 +1,165 @@ """Tests for Transmission config flow.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest from transmissionrpc.error import TransmissionError -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.components import transmission -from homeassistant.components.transmission import config_flow -from homeassistant.components.transmission.const import DEFAULT_SCAN_INTERVAL -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_SCAN_INTERVAL, - CONF_USERNAME, -) +from homeassistant.components.transmission.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import MOCK_CONFIG_DATA from tests.common import MockConfigEntry -NAME = "Transmission" -HOST = "192.168.1.100" -USERNAME = "username" -PASSWORD = "password" -PORT = 9091 -SCAN_INTERVAL = 10 -MOCK_ENTRY = { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_PORT: PORT, -} - - -@pytest.fixture(name="api") -def mock_transmission_api(): +@pytest.fixture(autouse=True) +def mock_api(): """Mock an api.""" - with patch("transmissionrpc.Client"): - yield + with patch("transmissionrpc.Client") as api: + yield api -@pytest.fixture(name="auth_error") -def mock_api_authentication_error(): - """Mock an api.""" - with patch( - "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized") - ): - yield - - -@pytest.fixture(name="conn_error") -def mock_api_connection_error(): - """Mock an api.""" - with patch( - "transmissionrpc.Client", - side_effect=TransmissionError("111: Connection refused"), - ): - yield - - -@pytest.fixture(name="unknown_error") -def mock_api_unknown_error(): - """Mock an api.""" - with patch("transmissionrpc.Client", side_effect=TransmissionError): - yield - - -@pytest.fixture(name="transmission_setup", autouse=True) -def transmission_setup_fixture(): - """Mock transmission entry setup.""" - with patch( - "homeassistant.components.transmission.async_setup_entry", return_value=True - ): - yield - - -def init_config_flow(hass): - """Init a configuration flow.""" - flow = config_flow.TransmissionFlowHandler() - flow.hass = hass - return flow - - -async def test_flow_user_config(hass, api): - """Test user config.""" +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.transmission.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_CONFIG_DATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Transmission" + assert result2["data"] == MOCK_CONFIG_DATA + assert len(mock_setup_entry.mock_calls) == 1 -async def test_flow_required_fields(hass, api): - """Test with required fields only.""" - result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data={CONF_NAME: NAME, CONF_HOST: HOST, CONF_PORT: PORT}, - ) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == NAME - assert result["data"][CONF_NAME] == NAME - assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_PORT] == PORT - - -async def test_flow_all_provided(hass, api): - """Test with all provided.""" - result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data=MOCK_ENTRY, - ) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == NAME - assert result["data"][CONF_NAME] == NAME - assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_PORT] == PORT - - -async def test_options(hass): - """Test updating options.""" - entry = MockConfigEntry( - domain=transmission.DOMAIN, - title=CONF_NAME, - data=MOCK_ENTRY, - options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, - ) - flow = init_config_flow(hass) - options_flow = flow.async_get_options_flow(entry) - - result = await options_flow.async_step_init() - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "init" - result = await options_flow.async_step_init({CONF_SCAN_INTERVAL: 10}) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"][CONF_SCAN_INTERVAL] == 10 - - -async def test_host_already_configured(hass, api): - """Test host is already configured.""" - entry = MockConfigEntry( - domain=transmission.DOMAIN, - data=MOCK_ENTRY, - options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, - ) +async def test_device_already_configured( + hass: HomeAssistant, +) -> None: + """Test aborting if the device is already configured.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) entry.add_to_hass(hass) - mock_entry_unique_name = MOCK_ENTRY.copy() - mock_entry_unique_name[CONF_NAME] = "Transmission 1" result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data=mock_entry_unique_name, + DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "abort" - assert result["reason"] == "already_configured" + assert result["type"] == FlowResultType.FORM - mock_entry_unique_port = MOCK_ENTRY.copy() - mock_entry_unique_port[CONF_PORT] = 9092 - mock_entry_unique_port[CONF_NAME] = "Transmission 2" - result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data=mock_entry_unique_port, + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_CONFIG_DATA, ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + await hass.async_block_till_done() - mock_entry_unique_host = MOCK_ENTRY.copy() - mock_entry_unique_host[CONF_HOST] = "192.168.1.101" - mock_entry_unique_host[CONF_NAME] = "Transmission 3" - result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data=mock_entry_unique_host, - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" -async def test_name_already_configured(hass, api): +async def test_name_already_configured(hass): """Test name is already configured.""" entry = MockConfigEntry( domain=transmission.DOMAIN, - data=MOCK_ENTRY, - options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, + data=MOCK_CONFIG_DATA, + options={"scan_interval": 120}, ) entry.add_to_hass(hass) - mock_entry = MOCK_ENTRY.copy() - mock_entry[CONF_HOST] = "0.0.0.0" + mock_entry = MOCK_CONFIG_DATA.copy() + mock_entry["host"] = "1.1.1.1" result = await hass.config_entries.flow.async_init( transmission.DOMAIN, context={"source": config_entries.SOURCE_USER}, data=mock_entry, ) - assert result["type"] == "form" - assert result["errors"] == {CONF_NAME: "name_exists"} + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"name": "name_exists"} -async def test_error_on_wrong_credentials(hass, auth_error): - """Test with wrong credentials.""" - flow = init_config_flow(hass) - - result = await flow.async_step_user( - { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_PORT: PORT, - } - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == { - CONF_USERNAME: "invalid_auth", - CONF_PASSWORD: "invalid_auth", - } - - -async def test_error_on_connection_failure(hass, conn_error): - """Test when connection to host fails.""" - flow = init_config_flow(hass) - - result = await flow.async_step_user( - { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_PORT: PORT, - } - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {"base": "cannot_connect"} - - -async def test_error_on_unknown_error(hass, unknown_error): - """Test when connection to host fails.""" - flow = init_config_flow(hass) - - result = await flow.async_step_user( - { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_PORT: PORT, - } - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {"base": "cannot_connect"} - - -async def test_reauth_success(hass, api): - """Test we can reauth.""" +async def test_options(hass: HomeAssistant) -> None: + """Test updating options.""" entry = MockConfigEntry( domain=transmission.DOMAIN, - data=MOCK_ENTRY, + data=MOCK_CONFIG_DATA, + options={"scan_interval": 120}, ) entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "entry_id": entry.entry_id, - }, - data=MOCK_ENTRY, + with patch( + "homeassistant.components.transmission.async_setup_entry", + return_value=True, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={"scan_interval": 10} ) - assert result["type"] == "form" - assert result["step_id"] == "reauth_confirm" - assert result["description_placeholders"] == {CONF_USERNAME: USERNAME} + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"]["scan_interval"] == 10 + +async def test_error_on_wrong_credentials( + hass: HomeAssistant, mock_api: MagicMock +) -> None: + """Test we handle invalid credentials.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_api.side_effect = TransmissionError("401: Unauthorized") result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_PASSWORD: "test-password", - }, + MOCK_CONFIG_DATA, ) - - assert result2["type"] == "abort" - assert result2["reason"] == "reauth_successful" - - -async def test_reauth_failed(hass, auth_error): - """Test we can reauth.""" - entry = MockConfigEntry( - domain=transmission.DOMAIN, - data=MOCK_ENTRY, - ) - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - transmission.DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "entry_id": entry.entry_id, - }, - data=MOCK_ENTRY, - ) - - assert result["type"] == "form" - assert result["step_id"] == "reauth_confirm" - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_PASSWORD: "test-wrong-password", - }, - ) - - assert result2["type"] == "form" + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == { - CONF_PASSWORD: "invalid_auth", + "username": "invalid_auth", + "password": "invalid_auth", } -async def test_reauth_failed_conn_error(hass, conn_error): +async def test_error_on_connection_failure( + hass: HomeAssistant, mock_api: MagicMock +) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_api.side_effect = TransmissionError("111: Connection refused") + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_CONFIG_DATA, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_reauth_success(hass: HomeAssistant) -> None: """Test we can reauth.""" entry = MockConfigEntry( domain=transmission.DOMAIN, - data=MOCK_ENTRY, + data=MOCK_CONFIG_DATA, ) entry.add_to_hass(hass) @@ -338,18 +169,92 @@ async def test_reauth_failed_conn_error(hass, conn_error): "source": config_entries.SOURCE_REAUTH, "entry_id": entry.entry_id, }, - data=MOCK_ENTRY, + data=MOCK_CONFIG_DATA, ) - assert result["type"] == "form" + assert result["type"] == FlowResultType.FORM assert result["step_id"] == "reauth_confirm" + assert result["description_placeholders"] == {"username": "user"} + with patch( + "homeassistant.components.transmission.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "password": "test-password", + }, + ) + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth_failed(hass: HomeAssistant, mock_api: MagicMock) -> None: + """Test we can't reauth due to invalid password.""" + entry = MockConfigEntry( + domain=transmission.DOMAIN, + data=MOCK_CONFIG_DATA, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + transmission.DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_CONFIG_DATA, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["description_placeholders"] == {"username": "user"} + + mock_api.side_effect = TransmissionError("401: Unauthorized") result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_PASSWORD: "test-wrong-password", + "password": "wrong-password", }, ) - assert result2["type"] == "form" + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"password": "invalid_auth"} + + +async def test_reauth_failed_connection_error( + hass: HomeAssistant, mock_api: MagicMock +) -> None: + """Test we can't reauth due to connection error.""" + entry = MockConfigEntry( + domain=transmission.DOMAIN, + data=MOCK_CONFIG_DATA, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + transmission.DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_CONFIG_DATA, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["description_placeholders"] == {"username": "user"} + + mock_api.side_effect = TransmissionError("111: Connection refused") + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "password": "test-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/transmission/test_init.py b/tests/components/transmission/test_init.py index c3dc924c54e..60e2d67d75c 100644 --- a/tests/components/transmission/test_init.py +++ b/tests/components/transmission/test_init.py @@ -1,125 +1,75 @@ """Tests for Transmission init.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest from transmissionrpc.error import TransmissionError -from homeassistant.components import transmission -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.setup import async_setup_component +from homeassistant.components.transmission.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant -from tests.common import MockConfigEntry, mock_coro +from . import MOCK_CONFIG_DATA -MOCK_ENTRY = MockConfigEntry( - domain=transmission.DOMAIN, - data={ - transmission.CONF_NAME: "Transmission", - transmission.CONF_HOST: "0.0.0.0", - transmission.CONF_USERNAME: "user", - transmission.CONF_PASSWORD: "pass", - transmission.CONF_PORT: 9091, - }, -) +from tests.common import MockConfigEntry -@pytest.fixture(name="api") -def mock_transmission_api(): +@pytest.fixture(autouse=True) +def mock_api(): """Mock an api.""" - with patch("transmissionrpc.Client"): - yield + with patch("transmissionrpc.Client") as api: + yield api -@pytest.fixture(name="auth_error") -def mock_api_authentication_error(): - """Mock an api.""" - with patch( - "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized") - ): - yield +async def test_successful_config_entry(hass: HomeAssistant) -> None: + """Test settings up integration from config entry.""" - -@pytest.fixture(name="unknown_error") -def mock_api_unknown_error(): - """Mock an api.""" - with patch("transmissionrpc.Client", side_effect=TransmissionError): - yield - - -async def test_setup_with_no_config(hass): - """Test that we do not discover anything or try to set up a Transmission client.""" - assert await async_setup_component(hass, transmission.DOMAIN, {}) is True - assert transmission.DOMAIN not in hass.data - - -async def test_setup_with_config(hass, api): - """Test that we import the config and setup the client.""" - config = { - transmission.DOMAIN: { - transmission.CONF_NAME: "Transmission", - transmission.CONF_HOST: "0.0.0.0", - transmission.CONF_USERNAME: "user", - transmission.CONF_PASSWORD: "pass", - transmission.CONF_PORT: 9091, - }, - transmission.DOMAIN: { - transmission.CONF_NAME: "Transmission2", - transmission.CONF_HOST: "0.0.0.1", - transmission.CONF_USERNAME: "user", - transmission.CONF_PASSWORD: "pass", - transmission.CONF_PORT: 9091, - }, - } - assert await async_setup_component(hass, transmission.DOMAIN, config) is True - - -async def test_successful_config_entry(hass, api): - """Test that configured transmission is configured successfully.""" - - entry = MOCK_ENTRY + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) entry.add_to_hass(hass) - assert await transmission.async_setup_entry(hass, entry) is True - assert entry.options == { - transmission.CONF_SCAN_INTERVAL: transmission.DEFAULT_SCAN_INTERVAL, - transmission.CONF_LIMIT: transmission.DEFAULT_LIMIT, - transmission.CONF_ORDER: transmission.DEFAULT_ORDER, - } + await hass.config_entries.async_setup(entry.entry_id) + + assert entry.state == ConfigEntryState.LOADED -async def test_setup_failed(hass): - """Test transmission failed due to an error.""" +async def test_setup_failed_connection_error( + hass: HomeAssistant, mock_api: MagicMock +) -> None: + """Test integration failed due to connection error.""" - entry = MOCK_ENTRY + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) entry.add_to_hass(hass) - # test connection error raising ConfigEntryNotReady - with patch( - "transmissionrpc.Client", - side_effect=TransmissionError("111: Connection refused"), - ), pytest.raises(ConfigEntryNotReady): + mock_api.side_effect = TransmissionError("111: Connection refused") - await transmission.async_setup_entry(hass, entry) - - # test Authentication error returning false - - with patch( - "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized") - ), pytest.raises(ConfigEntryAuthFailed): - - assert await transmission.async_setup_entry(hass, entry) is False + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ConfigEntryState.SETUP_RETRY -async def test_unload_entry(hass, api): - """Test removing transmission client.""" - entry = MOCK_ENTRY +async def test_setup_failed_auth_error( + hass: HomeAssistant, mock_api: MagicMock +) -> None: + """Test integration failed due to invalid credentials error.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) entry.add_to_hass(hass) - with patch.object( - hass.config_entries, "async_forward_entry_unload", return_value=mock_coro(True) - ) as unload_entry: - assert await transmission.async_setup_entry(hass, entry) + mock_api.side_effect = TransmissionError("401: Unauthorized") - assert await transmission.async_unload_entry(hass, entry) - assert unload_entry.call_count == 2 - assert entry.entry_id not in hass.data[transmission.DOMAIN] + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ConfigEntryState.SETUP_ERROR + + +async def test_unload_entry(hass: HomeAssistant) -> None: + """Test removing integration.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_DATA) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + assert not hass.data[DOMAIN] From 0f792eb92eb28f19a07dbd823e62df64c28b833c Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Aug 2022 13:02:46 +0200 Subject: [PATCH 3483/3516] Improve entity type hints [c] (#77023) --- homeassistant/components/caldav/calendar.py | 6 +++-- .../components/channels/media_player.py | 20 +++++++------- homeassistant/components/citybikes/sensor.py | 2 +- .../components/clementine/media_player.py | 24 ++++++++--------- .../components/cloud/binary_sensor.py | 4 +-- homeassistant/components/cmus/media_player.py | 27 ++++++++++--------- homeassistant/components/coinbase/sensor.py | 4 +-- .../components/comed_hourly_pricing/sensor.py | 2 +- .../components/command_line/switch.py | 4 +-- .../components/compensation/sensor.py | 2 +- .../components/concord232/binary_sensor.py | 2 +- .../components/coolmaster/climate.py | 13 ++++----- .../components/coronavirus/sensor.py | 2 +- homeassistant/components/cups/sensor.py | 6 ++--- .../components/currencylayer/sensor.py | 2 +- 15 files changed, 63 insertions(+), 57 deletions(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 17a8d5deb2f..d510c0c08e7 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -135,11 +135,13 @@ class WebDavCalendarEntity(CalendarEntity): """Return the next upcoming event.""" return self._event - async def async_get_events(self, hass, start_date, end_date): + async def async_get_events( + self, hass: HomeAssistant, start_date: datetime, end_date: datetime + ) -> list[CalendarEvent]: """Get all events in a specific time frame.""" return await self.data.async_get_events(hass, start_date, end_date) - def update(self): + def update(self) -> None: """Update event data.""" self.data.update() self._event = self.data.event diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index 36fa2fe7ba0..acb9f7ae680 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -1,6 +1,8 @@ """Support for interfacing with an instance of getchannels.com.""" from __future__ import annotations +from typing import Any + from pychannels import Channels import voluptuous as vol @@ -167,7 +169,7 @@ class ChannelsPlayer(MediaPlayerEntity): return None - def update(self): + def update(self) -> None: """Retrieve latest state.""" self.update_favorite_channels() self.update_state(self.client.status()) @@ -211,39 +213,39 @@ class ChannelsPlayer(MediaPlayerEntity): return None - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) player.""" if mute != self.muted: response = self.client.toggle_muted() self.update_state(response) - def media_stop(self): + def media_stop(self) -> None: """Send media_stop command to player.""" self.status = "stopped" response = self.client.stop() self.update_state(response) - def media_play(self): + def media_play(self) -> None: """Send media_play command to player.""" response = self.client.resume() self.update_state(response) - def media_pause(self): + def media_pause(self) -> None: """Send media_pause command to player.""" response = self.client.pause() self.update_state(response) - def media_next_track(self): + def media_next_track(self) -> None: """Seek ahead.""" response = self.client.skip_forward() self.update_state(response) - def media_previous_track(self): + def media_previous_track(self) -> None: """Seek back.""" response = self.client.skip_backward() self.update_state(response) - def select_source(self, source): + def select_source(self, source: str) -> None: """Select a channel to tune to.""" for channel in self.favorite_channels: if channel["name"] == source: @@ -251,7 +253,7 @@ class ChannelsPlayer(MediaPlayerEntity): self.update_state(response) break - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Send the play_media command to the player.""" if media_type == MEDIA_TYPE_CHANNEL: response = self.client.play_channel(media_id) diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index 418e206fc36..b9085e24f45 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -284,7 +284,7 @@ class CityBikesStation(SensorEntity): self._station_id = station_id self.entity_id = entity_id - async def async_update(self): + async def async_update(self) -> None: """Update station state.""" for station in self._network.stations: if station[ATTR_ID] == self._station_id: diff --git a/homeassistant/components/clementine/media_player.py b/homeassistant/components/clementine/media_player.py index 6b0f74a9e63..06bfb654ea1 100644 --- a/homeassistant/components/clementine/media_player.py +++ b/homeassistant/components/clementine/media_player.py @@ -78,7 +78,7 @@ class ClementineDevice(MediaPlayerEntity): self._client = client self._attr_name = name - def update(self): + def update(self) -> None: """Retrieve the latest data from the Clementine Player.""" try: client = self._client @@ -115,14 +115,14 @@ class ClementineDevice(MediaPlayerEntity): self._attr_state = STATE_OFF raise - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" client = self._client sources = [s for s in client.playlists.values() if s["name"] == source] if len(sources) == 1: client.change_song(sources[0]["id"], 0) - async def async_get_media_image(self): + async def async_get_media_image(self) -> tuple[bytes | None, str | None]: """Fetch media image of current playing image.""" if self._client.current_track: image = bytes(self._client.current_track["art"]) @@ -130,45 +130,45 @@ class ClementineDevice(MediaPlayerEntity): return None, None - def volume_up(self): + def volume_up(self) -> None: """Volume up the media player.""" newvolume = min(self._client.volume + 4, 100) self._client.set_volume(newvolume) - def volume_down(self): + def volume_down(self) -> None: """Volume down media player.""" newvolume = max(self._client.volume - 4, 0) self._client.set_volume(newvolume) - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Send mute command.""" self._client.set_volume(0) - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level.""" self._client.set_volume(int(100 * volume)) - def media_play_pause(self): + def media_play_pause(self) -> None: """Simulate play pause media player.""" if self.state == STATE_PLAYING: self.media_pause() else: self.media_play() - def media_play(self): + def media_play(self) -> None: """Send play command.""" self._attr_state = STATE_PLAYING self._client.play() - def media_pause(self): + def media_pause(self) -> None: """Send media pause command to media player.""" self._attr_state = STATE_PAUSED self._client.pause() - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" self._client.next() - def media_previous_track(self): + def media_previous_track(self) -> None: """Send the previous track command.""" self._client.previous() diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py index 5f4c715c41a..b09d282e56b 100644 --- a/homeassistant/components/cloud/binary_sensor.py +++ b/homeassistant/components/cloud/binary_sensor.py @@ -56,7 +56,7 @@ class CloudRemoteBinary(BinarySensorEntity): """Return True if entity is available.""" return self.cloud.remote.certificate is not None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register update dispatcher.""" async def async_state_update(data): @@ -68,7 +68,7 @@ class CloudRemoteBinary(BinarySensorEntity): self.hass, DISPATCHER_REMOTE_UPDATE, async_state_update ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Register update dispatcher.""" if self._unsub_dispatcher is not None: self._unsub_dispatcher() diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py index d80b4f6ffb1..09fec24b543 100644 --- a/homeassistant/components/cmus/media_player.py +++ b/homeassistant/components/cmus/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pycmus import exceptions, remote import voluptuous as vol @@ -115,7 +116,7 @@ class CmusDevice(MediaPlayerEntity): self._attr_name = name or auto_name self.status = {} - def update(self): + def update(self) -> None: """Get the latest data and update the state.""" try: status = self._remote.cmus.get_status_dict() @@ -150,19 +151,19 @@ class CmusDevice(MediaPlayerEntity): _LOGGER.warning("Received no status from cmus") - def turn_off(self): + def turn_off(self) -> None: """Service to send the CMUS the command to stop playing.""" self._remote.cmus.player_stop() - def turn_on(self): + def turn_on(self) -> None: """Service to send the CMUS the command to start playing.""" self._remote.cmus.player_play() - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self._remote.cmus.set_volume(int(volume * 100)) - def volume_up(self): + def volume_up(self) -> None: """Set the volume up.""" left = self.status["set"].get("vol_left") right = self.status["set"].get("vol_right") @@ -174,7 +175,7 @@ class CmusDevice(MediaPlayerEntity): if current_volume <= 100: self._remote.cmus.set_volume(int(current_volume) + 5) - def volume_down(self): + def volume_down(self) -> None: """Set the volume down.""" left = self.status["set"].get("vol_left") right = self.status["set"].get("vol_right") @@ -186,7 +187,7 @@ class CmusDevice(MediaPlayerEntity): if current_volume <= 100: self._remote.cmus.set_volume(int(current_volume) - 5) - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Send the play command.""" if media_type in [MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST]: self._remote.cmus.player_play_file(media_id) @@ -198,26 +199,26 @@ class CmusDevice(MediaPlayerEntity): MEDIA_TYPE_PLAYLIST, ) - def media_pause(self): + def media_pause(self) -> None: """Send the pause command.""" self._remote.cmus.player_pause() - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" self._remote.cmus.player_next() - def media_previous_track(self): + def media_previous_track(self) -> None: """Send next track command.""" self._remote.cmus.player_prev() - def media_seek(self, position): + def media_seek(self, position: float) -> None: """Send seek command.""" self._remote.cmus.seek(position) - def media_play(self): + def media_play(self) -> None: """Send the play command.""" self._remote.cmus.player_play() - def media_stop(self): + def media_stop(self) -> None: """Send the stop command.""" self._remote.cmus.stop() diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index fb9ce0fe434..d1e25dcf2a0 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -161,7 +161,7 @@ class AccountSensor(SensorEntity): ATTR_NATIVE_BALANCE: f"{self._native_balance} {self._native_currency}", } - def update(self): + def update(self) -> None: """Get the latest state of the sensor.""" self._coinbase_data.update() for account in self._coinbase_data.accounts: @@ -233,7 +233,7 @@ class ExchangeRateSensor(SensorEntity): """Return the state attributes of the sensor.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} - def update(self): + def update(self) -> None: """Get the latest state of the sensor.""" self._coinbase_data.update() self._state = round( diff --git a/homeassistant/components/comed_hourly_pricing/sensor.py b/homeassistant/components/comed_hourly_pricing/sensor.py index 4b658cdaddd..38421813439 100644 --- a/homeassistant/components/comed_hourly_pricing/sensor.py +++ b/homeassistant/components/comed_hourly_pricing/sensor.py @@ -101,7 +101,7 @@ class ComedHourlyPricingSensor(SensorEntity): self._attr_name = name self.offset = offset - async def async_update(self): + async def async_update(self) -> None: """Get the ComEd Hourly Pricing data from the web service.""" try: sensor_type = self.entity_description.key diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index ff0d9b65f9d..7142f14e82d 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -172,13 +172,13 @@ class CommandSwitch(SwitchEntity): payload = self._value_template.render_with_possible_json_value(payload) self._attr_is_on = payload.lower() == "true" - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" if self._switch(self._command_on) and not self._command_state: self._attr_is_on = True self.schedule_update_ha_state() - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" if self._switch(self._command_off) and not self._command_state: self._attr_is_on = False diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index a1781d454e7..58666e0f3be 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -90,7 +90,7 @@ class CompensationSensor(SensorEntity): self._unique_id = unique_id self._name = name - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle added to Hass.""" self.async_on_remove( async_track_state_change_event( diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index dc5f89d84bb..305822222ac 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -133,7 +133,7 @@ class Concord232ZoneSensor(BinarySensorEntity): # True means "faulted" or "open" or "abnormal state" return bool(self._zone["state"] != "Normal") - def update(self): + def update(self) -> None: """Get updated stats from API.""" last_update = dt_util.utcnow() - self._client.last_zone_update _LOGGER.debug("Zone: %s ", self._zone) diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 2f2ea58bd5b..8333e66753e 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -1,5 +1,6 @@ """CoolMasterNet platform to control of CoolMasterNet Climate Devices.""" import logging +from typing import Any from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ClimateEntityFeature, HVACMode @@ -93,7 +94,7 @@ class CoolmasterClimate(CoordinatorEntity, ClimateEntity): return self.unique_id @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" if self._unit.temperature_unit == "celsius": return TEMP_CELSIUS @@ -134,20 +135,20 @@ class CoolmasterClimate(CoordinatorEntity, ClimateEntity): """Return the list of available fan modes.""" return FAN_MODES - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: _LOGGER.debug("Setting temp of %s to %s", self.unique_id, str(temp)) self._unit = await self._unit.set_thermostat(temp) self.async_write_ha_state() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new fan mode.""" _LOGGER.debug("Setting fan mode of %s to %s", self.unique_id, fan_mode) self._unit = await self._unit.set_fan_speed(fan_mode) self.async_write_ha_state() - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" _LOGGER.debug("Setting operation mode of %s to %s", self.unique_id, hvac_mode) @@ -157,13 +158,13 @@ class CoolmasterClimate(CoordinatorEntity, ClimateEntity): self._unit = await self._unit.set_mode(HA_STATE_TO_CM[hvac_mode]) await self.async_turn_on() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on.""" _LOGGER.debug("Turning %s on", self.unique_id) self._unit = await self._unit.turn_on() self.async_write_ha_state() - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off.""" _LOGGER.debug("Turning %s off", self.unique_id) self._unit = await self._unit.turn_off() diff --git a/homeassistant/components/coronavirus/sensor.py b/homeassistant/components/coronavirus/sensor.py index 52ed49b759e..1a4f5b72fcd 100644 --- a/homeassistant/components/coronavirus/sensor.py +++ b/homeassistant/components/coronavirus/sensor.py @@ -53,7 +53,7 @@ class CoronavirusSensor(CoordinatorEntity, SensorEntity): self.info_type = info_type @property - def available(self): + def available(self) -> bool: """Return if sensor is available.""" return self.coordinator.last_update_success and ( self.country in self.coordinator.data or self.country == OPTION_WORLDWIDE diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index 103a352f6cd..893e92f546e 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -157,7 +157,7 @@ class CupsSensor(SensorEntity): ATTR_PRINTER_URI_SUPPORTED: self._printer["printer-uri-supported"], } - def update(self): + def update(self) -> None: """Get the latest data and updates the states.""" self.data.update() self._printer = self.data.printers.get(self._name) @@ -234,7 +234,7 @@ class IPPSensor(SensorEntity): return state_attributes - def update(self): + def update(self) -> None: """Fetch new state data for the sensor.""" self.data.update() self._attributes = self.data.attributes.get(self._name) @@ -309,7 +309,7 @@ class MarkerSensor(SensorEntity): ATTR_PRINTER_NAME: printer_name, } - def update(self): + def update(self) -> None: """Update the state of the sensor.""" # Data fetching is done by CupsSensor/IPPSensor self._attributes = self.data.attributes diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index 9c43b5f0bc1..85f8b876765 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -99,7 +99,7 @@ class CurrencylayerSensor(SensorEntity): """Return the state attributes of the sensor.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} - def update(self): + def update(self) -> None: """Update current date.""" self.rest.update() if (value := self.rest.data) is not None: From d0986c765083fd7d597f03ea4679245417d8a6f8 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 19 Aug 2022 13:20:41 +0200 Subject: [PATCH 3484/3516] Type feedreader strictly (#76707) * Type feedreader strictly * Run hassfest --- .strict-typing | 1 + .../components/feedreader/__init__.py | 56 ++++++++++++------- mypy.ini | 10 ++++ 3 files changed, 46 insertions(+), 21 deletions(-) diff --git a/.strict-typing b/.strict-typing index a215c2187ab..1e6a47f508b 100644 --- a/.strict-typing +++ b/.strict-typing @@ -98,6 +98,7 @@ homeassistant.components.energy.* homeassistant.components.evil_genius_labs.* homeassistant.components.fan.* homeassistant.components.fastdotcom.* +homeassistant.components.feedreader.* homeassistant.components.file_upload.* homeassistant.components.filesize.* homeassistant.components.fitbit.* diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index b3f1a916012..0ee4d3c39f3 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -1,9 +1,13 @@ """Support for RSS/Atom feeds.""" +from __future__ import annotations + from datetime import datetime, timedelta from logging import getLogger from os.path import exists import pickle from threading import Lock +from time import struct_time +from typing import cast import feedparser import voluptuous as vol @@ -44,9 +48,9 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Feedreader component.""" - urls = config[DOMAIN][CONF_URLS] - scan_interval = config[DOMAIN].get(CONF_SCAN_INTERVAL) - max_entries = config[DOMAIN].get(CONF_MAX_ENTRIES) + urls: list[str] = config[DOMAIN][CONF_URLS] + scan_interval: timedelta = config[DOMAIN][CONF_SCAN_INTERVAL] + max_entries: int = config[DOMAIN][CONF_MAX_ENTRIES] data_file = hass.config.path(f"{DOMAIN}.pickle") storage = StoredData(data_file) feeds = [ @@ -58,16 +62,23 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: class FeedManager: """Abstraction over Feedparser module.""" - def __init__(self, url, scan_interval, max_entries, hass, storage): + def __init__( + self, + url: str, + scan_interval: timedelta, + max_entries: int, + hass: HomeAssistant, + storage: StoredData, + ) -> None: """Initialize the FeedManager object, poll as per scan interval.""" self._url = url self._scan_interval = scan_interval self._max_entries = max_entries - self._feed = None + self._feed: feedparser.FeedParserDict | None = None self._hass = hass self._firstrun = True self._storage = storage - self._last_entry_timestamp = None + self._last_entry_timestamp: struct_time | None = None self._last_update_successful = False self._has_published_parsed = False self._has_updated_parsed = False @@ -76,23 +87,23 @@ class FeedManager: hass.bus.listen_once(EVENT_HOMEASSISTANT_START, lambda _: self._update()) self._init_regular_updates(hass) - def _log_no_entries(self): + def _log_no_entries(self) -> None: """Send no entries log at debug level.""" _LOGGER.debug("No new entries to be published in feed %s", self._url) - def _init_regular_updates(self, hass): + def _init_regular_updates(self, hass: HomeAssistant) -> None: """Schedule regular updates at the top of the clock.""" track_time_interval(hass, lambda now: self._update(), self._scan_interval) @property - def last_update_successful(self): + def last_update_successful(self) -> bool: """Return True if the last feed update was successful.""" return self._last_update_successful - def _update(self): + def _update(self) -> None: """Update the feed and publish new entries to the event bus.""" _LOGGER.info("Fetching new data from feed %s", self._url) - self._feed = feedparser.parse( + self._feed: feedparser.FeedParserDict = feedparser.parse( # type: ignore[no-redef] self._url, etag=None if not self._feed else self._feed.get("etag"), modified=None if not self._feed else self._feed.get("modified"), @@ -125,15 +136,16 @@ class FeedManager: self._publish_new_entries() if self._has_published_parsed or self._has_updated_parsed: self._storage.put_timestamp( - self._feed_id, self._last_entry_timestamp + self._feed_id, cast(struct_time, self._last_entry_timestamp) ) else: self._log_no_entries() self._last_update_successful = True _LOGGER.info("Fetch from feed %s completed", self._url) - def _filter_entries(self): + def _filter_entries(self) -> None: """Filter the entries provided and return the ones to keep.""" + assert self._feed is not None if len(self._feed.entries) > self._max_entries: _LOGGER.debug( "Processing only the first %s entries in feed %s", @@ -142,7 +154,7 @@ class FeedManager: ) self._feed.entries = self._feed.entries[0 : self._max_entries] - def _update_and_fire_entry(self, entry): + def _update_and_fire_entry(self, entry: feedparser.FeedParserDict) -> None: """Update last_entry_timestamp and fire entry.""" # Check if the entry has a published or updated date. if "published_parsed" in entry and entry.published_parsed: @@ -169,8 +181,9 @@ class FeedManager: entry.update({"feed_url": self._url}) self._hass.bus.fire(self._event_type, entry) - def _publish_new_entries(self): + def _publish_new_entries(self) -> None: """Publish new entries to the event bus.""" + assert self._feed is not None new_entries = False self._last_entry_timestamp = self._storage.get_timestamp(self._feed_id) if self._last_entry_timestamp: @@ -202,15 +215,15 @@ class FeedManager: class StoredData: """Abstraction over pickle data storage.""" - def __init__(self, data_file): + def __init__(self, data_file: str) -> None: """Initialize pickle data storage.""" self._data_file = data_file self._lock = Lock() self._cache_outdated = True - self._data = {} + self._data: dict[str, struct_time] = {} self._fetch_data() - def _fetch_data(self): + def _fetch_data(self) -> None: """Fetch data stored into pickle file.""" if self._cache_outdated and exists(self._data_file): try: @@ -223,20 +236,21 @@ class StoredData: "Error loading data from pickled file %s", self._data_file ) - def get_timestamp(self, feed_id): + def get_timestamp(self, feed_id: str) -> struct_time | None: """Return stored timestamp for given feed id (usually the url).""" self._fetch_data() return self._data.get(feed_id) - def put_timestamp(self, feed_id, timestamp): + def put_timestamp(self, feed_id: str, timestamp: struct_time) -> None: """Update timestamp for given feed id (usually the url).""" self._fetch_data() with self._lock, open(self._data_file, "wb") as myfile: self._data.update({feed_id: timestamp}) _LOGGER.debug( - "Overwriting feed %s timestamp in storage file %s", + "Overwriting feed %s timestamp in storage file %s: %s", feed_id, self._data_file, + timestamp, ) try: pickle.dump(self._data, myfile) diff --git a/mypy.ini b/mypy.ini index fd609d8099b..863e673401c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -739,6 +739,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.feedreader.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.file_upload.*] check_untyped_defs = true disallow_incomplete_defs = true From d8392ef6ba320a03ad460ac31b8ca400059fbbbc Mon Sep 17 00:00:00 2001 From: Stephan Uhle Date: Fri, 19 Aug 2022 13:28:03 +0200 Subject: [PATCH 3485/3516] Add edl21 sensor unit mapping for Hz (#76783) Added sensor unit mapping for Hz. --- homeassistant/components/edl21/sensor.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index 65603b0c8c4..730acabbc98 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -22,6 +22,7 @@ from homeassistant.const import ( ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, ENERGY_WATT_HOUR, + FREQUENCY_HERTZ, POWER_WATT, ) from homeassistant.core import HomeAssistant, callback @@ -252,6 +253,7 @@ SENSOR_UNIT_MAPPING = { "A": ELECTRIC_CURRENT_AMPERE, "V": ELECTRIC_POTENTIAL_VOLT, "°": DEGREE, + "Hz": FREQUENCY_HERTZ, } From 2d197fd59efafd7f9edb4ee4d192e395af9932fe Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 19 Aug 2022 15:24:53 +0200 Subject: [PATCH 3486/3516] Add state selector (#77024) Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/selector.py | 24 ++++++++++++++++++++++++ tests/helpers/test_selector.py | 15 +++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index deacc821672..deb5cc7401c 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -741,6 +741,30 @@ class TargetSelectorConfig(TypedDict, total=False): device: SingleDeviceSelectorConfig +class StateSelectorConfig(TypedDict): + """Class to represent an state selector config.""" + + entity_id: str + + +@SELECTORS.register("state") +class StateSelector(Selector): + """Selector for an entity state.""" + + selector_type = "state" + + CONFIG_SCHEMA = vol.Schema({vol.Required("entity_id"): cv.entity_id}) + + def __init__(self, config: StateSelectorConfig) -> None: + """Instantiate a selector.""" + super().__init__(config) + + def __call__(self, data: Any) -> str: + """Validate the passed selection.""" + state: str = vol.Schema(str)(data) + return state + + @SELECTORS.register("target") class TargetSelector(Selector): """Selector of a target value (area ID, device ID, entity ID etc). diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index d1018299d96..70e17058923 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -294,6 +294,21 @@ def test_time_selector_schema(schema, valid_selections, invalid_selections): _test_selector("time", schema, valid_selections, invalid_selections) +@pytest.mark.parametrize( + "schema,valid_selections,invalid_selections", + ( + ( + {"entity_id": "sensor.abc"}, + ("on", "armed"), + (None, True, 1), + ), + ), +) +def test_state_selector_schema(schema, valid_selections, invalid_selections): + """Test state selector.""" + _test_selector("state", schema, valid_selections, invalid_selections) + + @pytest.mark.parametrize( "schema,valid_selections,invalid_selections", ( From bf7239c25db06f1377a895244a906b43242c9963 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 19 Aug 2022 16:10:45 +0200 Subject: [PATCH 3487/3516] Improve entity type hints [d] (#77031) --- homeassistant/components/daikin/climate.py | 17 +++---- homeassistant/components/daikin/sensor.py | 7 +-- homeassistant/components/daikin/switch.py | 39 ++++++++-------- .../components/danfoss_air/binary_sensor.py | 2 +- .../components/danfoss_air/sensor.py | 2 +- .../components/danfoss_air/switch.py | 7 +-- homeassistant/components/darksky/sensor.py | 2 +- homeassistant/components/darksky/weather.py | 4 +- homeassistant/components/delijn/sensor.py | 2 +- homeassistant/components/demo/media_player.py | 46 ++++++++++--------- homeassistant/components/demo/vacuum.py | 7 ++- homeassistant/components/demo/water_heater.py | 10 ++-- .../components/denon/media_player.py | 24 +++++----- .../components/denonavr/media_player.py | 24 +++++----- homeassistant/components/derivative/sensor.py | 2 +- .../components/deutsche_bahn/sensor.py | 2 +- .../components/devolo_home_control/climate.py | 2 +- .../components/digital_ocean/binary_sensor.py | 2 +- .../components/digital_ocean/switch.py | 7 +-- .../components/directv/media_player.py | 19 ++++---- homeassistant/components/discogs/sensor.py | 2 +- homeassistant/components/dlink/switch.py | 7 +-- .../components/dlna_dmr/media_player.py | 2 +- homeassistant/components/doorbird/camera.py | 2 +- homeassistant/components/dovado/sensor.py | 2 +- .../components/dsmr_reader/sensor.py | 2 +- .../components/dte_energy_bridge/sensor.py | 2 +- .../components/dublin_bus_transport/sensor.py | 2 +- .../components/dwd_weather_warnings/sensor.py | 4 +- homeassistant/components/dweet/sensor.py | 2 +- homeassistant/components/dynalite/switch.py | 6 ++- 31 files changed, 141 insertions(+), 119 deletions(-) diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 1b84b182ac8..271449a01cd 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -177,7 +178,7 @@ class DaikinClimate(ClimateEntity): return self._api.device.mac @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement which this thermostat uses.""" return TEMP_CELSIUS @@ -196,7 +197,7 @@ class DaikinClimate(ClimateEntity): """Return the supported step of target temperature.""" return 1 - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" await self._set(kwargs) @@ -232,7 +233,7 @@ class DaikinClimate(ClimateEntity): """Return the fan setting.""" return self._api.device.represent(HA_ATTR_TO_DAIKIN[ATTR_FAN_MODE])[1].title() - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set fan mode.""" await self._set({ATTR_FAN_MODE: fan_mode}) @@ -246,7 +247,7 @@ class DaikinClimate(ClimateEntity): """Return the fan setting.""" return self._api.device.represent(HA_ATTR_TO_DAIKIN[ATTR_SWING_MODE])[1].title() - async def async_set_swing_mode(self, swing_mode): + async def async_set_swing_mode(self, swing_mode: str) -> None: """Set new target temperature.""" await self._set({ATTR_SWING_MODE: swing_mode}) @@ -275,7 +276,7 @@ class DaikinClimate(ClimateEntity): return PRESET_ECO return PRESET_NONE - async def async_set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set preset mode.""" if preset_mode == PRESET_AWAY: await self._api.device.set_holiday(ATTR_STATE_ON) @@ -309,15 +310,15 @@ class DaikinClimate(ClimateEntity): ret += [PRESET_ECO, PRESET_BOOST] return ret - async def async_update(self): + async def async_update(self) -> None: """Retrieve latest state.""" await self._api.async_update() - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn device on.""" await self._api.device.set({}) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn device off.""" await self._api.device.set( {HA_ATTR_TO_DAIKIN[ATTR_HVAC_MODE]: HA_STATE_TO_DAIKIN[HVACMode.OFF]} diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index 1843bdac25f..1adacd322cc 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -21,6 +21,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -181,7 +182,7 @@ class DaikinSensor(SensorEntity): self._attr_name = f"{api.name} {description.name}" @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID.""" return f"{self._api.device.mac}-{self.entity_description.key}" @@ -190,11 +191,11 @@ class DaikinSensor(SensorEntity): """Return the state of the sensor.""" return self.entity_description.value_func(self._api.device) - async def async_update(self): + async def async_update(self) -> None: """Retrieve latest state.""" await self._api.async_update() @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return self._api.device_info diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index d5601a38989..0f885e63bf7 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -1,13 +1,16 @@ """Support for Daikin AirBase zones.""" from __future__ import annotations +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as DAIKIN_DOMAIN +from . import DOMAIN as DAIKIN_DOMAIN, DaikinApi ZONE_ICON = "mdi:home-circle" STREAMER_ICON = "mdi:air-filter" @@ -32,7 +35,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Daikin climate based on config_entry.""" - daikin_api = hass.data[DAIKIN_DOMAIN][entry.entry_id] + daikin_api: DaikinApi = hass.data[DAIKIN_DOMAIN][entry.entry_id] switches: list[DaikinZoneSwitch | DaikinStreamerSwitch] = [] if zones := daikin_api.device.zones: switches.extend( @@ -54,13 +57,13 @@ async def async_setup_entry( class DaikinZoneSwitch(SwitchEntity): """Representation of a zone.""" - def __init__(self, daikin_api, zone_id): + def __init__(self, daikin_api: DaikinApi, zone_id): """Initialize the zone.""" self._api = daikin_api self._zone_id = zone_id @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID.""" return f"{self._api.device.mac}-zone{self._zone_id}" @@ -70,29 +73,29 @@ class DaikinZoneSwitch(SwitchEntity): return ZONE_ICON @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return f"{self._api.name} {self._api.device.zones[self._zone_id][0]}" @property - def is_on(self): + def is_on(self) -> bool: """Return the state of the sensor.""" return self._api.device.zones[self._zone_id][1] == "1" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return self._api.device_info - async def async_update(self): + async def async_update(self) -> None: """Retrieve latest state.""" await self._api.async_update() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the zone on.""" await self._api.device.set_zone(self._zone_id, "1") - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the zone off.""" await self._api.device.set_zone(self._zone_id, "0") @@ -100,12 +103,12 @@ class DaikinZoneSwitch(SwitchEntity): class DaikinStreamerSwitch(SwitchEntity): """Streamer state.""" - def __init__(self, daikin_api): + def __init__(self, daikin_api: DaikinApi) -> None: """Initialize streamer switch.""" self._api = daikin_api @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique ID.""" return f"{self._api.device.mac}-streamer" @@ -115,30 +118,30 @@ class DaikinStreamerSwitch(SwitchEntity): return STREAMER_ICON @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return f"{self._api.name} streamer" @property - def is_on(self): + def is_on(self) -> bool: """Return the state of the sensor.""" return ( DAIKIN_ATTR_STREAMER in self._api.device.represent(DAIKIN_ATTR_ADVANCED)[1] ) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" return self._api.device_info - async def async_update(self): + async def async_update(self) -> None: """Retrieve latest state.""" await self._api.async_update() - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the zone on.""" await self._api.device.set_streamer("on") - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the zone off.""" await self._api.device.set_streamer("off") diff --git a/homeassistant/components/danfoss_air/binary_sensor.py b/homeassistant/components/danfoss_air/binary_sensor.py index b01e51e5061..3764345a7b8 100644 --- a/homeassistant/components/danfoss_air/binary_sensor.py +++ b/homeassistant/components/danfoss_air/binary_sensor.py @@ -50,7 +50,7 @@ class DanfossAirBinarySensor(BinarySensorEntity): self._type = sensor_type self._attr_device_class = device_class - def update(self): + def update(self) -> None: """Fetch new state data for the sensor.""" self._data.update() diff --git a/homeassistant/components/danfoss_air/sensor.py b/homeassistant/components/danfoss_air/sensor.py index d85e8248e60..de736b2c599 100644 --- a/homeassistant/components/danfoss_air/sensor.py +++ b/homeassistant/components/danfoss_air/sensor.py @@ -119,7 +119,7 @@ class DanfossAir(SensorEntity): self._attr_device_class = device_class self._attr_state_class = state_class - def update(self): + def update(self) -> None: """Update the new state of the sensor. This is done through the DanfossAir object that does the actual diff --git a/homeassistant/components/danfoss_air/switch.py b/homeassistant/components/danfoss_air/switch.py index b2e7189550f..b1ee7dce44a 100644 --- a/homeassistant/components/danfoss_air/switch.py +++ b/homeassistant/components/danfoss_air/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pydanfossair.commands import ReadCommand, UpdateCommand @@ -75,17 +76,17 @@ class DanfossAir(SwitchEntity): """Return true if switch is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" _LOGGER.debug("Turning on switch with command %s", self._on_command) self._data.update_state(self._on_command, self._state_command) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" _LOGGER.debug("Turning off switch with command %s", self._off_command) self._data.update_state(self._off_command, self._state_command) - def update(self): + def update(self) -> None: """Update the switch's state.""" self._data.update() diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index 0686f304674..84a0b0f23f2 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -825,7 +825,7 @@ class DarkSkyAlertSensor(SensorEntity): """Return the state attributes.""" return self._alerts - def update(self): + def update(self) -> None: """Get the latest data from Dark Sky and updates the states.""" # Call the API for new forecast data. Each sensor will re-trigger this # same exact call, but that's fine. We cache results for a short period diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index 1bc56706007..88f3b6b2bc9 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -131,7 +131,7 @@ class DarkSkyWeather(WeatherEntity): self._ds_daily = None @property - def available(self): + def available(self) -> bool: """Return if weather data is available from Dark Sky.""" return self._ds_data is not None @@ -233,7 +233,7 @@ class DarkSkyWeather(WeatherEntity): return data - def update(self): + def update(self) -> None: """Get the latest data from Dark Sky.""" self._dark_sky.update() diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index ee58a4f21c7..f0a263d0d0f 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -92,7 +92,7 @@ class DeLijnPublicTransportSensor(SensorEntity): self.line = line self._attr_extra_state_attributes = {} - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from the De Lijn API.""" try: await self.line.get_passages() diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index f3046a6f807..e52bf8720e1 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -1,6 +1,8 @@ """Demo implementation of the media player.""" from __future__ import annotations +from typing import Any + from homeassistant.components.media_player import ( MediaPlayerDeviceClass, MediaPlayerEntity, @@ -164,57 +166,57 @@ class AbstractDemoPlayer(MediaPlayerEntity): """Return the device class of the media player.""" return self._device_class - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self._player_state = STATE_PLAYING self.schedule_update_ha_state() - def turn_off(self): + def turn_off(self) -> None: """Turn the media player off.""" self._player_state = STATE_OFF self.schedule_update_ha_state() - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute the volume.""" self._volume_muted = mute self.schedule_update_ha_state() - def volume_up(self): + def volume_up(self) -> None: """Increase volume.""" self._volume_level = min(1.0, self._volume_level + 0.1) self.schedule_update_ha_state() - def volume_down(self): + def volume_down(self) -> None: """Decrease volume.""" self._volume_level = max(0.0, self._volume_level - 0.1) self.schedule_update_ha_state() - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set the volume level, range 0..1.""" self._volume_level = volume self.schedule_update_ha_state() - def media_play(self): + def media_play(self) -> None: """Send play command.""" self._player_state = STATE_PLAYING self.schedule_update_ha_state() - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" self._player_state = STATE_PAUSED self.schedule_update_ha_state() - def media_stop(self): + def media_stop(self) -> None: """Send stop command.""" self._player_state = STATE_OFF self.schedule_update_ha_state() - def set_shuffle(self, shuffle): + def set_shuffle(self, shuffle: bool) -> None: """Enable/disable shuffle mode.""" self._shuffle = shuffle self.schedule_update_ha_state() - def select_sound_mode(self, sound_mode): + def select_sound_mode(self, sound_mode: str) -> None: """Select sound mode.""" self._sound_mode = sound_mode self.schedule_update_ha_state() @@ -291,12 +293,12 @@ class DemoYoutubePlayer(AbstractDemoPlayer): if self._player_state == STATE_PLAYING: return self._progress_updated_at - def play_media(self, media_type, media_id, **kwargs): + def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play a piece of media.""" self.youtube_id = media_id self.schedule_update_ha_state() - def media_pause(self): + def media_pause(self) -> None: """Send pause command.""" self._progress = self.media_position self._progress_updated_at = dt_util.utcnow() @@ -393,38 +395,38 @@ class DemoMusicPlayer(AbstractDemoPlayer): """Flag media player features that are supported.""" return MUSIC_PLAYER_SUPPORT - def media_previous_track(self): + def media_previous_track(self) -> None: """Send previous track command.""" if self._cur_track > 0: self._cur_track -= 1 self.schedule_update_ha_state() - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" if self._cur_track < len(self.tracks) - 1: self._cur_track += 1 self.schedule_update_ha_state() - def clear_playlist(self): + def clear_playlist(self) -> None: """Clear players playlist.""" self.tracks = [] self._cur_track = 0 self._player_state = STATE_OFF self.schedule_update_ha_state() - def set_repeat(self, repeat): + def set_repeat(self, repeat: str) -> None: """Enable/disable repeat mode.""" self._repeat = repeat self.schedule_update_ha_state() - def join_players(self, group_members): + def join_players(self, group_members: list[str]) -> None: """Join `group_members` as a player group with the current player.""" self._group_members = [ self.entity_id, ] + group_members self.schedule_update_ha_state() - def unjoin_player(self): + def unjoin_player(self) -> None: """Remove this player from any group.""" self._group_members = [] self.schedule_update_ha_state() @@ -505,19 +507,19 @@ class DemoTVShowPlayer(AbstractDemoPlayer): """Flag media player features that are supported.""" return NETFLIX_PLAYER_SUPPORT - def media_previous_track(self): + def media_previous_track(self) -> None: """Send previous track command.""" if self._cur_episode > 1: self._cur_episode -= 1 self.schedule_update_ha_state() - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" if self._cur_episode < self._episode_count: self._cur_episode += 1 self.schedule_update_ha_state() - def select_source(self, source): + def select_source(self, source: str) -> None: """Set the input source.""" self._source = source self.schedule_update_ha_state() diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index 58b76ba6347..10952f86785 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -240,7 +240,12 @@ class DemoVacuum(VacuumEntity): self._battery_level += 5 self.schedule_update_ha_state() - def send_command(self, command, params=None, **kwargs: Any) -> None: + def send_command( + self, + command: str, + params: dict[str, Any] | list[Any] | None = None, + **kwargs: Any, + ) -> None: """Send a command to the vacuum.""" if self.supported_features & VacuumEntityFeature.SEND_COMMAND == 0: return diff --git a/homeassistant/components/demo/water_heater.py b/homeassistant/components/demo/water_heater.py index 70332277d90..c3cb2be4fb3 100644 --- a/homeassistant/components/demo/water_heater.py +++ b/homeassistant/components/demo/water_heater.py @@ -1,6 +1,8 @@ """Demo platform that offers a fake water heater device.""" from __future__ import annotations +from typing import Any + from homeassistant.components.water_heater import ( WaterHeaterEntity, WaterHeaterEntityFeature, @@ -79,22 +81,22 @@ class DemoWaterHeater(WaterHeaterEntity): "off", ] - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" self._attr_target_temperature = kwargs.get(ATTR_TEMPERATURE) self.schedule_update_ha_state() - def set_operation_mode(self, operation_mode): + def set_operation_mode(self, operation_mode: str) -> None: """Set new operation mode.""" self._attr_current_operation = operation_mode self.schedule_update_ha_state() - def turn_away_mode_on(self): + def turn_away_mode_on(self) -> None: """Turn away mode on.""" self._attr_is_away_mode_on = True self.schedule_update_ha_state() - def turn_away_mode_off(self): + def turn_away_mode_off(self) -> None: """Turn away mode off.""" self._attr_is_away_mode_on = False self.schedule_update_ha_state() diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index d55adcf5db7..4533af66927 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -253,51 +253,51 @@ class DenonDevice(MediaPlayerEntity): if self._mediasource == name: return pretty_name - def turn_off(self): + def turn_off(self) -> None: """Turn off media player.""" self.telnet_command("PWSTANDBY") - def volume_up(self): + def volume_up(self) -> None: """Volume up media player.""" self.telnet_command("MVUP") - def volume_down(self): + def volume_down(self) -> None: """Volume down media player.""" self.telnet_command("MVDOWN") - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self.telnet_command(f"MV{round(volume * self._volume_max):02}") - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) media player.""" mute_status = "ON" if mute else "OFF" self.telnet_command(f"MU{mute_status})") - def media_play(self): + def media_play(self) -> None: """Play media player.""" self.telnet_command("NS9A") - def media_pause(self): + def media_pause(self) -> None: """Pause media player.""" self.telnet_command("NS9B") - def media_stop(self): + def media_stop(self) -> None: """Pause media player.""" self.telnet_command("NS9C") - def media_next_track(self): + def media_next_track(self) -> None: """Send the next track command.""" self.telnet_command("NS9D") - def media_previous_track(self): + def media_previous_track(self) -> None: """Send the previous track command.""" self.telnet_command("NS9E") - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self.telnet_command("PWON") - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" self.telnet_command(f"SI{self._source_list.get(source)}") diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 7c5d98ca1b3..bc3b4264d24 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -390,32 +390,32 @@ class DenonDevice(MediaPlayerEntity): return self._receiver.dynamic_eq @async_log_errors - async def async_media_play_pause(self): + async def async_media_play_pause(self) -> None: """Play or pause the media player.""" await self._receiver.async_toggle_play_pause() @async_log_errors - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" await self._receiver.async_play() @async_log_errors - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" await self._receiver.async_pause() @async_log_errors - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send previous track command.""" await self._receiver.async_previous_track() @async_log_errors - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" await self._receiver.async_next_track() @async_log_errors - async def async_select_source(self, source: str): + async def async_select_source(self, source: str) -> None: """Select input source.""" # Ensure that the AVR is turned on, which is necessary for input # switch to work. @@ -423,27 +423,27 @@ class DenonDevice(MediaPlayerEntity): await self._receiver.async_set_input_func(source) @async_log_errors - async def async_select_sound_mode(self, sound_mode: str): + async def async_select_sound_mode(self, sound_mode: str) -> None: """Select sound mode.""" await self._receiver.async_set_sound_mode(sound_mode) @async_log_errors - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on media player.""" await self._receiver.async_power_on() @async_log_errors - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off media player.""" await self._receiver.async_power_off() @async_log_errors - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Volume up the media player.""" await self._receiver.async_volume_up() @async_log_errors - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Volume down media player.""" await self._receiver.async_volume_down() @@ -458,7 +458,7 @@ class DenonDevice(MediaPlayerEntity): await self._receiver.async_set_volume(volume_denon) @async_log_errors - async def async_mute_volume(self, mute: bool): + async def async_mute_volume(self, mute: bool) -> None: """Send mute command.""" await self._receiver.async_mute(mute) diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index fb5bf7e518d..5337328bbe2 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -168,7 +168,7 @@ class DerivativeSensor(RestoreEntity, SensorEntity): self._unit_time = UNIT_TIME[unit_time] self._time_window = time_window.total_seconds() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await super().async_added_to_hass() if (state := await self.async_get_last_state()) is not None: diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py index 07c330fd402..9638fccf2cd 100644 --- a/homeassistant/components/deutsche_bahn/sensor.py +++ b/homeassistant/components/deutsche_bahn/sensor.py @@ -98,7 +98,7 @@ class DeutscheBahnSensor(SensorEntity): connections["next_on"] = self.data.connections[2]["departure"] return connections - def update(self): + def update(self) -> None: """Get the latest delay from bahn.de and updates the state.""" self.data.update() self._state = self.data.connections[0].get("departure", "Unknown") diff --git a/homeassistant/components/devolo_home_control/climate.py b/homeassistant/components/devolo_home_control/climate.py index cde7ca33152..4b8e8fc00e6 100644 --- a/homeassistant/components/devolo_home_control/climate.py +++ b/homeassistant/components/devolo_home_control/climate.py @@ -88,7 +88,7 @@ class DevoloClimateDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, ClimateEntit """Return the target temperature.""" return self._value - def set_hvac_mode(self, hvac_mode: str) -> None: + def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Do nothing as devolo devices do not support changing the hvac mode.""" def set_temperature(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/digital_ocean/binary_sensor.py b/homeassistant/components/digital_ocean/binary_sensor.py index b92e009e618..9728da99a6f 100644 --- a/homeassistant/components/digital_ocean/binary_sensor.py +++ b/homeassistant/components/digital_ocean/binary_sensor.py @@ -102,7 +102,7 @@ class DigitalOceanBinarySensor(BinarySensorEntity): ATTR_VCPUS: self.data.vcpus, } - def update(self): + def update(self) -> None: """Update state of sensor.""" self._digital_ocean.update() diff --git a/homeassistant/components/digital_ocean/switch.py b/homeassistant/components/digital_ocean/switch.py index efaa4fec6be..da955e221a3 100644 --- a/homeassistant/components/digital_ocean/switch.py +++ b/homeassistant/components/digital_ocean/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -94,17 +95,17 @@ class DigitalOceanSwitch(SwitchEntity): ATTR_VCPUS: self.data.vcpus, } - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Boot-up the droplet.""" if self.data.status != "active": self.data.power_on() - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Shutdown the droplet.""" if self.data.status == "active": self.data.power_off() - def update(self): + def update(self) -> None: """Get the latest data from the device and update the data.""" self._digital_ocean.update() diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index 2fb5acf8295..affefcacd85 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from directv import DIRECTV @@ -278,7 +279,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): return dt_util.as_local(self._program.start_time) - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on the receiver.""" if self._is_client: raise NotImplementedError() @@ -286,7 +287,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): _LOGGER.debug("Turn on %s", self.name) await self.dtv.remote("poweron", self._address) - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off the receiver.""" if self._is_client: raise NotImplementedError() @@ -294,32 +295,34 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): _LOGGER.debug("Turn off %s", self.name) await self.dtv.remote("poweroff", self._address) - async def async_media_play(self): + async def async_media_play(self) -> None: """Send play command.""" _LOGGER.debug("Play on %s", self.name) await self.dtv.remote("play", self._address) - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Send pause command.""" _LOGGER.debug("Pause on %s", self.name) await self.dtv.remote("pause", self._address) - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Send stop command.""" _LOGGER.debug("Stop on %s", self.name) await self.dtv.remote("stop", self._address) - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send rewind command.""" _LOGGER.debug("Rewind on %s", self.name) await self.dtv.remote("rew", self._address) - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send fast forward command.""" _LOGGER.debug("Fast forward on %s", self.name) await self.dtv.remote("ffwd", self._address) - async def async_play_media(self, media_type, media_id, **kwargs): + async def async_play_media( + self, media_type: str, media_id: str, **kwargs: Any + ) -> None: """Select input source.""" if media_type != MEDIA_TYPE_CHANNEL: _LOGGER.error( diff --git a/homeassistant/components/discogs/sensor.py b/homeassistant/components/discogs/sensor.py index 447eb4a754e..e207755ec24 100644 --- a/homeassistant/components/discogs/sensor.py +++ b/homeassistant/components/discogs/sensor.py @@ -157,7 +157,7 @@ class DiscogsSensor(SensorEntity): return None - def update(self): + def update(self) -> None: """Set state to the amount of records in user's collection.""" if self.entity_description.key == SENSOR_COLLECTION_TYPE: self._attr_native_value = self._discogs_data["collection_count"] diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index 4d5d7b5c639..f1ca99c51f2 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any import urllib from pyW215.pyW215 import SmartPlug @@ -106,15 +107,15 @@ class SmartPlugSwitch(SwitchEntity): """Return true if switch is on.""" return self.data.state == "ON" - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self.data.smartplug.state = "ON" - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self.data.smartplug.state = "OFF" - def update(self): + def update(self) -> None: """Get the latest data from the smart plug and updates the states.""" self.data.update() diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 9ecf9f8ad40..156e8fdffef 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -578,7 +578,7 @@ class DlnaDmrEntity(MediaPlayerEntity): await self._device.async_stop() @catch_request_errors - async def async_media_seek(self, position: int | float) -> None: + async def async_media_seek(self, position: float) -> None: """Send seek command.""" assert self._device is not None time = timedelta(seconds=position) diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index 77dc8a2d45c..fce76a65ff5 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -134,7 +134,7 @@ class DoorBirdCamera(DoorBirdEntity, Camera): ) return self._last_image - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Add callback after being added to hass. Registers entity_id map for the logbook diff --git a/homeassistant/components/dovado/sensor.py b/homeassistant/components/dovado/sensor.py index 99b1bd459fe..ee23592ef2d 100644 --- a/homeassistant/components/dovado/sensor.py +++ b/homeassistant/components/dovado/sensor.py @@ -133,7 +133,7 @@ class DovadoSensor(SensorEntity): return round(float(state) / 1e6, 1) return state - def update(self): + def update(self) -> None: """Update sensor values.""" self._data.update() self._attr_native_value = self._compute_state() diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py index 3dac30dcf6c..603b5682f42 100644 --- a/homeassistant/components/dsmr_reader/sensor.py +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -35,7 +35,7 @@ class DSMRSensor(SensorEntity): slug = slugify(description.key.replace("/", "_")) self.entity_id = f"sensor.{slug}" - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Subscribe to MQTT events.""" @callback diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py index 7f85710206d..b97a8eb83eb 100644 --- a/homeassistant/components/dte_energy_bridge/sensor.py +++ b/homeassistant/components/dte_energy_bridge/sensor.py @@ -71,7 +71,7 @@ class DteEnergyBridgeSensor(SensorEntity): self._attr_name = name - def update(self): + def update(self) -> None: """Get the energy usage data from the DTE energy bridge.""" try: response = requests.get(self._url, timeout=5) diff --git a/homeassistant/components/dublin_bus_transport/sensor.py b/homeassistant/components/dublin_bus_transport/sensor.py index e6ee4d92de9..cf65c18e91c 100644 --- a/homeassistant/components/dublin_bus_transport/sensor.py +++ b/homeassistant/components/dublin_bus_transport/sensor.py @@ -125,7 +125,7 @@ class DublinPublicTransportSensor(SensorEntity): """Icon to use in the frontend, if any.""" return ICON - def update(self): + def update(self) -> None: """Get the latest data from opendata.ch and update the states.""" self.data.update() self._times = self.data.info diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 941e1ee9017..1436b416031 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -164,11 +164,11 @@ class DwdWeatherWarningsSensor(SensorEntity): return data @property - def available(self): + def available(self) -> bool: """Could the device be accessed during the last update call.""" return self._api.api.data_valid - def update(self): + def update(self) -> None: """Get the latest data from the DWD-Weather-Warnings API.""" _LOGGER.debug( "Update requested for %s (%s) by %s", diff --git a/homeassistant/components/dweet/sensor.py b/homeassistant/components/dweet/sensor.py index fd5b64e206a..8a1b5a1bc6c 100644 --- a/homeassistant/components/dweet/sensor.py +++ b/homeassistant/components/dweet/sensor.py @@ -92,7 +92,7 @@ class DweetSensor(SensorEntity): """Return the state.""" return self._state - def update(self): + def update(self) -> None: """Get the latest data from REST API.""" self.dweet.update() diff --git a/homeassistant/components/dynalite/switch.py b/homeassistant/components/dynalite/switch.py index c98a1ce0ec4..3e459e45847 100644 --- a/homeassistant/components/dynalite/switch.py +++ b/homeassistant/components/dynalite/switch.py @@ -1,5 +1,7 @@ """Support for the Dynalite channels and presets as switches.""" +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -27,10 +29,10 @@ class DynaliteSwitch(DynaliteBase, SwitchEntity): """Return true if switch is on.""" return self._device.is_on - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" await self._device.async_turn_on() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" await self._device.async_turn_off() From 98c9399ff0b53c2221fbd78d2a4d4301affdc960 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Aug 2022 11:27:43 -1000 Subject: [PATCH 3488/3516] Bump yalexs-ble to 1.6.0 (#77042) --- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 4bc21e17d3a..1c97f687db6 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.4.0"], + "requirements": ["yalexs-ble==1.6.0"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], diff --git a/requirements_all.txt b/requirements_all.txt index 88174881477..561a7752b45 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2509,7 +2509,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.4.0 +yalexs-ble==1.6.0 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1291ff55669..fe7c8f4c37d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1710,7 +1710,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.4.0 +yalexs-ble==1.6.0 # homeassistant.components.august yalexs==1.2.1 From a076d3faa03f4fdd4744c26562f5471a54c800e1 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Fri, 19 Aug 2022 23:27:33 +0100 Subject: [PATCH 3489/3516] Address late review of system bridge media source (#77032) Co-authored-by: Martin Hjelmare --- .../components/system_bridge/media_source.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/system_bridge/media_source.py b/homeassistant/components/system_bridge/media_source.py index 6190cc1c5fe..dc6bc1bbbdc 100644 --- a/homeassistant/components/system_bridge/media_source.py +++ b/homeassistant/components/system_bridge/media_source.py @@ -124,7 +124,10 @@ def _build_base_url( entry: ConfigEntry, ) -> str: """Build base url for System Bridge media.""" - return f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}/api/media/file/data?apiKey={entry.data[CONF_API_KEY]}" + return ( + f"http://{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}" + f"/api/media/file/data?apiKey={entry.data[CONF_API_KEY]}" + ) def _build_root_paths( @@ -191,17 +194,19 @@ def _build_media_item( media_file: MediaFile, ) -> BrowseMediaSource: """Build individual media item.""" - ext = ( - f"~~{media_file.mime_type}" - if media_file.is_file and media_file.mime_type is not None - else "" - ) + ext = "" + if media_file.is_file and media_file.mime_type is not None: + ext = f"~~{media_file.mime_type}" + + if media_file.is_directory or media_file.mime_type is None: + media_class = MEDIA_CLASS_DIRECTORY + else: + media_class = MEDIA_CLASS_MAP[media_file.mime_type.split("/", 1)[0]] + return BrowseMediaSource( domain=DOMAIN, identifier=f"{path}/{media_file.name}{ext}", - media_class=MEDIA_CLASS_DIRECTORY - if media_file.is_directory or media_file.mime_type is None - else MEDIA_CLASS_MAP[media_file.mime_type.split("/", 1)[0]], + media_class=media_class, media_content_type=media_file.mime_type, title=media_file.name, can_play=media_file.is_file, From 21cd2f5db7e1a221b5b2b69b25e679c8f91adcbb Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sat, 20 Aug 2022 00:23:43 +0000 Subject: [PATCH 3490/3516] [ci skip] Translation update --- .../components/adax/translations/zh-Hant.json | 2 +- .../components/ambee/translations/es.json | 2 +- .../android_ip_webcam/translations/de.json | 3 +- .../android_ip_webcam/translations/es.json | 5 +- .../android_ip_webcam/translations/fr.json | 3 +- .../android_ip_webcam/translations/id.json | 3 +- .../android_ip_webcam/translations/it.json | 3 +- .../android_ip_webcam/translations/ja.json | 3 +- .../android_ip_webcam/translations/tr.json | 3 +- .../translations/zh-Hant.json | 3 +- .../components/awair/translations/id.json | 2 +- .../components/awair/translations/tr.json | 37 ++++++++++++-- .../components/bluetooth/translations/de.json | 11 +++- .../components/bluetooth/translations/en.json | 12 +++++ .../components/bluetooth/translations/es.json | 11 +++- .../components/bluetooth/translations/id.json | 11 +++- .../components/bluetooth/translations/it.json | 9 ++++ .../components/bluetooth/translations/ja.json | 9 ++++ .../components/bluetooth/translations/no.json | 11 +++- .../bluetooth/translations/pt-BR.json | 11 +++- .../components/bluetooth/translations/tr.json | 9 ++++ .../bluetooth/translations/zh-Hant.json | 15 ++++-- .../deutsche_bahn/translations/es.json | 2 +- .../fully_kiosk/translations/id.json | 20 ++++++++ .../fully_kiosk/translations/tr.json | 20 ++++++++ .../components/google/translations/es.json | 2 +- .../components/guardian/translations/es.json | 4 +- .../components/guardian/translations/tr.json | 13 +++++ .../translations/sensor.id.json | 2 + .../components/hue/translations/id.json | 2 +- .../components/hue/translations/tr.json | 5 +- .../lacrosse_view/translations/id.json | 3 +- .../lacrosse_view/translations/it.json | 3 +- .../lacrosse_view/translations/ja.json | 3 +- .../lacrosse_view/translations/no.json | 3 +- .../lacrosse_view/translations/tr.json | 3 +- .../lacrosse_view/translations/zh-Hant.json | 3 +- .../components/lametric/translations/id.json | 50 +++++++++++++++++++ .../components/lametric/translations/it.json | 46 +++++++++++++++++ .../components/lametric/translations/ja.json | 1 + .../components/lametric/translations/tr.json | 50 +++++++++++++++++++ .../landisgyr_heat_meter/translations/id.json | 23 +++++++++ .../landisgyr_heat_meter/translations/it.json | 23 +++++++++ .../landisgyr_heat_meter/translations/ja.json | 20 ++++++++ .../landisgyr_heat_meter/translations/no.json | 23 +++++++++ .../landisgyr_heat_meter/translations/tr.json | 23 +++++++++ .../translations/zh-Hant.json | 23 +++++++++ .../components/lyric/translations/es.json | 4 +- .../miflora/translations/zh-Hant.json | 2 +- .../mitemp_bt/translations/zh-Hant.json | 4 +- .../components/nest/translations/es.json | 2 +- .../components/pushover/translations/de.json | 34 +++++++++++++ .../components/pushover/translations/es.json | 34 +++++++++++++ .../components/pushover/translations/fr.json | 28 +++++++++++ .../components/pushover/translations/id.json | 34 +++++++++++++ .../components/pushover/translations/it.json | 34 +++++++++++++ .../components/pushover/translations/ja.json | 33 ++++++++++++ .../components/pushover/translations/no.json | 34 +++++++++++++ .../components/pushover/translations/tr.json | 34 +++++++++++++ .../pushover/translations/zh-Hant.json | 34 +++++++++++++ .../components/qingping/translations/tr.json | 22 ++++++++ .../radiotherm/translations/es.json | 2 +- .../components/schedule/translations/tr.json | 9 ++++ .../components/senz/translations/es.json | 4 +- .../simplepush/translations/es.json | 2 +- .../components/skybell/translations/de.json | 6 +++ .../components/skybell/translations/en.json | 4 +- .../components/skybell/translations/es.json | 6 +++ .../components/skybell/translations/fr.json | 5 ++ .../components/skybell/translations/id.json | 6 +++ .../components/skybell/translations/it.json | 6 +++ .../components/skybell/translations/ja.json | 6 +++ .../components/skybell/translations/tr.json | 6 +++ .../skybell/translations/zh-Hant.json | 6 +++ .../components/spotify/translations/es.json | 2 +- .../steam_online/translations/es.json | 2 +- .../components/uscis/translations/es.json | 2 +- .../components/xbox/translations/es.json | 2 +- .../xiaomi_miio/translations/select.id.json | 10 ++++ .../xiaomi_miio/translations/select.tr.json | 10 ++++ .../yalexs_ble/translations/tr.json | 31 ++++++++++++ .../components/zha/translations/de.json | 3 ++ .../components/zha/translations/en.json | 3 ++ .../components/zha/translations/es.json | 3 ++ .../components/zha/translations/fr.json | 3 ++ .../components/zha/translations/id.json | 3 ++ .../components/zha/translations/it.json | 3 ++ .../components/zha/translations/ja.json | 3 ++ .../components/zha/translations/tr.json | 3 ++ .../components/zha/translations/zh-Hant.json | 3 ++ 90 files changed, 984 insertions(+), 51 deletions(-) create mode 100644 homeassistant/components/fully_kiosk/translations/id.json create mode 100644 homeassistant/components/fully_kiosk/translations/tr.json create mode 100644 homeassistant/components/lametric/translations/id.json create mode 100644 homeassistant/components/lametric/translations/it.json create mode 100644 homeassistant/components/lametric/translations/tr.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/id.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/it.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/ja.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/no.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/tr.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/zh-Hant.json create mode 100644 homeassistant/components/pushover/translations/de.json create mode 100644 homeassistant/components/pushover/translations/es.json create mode 100644 homeassistant/components/pushover/translations/fr.json create mode 100644 homeassistant/components/pushover/translations/id.json create mode 100644 homeassistant/components/pushover/translations/it.json create mode 100644 homeassistant/components/pushover/translations/ja.json create mode 100644 homeassistant/components/pushover/translations/no.json create mode 100644 homeassistant/components/pushover/translations/tr.json create mode 100644 homeassistant/components/pushover/translations/zh-Hant.json create mode 100644 homeassistant/components/qingping/translations/tr.json create mode 100644 homeassistant/components/schedule/translations/tr.json create mode 100644 homeassistant/components/yalexs_ble/translations/tr.json diff --git a/homeassistant/components/adax/translations/zh-Hant.json b/homeassistant/components/adax/translations/zh-Hant.json index cd99affe40e..89275f8aab7 100644 --- a/homeassistant/components/adax/translations/zh-Hant.json +++ b/homeassistant/components/adax/translations/zh-Hant.json @@ -27,7 +27,7 @@ "data": { "connection_type": "\u9078\u64c7\u9023\u7dda\u985e\u5225" }, - "description": "\u9078\u64c7\u9023\u7dda\u985e\u5225\u3002\u672c\u5730\u7aef\u5c07\u9700\u8981\u5177\u5099\u85cd\u82bd\u52a0\u71b1\u5668" + "description": "\u9078\u64c7\u9023\u7dda\u985e\u5225\u3002\u672c\u5730\u7aef\u5c07\u9700\u8981\u5177\u5099\u85cd\u7259\u52a0\u71b1\u5668" } } } diff --git a/homeassistant/components/ambee/translations/es.json b/homeassistant/components/ambee/translations/es.json index 1bf2f5391ad..fde555ad801 100644 --- a/homeassistant/components/ambee/translations/es.json +++ b/homeassistant/components/ambee/translations/es.json @@ -27,7 +27,7 @@ }, "issues": { "pending_removal": { - "description": "La integraci\u00f3n Ambee est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nLa integraci\u00f3n se elimina porque Ambee elimin\u00f3 sus cuentas gratuitas (limitadas) y ya no proporciona una forma para que los usuarios regulares se registren en un plan pago. \n\nElimina la entrada de la integraci\u00f3n Ambee de tu instancia para solucionar este problema.", + "description": "La integraci\u00f3n Ambee est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nSe va a eliminar la integraci\u00f3n porque Ambee elimin\u00f3 sus cuentas gratuitas (limitadas) y ya no proporciona una forma para que los usuarios regulares se registren en un plan pago. \n\nElimina la entrada de la integraci\u00f3n Ambee de tu instancia para solucionar este problema.", "title": "Se va a eliminar la integraci\u00f3n Ambee" } } diff --git a/homeassistant/components/android_ip_webcam/translations/de.json b/homeassistant/components/android_ip_webcam/translations/de.json index 3fe34f4a259..1de9d79b389 100644 --- a/homeassistant/components/android_ip_webcam/translations/de.json +++ b/homeassistant/components/android_ip_webcam/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "user": { diff --git a/homeassistant/components/android_ip_webcam/translations/es.json b/homeassistant/components/android_ip_webcam/translations/es.json index d004be2aeeb..4d6b55c5238 100644 --- a/homeassistant/components/android_ip_webcam/translations/es.json +++ b/homeassistant/components/android_ip_webcam/translations/es.json @@ -4,7 +4,8 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "No se pudo conectar" + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "user": { @@ -19,7 +20,7 @@ }, "issues": { "deprecated_yaml": { - "description": "Se eliminar\u00e1 la configuraci\u00f3n de la Android IP Webcam mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Android IP Webcam de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se va a eliminar la configuraci\u00f3n de Android IP Webcam mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Android IP Webcam de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la configuraci\u00f3n YAML de la c\u00e1mara web IP de Android" } } diff --git a/homeassistant/components/android_ip_webcam/translations/fr.json b/homeassistant/components/android_ip_webcam/translations/fr.json index 0e83b0feaf7..19f42be4376 100644 --- a/homeassistant/components/android_ip_webcam/translations/fr.json +++ b/homeassistant/components/android_ip_webcam/translations/fr.json @@ -4,7 +4,8 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "cannot_connect": "\u00c9chec de connexion" + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide" }, "step": { "user": { diff --git a/homeassistant/components/android_ip_webcam/translations/id.json b/homeassistant/components/android_ip_webcam/translations/id.json index 430ebe3645f..593fa61dea3 100644 --- a/homeassistant/components/android_ip_webcam/translations/id.json +++ b/homeassistant/components/android_ip_webcam/translations/id.json @@ -4,7 +4,8 @@ "already_configured": "Perangkat sudah dikonfigurasi" }, "error": { - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" }, "step": { "user": { diff --git a/homeassistant/components/android_ip_webcam/translations/it.json b/homeassistant/components/android_ip_webcam/translations/it.json index db0cfc79d84..35ed4267a94 100644 --- a/homeassistant/components/android_ip_webcam/translations/it.json +++ b/homeassistant/components/android_ip_webcam/translations/it.json @@ -4,7 +4,8 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" }, "error": { - "cannot_connect": "Impossibile connettersi" + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" }, "step": { "user": { diff --git a/homeassistant/components/android_ip_webcam/translations/ja.json b/homeassistant/components/android_ip_webcam/translations/ja.json index 519edb51609..beb3f387d64 100644 --- a/homeassistant/components/android_ip_webcam/translations/ja.json +++ b/homeassistant/components/android_ip_webcam/translations/ja.json @@ -4,7 +4,8 @@ "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" }, "error": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { "user": { diff --git a/homeassistant/components/android_ip_webcam/translations/tr.json b/homeassistant/components/android_ip_webcam/translations/tr.json index cac43d82490..efd14cf5b21 100644 --- a/homeassistant/components/android_ip_webcam/translations/tr.json +++ b/homeassistant/components/android_ip_webcam/translations/tr.json @@ -4,7 +4,8 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, "step": { "user": { diff --git a/homeassistant/components/android_ip_webcam/translations/zh-Hant.json b/homeassistant/components/android_ip_webcam/translations/zh-Hant.json index 523c5a8b0b3..a8170bd2a7d 100644 --- a/homeassistant/components/android_ip_webcam/translations/zh-Hant.json +++ b/homeassistant/components/android_ip_webcam/translations/zh-Hant.json @@ -4,7 +4,8 @@ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557" + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { "user": { diff --git a/homeassistant/components/awair/translations/id.json b/homeassistant/components/awair/translations/id.json index 39b63bef7e5..3d633070f20 100644 --- a/homeassistant/components/awair/translations/id.json +++ b/homeassistant/components/awair/translations/id.json @@ -29,7 +29,7 @@ "data": { "host": "Alamat IP" }, - "description": "API Awair Local harus diaktifkan dengan mengikuti langkah-langkah berikut: {url}" + "description": "Ikuti [petunjuk ini]( {url} ) untuk mengaktifkan API Lokal Awair. \n\n Klik kirim setelah selesai." }, "local_pick": { "data": { diff --git a/homeassistant/components/awair/translations/tr.json b/homeassistant/components/awair/translations/tr.json index ef34620b5f7..8d49f985eae 100644 --- a/homeassistant/components/awair/translations/tr.json +++ b/homeassistant/components/awair/translations/tr.json @@ -2,14 +2,41 @@ "config": { "abort": { "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_configured_account": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "no_devices_found": "A\u011fda cihaz bulunamad\u0131", - "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "unreachable": "Ba\u011flanma hatas\u0131" }, "error": { "invalid_access_token": "Ge\u00e7ersiz eri\u015fim anahtar\u0131", - "unknown": "Beklenmeyen hata" + "unknown": "Beklenmeyen hata", + "unreachable": "Ba\u011flanma hatas\u0131" }, + "flow_title": "{model} ({device_id})", "step": { + "cloud": { + "data": { + "access_token": "Eri\u015fim Anahtar\u0131", + "email": "E-posta" + }, + "description": "Bir Awair geli\u015ftirici eri\u015fim belirtecine \u015fu adresten kaydolmal\u0131s\u0131n\u0131z: {url}" + }, + "discovery_confirm": { + "description": "{model} ( {device_id} ) kurulumunu yapmak istiyor musunuz?" + }, + "local": { + "data": { + "host": "IP Adresi" + }, + "description": "Awair Yerel API'sinin nas\u0131l etkinle\u015ftirilece\u011fiyle ilgili [bu talimatlar\u0131]( {url} ) uygulay\u0131n. \n\n \u0130\u015finiz bitti\u011finde g\u00f6nder'i t\u0131klay\u0131n." + }, + "local_pick": { + "data": { + "device": "Cihaz", + "host": "IP Adresi" + } + }, "reauth": { "data": { "access_token": "Eri\u015fim Anahtar\u0131", @@ -29,7 +56,11 @@ "access_token": "Eri\u015fim Anahtar\u0131", "email": "E-posta" }, - "description": "Awair geli\u015ftirici eri\u015fim belirteci i\u00e7in \u015fu adresten kaydolmal\u0131s\u0131n\u0131z: https://developer.getawair.com/onboard/login" + "description": "Awair geli\u015ftirici eri\u015fim belirteci i\u00e7in \u015fu adresten kaydolmal\u0131s\u0131n\u0131z: https://developer.getawair.com/onboard/login", + "menu_options": { + "cloud": "Bulut \u00fczerinden ba\u011flan\u0131n", + "local": "Yerel olarak ba\u011flan (tercih edilen)" + } } } } diff --git a/homeassistant/components/bluetooth/translations/de.json b/homeassistant/components/bluetooth/translations/de.json index 6e65b985478..50723328c4a 100644 --- a/homeassistant/components/bluetooth/translations/de.json +++ b/homeassistant/components/bluetooth/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Der Dienst ist bereits konfiguriert", - "no_adapters": "Keine Bluetooth-Adapter gefunden" + "no_adapters": "Keine unkonfigurierten Bluetooth-Adapter gefunden" }, "flow_title": "{name}", "step": { @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "M\u00f6chtest du Bluetooth einrichten?" }, + "multiple_adapters": { + "data": { + "adapter": "Adapter" + }, + "description": "W\u00e4hle einen Bluetooth-Adapter zum Einrichten aus" + }, + "single_adapter": { + "description": "M\u00f6chtest du den Bluetooth-Adapter {name} einrichten?" + }, "user": { "data": { "address": "Ger\u00e4t" diff --git a/homeassistant/components/bluetooth/translations/en.json b/homeassistant/components/bluetooth/translations/en.json index ac80cfb620e..5b40308cd3c 100644 --- a/homeassistant/components/bluetooth/translations/en.json +++ b/homeassistant/components/bluetooth/translations/en.json @@ -9,6 +9,9 @@ "bluetooth_confirm": { "description": "Do you want to setup {name}?" }, + "enable_bluetooth": { + "description": "Do you want to setup Bluetooth?" + }, "multiple_adapters": { "data": { "adapter": "Adapter" @@ -25,5 +28,14 @@ "description": "Choose a device to setup" } } + }, + "options": { + "step": { + "init": { + "data": { + "adapter": "The Bluetooth Adapter to use for scanning" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/es.json b/homeassistant/components/bluetooth/translations/es.json index 1dc8669a067..170f44e446f 100644 --- a/homeassistant/components/bluetooth/translations/es.json +++ b/homeassistant/components/bluetooth/translations/es.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El servicio ya est\u00e1 configurado", - "no_adapters": "No se encontraron adaptadores Bluetooth" + "no_adapters": "No se encontraron adaptadores Bluetooth no configurados" }, "flow_title": "{name}", "step": { @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "\u00bfQuieres configurar Bluetooth?" }, + "multiple_adapters": { + "data": { + "adapter": "Adaptador" + }, + "description": "Selecciona un adaptador Bluetooth para configurar" + }, + "single_adapter": { + "description": "\u00bfQuieres configurar el adaptador Bluetooth {name}?" + }, "user": { "data": { "address": "Dispositivo" diff --git a/homeassistant/components/bluetooth/translations/id.json b/homeassistant/components/bluetooth/translations/id.json index 3fc2d6a7623..a0ce38665b7 100644 --- a/homeassistant/components/bluetooth/translations/id.json +++ b/homeassistant/components/bluetooth/translations/id.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Layanan sudah dikonfigurasi", - "no_adapters": "Tidak ada adaptor Bluetooth yang ditemukan" + "no_adapters": "Tidak ada adaptor Bluetooth yang belum dikonfigurasi yang ditemukan" }, "flow_title": "{name}", "step": { @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "Ingin menyiapkan Bluetooth?" }, + "multiple_adapters": { + "data": { + "adapter": "Adaptor" + }, + "description": "Pilih adaptor Bluetooth untuk disiapkan" + }, + "single_adapter": { + "description": "Ingin menyiapkan adaptor Bluetooth {name}?" + }, "user": { "data": { "address": "Perangkat" diff --git a/homeassistant/components/bluetooth/translations/it.json b/homeassistant/components/bluetooth/translations/it.json index 86809b41a7d..fd03835ebce 100644 --- a/homeassistant/components/bluetooth/translations/it.json +++ b/homeassistant/components/bluetooth/translations/it.json @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "Vuoi configurare il Bluetooth?" }, + "multiple_adapters": { + "data": { + "adapter": "Adattatore" + }, + "description": "Seleziona un adattatore Bluetooth da configurare" + }, + "single_adapter": { + "description": "Vuoi configurare l'adattatore Bluetooth {name}?" + }, "user": { "data": { "address": "Dispositivo" diff --git a/homeassistant/components/bluetooth/translations/ja.json b/homeassistant/components/bluetooth/translations/ja.json index b3f6b794ee9..e7588f378af 100644 --- a/homeassistant/components/bluetooth/translations/ja.json +++ b/homeassistant/components/bluetooth/translations/ja.json @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "Bluetooth\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" }, + "multiple_adapters": { + "data": { + "adapter": "\u30a2\u30c0\u30d7\u30bf\u30fc" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u3001Bluetooth\u30a2\u30c0\u30d7\u30bf\u30fc\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + }, + "single_adapter": { + "description": "Bluetooth\u30a2\u30c0\u30d7\u30bf\u30fc {name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, "user": { "data": { "address": "\u30c7\u30d0\u30a4\u30b9" diff --git a/homeassistant/components/bluetooth/translations/no.json b/homeassistant/components/bluetooth/translations/no.json index fbc59772d6f..8e82e77216c 100644 --- a/homeassistant/components/bluetooth/translations/no.json +++ b/homeassistant/components/bluetooth/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Tjenesten er allerede konfigurert", - "no_adapters": "Finner ingen Bluetooth-adaptere" + "no_adapters": "Fant ingen ukonfigurerte Bluetooth-adaptere" }, "flow_title": "{name}", "step": { @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "Vil du konfigurere Bluetooth?" }, + "multiple_adapters": { + "data": { + "adapter": "Adapter" + }, + "description": "Velg en Bluetooth-adapter for \u00e5 konfigurere" + }, + "single_adapter": { + "description": "Vil du konfigurere Bluetooth-adapteren {name} ?" + }, "user": { "data": { "address": "Enhet" diff --git a/homeassistant/components/bluetooth/translations/pt-BR.json b/homeassistant/components/bluetooth/translations/pt-BR.json index 0a5cf354495..ec69310dc40 100644 --- a/homeassistant/components/bluetooth/translations/pt-BR.json +++ b/homeassistant/components/bluetooth/translations/pt-BR.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", - "no_adapters": "Nenhum adaptador Bluetooth encontrado" + "no_adapters": "N\u00e3o foram encontrados adaptadores Bluetooth n\u00e3o configurados" }, "flow_title": "{name}", "step": { @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "Deseja configurar o Bluetooth?" }, + "multiple_adapters": { + "data": { + "adapter": "Adaptador" + }, + "description": "Selecione um adaptador Bluetooth para configurar" + }, + "single_adapter": { + "description": "Deseja configurar o adaptador Bluetooth {name}?" + }, "user": { "data": { "address": "Dispositivo" diff --git a/homeassistant/components/bluetooth/translations/tr.json b/homeassistant/components/bluetooth/translations/tr.json index a464d65dd93..e2286fcd122 100644 --- a/homeassistant/components/bluetooth/translations/tr.json +++ b/homeassistant/components/bluetooth/translations/tr.json @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "Bluetooth'u kurmak istiyor musunuz?" }, + "multiple_adapters": { + "data": { + "adapter": "Adapt\u00f6r" + }, + "description": "Kurulum i\u00e7in bir Bluetooth adapt\u00f6r\u00fc se\u00e7in" + }, + "single_adapter": { + "description": "{name} Bluetooth adapt\u00f6r\u00fcn\u00fc kurmak istiyor musunuz?" + }, "user": { "data": { "address": "Cihaz" diff --git a/homeassistant/components/bluetooth/translations/zh-Hant.json b/homeassistant/components/bluetooth/translations/zh-Hant.json index 34ab75775ab..8dea681a178 100644 --- a/homeassistant/components/bluetooth/translations/zh-Hant.json +++ b/homeassistant/components/bluetooth/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "no_adapters": "\u627e\u4e0d\u5230\u4efb\u4f55\u85cd\u82bd\u50b3\u8f38\u5668" + "no_adapters": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u8a2d\u5b9a\u85cd\u7259\u50b3\u8f38\u5668" }, "flow_title": "{name}", "step": { @@ -10,7 +10,16 @@ "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, "enable_bluetooth": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u85cd\u82bd\uff1f" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u85cd\u7259\uff1f" + }, + "multiple_adapters": { + "data": { + "adapter": "\u50b3\u8f38\u5668" + }, + "description": "\u9078\u64c7\u9032\u884c\u8a2d\u5b9a\u7684\u85cd\u7259\u50b3\u8f38\u5668" + }, + "single_adapter": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u85cd\u7259\u50b3\u8f38\u5668 {name}\uff1f" }, "user": { "data": { @@ -24,7 +33,7 @@ "step": { "init": { "data": { - "adapter": "\u7528\u4ee5\u9032\u884c\u5075\u6e2c\u7684\u85cd\u82bd\u50b3\u8f38\u5668" + "adapter": "\u7528\u4ee5\u9032\u884c\u5075\u6e2c\u7684\u85cd\u7259\u50b3\u8f38\u5668" } } } diff --git a/homeassistant/components/deutsche_bahn/translations/es.json b/homeassistant/components/deutsche_bahn/translations/es.json index 2572474f2bc..6d8b849c11b 100644 --- a/homeassistant/components/deutsche_bahn/translations/es.json +++ b/homeassistant/components/deutsche_bahn/translations/es.json @@ -1,7 +1,7 @@ { "issues": { "pending_removal": { - "description": "La integraci\u00f3n Deutsche Bahn est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.11. \n\nLa integraci\u00f3n se elimina porque se basa en webscraping, algo que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de Deutsche Bahn de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "La integraci\u00f3n Deutsche Bahn est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.11. \n\nSe va a eliminar la integraci\u00f3n porque se basa en webscraping, algo que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de Deutsche Bahn de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la integraci\u00f3n Deutsche Bahn" } } diff --git a/homeassistant/components/fully_kiosk/translations/id.json b/homeassistant/components/fully_kiosk/translations/id.json new file mode 100644 index 00000000000..d9be1351db4 --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fully_kiosk/translations/tr.json b/homeassistant/components/fully_kiosk/translations/tr.json new file mode 100644 index 00000000000..5d1e2c90e1b --- /dev/null +++ b/homeassistant/components/fully_kiosk/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Sunucu", + "password": "Parola" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/translations/es.json b/homeassistant/components/google/translations/es.json index 4b36c5006c0..401a1f37b94 100644 --- a/homeassistant/components/google/translations/es.json +++ b/homeassistant/components/google/translations/es.json @@ -35,7 +35,7 @@ }, "issues": { "deprecated_yaml": { - "description": "La configuraci\u00f3n de Google Calendar en configuration.yaml se eliminar\u00e1 en Home Assistant 2022.9. \n\nTs credenciales OAuth de aplicaci\u00f3n existentes y la configuraci\u00f3n de acceso se han importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se va a eliminar la configuraci\u00f3n de Google Calendar en configuration.yaml en Home Assistant 2022.9. \n\nTus credenciales OAuth de aplicaci\u00f3n existentes y la configuraci\u00f3n de acceso se han importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la configuraci\u00f3n YAML de Google Calendar" }, "removed_track_new_yaml": { diff --git a/homeassistant/components/guardian/translations/es.json b/homeassistant/components/guardian/translations/es.json index 8df17ed3ff3..93bccfbd03d 100644 --- a/homeassistant/components/guardian/translations/es.json +++ b/homeassistant/components/guardian/translations/es.json @@ -24,11 +24,11 @@ "step": { "confirm": { "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con un ID de entidad de destino de `{alternate_target}`. Luego, haz clic en ENVIAR a continuaci\u00f3n para marcar este problema como resuelto.", - "title": "El servicio {deprecated_service} ser\u00e1 eliminado" + "title": "Se va a eliminar el servicio {deprecated_service}" } } }, - "title": "El servicio {deprecated_service} ser\u00e1 eliminado" + "title": "Se va a eliminar el servicio {deprecated_service}" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/tr.json b/homeassistant/components/guardian/translations/tr.json index fe4dffad3aa..e5de0cb73cd 100644 --- a/homeassistant/components/guardian/translations/tr.json +++ b/homeassistant/components/guardian/translations/tr.json @@ -17,5 +17,18 @@ "description": "Yerel bir Elexa Guardian cihaz\u0131 yap\u0131land\u0131r\u0131n." } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Bu hizmeti kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131, bunun yerine \" {alternate_service} {alternate_target} hizmetini kullanacak \u015fekilde g\u00fcncelleyin. Ard\u0131ndan, bu sorunu \u00e7\u00f6z\u00fcld\u00fc olarak i\u015faretlemek i\u00e7in a\u015fa\u011f\u0131daki G\u00d6NDER'i t\u0131klay\u0131n.", + "title": "{deprecated_service} hizmeti kald\u0131r\u0131l\u0131yor" + } + } + }, + "title": "{deprecated_service} hizmeti kald\u0131r\u0131l\u0131yor" + } } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/sensor.id.json b/homeassistant/components/homekit_controller/translations/sensor.id.json index 5697598ef54..feb6a4f869b 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.id.json +++ b/homeassistant/components/homekit_controller/translations/sensor.id.json @@ -1,6 +1,8 @@ { "state": { "homekit_controller__thread_node_capabilities": { + "full": "Perangkat Akhir Lengkap", + "minimal": "Perangkat Akhir Minimal", "none": "Tidak Ada" }, "homekit_controller__thread_status": { diff --git a/homeassistant/components/hue/translations/id.json b/homeassistant/components/hue/translations/id.json index 0b81e1093df..db37c4a60e3 100644 --- a/homeassistant/components/hue/translations/id.json +++ b/homeassistant/components/hue/translations/id.json @@ -63,7 +63,7 @@ "remote_double_button_long_press": "Kedua \"{subtype}\" dilepaskan setelah ditekan lama", "remote_double_button_short_press": "Kedua \"{subtype}\" dilepas", "repeat": "\"{subtype}\" ditekan terus", - "short_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan sebentar", + "short_release": "\"{subtype}\" dilepaskan setelah ditekan sebentar", "start": "\"{subtype}\" awalnya ditekan" } }, diff --git a/homeassistant/components/hue/translations/tr.json b/homeassistant/components/hue/translations/tr.json index 32ab9bb1887..df6d4e247a9 100644 --- a/homeassistant/components/hue/translations/tr.json +++ b/homeassistant/components/hue/translations/tr.json @@ -44,6 +44,8 @@ "button_2": "\u0130kinci d\u00fc\u011fme", "button_3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme", "button_4": "D\u00f6rd\u00fcnc\u00fc d\u00fc\u011fme", + "clock_wise": "Saat y\u00f6n\u00fcnde d\u00f6n\u00fc\u015f", + "counter_clock_wise": "Saat y\u00f6n\u00fcn\u00fcn tersine d\u00f6n\u00fc\u015f", "dim_down": "K\u0131sma", "dim_up": "A\u00e7ma", "double_buttons_1_3": "Birinci ve \u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fmeler", @@ -61,7 +63,8 @@ "remote_double_button_long_press": "Her iki \" {subtype} \" uzun bas\u0131\u015ftan sonra b\u0131rak\u0131ld\u0131", "remote_double_button_short_press": "Her iki \"{subtype}\" de b\u0131rak\u0131ld\u0131", "repeat": "\" {subtype} \" d\u00fc\u011fmesi bas\u0131l\u0131 tutuldu", - "short_release": "K\u0131sa bas\u0131ld\u0131ktan sonra \"{subtype}\" d\u00fc\u011fmesi b\u0131rak\u0131ld\u0131" + "short_release": "K\u0131sa bas\u0131ld\u0131ktan sonra \"{subtype}\" d\u00fc\u011fmesi b\u0131rak\u0131ld\u0131", + "start": "\" {subtype} \" ba\u015flang\u0131\u00e7ta bas\u0131ld\u0131" } }, "options": { diff --git a/homeassistant/components/lacrosse_view/translations/id.json b/homeassistant/components/lacrosse_view/translations/id.json index d244ba002b8..0f9446e354d 100644 --- a/homeassistant/components/lacrosse_view/translations/id.json +++ b/homeassistant/components/lacrosse_view/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Perangkat sudah dikonfigurasi" + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "invalid_auth": "Autentikasi tidak valid", diff --git a/homeassistant/components/lacrosse_view/translations/it.json b/homeassistant/components/lacrosse_view/translations/it.json index 9ce6c75dcbc..efa81bf75cd 100644 --- a/homeassistant/components/lacrosse_view/translations/it.json +++ b/homeassistant/components/lacrosse_view/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "invalid_auth": "Autenticazione non valida", diff --git a/homeassistant/components/lacrosse_view/translations/ja.json b/homeassistant/components/lacrosse_view/translations/ja.json index 6b058f78cdb..c4cca751722 100644 --- a/homeassistant/components/lacrosse_view/translations/ja.json +++ b/homeassistant/components/lacrosse_view/translations/ja.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c", diff --git a/homeassistant/components/lacrosse_view/translations/no.json b/homeassistant/components/lacrosse_view/translations/no.json index 9cef140da52..cd512e6fb86 100644 --- a/homeassistant/components/lacrosse_view/translations/no.json +++ b/homeassistant/components/lacrosse_view/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Enheten er allerede konfigurert" + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning", diff --git a/homeassistant/components/lacrosse_view/translations/tr.json b/homeassistant/components/lacrosse_view/translations/tr.json index cf82d698150..cff5bd2e3e8 100644 --- a/homeassistant/components/lacrosse_view/translations/tr.json +++ b/homeassistant/components/lacrosse_view/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", diff --git a/homeassistant/components/lacrosse_view/translations/zh-Hant.json b/homeassistant/components/lacrosse_view/translations/zh-Hant.json index 78235452297..a7ecf20d04e 100644 --- a/homeassistant/components/lacrosse_view/translations/zh-Hant.json +++ b/homeassistant/components/lacrosse_view/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", diff --git a/homeassistant/components/lametric/translations/id.json b/homeassistant/components/lametric/translations/id.json new file mode 100644 index 00000000000..69f776e636c --- /dev/null +++ b/homeassistant/components/lametric/translations/id.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "authorize_url_timeout": "Tenggang waktu pembuatan URL otorisasi habis.", + "invalid_discovery_info": "Informasi penemuan yang tidak valid diterima", + "link_local_address": "Tautan alamat lokal tidak didukung", + "missing_configuration": "Integrasi LaMetric tidak dikonfigurasi. Silakan ikuti dokumentasi.", + "no_devices": "Pengguna yang diotorisasi tidak memiliki perangkat LaMetric", + "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "Perangkat LaMetric bisa disiapkan di Home Assistant dengan dua cara berbeda.\n\nAnda dapat memasukkan sendiri semua informasi perangkat dan token API, atau Home Asssistant dapat mengimpornya dari akun LaMetric.com Anda.", + "menu_options": { + "manual_entry": "Masukkan secara manual", + "pick_implementation": "Impor dari LaMetric.com (direkomendasikan)" + } + }, + "manual_entry": { + "data": { + "api_key": "Kunci API", + "host": "Host" + }, + "data_description": { + "api_key": "Anda dapat menemukan kunci API ini di [halaman perangkat di akun pengembang LaMetric Anda](https://developer.lametric.com/user/devices).", + "host": "Alamat IP atau nama host LaMetric TIME di jaringan Anda." + } + }, + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + }, + "user_cloud_select_device": { + "data": { + "device": "Pilih perangkat LaMetric untuk ditambahkan" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "Integrasi LaMetric telah dimodernisasi: kini integrasinya dikonfigurasi dan disiapkan melalui antarmuka pengguna dan komunikasi menjadi lokal.\n\nSayangnya, tidak ada jalur migrasi otomatis yang mungkin dan oleh sebab itu Anda harus mengatur ulang integrasi LaMetric dengan Home Assistant. Baca dokumentasi integrasi LaMetric Home Assistant tentang cara persiapannya.\n\nHapus konfigurasi YAML LaMetric yang lama dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Migrasi manual diperlukan untuk LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/it.json b/homeassistant/components/lametric/translations/it.json new file mode 100644 index 00000000000..61159f16def --- /dev/null +++ b/homeassistant/components/lametric/translations/it.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", + "invalid_discovery_info": "Informazioni di rilevamento non valide ricevute", + "link_local_address": "Gli indirizzi locali di collegamento non sono supportati", + "missing_configuration": "L'integrazione LaMetric non \u00e8 configurata. Segui la documentazione.", + "no_devices": "L'utente autorizzato non dispone di dispositivi LaMetric", + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "Un dispositivo LaMetric pu\u00f2 essere configurato in Home Assistant in due modi diversi. \n\nPuoi inserire tu stesso tutte le informazioni sul dispositivo e i token API oppure Home Assistant pu\u00f2 importarli dal tuo account LaMetric.com.", + "menu_options": { + "manual_entry": "Inserisci manualmente", + "pick_implementation": "Importa da LaMetric.com (consigliato)" + } + }, + "manual_entry": { + "data": { + "api_key": "Chiave API", + "host": "Host" + } + }, + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + }, + "user_cloud_select_device": { + "data": { + "device": "Seleziona il dispositivo LaMetric da aggiungere." + } + } + } + }, + "issues": { + "manual_migration": { + "description": "L'integrazione LaMetric \u00e8 stata modernizzata: viene ora configurata e inizializzata tramite l'interfaccia utente e le comunicazioni sono ora locali. \n\nSfortunatamente, la migrazione automatica non \u00e8 disponibile, quindi \u00e8 necessario configurare nuovamente LaMetric con Home Assistant. Consulta la documentazione di Home Assistant sull'integrazione LaMetric per sapere come configurarlo. \n\nRimuovere la vecchia configurazione YAML di LaMetric dal file configuration.yaml e riavviare Home Assistant per risolvere questo problema.", + "title": "Migrazione manuale richiesta per LaMetric" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/ja.json b/homeassistant/components/lametric/translations/ja.json index 242a43bddb7..4f6768ca80b 100644 --- a/homeassistant/components/lametric/translations/ja.json +++ b/homeassistant/components/lametric/translations/ja.json @@ -15,6 +15,7 @@ }, "step": { "choice_enter_manual_or_fetch_cloud": { + "description": "LaMetric\u30c7\u30d0\u30a4\u30b9\u306f\u3001Home Assistant\u30672\u3064\u306e\u7570\u306a\u308b\u65b9\u6cd5\u3067\u8a2d\u5b9a\u3067\u304d\u307e\u3059\u3002\n\n\u3059\u3079\u3066\u306e\u30c7\u30d0\u30a4\u30b9\u60c5\u5831\u3068API\u30c8\u30fc\u30af\u30f3\u3092\u81ea\u5206\u3067\u5165\u529b\u3059\u308b\u3053\u3068\u3084\u3001LaMetric.com\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u304b\u3089Home Asssistant\u306b\u30a4\u30f3\u30dd\u30fc\u30c8\u3059\u308b\u3053\u3068\u3082\u3067\u304d\u307e\u3059\u3002", "menu_options": { "manual_entry": "\u624b\u52d5\u3067\u5165\u529b", "pick_implementation": "LaMetric.com\u304b\u3089\u306e\u30a4\u30f3\u30dd\u30fc\u30c8((\u63a8\u5968)" diff --git a/homeassistant/components/lametric/translations/tr.json b/homeassistant/components/lametric/translations/tr.json new file mode 100644 index 00000000000..1801d6aac08 --- /dev/null +++ b/homeassistant/components/lametric/translations/tr.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "authorize_url_timeout": "Yetkilendirme URL'si olu\u015ftururken zaman a\u015f\u0131m\u0131.", + "invalid_discovery_info": "Ge\u00e7ersiz ke\u015fif bilgisi al\u0131nd\u0131", + "link_local_address": "Ba\u011flant\u0131 yerel adresleri desteklenmiyor", + "missing_configuration": "LaMetric entegrasyonu yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "no_devices": "Yetkili kullan\u0131c\u0131n\u0131n LaMetric cihaz\u0131 yok", + "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "choice_enter_manual_or_fetch_cloud": { + "description": "Bir LaMetric cihaz\u0131, Home Assistant'ta iki farkl\u0131 \u015fekilde kurulabilir. \n\n T\u00fcm cihaz bilgilerini ve API belirte\u00e7lerini kendiniz girebilirsiniz veya Home Assistant bunlar\u0131 LaMetric.com hesab\u0131n\u0131zdan i\u00e7e aktarabilir.", + "menu_options": { + "manual_entry": "Manuel olarak giriniz", + "pick_implementation": "LaMetric.com'dan i\u00e7e aktar (\u00f6nerilir)" + } + }, + "manual_entry": { + "data": { + "api_key": "API Anahtar\u0131", + "host": "Sunucu" + }, + "data_description": { + "api_key": "Bu API anahtar\u0131n\u0131 [LaMetric geli\u015ftirici hesab\u0131n\u0131zdaki cihazlar sayfas\u0131nda] (https://developer.lametric.com/user/devices) bulabilirsiniz.", + "host": "A\u011f\u0131n\u0131zdaki LaMetric TIME'\u0131n\u0131z\u0131n IP adresi veya ana bilgisayar ad\u0131." + } + }, + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + }, + "user_cloud_select_device": { + "data": { + "device": "Eklenecek LaMetric cihaz\u0131n\u0131 se\u00e7in" + } + } + } + }, + "issues": { + "manual_migration": { + "description": "LaMetric entegrasyonu modernle\u015ftirildi: Art\u0131k kullan\u0131c\u0131 aray\u00fcz\u00fc \u00fczerinden yap\u0131land\u0131r\u0131ld\u0131 ve kuruldu ve ileti\u015fimler art\u0131k yerel. \n\n Maalesef otomatik ge\u00e7i\u015f yolu m\u00fcmk\u00fcn de\u011fildir ve bu nedenle LaMetric'inizi Home Assistant ile yeniden kurman\u0131z\u0131 gerektirir. L\u00fctfen nas\u0131l kurulaca\u011f\u0131na ili\u015fkin Home Assistant LaMetric entegrasyon belgelerine bak\u0131n. \n\n Bu sorunu d\u00fczeltmek i\u00e7in eski LaMetric YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "LaMetric i\u00e7in manuel ge\u00e7i\u015f gerekli" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/id.json b/homeassistant/components/landisgyr_heat_meter/translations/id.json new file mode 100644 index 00000000000..97bb43eb4ba --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "Jalur Perangkat USB" + } + }, + "user": { + "data": { + "device": "Pilih perangkat" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/it.json b/homeassistant/components/landisgyr_heat_meter/translations/it.json new file mode 100644 index 00000000000..1c320671a40 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "Percorso del dispositivo USB" + } + }, + "user": { + "data": { + "device": "Seleziona il dispositivo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/ja.json b/homeassistant/components/landisgyr_heat_meter/translations/ja.json new file mode 100644 index 00000000000..e05d6d2dffa --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/ja.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "USB\u30c7\u30d0\u30a4\u30b9\u306e\u30d1\u30b9" + } + }, + "user": { + "data": { + "device": "\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/no.json b/homeassistant/components/landisgyr_heat_meter/translations/no.json new file mode 100644 index 00000000000..7c3a6e348f3 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "USB enhetsbane" + } + }, + "user": { + "data": { + "device": "Velg enhet" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/tr.json b/homeassistant/components/landisgyr_heat_meter/translations/tr.json new file mode 100644 index 00000000000..1ff9d1c85d0 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "USB Cihaz Yolu" + } + }, + "user": { + "data": { + "device": "Cihaz se\u00e7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/landisgyr_heat_meter/translations/zh-Hant.json b/homeassistant/components/landisgyr_heat_meter/translations/zh-Hant.json new file mode 100644 index 00000000000..718576d8a26 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "USB \u88dd\u7f6e\u8def\u5f91" + } + }, + "user": { + "data": { + "device": "\u9078\u64c7\u88dd\u7f6e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/es.json b/homeassistant/components/lyric/translations/es.json index aaddd58b433..e4bd36ad952 100644 --- a/homeassistant/components/lyric/translations/es.json +++ b/homeassistant/components/lyric/translations/es.json @@ -20,8 +20,8 @@ }, "issues": { "removed_yaml": { - "description": "Se elimin\u00f3 la configuraci\u00f3n de Honeywell Lyric mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", - "title": "Se elimin\u00f3 la configuraci\u00f3n YAML de Honeywell Lyric" + "description": "Se ha eliminado la configuraci\u00f3n de Honeywell Lyric mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se ha eliminado la configuraci\u00f3n YAML de Honeywell Lyric" } } } \ No newline at end of file diff --git a/homeassistant/components/miflora/translations/zh-Hant.json b/homeassistant/components/miflora/translations/zh-Hant.json index e6af26efcb9..b90bc751176 100644 --- a/homeassistant/components/miflora/translations/zh-Hant.json +++ b/homeassistant/components/miflora/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "issues": { "replaced": { - "description": "\u5c0f\u7c73\u82b1\u82b1\u8349\u8349\u6574\u5408\u5df2\u7d93\u65bc Home Assistant 2022.7 \u4e2d\u505c\u6b62\u904b\u4f5c\u3001\u4e26\u65bc 2022.8 \u7248\u4e2d\u4ee5\u5c0f\u7c73\u85cd\u82bd\u6574\u5408\u9032\u884c\u53d6\u4ee3\u3002\n\n\u7531\u65bc\u6c92\u6709\u81ea\u52d5\u8f49\u79fb\u7684\u65b9\u5f0f\uff0c\u56e0\u6b64\u60a8\u5fc5\u9808\u624b\u52d5\u65bc\u6574\u5408\u4e2d\u65b0\u589e\u5c0f\u7c73\u82b1\u82b1\u8349\u8349\u88dd\u7f6e\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u65e2\u6709\u7684\u5c0f\u7c73\u82b1\u82b1\u8349\u8349 YAML \u8a2d\u5b9a\uff0c\u8acb\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "description": "\u5c0f\u7c73\u82b1\u82b1\u8349\u8349\u6574\u5408\u5df2\u7d93\u65bc Home Assistant 2022.7 \u4e2d\u505c\u6b62\u904b\u4f5c\u3001\u4e26\u65bc 2022.8 \u7248\u4e2d\u4ee5\u5c0f\u7c73\u85cd\u7259\u6574\u5408\u9032\u884c\u53d6\u4ee3\u3002\n\n\u7531\u65bc\u6c92\u6709\u81ea\u52d5\u8f49\u79fb\u7684\u65b9\u5f0f\uff0c\u56e0\u6b64\u60a8\u5fc5\u9808\u624b\u52d5\u65bc\u6574\u5408\u4e2d\u65b0\u589e\u5c0f\u7c73\u82b1\u82b1\u8349\u8349\u88dd\u7f6e\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u65e2\u6709\u7684\u5c0f\u7c73\u82b1\u82b1\u8349\u8349 YAML \u8a2d\u5b9a\uff0c\u8acb\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", "title": "\u5c0f\u7c73\u82b1\u82b1\u8349\u8349\u6574\u5408\u5df2\u88ab\u53d6\u4ee3" } } diff --git a/homeassistant/components/mitemp_bt/translations/zh-Hant.json b/homeassistant/components/mitemp_bt/translations/zh-Hant.json index 05799bbd712..a6e4805f0eb 100644 --- a/homeassistant/components/mitemp_bt/translations/zh-Hant.json +++ b/homeassistant/components/mitemp_bt/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "issues": { "replaced": { - "description": "\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668\u6574\u5408\u5df2\u7d93\u65bc Home Assistant 2022.7 \u4e2d\u505c\u6b62\u904b\u4f5c\u3001\u4e26\u65bc 2022.8 \u7248\u4e2d\u4ee5\u5c0f\u7c73\u85cd\u82bd\u6574\u5408\u9032\u884c\u53d6\u4ee3\u3002\n\n\u7531\u65bc\u6c92\u6709\u81ea\u52d5\u8f49\u79fb\u7684\u65b9\u5f0f\uff0c\u56e0\u6b64\u60a8\u5fc5\u9808\u624b\u52d5\u65bc\u6574\u5408\u4e2d\u65b0\u589e\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u88dd\u7f6e\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u65e2\u6709\u7684\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668 YAML \u8a2d\u5b9a\uff0c\u8acb\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "\u5c0f\u7c73\u7c73\u5bb6\u85cd\u82bd\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668\u6574\u5408\u5df2\u88ab\u53d6\u4ee3" + "description": "\u5c0f\u7c73\u7c73\u5bb6\u85cd\u7259\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668\u6574\u5408\u5df2\u7d93\u65bc Home Assistant 2022.7 \u4e2d\u505c\u6b62\u904b\u4f5c\u3001\u4e26\u65bc 2022.8 \u7248\u4e2d\u4ee5\u5c0f\u7c73\u85cd\u7259\u6574\u5408\u9032\u884c\u53d6\u4ee3\u3002\n\n\u7531\u65bc\u6c92\u6709\u81ea\u52d5\u8f49\u79fb\u7684\u65b9\u5f0f\uff0c\u56e0\u6b64\u60a8\u5fc5\u9808\u624b\u52d5\u65bc\u6574\u5408\u4e2d\u65b0\u589e\u5c0f\u7c73\u7c73\u5bb6\u85cd\u7259\u88dd\u7f6e\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u65e2\u6709\u7684\u5c0f\u7c73\u7c73\u5bb6\u85cd\u7259\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668 YAML \u8a2d\u5b9a\uff0c\u8acb\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "\u5c0f\u7c73\u7c73\u5bb6\u85cd\u7259\u6eab\u6fd5\u5ea6\u8a08\u611f\u6e2c\u5668\u6574\u5408\u5df2\u88ab\u53d6\u4ee3" } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index 535482b74da..7c7c8444479 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -99,7 +99,7 @@ }, "issues": { "deprecated_yaml": { - "description": "La configuraci\u00f3n de Nest en configuration.yaml se eliminar\u00e1 en Home Assistant 2022.10. \n\nTus credenciales OAuth de aplicaci\u00f3n existentes y la configuraci\u00f3n de acceso se han importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se va a eliminar la configuraci\u00f3n de Nest en configuration.yaml en Home Assistant 2022.10. \n\nTus credenciales OAuth de aplicaci\u00f3n existentes y la configuraci\u00f3n de acceso se han importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la configuraci\u00f3n YAML de Nest" }, "removed_app_auth": { diff --git a/homeassistant/components/pushover/translations/de.json b/homeassistant/components/pushover/translations/de.json new file mode 100644 index 00000000000..9a5aa0cb571 --- /dev/null +++ b/homeassistant/components/pushover/translations/de.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "invalid_user_key": "Ung\u00fcltiger Benutzerschl\u00fcssel" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-Schl\u00fcssel" + }, + "title": "Integration erneut authentifizieren" + }, + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "name": "Name", + "user_key": "Benutzerschl\u00fcssel" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Das Konfigurieren von Pushover mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die Pushover-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Pushover-YAML-Konfiguration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/es.json b/homeassistant/components/pushover/translations/es.json new file mode 100644 index 00000000000..c36644e575e --- /dev/null +++ b/homeassistant/components/pushover/translations/es.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_api_key": "Clave API no v\u00e1lida", + "invalid_user_key": "Clave de usuario no v\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Clave API" + }, + "title": "Volver a autenticar la integraci\u00f3n" + }, + "user": { + "data": { + "api_key": "Clave API", + "name": "Nombre", + "user_key": "Clave de usuario" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de Pushover mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de Pushover de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Pushover" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/fr.json b/homeassistant/components/pushover/translations/fr.json new file mode 100644 index 00000000000..a8d7a213d64 --- /dev/null +++ b/homeassistant/components/pushover/translations/fr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_api_key": "Cl\u00e9 d'API non valide", + "invalid_user_key": "Cl\u00e9 utilisateur non valide" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Cl\u00e9 d'API" + }, + "title": "R\u00e9-authentifier l'int\u00e9gration" + }, + "user": { + "data": { + "api_key": "Cl\u00e9 d'API", + "name": "Nom", + "user_key": "Cl\u00e9 utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/id.json b/homeassistant/components/pushover/translations/id.json new file mode 100644 index 00000000000..077ee9a450b --- /dev/null +++ b/homeassistant/components/pushover/translations/id.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_api_key": "Kunci API tidak valid", + "invalid_user_key": "Kunci pengguna tidak valid" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Kunci API" + }, + "title": "Autentikasi Ulang Integrasi" + }, + "user": { + "data": { + "api_key": "Kunci API", + "name": "Nama", + "user_key": "Kunci pengguna" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Pushover lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Pushover dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Pushover dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/it.json b/homeassistant/components/pushover/translations/it.json new file mode 100644 index 00000000000..c72f62b0c21 --- /dev/null +++ b/homeassistant/components/pushover/translations/it.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_api_key": "Chiave API non valida", + "invalid_user_key": "Chiave utente non valida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chiave API" + }, + "title": "Autentica nuovamente l'integrazione" + }, + "user": { + "data": { + "api_key": "Chiave API", + "name": "Nome", + "user_key": "Chiave utente" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Pushover tramite YAML sar\u00e0 rimossa.\n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente.\n\nRimuovi la configurazione YAML di Pushover dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Pushover sar\u00e0 rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/ja.json b/homeassistant/components/pushover/translations/ja.json new file mode 100644 index 00000000000..344e21952dc --- /dev/null +++ b/homeassistant/components/pushover/translations/ja.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_user_key": "\u7121\u52b9\u306a\u30e6\u30fc\u30b6\u30fc\u30ad\u30fc" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API\u30ad\u30fc" + }, + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" + }, + "user": { + "data": { + "api_key": "API\u30ad\u30fc", + "name": "\u540d\u524d", + "user_key": "\u30e6\u30fc\u30b6\u30fc\u30ad\u30fc" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Pushover\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089\u3001Pushover\u306eYAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "title": "Pushover YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/no.json b/homeassistant/components/pushover/translations/no.json new file mode 100644 index 00000000000..32f733eedcb --- /dev/null +++ b/homeassistant/components/pushover/translations/no.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_api_key": "Ugyldig API-n\u00f8kkel", + "invalid_user_key": "Ugyldig brukern\u00f8kkel" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "title": "Godkjenne integrering p\u00e5 nytt" + }, + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "name": "Navn", + "user_key": "Brukern\u00f8kkel" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Pushover med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern Pushover YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Pushover YAML-konfigurasjonen blir fjernet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/tr.json b/homeassistant/components/pushover/translations/tr.json new file mode 100644 index 00000000000..d91471736b1 --- /dev/null +++ b/homeassistant/components/pushover/translations/tr.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "invalid_user_key": "Ge\u00e7ersiz kullan\u0131c\u0131 anahtar\u0131" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API Anahtar\u0131" + }, + "title": "Entegrasyonu Yeniden Do\u011frula" + }, + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "name": "Ad", + "user_key": "Kullan\u0131c\u0131 anahtar\u0131" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "YAML kullanarak Pushover'\u0131 yap\u0131land\u0131rma kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z otomatik olarak kullan\u0131c\u0131 aray\u00fcz\u00fcne aktar\u0131ld\u0131. \n\n Pushover YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Pushover YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/zh-Hant.json b/homeassistant/components/pushover/translations/zh-Hant.json new file mode 100644 index 00000000000..bf359028bde --- /dev/null +++ b/homeassistant/components/pushover/translations/zh-Hant.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_api_key": "API \u91d1\u9470\u7121\u6548", + "invalid_user_key": "\u4f7f\u7528\u8005\u91d1\u9470\u7121\u6548" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "API \u91d1\u9470" + }, + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, + "user": { + "data": { + "api_key": "API \u91d1\u9470", + "name": "\u540d\u7a31", + "user_key": "\u4f7f\u7528\u8005\u91d1\u9470" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Pushover \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Pushover YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Pushover YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/tr.json b/homeassistant/components/qingping/translations/tr.json new file mode 100644 index 00000000000..f0ddbc274c9 --- /dev/null +++ b/homeassistant/components/qingping/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "not_supported": "Cihaz desteklenmiyor" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/es.json b/homeassistant/components/radiotherm/translations/es.json index 165068f38fa..fab444ede2a 100644 --- a/homeassistant/components/radiotherm/translations/es.json +++ b/homeassistant/components/radiotherm/translations/es.json @@ -21,7 +21,7 @@ }, "issues": { "deprecated_yaml": { - "description": "La configuraci\u00f3n de la plataforma clim\u00e1tica Radio Thermostat mediante YAML se eliminar\u00e1 en Home Assistant 2022.9. \n\nTu configuraci\u00f3n existente se ha importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se va a eliminar la configuraci\u00f3n de la plataforma clim\u00e1tica Radio Thermostat mediante YAML en Home Assistant 2022.9. \n\nTu configuraci\u00f3n existente se ha importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la configuraci\u00f3n YAML de Radio Thermostat" } }, diff --git a/homeassistant/components/schedule/translations/tr.json b/homeassistant/components/schedule/translations/tr.json new file mode 100644 index 00000000000..6c59c8507dc --- /dev/null +++ b/homeassistant/components/schedule/translations/tr.json @@ -0,0 +1,9 @@ +{ + "state": { + "_": { + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k" + } + }, + "title": "Zamanlama" +} \ No newline at end of file diff --git a/homeassistant/components/senz/translations/es.json b/homeassistant/components/senz/translations/es.json index 406d6b64017..671bd012f77 100644 --- a/homeassistant/components/senz/translations/es.json +++ b/homeassistant/components/senz/translations/es.json @@ -19,8 +19,8 @@ }, "issues": { "removed_yaml": { - "description": "Se elimin\u00f3 la configuraci\u00f3n de nVent RAYCHEM SENZ mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", - "title": "Se elimin\u00f3 la configuraci\u00f3n YAML de nVent RAYCHEM SENZ" + "description": "Se ha eliminado la configuraci\u00f3n de nVent RAYCHEM SENZ mediante YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se ha eliminado la configuraci\u00f3n YAML de nVent RAYCHEM SENZ" } } } \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/es.json b/homeassistant/components/simplepush/translations/es.json index acf2da9e5d2..e1bb97650ff 100644 --- a/homeassistant/components/simplepush/translations/es.json +++ b/homeassistant/components/simplepush/translations/es.json @@ -24,7 +24,7 @@ "title": "Se va a eliminar la configuraci\u00f3n YAML de Simplepush" }, "removed_yaml": { - "description": "Se ha eliminado la configuraci\u00f3n de Simplepush mediante YAML.\n\nTu configuraci\u00f3n YAML existente no es utilizada por Home Assistant.\n\nElimina la configuraci\u00f3n YAML de Simplepush de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se ha eliminado la configuraci\u00f3n de Simplepush usando YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de Simplepush de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se ha eliminado la configuraci\u00f3n YAML de Simplepush" } } diff --git a/homeassistant/components/skybell/translations/de.json b/homeassistant/components/skybell/translations/de.json index 65a21e4b8f5..2705eab7086 100644 --- a/homeassistant/components/skybell/translations/de.json +++ b/homeassistant/components/skybell/translations/de.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Die Konfiguration von Skybell mit YAML wurde entfernt. \n\nDeine vorhandene YAML-Konfiguration wird von Home Assistant nicht verwendet. \n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Skybell YAML-Konfiguration wurde entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/en.json b/homeassistant/components/skybell/translations/en.json index f9fa0048854..767f6bfe64e 100644 --- a/homeassistant/components/skybell/translations/en.json +++ b/homeassistant/components/skybell/translations/en.json @@ -20,8 +20,8 @@ }, "issues": { "removed_yaml": { - "title": "The Skybell YAML configuration has been removed", - "description": "Configuring Skybell using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + "description": "Configuring Skybell using YAML has been removed.\n\nYour existing YAML configuration is not used by Home Assistant.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Skybell YAML configuration has been removed" } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/es.json b/homeassistant/components/skybell/translations/es.json index 68633c72180..18cb57a194b 100644 --- a/homeassistant/components/skybell/translations/es.json +++ b/homeassistant/components/skybell/translations/es.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Se ha eliminado la configuraci\u00f3n de Skybell usando YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se ha eliminado la configuraci\u00f3n YAML de Skybell" + } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/fr.json b/homeassistant/components/skybell/translations/fr.json index 457e7c48157..ecb6a0d773d 100644 --- a/homeassistant/components/skybell/translations/fr.json +++ b/homeassistant/components/skybell/translations/fr.json @@ -17,5 +17,10 @@ } } } + }, + "issues": { + "removed_yaml": { + "title": "La configuration YAML pour Skybell a \u00e9t\u00e9 supprim\u00e9e" + } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/id.json b/homeassistant/components/skybell/translations/id.json index d59082eb80b..8e7aa4a5a87 100644 --- a/homeassistant/components/skybell/translations/id.json +++ b/homeassistant/components/skybell/translations/id.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Proses konfigurasi Skybell lewat YAML telah dihapus.\n\nKonfigurasi YAML yang ada tidak digunakan oleh Home Assistant.\n\nHapus konfigurasi YAML dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Skybell telah dihapus" + } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/it.json b/homeassistant/components/skybell/translations/it.json index 39d4856dbe4..d9e798e534c 100644 --- a/homeassistant/components/skybell/translations/it.json +++ b/homeassistant/components/skybell/translations/it.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "La configurazione di Skybell tramite YAML \u00e8 stata rimossa. \n\n La tua configurazione YAML esistente non \u00e8 utilizzata da Home Assistant. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di Skybell \u00e8 stata rimossa" + } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/ja.json b/homeassistant/components/skybell/translations/ja.json index 9e2d562206f..0cac70de54a 100644 --- a/homeassistant/components/skybell/translations/ja.json +++ b/homeassistant/components/skybell/translations/ja.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "Skybell\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "title": "Skybell YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" + } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/tr.json b/homeassistant/components/skybell/translations/tr.json index 68bd9029559..23e8cb87551 100644 --- a/homeassistant/components/skybell/translations/tr.json +++ b/homeassistant/components/skybell/translations/tr.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "YAML kullanarak Skybell'i yap\u0131land\u0131rma kald\u0131r\u0131ld\u0131. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lm\u0131yor. \n\n YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Skybell YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" + } } } \ No newline at end of file diff --git a/homeassistant/components/skybell/translations/zh-Hant.json b/homeassistant/components/skybell/translations/zh-Hant.json index 1e614212c45..faae37d9b31 100644 --- a/homeassistant/components/skybell/translations/zh-Hant.json +++ b/homeassistant/components/skybell/translations/zh-Hant.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a Skybell \u7684\u529f\u80fd\u5373\u5c07\u79fb\u9664\u3002\n\nHome Assistant \u5c07\u4e0d\u518d\u4f7f\u7528\u73fe\u6709\u7684 YAML \u8a2d\u5b9a\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Skybell YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/es.json b/homeassistant/components/spotify/translations/es.json index e2e3cf927a7..f9267f93c08 100644 --- a/homeassistant/components/spotify/translations/es.json +++ b/homeassistant/components/spotify/translations/es.json @@ -21,7 +21,7 @@ }, "issues": { "removed_yaml": { - "description": "Se elimin\u00f3 la configuraci\u00f3n de Spotify usando YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se ha eliminado la configuraci\u00f3n de Spotify usando YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se ha eliminado la configuraci\u00f3n YAML de Spotify" } }, diff --git a/homeassistant/components/steam_online/translations/es.json b/homeassistant/components/steam_online/translations/es.json index e558ead9ff5..dfb1cdbd50e 100644 --- a/homeassistant/components/steam_online/translations/es.json +++ b/homeassistant/components/steam_online/translations/es.json @@ -26,7 +26,7 @@ }, "issues": { "removed_yaml": { - "description": "Se elimin\u00f3 la configuraci\u00f3n de Steam usando YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se ha eliminado la configuraci\u00f3n de Steam usando YAML. \n\nHome Assistant no utiliza tu configuraci\u00f3n YAML existente. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se ha eliminado la configuraci\u00f3n YAML de Steam" } }, diff --git a/homeassistant/components/uscis/translations/es.json b/homeassistant/components/uscis/translations/es.json index f18acfff461..cb8689e1b8b 100644 --- a/homeassistant/components/uscis/translations/es.json +++ b/homeassistant/components/uscis/translations/es.json @@ -1,7 +1,7 @@ { "issues": { "pending_removal": { - "description": "La integraci\u00f3n de los Servicios de Inmigraci\u00f3n y Ciudadan\u00eda de los EE.UU. (USCIS) est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nLa integraci\u00f3n se elimina porque se basa en webscraping, algo que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "La integraci\u00f3n de los Servicios de Inmigraci\u00f3n y Ciudadan\u00eda de los EE.UU. (USCIS) est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nSe va a eliminar la integraci\u00f3n porque se basa en webscraping, algo que no est\u00e1 permitido. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la integraci\u00f3n USCIS" } } diff --git a/homeassistant/components/xbox/translations/es.json b/homeassistant/components/xbox/translations/es.json index f1b1945c832..eaf4275884b 100644 --- a/homeassistant/components/xbox/translations/es.json +++ b/homeassistant/components/xbox/translations/es.json @@ -16,7 +16,7 @@ }, "issues": { "deprecated_yaml": { - "description": "La configuraci\u00f3n de Xbox en configuration.yaml se eliminar\u00e1 en Home Assistant 2022.9. \n\nTus credenciales OAuth de aplicaci\u00f3n existentes y la configuraci\u00f3n de acceso se han importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "description": "Se va a eliminar la configuraci\u00f3n de Xbox en configuration.yaml en Home Assistant 2022.9. \n\nTus credenciales OAuth de aplicaci\u00f3n existentes y la configuraci\u00f3n de acceso se han importado a la IU autom\u00e1ticamente. Elimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", "title": "Se va a eliminar la configuraci\u00f3n YAML de Xbox" } } diff --git a/homeassistant/components/xiaomi_miio/translations/select.id.json b/homeassistant/components/xiaomi_miio/translations/select.id.json index 178bc06301c..149e2715e49 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.id.json +++ b/homeassistant/components/xiaomi_miio/translations/select.id.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "Maju", + "left": "Kiri", + "right": "Kanan" + }, "xiaomi_miio__led_brightness": { "bright": "Terang", "dim": "Redup", "off": "Mati" + }, + "xiaomi_miio__ptc_level": { + "high": "Tinggi", + "low": "Rendah", + "medium": "Sedang" } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/select.tr.json b/homeassistant/components/xiaomi_miio/translations/select.tr.json index 7767a54fe2d..9ffb8dbd212 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.tr.json +++ b/homeassistant/components/xiaomi_miio/translations/select.tr.json @@ -1,9 +1,19 @@ { "state": { + "xiaomi_miio__display_orientation": { + "forward": "\u0130leri", + "left": "Sol", + "right": "Sa\u011f" + }, "xiaomi_miio__led_brightness": { "bright": "Ayd\u0131nl\u0131k", "dim": "Dim", "off": "Kapal\u0131" + }, + "xiaomi_miio__ptc_level": { + "high": "Y\u00fcksek", + "low": "D\u00fc\u015f\u00fck", + "medium": "Orta" } } } \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/tr.json b/homeassistant/components/yalexs_ble/translations/tr.json new file mode 100644 index 00000000000..15711050225 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/tr.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "no_unconfigured_devices": "Yap\u0131land\u0131r\u0131lmam\u0131\u015f cihaz bulunamad\u0131." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_key_format": "\u00c7evrimd\u0131\u015f\u0131 anahtar 32 baytl\u0131k bir onalt\u0131l\u0131k dize olmal\u0131d\u0131r.", + "invalid_key_index": "\u00c7evrimd\u0131\u015f\u0131 anahtar yuvas\u0131, 0 ile 255 aras\u0131nda bir tam say\u0131 olmal\u0131d\u0131r.", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "{name}", + "step": { + "integration_discovery_confirm": { + "description": "{address} adresiyle Bluetooth \u00fczerinden {name} kurmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Bluetooth adresi", + "key": "\u00c7evrimd\u0131\u015f\u0131 Anahtar (32 baytl\u0131k onalt\u0131l\u0131k dize)", + "slot": "\u00c7evrimd\u0131\u015f\u0131 Anahtar Yuvas\u0131 (0 ile 255 aras\u0131nda tam say\u0131)" + }, + "description": "\u00c7evrimd\u0131\u015f\u0131 anahtar\u0131n nas\u0131l bulunaca\u011f\u0131na ili\u015fkin belgelere bak\u0131n." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 7786a3b7bf3..5ac32a1df1f 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -13,6 +13,9 @@ "confirm": { "description": "M\u00f6chtest du {name} einrichten?" }, + "confirm_hardware": { + "description": "M\u00f6chtest du {name} einrichten?" + }, "pick_radio": { "data": { "radio_type": "Funktyp" diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index 757ab338ec6..46e897167b0 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -13,6 +13,9 @@ "confirm": { "description": "Do you want to setup {name}?" }, + "confirm_hardware": { + "description": "Do you want to setup {name}?" + }, "pick_radio": { "data": { "radio_type": "Radio Type" diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 6dcc3c84e42..a3e5f0b63ef 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -13,6 +13,9 @@ "confirm": { "description": "\u00bfQuieres configurar {name} ?" }, + "confirm_hardware": { + "description": "\u00bfQuieres configurar {name}?" + }, "pick_radio": { "data": { "radio_type": "Tipo de Radio" diff --git a/homeassistant/components/zha/translations/fr.json b/homeassistant/components/zha/translations/fr.json index f69e4fb36ff..87027c7ca62 100644 --- a/homeassistant/components/zha/translations/fr.json +++ b/homeassistant/components/zha/translations/fr.json @@ -13,6 +13,9 @@ "confirm": { "description": "Voulez-vous configurer {name}\u00a0?" }, + "confirm_hardware": { + "description": "Voulez-vous configurer {name}\u00a0?" + }, "pick_radio": { "data": { "radio_type": "Type de radio" diff --git a/homeassistant/components/zha/translations/id.json b/homeassistant/components/zha/translations/id.json index ef91a4227c8..f9947809a6d 100644 --- a/homeassistant/components/zha/translations/id.json +++ b/homeassistant/components/zha/translations/id.json @@ -13,6 +13,9 @@ "confirm": { "description": "Ingin menyiapkan {name}?" }, + "confirm_hardware": { + "description": "Ingin menyiapkan {name}?" + }, "pick_radio": { "data": { "radio_type": "Jenis Radio" diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index a71e96ed308..9ceb94cbd00 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -13,6 +13,9 @@ "confirm": { "description": "Vuoi configurare {name}?" }, + "confirm_hardware": { + "description": "Vuoi configurare {name}?" + }, "pick_radio": { "data": { "radio_type": "Tipo di radio" diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index 25d5a5bd9f6..6cfd70056b6 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -13,6 +13,9 @@ "confirm": { "description": "{name} \u3092\u8a2d\u5b9a\u3057\u307e\u3059\u304b\uff1f" }, + "confirm_hardware": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, "pick_radio": { "data": { "radio_type": "\u7121\u7dda\u30bf\u30a4\u30d7" diff --git a/homeassistant/components/zha/translations/tr.json b/homeassistant/components/zha/translations/tr.json index 391b9315b48..82eb9b11b68 100644 --- a/homeassistant/components/zha/translations/tr.json +++ b/homeassistant/components/zha/translations/tr.json @@ -13,6 +13,9 @@ "confirm": { "description": "{name} kurulumunu yapmak istiyor musunuz?" }, + "confirm_hardware": { + "description": "{name} kurulumunu yapmak istiyor musunuz?" + }, "pick_radio": { "data": { "radio_type": "Radyo Tipi" diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index 546a2f77c31..a6c69311181 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -13,6 +13,9 @@ "confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, + "confirm_hardware": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, "pick_radio": { "data": { "radio_type": "\u7121\u7dda\u96fb\u985e\u5225" From 3a3f41f3df932368791d3ee3f5fbae5fb3b38bfe Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Aug 2022 07:52:55 +0200 Subject: [PATCH 3491/3516] Improve entity type hints [e] (#77041) --- homeassistant/components/ebox/sensor.py | 2 +- homeassistant/components/ebusd/sensor.py | 2 +- .../components/ecoal_boiler/sensor.py | 2 +- .../components/ecoal_boiler/switch.py | 8 ++++-- .../components/ecobee/binary_sensor.py | 4 +-- homeassistant/components/ecobee/climate.py | 19 +++++++------ homeassistant/components/ecobee/sensor.py | 6 ++-- homeassistant/components/ecobee/weather.py | 2 +- homeassistant/components/econet/climate.py | 16 ++++++----- .../components/econet/water_heater.py | 11 ++++---- homeassistant/components/ecovacs/vacuum.py | 22 +++++++++------ homeassistant/components/edimax/switch.py | 8 ++++-- homeassistant/components/edl21/sensor.py | 4 +-- .../components/egardia/binary_sensor.py | 2 +- homeassistant/components/eliqonline/sensor.py | 2 +- homeassistant/components/elv/switch.py | 7 +++-- homeassistant/components/emby/media_player.py | 14 +++++----- homeassistant/components/emoncms/sensor.py | 2 +- .../components/enigma2/media_player.py | 28 +++++++++---------- homeassistant/components/enocean/sensor.py | 2 +- homeassistant/components/enocean/switch.py | 6 ++-- .../components/envisalink/binary_sensor.py | 2 +- homeassistant/components/envisalink/sensor.py | 2 +- homeassistant/components/envisalink/switch.py | 7 +++-- homeassistant/components/ephember/climate.py | 11 ++++---- .../components/epson/media_player.py | 22 +++++++-------- .../components/epsonworkforce/sensor.py | 8 ++++-- .../components/eq3btsmart/climate.py | 9 +++--- homeassistant/components/etherscan/sensor.py | 2 +- homeassistant/components/eufy/switch.py | 8 ++++-- homeassistant/components/everlights/light.py | 5 ++-- .../components/evohome/water_heater.py | 4 +-- 32 files changed, 138 insertions(+), 111 deletions(-) diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index ef6c0f4b323..3e1a2fa2413 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -184,7 +184,7 @@ class EBoxSensor(SensorEntity): self._attr_name = f"{name} {description.name}" self.ebox_data = ebox_data - async def async_update(self): + async def async_update(self) -> None: """Get the latest data from EBox and update the state.""" await self.ebox_data.async_update() if self.entity_description.key in self.ebox_data.data: diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py index 1acbe47c2a1..923f94f705d 100644 --- a/homeassistant/components/ebusd/sensor.py +++ b/homeassistant/components/ebusd/sensor.py @@ -111,7 +111,7 @@ class EbusdSensor(SensorEntity): return self._unit_of_measurement @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): + def update(self) -> None: """Fetch new state data for the sensor.""" try: self.data.update(self._name, self._type) diff --git a/homeassistant/components/ecoal_boiler/sensor.py b/homeassistant/components/ecoal_boiler/sensor.py index d85bd9edf6c..5c8bf926fce 100644 --- a/homeassistant/components/ecoal_boiler/sensor.py +++ b/homeassistant/components/ecoal_boiler/sensor.py @@ -39,7 +39,7 @@ class EcoalTempSensor(SensorEntity): self._attr_name = name self._status_attr = status_attr - def update(self): + def update(self) -> None: """Fetch new state data for the sensor. This is the only method that should fetch new data for Home Assistant. diff --git a/homeassistant/components/ecoal_boiler/switch.py b/homeassistant/components/ecoal_boiler/switch.py index b137cf5832d..6922a35f5de 100644 --- a/homeassistant/components/ecoal_boiler/switch.py +++ b/homeassistant/components/ecoal_boiler/switch.py @@ -1,6 +1,8 @@ """Allows to configuration ecoal (esterownik.pl) pumps as switches.""" from __future__ import annotations +from typing import Any + from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -45,7 +47,7 @@ class EcoalSwitch(SwitchEntity): # status. self._contr_set_fun = getattr(self._ecoal_contr, f"set_{state_attr}") - def update(self): + def update(self) -> None: """Fetch new state data for the sensor. This is the only method that should fetch new data for Home Assistant. @@ -60,12 +62,12 @@ class EcoalSwitch(SwitchEntity): """ self._ecoal_contr.status = None - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn the device on.""" self._contr_set_fun(1) self.invalidate_ecoal_cache() - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn the device off.""" self._contr_set_fun(0) self.invalidate_ecoal_cache() diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 1f8f94e93df..2266d70e0ad 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -91,7 +91,7 @@ class EcobeeBinarySensor(BinarySensorEntity): return None @property - def available(self): + def available(self) -> bool: """Return true if device is available.""" thermostat = self.data.ecobee.get_thermostat(self.index) return thermostat["runtime"]["connected"] @@ -106,7 +106,7 @@ class EcobeeBinarySensor(BinarySensorEntity): """Return the class of this sensor, from DEVICE_CLASSES.""" return BinarySensorDeviceClass.OCCUPANCY - async def async_update(self): + async def async_update(self) -> None: """Get the latest state of the sensor.""" await self.data.update() for sensor in self.data.ecobee.get_remote_sensors(self.index): diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 3673728c7fa..d256d241a4f 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import collections +from typing import Any import voluptuous as vol @@ -330,7 +331,7 @@ class Thermostat(ClimateEntity): self._fan_modes = [FAN_AUTO, FAN_ON] self.update_without_throttle = False - async def async_update(self): + async def async_update(self) -> None: """Get the latest state from the thermostat.""" if self.update_without_throttle: await self.data.update(no_throttle=True) @@ -342,12 +343,12 @@ class Thermostat(ClimateEntity): self._last_active_hvac_mode = self.hvac_mode @property - def available(self): + def available(self) -> bool: """Return if device is available.""" return self.thermostat["runtime"]["connected"] @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" if self.has_humidifier_control: return SUPPORT_FLAGS | ClimateEntityFeature.TARGET_HUMIDITY @@ -563,7 +564,7 @@ class Thermostat(ClimateEntity): if self.is_aux_heat: _LOGGER.warning("# Changing aux heat is not supported") - def set_preset_mode(self, preset_mode): + def set_preset_mode(self, preset_mode: str) -> None: """Activate a preset.""" if preset_mode == self.preset_mode: return @@ -653,7 +654,7 @@ class Thermostat(ClimateEntity): self.update_without_throttle = True - def set_fan_mode(self, fan_mode): + def set_fan_mode(self, fan_mode: str) -> None: """Set the fan mode. Valid values are "on" or "auto".""" if fan_mode.lower() not in (FAN_ON, FAN_AUTO): error = "Invalid fan_mode value: Valid values are 'on' or 'auto'" @@ -689,7 +690,7 @@ class Thermostat(ClimateEntity): cool_temp = temp + delta self.set_auto_temp_hold(heat_temp, cool_temp) - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) @@ -704,7 +705,7 @@ class Thermostat(ClimateEntity): else: _LOGGER.error("Missing valid arguments for set_temperature in %s", kwargs) - def set_humidity(self, humidity): + def set_humidity(self, humidity: int) -> None: """Set the humidity level.""" if humidity not in range(0, 101): raise ValueError( @@ -714,7 +715,7 @@ class Thermostat(ClimateEntity): self.data.ecobee.set_humidity(self.thermostat_index, int(humidity)) self.update_without_throttle = True - def set_hvac_mode(self, hvac_mode): + def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set HVAC mode (auto, auxHeatOnly, cool, heat, off).""" ecobee_value = next( (k for k, v in ECOBEE_HVAC_TO_HASS.items() if v == hvac_mode), None @@ -821,7 +822,7 @@ class Thermostat(ClimateEntity): ) self.data.ecobee.delete_vacation(self.thermostat_index, vacation_name) - def turn_on(self): + def turn_on(self) -> None: """Set the thermostat to the last active HVAC mode.""" _LOGGER.debug( "Turning on ecobee thermostat %s in %s mode", diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 38671189132..a7d5639ae2c 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -105,6 +105,8 @@ async def async_setup_entry( class EcobeeSensor(SensorEntity): """Representation of an Ecobee sensor.""" + entity_description: EcobeeSensorEntityDescription + def __init__( self, data, @@ -163,7 +165,7 @@ class EcobeeSensor(SensorEntity): return None @property - def available(self): + def available(self) -> bool: """Return true if device is available.""" thermostat = self.data.ecobee.get_thermostat(self.index) return thermostat["runtime"]["connected"] @@ -183,7 +185,7 @@ class EcobeeSensor(SensorEntity): return self._state - async def async_update(self): + async def async_update(self) -> None: """Get the latest state of the sensor.""" await self.data.update() for sensor in self.data.ecobee.get_remote_sensors(self.index): diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index aca4dcdf2f5..69f02d26294 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -188,7 +188,7 @@ class EcobeeWeather(WeatherEntity): return forecasts return None - async def async_update(self): + async def async_update(self) -> None: """Get the latest weather data.""" await self.data.update() thermostat = self.data.ecobee.get_thermostat(self._index) diff --git a/homeassistant/components/econet/climate.py b/homeassistant/components/econet/climate.py index 16dd4e043dc..9fba4883644 100644 --- a/homeassistant/components/econet/climate.py +++ b/homeassistant/components/econet/climate.py @@ -1,4 +1,6 @@ """Support for Rheem EcoNet thermostats.""" +from typing import Any + from pyeconet.equipment import EquipmentType from pyeconet.equipment.thermostat import ThermostatFanMode, ThermostatOperationMode @@ -79,7 +81,7 @@ class EcoNetThermostat(EcoNetEntity, ClimateEntity): self.op_list.append(ha_mode) @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" if self._econet.supports_humidifier: return SUPPORT_FLAGS_THERMOSTAT | ClimateEntityFeature.TARGET_HUMIDITY @@ -125,7 +127,7 @@ class EcoNetThermostat(EcoNetEntity, ClimateEntity): return self._econet.cool_set_point return None - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" target_temp = kwargs.get(ATTR_TEMPERATURE) target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) @@ -161,14 +163,14 @@ class EcoNetThermostat(EcoNetEntity, ClimateEntity): return _current_op - def set_hvac_mode(self, hvac_mode): + def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" hvac_mode_to_set = HA_STATE_TO_ECONET.get(hvac_mode) if hvac_mode_to_set is None: raise ValueError(f"{hvac_mode} is not a valid mode.") self._econet.set_mode(hvac_mode_to_set) - def set_humidity(self, humidity: int): + def set_humidity(self, humidity: int) -> None: """Set new target humidity.""" self._econet.set_dehumidifier_set_point(humidity) @@ -201,15 +203,15 @@ class EcoNetThermostat(EcoNetEntity, ClimateEntity): fan_list.append(ECONET_FAN_STATE_TO_HA[mode]) return fan_list - def set_fan_mode(self, fan_mode): + def set_fan_mode(self, fan_mode: str) -> None: """Set the fan mode.""" self._econet.set_fan_mode(HA_FAN_STATE_TO_ECONET[fan_mode]) - def turn_aux_heat_on(self): + def turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" self._econet.set_mode(ThermostatOperationMode.EMERGENCY_HEAT) - def turn_aux_heat_off(self): + def turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" self._econet.set_mode(ThermostatOperationMode.HEATING) diff --git a/homeassistant/components/econet/water_heater.py b/homeassistant/components/econet/water_heater.py index 79b821c6cba..50f080217b4 100644 --- a/homeassistant/components/econet/water_heater.py +++ b/homeassistant/components/econet/water_heater.py @@ -1,5 +1,6 @@ """Support for Rheem EcoNet water heaters.""" import logging +from typing import Any from pyeconet.equipment import EquipmentType from pyeconet.equipment.water_heater import WaterHeaterOperationMode @@ -118,14 +119,14 @@ class EcoNetWaterHeater(EcoNetEntity, WaterHeaterEntity): ) return WaterHeaterEntityFeature.TARGET_TEMPERATURE - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (target_temp := kwargs.get(ATTR_TEMPERATURE)) is not None: self.water_heater.set_set_point(target_temp) else: _LOGGER.error("A target temperature must be provided") - def set_operation_mode(self, operation_mode): + def set_operation_mode(self, operation_mode: str) -> None: """Set operation mode.""" op_mode_to_set = HA_STATE_TO_ECONET.get(operation_mode) if op_mode_to_set is not None: @@ -156,17 +157,17 @@ class EcoNetWaterHeater(EcoNetEntity, WaterHeaterEntity): """ return self._poll - async def async_update(self): + async def async_update(self) -> None: """Get the latest energy usage.""" await self.water_heater.get_energy_usage() await self.water_heater.get_water_usage() self.async_write_ha_state() self._poll = False - def turn_away_mode_on(self): + def turn_away_mode_on(self) -> None: """Turn away mode on.""" self.water_heater.set_away_mode(True) - def turn_away_mode_off(self): + def turn_away_mode_off(self) -> None: """Turn away mode off.""" self.water_heater.set_away_mode(False) diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index 8557658d128..c380a760557 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import sucks @@ -107,7 +108,7 @@ class EcovacsVacuum(VacuumEntity): """Return the status of the vacuum cleaner.""" return self.device.vacuum_status - def return_to_base(self, **kwargs): + def return_to_base(self, **kwargs: Any) -> None: """Set the vacuum cleaner to return to the dock.""" self.device.run(sucks.Charge()) @@ -132,37 +133,42 @@ class EcovacsVacuum(VacuumEntity): """Return the fan speed of the vacuum cleaner.""" return self.device.fan_speed - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the vacuum on and start cleaning.""" self.device.run(sucks.Clean()) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the vacuum off stopping the cleaning and returning home.""" self.return_to_base() - def stop(self, **kwargs): + def stop(self, **kwargs: Any) -> None: """Stop the vacuum cleaner.""" self.device.run(sucks.Stop()) - def clean_spot(self, **kwargs): + def clean_spot(self, **kwargs: Any) -> None: """Perform a spot clean-up.""" self.device.run(sucks.Spot()) - def locate(self, **kwargs): + def locate(self, **kwargs: Any) -> None: """Locate the vacuum cleaner.""" self.device.run(sucks.PlaySound()) - def set_fan_speed(self, fan_speed, **kwargs): + def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set fan speed.""" if self.is_on: self.device.run(sucks.Clean(mode=self.device.clean_status, speed=fan_speed)) - def send_command(self, command, params=None, **kwargs): + def send_command( + self, + command: str, + params: dict[str, Any] | list[Any] | None = None, + **kwargs: Any, + ) -> None: """Send a command to a vacuum cleaner.""" self.device.run(sucks.VacBotCommand(command, params)) diff --git a/homeassistant/components/edimax/switch.py b/homeassistant/components/edimax/switch.py index 6f780f8da61..34f6a500917 100644 --- a/homeassistant/components/edimax/switch.py +++ b/homeassistant/components/edimax/switch.py @@ -1,6 +1,8 @@ """Support for Edimax switches.""" from __future__ import annotations +from typing import Any + from pyedimax.smartplug import SmartPlug import voluptuous as vol @@ -67,15 +69,15 @@ class SmartPlugSwitch(SwitchEntity): """Return true if switch is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self.smartplug.state = "ON" - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self.smartplug.state = "OFF" - def update(self): + def update(self) -> None: """Update edimax switch.""" if not self._info: self._info = self.smartplug.info diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index 730acabbc98..fe3e52548c5 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -386,7 +386,7 @@ class EDL21Entity(SensorEntity): self._async_remove_dispatcher = None self.entity_description = entity_description - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" @callback @@ -411,7 +411,7 @@ class EDL21Entity(SensorEntity): self.hass, SIGNAL_EDL21_TELEGRAM, handle_telegram ) - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" if self._async_remove_dispatcher: self._async_remove_dispatcher() diff --git a/homeassistant/components/egardia/binary_sensor.py b/homeassistant/components/egardia/binary_sensor.py index 7a207abfa22..021111e53b3 100644 --- a/homeassistant/components/egardia/binary_sensor.py +++ b/homeassistant/components/egardia/binary_sensor.py @@ -58,7 +58,7 @@ class EgardiaBinarySensor(BinarySensorEntity): self._device_class = device_class self._egardia_system = egardia_system - def update(self): + def update(self) -> None: """Update the status.""" egardia_input = self._egardia_system.getsensorstate(self._id) self._state = STATE_ON if egardia_input else STATE_OFF diff --git a/homeassistant/components/eliqonline/sensor.py b/homeassistant/components/eliqonline/sensor.py index ba4d32fbbd8..9b81ebad78a 100644 --- a/homeassistant/components/eliqonline/sensor.py +++ b/homeassistant/components/eliqonline/sensor.py @@ -97,7 +97,7 @@ class EliqSensor(SensorEntity): """Return the state of the device.""" return self._state - async def async_update(self): + async def async_update(self) -> None: """Get the latest data.""" try: response = await self._api.get_data_now(channelid=self._channel_id) diff --git a/homeassistant/components/elv/switch.py b/homeassistant/components/elv/switch.py index 8a7da161da0..d7e35f3e04c 100644 --- a/homeassistant/components/elv/switch.py +++ b/homeassistant/components/elv/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import pypca from serial import SerialException @@ -72,15 +73,15 @@ class SmartPlugSwitch(SwitchEntity): """Return true if switch is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" self._pca.turn_on(self._device_id) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" self._pca.turn_off(self._device_id) - def update(self): + def update(self) -> None: """Update the PCA switch's state.""" try: self._state = self._pca.get_state(self._device_id) diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index 0278028c458..d2dcfa2c629 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -153,7 +153,7 @@ class EmbyDevice(MediaPlayerEntity): self.media_status_last_position = None self.media_status_received = None - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callback.""" self.emby.add_update_callback(self.async_update_callback, self.device_id) @@ -311,26 +311,26 @@ class EmbyDevice(MediaPlayerEntity): return SUPPORT_EMBY return 0 - async def async_media_play(self): + async def async_media_play(self) -> None: """Play media.""" await self.device.media_play() - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Pause the media player.""" await self.device.media_pause() - async def async_media_stop(self): + async def async_media_stop(self) -> None: """Stop the media player.""" await self.device.media_stop() - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Send next track command.""" await self.device.media_next() - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Send next track command.""" await self.device.media_previous() - async def async_media_seek(self, position): + async def async_media_seek(self, position: float) -> None: """Send seek command.""" await self.device.media_seek(position) diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 0aab21458f4..e4148d1dea5 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -225,7 +225,7 @@ class EmonCmsSensor(SensorEntity): ATTR_LASTUPDATETIMESTR: template.timestamp_local(float(self._elem["time"])), } - def update(self): + def update(self) -> None: """Get the latest data and updates the state.""" self._data.update() diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index c240d882f8c..aab3514b8e0 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -140,15 +140,15 @@ class Enigma2Device(MediaPlayerEntity): return STATE_OFF if self.e2_box.in_standby else STATE_ON @property - def available(self): + def available(self) -> bool: """Return True if the device is available.""" return not self.e2_box.is_offline - def turn_off(self): + def turn_off(self) -> None: """Turn off media player.""" self.e2_box.turn_off() - def turn_on(self): + def turn_on(self) -> None: """Turn the media player on.""" self.e2_box.turn_on() @@ -187,15 +187,15 @@ class Enigma2Device(MediaPlayerEntity): """Picon url for the channel.""" return self.e2_box.picon_url - def set_volume_level(self, volume): + def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self.e2_box.set_volume(int(volume * 100)) - def volume_up(self): + def volume_up(self) -> None: """Volume up the media player.""" self.e2_box.set_volume(int(self.e2_box.volume * 100) + 5) - def volume_down(self): + def volume_down(self) -> None: """Volume down media player.""" self.e2_box.set_volume(int(self.e2_box.volume * 100) - 5) @@ -204,27 +204,27 @@ class Enigma2Device(MediaPlayerEntity): """Volume level of the media player (0..1).""" return self.e2_box.volume - def media_stop(self): + def media_stop(self) -> None: """Send stop command.""" self.e2_box.set_stop() - def media_play(self): + def media_play(self) -> None: """Play media.""" self.e2_box.toggle_play_pause() - def media_pause(self): + def media_pause(self) -> None: """Pause the media player.""" self.e2_box.toggle_play_pause() - def media_next_track(self): + def media_next_track(self) -> None: """Send next track command.""" self.e2_box.set_channel_up() - def media_previous_track(self): + def media_previous_track(self) -> None: """Send next track command.""" self.e2_box.set_channel_down() - def mute_volume(self, mute): + def mute_volume(self, mute: bool) -> None: """Mute or unmute.""" self.e2_box.mute_volume() @@ -238,11 +238,11 @@ class Enigma2Device(MediaPlayerEntity): """List of available input sources.""" return self.e2_box.source_list - def select_source(self, source): + def select_source(self, source: str) -> None: """Select input source.""" self.e2_box.select_source(self.e2_box.sources[source]) - def update(self): + def update(self) -> None: """Update state of the media_player.""" self.e2_box.update() diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index 06ea50d4cdb..84237852e80 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -162,7 +162,7 @@ class EnOceanSensor(EnOceanEntity, RestoreEntity, SensorEntity): self._attr_name = f"{description.name} {dev_name}" self._attr_unique_id = description.unique_id(dev_id) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Call when entity about to be added to hass.""" # If not None, we got an initial value. await super().async_added_to_hass() diff --git a/homeassistant/components/enocean/switch.py b/homeassistant/components/enocean/switch.py index 5edd2bb6155..28727bfb767 100644 --- a/homeassistant/components/enocean/switch.py +++ b/homeassistant/components/enocean/switch.py @@ -1,6 +1,8 @@ """Support for EnOcean switches.""" from __future__ import annotations +from typing import Any + from enocean.utils import combine_hex import voluptuous as vol @@ -94,7 +96,7 @@ class EnOceanSwitch(EnOceanEntity, SwitchEntity): """Return the device name.""" return self.dev_name - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" optional = [0x03] optional.extend(self.dev_id) @@ -106,7 +108,7 @@ class EnOceanSwitch(EnOceanEntity, SwitchEntity): ) self._on_state = True - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" optional = [0x03] optional.extend(self.dev_id) diff --git a/homeassistant/components/envisalink/binary_sensor.py b/homeassistant/components/envisalink/binary_sensor.py index d82f90aa4f4..f08989c32be 100644 --- a/homeassistant/components/envisalink/binary_sensor.py +++ b/homeassistant/components/envisalink/binary_sensor.py @@ -62,7 +62,7 @@ class EnvisalinkBinarySensor(EnvisalinkDevice, BinarySensorEntity): _LOGGER.debug("Setting up zone: %s", zone_name) super().__init__(zone_name, info, controller) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/envisalink/sensor.py b/homeassistant/components/envisalink/sensor.py index d8f31d7c4dd..72a64931070 100644 --- a/homeassistant/components/envisalink/sensor.py +++ b/homeassistant/components/envisalink/sensor.py @@ -59,7 +59,7 @@ class EnvisalinkSensor(EnvisalinkDevice, SensorEntity): _LOGGER.debug("Setting up sensor for partition: %s", partition_name) super().__init__(f"{partition_name} Keypad", info, controller) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( diff --git a/homeassistant/components/envisalink/switch.py b/homeassistant/components/envisalink/switch.py index 6f5179a8649..0bedc41e55e 100644 --- a/homeassistant/components/envisalink/switch.py +++ b/homeassistant/components/envisalink/switch.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.core import HomeAssistant, callback @@ -58,7 +59,7 @@ class EnvisalinkSwitch(EnvisalinkDevice, SwitchEntity): super().__init__(zone_name, info, controller) - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( @@ -71,11 +72,11 @@ class EnvisalinkSwitch(EnvisalinkDevice, SwitchEntity): """Return the boolean response if the zone is bypassed.""" return self._info["bypassed"] - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs: Any) -> None: """Send the bypass keypress sequence to toggle the zone bypass.""" self._controller.toggle_zone_bypass(self._zone_number) - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Send the bypass keypress sequence to toggle the zone bypass.""" self._controller.toggle_zone_bypass(self._zone_number) diff --git a/homeassistant/components/ephember/climate.py b/homeassistant/components/ephember/climate.py index 6bc95818329..7c9d9c04318 100644 --- a/homeassistant/components/ephember/climate.py +++ b/homeassistant/components/ephember/climate.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any from pyephember.pyephember import ( EphEmber, @@ -141,7 +142,7 @@ class EphEmberThermostat(ClimateEntity): """Return the supported operations.""" return OPERATION_LIST - def set_hvac_mode(self, hvac_mode): + def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set the operation mode.""" mode = self.map_mode_hass_eph(hvac_mode) if mode is not None: @@ -155,17 +156,17 @@ class EphEmberThermostat(ClimateEntity): return zone_is_boost_active(self._zone) - def turn_aux_heat_on(self): + def turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" self._ember.activate_boost_by_name( self._zone_name, zone_target_temperature(self._zone) ) - def turn_aux_heat_off(self): + def turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" self._ember.deactivate_boost_by_name(self._zone_name) - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return @@ -198,7 +199,7 @@ class EphEmberThermostat(ClimateEntity): return 35.0 - def update(self): + def update(self) -> None: """Get the latest data.""" self._zone = self._ember.get_zone(self._zone_name) diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 98152efb3b2..f978c145b4f 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -114,7 +114,7 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): ) return True - async def async_update(self): + async def async_update(self) -> None: """Update state of device.""" power_state = await self._projector.get_power() _LOGGER.debug("Projector status: %s", power_state) @@ -175,13 +175,13 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): """Return if projector is available.""" return self._available - async def async_turn_on(self): + async def async_turn_on(self) -> None: """Turn on epson.""" if self._state == STATE_OFF: await self._projector.send_command(TURN_ON) self._state = STATE_ON - async def async_turn_off(self): + async def async_turn_off(self) -> None: """Turn off epson.""" if self._state == STATE_ON: await self._projector.send_command(TURN_OFF) @@ -206,36 +206,36 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity): """Set color mode in Epson.""" await self._projector.send_command(CMODE_LIST_SET[cmode]) - async def async_select_source(self, source): + async def async_select_source(self, source: str) -> None: """Select input source.""" selected_source = INV_SOURCES[source] await self._projector.send_command(selected_source) - async def async_mute_volume(self, mute): + async def async_mute_volume(self, mute: bool) -> None: """Mute (true) or unmute (false) sound.""" await self._projector.send_command(MUTE) - async def async_volume_up(self): + async def async_volume_up(self) -> None: """Increase volume.""" await self._projector.send_command(VOL_UP) - async def async_volume_down(self): + async def async_volume_down(self) -> None: """Decrease volume.""" await self._projector.send_command(VOL_DOWN) - async def async_media_play(self): + async def async_media_play(self) -> None: """Play media via Epson.""" await self._projector.send_command(PLAY) - async def async_media_pause(self): + async def async_media_pause(self) -> None: """Pause media via Epson.""" await self._projector.send_command(PAUSE) - async def async_media_next_track(self): + async def async_media_next_track(self) -> None: """Skip to next.""" await self._projector.send_command(FAST) - async def async_media_previous_track(self): + async def async_media_previous_track(self) -> None: """Skip to previous.""" await self._projector.send_command(BACK) diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py index d19371c6104..3b31082f333 100644 --- a/homeassistant/components/epsonworkforce/sensor.py +++ b/homeassistant/components/epsonworkforce/sensor.py @@ -94,7 +94,9 @@ def setup_platform( class EpsonPrinterCartridge(SensorEntity): """Representation of a cartridge sensor.""" - def __init__(self, api, description: SensorEntityDescription): + def __init__( + self, api: EpsonPrinterAPI, description: SensorEntityDescription + ) -> None: """Initialize a cartridge sensor.""" self._api = api self.entity_description = description @@ -105,10 +107,10 @@ class EpsonPrinterCartridge(SensorEntity): return self._api.getSensorValue(self.entity_description.key) @property - def available(self): + def available(self) -> bool: """Could the device be accessed during the last update call.""" return self._api.available - def update(self): + def update(self) -> None: """Get the latest data from the Epson printer.""" self._api.update() diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index 412bb8eddeb..de75f04f91e 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import eq3bt as eq3 # pylint: disable=import-error import voluptuous as vol @@ -140,7 +141,7 @@ class EQ3BTSmartThermostat(ClimateEntity): """Return the temperature we try to reach.""" return self._thermostat.target_temperature - def set_temperature(self, **kwargs): + def set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return @@ -158,7 +159,7 @@ class EQ3BTSmartThermostat(ClimateEntity): """Return the list of available operation modes.""" return list(HA_TO_EQ_HVAC) - def set_hvac_mode(self, hvac_mode): + def set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set operation mode.""" self._thermostat.mode = HA_TO_EQ_HVAC[hvac_mode] @@ -206,13 +207,13 @@ class EQ3BTSmartThermostat(ClimateEntity): """Return the MAC address of the thermostat.""" return format_mac(self._mac) - def set_preset_mode(self, preset_mode): + def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if preset_mode == PRESET_NONE: self.set_hvac_mode(HVACMode.HEAT) self._thermostat.mode = HA_TO_EQ_PRESET[preset_mode] - def update(self): + def update(self) -> None: """Update the data from the thermostat.""" try: diff --git a/homeassistant/components/etherscan/sensor.py b/homeassistant/components/etherscan/sensor.py index 68c7307bba2..9f0c6f7eca9 100644 --- a/homeassistant/components/etherscan/sensor.py +++ b/homeassistant/components/etherscan/sensor.py @@ -83,7 +83,7 @@ class EtherscanSensor(SensorEntity): """Return the state attributes of the sensor.""" return {ATTR_ATTRIBUTION: ATTRIBUTION} - def update(self): + def update(self) -> None: """Get the latest state of the sensor.""" if self._token_address: diff --git a/homeassistant/components/eufy/switch.py b/homeassistant/components/eufy/switch.py index a7506daa552..a252f43a8ca 100644 --- a/homeassistant/components/eufy/switch.py +++ b/homeassistant/components/eufy/switch.py @@ -1,6 +1,8 @@ """Support for Eufy switches.""" from __future__ import annotations +from typing import Any + import lakeside from homeassistant.components.switch import SwitchEntity @@ -35,7 +37,7 @@ class EufySwitch(SwitchEntity): self._switch = lakeside.switch(self._address, self._code, self._type) self._switch.connect() - def update(self): + def update(self) -> None: """Synchronise state from the switch.""" self._switch.update() self._state = self._switch.power @@ -55,7 +57,7 @@ class EufySwitch(SwitchEntity): """Return true if device is on.""" return self._state - def turn_on(self, **kwargs): + def turn_on(self, **kwargs: Any) -> None: """Turn the specified switch on.""" try: self._switch.set_state(True) @@ -63,7 +65,7 @@ class EufySwitch(SwitchEntity): self._switch.connect() self._switch.set_state(power=True) - def turn_off(self, **kwargs): + def turn_off(self, **kwargs: Any) -> None: """Turn the specified switch off.""" try: self._switch.set_state(False) diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py index ab195d81530..13016ba6fe0 100644 --- a/homeassistant/components/everlights/light.py +++ b/homeassistant/components/everlights/light.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any import pyeverlights import voluptuous as vol @@ -155,11 +156,11 @@ class EverLightsLight(LightEntity): self._brightness = brightness self._effect = effect - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._api.clear_pattern(self._channel) - async def async_update(self): + async def async_update(self) -> None: """Synchronize state with control box.""" try: self._status = await self._api.get_status() diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index ff54cfbe4a6..86bbfc7d017 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -110,11 +110,11 @@ class EvoDHW(EvoChild, WaterHeaterEntity): self._evo_device.set_dhw_off(until=until) ) - async def async_turn_away_mode_on(self): + async def async_turn_away_mode_on(self) -> None: """Turn away mode on.""" await self._evo_broker.call_client_api(self._evo_device.set_dhw_off()) - async def async_turn_away_mode_off(self): + async def async_turn_away_mode_off(self) -> None: """Turn away mode off.""" await self._evo_broker.call_client_api(self._evo_device.set_dhw_auto()) From 0795d28ed5d3b981fdabc884ac2d6e1bdd10ae98 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sat, 20 Aug 2022 08:03:43 +0200 Subject: [PATCH 3492/3516] Remove name option from config_flow for P1 Monitor (#77046) --- homeassistant/components/p1_monitor/config_flow.py | 10 ++++------ homeassistant/components/p1_monitor/strings.json | 6 ++++-- .../components/p1_monitor/translations/en.json | 6 ++++-- tests/components/p1_monitor/test_config_flow.py | 8 ++++---- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/p1_monitor/config_flow.py b/homeassistant/components/p1_monitor/config_flow.py index 9e9d695f5e9..00b035aba7f 100644 --- a/homeassistant/components/p1_monitor/config_flow.py +++ b/homeassistant/components/p1_monitor/config_flow.py @@ -7,9 +7,10 @@ from p1monitor import P1Monitor, P1MonitorError import voluptuous as vol from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.const import CONF_HOST from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.selector import TextSelector from .const import DOMAIN @@ -37,7 +38,7 @@ class P1MonitorFlowHandler(ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" else: return self.async_create_entry( - title=user_input[CONF_NAME], + title="P1 Monitor", data={ CONF_HOST: user_input[CONF_HOST], }, @@ -47,10 +48,7 @@ class P1MonitorFlowHandler(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema( { - vol.Optional( - CONF_NAME, default=self.hass.config.location_name - ): str, - vol.Required(CONF_HOST): str, + vol.Required(CONF_HOST): TextSelector(), } ), errors=errors, diff --git a/homeassistant/components/p1_monitor/strings.json b/homeassistant/components/p1_monitor/strings.json index b088bf7adce..0c745554e9d 100644 --- a/homeassistant/components/p1_monitor/strings.json +++ b/homeassistant/components/p1_monitor/strings.json @@ -4,8 +4,10 @@ "user": { "description": "Set up P1 Monitor to integrate with Home Assistant.", "data": { - "host": "[%key:common::config_flow::data::host%]", - "name": "[%key:common::config_flow::data::name%]" + "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "The IP address or hostname of your P1 Monitor installation." } } }, diff --git a/homeassistant/components/p1_monitor/translations/en.json b/homeassistant/components/p1_monitor/translations/en.json index 4bd61c19bdc..394b6c0767b 100644 --- a/homeassistant/components/p1_monitor/translations/en.json +++ b/homeassistant/components/p1_monitor/translations/en.json @@ -6,8 +6,10 @@ "step": { "user": { "data": { - "host": "Host", - "name": "Name" + "host": "Host" + }, + "data_description": { + "host": "The IP address or hostname of your P1 Monitor installation." }, "description": "Set up P1 Monitor to integrate with Home Assistant." } diff --git a/tests/components/p1_monitor/test_config_flow.py b/tests/components/p1_monitor/test_config_flow.py index 13541e782b4..d7d0608e5b3 100644 --- a/tests/components/p1_monitor/test_config_flow.py +++ b/tests/components/p1_monitor/test_config_flow.py @@ -5,7 +5,7 @@ from p1monitor import P1MonitorError from homeassistant.components.p1_monitor.const import DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -27,11 +27,11 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_NAME: "Name", CONF_HOST: "example.com"}, + user_input={CONF_HOST: "example.com"}, ) assert result2.get("type") == FlowResultType.CREATE_ENTRY - assert result2.get("title") == "Name" + assert result2.get("title") == "P1 Monitor" assert result2.get("data") == {CONF_HOST: "example.com"} assert len(mock_setup_entry.mock_calls) == 1 @@ -47,7 +47,7 @@ async def test_api_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, - data={CONF_NAME: "Name", CONF_HOST: "example.com"}, + data={CONF_HOST: "example.com"}, ) assert result.get("type") == FlowResultType.FORM From 5cb79696d0218b66c391281e3e8c0bf91f74f756 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Sat, 20 Aug 2022 08:04:17 +0200 Subject: [PATCH 3493/3516] Use data description for Pure Energie integration (#77047) --- homeassistant/components/pure_energie/config_flow.py | 3 ++- homeassistant/components/pure_energie/strings.json | 3 +++ homeassistant/components/pure_energie/translations/en.json | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/pure_energie/config_flow.py b/homeassistant/components/pure_energie/config_flow.py index 2b1e20d645e..9e6c510c8b6 100644 --- a/homeassistant/components/pure_energie/config_flow.py +++ b/homeassistant/components/pure_energie/config_flow.py @@ -11,6 +11,7 @@ from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.selector import TextSelector from .const import DOMAIN @@ -50,7 +51,7 @@ class PureEnergieFlowHandler(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema( { - vol.Required(CONF_HOST): str, + vol.Required(CONF_HOST): TextSelector(), } ), errors=errors or {}, diff --git a/homeassistant/components/pure_energie/strings.json b/homeassistant/components/pure_energie/strings.json index 4f65d2d8be1..a76b4a001e6 100644 --- a/homeassistant/components/pure_energie/strings.json +++ b/homeassistant/components/pure_energie/strings.json @@ -5,6 +5,9 @@ "user": { "data": { "host": "[%key:common::config_flow::data::host%]" + }, + "data_description": { + "host": "The IP address or hostname of your Pure Energie Meter." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pure_energie/translations/en.json b/homeassistant/components/pure_energie/translations/en.json index 6773cf51478..0c4ebcc6e6e 100644 --- a/homeassistant/components/pure_energie/translations/en.json +++ b/homeassistant/components/pure_energie/translations/en.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "Host" + }, + "data_description": { + "host": "The IP address or hostname of your Pure Energie Meter." } }, "zeroconf_confirm": { From 828bf63ac2abdad569273550e00c62b4af5ecd82 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Aug 2022 20:14:47 -1000 Subject: [PATCH 3494/3516] Bump pySwitchbot to 0.18.12 (#77040) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index e70f467ae74..b4d7c69b315 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -2,7 +2,7 @@ "domain": "switchbot", "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": ["PySwitchbot==0.18.10"], + "requirements": ["PySwitchbot==0.18.12"], "config_flow": true, "dependencies": ["bluetooth"], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 561a7752b45..0d64b560978 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -37,7 +37,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.10 +PySwitchbot==0.18.12 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fe7c8f4c37d..85293c0fec4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -33,7 +33,7 @@ PyRMVtransport==0.3.3 PySocks==1.7.1 # homeassistant.components.switchbot -PySwitchbot==0.18.10 +PySwitchbot==0.18.12 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 52fbd50d3c41882200f36ade0621e9022d8b405f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Aug 2022 20:15:25 -1000 Subject: [PATCH 3495/3516] Bump yalexs_ble to 1.6.2 (#77056) --- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 1c97f687db6..b4e2f78906e 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.6.0"], + "requirements": ["yalexs-ble==1.6.2"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], diff --git a/requirements_all.txt b/requirements_all.txt index 0d64b560978..0925c02459c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2509,7 +2509,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.6.0 +yalexs-ble==1.6.2 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 85293c0fec4..15d02dc048f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1710,7 +1710,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.6.0 +yalexs-ble==1.6.2 # homeassistant.components.august yalexs==1.2.1 From fea0ec4d4dff867ca8e3fe5ba0888f91d6d81239 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Aug 2022 08:33:27 +0200 Subject: [PATCH 3496/3516] Improve type hints in vacuum entities (#76561) --- homeassistant/components/ecovacs/vacuum.py | 10 ++++---- homeassistant/components/sharkiq/vacuum.py | 26 +++++++++++++-------- homeassistant/components/template/vacuum.py | 15 ++++++------ 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index c380a760557..ca1a153b5ae 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -94,17 +94,17 @@ class EcovacsVacuum(VacuumEntity): return self.device.vacuum.get("did") @property - def is_on(self): + def is_on(self) -> bool: """Return true if vacuum is currently cleaning.""" return self.device.is_cleaning @property - def is_charging(self): + def is_charging(self) -> bool: """Return true if vacuum is currently charging.""" return self.device.is_charging @property - def status(self): + def status(self) -> str | None: """Return the status of the vacuum cleaner.""" return self.device.vacuum_status @@ -173,9 +173,9 @@ class EcovacsVacuum(VacuumEntity): self.device.run(sucks.VacBotCommand(command, params)) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the device-specific state attributes of this vacuum.""" - data = {} + data: dict[str, Any] = {} data[ATTR_ERROR] = self._error for key, val in self.device.components.items(): diff --git a/homeassistant/components/sharkiq/vacuum.py b/homeassistant/components/sharkiq/vacuum.py index a34c23012cf..48e82477809 100644 --- a/homeassistant/components/sharkiq/vacuum.py +++ b/homeassistant/components/sharkiq/vacuum.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Iterable +from typing import Any from sharkiq import OperatingModes, PowerModes, Properties, SharkIqVacuum @@ -89,11 +90,16 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum self._attr_unique_id = sharkiq.serial_number self._serial_number = sharkiq.serial_number - def clean_spot(self, **kwargs): + def clean_spot(self, **kwargs: Any) -> None: """Clean a spot. Not yet implemented.""" raise NotImplementedError() - def send_command(self, command, params=None, **kwargs): + def send_command( + self, + command: str, + params: dict[str, Any] | list[Any] | None = None, + **kwargs: Any, + ) -> None: """Send a command to the vacuum. Not yet implemented.""" raise NotImplementedError() @@ -146,7 +152,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum return self.sharkiq.get_property_value(Properties.RECHARGING_TO_RESUME) @property - def state(self): + def state(self) -> str | None: """ Get the current vacuum state. @@ -169,27 +175,27 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum """Get the current battery level.""" return self.sharkiq.get_property_value(Properties.BATTERY_CAPACITY) - async def async_return_to_base(self, **kwargs): + async def async_return_to_base(self, **kwargs: Any) -> None: """Have the device return to base.""" await self.sharkiq.async_set_operating_mode(OperatingModes.RETURN) await self.coordinator.async_refresh() - async def async_pause(self): + async def async_pause(self) -> None: """Pause the cleaning task.""" await self.sharkiq.async_set_operating_mode(OperatingModes.PAUSE) await self.coordinator.async_refresh() - async def async_start(self): + async def async_start(self) -> None: """Start the device.""" await self.sharkiq.async_set_operating_mode(OperatingModes.START) await self.coordinator.async_refresh() - async def async_stop(self, **kwargs): + async def async_stop(self, **kwargs: Any) -> None: """Stop the device.""" await self.sharkiq.async_set_operating_mode(OperatingModes.STOP) await self.coordinator.async_refresh() - async def async_locate(self, **kwargs): + async def async_locate(self, **kwargs: Any) -> None: """Cause the device to generate a loud chirp.""" await self.sharkiq.async_find_device() @@ -203,7 +209,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum fan_speed = k return fan_speed - async def async_set_fan_speed(self, fan_speed: str, **kwargs): + async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set the fan speed.""" await self.sharkiq.async_set_property_value( Properties.POWER_MODE, FAN_SPEEDS_MAP.get(fan_speed.capitalize()) @@ -227,7 +233,7 @@ class SharkVacuumEntity(CoordinatorEntity[SharkIqUpdateCoordinator], StateVacuum return self.sharkiq.get_property_value(Properties.LOW_LIGHT_MISSION) @property - def extra_state_attributes(self) -> dict: + def extra_state_attributes(self) -> dict[str, Any]: """Return a dictionary of device state attributes specific to sharkiq.""" data = { ATTR_ERROR_CODE: self.error_code, diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 0a74ee5c5fc..f95c2660164 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any import voluptuous as vol @@ -204,46 +205,46 @@ class TemplateVacuum(TemplateEntity, StateVacuumEntity): """Return the status of the vacuum cleaner.""" return self._state - async def async_start(self): + async def async_start(self) -> None: """Start or resume the cleaning task.""" await self.async_run_script(self._start_script, context=self._context) - async def async_pause(self): + async def async_pause(self) -> None: """Pause the cleaning task.""" if self._pause_script is None: return await self.async_run_script(self._pause_script, context=self._context) - async def async_stop(self, **kwargs): + async def async_stop(self, **kwargs: Any) -> None: """Stop the cleaning task.""" if self._stop_script is None: return await self.async_run_script(self._stop_script, context=self._context) - async def async_return_to_base(self, **kwargs): + async def async_return_to_base(self, **kwargs: Any) -> None: """Set the vacuum cleaner to return to the dock.""" if self._return_to_base_script is None: return await self.async_run_script(self._return_to_base_script, context=self._context) - async def async_clean_spot(self, **kwargs): + async def async_clean_spot(self, **kwargs: Any) -> None: """Perform a spot clean-up.""" if self._clean_spot_script is None: return await self.async_run_script(self._clean_spot_script, context=self._context) - async def async_locate(self, **kwargs): + async def async_locate(self, **kwargs: Any) -> None: """Locate the vacuum cleaner.""" if self._locate_script is None: return await self.async_run_script(self._locate_script, context=self._context) - async def async_set_fan_speed(self, fan_speed, **kwargs): + async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None: """Set fan speed.""" if self._set_fan_speed_script is None: return From 09ab07921a7ee0f4c58e41a1b6f7dec2bf42c5be Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Aug 2022 08:34:47 +0200 Subject: [PATCH 3497/3516] Improve type hint in compensation sensor entity (#77027) --- .../components/compensation/sensor.py | 86 +++++++------------ 1 file changed, 32 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/compensation/sensor.py b/homeassistant/components/compensation/sensor.py index 58666e0f3be..16226974120 100644 --- a/homeassistant/components/compensation/sensor.py +++ b/homeassistant/components/compensation/sensor.py @@ -2,6 +2,9 @@ from __future__ import annotations import logging +from typing import Any + +import numpy as np from homeassistant.components.sensor import SensorEntity from homeassistant.const import ( @@ -12,7 +15,7 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -42,11 +45,11 @@ async def async_setup_platform( if discovery_info is None: return - compensation = discovery_info[CONF_COMPENSATION] - conf = hass.data[DATA_COMPENSATION][compensation] + compensation: str = discovery_info[CONF_COMPENSATION] + conf: dict[str, Any] = hass.data[DATA_COMPENSATION][compensation] - source = conf[CONF_SOURCE] - attribute = conf.get(CONF_ATTRIBUTE) + source: str = conf[CONF_SOURCE] + attribute: str | None = conf.get(CONF_ATTRIBUTE) name = f"{DEFAULT_NAME} {source}" if attribute is not None: name = f"{name} {attribute}" @@ -69,26 +72,27 @@ async def async_setup_platform( class CompensationSensor(SensorEntity): """Representation of a Compensation sensor.""" + _attr_should_poll = False + def __init__( self, - unique_id, - name, - source, - attribute, - precision, - polynomial, - unit_of_measurement, - ): + unique_id: str | None, + name: str, + source: str, + attribute: str | None, + precision: int, + polynomial: np.poly1d, + unit_of_measurement: str | None, + ) -> None: """Initialize the Compensation sensor.""" self._source_entity_id = source self._precision = precision self._source_attribute = attribute - self._unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement self._poly = polynomial self._coefficients = polynomial.coefficients.tolist() - self._state = None - self._unique_id = unique_id - self._name = name + self._attr_unique_id = unique_id + self._attr_name = name async def async_added_to_hass(self) -> None: """Handle added to Hass.""" @@ -101,27 +105,7 @@ class CompensationSensor(SensorEntity): ) @property - def unique_id(self): - """Return the unique id of this sensor.""" - return self._unique_id - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def native_value(self): - """Return the state of the sensor.""" - return self._state - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the sensor.""" ret = { ATTR_SOURCE: self._source_entity_id, @@ -131,33 +115,27 @@ class CompensationSensor(SensorEntity): ret[ATTR_SOURCE_ATTRIBUTE] = self._source_attribute return ret - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit_of_measurement - @callback - def _async_compensation_sensor_state_listener(self, event): + def _async_compensation_sensor_state_listener(self, event: Event) -> None: """Handle sensor state changes.""" + new_state: State | None if (new_state := event.data.get("new_state")) is None: return - if self._unit_of_measurement is None and self._source_attribute is None: - self._unit_of_measurement = new_state.attributes.get( + if self.native_unit_of_measurement is None and self._source_attribute is None: + self._attr_native_unit_of_measurement = new_state.attributes.get( ATTR_UNIT_OF_MEASUREMENT ) + if self._source_attribute: + value = new_state.attributes.get(self._source_attribute) + else: + value = None if new_state.state == STATE_UNKNOWN else new_state.state try: - if self._source_attribute: - value = float(new_state.attributes.get(self._source_attribute)) - else: - value = ( - None if new_state.state == STATE_UNKNOWN else float(new_state.state) - ) - self._state = round(self._poly(value), self._precision) + self._attr_native_value = round(self._poly(float(value)), self._precision) except (ValueError, TypeError): - self._state = None + self._attr_native_value = None if self._source_attribute: _LOGGER.warning( "%s attribute %s is not numerical", From b88e71762de349faea2469823d4741bb6937b825 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Aug 2022 08:37:47 +0200 Subject: [PATCH 3498/3516] Improve type hint in cups sensor entity (#77030) --- homeassistant/components/cups/sensor.py | 97 +++++++++---------------- 1 file changed, 35 insertions(+), 62 deletions(-) diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index 893e92f546e..c642eb9112e 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta import importlib import logging +from typing import Any import voluptuous as vol @@ -62,10 +63,10 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the CUPS sensor.""" - host = config[CONF_HOST] - port = config[CONF_PORT] - printers = config[CONF_PRINTERS] - is_cups = config[CONF_IS_CUPS_SERVER] + host: str = config[CONF_HOST] + port: int = config[CONF_PORT] + printers: list[str] = config[CONF_PRINTERS] + is_cups: bool = config[CONF_IS_CUPS_SERVER] if is_cups: data = CupsData(host, port, None) @@ -73,6 +74,7 @@ def setup_platform( if data.available is False: _LOGGER.error("Unable to connect to CUPS server: %s:%s", host, port) raise PlatformNotReady() + assert data.printers is not None dev: list[SensorEntity] = [] for printer in printers: @@ -108,17 +110,14 @@ def setup_platform( class CupsSensor(SensorEntity): """Representation of a CUPS sensor.""" - def __init__(self, data, printer): + _attr_icon = ICON_PRINTER + + def __init__(self, data: CupsData, printer_name: str) -> None: """Initialize the CUPS sensor.""" self.data = data - self._name = printer - self._printer = None - self._available = False - - @property - def name(self): - """Return the name of the sensor.""" - return self._name + self._attr_name = printer_name + self._printer: dict[str, Any] | None = None + self._attr_available = False @property def native_value(self): @@ -129,16 +128,6 @@ class CupsSensor(SensorEntity): key = self._printer["printer-state"] return PRINTER_STATES.get(key, key) - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return ICON_PRINTER - @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" @@ -160,8 +149,10 @@ class CupsSensor(SensorEntity): def update(self) -> None: """Get the latest data and updates the states.""" self.data.update() - self._printer = self.data.printers.get(self._name) - self._available = self.data.available + assert self.name is not None + assert self.data.printers is not None + self._printer = self.data.printers.get(self.name) + self._attr_available = self.data.available class IPPSensor(SensorEntity): @@ -170,28 +161,20 @@ class IPPSensor(SensorEntity): This sensor represents the status of the printer. """ - def __init__(self, data, name): + _attr_icon = ICON_PRINTER + + def __init__(self, data: CupsData, printer_name: str) -> None: """Initialize the sensor.""" self.data = data - self._name = name + self._printer_name = printer_name self._attributes = None - self._available = False + self._attr_available = False @property def name(self): """Return the name of the sensor.""" return self._attributes["printer-make-and-model"] - @property - def icon(self): - """Return the icon to use in the frontend.""" - return ICON_PRINTER - - @property - def available(self): - """Return True if entity is available.""" - return self._available - @property def native_value(self): """Return the state of the sensor.""" @@ -237,8 +220,8 @@ class IPPSensor(SensorEntity): def update(self) -> None: """Fetch new state data for the sensor.""" self.data.update() - self._attributes = self.data.attributes.get(self._name) - self._available = self.data.available + self._attributes = self.data.attributes.get(self._printer_name) + self._attr_available = self.data.available class MarkerSensor(SensorEntity): @@ -247,24 +230,17 @@ class MarkerSensor(SensorEntity): This sensor represents the percentage of ink or toner. """ - def __init__(self, data, printer, name, is_cups): + _attr_icon = ICON_MARKER + _attr_native_unit_of_measurement = PERCENTAGE + + def __init__(self, data: CupsData, printer: str, name: str, is_cups: bool) -> None: """Initialize the sensor.""" self.data = data - self._name = name + self._attr_name = name self._printer = printer self._index = data.attributes[printer]["marker-names"].index(name) self._is_cups = is_cups - self._attributes = None - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return ICON_MARKER + self._attributes: dict[str, Any] | None = None @property def native_value(self): @@ -274,11 +250,6 @@ class MarkerSensor(SensorEntity): return self._attributes[self._printer]["marker-levels"][self._index] - @property - def native_unit_of_measurement(self): - """Return the unit of measurement.""" - return PERCENTAGE - @property def extra_state_attributes(self): """Return the state attributes of the sensor.""" @@ -318,17 +289,17 @@ class MarkerSensor(SensorEntity): class CupsData: """Get the latest data from CUPS and update the state.""" - def __init__(self, host, port, ipp_printers): + def __init__(self, host: str, port: int, ipp_printers: list[str] | None) -> None: """Initialize the data object.""" self._host = host self._port = port self._ipp_printers = ipp_printers self.is_cups = ipp_printers is None - self.printers = None - self.attributes = {} + self.printers: dict[str, dict[str, Any]] | None = None + self.attributes: dict[str, Any] = {} self.available = False - def update(self): + def update(self) -> None: """Get the latest data from CUPS.""" cups = importlib.import_module("cups") @@ -336,9 +307,11 @@ class CupsData: conn = cups.Connection(host=self._host, port=self._port) if self.is_cups: self.printers = conn.getPrinters() + assert self.printers is not None for printer in self.printers: self.attributes[printer] = conn.getPrinterAttributes(name=printer) else: + assert self._ipp_printers is not None for ipp_printer in self._ipp_printers: self.attributes[ipp_printer] = conn.getPrinterAttributes( uri=f"ipp://{self._host}:{self._port}/{ipp_printer}" From f329428c7f373e158f6ebb1295df4846ad1a5c02 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Aug 2022 08:38:59 +0200 Subject: [PATCH 3499/3516] Remove unused variable from directv media player (#77034) --- homeassistant/components/directv/media_player.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index affefcacd85..7d1434e9909 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -99,14 +99,13 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): self._last_update = None self._paused = None self._program = None - self._state = None - async def async_update(self): + async def async_update(self) -> None: """Retrieve latest state.""" - self._state = await self.dtv.state(self._address) - self._attr_available = self._state.available - self._is_standby = self._state.standby - self._program = self._state.program + state = await self.dtv.state(self._address) + self._attr_available = state.available + self._is_standby = state.standby + self._program = state.program if self._is_standby: self._attr_assumed_state = False @@ -118,7 +117,7 @@ class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerEntity): self._paused = self._last_position == self._program.position self._is_recorded = self._program.recorded self._last_position = self._program.position - self._last_update = self._state.at + self._last_update = state.at self._attr_assumed_state = self._is_recorded @property From 1edb68f8baf7b49b11d04c2b3f63aebf2d8a645a Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Aug 2022 08:39:53 +0200 Subject: [PATCH 3500/3516] Improve type hint in darksky sensor entity (#77035) --- homeassistant/components/darksky/sensor.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index 84a0b0f23f2..db79b82de53 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -663,8 +663,7 @@ class DarkSkySensor(SensorEntity): self.forecast_data = forecast_data self.forecast_day = forecast_day self.forecast_hour = forecast_hour - self._icon = None - self._unit_of_measurement = None + self._icon: str | None = None if forecast_day is not None: self._attr_name = f"{name} {description.name} {forecast_day}d" @@ -673,18 +672,13 @@ class DarkSkySensor(SensorEntity): else: self._attr_name = f"{name} {description.name}" - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - @property def unit_system(self): """Return the unit system of this entity.""" return self.forecast_data.unit_system @property - def entity_picture(self): + def entity_picture(self) -> str | None: """Return the entity picture to use in the frontend, if any.""" if self._icon is None or "summary" not in self.entity_description.key: return None @@ -694,13 +688,15 @@ class DarkSkySensor(SensorEntity): return None - def update_unit_of_measurement(self): + def update_unit_of_measurement(self) -> None: """Update units based on unit system.""" unit_key = MAP_UNIT_SYSTEM.get(self.unit_system, "si_unit") - self._unit_of_measurement = getattr(self.entity_description, unit_key) + self._attr_native_unit_of_measurement = getattr( + self.entity_description, unit_key + ) @property - def icon(self): + def icon(self) -> str | None: """Icon to use in the frontend, if any.""" if ( "summary" in self.entity_description.key @@ -710,7 +706,7 @@ class DarkSkySensor(SensorEntity): return self.entity_description.icon - def update(self): + def update(self) -> None: """Get the latest data from Dark Sky and updates the states.""" # Call the API for new forecast data. Each sensor will re-trigger this # same exact call, but that's fine. We cache results for a short period From c9fe1f44b89a38566fdcc22a60db03fc3aba9a17 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Aug 2022 08:40:51 +0200 Subject: [PATCH 3501/3516] Improve type hint in denon media player entity (#77036) --- homeassistant/components/denon/media_player.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index 4533af66927..2dd7a29e17e 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -84,7 +84,7 @@ def setup_platform( """Set up the Denon platform.""" denon = DenonDevice(config[CONF_NAME], config[CONF_HOST]) - if denon.update(): + if denon.do_update(): add_entities([denon]) @@ -161,8 +161,12 @@ class DenonDevice(MediaPlayerEntity): telnet.read_very_eager() # skip response telnet.close() - def update(self): + def update(self) -> None: """Get the latest details from the device.""" + self.do_update() + + def do_update(self) -> bool: + """Get the latest details from the device, as boolean.""" try: telnet = telnetlib.Telnet(self._host) except OSError: From 2c2e0cd4a0720485044f2feda0d95b8fd0da512d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Aug 2022 08:41:41 +0200 Subject: [PATCH 3502/3516] Improve type hint in daikin climate entity (#77037) --- homeassistant/components/daikin/climate.py | 28 +++++++--------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/daikin/climate.py b/homeassistant/components/daikin/climate.py index 271449a01cd..d7bfb8cf7a0 100644 --- a/homeassistant/components/daikin/climate.py +++ b/homeassistant/components/daikin/climate.py @@ -27,7 +27,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import DOMAIN as DAIKIN_DOMAIN +from . import DOMAIN as DAIKIN_DOMAIN, DaikinApi from .const import ( ATTR_INSIDE_TEMPERATURE, ATTR_OUTSIDE_TEMPERATURE, @@ -115,14 +115,17 @@ def format_target_temperature(target_temperature): class DaikinClimate(ClimateEntity): """Representation of a Daikin HVAC.""" - def __init__(self, api): + def __init__(self, api: DaikinApi) -> None: """Initialize the climate device.""" self._api = api + self._attr_hvac_modes = list(HA_STATE_TO_DAIKIN) + self._attr_fan_modes = self._api.device.fan_rate + self._attr_swing_modes = self._api.device.swing_modes self._list = { - ATTR_HVAC_MODE: list(HA_STATE_TO_DAIKIN), - ATTR_FAN_MODE: self._api.device.fan_rate, - ATTR_SWING_MODE: self._api.device.swing_modes, + ATTR_HVAC_MODE: self._attr_hvac_modes, + ATTR_FAN_MODE: self._attr_fan_modes, + ATTR_SWING_MODE: self._attr_swing_modes, } self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE @@ -219,11 +222,6 @@ class DaikinClimate(ClimateEntity): daikin_mode = self._api.device.represent(HA_ATTR_TO_DAIKIN[ATTR_HVAC_MODE])[1] return DAIKIN_TO_HA_STATE.get(daikin_mode, HVACMode.HEAT_COOL) - @property - def hvac_modes(self) -> list[HVACMode]: - """Return the list of available operation modes.""" - return self._list.get(ATTR_HVAC_MODE) - async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set HVAC mode.""" await self._set({ATTR_HVAC_MODE: hvac_mode}) @@ -237,11 +235,6 @@ class DaikinClimate(ClimateEntity): """Set fan mode.""" await self._set({ATTR_FAN_MODE: fan_mode}) - @property - def fan_modes(self): - """List of available fan modes.""" - return self._list.get(ATTR_FAN_MODE) - @property def swing_mode(self): """Return the fan setting.""" @@ -251,11 +244,6 @@ class DaikinClimate(ClimateEntity): """Set new target temperature.""" await self._set({ATTR_SWING_MODE: swing_mode}) - @property - def swing_modes(self): - """List of available swing modes.""" - return self._list.get(ATTR_SWING_MODE) - @property def preset_mode(self): """Return the preset_mode.""" From 9ac01b8c9be494cfcabc7b2a2a87a86cd288824b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Sat, 20 Aug 2022 11:27:01 +0200 Subject: [PATCH 3503/3516] Improve type hint in derivative sensor entity (#77038) --- homeassistant/components/derivative/sensor.py | 59 ++++++------------- 1 file changed, 19 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index 5337328bbe2..8e1934dcecf 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -1,7 +1,7 @@ """Numeric derivative of data coming from a source sensor over time.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta from decimal import Decimal, DecimalException import logging @@ -20,7 +20,7 @@ from homeassistant.const import ( TIME_MINUTES, TIME_SECONDS, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event @@ -133,6 +133,9 @@ async def async_setup_platform( class DerivativeSensor(RestoreEntity, SensorEntity): """Representation of an derivative sensor.""" + _attr_icon = ICON + _attr_should_poll = False + def __init__( self, *, @@ -150,19 +153,19 @@ class DerivativeSensor(RestoreEntity, SensorEntity): self._sensor_source_id = source_entity self._round_digits = round_digits self._state = 0 - self._state_list = ( - [] - ) # List of tuples with (timestamp_start, timestamp_end, derivative) + # List of tuples with (timestamp_start, timestamp_end, derivative) + self._state_list: list[tuple[datetime, datetime, Decimal]] = [] - self._name = name if name is not None else f"{source_entity} derivative" + self._attr_name = name if name is not None else f"{source_entity} derivative" + self._attr_extra_state_attributes = {ATTR_SOURCE_ID: source_entity} if unit_of_measurement is None: final_unit_prefix = "" if unit_prefix is None else unit_prefix self._unit_template = f"{final_unit_prefix}{{}}/{unit_time}" # we postpone the definition of unit_of_measurement to later - self._unit_of_measurement = None + self._attr_native_unit_of_measurement = None else: - self._unit_of_measurement = unit_of_measurement + self._attr_native_unit_of_measurement = unit_of_measurement self._unit_prefix = UNIT_PREFIXES[unit_prefix] self._unit_time = UNIT_TIME[unit_time] @@ -178,20 +181,21 @@ class DerivativeSensor(RestoreEntity, SensorEntity): _LOGGER.warning("Could not restore last state: %s", err) @callback - def calc_derivative(event): + def calc_derivative(event: Event) -> None: """Handle the sensor state changes.""" - old_state = event.data.get("old_state") - new_state = event.data.get("new_state") + old_state: State | None + new_state: State | None if ( - old_state is None + (old_state := event.data.get("old_state")) is None or old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) + or (new_state := event.data.get("new_state")) is None or new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE) ): return - if self._unit_of_measurement is None: + if self.native_unit_of_measurement is None: unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - self._unit_of_measurement = self._unit_template.format( + self._attr_native_unit_of_measurement = self._unit_template.format( "" if unit is None else unit ) @@ -242,7 +246,7 @@ class DerivativeSensor(RestoreEntity, SensorEntity): if elapsed_time > self._time_window: derivative = new_derivative else: - derivative = 0 + derivative = Decimal(0) for (start, end, value) in self._state_list: weight = calculate_weight(start, end, new_state.last_updated) derivative = derivative + (value * Decimal(weight)) @@ -256,32 +260,7 @@ class DerivativeSensor(RestoreEntity, SensorEntity): ) ) - @property - def name(self): - """Return the name of the sensor.""" - return self._name - @property def native_value(self): """Return the state of the sensor.""" return round(self._state, self._round_digits) - - @property - def native_unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit_of_measurement - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def extra_state_attributes(self): - """Return the state attributes of the sensor.""" - return {ATTR_SOURCE_ID: self._sensor_source_id} - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return ICON From 49957c752b9eebb596833ff30f9b3fac4f527c21 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 20 Aug 2022 19:06:35 +0200 Subject: [PATCH 3504/3516] Add coordinator and number platform to LaMetric (#76766) --- .coveragerc | 3 + homeassistant/components/lametric/__init__.py | 41 ++++----- homeassistant/components/lametric/const.py | 5 ++ .../components/lametric/coordinator.py | 38 ++++++++ homeassistant/components/lametric/entity.py | 29 ++++++ .../components/lametric/manifest.json | 2 +- homeassistant/components/lametric/number.py | 90 +++++++++++++++++++ 7 files changed, 183 insertions(+), 25 deletions(-) create mode 100644 homeassistant/components/lametric/coordinator.py create mode 100644 homeassistant/components/lametric/entity.py create mode 100644 homeassistant/components/lametric/number.py diff --git a/.coveragerc b/.coveragerc index 49cfbb3acc7..14773947be1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -640,7 +640,10 @@ omit = homeassistant/components/kwb/sensor.py homeassistant/components/lacrosse/sensor.py homeassistant/components/lametric/__init__.py + homeassistant/components/lametric/coordinator.py + homeassistant/components/lametric/entity.py homeassistant/components/lametric/notify.py + homeassistant/components/lametric/number.py homeassistant/components/lannouncer/notify.py homeassistant/components/lastfm/sensor.py homeassistant/components/launch_library/__init__.py diff --git a/homeassistant/components/lametric/__init__.py b/homeassistant/components/lametric/__init__.py index 2f89d88d79d..04f02a7f8f9 100644 --- a/homeassistant/components/lametric/__init__.py +++ b/homeassistant/components/lametric/__init__.py @@ -1,25 +1,17 @@ """Support for LaMetric time.""" -from demetriek import LaMetricConnectionError, LaMetricDevice import voluptuous as vol +from homeassistant.components import notify as hass_notify from homeassistant.components.repairs import IssueSeverity, async_create_issue from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_API_KEY, - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - CONF_HOST, - CONF_NAME, - Platform, -) +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_NAME, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery -from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN +from .const import DOMAIN, PLATFORMS +from .coordinator import LaMetricDataUpdateCoordinator CONFIG_SCHEMA = vol.Schema( vol.All( @@ -56,18 +48,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up LaMetric from a config entry.""" - lametric = LaMetricDevice( - host=entry.data[CONF_HOST], - api_key=entry.data[CONF_API_KEY], - session=async_get_clientsession(hass), - ) + coordinator = LaMetricDataUpdateCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() - try: - device = await lametric.device() - except LaMetricConnectionError as ex: - raise ConfigEntryNotReady("Cannot connect to LaMetric device") from ex - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = lametric + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # Set up notify platform, no entry support for notify component yet, # have to use discovery to load platform. @@ -76,8 +61,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass, Platform.NOTIFY, DOMAIN, - {CONF_NAME: device.name, "entry_id": entry.entry_id}, + {CONF_NAME: coordinator.data.name, "entry_id": entry.entry_id}, hass.data[DOMAIN]["hass_config"], ) ) return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload LaMetric config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + del hass.data[DOMAIN][entry.entry_id] + await hass_notify.async_reload(hass, DOMAIN) + return unload_ok diff --git a/homeassistant/components/lametric/const.py b/homeassistant/components/lametric/const.py index d357f678d9d..0fe4b3f21d8 100644 --- a/homeassistant/components/lametric/const.py +++ b/homeassistant/components/lametric/const.py @@ -1,11 +1,16 @@ """Constants for the LaMetric integration.""" +from datetime import timedelta import logging from typing import Final +from homeassistant.const import Platform + DOMAIN: Final = "lametric" +PLATFORMS = [Platform.NUMBER] LOGGER = logging.getLogger(__package__) +SCAN_INTERVAL = timedelta(seconds=30) CONF_CYCLES: Final = "cycles" CONF_ICON_TYPE: Final = "icon_type" diff --git a/homeassistant/components/lametric/coordinator.py b/homeassistant/components/lametric/coordinator.py new file mode 100644 index 00000000000..0a5e99e5668 --- /dev/null +++ b/homeassistant/components/lametric/coordinator.py @@ -0,0 +1,38 @@ +"""DataUpdateCoordinator for the LaMatric integration.""" +from __future__ import annotations + +from demetriek import Device, LaMetricDevice, LaMetricError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER, SCAN_INTERVAL + + +class LaMetricDataUpdateCoordinator(DataUpdateCoordinator[Device]): + """The LaMetric Data Update Coordinator.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the LaMatric coordinator.""" + self.config_entry = entry + self.lametric = LaMetricDevice( + host=entry.data[CONF_HOST], + api_key=entry.data[CONF_API_KEY], + session=async_get_clientsession(hass), + ) + + super().__init__(hass, LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) + + async def _async_update_data(self) -> Device: + """Fetch device information of the LaMetric device.""" + try: + return await self.lametric.device() + except LaMetricError as ex: + raise UpdateFailed( + "Could not fetch device information from LaMetric device" + ) from ex diff --git a/homeassistant/components/lametric/entity.py b/homeassistant/components/lametric/entity.py new file mode 100644 index 00000000000..1e31b2968af --- /dev/null +++ b/homeassistant/components/lametric/entity.py @@ -0,0 +1,29 @@ +"""Base entity for the LaMetric integration.""" +from __future__ import annotations + +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import LaMetricDataUpdateCoordinator + + +class LaMetricEntity(CoordinatorEntity[LaMetricDataUpdateCoordinator], Entity): + """Defines a LaMetric entity.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: LaMetricDataUpdateCoordinator) -> None: + """Initialize the LaMetric entity.""" + super().__init__(coordinator=coordinator) + self._attr_device_info = DeviceInfo( + connections={ + (CONNECTION_NETWORK_MAC, format_mac(coordinator.data.wifi.mac)) + }, + identifiers={(DOMAIN, coordinator.data.serial_number)}, + manufacturer="LaMetric Inc.", + model=coordinator.data.model, + name=coordinator.data.name, + sw_version=coordinator.data.os_version, + ) diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json index 1a40f962156..578906483fa 100644 --- a/homeassistant/components/lametric/manifest.json +++ b/homeassistant/components/lametric/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/lametric", "requirements": ["demetriek==0.2.2"], "codeowners": ["@robbiet480", "@frenck"], - "iot_class": "local_push", + "iot_class": "local_polling", "dependencies": ["application_credentials", "repairs"], "loggers": ["demetriek"], "config_flow": true, diff --git a/homeassistant/components/lametric/number.py b/homeassistant/components/lametric/number.py new file mode 100644 index 00000000000..c788eb3255e --- /dev/null +++ b/homeassistant/components/lametric/number.py @@ -0,0 +1,90 @@ +"""Support for LaMetric numbers.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any + +from demetriek import Device, LaMetricDevice + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import LaMetricDataUpdateCoordinator +from .entity import LaMetricEntity + + +@dataclass +class LaMetricEntityDescriptionMixin: + """Mixin values for LaMetric entities.""" + + value_fn: Callable[[Device], int | None] + set_value_fn: Callable[[LaMetricDevice, float], Awaitable[Any]] + + +@dataclass +class LaMetricNumberEntityDescription( + NumberEntityDescription, LaMetricEntityDescriptionMixin +): + """Class describing LaMetric number entities.""" + + +NUMBERS = [ + LaMetricNumberEntityDescription( + key="volume", + name="Volume", + icon="mdi:volume-high", + entity_category=EntityCategory.CONFIG, + native_step=1, + native_min_value=0, + native_max_value=100, + value_fn=lambda device: device.audio.volume, + set_value_fn=lambda api, volume: api.audio(volume=int(volume)), + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up LaMetric number based on a config entry.""" + coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + LaMetricNumberEntity( + coordinator=coordinator, + description=description, + ) + for description in NUMBERS + ) + + +class LaMetricNumberEntity(LaMetricEntity, NumberEntity): + """Representation of a LaMetric number.""" + + entity_description: LaMetricNumberEntityDescription + + def __init__( + self, + coordinator: LaMetricDataUpdateCoordinator, + description: LaMetricNumberEntityDescription, + ) -> None: + """Initiate LaMetric Number.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.data.serial_number}-{description.key}" + + @property + def native_value(self) -> int | None: + """Return the number value.""" + return self.entity_description.value_fn(self.coordinator.data) + + async def async_set_native_value(self, value: float) -> None: + """Change to new number value.""" + await self.entity_description.set_value_fn(self.coordinator.lametric, value) + await self.coordinator.async_request_refresh() From 18246bb8c88577bbeb99d83930079d2a5c908cb7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Aug 2022 07:22:42 -1000 Subject: [PATCH 3505/3516] Improve bluetooth logging when there are multiple adapters (#77007) --- homeassistant/components/bluetooth/__init__.py | 10 +++++++--- homeassistant/components/bluetooth/scanner.py | 9 +++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index e659cec60a0..83c1247e3e2 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -269,13 +269,17 @@ async def async_setup_entry( assert address is not None adapter = await manager.async_get_adapter_from_address(address) if adapter is None: - raise ConfigEntryNotReady(f"Bluetooth adapter with address {address} not found") + raise ConfigEntryNotReady( + f"Bluetooth adapter {adapter} with address {address} not found" + ) try: bleak_scanner = create_bleak_scanner(BluetoothScanningMode.ACTIVE, adapter) except RuntimeError as err: - raise ConfigEntryNotReady from err - scanner = HaScanner(hass, bleak_scanner, adapter) + raise ConfigEntryNotReady( + f"{adapter_human_name(adapter, address)}: {err}" + ) from err + scanner = HaScanner(hass, bleak_scanner, adapter, address) entry.async_on_unload(scanner.async_register_callback(manager.scanner_adv_received)) await scanner.async_start() entry.async_on_unload(manager.async_register_scanner(scanner)) diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index c3d45dbde95..6faada73e02 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -33,6 +33,7 @@ from .const import ( START_TIMEOUT, ) from .models import BluetoothScanningMode +from .util import adapter_human_name OriginalBleakScanner = bleak.BleakScanner MONOTONIC_TIME = time.monotonic @@ -76,7 +77,11 @@ class HaScanner: """ def __init__( - self, hass: HomeAssistant, scanner: bleak.BleakScanner, adapter: str | None + self, + hass: HomeAssistant, + scanner: bleak.BleakScanner, + adapter: str, + address: str, ) -> None: """Init bluetooth discovery.""" self.hass = hass @@ -89,7 +94,7 @@ class HaScanner: self._callbacks: list[ Callable[[BLEDevice, AdvertisementData, float, str], None] ] = [] - self.name = self.adapter or "default" + self.name = adapter_human_name(adapter, address) self.source = self.adapter or SOURCE_LOCAL @property From 453307e01ad8c865ce30767463f5b5ab7acef263 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 20 Aug 2022 19:30:38 +0200 Subject: [PATCH 3506/3516] Add attribute support to state selector (#77071) --- homeassistant/helpers/selector.py | 10 ++++++++-- tests/helpers/test_selector.py | 5 +++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index deb5cc7401c..9655f93ace5 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -741,10 +741,11 @@ class TargetSelectorConfig(TypedDict, total=False): device: SingleDeviceSelectorConfig -class StateSelectorConfig(TypedDict): +class StateSelectorConfig(TypedDict, total=False): """Class to represent an state selector config.""" entity_id: str + attribute: str @SELECTORS.register("state") @@ -753,7 +754,12 @@ class StateSelector(Selector): selector_type = "state" - CONFIG_SCHEMA = vol.Schema({vol.Required("entity_id"): cv.entity_id}) + CONFIG_SCHEMA = vol.Schema( + { + vol.Required("entity_id"): cv.entity_id, + vol.Optional("attribute"): str, + } + ) def __init__(self, config: StateSelectorConfig) -> None: """Instantiate a selector.""" diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 70e17058923..2d870a85f6f 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -302,6 +302,11 @@ def test_time_selector_schema(schema, valid_selections, invalid_selections): ("on", "armed"), (None, True, 1), ), + ( + {"entity_id": "sensor.abc", "attribute": "device_class"}, + ("temperature", "humidity"), + (None,), + ), ), ) def test_state_selector_schema(schema, valid_selections, invalid_selections): From 87be71ce6a111d5dc0ea6bf56ed79ea99dcbb4ef Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sat, 20 Aug 2022 20:18:27 +0200 Subject: [PATCH 3507/3516] Update pyotgw to 2.0.3 (#77073) --- homeassistant/components/opentherm_gw/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 02b1604ea11..97767ab8383 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -2,7 +2,7 @@ "domain": "opentherm_gw", "name": "OpenTherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", - "requirements": ["pyotgw==2.0.2"], + "requirements": ["pyotgw==2.0.3"], "codeowners": ["@mvn23"], "config_flow": true, "iot_class": "local_push", diff --git a/requirements_all.txt b/requirements_all.txt index 0925c02459c..b9820f94872 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1734,7 +1734,7 @@ pyopnsense==0.2.0 pyoppleio==1.0.5 # homeassistant.components.opentherm_gw -pyotgw==2.0.2 +pyotgw==2.0.3 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 15d02dc048f..5cb5b40a2c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1211,7 +1211,7 @@ pyopenuv==2022.04.0 pyopnsense==0.2.0 # homeassistant.components.opentherm_gw -pyotgw==2.0.2 +pyotgw==2.0.3 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp From 8b1713a691bd0c90824261be785f1998ad89f66f Mon Sep 17 00:00:00 2001 From: Kevin Addeman Date: Sat, 20 Aug 2022 16:56:19 -0400 Subject: [PATCH 3508/3516] Add support for non-serialized devices (light, switch, cover, fan in RA3 Zones) (#75323) Co-authored-by: J. Nick Koston --- CODEOWNERS | 4 +- .../components/lutron_caseta/__init__.py | 7 + .../components/lutron_caseta/manifest.json | 2 +- tests/components/lutron_caseta/__init__.py | 205 +++++++++++++++++- .../lutron_caseta/test_config_flow.py | 12 +- tests/components/lutron_caseta/test_cover.py | 19 ++ .../lutron_caseta/test_diagnostics.py | 62 +++++- tests/components/lutron_caseta/test_fan.py | 19 ++ tests/components/lutron_caseta/test_light.py | 27 +++ tests/components/lutron_caseta/test_switch.py | 18 ++ 10 files changed, 355 insertions(+), 20 deletions(-) create mode 100644 tests/components/lutron_caseta/test_cover.py create mode 100644 tests/components/lutron_caseta/test_fan.py create mode 100644 tests/components/lutron_caseta/test_light.py create mode 100644 tests/components/lutron_caseta/test_switch.py diff --git a/CODEOWNERS b/CODEOWNERS index 9a0c092eceb..2513e290230 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -630,8 +630,8 @@ build.json @home-assistant/supervisor /tests/components/luftdaten/ @fabaff @frenck /homeassistant/components/lupusec/ @majuss /homeassistant/components/lutron/ @JonGilmore -/homeassistant/components/lutron_caseta/ @swails @bdraco -/tests/components/lutron_caseta/ @swails @bdraco +/homeassistant/components/lutron_caseta/ @swails @bdraco @danaues +/tests/components/lutron_caseta/ @swails @bdraco @danaues /homeassistant/components/lyric/ @timmo001 /tests/components/lyric/ @timmo001 /homeassistant/components/mastodon/ @fabaff diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 5653504c98a..bcbaedeb8d1 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -387,6 +387,13 @@ class LutronCasetaDeviceUpdatableEntity(LutronCasetaDevice): self._device = self._smartbridge.get_device_by_id(self.device_id) _LOGGER.debug(self._device) + @property + def unique_id(self): + """Return a unique identifier if serial number is None.""" + if self.serial is None: + return f"{self._bridge_unique_id}_{self.device_id}" + return super().unique_id + def _id_to_identifier(lutron_id: str) -> tuple[str, str]: """Convert a lutron caseta identifier to a device identifier.""" diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index 206d8b51233..c80d0deb794 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -8,7 +8,7 @@ "homekit": { "models": ["Smart Bridge"] }, - "codeowners": ["@swails", "@bdraco"], + "codeowners": ["@swails", "@bdraco", "@danaues"], "iot_class": "local_push", "loggers": ["pylutron_caseta"] } diff --git a/tests/components/lutron_caseta/__init__.py b/tests/components/lutron_caseta/__init__.py index ace4066ae3b..91ddfe26fb5 100644 --- a/tests/components/lutron_caseta/__init__.py +++ b/tests/components/lutron_caseta/__init__.py @@ -1,6 +1,103 @@ """Tests for the Lutron Caseta integration.""" +from unittest.mock import patch + +from homeassistant.components.lutron_caseta import DOMAIN +from homeassistant.components.lutron_caseta.const import ( + CONF_CA_CERTS, + CONF_CERTFILE, + CONF_KEYFILE, +) +from homeassistant.const import CONF_HOST + +from tests.common import MockConfigEntry + +ENTRY_MOCK_DATA = { + CONF_HOST: "1.1.1.1", + CONF_KEYFILE: "", + CONF_CERTFILE: "", + CONF_CA_CERTS: "", +} + +_LEAP_DEVICE_TYPES = { + "light": [ + "WallDimmer", + "PlugInDimmer", + "InLineDimmer", + "SunnataDimmer", + "TempInWallPaddleDimmer", + "WallDimmerWithPreset", + "Dimmed", + ], + "switch": [ + "WallSwitch", + "OutdoorPlugInSwitch", + "PlugInSwitch", + "InLineSwitch", + "PowPakSwitch", + "SunnataSwitch", + "TempInWallPaddleSwitch", + "Switched", + ], + "fan": [ + "CasetaFanSpeedController", + "MaestroFanSpeedController", + "FanSpeed", + ], + "cover": [ + "SerenaHoneycombShade", + "SerenaRollerShade", + "TriathlonHoneycombShade", + "TriathlonRollerShade", + "QsWirelessShade", + "QsWirelessHorizontalSheerBlind", + "QsWirelessWoodBlind", + "RightDrawDrape", + "Shade", + "SerenaTiltOnlyWoodBlind", + ], + "sensor": [ + "Pico1Button", + "Pico2Button", + "Pico2ButtonRaiseLower", + "Pico3Button", + "Pico3ButtonRaiseLower", + "Pico4Button", + "Pico4ButtonScene", + "Pico4ButtonZone", + "Pico4Button2Group", + "FourGroupRemote", + "SeeTouchTabletopKeypad", + "SunnataKeypad", + "SunnataKeypad_2Button", + "SunnataKeypad_3ButtonRaiseLower", + "SunnataKeypad_4Button", + "SeeTouchHybridKeypad", + "SeeTouchInternational", + "SeeTouchKeypad", + "HomeownerKeypad", + "GrafikTHybridKeypad", + "AlisseKeypad", + "PalladiomKeypad", + ], +} + + +async def async_setup_integration(hass, mock_bridge) -> MockConfigEntry: + """Set up a mock bridge.""" + mock_entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_MOCK_DATA) + mock_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.lutron_caseta.Smartbridge.create_tls" + ) as create_tls: + create_tls.return_value = mock_bridge(can_connect=True) + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + return mock_entry + + class MockBridge: """Mock Lutron bridge that emulates configured connected status.""" @@ -12,26 +109,120 @@ class MockBridge: self.areas = {} self.occupancy_groups = {} self.scenes = self.get_scenes() - self.devices = self.get_devices() + self.devices = self.load_devices() async def connect(self): """Connect the mock bridge.""" if self.can_connect: self.is_currently_connected = True + def add_subscriber(self, device_id: str, callback_): + """Mock a listener to be notified of state changes.""" + def is_connected(self): """Return whether the mock bridge is connected.""" return self.is_currently_connected - def get_devices(self): - """Return devices on the bridge.""" + def load_devices(self): + """Load mock devices into self.devices.""" return { - "1": {"serial": 1234, "name": "bridge", "model": "model", "type": "type"} + "1": {"serial": 1234, "name": "bridge", "model": "model", "type": "type"}, + "801": { + "device_id": "801", + "current_state": 100, + "fan_speed": None, + "zone": "801", + "name": "Basement Bedroom_Main Lights", + "button_groups": None, + "type": "Dimmed", + "model": None, + "serial": None, + "tilt": None, + }, + "802": { + "device_id": "802", + "current_state": 100, + "fan_speed": None, + "zone": "802", + "name": "Basement Bedroom_Left Shade", + "button_groups": None, + "type": "SerenaRollerShade", + "model": None, + "serial": None, + "tilt": None, + }, + "803": { + "device_id": "803", + "current_state": 100, + "fan_speed": None, + "zone": "803", + "name": "Basement Bathroom_Exhaust Fan", + "button_groups": None, + "type": "Switched", + "model": None, + "serial": None, + "tilt": None, + }, + "804": { + "device_id": "804", + "current_state": 100, + "fan_speed": None, + "zone": "804", + "name": "Master Bedroom_Ceiling Fan", + "button_groups": None, + "type": "FanSpeed", + "model": None, + "serial": None, + "tilt": None, + }, + "901": { + "device_id": "901", + "current_state": 100, + "fan_speed": None, + "zone": "901", + "name": "Kitchen_Main Lights", + "button_groups": None, + "type": "WallDimmer", + "model": None, + "serial": 5442321, + "tilt": None, + }, } - def get_devices_by_domain(self, domain): - """Return devices on the bridge.""" - return {} + def get_devices(self) -> dict[str, dict]: + """Will return all known devices connected to the Smart Bridge.""" + return self.devices + + def get_devices_by_domain(self, domain: str) -> list[dict]: + """ + Return a list of devices for the given domain. + + :param domain: one of 'light', 'switch', 'cover', 'fan' or 'sensor' + :returns list of zero or more of the devices + """ + types = _LEAP_DEVICE_TYPES.get(domain, None) + + # return immediately if not a supported domain + if types is None: + return [] + + return self.get_devices_by_types(types) + + def get_devices_by_type(self, type_: str) -> list[dict]: + """ + Will return all devices of a given device type. + + :param type_: LEAP device type, e.g. WallSwitch + """ + return [device for device in self.devices.values() if device["type"] == type_] + + def get_devices_by_types(self, types: list[str]) -> list[dict]: + """ + Will return all devices for a list of given device types. + + :param types: list of LEAP device types such as WallSwitch, WallDimmer + """ + return [device for device in self.devices.values() if device["type"] in types] def get_scenes(self): """Return scenes on the bridge.""" diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index b5e8271d351..d1997051e26 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -20,7 +20,7 @@ from homeassistant.components.lutron_caseta.const import ( ) from homeassistant.const import CONF_HOST -from . import MockBridge +from . import ENTRY_MOCK_DATA, MockBridge from tests.common import MockConfigEntry @@ -151,13 +151,7 @@ async def test_bridge_invalid_ssl_error(hass): async def test_duplicate_bridge_import(hass): """Test that creating a bridge entry with a duplicate host errors.""" - entry_mock_data = { - CONF_HOST: "1.1.1.1", - CONF_KEYFILE: "", - CONF_CERTFILE: "", - CONF_CA_CERTS: "", - } - mock_entry = MockConfigEntry(domain=DOMAIN, data=entry_mock_data) + mock_entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_MOCK_DATA) mock_entry.add_to_hass(hass) with patch( @@ -168,7 +162,7 @@ async def test_duplicate_bridge_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data=entry_mock_data, + data=ENTRY_MOCK_DATA, ) assert result["type"] == data_entry_flow.FlowResultType.ABORT diff --git a/tests/components/lutron_caseta/test_cover.py b/tests/components/lutron_caseta/test_cover.py new file mode 100644 index 00000000000..ef5fc2a5228 --- /dev/null +++ b/tests/components/lutron_caseta/test_cover.py @@ -0,0 +1,19 @@ +"""Tests for the Lutron Caseta integration.""" + + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import MockBridge, async_setup_integration + + +async def test_cover_unique_id(hass: HomeAssistant) -> None: + """Test a light unique id.""" + await async_setup_integration(hass, MockBridge) + + cover_entity_id = "cover.basement_bedroom_left_shade" + + entity_registry = er.async_get(hass) + + # Assert that Caseta covers will have the bridge serial hash and the zone id as the uniqueID + assert entity_registry.async_get(cover_entity_id).unique_id == "000004d2_802" diff --git a/tests/components/lutron_caseta/test_diagnostics.py b/tests/components/lutron_caseta/test_diagnostics.py index 89fcb65df9d..42fc1dac5c1 100644 --- a/tests/components/lutron_caseta/test_diagnostics.py +++ b/tests/components/lutron_caseta/test_diagnostics.py @@ -48,7 +48,67 @@ async def test_diagnostics(hass, hass_client) -> None: "name": "bridge", "serial": 1234, "type": "type", - } + }, + "801": { + "device_id": "801", + "current_state": 100, + "fan_speed": None, + "zone": "801", + "name": "Basement Bedroom_Main Lights", + "button_groups": None, + "type": "Dimmed", + "model": None, + "serial": None, + "tilt": None, + }, + "802": { + "device_id": "802", + "current_state": 100, + "fan_speed": None, + "zone": "802", + "name": "Basement Bedroom_Left Shade", + "button_groups": None, + "type": "SerenaRollerShade", + "model": None, + "serial": None, + "tilt": None, + }, + "803": { + "device_id": "803", + "current_state": 100, + "fan_speed": None, + "zone": "803", + "name": "Basement Bathroom_Exhaust Fan", + "button_groups": None, + "type": "Switched", + "model": None, + "serial": None, + "tilt": None, + }, + "804": { + "device_id": "804", + "current_state": 100, + "fan_speed": None, + "zone": "804", + "name": "Master Bedroom_Ceiling Fan", + "button_groups": None, + "type": "FanSpeed", + "model": None, + "serial": None, + "tilt": None, + }, + "901": { + "device_id": "901", + "current_state": 100, + "fan_speed": None, + "zone": "901", + "name": "Kitchen_Main Lights", + "button_groups": None, + "type": "WallDimmer", + "model": None, + "serial": 5442321, + "tilt": None, + }, }, "occupancy_groups": {}, "scenes": {}, diff --git a/tests/components/lutron_caseta/test_fan.py b/tests/components/lutron_caseta/test_fan.py new file mode 100644 index 00000000000..f9c86cc9c58 --- /dev/null +++ b/tests/components/lutron_caseta/test_fan.py @@ -0,0 +1,19 @@ +"""Tests for the Lutron Caseta integration.""" + + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import MockBridge, async_setup_integration + + +async def test_fan_unique_id(hass: HomeAssistant) -> None: + """Test a light unique id.""" + await async_setup_integration(hass, MockBridge) + + fan_entity_id = "fan.master_bedroom_ceiling_fan" + + entity_registry = er.async_get(hass) + + # Assert that Caseta covers will have the bridge serial hash and the zone id as the uniqueID + assert entity_registry.async_get(fan_entity_id).unique_id == "000004d2_804" diff --git a/tests/components/lutron_caseta/test_light.py b/tests/components/lutron_caseta/test_light.py new file mode 100644 index 00000000000..6449ce04832 --- /dev/null +++ b/tests/components/lutron_caseta/test_light.py @@ -0,0 +1,27 @@ +"""Tests for the Lutron Caseta integration.""" + + +from homeassistant.const import STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import MockBridge, async_setup_integration + + +async def test_light_unique_id(hass: HomeAssistant) -> None: + """Test a light unique id.""" + await async_setup_integration(hass, MockBridge) + + ra3_entity_id = "light.basement_bedroom_main_lights" + caseta_entity_id = "light.kitchen_main_lights" + + entity_registry = er.async_get(hass) + + # Assert that RA3 lights will have the bridge serial hash and the zone id as the uniqueID + assert entity_registry.async_get(ra3_entity_id).unique_id == "000004d2_801" + + # Assert that Caseta lights will have the serial number as the uniqueID + assert entity_registry.async_get(caseta_entity_id).unique_id == "5442321" + + state = hass.states.get(ra3_entity_id) + assert state.state == STATE_ON diff --git a/tests/components/lutron_caseta/test_switch.py b/tests/components/lutron_caseta/test_switch.py new file mode 100644 index 00000000000..842aca94423 --- /dev/null +++ b/tests/components/lutron_caseta/test_switch.py @@ -0,0 +1,18 @@ +"""Tests for the Lutron Caseta integration.""" + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import MockBridge, async_setup_integration + + +async def test_switch_unique_id(hass: HomeAssistant) -> None: + """Test a light unique id.""" + await async_setup_integration(hass, MockBridge) + + switch_entity_id = "switch.basement_bathroom_exhaust_fan" + + entity_registry = er.async_get(hass) + + # Assert that Caseta covers will have the bridge serial hash and the zone id as the uniqueID + assert entity_registry.async_get(switch_entity_id).unique_id == "000004d2_803" From eb0828efdb672c4861125fa4c02933a3943ea911 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 20 Aug 2022 21:58:59 +0100 Subject: [PATCH 3509/3516] Dont rely on config flow to monitor homekit_controller c# changes (#76861) --- .../components/homekit_controller/__init__.py | 4 +- .../homekit_controller/config_flow.py | 18 ------- .../homekit_controller/connection.py | 24 +-------- .../homekit_controller/manifest.json | 2 +- .../components/homekit_controller/utils.py | 4 ++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/homekit_controller/common.py | 50 ++++++++----------- .../specific_devices/test_ecobee3.py | 7 ++- .../homekit_controller/test_config_flow.py | 4 +- .../homekit_controller/test_init.py | 2 + 11 files changed, 36 insertions(+), 83 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 3a5ba42848c..9b431b09c9e 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -21,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType from .config_flow import normalize_hkid from .connection import HKDevice from .const import ENTITY_MAP, KNOWN_DEVICES, TRIGGERS -from .storage import EntityMapStorage, async_get_entity_storage +from .storage import EntityMapStorage from .utils import async_get_controller _LOGGER = logging.getLogger(__name__) @@ -50,8 +50,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up for Homekit devices.""" - await async_get_entity_storage(hass) - await async_get_controller(hass) hass.data[KNOWN_DEVICES] = {} diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 2ccdd557a5b..62144077a94 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -24,7 +24,6 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import device_registry as dr -from .connection import HKDevice from .const import DOMAIN, KNOWN_DEVICES from .storage import async_get_entity_storage from .utils import async_get_controller @@ -253,17 +252,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): category = Categories(int(properties.get("ci", 0))) paired = not status_flags & 0x01 - # The configuration number increases every time the characteristic map - # needs updating. Some devices use a slightly off-spec name so handle - # both cases. - try: - config_num = int(properties["c#"]) - except KeyError: - _LOGGER.warning( - "HomeKit device %s: c# not exposed, in violation of spec", hkid - ) - config_num = None - # Set unique-id and error out if it's already configured existing_entry = await self.async_set_unique_id( normalized_hkid, raise_on_progress=False @@ -280,12 +268,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.hass.config_entries.async_update_entry( existing_entry, data={**existing_entry.data, **updated_ip_port} ) - conn: HKDevice = self.hass.data[KNOWN_DEVICES][hkid] - if config_num and conn.config_num != config_num: - _LOGGER.debug( - "HomeKit info %s: c# incremented, refreshing entities", hkid - ) - conn.async_notify_config_changed(config_num) return self.async_abort(reason="already_configured") _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index b2f3b3ae8e0..b4aaab5acf0 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -30,7 +30,6 @@ from .const import ( CHARACTERISTIC_PLATFORMS, CONTROLLER, DOMAIN, - ENTITY_MAP, HOMEKIT_ACCESSORY_DISPATCH, IDENTIFIER_ACCESSORY_ID, IDENTIFIER_LEGACY_ACCESSORY_ID, @@ -38,7 +37,6 @@ from .const import ( IDENTIFIER_SERIAL_NUMBER, ) from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry -from .storage import EntityMapStorage RETRY_INTERVAL = 60 # seconds MAX_POLL_FAILURES_TO_DECLARE_UNAVAILABLE = 3 @@ -182,14 +180,10 @@ class HKDevice: async def async_setup(self) -> None: """Prepare to use a paired HomeKit device in Home Assistant.""" - entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP] pairing = self.pairing transport = pairing.transport entry = self.config_entry - if cache := entity_storage.get_map(self.unique_id): - pairing.restore_accessories_state(cache["accessories"], cache["config_num"]) - # We need to force an update here to make sure we have # the latest values since the async_update we do in # async_process_entity_map will no values to poll yet @@ -203,7 +197,7 @@ class HKDevice: try: await self.pairing.async_populate_accessories_state(force_update=True) except AccessoryNotFoundError: - if transport != Transport.BLE or not cache: + if transport != Transport.BLE or not pairing.accessories: # BLE devices may sleep and we can't force a connection raise @@ -217,9 +211,6 @@ class HKDevice: await self.async_process_entity_map() - if not cache: - # If its missing from the cache, make sure we save it - self.async_save_entity_map() # If everything is up to date, we can create the entities # since we know the data is not stale. await self.async_add_new_entities() @@ -438,31 +429,18 @@ class HKDevice: self.config_entry, self.platforms ) - def async_notify_config_changed(self, config_num: int) -> None: - """Notify the pairing of a config change.""" - self.pairing.notify_config_changed(config_num) - def process_config_changed(self, config_num: int) -> None: """Handle a config change notification from the pairing.""" self.hass.async_create_task(self.async_update_new_accessories_state()) async def async_update_new_accessories_state(self) -> None: """Process a change in the pairings accessories state.""" - self.async_save_entity_map() await self.async_process_entity_map() if self.watchable_characteristics: await self.pairing.subscribe(self.watchable_characteristics) await self.async_update() await self.async_add_new_entities() - @callback - def async_save_entity_map(self) -> None: - """Save the entity map.""" - entity_storage: EntityMapStorage = self.hass.data[ENTITY_MAP] - entity_storage.async_create_or_update_map( - self.unique_id, self.config_num, self.entity_map.serialize() - ) - def add_accessory_factory(self, add_entities_cb) -> None: """Add a callback to run when discovering new entities for accessories.""" self.accessory_factories.append(add_entities_cb) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 3f8d7828236..143627fe7f0 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit==1.3.0"], + "requirements": ["aiohomekit==1.4.0"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], "dependencies": ["bluetooth", "zeroconf"], diff --git a/homeassistant/components/homekit_controller/utils.py b/homeassistant/components/homekit_controller/utils.py index 6e272067b54..b43f1ee05f7 100644 --- a/homeassistant/components/homekit_controller/utils.py +++ b/homeassistant/components/homekit_controller/utils.py @@ -8,6 +8,7 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant from .const import CONTROLLER +from .storage import async_get_entity_storage def folded_name(name: str) -> str: @@ -22,6 +23,8 @@ async def async_get_controller(hass: HomeAssistant) -> Controller: async_zeroconf_instance = await zeroconf.async_get_async_instance(hass) + char_cache = await async_get_entity_storage(hass) + # In theory another call to async_get_controller could have run while we were # trying to get the zeroconf instance. So we check again to make sure we # don't leak a Controller instance here. @@ -33,6 +36,7 @@ async def async_get_controller(hass: HomeAssistant) -> Controller: controller = Controller( async_zeroconf_instance=async_zeroconf_instance, bleak_scanner_instance=bleak_scanner_instance, # type: ignore[arg-type] + char_cache=char_cache, ) hass.data[CONTROLLER] = controller diff --git a/requirements_all.txt b/requirements_all.txt index b9820f94872..7901c1594a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.3.0 +aiohomekit==1.4.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5cb5b40a2c4..e6b9004cecf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ aioguardian==2022.07.0 aioharmony==0.2.9 # homeassistant.components.homekit_controller -aiohomekit==1.3.0 +aiohomekit==1.4.0 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 18367d28f63..fd543d55ffb 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -11,10 +11,9 @@ from unittest import mock from aiohomekit.model import Accessories, AccessoriesState, Accessory from aiohomekit.testing import FakeController, FakePairing +from aiohomekit.zeroconf import HomeKitService -from homeassistant.components import zeroconf from homeassistant.components.device_automation import DeviceAutomationType -from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import ( CONTROLLER, DOMAIN, @@ -22,6 +21,7 @@ from homeassistant.components.homekit_controller.const import ( IDENTIFIER_ACCESSORY_ID, IDENTIFIER_SERIAL_NUMBER, ) +from homeassistant.components.homekit_controller.utils import async_get_controller from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -175,12 +175,11 @@ async def setup_platform(hass): config = {"discovery": {}} with mock.patch( - "homeassistant.components.homekit_controller.utils.Controller" - ) as controller: - fake_controller = controller.return_value = FakeController() + "homeassistant.components.homekit_controller.utils.Controller", FakeController + ): await async_setup_component(hass, DOMAIN, config) - return fake_controller + return await async_get_controller(hass) async def setup_test_accessories(hass, accessories): @@ -228,31 +227,24 @@ async def device_config_changed(hass, accessories): pairing._accessories_state = AccessoriesState( accessories_obj, pairing.config_num + 1 ) - - discovery_info = zeroconf.ZeroconfServiceInfo( - host="127.0.0.1", - addresses=["127.0.0.1"], - hostname="mock_hostname", - name="TestDevice._hap._tcp.local.", - port=8080, - properties={ - "md": "TestDevice", - "id": "00:00:00:00:00:00", - "c#": "2", - "sf": "0", - }, - type="mock_type", + pairing._async_description_update( + HomeKitService( + name="TestDevice.local", + id="00:00:00:00:00:00", + model="", + config_num=2, + state_num=3, + feature_flags=0, + status_flags=0, + category=1, + protocol_version="1.0", + type="_hap._tcp.local.", + address="127.0.0.1", + addresses=["127.0.0.1"], + port=8080, + ) ) - # Config Flow will abort and notify us if the discovery event is of - # interest - in this case c# has incremented - flow = config_flow.HomekitControllerFlowHandler() - flow.hass = hass - flow.context = {} - result = await flow.async_step_zeroconf(discovery_info) - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - # Wait for services to reconfigure await hass.async_block_till_done() await hass.async_block_till_done() diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index 3c47195b442..4da2f572626 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -6,7 +6,7 @@ https://github.com/home-assistant/core/issues/15336 from unittest import mock -from aiohomekit import AccessoryDisconnectedError +from aiohomekit import AccessoryNotFoundError from aiohomekit.testing import FakePairing from homeassistant.components.climate.const import ( @@ -184,9 +184,8 @@ async def test_ecobee3_setup_connection_failure(hass): # Test that the connection fails during initial setup. # No entities should be created. - list_accessories = "list_accessories_and_characteristics" - with mock.patch.object(FakePairing, list_accessories) as laac: - laac.side_effect = AccessoryDisconnectedError("Connection failed") + with mock.patch.object(FakePairing, "async_populate_accessories_state") as laac: + laac.side_effect = AccessoryNotFoundError("Connection failed") # If there is no cached entity map and the accessory connection is # failing then we have to fail the config entry setup. diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 3f545be8931..5e2c8249560 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -1,7 +1,7 @@ """Tests for homekit_controller config flow.""" import asyncio import unittest.mock -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, patch import aiohomekit from aiohomekit.exceptions import AuthenticationError @@ -524,7 +524,6 @@ async def test_discovery_already_configured_update_csharp(hass, controller): entry.add_to_hass(hass) connection_mock = AsyncMock() - connection_mock.async_notify_config_changed = MagicMock() hass.data[KNOWN_DEVICES] = {"AA:BB:CC:DD:EE:FF": connection_mock} device = setup_mock_accessory(controller) @@ -547,7 +546,6 @@ async def test_discovery_already_configured_update_csharp(hass, controller): assert entry.data["AccessoryIP"] == discovery_info.host assert entry.data["AccessoryPort"] == discovery_info.port - assert connection_mock.async_notify_config_changed.call_count == 1 @pytest.mark.parametrize("exception,expected", PAIRING_START_ABORT_ERRORS) diff --git a/tests/components/homekit_controller/test_init.py b/tests/components/homekit_controller/test_init.py index 37d41fcf372..a91700f699c 100644 --- a/tests/components/homekit_controller/test_init.py +++ b/tests/components/homekit_controller/test_init.py @@ -119,6 +119,7 @@ async def test_offline_device_raises(hass, controller): nonlocal is_connected if not is_connected: raise AccessoryNotFoundError("any") + await super().async_populate_accessories_state(*args, **kwargs) async def get_characteristics(self, chars, *args, **kwargs): nonlocal is_connected @@ -173,6 +174,7 @@ async def test_ble_device_only_checks_is_available(hass, controller): nonlocal is_available if not is_available: raise AccessoryNotFoundError("any") + await super().async_populate_accessories_state(*args, **kwargs) async def get_characteristics(self, chars, *args, **kwargs): nonlocal is_available From ced8278e3222501dde7d769ea4b57aae75f62438 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Aug 2022 11:58:14 -1000 Subject: [PATCH 3510/3516] Auto recover when the Bluetooth adapter stops responding (#77043) --- .../components/bluetooth/__init__.py | 7 +- homeassistant/components/bluetooth/const.py | 29 ++- homeassistant/components/bluetooth/manager.py | 2 +- .../components/bluetooth/manifest.json | 6 +- homeassistant/components/bluetooth/scanner.py | 208 +++++++++++------ homeassistant/components/bluetooth/util.py | 10 + homeassistant/package_constraints.txt | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/bluetooth/test_scanner.py | 209 ++++++++++++++++++ 10 files changed, 406 insertions(+), 72 deletions(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 83c1247e3e2..f3b476a15ad 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -39,7 +39,7 @@ from .models import ( HaBleakScannerWrapper, ProcessAdvertisementCallback, ) -from .scanner import HaScanner, create_bleak_scanner +from .scanner import HaScanner, ScannerStartError, create_bleak_scanner from .util import adapter_human_name, adapter_unique_name, async_default_adapter if TYPE_CHECKING: @@ -281,7 +281,10 @@ async def async_setup_entry( ) from err scanner = HaScanner(hass, bleak_scanner, adapter, address) entry.async_on_unload(scanner.async_register_callback(manager.scanner_adv_received)) - await scanner.async_start() + try: + await scanner.async_start() + except ScannerStartError as err: + raise ConfigEntryNotReady from err entry.async_on_unload(manager.async_register_scanner(scanner)) await async_update_device(entry, manager, adapter, address) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = scanner diff --git a/homeassistant/components/bluetooth/const.py b/homeassistant/components/bluetooth/const.py index 0cd02bcbb8d..d6f7b515532 100644 --- a/homeassistant/components/bluetooth/const.py +++ b/homeassistant/components/bluetooth/const.py @@ -13,13 +13,12 @@ WINDOWS_DEFAULT_BLUETOOTH_ADAPTER = "bluetooth" MACOS_DEFAULT_BLUETOOTH_ADAPTER = "Core Bluetooth" UNIX_DEFAULT_BLUETOOTH_ADAPTER = "hci0" -DEFAULT_ADAPTERS = {MACOS_DEFAULT_BLUETOOTH_ADAPTER, UNIX_DEFAULT_BLUETOOTH_ADAPTER} - DEFAULT_ADAPTER_BY_PLATFORM = { "Windows": WINDOWS_DEFAULT_BLUETOOTH_ADAPTER, "Darwin": MACOS_DEFAULT_BLUETOOTH_ADAPTER, } + # Some operating systems hide the adapter address for privacy reasons (ex MacOS) DEFAULT_ADDRESS: Final = "00:00:00:00:00:00" @@ -28,9 +27,29 @@ SOURCE_LOCAL: Final = "local" DATA_MANAGER: Final = "bluetooth_manager" UNAVAILABLE_TRACK_SECONDS: Final = 60 * 5 -START_TIMEOUT = 12 -SCANNER_WATCHDOG_TIMEOUT: Final = 60 * 5 -SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=SCANNER_WATCHDOG_TIMEOUT) + +START_TIMEOUT = 15 + +MAX_DBUS_SETUP_SECONDS = 5 + +# Anything after 30s is considered stale, we have buffer +# for start timeouts and execution time +STALE_ADVERTISEMENT_SECONDS: Final = 30 + START_TIMEOUT + MAX_DBUS_SETUP_SECONDS + + +# We must recover before we hit the 180s mark +# where the device is removed from the stack +# or the devices will go unavailable. Since +# we only check every 30s, we need this number +# to be +# 180s Time when device is removed from stack +# - 30s check interval +# - 20s scanner restart time * 2 +# +SCANNER_WATCHDOG_TIMEOUT: Final = 110 +# How often to check if the scanner has reached +# the SCANNER_WATCHDOG_TIMEOUT without seeing anything +SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=30) class AdapterDetails(TypedDict, total=False): diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 0b588e71681..4b826efd6bd 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -23,6 +23,7 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( ADAPTER_ADDRESS, SOURCE_LOCAL, + STALE_ADVERTISEMENT_SECONDS, UNAVAILABLE_TRACK_SECONDS, AdapterDetails, ) @@ -46,7 +47,6 @@ FILTER_UUIDS: Final = "UUIDs" RSSI_SWITCH_THRESHOLD = 6 -STALE_ADVERTISEMENT_SECONDS = 180 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index ff99bd3d97d..21755723772 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -4,7 +4,11 @@ "documentation": "https://www.home-assistant.io/integrations/bluetooth", "dependencies": ["websocket_api"], "quality_scale": "internal", - "requirements": ["bleak==0.15.1", "bluetooth-adapters==0.2.0"], + "requirements": [ + "bleak==0.15.1", + "bluetooth-adapters==0.2.0", + "bluetooth-auto-recovery==0.2.1" + ], "codeowners": ["@bdraco"], "config_flow": true, "iot_class": "local_push" diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index 6faada73e02..ad6341910dd 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -5,6 +5,7 @@ import asyncio from collections.abc import Callable from datetime import datetime import logging +import platform import time import async_timeout @@ -21,19 +22,18 @@ from homeassistant.core import ( HomeAssistant, callback as hass_callback, ) -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.package import is_docker_env from .const import ( - DEFAULT_ADAPTERS, SCANNER_WATCHDOG_INTERVAL, SCANNER_WATCHDOG_TIMEOUT, SOURCE_LOCAL, START_TIMEOUT, ) from .models import BluetoothScanningMode -from .util import adapter_human_name +from .util import adapter_human_name, async_reset_adapter OriginalBleakScanner = bleak.BleakScanner MONOTONIC_TIME = time.monotonic @@ -44,6 +44,12 @@ _LOGGER = logging.getLogger(__name__) MONOTONIC_TIME = time.monotonic +NEED_RESET_ERRORS = [ + "org.bluez.Error.Failed", + "org.bluez.Error.InProgress", + "org.bluez.Error.NotReady", +] +START_ATTEMPTS = 2 SCANNING_MODE_TO_BLEAK = { BluetoothScanningMode.ACTIVE: "active", @@ -51,12 +57,17 @@ SCANNING_MODE_TO_BLEAK = { } +class ScannerStartError(HomeAssistantError): + """Error to indicate that the scanner failed to start.""" + + def create_bleak_scanner( scanning_mode: BluetoothScanningMode, adapter: str | None ) -> bleak.BleakScanner: """Create a Bleak scanner.""" scanner_kwargs = {"scanning_mode": SCANNING_MODE_TO_BLEAK[scanning_mode]} - if adapter and adapter not in DEFAULT_ADAPTERS: + # Only Linux supports multiple adapters + if adapter and platform.system() == "Linux": scanner_kwargs["adapter"] = adapter _LOGGER.debug("Initializing bluetooth scanner with %s", scanner_kwargs) try: @@ -66,7 +77,7 @@ def create_bleak_scanner( class HaScanner: - """Operate a BleakScanner. + """Operate and automatically recover a BleakScanner. Multiple BleakScanner can be used at the same time if there are multiple adapters. This is only useful @@ -91,6 +102,7 @@ class HaScanner: self._cancel_stop: CALLBACK_TYPE | None = None self._cancel_watchdog: CALLBACK_TYPE | None = None self._last_detection = 0.0 + self._start_time = 0.0 self._callbacks: list[ Callable[[BLEDevice, AdvertisementData, float, str], None] ] = [] @@ -129,9 +141,19 @@ class HaScanner: Currently this is used to feed the callbacks into the central manager. """ - self._last_detection = MONOTONIC_TIME() + callback_time = MONOTONIC_TIME() + if ( + advertisement_data.local_name + or advertisement_data.manufacturer_data + or advertisement_data.service_data + or advertisement_data.service_uuids + ): + # Don't count empty advertisements + # as the adapter is in a failure + # state if all the data is empty. + self._last_detection = callback_time for callback in self._callbacks: - callback(ble_device, advertisement_data, self._last_detection, self.source) + callback(ble_device, advertisement_data, callback_time, self.source) async def async_start(self) -> None: """Start bluetooth scanner.""" @@ -142,55 +164,85 @@ class HaScanner: async def _async_start(self) -> None: """Start bluetooth scanner under the lock.""" - try: - async with async_timeout.timeout(START_TIMEOUT): - await self.scanner.start() # type: ignore[no-untyped-call] - except InvalidMessageError as ex: + for attempt in range(START_ATTEMPTS): _LOGGER.debug( - "%s: Invalid DBus message received: %s", self.name, ex, exc_info=True - ) - raise ConfigEntryNotReady( - f"{self.name}: Invalid DBus message received: {ex}; try restarting `dbus`" - ) from ex - except BrokenPipeError as ex: - _LOGGER.debug( - "%s: DBus connection broken: %s", self.name, ex, exc_info=True - ) - if is_docker_env(): - raise ConfigEntryNotReady( - f"{self.name}: DBus connection broken: {ex}; try restarting `bluetooth`, `dbus`, and finally the docker container" - ) from ex - raise ConfigEntryNotReady( - f"{self.name}: DBus connection broken: {ex}; try restarting `bluetooth` and `dbus`" - ) from ex - except FileNotFoundError as ex: - _LOGGER.debug( - "%s: FileNotFoundError while starting bluetooth: %s", + "%s: Starting bluetooth discovery attempt: (%s/%s)", self.name, - ex, - exc_info=True, + attempt + 1, + START_ATTEMPTS, ) - if is_docker_env(): - raise ConfigEntryNotReady( - f"{self.name}: DBus service not found; docker config may be missing `-v /run/dbus:/run/dbus:ro`: {ex}" + try: + async with async_timeout.timeout(START_TIMEOUT): + await self.scanner.start() # type: ignore[no-untyped-call] + except InvalidMessageError as ex: + _LOGGER.debug( + "%s: Invalid DBus message received: %s", + self.name, + ex, + exc_info=True, + ) + raise ScannerStartError( + f"{self.name}: Invalid DBus message received: {ex}; " + "try restarting `dbus`" ) from ex - raise ConfigEntryNotReady( - f"{self.name}: DBus service not found; make sure the DBus socket is available to Home Assistant: {ex}" - ) from ex - except asyncio.TimeoutError as ex: - raise ConfigEntryNotReady( - f"{self.name}: Timed out starting Bluetooth after {START_TIMEOUT} seconds" - ) from ex - except BleakError as ex: - _LOGGER.debug( - "%s: BleakError while starting bluetooth: %s", - self.name, - ex, - exc_info=True, - ) - raise ConfigEntryNotReady( - f"{self.name}: Failed to start Bluetooth: {ex}" - ) from ex + except BrokenPipeError as ex: + _LOGGER.debug( + "%s: DBus connection broken: %s", self.name, ex, exc_info=True + ) + if is_docker_env(): + raise ScannerStartError( + f"{self.name}: DBus connection broken: {ex}; try restarting " + "`bluetooth`, `dbus`, and finally the docker container" + ) from ex + raise ScannerStartError( + f"{self.name}: DBus connection broken: {ex}; try restarting " + "`bluetooth` and `dbus`" + ) from ex + except FileNotFoundError as ex: + _LOGGER.debug( + "%s: FileNotFoundError while starting bluetooth: %s", + self.name, + ex, + exc_info=True, + ) + if is_docker_env(): + raise ScannerStartError( + f"{self.name}: DBus service not found; docker config may " + "be missing `-v /run/dbus:/run/dbus:ro`: {ex}" + ) from ex + raise ScannerStartError( + f"{self.name}: DBus service not found; make sure the DBus socket " + f"is available to Home Assistant: {ex}" + ) from ex + except asyncio.TimeoutError as ex: + if attempt == 0: + await self._async_reset_adapter() + continue + raise ScannerStartError( + f"{self.name}: Timed out starting Bluetooth after {START_TIMEOUT} seconds" + ) from ex + except BleakError as ex: + if attempt == 0: + error_str = str(ex) + if any( + needs_reset_error in error_str + for needs_reset_error in NEED_RESET_ERRORS + ): + await self._async_reset_adapter() + continue + _LOGGER.debug( + "%s: BleakError while starting bluetooth: %s", + self.name, + ex, + exc_info=True, + ) + raise ScannerStartError( + f"{self.name}: Failed to start Bluetooth: {ex}" + ) from ex + + # Everything is fine, break out of the loop + break + self._async_setup_scanner_watchdog() self._cancel_stop = self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_STOP, self._async_hass_stopping @@ -199,48 +251,78 @@ class HaScanner: @hass_callback def _async_setup_scanner_watchdog(self) -> None: """If Dbus gets restarted or updated, we need to restart the scanner.""" - self._last_detection = MONOTONIC_TIME() - self._cancel_watchdog = async_track_time_interval( - self.hass, self._async_scanner_watchdog, SCANNER_WATCHDOG_INTERVAL - ) + self._start_time = self._last_detection = MONOTONIC_TIME() + if not self._cancel_watchdog: + self._cancel_watchdog = async_track_time_interval( + self.hass, self._async_scanner_watchdog, SCANNER_WATCHDOG_INTERVAL + ) async def _async_scanner_watchdog(self, now: datetime) -> None: """Check if the scanner is running.""" time_since_last_detection = MONOTONIC_TIME() - self._last_detection + _LOGGER.debug( + "%s: Scanner watchdog time_since_last_detection: %s", + self.name, + time_since_last_detection, + ) if time_since_last_detection < SCANNER_WATCHDOG_TIMEOUT: return _LOGGER.info( - "%s: Bluetooth scanner has gone quiet for %s, restarting", + "%s: Bluetooth scanner has gone quiet for %ss, restarting", self.name, - SCANNER_WATCHDOG_INTERVAL, + SCANNER_WATCHDOG_TIMEOUT, ) async with self._start_stop_lock: - await self._async_stop() - await self._async_start() + # Stop the scanner but not the watchdog + # since we want to try again later if it's still quiet + await self._async_stop_scanner() + if self._start_time == self._last_detection or ( + time_since_last_detection + ) > (SCANNER_WATCHDOG_TIMEOUT + SCANNER_WATCHDOG_INTERVAL.total_seconds()): + await self._async_reset_adapter() + try: + await self._async_start() + except ScannerStartError as ex: + _LOGGER.error( + "%s: Failed to restart Bluetooth scanner: %s", + self.name, + ex, + exc_info=True, + ) async def _async_hass_stopping(self, event: Event) -> None: """Stop the Bluetooth integration at shutdown.""" self._cancel_stop = None await self.async_stop() + async def _async_reset_adapter(self) -> None: + """Reset the adapter.""" + _LOGGER.warning("%s: adapter stopped responding; executing reset", self.name) + result = await async_reset_adapter(self.adapter) + _LOGGER.info("%s: adapter reset result: %s", self.name, result) + async def async_stop(self) -> None: """Stop bluetooth scanner.""" async with self._start_stop_lock: await self._async_stop() async def _async_stop(self) -> None: - """Stop bluetooth discovery under the lock.""" - _LOGGER.debug("Stopping bluetooth discovery") + """Cancel watchdog and bluetooth discovery under the lock.""" if self._cancel_watchdog: self._cancel_watchdog() self._cancel_watchdog = None + await self._async_stop_scanner() + + async def _async_stop_scanner(self) -> None: + """Stop bluetooth discovery under the lock.""" if self._cancel_stop: self._cancel_stop() self._cancel_stop = None + _LOGGER.debug("%s: Stopping bluetooth discovery", self.name) try: await self.scanner.stop() # type: ignore[no-untyped-call] except BleakError as ex: # This is not fatal, and they may want to reload # the config entry to restart the scanner if they # change the bluetooth dongle. - _LOGGER.error("Error stopping scanner: %s", ex) + _LOGGER.error("%s: Error stopping scanner: %s", self.name, ex) diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 3133b2f210d..450c1812483 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -3,6 +3,8 @@ from __future__ import annotations import platform +from bluetooth_auto_recovery import recover_adapter + from homeassistant.core import callback from .const import ( @@ -65,3 +67,11 @@ def adapter_human_name(adapter: str, address: str) -> str: def adapter_unique_name(adapter: str, address: str) -> str: """Return a unique name for the adapter.""" return adapter if address == DEFAULT_ADDRESS else address + + +async def async_reset_adapter(adapter: str | None) -> bool | None: + """Reset the adapter.""" + if adapter and adapter.startswith("hci"): + adapter_id = int(adapter[3:]) + return await recover_adapter(adapter_id) + return False diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b6e5dbc4119..12febc7c2e3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,6 +12,7 @@ awesomeversion==22.6.0 bcrypt==3.1.7 bleak==0.15.1 bluetooth-adapters==0.2.0 +bluetooth-auto-recovery==0.2.1 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 diff --git a/requirements_all.txt b/requirements_all.txt index 7901c1594a2..50064f5af89 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -426,6 +426,9 @@ blockchain==1.4.4 # homeassistant.components.bluetooth bluetooth-adapters==0.2.0 +# homeassistant.components.bluetooth +bluetooth-auto-recovery==0.2.1 + # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6b9004cecf..81bc764f519 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -337,6 +337,9 @@ blinkpy==0.19.0 # homeassistant.components.bluetooth bluetooth-adapters==0.2.0 +# homeassistant.components.bluetooth +bluetooth-auto-recovery==0.2.1 + # homeassistant.components.bond bond-async==0.1.22 diff --git a/tests/components/bluetooth/test_scanner.py b/tests/components/bluetooth/test_scanner.py index bde1dbd1696..fc2e74144e7 100644 --- a/tests/components/bluetooth/test_scanner.py +++ b/tests/components/bluetooth/test_scanner.py @@ -8,12 +8,14 @@ from bleak.backends.scanner import ( BLEDevice, ) from dbus_next import InvalidMessageError +import pytest from homeassistant.components import bluetooth from homeassistant.components.bluetooth.const import ( SCANNER_WATCHDOG_INTERVAL, SCANNER_WATCHDOG_TIMEOUT, ) +from homeassistant.components.bluetooth.scanner import NEED_RESET_ERRORS from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.util import dt as dt_util @@ -140,6 +142,27 @@ async def test_invalid_dbus_message(hass, caplog, one_adapter): assert "dbus" in caplog.text +@pytest.mark.parametrize("error", NEED_RESET_ERRORS) +async def test_adapter_needs_reset_at_start(hass, caplog, one_adapter, error): + """Test we cycle the adapter when it needs a restart.""" + + with patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner.start", + side_effect=[BleakError(error), None], + ), patch( + "homeassistant.components.bluetooth.util.recover_adapter", return_value=True + ) as mock_recover_adapter: + await async_setup_with_one_adapter(hass) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_recover_adapter.mock_calls) == 1 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + async def test_recovery_from_dbus_restart(hass, one_adapter): """Test we can recover when DBus gets restarted out from under us.""" @@ -223,3 +246,189 @@ async def test_recovery_from_dbus_restart(hass, one_adapter): await hass.async_block_till_done() assert called_start == 2 + + +async def test_adapter_recovery(hass, one_adapter): + """Test we can recover when the adapter stops responding.""" + + called_start = 0 + called_stop = 0 + _callback = None + mock_discovered = [] + + class MockBleakScanner: + async def start(self, *args, **kwargs): + """Mock Start.""" + nonlocal called_start + called_start += 1 + + async def stop(self, *args, **kwargs): + """Mock Start.""" + nonlocal called_stop + called_stop += 1 + + @property + def discovered_devices(self): + """Mock discovered_devices.""" + nonlocal mock_discovered + return mock_discovered + + def register_detection_callback(self, callback: AdvertisementDataCallback): + """Mock Register Detection Callback.""" + nonlocal _callback + _callback = callback + + scanner = MockBleakScanner() + start_time_monotonic = 1000 + + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic, + ), patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner", + return_value=scanner, + ): + await async_setup_with_one_adapter(hass) + + assert called_start == 1 + + scanner = _get_manager() + mock_discovered = [MagicMock()] + + # Ensure we don't restart the scanner if we don't need to + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + 10, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert called_start == 1 + + # Ensure we don't restart the scanner if we don't need to + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + 20, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert called_start == 1 + + # We hit the timer with no detections, so we reset the adapter and restart the scanner + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + + SCANNER_WATCHDOG_TIMEOUT + + SCANNER_WATCHDOG_INTERVAL.total_seconds(), + ), patch( + "homeassistant.components.bluetooth.util.recover_adapter", return_value=True + ) as mock_recover_adapter: + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert len(mock_recover_adapter.mock_calls) == 1 + assert called_start == 2 + + +async def test_adapter_scanner_fails_to_start_first_time(hass, one_adapter): + """Test we can recover when the adapter stops responding and the first recovery fails.""" + + called_start = 0 + called_stop = 0 + _callback = None + mock_discovered = [] + + class MockBleakScanner: + async def start(self, *args, **kwargs): + """Mock Start.""" + nonlocal called_start + called_start += 1 + if called_start == 1: + return # Start ok the first time + if called_start < 4: + raise BleakError("Failed to start") + + async def stop(self, *args, **kwargs): + """Mock Start.""" + nonlocal called_stop + called_stop += 1 + + @property + def discovered_devices(self): + """Mock discovered_devices.""" + nonlocal mock_discovered + return mock_discovered + + def register_detection_callback(self, callback: AdvertisementDataCallback): + """Mock Register Detection Callback.""" + nonlocal _callback + _callback = callback + + scanner = MockBleakScanner() + start_time_monotonic = 1000 + + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic, + ), patch( + "homeassistant.components.bluetooth.scanner.OriginalBleakScanner", + return_value=scanner, + ): + await async_setup_with_one_adapter(hass) + + assert called_start == 1 + + scanner = _get_manager() + mock_discovered = [MagicMock()] + + # Ensure we don't restart the scanner if we don't need to + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + 10, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert called_start == 1 + + # Ensure we don't restart the scanner if we don't need to + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + 20, + ): + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert called_start == 1 + + # We hit the timer with no detections, so we reset the adapter and restart the scanner + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + + SCANNER_WATCHDOG_TIMEOUT + + SCANNER_WATCHDOG_INTERVAL.total_seconds(), + ), patch( + "homeassistant.components.bluetooth.util.recover_adapter", return_value=True + ) as mock_recover_adapter: + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert len(mock_recover_adapter.mock_calls) == 1 + assert called_start == 3 + + # We hit the timer again the previous start call failed, make sure + # we try again + with patch( + "homeassistant.components.bluetooth.scanner.MONOTONIC_TIME", + return_value=start_time_monotonic + + SCANNER_WATCHDOG_TIMEOUT + + SCANNER_WATCHDOG_INTERVAL.total_seconds(), + ), patch( + "homeassistant.components.bluetooth.util.recover_adapter", return_value=True + ) as mock_recover_adapter: + async_fire_time_changed(hass, dt_util.utcnow() + SCANNER_WATCHDOG_INTERVAL) + await hass.async_block_till_done() + + assert len(mock_recover_adapter.mock_calls) == 1 + assert called_start == 4 From 0bd49731347cd8ce8e037ff54327b19e4e96bd3f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Aug 2022 13:41:09 -1000 Subject: [PATCH 3511/3516] Bump bluetooth-auto-recovery to 0.2.2 (#77082) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 21755723772..29c534322f2 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "requirements": [ "bleak==0.15.1", "bluetooth-adapters==0.2.0", - "bluetooth-auto-recovery==0.2.1" + "bluetooth-auto-recovery==0.2.2" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 12febc7c2e3..412a1841394 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ awesomeversion==22.6.0 bcrypt==3.1.7 bleak==0.15.1 bluetooth-adapters==0.2.0 -bluetooth-auto-recovery==0.2.1 +bluetooth-auto-recovery==0.2.2 certifi>=2021.5.30 ciso8601==2.2.0 cryptography==37.0.4 diff --git a/requirements_all.txt b/requirements_all.txt index 50064f5af89..89ac7e7d77b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -427,7 +427,7 @@ blockchain==1.4.4 bluetooth-adapters==0.2.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.2.1 +bluetooth-auto-recovery==0.2.2 # homeassistant.components.bond bond-async==0.1.22 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81bc764f519..9504c1bd1ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -338,7 +338,7 @@ blinkpy==0.19.0 bluetooth-adapters==0.2.0 # homeassistant.components.bluetooth -bluetooth-auto-recovery==0.2.1 +bluetooth-auto-recovery==0.2.2 # homeassistant.components.bond bond-async==0.1.22 From 2d0b11f18e8aad8187e2065977947656bc79d90d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Aug 2022 13:41:25 -1000 Subject: [PATCH 3512/3516] Add a new constant for multiple bluetooth watchdog failure hits (#77081) --- homeassistant/components/bluetooth/scanner.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index ad6341910dd..96a0fe572c6 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -56,6 +56,15 @@ SCANNING_MODE_TO_BLEAK = { BluetoothScanningMode.PASSIVE: "passive", } +# The minimum number of seconds to know +# the adapter has not had advertisements +# and we already tried to restart the scanner +# without success when the first time the watch +# dog hit the failure path. +SCANNER_WATCHDOG_MULTIPLE = ( + SCANNER_WATCHDOG_TIMEOUT + SCANNER_WATCHDOG_INTERVAL.total_seconds() +) + class ScannerStartError(HomeAssistantError): """Error to indicate that the scanner failed to start.""" @@ -276,9 +285,13 @@ class HaScanner: # Stop the scanner but not the watchdog # since we want to try again later if it's still quiet await self._async_stop_scanner() - if self._start_time == self._last_detection or ( - time_since_last_detection - ) > (SCANNER_WATCHDOG_TIMEOUT + SCANNER_WATCHDOG_INTERVAL.total_seconds()): + # If there have not been any valid advertisements, + # or the watchdog has hit the failure path multiple times, + # do the reset. + if ( + self._start_time == self._last_detection + or time_since_last_detection > SCANNER_WATCHDOG_MULTIPLE + ): await self._async_reset_adapter() try: await self._async_start() From 296e52d91862fb15e9537d03f05ca9920b0ec146 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 21 Aug 2022 00:24:25 +0000 Subject: [PATCH 3513/3516] [ci skip] Translation update --- .../components/agent_dvr/translations/he.json | 2 +- .../android_ip_webcam/translations/pt-BR.json | 9 ++--- .../anthemav/translations/pt-BR.json | 6 ++-- .../components/awair/translations/pt-BR.json | 12 +++---- .../components/bluetooth/translations/fr.json | 6 ++++ .../components/bluetooth/translations/ru.json | 9 +++++ .../components/cloud/translations/cs.json | 1 + .../derivative/translations/cs.json | 9 +++++ .../components/escea/translations/pt-BR.json | 2 +- .../components/filesize/translations/cs.json | 7 ++++ .../flunearyou/translations/ja.json | 1 + .../fully_kiosk/translations/pt-BR.json | 6 ++-- .../components/google/translations/pt-BR.json | 4 +-- .../govee_ble/translations/pt-BR.json | 4 +-- .../inkbird/translations/pt-BR.json | 4 +-- .../components/isy994/translations/cs.json | 5 +++ .../components/jellyfin/translations/cs.json | 1 + .../justnimbus/translations/pt-BR.json | 4 +-- .../lacrosse_view/translations/pt-BR.json | 4 +-- .../lacrosse_view/translations/ru.json | 3 +- .../lametric/translations/pt-BR.json | 10 +++--- .../landisgyr_heat_meter/translations/ja.json | 3 ++ .../translations/pt-BR.json | 6 ++-- .../landisgyr_heat_meter/translations/ru.json | 23 +++++++++++++ .../lg_soundbar/translations/pt-BR.json | 6 ++-- .../life360/translations/pt-BR.json | 6 ++-- .../components/lifx/translations/pt-BR.json | 6 ++-- .../components/moat/translations/pt-BR.json | 4 +-- .../components/nest/translations/pt-BR.json | 2 +- .../nextdns/translations/pt-BR.json | 4 +-- .../components/nina/translations/pt-BR.json | 2 +- .../openexchangerates/translations/pt-BR.json | 10 +++--- .../opentherm_gw/translations/pt-BR.json | 2 +- .../p1_monitor/translations/de.json | 3 ++ .../p1_monitor/translations/en.json | 3 +- .../p1_monitor/translations/es.json | 3 ++ .../p1_monitor/translations/fr.json | 3 ++ .../p1_monitor/translations/ja.json | 3 ++ .../p1_monitor/translations/pt-BR.json | 3 ++ .../p1_monitor/translations/zh-Hant.json | 3 ++ .../components/point/translations/pt-BR.json | 2 +- .../pure_energie/translations/de.json | 3 ++ .../pure_energie/translations/es.json | 3 ++ .../pure_energie/translations/fr.json | 3 ++ .../pure_energie/translations/ja.json | 3 ++ .../pure_energie/translations/pt-BR.json | 3 ++ .../pure_energie/translations/zh-Hant.json | 3 ++ .../components/pushover/translations/ja.json | 1 + .../pushover/translations/pt-BR.json | 34 +++++++++++++++++++ .../components/pushover/translations/ru.json | 20 +++++++++++ .../qingping/translations/pt-BR.json | 4 +-- .../qnap_qsw/translations/pt-BR.json | 2 +- .../rhasspy/translations/pt-BR.json | 2 +- .../components/scrape/translations/cs.json | 11 ++++++ .../sensorpush/translations/pt-BR.json | 4 +-- .../simplepush/translations/pt-BR.json | 2 +- .../skybell/translations/pt-BR.json | 6 ++++ .../soundtouch/translations/pt-BR.json | 6 ++-- .../transmission/translations/pt-BR.json | 4 +-- .../tuya/translations/select.cs.json | 7 ++++ .../components/unifi/translations/cs.json | 18 ++++++---- .../components/uptime/translations/cs.json | 3 +- .../components/wallbox/translations/cs.json | 7 ++++ .../xiaomi_ble/translations/pt-BR.json | 4 +-- .../yalexs_ble/translations/pt-BR.json | 6 ++-- .../components/zha/translations/pt-BR.json | 3 ++ 66 files changed, 283 insertions(+), 85 deletions(-) create mode 100644 homeassistant/components/filesize/translations/cs.json create mode 100644 homeassistant/components/landisgyr_heat_meter/translations/ru.json create mode 100644 homeassistant/components/pushover/translations/pt-BR.json create mode 100644 homeassistant/components/pushover/translations/ru.json create mode 100644 homeassistant/components/scrape/translations/cs.json create mode 100644 homeassistant/components/tuya/translations/select.cs.json diff --git a/homeassistant/components/agent_dvr/translations/he.json b/homeassistant/components/agent_dvr/translations/he.json index d37b99a2f45..6105472b0d5 100644 --- a/homeassistant/components/agent_dvr/translations/he.json +++ b/homeassistant/components/agent_dvr/translations/he.json @@ -11,7 +11,7 @@ "user": { "data": { "host": "\u05de\u05d0\u05e8\u05d7", - "port": "\u05e4\u05d5\u05e8\u05d8" + "port": "\u05e4\u05ea\u05d7\u05d4" } } } diff --git a/homeassistant/components/android_ip_webcam/translations/pt-BR.json b/homeassistant/components/android_ip_webcam/translations/pt-BR.json index 95b5ffa7166..6ddbbaf40db 100644 --- a/homeassistant/components/android_ip_webcam/translations/pt-BR.json +++ b/homeassistant/components/android_ip_webcam/translations/pt-BR.json @@ -1,18 +1,19 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falhou ao conectar" + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { "user": { "data": { - "host": "Host", + "host": "Nome do host", "password": "Senha", "port": "Porta", - "username": "Nome de usu\u00e1rio" + "username": "Usu\u00e1rio" } } } diff --git a/homeassistant/components/anthemav/translations/pt-BR.json b/homeassistant/components/anthemav/translations/pt-BR.json index 5a6038bb480..dbfe4b35801 100644 --- a/homeassistant/components/anthemav/translations/pt-BR.json +++ b/homeassistant/components/anthemav/translations/pt-BR.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "cannot_receive_deviceinfo": "Falha ao recuperar o endere\u00e7o MAC. Verifique se o dispositivo est\u00e1 ligado" }, "step": { "user": { "data": { - "host": "Host", + "host": "Nome do host", "port": "Porta" } } diff --git a/homeassistant/components/awair/translations/pt-BR.json b/homeassistant/components/awair/translations/pt-BR.json index 20d2c104b60..109f123f9e2 100644 --- a/homeassistant/components/awair/translations/pt-BR.json +++ b/homeassistant/components/awair/translations/pt-BR.json @@ -2,23 +2,23 @@ "config": { "abort": { "already_configured": "A conta j\u00e1 foi configurada", - "already_configured_account": "A conta j\u00e1 est\u00e1 configurada", - "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_configured_account": "A conta j\u00e1 foi configurada", + "already_configured_device": "Dispositivo j\u00e1 est\u00e1 configurado", "no_devices_found": "Nenhum dispositivo encontrado na rede", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", - "unreachable": "Falhou ao conectar" + "unreachable": "Falha ao conectar" }, "error": { "invalid_access_token": "Token de acesso inv\u00e1lido", "unknown": "Erro inesperado", - "unreachable": "Falhou ao conectar" + "unreachable": "Falha ao conectar" }, "flow_title": "{model} ({device_id})", "step": { "cloud": { "data": { "access_token": "Token de acesso", - "email": "E-mail" + "email": "Email" }, "description": "Voc\u00ea deve se registrar para um token de acesso de desenvolvedor Awair em: {url}" }, @@ -47,7 +47,7 @@ "reauth_confirm": { "data": { "access_token": "Token de acesso", - "email": "E-mail" + "email": "Email" }, "description": "Insira novamente seu token de acesso de desenvolvedor Awair." }, diff --git a/homeassistant/components/bluetooth/translations/fr.json b/homeassistant/components/bluetooth/translations/fr.json index 80a0ac6ea3f..da430eedb0e 100644 --- a/homeassistant/components/bluetooth/translations/fr.json +++ b/homeassistant/components/bluetooth/translations/fr.json @@ -11,6 +11,12 @@ "enable_bluetooth": { "description": "Voulez-vous configurer le Bluetooth\u00a0?" }, + "multiple_adapters": { + "data": { + "adapter": "Adaptateur" + }, + "description": "S\u00e9lectionner un adaptateur Bluetooth \u00e0 configurer" + }, "user": { "data": { "address": "Appareil" diff --git a/homeassistant/components/bluetooth/translations/ru.json b/homeassistant/components/bluetooth/translations/ru.json index 802470d7c29..17108371409 100644 --- a/homeassistant/components/bluetooth/translations/ru.json +++ b/homeassistant/components/bluetooth/translations/ru.json @@ -12,6 +12,15 @@ "enable_bluetooth": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Bluetooth?" }, + "multiple_adapters": { + "data": { + "adapter": "\u0410\u0434\u0430\u043f\u0442\u0435\u0440" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0430\u0434\u0430\u043f\u0442\u0435\u0440 Bluetooth \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + }, + "single_adapter": { + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Bluetooth-\u0430\u0434\u0430\u043f\u0442\u0435\u0440 {name}?" + }, "user": { "data": { "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" diff --git a/homeassistant/components/cloud/translations/cs.json b/homeassistant/components/cloud/translations/cs.json index e6cf308e370..ac8af2ac812 100644 --- a/homeassistant/components/cloud/translations/cs.json +++ b/homeassistant/components/cloud/translations/cs.json @@ -10,6 +10,7 @@ "relayer_connected": "Relayer p\u0159ipojen", "remote_connected": "Vzd\u00e1len\u00e1 spr\u00e1va p\u0159ipojena", "remote_enabled": "Vzd\u00e1len\u00e1 spr\u00e1va povolena", + "remote_server": "Vzd\u00e1len\u00fd server", "subscription_expiration": "Platnost p\u0159edplatn\u00e9ho" } } diff --git a/homeassistant/components/derivative/translations/cs.json b/homeassistant/components/derivative/translations/cs.json index ee8ee0e6d86..087490dc949 100644 --- a/homeassistant/components/derivative/translations/cs.json +++ b/homeassistant/components/derivative/translations/cs.json @@ -1,4 +1,13 @@ { + "config": { + "step": { + "user": { + "data": { + "time_window": "\u010casov\u00e9 okno" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/escea/translations/pt-BR.json b/homeassistant/components/escea/translations/pt-BR.json index 5bcd3a25634..17aa2f1d1f7 100644 --- a/homeassistant/components/escea/translations/pt-BR.json +++ b/homeassistant/components/escea/translations/pt-BR.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Nenhum dispositivo encontrado na rede", - "single_instance_allowed": "J\u00e1 foi configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "confirm": { diff --git a/homeassistant/components/filesize/translations/cs.json b/homeassistant/components/filesize/translations/cs.json new file mode 100644 index 00000000000..1c38c1deb5d --- /dev/null +++ b/homeassistant/components/filesize/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "not_valid": "Cesta nen\u00ed platn\u00e1" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/ja.json b/homeassistant/components/flunearyou/translations/ja.json index 116800656a6..06cf83e27be 100644 --- a/homeassistant/components/flunearyou/translations/ja.json +++ b/homeassistant/components/flunearyou/translations/ja.json @@ -22,6 +22,7 @@ "fix_flow": { "step": { "confirm": { + "description": "Flu Near You\u3068\u306e\u7d71\u5408\u306b\u5fc5\u8981\u306a\u5916\u90e8\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u304c\u4f7f\u7528\u3067\u304d\u306a\u304f\u306a\u3063\u305f\u305f\u3081\u3001\u7d71\u5408\u306f\u6a5f\u80fd\u3057\u306a\u304f\u306a\u308a\u307e\u3057\u305f\u3002\n\nSUBMIT\u3092\u62bc\u3057\u3066\u3001Flu Near You\u3092Home Assistant\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u304b\u3089\u524a\u9664\u3057\u307e\u3059\u3002", "title": "\u8fd1\u304f\u306eFlu Near You\u3092\u524a\u9664" } } diff --git a/homeassistant/components/fully_kiosk/translations/pt-BR.json b/homeassistant/components/fully_kiosk/translations/pt-BR.json index 172933953e8..2649409ede7 100644 --- a/homeassistant/components/fully_kiosk/translations/pt-BR.json +++ b/homeassistant/components/fully_kiosk/translations/pt-BR.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "already_configured": "A conta j\u00e1 est\u00e1 configurada" + "already_configured": "A conta j\u00e1 foi configurada" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { "user": { "data": { - "host": "Host", + "host": "Nome do host", "password": "Senha" } } diff --git a/homeassistant/components/google/translations/pt-BR.json b/homeassistant/components/google/translations/pt-BR.json index 0b115c46423..709737dbe2d 100644 --- a/homeassistant/components/google/translations/pt-BR.json +++ b/homeassistant/components/google/translations/pt-BR.json @@ -6,13 +6,13 @@ "abort": { "already_configured": "A conta j\u00e1 foi configurada", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", - "cannot_connect": "Falha ao se conectar", + "cannot_connect": "Falha ao conectar", "code_expired": "O c\u00f3digo de autentica\u00e7\u00e3o expirou ou a configura\u00e7\u00e3o da credencial \u00e9 inv\u00e1lida. Tente novamente.", "invalid_access_token": "Token de acesso inv\u00e1lido", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "oauth_error": "Dados de token recebidos inv\u00e1lidos.", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", - "timeout_connect": "Tempo limite estabelecendo conex\u00e3o" + "timeout_connect": "Tempo limite para estabelecer conex\u00e3o atingido" }, "create_entry": { "default": "Autenticado com sucesso" diff --git a/homeassistant/components/govee_ble/translations/pt-BR.json b/homeassistant/components/govee_ble/translations/pt-BR.json index 2067d7f9312..3f93e65c087 100644 --- a/homeassistant/components/govee_ble/translations/pt-BR.json +++ b/homeassistant/components/govee_ble/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede" }, "flow_title": "{name}", diff --git a/homeassistant/components/inkbird/translations/pt-BR.json b/homeassistant/components/inkbird/translations/pt-BR.json index 2067d7f9312..3f93e65c087 100644 --- a/homeassistant/components/inkbird/translations/pt-BR.json +++ b/homeassistant/components/inkbird/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede" }, "flow_title": "{name}", diff --git a/homeassistant/components/isy994/translations/cs.json b/homeassistant/components/isy994/translations/cs.json index ac6773f09e1..d165050bf77 100644 --- a/homeassistant/components/isy994/translations/cs.json +++ b/homeassistant/components/isy994/translations/cs.json @@ -11,6 +11,11 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + } + }, "user": { "data": { "host": "URL", diff --git a/homeassistant/components/jellyfin/translations/cs.json b/homeassistant/components/jellyfin/translations/cs.json index c9a6f8f2462..5d03904568e 100644 --- a/homeassistant/components/jellyfin/translations/cs.json +++ b/homeassistant/components/jellyfin/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" } diff --git a/homeassistant/components/justnimbus/translations/pt-BR.json b/homeassistant/components/justnimbus/translations/pt-BR.json index c6fec98d719..7774e424ac9 100644 --- a/homeassistant/components/justnimbus/translations/pt-BR.json +++ b/homeassistant/components/justnimbus/translations/pt-BR.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, diff --git a/homeassistant/components/lacrosse_view/translations/pt-BR.json b/homeassistant/components/lacrosse_view/translations/pt-BR.json index 89f951e857b..b977e29baea 100644 --- a/homeassistant/components/lacrosse_view/translations/pt-BR.json +++ b/homeassistant/components/lacrosse_view/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { @@ -13,7 +13,7 @@ "user": { "data": { "password": "Senha", - "username": "Nome de usu\u00e1rio" + "username": "Usu\u00e1rio" } } } diff --git a/homeassistant/components/lacrosse_view/translations/ru.json b/homeassistant/components/lacrosse_view/translations/ru.json index 931b4c32274..c2730f9f07f 100644 --- a/homeassistant/components/lacrosse_view/translations/ru.json +++ b/homeassistant/components/lacrosse_view/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", diff --git a/homeassistant/components/lametric/translations/pt-BR.json b/homeassistant/components/lametric/translations/pt-BR.json index 4153b06e94d..7349baeb8dc 100644 --- a/homeassistant/components/lametric/translations/pt-BR.json +++ b/homeassistant/components/lametric/translations/pt-BR.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", "invalid_discovery_info": "Informa\u00e7\u00f5es de descoberta inv\u00e1lidas recebidas", "link_local_address": "Endere\u00e7os locais de links n\u00e3o s\u00e3o suportados", "missing_configuration": "A integra\u00e7\u00e3o LaMetric n\u00e3o est\u00e1 configurada. Por favor, siga a documenta\u00e7\u00e3o.", "no_devices": "O usu\u00e1rio autorizado n\u00e3o possui dispositivos LaMetric", - "no_url_available": "Nenhuma URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre este erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})" + "no_url_available": "N\u00e3o h\u00e1 URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a se\u00e7\u00e3o de ajuda]({docs_url})" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "unknown": "Erro inesperado" }, "step": { @@ -23,8 +23,8 @@ }, "manual_entry": { "data": { - "api_key": "Chave API", - "host": "Host" + "api_key": "Chave da API", + "host": "Nome do host" }, "data_description": { "api_key": "Voc\u00ea pode encontrar essa chave de API em [p\u00e1gina de dispositivos em sua conta de desenvolvedor LaMetric](https://developer.lametric.com/user/devices).", diff --git a/homeassistant/components/landisgyr_heat_meter/translations/ja.json b/homeassistant/components/landisgyr_heat_meter/translations/ja.json index e05d6d2dffa..b9ac41b8244 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/ja.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/ja.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json b/homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json index 1ac8f7b38bf..97cced694cf 100644 --- a/homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json +++ b/homeassistant/components/landisgyr_heat_meter/translations/pt-BR.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "unknown": "Erro inesperado" }, "step": { "setup_serial_manual_path": { "data": { - "device": "Caminho do dispositivo USB" + "device": "Caminho do Dispositivo USB" } }, "user": { diff --git a/homeassistant/components/landisgyr_heat_meter/translations/ru.json b/homeassistant/components/landisgyr_heat_meter/translations/ru.json new file mode 100644 index 00000000000..b4977ecdc39 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "setup_serial_manual_path": { + "data": { + "device": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + } + }, + "user": { + "data": { + "device": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/pt-BR.json b/homeassistant/components/lg_soundbar/translations/pt-BR.json index 8a2b69e069d..60e047a8acf 100644 --- a/homeassistant/components/lg_soundbar/translations/pt-BR.json +++ b/homeassistant/components/lg_soundbar/translations/pt-BR.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "existing_instance_updated": "Configura\u00e7\u00e3o existente atualizada." }, "error": { - "cannot_connect": "Falhou ao conectar" + "cannot_connect": "Falha ao conectar" }, "step": { "user": { "data": { - "host": "Host" + "host": "Nome do host" } } } diff --git a/homeassistant/components/life360/translations/pt-BR.json b/homeassistant/components/life360/translations/pt-BR.json index 13349bcef67..25e917f2578 100644 --- a/homeassistant/components/life360/translations/pt-BR.json +++ b/homeassistant/components/life360/translations/pt-BR.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "A conta j\u00e1 est\u00e1 configurada", + "already_configured": "A conta j\u00e1 foi configurada", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "reauth_successful": "Integra\u00e7\u00e3o Reautenticar", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", "unknown": "Erro inesperado" }, "create_entry": { @@ -11,7 +11,7 @@ }, "error": { "already_configured": "A conta j\u00e1 foi configurada", - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_username": "Nome de usu\u00e1rio Inv\u00e1lido", "unknown": "Erro inesperado" diff --git a/homeassistant/components/lifx/translations/pt-BR.json b/homeassistant/components/lifx/translations/pt-BR.json index ee340af857e..616f3f03cc8 100644 --- a/homeassistant/components/lifx/translations/pt-BR.json +++ b/homeassistant/components/lifx/translations/pt-BR.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede", "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { - "cannot_connect": "Falhou ao conectar" + "cannot_connect": "Falha ao conectar" }, "flow_title": "{label} ( {host} ) {serial}", "step": { @@ -24,7 +24,7 @@ }, "user": { "data": { - "host": "Host" + "host": "Nome do host" }, "description": "Se voc\u00ea deixar o host vazio, a descoberta ser\u00e1 usada para localizar dispositivos." } diff --git a/homeassistant/components/moat/translations/pt-BR.json b/homeassistant/components/moat/translations/pt-BR.json index 2067d7f9312..3f93e65c087 100644 --- a/homeassistant/components/moat/translations/pt-BR.json +++ b/homeassistant/components/moat/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede" }, "flow_title": "{name}", diff --git a/homeassistant/components/nest/translations/pt-BR.json b/homeassistant/components/nest/translations/pt-BR.json index 973a3cf3b69..9f5f9b9eff7 100644 --- a/homeassistant/components/nest/translations/pt-BR.json +++ b/homeassistant/components/nest/translations/pt-BR.json @@ -4,7 +4,7 @@ }, "config": { "abort": { - "already_configured": "Conta j\u00e1 configurada", + "already_configured": "A conta j\u00e1 foi configurada", "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", "invalid_access_token": "Token de acesso inv\u00e1lido", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", diff --git a/homeassistant/components/nextdns/translations/pt-BR.json b/homeassistant/components/nextdns/translations/pt-BR.json index 90d7cf3f31e..4eebf2c5900 100644 --- a/homeassistant/components/nextdns/translations/pt-BR.json +++ b/homeassistant/components/nextdns/translations/pt-BR.json @@ -4,7 +4,7 @@ "already_configured": "Este perfil NextDNS j\u00e1 est\u00e1 configurado." }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "invalid_api_key": "Chave de API inv\u00e1lida", "unknown": "Erro inesperado" }, @@ -16,7 +16,7 @@ }, "user": { "data": { - "api_key": "Chave de API" + "api_key": "Chave da API" } } } diff --git a/homeassistant/components/nina/translations/pt-BR.json b/homeassistant/components/nina/translations/pt-BR.json index 3da9c79f05f..774f51d044a 100644 --- a/homeassistant/components/nina/translations/pt-BR.json +++ b/homeassistant/components/nina/translations/pt-BR.json @@ -26,7 +26,7 @@ }, "options": { "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "no_selection": "Selecione pelo menos uma cidade/munic\u00edpio", "unknown": "Erro inesperado" }, diff --git a/homeassistant/components/openexchangerates/translations/pt-BR.json b/homeassistant/components/openexchangerates/translations/pt-BR.json index b7a8bc33614..7f6edf42577 100644 --- a/homeassistant/components/openexchangerates/translations/pt-BR.json +++ b/homeassistant/components/openexchangerates/translations/pt-BR.json @@ -2,20 +2,20 @@ "config": { "abort": { "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", - "timeout_connect": "Tempo limite estabelecendo conex\u00e3o" + "timeout_connect": "Tempo limite para estabelecer conex\u00e3o atingido" }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", - "timeout_connect": "Tempo limite estabelecendo conex\u00e3o", + "timeout_connect": "Tempo limite para estabelecer conex\u00e3o atingido", "unknown": "Erro inesperado" }, "step": { "user": { "data": { - "api_key": "Chave API", + "api_key": "Chave da API", "base": "Moeda base" }, "data_description": { diff --git a/homeassistant/components/opentherm_gw/translations/pt-BR.json b/homeassistant/components/opentherm_gw/translations/pt-BR.json index 3d2649aad08..a6ffea67c44 100644 --- a/homeassistant/components/opentherm_gw/translations/pt-BR.json +++ b/homeassistant/components/opentherm_gw/translations/pt-BR.json @@ -4,7 +4,7 @@ "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "cannot_connect": "Falha ao conectar", "id_exists": "ID do gateway j\u00e1 existe", - "timeout_connect": "Tempo limite estabelecendo conex\u00e3o" + "timeout_connect": "Tempo limite para estabelecer conex\u00e3o atingido" }, "step": { "init": { diff --git a/homeassistant/components/p1_monitor/translations/de.json b/homeassistant/components/p1_monitor/translations/de.json index 8ac00192251..8740c9dccbb 100644 --- a/homeassistant/components/p1_monitor/translations/de.json +++ b/homeassistant/components/p1_monitor/translations/de.json @@ -9,6 +9,9 @@ "host": "Host", "name": "Name" }, + "data_description": { + "host": "Die IP-Adresse oder der Hostname deiner P1 Monitor-Installation." + }, "description": "Richte den P1-Monitor zur Integration mit Home Assistant ein." } } diff --git a/homeassistant/components/p1_monitor/translations/en.json b/homeassistant/components/p1_monitor/translations/en.json index 394b6c0767b..4347e2d89d2 100644 --- a/homeassistant/components/p1_monitor/translations/en.json +++ b/homeassistant/components/p1_monitor/translations/en.json @@ -6,7 +6,8 @@ "step": { "user": { "data": { - "host": "Host" + "host": "Host", + "name": "Name" }, "data_description": { "host": "The IP address or hostname of your P1 Monitor installation." diff --git a/homeassistant/components/p1_monitor/translations/es.json b/homeassistant/components/p1_monitor/translations/es.json index 28dcb186505..3893952a1ec 100644 --- a/homeassistant/components/p1_monitor/translations/es.json +++ b/homeassistant/components/p1_monitor/translations/es.json @@ -9,6 +9,9 @@ "host": "Host", "name": "Nombre" }, + "data_description": { + "host": "La direcci\u00f3n IP o el nombre de host de tu instalaci\u00f3n de P1 Monitor." + }, "description": "Configura P1 Monitor para integrarlo con Home Assistant." } } diff --git a/homeassistant/components/p1_monitor/translations/fr.json b/homeassistant/components/p1_monitor/translations/fr.json index 34699234e0e..5da5fe439cd 100644 --- a/homeassistant/components/p1_monitor/translations/fr.json +++ b/homeassistant/components/p1_monitor/translations/fr.json @@ -9,6 +9,9 @@ "host": "H\u00f4te", "name": "Nom" }, + "data_description": { + "host": "L'adresse IP ou le nom d'h\u00f4te de votre installation P1 Monitor." + }, "description": "Configurez P1 Monitor pour l'int\u00e9grer \u00e0 Home Assistant." } } diff --git a/homeassistant/components/p1_monitor/translations/ja.json b/homeassistant/components/p1_monitor/translations/ja.json index 29bfd50332f..66e79ebdc0b 100644 --- a/homeassistant/components/p1_monitor/translations/ja.json +++ b/homeassistant/components/p1_monitor/translations/ja.json @@ -9,6 +9,9 @@ "host": "\u30db\u30b9\u30c8", "name": "\u540d\u524d" }, + "data_description": { + "host": "P1 Monitor\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u306e\u3001IP\u30a2\u30c9\u30ec\u30b9\u307e\u305f\u306f\u30db\u30b9\u30c8\u540d\u3002" + }, "description": "P1 Monitor\u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u3066\u3001Home Assistant\u3068\u9023\u643a\u3059\u308b\u3088\u3046\u306b\u3057\u307e\u3059\u3002" } } diff --git a/homeassistant/components/p1_monitor/translations/pt-BR.json b/homeassistant/components/p1_monitor/translations/pt-BR.json index dda88d419e6..97dfd9a0ec3 100644 --- a/homeassistant/components/p1_monitor/translations/pt-BR.json +++ b/homeassistant/components/p1_monitor/translations/pt-BR.json @@ -9,6 +9,9 @@ "host": "Nome do host", "name": "Nome" }, + "data_description": { + "host": "O endere\u00e7o IP ou o nome do host da instala\u00e7\u00e3o do P1 Monitor." + }, "description": "Configure o P1 Monitor para integrar com o Home Assistant." } } diff --git a/homeassistant/components/p1_monitor/translations/zh-Hant.json b/homeassistant/components/p1_monitor/translations/zh-Hant.json index a62ff38bbda..27e0200f1b2 100644 --- a/homeassistant/components/p1_monitor/translations/zh-Hant.json +++ b/homeassistant/components/p1_monitor/translations/zh-Hant.json @@ -9,6 +9,9 @@ "host": "\u4e3b\u6a5f\u7aef", "name": "\u540d\u7a31" }, + "data_description": { + "host": "P1 Monitor \u5b89\u88dd IP \u4f4d\u5740\u6216\u4e3b\u6a5f\u540d\u7a31\u3002" + }, "description": "\u8a2d\u5b9a P1 Monitor \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" } } diff --git a/homeassistant/components/point/translations/pt-BR.json b/homeassistant/components/point/translations/pt-BR.json index a940c67daf9..7ef07947019 100644 --- a/homeassistant/components/point/translations/pt-BR.json +++ b/homeassistant/components/point/translations/pt-BR.json @@ -4,7 +4,7 @@ "already_setup": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel.", "authorize_url_timeout": "Tempo limite gerando URL de autoriza\u00e7\u00e3o.", "external_setup": "Point configurado com \u00eaxito a partir de outro fluxo.", - "no_flows": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.\nVoc\u00ea precisa configurar o Point antes de ser capaz de autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es](https://www.home-assistant.io/components/point/).", + "no_flows": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." }, "create_entry": { diff --git a/homeassistant/components/pure_energie/translations/de.json b/homeassistant/components/pure_energie/translations/de.json index 6aafb35d5f9..a3632b93bd5 100644 --- a/homeassistant/components/pure_energie/translations/de.json +++ b/homeassistant/components/pure_energie/translations/de.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "Host" + }, + "data_description": { + "host": "Die IP-Adresse oder der Hostname deines Pure Energie Meters." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pure_energie/translations/es.json b/homeassistant/components/pure_energie/translations/es.json index 58d02574211..6bbff02d6a5 100644 --- a/homeassistant/components/pure_energie/translations/es.json +++ b/homeassistant/components/pure_energie/translations/es.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "Host" + }, + "data_description": { + "host": "La direcci\u00f3n IP o el nombre de host de tu Pure Energie Meter." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pure_energie/translations/fr.json b/homeassistant/components/pure_energie/translations/fr.json index e121c7f655c..98069d82e1f 100644 --- a/homeassistant/components/pure_energie/translations/fr.json +++ b/homeassistant/components/pure_energie/translations/fr.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "H\u00f4te" + }, + "data_description": { + "host": "L'adresse IP ou le nom d'h\u00f4te de votre compteur Pure Energie." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pure_energie/translations/ja.json b/homeassistant/components/pure_energie/translations/ja.json index bb7b3fe9f13..6558ed09594 100644 --- a/homeassistant/components/pure_energie/translations/ja.json +++ b/homeassistant/components/pure_energie/translations/ja.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "\u30db\u30b9\u30c8" + }, + "data_description": { + "host": "Pure Energie Meter\u306e\u3001IP\u30a2\u30c9\u30ec\u30b9\u307e\u305f\u306f\u30db\u30b9\u30c8\u540d\u3002" } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pure_energie/translations/pt-BR.json b/homeassistant/components/pure_energie/translations/pt-BR.json index b43c1c285ba..a2663ccc317 100644 --- a/homeassistant/components/pure_energie/translations/pt-BR.json +++ b/homeassistant/components/pure_energie/translations/pt-BR.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "Nome do host" + }, + "data_description": { + "host": "O endere\u00e7o IP ou nome de host do seu Medidor Pure Energie." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pure_energie/translations/zh-Hant.json b/homeassistant/components/pure_energie/translations/zh-Hant.json index 56235b16952..c758ee0cf55 100644 --- a/homeassistant/components/pure_energie/translations/zh-Hant.json +++ b/homeassistant/components/pure_energie/translations/zh-Hant.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "\u4e3b\u6a5f\u7aef" + }, + "data_description": { + "host": "Pure Energie Meter IP \u4f4d\u5740\u6216\u4e3b\u6a5f\u540d\u7a31\u3002" } }, "zeroconf_confirm": { diff --git a/homeassistant/components/pushover/translations/ja.json b/homeassistant/components/pushover/translations/ja.json index 344e21952dc..ab1170be1b0 100644 --- a/homeassistant/components/pushover/translations/ja.json +++ b/homeassistant/components/pushover/translations/ja.json @@ -6,6 +6,7 @@ }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "invalid_api_key": "\u7121\u52b9\u306aAPI\u30ad\u30fc", "invalid_user_key": "\u7121\u52b9\u306a\u30e6\u30fc\u30b6\u30fc\u30ad\u30fc" }, "step": { diff --git a/homeassistant/components/pushover/translations/pt-BR.json b/homeassistant/components/pushover/translations/pt-BR.json new file mode 100644 index 00000000000..60f2cb1b3b2 --- /dev/null +++ b/homeassistant/components/pushover/translations/pt-BR.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falha ao conectar", + "invalid_api_key": "Chave de API inv\u00e1lida", + "invalid_user_key": "Chave de usu\u00e1rio inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "Chave da API" + }, + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "api_key": "Chave da API", + "name": "Nome", + "user_key": "Chave do usu\u00e1rio" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Pushover usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a interface do usu\u00e1rio automaticamente. \n\n Remova a configura\u00e7\u00e3o Pushover YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o do Pushover YAML est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/translations/ru.json b/homeassistant/components/pushover/translations/ru.json new file mode 100644 index 00000000000..ae879b1fd27 --- /dev/null +++ b/homeassistant/components/pushover/translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "invalid_user_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f." + }, + "step": { + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/pt-BR.json b/homeassistant/components/qingping/translations/pt-BR.json index 0da7639fa2a..5b654163201 100644 --- a/homeassistant/components/qingping/translations/pt-BR.json +++ b/homeassistant/components/qingping/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede", "not_supported": "Dispositivo n\u00e3o suportado" }, diff --git a/homeassistant/components/qnap_qsw/translations/pt-BR.json b/homeassistant/components/qnap_qsw/translations/pt-BR.json index fe2016bdce5..e5a2b0fec96 100644 --- a/homeassistant/components/qnap_qsw/translations/pt-BR.json +++ b/homeassistant/components/qnap_qsw/translations/pt-BR.json @@ -12,7 +12,7 @@ "discovered_connection": { "data": { "password": "Senha", - "username": "Nome de usu\u00e1rio" + "username": "Usu\u00e1rio" } }, "user": { diff --git a/homeassistant/components/rhasspy/translations/pt-BR.json b/homeassistant/components/rhasspy/translations/pt-BR.json index 0e62dceb57d..c17f7891bd0 100644 --- a/homeassistant/components/rhasspy/translations/pt-BR.json +++ b/homeassistant/components/rhasspy/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "J\u00e1 foi configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + "single_instance_allowed": "J\u00e1 configurado. Apenas uma configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "step": { "user": { diff --git a/homeassistant/components/scrape/translations/cs.json b/homeassistant/components/scrape/translations/cs.json new file mode 100644 index 00000000000..8669b5b1330 --- /dev/null +++ b/homeassistant/components/scrape/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "headers": "Hlavi\u010dky" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/pt-BR.json b/homeassistant/components/sensorpush/translations/pt-BR.json index 2067d7f9312..3f93e65c087 100644 --- a/homeassistant/components/sensorpush/translations/pt-BR.json +++ b/homeassistant/components/sensorpush/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede" }, "flow_title": "{name}", diff --git a/homeassistant/components/simplepush/translations/pt-BR.json b/homeassistant/components/simplepush/translations/pt-BR.json index 90d21e1c44c..993c8d7214b 100644 --- a/homeassistant/components/simplepush/translations/pt-BR.json +++ b/homeassistant/components/simplepush/translations/pt-BR.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dispositivo j\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { "cannot_connect": "Falha ao conectar" diff --git a/homeassistant/components/skybell/translations/pt-BR.json b/homeassistant/components/skybell/translations/pt-BR.json index 9fed8c0da02..6ebac606d58 100644 --- a/homeassistant/components/skybell/translations/pt-BR.json +++ b/homeassistant/components/skybell/translations/pt-BR.json @@ -17,5 +17,11 @@ } } } + }, + "issues": { + "removed_yaml": { + "description": "A configura\u00e7\u00e3o do Skybell usando YAML foi removida. \n\n Sua configura\u00e7\u00e3o YAML existente n\u00e3o \u00e9 usada pelo Home Assistant. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML do Skybell foi removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/pt-BR.json b/homeassistant/components/soundtouch/translations/pt-BR.json index 7446cbc5a06..146e5352614 100644 --- a/homeassistant/components/soundtouch/translations/pt-BR.json +++ b/homeassistant/components/soundtouch/translations/pt-BR.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { - "cannot_connect": "Falhou ao conectar" + "cannot_connect": "Falha ao conectar" }, "step": { "user": { "data": { - "host": "Host" + "host": "Nome do host" } }, "zeroconf_confirm": { diff --git a/homeassistant/components/transmission/translations/pt-BR.json b/homeassistant/components/transmission/translations/pt-BR.json index 781d21e2900..5579b64e2d9 100644 --- a/homeassistant/components/transmission/translations/pt-BR.json +++ b/homeassistant/components/transmission/translations/pt-BR.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", - "reauth_successful": "Re-autenticado com sucesso" + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "cannot_connect": "Falha ao conectar", @@ -15,7 +15,7 @@ "password": "Senha" }, "description": "A senha para o usu\u00e1rio {username} est\u00e1 inv\u00e1lida.", - "title": "Re-autenticar integra\u00e7\u00e3o" + "title": "Reautenticar Integra\u00e7\u00e3o" }, "user": { "data": { diff --git a/homeassistant/components/tuya/translations/select.cs.json b/homeassistant/components/tuya/translations/select.cs.json new file mode 100644 index 00000000000..c1b2e3b1516 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.cs.json @@ -0,0 +1,7 @@ +{ + "state": { + "tuya__humidifier_spray_mode": { + "humidity": "Vlhkost" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/cs.json b/homeassistant/components/unifi/translations/cs.json index 0281dfbb750..981bb926d3a 100644 --- a/homeassistant/components/unifi/translations/cs.json +++ b/homeassistant/components/unifi/translations/cs.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ovlada\u010d je ji\u017e nastaven", + "already_configured": "Um\u00edst\u011bn\u00ed Unifi Network je ji\u017e nastaveno", + "configuration_updated": "Nastaven\u00ed aktualizov\u00e1no", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { @@ -9,7 +10,7 @@ "service_unavailable": "Nepoda\u0159ilo se p\u0159ipojit", "unknown_client_mac": "Na t\u00e9to MAC adrese nen\u00ed dostupn\u00fd \u017e\u00e1dn\u00fd klient" }, - "flow_title": "UniFi s\u00ed\u0165 {site} ({host})", + "flow_title": "{site} ({host})", "step": { "user": { "data": { @@ -20,11 +21,14 @@ "username": "U\u017eivatelsk\u00e9 jm\u00e9no", "verify_ssl": "Ov\u011b\u0159it certifik\u00e1t SSL" }, - "title": "Nastaven\u00ed UniFi ovlada\u010de" + "title": "Nastaven\u00ed UniFi Network" } } }, "options": { + "abort": { + "integration_not_setup": "Integrace UniFi nen\u00ed nastavena" + }, "step": { "client_control": { "data": { @@ -33,7 +37,7 @@ "poe_clients": "Povolit u klient\u016f ovl\u00e1d\u00e1n\u00ed POE" }, "description": "Nakonfigurujte ovl\u00e1dac\u00ed prvky klienta\n\nVytvo\u0159te vyp\u00edna\u010de pro s\u00e9riov\u00e1 \u010d\u00edsla, pro kter\u00e1 chcete \u0159\u00eddit p\u0159\u00edstup k s\u00edti.", - "title": "Mo\u017enosti UniFi 2/3" + "title": "Mo\u017enosti UniFi Network 2/3" }, "device_tracker": { "data": { @@ -44,7 +48,7 @@ "track_wired_clients": "V\u010detn\u011b klient\u016f p\u0159ipojen\u00fdch kabelem" }, "description": "Konfigurace sledov\u00e1n\u00ed za\u0159\u00edzen\u00ed", - "title": "Mo\u017enosti UniFi 1/3" + "title": "Mo\u017enosti UniFi Network 1/3" }, "simple_options": { "data": { @@ -52,7 +56,7 @@ "track_clients": "Sledov\u00e1n\u00ed p\u0159ipojen\u00fdch za\u0159\u00edzen\u00ed", "track_devices": "Sledov\u00e1n\u00ed s\u00ed\u0165ov\u00fdch za\u0159\u00edzen\u00ed (za\u0159\u00edzen\u00ed Ubiquiti)" }, - "description": "Konfigurace integrace UniFi" + "description": "Nastaven\u00ed integrace UniFi Network" }, "statistics_sensors": { "data": { @@ -60,7 +64,7 @@ "allow_uptime_sensors": "Vytvo\u0159it senzory doby provozuschopnosti pro s\u00ed\u0165ov\u00e9 klienty" }, "description": "Konfigurovat statistick\u00e9 senzory", - "title": "Mo\u017enosti UniFi 3/3" + "title": "Mo\u017enosti UniFi Network 3/3" } } } diff --git a/homeassistant/components/uptime/translations/cs.json b/homeassistant/components/uptime/translations/cs.json index ce19e127348..882fc5f4598 100644 --- a/homeassistant/components/uptime/translations/cs.json +++ b/homeassistant/components/uptime/translations/cs.json @@ -8,5 +8,6 @@ "description": "Chcete za\u010d\u00edt nastavovat?" } } - } + }, + "title": "Uptime" } \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/cs.json b/homeassistant/components/wallbox/translations/cs.json index 72df4a96818..5bc59de6051 100644 --- a/homeassistant/components/wallbox/translations/cs.json +++ b/homeassistant/components/wallbox/translations/cs.json @@ -6,6 +6,13 @@ "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "reauth_confirm": { + "data": { + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/pt-BR.json b/homeassistant/components/xiaomi_ble/translations/pt-BR.json index 4702ca14bcc..a21c3e1dd9c 100644 --- a/homeassistant/components/xiaomi_ble/translations/pt-BR.json +++ b/homeassistant/components/xiaomi_ble/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "decryption_failed": "A bindkey fornecida n\u00e3o funcionou, os dados do sensor n\u00e3o puderam ser descriptografados. Por favor verifique e tente novamente.", "expected_24_characters": "Espera-se uma bindkey hexadecimal de 24 caracteres.", "expected_32_characters": "Esperado um bindkey hexadecimal de 32 caracteres.", diff --git a/homeassistant/components/yalexs_ble/translations/pt-BR.json b/homeassistant/components/yalexs_ble/translations/pt-BR.json index 19467e26e36..c53e0d24567 100644 --- a/homeassistant/components/yalexs_ble/translations/pt-BR.json +++ b/homeassistant/components/yalexs_ble/translations/pt-BR.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "no_devices_found": "Nenhum dispositivo encontrado na rede", "no_unconfigured_devices": "Nenhum dispositivo n\u00e3o configurado encontrado." }, "error": { - "cannot_connect": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "invalid_key_format": "A chave offline deve ser uma string hexadecimal de 32 bytes.", "invalid_key_index": "O slot de chave offline deve ser um n\u00famero inteiro entre 0 e 255.", diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index 69b8ced6970..35936dbe60b 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -13,6 +13,9 @@ "confirm": { "description": "Voc\u00ea deseja configurar {name}?" }, + "confirm_hardware": { + "description": "Deseja configurar {name}?" + }, "pick_radio": { "data": { "radio_type": "Tipo de hub zigbee" From 9edb25887cb8888166fbd784f58b9a8fe75b0a96 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 20 Aug 2022 15:08:35 -1000 Subject: [PATCH 3514/3516] Bump yalexs_ble to 1.6.4 (#77080) --- homeassistant/components/yalexs_ble/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index b4e2f78906e..de0034c755f 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,7 +3,7 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.6.2"], + "requirements": ["yalexs-ble==1.6.4"], "dependencies": ["bluetooth"], "codeowners": ["@bdraco"], "bluetooth": [{ "manufacturer_id": 465 }], diff --git a/requirements_all.txt b/requirements_all.txt index 89ac7e7d77b..452f0f47ef2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2512,7 +2512,7 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.6.2 +yalexs-ble==1.6.4 # homeassistant.components.august yalexs==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9504c1bd1ae..78d547f75ca 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1713,7 +1713,7 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.8 # homeassistant.components.yalexs_ble -yalexs-ble==1.6.2 +yalexs-ble==1.6.4 # homeassistant.components.august yalexs==1.2.1 From 2689eddbe84adfb40cba25cdce72cdc9ec39afba Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 21 Aug 2022 09:47:04 +0200 Subject: [PATCH 3515/3516] =?UTF-8?q?Make=20sure=20we=20always=20connect?= =?UTF-8?q?=20to=20last=20known=20bluetooth=20device=20in=20fj=C3=A4r?= =?UTF-8?q?=C3=A5skupan=20(#77088)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make sure we always connect to last known device --- .../components/fjaraskupan/__init__.py | 15 ++++++++- homeassistant/components/fjaraskupan/fan.py | 31 ++++++++----------- homeassistant/components/fjaraskupan/light.py | 24 +++++++------- .../components/fjaraskupan/number.py | 14 +++------ 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 28032b3f997..75086c631a1 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -1,7 +1,8 @@ """The Fjäråskupan integration.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import AsyncIterator, Callable +from contextlib import asynccontextmanager from dataclasses import dataclass from datetime import timedelta import logging @@ -14,6 +15,7 @@ from homeassistant.components.bluetooth import ( BluetoothScanningMode, BluetoothServiceInfoBleak, async_address_present, + async_ble_device_from_address, async_rediscover_address, async_register_callback, ) @@ -84,9 +86,20 @@ class Coordinator(DataUpdateCoordinator[State]): def detection_callback(self, service_info: BluetoothServiceInfoBleak) -> None: """Handle a new announcement of data.""" + self.device.device = service_info.device self.device.detection_callback(service_info.device, service_info.advertisement) self.async_set_updated_data(self.device.state) + @asynccontextmanager + async def async_connect_and_update(self) -> AsyncIterator[Device]: + """Provide an up to date device for use during connections.""" + if ble_device := async_ble_device_from_address(self.hass, self.device.address): + self.device.device = ble_device + async with self.device: + yield self.device + + self.async_set_updated_data(self.device.state) + @dataclass class EntryState: diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index 3642438d5d6..e0c18158088 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -8,7 +8,6 @@ from fjaraskupan import ( COMMAND_AFTERCOOKINGTIMERMANUAL, COMMAND_AFTERCOOKINGTIMEROFF, COMMAND_STOP_FAN, - Device, State, ) @@ -58,7 +57,7 @@ async def async_setup_entry( """Set up sensors dynamically through discovery.""" def _constructor(coordinator: Coordinator): - return [Fan(coordinator, coordinator.device, coordinator.device_info)] + return [Fan(coordinator, coordinator.device_info)] async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) @@ -72,14 +71,12 @@ class Fan(CoordinatorEntity[Coordinator], FanEntity): def __init__( self, coordinator: Coordinator, - device: Device, device_info: DeviceInfo, ) -> None: """Init fan entity.""" super().__init__(coordinator) - self._device = device self._default_on_speed = 25 - self._attr_unique_id = device.address + self._attr_unique_id = coordinator.device.address self._attr_device_info = device_info self._percentage = 0 self._preset_mode = PRESET_MODE_NORMAL @@ -90,8 +87,8 @@ class Fan(CoordinatorEntity[Coordinator], FanEntity): new_speed = percentage_to_ordered_list_item( ORDERED_NAMED_FAN_SPEEDS, percentage ) - await self._device.send_fan_speed(int(new_speed)) - self.coordinator.async_set_updated_data(self._device.state) + async with self.coordinator.async_connect_and_update() as device: + await device.send_fan_speed(int(new_speed)) async def async_turn_on( self, @@ -111,34 +108,32 @@ class Fan(CoordinatorEntity[Coordinator], FanEntity): ORDERED_NAMED_FAN_SPEEDS, percentage ) - async with self._device: + async with self.coordinator.async_connect_and_update() as device: if preset_mode != self._preset_mode: if command := PRESET_TO_COMMAND.get(preset_mode): - await self._device.send_command(command) + await device.send_command(command) else: raise UnsupportedPreset(f"The preset {preset_mode} is unsupported") if preset_mode == PRESET_MODE_NORMAL: - await self._device.send_fan_speed(int(new_speed)) + await device.send_fan_speed(int(new_speed)) elif preset_mode == PRESET_MODE_AFTER_COOKING_MANUAL: - await self._device.send_after_cooking(int(new_speed)) + await device.send_after_cooking(int(new_speed)) elif preset_mode == PRESET_MODE_AFTER_COOKING_AUTO: - await self._device.send_after_cooking(0) - - self.coordinator.async_set_updated_data(self._device.state) + await device.send_after_cooking(0) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if command := PRESET_TO_COMMAND.get(preset_mode): - await self._device.send_command(command) - self.coordinator.async_set_updated_data(self._device.state) + async with self.coordinator.async_connect_and_update() as device: + await device.send_command(command) else: raise UnsupportedPreset(f"The preset {preset_mode} is unsupported") async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - await self._device.send_command(COMMAND_STOP_FAN) - self.coordinator.async_set_updated_data(self._device.state) + async with self.coordinator.async_connect_and_update() as device: + await device.send_command(COMMAND_STOP_FAN) @property def speed_count(self) -> int: diff --git a/homeassistant/components/fjaraskupan/light.py b/homeassistant/components/fjaraskupan/light.py index c42943710de..b6028e017d4 100644 --- a/homeassistant/components/fjaraskupan/light.py +++ b/homeassistant/components/fjaraskupan/light.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from fjaraskupan import COMMAND_LIGHT_ON_OFF, Device +from fjaraskupan import COMMAND_LIGHT_ON_OFF from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry @@ -23,7 +23,7 @@ async def async_setup_entry( """Set up tuya sensors dynamically through tuya discovery.""" def _constructor(coordinator: Coordinator) -> list[Entity]: - return [Light(coordinator, coordinator.device, coordinator.device_info)] + return [Light(coordinator, coordinator.device_info)] async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) @@ -36,31 +36,29 @@ class Light(CoordinatorEntity[Coordinator], LightEntity): def __init__( self, coordinator: Coordinator, - device: Device, device_info: DeviceInfo, ) -> None: """Init light entity.""" super().__init__(coordinator) - self._device = device self._attr_color_mode = ColorMode.BRIGHTNESS self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} - self._attr_unique_id = device.address + self._attr_unique_id = coordinator.device.address self._attr_device_info = device_info async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" - if ATTR_BRIGHTNESS in kwargs: - await self._device.send_dim(int(kwargs[ATTR_BRIGHTNESS] * (100.0 / 255.0))) - else: - if not self.is_on: - await self._device.send_command(COMMAND_LIGHT_ON_OFF) - self.coordinator.async_set_updated_data(self._device.state) + async with self.coordinator.async_connect_and_update() as device: + if ATTR_BRIGHTNESS in kwargs: + await device.send_dim(int(kwargs[ATTR_BRIGHTNESS] * (100.0 / 255.0))) + else: + if not self.is_on: + await device.send_command(COMMAND_LIGHT_ON_OFF) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" if self.is_on: - await self._device.send_command(COMMAND_LIGHT_ON_OFF) - self.coordinator.async_set_updated_data(self._device.state) + async with self.coordinator.async_connect_and_update() as device: + await device.send_command(COMMAND_LIGHT_ON_OFF) @property def is_on(self) -> bool: diff --git a/homeassistant/components/fjaraskupan/number.py b/homeassistant/components/fjaraskupan/number.py index fb793d2328e..d70f9b6a423 100644 --- a/homeassistant/components/fjaraskupan/number.py +++ b/homeassistant/components/fjaraskupan/number.py @@ -1,8 +1,6 @@ """Support for sensors.""" from __future__ import annotations -from fjaraskupan import Device - from homeassistant.components.number import NumberEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import TIME_MINUTES @@ -23,9 +21,7 @@ async def async_setup_entry( def _constructor(coordinator: Coordinator) -> list[Entity]: return [ - PeriodicVentingTime( - coordinator, coordinator.device, coordinator.device_info - ), + PeriodicVentingTime(coordinator, coordinator.device_info), ] async_setup_entry_platform(hass, config_entry, async_add_entities, _constructor) @@ -45,13 +41,11 @@ class PeriodicVentingTime(CoordinatorEntity[Coordinator], NumberEntity): def __init__( self, coordinator: Coordinator, - device: Device, device_info: DeviceInfo, ) -> None: """Init number entities.""" super().__init__(coordinator) - self._device = device - self._attr_unique_id = f"{device.address}-periodic-venting" + self._attr_unique_id = f"{coordinator.device.address}-periodic-venting" self._attr_device_info = device_info self._attr_name = "Periodic venting" @@ -64,5 +58,5 @@ class PeriodicVentingTime(CoordinatorEntity[Coordinator], NumberEntity): async def async_set_native_value(self, value: float) -> None: """Set new value.""" - await self._device.send_periodic_venting(int(value)) - self.coordinator.async_set_updated_data(self._device.state) + async with self.coordinator.async_connect_and_update() as device: + await device.send_periodic_venting(int(value)) From 4dad24bc51d74836613945e1ad752ce34e053ea2 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 21 Aug 2022 09:48:23 +0200 Subject: [PATCH 3516/3516] Don't check for periodic ventilation in fan control (#77089) Don't check for periodic ventilation --- homeassistant/components/fjaraskupan/fan.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/homeassistant/components/fjaraskupan/fan.py b/homeassistant/components/fjaraskupan/fan.py index e0c18158088..c037966f0ef 100644 --- a/homeassistant/components/fjaraskupan/fan.py +++ b/homeassistant/components/fjaraskupan/fan.py @@ -30,12 +30,10 @@ ORDERED_NAMED_FAN_SPEEDS = ["1", "2", "3", "4", "5", "6", "7", "8"] PRESET_MODE_NORMAL = "normal" PRESET_MODE_AFTER_COOKING_MANUAL = "after_cooking_manual" PRESET_MODE_AFTER_COOKING_AUTO = "after_cooking_auto" -PRESET_MODE_PERIODIC_VENTILATION = "periodic_ventilation" PRESET_MODES = [ PRESET_MODE_NORMAL, PRESET_MODE_AFTER_COOKING_AUTO, PRESET_MODE_AFTER_COOKING_MANUAL, - PRESET_MODE_PERIODIC_VENTILATION, ] PRESET_TO_COMMAND = { @@ -178,8 +176,6 @@ class Fan(CoordinatorEntity[Coordinator], FanEntity): self._preset_mode = PRESET_MODE_AFTER_COOKING_MANUAL else: self._preset_mode = PRESET_MODE_AFTER_COOKING_AUTO - elif data.periodic_venting_on: - self._preset_mode = PRESET_MODE_PERIODIC_VENTILATION else: self._preset_mode = PRESET_MODE_NORMAL